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

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

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-06-28 21:00:31 +08:00
1 parent 66d5a8714b
commit 98aec80d71
37 files changed
+1362 -195

No files matched your search

+2
View File
@@ -56,6 +56,7 @@ class PlVehicle(Base):
carrier_id = Column(String(64), nullable=True, comment="承运商ID(类型为承运商时必填)")
carrier_name = Column(String(200), nullable=True, comment="承运商名称(冗余)")
license_plate = Column(String(20), nullable=False, unique=True, comment="车牌号")
trailer_no = Column(String(20), nullable=True, comment="挂车号")
default_driver_id = Column(String(64), nullable=True, comment="默认司机ID")
default_driver_name = Column(String(100), nullable=True, comment="默认司机姓名(冗余)")
load_capacity = Column(DECIMAL(10, 2), nullable=True, comment="载重(吨)")
@@ -533,6 +534,7 @@ class PlFreightSettlement(Base):
# 发票信息
invoice_no = Column(String(50), nullable=True, comment="发票号")
invoice_date = Column(Date, nullable=True, comment="开票日期")
invoice_attachment_urls = Column(Text, nullable=True, comment="发票附件URLs,逗号分隔")
# 结算状态
settlement_status = Column(String(20), nullable=False, default="pending", comment="结算状态 pending-待提交 submitted-已提交 approved-审核通过 confirmed-已确认")
+2
View File
@@ -92,6 +92,7 @@ class VehicleBase(BaseModel):
carrier_id: Optional[str] = Field(default=None, alias="carrierId", description="承运商ID")
carrier_name: Optional[str] = Field(default=None, max_length=200, alias="carrierName", description="承运商名称")
license_plate: str = Field(..., max_length=20, alias="licensePlate", description="车牌号")
trailer_no: Optional[str] = Field(default=None, max_length=20, alias="trailerNo", description="挂车号")
default_driver_id: Optional[str] = Field(default=None, alias="defaultDriverId", description="默认司机ID(只读兼容)")
default_driver_name: Optional[str] = Field(default=None, max_length=100, alias="defaultDriverName", description="默认司机姓名(只读兼容)")
load_capacity: Optional[Decimal] = Field(default=None, alias="loadCapacity", description="载重(吨)")
@@ -110,6 +111,7 @@ class VehicleUpdate(BaseModel):
carrier_id: Optional[str] = Field(default=None, alias="carrierId")
carrier_name: Optional[str] = Field(default=None, max_length=200, alias="carrierName")
license_plate: Optional[str] = Field(default=None, max_length=20, alias="licensePlate")
trailer_no: Optional[str] = Field(default=None, max_length=20, alias="trailerNo")
load_capacity: Optional[Decimal] = Field(default=None, alias="loadCapacity")
vehicle_status: Optional[str] = Field(default=None, max_length=20, alias="vehicleStatus")
remark: Optional[str] = None
+4
View File
@@ -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;
+1
View File
@@ -417,6 +417,7 @@ CREATE TABLE `pl_freight_settlement` (
`invoice_amount` decimal(15,2) DEFAULT NULL COMMENT '发票金额',
`invoice_no` varchar(50) DEFAULT NULL COMMENT '发票号',
`invoice_date` date DEFAULT NULL COMMENT '开票日期',
`invoice_attachment_urls` text DEFAULT NULL COMMENT '发票附件URLs,逗号分隔',
`settlement_status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT '结算状态 pending-待提交 submitted-已提交 approved-审核通过 confirmed-已确认',
`tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
+4 -2
View File
@@ -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`
+6 -1
View File
@@ -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) {
+21 -7
View File
@@ -18,13 +18,6 @@ export const plRoutes: AppRouteRecord[] = [
component: '/pl/carrier/index',
meta: { title: '承运商管理', keepAlive: true, menuCode: 'pl:carrier:list' }
},
{
id: 'PL00000000000000000000000000003',
name: 'PlVehicle',
path: 'vehicle',
component: '/pl/vehicle/index',
meta: { title: '车辆管理', keepAlive: true, menuCode: 'pl:vehicle:list' }
},
{
id: 'PL00000000000000000000000000003S',
name: 'PlSelfVehicle',
@@ -32,6 +25,13 @@ export const plRoutes: AppRouteRecord[] = [
component: '/pl/self-vehicle/index',
meta: { title: '山峡自有车辆', keepAlive: true, menuCode: 'pl:self-vehicle:list' }
},
{
id: 'PL00000000000000000000000000003',
name: 'PlVehicle',
path: 'vehicle',
component: '/pl/vehicle/index',
meta: { title: '车辆管理', keepAlive: true, menuCode: 'pl:vehicle:list' }
},
{
id: 'PL00000000000000000000000000004',
name: 'PlPersonnel',
@@ -109,6 +109,20 @@ export const plRoutes: AppRouteRecord[] = [
component: '/pl/freight/index',
meta: { title: '运费结算', keepAlive: true, menuCode: 'pl:freight:list' }
},
{
id: 'PL00000000000000000000000000031',
name: 'PlFreightApproval',
path: 'freight-approval',
component: '/pl/freight-approval/index',
meta: { title: '运费结算审批', keepAlive: true, menuCode: 'pl:freight-approval:list' }
},
{
id: 'PL00000000000000000000000000032',
name: 'PlFreightDisburse',
path: 'freight-disburse',
component: '/pl/freight-disburse/index',
meta: { title: '运费财务发放', keepAlive: true, menuCode: 'pl:freight-disburse:list' }
},
{
id: 'PL00000000000000000000000000040',
name: 'PlInventory',
+7 -2
View File
@@ -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
+13 -2
View File
@@ -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
}
+2 -18
View File
@@ -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,
+12
View File
@@ -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,