544 lines
21 KiB
Markdown
544 lines
21 KiB
Markdown
# 运费结算完善 — Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 将运费结算重构为四 Tab(未结算计划 / 结算单 / 待财务发放 / 已结算),支持多计划组单、合同式多级审批、财务发放登记。
|
||
|
||
**Architecture:** 新建 `pl_freight_settlement_approval` 与 `pl_freight_settlement_service.py`(审批链 CRUD 对齐 `contract_approval.py`);`freight.py` 路由薄层调用 service;前端 `freight/index.vue` 改为 Tab 容器 + 四个 Panel 子组件 + 三个业务弹窗(查看/审批/发放)。
|
||
|
||
**Tech Stack:** FastAPI + SQLAlchemy + Pydantic v2;Vue 3 + Element Plus + 字典 Store
|
||
|
||
**Spec:** `docs/superpowers/specs/2026-06-25-pl-freight-settlement-design.md`
|
||
|
||
---
|
||
|
||
## 文件结构
|
||
|
||
| 文件 | 职责 |
|
||
|------|------|
|
||
| `docs/sql/20260625_pl_freight_settlement_approval.sql` | 审批表 DDL + 字典 + 按钮权限 |
|
||
| `backend/app/models/pl_models.py` | `PlFreightSettlementApproval` |
|
||
| `backend/app/schemas/pl/freight.py` | 组单/审批/发放/列表扩展 Schema |
|
||
| `backend/app/services/pl_freight_settlement_service.py` | 未结算查询、组单、审批、发放 |
|
||
| `backend/app/api/pl/freight.py` | 新路由;废弃旧 submit/approve |
|
||
| `backend/app/services/pl_plan_service.py` | 更新 `_LOCKED_SETTLEMENT_STATUSES` |
|
||
| `web-admin/src/api/pl/index.ts` | `freightApi` 扩展 |
|
||
| `web-admin/src/views/pl/freight/index.vue` | Tab 容器 |
|
||
| `web-admin/src/views/pl/freight/components/UnsettledPlanPanel.vue` | Tab1 |
|
||
| `web-admin/src/views/pl/freight/components/SettlementBillPanel.vue` | Tab2 |
|
||
| `web-admin/src/views/pl/freight/components/DisbursePanel.vue` | Tab3 |
|
||
| `web-admin/src/views/pl/freight/components/SettledPanel.vue` | Tab4 |
|
||
| `web-admin/src/components/business/pl-freight-settlement-view-dialog/index.vue` | 查看 |
|
||
| `web-admin/src/components/business/pl-freight-settlement-form-dialog/index.vue` | 草稿编辑 |
|
||
| `web-admin/src/components/business/pl-freight-approval-dialog/index.vue` | 提交/审批 |
|
||
| `web-admin/src/components/business/pl-freight-disburse-dialog/index.vue` | 财务发放 |
|
||
|
||
---
|
||
|
||
### Task 1: 数据库 DDL + 字典 + 菜单权限
|
||
|
||
**Files:**
|
||
- Create: `docs/sql/20260625_pl_freight_settlement_approval.sql`
|
||
- Modify: `docs/sql/pl_tables.sql`(同步表结构)
|
||
- Modify: `docs/sql/shanxia2.0.sql`(全量快照,可选后续导出)
|
||
|
||
- [ ] **Step 1:** 创建独立 SQL 脚本,含:
|
||
|
||
```sql
|
||
-- pl_freight_settlement_approval
|
||
CREATE TABLE IF NOT EXISTS pl_freight_settlement_approval (
|
||
id VARCHAR(64) NOT NULL COMMENT '主键ID',
|
||
settlement_id VARCHAR(64) NOT NULL COMMENT '结算单ID',
|
||
step INT NOT NULL DEFAULT 1 COMMENT '步骤序号',
|
||
step_name VARCHAR(50) NULL COMMENT '步骤名称',
|
||
approver_id VARCHAR(64) NOT NULL COMMENT '审批人ID',
|
||
approver_name VARCHAR(64) NULL COMMENT '审批人姓名',
|
||
approval_type VARCHAR(20) NOT NULL DEFAULT 'sequential' COMMENT 'sequential/parallel/or',
|
||
approval_result VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending/approved/rejected',
|
||
approval_opinion VARCHAR(500) NULL COMMENT '审批意见',
|
||
approval_time DATETIME NULL COMMENT '审批时间',
|
||
is_current TINYINT NOT NULL DEFAULT 0 COMMENT '是否当前节点',
|
||
tenant_no VARCHAR(64) NOT NULL DEFAULT 'shanxia2.0',
|
||
order_num INT NULL,
|
||
remark VARCHAR(512) NULL,
|
||
create_by VARCHAR(64) NULL,
|
||
create_dept_id VARCHAR(64) NOT NULL DEFAULT '',
|
||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
update_by VARCHAR(64) NULL,
|
||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
del_flag TINYINT NOT NULL DEFAULT 0,
|
||
PRIMARY KEY (id),
|
||
KEY idx_pl_fsa_settlement (settlement_id, del_flag),
|
||
KEY idx_pl_fsa_approver (approver_id, approval_result, is_current, del_flag)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运费结算审批记录';
|
||
```
|
||
|
||
- [ ] **Step 2:** 同一脚本更新 `pl_settlement_status` 字典:
|
||
|
||
```sql
|
||
-- 新增/更新
|
||
INSERT INTO sys_dict_data (...) VALUES
|
||
('PL_SETTLE_APPROVING', '审批中', 'approving', 'warning', 'pl_settlement_status', ...),
|
||
('PL_SETTLE_APPROVED', '待发放', 'approved', 'primary', 'pl_settlement_status', ...);
|
||
-- 更新 pending 标签为「草稿」、confirmed 为「已结算」
|
||
```
|
||
|
||
- [ ] **Step 3:** 按钮权限(parent `PL_FREIGHT`):
|
||
|
||
| id | menu_name | perms |
|
||
|----|-----------|-------|
|
||
| PL_FREIGHT_Q | 查询 | pl:freight:query |
|
||
| PL_FREIGHT_C | 组单 | pl:freight:create |
|
||
| PL_FREIGHT_E | 编辑 | pl:freight:edit |
|
||
| PL_FREIGHT_S | 提交审批 | pl:freight:submit |
|
||
| PL_FREIGHT_A | 审批 | pl:freight:approve |
|
||
| PL_FREIGHT_W | 撤回 | pl:freight:withdraw |
|
||
| PL_FREIGHT_D | 发放登记 | pl:freight:disburse |
|
||
|
||
- [ ] **Step 4:** 在目标库执行脚本并验证表存在
|
||
|
||
Run: `mysql ... -e "SHOW TABLES LIKE 'pl_freight_settlement_approval';"`
|
||
Expected: 1 row
|
||
|
||
---
|
||
|
||
### Task 2: ORM 模型
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/models/pl_models.py`
|
||
|
||
- [ ] **Step 1:** 在 `PlRemittanceVoucher` 之前追加:
|
||
|
||
```python
|
||
class PlFreightSettlementApproval(Base):
|
||
"""运费结算审批 pl_freight_settlement_approval"""
|
||
|
||
__tablename__ = "pl_freight_settlement_approval"
|
||
|
||
id = Column(String(64), primary_key=True)
|
||
settlement_id = Column(String(64), nullable=False)
|
||
step = Column(Integer, nullable=False, default=1)
|
||
step_name = Column(String(50), nullable=True)
|
||
approver_id = Column(String(64), nullable=False)
|
||
approver_name = Column(String(64), nullable=True)
|
||
approval_type = Column(String(20), nullable=False, default="sequential")
|
||
approval_result = Column(String(20), nullable=False, default="pending")
|
||
approval_opinion = Column(String(500), nullable=True)
|
||
approval_time = Column(DateTime, nullable=True)
|
||
is_current = Column(SmallInteger, nullable=False, default=0)
|
||
# ... 公共字段 tenant_no ~ del_flag 与其他 pl 表一致
|
||
```
|
||
|
||
- [ ] **Step 2:** 验证 import
|
||
|
||
Run: `cd backend && python3 -c "from app.models.pl_models import PlFreightSettlementApproval; print('ok')"`
|
||
Expected: `ok`
|
||
|
||
---
|
||
|
||
### Task 3: Pydantic Schema 扩展
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/schemas/pl/freight.py`
|
||
|
||
- [ ] **Step 1:** 扩展 `FreightSettlementCreate` 支持 `planIds`:
|
||
|
||
```python
|
||
class FreightSettlementCreate(BaseModel):
|
||
transport_ids: Optional[list[str]] = Field(default=None, alias="transportIds")
|
||
plan_ids: Optional[list[str]] = Field(default=None, alias="planIds")
|
||
model_config = {"populate_by_name": True}
|
||
|
||
@model_validator(mode="after")
|
||
def check_ids(self):
|
||
if not self.transport_ids and not self.plan_ids:
|
||
raise ValueError("transportIds 或 planIds 至少提供一个")
|
||
return self
|
||
```
|
||
|
||
- [ ] **Step 2:** 列表项扩展 `FreightSettlementListItem`:
|
||
|
||
```python
|
||
class FreightSettlementListItem(FreightSettlementRead):
|
||
plan_count: int = Field(default=0, alias="planCount")
|
||
license_plate_summary: Optional[str] = Field(default=None, alias="licensePlateSummary")
|
||
current_approver_name: Optional[str] = Field(default=None, alias="currentApproverName")
|
||
can_approve: bool = Field(default=False, alias="canApprove")
|
||
can_withdraw: bool = Field(default=False, alias="canWithdraw")
|
||
approval_finished_time: Optional[datetime] = Field(default=None, alias="approvalFinishedTime")
|
||
disburse_time: Optional[datetime] = Field(default=None, alias="disburseTime")
|
||
disburse_amount: Optional[Decimal] = Field(default=None, alias="disburseAmount")
|
||
```
|
||
|
||
- [ ] **Step 3:** 审批/发放 body:
|
||
|
||
```python
|
||
class FreightSettlementSubmitApprovalBody(BaseModel):
|
||
approval_type: str = Field(default="sequential", alias="approvalType") # sequential|parallel|or
|
||
approver_ids: list[str] = Field(..., alias="approverIds", min_length=1)
|
||
remark: Optional[str] = None
|
||
model_config = {"populate_by_name": True}
|
||
|
||
|
||
class FreightSettlementApprovalActionBody(BaseModel):
|
||
action: str = Field(..., description="approve|reject")
|
||
comment: Optional[str] = None
|
||
|
||
|
||
class FreightSettlementDisburseBody(BaseModel):
|
||
remittance_date: date = Field(..., alias="remittanceDate")
|
||
remittance_amount: Decimal = Field(..., alias="remittanceAmount", gt=0)
|
||
voucher_urls: list[str] = Field(..., alias="voucherUrls", min_length=1)
|
||
remark: Optional[str] = None
|
||
model_config = {"populate_by_name": True}
|
||
```
|
||
|
||
- [ ] **Step 4:** `FreightSettlementWithTransports` 增加 `plans`, `approvalFlow`, `vouchers`, `canApprove`
|
||
|
||
Run: `python3 -c "from app.schemas.pl.freight import FreightSettlementDisburseBody; print('ok')"`
|
||
|
||
---
|
||
|
||
### Task 4: Service — 未结算计划 + 组单
|
||
|
||
**Files:**
|
||
- Create: `backend/app/services/pl_freight_settlement_service.py`
|
||
|
||
- [ ] **Step 1:** 实现 `list_unsettled_plans(db, tenant_no, query, page, page_size)`:
|
||
|
||
逻辑(spec D5):
|
||
```python
|
||
def list_unsettled_plans(...):
|
||
# pl_plan JOIN pl_transport ON plan.transport_id = transport.id
|
||
# WHERE archive_status='archived' AND has_freight=1 AND follow_status!='cancelled'
|
||
# AND transport.freight_amount > 0
|
||
# AND transport_id NOT IN (
|
||
# SELECT st.transport_id FROM pl_freight_settlement_transport st
|
||
# JOIN pl_freight_settlement s ON s.id=st.settlement_id
|
||
# WHERE s.del_flag=0 AND st.del_flag=0
|
||
# ) -- 任何 pending 或非 pending 占用均排除;删草稿后重新可见
|
||
```
|
||
|
||
- [ ] **Step 2:** 实现 `create_settlement_from_plan_ids(db, tenant_no, plan_ids, user)`:
|
||
|
||
```python
|
||
def create_settlement_from_plan_ids(...):
|
||
plans = ... # 校验全部 archived + has_freight + transport_id
|
||
transport_ids = [p.transport_id for p in plans]
|
||
_assert_transports_not_occupied(db, tenant_no, transport_ids)
|
||
total = sum(transport.freight_amount)
|
||
settlement = PlFreightSettlement(settlement_status="pending", ...)
|
||
for t in transports:
|
||
db.add(PlFreightSettlementTransport(...))
|
||
db.commit()
|
||
return settlement
|
||
```
|
||
|
||
- [ ] **Step 3:** 实现 `delete_draft_settlement` — 仅 `pending`,软删 settlement + transport links
|
||
|
||
- [ ] **Step 4:** 实现 `_assert_transports_not_occupied` — 被任何未删 settlement 占用则 400
|
||
|
||
---
|
||
|
||
### Task 5: Service — 审批链(对齐 contract_approval)
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/services/pl_freight_settlement_service.py`
|
||
|
||
- [ ] **Step 1:** `create_settlement_approval_records(db, settlement_id, approval_type, approver_ids, user)`
|
||
|
||
参考 `contract_approval.create_approval_records`:
|
||
- sequential:step 递增,首个 `is_current=1`
|
||
- parallel:同 step 多人,`is_current=1` 全部
|
||
- or:同 step 多人,任一通过即可
|
||
|
||
- [ ] **Step 2:** `can_user_approve_settlement(db, settlement_id, user_id) -> bool`
|
||
|
||
- [ ] **Step 3:** `submit_settlement_approval(...)` — `pending → approving`,写审批记录
|
||
|
||
- [ ] **Step 4:** `process_settlement_approval(db, approval_id, action, comment, user)`:
|
||
|
||
通过:推进下一节点 / 会签计数 / 或签短路 → 全部完成则 `settlement_status='approved'`
|
||
|
||
驳回:`settlement_status='pending'`,当前链 `approval_result='rejected'`,`is_current=0`
|
||
|
||
- [ ] **Step 5:** `withdraw_settlement_approval(...)` — 仅提交人、`approving` 态,软删 pending 记录,回 `pending`
|
||
|
||
- [ ] **Step 6:** `get_settlement_approval_flow(db, settlement_id) -> list[dict]`
|
||
|
||
---
|
||
|
||
### Task 6: Service — 财务发放 + 列表 enrich
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/services/pl_freight_settlement_service.py`
|
||
|
||
- [ ] **Step 1:** `disburse_settlement(db, settlement_id, body, user)`:
|
||
|
||
```python
|
||
def disburse_settlement(...):
|
||
settlement = _get_settlement(...)
|
||
if settlement.settlement_status != "approved":
|
||
raise HTTPException(400, "仅待发放状态可登记")
|
||
if body.remittance_amount != settlement.total_freight and not (body.remark or "").strip():
|
||
raise HTTPException(400, "汇款金额与运费合计不一致时备注必填")
|
||
voucher = PlRemittanceVoucher(
|
||
settlement_id=settlement.id,
|
||
remittance_date=body.remittance_date,
|
||
remittance_amount=body.remittance_amount,
|
||
voucher_urls=json.dumps(body.voucher_urls),
|
||
remark=body.remark,
|
||
create_by=user.id,
|
||
...
|
||
)
|
||
settlement.settlement_status = "confirmed"
|
||
settlement.update_by = user.id
|
||
db.commit()
|
||
```
|
||
|
||
- [ ] **Step 2:** `list_settlements_enriched(...)` — 按 `settlement_status` 或 `tab` 过滤,附加 planCount、canApprove、currentApproverName
|
||
|
||
- [ ] **Step 3:** `get_settlement_detail(...)` — transports + 关联 plans + approvalFlow + vouchers
|
||
|
||
---
|
||
|
||
### Task 7: 更新运费锁定状态
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/services/pl_plan_service.py`
|
||
|
||
- [ ] **Step 1:** 替换 `_LOCKED_SETTLEMENT_STATUSES`:
|
||
|
||
```python
|
||
_LOCKED_SETTLEMENT_STATUSES = ("approving", "approved", "confirmed")
|
||
```
|
||
|
||
- [ ] **Step 2:** 组单占用逻辑:任何 `del_flag=0` 的 settlement 关联均视为占用(含 pending),与 Tab1 查询一致
|
||
|
||
---
|
||
|
||
### Task 8: API 路由重构
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/api/pl/freight.py`
|
||
|
||
- [ ] **Step 1:** 新增路由:
|
||
|
||
| 路由 | 处理函数 |
|
||
|------|----------|
|
||
| `GET /unsettled-plans` | `list_unsettled_plans` |
|
||
| `GET /` | `list_settlements_enriched`,query: `tab=bills|disburse|settled` 或 `settlementStatus` |
|
||
| `POST /` | body `planIds` → `create_settlement_from_plan_ids` |
|
||
| `DELETE /{id}` | `delete_draft_settlement` |
|
||
| `POST /{id}/submit-approval` | `submit_settlement_approval` |
|
||
| `POST /approval/{approval_id}` | `process_settlement_approval` |
|
||
| `POST /{id}/withdraw-approval` | `withdraw_settlement_approval` |
|
||
| `GET /{id}/approval-flow` | `get_settlement_approval_flow` |
|
||
| `POST /{id}/disburse` | `disburse_settlement` |
|
||
|
||
- [ ] **Step 2:** 保留旧 `POST /{id}/submit`、`POST /{id}/approve` 为 deprecated 别名(内部转新逻辑或 410),避免破坏未知调用
|
||
|
||
- [ ] **Step 3:** `GET /{id}` 返回 enrich 详情
|
||
|
||
Run: `python3 -c "from app.main import app; print('ok')"`
|
||
|
||
---
|
||
|
||
### Task 9: 前端 API 类型
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/api/pl/index.ts`
|
||
|
||
- [ ] **Step 1:** 扩展 `FreightSettlement` 类型字段:`planCount`, `licensePlateSummary`, `currentApproverName`, `canApprove`, `canWithdraw`, `approvalFinishedTime`, `disburseTime`, `disburseAmount`
|
||
|
||
- [ ] **Step 2:** 新增 `UnsettledPlan` 类型与 `freightApi.unsettledPlans(params)`
|
||
|
||
- [ ] **Step 3:** 更新 freightApi 方法:
|
||
|
||
```typescript
|
||
export const freightApi = {
|
||
unsettledPlans: (params) => request.get('/transport/freight/unsettled-plans', { params }),
|
||
list: (params: { tab?: 'bills' | 'disburse' | 'settled'; ... }) => ...,
|
||
create: (data: { planIds: string[] }) => request.post('/transport/freight/', data),
|
||
remove: (id: string) => request.delete(`/transport/freight/${id}`),
|
||
submitApproval: (id, data) => request.post(`/transport/freight/${id}/submit-approval`, data),
|
||
processApproval: (approvalId, data) => request.post(`/transport/freight/approval/${approvalId}`, data),
|
||
withdrawApproval: (id) => request.post(`/transport/freight/${id}/withdraw-approval`),
|
||
approvalFlow: (id) => request.get(`/transport/freight/${id}/approval-flow`),
|
||
disburse: (id, data) => request.post(`/transport/freight/${id}/disburse`, data),
|
||
// get, update 保留
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Task 10: Tab 容器 `freight/index.vue`
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/views/pl/freight/index.vue`
|
||
|
||
- [ ] **Step 1:** 重构为 P08 结构:
|
||
|
||
```vue
|
||
<template>
|
||
<div class="page-container">
|
||
<section class="panel">
|
||
<div class="panel-top"><h2 class="title-bar-heading">运费结算</h2></div>
|
||
<el-tabs v-model="activeTab" class="freight-tabs" @tab-change="handleTabChange">
|
||
<el-tab-pane label="未结算计划" name="unsettled" />
|
||
<el-tab-pane label="结算单" name="bills" />
|
||
<el-tab-pane label="待财务发放" name="disburse" />
|
||
<el-tab-pane label="已结算" name="settled" />
|
||
</el-tabs>
|
||
<UnsettledPlanPanel v-if="activeTab === 'unsettled'" @created="onSettlementCreated" />
|
||
<SettlementBillPanel v-else-if="activeTab === 'bills'" ref="billsRef" />
|
||
<DisbursePanel v-else-if="activeTab === 'disburse'" />
|
||
<SettledPanel v-else-if="activeTab === 'settled'" />
|
||
</section>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
- [ ] **Step 2:** `onSettlementCreated` 切换 `activeTab='bills'` 并 refresh
|
||
|
||
- [ ] **Step 3:** 删除原单列表逻辑(迁移到 Panel)
|
||
|
||
---
|
||
|
||
### Task 11: Tab1 `UnsettledPlanPanel.vue`
|
||
|
||
**Files:**
|
||
- Create: `web-admin/src/views/pl/freight/components/UnsettledPlanPanel.vue`
|
||
|
||
- [ ] **Step 1:** 筛选:计划编号、日期范围、车牌、单据类型(参考 `plan/index.vue` subset)
|
||
|
||
- [ ] **Step 2:** 表格 + selection + 工具栏「生成结算单」`type="primary"` + 已选 N 项
|
||
|
||
- [ ] **Step 3:** `handleCreateSettlement` → `freightApi.create({ planIds })` → emit `created`
|
||
|
||
- [ ] **Step 4:** 行操作「查看」→ `PlPlanViewDialog` 或现有 plan 查看组件
|
||
|
||
---
|
||
|
||
### Task 12: Tab2 `SettlementBillPanel.vue` + 草稿/查看弹窗
|
||
|
||
**Files:**
|
||
- Create: `web-admin/src/views/pl/freight/components/SettlementBillPanel.vue`
|
||
- Create: `web-admin/src/components/business/pl-freight-settlement-form-dialog/index.vue`
|
||
- Create: `web-admin/src/components/business/pl-freight-settlement-view-dialog/index.vue`
|
||
|
||
- [ ] **Step 1:** 列表 `freightApi.list({ tab: 'bills' })`,列按 spec §3.2
|
||
|
||
- [ ] **Step 2:** 行操作:
|
||
- `pending`:查看、编辑、提交审批、删除
|
||
- `approving`:查看、撤回(`canWithdraw`)、审批(`canApprove`)
|
||
|
||
- [ ] **Step 3:** 编辑弹窗 — 发票字段 + 只读明细表 + footer 取消/保存
|
||
|
||
- [ ] **Step 4:** 查看弹窗 — Hero + 结算信息 + 关联计划/运输表 + 审批时间线(无凭证)
|
||
|
||
- [ ] **Step 5:** 注册 `pl_settlement_status` 到 dictStore,Tag 渲染
|
||
|
||
---
|
||
|
||
### Task 13: 审批弹窗 `PlFreightApprovalDialog`
|
||
|
||
**Files:**
|
||
- Create: `web-admin/src/components/business/pl-freight-approval-dialog/index.vue`
|
||
|
||
- [ ] **Step 1:** Fork `crm/contract/components/ApprovalDialog.vue` 结构与样式
|
||
|
||
- [ ] **Step 2:** `actionType`: `submit` | `approve` | `reject`
|
||
|
||
- [ ] **Step 3:** submit → `freightApi.submitApproval(id, { approvalType, approverIds, remark })`
|
||
|
||
- [ ] **Step 4:** approve/reject → 先 `GET approval-flow` 取当前用户 pending 记录 id → `freightApi.processApproval(approvalId, { action, comment })`
|
||
|
||
- [ ] **Step 5:** SettlementBillPanel 打开弹窗;**禁止**顶栏批量审批
|
||
|
||
---
|
||
|
||
### Task 14: Tab3 `DisbursePanel` + 发放弹窗
|
||
|
||
**Files:**
|
||
- Create: `web-admin/src/views/pl/freight/components/DisbursePanel.vue`
|
||
- Create: `web-admin/src/components/business/pl-freight-disburse-dialog/index.vue`
|
||
|
||
- [ ] **Step 1:** 列表 `tab: 'disburse'`,列:结算单号、运费合计、审批完成时间、提交人、车牌摘要
|
||
|
||
- [ ] **Step 2:** 行操作:查看、发放登记(`v-hasPermi` 或 menuCode 校验 `pl:freight:disburse`)
|
||
|
||
- [ ] **Step 3:** 发放弹窗:
|
||
- 只读区:结算单号、合计、明细摘要、审批流
|
||
- 表单:汇款日期、汇款金额、附件卡片上传(`fetchUploadAttachment`)、备注
|
||
- 金额 ≠ 合计时前端校验备注必填
|
||
|
||
- [ ] **Step 4:** 提交 → `freightApi.disburse` → 刷新 Tab3;可选提示切换 Tab4
|
||
|
||
---
|
||
|
||
### Task 15: Tab4 `SettledPanel.vue`
|
||
|
||
**Files:**
|
||
- Create: `web-admin/src/views/pl/freight/components/SettledPanel.vue`
|
||
|
||
- [ ] **Step 1:** 列表 `tab: 'settled'`,增加汇款日期/金额/确认人/时间列
|
||
|
||
- [ ] **Step 2:** 仅「查看」— 复用 view-dialog,展示 voucher 附件预览/下载
|
||
|
||
---
|
||
|
||
### Task 16: 菜单权限 SQL(独立脚本)
|
||
|
||
**Files:**
|
||
- Create: `docs/sql/20260625_pl_freight_menu_permissions.sql`
|
||
|
||
- [ ] **Step 1:** 与 Task 1 Step 3 按钮权限一致,`ON DUPLICATE KEY UPDATE`
|
||
|
||
- [ ] **Step 2:** 可选:为已有 `PL_FREIGHT` 角色自动授权新按钮(仿 plan-archive 脚本)
|
||
|
||
---
|
||
|
||
### Task 17: 验证与图谱
|
||
|
||
- [ ] **Step 1:** 后端 smoke
|
||
|
||
Run: `cd backend && python3 -c "from app.main import app; print('backend ok')"`
|
||
|
||
- [ ] **Step 2:** 前端 lint(改动文件)
|
||
|
||
Run: `cd web-admin && pnpm exec eslint src/views/pl/freight src/components/business/pl-freight-* --max-warnings 0`
|
||
|
||
- [ ] **Step 3:** 更新图谱
|
||
|
||
Run: `./scripts/update-graph.sh all`
|
||
|
||
- [ ] **Step 4:** 手工验收 spec V1–V13
|
||
|
||
---
|
||
|
||
## Spec 覆盖自检
|
||
|
||
| Spec 章节 | Task |
|
||
|-----------|------|
|
||
| 四 Tab D1 | Task 10–15 |
|
||
| 审批 D2 | Task 5, 8, 13 |
|
||
| 财务 Tab D3 | Task 14 |
|
||
| 计划展示/运输落库 D4 | Task 4, 11 |
|
||
| 未结算准入 D5 | Task 4, 11 |
|
||
| 状态机 §6 | Task 1, 5, 6, 7 |
|
||
| 权限 §1.3 | Task 1, 16 |
|
||
| 验收 V1–V13 | Task 17 |
|
||
|
||
---
|
||
|
||
## 执行顺序建议
|
||
|
||
```
|
||
Task 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 (后端闭环)
|
||
Task 9 → 10 → 11 → 12 → 13 → 14 → 15 (前端)
|
||
Task 16 → 17 (权限 + 验收)
|
||
```
|
||
|
||
Phase 可分批合并 PR:P1=Task1-8+9-11,P2=12-13,P3=14-17。
|