328 lines
13 KiB
Markdown
328 lines
13 KiB
Markdown
# 运输计划 · 新增计划弹窗 — 设计文档
|
||
|
||
> **日期**: 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`)。**
|