From 5f10b837062209fc06e2d9e065bd3da893d1b168 Mon Sep 17 00:00:00 2001 From: shunli Date: Fri, 29 May 2026 13:10:06 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=AE=A1=E6=89=B9=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=E4=B8=8E=E6=8A=A5=E9=94=80=E8=AF=A6=E6=83=85=E9=99=8D?= =?UTF-8?q?=E5=99=AA=EF=BC=88approval-detail-simplification-refactor?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../check.jsonl | 4 + .../design.md | 71 +++ .../implement.jsonl | 4 + .../prd.md | 115 +++++ .../task.json | 30 ++ client/src/pages/ReimbursementDetailPage.tsx | 456 ++++++++++++++---- client/src/pages/WorkflowTasksPage.tsx | 178 ++++--- .../ReimbursementActions.tsx | 43 +- .../e2e/pc-core-workflow-visual-wave2.spec.ts | 6 + .../e2e/pc-detail-mobile-visual-wave3.spec.ts | 7 +- .../e2e/r102-reimbursement-main-flow.spec.ts | 2 + 11 files changed, 739 insertions(+), 177 deletions(-) create mode 100644 .trellis/tasks/05-29-approval-detail-simplification-refactor/check.jsonl create mode 100644 .trellis/tasks/05-29-approval-detail-simplification-refactor/design.md create mode 100644 .trellis/tasks/05-29-approval-detail-simplification-refactor/implement.jsonl create mode 100644 .trellis/tasks/05-29-approval-detail-simplification-refactor/prd.md create mode 100644 .trellis/tasks/05-29-approval-detail-simplification-refactor/task.json diff --git a/.trellis/tasks/05-29-approval-detail-simplification-refactor/check.jsonl b/.trellis/tasks/05-29-approval-detail-simplification-refactor/check.jsonl new file mode 100644 index 00000000..ab98dc6e --- /dev/null +++ b/.trellis/tasks/05-29-approval-detail-simplification-refactor/check.jsonl @@ -0,0 +1,4 @@ +{"file": ".trellis/spec/frontend/index.md", "reason": "前端规约入口,用于核对 React/Vite 页面实现是否走正确规则来源。"} +{"file": ".trellis/spec/hyb-oa/index.md", "reason": "hyb-oa 规约入口,用于核对项目级检查范围。"} +{"file": ".trellis/spec/hyb-oa/project-governance.md", "reason": "核对 Trellis 任务合同、验证和提交治理要求。"} +{"file": ".trellis/spec/hyb-oa/frontend-react-vite-design.md", "reason": "核对 A 类 PC 页面任务优先、详情页命令面和前端验证规则。"} diff --git a/.trellis/tasks/05-29-approval-detail-simplification-refactor/design.md b/.trellis/tasks/05-29-approval-detail-simplification-refactor/design.md new file mode 100644 index 00000000..076232b5 --- /dev/null +++ b/.trellis/tasks/05-29-approval-detail-simplification-refactor/design.md @@ -0,0 +1,71 @@ +# 审批中心与报销详情降噪实现方案 + +## Design Direction + +本轮沿用第一轮已确认的“安静、任务优先、工作台式极简”方向。审批中心和报销详情都是 A 类 PC 页面,不能做营销式 hero,也不引入新的视觉体系;重点是压缩首屏动作、重排信息层级、把低频能力后置。 + +硬性规则: + +- 每个页面首屏只允许 1 个主按钮。 +- 每个页面首屏最多 2 个次级入口。 +- 说明文案只服务风险、异常、空态或确认动作。 +- 低频能力后置,不删除。 + +## WorkflowTasksPage + +目标:从“审批台账 + 统计筛选”调整为“待我处理优先”。 + +实现要点: + +- `WorkspaceHeader` 去掉解释性 subtitle,首屏标题保持“审批中心”。 +- 默认保留待处理任务作为第一层;历史状态和业务类型筛选收纳到“高级 / 历史”折叠区。 +- 将统计卡从首屏常驻移入高级区,或压缩为待处理焦点旁的轻量摘要。 +- 删除默认 `WorkspaceNotice` 处理原则卡;错误、空态和二次确认继续保留说明。 +- 桌面表格行内主动作改为: + - pending 且允许 approve:主按钮“处理”或“通过”保留二次确认;“详情”作为次级或更多入口。 + - 非 pending:主按钮“查看”。 +- 移动布局不是本轮重构范围;若共享代码影响移动卡片,只做必要回归,不能大改移动壳。 + +## ReimbursementDetailPage + +目标:从“所有材料和操作平铺”调整为“状态驱动页面”。 + +实现要点: + +- `EntityOverviewHero` 顶部压缩说明文案;主动作只表达当前状态下最应该做的一件事。 +- 新增或重组“当前状态 / 下一步”首屏任务面板,回答: + - 当前状态是什么。 + - 下一步是什么。 + - 谁需要处理。 + - 明细/发票/附件是否满足当前状态要求。 +- 按状态决定主动作: + - `draft` / `rejected`:优先补齐材料或提交;删除草稿进“更多”。 + - `reviewing`:优先查看审批进度;催办/撤回作为次级状态动作。 + - `approved`:财务角色优先付款;普通员工看等待付款结果。 + - `paid`:优先查看付款结果;打印/PDF作为次级留档入口。 +- 明细和发票保留完整编辑能力,但在首屏之后作为“材料”工作区;缺发票、超标准、预算阻断等异常才前置提示。 +- 附件、项目关联、差旅关联、财务导出、审批历史保留,但移到次级区、侧栏或折叠区,不与主动作抢首屏。 +- 不改变 `ReimbursementActions` 的服务端动作调用和确认语义,只调整展示位置和入口密度。 + +## Shared Implementation Notes + +- 优先复用现有组件和类:`ListWorkspace`、`WorkspaceHeader`、`FilterBar`、`PrimaryTable`、`PageScaffold`、`EntityOverviewHero`、`SectionPanel`、`detail-workspace-grid`、`detail-command-panel`、`detail-fact-grid`、`detail-evidence-card`、`detail-timeline`。 +- 不新增 UI 框架,不引入新状态管理。 +- 不改 API 请求参数语义;高级/历史开关优先用本地状态或既有 URL 参数。 +- 不让无权限用户看到不可执行入口。 +- 不把低频入口删除;找不到合适位置时先进“更多”或次级区。 + +## Verification Plan + +- `cd client && npm run typecheck` +- `cd client && npm run build` +- 审批中心 PC 视觉验证: + - 默认显示待处理任务。 + - 高级/历史入口可展开历史状态、业务类型筛选和统计。 + - 行内动作预算符合 1 个主动作。 +- 报销详情 PC 视觉验证: + - 草稿/被驳回状态能看到补材料/提交路径。 + - 审批中状态能看到当前处理人和审批进度入口。 + - 已通过/已支付状态不展示编辑类主动作。 + - 打印、删除、财务导出、审批记录仍可达但不抢首屏。 +- Trellis governance 和 `git diff --check` 必须通过。 diff --git a/.trellis/tasks/05-29-approval-detail-simplification-refactor/implement.jsonl b/.trellis/tasks/05-29-approval-detail-simplification-refactor/implement.jsonl new file mode 100644 index 00000000..3e413036 --- /dev/null +++ b/.trellis/tasks/05-29-approval-detail-simplification-refactor/implement.jsonl @@ -0,0 +1,4 @@ +{"file": ".trellis/spec/frontend/index.md", "reason": "前端规约入口,确认 React/Vite 页面实现规则来源。"} +{"file": ".trellis/spec/hyb-oa/index.md", "reason": "hyb-oa 规约入口,确认项目级规则读取顺序。"} +{"file": ".trellis/spec/hyb-oa/project-governance.md", "reason": "Trellis 任务、批准、验证和提交治理规则。"} +{"file": ".trellis/spec/hyb-oa/frontend-react-vite-design.md", "reason": "A 类 PC 页面任务优先、详情页命令面和前端验证规则。"} diff --git a/.trellis/tasks/05-29-approval-detail-simplification-refactor/prd.md b/.trellis/tasks/05-29-approval-detail-simplification-refactor/prd.md new file mode 100644 index 00000000..8fa2916f --- /dev/null +++ b/.trellis/tasks/05-29-approval-detail-simplification-refactor/prd.md @@ -0,0 +1,115 @@ +# 审批中心与报销详情降噪重构 + +## Goal + +延续第一轮 PC 首页和报销列表降噪规则,把同一套“任务优先、主动作收敛、低频能力后置”推广到审批中心和报销详情,让审批人进入页面后能立即处理待办,让员工进入详情后能立即知道当前状态和下一步。 + +## Scope + +- 第二轮只重构 PC Web 的两个 A 类页面: + - `client/src/pages/WorkflowTasksPage.tsx` + - `client/src/pages/ReimbursementDetailPage.tsx` +- 同步更新受影响的 Playwright/E2E 断言和视觉截图验证。 +- 继续遵守 `.trellis/spec/hyb-oa/frontend-react-vite-design.md` 的 `A-Class PC Task-First Pages` 规则。 +- 不修改后端 API、数据库、权限模型、审批状态机、报销状态机、移动端/企业微信页面、生产配置或真实数据。 + +## What I already know + +- 用户已确认第一轮前后对比效果可作为统一设计规则。 +- 第一轮已完成首页“今日任务入口化”和报销列表“员工进度视图化”,并沉淀为前端规约。 +- 上一轮 PRD 已将审批中心和报销详情列为第二轮扩展对象。 +- 审批中心当前首屏包含统计卡、全部状态 Tab、业务类型筛选、处理原则说明、列表行内“通过/详情”等;复杂感主要来自统计和解释文案抢占首屏注意力。 +- 报销详情当前顶部同时暴露打印、返回、催办、删除草稿;正文首屏同时展示状态摘要、付款状态、差旅、项目、附件、明细、发票、预算、风险、AI、导出、审批记录和动作栏,信息层级偏平铺。 +- 目标用户是 10 人以下国内科技公司,审批链短、角色重叠;PC 端仍需要保留批量查看、财务复核、导出和留档能力,但不能抢占员工/审批人的第一屏路径。 + +## Assumptions + +- 本轮继续采用前端层信息架构重排,不做业务能力删除。 +- 审批中心默认服务审批人“待我处理”;发起人历史、已处理、业务类型筛选和统计属于次级能力。 +- 报销详情默认服务当前状态下的下一步;明细、发票、附件、预算、审批记录、财务导出和打印留档按状态相关性排序或折叠。 +- 说明文案只在风险、异常、空态和确认动作中出现;常规流程解释不默认铺开。 +- 低频能力后置到“更多”“高级/历史”“详情页次级区”或状态相关面板,不删除。 + +## Open Questions + +- 当前无阻塞问题;若实现发现现有 API 数据无法支持目标排序或显隐,先回到 PRD 讨论,不直接改后端。 + +## Requirements + +- 审批中心首屏默认只回答:有多少待我处理、最需要处理哪一条、下一步是处理还是查看。 +- 审批中心首屏主动作预算:列表行主动作只保留 1 个强动作;“通过”这种高风险动作必须保留二次确认。 +- 审批中心的统计摘要、业务类型筛选、已处理/已驳回/已取消/已跳过历史、全量查询放到“高级 / 历史”区域或次级折叠区。 +- 审批中心不再默认展示“处理原则”说明卡;仅在空态、错误或特殊批量/风险场景显示说明。 +- 报销详情顶部改为状态驱动:当前状态、下一步、谁处理、关键金额/票据完整性先出现。 +- 报销详情按状态展示主动作: + - 草稿/被驳回:补明细、补发票、提交。 + - 审批中:看进度、催办、撤回或返回列表。 + - 已通过:等待/执行付款,财务角色可付款。 + - 已支付:查看付款结果、打印/PDF、留档。 +- 报销详情首屏只保留 1 个主动作,最多 2 个次级入口;打印/PDF、删除草稿、财务导出、项目关联、附件治理、审批历史等后置。 +- 报销详情中的明细、发票、附件保留完整能力,但默认按“当前状态需要补什么”排序,不把所有面板同时作为第一层任务。 +- 普通用户界面继续避免 UUID、taskId、内部状态码或调试字段作为主标识。 +- 所有高风险动作继续使用既有确认弹窗、权限判断和服务端校验,不绕开现有安全机制。 + +## Acceptance Criteria + +- [x] 审批中心默认进入“待处理”任务视图,首屏不再被统计卡和解释卡占满。 +- [x] 审批中心低频历史/筛选/统计入口后置,仍可访问。 +- [x] 审批中心行内动作满足 1 个主动作原则,并保留通过动作二次确认。 +- [x] 报销详情顶部能直接表达当前状态、下一步、谁处理、关键完整性。 +- [x] 报销详情首屏主动作不超过 1 个,次级入口不超过 2 个。 +- [x] 报销详情低频能力后置但不删除:打印/PDF、删除草稿、项目关联、财务导出、审批记录、附件治理仍可达。 +- [x] 说明文案只服务风险、异常、空态和确认动作。 +- [x] 不修改后端 API、数据库、权限、状态机、移动端/企业微信页面。 +- [x] 补充或更新聚焦 Playwright/视觉验证,覆盖审批中心和报销详情 PC 视口。 + +## Definition of Done + +- PRD 和设计方案获得用户批准后再进入实现。 +- 实现前按 Trellis workflow 激活任务并读取前端规约。 +- 前端改动需执行 `cd client && npm run typecheck && npm run build`。 +- 涉及 A 类核心页面时补充必要 Playwright/视觉验证。 +- 执行 `python3 ./.trellis/scripts/task.py validate .trellis/tasks/05-29-approval-detail-simplification-refactor`。 +- 执行 `node scripts/qa/check-trellis-governance.mjs`。 +- 执行 `git diff --check`。 +- 面向用户可测试的页面改动,部署和公网检查通过后再通知用户测试。 + +## Out of Scope + +- 不修改后端 API、数据库、审批/报销状态机、权限判定和审批动作语义。 +- 不重构移动端/企业微信页面。 +- 不恢复或扩展差旅能力。 +- 不做全站一次性重写。 +- 不做生产部署、真实数据写入或企业微信写入。 + +## Technical Notes + +- 当前任务:`.trellis/tasks/05-29-approval-detail-simplification-refactor/` +- 上一轮任务:`.trellis/tasks/archive/2026-05/05-29-frontend-simplification-refactor/` +- 统一设计规则:`.trellis/spec/hyb-oa/frontend-react-vite-design.md` +- 体验事实源:`docs/产品设计规则.md` +- 审批中心:`client/src/pages/WorkflowTasksPage.tsx` +- 报销详情:`client/src/pages/ReimbursementDetailPage.tsx` +- 相关视觉验证可参考:`client/tests/e2e/pc-core-workflow-visual-wave2.spec.ts`、`client/tests/e2e/pc-detail-mobile-visual-wave3.spec.ts`、`client/tests/e2e/visual-shell.spec.ts`。 + +## Test Plan + +- `cd client && npm run typecheck` +- `cd client && npm run build` +- `cd client && npx playwright test tests/e2e/pc-core-workflow-visual-wave2.spec.ts --workers=1 --reporter=list` +- `cd client && npx playwright test tests/e2e/pc-detail-mobile-visual-wave3.spec.ts --grep "PC" --workers=1 --reporter=list` +- 必要时新增或更新聚焦测试,验证审批中心和报销详情 PC 首屏动作预算。 +- `python3 ./.trellis/scripts/task.py validate .trellis/tasks/05-29-approval-detail-simplification-refactor` +- `node scripts/qa/check-trellis-governance.mjs` +- `git diff --check` + +## Verification Results + +- PASS:`cd client && npm run typecheck` +- PASS:`cd client && npm run build` +- PASS:`cd client && npx playwright test tests/e2e/pc-core-workflow-visual-wave2.spec.ts --workers=1 --reporter=list` +- PASS:`cd client && npx playwright test tests/e2e/pc-detail-mobile-visual-wave3.spec.ts --grep "PC" --workers=1 --reporter=list` +- PASS:`cd client && npx playwright test tests/e2e/r102-reimbursement-main-flow.spec.ts --workers=1 --reporter=list` +- PASS:`python3 ./.trellis/scripts/task.py validate .trellis/tasks/05-29-approval-detail-simplification-refactor` +- PASS:`node scripts/qa/check-trellis-governance.mjs` +- PASS:`git diff --check` diff --git a/.trellis/tasks/05-29-approval-detail-simplification-refactor/task.json b/.trellis/tasks/05-29-approval-detail-simplification-refactor/task.json new file mode 100644 index 00000000..0755d9ed --- /dev/null +++ b/.trellis/tasks/05-29-approval-detail-simplification-refactor/task.json @@ -0,0 +1,30 @@ +{ + "id": "approval-detail-simplification-refactor", + "name": "approval-detail-simplification-refactor", + "title": "审批中心与报销详情降噪重构", + "description": "第二轮 PC Web 前端页面降噪重构:审批中心待处理优先,报销详情状态驱动。", + "status": "in_progress", + "dev_type": null, + "scope": "client", + "package": null, + "priority": "P2", + "creator": "shunli", + "assignee": "shunli", + "createdAt": "2026-05-29", + "completedAt": null, + "branch": null, + "base_branch": "main", + "worktree_path": null, + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [ + "client/src/pages/WorkflowTasksPage.tsx", + "client/src/pages/ReimbursementDetailPage.tsx", + ".trellis/spec/hyb-oa/frontend-react-vite-design.md" + ], + "notes": "", + "meta": {} +} \ No newline at end of file diff --git a/client/src/pages/ReimbursementDetailPage.tsx b/client/src/pages/ReimbursementDetailPage.tsx index 15d5201e..aa49a1ba 100644 --- a/client/src/pages/ReimbursementDetailPage.tsx +++ b/client/src/pages/ReimbursementDetailPage.tsx @@ -22,6 +22,7 @@ import { withdrawReimbursement, linkReimbursementProject, type ReimbursementItem, + type ReimbursementStatus, } from "../api/reimbursements"; import { remindWorkflowTask } from "../api/workflow"; import { @@ -145,6 +146,22 @@ function budgetControlModeLabel(controlMode: string | null | undefined) { return controlMode ? labels[controlMode] ?? "按制度处理" : "按制度处理"; } +function reimbursementStatusLabel(status: ReimbursementStatus) { + const labels: Record = { + draft: "草稿", + reviewing: "审批中", + approved: "已批准", + paid: "已支付", + rejected: "已驳回", + cancelled: "已取消", + }; + return labels[status] ?? status; +} + +function materialCompletenessLabel(count: number, noun: string) { + return count > 0 ? `${count} ${noun}` : `缺${noun}`; +} + export function ReimbursementDetailPage() { const { reimbursementId = "" } = useParams(); const navigate = useNavigate(); @@ -158,6 +175,7 @@ export function ReimbursementDetailPage() { const [confirmDeleteItemId, setConfirmDeleteItemId] = useState(""); const [confirmDeleteReimbursement, setConfirmDeleteReimbursement] = useState(false); const [isErpPreviewOpen, setIsErpPreviewOpen] = useState(false); + const [showSecondaryDetail, setShowSecondaryDetail] = useState(false); const [submitConfirmPrecheck, setSubmitConfirmPrecheck] = useState(null); const [isInvoiceDialogOpen, setIsInvoiceDialogOpen] = useState(false); const [invoiceSearchInput, setInvoiceSearchInput] = useState(""); @@ -971,12 +989,159 @@ export function ReimbursementDetailPage() { return `${linkedItem.expenseType?.name ?? "未识别费用类型"} · ${linkedItem.description ?? "未填写说明"}`; }; + const itemCount = detail.items.length; + const invoiceCount = detail.invoices.length; + const attachmentCount = (detail.attachments ?? []).length; + const hasBlockingPrecheck = blockStandardChecks.length > 0; + const currentHandlerText = (() => { + if (detail.status === "reviewing") { + return detail.pendingWorkflow?.task?.step_name ?? "当前审批节点"; + } + if (detail.status === "approved") { + return canPay ? "财务处理" : "等待财务"; + } + if (detail.status === "paid") { + return payment.paidBy?.name ?? "财务已处理"; + } + if (["draft", "rejected"].includes(detail.status)) { + return detail.owner?.name ?? "报销人"; + } + return "-"; + })(); + const nextStepText = (() => { + if (["draft", "rejected"].includes(detail.status)) { + if (!itemCount) { + return "补报销明细"; + } + if (!invoiceCount) { + return "添加发票"; + } + if (hasBlockingPrecheck) { + return "修改超标明细"; + } + return "提交审批"; + } + if (detail.status === "reviewing") { + return detail.workflowProgress + ? `查看第 ${detail.workflowProgress.currentStepIndex + 1} / ${detail.workflowProgress.totalSteps} 步` + : "查看审批进度"; + } + if (detail.status === "approved") { + return canPay ? "执行付款" : "等待财务付款"; + } + if (detail.status === "paid") { + return "查看付款结果"; + } + return "查看记录"; + })(); + const primaryAction = (() => { + if (["draft", "rejected"].includes(detail.status) && canEditItems) { + if (!itemCount) { + return ( + + ); + } + if (!invoiceCount) { + return ( + + ); + } + return ( + + ); + } + + if (detail.status === "reviewing") { + return ( + + ); + } + + if (detail.status === "approved") { + return ( + + ); + } + + if (detail.status === "paid") { + return ( + + ); + } + + return ( + + ); + })(); + + const secondaryActions = [ + detail.status === "reviewing" && canRemindPendingWorkflow && pendingTaskId ? ( + + ) : detail.status === "paid" ? ( + + ) : ( + + ), + , + ]; + return ( {downloadReasonDialog} @@ -987,43 +1152,134 @@ export function ReimbursementDetailPage() { )} actions={( -
- - - {canRemindPendingWorkflow && pendingTaskId ? ( - - ) : null} - {canDeleteReimbursement ? ( - - ) : null} +
+ {primaryAction} + {secondaryActions}
)} />
- -
+ +
+
+
+
下一步
+
{nextStepText}
+
+
+ +
+
+ +
+
+
状态
+
状态:{reimbursementStatusLabel(detail.status)}
+
+
+
处理人
+
{currentHandlerText}
+
+
+
金额
+
¥{detail.totalAmount ?? "-"}
+
+
+
材料完整性
+
+ {materialCompletenessLabel(itemCount, "条明细")} / {materialCompletenessLabel(invoiceCount, "张发票")} +
+
+
+ + {actionError ? ( +

+ {actionError} +

+ ) : null} + {submitInvoiceNotice ? ( +

+ {submitInvoiceNotice} +

+ ) : null} + {hasBlockingPrecheck ? ( +

+ 请修改超标明细后提交。 +

+ ) : null} +
+ + {["approved", "paid"].includes(detail.status) ? ( +
+
+

付款状态

+ + {detail.status === "paid" ? "已付款" : "未付款"} + +
+
+
+
付款日期
+
{payment.paymentDate ?? "-"}
+
+
+
付款凭证号
+
{payment.paymentVoucherNo ?? "-"}
+
+
+
付款渠道
+
{payment.paymentChannel ?? "-"}
+
+
+
操作人 / 时间
+
+ {payment.paidBy?.name ?? "-"} · {formatDateTime(payment.paidAt)} +
+
+
+
+ ) : null} + + {showSecondaryDetail ? ( +
+
+
+

更多入口

+
+ {detail.status !== "paid" ? ( + + ) : null} + + {canDeleteReimbursement ? ( + + ) : null} +
+
+
+ -
+
报销编号
@@ -1062,7 +1318,7 @@ export function ReimbursementDetailPage() {
-
+

付款状态

@@ -1163,7 +1419,6 @@ export function ReimbursementDetailPage() { ))}
-
@@ -1242,7 +1497,9 @@ export function ReimbursementDetailPage() { ) : (

暂无附件材料

)} -
+
+
+ ) : null} @@ -1579,75 +1836,84 @@ export function ReimbursementDetailPage() {
- -
报销编号:{displayCode}
-
报销人:{detail.owner?.name ?? "-"}
-
提交人:{detail.submitter?.name ?? "-"}
-
当前审批:{detail.pendingWorkflow?.task?.step_name ?? "-"}
-
明细数:{detail.items.length}
-
票据数:{detail.invoices.length}
-
关联项目:{detail.project ? `${detail.project.projectNo} · ${detail.project.name}` : "未关联"}
-
- - {detail.status === "reviewing" && detail.workflowProgress ? ( - -
- {detail.workflowProgress.steps.map((step, index) => ( -
-
- - 第 {index + 1} 步 · {step.name} - - - {step.status === "completed" ? "已完成" : step.status === "current" ? "当前节点" : "待处理"} - -
-
- ))} -
+ {showSecondaryDetail ? ( + +
报销编号:{displayCode}
+
报销人:{detail.owner?.name ?? "-"}
+
提交人:{detail.submitter?.name ?? "-"}
+
当前审批:{detail.pendingWorkflow?.task?.step_name ?? "-"}
+
明细数:{detail.items.length}
+
票据数:{detail.invoices.length}
+
附件数:{attachmentCount}
+
关联项目:{detail.project ? `${detail.project.projectNo} · ${detail.project.name}` : "未关联"}
) : null} - - {detail.approvalHistory.length ? ( -
- {detail.approvalHistory.map((item) => ( -
-
- {workflowActionLabel(item.action)} - {formatDateTime(item.createdAt)} + {detail.status === "reviewing" && detail.workflowProgress ? ( +
+ +
+ {detail.workflowProgress.steps.map((step, index) => ( +
+
+ + 第 {index + 1} 步 · {step.name} + + + {step.status === "completed" ? "已完成" : step.status === "current" ? "当前节点" : "待处理"} + +
-
- {item.stepName ?? "-"} · {item.actorName || "系统"} + ))} +
+ +
+ ) : null} + + {showSecondaryDetail ? ( + + {detail.approvalHistory.length ? ( +
+ {detail.approvalHistory.map((item) => ( +
+
+ {workflowActionLabel(item.action)} + {formatDateTime(item.createdAt)} +
+
+ {item.stepName ?? "-"} · {item.actorName || "系统"} +
+ {item.comment ?
{item.comment}
: null}
- {item.comment ?
{item.comment}
: null} -
- ))} -
- ) : ( -
暂无审批记录。
- )} - + ))} +
+ ) : ( +
暂无审批记录。
+ )} + + ) : null} 0} - submitHint={blockStandardChecks.length > 0 ? "请修改超标明细后提交" : ""} - submitNotice={submitInvoiceNotice} + submitHint="" + submitNotice="" isPending={mutation.isPending} onSubmit={() => void handleSubmitAction()} onWithdraw={(comment) => mutation.mutate({ action: "withdraw", comment })} diff --git a/client/src/pages/WorkflowTasksPage.tsx b/client/src/pages/WorkflowTasksPage.tsx index 6ff5ffe1..961a98df 100644 --- a/client/src/pages/WorkflowTasksPage.tsx +++ b/client/src/pages/WorkflowTasksPage.tsx @@ -21,7 +21,6 @@ import { StatStrip, WorkspaceEmptyState, WorkspaceHeader, - WorkspaceNotice, } from "../components/patterns/ListWorkspace"; import { MetricCard } from "../components/patterns/MetricCard"; import { Button } from "../components/ui/button"; @@ -137,12 +136,12 @@ function isAlreadyProcessedTaskError(error: unknown) { export function WorkflowTasksPage() { const [searchParams, setSearchParams] = useSearchParams(); - const [confirmApproveTaskId, setConfirmApproveTaskId] = useState(null); - const queryClient = useQueryClient(); - const status = normalizeStatusFilter(searchParams.get("status")); const entityType = normalizeEntityFilter(searchParams.get("entityType")); const page = Number(searchParams.get("page") ?? "1") || 1; + const [confirmApproveTaskId, setConfirmApproveTaskId] = useState(null); + const [showAdvanced, setShowAdvanced] = useState(() => status !== "pending" || entityType !== "all"); + const queryClient = useQueryClient(); function updateSearchParams(next: { status?: WorkflowStatusFilter; @@ -203,6 +202,15 @@ export function WorkflowTasksPage() { const totalPages = Math.max(1, Math.ceil((query.data?.total ?? 0) / PAGE_SIZE)); const taskItems = query.data?.items ?? []; const confirmCopy = confirmApproveTaskId ? getWorkflowConfirmCopy("approve") : null; + const isDefaultTaskView = status === "pending" && entityType === "all"; + const advancedVisible = showAdvanced || !isDefaultTaskView; + const pendingTotal = summaryQuery.data?.pendingTotal ?? "-"; + const overdueTotal = summaryQuery.data?.overdueTotal ?? "-"; + const pendingEntitySummary = + (summaryQuery.data?.byEntityType ?? []) + .filter((item) => item.pendingTotal > 0) + .map((item) => `${entityLabel(item.key)} ${item.pendingTotal}`) + .join(" / ") || "-"; return ( <> @@ -210,73 +218,107 @@ export function WorkflowTasksPage() { setShowAdvanced((value) => !value)} + > + {advancedVisible ? "收起高级" : "高级 / 历史"} + + } /> -
- - - - - - {(summaryQuery.data?.byEntityType ?? []) - .filter((item) => item.pendingTotal > 0) - .map((item) => `${entityLabel(item.key)} ${item.pendingTotal}`) - .join(" / ") || "-"} - - } - /> - -
-
- updateSearchParams({ status: value as WorkflowStatusFilter, page: 1 })} - > - - {WORKFLOW_STATUS_OPTIONS.map((item) => ( - - {item.label} - - ))} - - - +
+ 待处理 + {pendingTotal} + + 最早:{formatSummaryTime(summaryQuery.data?.earliestPendingAt)} + + {summaryQuery.data?.overdueTotal ? ( + + 超时 {summaryQuery.data.overdueTotal} + + ) : null} +
- - + {!isDefaultTaskView ? ( + + ) : null} + 业务:{pendingEntitySummary}
- {status === "pending" ? ( - - 先看业务编号、申请人、金额/天数和当前节点;需要查看附件、审批历史或驳回原因时进入详情页。 - + {advancedVisible ? ( +
+
+ + + + + {pendingEntitySummary}} + /> + +
+ + +
+ updateSearchParams({ status: value as WorkflowStatusFilter, page: 1 })} + > + + {WORKFLOW_STATUS_OPTIONS.map((item) => ( + + {item.label} + + ))} + + + +
+ + +
+
+
+
) : null} {query.isLoading ? : null} @@ -348,13 +390,15 @@ export function WorkflowTasksPage() { variant="secondary" data-testid={TEST_IDS.workflow.detailLink} > - 详情 + 查看
)) ) : ( - 当前筛选下没有审批任务。 + + {isDefaultTaskView ? "当前没有待处理任务。" : "当前筛选下没有审批任务。"} + )}
@@ -405,10 +449,10 @@ export function WorkflowTasksPage() {
@@ -417,7 +461,7 @@ export function WorkflowTasksPage() { ) : ( - 当前筛选下没有审批任务 + {isDefaultTaskView ? "当前没有待处理任务" : "当前筛选下没有审批任务"} )} diff --git a/client/src/pages/reimbursement-detail/ReimbursementActions.tsx b/client/src/pages/reimbursement-detail/ReimbursementActions.tsx index 233c6681..23cce09b 100644 --- a/client/src/pages/reimbursement-detail/ReimbursementActions.tsx +++ b/client/src/pages/reimbursement-detail/ReimbursementActions.tsx @@ -20,6 +20,9 @@ export interface ReimbursementActionsProps { submitDisabled?: boolean; submitHint?: string; submitNotice?: string; + showSubmitAction?: boolean; + showWithdrawAction?: boolean; + showPayAction?: boolean; isPending: boolean; onSubmit: () => void; onWithdraw: (comment: string) => void; @@ -33,6 +36,9 @@ export function ReimbursementActions({ submitDisabled = false, submitHint = "", submitNotice = "", + showSubmitAction = true, + showWithdrawAction = true, + showPayAction = true, isPending, onSubmit, onWithdraw, @@ -43,9 +49,16 @@ export function ReimbursementActions({ const [paymentDate, setPaymentDate] = useState("2026-02-08"); const [paymentChannel, setPaymentChannel] = useState("bank"); const [paymentNote, setPaymentNote] = useState(""); + const canShowSubmit = showSubmitAction && ["draft", "rejected"].includes(status); + const canShowWithdraw = showWithdrawAction && status === "reviewing"; + const canShowPay = showPayAction && canPay && status === "approved"; + + if (!canShowSubmit && !canShowWithdraw && !canShowPay && !actionError && !submitHint && !submitNotice) { + return null; + } return ( -
+

可执行动作

{actionError ? ( @@ -57,16 +70,18 @@ export function ReimbursementActions({

) : null} -
- -
+ {canShowSubmit ? ( +
+ +
+ ) : null} {submitHint ?

{submitHint}

: null} {submitNotice ? (

@@ -74,7 +89,7 @@ export function ReimbursementActions({

) : null} - {status === "reviewing" ? ( + {canShowWithdraw ? (

撤回报销