159 lines
10 KiB
Vue
159 lines
10 KiB
Vue
<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>
|