Merge branch 'release/test' of http://git.buggoglobal.com/html/admin-web into release/test

This commit is contained in:
2026-09-04 14:41:31 +08:00
4 changed files with 56 additions and 46 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ describe('inbound measurement API', () => {
await expect(getInboundMeasurement('10001', '7001')).resolves.toEqual(detail) await expect(getInboundMeasurement('10001', '7001')).resolves.toEqual(detail)
expect(http.get).toHaveBeenCalledWith('/platform/v1/first-leg/inbounds/7001/measurement', { expect(http.get).toHaveBeenCalledWith('/platform/v1/first-leg/inbounds/7001/measurement', {
params: { tenantId: '10001' }, params: { tenantId: '10001', requestAt: expect.any(Number) },
}) })
}) })
+3 -1
View File
@@ -37,7 +37,9 @@ export async function getInboundMeasurement(customerId: string, inboundId: strin
// items[].boxCode, machineNo, deviceMeasuredAt. Keep the response aligned with the backend OpenAPI. // items[].boxCode, machineNo, deviceMeasuredAt. Keep the response aligned with the backend OpenAPI.
const response = await http.get<ApiEnvelope<InboundMeasurementDetail>, ApiEnvelope<InboundMeasurementDetail>>( const response = await http.get<ApiEnvelope<InboundMeasurementDetail>, ApiEnvelope<InboundMeasurementDetail>>(
measurementPath(inboundId), measurementPath(inboundId),
{ params: { tenantId: customerId } }, // Calculation-mode saves use a sibling PUT endpoint. A cache may otherwise keep
// returning the pre-save measurement and make a successful update look failed.
{ params: { tenantId: customerId, requestAt: Date.now() } },
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
@@ -323,6 +323,7 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain('仓库修改外箱(宽)') expect(source).toContain('仓库修改外箱(宽)')
expect(source).toContain('仓库修改外箱(高)') expect(source).toContain('仓库修改外箱(高)')
expect(source).toContain('saveWarehouseOuterDimensions') expect(source).toContain('saveWarehouseOuterDimensions')
expect(source).toContain("'MEASURE_REJECTED', 'WAIT_SPLIT'")
expect(source).not.toContain('请至少输入一项需要修改的外箱尺寸') expect(source).not.toContain('请至少输入一项需要修改的外箱尺寸')
expect(source).toContain(".map((value) => value == null || value === '' ? null : Number(value))") expect(source).toContain(".map((value) => value == null || value === '' ? null : Number(value))")
expect(source).toContain('相关数值已重新计算') expect(source).toContain('相关数值已重新计算')
@@ -366,12 +367,11 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain("updateMeasurementSectionMode('actual', $event)") expect(source).toContain("updateMeasurementSectionMode('actual', $event)")
expect(source).not.toContain("updateMeasurementSectionMode('outer', $event)") expect(source).not.toContain("updateMeasurementSectionMode('outer', $event)")
expect(source.match(/@change="selectAndSaveOuterMeasurementMode\(\$event\)"/g)).toHaveLength(2) expect(source.match(/@change="selectAndSaveOuterMeasurementMode\(\$event\)"/g)).toHaveLength(2)
expect(source).toContain('@visible-change="handleOuterModeDropdownVisibleChange($event)"') expect(source).not.toContain('@visible-change="handleOuterModeDropdownVisibleChange($event)"')
expect(source.match(/:disabled="Boolean\(measurementStrategySavingId\) \|\| !can\('platform:firstleg:inbound:measure:edit'\)"/g)).toHaveLength(2) expect(source.match(/:disabled="Boolean\(measurementStrategySavingId\) \|\| !can\('platform:firstleg:inbound:measure:edit'\)"/g)).toHaveLength(2)
expect(source).toContain('const previousMode = measurementSectionModes.outer') expect(source).toContain('const previousMode = measurementSectionModes.outer')
expect(source).toContain('if (!saved) measurementSectionModes.outer = previousMode') expect(source).toContain('if (!saved) measurementSectionModes.outer = previousMode')
expect(source).toContain('const selectedSameMode = outerModeAtDropdownOpen.value === measurementSectionModes.outer') expect(source).not.toContain('handleOuterModeDropdownVisibleChange')
expect(source).toContain('await nextTick()')
expect(source).toContain('const saved = await updateMeasurementCalculationMode(null, mode, ruleId)') expect(source).toContain('const saved = await updateMeasurementCalculationMode(null, mode, ruleId)')
expect(source).toContain('calculatedMeasurementRow(row, measurementSectionModes.customer, true)') expect(source).toContain('calculatedMeasurementRow(row, measurementSectionModes.customer, true)')
expect(source).toContain('calculatedMeasurementRow(source, measurementSectionModes.outer)') expect(source).toContain('calculatedMeasurementRow(source, measurementSectionModes.outer)')
@@ -381,6 +381,7 @@ describe('UnifiedOrdersView contract', () => {
expect(source).not.toContain("!customerSource && mode !== 'ACTUAL'") expect(source).not.toContain("!customerSource && mode !== 'ACTUAL'")
expect(source).toContain('rawDimensions.map((value) => value + rule.edgeAdditionCm * 2)') expect(source).toContain('rawDimensions.map((value) => value + rule.edgeAdditionCm * 2)')
expect(source).toContain('matchedSpecificationCoefficient(chargeRule, calculationSource)') expect(source).toContain('matchedSpecificationCoefficient(chargeRule, calculationSource)')
expect(source).toContain(".find((item) => item.id === boundChargeRuleId && item.status === 'ACTIVE')")
expect(source).toContain('listCustomerChargeRules(customerId, tenantId)') expect(source).toContain('listCustomerChargeRules(customerId, tenantId)')
expect(source).toContain('matchedDimensions.map((value) => roundedUp(value * specificationCoefficient, 3))') expect(source).toContain('matchedDimensions.map((value) => roundedUp(value * specificationCoefficient, 3))')
expect(source).toContain('const selectedMode: MeasurementCalculationMode = measurementSectionModes.outer') expect(source).toContain('const selectedMode: MeasurementCalculationMode = measurementSectionModes.outer')
@@ -405,9 +406,14 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain('outerLengthCm: item.outerLengthCm') expect(source).toContain('outerLengthCm: item.outerLengthCm')
expect(source).toContain('outerWidthCm: item.outerWidthCm') expect(source).toContain('outerWidthCm: item.outerWidthCm')
expect(source).toContain('outerHeightCm: item.outerHeightCm') expect(source).toContain('outerHeightCm: item.outerHeightCm')
expect(source).toContain("String(item.chargeRuleId) === String(activeRule!.id)") expect(source).toContain('const savedDraft = await saveInboundMeasurementDraft')
expect(source).toContain('item.calculatedOuterLengthCm, item.calculatedOuterWidthCm, item.calculatedOuterHeightCm') expect(source).toContain('version: savedDraft.version')
expect(source).toContain('if (dimensions.every((dimension) => dimension == null)) return true') expect(source).toContain('waitForSavedMeasurementVersion(')
expect(source).toContain('Number(refreshed.version) >= savedVersion')
expect(source).toContain('setTimeout(resolve, 250)')
expect(source).toContain("chargeRuleId: mode === 'MULTIPLY' ? activeRule!.id : null")
expect(source).toContain('calculatedOuterLengthCm: null')
expect(source).not.toContain('计算策略保存后回读不一致,请刷新后重试')
expect(source).toContain('已保存并绑定计算规则,体积已重新计算') expect(source).toContain('已保存并绑定计算规则,体积已重新计算')
expect(source).toContain('const measuredGrossWeight = Number(measurementDetail.value?.actualGrossWeightKg)') expect(source).toContain('const measuredGrossWeight = Number(measurementDetail.value?.actualGrossWeightKg)')
expect(source).toContain('const measuredNetWeight = Number(measurementDetail.value?.actualNetWeightKg)') expect(source).toContain('const measuredNetWeight = Number(measurementDetail.value?.actualNetWeightKg)')
@@ -188,7 +188,6 @@ type MeasurementCalculationMode = 'ACTUAL' | 'ADD' | 'MULTIPLY'
const measurementSectionModes = reactive<Record<MeasurementSection, MeasurementCalculationMode>>({ const measurementSectionModes = reactive<Record<MeasurementSection, MeasurementCalculationMode>>({
customer: 'ACTUAL', outer: 'ACTUAL', actual: 'ACTUAL', customer: 'ACTUAL', outer: 'ACTUAL', actual: 'ACTUAL',
}) })
const outerModeAtDropdownOpen = ref<MeasurementCalculationMode | null>(null)
const cargoImagePreviewUrls = new Set<string>() const cargoImagePreviewUrls = new Set<string>()
const measurementDialogOpen = ref(false) const measurementDialogOpen = ref(false)
const openWarehouseSuggestionOnMeasurementLoad = ref(false) const openWarehouseSuggestionOnMeasurementLoad = ref(false)
@@ -375,7 +374,10 @@ function calculatedMeasurementRow(
const matchedDimensions = mode === 'ADD' && rule const matchedDimensions = mode === 'ADD' && rule
? rawDimensions.map((value) => value + rule.edgeAdditionCm * 2) ? rawDimensions.map((value) => value + rule.edgeAdditionCm * 2)
: rawDimensions : 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 const specificationCoefficient = mode === 'MULTIPLY' && chargeRule
? matchedSpecificationCoefficient(chargeRule, calculationSource) ? matchedSpecificationCoefficient(chargeRule, calculationSource)
: 1 : 1
@@ -460,19 +462,17 @@ async function selectAndSaveOuterMeasurementMode(mode: MeasurementCalculationMod
if (!saved) measurementSectionModes.outer = previousMode if (!saved) measurementSectionModes.outer = previousMode
} }
async function handleOuterModeDropdownVisibleChange(visible: boolean) { async function waitForSavedMeasurementVersion(tenantId: string, inboundId: string, savedVersion: number) {
if (visible) { for (let attempt = 0; attempt < 6; attempt += 1) {
outerModeAtDropdownOpen.value = measurementSectionModes.outer try {
return const refreshed = await getInboundMeasurement(tenantId, inboundId)
} if (Number(refreshed.version) >= savedVersion) return refreshed
// Element Plus may emit dropdown-close before change. Wait until the change handler has } catch {
// updated the mode/saving state, otherwise one click can submit both the old and new mode. // 保存结果已由 PUT 确认,短暂的详情读取失败只影响同步等待。
await nextTick() }
const selectedSameMode = outerModeAtDropdownOpen.value === measurementSectionModes.outer await new Promise((resolve) => setTimeout(resolve, 250))
outerModeAtDropdownOpen.value = null
if (selectedSameMode && !measurementStrategySavingId.value) {
void selectAndSaveOuterMeasurementMode(measurementSectionModes.outer)
} }
return null
} }
const statusTabs = computed(() => [ const statusTabs = computed(() => [
@@ -895,9 +895,6 @@ async function updateMeasurementCalculationMode(row: Record<string, unknown> | n
const targetIds = new Set(measurement.items const targetIds = new Set(measurement.items
.filter((item) => !row || (itemId ? String(item.id) === itemId : normalizedIdentifier(item.itemNo) === itemNo)) .filter((item) => !row || (itemId ? String(item.id) === itemId : normalizedIdentifier(item.itemNo) === itemNo))
.map((item) => String(item.id))) .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) { if (!targetIds.size) {
ElMessage.warning('当前数据尚未关联实测明细,暂不能设置计算方式') ElMessage.warning('当前数据尚未关联实测明细,暂不能设置计算方式')
return false return false
@@ -921,7 +918,7 @@ async function updateMeasurementCalculationMode(row: Record<string, unknown> | n
ElMessage.warning('该客户尚未启用系数配置,请先到客户计费规则中设置默认规则') ElMessage.warning('该客户尚未启用系数配置,请先到客户计费规则中设置默认规则')
return false return false
} }
await saveInboundMeasurementDraft(String(summary.tenantId), String(summary.inboundId), { const savedDraft = await saveInboundMeasurementDraft(String(summary.tenantId), String(summary.inboundId), {
measurementId: String(measurement.measurementId), measurementId: String(measurement.measurementId),
version: measurement.version, version: measurement.version,
actualGoodsName: measurement.actualGoodsName, actualGoodsName: measurement.actualGoodsName,
@@ -951,25 +948,28 @@ async function updateMeasurementCalculationMode(row: Record<string, unknown> | n
remark: item.remark, remark: item.remark,
})), })),
}) })
measurementLoadedKey.value = '' // PUT 返回的新版本已经代表事务保存成功。详情查询可能经过读副本并短暂返回旧版本,
await loadMeasurementDetails() // 不能用紧随其后的旧数据否定本次成功保存;先按新版本和本次规则更新本地视图。
const persistedItems = measurementDetail.value?.items measurementDetail.value = {
.filter((item) => targetItemNos.has(normalizedIdentifier(item.itemNo))) || [] ...measurement,
const persisted = persistedItems.length === targetItemNos.size && persistedItems.every((item) => ( version: savedDraft.version,
item.coefficientMode === mode measurementStatus: savedDraft.status,
&& (mode !== 'MULTIPLY' || String(item.chargeRuleId) === String(activeRule!.id)) items: measurement.items.map((item) => targetIds.has(String(item.id)) ? {
&& [item.calculatedOuterLengthCm, item.calculatedOuterWidthCm, item.calculatedOuterHeightCm] ...item,
.every((value, index, dimensions) => { coefficientMode: mode,
// 旧服务实例可能尚未返回新版计算结果字段;计算方式已回读成功时不能误判并回滚下拉。 chargeRuleId: mode === 'MULTIPLY' ? activeRule!.id : null,
if (dimensions.every((dimension) => dimension == null)) return true calculatedOuterLengthCm: null,
if (value == null) return false calculatedOuterWidthCm: null,
const expected = calculatedMeasurementRow(item as unknown as Record<string, unknown>, mode) calculatedOuterHeightCm: null,
return Math.abs(Number(value) - Number([expected.lengthCm, expected.widthCm, expected.heightCm][index])) < 0.001 } : item),
})
))
if (!persisted) {
throw new Error('计算策略保存后回读不一致,请刷新后重试')
} }
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 ? '当前数据' : '全部测量数据'}已保存并绑定计算规则,体积已重新计算`) ElMessage.success(`${row ? '当前数据' : '全部测量数据'}已保存并绑定计算规则,体积已重新计算`)
return true return true
} catch (error) { } catch (error) {
@@ -985,7 +985,9 @@ async function updateMeasurementCalculationMode(row: Record<string, unknown> | n
async function saveWarehouseOuterDimensions(row: Record<string, unknown>) { async function saveWarehouseOuterDimensions(row: Record<string, unknown>) {
const summary = detail.value?.summary const summary = detail.value?.summary
const measurement = measurementDetail.value 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 || '') const currentStatus = editableMeasurementStatuses.has(measurement?.processStatus || '')
? measurement!.processStatus ? measurement!.processStatus
: summary?.mainStatus || measurement?.processStatus || '' : summary?.mainStatus || measurement?.processStatus || ''
@@ -2125,7 +2127,7 @@ onBeforeUnmount(() => {
</section> </section>
<div class="measurement-rule-selector"> <div class="measurement-rule-selector">
<span class="measurement-rule-selector__label">箱规计算规则选择</span> <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="ACTUAL" />
<el-option label="箱规尺寸" value="ADD" /> <el-option label="箱规尺寸" value="ADD" />
<el-option label="箱规系数" value="MULTIPLY" :disabled="multiplyModeDisabled" /> <el-option label="箱规系数" value="MULTIPLY" :disabled="multiplyModeDisabled" />