2026-06-01-FE-04.md 53.5 KB

FE-04 用户信息单据 — 任务级 TDD 计划(前端)

阶段:前端(frontend)。作用域:frontend/**(页面 / api / 类型 / 样式 / 测试)。禁止backend/** / sql/** / scripts/**。 上游 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-001POST /api/usr/users)+ § REQ-USR-002PUT /api/usr/users/{id})+ § REQ-USR-003GET /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。 复用资产:FE-01(api/request.ts 已建 Axios 实例 + Result 拆包 + ApiError + TOKEN_STORAGE_KEY + NETWORK_ERROR_CODEapi/types.tsapi/usrApi.tslogin/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.tslistUsers + api/types.tsUserVO/PageResult/UserListQuery;列表入口「新增」→ /usr/users/new、行双击 → /usr/users/:id)。 本计划告诉 TDD 执行者做什么 / 文件边界 / 测试意图 / props 与类型契约 / 完成判据;具体代码由红-绿-提交循环产出,不在此 dump 整组件 / 类型文件内容。 本 FE 同时承载新增(POST)与修改(PUT)两种写场景,共用同一单据组件按 mode 分支;前端只做字段采集 + 轻量前置校验 + 写提交 + 反馈回流。用户名唯一性/格式终判、枚举与外键存在性、权限多对多写入与全量覆盖、密码 BCrypt 初始化、管理员权限、审计字段生成等真伪裁决全部在后端(spec § 5 末注)。


Goal(目标)

把 FE-02 在 router/index.tsx 留下的 /usr/users/new/usr/users/:id 占位(UserDetailPlaceholder)替换为真实「用户信息单据」页面,复刻原型 #screen-userdetail 的布局与交互语义(工具栏 / 3 列表单网格 / 权限页签条 / 权限分类勾选列表),按路由判定 create/edit 模式,数据/下拉/权限/提交真实对接后端:

  • 页面容器 UserDetailPage(路由 /usr/users/new/usr/users/:id 共用,渲染于 FE-02 AppLayout<Outlet/> 内):按路由 :id 判定 mode(有 :idedit/newcreate);纵向组合 UserDetailToolbar + UserBasicForm + PermissionTabs + PermissionGroupList;态由页面本地 hook useUserDetail 持有(spec § 8 D7,不进 Redux)。
  • API 封装api/usrApi.ts 新增 createUser / updateUser / getUserDetail / listEmployees / listPermissions(页面只调封装方法,不散用 axios,docs/04 § 2.3);api/types.ts 新增 UserCreateReq / UserUpdateReq / EmployeeOption / PermissionItem 契约(复用 FE-03 UserVO)。
  • 状态机 ≥6 态(spec § 3):initialLoading(挂载预取员工/权限 + edit 取详情)/ editing / submitting / submitError / submitSuccess / loadError,均有测试固化。
  • 业务规则 BR1~BR17 在组件层 / hook 层 / E2E 有断言(spec § 5)。
  • 工具栏:保存(校验通过后 create→POST / edit→PUT,BR12)/ 取消(有未保存改动二次确认后回 /usr/users,BR13 / D5)/ 新增(切 create 模式 → navigate('/usr/users/new'),BR14)/ 占位按钮(删除/重置密码/功能 → message.info("功能开发中"),作废/取消作废 → edit 态以 iIsVoid 启停用开关承载,D8)/ 设置齿轮(占位,D8)。
  • 表单网格:创建时间(只读 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)。
  • 权限页签 + 权限分类列表Tabs(仅「权限组」有内容,其余 5 个查看权限页签占位 D9)+ Checkbox.Group 列表(项来自 GET /api/usr/permissions,勾选集合 → permissionIds,edit 按已授权回勾、全量覆盖语义 BR10/BR11,表头全选 indeterminate 半选)。
  • 提交反馈与回流code=0message.success(create:「用户创建成功」/ edit:「保存成功」)→ navigate('/usr/users') 回流列表(BR16);非 0 按 § 4 错误码表反馈(40901 用户名冲突就近高亮、40401 用户不存在给返回列表入口、40001/40301/网络兜底,spec § 4);被动 401 由 request.ts 拦截器统一跳 /login(本页不重复处理)。
  • edit 预填:按路由 :id 复用 GET /api/usr/users(等于匹配 + pageSize=1)定位取 records[0] 作原值回填(D4);若 FE-03 行双击携带 location.state.user 则优先用之免二次请求(D4 备注)。
  • 语义色只用 var(--color-*);工具栏深色底为页面局部装饰,scoped 保留,不新增全局 token、不挪用语义 token(spec § 7 / D10)。

Architecture(架构 / 分层)

遵循 docs/04 § 2.1,落点全在 frontend/**新增/改动文件:

frontend/
├── src/
│   ├── api/types.ts                                       # 【改】新增 UserCreateReq / UserUpdateReq / EmployeeOption / PermissionItem(复用 FE-03 UserVO;不破坏既有类型)
│   ├── api/usrApi.ts                                      # 【改】新增 createUser / updateUser / getUserDetail / listEmployees / listPermissions(不改 FE-01/FE-03 既有方法)
│   ├── pages/usr/UserDetail/index.tsx                     # 【新增】UserDetailPage 页面容器:判 mode、装配 4 子组件、持 useUserDetail 态、提交反馈与导航回流
│   ├── pages/usr/UserDetail/useUserDetail.ts              # 【新增】单据 hook(mode/formValues/employees/permissions/checkedPermissionIds/loading/submitting/error + 动作)
│   ├── pages/usr/UserDetail/UserDetailToolbar.tsx         # 【新增】深色工具条:保存/取消/新增 + 占位按钮(删除/作废/重置密码/取消作废/功能)+ 齿轮
│   ├── pages/usr/UserDetail/UserBasicForm.tsx             # 【新增】AntD Form 3 列表单网格:8 个字段 + 员工联动 + 前置校验
│   ├── pages/usr/UserDetail/PermissionTabs.tsx            # 【新增】AntD Tabs 权限页签条(权限组 active + 5 占位页签 D9)
│   ├── pages/usr/UserDetail/PermissionGroupList.tsx       # 【新增】权限分类勾选列表:表头全选(indeterminate) + 逐项 Checkbox(D3)
│   ├── pages/usr/UserDetail/constants.ts                  # 【新增】合同级常量:类型/语言枚举、create 默认值、错误码、文案、用户名正则、占位文案
│   └── pages/usr/UserDetail/UserDetail.module.css         # 【新增】页面 scoped 样式:语义色用 var(--color-*);工具栏深色底局部装饰(D10)
├── src/router/index.tsx                                   # 【改】把 /usr/users/new 与 /usr/users/:id 占位 UserDetailPlaceholder 替换为真实 UserDetailPage(属 FE 共享骨架,留痕)
└── tests/
    ├── unit/usrApi.userdetail.test.ts                     # 【新增】createUser/updateUser/getUserDetail/listEmployees/listPermissions 透传与归一(沿用 usrApi.userlist.test.ts 桩模式)
    ├── unit/useUserDetail.test.tsx                        # 【新增】hook 状态机:create/edit 初始化、员工联动、提交成功/失败、loadError、权限回勾(renderHook)
    ├── unit/UserBasicForm.test.tsx                        # 【新增】字段渲染/默认值/只读规则/枚举/必填+格式校验/员工联动(BR1-BR9)
    ├── unit/PermissionGroupList.test.tsx                  # 【新增】列表渲染/勾选集合/全选 indeterminate/edit 回勾(BR10/BR11/D3)
    ├── unit/PermissionTabs.test.tsx                       # 【新增】权限组 active + 5 占位页签 disabled/空态(D9)
    ├── unit/UserDetailToolbar.test.tsx                    # 【新增】保存/取消/新增回调 + 提交中禁用 + 占位按钮(BR12/BR13/BR14/BR15/D8)
    ├── unit/UserDetailPage.test.tsx                       # 【新增】页面集成:create/edit 贯通、提交回流、错误就近、取数失败(状态机 + BR3/BR12/BR16/BR17)
    └── e2e/userdetail.spec.ts                             # 【新增】E2E:新增提交回流 / 编辑预填改保存 / 用户名冲突就近 / 取数失败重试
  • 跨阶段/跨模块:本 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/既有类型;属共享骨架扩展」。
  • 状态管理(docs/04 § 2.2 / spec D7):单据态(mode/formValues/employees/permissions/checkedPermissionIds/loading/submitting/error)为页面就近态,用 useUserDetail hook + useState 本地管理,不进 Redux;登录态复用 FE-01 authSlice(本页只读当前用户名用于 create 态制单人占位,可选)。
  • 请求封装 / 错误处理(docs/04 § 2.3 / § 2.4):写/读均走 usrApi.*request.ts Axios 实例(响应拦截器已拆 Resultcode=0 返回 data,非 0 抛 ApiError,被动 401 统一登出 FE-02 既有机制);hook/页面捕获 ApiError 按 code 分流:表单可定位字段错误就近在 Form.Item 展示(如 40901 用户名冲突,docs/04 § 2.4「表单提交错误就近在表单展示」),其余 message.error
  • 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 一致)。

Tech Stack(技术栈,源自 docs/04 § 零 + FE-01/FE-02/FE-03 骨架)

  • 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/iconsSaveOutlined / CloseCircleOutlined / PlusCircleOutlined / SettingOutlined)。
  • 不新增 npm 依赖:复用 FE-01/FE-03 既装依赖(见 frontend/package.json dependencies);前置校验用 AntD Form rules + 正则常量,无需额外校验库。
  • 测试:单测 Vitest(jsdom)+ @testing-library/react|jest-dom|user-event(沿用 tests/setup.tsrenderShell.tsx);E2E Playwright(沿用 playwright.config.tspage.route**/api/usr/users****/api/usr/employees****/api/usr/permissions**)。
  • 命令(docs/04 § 零):build npm run buildtsc --noEmit && vite build);lint npm run lint;unit npm run test:unitvitest run);e2e npm run test:e2eplaywright test)。子会话验证用 cd frontend && npm run test:unit -- <文件名片段>
  • 提交格式:<type>(<scope>): <subject> REQ-XXX-NNNscope 统一用 usr(业务模块名,CLAUDE.md § Git);subject 业务类(feat/fix/test)带 REQ-USR-001 后缀(本 FE 主承载增加用户;修改用户 REQ-USR-002 在涉 edit 的 task 追加标注,见各 task)。每个任务在其 commit 行注明 REQ tag。

