Files
BMS/bms-front/src/views/ar/bill/components/BillDetail.vue
T
2026-10-28 14:58:09 +08:00

159 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<el-drawer class="bill-detail-drawer" :model-value="modelValue" :title="detail?.billNo || '应收账单详情'" size="min(1000px, 100vw)" append-to-body destroy-on-close @update:model-value="$emit('update:modelValue', $event)">
<div v-loading="loading">
<el-empty v-if="!detail" description="暂无详情" />
<template v-else>
<el-descriptions :column="4" border size="small" class="summary">
<el-descriptions-item label="账单名称">{{ detail.billName || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户">{{ detail.customerName }} ({{ detail.customerCode || '-' }})</el-descriptions-item>
<el-descriptions-item label="账单截止日">截至 {{ date(detail.billPeriodEnd) }}</el-descriptions-item>
<el-descriptions-item label="状态"><el-tag :type="statusType(detail.status)">{{ statusText(detail.status) }}</el-tag></el-descriptions-item>
<el-descriptions-item label="币种">{{ detail.currency }}</el-descriptions-item>
<el-descriptions-item label="应收总额"><span class="total">{{ money(detail.totalAmount) }}</span></el-descriptions-item>
<el-descriptions-item label="已核销">{{ money(detail.writtenOffAmount) }}</el-descriptions-item>
<el-descriptions-item label="未核销">{{ money(detail.unwrittenAmount) }}</el-descriptions-item>
<el-descriptions-item label="收款账户">{{ detail.bankAccountName || '-' }}</el-descriptions-item>
<el-descriptions-item label="开户行">{{ detail.bankName || '-' }}</el-descriptions-item>
<el-descriptions-item label="收款账号">{{ mask(detail.bankAccountNo) }}</el-descriptions-item>
</el-descriptions>
<!-- 账单明细独立分页;费用明细在展开行时再懒加载。 -->
<div class="section-title">账单明细({{ detail.orderCount || 0 }})</div>
<el-table v-loading="detailListLoading" :data="detailRows" row-key="detailId" border size="small" @expand-change="handleExpandChange">
<el-table-column type="expand">
<template #default="{ row }">
<div v-loading="row.feeLoading" class="fee-panel">
<el-empty v-if="row.feesLoaded && !row.fees?.length" description="暂无费用明细" :image-size="60" />
<el-table v-else :data="row.fees || []" border size="small" class="fee-table">
<el-table-column prop="feeType" label="费用类型" width="140" />
<el-table-column prop="feeName" label="费用名称" min-width="160" />
<el-table-column prop="amount" label="金额" width="120" align="right"><template #default="{ row: fee }">{{ money(fee.amount) }}</template></el-table-column>
<el-table-column prop="productAmount" label="产品价" width="120" align="right"><template #default="{ row: fee }">{{ amount4(fee.productAmount) }}</template></el-table-column>
<el-table-column prop="redlineAmount" label="红线价" width="120" align="right"><template #default="{ row: fee }">{{ amount4(fee.redlineAmount) }}</template></el-table-column>
<el-table-column prop="currency" label="币种" width="80" />
<el-table-column prop="remark" label="说明" show-overflow-tooltip />
</el-table>
</div>
</template>
</el-table-column>
<el-table-column prop="detailNo" label="#" width="55" align="center" />
<el-table-column prop="orderNo" label="订单号" min-width="180" show-overflow-tooltip />
<el-table-column prop="trackingNo" label="运单号" min-width="160" show-overflow-tooltip />
<el-table-column prop="transferNo" label="转单号" min-width="160" show-overflow-tooltip />
<el-table-column label="产品" min-width="150"><template #default="{ row }">{{ row.productName || row.productCode || '-' }}</template></el-table-column>
<el-table-column label="目的地" min-width="120"><template #default="{ row }">{{ row.countryName || row.countryCode || '-' }}</template></el-table-column>
<el-table-column label="重量/尺寸" min-width="170"><template #default="{ row }">{{ row.chargeWeight || '-' }} kg / {{ dimensions(row) }}</template></el-table-column>
<el-table-column prop="totalAmount" label="订单金额" width="120" align="right"><template #default="{ row }">{{ money(row.totalAmount) }}</template></el-table-column>
<el-table-column prop="productAmount" label="整单产品价" width="130" align="right"><template #default="{ row }">{{ amount4(row.productAmount) }}</template></el-table-column>
<el-table-column prop="redlineAmount" label="整单红线价" width="130" align="right"><template #default="{ row }">{{ amount4(row.redlineAmount) }}</template></el-table-column>
</el-table>
<el-pagination
v-if="detailTotal > 0"
class="detail-pagination"
v-model:current-page="detailPageNum"
v-model:page-size="detailPageSize"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
:total="detailTotal"
@current-change="loadDetails"
@size-change="handleDetailSizeChange"
/>
<div class="section-title export-title"><span>导出历史</span><el-button size="small" type="primary" plain icon="Download" v-hasPermi="['ar:bill:export']" :loading="exporting" :disabled="exporting" @click="$emit('export')">{{ exporting ? '生成中' : '导出账单明细' }}</el-button></div>
<el-table :data="detail.exports || []" border size="small">
<el-table-column prop="fileName" label="文件名" min-width="250" show-overflow-tooltip />
<el-table-column prop="templateVersion" label="模板版本" width="100" />
<el-table-column prop="exportStatus" label="状态" width="100"><template #default="{ row }"><el-tag :type="exportStatusType(row.exportStatus)">{{ exportStatusText(row.exportStatus) }}</el-tag></template></el-table-column>
<el-table-column prop="remark" label="失败原因" min-width="180" show-overflow-tooltip />
<el-table-column prop="exportBy" label="导出人" width="100" />
<el-table-column prop="exportTime" label="导出时间" width="170" />
<el-table-column label="操作" width="90"><template #default="{ row }"><el-button v-if="row.exportStatus === AR_EXPORT_STATUS.SUCCESS" link type="primary" v-hasPermi="['ar:bill:download']" @click="$emit('download', row)">下载</el-button></template></el-table-column>
</el-table>
</template>
</div>
</el-drawer>
</template>
<script setup>
import { listArBillDetails, listArBillDetailFees } from '@/api/ar/bill'
import { AR_BILL_STATUS_OPTIONS, AR_BILL_STATUS_TYPES, AR_EXPORT_STATUS, AR_EXPORT_STATUS_OPTIONS, AR_EXPORT_STATUS_TYPES, arEnumLabel } from '@/constants/ar/enums'
const props = defineProps({ modelValue: Boolean, detail: { type: Object, default: null }, loading: Boolean })
defineEmits(['update:modelValue', 'download', 'export'])
const detailRows = ref([])
const detailTotal = ref(0)
const detailPageNum = ref(1)
// 详情默认每页 10 条,避免几十万条账单明细一次性进入浏览器内存。
const detailPageSize = ref(10)
const detailListLoading = ref(false)
let detailRequestSerial = 0
const exporting = computed(() => props.detail?.exports?.some(item => item.exportStatus === AR_EXPORT_STATUS.PROCESSING))
// 切换账单时重置分页和请求序号,旧账单的慢响应不能覆盖新账单。
watch(() => props.detail?.arBillId, billId => {
detailRequestSerial += 1
detailPageNum.value = 1
detailTotal.value = 0
detailRows.value = []
if (billId) loadDetails()
}, { immediate: true })
// 账单明细接口只返回当前页;分页变化时复用同一账单 ID 重新查询。
function loadDetails() {
const billId = props.detail?.arBillId
if (!billId) return
const serial = ++detailRequestSerial
detailListLoading.value = true
listArBillDetails(billId, { pageNum: detailPageNum.value, pageSize: detailPageSize.value }).then(res => {
if (serial !== detailRequestSerial) return
detailRows.value = (res.rows || []).map(row => ({ ...row, fees: [], feesLoaded: false, feeLoading: false }))
detailTotal.value = Number(res.total) || 0
}).finally(() => {
if (serial === detailRequestSerial) detailListLoading.value = false
})
}
function handleDetailSizeChange(size) {
detailPageSize.value = size
detailPageNum.value = 1
loadDetails()
}
// 费用明细按行懒加载,并用标记避免同一行重复请求。
function handleExpandChange(row, expandedRows) {
if (!expandedRows.includes(row) || row.feesLoaded || row.feeLoading) return
const billId = props.detail?.arBillId
if (!billId || !row.detailId) return
row.feeLoading = true
listArBillDetailFees(billId, row.detailId).then(res => {
row.fees = res.data || []
row.feesLoaded = true
}).finally(() => {
row.feeLoading = false
})
}
function date(value) { return value ? String(value).slice(0, 10) : '-' }
function money(value) { const num = Number(value); return Number.isNaN(num) ? '-' : num.toFixed(2) }
function amount4(value) { if (value == null || value === '') return '-'; const num = Number(value); return Number.isNaN(num) ? '-' : num.toFixed(4) }
function statusText(value) { return arEnumLabel(AR_BILL_STATUS_OPTIONS, value) }
function statusType(value) { return AR_BILL_STATUS_TYPES[value] || 'info' }
function exportStatusText(value) { return arEnumLabel(AR_EXPORT_STATUS_OPTIONS, value) }
function exportStatusType(value) { return AR_EXPORT_STATUS_TYPES[value] || 'info' }
function dimensions(row) { return [row.pkgLength, row.pkgWidth, row.pkgHeight].map(v => v == null ? '-' : v).join(' × ') }
function mask(value) { if (!value) return '-'; return value.length <= 4 ? '****' : '****' + value.slice(-4) }
</script>
<style scoped>
:deep(.bill-detail-drawer) { max-width: 100vw; }
.summary { margin-bottom: 18px; }
.total { color: #f56c6c; font-size: 16px; font-weight: 600; }
.section-title { display: flex; align-items: center; gap: 12px; margin: 18px 0 10px; font-weight: 600; }
.section-title::before { content: ''; width: 3px; height: 15px; border-radius: 2px; background: var(--el-color-primary); }
.export-title { justify-content: space-between; }
.export-title::before { margin-right: auto; }
.detail-pagination { justify-content: flex-end; margin: 12px 0 4px; }
.fee-panel { min-height: 54px; padding: 4px 12px 8px 42px; background: #fafafa; }
.fee-table { background: #fafafa; }
</style>