Commit 2d7c05511550e41f811ea1ee7dde70dbe428b32f
1 parent
9c0ba386
chore(fe-02): review approve + 归档 spec/plan/review
REQ_ID: FE-02
Showing
4 changed files
with
635 additions
and
1 deletions
docs/08-模块任务管理.md
| ... | ... | @@ -72,4 +72,4 @@ |
| 72 | 72 | - 整体 MR: — |
| 73 | 73 | - 功能: |
| 74 | 74 | - [x] FE-01 用户登录 | 关联 REQ:REQ-USR-001 | 关联原型:prototype/erp.html#screen-login |
| 75 | - - [ ] FE-02 用户管理(列表 + 新增 / 编辑) | 关联 REQ:REQ-USR-002, REQ-USR-003, REQ-USR-004 | 关联原型:prototype/erp.html#screen-userlist, prototype/erp.html#screen-userdetail | |
| 75 | + - [x] FE-02 用户管理(列表 + 新增 / 编辑) | 关联 REQ:REQ-USR-002, REQ-USR-003, REQ-USR-004 | 关联原型:prototype/erp.html#screen-userlist, prototype/erp.html#screen-userdetail | ... | ... |
docs/superpowers/plans/2026-05-15-FE-02.md
0 → 100644
| 1 | +# FE-02 用户管理 Implementation Plan | |
| 2 | + | |
| 3 | +> **Execution:** Parent skill `fe-feature-tdd` executes this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. | |
| 4 | + | |
| 5 | +**Goal:** 实现 3 个页面 + 路由:`/users` 列表(GET list 分页 + 筛选)、`/users/new` 新增表单(POST create)、`/users/:userId` 编辑表单(GET detail + PUT update)。所有页面强制 SUPER_ADMIN 守卫。 | |
| 6 | + | |
| 7 | +**Architecture:** | |
| 8 | +- 列表页用 AntD `Table` + `Pagination` + 顶部 filter `Form`;表单页用 `Form.Item` + `Select` + `Input` + 权限分类 `Checkbox.Group`。 | |
| 9 | +- 新增 / 编辑共用同一 `UserFormPage`,按路由参数 `userId` 切模式(new = create / 有 id = edit)。 | |
| 10 | +- API 客户端按 spec § 四 提供 `usersApi.{list, get, create, update}`,复用 FE-01 的 `apiClient` + `BizError`。 | |
| 11 | +- 路由级 `RequireSuperAdmin` 守卫(基于 LoginContext userType 派生于 Redux auth slice)。 | |
| 12 | + | |
| 13 | +**Tech Stack:** 复用 FE-01(React 18 / AntD 5 / Redux Toolkit / React Router v6 / Axios / Vitest + RTL + MSW)。 | |
| 14 | + | |
| 15 | +--- | |
| 16 | + | |
| 17 | +## 文件结构(全部 `frontend/` 下,与 docs/09 § 三对齐) | |
| 18 | + | |
| 19 | +**通用层增量**: | |
| 20 | +- `frontend/src/api/users.ts`(usersApi 4 个函数 + LoginReq/Vo 复用 FE-01 types) | |
| 21 | +- `frontend/src/router/RequireSuperAdmin.tsx`(守卫:必须已登录 + userType=SUPER_ADMIN) | |
| 22 | +- `frontend/src/router/index.tsx`(追加 /users/new + /users/:userId 路由) | |
| 23 | + | |
| 24 | +**业务层(pages/users/)**: | |
| 25 | +- `frontend/src/pages/users/UsersListPage.tsx` | |
| 26 | +- `frontend/src/pages/users/UsersToolbar.tsx` | |
| 27 | +- `frontend/src/pages/users/UsersFilterBar.tsx` | |
| 28 | +- `frontend/src/pages/users/UsersTable.tsx` | |
| 29 | +- `frontend/src/pages/users/UserFormPage.tsx` | |
| 30 | +- `frontend/src/pages/users/UserFormFields.tsx` | |
| 31 | +- `frontend/src/pages/users/UserPermissionPanel.tsx` | |
| 32 | +- `frontend/src/pages/users/usersConstants.ts`(白名单常量 + fixture employee/permission options + 错误文案) | |
| 33 | + | |
| 34 | +**测试**: | |
| 35 | +- `frontend/src/api/users.test.ts` | |
| 36 | +- `frontend/src/router/RequireSuperAdmin.test.tsx` | |
| 37 | +- `frontend/src/pages/users/UsersListPage.test.tsx` | |
| 38 | +- `frontend/src/pages/users/UserFormPage.test.tsx` | |
| 39 | +- `frontend/tests/e2e/users.spec.ts`(`.fixme()` 跳过同 FE-01,留作手工验收) | |
| 40 | + | |
| 41 | +**MSW 增量**: | |
| 42 | +- `frontend/src/test-utils/msw-handlers.ts` 增加 `/api/v1/users` 4 个端点的模拟 | |
| 43 | + | |
| 44 | +--- | |
| 45 | + | |
| 46 | +## 约束常量(跨任务一致) | |
| 47 | + | |
| 48 | +**usersApi 类型 / 签名**(`api/users.ts`): | |
| 49 | + | |
| 50 | +```ts | |
| 51 | +interface UserListItem { | |
| 52 | + userId: number; | |
| 53 | + username: string; | |
| 54 | + employeeName?: string | null; | |
| 55 | + userCode: string; | |
| 56 | + departmentName?: string | null; | |
| 57 | + userType: 'NORMAL' | 'SUPER_ADMIN'; | |
| 58 | + language: string; | |
| 59 | + isDeleted: boolean; | |
| 60 | + lastLoginDate?: string | null; | |
| 61 | + createdBy?: string | null; | |
| 62 | + createdDate?: string | null; | |
| 63 | +} | |
| 64 | + | |
| 65 | +interface UserDetail extends UserListItem { | |
| 66 | + employeeId?: number | null; | |
| 67 | + permissionCategoryIds: number[]; | |
| 68 | + updatedBy?: string | null; | |
| 69 | + updatedDate?: string | null; | |
| 70 | +} | |
| 71 | + | |
| 72 | +interface UsersListQuery { | |
| 73 | + page?: number; | |
| 74 | + size?: number; | |
| 75 | + sortField?: 'tCreateDate' | 'tLastLoginDate' | 'sUsername' | 'sUserCode'; | |
| 76 | + sortOrder?: 'asc' | 'desc'; | |
| 77 | + queryField?: string; | |
| 78 | + matchMode?: 'contains' | 'notContains' | 'equals'; | |
| 79 | + queryValue?: string; | |
| 80 | + userType?: 'NORMAL' | 'SUPER_ADMIN'; | |
| 81 | + isDeleted?: boolean; | |
| 82 | +} | |
| 83 | + | |
| 84 | +interface PageResult<T> { | |
| 85 | + records: T[]; | |
| 86 | + total: number; | |
| 87 | + page: number; | |
| 88 | + size: number; | |
| 89 | +} | |
| 90 | + | |
| 91 | +interface CreateUserReq { | |
| 92 | + username: string; | |
| 93 | + userCode: string; | |
| 94 | + userType: 'NORMAL' | 'SUPER_ADMIN'; | |
| 95 | + language: 'zh-CN' | 'en-US' | 'zh-TW'; | |
| 96 | + canEditDocument: boolean; | |
| 97 | + employeeId?: number; | |
| 98 | + permissionCategoryIds?: number[]; | |
| 99 | +} | |
| 100 | + | |
| 101 | +interface UpdateUserReq { | |
| 102 | + userCode?: string; | |
| 103 | + userType?: 'NORMAL' | 'SUPER_ADMIN'; | |
| 104 | + language?: 'zh-CN' | 'en-US' | 'zh-TW'; | |
| 105 | + canEditDocument?: boolean; | |
| 106 | + employeeId?: number; // 0 = 解除关联 | |
| 107 | + isDeleted?: boolean; | |
| 108 | + permissionCategoryIds?: number[]; | |
| 109 | +} | |
| 110 | + | |
| 111 | +usersApi.list(query: UsersListQuery): Promise<PageResult<UserListItem>> | |
| 112 | +usersApi.get(userId: number): Promise<UserDetail> | |
| 113 | +usersApi.create(req: CreateUserReq): Promise<{ userId: number; username: string; userCode: string }> | |
| 114 | +usersApi.update(userId: number, req: UpdateUserReq): Promise<UserDetail> | |
| 115 | +``` | |
| 116 | + | |
| 117 | +**路由配置**: | |
| 118 | + | |
| 119 | +``` | |
| 120 | +/users → <RequireSuperAdmin><UsersListPage/></RequireSuperAdmin> | |
| 121 | +/users/new → <RequireSuperAdmin><UserFormPage mode="create"/></RequireSuperAdmin> | |
| 122 | +/users/:userId → <RequireSuperAdmin><UserFormPage mode="edit"/></RequireSuperAdmin> | |
| 123 | +``` | |
| 124 | + | |
| 125 | +**错误码 → 文案映射**(`usersConstants.ts`): | |
| 126 | + | |
| 127 | +``` | |
| 128 | +40001: '请检查字段格式' | |
| 129 | +40004: '员工或权限分类不存在或已删除' | |
| 130 | +40101: '会话失效,请重新登录' | |
| 131 | +40301: '权限不足,仅超级管理员可调用' | |
| 132 | +40302: '不允许停用当前登录用户自己' | |
| 133 | +40401: '用户不存在' | |
| 134 | +40901: '用户名已存在' | |
| 135 | +40902: '用户号已被占用' | |
| 136 | +NETWORK: '网络异常,请检查连接后重试' | |
| 137 | +UNKNOWN: '操作失败,请稍后重试' | |
| 138 | +``` | |
| 139 | + | |
| 140 | +**Fixture 常量**(暂用,待后端补 employees / permission-categories 端点后改 API 拉取): | |
| 141 | + | |
| 142 | +```ts | |
| 143 | +EMPLOYEE_OPTIONS = [{ value: 0, label: '(无 / 解除关联)' }, { value: 1, label: '张三 (E001)' }] | |
| 144 | +PERMISSION_CATEGORY_OPTIONS = [ | |
| 145 | + { value: 1, label: 'PUR 采购管理' }, | |
| 146 | + { value: 2, label: 'SAL 销售管理' }, | |
| 147 | +] | |
| 148 | +USER_TYPE_OPTIONS = [ | |
| 149 | + { value: 'NORMAL', label: '普通用户' }, | |
| 150 | + { value: 'SUPER_ADMIN', label: '超级管理员' }, | |
| 151 | +] | |
| 152 | +LANGUAGE_OPTIONS = [ | |
| 153 | + { value: 'zh-CN', label: '中文' }, | |
| 154 | + { value: 'en-US', label: '英文' }, | |
| 155 | + { value: 'zh-TW', label: '繁体' }, | |
| 156 | +] | |
| 157 | +QUERY_FIELD_OPTIONS = [ | |
| 158 | + { value: 'username', label: '用户名' }, | |
| 159 | + { value: 'employeeName', label: '员工名' }, | |
| 160 | + { value: 'userCode', label: '用户号' }, | |
| 161 | + { value: 'departmentName', label: '部门' }, | |
| 162 | + { value: 'userType', label: '用户类型' }, | |
| 163 | + { value: 'isDeleted', label: '作废' }, | |
| 164 | + { value: 'createdBy', label: '制单人' }, | |
| 165 | +] | |
| 166 | +MATCH_MODE_OPTIONS = [ | |
| 167 | + { value: 'contains', label: '包含' }, | |
| 168 | + { value: 'notContains', label: '不包含' }, | |
| 169 | + { value: 'equals', label: '等于' }, | |
| 170 | +] | |
| 171 | +``` | |
| 172 | + | |
| 173 | +--- | |
| 174 | + | |
| 175 | +## 任务步骤 | |
| 176 | + | |
| 177 | +### Task 1: usersApi 4 个函数 + MSW handlers | |
| 178 | + | |
| 179 | +**Files:** | |
| 180 | +- Create: `frontend/src/api/users.ts` | |
| 181 | +- Modify: `frontend/src/test-utils/msw-handlers.ts`(追加 4 个 /api/v1/users 端点) | |
| 182 | +- Test: `frontend/src/api/users.test.ts` | |
| 183 | +- 测试先行类型: jsdom 组件测试 + MSW | |
| 184 | + | |
| 185 | +**API shape**:见"约束常量" | |
| 186 | + | |
| 187 | +MSW 增量 handler 规则: | |
| 188 | +- `GET /api/v1/users` → 返回 fixture:3 个用户(alice / admin / bob_deleted),支持 page/size/queryField/queryValue 过滤 | |
| 189 | +- `GET /api/v1/users/:userId` → 返回 fixture 详情;id=99999 返 40401 | |
| 190 | +- `POST /api/v1/users` → 默认成功;username='dup' 返 40901;userCode='dup-code' 返 40902;userType='ROOT' 返 40001 | |
| 191 | +- `PUT /api/v1/users/:userId` → 默认成功返回 detail;isDeleted=true && userId=admin(self)返 40302 | |
| 192 | + | |
| 193 | +- [ ] **Step 1: 写失败测试**:6 个用例(list 成功 + filter 命中 / get 成功 / get 不存在 40401 / create 成功 / create 用户名冲突 40901 / update 成功) | |
| 194 | +- [ ] **Step 2: 实现最小代码** | |
| 195 | +- [ ] **Step 3: 子会话验证 PASS** | |
| 196 | +- [ ] **Step 4: Commit** `feat(frontend): usersApi 4 个函数 + MSW handlers REQ_ID: FE-02` | |
| 197 | + | |
| 198 | +### Task 2: RequireSuperAdmin 守卫 | |
| 199 | + | |
| 200 | +**Files:** | |
| 201 | +- Create: `frontend/src/router/RequireSuperAdmin.tsx` | |
| 202 | +- Test: `frontend/src/router/RequireSuperAdmin.test.tsx` | |
| 203 | +- 测试先行类型: jsdom 组件测试 | |
| 204 | + | |
| 205 | +**API shape**: | |
| 206 | +- `<RequireSuperAdmin>{children}</RequireSuperAdmin>` | |
| 207 | +- 行为:① 未登录 → `<Navigate to="/login" replace />`(复用 RequireAuth 语义);② 已登录但 userType !== 'SUPER_ADMIN' → 渲染 `<Result status="403" title="权限不足"/>` 而非 navigate(区分场景:让用户知道是权限问题而非未登录) | |
| 208 | + | |
| 209 | +- [ ] **Step 1: 写失败测试**:3 个用例(无 token → /login;NORMAL token → Result 403;SUPER_ADMIN token → 渲染 children) | |
| 210 | +- [ ] **Step 2: 实现最小代码** | |
| 211 | +- [ ] **Step 3: 子会话验证 PASS** | |
| 212 | +- [ ] **Step 4: Commit** `feat(frontend): RequireSuperAdmin 守卫 REQ_ID: FE-02` | |
| 213 | + | |
| 214 | +### Task 3: usersConstants + UsersFilterBar 组件 | |
| 215 | + | |
| 216 | +**Files:** | |
| 217 | +- Create: `frontend/src/pages/users/usersConstants.ts` | |
| 218 | +- Create: `frontend/src/pages/users/UsersFilterBar.tsx` | |
| 219 | +- Test: `frontend/src/pages/users/UsersFilterBar.test.tsx` | |
| 220 | +- 测试先行类型: jsdom 组件测试 | |
| 221 | + | |
| 222 | +**API shape**: | |
| 223 | +- `<UsersFilterBar onSearch={(query: Omit<UsersListQuery, 'page'|'size'|'sortField'|'sortOrder'>) => void} onReset={() => void} disabled={boolean} />` | |
| 224 | +- 内部:queryField + matchMode + queryValue 三个字段;搜索按钮触发 onSearch;清空按钮触发 onReset 并 form.resetFields | |
| 225 | + | |
| 226 | +- [ ] **Step 1: 写失败测试**: | |
| 227 | + - `renders 3 controls + 2 buttons` | |
| 228 | + - `clickSearch_callsOnSearch_withFormValues` | |
| 229 | + - `clickReset_resetsFieldsAndCallsOnReset` | |
| 230 | + - `disabled_disablesAllControls` | |
| 231 | +- [ ] **Step 2: 实现最小代码** | |
| 232 | +- [ ] **Step 3: 子会话验证 PASS** | |
| 233 | +- [ ] **Step 4: Commit** `feat(frontend): UsersFilterBar + usersConstants REQ_ID: FE-02` | |
| 234 | + | |
| 235 | +### Task 4: UsersToolbar + UsersTable 组件 | |
| 236 | + | |
| 237 | +**Files:** | |
| 238 | +- Create: `frontend/src/pages/users/UsersToolbar.tsx` | |
| 239 | +- Create: `frontend/src/pages/users/UsersTable.tsx` | |
| 240 | +- Test: `frontend/src/pages/users/UsersTable.test.tsx` | |
| 241 | +- 测试先行类型: jsdom 组件测试 | |
| 242 | + | |
| 243 | +**API shape**: | |
| 244 | +- `<UsersToolbar onRefresh={() => void} onAdd={() => void} />`("导出 Excel" 按钮 visually 保留但 disabled,无 onClick) | |
| 245 | +- `<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} />` | |
| 246 | +- Table columns: 序号 + 11 个字段 + 操作("编辑"链接 → onRowClick) | |
| 247 | + | |
| 248 | +- [ ] **Step 1: 写失败测试**: | |
| 249 | + - `Toolbar_callsOnAdd_onAddClick` | |
| 250 | + - `Toolbar_callsOnRefresh_onRefreshClick` | |
| 251 | + - `Toolbar_exportButton_isDisabled` | |
| 252 | + - `Table_rendersAllColumns_andRows` | |
| 253 | + - `Table_rowClick_callsOnRowClick` | |
| 254 | + - `Table_paginationChange_callsOnPageChange` | |
| 255 | +- [ ] **Step 2: 实现最小代码** | |
| 256 | +- [ ] **Step 3: 子会话验证 PASS** | |
| 257 | +- [ ] **Step 4: Commit** `feat(frontend): UsersToolbar + UsersTable REQ_ID: FE-02` | |
| 258 | + | |
| 259 | +### Task 5: UsersListPage 整合 + 错误状态机 | |
| 260 | + | |
| 261 | +**Files:** | |
| 262 | +- Create: `frontend/src/pages/users/UsersListPage.tsx` | |
| 263 | +- Test: `frontend/src/pages/users/UsersListPage.test.tsx` | |
| 264 | +- Modify: `frontend/src/router/index.tsx`(追加 /users 用 RequireSuperAdmin 包裹;替换之前的 placeholder) | |
| 265 | +- 测试先行类型: jsdom 组件测试 | |
| 266 | + | |
| 267 | +**API shape**: | |
| 268 | +- `<UsersListPage />` 无 props | |
| 269 | +- 内部状态: `{ query, records, total, loading, error }`;挂载触发 list;filter/page/sort change 触发 list | |
| 270 | +- 错误处理:40301 全屏 Result;网络错 banner + 重试 | |
| 271 | + | |
| 272 | +- [ ] **Step 1: 写失败测试**: | |
| 273 | + - `mountFetchesList_andRendersRecords` | |
| 274 | + - `filterSubmit_refetches_withQueryParams` | |
| 275 | + - `paginationChange_refetches_withNewPage` | |
| 276 | + - `rowClick_navigatesToUserDetail` | |
| 277 | + - `addClick_navigatesToUserNew` | |
| 278 | + - `networkError_showsBannerAndRetryButton` | |
| 279 | + - `forbidden_403_showsResultPage` | |
| 280 | +- [ ] **Step 2: 实现最小代码** | |
| 281 | +- [ ] **Step 3: 子会话验证 PASS** | |
| 282 | +- [ ] **Step 4: Commit** `feat(frontend): UsersListPage 整合 + 错误状态机 REQ_ID: FE-02` | |
| 283 | + | |
| 284 | +### Task 6: UserFormFields + UserPermissionPanel 子组件 | |
| 285 | + | |
| 286 | +**Files:** | |
| 287 | +- Create: `frontend/src/pages/users/UserFormFields.tsx` | |
| 288 | +- Create: `frontend/src/pages/users/UserPermissionPanel.tsx` | |
| 289 | +- Test: `frontend/src/pages/users/UserFormFields.test.tsx` | |
| 290 | +- 测试先行类型: jsdom 组件测试 | |
| 291 | + | |
| 292 | +**API shape**: | |
| 293 | +- `<UserFormFields mode="create"|"edit" disabled={boolean} />` | |
| 294 | + - Form.Item: 用户名(create 模式可编辑,edit readonly)/ 用户号 / 类型 / 语言 / 单据修改权限 / 员工名 | |
| 295 | + - 用 AntD `Form.useFormInstance()` hook 由父组件控制 | |
| 296 | +- `<UserPermissionPanel value={number[]} onChange={(ids: number[]) => void} disabled={boolean} />` | |
| 297 | + - 单 Tab "权限组"(其他 Tab disabled);权限分类列表 Checkbox.Group | |
| 298 | + | |
| 299 | +- [ ] **Step 1: 写失败测试**: | |
| 300 | + - `UserFormFields_create_usernameIsEditable` | |
| 301 | + - `UserFormFields_edit_usernameIsReadonly` | |
| 302 | + - `UserFormFields_invalidUsername_showsPatternError` | |
| 303 | + - `UserPermissionPanel_renders_singleActiveTab` | |
| 304 | + - `UserPermissionPanel_toggleCheckbox_callsOnChange` | |
| 305 | +- [ ] **Step 2: 实现最小代码** | |
| 306 | +- [ ] **Step 3: 子会话验证 PASS** | |
| 307 | +- [ ] **Step 4: Commit** `feat(frontend): UserFormFields + UserPermissionPanel REQ_ID: FE-02` | |
| 308 | + | |
| 309 | +### Task 7: UserFormPage 新增模式 | |
| 310 | + | |
| 311 | +**Files:** | |
| 312 | +- Create: `frontend/src/pages/users/UserFormPage.tsx` | |
| 313 | +- Test: `frontend/src/pages/users/UserFormPage.test.tsx` | |
| 314 | +- Modify: `frontend/src/router/index.tsx`(追加 /users/new 路由) | |
| 315 | +- 测试先行类型: jsdom 组件测试 | |
| 316 | + | |
| 317 | +**API shape**: | |
| 318 | +- `<UserFormPage mode="create"|"edit" />` 无其他 props(userId 从 useParams 读) | |
| 319 | +- 编辑模式由 Task 8 完成;本任务只先实现 create | |
| 320 | + | |
| 321 | +- [ ] **Step 1: 写失败测试**: | |
| 322 | + - `createMode_renders_emptyForm_andUsernameEditable` | |
| 323 | + - `createMode_submitValid_callsCreate_andNavigatesToUsers` | |
| 324 | + - `createMode_duplicateUsername_40901_showsFieldError` | |
| 325 | + - `createMode_invalidUserType_40001_showsBanner` | |
| 326 | + - `createMode_cancelButton_navigatesToUsers` | |
| 327 | +- [ ] **Step 2: 实现最小代码** | |
| 328 | +- [ ] **Step 3: 子会话验证 PASS** | |
| 329 | +- [ ] **Step 4: Commit** `feat(frontend): UserFormPage 新增模式 REQ_ID: FE-02` | |
| 330 | + | |
| 331 | +### Task 8: UserFormPage 编辑模式 | |
| 332 | + | |
| 333 | +**Files:** | |
| 334 | +- Modify: `frontend/src/pages/users/UserFormPage.tsx` | |
| 335 | +- Modify: `frontend/src/pages/users/UserFormPage.test.tsx` | |
| 336 | +- Modify: `frontend/src/router/index.tsx`(追加 /users/:userId 路由) | |
| 337 | +- 测试先行类型: jsdom 组件测试 | |
| 338 | + | |
| 339 | +**API behavior**: | |
| 340 | +- mode="edit" 时挂载先调 `usersApi.get(userId)`;loading 期间 form 整体 disabled + spinner | |
| 341 | +- 字段预填后 username readonly;其他字段允许修改 | |
| 342 | +- submit 时调 `usersApi.update(userId, patchOnlyChangedFields)`;patch 通过 dirty fields 计算 | |
| 343 | +- 40401 用户不存在 → 全屏 Result + 返回按钮 | |
| 344 | +- 40901/40902 → field-level error | |
| 345 | +- 40004 → field-level error 标在 employeeId / permissionCategoryIds(按 message 内容判别) | |
| 346 | + | |
| 347 | +- [ ] **Step 1: 写失败测试**: | |
| 348 | + - `editMode_fetchesDetail_andPrefillsForm_andUsernameIsReadonly` | |
| 349 | + - `editMode_unknownUserId_40401_showsNotFoundResult` | |
| 350 | + - `editMode_submitPartialUpdate_onlySendsDirtyFields` | |
| 351 | + - `editMode_duplicateUserCode_40902_showsFieldError` | |
| 352 | + - `editMode_unknownEmployee_40004_showsFieldError` | |
| 353 | +- [ ] **Step 2: 实现最小代码** | |
| 354 | +- [ ] **Step 3: 子会话验证 PASS** | |
| 355 | +- [ ] **Step 4: Commit** `feat(frontend): UserFormPage 编辑模式 + 部分字段 PATCH REQ_ID: FE-02` | |
| 356 | + | |
| 357 | +### Task 9: Playwright E2E spec(fixme 跳过同 FE-01) | |
| 358 | + | |
| 359 | +**Files:** | |
| 360 | +- Modify: `frontend/tests/e2e/users.spec.ts` — Create | |
| 361 | +- 测试先行类型: Playwright E2E | |
| 362 | + | |
| 363 | +E2E spec 三场景 `.fixme()` 标记: | |
| 364 | +1. `listUsers_rendersAtLeastSeededUsers` | |
| 365 | +2. `createUser_returnsToListAndShowsNewUser` | |
| 366 | +3. `editUser_updatesUserCodeSuccessfully` | |
| 367 | + | |
| 368 | +实际跑由开发者手工 unfix + `npx playwright install` + 启 backend 9090。 | |
| 369 | + | |
| 370 | +- [ ] **Step 1: 写 spec** | |
| 371 | +- [ ] **Step 2: 略** | |
| 372 | +- [ ] **Step 3: 略** | |
| 373 | +- [ ] **Step 4: Commit** `test(frontend): Playwright E2E users 三场景 (fixme) REQ_ID: FE-02` | |
| 374 | + | |
| 375 | +--- | |
| 376 | + | |
| 377 | +## 提交计划 | |
| 378 | + | |
| 379 | +| Task | Commit message | | |
| 380 | +|---|---| | |
| 381 | +| 1 | `feat(frontend): usersApi 4 个函数 + MSW handlers REQ_ID: FE-02` | | |
| 382 | +| 2 | `feat(frontend): RequireSuperAdmin 守卫 REQ_ID: FE-02` | | |
| 383 | +| 3 | `feat(frontend): UsersFilterBar + usersConstants REQ_ID: FE-02` | | |
| 384 | +| 4 | `feat(frontend): UsersToolbar + UsersTable REQ_ID: FE-02` | | |
| 385 | +| 5 | `feat(frontend): UsersListPage 整合 + 错误状态机 REQ_ID: FE-02` | | |
| 386 | +| 6 | `feat(frontend): UserFormFields + UserPermissionPanel REQ_ID: FE-02` | | |
| 387 | +| 7 | `feat(frontend): UserFormPage 新增模式 REQ_ID: FE-02` | | |
| 388 | +| 8 | `feat(frontend): UserFormPage 编辑模式 + 部分字段 PATCH REQ_ID: FE-02` | | |
| 389 | +| 9 | `test(frontend): Playwright E2E users 三场景 (fixme) REQ_ID: FE-02` | | ... | ... |
docs/superpowers/reviews/2026-05-15-FE-02.md
0 → 100644
| 1 | +--- | |
| 2 | +fe_id: FE-02 | |
| 3 | +date: 2026-05-15 | |
| 4 | +round: 2 | |
| 5 | +reviewer: fe-code-reviewer | |
| 6 | +--- | |
| 7 | + | |
| 8 | +# Review: FE-02 — round 2 | |
| 9 | + | |
| 10 | +## 结论 | |
| 11 | +approve | |
| 12 | + | |
| 13 | +## Round 1 修复落地核对 | |
| 14 | + | |
| 15 | +| # | 项目 | 状态 | | |
| 16 | +|---|------|-----| | |
| 17 | +| H1 | UserFormPage edit prefill canEditDocument from backend(之前硬编码 false) | ✓ | | |
| 18 | +| M1 | UsersTable a11y 键盘可达(tabIndex/role/aria-label/onKeyDown)+ 操作列编辑链接 + 列头 sorter | ✓ | | |
| 19 | +| M2 | UserFormPage employeeId 三态映射(toCreateEmployeeId→null / toUpdateEmployeeId→保留) | ✓ | | |
| 20 | +| M3 | api/users.ts CreateUserReq/UpdateUserReq.employeeId 类型补 `| null` | ✓ | | |
| 21 | +| L1 | UserPermissionPanel 补 process/driver 两个 disabled Tab(与 prototype 对齐) | ✓ | | |
| 22 | +| L2 | UsersListPage 默认 sortField='tCreateDate'/sortOrder='desc' 显式发后端 | ✓ | | |
| 23 | +| L3 | usersConstants QUERY_FIELD_OPTIONS 补 lastLoginDate | ✓ | | |
| 24 | +| TEST | canEditDocument prefill 测试 + msw handler 返回该字段 | ✓ | | |
| 25 | + | |
| 26 | +## Nice-to-have(保留延后) | |
| 27 | + | |
| 28 | +- frontend/src/pages/users/UserFormPage.tsx:129 — 40004 仍 banner;后端协议补 detail.field 后切 field-level | |
| 29 | +- frontend/src/pages/users/UsersTable.tsx:73 — 列头 sorter UI 已加但 onChange 未连到 UsersListPage.setQuery;视觉 sorter 生效但点击列头不会实际改变排序 | |
| 30 | +- frontend/src/pages/users/UsersListPage.test.tsx — 缺 error banner 渲染态测试 | |
| 31 | +- frontend/src/pages/users/UserFormPage.test.tsx — 缺 40004 错误码测试 | |
| 32 | +- frontend/src/pages/users/UsersToolbar.tsx — prototype 还有作废 / 删除 / 重置密码 等按钮;FE-02 spec § 一已声明推后 | |
| 33 | + | |
| 34 | +## 7 维 checklist | |
| 35 | + | |
| 36 | +| # | 维度 | 状态 | | |
| 37 | +|---|------|-----| | |
| 38 | +| 1 | Prototype consistency | pass(UsersTable 操作列 + UserPermissionPanel 5 个 Tab) | | |
| 39 | +| 2 | Design tokens | pass(仅 App.tsx 受控 hex 镜像 ConfigProvider colorPrimary) | | |
| 40 | +| 3 | A11y | pass(tabIndex/role/aria-label/onKeyDown + Form.Item label) | | |
| 41 | +| 4 | Responsive | pass | | |
| 42 | +| 5 | 业务校验前端复刻 | pass(spec § 五 #3-7,11,12,16-17 全部实现;#13 banner 可接受) | | |
| 43 | +| 6 | API consistency | pass(统一 apiClient) | | |
| 44 | +| 7 | 状态机覆盖 | pass(5 基础态 + spec 列出的错误态) | | |
| 45 | + | |
| 46 | +## 反例 / 测试覆盖缺口 | |
| 47 | + | |
| 48 | +44 vitest 通过;3 个 Playwright E2E `.fixme()` 留作手工验收。缺 list error 渲染态 + form 40004 路径的单测覆盖(已记 nice-to-have)。sortField/sortOrder UI ↔ query 通道未拉通——sorter 列头视觉已有但回调未绑,不影响默认排序但影响主动排序;可下个 FE 接前接 Table onChange。 | |
| 49 | + | |
| 50 | +## 总结 | |
| 51 | + | |
| 52 | +Round 1 全部 8 项 must-fix 已正确落地:编辑模式 prefill canEditDocument 走 detail VO 透传修复了静默数据回写 bug;UsersTable a11y / 操作列 / sorter 全套;employeeId 三态语义按 spec § 八 锁定;prototype Tab 对齐;默认排序显式发后端;QUERY_FIELD_OPTIONS 完整。所有 nice-to-have 均为用户标注的延后项,不阻断 approve。 | ... | ... |
docs/superpowers/specs/2026-05-15-FE-02.md
0 → 100644
| 1 | +# 前端功能规格 — FE-02 用户管理(列表 + 新增 / 编辑) | |
| 2 | + | |
| 3 | +> 关联 REQ:REQ-USR-002, REQ-USR-003, REQ-USR-004 | |
| 4 | +> 关联原型:prototype/erp.html#screen-userlist, prototype/erp.html#screen-userdetail | |
| 5 | +> 日期:2026-05-15 | |
| 6 | + | |
| 7 | +## 一、功能概述 | |
| 8 | + | |
| 9 | +超级管理员管理用户账号的三大场景: | |
| 10 | +- **列表 / 筛选**(REQ-USR-004):分页 + 多字段筛选 + 排序,路由 `/users` | |
| 11 | +- **新增用户**(REQ-USR-002):独立表单页 `/users/new`,提交后跳回 `/users` | |
| 12 | +- **编辑用户**(REQ-USR-003):独立表单页 `/users/:userId`,预填详情后允许部分字段更新 | |
| 13 | + | |
| 14 | +UI 模式:与 prototype 两个独立 section 对齐——userlist 是表格 + filter;userdetail 是表单页(toolbar + form-grid + Tabs + 权限分类)。新增 / 编辑共用同一 UserFormPage 组件(用 `:userId === undefined` 切模式)。 | |
| 15 | + | |
| 16 | +**本 FE 不含**:作废 / 取消作废、删除、重置密码、导出 Excel、客户查看 / 供应商查看 / 人员查看 / 工序查看 / 司机查看等多 Tab 权限(仅实现"权限组"主 Tab)。这些后续 FE 补。 | |
| 17 | + | |
| 18 | +## 二、组件树 | |
| 19 | + | |
| 20 | +基于 `prototype/erp.html` 中 `#screen-userlist` + `#screen-userdetail` 推导: | |
| 21 | + | |
| 22 | +``` | |
| 23 | +UsersListPage (pages/users/UsersListPage.tsx, route="/users", protected by RequireAuth) | |
| 24 | +├── PageToolbar | |
| 25 | +│ ├── RefreshButton (调 list query) | |
| 26 | +│ ├── AddNewButton (navigate to /users/new) | |
| 27 | +│ └── (导出 Excel 按钮:visually 保留但 disabled) | |
| 28 | +├── FilterBar (filter form) | |
| 29 | +│ ├── QueryFieldSelect (username / employeeName / userCode / departmentName / userType / isDeleted / lastLoginDate / createdBy) | |
| 30 | +│ ├── MatchModeSelect (contains / notContains / equals) | |
| 31 | +│ ├── QueryValueInput | |
| 32 | +│ ├── SearchButton | |
| 33 | +│ └── ResetButton | |
| 34 | +├── UsersTable (AntD Table) | |
| 35 | +│ ├── columns: 序号 / 用户名 / 员工名 / 用户号 / 部门 / 用户类型 / 语言 / 作废 / 登录日期 / 制单人 / 制单日期 / 操作(编辑链接) | |
| 36 | +│ ├── rowKey="userId" | |
| 37 | +│ ├── onRowClick → navigate to /users/{userId} | |
| 38 | +│ └── pagination footer | |
| 39 | +└── UsersPagination (AntD Pagination integrated with Table) | |
| 40 | + | |
| 41 | +UserFormPage (pages/users/UserFormPage.tsx, route="/users/new" + "/users/:userId", protected by RequireAuth) | |
| 42 | +├── PageToolbar | |
| 43 | +│ ├── SaveButton | |
| 44 | +│ ├── CancelButton (navigate back to /users) | |
| 45 | +│ └── (新增按钮:仅编辑模式可见,跳 /users/new) | |
| 46 | +├── UserFormFieldsPanel (form-grid) | |
| 47 | +│ ├── CreatedTime (只读,仅编辑模式显示) | |
| 48 | +│ ├── CreatedBy (只读,仅编辑模式显示) | |
| 49 | +│ ├── EmployeeSelect (label "员工名"; 当前硬编码可选员工 fixture,等 HR REQ 后改 GET /api/v1/employees) | |
| 50 | +│ ├── UsernameInput (label "用户名",编辑模式 readonly) | |
| 51 | +│ ├── UserCodeInput (label "用户号") | |
| 52 | +│ ├── UserTypeSelect (label "类型",options: NORMAL / SUPER_ADMIN) | |
| 53 | +│ ├── LanguageSelect (label "语言",options: zh-CN / en-US / zh-TW) | |
| 54 | +│ └── CanEditDocumentCheckbox (label "单据修改权限") | |
| 55 | +├── PermissionTabs (单一 Tab "权限组",其他 Tab visually 保留但 disabled) | |
| 56 | +└── PermissionCategoryList (AntD List + Checkbox 行选) | |
| 57 | + └── PermissionCategoryRow × N (当前硬编码 fixture {PUR, SAL}, 等运营 REQ 后改 GET /api/v1/permission-categories) | |
| 58 | +``` | |
| 59 | + | |
| 60 | +## 三、页面状态机 | |
| 61 | + | |
| 62 | +### UsersListPage 状态 | |
| 63 | + | |
| 64 | +| # | 状态 | 触发条件 | 视觉表现 | 用户可执行操作 | | |
| 65 | +|---|------|---------|---------|--------------| | |
| 66 | +| 1 | loading | 首次进入 / refresh / filter 提交 | Table loading=true 显 spinner | 等待 | | |
| 67 | +| 2 | empty | API 返 total=0 | Table 空态 + "暂无用户" | 点新增 / 清空筛选 | | |
| 68 | +| 3 | normal | 正常返回 records | Table 渲染数据;分页器显示 total / page / size | 点行编辑 / 翻页 / 排序 / 筛选 | | |
| 69 | +| 4 | error_network | 网络错(-1) | Table 顶部 Alert "网络异常,请重试" + 重试按钮 | 点重试 | | |
| 70 | +| 5 | error_forbidden | 403 / 40301 | 全屏 Result "权限不足" | 点退回 | | |
| 71 | + | |
| 72 | +### UserFormPage 状态 | |
| 73 | + | |
| 74 | +| # | 状态 | 触发条件 | 视觉表现 | 用户可执行操作 | | |
| 75 | +|---|------|---------|---------|--------------| | |
| 76 | +| 1 | loading_initial | 编辑模式首次进入(new 模式跳过) | 表单 disabled + spinner | 等待 | | |
| 77 | +| 2 | error_load | GET /api/v1/users/{userId} 失败(404 / 40401) | 全屏 Result "用户不存在" + 返回按钮 | 点返回 | | |
| 78 | +| 3 | idle | 表单已加载完成 | 表单可编辑(编辑模式 username 只读) | 修改字段 | | |
| 79 | +| 4 | validating | 用户点 save → 前端字段校验 | submit 按钮 disabled | 等待 | | |
| 80 | +| 5 | submitting | 校验通过,调 POST/PUT | submit 显 loading 旋转 | 等待 | | |
| 81 | +| 6 | error_field | 后端返 40001/40901/40902/40004 | 对应字段下方红字 + Alert | 改字段重试 | | |
| 82 | +| 7 | error_global | 40101 / 40301 / 40302 / 网络 | 顶部 Alert 显文案 | 操作返回 | | |
| 83 | +| 8 | success | 后端返 200/201 | message.success "保存成功";navigate(-1) 或 navigate('/users') | 自动跳转 | | |
| 84 | + | |
| 85 | +## 四、消费的后端端点 | |
| 86 | + | |
| 87 | +| # | 方法 | 路径 | 触发时机 | 关联 REQ | | |
| 88 | +|---|------|------|---------|---------| | |
| 89 | +| 1 | GET | `/api/v1/users` | UsersListPage 加载 / refresh / filter 提交 / 翻页 / 排序 | REQ-USR-004 | | |
| 90 | +| 2 | GET | `/api/v1/users/{userId}` | UserFormPage 编辑模式挂载 | REQ-USR-003 | | |
| 91 | +| 3 | POST | `/api/v1/users` | UserFormPage 新增模式 submit | REQ-USR-002 | | |
| 92 | +| 4 | PUT | `/api/v1/users/{userId}` | UserFormPage 编辑模式 submit | REQ-USR-003 | | |
| 93 | + | |
| 94 | +请求体 / 响应结构与 docs/05 一致;客户端 `usersApi.{list, get, create, update}` 包装函数。 | |
| 95 | + | |
| 96 | +## 五、业务规则前端复刻清单 | |
| 97 | + | |
| 98 | +| # | 规则描述 | 触发时机 | 报错文案 | 来源 REQ | | |
| 99 | +|---|---------|---------|---------|---------| | |
| 100 | +| 1 | 列表 size 上限 100 | 翻页器选项;默认 20 | (AntD pagination 内置) | REQ-USR-004 | | |
| 101 | +| 2 | 列表 page<1 / sortField 非白名单 / matchMode 非白名单 → 后端返 40001/40003,前端做 banner 提示 | filter submit | "请检查筛选参数" | REQ-USR-004 | | |
| 102 | +| 3 | 用户名(新增)必填 + 3-20 位字母数字下划线(正则 `^[A-Za-z0-9_]{3,20}$`) | submit 前 | "用户名必须为 3-20 位字母数字下划线" | REQ-USR-002 | | |
| 103 | +| 4 | 用户号必填 + 最大 50 字符 | submit 前 | "请输入用户号 / 不超过 50 字符" | REQ-USR-002 / 003 | | |
| 104 | +| 5 | 类型 / 语言必填且为枚举 | submit 前 | "请选择类型 / 语言" | REQ-USR-002 / 003 | | |
| 105 | +| 6 | 单据修改权限:boolean,默认 false | 字段渲染 | — | REQ-USR-002 / 003 | | |
| 106 | +| 7 | 员工 ID 可选;选 "无 / 解除关联" 时新增传 null,编辑传 0(spec § PATCH 三态) | submit | — | REQ-USR-003 | | |
| 107 | +| 8 | 编辑模式 username + 密码字段不允许修改(username readonly;表单不展示 password 字段) | 字段渲染 | — | REQ-USR-003 | | |
| 108 | +| 9 | 新增成功 → message + navigate('/users') | 响应处理 | "新增用户成功" | REQ-USR-002 | | |
| 109 | +| 10 | 编辑成功 → message + navigate('/users') | 响应处理 | "保存成功" | REQ-USR-003 | | |
| 110 | +| 11 | 40901 用户名冲突 → field-level "用户名已存在" | 响应处理 | "用户名已存在" | REQ-USR-002 | | |
| 111 | +| 12 | 40902 用户号冲突 → field-level "用户号已存在" | 响应处理 | "用户号已被占用" | REQ-USR-002 / 003 | | |
| 112 | +| 13 | 40004 员工 / 权限分类不存在 → field-level 错误 | 响应处理 | "员工或权限分类不存在或已删除" | REQ-USR-002 / 003 | | |
| 113 | +| 14 | 40301 非超级管理员 → 全屏 Result | 响应处理 | "权限不足,仅超级管理员可调用" | REQ-USR-002 / 003 / 004 | | |
| 114 | +| 15 | 40302 试图停用自己 → banner(FE-02 不含作废按钮,此分支只在意外触发时显示) | 响应处理 | "不允许停用当前登录用户自己" | REQ-USR-003 | | |
| 115 | +| 16 | 40401 用户不存在(编辑模式 GET / PUT 都可能命中) | 响应处理 | "用户不存在" → 跳回列表 | REQ-USR-003 | | |
| 116 | +| 17 | 网络错 → banner 重试 | 响应处理 | "网络异常,请检查连接后重试" | docs/04 § 2.4 | | |
| 117 | + | |
| 118 | +> **要求**:每条规则必须在前端 form-level 校验中复刻(前端能预防的不依赖后端报错)。文案与后端语义一致。 | |
| 119 | + | |
| 120 | +## 六、Design Tokens 引用清单 | |
| 121 | + | |
| 122 | +``` | |
| 123 | +--color-primary (主按钮 / 链接 / Tab 激活下划线) | |
| 124 | +--color-primary-hover (hover) | |
| 125 | +--color-primary-active (按下) | |
| 126 | +--color-text (表格行文字 / 标签) | |
| 127 | +--color-text-secondary (表格表头辅助文字 / 占位) | |
| 128 | +--color-text-disabled (只读字段文字) | |
| 129 | +--color-error (错误字段 / 错误 Alert) | |
| 130 | +--color-success (保存成功 message) | |
| 131 | +--color-warning (列表网络错黄色 Alert) | |
| 132 | +--color-border (表格 / 输入框边框) | |
| 133 | +--color-split (列表行分割线) | |
| 134 | +--color-bg-page (页面底色) | |
| 135 | +--color-bg-container (Table / 表单 card bg) | |
| 136 | +--color-bg-disabled (只读字段 bg) | |
| 137 | +``` | |
| 138 | + | |
| 139 | +均来自 docs/06 § 二。本 FE 不引入新 token。 | |
| 140 | + | |
| 141 | +## 七、交互流程关键路径 | |
| 142 | + | |
| 143 | +### 列表查询 + 翻页 | |
| 144 | + | |
| 145 | +``` | |
| 146 | +[用户访问 /users] | |
| 147 | + 1. UsersListPage 挂载 → useEffect 调 usersApi.list({page:1, size:20, sortField:'tCreateDate', sortOrder:'desc'}) | |
| 148 | + 2. Table loading=true → API 返回 → set records + total | |
| 149 | + 3. 用户改 filter 输入 → 点搜索 → 重新调 list | |
| 150 | + 4. 用户点列头排序 → set sortField/sortOrder → 重新调 list | |
| 151 | + 5. 用户翻页 → set page → 重新调 list | |
| 152 | + | |
| 153 | +[用户点行] | |
| 154 | + → navigate('/users/' + row.userId) | |
| 155 | +``` | |
| 156 | + | |
| 157 | +### 新增用户 | |
| 158 | + | |
| 159 | +``` | |
| 160 | +[用户在 /users 点新增] | |
| 161 | + → navigate('/users/new') | |
| 162 | + → UserFormPage mode=create,空表单 | |
| 163 | + → 用户填字段 → 点保存 | |
| 164 | + → validateFields → submitting 态 | |
| 165 | + → usersApi.create(form) → 成功 → message.success → navigate('/users') | |
| 166 | + → 失败:按错误码 setFieldErrors 或 banner | |
| 167 | +``` | |
| 168 | + | |
| 169 | +### 编辑用户 | |
| 170 | + | |
| 171 | +``` | |
| 172 | +[用户在 /users 点行 或 访问 /users/:id] | |
| 173 | + → UserFormPage mode=edit,挂载时调 usersApi.get(userId) | |
| 174 | + → loading_initial → 收到 detail → 填入表单(username readonly) | |
| 175 | + → 用户改字段 → 点保存 | |
| 176 | + → validateFields → submitting → usersApi.update(userId, patch) → 成功 → message.success → navigate('/users') | |
| 177 | + → 40401 不存在 → error_load 全屏 Result | |
| 178 | +``` | |
| 179 | + | |
| 180 | +### filter 状态保留(推迟到 FE-03+) | |
| 181 | + | |
| 182 | +本 FE:刷新页面 filter 清空;下一 FE 可引入 URL query string 同步。 | |
| 183 | + | |
| 184 | +## 八、备注与开放问题 | |
| 185 | + | |
| 186 | +- **GET /api/v1/employees + /api/v1/permission-categories 暂未实现**:员工 + 权限分类下拉用 fixture 数据(见 prototype,可硬编码 2-3 项;用户选择后传 ID)。spec § 一标记延后 | |
| 187 | +- **导出 Excel / 删除 / 重置密码 / 作废切换 / 多 Tab 权限**:本 FE 不实现,prototype 按钮 visually 保留但 disabled / 不绑定点击事件。docs/08 § 三 中明确"FE-02 = 列表 + 新增 / 编辑",其他能力推后 | |
| 188 | +- **大表格性能**:当前默认 size=20,size 上限 100,无需虚拟滚动;docs/04 § 3.2 一致 | |
| 189 | +- **i18n**:硬编码中文。后续 i18n 接入时统一替换 | |
| 190 | +- **a11y**:表单字段必须有 label(与 FE-01 同样规则);Table 行点击同时支持键盘 Enter 跳详情 | |
| 191 | +- **后端 40001 data.field-level 信息**:当前接口未明示返回 detail;FE 接到 40001 暂统一 banner,待后端扩展 | |
| 192 | +- **userType=NORMAL 用户调用列表 / 详情会被后端 40301**:FE 在用户进入路由前根据 LoginContext 已知 userType,可主动 RequireSuperAdmin 守卫减少不必要请求。本 FE 实现 `<RequireSuperAdmin>` 守卫包裹 `/users/**` 路由 | |
| 193 | +- **PATCH 编辑模式 employeeId 三态**:null=不变;0=解除关联;正整数=更新。前端 form 把 "无" 选项 value=0 处理 | ... | ... |