2026-05-15-FE-02.md 15.5 KB

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):

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)返 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:

  • <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 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:

  • <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_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:

  • <UsersListPage /> 无 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:

  • <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_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:

  • <UserFormPage mode="create"|"edit" /> 无其他 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