diff --git a/AGENTS.md b/AGENTS.md index 1d2ee46d..c457b9ea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,61 +36,52 @@ ## 模块快速路由 -表中 `modules/`、`core/`、`shared/`、`app/` 和 `main.ts` 均相对 `src/`;裸文件名沿用同一单元格最近目录。模块细节只在最近指引维护,应用层保留入口。 +按短表定位后按需查 [业务细则](docs/business-index.md);涉及业务行为变更时必须核对对应细则及最近模块指引。从应用根执行 `rg -n -F '任务词或文件名' docs/business-index.md`,只读命中条款。 -| 业务 | Admin 入口 | 联查 | -|---|---|---| -| 登录、会话与统一 Token | `modules/auth/`、`core/auth/` | 国内 `auth/`、`iam/`;海外请求客户端再读 `shared/api/AGENTS.md` | -| 启动与懒加载资源恢复 | `main.ts`、`core/routing/navigationRecovery.ts` | 仅识别动态模块/Chunk 资源过期;30 秒内同目标最多自动恢复一次,存在未保存编辑时必须先确认 | -| 工作台与指标 | `modules/dashboard/` | 指标所属模块与服务端,不复制领域契约 | -| 自营销量、库存与数据采集 | `modules/self-operated/AGENTS.md` | 三个入口 `/self-operated/sales`、`/self-operated/inventory`、`/self-operated/collection`;后端 `lastleg-service/selfoperated/analytics/`,正式合同见 `../../docs/自营业务/README.md`。 | -| 船期日历 | `modules/sailing-calendar/` | 当前日历入口覆盖渠道、周模板、单次船期、收货截止时间和按渠道货量统计;后端按 `firstleg/` 船期接口定位,不读取已归档的货柜开发规划 | -| 预约管理、预约列表、跨客户同仓预约 | `modules/appointment-calendar/`;入仓单预约沿 `modules/overseas-warehouse/` | 统一规则见 `../../docs/海外仓业务/README.md` 的预约送仓;同仓跨客户共用预约,客户归属在货物明细;到港只读 Firstleg,送仓归 Lastleg | -| FBA 无匹配价格、人工定价 | `modules/inbounds/fee/api.ts` 的 `createManualInboundQuote`、`types.ts`;页面消费沿 `InboundFeePanel.vue` 核对 | `POST /manual-quote` 提交计费重量、体积、单价、原因和 `Idempotency-Key`;Backend `PlatformFbaPricingController.manualQuote` → `FbaPricingService.manualQuote`,权限 `firstleg:inbound:confirm-send-fee`。仅 FBA、已有实际测量且当前价格未命中时允许,客户已确认费用禁止改价;`MANUAL_QUOTE` 快照允许价格记录与版本为空,不伪造价格簿关联。API 回归 `modules/inbounds/fee/api.test.ts`,不能以 API 存在代替页面接入完成 | -| PDA版本、APK下载、WGT热更新发布与停发 | `modules/pda-releases/pages/PdaReleasesView.vue`、`modules/pda-releases/api/pdaReleases.ts`、`modules/pda-releases/routes.ts` | 系统菜单 `/system/pda-releases`;列表权限 `platform:pda-release:list`,上传/发布/停发权限 `platform:pda-release:manage`。国内/海外按 `domestic/overseas` 分开查询,上传只创建草稿,发布与停发需确认;管理请求使用国内 HTTP 的 `/platform/v1/files/pda-releases`,后端见 `file/AGENTS.md`,设备安装与重启查对应 PDA 的更新工具 | -| 客户、企业审核、客户统计 | `modules/customers/` | `iam/`、`auth/`、`file/`、`firstleg/customerstatistics/`;公司联系电话只作企业联系信息,不修改管理员/成员登录手机号且不要求跨企业唯一。审核必填资质仅营业执照与统一社会信用代码;法人身份证选填,可随资质包提交,也可在已有营业执照后单独补传或替换 | -| 供应商、国内整柜 | `modules/suppliers/`、`modules/domestic-containers/` | Backend `warehouse/` 及对应 OpenAPI | -| 平台仓库与仓储资源 | 仓库主档 `modules/warehouses/`,国内资源 `modules/warehouse-resources/` | 全部仓库 ID、类型、状态、地址和能力归国内 `warehouse/`;海外 `warehouse-overseas-service/` 只维护引用同一 warehouseId 的库位、托盘和锁 | -| 平台用户、RBAC、导航 | `modules/iam/`、`core/access/`、`core/navigation/` | `auth/`、`iam/`、平台权限契约 | -| 国内/海外 PDA 独立人员与海外仓绑定 | `modules/iam/pages/InternalUsersView.vue`、`modules/iam/api/platformIam.ts` | 国内 `CN_FIRSTLEG_PDA_OPERATOR` 与海外 `OVERSEAS_WAREHOUSE_PDA_OPERATOR` 互斥;只有海外角色必须且只能绑定一个 `overseasWarehouseId`,候选来自国内 Warehouse 平台仓库 API 的公共 `OVERSEAS + ACTIVE + RECEIVING` 收货仓 | -| 消息中心 | `modules/notifications/` | Backend `iam/.../notification/`;仅顶部消息中心,不进入左侧菜单 | -| 订单列表、批量审核预览、详情、商品图片与入库费用 | `modules/unified-orders/`、`modules/first-leg-orders/`;平台商品图片由 `modules/unified-orders/api/unifiedOrders.ts` 按客户与 fileId 下载 | `order/`、`firstleg/`、Finance;旧系统导入固定归属“IT测试公司”,前端不提交或选择客户,逐行结果展示入仓码与失败原因。图片读取归 `file/` 的 `PlatformProductImageController`。Order 状态为 `SUBMITTED` 时统一列表必须保持 `PENDING_REVIEW/APPROVE_ORDER`,不得被已存在的 Firstleg 准备中入库投影覆盖。批量审核弹框通过 `unifiedOrders.ts` 的 `/orders/first-leg/batch-review-preview` 一次取回全部预览,按服务端 `canApprove/blockReason` 展示,不逐票请求详情或用内部 ID 作标题 | -| 客户原始申报资料编辑与渠道变更 | `modules/unified-orders/components/OrderDeclarationEditorDialog.vue`、`api/unifiedOrders.ts`、`pages/UnifiedOrdersView.vue` | 使用订单版本和货物 `itemNo` 整体提交,不允许增删货物行;只编辑客户申报快照,不覆盖仓库/PDA 实测、测量审核或计费事实。渠道变化需二次确认并由 Backend `order/` 联查 `firstleg/` 的分柜、同柜、装柜与重新计价门禁;国内收货仓或中转仓变化必须重新下单 | -| 入库扣货、解除与凭证历史 | `modules/unified-orders/pages/UnifiedOrdersView.vue`、`modules/inbounds/core/api.ts`、`types.ts` | 页面只消费服务端 `holdFlag/currentHoldReason/canHold/canRelease/actionBlockReason/history`;扣货与解除提交当前入库版本、幂等键、必填原因和可选凭证,凭证下载只走 Firstleg 授权入口。Backend `firstleg/` 的 `InboundHoldService`、`InboundHoldGuard`、`InboundHoldEvidenceService`,文件归 `file/` | -| 入库、测量、称重库位状态、客户箱规策略与计价主数据 | 测量弹框/工作区 `modules/inbounds/measurement/InboundMeasurementDialog.vue`、`InboundMeasurementPanel.vue`,称重 `modules/inbounds/weight/components/WeightOcrPanel.vue`;主数据 `modules/fba-pricing/`、`modules/dimension-config/`、`modules/customers/components/CustomerBillingRulesPanel.vue`、`modules/fee-items/` | 客户固定四档箱规,默认计算方式为 `ADD/MULTIPLY`;测量行可选实际尺寸、箱规尺寸策略或客户默认尺寸系数,保存后以 Backend `firstleg/` 返回的外箱尺寸、体积重和计费基数为准,不在页面独立重算。费用确认的实际箱数、毛重和体积优先汇总当前 `actualMeasurementRows`,不得用可能滞后的工作区顶层合计覆盖明细。称重页在每张图片卡片只展示服务端 `locationCode/slotStatus/sharedOverflow`,不渲染或自行推导整票预留引导;删除图片须提示槽位仍归本票保留,不得按图片数组下标推导托盘或库位 | -| 一件代发托盘打印 | `modules/unified-orders/pages/UnifiedOrdersView.vue` 的“托盘打印”页签、`modules/inbounds/core/DropshipPalletPrintPanel.vue`、`core/api.ts` | 仅一件代发订单展示;测量审核预生成托盘码后由 Admin 电脑端打印,支持本票全部托盘合并打印和单托补打。PDA 只扫码组托,不承担 PDF 下载或打印机连接 | -| 入库单扣货、解除、凭证与历史 | `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,查看进入独立详情路由 `/loading-lists/:planId`,保留返回列表和打印;独立清单详情与货柜清单页签共同使用 `useLoadingListDetail.ts` 加载、校验和处理错误,`LoadingListDetailDrawer.vue` 统一密度、摘要、明细、历史及打印入口,打印实现归 `printLoadingList.ts`,外层只保留返回目标与货柜专属 PDA/报关区域;货柜列表经 `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/` | -| 订舱级拖车预约 | `modules/trucking/api/platformTrucking.ts` 的 `/candidates`、`components/TruckAppointmentForm.vue`、`pages/TruckAppointmentCreateView.vue`、`TruckAppointmentsView.vue` | 表单只选择服务端返回的 `RELEASED` 订舱与 `TRUCKING + truckingScope=DOMESTIC` 国内拖车供应商,不选择或提交客户、柜次、装柜计划或真实集装箱号;清关提柜固定选择 `OVERSEAS` 海外车队,类型在供应商主档单选维护;提单号和仓库由订舱冻结事实带入。一个订舱最多一张非终态拖车单,重复创建转入现有单继续编辑;PDA 完成不自动推进拖车。附件为平台作用域且不重复提单文件;Backend `TruckAppointmentService`、`truck-appointments-v1.yaml` | -| 目的港码头拖车与还空预警 | 海外清关提柜工作台/详情 `modules/container-loading/pages/OverseasContainersView.vue`、`OverseasContainerDetailView.vue`,API `modules/container-loading/api/terminalDrayages.ts`、`containerArrivalTimes.ts` | 与订舱级拖车预约是不同生命周期;按 `containerBookingId` 读取服务端状态、`allowedActions` 和历史,不能复用订舱级拖车表单或自行推进状态。列表“预计到仓”列直接提供人工日期时间选择器,保存时通过 Lastleg 柜级版本和幂等键原子更新该柜全部尾程订单,已有入库计划后禁止修改;“实际到仓”只读 `ll_receiving_container_session.receiving_at`,即海外 PDA 首次开始收货时间。还空截止、超时天/小时只展示服务端基于权威卸柜完成时间加 72 小时计算的结果;Backend `firstleg/terminaldrayage/`、`lastleg-service/`,邮件记录通过 `terminalDrayages.ts` 查询与重试。 | -| 历史报关协同与国内出仓 | `modules/customs/`、`modules/outbound-dispatch/` | `modules/customs/` 是历史报关案件/清单/代理提交与回执入口;装柜完成后的整柜退税/买单资料从货柜详情进入,不在旧模块复制;Backend `firstleg/outbound/dispatch/`、`firstleg/customs/`、`file/` | -| 主数据、下载、退避任务重试、审计 | `modules/master-data/`、`modules/downloads/pages/DownloadCenterView.vue`、`modules/audit/` | File 导出任务返回 `attemptCount/maxAttempts/nextRetryAt`;仅失败或已进入自动退避的等待任务显示重试,等待任务重试用于立即执行当前投递,不创建新任务;联查 `warehouse/`、`file/`、`audit-service/` | -| 海外接柜、入库、库存与库内服务 | `modules/overseas-warehouse/` | `gateway/`、`lastleg-service/`、`warehouse-overseas-service/`;与排车、出库、派送、异常退回和一件代发共同组成海外仓全流程 | -| 海外采购、卡车、散板 | `modules/overseas-business/` | `gateway/`、`lastleg-service/` | -| 海外仓业务合作仓与通用资料 | 合作仓 `modules/warehouses/`;报价/汇率/车辆 `modules/overseas-master-data/` | 国内 `warehouse/`、海外实际资源与服务 | -| 海外仓业务国内出仓后船节点 | `modules/port-schedules/`、`modules/ocean-bookings/`、货柜详情 | `firstleg/portschedule/`、`integration-service/`;预计节点只展示不推进实际进度,手工实际节点必须严格向后追加并防重复,十节点标准见 `../../docs/海外仓业务/船节点标准化契约.md` | -| 海外排车、出库与派送 | `modules/overseas-warehouse/AGENTS.md` | 按模块表定位排车、出库、派送页面及 API;履约边界在模块内维护 | -| 海外异常退回与客户回访 | `modules/overseas-warehouse/AGENTS.md` | 派送异常退回区别于退货库存;页面、API 与原子推进约束见模块指引 | +路径从应用根起算,`modules/`、`core/` 从 `src/` 起算;接口片段与符号沿同一行代码定位。 -全量验收按业务从 `../../docs/订单与客户/`、`入库测量与计费/`、`国内货柜履约/`、`财务管理/` 选择最小契约;海外跨模块流程或入口不明时读 `../../docs/海外仓业务/README.md`,已定位局部任务直接沿模块入口。进度、排期或验收状态问题再读取对应进度表和报告。 - -散货托盘码打印:`modules/loading-lists/components/PalletLabelPrintDialog.vue`、`modules/loading-lists/api/platformLoadingLists.ts`,由货柜详情与装柜清单详情进入;先调用 `/pallet-labels/prepare` 获取服务端按 FBA 仓分组的冻结清单,再通过 `/pallet-labels/print` 下载 PDF。托盘码使用“真实柜号-FBA 地址内部编号-三位序号”,页面同时展示 FBA 仓码和内部编号;支持全部打印或按仓指定张数补打,一件代发托盘不在该清单,前端不生成或重新编号托盘码。 +| 业务 | 首个入口 | +|---|---| +| 登录、会话与统一 Token | `modules/auth/`、`core/auth/` | +| 启动与懒加载资源恢复 | `src/main.ts`、`core/routing/navigationRecovery.ts` | +| 工作台与指标 | `modules/dashboard/` | +| 自营销量、库存与数据采集 | `modules/self-operated/AGENTS.md` | +| 船期日历 | `modules/sailing-calendar/` | +| 预约管理、预约列表、跨客户同仓预约 | `modules/appointment-calendar/`、`modules/overseas-warehouse/` | +| FBA 无匹配价格、人工定价 | `modules/inbounds/fee/api.ts`、`createManualInboundQuote` | +| PDA版本、APK下载、WGT热更新发布与停发 | `modules/pda-releases/pages/PdaReleasesView.vue`、`modules/pda-releases/api/pdaReleases.ts` | +| 客户、企业审核、客户统计 | `modules/customers/` | +| 供应商、国内整柜 | `modules/suppliers/`、`modules/domestic-containers/` | +| 平台仓库与仓储资源 | `modules/warehouses/`、`modules/warehouse-resources/` | +| 平台用户、RBAC、导航 | `modules/iam/`、`core/access/` | +| 国内/海外 PDA 独立人员与海外仓绑定 | `modules/iam/pages/InternalUsersView.vue`、`modules/iam/api/platformIam.ts` | +| 消息中心 | `modules/notifications/` | +| 订单列表、批量审核预览、详情、商品图片与入库费用 | `modules/unified-orders/`、`modules/first-leg-orders/` | +| 客户原始申报资料编辑与渠道变更 | `modules/unified-orders/components/OrderDeclarationEditorDialog.vue`、`modules/unified-orders/api/unifiedOrders.ts` | +| 入库扣货、解除与凭证历史 | `modules/unified-orders/pages/UnifiedOrdersView.vue`、`modules/inbounds/core/api.ts` | +| 入库、测量、称重库位状态、客户箱规策略与计价主数据 | `modules/inbounds/measurement/InboundMeasurementDialog.vue`、`modules/inbounds/measurement/InboundMeasurementPanel.vue` | +| 一件代发托盘打印 | `modules/unified-orders/pages/UnifiedOrdersView.vue`、`modules/inbounds/core/DropshipPalletPrintPanel.vue` | +| 入库单扣货、解除、凭证与历史 | `modules/inbounds/pages/InboundOrdersView.vue`、`modules/inbounds/core/api.ts` | +| 客户专属价格簿、柜级阶梯运费与受控调价 | `modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue`、`modules/fba-pricing/carrier-rates/pages/CarrierRateDetailView.vue` | +| 财务、客户资金账户与柜级公共费用分摊 | `modules/finance/AGENTS.md` | +| 货柜双工作台、排柜、预装/装柜清单、真实柜号后补与整柜报关 | `modules/container-loading/pages/DomesticContainersView.vue`、`modules/container-loading/pages/OverseasContainersView.vue` | +| 合柜报关 / 同柜不可拆组 | `modules/unified-orders/api/coLoadGroups.ts`、`modules/unified-orders/pages/UnifiedOrdersView.vue` | +| 全局智能装柜工作台、入仓码候选与箱数分配 | `modules/container-loading/components/SmartLoadingDialog.vue`、`modules/container-loading/pages/GlobalSmartLoadingView.vue` | +| 国内货柜履约订舱资料、打印、关联候选与出仓前船期 | `modules/ocean-bookings/`、`modules/ocean-bookings/components/OceanBookingDocumentsPanel.vue` | +| 订舱级拖车预约 | `modules/trucking/api/platformTrucking.ts`、`/candidates` | +| 目的港码头拖车与还空预警 | `modules/container-loading/pages/OverseasContainersView.vue`、`modules/container-loading/pages/OverseasContainerDetailView.vue` | +| 历史报关协同与国内出仓 | `modules/customs/`、`modules/outbound-dispatch/` | +| 主数据、下载、退避任务重试、审计 | `modules/master-data/`、`modules/downloads/pages/DownloadCenterView.vue` | +| 海外接柜、入库、库存与库内服务 | `modules/overseas-warehouse/` | +| 海外采购、卡车、散板 | `modules/overseas-business/` | +| 海外仓业务合作仓与通用资料 | `modules/warehouses/`、`modules/overseas-master-data/` | +| 海外仓业务国内出仓后船节点 | `modules/port-schedules/`、`modules/ocean-bookings/` | +| 海外排车、出库与派送 | `modules/overseas-warehouse/AGENTS.md` | +| 海外异常退回与客户回访 | `modules/overseas-warehouse/AGENTS.md` | ## 海外模块内部快速索引(海外仓全流程) -- 海外仓的尾程订单控制与柜级入仓计划转换、退货库存、排车出库、异常回访:读取 `src/modules/overseas-warehouse/AGENTS.md`,字段与流程仅在该模块维护。 - -| 关键词 | 页面/路由入口 | API 与联查 | -|---|---|---| -| 海外采购 | `modules/overseas-business/pages/PurchaseOrdersView.vue`、详情/编辑页 | `modules/overseas-business/api/platformOverseasBusiness.ts` 的 purchase 方法;Backend `lastleg-service/procurement/` | -| 美国卡车、本土散板 | `modules/overseas-business/pages/TransportOrdersView.vue`、`TransportOrderFormView.vue` | 同一 API 的 us-truck/looseboard 方法;Backend `lastleg-service/trucking/`、`looseboard/` | -| 海外报价、月度汇率、车辆 | `modules/overseas-master-data/pages/OverseasQuotesView.vue`、`ExchangeRatesView.vue`、`VehiclesView.vue` | `modules/overseas-master-data/api/overseasMasterData.ts`;Backend `lastleg-service/masterdata/` | -| 司机档案、驾驶证、到期提醒与资格预览 | `modules/overseas-master-data/pages/DriversView.vue`、`DriverDetailView.vue`;最近指引 `modules/overseas-master-data/AGENTS.md` | `api/drivers.ts`;Backend `lastleg-service/fleet/driver/`、File `FLEET_DRIVER_LICENSE` | +海外仓作业先读 `src/modules/overseas-warehouse/AGENTS.md`;采购、车队等页面/API 联查按需查 `docs/business-index.md` 同名节。 ## 业务、安全与共享装配 @@ -127,7 +118,7 @@ pnpm typecheck pnpm build ``` -- 普通变更至少运行 `pnpm typecheck` 和受影响测试;交付前运行 `pnpm test` 与 `pnpm build`。当前没有 lint 脚本,不声称 lint 通过。 +- 源码、依赖或构建配置变更至少运行 `pnpm typecheck` 和受影响测试,交付前运行 `pnpm test` 与 `pnpm build`;不影响代码、契约或配置的纯文档/索引变更只检查路径、链接及索引合同。当前没有 lint 脚本,不声称 lint 通过。 - 不手工修改 `dist/`、`node_modules/` 或生成声明;依赖变化同步 `package.json` 与锁文件。 - 测试环境全量验收入口为 `../../baiyue-firstleg/scripts/admin-test-env-acceptance.mjs`,模块说明见同仓库 `scripts/admin-test-env-modules/README.md`;它会写入或停用测试数据,只有本轮明确授权才能运行。 - `pnpm test/typecheck` 和本地回归脚本不代表真实服务验收完成;HTTP 验收后仍由人工完成页面和截图确认。 diff --git a/docs/business-index.md b/docs/business-index.md new file mode 100644 index 00000000..b5e72cbb --- /dev/null +++ b/docs/business-index.md @@ -0,0 +1,65 @@ +# Admin业务详细索引 + +本文件保存当前页面/API 入口及业务规则,是[应用指引](../AGENTS.md)的按需补充。涉及字段、状态、权限、金额、资源或作业恢复时必须读取受影响条款,不作为普通定位任务的全文首读。 + +正文代码路径以工作区 `apps/admin-web/` 应用根起算,源码包缩写与裸文件名沿对应条目的目录解释;明确标注“工作区”的路径从工作区根起算。Markdown 相对链接以本文件所在的 `docs/` 为起点。已知文件直接定位;行为变更同时核对最近模块指引及服务端合同。 + +原条款在本文件维护,主 AGENTS 仅保留短入口和通用边界;不要将新业务细节复制回主指引。 + +## 模块快速路由 + +表中 `modules/`、`core/`、`shared/`、`app/` 和 `main.ts` 均相对 `src/`;裸文件名沿用同一单元格最近目录。模块细节只在最近指引维护,应用层保留入口。 + +| 业务 | Admin 入口 | 联查 | +|---|---|---| +| 登录、会话与统一 Token | `modules/auth/`、`core/auth/` | 国内 `auth/`、`iam/`;海外请求客户端再读 `shared/api/AGENTS.md` | +| 启动与懒加载资源恢复 | `main.ts`、`core/routing/navigationRecovery.ts` | 仅识别动态模块/Chunk 资源过期;30 秒内同目标最多自动恢复一次,存在未保存编辑时必须先确认 | +| 工作台与指标 | `modules/dashboard/` | 指标所属模块与服务端,不复制领域契约 | +| 自营销量、库存与数据采集 | `modules/self-operated/AGENTS.md` | 三个入口 `/self-operated/sales`、`/self-operated/inventory`、`/self-operated/collection`;后端 `lastleg-service/selfoperated/analytics/`,正式合同见 `../../docs/自营业务/README.md`。 | +| 船期日历 | `modules/sailing-calendar/` | 当前日历入口覆盖渠道、周模板、单次船期、收货截止时间和按渠道货量统计;后端按 `firstleg/` 船期接口定位,不读取已归档的货柜开发规划 | +| 预约管理、预约列表、跨客户同仓预约 | `modules/appointment-calendar/`;入仓单预约沿 `modules/overseas-warehouse/` | 统一规则见 `../../docs/海外仓业务/README.md` 的预约送仓;同仓跨客户共用预约,客户归属在货物明细;到港只读 Firstleg,送仓归 Lastleg | +| FBA 无匹配价格、人工定价 | `modules/inbounds/fee/api.ts` 的 `createManualInboundQuote`、`types.ts`;页面消费沿 `InboundFeePanel.vue` 核对 | `POST /manual-quote` 提交计费重量、体积、单价、原因和 `Idempotency-Key`;Backend `PlatformFbaPricingController.manualQuote` → `FbaPricingService.manualQuote`,权限 `firstleg:inbound:confirm-send-fee`。仅 FBA、已有实际测量且当前价格未命中时允许,客户已确认费用禁止改价;`MANUAL_QUOTE` 快照允许价格记录与版本为空,不伪造价格簿关联。API 回归 `modules/inbounds/fee/api.test.ts`,不能以 API 存在代替页面接入完成 | +| PDA版本、APK下载、WGT热更新发布与停发 | `modules/pda-releases/pages/PdaReleasesView.vue`、`modules/pda-releases/api/pdaReleases.ts`、`modules/pda-releases/routes.ts` | 系统菜单 `/system/pda-releases`;列表权限 `platform:pda-release:list`,上传/发布/停发权限 `platform:pda-release:manage`。国内/海外按 `domestic/overseas` 分开查询,上传只创建草稿,发布与停发需确认;管理请求使用国内 HTTP 的 `/platform/v1/files/pda-releases`,后端见 `file/AGENTS.md`,设备安装与重启查对应 PDA 的更新工具 | +| 客户、企业审核、客户统计 | `modules/customers/` | `iam/`、`auth/`、`file/`、`firstleg/customerstatistics/`;公司联系电话只作企业联系信息,不修改管理员/成员登录手机号且不要求跨企业唯一。审核必填资质仅营业执照与统一社会信用代码;法人身份证选填,可随资质包提交,也可在已有营业执照后单独补传或替换 | +| 供应商、国内整柜 | `modules/suppliers/`、`modules/domestic-containers/` | Backend `warehouse/` 及对应 OpenAPI | +| 平台仓库与仓储资源 | 仓库主档 `modules/warehouses/`,国内资源 `modules/warehouse-resources/` | 全部仓库 ID、类型、状态、地址和能力归国内 `warehouse/`;海外 `warehouse-overseas-service/` 只维护引用同一 warehouseId 的库位、托盘和锁 | +| 平台用户、RBAC、导航 | `modules/iam/`、`core/access/`、`core/navigation/` | `auth/`、`iam/`、平台权限契约 | +| 国内/海外 PDA 独立人员与海外仓绑定 | `modules/iam/pages/InternalUsersView.vue`、`modules/iam/api/platformIam.ts` | 国内 `CN_FIRSTLEG_PDA_OPERATOR` 与海外 `OVERSEAS_WAREHOUSE_PDA_OPERATOR` 互斥;只有海外角色必须且只能绑定一个 `overseasWarehouseId`,候选来自国内 Warehouse 平台仓库 API 的公共 `OVERSEAS + ACTIVE + RECEIVING` 收货仓 | +| 消息中心 | `modules/notifications/` | Backend `iam/.../notification/`;仅顶部消息中心,不进入左侧菜单 | +| 订单列表、批量审核预览、详情、商品图片与入库费用 | `modules/unified-orders/`、`modules/first-leg-orders/`;平台商品图片由 `modules/unified-orders/api/unifiedOrders.ts` 按客户与 fileId 下载 | `order/`、`firstleg/`、Finance;旧系统导入固定归属“IT测试公司”,前端不提交或选择客户,逐行结果展示入仓码与失败原因。图片读取归 `file/` 的 `PlatformProductImageController`。Order 状态为 `SUBMITTED` 时统一列表必须保持 `PENDING_REVIEW/APPROVE_ORDER`,不得被已存在的 Firstleg 准备中入库投影覆盖。批量审核弹框通过 `unifiedOrders.ts` 的 `/orders/first-leg/batch-review-preview` 一次取回全部预览,按服务端 `canApprove/blockReason` 展示,不逐票请求详情或用内部 ID 作标题 | +| 客户原始申报资料编辑与渠道变更 | `modules/unified-orders/components/OrderDeclarationEditorDialog.vue`、`api/unifiedOrders.ts`、`pages/UnifiedOrdersView.vue` | 使用订单版本和货物 `itemNo` 整体提交,不允许增删货物行;只编辑客户申报快照,不覆盖仓库/PDA 实测、测量审核或计费事实。渠道变化需二次确认并由 Backend `order/` 联查 `firstleg/` 的分柜、同柜、装柜与重新计价门禁;国内收货仓或中转仓变化必须重新下单 | +| 入库扣货、解除与凭证历史 | `modules/unified-orders/pages/UnifiedOrdersView.vue`、`modules/inbounds/core/api.ts`、`types.ts` | 页面只消费服务端 `holdFlag/currentHoldReason/canHold/canRelease/actionBlockReason/history`;扣货与解除提交当前入库版本、幂等键、必填原因和可选凭证,凭证下载只走 Firstleg 授权入口。Backend `firstleg/` 的 `InboundHoldService`、`InboundHoldGuard`、`InboundHoldEvidenceService`,文件归 `file/` | +| 入库、测量、称重库位状态、客户箱规策略与计价主数据 | 测量弹框/工作区 `modules/inbounds/measurement/InboundMeasurementDialog.vue`、`InboundMeasurementPanel.vue`,称重 `modules/inbounds/weight/components/WeightOcrPanel.vue`;主数据 `modules/fba-pricing/`、`modules/dimension-config/`、`modules/customers/components/CustomerBillingRulesPanel.vue`、`modules/fee-items/` | 客户固定四档箱规,默认计算方式为 `ADD/MULTIPLY`;测量行可选实际尺寸、箱规尺寸策略或客户默认尺寸系数,保存后以 Backend `firstleg/` 返回的外箱尺寸、体积重和计费基数为准,不在页面独立重算。费用确认的实际箱数、毛重和体积优先汇总当前 `actualMeasurementRows`,不得用可能滞后的工作区顶层合计覆盖明细。称重页在每张图片卡片只展示服务端 `locationCode/slotStatus/sharedOverflow`,不渲染或自行推导整票预留引导;删除图片须提示槽位仍归本票保留,不得按图片数组下标推导托盘或库位 | +| 一件代发托盘打印 | `modules/unified-orders/pages/UnifiedOrdersView.vue` 的“托盘打印”页签、`modules/inbounds/core/DropshipPalletPrintPanel.vue`、`core/api.ts` | 仅一件代发订单展示;测量审核预生成托盘码后由 Admin 电脑端打印,支持本票全部托盘合并打印和单托补打。PDA 只扫码组托,不承担 PDF 下载或打印机连接 | +| 入库单扣货、解除、凭证与历史 | `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,查看进入独立详情路由 `/loading-lists/:planId`,保留返回列表和打印;独立清单详情与货柜清单页签共同使用 `useLoadingListDetail.ts` 加载、校验和处理错误,`LoadingListDetailDrawer.vue` 统一密度、摘要、明细、历史及打印入口,打印实现归 `printLoadingList.ts`,外层只保留返回目标与货柜专属 PDA/报关区域;货柜列表经 `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/` | +| 订舱级拖车预约 | `modules/trucking/api/platformTrucking.ts` 的 `/candidates`、`components/TruckAppointmentForm.vue`、`pages/TruckAppointmentCreateView.vue`、`TruckAppointmentsView.vue` | 表单只选择服务端返回的 `RELEASED` 订舱与 `TRUCKING + truckingScope=DOMESTIC` 国内拖车供应商,不选择或提交客户、柜次、装柜计划或真实集装箱号;清关提柜固定选择 `OVERSEAS` 海外车队,类型在供应商主档单选维护;提单号和仓库由订舱冻结事实带入。一个订舱最多一张非终态拖车单,重复创建转入现有单继续编辑;PDA 完成不自动推进拖车。附件为平台作用域且不重复提单文件;Backend `TruckAppointmentService`、`truck-appointments-v1.yaml` | +| 目的港码头拖车与还空预警 | 海外清关提柜工作台/详情 `modules/container-loading/pages/OverseasContainersView.vue`、`OverseasContainerDetailView.vue`,API `modules/container-loading/api/terminalDrayages.ts`、`containerArrivalTimes.ts` | 与订舱级拖车预约是不同生命周期;按 `containerBookingId` 读取服务端状态、`allowedActions` 和历史,不能复用订舱级拖车表单或自行推进状态。列表“预计到仓”列直接提供人工日期时间选择器,保存时通过 Lastleg 柜级版本和幂等键原子更新该柜全部尾程订单,已有入库计划后禁止修改;“实际到仓”只读 `ll_receiving_container_session.receiving_at`,即海外 PDA 首次开始收货时间。还空截止、超时天/小时只展示服务端基于权威卸柜完成时间加 72 小时计算的结果;Backend `firstleg/terminaldrayage/`、`lastleg-service/`,邮件记录通过 `terminalDrayages.ts` 查询与重试。 | +| 历史报关协同与国内出仓 | `modules/customs/`、`modules/outbound-dispatch/` | `modules/customs/` 是历史报关案件/清单/代理提交与回执入口;装柜完成后的整柜退税/买单资料从货柜详情进入,不在旧模块复制;Backend `firstleg/outbound/dispatch/`、`firstleg/customs/`、`file/` | +| 主数据、下载、退避任务重试、审计 | `modules/master-data/`、`modules/downloads/pages/DownloadCenterView.vue`、`modules/audit/` | File 导出任务返回 `attemptCount/maxAttempts/nextRetryAt`;仅失败或已进入自动退避的等待任务显示重试,等待任务重试用于立即执行当前投递,不创建新任务;联查 `warehouse/`、`file/`、`audit-service/` | +| 海外接柜、入库、库存与库内服务 | `modules/overseas-warehouse/` | `gateway/`、`lastleg-service/`、`warehouse-overseas-service/`;与排车、出库、派送、异常退回和一件代发共同组成海外仓全流程 | +| 海外采购、卡车、散板 | `modules/overseas-business/` | `gateway/`、`lastleg-service/` | +| 海外仓业务合作仓与通用资料 | 合作仓 `modules/warehouses/`;报价/汇率/车辆 `modules/overseas-master-data/` | 国内 `warehouse/`、海外实际资源与服务 | +| 海外仓业务国内出仓后船节点 | `modules/port-schedules/`、`modules/ocean-bookings/`、货柜详情 | `firstleg/portschedule/`、`integration-service/`;预计节点只展示不推进实际进度,手工实际节点必须严格向后追加并防重复,十节点标准见 `../../docs/海外仓业务/船节点标准化契约.md` | +| 海外排车、出库与派送 | `modules/overseas-warehouse/AGENTS.md` | 按模块表定位排车、出库、派送页面及 API;履约边界在模块内维护 | +| 海外异常退回与客户回访 | `modules/overseas-warehouse/AGENTS.md` | 派送异常退回区别于退货库存;页面、API 与原子推进约束见模块指引 | + +全量验收按业务从 `../../docs/订单与客户/`、`入库测量与计费/`、`国内货柜履约/`、`财务管理/` 选择最小契约;海外跨模块流程或入口不明时读 `../../docs/海外仓业务/README.md`,已定位局部任务直接沿模块入口。进度、排期或验收状态问题再读取对应进度表和报告。 + +散货托盘码打印:`modules/loading-lists/components/PalletLabelPrintDialog.vue`、`modules/loading-lists/api/platformLoadingLists.ts`,由货柜详情与装柜清单详情进入;先调用 `/pallet-labels/prepare` 获取服务端按 FBA 仓分组的冻结清单,再通过 `/pallet-labels/print` 下载 PDF。托盘码使用“真实柜号-FBA 地址内部编号-三位序号”,页面同时展示 FBA 仓码和内部编号;支持全部打印或按仓指定张数补打,一件代发托盘不在该清单,前端不生成或重新编号托盘码。 + +## 海外模块内部快速索引(海外仓全流程) + +- 海外仓的尾程订单控制与柜级入仓计划转换、退货库存、排车出库、异常回访:读取 `src/modules/overseas-warehouse/AGENTS.md`,字段与流程仅在该模块维护。 + +| 关键词 | 页面/路由入口 | API 与联查 | +|---|---|---| +| 海外采购 | `modules/overseas-business/pages/PurchaseOrdersView.vue`、详情/编辑页 | `modules/overseas-business/api/platformOverseasBusiness.ts` 的 purchase 方法;Backend `lastleg-service/procurement/` | +| 美国卡车、本土散板 | `modules/overseas-business/pages/TransportOrdersView.vue`、`TransportOrderFormView.vue` | 同一 API 的 us-truck/looseboard 方法;Backend `lastleg-service/trucking/`、`looseboard/` | +| 海外报价、月度汇率、车辆 | `modules/overseas-master-data/pages/OverseasQuotesView.vue`、`ExchangeRatesView.vue`、`VehiclesView.vue` | `modules/overseas-master-data/api/overseasMasterData.ts`;Backend `lastleg-service/masterdata/` | +| 司机档案、驾驶证、到期提醒与资格预览 | `modules/overseas-master-data/pages/DriversView.vue`、`DriverDetailView.vue`;最近指引 `modules/overseas-master-data/AGENTS.md` | `api/drivers.ts`;Backend `lastleg-service/fleet/driver/`、File `FLEET_DRIVER_LICENSE` | diff --git a/src/core/navigation/adminNavigation.test.ts b/src/core/navigation/adminNavigation.test.ts index db1a7e2d..2b20c3f7 100644 --- a/src/core/navigation/adminNavigation.test.ts +++ b/src/core/navigation/adminNavigation.test.ts @@ -31,12 +31,12 @@ describe('adminNavigation', () => { { title: '货柜管理', children: ['订舱列表', '拖车列表', '货柜列表', '装柜清单', '预装清单'] }, { title: '海外尾程', children: ['尾程订单', '入库单', '废弃单', '出库单', '换标单', '派送单', '托盘管理', '库位管理', '库存管理', '退货库存管理', '退货箱号管理'] }, { title: '预约管理', children: ['预约列表'] }, - { title: '车辆管理', children: ['排车列表', '车辆列表', '司机管理', '车次管理', '拼车接货', '出车记录'] }, - { title: '海外业务', children: ['采购订单', '本土卡车', '本土散板'] }, + { title: '车辆管理', children: ['排车列表', '车辆列表', '司机管理', '出车记录'] }, + { title: '海外业务', children: ['采购订单', '本土卡车', '本土散板', '散板需求'] }, { title: '客户服务', children: ['异常订单'] }, { title: '财务管理', children: ['客户账户', '客户应收应付', '应收款统计表', '客户对账单', '结算单应收', '结算单应付'] }, { title: '海外仓', children: ['经营总览', '销量分析', '库存分析', '数据采集'] }, - { title: '通用', children: ['费用项目', '汇率管理', '仓库管理', '海外仓报价', '车队报价', '船期价格表', '下载中心'] }, + { title: '通用', children: ['费用项目', '汇率管理', '仓库管理', '海外仓报价', '车队报价', '船期价格表', '下载中心', '供应商列表'] }, { title: '系统管理', children: ['用户管理', '角色权限', '菜单权限'] }, ]) expect(visible).toHaveLength(13) @@ -196,13 +196,15 @@ describe('adminNavigation', () => { expect(getNavigationLeaves(filtered).map((item) => item.path)).toEqual(['/dashboard', '/master-data/fee-items']) }) - it('retains the supplier route permission while omitting its entry from the latest sidebar', () => { + it('shows the restored supplier entry under general only with its list permission', () => { const visible = filterAdminNavigation(adminNavigation, new Set(['platform:supplier:list'])) const missing = filterAdminNavigation(adminNavigation, new Set()) expect(getNavigationLeaves(adminNavigation).find(item => item.path === '/suppliers')?.permission).toBe('platform:supplier:list') - expect(getNavigationLeaves(visible).map((item) => item.path)).toEqual(['/dashboard']) + expect(getNavigationLeaves(visible).map((item) => item.path)).toEqual(['/dashboard', '/suppliers']) + expect(visible.find(item => item.title === '通用')?.children?.map(item => item.path)).toEqual(['/suppliers']) expect(getNavigationLeaves(missing).map((item) => item.path)).toEqual(['/dashboard']) + expect(getNavigationLeaves(filterAdminNavigation(adminNavigation, new Set(['platform:supplier:create', 'platform:supplier:edit']))).map(item => item.path)).toEqual(['/dashboard']) }) it('shows split container workbenches without a standalone smart-loading menu', () => { diff --git a/src/core/navigation/adminNavigation.ts b/src/core/navigation/adminNavigation.ts index 65b13116..0b67565c 100644 --- a/src/core/navigation/adminNavigation.ts +++ b/src/core/navigation/adminNavigation.ts @@ -134,8 +134,7 @@ export const adminNavigation: AdminNavigationNode[] = [ { key: 'fba-dispatch', title: '排车列表', icon: Truck, path: '/overseas-warehouse/dispatches', permission: 'platform:overseas-warehouse:dispatch:list' }, { key: 'vehicle-list', title: '车辆列表', icon: Truck, path: '/overseas-master-data/vehicles', permission: 'platform:overseas-master-data:vehicle:list' }, { key: 'driver-management', title: '司机管理', icon: ContactRound, path: '/overseas-master-data/drivers', permission: 'platform:overseas-master-data:driver:list' }, - { key: 'fleet-trips', title: '车次管理', icon: Truck, path: '/overseas-master-data/trips', permission: 'platform:fleet:trip:list' }, - { key: 'fleet-capacity-offers', title: '拼车接货', icon: Truck, path: '/overseas-master-data/capacity-offers', permission: 'platform:fleet:capacity-offer:list', permissionAnyOf: ['platform:fleet:capacity-offer:manage'] }, + { key: 'fleet-trips', title: '车次管理', icon: Truck, path: '/overseas-master-data/trips', permission: 'platform:fleet:trip:list', hidden: true }, { key: 'fleet-operations', title: '出车记录', icon: Truck, path: '/overseas-master-data/trip-operations', permission: 'platform:fleet:operations:list' }, ], }, @@ -147,6 +146,7 @@ export const adminNavigation: AdminNavigationNode[] = [ { key: 'overseas-purchase', title: '采购订单', icon: ClipboardList, path: '/overseas-business/purchases', permission: 'platform:overseas-business:purchase:list' }, { key: 'overseas-truck', title: '本土卡车', icon: Truck, path: '/overseas-business/trucks', permission: 'platform:overseas-business:truck:list' }, { key: 'overseas-looseboard', title: '本土散板', icon: ClipboardList, path: '/overseas-business/looseboards', permission: 'platform:overseas-business:looseboard:list' }, + { key: 'fleet-capacity-offers', title: '散板需求', icon: Truck, path: '/overseas-master-data/capacity-offers', permission: 'platform:fleet:capacity-offer:list', permissionAnyOf: ['platform:fleet:capacity-offer:manage'] }, ], }, { @@ -204,7 +204,7 @@ export const adminNavigation: AdminNavigationNode[] = [ { key: 'fleet-quotes', title: '车队报价', icon: Truck, path: '/overseas-master-data/fleet-quotes', permission: 'platform:fleet:quote:list' }, { key: 'fba-carrier-rates', title: '船期价格表', icon: CircleDollarSign, path: '/first-leg-pricing/carrier-rates', permission: 'firstleg:fba-pricing:rate:view' }, { key: 'download-center', title: '下载中心', icon: Download, path: '/downloads', permission: 'platform:download:list' }, - { key: 'supplier-list', title: '供应商列表', icon: ContactRound, path: '/suppliers', permission: 'platform:supplier:list', hidden: true }, + { key: 'supplier-list', title: '供应商列表', icon: ContactRound, path: '/suppliers', permission: 'platform:supplier:list' }, { key: 'express-companies', title: '快递公司', icon: Truck, path: '/master-data/express-companies', permission: 'platform:express:list', hidden: true }, { key: 'counterparties', title: '往来单位', icon: ContactRound, path: '/master-data/counterparties', permission: 'platform:counterparty:list', hidden: true }, { key: 'mojia-sku-sync', title: '称重机器', icon: Database, path: '/general/mojia-sku-sync', permission: 'platform:mojia-sku-sync:list', hidden: true }, diff --git a/src/modules/appointment-calendar/AppointmentCalendarDialogs.native.test.ts b/src/modules/appointment-calendar/AppointmentCalendarDialogs.native.test.ts new file mode 100644 index 00000000..753fb4cd --- /dev/null +++ b/src/modules/appointment-calendar/AppointmentCalendarDialogs.native.test.ts @@ -0,0 +1,135 @@ +import ElementPlus from 'element-plus' +import { DOMWrapper, enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import AppDialog from '@/shared/components/AppDialog.vue' +import LineClampText from '@/shared/components/LineClampText.vue' +import AppointmentCalendarView from './AppointmentCalendarView.vue' +import { getCalendarSources, getWarehouseArrivals, saveWarehouseArrival, type WarehouseArrival } from './scheduleApi' +import { listTripCalendar } from './tripCalendar' +import { createSourceAppointment, listPlanningCargo, previewPlanningBatch, resolvePlanningSource, type PlanningCargo, type PlanningBatchPreview } from './planningApi' +import { batchContext } from './batch.test-support' +import type { ScheduleEvent } from './schedule' +import { getWorkDetail, type WorkAction } from './workApi' +import { workDetail, workItem } from './work.test-support' + +const routing = vi.hoisted(() => ({ query: { date: '2026-10-19' } as Record, push: vi.fn(), replace: vi.fn(), denied: new Set() })) +vi.mock('vue-router', () => ({ useRoute: () => routing, useRouter: () => routing, onBeforeRouteLeave: vi.fn(), onBeforeRouteUpdate: vi.fn() })) +vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: (permission: string) => !routing.denied.has(permission) }) })) +vi.mock('./scheduleApi', () => ({ getCalendarSources: vi.fn(), getWarehouseArrivals: vi.fn(), saveWarehouseArrival: vi.fn() })) +vi.mock('./tripCalendar', async original => ({ ...await original(), listTripCalendar: vi.fn() })) +vi.mock('./planningApi', async original => ({ ...await original(), listPlanningCargo: vi.fn(), previewPlanningBatch: vi.fn(), resolvePlanningSource: vi.fn(), createSourceAppointment: vi.fn() })) +vi.mock('./workApi', async original => ({ ...await original(), getWorkDetail: vi.fn() })) +enableAutoUnmount(afterEach) +afterEach(() => { vi.unstubAllGlobals() }) +const warehouse: WarehouseArrival = { sourceContainerBookingId: '9007199254740999', containerNo: 'TEST-CONT', warehouseName: '美国仓', warehouseCode: 'US-W', plannedAt: null, actualAt: null, version: 3, planCount: 0, canEdit: true } +const event: ScheduleEvent = { key: 'warehouse-1', type: 'warehouse', date: '2026-10-21', time: '', title: 'TEST-CONT 计划到仓', fields: [], warehouse, canEdit: true } +const cargo: PlanningCargo = { sourceKey: 'ORDER:9007199254740998', orderId: '9007199254740998', customerId: '9007199254740997', customerName: '预约长客户名称'.repeat(12), inboundCode: 'IN-TEST', containerBookingId: 'container-1', containerNo: 'TEST-CONT', originWarehouseId: 'origin-1', originWarehouseCode: 'US-W', originWarehouseName: '美国长仓库名称'.repeat(12), destinationWarehouseCode: 'ONT8', packages: 20, receivedPackages: 0, shippedPackages: 0, sourcePalletCount: null, volumeCbm: null, estimatedArrivalAt: null, estimatedWarehouseAt: null, orderVersion: 1, standardNode: 'DEPARTED', shippingVersion: null, shippingObservedAt: null, shippingProvider: null, hasInboundPlan: false, hasActiveAppointment: false, detained: false, mixedDestinations: false } +const source = { sourceKey: cargo.sourceKey, customerId: cargo.customerId, orderId: cargo.orderId, inboundCode: cargo.inboundCode, bookingNo: null, containerNo: cargo.containerNo, originWarehouseId: cargo.originWarehouseId, originWarehouseName: cargo.originWarehouseName, destinationKey: 'FBA:LAX9', destinationWarehouseCode: 'LAX9', packages: 20, sourceVersion: 'v1', canFollowup: true, blockReasons: [] } +const preview: PlanningBatchPreview = { group: { originWarehouseId: cargo.originWarehouseId, destinationKey: source.destinationKey, packages: 20, sourceCount: 1, customerCount: 1, readinessIncomplete: true, readyFrom: null, readyTo: null, unknownQuantitySources: 0 }, originWarehouseName: cargo.originWarehouseName, destinationWarehouseCode: 'LAX9', destinationTimezone: 'America/Los_Angeles', sources: [source], missingFields: [] } +beforeEach(() => { + vi.clearAllMocks(); routing.denied.clear(); routing.query = { date: '2026-10-19' } + vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} }) + vi.mocked(getCalendarSources).mockResolvedValue({ containers: [], emptyReturns: [], emptyReturnsReadable: true }) + vi.mocked(getWarehouseArrivals).mockResolvedValue([]) + vi.mocked(listTripCalendar).mockResolvedValue({ records: [], unresolvedRecords: [] }) + vi.mocked(saveWarehouseArrival).mockResolvedValue({}) + vi.mocked(listPlanningCargo).mockResolvedValue({ records: [cargo], total: 1, page: 1, size: 20, pages: 1 }) + vi.mocked(resolvePlanningSource).mockResolvedValue({ source, coverage: { status: 'NONE', missingFields: [], changeReviewRequired: false, appointments: [] } }) + vi.mocked(previewPlanningBatch).mockResolvedValue(preview) + vi.mocked(createSourceAppointment).mockResolvedValue(batchContext().appointment) + vi.mocked(getWorkDetail).mockResolvedValue(workDetail()) +}) +async function open() { + const wrapper = mount(AppointmentCalendarView, { global: { plugins: [ElementPlus], stubs: { + AppointmentCalendarPanel: { name: 'AppointmentCalendarPanel', template: '
' }, + } } }) + await flushPromises() + return wrapper +} +async function create(wrapper: Awaited>) { + wrapper.getComponent({ name: 'AppointmentCalendarPanel' }).vm.$emit('create'); await vi.dynamicImportSettled(); await flushPromises() + const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title') === '新增FBA送仓预约')! + const content = creationBody() + await content.get('input[type="checkbox"]').setValue(true) + await content.findAll('button').find(button => button.text().startsWith('下一步'))!.trigger('click'); await flushPromises() + return dialog +} +function creationBody() { return new DOMWrapper(Array.from(document.querySelectorAll('.el-dialog')).find(item => item.textContent?.includes('新增FBA送仓预约'))!) } +it('keeps the arrival context and typed local time across native fullscreen and submits the original version', async () => { + const wrapper = await open() + wrapper.getComponent({ name: 'AppointmentCalendarPanel' }).vm.$emit('edit', event); await flushPromises() + const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title') === '调整计划到仓')! + expect(dialog.props('width')).toBe('520px') + expect(dialog.get('[aria-label="到仓业务信息"]').text()).toContain('TEST-CONT 计划到仓') + expect(dialog.text()).not.toContain(warehouse.sourceContainerBookingId) + await dialog.get('input[type="datetime-local"]').setValue('2026-10-22T09:30') + await dialog.get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises() + expect(dialog.findComponent({ name: 'ElDialog' }).props('fullscreen')).toBe(true) + expect((dialog.get('input').element as HTMLInputElement).value).toBe('2026-10-22T09:30') + await dialog.get('.el-dialog__footer .el-button--primary').trigger('click'); await flushPromises() + expect(saveWarehouseArrival).toHaveBeenCalledExactlyOnceWith(warehouse.sourceContainerBookingId, { estimatedWarehouseArrivalAt: '2026-10-22T09:30:00', expectedVersion: 3 }, expect.any(String)) +}) +it('locks the real arrival input after an unknown outcome and reuses its immutable request', async () => { + vi.mocked(saveWarehouseArrival).mockRejectedValueOnce(new Error('connection lost')) + const wrapper = await open(); wrapper.getComponent({ name: 'AppointmentCalendarPanel' }).vm.$emit('edit', event); await flushPromises() + const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title') === '调整计划到仓')! + await dialog.get('input').setValue('2026-10-22T09:30') + await dialog.get('.el-dialog__footer .el-button--primary').trigger('click'); await flushPromises() + expect(dialog.get('input').attributes('disabled')).toBeDefined() + expect(dialog.text()).toContain('上次提交结果尚未确认') + expect(dialog.get('.el-dialog__footer .el-button--primary').text()).toBe('重试确认保存结果') + await dialog.get('.el-dialog__footer .el-button--primary').trigger('click'); await flushPromises() + expect(vi.mocked(saveWarehouseArrival).mock.calls[1]).toEqual(vi.mocked(saveWarehouseArrival).mock.calls[0]) +}) +it('keeps complete long cargo names accessible in the selectable table', async () => { + const wrapper = await open() + wrapper.getComponent({ name: 'AppointmentCalendarPanel' }).vm.$emit('create'); await vi.dynamicImportSettled(); await flushPromises() + const names = wrapper.findAllComponents(LineClampText).map(item => item.props('content')) + expect(names).toContain(cargo.customerName); expect(names).toContain(cargo.originWarehouseName) + expect(wrapper.getComponent({ name: 'DataTable' }).props('maxHeight')).toBe('') + expect(wrapper.text()).not.toMatch(/900719925474099[7-9]/) + expect(resolvePlanningSource).not.toHaveBeenCalled() +}) +it('preserves real FBA fields and entered values when expanding the create dialog', async () => { + const wrapper = await open(); const dialog = await create(wrapper) + const fields = dialog.getComponent({ name: 'FbaAppointmentFields' }) + expect(fields.props('continuous')).toBe(true) + const date = fields.findAllComponents({ name: 'ElDatePicker' })[0]! + date.vm.$emit('update:modelValue', '2026-10-25'); await flushPromises() + await creationBody().get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises() + expect(dialog.findComponent({ name: 'ElDialog' }).props('fullscreen')).toBe(true) + expect(date.props('modelValue')).toBe('2026-10-25') + await creationBody().findAll('button').find(button => button.text() === '保存预约安排')!.trigger('click'); await flushPromises() + expect(createSourceAppointment).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ sources: [{ sourceKey: source.sourceKey, customerId: source.customerId, destinationKey: 'FBA:LAX9' }], details: expect.objectContaining({ plannedDate: '2026-10-25', plannedAt: null, appointmentStatus: 'PENDING' }), idempotencyKey: expect.any(String) })) + expect(creationBody().get('[aria-label="预约保存回执"]').findAll('input, textarea, select')).toHaveLength(0) + expect(creationBody().text()).toContain('AP-001') +}) +it('shows every saved server field as readonly text including confirmed offsets and long remarks', async () => { + const saved = { ...batchContext().appointment, plannedDate: '2026-10-26', plannedAt: '2026-10-26T08:00:00', plannedEndAt: '2026-10-26T18:00:00', timezone: 'America/Los_Angeles', appointmentStatus: 'SUCCESS', confirmationNo: 'CONFIRM-001', appointmentAt: '2026-11-01T01:30:00', appointmentEndAt: '2026-11-01T01:45:00', appointmentStartUtc: '2026-11-01T08:30:00Z', appointmentEndUtc: '2026-11-01T09:45:00Z', appointmentRemark: '完整预约备注'.repeat(30) } + vi.mocked(createSourceAppointment).mockResolvedValue(saved) + const wrapper = await open(); await create(wrapper) + await creationBody().findAll('button').find(button => button.text() === '保存预约安排')!.trigger('click'); await flushPromises() + const receipt = creationBody().get('[aria-label="预约保存回执"]') + for (const text of ['AP-001', saved.plannedDate, saved.plannedAt, saved.plannedEndAt, saved.timezone, '已取得对方正式确认', saved.confirmationNo, saved.appointmentAt, saved.appointmentEndAt, '-07:00', '-08:00', saved.appointmentRemark]) expect(receipt.text()).toContain(text) + expect(receipt.findAll('input, textarea, select')).toHaveLength(0) + expect(receipt.text()).not.toMatch(/item-1|customer-a|FBA_APPOINTMENT:1/) +}) +it('does not open or request creation data without its original write permission', async () => { + routing.denied.add('platform:overseas-warehouse:inbound-order:fba-appointment') + const wrapper = await open(); wrapper.getComponent({ name: 'AppointmentCalendarPanel' }).vm.$emit('create'); await flushPromises() + expect(listPlanningCargo).not.toHaveBeenCalled(); expect(createSourceAppointment).not.toHaveBeenCalled() + expect(wrapper.findAllComponents(AppDialog).some(dialog => dialog.props('title') === '新增FBA送仓预约')).toBe(false) +}) +it('uses the same real continuous form through the original work detail deep link', async () => { + routing.query.workItem = 'work-delivery-1' + const action: WorkAction = { code: 'SAVE_PLAN', label: '安排预约', targetWorkItemKey: null, target: { kind: 'FBA_APPOINTMENT', appointmentId: null, batchKey: null, sources: [{ sourceKey: source.sourceKey, customerId: source.customerId, destinationKey: source.destinationKey }] } } + vi.mocked(getWorkDetail).mockResolvedValue(workDetail({ item: workItem({ allowedActions: [action], nextAction: action }) })) + const wrapper = await open(); await vi.dynamicImportSettled(); await flushPromises() + const content = new DOMWrapper(Array.from(document.querySelectorAll('.el-dialog')).find(item => item.textContent?.includes('原业务预约办理'))!) + await content.get('.primary-next-action').trigger('click'); await flushPromises() + expect(wrapper.getComponent({ name: 'FbaAppointmentFields' }).props('continuous')).toBe(true) + expect(previewPlanningBatch).toHaveBeenCalledExactlyOnceWith(action.target.kind === 'FBA_APPOINTMENT' ? action.target.sources : []) + await content.findAll('button').find(button => button.text() === '保存预约安排')!.trigger('click'); await flushPromises() + expect(createSourceAppointment).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ sources: [{ sourceKey: source.sourceKey, customerId: source.customerId, destinationKey: source.destinationKey }], details: expect.objectContaining({ plannedAt: null, appointmentStatus: 'PENDING' }) })) + expect(content.text()).toContain('本次保存已完成') +}) diff --git a/src/modules/appointment-calendar/AppointmentCalendarPanel.test.ts b/src/modules/appointment-calendar/AppointmentCalendarPanel.test.ts index 38fc7982..3ca8267c 100644 --- a/src/modules/appointment-calendar/AppointmentCalendarPanel.test.ts +++ b/src/modules/appointment-calendar/AppointmentCalendarPanel.test.ts @@ -13,7 +13,9 @@ describe('four-type appointment calendar', () => { expect(wrapper.get('[aria-label="安排类型"]').text()).toBe('到港排车还柜计划到仓') expect(wrapper.findAll('.detail-event')).toHaveLength(4) expect(wrapper.get('[aria-label="当天安排"] header').text()).toContain('4 项安排') - expect(wrapper.findAll('.calendar-day')).toHaveLength(42) + expect(wrapper.findAll('.calendar-day')).toHaveLength(35) + expect(wrapper.find('[data-date="2026-11-01"]').exists()).toBe(true) + expect(wrapper.find('[data-date="2026-11-02"]').exists()).toBe(false) expect(wrapper.text()).not.toMatch(/新增预约|全局待办|需复核|中转|直送/) }) it('filters locally without requesting another range', async () => { @@ -24,6 +26,15 @@ describe('four-type appointment calendar', () => { expect(wrapper.text()).toContain('当天暂无安排') expect(wrapper.emitted('range')).toBeUndefined() }) + it('offers creation in the header only when authorized', async () => { + const wrapper = render() + expect(wrapper.findAll('button').some(button => button.text() === '新增预约')).toBe(false) + await wrapper.setProps({ canCreate: true }) + await wrapper.findAll('.calendar-controls button').find(button => button.text() === '新增预约')!.trigger('click') + expect(wrapper.emitted('create')).toEqual([[]]) + expect(wrapper.emitted('range')).toBeUndefined() + wrapper.unmount() + }) it('changes the selected date locally and requests one range per mode or period change', async () => { const wrapper = render() await wrapper.get('[data-date="2026-10-20"]').trigger('click') diff --git a/src/modules/appointment-calendar/AppointmentCalendarPanel.vue b/src/modules/appointment-calendar/AppointmentCalendarPanel.vue index fd4bc039..cb9cc19d 100644 --- a/src/modules/appointment-calendar/AppointmentCalendarPanel.vue +++ b/src/modules/appointment-calendar/AppointmentCalendarPanel.vue @@ -1,12 +1,13 @@ + diff --git a/src/modules/appointment-calendar/PROTOTYPE_MATRIX.md b/src/modules/appointment-calendar/PROTOTYPE_MATRIX.md index 43202134..6264dbee 100644 --- a/src/modules/appointment-calendar/PROTOTYPE_MATRIX.md +++ b/src/modules/appointment-calendar/PROTOTYPE_MATRIX.md @@ -7,22 +7,41 @@ | 页面 | 操作 | 当前确认承载与布局 | 实现结论 | |---|---|---|---| | 预约列表 | 月/周、四类选择、当前范围搜索 | 左日历右当天安排;日期格简洁标签,最多三条 | 恢复 `AppointmentCalendarPanel`,不再打开全局工作明细弹框;待人工截图确认 | +| 预约列表 | 月视图周数 | 用户截图 `codex-clipboard-372a5dc5-0c82-4298-93db-089b6c97c8ca.png`;周一至周日,只覆盖当月所需完整周 | 4/5/6周自适应;2026年10月五周,保留末周11月1日,不追加11月2—8日整周;待人工截图确认 | | 预约列表 | 查看当天、未定日期 | 固定右侧区域,完整业务字段与滚动明细 | 类型仅到港、排车、还柜、计划到仓;待人工截图确认 | -| 预约列表 | 新增、审核、删除 | 本轮明确取消通用入口 | 来源业务生成记录;不在日历另建预约或改状态 | +| 预约列表 | 新增预约 | 用户本轮要求恢复顶部按钮,随后确认先支持FBA;1080px居中弹框,先选择已有货物,再登记预约时间 | 复用真实来源预览及预约保存流程,原权限/幂等/未保存保护;保存成功显示预约编号和只读内容;普通客户仓不在本次范围;待人工截图确认 | +| 新增FBA送仓预约 | 按目的仓查询 | 用户截图 `codex-clipboard-44b338fe-af49-45e2-abf2-3bc0bd27224a.png`;在原查询栏增加可搜索、可清空的目的FBA仓下拉框 | 选项使用同货物查询权限的权威美国FBA目录;提交查询后从第一页按目的仓过滤,翻页沿用条件;待人工截图确认 | +| 预约列表 | 审核、删除 | 不恢复通用入口 | 在原业务处理,不在日历改业务状态 | | 计划到仓 | 调整 | 原居中弹框保留,小型时间表单 | 整柜原命令、原权限/版本/幂等与计划冻结门禁;待人工截图确认 | | 到港、排车、还柜 | 查看与办理 | 跳原货柜/车次正式详情页 | 保留日期/月周返回上下文,无独立工作流程导航 | | 原业务深链 | 精确来源/事项办理 | 仍使用既有居中原业务表单 | 只承接其他模块实际使用的精确入口,不恢复全局工作待办;待人工截图确认 | +2026-10-19 目的FBA仓筛选补修:原查询栏增加可搜索、可清空的下拉框,首次展开读取完整权威美国FBA目录;复用原货物读取权限,不新增仓库管理权限。查询回第一页,翻页沿用目的仓条件,重置清除条件;选项失败独立提示并可重试,不隐藏已加载货物或伪装为空结果。预约模块37文件/276项及生产构建(含类型检查)通过,Gateway路由合同5项通过。后端生产源码编译通过,本次两个测试文件单独javac编译并经Surefire执行13项通过;常规Maven流程仍被派送模块三份测试中的5项编译错误阻挡,不能据定向结果宣称后端完整门禁通过。Admin全量405文件/3388项通过、1项输入提示合同失败,原因是其他在途运输单表单的7个输入框缺少placeholder;单独复查相同失败,未修改其他模块。日志为`.logs/appointment-destination-{test,build,full-test,placeholder-check}.log`,后端为同名前缀的`route`、`backend`、`javac`及`targeted-backend`日志。本补修未提交或部署,人工1920×1080/100%视觉待确认。 + +2026-10-19 新增FBA预约入口补修:顶部按钮要求原FBA预约写权限及尾程订单列表权限,点击后才读取候选;支持同起运仓、同目的仓的跨客户货物选择,核对最新真实来源后复用原创建表单。未知结果保留原请求并阻止关闭/返回,成功后移除创建表单、显示真实预约编号和只读内容,刷新原日历;不生成虚拟车次或第五类日历。本次已明确普通客户仓暂不加入。 + +验证:新增入口直接5文件/59项通过;`pnpm build`含类型检查通过,仅既有AnalyticsChart大分块提示。工作区首次全量为405文件/3385项通过、1项排车详情布局合同失败,后续该项单独6项复查通过;再次全量出现9文件/11项失败(以5秒超时为主),在不修改超时阈值和其他业务代码的情况下,以2个工作进程复查整个预约模块、密度及上述全部失败文件,47文件/385项全过。不将这些分次结果记录为单次全量绿灯。证据为`.logs/appointment-create-{focused-final,build,full-test,full-test-final,layout-recheck,bounded-recheck}.log`。额外临时候选核对未作为通过证据;工作区其他业务在途修改保留。本次未提交、发布或新增真实业务记录,人工1920×1080/100%视觉待确认。 + +2026-10-19 月视图周数补修:按月实际周数生成日期格,并同步查询开区间;覆盖2026年10月五周、跨年五周、完整二月四周与确需六周的八月。Admin全量405文件/3375项、类型检查及生产构建通过,`git diff --check`通过;仅既有AnalyticsChart大分块提示。本补修尚未提交或发布,人工视觉待确认;证据为`.logs/appointment-calendar-month-weeks-{test,build}.log`。 + 2026-10-19 发布候选检查:Admin全量405文件/3372项通过,定向输入提示、弹框密度及取货组件4文件/35项通过;`pnpm build`(含类型检查)通过,仅既有AnalyticsChart大分块提示。新增日历后端直接测试及受影响SQL、API、权限合同通过,Gateway路由/部署拓扑/回滚合同与Mapper注册合同通过。 发布检查中发现的既有问题已在本候选最小修正:调拨新增箱数和取货输入提示、取货弹框密度、尾程贴标文件页签结构与对应断言;取货测试Bean及API数量断言;国内空库SQL中误放的五张海外取货表归回海外空库基线,并同步现行车次用途字段和基线表数。这些仅修改源码基线,不对运行环境执行SQL。 本次发布单元为Admin、Firstleg、Lastleg;新增两个只读日历接口复用现有Gateway通配路由,日历功能不新增数据库结构。环境发布与真实HTTP结果只维护在`docs/测试环境单机部署与配置契约.md`。人工视觉待用户在1920×1080、100%缩放下截图确认。 +2026-10-16 本次 UI 检查:用户重新要求检查后,运行 Admin 全量测试,397 文件 / 3292 项通过;发现并修正 QueryToolbar 必填 `ariaLabel` 属性的模板写法。补充真实 KeepAlive + Teleport 离开/恢复及点击提醒弹层回归后,最新定向检查 3 文件 / 66 项通过,最终 `pnpm typecheck`、`pnpm build` 和 `git diff --check` 通过。构建仍有既有 AnalyticsChart 约 588 kB 分块提示。此记录更新下文历史“未运行验证”的结论;本地自动检查不代表测试环境或人工视觉验收。所属客户显示缺项仍待后端合同补齐,INP 采集报错的脚本来源未确认,本次未部署。日志为 `.logs/appointment-ui-current-{test,focused,typecheck,build}.log`。 + 本轮依据:`C:/Users/BY-OY/.codex/visualizations/2026/10/14/01a13a8d-e968-7962-906b-1a514792d921/appointment-calendar-guided.html`(V13,SHA-256 `61902e410be755aa9cbf9fcfaa9d393bfa49dd8d592cd8773af5396e78c29836`)及 `docs/海外仓业务/预约工作日历V13变更说明.md`、开发协作契约。保留既有日历和居中弹框优化,不改变原业务实际执行入口。 本次密度调整依据:用户提供的主页面截图 `codex-clipboard-93984795-68cb-4082-96b3-c83189d8a357.png`、展开明细截图 `codex-clipboard-a98edd3b-82af-4f1b-989e-7d0f028e0965.png`,以及用户确认的压缩留白、日期定位、控件统一、零值弱化和统计范围说明。 +2026-10-16 操作列样式核对:用户截图 `codex-clipboard-9d54345c-c9c1-4991-82ba-b319948f781b.png` 明确要求与外层表格统一。 + +| 页面 | 操作 | 截图依据与布局 | 当前实现 | 本轮结论 | +|---|---|---|---|---| +| 预约工作明细表格 | 查看详情或服务端提供的下一步操作 | 操作列内蓝色普通文字,无边框、底色或装饰图标 | `RowActions` 默认描边样式 | 使用公共 `text-actions` 模式,保留当前文案、权限、点击承载和长文案换行;待人工截图确认 | + 2026-10-16 当前承载调整:用户反馈“点击外面关闭不了,也看不清”。日期/类型/全局明细、事项详情、原侧边编辑表单和到港参考统一改用带遮罩的居中 `AppDialog`,常规宽度 1120px、高度 88dvh,支持全屏;标题与分页固定、仅正文滚动。遮罩、Esc 和关闭按钮统一经过未保存/提交结果待确认保护。此结论优先于下表早期侧边承载描述;原业务独立页面跳转、确认流程和统计口径保持原样。改后待人工截图确认。 V13 日历布局:按已确认定高要求,月日格固定 146px,周日格固定 270px;只展示五类服务端计数和独立到港参考,不追加逐事项卡片、不依明细条数扩高。替代下表早期“随内容自然增高”的结论;不裁掉五类入口,仍需人工核对 1920×1080、100% 下完整内容。 @@ -60,6 +79,7 @@ V13 日历布局:按已确认定高要求,月日格固定 146px,周日格 | V13 场景 | 当前前端与直接检查 | 依赖/剩余验收 | |---|---|---| +| 事项列表统一表格 | 用户截图 `codex-clipboard-1bde1991-b226-4956-8530-6de44074f818.png` 明确要求统一表格或卡片,不能混排 | 所有日期/类型/全局待办列表统一使用 DataTable 与 RowActions,替代早期多行卡片排版。操作固定最左;事项、类型、状态、作业时间、时区、流程、地点、负责人、下次跟进、提醒分列,时间保留实际/确认/目标口径。列表弹框加宽,表头与分页由共享表格承载;保留每页 8 项、筛选和返回上下文。当前调整覆盖旧卡片与独立分页结论;未运行验证或部署,待人工截图确认 | | 01 整客户 160 移出续办 | `WorkDetailPanel.test.ts` 保存后读取主要下一步;`WorkExistingDispatchForm.test.ts` 沿用真实排车单继续安排本批车辆 | 单客户移出命令、当前数量/车辆版本、原车 120 保留结果及后续目标;真实发车签收待集成 | | 02 100 先送、60 后送 | `AppointmentBatchForm.test.ts` 保留普通分批与来源版本;`WorkDetailPanel.test.ts` 区分未定作业日和跟进日 | 已确认预约/装载关系的部分移出与调整回复语义,不能以普通分批代替 | | 03 后送 60 加另一客户 40 共车 | 本批与另一客户按实际托盘各建排车单;结果页“与同仓其他排车单共车”沿业务号核对两单后复用 TripPreparationDialog/Fleet prepare,可加入已有计划车次;WorkDispatchForm、WorkSharedTripForm、TripPreparationDialog、trips API 测试覆盖传递与导航 | 实物须能对应本次数量,不能对应时先走原库存拆托;共车后的路线、容量、审核及发车须真实 HTTP 验收,非任意箱级拆托开发缺口 | @@ -161,8 +181,12 @@ V13 日历布局:按已确认定高要求,月日格固定 146px,周日格 | 本次操作 | 证据与现状 | 调整结论 | |---|---|---| +| 未定日期范围内搜索 | 用户截图 `codex-clipboard-07b5ab03-e467-4ce1-8410-13d05ac5bc51.png` 反馈大量数据缺少搜索,且测试环境仍显示旧卡片 | 全局待办弹框接入 QueryToolbar,支持柜号、批次号、车次号、地点关键词;沿现有 items API 同时传 view(含 UNDATED)与 keyword,服务端全范围筛选后分页,不过滤当前 8 条。搜索/清空回第 1 页,返回明细保留搜索词和分页,原业务跳转的 return URL 保留 workListKeyword。保留全局统计口径;表格与搜索均为当前工作区改动,本任务未部署,未运行验证 | +| 所属客户缺失排查 | 用户截图 `codex-clipboard-645f5354-0dfd-4b90-b6f7-05ff9b1f2ffb.png`;`BusinessReference` 只有 kind/key/businessNo/label,客户引用存在 `CUSTOMER + 内部客户键 + null + 所属客户` 的生产路径,详情查询仅合并引用 | 前端按 `businessNo || '—'` 展示,缺少真实客户名称;不能据此认定没有关联客户,也不能回退内部客户键。待工作事项后端按权威客户目录补充名称/业务编号并明确展示合同;遵循开发协作契约,本任务不保留后端改动。未完成接口或环境验收 | +| 提醒列与跳转遮罩 | 用户截图 `codex-clipboard-54e0b56d-448c-40da-a147-15798c932091.png` 和按钮反馈 | 提醒列固定 105px,仅显示去重后的条数,点击展开完整可滚动提醒,不撑高表格行。预约页被 KeepAlive 停用时卸载挂到 body 的弹框,恢复时保留父级事项/分页上下文。用户 startTime 截图实际是 INP 采集的空 entries 读取,脚本来源尚待确认,不能将该报错认定为已修复;本轮未运行验证或部署 | +| 事项详情的信息层级 | 用户 2026-10-16 截图 `codex-clipboard-cad4abc6-51d1-42f2-8e7b-7914b264348e.png` 反馈排版不清楚、缺少重点 | 顶部突出事项名称与状态;作业时间、时区、地点、负责人和下次跟进复用共享描述表;原有提醒去重后集中展示,保留完成态与跟进日期语义;当前操作与关联资料/历史分区,保留所有动作资格、原业务跳转、默认历史展开条件和未保存保护。空原业务进度不渲染空标题;已知事件代码使用中文名称,未知代码保留在辅助信息中,时间保留原时区。本轮未运行验证或部署,待人工截图确认 | | 列表信息层级与日历视觉细化 | 用户 2026-10-16 截图 `codex-clipboard-6858cde3-eda3-4a02-a043-e1a47143f67c.png`、`codex-clipboard-5a68965d-cdbd-4ce9-b8b3-385d9d76b4f2.png`,并明确要求按建议优化 | 查看详情/历史使用普通描边按钮,办理动作保留主色且仍在左侧;完整标题、状态、作业时间、时区、地点、流程、负责人、跟进日期及异常分层显示。日历类型改浅色底与弱边框,保留语义色、今天/选中态;周末表头与到港参考保持辅助层级,零值弱化、超期数字提示。保留现有覆盖面板、字号、统计和动作链;改后待人工截图确认 | -| 工作明细左侧长操作按钮 | Admin #621 真实 1920×1080 截图 `baiyue-firstleg/reports/appointment-browser-20261015/admin-621-follow-today-1920.png`;按钮与文本实际行高仅 13px | 保留完整文案和 108px 操作列,统一使用 20px 行高、自然换行和自适应高度;普通/展开面板及主操作/查看详情使用同一规则 | +| 工作明细左侧长操作按钮 | 用户 2026-10-16 截图 `codex-clipboard-b36b905a-a223-4fa6-98ca-7ece08149a37.png` 反馈按钮过大;原 108px 列宽、全宽实心按钮导致长文案换行撑高 | 操作列调整为 184px,按钮按文案宽度使用共享 `density-action` 描边样式和 28px 最小高度;完整保留服务端动作标签及点击行为,超长文案仍可换行。业务编号使用深色标题,辅助信息使用说明字号,行间距收紧。适用全部工作明细弹框及全屏态,覆盖早期实心主按钮结论;改后待人工截图确认,本轮未运行验证或部署 | | 流程与工具栏分组 | 用户后续流程下拉、日期工具栏及上下操作区截图(`115df974`、`d217350b`、`80dd9fa1`) | 流程改为四个直接切换按钮;全局待办、流程、日期与类型筛选收进连续白底工作区,各行靠左,保留全局与区间统计边界 | | 日历浏览与日期选择 | 主页面截图;原型 `renderCalendar()`、`.day.selected`、`.todaymark` | 缩小日格空白,今天与选择态分别标识;零项日期只显示日期;显示实际查询起止日期,保留加载失败状态 | | 日期/类型/全局明细 | 展开截图中日历被挤窄;原型第 11 行 `.workspace`、`.panel` 为右侧覆盖布局 | 恢复覆盖式侧边工作面板,不压缩日历;增加展开/收起,标题和关闭固定,正文滚动,分页固定;仍保留左侧业务操作 | @@ -186,10 +210,8 @@ V13 日历布局:按已确认定高要求,月日格固定 146px,周日格 | 客户仓批次用车 | 原排车与车次安排入口 | 按 DISPATCH 引用读取原排车单,再进入原车次安排 | `WorkExistingDispatchForm.vue`;核对不可变批次键与真实业务号,保留原实物;不调用 FBA 候选,不新建排车单;原派送进度按明确业务引用进入 | | 本柜接仓计划与记录 | `executeForm()`、原入库单详情 | 单计划进入对应客户原单,多计划列出本柜实际关联清单;缺计划时先预览整柜分组 | `WorkReceiptPlans.vue`;复用 container-summaries、原整柜预览/生成 API 和原入库单路由,快照与原资源重试键保持一致;实际收货仍由原仓库作业流程登记 | | 失败、无权限、未保存 | 真实接口与应用验收规则 | 明确失败和重试;错误不改为0项,保留输入和原上下文 | 各组件直接回归;未知命令不执行,未确认保存结果禁止离开 | - | 保存后工作清单同步等待 | 用户截图 `codex-clipboard-40520e09-5cbf-489f-9941-325bdf22bdea.png`,顶部保存成功、正文续办 A0409 | 保留当前事项弹框和保存结果,不改变承载方式 | `useWorkSyncRead.ts` 仅对当前完整工作清单同步守卫重读 GET;间隔2秒,累计等待最多30秒,超限显示原错误;日历、汇总、明细与批次续办一致,不重复写入 | | 查看原排车单、继续安排本批车辆 | 用户截图 `codex-clipboard-4791b73f-7a9f-4be5-910a-38911e135ef4.png`、`codex-clipboard-e5b41c4b-a00c-4b99-ac06-cd23ec910aa9.png` | 原排车单进入正式路由,事项弹框随缓存页停用而移除;继续配车沿目标事项读取资格 | 保留已有 `pageActive/onDeactivated` 修复;跨事项 `nextAction` 仅导航,目标事项重新核对 `allowedActions`,不继承送仓资格或执行旧动作。修复后待人工截图确认 | - | 刷新后沿用已有FBA排车单 | 当前对话指定沿用原DS;`WorkExistingDispatchForm.vue` 的既有原排车列表和 `TripPreparationDialog` | 保留事项弹框,显示本批原排车单;继续用原安排车次弹框,不重复选托盘或创建 | `WorkDispatchForm.vue` 先读当前批次 `receiving.transport`,再按原排车单 `appointmentBatches` 校验批次/客户;已绑定车次直接查看,关系读取失败不显示创建入口;剩余实物仅显式选择后读取。待人工截图确认 | 视觉目标:1920×1080、100%缩放,待人工截图确认。单元测试与类型检查单独报告,不代表真实接口或页面验收通过。原业务跳转缺少的业务号/客户范围在协作交接中列明,不由前端补造;工作 Slot 与原预约编辑均保留完整跨日时间。原业务页面返回入口只接受预约工作页,并保留原事项、前置事项、筛选及页码。 diff --git a/src/modules/appointment-calendar/TripAppointmentPanel.test.ts b/src/modules/appointment-calendar/TripAppointmentPanel.test.ts index 9f5298cb..f4e8b8b9 100644 --- a/src/modules/appointment-calendar/TripAppointmentPanel.test.ts +++ b/src/modules/appointment-calendar/TripAppointmentPanel.test.ts @@ -10,7 +10,7 @@ vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ vi.mock('@/modules/overseas-warehouse', async original => ({ ...await original(), getDispatch: vi.fn(), getFbaAppointment: vi.fn() })) vi.mock('./batchApi', async original => ({ ...await original(), getAppointmentBatch: vi.fn() })) vi.mock('./receivingApi', async original => ({ ...await original(), getReceivingContext: vi.fn() })) -const reference = (batchKey = 'FBA_APPOINTMENT:1', batchNo: string | null = 'AP-001'): DispatchBatchReference => ({ batchKey, batchNo, customerId: 'customer-a', sources: [{ sourceKey: 'ORDER:1', destinationKey: 'FBA:ONT8', packageCount: 70 }] }) +const reference = (batchKey = 'FBA_APPOINTMENT:1', batchNo: string | null = 'AP-001'): DispatchBatchReference => ({ batchKey, batchNo, recordVersion: 0, customerId: 'customer-a', sources: [{ sourceKey: 'ORDER:1', destinationKey: 'FBA:ONT8', packageCount: 70 }] }) function render(overrides: Partial['$props']> = {}) { return mount(TripAppointmentPanel, { props: { dispatches: [{ dispatchNo: 'DS-A', customerId: 'customer-a', packageCount: 70, appointmentBatches: [reference()] }], previousDispatches: [], historyUnavailableReason: null, canWrite: true, ...overrides }, global: { stubs: { ElButton: { props: ['disabled'], template: '' }, AppDialog: { props: ['modelValue'], template: '
' }, diff --git a/src/modules/appointment-calendar/WorkCalendarPanel.test.ts b/src/modules/appointment-calendar/WorkCalendarPanel.test.ts index 1ecf1057..1bdb83c2 100644 --- a/src/modules/appointment-calendar/WorkCalendarPanel.test.ts +++ b/src/modules/appointment-calendar/WorkCalendarPanel.test.ts @@ -57,7 +57,7 @@ it('shows the actual cross-month interval and hides empty-day zeros while keepin }) it('renders server type counts instead of individual event cards and opens exact date/type details', async () => { const wrapper = render() - expect(wrapper.findAll('.work-day')).toHaveLength(42) + expect(wrapper.findAll('.work-day')).toHaveLength(35) const day = wrapper.get('[data-date="2026-10-15"]') expect(day.get('.day-heading').text()).toContain('2 项') await day.get('[data-type="DELIVERY"]').trigger('click') diff --git a/src/modules/appointment-calendar/WorkCreateAppointmentForm.vue b/src/modules/appointment-calendar/WorkCreateAppointmentForm.vue index beef2652..249802e7 100644 --- a/src/modules/appointment-calendar/WorkCreateAppointmentForm.vue +++ b/src/modules/appointment-calendar/WorkCreateAppointmentForm.vue @@ -2,7 +2,7 @@ import { computed, onMounted, reactive, ref } from 'vue' import { ElMessage } from 'element-plus' import { useAccessPreviewStore } from '@/core/access/accessPreview' -import { FbaAppointmentFields, createAppointmentForm, appointmentFormError, appointmentPayload, overseasWarehousePermissions } from '@/modules/overseas-warehouse' +import { FbaAppointmentFields, createAppointmentForm, appointmentFormError, appointmentPayload, overseasWarehousePermissions, type FbaAppointmentDto } from '@/modules/overseas-warehouse' import LoadErrorState from '@/shared/components/LoadErrorState.vue' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' @@ -11,7 +11,7 @@ import type { SourceReference } from './workApi' import { rejectedCommand, useWorkDraft } from './useWorkDraft' import { workReason } from './workPresentation' const props = defineProps<{ sources: SourceReference[]; canWrite: boolean }>() -const emit = defineEmits<{ saved: [] }>() +const emit = defineEmits<{ saved: [appointment: FbaAppointmentDto] }>() const access = useAccessPreviewStore(), preview = ref(null) const form = reactive(createAppointmentForm()), baseline = ref(JSON.stringify(form)) const loading = ref(false), saving = ref(false), error = ref(null), saveError = ref(null) @@ -35,7 +35,7 @@ async function save() { try { const result = await createSourceAppointment(pending.value) pending.value = null; baseline.value = JSON.stringify(form) - ElMessage.success(`预约安排 ${result.appointmentNo} 已保存`); emit('saved') + ElMessage.success(`预约安排 ${result.appointmentNo} 已保存`); emit('saved', result) } catch (cause) { saveError.value = getApiErrorDetails(cause, '创建结果待确认,请重试本次请求') if (rejectedCommand(cause)) { pending.value = null; await load() } @@ -44,18 +44,23 @@ async function save() { onMounted(load) diff --git a/src/modules/appointment-calendar/WorkDetailPanel.vue b/src/modules/appointment-calendar/WorkDetailPanel.vue index 0d752e97..ee77b2db 100644 --- a/src/modules/appointment-calendar/WorkDetailPanel.vue +++ b/src/modules/appointment-calendar/WorkDetailPanel.vue @@ -21,6 +21,14 @@ import { useWorkSyncRead } from './useWorkSyncRead' const props = defineProps<{ itemKey: string; initialAction?: WorkAction | null; canLeave: LeaveGuard }>() const emit = defineEmits<{ open: [key: string, action?: WorkAction]; external: [path: string]; saved: [result?: WorkCommandResult, command?: WorkCommand['command']]; updated: []; container: [id: string, label: string] }>() const detail = ref(null), loading = ref(false), error = ref(null), action = ref(null) +const attentionReasons = computed(() => [...new Set([ + ...(detail.value?.item.blockReasons || []), + ...(detail.value?.item.missingFields || []), + ...(detail.value?.item.overdueReasons || []), +].map(workReason))]) +const needsFollowup = computed(() => !!detail.value && detail.value.item.executionStatus !== 'COMPLETED' && (!detail.value.item.owner || !detail.value.item.followupDate)) +const historyLabels: Record = { BASELINE_OBSERVED: '同步原业务状态', GENERATED: '创建工作事项', CONFIRM_ROUTE: '确认路线', SAVE_FOLLOWUP: '记录跟进', RESOLVE_REVIEW: '处理复核', SAVE_REQUIREMENT: '保存预约要求', HANDOFF_RETURN: '交接还空责任', ACCEPT_RETURN: '确认接手还空' } +function historyTime(value: string) { return value.replace('T', ' ').replace(/\.\d+(?=Z|[+-]\d{2}:\d{2}$)/, '').replace(/Z$/, ' UTC') } const receivingOwners = computed(() => [detail.value?.item.owner, ...(detail.value?.relatedItems.map(item => item.owner) || [])].filter((person): person is { id: string; name: string } => !!person?.id)) const actionNotice = ref('') const saveCompleted = ref(false) @@ -105,17 +113,33 @@ onMounted(async () => { onBeforeUnmount(() => { ++request; controller?.abort() })