feat(vehicle-return-settlement): 运维办理人转交与审批中费用明细只读

支持办理人/主管转交运维段任务并记录原因;待审批与审批中可进费用明细但运维费用只读,审批完成不可进入。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
王冕
2026-07-22 17:52:40 +08:00
parent a01d2ab708
commit b14425da5a
12 changed files with 1279 additions and 23 deletions

View File

@@ -0,0 +1,414 @@
# 还车应结款 · 运维转交 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:** 在还车应结款运维段增加「转交办理人」(办理人主动转 + 主管代转),并放开待审批/审批中费用明细只读查看。
**Architecture:** 列表仍在 `03-还车应结款.jsx`;「去处理」进入 `VehicleReturnSettlementHandle.jsx``VehicleReturnSettlementApp.jsx` 持有 `opsHandlerByKey` / `opsTransferLogsByKey` 覆盖层,转交后回写列表办理人展示。演示身份用 Handle 页顶栏切换(办理人 / 主管 / 其他人)。
**Tech Stack:** React + Ant DesignModal / Select / Input.TextArea / Button / Drawer本地种子人员清单无真实 API。
**Spec:** [docs/superpowers/specs/2026-07-22-vehicle-return-settlement-ops-transfer-design.md](../specs/2026-07-22-vehicle-return-settlement-ops-transfer-design.md)
---
## File map
| Path | Responsibility |
|------|----------------|
| `src/prototypes/vehicle-return-settlement/ops-transfer-demo.js` | 运维同事种子、演示身份、转交校验纯函数 |
| `src/prototypes/vehicle-return-settlement/VehicleReturnSettlementApp.jsx` | 覆盖层状态;把 row + callbacks 传给 Handle |
| `src/prototypes/vehicle-return-settlement/VehicleReturnSettlementHandle.jsx` | 转交弹窗、记录抽屉、审批中运维只读、身份切换 |
| `src/prototypes/oneos-web-finance/pages/03-还车应结款.jsx` | `opsHandler` 种子、`resolveDeptHandler`、费用明细入口规则 |
| `src/prototypes/vehicle-return-settlement/.spec/ops-transfer.md` | 业务判定规格 |
| `src/prototypes/vehicle-return-settlement/.spec/requirements-prd.md` | AutoPRD |
| `src/prototypes/vehicle-return-settlement/annotation-source.json` | 标注目录 PRD 节点 |
| `src/resources/prd/vehicle-return-settlement-autoprd.md` | 资源侧 PRD 副本 |
---
### Task 1: 转交校验与演示数据模块
**Files:**
- Create: `src/prototypes/vehicle-return-settlement/ops-transfer-demo.js`
- [ ] **Step 1: 新增种子与纯函数**
```js
/** 运维转交 · 原型演示数据与校验(未接真实 API */
export var OPS_COLLEAGUES = [
{ id: 'ops-chen', name: '陈还车', active: true },
{ id: 'ops-zhou', name: '周还车', active: true },
{ id: 'ops-liu', name: '刘还车', active: true },
{ id: 'ops-wang', name: '王五', active: true },
{ id: 'ops-left', name: '已离职-赵工', active: false }
];
export var DEMO_IDENTITIES = [
{ id: 'handler', label: '当前办理人' },
{ id: 'supervisor', label: '运维主管' },
{ id: 'other', label: '其他运维(无权限)' }
];
export function canInitiateOpsTransfer(identityId, currentHandlerName, viewerName) {
if (identityId === 'supervisor') return true;
if (identityId === 'handler') return true;
return false;
}
export function isWholeOrderTransferBlocked(approvalStatus) {
return String(approvalStatus || '') === '审批完成';
}
/** 整单待审批/审批中:运维费用只读(不可保存/提交/撤回) */
export function isOpsFeeReadOnly(approvalStatus) {
var st = String(approvalStatus || '');
return st === '待审批' || st === '审批中';
}
export function listTransferCandidates(colleagues, currentHandlerName) {
return (colleagues || []).filter(function (p) {
return p.active && p.name !== currentHandlerName;
});
}
export function validateOpsTransfer(input) {
var errors = [];
if (!input || !input.toName) errors.push('请选择新办理人');
if (!input || !String(input.reason || '').trim()) errors.push('请填写转交原因');
if (input && input.toName && input.toName === input.fromName) errors.push('不能转交给自己');
if (input && isWholeOrderTransferBlocked(input.approvalStatus)) errors.push('审批完成后不可转交');
return errors;
}
export function buildTransferLog(input) {
return {
at: input.at || new Date().toISOString().slice(0, 16).replace('T', ' '),
operator: input.operator,
fromName: input.fromName,
toName: input.toName,
reason: String(input.reason || '').trim(),
opsStatus: input.opsStatus || '',
approvalStatus: input.approvalStatus || ''
};
}
```
- [ ] **Step 2: 手动核验校验函数Node 一行)**
Run:
```bash
node -e "
const m = require('./src/prototypes/vehicle-return-settlement/ops-transfer-demo.js');
console.log(m.validateOpsTransfer({ fromName:'陈还车', toName:'周还车', reason:'离职', approvalStatus:'审批中' }));
console.log(m.validateOpsTransfer({ fromName:'陈还车', toName:'陈还车', reason:'x', approvalStatus:'待提交' }));
console.log(m.isOpsFeeReadOnly('审批中'), m.isWholeOrderTransferBlocked('审批完成'));
"
```
Expected: 第一行 `[]`;第二行含「不能转交给自己」;第三行 `true true`
若该包为 ESM 无 `require`,改为在文件末尾临时加 `typeof module !== 'undefined' && (module.exports = {...})` 双端导出,或用 `node --input-type=module -e "import(...)"`
- [ ] **Step 3: Commit仅当用户要求提交时执行**
```bash
git add src/prototypes/vehicle-return-settlement/ops-transfer-demo.js
git commit -m "$(cat <<'EOF'
feat(vehicle-return-settlement): add ops transfer demo helpers
EOF
)"
```
---
### Task 2: 列表 — opsHandler 与费用明细入口
**Files:**
- Modify: `src/prototypes/oneos-web-finance/pages/03-还车应结款.jsx`
- [ ] **Step 1: 列表组件接收覆盖层 props**
在列表 `Component` 顶部读取:
```js
var opsHandlerByKey = (props && props.opsHandlerByKey) || {};
```
- [ ] **Step 2: 种子行增加 `opsHandler`**
每条 mock 行增加:
```js
opsHandler: '<与该行 returnPerson 相同的姓名>',
opsTransferLogs: []
```
例如 `returnPerson: '陈还车'` 的行写 `opsHandler: '陈还车'`
- [ ] **Step 3: 改 `resolveDeptHandler`**
```js
case 'submitOperation':
return displayCellText(
(row.key && opsHandlerByKey[row.key]) || row.opsHandler || row.returnPerson
);
```
注意:`resolveDeptHandler` 若在 `useMemo`/闭包外,需能读到最新 `opsHandlerByKey`(不要写死进仅挂载一次的 columns memocolumns 的 `useMemo` 依赖补上 `opsHandlerByKey`)。
- [ ] **Step 4: 改费用明细入口可见性**
将操作列中:
```js
var showFeeDetail = !(st === '待审批' || st === '审批中' || st === '审批完成');
```
改为:
```js
var showFeeDetail = st !== '审批完成';
```
同步改文件内嵌需求注释/说明字符串里「待审批、审批中、审批完成时不显示费用明细」为:「审批完成时不显示费用明细;待审批/审批中可进入查看,运维段不可编辑」。
- [ ] **Step 5: 浏览器验收列表**
打开 `/prototypes/vehicle-return-settlement`
1. 找审批状态为「待审批」或「审批中」的行 → 操作里应有「去处理」
2. 「审批完成」行 → 无「去处理」
3. 运维组标签办理人显示为 `opsHandler` / `returnPerson`
- [ ] **Step 6: Commit用户要求时**
---
### Task 3: App 覆盖层打通列表 ↔ Handle
**Files:**
- Modify: `src/prototypes/vehicle-return-settlement/VehicleReturnSettlementApp.jsx`
- [ ] **Step 1: 增加覆盖层 state 并下发**
```jsx
var _handlers = useState({});
var opsHandlerByKey = _handlers[0];
var setOpsHandlerByKey = _handlers[1];
var _logs = useState({});
var opsTransferLogsByKey = _logs[0];
var setOpsTransferLogsByKey = _logs[1];
function applyOpsTransfer(rowKey, payload) {
setOpsHandlerByKey(function (p) {
var n = {}; for (var k in p) n[k] = p[k];
n[rowKey] = payload.toName;
return n;
});
setOpsTransferLogsByKey(function (p) {
var n = {}; for (var k in p) n[k] = p[k];
var prev = n[rowKey] ? n[rowKey].slice() : [];
prev.unshift(payload.log);
n[rowKey] = prev;
return n;
});
}
// list:
React.createElement(ReturnSettlementListPage, {
opsHandlerByKey: opsHandlerByKey,
onGoHandle: function (row) {
setSelectedRow(row || null);
setView('handle');
}
});
// handle:
React.createElement(VehicleReturnSettlementHandle, {
row: selectedRow,
opsHandler: selectedRow && (opsHandlerByKey[selectedRow.key] || selectedRow.opsHandler || selectedRow.returnPerson),
opsTransferLogs: selectedRow ? (opsTransferLogsByKey[selectedRow.key] || selectedRow.opsTransferLogs || []) : [],
onOpsTransfer: function (payload) {
if (!selectedRow) return;
applyOpsTransfer(selectedRow.key, payload);
},
onBack: function () {
setSelectedRow(null);
setView('list');
}
});
```
- [ ] **Step 2: 确认进入 Handle 后顶栏能读到办理人姓名**(下一 Task 做 UI
---
### Task 4: Handle 页 — 转交 / 只读 / 记录
**Files:**
- Modify: `src/prototypes/vehicle-return-settlement/VehicleReturnSettlementHandle.jsx`
- Modify: `src/prototypes/vehicle-return-settlement/styles/index.css`(弹窗/记录样式按需)
- [ ] **Step 1: 引入 demo 模块与本地状态**
```js
import {
OPS_COLLEAGUES,
DEMO_IDENTITIES,
canInitiateOpsTransfer,
isWholeOrderTransferBlocked,
isOpsFeeReadOnly,
listTransferCandidates,
validateOpsTransfer,
buildTransferLog
} from './ops-transfer-demo';
```
State`demoIdentity`(默认 `handler`)、`transferOpen``transferTo``transferReason``logsOpen`
`props.row``approvalStatus`;运维段本地 `opsStatus`(待提交/已提交,可由 deptData 初始);`opsHandler` 以 props 为准。
- [ ] **Step 2: 顶栏演示身份切换**
在页头增加 Select`当前办理人` / `运维主管` / `其他运维(无权限)`,便于验收权限。
- [ ] **Step 3: 运维区操作按钮按规则渲染**
```js
var approvalStatus = (props.row && props.row.approvalStatus) || '待提交';
var feeReadOnly = isOpsFeeReadOnly(approvalStatus);
var transferBlocked = isWholeOrderTransferBlocked(approvalStatus);
var canTransfer = !transferBlocked && canInitiateOpsTransfer(demoIdentity);
// header actions:
// - feeReadOnly: 无保存/提交/撤回canTransfer 则显示转交
// - !feeReadOnly && opsStatus===已提交: 撤回 + 转交
// - !feeReadOnly && opsStatus===待提交: 保存 + 提交 + 转交
// 另:始终提供「转交记录」文字按钮(有日志或可空状态)
```
保存/提交在 `feeReadOnly` 时不渲染;表格金额列只读(禁用 Input 或纯文本)。
- [ ] **Step 4: 转交 Modal**
字段:当前办理人只读、新办理人 Select`listTransferCandidates(OPS_COLLEAGUES, opsHandler)`)、原因 TextArea 必填。
确认时:
```js
var errors = validateOpsTransfer({
fromName: opsHandler,
toName: transferTo,
reason: transferReason,
approvalStatus: approvalStatus
});
if (errors.length) { message.error(errors[0]); return; }
var log = buildTransferLog({
operator: demoIdentity === 'supervisor' ? '运维主管' : opsHandler,
fromName: opsHandler,
toName: transferTo,
reason: transferReason,
opsStatus: opsStatus,
approvalStatus: approvalStatus
});
if (typeof props.onOpsTransfer === 'function') {
props.onOpsTransfer({ toName: transferTo, log: log });
}
message.success('转交成功');
setTransferOpen(false);
setTransferTo(undefined);
setTransferReason('');
```
二次确认可用 `Modal.confirm` 包一层,文案:`确认后将由【{transferTo}】继续处理本单运维段`
- [ ] **Step 5: 转交记录 Drawer**
列表展示 `props.opsTransferLogs`:时间、操作人、原→新、原因、运维状态、整单状态。
- [ ] **Step 6: 浏览器验收(对照规格 §7**
1. 身份=办理人,待提交单:转交成功 → 返回列表运维办理人变新人
2. 已提交:可转;转后身份切主管/新人可撤(非审批中时)
3. 身份=其他人:无转交按钮
4. 待审批/审批中:可进明细;运维费用不可编;仍可转交
5. 审批完成:列表无「去处理」
6. 原因空 / 转给自己:报错
- [ ] **Step 7: Commit用户要求时**
---
### Task 5: 业务规格 + AutoPRD + 标注
**Files:**
- Create: `src/prototypes/vehicle-return-settlement/.spec/ops-transfer.md`
- Create: `src/prototypes/vehicle-return-settlement/.spec/requirements-prd.md`
- Create: `src/prototypes/vehicle-return-settlement/annotation-source.json`(若尚无则按 sibling 原型模板)
- Create: `src/resources/prd/vehicle-return-settlement-autoprd.md`
- Modify: Handle/index 挂 `PrototypeAnnotationHost`(若入口尚未挂标注)
- [ ] **Step 1: 写 `.spec/ops-transfer.md`**
从设计规格摘判定表:发起人、接收人、状态、只读、入口、字段 `opsHandler` / `opsTransferLogs`、未接真实 API。含优先级表。
- [ ] **Step 2: 写 `requirements-prd.md`**
按 oneos-autoprd 结构:背景、用户、故事(忙不过来 / 离职代转 / 审批中只读查看)、正逆向、验收;链到 `ops-transfer.md`。**不写**表结构/接口字段代码名(规格文件可写字段名)。
- [ ] **Step 3: annotation-source + resources PRD 同步**
目录节点「产品需求说明PRD」markdown 与 `.spec/requirements-prd.md` 一致;`src/resources/prd/vehicle-return-settlement-autoprd.md` 同文。
- [ ] **Step 4: 导航同步**
```bash
npm run nav:sync -- --prototype vehicle-return-settlement --note "运维部办理人转交;审批中费用明细只读可进"
```
- [ ] **Step 5: Commit用户要求时**
---
### Task 6: 不纳入本期(显式跳过)
- `03-还车应结款.jsx``ComponentFeeDetail` 历史残留页:不接转交(入口未使用)
- 列表批量转交、操作列转交
- 其他部门转交
---
## Spec coverage
| Spec 节 | Task |
|---------|------|
| §3.1 发起人 | 1 `canInitiateOpsTransfer`, 4 身份切换 |
| §3.2 接收人 | 1 `listTransferCandidates` |
| §3.3 / 3.3.1 状态与只读入口 | 2 列表入口, 4 `isOpsFeeReadOnly` |
| §3.4 转交效果 | 3 覆盖层, 4 Modal |
| §3.5 原因必填 | 1 `validateOpsTransfer`, 4 |
| §4 弹窗与记录 | 4 |
| §5 数据模型 | 2 `opsHandler`, 3 logs |
| §6 文档 | 5 |
| §7 验收 | 2 Step5, 4 Step6 |
## Placeholder / 一致性自检
- 无 TBD字段名统一 `opsHandler` / `opsTransferLogs` / `onOpsTransfer`
- 主路径为 Handle`ComponentFeeDetail`
- 提交仅在用户明确要求时执行
---
## Execution handoff
Plan complete and saved to `docs/superpowers/plans/2026-07-22-vehicle-return-settlement-ops-transfer.md`.
**两种执行方式:**
1. **Subagent-Driven推荐** — 每任务派生子代理,任务间复审
2. **Inline Execution** — 本会话按 executing-plans 逐任务推进
你要哪一种?

View File

@@ -0,0 +1,187 @@
# 还车应结款 · 运维部转交 设计规格
| 项 | 说明 |
|---|---|
| 日期 | 2026-07-22 |
| 状态 | 已确认 |
| 方案 | 方案 A费用明细「运维部」卡片内单笔转交 |
| 关联原型 | `vehicle-return-settlement`;业务主实现复用 `oneos-web-finance` 还车应结款费用明细 |
| 主改文件 | `src/prototypes/oneos-web-finance/pages/03-还车应结款.jsx` |
---
## 1. 问题与目标
运维部人员处理还车应结款运维段时,存在两类卡住场景:
1. **无法及时处理**:当前办理人短期无法完成填写/提交,需把任务交给同事继续。
2. **人员离职**:原办理人账号停用或人已离开后,运维段无法由他人合法撤回、编辑,任务悬挂。
**本期目标**
1. 在运维部费用明细段提供 **转交办理人** 能力。
2. **当前办理人**可主动转交;**运维主管**可代转(覆盖离职/无人处理)。
3. 转交只换办理人,不改已填费用与整单审批流;改内容仍走既有撤回规则。
4. 转交原因必填,并留下可查操作记录。
**非目标(本期不做)**
- 列表批量转交、操作列快捷转交
- 独立「转交台 / 待处理转交」页面
- 其他部门(业管 / 能源 / 安全)转交
- 真实组织人事同步、账号停用自动触发转交
- 真实消息通知(可本地 Toast + 记录演示)
---
## 2. 方案选择
| 方案 | 说明 | 结论 |
|------|------|------|
| A. 费用明细运维卡片内转交 | 与保存/提交/撤回同工作面 | **采用** |
| B. 列表批量 + 明细单笔 | 离职清多笔快,交互与权限更重 | 二期候选 |
| C. 主管独立转交台 | 主管视角清晰,新产品面 | 二期候选 |
---
## 3. 权限与状态规则
### 3.1 谁可发起
| 角色 | 能力 |
|------|------|
| 当前办理人(`opsHandler` | 可将本单运维段转给其他运维同事 |
| 运维主管 | 可代转任意未禁止状态下的运维段(含原办理人已非在职) |
| 其他角色 / 非办理人非主管 | 无「转交」入口 |
### 3.2 转交对象
-**运维部在职同事**
- 排除:自己、已停用账号、非运维人员
- 选择器支持姓名模糊搜索
### 3.3 何时可转
| 维度 | 允许 | 禁止 |
|------|------|------|
| 运维段状态 | 待提交、已提交 | — |
| 整单审批状态 | 待提交、待审批、审批中、审批驳回、撤回 | **审批完成** |
说明:待审批 / 审批中仍可转交——**只换办理人**,不改已填内容、不改整单审批节点。
### 3.3.1 费用明细入口与运维段可编辑性(相对现网调整)
相对现网「待审批 / 审批中 / 审批完成不显示费用明细」:
| 顺序 | 条件 | 费用明细入口 | 运维段费用编辑 | 运维转交 |
|------|------|--------------|----------------|----------|
| 1 | 整单待提交 / 审批驳回 / 撤回等(费用明细本就可进) | 显示 | 按运维段状态:待提交可编;已提交须先撤回再编 | 办理人/主管可转 |
| 2 | 整单 **待审批 / 审批中** | **显示,不限角色**(任何人可点进查看) | **不可编辑**(保存/提交/撤回隐藏或禁用;表格只读) | 办理人/主管仍可转交 |
| 3 | 整单 **审批完成** | 不显示(或只读查看若产品另有查看页) | — | **无转交** |
要改审批中的运维费用内容:须先按现有规则 **整单撤回**,回到可编辑的整单状态后,再由当前办理人编辑。
### 3.4 转交效果与改内容
| 顺序 | 条件 | 结果 |
|------|------|------|
| 1 | 转交成功 | 更新 `opsHandler` 为新办理人写转交记录Toast 成功 |
| 2 | 转交时运维段已提交 | **不**自动撤回status / 历史提交人姓名保持 |
| 3 | 整单待审批 / 审批中 | 转交可用;运维费用只读;**不可**部门撤回改数 |
| 4 | 新办理人要改费用(整单未在审批中) | 若段状态为已提交 → 先走运维部 **撤回**,再编辑保存/提交 |
| 5 | 整单在审批中要改费用 | 先 **整单撤回**,再进入费用明细编辑 |
| 6 | 整单审批完成 | 无转交按钮 |
### 3.5 原因
- **必填**转交原因(多行文本)
- 空原因不可确认转交
---
## 4. 信息架构与交互
### 4.1 入口
费用明细页 → **运维部**卡片标题栏右侧:
| 场景 | 可见操作(在权限内) |
|------|----------------------|
| 整单可编 + 运维待提交 | 保存、提交、**转交** |
| 整单可编 + 运维已提交 | 撤回、**转交** |
| 整单待审批 / 审批中 | 费用只读;无保存/提交/撤回;办理人/主管可见 **转交** |
| 整单审批完成 | 无转交 |
列表页:
- 待审批 / 审批中:**显示「费用明细」**(不限角色),与现网「隐藏」不同,以本规格为准
- 运维组办理人展示随 `opsHandler` 更新
- **不**增加列表「转交」操作、**不**做批量
### 4.2 转交弹窗
1. 标题:转交运维办理人
2. 当前办理人:只读
3. 新办理人:必选下拉(运维在职、可搜索、排除自己)
4. 转交原因:必填多行
5. 取消 / 确认转交
6. 确认前二次说明:确认后将由【新办理人】继续处理本单运维段
成功后:关闭弹窗;头部办理人即时更新;若当前用户已不再是办理人且非主管,操作区按权限收敛。
### 4.3 转交记录
每笔至少记录:时间、操作人、原办理人、新办理人、原因、当时运维段状态、当时整单审批状态。
费用明细运维卡片提供「转交记录」入口(时间线或抽屉即可)。
---
## 5. 数据模型
| 字段 | 说明 |
|------|------|
| `opsHandler` | **当前办理人**(权威)。初始可由还车人 `returnPerson` 带入;转交只改本字段,**不改**历史还车人 |
| `submitOperation.status` | 运维段提交状态(待提交 / 已提交等既有口径) |
| `submitOperation.name` | **最近一次提交人**;转交不覆盖;新人再次提交时再更新 |
| `opsTransferLogs[]` | 转交记录数组(见 4.3 |
列表「运维组」办理人解析:读 `opsHandler`,不再直接绑死 `returnPerson`
原型演示:本地状态/种子即可,**未接真实 API**;组织人员用本地运维同事清单模拟。
---
## 6. 落地范围
| 项 | 路径 / 说明 |
|----|-------------|
| 主实现 | `src/prototypes/oneos-web-finance/pages/03-还车应结款.jsx`(费用明细运维段 + 列表 `resolveDeptHandler` |
| 薄壳对齐 | `src/prototypes/vehicle-return-settlement/` 办理预览若展示运维操作,对齐转交入口(轻量) |
| 业务规格 | `src/prototypes/vehicle-return-settlement/.spec/ops-transfer.md` |
| AutoPRD | `.spec/requirements-prd.md` + `annotation-source.json` + `src/resources/prd/vehicle-return-settlement-autoprd.md`(若约定存在则同步) |
---
## 7. 验收要点
1. 办理人可把待提交单转给同事,原因必填,列表办理人变为新人。
2. 已提交单可转交;新人可撤回后编辑再提交。
3. 主管可代转「非本人」的运维段任务。
4. 整单待审批 / 审批中:列表可进费用明细(不限角色);运维费用只读不可编;办理人/主管仍可转交;费用与审批流不变。
5. 审批完成后无转交按钮。
6. 不可转给自己或非运维人员;原因为空不可提交。
7. 转交记录可查(时间、人、原因、状态快照)。
---
## 8. 决策摘要(对齐记录)
| 决策点 | 结论 |
|--------|------|
| 发起人 | 当前办理人 + 运维主管 |
| 可转状态(部门段) | 待提交 + 已提交 |
| 整单审批中 | 仍可转,只换人;费用明细可进(不限角色);运维段不可编辑 |
| 接收人 | 仅运维部在职同事 |
| 原因 | 必填 |
| 交互方案 | A费用明细卡片内单笔转交 |