feat(报销): 收敛只读内容与审批上下文 [09-02-reimbursement-readonly-content-consistency]

这个提交包含在:
2026-09-02 21:13:08 +08:00
父节点 dda625b15f
当前提交 e48514d542
共修改 20 个文件,包含 740 行新增和 47 行删除
@@ -20,7 +20,8 @@
"subtasks": [],
"children": [
"09-01-reimbursement-approval-content-workbench",
"09-01-reimbursement-form-shared-component-migration"
"09-01-reimbursement-form-shared-component-migration",
"09-02-reimbursement-readonly-content-consistency"
],
"parent": null,
"relatedFiles": [],
@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
@@ -0,0 +1,91 @@
# Design:报销只读内容一致性与审批上下文收敛
## 1. Information Classification Contract
采用来源驱动的三分法,不按页面、区块位置或当前用户分类:
```text
ReimbursementDeclaredContent(报销人申报)
├─ header / description
├─ expense items / authoritative total
├─ invoice and attachment facts
└─ declared project / travel association
ReimbursementProcessingContext(审批人或系统形成)
├─ expense-policy / budget snapshot
├─ invoice amount-evidence state / B2 result
├─ processing progress
├─ workflow history
└─ decision actions
ReimbursementArchiveMetadata(打印动作形成)
└─ QR code / printedAt / printedBy
```
字段是否出现在打印页不决定类别。打印页可以组合正文、处理上下文和归档元数据;三模式一致性只约束 `ReimbursementDeclaredContent`。
## 2. Workspace Model Boundary
保持 B-2b 的壳/适配器边界,不建立第二套页面模型。共享工作台模型增加一个明确的权威明细汇总投影,PC/移动语义组件都消费同一值:
```ts
type ReimbursementAmountSummary = {
authoritativeTotalAmount: string | null;
integrity: "ready" | "mismatch" | "unavailable";
};
```
`authoritativeTotalAmount` 只来自 reimbursement detail 的服务端 `totalAmount`。PC `tfoot`、移动汇总行和页头均格式化同一字段;任何页面或渲染组件不得用 `items.reduce()` 生成展示总额。
完整性判断只在共享模型构建边界执行一次。明细金额以严格十进制字符串解析为整数分后求和,仅生成 `integrity`,派生和不得暴露为第二个业务总额或参与提交。无法安全解析时使用 `unavailable`,不伪造 mismatch;服务端仍是业务权威。
## 3. Integrity Failure Behavior
- `ready`:只显示普通合计,不显示说明。
- `mismatch`:在明细合计附近显示“报销总额与明细汇总不一致,请联系管理员”,并作为 blocking issue 注入审批 readiness,使通过、退回、驳回、转审全部 fail-closed。
- `unavailable`:保留服务端权威总额展示,同时说明“明细金额无法完整核对,请联系管理员”,并按既有“关键内容未就绪不得审批”合同 fail-closed;它不是 mismatch,也不得显示虚构差额。
异常不提供“修改总额”入口,不改变 B2 或服务端 recompute 合同。
## 4. Desktop and Mobile Amount Presentation
PC 在现有语义表格中使用真正的 `<tfoot>`,列跨度随 edit 操作列存在与否调整;标签为“明细合计”,金额单元格保持右对齐、tabular numerals,并通过 scope/可访问文本说明这是服务端权威总额。
移动端不模拟表格,在明细卡片组末尾渲染同一 `ReimbursementAmountSummary` 的汇总行。它与页头金额允许重复,因为两者分别服务“整单识别”和“逐条核对后的闭合”,但值与可访问名称必须一致。
## 5. Approval History Placement
PC approval 由适配器组合右栏内容,不让共享壳判断模式:
```text
BusinessDetailAside
├─ sticky DecisionActionPanel
└─ WorkflowHistoryPanel
```
审批动作卡继续 sticky;历史卡位于动作之后但不并入 sticky 容器,避免 6 条历史或长意见改变动作可达性。PC 主列不再传 `timeline`。移动适配器继续把相同完整历史传入正文末尾,底部动作保持独立安全区。
不新增历史摘要,不截断最近一条意见,不保留主列隐藏副本。历史空状态仍可见,但不使用告警视觉。
## 6. Progress Versus History
`ProcessingProgress` 和 workflow timeline 不合并:
- 普通详情的进度模型面向申请人,包含未来阶段、当前阶段和唯一付款批次入口。
- 审批 timeline 面向处理人,只呈现已发生 workflow 事件及意见。
共享的是格式化与动作/状态语义,不强求同一 DOM。测试使用同一 workflow fixture 对照已发生事件,证明名称、人员、时间和结果不矛盾;未来阶段只允许出现在进度模型。
## 7. Print and Compatibility Boundary
打印页继续作为组合归档视图:明细合计仍直接显示服务端 `totalAmount`,审批记录和归档元数据保持现状。本任务不把打印页迁入工作台,也不删除它的审批记录。
不改 API、schema、migration 或状态机。现有 B2、材料查看审计、离线缓存白名单和 capability 注入全部保持。
## 8. UI/UX Constraints
沿用 `ui-ux-pro-max` 适用规则:使用语义表格元素;长历史意见可自然换行且不得溢出;异步内容预留稳定位置;金额采用本地化格式;异常不能只靠颜色。PC 1280px 无页面级横向滚动,移动 375px/横屏无内容遮挡,触控目标不小于 44×44。
## 9. Rollout Boundary
本设计经最新规划评审批准后可以进入实施,不再等待父任务 AC11。commit、push、部署继续使用独立批准门。部署后由 AC11 检查三项假设:财务是否认可明细区合计、右栏历史是否更利于决策、申请人是否混淆进度与审批历史;反馈若推翻方案,再以新事实开启修订,不现场扩 scope。
@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
@@ -0,0 +1,51 @@
# Implementation Plan:报销只读内容一致性与审批上下文收敛
## Entry Gate
- [x] 用户已批准“先改进、后由 AC11 验收”的最新规划摘要,并运行 `task.py start`。
- [x] 实施前加载 `trellis-before-dev`,读取 frontend、reimbursement、workflow、privacy 和 UI QA 规约。
## Implementation Order
1. [x] 在共享工作台模型增加 `ReimbursementAmountSummary`,以服务端 `totalAmount` 作为唯一展示总额;用严格十进制分值解析生成仅用于完整性判断的状态。
2. [x] 扩展共享费用明细组件:PC 添加语义 `tfoot`,移动添加等价汇总行;edit/readonly/approval 均由同一模型注入。
3. [x] 把 mismatch/金额不可解析转为各自准确、可定位的 blocking issue,并接入审批 readiness 二次拦截;不提供手工改总额入口,不改变服务端合同。
4. [x] PC approval 将完整历史从主列移到右栏动作卡之后,动作卡保持独立 sticky;移动历史位置不变。
5. [x] 明确正文/上下文负向结构证据:三模式正文不得各自汇总金额;打印元数据不得进入工作台;审批上下文不得进入 edit/readonly 正文一致性断言。
6. [x] 更新 `docs/产品设计规则.md` 的历史布局规则:短审批流程可在 PC 右栏动作之后展示完整历史且主列不重复,移动仍在正文末尾;保留长流程使用主列的通用边界。
7. [x] 更新相关 PC/移动三模式、打印、权限、B2、离线和非报销回归;不修改真人单据。
## Verification
- [x] 纯函数:权威总额透传;`0`、小数、千位、非法金额、正常一致、mismatch、unavailable;使用整数分比较,无浮点误差。
- [x] 组件:PC `tfoot` 的 colspan 随 edit 操作列正确;移动汇总语义等价;三模式显示值一致;正常状态无额外说明。
- [x] 审批:mismatch 四动作 fail-closed;右栏动作在 6 条历史和长意见下保持可达;主列 timeline 为 0;历史只出现一次。
- [x] 处理语义:普通详情仍有 `ProcessingProgress` 与唯一付款批次入口;审批 timeline 不出现未来阶段;同一已发生事件的动作、人员、时间、结果一致。
- [x] 回归:edit 能维护、readonly 无维护、approval 无报销维护、B2 未归属恢复、打印合计/审批记录/二维码/打印元数据、移动离线白名单和非报销审批。
- [x] `cd client && npm run typecheck && npm run build`。
- [x] `node scripts/qa/check-ui-design-rules.mjs --strict` 与 `--self-test`。
- [x] 相关 Playwright:1280px、375px、横屏、三模式、长历史、完整性故障、打印回归。
- [x] `python3 ./.trellis/scripts/task.py validate 09-02-reimbursement-readonly-content-consistency`。
- [x] `node scripts/qa/check-process-contract.mjs --baseline --json`、`node scripts/qa/check-trellis-governance.mjs`、敏感内容检查、docs registry、`git diff --check`。
## Review and Delivery Boundary
- [x] 涉及 workflow 决策 readiness,实施后执行只读 review;inline 模式不派发 implement/check 子代理。
- [x] 无 schema、migration、flag、seed 或生产数据写入。
- [ ] commit、push、部署分别等待明确批准;部署后仅用公网真实 bundle + 合成 API 和生产只读事实验收。
- [ ] 本子任务部署不替代父任务 AC11 真人反馈;AC11 未完成时父任务继续保持开放。
## Rollback
- 模型或 fail-closed 回归失败:停止在本地,恢复到本任务前候选,不引入 feature flag 双轨。
- PC 历史布局影响动作可达:保留移动行为,PC 回到主列 timeline,再复核右栏结构。
- 公网验收失败:回滚应用候选,不处理业务数据。
## Local Verification Evidence
- `npm run build`:PASS(包含 typecheck 与 Vite production build)。
- `workflow-action-confirm-ui.spec.ts`:7/7 PASS,覆盖正常负向断言、两类完整性故障四动作 fail-closed、PC 六条长历史和移动 375px/横屏安全区。
- `r102-reimbursement-main-flow.spec.ts`:6/6 PASS;`r111-invoice-reimbursement-guard.spec.ts`:8/8 PASS。
- 共享模型/结构合同:13/13 PASS;移动报销离线缓存专项以 Chromium iPhone 视口 1/1 PASS。WebKit 浏览器二进制未安装,因此原 WebKit 命令为环境 PAUSED,不改测试口径。
- UI strict/self-test、task validate、process contract、Trellis governance、敏感内容、docs registry(保留既有 22 个未登记文件 warning)和 `git diff --check` 均通过。
- `bash scripts/check-minimal.sh` 的本轮相关检查通过;全量后端为 1364/1368,4 条失败均不在本任务改动面:3 条因当前 Python 缺少固定 BIFF8 依赖,1 条是 HEAD 已存在的通用审批页结构断言漂移。本任务不修改 server、付款导出或通用审批页。
@@ -0,0 +1,98 @@
# 报销只读内容一致性与审批上下文收敛
## Goal
让同一张报销单在 PC/移动、编辑/只读/审批和不同授权用户眼中保持同一份报销正文语义,同时把审批动作、审批历史和员工处理进度明确留在各自上下文中。补齐费用明细后的权威合计,避免审批人依赖页头回看或自行心算;在不复制内容、不引入第二业务金额来源的前提下保持 KISS。
## Background and Confirmed Facts
- B-2b 已部署统一工作台并通过公网真实 bundle + 合成 API 验收;旧报销审批分支已经删除,当前父任务仍等待 AC11 真人重提反馈。
- PC/移动工作台头部均显示服务端 `detail.totalAmount`,但共享费用明细组件没有明细区合计;打印页已有“合计”行,也直接显示同一个 `detail.totalAmount`。
- 服务端 `recomputeTotalAmount` 在明细变化和提交链路按明细聚合重写 `reimbursement_forms.total_amount`;`docs/报销发票附件规则.md` 明确 PC、手机和打印页只展示服务端权威总额,不提供手工填写。
- 当前审批适配器把完整 `timeline` 注入主列末尾;PC 右栏只放审批决定,移动端历史也位于内容末尾。
- 普通报销详情使用 `ProcessingProgress` 回答“我的报销/付款走到哪一步”;审批任务使用 workflow timeline 回答“谁执行了什么动作、留下什么意见”。二者服务不同问题,不视为同一组件的视觉分叉。
- 当前打印页包含三类来源不同的信息:报销人申报的报销头/事由/明细/合计/票据,审批人或系统形成的审批记录,以及打印动作形成的二维码/打印人/打印时间。页面位置不能替代信息来源成为分类依据。
## Scope
- 定义并验证报销正文、处理上下文和归档元数据的来源分类合同。
- 在统一工作台的 PC/移动明细区补充服务端权威合计,并建立仅用于完整性判断的共享状态。
- 将 PC 报销审批完整历史移入右栏动作之后且只保留一份;移动历史位置不变。
- 固化 `ProcessingProgress` 与 workflow timeline 的职责分工和一致性回归。
- 更新与上述行为直接冲突的产品体验事实和自动化验收;不改业务数据合同。
## Product Requirements
### R1 按信息来源划分正文、处理上下文和归档元数据
- 业务正文是报销人申报的内容:报销头、事由、费用明细、权威明细合计、票据与附件,以及由报销人申报的关联项目/差旅信息。PC/移动的 edit、readonly、approval 必须消费同一正文模型并逐字段一致。
- 处理上下文是审批人或系统对本次申报形成的判断与记录:提交时快照、费用标准/预算判断、票据金额证据状态、B2 检查、处理进度、审批历史和审批动作。上下文可以按模式和权限出现或省略;省略不构成正文不一致,但同一事实在不同模式出现时语义不得冲突。
- 归档元数据是打印动作形成的信息:二维码、打印时间和打印人。它只属于打印页,不进入交互工作台正文。
- 打印页是三类信息的组合交付视图,不再作为“页面内所有字段都是正文”的判据;字段归属只由来源决定。
- 布局可以响应设备变化:PC 合计使用语义表格 `tfoot`,移动端在明细卡片组末尾使用等价的“明细合计”汇总行;字段含义、金额来源和可访问名称必须一致。
- 模式只增加能力和上下文:edit 增加维护/提交能力,approval 增加快照、决策和 workflow 历史,readonly 不增加业务动作。不得为不同角色复制或重新计算正文。
### R2 明细合计只展示服务端权威总额
- 明细区合计显示统一工作台模型中的服务端 `totalAmount`,不得在 PC 表格、移动卡片或各页面用 `items.reduce()` 生成可展示的第二总额。
- 若实现完整性核对,明细派生和权威总额只能在共享模型构建边界比较一次;派生值仅用于判断完整性,不得作为报销金额展示或提交。
- 不一致不是正常业务“差额”,必须显示为数据完整性故障,文案明确为“报销总额与明细汇总不一致,请联系管理员”,并阻止审批动作;不得暗示用户可手工修正总额。
- 明细金额无法安全解析时不得伪造差额;应明确提示无法完整核对并按既有关键内容未就绪合同阻止审批动作。
- 金额比较必须使用十进制定点/分值口径,不能使用二进制浮点直接累加。
### R3 审批历史在 PC 右栏只保留一份
- PC approval 将完整 workflow 处理历史移到右侧审批上下文,位于审批动作之后;主列不再重复 timeline。
- 历史条目保留动作、处理人、时间和完整意见;正常短流程不新增“历史摘要 + 完整历史”双层表达。
- 历史增长不得把主要审批动作挤出可达区域;右栏的 sticky、滚动和长意见折行策略在设计阶段明确,并覆盖 1280px 与长退回意见。
- 移动端不复制 PC 右栏,完整历史继续位于正文之后、底部动作之前或安全区动作之前的既有阅读顺序。
### R4 处理进度与处理历史明确分工
- `ProcessingProgress` 保留在普通报销详情,回答员工“当前走到哪一步、付款到哪一步”,并继续提供唯一付款批次入口。
- workflow timeline 保留在审批任务,回答审批人“谁处理过、执行了什么动作、留下什么意见”。
- 两者可以使用不同视觉结构,但动作名称、人员、时间和状态含义不得互相矛盾;本任务不强行合并为一个组件。
### R5 先改进,再由 AC11 完成业务验收
- 用户决定不再以 AC11 真人反馈作为本子任务的实施前置;本规划经最新评审明确批准后即可 `task.py start`。
- 本子任务只改变已冻结的合计、历史位置和上下文表达,不借“先改进”继续扩充范围;commit、push、部署仍分别等待明确批准。
- 父任务 AC11 仍是最终业务验收:部署后至少记录一次员工真实重提与一次财务真实复审,核对员工提示、正文一致性、明细合计、PC 历史位置和进度/历史理解。
- 真人反馈若推翻已上线行为,必须作为新事实回到规划,不以现场猜测直接改代码;在 AC11 完成前不得归档父任务。
### R6 不扩展业务与数据范围
- 不修改 schema、migration、B1/B2、费用类型、workflow 状态机、材料权限或生产数据。
- 不操作 `0016/0017/0018`;它们仍用于父任务 AC11 的真人反馈。
- 不借本任务处理部署脚本、`.gitignore`、错误色 token、AGENTS skill 名称或其他治理债。
## Acceptance Criteria
- [x] AC1:同一合成报销数据在 PC/移动 edit、readonly、approval 中的报销头、事由、明细、合计、票据/附件和申报关联信息逐字段一致;差异仅来自已批准的能力与处理上下文。
- [x] AC2:PC 费用明细表使用 `tfoot` 显示“明细合计 + 服务端权威总额”;移动卡片组末尾显示语义等价汇总。三模式、不同用户均从同一工作台模型取值。
- [x] AC3:代码与静态检查证明 PC/移动明细组件不存在用于展示总额的 `items.reduce()` 或页面级金额汇总;展示金额与页头、打印页使用同一 `totalAmount`。
- [x] AC4:mismatch 与金额不可解析 fixture 分别触发准确故障提示并使报销审批 fail-closed;正常 fixture 不显示差额或“0 条”说明。
- [x] AC5:PC approval 的完整历史只在右栏出现一次且位于动作之后;主列 timeline 为 0。1280px、长意见和 6 条历史下动作可达、页面无横向溢出。
- [x] AC6:移动 approval 的历史仍在正文末尾,底部动作不遮挡;375px 与横屏无横向溢出,触控目标不小于 44×44。
- [x] AC7:普通详情继续使用 `ProcessingProgress`,审批继续使用 workflow timeline;付款批次入口仍只有一处,二者没有状态/动作语义冲突。测试明确证明票据本体属于正文,而票据金额证据状态属于可按模式展示的上下文。
- [x] AC8:edit 维护能力、readonly 零维护能力、approval 零报销维护能力和 B2 未归属恢复均无回归。
- [x] AC9:打印视图继续显示服务端权威合计且不获得工作台 mutation;二维码、打印人和打印时间不进入交互工作台,审批记录不被误纳入三模式正文一致性断言。
- [x] AC10:无 schema、migration、flag、seed 或生产数据写入;通过 typecheck/build、相关 Playwright、UI strict/self-test、Trellis governance 和 `git diff --check`。
- [ ] AC11:本子任务部署后,父任务记录员工真实重提和财务真实复审反馈;反馈未完成不阻止本子任务实施,但继续阻止父任务归档。
## Test Plan
- 共享模型:严格十进制金额解析、权威总额透传、正常一致、mismatch、金额不可解析和空明细边界。
- PC/移动组件:`tfoot`/移动汇总行、三模式逐字段一致、编辑操作列 colspan、正常无解释、完整性故障可访问文案。
- 审批行为:完整性故障四动作 fail-closed;PC 历史单一右栏位置、长意见与 6 条记录;移动历史和安全区动作不遮挡。
- 上下文一致性:同一 fixture 对照 `ProcessingProgress` 与 workflow timeline 的已发生事件;未来付款阶段只出现在进度模型,付款批次入口唯一。
- 回归:edit/readonly/approval capability、B2 未归属恢复、打印视图、移动离线缓存、非报销 workflow。
- 视觉与可访问性:1280px、375px、移动横屏,无页面级横向溢出;语义表格、键盘顺序、非颜色单一表达和 44×44 触控目标。
- 工程门禁:typecheck/build、相关 Playwright、UI strict/self-test、Trellis task validate/governance、process contract、docs registry、敏感内容检查和 `git diff --check`。
## Out of Scope
- 合并 `ProcessingProgress` 与 workflow timeline,或重做付款流程。
- 重新设计整张打印页、改变打印字段或归档政策;仅把它作为内容口径证据并保持回归。
- 在取得 AC11 反馈前据此宣称真人体验问题已经解决或归档父任务。
- 其他非报销 workflow 页面和历史债务。
@@ -0,0 +1,26 @@
{
"id": "reimbursement-readonly-content-consistency",
"name": "reimbursement-readonly-content-consistency",
"title": "报销只读内容一致性与审批上下文收敛",
"description": "统一报销工作台三模式的报销正文口径,补齐权威明细合计,并收敛 PC 审批历史与右侧决策上下文。",
"status": "in_progress",
"dev_type": null,
"scope": "trellis,client,docs",
"package": null,
"priority": "P2",
"creator": "shunli",
"assignee": "shunli",
"createdAt": "2026-09-02",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": "09-01-reimbursement-one-page-approval-workbench",
"relatedFiles": [],
"notes": "本地实施与专项验证完成,等待 diff 复核;权威合计、两类完整性 fail-closed、PC 历史右置和进度/历史职责合同已落地。未 commit、push、部署;AC11 仍待部署后真人单据验证。",
"meta": {}
}