feat(vehicle-return-settlement): 运维办理人转交与审批中费用明细只读
支持办理人/主管转交运维段任务并记录原因;待审批与审批中可进费用明细但运维费用只读,审批完成不可进入。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 Design(Modal / 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 memo;columns 的 `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 逐任务推进
|
||||
|
||||
你要哪一种?
|
||||
@@ -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:费用明细卡片内单笔转交 |
|
||||
@@ -56,6 +56,7 @@ function SearchIconSvg() {
|
||||
const Component = function (props) {
|
||||
var useState = React.useState;
|
||||
var useMemo = React.useMemo;
|
||||
var opsHandlerByKey = (props && props.opsHandlerByKey) || {};
|
||||
|
||||
var antd = window.antd;
|
||||
var App = antd.App;
|
||||
@@ -153,7 +154,9 @@ const Component = function (props) {
|
||||
case 'submitEnergy':
|
||||
return '许诗琪';
|
||||
case 'submitOperation':
|
||||
return displayCellText(row.returnPerson);
|
||||
return displayCellText(
|
||||
(row.key && opsHandlerByKey[row.key]) || row.opsHandler || row.returnPerson
|
||||
);
|
||||
case 'submitSafety':
|
||||
return isGuangzhouReturn(row) ? '罗日可' : '黄际聪';
|
||||
default:
|
||||
@@ -195,13 +198,22 @@ const Component = function (props) {
|
||||
return s || '—';
|
||||
}
|
||||
|
||||
function canEnterFeeDetail(row) {
|
||||
return String((row && row.approvalStatus) || '') !== '审批完成';
|
||||
}
|
||||
|
||||
function renderProjectInfoCell(_, r) {
|
||||
var contractText = displayCellText(r.contractCode);
|
||||
var allowEnter = canEnterFeeDetail(r);
|
||||
return React.createElement('button', {
|
||||
type: 'button',
|
||||
className: 'lc-project-info-btn',
|
||||
title: '进入还车应结款处理',
|
||||
onClick: function () { handleFeeDetail(r); }
|
||||
title: allowEnter ? '进入还车应结款处理' : '审批完成后不可进入费用明细',
|
||||
disabled: !allowEnter,
|
||||
onClick: function () {
|
||||
if (!allowEnter) return;
|
||||
handleFeeDetail(r);
|
||||
}
|
||||
},
|
||||
React.createElement('div', { className: 'vm-stack' },
|
||||
React.createElement('span', { className: 'primary', title: displayCellText(r.customerName) }, displayCellText(r.customerName)),
|
||||
@@ -346,7 +358,9 @@ const Component = function (props) {
|
||||
deliveryLocation: '广州市番禺区羚牛停车场',
|
||||
returnTime: '2026-02-27 16:20',
|
||||
returnPerson: '陈还车',
|
||||
returnLocation: '嘉兴市南湖区客户仓库'
|
||||
returnLocation: '嘉兴市南湖区客户仓库',
|
||||
opsHandler: '陈还车',
|
||||
opsTransferLogs: []
|
||||
},
|
||||
{
|
||||
key: 'r2',
|
||||
@@ -370,7 +384,9 @@ const Component = function (props) {
|
||||
deliveryLocation: '上海市浦东新区交付中心',
|
||||
returnTime: '2026-02-20 18:05',
|
||||
returnPerson: '周还车',
|
||||
returnLocation: '上海市嘉定区客户园区'
|
||||
returnLocation: '上海市嘉定区客户园区',
|
||||
opsHandler: '周还车',
|
||||
opsTransferLogs: []
|
||||
},
|
||||
{
|
||||
key: 'r3',
|
||||
@@ -394,7 +410,9 @@ const Component = function (props) {
|
||||
deliveryLocation: '北京市大兴区交付站',
|
||||
returnTime: '2026-02-02 11:15',
|
||||
returnPerson: '刘还车',
|
||||
returnLocation: '北京市通州区客户仓库'
|
||||
returnLocation: '北京市通州区客户仓库',
|
||||
opsHandler: '刘还车',
|
||||
opsTransferLogs: []
|
||||
},
|
||||
{
|
||||
key: 'r4',
|
||||
@@ -418,7 +436,9 @@ const Component = function (props) {
|
||||
deliveryLocation: '苏州市吴中区交付点',
|
||||
returnTime: '2026-02-26 09:10',
|
||||
returnPerson: '孙还车',
|
||||
returnLocation: '苏州市相城区客户基地'
|
||||
returnLocation: '苏州市相城区客户基地',
|
||||
opsHandler: '孙还车',
|
||||
opsTransferLogs: []
|
||||
},
|
||||
{
|
||||
key: 'r5',
|
||||
@@ -442,7 +462,9 @@ const Component = function (props) {
|
||||
deliveryLocation: '杭州市萧山区交付中心',
|
||||
returnTime: '2026-02-25',
|
||||
returnPerson: '吴还车',
|
||||
returnLocation: '杭州市余杭区港区还车点'
|
||||
returnLocation: '杭州市余杭区港区还车点',
|
||||
opsHandler: '吴还车',
|
||||
opsTransferLogs: []
|
||||
},
|
||||
{
|
||||
key: 'r6',
|
||||
@@ -466,7 +488,9 @@ const Component = function (props) {
|
||||
deliveryLocation: '宁波市北仑区交付站',
|
||||
returnTime: '2026-02-18',
|
||||
returnPerson: '郑还车',
|
||||
returnLocation: '宁波市镇海区客户仓库'
|
||||
returnLocation: '宁波市镇海区客户仓库',
|
||||
opsHandler: '郑还车',
|
||||
opsTransferLogs: []
|
||||
},
|
||||
{
|
||||
key: 'r7',
|
||||
@@ -490,7 +514,9 @@ const Component = function (props) {
|
||||
deliveryLocation: '合肥市蜀山区交付点',
|
||||
returnTime: '2026-02-28',
|
||||
returnPerson: '许还车',
|
||||
returnLocation: '合肥市包河区客户园区'
|
||||
returnLocation: '合肥市包河区客户园区',
|
||||
opsHandler: '许还车',
|
||||
opsTransferLogs: []
|
||||
},
|
||||
{
|
||||
key: 'r8',
|
||||
@@ -514,7 +540,9 @@ const Component = function (props) {
|
||||
deliveryLocation: '深圳市宝安区交付中心',
|
||||
returnTime: '2026-02-22',
|
||||
returnPerson: '彭还车',
|
||||
returnLocation: '东莞市虎门镇客户仓库'
|
||||
returnLocation: '东莞市虎门镇客户仓库',
|
||||
opsHandler: '彭还车',
|
||||
opsTransferLogs: []
|
||||
},
|
||||
{
|
||||
key: 'r9',
|
||||
@@ -538,7 +566,9 @@ const Component = function (props) {
|
||||
deliveryLocation: '天津市滨海新区交付站',
|
||||
returnTime: '2026-02-27',
|
||||
returnPerson: '韩还车',
|
||||
returnLocation: '天津市西青区客户基地'
|
||||
returnLocation: '天津市西青区客户基地',
|
||||
opsHandler: '韩还车',
|
||||
opsTransferLogs: []
|
||||
},
|
||||
{
|
||||
key: 'r10',
|
||||
@@ -562,7 +592,9 @@ const Component = function (props) {
|
||||
deliveryLocation: '武汉市东西湖区交付点',
|
||||
returnTime: '2026-02-16',
|
||||
returnPerson: '蒋还车',
|
||||
returnLocation: '武汉市江夏区客户园区'
|
||||
returnLocation: '武汉市江夏区客户园区',
|
||||
opsHandler: '蒋还车',
|
||||
opsTransferLogs: []
|
||||
}
|
||||
]);
|
||||
var tableDataAll = tableDataState[0];
|
||||
@@ -699,7 +731,7 @@ var requirementDocContent = useMemo(function () {
|
||||
2.12.操作:查看、生成账单、费用明细、撤回;
|
||||
2.12.1.查看:点击跳转还车应结款-查看页面;
|
||||
2.12.2.生成账单:点击生成账单,弹框显示账单界面,审批状态为待审批的记录才可生成账单,其他状态生成账单隐藏;
|
||||
2.12.3.费用明细:点击跳转还车应结款-费用明细页面,审批状态为待审批、审批中、审批完成时,不显示费用明细;
|
||||
2.12.3.费用明细:点击跳转还车应结款-费用明细页面,审批状态为审批完成时不显示;待审批/审批中可进入查看(运维段不可编辑);
|
||||
2.12.4.撤回:审批状态为待审批、审批中时,不显示费用明细点击二次确认,提示:是否确认撤回,点击确定后,提示:撤回成功,同时审批状态修改为撤回;
|
||||
2.13.右下角为分页符,支持单页查看数据条数;
|
||||
`);
|
||||
@@ -794,6 +826,10 @@ var requirementDocContent = useMemo(function () {
|
||||
message.info('跳转:还车应结款-查看(原型) 合同编号:' + (row && row.contractCode));
|
||||
}
|
||||
function handleFeeDetail(row) {
|
||||
if (!canEnterFeeDetail(row)) {
|
||||
message.warning('审批完成后不可进入费用明细');
|
||||
return;
|
||||
}
|
||||
if (props && typeof props.onGoHandle === 'function') {
|
||||
props.onGoHandle(row);
|
||||
return;
|
||||
@@ -897,7 +933,7 @@ var requirementDocContent = useMemo(function () {
|
||||
fixed: 'right',
|
||||
render: function (_, r) {
|
||||
var st = String(r && r.approvalStatus);
|
||||
var showFeeDetail = !(st === '待审批' || st === '审批中' || st === '审批完成');
|
||||
var showFeeDetail = st !== '审批完成';
|
||||
var showRevoke = (st === '待审批' || st === '审批中');
|
||||
var showGenerateBill = String(r && r.approvalStatus) === '待审批';
|
||||
var more = [];
|
||||
@@ -917,7 +953,7 @@ var requirementDocContent = useMemo(function () {
|
||||
}
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
}, [opsHandlerByKey]);
|
||||
|
||||
var listTableScrollX = columns.reduce(function (sum, col) {
|
||||
if (col.children && col.children.length) {
|
||||
|
||||
102
src/prototypes/vehicle-return-settlement/.spec/ops-transfer.md
Normal file
102
src/prototypes/vehicle-return-settlement/.spec/ops-transfer.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# 还车应结款 · 运维转交
|
||||
|
||||
> 实现:`ops-transfer-demo.js` · `VehicleReturnSettlementHandle.jsx` · `VehicleReturnSettlementApp.jsx`
|
||||
> 主页面:`src/prototypes/oneos-web-finance/pages/03-还车应结款.jsx`(费用明细运维段 + 列表 `resolveDeptHandler`)
|
||||
> 数据:本地状态/种子;**未接真实 API**
|
||||
|
||||
## 1. 谁可发起(优先级)
|
||||
|
||||
| 顺序 | 角色 | 能力 |
|
||||
|------|------|------|
|
||||
| 1 | 当前办理人(`opsHandler`) | 可将本单运维段转给其他运维同事 |
|
||||
| 2 | 运维主管 | 可代转任意未禁止状态下的运维段(含原办理人已非在职) |
|
||||
| 3 | 其他角色 / 非办理人非主管 | 无「转交」入口 |
|
||||
|
||||
代码入口:`canInitiateOpsTransfer`(`ops-transfer-demo.js`)
|
||||
|
||||
## 2. 转交对象
|
||||
|
||||
| 顺序 | 规则 |
|
||||
|------|------|
|
||||
| 1 | 仅 **运维部在职同事**(`active: true`) |
|
||||
| 2 | 排除:自己、已停用账号、非运维人员 |
|
||||
| 3 | 选择器支持姓名模糊搜索 |
|
||||
|
||||
代码入口:`listTransferCandidates`(`ops-transfer-demo.js`);人员清单 `OPS_COLLEAGUES`
|
||||
|
||||
## 3. 何时可转
|
||||
|
||||
### 3.1 运维段状态
|
||||
|
||||
| 运维段状态 | 允许转交 |
|
||||
|------------|----------|
|
||||
| 待提交 | 是 |
|
||||
| 已提交 | 是 |
|
||||
|
||||
### 3.2 整单审批状态
|
||||
|
||||
| 整单审批状态 | 允许转交 | 说明 |
|
||||
|--------------|----------|------|
|
||||
| 待提交 / 审批驳回 / 撤回等 | 是 | 费用明细本就可进 |
|
||||
| 待审批 / 审批中 | 是 | 只换办理人,不改费用与审批流 |
|
||||
| **审批完成** | **否** | 唯一整单级禁止条件 |
|
||||
|
||||
代码入口:`isWholeOrderTransferBlocked`(`ops-transfer-demo.js`)
|
||||
|
||||
## 4. 待审批 / 审批中:费用明细与只读
|
||||
|
||||
| 顺序 | 条件 | 费用明细入口 | 运维段费用编辑 | 运维转交 |
|
||||
|------|------|--------------|----------------|----------|
|
||||
| 1 | 整单待提交 / 审批驳回 / 撤回等 | 显示 | 待提交可编;已提交须先撤回再编 | 办理人/主管可转 |
|
||||
| 2 | 整单 **待审批 / 审批中** | **显示,不限角色** | **不可编辑**(保存/提交/撤回隐藏或禁用;表格只读) | 办理人/主管仍可转交 |
|
||||
| 3 | 整单 **审批完成** | 不显示(或只读查看若另有查看页) | — | **无转交** |
|
||||
|
||||
代码入口:`isOpsFeeReadOnly`(`ops-transfer-demo.js`)
|
||||
|
||||
## 5. 转交效果(不自动撤回)
|
||||
|
||||
| 顺序 | 条件 | 结果 |
|
||||
|------|------|------|
|
||||
| 1 | 转交成功 | 更新 `opsHandler` 为新办理人;写转交记录;Toast 成功 |
|
||||
| 2 | 转交时运维段已提交 | **不**自动撤回;`submitOperation.status` / 历史提交人姓名保持 |
|
||||
| 3 | 整单待审批 / 审批中 | 转交可用;运维费用只读;**不可**部门撤回改数 |
|
||||
| 4 | 新办理人要改费用(整单未在审批中) | 若段状态为已提交 → 先走运维部 **撤回**,再编辑保存/提交 |
|
||||
| 5 | 整单在审批中要改费用 | 先 **整单撤回**,再进入费用明细编辑 |
|
||||
|
||||
转交**只改** `opsHandler`;列表「运维组」办理人解析读 `opsHandler`(`resolveDeptHandler` in `03-还车应结款.jsx`)。
|
||||
|
||||
状态提升:`VehicleReturnSettlementApp.jsx` 的 `applyOpsTransfer`;办理预览:`VehicleReturnSettlementHandle.jsx`
|
||||
|
||||
## 6. 转交原因(必填)
|
||||
|
||||
| 顺序 | 规则 |
|
||||
|------|------|
|
||||
| 1 | 多行文本,空原因不可确认转交 |
|
||||
| 2 | 确认前二次说明:确认后将由【新办理人】继续处理本单运维段 |
|
||||
|
||||
代码入口:`validateOpsTransfer`(`ops-transfer-demo.js`)
|
||||
|
||||
## 7. 转交记录字段
|
||||
|
||||
每笔至少记录(`opsTransferLogs[]` / `buildTransferLog`):
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `at` | 操作时间 |
|
||||
| `operator` | 操作人 |
|
||||
| `fromName` | 原办理人 |
|
||||
| `toName` | 新办理人 |
|
||||
| `reason` | 转交原因 |
|
||||
| `opsStatus` | 当时运维段状态快照 |
|
||||
| `approvalStatus` | 当时整单审批状态快照 |
|
||||
|
||||
费用明细运维卡片提供「转交记录」入口(时间线或抽屉)。
|
||||
|
||||
## 8. 代码路径
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `ops-transfer-demo.js` | 人员清单、权限/状态校验、转交记录构建 |
|
||||
| `VehicleReturnSettlementHandle.jsx` | 办理预览页:转交弹窗、只读态、转交记录展示 |
|
||||
| `VehicleReturnSettlementApp.jsx` | 列表/办理视图切换;`opsHandlerByKey` / `opsTransferLogsByKey` 状态 overlay |
|
||||
| `03-还车应结款.jsx` | 主实现:费用明细运维卡片转交入口、列表 `resolveDeptHandler`、待审批/审批中费用明细可见性 |
|
||||
@@ -0,0 +1,67 @@
|
||||
# 还车应结款 · 运维转交 · 产品需求说明
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 模块名称 | 还车应结款 · 运维转交(还车应结款模块增量) |
|
||||
| 所属系统 | ONE-OS(PC) |
|
||||
| 交互原型 | `/prototypes/vehicle-return-settlement` |
|
||||
| 业务条线 | 财务管理 · 还车应结款 |
|
||||
| 文档状态 | AutoPRD · 首版 |
|
||||
| 关联规格 | [ops-transfer.md](./ops-transfer.md) · 设计 `docs/superpowers/specs/2026-07-22-vehicle-return-settlement-ops-transfer-design.md` |
|
||||
|
||||
---
|
||||
|
||||
## 1. 一句话与目标
|
||||
|
||||
**一句话**
|
||||
运维人员在还车应结款费用明细的运维段,可把办理任务转交给在职同事;主管可代转离职或无人处理的单;审批进行中仍可转交,费用只读不改流。
|
||||
|
||||
**要解决的问题**
|
||||
- 办理人短期忙不过来,任务无法及时跟进
|
||||
- 人员离职后运维段无人合法接手,任务悬挂
|
||||
- 现网审批中隐藏费用明细,主管/协作方无法查看运维段进度
|
||||
|
||||
**本期目标**
|
||||
1. 费用明细运维部卡片内提供「转交办理人」
|
||||
2. 当前办理人可自转;运维主管可代转
|
||||
3. 转交只换办理人,不改已填费用与整单审批流
|
||||
4. 转交原因必填,并留可查操作记录
|
||||
5. 整单待审批/审批中:费用明细人人可进、运维段只读、仍可转交
|
||||
|
||||
**非目标**
|
||||
- 列表批量转交、操作列快捷转交
|
||||
- 独立转交台 / 待处理转交页
|
||||
- 业管 / 能源 / 安全等其他部门转交
|
||||
- 真实组织人事同步、账号停用自动转交
|
||||
- 真实消息通知(本地 Toast + 记录演示即可)
|
||||
|
||||
---
|
||||
|
||||
## 2. 用户与故事
|
||||
|
||||
| 角色 | 故事 |
|
||||
|------|------|
|
||||
| 运维办理人 | 忙不过来时把待提交或已提交单转给同事,填原因留痕 |
|
||||
| 运维主管 | 原办理人离职或失联时代转,让任务继续可被处理 |
|
||||
| 任意查看者(审批中) | 待审批/审批中可进费用明细查看运维段;办理人/主管仍可转交 |
|
||||
|
||||
**用户故事(起点 → 运作 → 闭环)**
|
||||
1. As a 运维办理人, I want 在费用明细运维卡片把本单转给在职同事并必填原因, so that 同事能接手继续填写或提交。
|
||||
2. As a 运维主管, I want 代转非本人负责的运维段(含原办理人已不在职), so that 离职场景任务不悬挂。
|
||||
3. As a 运维办理人/主管, I want 整单审批中运维费用只读但仍可转交, so that 只换办理人不打断审批流。
|
||||
4. As a 接手同事, I want 转交后列表与明细头部办理人即时更新, so that 我知道谁在处理。
|
||||
5. As a 任意角色, I want 在运维卡片查看转交记录(时间、人、原因、状态快照), so that 转交过程可追溯。
|
||||
|
||||
---
|
||||
|
||||
## 3. 验收重点
|
||||
|
||||
1. 办理人可把待提交单转给同事,原因必填,列表办理人变为新人。
|
||||
2. 已提交单可转交;新人可撤回后编辑再提交。
|
||||
3. 主管可代转「非本人」的运维段任务。
|
||||
4. 整单待审批 / 审批中:列表可进费用明细(不限角色);运维费用只读不可编;办理人/主管仍可转交;费用与审批流不变。
|
||||
5. 审批完成后无转交按钮。
|
||||
6. 不可转给自己或非运维人员;原因为空不可提交。
|
||||
7. 转交记录可查(时间、人、原因、状态快照)。
|
||||
|
||||
数据:本地种子 + 页面状态;**未接真实 API**。
|
||||
@@ -9,10 +9,43 @@ const Component = function (props) {
|
||||
var _selectedRow = useState(null);
|
||||
var selectedRow = _selectedRow[0];
|
||||
var setSelectedRow = _selectedRow[1];
|
||||
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 (previous) {
|
||||
var next = {};
|
||||
for (var key in previous) next[key] = previous[key];
|
||||
next[rowKey] = payload.toName;
|
||||
return next;
|
||||
});
|
||||
setOpsTransferLogsByKey(function (previous) {
|
||||
var next = {};
|
||||
for (var key in previous) next[key] = previous[key];
|
||||
var logs = next[rowKey] ? next[rowKey].slice() : [];
|
||||
logs.unshift(payload.log);
|
||||
next[rowKey] = logs;
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
if (view === 'handle') {
|
||||
return 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');
|
||||
@@ -21,6 +54,7 @@ const Component = function (props) {
|
||||
}
|
||||
|
||||
return React.createElement(ReturnSettlementListPage, {
|
||||
opsHandlerByKey: opsHandlerByKey,
|
||||
onGoHandle: function (row) {
|
||||
setSelectedRow(row || null);
|
||||
setView('handle');
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
import { StatusTag } from '../vehicle-management/components/StatusTag';
|
||||
import {
|
||||
OPS_COLLEAGUES,
|
||||
DEMO_IDENTITIES,
|
||||
canInitiateOpsTransfer,
|
||||
isWholeOrderTransferBlocked,
|
||||
isOpsFeeReadOnly,
|
||||
listTransferCandidates,
|
||||
validateOpsTransfer,
|
||||
buildTransferLog
|
||||
} from './ops-transfer-demo';
|
||||
|
||||
var DEPT_ROLE_OPTIONS = [
|
||||
{ value: 'biz', label: '客户服务组(全量)' },
|
||||
@@ -141,13 +151,47 @@ const Component = function (props) {
|
||||
var Table = antd.Table;
|
||||
var Button = antd.Button;
|
||||
var Select = antd.Select;
|
||||
var Modal = antd.Modal;
|
||||
var Drawer = antd.Drawer;
|
||||
var Input = antd.Input;
|
||||
var message = antd.message;
|
||||
|
||||
var _role = useState(readDeptRole());
|
||||
var role = _role[0];
|
||||
var setRole = _role[1];
|
||||
var _demoIdentity = useState('handler');
|
||||
var demoIdentity = _demoIdentity[0];
|
||||
var setDemoIdentity = _demoIdentity[1];
|
||||
var _transferOpen = useState(false);
|
||||
var transferOpen = _transferOpen[0];
|
||||
var setTransferOpen = _transferOpen[1];
|
||||
var _transferTo = useState(undefined);
|
||||
var transferTo = _transferTo[0];
|
||||
var setTransferTo = _transferTo[1];
|
||||
var _transferReason = useState('');
|
||||
var transferReason = _transferReason[0];
|
||||
var setTransferReason = _transferReason[1];
|
||||
var _logsOpen = useState(false);
|
||||
var logsOpen = _logsOpen[0];
|
||||
var setLogsOpen = _logsOpen[1];
|
||||
var vehicle = useMemo(function () { return buildVehicleFromRow(props && props.row); }, [props && props.row]);
|
||||
var deptData = useMemo(buildDeptData, []);
|
||||
var approvalStatus = (props && props.row && props.row.approvalStatus) || '待提交';
|
||||
var _opsStatus = useState(function () {
|
||||
var rowDept = props && props.row && props.row.submitOperation && props.row.submitOperation.status;
|
||||
if (rowDept === '待提交' || !rowDept) {
|
||||
return String(deptData.ops.status || '').indexOf('已提交') >= 0 ? '已提交' : '待提交';
|
||||
}
|
||||
// 列表种子里运维段可能写「审批中/审批完成」等,均视为部门段已提交
|
||||
return '已提交';
|
||||
});
|
||||
var opsStatus = _opsStatus[0];
|
||||
var setOpsStatus = _opsStatus[1];
|
||||
var opsHandler = (props && props.opsHandler) || (props && props.row && (props.row.opsHandler || props.row.returnPerson)) || '—';
|
||||
var opsTransferLogs = (props && props.opsTransferLogs) || [];
|
||||
var feeReadOnly = isOpsFeeReadOnly(approvalStatus);
|
||||
var canTransfer = !isWholeOrderTransferBlocked(approvalStatus) && canInitiateOpsTransfer(demoIdentity);
|
||||
var transferCandidates = listTransferCandidates(OPS_COLLEAGUES, opsHandler);
|
||||
var stats = useMemo(function () {
|
||||
return {
|
||||
deposit: '10000.00',
|
||||
@@ -162,6 +206,61 @@ const Component = function (props) {
|
||||
writeDeptRole(next);
|
||||
}
|
||||
|
||||
function closeTransferModal() {
|
||||
setTransferOpen(false);
|
||||
setTransferTo(undefined);
|
||||
setTransferReason('');
|
||||
}
|
||||
|
||||
function confirmOpsTransfer() {
|
||||
var errors = validateOpsTransfer({
|
||||
fromName: opsHandler,
|
||||
toName: transferTo,
|
||||
reason: transferReason,
|
||||
approvalStatus: approvalStatus
|
||||
});
|
||||
if (errors.length) {
|
||||
message.error(errors[0]);
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '确认转交',
|
||||
content: '确认后将由【' + transferTo + '】继续处理本单运维段',
|
||||
okText: '确认转交',
|
||||
cancelText: '取消',
|
||||
onOk: function () {
|
||||
var log = buildTransferLog({
|
||||
operator: demoIdentity === 'supervisor' ? '运维主管' : opsHandler,
|
||||
fromName: opsHandler,
|
||||
toName: transferTo,
|
||||
reason: transferReason,
|
||||
opsStatus: opsStatus,
|
||||
approvalStatus: approvalStatus
|
||||
});
|
||||
if (props && typeof props.onOpsTransfer === 'function') {
|
||||
props.onOpsTransfer({ toName: transferTo, log: log });
|
||||
}
|
||||
message.success('转交成功');
|
||||
closeTransferModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function saveOpsFees() {
|
||||
setOpsStatus('待提交');
|
||||
message.success('运维费用已保存');
|
||||
}
|
||||
|
||||
function submitOpsFees() {
|
||||
setOpsStatus('已提交');
|
||||
message.success('运维费用已提交');
|
||||
}
|
||||
|
||||
function revokeOpsFees() {
|
||||
setOpsStatus('待提交');
|
||||
message.success('已撤回运维费用');
|
||||
}
|
||||
|
||||
function canSeeSection(sectionKey) {
|
||||
if (role === 'biz') return true;
|
||||
if (sectionKey === 'customer') return false;
|
||||
@@ -196,7 +295,9 @@ const Component = function (props) {
|
||||
React.createElement('div', { className: 'vrs-handle-dept-head__meta' },
|
||||
React.createElement('span', { className: 'tabular-nums' }, '总金额:', React.createElement('strong', null, dept.total), ' 元'),
|
||||
React.createElement('span', null, '提交人:', dept.submitter || '—'),
|
||||
React.createElement(StatusTag, { label: dept.status, tone: approvalTone(dept.status) })
|
||||
dept.handler ? React.createElement('span', null, '办理人:', dept.handler) : null,
|
||||
React.createElement(StatusTag, { label: dept.status, tone: approvalTone(dept.status) }),
|
||||
dept.actions || null
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -249,8 +350,30 @@ const Component = function (props) {
|
||||
|
||||
function renderOpsSection() {
|
||||
var d = deptData.ops;
|
||||
var actions = React.createElement('div', { className: 'vrs-handle-inline-actions' },
|
||||
!feeReadOnly && opsStatus === '已提交'
|
||||
? React.createElement(Button, { size: 'small', onClick: revokeOpsFees }, '撤回')
|
||||
: null,
|
||||
!feeReadOnly && opsStatus !== '已提交'
|
||||
? React.createElement(Button, { size: 'small', onClick: saveOpsFees }, '保存')
|
||||
: null,
|
||||
!feeReadOnly && opsStatus !== '已提交'
|
||||
? React.createElement(Button, { type: 'primary', size: 'small', onClick: submitOpsFees }, '提交')
|
||||
: null,
|
||||
canTransfer
|
||||
? React.createElement(Button, { size: 'small', onClick: function () { setTransferOpen(true); } }, '转交')
|
||||
: null,
|
||||
React.createElement(Button, { type: 'link', size: 'small', onClick: function () { setLogsOpen(true); } }, '转交记录')
|
||||
);
|
||||
return React.createElement('section', { className: 'vm-table-card vrs-handle-dept-card', key: 'ops' },
|
||||
renderDeptHeader({ title: '运维组', total: d.total, submitter: d.submitter, status: d.status }),
|
||||
renderDeptHeader({
|
||||
title: '运维组',
|
||||
total: d.total,
|
||||
submitter: d.submitter,
|
||||
handler: opsHandler,
|
||||
status: opsStatus,
|
||||
actions: actions
|
||||
}),
|
||||
React.createElement('div', { className: 'vm-table-wrap' },
|
||||
React.createElement(Table, {
|
||||
className: 'vm-list-table',
|
||||
@@ -267,10 +390,6 @@ const Component = function (props) {
|
||||
{ title: '最后更新时间', dataIndex: 'updatedAt', key: 'updatedAt', width: 148, className: 'tabular-nums' }
|
||||
]
|
||||
})
|
||||
),
|
||||
React.createElement('div', { className: 'vrs-handle-dept-actions' },
|
||||
React.createElement(Button, { size: 'small' }, '保存'),
|
||||
React.createElement(Button, { type: 'primary', size: 'small' }, '提交')
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -373,6 +492,15 @@ const Component = function (props) {
|
||||
style: { width: 200 },
|
||||
options: DEPT_ROLE_OPTIONS,
|
||||
onChange: handleRoleChange
|
||||
}),
|
||||
React.createElement('span', { className: 'vrs-handle-role-switch__label' }, '演示身份'),
|
||||
React.createElement(Select, {
|
||||
value: demoIdentity,
|
||||
style: { width: 170 },
|
||||
options: DEMO_IDENTITIES.map(function (identity) {
|
||||
return { value: identity.id, label: identity.label };
|
||||
}),
|
||||
onChange: setDemoIdentity
|
||||
})
|
||||
)
|
||||
),
|
||||
@@ -410,6 +538,57 @@ const Component = function (props) {
|
||||
role === 'biz'
|
||||
? React.createElement(Button, { type: 'primary', disabled: true }, '14天12小时后可提交审核')
|
||||
: null
|
||||
),
|
||||
React.createElement(Modal, {
|
||||
title: '转交运维办理人',
|
||||
open: transferOpen,
|
||||
okText: '确认转交',
|
||||
cancelText: '取消',
|
||||
onCancel: closeTransferModal,
|
||||
onOk: confirmOpsTransfer
|
||||
},
|
||||
React.createElement('div', { className: 'vrs-handle-transfer-form' },
|
||||
React.createElement('label', null, '当前办理人'),
|
||||
React.createElement(Input, { value: opsHandler, readOnly: true }),
|
||||
React.createElement('label', null, '新办理人'),
|
||||
React.createElement(Select, {
|
||||
value: transferTo,
|
||||
placeholder: '请选择新办理人',
|
||||
showSearch: true,
|
||||
optionFilterProp: 'label',
|
||||
options: transferCandidates.map(function (person) {
|
||||
return { value: person.name, label: person.name };
|
||||
}),
|
||||
onChange: setTransferTo
|
||||
}),
|
||||
React.createElement('label', null, '转交原因'),
|
||||
React.createElement(Input.TextArea, {
|
||||
value: transferReason,
|
||||
rows: 3,
|
||||
placeholder: '请填写转交原因',
|
||||
onChange: function (event) { setTransferReason(event.target.value); }
|
||||
})
|
||||
)
|
||||
),
|
||||
React.createElement(Drawer, {
|
||||
title: '运维转交记录',
|
||||
open: logsOpen,
|
||||
width: 520,
|
||||
onClose: function () { setLogsOpen(false); }
|
||||
},
|
||||
opsTransferLogs.length
|
||||
? React.createElement('div', { className: 'vrs-handle-transfer-log-list' },
|
||||
opsTransferLogs.map(function (log, index) {
|
||||
return React.createElement('article', { className: 'vrs-handle-transfer-log', key: (log.at || '') + index },
|
||||
React.createElement('strong', null, log.fromName || '—', ' → ', log.toName || '—'),
|
||||
React.createElement('span', null, '时间:', log.at || '—'),
|
||||
React.createElement('span', null, '操作人:', log.operator || '—'),
|
||||
React.createElement('span', null, '原因:', log.reason || '—'),
|
||||
React.createElement('span', null, '运维状态:', log.opsStatus || '—', ';整单状态:', log.approvalStatus || '—')
|
||||
);
|
||||
})
|
||||
)
|
||||
: React.createElement('div', { className: 'vrs-handle-empty' }, '暂无转交记录')
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"documentVersion": 1,
|
||||
"format": "axhub-annotation-source",
|
||||
"data": {
|
||||
"version": 2,
|
||||
"prototypeName": "vehicle-return-settlement",
|
||||
"pageId": "list",
|
||||
"updatedAt": 1784712340993,
|
||||
"nodes": []
|
||||
},
|
||||
"markdownMap": {},
|
||||
"assetMap": {},
|
||||
"directory": {
|
||||
"nodes": [
|
||||
{
|
||||
"type": "folder",
|
||||
"id": "vrs-doc-root",
|
||||
"title": "还车应结款说明",
|
||||
"defaultExpanded": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "vrs-doc-prd",
|
||||
"title": "产品需求说明(PRD)",
|
||||
"markdownPath": ".spec/requirements-prd.md",
|
||||
"markdown": "# 还车应结款 · 运维转交 · 产品需求说明\n\n| 项 | 内容 |\n|---|---|\n| 模块名称 | 还车应结款 · 运维转交(还车应结款模块增量) |\n| 所属系统 | ONE-OS(PC) |\n| 交互原型 | `/prototypes/vehicle-return-settlement` |\n| 业务条线 | 财务管理 · 还车应结款 |\n| 文档状态 | AutoPRD · 首版 |\n| 关联规格 | [ops-transfer.md](./ops-transfer.md) · 设计 `docs/superpowers/specs/2026-07-22-vehicle-return-settlement-ops-transfer-design.md` |\n\n---\n\n## 1. 一句话与目标\n\n**一句话** \n运维人员在还车应结款费用明细的运维段,可把办理任务转交给在职同事;主管可代转离职或无人处理的单;审批进行中仍可转交,费用只读不改流。\n\n**要解决的问题** \n- 办理人短期忙不过来,任务无法及时跟进 \n- 人员离职后运维段无人合法接手,任务悬挂 \n- 现网审批中隐藏费用明细,主管/协作方无法查看运维段进度 \n\n**本期目标** \n1. 费用明细运维部卡片内提供「转交办理人」 \n2. 当前办理人可自转;运维主管可代转 \n3. 转交只换办理人,不改已填费用与整单审批流 \n4. 转交原因必填,并留可查操作记录 \n5. 整单待审批/审批中:费用明细人人可进、运维段只读、仍可转交 \n\n**非目标** \n- 列表批量转交、操作列快捷转交 \n- 独立转交台 / 待处理转交页 \n- 业管 / 能源 / 安全等其他部门转交 \n- 真实组织人事同步、账号停用自动转交 \n- 真实消息通知(本地 Toast + 记录演示即可) \n\n---\n\n## 2. 用户与故事\n\n| 角色 | 故事 |\n|------|------|\n| 运维办理人 | 忙不过来时把待提交或已提交单转给同事,填原因留痕 |\n| 运维主管 | 原办理人离职或失联时代转,让任务继续可被处理 |\n| 任意查看者(审批中) | 待审批/审批中可进费用明细查看运维段;办理人/主管仍可转交 |\n\n**用户故事(起点 → 运作 → 闭环)** \n1. As a 运维办理人, I want 在费用明细运维卡片把本单转给在职同事并必填原因, so that 同事能接手继续填写或提交。 \n2. As a 运维主管, I want 代转非本人负责的运维段(含原办理人已不在职), so that 离职场景任务不悬挂。 \n3. As a 运维办理人/主管, I want 整单审批中运维费用只读但仍可转交, so that 只换办理人不打断审批流。 \n4. As a 接手同事, I want 转交后列表与明细头部办理人即时更新, so that 我知道谁在处理。 \n5. As a 任意角色, I want 在运维卡片查看转交记录(时间、人、原因、状态快照), so that 转交过程可追溯。 \n\n---\n\n## 3. 验收重点\n\n1. 办理人可把待提交单转给同事,原因必填,列表办理人变为新人。 \n2. 已提交单可转交;新人可撤回后编辑再提交。 \n3. 主管可代转「非本人」的运维段任务。 \n4. 整单待审批 / 审批中:列表可进费用明细(不限角色);运维费用只读不可编;办理人/主管仍可转交;费用与审批流不变。 \n5. 审批完成后无转交按钮。 \n6. 不可转给自己或非运维人员;原因为空不可提交。 \n7. 转交记录可查(时间、人、原因、状态快照)。 \n\n数据:本地种子 + 页面状态;**未接真实 API**。"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "vrs-doc-ops-transfer",
|
||||
"title": "运维转交规则",
|
||||
"markdownPath": ".spec/ops-transfer.md",
|
||||
"markdown": "# 还车应结款 · 运维转交\n\n> 实现:`ops-transfer-demo.js` · `VehicleReturnSettlementHandle.jsx` · `VehicleReturnSettlementApp.jsx` \n> 主页面:`src/prototypes/oneos-web-finance/pages/03-还车应结款.jsx`(费用明细运维段 + 列表 `resolveDeptHandler`) \n> 数据:本地状态/种子;**未接真实 API**\n\n## 1. 谁可发起(优先级)\n\n| 顺序 | 角色 | 能力 |\n|------|------|------|\n| 1 | 当前办理人(`opsHandler`) | 可将本单运维段转给其他运维同事 |\n| 2 | 运维主管 | 可代转任意未禁止状态下的运维段(含原办理人已非在职) |\n| 3 | 其他角色 / 非办理人非主管 | 无「转交」入口 |\n\n代码入口:`canInitiateOpsTransfer`(`ops-transfer-demo.js`)\n\n## 2. 转交对象\n\n| 顺序 | 规则 |\n|------|------|\n| 1 | 仅 **运维部在职同事**(`active: true`) |\n| 2 | 排除:自己、已停用账号、非运维人员 |\n| 3 | 选择器支持姓名模糊搜索 |\n\n代码入口:`listTransferCandidates`(`ops-transfer-demo.js`);人员清单 `OPS_COLLEAGUES`\n\n## 3. 何时可转\n\n### 3.1 运维段状态\n\n| 运维段状态 | 允许转交 |\n|------------|----------|\n| 待提交 | 是 |\n| 已提交 | 是 |\n\n### 3.2 整单审批状态\n\n| 整单审批状态 | 允许转交 | 说明 |\n|--------------|----------|------|\n| 待提交 / 审批驳回 / 撤回等 | 是 | 费用明细本就可进 |\n| 待审批 / 审批中 | 是 | 只换办理人,不改费用与审批流 |\n| **审批完成** | **否** | 唯一整单级禁止条件 |\n\n代码入口:`isWholeOrderTransferBlocked`(`ops-transfer-demo.js`)\n\n## 4. 待审批 / 审批中:费用明细与只读\n\n| 顺序 | 条件 | 费用明细入口 | 运维段费用编辑 | 运维转交 |\n|------|------|--------------|----------------|----------|\n| 1 | 整单待提交 / 审批驳回 / 撤回等 | 显示 | 待提交可编;已提交须先撤回再编 | 办理人/主管可转 |\n| 2 | 整单 **待审批 / 审批中** | **显示,不限角色** | **不可编辑**(保存/提交/撤回隐藏或禁用;表格只读) | 办理人/主管仍可转交 |\n| 3 | 整单 **审批完成** | 不显示(或只读查看若另有查看页) | — | **无转交** |\n\n代码入口:`isOpsFeeReadOnly`(`ops-transfer-demo.js`)\n\n## 5. 转交效果(不自动撤回)\n\n| 顺序 | 条件 | 结果 |\n|------|------|------|\n| 1 | 转交成功 | 更新 `opsHandler` 为新办理人;写转交记录;Toast 成功 |\n| 2 | 转交时运维段已提交 | **不**自动撤回;`submitOperation.status` / 历史提交人姓名保持 |\n| 3 | 整单待审批 / 审批中 | 转交可用;运维费用只读;**不可**部门撤回改数 |\n| 4 | 新办理人要改费用(整单未在审批中) | 若段状态为已提交 → 先走运维部 **撤回**,再编辑保存/提交 |\n| 5 | 整单在审批中要改费用 | 先 **整单撤回**,再进入费用明细编辑 |\n\n转交**只改** `opsHandler`;列表「运维组」办理人解析读 `opsHandler`(`resolveDeptHandler` in `03-还车应结款.jsx`)。\n\n状态提升:`VehicleReturnSettlementApp.jsx` 的 `applyOpsTransfer`;办理预览:`VehicleReturnSettlementHandle.jsx`\n\n## 6. 转交原因(必填)\n\n| 顺序 | 规则 |\n|------|------|\n| 1 | 多行文本,空原因不可确认转交 |\n| 2 | 确认前二次说明:确认后将由【新办理人】继续处理本单运维段 |\n\n代码入口:`validateOpsTransfer`(`ops-transfer-demo.js`)\n\n## 7. 转交记录字段\n\n每笔至少记录(`opsTransferLogs[]` / `buildTransferLog`):\n\n| 字段 | 说明 |\n|------|------|\n| `at` | 操作时间 |\n| `operator` | 操作人 |\n| `fromName` | 原办理人 |\n| `toName` | 新办理人 |\n| `reason` | 转交原因 |\n| `opsStatus` | 当时运维段状态快照 |\n| `approvalStatus` | 当时整单审批状态快照 |\n\n费用明细运维卡片提供「转交记录」入口(时间线或抽屉)。\n\n## 8. 代码路径\n\n| 文件 | 职责 |\n|------|------|\n| `ops-transfer-demo.js` | 人员清单、权限/状态校验、转交记录构建 |\n| `VehicleReturnSettlementHandle.jsx` | 办理预览页:转交弹窗、只读态、转交记录展示 |\n| `VehicleReturnSettlementApp.jsx` | 列表/办理视图切换;`opsHandlerByKey` / `opsTransferLogsByKey` 状态 overlay |\n| `03-还车应结款.jsx` | 主实现:费用明细运维卡片转交入口、列表 `resolveDeptHandler`、待审批/审批中费用明细可见性 |"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,18 @@ import { ConfigProvider } from 'antd';
|
||||
import zhCN from 'antd/locale/zh_CN';
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import {
|
||||
type AnnotationSourceDocument,
|
||||
type AnnotationViewerOptions,
|
||||
} from '@axhub/annotation';
|
||||
import 'antd/dist/reset.css';
|
||||
import '../vehicle-management/style.css';
|
||||
import './styles/index.css';
|
||||
import { PrototypeAnnotationHost } from '../../common/prototype-annotation-host';
|
||||
import { clearHostPrototypeRouteInfo } from '../../common/useHashPage';
|
||||
import ReturnSettlementApp from './VehicleReturnSettlementApp.jsx';
|
||||
import annotationSourceDocument from './annotation-source.json';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -70,9 +76,25 @@ export default function VehicleReturnSettlement() {
|
||||
clearHostPrototypeRouteInfo();
|
||||
}, []);
|
||||
|
||||
const annotationOptions = useMemo<AnnotationViewerOptions>(
|
||||
() => ({
|
||||
showToolbar: true,
|
||||
showThemeToggle: true,
|
||||
showColorFilter: true,
|
||||
emptyWhenNoData: false,
|
||||
toolbarEdge: 'right',
|
||||
currentPageId: 'list',
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfigProvider theme={vmTheme} locale={zhCN}>
|
||||
<ReturnSettlementApp />
|
||||
<PrototypeAnnotationHost
|
||||
source={annotationSourceDocument as AnnotationSourceDocument}
|
||||
options={annotationOptions}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
70
src/prototypes/vehicle-return-settlement/ops-transfer-demo.js
vendored
Normal file
70
src/prototypes/vehicle-return-settlement/ops-transfer-demo.js
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
/** 运维转交 · 原型演示数据与校验(未接真实 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 === '审批中' || 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 || ''
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
OPS_COLLEAGUES: OPS_COLLEAGUES,
|
||||
DEMO_IDENTITIES: DEMO_IDENTITIES,
|
||||
canInitiateOpsTransfer: canInitiateOpsTransfer,
|
||||
isWholeOrderTransferBlocked: isWholeOrderTransferBlocked,
|
||||
isOpsFeeReadOnly: isOpsFeeReadOnly,
|
||||
listTransferCandidates: listTransferCandidates,
|
||||
validateOpsTransfer: validateOpsTransfer,
|
||||
buildTransferLog: buildTransferLog
|
||||
};
|
||||
}
|
||||
@@ -257,6 +257,45 @@
|
||||
color: var(--ln-muted);
|
||||
}
|
||||
|
||||
.vrs-handle-inline-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vrs-handle-transfer-form {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vrs-handle-transfer-form label {
|
||||
margin-top: 4px;
|
||||
color: var(--ln-body);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.vrs-handle-transfer-log-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.vrs-handle-transfer-log {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--ln-hairline);
|
||||
border-radius: 8px;
|
||||
color: var(--ln-body);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.vrs-handle-transfer-log strong {
|
||||
color: var(--ln-ink);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.vrs-handle-field-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
67
src/resources/prd/vehicle-return-settlement-autoprd.md
Normal file
67
src/resources/prd/vehicle-return-settlement-autoprd.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# 还车应结款 · 运维转交 · 产品需求说明
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 模块名称 | 还车应结款 · 运维转交(还车应结款模块增量) |
|
||||
| 所属系统 | ONE-OS(PC) |
|
||||
| 交互原型 | `/prototypes/vehicle-return-settlement` |
|
||||
| 业务条线 | 财务管理 · 还车应结款 |
|
||||
| 文档状态 | AutoPRD · 首版 |
|
||||
| 关联规格 | [ops-transfer.md](./ops-transfer.md) · 设计 `docs/superpowers/specs/2026-07-22-vehicle-return-settlement-ops-transfer-design.md` |
|
||||
|
||||
---
|
||||
|
||||
## 1. 一句话与目标
|
||||
|
||||
**一句话**
|
||||
运维人员在还车应结款费用明细的运维段,可把办理任务转交给在职同事;主管可代转离职或无人处理的单;审批进行中仍可转交,费用只读不改流。
|
||||
|
||||
**要解决的问题**
|
||||
- 办理人短期忙不过来,任务无法及时跟进
|
||||
- 人员离职后运维段无人合法接手,任务悬挂
|
||||
- 现网审批中隐藏费用明细,主管/协作方无法查看运维段进度
|
||||
|
||||
**本期目标**
|
||||
1. 费用明细运维部卡片内提供「转交办理人」
|
||||
2. 当前办理人可自转;运维主管可代转
|
||||
3. 转交只换办理人,不改已填费用与整单审批流
|
||||
4. 转交原因必填,并留可查操作记录
|
||||
5. 整单待审批/审批中:费用明细人人可进、运维段只读、仍可转交
|
||||
|
||||
**非目标**
|
||||
- 列表批量转交、操作列快捷转交
|
||||
- 独立转交台 / 待处理转交页
|
||||
- 业管 / 能源 / 安全等其他部门转交
|
||||
- 真实组织人事同步、账号停用自动转交
|
||||
- 真实消息通知(本地 Toast + 记录演示即可)
|
||||
|
||||
---
|
||||
|
||||
## 2. 用户与故事
|
||||
|
||||
| 角色 | 故事 |
|
||||
|------|------|
|
||||
| 运维办理人 | 忙不过来时把待提交或已提交单转给同事,填原因留痕 |
|
||||
| 运维主管 | 原办理人离职或失联时代转,让任务继续可被处理 |
|
||||
| 任意查看者(审批中) | 待审批/审批中可进费用明细查看运维段;办理人/主管仍可转交 |
|
||||
|
||||
**用户故事(起点 → 运作 → 闭环)**
|
||||
1. As a 运维办理人, I want 在费用明细运维卡片把本单转给在职同事并必填原因, so that 同事能接手继续填写或提交。
|
||||
2. As a 运维主管, I want 代转非本人负责的运维段(含原办理人已不在职), so that 离职场景任务不悬挂。
|
||||
3. As a 运维办理人/主管, I want 整单审批中运维费用只读但仍可转交, so that 只换办理人不打断审批流。
|
||||
4. As a 接手同事, I want 转交后列表与明细头部办理人即时更新, so that 我知道谁在处理。
|
||||
5. As a 任意角色, I want 在运维卡片查看转交记录(时间、人、原因、状态快照), so that 转交过程可追溯。
|
||||
|
||||
---
|
||||
|
||||
## 3. 验收重点
|
||||
|
||||
1. 办理人可把待提交单转给同事,原因必填,列表办理人变为新人。
|
||||
2. 已提交单可转交;新人可撤回后编辑再提交。
|
||||
3. 主管可代转「非本人」的运维段任务。
|
||||
4. 整单待审批 / 审批中:列表可进费用明细(不限角色);运维费用只读不可编;办理人/主管仍可转交;费用与审批流不变。
|
||||
5. 审批完成后无转交按钮。
|
||||
6. 不可转给自己或非运维人员;原因为空不可提交。
|
||||
7. 转交记录可查(时间、人、原因、状态快照)。
|
||||
|
||||
数据:本地种子 + 页面状态;**未接真实 API**。
|
||||
Reference in New Issue
Block a user