# FE-02 用户管理 Implementation Plan > **Execution:** Parent skill `fe-feature-tdd` executes this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 实现 3 个页面 + 路由:`/users` 列表(GET list 分页 + 筛选)、`/users/new` 新增表单(POST create)、`/users/:userId` 编辑表单(GET detail + PUT update)。所有页面强制 SUPER_ADMIN 守卫。 **Architecture:** - 列表页用 AntD `Table` + `Pagination` + 顶部 filter `Form`;表单页用 `Form.Item` + `Select` + `Input` + 权限分类 `Checkbox.Group`。 - 新增 / 编辑共用同一 `UserFormPage`,按路由参数 `userId` 切模式(new = create / 有 id = edit)。 - API 客户端按 spec § 四 提供 `usersApi.{list, get, create, update}`,复用 FE-01 的 `apiClient` + `BizError`。 - 路由级 `RequireSuperAdmin` 守卫(基于 LoginContext userType 派生于 Redux auth slice)。 **Tech Stack:** 复用 FE-01(React 18 / AntD 5 / Redux Toolkit / React Router v6 / Axios / Vitest + RTL + MSW)。 --- ## 文件结构(全部 `frontend/` 下,与 docs/09 § 三对齐) **通用层增量**: - `frontend/src/api/users.ts`(usersApi 4 个函数 + LoginReq/Vo 复用 FE-01 types) - `frontend/src/router/RequireSuperAdmin.tsx`(守卫:必须已登录 + userType=SUPER_ADMIN) - `frontend/src/router/index.tsx`(追加 /users/new + /users/:userId 路由) **业务层(pages/users/)**: - `frontend/src/pages/users/UsersListPage.tsx` - `frontend/src/pages/users/UsersToolbar.tsx` - `frontend/src/pages/users/UsersFilterBar.tsx` - `frontend/src/pages/users/UsersTable.tsx` - `frontend/src/pages/users/UserFormPage.tsx` - `frontend/src/pages/users/UserFormFields.tsx` - `frontend/src/pages/users/UserPermissionPanel.tsx` - `frontend/src/pages/users/usersConstants.ts`(白名单常量 + fixture employee/permission options + 错误文案) **测试**: - `frontend/src/api/users.test.ts` - `frontend/src/router/RequireSuperAdmin.test.tsx` - `frontend/src/pages/users/UsersListPage.test.tsx` - `frontend/src/pages/users/UserFormPage.test.tsx` - `frontend/tests/e2e/users.spec.ts`(`.fixme()` 跳过同 FE-01,留作手工验收) **MSW 增量**: - `frontend/src/test-utils/msw-handlers.ts` 增加 `/api/v1/users` 4 个端点的模拟 --- ## 约束常量(跨任务一致) **usersApi 类型 / 签名**(`api/users.ts`): ```ts interface UserListItem { userId: number; username: string; employeeName?: string | null; userCode: string; departmentName?: string | null; userType: 'NORMAL' | 'SUPER_ADMIN'; language: string; isDeleted: boolean; lastLoginDate?: string | null; createdBy?: string | null; createdDate?: string | null; } interface UserDetail extends UserListItem { employeeId?: number | null; permissionCategoryIds: number[]; updatedBy?: string | null; updatedDate?: string | null; } interface UsersListQuery { page?: number; size?: number; sortField?: 'tCreateDate' | 'tLastLoginDate' | 'sUsername' | 'sUserCode'; sortOrder?: 'asc' | 'desc'; queryField?: string; matchMode?: 'contains' | 'notContains' | 'equals'; queryValue?: string; userType?: 'NORMAL' | 'SUPER_ADMIN'; isDeleted?: boolean; } interface PageResult { records: T[]; total: number; page: number; size: number; } interface CreateUserReq { username: string; userCode: string; userType: 'NORMAL' | 'SUPER_ADMIN'; language: 'zh-CN' | 'en-US' | 'zh-TW'; canEditDocument: boolean; employeeId?: number; permissionCategoryIds?: number[]; } interface UpdateUserReq { userCode?: string; userType?: 'NORMAL' | 'SUPER_ADMIN'; language?: 'zh-CN' | 'en-US' | 'zh-TW'; canEditDocument?: boolean; employeeId?: number; // 0 = 解除关联 isDeleted?: boolean; permissionCategoryIds?: number[]; } usersApi.list(query: UsersListQuery): Promise> usersApi.get(userId: number): Promise usersApi.create(req: CreateUserReq): Promise<{ userId: number; username: string; userCode: string }> usersApi.update(userId: number, req: UpdateUserReq): Promise ``` **路由配置**: ``` /users → /users/new → /users/:userId → ``` **错误码 → 文案映射**(`usersConstants.ts`): ``` 40001: '请检查字段格式' 40004: '员工或权限分类不存在或已删除' 40101: '会话失效,请重新登录' 40301: '权限不足,仅超级管理员可调用' 40302: '不允许停用当前登录用户自己' 40401: '用户不存在' 40901: '用户名已存在' 40902: '用户号已被占用' NETWORK: '网络异常,请检查连接后重试' UNKNOWN: '操作失败,请稍后重试' ``` **Fixture 常量**(暂用,待后端补 employees / permission-categories 端点后改 API 拉取): ```ts EMPLOYEE_OPTIONS = [{ value: 0, label: '(无 / 解除关联)' }, { value: 1, label: '张三 (E001)' }] PERMISSION_CATEGORY_OPTIONS = [ { value: 1, label: 'PUR 采购管理' }, { value: 2, label: 'SAL 销售管理' }, ] USER_TYPE_OPTIONS = [ { value: 'NORMAL', label: '普通用户' }, { value: 'SUPER_ADMIN', label: '超级管理员' }, ] LANGUAGE_OPTIONS = [ { value: 'zh-CN', label: '中文' }, { value: 'en-US', label: '英文' }, { value: 'zh-TW', label: '繁体' }, ] QUERY_FIELD_OPTIONS = [ { value: 'username', label: '用户名' }, { value: 'employeeName', label: '员工名' }, { value: 'userCode', label: '用户号' }, { value: 'departmentName', label: '部门' }, { value: 'userType', label: '用户类型' }, { value: 'isDeleted', label: '作废' }, { value: 'createdBy', label: '制单人' }, ] MATCH_MODE_OPTIONS = [ { value: 'contains', label: '包含' }, { value: 'notContains', label: '不包含' }, { value: 'equals', label: '等于' }, ] ``` --- ## 任务步骤 ### Task 1: usersApi 4 个函数 + MSW handlers **Files:** - Create: `frontend/src/api/users.ts` - Modify: `frontend/src/test-utils/msw-handlers.ts`(追加 4 个 /api/v1/users 端点) - Test: `frontend/src/api/users.test.ts` - 测试先行类型: jsdom 组件测试 + MSW **API shape**:见"约束常量" MSW 增量 handler 规则: - `GET /api/v1/users` → 返回 fixture:3 个用户(alice / admin / bob_deleted),支持 page/size/queryField/queryValue 过滤 - `GET /api/v1/users/:userId` → 返回 fixture 详情;id=99999 返 40401 - `POST /api/v1/users` → 默认成功;username='dup' 返 40901;userCode='dup-code' 返 40902;userType='ROOT' 返 40001 - `PUT /api/v1/users/:userId` → 默认成功返回 detail;isDeleted=true && userId=admin(self)返 40302 - [ ] **Step 1: 写失败测试**:6 个用例(list 成功 + filter 命中 / get 成功 / get 不存在 40401 / create 成功 / create 用户名冲突 40901 / update 成功) - [ ] **Step 2: 实现最小代码** - [ ] **Step 3: 子会话验证 PASS** - [ ] **Step 4: Commit** `feat(frontend): usersApi 4 个函数 + MSW handlers REQ_ID: FE-02` ### Task 2: RequireSuperAdmin 守卫 **Files:** - Create: `frontend/src/router/RequireSuperAdmin.tsx` - Test: `frontend/src/router/RequireSuperAdmin.test.tsx` - 测试先行类型: jsdom 组件测试 **API shape**: - `{children}` - 行为:① 未登录 → ``(复用 RequireAuth 语义);② 已登录但 userType !== 'SUPER_ADMIN' → 渲染 `` 而非 navigate(区分场景:让用户知道是权限问题而非未登录) - [ ] **Step 1: 写失败测试**:3 个用例(无 token → /login;NORMAL token → Result 403;SUPER_ADMIN token → 渲染 children) - [ ] **Step 2: 实现最小代码** - [ ] **Step 3: 子会话验证 PASS** - [ ] **Step 4: Commit** `feat(frontend): RequireSuperAdmin 守卫 REQ_ID: FE-02` ### Task 3: usersConstants + UsersFilterBar 组件 **Files:** - Create: `frontend/src/pages/users/usersConstants.ts` - Create: `frontend/src/pages/users/UsersFilterBar.tsx` - Test: `frontend/src/pages/users/UsersFilterBar.test.tsx` - 测试先行类型: jsdom 组件测试 **API shape**: - `) => void} onReset={() => void} disabled={boolean} />` - 内部:queryField + matchMode + queryValue 三个字段;搜索按钮触发 onSearch;清空按钮触发 onReset 并 form.resetFields - [ ] **Step 1: 写失败测试**: - `renders 3 controls + 2 buttons` - `clickSearch_callsOnSearch_withFormValues` - `clickReset_resetsFieldsAndCallsOnReset` - `disabled_disablesAllControls` - [ ] **Step 2: 实现最小代码** - [ ] **Step 3: 子会话验证 PASS** - [ ] **Step 4: Commit** `feat(frontend): UsersFilterBar + usersConstants REQ_ID: FE-02` ### Task 4: UsersToolbar + UsersTable 组件 **Files:** - Create: `frontend/src/pages/users/UsersToolbar.tsx` - Create: `frontend/src/pages/users/UsersTable.tsx` - Test: `frontend/src/pages/users/UsersTable.test.tsx` - 测试先行类型: jsdom 组件测试 **API shape**: - ` void} onAdd={() => void} />`("导出 Excel" 按钮 visually 保留但 disabled,无 onClick) - ` void} onPageChange={(page: number, size: number) => void} onSortChange={(sortField: string, sortOrder: 'asc'|'desc') => void} />` - Table columns: 序号 + 11 个字段 + 操作("编辑"链接 → onRowClick) - [ ] **Step 1: 写失败测试**: - `Toolbar_callsOnAdd_onAddClick` - `Toolbar_callsOnRefresh_onRefreshClick` - `Toolbar_exportButton_isDisabled` - `Table_rendersAllColumns_andRows` - `Table_rowClick_callsOnRowClick` - `Table_paginationChange_callsOnPageChange` - [ ] **Step 2: 实现最小代码** - [ ] **Step 3: 子会话验证 PASS** - [ ] **Step 4: Commit** `feat(frontend): UsersToolbar + UsersTable REQ_ID: FE-02` ### Task 5: UsersListPage 整合 + 错误状态机 **Files:** - Create: `frontend/src/pages/users/UsersListPage.tsx` - Test: `frontend/src/pages/users/UsersListPage.test.tsx` - Modify: `frontend/src/router/index.tsx`(追加 /users 用 RequireSuperAdmin 包裹;替换之前的 placeholder) - 测试先行类型: jsdom 组件测试 **API shape**: - `` 无 props - 内部状态: `{ query, records, total, loading, error }`;挂载触发 list;filter/page/sort change 触发 list - 错误处理:40301 全屏 Result;网络错 banner + 重试 - [ ] **Step 1: 写失败测试**: - `mountFetchesList_andRendersRecords` - `filterSubmit_refetches_withQueryParams` - `paginationChange_refetches_withNewPage` - `rowClick_navigatesToUserDetail` - `addClick_navigatesToUserNew` - `networkError_showsBannerAndRetryButton` - `forbidden_403_showsResultPage` - [ ] **Step 2: 实现最小代码** - [ ] **Step 3: 子会话验证 PASS** - [ ] **Step 4: Commit** `feat(frontend): UsersListPage 整合 + 错误状态机 REQ_ID: FE-02` ### Task 6: UserFormFields + UserPermissionPanel 子组件 **Files:** - Create: `frontend/src/pages/users/UserFormFields.tsx` - Create: `frontend/src/pages/users/UserPermissionPanel.tsx` - Test: `frontend/src/pages/users/UserFormFields.test.tsx` - 测试先行类型: jsdom 组件测试 **API shape**: - `` - Form.Item: 用户名(create 模式可编辑,edit readonly)/ 用户号 / 类型 / 语言 / 单据修改权限 / 员工名 - 用 AntD `Form.useFormInstance()` hook 由父组件控制 - ` void} disabled={boolean} />` - 单 Tab "权限组"(其他 Tab disabled);权限分类列表 Checkbox.Group - [ ] **Step 1: 写失败测试**: - `UserFormFields_create_usernameIsEditable` - `UserFormFields_edit_usernameIsReadonly` - `UserFormFields_invalidUsername_showsPatternError` - `UserPermissionPanel_renders_singleActiveTab` - `UserPermissionPanel_toggleCheckbox_callsOnChange` - [ ] **Step 2: 实现最小代码** - [ ] **Step 3: 子会话验证 PASS** - [ ] **Step 4: Commit** `feat(frontend): UserFormFields + UserPermissionPanel REQ_ID: FE-02` ### Task 7: UserFormPage 新增模式 **Files:** - Create: `frontend/src/pages/users/UserFormPage.tsx` - Test: `frontend/src/pages/users/UserFormPage.test.tsx` - Modify: `frontend/src/router/index.tsx`(追加 /users/new 路由) - 测试先行类型: jsdom 组件测试 **API shape**: - `` 无其他 props(userId 从 useParams 读) - 编辑模式由 Task 8 完成;本任务只先实现 create - [ ] **Step 1: 写失败测试**: - `createMode_renders_emptyForm_andUsernameEditable` - `createMode_submitValid_callsCreate_andNavigatesToUsers` - `createMode_duplicateUsername_40901_showsFieldError` - `createMode_invalidUserType_40001_showsBanner` - `createMode_cancelButton_navigatesToUsers` - [ ] **Step 2: 实现最小代码** - [ ] **Step 3: 子会话验证 PASS** - [ ] **Step 4: Commit** `feat(frontend): UserFormPage 新增模式 REQ_ID: FE-02` ### Task 8: UserFormPage 编辑模式 **Files:** - Modify: `frontend/src/pages/users/UserFormPage.tsx` - Modify: `frontend/src/pages/users/UserFormPage.test.tsx` - Modify: `frontend/src/router/index.tsx`(追加 /users/:userId 路由) - 测试先行类型: jsdom 组件测试 **API behavior**: - mode="edit" 时挂载先调 `usersApi.get(userId)`;loading 期间 form 整体 disabled + spinner - 字段预填后 username readonly;其他字段允许修改 - submit 时调 `usersApi.update(userId, patchOnlyChangedFields)`;patch 通过 dirty fields 计算 - 40401 用户不存在 → 全屏 Result + 返回按钮 - 40901/40902 → field-level error - 40004 → field-level error 标在 employeeId / permissionCategoryIds(按 message 内容判别) - [ ] **Step 1: 写失败测试**: - `editMode_fetchesDetail_andPrefillsForm_andUsernameIsReadonly` - `editMode_unknownUserId_40401_showsNotFoundResult` - `editMode_submitPartialUpdate_onlySendsDirtyFields` - `editMode_duplicateUserCode_40902_showsFieldError` - `editMode_unknownEmployee_40004_showsFieldError` - [ ] **Step 2: 实现最小代码** - [ ] **Step 3: 子会话验证 PASS** - [ ] **Step 4: Commit** `feat(frontend): UserFormPage 编辑模式 + 部分字段 PATCH REQ_ID: FE-02` ### Task 9: Playwright E2E spec(fixme 跳过同 FE-01) **Files:** - Modify: `frontend/tests/e2e/users.spec.ts` — Create - 测试先行类型: Playwright E2E E2E spec 三场景 `.fixme()` 标记: 1. `listUsers_rendersAtLeastSeededUsers` 2. `createUser_returnsToListAndShowsNewUser` 3. `editUser_updatesUserCodeSuccessfully` 实际跑由开发者手工 unfix + `npx playwright install` + 启 backend 9090。 - [ ] **Step 1: 写 spec** - [ ] **Step 2: 略** - [ ] **Step 3: 略** - [ ] **Step 4: Commit** `test(frontend): Playwright E2E users 三场景 (fixme) REQ_ID: FE-02` --- ## 提交计划 | Task | Commit message | |---|---| | 1 | `feat(frontend): usersApi 4 个函数 + MSW handlers REQ_ID: FE-02` | | 2 | `feat(frontend): RequireSuperAdmin 守卫 REQ_ID: FE-02` | | 3 | `feat(frontend): UsersFilterBar + usersConstants REQ_ID: FE-02` | | 4 | `feat(frontend): UsersToolbar + UsersTable REQ_ID: FE-02` | | 5 | `feat(frontend): UsersListPage 整合 + 错误状态机 REQ_ID: FE-02` | | 6 | `feat(frontend): UserFormFields + UserPermissionPanel REQ_ID: FE-02` | | 7 | `feat(frontend): UserFormPage 新增模式 REQ_ID: FE-02` | | 8 | `feat(frontend): UserFormPage 编辑模式 + 部分字段 PATCH REQ_ID: FE-02` | | 9 | `test(frontend): Playwright E2E users 三场景 (fixme) REQ_ID: FE-02` |