Files
shanxia2.0/docs/superpowers/specs/2026-06-20-pl-plan-form-dialog-design.md
T
2026-06-21 09:19:07 +08:00

328 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 运输计划 · 新增计划弹窗 — 设计文档
> **日期**: 2026-06-20
> **状态**: 已实施(2026-06-20)
> **前置**: `docs/运输计划管理-需求说明文档.md`(§7 计划单据、§8 运输管理、§10 运费结算)、OpenSpec `ui-conventions`(弹窗/form-grid-4)、`openspec/changes/transport-plan-management/specs/transport-plan/spec.md`
## 目标
将 `#/pl/plan` 列表页「新增计划」弹窗从脚手架表单升级为符合业务闭环的 **5 区域大弹窗**:合同驱动、产品快照、可选运输安排(含运费),并与现有 `pl_transport` / 承运商 / 车辆人员证书体系对齐。
## 已确认决策(2026-06-20)
| # | 决策 | 说明 |
|---|------|------|
| D1 | **运输量** | 默认 = 【产品信息】区域数量之和;用户可手工修改;按重量运费 = 运输量 × 运输单价 |
| D2 | **创建分支** | **未填车牌** → 仅创建计划草稿(`follow_status=draft`,`transport_status=pending`);**已填车牌** → 同事务创建 `pl_transport` + `pl_plan_transport`,计划 `transport_status=arranged` |
| D3 | **产品行** | 来自合同带出;支持删除行、行备注;**不可新增** |
| D4 | **弹窗布局** | 宽度 `min(1680px, calc(100vw - 80px))`;表单区 `form-grid-4`(一行 4 列) |
---
## 非目标(本期不做)
- 合同模块「发起计划」入口(可后续单独 PR,与本弹窗共用 `ContractSelector`)
- 框架合同 `priceTiers` 取价规则(首期带合同行价)
- 地图自动计算运输公里数
- 安排运输时易制毒/易制爆证件硬性拦截(需求文档安排页有校验,弹窗创建运输时 **沿用** 既有 `arrange` 校验即可)
- 编辑计划弹窗改造(本期仅 **新增** 弹窗;编辑仍走路由页,后续可对齐)
---
## 弹窗结构(5 区域)
```
PlPlanFormDialog
├── 【基础信息】 PlPlanFormBasicSection
├── 【合同信息】 PlPlanFormContractSection + ContractSelector
├── 【运输信息】 PlPlanFormTransportSection
├── 【产品信息】 PlPlanFormProductTable
└── 【备注】 计划级 textarea
```
### 1. 基础信息
| 字段 | 控件 | 规则 |
|------|------|------|
| 计划日期 | date | 必填;默认当天 |
| 归属公司 | select | 必填;`shanxia` / `yundong` |
| 单据类型 | select / 只读 Tag | 选合同后 **自动计算**(`_determine_plan_type`);可展示不可改 |
| 跟进人 | 只读或 select | 默认当前用户 |
**不再包含**:合同编号、客户/供应商手工录入(改由合同区只读带出)。
### 2. 合同信息
**选择**:`ContractSelector` 弹窗(仿 `QuoteSelector`),数据源 `listContract` + 确认时 `getContract(id)`。
**展示(只读,4 列 KV + 子块)**:
| 子块 | 内容 |
|------|------|
| 合同基础 | 合同编号、合同名称、合同类型、签订/起止日期、结算方式、合同金额 |
| 供方 | `supplierName` + `partyA*`(名称、联系人、电话、地址) |
| 需方 | `customerName` + `partyB*`(名称、联系人、电话、地址) |
| 合同产品预览 | 只读小表(列与产品区一致,不含删除) |
**联动**:
- 确认选择 → 填充基础信息(归属公司、单据类型、客户/供应商/目的地 ID 与名称等)
- 同步 **【产品信息】** 列表(合同 `products[]` 全量映射,可删行)
- 清空合同时重置产品列表与运输量
**供方/需方映射**:
- **供方** = CRM `supplierName` + 甲方(`partyA*`)联系信息
- **需方** = CRM `customerName` + 乙方(`partyB*`)联系信息
(与合同模板「甲乙方」字段一致,写入计划冗余字段 `party_a` / `party_b` / `supplier_*` / `customer_*`。)
### 3. 运输信息
#### 3.1 车辆与承运商
| 字段 | 控件 | 规则 |
|------|------|------|
| 车牌号 | `el-select` filterable | 数据:`vehicleApi.list({ vehicleStatus: 'available' })`;支持模糊 |
| 运输公司 | `el-select` filterable | 数据:`carrierApi.list`;选承运商车辆时 **自动带出** `carrierId/carrierName`,可改 |
| 运输公里数 | `el-input-number` | 精度 2;可空 |
**选车牌后展开**(复用现有组件):
- 车辆信息 KV(车牌、类型、载重、承运商、状态)
- 车辆证书:`PlCertViewList`
- 关联人员:`PlVehiclePersonnelViewList`(`layout="tabs"`)
> 人员体系已迁移至 `pl_personnel`;创建运输时司机 ID 从车辆关联人员中 **role=driver** 的默认项推导(与 `arrange.vue` 一致)。后端 `arrange` 若仍引用 `PlDriver`,实现阶段应改为 personnel。
#### 3.2 取货 / 目的地
| 分组 | 字段 | 存储 |
|------|------|------|
| 取货 | 取货地址、取货时间、注意事项 | `pl_plan` 新增字段(见 DDL) |
| 目的地 | 目的地地址、送达时间、注意事项 | `pl_plan` 新增字段 |
创建 `pl_transport` 时,可选将取货地址 → `start_point`、目的地地址 → `end_point` 冗余写入运输表(便于运输管理列表展示)。
#### 3.3 运费
| 字段 | 按重量 `by_weight` | 按趟 `by_trip` |
|------|-------------------|----------------|
| 是否有运输费用 | `has_freight` switch;关则隐藏运费块 | 同左 |
| 运费计算方式 | select,**默认按重量** | 同左 |
| 运输量(吨) | 可编辑;默认 `sum(产品.quantity)` | 隐藏 |
| 运输单价(元/吨) | 必填(当 has_freight=1) | 禁用 |
| 运费金额(元) | 自动 = 运输量×单价;**允许手工改**(需求 §10.2) | 手填必填 |
| 是否开票 | switch | 同左 |
**计算逻辑**(抽 composable `usePlFreightCalc`):
```typescript
defaultTransportQty = products.reduce((s, p) => s + (p.quantity || 0), 0)
// 用户改 transportQty 后不再自动覆盖,除非产品列表变化时可提示「是否重新汇总」
if (freightType === 'by_weight')
freightAmount = transportQty * freightPrice // 可被用户 override
if (freightType === 'by_trip')
freightAmount = manualTripAmount
```
### 4. 产品信息
**表格列**(横向滚动,`list-table`):
| 列 | 来源字段 | 可编辑 |
|----|----------|--------|
| 序号 | 行号 | 否 |
| 产品名称 | `productName` | 否 |
| 型号/规格 | `productModel` → `model` | 否 |
| 产品分类 | `categoryName` | 否 |
| 单位 | `unit` | 否 |
| 数量 | `quantity` | 否(合同带出;删行后重算运输量) |
| 包装规格 | `packageSpec` | 否 |
| 送货方式 | `deliveryMethod` | 否(Tag 展示) |
| 含税单价(元) | `price` | 否 |
| 含税总价(元) | `subtotal` | 否 |
| 税费(元) | `taxAmount` | 否 |
| 备注 | `remark` | **是** |
| 操作 | 删除 | — |
**约束**:无「添加产品」按钮;至少保留 1 行产品方可提交(`products.length >= 1`)。
### 5. 备注
计划级 `remark`,textarea 整行。
---
## 提交流程
```mermaid
flowchart TD
A[用户点击创建] --> B{表单校验}
B -->|失败| Z[滚动到首个错误]
B -->|通过| C{contractId 已选?}
C -->|否| Z2[提示选择合同]
C -->|是| D{vehicleId 已填?}
D -->|否| E[POST 创建计划草稿]
D -->|是| F[POST 创建计划 + 运输]
E --> G[follow_status=draft transport_status=pending]
F --> H[创建 pl_transport + pl_plan_transport]
H --> I[plan.transport_status=arranged]
F --> J[plan.freight_amount 写入汇总]
```
### API 设计
**方案(推荐)**:新增 `POST /transport/plan/create-full`(或扩展 `PlanCreate` 增加可选 `transport` 块),**单事务**:
```typescript
interface PlanCreateFullBody {
// 与 PlanCreate 对齐 + 扩展产品字段
contractId: string // 必填
planDate: string
belongsCompany: string
// ... 合同冗余字段、取货/目的地 6 字段
products: PlanProductCreateRow[]
remark?: string
transport?: {
vehicleId: string
driverId?: string // 可选,默认车辆默认司机
carrierId?: string
distanceKm?: number
pickupAddress?: string // 同步 start_point
destinationAddress?: string // 同步 end_point
freightType: 'by_weight' | 'by_trip'
transportQty?: number // 按重量;缺省后端用产品合计
freightPrice?: number
freightAmount?: number // 按趟或 override
hasInvoice?: number
remark?: string
}
}
```
**分支**:
- `transport` 缺省或 `vehicleId` 空 → 仅 `PlPlan` + `PlPlanProduct`
- `transport.vehicleId` 有值 → 复用 `arrange_transport` 核心逻辑(抽 `plan_service.create_transport_for_plan()`),设置 `transport_status=arranged`
**替代**:前端两次调用 `planApi.create` + `transportApi.arrange` — **不推荐**(中间失败难回滚)。
---
## 数据模型变更(DDL)
### `pl_plan` 增量
| 字段 | 类型 | 说明 |
|------|------|------|
| `pickup_address` | varchar(500) | 取货地址 |
| `pickup_time` | datetime | 取货时间 |
| `pickup_notes` | varchar(500) | 取货注意事项 |
| `destination_address` | varchar(500) | 目的地地址 |
| `delivery_time` | datetime | 送达时间 |
| `destination_notes` | varchar(500) | 目的地注意事项 |
SQL 文件:`docs/sql/20260620_pl_plan_logistics_fields.sql`
### `pl_plan_product` 增量
| 字段 | 类型 | 说明 |
|------|------|------|
| `category_name` | varchar(200) | 产品分类 |
| `package_spec` | varchar(200) | 包装规格 |
| `delivery_method` | varchar(50) | 送货方式 |
| `quantity` | decimal(15,3) | 合同数量(与 estimate 字段并存;写入时同步 estimate) |
| `tax_price` | decimal(15,4) | 含税单价 |
| `tax_amount` | decimal(15,2) | 税费 |
| `tax_subtotal` | decimal(15,2) | 含税总价 |
> 现有 `inbound/outbound_*` 字段保留;创建时按 `plan_type` 将 `quantity` 写入对应 `*_estimate_qty`。
### `pl_transport` 增量(可选)
| 字段 | 类型 | 说明 |
|------|------|------|
| `transport_qty` | decimal(15,3) | 本次运输量(按重量计费基数) |
---
## 前端组件
| 路径 | 职责 |
|------|------|
| `components/business/pl-plan-form-dialog/index.vue` | 弹窗壳、校验、提交 |
| `components/business/pl-plan-form-dialog/PlPlanFormBasicSection.vue` | 基础信息 |
| `components/business/pl-plan-form-dialog/PlPlanFormContractSection.vue` | 合同选择与只读展示 |
| `components/business/pl-plan-form-dialog/PlPlanFormTransportSection.vue` | 运输 + 运费 + 车辆详情 |
| `components/business/pl-plan-form-dialog/PlPlanFormProductTable.vue` | 产品表 |
| `components/business/contract-selector/index.vue` | 合同选择弹窗 |
| `composables/usePlFreightCalc.ts` | 运输量/运费计算(`arrange.vue` 同步改造) |
| `composables/usePlContractToPlan.ts` | 合同 VO → 计划表单 + 产品行映射 |
**UI 规范**:
- `dialog-form-layout`、`align-center`、`modal-class="order-module-dialog-mask"`、`backdrop-filter: none`
- 按钮带 `@element-plus/icons-vue` 图标
- 产品送货方式/单据类型用 `el-tag`
---
## 合同产品 → 计划产品映射
| 合同 `ContractProductForm` | 计划 `PlPlanProduct` / 前端 row |
|----------------------------|--------------------------------|
| productId | productId |
| productName | productName |
| productModel | model |
| categoryName | categoryName |
| unit | unit |
| quantity | quantity + inbound/outboundEstimateQty |
| packageSpec | packageSpec |
| deliveryMethod | deliveryMethod |
| price | taxPrice |
| subtotal | taxSubtotal |
| taxAmount | taxAmount |
| remark | remark |
---
## 校验规则
| 规则 | 说明 |
|------|------|
| V1 | 必选合同 |
| V2 | 产品至少 1 行 |
| V3 | 未选车牌:不校验运输/运费 |
| V4 | 已选车牌:`vehicleId` 有效;若车辆有司机则 `driverId` 必填或自动默认 |
| V5 | `has_freight=1` 且 `by_weight`:运输量 > 0,单价 > 0 |
| V6 | `has_freight=1` 且 `by_trip`:运费金额 > 0 |
| V7 | 易制毒/易制爆产品创建运输时:沿用 arrange 购买证/运输证校验 |
---
## 测试要点
1. 仅选合同 + 产品 → 创建成功,列表 `transport_status=待安排`
2. 选合同 + 车牌 + 按重量运费 → 计划 `已安排`,存在 `pl_transport` 关联
3. 修改运输量 → 运费重算;再手工改运费 → 提交值以 override 为准
4. 删产品行 → 运输量默认值更新(或提示重算)
5. 切换按趟/按重量 → 字段显隐正确
6. 字段一致性脚本 `check_field_consistency.py` 通过
---
## 实施顺序建议
1. DDL + Model + Schema(plan 物流字段、plan_product 扩展、transport_qty)
2. 后端 `create-full` + 抽取 `create_transport_for_plan`
3. `ContractSelector` + `usePlContractToPlan`
4. 弹窗 5 区域拆分与 `usePlFreightCalc`
5. `arrange.vue` 复用 composable(可选同 PR)
6. 联调 + 字段一致性检查
---
**文档结束 — 请评审后进入 implementation plan(`writing-plans`)。**