diff --git a/.trellis/tasks/05-29-frontend-ux-wave4-5-finance-admin-refresh/check.jsonl b/.trellis/tasks/05-29-frontend-ux-wave4-5-finance-admin-refresh/check.jsonl new file mode 100644 index 00000000..d3455975 --- /dev/null +++ b/.trellis/tasks/05-29-frontend-ux-wave4-5-finance-admin-refresh/check.jsonl @@ -0,0 +1,5 @@ +{"file": ".trellis/spec/hyb-oa/frontend-react-vite-design.md", "reason": "Verify changed pages follow frontend-design plus shared finance/admin surface conventions"} +{"file": ".trellis/spec/hyb-oa/project-governance.md", "reason": "Verify Trellis flow, approvals and task evidence are complete"} +{"file": ".trellis/spec/hyb-oa/docs-and-evidence.md", "reason": "Verify evidence paths and summaries avoid sensitive data and absolute local paths"} +{"file": ".trellis/spec/hyb-oa/security-privacy.md", "reason": "Verify admin/finance evidence does not expose secrets, full accounts, payloads or sensitive business content"} +{"file": ".trellis/tasks/05-29-frontend-ux-wave4-5-finance-admin-refresh/research/scope-audit.md", "reason": "Verify implementation matches the agreed Wave4 refresh and Wave5 gap-fill boundary"} diff --git a/.trellis/tasks/05-29-frontend-ux-wave4-5-finance-admin-refresh/implement.jsonl b/.trellis/tasks/05-29-frontend-ux-wave4-5-finance-admin-refresh/implement.jsonl new file mode 100644 index 00000000..95089168 --- /dev/null +++ b/.trellis/tasks/05-29-frontend-ux-wave4-5-finance-admin-refresh/implement.jsonl @@ -0,0 +1,5 @@ +{"file": ".trellis/spec/hyb-oa/frontend-react-vite-design.md", "reason": "Wave4-5 frontend design authority, shared finance/admin surface conventions and verification requirements"} +{"file": ".trellis/spec/hyb-oa/project-governance.md", "reason": "Trellis task contract, approval and commit boundaries for non-trivial implementation"} +{"file": ".trellis/spec/hyb-oa/deployment-trialrun.md", "reason": "Public deployment and live readiness gates if the frontend changes are released"} +{"file": ".trellis/spec/hyb-oa/security-privacy.md", "reason": "Finance/admin pages include payment, export, audit and WeCom-related privacy boundaries"} +{"file": ".trellis/tasks/05-29-frontend-ux-wave4-5-finance-admin-refresh/research/scope-audit.md", "reason": "Task-specific scope audit distinguishing historical Wave4 from current Wave5 gaps"} diff --git a/.trellis/tasks/05-29-frontend-ux-wave4-5-finance-admin-refresh/implement.md b/.trellis/tasks/05-29-frontend-ux-wave4-5-finance-admin-refresh/implement.md new file mode 100644 index 00000000..d8738324 --- /dev/null +++ b/.trellis/tasks/05-29-frontend-ux-wave4-5-finance-admin-refresh/implement.md @@ -0,0 +1,96 @@ +# Implementation Plan + +## Current Decision + +This task completes the remaining frontend wave work by combining: + +1. Wave4 finance/operations refresh after the newer Wave0-3 baseline. +2. Wave5 admin/config gap fill for pages that were not fully covered by the historical Wave4 task. + +It is not a blank rebuild of finance/admin pages. + +## Recommended Implementation Order + +1. Baseline audit: + - Compare current finance/admin pages against `frontend-design`, Wave0-3 shell/header conventions and historical Wave4 classes. + - Identify pages that need code changes versus pages that only need test coverage. +2. Wave5 gap pages: + - `ApprovalRulesPage` + - `PeoplePage` + - `OrganizationPage` + - `WorkspaceSetupPage` +3. Wave4 refresh pages: + - `PaymentBatchesPage` + - `ApOperationsPage` + - `InvoicePoolPage` +4. Regression surfaces: + - `UserManagementPage` + - `MasterDataPage` + - `NotificationChannelsPage` + - `SystemSettingsPage` + - `FeatureFlagsPage` + - `WorkflowDefinitionsPage` + - `AuditLogsPage` +5. Test baseline: + - Extend or replace the existing focused Wave4 Playwright spec so it covers Wave4 plus Wave5 gaps. + - Keep public deployment gated by explicit approval. + +## Guardrails + +- No backend, schema, permission, payment-state, approval-rule semantic, WeCom or env changes. +- Do not remove existing business actions. +- Payment, export, sync, enable/disable and config-publish actions keep original confirmation and permission behavior. +- Do not add a new design language; reuse existing finance/admin/ops classes. +- Do not claim user-test readiness until public deployment and public checks pass. + +## Local Verification Plan + +```bash +cd client && npm run typecheck +cd client && npm run build +cd client && npx playwright test tests/e2e/pc-finance-admin-visual-wave4.spec.ts --workers=1 --reporter=list +node scripts/qa/check-ui-design-rules.mjs --strict +python3 ./.trellis/scripts/task.py validate 05-29-frontend-ux-wave4-5-finance-admin-refresh +node scripts/qa/check-trellis-governance.mjs +git diff --check +``` + +## Completed Implementation + +2026-05-29: + +- Refreshed `ApprovalRulesPage` with Wave5 admin summary metrics, a command-oriented creation panel and a cleaner rule-list surface. +- Refreshed `PeoplePage` to use the shared workspace/header baseline, finance/admin metrics, WeCom HR command surface, filter panel, batch-action panel and focused table container. +- Refreshed `OrganizationPage` to use the shared management shell, organization summary metrics and admin command panels while keeping hierarchy readability. +- Refreshed `WorkspaceSetupPage` to use the shared management shell, module visibility metrics, role preset tiles and a focused module table with Chinese visibility labels. +- Extended `client/tests/e2e/pc-finance-admin-visual-wave4.spec.ts` from historical Wave4 coverage to Wave4-5 coverage. +- Kept Wave4 finance pages on the existing approved surface; no backend or business-state changes were made. + +## Local Verification Result + +- `cd client && npm run typecheck` passed. +- `cd client && npx playwright test tests/e2e/pc-finance-admin-visual-wave4.spec.ts --workers=1 --reporter=list` passed: 3 tests. +- `cd client && npm run build` passed. +- `node scripts/qa/check-ui-design-rules.mjs --strict` passed with 0 findings. +- `python3 ./.trellis/scripts/task.py validate 05-29-frontend-ux-wave4-5-finance-admin-refresh` passed. +- `node scripts/qa/check-trellis-governance.mjs` passed after task metadata was completed. +- `git diff --check` passed. + +## Spec Update Decision + +No `.trellis/spec/` update is needed for this implementation. The task applied existing frontend conventions (`ListWorkspace`, `WorkspaceHeader`, `finance-status-strip`, `admin-command-panel`, `PrimaryTable`) and did not introduce a new reusable frontend contract, API signature, env key, schema, permission rule or deployment behavior. + +## Deployment Plan + +Only after explicit approval: + +```bash +SYNC_ENV=0 LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash deploy/trialrun/deploy.sh +LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-live-smoke-readiness.sh +OAOPS1_REPORT_DIR=.trellis/tasks/05-29-frontend-ux-wave4-5-finance-admin-refresh/research/postdeploy--wave4-5-public \ + LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-oa-real-use-daily.sh +``` + +## Spec Update Expectation + +Only update `.trellis/spec/` if implementation introduces a reusable frontend convention beyond the existing finance/admin surfaces. If the task only applies existing conventions, record that no spec update is needed. diff --git a/.trellis/tasks/05-29-frontend-ux-wave4-5-finance-admin-refresh/prd.md b/.trellis/tasks/05-29-frontend-ux-wave4-5-finance-admin-refresh/prd.md new file mode 100644 index 00000000..0be598c6 --- /dev/null +++ b/.trellis/tasks/05-29-frontend-ux-wave4-5-finance-admin-refresh/prd.md @@ -0,0 +1,170 @@ +# 前端体验 Wave4-5 财务与管理页刷新 + +## Goal + +在 Wave0-3 新全局体验基线已完成并部署后,继续完成剩余前端任务:复审 Wave4 财务/运营页,并补齐 Wave5 管理配置页缺口,让财务、管理员和负责人在低频但高风险页面中仍看到统一、克制、可扫描、可操作的 OA 工作界面。 + +## What I Already Know + +- 用户要求“继续完成所有任务”,上下文指向 Wave4 和 Wave5 未完全按新基线收口。 +- 历史任务 `05-27-pc-finance-admin-visual-wave4` 已完成并部署验证过,覆盖付款批次、AP、票据池和部分 C 类管理页。 +- 历史 Wave4 发生在最新 Wave0-3 新基线之前,因此本任务不是从零重做,而是做“新基线后的复审 + 缺口补齐”。 +- Wave5 原计划为 `admin-config`;当前没有独立 Wave5 任务,且一部分管理页已被历史 Wave4 合并覆盖。 +- `frontend-design` skill 是视觉与交互裁决来源;Trellis、隐私、部署和真实业务边界仍按项目 specs 和 docs 执行。 +- 面向用户可测试的页面改动,只有部署和公网检查通过后才能告知用户测试。 + +## Product Direction + +- Tone:quiet finance/admin command surface,克制、紧凑、风险清晰。 +- 财务页优先回答:待处理是什么、异常在哪里、下一步动作是什么、是否会产生付款/导出/审计后果。 +- 管理页优先回答:当前配置状态是什么、改动影响范围是什么、危险动作如何确认和追溯。 +- 不做营销式页面、不新增装饰 hero、不降低高密信息页的扫描效率。 + +## Scope + +### Wave4 Refresh:财务与运营页 + +复审并必要调整: + +- `/payment-batches` +- `/ap-operations` +- `/invoice-pool` + +目标: + +- 对齐 Wave0-3 的 shell、header、command surface、状态摘要和表格/列表密度。 +- 保留现有付款批次、复核、回单、导出、票据上传、票据绑定、OCR/验真、风险提示和错误处理。 +- 不新增真实支付渠道,不改变付款状态机,不写 ERP。 + +### Wave5 Gap Fill:管理配置页 + +优先补齐尚未完全纳入新基线或历史 Wave4 覆盖不足的页面: + +- `/system/approval-rules` +- `/people` +- `/organization` +- `/system/workspace` + +同步复审已覆盖但需要防回退的页面: + +- `/system/users` +- `/system/master-data` +- `/system/notification-channels` +- `/system/settings` +- `/system/feature-flags` +- `/system/workflow-definitions` +- `/audit-logs` + +目标: + +- 管理配置页统一 `ListWorkspace`/`WorkspaceHeader`、状态摘要、命令区、主表格/主列表和危险操作层级。 +- 降低解释性文案,但保留配置影响、权限、审计、不可逆动作和外部集成边界提示。 +- 组织/人员页面避免“普通资料浏览”和“管理配置”风格割裂。 + +## Requirements + +### R1. Wave4 不重复重写 + +- 历史 Wave4 已通过的财务页只做 gap-driven refresh。 +- 不为了视觉统一改动 API、状态机、权限或付款动作。 +- 若页面已符合新基线,只更新测试覆盖或验收记录,不强行改代码。 + +### R2. Wave5 管理配置补齐 + +- 审批规则页面必须突出规则数量、启用状态、匹配顺序、影响范围和“只影响新单据”的边界。 +- 人员/组织/工作区页面必须对齐管理页骨架,减少独立 `card-std` 拼接感。 +- 配置、导出、同步、停用、启用、写入企业微信或影响导航的动作必须保留原确认、权限和错误提示。 + +### R3. 视觉与交互一致性 + +- 复用 `finance-status-strip`、`finance-metric-card`、`finance-command-panel`、`admin-command-panel`、`admin-tile-grid`、`ops-table-focus` 等现有共享类。 +- 不新增一套新的页面局部设计系统。 +- 按 `frontend-design` 做实际页面质量判断,避免啰嗦、按钮挤压、说明堆叠和风格漂移。 + +### R4. 验证与发布 + +- 本地必须通过 typecheck/build、UI design rules、focused Playwright、Trellis governance 和 `git diff --check`。 +- Focused Playwright 必须覆盖 Wave4 财务页和 Wave5 管理缺口页。 +- 部署给用户测试前必须跑部署脚本、公网 readiness 和 OAOPS1 只读巡检。 + +## Acceptance Criteria + +- [x] Wave4 财务/运营页完成新基线复审;必要改动已实施,未发现回退。 +- [x] Wave5 管理配置缺口页完成统一骨架、状态摘要、命令区和主内容收敛。 +- [x] 历史 Wave4 覆盖页完成防回退视觉/交互检查。 +- [x] 没有改变后端 API、权限模型、付款状态机、审批规则语义、数据库 schema、企业微信配置或公网真实数据。 +- [x] `cd client && npm run typecheck` 通过。 +- [x] `cd client && npm run build` 通过。 +- [x] Focused Playwright 覆盖 Wave4-5 页面并通过。 +- [x] `node scripts/qa/check-ui-design-rules.mjs --strict` 通过。 +- [x] `python3 ./.trellis/scripts/task.py validate 05-29-frontend-ux-wave4-5-finance-admin-refresh` 通过。 +- [x] `node scripts/qa/check-trellis-governance.mjs` 和 `git diff --check` 通过。 +- [ ] 若部署给用户测试,已完成公网部署与公网检查并记录证据。 + +## Implementation Result + +2026-05-29 本地实施完成: + +- Wave5 缺口页完成新基线补齐: + - `/system/approval-rules` 新增规则状态摘要、创建命令区和规则列表命令区,突出启用数、优先级和“只影响新单据”边界。 + - `/people` 迁移到 `ListWorkspace`/`WorkspaceHeader`,新增人员状态摘要、企微 HR 主数据命令区、筛选命令区、批量动作区和 `PrimaryTable`。 + - `/organization` 迁移到统一管理骨架,新增组织摘要、部门命令面板和统一组织节点卡片。 + - `/system/workspace` 迁移到统一管理骨架,新增模块显隐摘要、工作区基线面板、角色预设卡片和模块显隐主表。 +- Wave4 财务/运营页未重写业务结构,继续复用历史已通过的财务 surface,并通过 focused Playwright 防回退。 +- Focused Playwright 扩展到 Wave4-5 页面:付款批次、AP、票据池、审批规则、人员、组织、工作区、用户、主数据、通知渠道、系统设置、功能开关、流程定义、审计日志。 +- 本轮没有改后端、数据库、权限、审批规则语义、付款状态机、企业微信配置、SMTP 或生产配置。 + +## Test Plan + +```bash +cd client && npm run typecheck +cd client && npm run build +cd client && npx playwright test tests/e2e/pc-finance-admin-visual-wave4.spec.ts --workers=1 --reporter=list +node scripts/qa/check-ui-design-rules.mjs --strict +python3 ./.trellis/scripts/task.py validate 05-29-frontend-ux-wave4-5-finance-admin-refresh +node scripts/qa/check-trellis-governance.mjs +git diff --check +``` + +Deployment gate, only after approval: + +```bash +SYNC_ENV=0 LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash deploy/trialrun/deploy.sh +LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-live-smoke-readiness.sh +OAOPS1_REPORT_DIR=.trellis/tasks/05-29-frontend-ux-wave4-5-finance-admin-refresh/research/postdeploy--wave4-5-public \ + LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-oa-real-use-daily.sh +``` + +## Out Of Scope + +- 不新增真实付款渠道、ERP 写入、企业微信写入、数据库迁移或生产配置变更。 +- 不新增通用文件中心。 +- 不恢复差旅主线能力。 +- 不把移动端改造成完整 PC 管理后台。 +- 不删除已有业务能力;如发现业务流程缺陷,记录为后续业务流程任务。 + +## Research References + +- `research/scope-audit.md` — Wave4 历史覆盖、Wave5 缺口和本任务页面清单。 +- `.trellis/tasks/archive/2026-05/05-27-pc-finance-admin-visual-wave4/` — 历史 Wave4 范围、实现和部署证据。 +- `.trellis/tasks/archive/2026-05/05-28-frontend-ux-redesign/` — Wave0-2 新基线和 Wave3-5 后续计划。 +- `.trellis/tasks/archive/2026-05/05-28-frontend-ux-wave3-business-mobile/` — 最新 Wave3 refresh 和部署证据。 + +## Technical Notes + +- 重点代码区域: + - `client/src/pages/PaymentBatchesPage.tsx` + - `client/src/pages/ApOperationsPage.tsx` + - `client/src/pages/InvoicePoolPage.tsx` + - `client/src/pages/ApprovalRulesPage.tsx` + - `client/src/pages/PeoplePage.tsx` + - `client/src/pages/OrganizationPage.tsx` + - `client/src/pages/WorkspaceSetupPage.tsx` + - `client/src/pages/UserManagementPage.tsx` + - `client/src/pages/MasterDataPage.tsx` + - `client/src/pages/NotificationChannelsPage.tsx` + - `client/src/pages/SystemSettingsPage.tsx` + - `client/src/pages/FeatureFlagsPage.tsx` + - `client/src/pages/WorkflowDefinitionsPage.tsx` + - `client/src/pages/AuditLogsPage.tsx` + - `client/tests/e2e/pc-finance-admin-visual-wave4.spec.ts` diff --git a/.trellis/tasks/05-29-frontend-ux-wave4-5-finance-admin-refresh/research/scope-audit.md b/.trellis/tasks/05-29-frontend-ux-wave4-5-finance-admin-refresh/research/scope-audit.md new file mode 100644 index 00000000..a407a376 --- /dev/null +++ b/.trellis/tasks/05-29-frontend-ux-wave4-5-finance-admin-refresh/research/scope-audit.md @@ -0,0 +1,65 @@ +# Wave4-5 Scope Audit + +## Summary + +Historical Wave4 is complete, deployed and archived, but it predates the latest Wave0-3 refresh line. The remaining work should not redo the whole finance/admin surface. It should validate the historical Wave4 pages against the new baseline and fill the missing Wave5 admin/config pages. + +## Historical Wave4 Covered + +Task: `.trellis/tasks/archive/2026-05/05-27-pc-finance-admin-visual-wave4/` + +Covered pages: + +- `/payment-batches` +- `/ap-operations` +- `/invoice-pool` +- `/system/users` +- `/system/master-data` +- `/system/notification-channels` +- `/system/settings` +- `/system/feature-flags` +- `/system/workflow-definitions` +- `/audit-logs` + +Evidence: + +- Local typecheck/build/focused Playwright/design rules/Trellis governance passed. +- Deployed commit `c5eb27e` passed public deploy checks and Wave4 public visual smoke. +- OAOPS1 postdeploy Wave4 read-only巡检 later recorded `PASS_OA_REAL_USE_DAILY_READY`. + +## Current New Baseline + +Wave0-2 task established the new global shell, navigation, page header, list/detail surface and high-frequency core pages. Wave3 then refreshed business detail and mobile key pages against that new baseline and passed public checks on 2026-05-29. + +Therefore the next work should be a new-baseline refresh, not a full redesign. + +## Gap Pages For Wave5 + +The following pages are active management/config pages but were not first-class targets in historical Wave4: + +- `/system/approval-rules` — currently uses `ListWorkspace`/`WorkspaceHeader`, but still has a large create form and rule table that should be brought into admin command/status patterns. +- `/people` — people and HR master operation surface still uses older `PageHeader`/card/table style; it has high-risk actions including export, batch disable/enable, manager/department transfer and WeCom HR master sync. +- `/organization` — still uses older `PageHeader` and standalone cards; should align with management overview style while preserving hierarchy readability. +- `/system/workspace` — still uses older `PageHeader` and cards; affects navigation visibility and small-team workspace setup, so it belongs in Wave5 admin-config. + +Secondary watchlist: + +- `/system/departments` +- `/system/expense-standards` +- `/system/integrations/erp` +- `/system/cutover` + +These are lower priority unless the initial implementation finds visible inconsistency or existing focused tests already cover them cheaply. + +## Recommended Task Boundary + +Do in this task: + +- Refresh the four Wave5 gap pages. +- Re-run/extend focused Wave4 visual tests to cover both historical Wave4 pages and new Wave5 gap pages. +- Touch Wave4 finance pages only if the test/audit finds current new-baseline gaps. + +Do not do in this task: + +- Backend workflow, approval semantics, permission, payment state machine, database migration or Enterprise WeChat write changes. +- Real payment, ERP write, production config mutation or real business data automation. diff --git a/.trellis/tasks/05-29-frontend-ux-wave4-5-finance-admin-refresh/task.json b/.trellis/tasks/05-29-frontend-ux-wave4-5-finance-admin-refresh/task.json new file mode 100644 index 00000000..69945eaa --- /dev/null +++ b/.trellis/tasks/05-29-frontend-ux-wave4-5-finance-admin-refresh/task.json @@ -0,0 +1,26 @@ +{ + "id": "frontend-ux-wave4-5-finance-admin-refresh", + "name": "frontend-ux-wave4-5-finance-admin-refresh", + "title": "前端体验 Wave4-5 财务与管理页刷新", + "description": "在 Wave0-3 新基线后复审财务/运营页并补齐管理配置页体验缺口。", + "status": "in_progress", + "dev_type": null, + "scope": "client frontend finance admin ux", + "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": [], + "notes": "", + "meta": {} +} diff --git a/client/src/pages/ApprovalRulesPage.tsx b/client/src/pages/ApprovalRulesPage.tsx index 0014a59d..3056b325 100644 --- a/client/src/pages/ApprovalRulesPage.tsx +++ b/client/src/pages/ApprovalRulesPage.tsx @@ -61,6 +61,22 @@ export function ApprovalRulesPage() { }); const rules = listQuery.data?.items ?? []; + const ruleStats = useMemo(() => { + const enabledRules = rules.filter((rule) => rule.enabled); + const disabledRules = rules.length - enabledRules.length; + const fallbackRules = rules.filter((rule) => rule.ownerSelfFallbackRole).length; + const topPriority = enabledRules.length + ? Math.min(...enabledRules.map((rule) => rule.priority)) + : null; + + return { + total: rules.length, + enabled: enabledRules.length, + disabled: disabledRules, + fallback: fallbackRules, + topPriority, + }; + }, [rules]); const createPayload = useMemo(() => { return { @@ -169,8 +185,41 @@ export function ApprovalRulesPage() {

) : null} -
-

