文件
hyb-oa/.trellis/tasks/archive/2026-10/09-01-reimbursement-form-shared-component-migration/design.md
T
wangsli和Claude Opus 5.5 b85ff6a9bf 归档 11 个已上线 Trellis 任务并补收口记录(09-17-company-loan-principal-repayment 任务整理)
- 报销一页式审批工作台父任务及 B-2a/B-2b/只读一致性子任务(AC11 依用户 2026-09-27 确认)
- 报销详情材料预览修复及其部署子任务(由候选 59dc5f6 发布覆盖,原步骤未执行,记录保留)
- 资产事前批准纠偏、Trellis 流程对齐、ERP 遗留精简、预算池并发、采购提交超时
- 各任务 prd 顶部新增收口记录,实现均包含于生产 4a5cd8f2;原未勾清单保留不补勾
- B 组与有真实待办的任务保持进行中

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 10:19:21 +08:00

12 KiB

Design:B-2b 统一报销工作台迁移

1. Architecture Decision

采用“任务路由分派 + 唯一报销工作台内容合同 + PC/移动布局壳 + 三个模式适配器”。不把审批模式继续塞进现有大型详情页,也不让通用 workflow 页面继续拥有报销业务布局。

/workflow/tasks/:taskId              /mobile/approvals/:taskId
          │                                      │
          └──── authorized task route dispatcher ┘
                              │
                    entityType=reimbursement
                              │
             ReimbursementApprovalAdapter
                              │
             ReimbursementDesktopWorkspaceShell
                    (共享内容合同)

/reimbursements/:reimbursementId
          │
  reimbursement route selector
          ├─ participant + draft/rejected -> ReimbursementEditAdapter
          └─ otherwise authorized         -> ReimbursementReadonlyAdapter
                                             │
                         Desktop / Mobile WorkspaceShell
                              (共享内容合同)

任务分派器通过唯一一次已授权 task detail 查询选择实体渲染器,并把同一份 query result(含受控 refetch 能力)移交给所选渲染器。它不重复查询、不渲染报销摘要、不计算报销检查、不持有审批表单状态。非报销实体继续进入现有通用详情。

2. Shell Contract

PC/移动布局壳都是纯组合组件,共享模型和 capability 合同,但不强求同一份 DOM,也不接收 mode 用于业务判断:

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

报销单号 + 状态
报销人 · 日期 · 合计
事由
┌────────────────────────────────────┬────────────────────┐
│ 费用明细表                         │ 当前节点/状态      │
│ 票据与附件(发票/补充附件分组)    │ 可决策状态         │
│ 未归属恢复(仅有时)               │ 异常锚点(仅有时) │
│ 异常/快照差异(仅有时)            │ 审批/提交动作      │
│ 处理历史/进度                      │ sticky             │
└────────────────────────────────────┴────────────────────┘

Mobile

标题/状态 → 报销人/日期/合计 → 事由 → 费用明细卡 → 票据与附件(分组)→ 未归属恢复(仅有时)→ 异常/差异(仅有时)→ 历史/进度 → 底部动作。

正常检查不生成卡片。异常模型必须带对象引用和锚点:

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 的信息架构为基线。

11. Post-deployment KISS Evidence Layout

上线真人目视反馈证明,统一工作台架构正确,但 ReimbursementItemEvidenceList 仍把“费用明细、所属票据、文件动作、退回引用”压进同一嵌套卡片。收尾迭代保持适配器和 capability 边界不变,把展示层拆成三个语义组件:

ReimbursementEvidenceSection
├─ ReimbursementExpenseItems
│  ├─ desktop: ExpenseItemsTable
│  └─ mobile: ExpenseItemCards
├─ ReimbursementMaterials
│  ├─ invoices
│  └─ supplementary attachments
└─ ReimbursementReturnReferencePicker(仅退回动作展开时挂载)
  • ExpenseItemsTable 与 ExpenseItemCards 消费同一行模型:类型、日期、说明、金额、票据数量/状态、异常摘要、稳定锚点。PC/移动可以不同 DOM,不得各自重新计算业务状态。
  • 发票不再嵌套在明细 DOM 内;projection 继续保留 linkedItemId,由纯函数派生每行票据摘要和独立材料列表。点击问题锚点仍能定位到对应明细行或材料行。
  • 文件行的文件名/整行采用语义按钮或链接作为唯一查看入口;视觉上不重复“查看原件/查看附件”。移动命中区域不小于 44px,键盘焦点和 aria-label 必须包含材料类型与文件名/票号。
  • edit capability 映射为明细单一编辑入口和材料区维护入口;readonly/approval 不注入维护能力。未归属或阻断票据是例外,可在材料行就地显示关联恢复动作。
  • onReferenceItem / onReferenceInvoice 不再传给常驻明细/材料组件;审批适配器仅在 activeSecondaryAction === "return" 时把异常优先的候选模型传给引用选择器。选择器只写入本地退回意见文本,不调用 API。
  • “查看完整材料”是区块级批量预览能力,与单文件入口并存时必须保持次要层级;单文件打开仍沿用既有短时 URL、用途审计和失败隔离。

参考 /home/shunli/shenzhou-business-suite 的费用明细表、移动卡片和附件分区只用于确定信息架构。实现不依赖该项目代码,也不复制其权限、状态或样式实现。