FE-02 用户管理 Implementation Plan
Execution: Parent skill
fe-feature-tddexecutes 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+ 顶部 filterForm;表单页用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.tsxfrontend/src/pages/users/UsersToolbar.tsxfrontend/src/pages/users/UsersFilterBar.tsxfrontend/src/pages/users/UsersTable.tsxfrontend/src/pages/users/UserFormPage.tsxfrontend/src/pages/users/UserFormFields.tsxfrontend/src/pages/users/UserPermissionPanel.tsx-
frontend/src/pages/users/usersConstants.ts(白名单常量 + fixture employee/permission options + 错误文案)
测试:
frontend/src/api/users.test.tsfrontend/src/router/RequireSuperAdmin.test.tsxfrontend/src/pages/users/UsersListPage.test.tsxfrontend/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/users4 个端点的模拟
约束常量(跨任务一致)
usersApi 类型 / 签名(api/users.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<T> {
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<PageResult<UserListItem>>
usersApi.get(userId: number): Promise<UserDetail>
usersApi.create(req: CreateUserReq): Promise<{ userId: number; username: string; userCode: string }>
usersApi.update(userId: number, req: UpdateUserReq): Promise<UserDetail>
路由配置:
/users → <RequireSuperAdmin><UsersListPage/></RequireSuperAdmin>
/users/new → <RequireSuperAdmin><UserFormPage mode="create"/></RequireSuperAdmin>
/users/:userId → <RequireSuperAdmin><UserFormPage mode="edit"/></RequireSuperAdmin>
错误码 → 文案映射(usersConstants.ts):
40001: '请检查字段格式'
40004: '员工或权限分类不存在或已删除'
40101: '会话失效,请重新登录'
40301: '权限不足,仅超级管理员可调用'
40302: '不允许停用当前登录用户自己'
40401: '用户不存在'
40901: '用户名已存在'
40902: '用户号已被占用'
NETWORK: '网络异常,请检查连接后重试'
UNKNOWN: '操作失败,请稍后重试'
Fixture 常量(暂用,待后端补 employees / permission-categories 端点后改 API 拉取):
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)返 40302Step 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:
<RequireSuperAdmin>{children}</RequireSuperAdmin>行为:① 未登录 →
<Navigate to="/login" replace />(复用 RequireAuth 语义);② 已登录但 userType !== 'SUPER_ADMIN' → 渲染<Result status="403" title="权限不足"/>而非 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:
<UsersFilterBar onSearch={(query: Omit<UsersListQuery, 'page'|'size'|'sortField'|'sortOrder'>) => void} onReset={() => void} disabled={boolean} />内部:queryField + matchMode + queryValue 三个字段;搜索按钮触发 onSearch;清空按钮触发 onReset 并 form.resetFields
-
Step 1: 写失败测试:
renders 3 controls + 2 buttonsclickSearch_callsOnSearch_withFormValuesclickReset_resetsFieldsAndCallsOnResetdisabled_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:
-
<UsersToolbar onRefresh={() => void} onAdd={() => void} />("导出 Excel" 按钮 visually 保留但 disabled,无 onClick) <UsersTable records={UserListItem[]} loading={boolean} total={number} page={number} size={number} onRowClick={(row: UserListItem) => void} onPageChange={(page: number, size: number) => void} onSortChange={(sortField: string, sortOrder: 'asc'|'desc') => void} />Table columns: 序号 + 11 个字段 + 操作("编辑"链接 → onRowClick)
-
Step 1: 写失败测试:
Toolbar_callsOnAdd_onAddClickToolbar_callsOnRefresh_onRefreshClickToolbar_exportButton_isDisabledTable_rendersAllColumns_andRowsTable_rowClick_callsOnRowClickTable_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:
-
<UsersListPage />无 props - 内部状态:
{ query, records, total, loading, error };挂载触发 list;filter/page/sort change 触发 list 错误处理:40301 全屏 Result;网络错 banner + 重试
-
Step 1: 写失败测试:
mountFetchesList_andRendersRecordsfilterSubmit_refetches_withQueryParamspaginationChange_refetches_withNewPagerowClick_navigatesToUserDetailaddClick_navigatesToUserNewnetworkError_showsBannerAndRetryButtonforbidden_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:
-
<UserFormFields mode="create"|"edit" disabled={boolean} />- Form.Item: 用户名(create 模式可编辑,edit readonly)/ 用户号 / 类型 / 语言 / 单据修改权限 / 员工名
- 用 AntD
Form.useFormInstance()hook 由父组件控制
-
<UserPermissionPanel value={number[]} onChange={(ids: number[]) => void} disabled={boolean} />- 单 Tab "权限组"(其他 Tab disabled);权限分类列表 Checkbox.Group
-
Step 1: 写失败测试:
UserFormFields_create_usernameIsEditableUserFormFields_edit_usernameIsReadonlyUserFormFields_invalidUsername_showsPatternErrorUserPermissionPanel_renders_singleActiveTabUserPermissionPanel_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:
-
<UserFormPage mode="create"|"edit" />无其他 props(userId 从 useParams 读) 编辑模式由 Task 8 完成;本任务只先实现 create
-
Step 1: 写失败测试:
createMode_renders_emptyForm_andUsernameEditablecreateMode_submitValid_callsCreate_andNavigatesToUserscreateMode_duplicateUsername_40901_showsFieldErrorcreateMode_invalidUserType_40001_showsBannercreateMode_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_andUsernameIsReadonlyeditMode_unknownUserId_40401_showsNotFoundResulteditMode_submitPartialUpdate_onlySendsDirtyFieldseditMode_duplicateUserCode_40902_showsFieldErroreditMode_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() 标记:
listUsers_rendersAtLeastSeededUserscreateUser_returnsToListAndShowsNewUsereditUser_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 |