reconciliationPartner.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. <template>
  2. <div class="reconciliation-partner-page" style="height: 100%">
  3. <AiCrudPage
  4. ref="crudRef"
  5. :api-config="{
  6. list: 'get@/hotel/reconciliation/partner/page',
  7. export: 'post@/api/excel/export/hotel_reconciliation_export',
  8. }"
  9. :search-schema="searchSchema"
  10. :columns="tableColumns"
  11. row-key="id"
  12. :hide-add="true"
  13. :hide-batch-delete="true"
  14. :hide-selection="true"
  15. :show-import="false"
  16. :show-export="true"
  17. export-button-text="导出对账单"
  18. export-file-name="对账单列表(餐厅).xlsx"
  19. :before-load-list="transformSearchParams"
  20. />
  21. <!-- 详情抽屉 -->
  22. <NDrawer v-model:show="detailVisible" :width="800" placement="right">
  23. <NDrawerContent :title="`对账单详情 - ${detail.reconciliationNo || ''}`" closable>
  24. <!-- 汇总信息 -->
  25. <NDescriptions label-placement="left" bordered :column="2" size="small">
  26. <NDescriptionsItem label="对账单编号">
  27. {{ detail.reconciliationNo }}
  28. </NDescriptionsItem>
  29. <NDescriptionsItem label="状态">
  30. <DictTag dict-type="hotel_reconciliation_status" :value="String(detail.status)" size="small" />
  31. </NDescriptionsItem>
  32. <NDescriptionsItem label="对账周期">
  33. {{ formatDate(detail.periodStart) }} ~ {{ formatDate(detail.periodEnd) }}
  34. </NDescriptionsItem>
  35. <NDescriptionsItem label="订单笔数">
  36. {{ detail.orderCount }} 笔
  37. </NDescriptionsItem>
  38. <NDescriptionsItem label="订单总额">
  39. <span style="font-weight: 600">¥{{ detail.totalAmount }}</span>
  40. </NDescriptionsItem>
  41. <NDescriptionsItem label="排除笔数/金额">
  42. <span style="color: #e03e3e">{{ detail.excludedCount }} 笔 / ¥{{ detail.excludedAmount }}</span>
  43. </NDescriptionsItem>
  44. <NDescriptionsItem label="应付金额">
  45. <span style="font-weight: 700; color: #18a058; font-size: 16px">¥{{ detail.settlementAmount }}</span>
  46. </NDescriptionsItem>
  47. <NDescriptionsItem label="创建时间">
  48. {{ detail.createTime }}
  49. </NDescriptionsItem>
  50. <NDescriptionsItem v-if="detail.confirmedTime" label="确认时间">
  51. {{ formatDate(detail.confirmedTime) }}
  52. </NDescriptionsItem>
  53. <NDescriptionsItem v-if="detail.settledTime" label="结算时间">
  54. {{ formatDate(detail.settledTime) }}
  55. </NDescriptionsItem>
  56. <NDescriptionsItem v-if="detail.settledRemark" label="结算备注" :span="2">
  57. {{ detail.settledRemark }}
  58. </NDescriptionsItem>
  59. </NDescriptions>
  60. <!-- 操作按钮 -->
  61. <div v-if="detail.id && detail.status === 1" style="margin: 16px 0">
  62. <NButton type="primary" size="small" @click="handleConfirm(detail.id)">
  63. 确认对账单(金额无误)
  64. </NButton>
  65. </div>
  66. <!-- 明细列表 -->
  67. <div style="margin-top: 8px; margin-bottom: 8px; font-weight: 600; font-size: 14px">
  68. 订单明细
  69. </div>
  70. <NDataTable
  71. :columns="detailColumns"
  72. :data="detail.items"
  73. :row-key="row => row.id"
  74. size="small"
  75. striped
  76. :bordered="false"
  77. max-height="500"
  78. />
  79. <template #footer>
  80. <NSpace justify="end">
  81. <NButton @click="detailVisible = false">
  82. 关闭
  83. </NButton>
  84. </NSpace>
  85. </template>
  86. </NDrawerContent>
  87. </NDrawer>
  88. <!-- 订单详情弹窗 -->
  89. <NModal
  90. v-model:show="orderDetailVisible"
  91. title="订单详情"
  92. preset="card"
  93. style="width: 640px; max-width: 95vw"
  94. :bordered="false"
  95. :segmented="{ content: true }"
  96. >
  97. <div v-if="orderDetail">
  98. <NDescriptions label-placement="left" bordered :column="2" size="small">
  99. <NDescriptionsItem label="订单号">
  100. {{ orderDetail.orderNo }}
  101. </NDescriptionsItem>
  102. <NDescriptionsItem label="房间号">
  103. {{ orderDetail.roomNo }}
  104. </NDescriptionsItem>
  105. <NDescriptionsItem label="房型">
  106. {{ orderDetail.roomType || '-' }}
  107. </NDescriptionsItem>
  108. <NDescriptionsItem label="联系人">
  109. {{ orderDetail.contactName || '-' }}
  110. </NDescriptionsItem>
  111. <NDescriptionsItem label="订单状态">
  112. <DictTag dict-type="hotel_order_status" :value="String(orderDetail.status)" size="small" />
  113. </NDescriptionsItem>
  114. <NDescriptionsItem label="支付状态">
  115. <span v-if="orderDetail.payStatus === 1" style="color:#18a058">已支付</span>
  116. <span v-else style="color:#999">未支付</span>
  117. </NDescriptionsItem>
  118. <NDescriptionsItem label="退款状态">
  119. <span v-if="orderDetail.refundStatus === 1" style="color:#18a058">已退款</span>
  120. <span v-else-if="orderDetail.refundStatus === 2" style="color:#e03e3e">退款失败</span>
  121. <span v-else>未退款</span>
  122. </NDescriptionsItem>
  123. <NDescriptionsItem label="下单时间">
  124. {{ orderDetail.createTime }}
  125. </NDescriptionsItem>
  126. <NDescriptionsItem label="订单总额" :span="2">
  127. <span style="font-weight:600">¥{{ orderDetail.totalAmount }}</span>
  128. <span v-if="orderDetail.paidAmount" style="margin-left:12px;color:#18a058">实付 ¥{{ orderDetail.paidAmount }}</span>
  129. </NDescriptionsItem>
  130. <NDescriptionsItem v-if="orderDetail.note" label="备注" :span="2">
  131. {{ orderDetail.note }}
  132. </NDescriptionsItem>
  133. </NDescriptions>
  134. <div v-if="orderDetail.items && orderDetail.items.length" style="margin-top:16px">
  135. <div style="font-weight:600;font-size:14px;margin-bottom:8px">
  136. 菜品明细
  137. </div>
  138. <NDataTable :columns="orderItemColumns" :data="orderDetail.items" :row-key="row => row.id" size="small" striped :bordered="false" :pagination="false" />
  139. </div>
  140. </div>
  141. </NModal>
  142. </div>
  143. </template>
  144. <script setup>
  145. import {
  146. NButton,
  147. NDataTable,
  148. NDescriptions,
  149. NDescriptionsItem,
  150. NDrawer,
  151. NDrawerContent,
  152. NModal,
  153. NSpace,
  154. useMessage,
  155. } from 'naive-ui'
  156. import { computed, h, ref } from 'vue'
  157. import {
  158. confirmReconciliation,
  159. getOrderDetail,
  160. getPartnerReconciliationDetail,
  161. } from '@/api/hotel'
  162. import { AiCrudPage } from '@/components/ai-form'
  163. import DictTag from '@/components/DictTag.vue'
  164. import { useDict } from '@/composables/useDict'
  165. defineOptions({ name: 'HotelReconciliationPartner' })
  166. const message = useMessage()
  167. const { dict } = useDict('hotel_reconciliation_status', 'hotel_order_status')
  168. const crudRef = ref(null)
  169. // ==================== 搜索配置 ====================
  170. const statusOptions = computed(() => {
  171. const list = dict.value.hotel_reconciliation_status || []
  172. // 餐厅端只能看到已确认(1)和已结算(2)
  173. return list
  174. .filter(item => Number(item.value) >= 1)
  175. .map(item => ({ label: item.label, value: Number(item.value) }))
  176. })
  177. const searchSchema = computed(() => [
  178. {
  179. field: 'reconciliationNo',
  180. label: '对账单编号',
  181. type: 'input',
  182. props: { placeholder: '请输入编号' },
  183. },
  184. {
  185. field: 'yearMonth',
  186. label: '对账月份',
  187. type: 'month',
  188. props: { valueFormat: 'yyyy-MM', placeholder: '请选择月份' },
  189. },
  190. {
  191. field: 'status',
  192. label: '状态',
  193. type: 'select',
  194. props: { placeholder: '请选择状态', options: statusOptions.value },
  195. },
  196. ])
  197. function transformSearchParams(params) {
  198. // yearMonth 字段直接由 month picker 以 "yyyy-MM" 格式返回,无需转换
  199. return params
  200. }
  201. // ==================== 表格列配置 ====================
  202. const tableColumns = computed(() => [
  203. {
  204. prop: 'reconciliationNo',
  205. label: '对账单编号',
  206. width: 170,
  207. render: row => h('a', {
  208. class: 'text-primary cursor-pointer hover:opacity-80',
  209. onClick: () => openDetail(row.id),
  210. }, row.reconciliationNo),
  211. },
  212. {
  213. prop: 'periodStart',
  214. label: '对账周期',
  215. width: 200,
  216. render: row => `${formatDate(row.periodStart)} ~ ${formatDate(row.periodEnd)}`,
  217. },
  218. { prop: 'orderCount', label: '订单数', width: 80, align: 'right' },
  219. {
  220. prop: 'totalAmount',
  221. label: '总额(元)',
  222. width: 110,
  223. align: 'right',
  224. render: row => `¥${row.totalAmount}`,
  225. },
  226. {
  227. prop: 'excludedCount',
  228. label: '排除',
  229. width: 100,
  230. align: 'right',
  231. render: row => row.excludedCount > 0 ? `${row.excludedCount}笔/¥${row.excludedAmount}` : '0',
  232. },
  233. {
  234. prop: 'settlementAmount',
  235. label: '应付(元)',
  236. width: 120,
  237. align: 'right',
  238. render: row => h('span', { style: 'font-weight:600;color:#18a058' }, `¥${row.settlementAmount}`),
  239. },
  240. {
  241. prop: 'status',
  242. label: '状态',
  243. width: 90,
  244. render: row => h(DictTag, { dictType: 'hotel_reconciliation_status', value: String(row.status), size: 'small' }),
  245. },
  246. { prop: 'createTime', label: '创建时间', width: 170 },
  247. {
  248. label: '操作',
  249. width: 140,
  250. fixed: 'right',
  251. render: (row) => {
  252. const actions = []
  253. if (row.status === 1) {
  254. actions.push(h('a', {
  255. class: 'text-success cursor-pointer hover:opacity-80',
  256. onClick: () => handleConfirm(row.id),
  257. }, '确认'))
  258. }
  259. return h('div', { class: 'flex gap-2' }, actions)
  260. },
  261. },
  262. ])
  263. // ==================== 详情抽屉 ====================
  264. const detailVisible = ref(false)
  265. const detail = ref({})
  266. async function openDetail(id) {
  267. try {
  268. const res = await getPartnerReconciliationDetail(id)
  269. if (res.code === 200 && res.data) {
  270. detail.value = res.data
  271. detailVisible.value = true
  272. }
  273. }
  274. catch (e) {
  275. message.error(e.message || '加载详情失败')
  276. }
  277. }
  278. // 明细表格列(只读,无操作列)
  279. const detailColumns = [
  280. {
  281. title: '序号',
  282. key: 'index',
  283. width: 55,
  284. render: (row, rowIndex) => rowIndex + 1,
  285. },
  286. {
  287. title: '订单号',
  288. key: 'orderNo',
  289. width: 160,
  290. render: row => h('a', {
  291. class: 'text-primary cursor-pointer hover:opacity-80',
  292. onClick: () => openOrderDetail(row.orderId),
  293. }, row.orderNo),
  294. },
  295. { title: '房间号', key: 'roomNo', width: 80 },
  296. { title: '联系人', key: 'contactName', width: 90 },
  297. {
  298. title: '实付金额',
  299. key: 'payAmount',
  300. width: 100,
  301. align: 'right',
  302. render: row => `¥${row.payAmount}`,
  303. },
  304. {
  305. title: '退款状态',
  306. key: 'refundStatus',
  307. width: 90,
  308. render: (row) => {
  309. if (row.refundStatus === 0 || row.refundStatus == null)
  310. return '未退款'
  311. if (row.refundStatus === 1)
  312. return h('span', { style: 'color:#18a058' }, '已退款')
  313. if (row.refundStatus === 2)
  314. return h('span', { style: 'color:#e03e3e' }, '退款失败')
  315. return '-'
  316. },
  317. },
  318. {
  319. title: '排除',
  320. key: 'excluded',
  321. width: 80,
  322. render: row => row.excluded === 1
  323. ? h('span', { style: 'color:#e03e3e;font-weight:600' }, '已排除')
  324. : '否',
  325. },
  326. ]
  327. // ==================== 订单详情弹窗 ====================
  328. const orderDetailVisible = ref(false)
  329. const orderDetail = ref(null)
  330. const orderItemColumns = [
  331. { title: '菜品', key: 'dishName', width: 160 },
  332. { title: '规格', key: 'specDesc', width: 120, render: row => row.specDesc || '-' },
  333. { title: '数量', key: 'quantity', width: 60, align: 'right' },
  334. { title: '单价', key: 'unitPrice', width: 80, align: 'right', render: row => `¥${row.unitPrice || '0.00'}` },
  335. { title: '小计', key: 'subtotal', width: 80, align: 'right', render: row => `¥${row.subtotal || '0.00'}` },
  336. ]
  337. async function openOrderDetail(orderId) {
  338. try {
  339. const res = await getOrderDetail(orderId)
  340. if (res.code === 200 && res.data) {
  341. orderDetail.value = res.data
  342. orderDetailVisible.value = true
  343. }
  344. }
  345. catch (e) {
  346. message.error(e.message || '加载订单详情失败')
  347. }
  348. }
  349. // ==================== 确认对账单 ====================
  350. async function handleConfirm(id) {
  351. const dialog = window.$dialog
  352. if (!dialog)
  353. return
  354. dialog.warning({
  355. title: '确认对账单',
  356. content: '确认该对账单金额无误?确认后不可修改。',
  357. positiveText: '确认无误',
  358. negativeText: '再想想',
  359. onPositiveClick: async () => {
  360. try {
  361. await confirmReconciliation(id)
  362. message.success('已确认对账单')
  363. crudRef.value?.refresh()
  364. if (detailVisible.value) {
  365. const res = await getPartnerReconciliationDetail(id)
  366. if (res.code === 200 && res.data) {
  367. detail.value = res.data
  368. }
  369. }
  370. }
  371. catch (e) {
  372. message.error(e.message || '确认失败')
  373. }
  374. },
  375. })
  376. }
  377. // ==================== 工具方法 ====================
  378. function formatDate(val) {
  379. if (!val)
  380. return ''
  381. if (typeof val === 'string')
  382. return val.substring(0, 10)
  383. const d = new Date(val)
  384. return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
  385. }
  386. </script>
  387. <style scoped>
  388. .reconciliation-partner-page {
  389. padding: 16px;
  390. }
  391. </style>