diff --git a/AGENTS.md b/AGENTS.md index 876c3a21..d5f9bc0d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,27 +47,31 @@ | 订单列表、详情、商品图片与入库费用 | `modules/unified-orders/`、`modules/first-leg-orders/`;平台商品图片由 `modules/unified-orders/api/unifiedOrders.ts` 按客户与 fileId 下载 | `order/`、`firstleg/`、Finance;图片读取归 `file/` 的 `PlatformProductImageController` | | 入库、测量与计价主数据 | `modules/inbounds/`、`modules/fba-pricing/`、`modules/dimension-config/`、`modules/fee-items/` | `firstleg/`、`order/`、`file/`、`integration-service/` | | 财务 | `modules/finance/`,继续读该目录 `AGENTS.md` | `finance/`、`firstleg/`、`file/`、`docs/财务管理/` | -| 货柜、排柜、预装/装柜清单与整柜报关 | `modules/container-loading/`、`modules/loading-lists/`;预装详情 `pages/PreloadPlanDetailView.vue`,货柜详情 `pages/ContainerDetailView.vue`,资料/PDA 证据 `components/ContainerOverviewPanel.vue`,报关 `components/ContainerCustomsPanel.vue`、`api/platformContainerCustoms.ts` | 预装详情先调用 `/loading-plans/{planId}/lifecycle`,`COMPLETED` 时替换为正式装柜清单详情并关闭旧工作台标签,不按 A0404 或错误文案猜阶段。整柜报关按 `loadingPlanId` 展示,逐资料完成,全买单柜使用 `complete-all-buy`;Backend `LoadingListService`、`loading-lists-v1.yaml`、`firstleg/customs/container/`,文件归 `file/` | +| 货柜、排柜、预装/装柜清单、真实柜号后补与整柜报关 | `modules/container-loading/`、`modules/loading-lists/`;预装详情 `pages/PreloadPlanDetailView.vue`,货柜详情 `pages/ContainerDetailView.vue`,资料/PDA 证据 `components/ContainerOverviewPanel.vue`,报关 `components/ContainerCustomsPanel.vue`、`api/platformContainerCustoms.ts` | 创建柜次时自动生成唯一预装计划,加入入仓码即同步进入该计划,页面不提供创建计划或推送预装操作。预装详情先调用 `/loading-plans/{planId}/lifecycle`,`COMPLETED` 时替换为正式装柜清单详情并关闭旧工作台标签,不按 A0404 或错误文案猜阶段。订舱关联和预装确认允许真实集装箱号为空;首次补录可在 `EDITING/LOADING` 进行,PDA 最终完成前必须补齐;已有柜号纠正只允许 `EDITING` 且任务未领取、无物理作业事实。整柜报关按 `loadingPlanId` 展示、逐资料完成,全买单柜使用 `complete-all-buy`;后端为 `LoadingListService`、`loading-lists-v1.yaml`、`firstleg/customs/container/`,文件归 `file/` | | 合柜报关 / 同柜不可拆组 | `modules/unified-orders/api/coLoadGroups.ts`、`pages/UnifiedOrdersView.vue`;装柜消费 `modules/container-loading/pages/SmartLoadingWorkbenchView.vue` | 前端只提交订单 ID;Backend `firstleg/loading/coload/` 展开并冻结每票全部可用入仓片段。成员数是入仓片段数,不等于订单票数;只要求同国内仓、同渠道,允许跨客户及不同目的仓港;分配、移除、取消均整组处理 | | 智能装柜按入仓码候选与箱数分配 | `modules/container-loading/pages/SmartLoadingWorkbenchView.vue`、`api/platformContainerLoading.ts` | Backend `firstleg/` 的 `PlatformSmartLoadingController`、`SmartLoadingService`;`GlobalSmartLoadingView.vue` 当前未注册路由,不作为正式入口 | -| 三期订舱、关联候选与出仓前船期 | `modules/ocean-bookings/`、`modules/container-loading/`、`modules/port-schedules/` | `platformOceanBookings.ts` 的 `/link-candidates` 只消费服务端分页;列表 `containerCount` 是柜型需求 `quantity` 总和,不是已关联柜数;Backend `firstleg/oceanbooking/`、`firstleg/portschedule/`,出仓后节点归四期 | -| 拖车、历史报关协同、国内出仓 | `modules/trucking/`、`modules/customs/`、`modules/outbound-dispatch/` | `modules/customs/` 是报关案件/清单/代理提交与回执入口;装柜完成后的整柜退税/买单资料改从货柜详情进入,不得在旧模块复制一套;Backend `firstleg/outbound/dispatch/`、`firstleg/customs/`、`file/` | +| 三期订舱资料、打印、关联候选与出仓前船期 | `modules/ocean-bookings/` 的 `components/OceanBookingDocumentsPanel.vue`、`OceanBookingPrintDialog.vue`,以及 `modules/container-loading/`、`modules/port-schedules/` | 订舱草稿维护资料,放舱后资料与表单只读;打印只对 `RELEASED` 开放。`platformOceanBookings.ts` 的 `/link-candidates` 只消费服务端分页,`containerCount` 是柜型需求 `quantity` 总和;Backend `firstleg/oceanbooking/`、`file/`、`print/`、`firstleg/portschedule/` | +| 订舱拥有的拖车预约 | `modules/trucking/api/platformTrucking.ts`、`components/TruckAppointmentForm.vue`、`pages/TruckAppointmentsView.vue` | 不选择或提交客户;先选 `RELEASED` 订舱,再选其已分配且有真实 ISO 柜号的柜次,提单号和仓库由订舱冻结事实带入;订舱须有有效 `BOOKING_CONFIRMATION` 或 `BILL_OF_LADING` 文件,同一真实柜最多一条有效预约。拖车附件为平台作用域且不重复提单文件;Backend `TruckAppointmentService`、`truck-appointments-v1.yaml` | +| 历史报关协同与国内出仓 | `modules/customs/`、`modules/outbound-dispatch/` | `modules/customs/` 是历史报关案件/清单/代理提交与回执入口;装柜完成后的整柜退税/买单资料从货柜详情进入,不在旧模块复制;Backend `firstleg/outbound/dispatch/`、`firstleg/customs/`、`file/` | | 主数据、下载、审计 | `modules/master-data/`、`modules/downloads/`、`modules/audit/` | `warehouse/`、`file/`、`audit-service/` | | 四期海外接柜、入库与库存 | `modules/overseas-warehouse/` | `gateway-overseas/`、`lastleg-service/`、`warehouse-overseas-service/`;不含出库和派送 | | 四期海外采购、卡车、散板 | `modules/overseas-business/` | `gateway-overseas/`、`lastleg-service/` | | 四期合作仓与通用资料 | 合作仓 `modules/warehouses/`;报价/汇率/车辆 `modules/overseas-master-data/` | 国内 `warehouse/`、海外实际资源与服务 | | 四期国内出仓后船节点 | `modules/port-schedules/`、`modules/ocean-bookings/`、货柜详情 | `firstleg/portschedule/`、`integration-service/`;预计节点只展示不推进实际进度,手工实际节点必须严格向后追加并防重复,十节点标准见 `../../docs/四期/船节点标准化契约.md` | +| 五期海外排车、出库与派送 | `modules/overseas-warehouse/pages/DispatchesView.vue`、`DispatchDetailView.vue`、`OutboundsView.vue`、`DeliveryOrdersView.vue`、`DeliveryOrderDetailView.vue` | API 为 `dispatches.ts`、`outbounds.ts`、`deliveryOrders.ts`;Backend `lastleg-service/dispatch/`、`delivery/`、`dropship/`。一件代发出库从服务端库存位置选择并携带幂等键,排车转派送按稳定 `dispatchNo` 进入服务端命令 | +| 五期海外异常退回与客户回访 | `modules/overseas-warehouse/pages/ExceptionOrdersView.vue`、`CustomerVisitsView.vue` | API 为 `exceptionOrders.ts`、`customerVisits.ts`;Backend `lastleg-service/exceptionorder/`、`customervisit/`。派送退回先从服务端查询可关联入仓单,回访附件和派送完成/状态均走海外真实 API | 全量验收按业务从 `../../docs/一期/`、`二期/`、`三期/`、`财务管理/` 选择最小契约;四期开发先读 `../../docs/四期/README.md`。进度、排期或验收状态问题再读取进度表和报告。 -## 四期海外模块内部快速索引 +## 海外模块内部快速索引(四期/五期) | 关键词 | 页面/路由入口 | API 与联查 | |---|---|---| | 尾程订单与柜级入仓计划进度 | `modules/overseas-warehouse/pages/LastMileOrdersView.vue`、`LastMileOrderDetailView.vue` | 按 `sourceContainerBookingId` 消费 `/orders/container-summaries` 的 `allPlansReady`、计划统计与冻结 `plannedArrivalAt`,详情由服务端返回全柜订单且不受列表分页截断;不得在浏览器拼柜或自行推导就绪。整柜生成调用 `/inbound-plans/convert-container`,每票返回独立计划且整批原子;转仓候选只取 `modules/iam/api/platformIam.ts` 的平台公共 `ACTIVE + FBA` 仓主档,并提交 ID/编码/名称快照。业务 API `modules/overseas-warehouse/api/lastMileOrders.ts`,Backend `lastleg-service/` | | 入仓单、入仓计划、FBA 预约 | `modules/overseas-warehouse/pages/InboundOrdersView.vue`、`InboundOrderDetailView.vue` | 列表/预约 `modules/overseas-warehouse/api/inboundOrders.ts`,计划列表/详情 `inboundPlans.ts`;Backend `lastleg-service/inboundplan/` | -| 排车查询与智能预排 | `modules/overseas-warehouse/pages/DispatchesView.vue`、`DispatchDetailView.vue`、`components/SmartDispatchDialog.vue` | `modules/overseas-warehouse/api/dispatches.ts`;Backend `lastleg-service/dispatch/`;覆盖详情、候选、草稿柜、托盘即时锁/解锁/整草稿释放和 `pre-arrange`,不据此开放出库编辑、取消、发车、PDA 出库或派送 | -| 海外出库、派送、异常与回访(范围外现存代码) | `pages/OutboundsView.vue`、`DeliveryOrdersView.vue`、`DeliveryOrderDetailView.vue`、`ExceptionOrdersView.vue`、`CustomerVisitsView.vue` 及对应 `api/outbounds.ts`、`deliveryOrders.ts`、`exceptionOrders.ts`、`customerVisits.ts`;当前路由和导航已有注册 | 当前四期契约仍排除海外仓出库、派送及其派生运营页面;不得因代码、菜单、权限或 API 存在而纳入正式范围、验收或继续扩展,只有用户当前明确确认新范围后才能启用其业务语义 | +| 四期排车查询与智能预排、五期转派送 | `modules/overseas-warehouse/pages/DispatchesView.vue`、`DispatchDetailView.vue`、`components/SmartDispatchDialog.vue` | `modules/overseas-warehouse/api/dispatches.ts`;Backend `lastleg-service/dispatch/`、`delivery/`;四期覆盖详情、候选、草稿柜、托盘锁和 `pre-arrange`,五期继续使用 `transfer-delivery` 转派送。后台暂无排车编辑、取消和发车调用时,不得仅凭后端 Controller 声称页面已支持 | +| 五期海外出库与派送 | `pages/OutboundsView.vue`、`DeliveryOrdersView.vue`、`DeliveryOrderDetailView.vue` | `api/outbounds.ts`、`deliveryOrders.ts`;一件代发出库、取消、派送列表/详情及退回入口对应 Backend `lastleg-service/delivery/`、`dropship/` | +| 五期异常退回与客户回访 | `pages/ExceptionOrdersView.vue`、`CustomerVisitsView.vue` | `api/exceptionOrders.ts`、`customerVisits.ts`;异常关系解析、异常建档、派送退回、完成/状态和回访附件对应 Backend `lastleg-service/exceptionorder/`、`customervisit/` | | 库存余额、库位与流水 | `modules/overseas-warehouse/pages/InventoryView.vue`、`InventoryDetailView.vue`、`InventoryTransactionsView.vue` | `modules/overseas-warehouse/api/inventory.ts`;业务库存归 `lastleg-service/inventory/`,实物资源归 `warehouse-overseas-service/` | | 托盘、箱唛、产品与换标 | `modules/overseas-warehouse/pages/PalletsView.vue`、`CartonsView.vue`、`ProductsView.vue`、`RelabelOrdersView.vue` 及详情页 | `modules/overseas-warehouse/api/pallets.ts`、`relabelOrders.ts`;路由是否公开以 `modules/overseas-warehouse/routes.ts` 为准 | | 海外采购 | `modules/overseas-business/pages/PurchaseOrdersView.vue`、详情/编辑页 | `modules/overseas-business/api/platformOverseasBusiness.ts` 的 purchase 方法;Backend `lastleg-service/procurement/` | diff --git a/src/app/DomesticOutboundListLayout.test.ts b/src/app/DomesticOutboundListLayout.test.ts index 888cdd21..56b5c209 100644 --- a/src/app/DomesticOutboundListLayout.test.ts +++ b/src/app/DomesticOutboundListLayout.test.ts @@ -42,7 +42,8 @@ describe('domestic outbound list responsive contracts', () => { const listLayout = source('src/shared/components/ListPageLayout.vue') expect(queryToolbar).toContain('@media (max-width: 720px)') - expect(queryToolbar).toContain('.query-toolbar__field { width: 100%; }') + expect(queryToolbar).toContain('.query-toolbar__field { width: 100%; flex: 0 0 auto; }') + expect(queryToolbar).toContain('.query-toolbar__control { width: 100%; flex: 0 0 auto; }') expect(queryToolbar).toContain('grid-template-columns: repeat(auto-fit, minmax(120px, 1fr))') expect(dataTable).toContain('@media (max-width: 720px)') expect(dataTable).toContain('flex-direction: column') diff --git a/src/core/auth/auth.test.ts b/src/core/auth/auth.test.ts index eed3a8cc..ee88a86c 100644 --- a/src/core/auth/auth.test.ts +++ b/src/core/auth/auth.test.ts @@ -98,6 +98,10 @@ describe('admin auth session', () => { await auth.login('platform_admin', 'password') + expect(mocks.post).toHaveBeenCalledWith('/auth/login', { + loginId: 'platform_admin', + password: 'password', + }) expect(auth.isAuthenticated).toBe(true) expect(auth.refreshToken).toBe('refresh-domestic') }) diff --git a/src/core/auth/auth.ts b/src/core/auth/auth.ts index 03aa5f7b..6d995ae4 100644 --- a/src/core/auth/auth.ts +++ b/src/core/auth/auth.ts @@ -176,7 +176,7 @@ export const useAuthStore = defineStore('admin-auth', () => { try { const response = await http.post, ApiEnvelope>( '/auth/login', - { username: account, password }, + { loginId: account, password }, ) const result = unwrapApiResponse(response) await applySession(result, expectedGeneration) diff --git a/src/modules/container-loading/api/platformContainerLoading.test.ts b/src/modules/container-loading/api/platformContainerLoading.test.ts index cbcb13e1..05f266a1 100644 --- a/src/modules/container-loading/api/platformContainerLoading.test.ts +++ b/src/modules/container-loading/api/platformContainerLoading.test.ts @@ -3,8 +3,8 @@ import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import { http } from '@/shared/api/http' import { - adjustSmartLoadingItem, assignSmartLoadingGroup, assignSmartLoadingItem, createSmartLoadingPlan, getContainer, - listContainers, listSmartLoadingCandidates, pushSmartLoadingPreload, removeSmartLoadingGroup, removeSmartLoadingItem, + adjustSmartLoadingItem, assignSmartLoadingGroup, assignSmartLoadingItem, getContainer, + listContainers, listSmartLoadingCandidates, removeSmartLoadingGroup, removeSmartLoadingItem, getContainerClearance, downloadContainerClearance, uploadContainerClearance, completeContainerClearance, getContainerOverview, markContainerException, cancelContainerException, uploadContainerDocument, downloadContainerDocument, downloadContainerPdaPhoto, getCustomerContainerTransitSummary, @@ -66,28 +66,23 @@ describe('platform container loading API', () => { it('normalizes every long id in candidates and plans', async () => { vi.mocked(http.get) .mockResolvedValueOnce({ code: '200', data: container }) - .mockResolvedValueOnce({ code: '200', data: [{ accountId: 1, inboundId: 2, orderId: 4, inboundCode: 'IN-1', warehouseId: 5, destinationWarehouseId: 6, channelCode: 'NY_EXX', channelName: '纽约EXX海铁', goodsSummary: '服装、鞋类', availablePackageCount: '7', unitGrossWeightKg: '12.5', unitNetWeightKg: '11.8', averageUnitVolumeCbm: '0.187', availableVolumeCbm: '1.309', coLoadGroupId: 9007199254740999n, groupNo: 'CLG-9007199254740999', memberCount: '3', groupTotalBoxes: '21', locked: false, blockReason: null }] }) + .mockResolvedValueOnce({ code: '200', data: [{ accountId: 1, inboundId: 2, orderId: 4, inboundCode: 'IN-1', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-10T09:30:00', warehouseId: 5, destinationWarehouseId: 6, channelCode: 'NY_EXX', channelName: '纽约EXX海铁', goodsSummary: '服装、鞋类', availablePackageCount: '7', unitGrossWeightKg: '12.5', unitNetWeightKg: '11.8', averageUnitVolumeCbm: '0.187', availableVolumeCbm: '1.309', coLoadGroupId: 9007199254740999n, groupNo: 'CLG-9007199254740999', memberCount: '3', groupTotalBoxes: '21', locked: false, blockReason: null }] }) expect((await getContainer('9')).id).toBe(String(container.id)) - expect(await listSmartLoadingCandidates('7', '5')).toMatchObject([{ accountId: '1', inboundId: '2', inboundCode: 'IN-1', destinationWarehouseId: '6', channelCode: 'NY_EXX', channelName: '纽约EXX海铁', goodsSummary: '服装、鞋类', availablePackageCount: 7, unitGrossWeightKg: 12.5, unitNetWeightKg: 11.8, averageUnitVolumeCbm: 0.187, availableVolumeCbm: 1.309, coLoadGroupId: '9007199254740999', groupNo: 'CLG-9007199254740999', memberCount: 3, groupTotalBoxes: 21, locked: false, blockReason: null }]) + expect(await listSmartLoadingCandidates('7', '5')).toMatchObject([{ accountId: '1', inboundId: '2', inboundCode: 'IN-1', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-10T09:30:00', destinationWarehouseId: '6', channelCode: 'NY_EXX', channelName: '纽约EXX海铁', goodsSummary: '服装、鞋类', availablePackageCount: 7, unitGrossWeightKg: 12.5, unitNetWeightKg: 11.8, averageUnitVolumeCbm: 0.187, availableVolumeCbm: 1.309, coLoadGroupId: '9007199254740999', groupNo: 'CLG-9007199254740999', memberCount: 3, groupTotalBoxes: 21, locked: false, blockReason: null }]) expect(http.get).toHaveBeenLastCalledWith('/platform/v1/first-leg/smart-loading/inbound-candidates', { params: { bookingId: '7', warehouseId: '5' } }) }) - it('sends plan and item versions plus operation and idempotency keys', async () => { + it('sends plan and item versions plus operation identifiers', async () => { vi.mocked(http.post).mockResolvedValue({ code: '200', data: plan }) vi.mocked(http.patch).mockResolvedValue({ code: '200', data: plan }) vi.mocked(http.delete).mockResolvedValue({ code: '200', data: plan }) - const normalized = await createSmartLoadingPlan('7', null) await assignSmartLoadingItem('10', { inboundId: '2', packageCount: 4, expectedVersion: 5 }) await adjustSmartLoadingItem('10', '11', 6, 7, 8) await removeSmartLoadingItem('10', '11', 9, 10) - await pushSmartLoadingPreload('10', 11) - expect(http.post).toHaveBeenNthCalledWith(1, '/platform/v1/first-leg/smart-loading/plans', { bookingId: '7', remark: null }) - expect(normalized).toMatchObject({ id: String(plan.id), version: 3, canAssign: true, canEdit: true, pdaTaskId: String(plan.pdaTaskId), pdaTaskVersion: 4, items: [{ version: 1, pickedPackageCount: 2, minPackageCount: 2, canAdjust: true, canRemove: false }] }) - expect(vi.mocked(http.post).mock.calls[1]?.[0]).toBe('/platform/v1/first-leg/smart-loading/plans/10/inbound-assignments') - expect(vi.mocked(http.post).mock.calls[1]?.[1]).toMatchObject({ expectedVersion: 5, operationId: expect.stringContaining('assign-inbound-') }) + expect(vi.mocked(http.post).mock.calls[0]?.[0]).toBe('/platform/v1/first-leg/smart-loading/plans/10/inbound-assignments') + expect(vi.mocked(http.post).mock.calls[0]?.[1]).toMatchObject({ expectedVersion: 5, operationId: expect.stringContaining('assign-inbound-') }) expect(vi.mocked(http.patch).mock.calls[0]?.[1]).toMatchObject({ expectedPlanVersion: 7, expectedItemVersion: 8, operationId: expect.stringContaining('adjust-') }) expect(vi.mocked(http.delete).mock.calls[0]?.[1]).toMatchObject({ data: { expectedPlanVersion: 9, expectedItemVersion: 10, operationId: expect.stringContaining('remove-') } }) - expect(vi.mocked(http.post).mock.calls[2]?.[2]).toMatchObject({ headers: { 'Idempotency-Key': expect.stringContaining('push-preload-') } }) }) it('uses only the atomic group assign and whole-group remove endpoints for co-load groups', async () => { diff --git a/src/modules/container-loading/api/platformContainerLoading.ts b/src/modules/container-loading/api/platformContainerLoading.ts index 91cffa06..322775a0 100644 --- a/src/modules/container-loading/api/platformContainerLoading.ts +++ b/src/modules/container-loading/api/platformContainerLoading.ts @@ -252,11 +252,6 @@ export async function getSmartLoadingPlan(planId: string) { return plan(unwrapApiResponse(response)) } -export async function createSmartLoadingPlan(bookingId: string, remark: string | null) { - const response = await http.post, ApiEnvelope>(`${smartLoadingPath}/plans`, { bookingId, remark }) - return plan(unwrapApiResponse(response)) -} - export async function assignSmartLoadingItem(planId: string, payload: { inboundId: string; packageCount: number; expectedVersion: number }) { const response = await http.post, ApiEnvelope>(`${smartLoadingPath}/plans/${encodeURIComponent(planId)}/inbound-assignments`, { ...payload, operationId: operationId('assign-inbound') }) return plan(unwrapApiResponse(response)) @@ -269,7 +264,6 @@ export async function assignSmartLoadingGroup(planId: string, inboundId: string, ) return plan(unwrapApiResponse(response)) } - export async function recommendSmartLoading(planId: string, expectedVersion: number) { const response = await http.post, ApiEnvelope>(`${smartLoadingPath}/plans/${encodeURIComponent(planId)}/recommend`, { expectedVersion, operationId: operationId('recommend') }) return plan(unwrapApiResponse(response)) @@ -293,7 +287,3 @@ export async function removeSmartLoadingGroup(planId: string, groupId: string, e return plan(unwrapApiResponse(response)) } -export async function pushSmartLoadingPreload(planId: string, expectedVersion: number) { - const response = await http.post, ApiEnvelope>(`${smartLoadingPath}/plans/${encodeURIComponent(planId)}/push-preload`, { expectedVersion }, { headers: { 'Idempotency-Key': operationId('push-preload') } }) - return plan(unwrapApiResponse(response)) -} diff --git a/src/modules/container-loading/components/ContainerCustomsPanel.test.ts b/src/modules/container-loading/components/ContainerCustomsPanel.test.ts index 3166eac1..f55bcad7 100644 --- a/src/modules/container-loading/components/ContainerCustomsPanel.test.ts +++ b/src/modules/container-loading/components/ContainerCustomsPanel.test.ts @@ -69,27 +69,30 @@ describe('ContainerCustomsPanel', () => { const wrapper = await render('READY_TO_START') expect(wrapper.text()).toContain('开始报关') expect(wrapper.text()).toContain('客户退税资料.pdf') - expect(wrapper.find('[aria-label="下载源资料 客户退税资料.pdf"]').exists()).toBe(true) - expect(buttonTexts(wrapper)).not.toContain('上传完成文件') - expect(buttonTexts(wrapper)).not.toContain('覆盖完成文件') + expect(wrapper.text()).toContain('客户退税资料') + expect(wrapper.text()).toContain('报关完成文件') + expect(wrapper.text()).not.toContain('客户报关资料') + expect(wrapper.find('[aria-label="下载客户退税资料 客户退税资料.pdf"]').exists()).toBe(true) + expect(buttonTexts(wrapper)).not.toContain('上传报关完成文件') + expect(buttonTexts(wrapper)).not.toContain('覆盖报关完成文件') expect(buttonTexts(wrapper)).not.toContain('提交') }) it('allows completion upload and overwrite only while DECLARING with manage permission', async () => { let wrapper = await render('DECLARING', true, false) - expect(wrapper.text()).toContain('上传完成文件') + expect(wrapper.text()).toContain('上传报关完成文件') expect(wrapper.text()).toContain('提交') wrapper.unmount() wrapper = await render('DECLARING', true, true) - expect(wrapper.text()).toContain('覆盖完成文件') + expect(wrapper.text()).toContain('覆盖报关完成文件') expect(wrapper.text()).toContain('报关完成文件.pdf') wrapper.unmount() wrapper = await render('DECLARING', false, true) expect(wrapper.text()).toContain('只读') - expect(buttonTexts(wrapper)).not.toContain('上传完成文件') - expect(buttonTexts(wrapper)).not.toContain('覆盖完成文件') + expect(buttonTexts(wrapper)).not.toContain('上传报关完成文件') + expect(buttonTexts(wrapper)).not.toContain('覆盖报关完成文件') expect(buttonTexts(wrapper)).not.toContain('提交') }) @@ -98,19 +101,19 @@ describe('ContainerCustomsPanel', () => { expect(wrapper.text()).toContain('客户退税资料.pdf') expect(wrapper.text()).toContain('报关完成文件.pdf') expect(wrapper.text()).toContain('报关已完成,待其他离仓条件') - expect(buttonTexts(wrapper)).not.toContain('上传完成文件') - expect(buttonTexts(wrapper)).not.toContain('覆盖完成文件') + expect(buttonTexts(wrapper)).not.toContain('上传报关完成文件') + expect(buttonTexts(wrapper)).not.toContain('覆盖报关完成文件') expect(buttonTexts(wrapper)).not.toContain('提交') }) it('routes source and completion downloads independently by the long document id and original names', async () => { const wrapper = await render('COMPLETED', false, true) - await wrapper.get('[aria-label="下载源资料 客户退税资料.pdf"]').trigger('click') + await wrapper.get('[aria-label="下载客户退税资料 客户退税资料.pdf"]').trigger('click') await flushPromises() expect(api.downloadSource).toHaveBeenCalledWith('2092438843630391407') expect(api.downloadCompletion).not.toHaveBeenCalled() - await wrapper.get('[aria-label="下载完成文件 报关完成文件.pdf"]').trigger('click') + await wrapper.get('[aria-label="下载报关完成文件 报关完成文件.pdf"]').trigger('click') await flushPromises() expect(api.downloadCompletion).toHaveBeenCalledWith('2092438843630391407') expect(HTMLAnchorElement.prototype.click).toHaveBeenCalledTimes(2) @@ -122,7 +125,7 @@ describe('ContainerCustomsPanel', () => { api.downloadSource.mockRejectedValueOnce(new ApiResponseError('文件不可用', 'CUSTOMS_FILE_404', 404, undefined, 'request-customs-download', 'trace-customs-download')) const wrapper = await render('COMPLETED', false, true) - await wrapper.get('[aria-label="下载源资料 客户退税资料.pdf"]').trigger('click') + await wrapper.get('[aria-label="下载客户退税资料 客户退税资料.pdf"]').trigger('click') await flushPromises() expect(wrapper.text()).toContain('文件不可用') diff --git a/src/modules/container-loading/components/ContainerCustomsPanel.vue b/src/modules/container-loading/components/ContainerCustomsPanel.vue index 40847c08..3bb09b2d 100644 --- a/src/modules/container-loading/components/ContainerCustomsPanel.vue +++ b/src/modules/container-loading/components/ContainerCustomsPanel.vue @@ -40,7 +40,7 @@ const missingCoverageCount = computed(() => Math.max(0, (customs.value?.taxRefun const allBuy = computed(() => customs.value?.taxRefundCount === 0) const missingCompletionCount = computed(() => customs.value?.documents.filter((item) => !item.completion).length || 0) const statusText: Record = { - WAITING_CUSTOMER_DOCUMENTS: '待客户补齐资料', READY_TO_START: '可开始报关', DECLARING: '报关中', COMPLETED: '报关已完成', + WAITING_CUSTOMER_DOCUMENTS: '待客户补齐退税资料', READY_TO_START: '可开始报关', DECLARING: '报关中', COMPLETED: '报关已完成', } function documentCustomers(document: ContainerCustomsDocument) { @@ -64,7 +64,7 @@ async function load() { async function start() { if (!customs.value || customs.value.status !== 'READY_TO_START') return try { - await ElMessageBox.confirm('开始后将冻结客户当前资料版本、关联入仓码覆盖范围及平台买单准备事实,确认开始整柜报关?', '开始整柜报关', { type: 'warning', confirmButtonText: '确认开始' }) + await ElMessageBox.confirm('开始后将冻结客户当前退税资料版本、关联入仓码覆盖范围及平台买单准备事实,确认开始整柜报关?', '开始整柜报关', { type: 'warning', confirmButtonText: '确认开始' }) } catch { return } actionLoading.value = true; actionError.value = null try { customs.value = await startContainerCustoms(props.loadingPlanId, customs.value.version); ElMessage.success('整柜报关已开始') } @@ -75,11 +75,11 @@ async function start() { async function uploadCompletion(document: ContainerCustomsDocument) { if (!props.canManage || customs.value?.status !== 'DECLARING') return const selected = completionFiles[document.documentId] - if (!selected) { ElMessage.warning('请选择对应完成文件'); return } + if (!selected) { ElMessage.warning('请选择对应的报关完成文件'); return } const finalConfirmation = !document.completion && missingCompletionCount.value === 1 if (finalConfirmation) { try { - await ElMessageBox.confirm('这是最后一份待上传完成文件;确认后系统将自动完成当前混合/退税柜报关。是否继续?', '完成整柜报关', { type: 'warning', confirmButtonText: '确认上传并完成' }) + await ElMessageBox.confirm('这是最后一份待上传的报关完成文件;确认后系统将自动完成当前混合/退税柜报关。是否继续?', '完成整柜报关', { type: 'warning', confirmButtonText: '确认上传并完成' }) } catch { return } } actionLoading.value = true; actionError.value = null @@ -88,8 +88,8 @@ async function uploadCompletion(document: ContainerCustomsDocument) { completionFiles[document.documentId] = null await load() emit('refreshed') - ElMessage.success(finalConfirmation ? '完成文件已上传,整柜报关已完成' : '完成文件已上传') - } catch (error) { actionError.value = getApiErrorDetails(error, '完成文件上传失败') } + ElMessage.success(finalConfirmation ? '报关完成文件已上传,整柜报关已完成' : '报关完成文件已上传') + } catch (error) { actionError.value = getApiErrorDetails(error, '报关完成文件上传失败') } finally { actionLoading.value = false } } @@ -113,9 +113,9 @@ async function downloadFile(document: ContainerCustomsDocument, kind: 'source' | ? await downloadContainerCustomsDocument(document.documentId) : await downloadContainerCustomsCompletion(document.documentId) saveBlob(blob, target.fileName) - ElMessage.success(`${kind === 'source' ? '源资料' : '完成文件'}下载已开始`) + ElMessage.success(`${kind === 'source' ? '客户退税资料' : '报关完成文件'}下载已开始`) } catch (error) { - actionError.value = getApiErrorDetails(error, `${kind === 'source' ? '源资料' : '完成文件'}下载失败`) + actionError.value = getApiErrorDetails(error, `${kind === 'source' ? '客户退税资料' : '报关完成文件'}下载失败`) } finally { downloadingKey.value = '' } @@ -168,17 +168,17 @@ onMounted(load) diff --git a/src/modules/container-loading/pages/ContainerDetailView.vue b/src/modules/container-loading/pages/ContainerDetailView.vue index 329a672c..61ed3153 100644 --- a/src/modules/container-loading/pages/ContainerDetailView.vue +++ b/src/modules/container-loading/pages/ContainerDetailView.vue @@ -363,7 +363,7 @@ onMounted(async () => { await load(); if (route.query.action === 'documents') op
返回列表

{{ title }}

柜次资料、履约轨迹、PDA 进度与单证

-
柜次号{{ detail.cabinetNo || '—' }}
集装箱号{{ detail.containerNo || '待录入' }}
装柜清单号{{ detail.planNo || '—' }}
柜型{{ detail.containerType || '—' }}
装柜仓库{{ detail.warehouseName || '—' }}
+
柜次号{{ detail.cabinetNo || '—' }}
集装箱号{{ detail.containerNo || '待补录' }}
装柜清单号{{ detail.planNo || '—' }}
柜型{{ detail.containerType || '—' }}
装柜仓库{{ detail.warehouseName || '—' }}