Commit 82127d80eea07241aa564378c1c599ed56086010
merge(frontend-phase): integrate frontend-phase
Showing
127 changed files
with
18066 additions
and
4 deletions
Too many changes to show.
To preserve performance only 23 of 127 files are displayed.
.gitignore
docs/08-模块任务管理.md
| @@ -58,7 +58,7 @@ | @@ -58,7 +58,7 @@ | ||
| 58 | 58 | ||
| 59 | - 整体里程碑: — | 59 | - 整体里程碑: — |
| 60 | - 功能: | 60 | - 功能: |
| 61 | - - [ ] FE-01 登录页(用户名/密码/版本下拉登录,对接 POST /api/usr/login) | ||
| 62 | - - [ ] FE-02 主页与导航框架(顶栏 + 全部导航总览 + 主页 KPI 看板 + 常用操作;登录后落地页与路由壳) | ||
| 63 | - - [ ] FE-03 用户列表与查询(工具栏刷新/导出 + 筛选条件 + 用户表格 + 分页,对接 GET /api/usr/users) | ||
| 64 | - - [ ] FE-04 用户信息单据(新增/修改用户表单 + 权限组勾选,对接 POST /api/usr/users 与 PUT /api/usr/users/{id}) | 61 | + - [x] FE-01 登录页(用户名/密码/版本下拉登录,对接 POST /api/usr/login) |
| 62 | + - [x] FE-02 主页与导航框架(顶栏 + 全部导航总览 + 主页 KPI 看板 + 常用操作;登录后落地页与路由壳) | ||
| 63 | + - [x] FE-03 用户列表与查询(工具栏刷新/导出 + 筛选条件 + 用户表格 + 分页,对接 GET /api/usr/users) | ||
| 64 | + - [x] FE-04 用户信息单据(新增/修改用户表单 + 权限组勾选,对接 POST /api/usr/users 与 PUT /api/usr/users/{id}) |
docs/superpowers/module-reports/2026-06-02-frontend-phase.md
0 → 100644
| 1 | +# 模块完成报告 — 前端阶段(frontend-phase) | ||
| 2 | + | ||
| 3 | +> 标准化 12 节《模块完成报告》。本报告由 test-gate 绿后渲染,供 milestone 标记使用。 | ||
| 4 | +> 数据来源:仅取 git 摘要(`diff --stat` / `log --oneline`)、FE specs/plans/reviews、test-gate 证据,未读 diff 正文进上下文。 | ||
| 5 | +> 输出语言:中文。阶段 = 前端(frontend),实现作用域 = `frontend/`。 | ||
| 6 | + | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +## ① 模块标识 | ||
| 10 | + | ||
| 11 | +| 字段 | 值 | | ||
| 12 | +|---|---| | ||
| 13 | +| module_id | `frontend-phase` | | ||
| 14 | +| module_name | 前端阶段(整体) | | ||
| 15 | +| 阶段 | 前端(frontend) | | ||
| 16 | +| 分支 | `frontend-phase`(自默认分支 `master` 分叉) | | ||
| 17 | +| 默认分支 | `master` | | ||
| 18 | +| 分叉点(merge-base) | `236c42b1e28ff8ecd775faf06071be595fbf81d3` | | ||
| 19 | +| 里程碑 | 整个前端阶段 1 个里程碑 tag(`milestone/frontend-phase`),由本报告 commit 后于 milestone 步骤标记 | | ||
| 20 | +| 报告日期 | 2026-06-02 | | ||
| 21 | + | ||
| 22 | +--- | ||
| 23 | + | ||
| 24 | +## ② FE 完成清单 | ||
| 25 | + | ||
| 26 | +前端阶段以 `frontend-phase` 单一阶段聚合 4 个 FE 业务功能(粒度=业务功能,可关联多个 prototype 区域与多个 REQ)。按 FE-NN 顺序: | ||
| 27 | + | ||
| 28 | +| FE-NN | 业务功能 | 关联原型区域 | 关联 REQ | spec / plan / review / verify 产物 | docs/08 § 三状态 | | ||
| 29 | +|---|---|---|---|---|---| | ||
| 30 | +| FE-01 | 登录页(用户名/密码/版本下拉登录,对接 `POST /api/usr/login`) | `#screen-login`(`.login-wrap` / `.login-card` / `.login-foot`) | REQ-USR-004(主);配套读端点 `GET /api/usr/companies` | spec `2026-06-01-FE-01.md` / plan 同名 / review `2026-06-01-FE-01.md`(approve)/ verify `2026-06-01-FE-01-verify.md` | [x] approved | | ||
| 31 | +| FE-02 | 主页与导航框架(顶栏 + 全部导航总览 + 主页 KPI 看板 + 常用操作;登录后落地页与路由壳) | `#topbar` / `#nav-overlay` / `#screen-main`(`.kpi-head` / `.three-col` / `.common-ops`) | 壳层无直接 CRUD REQ,承载 FE-01/03/04;当前用户身份来自 REQ-USR-004 响应 | spec `2026-06-01-FE-02.md` / plan 同名 / review `2026-06-01-FE-02.md`(approve)/ verify `2026-06-01-FE-02-verify.md` | [x] approved | | ||
| 32 | +| FE-03 | 用户列表与查询(工具栏刷新/导出 + 筛选条件 + 用户表格 + 分页,对接 `GET /api/usr/users`) | `#screen-userlist`(`.toolbar` / `.filterbar` / `.grid-table` / `.pager`) | REQ-USR-003(主) | spec `2026-06-01-FE-03.md` / plan 同名 / review `2026-06-01-FE-03.md`(approve)/ verify `2026-06-01-FE-03-verify.md` | [x] approved | | ||
| 33 | +| FE-04 | 用户信息单据(新增/修改用户表单 + 权限组勾选,对接 `POST /api/usr/users` + `PUT /api/usr/users/{id}`) | `#screen-userdetail`(`.toolbar` / `.form-grid` / `.tabs-row` / `.perm-list`) | REQ-USR-001(增)+ REQ-USR-002(改) | spec `2026-06-01-FE-04.md` / plan 同名 / review `2026-06-01-FE-04.md`(r1 request-changes → r2 approve)/ verify `2026-06-01-FE-04-verify.md` + `-verify-r1.md` | [x] approved | | ||
| 34 | + | ||
| 35 | +> 4 个 FE 在 docs/08 § 三均为 `[x]`(已 approved)。FE-04 经历 1 轮 request-changes(must-fix B1:edit 态预填把用户主键当「用户号」查询)后 fix 复验通过,详见 § ⑧/§ ⑨。 | ||
| 36 | + | ||
| 37 | +--- | ||
| 38 | + | ||
| 39 | +## ③ 文件变更(git diff --stat master...HEAD,三点 diff) | ||
| 40 | + | ||
| 41 | +区间 = 功能分支 `frontend-phase` 自默认分支 `master` 分叉以来的全部改动。 | ||
| 42 | + | ||
| 43 | +- **总计**:126 files changed,**17860 insertions(+)**,4 deletions(-)。 | ||
| 44 | +- **实现代码(`frontend/`)**:源码 + 配置 + 测试,全部落在 `frontend/` 作用域内(无 `backend/` / `sql/` / `scripts/` 越界)。 | ||
| 45 | +- **文档(`docs/`)**:FE-01~04 的 specs/plans/reviews/verify + test-gate 证据 + docs/08 § 三勾选更新。 | ||
| 46 | +- **根级**:`.gitignore`(+3)。 | ||
| 47 | + | ||
| 48 | +### 分类汇总 | ||
| 49 | + | ||
| 50 | +| 分类 | 主要文件(节选) | 量级 | | ||
| 51 | +|---|---|---| | ||
| 52 | +| 工程骨架 / 配置 | `frontend/package.json` / `package-lock.json`(+6352)/ `vite.config.ts` / `tsconfig.json` / `.eslintrc.cjs` / `playwright.config.ts` / `index.html` | 8 文件 | | ||
| 53 | +| API 基建 | `src/api/request.ts`(Axios 实例 + Result 拆包 + 401 拦截)/ `src/api/types.ts` / `src/api/usrApi.ts`(login/companies/listUsers/create/update/detail/employees/permissions) | 3 文件 | | ||
| 54 | +| Store | `src/store/store.ts` / `hooks.ts` / `slices/authSlice.ts`(登录态 + token 持久化) | 3 文件 | | ||
| 55 | +| 路由 | `src/router/index.tsx` / `RequireAuth.tsx` / `RedirectIfAuthed.tsx` / `AppErrorBoundary.tsx` | 4 文件 | | ||
| 56 | +| 外壳(FE-02) | `src/layouts/AppLayout/*`(AppLayout / TopBar / NavOverlay / CurrentUserMenu / AppFooter / useTabStack / navConfig / shellMessages) | 10 文件 | | ||
| 57 | +| 登录页(FE-01) | `src/pages/usr/Login/*`(LoginPage / Login.module.css / loginMessages) | 3 文件 | | ||
| 58 | +| 主页(FE-02) | `src/pages/home/HomePage/*`(HomePage / KpiBoard / KpiHeadBar / RoleProcessTree / CommonOps / dashboardData / 样式) | 7 文件 | | ||
| 59 | +| 用户列表(FE-03) | `src/pages/usr/UserList/*`(index / UserToolbar / UserFilterBar / UserTable / columns / constants / exportUtils / useUserList / 样式) | 9 文件 | | ||
| 60 | +| 用户单据(FE-04) | `src/pages/usr/UserDetail/*`(index / UserDetailToolbar / UserBasicForm / PermissionTabs / PermissionGroupList / useUserDetail / constants / 样式) | 9 文件 | | ||
| 61 | +| 全局样式 / 入口 | `src/main.tsx` / `App.tsx` / `styles/global.css` / `styles/theme.ts` / `vite-env.d.ts` | 5 文件 | | ||
| 62 | +| 测试 | `tests/setup.ts` + `tests/unit/*`(40 单测文件)+ `tests/e2e/*`(login / shell / userlist / userdetail 4 spec) | 45 文件 | | ||
| 63 | +| 文档证据 | `docs/superpowers/{specs,plans,reviews}/2026-06-01-FE-0*.md` + `module-reports/frontend-phase-test-gate-r1.md` + docs/08 § 三 | 17 文件 | | ||
| 64 | + | ||
| 65 | +--- | ||
| 66 | + | ||
| 67 | +## ④ 数据库使用表 | ||
| 68 | + | ||
| 69 | +`N/A(前端阶段)` —— 前端阶段不直接访问数据库,所有数据经后端端点(`/api/usr/*`)消费。 | ||
| 70 | + | ||
| 71 | +--- | ||
| 72 | + | ||
| 73 | +## ⑤ 测试闸(test-gate)记录与 flake 判定 | ||
| 74 | + | ||
| 75 | +> 汇总 `docs/superpowers/module-reports/frontend-phase-test-gate-r*.md`(按 attempt 升序)。 | ||
| 76 | + | ||
| 77 | +- **attempt 总数**:1(仅 `frontend-phase-test-gate-r1.md`)。 | ||
| 78 | +- **flake 判定**:**无 flake**(仅 1 次 attempt,无 red→green 切换)。最后一份(也是唯一一份)= **GREEN**,前置闸通过。 | ||
| 79 | + | ||
| 80 | +### attempt 1(r1)— GREEN | ||
| 81 | + | ||
| 82 | +| 套件 | 命令 | exit_code | passed | failed | 时间窗口 | | ||
| 83 | +|---|---|---|---|---|---| | ||
| 84 | +| 单元(vitest) | `npm run test:unit`(= `vitest run`) | 0 | **193**(40 文件) | 0 | 2026-06-02 09:26:46 → 09:26:53 | | ||
| 85 | +| E2E(playwright) | `npm run test:e2e`(= `playwright test`,chromium,Vite dev server 5173,`page.route` 桩后端) | 0 | **20** | 0 | 2026-06-02 09:26:57 → 09:27:04 | | ||
| 86 | + | ||
| 87 | +- **结论**:两套件 `exit_code` 均 0、`failed=0`,覆盖全部 FE 回归(vitest 193 + playwright 20)。 | ||
| 88 | +- E2E 覆盖 spec:`login.spec.ts` / `shell.spec.ts` / `userlist.spec.ts` / `userdetail.spec.ts`。 | ||
| 89 | +- **非致命噪声(不影响断言)**:jsdom 侧 `window.getComputedStyle not implemented`、React Router v7 future flag 警告;E2E 侧 `[vite] http proxy error ... ECONNREFUSED`(dev proxy 向未启动的真实后端转发,用例已用 `page.route` 拦截 `/api/**`,与断言无关)。 | ||
| 90 | +- 命令来源:`docs/04-技术规范.md § 零` 命令清单(前端 unit=`vitest run`、e2e=`playwright test`)。执行方式:派发独立(detached)子进程跑测试,主会话仅消费结构化结果,未在主会话直接跑测试。 | ||
| 91 | + | ||
| 92 | +--- | ||
| 93 | + | ||
| 94 | +## ⑥ Migration(Flyway) | ||
| 95 | + | ||
| 96 | +`N/A(前端阶段)` —— 前端阶段不涉及 schema 演化,未新增 `sql/migrations/V*.sql`。 | ||
| 97 | + | ||
| 98 | +--- | ||
| 99 | + | ||
| 100 | +## ⑦ 跨模块改动 | ||
| 101 | + | ||
| 102 | +`N/A(前端阶段)` —— 实现改动全部落在 `frontend/` 作用域内(git diff --stat 确认无 `backend/` / `sql/` / `scripts/` 文件变更);无跨模块日志(`docs/superpowers/cross-module-log*` 不存在),无需记录跨模块影响评估。 | ||
| 103 | + | ||
| 104 | +> 待对齐项(非本阶段改动,记于此供后端编码期消化):FE-04 § 8 D1/D2 的支撑只读端点 `GET /api/usr/employees`(员工下拉)/ `GET /api/usr/permissions`(权限组)需在后端 REQ-USR-001/002 实现内补齐契约(同 FE-01 `GET /api/usr/companies` 先例)。这是跨阶段(前端→后端)的接口对齐项,前端已按既有先例消费,未越界改后端代码。 | ||
| 105 | + | ||
| 106 | +--- | ||
| 107 | + | ||
| 108 | +## ⑧ 偏离清单(实际渲染 DOM ↔ 各 FE 关联原型主结构) | ||
| 109 | + | ||
| 110 | +> 硬验证项:逐 FE 列举「实际渲染 DOM 与关联原型主结构的差异」。原型 `prototype/erp.html` 为单文件静态 demo(4 个 `#screen-*` 区段),实现为 React + AntD 5 路由化应用。下列偏离**均为 spec 已登记的有意决策(D 系列)或交互语义复刻取舍**,非缺陷;每条注明对应决策来源。 | ||
| 111 | + | ||
| 112 | +### FE-01 登录页(原型 `#screen-login`) | ||
| 113 | + | ||
| 114 | +| # | 偏离 | 性质 | 来源 | | ||
| 115 | +|---|---|---|---| | ||
| 116 | +| 1 | 原型「屏切换」demo(`goTo('login')` + 按钮 `data-go="main"` 直接切主页)→ 实现为独立路由 `/login`,提交走 `POST /api/usr/login`,成功才 `navigate('/')` | 有意(交互语义复刻) | FE-01 spec § 6.4 / D3 | | ||
| 117 | +| 2 | 原型版本下拉硬编码「标准版」demo 项 → 实现 options 全部来自 `GET /api/usr/companies`,不硬编码 | 有意 | FE-01 D1/D8 | | ||
| 118 | +| 3 | 主视觉深蓝渐变 / 网格透视背景作为登录页局部 scoped 装饰样式保留,不新增全局 token、不挪用语义 token | 有意(tokens.css 无对应品牌深色) | FE-01 D7 | | ||
| 119 | +| 4 | 登录失败(40101/42901)后清空密码并聚焦 —— 原型无此交互 | 有意(通用安全交互增强) | FE-01 D5 | | ||
| 120 | + | ||
| 121 | +### FE-02 主页与导航框架(原型 `#topbar` / `#nav-overlay` / `#screen-main`) | ||
| 122 | + | ||
| 123 | +| # | 偏离 | 性质 | 来源 | | ||
| 124 | +|---|---|---|---| | ||
| 125 | +| 1 | 原型单页多屏(`goTo(name)` 切 `.screen.active`)→ 实现为 React Router v6 真实路由 + `<AppLayout>` 布局路由包裹受保护子路由 | 有意(核心架构复刻) | FE-02 spec § 6.1/6.3 | | ||
| 126 | +| 2 | KPI 看板 / 角色流程树 / 导航分组 = 原型静态 demo → 实现为前端静态配置(`dashboardData.ts` / `navConfig.ts`),**不新增后端取数**(docs/05 无 KPI/导航端点) | 有意(不杜撰端点) | FE-02 D1/D2 | | ||
| 127 | +| 3 | 搜索 / 通知 / 更多 / AI 助手图标、导航总览中无路由的叶子项 → 占位(不绑后端,点击关 overlay 不跳转或 `message.info`) | 有意(占位化) | FE-02 D4 | | ||
| 128 | +| 4 | 顶栏 / 导航 overlay 深色底作为 `AppLayout` 局部 scoped 装饰样式保留 | 有意(tokens.css 无对应品牌深色) | FE-02 D9 | | ||
| 129 | +| 5 | 退出登录为纯前端注销(清 `authSlice` + 删 token + 跳 `/login`),无后端注销端点 | 有意(无状态 JWT) | FE-02 D6 | | ||
| 130 | +| 6 | 标签栈 / overlay 开关用 `AppLayout` 本地 `useState`(`useTabStack`),未上提全局 store | 有意(就近态) | FE-02 D3 | | ||
| 131 | + | ||
| 132 | +### FE-03 用户列表与查询(原型 `#screen-userlist`) | ||
| 133 | + | ||
| 134 | +| # | 偏离 | 性质 | 来源 | | ||
| 135 | +|---|---|---|---| | ||
| 136 | +| 1 | 原型 `users` 静态数组直接渲染 `tbody` → 实现真实对接 `GET /api/usr/users`(服务端分页 + 筛选 + 加载/空/错误态) | 有意(核心功能复刻) | FE-03 D1 | | ||
| 137 | +| 2 | 原型分页写死「共37条记录 / 10000 条/页」→ 实现 `pageSize` 默认 10、`showSizeChanger=[10,20,50,100]`(上限对齐 docs/05 最大 100),不沿用 demo 10000 | 有意(对齐契约约束) | FE-03 D4 | | ||
| 138 | +| 3 | 筛选栏首个下拉「全部用户」(原型单项 demo)→ 保留控件位但不向后端传额外「范围」参数(契约无此参数) | 有意(不杜撰参数) | FE-03 D2 | | ||
| 139 | +| 4 | 筛选栏「▾」更多条件、工具栏设置齿轮「⚙」→ 占位(无额外后端参数/端点) | 有意(占位化) | FE-03 D3/D7 | | ||
| 140 | +| 5 | 「导出Excel」原型无后端脚本 → 实现为前端导出当前查询结果(不杜撰后端导出端点) | 有意(前端实现) | FE-03 D5 | | ||
| 141 | +| 6 | 「作废」列原型为 demo 复选框 → 实现只读展示 `iIsVoid`(0/1→否/是,不可勾选) | 有意(只读查询语义) | FE-03 BR6 | | ||
| 142 | +| 7 | 行 `dblclick` 原型 `goTo('userdetail')` → 实现 `navigate('/usr/users/'+row.id)` 携 id 进 FE-04 | 有意(路由复刻) | FE-03 BR12 | | ||
| 143 | + | ||
| 144 | +### FE-04 用户信息单据(原型 `#screen-userdetail`) | ||
| 145 | + | ||
| 146 | +| # | 偏离 | 性质 | 来源 | | ||
| 147 | +|---|---|---|---| | ||
| 148 | +| 1 | 原型表单字段值写死 + `setUserDetailMode('new')` 仅做 DOM 文本清空 → 实现按路由 mode(`/new`=create / `/:id`=edit)分支,create 默认值预填、edit 回填原值 | 有意(核心功能复刻) | FE-04 spec § 2/3 | | ||
| 149 | +| 2 | 员工名下拉 / 权限组列表 = 原型静态 demo → 实现消费支撑只读端点 `GET /api/usr/employees` / `GET /api/usr/permissions`(端点须后端补齐,见 § ⑦ 待对齐项) | 有意(下拉需读源) | FE-04 D1/D2 | | ||
| 150 | +| 3 | edit 态预填复用列表端点 `GET /api/usr/users`(「等于」匹配 + pageSize=1 取 `records[0]`),不杜撰单用户详情端点;并支持 FE-03 经路由 state 携行数据免二次请求 | 有意(不杜撰端点);**含 1 处经 review 修复的偏离**:r1 误把用户主键当「用户号」查询导致 40401,r2 已修为走 navigate state + loadError 返回列表入口 | FE-04 D4 / review B1(见 § ⑨) | | ||
| 151 | +| 4 | 权限页签条除「权限组」外的 5 个查看权限页签(客户/供应商/人员/工序/司机)→ 占位页签(无 REQ/端点) | 有意(占位化) | FE-04 D9 | | ||
| 152 | +| 5 | 工具栏「删除/作废/重置密码/取消作废/功能」按钮 → 占位(无对应端点);其中「作废」语义经 PUT 已有字段 `iIsVoid` 承载,不新增端点 | 有意(占位化) | FE-04 D8 | | ||
| 153 | +| 6 | 密码字段不在 UI 呈现(create 后端默认 666666 初始化,edit 不改密码) | 有意(契约语义) | FE-04 BR9 | | ||
| 154 | +| 7 | 工具栏深色底作为 `UserDetail` 局部 scoped 装饰样式;原型必填红 `--label` 映射到 `var(--color-error)` | 有意(tokens 优先) | FE-04 D10 | | ||
| 155 | + | ||
| 156 | +> **偏离总结**:所有偏离均为「静态 demo → 真实路由/API」「不杜撰后端端点的占位化」「tokens.css 优先于原型私有色值」三类有意决策,无未登记的非预期偏离。唯一一处经 review 纠错的偏离(FE-04 edit 预填数据流 B1)已于 r2 修复并复验通过。各 FE 实现组件与原型 4 个 `#screen-*` 主结构一一对应(登录/主页/列表/单据 + 顶栏 + 导航 overlay)。 | ||
| 157 | + | ||
| 158 | +--- | ||
| 159 | + | ||
| 160 | +## ⑨ Review 与缺陷修复记录 | ||
| 161 | + | ||
| 162 | +| FE-NN | review 轮次 | 裁决 | must-fix | 修复 commit | | ||
| 163 | +|---|---|---|---|---| | ||
| 164 | +| FE-01 | r1 | approve | 无 | — | | ||
| 165 | +| FE-02 | r1 | approve | 无 | — | | ||
| 166 | +| FE-03 | r1 | approve | 无 | — | | ||
| 167 | +| FE-04 | r1 | request-changes | B1:edit 态预填把用户主键当「用户号」查询,正常导航流必然取不到记录 → 40401 | `96e88d3 fix(usr): 修复 review must-fix FE: FE-04 编辑预填走 navigate state 并补 loadError 返回列表入口` | | ||
| 168 | +| FE-04 | r2 | approve | 无(B1 已修复复验,verify-r1 全 PASS) | — | | ||
| 169 | + | ||
| 170 | +> FE-04 修复后单测 55 passed / E2E 5 passed(`2026-06-01-FE-04-verify-r1.md`),edit 预填用例(`useUserDetail`/`UserDetailPage` edit 分支、E2E `edit user prefill then save`)全部通过。最终 test-gate r1 GREEN 覆盖全量回归。 | ||
| 171 | + | ||
| 172 | +--- | ||
| 173 | + | ||
| 174 | +## ⑩ 关键技术决策与约定(跨 FE 复用资产) | ||
| 175 | + | ||
| 176 | +| 约定 | 落地 | 来源 | | ||
| 177 | +|---|---|---| | ||
| 178 | +| 请求基建 | `src/api/request.ts`:Axios 实例 `baseURL=/api`,开发期 Vite proxy 转发到后端 `http://localhost:5172`(`config-vars.yaml backend.http_port`);响应拦截器拆 `Result`(code=0 取 data,非 0 抛错),401 统一跳 `/login` | docs/04 § 2.3/2.4;FE-01 D2 | | ||
| 179 | +| 登录态 | `authSlice`(RTK)持 token + user;token 持久化 localStorage 键 `xly_erp_token`,刷新后由 `request.ts` 注入 `Authorization: Bearer` | docs/04 § 2.2;FE-01 D6 | | ||
| 180 | +| 路由守卫 | `RequireAuth`(三态:authResolving/unauthenticated/ready)+ `RedirectIfAuthed`(已登录访问 /login 回主页)+ `AppErrorBoundary`(路由级兜底) | docs/04 § 2.1;FE-02 § 6.2 | | ||
| 181 | +| 状态分层 | 服务端数据就近用页面 hook(`useUserList` / `useUserDetail`),不进全局 store;仅登录态进 store | docs/04 § 2.2;FE-03 D6 / FE-04 D7 | | ||
| 182 | +| 色值 SSoT | 组件样式只用 `var(--color-*)`(`src/styles/tokens.css`),tokens.css 优先于原型私有色值;品牌深色(顶栏/overlay/登录主视觉/工具条)作局部 scoped 装饰,不新增全局 token、不挪用语义 token | 各 FE § 7 + D7/D9/D10 | | ||
| 183 | +| 不杜撰后端端点 | 仅消费 docs/05 已定义端点 + 既有先例支撑只读端点(companies/employees/permissions);导出、注销、KPI/导航均不杜撰端点 | 硬约束 + 各 FE D 系列 | | ||
| 184 | + | ||
| 185 | +--- | ||
| 186 | + | ||
| 187 | +## ⑪ 下一模块预览(上线 / 部署后续步骤) | ||
| 188 | + | ||
| 189 | +前端阶段是 Coding 阶段最后一个聚合阶段(排在所有后端模块之后)。本报告 commit + milestone 标记后,进入上线 / 部署后续: | ||
| 190 | + | ||
| 191 | +1. **里程碑标记**:本报告 commit 落地后,于 milestone 步骤打 `milestone/frontend-phase` tag(前置依赖工作树干净,本 commit 满足)。 | ||
| 192 | +2. **分支合并**:`frontend-phase` 合并回默认分支 `master`(按 superpowers `finishing-a-development-branch` 流程,PR 或直接合并由上层编排决定)。 | ||
| 193 | +3. **跨阶段接口对齐(阻塞上线联调)**:后端需补齐 FE 消费的支撑只读端点契约——`GET /api/usr/companies`(FE-01 版本下拉,REQ-USR-004 后端规格已记)、`GET /api/usr/employees`(FE-04 员工下拉)、`GET /api/usr/permissions`(FE-04 权限组);否则前后端联调时对应下拉无真实数据。详见 § ⑦ 待对齐项。 | ||
| 194 | +4. **前后端联调验证**:当前 E2E 用 `page.route` 桩后端,上线前需起真实后端(端口 5172)+ Vite proxy 做端到端联调,验证登录 / 列表分页 / 单据增改 / 401 被动登出全链路。 | ||
| 195 | +5. **导出端点取舍**:FE-03「导出Excel」当前为前端实现(SheetJS);若后续后端补 `/export` 端点可切换为服务端导出(D5 已留切换口)。 | ||
| 196 | +6. **私有化部署**:按 CLAUDE.md「私有化部署」,前端构建产物(`vite build`)随后端打包部署,`baseURL=/api` 经反向代理指向后端。 | ||
| 197 | + | ||
| 198 | +--- | ||
| 199 | + | ||
| 200 | +## ⑫ 结论 | ||
| 201 | + | ||
| 202 | +- **前置闸**:test-gate **GREEN**(唯一 attempt r1,**无 flake**),vitest 193 + playwright 20 全绿,覆盖 4 个 FE 全量回归。 | ||
| 203 | +- **作用域**:实现改动全部落在 `frontend/`,无 `backend/` / `sql/` / `scripts/` 越界,无跨模块改动。 | ||
| 204 | +- **完成度**:FE-01~04 全部 approved(docs/08 § 三 `[x]`);FE-04 经 1 轮 request-changes 修复(edit 预填 B1)后复验通过。 | ||
| 205 | +- **偏离**:§ ⑧ 已逐 FE 列举全部偏离,**均为 spec 登记的有意决策**(静态 demo→真实路由/API、不杜撰端点的占位化、tokens 优先),**无未登记的非预期偏离**;实现组件与原型 4 个 `#screen-*` 主结构一一对应。 | ||
| 206 | +- **可进入 milestone 标记**:本报告 commit 后工作树干净,满足里程碑前置。 |
docs/superpowers/module-reports/frontend-phase-test-gate-r1.md
0 → 100644
| 1 | +# 前端阶段 硬测试闸 证据(frontend-phase-test-gate) | ||
| 2 | + | ||
| 3 | +- attempt: 1 | ||
| 4 | +- phase: frontend | ||
| 5 | +- 分支: frontend-phase | ||
| 6 | +- 命令: `npm run test:unit && npm run test:e2e`(来源 docs/04-技术规范.md § 零 命令清单:前端 unit=`vitest run`、e2e=`playwright test`) | ||
| 7 | +- 工作目录: `/Users/reporkey/Desktop/mvp/test6/frontend` | ||
| 8 | +- 执行方式: 派发独立(detached)子执行进程跑测试,主会话仅消费结构化结果,未在主会话直接跑测试。 | ||
| 9 | + | ||
| 10 | +## 结论 | ||
| 11 | + | ||
| 12 | +**GREEN** — 两个套件 exit_code 均为 0,failed = 0,覆盖全部 FE 回归(vitest + playwright)。 | ||
| 13 | + | ||
| 14 | +## 单元测试(vitest) | ||
| 15 | + | ||
| 16 | +- 命令: `npm run test:unit`(= `vitest run`) | ||
| 17 | +- 时间窗口: 2026-06-02 09:26:46 → 09:26:53 | ||
| 18 | +- exit_code: 0 | ||
| 19 | +- 结果: **Test Files 40 passed (40) / Tests 193 passed (193)** | ||
| 20 | +- 备注: 日志中 `window.getComputedStyle` not implemented 与 React Router v7 future flag 为 jsdom/antd 的非致命 stderr 警告,未导致任何用例失败。 | ||
| 21 | + | ||
| 22 | +## E2E 测试(playwright) | ||
| 23 | + | ||
| 24 | +- 命令: `npm run test:e2e`(= `playwright test`,chromium 项目,webServer 起 Vite dev server 5173,后端经 `page.route` 桩,不依赖真实后端) | ||
| 25 | +- 时间窗口: 2026-06-02 09:26:57 → 09:27:04 | ||
| 26 | +- exit_code: 0 | ||
| 27 | +- 结果: **20 passed (6.4s)** | ||
| 28 | +- 覆盖 spec: `login.spec.ts` / `shell.spec.ts` / `userlist.spec.ts` / `userdetail.spec.ts` | ||
| 29 | +- 备注: 日志中 `[vite] http proxy error ... ECONNREFUSED` 为 dev server 代理向未启动的真实后端转发产生,E2E 用例已用 `page.route` 拦截 `/api/**`,与用例断言无关,全部用例通过。 | ||
| 30 | + | ||
| 31 | +## 子会话结构化返回 | ||
| 32 | + | ||
| 33 | +```json | ||
| 34 | +{ | ||
| 35 | + "unit": { | ||
| 36 | + "command": "npm run test:unit", | ||
| 37 | + "exit_code": 0, | ||
| 38 | + "passed": 193, | ||
| 39 | + "failed": 0, | ||
| 40 | + "stdout_excerpt": "Test Files 40 passed (40)\nTests 193 passed (193)\nDuration 6.12s" | ||
| 41 | + }, | ||
| 42 | + "e2e": { | ||
| 43 | + "command": "npm run test:e2e", | ||
| 44 | + "exit_code": 0, | ||
| 45 | + "passed": 20, | ||
| 46 | + "failed": 0, | ||
| 47 | + "stdout_excerpt": "20 passed (6.4s)" | ||
| 48 | + } | ||
| 49 | +} | ||
| 50 | +``` |
docs/superpowers/plans/2026-06-01-FE-01.md
0 → 100644
| 1 | +# FE-01 登录页 — 任务级 TDD 计划(前端) | ||
| 2 | + | ||
| 3 | +> 阶段:前端(frontend)。作用域:`frontend/**`(页面 / 组件 / 路由 / store / api / 样式 / 测试 / 工程配置)。**禁止**写 `backend/**` / `sql/**` / `scripts/**`。 | ||
| 4 | +> 上游 SSoT:spec `docs/superpowers/specs/2026-06-01-FE-01.md`;需求卡片 `docs/01-需求清单/USR-用户管理/REQ-USR-004.md`;API 契约 `docs/05-API接口契约.md` § REQ-USR-004;原型 `prototype/erp.html`(`#screen-login` 布局/交互权威);技术规范 `docs/04-技术规范.md` § 零 / § 二;Design Tokens `src/styles/tokens.css`;配置 `config-vars.yaml`。 | ||
| 5 | +> 本计划告诉 TDD 执行者**做什么 / 文件边界 / 测试意图 / props 与 API 形状 / 完成判据**;具体代码由红-绿-提交循环产出,不在此 dump 整个组件 / 配置文件内容。 | ||
| 6 | +> **本 REQ 是仓库首个前端任务**:`frontend/` 目录尚不存在,FE-01 需先搭建最小前端工程骨架(package.json / vite / 测试栈 / Redux / Router / AntD / Axios 封装),再实现登录页。后续 FE-02~FE-04 复用此骨架。 | ||
| 7 | + | ||
| 8 | +--- | ||
| 9 | + | ||
| 10 | +## Goal(目标) | ||
| 11 | + | ||
| 12 | +在 `/login` 路由实现登录页 `LoginPage`,复刻原型 `#screen-login` 的三段式布局(品牌头 / 深蓝主视觉 + 右侧浮层登录卡 / 页脚版权)与交互语义,但表单校验、提交、版本下拉取数、错误反馈全部真实对接后端: | ||
| 13 | + | ||
| 14 | +- 页面挂载即调 `GET /api/usr/companies` 预加载「版本」下拉项(`companiesLoading` → `idle`/`empty`);取数失败给重试入口 + `message.error`。 | ||
| 15 | +- 用户填用户名 / 密码、选版本后点「登录」→ 前端必填校验通过 → 调 `POST /api/usr/login`(body `{ sUserName, password, companyId }`);提交中 `submitting`(按钮 loading、字段禁用、防重复提交)。 | ||
| 16 | +- 成功(`code=0`):拿 `token` + `user` 写入 Redux `authSlice.setCredentials`,token 持久化到 `localStorage`(键 `xly_erp_token`),`message.success("登录成功")`,`navigate('/', { replace:true })`。 | ||
| 17 | +- 失败:按错误码(`40001`/`40101`/`40302`/`42901`/网络异常)渲染对应中文文案;`40101`/`42901` 后清空密码框并聚焦,保留用户名与版本。 | ||
| 18 | +- 状态机覆盖 ≥5 态:`companiesLoading` / `idle` / `empty` / `submitting` / `error` / `success`(spec § 3)。 | ||
| 19 | +- 业务规则前端复刻 BR1~BR11(spec § 5),其中身份真伪 / 禁用 / 限流均由后端裁决,前端仅按返回码渲染。 | ||
| 20 | + | ||
| 21 | +## Architecture(架构 / 分层) | ||
| 22 | + | ||
| 23 | +遵循 `docs/04 § 2.1`,前端为仓库根 `frontend/` 子项目,包名 `xly-erp-web`(config-vars `frontend.pkg_name`)。本 REQ 新建工程骨架 + 登录页相关文件: | ||
| 24 | + | ||
| 25 | +``` | ||
| 26 | +frontend/ | ||
| 27 | +├── package.json # 【新增】name=xly-erp-web;scripts dev/build/lint/test:unit/test:e2e;依赖 react18 / antd5 / @reduxjs/toolkit / react-redux / react-router-dom@6 / axios / @ant-design/icons + devDeps vite / vitest / jsdom / @testing-library/react|jest-dom|user-event / @playwright/test / typescript / eslint | ||
| 28 | +├── tsconfig.json # 【新增】TS 配置(jsx react-jsx、strict) | ||
| 29 | +├── vite.config.ts # 【新增】React 插件 + server.port=5173 + server.proxy '/api'→http://localhost:5172(D2)+ vitest test 配置(environment jsdom、globals、setupFiles) | ||
| 30 | +├── playwright.config.ts # 【新增】e2e:testDir=tests/e2e,baseURL 取 dev server | ||
| 31 | +├── index.html # 【新增】挂载点 #root + 引入 main.tsx | ||
| 32 | +├── tests/ | ||
| 33 | +│ ├── setup.ts # 【新增】vitest setup:import '@testing-library/jest-dom';引入 tokens.css 供 jsdom(可选) | ||
| 34 | +│ ├── unit/ # 【新增】jsdom 组件 / store / api 单测(Vitest + RTL) | ||
| 35 | +│ └── e2e/ # 【新增】Playwright E2E | ||
| 36 | +├── src/ | ||
| 37 | +│ ├── main.tsx # 【新增】入口:Redux Provider + BrowserRouter + AntD ConfigProvider(theme.colorPrimary=var(--color-primary) 取值) + 引入 styles/tokens 与全局样式 | ||
| 38 | +│ ├── App.tsx # 【新增】挂载路由 <AppRouter/> | ||
| 39 | +│ ├── router/index.tsx # 【新增】React Router v6 路由表:/login → LoginPage;'/' 占位(FE-02 落地,本 REQ 仅需 /login 可达 + 成功后 navigate('/')) | ||
| 40 | +│ ├── store/store.ts # 【新增】configureStore({ reducer: { auth: authReducer } }) + 导出 RootState / AppDispatch | ||
| 41 | +│ ├── store/slices/authSlice.ts # 【新增】authSlice:state { token, user };reducer setCredentials / clearCredentials | ||
| 42 | +│ ├── api/request.ts # 【新增】Axios 实例:baseURL '/api';请求拦截器(已登录注入 Authorization: Bearer,登录端点放行);响应拦截器(拆 Result:code=0 取 data,非 0 抛 ApiError,网络异常兜底) | ||
| 43 | +│ ├── api/usrApi.ts # 【新增】login(payload) / fetchCompanies();调 request.ts,返回 data | ||
| 44 | +│ ├── pages/usr/Login/LoginPage.tsx # 【新增】登录页根组件(容器 + 状态机 + 提交逻辑) | ||
| 45 | +│ ├── pages/usr/Login/components/ # 【新增】LoginHeader / LoginHero / LoginCard / LoginForm 等区域子组件(按需拆分;纯展示子组件可内联) | ||
| 46 | +│ ├── pages/usr/Login/Login.module.css # 【新增】登录页 scoped 样式:语义色用 var(--color-*);主视觉深蓝渐变/网格为局部装饰(D7,不新增全局 token) | ||
| 47 | +│ └── styles/ # 【新增】全局样式入口,import 仓库根 ../../src/styles/tokens.css(Design Tokens SSoT,见 D9) | ||
| 48 | +└── (eslint 配置 .eslintrc.cjs 或 eslint.config.js 按 vite-react 模板) | ||
| 49 | +``` | ||
| 50 | + | ||
| 51 | +- **跨模块 / 跨阶段**:本 REQ 落点全在 `frontend/**`,不触 `backend/` / `sql/` / `scripts/`。新建的 `store` / `router` / `api/request.ts` / `main.tsx` / 工程配置属全前端共享骨架,FE-02~FE-04 复用(非 FE-01 私有),在《模块完成报告》留痕「FE-01 搭建前端工程骨架,后续 FE 复用」。 | ||
| 52 | +- **状态管理**(docs/04 § 2.2):全局登录态(token + user)进 Redux `authSlice`;页面内瞬时态(`companiesLoading` / `companies` 列表 / `empty` / `submitting`)就近用 `useState`,不塞全局。 | ||
| 53 | +- **请求封装**(docs/04 § 2.3):统一走 `api/request.ts`,页面只调 `usrApi.ts` 封装方法,不散用 axios。响应拦截器拆 `Result`,非 0 `code` 抛出携带 `code` 的错误供页面分流文案;网络/超时/5xx 兜底。 | ||
| 54 | +- **错误处理**(docs/04 § 2.4):表单校验错误就近 AntD 红字;提交失败按错误码 `message.error`;版本取数失败空态 + 重试。 | ||
| 55 | +- **Design Tokens**(docs/04 § 2.1 / spec § 7):语义色(按钮 / 文字 / 边框 / 错误 / 背景)只用 `var(--color-*)`,禁止硬编码 hex/rgba;AntD `colorPrimary` 经 `ConfigProvider` 对齐 `--color-primary`;主视觉深蓝渐变 / 网格透视为登录页局部装饰,scoped 保留,不挪用语义 token、不新增全局 token(D7)。 | ||
| 56 | + | ||
| 57 | +## Tech Stack(技术栈,源自 docs/04 § 零 + config-vars) | ||
| 58 | + | ||
| 59 | +- React 18.x / Ant Design 5.x / Redux Toolkit(最新稳定)/ React Router v6 / Vite(最新稳定)/ Axios(最新稳定)/ TypeScript;`@ant-design/icons`(`UserOutlined` / `LockOutlined`)。 | ||
| 60 | +- 测试:单测 Vitest(jsdom 环境)+ `@testing-library/react` / `@testing-library/jest-dom` / `@testing-library/user-event`;E2E Playwright(`@playwright/test`)。 | ||
| 61 | +- 命令(docs/04 § 零,package.json scripts 必须提供):build `npm run build`;lint `npm run lint`;unit `npm run test:unit`;e2e `npm run test:e2e`;另需 `dev`(Vite dev server,端口 5173)。 | ||
| 62 | +- 端口 / 转发:dev server 端口 `5173`(config-vars `frontend.dev_port`),Vite proxy `/api` → `http://localhost:5172`(config-vars `backend.http_port`,D2);不硬编码到组件,集中在 `vite.config.ts`。 | ||
| 63 | + | ||
| 64 | +## 合同级常量(跨 task 必须一致) | ||
| 65 | + | ||
| 66 | +- 路由 path:`/login`(登录页);登录成功跳转目标 `/`(D3,`{ replace: true }`)。 | ||
| 67 | +- API client 签名(`api/usrApi.ts`,跨 task 一致): | ||
| 68 | + - `login(payload: LoginPayload): Promise<LoginResult>`,其中 `LoginPayload = { sUserName: string; password: string; companyId: number }`;`LoginResult = { token: string; user: AuthUser }`;`AuthUser = { id: number; sUserName: string; sUserType: string; sLanguage: string }`(对齐 docs/05 § REQ-USR-004 响应 `Result<{ token, user:{ id, sUserName, sUserType, sLanguage } }>`)。 | ||
| 69 | + - `fetchCompanies(): Promise<CompanyOption[]>`,其中 `CompanyOption = { id: number; sCompanyName: string; sVersion: string | null }`(对齐 spec § 4 / D8;`sVersion` 可空)。 | ||
| 70 | +- HTTP 形状(`api/request.ts`): | ||
| 71 | + - `baseURL = '/api'`;登录端点 `POST /api/usr/login`(最终 path `/usr/login`)放行、**不带** `Authorization`;版本端点 `GET /api/usr/companies`(最终 path `/usr/companies`)放行。 | ||
| 72 | + - 后端统一响应 `Result<T> = { code: number; message: string; data: T }`(docs/04 § 1.4);响应拦截器:`code===0` 返回 `data`;`code!==0` 抛 `ApiError`(携带 `code:number` + `message:string`);无响应(网络/超时/5xx)抛 `ApiError`(`code` 置统一标识,如 `-1` 表网络异常)。 | ||
| 73 | + - 请求拦截器:从 `localStorage` 读 `xly_erp_token`,存在则注入 `Authorization: Bearer <token>`;登录 / 版本端点本就放行,无 token 时不注入(自然跳过)。 | ||
| 74 | +- token 持久化键名:`localStorage` key = `xly_erp_token`(D6)。集中常量(如 `src/api/request.ts` 或 `src/store/slices/authSlice.ts` 导出 `TOKEN_STORAGE_KEY`),跨 task 引用同一常量,不各处写字面量。 | ||
| 75 | +- 错误码 → 前端文案(对齐 spec § 4 / docs/05;登录失败分流,**严格沿用**,不得细化 `40101`): | ||
| 76 | + | code | 前端文案 | 展示方式 | | ||
| 77 | + |---|---|---| | ||
| 78 | + | `0` | 「登录成功」 | `message.success` 后跳转 | | ||
| 79 | + | `40001` | 「请填写用户名、密码并选择版本」 | `message.error`(兜底,正常前端校验已拦截) | | ||
| 80 | + | `40101` | 「用户名或密码错误」 | `message.error` + 清空密码框聚焦(不细化为「账号不存在」等枚举文案,BR6) | | ||
| 81 | + | `40302` | 「该账号已被禁用,请联系管理员」 | `message.error` | | ||
| 82 | + | `42901` | 「登录尝试过于频繁,请稍后再试」 | `message.error` + 清空密码框聚焦 | | ||
| 83 | + | 网络/超时/5xx | 「网络异常,请稍后重试」 | 响应拦截器兜底 `message.error` | | ||
| 84 | + > 文案集中在一处映射(如 `LoginPage` 内 `LOGIN_ERROR_MESSAGES: Record<number, string>` 或 `usrApi`/util 常量),按 `ApiError.code` 查表,未命中走网络异常兜底文案。 | ||
| 85 | +- 表单字段名(AntD `Form` `name`,提交时映射到 `LoginPayload`):`sUserName` / `password` / `companyId`;占位文案:用户名「请输入你的用户名」、密码「请输入你的密码」(沿用原型)。 | ||
| 86 | +- 版本下拉文案:`placeholder`「请选择版本」;加载中 `placeholder`「加载版本中…」;空态 `notFoundContent`「暂无可用版本」。 | ||
| 87 | +- 校验文案(AntD `rules.message`):用户名「请输入用户名」(BR1);密码「请输入密码」(BR2);版本「请选择版本」(BR4)。 | ||
| 88 | +- 版本下拉 label 规则(D8):返回项 `sVersion` 非空 → label = `` `${sCompanyName}(${sVersion})` ``(全角括号);`sVersion` 为空/null → label = `sCompanyName`;`value` 恒取 `id`(提交作 `companyId`);列表仅 1 项时默认选中该项(spec § 6.3)。 | ||
| 89 | + | ||
| 90 | +## 关键签名(首次出现处给出,跨 task 保持一致) | ||
| 91 | + | ||
| 92 | +- `authSlice`(`store/slices/authSlice.ts`): | ||
| 93 | + - `AuthState = { token: string | null; user: AuthUser | null }`,`initialState` 从 `localStorage` 读 `xly_erp_token` 初始化 `token`(user 初始 null)。 | ||
| 94 | + - `setCredentials(state, action: PayloadAction<{ token: string; user: AuthUser }>)`:写 token + user,并 `localStorage.setItem(TOKEN_STORAGE_KEY, token)`。 | ||
| 95 | + - `clearCredentials(state)`:清 token + user,并 `localStorage.removeItem(TOKEN_STORAGE_KEY)`。 | ||
| 96 | + - 导出 `authReducer`(default)、`setCredentials` / `clearCredentials` actions。 | ||
| 97 | + > 注:reducer 内写 localStorage 属副作用,为简洁集中放此(MVP 取舍);若 TDD 期偏好纯 reducer,可改由 `LoginPage` 在 dispatch 后单独持久化——二者择一,全项目统一(默认前者,登记 D6)。 | ||
| 98 | +- `request.ts`:导出 axios 实例 `request`(default)+ `TOKEN_STORAGE_KEY` 常量 + `ApiError`(含 `code: number`、`message: string`)。 | ||
| 99 | +- `usrApi.ts`:`login(payload)` / `fetchCompanies()`(签名见合同级常量)。 | ||
| 100 | +- `LoginPage`(`pages/usr/Login/LoginPage.tsx`,default export,无 props,路由组件);内部用 `useDispatch<AppDispatch>()` + `useNavigate()` + AntD `Form.useForm()` + `App.useApp()`/`message`(AntD 5 推荐 `App` 包裹用 `message` 实例,否则用静态 `message`——择一统一)。 | ||
| 101 | +- 区域子组件(若拆分,均在 `pages/usr/Login/components/`,纯展示,props 明确): | ||
| 102 | + - `LoginHeader`(无 props 或 `{ brandName?: string }`):渲染 Logo SVG + 「Antler ERP」+ 「欢迎登录EBC平台」。 | ||
| 103 | + - `LoginHero`(`{ children: ReactNode }` 或无):深蓝主视觉容器 + 标语文本,`children` 槽放登录卡。 | ||
| 104 | + - `LoginCard`(`{ children }`):右侧浮层卡片容器 + 标题「用户登录」。 | ||
| 105 | + - `LoginFooter`(无 props):版权 / 备案号文本。 | ||
| 106 | + - 表单逻辑(取数 / 提交 / 状态)集中在 `LoginPage`(或抽 `LoginForm` 子组件接收 `{ companies, companiesLoading, companiesError, submitting, onSubmit, onRetryCompanies, formRef }` props);**抽与不抽由 TDD 期决定**,但若抽 `LoginForm`,其 props 契约以此为准,跨 task 一致。 | ||
| 107 | + | ||
| 108 | +## 测试栈说明 | ||
| 109 | + | ||
| 110 | +- **jsdom 组件 / store / api 单测**(Vitest + RTL):默认 mock `api/usrApi` 或 axios(用 `vi.mock`)以隔离网络;断言渲染、交互、状态切换、dispatch、navigate、message 文案。覆盖 spec § 3 状态机与 § 5 BR1~BR11 中可在组件层验证者。 | ||
| 111 | +- **Playwright E2E**:覆盖关键用户旅程(页面可达 + 校验拦截 + 成功跳转),通过拦截/桩 `**/api/usr/companies` 与 `**/api/usr/login` 路由响应(`page.route`)模拟后端,不依赖真实后端起服。 | ||
| 112 | + | ||
| 113 | +--- | ||
| 114 | + | ||
| 115 | +## 任务列表(每个 task = red → green → 子会话验证 → commit) | ||
| 116 | + | ||
| 117 | +> 硬护栏:以下每个 `impl_file` / `test_file` 均以 `frontend/` 开头;无任何 `backend/` / `sql/` / `scripts/` 落点。 | ||
| 118 | +> 提交信息格式:`<type>(<scope>): <subject> REQ-USR-004`(FE-01 关联 REQ-USR-004;scope 用 `fe-login` 或 `usr`)。 | ||
| 119 | + | ||
| 120 | +### T0 — 前端工程骨架可启动 + 测试栈可运行(chore,先建地基) | ||
| 121 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/smoke.test.tsx::renders a trivial component`——一个最小冒烟用例(渲染 `<div>ok</div>` 或一个空 `App`),用于驱动 Vitest + jsdom + RTL 配置就绪;初始因无工程 / 无配置而失败。 | ||
| 122 | +- [ ] **2. 实现最小代码**:`frontend/package.json` + `frontend/tsconfig.json` + `frontend/vite.config.ts`(含 vitest test 配置 environment=jsdom / globals=true / setupFiles=tests/setup.ts、server.port=5173、proxy `/api`→`http://localhost:5172`)+ `frontend/tests/setup.ts`(import `@testing-library/jest-dom`)+ `frontend/index.html` + `frontend/src/main.tsx`(暂最小,可仅挂载占位)+ `frontend/eslint` 配置。安装依赖(`npm i`)。scripts 提供 `dev` / `build` / `lint` / `test:unit`(`vitest run`)/ `test:e2e`(`playwright test`)。 | ||
| 123 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit` 冒烟用例通过;`npm run lint` 与 `npm run build` 可执行(build 至少不因配置错误失败)。 | ||
| 124 | +- [ ] **4. commit**:`chore(fe-login): 初始化前端工程骨架与测试栈 REQ-USR-004` | ||
| 125 | + | ||
| 126 | +### T1 — Axios 请求封装拆 Result + 拦截器(jsdom 单测) | ||
| 127 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/request.test.ts`: | ||
| 128 | + - `::baseURL is /api`——实例 `baseURL === '/api'`。 | ||
| 129 | + - `::unwraps data when code is 0`——mock 适配器返回 `{ code:0, message:'success', data:{ foo:1 } }`,`request.get(...)` resolve 为 `{ foo:1 }`(拆 data)。 | ||
| 130 | + - `::throws ApiError carrying business code when code is non-zero`——返回 `{ code:40101, message:'认证失败', data:null }`,调用 reject 为 `ApiError` 且 `err.code===40101`。 | ||
| 131 | + - `::throws network ApiError on no-response error`——模拟无响应(网络异常),reject 为 `ApiError`,`code` 为网络异常标识(如 `-1`)。 | ||
| 132 | + - `::injects Authorization header when token present`——`localStorage` 预置 `xly_erp_token`,请求拦截器把 `Authorization: 'Bearer <token>'` 加到 config.headers;无 token 时不加。 | ||
| 133 | +- [ ] **2. 实现最小代码**:`frontend/src/api/request.ts`——导出 axios 实例(`baseURL:'/api'`)、`TOKEN_STORAGE_KEY='xly_erp_token'`、`ApiError`;请求拦截器注入 token,响应拦截器拆 `Result` / 抛 `ApiError` / 网络兜底。 | ||
| 134 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- request` | ||
| 135 | +- [ ] **4. commit**:`feat(fe-login): Axios 实例与 Result 拆包/错误拦截 REQ-USR-004` | ||
| 136 | + | ||
| 137 | +### T2 — usrApi.login / fetchCompanies(jsdom 单测) | ||
| 138 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/usrApi.test.ts`(`vi.mock('../../src/api/request')` 桩实例): | ||
| 139 | + - `::login posts to /usr/login with sUserName/password/companyId`——断言 `request.post` 收到 path `/usr/login` 与 body `{ sUserName, password, companyId }`,返回值透传为 `{ token, user }`。 | ||
| 140 | + - `::fetchCompanies gets /usr/companies and returns list`——断言 `request.get('/usr/companies')`,返回 `CompanyOption[]`。 | ||
| 141 | +- [ ] **2. 实现最小代码**:`frontend/src/api/usrApi.ts`——`login(payload)` / `fetchCompanies()`(签名见合同级常量)。 | ||
| 142 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- usrApi` | ||
| 143 | +- [ ] **4. commit**:`feat(fe-login): usrApi 登录与版本下拉取数封装 REQ-USR-004` | ||
| 144 | + | ||
| 145 | +### T3 — authSlice setCredentials / clearCredentials + token 持久化(jsdom 单测) | ||
| 146 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/authSlice.test.ts`: | ||
| 147 | + - `::setCredentials stores token and user and persists token`——dispatch `setCredentials({ token:'t', user:{...} })`,state.token/user 更新,且 `localStorage.getItem('xly_erp_token')==='t'`。 | ||
| 148 | + - `::clearCredentials clears state and removes persisted token`——清空 state 且 `localStorage` 该键被移除。 | ||
| 149 | + - `::initialState reads persisted token`——预置 `localStorage` token 后初始化 reducer,`token` 为该值。 | ||
| 150 | +- [ ] **2. 实现最小代码**:`frontend/src/store/slices/authSlice.ts`(签名见关键签名)+ `frontend/src/store/store.ts`(`configureStore`,挂 `auth` reducer,导出 `RootState` / `AppDispatch`)。 | ||
| 151 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- authSlice` | ||
| 152 | +- [ ] **4. commit**:`feat(fe-login): authSlice 登录态与 token 持久化 REQ-USR-004` | ||
| 153 | + | ||
| 154 | +### T4 — 登录页布局与区域结构渲染(jsdom 组件测) | ||
| 155 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/LoginPage.layout.test.tsx`(用 Redux Provider + MemoryRouter 包裹渲染;mock `usrApi.fetchCompanies` resolve 空列表避免 act 警告): | ||
| 156 | + - `::renders brand header / hero slogan / footer`——存在品牌名「Antler ERP」、副标题「欢迎登录EBC平台」、主视觉中文「企业业务能力平台」与「ERP」、页脚版权文本(含备案号)。 | ||
| 157 | + - `::renders login card title 用户登录`——卡片标题文案存在。 | ||
| 158 | + - `::renders username/password/version fields and submit button 登 录`——用户名 `Input`(占位「请输入你的用户名」)、密码 `Input.Password`(占位「请输入你的密码」、`type` 掩码,BR3)、版本 `Select`、提交按钮文案「登 录」。 | ||
| 159 | +- [ ] **2. 实现最小代码**:`frontend/src/pages/usr/Login/LoginPage.tsx`(+ 按需 `components/` 区域子组件、`Login.module.css`);`frontend/src/router/index.tsx` 挂 `/login`;`frontend/src/App.tsx` / `frontend/src/main.tsx` 接入 Provider + Router + `ConfigProvider`(colorPrimary 对齐 `--color-primary`);`frontend/src/styles/` 入口 import 仓库根 tokens.css。语义色用 `var(--color-*)`,主视觉装饰 scoped。 | ||
| 160 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- LoginPage.layout` | ||
| 161 | +- [ ] **4. commit**:`feat(fe-login): 登录页三段式布局与区域结构 REQ-USR-004` | ||
| 162 | + | ||
| 163 | +### T5 — 版本下拉预加载状态机:loading / idle / empty / 取数失败重试(jsdom 组件测) | ||
| 164 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/LoginPage.companies.test.tsx`(`vi.mock` `usrApi`): | ||
| 165 | + - `::shows loading placeholder and disabled select while fetching`——`fetchCompanies` pending 时版本 `Select` 处 loading/禁用、`placeholder` 含「加载版本中」(`companiesLoading` 态)。 | ||
| 166 | + - `::renders options with label rule on resolve (idle)`——resolve `[{id:1,sCompanyName:'甲公司',sVersion:'标准版'},{id:2,sCompanyName:'乙公司',sVersion:null}]`,下拉项 label 分别为「甲公司(标准版)」「乙公司」(D8);展开可见。 | ||
| 167 | + - `::auto-selects when single option`——resolve 仅 1 项时该项默认选中(spec § 6.3)。 | ||
| 168 | + - `::empty state when companies is empty`——resolve `[]`,`Select` 空态文案「暂无可用版本」+ 轻量提示「未获取到可登录版本,请联系管理员」(`empty` 态)。 | ||
| 169 | + - `::shows error with retry when fetch fails`——`fetchCompanies` reject,出现「版本加载失败」+ 重试入口;点重试再次调用 `fetchCompanies`(BR5)。 | ||
| 170 | +- [ ] **2. 实现最小代码**:在 `LoginPage` 中加挂载即取数(`useEffect`)、`companies` / `companiesLoading` / `companiesError` 本地态、label 映射(D8)、单项自动选中、空态与重试逻辑。 | ||
| 171 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- LoginPage.companies` | ||
| 172 | +- [ ] **4. commit**:`feat(fe-login): 版本下拉预加载/空态/重试 REQ-USR-004` | ||
| 173 | + | ||
| 174 | +### T6 — 表单必填校验拦截提交(BR1/BR2/BR4)(jsdom 组件测) | ||
| 175 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/LoginPage.validation.test.tsx`(mock `usrApi`,companies 预置非空): | ||
| 176 | + - `::blocks submit and shows required messages when empty`——直接点「登录」,出现「请输入用户名」「请输入密码」「请选择版本」三条校验红字,且 `usrApi.login` **未被调用**(BR1/BR2/BR4 + spec § 5)。 | ||
| 177 | + - `::submits with payload when all filled`——填全三项后点登录,`usrApi.login` 收到 `{ sUserName, password, companyId }`(companyId 为所选项 id)。 | ||
| 178 | +- [ ] **2. 实现最小代码**:在 `LoginForm`/`LoginPage` 配置 AntD `Form` `rules`(三字段 required + 对应 message)、`onFinish` 组装 `LoginPayload` 调 `usrApi.login`。 | ||
| 179 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- LoginPage.validation` | ||
| 180 | +- [ ] **4. commit**:`feat(fe-login): 登录表单必填校验与提交装配 REQ-USR-004` | ||
| 181 | + | ||
| 182 | +### T7 — 提交中态防重复提交(submitting,BR10)(jsdom 组件测) | ||
| 183 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/LoginPage.submitting.test.tsx`: | ||
| 184 | + - `::button loading and fields disabled while submitting`——`usrApi.login` 返回 pending Promise,提交后按钮 `loading` 且三字段禁用(`submitting` 态)。 | ||
| 185 | + - `::ignores duplicate submit while pending`——pending 期间再次点击 / 回车,`usrApi.login` 仅被调用 1 次(防重复提交,BR10)。 | ||
| 186 | +- [ ] **2. 实现最小代码**:`LoginPage` 加 `submitting` 本地态(提交前置 true、settle 后置 false),按钮 `loading={submitting}`、字段 `disabled`,提交时若已 `submitting` 直接 return。 | ||
| 187 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- LoginPage.submitting` | ||
| 188 | +- [ ] **4. commit**:`feat(fe-login): 提交中态与防重复提交 REQ-USR-004` | ||
| 189 | + | ||
| 190 | +### T8 — 登录成功:写 authSlice + 持久化 + 跳转 /(success,BR9)(jsdom 组件测) | ||
| 191 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/LoginPage.success.test.tsx`(真实 store + `useNavigate` spy / MemoryRouter 记录跳转;mock `usrApi.login` resolve `{ token:'tk', user:{ id:1, sUserName:'admin', sUserType:'超级管理员', sLanguage:'中文' } }`;mock `message`): | ||
| 192 | + - `::dispatches setCredentials and persists token on success`——成功后 store `auth.token==='tk'`、`auth.user` 写入,`localStorage.xly_erp_token==='tk'`。 | ||
| 193 | + - `::shows success message and navigates to '/' with replace`——`message.success('登录成功')` 被调,`navigate('/', { replace:true })` 被调(D3)。 | ||
| 194 | +- [ ] **2. 实现最小代码**:`onFinish` 成功分支 dispatch `setCredentials({ token, user })`、`message.success('登录成功')`、`navigate('/', { replace:true })`。 | ||
| 195 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- LoginPage.success` | ||
| 196 | +- [ ] **4. commit**:`feat(fe-login): 登录成功落地登录态与跳转 REQ-USR-004` | ||
| 197 | + | ||
| 198 | +### T9 — 登录失败错误码分流文案 + 失败后清空聚焦(error,BR6/BR7/BR8 + D5)(jsdom 组件测) | ||
| 199 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/LoginPage.error.test.tsx`(mock `usrApi.login` reject `ApiError`;mock `message`;表单预填合法值): | ||
| 200 | + - `::40101 shows 用户名或密码错误 and clears+focuses password`——reject `code:40101`,`message.error('用户名或密码错误')`,密码框被清空且获焦(BR6 + D5)。 | ||
| 201 | + - `::40302 shows 该账号已被禁用,请联系管理员`——`message.error` 文案匹配(BR7)。 | ||
| 202 | + - `::42901 shows 登录尝试过于频繁,请稍后再试 and clears password`——文案匹配 + 密码清空(BR8 + D5)。 | ||
| 203 | + - `::40001 shows 请填写用户名、密码并选择版本`——兜底文案(spec § 4)。 | ||
| 204 | + - `::network error shows 网络异常,请稍后重试`——reject 网络异常 `ApiError`(如 `code:-1`),兜底文案。 | ||
| 205 | + - `::button recovers clickable and username/version preserved after failure`——失败后按钮恢复可点、用户名与版本保留(spec § 3 error 态 / D5)。 | ||
| 206 | +- [ ] **2. 实现最小代码**:`onFinish` 失败分支按 `err.code` 查 `LOGIN_ERROR_MESSAGES` 表(未命中走网络异常文案)`message.error(...)`;`40101`/`42901` 后 `form.setFieldValue('password', '')` 并聚焦密码框;恢复 `submitting=false`。 | ||
| 207 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- LoginPage.error` | ||
| 208 | +- [ ] **4. commit**:`feat(fe-login): 登录失败错误码分流文案与失败后处理 REQ-USR-004` | ||
| 209 | + | ||
| 210 | +### T10 — E2E 登录关键旅程(Playwright) | ||
| 211 | +- [ ] **1. 写失败测试**:`frontend/tests/e2e/login.spec.ts`(`page.route` 桩 `**/api/usr/companies` 返回 `{code:0,...,data:[...]}`、`**/api/usr/login` 按用例返回成功 / `40101`): | ||
| 212 | + - `::loads /login and shows version options`——访问 `/login`,版本下拉渲染桩返回项。 | ||
| 213 | + - `::blocks submit with validation when empty`——空提交看到必填校验提示,未发起 login 请求。 | ||
| 214 | + - `::successful login navigates away from /login`——填全 + 桩成功响应 → URL 离开 `/login`(到 `/`)、可见「登录成功」提示。 | ||
| 215 | + - `::failed login stays on /login with error`——桩 `40101` → 停留 `/login`、可见「用户名或密码错误」。 | ||
| 216 | +- [ ] **2. 实现最小代码**:补齐 `playwright.config.ts`(`webServer` 起 `npm run dev`、`baseURL` 指向 dev server)及任何为可测性需要的最小 `data-testid`(仅在 RTL 无法稳定定位时添加)。 | ||
| 217 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:e2e -- login`(首次需 `npx playwright install` 装浏览器)。 | ||
| 218 | +- [ ] **4. commit**:`test(fe-login): 登录页 E2E 关键旅程 REQ-USR-004` | ||
| 219 | + | ||
| 220 | +### T11 — 全量门禁回归 + 收尾(chore) | ||
| 221 | +- [ ] **1. 写失败测试**:无新增测试;本任务跑全量验证。 | ||
| 222 | +- [ ] **2. 实现最小代码**:修复 lint / build / 类型问题(如有);确认语义色全部 `var(--color-*)`、无硬编码 hex/rgba(主视觉装饰 scoped 除外,D7)。 | ||
| 223 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run lint && npm run build && npm run test:unit && npm run test:e2e` 全绿。 | ||
| 224 | +- [ ] **4. commit**:`chore(fe-login): FE-01 门禁回归通过 REQ-USR-004` | ||
| 225 | + | ||
| 226 | +--- | ||
| 227 | + | ||
| 228 | +## 完成判据(Definition of Done) | ||
| 229 | + | ||
| 230 | +1. `/login` 路由可达,渲染复刻原型 `#screen-login` 的品牌头 / 深蓝主视觉 + 右侧浮层登录卡 / 页脚三段式(spec § 2 / § 6.1-6.2)。 | ||
| 231 | +2. 状态机 ≥5 态全部覆盖并有测试固化:`companiesLoading` / `idle` / `empty` / `submitting` / `error` / `success`(spec § 3)。 | ||
| 232 | +3. 业务规则 BR1~BR11 在组件层 / E2E 有对应断言(身份真伪 / 禁用 / 限流由后端裁决,前端按返回码渲染,不复制后端逻辑)(spec § 5)。 | ||
| 233 | +4. 消费 `GET /api/usr/companies`(预加载、空态、重试)与 `POST /api/usr/login`(成功 / `40001` / `40101` / `40302` / `42901` / 网络异常)按错误码表分流文案(spec § 4),文案逐字一致。 | ||
| 234 | +5. 成功后写 Redux `authSlice` + 持久化 `localStorage[xly_erp_token]` + `navigate('/', { replace:true })`(spec § 6.6 / D3 / D6)。 | ||
| 235 | +6. 统一走 `api/request.ts` + `api/usrApi.ts`,不在页面散用 axios(docs/04 § 2.3)。 | ||
| 236 | +7. 语义色只用 `var(--color-*)`,AntD `colorPrimary` 对齐 `--color-primary`,主视觉装饰 scoped 不新增全局 token(spec § 7 / D7)。 | ||
| 237 | +8. 全部落点在 `frontend/**`,无 `backend/` / `sql/` / `scripts/` 改动。 | ||
| 238 | +9. 门禁全绿:`npm run lint` / `npm run build` / `npm run test:unit` / `npm run test:e2e`(docs/04 § 零)。 | ||
| 239 | + | ||
| 240 | +## 自审记录 | ||
| 241 | + | ||
| 242 | +- **占位符扫描**:本计划无 `【人工填写:】` / `TBD` / `TODO` 占位。 | ||
| 243 | +- **spec coverage**:spec § 2 组件树→T4;§ 3 状态机→T5(loading/idle/empty)/T7(submitting)/T8(success)/T9(error);§ 4 端点与错误码→T1/T2/T8/T9/T10;§ 5 BR1-BR11→T6(BR1/2/4)/T4(BR3)/T5(BR5)/T9(BR6/7/8)/T8(BR9)/T7(BR10)/T2+T6(BR11,前端原样提交不处理密码);§ 6 交互→T4/T5/T8/T9;§ 7 tokens→T4/T11;§ 8 decisions D1-D8 已在合同级常量 / 架构中落实,新增 D9(本计划层)见下。 | ||
| 244 | +- **本计划新增决策 D9(tokens.css 复用路径)**:Design Tokens SSoT 为仓库根 `src/styles/tokens.css`(docs/04 § 2.1,由 skeleton-gen 生成),前端工程在 `frontend/` 子目录内。本计划选择由 `frontend/src/styles/` 入口以相对路径 `import '../../../src/styles/tokens.css'`(或经 Vite alias)复用该唯一文件,**不在 `frontend/` 内拷贝一份**,避免双 SSoT 漂移;依据 docs/04 § 2.1「色值单一来源在仓库根 `src/styles/tokens.css`,前端在 main.tsx/全局样式中引入」。置信度 high。该决策亦登记进返回的 decisions[]。 | ||
| 245 | +- **类型一致性**:`LoginPayload` / `LoginResult` / `AuthUser` / `CompanyOption` / `ApiError` / `TOKEN_STORAGE_KEY` / 错误码文案表 / 路由 path / API path 跨 T1-T10 一致;与 docs/05 § REQ-USR-004 响应形状(`token` + `user{ id, sUserName, sUserType, sLanguage }`)及 companies `{ id, sCompanyName, sVersion }` 对齐。 |
docs/superpowers/plans/2026-06-01-FE-02.md
0 → 100644
| 1 | +# FE-02 主页与导航框架 — 任务级 TDD 计划(前端) | ||
| 2 | + | ||
| 3 | +> 阶段:前端(frontend)。作用域:`frontend/**`(页面 / 布局 / 路由 / store / api / 样式 / 测试)。**禁止**写 `backend/**` / `sql/**` / `scripts/**`。 | ||
| 4 | +> 上游 SSoT:spec `docs/superpowers/specs/2026-06-01-FE-02.md`;原型 `prototype/erp.html`(`#topbar` / `#nav-overlay` / `#screen-main` 布局/交互权威,含内联 `kpiRows` / `navSide` / `navCols` demo 数据);技术规范 `docs/04-技术规范.md` § 零 / § 二;Design Tokens 仓库根 `src/styles/tokens.css`;登录态来源 FE-01(`authSlice` + `request.ts` + token 持久化键 `xly_erp_token`)。 | ||
| 5 | +> 本计划告诉 TDD 执行者**做什么 / 文件边界 / 测试意图 / props 与配置形状 / 完成判据**;具体代码由红-绿-提交循环产出,不在此 dump 整组件 / 配置文件内容。 | ||
| 6 | +> **本 FE 是登录后落地页 + 应用外壳(壳层)**,复用 FE-01 已搭好的工程骨架(package.json / vite / 测试栈 / Redux / Router / `api/request.ts` / `authSlice` / tokens 引入)。它**不新增任何后端取数**(spec § 4 / D1):主页 KPI 看板、角色/流程树、导航分组均为前端静态配置(复刻原型 demo);当前用户身份复用 `authSlice.user`。 | ||
| 7 | + | ||
| 8 | +--- | ||
| 9 | + | ||
| 10 | +## Goal(目标) | ||
| 11 | + | ||
| 12 | +把 FE-01 留下的 `/` 占位(`HomePlaceholder`)替换为真实应用外壳与主页落地页,复刻原型 `#topbar` + `#nav-overlay` + `#screen-main` 的布局与交互语义,登录态守卫与导航编排真实生效,KPI/导航数据为前端静态 demo: | ||
| 13 | + | ||
| 14 | +- **路由壳与守卫**:`<RequireAuth>` 包裹布局路由 `<AppLayout>`;index `/` → `<HomePage>`,子路由 `/usr/users`(FE-03 容器)、`/usr/users/new` 与 `/usr/users/:id`(FE-04 容器)。未登录进受保护路由 → `<Navigate to="/login" replace state={{from}}/>`(BR1);token 已存在但 `user` 未就绪 → `Spin` 占位(`authResolving`)。已登录访问 `/login` → 回主页(BR2,FE-01 § 6.7 已指明此守卫归 FE-02)。 | ||
| 15 | +- **顶栏 TopBar**:品牌 Logo(鹿角 SVG,点击回 `/`)、「全部导航」汉堡按钮(切 overlay,`navOverlayOpen` 时高亮)、固定「主页」标签(不可关)+ 动态业务标签栈、右侧搜索/通知图标(占位)、当前用户区 `sUserName(sUserType)` + 下拉「退出登录」、更多「⋯」占位。 | ||
| 16 | +- **标签栈**(BR4/BR5/BR6,复刻 `tabsOpen`/`openTab`/`.close`):本地受控态。「主页」恒在最左不可关;打开 FE-03 → 追加「用户列表」;打开 FE-04 → 先确保「用户列表」存在再追加「用户信息单据」;关「用户列表」联动关「用户信息单据」并回主页;关「用户信息单据」回「用户列表」。标签激活与当前路由同步(点标签 = `navigate`)。 | ||
| 17 | +- **全部导航总览 NavOverlay**(BR7,复刻 `#nav-overlay`):覆盖内容区深色浮层,左列 20 个一级模块(`navSide`,「系统设置」默认 active)、右侧 7 列分组(`navCols`)。仅「用户列表」有真实路由(点击 → 关 overlay + `navigate('/usr/users')`);其余占位项点击关 overlay + `message.info('功能开发中')`;「用户列表」「系统功能模块设置」带 ★。点遮罩 / Esc 关闭。 | ||
| 18 | +- **主页 HomePage**(BR11,复刻 `#screen-main`):`KpiHeadBar`(标题 + 今日未处理 37428 红 / 未清总数 56433 蓝 + AI 助手占位按钮)+ `DashboardThreeCol`(左 280px 角色/流程树 + 右 KPI 合并网格)+ `CommonOps`(常用操作:用户列表 → `/usr/users`;系统功能模块设置 → 占位)+ `AppFooter` 页脚。数据全部来自 `dashboardData.ts` 静态 demo(D1/D2)。 | ||
| 19 | +- **退出登录**(BR9):下拉「退出登录」→ `dispatch(clearCredentials())`(自动清 localStorage token)→ `message.success('已退出登录')` → `navigate('/login', { replace:true })`。 | ||
| 20 | +- **被动 401**(BR10):`request.ts` 响应拦截器对 401 触发统一登出回调(清登录态 + `message.warning('登录已失效,请重新登录')` + 跳 `/login`),由外壳在挂载时注册回调(拦截器内无法用 React hooks,见 D11)。 | ||
| 21 | +- **状态机 ≥5 态**(spec § 3):`authResolving` / `unauthenticated` / `ready` / `navOverlayOpen` / `tabOpen` / `empty` / `error` 均有测试固化。 | ||
| 22 | +- **语义色只用 `var(--color-*)`**;顶栏 / 导航 overlay 深色底为外壳局部装饰,scoped 保留,不新增全局 token、不挪用语义 token(spec § 7 / D9)。 | ||
| 23 | + | ||
| 24 | +## Architecture(架构 / 分层) | ||
| 25 | + | ||
| 26 | +遵循 `docs/04 § 2.1`,落点全在 `frontend/**`。**新增/改动**文件: | ||
| 27 | + | ||
| 28 | +``` | ||
| 29 | +frontend/ | ||
| 30 | +├── src/ | ||
| 31 | +│ ├── router/index.tsx # 【改】替换 HomePlaceholder:嵌套路由 RequireAuth>AppLayout>{index HomePage, /usr/users, /usr/users/new, /usr/users/:id} + /login 包 RedirectIfAuthed + ErrorBoundary + 未匹配重定向 / | ||
| 32 | +│ ├── router/RequireAuth.tsx # 【新增】受保护区守卫:读 authSlice.token/user → ready / authResolving(Spin) / unauthenticated(Navigate to /login,state.from) | ||
| 33 | +│ ├── router/RedirectIfAuthed.tsx # 【新增】/login 守卫:已有有效登录态 → Navigate 回 from 或 /(BR2) | ||
| 34 | +│ ├── router/AppErrorBoundary.tsx # 【新增】路由级 ErrorBoundary:子路由渲染抛错兜底「页面出错,请刷新或返回主页」+ 返回主页入口(spec § 3 error / D7) | ||
| 35 | +│ ├── layouts/AppLayout/AppLayout.tsx # 【新增】应用外壳:TopBar + NavOverlay + <Outlet/> + AppFooter;持有标签栈 / overlay 开关本地态(D3) | ||
| 36 | +│ ├── layouts/AppLayout/TopBar.tsx # 【新增】顶栏:Logo + 导航按钮 + 标签条 + 右侧用户区(接收标签栈/overlay/用户 props) | ||
| 37 | +│ ├── layouts/AppLayout/NavOverlay.tsx # 【新增】全部导航总览浮层(受控 open,左 navSide 列 + 右 navCols 网格;onNavigate / onClose) | ||
| 38 | +│ ├── layouts/AppLayout/CurrentUserMenu.tsx # 【新增】当前用户 Dropdown(展示 sUserName(sUserType) + 退出登录) | ||
| 39 | +│ ├── layouts/AppLayout/AppFooter.tsx # 【新增】页脚版权/经营范围/备案号(复刻原型 footer.foot) | ||
| 40 | +│ ├── layouts/AppLayout/useTabStack.ts # 【新增】标签栈 hook(openTab/closeTab/activeKey 逻辑,BR4/5/6,复刻 tabsOpen/openTab/.close) | ||
| 41 | +│ ├── layouts/AppLayout/navConfig.ts # 【新增】静态导航配置:NAV_SIDE(20 项)+ NAV_COLS(7 列,复刻 navSide/navCols;标注 routePath / star) | ||
| 42 | +│ ├── layouts/AppLayout/AppLayout.module.css # 【新增】外壳 scoped 样式:语义色用 var(--color-*);顶栏/overlay 深色底为局部装饰(D9) | ||
| 43 | +│ ├── pages/home/HomePage/HomePage.tsx # 【新增】主页落地页根(组合 KpiHeadBar + DashboardThreeCol + CommonOps) | ||
| 44 | +│ ├── pages/home/HomePage/KpiHeadBar.tsx # 【新增】KPI 头条(标题 + 今日未处理/未清总数统计 + AI 占位按钮) | ||
| 45 | +│ ├── pages/home/HomePage/RoleProcessTree.tsx # 【新增】左侧角色/流程树(按角色/按流程分组 + 计数;点击高亮,不取数) | ||
| 46 | +│ ├── pages/home/HomePage/KpiBoard.tsx # 【新增】KPI 合并网格(导航类型/角色/子流程列跨行合并;空数据 Empty,BR11/D5) | ||
| 47 | +│ ├── pages/home/HomePage/CommonOps.tsx # 【新增】常用操作卡(用户列表 → 路由;系统功能模块设置 → 占位) | ||
| 48 | +│ ├── pages/home/HomePage/dashboardData.ts # 【新增】静态 demo 数据:KPI_STATS / ROLE_GROUPS / PROCESS_GROUPS / KPI_ROWS(复刻原型 kpiRows,D1/D2) | ||
| 49 | +│ └── pages/home/HomePage/HomePage.module.css # 【新增】主页 scoped 样式(语义色用 var(--color-*);网格线/底色经 token) | ||
| 50 | +├── src/api/request.ts # 【改】响应拦截器加 401 处理:触发已注册的 onUnauthorized 回调(D11,集中常量 HTTP_UNAUTHORIZED=401) | ||
| 51 | +└── tests/ | ||
| 52 | + ├── unit/RequireAuth.test.tsx # 【新增】守卫态:ready / authResolving / unauthenticated | ||
| 53 | + ├── unit/RedirectIfAuthed.test.tsx # 【新增】已登录访问 /login 回主页(BR2) | ||
| 54 | + ├── unit/useTabStack.test.ts(x) # 【新增】标签栈 BR4/5/6 联动 | ||
| 55 | + ├── unit/AppLayout.topbar.test.tsx # 【新增】顶栏结构 + 当前用户文案 + 退出登录(BR3/BR9) | ||
| 56 | + ├── unit/NavOverlay.test.tsx # 【新增】overlay 开关 + 分组渲染 + 路由项/占位项点击(BR7/BR8) | ||
| 57 | + ├── unit/AppErrorBoundary.test.tsx # 【新增】子组件抛错兜底 | ||
| 58 | + ├── unit/HomePage.test.tsx # 【新增】主页区域结构 + 统计文案 + 常用操作跳转(BR8/BR11) | ||
| 59 | + ├── unit/KpiBoard.test.tsx # 【新增】KPI 网格表头/行渲染 + 空数据 Empty(BR11/empty 态) | ||
| 60 | + ├── unit/request.unauthorized.test.ts # 【新增】401 触发 onUnauthorized 回调(BR10) | ||
| 61 | + ├── unit/renderShell.tsx # 【新增】外壳/路由测试共享渲染工具(Provider + 真实 store + MemoryRouter + AntD App) | ||
| 62 | + └── e2e/shell.spec.ts # 【新增】E2E 关键旅程:登录后落地主页 / 导航 overlay / 打开关闭用户列表标签 / 退出登录 | ||
| 63 | +``` | ||
| 64 | + | ||
| 65 | +- **跨阶段/跨模块**:本 FE 落点全在 `frontend/**`,不触 `backend/` / `sql/` / `scripts/`。改动 `src/router/index.tsx` 与 `src/api/request.ts` 属 FE-01 搭建的**全前端共享骨架**(非 FE-02 私有),在《模块完成报告》留痕「FE-02 将 `/` 占位替换为应用外壳 + 受保护路由守卫;为 BR10 给 `request.ts` 增 401 统一登出回调;改动属共享骨架,FE-03/FE-04 复用」。 | ||
| 66 | +- **状态管理**(docs/04 § 2.2 / D3):标签栈(已打开集合 + activeKey)与 overlay 开关为外壳局部 UI 态,用 `AppLayout` 内 `useState` / `useTabStack` 本地管理,不进 Redux;登录态(token/user)复用 Redux `authSlice`。 | ||
| 67 | +- **请求封装 / 错误处理**(docs/04 § 2.3/2.4):本壳不新增业务取数(D1);仅扩展 `request.ts` 401 统一处理(D11),与守卫 `unauthenticated` 态协同。 | ||
| 68 | +- **Design Tokens**(docs/04 § 2.1 / spec § 7):语义色(主操作/文字/边框/错误/成功/表头/行)只用 `var(--color-*)`;顶栏 `#1f1f23`、overlay `#2b3137` 等品牌深色底为外壳局部装饰,scoped 在 `*.module.css`,不新增全局 token、不挪用语义 token(D9,与 FE-01 § 7 D7 一致)。 | ||
| 69 | + | ||
| 70 | +## Tech Stack(技术栈,源自 docs/04 § 零 + FE-01 骨架) | ||
| 71 | + | ||
| 72 | +- React 18 / Ant Design 5(`Tabs` / `Dropdown` / `Drawer`或受控 div / `Tree`或列表 / `Table`或 CSS Grid / `Empty` / `Spin` / `message`)/ Redux Toolkit / React Router v6(`Outlet` / `Navigate` / `useNavigate` / `useLocation` / `useMatch`或`matchPath`)/ Vite / Axios / TypeScript;`@ant-design/icons`。 | ||
| 73 | +- 测试:单测 Vitest(jsdom)+ `@testing-library/react|jest-dom|user-event`;E2E Playwright。沿用 FE-01 `tests/setup.ts`、`vite.config.ts` 配置。 | ||
| 74 | +- 命令(docs/04 § 零):build `npm run build`;lint `npm run lint`;unit `npm run test:unit`;e2e `npm run test:e2e`。子会话验证用 `cd frontend && npm run test:unit -- <文件名片段>`。 | ||
| 75 | +- 提交格式:`<type>(<scope>): <subject> REQ-USR-XXX`。本 FE 无单一 CRUD REQ,是 USR 子功能的导航/承载容器;**scope 统一用 `fe-shell`**;subject REQ 后缀按导向的子功能标注(导航壳/路由/守卫/标签栈相关用 `REQ-USR-003`=其承载的首要入口「用户列表」;用户身份展示/退出相关用 `REQ-USR-004`=登录态来源)。每个任务在其 commit 行注明所用 REQ tag。 | ||
| 76 | + | ||
| 77 | +## 合同级常量(跨 task 必须一致) | ||
| 78 | + | ||
| 79 | +- **路由 path**(React Router v6): | ||
| 80 | + - `/login`(FE-01 登录页,放行;包 `RedirectIfAuthed`,不包 `AppLayout`)。 | ||
| 81 | + - `/`(index,受保护,→ `HomePage`)。 | ||
| 82 | + - `/usr/users`(受保护,FE-03 容器;本 FE 仅提供导航入口与标签挂载位,目标内容属 FE-03)。 | ||
| 83 | + - `/usr/users/new`(受保护,FE-04 新增容器)。 | ||
| 84 | + - `/usr/users/:id`(受保护,FE-04 修改容器)。 | ||
| 85 | + - 未匹配:受保护区内 → `Navigate to="/"`;整体未匹配兜底 → 经守卫落到 `/login`(D7)。 | ||
| 86 | +- **标签栈 key(业务标签标识,跨组件一致)**:`'userlist'`(标题「用户列表」,路由 `/usr/users`,可关闭)、`'userdetail'`(标题「用户信息单据」,路由 `/usr/users/new` 或 `/usr/users/:id`,可关闭);固定标签 `'home'`(标题「主页」,路由 `/`,**不可关闭,恒在最左**)。 | ||
| 87 | +- **localStorage token 键**:`TOKEN_STORAGE_KEY = 'xly_erp_token'`(复用 `api/request.ts` 已导出常量,不写字面量)。 | ||
| 88 | +- **HTTP 状态码常量**(`api/request.ts`,新增):`HTTP_UNAUTHORIZED = 401`。 | ||
| 89 | +- **静态文案(逐字一致,复刻原型 / spec)**: | ||
| 90 | + | 用途 | 文案 | | ||
| 91 | + |---|---| | ||
| 92 | + | 全部导航按钮 | `全部导航` | | ||
| 93 | + | 主页标签 | `主页` | | ||
| 94 | + | 用户列表标签/入口 | `用户列表` | | ||
| 95 | + | 用户单据标签 | `用户信息单据` | | ||
| 96 | + | KPI 标题 | `KPI监控` | | ||
| 97 | + | 今日未处理统计前缀 | `今日未处理:`(值 `37428`,红,`var(--color-error)`) | | ||
| 98 | + | 未清总数统计前缀 | `未清总数:`(值 `56433`,蓝,`var(--color-primary)`) | | ||
| 99 | + | AI 助手按钮 | `小ai同学,请帮我安排今日工作` | | ||
| 100 | + | 常用操作卡标题 | `常用操作` | | ||
| 101 | + | 常用操作项 | `用户列表` / `系统功能模块设置`(后者占位) | | ||
| 102 | + | KPI 网格表头 7 列 | `导航类型` / `角色` / `KPI待处理事项(当前行双击进入)` / `KPI内容描述及处理结果(点击蓝色查看明细)` / `今日未处理` / `未清总数` / `子流程` | | ||
| 103 | + | 退出登录菜单项 | `退出登录` | | ||
| 104 | + | 退出登录成功提示 | `已退出登录`(`message.success`,`var(--color-success)`) | | ||
| 105 | + | 被动 401 提示 | `登录已失效,请重新登录`(`message.warning`,BR10) | | ||
| 106 | + | 导航占位项点击提示 | `功能开发中`(`message.info`,BR7/D4) | | ||
| 107 | + | 角色树分组 | `按角色` / `按流程` | | ||
| 108 | + | 空数据占位 | AntD `Empty` 默认「暂无数据」(KPI 网格/角色树空时,empty 态) | | ||
| 109 | + | 路由级错误兜底 | `页面出错,请刷新或返回主页` + 「返回主页」入口(D7) | | ||
| 110 | + | 页脚正文 | 复刻原型 footer.foot:`©Copyright Antler Software | 印刷智慧工厂 | 印刷MES | 印刷ERP | 印刷电商平台 | 文件智能处理 | 印前自动化 | 400-880-6237` + 备案号 `沪ICP备14034791号-1` | | ||
| 111 | + | 当前用户区文案规则 | `` `${sUserName}(${sUserType})` ``(D10;`user` 缺失时退化为占位用户名,见 BR3) | | ||
| 112 | +- **当前用户显示规则(BR3 / D10)**:拼 `` `${user.sUserName}(${user.sUserType})` ``(`sUserType` 已是中文「超级管理员/普通用户」,不再映射);`user` 为 null 时退化展示占位(如 `未登录用户` 或 `用户`,TDD 期定,登记于本计划占位常量并保持一处定义)。 | ||
| 113 | + | ||
| 114 | +## 关键签名(首次出现处给出,跨 task 一致) | ||
| 115 | + | ||
| 116 | +- **路由守卫**(`router/`): | ||
| 117 | + - `RequireAuth`(`{ children?: ReactNode }` 或用于 `<Route element={<RequireAuth/>}>` 的布局守卫,内部渲染 `<Outlet/>` 或 `children`):读 `useAppSelector(s => s.auth)`;`token` 为 null → `<Navigate to="/login" replace state={{ from: location.pathname }} />`(BR1);`token` 存在但 `user` 为 null → 渲染 `Spin` 占位(`authResolving`,data-testid 可选 `auth-resolving`);否则放行(`ready`)。 | ||
| 118 | + - `RedirectIfAuthed`(包 `/login`,`{ children: ReactNode }`):`token` 与 `user` 均就绪(或仅 token 即视为已登录,TDD 期定,登记一处)→ `<Navigate to={from ?? '/'} replace />`(BR2,`from` 取 `location.state.from`);否则渲染 `children`(LoginPage)。 | ||
| 119 | + - `AppErrorBoundary`(React class ErrorBoundary,`{ children: ReactNode }`):`componentDidCatch` 后渲染兜底 UI + 「返回主页」按钮(`window.location` 或 navigate `/`)。 | ||
| 120 | +- **应用外壳**(`layouts/AppLayout/`): | ||
| 121 | + - `AppLayout`(default export,无 props,布局路由组件):内部 `useTabStack()` + `useState(navOverlayOpen)` + `useAppSelector(authSlice.user)` + `useNavigate` + `useLocation`;渲染 `TopBar` / `NavOverlay` / `<Outlet/>` / `AppFooter`;挂载时注册 `request.ts` 的 `onUnauthorized`(D11)。 | ||
| 122 | + - `useTabStack()` → `{ tabs: TabItem[]; activeKey: string; openTab(key: 'userlist'|'userdetail'): void; closeTab(key: string): void; setActive(key: string): void }`,其中 `TabItem = { key: string; title: string; closable: boolean; routePath: string }`。逻辑(BR4/5/6):恒含 `home`(不可关、最左);`openTab('userlist')` 确保 userlist 在;`openTab('userdetail')` 确保 userlist+userdetail 在;`closeTab('userlist')` 同时移除 userdetail 并激活 home;`closeTab('userdetail')` 激活 userlist。**该 hook 只管标签集合与 activeKey,路由跳转由调用方据 `routePath` 执行**(保持 hook 纯净可测)。 | ||
| 123 | + - `TopBar`(`{ user: AuthUser | null; tabs: TabItem[]; activeKey: string; navOverlayOpen: boolean; onToggleNav(): void; onSelectTab(key): void; onCloseTab(key): void; onLogout(): void; onLogoHome(): void }`)。 | ||
| 124 | + - `NavOverlay`(`{ open: boolean; onClose(): void; onNavigate(routePath: string): void; onPlaceholder(): void }`):渲染 `NAV_SIDE` 左列 + `NAV_COLS` 右网格;叶子项有 `routePath` → `onNavigate`,否则 → `onPlaceholder`;点遮罩/Esc → `onClose`。 | ||
| 125 | + - `CurrentUserMenu`(`{ user: AuthUser | null; onLogout(): void }`):AntD `Dropdown`,menu 项「退出登录」→ `onLogout`。 | ||
| 126 | + - `AppFooter`(无 props):静态文案条。 | ||
| 127 | +- **导航静态配置**(`layouts/AppLayout/navConfig.ts`): | ||
| 128 | + - `NAV_SIDE: { key: string; label: string; active?: boolean }[]`(20 项,复刻 `navSide` 的 label 与「系统设置」active;图标可省或用占位)。 | ||
| 129 | + - `NAV_COLS: { title: string; items: NavLeaf[] }[]`,`NavLeaf = { label: string; routePath?: string; star?: boolean }`(复刻 `navCols`;仅「用户列表」`routePath:'/usr/users'`;「用户列表」「系统功能模块设置」`star:true`;其余 `routePath` 省略=占位)。 | ||
| 130 | +- **主页静态数据**(`pages/home/HomePage/dashboardData.ts`): | ||
| 131 | + - `KPI_STATS = { todayPending: 37428; openTotal: 56433 }`(复刻原型 `.kpi-head`,D2)。 | ||
| 132 | + - `ROLE_GROUPS: { label: string; count: number }[]` 与 `PROCESS_GROUPS: { label: string; count: number }[]`(复刻原型「按角色」「按流程」条目与计数,D2)。 | ||
| 133 | + - `KPI_ROWS: KpiRow[]`,`KpiRow = { role: string | null; item: string; desc: string; today: string; total: string; sub?: string; red?: boolean; navTypeFirst?: boolean; roleSpan?: number; subSpan?: number }`(**字段名与原型 `kpiRows` 一致**,复刻全量行,D2)。 | ||
| 134 | +- **主页组件 props**: | ||
| 135 | + - `KpiHeadBar`(`{ stats: typeof KPI_STATS }`);`RoleProcessTree`(`{ roleGroups; processGroups; onSelect?(label) }`,本地高亮态);`KpiBoard`(`{ rows: KpiRow[] }`,空数组 → `Empty`);`CommonOps`(`{ onOpenUserList(): void }`)。 | ||
| 136 | +- **request.ts 401 钩子**(`api/request.ts`,新增导出): | ||
| 137 | + - `HTTP_UNAUTHORIZED = 401` 常量。 | ||
| 138 | + - `registerUnauthorizedHandler(fn: () => void): void`(模块级单例,外壳挂载时注册;响应拦截器捕获 HTTP 401 时调用该回调再抛 `ApiError`)。`onUnauthorized` 回调内容由外壳提供:`clearCredentials` + `message.warning('登录已失效,请重新登录')` + 跳 `/login`(BR10)。 | ||
| 139 | + | ||
| 140 | +## 测试栈说明 | ||
| 141 | + | ||
| 142 | +- **jsdom 组件 / hook / store 单测**(Vitest + RTL):默认用真实 `store`(`configureStore`)+ `MemoryRouter`(`initialEntries` 指定路由)+ AntD `App` 上下文(经 `renderShell.tsx`)。守卫/标签栈/overlay/主页交互均在组件层断言;不依赖真实后端(本壳无取数)。`useTabStack` 纯逻辑可用 `renderHook` 直测。 | ||
| 143 | +- **Playwright E2E**:`page.route` 桩 `**/api/usr/login` 与 `**/api/usr/users`(FE-03 取数桩,仅为标签可挂载,不验列表内容),覆盖:登录→落地主页(顶栏可见、KPI 标题可见);点「全部导航」→ overlay 显隐;从常用操作/导航打开「用户列表」标签并关闭(联动回主页);退出登录回 `/login`。不依赖真实后端起服。 | ||
| 144 | +- **可测性**:优先用语义查询(role/text/label);仅当 RTL/Playwright 无法稳定定位时添加最小 `data-testid`(如 `nav-overlay` / `auth-resolving` / `tab-userlist`)。 | ||
| 145 | + | ||
| 146 | +--- | ||
| 147 | + | ||
| 148 | +## 任务列表(每个 task = red → green → 子会话验证 → commit) | ||
| 149 | + | ||
| 150 | +> 硬护栏:以下每个 `impl_file` / `test_file` 均以 `frontend/` 开头;无任何 `backend/` / `sql/` / `scripts/` 落点。 | ||
| 151 | +> 提交 scope 统一 `fe-shell`;REQ tag 按任务承载的子功能标注(见上「合同级常量」末段规则)。 | ||
| 152 | + | ||
| 153 | +### T0 — 外壳/路由测试共享渲染工具(chore,先建测试地基) | ||
| 154 | +- **测试先行类型**:jsdom 组件测试(自身即一个最小冒烟用例) | ||
| 155 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/renderShell.tsx` 自带最小冒烟 `frontend/tests/unit/renderShell.smoke.test.tsx::renderShell mounts a route element`——用 `renderShell(<div>shell-ok</div>, { initialEntries:['/'], preloadedAuth:{ token:'t', user:{...} } })` 渲染并断言 `shell-ok` 在文档中;初始因工具未实现而失败。 | ||
| 156 | +- [ ] **2. 实现最小代码**:`frontend/tests/unit/renderShell.tsx`——导出 `makeShellStore(preloadedAuth?)`(`configureStore` 挂 `auth`,可注入 `token`/`user`)与 `renderShell(ui, { initialEntries?, preloadedAuth?, store? })`(Provider + 真实 store + `ConfigProvider` + AntD `App` + `MemoryRouter`)。复用 FE-01 `renderLogin.tsx` 模式。 | ||
| 157 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- renderShell` | ||
| 158 | +- [ ] **4. commit**:`test(fe-shell): 外壳/路由测试共享渲染工具 REQ-USR-003` | ||
| 159 | + | ||
| 160 | +### T1 — RequireAuth 守卫三态(authResolving / unauthenticated / ready,BR1)(jsdom 组件测) | ||
| 161 | +- **测试先行类型**:jsdom 组件测试 | ||
| 162 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/RequireAuth.test.tsx`: | ||
| 163 | + - `::redirects to /login when no token`——无 token 时进 `/`,断言落到 `/login` 渲染(用一条 `/login` 哨兵路由 + 断言哨兵文本/URL),且携带 `state.from`(可用哨兵读 `useLocation().state`)。 | ||
| 164 | + - `::renders Spin placeholder when token present but user not resolved`——`preloadedAuth:{ token:'t', user:null }`,断言出现加载占位(`auth-resolving` / `Spin`),不渲染受保护内容。 | ||
| 165 | + - `::renders protected content when token and user ready`——`token` + `user` 就绪,断言放行渲染 `<Outlet/>` 子内容(哨兵)。 | ||
| 166 | +- [ ] **2. 实现最小代码**:`frontend/src/router/RequireAuth.tsx`(签名见关键签名;用 `useLocation` 取 `from`)。 | ||
| 167 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- RequireAuth` | ||
| 168 | +- [ ] **4. commit**:`feat(fe-shell): 受保护路由守卫 RequireAuth 三态 REQ-USR-004` | ||
| 169 | + | ||
| 170 | +### T2 — RedirectIfAuthed:已登录访问 /login 回主页(BR2)(jsdom 组件测) | ||
| 171 | +- **测试先行类型**:jsdom 组件测试 | ||
| 172 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/RedirectIfAuthed.test.tsx`: | ||
| 173 | + - `::renders children when unauthenticated`——无登录态,进 `/login`,渲染 children(哨兵「login-screen」)。 | ||
| 174 | + - `::redirects to / when already authenticated`——`token`+`user` 就绪进 `/login`,断言重定向到 `/`(哨兵)。 | ||
| 175 | + - `::redirects to from when present`——`state.from='/usr/users'` 且已登录 → 重定向到 `/usr/users`。 | ||
| 176 | +- [ ] **2. 实现最小代码**:`frontend/src/router/RedirectIfAuthed.tsx`(签名见关键签名)。 | ||
| 177 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- RedirectIfAuthed` | ||
| 178 | +- [ ] **4. commit**:`feat(fe-shell): 已登录访问登录页重定向守卫 REQ-USR-004` | ||
| 179 | + | ||
| 180 | +### T3 — useTabStack 标签栈逻辑(BR4/BR5/BR6)(jsdom hook 测) | ||
| 181 | +- **测试先行类型**:jsdom 组件测试(`renderHook`) | ||
| 182 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/useTabStack.test.tsx`: | ||
| 183 | + - `::starts with fixed home tab only (closable false, leftmost)`——初始 `tabs` 仅 `home`,`activeKey==='home'`,home `closable===false`。 | ||
| 184 | + - `::openTab userlist appends userlist and activates it`——`openTab('userlist')` 后 tabs 含 home+userlist,`activeKey==='userlist'`,userlist `closable===true`、`routePath==='/usr/users'`(BR4)。 | ||
| 185 | + - `::openTab userdetail ensures userlist exists then appends userdetail`——直接 `openTab('userdetail')`,tabs 含 home+userlist+userdetail,`activeKey==='userdetail'`(BR6)。 | ||
| 186 | + - `::closeTab userlist also removes userdetail and activates home`——先开 userlist+userdetail,`closeTab('userlist')` → tabs 仅 home,`activeKey==='home'`(BR5)。 | ||
| 187 | + - `::closeTab userdetail activates userlist`——开 userlist+userdetail,`closeTab('userdetail')` → tabs 含 home+userlist,`activeKey==='userlist'`。 | ||
| 188 | + - `::open existing tab does not duplicate`——重复 `openTab('userlist')` 不产生重复项。 | ||
| 189 | +- [ ] **2. 实现最小代码**:`frontend/src/layouts/AppLayout/useTabStack.ts`(签名 / `TabItem` 见关键签名;标签 key/title/routePath 用合同级常量)。 | ||
| 190 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- useTabStack` | ||
| 191 | +- [ ] **4. commit**:`feat(fe-shell): 顶栏标签栈联动逻辑 useTabStack REQ-USR-003` | ||
| 192 | + | ||
| 193 | +### T4 — navConfig 与 dashboardData 静态配置(D1/D2/D4)(jsdom 单测) | ||
| 194 | +- **测试先行类型**:jsdom 组件测试(纯数据断言) | ||
| 195 | +- [ ] **1. 写失败测试**: | ||
| 196 | + - `frontend/tests/unit/navConfig.test.ts`:`NAV_SIDE` 长度为 20 且含「系统设置」`active:true`;`NAV_COLS` 含 7 组、组标题为 `期初设置/用户管理/系统参数/计算方案/日志/开发平台/API对接管理`;「用户管理」组内「用户列表」`routePath==='/usr/users'` 且 `star===true`;「开发平台」组内「系统功能模块设置」`star===true` 且无 `routePath`(占位,BR7/D4)。 | ||
| 197 | + - `frontend/tests/unit/dashboardData.test.ts`:`KPI_STATS.todayPending===37428`、`openTotal===56433`(D2);`KPI_ROWS` 首行 `role==='核价人员'`、`navTypeFirst===true`、`roleSpan===4`、`sub==='估价管理流程'`、`subSpan===5`(复刻原型 kpiRows 字段,D2);`ROLE_GROUPS` 含「所有部门」计数 37428、「客服部」计数 30127。 | ||
| 198 | +- [ ] **2. 实现最小代码**:`frontend/src/layouts/AppLayout/navConfig.ts`(`NAV_SIDE` / `NAV_COLS`)+ `frontend/src/pages/home/HomePage/dashboardData.ts`(`KPI_STATS` / `ROLE_GROUPS` / `PROCESS_GROUPS` / `KPI_ROWS`),全量复刻原型内联数据(D1/D2)。 | ||
| 199 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- navConfig dashboardData` | ||
| 200 | +- [ ] **4. commit**:`feat(fe-shell): 导航与主页 KPI 静态配置数据 REQ-USR-003` | ||
| 201 | + | ||
| 202 | +### T5 — KpiBoard KPI 合并网格 + 空数据(BR11 / empty 态 / D5)(jsdom 组件测) | ||
| 203 | +- **测试先行类型**:jsdom 组件测试 | ||
| 204 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/KpiBoard.test.tsx`: | ||
| 205 | + - `::renders 7 column headers`——渲染 7 个表头文案(合同级常量逐字一致)。 | ||
| 206 | + - `::renders all kpi rows with item/desc/today/total`——传 `KPI_ROWS`,断言可见某几行 `item`(如「01/04【新增】新报价单」)与红色统计数(red 行数字用 `var(--color-error)`)。 | ||
| 207 | + - `::renders Empty when rows is empty`——传 `rows={[]}`,渲染 AntD `Empty`(empty 态,BR11)。 | ||
| 208 | + - `::KPI item/desc rendered as link-styled text without navigation`——「KPI待处理事项/内容描述」为蓝色链接样式但点击不发生路由跳转(纯展示,BR11)。 | ||
| 209 | +- [ ] **2. 实现最小代码**:`frontend/src/pages/home/HomePage/KpiBoard.tsx`(AntD `Table` rowSpan 合并「导航类型/角色/子流程」列,或 CSS Grid `gridRow span` 复刻原型——TDD 期二选一,登记 D5;空数组 → `Empty`)+ `HomePage.module.css` 网格样式(线/底色用 `var(--color-border)` / `var(--color-table-header-bg)` 等 token)。 | ||
| 210 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- KpiBoard` | ||
| 211 | +- [ ] **4. commit**:`feat(fe-shell): 主页 KPI 合并网格与空数据态 REQ-USR-003` | ||
| 212 | + | ||
| 213 | +### T6 — HomePage 落地页区域组合(KPI 头条 / 角色树 / 常用操作 / 页脚,BR8/BR11)(jsdom 组件测) | ||
| 214 | +- **测试先行类型**:jsdom 组件测试 | ||
| 215 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/HomePage.test.tsx`(`renderShell`,`initialEntries:['/']`): | ||
| 216 | + - `::renders KPI head with title and stats`——可见「KPI监控」、「今日未处理:」+ 37428、「未清总数:」+ 56433、AI 按钮文案「小ai同学,请帮我安排今日工作」。 | ||
| 217 | + - `::renders role/process tree groups`——可见「按角色」「按流程」分组及条目(如「所有部门 (37428)」「客服部 (30127)」)。 | ||
| 218 | + - `::tree item click highlights without navigation`——点击角色条目后该项高亮(active),不触发路由跳转/取数(BR11)。 | ||
| 219 | + - `::common ops user-list click navigates to /usr/users and opens tab`——点「常用操作 > 用户列表」触发 `navigate('/usr/users')`(断言哨兵/URL 改变或 `onOpenUserList` 被调)(BR8)。 | ||
| 220 | + - `::renders footer copyright text`——可见页脚版权与备案号文本。 | ||
| 221 | +- [ ] **2. 实现最小代码**:`frontend/src/pages/home/HomePage/HomePage.tsx` + `KpiHeadBar.tsx` + `RoleProcessTree.tsx`(本地高亮 `useState`)+ `CommonOps.tsx`(用户列表 → 调用 `onOpenUserList` / navigate)+ `pages/home/HomePage/AppFooter` 复用外壳 `AppFooter`(页脚归外壳;主页内 `.foot` 由 `AppLayout` 渲染——若主页自带页脚则置于 HomePage,TDD 期定一处,登记);`HomePage.module.css`。语义色用 token,统计红/蓝用 `var(--color-error)`/`var(--color-primary)`。 | ||
| 222 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- HomePage` | ||
| 223 | +- [ ] **4. commit**:`feat(fe-shell): 主页落地页区域组合 REQ-USR-003` | ||
| 224 | + | ||
| 225 | +### T7 — NavOverlay 全部导航总览(开关 / 分组渲染 / 路由项与占位项,BR7/BR8/D4)(jsdom 组件测) | ||
| 226 | +- **测试先行类型**:jsdom 组件测试 | ||
| 227 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/NavOverlay.test.tsx`: | ||
| 228 | + - `::hidden when open is false / visible when true`——`open=false` 不渲染网格内容;`open=true` 渲染左列 20 项 + 7 组标题。 | ||
| 229 | + - `::side has 系统设置 active`——左列「系统设置」处激活态。 | ||
| 230 | + - `::clicking 用户列表 calls onNavigate('/usr/users')`——点右网格「用户列表」叶子 → `onNavigate` 收到 `/usr/users`(BR8)。 | ||
| 231 | + - `::clicking placeholder leaf calls onPlaceholder (no navigate)`——点占位叶子(如「系统权限」)→ `onPlaceholder` 被调、`onNavigate` 未调(BR7/D4)。 | ||
| 232 | + - `::Esc / mask click calls onClose`——按 Esc 或点遮罩 → `onClose` 被调。 | ||
| 233 | +- [ ] **2. 实现最小代码**:`frontend/src/layouts/AppLayout/NavOverlay.tsx`(受控 `open`;左列渲染 `NAV_SIDE`、右网格渲染 `NAV_COLS`;叶子据 `routePath` 分流 `onNavigate`/`onPlaceholder`;Esc/遮罩 `onClose`)+ overlay 深色底 scoped 样式(D9)。 | ||
| 234 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- NavOverlay` | ||
| 235 | +- [ ] **4. commit**:`feat(fe-shell): 全部导航总览浮层 NavOverlay REQ-USR-003` | ||
| 236 | + | ||
| 237 | +### T8 — TopBar 顶栏结构 + 当前用户 + 退出登录(BR3/BR9)(jsdom 组件测) | ||
| 238 | +- **测试先行类型**:jsdom 组件测试 | ||
| 239 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/AppLayout.topbar.test.tsx`: | ||
| 240 | + - `::renders brand logo / 全部导航 button / 主页 tab`——可见 Logo、「全部导航」按钮、固定「主页」标签(无关闭按钮)。 | ||
| 241 | + - `::renders current user as sUserName(sUserType)`——`user:{ sUserName:'朱子纯', sUserType:'超级管理员', ... }` → 可见「朱子纯(超级管理员)」(BR3/D10)。 | ||
| 242 | + - `::user fallback when user is null`——`user:null` → 退化占位用户名(合同级占位常量),不崩溃。 | ||
| 243 | + - `::logout menu dispatches clearCredentials, shows success, navigates /login`——展开当前用户下拉点「退出登录」→ store `auth.token===null`、`localStorage` token 被移除、`message.success('已退出登录')`、URL/哨兵到 `/login`(BR9)。 | ||
| 244 | + - `::nav toggle button highlights when navOverlayOpen`——`navOverlayOpen=true` 时「全部导航」按钮带激活态。 | ||
| 245 | + - `::clicking business tab close calls onCloseTab`——点 userlist 标签「✕」→ `onCloseTab('userlist')` 被调。 | ||
| 246 | +- [ ] **2. 实现最小代码**:`frontend/src/layouts/AppLayout/TopBar.tsx` + `CurrentUserMenu.tsx`(签名见关键签名;标签条用 AntD `Tabs` editable-card 或自定义受控 div,D3——主页 tab `closable:false`;退出登录 `onLogout` 内 `dispatch(clearCredentials())` + `message.success` + `navigate('/login',{replace:true})`,由 `AppLayout` 提供回调)。 | ||
| 247 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- AppLayout.topbar` | ||
| 248 | +- [ ] **4. commit**:`feat(fe-shell): 顶栏与当前用户/退出登录 REQ-USR-004` | ||
| 249 | + | ||
| 250 | +### T9 — AppLayout 外壳装配 + 标签↔路由同步(ready / navOverlayOpen / tabOpen 态)(jsdom 组件测) | ||
| 251 | +- **测试先行类型**:jsdom 组件测试 | ||
| 252 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/AppLayout.shell.test.tsx`(`renderShell` 已登录,渲染真实 `AppLayout` + 子哨兵路由): | ||
| 253 | + - `::renders TopBar + Outlet + Footer when ready`——已登录进 `/`,顶栏 + 主页 Outlet + 页脚同时在(ready 态)。 | ||
| 254 | + - `::toggle 全部导航 opens/closes overlay`——点「全部导航」→ overlay 显(navOverlayOpen);再点/遮罩 → 隐。 | ||
| 255 | + - `::nav overlay 用户列表 navigates and opens tab`——overlay 点「用户列表」→ overlay 关、URL 到 `/usr/users`、顶栏出现「用户列表」标签并激活(tabOpen 态,BR8)。 | ||
| 256 | + - `::clicking home tab navigates back to /`——多标签下点「主页」标签 → URL 回 `/`、主页激活。 | ||
| 257 | + - `::active tab syncs with current route`——直接进 `/usr/users` 时「用户列表」标签为激活态(路由→标签同步)。 | ||
| 258 | +- [ ] **2. 实现最小代码**:`frontend/src/layouts/AppLayout/AppLayout.tsx`(组合 `TopBar`/`NavOverlay`/`<Outlet/>`/`AppFooter`;`useTabStack` + `navOverlayOpen` 本地态;标签点击 → `navigate(routePath)`;据 `useLocation` 反向同步 activeKey 与已打开标签;overlay 路由项 → 关 overlay + openTab + navigate)+ `AppLayout.module.css`(顶栏深色底 scoped,D9)。 | ||
| 259 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- AppLayout.shell` | ||
| 260 | +- [ ] **4. commit**:`feat(fe-shell): 应用外壳装配与标签路由同步 REQ-USR-003` | ||
| 261 | + | ||
| 262 | +### T10 — 路由表接线 + ErrorBoundary(替换 / 占位,含 401 协同入口,BR1/BR2/D7)(jsdom 组件测) | ||
| 263 | +- **测试先行类型**:jsdom 组件测试 | ||
| 264 | +- [ ] **1. 写失败测试**: | ||
| 265 | + - `frontend/tests/unit/router.test.tsx`(用真实 `AppRouter` + `MemoryRouter`,mock 子页面/FE-03/FE-04 为哨兵占位以隔离未实现页): | ||
| 266 | + - `::unauthenticated / redirects to /login`(BR1);`::authenticated / renders HomePage shell`(落地主页);`::authenticated /usr/users renders under AppLayout`(受保护子路由在外壳内);`::authenticated /login redirects to /`(BR2);`::unknown protected path redirects to /`(D7)。 | ||
| 267 | + - `frontend/tests/unit/AppErrorBoundary.test.tsx`:`::renders fallback with 返回主页 when child throws`——子组件抛错 → 兜底文案「页面出错,请刷新或返回主页」+「返回主页」入口。 | ||
| 268 | +- [ ] **2. 实现最小代码**:改 `frontend/src/router/index.tsx`——嵌套结构 `RequireAuth > AppLayout`(index `HomePage` + `/usr/users` / `/usr/users/new` / `/usr/users/:id` 暂用占位/懒加载位,FE-03/FE-04 落地时替换);`/login` 包 `RedirectIfAuthed`;外壳内套 `AppErrorBoundary`;未匹配 `Navigate to="/"`(受保护)/经守卫到 `/login`。新增 `frontend/src/router/AppErrorBoundary.tsx`。**子路由目标内容(FE-03/FE-04)不在本 FE 实现**,仅留可挂载的占位元素。 | ||
| 269 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- router AppErrorBoundary` | ||
| 270 | +- [ ] **4. commit**:`feat(fe-shell): 受保护嵌套路由表与错误边界 REQ-USR-003` | ||
| 271 | + | ||
| 272 | +### T11 — request.ts 401 统一登出回调(BR10 / D11)(jsdom 单测) | ||
| 273 | +- **测试先行类型**:jsdom 组件测试 | ||
| 274 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/request.unauthorized.test.ts`(沿用 FE-01 `axios-mock-adapter` 模式): | ||
| 275 | + - `::HTTP 401 triggers registered onUnauthorized then rejects ApiError`——`registerUnauthorizedHandler(spy)`,mock 适配器对某请求返回 HTTP 401,断言 `spy` 被调用一次且请求最终 reject 为 `ApiError`。 | ||
| 276 | + - `::no handler registered does not throw`——未注册回调时 401 仍 reject `ApiError`,不抛额外异常。 | ||
| 277 | + - 不破坏 FE-01 既有断言(401 业务体仍映射 `ApiError`)。 | ||
| 278 | +- [ ] **2. 实现最小代码**:改 `frontend/src/api/request.ts`——加 `HTTP_UNAUTHORIZED=401` 常量、模块级 `registerUnauthorizedHandler(fn)` 与 `onUnauthorized` 单例;响应错误拦截器中 `error.response?.status===401` 时调用回调(若已注册)再走原 `ApiError` 映射逻辑。**不改变** FE-01 既有拆包/网络兜底语义。 | ||
| 279 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- request`(含 FE-01 `request` 既有用例不回归) | ||
| 280 | +- [ ] **4. commit**:`feat(fe-shell): 401 统一登出回调接入 request 拦截器 REQ-USR-004` | ||
| 281 | + | ||
| 282 | +### T12 — AppLayout 注册 401 登出处理(壳层接线,BR10)(jsdom 组件测) | ||
| 283 | +- **测试先行类型**:jsdom 组件测试 | ||
| 284 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/AppLayout.unauthorized.test.tsx`: | ||
| 285 | + - `::registers onUnauthorized on mount; invoking it clears auth + warns + navigates /login`——渲染已登录 `AppLayout`,捕获注册的回调(spy `registerUnauthorizedHandler`),调用之 → store `auth.token===null`、`message.warning('登录已失效,请重新登录')`、URL 到 `/login`(BR10)。 | ||
| 286 | +- [ ] **2. 实现最小代码**:在 `AppLayout` `useEffect` 内 `registerUnauthorizedHandler(() => { dispatch(clearCredentials()); message.warning('登录已失效,请重新登录'); navigate('/login',{replace:true}); })`(卸载时可清理)。 | ||
| 287 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- AppLayout.unauthorized` | ||
| 288 | +- [ ] **4. commit**:`feat(fe-shell): 外壳注册被动401统一登出 REQ-USR-004` | ||
| 289 | + | ||
| 290 | +### T13 — E2E 外壳关键旅程(Playwright) | ||
| 291 | +- **测试先行类型**:Playwright E2E | ||
| 292 | +- [ ] **1. 写失败测试**:`frontend/tests/e2e/shell.spec.ts`(`page.route` 桩 `**/api/usr/login` 成功响应;桩 `**/api/usr/users` 返回 `{code:0,...,data:{records:[],total:0,...}}` 仅为标签挂载;不验 FE-03 列表内容): | ||
| 293 | + - `::login then lands on home with topbar and KPI title`——登录成功后 URL 到 `/`,顶栏「全部导航」与「KPI监控」可见。 | ||
| 294 | + - `::open and close 全部导航 overlay`——点「全部导航」overlay 显示 7 组;Esc/遮罩关闭。 | ||
| 295 | + - `::open 用户列表 tab from common ops then close back to home`——常用操作点「用户列表」→ 出现「用户列表」标签、URL `/usr/users`;点标签「✕」→ 回主页、标签移除(BR5/BR8)。 | ||
| 296 | + - `::logout returns to /login`——当前用户下拉「退出登录」→ URL 回 `/login`、可见「已退出登录」提示。 | ||
| 297 | + - `::visiting / unauthenticated redirects to /login`——清除 token 后直接访问 `/` → 跳 `/login`(BR1)。 | ||
| 298 | +- [ ] **2. 实现最小代码**:补任何为可测性需要的最小 `data-testid`(仅 RTL/Playwright 无法稳定定位时);E2E 桩中 FE-03/FE-04 子路由用占位即可(本 FE 不实现其内容)。沿用 FE-01 `playwright.config.ts`。 | ||
| 299 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:e2e -- shell` | ||
| 300 | +- [ ] **4. commit**:`test(fe-shell): 应用外壳 E2E 关键旅程 REQ-USR-003` | ||
| 301 | + | ||
| 302 | +### T14 — 全量门禁回归 + 收尾(chore) | ||
| 303 | +- **测试先行类型**:无新增测试(全量验证) | ||
| 304 | +- [ ] **1. 写失败测试**:无。 | ||
| 305 | +- [ ] **2. 实现最小代码**:修 lint / build / 类型问题;确认语义色全部 `var(--color-*)`、无硬编码 hex/rgba(顶栏/overlay 深色装饰 scoped 例外,D9);确认无 `TBD/TODO/【人工填写】`。 | ||
| 306 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run lint && npm run build && npm run test:unit && npm run test:e2e` 全绿。 | ||
| 307 | +- [ ] **4. commit**:`chore(fe-shell): FE-02 门禁回归通过 REQ-USR-003` | ||
| 308 | + | ||
| 309 | +--- | ||
| 310 | + | ||
| 311 | +## 完成判据(Definition of Done) | ||
| 312 | + | ||
| 313 | +1. 登录后落地主页 `/`,渲染复刻原型 `#screen-main` 的 KPI 头条 / 角色流程树 / KPI 合并网格 / 常用操作 / 页脚(spec § 2 / § 6.6)。 | ||
| 314 | +2. 应用外壳 `AppLayout` 渲染顶栏(Logo + 全部导航 + 标签栈 + 当前用户 + 退出)+ 导航总览 overlay + `<Outlet/>` + 页脚(spec § 2)。 | ||
| 315 | +3. 状态机覆盖并测试固化:`authResolving` / `unauthenticated` / `ready`(T1/T10)、`navOverlayOpen`(T7/T9)、`tabOpen`(T3/T9)、`empty`(T5)、`error`(T10 ErrorBoundary)(spec § 3)。 | ||
| 316 | +4. 业务/交互规则 BR1~BR11 在组件层 / E2E 有断言:BR1/BR2(T1/T2/T10)、BR3/BR9(T8)、BR4/BR5/BR6(T3/T9)、BR7(T7)、BR8(T6/T7/T9)、BR10(T11/T12)、BR11(T5/T6)(spec § 5)。 | ||
| 317 | +5. **不新增任何后端取数**;KPI/导航/角色树为前端静态配置(`dashboardData.ts` / `navConfig.ts`),复刻原型 demo(spec § 4 / D1/D2)。 | ||
| 318 | +6. 退出登录纯前端清登录态 + 删 token + 跳 `/login`(spec § 6.7 / D6);被动 401 经 `request.ts` 统一登出回调跳 `/login`(BR10 / D11)。 | ||
| 319 | +7. 标签栈复刻原型 `tabsOpen/openTab/.close` 联动(主页固定不可关、userlist↔userdetail 父子联动)(spec § 6.4 / BR4-6)。 | ||
| 320 | +8. 语义色只用 `var(--color-*)`,AntD `colorPrimary` 沿用 FE-01 `ConfigProvider` 对齐;顶栏/overlay 深色底 scoped 装饰不新增全局 token(spec § 7 / D9)。 | ||
| 321 | +9. 全部落点在 `frontend/**`,无 `backend/` / `sql/` / `scripts/` 改动;改 `router/index.tsx`、`api/request.ts` 属共享骨架,已在《模块完成报告》留痕。 | ||
| 322 | +10. 门禁全绿:`npm run lint` / `npm run build` / `npm run test:unit` / `npm run test:e2e`(docs/04 § 零)。 | ||
| 323 | + | ||
| 324 | +## 自审记录 | ||
| 325 | + | ||
| 326 | +- **占位符扫描**:本计划无 `【人工填写:】` / `TBD` / `TODO` 占位(正文中 `TBD/TODO` 仅作为「禁止出现的字样」被引用,非真实占位)。 | ||
| 327 | +- **spec coverage**:spec § 1 关联 REQ/原型→架构 + 全任务;§ 2 组件树→T5(KpiBoard)/T6(HomePage)/T7(NavOverlay)/T8(TopBar)/T9(AppLayout)/T10(路由);§ 3 状态机→T1/T10(auth 三态+error)/T7/T9(navOverlayOpen)/T3/T9(tabOpen)/T5(empty);§ 4 端点关系(仅承接守卫/跳转,不取数)→T1/T2/T10/T11/T12;§ 5 BR1-BR11→见 DoD 第 4 条逐条映射;§ 6 路由与交互→T1/T2/T3/T7/T8/T9/T10;§ 7 tokens→T5/T7/T8/T9/T14;§ 8 decisions D1-D10 已落实于架构/合同级常量/任务,本计划新增 D11(401 钩子机制)见下。 | ||
| 328 | +- **本计划新增决策 D11(401 统一登出机制)**:`request.ts` 拦截器内无法使用 React hooks(`useNavigate`/`message`),故采用「模块级 `registerUnauthorizedHandler` 注册回调」模式:`AppLayout` 挂载时注册含 `clearCredentials + message.warning + navigate('/login')` 的回调,拦截器捕获 HTTP 401 时调用之(spec BR10 / docs/04 § 2.4)。这是对 FE-01 共享 `request.ts` 的最小非破坏性扩展(不改既有拆包/网络兜底语义),并在《模块完成报告》留痕。置信度 high,登记进 decisions[]。 | ||
| 329 | +- **类型一致性**:标签 key(`home/userlist/userdetail`)、`TabItem`、路由 path(`/` `/login` `/usr/users` `/usr/users/new` `/usr/users/:id`)、`NAV_SIDE/NAV_COLS/NavLeaf`、`KPI_STATS/KPI_ROWS/KpiRow`(字段名对齐原型 `kpiRows`)、`AuthUser`(复用 FE-01 `api/types.ts`)、`TOKEN_STORAGE_KEY`/`HTTP_UNAUTHORIZED`、各文案常量跨 T0-T14 一致;当前用户文案规则 `` `${sUserName}(${sUserType})` `` 与 BR3/D10 一致。 | ||
| 330 | +- **作用域自审**:所有 `impl_file` / `test_file` 均以 `frontend/` 开头;无 `backend/` / `sql/` / `scripts/` 落点。 |
docs/superpowers/plans/2026-06-01-FE-03.md
0 → 100644
| 1 | +# FE-03 用户列表与查询 — 任务级 TDD 计划(前端) | ||
| 2 | + | ||
| 3 | +> 阶段:前端(frontend)。作用域:`frontend/**`(页面 / api / 类型 / 样式 / 测试)。**禁止**写 `backend/**` / `sql/**` / `scripts/**`。 | ||
| 4 | +> 上游 SSoT:spec `docs/superpowers/specs/2026-06-01-FE-03.md`;需求卡片 `docs/01-需求清单/USR-用户管理/REQ-USR-003.md`;API 契约 `docs/05-API接口契约.md § REQ-USR-003`(`GET /api/usr/users`);原型 `prototype/erp.html` → `<section id="screen-userlist">`(`.toolbar` / `.filterbar` / `.table-shell .grid-table#user-table` / `.pager`,布局与交互权威);技术规范 `docs/04-技术规范.md § 零 / § 2.1 / § 2.3 / § 2.4 / § 3.2`;Design Tokens 仓库根 `src/styles/tokens.css`。 | ||
| 5 | +> 复用资产:FE-01(`api/request.ts` 已建 Axios 实例 + Result 拆包 + `ApiError` + `TOKEN_STORAGE_KEY`;`api/types.ts`)、FE-02(`AppLayout` 外壳 + `RequireAuth` 守卫 + 标签栈 `useTabStack` + 路由表 `router/index.tsx` 已挂 `/usr/users` 占位 `UserListPlaceholder`)。 | ||
| 6 | +> 本计划告诉 TDD 执行者**做什么 / 文件边界 / 测试意图 / props 与类型契约 / 完成判据**;具体代码由红-绿-提交循环产出,不在此 dump 整组件 / 类型文件内容。 | ||
| 7 | +> **本 FE 为只读查询**:仅 `GET /api/usr/users`,无任何写副作用(spec § 5 BR5)。查询匹配语义、敏感字段过滤、分页越界回退等真伪裁决全部在后端,前端只采集条件 + 发起分页查询 + 依响应渲染表格 / 分页 / 空态 / 错误态 + 导航跳转。 | ||
| 8 | + | ||
| 9 | +--- | ||
| 10 | + | ||
| 11 | +## Goal(目标) | ||
| 12 | + | ||
| 13 | +把 FE-02 在 `router/index.tsx` 留下的 `/usr/users` 占位(`UserListPlaceholder`)替换为真实「用户列表与查询」页面,复刻原型 `#screen-userlist` 的布局与交互语义(工具栏 / 筛选栏 / 用户表格 / 分页栏),数据/筛选/分页真实对接 `GET /api/usr/users`: | ||
| 14 | + | ||
| 15 | +- **页面容器 `UserListPage`**(路由 `/usr/users`,渲染于 FE-02 `AppLayout` 的 `<Outlet/>` 内):纵向组合 `UserToolbar` + `UserFilterBar` + `UserTable` + 内置分页,状态由页面本地 hook `useUserList` 持有(spec § 8 D6,不进 Redux)。 | ||
| 16 | +- **API 封装**:`api/usrApi.ts` 新增 `listUsers(query): Promise<PageResult<UserVO>>`(页面只调封装方法,不散用 axios,docs/04 § 2.3);`api/types.ts` 新增 `UserVO` / `PageResult<T>` / `UserListQuery` 契约。 | ||
| 17 | +- **状态机 ≥6 态**(spec § 3):`initialLoading` / `loading`(查询·翻页·刷新进行中)/ `success` / `empty` / `error` / `exporting`,均有测试固化。 | ||
| 18 | +- **业务规则 BR1~BR15** 在组件层 / hook 层 / E2E 有断言(spec § 5)。 | ||
| 19 | +- **工具栏**:刷新(保持当前条件+当前页重取,BR8)/ 新增(`navigate('/usr/users/new')`,BR13)/ 导出Excel(导出当前条件命中结果,BR9 / D5 见下方决策 D-PLAN-1)/ 设置齿轮(占位,spec D7)。 | ||
| 20 | +- **筛选栏**:用户范围下拉(占位不传后端参数,spec D2)/ 查询字段下拉(默认「用户名」,BR2/BR4)/ 匹配方式下拉(默认「包含」,BR2/BR4)/ 查询值输入(空为全部,BR3,回车触发搜索 BR7)/ 更多「▾」(占位,spec D3)/ 搜索(回第 1 页查询,BR7)/ 清空(重置默认并全量查询,BR10)。 | ||
| 21 | +- **用户表格**:AntD `Table`(`rowKey="id"`,服务端受控分页,`rowSelection={{type:'radio'}}` 单选标记仅服务进单据不参与查询 spec D8,行双击 → `navigate('/usr/users/'+id)` BR12,「作废」列只读 0/1→否/是 BR6,「序号」列按当前页计算 BR1)。 | ||
| 22 | +- **分页**:AntD `Table` 内置 `pagination` 受控(`current`/`pageSize`/`total`/`showSizeChanger`/`showTotal`),`pageSize` 默认 10、可选 `[10,20,50,100]`(spec D4);切页/改每页条数重取(改每页条数回第 1 页,BR11);越界由后端回退、前端按响应 `pageNum` 回显(BR15)。 | ||
| 23 | +- **空态 / 错误态**:空 `records` → AntD `Empty`「暂无匹配的用户」不报错(BR14);非 0 code / 网络异常 → 错误占位 +「点击重试」+ `message` 文案(spec § 4 错误码表);被动 401 由 `request.ts` 拦截器统一跳 `/login`(FE-02 D11,本页不重复处理)。 | ||
| 24 | +- **语义色只用 `var(--color-*)`**;工具栏深色底为页面局部装饰,scoped 保留,不新增全局 token、不挪用语义 token(spec § 7 / D10)。 | ||
| 25 | + | ||
| 26 | +## Architecture(架构 / 分层) | ||
| 27 | + | ||
| 28 | +遵循 `docs/04 § 2.1`,落点全在 `frontend/**`。**新增/改动**文件: | ||
| 29 | + | ||
| 30 | +``` | ||
| 31 | +frontend/ | ||
| 32 | +├── src/ | ||
| 33 | +│ ├── api/types.ts # 【改】新增 UserVO / PageResult<T> / UserListQuery 契约(沿用 FE-01 既有类型,不破坏) | ||
| 34 | +│ ├── api/usrApi.ts # 【改】新增 listUsers(query): Promise<PageResult<UserVO>>(GET /api/usr/users) | ||
| 35 | +│ ├── pages/usr/UserList/index.tsx # 【新增】UserListPage 页面容器:组合工具栏/筛选栏/表格+分页;持 useUserList 态;导航跳转 | ||
| 36 | +│ ├── pages/usr/UserList/useUserList.ts # 【新增】列表查询 hook(list/total/loading/error/query/exporting + 动作) | ||
| 37 | +│ ├── pages/usr/UserList/UserToolbar.tsx # 【新增】深色工具条:刷新/新增/导出Excel/设置齿轮 | ||
| 38 | +│ ├── pages/usr/UserList/UserFilterBar.tsx # 【新增】筛选栏:范围/查询字段/匹配方式/查询值/更多/搜索/清空 | ||
| 39 | +│ ├── pages/usr/UserList/UserTable.tsx # 【新增】AntD Table:列定义/序号/作废只读/行双击/单选 rowSelection/受控分页 | ||
| 40 | +│ ├── pages/usr/UserList/columns.tsx # 【新增】列定义 + 表头文案 + 作废渲染(与 UserVO 字段映射,spec § 6) | ||
| 41 | +│ ├── pages/usr/UserList/constants.ts # 【新增】合同级常量:查询字段/匹配方式枚举、默认 query、pageSize 选项、文案 | ||
| 42 | +│ ├── pages/usr/UserList/exportUtils.ts # 【新增】前端导出工具(CSV+BOM Blob 下载,D-PLAN-1) | ||
| 43 | +│ └── pages/usr/UserList/UserList.module.css # 【新增】页面 scoped 样式:语义色用 var(--color-*);工具栏深色底局部装饰(D10) | ||
| 44 | +├── src/router/index.tsx # 【改】把 /usr/users 占位 UserListPlaceholder 替换为真实 UserListPage(属 FE 共享骨架,留痕) | ||
| 45 | +└── tests/ | ||
| 46 | + ├── unit/usrApi.userlist.test.ts # 【新增】listUsers 透传 query 到 GET /usr/users 并返回 PageResult(沿用 usrApi.test.ts 桩模式) | ||
| 47 | + ├── unit/useUserList.test.tsx # 【新增】hook 取数/翻页/刷新/搜索/清空/空/错误/导出态(renderHook) | ||
| 48 | + ├── unit/UserFilterBar.test.tsx # 【新增】默认值/枚举 options/回车搜索/清空回调(BR2/BR4/BR7/BR10) | ||
| 49 | + ├── unit/UserTable.test.tsx # 【新增】列渲染/序号/作废只读/行双击导航/受控分页/空 Empty(BR1/BR6/BR11/BR12/BR14) | ||
| 50 | + ├── unit/UserToolbar.test.tsx # 【新增】刷新/新增导航/导出中禁用/齿轮占位(BR8/BR9/BR13/D7) | ||
| 51 | + ├── unit/UserListPage.test.tsx # 【新增】页面集成:挂载默认查询/搜索/翻页/错误重试/空态(状态机 + BR3/BR7/BR15) | ||
| 52 | + └── e2e/userlist.spec.ts # 【新增】E2E:进入用户列表→渲染行/空态/搜索/翻页/行双击进单据/错误重试 | ||
| 53 | +``` | ||
| 54 | + | ||
| 55 | +- **跨阶段/跨模块**:本 FE 落点全在 `frontend/**`,不触 `backend/` / `sql/` / `scripts/`。改 `src/router/index.tsx`(把 `/usr/users` 占位换为真实页)与 `src/api/usrApi.ts` / `src/api/types.ts`(FE-01/FE-02 搭建的全前端共享骨架)属共享资产扩展,在《模块完成报告》留痕:「FE-03 将 `/usr/users` 占位 `UserListPlaceholder` 替换为真实 `UserListPage`;在 `usrApi.ts`/`types.ts` 增列表查询契约(`listUsers`/`UserVO`/`PageResult`/`UserListQuery`),不改 FE-01 既有 `login`/`fetchCompanies`/类型;属共享骨架扩展,FE-04 可复用 `UserVO`」。 | ||
| 56 | +- **状态管理**(docs/04 § 2.2 / spec D6):列表查询态(list/total/loading/error/query/exporting)为页面就近态,用 `useUserList` hook + `useState` 本地管理,不进 Redux;登录态复用 FE-01 `authSlice`(本页不读写)。 | ||
| 57 | +- **请求封装 / 错误处理**(docs/04 § 2.3 / § 2.4):取数走 `usrApi.listUsers` → `request.ts` Axios 实例(响应拦截器已拆 `Result`:`code=0` 返回 `data`,非 0 抛 `ApiError`,被动 401 统一登出 FE-02 D11);页面/hook 捕获 `ApiError` 按 code 分流 `message` 文案 + 错误占位。 | ||
| 58 | +- **Design Tokens**(docs/04 § 2.1 / spec § 7):筛选栏/表格/分页/空态/错误/成功/警告等语义色只用 `var(--color-*)`;工具栏深色底为页面局部装饰,scoped 在 `UserList.module.css`,不新增全局 token、不挪用语义 token(spec § 7 / D10,与 FE-02 § 8 D9 一致)。 | ||
| 59 | + | ||
| 60 | +## Tech Stack(技术栈,源自 docs/04 § 零 + FE-01/FE-02 骨架) | ||
| 61 | + | ||
| 62 | +- React 18 / Ant Design 5(`Table` / `Select` / `Input` / `Button` / `Tag`或只读 `Checkbox` / `Empty` / `Result`或错误占位 / `Spin` / `message` / `Space`/`Form`(可选 inline))/ React Router v6(`useNavigate`)/ Axios / TypeScript;`@ant-design/icons`(`ReloadOutlined` / `PlusCircleOutlined` / `FileExcelOutlined` / `SettingOutlined` / `SearchOutlined`)。 | ||
| 63 | +- **不新增 npm 依赖**:导出用零依赖 CSV(UTF-8 BOM + Blob + `<a download>`),不引入 `xlsx`/SheetJS(D-PLAN-1)。 | ||
| 64 | +- 测试:单测 Vitest(jsdom)+ `@testing-library/react|jest-dom|user-event`(沿用 `tests/setup.ts`、`renderShell.tsx`);E2E Playwright(沿用 `playwright.config.ts`,`page.route` 桩 `**/api/usr/users**`)。 | ||
| 65 | +- 命令(docs/04 § 零):build `npm run build`;lint `npm run lint`;unit `npm run test:unit`;e2e `npm run test:e2e`。子会话验证用 `cd frontend && npm run test:unit -- <文件名片段>`。 | ||
| 66 | +- 提交格式:`<type>(<scope>): <subject> REQ-USR-003`。**scope 统一用 `usr`**(业务模块名,CLAUDE.md § Git);subject 业务类(feat/fix/test)带 `REQ-USR-003` 后缀。每个任务在其 commit 行注明 REQ tag。 | ||
| 67 | + | ||
| 68 | +## 合同级常量(跨 task 必须一致) | ||
| 69 | + | ||
| 70 | +- **API 路径 / 方法**:`GET /api/usr/users`(`usrApi` 内传 `/usr/users`,`request.ts` baseURL=`/api` 已含前缀;query 参数走 axios `params`)。 | ||
| 71 | +- **查询字段枚举**(`QUERY_FIELD_OPTIONS`,对齐 REQ 输入表 1「显示来源」/ docs/05):`用户名` / `员工名` / `用户号` / `部门` / `用户类型` / `作废` / `登录日期` / `制单人`(**逐字一致**,原样作为 `queryField` 提交值,前端不映射,匹配语义由后端裁决)。默认 `用户名`(BR2)。 | ||
| 72 | +- **匹配方式枚举**(`MATCH_TYPE_OPTIONS`):`包含` / `不包含` / `等于`(原样作为 `matchType` 提交值)。默认 `包含`(BR2)。 | ||
| 73 | +- **用户范围下拉**(`SCOPE_OPTIONS`,占位 demo,spec D2):仅 `全部用户` 一项;选中「全部用户」时**不向后端传任何额外参数**(不杜撰 docs/05 未定义的「范围」参数)。 | ||
| 74 | +- **默认查询 `DEFAULT_QUERY: UserListQuery`**:`{ queryField: '用户名', matchType: '包含', queryValue: '', pageNum: 1, pageSize: 10 }`(BR2/BR3,pageSize 默认 10 对齐 docs/05,spec D4)。`queryValue` 为空字符串时提交时省略或传空(后端按全量处理,BR3)。 | ||
| 75 | +- **pageSize 选项**(`PAGE_SIZE_OPTIONS`):`[10, 20, 50, 100]`(上限 100 对齐 docs/05 / REQ 边界,spec D4;不采用原型 demo 的 10000)。 | ||
| 76 | +- **错误码常量**(对齐 docs/05 § REQ-USR-003):`ERR_PAGE_INVALID = 42201`(分页参数非法)、`ERR_QUERY_INVALID = 40001`(查询参数校验失败);网络/超时/5xx 经 `request.ts` 映射为 `NETWORK_ERROR_CODE = -1`(复用 FE-01 常量);`401` 由 `request.ts` 统一处理(本页不分流)。 | ||
| 77 | +- **表格列定义(中文表头,逐字一致,对齐原型 thead + REQ 输出表 1 + spec § 6)**,列顺序固定: | ||
| 78 | + | 列序 | 表头文案 | 数据字段(UserVO) | 渲染 | | ||
| 79 | + |---|---|---|---| | ||
| 80 | + | 0 | (单选列,无表头) | —(`rowSelection` 渲染) | radio 单选标记(spec D8) | | ||
| 81 | + | 1 | `序号` | —(前端生成) | `(pageNum-1)*pageSize + index + 1`(BR1) | | ||
| 82 | + | 2 | `用户名` | `sUserName` | 文本(双击进单据主标识) | | ||
| 83 | + | 3 | `员工名` | `employeeName`(映射自 docs/05 `员工名`,D-PLAN-2) | 文本,可空 | | ||
| 84 | + | 4 | `用户号` | `sUserNo` | 文本,可空 | | ||
| 85 | + | 5 | `部门` | `departmentName`(映射自 docs/05 `部门`,D-PLAN-2) | 文本,可空 | | ||
| 86 | + | 6 | `用户类型` | `sUserType` | 文本(已中文) | | ||
| 87 | + | 7 | `语言` | `sLanguage` | 文本 | | ||
| 88 | + | 8 | `作废` | `iIsVoid` | 只读:`0`→`否`、`1`→`是`(BR6,`Tag` 或禁用 `Checkbox`,点击无效) | | ||
| 89 | + | 9 | `登录日期` | `tLastLoginDate` | 日期时间文本,可空 | | ||
| 90 | + | 10 | `制单人` | `sCreator` | 文本 | | ||
| 91 | + | 11 | `制单日期` | `tCreateDate` | 日期时间文本 | | ||
| 92 | +- **静态文案(逐字一致,复刻原型 / spec)**: | ||
| 93 | + | 用途 | 文案 | | ||
| 94 | + |---|---| | ||
| 95 | + | 工具栏刷新 | `刷新` | | ||
| 96 | + | 工具栏新增 | `新增` | | ||
| 97 | + | 工具栏导出 | `导出Excel` | | ||
| 98 | + | 搜索按钮 | `搜索` | | ||
| 99 | + | 清空按钮 | `清空`(含原型 `⊗` 前缀,文案以「清空」为可定位文本) | | ||
| 100 | + | 空态文案 | `暂无匹配的用户`(BR14) | | ||
| 101 | + | 错误占位文案 | `加载失败,点击重试`(spec § 4) | | ||
| 102 | + | 分页统计文案 | `共 {total} 条记录`(`showTotal`,total 来自 `PageResult.total`,BR1/§ 3 success/empty) | | ||
| 103 | + | 导出成功提示 | `导出成功`(`message.success`,BR9) | | ||
| 104 | + | 导出失败提示 | `导出失败`(`message.error`,BR9) | | ||
| 105 | + | 42201 提示 | `分页参数有误,已重置为第 1 页`(`message.warning`,spec § 4) | | ||
| 106 | + | 40001 提示 | `查询条件有误,请检查后重试`(`message.error`,spec § 4) | | ||
| 107 | + | 网络/5xx 提示 | `加载失败,请稍后重试`(`message.error`,spec § 4) | | ||
| 108 | +- **路由 path(导航目标,FE-02 已注册)**:新增 → `/usr/users/new`(BR13);行双击修改 → `/usr/users/:id`(`navigate('/usr/users/' + row.id)`,BR12)。 | ||
| 109 | +- **localStorage token 键**:`TOKEN_STORAGE_KEY = 'xly_erp_token'`(FE-01 `request.ts` 已导出,本页不直接读写)。 | ||
| 110 | + | ||
| 111 | +## 关键签名(首次出现处给出,跨 task 一致) | ||
| 112 | + | ||
| 113 | +- **类型契约**(`api/types.ts`,新增;沿用 FE-01 既有 `AuthUser`/`LoginPayload` 等不动): | ||
| 114 | + - `UserVO`:`{ id: number; sUserName: string; employeeName: string | null; sUserNo: string | null; departmentName: string | null; sUserType: string; sLanguage: string; iIsVoid: number; tLastLoginDate: string | null; sCreator: string; tCreateDate: string }`。 | ||
| 115 | + > docs/05 `UserVO` 以中文键名 `员工名`/`部门` 给出(后端关联职员表派生);前端在 api 层做一次别名映射 `员工名→employeeName`、`部门→departmentName`,组件/列定义统一用 ASCII 键(spec D9 / D-PLAN-2)。映射在 `usrApi.listUsers` 内对 `records` 逐项归一。 | ||
| 116 | + - `PageResult<T>`:`{ records: T[]; total: number; pageNum: number; pageSize: number }`(docs/04 § 1.4 / § 3.2)。 | ||
| 117 | + - `UserListQuery`:`{ queryField?: string; matchType?: string; queryValue?: string; pageNum: number; pageSize: number }`(提交给 `GET /api/usr/users` 的 query;`queryValue` 空时省略,BR3)。 | ||
| 118 | +- **API 封装**(`api/usrApi.ts`,新增方法): | ||
| 119 | + - `listUsers(query: UserListQuery): Promise<PageResult<UserVO>>`——`request.get('/usr/users', { params })`,对返回 `records` 做中文键→ASCII 别名归一后返回(D-PLAN-2)。响应拦截器已拆 `Result.data`,故此方法返回 `PageResult<UserVO>` 本体(沿用 FE-01 `as unknown as Promise<...>` 桥接模式)。 | ||
| 120 | +- **页面 hook**(`pages/usr/UserList/useUserList.ts`): | ||
| 121 | + - `useUserList()` → `{ list: UserVO[]; total: number; loading: boolean; error: ApiError | null; query: UserListQuery; exporting: boolean; search(): void; refresh(): void; clear(): void; setQueryField(v: string): void; setMatchType(v: string): void; setQueryValue(v: string): void; changePage(pageNum: number, pageSize: number): void; exportExcel(): Promise<void>; }`。 | ||
| 122 | + - 挂载时以 `DEFAULT_QUERY` 取数(initialLoading,BR2);`search()` 回第 1 页以当前条件取数(BR7);`refresh()` 保持当前 `query`(含 `pageNum`)重取(BR8);`clear()` 重置为 `DEFAULT_QUERY` 后取数(BR10);`changePage()` 改页/页大小重取(改 pageSize 回第 1 页,BR11);取数成功同步 `total`/`pageNum`/`pageSize` 以响应回显(BR15);非 0 code 按错误码分流 `message`(42201 warning+重置重查、40001 error 保留条件不自动重查、网络兜底 error),并置 `error` 供错误占位(spec § 4)。 | ||
| 123 | + - `exportExcel()`:置 `exporting=true` → 调 `listUsers` 拉当前条件命中结果(按 `total` 在 `pageSize≤100` 约束内一次或分批取,D-PLAN-1)→ 生成 CSV Blob 下载 → `message.success('导出成功')` / 失败 `message.error('导出失败')` → `exporting=false`(BR9)。 | ||
| 124 | +- **导出工具**(`pages/usr/UserList/exportUtils.ts`): | ||
| 125 | + - `buildUserCsv(rows: UserVO[]): string`——按列定义顺序与中文表头生成 CSV 文本(含表头行,作废 0/1→否/是,空值→空串)。 | ||
| 126 | + - `downloadCsv(filename: string, csv: string): void`——前置 UTF-8 BOM(``)→ `new Blob` → `URL.createObjectURL` → 触发 `<a download>`(jsdom 下可桩 `URL.createObjectURL`/`a.click` 验证调用)。 | ||
| 127 | +- **组件 props**(`pages/usr/UserList/`): | ||
| 128 | + - `UserToolbar`(`{ onRefresh(): void; onAdd(): void; onExport(): void; exporting: boolean; loading?: boolean }`):刷新/新增/导出/齿轮;导出中 `导出Excel` 置 `loading` 且禁用(BR9);齿轮无动作(spec D7)。 | ||
| 129 | + - `UserFilterBar`(`{ query: UserListQuery; onChangeQueryField(v): void; onChangeMatchType(v): void; onChangeQueryValue(v): void; onSearch(): void; onClear(): void }`):渲染 `SCOPE_OPTIONS`/`QUERY_FIELD_OPTIONS`/`MATCH_TYPE_OPTIONS` 下拉 + 查询值 `Input`(`onPressEnter`→`onSearch`,BR7)+ 搜索/清空按钮;更多「▾」占位(spec D3)。 | ||
| 130 | + - `UserTable`(`{ rows: UserVO[]; loading: boolean; total: number; pageNum: number; pageSize: number; onChangePage(pageNum, pageSize): void; onRowDoubleClick(row: UserVO): void; selectedRowKey?: number | null; onSelectRow?(key: number | null): void }`):AntD `Table` `rowKey="id"`,`columns` 来自 `columns.tsx`,受控 `pagination={{ current:pageNum, pageSize, total, showSizeChanger:true, pageSizeOptions:PAGE_SIZE_OPTIONS, showTotal:(t)=>`共 ${t} 条记录` }}`,`rowSelection={{ type:'radio', selectedRowKeys, onChange }}`(spec D8),`onRow=(row)=>({ onDoubleClick: ()=>onRowDoubleClick(row) })`(BR12),空 `rows` → `locale.emptyText` 为 AntD `Empty`「暂无匹配的用户」(BR14)。 | ||
| 131 | + - `UserListPage`(default export,无 props):`useUserList()` + `useNavigate()`;装配 `UserToolbar`/`UserFilterBar`/`UserTable`;`onAdd`→`navigate('/usr/users/new')`(BR13);`onRowDoubleClick`→`navigate('/usr/users/'+row.id)`(BR12);错误态渲染错误占位 +「点击重试」→ `refresh()`(spec § 4)。 | ||
| 132 | +- **列定义**(`pages/usr/UserList/columns.tsx`): | ||
| 133 | + - `buildUserColumns(opts: { pageNum: number; pageSize: number }): ColumnsType<UserVO>`——返回上表 1~11 列(序号列用 `render:(_,__,index)=>(pageNum-1)*pageSize+index+1`,BR1;作废列 `render:(v)=> v===1 ? '是' : '否'`,只读 BR6)。 | ||
| 134 | + | ||
| 135 | +## 测试栈说明 | ||
| 136 | + | ||
| 137 | +- **jsdom 组件 / hook / api 单测**(Vitest + RTL):组件测用 `renderShell`(已存在,Provider + 真实 store + `MemoryRouter` + AntD `App`/`ConfigProvider`);导航断言用 `LocationProbe`(复用 `HomePage.test.tsx` 的 `useLocation` 探针模式)+ 路由哨兵(`/usr/users/new`、`/usr/users/:id` 哨兵元素)。`useUserList` 用 `renderHook`,对 `usrApi.listUsers` 用 `vi.mock('../../src/api/usrApi')` 桩(沿用 `usrApi.test.ts` 的 `vi.mock` 模式)。`listUsers` api 测桩底层 `request` 实例(同 `usrApi.test.ts`)。 | ||
| 138 | +- **Playwright E2E**:`page.route` 桩 `**/api/usr/users**`(先返回非空 `records` 验渲染/翻页/行双击;再用单独用例桩空 `records` 验空态、桩 5xx 验错误重试);先经登录桩落地外壳再进 `/usr/users`(沿用 `shell.spec.ts` 的 `stubBackend`/`login` 模式,可抽到本 spec 内)。不依赖真实后端起服。 | ||
| 139 | +- **可测性**:优先语义查询(role/text/label/columnheader);仅当无法稳定定位时加最小 `data-testid`(如 `user-table` / `filter-query-field` / `filter-match-type` / `filter-query-value` / `btn-search` / `btn-clear` / `btn-refresh` / `btn-export` / `userlist-error`)。 | ||
| 140 | + | ||
| 141 | +--- | ||
| 142 | + | ||
| 143 | +## 任务列表(每个 task = red → green → 子会话验证 → commit) | ||
| 144 | + | ||
| 145 | +> 硬护栏:以下每个 `impl_file` / `test_file` 均以 `frontend/` 开头;无任何 `backend/` / `sql/` / `scripts/` 落点。 | ||
| 146 | +> 提交 scope 统一 `usr`;REQ tag 统一 `REQ-USR-003`。 | ||
| 147 | + | ||
| 148 | +### T1 — 类型契约 + listUsers API 封装(GET /api/usr/users,中文键归一 D-PLAN-2)(jsdom api 测) | ||
| 149 | +- **测试先行类型**:jsdom 组件测试(api 单测) | ||
| 150 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/usrApi.userlist.test.ts`(沿用 `usrApi.test.ts` 的 `vi.mock('../../src/api/request')` 桩底层实例): | ||
| 151 | + - `::listUsers gets /usr/users with query params`——调 `listUsers({ queryField:'用户名', matchType:'包含', queryValue:'李', pageNum:2, pageSize:20 })`,断言 `request.get` 以 `'/usr/users'` + `{ params: { ...含传入字段 } }` 被调用。 | ||
| 152 | + - `::listUsers omits empty queryValue`——`queryValue:''` 时 params 不含 `queryValue`(或传空,按实现一处定,与 BR3 一致;测试断言后端不收到非空 queryValue)。 | ||
| 153 | + - `::listUsers normalizes chinese keys 员工名/部门 to employeeName/departmentName`——桩 `request.get` resolve `{ records:[{ id:1, sUserName:'a', 员工名:'张三', 部门:'技术', sUserNo:'a', sUserType:'超级管理员', sLanguage:'中文', iIsVoid:0, tLastLoginDate:null, sCreator:'x', tCreateDate:'t' }], total:1, pageNum:2, pageSize:20 }`,断言返回 `records[0].employeeName==='张三'`、`departmentName==='技术'`(D-PLAN-2)。 | ||
| 154 | +- [ ] **2. 实现最小代码**:`frontend/src/api/types.ts`(新增 `UserVO`/`PageResult<T>`/`UserListQuery`,签名见关键签名)+ `frontend/src/api/usrApi.ts`(新增 `listUsers`,`request.get('/usr/users',{params})` + records 中文键归一)。 | ||
| 155 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- usrApi`(含 FE-01 `usrApi` 既有用例不回归) | ||
| 156 | +- [ ] **4. commit**:`feat(usr): 用户列表查询 API 与类型契约 listUsers REQ-USR-003` | ||
| 157 | + | ||
| 158 | +### T2 — 页面常量 + 导出工具(枚举/默认/pageSize/文案 + CSV 下载,D-PLAN-1)(jsdom 单测) | ||
| 159 | +- **测试先行类型**:jsdom 组件测试(纯逻辑断言) | ||
| 160 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/UserToolbar.test.tsx` 暂不依赖此,单独建 `frontend/tests/unit/exportUtils.test.ts`: | ||
| 161 | + - `::buildUserCsv has header row and maps 作废 0/1`——传 2 行(`iIsVoid:0` 与 `1`),断言 CSV 首行含中文表头(`用户名`/`员工名`/`作废` 等逐字),数据行作废列分别为 `否`/`是`,空值字段为空串。 | ||
| 162 | + - `::downloadCsv triggers blob download with UTF-8 BOM`——桩 `URL.createObjectURL`(vi.fn)与 `HTMLAnchorElement.prototype.click`,调 `downloadCsv('users.csv', 'x')`,断言 `createObjectURL` 收到的 Blob 内容以 `` 开头且 `click` 被调一次。 | ||
| 163 | + - (常量断言并入此文件或 `useUserList.test.tsx`,TDD 期定一处):`DEFAULT_QUERY.queryField==='用户名'`、`matchType==='包含'`、`pageSize===10`、`PAGE_SIZE_OPTIONS` 末项为 `100`、`QUERY_FIELD_OPTIONS` 含 8 项首项 `用户名`、`MATCH_TYPE_OPTIONS` 为 `['包含','不包含','等于']`。 | ||
| 164 | +- [ ] **2. 实现最小代码**:`frontend/src/pages/usr/UserList/constants.ts`(枚举/默认/pageSize/错误码/文案常量,见合同级常量)+ `frontend/src/pages/usr/UserList/exportUtils.ts`(`buildUserCsv`/`downloadCsv`,签名见关键签名)。 | ||
| 165 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- exportUtils` | ||
| 166 | +- [ ] **4. commit**:`feat(usr): 用户列表页面常量与前端 CSV 导出工具 REQ-USR-003` | ||
| 167 | + | ||
| 168 | +### T3 — useUserList 列表查询 hook(状态机:initialLoading/loading/success/empty/error/exporting)(jsdom hook 测) | ||
| 169 | +- **测试先行类型**:jsdom 组件测试(`renderHook`,`vi.mock('../../src/api/usrApi')`) | ||
| 170 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/useUserList.test.tsx`: | ||
| 171 | + - `::mounts with default query and loads first page (initialLoading→success)`——`listUsers` resolve 非空 `records`,断言挂载即以 `DEFAULT_QUERY` 调用 `listUsers`、`loading` 由 true→false、`list`/`total`/`query.pageNum` 同步(BR2/§ 3 initialLoading/success)。 | ||
| 172 | + - `::empty records sets empty state without error`——resolve `records:[]`,`total:0`,断言 `list` 为空、`error===null`、无 `message.error`(BR14/§ 3 empty)。 | ||
| 173 | + - `::search resets to page 1 and refetches with current filters`——改 `queryField`/`queryValue` 后 `search()`,断言以 `pageNum:1` + 当前条件再次调用(BR7)。 | ||
| 174 | + - `::refresh keeps current query and page`——先翻到第 2 页,`refresh()` 断言以**当前** `pageNum`(2)+ 当前条件调用,不回第 1 页、不重置条件(BR8)。 | ||
| 175 | + - `::clear resets to DEFAULT_QUERY then refetches`——改条件后 `clear()`,断言 `query` 回 `DEFAULT_QUERY`(用户名/包含/空/page1)并以之取数(BR10)。 | ||
| 176 | + - `::changePage refetch; changing pageSize resets to page 1`——`changePage(3, 10)` 以 pageNum=3 取;`changePage(1, 50)`(改 pageSize)回第 1 页取(BR11)。 | ||
| 177 | + - `::ApiError 40001 keeps filters and shows error, sets error state`——`listUsers` reject `new ApiError(40001,...)`,断言 `error` 置位、`query` 未变、不自动重查(spec § 4 / 40001)。 | ||
| 178 | + - `::ApiError 42201 warns and refetches at page 1`——reject `ApiError(42201)`,断言重置 `pageNum=1`(与 pageSize 收敛)后重查(spec § 4 / 42201 兜底)。 | ||
| 179 | + - `::network error (code -1) sets error state`——reject `ApiError(-1)`,断言 `error` 置位(错误占位由页面消费,spec § 4)。 | ||
| 180 | + - `::exportExcel toggles exporting and downloads`——桩 `listUsers` 返回结果集 + 桩 `downloadCsv`,调 `exportExcel()`,断言 `exporting` true→false、`downloadCsv` 被调(BR9/§ 3 exporting);reject 时 `message.error('导出失败')`。 | ||
| 181 | + > 取数响应回显(BR15):success 用例额外断言 `query.pageNum`/`pageSize` 跟随响应 `PageResult` 同步(即使请求 pageNum 越界,前端信任响应回显)。`message` 断言可桩 `antd` `App.useApp().message` 或 `vi.mock('antd', ...)` 暴露 `message`,TDD 期定一处。 | ||
| 182 | +- [ ] **2. 实现最小代码**:`frontend/src/pages/usr/UserList/useUserList.ts`(签名见关键签名;内部 `useState` 持 `query`/`list`/`total`/`loading`/`error`/`exporting`,`useEffect` 挂载取数;错误码分流按合同级常量;导出复用 T2 `exportUtils`)。 | ||
| 183 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- useUserList` | ||
| 184 | +- [ ] **4. commit**:`feat(usr): 用户列表查询 hook useUserList 状态机 REQ-USR-003` | ||
| 185 | + | ||
| 186 | +### T4 — UserFilterBar 筛选栏(默认值/枚举 options/回车搜索/清空,BR2/BR3/BR4/BR7/BR10/D2/D3)(jsdom 组件测) | ||
| 187 | +- **测试先行类型**:jsdom 组件测试 | ||
| 188 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/UserFilterBar.test.tsx`(`renderShell` 提供 AntD App 上下文): | ||
| 189 | + - `::renders defaults 用户名 / 包含 and empty value`——传 `query=DEFAULT_QUERY`,断言查询字段下拉显「用户名」、匹配方式显「包含」、查询值框为空(BR2)。 | ||
| 190 | + - `::query field options match enum`——展开查询字段下拉,断言 8 个选项逐字一致(用户名…制单人,BR4);展开匹配方式断言 `包含/不包含/等于`(BR4)。 | ||
| 191 | + - `::scope select shows 全部用户 only`——范围下拉仅「全部用户」(占位 demo,D2)。 | ||
| 192 | + - `::Enter in value triggers onSearch`——查询值框聚焦回车 → `onSearch` 被调一次(BR7)。 | ||
| 193 | + - `::click 搜索 calls onSearch / click 清空 calls onClear`——点「搜索」→ `onSearch`;点「清空」→ `onClear`(BR7/BR10)。 | ||
| 194 | + - `::changing selects/value calls respective onChange`——改下拉/输入触发 `onChangeQueryField`/`onChangeMatchType`/`onChangeQueryValue`。 | ||
| 195 | + - `::more toggle ▾ is placeholder (no extra callback)`——「▾」点击不触发查询回调(占位,D3)。 | ||
| 196 | +- [ ] **2. 实现最小代码**:`frontend/src/pages/usr/UserList/UserFilterBar.tsx`(签名见关键签名;options 来自 T2 常量;`Input` `onPressEnter`→`onSearch`)+ `UserList.module.css` 筛选栏样式(语义色用 `var(--color-*)`:白底 `--color-form-bg-edit`、下边线 `--color-border`、搜索按钮主色 `--color-primary`)。 | ||
| 197 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- UserFilterBar` | ||
| 198 | +- [ ] **4. commit**:`feat(usr): 用户列表筛选栏 UserFilterBar REQ-USR-003` | ||
| 199 | + | ||
| 200 | +### T5 — UserTable 表格(列/序号/作废只读/行双击/单选/受控分页/空态,BR1/BR6/BR11/BR12/BR14/D8)(jsdom 组件测) | ||
| 201 | +- **测试先行类型**:jsdom 组件测试 | ||
| 202 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/UserTable.test.tsx`(`renderShell`): | ||
| 203 | + - `::renders 11 column headers in order`——断言列头文案逐字、顺序与合同级常量列表一致(序号…制单日期)。 | ||
| 204 | + - `::serial number is page-aware`——传 `pageNum=2, pageSize=10` + 2 行,断言首行序号为 `11`(`(2-1)*10+0+1`,BR1)。 | ||
| 205 | + - `::作废 column renders 否/是 read-only`——`iIsVoid:0`→「否」、`1`→「是」;点击作废单元不触发任何 onChange/写动作(BR6)。 | ||
| 206 | + - `::double click row navigates via onRowDoubleClick`——双击某数据行 → `onRowDoubleClick` 收到该行(含 `id`,BR12)。 | ||
| 207 | + - `::controlled pagination reflects current/pageSize/total + showTotal`——传 `total=37,pageNum=1,pageSize=10`,断言分页显示「共 37 条记录」、当前页 1;点下一页/改每页条数 → `onChangePage` 收到新 `(pageNum,pageSize)`(改 pageSize 期望回第 1 页由页面/hook 处理,本组件原样上报,BR11)。 | ||
| 208 | + - `::empty rows shows Empty 暂无匹配的用户`——`rows=[]` → 渲染「暂无匹配的用户」(BR14)。 | ||
| 209 | + - `::radio rowSelection single-select does not affect query`——选中某行 → `onSelectRow` 收到该行 key,不触发取数/查询(spec D8;本组件不持查询态,断言仅回调)。 | ||
| 210 | +- [ ] **2. 实现最小代码**:`frontend/src/pages/usr/UserList/columns.tsx`(`buildUserColumns`,序号/作废 render)+ `frontend/src/pages/usr/UserList/UserTable.tsx`(AntD `Table` 受控分页/rowSelection/onRow 双击/空态 locale)+ `UserList.module.css` 表格样式(表头 `--color-table-header-bg`/`--color-table-header-fg`、行文字 `--color-table-row-fg`、hover `--color-table-row-bg-hover`、选中 `--color-table-row-bg-selected`、网格线 `--color-border`)。 | ||
| 211 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- UserTable` | ||
| 212 | +- [ ] **4. commit**:`feat(usr): 用户列表表格 UserTable 与列定义 REQ-USR-003` | ||
| 213 | + | ||
| 214 | +### T6 — UserToolbar 工具栏(刷新/新增导航/导出中禁用/齿轮占位,BR8/BR9/BR13/D7/D10)(jsdom 组件测) | ||
| 215 | +- **测试先行类型**:jsdom 组件测试 | ||
| 216 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/UserToolbar.test.tsx`(`renderShell`): | ||
| 217 | + - `::renders 刷新/新增/导出Excel/设置 buttons`——可见四个工具项(文案逐字 + 对应图标)。 | ||
| 218 | + - `::click 刷新 calls onRefresh / click 新增 calls onAdd`——点刷新→`onRefresh`;点新增→`onAdd`(BR8/BR13)。 | ||
| 219 | + - `::click 导出Excel calls onExport`——点导出→`onExport`(BR9)。 | ||
| 220 | + - `::exporting disables 导出Excel and shows loading`——`exporting=true` 时「导出Excel」禁用且 loading,再点不再触发 `onExport`(BR9)。 | ||
| 221 | + - `::gear setting is placeholder (no callback)`——点齿轮无任何业务回调(占位,D7)。 | ||
| 222 | +- [ ] **2. 实现最小代码**:`frontend/src/pages/usr/UserList/UserToolbar.tsx`(签名见关键签名;图标 `ReloadOutlined`/`PlusCircleOutlined`/`FileExcelOutlined`/`SettingOutlined`)+ `UserList.module.css` 工具栏深色底(scoped 局部装饰,非语义 token,D10)。 | ||
| 223 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- UserToolbar` | ||
| 224 | +- [ ] **4. commit**:`feat(usr): 用户列表工具栏 UserToolbar REQ-USR-003` | ||
| 225 | + | ||
| 226 | +### T7 — UserListPage 页面集成 + 路由接线(状态机贯通 + 导航 + 错误重试,BR3/BR7/BR8/BR9/BR12/BR13/BR15)(jsdom 组件测) | ||
| 227 | +- **测试先行类型**:jsdom 组件测试 | ||
| 228 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/UserListPage.test.tsx`(`renderShell`,`vi.mock('../../src/api/usrApi')` 桩 `listUsers`;用 `LocationProbe` + `/usr/users/new`、`/usr/users/:id` 哨兵路由验导航): | ||
| 229 | + - `::initial load renders rows from listUsers (default query)`——挂载即调 `listUsers(DEFAULT_QUERY)`、渲染返回行(BR2/initialLoading→success)。 | ||
| 230 | + - `::search with value submits queryValue and shows results`——填查询值「李」点搜索 → `listUsers` 以 `queryValue:'李', pageNum:1` 调用、渲染结果(BR7/BR3)。 | ||
| 231 | + - `::empty response shows 暂无匹配的用户`——桩空 `records` → 空态(BR14)。 | ||
| 232 | + - `::error response shows error placeholder with 点击重试; retry calls refresh`——桩 reject `ApiError(-1)` → 可见「加载失败,点击重试」;点重试再次取数(spec § 4)。 | ||
| 233 | + - `::新增 navigates to /usr/users/new`——点工具栏「新增」→ URL/哨兵到 `/usr/users/new`(BR13)。 | ||
| 234 | + - `::double click row navigates to /usr/users/:id`——桩返回含 `id` 的行,双击 → URL/哨兵到 `/usr/users/{id}`(BR12)。 | ||
| 235 | + - `::refresh keeps current page`——翻到第 2 页后点刷新 → `listUsers` 以 `pageNum:2` 调用(BR8)。 | ||
| 236 | + - `::response pageNum echo syncs pagination`——请求越界 `pageNum`、桩响应回 `pageNum=最后一页`,断言分页当前页跟随响应(BR15)。 | ||
| 237 | +- [ ] **2. 实现最小代码**:`frontend/src/pages/usr/UserList/index.tsx`(`UserListPage`:`useUserList` + `useNavigate`,装配 `UserToolbar`/`UserFilterBar`/`UserTable`,错误态渲染「加载失败,点击重试」→`refresh`,新增/行双击导航);改 `frontend/src/router/index.tsx`——把 `/usr/users` 的 `UserListPlaceholder` 替换为 `UserListPage`(移除占位组件,import 真实页;`/usr/users/new`、`/usr/users/:id` 仍为 FE-04 占位,不动)。 | ||
| 238 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- UserListPage router`(确认 FE-02 `router.test.tsx` 不回归——若其断言 `/usr/users` 渲染占位 `data-testid`,需同步更新该断言为真实页可定位元素,并在 commit 说明) | ||
| 239 | +- [ ] **4. commit**:`feat(usr): 用户列表页面集成与路由接线 UserListPage REQ-USR-003` | ||
| 240 | + | ||
| 241 | +### T8 — E2E 用户列表关键旅程(Playwright) | ||
| 242 | +- **测试先行类型**:Playwright E2E | ||
| 243 | +- [ ] **1. 写失败测试**:`frontend/tests/e2e/userlist.spec.ts`(沿用 `shell.spec.ts` 的登录桩;`page.route('**/api/usr/users**')` 按用例返回不同体): | ||
| 244 | + - `::enter user list renders rows`——桩非空 `records`,从主页「常用操作 > 用户列表」进入 `/usr/users`,断言表格出现某用户名行 + 分页「共 N 条记录」。 | ||
| 245 | + - `::empty result shows 暂无匹配的用户`——桩空 `records`,断言空态文案。 | ||
| 246 | + - `::search by value triggers query`——填查询值点「搜索」,断言请求携带 `queryValue`(用 `page.waitForRequest('**/api/usr/users**')` 校验 URL 含 `queryValue`)并渲染结果。 | ||
| 247 | + - `::pagination next page refetches`——桩 `total>pageSize`,点下一页,断言请求 `pageNum=2`。 | ||
| 248 | + - `::double click row navigates to user detail`——双击行 → URL 到 `/usr/users/{id}`(FE-04 占位即可,本 FE 不验单据内容)。 | ||
| 249 | + - `::error response shows retry`——桩 5xx,断言「加载失败,点击重试」可见;点重试(改桩为成功)后渲染行。 | ||
| 250 | +- [ ] **2. 实现最小代码**:补任何为可测性需要的最小 `data-testid`(仅 Playwright 无法稳定定位时,如 `user-table`/`btn-search`/`btn-refresh`/`userlist-error`)。沿用 `playwright.config.ts`。 | ||
| 251 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:e2e -- userlist` | ||
| 252 | +- [ ] **4. commit**:`test(usr): 用户列表 E2E 关键旅程 REQ-USR-003` | ||
| 253 | + | ||
| 254 | +### T9 — 全量门禁回归 + 收尾(chore) | ||
| 255 | +- **测试先行类型**:无新增测试(全量验证) | ||
| 256 | +- [ ] **1. 写失败测试**:无。 | ||
| 257 | +- [ ] **2. 实现最小代码**:修 lint / build(`tsc --noEmit`)/ 类型问题;确认语义色全部 `var(--color-*)`、无硬编码 hex/rgba(工具栏深色装饰 scoped 例外,D10);确认无 `TBD/TODO/【人工填写】`;确认 FE-01/FE-02 既有单测/E2E 不回归(尤其 `router.test.tsx`、`shell.spec.ts`)。 | ||
| 258 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run lint && npm run build && npm run test:unit && npm run test:e2e` 全绿。 | ||
| 259 | +- [ ] **4. commit**:`chore(usr): FE-03 门禁回归通过 REQ-USR-003` | ||
| 260 | + | ||
| 261 | +--- | ||
| 262 | + | ||
| 263 | +## 完成判据(Definition of Done) | ||
| 264 | + | ||
| 265 | +1. `/usr/users` 渲染真实 `UserListPage`(复刻原型 `#screen-userlist` 的工具栏/筛选栏/用户表格/分页),数据/筛选/分页真实对接 `GET /api/usr/users`(spec § 1/§ 2 / D1)。 | ||
| 266 | +2. 状态机覆盖并测试固化:`initialLoading`(T3/T7)、`loading`(T3)、`success`(T3/T5/T7)、`empty`(T3/T5/T7)、`error`(T3/T7)、`exporting`(T3/T6)(spec § 3)。 | ||
| 267 | +3. 业务规则 BR1~BR15 在 hook/组件/E2E 有断言:BR1(T5)、BR2(T3/T4)、BR3(T3/T7)、BR4(T4)、BR5(只读:全程仅 GET,T1/T3)、BR6(T5)、BR7(T3/T4/T7)、BR8(T3/T6/T7)、BR9(T3/T6)、BR10(T3/T4)、BR11(T3/T5)、BR12(T5/T7/T8)、BR13(T6/T7/T8)、BR14(T3/T5/T7/T8)、BR15(T3/T7)(spec § 5)。 | ||
| 268 | +4. 列定义/字段映射对齐 REQ 输出表 1 + 原型 thead + `UserVO`(11 列顺序/表头/作废只读,spec § 6 / D9);`员工名`/`部门` 中文键在 api 层归一为 `employeeName`/`departmentName`(D-PLAN-2)。 | ||
| 269 | +5. API 经 `usrApi.listUsers` → `request.ts`(拆 `Result`、`ApiError` 分流、被动 401 统一登出复用 FE-02),页面不散用 axios(docs/04 § 2.3);列表态在页面 hook 不进 Redux(docs/04 § 2.2 / spec D6)。 | ||
| 270 | +6. 错误码分流文案对齐 spec § 4(42201 warning+重置重查、40001 error 保留条件、网络兜底 error、空态不报错)。 | ||
| 271 | +7. 导出为前端零依赖 CSV(UTF-8 BOM Blob 下载),不杜撰后端导出端点、不新增 npm 依赖(spec D5 / D-PLAN-1)。 | ||
| 272 | +8. 语义色只用 `var(--color-*)`,AntD `colorPrimary` 沿用 FE-01 `ConfigProvider`;工具栏深色底 scoped 装饰不新增全局 token、不挪用语义 token(spec § 7 / D10)。 | ||
| 273 | +9. 全部落点在 `frontend/**`,无 `backend/` / `sql/` / `scripts/` 改动;改 `router/index.tsx`(占位换真实页)、`usrApi.ts`/`types.ts`(增列表契约)属共享骨架,已在《模块完成报告》留痕。 | ||
| 274 | +10. 门禁全绿:`npm run lint` / `npm run build` / `npm run test:unit` / `npm run test:e2e`(docs/04 § 零)。 | ||
| 275 | + | ||
| 276 | +## 自审记录 | ||
| 277 | + | ||
| 278 | +- **占位符扫描**:本计划无 `【人工填写:】` / `TBD` / `TODO` 真实占位(正文 `TBD/TODO` 仅作为「禁止出现的字样」被引用)。 | ||
| 279 | +- **spec coverage**:§ 1 关联 REQ/原型/路由/落地目录 → 架构 + T1/T7(路由接线);§ 2 组件树 → T4(FilterBar)/T5(Table+列)/T6(Toolbar)/T7(Page 装配);§ 3 状态机 6 态 → 见 DoD 第 2 条;§ 4 端点/错误码 → T1(api)/T3(错误码分流)/T7(错误占位重试);§ 5 BR1-BR15 → 见 DoD 第 3 条逐条映射;§ 6 列定义/字段映射 → T5/columns + T1(归一);§ 7 tokens → T4/T5/T6/T9;§ 8 decisions D1-D10 已落实于架构/合同级常量/任务(D1 真实对接=全任务;D2 范围占位=T4;D3 更多占位=T4;D4 pageSize=T2/T5;D5 导出=T2/T3/T6 → 细化为 D-PLAN-1;D6 本地 hook=T3;D7 齿轮占位=T6;D8 单选=T5;D9 中文键=T1 → 细化为 D-PLAN-2;D10 工具栏深色=T6)。本计划新增/细化 D-PLAN-1、D-PLAN-2 见下。 | ||
| 280 | +- **本计划细化决策**: | ||
| 281 | + - **D-PLAN-1(导出实现具体化 spec D5)**:spec D5 允许「用前端库(如 xlsx/SheetJS)」且为 medium 置信度并写明「实现期如后端补导出端点可切换」。经核 `frontend/package.json`,**项目未声明 `xlsx`/SheetJS 依赖**,且现有前端无新增 npm 依赖先例;为不引入新依赖、不杜撰后端端点,采用**零依赖 CSV 导出**(拉当前条件命中结果 → UTF-8 BOM CSV → Blob + `<a download>` 下载),文件名 `用户列表.csv`。导出语义(导出当前查询命中结果、过程禁用按钮、成功/失败提示)与 BR9 完全一致。置信度 medium。 | ||
| 282 | + - **D-PLAN-2(中文键归一具体化 spec D9)**:spec D9 允许「在 api 层做一次到 `employeeName`/`departmentName` 的别名映射后供组件用」。本计划锁定该路径:`usrApi.listUsers` 内对 `PageResult.records` 逐项把 `员工名→employeeName`、`部门→departmentName` 归一,组件/列定义统一用 ASCII 键(避免 TS 中文键访问与 lint 摩擦);列渲染语义与 docs/05 契约不变。置信度 high。 | ||
| 283 | +- **类型一致性**:`UserVO`(ASCII 键,跨 T1/T3/T5/T7 一致)、`PageResult<T>`、`UserListQuery`、`DEFAULT_QUERY`、`QUERY_FIELD_OPTIONS`/`MATCH_TYPE_OPTIONS`/`SCOPE_OPTIONS`/`PAGE_SIZE_OPTIONS`、错误码常量(42201/40001/-1)、列表表头文案与顺序、`listUsers`/`useUserList`/`UserToolbar`/`UserFilterBar`/`UserTable`/`buildUserColumns`/`buildUserCsv`/`downloadCsv` 签名跨 T1-T9 一致;路由 path(`/usr/users` / `/usr/users/new` / `/usr/users/:id`)与 FE-02 合同级常量一致;`ApiError`/`TOKEN_STORAGE_KEY`/`NETWORK_ERROR_CODE` 复用 FE-01 `request.ts`。 | ||
| 284 | +- **作用域自审**:所有 `impl_file` / `test_file` 均以 `frontend/` 开头;无 `backend/` / `sql/` / `scripts/` 落点。改 `src/router/index.tsx`、`src/api/usrApi.ts`、`src/api/types.ts` 属 FE 共享骨架扩展(非新阶段越界),已在架构段与 DoD 第 9 条登记留痕要求。 |
docs/superpowers/plans/2026-06-01-FE-04.md
0 → 100644
| 1 | +# FE-04 用户信息单据 — 任务级 TDD 计划(前端) | ||
| 2 | + | ||
| 3 | +> 阶段:前端(frontend)。作用域:`frontend/**`(页面 / api / 类型 / 样式 / 测试)。**禁止**写 `backend/**` / `sql/**` / `scripts/**`。 | ||
| 4 | +> 上游 SSoT:spec `docs/superpowers/specs/2026-06-01-FE-04.md`;需求卡片 `docs/01-需求清单/USR-用户管理/REQ-USR-001.md`(增加用户)+ `REQ-USR-002.md`(修改用户);API 契约 `docs/05-API接口契约.md § REQ-USR-001`(`POST /api/usr/users`)+ `§ REQ-USR-002`(`PUT /api/usr/users/{id}`)+ `§ REQ-USR-003`(`GET /api/usr/users`,编辑预填复用);原型 `prototype/erp.html` → `<section id="screen-userdetail">`(`.toolbar` / `.form-grid` / `.tabs-row` / `.perm-list` + 脚本 `setUserDetailMode('new')`,布局与交互权威);技术规范 `docs/04-技术规范.md § 零 / § 2.1 / § 2.2 / § 2.3 / § 2.4 / § 3.2`;Design Tokens 仓库根 `src/styles/tokens.css`。 | ||
| 5 | +> 复用资产:FE-01(`api/request.ts` 已建 Axios 实例 + Result 拆包 + `ApiError` + `TOKEN_STORAGE_KEY` + `NETWORK_ERROR_CODE`;`api/types.ts`;`api/usrApi.ts` 含 `login`/`fetchCompanies`/`listUsers`)、FE-02(`AppLayout` 外壳 + `RequireAuth` 守卫 + 标签栈 `useTabStack`,已注册 `userdetail` 标签「用户信息单据」routePath `/usr/users/new`;路由表 `router/index.tsx` 已挂 `/usr/users/new` 与 `/usr/users/:id` 占位 `UserDetailPlaceholder`)、FE-03(`api/usrApi.ts` 的 `listUsers` + `api/types.ts` 的 `UserVO`/`PageResult`/`UserListQuery`;列表入口「新增」→ `/usr/users/new`、行双击 → `/usr/users/:id`)。 | ||
| 6 | +> 本计划告诉 TDD 执行者**做什么 / 文件边界 / 测试意图 / props 与类型契约 / 完成判据**;具体代码由红-绿-提交循环产出,不在此 dump 整组件 / 类型文件内容。 | ||
| 7 | +> **本 FE 同时承载新增(`POST`)与修改(`PUT`)两种写场景**,共用同一单据组件按 `mode` 分支;前端只做字段采集 + 轻量前置校验 + 写提交 + 反馈回流。用户名唯一性/格式终判、枚举与外键存在性、权限多对多写入与全量覆盖、密码 BCrypt 初始化、管理员权限、审计字段生成等真伪裁决全部在后端(spec § 5 末注)。 | ||
| 8 | + | ||
| 9 | +--- | ||
| 10 | + | ||
| 11 | +## Goal(目标) | ||
| 12 | + | ||
| 13 | +把 FE-02 在 `router/index.tsx` 留下的 `/usr/users/new` 与 `/usr/users/:id` 占位(`UserDetailPlaceholder`)替换为真实「用户信息单据」页面,复刻原型 `#screen-userdetail` 的布局与交互语义(工具栏 / 3 列表单网格 / 权限页签条 / 权限分类勾选列表),按路由判定 `create`/`edit` 模式,数据/下拉/权限/提交真实对接后端: | ||
| 14 | + | ||
| 15 | +- **页面容器 `UserDetailPage`**(路由 `/usr/users/new` 与 `/usr/users/:id` 共用,渲染于 FE-02 `AppLayout` 的 `<Outlet/>` 内):按路由 `:id` 判定 `mode`(有 `:id` → `edit`,`/new` → `create`);纵向组合 `UserDetailToolbar` + `UserBasicForm` + `PermissionTabs` + `PermissionGroupList`;态由页面本地 hook `useUserDetail` 持有(spec § 8 D7,不进 Redux)。 | ||
| 16 | +- **API 封装**:`api/usrApi.ts` 新增 `createUser` / `updateUser` / `getUserDetail` / `listEmployees` / `listPermissions`(页面只调封装方法,不散用 axios,docs/04 § 2.3);`api/types.ts` 新增 `UserCreateReq` / `UserUpdateReq` / `EmployeeOption` / `PermissionItem` 契约(复用 FE-03 `UserVO`)。 | ||
| 17 | +- **状态机 ≥6 态**(spec § 3):`initialLoading`(挂载预取员工/权限 + edit 取详情)/ `editing` / `submitting` / `submitError` / `submitSuccess` / `loadError`,均有测试固化。 | ||
| 18 | +- **业务规则 BR1~BR17** 在组件层 / hook 层 / E2E 有断言(spec § 5)。 | ||
| 19 | +- **工具栏**:保存(校验通过后 create→`POST` / edit→`PUT`,BR12)/ 取消(有未保存改动二次确认后回 `/usr/users`,BR13 / D5)/ 新增(切 create 模式 → `navigate('/usr/users/new')`,BR14)/ 占位按钮(删除/重置密码/功能 → `message.info("功能开发中")`,作废/取消作废 → edit 态以 `iIsVoid` 启停用开关承载,D8)/ 设置齿轮(占位,D8)。 | ||
| 20 | +- **表单网格**:创建时间(只读 BR1)/ 制单人(只读 BR2)/ 员工名(`Select`,options 来自 `GET /api/usr/employees`,选中联动带出用户号/用户名 BR5)/ 用户名(`Input`,create 必填 + 3-20 位字母数字下划线前置校验,edit 只读 BR3)/ 类型(`Select` 枚举,create 默认普通用户 BR6)/ 语言(`Select` 枚举 BR7)/ 用户号(`Input` 必填 BR4)/ 单据修改权限(`Checkbox` 默认否 BR8);密码不在 UI(BR9)。 | ||
| 21 | +- **权限页签 + 权限分类列表**:`Tabs`(仅「权限组」有内容,其余 5 个查看权限页签占位 D9)+ `Checkbox.Group` 列表(项来自 `GET /api/usr/permissions`,勾选集合 → `permissionIds`,edit 按已授权回勾、全量覆盖语义 BR10/BR11,表头全选 `indeterminate` 半选)。 | ||
| 22 | +- **提交反馈与回流**:`code=0` → `message.success`(create:「用户创建成功」/ edit:「保存成功」)→ `navigate('/usr/users')` 回流列表(BR16);非 0 按 § 4 错误码表反馈(40901 用户名冲突就近高亮、40401 用户不存在给返回列表入口、40001/40301/网络兜底,spec § 4);被动 401 由 `request.ts` 拦截器统一跳 `/login`(本页不重复处理)。 | ||
| 23 | +- **edit 预填**:按路由 `:id` 复用 `GET /api/usr/users`(等于匹配 + pageSize=1)定位取 `records[0]` 作原值回填(D4);若 FE-03 行双击携带 `location.state.user` 则优先用之免二次请求(D4 备注)。 | ||
| 24 | +- **语义色只用 `var(--color-*)`**;工具栏深色底为页面局部装饰,scoped 保留,不新增全局 token、不挪用语义 token(spec § 7 / D10)。 | ||
| 25 | + | ||
| 26 | +## Architecture(架构 / 分层) | ||
| 27 | + | ||
| 28 | +遵循 `docs/04 § 2.1`,落点全在 `frontend/**`。**新增/改动**文件: | ||
| 29 | + | ||
| 30 | +``` | ||
| 31 | +frontend/ | ||
| 32 | +├── src/ | ||
| 33 | +│ ├── api/types.ts # 【改】新增 UserCreateReq / UserUpdateReq / EmployeeOption / PermissionItem(复用 FE-03 UserVO;不破坏既有类型) | ||
| 34 | +│ ├── api/usrApi.ts # 【改】新增 createUser / updateUser / getUserDetail / listEmployees / listPermissions(不改 FE-01/FE-03 既有方法) | ||
| 35 | +│ ├── pages/usr/UserDetail/index.tsx # 【新增】UserDetailPage 页面容器:判 mode、装配 4 子组件、持 useUserDetail 态、提交反馈与导航回流 | ||
| 36 | +│ ├── pages/usr/UserDetail/useUserDetail.ts # 【新增】单据 hook(mode/formValues/employees/permissions/checkedPermissionIds/loading/submitting/error + 动作) | ||
| 37 | +│ ├── pages/usr/UserDetail/UserDetailToolbar.tsx # 【新增】深色工具条:保存/取消/新增 + 占位按钮(删除/作废/重置密码/取消作废/功能)+ 齿轮 | ||
| 38 | +│ ├── pages/usr/UserDetail/UserBasicForm.tsx # 【新增】AntD Form 3 列表单网格:8 个字段 + 员工联动 + 前置校验 | ||
| 39 | +│ ├── pages/usr/UserDetail/PermissionTabs.tsx # 【新增】AntD Tabs 权限页签条(权限组 active + 5 占位页签 D9) | ||
| 40 | +│ ├── pages/usr/UserDetail/PermissionGroupList.tsx # 【新增】权限分类勾选列表:表头全选(indeterminate) + 逐项 Checkbox(D3) | ||
| 41 | +│ ├── pages/usr/UserDetail/constants.ts # 【新增】合同级常量:类型/语言枚举、create 默认值、错误码、文案、用户名正则、占位文案 | ||
| 42 | +│ └── pages/usr/UserDetail/UserDetail.module.css # 【新增】页面 scoped 样式:语义色用 var(--color-*);工具栏深色底局部装饰(D10) | ||
| 43 | +├── src/router/index.tsx # 【改】把 /usr/users/new 与 /usr/users/:id 占位 UserDetailPlaceholder 替换为真实 UserDetailPage(属 FE 共享骨架,留痕) | ||
| 44 | +└── tests/ | ||
| 45 | + ├── unit/usrApi.userdetail.test.ts # 【新增】createUser/updateUser/getUserDetail/listEmployees/listPermissions 透传与归一(沿用 usrApi.userlist.test.ts 桩模式) | ||
| 46 | + ├── unit/useUserDetail.test.tsx # 【新增】hook 状态机:create/edit 初始化、员工联动、提交成功/失败、loadError、权限回勾(renderHook) | ||
| 47 | + ├── unit/UserBasicForm.test.tsx # 【新增】字段渲染/默认值/只读规则/枚举/必填+格式校验/员工联动(BR1-BR9) | ||
| 48 | + ├── unit/PermissionGroupList.test.tsx # 【新增】列表渲染/勾选集合/全选 indeterminate/edit 回勾(BR10/BR11/D3) | ||
| 49 | + ├── unit/PermissionTabs.test.tsx # 【新增】权限组 active + 5 占位页签 disabled/空态(D9) | ||
| 50 | + ├── unit/UserDetailToolbar.test.tsx # 【新增】保存/取消/新增回调 + 提交中禁用 + 占位按钮(BR12/BR13/BR14/BR15/D8) | ||
| 51 | + ├── unit/UserDetailPage.test.tsx # 【新增】页面集成:create/edit 贯通、提交回流、错误就近、取数失败(状态机 + BR3/BR12/BR16/BR17) | ||
| 52 | + └── e2e/userdetail.spec.ts # 【新增】E2E:新增提交回流 / 编辑预填改保存 / 用户名冲突就近 / 取数失败重试 | ||
| 53 | +``` | ||
| 54 | + | ||
| 55 | +- **跨阶段/跨模块**:本 FE 落点全在 `frontend/**`,不触 `backend/` / `sql/` / `scripts/`。改 `src/router/index.tsx`(把 `/usr/users/new` 与 `/usr/users/:id` 占位换为真实页、移除 `UserDetailPlaceholder` 占位组件)与 `src/api/usrApi.ts` / `src/api/types.ts`(FE-01/FE-03 搭建的全前端共享骨架)属共享资产扩展,在《模块完成报告》留痕:「FE-04 将 `/usr/users/new`、`/usr/users/:id` 占位 `UserDetailPlaceholder` 替换为真实 `UserDetailPage`;在 `usrApi.ts`/`types.ts` 增写端点契约(`createUser`/`updateUser`/`getUserDetail`/`listEmployees`/`listPermissions` + `UserCreateReq`/`UserUpdateReq`/`EmployeeOption`/`PermissionItem`),复用 FE-03 `UserVO`/`PageResult`,不改 FE-01/FE-03 既有 `login`/`fetchCompanies`/`listUsers`/既有类型;属共享骨架扩展」。 | ||
| 56 | +- **状态管理**(docs/04 § 2.2 / spec D7):单据态(mode/formValues/employees/permissions/checkedPermissionIds/loading/submitting/error)为页面就近态,用 `useUserDetail` hook + `useState` 本地管理,不进 Redux;登录态复用 FE-01 `authSlice`(本页只读当前用户名用于 create 态制单人占位,可选)。 | ||
| 57 | +- **请求封装 / 错误处理**(docs/04 § 2.3 / § 2.4):写/读均走 `usrApi.*` → `request.ts` Axios 实例(响应拦截器已拆 `Result`:`code=0` 返回 `data`,非 0 抛 `ApiError`,被动 401 统一登出 FE-02 既有机制);hook/页面捕获 `ApiError` 按 code 分流:表单可定位字段错误就近在 `Form.Item` 展示(如 40901 用户名冲突,docs/04 § 2.4「表单提交错误就近在表单展示」),其余 `message.error`。 | ||
| 58 | +- **Design Tokens**(docs/04 § 2.1 / spec § 7):表单网格/只读字段/下拉/权限列表表头/权限行/校验错误/成功/警告等语义色只用 `var(--color-*)`(清单见 spec § 7);工具栏深色底为页面局部装饰,scoped 在 `UserDetail.module.css`,不新增全局 token、不挪用语义 token(spec § 7 / D10,与 FE-02/FE-03 一致)。 | ||
| 59 | + | ||
| 60 | +## Tech Stack(技术栈,源自 docs/04 § 零 + FE-01/FE-02/FE-03 骨架) | ||
| 61 | + | ||
| 62 | +- React 18 / Ant Design 5(`Form` / `Input` / `Select` / `Checkbox`(含 `Checkbox.Group`)/ `Tabs` / `Button` / `Spin` / `Result`或错误占位 / `Modal`(`Modal.confirm` 取消二次确认) / `message` / `Space`)/ React Router v6(`useNavigate` / `useParams` / `useLocation`)/ Axios / TypeScript;`@ant-design/icons`(`SaveOutlined` / `CloseCircleOutlined` / `PlusCircleOutlined` / `SettingOutlined`)。 | ||
| 63 | +- **不新增 npm 依赖**:复用 FE-01/FE-03 既装依赖(见 `frontend/package.json` `dependencies`);前置校验用 AntD `Form` `rules` + 正则常量,无需额外校验库。 | ||
| 64 | +- 测试:单测 Vitest(jsdom)+ `@testing-library/react|jest-dom|user-event`(沿用 `tests/setup.ts`、`renderShell.tsx`);E2E Playwright(沿用 `playwright.config.ts`,`page.route` 桩 `**/api/usr/users**`、`**/api/usr/employees**`、`**/api/usr/permissions**`)。 | ||
| 65 | +- 命令(docs/04 § 零):build `npm run build`(`tsc --noEmit && vite build`);lint `npm run lint`;unit `npm run test:unit`(`vitest run`);e2e `npm run test:e2e`(`playwright test`)。子会话验证用 `cd frontend && npm run test:unit -- <文件名片段>`。 | ||
| 66 | +- 提交格式:`<type>(<scope>): <subject> REQ-XXX-NNN`。**scope 统一用 `usr`**(业务模块名,CLAUDE.md § Git);subject 业务类(feat/fix/test)带 `REQ-USR-001` 后缀(本 FE 主承载增加用户;修改用户 REQ-USR-002 在涉 edit 的 task 追加标注,见各 task)。每个任务在其 commit 行注明 REQ tag。 | ||
| 67 | + | ||
| 68 | +## 合同级常量(跨 task 必须一致) | ||
| 69 | + | ||
| 70 | +- **API 路径 / 方法**(`usrApi` 内传相对路径,`request.ts` baseURL=`/api` 已含前缀): | ||
| 71 | + - `POST /usr/users`(create,body=`UserCreateReq`,返回 `{ id: number }`)。 | ||
| 72 | + - `PUT /usr/users/{id}`(edit,路径 `id`,body=`UserUpdateReq`,返回 `{ id: number }`)。 | ||
| 73 | + - `GET /usr/users`(edit 预填复用,query `{ queryField, matchType:'等于', queryValue, pageNum:1, pageSize:1 }`,取 `records[0]`,复用 FE-03 `listUsers`,D4)。 | ||
| 74 | + - `GET /usr/employees`(员工名下拉数据源,无参全量,D1)。 | ||
| 75 | + - `GET /usr/permissions`(权限分类列表数据源,无参全量,D2)。 | ||
| 76 | +- **类型枚举(逐字一致,原样作为提交值,前端不映射,由后端裁决)**: | ||
| 77 | + - **用户类型** `USER_TYPE_OPTIONS = ['普通用户', '超级管理员']`,create 默认 `'普通用户'`(BR6)。 | ||
| 78 | + - **语言** `LANGUAGE_OPTIONS = ['中文', '英文', '繁体']`(BR7,无默认强制选,create 必选)。 | ||
| 79 | +- **create 默认表单值 `CREATE_DEFAULTS`**:`{ sUserName: '', sUserNo: '', iEmployeeId: null, sUserType: '普通用户', sLanguage: undefined, iCanModifyBill: 0 }`(BR1/BR2/BR6/BR8;`sLanguage` 未选触发必填校验 BR7)。 | ||
| 80 | +- **用户名前置校验正则 `USERNAME_PATTERN = /^[A-Za-z0-9_]{3,20}$/`**(3-20 位字母数字下划线,BR3,对齐 docs/05 § REQ-USR-001)。 | ||
| 81 | +- **错误码常量(对齐 docs/05 § REQ-USR-001 / § REQ-USR-002 / spec § 4)**:`ERR_VALIDATION = 40001`(参数校验失败)、`ERR_USERNAME_EXISTS = 40901`(用户名已存在,仅 create)、`ERR_USER_NOT_FOUND = 40401`(用户不存在,仅 edit)、`ERR_NO_PERMISSION = 40301`(无权限);网络/超时/5xx 经 `request.ts` 映射为 `NETWORK_ERROR_CODE = -1`(复用 FE-01 常量);`401` 由 `request.ts` 统一处理(本页不分流)。 | ||
| 82 | +- **mode 常量 `MODE_CREATE = 'create'` / `MODE_EDIT = 'edit'`**(由路由 `:id` 判定)。 | ||
| 83 | +- **静态文案(逐字一致,复刻原型 / spec)**: | ||
| 84 | + | 用途 | 文案 | | ||
| 85 | + |---|---| | ||
| 86 | + | 工具栏保存 | `保存` | | ||
| 87 | + | 工具栏取消 | `取消` | | ||
| 88 | + | 工具栏新增 | `新增` | | ||
| 89 | + | 工具栏占位按钮 | `删除` / `作废` / `重置密码` / `取消作废` / `功能`(点击 `message.info` 文案 `功能开发中`,D8) | | ||
| 90 | + | 制单人 create 占位 | `保存后自动生成`(复刻 `setUserDetailMode('new')`,BR2) | | ||
| 91 | + | 创建时间标签 | `创建时间` | | ||
| 92 | + | 制单人标签 | `制单人` | | ||
| 93 | + | 员工名标签 | `员工名` | | ||
| 94 | + | 用户名标签 | `用户名` | | ||
| 95 | + | 类型标签 | `类型` | | ||
| 96 | + | 语言标签 | `语言` | | ||
| 97 | + | 用户号标签 | `用户号` | | ||
| 98 | + | 单据修改权限标签 | `单据修改权限` | | ||
| 99 | + | 权限组页签 | `权限组` | | ||
| 100 | + | 占位页签 | `客户查看权限` / `供应商查看权限` / `人员查看权限` / `工序查看权限` / `司机查看权限`(D9) | | ||
| 101 | + | 权限列表表头 | `权限分类` | | ||
| 102 | + | 用户名格式错误 | `用户名须为 3-20 位字母数字下划线`(BR3) | | ||
| 103 | + | 用户名必填 | `请输入用户名`(BR3) | | ||
| 104 | + | 用户号必填 | `请输入用户号`(BR4) | | ||
| 105 | + | 类型必填 | `请选择类型`(BR6) | | ||
| 106 | + | 语言必填 | `请选择语言`(BR7) | | ||
| 107 | + | create 成功提示 | `用户创建成功`(BR16) | | ||
| 108 | + | edit 成功提示 | `保存成功`(BR16) | | ||
| 109 | + | 40001 提示 | `提交信息有误,请检查后重试`(spec § 4) | | ||
| 110 | + | 40901 提示 | `用户名已存在,请更换`(spec § 4,就近高亮用户名字段) | | ||
| 111 | + | 40401 提示 | `该用户不存在或已被删除`(spec § 4,提供「返回列表」入口) | | ||
| 112 | + | 40301 提示 | `无权限执行此操作`(spec § 4) | | ||
| 113 | + | 网络/5xx 提示 | `保存失败,请稍后重试`(spec § 4 兜底) | | ||
| 114 | + | 员工列表加载失败 | `员工列表加载失败`(spec § 4 loadError,D1) | | ||
| 115 | + | 权限列表加载失败 | `权限列表加载失败`(spec § 4 loadError,D2) | | ||
| 116 | + | 详情加载失败 | `加载失败,点击重试`(edit 详情取数失败,spec § 4 loadError) | | ||
| 117 | + | 取消二次确认 | `放弃未保存的修改?`(`Modal.confirm`,D5) | | ||
| 118 | + | 占位功能提示 | `功能开发中`(`message.info`,D8/D9) | | ||
| 119 | +- **路由 path(FE-02 已注册占位)**:create → `/usr/users/new`(BR14 / 取消回流的兄弟路由);edit → `/usr/users/:id`;保存成功/取消回流 → `/usr/users`(FE-03 列表,BR13/BR16;标签联动属 FE-02)。 | ||
| 120 | +- **localStorage token 键**:`TOKEN_STORAGE_KEY = 'xly_erp_token'`(FE-01 `request.ts` 已导出,本页不直接读写,由 `request.ts` 拦截器注入)。 | ||
| 121 | + | ||
| 122 | +## 关键签名(首次出现处给出,跨 task 一致) | ||
| 123 | + | ||
| 124 | +- **类型契约**(`api/types.ts`,新增;复用 FE-03 `UserVO`/`PageResult`/`UserListQuery` 不动): | ||
| 125 | + - `EmployeeOption`:`{ value: number; label: string; sEmployeeNo: string | null }`(员工名下拉项;`value` 映射后端 `iIncrement`、`label` 映射 `sEmployeeName`、`sEmployeeNo` 供联动带出用户号,D1/BR5)。 | ||
| 126 | + - `PermissionItem`:`{ id: number; name: string; category: string }`(权限项;`id` 映射后端 `iIncrement`、`name` 映射 `sPermissionName`、`category` 映射 `sPermissionCategory`,D2/D3)。 | ||
| 127 | + - `UserCreateReq`:`{ sUserName: string; sUserNo?: string; iEmployeeId?: number | null; sUserType: string; sLanguage: string; iCanModifyBill?: 0 | 1; permissionIds?: number[] }`(密码 `initialPassword` 前端不传,由后端默认 666666,BR9;对齐 docs/05 § REQ-USR-001)。 | ||
| 128 | + - `UserUpdateReq`:`{ sUserNo?: string; iEmployeeId?: number | null; sUserType: string; sLanguage: string; iCanModifyBill?: 0 | 1; iIsVoid?: 0 | 1; permissionIds?: number[] }`(`sUserName` 不可改不传、密码不在本接口;对齐 docs/05 § REQ-USR-002)。 | ||
| 129 | + - `UserDetailMode`:`'create' | 'edit'`。 | ||
| 130 | +- **API 封装**(`api/usrApi.ts`,新增方法;响应拦截器已拆 `Result.data`,沿用 FE-01/FE-03 `as unknown as Promise<...>` 桥接): | ||
| 131 | + - `createUser(body: UserCreateReq): Promise<{ id: number }>`——`request.post('/usr/users', body)`。 | ||
| 132 | + - `updateUser(id: number, body: UserUpdateReq): Promise<{ id: number }>`——`request.put('/usr/users/' + id, body)`。 | ||
| 133 | + - `getUserDetail(params: { queryField: string; queryValue: string }): Promise<UserVO | null>`——内部以「等于」匹配 + `pageNum:1, pageSize:1` 调 `listUsers`,返回 `records[0] ?? null`(复用 FE-03 `listUsers` 与中文键归一,D4)。 | ||
| 134 | + - `listEmployees(): Promise<EmployeeOption[]>`——`request.get('/usr/employees')`,对后端原始行(`iIncrement`/`sEmployeeName`/`sEmployeeNo`)归一为 `EmployeeOption`(D1)。 | ||
| 135 | + - `listPermissions(): Promise<PermissionItem[]>`——`request.get('/usr/permissions')`,对后端原始行(`iIncrement`/`sPermissionName`/`sPermissionCategory`)归一为 `PermissionItem`(D2/D3)。 | ||
| 136 | +- **页面 hook**(`pages/usr/UserDetail/useUserDetail.ts`): | ||
| 137 | + - `useUserDetail(args: { mode: UserDetailMode; userId?: number; presetUser?: UserVO | null })` → `{ mode: UserDetailMode; formValues: UserFormValues; employees: EmployeeOption[]; permissions: PermissionItem[]; checkedPermissionIds: number[]; loading: boolean; submitting: boolean; error: ApiError | null; loadFailed: boolean; setField(name: keyof UserFormValues, value: unknown): void; selectEmployee(value: number | null): void; togglePermission(id: number, checked: boolean): void; toggleAll(checked: boolean): void; submit(values: UserFormValues): Promise<{ ok: boolean; id?: number; fieldError?: { field: keyof UserFormValues; message: string } }>; reload(): void; }`。 | ||
| 138 | + - 其中 `UserFormValues = { sUserName: string; sUserNo: string; iEmployeeId: number | null; sUserType: string; sLanguage: string | undefined; iCanModifyBill: 0 | 1; iIsVoid?: 0 | 1 }`(受控表单值,与提交映射 spec § 6 一致;`tCreateDate`/`sCreator` 只读展示态另存,不在提交值内)。 | ||
| 139 | + - 挂载即并发预取 `listEmployees()` + `listPermissions()`(initialLoading,spec § 3);`edit` 态额外按 `userId` 用 `presetUser ?? getUserDetail(...)` 取原值回填 `formValues` 并以已授权权限初始化 `checkedPermissionIds`(BR17 / D4);任一预取/详情取数失败置 `loadFailed=true` + 对应 `message.error`(spec § 4 loadError,文案见合同级常量)。 | ||
| 140 | + - `selectEmployee(value)`:设 `iEmployeeId` 并按选中员工 `label`/`sEmployeeNo` 联动带出 `sUserName`(create 态)/`sUserNo`(用户仍可改,BR5)。 | ||
| 141 | + - `submit(values)`:置 `submitting=true`(防重 BR15)→ create 调 `createUser(toCreateReq(values, checkedPermissionIds))` / edit 调 `updateUser(userId, toUpdateReq(values, checkedPermissionIds))`(permissionIds 全量覆盖 BR11)→ `code=0` 返回 `{ ok:true, id }`(页面负责 `message.success` + 回流 BR16);`ApiError` 按 code 分流:`40901` 返回 `{ ok:false, fieldError:{ field:'sUserName', message } }`(就近高亮)、`40401`/`40301`/`40001`/网络兜底 `message.error` 并返回 `{ ok:false }`(spec § 4);finally 复位 `submitting`。 | ||
| 142 | + - `reload()`:重跑挂载预取/详情(loadError 重试入口用,spec § 4)。 | ||
| 143 | + - **纯映射函数**(`pages/usr/UserDetail/constants.ts` 或 hook 内,跨 task 一致,便于单测):`toCreateReq(values: UserFormValues, permissionIds: number[]): UserCreateReq`、`toUpdateReq(values: UserFormValues, permissionIds: number[]): UserUpdateReq`、`userVoToFormValues(vo: UserVO): UserFormValues`(edit 回填,BR17)。 | ||
| 144 | +- **组件 props**(`pages/usr/UserDetail/`): | ||
| 145 | + - `UserDetailToolbar`(`{ mode: UserDetailMode; submitting: boolean; canSave: boolean; onSave(): void; onCancel(): void; onNew(): void }`):保存(`type="primary"`,`submitting` 时 `loading`+`disabled` BR15,`canSave` 控可点)/ 取消(BR13)/ 新增(BR14)/ 占位按钮(删除/作废/重置密码/取消作废/功能 + 齿轮,点击 `message.info('功能开发中')`,D8)。 | ||
| 146 | + - `UserBasicForm`(`{ form: FormInstance; mode: UserDetailMode; employees: EmployeeOption[]; readonlyCreateTime?: string; readonlyCreator?: string; onSelectEmployee(value: number | null): void }`):AntD `Form`(受 `form` 实例控制,3 列网格布局);渲染 8 字段(spec § 2 控件选型);create 态用户名可编辑(`rules` 含必填 + `USERNAME_PATTERN`)、edit 态用户名 `disabled`(BR3);类型/语言/员工名 `Select`(options 来自常量/`employees`);创建时间/制单人只读展示(create 制单人显「保存后自动生成」BR2,edit 回填 `readonlyCreator`/`readonlyCreateTime` BR1/BR2);员工名 `onChange`→`onSelectEmployee`(BR5)。 | ||
| 147 | + - `PermissionTabs`(`{ activeKey?: string; onChange?(key: string): void; children: ReactNode }`):AntD `Tabs`,第一项 `权限组`(key=`group`)承载 `children`(权限列表),其余 5 个 `客户查看权限`…`司机查看权限` 为 `disabled` 占位页签(D9)。 | ||
| 148 | + - `PermissionGroupList`(`{ permissions: PermissionItem[]; checkedIds: number[]; onToggle(id: number, checked: boolean): void; onToggleAll(checked: boolean): void; loading?: boolean }`):表头行「全选 `Checkbox`(`indeterminate` 半选 / `checked` 全选)+ `权限分类` + 排序图标占位」(复刻原型 `.perm-row.head`);逐项 `Checkbox` 行(`PermItem.name`,按 `checkedIds.includes(id)` 勾选;D3 逐项展示);空 `permissions` 时空态(loadError 由页面消费,spec § 4)。 | ||
| 149 | + - `UserDetailPage`(default export,无 props):`useParams<{ id?: string }>()` 判 `mode`(有 `id`→`edit`、`userId=Number(id)`;否则 `create`)+ `useLocation()`(取 `state.user` 作 `presetUser`,D4 备注)+ `useNavigate()` + AntD `Form.useForm()`;`useUserDetail({ mode, userId, presetUser })`;装配 `UserDetailToolbar`/`UserBasicForm`/`PermissionTabs`(含 `PermissionGroupList`);`onSave`→`form.validateFields()` 通过后调 `submit(values)`,`ok` 则 `message.success`(按 mode) + `navigate('/usr/users')`(BR16),`fieldError` 则 `form.setFields([{ name, errors:[message] }])` 就近高亮(40901,spec § 4);`onCancel`→ 有未保存改动 `Modal.confirm('放弃未保存的修改?')` 通过后 `navigate('/usr/users')`(BR13/D5);`onNew`→`navigate('/usr/users/new')`(BR14);`loadFailed` 渲染错误占位 +「点击重试」→`reload()` 或「返回列表」(spec § 4 loadError)。 | ||
| 150 | +- **常量映射**(`pages/usr/UserDetail/constants.ts`):枚举/默认/错误码/文案/正则/`MODE_*`(见合同级常量)+ 上述 `toCreateReq`/`toUpdateReq`/`userVoToFormValues` 纯函数签名。 | ||
| 151 | + | ||
| 152 | +## 测试栈说明 | ||
| 153 | + | ||
| 154 | +- **jsdom 组件 / hook / api 单测**(Vitest + RTL):组件测用 `renderShell`(已存在,Provider + 真实 store + `MemoryRouter` + AntD `App`/`ConfigProvider`);`mode`/`:id` 用 `renderShell(<AppRouter/>, { initialEntries:['/usr/users/new' | '/usr/users/7'], preloadedAuth:{token:'t',user:ADMIN} })` 或直接渲染 `UserDetailPage` 包一层 `Routes`/`MemoryRouter` 注入路由参数;导航回流断言用 `LocationProbe`(复用 `router.test.tsx`/`UserListPage.test.tsx` 的 `useLocation` 探针模式)+ 列表路由哨兵。`useUserDetail` 用 `renderHook`(需 AntD `App` wrapper 提供 `message`,沿用 `useUserList.test.tsx` 的 wrapper 模式)。api 测桩底层 `request` 实例(沿用 `usrApi.userlist.test.ts` 的 `vi.mock('../../src/api/request')` 模式);hook/页面测对 `usrApi` 用 `vi.mock('../../src/api/usrApi')` 桩(沿用 `useUserList.test.tsx` 模式)。 | ||
| 155 | +- **Playwright E2E**:沿用 `shell.spec.ts`/`userlist.spec.ts` 的登录桩(`stubBackend`/`login`);`page.route` 桩 `**/api/usr/employees**`(返回员工项)、`**/api/usr/permissions**`(返回权限项)、`**/api/usr/users**`(GET 返回 edit 预填单条;POST/PUT 按用例返回 `code=0` 或 40901/5xx)。先经登录桩落地外壳后从 `/usr/users` 进入或直接导航 `/usr/users/new`、`/usr/users/{id}`。不依赖真实后端起服。 | ||
| 156 | +- **可测性**:优先语义查询(role/label/text/`Form.Item` label);仅当无法稳定定位时加最小 `data-testid`(如 `userdetail-page` / `btn-save` / `btn-cancel` / `btn-new` / `field-username` / `field-userno` / `select-employee` / `select-usertype` / `select-language` / `perm-list` / `perm-check-all` / `userdetail-loaderror`)。 | ||
| 157 | + | ||
| 158 | +--- | ||
| 159 | + | ||
| 160 | +## 任务列表(每个 task = red → green → 子会话验证 → commit) | ||
| 161 | + | ||
| 162 | +> 硬护栏:以下每个 `impl_file` / `test_file` 均以 `frontend/` 开头;无任何 `backend/` / `sql/` / `scripts/` 落点。 | ||
| 163 | +> 提交 scope 统一 `usr`;REQ tag:纯 create 链路标 `REQ-USR-001`,涉 edit(预填/更新)链路追加 `REQ-USR-002`(在 commit 行注明)。 | ||
| 164 | + | ||
| 165 | +### T1 — 类型契约 + API 封装(createUser/updateUser/getUserDetail/listEmployees/listPermissions,D1/D2/D4)(jsdom api 测) | ||
| 166 | +- **测试先行类型**:jsdom 组件测试(api 单测) | ||
| 167 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/usrApi.userdetail.test.ts`(沿用 `usrApi.userlist.test.ts` 的 `vi.mock('../../src/api/request')` 桩底层实例 `post`/`put`/`get`): | ||
| 168 | + - `::createUser posts /usr/users with body`——调 `createUser({ sUserName:'zhangsan', sUserNo:'zs', iEmployeeId:3, sUserType:'普通用户', sLanguage:'中文', iCanModifyBill:0, permissionIds:[1,2] })`,断言 `request.post` 以 `'/usr/users'` + 该 body 被调,且 body 不含 `initialPassword`/`sCreator`/`tCreateDate`(BR9)。 | ||
| 169 | + - `::updateUser puts /usr/users/{id} with body`——调 `updateUser(7, { sUserType:'超级管理员', sLanguage:'英文', iCanModifyBill:1, iIsVoid:0, permissionIds:[2] })`,断言 `request.put` 以 `'/usr/users/7'` + 该 body 被调,且 body **不含** `sUserName`(不可改 BR3)。 | ||
| 170 | + - `::getUserDetail queries equals match pageSize 1 and returns records[0]`——桩 `request.get` resolve `{ records:[{ id:7, sUserName:'zhangsan', 员工名:'张三', sUserNo:'zs', 部门:null, sUserType:'普通用户', sLanguage:'中文', iIsVoid:0, tLastLoginDate:null, sCreator:'admin', tCreateDate:'2026-01-01T00:00:00' }], total:1, pageNum:1, pageSize:1 }`,调 `getUserDetail({ queryField:'用户名', queryValue:'zhangsan' })`,断言 `request.get` query 含 `matchType:'等于', pageNum:1, pageSize:1`,返回对象 `id===7`、`employeeName==='张三'`(复用归一),空 records 时返回 `null`。 | ||
| 171 | + - `::listEmployees normalizes iIncrement/sEmployeeName/sEmployeeNo to EmployeeOption`——桩 resolve `[{ iIncrement:3, sEmployeeName:'张三', sEmployeeNo:'zs' }]`,断言返回 `[{ value:3, label:'张三', sEmployeeNo:'zs' }]`(D1)。 | ||
| 172 | + - `::listPermissions normalizes to PermissionItem`——桩 resolve `[{ iIncrement:1, sPermissionName:'默认显示', sPermissionCategory:'基础' }]`,断言返回 `[{ id:1, name:'默认显示', category:'基础' }]`(D2/D3)。 | ||
| 173 | + > FE-01/FE-03 既有 `login`/`fetchCompanies`/`listUsers` 用例不回归(同文件或既有文件保持绿)。 | ||
| 174 | +- [ ] **2. 实现最小代码**:`frontend/src/api/types.ts`(新增 `EmployeeOption`/`PermissionItem`/`UserCreateReq`/`UserUpdateReq`/`UserDetailMode`,签名见关键签名;复用 FE-03 `UserVO`/`PageResult`/`UserListQuery`)+ `frontend/src/api/usrApi.ts`(新增 5 方法,签名见关键签名;`getUserDetail` 复用 `listUsers`;`listEmployees`/`listPermissions` 做后端原始键归一)。 | ||
| 175 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- usrApi`(含 FE-01/FE-03 `usrApi` 既有用例不回归) | ||
| 176 | +- [ ] **4. commit**:`feat(usr): 用户单据 API 与类型契约 create/update/detail/employees/permissions REQ-USR-001 REQ-USR-002` | ||
| 177 | + | ||
| 178 | +### T2 — 页面常量 + 提交映射纯函数(枚举/默认/正则/错误码/文案 + toCreateReq/toUpdateReq/userVoToFormValues)(jsdom 单测) | ||
| 179 | +- **测试先行类型**:jsdom 组件测试(纯逻辑断言) | ||
| 180 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/userDetailMappers.test.ts`: | ||
| 181 | + - `::constants enums and defaults`——断言 `USER_TYPE_OPTIONS===['普通用户','超级管理员']`、`LANGUAGE_OPTIONS===['中文','英文','繁体']`、`CREATE_DEFAULTS.sUserType==='普通用户'`、`CREATE_DEFAULTS.iCanModifyBill===0`、`USERNAME_PATTERN.test('ab_12')===true` 且 `USERNAME_PATTERN.test('ab')===false`(少于 3 位)、错误码 `ERR_USERNAME_EXISTS===40901`/`ERR_USER_NOT_FOUND===40401`/`ERR_NO_PERMISSION===40301`/`ERR_VALIDATION===40001`、`MODE_CREATE==='create'`/`MODE_EDIT==='edit'`。 | ||
| 182 | + - `::toCreateReq maps form values + permissionIds (no password)`——传 `UserFormValues` + `[1,2]`,断言产出 `UserCreateReq` 含 `permissionIds:[1,2]`、`iCanModifyBill` 为 0/1、不含 `initialPassword`/`iIsVoid`(create 无作废,BR9)。 | ||
| 183 | + - `::toUpdateReq maps without sUserName + includes iIsVoid + full permissionIds`——断言产出 `UserUpdateReq` 不含 `sUserName`(BR3),含 `iIsVoid`、`permissionIds`(全量覆盖 BR11)。 | ||
| 184 | + - `::userVoToFormValues fills from UserVO`——传 `UserVO`,断言回填 `sUserName`/`sUserNo`/`sUserType`/`sLanguage`/`iCanModifyBill`/`iIsVoid` 与 VO 一致(BR17)。 | ||
| 185 | +- [ ] **2. 实现最小代码**:`frontend/src/pages/usr/UserDetail/constants.ts`(枚举/默认/正则/错误码/`MODE_*`/文案常量 + `toCreateReq`/`toUpdateReq`/`userVoToFormValues` 纯函数,签名见关键签名)。 | ||
| 186 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- userDetailMappers` | ||
| 187 | +- [ ] **4. commit**:`feat(usr): 用户单据页面常量与提交映射纯函数 REQ-USR-001 REQ-USR-002` | ||
| 188 | + | ||
| 189 | +### T3 — useUserDetail 单据 hook(状态机:initialLoading/editing/submitting/submitError/submitSuccess/loadError)(jsdom hook 测) | ||
| 190 | +- **测试先行类型**:jsdom 组件测试(`renderHook`,`vi.mock('../../src/api/usrApi')`,AntD `App` wrapper) | ||
| 191 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/useUserDetail.test.tsx`: | ||
| 192 | + - `::create mode initial load prefetches employees+permissions (initialLoading→editing)`——`listEmployees`/`listPermissions` resolve 非空,`mode:'create'`,断言挂载即并发取数、`loading` true→false、`formValues` 为 `CREATE_DEFAULTS`、`checkedPermissionIds` 为空(spec § 3 initialLoading→editing / BR2/BR6/BR8)。 | ||
| 193 | + - `::edit mode prefills from getUserDetail and pre-checks permissions`——`mode:'edit'`,`userId:7`,无 `presetUser`,桩 `getUserDetail` 返回含权限的 `UserVO`,断言 `getUserDetail` 被调、`formValues` 回填原值、`checkedPermissionIds` 含已授权(BR17/D4)。 | ||
| 194 | + - `::edit mode with presetUser skips getUserDetail`——传 `presetUser`,断言 `getUserDetail` 未被调、直接以 preset 回填(D4 备注)。 | ||
| 195 | + - `::selectEmployee fills userNo/userName from employee (create)`——`selectEmployee(3)`,断言 `iEmployeeId===3` 且 `sUserName`/`sUserNo` 按选中员工带出(BR5)。 | ||
| 196 | + - `::toggle permission and toggleAll update checkedPermissionIds`——`togglePermission(1,true)` 加入 1、`toggleAll(true)` 选全部、`toggleAll(false)` 清空(BR10/BR11)。 | ||
| 197 | + - `::submit create calls createUser and returns {ok,id}`——`mode:'create'`,桩 `createUser` resolve `{id:9}`,`submit(values)` 返回 `{ ok:true, id:9 }`,期间 `submitting` true→false(BR12/BR15)。 | ||
| 198 | + - `::submit edit calls updateUser with userId and full permissionIds`——`mode:'edit'`,`userId:7`,桩 `updateUser` resolve `{id:7}`,断言以 `(7, UserUpdateReq含permissionIds)` 调用,返回 `{ ok:true, id:7 }`(BR11/BR12)。 | ||
| 199 | + - `::submit 40901 returns fieldError on sUserName`——桩 `createUser` reject `new ApiError(40901,...)`,断言返回 `{ ok:false, fieldError:{ field:'sUserName' } }`(spec § 4 / BR3)。 | ||
| 200 | + - `::submit 40401/40301/40001/network show message and return ok:false`——分别 reject 对应 `ApiError`,断言 `message.error` 文案(按合同级常量)且返回 `{ ok:false }`(spec § 4)。 | ||
| 201 | + - `::loadError when prefetch fails sets loadFailed and message`——桩 `listPermissions` reject,断言 `loadFailed===true` + `message.error('权限列表加载失败')`(spec § 4 loadError / D2);`reload()` 重新取数清 `loadFailed`。 | ||
| 202 | + > `message` 断言桩 AntD `App.useApp().message`(沿用 `useUserList.test.tsx` 的 wrapper/桩模式,TDD 期定一处)。 | ||
| 203 | +- [ ] **2. 实现最小代码**:`frontend/src/pages/usr/UserDetail/useUserDetail.ts`(签名见关键签名;内部 `useState` 持 `mode`/`formValues`/`employees`/`permissions`/`checkedPermissionIds`/`loading`/`submitting`/`error`/`loadFailed`,`useEffect` 挂载并发预取 + edit 详情;错误码分流按合同级常量;复用 T1 api + T2 映射)。 | ||
| 204 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- useUserDetail` | ||
| 205 | +- [ ] **4. commit**:`feat(usr): 用户单据 hook useUserDetail 状态机 REQ-USR-001 REQ-USR-002` | ||
| 206 | + | ||
| 207 | +### T4 — UserBasicForm 表单网格(字段/默认/只读/枚举/必填+格式/员工联动,BR1-BR9)(jsdom 组件测) | ||
| 208 | +- **测试先行类型**:jsdom 组件测试 | ||
| 209 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/UserBasicForm.test.tsx`(`renderShell` 提供 AntD App;测试内用 `Form.useForm()` wrapper 注入 `form`): | ||
| 210 | + - `::renders 8 labeled fields`——断言创建时间/制单人/员工名/用户名/类型/语言/用户号/单据修改权限 8 个标签可见(文案逐字)。 | ||
| 211 | + - `::create mode username editable with default empty; edit mode username disabled`——`mode='create'` 用户名 `Input` 可编辑;`mode='edit'` 用户名框 `disabled`(BR3)。 | ||
| 212 | + - `::create mode defaults usertype 普通用户`——create 类型下拉默认显「普通用户」(BR6)。 | ||
| 213 | + - `::username format rule rejects short/invalid and required when empty`——create 态填「ab」触发校验报「用户名须为 3-20 位字母数字下划线」;清空报「请输入用户名」(BR3)。 | ||
| 214 | + - `::userno required`——用户号空提交报「请输入用户号」(BR4)。 | ||
| 215 | + - `::usertype/language selects expose enum options only`——展开类型断言 `普通用户/超级管理员`;展开语言断言 `中文/英文/繁体`(BR6/BR7,无自由输入)。 | ||
| 216 | + - `::create mode creator shows 保存后自动生成`——create 制单人只读区文本「保存后自动生成」(BR2);edit 态显 `readonlyCreator`。 | ||
| 217 | + - `::selecting employee calls onSelectEmployee`——员工名下拉选中 → `onSelectEmployee` 收到 value(BR5)。 | ||
| 218 | + - `::单据修改权限 checkbox default unchecked (create)`——create 复选框默认未勾(BR8)。 | ||
| 219 | +- [ ] **2. 实现最小代码**:`frontend/src/pages/usr/UserDetail/UserBasicForm.tsx`(签名见关键签名;3 列网格 `Form`;用户名 `rules` 含必填 + `USERNAME_PATTERN`,edit `disabled`;类型/语言/员工 `Select`;创建时间/制单人只读;员工 `onChange`→`onSelectEmployee`)+ `UserDetail.module.css` 表单样式(网格 `--color-form-bg-edit`、只读字段 `--color-form-bg-readonly`、文字 `--color-form-fg`、必填 `*` `--color-error`、网格线 `--color-border`、下拉 hover `--color-form-bg-hover`)。 | ||
| 220 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- UserBasicForm` | ||
| 221 | +- [ ] **4. commit**:`feat(usr): 用户单据表单网格 UserBasicForm REQ-USR-001 REQ-USR-002` | ||
| 222 | + | ||
| 223 | +### T5 — PermissionGroupList 权限分类勾选列表(渲染/勾选集合/全选 indeterminate/回勾,BR10/BR11/D3)(jsdom 组件测) | ||
| 224 | +- **测试先行类型**:jsdom 组件测试 | ||
| 225 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/PermissionGroupList.test.tsx`(`renderShell`): | ||
| 226 | + - `::renders header 权限分类 and one row per permission`——传 3 个 `PermissionItem`,断言表头「权限分类」+ 3 行复选框逐项名(D3)。 | ||
| 227 | + - `::checked rows reflect checkedIds`——`checkedIds=[1]`,断言 id=1 行勾选、其余未勾(BR10/edit 回勾 BR17)。 | ||
| 228 | + - `::toggling a row calls onToggle(id, checked)`——点未勾行 → `onToggle(id,true)`;点已勾行 → `onToggle(id,false)`(BR10/BR11)。 | ||
| 229 | + - `::header select-all checked when all selected; indeterminate when partial`——全勾时表头全选 `checked`、部分勾时 `indeterminate`、全不勾时未勾(半选语义)。 | ||
| 230 | + - `::header toggle calls onToggleAll`——点表头全选(当前未全选)→ `onToggleAll(true)`;全选态再点 → `onToggleAll(false)`。 | ||
| 231 | + - `::empty permissions renders empty list (no rows)`——`permissions=[]` 无数据行(loadError 占位由页面消费)。 | ||
| 232 | +- [ ] **2. 实现最小代码**:`frontend/src/pages/usr/UserDetail/PermissionGroupList.tsx`(签名见关键签名;表头全选 `Checkbox`(`checked`/`indeterminate` 由 `checkedIds` 与 `permissions` 计算)+ 逐项行 `Checkbox`)+ `UserDetail.module.css` 权限列表样式(表头 `--color-table-header-bg`/`--color-table-header-fg`、行文字 `--color-text`、行 hover `--color-table-row-bg-hover`、行下边线 `--color-border`、列表白底 `--color-form-bg-edit`)。 | ||
| 233 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- PermissionGroupList` | ||
| 234 | +- [ ] **4. commit**:`feat(usr): 用户单据权限分类勾选列表 PermissionGroupList REQ-USR-001 REQ-USR-002` | ||
| 235 | + | ||
| 236 | +### T6 — PermissionTabs 权限页签条(权限组 active + 5 占位页签,D9)(jsdom 组件测) | ||
| 237 | +- **测试先行类型**:jsdom 组件测试 | ||
| 238 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/PermissionTabs.test.tsx`(`renderShell`): | ||
| 239 | + - `::renders 权限组 active with children`——传 `children` 哨兵元素,断言「权限组」页签存在且默认激活、`children`(权限列表)可见。 | ||
| 240 | + - `::renders 5 placeholder tabs disabled`——断言「客户查看权限/供应商查看权限/人员查看权限/工序查看权限/司机查看权限」5 个页签存在且 `disabled`(不渲染数据,D9)。 | ||
| 241 | + - `::placeholder tabs do not show permission list`——切到(被 disabled 无法切,或断言占位页签无权限行内容)确认占位(D9)。 | ||
| 242 | +- [ ] **2. 实现最小代码**:`frontend/src/pages/usr/UserDetail/PermissionTabs.tsx`(签名见关键签名;AntD `Tabs` `items`:`group` 项含 `children`,5 占位项 `disabled:true`)+ `UserDetail.module.css` 页签条样式(active 下划线 `--color-primary`、页签文字 `--color-text`、占位文字 `--color-text-secondary`、下边线 `--color-border`)。 | ||
| 243 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- PermissionTabs` | ||
| 244 | +- [ ] **4. commit**:`feat(usr): 用户单据权限页签条 PermissionTabs REQ-USR-001` | ||
| 245 | + | ||
| 246 | +### T7 — UserDetailToolbar 工具栏(保存/取消/新增 + 提交中禁用 + 占位按钮,BR12/BR13/BR14/BR15/D8/D10)(jsdom 组件测) | ||
| 247 | +- **测试先行类型**:jsdom 组件测试 | ||
| 248 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/UserDetailToolbar.test.tsx`(`renderShell`): | ||
| 249 | + - `::renders 保存/取消/新增 + placeholder buttons + gear`——断言保存/取消/新增可见,占位「删除/作废/重置密码/取消作废/功能」与齿轮可见(文案逐字 + 图标)。 | ||
| 250 | + - `::click 保存 calls onSave / 取消 calls onCancel / 新增 calls onNew`——分别点击触发对应回调(BR12/BR13/BR14)。 | ||
| 251 | + - `::submitting disables 保存 and shows loading`——`submitting=true` 时「保存」禁用且 loading,再点不触发 `onSave`(BR15)。 | ||
| 252 | + - `::canSave=false disables 保存`——`canSave=false` 时「保存」禁用。 | ||
| 253 | + - `::placeholder buttons show 功能开发中 (no business callback)`——点占位按钮/齿轮触发 `message.info('功能开发中')`、不触发 `onSave`/`onCancel`/`onNew`(D8)。 | ||
| 254 | +- [ ] **2. 实现最小代码**:`frontend/src/pages/usr/UserDetail/UserDetailToolbar.tsx`(签名见关键签名;图标 `SaveOutlined`/`CloseCircleOutlined`/`PlusCircleOutlined`/`SettingOutlined`;占位按钮 `message.info('功能开发中')`)+ `UserDetail.module.css` 工具栏深色底(scoped 局部装饰,非语义 token,D10;保存主操作 `--color-primary`、占位按钮文字 `--color-text-secondary`)。 | ||
| 255 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- UserDetailToolbar` | ||
| 256 | +- [ ] **4. commit**:`feat(usr): 用户单据工具栏 UserDetailToolbar REQ-USR-001 REQ-USR-002` | ||
| 257 | + | ||
| 258 | +### T8 — UserDetailPage 页面集成 + 路由接线(create/edit 贯通 + 提交回流 + 错误就近 + 取数失败,BR3/BR12/BR16/BR17/D4/D5)(jsdom 组件测) | ||
| 259 | +- **测试先行类型**:jsdom 组件测试 | ||
| 260 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/UserDetailPage.test.tsx`(`renderShell`,`vi.mock('../../src/api/usrApi')` 桩 5 方法;用 `LocationProbe` + `/usr/users` 列表哨兵验回流;用 `MemoryRouter initialEntries` 注入 `/usr/users/new` 与 `/usr/users/7`): | ||
| 261 | + - `::create mode renders empty form with defaults`——进 `/usr/users/new`,挂载预取员工/权限、渲染空表单 + 默认普通用户 + 制单人「保存后自动生成」(BR2/BR6/initialLoading→editing)。 | ||
| 262 | + - `::create submit success navigates to /usr/users with success`——填合法字段(用户名「zhangsan」/用户号/语言 + 勾权限),点保存 → `createUser` 以表单值被调、`message.success('用户创建成功')`、URL/哨兵回 `/usr/users`(BR12/BR16)。 | ||
| 263 | + - `::create username format invalid blocks submit`——填「ab」点保存 → 校验拦截、`createUser` 未被调、就近报错「用户名须为 3-20 位字母数字下划线」(BR3)。 | ||
| 264 | + - `::create 40901 highlights username field`——桩 `createUser` reject `ApiError(40901)`,点保存 → 用户名 `Form.Item` 就近报「用户名已存在,请更换」+ `message.error`(spec § 4)。 | ||
| 265 | + - `::edit mode prefills from getUserDetail and username disabled`——进 `/usr/users/7`,桩 `getUserDetail` 返回 `UserVO`,断言表单回填原值、用户名框禁用、权限按已授权回勾(BR17/BR3/D4)。 | ||
| 266 | + - `::edit submit success navigates to /usr/users with 保存成功`——edit 改类型后保存 → `updateUser(7, ...)` 被调、`message.success('保存成功')`、回流 `/usr/users`(BR16)。 | ||
| 267 | + - `::cancel with dirty form confirms then navigates`——改过字段后点取消 → 弹「放弃未保存的修改?」,确认后回 `/usr/users`(BR13/D5)。 | ||
| 268 | + - `::新增 navigates to /usr/users/new`——点工具栏「新增」→ URL/哨兵到 `/usr/users/new`(BR14)。 | ||
| 269 | + - `::loadError shows retry; retry calls reload`——桩 `listPermissions` reject → 可见「加载失败,点击重试」或对应 loadError 占位;点重试再次取数(spec § 4)。 | ||
| 270 | + - `::edit 40401 offers 返回列表`——桩 `getUserDetail` 返回 `null`(或 `updateUser` reject 40401)→ 提示「该用户不存在或已被删除」+「返回列表」入口回 `/usr/users`(spec § 4)。 | ||
| 271 | +- [ ] **2. 实现最小代码**:`frontend/src/pages/usr/UserDetail/index.tsx`(`UserDetailPage`:`useParams`/`useLocation`/`useNavigate` + `Form.useForm()` + `useUserDetail`,装配 4 子组件,`onSave`→`validateFields`→`submit`→`ok` 则 `message.success`+`navigate('/usr/users')`、`fieldError` 则 `form.setFields` 就近,`onCancel`→脏检 `Modal.confirm`→`navigate`,`onNew`→`navigate('/usr/users/new')`,`loadFailed` 渲染重试/返回列表);改 `frontend/src/router/index.tsx`——把 `/usr/users/new` 与 `/usr/users/:id` 的 `UserDetailPlaceholder` 替换为 `UserDetailPage`(移除占位组件,import 真实页;FE-03 `/usr/users` 不动)。 | ||
| 272 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- UserDetailPage router`(确认 FE-02 `router.test.tsx` 不回归——若其断言 `/usr/users/new`/`:id` 渲染占位 `data-testid`,需同步更新为真实页可定位元素并在 commit 说明) | ||
| 273 | +- [ ] **4. commit**:`feat(usr): 用户单据页面集成与路由接线 UserDetailPage REQ-USR-001 REQ-USR-002` | ||
| 274 | + | ||
| 275 | +### T9 — E2E 用户单据关键旅程(Playwright) | ||
| 276 | +- **测试先行类型**:Playwright E2E | ||
| 277 | +- [ ] **1. 写失败测试**:`frontend/tests/e2e/userdetail.spec.ts`(沿用 `shell.spec.ts`/`userlist.spec.ts` 登录桩;`page.route` 桩 `**/api/usr/employees**`/`**/api/usr/permissions**`/`**/api/usr/users**`): | ||
| 278 | + - `::create user and return to list`——进 `/usr/users/new`,填合法字段 + 勾权限,桩 `POST` 返回 `{code:0,data:{id:9}}`,点保存 → 断言 `message`「用户创建成功」+ URL 回 `/usr/users`(用 `page.waitForRequest` 校验 POST body 含 `sUserName`)。 | ||
| 279 | + - `::edit user prefill then save`——桩 `GET /usr/users`(等于匹配)返回单条用户,进 `/usr/users/7`,断言用户名框值回填且禁用,改语言后保存(桩 `PUT` 返回 `code:0`)→ `message`「保存成功」+ 回 `/usr/users`。 | ||
| 280 | + - `::username conflict shows inline error`——桩 `POST` 返回 `{code:40901}`,提交 → 断言用户名字段就近报「用户名已存在,请更换」。 | ||
| 281 | + - `::load error shows retry`——桩 `GET /usr/permissions` 5xx,进 `/usr/users/new` → 断言「加载失败,点击重试」可见;点重试(改桩为成功)后表单可用。 | ||
| 282 | + - `::placeholder tabs/buttons are inert`——断言 5 个查看权限页签 disabled、点「删除」出现「功能开发中」(D8/D9)。 | ||
| 283 | +- [ ] **2. 实现最小代码**:补任何为可测性需要的最小 `data-testid`(仅 Playwright 无法稳定定位时,如 `userdetail-page`/`btn-save`/`field-username`/`perm-list`/`userdetail-loaderror`)。沿用 `playwright.config.ts`。 | ||
| 284 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:e2e -- userdetail` | ||
| 285 | +- [ ] **4. commit**:`test(usr): 用户单据 E2E 关键旅程 REQ-USR-001 REQ-USR-002` | ||
| 286 | + | ||
| 287 | +### T10 — 全量门禁回归 + 收尾(chore) | ||
| 288 | +- **测试先行类型**:无新增测试(全量验证) | ||
| 289 | +- [ ] **1. 写失败测试**:无。 | ||
| 290 | +- [ ] **2. 实现最小代码**:修 lint / build(`tsc --noEmit`)/ 类型问题;确认语义色全部 `var(--color-*)`、无硬编码 hex/rgba(工具栏深色装饰 scoped 例外,D10);确认无 `TBD/TODO/【人工填写】`;确认 FE-01/FE-02/FE-03 既有单测/E2E 不回归(尤其 `router.test.tsx`、`shell.spec.ts`、`usrApi.userlist.test.ts`、`useUserList.test.tsx`)。 | ||
| 291 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run lint && npm run build && npm run test:unit && npm run test:e2e` 全绿。 | ||
| 292 | +- [ ] **4. commit**:`chore(usr): FE-04 门禁回归通过 REQ-USR-001 REQ-USR-002` | ||
| 293 | + | ||
| 294 | +--- | ||
| 295 | + | ||
| 296 | +## 完成判据(Definition of Done) | ||
| 297 | + | ||
| 298 | +1. `/usr/users/new` 与 `/usr/users/:id` 渲染真实 `UserDetailPage`(复刻原型 `#screen-userdetail` 的工具栏/3 列表单网格/权限页签条/权限分类列表),create/edit 模式由路由 `:id` 判定;写提交真实对接 `POST /api/usr/users`(create)/ `PUT /api/usr/users/{id}`(edit),下拉/权限/edit 预填经 `GET /api/usr/employees`/`GET /api/usr/permissions`/`GET /api/usr/users`(spec § 1/§ 2/§ 4 / D1/D2/D4)。 | ||
| 299 | +2. 状态机覆盖并测试固化:`initialLoading`(T3/T8)、`editing`(T3/T4/T8)、`submitting`(T3/T7)、`submitError`(T3/T8)、`submitSuccess`(T3/T8)、`loadError`(T3/T8/T9)(spec § 3)。 | ||
| 300 | +3. 业务规则 BR1~BR17 在 hook/组件/E2E 有断言:BR1(T4)、BR2(T3/T4/T8)、BR3(T2/T4/T8/T9)、BR4(T4)、BR5(T3/T4)、BR6(T2/T4/T8)、BR7(T4)、BR8(T2/T4)、BR9(T1/T2)、BR10(T3/T5)、BR11(T1/T2/T3/T5)、BR12(T3/T7/T8/T9)、BR13(T7/T8)、BR14(T7/T8)、BR15(T3/T7)、BR16(T3/T8/T9)、BR17(T2/T3/T8/T9)(spec § 5)。 | ||
| 301 | +4. 字段定义/提交映射对齐 REQ 表 1/表 2 + docs/05 请求体 + 原型(8 字段 + 权限 `permissionIds`,create 不传密码、edit 不传 `sUserName`,全量覆盖语义,spec § 6 / T1/T2);`UserVO`(FE-03)/`EmployeeOption`/`PermissionItem` 字段映射一致(D1/D2/D3)。 | ||
| 302 | +5. API 经 `usrApi.*` → `request.ts`(拆 `Result`、`ApiError` 分流、被动 401 统一登出复用 FE-01/FE-02),页面不散用 axios(docs/04 § 2.3);单据态在页面 hook 不进 Redux(docs/04 § 2.2 / spec D7)。 | ||
| 303 | +6. 错误码分流文案对齐 spec § 4(40901 用户名就近高亮、40401 返回列表、40001/40301 message、网络兜底;loadError 重试入口;被动 401 统一跳登录)。 | ||
| 304 | +7. 占位项不杜撰后端端点:工具栏删除/重置密码/功能 + 5 个查看权限页签 + 设置齿轮均占位(`message.info('功能开发中')`/`disabled`),作废/取消作废经 PUT `iIsVoid` 承载(spec D8/D9)。 | ||
| 305 | +8. 语义色只用 `var(--color-*)`,AntD `colorPrimary` 沿用 FE-01 `ConfigProvider`;工具栏深色底 scoped 装饰不新增全局 token、不挪用语义 token(spec § 7 / D10)。 | ||
| 306 | +9. 全部落点在 `frontend/**`,无 `backend/` / `sql/` / `scripts/` 改动;改 `router/index.tsx`(占位换真实页)、`usrApi.ts`/`types.ts`(增写端点契约)属共享骨架,已在《模块完成报告》留痕。 | ||
| 307 | +10. 门禁全绿:`npm run lint` / `npm run build` / `npm run test:unit` / `npm run test:e2e`(docs/04 § 零)。 | ||
| 308 | + | ||
| 309 | +## 自审记录 | ||
| 310 | + | ||
| 311 | +- **占位符扫描**:本计划无 `【人工填写:】` / `TBD` / `TODO` 真实占位(正文 `TBD/TODO` 仅作为「禁止出现的字样」被引用)。 | ||
| 312 | +- **spec coverage**:§ 1 关联 REQ/原型/路由/落地目录 → 架构 + T1/T8(路由接线);§ 2 组件树 → T4(UserBasicForm)/T5(PermissionGroupList)/T6(PermissionTabs)/T7(UserDetailToolbar)/T8(Page 装配);§ 3 状态机 6 态 → 见 DoD 第 2 条;§ 4 端点/错误码 → T1(api)/T3(错误码分流)/T8(就近高亮/loadError 重试);§ 5 BR1-BR17 → 见 DoD 第 3 条逐条映射;§ 6 字段定义与提交映射 → T1/T2(纯函数)/T4(渲染)/T5(权限);§ 7 tokens → T4/T5/T6/T7/T10;§ 8 decisions D1-D11 已落实于架构/合同级常量/任务(D1 员工端点=T1/T4;D2 权限端点=T1/T5;D3 逐项展示=T5;D4 edit 预填复用列表=T1/T3/T8;D5 取消二次确认+防重=T3/T7/T8;D6 回流列表=T3/T8;D7 本地 hook=T3;D8 占位按钮/作废=T7;D9 占位页签=T6;D10 工具栏深色=T7;D11 用户号前端必填=T4)。 | ||
| 313 | +- **类型一致性**:`EmployeeOption`/`PermissionItem`/`UserCreateReq`/`UserUpdateReq`/`UserDetailMode`/`UserFormValues`(跨 T1/T2/T3/T4/T5/T8 一致)、复用 FE-03 `UserVO`/`PageResult`/`UserListQuery`、`USER_TYPE_OPTIONS`/`LANGUAGE_OPTIONS`/`CREATE_DEFAULTS`/`USERNAME_PATTERN`/`MODE_CREATE`/`MODE_EDIT`、错误码常量(40001/40901/40401/40301/-1)、`createUser`/`updateUser`/`getUserDetail`/`listEmployees`/`listPermissions`/`useUserDetail`/`toCreateReq`/`toUpdateReq`/`userVoToFormValues`/`UserDetailToolbar`/`UserBasicForm`/`PermissionTabs`/`PermissionGroupList`/`UserDetailPage` 签名跨 T1-T10 一致;路由 path(`/usr/users` / `/usr/users/new` / `/usr/users/:id`)与 FE-02/FE-03 合同级常量一致;`ApiError`/`TOKEN_STORAGE_KEY`/`NETWORK_ERROR_CODE` 复用 FE-01 `request.ts`。 | ||
| 314 | +- **作用域自审**:所有 `impl_file` / `test_file` 均以 `frontend/` 开头;无 `backend/` / `sql/` / `scripts/` 落点。改 `src/router/index.tsx`、`src/api/usrApi.ts`、`src/api/types.ts` 属 FE 共享骨架扩展(非新阶段越界),已在架构段与 DoD 第 9 条登记留痕要求。 | ||
| 315 | +- **本计划承接的跨阶段待对齐项(自 spec § 8 末注)**:D1/D2 的支撑只读端点 `GET /api/usr/employees`、`GET /api/usr/permissions` 须在后端编码期于 REQ-USR-001/002 后端实现内补齐契约(如同 FE-01 `GET /api/usr/companies` 先例);前端 E2E/单测以桩覆盖,不依赖真实后端起服,故本 FE 可独立按 TDD 红绿推进。 |
docs/superpowers/reviews/2026-06-01-FE-01-verify.md
0 → 100644
| 1 | +# FE-01 登录页 — 功能测试证据(verify, round=0) | ||
| 2 | + | ||
| 3 | +> 阶段:前端(frontend)。作用域:`frontend/**`(无 `backend/` / `sql/` / `scripts/` 越界)。 | ||
| 4 | +> 上游 spec:`docs/superpowers/specs/2026-06-01-FE-01.md`;上游 plan:`docs/superpowers/plans/2026-06-01-FE-01.md`。 | ||
| 5 | +> 关联 REQ:REQ-USR-004(登录用户)。提交标签 `REQ-USR-004`,T0~T11 共 12 个提交。 | ||
| 6 | +> 测试命令来源:`docs/04-技术规范.md § 零`(unit=`npm run test:unit` / e2e=`npm run test:e2e`),在 `frontend/` 子项目执行。 | ||
| 7 | + | ||
| 8 | +--- | ||
| 9 | + | ||
| 10 | +## 1. 结论 | ||
| 11 | + | ||
| 12 | +| 目标 | 命令 | exit_code | passed | failed | 结论 | | ||
| 13 | +|---|---|---|---|---|---| | ||
| 14 | +| 单测(jsdom / Vitest + RTL) | `npm run test:unit`(`cd frontend`) | `0` | `32` | `0` | 全绿 | | ||
| 15 | +| E2E(Playwright chromium) | `npm run test:e2e`(`cd frontend`) | `0` | `4` | `0` | 全绿 | | ||
| 16 | + | ||
| 17 | +**总判定:通过(两目标 exit_code=0 且 failed=0)。** `failed_list` 均为空。可进入 review 阶段。 | ||
| 18 | + | ||
| 19 | +--- | ||
| 20 | + | ||
| 21 | +## 2. 单测证据(unit) | ||
| 22 | + | ||
| 23 | +- **command**:`cd /Users/reporkey/Desktop/mvp/test6/frontend && npm run test:unit`(即 `vitest run`) | ||
| 24 | +- **exit_code**:`0` | ||
| 25 | +- **passed**:`32` | ||
| 26 | +- **failed**:`0` | ||
| 27 | +- **failed_list**:(空) | ||
| 28 | +- **test files**:`10 passed (10)` | ||
| 29 | + | ||
| 30 | +测试文件清单(10 个,覆盖 spec § 3 状态机六态 + § 5 BR1~BR11): | ||
| 31 | + | ||
| 32 | +| 文件 | 用例数 | 覆盖点 | | ||
| 33 | +|---|---|---| | ||
| 34 | +| `tests/unit/smoke.test.tsx` | 1 | 工程骨架冒烟(T0) | | ||
| 35 | +| `tests/unit/request.test.ts` | — | Axios 实例 baseURL `/api`、Result 拆包、`ApiError` 业务码/网络码、Authorization 注入(T1) | | ||
| 36 | +| `tests/unit/usrApi.test.ts` | — | `login` POST `/usr/login`、`fetchCompanies` GET `/usr/companies`(T2) | | ||
| 37 | +| `tests/unit/authSlice.test.ts` | — | `setCredentials`/`clearCredentials` + token 持久化 + initialState 读取(T3) | | ||
| 38 | +| `tests/unit/LoginPage.layout.test.tsx` | 3 | 品牌头/主视觉/页脚/卡片标题/三字段+按钮(T4,BR3 掩码) | | ||
| 39 | +| `tests/unit/LoginPage.companies.test.tsx` | 5 | 版本下拉 loading/idle(D8 label)/单项自动选中/空态/取数失败重试(T5,BR5) | | ||
| 40 | +| `tests/unit/LoginPage.validation.test.tsx` | 2 | 必填校验拦截 + 全填提交 payload(T6,BR1/BR2/BR4) | | ||
| 41 | +| `tests/unit/LoginPage.submitting.test.tsx` | 2 | 提交中 loading+禁用 + 防重复提交(T7,BR10) | | ||
| 42 | +| `tests/unit/LoginPage.success.test.tsx` | 2 | 写 authSlice+持久化+`navigate('/',{replace})`(T8,BR9) | | ||
| 43 | +| `tests/unit/LoginPage.error.test.tsx` | 6 | 40101/40302/42901/40001/网络异常文案分流 + 失败后清空聚焦+保留(T9,BR6/7/8+D5) | | ||
| 44 | + | ||
| 45 | +> 注:`tests/unit/renderLogin.tsx` 为共享渲染工具,非测试文件,不计入 10 个 test files。 | ||
| 46 | + | ||
| 47 | +`stdout_excerpt`(≤30 行,去 act 警告噪声): | ||
| 48 | + | ||
| 49 | +``` | ||
| 50 | + ✓ tests/unit/LoginPage.layout.test.tsx (3 tests) 291ms | ||
| 51 | + ✓ tests/unit/LoginPage.companies.test.tsx (5 tests) 491ms | ||
| 52 | + ✓ tests/unit/LoginPage.success.test.tsx (2 tests) 519ms | ||
| 53 | + ✓ LoginPage 登录成功 > dispatches setCredentials and persists token on success 358ms | ||
| 54 | + ✓ tests/unit/LoginPage.validation.test.tsx (2 tests) 584ms | ||
| 55 | + ✓ tests/unit/LoginPage.submitting.test.tsx (2 tests) 598ms | ||
| 56 | + ✓ LoginPage 提交中态 > button loading and fields disabled while submitting 362ms | ||
| 57 | + ✓ tests/unit/LoginPage.error.test.tsx (6 tests) 999ms | ||
| 58 | + ✓ LoginPage 登录失败错误码分流 > 40101 shows 用户名或密码错误 and clears+focuses password 370ms | ||
| 59 | + | ||
| 60 | + Test Files 10 passed (10) | ||
| 61 | + Tests 32 passed (32) | ||
| 62 | + Duration 2.46s | ||
| 63 | +``` | ||
| 64 | + | ||
| 65 | +> 运行期 React `act(...)` 警告为 AntD + 异步 `useEffect`(版本预取)触发的 stderr 提示,**非断言失败**,不影响 exit_code=0 / 32 passed;登记为已知噪声(见 decisions)。 | ||
| 66 | + | ||
| 67 | +--- | ||
| 68 | + | ||
| 69 | +## 3. E2E 证据(e2e) | ||
| 70 | + | ||
| 71 | +- **command**:`cd /Users/reporkey/Desktop/mvp/test6/frontend && npm run test:e2e`(即 `playwright test`,chromium) | ||
| 72 | +- **exit_code**:`0` | ||
| 73 | +- **passed**:`4` | ||
| 74 | +- **failed**:`0` | ||
| 75 | +- **failed_list**:(空) | ||
| 76 | + | ||
| 77 | +用例清单(`tests/e2e/login.spec.ts`,`page.route` 桩 `**/api/usr/companies` 与 `**/api/usr/login`,不依赖真实后端): | ||
| 78 | + | ||
| 79 | +1. `loads /login and shows version options` — 访问 `/login` 渲染桩版本项。 | ||
| 80 | +2. `blocks submit with validation when empty` — 空提交必填拦截、未发起 login。 | ||
| 81 | +3. `successful login navigates away from /login` — 填全+桩成功 → 离开 `/login` 到 `/`、见「登录成功」。 | ||
| 82 | +4. `failed login stays on /login with error` — 桩 40101 → 停留 `/login`、见「用户名或密码错误」。 | ||
| 83 | + | ||
| 84 | +`stdout_excerpt`(≤30 行,去 DeprecationWarning 噪声): | ||
| 85 | + | ||
| 86 | +``` | ||
| 87 | +Running 4 tests using 4 workers | ||
| 88 | + ✓ 4 [chromium] › tests/e2e/login.spec.ts:22:3 › 登录页关键旅程 › loads /login and shows version options (360ms) | ||
| 89 | + ✓ 1 [chromium] › tests/e2e/login.spec.ts:32:3 › 登录页关键旅程 › blocks submit with validation when empty (414ms) | ||
| 90 | + ✓ 3 [chromium] › tests/e2e/login.spec.ts:77:3 › 登录页关键旅程 › failed login stays on /login with error (699ms) | ||
| 91 | + ✓ 2 [chromium] › tests/e2e/login.spec.ts:50:3 › 登录页关键旅程 › successful login navigates away from /login (712ms) | ||
| 92 | + 4 passed (1.8s) | ||
| 93 | +``` | ||
| 94 | + | ||
| 95 | +> Node `DEP0205 module.register()` 为 Playwright/Node 内部弃用提示,非测试失败,不影响 exit_code=0 / 4 passed。 | ||
| 96 | + | ||
| 97 | +--- | ||
| 98 | + | ||
| 99 | +## 4. 作用域与命令合规核对 | ||
| 100 | + | ||
| 101 | +- 所有实现/测试文件均在 `frontend/**`;未触 `backend/` / `sql/` / `scripts/`,无越界。 | ||
| 102 | +- 测试命令取自 `docs/04-技术规范.md § 零`:unit=`npm run test:unit`、e2e=`npm run test:e2e`,与 `frontend/package.json` scripts 一致。 | ||
| 103 | +- 本轮为 round=0,证据落盘固定路径 `docs/superpowers/reviews/2026-06-01-FE-01-verify.md`(`-verify.md` 后缀)。 | ||
| 104 | + | ||
| 105 | +--- | ||
| 106 | + | ||
| 107 | +## 5. 自主决策记录(decisions) | ||
| 108 | + | ||
| 109 | +| # | 问题 | 选择 | 依据 | 置信度 | | ||
| 110 | +|---|---|---|---|---| | ||
| 111 | +| V1 | 本环境未暴露独立的「子会话/Agent 派发」工具,如何执行 plan「派子会话依次跑 unit + e2e」 | 由本(fe-feature-verify 非交互子代理)会话经唯一可用执行原语 Bash 直接运行 `npm run test:unit` / `npm run test:e2e` 并采集结构化结果(command/exit_code/passed/failed/failed_list/stdout_excerpt),按结构化 JSON 渲染本证据 | 当前工具集只暴露 Bash 作为执行原语,无 sub-session spawn 工具;结果仍按 plan 约定的结构化 JSON 字段采集与渲染,语义等价、不污染源码 | high | | ||
| 112 | +| V2 | 单测运行期大量 React `act(...)` 警告是否构成失败 | 视为非阻断噪声,不计入 failed | 警告打印到 stderr,由 AntD + 异步 `useEffect` 版本预取触发;Vitest 报告 exit_code=0 / 32 passed / 0 failed,断言全过 | high | | ||
| 113 | + | ||
| 114 | +--- | ||
| 115 | + | ||
| 116 | +## 6. 摘要(供 review stage 引用) | ||
| 117 | + | ||
| 118 | +- unit:`exit_code=0` / `passed=32` / `failed=0` / `failed_list=[]`(10 test files)。 | ||
| 119 | +- e2e:`exit_code=0` / `passed=4` / `failed=0` / `failed_list=[]`(chromium)。 | ||
| 120 | +- 总判定:**全绿通过**,可进入 review。 |
docs/superpowers/reviews/2026-06-01-FE-01.md
0 → 100644
| 1 | +# FE-01 登录页 — AI 自审报告(第 1 轮) | ||
| 2 | + | ||
| 3 | +- 阶段:前端(frontend) | ||
| 4 | +- 关联 REQ:REQ-USR-004(主);配套 `GET /api/usr/companies` | ||
| 5 | +- 关联原型:`prototype/erp.html` → `<section id="screen-login">` | ||
| 6 | +- 规格:`docs/superpowers/specs/2026-06-01-FE-01.md` | ||
| 7 | +- 裁决:**approve** | ||
| 8 | +- must-fix issues:无(空数组) | ||
| 9 | + | ||
| 10 | +## 审阅范围 | ||
| 11 | + | ||
| 12 | +本轮 diff(自 `2dce637^` 起的 FE-01 提交链)落在 `frontend/` 内,作用域合规,未触碰 `backend/` / `sql/` / `scripts/`。核心文件: | ||
| 13 | + | ||
| 14 | +- `frontend/src/pages/usr/Login/LoginPage.tsx` | ||
| 15 | +- `frontend/src/pages/usr/Login/Login.module.css` | ||
| 16 | +- `frontend/src/pages/usr/Login/loginMessages.ts` | ||
| 17 | +- `frontend/src/api/{request.ts,types.ts,usrApi.ts}` | ||
| 18 | +- `frontend/src/store/slices/authSlice.ts`、`frontend/src/router/index.tsx`、`frontend/src/App.tsx`、`frontend/src/styles/theme.ts` | ||
| 19 | + | ||
| 20 | +## 质量闸(独立复跑验证) | ||
| 21 | + | ||
| 22 | +- `tsc --noEmit`:通过(exit 0)。 | ||
| 23 | +- `vitest run`:10 文件 / 32 用例全绿(含布局、版本预加载/空态/重试、校验、提交中态、错误码分流、成功落地、authSlice、request 拦截器)。 | ||
| 24 | +- `eslint .`:通过(exit 0)。 | ||
| 25 | + | ||
| 26 | +## 前端 7 维 checklist | ||
| 27 | + | ||
| 28 | +1. **原型一致性(客观)— 通过**:复刻 `.login-wrap` 三段式(品牌头 / 深蓝主视觉+右侧浮层登录卡 / 页脚版权);登录卡 `right:8% top:50% translateY(-50%)`、宽 380px、主操作为底部 `block` 主按钮,与原型 DOM 结构与主操作位置一致。下拉以 AntD `Select` 等价复刻原型 `.lf.dropdown`,属允许的实现差异。 | ||
| 29 | +2. **Design Tokens(客观)— 通过**:语义色(按钮 / 文字 / 标题 / 占位 / 边框 / 背景 / 错误)全部走 `var(--color-*)`,无硬编码。`Login.module.css` 中的 hex/rgba 全部位于 `.hero` 主视觉装饰(深蓝渐变 / 网格透视 / hero 文案)及阴影 alpha,属规格 D7 明确豁免的「登录页局部装饰,不挪用语义 token、不新增全局 token」。`LoginPage.tsx` 内两处 `fill="#0e1216"` / `#3a6cb6` 为原型逐字复刻的品牌 Logo / ICP 图标 SVG 装饰填充,非语义 UI 色,归同一装饰豁免。`theme.ts` 的 `#1890ff` 为读取 `--color-primary` 失败时的兜底常量(= token 现值),运行时优先读 CSS 变量。token 优先于原型的色值精度差异处理正确(原型品牌名为琥珀色 `#e0a020`,实现改用 `--color-primary`,符合「tokens.css 优先于 prototype」)。 | ||
| 30 | +3. **无障碍(best-effort)**:表单控件仅用 `placeholder`,无显式 `<label>`/`aria-label`——但原型本身即 placeholder-only 无 label 区,规格未要求可见标签,故按 best-effort 处理,不作 must-fix(建议项见下)。装饰 SVG 正确带 `aria-hidden`;无危险/不可逆操作,无需确认弹窗;支持回车提交(AntD Form 默认),键盘可达。 | ||
| 31 | +4. **响应式(best-effort)**:`@media (max-width:768px)` 将卡片回流为居中静态布局(D4)。目标用户为桌面端 ERP,无 must-fix。 | ||
| 32 | +5. **业务校验前端复刻(客观)— 通过**:BR1/BR2/BR4 用 AntD `Form` 必填校验(文案「请输入用户名」「请输入密码」「请选择版本」);BR3 `Input.Password` 掩码;BR6/BR7/BR8 错误码 40101/40302/42901 文案与后端语义一致且不细化枚举;BR9 成功落地;BR10 `submitting` 期间禁用+早返回防重复提交;BR11 明文原样提交。 | ||
| 33 | +6. **API 调用一致性(客观)— 通过**:`POST /api/usr/login`、`GET /api/usr/companies` 均经统一 `request.ts` Axios 实例,无裸 `fetch`/手拼 URL。请求体 `{sUserName,password,companyId}`、响应 `{token,user:{id,sUserName,sUserType,sLanguage}}`、公司项 `{id,sCompanyName,sVersion}` 与 docs/05 + REQ-USR-004 后端规格(§2.3 / §8 D1,含 `GET /api/usr/companies` 与 `42901` 限流码)一致。响应拦截器拆 `Result`、登录端点不强制带 token,符合 docs/04 § 2.3/2.4。 | ||
| 34 | +7. **状态机覆盖(客观)— 通过**:5 态齐备——`companiesLoading`(Select `loading`+禁用+占位「加载版本中…」)、`idle`、`empty`(`notFoundContent="暂无可用版本"`+轻提示)、`submitting`(按钮 loading + Form 整体 `disabled` + 早返回)、`error`(错误码分流 `message.error` + 版本失败重试入口)、`success`(`setCredentials` + 持久化 + `navigate('/',{replace:true})`)。 | ||
| 35 | + | ||
| 36 | +## 通用四维 | ||
| 37 | + | ||
| 38 | +- 计划对齐:实现与 spec §2-§7 / REQ-USR-004 输入表逐项对应,自主决策 D1-D8 在代码注释与结构中可追溯。 | ||
| 39 | +- 质量:错误处理集中在响应拦截器 + 页面错误码映射;TS 类型契约(`types.ts`)跨 task 共享;测试覆盖充分;登录端点放行、token 仅登录态注入,无明显安全/性能问题。 | ||
| 40 | +- 架构:api / store / pages / router / styles 分层清晰,页面只调 `usrApi`,不散用 axios;token 持久化键 `TOKEN_STORAGE_KEY` 单点定义。 | ||
| 41 | +- 文档:文件含 `REQ-USR-004` 注释与决策追溯,符合 CLAUDE.md 约定。 | ||
| 42 | + | ||
| 43 | +## 非阻塞建议(口头,不入 issues) | ||
| 44 | + | ||
| 45 | +- A11y 增强(建议):可为用户名 / 密码 / 版本三控件补 `aria-label`(如「用户名」「密码」「版本」),在 placeholder-only 布局下提升读屏可用性。非 must-fix。 | ||
| 46 | +- `authSlice.setCredentials` reducer 内做 `localStorage` 写副作用为 MVP 取舍(D6 已登记),后续如引入 SSR/严格纯 reducer 规范可迁至中间件或 thunk。非本轮阻塞。 | ||
| 47 | + | ||
| 48 | +## 决策(decisions) | ||
| 49 | + | ||
| 50 | +本轮审阅未做改变源码语义的自主默认;对 D2/D7 的「装饰色豁免」「品牌 SVG 装饰填充非语义色」的判定均基于规格 D7 与原型逐字复刻证据,记录如下: | ||
| 51 | + | ||
| 52 | +- question: `LoginPage.tsx`/`Login.module.css` 中的 hex/rgba 是否构成 Design Token 违规? | choice: 不构成,判 approve | rationale: 全部位于主视觉装饰 / 品牌 Logo / ICP 图标 SVG / 阴影 alpha / 兜底常量,规格 D7 已豁免,且语义色无一硬编码 | confidence: high | ||
| 53 | +- question: 表单无 `<label>` 是否 must-fix? | choice: 否,降级为口头建议 | rationale: 原型即 placeholder-only 无 label 区,规格未要求可见标签,按 a11y best-effort 规则不单独触发 request-changes | confidence: high |
docs/superpowers/reviews/2026-06-01-FE-02-verify.md
0 → 100644
| 1 | +# FE-02 证据验证报告(verify, round=0) | ||
| 2 | + | ||
| 3 | +> 业务功能:FE-02 主页与导航框架(应用外壳 + 登录后落地主页 + 全部导航总览 + 标签页栈 + 路由壳与守卫) | ||
| 4 | +> 上游 spec:`docs/superpowers/specs/2026-06-01-FE-02.md` | ||
| 5 | +> 上游 plan:`docs/superpowers/plans/2026-06-01-FE-02.md` | ||
| 6 | +> 阶段:前端(frontend)。作用域:所有实现/测试文件落在 `frontend/` 下,零 `backend/` / `sql/` / `scripts/` 越界。 | ||
| 7 | +> 分支:`frontend-phase` | ||
| 8 | +> 验证时间:2026-06-01 | ||
| 9 | +> 结论:**全部门禁绿色(PASS)** | ||
| 10 | + | ||
| 11 | +--- | ||
| 12 | + | ||
| 13 | +## 1. 验证目标与命令来源 | ||
| 14 | + | ||
| 15 | +测试命令取自 `docs/04-技术规范.md § 零`(frontend 测试命令表): | ||
| 16 | + | ||
| 17 | +| 门禁 | 命令 | 工作目录 | | ||
| 18 | +|---|---|---| | ||
| 19 | +| lint | `npm run lint`(= `eslint .`) | `frontend/` | | ||
| 20 | +| build | `npm run build`(= `tsc --noEmit && vite build`) | `frontend/` | | ||
| 21 | +| unit(jsdom / Vitest) | `npm run test:unit`(= `vitest run`) | `frontend/` | | ||
| 22 | +| e2e(Playwright) | `npm run test:e2e`(= `playwright test`) | `frontend/` | | ||
| 23 | + | ||
| 24 | +unit 目标来源:plan 中「测试先行类型 = jsdom」的全部组件/hook/数据单测;e2e 目标来源:plan 中「测试先行类型 = Playwright E2E」的 `tests/e2e/shell.spec.ts`(并含 FE-01 `login.spec.ts` 无回归)。本轮按上游摘要要求对**全量套件**做门禁验证(含 FE-01 回归),而非仅过滤 FE-02 子集。 | ||
| 25 | + | ||
| 26 | +--- | ||
| 27 | + | ||
| 28 | +## 2. 门禁结果总览(结构化) | ||
| 29 | + | ||
| 30 | +| 门禁 | command | exit_code | 统计 | failed | 结论 | | ||
| 31 | +|---|---|---|---|---|---| | ||
| 32 | +| lint | `npm run lint` | 0 | 0 error | 0 | PASS | | ||
| 33 | +| build | `npm run build` | 0 | 构建成功,产出 `dist/`(index.js 863.35 kB / index.css 9.71 kB;仅 chunk>500kB 体积告警,非错误) | 0 | PASS | | ||
| 34 | +| unit | `npm run test:unit` | 0 | 25 文件 / 88 用例 全部通过 | 0 | PASS | | ||
| 35 | +| e2e | `npm run test:e2e` | 0 | 9 用例全部通过(login 4 + shell 5) | 0 | PASS | | ||
| 36 | + | ||
| 37 | +failed_list(unit):(空) | ||
| 38 | +failed_list(e2e):(空) | ||
| 39 | + | ||
| 40 | +> 与上游 TDD 摘要门禁口径一致:lint=0 error、build 成功、test:unit=88/88(25 文件,含 FE-01 无回归)、test:e2e=9/9(login 4 + shell 5)。 | ||
| 41 | + | ||
| 42 | +--- | ||
| 43 | + | ||
| 44 | +## 3. 单元测试明细(unit,jsdom / Vitest) | ||
| 45 | + | ||
| 46 | +``` | ||
| 47 | +Test Files 25 passed (25) | ||
| 48 | + Tests 88 passed (88) | ||
| 49 | +exit_code = 0 | ||
| 50 | +``` | ||
| 51 | + | ||
| 52 | +按文件逐项(用例数): | ||
| 53 | + | ||
| 54 | +| 测试文件 | 用例数 | 覆盖(plan 任务 / BR) | | ||
| 55 | +|---|---|---| | ||
| 56 | +| tests/unit/renderShell.smoke.test.tsx | 1 | T0 渲染壳冒烟 | | ||
| 57 | +| tests/unit/RequireAuth.test.tsx | 3 | T1 守卫三态 authResolving/unauthenticated/ready(BR1) | | ||
| 58 | +| tests/unit/RedirectIfAuthed.test.tsx | 3 | T2 已登录访问 /login 回主页(BR2) | | ||
| 59 | +| tests/unit/useTabStack.test.tsx | 6 | T3 标签栈联动(BR4/BR5/BR6) | | ||
| 60 | +| tests/unit/navConfig.test.ts | 4 | T4 导航静态配置(D1/D4/BR7/BR8) | | ||
| 61 | +| tests/unit/dashboardData.test.ts | 4 | T4 仪表盘静态数据(D2) | | ||
| 62 | +| tests/unit/KpiBoard.test.tsx | 4 | T5 KPI 合并网格 + 空数据 Empty(BR11/empty 态/D5) | | ||
| 63 | +| tests/unit/HomePage.test.tsx | 5 | T6 主页区域组合 + 常用操作跳转(BR8/BR11) | | ||
| 64 | +| tests/unit/NavOverlay.test.tsx | 5 | T7 导航总览开关/分组/路由项与占位项(BR7/BR8/D4) | | ||
| 65 | +| tests/unit/AppLayout.topbar.test.tsx | 6 | T8 顶栏结构 + 当前用户文案 + 退出登录(BR3/BR9) | | ||
| 66 | +| tests/unit/AppLayout.shell.test.tsx | 5 | T9 外壳装配 + 标签↔路由同步(ready/navOverlayOpen/tabOpen 态) | | ||
| 67 | +| tests/unit/router.test.tsx | 5 | T10 路由表接线(BR1/BR2/D7) | | ||
| 68 | +| tests/unit/AppErrorBoundary.test.tsx | 1 | T10 子组件抛错兜底(error 态) | | ||
| 69 | +| tests/unit/request.unauthorized.test.ts | 3 | T11 401 触发 onUnauthorized 回调(BR10/D11) | | ||
| 70 | +| tests/unit/AppLayout.unauthorized.test.tsx | 1 | T12 壳层注册 401 登出处理(BR10) | | ||
| 71 | +| tests/unit/authSlice.test.ts | 3 | FE-01 回归 | | ||
| 72 | +| tests/unit/request.test.ts | 6 | FE-01 回归 | | ||
| 73 | +| tests/unit/usrApi.test.ts | 2 | FE-01 回归 | | ||
| 74 | +| tests/unit/smoke.test.tsx | 1 | FE-01 回归 | | ||
| 75 | +| tests/unit/LoginPage.layout.test.tsx | 3 | FE-01 回归 | | ||
| 76 | +| tests/unit/LoginPage.validation.test.tsx | 2 | FE-01 回归 | | ||
| 77 | +| tests/unit/LoginPage.submitting.test.tsx | 2 | FE-01 回归 | | ||
| 78 | +| tests/unit/LoginPage.success.test.tsx | 2 | FE-01 回归 | | ||
| 79 | +| tests/unit/LoginPage.error.test.tsx | 6 | FE-01 回归 | | ||
| 80 | +| tests/unit/LoginPage.companies.test.tsx | 5 | FE-01 回归 | | ||
| 81 | +| **合计** | **88** | FE-02 新增 18 文件 + FE-01 回归 7 文件 = 25 文件 | | ||
| 82 | + | ||
| 83 | +> stderr 中的 antd `act(...)`/message-in-render 提示与 jsdom XHR `AggregateError` 为非阻断告警(来自 FE-01 LoginPage 既有用例的异步桩场景),不影响任何用例判定,全部 88 用例 PASS。 | ||
| 84 | + | ||
| 85 | +--- | ||
| 86 | + | ||
| 87 | +## 4. 端到端测试明细(e2e,Playwright) | ||
| 88 | + | ||
| 89 | +``` | ||
| 90 | +Running 9 tests using 9 workers | ||
| 91 | +9 passed (1.9s) | ||
| 92 | +exit_code = 0 | ||
| 93 | +``` | ||
| 94 | + | ||
| 95 | +逐项(test title): | ||
| 96 | + | ||
| 97 | +| spec | 用例 | | ||
| 98 | +|---|---| | ||
| 99 | +| tests/e2e/login.spec.ts | loads /login and shows version options | | ||
| 100 | +| tests/e2e/login.spec.ts | blocks submit with validation when empty | | ||
| 101 | +| tests/e2e/login.spec.ts | successful login navigates away from /login | | ||
| 102 | +| tests/e2e/login.spec.ts | failed login stays on /login with error | | ||
| 103 | +| tests/e2e/shell.spec.ts | login then lands on home with topbar and KPI title | | ||
| 104 | +| tests/e2e/shell.spec.ts | open and close 全部导航 overlay | | ||
| 105 | +| tests/e2e/shell.spec.ts | open 用户列表 tab from common ops then close back to home | | ||
| 106 | +| tests/e2e/shell.spec.ts | logout returns to /login | | ||
| 107 | +| tests/e2e/shell.spec.ts | visiting / unauthenticated redirects to /login | | ||
| 108 | + | ||
| 109 | +> shell.spec 5 条覆盖 FE-02 关键旅程:登录落地主页(顶栏 + KPI 标题可见)、导航 overlay 显隐、常用操作打开/关闭「用户列表」标签并联动回主页、退出登录回 `/login`、未登录访问受保护根路由重定向 `/login`(BR1/BR3/BR7/BR8/BR9)。login.spec 4 条为 FE-01 回归无破坏。 | ||
| 110 | + | ||
| 111 | +--- | ||
| 112 | + | ||
| 113 | +## 5. 状态机与业务规则覆盖核对 | ||
| 114 | + | ||
| 115 | +状态机(spec § 3)均有断言覆盖:`authResolving`/`unauthenticated`/`ready`(T1/T10)、`navOverlayOpen`(T7/T9)、`tabOpen`(T3/T9)、`empty`(T5)、`error`(T10 ErrorBoundary)。 | ||
| 116 | + | ||
| 117 | +业务/交互规则 BR1~BR11(spec § 5)均落断言:BR1/BR2(T1/T2/T10)、BR3/BR9(T8)、BR4/BR5/BR6(T3/T9)、BR7(T7)、BR8(T6/T7/T9)、BR10(T11/T12)、BR11(T5/T6)。 | ||
| 118 | + | ||
| 119 | +--- | ||
| 120 | + | ||
| 121 | +## 6. 作用域与红线自审 | ||
| 122 | + | ||
| 123 | +- 全部实现文件与测试文件均位于 `frontend/` 下;本轮验证未触碰 `backend/` / `sql/` / `scripts/`,无越界。 | ||
| 124 | +- 无 `TBD` / `TODO` / `【人工填写】` 占位(实现摘要已声明,本轮证据不引入新占位)。 | ||
| 125 | +- 工作树验证前为 clean,分支 `frontend-phase`。 | ||
| 126 | + | ||
| 127 | +--- | ||
| 128 | + | ||
| 129 | +## 7. 验证方法说明(decisions 关联) | ||
| 130 | + | ||
| 131 | +本轮通过隔离的命令执行逐项跑出 lint / build / unit / e2e 四道门禁,证据严格依据真实命令的 `exit_code` 与统计输出渲染,未手工编造任何通过/失败计数。任一目标 `exit_code != 0` 或 `failed > 0` 时本应在渲染证据后 halt;本轮四项全部 `exit_code = 0`、`failed = 0`,故判定 PASS,可进入 review 阶段。 |
docs/superpowers/reviews/2026-06-01-FE-02.md
0 → 100644
| 1 | +# FE-02 主页与导航框架 — 代码审阅报告(第 1 轮) | ||
| 2 | + | ||
| 3 | +- 规格:`docs/superpowers/specs/2026-06-01-FE-02.md` | ||
| 4 | +- 阶段:前端(frontend) | ||
| 5 | +- 裁决:**approve** | ||
| 6 | +- 轮次:1 | ||
| 7 | +- 审阅范围:自 FE-01 approve(`3119517`)至 `HEAD` 的 `frontend/` 增量 diff(39 文件,2579+ 行) | ||
| 8 | + | ||
| 9 | +--- | ||
| 10 | + | ||
| 11 | +## 一、结论 | ||
| 12 | + | ||
| 13 | +FE-02 应用外壳(TopBar + NavOverlay + 标签栈 + 路由壳 + 路由守卫)与主页 KPI 看板落地页实现质量良好,与规格 / 原型 / Design Tokens 一致,未发现客观可验证的 must-fix 缺陷。验证基线: | ||
| 14 | + | ||
| 15 | +- `npm run lint`:通过(无 error / warning)。 | ||
| 16 | +- `npx tsc --noEmit`:通过(exit 0)。 | ||
| 17 | +- `npm run test:unit`:25 文件 / 88 用例全绿。 | ||
| 18 | +- e2e `tests/e2e/shell.spec.ts`:覆盖登录落地 / 导航浮层开关+Esc / 标签开关联动 / 退出登录 / 未登录重定向 5 条关键旅程。 | ||
| 19 | +- 作用域:全部改动落在 `frontend/`(与 `docs/`),未触碰 `backend/` / `sql/` / `scripts/`,符合前端阶段硬作用域。 | ||
| 20 | + | ||
| 21 | +--- | ||
| 22 | + | ||
| 23 | +## 二、前端 7 维 checklist 逐项 | ||
| 24 | + | ||
| 25 | +### 1. 原型一致性(客观)— 通过 | ||
| 26 | +- TopBar 复刻 `#topbar`:Logo(鹿角 SVG,点击回主页)+ 「全部导航」按钮 + 标签条(主页固定 + 业务标签可关)+ 右侧 搜索/通知/当前用户/更多,位置与原型 `.right` 一致。 | ||
| 27 | +- NavOverlay 复刻 `#nav-overlay`:左列 20 项一级模块(`navConfig.NAV_SIDE`,「系统设置」default active),右侧 7 列分组(`NAV_COLS`,标题与叶子标签逐字对齐 `navCols`,「用户列表」「系统功能模块设置」带 ★)。 | ||
| 28 | +- HomePage 复刻 `#screen-main`:`1fr / 280px` 主网格、`kpi-head`(标题 + 今日未处理/未清总数 + AI 按钮)、`three-col`(左角色/流程树 + 右 KPI 合并网格)、`common-ops`(「用户列表」路由 + 「系统功能模块设置」占位,与原型两条链接对齐)、`footer.foot` 文案逐字复刻。 | ||
| 29 | +- KPI 合并网格用 CSS Grid `gridRow span` 复刻(D5 兜底路径),`KPI_ROWS` 17 行 + roleSpan/subSpan/red 与原型 `kpiRows` 逐字一致。 | ||
| 30 | +- 结论:主操作位置、栅格列数、关键区域布局均无结构性偏移。 | ||
| 31 | + | ||
| 32 | +### 2. Design Tokens(客观)— 通过 | ||
| 33 | +- 语义色(主操作/文字/边框/错误/成功/表头/行 hover/选中)全部走 `var(--color-*)`,与 `src/styles/tokens.css` 对齐。 | ||
| 34 | +- 仅顶栏 `#1f1f23` / overlay `#2b3137` 等品牌深色 chrome 硬编码于 `AppLayout.module.css` scoped 样式——由 D9 显式登记(`tokens.css` 无对应品牌深色语义 token,且为纯装饰非状态语义),与 FE-01 § 7 D7 处理一致,不违反「语义色只用 token」约束。 | ||
| 35 | +- 未发现未登记的新 token。 | ||
| 36 | + | ||
| 37 | +### 3. 无障碍(best-effort,仅客观项可 must-fix)— 通过 | ||
| 38 | +- 交互控件可达:Logo `aria-label`;「全部导航」`aria-pressed`;标签关闭「✕」`role=button`/`tabIndex=0`/`aria-label`/Enter+Space 键盘触发;NavOverlay `role=dialog`/`aria-modal`;角色树项 `aria-pressed`。 | ||
| 39 | +- 退出登录走 AntD `Dropdown` 菜单项(键盘可达)。 | ||
| 40 | +- 搜索/通知/更多图标为 `aria-hidden` 纯装饰占位(D4,无业务交互),不构成可达性缺陷。 | ||
| 41 | +- 注:本壳层无破坏性操作(删除/不可逆),故确认对话框不适用。 | ||
| 42 | + | ||
| 43 | +### 4. 响应式(best-effort)— 不阻断 | ||
| 44 | +- 桌面定宽 ERP(D8),不设移动断点。符合规格,且按规约响应式不单独触发 request-changes。 | ||
| 45 | + | ||
| 46 | +### 5. 业务规则前端复刻(客观)— 通过 | ||
| 47 | +- BR1 未登录守卫(`RequireAuth` → `<Navigate to="/login" state.from>`);BR2 已登录访问 `/login` 回跳(`RedirectIfAuthed`);BR3 当前用户 `sUserName(sUserType)` + 缺失退化占位;BR4/5/6 标签栈固定主页/联动关闭/隐式开父标签(`useTabStack`);BR7 占位项「功能开发中」;BR8 用户列表跳转 `/usr/users`;BR9 退出登录清态+跳登录+`message.success`;BR10 被动 401 统一登出(`registerUnauthorizedHandler` + 拦截器);BR11 KPI/树只读高亮无副作用。均落地。 | ||
| 48 | + | ||
| 49 | +### 6. API 调用一致性(客观)— 通过 | ||
| 50 | +- 壳层不直接调用业务端点(与 docs/05 一致,无 KPI/dashboard/nav 端点,D1 不杜撰)。 | ||
| 51 | +- `request.ts` 为 FE-01 已建统一 Axios 实例(`baseURL=/api`);FE-02 仅新增 401 统一登出回调单例 + 拦截器分支,无 raw fetch / 手拼 URL。 | ||
| 52 | + | ||
| 53 | +### 7. 状态机覆盖(客观)— 通过 | ||
| 54 | +- `authResolving`(`Spin` 占位)/ `unauthenticated`(`Navigate`)/ `ready`(外壳就绪)/ `navOverlayOpen` / `tabOpen` / `empty`(KPI `Empty` 兜底)/ `error`(`AppErrorBoundary` Result + 返回主页)均处理。 | ||
| 55 | +- 「提交中」对本壳层不适用(无提交动作),规格 § 3 已将 5 态映射到外壳语义,非缺失。 | ||
| 56 | + | ||
| 57 | +--- | ||
| 58 | + | ||
| 59 | +## 三、通用维度 | ||
| 60 | + | ||
| 61 | +- 计划对齐:组件树 / 路由表 / decisions(D1~D10)与规格一一对应。 | ||
| 62 | +- 质量:lint/tsc/unit/e2e 全绿;文案集中于 `shellMessages.ts` 单一来源;标签栈逻辑抽为可测 hook。 | ||
| 63 | +- 架构:壳层 UI 态用本地 `useState`(D3),登录态复用 Redux `authSlice`,关注点分离清晰;401 回调单例解了「拦截器内无 React hooks」问题。 | ||
| 64 | +- 文档/注释:每文件含 `REQ-USR-003/004` 追溯注释,符合 CLAUDE.md 约定。 | ||
| 65 | + | ||
| 66 | +--- | ||
| 67 | + | ||
| 68 | +## 四、建议(非 must-fix,不计入 issues) | ||
| 69 | + | ||
| 70 | +- `AppErrorBoundary.handleGoHome` 用 `window.location.assign('/')` 触发整页刷新而非 SPA 路由跳转,存在轻微 UX 损耗(会丢失 SPA 状态)。功能正确,且注释已说明 class 组件无 hooks 约束;如后续优化可注入 navigate 或改 `useNavigate` 包装。属可选改进,不阻断。 | ||
| 71 | +- `KpiBoard` props 保留未使用的 `onNavigate`(KPI 链接纯展示);可在后续清理,无功能影响(lint 已通过,未触发 unused 规则因其为可选 prop)。 | ||
| 72 | + | ||
| 73 | +--- | ||
| 74 | + | ||
| 75 | +## 五、issues(must-fix) | ||
| 76 | + | ||
| 77 | +无。`verdict = approve`,`issues = []`。 |
docs/superpowers/reviews/2026-06-01-FE-03-verify.md
0 → 100644
| 1 | +# FE-03 用户列表与查询 — 证据验证报告(round=0) | ||
| 2 | + | ||
| 3 | +> 阶段:前端(frontend)。验证对象:`FE-03`(关联 `REQ-USR-003`,scope=usr)。 | ||
| 4 | +> 上游 spec:`docs/superpowers/specs/2026-06-01-FE-03.md`;上游 plan:`docs/superpowers/plans/2026-06-01-FE-03.md`。 | ||
| 5 | +> 验证时间:2026-06-01。本报告由 fe-feature-verify 渲染,主会话不自由编写测试,仅按测试命令的结构化结果落盘。 | ||
| 6 | + | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +## 一、验证结论 | ||
| 10 | + | ||
| 11 | +**全部通过(PASS)**。单测与 E2E 两个门禁目标 `exit_code` 均为 0,`failed=0`,可进入 review。 | ||
| 12 | + | ||
| 13 | +| 目标 | 命令 | exit_code | passed | failed | 结论 | | ||
| 14 | +|---|---|---|---|---|---| | ||
| 15 | +| 单元测试(jsdom / Vitest) | `npm run test:unit`(= `vitest run`) | 0 | 138(32 文件) | 0 | ✅ PASS | | ||
| 16 | +| 端到端(Playwright) | `npm run test:e2e`(= `playwright test`) | 0 | 15 | 0 | ✅ PASS | | ||
| 17 | + | ||
| 18 | +> 命令来源:`docs/04-技术规范.md § 零 命令清单`(前端 unit=`npm run test:unit`、e2e=`npm run test:e2e`)。本验证执行全量套件(覆盖 plan T1–T9 全部新增 jsdom 单测 + `userlist.spec.ts` E2E,并校验 FE-01/FE-02 既有 unit/E2E 不回归,对齐 plan T9「全量验证」门禁)。 | ||
| 19 | + | ||
| 20 | +--- | ||
| 21 | + | ||
| 22 | +## 二、单元测试(jsdom / Vitest) | ||
| 23 | + | ||
| 24 | +- 命令:`cd frontend && npm run test:unit` | ||
| 25 | +- exit_code:`0` | ||
| 26 | +- 结果:`Test Files 32 passed (32)` / `Tests 138 passed (138)` | ||
| 27 | +- failed_list:(空) | ||
| 28 | +- 覆盖 FE-03 新增的 7 个单测文件均通过: | ||
| 29 | + - `tests/unit/usrApi.userlist.test.ts`(listUsers 透传 query → GET /usr/users,返回 PageResult;3 用例) | ||
| 30 | + - `tests/unit/exportUtils.test.ts`(CSV/常量/默认值断言) | ||
| 31 | + - `tests/unit/useUserList.test.tsx`(状态机 6 态:initialLoading/loading/success/empty/error/exporting) | ||
| 32 | + - `tests/unit/UserFilterBar.test.tsx`(BR2/BR3/BR4/BR7/BR10 默认值/枚举/回车搜索/清空) | ||
| 33 | + - `tests/unit/UserTable.test.tsx`(BR1/BR6/BR11/BR12/BR14 序号/作废只读/行双击导航/受控分页/空态) | ||
| 34 | + - `tests/unit/UserToolbar.test.tsx`(BR8/BR9/BR13/D7 刷新/新增导航/导出中禁用/齿轮占位) | ||
| 35 | + - `tests/unit/UserListPage.test.tsx`(8 用例:挂载默认查询/搜索/翻页 BR8/错误重试/空态/BR15 pageNum 回显) | ||
| 36 | +- FE-01/FE-02 既有单测(authSlice / request / LoginPage* / AppLayout* / router / RequireAuth / RedirectIfAuthed / Home / Kpi / Nav 等)一并绿,无回归。 | ||
| 37 | + | ||
| 38 | +### stdout 摘录(≤30 行) | ||
| 39 | + | ||
| 40 | +``` | ||
| 41 | + ✓ tests/unit/UserListPage.test.tsx (8 tests) 1509ms | ||
| 42 | + ✓ UserListPage 集成 > initial load renders rows from listUsers (default query) (BR2) 393ms | ||
| 43 | + ✓ tests/unit/usrApi.userlist.test.ts (3 tests) 2ms | ||
| 44 | + ✓ tests/unit/usrApi.test.ts (2 tests) 2ms | ||
| 45 | + ✓ tests/unit/RequireAuth.test.tsx (3 tests) 71ms | ||
| 46 | + ✓ tests/unit/RedirectIfAuthed.test.tsx (3 tests) 57ms | ||
| 47 | + ✓ tests/unit/AppLayout.unauthorized.test.tsx (1 test) 81ms | ||
| 48 | + ✓ tests/unit/LoginPage.error.test.tsx (6 tests) 1752ms | ||
| 49 | + | ||
| 50 | + Test Files 32 passed (32) | ||
| 51 | + Tests 138 passed (138) | ||
| 52 | + Start at 17:48:22 | ||
| 53 | + Duration 4.43s (transform 532ms, setup 2.36s, collect 37.88s, tests 13.58s, environment 8.09s, prepare 1.44s) | ||
| 54 | +``` | ||
| 55 | + | ||
| 56 | +> 说明:日志中 `window.getComputedStyle(elt, pseudoElt) Not implemented`(来自 rc-table measureScrollbarSize)与 React Router v7 future flag / `localStorage --localstorage-file` 等为 jsdom/依赖侧的非致命 stderr 警告,不影响断言,全部用例仍判定通过(`exit_code=0`,`138 passed`)。 | ||
| 57 | + | ||
| 58 | +--- | ||
| 59 | + | ||
| 60 | +## 三、端到端测试(Playwright) | ||
| 61 | + | ||
| 62 | +- 命令:`cd frontend && npm run test:e2e` | ||
| 63 | +- exit_code:`0` | ||
| 64 | +- 结果:`Running 15 tests using 9 workers` → `15 passed (2.5s)` | ||
| 65 | +- failed_list:(空) | ||
| 66 | +- FE-03 `tests/e2e/userlist.spec.ts` 6 用例全过:进入用户列表渲染行 / 空结果显示「暂无匹配的用户」/ 按值搜索触发查询 / 翻页 pageNum=2 重取 / 行双击进用户单据 / 错误响应重试后恢复。 | ||
| 67 | +- FE-01 `login.spec.ts`(4 用例)+ FE-02 `shell.spec.ts`(5 用例)一并绿,无回归。 | ||
| 68 | + | ||
| 69 | +### stdout 摘录(≤30 行) | ||
| 70 | + | ||
| 71 | +``` | ||
| 72 | +Running 15 tests using 9 workers | ||
| 73 | + ✓ 7 [chromium] › tests/e2e/login.spec.ts:22:3 › loads /login and shows version options (501ms) | ||
| 74 | + ✓ 3 [chromium] › tests/e2e/login.spec.ts:32:3 › blocks submit with validation when empty (582ms) | ||
| 75 | + ✓ 6 [chromium] › tests/e2e/shell.spec.ts:95:3 › visiting / unauthenticated redirects to /login (625ms) | ||
| 76 | + ✓ 4 [chromium] › tests/e2e/shell.spec.ts:61:3 › open and close 全部导航 overlay (677ms) | ||
| 77 | + ✓ 9 [chromium] › tests/e2e/shell.spec.ts:53:3 › login then lands on home with topbar and KPI title (674ms) | ||
| 78 | + ✓ 1 [chromium] › tests/e2e/shell.spec.ts:73:3 › open 用户列表 tab from common ops then close back to home (764ms) | ||
| 79 | + ✓ 2 [chromium] › tests/e2e/login.spec.ts:50:3 › successful login navigates away from /login (1.0s) | ||
| 80 | + ✓ 10 [chromium] › tests/e2e/userlist.spec.ts:73:3 › enter user list renders rows (486ms) | ||
| 81 | + ✓ 8 [chromium] › tests/e2e/login.spec.ts:77:3 › failed login stays on /login with error (1.0s) | ||
| 82 | + ✓ 11 [chromium] › tests/e2e/userlist.spec.ts:88:3 › empty result shows 暂无匹配的用户 (471ms) | ||
| 83 | + ✓ 5 [chromium] › tests/e2e/shell.spec.ts:86:3 › logout returns to /login (1.1s) | ||
| 84 | + ✓ 12 [chromium] › tests/e2e/userlist.spec.ts:102:3 › search by value triggers query (523ms) | ||
| 85 | + ✓ 14 [chromium] › tests/e2e/userlist.spec.ts:146:3 › double click row navigates to user detail (492ms) | ||
| 86 | + ✓ 13 [chromium] › tests/e2e/userlist.spec.ts:124:3 › pagination next page refetches with pageNum=2 (528ms) | ||
| 87 | + ✓ 15 [chromium] › tests/e2e/userlist.spec.ts:161:3 › error response shows retry then recovers (478ms) | ||
| 88 | + | ||
| 89 | + 15 passed (2.5s) | ||
| 90 | +``` | ||
| 91 | + | ||
| 92 | +--- | ||
| 93 | + | ||
| 94 | +## 四、作用域校验 | ||
| 95 | + | ||
| 96 | +- 验证仅运行 `frontend/` 下的测试套件;未触碰 `backend/` / `sql/` / `scripts/`,作用域校验通过。 | ||
| 97 | +- 本报告落盘于 `docs/superpowers/reviews/`(与 review 报告同目录),round=0 → 文件名后缀 `-verify.md`,符合命名约定。 | ||
| 98 | + | ||
| 99 | +--- | ||
| 100 | + | ||
| 101 | +## 五、自主决策记录(decisions) | ||
| 102 | + | ||
| 103 | +| # | 问题 | 选择 | 依据 | 置信度 | | ||
| 104 | +|---|---|---|---|---| | ||
| 105 | +| V1 | 测试命令默认值取 `pnpm test:ci`/`pnpm e2e:ci` 还是 docs/04 锁定值 | 取 `docs/04-技术规范.md § 零` 锁定的 `npm run test:unit` / `npm run test:e2e`(package.json scripts 实测存在且对应 `vitest run` / `playwright test`) | flow 明确「命令从 docs/04 § 零取,缺失才用默认」;本仓 docs/04 § 零命令清单已锁定前端命令,且 `frontend/package.json` 中两脚本均存在;默认值仅在缺失时兜底,此处不缺失 | high | | ||
| 106 | +| V2 | 仅跑 plan 列出的 FE-03 新增测试文件,还是跑全量套件 | 跑全量 `test:unit` + `test:e2e` | plan T9 门禁要求「`npm run test:unit && npm run test:e2e` 全绿」且需校验 FE-01/FE-02 不回归(router.test/shell.spec);全量执行同时覆盖 FE-03 新增用例与回归面,过滤模式仅为子任务级验证手段 | high | |
docs/superpowers/reviews/2026-06-01-FE-03.md
0 → 100644
| 1 | +# FE-03 用户列表与查询 — AI 自审报告(round=1) | ||
| 2 | + | ||
| 3 | +> 阶段:前端(frontend)。审阅对象:`FE-03`(关联 `REQ-USR-003`,scope=usr)。 | ||
| 4 | +> 上游 spec:`docs/superpowers/specs/2026-06-01-FE-03.md`;上游 plan:`docs/superpowers/plans/2026-06-01-FE-03.md`。 | ||
| 5 | +> 关联原型:`prototype/erp.html` → `<section id="screen-userlist">`。 | ||
| 6 | +> Design Tokens SSoT:`src/styles/tokens.css`。API 契约:`docs/05-API接口契约.md § REQ-USR-003`。 | ||
| 7 | +> 审阅时间:2026-06-01。本报告由 fe-feature-review 渲染。 | ||
| 8 | + | ||
| 9 | +--- | ||
| 10 | + | ||
| 11 | +## 一、裁决 | ||
| 12 | + | ||
| 13 | +**approve**(无 must-fix)。 | ||
| 14 | + | ||
| 15 | +- 门禁回归(verify round=0):单测 138 passed / 0 failed;E2E 15 passed / 0 failed。 | ||
| 16 | +- reviewer 复核:`npm run lint` 干净;FE-03 新增 7 个单测文件本地复跑 50 passed / 0 failed(stderr 仅 jsdom `getComputedStyle` rc-table 非致命警告)。 | ||
| 17 | +- 作用域:全部改动落在 `frontend/`(含 docs/plans、docs/reviews),未触碰 `backend/` / `sql/` / `scripts/`,无越界。 | ||
| 18 | + | ||
| 19 | +--- | ||
| 20 | + | ||
| 21 | +## 二、通用四维 | ||
| 22 | + | ||
| 23 | +### 计划一致性(plan-alignment) | ||
| 24 | +- 组件树(页面容器 + UserToolbar / UserFilterBar / UserTable / UserPager 内置于 Table)与 spec § 2 / plan 文件边界一致。 | ||
| 25 | +- 列定义(序号 / 用户名 / 员工名 / 用户号 / 部门 / 用户类型 / 语言 / 作废 / 登录日期 / 制单人 / 制单日期)与 REQ 输出表 1 + 原型 thead 顺序一致(`columns.tsx`)。 | ||
| 26 | +- 业务规则 BR1–BR15 全部复刻(序号按当前页生成、作废只读 0/1→否/是、搜索回第 1 页、刷新保持当前页、清空重置默认、改页大小回第 1 页、行双击/新增导航、空态不报错、越界信任后端回显)。 | ||
| 27 | +- 自主决策 D1–D10 + D-PLAN-1/2 均落地(范围下拉占位不传参 D2、更多/齿轮占位 D3/D7、pageSize 默认 10 上限 100 D4、前端零依赖 CSV 导出 D5/D-PLAN-1、页面就近 hook D6、单选标记不参与查询 D8、中文键归一 D9/D-PLAN-2、工具栏深色局部装饰 D10)。 | ||
| 28 | + | ||
| 29 | +### 质量(quality) | ||
| 30 | +- 类型安全:`UserVO`/`PageResult<T>`/`UserListQuery` 契约清晰;`as unknown` 桥接仅用于拦截器已解包语义,与 FE-01/FE-02 既有约定一致。 | ||
| 31 | +- 错误处理:`useUserList.runFetch` 覆盖 42201(warning + 收敛分页重查)/ 40001(error + 保留条件不重查)/ 网络兜底(error + 错误占位重试);`ApiError` 分流文案与 spec § 4 错误码表逐条对齐。 | ||
| 32 | +- 健壮性:`mountedRef` 卸载守卫避免异步 resolve/reject 后 setState;`queryRef` 镜像避免闭包读旧值。 | ||
| 33 | +- 测试:状态机 6 态、各组件交互、CSV 导出、API 透传/归一均有单测 + E2E 固化。 | ||
| 34 | + | ||
| 35 | +### 架构(architecture) | ||
| 36 | +- 分层清晰:页面容器 / 子组件 / 列定义 / hook / api / 常量 / 导出工具各司其职。 | ||
| 37 | +- API 统一走 `frontend/src/api/request.ts` Axios 实例,方法集中于 `usrApi.ts listUsers`,页面不散用 axios(docs/04 § 2.3)。 | ||
| 38 | +- 列表查询态留在页面本地 hook,不进全局 store(docs/04 § 2.2 / D6)。 | ||
| 39 | + | ||
| 40 | +### 文档(docs) | ||
| 41 | +- 每文件首行带 `REQ-USR-003` 追溯注释 + BR/D 编号,符合 CLAUDE.md 注释约定。 | ||
| 42 | +- 无 TODO/TBD/人工填写占位残留。 | ||
| 43 | + | ||
| 44 | +--- | ||
| 45 | + | ||
| 46 | +## 三、前端 7 维 checklist | ||
| 47 | + | ||
| 48 | +| 维度 | 结论 | 说明 | | ||
| 49 | +|---|---|---| | ||
| 50 | +| 1 原型一致性 | PASS | toolbar(刷新/新增/导出Excel/spacer/齿轮)、filterbar(范围下拉/字段下拉/匹配下拉/查询值/▾/搜索/清空)、grid-table(11 列 + 单选首列)、pager(统计+上/下页+当前页+每页条数)与 `#screen-userlist` 结构逐块对应;主操作(搜索按钮)位置、列顺序、分页右对齐均一致。AntD 组件化为允许的实现差异。 | | ||
| 51 | +| 2 Design Tokens | PASS | 语义/状态色(primary/error/warning/success/text/secondary/border/table-header/table-row hover/selected/form-bg)全部走 `var(--color-*)`;硬编码 `#2c2f36`/`#ffffff` 仅用于工具栏深色装饰底,为 spec § 7 / D10 显式登记的非语义局部装饰,与已 approve 的 FE-02 § 8 D9(topbar `#1f1f23` 等)同源精度,tokens.css 无对应工具条品牌色 token,不构成 token 违规。 | | ||
| 52 | +| 3 无障碍 | PASS(best-effort) | 齿轮占位带 `aria-label="设置"`;筛选控件用 AntD `Select`/`Input`(无 spec 要求的强制 label,原型亦无);危险/不可逆操作本页不存在(只读查询)。对比度为主观项,无明显失败。 | | ||
| 53 | +| 4 响应式 | PASS(best-effort) | 表格 `scroll={{ x:'max-content' }}` 复刻原型横向滚动;filterbar `flex-wrap`;无明显横向溢出或 hover-only 关键操作。 | | ||
| 54 | +| 5 业务校验前端复刻 | PASS | 查询字段/匹配方式枚举受限于 `Select` 合法项(BR4);查询值空为全部(BR3,listUsers 省略空 `queryValue`);默认值预填(BR2);错误码文案与后端语义一致(spec § 4)。 | | ||
| 55 | +| 6 API 调用一致性 | PASS | `GET /usr/users`(baseURL=/api → `/api/usr/users`)与 docs/05 路径一致;query 字段 `queryField/matchType/queryValue/pageNum/pageSize` 与契约一致;`UserVO` 字段与契约一致(中文键 `员工名`/`部门` 在 api 层归一为 `employeeName`/`departmentName`,D9);统一客户端,无裸 fetch/axios。 | | ||
| 56 | +| 7 状态机覆盖 | PASS | loading(初始 `useState(true)` + 取数中)/ empty(AntD `Empty`「暂无匹配的用户」)/ error(errorBox + 点击重试)/ 正常(表格行)/ 提交中(exporting:导出按钮 loading+disabled)五态俱全。 | | ||
| 57 | + | ||
| 58 | +--- | ||
| 59 | + | ||
| 60 | +## 四、非阻塞建议(不计入 must-fix) | ||
| 61 | + | ||
| 62 | +- **导出上限**:`useUserList.exportExcel` 将 `fetchSize` 收敛至 `≤100`(`Math.min(Math.max(total, pageSize, 1), 100)`),当 `total>100` 时仅导出前 100 行。此为 spec D5(confidence=medium,「MVP 阶段前端导出 … 一次或分批取」)显式登记的取舍,未做分页循环属已记录的 MVP 简化,非 spec 违规。后续若后端补 `/export` 端点或需求要求全量导出,可改为按页循环取。建议在用户量级增长后跟进,不阻塞本轮。 | ||
| 63 | +- 工具栏深色装饰底(`#2c2f36`)目前在 `UserList.module.css` 与 FE-02 `AppLayout.module.css`(`#1f1f23`)各自硬编码。若后续多处工具条深色趋于一致,可考虑提一个非语义装饰变量集中管理;当前各页局部化处理与已批准的 FE-02 决策一致,不强制。 | ||
| 64 | + | ||
| 65 | +--- | ||
| 66 | + | ||
| 67 | +## 五、结论 | ||
| 68 | + | ||
| 69 | +实现忠实复刻原型布局与交互语义,真实对接 `GET /api/usr/users`,状态机完整、错误码分流到位、tokens 使用规范、API 契约一致、作用域无越界,门禁与 lint 全绿。无客观可验证的 must-fix 缺陷。 | ||
| 70 | + | ||
| 71 | +**verdict = approve** |
docs/superpowers/reviews/2026-06-01-FE-04-verify-r1.md
0 → 100644
| 1 | +# FE-04 用户信息单据 — 证据验证报告(round=1,第 1 轮 fix 后复验) | ||
| 2 | + | ||
| 3 | +> 阶段:前端(frontend)。验证对象:`FE-04`(关联 `REQ-USR-001` 增加用户 + `REQ-USR-002` 修改用户,scope=usr)。 | ||
| 4 | +> 上游 spec:`docs/superpowers/specs/2026-06-01-FE-04.md`(产物文件名日期前缀 `2026-06-01` 取自 spec 文件名首段)。 | ||
| 5 | +> 上游 TDD 摘要:第 1 轮 fix 后复验,上轮 review(`2026-06-01-FE-04.md`)裁决 request-changes,must-fix 1 项(B1:edit 态预填把用户主键当「用户号」查询,正常导航流必然取不到记录 → 40401)。 | ||
| 6 | +> 验证时间:2026-06-02。本报告由 fe-feature-verify 渲染;主会话不自由编写测试,仅按测试命令的结构化结果落盘。 | ||
| 7 | +> 测试以分离进程执行(非主会话交互式跑),仅运行 `frontend/` 下套件。 | ||
| 8 | + | ||
| 9 | +--- | ||
| 10 | + | ||
| 11 | +## 一、验证结论 | ||
| 12 | + | ||
| 13 | +**全部通过(PASS)**。单测与 E2E 两个门禁目标 `exit_code` 均为 0,`failed=0`,可进入 review。 | ||
| 14 | + | ||
| 15 | +| 目标 | 命令 | exit_code | passed | failed | 结论 | | ||
| 16 | +|---|---|---|---|---|---| | ||
| 17 | +| 单元测试(jsdom / Vitest) | `npx vitest run <FE-04 8 文件>` | 0 | 55(8 文件) | 0 | PASS | | ||
| 18 | +| 端到端(Playwright) | `npx playwright test tests/e2e/userdetail.spec.ts` | 0 | 5 | 0 | PASS | | ||
| 19 | + | ||
| 20 | +> 命令来源:`docs/04-技术规范.md § 零 命令清单`(前端 unit=`npm run test:unit`=`vitest run`、e2e=`npm run test:e2e`=`playwright test`;`config-vars.yaml` 未覆盖测试命令,故按 docs/04 锁定值,未触发 flow 默认兜底 `pnpm test:ci`/`pnpm e2e:ci`)。本验证按 flow 取 FE-04 计划中 `测试先行类型=jsdom` 的 8 个单测文件拼 vitest 过滤模式、取 `测试先行类型=e2e` 的 `userdetail.spec.ts` 拼 Playwright 过滤模式,scope 收敛到 FE-04 新增/改动的测试。 | ||
| 21 | +> 上轮 must-fix(B1)针对 edit 预填数据流,本轮 fix 改动落在 `frontend/src/pages/usr/UserDetail/` 与对应单测(`useUserDetail.test.tsx`、`UserDetailPage.test.tsx`),复验后涉及 edit 预填的用例(单测 `useUserDetail`/`UserDetailPage` 的 edit 分支、E2E `edit user prefill then save`)全部通过。 | ||
| 22 | + | ||
| 23 | +--- | ||
| 24 | + | ||
| 25 | +## 二、单元测试(jsdom / Vitest) | ||
| 26 | + | ||
| 27 | +- 命令:`cd frontend && npx vitest run tests/unit/usrApi.userdetail.test.ts tests/unit/userDetailMappers.test.ts tests/unit/useUserDetail.test.tsx tests/unit/UserBasicForm.test.tsx tests/unit/PermissionGroupList.test.tsx tests/unit/PermissionTabs.test.tsx tests/unit/UserDetailToolbar.test.tsx tests/unit/UserDetailPage.test.tsx` | ||
| 28 | +- exit_code:`0` | ||
| 29 | +- 结果:`Test Files 8 passed (8)` / `Tests 55 passed (55)` | ||
| 30 | +- failed_list:(空) | ||
| 31 | +- FE-04 的 8 个单测文件逐个通过: | ||
| 32 | + - `tests/unit/usrApi.userdetail.test.ts`(6 用例:createUser/updateUser/getUserDetail/listEmployees/listPermissions 端点透传与 Result 拆包) | ||
| 33 | + - `tests/unit/userDetailMappers.test.ts`(4 用例:EmployeeOption/PermissionItem/UserVO→表单值映射) | ||
| 34 | + - `tests/unit/useUserDetail.test.tsx`(10 用例:状态机 initialLoading/editing/submitting/submitError/submitSuccess/loadError + create/edit 分支,含 edit 预填) | ||
| 35 | + - `tests/unit/UserBasicForm.test.tsx`(9 用例:BR1-BR8 字段渲染/只读/默认值/必填校验/员工联动) | ||
| 36 | + - `tests/unit/PermissionGroupList.test.tsx`(8 用例:BR10/BR11 权限项渲染/全选 indeterminate/勾选集合/edit 回勾) | ||
| 37 | + - `tests/unit/PermissionTabs.test.tsx`(3 用例:权限组 active + 5 个占位页签 D9) | ||
| 38 | + - `tests/unit/UserDetailToolbar.test.tsx`(5 用例:保存/取消/新增 + 占位按钮 D8) | ||
| 39 | + - `tests/unit/UserDetailPage.test.tsx`(10 用例:create 默认值/edit 预填/提交成功回流/用户名冲突就近高亮/dirty 取消二次确认/loadError 重试) | ||
| 40 | + | ||
| 41 | +### stdout 摘录(≤30 行) | ||
| 42 | + | ||
| 43 | +``` | ||
| 44 | + ✓ tests/unit/userDetailMappers.test.ts (4 tests) 3ms | ||
| 45 | + ✓ tests/unit/usrApi.userdetail.test.ts (6 tests) 3ms | ||
| 46 | + ✓ tests/unit/PermissionTabs.test.tsx (3 tests) 137ms | ||
| 47 | + ✓ tests/unit/PermissionGroupList.test.tsx (8 tests) 154ms | ||
| 48 | + ✓ tests/unit/UserDetailToolbar.test.tsx (5 tests) 163ms | ||
| 49 | + ✓ tests/unit/useUserDetail.test.tsx (10 tests) 603ms | ||
| 50 | + ✓ tests/unit/UserBasicForm.test.tsx (9 tests) 751ms | ||
| 51 | + ✓ tests/unit/UserDetailPage.test.tsx (10 tests) 1651ms | ||
| 52 | + | ||
| 53 | + Test Files 8 passed (8) | ||
| 54 | + Tests 55 passed (55) | ||
| 55 | + Duration 3.26s | ||
| 56 | +``` | ||
| 57 | + | ||
| 58 | +> 说明:日志中 `window.getComputedStyle(elt, pseudoElt) Not implemented`(来自 rc-util measureScrollbarSize / AntD Modal 滚动锁)、`act(...)` 包裹建议(rc-field-form 异步校验态)、React Router v7 future flag 等均为 jsdom/依赖侧非致命 stderr 警告,不影响断言,全部用例仍判定通过(`exit_code=0`,`55 passed`)。 | ||
| 59 | + | ||
| 60 | +--- | ||
| 61 | + | ||
| 62 | +## 三、端到端测试(Playwright) | ||
| 63 | + | ||
| 64 | +- 命令:`cd frontend && npx playwright test tests/e2e/userdetail.spec.ts` | ||
| 65 | +- exit_code:`0` | ||
| 66 | +- 结果:`5 passed (5.4s)` | ||
| 67 | +- failed_list:(空) | ||
| 68 | +- FE-04 `tests/e2e/userdetail.spec.ts` 5 用例全过: | ||
| 69 | + - `create user and return to list`(新增提交成功回流列表 BR12/BR16) | ||
| 70 | + - `edit user prefill then save`(edit 预填原值 + 保存 BR17/BR11;对应上轮 B1 数据流修复点) | ||
| 71 | + - `username conflict shows inline error`(40901 用户名冲突就近报错) | ||
| 72 | + - `placeholder tabs/buttons are inert`(占位页签/按钮惰性 D8/D9) | ||
| 73 | + - `load error shows retry`(loadError 取数失败重试入口) | ||
| 74 | + | ||
| 75 | +### stdout 摘录(≤30 行) | ||
| 76 | + | ||
| 77 | +``` | ||
| 78 | +Running 5 tests using 5 workers | ||
| 79 | + ✓ 1 [chromium] › tests/e2e/userdetail.spec.ts:170:3 › 用户单据关键旅程 › placeholder tabs/buttons are inert (904ms) | ||
| 80 | + ✓ 5 [chromium] › tests/e2e/userdetail.spec.ts:126:3 › 用户单据关键旅程 › username conflict shows inline error (1.2s) | ||
| 81 | + ✓ 3 [chromium] › tests/e2e/userdetail.spec.ts:97:3 › 用户单据关键旅程 › edit user prefill then save (1.3s) | ||
| 82 | + ✓ 4 [chromium] › tests/e2e/userdetail.spec.ts:69:3 › 用户单据关键旅程 › create user and return to list (1.3s) | ||
| 83 | + ✓ 2 [chromium] › tests/e2e/userdetail.spec.ts:146:3 › 用户单据关键旅程 › load error shows retry (4.3s) | ||
| 84 | + | ||
| 85 | + 5 passed (5.4s) | ||
| 86 | +``` | ||
| 87 | + | ||
| 88 | +> 说明:WebServer 日志中 `[vite] http proxy error ... ECONNREFUSED /api/usr/users` 为预期噪声——E2E 在路由层 mock API 响应,开发后端(`config-vars.yaml backend.http_port=5172`)未启动,Vite proxy 透传请求被拒;用例不依赖真实后端,5 个用例全部判定通过(`exit_code=0`,`5 passed`),与 FE-01/FE-02/FE-03 验证及本 FE round=0 验证同型非致命基建告警。 | ||
| 89 | + | ||
| 90 | +--- | ||
| 91 | + | ||
| 92 | +## 四、作用域校验 | ||
| 93 | + | ||
| 94 | +- 验证仅运行 `frontend/` 下的测试套件(`frontend/tests/unit/*`(FE-04 8 文件)+ `frontend/tests/e2e/userdetail.spec.ts`);未触碰 `backend/` / `sql/` / `scripts/`,作用域校验通过。 | ||
| 95 | +- 本报告落盘于 `docs/superpowers/reviews/`(与 review 报告同目录),round=1 → 文件名后缀 `-verify-r1.md`,与 round=0 的 `-verify.md` 各自独立、不覆盖前轮,符合命名约定。 | ||
| 96 | + | ||
| 97 | +--- | ||
| 98 | + | ||
| 99 | +## 五、自主决策记录(decisions) | ||
| 100 | + | ||
| 101 | +| # | 问题 | 选择 | 依据 | 置信度 | | ||
| 102 | +|---|---|---|---|---| | ||
| 103 | +| V1 | 测试命令默认值取 `pnpm test:ci`/`pnpm e2e:ci` 还是 docs/04 锁定值 | 取 `docs/04-技术规范.md § 零` 锁定的前端命令(unit=`npm run test:unit`=`vitest run`、e2e=`npm run test:e2e`=`playwright test`,`frontend/package.json` scripts 实测存在;`config-vars.yaml` 未覆盖测试命令) | flow 明确「命令从 docs/04 § 零取,缺失才用默认」;本仓 docs/04 § 零已锁定前端命令,默认值仅缺失兜底,此处不缺失 | high | | ||
| 104 | +| V2 | 单测/E2E 跑全量套件还是 FE-04 过滤模式 | 按 flow 取 FE-04 计划中 `测试先行类型=jsdom` 的 8 个单测文件拼 vitest 过滤、取 `=e2e` 的 `userdetail.spec.ts` 拼 Playwright 过滤,scope 收敛到本 FE | flow 明确「从 plan 取 `测试先行类型=jsdom` 的 test_file 拼 vitest 过滤、`=e2e` 的拼 Playwright spec 过滤」 | high | | ||
| 105 | +| V3 | E2E 运行中 `ECONNREFUSED /api/usr/users` proxy 报错是否判失败 | 判通过(非致命基建噪声) | E2E 在路由层 mock 响应,不依赖真实后端;Playwright 汇总 `5 passed` 且进程 `exit_code=0`,proxy 噪声来自 Vite dev server 透传未启动的后端端口,与用例断言无关,与历次验证同型处理 | high | |
docs/superpowers/reviews/2026-06-01-FE-04-verify.md
0 → 100644
| 1 | +# FE-04 用户信息单据 — 证据验证报告(round=0) | ||
| 2 | + | ||
| 3 | +> 阶段:前端(frontend)。验证对象:`FE-04`(关联 `REQ-USR-001` 增加用户 + `REQ-USR-002` 修改用户,scope=usr)。 | ||
| 4 | +> 上游 spec:`docs/superpowers/specs/2026-06-01-FE-04.md`。 | ||
| 5 | +> 验证时间:2026-06-01。本报告由 fe-feature-verify 渲染;主会话不自由编写测试,仅按测试命令的结构化结果落盘。 | ||
| 6 | +> 测试以后台分离进程执行(非主会话交互式跑),仅运行 `frontend/` 下套件。 | ||
| 7 | + | ||
| 8 | +--- | ||
| 9 | + | ||
| 10 | +## 一、验证结论 | ||
| 11 | + | ||
| 12 | +**全部通过(PASS)**。单测与 E2E 两个门禁目标 `exit_code` 均为 0,`failed=0`,可进入 review。 | ||
| 13 | + | ||
| 14 | +| 目标 | 命令 | exit_code | passed | failed | 结论 | | ||
| 15 | +|---|---|---|---|---|---| | ||
| 16 | +| 单元测试(jsdom / Vitest) | `npx vitest run <FE-04 8 文件>` | 0 | 55(8 文件) | 0 | ✅ PASS | | ||
| 17 | +| 端到端(Playwright) | `npx playwright test tests/e2e/userdetail.spec.ts` | 0 | 5 | 0 | ✅ PASS | | ||
| 18 | + | ||
| 19 | +> 命令来源:`docs/04-技术规范.md § 零 命令清单`(前端 unit=`npm run test:unit`=`vitest run`、e2e=`npm run test:e2e`=`playwright test`)。本验证按 flow 取 FE-04 的 jsdom 单测文件拼 vitest 过滤模式、取 e2e spec 拼 Playwright 过滤模式,scope 收敛到 FE-04 新增/改动的测试。全量门禁(193 passed / 40 文件单测、20 passed E2E)已在上游 T10 回归通过(提交 32f94ad)。 | ||
| 20 | + | ||
| 21 | +--- | ||
| 22 | + | ||
| 23 | +## 二、单元测试(jsdom / Vitest) | ||
| 24 | + | ||
| 25 | +- 命令:`cd frontend && npx vitest run tests/unit/usrApi.userdetail.test.ts tests/unit/userDetailMappers.test.ts tests/unit/useUserDetail.test.tsx tests/unit/UserBasicForm.test.tsx tests/unit/PermissionGroupList.test.tsx tests/unit/PermissionTabs.test.tsx tests/unit/UserDetailToolbar.test.tsx tests/unit/UserDetailPage.test.tsx` | ||
| 26 | +- exit_code:`0` | ||
| 27 | +- 结果:`Test Files 8 passed (8)` / `Tests 55 passed (55)` | ||
| 28 | +- failed_list:(空) | ||
| 29 | +- FE-04 的 8 个单测文件逐个通过: | ||
| 30 | + - `tests/unit/usrApi.userdetail.test.ts`(6 用例:createUser/updateUser/getUserDetail/listEmployees/listPermissions 端点透传与 Result 拆包) | ||
| 31 | + - `tests/unit/userDetailMappers.test.ts`(4 用例:EmployeeOption/PermissionItem/UserVO→表单值映射) | ||
| 32 | + - `tests/unit/useUserDetail.test.tsx`(10 用例:状态机 initialLoading/editing/submitting/submitError/submitSuccess/loadError + create/edit 分支) | ||
| 33 | + - `tests/unit/UserBasicForm.test.tsx`(9 用例:BR1-BR8 字段渲染/只读/默认值/必填校验/员工联动) | ||
| 34 | + - `tests/unit/PermissionGroupList.test.tsx`(8 用例:BR10/BR11 权限项渲染/全选 indeterminate/勾选集合/edit 回勾) | ||
| 35 | + - `tests/unit/PermissionTabs.test.tsx`(3 用例:权限组 active + 5 个占位页签 D9) | ||
| 36 | + - `tests/unit/UserDetailToolbar.test.tsx`(5 用例:保存/取消/新增 + 占位按钮 D8) | ||
| 37 | + - `tests/unit/UserDetailPage.test.tsx`(10 用例:create 默认值/edit 预填/提交成功回流/用户名冲突就近高亮/dirty 取消二次确认/loadError 重试) | ||
| 38 | + | ||
| 39 | +### stdout 摘录(≤30 行) | ||
| 40 | + | ||
| 41 | +``` | ||
| 42 | + ✓ tests/unit/userDetailMappers.test.ts (4 tests) 2ms | ||
| 43 | + ✓ tests/unit/usrApi.userdetail.test.ts (6 tests) 3ms | ||
| 44 | + ✓ tests/unit/PermissionTabs.test.tsx (3 tests) 127ms | ||
| 45 | + ✓ tests/unit/PermissionGroupList.test.tsx (8 tests) 145ms | ||
| 46 | + ✓ tests/unit/UserDetailToolbar.test.tsx (5 tests) 191ms | ||
| 47 | + ✓ tests/unit/useUserDetail.test.tsx (10 tests) 599ms | ||
| 48 | + ✓ tests/unit/UserBasicForm.test.tsx (9 tests) 761ms | ||
| 49 | + ✓ tests/unit/UserDetailPage.test.tsx (10 tests) 1406ms | ||
| 50 | + | ||
| 51 | + Test Files 8 passed (8) | ||
| 52 | + Tests 55 passed (55) | ||
| 53 | +``` | ||
| 54 | + | ||
| 55 | +> 说明:日志中 `window.getComputedStyle(elt, pseudoElt) Not implemented`(来自 rc-util measureScrollbarSize / AntD Modal 滚动锁)、`act(...)` 包裹建议(rc-field-form 异步校验态)、React Router v7 future flag 等均为 jsdom/依赖侧非致命 stderr 警告,不影响断言,全部用例仍判定通过(`exit_code=0`,`55 passed`)。 | ||
| 56 | + | ||
| 57 | +--- | ||
| 58 | + | ||
| 59 | +## 三、端到端测试(Playwright) | ||
| 60 | + | ||
| 61 | +- 命令:`cd frontend && npx playwright test tests/e2e/userdetail.spec.ts` | ||
| 62 | +- exit_code:`0` | ||
| 63 | +- 结果:`5 passed (5.2s)` | ||
| 64 | +- failed_list:(空) | ||
| 65 | +- FE-04 `tests/e2e/userdetail.spec.ts` 5 用例全过: | ||
| 66 | + - `create user and return to list`(新增提交成功回流列表 BR12/BR16) | ||
| 67 | + - `edit user prefill then save`(edit 预填原值 + 保存 BR17/BR11) | ||
| 68 | + - `username conflict shows inline error`(40901 用户名冲突就近报错) | ||
| 69 | + - `placeholder tabs/buttons are inert`(占位页签/按钮惰性 D8/D9) | ||
| 70 | + - `load error shows retry`(loadError 取数失败重试入口) | ||
| 71 | + | ||
| 72 | +### stdout 摘录(≤30 行) | ||
| 73 | + | ||
| 74 | +``` | ||
| 75 | + ✓ 2 [chromium] › tests/e2e/userdetail.spec.ts:170:3 › 用户单据关键旅程 › placeholder tabs/buttons are inert (729ms) | ||
| 76 | + ✓ 3 [chromium] › tests/e2e/userdetail.spec.ts:126:3 › 用户单据关键旅程 › username conflict shows inline error (1.1s) | ||
| 77 | + ✓ 4 [chromium] › tests/e2e/userdetail.spec.ts:97:3 › 用户单据关键旅程 › edit user prefill then save (1.1s) | ||
| 78 | + ✓ 5 [chromium] › tests/e2e/userdetail.spec.ts:69:3 › 用户单据关键旅程 › create user and return to list (1.1s) | ||
| 79 | + ✓ 1 [chromium] › tests/e2e/userdetail.spec.ts:146:3 › 用户单据关键旅程 › load error shows retry (4.1s) | ||
| 80 | + | ||
| 81 | + 5 passed (5.2s) | ||
| 82 | +``` | ||
| 83 | + | ||
| 84 | +> 说明:WebServer 日志中 `[vite] http proxy error ... ECONNREFUSED /api/usr/users` 为预期噪声——E2E 在路由层 mock API 响应,开发后端(`config-vars.yaml backend.http_port=5172`)未启动,Vite proxy 透传请求被拒;用例不依赖真实后端,5 个用例全部判定通过(`exit_code=0`,`5 passed`),与 FE-03 验证同型非致命基建告警。 | ||
| 85 | + | ||
| 86 | +--- | ||
| 87 | + | ||
| 88 | +## 四、作用域校验 | ||
| 89 | + | ||
| 90 | +- 验证仅运行 `frontend/` 下的测试套件(`frontend/tests/unit/*` + `frontend/tests/e2e/userdetail.spec.ts`);未触碰 `backend/` / `sql/` / `scripts/`,作用域校验通过。 | ||
| 91 | +- 本报告落盘于 `docs/superpowers/reviews/`(与 review 报告同目录),round=0 → 文件名后缀 `-verify.md`,符合命名约定。 | ||
| 92 | + | ||
| 93 | +--- | ||
| 94 | + | ||
| 95 | +## 五、自主决策记录(decisions) | ||
| 96 | + | ||
| 97 | +| # | 问题 | 选择 | 依据 | 置信度 | | ||
| 98 | +|---|---|---|---|---| | ||
| 99 | +| V1 | 测试命令默认值取 `pnpm test:ci`/`pnpm e2e:ci` 还是 docs/04 锁定值 | 取 `docs/04-技术规范.md § 零` 锁定的前端命令(unit=`npm run test:unit`=`vitest run`、e2e=`npm run test:e2e`=`playwright test`,`frontend/package.json` scripts 实测存在) | flow 明确「命令从 docs/04 § 零取,缺失才用默认」;本仓 docs/04 § 零已锁定前端命令,默认值仅缺失兜底,此处不缺失 | high | | ||
| 100 | +| V2 | 单测/ E2E 跑全量套件还是 FE-04 过滤模式 | 按 flow 取 FE-04 的 jsdom 单测 8 文件拼 vitest 过滤、取 `userdetail.spec.ts` 拼 Playwright 过滤,scope 收敛到本 FE | flow 明确「从 plan 取 `测试先行类型=jsdom` 的 test_file 拼 vitest 过滤、`=e2e` 的拼 Playwright spec 过滤」;全量门禁(193/40 单测、20 E2E)已由上游 T10 回归通过(32f94ad),本轮做 FE-04 范围确认 | high | | ||
| 101 | +| V3 | E2E 运行中 `ECONNREFUSED /api/usr/users` proxy 报错是否判失败 | 判通过(非致命基建噪声) | E2E 在路由层 mock 响应,不依赖真实后端;Playwright 汇总 `5 passed` 且进程 `exit_code=0`,proxy 噪声来自 Vite dev server 透传未启动的后端端口,与用例断言无关,与 FE-03 验证同型处理 | high | |
docs/superpowers/reviews/2026-06-01-FE-04.md
0 → 100644
| 1 | +# FE-04 用户信息单据 — AI 代码评审报告(第 2 轮) | ||
| 2 | + | ||
| 3 | +- 规格:`docs/superpowers/specs/2026-06-01-FE-04.md` | ||
| 4 | +- 阶段:前端(frontend) | ||
| 5 | +- 关联 REQ:REQ-USR-001(增加用户)/ REQ-USR-002(修改用户) | ||
| 6 | +- 关联原型:`prototype/erp.html` `<section id="screen-userdetail">` | ||
| 7 | +- 本轮 fix commit:`96e88d3`(编辑预填走 navigate state + 补 loadError 返回列表入口) | ||
| 8 | +- 裁决:**approve** | ||
| 9 | + | ||
| 10 | +--- | ||
| 11 | + | ||
| 12 | +## 一、作用域核查(通过) | ||
| 13 | + | ||
| 14 | +本轮 diff 实现文件全部落在 `frontend/` 下(`src/pages/usr/UserDetail/`、`src/pages/usr/UserList/index.tsx`、`tests/unit/`),未触碰 `backend/` / `sql/` / `scripts/`,符合前端阶段路径作用域硬约束。工作树干净(fix 已 commit)。 | ||
| 15 | + | ||
| 16 | +## 二、上轮 must-fix 复验(核心维度) | ||
| 17 | + | ||
| 18 | +### B1(上轮 blocker)— 已修复 ✔ | ||
| 19 | + | ||
| 20 | +上轮判定:edit 态预填把路由主键 `:id` 当「用户号」去 `GET /api/usr/users?queryField=用户号` 精确匹配,真实后端必然返回空 → 误报 40401,无法满足 REQ-USR-002 编辑预填(BR17)。 | ||
| 21 | + | ||
| 22 | +本轮 fix(commit `96e88d3`)按上轮修复方向 #1 落地,复验确认到位: | ||
| 23 | + | ||
| 24 | +- `frontend/src/pages/usr/UserList/index.tsx:36` 双击行改为 `navigate('/usr/users/' + row.id, { state: { user: row } })`,把列表行 `UserVO` 经 navigate state 透传。 | ||
| 25 | +- `frontend/src/pages/usr/UserDetail/index.tsx:43` 从 `location.state.user` 取 `presetUser` 传入 hook。 | ||
| 26 | +- `frontend/src/pages/usr/UserDetail/useUserDetail.ts:116-131` edit 分支改为:有 `presetUser` → `initFromVo` 回填;缺 `presetUser`(直链访问 / 刷新丢 state)→ 进入 `loadError`(`loadFailed=true` + `MSG_LOAD_DETAIL_FAIL`),**不再**调 `getUserDetail`、不再把主键塞进「用户号」查询。原 `getUserDetail` import 与 `notFound` 状态已彻底移除。 | ||
| 27 | +- `frontend/src/pages/usr/UserDetail/index.tsx:108-128` loadError 整页态在 edit 模式额外提供「返回列表」入口(spec § 4「edit 详情失败给整页重试或返回列表」),替代原误导性的 40401「该用户不存在」页。 | ||
| 28 | +- 残留核查(grep):`src/` 内已无 `getUserDetail` 调用方(仅 `usrApi.ts` 保留导出,见 S1)、无 `notFound` 状态、无「按主键查用户号」路径;`MSG_ERR_USER_NOT_FOUND` 仅在 **submit** 路径(`useUserDetail.ts:225`,PUT 返回 40401)保留使用,符合 spec § 4「edit submit 可命中 40401」,非误用。 | ||
| 29 | + | ||
| 30 | +测试同步更新且通过: | ||
| 31 | +- `tests/unit/useUserDetail.test.tsx`:新增「edit 缺 presetUser → loadFailed 且不调 getUserDetail」、保留「edit 带 presetUser 跳过 getUserDetail」。 | ||
| 32 | +- `tests/unit/UserDetailPage.test.tsx`:edit 预填用例改为经 navigate state 注入 `presetUser` 并断言 `mockedDetail` 未被调用;新增「edit 缺 state → loadError 提供 点击重试 + 返回列表」。 | ||
| 33 | +- `tests/e2e/userdetail.spec.ts:97` `edit user prefill then save`:经列表双击(携带 state)预填→改语言→PUT 保存→回流列表,通过。 | ||
| 34 | + | ||
| 35 | +> 结论:B1 已被真实修复(非掩盖),数据流自洽,且测试改造正确反映了「主键无 by-id 读端点、预填依赖列表行 state」这一事实约束。 | ||
| 36 | + | ||
| 37 | +## 三、本轮门禁证据(复跑确认) | ||
| 38 | + | ||
| 39 | +- `npm run lint`:exit 0(无 error/warning)。 | ||
| 40 | +- `npx tsc --noEmit`:exit 0(含 `renderShell.tsx` 新增 `ShellInitialEntry` 本地类型,类型自洽)。 | ||
| 41 | +- `npm run test:unit`:40 文件 / 193 用例全通过(FE-04 相关 UserDetailPage 10、useUserDetail、UserDetailToolbar 等全绿)。 | ||
| 42 | +- `npm run test:e2e -- userdetail.spec.ts`:5 passed / 0 failed。ECONNREFUSED 为 route 层 mock 下的预期 vite proxy 噪声,不影响判定。 | ||
| 43 | + | ||
| 44 | +## 四、对照确认(七维 + 通用维,沿用上轮通过项,本轮 diff 未改动者不再赘述) | ||
| 45 | + | ||
| 46 | +- 原型一致性:工具栏 / 3 列 form-grid / tabs-row(权限组 active + 5 占位页签 disabled)/ perm-list 结构与 `#screen-userdetail` 一致,主操作「保存」置工具栏,无结构性偏移;本轮 diff 未触碰布局。 | ||
| 47 | +- Design Tokens:语义色仍走 `var(--color-*)`;本轮 diff 未引入新色值。 | ||
| 48 | +- a11y(客观项):表单控件经 AntD `Form.Item label` 关联;「取消」脏表单二次确认(`Modal.confirm`)保留;loadError「返回列表」为标准 `Button`,键盘可达。 | ||
| 49 | +- 业务校验前端复刻:BR3/BR4/BR6/BR7/BR8/BR5/BR9/BR11 与错误码 40001/40901/40401/40301 文案及就近高亮均保持。 | ||
| 50 | +- API 调用一致性:create/update/listEmployees/listPermissions 经统一 `request.ts` 实例,无裸 fetch/axios;请求体字段对齐 docs/05。本轮去掉了 edit 预填对列表端点的误用,API 调用面更收敛。 | ||
| 51 | +- 状态机覆盖:loading / empty / error(loadError 重试 + edit 返回列表)/ 正常 / 提交中 五态齐备;edit 缺 state 归入 loadError,语义比上轮 40401 更准确。 | ||
| 52 | + | ||
| 53 | +## 五、建议项(非 must-fix,可后续消化,不影响本轮 approve) | ||
| 54 | + | ||
| 55 | +- S1(沿上轮 S1,已部分自然消解):`frontend/src/api/usrApi.ts:107` `getUserDetail` 现已无生产调用方(仅测试 mock 中作为占位导出)。可在后续清理为「删除该函数」或「待后端补单用户详情/按主键定位端点后复用」。当前保留不构成缺陷(lint/tsc 均不报未用导出)。 | ||
| 56 | +- S2:`userVoToFormValues`(`constants.ts:146`)因 `UserVO`(FE-03 列表 VO)不含 `iEmployeeId` / `iCanModifyBill` / 已授权权限 id,edit 预填时这三项被置默认(`iEmployeeId=null` / `iCanModifyBill=0` / `checkedPermissionIds=[]`)。这是 spec D4 已登记的数据模型限制(列表 VO 为唯一可用源),非本轮回归;待后端补单用户详情读端点后可完整回勾。已属 spec 决策范畴,不作 must-fix。 | ||
| 57 | +- S3(沿上轮 S3):`useUserDetail.ts:143` catch 文案分支读 `permissions.length` 为闭包旧值,仅影响 loadError 文案精度,不影响功能。 | ||
| 58 | + | ||
| 59 | +## 六、裁决 | ||
| 60 | + | ||
| 61 | +上轮唯一 blocker(B1)已在本轮 diff 中真实修复,数据流自洽、测试改造正确、门禁四项(lint / tsc / unit / e2e)全绿,未引入新缺陷、未回归既有通过项。其余为可选建议项,均不构成 must-fix。 | ||
| 62 | + | ||
| 63 | +**verdict: approve** |
docs/superpowers/specs/2026-06-01-FE-01.md
0 → 100644
| 1 | +# FE-01 登录页 — 实现规格(前端) | ||
| 2 | + | ||
| 3 | +> 阶段:前端(frontend)。作用域限定 `frontend/` 下的页面 / 组件 / 路由 / store / api / 样式。 | ||
| 4 | +> SSoT 引用:需求卡片 `docs/01-需求清单/USR-用户管理/REQ-USR-004.md`;原型 `prototype/erp.html`(`#screen-login` 区域,布局/交互权威);API 契约 `docs/05-API接口契约.md` § REQ-USR-004;技术规范 `docs/04-技术规范.md` § 零 / § 二;Design Tokens `src/styles/tokens.css`。 | ||
| 5 | +> 本规格只消费已锁定事实。后端身份认证、BCrypt 比对、JWT 签发、限流等业务逻辑全部在后端(见 REQ-USR-004 后端规格),前端只负责采集输入、提交、依据响应/错误码渲染状态与文案。 | ||
| 6 | + | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +## 1. 关联 REQ + 关联原型 | ||
| 10 | + | ||
| 11 | +| 维度 | 内容 | | ||
| 12 | +|---|---| | ||
| 13 | +| 业务功能 | FE-01 登录页(用户名/密码/版本下拉登录) | | ||
| 14 | +| 关联 REQ | REQ-USR-004 登录用户(主);其「版本」下拉数据依赖后端 `GET /api/usr/companies`(REQ-USR-004 后端补齐的配套只读端点) | | ||
| 15 | +| 关联原型 | `prototype/erp.html` → `<section id="screen-login">`(含 `.login-wrap` / `.login-head` / `.login-hero` / `.login-card` / `.login-foot`) | | ||
| 16 | +| 路由 | `/login`(React Router v6)。登录成功后跳转主页落地路由(属 FE-02 范畴,本页只负责导航跳转动作,目标路径默认 `/`,见 § 7 决策 D3) | | ||
| 17 | +| 落地组件目录 | `frontend/src/pages/usr/Login/`(页面);登录态写入 `frontend/src/store/slices/authSlice`;接口走 `frontend/src/api/usrApi.ts` + `frontend/src/api/request.ts` | | ||
| 18 | + | ||
| 19 | +> 原型 `#screen-login` 用纯静态 HTML + 内联 demo 脚本(`goTo('login')` 默认进登录页、`.submit[data-go=main]` 点击直接切主页、`#ver-drop` 点击切换 `.open` 展开版本项)模拟交互。本规格按 React + AntD 5 复刻其**布局与交互语义**,但表单校验、提交、下拉取数、错误反馈改为真实对接后端。 | ||
| 20 | + | ||
| 21 | +--- | ||
| 22 | + | ||
| 23 | +## 2. 组件树(按区域分块,推导自 prototype DOM) | ||
| 24 | + | ||
| 25 | +页面根 `LoginPage`(路由 `/login` 挂载),结构对应原型 `.login-wrap`(占满视口、纵向 flex:头部 / 主视觉 / 页脚): | ||
| 26 | + | ||
| 27 | +``` | ||
| 28 | +LoginPage (容器,对应 .login-wrap:position 占满、flex column、背景 --color-bg-base) | ||
| 29 | +├── LoginHeader (对应 .login-head:左 Logo SVG + 品牌名「Antler ERP」+ 副标题「欢迎登录EBC平台」) | ||
| 30 | +│ ├── BrandLogo (鹿角 SVG,复用原型 inline svg path) | ||
| 31 | +│ ├── BrandName ("Antler ERP") | ||
| 32 | +│ └── BrandSub ("欢迎登录EBC平台") | ||
| 33 | +├── LoginHero (对应 .login-hero:占满剩余高度的主视觉区,深蓝渐变 + 网格透视背景) | ||
| 34 | +│ ├── HeroText (对应 .login-text:英文标语 / 中文「企业业务能力平台」/ 巨型 "ERP") | ||
| 35 | +│ └── LoginCard (对应 .login-card:右侧浮层登录卡片,AntD <Card> 或 <Form> 容器) | ||
| 36 | +│ ├── CardTitle ("用户登录") | ||
| 37 | +│ └── LoginForm (AntD <Form>,提交触发认证) | ||
| 38 | +│ ├── Form.Item[sUserName] → <Input prefix={用户图标}> 占位「请输入你的用户名」 | ||
| 39 | +│ ├── Form.Item[password] → <Input.Password prefix={锁图标}> 占位「请输入你的密码」(输入显示星号) | ||
| 40 | +│ ├── Form.Item[companyId] → <Select>(版本下拉,options 来自 GET /api/usr/companies) | ||
| 41 | +│ └── SubmitButton → <Button type="primary" htmlType="submit" block loading={submitting}> "登 录" | ||
| 42 | +└── LoginFooter (对应 .login-foot:版权 / 备案号文本条,置底) | ||
| 43 | +``` | ||
| 44 | + | ||
| 45 | +- 控件选型(依据 `docs/04 § 零` `frontend.ui_lib = Ant Design 5.x`): | ||
| 46 | + - 用户名 → `Input`,带 `prefix` 用户图标(`@ant-design/icons` `UserOutlined`)。 | ||
| 47 | + - 密码 → `Input.Password`,带 `prefix` 锁图标(`LockOutlined`),AntD 默认掩码显示,满足卡片「输入显示星号」。 | ||
| 48 | + - 版本 → `Select`(单选),`options` 由 `GET /api/usr/companies` 返回项映射(`label=sCompanyName`(含 `sVersion` 时拼接展示,见 § 6 规则)`value=id`)。原型用自定义 `.lf.dropdown` 模拟,本规格以 AntD `Select` 等价复刻下拉交互。 | ||
| 49 | + - 登录按钮 → `Button type="primary" block`,提交中置 `loading`。 | ||
| 50 | +- 整页用 `Form`(`onFinish` 触发提交,`onFinishFailed` 不阻断——校验失败 AntD 就近红字提示)。 | ||
| 51 | +- 页面顶栏(`#topbar`)在登录态隐藏(原型 `goTo('login')` 即 `topbar.display='none'`);本规格中登录页是独立路由,不渲染应用顶栏 / 导航壳(顶栏属 FE-02)。 | ||
| 52 | + | ||
| 53 | +--- | ||
| 54 | + | ||
| 55 | +## 3. 页面状态机(≥5 态) | ||
| 56 | + | ||
| 57 | +| 状态 | 触发时机 | UI 表现 | | ||
| 58 | +|---|---|---| | ||
| 59 | +| `companiesLoading`(版本下拉加载中) | 页面挂载即调 `GET /api/usr/companies` 拉版本项(卡片「预加载=页面加载时」) | 版本 `Select` 置 `loading` 且禁用,`placeholder="加载版本中…"`;用户名/密码可先填 | | ||
| 60 | +| `idle`(正常待输入) | 版本项加载完成、表单未提交 | 三字段可编辑,版本 `Select` 展示选项;登录按钮可点;无错误提示 | | ||
| 61 | +| `empty`(版本列表为空) | `GET /api/usr/companies` 返回 `data` 为空数组 | 版本 `Select` 显示空态 `notFoundContent="暂无可用版本"`;版本必填校验仍生效,无法提交(见 § 5);下方轻量提示「未获取到可登录版本,请联系管理员」 | | ||
| 62 | +| `submitting`(表单提交中) | 点击「登录」且前端校验通过,`POST /api/usr/login` 进行中 | 登录按钮 `loading` 且禁用,三字段禁用(防重复提交);拦截重复回车提交 | | ||
| 63 | +| `error`(登录失败 / 取数失败) | 登录接口返回非 0 `code`(40001/40101/40302/42901)或网络异常;或版本接口取数失败 | 按 § 4 文案规则在卡片内/全局 `message.error` 展示对应文案;按钮恢复可点;密码框清空并聚焦(见 § 6 规则 5);版本取数失败时给「版本加载失败,点击重试」入口 | | ||
| 64 | +| `success`(登录成功) | 登录接口返回 `code=0`,拿到 `token` + `user` | 写入 `authSlice`(token + user)→ 持久化 token(见 § 6 规则 6)→ `message.success("登录成功")` → `navigate(目标路由, { replace:true })`(默认 `/`) | | ||
| 65 | + | ||
| 66 | +> 状态以本地组件态 + RTK `authSlice` 表达:`submitting` 用本地 `useState` / `Form` 提交态;`companiesLoading` / `empty` 用本地态;`success` 时 dispatch `authSlice` 的 `setCredentials`。 | ||
| 67 | + | ||
| 68 | +--- | ||
| 69 | + | ||
| 70 | +## 4. 消费的后端端点(对齐 docs/05) | ||
| 71 | + | ||
| 72 | +| 端点 | 方法 | 触发 | 请求 | 成功响应(取用字段) | 失败处理 | | ||
| 73 | +|---|---|---|---|---|---| | ||
| 74 | +| `/api/usr/login` | POST | 点击「登录」且校验通过 | JSON `{ sUserName, password, companyId }` | `Result<{ token, user:{ id, sUserName, sUserType, sLanguage } }>`(`code=0`) | 见下错误码表 | | ||
| 75 | +| `/api/usr/companies` | GET | 页面挂载时(版本下拉预加载) | 无参 | `Result<List<{ id, sCompanyName, sVersion }>>`(`code=0`) | 取数失败 → 版本 `Select` 空 + 重试入口 + `message.error("版本加载失败")` | | ||
| 76 | + | ||
| 77 | +> `/api/usr/companies` 在 docs/05 主清单未单列,但 REQ-USR-004 后端规格(`docs/superpowers/specs/2026-06-01-REQ-USR-004.md` § 8 D1)明确补齐该放行只读端点专供登录「版本」下拉取数;前端据此消费(见 § 7 决策 D1)。 | ||
| 78 | + | ||
| 79 | +请求 / 响应约定(依据 `docs/04 § 1.4 / § 2.3 / § 2.4`): | ||
| 80 | +- 统一走 `frontend/src/api/request.ts` 的 Axios 实例(`baseURL` 指向 `/api`,端口取 `config-vars.yaml backend.http_port=5172`,开发期经 Vite proxy 转发,见 § 7 决策 D2)。 | ||
| 81 | +- 响应拦截器拆 `Result`:`code=0` 取 `data`;非 0 `code` 抛出供页面按错误码分流文案;登录端点放行、不带 `Authorization` 头(请求拦截器仅对已登录态注入 token)。 | ||
| 82 | + | ||
| 83 | +### 错误码 → 前端文案(对齐 docs/05 § REQ-USR-004) | ||
| 84 | + | ||
| 85 | +| code | 含义(后端) | 前端文案 | 展示方式 | | ||
| 86 | +|---|---|---|---| | ||
| 87 | +| `0` | 成功 | 「登录成功」 | `message.success` 后跳转 | | ||
| 88 | +| `40001` | 参数校验失败(缺用户名/密码/版本,或 companyId 非法) | 「请填写用户名、密码并选择版本」 | 卡片内 `Alert`/`message.error`;正常情况下前端必填校验已拦截,此为兜底 | | ||
| 89 | +| `40101` | 认证失败(用户名或密码错误,不区分以防枚举) | 「用户名或密码错误」 | `message.error` + 密码框清空聚焦 | | ||
| 90 | +| `40302` | 账号已禁用(iIsVoid=1) | 「该账号已被禁用,请联系管理员」 | `message.error` | | ||
| 91 | +| `42901` | 登录过于频繁(连续失败超阈值被限流) | 「登录尝试过于频繁,请稍后再试」 | `message.error` | | ||
| 92 | +| 网络/超时/5xx | 请求异常 | 「网络异常,请稍后重试」 | 响应拦截器兜底 `message.error` | | ||
| 93 | + | ||
| 94 | +> `40101` 严格沿用后端「不区分账号不存在/密码错误」的统一提示,前端**不得**自行细化为「该用户不存在」等可枚举文案(卡片边界 + REQ-USR-004 后端规格规则 2/3)。 | ||
| 95 | + | ||
| 96 | +--- | ||
| 97 | + | ||
| 98 | +## 5. 业务规则前端复刻清单(逐条) | ||
| 99 | + | ||
| 100 | +| # | 规则 | 触发时机 | 前端报错文案 | 来源 | | ||
| 101 | +|---|---|---|---|---| | ||
| 102 | +| BR1 | 用户名必填 | 失焦 / 提交时 AntD `Form` 校验 | 「请输入用户名」 | REQ-USR-004 输入表(用户名 必填=是)/ 原型占位「请输入你的用户名」 | | ||
| 103 | +| BR2 | 密码必填 | 失焦 / 提交时校验 | 「请输入密码」 | REQ-USR-004 输入表(密码 必填=是)/ 原型占位「请输入你的密码」 | | ||
| 104 | +| BR3 | 密码输入掩码显示(星号) | 输入时 | —(无报错,`Input.Password` 默认掩码) | REQ-USR-004 输入表(密码 业务规则=「输入显示星号」) | | ||
| 105 | +| BR4 | 版本必填(下拉单选) | 提交时校验 | 「请选择版本」 | REQ-USR-004 输入表(版本 必填=是,输入方式=下拉单选) | | ||
| 106 | +| BR5 | 版本选项来自后端公司表,页面加载时预取 | 页面挂载 | 取数失败:「版本加载失败」 | REQ-USR-004 输入表(版本 显示来源=`公司表`、预加载=页面加载时)+ 依赖接口注记 | | ||
| 107 | +| BR6 | 认证失败统一提示(防账号枚举),不区分账号不存在/密码错误 | 登录接口返回 `40101` | 「用户名或密码错误」 | REQ-USR-004 边界 + 后端规格规则 2/3 | | ||
| 108 | +| BR7 | 禁用用户禁止登录 | 登录接口返回 `40302` | 「该账号已被禁用,请联系管理员」 | REQ-USR-004 边界(已禁用用户禁止登录) | | ||
| 109 | +| BR8 | 连续失败限流提示 | 登录接口返回 `42901` | 「登录尝试过于频繁,请稍后再试」 | REQ-USR-004 跨字段规则(连续失败需限流)+ 后端规格规则 8 | | ||
| 110 | +| BR9 | 登录成功签发 token,前端据此进入受保护区 | 登录接口返回 `code=0` | —(成功,`message.success("登录成功")`) | REQ-USR-004 跨字段规则(登录成功签发访问令牌)+ docs/04 § 2.2 登录态进 store | | ||
| 111 | +| BR10 | 防重复提交 | `submitting` 期间 | —(按钮 loading + 字段禁用拦截重复提交) | 通用交互安全(提交中态)+ docs/04 § 2.4 错误就近处理 | | ||
| 112 | +| BR11 | 前端不做密码强度/明文处理,仅原样提交(明文经 HTTPS,后端 BCrypt 比对) | 提交时 | — | REQ-USR-004 后端契约(password 提交明文经 HTTPS)/ docs/04 § 1.7 | | ||
| 113 | + | ||
| 114 | +> 前端只做**输入完整性校验(必填/格式)**;身份真伪、禁用判定、限流计数均由后端裁决,前端按返回码渲染文案,不复制后端认证逻辑。 | ||
| 115 | + | ||
| 116 | +--- | ||
| 117 | + | ||
| 118 | +## 6. 交互与实现要点(复刻原型语义) | ||
| 119 | + | ||
| 120 | +1. **页面布局**:复刻 `.login-wrap` 三段式——顶部品牌头(`.login-head`)、中部深蓝主视觉 + 右侧浮层登录卡(`.login-hero` + `.login-card` 绝对定位居右垂直居中)、底部版权条(`.login-foot`)。主视觉的网格透视 / 径向渐变背景按原型 `::before`/`::after` 装饰性复刻(纯展示,无交互)。 | ||
| 121 | +2. **登录卡定位**:卡片宽约 380px,右侧 8% 处垂直居中(原型 `.login-card{right:8%;top:50%;transform:translateY(-50%)}`),带阴影浮层。响应式收窄时允许卡片回流居中(默认行为,见 § 7 决策 D4)。 | ||
| 122 | +3. **版本下拉**:原型默认值「标准版」、点击 `#ver-drop` 展开 `.opt` 列表。本规格用 AntD `Select`,options 全部来自 `GET /api/usr/companies`,**不硬编码「标准版」**(原型为静态 demo 值);若返回项含 `sVersion` 则下拉 label 展示为 `sCompanyName(sVersion)`,否则仅 `sCompanyName`;`value` 一律取 `id`,提交时作为 `companyId`。仅一项时默认选中该项。 | ||
| 123 | +4. **提交触发**:原型按钮 `data-go="main"` 是直接切屏的 demo 行为;本规格改为 `Form.onFinish` → 调 `POST /api/usr/login`,成功才跳转,失败留在登录页并提示。支持回车提交(AntD `Form` 默认)。 | ||
| 124 | +5. **失败后处理**:`40101`/`42901` 等失败后清空密码字段并聚焦密码框,用户名与版本保留,便于重试(通用登录交互;不属硬业务规则,登记于 § 7 决策 D5)。 | ||
| 125 | +6. **登录态落地**(docs/04 § 2.2 / § 2.3):成功后 `token` + `user` 经 `authSlice.setCredentials` 进 Redux store;token 同时持久化(默认 `localStorage`,键名 `xly_erp_token`,供刷新后 `request.ts` 注入 `Authorization: Bearer`,见 § 7 决策 D6);随后 `navigate('/', { replace:true })`。 | ||
| 126 | +7. **路由守卫协作**:登录页本身为放行路由;已登录用户访问 `/login` 时直接重定向到主页(避免重复登录)。守卫逻辑实现细节属路由壳(FE-02),本页只在 `success` 后触发导航。 | ||
| 127 | + | ||
| 128 | +--- | ||
| 129 | + | ||
| 130 | +## 7. Design Tokens 引用清单(`src/styles/tokens.css`,仅 `var(--color-*)`) | ||
| 131 | + | ||
| 132 | +> 约束:组件样式只用 `var(--color-*)`,禁止硬编码 hex/rgba;色值冲突时 `tokens.css` 优先于 `prototype/`(原型内联 `:root` 变量为 demo 私有,不作为色值 SSoT)。AntD 主题色经 `ConfigProvider` 对齐 `--color-primary`。 | ||
| 133 | + | ||
| 134 | +| 用途 | Token | 备注 | | ||
| 135 | +|---|---|---| | ||
| 136 | +| 登录按钮 / 主操作 / 链接强调 | `var(--color-primary)` | 对应原型 `.submit` 蓝色按钮;同时作为 AntD 主题 `colorPrimary` | | ||
| 137 | +| 登录卡背景 / 输入框背景 | `var(--color-form-bg-edit)` | 卡片与可编辑输入框底色(白) | | ||
| 138 | +| 输入框字体色 | `var(--color-form-fg)` | 输入文本色 | | ||
| 139 | +| 下拉项 hover 背景 | `var(--color-form-bg-hover)` | 版本 `Select` 选项 hover(原型 `.opt .o:hover`) | | ||
| 140 | +| 通用文字 / 标题 | `var(--color-text)` | 卡片标题「用户登录」、品牌副标题 | | ||
| 141 | +| 次要文字 / 占位 / 页脚版权 | `var(--color-text-secondary)` | 输入占位、`.login-foot` 版权文本、备案号 | | ||
| 142 | +| 边框 / 分隔线 / 输入框描边 | `var(--color-border)` | 输入框边框(原型 `.lf` 描边)、卡片描边 | | ||
| 143 | +| 页面基础背景 | `var(--color-bg-base)` | `.login-wrap` / `.login-head` / `.login-foot` 浅灰底(原型用 `#eaedf2`,统一映射到基础底色 token) | | ||
| 144 | +| 错误 / 失败提示文字 | `var(--color-error)` | 校验红字、`message.error` 强调(AntD 默认已用主题 error,显式登记以备自定义文案样式) | | ||
| 145 | + | ||
| 146 | +> 主视觉 `.login-hero` 的深蓝渐变 / 网格透视为纯装饰,`tokens.css` 未定义对应深色品牌色 token;本规格将其作为登录页**局部装饰样式**保留在 `Login` 的 scoped 样式里,不引入新全局 token,也不挪用语义 token(见 § 8 决策 D7)。该装饰不承载状态语义,不违反「语义色只用 token」约束。 | ||
| 147 | + | ||
| 148 | +--- | ||
| 149 | + | ||
| 150 | +## 8. 自主决策记录(decisions) | ||
| 151 | + | ||
| 152 | +| # | 问题 | 选择 | 依据 | 置信度 | | ||
| 153 | +|---|---|---|---|---| | ||
| 154 | +| D1 | 版本下拉数据从哪个端点取(docs/05 主清单只列 `POST /api/usr/login`) | 消费 `GET /api/usr/companies`(放行只读端点) | REQ-USR-004 后端规格 § 8 D1 已补齐该端点专供登录「版本」预加载;卡片输入表标版本「显示来源=公司表、预加载=页面加载时」 | high | | ||
| 155 | +| D2 | 开发期前端如何到达后端(跨端口 5173→5172) | `request.ts` baseURL=`/api`,Vite dev proxy 把 `/api` 转发到 `http://localhost:5172`(取 config-vars `backend.http_port`) | docs/04 § 2.3「baseURL 指向后端 /api」;config-vars 锁定 dev_port=5173 / http_port=5172,proxy 是 Vite 标准跨端口方案 | high | | ||
| 156 | +| D3 | 登录成功后跳转目标路由 | 默认跳主页 `/`(应用落地路由,属 FE-02 范畴);用 `replace:true` 防回退到登录页 | 原型登录按钮 `data-go="main"` 即进主页;主页路由壳由 FE-02 定义,本页仅触发导航到根路由 | high | | ||
| 157 | +| D4 | 小屏 / 窄视口下登录卡布局 | 默认随容器回流(卡片可居中),不专门设计移动端断点 | 目标用户为「企业内部管理人员」桌面端 ERP(CLAUDE.md),原型为定宽桌面布局;移动适配非本 REQ 验收项 | medium | | ||
| 158 | +| D5 | 登录失败后是否清空密码 / 聚焦 | 失败(40101/42901 等)后清空密码框并聚焦,保留用户名与版本 | 通用安全登录交互(避免残留密码、便于重试),不与任何业务规则冲突;非硬性需求,故登记 | medium | | ||
| 159 | +| D6 | token 持久化方式 | `localStorage`(键 `xly_erp_token`),供刷新后 `request.ts` 注入;登录态同时进 Redux `authSlice` | docs/04 § 2.2「登录态/token 用 Redux 管理」+ § 2.3「请求拦截器注入 Authorization」;持久化需跨刷新留存,localStorage 为常见取舍(无 SSoT 明确指定,故登记;如后续要求更严可换 sessionStorage/HttpOnly) | medium | | ||
| 160 | +| D7 | 主视觉深蓝渐变 / 网格背景的色值来源(tokens.css 无对应深色品牌 token) | 作为登录页局部装饰样式保留(scoped),不新增全局 token、不挪用语义 token;语义色(按钮/文字/边框/错误)严格走 token | tokens.css 仅定义语义/状态色,无品牌主视觉深色;该背景纯装饰无状态语义,局部化最小侵入;符合「语义色只用 var(--color-*)」约束 | medium | | ||
| 161 | +| D8 | 版本下拉 label 显示格式(含 sVersion 时) | `sCompanyName(sVersion)`,无 sVersion 时仅 `sCompanyName`;value 恒取 id | 后端返回 `{id, sCompanyName, sVersion}`,sVersion 可为 null;拼接展示更可辨识账套,value 用 id 对齐 login 入参 companyId | high | | ||
| 162 | + | ||
| 163 | +> 本规格不含后端实现细节(认证比对 / 令牌签发 / 数据访问 / 库表迁移等均不在前端作用域);所有认证裁决与错误码均由后端产生,前端仅消费与渲染。 |
docs/superpowers/specs/2026-06-01-FE-02.md
0 → 100644
| 1 | +# FE-02 主页与导航框架 — 实现规格(前端) | ||
| 2 | + | ||
| 3 | +> 阶段:前端(frontend)。作用域限定 `frontend/` 下的页面 / 组件 / 路由 / store / api / 样式。 | ||
| 4 | +> SSoT 引用:原型 `prototype/erp.html`(`#topbar` / `#nav-overlay` / `#screen-main` 三区域,布局与交互权威);技术规范 `docs/04-技术规范.md` § 零 / § 二;Design Tokens `src/styles/tokens.css`;登录态来源 FE-01 规格 `docs/superpowers/specs/2026-06-01-FE-01.md`(`authSlice` + token 持久化约定)。 | ||
| 5 | +> 业务功能粒度:本 FE 覆盖**登录后落地页(主页/KPI 看板)** + **应用外壳(顶栏 + 全部导航总览 overlay + 标签页栈 + 路由壳 + 路由守卫)** + **常用操作快捷入口**。它是 FE-03(用户列表)/ FE-04(用户单据)的承载容器,但不实现这两个功能本身(标签页只负责挂载与切换,目标页内容属各自 FE)。 | ||
| 6 | +> 本规格只消费已锁定事实。原型 KPI 看板 / 角色流程树 / 导航分组均为**静态 demo 数据**,docs/05 未定义任何 KPI / 导航 / 仪表盘后端端点;故本壳层除复用 FE-01 已写入 `authSlice` 的当前用户身份外,**不新增后端取数**(见 § 8 决策 D1)。 | ||
| 7 | + | ||
| 8 | +--- | ||
| 9 | + | ||
| 10 | +## 1. 关联 REQ + 关联原型 | ||
| 11 | + | ||
| 12 | +| 维度 | 内容 | | ||
| 13 | +|---|---| | ||
| 14 | +| 业务功能 | FE-02 主页与导航框架(顶栏 + 全部导航总览 + 主页 KPI 看板 + 常用操作;登录后落地页与路由壳) | | ||
| 15 | +| 关联 REQ | 无直接的业务 CRUD REQ。本 FE 是壳层 / 落地页,是 REQ-USR-001~004 对应前端功能(FE-03 用户列表 / FE-04 用户单据 / FE-01 登录)的**导航与承载容器**:常用操作「用户列表」与导航总览「用户管理 > 用户列表」入口指向 FE-03(`GET /api/usr/users`),新增入口指向 FE-04(`POST /api/usr/users`)。当前用户身份展示复用 FE-01 登录后写入 `authSlice` 的 `user`(来源 REQ-USR-004 响应 `user:{ id, sUserName, sUserType, sLanguage }`) | | ||
| 16 | +| 关联原型 | `prototype/erp.html` → `#topbar`(顶栏)、`#nav-overlay`(全部导航总览浮层)、`#screen-main`(主页:`.kpi-head` / `.three-col` 左侧角色流程树 + `.kpi-body` KPI 网格 / `.common-ops` 常用操作 / `footer.foot` 页脚)、`.app` / `.stage` / `.screen`(屏切换壳) | | ||
| 17 | +| 路由 | 应用受保护区根路由 `/`(登录后落地到主页 `/` = `<HomePage>`);子路由 `/usr/users`(FE-03)、`/usr/users/new` 与 `/usr/users/:id`(FE-04)。React Router v6,外壳布局路由 `<AppLayout>` 包裹所有受保护子路由(见 § 6 路由与守卫) | | ||
| 18 | +| 落地组件目录 | `frontend/src/layouts/AppLayout/`(应用外壳:顶栏 + 标签栈 + 导航 overlay + 内容 outlet + 页脚);`frontend/src/pages/home/HomePage/`(主页 KPI 看板);`frontend/src/router/`(路由表 + `RequireAuth` 守卫);导航 / KPI / 常用操作的静态配置置于 `frontend/src/layouts/AppLayout/navConfig.ts` 与 `frontend/src/pages/home/HomePage/dashboardData.ts`;当前用户身份读 `frontend/src/store/slices/authSlice` | | ||
| 19 | + | ||
| 20 | +> 原型用纯静态 HTML + 内联 demo 脚本(`goTo(name)` 切 `.screen.active`、`#nav-toggle` 切换 `#nav-overlay.show`、`openTab/tabsOpen` 维护用户列表/用户单据标签的显隐、`.close` 关闭标签、`data-go` 委托点击切屏)模拟单页多屏。本规格按 React Router v6 + AntD 5 复刻其**布局与交互语义**:原型的「屏切换」改为真实路由切换,「标签栈」改为受控的标签页状态,导航与 KPI 数据改为前端静态配置(无后端)。 | ||
| 21 | + | ||
| 22 | +--- | ||
| 23 | + | ||
| 24 | +## 2. 组件树(按区域分块,推导自 prototype DOM) | ||
| 25 | + | ||
| 26 | +应用外壳 `AppLayout`(受保护区布局路由,对应原型 `.app`:纵向 flex、占满视口、`overflow:hidden`): | ||
| 27 | + | ||
| 28 | +``` | ||
| 29 | +AppLayout(对应 .app:column flex / 100vh / overflow hidden;登录后所有受保护路由的外壳) | ||
| 30 | +├── TopBar(对应 #topbar:高 44px 深色顶栏,z 高于内容) | ||
| 31 | +│ ├── BrandLogo(对应 .logo:鹿角 SVG,点击回主页 / data-go="main",复用原型 inline svg path) | ||
| 32 | +│ ├── TabStrip(对应 .tabs:左起 全部导航按钮 + 固定「主页」标签 + 动态业务标签栈) | ||
| 33 | +│ │ ├── NavToggleButton(对应 .nav-btn#nav-toggle:「全部导航」汉堡按钮,切换导航总览 overlay;overlay 打开时 .active) | ||
| 34 | +│ │ ├── HomeTab(对应 .tab[data-go=main]:固定「主页」标签,不可关闭) | ||
| 35 | +│ │ └── DynamicTab[](对应 #tab-userlist / #tab-userdetail:业务标签,含标题 + 关闭「✕」;按已打开集合渲染,见 § 3 标签栈状态) | ||
| 36 | +│ └── TopBarRight(对应 .topbar .right:搜索图标 / 通知图标 / 当前用户区 / 更多「⋯」) | ||
| 37 | +│ ├── IconSearch(对应 .right .ic[title=搜索],纯占位,无后端,见 § 8 决策 D4) | ||
| 38 | +│ ├── IconNotice(对应 .right .ic[title=通知],纯占位,无后端,见 § 8 决策 D4) | ||
| 39 | +│ ├── CurrentUserMenu(对应 .user:展示 `sUserName(sUserType)` + 下拉箭头;下拉含「退出登录」) | ||
| 40 | +│ └── IconMore(对应 .more「⋯」,纯占位) | ||
| 41 | +├── NavOverlay(对应 #nav-overlay:覆盖内容区的全屏深色导航总览浮层,默认隐藏 / .show 显示) | ||
| 42 | +│ ├── NavOverlaySide(对应 #nav-overlay .side:左侧一级模块竖列,20 项,「系统设置」默认 active) | ||
| 43 | +│ └── NavOverlayGrid(对应 #nav-overlay .grid:右侧 7 列分组卡片,每组标题 + 链接项;「用户列表」「系统功能模块设置」带 ★) | ||
| 44 | +├── ContentOutlet(对应 .stage > .screen:受保护子路由内容渲染位,React Router <Outlet/>) | ||
| 45 | +│ └── (主页 HomePage / 用户列表 FE-03 / 用户单据 FE-04 按当前路由挂载) | ||
| 46 | +└── AppFooter(对应 footer.foot / .login-foot:版权 + 经营范围 + 备案号文本条,置底) | ||
| 47 | + | ||
| 48 | +HomePage(对应 #screen-main .main-wrap:主网格 1fr / 280px 两列,落地路由 `/`) | ||
| 49 | +├── KpiHeadBar(对应 .kpi-head:标题「KPI监控」+ 今日未处理统计 + 未清总数统计 + AI 助手按钮) | ||
| 50 | +│ ├── KpiTitle("KPI监控") | ||
| 51 | +│ ├── KpiStat × 2("今日未处理"红色数字 / "未清总数"蓝色数字,数据见 § 8 决策 D2) | ||
| 52 | +│ └── AiAssistantButton(对应 .ai-btn:「小ai同学,请帮我安排今日工作」,纯占位按钮,无后端) | ||
| 53 | +├── DashboardThreeCol(对应 .three-col:左 280px 角色/流程树 + 右 KPI 主表) | ||
| 54 | +│ ├── RoleProcessTree(对应 .left-nav.nav-tree:「按角色」「按流程」分组 + 部门/流程条目,静态,点击高亮,见 § 8 决策 D2) | ||
| 55 | +│ └── KpiBoard(对应 .kpi-body 7 列网格:导航类型 / 角色 / KPI待处理事项 / KPI内容描述 / 今日未处理 / 未清总数 / 子流程;含跨行合并,静态数据) | ||
| 56 | +└── CommonOps(对应 .common-ops:右侧「常用操作」卡片,链接:用户列表 → FE-03 路由;系统功能模块设置 → 占位) | ||
| 57 | +``` | ||
| 58 | + | ||
| 59 | +- 控件选型(依据 `docs/04 § 零` `frontend.ui_lib = Ant Design 5.x`): | ||
| 60 | + - 顶栏标签栈 → AntD `Tabs`(`type="editable-card"` 或自定义受控标签条,复刻原型「主页固定 + 业务标签可关」语义);亦可用自定义 `div` 条配合路由(实现取舍登记于 § 8 决策 D3),「主页」标签 `closable=false`。 | ||
| 61 | + - 全部导航总览 → AntD `Drawer`(自定义全屏样式)或受控覆盖层 `div`,内部左列用列表、右侧用 `Row/Col` 栅格分组(对应原型 7 列);点击叶子链接 → 路由跳转或关闭 overlay(仅「用户列表」等有真实路由的项跳转,其余为占位项,见 § 8 决策 D4)。 | ||
| 62 | + - 当前用户菜单 → AntD `Dropdown`,菜单项「退出登录」。 | ||
| 63 | + - KPI 看板 → AntD `Table`(含 `rowSpan` 合并「导航类型 / 角色 / 子流程」列)或自定义 CSS Grid 复刻原型 `.kpi-body` 合并网格(实现取舍登记于 § 8 决策 D5);「KPI待处理事项」「KPI内容描述」为蓝色可点链接样式(原型 `.link`,纯展示,无明细页)。 | ||
| 64 | + - 角色/流程树 → AntD `Tree` 或自定义列表,「按角色」「按流程」为分组节点,叶子带计数后缀(如「客服部 (30127)」),点击高亮选中(原型 `.item.active`),不触发取数。 | ||
| 65 | + - 常用操作 / 链接 → 文本链接(AntD `Typography.Link` 或 `<a>`),「用户列表」`onClick` → `navigate('/usr/users')`(打开 FE-03 标签)。 | ||
| 66 | + - 页脚 → 静态文本条。 | ||
| 67 | +- 顶栏在登录页(`/login`,FE-01)**不渲染**(原型 `goTo('login')` 即 `topbar.display='none'`;本规格中 `/login` 不挂 `AppLayout`,故无顶栏)。 | ||
| 68 | + | ||
| 69 | +--- | ||
| 70 | + | ||
| 71 | +## 3. 页面状态机(≥5 态) | ||
| 72 | + | ||
| 73 | +外壳与主页的状态以「路由守卫态 + 标签栈本地态 + 导航 overlay 开关态 + 主页静态数据渲染态」表达。主页无后端取数,其「loading/error/empty」语义落在**身份就绪判定**与**静态数据渲染**上: | ||
| 74 | + | ||
| 75 | +| 状态 | 触发时机 | UI 表现 | | ||
| 76 | +|---|---|---| | ||
| 77 | +| `authResolving`(登录态校验中 / loading) | 进入任一受保护路由(含主页 `/`)时,`RequireAuth` 守卫判定 token 是否存在并解析 `authSlice.user`;刷新后 `user` 尚未从持久化 token 恢复时的瞬时态 | 外壳显示轻量加载占位(AntD `Spin` 居中),不渲染内容;解析完成后转 `ready` 或重定向 `/login` | | ||
| 78 | +| `unauthenticated`(未登录 → 重定向) | 守卫判定无有效 token(未登录 / token 失效 / 收到 401) | 不渲染外壳,`<Navigate to="/login" replace />`(携带 `from` 以便登录后回跳,见 § 6) | | ||
| 79 | +| `ready`(外壳就绪 / 正常) | 守卫通过、`authSlice.user` 已就绪 | 渲染 TopBar(含当前用户 `sUserName(sUserType)`)+ 主页落地内容(KPI 看板 / 角色树 / 常用操作);标签栈仅含「主页」 | | ||
| 80 | +| `navOverlayOpen`(导航总览展开) | 点击「全部导航」按钮 | `NavOverlay` 覆盖内容区(`.show`),「全部导航」按钮 `.active`;点击叶子项 / 再次点击按钮 / Esc 关闭 | | ||
| 81 | +| `tabOpen`(业务标签已打开 / 多标签栈) | 从主页常用操作或导航总览点击「用户列表」「新增用户」等有路由的入口 | 顶栏出现对应业务标签(FE-03「用户列表」/ FE-04「用户信息单据」),内容区切到该子路由;当前标签高亮,主页标签保留;关闭标签回退到前一个有效标签(默认主页,见 § 6 标签栈规则) | | ||
| 82 | +| `empty`(KPI 看板 / 角色树空数据) | 静态数据源(`dashboardData.ts`)为空数组(理论兜底;当前为内置 demo 数据,非空) | KPI 看板与角色树渲染 AntD `Empty`「暂无数据」占位,不报错 | | ||
| 83 | +| `error`(壳层渲染异常兜底) | 子路由组件渲染抛错(FE-03/FE-04 内部异常)或 token 过期被动 401 | 路由级 `ErrorBoundary` 兜底展示「页面出错,请刷新或返回主页」+ 返回主页入口;401 由 `request.ts` 响应拦截器统一跳 `/login`(docs/04 § 2.4) | | ||
| 84 | + | ||
| 85 | +> 状态以本地组件态 + RTK `authSlice` 表达:`navOverlayOpen` / 标签栈(已打开标签集合 + 当前激活标签)用本地 `useState`(或一个轻量 `uiSlice`,取舍见 § 8 决策 D3);`authResolving` / `unauthenticated` / `ready` 由 `RequireAuth` 读 `authSlice.token`/`user` 判定;本壳层**不发起 KPI/导航后端请求**,故无 `dataLoading`/`fetchError` 态(与原型静态 demo 一致)。 | ||
| 86 | + | ||
| 87 | +--- | ||
| 88 | + | ||
| 89 | +## 4. 消费的后端端点(对齐 docs/05) | ||
| 90 | + | ||
| 91 | +本壳层 / 落地页**自身不直接调用任何后端业务端点**。原型主页的 KPI 看板、角色/流程树、导航分组均为静态 demo,docs/05 未定义对应端点(无 KPI / dashboard / nav 接口),故不消费、不杜撰(见 § 8 决策 D1)。涉及的端点均为「外壳作为入口/容器」间接关联: | ||
| 92 | + | ||
| 93 | +| 端点 | 方法 | 与本 FE 的关系 | 触发位置 | 归属 FE | | ||
| 94 | +|---|---|---|---|---| | ||
| 95 | +| `/api/usr/login` | POST | 登录态来源:FE-01 登录成功写入 `authSlice`(`token` + `user`);本壳读 `authSlice.user` 渲染当前用户区,读 `authSlice.token` 做路由守卫 | 不由本壳触发(FE-01 触发) | FE-01(REQ-USR-004) | | ||
| 96 | +| `/api/usr/users` | GET | 「常用操作 > 用户列表」与「导航总览 > 用户管理 > 用户列表」入口跳转后由 FE-03 调用;本壳只负责导航到 `/usr/users`,不直接调 | 入口点击 → 路由跳转 | FE-03(REQ-USR-003) | | ||
| 97 | +| `/api/usr/users` | POST | 「新增用户」入口跳转后由 FE-04 调用;本壳只负责导航到 `/usr/users/new` | 「新增」入口点击 → 路由跳转 | FE-04(REQ-USR-001) | | ||
| 98 | + | ||
| 99 | +请求 / 响应约定(依据 `docs/04 § 1.4 / § 2.3 / § 2.4`,本壳仅承接守卫与跳转,不新增取数逻辑): | ||
| 100 | +- 统一走 FE-01 已建立的 `frontend/src/api/request.ts` Axios 实例(`baseURL=/api`,开发期经 Vite proxy 转发到后端 `http://localhost:5172`,端口取 `config-vars.yaml backend.http_port=5172`;FE-01 § 7 D2 已锁定)。 | ||
| 101 | +- 响应拦截器对 `401` 统一跳 `/login`(docs/04 § 2.4),与本壳路由守卫 `unauthenticated` 态协同:被动 401 清理 `authSlice` 并重定向登录页。 | ||
| 102 | +- 退出登录:清空 `authSlice`(`clearCredentials`)+ 移除持久化 token(FE-01 锁定键名 `xly_erp_token`,localStorage)→ `navigate('/login', { replace:true })`(纯前端动作,无注销端点,见 § 8 决策 D6)。 | ||
| 103 | + | ||
| 104 | +> 错误码表:本壳层自身无业务端点,无专属错误码。被动 401(token 失效)按 docs/04 § 2.4 由响应拦截器兜底跳登录;子路由业务错误码由 FE-03 / FE-04 各自规格承载。 | ||
| 105 | + | ||
| 106 | +--- | ||
| 107 | + | ||
| 108 | +## 5. 业务规则前端复刻清单(逐条) | ||
| 109 | + | ||
| 110 | +> 本 FE 无 CRUD 业务校验规则(不在表单/查询语义内);其「规则」为**外壳导航 / 权限可见性 / 登录态约束**的交互规则,逐条复刻自原型交互语义与 docs/04 认证约定。 | ||
| 111 | + | ||
| 112 | +| # | 规则 | 触发时机 | 前端报错 / 反馈文案 | 来源 | | ||
| 113 | +|---|---|---|---|---| | ||
| 114 | +| BR1 | 未登录不得进入受保护区(主页及所有业务子路由) | 进入 `/`、`/usr/**` 等受保护路由时守卫判定无有效 token | —(无报错,静默 `<Navigate to="/login" replace />`,携 `from`) | docs/04 § 2.1 路由守卫「未登录跳登录页」/ § 1.7 认证 | | ||
| 115 | +| BR2 | 已登录用户访问 `/login` 直接回主页 | 进入 `/login`(FE-01)时检测到有效登录态 | —(重定向到 `/` 或 `from`) | FE-01 § 6.7 路由守卫协作(已登录访问登录页重定向) | | ||
| 116 | +| BR3 | 顶栏当前用户区展示登录用户标识 | 外壳就绪渲染时 | 展示 `sUserName(sUserType 中文名)`(如「朱子纯(超级管理员)」,对应原型文案);`user` 缺失时退化为占位用户名 | REQ-USR-004 响应 `user:{ sUserName, sUserType }` / 原型 `.user` 文案 | | ||
| 117 | +| BR4 | 「主页」标签固定且不可关闭;业务标签可关闭 | 标签栈渲染 / 点击「✕」 | —(主页无关闭按钮;关闭业务标签后激活前一有效标签,默认主页) | 原型 `openTab/tabsOpen/.close` 逻辑(主页 tab 无 `.close`,userlist/userdetail tab 有) | | ||
| 118 | +| BR5 | 关闭「用户列表」标签同时关闭其下「用户信息单据」标签 | 点击「用户列表」标签的「✕」 | —(联动关闭,回主页) | 原型 `data-close=userlist` 分支:`tabsOpen.userdetail=false` 后 `goTo('main')` | | ||
| 119 | +| BR6 | 打开「用户信息单据」标签时自动确保「用户列表」标签存在 | 从主页/导航打开用户单据(如「新增用户」) | —(隐式打开父标签) | 原型 `openTab('userdetail')` 内 `tabsOpen.userlist=true` | | ||
| 120 | +| BR7 | 全部导航总览:仅「用户列表」等已实现路由项可跳转,其余为占位(不跳转 / 不报错) | 点击导航总览叶子项 | 占位项点击:关闭 overlay 不跳转(或 `message.info("功能开发中")`,见 § 8 决策 D4) | 原型 `data-go` 仅 `userlist` 有目标,其余 `<a>` 无 `data-go`(占位) | | ||
| 121 | +| BR8 | 常用操作「用户列表」跳转到用户查询页(FE-03) | 点击常用操作或导航中「用户列表」 | —(`navigate('/usr/users')` + 打开标签) | 原型 `.common-ops a[data-go=userlist]` / 导航「用户管理>用户列表 ★」 | | ||
| 122 | +| BR9 | 退出登录清理登录态并回登录页 | 点击当前用户下拉「退出登录」 | `message.success("已退出登录")` 后跳 `/login` | docs/04 § 2.2 登录态进 store(退出即清)/ FE-01 token 持久化约定(清键) | | ||
| 123 | +| BR10 | 被动登录失效(401)统一跳登录页 | 任意受保护请求返回 401(token 过期/无效) | `message.warning("登录已失效,请重新登录")`(拦截器兜底)后跳 `/login` | docs/04 § 2.4「401 跳登录」 | | ||
| 124 | +| BR11 | KPI 看板 / 角色流程树为只读展示,不触发后端、不产生写副作用 | 主页渲染 / 点击树节点 / 点击 KPI 链接 | —(纯前端高亮 / 无明细页,静态 demo 数据) | 原型主页为静态 demo;docs/05 无 KPI 端点(见 § 8 决策 D1) | | ||
| 125 | + | ||
| 126 | +> 本壳层只做**导航编排 + 登录态守卫 + 静态展示**;任何业务真伪 / 权限裁决均由后端在子功能(FE-03/FE-04)调用时裁决,本壳不复制后端逻辑,亦不杜撰 KPI/导航后端取数。 | ||
| 127 | + | ||
| 128 | +--- | ||
| 129 | + | ||
| 130 | +## 6. 路由与交互实现要点(复刻原型语义) | ||
| 131 | + | ||
| 132 | +1. **路由结构(React Router v6,docs/04 § 2.1)**: | ||
| 133 | + - `/login` → FE-01 `<LoginPage>`(放行路由,**不**包 `AppLayout`,故无顶栏,复刻原型登录态隐藏顶栏)。 | ||
| 134 | + - 受保护区:`<RequireAuth>` 包裹的布局路由 `<AppLayout>`,子路由: | ||
| 135 | + - index `/` → `<HomePage>`(登录后落地页 = 主页 KPI 看板)。 | ||
| 136 | + - `/usr/users` → FE-03 用户列表。 | ||
| 137 | + - `/usr/users/new` → FE-04 新增用户单据。 | ||
| 138 | + - `/usr/users/:id` → FE-04 修改用户单据。 | ||
| 139 | + - 未匹配路由 → 重定向 `/`(或 404 占位,见 § 8 决策 D7)。 | ||
| 140 | +2. **路由守卫 `RequireAuth`(BR1/BR2/BR10)**:读 `authSlice.token`(刷新后由 FE-01 持久化键 `xly_erp_token` 恢复,FE-01 § 7 D6)。无 token → `<Navigate to="/login" replace state={{ from }} />`;有 token 但 `user` 未就绪 → `authResolving` 占位(`Spin`)。已登录访问 `/login` 由 FE-01 守卫处理(重定向到 `from` 或 `/`)。 | ||
| 141 | +3. **屏切换 → 路由切换**:原型 `goTo(name)` 切 `.screen.active` 的单页多屏,本规格改为真实路由切换;`.stage > .screen` 容器对应 `AppLayout` 的 `<Outlet/>` 渲染位。 | ||
| 142 | +4. **标签栈(BR4/BR5/BR6,复刻原型 `tabsOpen`/`openTab`/`.close`)**:顶栏维护一组「已打开业务标签」(受控本地态或 `uiSlice`)。「主页」为固定标签(不可关、恒在最左、对应 index 路由)。打开 FE-03 → 追加「用户列表」标签并激活;打开 FE-04 → 确保「用户列表」标签存在再追加「用户信息单据」标签(BR6)。关闭「用户列表」标签 → 联动移除「用户信息单据」标签并激活主页(BR5);关闭「用户信息单据」→ 回到「用户列表」标签。标签激活状态与当前路由保持同步(点击标签 = `navigate` 到对应路由)。 | ||
| 143 | +5. **全部导航总览 overlay(BR7,复刻 `#nav-overlay` + `#nav-toggle`)**:点击「全部导航」按钮切换 overlay 显隐(`navOverlayOpen`),按钮高亮。overlay 左列 20 个一级模块(`navConfig.ts` 静态,「系统设置」默认 active)、右侧 7 列分组(`期初设置 / 用户管理 / 系统参数 / 计算方案 / 日志 / 开发平台 / API对接管理`,复刻原型 `navCols`)。叶子项中仅「用户列表」有真实路由(点击 → 关 overlay + `navigate('/usr/users')`);其余为占位项(点击关 overlay,不跳转或 `message.info("功能开发中")`,§ 8 D4)。「系统功能模块设置」「用户列表」带 ★ 标记(原型 `it.star`)。支持点击遮罩 / Esc 关闭。 | ||
| 144 | +6. **主页 KPI 看板(BR11)**:复刻原型 `.kpi-head`(标题 + 今日未处理/未清总数统计 + AI 助手按钮)+ `.three-col`(左角色/流程树 + 右 KPI 合并网格)+ `.common-ops`(右侧常用操作)。数据来自 `dashboardData.ts` 内置静态 demo(角色/流程分组、KPI 行含跨行合并、统计汇总数;§ 8 D2 登记其静态来源)。KPI 网格含「导航类型 / 角色 / 子流程」列的纵向跨行合并(原型用 CSS Grid `gridRow span` 模拟),复刻为 AntD `Table` 的 `rowSpan` 或等价 CSS Grid(§ 8 D5)。「KPI待处理事项 / KPI内容描述」为蓝色链接样式但**无明细页**(纯展示)。AI 助手按钮为占位(无后端,§ 8 D4)。 | ||
| 145 | +7. **退出登录(BR9)**:当前用户 `Dropdown` → 「退出登录」→ dispatch `authSlice.clearCredentials` + 删除 localStorage `xly_erp_token`(FE-01 锁定键)→ `message.success` → `navigate('/login', { replace:true })`。 | ||
| 146 | +8. **顶栏 / 页脚静态文案**:品牌 Logo(鹿角 SVG,复用 FE-01/原型 inline path)、搜索/通知/更多图标为**纯占位**(无交互后端,§ 8 D4);页脚版权 / 经营范围 / 备案号文本复刻原型 `footer.foot`。 | ||
| 147 | +9. **响应式**:目标为桌面端 ERP(CLAUDE.md「企业内部管理人员」),原型为定宽桌面布局(主网格 `1fr / 280px`、导航 7 列);不专门设计移动端断点,窄屏允许默认回流(§ 8 D8)。 | ||
| 148 | + | ||
| 149 | +--- | ||
| 150 | + | ||
| 151 | +## 7. Design Tokens 引用清单(`src/styles/tokens.css`,仅 `var(--color-*)`) | ||
| 152 | + | ||
| 153 | +> 约束:组件样式只用 `var(--color-*)`,禁止硬编码 hex/rgba;色值冲突时 `tokens.css` 优先于 `prototype/`(原型内联 `:root` 变量为 demo 私有,不作色值 SSoT)。AntD 主题色经 `ConfigProvider` 对齐 `--color-primary`。 | ||
| 154 | + | ||
| 155 | +| 用途 | Token | 备注 | | ||
| 156 | +|---|---|---| | ||
| 157 | +| 主操作 / 激活标签 / 「全部导航」激活态 / AI 按钮 / 链接强调 | `var(--color-primary)` | 对应原型 `.nav-btn.active` / `.tab.active` / `.ai-btn` 蓝;同时作为 AntD 主题 `colorPrimary` | | ||
| 158 | +| 页面 / 内容区基础背景 | `var(--color-bg-base)` | 对应原型 `.stage` / `.main-wrap` 浅灰底(原型 `#f3f4f6`,映射到基础底色 token) | | ||
| 159 | +| 面板 / 卡片 / 表格背景(白) | `var(--color-form-bg-edit)` | KPI 看板面板、常用操作卡、导航卡片白底(原型 `.panel` `#fff`) | | ||
| 160 | +| 表头背景(KPI 网格表头 / 流程树分组头) | `var(--color-table-header-bg)` | 对应原型 `.kpi-body .h` / `.header-bg` | | ||
| 161 | +| 表头文字 | `var(--color-table-header-fg)` | KPI 网格列头文字 | | ||
| 162 | +| 表格行文字 | `var(--color-table-row-fg)` | KPI 行 / 用户名等单元文字 | | ||
| 163 | +| 行 hover 背景 | `var(--color-table-row-bg-hover)` | KPI/树项 hover(原型 `.item:hover` / `tr:hover`,映射到行 hover token) | | ||
| 164 | +| 选中行 / 树节点选中背景 | `var(--color-table-row-bg-selected)` | 角色/流程树 `.item.active` 选中高亮 | | ||
| 165 | +| 通用文字 / 标题 | `var(--color-text)` | 「KPI监控」标题、统计文案、页脚正文 | | ||
| 166 | +| 次要文字 / 占位 / 页脚版权 | `var(--color-text-secondary)` | 统计副文案、占位图标、备案号 / 版权文本 | | ||
| 167 | +| 边框 / 分隔线 | `var(--color-border)` | 面板描边、KPI 网格线、标签分隔(原型 `.border` / `.kpi-body` 网格边) | | ||
| 168 | +| 异常 / 红色统计数字 / 错误提示 | `var(--color-error)` | 「今日未处理」红色数字、KPI 红色待处理数(原型 `.num-red` / `.danger`)、`message.error` / `message.warning` 强调 | | ||
| 169 | +| 成功提示(退出登录 / 操作成功反馈) | `var(--color-success)` | `message.success("已退出登录")` 等(显式登记以备自定义文案样式) | | ||
| 170 | + | ||
| 171 | +> 顶栏 `#topbar` 的深色底(原型 `--topbar:#1f1f23`)、导航总览 overlay 深色底(原型 `#2b3137`)、登录主视觉深蓝渐变(FE-01)等**品牌深色 / 装饰色**在 `tokens.css` 中**无对应语义 token**;本规格将顶栏与导航 overlay 的深色底作为**外壳局部装饰样式**保留在 `AppLayout` 的 scoped 样式里(不新增全局 token、不挪用语义 token,与 FE-01 § 7 D7 处理一致,见 § 8 决策 D9)。这些深色仅承载「外壳容器」视觉,不承载状态语义,不违反「语义色只用 token」约束。 | ||
| 172 | + | ||
| 173 | +--- | ||
| 174 | + | ||
| 175 | +## 8. 自主决策记录(decisions) | ||
| 176 | + | ||
| 177 | +| # | 问题 | 选择 | 依据 | 置信度 | | ||
| 178 | +|---|---|---|---|---| | ||
| 179 | +| D1 | 主页 KPI 看板 / 角色流程树 / 导航分组的数据来源(docs/05 无 KPI/dashboard/nav 端点) | 不调后端,全部用前端静态配置(`dashboardData.ts` / `navConfig.ts`)复刻原型 demo 数据 | docs/05 接口清单仅 4 个 USR 端点,无任何 KPI/导航/仪表盘端点;原型主页为纯静态 demo;硬约束禁止杜撰与现有约定冲突的「事实」(不得编造后端端点) | high | | ||
| 180 | +| D2 | KPI 统计数字 / 角色计数 / KPI 行的具体值如何确定 | 沿用原型内置 demo 数值(如今日未处理 37428、未清总数 56433、各部门计数、kpiRows 全量行),作为静态展示数据 | 原型 `prototype/erp.html` 内联 `kpiRows` / `.kpi-head` 统计 / 角色树计数即唯一数据来源;无后端来源可对接;保留原值即「最有依据的解读」 | high | | ||
| 181 | +| D3 | 标签栈 / 导航 overlay 开关 / 当前激活标签的状态放哪 | 优先用 `AppLayout` 本地 `useState`(标签栈 + overlay 开关),仅当需跨组件共享再抽 `uiSlice` | docs/04 § 2.2「跨页面共享的才进 store,避免把所有状态塞全局」;标签栈是外壳局部 UI 态,本地态更轻;登记以备实现期按需上提 | medium | | ||
| 182 | +| D4 | 占位入口(搜索/通知/更多/AI 助手/导航总览中无路由的项)点击行为 | 一律为视觉占位:无路由项点击关 overlay 不跳转,可选 `message.info("功能开发中")`;搜索/通知/AI 等图标按钮不绑业务动作 | 原型对应元素均无 `data-go`/无目标(纯 demo);docs/05 无对应端点;占位化是最小且不杜撰功能的处理 | high | | ||
| 183 | +| D5 | KPI 合并网格用 AntD Table 还是 CSS Grid 复刻 | 实现期二选一:默认 AntD `Table`(`rowSpan` 合并「导航类型/角色/子流程」列);若合并规则难以用 Table 表达则回退原型同款 CSS Grid `gridRow span` | `frontend.ui_lib=AntD 5`,优先组件库;原型已用 CSS Grid 合并验证可行,作为兜底;二者都满足布局语义,留实现期取舍并登记 | medium | | ||
| 184 | +| D6 | 退出登录是否调后端注销端点 | 纯前端注销:清 `authSlice` + 删 localStorage token + 跳 `/login`,不调后端 | docs/04 § 1.7 为无状态 JWT(无服务端会话需失效);docs/05 无注销端点;无状态认证下前端清 token 即注销,不杜撰端点 | high | | ||
| 185 | +| D7 | 未匹配路由(404)处理 | 受保护区内未匹配 → 重定向到主页 `/`;未登录态下未匹配 → 经守卫重定向 `/login` | 原型为固定几屏的单页 demo,无 404 设计;ERP 内部系统重定向主页是常见稳妥取舍;非验收硬项,故登记 | medium | | ||
| 186 | +| D8 | 窄屏 / 移动端适配 | 不专门设计移动断点,桌面定宽布局默认随容器回流 | 目标用户为「企业内部管理人员」桌面端 ERP(CLAUDE.md);原型为定宽桌面布局;移动适配非本 FE 验收项(与 FE-01 § 7 D4 处理一致) | medium | | ||
| 187 | +| D9 | 顶栏 / 导航 overlay 深色底色值来源(tokens.css 无对应品牌深色 token) | 作为外壳局部装饰样式保留在 `AppLayout` scoped 样式,不新增全局 token、不挪用语义 token;语义色(主操作/文字/边框/错误/成功)严格走 token | tokens.css 仅定义语义/状态色,无品牌深色顶栏 token;深色底纯装饰无状态语义,局部化最小侵入;与 FE-01 § 7 D7 一致 | medium | | ||
| 188 | +| D10 | 当前用户区 `sUserType` 显示文案 | 直接展示后端 `user.sUserType`(取值「超级管理员/普通用户」,已是中文),拼为 `sUserName(sUserType)` | REQ-USR-001/004 中 `sUserType` 取值即中文「普通用户/超级管理员」;无需再映射;原型展示「朱子纯(超级管理员)」同形 | high | | ||
| 189 | + | ||
| 190 | +> 本规格不含后端实现细节(认证比对 / 令牌签发 / 数据访问 / 库表迁移等均不在前端作用域),亦不杜撰任何 KPI / 导航后端端点;当前用户身份来自 FE-01 已落地的 `authSlice`,所有业务裁决在子功能(FE-03/FE-04)调用时由后端产生。 |
docs/superpowers/specs/2026-06-01-FE-03.md
0 → 100644
| 1 | +# FE-03 用户列表与查询 — 实现规格(前端) | ||
| 2 | + | ||
| 3 | +> 阶段:前端(frontend)。作用域限定 `frontend/` 下的页面 / 组件 / 路由 / store / api / 样式。 | ||
| 4 | +> SSoT 引用:需求卡片 `docs/01-需求清单/USR-用户管理/REQ-USR-003.md`;原型 `prototype/erp.html`(`<section id="screen-userlist">` 区域:`.toolbar` / `.filterbar` / `.table-shell .grid-table#user-table` / `.pager`,布局与交互权威);API 契约 `docs/05-API接口契约.md` § REQ-USR-003(`GET /api/usr/users`);技术规范 `docs/04-技术规范.md` § 零(技术栈)/ § 2.1 目录约定 / § 2.3 接口通信 / § 2.4 状态与错误 / § 3.2 分页查询;Design Tokens `src/styles/tokens.css`;承载外壳 FE-02 规格 `docs/superpowers/specs/2026-06-01-FE-02.md`(`AppLayout` + 标签栈 + `RequireAuth` 守卫);登录态/请求基建 FE-01 规格 `docs/superpowers/specs/2026-06-01-FE-01.md`(`authSlice` + `request.ts` + token 持久化键 `xly_erp_token`)。 | ||
| 5 | +> 本规格只消费已锁定事实。查询条件解析、文本模糊/精确匹配、分页越界回退、密码与敏感字段过滤等业务逻辑全部在后端(见 REQ-USR-003 后端规格),前端只负责采集筛选条件、发起分页查询、依据响应渲染表格 / 分页 / 空态 / 错误态。本 FE 为**只读查询**,不产生任何写副作用。 | ||
| 6 | + | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +## 1. 关联 REQ + 关联原型 | ||
| 10 | + | ||
| 11 | +| 维度 | 内容 | | ||
| 12 | +|---|---| | ||
| 13 | +| 业务功能 | FE-03 用户列表与查询(工具栏刷新/导出 + 筛选条件 + 用户表格 + 分页,对接 `GET /api/usr/users`) | | ||
| 14 | +| 关联 REQ | REQ-USR-003 查询用户(主)。输入表 1:查询字段(下拉单选,默认「用户名」)/ 匹配方式(下拉单选,默认「包含」)/ 查询值(手工输入,空为全部);输出表 1:序号 / 用户名 / 员工名 / 用户号 / 部门 / 用户类型 / 语言 / 作废 / 登录日期 / 制单人 / 制单日期 | | ||
| 15 | +| 关联原型 | `prototype/erp.html` → `<section id="screen-userlist">`:`.toolbar`(刷新 / 新增 / 导出Excel / 设置齿轮)、`.filterbar`(下拉 + 文本框 + 搜索 / 清空)、`.table-shell > table.grid-table#user-table`(表头 + `tbody#user-tbody`)、`.pager`(统计文案 + 上/下页 + 当前页 + 每页条数下拉) | | ||
| 16 | +| 路由 | `/usr/users`(React Router v6,受保护区子路由,由 FE-02 `AppLayout` + `RequireAuth` 包裹)。从 FE-02 主页「常用操作 > 用户列表」或导航总览「用户管理 > 用户列表 ★」进入并在顶栏打开「用户列表」标签(标签栈逻辑属 FE-02) | | ||
| 17 | +| 落地组件目录 | 页面 `frontend/src/pages/usr/UserList/`(`index.tsx` + 子组件 `UserToolbar.tsx` / `UserFilterBar.tsx` / `UserTable.tsx`);接口走 `frontend/src/api/usrApi.ts`(新增 `listUsers` 方法)+ `frontend/src/api/request.ts`(FE-01 已建);类型集中 `frontend/src/api/types.ts`(`UserVO` / `PageResult<T>` / `UserListQuery`);列表查询态就近用页面 hook(不进全局 store,见 § 8 决策 D6) | | ||
| 18 | + | ||
| 19 | +> 原型 `#screen-userlist` 为纯静态 HTML + 内联 demo 脚本:`users` 数组直接渲染 `tbody`、`filterbar` 的下拉/输入无真实查询、`.pager` 文案为写死 demo(「共37个单据 共37条记录」「10000 条/页」)、行 `dblclick` 调 `goTo('userdetail')` 切到用户单据屏、工具栏「新增」`data-add-user` 切到新增单据。本规格按 React + AntD 5 复刻其**布局与交互语义**,但表格数据、筛选、分页改为真实对接 `GET /api/usr/users`;「新增」「行双击进单据」为跳转到 FE-04 的导航动作(目标页内容属 FE-04,本页只发起 `navigate`)。 | ||
| 20 | + | ||
| 21 | +--- | ||
| 22 | + | ||
| 23 | +## 2. 组件树(按区域分块,推导自 prototype DOM) | ||
| 24 | + | ||
| 25 | +页面根 `UserListPage`(路由 `/usr/users` 挂载,渲染于 FE-02 `AppLayout` 的 `<Outlet/>` 内),纵向结构对应原型 `#screen-userlist`(工具栏 / 筛选栏 / 表格壳 / 分页栏): | ||
| 26 | + | ||
| 27 | +``` | ||
| 28 | +UserListPage(页面容器,对应 #screen-userlist:纵向布局,内容区填充外壳 Outlet) | ||
| 29 | +├── UserToolbar(对应 .toolbar:深色工具条,左侧动作按钮 + 右侧设置齿轮占位) | ||
| 30 | +│ ├── BtnRefresh(对应 .toolbar 刷新:重新拉取当前查询条件 + 当前页,见 § 5 BR8) | ||
| 31 | +│ ├── BtnAdd(对应 .tb-btn#btn-add[data-add-user]:跳转 FE-04 新增用户 `/usr/users/new`;本页仅 navigate) | ||
| 32 | +│ ├── BtnExportExcel(对应 .toolbar「导出Excel」:导出当前查询结果,见 § 5 BR9 / § 8 决策 D5) | ||
| 33 | +│ └── GearSetting(对应 .toolbar .gear「⚙」:列设置占位,无后端,见 § 8 决策 D7) | ||
| 34 | +├── UserFilterBar(对应 .filterbar:查询条件行) | ||
| 35 | +│ ├── ScopeSelect(对应首个 <select>「全部用户」:用户范围下拉,原型 demo 项,见 § 8 决策 D2 —— 默认不映射后端参数) | ||
| 36 | +│ ├── QueryFieldSelect(对应第 2 个 <select>「用户名」:查询字段下拉单选,options=用户名/员工名/用户号/部门/用户类型/作废/登录日期/制单人,默认「用户名」→ 提交 queryField) | ||
| 37 | +│ ├── MatchTypeSelect(对应第 3 个 <select>「包含」:匹配方式下拉单选,options=包含/不包含/等于,默认「包含」→ 提交 matchType) | ||
| 38 | +│ ├── QueryValueInput(对应 .filterbar <input>:查询值文本框,空为查询全部 → 提交 queryValue) | ||
| 39 | +│ ├── FilterMoreToggle(对应 .filterbar .down「▾」:更多条件占位,见 § 8 决策 D3) | ||
| 40 | +│ ├── BtnSearch(对应 .filterbar .btn「搜索」:以当前条件回到第 1 页发起查询,见 § 5 BR7) | ||
| 41 | +│ └── BtnClear(对应 .filterbar .btn.ghost「⊗ 清空」:重置筛选为默认值并回到第 1 页全量查询,见 § 5 BR10) | ||
| 42 | +├── UserTable(对应 .table-shell > table.grid-table#user-table:可横向滚动用户表格) | ||
| 43 | +│ ├── RadioColumn(对应 thead 首列空 + tbody .radio-cell .radio-dot:行选择单选标记,见 § 8 决策 D8) | ||
| 44 | +│ ├── 列:序号 / 用户名 / 员工名 / 用户号 / 部门 / 用户类型 / 语言 / 作废 / 登录日期 / 制单人 / 制单日期(对应 REQ 输出表 1 + 原型 thead) | ||
| 45 | +│ └── Row[](对应 tbody#user-tbody 各 <tr>;双击行 → 跳转 FE-04 修改单据 `/usr/users/:id`,见 § 5 BR12) | ||
| 46 | +└── UserPager(对应 .pager:右对齐分页条) | ||
| 47 | + ├── PageSummary(对应「共 N 条记录」统计文案,来源 PageResult.total) | ||
| 48 | + ├── Pager(对应 ‹ / 当前页 / › :上一页 / 当前页 / 下一页) | ||
| 49 | + └── PageSizeSelect(对应 .pager <select>「条/页」:每页条数下拉,见 § 8 决策 D4) | ||
| 50 | +``` | ||
| 51 | + | ||
| 52 | +- 控件选型(依据 `docs/04 § 零` `frontend.ui_lib = Ant Design 5.x`): | ||
| 53 | + - 工具栏 → 自定义深色 `div` 条 + AntD `Button`(`type="text"`,白字图标按钮,复刻原型 `.tb-btn`),「刷新」配 `ReloadOutlined`、「新增」`PlusCircleOutlined`、「导出Excel」`FileExcelOutlined`、设置 `SettingOutlined`。 | ||
| 54 | + - 筛选栏 → AntD `Select`(查询字段 / 匹配方式 / 用户范围,单选)+ `Input`(查询值,支持回车触发搜索 `onPressEnter`)+ `Button`(搜索 `type="primary"` 配 `SearchOutlined` / 清空 `type="default"`)。可用 AntD `Form` 承载(`layout="inline"`),亦可受控本地态,取舍登记 § 8 D6。 | ||
| 55 | + - 用户表格 → AntD `Table`(`rowKey="id"`,`columns` 对应输出列,服务端分页 `pagination` 受控;`loading` 绑加载态;`scroll={{ x }}` 复刻原型横向滚动 `white-space:nowrap`;行 `onRow.onDoubleClick` → 跳 FE-04)。「作废」列用 AntD `Tag` 或只读 `Checkbox`(`iIsVoid` 0/1 → 否/是,复刻原型 `tbody` 复选框单元,本页只读不可勾,见 § 5 BR6)。 | ||
| 56 | + - 分页 → 用 AntD `Table` 内置 `pagination`(受控 `current`/`pageSize`/`total`/`showSizeChanger`/`showTotal`),复刻原型 `.pager`「共 N 条记录 + 上/下页 + 当前页 + 每页条数」;不另造分页组件。 | ||
| 57 | + - 行选择标记 → AntD `Table` 的 `rowSelection={{ type:'radio' }}`(单选,复刻原型 `.radio-dot`),选择仅用于「双击/选中后进单据」语义,不参与查询(见 § 8 D8)。 | ||
| 58 | +- 页面在受保护区内渲染(无 token 由 FE-02 `RequireAuth` 拦截重定向,本页不重复守卫)。顶栏 / 标签栈属 FE-02,本页只提供内容区。 | ||
| 59 | + | ||
| 60 | +--- | ||
| 61 | + | ||
| 62 | +## 3. 页面状态机(≥5 态) | ||
| 63 | + | ||
| 64 | +列表查询态以「首次加载 / 加载中 / 正常有数据 / 空结果 / 错误 / 导出中」表达;查询参数(queryField/matchType/queryValue/pageNum/pageSize)由页面 hook 持有,任一变更触发取数: | ||
| 65 | + | ||
| 66 | +| 状态 | 触发时机 | UI 表现 | | ||
| 67 | +|---|---|---| | ||
| 68 | +| `idle/initialLoading`(首次加载 / loading) | 页面挂载即以默认条件(queryField=用户名、matchType=包含、queryValue 空、pageNum=1、pageSize=见 § 8 D4)调 `GET /api/usr/users`(REQ 输入表「预加载=页面加载时」) | 表格区 AntD `Table` 置 `loading`(骨架 / `Spin` 遮罩),分页与工具栏可见但表体显加载占位;筛选栏可操作 | | ||
| 69 | +| `loading`(查询/翻页/刷新进行中) | 点击「搜索」/「清空」/「刷新」/ 切页 / 改每页条数后重新取数 | `Table` `loading=true`,保留上次行直至新数据返回(避免闪烁);「搜索」「刷新」按钮置 `loading`/禁用防重复提交 | | ||
| 70 | +| `success`(正常有数据) | 接口返回 `code=0` 且 `data.records` 非空 | 渲染表格行(序号按当前页 `(pageNum-1)*pageSize + index + 1` 生成,见 § 5 BR1);分页显示真实 `total`/`pageNum`/`pageSize`;统计文案「共 {total} 条记录」 | | ||
| 71 | +| `empty`(无匹配结果) | 接口返回 `code=0` 且 `data.records` 为空数组(含「条件无匹配」与「全量为空」) | `Table` 渲染 AntD `Empty`「暂无匹配的用户」占位,**不报错**(REQ 验收:无匹配返回空列表而非报错);分页 `total=0`,统计「共 0 条记录」 | | ||
| 72 | +| `error`(查询失败) | 接口返回非 0 `code`(40001/42201)或网络/超时/5xx | 表格区显错误占位(AntD `Result`/`Empty` + 「加载失败,点击重试」)+ `message.error` 文案(见 § 4);保留筛选条件可重试;被动 401 由 `request.ts` 拦截器统一跳 `/login`(docs/04 § 2.4,不在本页处理) | | ||
| 73 | +| `exporting`(导出 Excel 进行中) | 点击「导出Excel」 | 「导出Excel」按钮置 `loading` 并禁用;完成后 `message.success("导出成功")` / 失败 `message.error("导出失败")`(导出实现见 § 8 D5) | | ||
| 74 | + | ||
| 75 | +> 状态以页面本地 hook(`useState`/自定义 `useUserList`)表达:`{ list, total, loading, error, query:{queryField,matchType,queryValue,pageNum,pageSize}, exporting }`;不进全局 `store`(列表数据为页面就近态,docs/04 § 2.2「跨页面共享的才进 store」,见 § 8 D6)。`authResolving`/`unauthenticated` 由 FE-02 守卫承担,本页不重复建态。 | ||
| 76 | + | ||
| 77 | +--- | ||
| 78 | + | ||
| 79 | +## 4. 消费的后端端点(对齐 docs/05) | ||
| 80 | + | ||
| 81 | +| 端点 | 方法 | 触发时机 | 请求参数 | 成功响应 | 失败处理 | | ||
| 82 | +|---|---|---|---|---|---| | ||
| 83 | +| `/api/usr/users` | GET | 页面挂载(默认条件)/ 点击搜索 / 清空 / 刷新 / 切页 / 改每页条数 | query:`{ queryField?(用户名/员工名/用户号/部门/用户类型/作废/登录日期/制单人,默认用户名), matchType?(包含/不包含/等于,默认包含), queryValue?(空为全部), pageNum(默认1), pageSize(默认10,最大100) }`(对齐 docs/05 § REQ-USR-003) | `Result<PageResult<UserVO>>`,`code=0`;`PageResult={ records:UserVO[], total, pageNum, pageSize }`;`UserVO={ id, sUserName, 员工名, sUserNo, 部门, sUserType, sLanguage, iIsVoid, tLastLoginDate, sCreator, tCreateDate }`(密码与敏感字段后端不返回) | 非 0 `code` 弹 `message.error`(见错误码表);表格回退到空/错误占位 + 重试 | | ||
| 84 | + | ||
| 85 | +请求 / 响应约定(依据 `docs/04 § 2.3 / § 2.4 / § 3.2`): | ||
| 86 | +- 统一走 `frontend/src/api/request.ts` 的 Axios 实例(FE-01 已建:`baseURL=/api`,开发期经 Vite proxy 转发到后端 `http://localhost:5172`,端口取 `config-vars.yaml backend.http_port=5172`;请求拦截器注入 `Authorization: Bearer <token>`,token 取 localStorage 键 `xly_erp_token`)。 | ||
| 87 | +- 响应拦截器统一拆 `Result`:`code=0` 取 `data`;非 0 `code` 抛业务错误并弹 `message.error`;`401` 统一跳 `/login`(docs/04 § 2.4)。本页方法集中在 `usrApi.ts` 的 `listUsers(query): Promise<PageResult<UserVO>>`,页面不直接散用 axios(docs/04 § 2.3)。 | ||
| 88 | +- 分页对齐 docs/04 § 3.2:`pageNum`/`pageSize` 传后端,回显 `PageResult` 的 `total`/`pageNum`/`pageSize`;文本条件模糊匹配、枚举/外键条件精确匹配、空条件全量分页均由后端裁决(前端不预判匹配语义,原样传 `matchType`)。 | ||
| 89 | + | ||
| 90 | +#### 错误码表(对齐 docs/05 § REQ-USR-003) | ||
| 91 | + | ||
| 92 | +| code | 含义 | 前端文案 | 处理 | | ||
| 93 | +|---|---|---|---| | ||
| 94 | +| `0` | 成功 | —(渲染结果或空态) | 正常渲染 | | ||
| 95 | +| `42201` | 分页参数非法(pageNum<1 或 pageSize 超上限 100) | 「分页参数有误,已重置为第 1 页」 | `message.warning` + 前端把分页重置为合法值(pageNum=1,pageSize 收敛至 ≤100)后重查;正常情况下前端分页组件已约束,此为兜底 | | ||
| 96 | +| `40001` | 查询参数校验失败 | 「查询条件有误,请检查后重试」 | `message.error` + 保留条件不自动重查 | | ||
| 97 | +| 网络/超时/5xx | 请求异常 | 「加载失败,请稍后重试」 | 响应拦截器兜底 `message.error` + 表格错误占位「点击重试」 | | ||
| 98 | +| `401` | 登录失效(被动) | 「登录已失效,请重新登录」 | 由 `request.ts` 拦截器统一跳 `/login`(docs/04 § 2.4),本页不单独处理 | | ||
| 99 | + | ||
| 100 | +--- | ||
| 101 | + | ||
| 102 | +## 5. 业务规则前端复刻清单(逐条) | ||
| 103 | + | ||
| 104 | +| # | 规则 | 触发时机 | 前端报错 / 反馈文案 | 来源 | | ||
| 105 | +|---|---|---|---|---| | ||
| 106 | +| BR1 | 「序号」列为系统生成的连续行号,按当前分页计算 `(pageNum-1)*pageSize + 行内索引 + 1` | 每次渲染表格行 | —(纯展示,无报错) | REQ-USR-003 输出表 1「序号 = 系统生成」;原型 `renderUsers` 用 `i+1` | | ||
| 107 | +| BR2 | 查询字段默认「用户名」、匹配方式默认「包含」、查询值默认空 | 页面挂载初始化筛选栏 | —(默认值预填,对应原型下拉默认选中项) | REQ-USR-003 输入表 1「默认值=用户名 / 包含」「预加载=页面加载时」 | | ||
| 108 | +| BR3 | 查询值为空时按「选择全部」处理(不传或传空 `queryValue`,返回全量分页) | 搜索 / 清空 / 默认加载时 queryValue 为空 | —(无报错,全量分页) | REQ-USR-003 输入表 1「查询值…空为选择全部」+ docs/04 § 3.2「空条件返回全量分页」 | | ||
| 109 | +| BR4 | 查询字段与匹配方式取值受限于固定枚举(字段:用户名/员工名/用户号/部门/用户类型/作废/登录日期/制单人;匹配:包含/不包含/等于) | 渲染下拉 options | —(`Select` 仅提供合法选项,无自由输入) | REQ-USR-003 输入表 1「显示来源」枚举列表 | | ||
| 110 | +| BR5 | 查询为只读,不产生写副作用 | 任何查询 / 翻页 / 刷新 | —(仅 GET,不改任何数据) | REQ-USR-003 边界「查询为只读,不产生写副作用」 | | ||
| 111 | +| BR6 | 「作废」列只读展示 `iIsVoid`(0=否 / 1=是),不可在列表中编辑勾选 | 渲染「作废」单元 | —(只读 `Tag`/禁用 `Checkbox`,点击无效) | REQ-USR-003 输出表 1「作废=布尔」;原型 `tbody` 复选框为 demo 展示(本页只读) | | ||
| 112 | +| BR7 | 点击「搜索」以当前筛选条件回到第 1 页发起查询 | 点击「搜索」或查询值框回车 | —(成功渲染 / 失败按 § 4 文案) | 原型 `.filterbar .btn`「搜索」;docs/04 § 3.2 分页查询 | | ||
| 113 | +| BR8 | 点击「刷新」以**当前已生效的查询条件 + 当前页**重新取数(不重置条件,不回第 1 页) | 点击工具栏「刷新」 | —(刷新成功 / 失败按 § 4) | 原型 `.toolbar` 刷新图标语义(重载当前视图) | | ||
| 114 | +| BR9 | 「导出Excel」导出**当前查询条件命中的结果**(非仅当前页),导出过程禁用按钮 | 点击「导出Excel」 | 成功 `message.success("导出成功")` / 失败 `message.error("导出失败")`(实现见 § 8 D5) | 原型 `.toolbar`「导出Excel」 | | ||
| 115 | +| BR10 | 点击「清空」重置查询字段=用户名、匹配方式=包含、查询值=空、范围=全部,回到第 1 页全量查询 | 点击「清空」 | —(重置后自动重查全量) | 原型 `.filterbar .btn.ghost`「⊗ 清空」 | | ||
| 116 | +| BR11 | 分页参数变更(切页 / 改每页条数)即重新取数;改每页条数回到第 1 页 | 点击上/下页、选每页条数 | —(成功渲染) | docs/04 § 3.2 分页;AntD `Table` 受控分页惯例 | | ||
| 117 | +| BR12 | 双击表格行 → 跳转 FE-04 修改用户单据(`/usr/users/:id`,携该行 `id`) | 双击任一数据行 | —(`navigate('/usr/users/'+row.id)`,目标内容属 FE-04) | 原型 `tr.addEventListener('dblclick', ()=>goTo('userdetail'))` | | ||
| 118 | +| BR13 | 「新增」→ 跳转 FE-04 新增用户单据(`/usr/users/new`) | 点击工具栏「新增」 | —(`navigate('/usr/users/new')`,目标内容属 FE-04) | 原型 `.tb-btn#btn-add[data-add-user]` → `setUserDetailMode('new')` + `openTab('userdetail')` | | ||
| 119 | +| BR14 | 无匹配结果时展示空态而非错误 | 接口返回空 `records` | AntD `Empty`「暂无匹配的用户」(无 `message.error`) | REQ-USR-003 验收「无匹配时返回空列表而非报错」 | | ||
| 120 | +| BR15 | 分页越界由后端回退到最后一页,前端按响应回显实际 `pageNum` | 请求的 `pageNum` 超出总页数 | —(前端以响应 `PageResult.pageNum` 同步分页当前页,不报错) | REQ-USR-003 验收「分页参数越界时返回最后一页」(前端信任后端回显) | | ||
| 121 | + | ||
| 122 | +> 本页只做**条件采集 + 分页查询 + 只读展示 + 导航跳转**;查询匹配语义、敏感字段过滤、越界回退等真伪裁决全部由后端在 `GET /api/usr/users` 内完成,前端不复制后端逻辑、不预判结果、不杜撰端点。 | ||
| 123 | + | ||
| 124 | +--- | ||
| 125 | + | ||
| 126 | +## 6. 列定义与字段映射(对齐 REQ 输出表 1 + UserVO) | ||
| 127 | + | ||
| 128 | +| 列(中文表头,对应原型 thead / REQ 输出表 1) | 数据字段(UserVO) | 渲染说明 | | ||
| 129 | +|---|---|---| | ||
| 130 | +| 序号 | —(前端生成) | `(pageNum-1)*pageSize + index + 1`(BR1) | | ||
| 131 | +| 用户名 | `sUserName` | 文本,可作为「双击进单据」主标识 | | ||
| 132 | +| 员工名 | `员工名`(来自职员表关联) | 文本,可空(原型有空值行如 `admin`/`李丹`) | | ||
| 133 | +| 用户号 | `sUserNo` | 文本,可空 | | ||
| 134 | +| 部门 | `部门`(来自职员表关联) | 文本,可空 | | ||
| 135 | +| 用户类型 | `sUserType` | 文本(普通用户 / 超级管理员,已为中文) | | ||
| 136 | +| 语言 | `sLanguage` | 文本(中文 / 英文 / 繁体) | | ||
| 137 | +| 作废 | `iIsVoid` | 只读布尔:0→否、1→是(BR6,`Tag`/禁用 `Checkbox`) | | ||
| 138 | +| 登录日期 | `tLastLoginDate` | 日期时间文本,可空(原型有空值,如 `lzj` 行) | | ||
| 139 | +| 制单人 | `sCreator` | 文本 | | ||
| 140 | +| 制单日期 | `tCreateDate` | 日期时间文本 | | ||
| 141 | + | ||
| 142 | +> 列顺序与表头文案以原型 `#user-table` thead + REQ 输出表 1 为准(原型表头「作」为「作废」的窄列简写,本规格列名用「作废」全称)。`员工名`/`部门` 在 docs/05 `UserVO` 中以中文键名给出(后端关联职员表派生字段),前端类型按响应实际键名映射(见 § 8 D9)。 | ||
| 143 | + | ||
| 144 | +--- | ||
| 145 | + | ||
| 146 | +## 7. Design Tokens 引用清单(`src/styles/tokens.css`,仅 `var(--color-*)`) | ||
| 147 | + | ||
| 148 | +> 约束:组件样式只用 `var(--color-*)`,禁止硬编码 hex/rgba;色值冲突时 `tokens.css` 优先于 `prototype/`(原型内联 `:root` 变量为 demo 私有,不作色值 SSoT)。AntD 主题色经 FE-02/FE-01 已配置的 `ConfigProvider` 对齐 `--color-primary`。 | ||
| 149 | + | ||
| 150 | +| 用途 | Token | 备注 | | ||
| 151 | +|---|---|---| | ||
| 152 | +| 主操作(搜索按钮 / 当前页码高亮 / 链接强调) | `var(--color-primary)` | 对应原型 `.filterbar .btn` / `.pager .pgcur` 蓝;同时作为 AntD `colorPrimary` | | ||
| 153 | +| 页面 / 内容区基础背景 | `var(--color-bg-base)` | 内容区浅灰底(外壳 Outlet 内) | | ||
| 154 | +| 筛选栏 / 表格 / 分页栏背景(白) | `var(--color-form-bg-edit)` | 对应原型 `.filterbar` / `.grid-table` / `.pager` 白底(原型 `--panel` `#fff`) | | ||
| 155 | +| 表头背景 | `var(--color-table-header-bg)` | 对应原型 `.grid-table thead th` 表头底(原型 `--header-bg`) | | ||
| 156 | +| 表头文字 | `var(--color-table-header-fg)` | 列头文字 | | ||
| 157 | +| 表格行文字 | `var(--color-table-row-fg)` | 用户名 / 部门等单元文字 | | ||
| 158 | +| 行 hover 背景 | `var(--color-table-row-bg-hover)` | 行悬停高亮(对应原型 `.grid-table tbody tr:hover` 浅蓝,映射到行 hover token) | | ||
| 159 | +| 选中行背景 | `var(--color-table-row-bg-selected)` | 单选行选中高亮(`rowSelection` 选中行,对应原型 `.radio-dot` 选中语义) | | ||
| 160 | +| 表单输入框背景(查询值框 / 下拉) | `var(--color-form-bg-edit)` | 筛选栏 `Input`/`Select` 可编辑底(原型 `--field-bg`/白底映射到表单可编辑 token) | | ||
| 161 | +| 下拉项 hover 背景 | `var(--color-form-bg-hover)` | `Select` 选项悬停(tokens 注「仅下拉框使用」) | | ||
| 162 | +| 通用文字 / 统计文案 | `var(--color-text)` | 分页「共 N 条记录」、列内容文本 | | ||
| 163 | +| 次要文字 / 占位 / 空态提示 | `var(--color-text-secondary)` | 空态「暂无匹配的用户」、占位图标(齿轮 / 更多) | | ||
| 164 | +| 边框 / 分隔线 / 表格网格线 | `var(--color-border)` | 筛选栏下边线、表格单元边框、分页栏上边线(原型 `--border`) | | ||
| 165 | +| 错误 / 失败提示 | `var(--color-error)` | `message.error`(查询失败 / 导出失败)、错误占位强调(原型 `--danger`) | | ||
| 166 | +| 警告提示 | `var(--color-warning)` | `message.warning`(分页参数越界兜底 42201) | | ||
| 167 | +| 成功提示 | `var(--color-success)` | `message.success("导出成功")` | | ||
| 168 | + | ||
| 169 | +> 顶栏 `.toolbar` 的深色底(原型 `--toolbar-bg:#2c2f36`)在 `tokens.css` 中**无对应语义 token**(与 FE-02 顶栏深色处理一致);本规格将用户列表工具条深色底作为**页面局部装饰样式**保留在 `UserList` 的 scoped 样式里(不新增全局 token、不挪用语义 token,与 FE-02 § 8 D9 一致,见 § 8 决策 D10)。该深色仅承载工具条容器视觉,不承载状态语义,不违反「语义色只用 token」约束。 | ||
| 170 | + | ||
| 171 | +--- | ||
| 172 | + | ||
| 173 | +## 8. 自主决策记录(decisions) | ||
| 174 | + | ||
| 175 | +| # | 问题 | 选择 | 依据 | 置信度 | | ||
| 176 | +|---|---|---|---|---| | ||
| 177 | +| D1 | 列表数据 / 筛选 / 分页是真实对接后端还是沿用原型静态 `users` 数组 | 真实对接 `GET /api/usr/users`,原型 `users` demo 数组仅作列结构参照,不写入前端 | docs/05 § REQ-USR-003 明确定义该分页查询端点;REQ-USR-003 为真实查询功能;原型静态数据是 demo,不应固化到生产代码 | high | | ||
| 178 | +| D2 | 筛选栏首个下拉「全部用户」(原型 demo 项)映射到哪个后端参数 | 作为「用户范围」前端筛选项,默认「全部用户」时**不向后端传额外参数**(仅 queryField/matchType/queryValue + 分页参与查询);保留控件位以复刻原型布局 | docs/05 § REQ-USR-003 请求参数仅含 queryField/matchType/queryValue/pageNum/pageSize,无「范围」参数;原型「全部用户」为单项 demo(仅一个 `<option>`),无其他范围语义可锁;不杜撰后端不存在的参数 | high | | ||
| 179 | +| D3 | 筛选栏「▾」更多条件按钮(原型 `.filterbar .down`)行为 | 视觉占位(高级筛选预留),点击不触发额外后端参数;MVP 仅复刻表 1 的 3 个查询输入 | REQ-USR-003 输入表 1 仅定义查询字段/匹配方式/查询值 3 项;原型 `.down` 无对应交互脚本(纯 demo);占位化是最小且不杜撰功能的处理 | medium | | ||
| 180 | +| D4 | 每页条数 `pageSize` 默认值与可选项(原型写死「10000 条/页」) | 默认 `pageSize=10`(对齐 docs/05 默认),`showSizeChanger` 可选 `[10,20,50,100]`(上限对齐 docs/05 最大 100);不采用原型 demo 的 10000 | docs/05 § REQ-USR-003「pageSize 默认10,最大100」+ REQ 边界「单页最大条数受限(默认100)」;原型 10000 为 demo 写死值,超出后端上限 100,不可沿用 | high | | ||
| 181 | +| D5 | 「导出Excel」如何实现(docs/05 无导出端点) | MVP 阶段前端导出:拉取当前查询条件命中的结果(按 `total` 在上限内一次或分批取),用前端库(如 `xlsx`/SheetJS)生成 .xlsx 下载;不杜撰后端导出端点 | docs/05 接口清单无 `/export` 端点,不得编造后端端点(硬约束);原型「导出Excel」为工具栏动作但无后端脚本;前端导出当前结果集是不依赖新端点的可行实现;若结果量超单次查询上限,按分页循环取(受 pageSize≤100 约束)。实现期如后端补导出端点可切换 | medium | | ||
| 182 | +| D6 | 列表查询态放页面本地 hook 还是全局 store | 页面本地 hook(`useUserList`,含 list/total/loading/error/query),不进全局 `store` | docs/04 § 2.2「服务端数据优先就近在页面用 hooks 拉取,跨页面共享的才进 store」;列表数据仅本页用,无跨页共享需求 | high | | ||
| 183 | +| D7 | 工具栏设置齿轮「⚙」(原型 `.gear`)行为 | 视觉占位(列显隐/偏好设置预留),无后端动作 | 原型 `.gear` 无交互脚本(demo);docs/05 无对应端点;占位化最小侵入 | medium | | ||
| 184 | +| D8 | 表格行选择标记(原型 `.radio-dot` 单选圆点)是否参与查询 | 用 AntD `rowSelection={{type:'radio'}}` 复刻单选语义,仅用于「选中行 → 可进单据」交互,不参与查询条件、不向后端传选中项 | 原型行选择为单选圆点(无多选/批量动作脚本);REQ-USR-003 查询参数无「选中行」语义;选择仅服务于进入 FE-04 单据,不污染查询 | medium | | ||
| 185 | +| D9 | `UserVO` 中 `员工名`/`部门` 为中文键名,前端类型如何映射 | 前端 `UserVO` 类型按 docs/05 响应实际键名定义(`员工名`/`部门` 作为对象键,或在 api 层做一次到 `employeeName`/`departmentName` 的别名映射后供组件用);列渲染以 docs/05 给定键名为准 | docs/05 § REQ-USR-003 `UserVO` 显式列出中文键名(后端关联职员表派生);前端不擅自改后端响应键名,按契约消费;是否做内部别名属实现细节,列渲染语义不变 | medium | | ||
| 186 | +| D10 | 工具栏深色底色值来源(tokens.css 无对应工具条深色 token) | 作为页面局部装饰样式保留在 `UserList` scoped 样式,不新增全局 token、不挪用语义 token;语义色(主操作/文字/边框/错误等)严格走 token | tokens.css 仅定义语义/状态色,无工具条品牌深色 token;深色底纯装饰无状态语义,局部化最小侵入;与 FE-02 § 8 D9 处理一致 | medium | | ||
| 187 | + | ||
| 188 | +> 本规格不含后端实现细节(查询解析 / 匹配语义 / 敏感字段过滤 / 越界回退 / 数据访问 / 库表迁移等均不在前端作用域),亦不杜撰任何后端端点(导出按 D5 前端实现);登录态 / 请求基建 / 路由守卫复用 FE-01 + FE-02 已落地资产,所有查询真伪由后端在 `GET /api/usr/users` 调用时裁决。 |
docs/superpowers/specs/2026-06-01-FE-04.md
0 → 100644
| 1 | +# FE-04 用户信息单据 — 实现规格(前端) | ||
| 2 | + | ||
| 3 | +> 阶段:前端(frontend)。作用域限定 `frontend/` 下的页面 / 组件 / 路由 / store / api / 样式。 | ||
| 4 | +> SSoT 引用:需求卡片 `docs/01-需求清单/USR-用户管理/REQ-USR-001.md`(增加用户)+ `docs/01-需求清单/USR-用户管理/REQ-USR-002.md`(修改用户);原型 `prototype/erp.html`(`<section id="screen-userdetail">` 区域:`.toolbar` / `.form-grid` / `.tabs-row` / `.perm-list`,布局与交互权威,含 `setUserDetailMode('new')` 新增/编辑切换脚本);API 契约 `docs/05-API接口契约.md` § REQ-USR-001(`POST /api/usr/users`)+ § REQ-USR-002(`PUT /api/usr/users/{id}`)+ § REQ-USR-003(`GET /api/usr/users`,编辑预填复用,见 § 8 D4);技术规范 `docs/04-技术规范.md` § 零(技术栈)/ § 2.1 目录约定 / § 2.2 状态管理 / § 2.3 请求封装 / § 2.4 错误处理;Design Tokens `src/styles/tokens.css`;承载外壳 FE-02 规格 `docs/superpowers/specs/2026-06-01-FE-02.md`(`AppLayout` + 标签栈「用户信息单据」+ `RequireAuth` 守卫 + 路由 `/usr/users/new` 与 `/usr/users/:id`);列表来源 FE-03 规格 `docs/superpowers/specs/2026-06-01-FE-03.md`(「新增」按钮 → `/usr/users/new`、双击行 → `/usr/users/:id`);登录态/请求基建 FE-01 规格 `docs/superpowers/specs/2026-06-01-FE-01.md`(`authSlice` + `request.ts` + token 持久化键 `xly_erp_token`,及「支撑读端点供下拉预加载」先例 `GET /api/usr/companies`)。 | ||
| 5 | + | ||
| 6 | +> 本规格只消费已锁定事实。用户名全局唯一校验、用户名格式(3-20 位字母数字下划线)、密码 BCrypt 初始化(默认 666666)、权限组授权多对多写入与全量覆盖、职员/权限 id 存在性校验、管理员权限判定、审计字段(制单人/创建时间)系统生成等业务真伪裁决全部在后端(见 REQ-USR-001 / REQ-USR-002 后端规格)。前端只负责:按新增/编辑模式渲染单据表单、预填默认值或原值、采集字段、做轻量必填/格式前置校验(减少无效请求)、提交后按响应码反馈、成功后回流列表并刷新。本 FE 同时承载**新增**(`POST`)与**修改**(`PUT`)两种写场景,共用同一单据组件按 `mode` 分支。 | ||
| 7 | + | ||
| 8 | +--- | ||
| 9 | + | ||
| 10 | +## 1. 关联 REQ + 关联原型 | ||
| 11 | + | ||
| 12 | +| 维度 | 内容 | | ||
| 13 | +|---|---| | ||
| 14 | +| 业务功能 | FE-04 用户信息单据(新增/修改用户表单 + 权限组勾选,对接 `POST /api/usr/users` 与 `PUT /api/usr/users/{id}`) | | ||
| 15 | +| 关联 REQ(增) | REQ-USR-001 增加用户。表 1 输入字段:创建时间(系统生成/只读,预加载=页面加载、默认当前日期)、制单人(系统生成/只读,默认当前登录用户)、员工名(下拉单选,来源职员表,可选)、用户号(手工输入,必填,关联职员后自动带出)、用户名(手工输入,必填,关联职员后自动带出)、类型(下拉单选,普通用户/超级管理员,默认普通用户)、语言(下拉单选,中文/英文/繁体)、单据修改权限(复选框,默认否)、密码(系统生成不显示,默认 666666)。表 2 权限组:复选框 + 权限分类(页面加载预取)。输出表 1:用户号。 | | ||
| 16 | +| 关联 REQ(改) | REQ-USR-002 修改用户。表 1 字段同上但「预加载=页面加载时」「默认值=原值」(编辑预填);用户名作为唯一标识不可修改;密码不在本接口修改。表 2 权限组:复选框预加载原值(已授权项勾选)。输出表 1:用户 id。 | | ||
| 17 | +| 关联原型 | `prototype/erp.html` → `<section id="screen-userdetail">`:`.toolbar`(新增/修改/删除/保存/取消/功能/作废/重置密码/取消作废 + 设置齿轮)、`.form-grid`(3 列表单网格:创建时间只读 / 制单人只读 / 员工名下拉 / 用户名输入 / 类型下拉 / 语言下拉 / 用户号输入 / 单据修改权限复选框)、`.tabs-row`(权限页签:权限组 / 客户查看权限 / 供应商查看权限 / 人员查看权限 / 工序查看权限 / 司机查看权限)、`.perm-list`(权限分类列表,每行复选框 + 权限分类名 + 表头排序图标);脚本 `setUserDetailMode('new')` 在新增态清空各字段、制单人显示「保存后自动生成」、清除权限勾选。 | | ||
| 18 | +| 路由 | 新增 `/usr/users/new`;修改 `/usr/users/:id`(React Router v6,受保护区子路由,由 FE-02 `AppLayout` + `RequireAuth` 包裹,挂载于「用户信息单据」标签的 `<Outlet/>`)。入口:FE-03 工具栏「新增」→ `navigate('/usr/users/new')`;FE-03 双击行 → `navigate('/usr/users/'+row.id)`;FE-02 导航/常用操作的「新增用户」入口 → `/usr/users/new`。 | | ||
| 19 | +| 落地组件目录 | 页面 `frontend/src/pages/usr/UserDetail/`(`index.tsx` 单据容器 + 子组件 `UserDetailToolbar.tsx` / `UserBasicForm.tsx`(表单网格)/ `PermissionTabs.tsx`(权限页签条)/ `PermissionGroupList.tsx`(权限分类勾选列表));接口走 `frontend/src/api/usrApi.ts`(新增 `createUser` / `updateUser` / `getUserDetail` / `listEmployees` / `listPermissions` 方法)+ `frontend/src/api/request.ts`(FE-01 已建);类型集中 `frontend/src/api/types.ts`(`UserCreateReq` / `UserUpdateReq` / `EmployeeOption` / `PermissionItem` / 复用 `UserVO`);单据态就近用页面 hook(不进全局 store,见 § 8 D7)。 | | ||
| 20 | + | ||
| 21 | +> 原型 `#screen-userdetail` 为纯静态 HTML + 内联 demo 脚本:`.form-grid` 各字段值写死(`f-ctime`/`f-creator`/`f-empname`/`f-username`/`f-userno`/`f-type`/`f-lang`),`setUserDetailMode('new')` 仅做 DOM 文本清空,`.perm-list` 由静态 `perms` 数组渲染、复选框无真实勾选态,工具栏按钮(保存/取消等)无后端动作。本规格按 React + AntD 5 复刻其**布局与交互语义**,但表单数据、下拉数据源、权限勾选、保存提交改为真实对接后端。原型 `.tabs-row` 中除「权限组」外的 5 个查看权限页签(客户/供应商/人员/工序/司机)在 docs/01 REQ-USR-001/002 卡片与 docs/05 契约中**均无对应字段或端点**,按占位处理(见 § 8 D9);工具栏「删除/作废/重置密码/取消作废/功能」同样无对应 REQ/端点,按占位处理(见 § 8 D8)。 | ||
| 22 | + | ||
| 23 | +--- | ||
| 24 | + | ||
| 25 | +## 2. 组件树(按区域分块,推导自 prototype DOM) | ||
| 26 | + | ||
| 27 | +页面根 `UserDetailPage`(路由 `/usr/users/new` 与 `/usr/users/:id` 共用,渲染于 FE-02 `AppLayout` 的 `<Outlet/>` 内),纵向结构对应原型 `#screen-userdetail`(工具栏 / 表单网格 / 权限页签条 / 权限分类列表): | ||
| 28 | + | ||
| 29 | +``` | ||
| 30 | +UserDetailPage(单据容器,对应 #screen-userdetail;按路由判定 mode:有 :id → 'edit',/new → 'create') | ||
| 31 | +├── UserDetailToolbar(对应 .toolbar:深色工具条) | ||
| 32 | +│ ├── BtnSave(对应「保存」:校验通过后提交 create/update,见 § 5 BR12) | ||
| 33 | +│ ├── BtnCancel(对应「取消」:放弃编辑回到列表 /usr/users,见 § 5 BR13) | ||
| 34 | +│ ├── BtnNew(对应「新增」:在单据内切到 create 模式 / 跳 /usr/users/new,见 § 5 BR14) | ||
| 35 | +│ ├── BtnDelete / BtnVoid / BtnResetPwd / BtnUnvoid / BtnFunc(对应「删除/作废/重置密码/取消作废/功能」:占位,无后端端点,见 § 8 D8) | ||
| 36 | +│ └── GearSetting(对应 .gear「⚙」:偏好设置占位,见 § 8 D8) | ||
| 37 | +├── UserBasicForm(对应 .form-grid:3 列表单网格,AntD Form) | ||
| 38 | +│ ├── FieldCreateTime(对应 #f-ctime .field.readonly:创建时间,只读;create 态空/「保存后自动生成」,edit 态回填原值,见 § 5 BR1) | ||
| 39 | +│ ├── FieldCreator(对应 #f-creator .field.readonly:制单人,只读;create 态「保存后自动生成」,edit 态回填原值 sCreator,见 § 5 BR2) | ||
| 40 | +│ ├── FieldEmployee(对应 #f-empname .field.with-caret:员工名下拉单选,options 来自 GET /api/usr/employees,可空,选中联动带出用户号/用户名,见 § 5 BR5) | ||
| 41 | +│ ├── FieldUserName(对应 #f-username input:用户名,必填;create 可编辑,edit 只读不可改(唯一标识),见 § 5 BR3) | ||
| 42 | +│ ├── FieldUserType(对应 #f-type .field.with-caret:类型下拉单选,普通用户/超级管理员,create 默认普通用户,见 § 5 BR6) | ||
| 43 | +│ ├── FieldLanguage(对应 #f-lang .field.with-caret:语言下拉单选,中文/英文/繁体,见 § 5 BR7) | ||
| 44 | +│ ├── FieldUserNo(对应 #f-userno input:用户号,必填,关联职员后可自动带出,见 § 5 BR4/BR5) | ||
| 45 | +│ └── FieldCanModifyBill(对应 .form-cell .cb「单据修改权限」:复选框,默认否,见 § 5 BR8) | ||
| 46 | +│ (密码字段不在 UI 呈现:create 由后端默认 666666 初始化,edit 不改密码;对应卡片「密码=系统生成/不显示」,见 § 5 BR9) | ||
| 47 | +├── PermissionTabs(对应 .tabs-row:权限页签条) | ||
| 48 | +│ ├── TabPermGroup(对应「权限组」active 页签:本 FE 唯一有数据的页签,承载权限分类勾选) | ||
| 49 | +│ └── TabPlaceholder[](对应「客户查看权限/供应商查看权限/人员查看权限/工序查看权限/司机查看权限」:占位页签,无 REQ/端点,见 § 8 D9) | ||
| 50 | +└── PermissionGroupList(对应 .perm-list:权限分类勾选列表) | ||
| 51 | + ├── HeadRow(对应 .perm-row.head:表头「☐ 权限分类 ⇅」,含全选复选框 + 排序图标占位) | ||
| 52 | + └── PermRow[](对应 .perm-row:每行复选框 + 权限分类名;数据来自 GET /api/usr/permissions;勾选集合 → 提交 permissionIds,见 § 5 BR10/BR11) | ||
| 53 | +``` | ||
| 54 | + | ||
| 55 | +- 控件选型(依据 `docs/04 § 零` `frontend.ui_lib = Ant Design 5.x`): | ||
| 56 | + - 工具栏 → 自定义深色 `div` 条 + AntD `Button`(`type="text"`,白字图标按钮,复刻原型 `.tb-btn`),「保存」`type="primary"`(或主操作高亮)配 `SaveOutlined`、「新增」`PlusCircleOutlined`、「取消」`CloseCircleOutlined`、设置 `SettingOutlined`;占位按钮(删除/作废/重置密码/取消作废/功能)渲染但 `disabled` 或点击 `message.info("功能开发中")`(见 § 8 D8)。 | ||
| 57 | + - 表单网格 → AntD `Form`(`layout` 自定义为 3 列网格,复刻原型 `.form-grid grid-template-columns:repeat(3,1fr)`),各字段为 `Form.Item`;必填项标 `required`(红色 `*` 复刻原型 `.lbl.req::before`)。 | ||
| 58 | + - 员工名 / 类型 / 语言 → `Select`(单选);员工名 options 来自 `GET /api/usr/employees`,类型/语言 options 为固定枚举(见 § 5 BR6/BR7,不杜撰端点)。 | ||
| 59 | + - 用户名 / 用户号 → `Input`(用户名 edit 态 `disabled`,见 BR3)。 | ||
| 60 | + - 单据修改权限 → `Checkbox`。 | ||
| 61 | + - 创建时间 / 制单人 → 只读展示(`Input disabled` 或纯文本 `div`,复刻原型 `.field.readonly`)。 | ||
| 62 | + - 权限页签 → AntD `Tabs`(仅「权限组」页签有内容,其余占位页签 `disabled` 或空态,见 § 8 D9)。 | ||
| 63 | + - 权限分类列表 → AntD `Checkbox.Group` + 列表行(复刻原型 `.perm-list .perm-row`),表头「全选」用 `Checkbox`(`indeterminate` 表达半选);数据来自 `GET /api/usr/permissions`,勾选集合即提交的 `permissionIds`。 | ||
| 64 | +- 页面在受保护区内渲染(无 token 由 FE-02 `RequireAuth` 拦截重定向,本页不重复守卫)。顶栏 / 标签栈属 FE-02,本页只提供内容区;保存成功/取消后由本页 `navigate('/usr/users')` 回流列表(标签联动属 FE-02,见 § 8 D6)。 | ||
| 65 | + | ||
| 66 | +--- | ||
| 67 | + | ||
| 68 | +## 3. 页面状态机(≥5 态) | ||
| 69 | + | ||
| 70 | +单据态以「初始化加载 / 正常编辑 / 表单提交中 / 提交失败 / 提交成功 / 预填或下拉取数失败」表达。`mode` 由路由判定(`/usr/users/new` → `create`,`/usr/users/:id` → `edit`);页面 hook 持有 `{ mode, formValues, employees, permissions, checkedPermissionIds, loading, submitting, error }`: | ||
| 71 | + | ||
| 72 | +| 状态 | 触发时机 | UI 表现 | | ||
| 73 | +|---|---|---| | ||
| 74 | +| `initialLoading`(初始化加载中 / loading) | 页面挂载即预取下拉数据:`GET /api/usr/employees`(员工名)+ `GET /api/usr/permissions`(权限分类);edit 态额外取该用户详情(见 § 8 D4)。对应卡片「预加载=页面加载时」 | 单据区 AntD `Spin` 遮罩或骨架;表单字段禁用直至数据就绪;工具栏「保存」禁用 | | ||
| 75 | +| `editing`(正常编辑 / 正常态) | 下拉数据就绪、(edit 态)原值回填完成、表单未提交 | 表单字段按 mode 可编辑(create 全部可编辑+默认值预填;edit 用户名只读、其余回填原值可改);权限列表渲染并按已授权回勾(edit);「保存」可点 | | ||
| 76 | +| `submitting`(表单提交中 / 提交中) | 点击「保存」且前置校验通过,调 `POST`(create)/ `PUT`(edit)期间 | 「保存」按钮置 `loading` 并禁用(防重复提交,见 § 5 BR15);表单整体置 `disabled`/遮罩;其余工具栏按钮禁用 | | ||
| 77 | +| `submitError`(提交失败) | 接口返回非 0 `code`(40001/40901/40301/40401)或网络/超时/5xx | 按 § 4 错误码表在表单内(就近字段)或全局 `message.error` 展示文案;用户名冲突(40901)就近高亮用户名字段(docs/04 § 2.4「表单提交错误就近在表单展示」);保留已填值可修正重试;按钮恢复可点 | | ||
| 78 | +| `submitSuccess`(提交成功) | 接口返回 `code=0`,返回 `data.id` | `message.success`(create:「用户创建成功」/ edit:「保存成功」);按 BR16 回流:`navigate('/usr/users')` 回到列表并触发列表刷新(标签联动属 FE-02);或留在单据切到 edit 态(取舍见 § 8 D6) | | ||
| 79 | +| `loadError`(预填/下拉取数失败) | `GET /api/usr/employees`、`GET /api/usr/permissions` 或 edit 详情取数失败(非 0 / 网络异常) | 对应区域显错误占位 + 「加载失败,点击重试」入口(员工/权限下拉空 + 重试;edit 详情失败给整页重试或返回列表);`message.error` 文案见 § 4;被动 401 由 `request.ts` 拦截器统一跳 `/login`(docs/04 § 2.4) | | ||
| 80 | + | ||
| 81 | +> 状态以页面本地 hook(`useState`/自定义 `useUserDetail`)表达:`{ mode, formValues, employees, permissions, checkedPermissionIds, loading, submitting, error }`;不进全局 `store`(单据态为页面就近态,docs/04 § 2.2「跨页面共享的才进 store」,见 § 8 D7)。`authResolving`/`unauthenticated` 由 FE-02 守卫承担,本页不重复建态。 | ||
| 82 | + | ||
| 83 | +--- | ||
| 84 | + | ||
| 85 | +## 4. 消费的后端端点(对齐 docs/05) | ||
| 86 | + | ||
| 87 | +| 端点 | 方法 | 触发时机 | 请求参数 | 成功响应 | 失败处理 | | ||
| 88 | +|---|---|---|---|---|---| | ||
| 89 | +| `/api/usr/users` | POST | create 态点击「保存」且校验通过 | JSON body `{ sUserName, sUserNo?, iEmployeeId?, sUserType, sLanguage, iCanModifyBill?(0/1), permissionIds?(number[]), initialPassword?(默认666666,前端不显式传,由后端默认) }`(对齐 docs/05 § REQ-USR-001) | `Result<{ id:number }>`,`code=0`,返回新建用户主键 id | 非 0 `code` 按错误码表反馈;用户名冲突就近高亮 | | ||
| 90 | +| `/api/usr/users/{id}` | PUT | edit 态点击「保存」且校验通过 | 路径 `id`;JSON body `{ sUserNo?, iEmployeeId?, sUserType, sLanguage, iCanModifyBill?(0/1), iIsVoid?(0/1), permissionIds?(number[],全量覆盖语义) }`(sUserName 不可改、密码不在本接口改;对齐 docs/05 § REQ-USR-002) | `Result<{ id:number }>`,`code=0`,返回被修改用户 id | 非 0 `code` 按错误码表反馈 | | ||
| 91 | +| `/api/usr/users` | GET | edit 态预填该用户原值(复用 REQ-USR-003 列表查询,按用户名/号精确匹配定位单条,见 § 8 D4) | query `{ queryField, matchType:等于, queryValue, pageNum:1, pageSize:1 }`(对齐 docs/05 § REQ-USR-003) | `Result<PageResult<UserVO>>`,取 `records[0]` 作为原值(`UserVO` 不含密码) | 取数失败 → 整页重试或返回列表 + `message.error` | | ||
| 92 | +| `/api/usr/employees` | GET | 页面挂载(员工名下拉预加载) | 无参(或可选关键字,MVP 无参全量) | `Result<List<{ iIncrement, sEmployeeName, sEmployeeNo, 部门? }>>`,`code=0`(映射为 `EmployeeOption{ value=iIncrement, label=sEmployeeName }`) | 取数失败 → 员工 `Select` 空 + 重试入口 + `message.error("员工列表加载失败")`(端点来源见 § 8 D1) | | ||
| 93 | +| `/api/usr/permissions` | GET | 页面挂载(权限组列表预加载) | 无参(MVP 全量) | `Result<List<{ iIncrement, sPermissionName, sPermissionCategory }>>`,`code=0`(映射为 `PermissionItem{ id=iIncrement, name=sPermissionName, category=sPermissionCategory }`) | 取数失败 → 权限列表空 + 重试入口 + `message.error("权限列表加载失败")`(端点来源见 § 8 D2) | | ||
| 94 | + | ||
| 95 | +请求 / 响应约定(依据 `docs/04 § 2.3 / § 2.4`): | ||
| 96 | +- 统一走 `frontend/src/api/request.ts` 的 Axios 实例(FE-01 已建:`baseURL=/api`,开发期经 Vite proxy 转发到后端端口 `config-vars.yaml backend.http_port=5172`;请求拦截器注入 `Authorization: Bearer <token>`,token 取 localStorage 键 `xly_erp_token`)。 | ||
| 97 | +- 响应拦截器统一拆 `Result`:`code=0` 取 `data`;非 0 `code` 抛业务错误(默认弹 `message.error`,本页对可就近展示的字段错误覆盖为表单内提示);`401` 统一跳 `/login`(docs/04 § 2.4)。本页方法集中在 `usrApi.ts`(`createUser` / `updateUser` / `getUserDetail` / `listEmployees` / `listPermissions`),页面不直接散用 axios(docs/04 § 2.3)。 | ||
| 98 | + | ||
| 99 | +#### 错误码表(对齐 docs/05 § REQ-USR-001 / § REQ-USR-002) | ||
| 100 | + | ||
| 101 | +| code | 含义 | 前端文案 | 处理 | | ||
| 102 | +|---|---|---|---| | ||
| 103 | +| `0` | 成功 | —(`message.success` + 回流列表) | create:「用户创建成功」/ edit:「保存成功」 | | ||
| 104 | +| `40001` | 参数校验失败(字段格式/必填/枚举越界/关联 id 不存在) | 「提交信息有误,请检查后重试」(能定位到具体字段时就近提示,如用户名格式「用户名须为 3-20 位字母数字下划线」) | `message.error` + 就近字段高亮;保留已填值,正常情况下前端前置校验已拦截,此为兜底 | | ||
| 105 | +| `40901` | 用户名已存在(sUserName 全局唯一冲突,仅 create) | 「用户名已存在,请更换」 | 用户名字段就近 `Form.Item` 报错高亮 + `message.error`;聚焦用户名框 | | ||
| 106 | +| `40401` | 用户不存在(id 无对应记录,仅 edit) | 「该用户不存在或已被删除」 | `message.error` + 提供「返回列表」入口(数据已不在) | | ||
| 107 | +| `40301` | 无权限(非管理员调用) | 「无权限执行此操作」 | `message.error`;正常情况下入口仅对管理员开放,此为后端兜底 | | ||
| 108 | +| 网络/超时/5xx | 请求异常 | 「保存失败,请稍后重试」 | 响应拦截器兜底 `message.error`;按钮恢复可点 | | ||
| 109 | +| `401` | 登录失效(被动) | 「登录已失效,请重新登录」 | 由 `request.ts` 拦截器统一跳 `/login`(docs/04 § 2.4),本页不单独处理 | | ||
| 110 | + | ||
| 111 | +> create 态可能命中 40001/40901/40301;edit 态可能命中 40001/40401/40301(无 40901,用户名不可改)。前端按 mode 仅订阅相关码,未列码归入「保存失败」兜底。 | ||
| 112 | + | ||
| 113 | +--- | ||
| 114 | + | ||
| 115 | +## 5. 业务规则前端复刻清单(逐条) | ||
| 116 | + | ||
| 117 | +| # | 规则 | 触发时机 | 前端报错 / 反馈文案 | 来源 | | ||
| 118 | +|---|---|---|---|---| | ||
| 119 | +| BR1 | 创建时间为系统生成、只读;create 态显示空/「保存后自动生成」,edit 态回填原值 `tCreateDate`,前端均不提交该字段 | 渲染单据 | —(只读展示,不参与提交) | REQ-USR-001/002 表 1「创建时间=系统生成/只读,保存后自动生成」;原型 `#f-ctime .readonly` + `setUserDetailMode('new')` 置空 | | ||
| 120 | +| BR2 | 制单人为系统生成、只读;create 态显示「保存后自动生成」,edit 态回填原值 `sCreator`,前端不提交 | 渲染单据 | —(只读展示,不提交) | REQ-USR-001/002 表 1「制单人=系统生成/只读,默认当前登录用户/原值」;原型 `#f-creator` + 新增态文本「保存后自动生成」 | | ||
| 121 | +| BR3 | 用户名必填;create 态可手工输入(前端前置校验 3-20 位字母数字下划线),edit 态只读不可改(唯一标识) | create 输入 / edit 渲染 | create 格式不符:「用户名须为 3-20 位字母数字下划线」;为空:「请输入用户名」 | REQ-USR-001 表 1「用户名=必填/手工输入」+ docs/05 § REQ-USR-001「3-20 位字母数字下划线,全局唯一」;REQ-USR-002 跨字段「sUserName 不可修改」 | | ||
| 122 | +| BR4 | 用户号必填,手工输入 | 提交校验 | 为空:「请输入用户号」 | REQ-USR-001/002 表 1「用户号=必填/手工输入」 | | ||
| 123 | +| BR5 | 选择「员工名」后自动带出用户号/用户名(前端联动预填,用户仍可改) | 员工名下拉选中 | —(联动填充,无报错) | REQ-USR-001/002 表 1「关联职员选择后自动输入员工姓名」;REQ-USR-001 后端规格 D3「自动带出属前端交互」 | | ||
| 124 | +| BR6 | 类型下拉单选,取值仅普通用户/超级管理员;create 默认「普通用户」,edit 回填原值 | 渲染下拉 / 提交校验 | 越界由 `Select` 限制(无自由输入);必填为空:「请选择类型」 | REQ-USR-001 表 1「类型=必填/下拉/默认普通用户」;REQ-USR-002「默认原值」;docs/05 枚举 {普通用户,超级管理员} | | ||
| 125 | +| BR7 | 语言下拉单选,取值仅中文/英文/繁体;必填 | 渲染下拉 / 提交校验 | 必填为空:「请选择语言」 | REQ-USR-001/002 表 1「语言=必填/下拉/中文,英文,繁体」 | | ||
| 126 | +| BR8 | 单据修改权限复选框,默认否(create)/ 原值(edit);提交为 0/1 | 渲染 / 提交 | —(布尔,无报错) | REQ-USR-001/002 表 1「单据修改权限=布尔/复选框/默认否」+ docs/05 `iCanModifyBill(0/1)` | | ||
| 127 | +| BR9 | 密码不在 UI 呈现:create 由后端默认 666666 初始化(前端不显式传),edit 不修改密码 | 提交 | —(前端不采集/不显示密码) | REQ-USR-001 表 1「密码=系统生成/不显示/默认 666666」;REQ-USR-002 跨字段「密码不在该接口修改」 | | ||
| 128 | +| BR10 | 权限组:列表项来自后端权限分类,复选框勾选集合即提交的 `permissionIds`;edit 态按已授权回勾 | 渲染权限列表 / 提交 | —(勾选集合,无报错;取数失败见 § 4 loadError) | REQ-USR-001/002 表 2「权限组复选框 + 权限分类」;docs/05 `permissionIds(number[])` | | ||
| 129 | +| BR11 | edit 态权限为全量覆盖语义:提交的 `permissionIds` 即该用户最终授权全集(不勾即取消授权;空集清空) | edit 保存 | —(前端提交当前勾选全集,覆盖语义由后端执行) | REQ-USR-002 后端规格 D4「permissionIds 全量覆盖」+ § 3 规则 8 | | ||
| 130 | +| BR12 | 点击「保存」:前置必填/格式校验通过后,create 调 `POST /api/usr/users`、edit 调 `PUT /api/usr/users/{id}` | 点击「保存」 | 校验不通过:就近字段报错不发请求;通过后按 § 4 反馈 | 原型 `.toolbar`「保存」;docs/05 § REQ-USR-001/002 | | ||
| 131 | +| BR13 | 点击「取消」:放弃当前编辑,回到用户列表 `/usr/users`(不提交) | 点击「取消」 | —(`navigate('/usr/users')`;有未保存改动时可二次确认,见 § 8 D5) | 原型 `.toolbar`「取消」 | | ||
| 132 | +| BR14 | 点击「新增」:在单据内切到 create 模式(或 `navigate('/usr/users/new')`),清空各字段、制单人显示「保存后自动生成」、清除权限勾选 | 点击「新增」 | —(复刻 `setUserDetailMode('new')`) | 原型 `.toolbar`「新增」+ `setUserDetailMode('new')` 脚本 | | ||
| 133 | +| BR15 | 提交进行中禁用「保存」按钮并置 loading,防重复提交 | submitting 态 | —(按钮 `loading`+`disabled`) | docs/04 § 2.4 错误处理 / 通用表单防重(登记 § 8 D5) | | ||
| 134 | +| BR16 | 保存成功后回流用户列表并触发列表刷新(保证新增/修改实时反映在列表,对应验收) | submitSuccess | `message.success` 后 `navigate('/usr/users')` | REQ-USR-001 验收「提交合法数据后用户记录出现在列表」;REQ-USR-002 验收「修改后立即反映在用户列表」 | | ||
| 135 | +| BR17 | edit 态进入时按路由 `:id` 预填该用户原值(基本字段 + 已授权权限回勾) | edit 页面挂载 | 取数失败按 § 4 loadError | REQ-USR-002 表 1「预加载=页面加载时/默认值=原值」;表 2「预加载原值」 | | ||
| 136 | + | ||
| 137 | +> 本页只做**字段采集 + 轻量前置校验 + 写提交 + 反馈回流**;用户名唯一性/格式终判、枚举与外键存在性、权限多对多写入与全量覆盖、密码 BCrypt 初始化、管理员权限、审计字段生成等真伪裁决全部由后端在 `POST /api/usr/users` / `PUT /api/usr/users/{id}` 内完成。前端前置校验仅为减少无效请求与即时反馈,不替代后端校验、不预判结果、不杜撰业务端点。 | ||
| 138 | + | ||
| 139 | +--- | ||
| 140 | + | ||
| 141 | +## 6. 字段定义与提交映射(对齐 REQ 表 1/表 2 + docs/05 请求体) | ||
| 142 | + | ||
| 143 | +| 表单字段(中文标签,对应原型) | 提交字段(docs/05) | mode 行为 | 渲染/校验说明 | | ||
| 144 | +|---|---|---|---| | ||
| 145 | +| 创建时间 | —(不提交) | create 空/占位;edit 回填 `tCreateDate` | 只读(BR1) | | ||
| 146 | +| 制单人 | —(不提交) | create「保存后自动生成」;edit 回填 `sCreator` | 只读(BR2) | | ||
| 147 | +| 员工名 | `iEmployeeId`(可选) | create 可选空;edit 回填原值 | `Select`,options 来自 `GET /api/usr/employees`(`value=iIncrement`,`label=sEmployeeName`);选中联动带出用户号/用户名(BR5) | | ||
| 148 | +| 用户名 | `sUserName`(create 必传;edit 不传) | create 可编辑必填;edit 只读不可改 | `Input`,前置校验 3-20 位字母数字下划线(BR3) | | ||
| 149 | +| 用户号 | `sUserNo`(可选,本 UI 必填采集) | create/edit 均可编辑,可由员工名联动带出 | `Input`,前端必填(BR4),后端契约可空 | | ||
| 150 | +| 类型 | `sUserType`(必填) | create 默认普通用户;edit 回填原值 | `Select`,枚举 {普通用户,超级管理员}(BR6) | | ||
| 151 | +| 语言 | `sLanguage`(必填) | create 必选;edit 回填原值 | `Select`,枚举 {中文,英文,繁体}(BR7) | | ||
| 152 | +| 单据修改权限 | `iCanModifyBill`(0/1) | create 默认 0;edit 回填原值 | `Checkbox`(BR8) | | ||
| 153 | +| (作废,仅 edit 可选) | `iIsVoid`(0/1,仅 PUT) | edit 可改(控制启停用);create 不提交 | docs/05 § REQ-USR-002 含 `iIsVoid`;原型工具栏「作废/取消作废」语义映射到此布尔(见 § 8 D8),MVP 可作为表单内启停用开关或暂随占位按钮,登记 D8 | | ||
| 154 | +| 权限组勾选 | `permissionIds`(number[]) | create 勾选集;edit 全量覆盖集 | `Checkbox.Group`,项来自 `GET /api/usr/permissions`(`value=iIncrement`,`label=sPermissionName`/`sPermissionCategory`)(BR10/BR11) | | ||
| 155 | +| (密码) | `initialPassword`(create 可选,前端不传由后端默认 666666) | 不在 UI | 不采集(BR9) | | ||
| 156 | + | ||
| 157 | +> 输出:create 返回 `data.id`(卡片输出表 1「用户号」语义上对应新建用户主键,前端用于回流/提示);edit 返回 `data.id`(被改用户)。前端不直接展示返回 id,仅用于成功反馈与列表刷新定位。 | ||
| 158 | + | ||
| 159 | +--- | ||
| 160 | + | ||
| 161 | +## 7. Design Tokens 引用清单(`src/styles/tokens.css`,仅 `var(--color-*)`) | ||
| 162 | + | ||
| 163 | +> 约束:组件样式只用 `var(--color-*)`,禁止硬编码 hex/rgba;色值冲突时 `tokens.css` 优先于 `prototype/`(原型内联 `:root` 变量为 demo 私有,不作色值 SSoT)。AntD 主题色经 FE-02/FE-01 已配置的 `ConfigProvider` 对齐 `--color-primary`。 | ||
| 164 | + | ||
| 165 | +| 用途 | Token | 备注 | | ||
| 166 | +|---|---|---| | ||
| 167 | +| 主操作(保存按钮 / 激活页签下划线 / 链接强调) | `var(--color-primary)` | 对应原型「保存」主操作、`.tabs-row .tb.active` 蓝色下划线;同时作为 AntD `colorPrimary` | | ||
| 168 | +| 页面 / 内容区基础背景 | `var(--color-bg-base)` | 单据内容区浅灰底(外壳 Outlet 内) | | ||
| 169 | +| 表单网格 / 权限列表背景(白) | `var(--color-form-bg-edit)` | 对应原型 `.form-grid` / `.perm-list` 白底(原型 `--panel`/`#fff`) | | ||
| 170 | +| 可编辑输入框背景(用户名/用户号/下拉) | `var(--color-form-bg-edit)` | 可编辑字段底(原型 `--field-bg` 映射到表单可编辑 token) | | ||
| 171 | +| 只读字段背景(创建时间/制单人) | `var(--color-form-bg-readonly)` | 对应原型 `.field.readonly`(原型 `--field-bg-readonly`) | | ||
| 172 | +| 下拉项 hover 背景 | `var(--color-form-bg-hover)` | 员工名/类型/语言 `Select` 选项 hover(tokens 注「仅下拉框使用」) | | ||
| 173 | +| 表单文字 / 字段值 | `var(--color-form-fg)` | 输入与展示文字 | | ||
| 174 | +| 权限列表表头背景 | `var(--color-table-header-bg)` | 对应原型 `.perm-row.head` 表头底(原型 `--header-bg`) | | ||
| 175 | +| 权限列表表头文字 | `var(--color-table-header-fg)` | 「权限分类」表头文字 | | ||
| 176 | +| 权限行 hover 背景 | `var(--color-table-row-bg-hover)` | 权限行悬停高亮(复刻原型行 hover 浅色) | | ||
| 177 | +| 通用文字 / 标签文字 | `var(--color-text)` | 字段标签、页签文字 | | ||
| 178 | +| 次要文字 / 占位提示 | `var(--color-text-secondary)` | 占位页签、占位按钮、空态提示、设置齿轮 | | ||
| 179 | +| 必填星号 / 用户名冲突等校验错误 | `var(--color-error)` | 必填 `*`(复刻原型 `.lbl.req` 红 `--label`)、`message.error`、字段就近报错强调 | | ||
| 180 | +| 边框 / 分隔线 / 网格线 | `var(--color-border)` | 表单网格分隔、权限行下边线、页签条下边线(原型 `--border`) | | ||
| 181 | +| 警告提示 | `var(--color-warning)` | 未保存改动二次确认等轻提示(D5) | | ||
| 182 | +| 成功提示 | `var(--color-success)` | `message.success`(创建/保存成功) | | ||
| 183 | + | ||
| 184 | +> 工具栏 `.toolbar` 的深色底(原型 `--toolbar-bg:#2c2f36`)在 `tokens.css` 中**无对应语义 token**;与 FE-02/FE-03 一致,本规格将单据工具条深色底作为**页面局部装饰样式**保留在 `UserDetail` 的 scoped 样式里(不新增全局 token、不挪用语义 token)。该深色仅承载工具条容器视觉,不承载状态语义,不违反「语义色只用 token」约束(见 § 8 D10)。原型必填标签红色 `--label:#f04848` 在 tokens 中映射到 `var(--color-error)`(语义=校验错误/必填强调),以 tokens.css 为准。 | ||
| 185 | + | ||
| 186 | +--- | ||
| 187 | + | ||
| 188 | +## 8. 自主决策记录(decisions) | ||
| 189 | + | ||
| 190 | +| # | 问题 | 选择 | 依据 | 置信度 | | ||
| 191 | +|---|---|---|---|---| | ||
| 192 | +| D1 | 员工名下拉数据从哪个端点取(docs/05 主清单与 REQ-USR-001/002 后端规格均只列写端点 `POST`/`PUT`,未定义员工读端点) | 消费支撑只读端点 `GET /api/usr/employees`(返回 `usr_employee` 的 iIncrement/sEmployeeName/sEmployeeNo/部门),映射为员工名下拉 options | REQ-USR-001/002 表 1 明确「员工名=下拉单选/显示来源=职员表/预加载=页面加载时」,必须有读源;docs/03 `usr_employee` 定义了 sEmployeeName 且注「用户『员工名』下拉来源」;沿用 FE-01 § 8 D1 既有先例(下拉所需的支撑只读端点由对应后端 REQ 补齐,如 `GET /api/usr/companies`);不杜撰参数、仅按职员表字段定义最小读端点,留待后端实现期补契约/或后端 REQ 补齐 | medium | | ||
| 193 | +| D2 | 权限组列表数据从哪个端点取(docs/05 未定义权限读端点) | 消费支撑只读端点 `GET /api/usr/permissions`(返回 `usr_permission` 的 iIncrement/sPermissionName/sPermissionCategory),映射为权限分类勾选列表 | REQ-USR-001/002 表 2「权限组复选框 + 权限分类/预加载=页面加载时」必须有读源;docs/03 `usr_permission` 定义 sPermissionName/sPermissionCategory 且注「对应新增/修改用户界面『权限组』」;提交字段 `permissionIds` 需先有可选权限项;与 D1 同先例处理,不杜撰额外语义 | medium | | ||
| 194 | +| D3 | 「权限分类」列表是按分类聚合展示还是逐权限项展示 | MVP 按后端返回的权限项逐行展示(每行一个可勾选权限),列名「权限分类」沿用原型表头;若后端按分类聚合返回则按分类分组渲染(实现期对齐响应结构) | docs/03 `usr_permission` 注「权限粒度(按分类/按具体功能点)待确认」为 DB 文档遗留占位(标注「需用户审阅」),非本前端可消解;原型 `.perm-list` 逐行渲染 `perms` 数组(扁平项),故 MVP 取逐行展示最贴合原型;提交 `permissionIds` 为权限项 id 集合(docs/05 `permissionIds(number[])`),与逐项勾选一致 | medium | | ||
| 195 | +| D4 | edit 态预填用户原值从哪个端点取(docs/05 无单用户详情 GET 端点,仅列表 `GET /api/usr/users`) | 复用 REQ-USR-003 列表端点 `GET /api/usr/users`,以「等于」匹配 + pageSize=1 按 :id 对应的用户名/用户号定位取 `records[0]`;不杜撰新的 `GET /api/usr/users/{id}` 详情端点 | docs/05 仅定义列表查询端点,无单据详情端点;硬约束禁止编造后端端点;列表 `UserVO` 已含编辑所需基本字段(不含密码,正符合「密码不在编辑接口」);FE-03 双击行携带的 `id` 与列表行数据可经路由 state 传递时甚至无需重新取数(见备注)。备注:FE-03 双击进入时可经 React Router `navigate(..., {state:{user}})` 把行数据带入 edit 预填,避免二次请求;二者择一实现期定,列渲染语义不变 | medium | | ||
| 196 | +| D5 | 「保存」防重复提交 / 「取消」未保存改动是否二次确认 | 提交中禁用「保存」并置 loading(BR15);「取消」时若表单有未保存改动弹 AntD `Modal.confirm`「放弃未保存的修改?」,无改动直接返回 | 通用表单交互安全实践(防重复写、防误丢编辑),不与任何业务规则冲突;非硬性 REQ 要求,故登记 | medium | | ||
| 197 | +| D6 | 保存成功后留在单据还是回流列表 | 回流用户列表 `/usr/users` 并触发列表刷新(`message.success` 后 `navigate`),由 FE-02 标签栈联动(关闭/切回列表标签语义属 FE-02) | REQ-USR-001/002 验收均要求「记录出现/反映在用户列表」,回流列表最直接满足验收且与原型「单据隶属列表标签下」结构一致(FE-02 BR5/BR6 标签联动);留在单据需额外切 edit 态,MVP 取回流更简 | medium | | ||
| 198 | +| D7 | 单据态放页面本地 hook 还是全局 store | 页面本地 hook(`useUserDetail`,含 mode/formValues/employees/permissions/checkedPermissionIds/loading/submitting/error),不进全局 store | docs/04 § 2.2「服务端数据优先就近在页面用 hooks 拉取,跨页面共享的才进 store」;单据数据仅本页用,无跨页共享需求;与 FE-03 D6 一致 | high | | ||
| 199 | +| D8 | 工具栏「删除/作废/重置密码/取消作废/功能」按钮(原型有,但 docs/05/REQ 无对应端点)如何处理 | 占位渲染(`disabled` 或点击 `message.info("功能开发中")`),不杜撰后端端点;其中「作废/取消作废」语义对应 `iIsVoid` 0/1,MVP 可在 edit 表单内以启停用开关承载(提交 PUT 的 `iIsVoid`),其余删除/重置密码/功能纯占位 | docs/05 仅定义 create/update/list/login 端点,无删除/重置密码/作废独立端点(硬约束禁止编造);`iIsVoid` 是 PUT 已有字段(docs/05 § REQ-USR-002),故「作废」可经 PUT 实现而无需新端点;其余按钮无任何端点旁证,占位化最小侵入 | medium | | ||
| 200 | +| D9 | 权限页签条除「权限组」外的 5 个查看权限页签(客户/供应商/人员/工序/司机)如何处理 | 占位页签(`disabled` 或空态「功能开发中」),不实现、不取数 | REQ-USR-001/002 卡片仅定义「权限组」(表 2),其余查看权限在 docs/01/docs/03/docs/05 均无字段/表/端点;原型为静态 demo 页签(无脚本);占位化不杜撰功能 | high | | ||
| 201 | +| D10 | 工具栏深色底色值来源(tokens.css 无工具条深色 token) | 作为页面局部装饰样式保留在 `UserDetail` scoped 样式,不新增全局 token、不挪用语义 token;语义色严格走 token | tokens.css 仅定义语义/状态色,无工具条品牌深色 token;深色底纯装饰无状态语义,局部化最小侵入;与 FE-02/FE-03 一致 | medium | | ||
| 202 | +| D11 | 用户号契约可空但本 UI 是否必填 | 本 UI 按 REQ 卡片表 1「用户号=必填」前端必填采集;docs/05 标 `sUserNo(可选)` 为后端宽容接收(联动带出/为空存 null),二者不冲突 | REQ-USR-001/002 表 1 明确「用户号 必填=是」(业务期望填写);docs/05/后端 D3 标可选是后端容错(按传入值落库);前端按业务卡片必填、后端按契约兜底,取更严的前端必填满足业务期望 | medium | | ||
| 203 | + | ||
| 204 | +> 本规格不含后端实现细节(用户名唯一性/格式终判、枚举与外键存在性校验、权限多对多写入与全量覆盖、密码 BCrypt 初始化、管理员权限、审计字段生成、数据访问、库表迁移等均不在前端作用域);除按 D1/D2/D4 消费/复用支撑读端点外不杜撰任何写端点;登录态/请求基建/路由守卫/标签栈复用 FE-01 + FE-02 + FE-03 已落地资产,所有写操作真伪由后端在 `POST /api/usr/users` / `PUT /api/usr/users/{id}` 调用时裁决。D1/D2 的支撑只读端点(员工/权限下拉数据源)须在后端编码期于 REQ-USR-001/002 后端实现内补齐契约(如同 FE-01 的 `GET /api/usr/companies` 先例),否则前端下拉无数据来源——此为跨阶段待对齐项,记于此供编码阶段消化。 |
frontend/.eslintrc.cjs
0 → 100644
| 1 | +/* eslint-env node */ | ||
| 2 | +module.exports = { | ||
| 3 | + root: true, | ||
| 4 | + env: { browser: true, es2021: true, node: true }, | ||
| 5 | + extends: [ | ||
| 6 | + 'eslint:recommended', | ||
| 7 | + 'plugin:@typescript-eslint/recommended', | ||
| 8 | + 'plugin:react-hooks/recommended', | ||
| 9 | + ], | ||
| 10 | + parser: '@typescript-eslint/parser', | ||
| 11 | + parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, | ||
| 12 | + plugins: ['@typescript-eslint', 'react-refresh'], | ||
| 13 | + ignorePatterns: ['dist', 'node_modules', 'coverage', 'playwright-report', 'test-results'], | ||
| 14 | + rules: { | ||
| 15 | + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], | ||
| 16 | + '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], | ||
| 17 | + '@typescript-eslint/no-explicit-any': 'off', | ||
| 18 | + }, | ||
| 19 | + overrides: [ | ||
| 20 | + { | ||
| 21 | + files: ['**/*.test.ts', '**/*.test.tsx', 'tests/**/*'], | ||
| 22 | + globals: { | ||
| 23 | + describe: 'readonly', | ||
| 24 | + it: 'readonly', | ||
| 25 | + test: 'readonly', | ||
| 26 | + expect: 'readonly', | ||
| 27 | + beforeEach: 'readonly', | ||
| 28 | + afterEach: 'readonly', | ||
| 29 | + beforeAll: 'readonly', | ||
| 30 | + afterAll: 'readonly', | ||
| 31 | + vi: 'readonly', | ||
| 32 | + }, | ||
| 33 | + rules: { | ||
| 34 | + 'react-refresh/only-export-components': 'off', | ||
| 35 | + }, | ||
| 36 | + }, | ||
| 37 | + ], | ||
| 38 | +}; |
frontend/index.html
0 → 100644
| 1 | +<!doctype html> | ||
| 2 | +<html lang="zh-CN"> | ||
| 3 | + <head> | ||
| 4 | + <meta charset="UTF-8" /> | ||
| 5 | + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| 6 | + <title>小羚羊 ERP - 企业业务能力平台</title> | ||
| 7 | + </head> | ||
| 8 | + <body> | ||
| 9 | + <div id="root"></div> | ||
| 10 | + <script type="module" src="/src/main.tsx"></script> | ||
| 11 | + </body> | ||
| 12 | +</html> |