实现 B-2b 统一报销工作台迁移(reimbursement-form-shared-component-migration)

这个提交包含在:
2026-09-02 07:35:01 +08:00
父节点 6bca91a360
当前提交 abdd250325
共修改 38 个文件,包含 1850 行新增和 2723 行删除
@@ -1,21 +1,164 @@
# Design:B-2b 报销填写态共享组件迁移
# Design:B-2b 统一报销工作台迁移
## Current Decision Boundary
## 1. Architecture Decision
B-2b 不重新定义业务内容组件,而是采用 B-2a 已验证的展示模型、字段顺序和快照/实时标签;本阶段增加 edit capability、票据抽屉和提交前检查,并删除旧重复布局。
采用“任务路由分派 + 唯一报销工作台内容合同 + PC/移动布局壳 + 三个模式适配器”。不把审批模式继续塞进现有大型详情页,也不让通用 workflow 页面继续拥有报销业务布局。
详细交互在 B-2a 真人反馈后收敛,但以下架构边界已经冻结:
```text
/workflow/tasks/:taskId /mobile/approvals/:taskId
│ │
└──── authorized task route dispatcher ┘
│
entityType=reimbursement
│
ReimbursementApprovalAdapter
│
ReimbursementDesktopWorkspaceShell
(共享内容合同)
- 共享组件不直接拥有 API 查询或 mutation;填写页通过显式能力和回调注入新增、编辑、删除、票据关联与恢复动作。
- review/readonly 模式不接收 edit capability,不能靠 CSS 隐藏维护动作。
- PC/移动复用同一 projection 与语义 DOM;响应式容器决定抽屉/底部 sheet 形态。
- 服务端 B2 门禁仍是提交权威,前端检查只做同合同的提前反馈。
- 旧布局在迁移验证通过后删除,不保留永久 feature flag 双实现。
/reimbursements/:reimbursementId
│
reimbursement route selector
├─ participant + draft/rejected -> ReimbursementEditAdapter
└─ otherwise authorized -> ReimbursementReadonlyAdapter
│
Desktop / Mobile WorkspaceShell
(共享内容合同)
```
## Deferred Until Entry Conditions Are Met
任务分派器通过唯一一次已授权 task detail 查询选择实体渲染器,并把同一份 query result(含受控 refetch 能力)移交给所选渲染器。它不重复查询、不渲染报销摘要、不计算报销检查、不持有审批表单状态。非报销实体继续进入现有通用详情。
- 票据抽屉在 PC 使用侧栏还是对话框、移动使用 bottom sheet 还是全屏层。
- 编辑入口的最终密度、明细卡片折叠规则和提交摘要的视觉权重。
- B-2a 真人反馈要求的字段顺序微调。
## 2. Shell Contract
这些是 B-2b 启动前要补齐的设计细节,不改变本 PRD 的迁移承诺和验收结果。
PC/移动布局壳都是纯组合组件,共享模型和 capability 合同,但不强求同一份 DOM,也不接收 `mode` 用于业务判断:
```ts
type ReimbursementWorkspaceModel = {
header: ReimbursementWorkspaceHeaderModel;
evidence: ReimbursementWorkspaceEvidenceProjection;
issueSummary: ReimbursementWorkspaceIssueSummary | null;
timeline: ReimbursementWorkspaceTimelineModel;
};
type ReimbursementWorkspaceCapabilities = {
item?: ReimbursementEvidenceItemCapabilities;
invoice?: ReimbursementEvidenceInvoiceCapabilities;
attachment?: ReimbursementEvidenceAttachmentCapabilities;
decision?: ReimbursementDecisionCapabilities;
};
type ReimbursementWorkspaceShellProps = {
model: ReimbursementWorkspaceModel;
capabilities: ReimbursementWorkspaceCapabilities;
primaryActions?: ReactNode;
secondaryActions?: ReactNode;
statusSummary?: ReactNode;
transientFeedback?: ReactNode;
};
```
`ReimbursementDesktopWorkspaceShell` 与 `ReimbursementMobileWorkspaceShell` 可以根据数据是否存在组合区块,也可以渲染注入的 slot;两者必须保持同一业务内容顺序和语义子组件,但可分别实现 PC 右栏与移动卡片/底部安全区布局。它们不能根据角色、permission、workflow 状态、报销状态或模式推导能力,不能用 `isMobile` 业务分支互相模拟。壳内禁止 `useQuery`、`useMutation`、API client 和业务 mutation state。纯视觉展开优先使用语义 HTML;确需本地视觉状态时不得承载业务事实。
## 3. Adapter Responsibilities
### Edit Adapter
- 获取/刷新报销详情、费用类型、项目、票据候选和 participant precheck。
- 拥有表头、明细、票据、附件、提交、删除、OCR 状态与错误恢复 mutation/state。
- 将可用动作投影为 capabilities;状态或权限不允许时不注入,不让壳二次判断。
- 服务端提交/B2 结果覆盖客户端乐观判断。
### Approval Adapter
- 消费路由边界移交的 workflow task detail/query result,不为同一 task 再发一份分派查询;另行获取实时报销详情。
- 拥有 task detail 的业务解释和刷新策略、allowedActions、提交快照、处理历史、审批 mutation、退回引用、材料查看、联网状态和 fail-closed。
- 只注入证据查看、引用和审批决策能力;编辑/关联/解绑 capability 必须不存在。
- 任务、快照、实时报销内容任一关键部分未就绪时,输出不可决策模型和原因。
### Readonly Adapter
- 只获取既有授权允许的报销详情、处理进度和材料查看能力。
- 不请求参与人专用 precheck,不持有任何业务 mutation。
- 不注入编辑、提交或 workflow 决策能力。
共享查询键、材料打开 helper、展示投影和格式化函数可以复用;适配器不能通过一个巨大 hook 返回所有模式的 mutation 再由页面隐藏。
## 4. Information Architecture
### PC
```text
报销单号 + 状态
报销人 · 日期 · 合计
事由
┌────────────────────────────────────┬────────────────────┐
│ 明细及所属票据 │ 当前节点/状态 │
│ 未归属票据(仅有时) │ 可决策状态 │
│ 补充附件 │ 异常锚点(仅有时) │
│ 异常/快照差异(仅有时) │ 审批/提交动作 │
│ 处理历史/进度 │ sticky │
└────────────────────────────────────┴────────────────────┘
```
### Mobile
标题/状态 → 报销人/日期/合计 → 事由 → 明细/票据 → 未归属(仅有时)→ 附件 → 异常/差异(仅有时)→ 历史/进度 → 底部动作。
正常检查不生成卡片。异常模型必须带对象引用和锚点:
```ts
type ReimbursementWorkspaceIssue = {
key: string;
target: { kind: "item" | "invoice" | "submission"; displayLabel: string };
severity: "blocking" | "review" | "info";
reason: string;
impact: string;
nextStep?: string;
anchorId: string;
};
```
右栏只投影非零、可定位的问题。快照缺失是 `info`,视觉低于 blocking/review;快照与实时事实一致时不生成独立差异区。
## 5. Route and Navigation
- PC `/workflow/tasks/:taskId` 与移动 `/mobile/approvals/:taskId` 保持不变,避免破坏通知、待办、刷新和深链。
- 分派期间显示统一加载骨架;不得先挂载 `WorkflowTaskDetailPage`/通用移动详情再切换报销工作台。
- `/reimbursements/:id` 继续是报销记录入口。返回目标由合法、受限的 navigation state/source helper 解析;审批工作台不再提供“返回审批详情”的自循环外链。
- 新增或重分类路由时同步 `scripts/qa/route-manifest.json`;如果只是同一路由内分派,也要为 reimbursement renderer 增加行为证据。
## 6. Legacy Removal
最终候选删除:
- `WorkflowTaskDetailPage` 的报销 DetailField、报销证据/检查区、报销右栏摘要与报销专用动作分支。
- `MobileApprovalsPage` 的报销内容、报销检查、报销离线解释和报销动作业务分支;通用列表/非报销详情可保留。
- `ReimbursementDetailPage`/`MobileReimbursementDetailPage` 中被工作台替代的旧明细表格和旧材料分组。
- 旧报销专用文案、测试 fixture 与只为休眠路径服务的 helper/import。
允许在实现过程的未提交工作区短暂双接线;业务候选提交前必须用结构测试/`rg` allowlist 证明旧分支为 0。不得保留 feature flag 双轨。
上述壳层禁用项与通用审批旧分支清零必须实现为 `scripts/qa/check-ui-design-rules.mjs` 的 error 规则,并通过既有 `--strict` 部署 preflight;单元测试只补行为和类型证据,不能替代该门禁。
## 7. Data and Permission Contracts
- 不改后端 schema。现有 reimbursement detail、workflow detail、材料预览和动作 API 保持权威。
- 提交时费用标准/预算来自 workflow context 快照;当前票据/附件来自 reimbursement detail。
- approval adapter 复用 B-2a IndexedDB 白名单投影;工作台 model/evidence 不进入 cache。
- edit adapter 继续要求参与人 + `draft/rejected` + 现有 permission;readonly/approval 不因 UI 复用获得写能力。
- 材料 URL、访问参数和原始 payload 不进入 task、日志、浏览器持久缓存或错误文案。
## 8. Print Boundary
`/reimbursements/:id/print` 继续作为专用打印/PDF 视图,允许独立的纸面布局。它不参与三模式交互工作台,不持有工作台 mutation,也不作为审批取得核心内容的入口。本轮只做回归,避免字段删除误伤打印;不迁移或重新设计打印结构。
## 9. Rollout and Rollback
- 单一独立候选上线,不新增 migration、seed、flag 或生产数据写入。
- 切换前必须完成 PC/移动三模式和非报销回归;上线后使用公网真实 bundle + 合成 API 验收,不操作真人单据。
- 回滚是完整回到 B-2a 已部署版本;不保留运行时双壳开关。
- 真实三张 rejected 单据可与开发并行,任何反馈只通过新的规划修订进入本任务,不直接现场改产品代码。
## 10. UI/UX Basis
本任务不引入新视觉系统。采用 `ui-ux-pro-max` 对本场景适用的通用原则:一致标题层级、组合优于模式继承、异常不只靠颜色、反馈明确、触控目标至少 44×44、渐进披露避免把所有解释预先铺开。其数据库没有命中特定“审批工作台”React 模板,因此不采用不存在的模板结论,继续以项目现有组件和 `docs/产品设计规则.md` 的信息架构为基线。