合同级常量(跨 task 必须一致)

  • API 路径 / 方法usrApi 内传相对路径,request.ts baseURL=/api 已含前缀):
    • POST /usr/users(create,body=UserCreateReq,返回 { id: number })。
    • PUT /usr/users/{id}(edit,路径 id,body=UserUpdateReq,返回 { id: number })。
    • GET /usr/users(edit 预填复用,query { queryField, matchType:'等于', queryValue, pageNum:1, pageSize:1 },取 records[0],复用 FE-03 listUsers,D4)。
    • GET /usr/employees(员工名下拉数据源,无参全量,D1)。
    • GET /usr/permissions(权限分类列表数据源,无参全量,D2)。
  • 类型枚举(逐字一致,原样作为提交值,前端不映射,由后端裁决)
    • 用户类型 USER_TYPE_OPTIONS = ['普通用户', '超级管理员'],create 默认 '普通用户'(BR6)。
    • 语言 LANGUAGE_OPTIONS = ['中文', '英文', '繁体'](BR7,无默认强制选,create 必选)。
  • create 默认表单值 CREATE_DEFAULTS{ sUserName: '', sUserNo: '', iEmployeeId: null, sUserType: '普通用户', sLanguage: undefined, iCanModifyBill: 0 }(BR1/BR2/BR6/BR8;sLanguage 未选触发必填校验 BR7)。
  • 用户名前置校验正则 USERNAME_PATTERN = /^[A-Za-z0-9_]{3,20}$/(3-20 位字母数字下划线,BR3,对齐 docs/05 § REQ-USR-001)。
  • 错误码常量(对齐 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 常量);401request.ts 统一处理(本页不分流)。
  • mode 常量 MODE_CREATE = 'create' / MODE_EDIT = 'edit'(由路由 :id 判定)。
  • 静态文案(逐字一致,复刻原型 / spec): | 用途 | 文案 | |---|---| | 工具栏保存 | 保存 | | 工具栏取消 | 取消 | | 工具栏新增 | 新增 | | 工具栏占位按钮 | 删除 / 作废 / 重置密码 / 取消作废 / 功能(点击 message.info 文案 功能开发中,D8) | | 制单人 create 占位 | 保存后自动生成(复刻 setUserDetailMode('new'),BR2) | | 创建时间标签 | 创建时间 | | 制单人标签 | 制单人 | | 员工名标签 | 员工名 | | 用户名标签 | 用户名 | | 类型标签 | 类型 | | 语言标签 | 语言 | | 用户号标签 | 用户号 | | 单据修改权限标签 | 单据修改权限 | | 权限组页签 | 权限组 | | 占位页签 | 客户查看权限 / 供应商查看权限 / 人员查看权限 / 工序查看权限 / 司机查看权限(D9) | | 权限列表表头 | 权限分类 | | 用户名格式错误 | 用户名须为 3-20 位字母数字下划线(BR3) | | 用户名必填 | 请输入用户名(BR3) | | 用户号必填 | 请输入用户号(BR4) | | 类型必填 | 请选择类型(BR6) | | 语言必填 | 请选择语言(BR7) | | create 成功提示 | 用户创建成功(BR16) | | edit 成功提示 | 保存成功(BR16) | | 40001 提示 | 提交信息有误,请检查后重试(spec § 4) | | 40901 提示 | 用户名已存在,请更换(spec § 4,就近高亮用户名字段) | | 40401 提示 | 该用户不存在或已被删除(spec § 4,提供「返回列表」入口) | | 40301 提示 | 无权限执行此操作(spec § 4) | | 网络/5xx 提示 | 保存失败,请稍后重试(spec § 4 兜底) | | 员工列表加载失败 | 员工列表加载失败(spec § 4 loadError,D1) | | 权限列表加载失败 | 权限列表加载失败(spec § 4 loadError,D2) | | 详情加载失败 | 加载失败,点击重试(edit 详情取数失败,spec § 4 loadError) | | 取消二次确认 | 放弃未保存的修改?Modal.confirm,D5) | | 占位功能提示 | 功能开发中message.info,D8/D9) |
  • 路由 path(FE-02 已注册占位):create → /usr/users/new(BR14 / 取消回流的兄弟路由);edit → /usr/users/:id;保存成功/取消回流 → /usr/users(FE-03 列表,BR13/BR16;标签联动属 FE-02)。
  • localStorage token 键TOKEN_STORAGE_KEY = 'xly_erp_token'(FE-01 request.ts 已导出,本页不直接读写,由 request.ts 拦截器注入)。

