优化
This commit is contained in:
@@ -5,13 +5,15 @@ export { default as InboundMeasurementDialog } from './measurement/InboundMeasur
|
||||
export { default as MeasurementEvidenceThumbnail } from './measurement/components/MeasurementEvidenceThumbnail.vue'
|
||||
export { default as InboundFeePanel } from './fee/InboundFeePanel.vue'
|
||||
export {
|
||||
adjustPublishedInboundFee,
|
||||
calculateInboundFee,
|
||||
confirmInboundFee,
|
||||
createFeeAdjustmentIdempotencyKey,
|
||||
createFeeActionIdempotencyKey,
|
||||
getLatestInboundFee,
|
||||
sendInboundFee,
|
||||
} from './fee/api'
|
||||
export type { InboundFeeSnapshot } from './fee/types'
|
||||
export type { AdjustPublishedInboundFeeRequest, InboundFeeSnapshot } from './fee/types'
|
||||
export { downloadWeightImage } from './weight/api/weightOcr'
|
||||
export type { InboundMeasurementDetail } from './measurement/types'
|
||||
export type { InboundHoldCommand, InboundHoldDetail, InboundHoldEvidenceUpload, InboundHoldHistoryEntry } from './core/types'
|
||||
|
||||
@@ -7,10 +7,14 @@ const source = readFileSync(resolve(process.cwd(), 'src/modules/unified-orders/p
|
||||
const routeSource = readFileSync(resolve(process.cwd(), 'src/modules/unified-orders/routes.ts'), 'utf8').replace(/\r\n/g, '\n')
|
||||
|
||||
describe('UnifiedOrdersView contract', () => {
|
||||
it('shows the server confirmation lock without reading a stale fee snapshot', () => {
|
||||
it('loads the reference unit price while rechecking the confirmation lock before sending', () => {
|
||||
const openDialogSource = source.slice(source.indexOf('async function openFeeConfirmDialog()'), source.indexOf('async function executeConfirmAndSendFee()'))
|
||||
const sendFeeSource = source.slice(source.indexOf('async function executeConfirmAndSendFee()'), source.indexOf('function handleOrderSelectionChange'))
|
||||
expect(openDialogSource).not.toContain('getLatestInboundFee')
|
||||
expect(openDialogSource).toContain('getLatestInboundFee')
|
||||
expect(sendFeeSource).toContain('getLatestInboundFee')
|
||||
expect(sendFeeSource).toContain('adjustPublishedInboundFee')
|
||||
expect(source).toContain('价格表参考单价')
|
||||
expect(source).toContain('本次对客单价')
|
||||
expect(sendFeeSource).toContain("ElMessage.error(getApiErrorDetails(error, '确认费用发送失败').message)")
|
||||
})
|
||||
|
||||
@@ -443,7 +447,7 @@ describe('UnifiedOrdersView contract', () => {
|
||||
const sendFeeSource = source.slice(source.indexOf('async function executeConfirmAndSendFee()'), source.indexOf('function handleOrderSelectionChange'))
|
||||
expect(sendFeeSource).not.toContain('saveInboundMeasurementDraft')
|
||||
expect(sendFeeSource).not.toContain('updateMeasurementCalculationMode')
|
||||
expect(source.slice(source.indexOf('async function openFeeConfirmDialog()'), source.indexOf('async function executeConfirmAndSendFee()'))).not.toContain('getLatestInboundFee')
|
||||
expect(source.slice(source.indexOf('async function openFeeConfirmDialog()'), source.indexOf('async function executeConfirmAndSendFee()'))).toContain('getLatestInboundFee')
|
||||
})
|
||||
|
||||
it('clears an invalidated measurement loading state before refreshing order details', () => {
|
||||
|
||||
@@ -59,9 +59,11 @@ import {
|
||||
unifiedOrderStatusLabel,
|
||||
} from '@/modules/unified-orders/status'
|
||||
import {
|
||||
adjustPublishedInboundFee,
|
||||
downloadWeightImage,
|
||||
calculateInboundFee,
|
||||
confirmInboundFee,
|
||||
createFeeAdjustmentIdempotencyKey,
|
||||
createFeeActionIdempotencyKey,
|
||||
getLatestInboundFee,
|
||||
getInboundMeasurement,
|
||||
@@ -75,6 +77,7 @@ import {
|
||||
InboundMeasurementDialog,
|
||||
MeasurementEvidenceThumbnail,
|
||||
sendInboundFee,
|
||||
type InboundFeeSnapshot,
|
||||
type InboundMeasurementDetail,
|
||||
type InboundHoldDetail,
|
||||
} from '@/modules/inbounds'
|
||||
@@ -146,6 +149,12 @@ const customerFeeTotal = computed(() => (
|
||||
function feeMoney(value: number) {
|
||||
return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: otherFeeSummary.value.currency || 'CNY' }).format(value)
|
||||
}
|
||||
function feeConfirmMoney(value: number, currency = 'CNY') {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency,
|
||||
}).format(value)
|
||||
}
|
||||
|
||||
type OtherFeeSummary = { currency: string; approvedReceivable: number; approvedPayable: number }
|
||||
type AutomaticFeeSummary = { feeStatus: string; totalAmount: number }
|
||||
@@ -222,6 +231,25 @@ const confirmFeeSending = ref(false)
|
||||
const feeConfirmOpen = ref(false)
|
||||
const feeConfirmWeight = ref<number | null>(null)
|
||||
const feeConfirmVolume = ref<number | null>(null)
|
||||
const feeConfirmReferenceLoading = ref(false)
|
||||
const feeConfirmReference = ref<InboundFeeSnapshot | null>(null)
|
||||
const feeConfirmUnitPrice = ref<number | null>(null)
|
||||
const feeConfirmPriceReason = ref('')
|
||||
const feeConfirmPriceChanged = computed(() => {
|
||||
const reference = Number(feeConfirmReference.value?.unitPrice)
|
||||
const current = Number(feeConfirmUnitPrice.value)
|
||||
return reference >= 0 && current >= 0 && Math.abs(reference - current) > 0.000001
|
||||
})
|
||||
const feeConfirmPriceUnit = computed(() => {
|
||||
const snapshot = feeConfirmReference.value
|
||||
return snapshot ? `${snapshot.currency} / ${snapshot.chargeUnit}` : '—'
|
||||
})
|
||||
const feeConfirmEstimatedTotal = computed(() => {
|
||||
const snapshot = feeConfirmReference.value
|
||||
const unitPrice = Number(feeConfirmUnitPrice.value)
|
||||
if (!snapshot || !(unitPrice >= 0)) return null
|
||||
return Number(snapshot.chargeableQuantity || 0) * unitPrice + Number(snapshot.additionalAmount || 0)
|
||||
})
|
||||
const feeConfirmRows = computed(() => {
|
||||
const summary = detail.value?.summary
|
||||
const measuredGrossWeight = Number(measurementDetail.value?.actualGrossWeightKg)
|
||||
@@ -1631,7 +1659,40 @@ async function openFeeConfirmDialog() {
|
||||
? Number(actualTotals.gross)
|
||||
: measuredGrossWeight > 0 ? measuredGrossWeight : measuredNetWeight > 0 ? measuredNetWeight : null
|
||||
feeConfirmVolume.value = Number(actualTotals.volume) > 0 ? Number(actualTotals.volume) : null
|
||||
feeConfirmReference.value = null
|
||||
feeConfirmUnitPrice.value = null
|
||||
feeConfirmPriceReason.value = ''
|
||||
feeConfirmOpen.value = true
|
||||
feeConfirmReferenceLoading.value = true
|
||||
try {
|
||||
const tenantId = String(summary.tenantId)
|
||||
const inboundId = String(summary.inboundId)
|
||||
let snapshot: InboundFeeSnapshot
|
||||
try {
|
||||
snapshot = await getLatestInboundFee(tenantId, inboundId)
|
||||
} catch (error) {
|
||||
const details = getApiErrorDetails(error, '价格表参考单价加载失败')
|
||||
if (details.code !== 'A0404') throw error
|
||||
snapshot = await calculateInboundFee(tenantId, inboundId, {
|
||||
selectedWeightKg: Number(feeConfirmWeight.value),
|
||||
selectedVolumeCbm: Number(feeConfirmVolume.value),
|
||||
weightSource: 'ACTUAL',
|
||||
volumeSource: 'ACTUAL',
|
||||
}, createFeeActionIdempotencyKey('calculate'))
|
||||
}
|
||||
if (snapshot.customerConfirmationStatus === 'CONFIRMED' || snapshot.customerConfirmedAt) {
|
||||
ElMessage.warning('客户已确认该费用,费用已锁定,不允许再修改或重新发送')
|
||||
feeConfirmOpen.value = false
|
||||
return
|
||||
}
|
||||
feeConfirmReference.value = snapshot
|
||||
feeConfirmUnitPrice.value = Number(snapshot.unitPrice)
|
||||
} catch (error) {
|
||||
ElMessage.error(getApiErrorDetails(error, '价格表参考单价加载失败,暂不能推送费用').message)
|
||||
feeConfirmOpen.value = false
|
||||
} finally {
|
||||
feeConfirmReferenceLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function executeConfirmAndSendFee() {
|
||||
@@ -1639,10 +1700,19 @@ async function executeConfirmAndSendFee() {
|
||||
if (!summary?.inboundId) return
|
||||
const selectedWeightKg = Number(feeConfirmWeight.value)
|
||||
const selectedVolumeCbm = Number(feeConfirmVolume.value)
|
||||
const selectedUnitPrice = Number(feeConfirmUnitPrice.value)
|
||||
if (!(selectedWeightKg > 0) || !(selectedVolumeCbm > 0)) {
|
||||
ElMessage.warning('计费毛重和计费体积必须大于 0')
|
||||
return
|
||||
}
|
||||
if (!(selectedUnitPrice > 0)) {
|
||||
ElMessage.warning('对客单价必须大于 0')
|
||||
return
|
||||
}
|
||||
if (feeConfirmPriceChanged.value && feeConfirmPriceReason.value.trim().length < 2) {
|
||||
ElMessage.warning('人工修改参考单价后,请填写至少 2 个字符的调整原因')
|
||||
return
|
||||
}
|
||||
confirmFeeSending.value = true
|
||||
try {
|
||||
const tenantId = String(summary.tenantId)
|
||||
@@ -1667,12 +1737,23 @@ async function executeConfirmAndSendFee() {
|
||||
weightSource: 'ACTUAL',
|
||||
volumeSource: 'ACTUAL',
|
||||
}, createFeeActionIdempotencyKey('calculate'))
|
||||
const calculatedUnitPrice = Number(snapshot.unitPrice)
|
||||
if (Math.abs(selectedUnitPrice - calculatedUnitPrice) > 0.000001) {
|
||||
snapshot = await adjustPublishedInboundFee(tenantId, inboundId, {
|
||||
expectedFeeVersion: snapshot.feeVersion,
|
||||
chargeableQuantity: Number(snapshot.chargeableQuantity),
|
||||
unitPrice: selectedUnitPrice,
|
||||
additionalAmount: Number(snapshot.additionalAmount || 0),
|
||||
reason: feeConfirmPriceReason.value.trim() || '推送前按已展示参考单价人工确认',
|
||||
toleranceConfirmed: true,
|
||||
}, createFeeAdjustmentIdempotencyKey())
|
||||
}
|
||||
snapshot = await confirmInboundFee(tenantId, inboundId, createFeeActionIdempotencyKey('confirm'))
|
||||
if (snapshot.feeStatus === 'CONFIRMED') {
|
||||
const sent = await sendInboundFee(tenantId, inboundId, createFeeActionIdempotencyKey('send'))
|
||||
automaticFee.value = { feeStatus: sent.feeStatus, totalAmount: Number(sent.totalAmount || 0) }
|
||||
feePanelRevision.value += 1
|
||||
ElMessage.success('计费重已确认并发送给客户')
|
||||
ElMessage.success('计费量和单价已确认并发送给客户')
|
||||
feeConfirmOpen.value = false
|
||||
await loadDetail(true)
|
||||
return
|
||||
@@ -2127,13 +2208,22 @@ onBeforeUnmount(() => {
|
||||
<template>
|
||||
<AdminPage>
|
||||
<section class="business-page unified-orders-page">
|
||||
<AppDialog v-model="feeConfirmOpen" title="编辑计费数据" confirm-text="确认" :confirm-loading="confirmFeeSending" width="min(920px, 94vw)" :teleported="false" @confirm="executeConfirmAndSendFee">
|
||||
<AppDialog v-model="feeConfirmOpen" title="编辑计费数据" confirm-text="确认并发送" :confirm-loading="confirmFeeSending || feeConfirmReferenceLoading" width="min(920px, 94vw)" :teleported="false" @confirm="executeConfirmAndSendFee">
|
||||
<el-alert class="fee-confirm-price-alert" title="价格表单价仅作为参考;本次对客单价可人工修改,客户将一次确认计费量、单价和费用合计。" type="info" :closable="false" show-icon />
|
||||
<el-form label-position="top" class="fee-confirm-form">
|
||||
<el-form-item label="入仓码"><el-input :model-value="detail?.summary.inboundCode || '—'" disabled /></el-form-item>
|
||||
<el-form-item label="渠道"><el-input :model-value="detail?.summary.channelName || detail?.summary.channelCode || '—'" disabled /></el-form-item>
|
||||
<el-form-item label="计费毛重"><div class="fee-confirm-number-wrap"><el-input-number v-model="feeConfirmWeight" :min="0" :precision="3" :controls="false" class="fee-confirm-number" placeholder="请输入计费毛重" /><span class="fee-confirm-unit">kg</span></div></el-form-item>
|
||||
<el-form-item label="计费体积"><div class="fee-confirm-number-wrap"><el-input-number v-model="feeConfirmVolume" :min="0" :precision="6" :controls="false" class="fee-confirm-number" placeholder="请输入计费体积" /><span class="fee-confirm-unit">m³</span></div></el-form-item>
|
||||
<el-form-item label="价格表参考单价"><div class="fee-confirm-number-wrap"><el-input-number :model-value="feeConfirmReference?.unitPrice == null ? undefined : Number(feeConfirmReference.unitPrice)" :precision="4" :controls="false" disabled class="fee-confirm-number" placeholder="正在读取参考单价" /><span class="fee-confirm-unit">{{ feeConfirmPriceUnit }}</span></div></el-form-item>
|
||||
<el-form-item label="本次对客单价"><div class="fee-confirm-number-wrap"><el-input-number v-model="feeConfirmUnitPrice" :min="0" :precision="4" :controls="false" :disabled="feeConfirmReferenceLoading" class="fee-confirm-number" placeholder="请输入对客单价" /><span class="fee-confirm-unit">{{ feeConfirmPriceUnit }}</span></div></el-form-item>
|
||||
<el-form-item v-if="feeConfirmPriceChanged" label="单价调整原因" class="fee-confirm-price-reason"><el-input v-model="feeConfirmPriceReason" type="textarea" :rows="2" maxlength="500" show-word-limit placeholder="请说明本次人工改单价的原因" /></el-form-item>
|
||||
</el-form>
|
||||
<dl v-if="feeConfirmReference" class="fee-confirm-price-summary">
|
||||
<div><dt>参考计费量</dt><dd>{{ formatNumber(Number(feeConfirmReference.chargeableQuantity), feeConfirmReference.roundingScale) }} {{ feeConfirmReference.chargeUnit }}</dd></div>
|
||||
<div><dt>固定附加费</dt><dd>{{ feeConfirmMoney(Number(feeConfirmReference.additionalAmount || 0), feeConfirmReference.currency) }}</dd></div>
|
||||
<div><dt>按当前单价预计合计</dt><dd>{{ feeConfirmEstimatedTotal == null ? '—' : feeConfirmMoney(feeConfirmEstimatedTotal, feeConfirmReference.currency) }}</dd></div>
|
||||
</dl>
|
||||
<h4 class="fee-confirm-reference-title">参考数据</h4>
|
||||
<el-table :data="feeConfirmRows" border size="small">
|
||||
<el-table-column prop="source" label="数据来源" />
|
||||
@@ -2794,11 +2884,18 @@ onBeforeUnmount(() => {
|
||||
.measurement-title-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
|
||||
.measurement-title-actions :deep(.el-button) { margin-left: 0; }
|
||||
.fee-confirm-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 22px; }
|
||||
.fee-confirm-price-alert { margin-bottom: 14px; }
|
||||
.fee-confirm-number-wrap { position: relative; width: 100%; }
|
||||
.fee-confirm-number { width: 100%; }
|
||||
.fee-confirm-number :deep(.el-input__wrapper) { padding-right: 42px; }
|
||||
.fee-confirm-number :deep(.el-input__wrapper) { padding-right: 92px; }
|
||||
.fee-confirm-unit { position: absolute; top: 50%; right: 12px; z-index: 1; transform: translateY(-50%); color: var(--cc-muted); pointer-events: none; }
|
||||
.fee-confirm-reference-title { margin: 8px 0 12px; color: var(--cc-text); font-size: 14px; }
|
||||
.fee-confirm-price-reason { grid-column: 1 / -1; }
|
||||
.fee-confirm-price-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 0 0 16px; border: 1px solid var(--cc-border); background: #f8fafc; }
|
||||
.fee-confirm-price-summary > div { min-width: 0; padding: 10px 12px; }
|
||||
.fee-confirm-price-summary > div + div { border-left: 1px solid var(--cc-border); }
|
||||
.fee-confirm-price-summary dt { color: var(--cc-muted); font-size: 11px; }
|
||||
.fee-confirm-price-summary dd { margin: 4px 0 0; color: var(--cc-text); font-size: 13px; font-weight: 650; }
|
||||
@media (max-width: 680px) { .fee-confirm-form { grid-template-columns: 1fr; } .fee-confirm-unit { right: 12px; } }
|
||||
.fee-section-title { margin: 14px 0 8px; color: #344054; font-size: 13px; line-height: 1.4; }
|
||||
.fee-overview { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 8px 0 0; border: 1px solid var(--cc-border); }
|
||||
|
||||
Reference in New Issue
Block a user