Files
BMS/bms-front/src/views/price/billing/components/BillDetail.vue
T
2026-09-11 16:12:01 +08:00

309 lines
11 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
:model-value="modelValue"
:title="title"
size="840px"
append-to-body
destroy-on-close
class="bill-detail-drawer"
@update:model-value="$emit('update:modelValue', $event)"
>
<div v-loading="loading" class="bill-detail-wrap">
<el-empty v-if="!detail" description="暂无详情" />
<div v-else class="bill-detail">
<section class="detail-block">
<div class="sec-title">订单信息</div>
<el-descriptions class="detail-desc" :column="2" border size="small">
<el-descriptions-item label="订单号">{{ detail.orderNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="运单号">{{ detail.trackingNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户">{{ displayCustomer(detail) }}</el-descriptions-item>
<el-descriptions-item label="仓库">{{ detail.warehouseCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="发件人">{{ detail.shipperName || '-' }}</el-descriptions-item>
<el-descriptions-item label="收件人">{{ detail.consigneeName || '-' }}</el-descriptions-item>
<el-descriptions-item label="目的国/地区">{{ countryLabel(detail) }}</el-descriptions-item>
<el-descriptions-item label="目的邮编">{{ detail.destPostcode || '-' }}</el-descriptions-item>
<el-descriptions-item label="件数">{{ detail.pieces != null ? detail.pieces : '-' }}</el-descriptions-item>
<el-descriptions-item label="尺寸(cm)">{{ dimLabel(detail) }}</el-descriptions-item>
<el-descriptions-item label="实重(kg)">{{ formatMoney(detail.actualWeight) }}</el-descriptions-item>
<el-descriptions-item label="材积重(kg)">{{ formatMoney(detail.volumeWeight) }}</el-descriptions-item>
<el-descriptions-item label="体积(m³)">{{ formatMoney(detail.volume) }}</el-descriptions-item>
<el-descriptions-item label="下单时间">{{ formatDateTime(detail.orderTime) }}</el-descriptions-item>
<el-descriptions-item label="订单备注" :span="2">{{ detail.orderRemark || '-' }}</el-descriptions-item>
</el-descriptions>
</section>
<section class="detail-block">
<div class="sec-title">计费信息</div>
<div class="amount-summary">
<div class="amount-card">
<div class="amount-label">运费金额</div>
<div class="amount-value">{{ formatMoney(detail.freightAmount) }}</div>
</div>
<div class="amount-card">
<div class="amount-label">挂号费</div>
<div class="amount-value">{{ formatMoney(detail.registrationFee) }}</div>
</div>
<div class="amount-card">
<div class="amount-label">附加费</div>
<div class="amount-value">{{ formatMoney(detail.surchargeAmount) }}</div>
</div>
<div class="amount-card is-total">
<div class="amount-label">应收总额</div>
<div class="amount-value">{{ formatMoney(detail.payableTotal) }}</div>
</div>
</div>
<el-descriptions class="detail-desc" :column="2" border size="small">
<el-descriptions-item label="产品">{{ productLabel(detail) }}</el-descriptions-item>
<el-descriptions-item label="计费重量(kg)">{{ detail.chargeWeight ?? detail.weight ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="重量段">{{ detail.weightLabel || '-' }}</el-descriptions-item>
<el-descriptions-item label="计费日期">{{ formatDate(detail.bizDate) }}</el-descriptions-item>
<el-descriptions-item label="计价来源">
<el-tag size="small" :type="sourceTagType(detail.matchSource)">{{ sourceLabel(detail.matchSource) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="套餐">{{ priceSetLabel(detail) }}</el-descriptions-item>
<el-descriptions-item label="币种">
<dict-tag :options="bms_currency" :value="displayCurrency(detail.currency)" />
</el-descriptions-item>
<el-descriptions-item label="取值方式">{{ roundModeLabel(detail.roundMode) }}</el-descriptions-item>
<el-descriptions-item label="运费单价" :span="2">{{ formatMoney(detail.freightRate) }}</el-descriptions-item>
</el-descriptions>
<div v-if="detail.matchMessage" class="match-note">
<span class="match-note__label">命中说明</span>
<span>{{ detail.matchMessage }}</span>
</div>
</section>
<section v-if="detail.feeDetails && detail.feeDetails.length" class="detail-block">
<div class="sec-title">费用明细</div>
<el-table :data="detail.feeDetails" border size="small" class="fee-table">
<el-table-column label="费用名称" min-width="140" show-overflow-tooltip>
<template #default="{ row }">{{ displayFeeName(row) }}</template>
</el-table-column>
<el-table-column label="费用类型" width="110" align="center">
<template #default="{ row }">
<el-tag size="small" :type="feeTypeTag(row.feeType)" effect="plain">
{{ feeTypeLabel(row.feeType) || row.feeType }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="金额" min-width="120" align="right">
<template #default="{ row }">
<span class="fee-amount">{{ formatMoney(row.amount) }}</span>
</template>
</el-table-column>
<el-table-column label="币种" width="90" align="center">
<template #default="{ row }">
<dict-tag :options="bms_currency" :value="displayCurrency(row.currency)" />
</template>
</el-table-column>
<el-table-column label="说明" min-width="160" show-overflow-tooltip>
<template #default="{ row }">{{ row.remark || '-' }}</template>
</el-table-column>
</el-table>
</section>
</div>
</div>
</el-drawer>
</template>
<script setup>
defineProps({
modelValue: { type: Boolean, default: false },
title: { type: String, default: '计费详情' },
loading: { type: Boolean, default: false },
detail: { type: Object, default: null }
})
defineEmits(['update:modelValue'])
const { bms_fee_type, bms_currency } = useDict('bms_fee_type', 'bms_currency')
function displayCustomer(row) {
if (row.customerName && row.customerCode) return row.customerName + ' (' + row.customerCode + ')'
return row.customerName || row.customerCode || row.customerId || '-'
}
function countryLabel(row) {
return (row.countryName || '-') + ' (' + (row.countryCode || '-') + ')'
}
function productLabel(row) {
return (row.productName || '-') + ' (' + (row.productCode || '-') + ')'
}
function priceSetLabel(row) {
const name = row.priceSetName || '-'
return row.priceSetCode ? name + ' (' + row.priceSetCode + ')' : name
}
function dimLabel(row) {
if (row.pkgLength == null && row.pkgWidth == null && row.pkgHeight == null) return '-'
return [row.pkgLength, row.pkgWidth, row.pkgHeight].map(v => v == null ? '-' : v).join(' × ')
}
function formatMoney(val) {
if (val === null || val === undefined || val === '') return '-'
const num = Number(val)
if (Number.isNaN(num)) return val
return num.toFixed(4)
}
function formatDate(val) {
if (!val) return '-'
const d = new Date(val)
if (Number.isNaN(d.getTime())) return val
return d.toISOString().slice(0, 10)
}
function formatDateTime(val) {
if (!val) return '-'
const d = new Date(val)
if (Number.isNaN(d.getTime())) return val
const pad = n => String(n).padStart(2, '0')
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds())
}
function sourceLabel(source) {
const map = {
PACKAGE_FIXED: '客户固定价套餐',
PACKAGE_DISCOUNT: '客户折扣套餐',
PRODUCT_DEFAULT: '产品默认价',
FIXED: '客户固定价套餐',
DISCOUNT: '客户折扣套餐'
}
return map[source] || source || '-'
}
function roundModeLabel(mode) {
const map = { UP: '向上取值', DOWN: '向下取整', HALF_UP: '四舍五入' }
return map[mode] || mode || '四舍五入'
}
function feeTypeLabel(type) {
const found = (bms_fee_type.value || []).find(d => d.value === type)
return found ? found.label : ''
}
function feeTypeTag(type) {
if (type === 'FREIGHT') return ''
if (type === 'REGISTRATION') return 'warning'
if (type === 'SURCHARGE') return 'danger'
return 'info'
}
function displayCurrency(v) {
if (!v || v === 'RMB' || v === 'rmb') return 'CNY'
return v
}
function displayFeeName(row) {
const typeLabel = feeTypeLabel(row.feeType)
const name = String(row.feeName || row.fee_name || '').trim()
const isSurcharge = row.feeType === 'SURCHARGE' || row.feeType === '附加费'
if (isSurcharge && name && name !== typeLabel && name !== 'SURCHARGE' && name !== '附加费') {
return name
}
return name || typeLabel || row.feeType || '-'
}
function sourceTagType(source) {
if (source === 'PACKAGE_FIXED' || source === 'FIXED') return 'success'
if (source === 'PACKAGE_DISCOUNT' || source === 'DISCOUNT') return 'warning'
if (source === 'PRODUCT_DEFAULT') return 'info'
return ''
}
</script>
<style scoped>
.bill-detail-wrap {
min-height: 240px;
}
.bill-detail {
padding-bottom: 8px;
}
.detail-block {
margin-bottom: 20px;
}
.detail-block:last-child {
margin-bottom: 0;
}
.sec-title {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 12px;
font-size: 14px;
font-weight: 600;
color: #303133;
line-height: 1;
}
.sec-title::before {
content: '';
width: 3px;
height: 14px;
border-radius: 2px;
background: var(--el-color-primary);
}
.amount-summary {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin-bottom: 12px;
}
.amount-card {
padding: 12px 14px;
background: #f7f8fa;
border-radius: 6px;
}
.amount-card.is-total {
background: #fff1f0;
}
.amount-label {
font-size: 12px;
color: #909399;
line-height: 1.2;
}
.amount-value {
margin-top: 6px;
font-size: 16px;
font-weight: 600;
color: #303133;
font-variant-numeric: tabular-nums;
line-height: 1.2;
}
.amount-card.is-total .amount-value {
color: #f56c6c;
}
.detail-desc :deep(.el-descriptions__label) {
width: 108px;
color: #909399;
font-weight: 400;
background: #fafbfc;
}
.detail-desc :deep(.el-descriptions__content) {
color: #303133;
}
.detail-desc :deep(.el-descriptions__cell) {
padding: 9px 12px;
}
.match-note {
display: flex;
gap: 8px;
margin-top: 10px;
padding: 8px 12px;
font-size: 12px;
line-height: 1.6;
color: #606266;
background: #f4f4f5;
border-radius: 4px;
}
.match-note__label {
flex-shrink: 0;
color: #909399;
}
.fee-amount {
font-variant-numeric: tabular-nums;
font-weight: 500;
}
</style>