关键签名(首次出现处给出,跨 task 一致)

  • 类型契约api/types.ts,新增;复用 FE-03 UserVO/PageResult/UserListQuery 不动):
    • EmployeeOption{ value: number; label: string; sEmployeeNo: string | null }(员工名下拉项;value 映射后端 iIncrementlabel 映射 sEmployeeNamesEmployeeNo 供联动带出用户号,D1/BR5)。
    • PermissionItem{ id: number; name: string; category: string }(权限项;id 映射后端 iIncrementname 映射 sPermissionNamecategory 映射 sPermissionCategory,D2/D3)。
    • 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)。
    • UserUpdateReq{ sUserNo?: string; iEmployeeId?: number | null; sUserType: string; sLanguage: string; iCanModifyBill?: 0 | 1; iIsVoid?: 0 | 1; permissionIds?: number[] }sUserName 不可改不传、密码不在本接口;对齐 docs/05 § REQ-USR-002)。
    • UserDetailMode'create' | 'edit'
  • API 封装api/usrApi.ts,新增方法;响应拦截器已拆 Result.data,沿用 FE-01/FE-03 as unknown as Promise<...> 桥接):
    • createUser(body: UserCreateReq): Promise<{ id: number }>——request.post('/usr/users', body)
    • updateUser(id: number, body: UserUpdateReq): Promise<{ id: number }>——request.put('/usr/users/' + id, body)
    • getUserDetail(params: { queryField: string; queryValue: string }): Promise<UserVO | null>——内部以「等于」匹配 + pageNum:1, pageSize:1listUsers,返回 records[0] ?? null(复用 FE-03 listUsers 与中文键归一,D4)。
    • listEmployees(): Promise<EmployeeOption[]>——request.get('/usr/employees'),对后端原始行(iIncrement/sEmployeeName/sEmployeeNo)归一为 EmployeeOption(D1)。
    • listPermissions(): Promise<PermissionItem[]>——request.get('/usr/permissions'),对后端原始行(iIncrement/sPermissionName/sPermissionCategory)归一为 PermissionItem(D2/D3)。
  • 页面 hookpages/usr/UserDetail/useUserDetail.ts):
    • 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; }
    • 其中 UserFormValues = { sUserName: string; sUserNo: string; iEmployeeId: number | null; sUserType: string; sLanguage: string | undefined; iCanModifyBill: 0 | 1; iIsVoid?: 0 | 1 }(受控表单值,与提交映射 spec § 6 一致;tCreateDate/sCreator 只读展示态另存,不在提交值内)。
    • 挂载即并发预取 listEmployees() + listPermissions()(initialLoading,spec § 3);edit 态额外按 userIdpresetUser ?? getUserDetail(...) 取原值回填 formValues 并以已授权权限初始化 checkedPermissionIds(BR17 / D4);任一预取/详情取数失败置 loadFailed=true + 对应 message.error(spec § 4 loadError,文案见合同级常量)。
    • selectEmployee(value):设 iEmployeeId 并按选中员工 label/sEmployeeNo 联动带出 sUserName(create 态)/sUserNo(用户仍可改,BR5)。
    • 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
    • reload():重跑挂载预取/详情(loadError 重试入口用,spec § 4)。
    • 纯映射函数pages/usr/UserDetail/constants.ts 或 hook 内,跨 task 一致,便于单测):toCreateReq(values: UserFormValues, permissionIds: number[]): UserCreateReqtoUpdateReq(values: UserFormValues, permissionIds: number[]): UserUpdateRequserVoToFormValues(vo: UserVO): UserFormValues(edit 回填,BR17)。
  • 组件 propspages/usr/UserDetail/):
    • UserDetailToolbar{ mode: UserDetailMode; submitting: boolean; canSave: boolean; onSave(): void; onCancel(): void; onNew(): void }):保存(type="primary"submittingloading+disabled BR15,canSave 控可点)/ 取消(BR13)/ 新增(BR14)/ 占位按钮(删除/作废/重置密码/取消作废/功能 + 齿轮,点击 message.info('功能开发中'),D8)。
    • 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);员工名 onChangeonSelectEmployee(BR5)。
    • PermissionTabs{ activeKey?: string; onChange?(key: string): void; children: ReactNode }):AntD Tabs,第一项 权限组(key=group)承载 children(权限列表),其余 5 个 客户查看权限司机查看权限disabled 占位页签(D9)。
    • PermissionGroupList{ permissions: PermissionItem[]; checkedIds: number[]; onToggle(id: number, checked: boolean): void; onToggleAll(checked: boolean): void; loading?: boolean }):表头行「全选 Checkboxindeterminate 半选 / checked 全选)+ 权限分类 + 排序图标占位」(复刻原型 .perm-row.head);逐项 Checkbox 行(PermItem.name,按 checkedIds.includes(id) 勾选;D3 逐项展示);空 permissions 时空态(loadError 由页面消费,spec § 4)。
    • UserDetailPage(default export,无 props):useParams<{ id?: string }>()mode(有 idedituserId=Number(id);否则 create)+ useLocation()(取 state.userpresetUser,D4 备注)+ useNavigate() + AntD Form.useForm()useUserDetail({ mode, userId, presetUser });装配 UserDetailToolbar/UserBasicForm/PermissionTabs(含 PermissionGroupList);onSaveform.validateFields() 通过后调 submit(values)okmessage.success(按 mode) + navigate('/usr/users')(BR16),fieldErrorform.setFields([{ name, errors:[message] }]) 就近高亮(40901,spec § 4);onCancel→ 有未保存改动 Modal.confirm('放弃未保存的修改?') 通过后 navigate('/usr/users')(BR13/D5);onNewnavigate('/usr/users/new')(BR14);loadFailed 渲染错误占位 +「点击重试」→reload() 或「返回列表」(spec § 4 loadError)。
  • 常量映射pages/usr/UserDetail/constants.ts):枚举/默认/错误码/文案/正则/MODE_*(见合同级常量)+ 上述 toCreateReq/toUpdateReq/userVoToFormValues 纯函数签名。

