From 835d1d021615aa2ba7697fa33b2f542213171c64 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sat, 15 Aug 2026 17:04:00 +0800 Subject: [PATCH] feat: connect plan-level preload confirmation --- src/app/DomesticOutboundListLayout.test.ts | 2 +- src/modules/container-loading/index.ts | 1 + .../pages/ContainersView.test.ts | 6 +- .../pages/ContainersView.vue | 24 ++-- .../pages/GlobalSmartLoadingView.test.ts | 8 +- .../pages/GlobalSmartLoadingView.vue | 39 +++--- .../pages/SmartLoadingWorkbenchView.test.ts | 6 +- .../pages/SmartLoadingWorkbenchView.vue | 32 +++-- .../api/platformLoadingLists.test.ts | 54 ++++++- .../loading-lists/api/platformLoadingLists.ts | 65 ++++++++- .../components/LoadingListDetailDrawer.vue | 38 ++++- .../components/LoadingListsView.test.ts | 65 ++++++--- .../components/LoadingListsView.vue | 132 +----------------- .../pages/PreloadPlanDetailView.test.ts | 26 ++++ .../pages/PreloadPlanDetailView.vue | 65 ++++++++- src/modules/loading-lists/routes.test.ts | 1 + .../pages/OceanBookingDetailView.test.ts | 2 +- .../pages/OceanBookingDetailView.vue | 3 +- .../components/TruckAppointmentForm.test.ts | 5 + .../components/TruckAppointmentForm.vue | 2 +- .../pages/UnifiedOrdersView.vue | 2 +- .../unified-orders/workflows/README.md | 2 +- .../unified-orders/workflows/api.test.ts | 10 -- src/modules/unified-orders/workflows/api.ts | 28 ---- .../UnifiedOrderWorkflowPanel.test.ts | 14 ++ .../components/UnifiedOrderWorkflowPanel.vue | 33 +++-- 26 files changed, 398 insertions(+), 267 deletions(-) diff --git a/src/app/DomesticOutboundListLayout.test.ts b/src/app/DomesticOutboundListLayout.test.ts index 73bd1633..888cdd21 100644 --- a/src/app/DomesticOutboundListLayout.test.ts +++ b/src/app/DomesticOutboundListLayout.test.ts @@ -9,7 +9,7 @@ const pages = [ ['src/modules/port-schedules/pages/PortSchedulesView.vue', "prop: 'schedule.scheduleNo'"], ['src/modules/trucking/pages/TruckAppointmentsView.vue', "key: 'appointmentNo'"], ['src/modules/customs/pages/CustomsCasesView.vue', "prop: 'customsNo'"], - ['src/modules/outbound-dispatch/pages/OutboundDispatchesView.vue', "prop: 'orderNo'"], + ['src/modules/outbound-dispatch/pages/OutboundDispatchesView.vue', "prop: 'inboundCode'"], ] as const function source(path: string) { diff --git a/src/modules/container-loading/index.ts b/src/modules/container-loading/index.ts index f03c0327..d8c0bdbe 100644 --- a/src/modules/container-loading/index.ts +++ b/src/modules/container-loading/index.ts @@ -1,2 +1,3 @@ export * from './api/platformContainerLoading' export * from './types' +export { default as SmartLoadingDialog } from './components/SmartLoadingDialog.vue' diff --git a/src/modules/container-loading/pages/ContainersView.test.ts b/src/modules/container-loading/pages/ContainersView.test.ts index 8a44a043..e0d10569 100644 --- a/src/modules/container-loading/pages/ContainersView.test.ts +++ b/src/modules/container-loading/pages/ContainersView.test.ts @@ -199,7 +199,7 @@ describe('ContainersView', () => { it('keeps the prototype field sequence ahead of contract-only supplemental fields', () => { const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8') - const labels = ['集装箱号', '柜号', '装柜仓库', '提单号', '船名', '航次', '箱型(货柜类型)', '船公司', '起运港', '目的港', '目的仓', '货柜容量(m³)'] + const labels = ['集装箱号', '柜次号', '装柜仓库', '提单号', '船名', '航次', '箱型(货柜类型)', '船公司', '起运港', '目的港', '目的仓', '货柜容量(m³)'] let previous = -1 for (const label of labels) { const current = source.indexOf(`label="${label}"`, previous + 1) @@ -235,7 +235,7 @@ describe('ContainersView', () => { expect(source).toContain('新增模拟柜') expect(source).toContain('createContainerSimulation({ ...simulationForm })') expect(source).toContain("channelCode: '', warehouseId: '', containerType: '40HQ', capacityCbm: 68, maxWeightKg: 28000") - expect(source).toContain('服务端会生成稳定的内部柜次') + expect(source).toContain('服务端会生成稳定的柜次号') expect(source).not.toContain('openCreate') }) @@ -244,7 +244,7 @@ describe('ContainersView', () => { expect(source).toContain("!row.oceanBookingId && ['WAIT_LOADING','EDITING'].includes(row.planStatus)") expect(source).toContain("row.oceanBookingId && row.planStatus === 'WAIT_LOADING'") expect(source).toContain('已预排货但尚未进入 PDA、取证或单证阶段的模拟柜仍可首次关联') - expect(source).toContain('内部柜次与装柜明细保持不变') + expect(source).toContain('柜次号与装柜明细保持不变') expect(source).toContain('只有尚未排货的空柜可以解除') expect(source).toContain('linkContainerOceanBooking(target.id, { oceanBookingId: candidate.oceanBookingId, requirementId: candidate.requirementId, expectedVersion: target.version })') expect(source).toContain('unlinkContainerOceanBooking(target.id, target.version, unlinkReason.value.trim())') diff --git a/src/modules/container-loading/pages/ContainersView.vue b/src/modules/container-loading/pages/ContainersView.vue index e0bcc0d3..11f7cf4d 100644 --- a/src/modules/container-loading/pages/ContainersView.vue +++ b/src/modules/container-loading/pages/ContainersView.vue @@ -164,14 +164,14 @@ const destinationWarehouseValue = computed(() => form.destinationWarehouseCode | const queryFields = computed(() => [ { key: 'customerId', type: 'select', label: '客户', width: 220, placeholder: '全部客户', options: customers.value.filter((customer) => customer.status === 'ACTIVE').map((customer) => ({ label: customer.tenantName, value: String(customer.id) })), props: { filterable: true, loading: customersLoading.value } }, - { key: 'keyword', type: 'input', label: '关键词', width: 220, placeholder: '柜次号、柜号或提单号', props: { 'aria-label': '关键词', maxlength: 128 } }, + { key: 'keyword', type: 'input', label: '关键词', width: 220, placeholder: '柜次号、集装箱号或提单号', props: { 'aria-label': '关键词', maxlength: 128 } }, { key: 'containerType', type: 'select', label: '柜型', width: 140, options: ['20GP', '40GP', '40HQ', '45HQ'].map((value) => ({ label: value, value })), props: { 'aria-label': '柜型' } }, { key: 'status', type: 'select', label: '排柜状态', width: 150, options: [{ label: '待装柜', value: 'WAIT_LOADING' }, { label: '预装柜中', value: 'EDITING' }, { label: '拣货中', value: 'LOADING' }, { label: '装柜完成', value: 'COMPLETED' }], props: { 'aria-label': '排柜状态' } }, { key: 'sailingRange', type: 'daterange', label: '预计开船', width: 260, props: { 'aria-label': '预计开船日期范围' } }, ]) const columns: DataTableColumn[] = [ { key: 'actions', label: '操作', width: 310, fixed: 'left', slotName: 'actions' }, - { key: 'cabinetNo', label: '柜次', minWidth: 180, slotName: 'cabinet' }, + { key: 'cabinetNo', label: '柜次号', minWidth: 180, slotName: 'cabinet' }, { key: 'oceanBooking', label: '真实订舱', minWidth: 170, slotName: 'oceanBooking' }, { key: 'containerNo', label: '集装箱号', minWidth: 150, slotName: 'containerNo' }, { key: 'planStatus', label: '状态', width: 110, slotName: 'status' }, @@ -387,7 +387,7 @@ async function saveLink() { try { await linkContainerOceanBooking(target.id, { oceanBookingId: candidate.oceanBookingId, requirementId: candidate.requirementId, expectedVersion: target.version }) linkOpen.value = false - ElMessage.success('已关联真实订舱柜,内部柜次与装柜明细保持不变') + ElMessage.success('已关联真实订舱柜,柜次号与装柜明细保持不变') await loadRows() } catch (error) { linkError.value = getApiErrorDetails(error, '关联真实订舱柜失败') } finally { linkSaving.value = false } @@ -406,7 +406,7 @@ async function saveUnlink() { try { await unlinkContainerOceanBooking(target.id, target.version, unlinkReason.value.trim()) unlinkOpen.value = false - ElMessage.success('真实订舱关联已解除,内部柜次保持不变') + ElMessage.success('真实订舱关联已解除,柜次号保持不变') await loadRows() } catch (error) { linkError.value = getApiErrorDetails(error, '解除真实订舱关联失败') } finally { linkSaving.value = false } @@ -597,8 +597,8 @@ onMounted(() => { void Promise.all([loadCustomers(), loadWarehouses(), loadBusin - - + + @@ -615,7 +615,7 @@ onMounted(() => { void Promise.all([loadCustomers(), loadWarehouses(), loadBusin - + 选择文件 @@ -625,7 +625,7 @@ onMounted(() => { void Promise.all([loadCustomers(), loadWarehouses(), loadBusin - + @@ -637,14 +637,14 @@ onMounted(() => { void Promise.all([loadCustomers(), loadWarehouses(), loadBusin - - + +

已预排货但尚未进入 PDA、取证或单证阶段的模拟柜仍可首次关联;最终状态门禁以后端为准。

- + @@ -656,7 +656,7 @@ onMounted(() => { void Promise.all([loadCustomers(), loadWarehouses(), loadBusin 重新加载业务选项 - + diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts index 96a00296..ae7d0241 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts @@ -115,12 +115,14 @@ describe('GlobalSmartLoadingView', () => { const option = wrapper.find('.container-option') expect(option.text()).toContain('模拟柜次') - expect(option.text()).toContain('模拟柜次:真实柜号待录入') - expect(option.text()).toContain('真实柜号:待录入') + expect(option.text()).toContain('柜次号:待生成') + expect(option.text()).toContain('集装箱号:待录入') + expect(option.text()).toContain('开始装柜前录入集装箱号') expect(option.text()).toContain('渠道:美森正班海卡包税') expect(option.text()).toContain('仓库:义乌国内仓') expect(option.text()).toContain('柜型:40HQ') expect(option.text()).not.toContain('2088475410134773762') + expect(wrapper.text()).not.toContain('真实柜号') }) it('groups containers by channel and reloads candidates when the channel changes', async () => { @@ -368,5 +370,7 @@ describe('GlobalSmartLoadingView', () => { expect(source).toContain('class="capacity-grid"') expect(source).toContain('智能推荐') expect(source).toContain('推送预装') + expect(source).toContain('推送预装仅生成预装清单,不会直接开放 PDA') + expect(source).not.toContain("canPush && plan?.status === 'EDITING' && selectedContainer?.containerNo") }) }) diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.vue b/src/modules/container-loading/pages/GlobalSmartLoadingView.vue index d1cd20dc..c7b309e1 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.vue +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.vue @@ -138,21 +138,18 @@ function adjustmentMax(source: object) { const remaining = candidates.value.find((candidate) => candidate.inboundDetailId === item.inboundDetailId)?.availablePackageCount || 0 return Math.max(1, item.packageCount + remaining) } -function displayContainerNo(container: ContainerBookingResource | null | undefined) { - if (!container) return '柜号待生成' - const actual = String(container.containerNo || '').trim() - const internalReference = /^(?:MAN|SPLIT)-/i.test(actual) - if (actual && !internalReference) return actual +function displayCabinetNo(container: ContainerBookingResource | null | undefined) { + if (!container) return '待生成' const cabinetReference = String(container.cabinetNo || '').trim() - return /^\d{15,}$/.test(cabinetReference) ? '模拟柜次' : cabinetReference || '柜号待生成' + return /^\d{15,}$/.test(cabinetReference) ? '待生成' : cabinetReference || '待生成' } function hasRealContainerNo(container: ContainerBookingResource | null | undefined) { if (!container) return false const value = String(container.containerNo || '').trim() return Boolean(value) && !/^(?:MAN|SPLIT)-/i.test(value) } -function hasOpaqueCabinetReference(container: ContainerBookingResource | null | undefined) { - return Boolean(container && /^\d{15,}$/.test(String(container.cabinetNo || '').trim())) +function displayActualContainerNo(container: ContainerBookingResource | null | undefined) { + return hasRealContainerNo(container) ? String(container?.containerNo || '').trim() : '待录入' } function syncDrafts(next: SmartLoadingPlan, preserved: Record = {}) { for (const key of Object.keys(quantityDrafts)) delete quantityDrafts[key] @@ -346,13 +343,13 @@ async function removeAll() { } async function pushPreload() { if (!plan.value) return - const confirmed = await ElMessageBox.confirm('推送后当前方案进入装柜流程,不能继续调整。确认推送预装清单?', '推送预装清单', { + const confirmed = await ElMessageBox.confirm('推送后生成预装清单,不会直接开放 PDA。后续还需补集装箱号、关联并放舱真实订舱、确认预装。确认继续?', '推送预装清单', { type: 'warning', confirmButtonText: '确认推送', cancelButtonText: '取消', }).catch(() => false) if (!confirmed || !plan.value) return await mutate(async () => { acceptPlan(await pushSmartLoadingPreload(plan.value!.id, plan.value!.version)) - ElMessage.success('已推送预装清单') + ElMessage.success('预装清单已生成,请继续完成集装箱号、订舱放舱和预装确认') }, '推送预装清单失败') } @@ -456,24 +453,23 @@ onMounted(() => { void load(); void loadCustomers() }) @click="selectContainer(item.id)" > - {{ hasRealContainerNo(item) ? displayContainerNo(item) : '模拟柜次' }} + {{ hasRealContainerNo(item) ? displayActualContainerNo(item) : '模拟柜次' }} - {{ hasRealContainerNo(item) ? '已录入柜号' : '空柜' }} + {{ hasRealContainerNo(item) ? '已录入集装箱号' : '空柜' }} - - {{ hasRealContainerNo(item) ? `柜号:${displayContainerNo(item)}` : (hasOpaqueCabinetReference(item) ? '模拟柜次:真实柜号待录入' : `柜次参考:${displayContainerNo(item)}`) }} - + 柜次号:{{ displayCabinetNo(item) }} + 集装箱号:{{ displayActualContainerNo(item) }} 渠道:{{ item.channelName || item.channelCode || '渠道待补充' }} 仓库:{{ item.warehouseName || item.warehouseId || '仓库待补充' }} · 柜型:{{ item.containerType || '柜型待补充' }} - 真实柜号:待录入 + 可先排货,开始装柜前录入集装箱号
-
{{ displayContainerNo(selectedContainer) }} 装柜方案{{ plan ? '当前柜号的装柜明细' : '尚未创建方案' }}
+
{{ hasRealContainerNo(selectedContainer) ? displayActualContainerNo(selectedContainer) : displayCabinetNo(selectedContainer) }} 装柜方案{{ selectedContainer ? `柜次号:${displayCabinetNo(selectedContainer)} · 集装箱号:${displayActualContainerNo(selectedContainer)}` : '尚未选择柜次' }}
智能推荐 @@ -496,6 +492,14 @@ onMounted(() => { void load(); void loadCustomers() }) 创建装柜方案 @@ -121,6 +175,7 @@ onMounted(() => void loadDetail()) .back-button { width: fit-content; } .transport-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 16px; } .transport-form :deep(.el-form-item:last-child) { grid-column: 1 / -1; } +.confirm-content { display: grid; gap: 16px; } :global(.loading-transport-dialog.el-dialog) { max-height: 90vh; overflow: hidden; } @media (max-width: 720px) { .transport-form { grid-template-columns: 1fr; } .transport-form :deep(.el-form-item:last-child) { grid-column: auto; } } diff --git a/src/modules/loading-lists/routes.test.ts b/src/modules/loading-lists/routes.test.ts index 6b465e96..9816e5e1 100644 --- a/src/modules/loading-lists/routes.test.ts +++ b/src/modules/loading-lists/routes.test.ts @@ -7,6 +7,7 @@ describe('loading list routes', () => { ['loading-plans', 'platform:firstleg:preload:list'], ['loading-plans/:planId', 'platform:firstleg:preload:list'], ['loading-lists', 'platform:firstleg:loading-list:list'], + ['loading-lists/:planId', 'platform:firstleg:loading-list:list'], ]) }) }) diff --git a/src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts b/src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts index 2b1f8f7e..1307cbe0 100644 --- a/src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts +++ b/src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts @@ -52,7 +52,7 @@ describe('OceanBookingDetailView interaction contract', () => { expect(source).toContain("form.data.shippingCompanyName || '船公司名称缺失'") expect(source).toContain("form.data.bookingAgentName || '订舱代理名称缺失'") expect(source).toContain("from '@/modules/master-data'") - expect(source).toContain("from '@/modules/suppliers/api/platformSuppliers'") + expect(source).toContain("from '@/modules/suppliers'") }) it('renders the translated status once in the detail heading', () => { diff --git a/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue b/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue index b1e29153..42ce5289 100644 --- a/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue +++ b/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue @@ -14,8 +14,7 @@ import DetailPageLayout from '@/shared/components/DetailPageLayout.vue' import PageToolbar from '@/shared/components/PageToolbar.vue' import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam' import { listPlatformPartners, type PlatformPartnerResource } from '@/modules/master-data' -import { listSuppliers } from '@/modules/suppliers/api/platformSuppliers' -import type { Supplier } from '@/modules/suppliers/types' +import { listSuppliers, type Supplier } from '@/modules/suppliers' import { listPlatformWarehouses, type PlatformWarehouseResource } from '@/modules/warehouses' import { createOceanBooking, diff --git a/src/modules/trucking/components/TruckAppointmentForm.test.ts b/src/modules/trucking/components/TruckAppointmentForm.test.ts index 1d3398bb..b2f6b7e8 100644 --- a/src/modules/trucking/components/TruckAppointmentForm.test.ts +++ b/src/modules/trucking/components/TruckAppointmentForm.test.ts @@ -45,4 +45,9 @@ describe('TruckAppointmentForm interaction contract', () => { expect(source).toContain('placeholder="请输入目的门店地址"') expect(source).toContain('placeholder="请输入拖车预约备注(可选)"') }) + + it('labels the generated cabinet reference and physical container number separately', () => { + expect(source).toContain('柜次号:${item.cabinetNo') + expect(source).toContain('集装箱号:${item.containerNo') + }) }) diff --git a/src/modules/trucking/components/TruckAppointmentForm.vue b/src/modules/trucking/components/TruckAppointmentForm.vue index d7f175f3..2c03c0f9 100644 --- a/src/modules/trucking/components/TruckAppointmentForm.vue +++ b/src/modules/trucking/components/TruckAppointmentForm.vue @@ -87,7 +87,7 @@ watch(() => props.readonly, (readonly) => { if (!readonly && !customers.value.le 重新加载选项 关联资源 - + diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.vue b/src/modules/unified-orders/pages/UnifiedOrdersView.vue index e9be25b0..2e1df34e 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.vue +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.vue @@ -40,7 +40,7 @@ import { firstLegTransportModeLabel, } from '@/shared/utils/firstLegBusinessLabels' import { UnifiedOrderWorkflowPanel } from '@/modules/unified-orders/workflows' -import SmartLoadingDialog from '@/modules/container-loading/components/SmartLoadingDialog.vue' +import { SmartLoadingDialog } from '@/modules/container-loading' import { unifiedOrderHistoryActionLabel, unifiedOrderLoadingStatusLabel, diff --git a/src/modules/unified-orders/workflows/README.md b/src/modules/unified-orders/workflows/README.md index dc058054..c0bf8023 100644 --- a/src/modules/unified-orders/workflows/README.md +++ b/src/modules/unified-orders/workflows/README.md @@ -5,7 +5,7 @@ 真实接口均使用 `/api/platform/v1/inbounds/{inboundId}`: - `POST /split/smart`:智能分柜/保存预装清单 -- `GET /preload`、`POST /preload/confirming`、`POST /preload/confirm`:预装清单查询与确认 +- `GET /preload`:单票预装状态与明细只读查询;整份计划的确认统一在预装清单真实 `loadingPlan` 详情完成 - `GET/POST /loading/picking`、`/loading/picking/start`、`/loading/picking/complete`:拣货 - `POST /platform/v1/files/loading-evidence/{tenantId}/{inboundId}`:上传真实装柜证据文件 - `GET/POST /loading/evidence`:查询并登记五类装柜证据及锁码 diff --git a/src/modules/unified-orders/workflows/api.test.ts b/src/modules/unified-orders/workflows/api.test.ts index 88cdb948..67791ad8 100644 --- a/src/modules/unified-orders/workflows/api.test.ts +++ b/src/modules/unified-orders/workflows/api.test.ts @@ -4,7 +4,6 @@ import { adjustSmartLoadingItem, assignSmartLoadingItem, completeLoading, - confirmingPreload, deleteSmartLoadingItem, downloadEvidenceFile, getContainerBooking, @@ -62,15 +61,6 @@ describe('unified order workflow API contract', () => { await expect(getPreload('2', '8')).rejects.toThrow('无权限') }) - it('supports the intermediate confirming preload action', async () => { - vi.mocked(http.post).mockResolvedValue({ data: { mainStatus: 'WAIT_SPLIT', preloadPlan: null } }) - await confirmingPreload('2', '8', { expectedVersion: 1 }) - expect(http.post).toHaveBeenCalledWith( - '/platform/v1/inbounds/8/preload/confirming', { expectedVersion: 1 }, - expect.objectContaining({ params: { tenantId: '2' } }), - ) - }) - it('uploads a real evidence file and voids evidence through audited endpoints', async () => { vi.mocked(http.post) .mockResolvedValueOnce({ data: { fileId: '88', fileName: 'empty.jpg' } }) diff --git a/src/modules/unified-orders/workflows/api.ts b/src/modules/unified-orders/workflows/api.ts index 82cf6219..5241ca1f 100644 --- a/src/modules/unified-orders/workflows/api.ts +++ b/src/modules/unified-orders/workflows/api.ts @@ -186,10 +186,6 @@ export interface SmartSplitRequest { remark?: string } -export interface ConfirmPreloadRequest { - expectedVersion: number -} - export interface UpsertEvidenceRequest { expectedVersion: number evidenceType: EvidenceType @@ -446,30 +442,6 @@ export async function smartSplit( return normalizeSplitAction(unwrapApiResponse(response)) } -export async function confirmingPreload( - tenantId: string | number, - inboundId: string | number, - request: ConfirmPreloadRequest, -) { - const response = await http.post>, ApiEnvelope>>( - `${inboundBasePath}/${inboundId}/preload/confirming`, request, - writeOptions('preload-confirming', inboundId, tenantId), - ) - return normalizeSplitAction(unwrapApiResponse(response)) -} - -export async function confirmPreload( - tenantId: string | number, - inboundId: string | number, - request: ConfirmPreloadRequest, -) { - const response = await http.post>, ApiEnvelope>>( - `${inboundBasePath}/${inboundId}/preload/confirm`, request, - writeOptions('preload-confirm', inboundId, tenantId), - ) - return normalizeSplitAction(unwrapApiResponse(response)) -} - export async function getPicking(tenantId: string | number, inboundId: string | number) { const response = await http.get, ApiEnvelope>( `${inboundBasePath}/${inboundId}/loading/picking`, { params: query(tenantId) }, diff --git a/src/modules/unified-orders/workflows/components/UnifiedOrderWorkflowPanel.test.ts b/src/modules/unified-orders/workflows/components/UnifiedOrderWorkflowPanel.test.ts index 8597df71..0ecbd78a 100644 --- a/src/modules/unified-orders/workflows/components/UnifiedOrderWorkflowPanel.test.ts +++ b/src/modules/unified-orders/workflows/components/UnifiedOrderWorkflowPanel.test.ts @@ -83,6 +83,20 @@ describe('UnifiedOrderWorkflowPanel preload editor', () => { expect(wrapper.find('button[aria-label="编辑第 1 条预装明细"]').exists()).toBe(true) }) + it('keeps single-order preload confirmation read-only and links to the plan list', async () => { + const wrapper = mountPanel(['firstleg:inbound:preload-view', 'platform:firstleg:preload:list']) + await flushPromises() + + expect(wrapper.text()).toContain('计划级预装确认') + expect(wrapper.text()).toContain('从实际预装计划详情确认整份计划') + expect(wrapper.text()).toContain('前往预装清单') + expect(wrapper.text()).not.toContain('提交确认') + expect(wrapper.text()).not.toContain('确认预装清单') + + const source = readFileSync(resolve(process.cwd(), 'src/modules/unified-orders/workflows/components/UnifiedOrderWorkflowPanel.vue'), 'utf8') + expect(source).toContain("router.push({ name: 'loading-plans' })") + }) + it('does not query or expose editing controls without permission', async () => { const wrapper = mountPanel([]) await flushPromises() diff --git a/src/modules/unified-orders/workflows/components/UnifiedOrderWorkflowPanel.vue b/src/modules/unified-orders/workflows/components/UnifiedOrderWorkflowPanel.vue index bfa9cae5..be8b2c01 100644 --- a/src/modules/unified-orders/workflows/components/UnifiedOrderWorkflowPanel.vue +++ b/src/modules/unified-orders/workflows/components/UnifiedOrderWorkflowPanel.vue @@ -2,6 +2,7 @@ import { computed, onMounted, ref, watch } from 'vue' import { ElMessageBox } from 'element-plus' import { RefreshCw, Save, Check, Play, Camera, Eye, Plus, Pencil, Trash2 } from '@lucide/vue' +import { useRouter } from 'vue-router' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { ApiResponseError, getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' @@ -15,8 +16,6 @@ import { sanitizePreloadItem, validatePreloadItem, validatePreloadItems } from ' import { completeLoading, completePicking, - confirmingPreload, - confirmPreload, downloadEvidenceFile, getEvidence, getOutboundDispatch, @@ -52,6 +51,7 @@ const props = withDefaults(defineProps<{ const emit = defineEmits<{ updated: [] }>() const access = useAccessPreviewStore() +const router = useRouter() const preload = ref(null) const preloadItems = ref([]) const picking = ref(null) @@ -87,8 +87,8 @@ const preloadVersion = computed(() => preload.value?.version ?? 0) const pickingExpectedVersion = computed(() => picking.value?.version ?? props.pickingVersion ?? null) const inboundExpectedVersion = computed(() => props.inboundVersion) const canSplit = computed(() => !props.readOnly && access.can('firstleg:inbound:split')) -const canPreload = computed(() => !props.readOnly && access.can('firstleg:inbound:preload')) const canPreloadView = computed(() => access.can('firstleg:inbound:preload-view')) +const canViewLoadingPlans = computed(() => access.can('platform:firstleg:preload:list')) const canPicking = computed(() => !props.readOnly && access.can('platform:firstleg:inbound:picking:edit')) const canPickingView = computed(() => access.can('platform:firstleg:inbound:picking:view')) // 操作员、理货员由 IAM 授予 loading:edit;拣货员仅能在 PDA 现场采集,不授予该权限。 @@ -292,18 +292,8 @@ async function runSmartSplit() { } } -async function startPreloadConfirmation() { - const result = await run('preload-confirming', () => confirmingPreload(props.tenantId, props.inboundId, { - expectedVersion: preloadVersion.value, - }), '预装清单已进入确认中') - if (result?.preloadPlan) preload.value = result.preloadPlan -} - -async function finishPreloadConfirmation() { - const result = await run('preload-confirm', () => confirmPreload(props.tenantId, props.inboundId, { - expectedVersion: preloadVersion.value, - }), '预装清单已确认,订单进入装柜中') - if (result?.preloadPlan) preload.value = result.preloadPlan +function openPreloadPlans() { + void router.push({ name: 'loading-plans' }) } function pickingRequest(): { expectedVersion: number; expectedCount: number; pickedCount: number; remark?: string } | null { @@ -465,8 +455,11 @@ onMounted(() => { void refresh() })
保存分柜清单 - 提交确认 - 确认预装清单 +
+
+
计划级预装确认当前单票状态:{{ preloadStatusLabel(preload?.status) }}。请前往预装清单,从实际预装计划详情确认整份计划。
+ 前往预装清单 + 当前账号无预装清单列表权限。
@@ -573,6 +566,11 @@ onMounted(() => { void refresh() }) .preload-dialog-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; } .workflow-actions { margin-top: 14px; } .workflow-remark { margin-top: 10px; } +.preload-confirmation-guide { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 12px; padding: 12px 14px; border: 1px solid var(--cc-border); border-radius: 6px; background: #f8fafc; } +.preload-confirmation-guide > div { display: grid; min-width: 0; gap: 4px; } +.preload-confirmation-guide strong { color: var(--cc-text, #1f2937); font-size: 13px; } +.preload-confirmation-guide span { color: var(--cc-text-secondary, #64748b); font-size: 12px; line-height: 1.5; } +.preload-confirmation-guide > .el-button { flex: 0 0 auto; } .evidence-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; } .evidence-item { display: grid; min-width: 0; gap: 8px; align-content: start; padding: 12px; border: 1px solid var(--cc-border); border-radius: 6px; } .evidence-item strong { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; } @@ -592,6 +590,7 @@ onMounted(() => { void refresh() }) @media (max-width: 640px) { .preload-dialog-grid, .evidence-grid { grid-template-columns: 1fr; } .preload-dialog-grid { gap: 0; } + .preload-confirmation-guide { align-items: stretch; flex-direction: column; } .evidence-item, .gate-grid { grid-template-columns: 1fr; } .workflow-actions > .el-button { flex: 1 1 150px; } }