日期: 2026-06-20
状态: 已实施(2026-06-20)
前置:docs/运输计划管理-需求说明文档.md(§7 计划单据、§8 运输管理、§10 运费结算)、OpenSpecui-conventions(弹窗/form-grid-4)、openspec/changes/transport-plan-management/specs/transport-plan/spec.md
将 #/pl/plan 列表页「新增计划」弹窗从脚手架表单升级为符合业务闭环的 5 区域大弹窗:合同驱动、产品快照、可选运输安排(含运费),并与现有 pl_transport / 承运商 / 车辆人员证书体系对齐。
| # | 决策 | 说明 |
|---|---|---|
| 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 列) |
ContractSelector)priceTiers 取价规则(首期带合同行价)arrange 校验即可)PlPlanFormDialog
├── 【基础信息】 PlPlanFormBasicSection
├── 【合同信息】 PlPlanFormContractSection + ContractSelector
├── 【运输信息】 PlPlanFormTransportSection
├── 【产品信息】 PlPlanFormProductTable
└── 【备注】 计划级 textarea
| 字段 | 控件 | 规则 |
|---|---|---|
| 计划日期 | date | 必填;默认当天 |
| 归属公司 | select | 必填;shanxia / yundong |
| 单据类型 | select / 只读 Tag | 选合同后 自动计算(_determine_plan_type);可展示不可改 |
| 跟进人 | 只读或 select | 默认当前用户 |
不再包含:合同编号、客户/供应商手工录入(改由合同区只读带出)。
选择:ContractSelector 弹窗(仿 QuoteSelector),数据源 listContract + 确认时 getContract(id)。
展示(只读,4 列 KV + 子块):
| 子块 | 内容 |
|---|---|
| 合同基础 | 合同编号、合同名称、合同类型、签订/起止日期、结算方式、合同金额 |
| 供方 | supplierName + partyA*(名称、联系人、电话、地址) |
| 需方 | customerName + partyB*(名称、联系人、电话、地址) |
| 合同产品预览 | 只读小表(列与产品区一致,不含删除) |
联动:
products[] 全量映射,可删行)供方/需方映射:
supplierName + 甲方(partyA*)联系信息customerName + 乙方(partyB*)联系信息(与合同模板「甲乙方」字段一致,写入计划冗余字段 party_a / party_b / supplier_* / customer_*。)
| 字段 | 控件 | 规则 |
|---|---|---|
| 车牌号 | el-select filterable |
数据:vehicleApi.list({ vehicleStatus: 'available' });支持模糊 |
| 运输公司 | el-select filterable |
数据:carrierApi.list;选承运商车辆时 自动带出 carrierId/carrierName,可改 |
| 运输公里数 | el-input-number |
精度 2;可空 |
选车牌后展开(复用现有组件):
PlCertViewListPlVehiclePersonnelViewList(layout="tabs")人员体系已迁移至
pl_personnel;创建运输时司机 ID 从车辆关联人员中 role=driver 的默认项推导(与arrange.vue一致)。后端arrange若仍引用PlDriver,实现阶段应改为 personnel。
| 分组 | 字段 | 存储 |
|---|---|---|
| 取货 | 取货地址、取货时间、注意事项 | pl_plan 新增字段(见 DDL) |
| 目的地 | 目的地地址、送达时间、注意事项 | pl_plan 新增字段 |
创建 pl_transport 时,可选将取货地址 → start_point、目的地地址 → end_point 冗余写入运输表(便于运输管理列表展示)。
| 字段 | 按重量 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
表格列(横向滚动,list-table):
| 列 | 来源字段 | 可编辑 |
|---|---|---|
| 序号 | 行号 | 否 |
| 产品名称 | productName |
否 |
| 型号/规格 | productModel → model |
否 |
| 产品分类 | categoryName |
否 |
| 单位 | unit |
否 |
| 数量 | quantity |
否(合同带出;删行后重算运输量) |
| 包装规格 | packageSpec |
否 |
| 送货方式 | deliveryMethod |
否(Tag 展示) |
| 含税单价(元) | price |
否 |
| 含税总价(元) | subtotal |
否 |
| 税费(元) | taxAmount |
否 |
| 备注 | remark |
是 |
| 操作 | 删除 | — |
约束:无「添加产品」按钮;至少保留 1 行产品方可提交(products.length >= 1)。
计划级 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 写入汇总]
方案(推荐):新增 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 + PlPlanProducttransport.vehicleId 有值 → 复用 arrange_transport 核心逻辑(抽 plan_service.create_transport_for_plan()),设置 transport_status=arranged替代:前端两次调用 planApi.create + transportApi.arrange — 不推荐(中间失败难回滚)。
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 购买证/运输证校验 |
transport_status=待安排已安排,存在 pl_transport 关联check_field_consistency.py 通过create-full + 抽取 create_transport_for_planContractSelector + usePlContractToPlanusePlFreightCalcarrange.vue 复用 composable(可选同 PR)文档结束 — 请评审后进入 implementation plan(writing-plans)。