Merge remote-tracking branch 'origin/release/test' into dev

# Conflicts:
#	AGENTS.md
#	src/modules/container-loading/pages/GlobalSmartLoadingView.vue
#	src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue
This commit is contained in:
2026-09-04 16:21:32 +08:00
26 changed files with 278 additions and 233 deletions
@@ -189,7 +189,6 @@ type MeasurementCalculationMode = 'ACTUAL' | 'ADD' | 'MULTIPLY'
const measurementSectionModes = reactive<Record<MeasurementSection, MeasurementCalculationMode>>({
customer: 'ACTUAL', outer: 'ACTUAL', actual: 'ACTUAL',
})
const outerModeAtDropdownOpen = ref<MeasurementCalculationMode | null>(null)
const cargoImagePreviewUrls = new Set<string>()
const measurementDialogOpen = ref(false)
const openWarehouseSuggestionOnMeasurementLoad = ref(false)
@@ -201,16 +200,16 @@ const feeConfirmRows = computed(() => {
const summary = detail.value?.summary
const measuredGrossWeight = Number(measurementDetail.value?.actualGrossWeightKg)
const measuredNetWeight = Number(measurementDetail.value?.actualNetWeightKg)
const measuredVolume = Number(measurementDetail.value?.actualVolumeCbm)
const actualWeight = measuredGrossWeight > 0
? measuredGrossWeight
: measuredNetWeight > 0 ? measuredNetWeight : null
const actualVolume = measuredVolume > 0 ? measuredVolume : null
const actualTotals = measurementTotals(actualMeasurementRows.value)
const actualWeight = Number(actualTotals.gross) > 0
? actualTotals.gross
: measuredGrossWeight > 0 ? measuredGrossWeight : measuredNetWeight > 0 ? measuredNetWeight : null
const actualVolume = Number(actualTotals.volume) > 0 ? actualTotals.volume : null
const customerTotals = measurementTotals(customerMeasurementRows.value)
return [
{ source: '客户下单数据', count: customerTotals.count, weight: customerTotals.gross, volume: customerTotals.volume ?? summary?.expectedVolumeCbm },
{ source: '外箱规数据', count: measurementTotals(outerMeasurementRows.value).count, weight: measurementTotals(outerMeasurementRows.value).gross, volume: measurementTotals(outerMeasurementRows.value).volume },
{ source: '实际测量数据', count: measurementDetail.value?.actualPackageCount, weight: actualWeight, volume: actualVolume },
{ source: '实际测量数据', count: actualTotals.count, weight: actualWeight, volume: actualVolume },
]
})
const smartLoadingOpen = ref(false)
@@ -376,7 +375,10 @@ function calculatedMeasurementRow(
const matchedDimensions = mode === 'ADD' && rule
? rawDimensions.map((value) => value + rule.edgeAdditionCm * 2)
: rawDimensions
const chargeRule = activeMeasurementChargeRule.value
const boundChargeRuleId = String(source.chargeRuleId || '')
const chargeRule = measurementChargeRules.value
.find((item) => item.id === boundChargeRuleId && item.status === 'ACTIVE')
|| activeMeasurementChargeRule.value
const specificationCoefficient = mode === 'MULTIPLY' && chargeRule
? matchedSpecificationCoefficient(chargeRule, calculationSource)
: 1
@@ -461,19 +463,17 @@ async function selectAndSaveOuterMeasurementMode(mode: MeasurementCalculationMod
if (!saved) measurementSectionModes.outer = previousMode
}
async function handleOuterModeDropdownVisibleChange(visible: boolean) {
if (visible) {
outerModeAtDropdownOpen.value = measurementSectionModes.outer
return
}
// Element Plus may emit dropdown-close before change. Wait until the change handler has
// updated the mode/saving state, otherwise one click can submit both the old and new mode.
await nextTick()
const selectedSameMode = outerModeAtDropdownOpen.value === measurementSectionModes.outer
outerModeAtDropdownOpen.value = null
if (selectedSameMode && !measurementStrategySavingId.value) {
void selectAndSaveOuterMeasurementMode(measurementSectionModes.outer)
async function waitForSavedMeasurementVersion(tenantId: string, inboundId: string, savedVersion: number) {
for (let attempt = 0; attempt < 6; attempt += 1) {
try {
const refreshed = await getInboundMeasurement(tenantId, inboundId)
if (Number(refreshed.version) >= savedVersion) return refreshed
} catch {
// 保存结果已由 PUT 确认,短暂的详情读取失败只影响同步等待。
}
await new Promise((resolve) => setTimeout(resolve, 250))
}
return null
}
const statusTabs = computed(() => [
@@ -896,9 +896,6 @@ async function updateMeasurementCalculationMode(row: Record<string, unknown> | n
const targetIds = new Set(measurement.items
.filter((item) => !row || (itemId ? String(item.id) === itemId : normalizedIdentifier(item.itemNo) === itemNo))
.map((item) => String(item.id)))
const targetItemNos = new Set(measurement.items
.filter((item) => targetIds.has(String(item.id)))
.map((item) => normalizedIdentifier(item.itemNo)))
if (!targetIds.size) {
ElMessage.warning('当前数据尚未关联实测明细,暂不能设置计算方式')
return false
@@ -922,7 +919,7 @@ async function updateMeasurementCalculationMode(row: Record<string, unknown> | n
ElMessage.warning('该客户尚未启用系数配置,请先到客户计费规则中设置默认规则')
return false
}
await saveInboundMeasurementDraft(String(summary.tenantId), String(summary.inboundId), {
const savedDraft = await saveInboundMeasurementDraft(String(summary.tenantId), String(summary.inboundId), {
measurementId: String(measurement.measurementId),
version: measurement.version,
actualGoodsName: measurement.actualGoodsName,
@@ -952,25 +949,28 @@ async function updateMeasurementCalculationMode(row: Record<string, unknown> | n
remark: item.remark,
})),
})
measurementLoadedKey.value = ''
await loadMeasurementDetails()
const persistedItems = measurementDetail.value?.items
.filter((item) => targetItemNos.has(normalizedIdentifier(item.itemNo))) || []
const persisted = persistedItems.length === targetItemNos.size && persistedItems.every((item) => (
item.coefficientMode === mode
&& (mode !== 'MULTIPLY' || String(item.chargeRuleId) === String(activeRule!.id))
&& [item.calculatedOuterLengthCm, item.calculatedOuterWidthCm, item.calculatedOuterHeightCm]
.every((value, index, dimensions) => {
// 旧服务实例可能尚未返回新版计算结果字段;计算方式已回读成功时不能误判并回滚下拉。
if (dimensions.every((dimension) => dimension == null)) return true
if (value == null) return false
const expected = calculatedMeasurementRow(item as unknown as Record<string, unknown>, mode)
return Math.abs(Number(value) - Number([expected.lengthCm, expected.widthCm, expected.heightCm][index])) < 0.001
})
))
if (!persisted) {
throw new Error('计算策略保存后回读不一致,请刷新后重试')
// PUT 返回的新版本已经代表事务保存成功。详情查询可能经过读副本并短暂返回旧版本,
// 不能用紧随其后的旧数据否定本次成功保存;先按新版本和本次规则更新本地视图。
measurementDetail.value = {
...measurement,
version: savedDraft.version,
measurementStatus: savedDraft.status,
items: measurement.items.map((item) => targetIds.has(String(item.id)) ? {
...item,
coefficientMode: mode,
chargeRuleId: mode === 'MULTIPLY' ? activeRule!.id : null,
calculatedOuterLengthCm: null,
calculatedOuterWidthCm: null,
calculatedOuterHeightCm: null,
} : item),
}
measurementSectionModes.outer = mode
const refreshed = await waitForSavedMeasurementVersion(
String(summary.tenantId), String(summary.inboundId), savedDraft.version,
)
if (refreshed) measurementDetail.value = refreshed
measurementSectionModes.outer = mode
measurementLoadedKey.value = ''
ElMessage.success(`${row ? '当前数据' : '全部测量数据'}已保存并绑定计算规则,体积已重新计算`)
return true
} catch (error) {
@@ -986,7 +986,9 @@ async function updateMeasurementCalculationMode(row: Record<string, unknown> | n
async function saveWarehouseOuterDimensions(row: Record<string, unknown>) {
const summary = detail.value?.summary
const measurement = measurementDetail.value
const editableMeasurementStatuses = new Set(['WAIT_MEASURE', 'MEASURING', 'MEASURE_PENDING', 'MEASURE_REJECTED'])
const editableMeasurementStatuses = new Set([
'WAIT_MEASURE', 'MEASURING', 'MEASURE_PENDING', 'MEASURE_REJECTED', 'WAIT_SPLIT',
])
const currentStatus = editableMeasurementStatuses.has(measurement?.processStatus || '')
? measurement!.processStatus
: summary?.mainStatus || measurement?.processStatus || ''
@@ -1430,15 +1432,10 @@ async function openFeeConfirmDialog() {
const actualTotals = measurementTotals(actualMeasurementRows.value)
const measuredGrossWeight = Number(measurementDetail.value?.actualGrossWeightKg)
const measuredNetWeight = Number(measurementDetail.value?.actualNetWeightKg)
const actualVolume = Number(measurementDetail.value?.actualVolumeCbm)
feeConfirmWeight.value = measuredGrossWeight > 0
? measuredGrossWeight
: measuredNetWeight > 0
? measuredNetWeight
: Number(actualTotals.gross) > 0 ? Number(actualTotals.gross) : null
feeConfirmVolume.value = actualVolume > 0
? actualVolume
: Number(actualTotals.volume) > 0 ? Number(actualTotals.volume) : null
feeConfirmWeight.value = Number(actualTotals.gross) > 0
? Number(actualTotals.gross)
: measuredGrossWeight > 0 ? measuredGrossWeight : measuredNetWeight > 0 ? measuredNetWeight : null
feeConfirmVolume.value = Number(actualTotals.volume) > 0 ? Number(actualTotals.volume) : null
feeConfirmOpen.value = true
}
@@ -2140,7 +2137,7 @@ onBeforeUnmount(() => {
</section>
<div class="measurement-rule-selector">
<span class="measurement-rule-selector__label">箱规计算规则选择</span>
<el-select class="measurement-rule-selector__control" :model-value="measurementSectionModes.outer" aria-label="外箱规数据计算方式" :loading="Boolean(measurementStrategySavingId)" :disabled="Boolean(measurementStrategySavingId) || !can('platform:firstleg:inbound:measure:edit')" @change="selectAndSaveOuterMeasurementMode($event)" @visible-change="handleOuterModeDropdownVisibleChange($event)">
<el-select class="measurement-rule-selector__control" :model-value="measurementSectionModes.outer" aria-label="外箱规数据计算方式" :loading="Boolean(measurementStrategySavingId)" :disabled="Boolean(measurementStrategySavingId) || !can('platform:firstleg:inbound:measure:edit')" @change="selectAndSaveOuterMeasurementMode($event)">
<el-option label="实测原尺寸(不调整)" value="ACTUAL" />
<el-option label="箱规尺寸" value="ADD" />
<el-option label="箱规系数" value="MULTIPLY" :disabled="multiplyModeDisabled" />