Просмотр исходного кода

feat(pl): 运费结算审批分流与车辆挂车号等 PL 能力完善

拆分运费审批/出款页面,补充结算发票附件与菜单 SQL,并同步前后端模型与车辆相关展示。

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 4 месяцев назад
Родитель
Сommit
98aec80d71
37 измененных файлов с 1362 добавлено и 195 удалено
  1. 2 0
      backend/app/models/pl_models.py
  2. 2 0
      backend/app/schemas/pl/base_data.py
  3. 4 0
      backend/app/schemas/pl/freight.py
  4. 3 0
      docs/sql/20260628_pl_freight_settlement_invoice_attachment.sql
  5. 99 0
      docs/sql/20260628_pl_freight_settlement_split_menu.sql
  6. 26 0
      docs/sql/20260628_pl_vehicle_menu_order.sql
  7. 32 0
      docs/sql/20260628_pl_vehicle_trailer_no.sql
  8. 1 0
      docs/sql/pl_tables.sql
  9. 4 2
      docs/sql/shanxia2.0.sql
  10. 32 0
      docs/superpowers/specs/2026-06-28-pl-freight-settlement-split-design.md
  11. 6 1
      web-admin/src/api/pl/index.ts
  12. 268 62
      web-admin/src/components/business/pl-freight-disburse-dialog/index.vue
  13. 86 22
      web-admin/src/components/business/pl-freight-settlement-form-dialog/index.vue
  14. 141 29
      web-admin/src/components/business/pl-freight-settlement-view-dialog/index.vue
  15. 5 3
      web-admin/src/components/business/pl-plan-arrange-dialog/index.vue
  16. 7 1
      web-admin/src/components/business/pl-plan-form-dialog/index.vue
  17. 2 2
      web-admin/src/components/business/pl-plan-view-dialog/index.vue
  18. 10 3
      web-admin/src/components/business/pl-plan-view-tabs/index.vue
  19. 3 0
      web-admin/src/components/business/pl-vehicle-summary-strip/index.vue
  20. 1 0
      web-admin/src/components/business/pl-vehicle-view-dialog/index.vue
  21. 1 0
      web-admin/src/components/business/pl-vehicle-view-panel/index.vue
  22. 23 4
      web-admin/src/composables/usePlPlanArrangeActions.ts
  23. 21 7
      web-admin/src/router/modules/pl.ts
  24. 7 2
      web-admin/src/utils/db-site-summary.ts
  25. 18 0
      web-admin/src/utils/download-attachment.ts
  26. 13 2
      web-admin/src/utils/pl-display.ts
  27. 41 0
      web-admin/src/views/pl/freight-approval/index.vue
  28. 47 0
      web-admin/src/views/pl/freight-disburse/index.vue
  29. 176 0
      web-admin/src/views/pl/freight/components/ApprovalDonePanel.vue
  30. 184 0
      web-admin/src/views/pl/freight/components/ApprovalPendingPanel.vue
  31. 45 29
      web-admin/src/views/pl/freight/components/SettlementBillPanel.vue
  32. 25 1
      web-admin/src/views/pl/freight/components/UnsettledPlanPanel.vue
  33. 1 1
      web-admin/src/views/pl/freight/freight-status.ts
  34. 2 18
      web-admin/src/views/pl/freight/index.vue
  35. 0 6
      web-admin/src/views/pl/plan-archive/index.vue
  36. 12 0
      web-admin/src/views/pl/self-vehicle/index.vue
  37. 12 0
      web-admin/src/views/pl/vehicle/index.vue

+ 2 - 0
backend/app/models/pl_models.py

@@ -56,6 +56,7 @@ class PlVehicle(Base):
     carrier_id = Column(String(64), nullable=True, comment="承运商ID(类型为承运商时必填)")
     carrier_name = Column(String(200), nullable=True, comment="承运商名称(冗余)")
     license_plate = Column(String(20), nullable=False, unique=True, comment="车牌号")
+    trailer_no = Column(String(20), nullable=True, comment="挂车号")
     default_driver_id = Column(String(64), nullable=True, comment="默认司机ID")
     default_driver_name = Column(String(100), nullable=True, comment="默认司机姓名(冗余)")
     load_capacity = Column(DECIMAL(10, 2), nullable=True, comment="载重(吨)")
@@ -533,6 +534,7 @@ class PlFreightSettlement(Base):
     # 发票信息
     invoice_no = Column(String(50), nullable=True, comment="发票号")
     invoice_date = Column(Date, nullable=True, comment="开票日期")
+    invoice_attachment_urls = Column(Text, nullable=True, comment="发票附件URLs,逗号分隔")
 
     # 结算状态
     settlement_status = Column(String(20), nullable=False, default="pending", comment="结算状态 pending-待提交 submitted-已提交 approved-审核通过 confirmed-已确认")

+ 2 - 0
backend/app/schemas/pl/base_data.py

@@ -92,6 +92,7 @@ class VehicleBase(BaseModel):
     carrier_id: Optional[str] = Field(default=None, alias="carrierId", description="承运商ID")
     carrier_name: Optional[str] = Field(default=None, max_length=200, alias="carrierName", description="承运商名称")
     license_plate: str = Field(..., max_length=20, alias="licensePlate", description="车牌号")
+    trailer_no: Optional[str] = Field(default=None, max_length=20, alias="trailerNo", description="挂车号")
     default_driver_id: Optional[str] = Field(default=None, alias="defaultDriverId", description="默认司机ID(只读兼容)")
     default_driver_name: Optional[str] = Field(default=None, max_length=100, alias="defaultDriverName", description="默认司机姓名(只读兼容)")
     load_capacity: Optional[Decimal] = Field(default=None, alias="loadCapacity", description="载重(吨)")
@@ -110,6 +111,7 @@ class VehicleUpdate(BaseModel):
     carrier_id: Optional[str] = Field(default=None, alias="carrierId")
     carrier_name: Optional[str] = Field(default=None, max_length=200, alias="carrierName")
     license_plate: Optional[str] = Field(default=None, max_length=20, alias="licensePlate")
+    trailer_no: Optional[str] = Field(default=None, max_length=20, alias="trailerNo")
     load_capacity: Optional[Decimal] = Field(default=None, alias="loadCapacity")
     vehicle_status: Optional[str] = Field(default=None, max_length=20, alias="vehicleStatus")
     remark: Optional[str] = None

+ 4 - 0
backend/app/schemas/pl/freight.py

@@ -16,6 +16,9 @@ class FreightSettlementBase(BaseModel):
     invoice_amount: Optional[Decimal] = Field(default=None, alias="invoiceAmount", description="发票金额")
     invoice_no: Optional[str] = Field(default=None, max_length=50, alias="invoiceNo", description="发票号")
     invoice_date: Optional[date] = Field(default=None, alias="invoiceDate", description="开票日期")
+    invoice_attachment_urls: Optional[str] = Field(
+        default=None, alias="invoiceAttachmentUrls", description="发票附件URLs,逗号分隔"
+    )
     settlement_status: str = Field(default="pending", alias="settlementStatus", description="结算状态")
     remark: Optional[str] = None
 
@@ -36,6 +39,7 @@ class FreightSettlementUpdate(BaseModel):
     invoice_amount: Optional[Decimal] = Field(default=None, alias="invoiceAmount")
     invoice_no: Optional[str] = Field(default=None, max_length=50, alias="invoiceNo")
     invoice_date: Optional[date] = Field(default=None, alias="invoiceDate")
+    invoice_attachment_urls: Optional[str] = Field(default=None, alias="invoiceAttachmentUrls")
     remark: Optional[str] = None
     model_config = {"populate_by_name": True}
 

+ 3 - 0
docs/sql/20260628_pl_freight_settlement_invoice_attachment.sql

@@ -0,0 +1,3 @@
+-- 运费结算:发票附件字段
+ALTER TABLE `pl_freight_settlement`
+  ADD COLUMN `invoice_attachment_urls` text DEFAULT NULL COMMENT '发票附件URLs,逗号分隔' AFTER `invoice_date`;

+ 99 - 0
docs/sql/20260628_pl_freight_settlement_split_menu.sql

@@ -0,0 +1,99 @@
+-- =============================================================================
+-- 运费结算 · 拆分为三菜单(结算 / 审批 / 财务发放)
+-- 执行库:shanxia2.0 门户库
+-- 说明:可重复执行;执行后重新登录刷新菜单
+-- =============================================================================
+
+SET NAMES utf8mb4;
+
+-- -----------------------------------------------------------------------------
+-- 1. 新增菜单
+-- -----------------------------------------------------------------------------
+
+INSERT INTO `sys_menu` (
+  `id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`,
+  `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`,
+  `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
+) VALUES
+(
+  'PL_FREIGHT_APPROVAL', '运费结算审批', 'PL_MENU_ROOT', 'C',
+  'freight-approval', 'pl/freight-approval/index', 'pl:freight-approval:list',
+  1, '1', '1', 'shanxia2.0', 31, '待审批与已审批结算单',
+  'system', '', NOW(), NULL, NOW(), 0
+),
+(
+  'PL_FREIGHT_APPROVAL_Q', '查询', 'PL_FREIGHT_APPROVAL', 'F',
+  '', '', 'pl:freight-approval:query',
+  1, '1', '1', 'shanxia2.0', 1, NULL,
+  'system', '', NOW(), NULL, NOW(), 0
+),
+(
+  'PL_FREIGHT_DISBURSE_MENU', '运费财务发放', 'PL_MENU_ROOT', 'C',
+  'freight-disburse', 'pl/freight-disburse/index', 'pl:freight-disburse:list',
+  1, '1', '1', 'shanxia2.0', 32, '待发放与已发放结算单',
+  'system', '', NOW(), NULL, NOW(), 0
+),
+(
+  'PL_FREIGHT_DISBURSE_Q', '查询', 'PL_FREIGHT_DISBURSE_MENU', 'F',
+  '', '', 'pl:freight-disburse:query',
+  1, '1', '1', 'shanxia2.0', 1, NULL,
+  'system', '', NOW(), NULL, NOW(), 0
+)
+ON DUPLICATE KEY UPDATE
+  `menu_name`   = VALUES(`menu_name`),
+  `parent_id`   = VALUES(`parent_id`),
+  `menu_type`   = VALUES(`menu_type`),
+  `path`        = VALUES(`path`),
+  `component`   = VALUES(`component`),
+  `perms`       = VALUES(`perms`),
+  `visible`     = VALUES(`visible`),
+  `status`      = VALUES(`status`),
+  `order_num`   = VALUES(`order_num`),
+  `update_time` = NOW(),
+  `del_flag`    = 0;
+
+-- 运费结算菜单排序保持在审批、发放之前
+UPDATE `sys_menu`
+SET `order_num` = 30, `update_time` = NOW()
+WHERE `id` = 'PL_FREIGHT' AND `del_flag` = 0;
+
+-- -----------------------------------------------------------------------------
+-- 2. 为已拥有「运费结算」菜单的角色自动授权新菜单
+-- -----------------------------------------------------------------------------
+
+INSERT INTO `sys_role_menu` (
+  `id`, `role_id`, `menu_id`, `tenant_no`, `order_num`, `remark`,
+  `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
+)
+SELECT
+  REPLACE(UUID(), '-', ''),
+  rm.`role_id`,
+  m.`menu_id`,
+  rm.`tenant_no`,
+  m.`order_num`,
+  'pl freight split menus auto grant',
+  'system',
+  '',
+  NOW(),
+  NULL,
+  NOW(),
+  0
+FROM `sys_role_menu` rm
+INNER JOIN (
+  SELECT 'PL_FREIGHT_APPROVAL' AS menu_id, 31 AS order_num UNION ALL
+  SELECT 'PL_FREIGHT_APPROVAL_Q', 1 UNION ALL
+  SELECT 'PL_FREIGHT_DISBURSE_MENU', 32 UNION ALL
+  SELECT 'PL_FREIGHT_DISBURSE_Q', 1
+) m ON 1 = 1
+WHERE rm.`menu_id` = 'PL_FREIGHT'
+  AND rm.`del_flag` = 0
+  AND NOT EXISTS (
+    SELECT 1 FROM `sys_role_menu` x
+    WHERE x.`role_id` = rm.`role_id` AND x.`menu_id` = m.`menu_id` AND x.`del_flag` = 0
+  );
+
+SELECT `id`, `menu_name`, `path`, `component`, `perms`, `order_num`
+FROM `sys_menu`
+WHERE `id` IN ('PL_FREIGHT', 'PL_FREIGHT_APPROVAL', 'PL_FREIGHT_DISBURSE_MENU')
+  AND `del_flag` = 0
+ORDER BY `order_num`;

+ 26 - 0
docs/sql/20260628_pl_vehicle_menu_order.sql

