diff --git a/docs/superpowers/plans/2026-07-22-vehicle-return-settlement-ops-transfer.md b/docs/superpowers/plans/2026-07-22-vehicle-return-settlement-ops-transfer.md new file mode 100644 index 0000000..1c59e45 --- /dev/null +++ b/docs/superpowers/plans/2026-07-22-vehicle-return-settlement-ops-transfer.md @@ -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 逐任务推进 + +你要哪一种? diff --git a/docs/superpowers/specs/2026-07-22-vehicle-return-settlement-ops-transfer-design.md b/docs/superpowers/specs/2026-07-22-vehicle-return-settlement-ops-transfer-design.md new file mode 100644 index 0000000..7af07fa --- /dev/null +++ b/docs/superpowers/specs/2026-07-22-vehicle-return-settlement-ops-transfer-design.md @@ -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:费用明细卡片内单笔转交 | diff --git a/src/prototypes/oneos-web-finance/pages/03-还车应结款.jsx b/src/prototypes/oneos-web-finance/pages/03-还车应结款.jsx index 6a43a7e..4f219d3 100644 --- a/src/prototypes/oneos-web-finance/pages/03-还车应结款.jsx +++ b/src/prototypes/oneos-web-finance/pages/03-还车应结款.jsx @@ -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) { diff --git a/src/prototypes/vehicle-return-settlement/.spec/ops-transfer.md b/src/prototypes/vehicle-return-settlement/.spec/ops-transfer.md new file mode 100644 index 0000000..bfad426 --- /dev/null +++ b/src/prototypes/vehicle-return-settlement/.spec/ops-transfer.md @@ -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`、待审批/审批中费用明细可见性 | diff --git a/src/prototypes/vehicle-return-settlement/.spec/requirements-prd.md b/src/prototypes/vehicle-return-settlement/.spec/requirements-prd.md new file mode 100644 index 0000000..445e1e1 --- /dev/null +++ b/src/prototypes/vehicle-return-settlement/.spec/requirements-prd.md @@ -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**。 diff --git a/src/prototypes/vehicle-return-settlement/VehicleReturnSettlementApp.jsx b/src/prototypes/vehicle-return-settlement/VehicleReturnSettlementApp.jsx index f9f1e7c..5d435a9 100644 --- a/src/prototypes/vehicle-return-settlement/VehicleReturnSettlementApp.jsx +++ b/src/prototypes/vehicle-return-settlement/VehicleReturnSettlementApp.jsx @@ -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'); diff --git a/src/prototypes/vehicle-return-settlement/VehicleReturnSettlementHandle.jsx b/src/prototypes/vehicle-return-settlement/VehicleReturnSettlementHandle.jsx index d2cdf25..b6c4be2 100644 --- a/src/prototypes/vehicle-return-settlement/VehicleReturnSettlementHandle.jsx +++ b/src/prototypes/vehicle-return-settlement/VehicleReturnSettlementHandle.jsx @@ -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' }, '暂无转交记录') ) ) ); diff --git a/src/prototypes/vehicle-return-settlement/annotation-source.json b/src/prototypes/vehicle-return-settlement/annotation-source.json new file mode 100644 index 0000000..8aa80fd --- /dev/null +++ b/src/prototypes/vehicle-return-settlement/annotation-source.json @@ -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`、待审批/审批中费用明细可见性 |" + } + ] + } + ] + } +} diff --git a/src/prototypes/vehicle-return-settlement/index.tsx b/src/prototypes/vehicle-return-settlement/index.tsx index 456f2da..cf052b2 100644 --- a/src/prototypes/vehicle-return-settlement/index.tsx +++ b/src/prototypes/vehicle-return-settlement/index.tsx @@ -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( + () => ({ + showToolbar: true, + showThemeToggle: true, + showColorFilter: true, + emptyWhenNoData: false, + toolbarEdge: 'right', + currentPageId: 'list', + }), + [], + ); + return ( + ); } diff --git a/src/prototypes/vehicle-return-settlement/ops-transfer-demo.js b/src/prototypes/vehicle-return-settlement/ops-transfer-demo.js new file mode 100644 index 0000000..80e49af --- /dev/null +++ b/src/prototypes/vehicle-return-settlement/ops-transfer-demo.js @@ -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 + }; +} diff --git a/src/prototypes/vehicle-return-settlement/styles/index.css b/src/prototypes/vehicle-return-settlement/styles/index.css index c499175..b224810 100644 --- a/src/prototypes/vehicle-return-settlement/styles/index.css +++ b/src/prototypes/vehicle-return-settlement/styles/index.css @@ -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)); diff --git a/src/resources/prd/vehicle-return-settlement-autoprd.md b/src/resources/prd/vehicle-return-settlement-autoprd.md new file mode 100644 index 0000000..445e1e1 --- /dev/null +++ b/src/resources/prd/vehicle-return-settlement-autoprd.md @@ -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**。