2026-06-20-pl-plan-form-dialog-design.md 13 KB

运输计划 · 新增计划弹窗 — 设计文档

日期: 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):

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 整行。


提交流程

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 块),单事务:

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)。