# FE-04 用户信息单据 — AI 代码评审报告(第 1 轮) - 规格:`docs/superpowers/specs/2026-06-01-FE-04.md` - 阶段:前端(frontend) - 关联 REQ:REQ-USR-001(增加用户)/ REQ-USR-002(修改用户) - 关联原型:`prototype/erp.html` `
` - 裁决:**request-changes** --- ## 一、作用域核查(通过) 本轮 diff 实现文件全部落在 `frontend/` 下(`src/api/`、`src/pages/usr/UserDetail/`、`src/router/index.tsx`、`tests/`),未触碰 `backend/` / `sql/` / `scripts/`,符合前端阶段路径作用域硬约束。 ## 二、阻断项(must-fix) ### B1. edit 态预填按主键查「用户号」字段,正常导航流必然取不到记录(blocker) - 位置:`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)`)。 - 事实依据: - 路由 `/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`。 - 因此 edit 态 `presetUser` 恒为 `null`,必走 `getUserDetail` 分支;该分支把主键值当作「用户号」去 `GET /api/usr/users?queryField=用户号&matchType=等于&queryValue=<主键>`。 - 主键 `id` 与业务字段「用户号」`sUserNo` 是不同字段,正常情况下二者取值不相等 → 列表精确匹配返回空 `records` → `getUserDetail` 返回 `null` → 进入 `notFound`(40401)分支,页面显示「该用户不存在或已被删除」。 - 真实后端下,REQ-USR-002 验收「编辑预填该用户原值(基本字段 + 已授权权限回勾)」/ spec BR17 将无法满足。 - 附加事实:docs/05 § REQ-USR-003 列表端点的 `queryField` 合法取值为 `用户名/员工名/用户号/部门/用户类型/作废/登录日期/制单人`,**不含主键**,因此无法用列表端点按 `:id` 主键定位单条。这是 spec D4「复用列表端点按 :id 定位」未消解的跨阶段数据流缺口;当前用「用户号=主键值」的实现并未解决该缺口,反而引入了确定性错误。 - 修复方向(任选其一,实现期定): 1. FE-03 双击进入时经 `navigate('/usr/users/' + row.id, { state: { user: row } })` 携带行数据,使 edit 态走 `presetUser` 分支(spec D4 备注已给此先例),避免二次取数;并相应为 `/new` 之外的 edit 直链场景兜底;或 2. 与后端对齐补「单用户详情」读端点 / 列表端点按主键定位的能力(跨阶段对齐项,记入 decisions),不要把主键塞进「用户号」查询字段。 > 该项单独成立即触发 request-changes。 ## 三、建议项(非 must-fix,可在本轮或后续消化) - S1. `frontend/tests/e2e/userdetail.spec.ts:103-110` 的 GET 存根对**任意** `/api/usr/users**` 请求都返回 `makeUser(7,'zhangsan')`,与请求 query 参数无关,因而掩盖了 B1(存根没有校验 `queryField/queryValue` 是否真的命中目标用户)。建议在 edit 预填用例里断言实际发出的 query 参数,避免再次掩盖此类数据流缺陷。 - 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 一并替换。 - S3. `frontend/src/pages/usr/UserDetail/useUserDetail.ts:150` catch 分支用于错误文案选择的 `permissions.length` 读取的是闭包旧值(`runLoad` 的 `eslint-disable exhaustive-deps`),仅影响 loadError 文案精度,不影响功能;可改读 `permissionsRef` 或不区分文案。 ## 四、对照确认(通过项,留痕) - 原型一致性:工具栏(保存/取消/新增 + 5 占位按钮 + 齿轮)、3 列 `form-grid`(创建时间/制单人只读 + 员工名/用户名/类型/语言/用户号 + 单据修改权限复选框)、`tabs-row`(权限组 active + 5 占位页签 disabled)、`perm-list`(表头全选 indeterminate + 逐行复选框)结构与 `#screen-userdetail` 一致;主操作「保存」置于工具栏左侧主按钮,无结构性偏移。 - Design Tokens:表单/权限区语义色均走 `var(--color-*)`;工具栏深色底 `#2c2f36` 为 D10 豁免装饰;未发现语义色硬编码(白色前景见 S2)。 - 业务校验前端复刻:用户名必填 + 3-20 位字母数字下划线正则(BR3)、用户号必填(BR4)、类型/语言必填枚举(BR6/BR7)、单据修改权限默认否(BR8)、员工名联动带出(BR5)、密码不采集(BR9)、权限全量覆盖语义(BR11)均已实现;错误码 40001/40901/40401/40301 文案与就近高亮(用户名冲突 `form.setFields`)对齐 spec § 4。 - 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。 - 状态机覆盖:loading(`initialLoading` + `Spin`)/ empty(权限空态 `perm-empty`)/ error(`loadError` 重试入口 + `notFound` 返回列表)/ 正常(editing)/ 提交中(`submitting` 置 `loading`+`disabled` 防重)五态齐备。 - a11y(客观项):表单控件均经 AntD `Form.Item label` 关联;危险/导航类「取消」有未保存改动二次确认(`Modal.confirm`)。颜色对比度/响应式为 best-effort,未见明显失败,不单独触发 request-changes。 --- ## 五、裁决 存在 1 项 blocker(B1:edit 预填数据流错误,正常导航流必然 40401),故本轮裁决 **request-changes**。其余为通过项与建议项。