| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413 |
- <template>
- <div class="reconciliation-partner-page" style="height: 100%">
- <AiCrudPage
- ref="crudRef"
- :api-config="{
- list: 'get@/hotel/reconciliation/partner/page',
- export: 'post@/api/excel/export/hotel_reconciliation_export',
- }"
- :search-schema="searchSchema"
- :columns="tableColumns"
- row-key="id"
- :hide-add="true"
- :hide-batch-delete="true"
- :hide-selection="true"
- :show-import="false"
- :show-export="true"
- export-button-text="导出对账单"
- export-file-name="对账单列表(餐厅).xlsx"
- :before-load-list="transformSearchParams"
- />
- <!-- 详情抽屉 -->
- <NDrawer v-model:show="detailVisible" :width="800" placement="right">
- <NDrawerContent :title="`对账单详情 - ${detail.reconciliationNo || ''}`" closable>
- <!-- 汇总信息 -->
- <NDescriptions label-placement="left" bordered :column="2" size="small">
- <NDescriptionsItem label="对账单编号">
- {{ detail.reconciliationNo }}
- </NDescriptionsItem>
- <NDescriptionsItem label="状态">
- <DictTag dict-type="hotel_reconciliation_status" :value="String(detail.status)" size="small" />
- </NDescriptionsItem>
- <NDescriptionsItem label="对账周期">
- {{ formatDate(detail.periodStart) }} ~ {{ formatDate(detail.periodEnd) }}
- </NDescriptionsItem>
- <NDescriptionsItem label="订单笔数">
- {{ detail.orderCount }} 笔
- </NDescriptionsItem>
- <NDescriptionsItem label="订单总额">
- <span style="font-weight: 600">¥{{ detail.totalAmount }}</span>
- </NDescriptionsItem>
- <NDescriptionsItem label="排除笔数/金额">
- <span style="color: #e03e3e">{{ detail.excludedCount }} 笔 / ¥{{ detail.excludedAmount }}</span>
- </NDescriptionsItem>
- <NDescriptionsItem label="应付金额">
- <span style="font-weight: 700; color: #18a058; font-size: 16px">¥{{ detail.settlementAmount }}</span>
- </NDescriptionsItem>
- <NDescriptionsItem label="创建时间">
- {{ detail.createTime }}
- </NDescriptionsItem>
- <NDescriptionsItem v-if="detail.confirmedTime" label="确认时间">
- {{ formatDate(detail.confirmedTime) }}
- </NDescriptionsItem>
- <NDescriptionsItem v-if="detail.settledTime" label="结算时间">
- {{ formatDate(detail.settledTime) }}
- </NDescriptionsItem>
- <NDescriptionsItem v-if="detail.settledRemark" label="结算备注" :span="2">
- {{ detail.settledRemark }}
- </NDescriptionsItem>
- </NDescriptions>
- <!-- 操作按钮 -->
- <div v-if="detail.id && detail.status === 1" style="margin: 16px 0">
- <NButton type="primary" size="small" @click="handleConfirm(detail.id)">
- 确认对账单(金额无误)
- </NButton>
- </div>
- <!-- 明细列表 -->
- <div style="margin-top: 8px; margin-bottom: 8px; font-weight: 600; font-size: 14px">
- 订单明细
- </div>
- <NDataTable
- :columns="detailColumns"
- :data="detail.items"
- :row-key="row => row.id"
- size="small"
- striped
- :bordered="false"
- max-height="500"
- />
- <template #footer>
- <NSpace justify="end">
- <NButton @click="detailVisible = false">
- 关闭
- </NButton>
- </NSpace>
- </template>
- </NDrawerContent>
- </NDrawer>
- <!-- 订单详情弹窗 -->
- <NModal
- v-model:show="orderDetailVisible"
- title="订单详情"
- preset="card"
- style="width: 640px; max-width: 95vw"
- :bordered="false"
- :segmented="{ content: true }"
- >
- <div v-if="orderDetail">
- <NDescriptions label-placement="left" bordered :column="2" size="small">
- <NDescriptionsItem label="订单号">
- {{ orderDetail.orderNo }}
- </NDescriptionsItem>
- <NDescriptionsItem label="房间号">
- {{ orderDetail.roomNo }}
- </NDescriptionsItem>
- <NDescriptionsItem label="房型">
- {{ orderDetail.roomType || '-' }}
- </NDescriptionsItem>
- <NDescriptionsItem label="联系人">
- {{ orderDetail.contactName || '-' }}
- </NDescriptionsItem>
- <NDescriptionsItem label="订单状态">
- <DictTag dict-type="hotel_order_status" :value="String(orderDetail.status)" size="small" />
- </NDescriptionsItem>
- <NDescriptionsItem label="支付状态">
- <span v-if="orderDetail.payStatus === 1" style="color:#18a058">已支付</span>
- <span v-else style="color:#999">未支付</span>
- </NDescriptionsItem>
- <NDescriptionsItem label="退款状态">
- <span v-if="orderDetail.refundStatus === 1" style="color:#18a058">已退款</span>
- <span v-else-if="orderDetail.refundStatus === 2" style="color:#e03e3e">退款失败</span>
- <span v-else>未退款</span>
- </NDescriptionsItem>
- <NDescriptionsItem label="下单时间">
- {{ orderDetail.createTime }}
- </NDescriptionsItem>
- <NDescriptionsItem label="订单总额" :span="2">
- <span style="font-weight:600">¥{{ orderDetail.totalAmount }}</span>
- <span v-if="orderDetail.paidAmount" style="margin-left:12px;color:#18a058">实付 ¥{{ orderDetail.paidAmount }}</span>
- </NDescriptionsItem>
- <NDescriptionsItem v-if="orderDetail.note" label="备注" :span="2">
- {{ orderDetail.note }}
- </NDescriptionsItem>
- </NDescriptions>
- <div v-if="orderDetail.items && orderDetail.items.length" style="margin-top:16px">
- <div style="font-weight:600;font-size:14px;margin-bottom:8px">
- 菜品明细
- </div>
- <NDataTable :columns="orderItemColumns" :data="orderDetail.items" :row-key="row => row.id" size="small" striped :bordered="false" :pagination="false" />
- </div>
- </div>
- </NModal>
- </div>
- </template>
- <script setup>
- import {
- NButton,
- NDataTable,
- NDescriptions,
- NDescriptionsItem,
- NDrawer,
- NDrawerContent,
- NModal,
- NSpace,
- useMessage,
- } from 'naive-ui'
- import { computed, h, ref } from 'vue'
- import {
- confirmReconciliation,
- getOrderDetail,
- getPartnerReconciliationDetail,
- } from '@/api/hotel'
- import { AiCrudPage } from '@/components/ai-form'
- import DictTag from '@/components/DictTag.vue'
- import { useDict } from '@/composables/useDict'
- defineOptions({ name: 'HotelReconciliationPartner' })
- const message = useMessage()
- const { dict } = useDict('hotel_reconciliation_status', 'hotel_order_status')
- const crudRef = ref(null)
- // ==================== 搜索配置 ====================
- const statusOptions = computed(() => {
- const list = dict.value.hotel_reconciliation_status || []
- // 餐厅端只能看到已确认(1)和已结算(2)
- return list
- .filter(item => Number(item.value) >= 1)
- .map(item => ({ label: item.label, value: Number(item.value) }))
- })
- const searchSchema = computed(() => [
- {
- field: 'reconciliationNo',
- label: '对账单编号',
- type: 'input',
- props: { placeholder: '请输入编号' },
- },
- {
- field: 'yearMonth',
- label: '对账月份',
- type: 'month',
- props: { valueFormat: 'yyyy-MM', placeholder: '请选择月份' },
- },
- {
- field: 'status',
- label: '状态',
- type: 'select',
- props: { placeholder: '请选择状态', options: statusOptions.value },
- },
- ])
- function transformSearchParams(params) {
- // yearMonth 字段直接由 month picker 以 "yyyy-MM" 格式返回,无需转换
- return params
- }
- // ==================== 表格列配置 ====================
- const tableColumns = computed(() => [
- {
- prop: 'reconciliationNo',
- label: '对账单编号',
- width: 170,
- render: row => h('a', {
- class: 'text-primary cursor-pointer hover:opacity-80',
- onClick: () => openDetail(row.id),
- }, row.reconciliationNo),
- },
- {
- prop: 'periodStart',
- label: '对账周期',
- width: 200,
- render: row => `${formatDate(row.periodStart)} ~ ${formatDate(row.periodEnd)}`,
- },
- { prop: 'orderCount', label: '订单数', width: 80, align: 'right' },
- {
- prop: 'totalAmount',
- label: '总额(元)',
- width: 110,
- align: 'right',
- render: row => `¥${row.totalAmount}`,
- },
- {
- prop: 'excludedCount',
- label: '排除',
- width: 100,
- align: 'right',
- render: row => row.excludedCount > 0 ? `${row.excludedCount}笔/¥${row.excludedAmount}` : '0',
- },
- {
- prop: 'settlementAmount',
- label: '应付(元)',
- width: 120,
- align: 'right',
- render: row => h('span', { style: 'font-weight:600;color:#18a058' }, `¥${row.settlementAmount}`),
- },
- {
- prop: 'status',
- label: '状态',
- width: 90,
- render: row => h(DictTag, { dictType: 'hotel_reconciliation_status', value: String(row.status), size: 'small' }),
- },
- { prop: 'createTime', label: '创建时间', width: 170 },
- {
- label: '操作',
- width: 140,
- fixed: 'right',
- render: (row) => {
- const actions = []
- if (row.status === 1) {
- actions.push(h('a', {
- class: 'text-success cursor-pointer hover:opacity-80',
- onClick: () => handleConfirm(row.id),
- }, '确认'))
- }
- return h('div', { class: 'flex gap-2' }, actions)
- },
- },
- ])
- // ==================== 详情抽屉 ====================
- const detailVisible = ref(false)
- const detail = ref({})
- async function openDetail(id) {
- try {
- const res = await getPartnerReconciliationDetail(id)
- if (res.code === 200 && res.data) {
- detail.value = res.data
- detailVisible.value = true
- }
- }
- catch (e) {
- message.error(e.message || '加载详情失败')
- }
- }
- // 明细表格列(只读,无操作列)
- const detailColumns = [
- {
- title: '序号',
- key: 'index',
- width: 55,
- render: (row, rowIndex) => rowIndex + 1,
- },
- {
- title: '订单号',
- key: 'orderNo',
- width: 160,
- render: row => h('a', {
- class: 'text-primary cursor-pointer hover:opacity-80',
- onClick: () => openOrderDetail(row.orderId),
- }, row.orderNo),
- },
- { title: '房间号', key: 'roomNo', width: 80 },
- { title: '联系人', key: 'contactName', width: 90 },
- {
- title: '实付金额',
- key: 'payAmount',
- width: 100,
- align: 'right',
- render: row => `¥${row.payAmount}`,
- },
- {
- title: '退款状态',
- key: 'refundStatus',
- width: 90,
- render: (row) => {
- if (row.refundStatus === 0 || row.refundStatus == null)
- return '未退款'
- if (row.refundStatus === 1)
- return h('span', { style: 'color:#18a058' }, '已退款')
- if (row.refundStatus === 2)
- return h('span', { style: 'color:#e03e3e' }, '退款失败')
- return '-'
- },
- },
- {
- title: '排除',
- key: 'excluded',
- width: 80,
- render: row => row.excluded === 1
- ? h('span', { style: 'color:#e03e3e;font-weight:600' }, '已排除')
- : '否',
- },
- ]
- // ==================== 订单详情弹窗 ====================
- const orderDetailVisible = ref(false)
- const orderDetail = ref(null)
- const orderItemColumns = [
- { title: '菜品', key: 'dishName', width: 160 },
- { title: '规格', key: 'specDesc', width: 120, render: row => row.specDesc || '-' },
- { title: '数量', key: 'quantity', width: 60, align: 'right' },
- { title: '单价', key: 'unitPrice', width: 80, align: 'right', render: row => `¥${row.unitPrice || '0.00'}` },
- { title: '小计', key: 'subtotal', width: 80, align: 'right', render: row => `¥${row.subtotal || '0.00'}` },
- ]
- async function openOrderDetail(orderId) {
- try {
- const res = await getOrderDetail(orderId)
- if (res.code === 200 && res.data) {
- orderDetail.value = res.data
- orderDetailVisible.value = true
- }
- }
- catch (e) {
- message.error(e.message || '加载订单详情失败')
- }
- }
- // ==================== 确认对账单 ====================
- async function handleConfirm(id) {
- const dialog = window.$dialog
- if (!dialog)
- return
- dialog.warning({
- title: '确认对账单',
- content: '确认该对账单金额无误?确认后不可修改。',
- positiveText: '确认无误',
- negativeText: '再想想',
- onPositiveClick: async () => {
- try {
- await confirmReconciliation(id)
- message.success('已确认对账单')
- crudRef.value?.refresh()
- if (detailVisible.value) {
- const res = await getPartnerReconciliationDetail(id)
- if (res.code === 200 && res.data) {
- detail.value = res.data
- }
- }
- }
- catch (e) {
- message.error(e.message || '确认失败')
- }
- },
- })
- }
- // ==================== 工具方法 ====================
- function formatDate(val) {
- if (!val)
- return ''
- if (typeof val === 'string')
- return val.substring(0, 10)
- const d = new Date(val)
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
- }
- </script>
- <style scoped>
- .reconciliation-partner-page {
- padding: 16px;
- }
- </style>
|