测试栈说明

  • jsdom 组件 / hook / api 单测(Vitest + RTL):组件测用 renderShell(已存在,Provider + 真实 store + MemoryRouter + AntD App/ConfigProvider);mode/:idrenderShell(<AppRouter/>, { initialEntries:['/usr/users/new' | '/usr/users/7'], preloadedAuth:{token:'t',user:ADMIN} }) 或直接渲染 UserDetailPage 包一层 Routes/MemoryRouter 注入路由参数;导航回流断言用 LocationProbe(复用 router.test.tsx/UserListPage.test.tsxuseLocation 探针模式)+ 列表路由哨兵。useUserDetailrenderHook(需 AntD App wrapper 提供 message,沿用 useUserList.test.tsx 的 wrapper 模式)。api 测桩底层 request 实例(沿用 usrApi.userlist.test.tsvi.mock('../../src/api/request') 模式);hook/页面测对 usrApivi.mock('../../src/api/usrApi') 桩(沿用 useUserList.test.tsx 模式)。
  • 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}。不依赖真实后端起服。
  • 可测性:优先语义查询(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)。

任务列表(每个 task = red → green → 子会话验证 → commit)

硬护栏:以下每个 impl_file / test_file 均以 frontend/ 开头;无任何 backend/ / sql/ / scripts/ 落点。 提交 scope 统一 usr;REQ tag:纯 create 链路标 REQ-USR-001,涉 edit(预填/更新)链路追加 REQ-USR-002(在 commit 行注明)。

