Commit 23705a827b76ed3361bd0befad4aa3a0395a1c2b

Authored by zichun
1 parent 8943945a

docs(review:FE-04:r2): approve

docs/superpowers/reviews/2026-06-01-FE-04.md
1 -# FE-04 用户信息单据 — AI 代码评审报告(第 1 轮) 1 +# FE-04 用户信息单据 — AI 代码评审报告(第 2 轮)
2 2
3 - 规格:`docs/superpowers/specs/2026-06-01-FE-04.md` 3 - 规格:`docs/superpowers/specs/2026-06-01-FE-04.md`
4 - 阶段:前端(frontend) 4 - 阶段:前端(frontend)
5 - 关联 REQ:REQ-USR-001(增加用户)/ REQ-USR-002(修改用户) 5 - 关联 REQ:REQ-USR-001(增加用户)/ REQ-USR-002(修改用户)
6 - 关联原型:`prototype/erp.html` `<section id="screen-userdetail">` 6 - 关联原型:`prototype/erp.html` `<section id="screen-userdetail">`
7 -- 裁决:**request-changes** 7 +- 本轮 fix commit:`96e88d3`(编辑预填走 navigate state + 补 loadError 返回列表入口)
  8 +- 裁决:**approve**
8 9
9 --- 10 ---
10 11
11 ## 一、作用域核查(通过) 12 ## 一、作用域核查(通过)
12 13
13 -本轮 diff 实现文件全部落在 `frontend/` 下(`src/api/`、`src/pages/usr/UserDetail/`、`src/router/index.tsx`、`tests/`),未触碰 `backend/` / `sql/` / `scripts/`,符合前端阶段路径作用域硬约束 14 +本轮 diff 实现文件全部落在 `frontend/` 下(`src/pages/usr/UserDetail/`、`src/pages/usr/UserList/index.tsx`、`tests/unit/`),未触碰 `backend/` / `sql/` / `scripts/`,符合前端阶段路径作用域硬约束。工作树干净(fix 已 commit)
14 15
15 -## 二、阻断项(must-fix 16 +## 二、上轮 must-fix 复验(核心维度
16 17
17 -### B1. edit 态预填按主键查「用户号」字段,正常导航流必然取不到记录(blocker) 18 +### B1(上轮 blocker)— 已修复 ✔
18 19
19 -- 位置:`frontend/src/pages/usr/UserDetail/useUserDetail.ts:124`(`getUserDetail({ queryField: '用户号', queryValue: String(userId) })`);连带 `frontend/src/pages/usr/UserDetail/index.tsx:43`(`userId = Number(params.id)`)。  
20 -- 事实依据:  
21 - - 路由 `/usr/users/:id` 的 `:id` 是**用户主键**——docs/05 § REQ-USR-002「路径参数 `id`(用户主键)」,且 FE-03 双击行入口为 `frontend/src/pages/usr/UserList/index.tsx:36` `navigate('/usr/users/' + row.id)`(`row.id` 即 `UserVO.id` 主键),**未**经 router state 传 `presetUser`。  
22 - - 因此 edit 态 `presetUser` 恒为 `null`,必走 `getUserDetail` 分支;该分支把主键值当作「用户号」去 `GET /api/usr/users?queryField=用户号&matchType=等于&queryValue=<主键>`。  
23 - - 主键 `id` 与业务字段「用户号」`sUserNo` 是不同字段,正常情况下二者取值不相等 → 列表精确匹配返回空 `records` → `getUserDetail` 返回 `null` → 进入 `notFound`(40401)分支,页面显示「该用户不存在或已被删除」。  
24 - - 真实后端下,REQ-USR-002 验收「编辑预填该用户原值(基本字段 + 已授权权限回勾)」/ spec BR17 将无法满足。  
25 -- 附加事实:docs/05 § REQ-USR-003 列表端点的 `queryField` 合法取值为 `用户名/员工名/用户号/部门/用户类型/作废/登录日期/制单人`,**不含主键**,因此无法用列表端点按 `:id` 主键定位单条。这是 spec D4「复用列表端点按 :id 定位」未消解的跨阶段数据流缺口;当前用「用户号=主键值」的实现并未解决该缺口,反而引入了确定性错误。  
26 -- 修复方向(任选其一,实现期定):  
27 - 1. FE-03 双击进入时经 `navigate('/usr/users/' + row.id, { state: { user: row } })` 携带行数据,使 edit 态走 `presetUser` 分支(spec D4 备注已给此先例),避免二次取数;并相应为 `/new` 之外的 edit 直链场景兜底;或  
28 - 2. 与后端对齐补「单用户详情」读端点 / 列表端点按主键定位的能力(跨阶段对齐项,记入 decisions),不要把主键塞进「用户号」查询字段。 20 +上轮判定:edit 态预填把路由主键 `:id` 当「用户号」去 `GET /api/usr/users?queryField=用户号` 精确匹配,真实后端必然返回空 → 误报 40401,无法满足 REQ-USR-002 编辑预填(BR17)。
29 21
30 -> 该项单独成立即触发 request-changes。 22 +本轮 fix(commit `96e88d3`)按上轮修复方向 #1 落地,复验确认到位:
31 23
32 -## 三、建议项(非 must-fix,可在本轮或后续消化) 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」,非误用。
33 29
34 -- S1. `frontend/tests/e2e/userdetail.spec.ts:103-110` 的 GET 存根对**任意** `/api/usr/users**` 请求都返回 `makeUser(7,'zhangsan')`,与请求 query 参数无关,因而掩盖了 B1(存根没有校验 `queryField/queryValue` 是否真的命中目标用户)。建议在 edit 预填用例里断言实际发出的 query 参数,避免再次掩盖此类数据流缺陷。  
35 -- S2. `frontend/src/pages/usr/UserDetail/UserDetail.module.css:25/29/34` 工具栏按钮与齿轮用硬编码 `#ffffff`。tokens.css 无「深色条上的白色前景」语义 token;该白色前景与 `.toolbar` 的 `#2c2f36` 深色装饰底强绑定,属 spec § 7 D10 已豁免的「工具条局部装饰」同一范畴,故不作 must-fix。若希望严格收敛,可在 tokens.css 登记一个工具条前景/底色 token 一并替换。  
36 -- S3. `frontend/src/pages/usr/UserDetail/useUserDetail.ts:150` catch 分支用于错误文案选择的 `permissions.length` 读取的是闭包旧值(`runLoad` 的 `eslint-disable exhaustive-deps`),仅影响 loadError 文案精度,不影响功能;可改读 `permissionsRef` 或不区分文案。 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 保存→回流列表,通过。
37 34
38 -## 四、对照确认(通过项,留痕) 35 +> 结论:B1 已被真实修复(非掩盖),数据流自洽,且测试改造正确反映了「主键无 by-id 读端点、预填依赖列表行 state」这一事实约束。
39 36
40 -- 原型一致性:工具栏(保存/取消/新增 + 5 占位按钮 + 齿轮)、3 列 `form-grid`(创建时间/制单人只读 + 员工名/用户名/类型/语言/用户号 + 单据修改权限复选框)、`tabs-row`(权限组 active + 5 占位页签 disabled)、`perm-list`(表头全选 indeterminate + 逐行复选框)结构与 `#screen-userdetail` 一致;主操作「保存」置于工具栏左侧主按钮,无结构性偏移。  
41 -- Design Tokens:表单/权限区语义色均走 `var(--color-*)`;工具栏深色底 `#2c2f36` 为 D10 豁免装饰;未发现语义色硬编码(白色前景见 S2)。  
42 -- 业务校验前端复刻:用户名必填 + 3-20 位字母数字下划线正则(BR3)、用户号必填(BR4)、类型/语言必填枚举(BR6/BR7)、单据修改权限默认否(BR8)、员工名联动带出(BR5)、密码不采集(BR9)、权限全量覆盖语义(BR11)均已实现;错误码 40001/40901/40401/40301 文案与就近高亮(用户名冲突 `form.setFields`)对齐 spec § 4。  
43 -- API 调用一致性:`createUser`(POST `/usr/users`)、`updateUser`(PUT `/usr/users/{id}`)、`listEmployees`/`listPermissions`/`getUserDetail` 全部经 `frontend/src/api/request.ts` 统一实例,无裸 `fetch`/`axios`;请求体字段(`sUserName/sUserNo/iEmployeeId/sUserType/sLanguage/iCanModifyBill/permissionIds`、PUT 含 `iIsVoid` 不含 `sUserName`)对齐 docs/05 § REQ-USR-001/002。  
44 -- 状态机覆盖:loading(`initialLoading` + `Spin`)/ empty(权限空态 `perm-empty`)/ error(`loadError` 重试入口 + `notFound` 返回列表)/ 正常(editing)/ 提交中(`submitting` 置 `loading`+`disabled` 防重)五态齐备。  
45 -- a11y(客观项):表单控件均经 AntD `Form.Item label` 关联;危险/导航类「取消」有未保存改动二次确认(`Modal.confirm`)。颜色对比度/响应式为 best-effort,未见明显失败,不单独触发 request-changes。 37 +## 三、本轮门禁证据(复跑确认)
46 38
47 ---- 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 +## 六、裁决
48 60
49 -## 五、裁决 61 +上轮唯一 blocker(B1)已在本轮 diff 中真实修复,数据流自洽、测试改造正确、门禁四项(lint / tsc / unit / e2e)全绿,未引入新缺陷、未回归既有通过项。其余为可选建议项,均不构成 must-fix。
50 62
51 -存在 1 项 blocker(B1:edit 预填数据流错误,正常导航流必然 40401),故本轮裁决 **request-changes**。其余为通过项与建议项。 63 +**verdict: approve**