优化
This commit is contained in:
@@ -95,6 +95,31 @@ describe('InboundTaskList measurement scope', () => {
|
||||
expect(source).not.toContain("name: 'inbound-order-detail', params: { inboundId: row.id }")
|
||||
})
|
||||
|
||||
it('shows authoritative measurement totals with operation first and no internal IDs as primary columns', () => {
|
||||
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
|
||||
const columnsSource = source.slice(source.indexOf('const columns'), source.indexOf('function text'))
|
||||
|
||||
expect(columnsSource.indexOf("key: 'actions'")).toBeLessThan(columnsSource.indexOf("key: 'inboundCode'"))
|
||||
expect(columnsSource).toContain("key: 'customerName', label: '客户'")
|
||||
expect(columnsSource).toContain("key: 'channelName', label: '渠道'")
|
||||
expect(columnsSource).toContain("key: 'totalPackageCount', label: '总箱数'")
|
||||
expect(columnsSource).toContain("key: 'palletCount', label: '托盘数'")
|
||||
expect(columnsSource).toContain("key: 'actualGrossWeightKg', label: '实测毛重(kg)'")
|
||||
expect(columnsSource).toContain("key: 'actualNetWeightKg', label: '货物重量(kg)'")
|
||||
expect(columnsSource).toContain("key: 'actualVolumeCbm', label: '实测体积(m³)'")
|
||||
expect(columnsSource).toContain("key: 'measureCompletedAt', label: '提交时间'")
|
||||
expect(columnsSource).not.toContain("key: 'id'")
|
||||
expect(columnsSource).not.toContain("key: 'orderId'")
|
||||
})
|
||||
|
||||
it('renders missing list facts as an explicit dash instead of guessing values', () => {
|
||||
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
|
||||
|
||||
expect(source).toContain("value === null || value === undefined || value === '' ? '—'")
|
||||
expect(source).toContain("value === null || value === undefined ? '—'")
|
||||
expect(source).toContain("if (!value) return '—'")
|
||||
})
|
||||
|
||||
it('refreshes cached task lists when users return from measurement work', () => {
|
||||
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
|
||||
|
||||
|
||||
@@ -93,9 +93,11 @@ const columns = computed<DataTableColumn[]>(() => [
|
||||
{ key: 'businessType', label: '业务类型', minWidth: 120, slotName: 'businessType' },
|
||||
{ key: 'channelName', label: '渠道', minWidth: 140, slotName: 'channelName' },
|
||||
{ key: 'totalPackageCount', label: '总箱数', width: 92, slotName: 'totalPackageCount' },
|
||||
{ key: 'palletCount', label: '托盘数', width: 92, slotName: 'palletCount' },
|
||||
{ key: 'actualGrossWeightKg', label: '实测毛重(kg)', minWidth: 128, slotName: 'actualGrossWeightKg' },
|
||||
{ key: 'actualNetWeightKg', label: '货物重量(kg)', minWidth: 128, slotName: 'actualNetWeightKg' },
|
||||
{ key: 'actualVolumeCbm', label: '实测体积(m³)', minWidth: 128, slotName: 'actualVolumeCbm' },
|
||||
{ key: 'measureCompletedAt', label: '测量完成时间', minWidth: 164, slotName: 'measureCompletedAt' },
|
||||
{ key: 'measureCompletedAt', label: '提交时间', minWidth: 164, slotName: 'measureCompletedAt' },
|
||||
])
|
||||
|
||||
function text(value: unknown) {
|
||||
@@ -334,7 +336,9 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria
|
||||
<template #businessType="{ row }">{{ text((row as InboundOrderSummary).businessType) }}</template>
|
||||
<template #channelName="{ row }">{{ text((row as InboundOrderSummary).channelName || (row as InboundOrderSummary).channelCode) }}</template>
|
||||
<template #totalPackageCount="{ row }">{{ text((row as InboundOrderSummary).totalPackageCount) }}</template>
|
||||
<template #palletCount="{ row }">{{ text((row as InboundOrderSummary).palletCount) }}</template>
|
||||
<template #actualGrossWeightKg="{ row }">{{ formatNumber((row as InboundOrderSummary).actualGrossWeightKg) }}</template>
|
||||
<template #actualNetWeightKg="{ row }">{{ formatNumber((row as InboundOrderSummary).actualNetWeightKg) }}</template>
|
||||
<template #actualVolumeCbm="{ row }">{{ formatNumber((row as InboundOrderSummary).actualVolumeCbm) }}</template>
|
||||
<template #measureCompletedAt="{ row }">{{ formatDate((row as InboundOrderSummary).measureCompletedAt) }}</template>
|
||||
<template #empty><el-empty :description="emptyText" /></template>
|
||||
|
||||
@@ -30,7 +30,11 @@ describe('platform inbound query API', () => {
|
||||
it('uses the implemented non-v1 list path and normalizes page values and IDs', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({
|
||||
code: '200', message: 'success', data: {
|
||||
records: [{ id: 19001, tenantId: 10002, orderId: 18001, customerId: 10001, totalPackageCount: '6', holdFlag: 'false' }],
|
||||
records: [{
|
||||
id: 19001, tenantId: 10002, orderId: 18001, customerId: 10001,
|
||||
totalPackageCount: '6', palletCount: '2', actualGrossWeightKg: '21.5',
|
||||
actualNetWeightKg: '18.25', actualVolumeCbm: '0.456', holdFlag: 'false',
|
||||
}],
|
||||
page: '1', size: '20', total: '1', pages: '1',
|
||||
},
|
||||
})
|
||||
@@ -38,7 +42,11 @@ describe('platform inbound query API', () => {
|
||||
await expect(listPlatformInbounds('10001', {
|
||||
page: 1, size: 20, tab: 'WAIT_MEASURE', inboundCode: 'RK', holdFlag: false,
|
||||
})).resolves.toMatchObject({
|
||||
records: [{ id: '19001', tenantId: '10002', orderId: '18001', customerId: '10001', totalPackageCount: 6, holdFlag: false }],
|
||||
records: [{
|
||||
id: '19001', tenantId: '10002', orderId: '18001', customerId: '10001',
|
||||
totalPackageCount: 6, palletCount: 2, actualGrossWeightKg: 21.5,
|
||||
actualNetWeightKg: 18.25, actualVolumeCbm: 0.456, holdFlag: false,
|
||||
}],
|
||||
total: 1, pages: 1,
|
||||
})
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/inbounds', {
|
||||
|
||||
@@ -75,7 +75,7 @@ describe('InboundFeePanel', () => {
|
||||
|
||||
expect(getLatestInboundFee).toHaveBeenCalledWith('10001', '88')
|
||||
expect(document.body.textContent).not.toContain('租户')
|
||||
expect(document.body.textContent).toContain('对客费用总额 · CNY')
|
||||
expect(document.body.textContent).toContain('头程自动费用 · CNY')
|
||||
expect(document.body.textContent).toContain('¥1,235.60')
|
||||
expect(document.body.textContent).toContain('基础运费小计 ¥1,135.60')
|
||||
expect(document.body.textContent).toContain('附加费 ¥100.00')
|
||||
@@ -130,8 +130,10 @@ describe('InboundFeePanel', () => {
|
||||
|
||||
const content = wrapper.find('.fee-content').text()
|
||||
expect(wrapper.findAll('.fee-summary > div > span').map((node) => node.text())).toEqual([
|
||||
'对客费用总额 · CNY',
|
||||
'头程自动费用 · CNY',
|
||||
'最终计费量',
|
||||
'单价',
|
||||
'附加费',
|
||||
'快照版本',
|
||||
])
|
||||
expect(wrapper.findAll('.calculation-flow > div > span').map((node) => node.text())).toEqual([
|
||||
@@ -169,12 +171,13 @@ describe('InboundFeePanel', () => {
|
||||
expect(document.querySelector('[role="alert"]')).toBeNull()
|
||||
})
|
||||
|
||||
it('shows adjustment only for authorized published inbound fees', async () => {
|
||||
it('shows adjustment only for authorized calculated inbound fees', async () => {
|
||||
mountPanel(['firstleg:fba-pricing:view'])
|
||||
await flushPromises()
|
||||
expect(document.querySelector('[data-testid="adjust-fee-button"]')).toBeNull()
|
||||
|
||||
document.body.innerHTML = ''
|
||||
vi.mocked(getLatestInboundFee).mockResolvedValue({ ...snapshot, feeStatus: 'CALCULATED' })
|
||||
mountPanel()
|
||||
await flushPromises()
|
||||
expect(document.querySelector('[data-testid="adjust-fee-button"]')).not.toBeNull()
|
||||
@@ -189,6 +192,7 @@ describe('InboundFeePanel', () => {
|
||||
})
|
||||
|
||||
it('submits a reasoned adjustment and emits the refreshed snapshot', async () => {
|
||||
vi.mocked(getLatestInboundFee).mockResolvedValue({ ...snapshot, feeStatus: 'CALCULATED' })
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
;(document.querySelector('[data-testid="adjust-fee-button"]') as HTMLButtonElement).click()
|
||||
@@ -233,6 +237,7 @@ describe('InboundFeePanel', () => {
|
||||
})
|
||||
|
||||
it('refreshes the latest snapshot after a conflict while retaining the dialog', async () => {
|
||||
vi.mocked(getLatestInboundFee).mockResolvedValue({ ...snapshot, feeStatus: 'CALCULATED' })
|
||||
vi.mocked(adjustPublishedInboundFee).mockRejectedValue(
|
||||
new ApiResponseError('费用已被其他操作员修改,请刷新后重试', 'A0409', 409),
|
||||
)
|
||||
|
||||
@@ -22,10 +22,12 @@ const props = defineProps<{
|
||||
customerId?: string
|
||||
tenantId?: string
|
||||
inboundId: string
|
||||
compact?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
changed: [snapshot: InboundFeeSnapshot]
|
||||
loaded: [snapshot: InboundFeeSnapshot | null]
|
||||
}>()
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
@@ -64,7 +66,7 @@ const canView = computed(() => access.can('firstleg:fba-pricing:view'))
|
||||
const canAdjust = computed(() => access.can('firstleg:fba-pricing:adjust'))
|
||||
const customerContextId = computed(() => props.customerId || props.tenantId || '')
|
||||
const adjustmentEligible = computed(() => (
|
||||
['CALCULATED', 'CONFIRMED', 'SENT'].includes(snapshot.value?.feeStatus || '')
|
||||
snapshot.value?.feeStatus === 'CALCULATED'
|
||||
&& snapshot.value?.nextProcessStatus === 'WAIT_SPLIT'
|
||||
))
|
||||
const chargeModeLabel = computed(() => snapshot.value?.chargeMode === 'KG' ? '按重量计费' : '按体积计费')
|
||||
@@ -133,10 +135,12 @@ async function loadFee(options: { preserveAdjustment?: boolean } = {}) {
|
||||
const result = await getLatestInboundFee(customerContextId.value, props.inboundId)
|
||||
if (sequence !== loadSequence) return
|
||||
snapshot.value = result
|
||||
emit('loaded', result)
|
||||
if (!options.preserveAdjustment && adjustOpen.value) fillAdjustmentForm(result)
|
||||
} catch (error) {
|
||||
if (sequence !== loadSequence) return
|
||||
snapshot.value = null
|
||||
emit('loaded', null)
|
||||
if (error instanceof ApiResponseError && error.code === 'A0404') notFound.value = true
|
||||
else loadError.value = getApiErrorDetails(error, '费用快照加载失败,请稍后重试')
|
||||
} finally {
|
||||
@@ -228,8 +232,8 @@ defineExpose({ reload: loadFee })
|
||||
|
||||
<template>
|
||||
<section class="inbound-fee-panel" :aria-busy="loading" aria-labelledby="inbound-fee-title">
|
||||
<header class="fee-heading">
|
||||
<div class="fee-heading-copy">
|
||||
<header class="fee-heading" :class="{ 'fee-heading--compact': compact }">
|
||||
<div v-if="!compact" class="fee-heading-copy">
|
||||
<span class="fee-heading-icon" aria-hidden="true"><CircleDollarSign :size="20" /></span>
|
||||
<div>
|
||||
<h3 id="inbound-fee-title">入库费用快照</h3>
|
||||
@@ -279,7 +283,7 @@ defineExpose({ reload: loadFee })
|
||||
<div v-else-if="snapshot" class="fee-content">
|
||||
<div class="fee-summary" aria-label="费用关键结果">
|
||||
<div class="fee-total">
|
||||
<span>对客费用总额 · {{ snapshot.currency }}</span>
|
||||
<span>头程自动费用 · {{ snapshot.currency }}</span>
|
||||
<strong>{{ formatMoney(snapshot.totalAmount, snapshot.currency) }}</strong>
|
||||
<small>基础运费小计 {{ formatMoney(snapshot.subtotalAmount, snapshot.currency) }} · 附加费 {{ formatMoney(snapshot.additionalAmount, snapshot.currency) }}</small>
|
||||
</div>
|
||||
@@ -288,6 +292,16 @@ defineExpose({ reload: loadFee })
|
||||
<strong>{{ formatNumber(snapshot.chargeableQuantity, snapshot.roundingScale) }} {{ chargeUnitLabel }}</strong>
|
||||
<small>{{ chargeModeLabel }}</small>
|
||||
</div>
|
||||
<div class="fee-summary-item">
|
||||
<span>单价</span>
|
||||
<strong>{{ formatMoney(snapshot.unitPrice, snapshot.currency) }} / {{ chargeUnitLabel }}</strong>
|
||||
<small>{{ snapshot.channelCode }}</small>
|
||||
</div>
|
||||
<div class="fee-summary-item">
|
||||
<span>附加费</span>
|
||||
<strong>{{ formatMoney(snapshot.additionalAmount, snapshot.currency) }}</strong>
|
||||
<small>{{ snapshot.additionalAmount ? '已计入总额' : '无附加费' }}</small>
|
||||
</div>
|
||||
<div class="fee-summary-item">
|
||||
<span>快照版本</span>
|
||||
<div class="version-status"><strong>V{{ snapshot.feeVersion }}</strong><el-tag :type="statusInfo.type" effect="light">{{ statusInfo.label }}</el-tag></div>
|
||||
@@ -297,7 +311,7 @@ defineExpose({ reload: loadFee })
|
||||
|
||||
<section class="calculation-section" aria-labelledby="calculation-title">
|
||||
<div class="section-title">
|
||||
<div><h4 id="calculation-title">计费过程</h4><p>实际量与折算量取大后计费,金额以服务端快照为准。</p></div>
|
||||
<div><h4 id="calculation-title">计费依据</h4><p v-if="!compact">实际量与折算量取大后计费,金额以服务端快照为准。</p></div>
|
||||
<el-tag effect="plain">{{ taxModeLabel(snapshot.taxMode) }}</el-tag>
|
||||
</div>
|
||||
<div class="calculation-flow">
|
||||
@@ -397,6 +411,7 @@ defineExpose({ reload: loadFee })
|
||||
<style scoped>
|
||||
.inbound-fee-panel { min-width: 0; color: var(--cc-text); }
|
||||
.fee-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding-bottom: 16px; }
|
||||
.fee-heading--compact { justify-content: flex-end; padding-bottom: 6px; }
|
||||
.fee-heading-copy { display: flex; min-width: 0; align-items: flex-start; gap: 12px; }
|
||||
.fee-heading-icon { display: grid; width: 38px; height: 38px; flex: 0 0 38px; place-items: center; border-radius: 10px; background: #eef4ff; color: var(--cc-primary); }
|
||||
.fee-heading h3, .section-title h4 { margin: 0; }
|
||||
@@ -409,24 +424,26 @@ defineExpose({ reload: loadFee })
|
||||
.inbound-fee-panel :deep(.el-alert .el-button) { margin-top: 8px; }
|
||||
.empty-note { margin: 0; color: var(--cc-muted); font-size: 12px; }
|
||||
.fee-content { border-top: 1px solid var(--cc-border); }
|
||||
.fee-summary { display: grid; grid-template-columns: minmax(240px, 1.25fr) repeat(2, minmax(160px, 1fr)); border-bottom: 1px solid var(--cc-border); }
|
||||
.fee-summary { display: grid; grid-template-columns: minmax(230px, 1.3fr) repeat(4, minmax(135px, 1fr)); border-top: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
|
||||
.fee-total, .fee-summary-item { display: flex; min-width: 0; flex-direction: column; justify-content: center; gap: 3px; padding: 11px 14px; }
|
||||
.fee-summary-item { border-left: 1px solid var(--cc-border); }
|
||||
.fee-summary span, .fee-summary small { color: var(--cc-muted); font-size: 11px; }
|
||||
.fee-summary strong { font-size: 18px; line-height: 1.25; overflow-wrap: anywhere; }
|
||||
.fee-total strong { color: #0f5c49; font-size: 21px; letter-spacing: -.01em; }
|
||||
.fee-summary-item strong { font-size: 15px; }
|
||||
.version-status { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
||||
.calculation-section, .fee-timeline { padding: 14px 0; border-bottom: 1px solid var(--cc-border); }
|
||||
.calculation-section, .fee-timeline { padding: 10px 0; border-bottom: 1px solid var(--cc-border); }
|
||||
.section-title { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
|
||||
.section-title h4 { font-size: 14px; }
|
||||
.calculation-flow { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin-top: 10px; border-top: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
|
||||
.calculation-flow > div { display: flex; min-width: 0; min-height: 58px; flex-direction: column; justify-content: center; gap: 3px; padding: 9px 12px; border-right: 1px solid var(--cc-border); }
|
||||
.calculation-flow { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin-top: 7px; border-top: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
|
||||
.calculation-flow > div { display: flex; min-width: 0; min-height: 46px; flex-direction: row; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 10px; border-right: 1px solid var(--cc-border); }
|
||||
.calculation-flow > div:last-child { border-right: 0; }
|
||||
.calculation-flow span, .calculation-flow small { color: var(--cc-muted); font-size: 11px; }
|
||||
.calculation-flow strong { font-size: 15px; overflow-wrap: anywhere; }
|
||||
.calculation-flow strong { font-size: 13px; overflow-wrap: anywhere; }
|
||||
.calculation-flow small { display: none; }
|
||||
.calculation-flow .is-chargeable { background: #f7fbfa; }
|
||||
.calculation-flow .is-chargeable strong { color: #087d70; }
|
||||
.amount-equation { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 8px; padding: 7px 0; color: #475467; font-size: 12px; }
|
||||
.amount-equation { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; margin-top: 5px; padding: 4px 0 0; color: #475467; font-size: 12px; }
|
||||
.amount-equation i { color: #98a2b3; font-style: normal; }
|
||||
.amount-equation strong { color: var(--cc-text); font-size: 15px; }
|
||||
.fee-detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); border-bottom: 1px solid var(--cc-border); }
|
||||
|
||||
Reference in New Issue
Block a user