T1 — 类型契约 + API 封装(createUser/updateUser/getUserDetail/listEmployees/listPermissions,D1/D2/D4)(jsdom api 测)

  • 测试先行类型:jsdom 组件测试(api 单测)
  • 1. 写失败测试frontend/tests/unit/usrApi.userdetail.test.ts(沿用 usrApi.userlist.test.tsvi.mock('../../src/api/request') 桩底层实例 post/put/get):
    • ::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)。
    • ::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)。
    • ::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===7employeeName==='张三'(复用归一),空 records 时返回 null
    • ::listEmployees normalizes iIncrement/sEmployeeName/sEmployeeNo to EmployeeOption——桩 resolve [{ iIncrement:3, sEmployeeName:'张三', sEmployeeNo:'zs' }],断言返回 [{ value:3, label:'张三', sEmployeeNo:'zs' }](D1)。
    • ::listPermissions normalizes to PermissionItem——桩 resolve [{ iIncrement:1, sPermissionName:'默认显示', sPermissionCategory:'基础' }],断言返回 [{ id:1, name:'默认显示', category:'基础' }](D2/D3)。 > FE-01/FE-03 既有 login/fetchCompanies/listUsers 用例不回归(同文件或既有文件保持绿)。
  • 2. 实现最小代码frontend/src/api/types.ts(新增 EmployeeOption/PermissionItem/UserCreateReq/UserUpdateReq/UserDetailMode,签名见关键签名;复用 FE-03 UserVO/PageResult/UserListQuery)+ frontend/src/api/usrApi.ts(新增 5 方法,签名见关键签名;getUserDetail 复用 listUserslistEmployees/listPermissions 做后端原始键归一)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- usrApi(含 FE-01/FE-03 usrApi 既有用例不回归)
  • 4. commitfeat(usr): 用户单据 API 与类型契约 create/update/detail/employees/permissions REQ-USR-001 REQ-USR-002

T2 — 页面常量 + 提交映射纯函数(枚举/默认/正则/错误码/文案 + toCreateReq/toUpdateReq/userVoToFormValues)(jsdom 单测)

  • 测试先行类型:jsdom 组件测试(纯逻辑断言)
  • 1. 写失败测试frontend/tests/unit/userDetailMappers.test.ts
    • ::constants enums and defaults——断言 USER_TYPE_OPTIONS===['普通用户','超级管理员']LANGUAGE_OPTIONS===['中文','英文','繁体']CREATE_DEFAULTS.sUserType==='普通用户'CREATE_DEFAULTS.iCanModifyBill===0USERNAME_PATTERN.test('ab_12')===trueUSERNAME_PATTERN.test('ab')===false(少于 3 位)、错误码 ERR_USERNAME_EXISTS===40901/ERR_USER_NOT_FOUND===40401/ERR_NO_PERMISSION===40301/ERR_VALIDATION===40001MODE_CREATE==='create'/MODE_EDIT==='edit'
    • ::toCreateReq maps form values + permissionIds (no password)——传 UserFormValues + [1,2],断言产出 UserCreateReqpermissionIds:[1,2]iCanModifyBill 为 0/1、不含 initialPassword/iIsVoid(create 无作废,BR9)。
    • ::toUpdateReq maps without sUserName + includes iIsVoid + full permissionIds——断言产出 UserUpdateReq 不含 sUserName(BR3),含 iIsVoidpermissionIds(全量覆盖 BR11)。
    • ::userVoToFormValues fills from UserVO——传 UserVO,断言回填 sUserName/sUserNo/sUserType/sLanguage/iCanModifyBill/iIsVoid 与 VO 一致(BR17)。
  • 2. 实现最小代码frontend/src/pages/usr/UserDetail/constants.ts(枚举/默认/正则/错误码/MODE_*/文案常量 + toCreateReq/toUpdateReq/userVoToFormValues 纯函数,签名见关键签名)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- userDetailMappers
  • 4. commitfeat(usr): 用户单据页面常量与提交映射纯函数 REQ-USR-001 REQ-USR-002

T3 — useUserDetail 单据 hook(状态机:initialLoading/editing/submitting/submitError/submitSuccess/loadError)(jsdom hook 测)

  • 测试先行类型:jsdom 组件测试(renderHookvi.mock('../../src/api/usrApi'),AntD App wrapper)
  • 1. 写失败测试frontend/tests/unit/useUserDetail.test.tsx
    • ::create mode initial load prefetches employees+permissions (initialLoading→editing)——listEmployees/listPermissions resolve 非空,mode:'create',断言挂载即并发取数、loading true→false、formValuesCREATE_DEFAULTScheckedPermissionIds 为空(spec § 3 initialLoading→editing / BR2/BR6/BR8)。
    • ::edit mode prefills from getUserDetail and pre-checks permissions——mode:'edit',userId:7,无 presetUser,桩 getUserDetail 返回含权限的 UserVO,断言 getUserDetail 被调、formValues 回填原值、checkedPermissionIds 含已授权(BR17/D4)。
    • ::edit mode with presetUser skips getUserDetail——传 presetUser,断言 getUserDetail 未被调、直接以 preset 回填(D4 备注)。
    • ::selectEmployee fills userNo/userName from employee (create)——selectEmployee(3),断言 iEmployeeId===3sUserName/sUserNo 按选中员工带出(BR5)。
    • ::toggle permission and toggleAll update checkedPermissionIds——togglePermission(1,true) 加入 1、toggleAll(true) 选全部、toggleAll(false) 清空(BR10/BR11)。
    • ::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)。
    • ::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)。
    • ::submit 40901 returns fieldError on sUserName——桩 createUser reject new ApiError(40901,...),断言返回 { ok:false, fieldError:{ field:'sUserName' } }(spec § 4 / BR3)。
    • ::submit 40401/40301/40001/network show message and return ok:false——分别 reject 对应 ApiError,断言 message.error 文案(按合同级常量)且返回 { ok:false }(spec § 4)。
    • ::loadError when prefetch fails sets loadFailed and message——桩 listPermissions reject,断言 loadFailed===true + message.error('权限列表加载失败')(spec § 4 loadError / D2);reload() 重新取数清 loadFailed。 > message 断言桩 AntD App.useApp().message(沿用 useUserList.test.tsx 的 wrapper/桩模式,TDD 期定一处)。
  • 2. 实现最小代码frontend/src/pages/usr/UserDetail/useUserDetail.ts(签名见关键签名;内部 useStatemode/formValues/employees/permissions/checkedPermissionIds/loading/submitting/error/loadFaileduseEffect 挂载并发预取 + edit 详情;错误码分流按合同级常量;复用 T1 api + T2 映射)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- useUserDetail
  • 4. commitfeat(usr): 用户单据 hook useUserDetail 状态机 REQ-USR-001 REQ-USR-002

