From 4eda56acf1ed703593d7c8e2ff4d10cab959e6aa Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 4 Sep 2026 12:18:39 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/GlobalSmartLoadingView.test.ts | 24 ++- .../pages/GlobalSmartLoadingView.vue | 5 +- .../pages/SmartLoadingWorkbenchView.test.ts | 3 +- .../pages/SmartLoadingWorkbenchView.vue | 5 +- .../WeightOcrPanel.behavior.test.ts | 198 +++++++++++++++++- .../weight/components/WeightOcrPanel.test.ts | 9 + .../weight/components/WeightOcrPanel.vue | 118 ++++++++++- src/modules/inbounds/weight/types.ts | 31 +++ 8 files changed, 381 insertions(+), 12 deletions(-) diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts index 6b79bb66..2d58fb54 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts @@ -18,6 +18,7 @@ import { listSmartLoadingCandidates, listSmartLoadingWorkbenchContainers, recommendSmartLoading, + adjustSmartLoadingItem, removeAllUnpickedSmartLoadingItems, removeSmartLoadingGroup, removeSmartLoadingItem, @@ -525,11 +526,32 @@ describe('GlobalSmartLoadingView', () => { it('keeps the package count editor inside its table cell without overflow ellipsis', () => { const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8') expect(source).toContain('
{ + vi.mocked(adjustSmartLoadingItem).mockResolvedValue({ + ...plan, + version: 3, + items: [{ ...plan.items[0], packageCount: 4, version: 2 }], + }) + const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) + await flushPromises() + + const quantityInput = wrapper.findAllComponents(ElInputNumber).find((input) => input.props('modelValue') === 6) + expect(quantityInput?.props('controls')).toBe(false) + quantityInput?.vm.$emit('update:modelValue', 4) + await flushPromises() + quantityInput?.vm.$emit('blur') + await flushPromises() + + expect(adjustSmartLoadingItem).toHaveBeenCalledTimes(1) + expect(adjustSmartLoadingItem).toHaveBeenCalledWith(plan.id, plan.items[0].id, 4, plan.version, plan.items[0].version) + }) + it('rounds capacity percentages and hides column settings in embedded tables', async () => { vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({ ...plan, diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.vue b/src/modules/container-loading/pages/GlobalSmartLoadingView.vue index 62278091..ec3efbcd 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.vue +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.vue @@ -527,8 +527,9 @@ async function recommend() { } async function adjust(source: object) { const item = itemRow(source) - if (!plan.value) return + if (!plan.value || mutating.value) return const count = Number(quantityDrafts[item.id]) + if (count === item.packageCount) return if (!Number.isInteger(count) || count < item.minPackageCount) { ElMessage.warning(`分配箱数不能低于已拣下限 ${item.minPackageCount} 箱`) return @@ -760,7 +761,7 @@ onMounted(() => { void load(); void loadCustomers() }) - + diff --git a/src/modules/container-loading/pages/SmartLoadingWorkbenchView.test.ts b/src/modules/container-loading/pages/SmartLoadingWorkbenchView.test.ts index dfaab72f..9678238d 100644 --- a/src/modules/container-loading/pages/SmartLoadingWorkbenchView.test.ts +++ b/src/modules/container-loading/pages/SmartLoadingWorkbenchView.test.ts @@ -132,7 +132,8 @@ describe('SmartLoadingWorkbenchView', () => { it('keeps the package count editor inside its table cell without overflow ellipsis', () => { const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue'), 'utf8') expect(source).toContain('
- + diff --git a/src/modules/inbounds/weight/components/WeightOcrPanel.behavior.test.ts b/src/modules/inbounds/weight/components/WeightOcrPanel.behavior.test.ts index f9745c05..d570f9b5 100644 --- a/src/modules/inbounds/weight/components/WeightOcrPanel.behavior.test.ts +++ b/src/modules/inbounds/weight/components/WeightOcrPanel.behavior.test.ts @@ -61,6 +61,14 @@ const pendingWeight: WeightResultResource = { adjustedAt: null, confirmedBy: null, confirmedAt: null, + palletSequence: 1, + blockId: '70001', + slotNo: 1, + reservationId: '71001', + locationId: '72001', + locationCode: 'A01', + sharedOverflow: false, + slotStatus: 'BOUND', version: 0, } @@ -71,6 +79,24 @@ const workspace: WeightWorkspaceResource = { actualGrossWeightKg: null, actualNetWeightKg: null, weightResults: [pendingWeight], + palletLocationGuide: { + blockId: '70001', + reservedSlotCount: 1, + actualPalletCount: 1, + reservedEmptySlotCount: 0, + slots: [{ + slotNo: 1, + reservationId: '71001', + locationId: '72001', + locationCode: 'A01', + sharedOverflow: false, + slotStatus: 'BOUND', + boundWeightResultId: '30001', + palletSequence: 1, + palletId: '50001', + palletCode: 'TP-001', + }], + }, } function palletPage(records = [{ @@ -205,9 +231,137 @@ describe('WeightOcrPanel confirmation behavior', () => { expect(wrapper.text()).not.toContain('raw-0') }) + it('renders server-bound pallet sequence, target location, and pending putaway status ahead of OCR facts', async () => { + const wrapper = mountPanel() + await flushPromises() + + const card = wrapper.get('.weight-card') + expect(card.text()).toContain('第 1 托') + expect(card.text()).toContain('目标库位') + expect(card.text()).toContain('A01') + expect(card.text()).toContain('整票预留库位') + expect(card.text()).toContain('待上架') + const putawayGuide = card.get('.weight-card__putaway').element + expect(putawayGuide.compareDocumentPosition( + card.get('.weight-card__value').element, + ) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() + }) + + it('renders the authoritative whole-inbound reservation summary and an empty reserved slot', async () => { + vi.mocked(getWeightWorkspace).mockResolvedValue({ + ...workspace, + palletLocationGuide: { + blockId: '70001', + reservedSlotCount: 3, + actualPalletCount: 2, + reservedEmptySlotCount: 1, + slots: [ + { slotNo: 1, reservationId: '71001', locationId: '72001', locationCode: 'A01', sharedOverflow: false, slotStatus: 'BOUND', boundWeightResultId: '30001', palletSequence: 1, palletId: '50001', palletCode: 'TP-001' }, + { slotNo: 2, reservationId: '71002', locationId: '72002', locationCode: 'A02', sharedOverflow: false, slotStatus: 'TRANSFERRED', boundWeightResultId: '30002', palletSequence: 2, palletId: '50002', palletCode: 'TP-002' }, + { slotNo: 3, reservationId: '71003', locationId: '72003', locationCode: 'A03', sharedOverflow: false, slotStatus: 'RESERVED_EMPTY', boundWeightResultId: null, palletSequence: null, palletId: null, palletCode: null }, + ], + }, + }) + const wrapper = mountPanel() + await flushPromises() + + const guide = wrapper.get('.putaway-guide') + expect(guide.text()).toContain('整票预留库位') + expect(guide.text()).toContain('A01 / A02 / A03') + expect(guide.text()).toContain('预留槽位数3') + expect(guide.text()).toContain('实际托数2') + expect(guide.text()).toContain('预留空位数1') + expect(guide.text()).toContain('A03预留空位本票预留空位') + }) + + it('shows an explicit whole-inbound unassigned state when no guide exists', async () => { + vi.mocked(getWeightWorkspace).mockResolvedValue({ ...workspace, palletLocationGuide: null }) + const wrapper = mountPanel() + await flushPromises() + + expect(wrapper.text()).toContain('尚未分配整票预留库位') + }) + + it('marks A0 as a temporary overflow location from the server flag', async () => { + vi.mocked(getWeightWorkspace).mockResolvedValue({ + ...workspace, + weightResults: [{ ...pendingWeight, locationCode: 'A0', sharedOverflow: true }], + palletLocationGuide: { + ...workspace.palletLocationGuide!, + slots: [{ + ...workspace.palletLocationGuide!.slots[0]!, + locationCode: 'A0', + sharedOverflow: true, + }], + }, + }) + const wrapper = mountPanel() + await flushPromises() + + expect(wrapper.text()).toContain('A0 临时溢出位') + expect(wrapper.get('.weight-card__putaway').classes()).toContain('is-overflow') + expect(wrapper.get('.putaway-guide__slots li').classes()).toContain('is-overflow') + }) + + it.each([ + ['TRANSFERRED', '已上架'], + ['RELEASED', '已释放'], + ['RESERVED_EMPTY', '预留空位'], + ] as const)('renders the authoritative %s slot status as %s', async (slotStatus, label) => { + vi.mocked(getWeightWorkspace).mockResolvedValue({ + ...workspace, + weightResults: [{ ...pendingWeight, slotStatus }], + }) + const wrapper = mountPanel() + await flushPromises() + + expect(wrapper.text()).toContain(label) + }) + + it('shows an explicit unassigned state when the server has not bound a slot', async () => { + vi.mocked(getWeightWorkspace).mockResolvedValue({ + ...workspace, + weightResults: [{ + ...pendingWeight, + palletSequence: null, + blockId: null, + slotNo: null, + reservationId: null, + locationId: null, + locationCode: null, + sharedOverflow: null, + slotStatus: null, + }], + }) + const wrapper = mountPanel() + await flushPromises() + + expect(wrapper.text()).toContain('托盘未分配') + expect(wrapper.text()).toContain('目标库位未分配') + }) + it('deletes a pending image with its optimistic-lock version', async () => { - vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never) + const confirm = vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never) vi.mocked(deleteWeightResult).mockResolvedValue(undefined) + vi.mocked(getWeightWorkspace) + .mockResolvedValueOnce(workspace) + .mockResolvedValueOnce({ + ...workspace, + weightResults: [], + palletLocationGuide: { + ...workspace.palletLocationGuide!, + actualPalletCount: 0, + reservedEmptySlotCount: 1, + slots: [{ + ...workspace.palletLocationGuide!.slots[0]!, + slotStatus: 'RESERVED_EMPTY', + boundWeightResultId: null, + palletSequence: null, + palletId: null, + palletCode: null, + }], + }, + }) const wrapper = mountPanel() await flushPromises() @@ -215,6 +369,48 @@ describe('WeightOcrPanel confirmation behavior', () => { await flushPromises() expect(deleteWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', 0) + expect(confirm).toHaveBeenCalledWith( + expect.stringContaining('对应库位仍为本票保留,不会释放给其他订单'), + '删除称重图片', + expect.any(Object), + ) + expect(getWeightWorkspace).toHaveBeenCalledTimes(2) + expect(wrapper.findAll('.weight-card')).toHaveLength(0) + expect(wrapper.get('.putaway-guide').text()).toContain('预留空位数1') + expect(wrapper.get('.putaway-guide').text()).toContain('A01预留空位本票预留空位') + }) + + it('keeps the image and server workspace visible when deletion fails', async () => { + vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never) + const errorMessage = vi.spyOn(ElMessage, 'error') + vi.mocked(deleteWeightResult).mockRejectedValueOnce( + new ApiResponseError('无权删除当前称重图片', 'A0403', 403), + ) + const wrapper = mountPanel() + await flushPromises() + + await wrapper.get('button[aria-label="删除称重图片 1"]').trigger('click') + await flushPromises() + + expect(wrapper.findAll('.weight-card')).toHaveLength(1) + expect(wrapper.text()).toContain('A01') + expect(getWeightWorkspace).toHaveBeenCalledTimes(1) + expect(errorMessage).toHaveBeenCalledWith(expect.objectContaining({ + message: '当前账号无权访问该数据,请联系管理员确认权限', + })) + }) + + it('shows a retryable workspace load failure without fabricating putaway guidance', async () => { + vi.mocked(getWeightWorkspace).mockRejectedValueOnce( + new ApiResponseError('称重工作区暂不可用', 'A0500', 503), + ) + const wrapper = mountPanel() + await flushPromises() + + expect(wrapper.text()).toContain('称重数据加载失败') + expect(wrapper.text()).toContain('称重工作区暂不可用') + expect(wrapper.find('.putaway-guide').exists()).toBe(false) + expect(wrapper.find('.putaway-guide-empty').exists()).toBe(false) }) it('shows only the key weights and allows direct confirmation', async () => { diff --git a/src/modules/inbounds/weight/components/WeightOcrPanel.test.ts b/src/modules/inbounds/weight/components/WeightOcrPanel.test.ts index 24903aa8..76428385 100644 --- a/src/modules/inbounds/weight/components/WeightOcrPanel.test.ts +++ b/src/modules/inbounds/weight/components/WeightOcrPanel.test.ts @@ -35,6 +35,15 @@ describe('WeightOcrPanel contract', () => { expect(source).toContain('货物重量') expect(source).toContain('palletTareWeightKg') expect(source).toContain('netWeightKg') + expect(source).toContain('row.palletSequence') + expect(source).toContain('row.locationCode') + expect(source).toContain('row.sharedOverflow') + expect(source).toContain('putawayStatusLabel(row.slotStatus)') + expect(source).toContain('workspace.palletLocationGuide.reservedSlotCount') + expect(source).toContain('workspace.palletLocationGuide.actualPalletCount') + expect(source).toContain('workspace.palletLocationGuide.reservedEmptySlotCount') + expect(source).toContain('本票预留空位') + expect(source).toContain('对应库位仍为本票保留,不会释放给其他订单') expect(source).toContain("emit('summaryChanged', workspace.value?.actualGrossWeightKg ?? null, workspace.value?.actualNetWeightKg ?? null)") }) diff --git a/src/modules/inbounds/weight/components/WeightOcrPanel.vue b/src/modules/inbounds/weight/components/WeightOcrPanel.vue index 2518c26a..9ef1b013 100644 --- a/src/modules/inbounds/weight/components/WeightOcrPanel.vue +++ b/src/modules/inbounds/weight/components/WeightOcrPanel.vue @@ -22,7 +22,12 @@ import { getWeightWorkspace, uploadWeightImage, } from '../api/weightOcr' -import type { WeightPalletOption, WeightResultResource, WeightWorkspaceResource } from '../types' +import type { + WeightPalletOption, + WeightPutawaySlotStatus, + WeightResultResource, + WeightWorkspaceResource, +} from '../types' import WeightImageThumbnail from './WeightImageThumbnail.vue' const props = withDefaults(defineProps<{ @@ -180,6 +185,26 @@ function cardWeight(row: WeightResultResource) { return formatWeight(draft?.value ?? null, draft?.unit ?? 'KG') } +function putawayStatusLabel(status: WeightResultResource['slotStatus']) { + return ({ + RESERVED_EMPTY: '预留空位', + BOUND: '待上架', + TRANSFERRED: '已上架', + RELEASED: '已释放', + } as const)[status as Exclude] || '未分配' +} + +function putawayStatusType(status: WeightResultResource['slotStatus']) { + if (status === 'TRANSFERRED') return 'success' + if (status === 'BOUND') return 'warning' + if (status === 'RELEASED') return 'info' + return 'info' +} + +function guideSlotStatusType(status: WeightPutawaySlotStatus) { + return putawayStatusType(status) +} + function hasWeightDraftChanged(row: WeightResultResource) { const draft = weightDrafts.value[row.id] if (!draft) return false @@ -403,8 +428,8 @@ async function removeResult(row: WeightResultResource) { try { await ElMessageBox.confirm( row.recognitionStatus === 'CONFIRMED' - ? '删除后将重新计算货物总重量,确认删除这张称重图片吗?' - : '确认删除这张称重图片吗?', + ? '删除后将重新计算货物总重量;对应库位仍为本票保留,不会释放给其他订单。确认删除这张称重图片吗?' + : '对应库位仍为本票保留,不会释放给其他订单。确认删除这张称重图片吗?', '删除称重图片', { type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' }, ) @@ -532,6 +557,41 @@ defineExpose({ reload: loadWorkspace })