feat(pl): 运费结算审批分流与车辆挂车号等 PL 能力完善
拆分运费审批/出款页面,补充结算发票附件与菜单 SQL,并同步前后端模型与车辆相关展示。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
66d5a8714b
commit
98aec80d71
37 files changed
+1362
-195
No files matched your search
@@ -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-已确认")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- 运费结算:发票附件字段
|
||||
ALTER TABLE `pl_freight_settlement`
|
||||
ADD COLUMN `invoice_attachment_urls` text DEFAULT NULL COMMENT '发票附件URLs,逗号分隔' AFTER `invoice_date`;
|
||||
@@ -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`;
|
||||
@@ -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`;
|
||||
@@ -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;
|
||||
@@ -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 '排序号',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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`
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 个附件"
|
||||
:model-value="settlement.invoiceAttachmentUrls"
|
||||
disabled
|
||||
/>
|
||||
</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>
|
||||
</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)
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.remittanceDate = ''
|
||||
form.remittanceAmount = undefined
|
||||
form.voucherUrls = ''
|
||||
form.remark = ''
|
||||
}
|
||||
|
||||
async function open(id: string) {
|
||||
visible.value = true
|
||||
loading.value = true
|
||||
form.remittanceDate = ''
|
||||
form.remittanceAmount = undefined
|
||||
form.remark = ''
|
||||
voucherUrlsStr.value = ''
|
||||
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>
|
||||
@@ -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('保存成功')
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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 || ''
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) || '—')
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -80,6 +80,7 @@ const viewSections = computed<ViewSection[]>(() => {
|
||||
tableMode: 'quad',
|
||||
fields: [
|
||||
{ label: '车牌号', value: d.licensePlate },
|
||||
{ label: '挂车号', value: d.trailerNo || '—' },
|
||||
{
|
||||
label: '车辆类型',
|
||||
value: getPlVehicleTypeLabel(d.vehicleType),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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('')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) : ''
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export function getSettlementStatusLabel(status: string) {
|
||||
pending: '草稿',
|
||||
approving: '审批中',
|
||||
approved: '待发放',
|
||||
confirmed: '已结算'
|
||||
confirmed: '已发放'
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user