Commit b9c7da30f6ffa1d900050987543144bee1c9a5fd

Authored by zichun
1 parent f7becddb

docs(review:FE-04:r1): request-changes

docs/superpowers/reviews/2026-06-01-FE-04.md 0 → 100644
  1 +# FE-04 用户信息单据 — AI 代码评审报告(第 1 轮)
  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 +- 裁决:**request-changes**
  8 +
  9 +---
  10 +
  11 +## 一、作用域核查(通过)
  12 +
  13 +本轮 diff 实现文件全部落在 `frontend/` 下(`src/api/`、`src/pages/usr/UserDetail/`、`src/router/index.tsx`、`tests/`),未触碰 `backend/` / `sql/` / `scripts/`,符合前端阶段路径作用域硬约束。
  14 +
  15 +## 二、阻断项(must-fix)
  16 +
  17 +### B1. edit 态预填按主键查「用户号」字段,正常导航流必然取不到记录(blocker)
  18 +
  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),不要把主键塞进「用户号」查询字段。
  29 +
  30 +> 该项单独成立即触发 request-changes。
  31 +
  32 +## 三、建议项(非 must-fix,可在本轮或后续消化)
  33 +
  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` 或不区分文案。
  37 +
  38 +## 四、对照确认(通过项,留痕)
  39 +
  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。
  46 +
  47 +---
  48 +
  49 +## 五、裁决
  50 +
  51 +存在 1 项 blocker(B1:edit 预填数据流错误,正常导航流必然 40401),故本轮裁决 **request-changes**。其余为通过项与建议项。