Release/test 合并上生产 #2

Merged
admin merged 736 commits from release/test into main 2026-09-17 11:50:56 +08:00
7 changed files with 136 additions and 24 deletions
Showing only changes of commit 82f0479928 - Show all commits
@@ -33,6 +33,7 @@ describe('adminNavigation', () => {
'订单测量',
'测量数据审核',
'库位管理',
'托盘管理',
'整柜业务',
])
expect(adminNavigation.find((item) => item.key === 'customer-service')?.children?.map((item) => item.title)).toEqual(['异常订单', '客户回访'])
+1
View File
@@ -68,6 +68,7 @@ export const adminNavigation: AdminNavigationNode[] = [
{ key: 'inbound-dedup', title: '订单测量', icon: ClipboardList, path: '/inbounds/measurements', permission: 'platform:firstleg:inbound:measure:view' },
{ key: 'inbound-measure', title: '测量数据审核', icon: FileCheck2, path: '/inbounds/measurement-reviews', permission: 'firstleg:inbound:measurement-review' },
{ key: 'locations', title: '库位管理', icon: WarehouseIcon, path: '/master-data/locations', permission: 'platform:location:list' },
{ key: 'pallets', title: '托盘管理', icon: WarehouseIcon, path: '/master-data/pallets', permission: 'platform:pallet:list' },
{ key: 'domestic-containers', title: '整柜业务', icon: WarehouseIcon, path: '/domestic-containers', permission: 'platform:domestic-container:list' },
],
},
@@ -93,6 +93,18 @@ describe('inbound weight OCR API', () => {
)
})
it('passes an abort signal without changing the frozen pallet endpoint parameters', async () => {
vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: [] })
const controller = new AbortController()
await getWeightPalletOptions('10001', '20001', controller.signal)
expect(http.get).toHaveBeenCalledWith(
'/platform/v1/first-leg/inbounds/20001/weight-pallet-options',
{ params: { tenantId: '10001' }, signal: controller.signal },
)
})
it('registers, corrects, and confirms a weight result using the implemented contracts', async () => {
vi.mocked(http.post).mockResolvedValue({ code: '200', message: 'success', data: { id: '1' } })
vi.mocked(http.put).mockResolvedValue({ code: '200', message: 'success', data: { id: '1', version: 1 } })
+2 -2
View File
@@ -60,10 +60,10 @@ export async function getWeightWorkspace(tenantId: string, inboundId: string) {
return unwrapApiResponse(response)
}
export async function getWeightPalletOptions(tenantId: string, inboundId: string) {
export async function getWeightPalletOptions(tenantId: string, inboundId: string, signal?: AbortSignal) {
const response = await http.get<ApiEnvelope<WeightPalletOption[]>, ApiEnvelope<WeightPalletOption[]>>(
`${measurementBasePath}/${segment(inboundId)}/weight-pallet-options`,
{ params: { tenantId } },
{ params: { tenantId }, ...(signal ? { signal } : {}) },
)
return unwrapApiResponse(response)
}
@@ -352,6 +352,64 @@ describe('WeightOcrPanel confirmation behavior', () => {
expect(getWeightPalletOptions).not.toHaveBeenCalled()
})
it('keeps historical pallet snapshots visible without requesting candidates for a read-only operator', async () => {
const wrapper = mountPanel(['platform:firstleg:inbound:measure:view'])
await flushPromises()
expect(getWeightWorkspace).toHaveBeenCalledOnce()
expect(getWeightPalletOptions).not.toHaveBeenCalled()
expect(wrapper.find('.pallet-picker .el-select').exists()).toBe(false)
expect(wrapper.text()).toContain('木托盘')
expect(wrapper.text()).toContain('12.000 KG')
})
it('does not request pallet candidates when the inbound status is not editable', async () => {
vi.mocked(getWeightWorkspace).mockResolvedValue({ ...workspace, processStatus: 'MEASURE_PENDING' })
mountPanel()
await flushPromises()
expect(getWeightPalletOptions).not.toHaveBeenCalled()
})
it('virtualizes one thousand pallet candidates instead of rendering one thousand option nodes', async () => {
vi.mocked(getWeightPalletOptions).mockResolvedValue(Array.from({ length: 1_000 }, (_, index) => ({
palletId: String(60_000 + index),
palletCode: `TP-${index}`,
palletType: `托盘类型 ${index}`,
capacityCbm: 1.2,
tareWeightKg: 12,
})))
const wrapper = mountPanel()
await flushPromises()
await wrapper.find('.pallet-picker .el-select').trigger('click')
await flushPromises()
const renderedOptions = document.body.querySelectorAll('.el-select-dropdown__item')
expect(renderedOptions.length).toBeGreaterThan(0)
expect(renderedOptions.length).toBeLessThan(100)
})
it('aborts an in-flight pallet request and ignores its late result when the panel closes', async () => {
let resolveOptions: ((value: Awaited<ReturnType<typeof getWeightPalletOptions>>) => void) | undefined
vi.mocked(getWeightPalletOptions).mockImplementationOnce(() => new Promise((resolve) => {
resolveOptions = resolve
}))
const wrapper = mountPanel()
await flushPromises()
const signal = vi.mocked(getWeightPalletOptions).mock.calls[0]?.[2]
wrapper.unmount()
resolveOptions?.([{
palletId: 'late', palletCode: 'TP-LATE', palletType: '过期托盘', capacityCbm: 1, tareWeightKg: 10,
}])
await flushPromises()
expect(signal?.aborted).toBe(true)
expect(document.body.textContent).not.toContain('过期托盘')
})
it('keeps a low-confidence OCR result focused on the editable weight', async () => {
vi.mocked(getWeightWorkspace).mockResolvedValue({
...workspace,
@@ -55,8 +55,10 @@ describe('WeightOcrPanel contract', () => {
expect(source).toContain("option.palletType || '未记录类型'")
})
it('keeps pallet type, tare weight, and capacity in one select line', () => {
expect(source).toContain(':label="palletOptionLabel(option)"')
it('keeps pallet type, tare weight, and capacity in one virtualized select line', () => {
expect(source).toContain('<el-select-v2')
expect(source).toContain(':options="virtualPalletOptions"')
expect(source).toContain('label: palletOptionLabel(option)')
expect(source).toContain("`${option.palletType || '未记录类型'} · 皮重 ${formatWeight(option.tareWeightKg)} · 容量 ${option.capacityCbm} m³`")
expect(source).not.toContain('<small v-if="selectedPallet">')
expect(source).toContain('width: clamp(360px, 36vw, 520px)')
@@ -67,6 +69,9 @@ describe('WeightOcrPanel contract', () => {
expect(source).toContain("platform:firstleg:inbound:weight:edit")
expect(source).toContain("platform:firstleg:inbound:weight:confirm")
expect(source).toContain("new Set(['WAIT_MEASURE', 'MEASURING', 'MEASURE_REJECTED'])")
expect(source).toContain('&& canEdit.value')
expect(source).toContain('&& editable.value')
expect(source).toContain('cancelPalletOptionsLoad()')
expect(source).toContain("workspace.value?.processStatus === 'MEASURE_PENDING'")
expect(source).toContain('v-if="canEdit && !reviewMode"')
expect(source).toContain('v-if="canEdit && editable && !reviewMode"')
@@ -59,6 +59,8 @@ const previewResultId = ref('')
const previewSnapshot = ref<WeightResultResource | null>(null)
const weightDrafts = ref<Record<string, { value: number | null; unit: 'KG' | 'LB'; reason: string }>>({})
let loadSequence = 0
let palletLoadSequence = 0
let palletAbortController: AbortController | null = null
const editableStatuses = new Set(['WAIT_MEASURE', 'MEASURING', 'MEASURE_REJECTED'])
const minimumWeight = { KG: 0.001, LB: 0.002 } as const
@@ -68,6 +70,18 @@ const canEdit = computed(() => access.can('platform:firstleg:inbound:weight:edit
const canConfirm = computed(() => access.can('platform:firstleg:inbound:weight:confirm'))
const canView = computed(() => access.can('platform:firstleg:inbound:measure:view'))
const editable = computed(() => Boolean(workspace.value && editableStatuses.has(workspace.value.processStatus)))
const canLoadPalletOptions = computed(() => Boolean(
!props.reviewMode
&& customerId.value
&& props.inboundId
&& canView.value
&& canEdit.value
&& editable.value,
))
const virtualPalletOptions = computed(() => palletOptions.value.map((option) => ({
label: palletOptionLabel(option),
value: option.palletId,
})))
const reviewCorrectionEnabled = computed(() => Boolean(
props.reviewMode
&& props.correctionMode
@@ -212,32 +226,44 @@ async function loadWorkspace() {
}
async function loadPalletOptions() {
if (props.reviewMode) {
if (!canLoadPalletOptions.value) {
cancelPalletOptionsLoad()
palletOptions.value = []
selectedPalletId.value = ''
palletError.value = null
return
}
if (!customerId.value || !props.inboundId || !canView.value) {
palletOptions.value = []
selectedPalletId.value = ''
return
}
cancelPalletOptionsLoad()
const sequence = ++palletLoadSequence
const controller = new AbortController()
palletAbortController = controller
palletLoading.value = true
palletError.value = null
try {
const result = await getWeightPalletOptions(customerId.value, props.inboundId)
const result = await getWeightPalletOptions(customerId.value, props.inboundId, controller.signal)
if (sequence !== palletLoadSequence || controller.signal.aborted) return
palletOptions.value = result
if (!result.some((option) => option.palletId === selectedPalletId.value)) selectedPalletId.value = ''
} catch (error) {
if (sequence !== palletLoadSequence || controller.signal.aborted) return
palletOptions.value = []
selectedPalletId.value = ''
palletError.value = getApiErrorDetails(error, '可选托盘加载失败,请稍后重试')
} finally {
palletLoading.value = false
if (sequence === palletLoadSequence) {
palletLoading.value = false
if (palletAbortController === controller) palletAbortController = null
}
}
}
function cancelPalletOptionsLoad() {
++palletLoadSequence
palletAbortController?.abort()
palletAbortController = null
palletLoading.value = false
}
function chooseFile() {
if (!editable.value) {
ElMessage.warning('当前入库状态不允许上传称重图片')
@@ -397,17 +423,32 @@ watch(
previewOpen.value = false
releasePreview()
void loadWorkspace()
void loadPalletOptions()
},
{ immediate: true },
)
watch(
() => [customerId.value, props.inboundId, canLoadPalletOptions.value] as const,
() => {
cancelPalletOptionsLoad()
palletOptions.value = []
selectedPalletId.value = ''
palletError.value = null
if (canLoadPalletOptions.value) void loadPalletOptions()
},
)
watch(previewOpen, (open) => {
if (!open) {
releasePreview()
}
})
onBeforeUnmount(releasePreview)
onBeforeUnmount(() => {
cancelPalletOptionsLoad()
palletOptions.value = []
selectedPalletId.value = ''
releasePreview()
})
defineExpose({ reload: loadWorkspace })
</script>
@@ -424,22 +465,16 @@ defineExpose({ reload: loadWorkspace })
<div class="weight-summary"><span>货物重量</span><strong>{{ formatWeight(workspace.actualNetWeightKg) }}</strong></div>
</div>
<div class="pallet-picker">
<el-select
v-if="!reviewMode"
<el-select-v2
v-if="canEdit && !reviewMode"
v-model="selectedPalletId"
:options="virtualPalletOptions"
aria-label="选择称重托盘"
placeholder="选择称重托盘"
filterable
:loading="palletLoading"
:disabled="loading || uploading || !editable || !canEdit || Boolean(palletError)"
>
<el-option
v-for="option in palletOptions"
:key="option.palletId"
:label="palletOptionLabel(option)"
:value="option.palletId"
/>
</el-select>
/>
</div>
<input
v-if="!reviewMode"