@@ -0,0 +1,26 @@
+-- =============================================================================
+-- 运输管理 · 调整「山峡自有车辆」与「车辆管理」菜单排序
+-- 执行库:shanxia2.0 门户库(与 web-admin 同库)
+-- 说明:可重复执行;交换 order_num,使「山峡自有车辆」排在「车辆管理」之上
+-- 执行后:重新登录或刷新菜单缓存
+-- =============================================================================
+
+SET NAMES utf8mb4;
+
+UPDATE `sys_menu`
+SET `order_num` = 2, `update_time` = NOW()
+WHERE `id` = 'PL_SELF_VEHICLE'
+  AND `del_flag` = 0
+  AND (`order_num` IS NULL OR `order_num` <> 2);
+
+UPDATE `sys_menu`
+SET `order_num` = 4, `update_time` = NOW()
+WHERE `id` = 'PL_VEHICLE'
+  AND `del_flag` = 0
+  AND (`order_num` IS NULL OR `order_num` <> 4);
+
+SELECT `id`, `menu_name`, `parent_id`, `order_num`
+FROM `sys_menu`
+WHERE `id` IN ('PL_SELF_VEHICLE', 'PL_VEHICLE')
+  AND `del_flag` = 0
+ORDER BY `order_num`;

+ 32 - 0
docs/sql/20260628_pl_vehicle_trailer_no.sql

@@ -0,0 +1,32 @@
+-- =============================================================================
+-- 运输管理 · 车辆表增加挂车号
+-- 执行库:shanxia2.0 门户库(与 web-admin 同库)
+-- 说明:可重复执行;挂车号为车辆主数据属性,地磅/出入库仍使用 license_plate
+-- =============================================================================
+
+SET NAMES utf8mb4;
+
+SET @col_exists := (
+  SELECT COUNT(*)
+  FROM information_schema.COLUMNS
+  WHERE TABLE_SCHEMA = DATABASE()
+    AND TABLE_NAME = 'pl_vehicle'
+    AND COLUMN_NAME = 'trailer_no'
+);
+
+SET @ddl := IF(
+  @col_exists = 0,
+  'ALTER TABLE `pl_vehicle` ADD COLUMN `trailer_no` varchar(20) DEFAULT NULL COMMENT ''挂车号'' AFTER `license_plate`',
+  'SELECT ''trailer_no already exists'' AS message'
+);
+
+PREPARE stmt FROM @ddl;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_COMMENT
+FROM information_schema.COLUMNS
+WHERE TABLE_SCHEMA = DATABASE()
+  AND TABLE_NAME = 'pl_vehicle'
+  AND COLUMN_NAME IN ('license_plate', 'trailer_no')
+ORDER BY ORDINAL_POSITION;

+ 1 - 0
docs/sql/pl_tables.sql