T4 — UserBasicForm 表单网格(字段/默认/只读/枚举/必填+格式/员工联动,BR1-BR9)(jsdom 组件测)

  • 测试先行类型:jsdom 组件测试
  • 1. 写失败测试frontend/tests/unit/UserBasicForm.test.tsxrenderShell 提供 AntD App;测试内用 Form.useForm() wrapper 注入 form):
    • ::renders 8 labeled fields——断言创建时间/制单人/员工名/用户名/类型/语言/用户号/单据修改权限 8 个标签可见(文案逐字)。
    • ::create mode username editable with default empty; edit mode username disabled——mode='create' 用户名 Input 可编辑;mode='edit' 用户名框 disabled(BR3)。
    • ::create mode defaults usertype 普通用户——create 类型下拉默认显「普通用户」(BR6)。
    • ::username format rule rejects short/invalid and required when empty——create 态填「ab」触发校验报「用户名须为 3-20 位字母数字下划线」;清空报「请输入用户名」(BR3)。
    • ::userno required——用户号空提交报「请输入用户号」(BR4)。
    • ::usertype/language selects expose enum options only——展开类型断言 普通用户/超级管理员;展开语言断言 中文/英文/繁体(BR6/BR7,无自由输入)。
    • ::create mode creator shows 保存后自动生成——create 制单人只读区文本「保存后自动生成」(BR2);edit 态显 readonlyCreator
    • ::selecting employee calls onSelectEmployee——员工名下拉选中 → onSelectEmployee 收到 value(BR5)。
    • ::单据修改权限 checkbox default unchecked (create)——create 复选框默认未勾(BR8)。
  • 2. 实现最小代码frontend/src/pages/usr/UserDetail/UserBasicForm.tsx(签名见关键签名;3 列网格 Form;用户名 rules 含必填 + USERNAME_PATTERN,edit disabled;类型/语言/员工 Select;创建时间/制单人只读;员工 onChangeonSelectEmployee)+ UserDetail.module.css 表单样式(网格 --color-form-bg-edit、只读字段 --color-form-bg-readonly、文字 --color-form-fg、必填 * --color-error、网格线 --color-border、下拉 hover --color-form-bg-hover)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- UserBasicForm
  • 4. commitfeat(usr): 用户单据表单网格 UserBasicForm REQ-USR-001 REQ-USR-002

T5 — PermissionGroupList 权限分类勾选列表(渲染/勾选集合/全选 indeterminate/回勾,BR10/BR11/D3)(jsdom 组件测)

  • 测试先行类型:jsdom 组件测试
  • 1. 写失败测试frontend/tests/unit/PermissionGroupList.test.tsxrenderShell):
    • ::renders header 权限分类 and one row per permission——传 3 个 PermissionItem,断言表头「权限分类」+ 3 行复选框逐项名(D3)。
    • ::checked rows reflect checkedIds——checkedIds=[1],断言 id=1 行勾选、其余未勾(BR10/edit 回勾 BR17)。
    • ::toggling a row calls onToggle(id, checked)——点未勾行 → onToggle(id,true);点已勾行 → onToggle(id,false)(BR10/BR11)。
    • ::header select-all checked when all selected; indeterminate when partial——全勾时表头全选 checked、部分勾时 indeterminate、全不勾时未勾(半选语义)。
    • ::header toggle calls onToggleAll——点表头全选(当前未全选)→ onToggleAll(true);全选态再点 → onToggleAll(false)
    • ::empty permissions renders empty list (no rows)——permissions=[] 无数据行(loadError 占位由页面消费)。
  • 2. 实现最小代码frontend/src/pages/usr/UserDetail/PermissionGroupList.tsx(签名见关键签名;表头全选 Checkboxchecked/indeterminatecheckedIdspermissions 计算)+ 逐项行 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)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- PermissionGroupList
  • 4. commitfeat(usr): 用户单据权限分类勾选列表 PermissionGroupList REQ-USR-001 REQ-USR-002

