Files
BMS/bms-front/src/views/price/billing/components/BillDetail.vue
T
2026-10-14 15:25:26 +08:00

497 lines
19 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="转单号">{{ detail.transferNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="销售员">{{ detail.belongSaler || '-' }}</el-descriptions-item>
<el-descriptions-item label="结算员">{{ detail.settlementMan || '-' }}</el-descriptions-item>
<el-descriptions-item label="客服员">{{ detail.serviceMan || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户">{{ displayCustomer(detail) }}</el-descriptions-item>
<el-descriptions-item label="客户状态">{{ customerActiveLabel(detail.customerActiveStatus) }}</el-descriptions-item>
<el-descriptions-item label="额度欠费">{{ creditArrearsLabel(detail.creditArrearsStatus) }}</el-descriptions-item>
<el-descriptions-item label="退费状态">{{ refundLabel(detail.refundStatus) }}</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)">{{ formatWeight(detail.actualWeight) }}</el-descriptions-item>
<el-descriptions-item label="收货重(kg)">{{ formatWeight(detail.receiveWeight) }}</el-descriptions-item>
<el-descriptions-item label="材积重(kg)">{{ formatWeight(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.incomeTotal ?? detail.payableTotal) }}</div>
</div>
<div class="amount-card is-total">
<div class="amount-label">应付总额</div>
<div class="amount-value">{{ formatMoney(detail.payTotal) }}</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)">{{ formatWeight(detail.chargeWeight ?? detail.weight) }}</el-descriptions-item>
<el-descriptions-item label="原计费重(kg)">{{ formatWeight(detail.weight) }}</el-descriptions-item>
<el-descriptions-item label="重量段">{{ detail.weightLabel || '-' }}</el-descriptions-item>
<el-descriptions-item label="计费时间">{{ 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="套餐版本">{{ detail.versionNo || (detail.psvId ? ('psv=' + detail.psvId) : '-') }}</el-descriptions-item> -->
<!-- <el-descriptions-item label="套餐明细ID">{{ detail.psdId != null ? detail.psdId : '-' }}</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="运费单价" >{{ formatRate(detail.freightRate) }}</el-descriptions-item>
<el-descriptions-item label="挂号费" >{{ formatMoney(detail.registrationFee) }}</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 class="detail-block">
<div class="sec-title">费用明细</div>
<el-tabs v-model="feeTab" class="fee-tabs">
<el-tab-pane label="应收" name="income">
<div class="fee-toolbar">
<el-button type="primary" plain size="small" icon="Plus" v-hasPermi="['price:billing:feeAdd']" @click="openAddFee">增加费用</el-button>
<el-button type="primary" plain size="small" icon="Edit" v-hasPermi="['price:billing:chargeWeightEdit']" :disabled="detail.refundStatus === 'REFUNDED'" @click="openEditCw">修改计费重</el-button>
</div>
<el-table v-if="incomeFees.length" :data="incomeFees" 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-column label="操作" width="120" align="center" fixed="right">
<template #default="{ row }">
<template v-if="canEditManualFee(row)">
<el-button link type="primary" v-hasPermi="['price:billing:feeEdit']" @click="openEditFee(row)">编辑</el-button>
<el-button link type="danger" v-hasPermi="['price:billing:feeRemove']" @click="handleDeleteFee(row)">删除</el-button>
</template>
<span v-else>-</span>
</template>
</el-table-column>
</el-table>
<el-empty v-else description="暂无应收明细" :image-size="64" />
</el-tab-pane>
<el-tab-pane label="应付" name="pay">
<el-table v-if="payFees.length" :data="payFees" 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>
<el-empty v-else description="暂无应付明细" :image-size="64" />
</el-tab-pane>
</el-tabs>
</section>
</div>
</div>
<add-income-fee-dialog
v-model="addFeeOpen"
:bill-id="detail?.billId || ''"
:charge-weight="detail?.chargeWeight ?? detail?.weight"
:freight-amount="detail?.freightAmount"
:edit-row="editFeeRow"
@success="onFeeAdded"
/>
<edit-charge-weight-dialog
v-model="editCwOpen"
:bill-id="detail?.billId || ''"
:order-no="detail?.orderNo || ''"
:origin-weight="detail?.weight"
:current-charge-weight="detail?.chargeWeight"
@success="onChargeWeightUpdated"
/>
</el-drawer>
</template>
<script setup>
import AddIncomeFeeDialog from './AddIncomeFeeDialog.vue'
import EditChargeWeightDialog from './EditChargeWeightDialog.vue'
import { deleteIncomeBillFee } from '@/api/price/billing'
const { proxy } = getCurrentInstance()
const props = defineProps({
modelValue: { type: Boolean, default: false },
title: { type: String, default: '计费详情' },
loading: { type: Boolean, default: false },
detail: { type: Object, default: null }
})
const emit = defineEmits(['update:modelValue', 'updated'])
const feeTab = ref('income')
const addFeeOpen = ref(false)
const editFeeRow = ref(null)
const editCwOpen = ref(false)
const { bms_fee_type, bms_currency } = useDict('bms_fee_type', 'bms_currency')
const incomeFees = computed(() => {
const d = props.detail
if (!d) return []
return d.feeDetails || d.incomeFeeDetails || []
})
const payFees = computed(() => {
const d = props.detail
if (!d) return []
return d.payFeeDetails || []
})
watch(() => props.detail, () => {
feeTab.value = 'income'
})
function canEditManualFee(row) {
return row && String(row.manualFlag) === '1' && row.arBillStatus !== 'BILLED'
}
function openAddFee() {
if (!props.detail?.billId) return
editFeeRow.value = null
addFeeOpen.value = true
}
function openEditFee(row) {
editFeeRow.value = row
addFeeOpen.value = true
}
function handleDeleteFee(row) {
if (!canEditManualFee(row)) return
proxy.$modal.confirm('确认删除该费用吗?').then(() => {
return deleteIncomeBillFee(row.feeId)
}).then(res => {
proxy.$modal.msgSuccess('删除成功')
emit('updated', res.data || null)
}).catch(() => {})
}
function onFeeAdded(data) {
editFeeRow.value = null
if (data) emit('updated', data)
}
function openEditCw() {
if (!props.detail?.billId) return
if (props.detail.refundStatus === 'REFUNDED') return
editCwOpen.value = true
}
function onChargeWeightUpdated(data) {
if (data) emit('updated', data)
}
function customerActiveLabel(status) {
const map = { ACTIVE: '正常', MISSING: '不存在', DISABLED: '停用' }
return map[status] || status || '-'
}
function creditArrearsLabel(status) {
const map = { NORMAL: '正常', ARREARS: '欠费' }
return map[status] || status || '-'
}
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(2)
}
function formatWeight(val) {
if (val === null || val === undefined || val === '') return '-'
const num = Number(val)
if (Number.isNaN(num)) return val
return num.toFixed(3)
}
function formatRate(val) {
if (val === null || val === undefined || val === '') return '-'
const num = Number(val)
if (Number.isNaN(num)) return val
return num.toFixed(2)
}
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 ''
}
function refundLabel(status) {
const map = { NONE: '未退费', REFUNDED: '已退费' }
return map[status] || status || '未退费'
}
</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(5, 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;
}
.fee-toolbar {
display: flex;
/* justify-content: flex-end; */
margin-bottom: 10px;
}
.fee-tabs {
margin-top: 0;
}
.fee-tabs :deep(.el-tabs__header) {
margin-bottom: 12px;
}
.fee-tabs :deep(.el-empty) {
padding: 24px 0;
}
</style>