@@ -417,6 +417,7 @@ CREATE TABLE `pl_freight_settlement` (
   `invoice_amount` decimal(15,2) DEFAULT NULL COMMENT '发票金额',
   `invoice_no` varchar(50) DEFAULT NULL COMMENT '发票号',
   `invoice_date` date DEFAULT NULL COMMENT '开票日期',
+  `invoice_attachment_urls` text DEFAULT NULL COMMENT '发票附件URLs,逗号分隔',
   `settlement_status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT '结算状态 pending-待提交 submitted-已提交 approved-审核通过 confirmed-已确认',
   `tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0' COMMENT '租户编号',
   `order_num` int(11) DEFAULT NULL COMMENT '排序号',

+ 4 - 2
docs/sql/shanxia2.0.sql

@@ -2425,6 +2425,7 @@ CREATE TABLE `pl_freight_settlement` (
   `invoice_amount` decimal(15,2) DEFAULT NULL COMMENT '发票金额',
   `invoice_no` varchar(50) DEFAULT NULL COMMENT '发票号',
   `invoice_date` date DEFAULT NULL COMMENT '开票日期',
+  `invoice_attachment_urls` text DEFAULT NULL COMMENT '发票附件URLs,逗号分隔',
   `settlement_status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT '结算状态 pending-待提交 submitted-已提交 approved-审核通过 confirmed-已确认',
   `tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0' COMMENT '租户编号',
   `order_num` int(11) DEFAULT NULL COMMENT '排序号',
@@ -2993,6 +2994,7 @@ CREATE TABLE `pl_vehicle` (
   `carrier_id` varchar(64) DEFAULT NULL COMMENT '承运商ID(类型为承运商时必填)',
   `carrier_name` varchar(200) DEFAULT NULL COMMENT '承运商名称(冗余)',
   `license_plate` varchar(20) NOT NULL COMMENT '车牌号',
+  `trailer_no` varchar(20) DEFAULT NULL COMMENT '挂车号',
   `default_driver_id` varchar(64) DEFAULT NULL COMMENT '默认司机ID',
   `default_driver_name` varchar(100) DEFAULT NULL COMMENT '默认司机姓名(冗余)',
   `load_capacity` decimal(10,2) DEFAULT NULL COMMENT '载重(吨)',
@@ -8499,9 +8501,9 @@ INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `co
 INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_PLAN_ARCHIVE_E', '编辑', 'PL_PLAN_ARCHIVE', 'F', '', '', 'pl:plan-archive:edit', 1, '1', '1', 'shanxia2.0', 2, NULL, 'system', '', '2026-06-25 12:00:00', NULL, '2026-06-25 12:00:00', 0);
 INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_PLAN_ARCHIVE_A', '归档', 'PL_PLAN_ARCHIVE', 'F', '', '', 'pl:plan-archive:archive', 1, '1', '1', 'shanxia2.0', 3, NULL, 'system', '', '2026-06-25 12:00:00', NULL, '2026-06-25 12:00:00', 0);
 INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_PLAN_CERT', '危化品证上传', 'PL_MENU_ROOT', 'C', 'plan-cert', 'pl/plan-cert/index', 'pl:plan-cert:list', 1, '1', '1', 'shanxia2.0', 12, '危化品购买证/运输证上传', 'system', '', '2026-06-20 18:18:10', NULL, '2026-06-20 18:18:10', 0);
-INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_SELF_VEHICLE', '山峡自有车辆', 'PL_MENU_ROOT', 'C', 'self-vehicle', 'pl/self-vehicle/index', 'pl:self-vehicle:list', 1, '1', '1', 'shanxia2.0', 4, '展示全部自有类型车辆', 'system', '', '2026-06-20 08:07:00', NULL, '2026-06-20 08:07:00', 0);
+INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_SELF_VEHICLE', '山峡自有车辆', 'PL_MENU_ROOT', 'C', 'self-vehicle', 'pl/self-vehicle/index', 'pl:self-vehicle:list', 1, '1', '1', 'shanxia2.0', 2, '展示全部自有类型车辆', 'system', '', '2026-06-20 08:07:00', NULL, '2026-06-20 08:07:00', 0);
 INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_TRANSPORT', '运输管理', 'PL_MENU_ROOT', 'C', 'transport', 'pl/transport/index', 'pl:transport:list', 1, '0', '1', 'shanxia2.0', 20, NULL, 'system', '', '2026-06-11 05:17:13', NULL, '2026-06-20 21:42:04', 0);
-INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_VEHICLE', '车辆管理', 'PL_MENU_ROOT', 'C', 'vehicle', 'pl/vehicle/index', 'pl:vehicle:list', 1, '1', '1', 'shanxia2.0', 2, NULL, 'system', '', '2026-06-11 05:17:12', NULL, '2026-06-11 05:17:12', 0);
+INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_VEHICLE', '车辆管理', 'PL_MENU_ROOT', 'C', 'vehicle', 'pl/vehicle/index', 'pl:vehicle:list', 1, '1', '1', 'shanxia2.0', 4, NULL, 'system', '', '2026-06-11 05:17:12', NULL, '2026-06-11 05:17:12', 0);
 INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('SYSB000000000000000000000000000001', '系统基础信息', '2', 'M', '/system-base', '/index/index', NULL, 1, '1', '1', 'shanxia2.0', 11, NULL, NULL, '', '2026-05-27 16:26:30', NULL, '2026-05-30 18:16:14', 1);
 INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('SYSM000000000000000000000000000001', '系统管理', '2', 'M', '/system', '/index/index', NULL, 1, '1', '1', 'shanxia2.0', 13, NULL, NULL, '', '2026-05-27 16:26:30', NULL, '2026-05-30 17:06:32', 0);
 INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('WKF00000000000000000000000000001', '工作流管理', '2', 'M', '/workflow', '/index/index', NULL, 1, '1', '1', 'shanxia2.0', 12, NULL, NULL, '', '2026-05-27 16:26:30', NULL, '2026-05-30 17:06:32', 0);

+ 32 - 0
docs/superpowers/specs/2026-06-28-pl-freight-settlement-split-design.md

@@ -0,0 +1,32 @@
+# 运费结算三菜单拆分 — 设计文档
+
+> **日期**: 2026-06-28  
+> **状态**: 已确认  
+> **前置**: `docs/superpowers/specs/2026-06-25-pl-freight-settlement-design.md`
+
+## 目标
+
+将原「运费结算」单页四 Tab 拆为三个独立菜单,按角色分离:计划专员组单/结算、审批人审批、财务发放。
+
+## 菜单结构
+
+| 菜单 | 路由 | Tab | 数据范围 |
+|------|------|-----|----------|
+| 运费结算 | `/#/pl/freight` | 未结算计划 / 结算单 | 计划;结算单 `pending`/`approving`/`approved` |
+| 运费结算审批 | `/#/pl/freight-approval` | 待审批 / 已审批 | `approving`;`approved`+`confirmed`(全部) |
+| 运费财务发放 | `/#/pl/freight-disburse` | 待发放 / 已发放 | `approved`;`confirmed` |
+
+## 关键决策
+
+- **已审批 Tab**:展示租户内全部审批已完成结算单(选项 A)
+- **发票信息**:审批通过后(`approved`)在「运费结算 → 结算单」Tab 维护
+- **后端 tab 参数**:`bills` | `approval_pending` | `approval_done` | `disburse` | `settled`
+- **编辑权限**:`pending` 全字段编辑;`approved` 仅发票字段 + 备注
+
+## 业务流程
+
+未结算计划 → 组单(草稿)→ 提交审批 → 审批页通过 → 结算页补发票 → 财务页发放 → 已发放
+
+## SQL
+
+`docs/sql/20260628_pl_freight_settlement_split_menu.sql`

+ 6 - 1
web-admin/src/api/pl/index.ts

@@ -67,6 +67,7 @@ export interface Vehicle {
   carrierId?: string
   carrierName?: string
   licensePlate: string
+  trailerNo?: string
   defaultDriverId?: string
   defaultDriverName?: string
   personnelSummary?: string
@@ -86,6 +87,7 @@ export interface VehiclePayload {
   carrierId?: string
   carrierName?: string
   licensePlate: string
+  trailerNo?: string
   loadCapacity?: number | null
   vehicleStatus: string
   remark?: string
@@ -575,6 +577,7 @@ export interface UnsettledPlanItem {
 
 export interface UnsettledPlanQuery {
   q?: string
+  carrierId?: string
   planType?: string
   licensePlate?: string
   page?: number
@@ -624,6 +627,7 @@ export interface FreightSettlement {
   invoiceAmount?: number
   invoiceNo?: string
   invoiceDate?: string
+  invoiceAttachmentUrls?: string
   settlementStatus: string
   remark?: string
   createBy?: string
@@ -647,8 +651,9 @@ export interface FreightSettlement {
 
 export interface FreightSettlementQuery {
   q?: string
+  carrierId?: string
   settlementStatus?: string
-  tab?: 'bills' | 'disburse' | 'settled'
+  tab?: 'bills' | 'approval_pending' | 'approval_done' | 'disburse' | 'settled'
   page?: number
   pageSize?: number
 }

+ 268 - 62
web-admin/src/components/business/pl-freight-disburse-dialog/index.vue

@@ -2,7 +2,7 @@
   <el-dialog
     v-model="visible"
     title="财务发放登记"
-    width="640px"
+    width="1200px"
     align-center
     append-to-body
     destroy-on-close
@@ -10,60 +10,156 @@
     modal-class="order-module-dialog-mask"
     :close-on-click-modal="false"
   >
-    <el-form
-      ref="formRef"
-      v-loading="loading"
-      :model="form"
-      :rules="rules"
-      label-position="top"
-      scroll-to-error
-      :scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
-    >
-      <div class="form-grid-2">
-        <el-form-item label="结算单号">
-          <el-input :model-value="settlement?.settlementNo" disabled />
-        </el-form-item>
-        <el-form-item label="运费合计(元)">
-          <el-input :model-value="formatAmount(settlement?.totalFreight)" disabled />
-        </el-form-item>
-        <el-form-item label="汇款日期" prop="remittanceDate">
-          <el-date-picker
-            v-model="form.remittanceDate"
-            type="date"
-            value-format="YYYY-MM-DD"
-            placeholder="选择汇款日期"
-            style="width: 100%"
-          />
-        </el-form-item>
-        <el-form-item label="汇款金额(元)" prop="remittanceAmount">
-          <el-input-number
-            v-model="form.remittanceAmount"
-            :min="0.01"
-            :precision="2"
-            :controls="false"
-            placeholder="请输入汇款金额"
-            style="width: 100%"
-          />
-        </el-form-item>
-        <el-form-item label="汇款凭证" prop="voucherUrls" class="form-col-span-2">
+    <div v-loading="loading" class="disburse-dialog-body">
+      <section v-if="settlement" class="dialog-section">
+        <h3 class="dialog-section-title">结算摘要</h3>
+        <div class="summary-grid">
+          <div class="summary-item">
+            <span class="summary-label">结算单号</span>
+            <span class="summary-value">{{ settlement.settlementNo }}</span>
+          </div>
+          <div class="summary-item">
+            <span class="summary-label">运费合计(元)</span>
+            <span class="summary-value summary-value--amount">{{
+              formatAmount(settlement.totalFreight)
+            }}</span>
+          </div>
+        </div>
+      </section>
+
+      <section v-if="settlement" class="dialog-section">
+        <h3 class="dialog-section-title">发票信息</h3>
+        <div class="info-kv-grid info-kv-grid--cols-3">
+          <div class="info-kv-item">
+            <span class="info-kv-label">发票号</span>
+            <span class="info-kv-value">{{ settlement.invoiceNo || '—' }}</span>
+          </div>
+          <div class="info-kv-item">
+            <span class="info-kv-label">开票日期</span>
+            <span class="info-kv-value">{{ settlement.invoiceDate || '—' }}</span>
+          </div>
+          <div class="info-kv-item">
+            <span class="info-kv-label">发票金额(元)</span>
+            <span class="info-kv-value">{{
+              settlement.invoiceAmount ? formatAmount(settlement.invoiceAmount) : '—'
+            }}</span>
+          </div>
+        </div>
+        <div v-if="settlement.invoiceAttachmentUrls" class="info-kv-item info-kv-item--full">
+          <span class="info-kv-label">发票附件</span>
           <AttachmentUploadField
-            v-model="voucherUrlsStr"
-            folder="pl/freight-voucher"
-            hint="请上传汇款凭证,至少 1 个附件"
-          />
-        </el-form-item>
-        <el-form-item label="备注" prop="remark" class="form-col-span-2">
-          <el-input
-            v-model="form.remark"
-            type="textarea"
-            :rows="3"
-            placeholder="请输入备注"
-            maxlength="500"
-            show-word-limit
+            :model-value="settlement.invoiceAttachmentUrls"
+            disabled
           />
-        </el-form-item>
-      </div>
-    </el-form>
+        </div>
+      </section>
+
+      <section class="dialog-section">
+        <h3 class="dialog-section-title">发放登记</h3>
+        <el-form
+          ref="formRef"
+          :model="form"
+          :rules="rules"
+          label-position="top"
+          scroll-to-error
+          :scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
+        >
+          <div class="form-grid-2">
+            <el-form-item label="汇款日期" prop="remittanceDate">
+              <el-date-picker
+                v-model="form.remittanceDate"
+                type="date"
+                value-format="YYYY-MM-DD"
+                placeholder="选择汇款日期"
+                style="width: 100%"
+              />
+            </el-form-item>
+            <el-form-item label="汇款金额(元)" prop="remittanceAmount">
+              <el-input-number
+                v-model="form.remittanceAmount"
+                :min="0.01"
+                :precision="2"
+                :controls="false"
+                placeholder="请输入汇款金额"
+                style="width: 100%"
+              />
+            </el-form-item>
+            <el-form-item label="汇款凭证附件" prop="voucherUrls" class="form-col-span-2">
+              <AttachmentUploadField
+                ref="attachmentRef"
+                v-model="form.voucherUrls"
+                folder="pl/freight-voucher"
+                hint="请上传银行回单、转账截图等汇款凭证,至少 1 个附件"
+                @update:model-value="formRef?.validateField('voucherUrls')"
+              />
+            </el-form-item>
+            <el-form-item label="备注" prop="remark" class="form-col-span-2">
+              <el-input
+                v-model="form.remark"
+                type="textarea"
+                :rows="3"
+                placeholder="汇款金额与运费合计不一致时请填写说明"
+                maxlength="500"
+                show-word-limit
+              />
+            </el-form-item>
+          </div>
+        </el-form>
+      </section>
+
+      <section v-if="settlement" class="dialog-section">
+        <h3 class="dialog-section-title">关联运输信息</h3>
+        <div class="panel dialog-list-panel">
+          <div class="list-body list-wrap dialog-list-body-only">
+            <el-table
+              :data="settlement.transports || []"
+              class="list-table"
+              stripe
+              empty-text="暂无数据"
+            >
+              <el-table-column type="index" label="序号" min-width="60" align="center" />
+              <el-table-column
+                prop="transportNo"
+                label="运输编号"
+                min-width="160"
+                show-overflow-tooltip
+              />
+              <el-table-column prop="licensePlate" label="车牌号" min-width="110" align="center">
+                <template #default="{ row }">{{ row.licensePlate || '—' }}</template>
+              </el-table-column>
+              <el-table-column prop="driverName" label="司机" min-width="90" align="center">
+                <template #default="{ row }">{{ row.driverName || '—' }}</template>
+              </el-table-column>
+              <el-table-column
+                prop="carrierName"
+                label="承运商"
+                min-width="120"
+                show-overflow-tooltip
+              >
+                <template #default="{ row }">{{ row.carrierName || '—' }}</template>
+              </el-table-column>
+              <el-table-column
+                prop="transportMedium"
+                label="运输介质"
+                min-width="100"
+                align="center"
+              >
+                <template #default="{ row }">{{ row.transportMedium || '—' }}</template>
+              </el-table-column>
+              <el-table-column prop="startPoint" label="出发地" min-width="140" show-overflow-tooltip>
+                <template #default="{ row }">{{ row.startPoint || '—' }}</template>
+              </el-table-column>
+              <el-table-column prop="endPoint" label="目的地" min-width="140" show-overflow-tooltip>
+                <template #default="{ row }">{{ row.endPoint || '—' }}</template>
+              </el-table-column>
+              <el-table-column prop="freightAmount" label="运费(元)" min-width="110" align="right">
+                <template #default="{ row }">{{ formatAmount(row.freightAmount) }}</template>
+              </el-table-column>
+            </el-table>
+          </div>
+        </div>
+      </section>
+    </div>
 
     <template #footer>
       <el-button @click="visible = false">
@@ -89,11 +185,12 @@ const loading = ref(false)
 const submitLoading = ref(false)
 const settlement = ref<FreightSettlement | null>(null)
 const formRef = ref<ElFormInstance>()
-const voucherUrlsStr = ref('')
+const attachmentRef = ref<InstanceType<typeof AttachmentUploadField>>()
 
 const form = reactive({
   remittanceDate: '',
   remittanceAmount: undefined as number | undefined,
+  voucherUrls: '',
   remark: ''
 })
 
@@ -102,12 +199,12 @@ const rules = {
   remittanceAmount: [{ required: true, message: '请输入汇款金额', trigger: 'blur' }],
   voucherUrls: [
     {
-      validator: (_rule: unknown, _value: unknown, callback: (err?: Error) => void) => {
-        const urls = voucherUrlsStr.value
+      validator: (_rule: unknown, value: string, callback: (err?: Error) => void) => {
+        const urls = String(value || '')
           .split(',')
           .map((s) => s.trim())
           .filter(Boolean)
-        if (!urls.length) callback(new Error('请上传至少一个汇款凭证'))
+        if (!urls.length) callback(new Error('请上传至少一个汇款凭证附件'))
         else callback()
       },
       trigger: 'change'
@@ -120,13 +217,18 @@ function formatAmount(amount?: number) {
   return formatMoneyDisplay(amount)
 }
 
-async function open(id: string) {
-  visible.value = true
-  loading.value = true
+function resetForm() {
   form.remittanceDate = ''
   form.remittanceAmount = undefined
+  form.voucherUrls = ''
   form.remark = ''
-  voucherUrlsStr.value = ''
+}
+
+async function open(id: string) {
+  visible.value = true
+  loading.value = true
+  resetForm()
+  settlement.value = null
   try {
     const res = await freightApi.get(id)
     settlement.value = (res.data ?? res) as FreightSettlement
@@ -138,8 +240,12 @@ async function open(id: string) {
 
 async function handleSubmit() {
   if (!settlement.value) return
+  if (attachmentRef.value?.hasUploading?.()) {
+    ElMessage.warning('附件上传中,请稍候')
+    return
+  }
   await formRef.value?.validate()
-  const voucherUrls = voucherUrlsStr.value
+  const voucherUrls = form.voucherUrls
     .split(',')
     .map((s) => s.trim())
     .filter(Boolean)
@@ -161,3 +267,103 @@ async function handleSubmit() {
 
 defineExpose({ open })
 </script>
+
+<style scoped lang="scss">
+.disburse-dialog-body {
+  min-height: 120px;
+}
+
+.dialog-section {
+  margin-bottom: 20px;
+
+  &:last-child {
+    margin-bottom: 0;
+  }
+}
+
+.dialog-section-title {
+  margin: 0 0 12px;
+  font-size: 14px;
+  font-weight: 600;
+}
+
+.summary-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 12px 24px;
+  padding: 16px;
+  border-radius: 10px;
+  background: var(--primary-bg, #ecfdf5);
+  border: 1px solid #a7f3d0;
+}
+
+.summary-item {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  min-width: 0;
+}
+
+.summary-label {
+  font-size: 12px;
+  color: var(--text-muted, #64748b);
+}
+
+.summary-value {
+  font-size: 14px;
+  color: var(--text, #1e293b);
+  word-break: break-all;
+
+  &--amount {
+    font-size: 18px;
+    font-weight: 700;
+    color: var(--primary-dark, #059669);
+  }
+}
+
+.info-kv-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 12px 24px;
+
+  &--cols-3 {
+    grid-template-columns: repeat(3, minmax(0, 1fr));
+  }
+}
+
+.info-kv-item {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  min-width: 0;
+
+  &--full {
+    margin-top: 12px;
+  }
+}
+
+.info-kv-label {
+  font-size: 12px;
+  color: var(--text-muted, #64748b);
+}
+
+.info-kv-value {
+  font-size: 14px;
+  color: var(--text, #1e293b);
+  word-break: break-all;
+}
+
+@media (max-width: 900px) {
+  .info-kv-grid--cols-3 {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+}
+
+@media (max-width: 640px) {
+  .summary-grid,
+  .info-kv-grid,
+  .info-kv-grid--cols-3 {
+    grid-template-columns: 1fr;
+  }
+}
+</style>

+ 86 - 22
web-admin/src/components/business/pl-freight-settlement-form-dialog/index.vue

@@ -1,8 +1,8 @@
 <template>
   <el-dialog
     v-model="visible"
-    title="编辑结算单"
-    width="720px"
+    :title="dialogTitle"
+    width="1200px"
     align-center
     append-to-body
     destroy-on-close
@@ -26,6 +26,8 @@
         <el-form-item label="运费合计(元)">
           <el-input :model-value="formatAmount(settlement?.totalFreight)" disabled />
         </el-form-item>
+      </div>
+      <div class="form-grid-3">
         <el-form-item label="发票号" prop="invoiceNo">
           <el-input v-model="form.invoiceNo" placeholder="请输入发票号" clearable />
         </el-form-item>
@@ -48,30 +50,77 @@
             style="width: 100%"
           />
         </el-form-item>
-        <el-form-item label="备注" prop="remark" class="form-col-span-2">
-          <el-input
-            v-model="form.remark"
-            type="textarea"
-            :rows="3"
-            placeholder="请输入备注"
-            maxlength="500"
-            show-word-limit
-          />
-        </el-form-item>
       </div>
+      <el-form-item label="发票附件" prop="invoiceAttachmentUrls" class="form-col-span-2">
+        <AttachmentUploadField
+          ref="attachmentRef"
+          v-model="invoiceAttachmentUrlsStr"
+          folder="pl/freight-invoice"
+          hint="支持上传发票扫描件或电子发票,可多文件"
+        />
+      </el-form-item>
+      <el-form-item label="备注" prop="remark">
+        <el-input
+          v-model="form.remark"
+          type="textarea"
+          :rows="3"
+          placeholder="请输入备注"
+          maxlength="500"
+          show-word-limit
+        />
+      </el-form-item>
     </el-form>
 
     <section class="dialog-section">
-      <h3 class="dialog-section-title">关联运输明细</h3>
-      <div class="list-body list-wrap dialog-list-body-only">
-        <el-table :data="settlement?.transports || []" class="list-table" stripe empty-text="暂无数据">
-          <el-table-column type="index" label="序号" min-width="60" align="center" />
-          <el-table-column prop="transportNo" label="运输编号" min-width="160" show-overflow-tooltip />
-          <el-table-column prop="licensePlate" label="车牌号" min-width="120" align="center" />
-          <el-table-column prop="freightAmount" label="运费(元)" min-width="120" align="right">
-            <template #default="{ row }">{{ formatAmount(row.freightAmount) }}</template>
-          </el-table-column>
-        </el-table>
+      <h3 class="dialog-section-title">关联运输信息</h3>
+      <div class="panel dialog-list-panel">
+        <div class="list-body list-wrap dialog-list-body-only">
+          <el-table
+            :data="settlement?.transports || []"
+            class="list-table"
+            stripe
+            empty-text="暂无数据"
+          >
+            <el-table-column type="index" label="序号" min-width="60" align="center" />
+            <el-table-column
+              prop="transportNo"
+              label="运输编号"
+              min-width="160"
+              show-overflow-tooltip
+            />
+            <el-table-column prop="licensePlate" label="车牌号" min-width="110" align="center">
+              <template #default="{ row }">{{ row.licensePlate || '—' }}</template>
+            </el-table-column>
+            <el-table-column prop="driverName" label="司机" min-width="90" align="center">
+              <template #default="{ row }">{{ row.driverName || '—' }}</template>
+            </el-table-column>
+            <el-table-column
+              prop="carrierName"
+              label="承运商"
+              min-width="120"
+              show-overflow-tooltip
+            >
+              <template #default="{ row }">{{ row.carrierName || '—' }}</template>
+            </el-table-column>
+            <el-table-column
+              prop="transportMedium"
+              label="运输介质"
+              min-width="100"
+              align="center"
+            >
+              <template #default="{ row }">{{ row.transportMedium || '—' }}</template>
+            </el-table-column>
+            <el-table-column prop="startPoint" label="出发地" min-width="140" show-overflow-tooltip>
+              <template #default="{ row }">{{ row.startPoint || '—' }}</template>
+            </el-table-column>
+            <el-table-column prop="endPoint" label="目的地" min-width="140" show-overflow-tooltip>
+              <template #default="{ row }">{{ row.endPoint || '—' }}</template>
+            </el-table-column>
+            <el-table-column prop="freightAmount" label="运费(元)" min-width="110" align="right">
+              <template #default="{ row }">{{ formatAmount(row.freightAmount) }}</template>
+            </el-table-column>
+          </el-table>
+        </div>
       </div>
     </section>
 
@@ -89,6 +138,7 @@
 <script setup lang="ts">
 import { Check, Close } from '@element-plus/icons-vue'
 import { freightApi, type FreightSettlement } from '@/api/pl'
+import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
 import { formatMoneyDisplay } from '@/utils/money-format'
 
 const emit = defineEmits<{ success: [] }>()
@@ -98,6 +148,8 @@ const loading = ref(false)
 const submitLoading = ref(false)
 const settlement = ref<FreightSettlement | null>(null)
 const formRef = ref<ElFormInstance>()
+const attachmentRef = ref<InstanceType<typeof AttachmentUploadField>>()
+const invoiceAttachmentUrlsStr = ref('')
 
 const form = reactive({
   invoiceNo: '',
@@ -108,6 +160,11 @@ const form = reactive({
 
 const rules = {}
 
+const dialogTitle = computed(() => {
+  if (settlement.value?.settlementStatus === 'approved') return '发票信息'
+  return '编辑结算单'
+})
+
 function formatAmount(amount?: number) {
   if (!amount) return '¥0.00'
   return formatMoneyDisplay(amount)
@@ -116,6 +173,7 @@ function formatAmount(amount?: number) {
 async function open(id: string) {
   visible.value = true
   loading.value = true
+  invoiceAttachmentUrlsStr.value = ''
   try {
     const res = await freightApi.get(id)
     settlement.value = (res.data ?? res) as FreightSettlement
@@ -123,6 +181,7 @@ async function open(id: string) {
     form.invoiceDate = settlement.value.invoiceDate || ''
     form.invoiceAmount = settlement.value.invoiceAmount
     form.remark = settlement.value.remark || ''
+    invoiceAttachmentUrlsStr.value = settlement.value.invoiceAttachmentUrls || ''
   } finally {
     loading.value = false
   }
@@ -130,12 +189,17 @@ async function open(id: string) {
 
 async function handleSubmit() {
   if (!settlement.value) return
+  if (attachmentRef.value?.hasUploading?.()) {
+    ElMessage.warning('附件上传中,请稍候')
+    return
+  }
   submitLoading.value = true
   try {
     await freightApi.update(settlement.value.id, {
       invoiceNo: form.invoiceNo || undefined,
       invoiceDate: form.invoiceDate || undefined,
       invoiceAmount: form.invoiceAmount,
+      invoiceAttachmentUrls: invoiceAttachmentUrlsStr.value || undefined,
       remark: form.remark || undefined
     })
     ElMessage.success('保存成功')

+ 141 - 29
web-admin/src/components/business/pl-freight-settlement-view-dialog/index.vue

@@ -2,7 +2,7 @@
   <el-dialog
     v-model="visible"
     :title="settlement?.settlementNo || '结算单详情'"
-    width="calc(100vw - 100px)"
+    width="1200px"
     align-center
     append-to-body
     destroy-on-close
@@ -25,7 +25,7 @@
 
       <section v-if="settlement" class="view-section">
         <h4 class="view-section-title">发票信息</h4>
-        <div class="view-kv-grid">
+        <div class="view-kv-grid view-kv-grid--cols-3">
           <div class="view-kv-item">
             <span class="view-kv-label">发票号</span>
             <span class="view-kv-value">{{ settlement.invoiceNo || '—' }}</span>
@@ -40,11 +40,22 @@
               settlement.invoiceAmount ? formatAmount(settlement.invoiceAmount) : '—'
             }}</span>
           </div>
+        </div>
+        <div class="view-kv-item view-kv-item--full">
+          <span class="view-kv-label">发票附件</span>
+          <AttachmentUploadField
+            v-if="settlement.invoiceAttachmentUrls"
+            :model-value="settlement.invoiceAttachmentUrls"
+            disabled
+          />
+          <span v-else class="view-kv-value">—</span>
+        </div>
+        <div class="view-kv-grid view-kv-grid--cols-2 view-kv-grid--mt">
           <div class="view-kv-item">
             <span class="view-kv-label">创建时间</span>
             <span class="view-kv-value">{{ formatDateTime(settlement.createTime) }}</span>
           </div>
-          <div class="view-kv-item view-kv-item--full">
+          <div class="view-kv-item">
             <span class="view-kv-label">备注</span>
             <span class="view-kv-value">{{ settlement.remark || '—' }}</span>
           </div>
@@ -52,17 +63,55 @@
       </section>
 
       <section v-if="settlement" class="view-section">
-        <h4 class="view-section-title">关联运输明细</h4>
-        <div class="list-body list-wrap dialog-list-body-only">
-          <el-table :data="settlement.transports || []" class="list-table" stripe empty-text="暂无数据">
-            <el-table-column type="index" label="序号" min-width="60" align="center" />
-            <el-table-column prop="transportNo" label="运输编号" min-width="160" show-overflow-tooltip />
-            <el-table-column prop="licensePlate" label="车牌号" min-width="120" align="center" />
-            <el-table-column prop="driverName" label="司机" min-width="100" align="center" />
-            <el-table-column prop="freightAmount" label="运费(元)" min-width="120" align="right">
-              <template #default="{ row }">{{ formatAmount(row.freightAmount) }}</template>
-            </el-table-column>
-          </el-table>
+        <h4 class="view-section-title">关联运输信息</h4>
+        <div class="panel dialog-list-panel">
+          <div class="list-body list-wrap dialog-list-body-only">
+            <el-table
+              :data="settlement.transports || []"
+              class="list-table"
+              stripe
+              empty-text="暂无数据"
+            >
+              <el-table-column type="index" label="序号" min-width="60" align="center" />
+              <el-table-column
+                prop="transportNo"
+                label="运输编号"
+                min-width="160"
+                show-overflow-tooltip
+              />
+              <el-table-column prop="licensePlate" label="车牌号" min-width="110" align="center">
+                <template #default="{ row }">{{ row.licensePlate || '—' }}</template>
+              </el-table-column>
+              <el-table-column prop="driverName" label="司机" min-width="90" align="center">
+                <template #default="{ row }">{{ row.driverName || '—' }}</template>
+              </el-table-column>
+              <el-table-column
+                prop="carrierName"
+                label="承运商"
+                min-width="120"
+                show-overflow-tooltip
+              >
+                <template #default="{ row }">{{ row.carrierName || '—' }}</template>
+              </el-table-column>
+              <el-table-column
+                prop="transportMedium"
+                label="运输介质"
+                min-width="100"
+                align="center"
+              >
+                <template #default="{ row }">{{ row.transportMedium || '—' }}</template>
+              </el-table-column>
+              <el-table-column prop="startPoint" label="出发地" min-width="140" show-overflow-tooltip>
+                <template #default="{ row }">{{ row.startPoint || '—' }}</template>
+              </el-table-column>
+              <el-table-column prop="endPoint" label="目的地" min-width="140" show-overflow-tooltip>
+                <template #default="{ row }">{{ row.endPoint || '—' }}</template>
+              </el-table-column>
+              <el-table-column prop="freightAmount" label="运费(元)" min-width="110" align="right">
+                <template #default="{ row }">{{ formatAmount(row.freightAmount) }}</template>
+              </el-table-column>
+            </el-table>
+          </div>
         </div>
       </section>
 
@@ -76,17 +125,42 @@
         class="view-section"
       >
         <h4 class="view-section-title">汇款凭证</h4>
-        <div class="list-body list-wrap dialog-list-body-only">
-          <el-table :data="settlement.vouchers" class="list-table" stripe>
-            <el-table-column prop="remittanceDate" label="汇款日期" min-width="120" align="center" />
-            <el-table-column prop="remittanceAmount" label="汇款金额" min-width="120" align="right">
-              <template #default="{ row }">{{ formatAmount(row.remittanceAmount) }}</template>
-            </el-table-column>
-            <el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
-            <el-table-column prop="createTime" label="登记时间" min-width="160" align="center">
-              <template #default="{ row }">{{ formatDateTime(row.createTime) }}</template>
-            </el-table-column>
-          </el-table>
+        <div
+          v-for="(voucher, index) in settlement.vouchers"
+          :key="voucher.id || index"
+          class="voucher-block"
+        >
+          <div class="view-kv-grid view-kv-grid--cols-3">
+            <div class="view-kv-item">
+              <span class="view-kv-label">汇款日期</span>
+              <span class="view-kv-value">{{ voucher.remittanceDate || '—' }}</span>
+            </div>
+            <div class="view-kv-item">
+              <span class="view-kv-label">汇款金额</span>
+              <span class="view-kv-value">{{
+                voucher.remittanceAmount ? formatAmount(voucher.remittanceAmount) : '—'
+              }}</span>
+            </div>
+            <div class="view-kv-item">
+              <span class="view-kv-label">登记时间</span>
+              <span class="view-kv-value">{{
+                voucher.createTime ? formatDateTime(voucher.createTime) : '—'
+              }}</span>
+            </div>
+          </div>
+          <div v-if="voucher.remark" class="view-kv-item view-kv-item--full">
+            <span class="view-kv-label">备注</span>
+            <span class="view-kv-value">{{ voucher.remark }}</span>
+          </div>
+          <div class="view-kv-item view-kv-item--full">
+            <span class="view-kv-label">汇款凭证附件</span>
+            <AttachmentUploadField
+              v-if="normalizeVoucherUrls(voucher.voucherUrls)"
+              :model-value="normalizeVoucherUrls(voucher.voucherUrls)"
+              disabled
+            />
+            <span v-else class="view-kv-value">—</span>
+          </div>
         </div>
       </section>
     </div>
@@ -103,6 +177,8 @@
 import { Close } from '@element-plus/icons-vue'
 import { freightApi, type FreightSettlement } from '@/api/pl'
 import ApprovalFlowView from '@/components/bx/base/ApprovalFlowView.vue'
+import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
+import { normalizeAttachmentUrls } from '@/utils/download-attachment'
 import {
   getSettlementStatusLabel,
   getSettlementStatusTagType,
@@ -124,6 +200,10 @@ function formatAmount(amount?: number) {
   return formatMoneyDisplay(amount)
 }
 
+function normalizeVoucherUrls(raw?: string) {
+  return normalizeAttachmentUrls(raw)
+}
+
 async function open(id: string) {
   visible.value = true
   loading.value = true
@@ -199,16 +279,29 @@ defineExpose({ open })
   display: grid;
   grid-template-columns: repeat(2, minmax(0, 1fr));
   gap: 12px 24px;
+
+  &--cols-3 {
+    grid-template-columns: repeat(3, minmax(0, 1fr));
+  }
+
+  &--cols-2 {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
+  &--mt {
+    margin-top: 12px;
+  }
 }
 
 .view-kv-item {
   display: flex;
   flex-direction: column;
   gap: 4px;
-}
+  min-width: 0;
 
-.view-kv-item--full {
-  grid-column: 1 / -1;
+  &--full {
+    margin-top: 12px;
+  }
 }
 
 .view-kv-label {
@@ -222,8 +315,27 @@ defineExpose({ open })
   word-break: break-all;
 }
 
+.voucher-block {
+  padding: 16px;
+  border: 1px solid var(--border, #d9e8e2);
+  border-radius: 10px;
+  background: #fff;
+
+  & + & {
+    margin-top: 12px;
+  }
+}
+
+@media (max-width: 900px) {
+  .view-kv-grid--cols-3 {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+}
+
 @media (max-width: 640px) {
-  .view-kv-grid {
+  .view-kv-grid,
+  .view-kv-grid--cols-3,
+  .view-kv-grid--cols-2 {
     grid-template-columns: 1fr;
   }
 }

+ 5 - 3
web-admin/src/components/business/pl-plan-arrange-dialog/index.vue

@@ -313,18 +313,20 @@
               copyPlanSummary({
                 pickupAddress: formData.pickupAddress,
                 destinationAddress: formData.destinationAddress,
+                pickupNotes: formData.pickupNotes,
+                destinationNotes: formData.destinationNotes,
                 licensePlate: vehicleList.find(v => v.id === formData.vehicleId)?.licensePlate,
                 driverUrl: arrangeResult?.driverUrl
               })
             "
           >
-            <el-icon><CopyDocument /></el-icon>复制计划简述
+            <el-icon><CopyDocument /></el-icon>复制给司机
           </el-button>
           <el-button
             v-if="hasSiteInventoryRecords"
             @click="copySiteSummary({ licensePlate: vehicleList.find(v => v.id === formData.vehicleId)?.licensePlate })"
           >
-            <el-icon><CopyDocument /></el-icon>复制现场简述
+            <el-icon><CopyDocument /></el-icon>复制给现场
           </el-button>
           <el-button
             v-if="hasSiteInventoryRecords && siteRecords.length > 1"
@@ -1058,7 +1060,7 @@ async function handleSubmit() {
 
 <style lang="scss">
 .el-dialog.pl-plan-arrange-dialog.dialog-form-layout {
-  width: min(1680px, calc(100vw - 80px)) !important;
+  width: min(1840px, calc(100vw - 48px)) !important;
   max-height: calc(100vh - 48px);
 }
 </style>

+ 7 - 1
web-admin/src/components/business/pl-plan-form-dialog/index.vue

@@ -74,7 +74,7 @@
           </div>
 
           <h4 class="pl-plan-subtitle">合同产品预览</h4>
-          <PlPlanFormProductTable :products="formData.products" />
+          <PlPlanFormProductTable :products="contractProductRows" />
         </template>
       </section>
 
@@ -281,6 +281,7 @@ import PlVehicleViewDialog from '@/components/business/pl-vehicle-view-dialog/in
 import PlPlanFormProductTable from './PlPlanFormProductTable.vue'
 import {
   applyContractToPlanForm,
+  mapContractProducts,
   type PlanFormProductRow
 } from '@/composables/usePlContractToPlan'
 import { enrichPlanProductsHazardFromCrm } from '@/composables/usePlToxicProductCertCheck'
@@ -464,6 +465,11 @@ const contractBasicFieldRows = computed(() => {
 const contractBasicRow1Fields = computed(() => contractBasicFieldRows.value.row1)
 const contractBasicRow2Fields = computed(() => contractBasicFieldRows.value.row2)
 
+/** 合同产品预览:固定展示合同原始明细,与下方可编辑的计划产品分离 */
+const contractProductRows = computed<PlanFormProductRow[]>(() =>
+  mapContractProducts(contractSnapshot.value?.products)
+)
+
 function resolveSelfCarrierId() {
   const matched = carrierList.value.find(item => item.carrierName === SELF_CARRIER_NAME)
   formData.carrierId = matched?.id || ''

+ 2 - 2
web-admin/src/components/business/pl-plan-view-dialog/index.vue

@@ -65,10 +65,10 @@
       <div class="dialog-footer pl-plan-view-footer">
         <template v-if="showArrangeActions">
           <el-button @click="copyPlanSummary()">
-            <el-icon><CopyDocument /></el-icon>复制计划简述
+            <el-icon><CopyDocument /></el-icon>复制给司机
           </el-button>
           <el-button v-if="hasSiteInventoryRecords" @click="copySiteSummary()">
-            <el-icon><CopyDocument /></el-icon>复制现场简述
+            <el-icon><CopyDocument /></el-icon>复制给现场
           </el-button>
           <el-button
             v-if="hasSiteInventoryRecords && siteRecords.length > 1"

+ 10 - 3
web-admin/src/components/business/pl-plan-view-tabs/index.vue

@@ -135,7 +135,7 @@
                   <div class="pl-plan-dibang-head-actions">
                     <el-button class="btn-op-item" @click="copySiteSummaryForRow(row)">
                       <el-icon><CopyDocument /></el-icon>
-                      复制现场简述
+                      复制给现场
                     </el-button>
                     <el-button class="btn-op-item" @click="copySiteLinkForRow(row)">
                       <el-icon><CopyDocument /></el-icon>
@@ -374,6 +374,7 @@
     buildSiteSummaryFromInventoryLinks
   } from '@/utils/db-site-summary'
   import {
+    formatPlanProductListText,
     getPlFreightTypeLabel,
     getPlPlanCompanyLabel,
     getPlPlanTypeLabel,
@@ -492,15 +493,21 @@
       ElMessage.warning('暂无现场链接')
       return
     }
+    const product = products.value.find(item => item.id === row.link.planProductId)
+    const productText = product
+      ? formatPlanProductListText(product, unit => dictStore.getLabel('crm_product_unit', unit) || unit, {
+          usePlannedQuantity: true
+        })
+      : row.link.productName || '—'
     const text = buildSiteSummaryFromInventoryLinks({
       planNo: row.link.planNo || plan.value?.planNo,
       licensePlate: plan.value?.licensePlate || transport.value?.licensePlate,
       pickupTime: plan.value?.pickupTime,
       planDate: plan.value?.planDate,
-      records: [{ ...row.link, siteUrl: url }]
+      records: [{ ...row.link, siteUrl: url, productText }]
     })
     await navigator.clipboard.writeText(text)
-    ElMessage.success('已复制现场简述')
+    ElMessage.success('已复制给现场')
   }
 
   const displayFeedbackTime = computed(() => formatDateTime(transport.value?.feedbackTime) || '—')

+ 3 - 0
web-admin/src/components/business/pl-vehicle-summary-strip/index.vue

@@ -6,6 +6,9 @@
         <span class="pl-vehicle-summary-value pl-vehicle-summary-value--strong">
           {{ detail.licensePlate || '—' }}
         </span>
+        <span v-if="detail.trailerNo" class="pl-vehicle-summary-value">
+          {{ detail.trailerNo }}(挂车)
+        </span>
         <el-tag :type="getPlVehicleTypeTagType(detail.vehicleType)" size="small">
           {{ getPlVehicleTypeLabel(detail.vehicleType) }}
         </el-tag>

+ 1 - 0
web-admin/src/components/business/pl-vehicle-view-dialog/index.vue

@@ -80,6 +80,7 @@ const viewSections = computed<ViewSection[]>(() => {
       tableMode: 'quad',
       fields: [
         { label: '车牌号', value: d.licensePlate },
+        { label: '挂车号', value: d.trailerNo || '—' },
         {
           label: '车辆类型',
           value: getPlVehicleTypeLabel(d.vehicleType),

+ 1 - 0
web-admin/src/components/business/pl-vehicle-view-panel/index.vue

@@ -67,6 +67,7 @@ const vehicleViewFields = computed<ViewField[]>(() => {
   const d = props.detail
   return [
     { label: '车牌号', value: d.licensePlate },
+    { label: '挂车号', value: d.trailerNo || '—' },
     {
       label: '车辆类型',
       value: getPlVehicleTypeLabel(d.vehicleType),

+ 23 - 4
web-admin/src/composables/usePlPlanArrangeActions.ts

@@ -110,6 +110,8 @@ export function usePlPlanArrangeActions() {
     endPoint?: string
     pickupAddress?: string
     destinationAddress?: string
+    pickupNotes?: string
+    destinationNotes?: string
     licensePlate?: string
     driverUrl?: string
   }) {
@@ -118,7 +120,9 @@ export function usePlPlanArrangeActions() {
     lines.push(
       `时间:${formatDateTime(plan.value?.pickupTime) || plan.value?.planDate || '—'}`
     )
-    const productText = products.value.map(p => formatPlanProductListText(p, getUnitLabel)).join(';')
+    const productText = products.value
+      .map(p => formatPlanProductListText(p, getUnitLabel, { usePlannedQuantity: true }))
+      .join(';')
     lines.push(`产品:${productText || '—'}`)
     lines.push(
       `路线:${options?.pickupAddress || plan.value?.pickupAddress || options?.startPoint || '—'} → ${
@@ -129,6 +133,8 @@ export function usePlPlanArrangeActions() {
         '—'
       }`
     )
+    lines.push(`取货注意事项:${options?.pickupNotes || plan.value?.pickupNotes || '—'}`)
+    lines.push(`送货注意事项:${options?.destinationNotes || plan.value?.destinationNotes || '—'}`)
     lines.push(`司机链接:${options?.driverUrl || driverUrl.value || '—'}`)
     return lines.join('\n')
   }
@@ -168,13 +174,24 @@ export function usePlPlanArrangeActions() {
     ElMessage.success('已复制全部现场链接')
   }
 
+  function resolveSiteRecordProductText(record: TransportSiteLink) {
+    const product = products.value.find(item => item.id === record.planProductId)
+    if (product) {
+      return formatPlanProductListText(product, getUnitLabel, { usePlannedQuantity: true })
+    }
+    return record.productName || '—'
+  }
+
   function buildSiteSummaryText(options?: { licensePlate?: string }) {
     return buildSiteSummaryFromInventoryLinks({
       planNo: plan.value?.planNo,
       licensePlate: options?.licensePlate || licensePlate.value || plan.value?.licensePlate,
       pickupTime: plan.value?.pickupTime,
       planDate: plan.value?.planDate,
-      records: siteRecords.value
+      records: siteRecords.value.map(record => ({
+        ...record,
+        productText: resolveSiteRecordProductText(record)
+      }))
     })
   }
 
@@ -184,7 +201,7 @@ export function usePlPlanArrangeActions() {
       return
     }
     await navigator.clipboard.writeText(buildSiteSummaryText(options))
-    ElMessage.success('已复制现场简述')
+    ElMessage.success('已复制给现场')
   }
 
   async function copyPlanSummary(options?: {
@@ -192,11 +209,13 @@ export function usePlPlanArrangeActions() {
     endPoint?: string
     pickupAddress?: string
     destinationAddress?: string
+    pickupNotes?: string
+    destinationNotes?: string
     licensePlate?: string
     driverUrl?: string
   }) {
     await navigator.clipboard.writeText(buildPlanSummaryText(options))
-    ElMessage.success('已复制计划简述')
+    ElMessage.success('已复制给司机')
   }
 
   function resolveInventoryUnitLabel(record: TransportInventoryLink) {

+ 21 - 7
web-admin/src/router/modules/pl.ts

@@ -18,13 +18,6 @@ export const plRoutes: AppRouteRecord[] = [
         component: '/pl/carrier/index',
         meta: { title: '承运商管理', keepAlive: true, menuCode: 'pl:carrier:list' }
       },
-      {
-        id: 'PL00000000000000000000000000003',
-        name: 'PlVehicle',
-        path: 'vehicle',
-        component: '/pl/vehicle/index',
-        meta: { title: '车辆管理', keepAlive: true, menuCode: 'pl:vehicle:list' }
-      },
       {
         id: 'PL00000000000000000000000000003S',
         name: 'PlSelfVehicle',
@@ -32,6 +25,13 @@ export const plRoutes: AppRouteRecord[] = [
         component: '/pl/self-vehicle/index',
         meta: { title: '山峡自有车辆', keepAlive: true, menuCode: 'pl:self-vehicle:list' }
       },
+      {
+        id: 'PL00000000000000000000000000003',
+        name: 'PlVehicle',
+        path: 'vehicle',
+        component: '/pl/vehicle/index',
+        meta: { title: '车辆管理', keepAlive: true, menuCode: 'pl:vehicle:list' }
+      },
       {
         id: 'PL00000000000000000000000000004',
         name: 'PlPersonnel',
@@ -109,6 +109,20 @@ export const plRoutes: AppRouteRecord[] = [
         component: '/pl/freight/index',
         meta: { title: '运费结算', keepAlive: true, menuCode: 'pl:freight:list' }
       },
+      {
+        id: 'PL00000000000000000000000000031',
+        name: 'PlFreightApproval',
+        path: 'freight-approval',
+        component: '/pl/freight-approval/index',
+        meta: { title: '运费结算审批', keepAlive: true, menuCode: 'pl:freight-approval:list' }
+      },
+      {
+        id: 'PL00000000000000000000000000032',
+        name: 'PlFreightDisburse',
+        path: 'freight-disburse',
+        component: '/pl/freight-disburse/index',
+        meta: { title: '运费财务发放', keepAlive: true, menuCode: 'pl:freight-disburse:list' }
+      },
       {
         id: 'PL00000000000000000000000000040',
         name: 'PlInventory',

+ 7 - 2
web-admin/src/utils/db-site-summary.ts

@@ -6,11 +6,16 @@ const SITE_INSTRUCTION = '说明:请打开链接上传现场附件并填写备
 
 type SiteSummaryRecord = {
   productName?: string
+  productText?: string
   batchNo?: string
   planNo?: string
   siteUrl?: string
 }
 
+function formatSiteSummaryProductLine(record: SiteSummaryRecord) {
+  return record.productText || record.productName || '—'
+}
+
 export function buildSiteSummaryFromInventoryLinks(options: {
   planNo?: string
   licensePlate?: string
@@ -33,13 +38,13 @@ export function buildSiteSummaryFromInventoryLinks(options: {
 
   if (validRecords.length === 1) {
     const record = validRecords[0]
-    lines.push(`产品:${record.productName || '—'}`)
+    lines.push(`产品:${formatSiteSummaryProductLine(record)}`)
     if (record.batchNo) lines.push(`出入库单号:${record.batchNo}`)
     lines.push(`现场链接:${record.siteUrl}`)
   } else {
     lines.push('')
     validRecords.forEach((record, index) => {
-      lines.push(`【${index + 1}】${record.productName || '产品'}`)
+      lines.push(`【${index + 1}】${formatSiteSummaryProductLine(record)}`)
       if (record.batchNo) lines.push(`出入库单号:${record.batchNo}`)
       lines.push(`现场链接:${record.siteUrl}`)
       if (index < validRecords.length - 1) lines.push('')

+ 18 - 0
web-admin/src/utils/download-attachment.ts

@@ -6,6 +6,24 @@ export type AttachmentDownloadItem = {
   fileName?: string
 }
 
+/** 将 JSON 数组或逗号分隔的附件 URL 统一为逗号分隔字符串 */
+export function normalizeAttachmentUrls(raw?: string | null): string {
+  if (!raw) return ''
+  const trimmed = String(raw).trim()
+  if (!trimmed) return ''
+  if (trimmed.startsWith('[')) {
+    try {
+      const parsed = JSON.parse(trimmed)
+      if (Array.isArray(parsed)) {
+        return parsed.map((url) => String(url).trim()).filter(Boolean).join(',')
+      }
+    } catch {
+      /* ignore */
+    }
+  }
+  return trimmed
+}
+
 function triggerAnchorDownload(href: string, fileName: string) {
   const a = document.createElement('a')
   a.href = href

+ 13 - 2
web-admin/src/utils/pl-display.ts

@@ -51,6 +51,14 @@ export function getPlanProductQuantity(product: {
   return String(qty)
 }
 
+export function getPlanProductPlannedQuantity(product: {
+  quantity?: number | string | null
+}) {
+  const qty = product.quantity
+  if (qty === undefined || qty === null || qty === '') return '—'
+  return String(qty)
+}
+
 export function formatPlanProductListText(
   product: {
     productName?: string
@@ -62,11 +70,14 @@ export function formatPlanProductListText(
     inboundEstimateQty?: number | string | null
     outboundEstimateQty?: number | string | null
   },
-  getUnitLabel?: (unit: string) => string
+  getUnitLabel?: (unit: string) => string,
+  options?: { usePlannedQuantity?: boolean }
 ) {
   const name = product.productName || '—'
   const spec = getPlanProductSpec(product)
-  const qty = getPlanProductQuantity(product)
+  const qty = options?.usePlannedQuantity
+    ? getPlanProductPlannedQuantity(product)
+    : getPlanProductQuantity(product)
   const unitRaw = product.unit || ''
   const unit = unitRaw ? (getUnitLabel ? getUnitLabel(unitRaw) || unitRaw : unitRaw) : ''
 

+ 41 - 0
web-admin/src/views/pl/freight-approval/index.vue

@@ -0,0 +1,41 @@
+<!-- 运费结算审批 -->
+<template>
+  <div class="page-container">
+    <section class="panel">
+      <div class="panel-top">
+        <h2 class="title-bar-heading">运费结算审批</h2>
+      </div>
+
+      <div class="panel-tabs">
+        <el-tabs v-model="activeTab" @tab-change="handleTabChange">
+          <el-tab-pane label="待审批" name="pending">
+            <ApprovalPendingPanel ref="pendingRef" />
+          </el-tab-pane>
+          <el-tab-pane label="已审批" name="done">
+            <ApprovalDonePanel ref="doneRef" />
+          </el-tab-pane>
+        </el-tabs>
+      </div>
+    </section>
+  </div>
+</template>
+
+<script setup lang="ts">
+import ApprovalPendingPanel from '../freight/components/ApprovalPendingPanel.vue'
+import ApprovalDonePanel from '../freight/components/ApprovalDonePanel.vue'
+
+type ApprovalTab = 'pending' | 'done'
+
+const activeTab = ref<ApprovalTab>('pending')
+const pendingRef = ref<InstanceType<typeof ApprovalPendingPanel>>()
+const doneRef = ref<InstanceType<typeof ApprovalDonePanel>>()
+
+function refreshTab(name: ApprovalTab) {
+  if (name === 'pending') pendingRef.value?.refresh()
+  else doneRef.value?.refresh()
+}
+
+function handleTabChange(name: string | number) {
+  refreshTab(String(name) as ApprovalTab)
+}
+</script>

+ 47 - 0
web-admin/src/views/pl/freight-disburse/index.vue

@@ -0,0 +1,47 @@
+<!-- 运费财务发放 -->
+<template>
+  <div class="page-container">
+    <section class="panel">
+      <div class="panel-top">
+        <h2 class="title-bar-heading">运费财务发放</h2>
+      </div>
+
+      <div class="panel-tabs">
+        <el-tabs v-model="activeTab" @tab-change="handleTabChange">
+          <el-tab-pane label="待发放" name="disburse">
+            <DisbursePanel ref="disburseRef" @disbursed="handleDisbursed" />
+          </el-tab-pane>
+          <el-tab-pane label="已发放" name="settled">
+            <SettledPanel ref="settledRef" />
+          </el-tab-pane>
+        </el-tabs>
+      </div>
+    </section>
+  </div>
+</template>
+
+<script setup lang="ts">
+import DisbursePanel from '../freight/components/DisbursePanel.vue'
+import SettledPanel from '../freight/components/SettledPanel.vue'
+
+type DisburseTab = 'disburse' | 'settled'
+
+const activeTab = ref<DisburseTab>('disburse')
+const disburseRef = ref<InstanceType<typeof DisbursePanel>>()
+const settledRef = ref<InstanceType<typeof SettledPanel>>()
+
+function refreshTab(name: DisburseTab) {
+  if (name === 'disburse') disburseRef.value?.refresh()
+  else settledRef.value?.refresh()
+}
+
+function handleTabChange(name: string | number) {
+  refreshTab(String(name) as DisburseTab)
+}
+
+function handleDisbursed() {
+  nextTick(() => {
+    settledRef.value?.refresh()
+  })
+}
+</script>

+ 176 - 0
web-admin/src/views/pl/freight/components/ApprovalDonePanel.vue

@@ -0,0 +1,176 @@
+<template>
+  <div class="freight-tab-panel">
+    <div class="filter-block">
+      <el-form :model="searchForm" class="filter-form" @submit.prevent>
+        <div class="filter-grid">
+          <el-form-item label="结算单号">
+            <el-input
+              v-model="searchForm.q"
+              placeholder="搜索结算单号"
+              clearable
+              @keyup.enter="handleSearch"
+            />
+          </el-form-item>
+          <el-form-item label="结算状态">
+            <el-select v-model="searchForm.settlementStatus" placeholder="全部" clearable>
+              <el-option label="待发放" value="approved" />
+              <el-option label="已发放" value="confirmed" />
+            </el-select>
+          </el-form-item>
+        </div>
+        <div class="filter-actions">
+          <div class="filter-actions-end">
+            <el-button class="btn-ghost" :loading="loading" @click="handleReset">
+              <el-icon><RefreshLeft /></el-icon>重置
+            </el-button>
+            <el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
+              <el-icon><Search /></el-icon>搜索
+            </el-button>
+          </div>
+        </div>
+      </el-form>
+    </div>
+
+    <div class="list-toolbar">
+      <div class="list-toolbar-row">
+        <div class="list-toolbar-selected">已选 <span class="sel-num">0</span> 项</div>
+      </div>
+    </div>
+
+    <div class="list-body list-wrap">
+      <el-table class="list-table" :data="settlementList" v-loading="loading" stripe row-key="id">
+        <el-table-column prop="settlementNo" label="结算单号" min-width="180" show-overflow-tooltip>
+          <template #default="{ row }">
+            <el-link type="primary" @click="handleView(row)">{{ row.settlementNo }}</el-link>
+          </template>
+        </el-table-column>
+        <el-table-column prop="totalFreight" label="运费合计(元)" min-width="130" align="right">
+          <template #default="{ row }">{{ formatAmount(row.totalFreight) }}</template>
+        </el-table-column>
+        <el-table-column prop="planCount" label="关联计划数" min-width="110" align="center" />
+        <el-table-column prop="licensePlateSummary" label="车牌摘要" min-width="140" show-overflow-tooltip />
+        <el-table-column prop="settlementStatus" label="结算状态" min-width="100" align="center">
+          <template #default="{ row }">
+            <el-tag :type="getSettlementStatusTagType(row.settlementStatus)" size="small">
+              {{ getSettlementStatusLabel(row.settlementStatus) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="approvalFinishedTime" label="审批完成时间" min-width="160" align="center">
+          <template #default="{ row }">{{
+            row.approvalFinishedTime ? formatDateTime(row.approvalFinishedTime) : '—'
+          }}</template>
+        </el-table-column>
+        <el-table-column prop="submitterName" label="提交人" min-width="100" align="center">
+          <template #default="{ row }">{{ row.submitterName || '—' }}</template>
+        </el-table-column>
+        <el-table-column label="操作" min-width="120" fixed="right" align="center">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="handleView(row)">
+              <el-icon><View /></el-icon>查看
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div class="pagination list-pager-bar">
+        <el-pagination
+          v-model:current-page="pagination.page"
+          v-model:page-size="pagination.pageSize"
+          :total="pagination.total"
+          :page-sizes="[10, 20, 50, 100]"
+          layout="total, sizes, prev, pager, next"
+          @size-change="handleSizeChange"
+          @current-change="handlePageChange"
+        />
+      </div>
+    </div>
+
+    <PlFreightSettlementViewDialog ref="viewDialogRef" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { RefreshLeft, Search, View } from '@element-plus/icons-vue'
+import { freightApi, type FreightSettlement } from '@/api/pl'
+import PlFreightSettlementViewDialog from '@/components/business/pl-freight-settlement-view-dialog/index.vue'
+import {
+  getSettlementStatusLabel,
+  getSettlementStatusTagType
+} from '@/views/pl/freight/freight-status'
+import { formatMoneyDisplay } from '@/utils/money-format'
+import { formatDateTime } from '@/utils/format'
+
+const loading = ref(false)
+const settlementList = ref<FreightSettlement[]>([])
+const viewDialogRef = ref<InstanceType<typeof PlFreightSettlementViewDialog>>()
+
+const searchForm = reactive({
+  q: '',
+  settlementStatus: ''
+})
+
+const pagination = reactive({
+  page: 1,
+  pageSize: 20,
+  total: 0
+})
+
+function formatAmount(amount: number) {
+  if (!amount) return '¥0.00'
+  return formatMoneyDisplay(amount)
+}
+
+async function fetchData() {
+  loading.value = true
+  try {
+    const res = await freightApi.list({
+      tab: 'approval_done',
+      q: searchForm.q || undefined,
+      settlementStatus: searchForm.settlementStatus || undefined,
+      page: pagination.page,
+      pageSize: pagination.pageSize
+    })
+    const data = res.data ?? res
+    settlementList.value = data.list || []
+    pagination.total = data.total || 0
+  } finally {
+    loading.value = false
+  }
+}
+
+function handleSearch() {
+  pagination.page = 1
+  fetchData()
+}
+
+function handleReset() {
+  searchForm.q = ''
+  searchForm.settlementStatus = ''
+  pagination.page = 1
+  fetchData()
+}
+
+function handlePageChange() {
+  fetchData()
+}
+
+function handleSizeChange() {
+  pagination.page = 1
+  fetchData()
+}
+
+function handleView(row: FreightSettlement) {
+  viewDialogRef.value?.open(row.id)
+}
+
+function refresh() {
+  fetchData()
+}
+
+onMounted(() => {
+  fetchData()
+})
+
+defineExpose({ refresh })
+</script>

+ 184 - 0
web-admin/src/views/pl/freight/components/ApprovalPendingPanel.vue

@@ -0,0 +1,184 @@
+<template>
+  <div class="freight-tab-panel">
+    <div class="filter-block">
+      <el-form :model="searchForm" class="filter-form" @submit.prevent>
+        <div class="filter-grid">
+          <el-form-item label="结算单号">
+            <el-input
+              v-model="searchForm.q"
+              placeholder="搜索结算单号"
+              clearable
+              @keyup.enter="handleSearch"
+            />
+          </el-form-item>
+        </div>
+        <div class="filter-actions">
+          <div class="filter-actions-end">
+            <el-button class="btn-ghost" :loading="loading" @click="handleReset">
+              <el-icon><RefreshLeft /></el-icon>重置
+            </el-button>
+            <el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
+              <el-icon><Search /></el-icon>搜索
+            </el-button>
+          </div>
+        </div>
+      </el-form>
+    </div>
+
+    <div class="list-toolbar">
+      <div class="list-toolbar-row">
+        <div class="list-toolbar-selected">已选 <span class="sel-num">0</span> 项</div>
+      </div>
+    </div>
+
+    <div class="list-body list-wrap">
+      <el-table class="list-table" :data="settlementList" v-loading="loading" stripe row-key="id">
+        <el-table-column prop="settlementNo" label="结算单号" min-width="180" show-overflow-tooltip>
+          <template #default="{ row }">
+            <el-link type="primary" @click="handleView(row)">{{ row.settlementNo }}</el-link>
+          </template>
+        </el-table-column>
+        <el-table-column prop="totalFreight" label="运费合计(元)" min-width="130" align="right">
+          <template #default="{ row }">{{ formatAmount(row.totalFreight) }}</template>
+        </el-table-column>
+        <el-table-column prop="planCount" label="关联计划数" min-width="110" align="center" />
+        <el-table-column prop="licensePlateSummary" label="车牌摘要" min-width="140" show-overflow-tooltip />
+        <el-table-column prop="currentApproverName" label="当前审批人" min-width="110" align="center">
+          <template #default="{ row }">{{ row.currentApproverName || '—' }}</template>
+        </el-table-column>
+        <el-table-column prop="submitterName" label="提交人" min-width="100" align="center">
+          <template #default="{ row }">{{ row.submitterName || '—' }}</template>
+        </el-table-column>
+        <el-table-column prop="createTime" label="提交时间" min-width="160" align="center">
+          <template #default="{ row }">{{ formatDateTime(row.createTime) }}</template>
+        </el-table-column>
+        <el-table-column label="操作" min-width="260" fixed="right" align="center">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="handleView(row)">
+              <el-icon><View /></el-icon>查看
+            </el-button>
+            <el-button
+              v-if="row.canApprove && row.currentApprovalId"
+              link
+              type="success"
+              @click="handleApprove(row)"
+            >
+              <el-icon><CircleCheck /></el-icon>通过
+            </el-button>
+            <el-button
+              v-if="row.canApprove && row.currentApprovalId"
+              link
+              type="danger"
+              @click="handleReject(row)"
+            >
+              <el-icon><CircleClose /></el-icon>驳回
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div class="pagination list-pager-bar">
+        <el-pagination
+          v-model:current-page="pagination.page"
+          v-model:page-size="pagination.pageSize"
+          :total="pagination.total"
+          :page-sizes="[10, 20, 50, 100]"
+          layout="total, sizes, prev, pager, next"
+          @size-change="handleSizeChange"
+          @current-change="handlePageChange"
+        />
+      </div>
+    </div>
+
+    <PlFreightSettlementViewDialog ref="viewDialogRef" />
+    <PlFreightApprovalDialog ref="approvalDialogRef" @success="fetchData" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { CircleCheck, CircleClose, RefreshLeft, Search, View } from '@element-plus/icons-vue'
+import { freightApi, type FreightSettlement } from '@/api/pl'
+import PlFreightApprovalDialog from '@/components/business/pl-freight-approval-dialog/index.vue'
+import PlFreightSettlementViewDialog from '@/components/business/pl-freight-settlement-view-dialog/index.vue'
+import { formatMoneyDisplay } from '@/utils/money-format'
+import { formatDateTime } from '@/utils/format'
+
+const loading = ref(false)
+const settlementList = ref<FreightSettlement[]>([])
+const viewDialogRef = ref<InstanceType<typeof PlFreightSettlementViewDialog>>()
+const approvalDialogRef = ref<InstanceType<typeof PlFreightApprovalDialog>>()
+
+const searchForm = reactive({ q: '' })
+
+const pagination = reactive({
+  page: 1,
+  pageSize: 20,
+  total: 0
+})
+
+function formatAmount(amount: number) {
+  if (!amount) return '¥0.00'
+  return formatMoneyDisplay(amount)
+}
+
+async function fetchData() {
+  loading.value = true
+  try {
+    const res = await freightApi.list({
+      tab: 'approval_pending',
+      q: searchForm.q || undefined,
+      page: pagination.page,
+      pageSize: pagination.pageSize
+    })
+    const data = res.data ?? res
+    settlementList.value = data.list || []
+    pagination.total = data.total || 0
+  } finally {
+    loading.value = false
+  }
+}
+
+function handleSearch() {
+  pagination.page = 1
+  fetchData()
+}
+
+function handleReset() {
+  searchForm.q = ''
+  pagination.page = 1
+  fetchData()
+}
+
+function handlePageChange() {
+  fetchData()
+}
+
+function handleSizeChange() {
+  pagination.page = 1
+  fetchData()
+}
+
+function handleView(row: FreightSettlement) {
+  viewDialogRef.value?.open(row.id)
+}
+
+function handleApprove(row: FreightSettlement) {
+  if (!row.currentApprovalId) return
+  approvalDialogRef.value?.openApprove(row.id, row.currentApprovalId)
+}
+
+function handleReject(row: FreightSettlement) {
+  if (!row.currentApprovalId) return
+  approvalDialogRef.value?.openReject(row.id, row.currentApprovalId)
+}
+
+function refresh() {
+  fetchData()
+}
+
+onMounted(() => {
+  fetchData()
+})
+
+defineExpose({ refresh })
+</script>

+ 45 - 29
web-admin/src/views/pl/freight/components/SettlementBillPanel.vue

@@ -3,6 +3,16 @@
     <div class="filter-block">
       <el-form :model="searchForm" class="filter-form" @submit.prevent>
         <div class="filter-grid">
+          <el-form-item label="运输公司">
+            <el-select v-model="searchForm.carrierId" placeholder="全部" clearable filterable>
+              <el-option
+                v-for="c in carrierList"
+                :key="c.id"
+                :label="c.carrierName"
+                :value="c.id"
+              />
+            </el-select>
+          </el-form-item>
           <el-form-item label="结算单号">
             <el-input
               v-model="searchForm.q"
@@ -15,6 +25,8 @@
             <el-select v-model="searchForm.settlementStatus" placeholder="全部" clearable>
               <el-option label="草稿" value="pending" />
               <el-option label="审批中" value="approving" />
+              <el-option label="待发放" value="approved" />
+              <el-option label="已发放" value="confirmed" />
             </el-select>
           </el-form-item>
         </div>
@@ -56,6 +68,16 @@
             </el-tag>
           </template>
         </el-table-column>
+        <el-table-column prop="disburseAmount" label="发放金额(元)" min-width="130" align="right">
+          <template #default="{ row }">{{
+            row.disburseAmount ? formatAmount(row.disburseAmount) : '—'
+          }}</template>
+        </el-table-column>
+        <el-table-column prop="disburseTime" label="发放时间" min-width="160" align="center">
+          <template #default="{ row }">{{
+            row.disburseTime ? formatDateTime(row.disburseTime) : '—'
+          }}</template>
+        </el-table-column>
         <el-table-column prop="currentApproverName" label="当前审批人" min-width="110" align="center">
           <template #default="{ row }">{{ row.currentApproverName || '—' }}</template>
         </el-table-column>
@@ -78,6 +100,14 @@
             >
               <el-icon><Edit /></el-icon>编辑
             </el-button>
+            <el-button
+              v-if="row.settlementStatus === 'approved'"
+              link
+              type="primary"
+              @click="handleEdit(row)"
+            >
+              <el-icon><Edit /></el-icon>发票信息
+            </el-button>
             <el-button
               v-if="row.settlementStatus === 'pending'"
               link
@@ -102,22 +132,6 @@
             >
               <el-icon><RefreshLeft /></el-icon>撤回
             </el-button>
-            <el-button
-              v-if="row.canApprove && row.currentApprovalId"
-              link
-              type="success"
-              @click="handleApprove(row)"
-            >
-              <el-icon><CircleCheck /></el-icon>审批
-            </el-button>
-            <el-button
-              v-if="row.canApprove && row.currentApprovalId"
-              link
-              type="danger"
-              @click="handleReject(row)"
-            >
-              <el-icon><CircleClose /></el-icon>驳回
-            </el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -143,8 +157,6 @@
 
 <script setup lang="ts">
 import {
-  CircleCheck,
-  CircleClose,
   Delete,
   Edit,
   Promotion,
@@ -152,7 +164,7 @@ import {
   Search,
   View
 } from '@element-plus/icons-vue'
-import { freightApi, type FreightSettlement } from '@/api/pl'
+import { carrierApi, freightApi, type Carrier, type FreightSettlement } from '@/api/pl'
 import PlFreightApprovalDialog from '@/components/business/pl-freight-approval-dialog/index.vue'
 import PlFreightSettlementFormDialog from '@/components/business/pl-freight-settlement-form-dialog/index.vue'
 import PlFreightSettlementViewDialog from '@/components/business/pl-freight-settlement-view-dialog/index.vue'
@@ -164,12 +176,14 @@ import { formatMoneyDisplay } from '@/utils/money-format'
 import { formatDateTime } from '@/utils/format'
 
 const loading = ref(false)
+const carrierList = ref<Carrier[]>([])
 const settlementList = ref<FreightSettlement[]>([])
 const viewDialogRef = ref<InstanceType<typeof PlFreightSettlementViewDialog>>()
 const formDialogRef = ref<InstanceType<typeof PlFreightSettlementFormDialog>>()
 const approvalDialogRef = ref<InstanceType<typeof PlFreightApprovalDialog>>()
 
 const searchForm = reactive({
+  carrierId: '',
   q: '',
   settlementStatus: ''
 })
@@ -190,6 +204,7 @@ async function fetchData() {
   try {
     const res = await freightApi.list({
       tab: 'bills',
+      carrierId: searchForm.carrierId || undefined,
       q: searchForm.q || undefined,
       settlementStatus: searchForm.settlementStatus || undefined,
       page: pagination.page,
@@ -209,12 +224,22 @@ function handleSearch() {
 }
 
 function handleReset() {
+  searchForm.carrierId = ''
   searchForm.q = ''
   searchForm.settlementStatus = ''
   pagination.page = 1
   fetchData()
 }
 
+async function loadCarrierList() {
+  try {
+    const res = await carrierApi.list({ status: '1' })
+    carrierList.value = (res.data ?? res) || []
+  } catch {
+    carrierList.value = []
+  }
+}
+
 function handlePageChange() {
   fetchData()
 }
@@ -236,16 +261,6 @@ function handleSubmit(row: FreightSettlement) {
   approvalDialogRef.value?.openSubmit(row.id)
 }
 
-function handleApprove(row: FreightSettlement) {
-  if (!row.currentApprovalId) return
-  approvalDialogRef.value?.openApprove(row.id, row.currentApprovalId)
-}
-
-function handleReject(row: FreightSettlement) {
-  if (!row.currentApprovalId) return
-  approvalDialogRef.value?.openReject(row.id, row.currentApprovalId)
-}
-
 async function handleWithdraw(row: FreightSettlement) {
   await ElMessageBox.confirm('确定撤回该结算单的审批吗?', '提示', { type: 'warning' })
   await freightApi.withdrawApproval(row.id)
@@ -267,6 +282,7 @@ function refresh() {
 }
 
 onMounted(() => {
+  loadCarrierList()
   fetchData()
 })
 

+ 25 - 1
web-admin/src/views/pl/freight/components/UnsettledPlanPanel.vue

@@ -3,6 +3,16 @@
     <div class="filter-block">
       <el-form :model="searchForm" class="filter-form" @submit.prevent>
         <div class="filter-grid">
+          <el-form-item label="运输公司">
+            <el-select v-model="searchForm.carrierId" placeholder="全部" clearable filterable>
+              <el-option
+                v-for="c in carrierList"
+                :key="c.id"
+                :label="c.carrierName"
+                :value="c.id"
+              />
+            </el-select>
+          </el-form-item>
           <el-form-item label="计划编号">
             <el-input
               v-model="searchForm.q"
@@ -114,7 +124,7 @@
 
 <script setup lang="ts">
 import { DocumentAdd, RefreshLeft, Search, View } from '@element-plus/icons-vue'
-import { freightApi, type UnsettledPlanItem } from '@/api/pl'
+import { carrierApi, freightApi, type Carrier, type UnsettledPlanItem } from '@/api/pl'
 import PlPlanViewDialog from '@/components/business/pl-plan-view-dialog/index.vue'
 import { getPlanTypeLabel, getPlanTypeTagType } from '@/views/pl/freight/freight-status'
 import { formatMoneyDisplay } from '@/utils/money-format'
@@ -124,12 +134,14 @@ const emit = defineEmits<{ created: [settlementId: string] }>()
 
 const loading = ref(false)
 const createLoading = ref(false)
+const carrierList = ref<Carrier[]>([])
 const planList = ref<UnsettledPlanItem[]>([])
 const selectedRows = ref<UnsettledPlanItem[]>([])
 const planViewVisible = ref(false)
 const viewPlanId = ref('')
 
 const searchForm = reactive({
+  carrierId: '',
   q: '',
   planType: '',
   licensePlate: ''
@@ -150,6 +162,7 @@ async function fetchData() {
   loading.value = true
   try {
     const res = await freightApi.listUnsettledPlans({
+      carrierId: searchForm.carrierId || undefined,
       q: searchForm.q || undefined,
       planType: searchForm.planType || undefined,
       licensePlate: searchForm.licensePlate || undefined,
@@ -170,6 +183,7 @@ function handleSearch() {
 }
 
 function handleReset() {
+  searchForm.carrierId = ''
   searchForm.q = ''
   searchForm.planType = ''
   searchForm.licensePlate = ''
@@ -177,6 +191,15 @@ function handleReset() {
   fetchData()
 }
 
+async function loadCarrierList() {
+  try {
+    const res = await carrierApi.list({ status: '1' })
+    carrierList.value = (res.data ?? res) || []
+  } catch {
+    carrierList.value = []
+  }
+}
+
 function handlePageChange() {
   fetchData()
 }
@@ -222,6 +245,7 @@ function refresh() {
 }
 
 onMounted(() => {
+  loadCarrierList()
   fetchData()
 })
 

+ 1 - 1
web-admin/src/views/pl/freight/freight-status.ts

@@ -17,7 +17,7 @@ export function getSettlementStatusLabel(status: string) {
     pending: '草稿',
     approving: '审批中',
     approved: '待发放',
-    confirmed: '已结算'
+    confirmed: '已发放'
   }
   return map[status] || status
 }

+ 2 - 18
web-admin/src/views/pl/freight/index.vue

@@ -14,12 +14,6 @@
           <el-tab-pane label="结算单" name="bills">
             <SettlementBillPanel ref="billsRef" />
           </el-tab-pane>
-          <el-tab-pane label="待财务发放" name="disburse">
-            <DisbursePanel ref="disburseRef" @disbursed="handleDisbursed" />
-          </el-tab-pane>
-          <el-tab-pane label="已结算" name="settled">
-            <SettledPanel ref="settledRef" />
-          </el-tab-pane>
         </el-tabs>
       </div>
     </section>
@@ -29,22 +23,16 @@
 <script setup lang="ts">
 import UnsettledPlanPanel from './components/UnsettledPlanPanel.vue'
 import SettlementBillPanel from './components/SettlementBillPanel.vue'
-import DisbursePanel from './components/DisbursePanel.vue'
-import SettledPanel from './components/SettledPanel.vue'
 
-type FreightTab = 'unsettled' | 'bills' | 'disburse' | 'settled'
+type FreightTab = 'unsettled' | 'bills'
 
 const activeTab = ref<FreightTab>('unsettled')
 const unsettledRef = ref<InstanceType<typeof UnsettledPlanPanel>>()
 const billsRef = ref<InstanceType<typeof SettlementBillPanel>>()
-const disburseRef = ref<InstanceType<typeof DisbursePanel>>()
-const settledRef = ref<InstanceType<typeof SettledPanel>>()
 
 function refreshTab(name: FreightTab) {
   if (name === 'unsettled') unsettledRef.value?.refresh()
-  else if (name === 'bills') billsRef.value?.refresh()
-  else if (name === 'disburse') disburseRef.value?.refresh()
-  else if (name === 'settled') settledRef.value?.refresh()
+  else billsRef.value?.refresh()
 }
 
 function handleTabChange(name: string | number) {
@@ -57,8 +45,4 @@ function handleSettlementCreated() {
     billsRef.value?.refresh()
   })
 }
-
-function handleDisbursed() {
-  settledRef.value?.refresh()
-}
 </script>

+ 0 - 6
web-admin/src/views/pl/plan-archive/index.vue

@@ -133,11 +133,6 @@
               <PlPlanListProductCell :products="row.products" />
             </template>
           </el-table-column>
-          <el-table-column label="计划附件" min-width="400">
-            <template #default="{ row }">
-              <PlPlanListCertCell :products="row.products" />
-            </template>
-          </el-table-column>
           <el-table-column prop="archiveStatus" label="归档状态" min-width="100" align="center">
             <template #default="{ row }">
               <el-tag :type="getPlPlanArchiveStatusTagType(row.archiveStatus)" size="small">
@@ -235,7 +230,6 @@ import {
 import { planApi, type Plan } from '@/api/pl'
 import PlPlanArchiveDialog from '@/components/business/pl-plan-archive-dialog/index.vue'
 import PlPlanFormDialog from '@/components/business/pl-plan-form-dialog/index.vue'
-import PlPlanListCertCell from '@/components/business/pl-plan-list-cert-cell/index.vue'
 import PlPlanListProductCell from '@/components/business/pl-plan-list-product-cell/index.vue'
 import PlPlanViewDialog from '@/components/business/pl-plan-view-dialog/index.vue'
 import { useDbInstantSearch } from '@/composables/useDbInstantSearch'

+ 12 - 0
web-admin/src/views/pl/self-vehicle/index.vue

@@ -76,6 +76,10 @@
             </div>
 
             <dl class="vehicle-card-kv">
+              <div class="vehicle-card-kv-item">
+                <dt>挂车号</dt>
+                <dd>{{ row.trailerNo || '—' }}</dd>
+              </div>
               <div class="vehicle-card-kv-item">
                 <dt>载重(吨)</dt>
                 <dd>{{ row.loadCapacity || '—' }}</dd>
@@ -119,6 +123,9 @@
           <el-form-item label="车牌号" prop="licensePlate">
             <el-input v-model="formData.licensePlate" placeholder="请输入车牌号" maxlength="20" />
           </el-form-item>
+          <el-form-item label="挂车号" prop="trailerNo">
+            <el-input v-model="formData.trailerNo" placeholder="请输入挂车号(选填)" maxlength="20" clearable />
+          </el-form-item>
           <el-form-item label="载重(吨)" prop="loadCapacity">
             <el-input-number
               v-model="formData.loadCapacity"
@@ -315,6 +322,7 @@ const searchForm = reactive({
 const formData = reactive({
   id: '',
   licensePlate: '',
+  trailerNo: '',
   loadCapacity: null as number | null,
   vehicleStatus: 'available',
   remark: ''
@@ -349,6 +357,7 @@ const viewSections = computed<ViewSection[]>(() => {
       title: '车辆信息',
       fields: [
         { label: '车牌号', value: d.licensePlate },
+        { label: '挂车号', value: d.trailerNo || '—' },
         { label: '车辆类型', value: '自有', tagType: 'success' },
         { label: '载重(吨)', value: d.loadCapacity ?? '—' },
         {
@@ -553,6 +562,7 @@ function handleAdd() {
   Object.assign(formData, {
     id: '',
     licensePlate: '',
+    trailerNo: '',
     loadCapacity: null,
     vehicleStatus: 'available',
     remark: ''
@@ -596,6 +606,7 @@ async function handleEdit(row: Vehicle) {
     Object.assign(formData, {
       id: detail.id,
       licensePlate: detail.licensePlate,
+      trailerNo: detail.trailerNo || '',
       loadCapacity: detail.loadCapacity ?? null,
       vehicleStatus: detail.vehicleStatus,
       remark: detail.remark
@@ -634,6 +645,7 @@ async function handleSubmit() {
   try {
     const payload = {
       licensePlate: formData.licensePlate,
+      trailerNo: formData.trailerNo || undefined,
       vehicleType: VEHICLE_TYPE_SELF,
       loadCapacity: formData.loadCapacity,
       vehicleStatus: formData.vehicleStatus,

+ 12 - 0
web-admin/src/views/pl/vehicle/index.vue

@@ -97,6 +97,10 @@
             </div>
 
             <dl class="vehicle-card-kv">
+              <div class="vehicle-card-kv-item">
+                <dt>挂车号</dt>
+                <dd>{{ row.trailerNo || '—' }}</dd>
+              </div>
               <div class="vehicle-card-kv-item">
                 <dt>载重(吨)</dt>
                 <dd>{{ row.loadCapacity || '—' }}</dd>
@@ -145,6 +149,9 @@
           <el-form-item label="车牌号" prop="licensePlate">
             <el-input v-model="formData.licensePlate" placeholder="请输入车牌号" maxlength="20" />
           </el-form-item>
+          <el-form-item label="挂车号" prop="trailerNo">
+            <el-input v-model="formData.trailerNo" placeholder="请输入挂车号(选填)" maxlength="20" clearable />
+          </el-form-item>
           <el-form-item label="车辆类型" prop="vehicleType">
             <el-select v-model="formData.vehicleType" placeholder="请选择">
               <el-option label="自有" value="self" />
@@ -358,6 +365,7 @@ const searchForm = reactive({
 const formData = reactive({
   id: '',
   licensePlate: '',
+  trailerNo: '',
   vehicleType: 'self',
   carrierId: '',
   carrierName: '',
@@ -416,6 +424,7 @@ const viewSections = computed<ViewSection[]>(() => {
       title: '车辆信息',
       fields: [
         { label: '车牌号', value: d.licensePlate },
+        { label: '挂车号', value: d.trailerNo || '—' },
         {
           label: '车辆类型',
           value: getVehicleTypeLabel(d.vehicleType),
@@ -651,6 +660,7 @@ function handleAdd() {
   Object.assign(formData, {
     id: '',
     licensePlate: '',
+    trailerNo: '',
     vehicleType: 'self',
     carrierId: '',
     carrierName: '',
@@ -693,6 +703,7 @@ async function handleEdit(row: Vehicle) {
     Object.assign(formData, {
       id: detail.id,
       licensePlate: detail.licensePlate,
+      trailerNo: detail.trailerNo || '',
       vehicleType: detail.vehicleType,
       carrierId: detail.carrierId,
       carrierName: detail.carrierName,
@@ -740,6 +751,7 @@ async function handleSubmit() {
   try {
     const payload = {
       licensePlate: formData.licensePlate,
+      trailerNo: formData.trailerNo || undefined,
       vehicleType: formData.vehicleType,
       carrierId: formData.carrierId || undefined,
       carrierName: formData.carrierName || undefined,