T6 — PermissionTabs 权限页签条(权限组 active + 5 占位页签,D9)(jsdom 组件测)

  • 测试先行类型:jsdom 组件测试
  • 1. 写失败测试frontend/tests/unit/PermissionTabs.test.tsxrenderShell):
    • ::renders 权限组 active with children——传 children 哨兵元素,断言「权限组」页签存在且默认激活、children(权限列表)可见。
    • ::renders 5 placeholder tabs disabled——断言「客户查看权限/供应商查看权限/人员查看权限/工序查看权限/司机查看权限」5 个页签存在且 disabled(不渲染数据,D9)。
    • ::placeholder tabs do not show permission list——切到(被 disabled 无法切,或断言占位页签无权限行内容)确认占位(D9)。
  • 2. 实现最小代码frontend/src/pages/usr/UserDetail/PermissionTabs.tsx(签名见关键签名;AntD Tabs itemsgroup 项含 children,5 占位项 disabled:true)+ UserDetail.module.css 页签条样式(active 下划线 --color-primary、页签文字 --color-text、占位文字 --color-text-secondary、下边线 --color-border)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- PermissionTabs
  • 4. commitfeat(usr): 用户单据权限页签条 PermissionTabs REQ-USR-001

T7 — UserDetailToolbar 工具栏(保存/取消/新增 + 提交中禁用 + 占位按钮,BR12/BR13/BR14/BR15/D8/D10)(jsdom 组件测)

  • 测试先行类型:jsdom 组件测试
  • 1. 写失败测试frontend/tests/unit/UserDetailToolbar.test.tsxrenderShell):
    • ::renders 保存/取消/新增 + placeholder buttons + gear——断言保存/取消/新增可见,占位「删除/作废/重置密码/取消作废/功能」与齿轮可见(文案逐字 + 图标)。
    • ::click 保存 calls onSave / 取消 calls onCancel / 新增 calls onNew——分别点击触发对应回调(BR12/BR13/BR14)。
    • ::submitting disables 保存 and shows loading——submitting=true 时「保存」禁用且 loading,再点不触发 onSave(BR15)。
    • ::canSave=false disables 保存——canSave=false 时「保存」禁用。
    • ::placeholder buttons show 功能开发中 (no business callback)——点占位按钮/齿轮触发 message.info('功能开发中')、不触发 onSave/onCancel/onNew(D8)。
  • 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)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- UserDetailToolbar
  • 4. commitfeat(usr): 用户单据工具栏 UserDetailToolbar REQ-USR-001 REQ-USR-002

T8 — UserDetailPage 页面集成 + 路由接线(create/edit 贯通 + 提交回流 + 错误就近 + 取数失败,BR3/BR12/BR16/BR17/D4/D5)(jsdom 组件测)

  • 测试先行类型:jsdom 组件测试
  • 1. 写失败测试frontend/tests/unit/UserDetailPage.test.tsxrenderShellvi.mock('../../src/api/usrApi') 桩 5 方法;用 LocationProbe + /usr/users 列表哨兵验回流;用 MemoryRouter initialEntries 注入 /usr/users/new/usr/users/7):
    • ::create mode renders empty form with defaults——进 /usr/users/new,挂载预取员工/权限、渲染空表单 + 默认普通用户 + 制单人「保存后自动生成」(BR2/BR6/initialLoading→editing)。
    • ::create submit success navigates to /usr/users with success——填合法字段(用户名「zhangsan」/用户号/语言 + 勾权限),点保存 → createUser 以表单值被调、message.success('用户创建成功')、URL/哨兵回 /usr/users(BR12/BR16)。
    • ::create username format invalid blocks submit——填「ab」点保存 → 校验拦截、createUser 未被调、就近报错「用户名须为 3-20 位字母数字下划线」(BR3)。
    • ::create 40901 highlights username field——桩 createUser reject ApiError(40901),点保存 → 用户名 Form.Item 就近报「用户名已存在,请更换」+ message.error(spec § 4)。
    • ::edit mode prefills from getUserDetail and username disabled——进 /usr/users/7,桩 getUserDetail 返回 UserVO,断言表单回填原值、用户名框禁用、权限按已授权回勾(BR17/BR3/D4)。
    • ::edit submit success navigates to /usr/users with 保存成功——edit 改类型后保存 → updateUser(7, ...) 被调、message.success('保存成功')、回流 /usr/users(BR16)。
    • ::cancel with dirty form confirms then navigates——改过字段后点取消 → 弹「放弃未保存的修改?」,确认后回 /usr/users(BR13/D5)。
    • ::新增 navigates to /usr/users/new——点工具栏「新增」→ URL/哨兵到 /usr/users/new(BR14)。
    • ::loadError shows retry; retry calls reload——桩 listPermissions reject → 可见「加载失败,点击重试」或对应 loadError 占位;点重试再次取数(spec § 4)。
    • ::edit 40401 offers 返回列表——桩 getUserDetail 返回 null(或 updateUser reject 40401)→ 提示「该用户不存在或已被删除」+「返回列表」入口回 /usr/users(spec § 4)。
  • 2. 实现最小代码frontend/src/pages/usr/UserDetail/index.tsxUserDetailPageuseParams/useLocation/useNavigate + Form.useForm() + useUserDetail,装配 4 子组件,onSavevalidateFieldssubmitokmessage.success+navigate('/usr/users')fieldErrorform.setFields 就近,onCancel→脏检 Modal.confirmnavigateonNewnavigate('/usr/users/new')loadFailed 渲染重试/返回列表);改 frontend/src/router/index.tsx——把 /usr/users/new/usr/users/:idUserDetailPlaceholder 替换为 UserDetailPage(移除占位组件,import 真实页;FE-03 /usr/users 不动)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- UserDetailPage router(确认 FE-02 router.test.tsx 不回归——若其断言 /usr/users/new/:id 渲染占位 data-testid,需同步更新为真实页可定位元素并在 commit 说明)
  • 4. commitfeat(usr): 用户单据页面集成与路由接线 UserDetailPage REQ-USR-001 REQ-USR-002