新建审批规则

+
+
+
规则总数
+
{ruleStats.total}
+
+
+
启用规则
+
{ruleStats.enabled}
+
+
+
停用规则
+
{ruleStats.disabled}
+
+
+
最高优先级
+
{ruleStats.topPriority ?? "-"}
+
+
+
本人兜底
+
{ruleStats.fallback}
+
+
+ +
+
+
+

新建审批规则

+

+ 规则按优先级匹配,只影响新提交单据;历史审批实例不回写。 +

+
+
+ 金额段 + 角色链 +
+
@@ -311,10 +360,20 @@ export function ApprovalRulesPage() {
-
+ -
-

规则列表

+
+
+
+

规则列表

+

+ 优先级越小越先匹配;启停动作会经过确认。 +

+
+
+ 当前 {ruleStats.enabled} 条启用 +
+
{listQuery.isLoading ? : null} @@ -403,7 +462,7 @@ export function ApprovalRulesPage() { ) : null} -
+ total + 1 + countNodes(node.children), 0); } +function countExternalManagers(nodes: OrganizationTreeNode[]): number { + return nodes.reduce( + (total, node) => total + (node.externalManager ? 1 : 0) + countExternalManagers(node.children), + 0, + ); +} + function OrganizationNodeCard({ node, depth = 0 }: { node: OrganizationTreeNode; depth?: number }) { return (
@@ -78,20 +85,45 @@ export function OrganizationPage() { }, [organizationQuery.data?.items]); if (organizationQuery.isLoading) { - return
加载组织架构中...
; + return ( + + +
加载组织架构中...
+
+ ); } if (organizationQuery.isError) { - return
加载组织架构失败
; + return ( + + +
加载组织架构失败
+
+ ); } const sections = organizationQuery.data?.items ?? []; + const totalEmployees = sections.reduce((total, section) => total + countNodes(section.roots), 0); + const externalManagerCount = sections.reduce((total, section) => total + countExternalManagers(section.roots), 0); + const expandedCount = sections.filter((section) => { + const key = section.department?.id ?? section.department?.name ?? "unknown"; + return expandedDepartments[key] ?? true; + }).length; return ( -
- +
+ ); } diff --git a/client/src/pages/PeoplePage.tsx b/client/src/pages/PeoplePage.tsx index 8b0bd8ec..80fbf7bb 100644 --- a/client/src/pages/PeoplePage.tsx +++ b/client/src/pages/PeoplePage.tsx @@ -29,7 +29,7 @@ import { hasPermission } from "../auth/permissions"; import { EmployeeAvatar } from "../components/common/EmployeeAvatar"; import { ConfirmDialog } from "../components/common/ConfirmDialog"; import { DateTextInput } from "../components/common/DateTextInput"; -import { PageHeader } from "../components/common/PageHeader"; +import { ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace"; import { Button } from "../components/ui/button"; import { Dialog, @@ -397,6 +397,13 @@ export function PeoplePage() { const wecomHrMasterError = wecomHrMasterRunsQuery.isError ? mapApiMessage(wecomHrMasterRunsQuery.error, "企微 HR 主数据同步未就绪") : ""; + const peopleStats = { + total: employeesQuery.data?.total ?? items.length, + active: items.filter((item) => item.status === "active").length, + archived: items.filter((item) => item.status === "archived").length, + departments: new Set(items.map((item) => item.department?.id).filter(Boolean)).size, + wecomAttention: items.filter((item) => shouldShowWecomRetry(item.wecomDirectory)).length, + }; function toggleSelection(employeeId: string, checked: boolean) { setSelectedEmployeeIds((current) => { @@ -415,10 +422,11 @@ export function PeoplePage() { } return ( -
- + {canManageUsers ? ( @@ -462,8 +470,31 @@ export function PeoplePage() { } /> +
+
+
员工总数
+
{peopleStats.total}
+
+
+
当前页生效
+
{peopleStats.active}
+
+
+
当前页归档
+
{peopleStats.archived}
+
+
+
涉及部门
+
{peopleStats.departments}
+
+
+
企微待处理
+
{peopleStats.wecomAttention}
+
+
+ {canManageUsers ? ( -
+
@@ -507,10 +538,21 @@ export function PeoplePage() {
-
+
) : null} -
+
+
+
+

筛选与批量范围

+

+ 先收窄部门、状态、上级和入职时间,再执行批量动作。 +

+
+
+ 已选 {selectedEmployeeIds.length} 人 +
+
到 {hireDateTo || "-"}
-
+ {canManageUsers && selectedEmployeeIds.length ? ( -
+
已选 {selectedEmployeeIds.length} 名员工
@@ -622,10 +664,16 @@ export function PeoplePage() { 清空选择
-
+ ) : null} -
+
+
+

人员列表

+

+ 查看档案进入个人详情;启停、离职和企微重试会保留操作结果。 +

+
{exportError ? (

{exportError} @@ -636,124 +684,126 @@ export function PeoplePage() { {mapApiMessage(employeesQuery.error, "加载人员列表失败")}

) : null} - - - - {canManageUsers ? 选择 : null} - 员工 - 部门 / 上级 - 岗位 / 雇用状态 - 角色 - 账号状态 - 企业微信 - 操作 - - - - {items.length ? ( - items.map((item) => { - const wecomView = wecomDirectoryStatusView(item.wecomDirectory); - return ( - - {canManageUsers ? ( + +
+ + + {canManageUsers ? 选择 : null} + 员工 + 部门 / 上级 + 岗位 / 雇用状态 + 角色 + 账号状态 + 企业微信 + 操作 + + + + {items.length ? ( + items.map((item) => { + const wecomView = wecomDirectoryStatusView(item.wecomDirectory); + return ( + + {canManageUsers ? ( + + toggleSelection(item.id, event.target.checked)} + /> + + ) : null} - toggleSelection(item.id, event.target.checked)} - /> - - ) : null} - -
- -
-
{item.name}
-
- {item.employeeNo} · {item.email} +
+ +
+
{item.name}
+
+ {item.employeeNo} · {item.email} +
-
- - -
-
{item.department?.name ?? "-"}
-
- 上级:{item.directManager ? `${item.directManager.name} · ${item.directManager.employeeNo}` : "-"} + + +
+
{item.department?.name ?? "-"}
+
+ 上级:{item.directManager ? `${item.directManager.name} · ${item.directManager.employeeNo}` : "-"} +
-
- - -
-
{item.title || "-"}
-
- {employmentStatusLabel(item.employmentStatus)} · 入职 {item.hireDate ?? "-"} + + +
+
{item.title || "-"}
+
+ {employmentStatusLabel(item.employmentStatus)} · 入职 {item.hireDate ?? "-"} +
-
- - {item.roleCodes.join(" / ") || "-"} - - - - -
- -
{wecomView.hint}
-
-
- -
- - {canManageUsers ? ( - <> - - - {shouldShowWecomRetry(item.wecomDirectory) ? ( + + {item.roleCodes.join(" / ") || "-"} + + + + +
+ +
{wecomView.hint}
+
+
+ +
+ + {canManageUsers ? ( + <> + + + {shouldShowWecomRetry(item.wecomDirectory) ? ( + + ) : null} - ) : null} - - - ) : null} -
-
- - ); - }) - ) : ( - - - 暂无匹配员工 - - - )} - -
-
+ + ) : null} +
+ + + ); + }) + ) : ( + + + 暂无匹配员工 + + + )} + + + + @@ -890,7 +940,7 @@ export function PeoplePage() { } }} /> -
+ ); } diff --git a/client/src/pages/WorkspaceSetupPage.tsx b/client/src/pages/WorkspaceSetupPage.tsx index 7e322f04..e0663a9f 100644 --- a/client/src/pages/WorkspaceSetupPage.tsx +++ b/client/src/pages/WorkspaceSetupPage.tsx @@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { fetchSystemNavigation, fetchWorkspaceSettings, updateWorkspaceSettings } from "../api/workspace"; import { isApiError } from "../api/types"; import { mapApiErrorMessage } from "../api/errorMessages"; -import { PageHeader } from "../components/common/PageHeader"; +import { ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace"; import { Button } from "../components/ui/button"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table"; import { toast } from "sonner"; @@ -46,6 +46,16 @@ const MODULE_LABELS: Record = { profile: "个人设置", }; +const VISIBILITY_OPTIONS = [ + { value: "main", label: "主导航" }, + { value: "admin", label: "管理区" }, + { value: "hidden", label: "隐藏" }, +] as const; + +function visibilityModeLabel(mode: string) { + return VISIBILITY_OPTIONS.find((item) => item.value === mode)?.label ?? mode; +} + export function WorkspaceSetupPage() { const queryClient = useQueryClient(); const workspaceQuery = useQuery({ queryKey: ["workspace-settings"], queryFn: fetchWorkspaceSettings }); @@ -74,95 +84,139 @@ export function WorkspaceSetupPage() { const workspace = workspaceQuery.data; const rolePresets = navigationQuery.data?.recommendedRolePresets ?? []; const moduleEntries = useMemo(() => Object.entries(workspace?.moduleVisibility ?? {}), [workspace?.moduleVisibility]); + const workspaceStats = useMemo(() => { + const entries = Object.entries(workspace?.moduleVisibility ?? {}); + return { + total: entries.length, + main: entries.filter(([, mode]) => mode === "main").length, + admin: entries.filter(([, mode]) => mode === "admin").length, + hidden: entries.filter(([, mode]) => mode === "hidden").length, + rolePresets: rolePresets.length, + }; + }, [rolePresets.length, workspace?.moduleVisibility]); if (workspaceQuery.isLoading || !workspace) { return ( -
- + -
加载工作区设置中...
-
+
加载工作区设置中...
+ ); } return ( -
- + -
-
-
产品模式
-
{workspace.profile}
-
单公司、账号密码、站内通知、桌面优先。
+
+
+
模块总数
+
{workspaceStats.total}
-
-
初始化状态
-
{workspace.persisted ? "已完成" : "待初始化"}
-
首次管理员进入此页后保存一次,即完成小团队工作区初始化。
+
+
主导航
+
{workspaceStats.main}
-
-
推荐角色预设
-
+
+
管理区
+
{workspaceStats.admin}
+
+
+
隐藏
+
{workspaceStats.hidden}
+
+
+
初始化
+
{workspace.persisted ? "已完成" : "待初始化"}
+
+
+ +
+
+
+

工作区基线

+

+ {workspace.profile} · 单公司 · 账号密码 · 站内通知 · 桌面优先。 +

+
+
+ {workspaceStats.rolePresets} 个角色预设 +
+
+ + {rolePresets.length ? ( +
{rolePresets.map((item) => ( -
+
{item.label}
{item.description}
角色:{item.roleCodes.join(" / ")}
))}
-
-
+ ) : ( +
+ 当前没有推荐角色预设。 +
+ )} + -
+

模块显隐

-

主导航只保留高频入口;备用模块保留代码与数据,但默认放在管理员备用区。

+

+ 主导航只保留高频入口;备用模块保留代码与数据,低频功能默认收进管理区或隐藏。 +

- - - - 模块 - 当前显隐 - 操作 - - - - {moduleEntries.map(([key, mode]) => ( - - {MODULE_LABELS[key] ?? key} - {mode} - -
- {(["main", "admin", "hidden"] as const).map((nextMode) => ( - - ))} -
-
+ +
+ + + 模块 + 当前显隐 + 操作 - ))} - -
-
-
+ + + {moduleEntries.map(([key, mode]) => ( + + {MODULE_LABELS[key] ?? key} + {visibilityModeLabel(mode)} + +
+ {VISIBILITY_OPTIONS.map((option) => ( + + ))} +
+
+
+ ))} +
+ + + + ); } diff --git a/client/tests/e2e/pc-finance-admin-visual-wave4.spec.ts b/client/tests/e2e/pc-finance-admin-visual-wave4.spec.ts index b65fcb99..2c8b0061 100644 --- a/client/tests/e2e/pc-finance-admin-visual-wave4.spec.ts +++ b/client/tests/e2e/pc-finance-admin-visual-wave4.spec.ts @@ -30,8 +30,11 @@ const allPermissions = [ "risk.review.manage", "travel.payment.manage", "system.user.manage", + "system.users", "system.settings", "system.audit.view", + "masterdata.manage", + "notification.channel.manage", ]; const visualMe = { @@ -517,9 +520,13 @@ async function mockWave4Apis(page: Page, options: Wave4MockOptions = {}) { navigationItem("invoice-pool", "/invoice-pool", "票据中心", "receipt_long"), ], admin: [ + navigationItem("people", "/people", "人员管理", "badge", "admin"), + navigationItem("organization", "/organization", "组织架构", "account_tree", "admin"), + navigationItem("workspace", "/system/workspace", "工作区设置", "dashboard_customize", "admin"), navigationItem("users", "/system/users", "用户管理", "group", "admin"), navigationItem("master-data", "/system/master-data", "主数据", "database", "admin"), navigationItem("notification-channels", "/system/notification-channels", "通知渠道", "notifications", "admin"), + navigationItem("approval-rules", "/system/approval-rules", "审批规则", "account_tree", "admin"), navigationItem("settings", "/system/settings", "系统设置", "settings", "admin"), navigationItem("feature-flags", "/system/feature-flags", "功能开关", "toggle_on", "admin"), navigationItem("workflow-definitions", "/system/workflow-definitions", "流程定义", "schema", "admin"), @@ -532,6 +539,35 @@ async function mockWave4Apis(page: Page, options: Wave4MockOptions = {}) { return; } + if (pathName === "/api/system/workspace") { + await fulfillJson(route, { + id: "workspace-wave4", + persisted: true, + profile: "small_team_internal", + orgMode: "single_company", + primaryEntryMode: "password", + notificationStrategy: "in_app", + devicePriority: "desktop", + moduleVisibility: { + portal: "main", + workflowTasks: "main", + reimbursements: "main", + paymentBatches: "admin", + apOperations: "admin", + invoicePool: "admin", + people: "admin", + organization: "admin", + approvalRules: "admin", + featureFlags: "admin", + travelRequests: "hidden", + }, + createdAt: null, + updatedAt: "2026-05-29T08:00:00.000Z", + updatedBy: visualMe.id, + }); + return; + } + if (pathName === "/api/notifications") { await fulfillJson(route, { page: 1, pageSize: 5, total: 0, unreadOnly: false, unreadCount: 0, items: [] }); return; @@ -803,6 +839,97 @@ async function mockWave4Apis(page: Page, options: Wave4MockOptions = {}) { return; } + if (pathName === "/api/employees/organization") { + await fulfillJson(route, { + items: [ + { + department: departments[0], + roots: [ + { + id: visualMe.id, + employeeNo: visualMe.employeeNo, + name: visualMe.name, + title: "管理员", + status: "active", + avatarUrl: null, + department: departments[0], + externalManager: null, + children: [], + }, + ], + }, + { + department: departments[1], + roots: [], + }, + ], + }); + return; + } + + if (pathName === "/api/employees/wecom-hr-master-sync/runs") { + await fulfillJson(route, { + items: [ + { + id: "wecom-hr-run-wave4", + mode: "dry_run", + status: "success", + tokenSource: null, + requestedUserids: null, + limit: 50, + summary: { total: 1, created: 0, updated: 1, skipped: 0, conflicts: 0, failed: 0 }, + errorCode: null, + errorMessage: null, + triggeredBy: visualMe.id, + startedAt: "2026-05-29T07:50:00.000Z", + finishedAt: "2026-05-29T07:51:00.000Z", + createdAt: "2026-05-29T07:50:00.000Z", + updatedAt: "2026-05-29T07:51:00.000Z", + itemCount: 1, + items: [], + }, + ], + }); + return; + } + + if (pathName === "/api/approval-rules") { + await fulfillJson(route, { + enabled: null, + items: [ + { + id: "approval-rule-wave5-1", + name: "5000 以下经理审批", + minAmount: "0.00", + maxAmount: "5000.00", + level1Role: "manager", + level2Role: null, + level3Role: null, + ownerSelfFallbackRole: "finance_mgr", + enabled: true, + priority: 1, + createdAt: "2026-05-29T08:00:00.000Z", + updatedAt: "2026-05-29T08:00:00.000Z", + }, + { + id: "approval-rule-wave5-2", + name: "高额财务负责人复核", + minAmount: "5000.01", + maxAmount: null, + level1Role: "manager", + level2Role: "finance_mgr", + level3Role: "ceo", + ownerSelfFallbackRole: null, + enabled: false, + priority: 2, + createdAt: "2026-05-29T08:00:00.000Z", + updatedAt: "2026-05-29T08:00:00.000Z", + }, + ], + }); + return; + } + if (pathName === "/api/master-data/legal-entities") { await fulfillJson(route, { includeArchived: false, items: legalEntities }); return; @@ -886,16 +1013,20 @@ const pages = [ { path: "/payment-batches", summary: "payment-batches-summary", proof: "text=批次列表", screenshot: "01-payment-batches" }, { path: "/ap-operations", summary: "ap-operations-summary", proof: "text=异常付款", screenshot: "02-ap-operations" }, { path: "/invoice-pool", summary: "invoice-pool-summary", proof: "data-testid=invoice-pool-table", screenshot: "03-invoice-pool" }, - { path: "/system/users", summary: "users-summary", proof: "data-testid=users-table", screenshot: "04-users" }, - { path: "/system/master-data", summary: "master-data-summary", proof: "text=法体列表", screenshot: "05-master-data" }, - { path: "/system/notification-channels", summary: "notification-channels-summary", proof: "data-testid=notification-channels-table", screenshot: "06-notification-channels" }, - { path: "/system/settings", summary: "system-settings-summary", proof: "text=系统设置", screenshot: "07-system-settings" }, - { path: "/system/feature-flags", summary: "feature-flags-summary", proof: "data-testid=feature-flags-table", screenshot: "08-feature-flags" }, - { path: "/system/workflow-definitions", summary: "workflow-definitions-summary", proof: "text=版本列表", screenshot: "09-workflow-definitions" }, - { path: "/audit-logs", summary: "audit-summary", proof: "text=PAYMENT_BATCH_REVIEW", screenshot: "10-audit-logs" }, + { path: "/system/approval-rules", summary: "approval-rules-summary", proof: "data-testid=approval-rules-table", screenshot: "04-approval-rules" }, + { path: "/people", summary: "people-summary", proof: "data-testid=people-table", screenshot: "05-people" }, + { path: "/organization", summary: "organization-summary", proof: "text=研发部", screenshot: "06-organization" }, + { path: "/system/workspace", summary: "workspace-settings-summary", proof: "data-testid=workspace-modules-table", screenshot: "07-workspace" }, + { path: "/system/users", summary: "users-summary", proof: "data-testid=users-table", screenshot: "08-users" }, + { path: "/system/master-data", summary: "master-data-summary", proof: "text=法体列表", screenshot: "09-master-data" }, + { path: "/system/notification-channels", summary: "notification-channels-summary", proof: "data-testid=notification-channels-table", screenshot: "10-notification-channels" }, + { path: "/system/settings", summary: "system-settings-summary", proof: "text=系统设置", screenshot: "11-system-settings" }, + { path: "/system/feature-flags", summary: "feature-flags-summary", proof: "data-testid=feature-flags-table", screenshot: "12-feature-flags" }, + { path: "/system/workflow-definitions", summary: "workflow-definitions-summary", proof: "text=版本列表", screenshot: "13-workflow-definitions" }, + { path: "/audit-logs", summary: "audit-summary", proof: "text=PAYMENT_BATCH_REVIEW", screenshot: "14-audit-logs" }, ]; -test("PC 财务与管理页 Wave4 视觉冒烟", async ({ page }) => { +test("PC 财务与管理页 Wave4-5 视觉冒烟", async ({ page }) => { await mockWave4Apis(page); await page.setViewportSize({ width: 1440, height: 960 }); await login(page);