From f24b592705de967ec4db001ce0646746f425abf2 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 8 Oct 2026 13:59:20 +0800 Subject: [PATCH 1/9] fix(admin): restore loading list detail page navigation --- AGENTS.md | 2 +- .../LoadingListsView.behavior.test.ts | 43 ++++++------ .../components/LoadingListsView.test.ts | 10 +-- .../components/LoadingListsView.vue | 56 ++-------------- .../ConfirmedLoadingListDetailView.test.ts | 65 +++++++++++++++++++ .../pages/ConfirmedLoadingListDetailView.vue | 16 ++++- src/modules/loading-lists/routes.test.ts | 1 + src/modules/loading-lists/routes.ts | 6 ++ 8 files changed, 123 insertions(+), 76 deletions(-) create mode 100644 src/modules/loading-lists/pages/ConfirmedLoadingListDetailView.test.ts diff --git a/AGENTS.md b/AGENTS.md index 26e35755..45816340 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,7 +60,7 @@ | 入库单扣货、解除、凭证与历史 | `modules/inbounds/pages/InboundOrdersView.vue`、详情工作区、`modules/inbounds/core/api.ts` | 列表展示服务端 `holdFlag/currentHoldReason` 并支持筛选;详情只按 `canHold/canRelease/actionBlockReason` 开放操作。扣货与解除提交当前 `version`、原因和独立幂等键,可选凭证先按明确客户与入库单上传;历史文件只调用专用下载入口,不把上传成功但未提交命令的文件冒充扣货证据 | | 客户专属价格簿、柜级阶梯运费与受控调价 | 列表/上传 `modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue`,版本详情/仓库映射/确认 `modules/fba-pricing/carrier-rates/pages/CarrierRateDetailView.vue`,路由 `modules/fba-pricing/routes.ts`;`modules/inbounds/fee/`、`modules/unified-orders/components/InboundFeeDetailManager.vue` | 列表与上传后按 `versionId` 进入 `fba-carrier-rate-detail` 独立详情页,价格行只读;回归入口 `modules/fba-pricing/carrier-rates/pages/CarrierRatesView.test.ts`。价格簿页面显式选择公共报价或具体客户;列表、上传、确认只作用于该 `customerId`,不得混合版本。计价优先消费客户专属 ACTIVE 版本,无专属版本才回退公共 ACTIVE;柜级阶梯金额和各客户分摊只展示服务端结果。调整计费量只能在服务端返回的上下 10% 范围内并提交当前费用版本、原因与二次确认;私人仓尾程派送费使用稳定 `PRIVATE_LAST_MILE_DELIVERY` 代码且只对 `PRIVATE` 目的地开放;一件代发须在预装确认前完成试算,计价后保持服务端 `WAIT_PALLETIZE` 状态 | | 财务、客户资金账户与柜级公共费用分摊 | `modules/finance/`,继续读该目录 `AGENTS.md`;客户账户 `pages/FinanceCustomerAccountsView.vue`,柜费 `modules/loading-lists/components/ContainerFeesPanel.vue`、`api/containerFees.ts` | `finance/`、`firstleg/`、`file/`、`docs/财务管理/`;实际、冻结、可用余额、授信与流水只消费 Finance 权威结果,充值审核和手工充扣携带版本与幂等键。跨客户公共柜费只按服务端柜内客户/订单事实分摊,金额必须守恒,创建与审核、驳回、调整、作废均整组原子处理 | -| 货柜双工作台、排柜、预装/装柜清单、真实柜号后补与整柜报关 | 国内 `modules/container-loading/pages/DomesticContainersView.vue`、海外 `OverseasContainersView.vue`,共享详情实现 `ContainerDetailView.vue`;预装详情 `modules/loading-lists/pages/PreloadPlanDetailView.vue`,资料/PDA 证据 `components/ContainerOverviewPanel.vue`,报关 `components/ContainerCustomsPanel.vue` | 国内“送柜报关”只消费 `/containers/domestic` 与服务端 `allowedActions`,承载模拟柜、订舱关联、智能装柜;模拟柜创建选择与智能装柜一致的 9 类计划渠道,未关联订舱前实际渠道为空,订舱关联候选按计划渠道组匹配并同时展示订舱号与提单号、装柜清单页签、出口报关和国内出仓;创建柜次时自动生成唯一预装计划,预装详情先调用 `/loading-plans/{planId}/lifecycle`,完成后深链进入国内货柜详情的装柜清单页签,独立装柜清单导航 `/loading-lists` 复用已确认清单 API,列表下方展开只读明细并提供打印;货柜列表经 `ContainerWorkbenchView.vue` 在国内装柜与海外清关提柜之间按权限切换。订舱关联允许真实集装箱号为空,但确认预装和创建 PDA 任务前必须补齐;历史已建但缺号的任务仍可在预装详情首次补录,保存后恢复 PDA 可进入状态;全买单柜继续使用 `complete-all-buy`。海外“清关提柜”只消费 `/containers/overseas`,承载船节点、目的港清关、DO/PU、码头车队和还空;原预约/实际提柜展示位改为整柜预计/实际到仓,预计到仓在列表内人工快捷维护并经海外接口同步整柜尾程订单,实际到仓只读海外 PDA 首次开始收货时间。还空截止事实由服务端基于卸柜完成时间加 72 小时计算,前端不重算。两端统一使用 `containerBookingId`,列表范围由服务端 `scope=ACTIVE/HISTORY` 分页,禁止浏览器筛选或本地推导写资格。后端为 `LoadingListService`、`firstleg/customs/container/`、`firstleg/terminaldrayage/` 和 `lastleg-service/`,文件归 `file/` | +| 货柜双工作台、排柜、预装/装柜清单、真实柜号后补与整柜报关 | 国内 `modules/container-loading/pages/DomesticContainersView.vue`、海外 `OverseasContainersView.vue`,共享详情实现 `ContainerDetailView.vue`;预装详情 `modules/loading-lists/pages/PreloadPlanDetailView.vue`,资料/PDA 证据 `components/ContainerOverviewPanel.vue`,报关 `components/ContainerCustomsPanel.vue` | 国内“送柜报关”只消费 `/containers/domestic` 与服务端 `allowedActions`,承载模拟柜、订舱关联、智能装柜;模拟柜创建选择与智能装柜一致的 9 类计划渠道,未关联订舱前实际渠道为空,订舱关联候选按计划渠道组匹配并同时展示订舱号与提单号、装柜清单页签、出口报关和国内出仓;创建柜次时自动生成唯一预装计划,预装详情先调用 `/loading-plans/{planId}/lifecycle`,完成后深链进入国内货柜详情的装柜清单页签,独立装柜清单导航 `/loading-lists` 复用已确认清单 API,查看进入独立详情路由 `/loading-lists/:planId`,保留返回列表和打印;货柜列表经 `ContainerWorkbenchView.vue` 在国内装柜与海外清关提柜之间按权限切换。订舱关联允许真实集装箱号为空,但确认预装和创建 PDA 任务前必须补齐;历史已建但缺号的任务仍可在预装详情首次补录,保存后恢复 PDA 可进入状态;全买单柜继续使用 `complete-all-buy`。海外“清关提柜”只消费 `/containers/overseas`,承载船节点、目的港清关、DO/PU、码头车队和还空;原预约/实际提柜展示位改为整柜预计/实际到仓,预计到仓在列表内人工快捷维护并经海外接口同步整柜尾程订单,实际到仓只读海外 PDA 首次开始收货时间。还空截止事实由服务端基于卸柜完成时间加 72 小时计算,前端不重算。两端统一使用 `containerBookingId`,列表范围由服务端 `scope=ACTIVE/HISTORY` 分页,禁止浏览器筛选或本地推导写资格。后端为 `LoadingListService`、`firstleg/customs/container/`、`firstleg/terminaldrayage/` 和 `lastleg-service/`,文件归 `file/` | | 合柜报关 / 同柜不可拆组 | `modules/unified-orders/api/coLoadGroups.ts`、`pages/UnifiedOrdersView.vue`;装柜消费 `modules/container-loading/pages/SmartLoadingWorkbenchView.vue` | 前端只提交订单 ID;Backend `firstleg/loading/coload/` 展开并冻结每票全部可用入仓片段。成员数是入仓片段数,不等于订单票数;只要求同国内仓、同渠道,允许跨客户及不同目的仓港;分配、移除、取消均整组处理 | | 全局智能装柜工作台、入仓码候选与箱数分配 | `modules/container-loading/components/SmartLoadingDialog.vue` 内嵌 `pages/GlobalSmartLoadingView.vue`,柜次页 `SmartLoadingWorkbenchView.vue`,以及 `smartLoadingChannelGroups.ts`、`smartLoadingItems.ts`、`api/platformContainerLoading.ts`;全局页当前不注册独立路由 | Backend `firstleg/` 的 `/inbound-candidates`、`/containers`、`/inbound-assignments`、`/plans/{planId}/pending-review-assignments`、`/group-assignments`、`/unpicked-items`。`orderStatuses` 最多四项且未传表示全部未完成状态;候选状态多选覆盖待审核、待测量、待分柜、装柜中,状态只用于筛选展示,按钮资格以服务端 `canAssign` 为准;待审核/待测量可提前占位但不得提前确认预装或启动 PDA。计划箱数编辑上限、已用和未用展示只消费服务端 `totalPackageCount/remainingPackageCount`,不得从候选页或本地规格重新拼算。人工加入允许跨渠道,智能推荐仍按柜次渠道。柜次容量和限重取服务端字段;同柜组作为单卡整组分页、分配和移除,任一成员已拣时整组保留 | | 国内货柜履约订舱资料、打印、关联候选与出仓前船期 | `modules/ocean-bookings/` 的 `components/OceanBookingDocumentsPanel.vue`、`OceanBookingPrintDialog.vue`,以及 `modules/container-loading/`、`modules/port-schedules/` | 订舱草稿按当前版本维护资料,放舱后资料与表单只读;打印对草稿与已放舱订舱均开放,按当前已保存资料生成,未填写字段显示“—”。`platformOceanBookings.ts` 的 `/link-candidates` 只消费服务端分页,`containerCount` 是柜型需求 `quantity` 总和;Backend `firstleg/oceanbooking/`、`file/`、`print/`、`firstleg/portschedule/` | diff --git a/src/modules/loading-lists/components/LoadingListsView.behavior.test.ts b/src/modules/loading-lists/components/LoadingListsView.behavior.test.ts index bbb4592d..f999c5a9 100644 --- a/src/modules/loading-lists/components/LoadingListsView.behavior.test.ts +++ b/src/modules/loading-lists/components/LoadingListsView.behavior.test.ts @@ -2,8 +2,8 @@ import { flushPromises, mount } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' import LoadingListsView from './LoadingListsView.vue' -const mocks = vi.hoisted(() => ({ get: vi.fn(), list: vi.fn(), can: vi.fn(), push: vi.fn() })) -vi.mock('vue-router', () => ({ useRoute: () => ({ query: {} }), useRouter: () => ({ push: mocks.push, replace: vi.fn() }) })) +const mocks = vi.hoisted(() => ({ get: vi.fn(), list: vi.fn(), can: vi.fn(), push: vi.fn(), replace: vi.fn(), query: {} as Record })) +vi.mock('vue-router', () => ({ useRoute: () => ({ query: mocks.query }), useRouter: () => ({ push: mocks.push, replace: mocks.replace }) })) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: mocks.can }) })) vi.mock('@/modules/container-loading', () => ({ ContainerSimulationCreate: { template: '
' } })) vi.mock('@/modules/iam', () => ({ listPlatformTenants: async () => ({ records: [] }) })) @@ -20,27 +20,32 @@ function render() { ElButton: { template: '' }, } } }) } -describe('confirmed manifest inline detail', () => { - beforeEach(() => { vi.clearAllMocks(); mocks.can.mockReturnValue(true); mocks.list.mockResolvedValue({ records: rows, total: 2 }) }) - it('ignores an older detail response when another manifest is selected', async () => { - let resolveFirst!: (value: unknown) => void - mocks.get.mockImplementationOnce(() => new Promise(resolve => { resolveFirst = resolve })) - .mockResolvedValueOnce({ summary: { status: 'COMPLETED', planId: '2', loadingListNo: 'LIST-2' } }) +describe('confirmed manifest navigation', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.query = {} + mocks.can.mockReturnValue(true) + mocks.list.mockResolvedValue({ records: rows, total: 2 }) + }) + it('opens each manifest on its dedicated detail route with warehouse context', async () => { const wrapper = render(); await flushPromises() await wrapper.findAll('button')[0]!.trigger('click') - await wrapper.findAll('button')[2]!.trigger('click'); await flushPromises() - resolveFirst({ summary: { status: 'COMPLETED', planId: '1', loadingListNo: 'LIST-1' } }); await flushPromises() - expect(wrapper.get('[data-testid="detail"]').text()).toBe('LIST-2') - expect(mocks.get).toHaveBeenNthCalledWith(2, '2', '22') - expect(mocks.push).not.toHaveBeenCalled() + expect(mocks.push).toHaveBeenLastCalledWith({ name: 'loading-list-detail', params: { planId: '1' }, query: { warehouseId: '11' } }) + await wrapper.findAll('button')[2]!.trigger('click') + expect(mocks.push).toHaveBeenLastCalledWith({ name: 'loading-list-detail', params: { planId: '2' }, query: { warehouseId: '22' } }) + expect(mocks.get).not.toHaveBeenCalled() + expect(wrapper.find('[data-testid="detail"]').exists()).toBe(false) }) - it('does not print when loading fails', async () => { - const print = vi.spyOn(window, 'print').mockImplementation(() => {}) - mocks.get.mockRejectedValueOnce(new Error('清单不可用')) + it('prints through the dedicated detail page', async () => { const wrapper = render(); await flushPromises() - await wrapper.findAll('button')[1]!.trigger('click'); await flushPromises() - expect(wrapper.get('[data-testid="detail"]').text()).toContain('清单不可用') - expect(print).not.toHaveBeenCalled(); print.mockRestore() + await wrapper.findAll('button')[1]!.trigger('click') + expect(mocks.push).toHaveBeenCalledWith({ name: 'loading-list-detail', params: { planId: '1' }, query: { warehouseId: '11', print: '1' } }) + expect(mocks.get).not.toHaveBeenCalled() + }) + it('replaces a list deep link with the dedicated detail route', async () => { + mocks.query = { planId: '2', warehouseId: '22' } + render(); await flushPromises() + expect(mocks.replace).toHaveBeenCalledWith({ name: 'loading-list-detail', params: { planId: '2' }, query: { warehouseId: '22' } }) }) it('does not fetch manifests without list permission', async () => { mocks.can.mockReturnValue(false) diff --git a/src/modules/loading-lists/components/LoadingListsView.test.ts b/src/modules/loading-lists/components/LoadingListsView.test.ts index d5b494e1..ad574499 100644 --- a/src/modules/loading-lists/components/LoadingListsView.test.ts +++ b/src/modules/loading-lists/components/LoadingListsView.test.ts @@ -59,15 +59,15 @@ describe('LoadingListsView', () => { it('keeps preload routes while completed lists move into domestic container detail', () => { expect(source).toContain("name: 'preload-plan-detail'") - expect(source).toContain('loadConfirmedDetail') - expect(source).toContain('confirmed-inline-detail') + expect(source).toContain("name: 'loading-list-detail'") + expect(source).not.toContain('confirmed-inline-detail') expect(preloadDetailSource).toContain('返回列表') expect(preloadDetailSource).toContain('router.back()') expect(confirmedDetailSource).toContain('返回列表') expect(confirmedDetailSource).toContain('router.back()') expect(confirmedDetailSource).toContain('@media (min-width: 1920px) { .confirmed-loading-detail-page { width: 100%; max-width: none; gap: 12px; } }') - expect(routesSource).not.toContain("path: 'loading-lists/:planId'") - expect(routesSource).not.toContain("name: 'loading-list-detail'") + expect(routesSource).toContain("path: 'loading-lists/:planId'") + expect(routesSource).toContain("name: 'loading-list-detail'") }) it('keeps the real transport edit contract in a 620px centered modal on the detail page', () => { @@ -111,7 +111,7 @@ describe('LoadingListsView', () => { expect(source).toContain('useRoute()') expect(source).toContain('route.query.planId') expect(source).toContain('route.query.warehouseId') - expect(source).toContain('await loadConfirmedDetail(planId, warehouseId)') + expect(source).toContain("await router.replace({ name: 'loading-list-detail'") expect(confirmedDetailSource).toContain("result.summary.status !== 'COMPLETED'") expect(confirmedDetailSource).toContain('MODE_STATUS_MISMATCH') expect(confirmedDetailSource).toContain('watch([planId, warehouseId], () => void loadDetail())') diff --git a/src/modules/loading-lists/components/LoadingListsView.vue b/src/modules/loading-lists/components/LoadingListsView.vue index 1c39c7fc..30da21af 100644 --- a/src/modules/loading-lists/components/LoadingListsView.vue +++ b/src/modules/loading-lists/components/LoadingListsView.vue @@ -1,7 +1,6 @@ @@ -58,6 +70,7 @@ watch([planId, warehouseId], () => void loadDetail())