diff --git a/src/modules/container-loading/api/platformContainerLoading.test.ts b/src/modules/container-loading/api/platformContainerLoading.test.ts index a494009e..27fa86be 100644 --- a/src/modules/container-loading/api/platformContainerLoading.test.ts +++ b/src/modules/container-loading/api/platformContainerLoading.test.ts @@ -3,7 +3,7 @@ import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import { http } from '@/shared/api/http' import { - adjustSmartLoadingItem, assignSmartLoadingGroup, assignSmartLoadingItem, getContainer, + adjustSmartLoadingItem, assignPendingReviewSmartLoadingOrder, assignSmartLoadingGroup, assignSmartLoadingItem, getContainer, listContainers, listSmartLoadingCandidates, listSmartLoadingWorkbenchContainers, removeSmartLoadingGroup, removeSmartLoadingItem, getContainerClearance, downloadContainerClearance, uploadContainerClearance, completeContainerClearance, getContainerOverview, markContainerException, cancelContainerException, uploadContainerDocument, downloadContainerDocument, downloadContainerPdaPhoto, @@ -88,21 +88,39 @@ describe('platform container loading API', () => { expect(vi.mocked(http.delete).mock.calls[0]?.[1]).toMatchObject({ data: { expectedPlanVersion: 9, expectedItemVersion: 10, operationId: expect.stringContaining('remove-') } }) }) + it('uses the dedicated provisional assignment command for a pending-review order', async () => { + vi.mocked(http.post).mockResolvedValue({ code: '200', data: plan }) + + await assignPendingReviewSmartLoadingOrder('10', { + customerId: '21', orderId: '31', inboundCode: 'EXXHK2609020361', + packageCount: 100, expectedVersion: 5, + }) + + expect(http.post).toHaveBeenCalledWith( + '/platform/v1/first-leg/smart-loading/plans/10/pending-review-assignments', + expect.objectContaining({ + customerId: '21', orderId: '31', inboundCode: 'EXXHK2609020361', + packageCount: 100, expectedVersion: 5, + operationId: expect.stringContaining('assign-pending-review-'), + }), + ) + }) + it('queries workbench containers with the same global filters and excludes the retired booking parameter', async () => { vi.mocked(http.get).mockResolvedValueOnce({ code: '200', data: { records: [{ bookingId: 9007199254740993n, bookingNo: 'BK-1', cabinetNo: 'CAB-1', containerNo: null, warehouseId: 8, warehouseName: '义乌仓', channelCode: 'MATSON_CLX_TRUCK', channelName: '美森正班海卡', containerType: '40HC', planId: 9007199254740994n, planNo: 'LP-1', planStatus: 'EDITING', version: '2' }], page: '1', size: '200', total: '1', pages: '1', } }) - const result = await listSmartLoadingWorkbenchContainers({ channelGroupCodes: ['MATSON_CLX', 'EXX'], customerId: '12', inboundCode: 'IN-1', expectedArrivalStart: '2026-09-01', expectedArrivalEnd: '2026-09-03', orderStatus: 'WAIT_SPLIT', page: 1, size: 200 }) + const result = await listSmartLoadingWorkbenchContainers({ channelGroupCodes: ['MATSON_CLX', 'EXX'], customerId: '12', inboundCode: 'IN-1', createdStart: '2026-09-01', createdEnd: '2026-09-03', orderStatus: 'WAIT_SPLIT', page: 1, size: 200 }) expect(result).toMatchObject({ records: [{ bookingId: '9007199254740993', warehouseId: '8', planId: '9007199254740994', version: 2 }], total: 1 }) const params = vi.mocked(http.get).mock.calls.at(-1)?.[1]?.params as URLSearchParams expect(params.getAll('channelGroupCodes')).toEqual(['MATSON_CLX', 'EXX']) expect(params.get('customerId')).toBe('12') expect(params.get('inboundCode')).toBe('IN-1') - expect(params.get('expectedArrivalStart')).toBe('2026-09-01') - expect(params.get('expectedArrivalEnd')).toBe('2026-09-03') + expect(params.get('createdStart')).toBe('2026-09-01') + expect(params.get('createdEnd')).toBe('2026-09-03') expect(params.get('orderStatus')).toBe('WAIT_SPLIT') expect(params.has('bookingId')).toBe(false) }) diff --git a/src/modules/container-loading/api/platformContainerLoading.ts b/src/modules/container-loading/api/platformContainerLoading.ts index af9e69cc..d6d82bda 100644 --- a/src/modules/container-loading/api/platformContainerLoading.ts +++ b/src/modules/container-loading/api/platformContainerLoading.ts @@ -233,7 +233,7 @@ function smartLoadingQueryParams(query: SmartLoadingCandidateQuery) { for (const channelGroupCode of query.channelGroupCodes) params.append('channelGroupCodes', channelGroupCode) const optionalParams: Array<[string, string | number | undefined]> = [ ['warehouseId', query.warehouseId], ['customerId', query.customerId], ['inboundCode', query.inboundCode], - ['expectedArrivalStart', query.expectedArrivalStart], ['expectedArrivalEnd', query.expectedArrivalEnd], + ['createdStart', query.createdStart], ['createdEnd', query.createdEnd], ['orderStatus', query.orderStatus], ['page', query.page], ['size', query.size], ] for (const [key, value] of optionalParams) if (value !== undefined && value !== '') params.append(key, String(value)) @@ -249,7 +249,7 @@ export async function listSmartLoadingCandidates(query: SmartLoadingCandidateQue page: numberValue(result.page, query.page), size: numberValue(result.size, query.size), total: numberValue(result.total), pages: numberValue(result.pages), records: result.records.map((raw) => ({ - ...raw, accountId: id(raw.accountId), customerId: id(raw.customerId), inboundId: id(raw.inboundId), + ...raw, accountId: id(raw.accountId), customerId: id(raw.customerId), inboundId: optionalId(raw.inboundId), orderId: id(raw.orderId), warehouseId: id(raw.warehouseId), destinationWarehouseId: optionalId(raw.destinationWarehouseId), coLoadGroupId: optionalId(raw.coLoadGroupId), memberCount: raw.memberCount == null ? null : numberValue(raw.memberCount), @@ -297,6 +297,14 @@ export async function assignSmartLoadingItem(planId: string, payload: { inboundI return plan(unwrapApiResponse(response)) } +export async function assignPendingReviewSmartLoadingOrder(planId: string, payload: { customerId: string; orderId: string; inboundCode: string; packageCount: number; expectedVersion: number }) { + const response = await http.post, ApiEnvelope>( + `${smartLoadingPath}/plans/${encodeURIComponent(planId)}/pending-review-assignments`, + { ...payload, operationId: operationId('assign-pending-review') }, + ) + return plan(unwrapApiResponse(response)) +} + export async function assignSmartLoadingGroup(planId: string, inboundId: string, expectedVersion: number) { const response = await http.post, ApiEnvelope>( `${smartLoadingPath}/plans/${encodeURIComponent(planId)}/group-assignments`, diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts index 931719b7..6cffd382 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts @@ -10,6 +10,7 @@ import GlobalSmartLoadingView from './GlobalSmartLoadingView.vue' import { resolveSmartLoadingChannelGroup } from '../smartLoadingChannelGroups' import type { ContainerBookingResource } from '../types' import { + assignPendingReviewSmartLoadingOrder, assignSmartLoadingGroup, assignSmartLoadingItem, getSmartLoadingPlanByBooking, @@ -40,6 +41,7 @@ vi.mock('../api/platformContainerLoading', () => ({ listSmartLoadingWorkbenchContainers: vi.fn(), createSmartLoadingPlan: vi.fn(), assignSmartLoadingItem: vi.fn(), + assignPendingReviewSmartLoadingOrder: vi.fn(), assignSmartLoadingGroup: vi.fn(), recommendSmartLoading: vi.fn(), adjustSmartLoadingItem: vi.fn(), @@ -278,7 +280,7 @@ describe('GlobalSmartLoadingView', () => { expect(wrapper.text()).toContain('MSCU-DG') const containerRequestCount = vi.mocked(listSmartLoadingWorkbenchContainers).mock.calls.length - wrapper.findComponent(QueryToolbar).vm.$emit('search', { warehouseId: '18', customerId: '', inboundCode: '', expectedArrivalRange: [], orderStatus: '' }) + wrapper.findComponent(QueryToolbar).vm.$emit('search', { warehouseId: '18', customerId: '', inboundCode: '', createdRange: [], orderStatus: '' }) await flushPromises() expect(wrapper.text()).toContain('MSCU-DG') @@ -305,7 +307,7 @@ describe('GlobalSmartLoadingView', () => { await flushPromises() const containerRequestCount = vi.mocked(listSmartLoadingWorkbenchContainers).mock.calls.length - wrapper.findComponent(QueryToolbar).vm.$emit('search', { warehouseId: '18', customerId: '', inboundCode: '', expectedArrivalRange: [], orderStatus: '' }) + wrapper.findComponent(QueryToolbar).vm.$emit('search', { warehouseId: '18', customerId: '', inboundCode: '', createdRange: [], orderStatus: '' }) await flushPromises() expect(wrapper.find('.channel-tags .el-checkbox-button.is-checked').text()).toBe('美森正班') @@ -336,7 +338,7 @@ describe('GlobalSmartLoadingView', () => { const containerRequestCount = vi.mocked(listSmartLoadingWorkbenchContainers).mock.calls.length wrapper.findComponent(QueryToolbar).vm.$emit('search', { - warehouseId: '', customerId: '', inboundCode: 'NYEXX2609030002', expectedArrivalRange: [], orderStatus: '', + warehouseId: '', customerId: '', inboundCode: 'NYEXX2609030002', createdRange: [], orderStatus: '', }) await flushPromises() @@ -374,7 +376,7 @@ describe('GlobalSmartLoadingView', () => { const containerRequestCount = vi.mocked(listSmartLoadingWorkbenchContainers).mock.calls.length wrapper.getComponent(QueryToolbar).vm.$emit('search', { - customerId: '', inboundCode: '', orderStatus: 'WAIT_SPLIT', expectedArrivalRange: ['2026-09-10', '2026-09-12'], + customerId: '', inboundCode: '', orderStatus: 'WAIT_SPLIT', createdRange: ['2026-09-10', '2026-09-12'], }) await flushPromises() @@ -382,7 +384,7 @@ describe('GlobalSmartLoadingView', () => { expect(candidates).toContain('IN-MATCH') expect(candidates).not.toContain('IN-WRONG-STATUS') expect(candidates).not.toContain('IN-WRONG-DATE') - expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith({ channelGroupCodes: ['MATSON_CLX'], expectedArrivalStart: '2026-09-10', expectedArrivalEnd: '2026-09-12', orderStatus: 'WAIT_SPLIT', page: 1, size: 20 }) + expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith({ channelGroupCodes: ['MATSON_CLX'], createdStart: '2026-09-10', createdEnd: '2026-09-12', orderStatus: 'WAIT_SPLIT', page: 1, size: 20 }) expect(listSmartLoadingWorkbenchContainers).toHaveBeenCalledTimes(containerRequestCount) expect(listSmartLoadingWorkbenchContainers).toHaveBeenLastCalledWith({ channelGroupCodes: ['MATSON_CLX'], page: 1, size: 200 }) }) @@ -445,6 +447,37 @@ describe('GlobalSmartLoadingView', () => { wrapper.unmount() }) + it('allows a pending-review order to join through the provisional assignment command', async () => { + vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([{ + ...candidate, + customerId: '1', + inboundId: null, + orderStatusSnapshot: 'PENDING_REVIEW', + inboundCode: 'EXXHK2609020361', + blockReason: null, + }])) + vi.mocked(assignPendingReviewSmartLoadingOrder).mockResolvedValue(plan) + const wrapper = mount(GlobalSmartLoadingView, { attachTo: document.body, global: { plugins: [ElementPlus] } }) + await flushPromises() + + expect(wrapper.get('.candidate-pane').text()).toContain('待审核') + const add = wrapper.get('.candidate-pane .el-table__body-wrapper').findAll('button') + .find((button) => button.text().includes('加入'))! + expect(add.classes()).not.toContain('is-disabled') + await add.trigger('click') + await flushPromises() + ;([...document.body.querySelectorAll('button')] + .find((button) => button.textContent?.includes('确认加入')) as HTMLButtonElement).click() + await flushPromises() + + expect(assignPendingReviewSmartLoadingOrder).toHaveBeenCalledWith(plan.id, { + customerId: '1', orderId: '7', inboundCode: 'EXXHK2609020361', + packageCount: 12, expectedVersion: plan.version, + }) + expect(assignSmartLoadingItem).not.toHaveBeenCalled() + wrapper.unmount() + }) + it('blocks an incomplete three-member candidate group without exposing member assignment', async () => { const members = ['401', '402', '403'].map((inboundId, index) => ({ accountId: `${index + 1}`, inboundId, orderId: `50${index}`, orderNo: `ORD-${index}`, inboundCode: `IN-${inboundId}`, customerName: `客户${index}`, availablePackageCount: 6, orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: null })) vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([{ @@ -488,7 +521,7 @@ describe('GlobalSmartLoadingView', () => { await flushPromises() const fields = wrapper.findComponent(QueryToolbar).props('fields') as Array<{ key: string; options?: Array<{ label: string; value: string }> }> - expect(fields.map((field) => field.key)).toEqual(['warehouseId', 'customerId', 'inboundCode', 'expectedArrivalRange', 'orderStatus']) + expect(fields.map((field) => field.key)).toEqual(['warehouseId', 'customerId', 'inboundCode', 'createdRange', 'orderStatus']) expect(fields.find((field) => field.key === 'warehouseId')?.options).toEqual([{ label: '义乌仓', value: '8' }]) expect(fields.find((field) => field.key === 'orderStatus')?.options).toEqual([ { value: '', label: '全部' }, @@ -759,6 +792,7 @@ describe('GlobalSmartLoadingView', () => { expect(source).toContain('size="small"') expect(source).toContain('type="danger" plain') expect(source).toContain(':max="adjustmentMax(row)"') + expect(source).toContain("{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 70, slotName: 'customerName' }") expect(source).not.toContain('link type="danger"') expect(source).toContain('@media (max-width: 720px)') expect(source.match(/ { expect(source).toContain('.candidate-pane,.plan-pane { height: min(68vh, 560px); min-height: 420px; }') expect(source).toContain('.container-pane { max-height: min(58vh, 460px); }') expect(source).toContain('min-height: 112px') - expect(source).toContain("label: '预计到货时间'") + expect(source).toContain("label: '创建日期'") expect(source).toContain("label: '订单状态'") expect(source).toContain('.smart-loading-query .query-toolbar__filters) { flex-wrap: nowrap; gap: 8px; overflow-x: auto; }') expect(source).toContain('.smart-loading-query .query-toolbar__actions) { flex-wrap: nowrap; gap: 6px; }') diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.vue b/src/modules/container-loading/pages/GlobalSmartLoadingView.vue index 9a83fb45..9594301d 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.vue +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.vue @@ -9,10 +9,12 @@ import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import AppDialog from '@/shared/components/AppDialog.vue' import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue' +import LineClampText from '@/shared/components/LineClampText.vue' import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import { adjustSmartLoadingItem, + assignPendingReviewSmartLoadingOrder, assignSmartLoadingGroup, assignSmartLoadingItem, getSmartLoadingPlanByBooking, @@ -61,7 +63,7 @@ const selectedCandidate = ref(null) const assignForm = reactive({ packageCount: 1 }) const quantityDrafts = reactive>({}) const planGroupsByInboundId = ref(new Map()) -const query = ref({ warehouseId: '', customerId: '', inboundCode: '', expectedArrivalRange: [], orderStatus: '' }) +const query = ref({ warehouseId: '', customerId: '', inboundCode: '', createdRange: [], orderStatus: '' }) const appliedQuery = ref({ ...query.value }) const candidatePage = ref(1) const candidatePageSize = ref(20) @@ -120,12 +122,12 @@ function candidateTotalVolume(candidate: SmartLoadingCandidate) { } const displayedCandidates = computed(() => { return candidates.value.map((candidate) => { - const members: Array<{ accountId: string; inboundId: string; orderId: string; orderNo?: string | null; inboundCode?: string | null }> = candidate.groupMembers.length + const members: Array<{ accountId: string; inboundId: string | null; orderId: string; orderNo?: string | null; inboundCode?: string | null }> = candidate.groupMembers.length ? candidate.groupMembers : [candidate] return { ...candidate, - memberInboundIds: members.map((member) => member.inboundId), + memberInboundIds: members.map((member) => member.inboundId).filter((id): id is string => Boolean(id)), memberInboundCodes: members.map((member) => member.inboundCode || '入仓码暂缺'), memberOrderSummaries: memberOrderSummaries(members), memberOrderCount: new Set(members.map((member) => member.orderId)).size, @@ -189,11 +191,14 @@ const candidateOrderStatusOptions = [ { value: 'WAIT_SPLIT', label: '待分柜' }, { value: 'LOADING', label: '装柜中' }, ] +function candidateStatusLabel(status: string | null | undefined) { + return candidateOrderStatusOptions.find((item) => item.value === status)?.label || '—' +} const queryFields = computed(() => [ { key: 'warehouseId', type: 'select', label: '发货仓', placeholder: '全部发货仓', width: 160, options: warehouseOptions.value, props: { filterable: true } }, { key: 'customerId', type: 'select', label: '客户', placeholder: '全部客户', width: 180, options: customers.value.filter((customer) => customer.status === 'ACTIVE').map((customer) => ({ label: customer.tenantName, value: String(customer.id) })), props: { filterable: true, loading: customersLoading.value } }, { key: 'inboundCode', type: 'input', label: '入仓码', placeholder: '请输入入仓码', width: 210 }, - { key: 'expectedArrivalRange', type: 'daterange', label: '预计到货时间', placeholder: '请选择预计到货时间范围', width: 260 }, + { key: 'createdRange', type: 'daterange', label: '创建日期', placeholder: '请选择创建日期范围', width: 260 }, { key: 'orderStatus', type: 'select', label: '订单状态', placeholder: '全部订单状态', width: 160, options: candidateOrderStatusOptions }, ]) @@ -216,7 +221,8 @@ const candidateColumns: DataTableColumn[] = [ { key: 'actions', label: '操作', width: 92, fixed: 'left', slotName: 'actions' }, { key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 140, fixed: 'left', showOverflowTooltip: true }, { key: 'coLoadGroup', label: '同柜', minWidth: 80, slotName: 'coLoadGroup' }, - { key: 'customerName', prop: 'customerName', label: '客户', minWidth: 70 }, + { key: 'customerName', prop: 'customerName', label: '客户', minWidth: 70, slotName: 'customerName' }, + { key: 'orderStatusSnapshot', prop: 'orderStatusSnapshot', label: '订单状态', width: 100, slotName: 'orderStatusSnapshot' }, { key: 'availablePackageCount', prop: 'availablePackageCount', label: '可用箱数', width: 80 }, // { key: 'cartonIdentityStatus', label: '箱号归属', width: 105, slotName: 'cartonIdentityStatus' }, { key: 'unitGrossWeightKg', label: '单箱毛重', width: 80, slotName: 'unitGrossWeight' }, @@ -228,7 +234,7 @@ const itemColumns: DataTableColumn[] = [ { key: 'actions', label: '操作', width: 80, fixed: 'left', slotName: 'actions' }, { key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 140, fixed: 'left', showOverflowTooltip: true }, { key: 'coLoadGroup', label: '同柜', minWidth: 80, slotName: 'coLoadGroup' }, - { key: 'customerName', prop: 'customerName', label: '客户', minWidth: 70 }, + { key: 'customerName', prop: 'customerName', label: '客户', minWidth: 70, slotName: 'customerName' }, { key: 'packageCount', label: '分配箱数', width: 100, slotName: 'packageCount' }, { key: 'grossWeightKg', label: '毛重', width: 80, slotName: 'weight' }, { key: 'volumeCbm', label: '体积', width: 80, slotName: 'volume' }, @@ -283,8 +289,8 @@ function clearSelectionData() { } function candidateQueryParams(): Parameters[0] { - const arrivalRange = Array.isArray(appliedQuery.value.expectedArrivalRange) - ? appliedQuery.value.expectedArrivalRange.map((value) => String(value || '').slice(0, 10)) + const createdRange = Array.isArray(appliedQuery.value.createdRange) + ? appliedQuery.value.createdRange.map((value) => String(value || '').slice(0, 10)) : [] const params: Parameters[0] = { channelGroupCodes: [...selectedChannelCodes.value], @@ -298,9 +304,9 @@ function candidateQueryParams(): Parameters[0 if (warehouseId) params.warehouseId = warehouseId if (customerId) params.customerId = customerId if (inboundCode) params.inboundCode = inboundCode - if (arrivalRange.length === 2) { - params.expectedArrivalStart = arrivalRange[0] - params.expectedArrivalEnd = arrivalRange[1] + if (createdRange.length === 2) { + params.createdStart = createdRange[0] + params.createdEnd = createdRange[1] } if (orderStatus) params.orderStatus = orderStatus return params @@ -471,10 +477,18 @@ async function assign() { } await mutate(async () => { const selected = selectedCandidate.value! - acceptPlan(selected.coLoadGroupId + acceptPlan(selected.orderStatusSnapshot === 'PENDING_REVIEW' + ? await assignPendingReviewSmartLoadingOrder(plan.value!.id, { + customerId: selected.customerId, + orderId: selected.orderId, + inboundCode: selected.inboundCode || '', + packageCount: assignForm.packageCount, + expectedVersion: plan.value!.version, + }) + : selected.coLoadGroupId && selected.inboundId ? await assignSmartLoadingGroup(plan.value!.id, selected.inboundId, plan.value!.version) : await assignSmartLoadingItem(plan.value!.id, { - inboundId: selected.inboundId, + inboundId: selected.inboundId!, packageCount: assignForm.packageCount, expectedVersion: plan.value!.version, })) @@ -484,7 +498,13 @@ async function assign() { }, '分配明细失败', { preserveAssign: true }) } function candidateBlocked(candidate: SmartLoadingCandidateRow) { + if (candidate.orderStatusSnapshot === 'PENDING_REVIEW') { + return Boolean(candidate.blockReason) || candidate.locked === true + || !candidate.inboundCode || candidate.availablePackageCount < 1 + } return Boolean(candidate.blockReason) || candidate.locked === true + || !candidate.inboundId + || !candidate.inboundCode || candidate.memberInboundIds.some((inboundId) => assignedInboundIds.value.has(inboundId)) || candidate.availablePackageCount < 1 } @@ -676,7 +696,9 @@ onMounted(() => { void load(); void loadCustomers() }) @click="openAssign(row)" >{{ row.coLoadGroupId ? '整组分配' : '加入' }} + + @@ -747,6 +769,7 @@ onMounted(() => { void load(); void loadCustomers() }) + diff --git a/src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue b/src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue index d25803a0..f4a021fb 100644 --- a/src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue +++ b/src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue @@ -9,6 +9,7 @@ import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import AppDialog from '@/shared/components/AppDialog.vue' import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue' +import LineClampText from '@/shared/components/LineClampText.vue' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import { adjustSmartLoadingItem, assignSmartLoadingGroup, assignSmartLoadingItem, getContainer, @@ -62,12 +63,12 @@ function groupMemberLabel(group: SmartLoadingPlanCoLoadGroup) { } const displayedCandidates = computed(() => { return candidates.value.map((candidate) => { - const members: Array<{ inboundId: string; orderId: string; orderNo?: string | null; inboundCode?: string | null }> = candidate.groupMembers.length + const members: Array<{ inboundId: string | null; orderId: string; orderNo?: string | null; inboundCode?: string | null }> = candidate.groupMembers.length ? candidate.groupMembers : [candidate] return { ...candidate, - memberInboundIds: members.map((member) => member.inboundId), + memberInboundIds: members.map((member) => member.inboundId).filter((id): id is string => Boolean(id)), memberInboundCodes: members.map((member) => member.inboundCode || '入仓码暂缺'), memberOrderSummaries: memberOrderSummaries(members), memberOrderCount: new Set(members.map((member) => member.orderId)).size, @@ -129,7 +130,7 @@ const itemColumns: DataTableColumn[] = [ { key: 'sequence', prop: 'loadSequence', label: '顺序', width: 75 }, { key: 'order', label: '入仓码', minWidth: 190, slotName: 'order' }, { key: 'coLoadGroup', label: '同柜不可拆', minWidth: 210, slotName: 'coLoadGroup' }, - { key: 'customerName', prop: 'customerName', label: '客户', minWidth: 150 }, + { key: 'customerName', prop: 'customerName', label: '客户', minWidth: 150, slotName: 'customerName' }, { key: 'goods', label: '货物', minWidth: 190, slotName: 'goods' }, { key: 'packageCount', label: '分配箱数', width: 150, slotName: 'packageCount' }, { key: 'volumeCbm', label: '体积', width: 120, slotName: 'volume' }, @@ -139,7 +140,7 @@ const candidateColumns: DataTableColumn[] = [ { key: 'actions', label: '操作', width: 110, fixed: 'left', slotName: 'actions' }, { key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 190, fixed: 'left', showOverflowTooltip: true }, { key: 'coLoadGroup', label: '同柜不可拆', minWidth: 210, slotName: 'coLoadGroup' }, - { key: 'customerName', prop: 'customerName', label: '客户', minWidth: 150 }, + { key: 'customerName', prop: 'customerName', label: '客户', minWidth: 150, slotName: 'customerName' }, { key: 'goodsSummary', prop: 'goodsSummary', label: '货物摘要', minWidth: 180, showOverflowTooltip: true }, { key: 'availablePackageCount', prop: 'availablePackageCount', label: '可用箱数', width: 110 }, { key: 'cartonIdentityStatus', label: '箱号归属', width: 110, slotName: 'cartonIdentityStatus' }, @@ -217,15 +218,17 @@ function openAssign(source: object) { assignOpen.value = true } async function assign() { - if (!plan.value || !selectedCandidate.value || assignForm.packageCount < 1) return - if (selectedCandidate.value.coLoadGroupId && (selectedCandidate.value.blockReason || selectedCandidate.value.locked)) return + const currentPlan = plan.value + const selected = selectedCandidate.value + const inboundId = selected?.inboundId + if (!currentPlan || !selected || !inboundId || assignForm.packageCount < 1) return + if (selected.coLoadGroupId && (selected.blockReason || selected.locked)) return mutating.value = true mutationError.value = null try { - const selected = selectedCandidate.value const next = selected.coLoadGroupId - ? await assignSmartLoadingGroup(plan.value.id, selected.inboundId, plan.value.version) - : await assignSmartLoadingItem(plan.value.id, { inboundId: selected.inboundId, packageCount: assignForm.packageCount, expectedVersion: plan.value.version }) + ? await assignSmartLoadingGroup(currentPlan.id, inboundId, currentPlan.version) + : await assignSmartLoadingItem(currentPlan.id, { inboundId, packageCount: assignForm.packageCount, expectedVersion: currentPlan.version }) await acceptPlanWithGroups(next) assignOpen.value = false ElMessage.success(selected.coLoadGroupId ? `同柜不可拆组 ${selected.groupNo} 已整组分配` : '货物已分配') @@ -234,6 +237,7 @@ async function assign() { } function candidateBlocked(candidate: SmartLoadingCandidateRow) { return Boolean(candidate.blockReason) || candidate.locked === true + || !candidate.inboundId || candidate.memberInboundIds.some((inboundId) => assignedInboundIds.value.has(inboundId)) || candidate.availablePackageCount < 1 } @@ -356,6 +360,7 @@ onMounted(load) + @@ -364,6 +369,7 @@ onMounted(load) + diff --git a/src/modules/container-loading/types.ts b/src/modules/container-loading/types.ts index a2b626f6..8e0b4743 100644 --- a/src/modules/container-loading/types.ts +++ b/src/modules/container-loading/types.ts @@ -232,7 +232,7 @@ export interface ContainerSaveRequest { export interface SmartLoadingCandidate { accountId: string customerId: string - inboundId: string + inboundId: string | null orderId: string orderNo: string | null inboundCode: string | null @@ -277,8 +277,8 @@ export interface SmartLoadingCandidateQuery { warehouseId?: string customerId?: string inboundCode?: string - expectedArrivalStart?: string - expectedArrivalEnd?: string + createdStart?: string + createdEnd?: string orderStatus?: 'PENDING_REVIEW' | 'WAIT_MEASURE' | 'WAIT_SPLIT' | 'LOADING' page: number size: number diff --git a/src/modules/inbounds/components/InboundTaskList.vue b/src/modules/inbounds/components/InboundTaskList.vue index f4ff9ef8..b24a674c 100644 --- a/src/modules/inbounds/components/InboundTaskList.vue +++ b/src/modules/inbounds/components/InboundTaskList.vue @@ -12,6 +12,7 @@ import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue' import ListPageLayout from '@/shared/components/ListPageLayout.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue' +import LineClampText from '@/shared/components/LineClampText.vue' import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue' import { firstLegBusinessTypeLabel } from '@/shared/utils/firstLegBusinessLabels' import { listPlatformInbounds } from '../core/api' @@ -82,26 +83,26 @@ const queryFields = computed(() => [ ]) const columns = computed(() => [ - { key: 'actions', label: '操作', width: 190, fixed: 'left', slotName: 'actions' }, + { key: 'actions', label: '操作', width: 160, fixed: 'left', slotName: 'actions' }, ...(isReview.value && access.can(actionPermission.value) ? [{ key: 'selection', type: 'selection', width: 48, fixed: 'left', selectable: (row: InboundOrderSummary) => row.measureReviewStatus === 'PENDING' && !batchReviewLoading.value, } satisfies DataTableColumn] : []), - { key: 'inboundCode', label: '入仓码', minWidth: 170, slotName: 'inboundCode' }, + { key: 'inboundCode', label: '入仓码', minWidth: 140, slotName: 'inboundCode' }, { key: 'processStatus', label: '流程状态', minWidth: 132, slotName: 'processStatus' }, - { key: 'measureReviewStatus', label: '测量审核状态', minWidth: 142, slotName: 'measureReviewStatus' }, - { key: 'expectedArrivalAt', label: '预计到达时间', minWidth: 164, slotName: 'expectedArrivalAt' }, - { key: 'customerName', label: '客户', minWidth: 160, slotName: 'customerName' }, - { key: 'warehouseName', label: '仓库', minWidth: 140, slotName: 'warehouseName' }, - { key: 'deliveryWarehouseName', label: '交货仓', minWidth: 140, slotName: 'deliveryWarehouseName' }, - { key: 'businessType', label: '业务类型', minWidth: 120, slotName: 'businessType' }, - { key: 'channelName', label: '渠道', minWidth: 140, slotName: 'channelName' }, - { key: 'totalPackageCount', label: '总箱数', width: 92, slotName: 'totalPackageCount' }, - { key: 'palletCount', label: '托盘数', width: 92, slotName: 'palletCount' }, - { key: 'actualGrossWeightKg', label: '实测毛重(kg)', minWidth: 128, slotName: 'actualGrossWeightKg' }, - { key: 'actualNetWeightKg', label: '货物重量(kg)', minWidth: 128, slotName: 'actualNetWeightKg' }, - { key: 'actualVolumeCbm', label: '实测体积(m³)', minWidth: 128, slotName: 'actualVolumeCbm' }, - { key: 'measureCompletedAt', label: '提交时间', minWidth: 164, slotName: 'measureCompletedAt' }, + { key: 'measureReviewStatus', label: '测量审核状态', minWidth: 120, slotName: 'measureReviewStatus' }, + { key: 'expectedArrivalAt', label: '预计到达时间', minWidth: 140, slotName: 'expectedArrivalAt' }, + { key: 'customerName', label: '客户', minWidth: 140, slotName: 'customerName' }, + { key: 'warehouseName', label: '仓库', minWidth: 120, slotName: 'warehouseName' }, + { key: 'deliveryWarehouseName', label: '交货仓', minWidth: 120, slotName: 'deliveryWarehouseName' }, + { key: 'businessType', label: '业务类型', minWidth: 100, slotName: 'businessType' }, + { key: 'channelName', label: '渠道', minWidth: 100, slotName: 'channelName' }, + { key: 'totalPackageCount', label: '总箱数', width: 90, slotName: 'totalPackageCount' }, + { key: 'palletCount', label: '托盘数', width: 90, slotName: 'palletCount' }, + { key: 'actualGrossWeightKg', label: '实测毛重(kg)', minWidth: 100, slotName: 'actualGrossWeightKg' }, + { key: 'actualNetWeightKg', label: '货物重量(kg)', minWidth: 100, slotName: 'actualNetWeightKg' }, + { key: 'actualVolumeCbm', label: '实测体积(m³)', minWidth: 100, slotName: 'actualVolumeCbm' }, + { key: 'measureCompletedAt', label: '提交时间', minWidth: 140, slotName: 'measureCompletedAt' }, ]) function text(value: unknown) { @@ -381,11 +382,11 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria - + - + diff --git a/src/modules/inbounds/pages/InboundOrdersView.vue b/src/modules/inbounds/pages/InboundOrdersView.vue index b621da84..5fe1610c 100644 --- a/src/modules/inbounds/pages/InboundOrdersView.vue +++ b/src/modules/inbounds/pages/InboundOrdersView.vue @@ -11,6 +11,7 @@ import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.v import ListPageLayout from '@/shared/components/ListPageLayout.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue' import InboundCodeRelationshipDialog from '@/shared/components/InboundCodeRelationshipDialog.vue' +import LineClampText from '@/shared/components/LineClampText.vue' import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue' import { firstLegBusinessTypeLabel, firstLegContainerModeLabel, firstLegDestinationTypeLabel, firstLegTaxModeLabel } from '@/shared/utils/firstLegBusinessLabels' import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam' @@ -424,7 +425,7 @@ onBeforeUnmount(() => { disposed = true; loadRequestId += 1; customerRequestId + - + @@ -434,7 +435,7 @@ onBeforeUnmount(() => { disposed = true; loadRequestId += 1; customerRequestId + - + diff --git a/src/modules/loading-lists/components/LoadingListsView.vue b/src/modules/loading-lists/components/LoadingListsView.vue index 17bb3d3f..e485adaf 100644 --- a/src/modules/loading-lists/components/LoadingListsView.vue +++ b/src/modules/loading-lists/components/LoadingListsView.vue @@ -10,6 +10,7 @@ import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue' import ListPageLayout from '@/shared/components/ListPageLayout.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue' +import LineClampText from '@/shared/components/LineClampText.vue' import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue' import StatusBadge from '@/shared/components/StatusBadge.vue' import { loadingListStatusLabel } from '../status' @@ -50,20 +51,20 @@ const fields = computed(() => [ ]) const columns = computed(() => isPreload.value ? [ - { key: 'actions', label: '操作', fixed: 'left', width: 110, slotName: 'actions', hideable: false, movable: false }, - { key: 'preloadNo', prop: 'preloadNo', label: '预装单号', minWidth: 165 }, - { key: 'containerNo', prop: 'containerNo', label: '集装箱号', minWidth: 145 }, - { key: 'cabinetNo', prop: 'cabinetNo', label: '柜次号', minWidth: 150 }, - { key: 'customers', label: '客户', minWidth: 190, slotName: 'customers' }, - { key: 'warehouseName', prop: 'warehouseName', label: '仓库', minWidth: 140 }, + { key: 'actions', label: '操作', fixed: 'left', width: 88, slotName: 'actions', hideable: false, movable: false }, + { key: 'preloadNo', prop: 'preloadNo', label: '预装单号', minWidth: 120 }, + { key: 'containerNo', prop: 'containerNo', label: '集装箱号', minWidth: 120 }, + { key: 'cabinetNo', prop: 'cabinetNo', label: '柜次号', minWidth: 140 }, + // { key: 'customers', label: '客户', minWidth: 190, slotName: 'customers' }, + { key: 'warehouseName', prop: 'warehouseName', label: '仓库', minWidth: 80 }, { key: 'vesselVoyage', label: '船名 / 航次', minWidth: 175, slotName: 'vesselVoyage' }, - { key: 'containerType', prop: 'containerType', label: '箱型', width: 100 }, + { key: 'containerType', prop: 'containerType', label: '箱型', width: 80 }, { key: 'status', label: '状态', width: 108, slotName: 'status' }, - { key: 'skuCount', prop: 'skuCount', label: 'SKU 数', width: 90, align: 'right' }, { key: 'totalPackageCount', prop: 'totalPackageCount', label: '总箱数', width: 90, align: 'right' }, - { key: 'totalVolumeCbm', label: '总体积(m³)', width: 120, align: 'right', slotName: 'volume' }, - { key: 'totalGrossWeightKg', label: '总毛重(kg)', width: 125, align: 'right', slotName: 'grossWeight' }, - { key: 'createdAt', label: '创建时间', minWidth: 155, slotName: 'createdAt' }, + { key: 'skuCount', prop: 'skuCount', label: 'SKU 数', width: 90, align: 'right' }, + { key: 'totalVolumeCbm', label: '总体积(m³)', width: 90, align: 'right', slotName: 'volume' }, + { key: 'totalGrossWeightKg', label: '总毛重(kg)', width: 90, align: 'right', slotName: 'grossWeight' }, + { key: 'createdAt', label: '创建时间', minWidth: 140, slotName: 'createdAt' }, ] : [ { key: 'actions', label: '操作', fixed: 'left', width: 110, slotName: 'actions', hideable: false, movable: false }, { key: 'loadingListNo', prop: 'loadingListNo', label: '装柜清单号', minWidth: 175 }, @@ -211,7 +212,7 @@ onMounted(() => { 查看 - + diff --git a/src/modules/master-data/counterparties/pages/PartnersView.test.ts b/src/modules/master-data/counterparties/pages/PartnersView.test.ts index f5d4f5f5..f0b39b09 100644 --- a/src/modules/master-data/counterparties/pages/PartnersView.test.ts +++ b/src/modules/master-data/counterparties/pages/PartnersView.test.ts @@ -93,6 +93,12 @@ enableAutoUnmount(afterEach) afterAll(() => vi.unstubAllGlobals()) describe('PartnersView real API integration', () => { + it('clamps long unit names with the shared component', () => { + expect(source).toContain("LineClampText from '@/shared/components/LineClampText.vue'") + expect(source).toContain('') + expect(source).not.toContain('.partner-cell strong') + }) + it('keeps operation first and separates the prototype contact columns', () => { const actionsIndex = source.indexOf("{ key: 'actions', label: '操作'") const nameIndex = source.indexOf("{ key: 'partner', label: '单位名称'") diff --git a/src/modules/master-data/counterparties/pages/PartnersView.vue b/src/modules/master-data/counterparties/pages/PartnersView.vue index e7d1a86f..bf1662a7 100644 --- a/src/modules/master-data/counterparties/pages/PartnersView.vue +++ b/src/modules/master-data/counterparties/pages/PartnersView.vue @@ -11,6 +11,7 @@ import DetailPageLayout from '@/shared/components/DetailPageLayout.vue' import FormSection from '@/shared/components/FormSection.vue' import ListPageLayout from '@/shared/components/ListPageLayout.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue' +import LineClampText from '@/shared/components/LineClampText.vue' import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue' import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' @@ -408,7 +409,7 @@ onMounted(() => { {{ (row as PlatformPartnerResource).status === 'ACTIVE' ? '停用' : '启用' }} - + @@ -495,7 +496,7 @@ onMounted(() => { .partner-cell { display: flex; align-items: center; gap: 10px; } .partner-cell > span { display: grid; width: 34px; height: 34px; flex: 0 0 34px; place-items: center; border-radius: 7px; background: #eef3fa; color: var(--cc-primary); } .partner-cell > div { display: flex; min-width: 0; flex-direction: column; gap: 3px; } -.partner-cell strong { overflow-wrap: anywhere; } +.partner-cell :deep(.line-clamp-text) { width: 100%; } .partner-address { display: flex; align-items: flex-start; gap: 7px; line-height: 1.55; overflow-wrap: anywhere; } .partner-address svg { flex: 0 0 15px; margin-top: 3px; color: var(--cc-muted); } .partner-detail { min-height: 180px; padding: 20px 24px 28px; box-sizing: border-box; } diff --git a/src/modules/suppliers/pages/SuppliersView.test.ts b/src/modules/suppliers/pages/SuppliersView.test.ts index e1351056..bee023be 100644 --- a/src/modules/suppliers/pages/SuppliersView.test.ts +++ b/src/modules/suppliers/pages/SuppliersView.test.ts @@ -8,6 +8,13 @@ const detailSource = readFileSync(resolve(process.cwd(), 'src/modules/suppliers/ const routesSource = readFileSync(resolve(process.cwd(), 'src/modules/suppliers/routes.ts'), 'utf8') describe('supplier formal pages', () => { + it('clamps long supplier and represented-carrier names with the shared component', () => { + expect(source).toContain("LineClampText from '@/shared/components/LineClampText.vue'") + expect(source).toContain('') + expect(source).toContain(' - + - + @@ -168,7 +169,7 @@ onActivated(() => { diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.vue b/src/modules/unified-orders/pages/UnifiedOrdersView.vue index c5ffa007..0a2ba7ab 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.vue +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.vue @@ -2223,7 +2223,7 @@ onBeforeUnmount(() => { - + - + diff --git a/src/shared/components/LineClampText.domestic-lists.test.ts b/src/shared/components/LineClampText.domestic-lists.test.ts new file mode 100644 index 00000000..74cd2d5e --- /dev/null +++ b/src/shared/components/LineClampText.domestic-lists.test.ts @@ -0,0 +1,50 @@ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { describe, expect, it } from 'vitest' + +const read = (path: string) => readFileSync(resolve(process.cwd(), path), 'utf8') + +describe('LineClampText domestic list consumers', () => { + it.each([ + ['统一订单', 'src/modules/unified-orders/pages/UnifiedOrdersView.vue'], + ['入库任务', 'src/modules/inbounds/components/InboundTaskList.vue'], + ['入库单', 'src/modules/inbounds/pages/InboundOrdersView.vue'], + ['装柜清单', 'src/modules/loading-lists/components/LoadingListsView.vue'], + ['柜次智能装柜', 'src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue'], + ['全局智能装柜', 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'], + ])('%s列表复用共享的行数省略组件', (_name, path) => { + expect(read(path)).toContain("LineClampText from '@/shared/components/LineClampText.vue'") + }) + + it('统一订单客户与渠道都使用一行省略,并保留渠道变更标记', () => { + const source = read('src/modules/unified-orders/pages/UnifiedOrdersView.vue') + expect(source).toContain("column.property === 'customerName'") + expect(source).toContain('') + expect(source).toContain('曾变更渠道') + }) + + it.each([ + ['src/modules/inbounds/components/InboundTaskList.vue', '#customerName', '#channelName'], + ['src/modules/inbounds/pages/InboundOrdersView.vue', '#customerName', '#channelName'], + ])('入库列表客户与渠道均使用共享组件:%s', (path, customerSlot, channelSlot) => { + const source = read(path) + expect(source).toContain(`