更新计费预览与测量精度并精简作业界面
This commit is contained in:
@@ -76,7 +76,7 @@ import {
|
||||
confirmInboundFee,
|
||||
createFeeAdjustmentIdempotencyKey,
|
||||
createFeeActionIdempotencyKey,
|
||||
getLatestInboundFee,
|
||||
previewInboundFee,
|
||||
getInboundMeasurement,
|
||||
getPlatformInboundHold,
|
||||
downloadInboundCartonLabelsBatch,
|
||||
@@ -93,6 +93,7 @@ import {
|
||||
DropshipPalletPrintPanel,
|
||||
sendInboundFee,
|
||||
type InboundFeeSnapshot,
|
||||
type InboundFeePreview,
|
||||
type InboundMeasurementDetail,
|
||||
type InboundHoldDetail,
|
||||
} from '@/modules/inbounds'
|
||||
@@ -270,10 +271,16 @@ 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 feeConfirmReference = ref<InboundFeePreview | null>(null)
|
||||
const feeConfirmPriceReference = ref<Pick<InboundFeePreview, 'unitPrice' | 'currency' | 'chargeUnit'> | null>(null)
|
||||
const feeConfirmPriceMissing = ref(false)
|
||||
const feeConfirmUnitPrice = ref<number | null>(null)
|
||||
const feeConfirmPriceReason = ref('')
|
||||
let feeConfirmPreviewSerial = 0
|
||||
let feeConfirmPreviewTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const feeConfirmPreviewKey = ref('')
|
||||
let feeConfirmSettingInitialPrice = false
|
||||
const feeConfirmSelectionKey = computed(() => JSON.stringify([feeConfirmWeight.value, feeConfirmVolume.value, feeConfirmUnitPrice.value]))
|
||||
type FeeConfirmAttempt = {
|
||||
fingerprint: string
|
||||
calculateKey: string
|
||||
@@ -285,19 +292,20 @@ type FeeConfirmAttempt = {
|
||||
let feeConfirmAttempt: FeeConfirmAttempt | null = null
|
||||
const feeConfirmPriceChanged = computed(() => {
|
||||
if (feeConfirmPriceMissing.value) return true
|
||||
const reference = Number(feeConfirmReference.value?.unitPrice)
|
||||
const reference = feeConfirmPriceReference.value?.unitPrice
|
||||
const current = Number(feeConfirmUnitPrice.value)
|
||||
return reference >= 0 && current >= 0 && Math.abs(reference - current) > 0.000001
|
||||
return reference != null && reference >= 0 && current >= 0 && Math.abs(reference - current) > 0.000001
|
||||
})
|
||||
const feeConfirmPriceUnit = computed(() => {
|
||||
const snapshot = feeConfirmReference.value
|
||||
const snapshot = feeConfirmPriceReference.value
|
||||
return snapshot ? `${snapshot.currency} / ${snapshot.chargeUnit}` : 'CNY / 计费单位'
|
||||
})
|
||||
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)
|
||||
if (!snapshot || feeConfirmReferenceLoading.value || feeConfirmPreviewKey.value !== feeConfirmSelectionKey.value
|
||||
|| feeConfirmUnitPrice.value == null || !(unitPrice > 0)) return null
|
||||
return snapshot.totalAmount
|
||||
})
|
||||
const feeConfirmRows = computed(() => {
|
||||
const summary = detail.value?.summary
|
||||
@@ -537,34 +545,37 @@ function calculatedMeasurementRow(
|
||||
const customerMeasurementRows = computed(() => (detail.value?.order.cargoes || []).map((row) => (
|
||||
calculatedMeasurementRow(row, measurementSectionModes.customer, true)
|
||||
)))
|
||||
const outerMeasurementRows = computed(() => (measurementDetail.value?.items || []).map((row) => {
|
||||
function hasSavedOuterMeasurement(row: InboundMeasurementDetail['items'][number]) {
|
||||
return [row.calculatedOuterLengthCm, row.calculatedOuterWidthCm, row.calculatedOuterHeightCm,
|
||||
row.unitVolumeCbm, row.totalVolumeCbm].every(value => value != null && Number.isFinite(Number(value)) && Number(value) > 0)
|
||||
&& Boolean(row.calculationRuleVersion)
|
||||
}
|
||||
|
||||
const outerMeasurementRows = computed(() => (measurementDetail.value?.items || []).map((row): Record<string, unknown> => {
|
||||
const source = row as unknown as Record<string, unknown>
|
||||
const recalculated = calculatedMeasurementRow(source, measurementSectionModes.outer)
|
||||
const savedDimensions = [row.calculatedOuterLengthCm, row.calculatedOuterWidthCm, row.calculatedOuterHeightCm]
|
||||
const recalculatedDimensions = [recalculated.lengthCm, recalculated.widthCm, recalculated.heightCm]
|
||||
const warehouseOuterOverride = String(row.calculationRuleVersion || '').startsWith('WAREHOUSE_OUTER_OVERRIDE_')
|
||||
if (savedDimensions.every((value) => Number.isFinite(Number(value)) && Number(value) > 0)
|
||||
&& (warehouseOuterOverride
|
||||
|| savedDimensions.every((value, index) => Math.abs(Number(value) - Number(recalculatedDimensions[index])) < 0.001))
|
||||
&& measurementCalculationModeForRow(source) === measurementSectionModes.outer) {
|
||||
const packageCount = Number(row.packageCount)
|
||||
const unitWeightKg = Number(row.unitWeightKg) > 0
|
||||
? Number(row.unitWeightKg)
|
||||
: Number.isFinite(Number(measurementDetail.value?.actualGrossWeightKg))
|
||||
&& Number(measurementDetail.value?.actualPackageCount) > 0
|
||||
? Number(measurementDetail.value?.actualGrossWeightKg) / Number(measurementDetail.value?.actualPackageCount)
|
||||
: null
|
||||
return {
|
||||
...source,
|
||||
lengthCm: Number(savedDimensions[0]),
|
||||
widthCm: Number(savedDimensions[1]),
|
||||
heightCm: Number(savedDimensions[2]),
|
||||
unitWeightKg,
|
||||
totalWeightKg: unitWeightKg != null && Number.isFinite(packageCount) ? unitWeightKg * packageCount : null,
|
||||
}
|
||||
const saved = hasSavedOuterMeasurement(row)
|
||||
const packageCount = Number(row.packageCount)
|
||||
const unitWeightKg = Number(row.unitWeightKg) > 0
|
||||
? Number(row.unitWeightKg)
|
||||
: Number.isFinite(Number(measurementDetail.value?.actualGrossWeightKg))
|
||||
&& Number(measurementDetail.value?.actualPackageCount) > 0
|
||||
? Number(measurementDetail.value?.actualGrossWeightKg) / Number(measurementDetail.value?.actualPackageCount)
|
||||
: null
|
||||
// 外箱规和对客计费只认已保存快照,当前客户配置的变化不得在前端重写历史测量。
|
||||
return {
|
||||
...source,
|
||||
lengthCm: saved ? row.calculatedOuterLengthCm : null,
|
||||
widthCm: saved ? row.calculatedOuterWidthCm : null,
|
||||
heightCm: saved ? row.calculatedOuterHeightCm : null,
|
||||
unitVolumeCbm: saved ? row.unitVolumeCbm : null,
|
||||
totalVolumeCbm: saved ? row.totalVolumeCbm : null,
|
||||
volumeWeightKg: saved ? row.volumeWeightKg : null,
|
||||
unitWeightKg,
|
||||
totalWeightKg: unitWeightKg != null && Number.isFinite(packageCount) ? unitWeightKg * packageCount : null,
|
||||
}
|
||||
return recalculated
|
||||
}))
|
||||
const hasSavedOuterMeasurementData = computed(() => hasActualMeasurementData.value
|
||||
&& measurementDetail.value!.items.every(hasSavedOuterMeasurement))
|
||||
const actualMeasurementRows = computed(() => (measurementDetail.value?.items || []).map((row) => (
|
||||
calculatedMeasurementRow(row as unknown as Record<string, unknown>, measurementSectionModes.actual, false, false)
|
||||
)))
|
||||
@@ -878,7 +889,7 @@ function formatNumber(value: number | null | undefined, digits = 2) {
|
||||
|
||||
function formatMeasurementVolume(value: number | null | undefined) {
|
||||
return value == null || !Number.isFinite(value) ? '—'
|
||||
: value.toLocaleString('zh-CN', { minimumFractionDigits: 3, maximumFractionDigits: 3 })
|
||||
: value.toLocaleString('zh-CN', { minimumFractionDigits: 4, maximumFractionDigits: 4 })
|
||||
}
|
||||
|
||||
function partyValue(parties: Array<Record<string, unknown>>, type: 'SENDER' | 'RECEIVER', key: string) {
|
||||
@@ -1163,9 +1174,10 @@ async function saveWarehouseOuterDimensions(row: Record<string, unknown>) {
|
||||
outerLengthCm: dimensions[0],
|
||||
outerWidthCm: dimensions[1],
|
||||
outerHeightCm: dimensions[2],
|
||||
calculatedOuterLengthCm: dimensions[0],
|
||||
calculatedOuterWidthCm: dimensions[1],
|
||||
calculatedOuterHeightCm: dimensions[2],
|
||||
// 保存完成后等待服务端返回完整计算快照,不用修正输入值伪装计算结果。
|
||||
calculatedOuterLengthCm: null,
|
||||
calculatedOuterWidthCm: null,
|
||||
calculatedOuterHeightCm: null,
|
||||
} : item),
|
||||
}
|
||||
const refreshed = await waitForSavedMeasurementVersion(
|
||||
@@ -1737,12 +1749,12 @@ async function openFeeConfirmDialog() {
|
||||
if (confirmFeeSending.value) {
|
||||
return
|
||||
}
|
||||
if (measurementStrategySavingId.value) {
|
||||
if (measurementStrategySavingId.value || measurementOuterSavingId.value) {
|
||||
ElMessage.warning('外箱规计算规则正在保存,请稍后再发送费用')
|
||||
return
|
||||
}
|
||||
const outerTotals = measurementTotals(outerMeasurementRows.value)
|
||||
if (!outerMeasurementRows.value.length || !(Number(outerTotals.volume) > 0)) {
|
||||
if (!hasSavedOuterMeasurementData.value || !(Number(outerTotals.volume) > 0)) {
|
||||
ElMessage.warning('外箱规数据不完整,请先保存外箱规尺寸和计算规则')
|
||||
return
|
||||
}
|
||||
@@ -1754,44 +1766,79 @@ async function openFeeConfirmDialog() {
|
||||
: measuredGrossWeight > 0 ? measuredGrossWeight : measuredNetWeight > 0 ? measuredNetWeight : null
|
||||
feeConfirmVolume.value = Number(outerTotals.volume)
|
||||
feeConfirmReference.value = null
|
||||
feeConfirmPriceReference.value = null
|
||||
feeConfirmPriceMissing.value = false
|
||||
feeConfirmUnitPrice.value = null
|
||||
feeConfirmPriceReason.value = ''
|
||||
feeConfirmAttempt = null
|
||||
feeConfirmOpen.value = true
|
||||
await refreshFeeConfirmPreview(true)
|
||||
}
|
||||
|
||||
function cancelFeeConfirmPreview() {
|
||||
feeConfirmPreviewSerial += 1
|
||||
if (feeConfirmPreviewTimer) clearTimeout(feeConfirmPreviewTimer)
|
||||
feeConfirmPreviewTimer = null
|
||||
feeConfirmReferenceLoading.value = false
|
||||
feeConfirmPreviewKey.value = ''
|
||||
}
|
||||
|
||||
async function refreshFeeConfirmPreview(initializePrice = false) {
|
||||
cancelFeeConfirmPreview()
|
||||
const summary = detail.value?.summary
|
||||
const selectedWeightKg = Number(feeConfirmWeight.value)
|
||||
const selectedVolumeCbm = Number(feeConfirmVolume.value)
|
||||
const selectedUnitPrice = feeConfirmUnitPrice.value
|
||||
if (!feeConfirmOpen.value || !summary?.inboundId || !(selectedWeightKg > 0) || !(selectedVolumeCbm > 0)
|
||||
|| (selectedUnitPrice != null && !(selectedUnitPrice > 0))) {
|
||||
feeConfirmReference.value = null
|
||||
return
|
||||
}
|
||||
const serial = feeConfirmPreviewSerial
|
||||
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: 'OUTER',
|
||||
}, createFeeActionIdempotencyKey('calculate'))
|
||||
}
|
||||
const snapshot = await previewInboundFee(String(summary.tenantId), String(summary.inboundId), {
|
||||
selectedWeightKg, selectedVolumeCbm, selectedUnitPrice, weightSource: 'ACTUAL', volumeSource: 'OUTER',
|
||||
})
|
||||
if (serial !== feeConfirmPreviewSerial || !feeConfirmOpen.value) return
|
||||
feeConfirmReference.value = snapshot
|
||||
feeConfirmUnitPrice.value = Number(snapshot.unitPrice)
|
||||
} catch (error) {
|
||||
const details = getApiErrorDetails(error, '价格表参考单价加载失败')
|
||||
if (details.code === 'A0404') {
|
||||
feeConfirmPriceMissing.value = true
|
||||
ElMessage.warning('当前FBA价格未命中,请填写本次对客单价和人工定价原因')
|
||||
} else {
|
||||
ElMessage.error(details.message)
|
||||
feeConfirmOpen.value = false
|
||||
// unitPrice 是价格表匹配价;人工单价只参与服务端金额预览,不覆盖该只读参考。
|
||||
feeConfirmPriceReference.value = { unitPrice: snapshot.unitPrice, currency: snapshot.currency, chargeUnit: snapshot.chargeUnit }
|
||||
feeConfirmPriceMissing.value = !snapshot.priceMatched
|
||||
if (initializePrice) {
|
||||
feeConfirmSettingInitialPrice = true
|
||||
feeConfirmUnitPrice.value = snapshot.unitPrice
|
||||
feeConfirmSettingInitialPrice = false
|
||||
}
|
||||
feeConfirmPreviewKey.value = feeConfirmSelectionKey.value
|
||||
if (initializePrice && !snapshot.priceMatched) ElMessage.warning('当前FBA价格未命中,请填写本次对客单价和人工定价原因')
|
||||
} catch (error) {
|
||||
if (serial !== feeConfirmPreviewSerial || !feeConfirmOpen.value) return
|
||||
feeConfirmReference.value = null
|
||||
ElMessage.error(getApiErrorDetails(error, '计费预览加载失败,请稍后重试').message)
|
||||
} finally {
|
||||
feeConfirmReferenceLoading.value = false
|
||||
if (serial === feeConfirmPreviewSerial) feeConfirmReferenceLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch([feeConfirmWeight, feeConfirmVolume, feeConfirmUnitPrice], () => {
|
||||
if (!feeConfirmOpen.value || confirmFeeSending.value || feeConfirmSettingInitialPrice) return
|
||||
cancelFeeConfirmPreview()
|
||||
feeConfirmReference.value = null
|
||||
if (!(Number(feeConfirmWeight.value) > 0) || !(Number(feeConfirmVolume.value) > 0)
|
||||
|| (feeConfirmUnitPrice.value != null && !(feeConfirmUnitPrice.value > 0))) return
|
||||
feeConfirmReferenceLoading.value = true
|
||||
feeConfirmPreviewTimer = setTimeout(() => { void refreshFeeConfirmPreview() }, 250)
|
||||
}, { flush: 'sync' })
|
||||
|
||||
watch(feeConfirmOpen, open => {
|
||||
if (!open) cancelFeeConfirmPreview()
|
||||
}, { flush: 'sync' })
|
||||
|
||||
watch(() => `${detail.value?.summary.tenantId}:${detail.value?.summary.inboundId}`, () => {
|
||||
feeConfirmOpen.value = false
|
||||
}, { flush: 'sync' })
|
||||
|
||||
function currentFeeConfirmAttempt(
|
||||
selectedWeightKg: number,
|
||||
selectedVolumeCbm: number,
|
||||
@@ -1831,6 +1878,11 @@ async function executeConfirmAndSendFee() {
|
||||
ElMessage.warning('对客单价必须大于 0')
|
||||
return
|
||||
}
|
||||
if (feeConfirmReferenceLoading.value || !feeConfirmReference.value
|
||||
|| feeConfirmPreviewKey.value !== feeConfirmSelectionKey.value) {
|
||||
ElMessage.warning('请等待当前计费重量和体积的预览更新后再发送')
|
||||
return
|
||||
}
|
||||
if (feeConfirmPriceChanged.value && feeConfirmPriceReason.value.trim().length < 2) {
|
||||
ElMessage.warning('人工修改参考单价后,请填写至少 2 个字符的调整原因')
|
||||
return
|
||||
@@ -2402,6 +2454,7 @@ onMounted(() => {
|
||||
if (!isDetailRoute.value) void loadOrders()
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
cancelFeeConfirmPreview()
|
||||
ordersRequestSerial += 1
|
||||
statusCountsRequestSerial += 1
|
||||
detailAssociationRequestSerial += 1
|
||||
@@ -2418,7 +2471,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
|
||||
<template>
|
||||
<AdminPage class="admin-workbench admin-density">
|
||||
<section class="business-page unified-orders-page admin-density">
|
||||
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="feeConfirmOpen" :title="orderDialogTitle('编辑计费数据', detail?.summary.inboundCode)" confirm-text="确认并发送" :confirm-loading="confirmFeeSending || feeConfirmReferenceLoading" width="min(920px, 94vw)" fullscreenable :teleported="false" @confirm="executeConfirmAndSendFee">
|
||||
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="feeConfirmOpen" :title="orderDialogTitle('编辑计费数据', detail?.summary.inboundCode)" confirm-text="确认并发送" :confirm-loading="confirmFeeSending" :confirm-disabled="feeConfirmReferenceLoading || !feeConfirmReference" width="min(920px, 94vw)" fullscreenable :teleported="false" @confirm="executeConfirmAndSendFee">
|
||||
<el-alert class="fee-confirm-price-alert" title="价格表单价仅作为参考;本次对客单价可人工修改,客户将一次确认计费量、单价和费用合计。" type="info" :closable="false" show-icon />
|
||||
<section class="density-dialog-section" data-tone="work">
|
||||
<h3 class="density-dialog-section__title">本次计费数据 <small>毛重、体积和对客单价可修改</small></h3>
|
||||
@@ -2431,15 +2484,15 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="fee-confirm-weight">计费毛重</label></th>
|
||||
<td><div class="density-table-editor density-table-editor--outlined"><el-input-number id="fee-confirm-weight" v-model="feeConfirmWeight" :min="0" :precision="3" :controls="false" placeholder="请输入计费毛重" /><span>kg</span></div></td>
|
||||
<td><div class="density-table-editor density-table-editor--outlined"><el-input-number id="fee-confirm-weight" v-model="feeConfirmWeight" :min="0" :precision="3" :controls="false" :disabled="confirmFeeSending" placeholder="请输入计费毛重" /><span>kg</span></div></td>
|
||||
<th scope="row"><label for="fee-confirm-volume">计费体积</label></th>
|
||||
<td><div class="density-table-editor density-table-editor--outlined"><el-input-number id="fee-confirm-volume" v-model="feeConfirmVolume" :min="0" :precision="6" :controls="false" placeholder="请输入计费体积" /><span>m³</span></div></td>
|
||||
<td><div class="density-table-editor density-table-editor--outlined"><el-input-number id="fee-confirm-volume" v-model="feeConfirmVolume" :min="0" :precision="6" :controls="false" :disabled="confirmFeeSending" placeholder="请输入计费体积" /><span>m³</span></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">价格表参考单价</th>
|
||||
<td>{{ feeConfirmPriceMissing ? '未命中' : feeConfirmReference?.unitPrice == null ? '正在读取参考单价' : `${formatNumber(Number(feeConfirmReference.unitPrice), 4)} ${feeConfirmPriceUnit}` }}</td>
|
||||
<td>{{ feeConfirmPriceMissing ? '未命中' : feeConfirmPriceReference?.unitPrice == null ? '正在读取参考单价' : `${formatNumber(feeConfirmPriceReference.unitPrice, 4)} ${feeConfirmPriceUnit}` }}</td>
|
||||
<th scope="row"><label for="fee-confirm-unit-price">本次对客单价</label></th>
|
||||
<td><div class="density-table-editor density-table-editor--outlined"><el-input-number id="fee-confirm-unit-price" v-model="feeConfirmUnitPrice" :min="0" :precision="4" :controls="false" :disabled="feeConfirmReferenceLoading" placeholder="请输入对客单价" /><span>{{ feeConfirmPriceUnit }}</span></div></td>
|
||||
<td><div class="density-table-editor density-table-editor--outlined"><el-input-number id="fee-confirm-unit-price" v-model="feeConfirmUnitPrice" :min="0" :precision="4" :controls="false" :disabled="feeConfirmReferenceLoading || confirmFeeSending" placeholder="请输入对客单价" /><span>{{ feeConfirmPriceUnit }}</span></div></td>
|
||||
</tr>
|
||||
<tr v-if="feeConfirmReference">
|
||||
<th scope="row">参考计费量</th><td>{{ formatNumber(Number(feeConfirmReference.chargeableQuantity), feeConfirmReference.roundingScale) }} {{ feeConfirmReference.chargeUnit }}</td>
|
||||
|
||||
Reference in New Issue
Block a user