优化
This commit is contained in:
@@ -33,6 +33,7 @@ describe('adminNavigation', () => {
|
|||||||
'订单测量',
|
'订单测量',
|
||||||
'测量数据审核',
|
'测量数据审核',
|
||||||
'库位管理',
|
'库位管理',
|
||||||
|
'托盘管理',
|
||||||
'整柜业务',
|
'整柜业务',
|
||||||
])
|
])
|
||||||
expect(adminNavigation.find((item) => item.key === 'customer-service')?.children?.map((item) => item.title)).toEqual(['异常订单', '客户回访'])
|
expect(adminNavigation.find((item) => item.key === 'customer-service')?.children?.map((item) => item.title)).toEqual(['异常订单', '客户回访'])
|
||||||
|
|||||||
@@ -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-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: '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: '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' },
|
{ 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 () => {
|
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.post).mockResolvedValue({ code: '200', message: 'success', data: { id: '1' } })
|
||||||
vi.mocked(http.put).mockResolvedValue({ code: '200', message: 'success', data: { id: '1', version: 1 } })
|
vi.mocked(http.put).mockResolvedValue({ code: '200', message: 'success', data: { id: '1', version: 1 } })
|
||||||
|
|||||||
@@ -60,10 +60,10 @@ export async function getWeightWorkspace(tenantId: string, inboundId: string) {
|
|||||||
return unwrapApiResponse(response)
|
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[]>>(
|
const response = await http.get<ApiEnvelope<WeightPalletOption[]>, ApiEnvelope<WeightPalletOption[]>>(
|
||||||
`${measurementBasePath}/${segment(inboundId)}/weight-pallet-options`,
|
`${measurementBasePath}/${segment(inboundId)}/weight-pallet-options`,
|
||||||
{ params: { tenantId } },
|
{ params: { tenantId }, ...(signal ? { signal } : {}) },
|
||||||
)
|
)
|
||||||
return unwrapApiResponse(response)
|
return unwrapApiResponse(response)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -352,6 +352,64 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
|||||||
expect(getWeightPalletOptions).not.toHaveBeenCalled()
|
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 () => {
|
it('keeps a low-confidence OCR result focused on the editable weight', async () => {
|
||||||
vi.mocked(getWeightWorkspace).mockResolvedValue({
|
vi.mocked(getWeightWorkspace).mockResolvedValue({
|
||||||
...workspace,
|
...workspace,
|
||||||
|
|||||||
@@ -55,8 +55,10 @@ describe('WeightOcrPanel contract', () => {
|
|||||||
expect(source).toContain("option.palletType || '未记录类型'")
|
expect(source).toContain("option.palletType || '未记录类型'")
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps pallet type, tare weight, and capacity in one select line', () => {
|
it('keeps pallet type, tare weight, and capacity in one virtualized select line', () => {
|
||||||
expect(source).toContain(':label="palletOptionLabel(option)"')
|
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).toContain("`${option.palletType || '未记录类型'} · 皮重 ${formatWeight(option.tareWeightKg)} · 容量 ${option.capacityCbm} m³`")
|
||||||
expect(source).not.toContain('<small v-if="selectedPallet">')
|
expect(source).not.toContain('<small v-if="selectedPallet">')
|
||||||
expect(source).toContain('width: clamp(360px, 36vw, 520px)')
|
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:edit")
|
||||||
expect(source).toContain("platform:firstleg:inbound:weight:confirm")
|
expect(source).toContain("platform:firstleg:inbound:weight:confirm")
|
||||||
expect(source).toContain("new Set(['WAIT_MEASURE', 'MEASURING', 'MEASURE_REJECTED'])")
|
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("workspace.value?.processStatus === 'MEASURE_PENDING'")
|
||||||
expect(source).toContain('v-if="canEdit && !reviewMode"')
|
expect(source).toContain('v-if="canEdit && !reviewMode"')
|
||||||
expect(source).toContain('v-if="canEdit && editable && !reviewMode"')
|
expect(source).toContain('v-if="canEdit && editable && !reviewMode"')
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ const previewResultId = ref('')
|
|||||||
const previewSnapshot = ref<WeightResultResource | null>(null)
|
const previewSnapshot = ref<WeightResultResource | null>(null)
|
||||||
const weightDrafts = ref<Record<string, { value: number | null; unit: 'KG' | 'LB'; reason: string }>>({})
|
const weightDrafts = ref<Record<string, { value: number | null; unit: 'KG' | 'LB'; reason: string }>>({})
|
||||||
let loadSequence = 0
|
let loadSequence = 0
|
||||||
|
let palletLoadSequence = 0
|
||||||
|
let palletAbortController: AbortController | null = null
|
||||||
|
|
||||||
const editableStatuses = new Set(['WAIT_MEASURE', 'MEASURING', 'MEASURE_REJECTED'])
|
const editableStatuses = new Set(['WAIT_MEASURE', 'MEASURING', 'MEASURE_REJECTED'])
|
||||||
const minimumWeight = { KG: 0.001, LB: 0.002 } as const
|
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 canConfirm = computed(() => access.can('platform:firstleg:inbound:weight:confirm'))
|
||||||
const canView = computed(() => access.can('platform:firstleg:inbound:measure:view'))
|
const canView = computed(() => access.can('platform:firstleg:inbound:measure:view'))
|
||||||
const editable = computed(() => Boolean(workspace.value && editableStatuses.has(workspace.value.processStatus)))
|
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(
|
const reviewCorrectionEnabled = computed(() => Boolean(
|
||||||
props.reviewMode
|
props.reviewMode
|
||||||
&& props.correctionMode
|
&& props.correctionMode
|
||||||
@@ -212,32 +226,44 @@ async function loadWorkspace() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadPalletOptions() {
|
async function loadPalletOptions() {
|
||||||
if (props.reviewMode) {
|
if (!canLoadPalletOptions.value) {
|
||||||
|
cancelPalletOptionsLoad()
|
||||||
palletOptions.value = []
|
palletOptions.value = []
|
||||||
selectedPalletId.value = ''
|
selectedPalletId.value = ''
|
||||||
palletError.value = null
|
palletError.value = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!customerId.value || !props.inboundId || !canView.value) {
|
cancelPalletOptionsLoad()
|
||||||
palletOptions.value = []
|
const sequence = ++palletLoadSequence
|
||||||
selectedPalletId.value = ''
|
const controller = new AbortController()
|
||||||
return
|
palletAbortController = controller
|
||||||
}
|
|
||||||
palletLoading.value = true
|
palletLoading.value = true
|
||||||
palletError.value = null
|
palletError.value = null
|
||||||
try {
|
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
|
palletOptions.value = result
|
||||||
if (!result.some((option) => option.palletId === selectedPalletId.value)) selectedPalletId.value = ''
|
if (!result.some((option) => option.palletId === selectedPalletId.value)) selectedPalletId.value = ''
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (sequence !== palletLoadSequence || controller.signal.aborted) return
|
||||||
palletOptions.value = []
|
palletOptions.value = []
|
||||||
selectedPalletId.value = ''
|
selectedPalletId.value = ''
|
||||||
palletError.value = getApiErrorDetails(error, '可选托盘加载失败,请稍后重试')
|
palletError.value = getApiErrorDetails(error, '可选托盘加载失败,请稍后重试')
|
||||||
} finally {
|
} 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() {
|
function chooseFile() {
|
||||||
if (!editable.value) {
|
if (!editable.value) {
|
||||||
ElMessage.warning('当前入库状态不允许上传称重图片')
|
ElMessage.warning('当前入库状态不允许上传称重图片')
|
||||||
@@ -397,17 +423,32 @@ watch(
|
|||||||
previewOpen.value = false
|
previewOpen.value = false
|
||||||
releasePreview()
|
releasePreview()
|
||||||
void loadWorkspace()
|
void loadWorkspace()
|
||||||
void loadPalletOptions()
|
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ 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) => {
|
watch(previewOpen, (open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
releasePreview()
|
releasePreview()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
onBeforeUnmount(releasePreview)
|
onBeforeUnmount(() => {
|
||||||
|
cancelPalletOptionsLoad()
|
||||||
|
palletOptions.value = []
|
||||||
|
selectedPalletId.value = ''
|
||||||
|
releasePreview()
|
||||||
|
})
|
||||||
|
|
||||||
defineExpose({ reload: loadWorkspace })
|
defineExpose({ reload: loadWorkspace })
|
||||||
</script>
|
</script>
|
||||||
@@ -424,22 +465,16 @@ defineExpose({ reload: loadWorkspace })
|
|||||||
<div class="weight-summary"><span>货物重量</span><strong>{{ formatWeight(workspace.actualNetWeightKg) }}</strong></div>
|
<div class="weight-summary"><span>货物重量</span><strong>{{ formatWeight(workspace.actualNetWeightKg) }}</strong></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pallet-picker">
|
<div class="pallet-picker">
|
||||||
<el-select
|
<el-select-v2
|
||||||
v-if="!reviewMode"
|
v-if="canEdit && !reviewMode"
|
||||||
v-model="selectedPalletId"
|
v-model="selectedPalletId"
|
||||||
|
:options="virtualPalletOptions"
|
||||||
aria-label="选择称重托盘"
|
aria-label="选择称重托盘"
|
||||||
placeholder="选择称重托盘"
|
placeholder="选择称重托盘"
|
||||||
filterable
|
filterable
|
||||||
:loading="palletLoading"
|
:loading="palletLoading"
|
||||||
:disabled="loading || uploading || !editable || !canEdit || Boolean(palletError)"
|
: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>
|
</div>
|
||||||
<input
|
<input
|
||||||
v-if="!reviewMode"
|
v-if="!reviewMode"
|
||||||
|
|||||||
Reference in New Issue
Block a user