diff --git a/.trellis/spec/hyb-oa/frontend-react-vite-design.md b/.trellis/spec/hyb-oa/frontend-react-vite-design.md
index b5d19737..9887577c 100644
--- a/.trellis/spec/hyb-oa/frontend-react-vite-design.md
+++ b/.trellis/spec/hyb-oa/frontend-react-vite-design.md
@@ -168,6 +168,65 @@ reference, not a copy of Feishu or Ant Design branding.
| `请假记录` / `考勤` | 业务台账列表 or record view | Enterprise WeChat-owned records, mirror status, exceptions and audit | Ordinary users do not see OA daily leave/punch approval creation; OA emergency entry only for authorized emergency/admin scope. |
| Mobile routes | Mobile expression of the same template | Top status/focus, card list or sectioned detail | One bottom safe-area primary action; secondary actions in `更多` or confirmation. |
+## Scenario: Approval Task Business Detail Exit
+
+### 1. Scope / Trigger
+
+- Trigger: an approval task detail shows only a business summary while the approver must inspect line items, invoices, attachments or other evidence before acting.
+
+### 2. Signatures
+
+- Mobile reimbursement target: `/mobile/reimbursements/:reimbursementId`.
+- PC reimbursement target: `/reimbursements/:reimbursementId`.
+- Mobile source state: ` `.
+
+### 3. Contracts
+
+- The pending-task list card continues to open `/mobile/approvals/:taskId`; do not confuse task selection with the separate business-detail exit inside the task detail.
+- When `WorkflowTaskDetail.reimbursement` exists, the task detail exposes an explicitly named `查看完整报销单` or `查看报销单` link near the reimbursement summary.
+- Reuse the existing business detail route and its permissions. Do not duplicate reimbursement items, invoice or attachment viewers inside the approval page merely to add reachability.
+- Mobile navigation passes the current approval task URL through the existing `state.from` convention so the business detail back button returns to that task. This is a one-level source contract, not a new nested return stack.
+- Approval actions remain on the approval task page. Opening the business detail does not approve, reject, return or mutate the task.
+
+### 4. Validation & Error Matrix
+
+- Task has a reimbursement -> show the reimbursement-detail link.
+- Task is another entity type -> do not show a reimbursement link; add that entity's route only in a scoped task.
+- Reimbursement summary is absent -> render no broken or guessed target.
+- Business detail permission fails -> preserve the existing permission-denied behavior; do not expand roles from the frontend.
+
+### 5. Good/Base/Bad Cases
+
+- Good: approver opens the task, follows `查看完整报销单`, inspects evidence, returns to the same task and then chooses an approval action.
+- Base: a completed reimbursement task still provides the read-only business-detail exit when its existing detail permission allows it.
+- Bad: make a static-looking summary card secretly clickable, change the pending-list click handler, or copy a partial invoice/attachment viewer into the task page.
+
+### 6. Tests Required
+
+- Mobile E2E: task detail -> reimbursement detail -> items/evidence visible -> back to the same task -> approval actions still visible.
+- PC E2E: task detail link -> existing reimbursement detail route and overview visible.
+- Assert non-reimbursement task details do not receive a reimbursement target.
+- Run `cd client && npm run typecheck && npm run build`.
+
+### 7. Wrong vs Correct
+
+#### Wrong
+
+```tsx
+
handleTaskSelection(task.id)}>报销摘要
+```
+
+#### Correct
+
+```tsx
+
+ 查看完整报销单
+
+```
+
**Implementation order for design refactors**:
1. A-class frequent pages: workbench, pending tasks, workflow task detail,
diff --git a/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/design.md b/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/design.md
index f25e108a..8e825ffc 100644
--- a/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/design.md
+++ b/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/design.md
@@ -147,3 +147,12 @@
- 后续只有出现第二名实际审批主管,或有人基于真实职责承担 `finance_mgr`,并且业务证明需要金额升级时才重开分级设计。大额关注使用风险标记或通知,不通过同一自然人重复审批实现。
- 生产变更复用 `PATCH /api/approval-rules/:id`:先把低档规则扩为 `[0,+∞)`,再停用中、高两档,避免出现无人可匹配的短暂金额空窗;每步保留 `APPROVAL_RULE_UPDATED` 审计,失败则通过同一接口补偿恢复原三档。
- 最终审批只把报销置为 `approved`,不会自动创建付款批次或付款记录;付款批次仍由王芝玲主动创建并由马建勇复核。
+
+## 9. 审批任务到完整报销单的可达性
+
+- 根因不在待办列表卡片:移动待办列表已经通过 `handleTaskSelection` 进入 `/mobile/approvals/:taskId`。真实阻断发生在审批任务详情,其报销摘要只有单号、申请人和金额,没有费用明细、发票和附件,也没有进入既有报销详情的出口。
+- 移动端只在审批任务详情的报销摘要区增加明确的“查看完整报销单”入口,目标复用 `/mobile/reimbursements/:reimbursementId`;导航继续使用当前移动页面通用的 `{ state: { from: currentMobileSource } }` 约定,不新增 `returnTo` 参数或另一套回跳状态。
+- PC 端在 `WorkflowTaskDetailPage` 的报销摘要区提供“查看报销单”入口,目标复用 `/reimbursements/:reimbursementId`。
+- 现有 `manager`、`ceo` 和 `finance` 角色已经包含 `expense.view_all`,因此本轮不调整角色、权限矩阵、后端报销详情鉴权或 workflow 数据结构。
+- 不把整个摘要卡片改造成无提示的隐式链接;使用明确命名的链接按钮,避免与待办列表卡片的既有任务选择交互混淆。
+- 请假、采购、差旅审批详情存在同类业务详情缺口,但本轮只修复真实报销链;其他类型另轮统一处理。
diff --git a/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/implement.md b/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/implement.md
index 45841b91..f8e27832 100644
--- a/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/implement.md
+++ b/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/implement.md
@@ -67,11 +67,19 @@
- [x] 在杨俊提交前通过现有审批规则管理接口把三档规则收敛为唯一 `[0,+∞)` 规则,审批链固定为 `department_manager → finance`;不扩角色、不保留 CEO 重复节点。
- [x] 规则写入前后只读核对 499.99 / 500 / 5000 三个金额,确认解析结果完全相同且 active `finance` 有唯一实际处理人。
- [ ] 请杨俊本人从企业微信进入 OA,确认身份解析成功并能看到报销入口。
-- [ ] 杨俊使用真实业务数据创建报销,至少添加一条费用明细并上传/绑定状态有效的真实票据后提交。
+- [x] 杨俊使用真实业务数据创建报销,至少添加一条费用明细并上传/绑定状态有效的真实票据后提交。
- [ ] 确认马建勇能看到该真实审批待办并按业务事实处理。
- [ ] 审批通过后确认单据进入王芝玲负责的财务处理链路;后续付款批次继续遵守既有 maker-checker。
- [ ] 用户验收期间不在聊天、截图或任务证据中粘贴票据、金额、银行资料或完整账号。
+## 6.1 审批详情可达性修复
+
+- [x] 保留移动待办列表卡片现有 `handleTaskSelection` 行为,不修改任务列表到审批任务详情的路由。
+- [x] 在 `MobileApprovalsPage` 的审批任务详情报销摘要区增加“查看完整报销单”,复用 `/mobile/reimbursements/:id` 和现有 `state.from` 来源回跳约定。
+- [x] 在 `WorkflowTaskDetailPage` 的报销摘要区增加“查看报销单”,复用 PC 报销详情路由。
+- [x] 不修改后端、审批状态机、权限矩阵或生产数据;请假、采购、差旅同类入口缺口仅记录,不在本轮扩展。
+- [x] 增加聚焦前端回归,验证移动进入/返回及 PC 入口;运行前端 typecheck、build、Trellis task/governance 和 `git diff --check`。
+
## 7. 完成条件
- [ ] PRD 全部验收项有脱敏证据或明确由用户完成的真实业务验收结果。
@@ -98,3 +106,10 @@
- 审批规则 finding:`[0,500)` 可由马建勇和唯一 finance 王芝玲处理;`[500,5000)` 与 `[5000,+∞)` 均以 `finance_mgr` 收尾,但 active `finance_mgr=0`,达到 500 元会形成无人处理待办。未修正规则前不得笼统通知杨俊可提交。
- 审批规则收口:写入前报销 reviewing/工作流 pending/待办 pending 均为 0;备份 watchdog 与 ARM dead-man 均为 success。正式接口更新 3 次、审计新增 3,生产仅剩 1 条 `[0,+∞)` active 规则。
- 金额边界验证:实际工作流构造函数对 499.99、500、5000 均输出 `department_manager → finance`;直属主管解析到马建勇,active finance 唯一且由王芝玲承担。写后仍无报销、实例或待办新增,公网 ready 正常、构建标识未变化。
+- 真实报销生产只读核验:杨俊最新真实报销为 `reviewing`,工作流为 `pending`,当前唯一待办为主管审批且已指派主管;付款批次 0。未输出金额、单号、UUID、附件或原始 payload,未执行生产写入。
+- 真实验收 finding:主管能进入审批任务详情,但该页只有报销摘要,没有费用明细、发票、附件或完整报销单入口;因此尚未按业务事实审批。
+- 审批详情入口本地实现:移动任务详情新增“查看完整报销单”,复用移动报销详情和 `state.from`;PC 任务详情新增“查看报销单”。未修改后端、权限矩阵、审批状态机或列表卡片点击行为。
+- 首次聚焦 Playwright 运行因本地前端服务未启动,9 个用例均在 `/login` 前以 `ERR_CONNECTION_REFUSED` 失败,未执行到功能断言;启动 Vite 后改为只跑本次相关用例。
+- 移动聚焦用例首次进入/返回链路通过,随后因固定在 2026-05 的“已处理”测试数据超过页面最近 30 天窗口而失败;已把完成时间改为运行时近期时间,避免日历漂移。
+- 最终本地验证:`npm run typecheck` 通过,`npm run build` 通过;移动审批入口聚焦 Playwright 1/1 通过,PC 审批入口聚焦 Playwright 1/1 通过。
+- 交付门禁:当前 task validate、Trellis governance、process contract、敏感扫描和 `git diff --check` 均通过;文档注册审计为 0 error、1 条既有未注册文档汇总 warning。
diff --git a/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/prd.md b/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/prd.md
index 9436f1cc..73eaa452 100644
--- a/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/prd.md
+++ b/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/prd.md
@@ -36,10 +36,11 @@
- 最近部署后必须重装自动备份三项 service/timer 和日常巡检 timer,使 unit BUILD_SHA 与活动 server 一致;重装后手工触发一次真实自动异机备份,确认源端成功且 ARM 恢复点新鲜,再允许写入杨俊账号或主管关系。
- 第一笔真实业务由员工本人正常发起,目标链路为“员工提交 → 真实主管审批 → 财务处理”;付款批次仍遵守既有 maker-checker。
- 当前小团队取消报销金额分层,所有金额统一按“直属主管 → finance”审批;不得保留无人持有的 `finance_mgr` 节点,也不得因马建勇同时是主管和 CEO 而让同一人重复审批。
+- 真实首笔报销暴露审批可读性阻断:待办列表卡片能够进入审批任务详情,但任务详情只显示单号、申请人和金额摘要,没有通往费用明细、发票和附件的业务详情入口。移动端必须在审批任务详情区提供明确的“查看完整报销单”入口,复用既有 `/mobile/reimbursements/:id` 路由和 `state.from` 回跳约定;PC 审批任务详情同步提供报销单入口。
## Scope
-本轮范围只包括:修复生产 automatic-backup/WARCH3 timer 的构建合同并取得新鲜异机恢复点;从通讯录导入杨俊及两个既有企微测试成员;设置各自真实或测试主管关系;用两个测试成员完成一条可清理的生产报销体验验收;清理测试业务数据并停用测试 OA 账号;最后由杨俊本人使用真实数据发起报销。所有真实报销内容由杨俊本人提供和提交,所有真实审批与财务处理由对应真实人员完成。
+本轮范围只包括:修复生产 automatic-backup/WARCH3 timer 的构建合同并取得新鲜异机恢复点;从通讯录导入杨俊及两个既有企微测试成员;设置各自真实或测试主管关系;用两个测试成员完成一条可清理的生产报销体验验收;清理测试业务数据并停用测试 OA 账号;最后由杨俊本人使用真实数据发起报销;修复真实审批任务详情到完整报销单的移动端和 PC 入口。所有真实报销内容由杨俊本人提供和提交,所有真实审批与财务处理由对应真实人员完成。
## Acceptance Criteria
@@ -60,6 +61,8 @@
- [x] 生产已显式关闭 mock OCR;`off` 路径已用测试账号复验,空发票元数据仍可绑定、提交、主管退回和清理,真实使用暂由财务查看 PDF 原件并人工核对。
- [x] 本轮没有调用企微成员写接口;企微成员资料未被 OA 反向修改。
- [x] 生产审批规则已收敛为唯一全金额规则,499.99、500、5000 三个边界均解析为 `department_manager → finance`,且没有 CEO 重复节点或 `finance_mgr` 空节点。
+- [ ] 移动审批任务详情中的报销摘要区提供明确的“查看完整报销单”入口,进入现有移动报销详情并能通过既有 `state.from` 返回原审批任务。
+- [ ] PC 审批任务详情提供对应的“查看报销单”入口;两端均不修改待办列表卡片的既有点击行为,不修改审批状态机、权限矩阵或后端数据合同。
## Out of Scope
@@ -70,6 +73,7 @@
- 不在未获单独批准时执行通讯录 apply、主管/法体写入、生产报销或付款操作。
- 不使用真实发票、真实金额、真实银行资料或真实员工个人资料作为测试材料;不让测试单进入“通过/待财务/付款”状态。
- 不把手机号缺失视为导入阻断项;该员工使用企微或企业邮箱登录,后续按需补手机号。
+- 不在本轮顺带修复请假、采购、差旅等其他审批类型的同类“业务单据详情入口”缺口;将其记录为后续统一审批中心体验任务候选。
## Test Plan
@@ -77,3 +81,4 @@
- 通讯录聚焦 dry-run 仅输出人数、计划、冲突、跳过和失败计数。
- 生产只读 SQL/Prisma 只返回布尔值和计数,不输出手机号、邮箱、企微 userid、完整账号或原始 payload。
- 使用既有报销 workflow/权限聚焦回归验证主管解析和 `expense.approve`,不写生产业务单据。
+- 前端运行 `cd client && npm run typecheck && npm run build`,并用聚焦 Playwright 覆盖:移动审批任务详情进入完整报销单、返回原任务后审批动作仍可见,以及 PC 审批任务详情进入报销单。
diff --git a/client/src/pages/MobileApprovalsPage.tsx b/client/src/pages/MobileApprovalsPage.tsx
index 4e83e9fc..e163d755 100644
--- a/client/src/pages/MobileApprovalsPage.tsx
+++ b/client/src/pages/MobileApprovalsPage.tsx
@@ -1177,8 +1177,19 @@ export function MobileApprovalsPage() {
) : null}
{selectedTask.reimbursement ? (
-
- {joinBusinessMeta([`报销日期:${selectedTask.reimbursement.reimbursementDate ?? "-"}`, selectedTask.reimbursement.description])}
+
+
+ {joinBusinessMeta([`报销日期:${selectedTask.reimbursement.reimbursementDate ?? "-"}`, selectedTask.reimbursement.description])}
+
+
+
+ 查看完整报销单
+
+
) : null}
{selectedTask.leaveRequest ? (
diff --git a/client/src/pages/WorkflowTaskDetailPage.tsx b/client/src/pages/WorkflowTaskDetailPage.tsx
index 80f9128c..81885e99 100644
--- a/client/src/pages/WorkflowTaskDetailPage.tsx
+++ b/client/src/pages/WorkflowTaskDetailPage.tsx
@@ -398,6 +398,17 @@ export function WorkflowTaskDetailPage() {
>
) : null}
+
+ {detail.reimbursement ? (
+
+
+ 查看报销单
+
+
+ ) : null}
diff --git a/client/tests/e2e/dg7-mobile-information-dedupe.spec.ts b/client/tests/e2e/dg7-mobile-information-dedupe.spec.ts
index 22b5729d..63e66c69 100644
--- a/client/tests/e2e/dg7-mobile-information-dedupe.spec.ts
+++ b/client/tests/e2e/dg7-mobile-information-dedupe.spec.ts
@@ -242,6 +242,7 @@ function buildNotifications() {
function workflowTaskSummary(status: "pending" | "completed" = "pending") {
const isCompleted = status === "completed";
+ const completedAt = isCompleted ? new Date().toISOString() : null;
return {
id: isCompleted ? "task-dg7-completed" : "task-dg7",
instanceId: "instance-dg7",
@@ -252,7 +253,7 @@ function workflowTaskSummary(status: "pending" | "completed" = "pending") {
status,
outcome: isCompleted ? "approved" : null,
createdAt: "2026-05-12T08:00:00.000Z",
- completedAt: isCompleted ? "2026-05-12T09:00:00.000Z" : null,
+ completedAt,
workflowInstance: {
entityType: "reimbursement",
entityId: "reimbursement-dg7",
@@ -272,6 +273,7 @@ function workflowTaskSummary(status: "pending" | "completed" = "pending") {
function workflowTaskDetail(status: "pending" | "completed" = "pending") {
const isCompleted = status === "completed";
+ const completedAt = isCompleted ? new Date().toISOString() : null;
const history = [
{
id: "history-workflow-submit-dg7",
@@ -309,7 +311,7 @@ function workflowTaskDetail(status: "pending" | "completed" = "pending") {
outcome: isCompleted ? "approved" : null,
comment: null,
createdAt: "2026-05-12T08:00:00.000Z",
- completedAt: isCompleted ? "2026-05-12T09:00:00.000Z" : null,
+ completedAt,
},
workflowInstance: {
id: "instance-dg7",
@@ -989,6 +991,16 @@ test.describe("MIF7 移动信息架构整改", () => {
await mobile.page.getByTestId("mobile-approvals-detail-header").getByRole("button", { name: "返回" }).click();
await expect(mobile.page).toHaveURL(/\/mobile$/);
+
+ await mobile.page.getByTestId("mobile-pending-summary").getByRole("link").first().click();
+ await expect(mobile.page).toHaveURL(/\/mobile\/approvals\/task-dg7$/);
+ await mobile.page.getByTestId("mobile-approval-reimbursement-detail-link").click();
+ await expect(mobile.page).toHaveURL(/\/mobile\/reimbursements\/reimbursement-dg7$/);
+ await expect(mobile.page.getByTestId("mobile-reimbursement-items")).toBeVisible();
+ await mobile.page.getByTestId("mobile-reimbursement-detail-header").getByRole("button", { name: "返回" }).click();
+ await expect(mobile.page).toHaveURL(/\/mobile\/approvals\/task-dg7$/);
+ await expectApprovalActionsStable(mobile.page);
+
await mobile.page.goto("/mobile/approvals");
await expect(mobile.page.getByRole("tab", { name: "待我处理" })).toBeVisible();
await expect(mobile.page.getByRole("tab", { name: "已处理" })).toBeVisible();
diff --git a/client/tests/e2e/pc-detail-mobile-visual-wave3.spec.ts b/client/tests/e2e/pc-detail-mobile-visual-wave3.spec.ts
index a641995e..aa74656a 100644
--- a/client/tests/e2e/pc-detail-mobile-visual-wave3.spec.ts
+++ b/client/tests/e2e/pc-detail-mobile-visual-wave3.spec.ts
@@ -1097,6 +1097,12 @@ test("Wave3 PC 详情和通知中心视觉截图", async ({ page }) => {
await expect(page.getByText("审批动作")).toHaveCount(0);
await expect(page.getByRole("heading", { name: "处理", exact: true })).toBeVisible();
await expect(page.getByTestId("workflow-task-overview")).toBeVisible();
+ await page.getByTestId("workflow-task-reimbursement-detail-link").click();
+ await expect(page).toHaveURL(/\/reimbursements\/reim-wave3$/);
+ await expect(page.getByTestId("reimbursement-overview")).toBeVisible();
+ await page.goBack();
+ await expect(page).toHaveURL(/\/workflow\/tasks\/task-wave3$/);
+ await expect(page.getByTestId("workflow-task-overview")).toBeVisible();
await page.screenshot({ path: screenshotPath("02-pc-workflow-detail.png"), fullPage: true });
await page.goto("/notifications");