319 lines
15 KiB
Markdown
319 lines
15 KiB
Markdown
# 运输计划归档 — 设计文档
|
||
|
||
> **日期**: 2026-06-25
|
||
> **状态**: 待审阅
|
||
> **前置**: `docs/运输计划管理-需求说明文档.md`、`docs/superpowers/specs/2026-06-21-pl-arrange-transport-refine-design.md`、`openspec/changes/transport-plan-management/specs/transport-plan/spec.md`
|
||
> **界面模式**: P06 复杂业务列表 + 弹窗表单;归档弹窗上区编辑、下区只读对照(对齐 `PlPlanArrangeDialog`)
|
||
|
||
## 目标
|
||
|
||
在【运输管理】下新增菜单【运输计划归档】,提供与【运输计划】相同结构的列表与检索;操作列仅 **查看 / 编辑 / 归档**。归档通过专用弹窗一次性完成:**填写产品实际数量 → 调整运费 → 执行归档**。原【运输计划】页移除全部归档入口,归档职责集中到本菜单。
|
||
|
||
---
|
||
|
||
## 已确认决策(2026-06-25)
|
||
|
||
| # | 决策 | 说明 |
|
||
|---|------|------|
|
||
| D1 | **列表展示全部计划** | 默认不过滤 `archiveStatus`;已归档计划仍保留在列表中作为历史可查 |
|
||
| D2 | **已归档也可调整** | 已归档行仍显示「查看 / 编辑 / 调整」;「归档」按钮在已归档时文案改为「调整」,打开同一弹窗修改 actualQty 与运费 |
|
||
| D2a | **归档页编辑不受归档态限制** | 本菜单内 `PlPlanFormDialog` 对已归档计划可打开;后端 `update_plan` 移除「已归档不可修改」阻断(运输计划页仍通过 UI 隐藏已归档行的编辑入口) |
|
||
| D3 | **原页移除归档入口** | `views/pl/plan/index.vue` 去掉「批量归档」;`views/pl/plan/detail.vue` 去掉「归档」按钮;归档只在新区操作 |
|
||
| D4 | **弹窗布局对齐安排运输** | 上区:归档表单;下区:完整 `PlPlanViewTabs`(计划概况 / 运输信息含司机·现场链接 / 合同信息) |
|
||
| D5 | **归档 API 增强** | 单次请求原子完成:写 `actual_qty` + 同步入库/出库数与金额 + 更新运费 + 改归档/完成状态 |
|
||
| D6 | **列表/筛选与运输计划一致** | 列、筛选字段、分页行为与 `/#/pl/plan` 相同;可额外增加「归档状态」筛选项(展开区) |
|
||
|
||
---
|
||
|
||
## 非目标(本期不做)
|
||
|
||
- 跟进状态 12 态完整流转(仍用现有 `draft/processing/completed/cancelled` 简化枚举;归档时将 `follow_status` 置为 `completed`)
|
||
- 归档后允许反归档 / 撤销
|
||
- 运费结算单已生成后的运费修订流程(见约束:有关联结算且非草稿则阻断改运费)
|
||
- OpenSpec 独立 change 归档(可在实现后补 `openspec/changes/` 条目)
|
||
|
||
---
|
||
|
||
## 一、页面与路由
|
||
|
||
### 1.1 新菜单
|
||
|
||
| 项 | 值 |
|
||
|----|-----|
|
||
| 菜单名 | 运输计划归档 |
|
||
| 路由 path | `plan-archive` |
|
||
| Hash | `/#/pl/plan-archive` |
|
||
| 组件 | `web-admin/src/views/pl/plan-archive/index.vue` |
|
||
| 列表标题 | `运输计划归档`(写入 `App.vue` → `panelTitleBarTextRules`) |
|
||
| menuCode | `pl:plan-archive:list` |
|
||
| 排序 | 紧接「运输计划」之后,`order_num=10.5` 或 `11`(危化品证上传顺延) |
|
||
|
||
### 1.2 权限按钮
|
||
|
||
| 权限码 | 用途 |
|
||
|--------|------|
|
||
| `pl:plan-archive:list` | 菜单 + 列表 |
|
||
| `pl:plan-archive:query` | 查看弹窗 |
|
||
| `pl:plan-archive:edit` | 编辑弹窗(含已归档) |
|
||
| `pl:plan-archive:archive` | 打开归档/调整弹窗 + 提交(含已归档调整) |
|
||
|
||
编辑可复用现有 `PlPlanFormDialog` + `pl:plan:edit` 或映射到 `pl:plan-archive:edit`(实现时二选一,推荐归档页独立权限,后端 update 仍走 `/transport/plan/{id}`)。
|
||
|
||
### 1.3 列表行为(D1 / D2)
|
||
|
||
- **数据范围**:调用同一 `planApi.list`,默认不传 `archiveStatus`(全部)。
|
||
- **筛选**:与运输计划相同 6 项;**展开区增加「归档状态」**(未归档 / 已归档 / 全部)。
|
||
- **表格列**:与 `views/pl/plan/index.vue` 一致(计划编号、日期、类型、客户、产品、证书、跟进/运输状态、金额、车牌、司机、跟进人等)。
|
||
- **操作列** `min-width` 约 260(3 个 link 按钮),`fixed="right"`。
|
||
|
||
| 条件 | 查看 | 编辑 | 归档 / 调整 |
|
||
|------|------|------|-------------|
|
||
| `followStatus !== 'cancelled'` | ✅ | ✅ | ✅(未归档文案「归档」,已归档文案「调整」) |
|
||
| `followStatus === 'cancelled'` | ✅ | ❌ | ❌ |
|
||
|
||
### 1.4 工具栏
|
||
|
||
| 按钮 | 规则 |
|
||
|------|------|
|
||
| 已选 N 项 | 保留 |
|
||
| 批量归档 | 保留;仅对选中行中未归档且未作废的执行;已归档跳过并汇总提示 |
|
||
| 新增 / 删除 / 证书 / 安排 | **不出现** |
|
||
|
||
### 1.5 原运输计划页改动(D3)
|
||
|
||
| 文件 | 改动 |
|
||
|------|------|
|
||
| `views/pl/plan/index.vue` | 删除「批量归档」按钮及 `handleBatchArchive`;删除 `FolderChecked` 导入(若无他用) |
|
||
| `views/pl/plan/detail.vue` | 删除「归档」按钮及 `handleArchive` |
|
||
|
||
---
|
||
|
||
## 二、归档弹窗(`PlPlanArchiveDialog`)
|
||
|
||
### 2.1 布局
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ 运输计划归档 · {planNo} [×] │
|
||
├──────────────────────────────────────────────────────────────┤
|
||
│ ▲ 上区:归档操作(max-height ~42vh,可滚动) │
|
||
│ [产品实际数量] PlPlanFormProductTable │
|
||
│ showActualQty + editableQty(仅 actualQty) │
|
||
│ [运费信息] 复用安排运输区块(只读 hasFreight 来自计划)│
|
||
│ [归档说明] 可选 remark(写入 plan.remark 或仅展示) │
|
||
├──────────────────────────────────────────────────────────────┤
|
||
│ ▼ 下区:计划单查看(flex:1,min-height 280px) │
|
||
│ PlPlanViewTabs embedded — 三 Tab 与查看/安排弹窗一致 │
|
||
└──────────────────────────────────────────────────────────────┘
|
||
│ 取消 │ 确认归档 / 确认保存 │
|
||
```
|
||
|
||
**模式切换**:
|
||
|
||
| 模式 | 标题后缀 | 主按钮 | 提交语义 |
|
||
|------|----------|--------|----------|
|
||
| 首次归档 | `运输计划归档 · {planNo}` | 确认归档 | 写数量/运费 + 置 `archived` |
|
||
| 已归档调整 | `调整归档内容 · {planNo}` | 确认保存 | 仅写数量/运费,**不改变** `archive_status` / `complete_status` |
|
||
|
||
**尺寸与规范**:`dialog-form-layout`、`align-center`、`width: min(1680px, calc(100vw - 80px))`、`modal-class: order-module-dialog-mask`、`backdrop-filter: none`、表单 `label-position="top"`;底栏 Close / Check 图标间距 6px。
|
||
|
||
### 2.2 上区 — 产品实际数量
|
||
|
||
- 使用 `PlPlanFormProductTable`:`show-actual-qty`、`editable-qty`;**计划数量列只读**(不传 editable 到 quantity 或单独 prop `editableActualQtyOnly`)。
|
||
- 初始值:`actualQty ?? quantity ?? inboundEstimateQty ?? outboundEstimateQty ?? 0`(与 `usePlPlanViewDetail` 一致)。
|
||
- 校验:每行 `actualQty >= 0` 必填;至少一行有有效数量。
|
||
- 提交后后端按 `plan_type` 同步:
|
||
- `inbound`:`inbound_qty = actual_qty`,`inbound_amount = inbound_qty × inbound_price`(价缺省用 `tax_price`)
|
||
- `outbound`:`outbound_qty = actual_qty`,`outbound_amount = outbound_qty × outbound_price`
|
||
- `direct`:仅写 `actual_qty`,重算 `tax_subtotal` 若业务需要(按 quantity 单价比例,或保持计划金额不变 — **本期以 actual_qty 落库为主,金额字段按 inbound/outbound 规则**)
|
||
|
||
### 2.3 上区 — 运费信息
|
||
|
||
复用 `PlPlanArrangeDialog` 运费区块 + `usePlFreightCalc`:
|
||
|
||
| 字段 | 规则 |
|
||
|------|------|
|
||
| `hasFreight` | 来自计划,只读 |
|
||
| `distanceKm` | 可编辑;预填 transport / plan |
|
||
| `freightType` | `by_weight` / `by_trip` |
|
||
| 运输量 | `by_weight` 时默认 **actualQty 汇总**(`sumProductActualQuantities`) |
|
||
| `freightPrice` / `freightAmount` | 可编辑;联动计算可手动覆盖 |
|
||
| `hasInvoice` | 可编辑 |
|
||
|
||
`hasFreight=0` 时显示「本计划无运输费用」,跳过运费写入。
|
||
|
||
### 2.4 下区 — 只读对照
|
||
|
||
- 完整嵌入 `PlPlanViewTabs`(`embedded`),含:
|
||
- 计划概况(含产品 actualQty 展示列)
|
||
- 运输信息(车辆、物流、运费、**司机链接复制**、**现场链接复制**、回传、地磅)
|
||
- 合同信息
|
||
- 提交成功后 `tabsRef.reload()` 刷新下区。
|
||
|
||
### 2.5 入口与禁用
|
||
|
||
- `followStatus !== 'cancelled'` 均可打开(**含已归档**)。
|
||
- 打开时 `planApi.get` + 若已安排则读 transport 运费预填;已归档时进入「调整」模式。
|
||
|
||
---
|
||
|
||
## 三、后端 API
|
||
|
||
### 3.1 增强归档接口
|
||
|
||
**方案**:扩展 `POST /transport/plan/{id}/archive`,请求体可选;无 body 时保持向后兼容(仅改状态,供脚本/旧调用),**前端归档弹窗必须带 body**。
|
||
|
||
**Request `PlanArchiveBody`**:
|
||
|
||
```json
|
||
{
|
||
"products": [
|
||
{ "id": "计划产品明细ID", "actualQty": 4.7 }
|
||
],
|
||
"freight": {
|
||
"distanceKm": 120.5,
|
||
"freightType": "by_weight",
|
||
"freightPrice": 50,
|
||
"freightAmount": 235,
|
||
"hasInvoice": 1
|
||
}
|
||
}
|
||
```
|
||
|
||
**处理顺序(单事务)**:
|
||
|
||
1. 校验:计划存在、租户、`follow_status != cancelled`
|
||
2. 校验:每个 `products[].id` 属于该计划
|
||
3. 更新 `pl_plan_product.actual_qty`;按 `plan_type` 写 `inbound_qty/outbound_qty` 及金额
|
||
4. 若 `plan.has_freight == 1` 且 body.freight 存在:
|
||
- 若存在 `plan.transport_id` → 更新 `pl_transport` 运费字段 + `transport_qty`
|
||
- 更新 `plan.freight_amount`
|
||
- **若运输单已纳入非草稿运费结算** → 400 阻断(查 `pl_freight_settlement` 关联)
|
||
5. **状态分支**:
|
||
- 若 `archive_status != archived`:置 `archive_status=archived`、`complete_status=yes`、`follow_status=completed`
|
||
- 若 **已归档**:跳过状态字段,仅更新业务数据(D2)
|
||
6. 写 `pl_plan_change`:`change_type=archive`(首次)或 `archive_adjust`(已归档调整)
|
||
7. commit
|
||
|
||
**`update_plan`(编辑弹窗)**:移除 `archive_status == archived` 时的 400 阻断;已归档计划允许通过 PUT 修改计划主表/产品(与归档弹窗互补:表单改计划信息,归档弹窗改 actualQty/运费)。
|
||
|
||
**Response**:`PlanWithProducts`(与 get 一致)
|
||
|
||
### 3.2 Schema 新增
|
||
|
||
文件:`backend/app/schemas/pl/plan.py`
|
||
|
||
- `PlanArchiveProductItem`:`id`, `actualQty`
|
||
- `PlanArchiveFreightBlock`:与 transport arrange 运费字段子集一致
|
||
- `PlanArchiveBody`:`products`, `freight?`
|
||
|
||
### 3.3 Service 抽取
|
||
|
||
在 `pl_plan_service.py` 新增:
|
||
|
||
- `apply_archive_product_quantities(plan, products_payload)` — 写 actual + inbound/outbound
|
||
- `apply_archive_freight(plan, transport, freight_payload, products)` — 运费同步
|
||
|
||
便于单测与 `archive_plan` 路由保持薄。
|
||
|
||
---
|
||
|
||
## 四、前端文件清单
|
||
|
||
| 文件 | 动作 |
|
||
|------|------|
|
||
| `views/pl/plan-archive/index.vue` | **新建** — 列表页 |
|
||
| `components/business/pl-plan-archive-dialog/index.vue` | **新建** — 归档弹窗 |
|
||
| `composables/usePlPlanList.ts` | **新建(推荐)** — 从 plan/index 抽取 list/search/pagination |
|
||
| `views/pl/plan/index.vue` | 删除批量归档;可选改用 composable |
|
||
| `views/pl/plan/detail.vue` | 删除归档按钮 |
|
||
| `router/modules/pl.ts` | 新增 plan-archive 路由 |
|
||
| `api/pl/index.ts` | `planApi.archive(id, body)` 扩展 |
|
||
| `utils/router/menu-icon.ts` | `pl:plan-archive:list` 图标 |
|
||
| `App.vue` | `panelTitleBarTextRules` 增加「运输计划归档」 |
|
||
| `docs/sql/shanxia2.0.sql` | `sys_menu` 插入归档菜单 + 按钮权限 |
|
||
| `backend/app/api/pl/plan.py` | 增强 `archive_plan`;`update_plan` 允许已归档编辑 |
|
||
| `backend/app/schemas/pl/plan.py` | Archive body schema |
|
||
| `backend/app/services/pl_plan_service.py` | archive 业务函数 |
|
||
|
||
### 4.1 组件复用策略
|
||
|
||
| 组件 | 用途 |
|
||
|------|------|
|
||
| `PlPlanViewDialog` | 查看 |
|
||
| `PlPlanFormDialog` | 编辑 |
|
||
| `PlPlanViewTabs` | 归档弹窗下区 |
|
||
| `PlPlanFormProductTable` | 归档弹窗 actualQty(可增 prop `actualQtyEditableOnly`) |
|
||
| `usePlFreightCalc` | 归档弹窗运费 |
|
||
|
||
---
|
||
|
||
## 五、数据库与菜单 SQL
|
||
|
||
```sql
|
||
-- 菜单(示例 ID,实现时按现有 PL_* 规范)
|
||
INSERT INTO sys_menu (...) VALUES
|
||
('PL_PLAN_ARCHIVE', '运输计划归档', 'PL_MENU_ROOT', 'C', 'plan-archive', 'pl/plan-archive/index', 'pl:plan-archive:list', ...);
|
||
|
||
-- 按钮权限(F 类型,parent=PL_PLAN_ARCHIVE)
|
||
-- pl:plan-archive:query | edit | archive
|
||
```
|
||
|
||
---
|
||
|
||
## 六、安全与约束
|
||
|
||
| 项 | 规则 |
|
||
|----|------|
|
||
| 已归档 | 可继续「编辑」与「调整」actualQty/运费;不可反归档(`archive_status` 不回退为 `unarchived`) |
|
||
| 已作废 | 不可归档 / 不可调整 |
|
||
| 运费结算 | 运输单若已在「已提交/审核通过/已确认」结算单中,禁止归档时改运费 |
|
||
| 无 transport | `has_freight=1` 但无 `transport_id` 时,仅更新 `plan.freight_amount`,并 warn 日志(或 400 要求先安排运输 — **推荐 400:有运费必须先安排运输**) |
|
||
| 租户 | 所有读写 `tenant_no` |
|
||
| 事务 | 产品 + 运费 + 归档同一 commit |
|
||
|
||
---
|
||
|
||
## 七、验收标准
|
||
|
||
| # | 场景 | 预期 |
|
||
|---|------|------|
|
||
| V1 | 打开归档列表 | 展示全部计划;筛选与运输计划一致;多「归档状态」筛选项 |
|
||
| V2 | 未归档行 | 查看 / 编辑 / 归档 均可用 |
|
||
| V3 | 已归档行 | 查看 / 编辑 / 「调整」均可用 |
|
||
| V4 | 打开归档/调整弹窗 | 上区 actualQty、运费可编辑;下区三 Tab 完整 |
|
||
| V5 | 首次提交归档 | actual_qty、运费、archive/complete 状态一次成功 |
|
||
| V6 | 已归档后再次「调整」 | 仅更新 actual_qty/运费/金额;`archive_status` 保持 `archived` |
|
||
| V6a | 已归档「编辑」 | `PlPlanFormDialog` 可保存计划主表/产品变更 |
|
||
| V7 | 原运输计划页 | 无批量归档、详情无归档按钮 |
|
||
| V8 | 批量归档 | 仅处理未归档行;已归档跳过有提示 |
|
||
| V9 | 有运费未安排运输 | 归档 400,提示先安排运输 |
|
||
| V10 | 按重量运费 | 运输量默认 actualQty 合计,改 actualQty 后运费量联动 |
|
||
| V11 | 下区链接 | 司机/现场链接可复制(与安排运输下区一致) |
|
||
| V12 | 权限 | 无 `pl:plan-archive:archive` 不显示归档按钮 |
|
||
|
||
---
|
||
|
||
## 八、实施分期
|
||
|
||
| Phase | 内容 |
|
||
|-------|------|
|
||
| **P1** | 后端:`PlanArchiveBody` + service + 增强 `archive_plan` |
|
||
| **P2** | 前端:`PlPlanArchiveDialog` + `plan-archive/index.vue` + 路由/菜单/API |
|
||
| **P3** | 原页移除归档入口;抽取 `usePlPlanList`(可选) |
|
||
| **P4** | 联调验收 V1–V12 |
|
||
|
||
---
|
||
|
||
## 九、参考路径
|
||
|
||
| 说明 | 路径 |
|
||
|------|------|
|
||
| 运输计划列表 | `web-admin/src/views/pl/plan/index.vue` |
|
||
| 安排运输弹窗(布局基准) | `web-admin/src/components/business/pl-plan-arrange-dialog/index.vue` |
|
||
| 下区三 Tab | `web-admin/src/components/business/pl-plan-view-tabs/index.vue` |
|
||
| 产品表 | `web-admin/src/components/business/pl-plan-form-dialog/PlPlanFormProductTable.vue` |
|
||
| 运费计算 | `web-admin/src/composables/usePlFreightCalc.ts` |
|
||
| 现归档 API | `backend/app/api/pl/plan.py` → `archive_plan` |
|
||
| 需求 BR-15/32/42 | `docs/运输计划管理-需求说明文档.md` |
|