T9 — E2E 用户单据关键旅程(Playwright)

  • 测试先行类型:Playwright E2E
  • 1. 写失败测试frontend/tests/e2e/userdetail.spec.ts(沿用 shell.spec.ts/userlist.spec.ts 登录桩;page.route**/api/usr/employees**/**/api/usr/permissions**/**/api/usr/users**):
    • ::create user and return to list——进 /usr/users/new,填合法字段 + 勾权限,桩 POST 返回 {code:0,data:{id:9}},点保存 → 断言 message「用户创建成功」+ URL 回 /usr/users(用 page.waitForRequest 校验 POST body 含 sUserName)。
    • ::edit user prefill then save——桩 GET /usr/users(等于匹配)返回单条用户,进 /usr/users/7,断言用户名框值回填且禁用,改语言后保存(桩 PUT 返回 code:0)→ message「保存成功」+ 回 /usr/users
    • ::username conflict shows inline error——桩 POST 返回 {code:40901},提交 → 断言用户名字段就近报「用户名已存在,请更换」。
    • ::load error shows retry——桩 GET /usr/permissions 5xx,进 /usr/users/new → 断言「加载失败,点击重试」可见;点重试(改桩为成功)后表单可用。
    • ::placeholder tabs/buttons are inert——断言 5 个查看权限页签 disabled、点「删除」出现「功能开发中」(D8/D9)。
  • 2. 实现最小代码:补任何为可测性需要的最小 data-testid(仅 Playwright 无法稳定定位时,如 userdetail-page/btn-save/field-username/perm-list/userdetail-loaderror)。沿用 playwright.config.ts
  • 3. 子会话验证 PASScd frontend && npm run test:e2e -- userdetail
  • 4. committest(usr): 用户单据 E2E 关键旅程 REQ-USR-001 REQ-USR-002

T10 — 全量门禁回归 + 收尾(chore)

  • 测试先行类型:无新增测试(全量验证)
  • 1. 写失败测试:无。
  • 2. 实现最小代码:修 lint / build(tsc --noEmit)/ 类型问题;确认语义色全部 var(--color-*)、无硬编码 hex/rgba(工具栏深色装饰 scoped 例外,D10);确认无 TBD/TODO/【人工填写】;确认 FE-01/FE-02/FE-03 既有单测/E2E 不回归(尤其 router.test.tsxshell.spec.tsusrApi.userlist.test.tsuseUserList.test.tsx)。
  • 3. 子会话验证 PASScd frontend && npm run lint && npm run build && npm run test:unit && npm run test:e2e 全绿。
  • 4. commitchore(usr): FE-04 门禁回归通过 REQ-USR-001 REQ-USR-002

完成判据(Definition of Done)

  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)。
  2. 状态机覆盖并测试固化:initialLoading(T3/T8)、editing(T3/T4/T8)、submitting(T3/T7)、submitError(T3/T8)、submitSuccess(T3/T8)、loadError(T3/T8/T9)(spec § 3)。
  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)。
  4. 字段定义/提交映射对齐 REQ 表 1/表 2 + docs/05 请求体 + 原型(8 字段 + 权限 permissionIds,create 不传密码、edit 不传 sUserName,全量覆盖语义,spec § 6 / T1/T2);UserVO(FE-03)/EmployeeOption/PermissionItem 字段映射一致(D1/D2/D3)。
  5. API 经 usrApi.*request.ts(拆 ResultApiError 分流、被动 401 统一登出复用 FE-01/FE-02),页面不散用 axios(docs/04 § 2.3);单据态在页面 hook 不进 Redux(docs/04 § 2.2 / spec D7)。
  6. 错误码分流文案对齐 spec § 4(40901 用户名就近高亮、40401 返回列表、40001/40301 message、网络兜底;loadError 重试入口;被动 401 统一跳登录)。
  7. 占位项不杜撰后端端点:工具栏删除/重置密码/功能 + 5 个查看权限页签 + 设置齿轮均占位(message.info('功能开发中')/disabled),作废/取消作废经 PUT iIsVoid 承载(spec D8/D9)。
  8. 语义色只用 var(--color-*),AntD colorPrimary 沿用 FE-01 ConfigProvider;工具栏深色底 scoped 装饰不新增全局 token、不挪用语义 token(spec § 7 / D10)。
  9. 全部落点在 frontend/**,无 backend/ / sql/ / scripts/ 改动;改 router/index.tsx(占位换真实页)、usrApi.ts/types.ts(增写端点契约)属共享骨架,已在《模块完成报告》留痕。
  10. 门禁全绿:npm run lint / npm run build / npm run test:unit / npm run test:e2e(docs/04 § 零)。

自审记录

  • 占位符扫描:本计划无 【人工填写:】 / TBD / TODO 真实占位(正文 TBD/TODO 仅作为「禁止出现的字样」被引用)。
  • 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)。
  • 类型一致性EmployeeOption/PermissionItem/UserCreateReq/UserUpdateReq/UserDetailMode/UserFormValues(跨 T1/T2/T3/T4/T5/T8 一致)、复用 FE-03 UserVO/PageResult/UserListQueryUSER_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
  • 作用域自审:所有 impl_file / test_file 均以 frontend/ 开头;无 backend/ / sql/ / scripts/ 落点。改 src/router/index.tsxsrc/api/usrApi.tssrc/api/types.ts 属 FE 共享骨架扩展(非新阶段越界),已在架构段与 DoD 第 9 条登记留痕要求。
  • 本计划承接的跨阶段待对齐项(自 spec § 8 末注):D1/D2 的支撑只读端点 GET /api/usr/employeesGET /api/usr/permissions 须在后端编码期于 REQ-USR-001/002 后端实现内补齐契约(如同 FE-01 GET /api/usr/companies 先例);前端 E2E/单测以桩覆盖,不依赖真实后端起服,故本 FE 可独立按 TDD 红绿推进。