diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts index 004e8ed8..90e10c0b 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts @@ -1,4 +1,4 @@ -import ElementPlus, { ElMessage, ElMessageBox } from 'element-plus' +import ElementPlus, { ElInputNumber, ElMessage, ElMessageBox } from 'element-plus' import { AxiosError } from 'axios' import { createPinia, setActivePinia } from 'pinia' import { flushPromises, mount } from '@vue/test-utils' @@ -71,10 +71,28 @@ describe('GlobalSmartLoadingView', () => { expect(listSmartLoadingCandidates).toHaveBeenCalledWith(container.id, container.warehouseId) expect(wrapper.text()).toContain('候选入库单与货物') expect(wrapper.text()).toContain('美森正班海卡包税') - expect(wrapper.text()).toContain('ORD-2') - expect(wrapper.text()).toContain('CB-1 装柜方案') - expect(wrapper.text()).toContain('LP-99') - expect(wrapper.text()).toContain('ORD-1') + expect(wrapper.text()).toContain('IN-2') + expect(wrapper.text()).toContain('MSCU-1 装柜方案') + expect(wrapper.text()).not.toContain('LP-99') + expect(wrapper.text()).toContain('IN-1') + expect(wrapper.text()).not.toContain('ORD-1') + expect(wrapper.text()).not.toContain('ORD-2') + }) + + it('shows one user-facing container number and hides internal manual or split references', async () => { + vi.mocked(listContainers).mockResolvedValue({ + records: [{ ...container, cabinetNo: 'CAB-20260814-01', containerNo: 'SPLIT-814022117898chain1' }], + page: 1, + size: 100, + total: 1, + pages: 1, + }) + const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) + await flushPromises() + + expect(wrapper.text()).toContain('CAB-20260814-01') + expect(wrapper.text()).not.toContain('SPLIT-814022117898chain1') + expect(wrapper.text()).not.toContain('LP-99') }) it('groups containers by channel and reloads candidates when the channel changes', async () => { @@ -83,6 +101,7 @@ describe('GlobalSmartLoadingView', () => { id: '9007199254740998', planId: '9007199254740999', cabinetNo: 'CB-2', + containerNo: 'MSCU-2', channelCode: 'HDHK', channelName: '合德海卡包税', } @@ -92,8 +111,9 @@ describe('GlobalSmartLoadingView', () => { await flushPromises() expect(wrapper.findAll('.container-option')).toHaveLength(1) - expect(wrapper.text()).toContain('CB-1') - expect(wrapper.text()).not.toContain('CB-2') + expect(wrapper.text()).toContain('MSCU-1') + expect(wrapper.text()).not.toContain('MSCU-2') + expect(wrapper.text()).not.toContain('个柜次') const tab = wrapper.findAll('[role="tab"]').find((item) => item.text().includes('合德海卡包税')) expect(tab).toBeDefined() @@ -101,8 +121,8 @@ describe('GlobalSmartLoadingView', () => { await flushPromises() expect(wrapper.findAll('.container-option')).toHaveLength(1) - expect(wrapper.text()).toContain('CB-2') - expect(wrapper.text()).not.toContain('CB-1') + expect(wrapper.text()).toContain('MSCU-2') + expect(wrapper.text()).not.toContain('MSCU-1') expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith(other.id, other.warehouseId) }) @@ -129,16 +149,14 @@ describe('GlobalSmartLoadingView', () => { expect(wrapper.findAll('button[aria-label="列设置"]')).toHaveLength(0) }) - it('keeps unsupported prototype filters out of the real workbench', async () => { + it('keeps only customer, inbound code and destination warehouse filters', async () => { const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) await flushPromises() - const statusField = wrapper.findComponent(QueryToolbar).props('fields') - .find((field: { key: string }) => field.key === 'status') - expect(statusField?.options).toEqual([ - { label: '待装柜', value: 'WAIT_LOADING' }, - { label: '预装柜中', value: 'EDITING' }, - { label: '拣货中', value: 'LOADING' }, + const fields = wrapper.findComponent(QueryToolbar).props('fields') as Array<{ key: string; options?: Array<{ label: string; value: string }> }> + expect(fields.map((field) => field.key)).toEqual(['customerId', 'inboundCode', 'destinationWarehouse']) + expect(fields.find((field) => field.key === 'destinationWarehouse')?.options).toEqual([ + { label: 'LAX9', value: '9' }, ]) expect(wrapper.text()).not.toContain('税率') @@ -152,11 +170,11 @@ describe('GlobalSmartLoadingView', () => { await flushPromises() expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(container.id) - expect(wrapper.text()).toContain('CB-1 装柜方案') + expect(wrapper.text()).toContain('MSCU-1 装柜方案') }) - it('does not overwrite the container status when selecting it', async () => { - const waiting = { ...container, id: '9007199254740995', planId: '9007199254740996', cabinetNo: 'CB-WAIT', planStatus: 'WAIT_LOADING' as const } + it('keeps waiting and editing empty containers without exposing their status labels', async () => { + const waiting = { ...container, id: '9007199254740995', planId: '9007199254740996', cabinetNo: 'CB-WAIT', containerNo: null, planStatus: 'WAIT_LOADING' as const } vi.mocked(listContainers).mockResolvedValue({ records: [container, waiting], page: 1, size: 100, total: 2, pages: 1 }) vi.mocked(getSmartLoadingPlanByBooking).mockImplementation(async (bookingId) => ( bookingId === waiting.id @@ -167,31 +185,27 @@ describe('GlobalSmartLoadingView', () => { await flushPromises() const waitingOption = wrapper.findAll('.container-option').find((item) => item.text().includes('CB-WAIT'))! - expect(waitingOption.text()).toContain('待装柜') + expect(waitingOption.text()).toContain('CB-WAIT') + expect(waitingOption.text()).not.toContain('待装柜') await waitingOption.trigger('click') await flushPromises() expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(waiting.id) - expect(waitingOption.text()).toContain('待装柜') + expect(waitingOption.text()).toContain('CB-WAIT') expect(waitingOption.text()).not.toContain('拣货中') }) - it('keeps picking containers editable while hiding the push action', async () => { + it('excludes picking containers from the empty-container workbench', async () => { const pushed = { ...container, id: '9007199254740995', planId: '9007199254740996', cabinetNo: 'CB-2', planStatus: 'LOADING' as const } - const pickingPlan = { ...plan, bookingId: pushed.id, status: 'LOADING' as const } vi.mocked(listContainers).mockResolvedValue({ records: [pushed], page: 1, size: 100, total: 1, pages: 1 }) - vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue(pickingPlan) const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) await flushPromises() - expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(pushed.id) - expect(listSmartLoadingCandidates).toHaveBeenCalledWith(pushed.id, pushed.warehouseId) - expect(wrapper.text()).toContain('CB-2') - expect(wrapper.text()).toContain('拣货中') - expect(wrapper.text()).toContain('智能推荐') - expect(wrapper.text()).toContain('加入') - expect(wrapper.findAll('button').some((item) => item.text().trim() === '推送预装')).toBe(false) + expect(getSmartLoadingPlanByBooking).not.toHaveBeenCalled() + expect(listSmartLoadingCandidates).not.toHaveBeenCalled() + expect(wrapper.findAll('.container-option')).toHaveLength(0) + expect(wrapper.text()).not.toContain('CB-2') }) it('hides completed containers from the smart loading workbench', async () => { @@ -206,6 +220,7 @@ describe('GlobalSmartLoadingView', () => { }) it('refreshes the latest plan after A0409 without losing the selected container', async () => { + const warningMessage = vi.spyOn(ElMessage, 'warning').mockImplementation(() => undefined as never) const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) await flushPromises() vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({ ...plan, version: 3 }) @@ -224,16 +239,12 @@ describe('GlobalSmartLoadingView', () => { expect(recommendSmartLoading).toHaveBeenCalledWith(plan.id, 2) expect(getSmartLoadingPlanByBooking).toHaveBeenCalledTimes(2) - expect(wrapper.text()).toContain('版本 3') - expect(wrapper.text()).toContain('已刷新最新版本') - expect(wrapper.text()).toContain('request-1') + expect(warningMessage).toHaveBeenCalledWith('方案已被其他人修改,已刷新最新版本,请重新确认后提交') + expect(wrapper.text()).not.toContain('装柜操作失败') + expect(wrapper.text()).not.toContain('request-1') }) it('shows a picking-specific message when a picked order cannot be removed', async () => { - const loadingContainer = { ...container, planStatus: 'LOADING' as const } - const loadingPlan = { ...plan, status: 'LOADING' as const } - vi.mocked(listContainers).mockResolvedValue({ records: [loadingContainer], page: 1, size: 100, total: 1, pages: 1 }) - vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue(loadingPlan) vi.mocked(removeSmartLoadingItem).mockRejectedValue(new AxiosError( 'conflict', 'ERR_BAD_REQUEST', @@ -253,15 +264,80 @@ describe('GlobalSmartLoadingView', () => { expect(removeSmartLoadingItem).toHaveBeenCalled() expect(errorMessage).toHaveBeenCalledWith('该订单在拣货中不许删除') - expect(wrapper.text()).toContain('该订单在拣货中不许删除') + expect(wrapper.text()).not.toContain('装柜操作失败') + expect(wrapper.text()).not.toContain('该订单在拣货中不许删除') + }) + + it('removes every item with the latest returned plan version', async () => { + const secondItem = { ...plan.items[0], id: '102', inboundDetailId: '4', inboundCode: 'IN-2' } + const initialPlan = { ...plan, items: [plan.items[0], secondItem] } + const afterFirst = { ...plan, version: 3, items: [secondItem] } + const emptyPlan = { ...plan, version: 4, items: [] } + vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValueOnce(initialPlan).mockResolvedValue(emptyPlan) + vi.mocked(removeSmartLoadingItem).mockResolvedValueOnce(afterFirst).mockResolvedValueOnce(emptyPlan) + vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as never) + + const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) + await flushPromises() + const removeAllButton = wrapper.findAll('button').find((button) => button.text().includes('全部移除')) + expect(removeAllButton).toBeDefined() + await removeAllButton!.trigger('click') + await flushPromises() + + expect(removeSmartLoadingItem).toHaveBeenNthCalledWith(1, plan.id, plan.items[0].id, 2, 1) + expect(removeSmartLoadingItem).toHaveBeenNthCalledWith(2, plan.id, secondItem.id, 3, 1) + expect(wrapper.text()).toContain('当前柜次暂无已装货物') + }) + + it('limits adjusted package count to the assigned count plus remaining availability', async () => { + vi.mocked(listSmartLoadingCandidates).mockResolvedValue([{ accountId: '1', inboundId: '2', inboundDetailId: '3', orderId: '4', orderNo: 'ORD-1', inboundCode: 'IN-1', warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '示例企业', goodsName: '服装', availablePackageCount: 4, unitWeightKg: 10, unitVolumeCbm: 0.2 }]) + const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) + await flushPromises() + + const maxima = wrapper.findAllComponents(ElInputNumber).map((input) => input.props('max')) + expect(maxima).toContain(10) }) it('uses bordered assignment and adjustment actions at narrow widths', () => { const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8') expect(source).toContain('size="small"') expect(source).toContain('type="danger" plain') + expect(source).toContain(':max="adjustmentMax(row)"') expect(source).not.toContain('link type="danger"') expect(source).toContain('@media (max-width: 720px)') expect(source.match(/ { + const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8') + + expect(source).toContain('height: clamp(520px, 68vh, 720px); min-height: 0;') + expect(source).toContain('.dispatch-pane { display: flex; min-width: 0; min-height: 0; flex-direction: column; }') + expect(source).toContain('.candidate-pane :deep(.data-table),.plan-pane :deep(.data-table) { min-height: 0; flex: 1; }') + expect(source).toContain('.container-list { display: grid; min-height: 0; flex: 1;') + expect(source).toContain('grid-auto-rows: max-content;') + expect(source).toContain('overflow-wrap: anywhere;') + expect(source).toContain('.candidate-pane,.plan-pane { height: min(68vh, 560px); min-height: 420px; }') + expect(source).toContain('.container-pane { max-height: min(58vh, 460px); }') + }) + + it('uses overflow-aware channel tabs with a compact right-aligned summary', () => { + const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8') + + expect(source).toContain('候选货物') + expect(source).toContain('
空柜
') + expect(source).toContain('
已装
') + expect(source).toContain('
总重量
') + expect(source).toContain('
总体积
') + expect(source).toContain('.channel-tabs :deep(.el-tabs__content) { display: none; }') + expect(source).not.toContain('class="summary-band"') + expect(source).toContain('class="capacity-grid"') + expect(source).toContain('智能推荐') + expect(source).toContain('推送预装') + }) }) diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.vue b/src/modules/container-loading/pages/GlobalSmartLoadingView.vue index 19d45c60..1e790693 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.vue +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.vue @@ -46,13 +46,12 @@ const selectionLoading = ref(false) const mutating = ref(false) const loadError = ref(null) const selectionError = ref(null) -const mutationError = ref(null) const noDraft = ref(false) const assignOpen = ref(false) const selectedCandidate = ref(null) const assignForm = reactive({ packageCount: 1 }) const quantityDrafts = reactive>({}) -const query = ref({ customerId: '', orderKeyword: '', containerKeyword: '', status: '' }) +const query = ref({ customerId: '', inboundCode: '', destinationWarehouse: '' }) const appliedQuery = ref({ ...query.value }) let selectionRequest = 0 @@ -62,15 +61,12 @@ const canDelete = computed(() => access.can('platform:firstleg:smart-loading:del const canPush = computed(() => access.can('platform:firstleg:smart-loading:push')) const selectedContainer = computed(() => containers.value.find((item) => item.id === selectedContainerId.value) || null) const channelTabs = computed(() => { - const channels = new Map() + const channels = new Map() for (const item of containers.value) { if (!['WAIT_LOADING', 'EDITING'].includes(item.planStatus) || !item.channelCode) continue - const current = channels.get(item.channelCode) - if (current) current.containerCount += 1 - else channels.set(item.channelCode, { + if (!channels.has(item.channelCode)) channels.set(item.channelCode, { code: item.channelCode, name: item.channelName || item.channelCode, - containerCount: 1, }) } return [...channels.values()] @@ -88,49 +84,45 @@ const volumePercent = computed(() => volumeLimit.value ? totalVolume.value / vol const weightPercent = computed(() => weightLimit.value ? totalWeight.value / weightLimit.value * 100 : 0) const displayedVolumePercent = computed(() => percentage(volumePercent.value)) const displayedWeightPercent = computed(() => percentage(weightPercent.value)) - +const destinationWarehouseOptions = computed(() => { + const options = new Map() + for (const item of candidates.value) { + const value = destinationWarehouseKey(item) + if (value) options.set(value, item.destinationWarehouseName || item.destinationWarehouseCode || value) + } + return [...options].map(([value, label]) => ({ label, value })) +}) const queryFields = computed(() => [ { key: 'customerId', type: 'select', label: '客户', placeholder: '全部客户', width: 220, options: customers.value.filter((customer) => customer.status === 'ACTIVE').map((customer) => ({ label: customer.tenantName, value: String(customer.id) })), props: { filterable: true, loading: customersLoading.value } }, - { key: 'orderKeyword', type: 'input', label: '订单或货物', placeholder: '订单号、入库单、货物', width: 250 }, - { key: 'containerKeyword', type: 'input', label: '柜号', placeholder: '请输入柜号', width: 230 }, - { key: 'status', type: 'select', label: '柜次状态', width: 160, options: [ - { label: '待装柜', value: 'WAIT_LOADING' }, - { label: '预装柜中', value: 'EDITING' }, - ] }, + { key: 'inboundCode', type: 'input', label: '入仓码', placeholder: '请输入入仓码', width: 240 }, + { key: 'destinationWarehouse', type: 'select', label: '目的仓', placeholder: '全部目的仓', width: 200, options: destinationWarehouseOptions.value, props: { filterable: true } }, ]) -const filteredContainers = computed(() => { - const keyword = String(appliedQuery.value.containerKeyword || '').trim().toLowerCase() - const status = String(appliedQuery.value.status || '') - return containers.value.filter((item) => { - if (!['WAIT_LOADING', 'EDITING'].includes(item.planStatus)) return false - if (!activeChannelCode.value || item.channelCode !== activeChannelCode.value) return false - if (status && item.planStatus !== status) return false - if (!keyword) return true - return displayContainerNo(item).toLowerCase().includes(keyword) - }) -}) +const filteredContainers = computed(() => containers.value.filter((item) => ( + ['WAIT_LOADING', 'EDITING'].includes(item.planStatus) + && Boolean(activeChannelCode.value) + && item.channelCode === activeChannelCode.value +))) const filteredCandidates = computed(() => { const customerId = String(appliedQuery.value.customerId || '') - const keyword = String(appliedQuery.value.orderKeyword || '').trim().toLowerCase() + const inboundCode = String(appliedQuery.value.inboundCode || '').trim().toLowerCase() + const destinationWarehouse = String(appliedQuery.value.destinationWarehouse || '') return candidates.value.filter((item) => { if (customerId && item.accountId !== customerId) return false - if (!keyword) return true - return [item.orderNo, item.inboundCode, item.goodsName] - .some((value) => String(value || '').toLowerCase().includes(keyword)) + if (destinationWarehouse && destinationWarehouseKey(item) !== destinationWarehouse) return false + return !inboundCode || String(item.inboundCode || '').toLowerCase().includes(inboundCode) }) }) function preferredContainerId() { const visible = filteredContainers.value - if (String(appliedQuery.value.status || '').trim()) return visible[0]?.id || '' return visible.find((item) => item.planStatus === 'EDITING')?.id || visible.find((item) => item.planStatus === 'WAIT_LOADING')?.id || visible[0]?.id || '' } const candidateColumns: DataTableColumn[] = [ { key: 'actions', label: '操作', width: 92, fixed: 'left', slotName: 'actions' }, - { key: 'order', label: '订单/入库单', minWidth: 170, slotName: 'order' }, + { key: 'order', label: '入仓码', minWidth: 170, slotName: 'order' }, { key: 'goodsName', prop: 'goodsName', label: '货物', minWidth: 150 }, { key: 'customerName', prop: 'customerName', label: '客户', minWidth: 140 }, { key: 'availablePackageCount', prop: 'availablePackageCount', label: '可用箱数', width: 100 }, @@ -139,7 +131,7 @@ const candidateColumns: DataTableColumn[] = [ ] const itemColumns: DataTableColumn[] = [ { key: 'actions', label: '操作', width: 170, fixed: 'left', slotName: 'actions' }, - { key: 'order', label: '订单/入库单', minWidth: 170, slotName: 'order' }, + { key: 'order', label: '入仓码', minWidth: 170, slotName: 'order' }, { key: 'goods', label: '货物', minWidth: 160, slotName: 'goods' }, { key: 'packageCount', label: '分配箱数', width: 140, slotName: 'packageCount' }, { key: 'volumeCbm', label: '体积', width: 105, slotName: 'volume' }, @@ -152,6 +144,14 @@ function formatNumber(value: number, digits = 3) { function percentage(value: number) { return Number.isFinite(value) ? Number(Math.max(value, 0).toFixed(2)) : 0 } function candidateRow(source: object) { return source as SmartLoadingCandidate } function itemRow(source: object) { return source as SmartLoadingItem } +function adjustmentMax(source: object) { + const item = itemRow(source) + const remaining = candidates.value.find((candidate) => candidate.inboundDetailId === item.inboundDetailId)?.availablePackageCount || 0 + return Math.max(1, item.packageCount + remaining) +} +function destinationWarehouseKey(candidate: SmartLoadingCandidate) { + return candidate.destinationWarehouseId || candidate.destinationWarehouseCode || candidate.destinationWarehouseName || '' +} function displayContainerNo(container: ContainerBookingResource | null | undefined) { if (!container) return '柜号待生成' const actual = String(container.containerNo || '').trim() @@ -250,14 +250,12 @@ async function loadCustomers() { async function selectContainer(containerId: string) { if (selectedContainerId.value === containerId) return selectedContainerId.value = containerId - mutationError.value = null await loadSelectedData() } async function selectChannel(channelCode: string) { if (activeChannelCode.value === channelCode) return activeChannelCode.value = channelCode selectedContainerId.value = preferredContainerId() - mutationError.value = null await loadSelectedData() } async function applyFilters(model: QueryModel) { @@ -281,6 +279,10 @@ function openAssign(source: object) { } async function assign() { if (!plan.value || !selectedCandidate.value || assignForm.packageCount < 1) return + if (assignForm.packageCount > selectedCandidate.value.availablePackageCount) { + ElMessage.warning(`本次最多可分配 ${selectedCandidate.value.availablePackageCount} 箱`) + return + } await mutate(async () => { acceptPlan(await assignSmartLoadingItem(plan.value!.id, { inboundId: selectedCandidate.value!.inboundId, @@ -289,6 +291,7 @@ async function assign() { expectedVersion: plan.value!.version, })) assignOpen.value = false + await loadSelectedData() ElMessage.success('货物已加入当前柜次') }, '分配货物失败', { preserveAssign: true }) } @@ -296,6 +299,7 @@ async function recommend() { if (!plan.value) return await mutate(async () => { acceptPlan(await recommendSmartLoading(plan.value!.id, plan.value!.version)) + await loadSelectedData() ElMessage.success('智能推荐已生成') }, '智能推荐失败') } @@ -304,23 +308,46 @@ async function adjust(source: object) { if (!plan.value) return const count = Number(quantityDrafts[item.id]) if (!Number.isInteger(count) || count < 1) { ElMessage.warning('分配箱数必须为正整数'); return } + const max = adjustmentMax(item) + if (count > max) { ElMessage.warning(`该货物最多可分配 ${max} 箱`); return } await mutate(async () => { acceptPlan(await adjustSmartLoadingItem(plan.value!.id, item.id, count, plan.value!.version, item.version)) + await loadSelectedData() ElMessage.success('箱数已调整') }, '调整箱数失败', { preserveDrafts: { ...quantityDrafts } }) } async function remove(source: object) { const item = itemRow(source) if (!plan.value) return - const confirmed = await ElMessageBox.confirm(`确认从当前柜次移除“${item.productNameZh || item.orderNo || item.id}”吗?`, '移除装柜明细', { + const confirmed = await ElMessageBox.confirm(`确认从当前柜次移除“${item.productNameZh || item.inboundCode || '该货物'}”吗?`, '移除装柜明细', { type: 'warning', confirmButtonText: '移除', cancelButtonText: '取消', }).catch(() => false) if (!confirmed || !plan.value) return await mutate(async () => { acceptPlan(await removeSmartLoadingItem(plan.value!.id, item.id, plan.value!.version, item.version)) + await loadSelectedData() ElMessage.success('装柜明细已移除') }, '移除装柜明细失败', { pickingConflict: true }) } +async function removeAll() { + if (!plan.value?.items.length) return + const itemCount = plan.value.items.length + const confirmed = await ElMessageBox.confirm(`确认移除当前方案的全部 ${itemCount} 条装柜明细吗?`, '全部移除', { + type: 'warning', confirmButtonText: '全部移除', cancelButtonText: '取消', + }).catch(() => false) + if (!confirmed || !plan.value) return + await mutate(async () => { + let current = plan.value! + for (const original of [...current.items]) { + const item = current.items.find((value) => value.id === original.id) + if (!item) continue + current = await removeSmartLoadingItem(current.id, item.id, current.version, item.version) + acceptPlan(current) + } + await loadSelectedData() + ElMessage.success(`已移除全部 ${itemCount} 条装柜明细`) + }, '全部移除失败', { pickingConflict: true }) +} async function pushPreload() { if (!plan.value) return const confirmed = await ElMessageBox.confirm('推送后当前方案进入装柜流程,不能继续调整。确认推送预装清单?', '推送预装清单', { @@ -339,7 +366,6 @@ async function mutate( recovery: { preserveDrafts?: Record; preserveAssign?: boolean; pickingConflict?: boolean } = {}, ) { mutating.value = true - mutationError.value = null try { await action() } catch (error) { @@ -349,12 +375,11 @@ async function mutate( const pickingMessage = recovery.pickingConflict && details.message.includes('拣货') ? '该订单在拣货中不许删除' : null - mutationError.value = { ...details, message: pickingMessage || '方案已被其他人修改,已刷新最新版本并保留本次输入,请重新确认后提交' } + const message = pickingMessage || '方案已被其他人修改,已刷新最新版本,请重新确认后提交' if (recovery.preserveAssign) assignOpen.value = true if (pickingMessage) ElMessage.error(pickingMessage) - else ElMessage.warning(mutationError.value.message) + else ElMessage.warning(message) } else { - mutationError.value = details ElMessage.error(details.message) } } finally { @@ -368,26 +393,24 @@ onMounted(() => { void load(); void loadCustomers() }) -

{{ selectedCandidate.goodsName || '未命名货物' }}

{{ selectedCandidate.orderNo || '—' }} · 可用 {{ selectedCandidate.availablePackageCount }} 箱

+

{{ selectedCandidate.goodsName || '未命名货物' }}

{{ selectedCandidate.inboundCode || '入仓码暂缺' }} · 可用 {{ selectedCandidate.availablePackageCount }} 箱

@@ -262,7 +262,7 @@ onMounted(load) .workbench h2 + p,.workbench small { display: block; margin-top: 4px; color: var(--cc-muted); } .header-actions,.row-actions { flex-wrap: wrap; gap: 8px; } .header-actions :deep(.el-button + .el-button),.row-actions :deep(.el-button + .el-button) { margin-left: 0; } -.summary-band { display: grid; grid-template-columns: repeat(4,minmax(0,1fr)); gap: 1px; border: 1px solid var(--cc-border); background: var(--cc-border); } +.summary-band { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 1px; border: 1px solid var(--cc-border); background: var(--cc-border); } .summary-band>div { display: grid; gap: 5px; padding: 14px; background: #fff; } .summary-band span { color: var(--cc-muted); font-size: 12px; } .capacity-grid { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 16px; padding: 16px; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; } diff --git a/src/modules/inbounds/components/InboundTaskList.vue b/src/modules/inbounds/components/InboundTaskList.vue index c97145ad..6bb5377d 100644 --- a/src/modules/inbounds/components/InboundTaskList.vue +++ b/src/modules/inbounds/components/InboundTaskList.vue @@ -23,7 +23,6 @@ import WeightOcrPanel from '../weight/components/WeightOcrPanel.vue' type FilterModel = QueryModel & { customerId: string inboundCode: string - orderNo: string warehouseId: string expectedArrivalRange: string[] } @@ -36,7 +35,7 @@ const pageSize = ref(20) const loading = ref(false) const loadError = ref(null) const result = ref({ records: [] as InboundOrderSummary[], total: 0 }) -const filters = ref({ customerId: '', inboundCode: '', orderNo: '', warehouseId: '', expectedArrivalRange: [] }) +const filters = ref({ customerId: '', inboundCode: '', warehouseId: '', expectedArrivalRange: [] }) const appliedFilters = ref({ ...filters.value, expectedArrivalRange: [] }) const customers = ref([]) const customerLoadError = ref(null) @@ -72,7 +71,6 @@ const queryFields = computed(() => [ }, { key: 'warehouseId', type: 'select', label: '定位仓库', width: 220, placeholder: '全部仓库', options: warehouseOptions.value, props: { filterable: true } }, { key: 'inboundCode', type: 'input', label: '入仓码', width: 220, placeholder: '请输入入仓码' }, - { key: 'orderNo', type: 'input', label: '订单号', width: 220, placeholder: '请输入订单号' }, { key: 'expectedArrivalRange', type: 'daterange', label: '预计到达时间', width: 320, placeholder: '请选择日期范围' }, ]) @@ -142,7 +140,6 @@ async function loadTasks() { size: pageSize.value, warehouseId: applied.warehouseId || undefined, inboundCode: applied.inboundCode.trim() || undefined, - orderNo: applied.orderNo.trim() || undefined, expectedArrivalFrom: dateParam(applied.expectedArrivalRange[0]), expectedArrivalTo: dateParam(applied.expectedArrivalRange[1], true), })) @@ -163,7 +160,7 @@ function applyFilters() { } function resetFilters() { - filters.value = { customerId: '', inboundCode: '', orderNo: '', warehouseId: '', expectedArrivalRange: [] } + filters.value = { customerId: '', inboundCode: '', warehouseId: '', expectedArrivalRange: [] } appliedFilters.value = { ...filters.value, expectedArrivalRange: [] } if (page.value === 1) void loadTasks() else page.value = 1 @@ -260,7 +257,7 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria 无操作权限 - + diff --git a/src/modules/inbounds/components/InboundWorkflowWorkspace.test.ts b/src/modules/inbounds/components/InboundWorkflowWorkspace.test.ts index b33a3906..db4cf041 100644 --- a/src/modules/inbounds/components/InboundWorkflowWorkspace.test.ts +++ b/src/modules/inbounds/components/InboundWorkflowWorkspace.test.ts @@ -23,9 +23,9 @@ describe('InboundWorkflowWorkspace', () => { }) it('opens measurement editing and review in prototype-sized centered dialogs', () => { - expect(source).toContain('title="编辑入库测量数据"') + expect(source).toContain(':title="`测量 · ${title}`"') expect(source).toContain('width="min(1240px, calc(100vw - 32px))"') - expect(source).toContain('title="审核入库测量数据"') + expect(source).toContain(':title="`测量审核 · ${title}`"') expect(source).toContain('width="min(1240px, calc(100vw - 32px))"') expect(source).toContain('height: 92vh') expect(source).toContain('max-height: 92vh') diff --git a/src/modules/inbounds/components/InboundWorkflowWorkspace.vue b/src/modules/inbounds/components/InboundWorkflowWorkspace.vue index e68784eb..0ebf879f 100644 --- a/src/modules/inbounds/components/InboundWorkflowWorkspace.vue +++ b/src/modules/inbounds/components/InboundWorkflowWorkspace.vue @@ -194,7 +194,7 @@ onBeforeUnmount(() => { { ) { return in
-

入库码:{{ summary.inboundCode || `入库单 ${summary.id}` }}

-

订单 {{ value(summary.orderNo) }} · {{ value(summary.customerName) }}

+

入仓码:{{ summary.inboundCode || '暂缺' }}

+

{{ value(summary.customerName) }}

{{ processLabel(summary.processStatus) }}
@@ -39,7 +39,6 @@ function label(value: string | null, labels: Record) { return in

基本信息

-
头程订单号
{{ value(summary.orderNo) }}
订单状态
{{ label(summary.orderStatusSnapshot, inboundOrderStatusLabels) }}
客户
{{ value(summary.customerName) }}
公司
{{ value(summary.companyName) }}
diff --git a/src/modules/inbounds/measurement/InboundMeasurementPanel.test.ts b/src/modules/inbounds/measurement/InboundMeasurementPanel.test.ts index abbbc8d5..89543dfc 100644 --- a/src/modules/inbounds/measurement/InboundMeasurementPanel.test.ts +++ b/src/modules/inbounds/measurement/InboundMeasurementPanel.test.ts @@ -149,13 +149,13 @@ describe('InboundMeasurementPanel', () => { const wrapper = await mountPanel() expect(getInboundMeasurement).toHaveBeenCalledWith('10001', '7001') - expect((wrapper.find('input[readonly]').element as HTMLInputElement).value).toBe('RK202608040001') expect(wrapper.text()).toContain('RK202608040001') - expect(wrapper.text()).toContain('周长(cm)') - expect(wrapper.find('.measurement-table').text()).toContain('190') expect(wrapper.find('.measurement-table').text()).toContain('0.6') + expect(wrapper.find('.measurement-table').text()).not.toContain('周长') await wrapper.find('.el-table__expand-icon').trigger('click') await flushPromises() + expect(wrapper.text()).toContain('箱规周长') + expect(wrapper.text()).toContain('190 cm') expect(wrapper.text()).toContain('MJ20210327') expect(wrapper.text()).toContain('2026-08-04 10:30:00+08:00') expect(wrapper.find('button[aria-label="预览测量图片"]').exists()).toBe(true) @@ -189,27 +189,29 @@ describe('InboundMeasurementPanel', () => { const wrapper = await mountPanel() - expect(wrapper.findAll('input[readonly]').some((input) => (input.element as HTMLInputElement).value === '未知来源')).toBe(true) + expect(wrapper.text()).toContain('未知来源') expect(wrapper.text()).not.toContain('LEGACY_DEVICE') }) - it('follows the prototype grouping, labels, and editable field order', async () => { + it('keeps the first view focused on key totals and moves secondary fields behind disclosure', async () => { const wrapper = await mountPanel() const text = wrapper.text() - expect(text).toContain('原单数据与测量汇总') - expect(text).toContain('扫描箱测量数据明细') - expect(text).toContain('订单总箱数') - expect(text).toContain('确认箱数合计') - expect(text).toContain('货物总重量') - expect(text).toContain('实测总体积') - expect(text).toContain('实测总材积重') + expect(text).toContain('实测箱数') + expect(text).toContain('货物重量') + expect(text).toContain('总体积') + expect(text).toContain('材积重') + expect(text).toContain('申报信息') + expect(text).toContain('箱规明细') expect(text).not.toContain('租户') + expect(text).not.toContain('数据版本') expect(wrapper.find('.measurement-table').exists()).toBe(true) expect(wrapper.find('.measurement-item').exists()).toBe(false) - expect(wrapper.find('.measurement-table').text()).toContain('周长(cm)') + expect(wrapper.find('.measurement-table').text()).not.toContain('周长') const source = await import('./InboundMeasurementPanel.vue?raw').then((module) => module.default) + expect(source).toContain('') + expect(source).toContain('grid-template-columns: repeat(4, minmax(0, 1fr))') expect(source).toContain('.measurement-form-grid, .measurement-data-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));') expect(source).toContain('.measurement-table { width: 100%; font-size: 12px; }') expect(source).toContain('.measurement-data-grid :deep(.el-input__wrapper)') @@ -224,7 +226,7 @@ describe('InboundMeasurementPanel', () => { wrapper.vm.updateActualGrossWeight(125.75) await flushPromises() - expect(wrapper.findAll('input[readonly]').some((input) => (input.element as HTMLInputElement).value === '125.75')).toBe(true) + expect(wrapper.find('.measurement-metrics').text()).toContain('125.75') expect((countInput.element as HTMLInputElement).value).toBe('9') }) @@ -232,8 +234,8 @@ describe('InboundMeasurementPanel', () => { const wrapper = await mountPanel() expect(wrapper.classes()).toContain('surface-panel') - expect(wrapper.find('.measurement-panel__heading').exists()).toBe(true) - expect(wrapper.findAll('input[readonly]').some((input) => (input.element as HTMLInputElement).value === '测量中')).toBe(true) + expect(wrapper.find('.measurement-summary-header').exists()).toBe(true) + expect(wrapper.find('.measurement-summary-status').text()).toContain('测量中') }) it('saves only the changed confirmed package count with the current measurement version', async () => { @@ -343,8 +345,9 @@ describe('InboundMeasurementPanel', () => { await flushPromises() expect(getInboundMeasurement).toHaveBeenLastCalledWith('10002', '7002') - expect(wrapper.findAll('input[readonly]').some((input) => (input.element as HTMLInputElement).value === 'RK202608040002')).toBe(true) - expect(wrapper.findAll('input[readonly]').some((input) => (input.element as HTMLInputElement).value === '测试企业')).toBe(false) + expect(wrapper.find('.measurement-summary-header').text()).toContain('RK202608040002') + expect(wrapper.find('.measurement-summary-header').text()).toContain('第二企业') + expect(wrapper.find('.measurement-summary-header').text()).not.toContain('测试企业') }) it('renders a permission-specific read-only state without requesting data', async () => { diff --git a/src/modules/inbounds/measurement/InboundMeasurementPanel.vue b/src/modules/inbounds/measurement/InboundMeasurementPanel.vue index 8b4eecb6..5a3c56b1 100644 --- a/src/modules/inbounds/measurement/InboundMeasurementPanel.vue +++ b/src/modules/inbounds/measurement/InboundMeasurementPanel.vue @@ -516,32 +516,45 @@ defineExpose({ saveDraft, submitForReview, updateActualGrossWeight, mayMaintain,
测量草稿 -
-
-

原单数据与测量汇总

-
-
- - - - - - - - - - - - - - - -
+
+
+
+ 入仓码 + {{ detail.inboundCode || '—' }} +

{{ detail.companyName || '客户未命名' }} · {{ detail.channelName || detail.channelCode || '渠道未填写' }}

+
+
+ {{ processStatusLabel(detail.processStatus) }} + {{ sourceLabel }} +
+
+ +
+
+
实测箱数
+
{{ formatNumber(confirmedPackageCount, 0) }} / 申报 {{ formatNumber(expectedPackageCount, 0) }}
+
+
货物重量
{{ formatNumber(detail.actualGrossWeightKg) }} kg
+
总体积
{{ formatNumber(detail.actualVolumeCbm, 4) }} m³
+
材积重
{{ formatNumber(detail.volumeWeightKg) }} kg
+
+ + + +
+ + + + + +
+
+
-

扫描箱测量数据明细

每条记录对应一个实际扫描箱;人工只确认该条记录代表的箱数,设备实测值不可在此修改。

+

箱规明细

手动录入箱规
@@ -559,7 +572,10 @@ defineExpose({ saveDraft, submitForReview, updateActualGrossWeight, mayMaintain,
机器编号
{{ row.machineNo || '人工录入' }}
测量时间
{{ row.measuredAt ? row.measuredAt.replace('T', ' ') : '—' }}
+
每箱毛重
{{ formatNumber(row.unitWeightKg) }} kg
+
单箱体积
{{ formatNumber(row.serverValues.unitVolumeCbm, 4) }} m³
箱规周长
{{ formatNumber(row.serverValues.girthCm) }} cm
+
总材积重
{{ formatNumber(row.serverValues.volumeWeightKg) }} kg
备注
{{ row.remark || '—' }}
@@ -585,24 +601,12 @@ defineExpose({ saveDraft, submitForReview, updateActualGrossWeight, mayMaintain, /> - - - - - - - - - - + - - -