FE-04 用户信息单据 — 任务级 TDD 计划(前端)
阶段:前端(frontend)。作用域:
frontend/**(页面 / api / 类型 / 样式 / 测试)。禁止写backend/**/sql/**/scripts/**。 上游 SSoT:specdocs/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。 复用资产: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)。 本计划告诉 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-02AppLayout的<Outlet/>内):按路由:id判定mode(有:id→edit,/new→create);纵向组合UserDetailToolbar+UserBasicForm+PermissionTabs+PermissionGroupList;态由页面本地 hookuseUserDetail持有(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-03UserVO)。 -
状态机 ≥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=0→message.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-03UserVO/PageResult,不改 FE-01/FE-03 既有login/fetchCompanies/listUsers/既有类型;属共享骨架扩展」。 -
状态管理(docs/04 § 2.2 / spec D7):单据态(mode/formValues/employees/permissions/checkedPermissionIds/loading/submitting/error)为页面就近态,用
useUserDetailhook +useState本地管理,不进 Redux;登录态复用 FE-01authSlice(本页只读当前用户名用于 create 态制单人占位,可选)。 -
请求封装 / 错误处理(docs/04 § 2.3 / § 2.4):写/读均走
usrApi.*→request.tsAxios 实例(响应拦截器已拆Result:code=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/icons(SaveOutlined/CloseCircleOutlined/PlusCircleOutlined/SettingOutlined)。 -
不新增 npm 依赖:复用 FE-01/FE-03 既装依赖(见
frontend/package.jsondependencies);前置校验用 AntDFormrules+ 正则常量,无需额外校验库。 - 测试:单测 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**)。 - 命令(docs/04 § 零):build
npm run build(tsc --noEmit && vite build);lintnpm run lint;unitnpm run test:unit(vitest run);e2enpm run test:e2e(playwright test)。子会话验证用cd frontend && npm run test:unit -- <文件名片段>。 - 提交格式:
<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。
合同级常量(跨 task 必须一致)
-
API 路径 / 方法(
usrApi内传相对路径,request.tsbaseURL=/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-03listUsers,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 常量);401由request.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-01request.ts已导出,本页不直接读写,由request.ts拦截器注入)。
关键签名(首次出现处给出,跨 task 一致)
-
类型契约(
api/types.ts,新增;复用 FE-03UserVO/PageResult/UserListQuery不动):-
EmployeeOption:{ value: number; label: string; sEmployeeNo: string | null }(员工名下拉项;value映射后端iIncrement、label映射sEmployeeName、sEmployeeNo供联动带出用户号,D1/BR5)。 -
PermissionItem:{ id: number; name: string; category: string }(权限项;id映射后端iIncrement、name映射sPermissionName、category映射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-03as 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:1调listUsers,返回records[0] ?? null(复用 FE-03listUsers与中文键归一,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)。
-
-
页面 hook(
pages/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态额外按userId用presetUser ?? 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[]): UserCreateReq、toUpdateReq(values: UserFormValues, permissionIds: number[]): UserUpdateReq、userVoToFormValues(vo: UserVO): UserFormValues(edit 回填,BR17)。
-
-
组件 props(
pages/usr/UserDetail/):-
UserDetailToolbar({ mode: UserDetailMode; submitting: boolean; canSave: boolean; onSave(): void; onCancel(): void; onNew(): void }):保存(type="primary",submitting时loading+disabledBR15,canSave控可点)/ 取消(BR13)/ 新增(BR14)/ 占位按钮(删除/作废/重置密码/取消作废/功能 + 齿轮,点击message.info('功能开发中'),D8)。 -
UserBasicForm({ form: FormInstance; mode: UserDetailMode; employees: EmployeeOption[]; readonlyCreateTime?: string; readonlyCreator?: string; onSelectEmployee(value: number | null): void }):AntDForm(受form实例控制,3 列网格布局);渲染 8 字段(spec § 2 控件选型);create 态用户名可编辑(rules含必填 +USERNAME_PATTERN)、edit 态用户名disabled(BR3);类型/语言/员工名Select(options 来自常量/employees);创建时间/制单人只读展示(create 制单人显「保存后自动生成」BR2,edit 回填readonlyCreator/readonlyCreateTimeBR1/BR2);员工名onChange→onSelectEmployee(BR5)。 -
PermissionTabs({ activeKey?: string; onChange?(key: string): void; children: ReactNode }):AntDTabs,第一项权限组(key=group)承载children(权限列表),其余 5 个客户查看权限…司机查看权限为disabled占位页签(D9)。 -
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)。 -
UserDetailPage(default export,无 props):useParams<{ id?: string }>()判mode(有id→edit、userId=Number(id);否则create)+useLocation()(取state.user作presetUser,D4 备注)+useNavigate()+ AntDForm.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)。
-
-
常量映射(
pages/usr/UserDetail/constants.ts):枚举/默认/错误码/文案/正则/MODE_*(见合同级常量)+ 上述toCreateReq/toUpdateReq/userVoToFormValues纯函数签名。
测试栈说明
-
jsdom 组件 / hook / api 单测(Vitest + RTL):组件测用
renderShell(已存在,Provider + 真实 store +MemoryRouter+ AntDApp/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(需 AntDAppwrapper 提供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模式)。 -
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.Itemlabel);仅当无法稳定定位时加最小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.ts的vi.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.getresolve{ 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.getquery 含matchType:'等于', pageNum:1, pageSize:1,返回对象id===7、employeeName==='张三'(复用归一),空 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-03UserVO/PageResult/UserListQuery)+frontend/src/api/usrApi.ts(新增 5 方法,签名见关键签名;getUserDetail复用listUsers;listEmployees/listPermissions做后端原始键归一)。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- usrApi(含 FE-01/FE-03usrApi既有用例不回归) - 4. commit:
feat(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===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'。 -
::toCreateReq maps form values + permissionIds (no password)——传UserFormValues+[1,2],断言产出UserCreateReq含permissionIds:[1,2]、iCanModifyBill为 0/1、不含initialPassword/iIsVoid(create 无作废,BR9)。 -
::toUpdateReq maps without sUserName + includes iIsVoid + full permissionIds——断言产出UserUpdateReq不含sUserName(BR3),含iIsVoid、permissionIds(全量覆盖 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. 子会话验证 PASS:
cd frontend && npm run test:unit -- userDetailMappers - 4. commit:
feat(usr): 用户单据页面常量与提交映射纯函数 REQ-USR-001 REQ-USR-002
T3 — useUserDetail 单据 hook(状态机:initialLoading/editing/submitting/submitError/submitSuccess/loadError)(jsdom hook 测)
-
测试先行类型:jsdom 组件测试(
renderHook,vi.mock('../../src/api/usrApi'),AntDAppwrapper) - 1. 写失败测试:
frontend/tests/unit/useUserDetail.test.tsx:-
::create mode initial load prefetches employees+permissions (initialLoading→editing)——listEmployees/listPermissionsresolve 非空,mode:'create',断言挂载即并发取数、loadingtrue→false、formValues为CREATE_DEFAULTS、checkedPermissionIds为空(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===3且sUserName/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',桩createUserresolve{id:9},submit(values)返回{ ok:true, id:9 },期间submittingtrue→false(BR12/BR15)。 -
::submit edit calls updateUser with userId and full permissionIds——mode:'edit',userId:7,桩updateUserresolve{id:7},断言以(7, UserUpdateReq含permissionIds)调用,返回{ ok:true, id:7 }(BR11/BR12)。 -
::submit 40901 returns fieldError on sUserName——桩createUserrejectnew 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——桩listPermissionsreject,断言loadFailed===true+message.error('权限列表加载失败')(spec § 4 loadError / D2);reload()重新取数清loadFailed。 >message断言桩 AntDApp.useApp().message(沿用useUserList.test.tsx的 wrapper/桩模式,TDD 期定一处)。
-
- 2. 实现最小代码:
frontend/src/pages/usr/UserDetail/useUserDetail.ts(签名见关键签名;内部useState持mode/formValues/employees/permissions/checkedPermissionIds/loading/submitting/error/loadFailed,useEffect挂载并发预取 + edit 详情;错误码分流按合同级常量;复用 T1 api + T2 映射)。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- useUserDetail - 4. commit:
feat(usr): 用户单据 hook useUserDetail 状态机 REQ-USR-001 REQ-USR-002
T4 — UserBasicForm 表单网格(字段/默认/只读/枚举/必填+格式/员工联动,BR1-BR9)(jsdom 组件测)
- 测试先行类型:jsdom 组件测试
- 1. 写失败测试:
frontend/tests/unit/UserBasicForm.test.tsx(renderShell提供 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,editdisabled;类型/语言/员工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)。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- UserBasicForm - 4. commit:
feat(usr): 用户单据表单网格 UserBasicForm REQ-USR-001 REQ-USR-002
T5 — PermissionGroupList 权限分类勾选列表(渲染/勾选集合/全选 indeterminate/回勾,BR10/BR11/D3)(jsdom 组件测)
- 测试先行类型:jsdom 组件测试
- 1. 写失败测试:
frontend/tests/unit/PermissionGroupList.test.tsx(renderShell):-
::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(签名见关键签名;表头全选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)。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- PermissionGroupList - 4. commit:
feat(usr): 用户单据权限分类勾选列表 PermissionGroupList REQ-USR-001 REQ-USR-002
T6 — PermissionTabs 权限页签条(权限组 active + 5 占位页签,D9)(jsdom 组件测)
- 测试先行类型:jsdom 组件测试
- 1. 写失败测试:
frontend/tests/unit/PermissionTabs.test.tsx(renderShell):-
::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(签名见关键签名;AntDTabsitems:group项含children,5 占位项disabled:true)+UserDetail.module.css页签条样式(active 下划线--color-primary、页签文字--color-text、占位文字--color-text-secondary、下边线--color-border)。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- PermissionTabs - 4. commit:
feat(usr): 用户单据权限页签条 PermissionTabs REQ-USR-001
T7 — UserDetailToolbar 工具栏(保存/取消/新增 + 提交中禁用 + 占位按钮,BR12/BR13/BR14/BR15/D8/D10)(jsdom 组件测)
- 测试先行类型:jsdom 组件测试
- 1. 写失败测试:
frontend/tests/unit/UserDetailToolbar.test.tsx(renderShell):-
::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. 子会话验证 PASS:
cd frontend && npm run test:unit -- UserDetailToolbar - 4. commit:
feat(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.tsx(renderShell,vi.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——桩createUserrejectApiError(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——桩listPermissionsreject → 可见「加载失败,点击重试」或对应 loadError 占位;点重试再次取数(spec § 4)。 -
::edit 40401 offers 返回列表——桩getUserDetail返回null(或updateUserreject 40401)→ 提示「该用户不存在或已被删除」+「返回列表」入口回/usr/users(spec § 4)。
-
- 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不动)。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- UserDetailPage router(确认 FE-02router.test.tsx不回归——若其断言/usr/users/new/:id渲染占位data-testid,需同步更新为真实页可定位元素并在 commit 说明) - 4. commit:
feat(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/permissions5xx,进/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. 子会话验证 PASS:
cd frontend && npm run test:e2e -- userdetail - 4. commit:
test(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.tsx、shell.spec.ts、usrApi.userlist.test.ts、useUserList.test.tsx)。 - 3. 子会话验证 PASS:
cd frontend && npm run lint && npm run build && npm run test:unit && npm run test:e2e全绿。 - 4. commit:
chore(usr): FE-04 门禁回归通过 REQ-USR-001 REQ-USR-002
完成判据(Definition of Done)
-
/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)。 - 状态机覆盖并测试固化:
initialLoading(T3/T8)、editing(T3/T4/T8)、submitting(T3/T7)、submitError(T3/T8)、submitSuccess(T3/T8)、loadError(T3/T8/T9)(spec § 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)。
- 字段定义/提交映射对齐 REQ 表 1/表 2 + docs/05 请求体 + 原型(8 字段 + 权限
permissionIds,create 不传密码、edit 不传sUserName,全量覆盖语义,spec § 6 / T1/T2);UserVO(FE-03)/EmployeeOption/PermissionItem字段映射一致(D1/D2/D3)。 - API 经
usrApi.*→request.ts(拆Result、ApiError分流、被动 401 统一登出复用 FE-01/FE-02),页面不散用 axios(docs/04 § 2.3);单据态在页面 hook 不进 Redux(docs/04 § 2.2 / spec D7)。 - 错误码分流文案对齐 spec § 4(40901 用户名就近高亮、40401 返回列表、40001/40301 message、网络兜底;loadError 重试入口;被动 401 统一跳登录)。
- 占位项不杜撰后端端点:工具栏删除/重置密码/功能 + 5 个查看权限页签 + 设置齿轮均占位(
message.info('功能开发中')/disabled),作废/取消作废经 PUTiIsVoid承载(spec D8/D9)。 - 语义色只用
var(--color-*),AntDcolorPrimary沿用 FE-01ConfigProvider;工具栏深色底 scoped 装饰不新增全局 token、不挪用语义 token(spec § 7 / D10)。 - 全部落点在
frontend/**,无backend//sql//scripts/改动;改router/index.tsx(占位换真实页)、usrApi.ts/types.ts(增写端点契约)属共享骨架,已在《模块完成报告》留痕。 - 门禁全绿:
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-03UserVO/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-01request.ts。 -
作用域自审:所有
impl_file/test_file均以frontend/开头;无backend//sql//scripts/落点。改src/router/index.tsx、src/api/usrApi.ts、src/api/types.ts属 FE 共享骨架扩展(非新阶段越界),已在架构段与 DoD 第 9 条登记留痕要求。 -
本计划承接的跨阶段待对齐项(自 spec § 8 末注):D1/D2 的支撑只读端点
GET /api/usr/employees、GET /api/usr/permissions须在后端编码期于 REQ-USR-001/002 后端实现内补齐契约(如同 FE-01GET /api/usr/companies先例);前端 E2E/单测以桩覆盖,不依赖真实后端起服,故本 FE 可独立按 TDD 红绿推进。