Commit 7cfb6ee7a4c6610b1e3f82fa8f1e718d4dc2e810
1 parent
4d4d51c8
docs(plan:FE-03): 任务级 TDD 计划
Showing
1 changed file
with
284 additions
and
0 deletions
docs/superpowers/plans/2026-06-01-FE-03.md
0 → 100644
| 1 | +# FE-03 用户列表与查询 — 任务级 TDD 计划(前端) | ||
| 2 | + | ||
| 3 | +> 阶段:前端(frontend)。作用域:`frontend/**`(页面 / api / 类型 / 样式 / 测试)。**禁止**写 `backend/**` / `sql/**` / `scripts/**`。 | ||
| 4 | +> 上游 SSoT:spec `docs/superpowers/specs/2026-06-01-FE-03.md`;需求卡片 `docs/01-需求清单/USR-用户管理/REQ-USR-003.md`;API 契约 `docs/05-API接口契约.md § REQ-USR-003`(`GET /api/usr/users`);原型 `prototype/erp.html` → `<section id="screen-userlist">`(`.toolbar` / `.filterbar` / `.table-shell .grid-table#user-table` / `.pager`,布局与交互权威);技术规范 `docs/04-技术规范.md § 零 / § 2.1 / § 2.3 / § 2.4 / § 3.2`;Design Tokens 仓库根 `src/styles/tokens.css`。 | ||
| 5 | +> 复用资产:FE-01(`api/request.ts` 已建 Axios 实例 + Result 拆包 + `ApiError` + `TOKEN_STORAGE_KEY`;`api/types.ts`)、FE-02(`AppLayout` 外壳 + `RequireAuth` 守卫 + 标签栈 `useTabStack` + 路由表 `router/index.tsx` 已挂 `/usr/users` 占位 `UserListPlaceholder`)。 | ||
| 6 | +> 本计划告诉 TDD 执行者**做什么 / 文件边界 / 测试意图 / props 与类型契约 / 完成判据**;具体代码由红-绿-提交循环产出,不在此 dump 整组件 / 类型文件内容。 | ||
| 7 | +> **本 FE 为只读查询**:仅 `GET /api/usr/users`,无任何写副作用(spec § 5 BR5)。查询匹配语义、敏感字段过滤、分页越界回退等真伪裁决全部在后端,前端只采集条件 + 发起分页查询 + 依响应渲染表格 / 分页 / 空态 / 错误态 + 导航跳转。 | ||
| 8 | + | ||
| 9 | +--- | ||
| 10 | + | ||
| 11 | +## Goal(目标) | ||
| 12 | + | ||
| 13 | +把 FE-02 在 `router/index.tsx` 留下的 `/usr/users` 占位(`UserListPlaceholder`)替换为真实「用户列表与查询」页面,复刻原型 `#screen-userlist` 的布局与交互语义(工具栏 / 筛选栏 / 用户表格 / 分页栏),数据/筛选/分页真实对接 `GET /api/usr/users`: | ||
| 14 | + | ||
| 15 | +- **页面容器 `UserListPage`**(路由 `/usr/users`,渲染于 FE-02 `AppLayout` 的 `<Outlet/>` 内):纵向组合 `UserToolbar` + `UserFilterBar` + `UserTable` + 内置分页,状态由页面本地 hook `useUserList` 持有(spec § 8 D6,不进 Redux)。 | ||
| 16 | +- **API 封装**:`api/usrApi.ts` 新增 `listUsers(query): Promise<PageResult<UserVO>>`(页面只调封装方法,不散用 axios,docs/04 § 2.3);`api/types.ts` 新增 `UserVO` / `PageResult<T>` / `UserListQuery` 契约。 | ||
| 17 | +- **状态机 ≥6 态**(spec § 3):`initialLoading` / `loading`(查询·翻页·刷新进行中)/ `success` / `empty` / `error` / `exporting`,均有测试固化。 | ||
| 18 | +- **业务规则 BR1~BR15** 在组件层 / hook 层 / E2E 有断言(spec § 5)。 | ||
| 19 | +- **工具栏**:刷新(保持当前条件+当前页重取,BR8)/ 新增(`navigate('/usr/users/new')`,BR13)/ 导出Excel(导出当前条件命中结果,BR9 / D5 见下方决策 D-PLAN-1)/ 设置齿轮(占位,spec D7)。 | ||
| 20 | +- **筛选栏**:用户范围下拉(占位不传后端参数,spec D2)/ 查询字段下拉(默认「用户名」,BR2/BR4)/ 匹配方式下拉(默认「包含」,BR2/BR4)/ 查询值输入(空为全部,BR3,回车触发搜索 BR7)/ 更多「▾」(占位,spec D3)/ 搜索(回第 1 页查询,BR7)/ 清空(重置默认并全量查询,BR10)。 | ||
| 21 | +- **用户表格**:AntD `Table`(`rowKey="id"`,服务端受控分页,`rowSelection={{type:'radio'}}` 单选标记仅服务进单据不参与查询 spec D8,行双击 → `navigate('/usr/users/'+id)` BR12,「作废」列只读 0/1→否/是 BR6,「序号」列按当前页计算 BR1)。 | ||
| 22 | +- **分页**:AntD `Table` 内置 `pagination` 受控(`current`/`pageSize`/`total`/`showSizeChanger`/`showTotal`),`pageSize` 默认 10、可选 `[10,20,50,100]`(spec D4);切页/改每页条数重取(改每页条数回第 1 页,BR11);越界由后端回退、前端按响应 `pageNum` 回显(BR15)。 | ||
| 23 | +- **空态 / 错误态**:空 `records` → AntD `Empty`「暂无匹配的用户」不报错(BR14);非 0 code / 网络异常 → 错误占位 +「点击重试」+ `message` 文案(spec § 4 错误码表);被动 401 由 `request.ts` 拦截器统一跳 `/login`(FE-02 D11,本页不重复处理)。 | ||
| 24 | +- **语义色只用 `var(--color-*)`**;工具栏深色底为页面局部装饰,scoped 保留,不新增全局 token、不挪用语义 token(spec § 7 / D10)。 | ||
| 25 | + | ||
| 26 | +## Architecture(架构 / 分层) | ||
| 27 | + | ||
| 28 | +遵循 `docs/04 § 2.1`,落点全在 `frontend/**`。**新增/改动**文件: | ||
| 29 | + | ||
| 30 | +``` | ||
| 31 | +frontend/ | ||
| 32 | +├── src/ | ||
| 33 | +│ ├── api/types.ts # 【改】新增 UserVO / PageResult<T> / UserListQuery 契约(沿用 FE-01 既有类型,不破坏) | ||
| 34 | +│ ├── api/usrApi.ts # 【改】新增 listUsers(query): Promise<PageResult<UserVO>>(GET /api/usr/users) | ||
| 35 | +│ ├── pages/usr/UserList/index.tsx # 【新增】UserListPage 页面容器:组合工具栏/筛选栏/表格+分页;持 useUserList 态;导航跳转 | ||
| 36 | +│ ├── pages/usr/UserList/useUserList.ts # 【新增】列表查询 hook(list/total/loading/error/query/exporting + 动作) | ||
| 37 | +│ ├── pages/usr/UserList/UserToolbar.tsx # 【新增】深色工具条:刷新/新增/导出Excel/设置齿轮 | ||
| 38 | +│ ├── pages/usr/UserList/UserFilterBar.tsx # 【新增】筛选栏:范围/查询字段/匹配方式/查询值/更多/搜索/清空 | ||
| 39 | +│ ├── pages/usr/UserList/UserTable.tsx # 【新增】AntD Table:列定义/序号/作废只读/行双击/单选 rowSelection/受控分页 | ||
| 40 | +│ ├── pages/usr/UserList/columns.tsx # 【新增】列定义 + 表头文案 + 作废渲染(与 UserVO 字段映射,spec § 6) | ||
| 41 | +│ ├── pages/usr/UserList/constants.ts # 【新增】合同级常量:查询字段/匹配方式枚举、默认 query、pageSize 选项、文案 | ||
| 42 | +│ ├── pages/usr/UserList/exportUtils.ts # 【新增】前端导出工具(CSV+BOM Blob 下载,D-PLAN-1) | ||
| 43 | +│ └── pages/usr/UserList/UserList.module.css # 【新增】页面 scoped 样式:语义色用 var(--color-*);工具栏深色底局部装饰(D10) | ||
| 44 | +├── src/router/index.tsx # 【改】把 /usr/users 占位 UserListPlaceholder 替换为真实 UserListPage(属 FE 共享骨架,留痕) | ||
| 45 | +└── tests/ | ||
| 46 | + ├── unit/usrApi.userlist.test.ts # 【新增】listUsers 透传 query 到 GET /usr/users 并返回 PageResult(沿用 usrApi.test.ts 桩模式) | ||
| 47 | + ├── unit/useUserList.test.tsx # 【新增】hook 取数/翻页/刷新/搜索/清空/空/错误/导出态(renderHook) | ||
| 48 | + ├── unit/UserFilterBar.test.tsx # 【新增】默认值/枚举 options/回车搜索/清空回调(BR2/BR4/BR7/BR10) | ||
| 49 | + ├── unit/UserTable.test.tsx # 【新增】列渲染/序号/作废只读/行双击导航/受控分页/空 Empty(BR1/BR6/BR11/BR12/BR14) | ||
| 50 | + ├── unit/UserToolbar.test.tsx # 【新增】刷新/新增导航/导出中禁用/齿轮占位(BR8/BR9/BR13/D7) | ||
| 51 | + ├── unit/UserListPage.test.tsx # 【新增】页面集成:挂载默认查询/搜索/翻页/错误重试/空态(状态机 + BR3/BR7/BR15) | ||
| 52 | + └── e2e/userlist.spec.ts # 【新增】E2E:进入用户列表→渲染行/空态/搜索/翻页/行双击进单据/错误重试 | ||
| 53 | +``` | ||
| 54 | + | ||
| 55 | +- **跨阶段/跨模块**:本 FE 落点全在 `frontend/**`,不触 `backend/` / `sql/` / `scripts/`。改 `src/router/index.tsx`(把 `/usr/users` 占位换为真实页)与 `src/api/usrApi.ts` / `src/api/types.ts`(FE-01/FE-02 搭建的全前端共享骨架)属共享资产扩展,在《模块完成报告》留痕:「FE-03 将 `/usr/users` 占位 `UserListPlaceholder` 替换为真实 `UserListPage`;在 `usrApi.ts`/`types.ts` 增列表查询契约(`listUsers`/`UserVO`/`PageResult`/`UserListQuery`),不改 FE-01 既有 `login`/`fetchCompanies`/类型;属共享骨架扩展,FE-04 可复用 `UserVO`」。 | ||
| 56 | +- **状态管理**(docs/04 § 2.2 / spec D6):列表查询态(list/total/loading/error/query/exporting)为页面就近态,用 `useUserList` hook + `useState` 本地管理,不进 Redux;登录态复用 FE-01 `authSlice`(本页不读写)。 | ||
| 57 | +- **请求封装 / 错误处理**(docs/04 § 2.3 / § 2.4):取数走 `usrApi.listUsers` → `request.ts` Axios 实例(响应拦截器已拆 `Result`:`code=0` 返回 `data`,非 0 抛 `ApiError`,被动 401 统一登出 FE-02 D11);页面/hook 捕获 `ApiError` 按 code 分流 `message` 文案 + 错误占位。 | ||
| 58 | +- **Design Tokens**(docs/04 § 2.1 / spec § 7):筛选栏/表格/分页/空态/错误/成功/警告等语义色只用 `var(--color-*)`;工具栏深色底为页面局部装饰,scoped 在 `UserList.module.css`,不新增全局 token、不挪用语义 token(spec § 7 / D10,与 FE-02 § 8 D9 一致)。 | ||
| 59 | + | ||
| 60 | +## Tech Stack(技术栈,源自 docs/04 § 零 + FE-01/FE-02 骨架) | ||
| 61 | + | ||
| 62 | +- React 18 / Ant Design 5(`Table` / `Select` / `Input` / `Button` / `Tag`或只读 `Checkbox` / `Empty` / `Result`或错误占位 / `Spin` / `message` / `Space`/`Form`(可选 inline))/ React Router v6(`useNavigate`)/ Axios / TypeScript;`@ant-design/icons`(`ReloadOutlined` / `PlusCircleOutlined` / `FileExcelOutlined` / `SettingOutlined` / `SearchOutlined`)。 | ||
| 63 | +- **不新增 npm 依赖**:导出用零依赖 CSV(UTF-8 BOM + Blob + `<a download>`),不引入 `xlsx`/SheetJS(D-PLAN-1)。 | ||
| 64 | +- 测试:单测 Vitest(jsdom)+ `@testing-library/react|jest-dom|user-event`(沿用 `tests/setup.ts`、`renderShell.tsx`);E2E Playwright(沿用 `playwright.config.ts`,`page.route` 桩 `**/api/usr/users**`)。 | ||
| 65 | +- 命令(docs/04 § 零):build `npm run build`;lint `npm run lint`;unit `npm run test:unit`;e2e `npm run test:e2e`。子会话验证用 `cd frontend && npm run test:unit -- <文件名片段>`。 | ||
| 66 | +- 提交格式:`<type>(<scope>): <subject> REQ-USR-003`。**scope 统一用 `usr`**(业务模块名,CLAUDE.md § Git);subject 业务类(feat/fix/test)带 `REQ-USR-003` 后缀。每个任务在其 commit 行注明 REQ tag。 | ||
| 67 | + | ||
| 68 | +## 合同级常量(跨 task 必须一致) | ||
| 69 | + | ||
| 70 | +- **API 路径 / 方法**:`GET /api/usr/users`(`usrApi` 内传 `/usr/users`,`request.ts` baseURL=`/api` 已含前缀;query 参数走 axios `params`)。 | ||
| 71 | +- **查询字段枚举**(`QUERY_FIELD_OPTIONS`,对齐 REQ 输入表 1「显示来源」/ docs/05):`用户名` / `员工名` / `用户号` / `部门` / `用户类型` / `作废` / `登录日期` / `制单人`(**逐字一致**,原样作为 `queryField` 提交值,前端不映射,匹配语义由后端裁决)。默认 `用户名`(BR2)。 | ||
| 72 | +- **匹配方式枚举**(`MATCH_TYPE_OPTIONS`):`包含` / `不包含` / `等于`(原样作为 `matchType` 提交值)。默认 `包含`(BR2)。 | ||
| 73 | +- **用户范围下拉**(`SCOPE_OPTIONS`,占位 demo,spec D2):仅 `全部用户` 一项;选中「全部用户」时**不向后端传任何额外参数**(不杜撰 docs/05 未定义的「范围」参数)。 | ||
| 74 | +- **默认查询 `DEFAULT_QUERY: UserListQuery`**:`{ queryField: '用户名', matchType: '包含', queryValue: '', pageNum: 1, pageSize: 10 }`(BR2/BR3,pageSize 默认 10 对齐 docs/05,spec D4)。`queryValue` 为空字符串时提交时省略或传空(后端按全量处理,BR3)。 | ||
| 75 | +- **pageSize 选项**(`PAGE_SIZE_OPTIONS`):`[10, 20, 50, 100]`(上限 100 对齐 docs/05 / REQ 边界,spec D4;不采用原型 demo 的 10000)。 | ||
| 76 | +- **错误码常量**(对齐 docs/05 § REQ-USR-003):`ERR_PAGE_INVALID = 42201`(分页参数非法)、`ERR_QUERY_INVALID = 40001`(查询参数校验失败);网络/超时/5xx 经 `request.ts` 映射为 `NETWORK_ERROR_CODE = -1`(复用 FE-01 常量);`401` 由 `request.ts` 统一处理(本页不分流)。 | ||
| 77 | +- **表格列定义(中文表头,逐字一致,对齐原型 thead + REQ 输出表 1 + spec § 6)**,列顺序固定: | ||
| 78 | + | 列序 | 表头文案 | 数据字段(UserVO) | 渲染 | | ||
| 79 | + |---|---|---|---| | ||
| 80 | + | 0 | (单选列,无表头) | —(`rowSelection` 渲染) | radio 单选标记(spec D8) | | ||
| 81 | + | 1 | `序号` | —(前端生成) | `(pageNum-1)*pageSize + index + 1`(BR1) | | ||
| 82 | + | 2 | `用户名` | `sUserName` | 文本(双击进单据主标识) | | ||
| 83 | + | 3 | `员工名` | `employeeName`(映射自 docs/05 `员工名`,D-PLAN-2) | 文本,可空 | | ||
| 84 | + | 4 | `用户号` | `sUserNo` | 文本,可空 | | ||
| 85 | + | 5 | `部门` | `departmentName`(映射自 docs/05 `部门`,D-PLAN-2) | 文本,可空 | | ||
| 86 | + | 6 | `用户类型` | `sUserType` | 文本(已中文) | | ||
| 87 | + | 7 | `语言` | `sLanguage` | 文本 | | ||
| 88 | + | 8 | `作废` | `iIsVoid` | 只读:`0`→`否`、`1`→`是`(BR6,`Tag` 或禁用 `Checkbox`,点击无效) | | ||
| 89 | + | 9 | `登录日期` | `tLastLoginDate` | 日期时间文本,可空 | | ||
| 90 | + | 10 | `制单人` | `sCreator` | 文本 | | ||
| 91 | + | 11 | `制单日期` | `tCreateDate` | 日期时间文本 | | ||
| 92 | +- **静态文案(逐字一致,复刻原型 / spec)**: | ||
| 93 | + | 用途 | 文案 | | ||
| 94 | + |---|---| | ||
| 95 | + | 工具栏刷新 | `刷新` | | ||
| 96 | + | 工具栏新增 | `新增` | | ||
| 97 | + | 工具栏导出 | `导出Excel` | | ||
| 98 | + | 搜索按钮 | `搜索` | | ||
| 99 | + | 清空按钮 | `清空`(含原型 `⊗` 前缀,文案以「清空」为可定位文本) | | ||
| 100 | + | 空态文案 | `暂无匹配的用户`(BR14) | | ||
| 101 | + | 错误占位文案 | `加载失败,点击重试`(spec § 4) | | ||
| 102 | + | 分页统计文案 | `共 {total} 条记录`(`showTotal`,total 来自 `PageResult.total`,BR1/§ 3 success/empty) | | ||
| 103 | + | 导出成功提示 | `导出成功`(`message.success`,BR9) | | ||
| 104 | + | 导出失败提示 | `导出失败`(`message.error`,BR9) | | ||
| 105 | + | 42201 提示 | `分页参数有误,已重置为第 1 页`(`message.warning`,spec § 4) | | ||
| 106 | + | 40001 提示 | `查询条件有误,请检查后重试`(`message.error`,spec § 4) | | ||
| 107 | + | 网络/5xx 提示 | `加载失败,请稍后重试`(`message.error`,spec § 4) | | ||
| 108 | +- **路由 path(导航目标,FE-02 已注册)**:新增 → `/usr/users/new`(BR13);行双击修改 → `/usr/users/:id`(`navigate('/usr/users/' + row.id)`,BR12)。 | ||
| 109 | +- **localStorage token 键**:`TOKEN_STORAGE_KEY = 'xly_erp_token'`(FE-01 `request.ts` 已导出,本页不直接读写)。 | ||
| 110 | + | ||
| 111 | +## 关键签名(首次出现处给出,跨 task 一致) | ||
| 112 | + | ||
| 113 | +- **类型契约**(`api/types.ts`,新增;沿用 FE-01 既有 `AuthUser`/`LoginPayload` 等不动): | ||
| 114 | + - `UserVO`:`{ id: number; sUserName: string; employeeName: string | null; sUserNo: string | null; departmentName: string | null; sUserType: string; sLanguage: string; iIsVoid: number; tLastLoginDate: string | null; sCreator: string; tCreateDate: string }`。 | ||
| 115 | + > docs/05 `UserVO` 以中文键名 `员工名`/`部门` 给出(后端关联职员表派生);前端在 api 层做一次别名映射 `员工名→employeeName`、`部门→departmentName`,组件/列定义统一用 ASCII 键(spec D9 / D-PLAN-2)。映射在 `usrApi.listUsers` 内对 `records` 逐项归一。 | ||
| 116 | + - `PageResult<T>`:`{ records: T[]; total: number; pageNum: number; pageSize: number }`(docs/04 § 1.4 / § 3.2)。 | ||
| 117 | + - `UserListQuery`:`{ queryField?: string; matchType?: string; queryValue?: string; pageNum: number; pageSize: number }`(提交给 `GET /api/usr/users` 的 query;`queryValue` 空时省略,BR3)。 | ||
| 118 | +- **API 封装**(`api/usrApi.ts`,新增方法): | ||
| 119 | + - `listUsers(query: UserListQuery): Promise<PageResult<UserVO>>`——`request.get('/usr/users', { params })`,对返回 `records` 做中文键→ASCII 别名归一后返回(D-PLAN-2)。响应拦截器已拆 `Result.data`,故此方法返回 `PageResult<UserVO>` 本体(沿用 FE-01 `as unknown as Promise<...>` 桥接模式)。 | ||
| 120 | +- **页面 hook**(`pages/usr/UserList/useUserList.ts`): | ||
| 121 | + - `useUserList()` → `{ list: UserVO[]; total: number; loading: boolean; error: ApiError | null; query: UserListQuery; exporting: boolean; search(): void; refresh(): void; clear(): void; setQueryField(v: string): void; setMatchType(v: string): void; setQueryValue(v: string): void; changePage(pageNum: number, pageSize: number): void; exportExcel(): Promise<void>; }`。 | ||
| 122 | + - 挂载时以 `DEFAULT_QUERY` 取数(initialLoading,BR2);`search()` 回第 1 页以当前条件取数(BR7);`refresh()` 保持当前 `query`(含 `pageNum`)重取(BR8);`clear()` 重置为 `DEFAULT_QUERY` 后取数(BR10);`changePage()` 改页/页大小重取(改 pageSize 回第 1 页,BR11);取数成功同步 `total`/`pageNum`/`pageSize` 以响应回显(BR15);非 0 code 按错误码分流 `message`(42201 warning+重置重查、40001 error 保留条件不自动重查、网络兜底 error),并置 `error` 供错误占位(spec § 4)。 | ||
| 123 | + - `exportExcel()`:置 `exporting=true` → 调 `listUsers` 拉当前条件命中结果(按 `total` 在 `pageSize≤100` 约束内一次或分批取,D-PLAN-1)→ 生成 CSV Blob 下载 → `message.success('导出成功')` / 失败 `message.error('导出失败')` → `exporting=false`(BR9)。 | ||
| 124 | +- **导出工具**(`pages/usr/UserList/exportUtils.ts`): | ||
| 125 | + - `buildUserCsv(rows: UserVO[]): string`——按列定义顺序与中文表头生成 CSV 文本(含表头行,作废 0/1→否/是,空值→空串)。 | ||
| 126 | + - `downloadCsv(filename: string, csv: string): void`——前置 UTF-8 BOM(``)→ `new Blob` → `URL.createObjectURL` → 触发 `<a download>`(jsdom 下可桩 `URL.createObjectURL`/`a.click` 验证调用)。 | ||
| 127 | +- **组件 props**(`pages/usr/UserList/`): | ||
| 128 | + - `UserToolbar`(`{ onRefresh(): void; onAdd(): void; onExport(): void; exporting: boolean; loading?: boolean }`):刷新/新增/导出/齿轮;导出中 `导出Excel` 置 `loading` 且禁用(BR9);齿轮无动作(spec D7)。 | ||
| 129 | + - `UserFilterBar`(`{ query: UserListQuery; onChangeQueryField(v): void; onChangeMatchType(v): void; onChangeQueryValue(v): void; onSearch(): void; onClear(): void }`):渲染 `SCOPE_OPTIONS`/`QUERY_FIELD_OPTIONS`/`MATCH_TYPE_OPTIONS` 下拉 + 查询值 `Input`(`onPressEnter`→`onSearch`,BR7)+ 搜索/清空按钮;更多「▾」占位(spec D3)。 | ||
| 130 | + - `UserTable`(`{ rows: UserVO[]; loading: boolean; total: number; pageNum: number; pageSize: number; onChangePage(pageNum, pageSize): void; onRowDoubleClick(row: UserVO): void; selectedRowKey?: number | null; onSelectRow?(key: number | null): void }`):AntD `Table` `rowKey="id"`,`columns` 来自 `columns.tsx`,受控 `pagination={{ current:pageNum, pageSize, total, showSizeChanger:true, pageSizeOptions:PAGE_SIZE_OPTIONS, showTotal:(t)=>`共 ${t} 条记录` }}`,`rowSelection={{ type:'radio', selectedRowKeys, onChange }}`(spec D8),`onRow=(row)=>({ onDoubleClick: ()=>onRowDoubleClick(row) })`(BR12),空 `rows` → `locale.emptyText` 为 AntD `Empty`「暂无匹配的用户」(BR14)。 | ||
| 131 | + - `UserListPage`(default export,无 props):`useUserList()` + `useNavigate()`;装配 `UserToolbar`/`UserFilterBar`/`UserTable`;`onAdd`→`navigate('/usr/users/new')`(BR13);`onRowDoubleClick`→`navigate('/usr/users/'+row.id)`(BR12);错误态渲染错误占位 +「点击重试」→ `refresh()`(spec § 4)。 | ||
| 132 | +- **列定义**(`pages/usr/UserList/columns.tsx`): | ||
| 133 | + - `buildUserColumns(opts: { pageNum: number; pageSize: number }): ColumnsType<UserVO>`——返回上表 1~11 列(序号列用 `render:(_,__,index)=>(pageNum-1)*pageSize+index+1`,BR1;作废列 `render:(v)=> v===1 ? '是' : '否'`,只读 BR6)。 | ||
| 134 | + | ||
| 135 | +## 测试栈说明 | ||
| 136 | + | ||
| 137 | +- **jsdom 组件 / hook / api 单测**(Vitest + RTL):组件测用 `renderShell`(已存在,Provider + 真实 store + `MemoryRouter` + AntD `App`/`ConfigProvider`);导航断言用 `LocationProbe`(复用 `HomePage.test.tsx` 的 `useLocation` 探针模式)+ 路由哨兵(`/usr/users/new`、`/usr/users/:id` 哨兵元素)。`useUserList` 用 `renderHook`,对 `usrApi.listUsers` 用 `vi.mock('../../src/api/usrApi')` 桩(沿用 `usrApi.test.ts` 的 `vi.mock` 模式)。`listUsers` api 测桩底层 `request` 实例(同 `usrApi.test.ts`)。 | ||
| 138 | +- **Playwright E2E**:`page.route` 桩 `**/api/usr/users**`(先返回非空 `records` 验渲染/翻页/行双击;再用单独用例桩空 `records` 验空态、桩 5xx 验错误重试);先经登录桩落地外壳再进 `/usr/users`(沿用 `shell.spec.ts` 的 `stubBackend`/`login` 模式,可抽到本 spec 内)。不依赖真实后端起服。 | ||
| 139 | +- **可测性**:优先语义查询(role/text/label/columnheader);仅当无法稳定定位时加最小 `data-testid`(如 `user-table` / `filter-query-field` / `filter-match-type` / `filter-query-value` / `btn-search` / `btn-clear` / `btn-refresh` / `btn-export` / `userlist-error`)。 | ||
| 140 | + | ||
| 141 | +--- | ||
| 142 | + | ||
| 143 | +## 任务列表(每个 task = red → green → 子会话验证 → commit) | ||
| 144 | + | ||
| 145 | +> 硬护栏:以下每个 `impl_file` / `test_file` 均以 `frontend/` 开头;无任何 `backend/` / `sql/` / `scripts/` 落点。 | ||
| 146 | +> 提交 scope 统一 `usr`;REQ tag 统一 `REQ-USR-003`。 | ||
| 147 | + | ||
| 148 | +### T1 — 类型契约 + listUsers API 封装(GET /api/usr/users,中文键归一 D-PLAN-2)(jsdom api 测) | ||
| 149 | +- **测试先行类型**:jsdom 组件测试(api 单测) | ||
| 150 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/usrApi.userlist.test.ts`(沿用 `usrApi.test.ts` 的 `vi.mock('../../src/api/request')` 桩底层实例): | ||
| 151 | + - `::listUsers gets /usr/users with query params`——调 `listUsers({ queryField:'用户名', matchType:'包含', queryValue:'李', pageNum:2, pageSize:20 })`,断言 `request.get` 以 `'/usr/users'` + `{ params: { ...含传入字段 } }` 被调用。 | ||
| 152 | + - `::listUsers omits empty queryValue`——`queryValue:''` 时 params 不含 `queryValue`(或传空,按实现一处定,与 BR3 一致;测试断言后端不收到非空 queryValue)。 | ||
| 153 | + - `::listUsers normalizes chinese keys 员工名/部门 to employeeName/departmentName`——桩 `request.get` resolve `{ records:[{ id:1, sUserName:'a', 员工名:'张三', 部门:'技术', sUserNo:'a', sUserType:'超级管理员', sLanguage:'中文', iIsVoid:0, tLastLoginDate:null, sCreator:'x', tCreateDate:'t' }], total:1, pageNum:2, pageSize:20 }`,断言返回 `records[0].employeeName==='张三'`、`departmentName==='技术'`(D-PLAN-2)。 | ||
| 154 | +- [ ] **2. 实现最小代码**:`frontend/src/api/types.ts`(新增 `UserVO`/`PageResult<T>`/`UserListQuery`,签名见关键签名)+ `frontend/src/api/usrApi.ts`(新增 `listUsers`,`request.get('/usr/users',{params})` + records 中文键归一)。 | ||
| 155 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- usrApi`(含 FE-01 `usrApi` 既有用例不回归) | ||
| 156 | +- [ ] **4. commit**:`feat(usr): 用户列表查询 API 与类型契约 listUsers REQ-USR-003` | ||
| 157 | + | ||
| 158 | +### T2 — 页面常量 + 导出工具(枚举/默认/pageSize/文案 + CSV 下载,D-PLAN-1)(jsdom 单测) | ||
| 159 | +- **测试先行类型**:jsdom 组件测试(纯逻辑断言) | ||
| 160 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/UserToolbar.test.tsx` 暂不依赖此,单独建 `frontend/tests/unit/exportUtils.test.ts`: | ||
| 161 | + - `::buildUserCsv has header row and maps 作废 0/1`——传 2 行(`iIsVoid:0` 与 `1`),断言 CSV 首行含中文表头(`用户名`/`员工名`/`作废` 等逐字),数据行作废列分别为 `否`/`是`,空值字段为空串。 | ||
| 162 | + - `::downloadCsv triggers blob download with UTF-8 BOM`——桩 `URL.createObjectURL`(vi.fn)与 `HTMLAnchorElement.prototype.click`,调 `downloadCsv('users.csv', 'x')`,断言 `createObjectURL` 收到的 Blob 内容以 `` 开头且 `click` 被调一次。 | ||
| 163 | + - (常量断言并入此文件或 `useUserList.test.tsx`,TDD 期定一处):`DEFAULT_QUERY.queryField==='用户名'`、`matchType==='包含'`、`pageSize===10`、`PAGE_SIZE_OPTIONS` 末项为 `100`、`QUERY_FIELD_OPTIONS` 含 8 项首项 `用户名`、`MATCH_TYPE_OPTIONS` 为 `['包含','不包含','等于']`。 | ||
| 164 | +- [ ] **2. 实现最小代码**:`frontend/src/pages/usr/UserList/constants.ts`(枚举/默认/pageSize/错误码/文案常量,见合同级常量)+ `frontend/src/pages/usr/UserList/exportUtils.ts`(`buildUserCsv`/`downloadCsv`,签名见关键签名)。 | ||
| 165 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- exportUtils` | ||
| 166 | +- [ ] **4. commit**:`feat(usr): 用户列表页面常量与前端 CSV 导出工具 REQ-USR-003` | ||
| 167 | + | ||
| 168 | +### T3 — useUserList 列表查询 hook(状态机:initialLoading/loading/success/empty/error/exporting)(jsdom hook 测) | ||
| 169 | +- **测试先行类型**:jsdom 组件测试(`renderHook`,`vi.mock('../../src/api/usrApi')`) | ||
| 170 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/useUserList.test.tsx`: | ||
| 171 | + - `::mounts with default query and loads first page (initialLoading→success)`——`listUsers` resolve 非空 `records`,断言挂载即以 `DEFAULT_QUERY` 调用 `listUsers`、`loading` 由 true→false、`list`/`total`/`query.pageNum` 同步(BR2/§ 3 initialLoading/success)。 | ||
| 172 | + - `::empty records sets empty state without error`——resolve `records:[]`,`total:0`,断言 `list` 为空、`error===null`、无 `message.error`(BR14/§ 3 empty)。 | ||
| 173 | + - `::search resets to page 1 and refetches with current filters`——改 `queryField`/`queryValue` 后 `search()`,断言以 `pageNum:1` + 当前条件再次调用(BR7)。 | ||
| 174 | + - `::refresh keeps current query and page`——先翻到第 2 页,`refresh()` 断言以**当前** `pageNum`(2)+ 当前条件调用,不回第 1 页、不重置条件(BR8)。 | ||
| 175 | + - `::clear resets to DEFAULT_QUERY then refetches`——改条件后 `clear()`,断言 `query` 回 `DEFAULT_QUERY`(用户名/包含/空/page1)并以之取数(BR10)。 | ||
| 176 | + - `::changePage refetch; changing pageSize resets to page 1`——`changePage(3, 10)` 以 pageNum=3 取;`changePage(1, 50)`(改 pageSize)回第 1 页取(BR11)。 | ||
| 177 | + - `::ApiError 40001 keeps filters and shows error, sets error state`——`listUsers` reject `new ApiError(40001,...)`,断言 `error` 置位、`query` 未变、不自动重查(spec § 4 / 40001)。 | ||
| 178 | + - `::ApiError 42201 warns and refetches at page 1`——reject `ApiError(42201)`,断言重置 `pageNum=1`(与 pageSize 收敛)后重查(spec § 4 / 42201 兜底)。 | ||
| 179 | + - `::network error (code -1) sets error state`——reject `ApiError(-1)`,断言 `error` 置位(错误占位由页面消费,spec § 4)。 | ||
| 180 | + - `::exportExcel toggles exporting and downloads`——桩 `listUsers` 返回结果集 + 桩 `downloadCsv`,调 `exportExcel()`,断言 `exporting` true→false、`downloadCsv` 被调(BR9/§ 3 exporting);reject 时 `message.error('导出失败')`。 | ||
| 181 | + > 取数响应回显(BR15):success 用例额外断言 `query.pageNum`/`pageSize` 跟随响应 `PageResult` 同步(即使请求 pageNum 越界,前端信任响应回显)。`message` 断言可桩 `antd` `App.useApp().message` 或 `vi.mock('antd', ...)` 暴露 `message`,TDD 期定一处。 | ||
| 182 | +- [ ] **2. 实现最小代码**:`frontend/src/pages/usr/UserList/useUserList.ts`(签名见关键签名;内部 `useState` 持 `query`/`list`/`total`/`loading`/`error`/`exporting`,`useEffect` 挂载取数;错误码分流按合同级常量;导出复用 T2 `exportUtils`)。 | ||
| 183 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- useUserList` | ||
| 184 | +- [ ] **4. commit**:`feat(usr): 用户列表查询 hook useUserList 状态机 REQ-USR-003` | ||
| 185 | + | ||
| 186 | +### T4 — UserFilterBar 筛选栏(默认值/枚举 options/回车搜索/清空,BR2/BR3/BR4/BR7/BR10/D2/D3)(jsdom 组件测) | ||
| 187 | +- **测试先行类型**:jsdom 组件测试 | ||
| 188 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/UserFilterBar.test.tsx`(`renderShell` 提供 AntD App 上下文): | ||
| 189 | + - `::renders defaults 用户名 / 包含 and empty value`——传 `query=DEFAULT_QUERY`,断言查询字段下拉显「用户名」、匹配方式显「包含」、查询值框为空(BR2)。 | ||
| 190 | + - `::query field options match enum`——展开查询字段下拉,断言 8 个选项逐字一致(用户名…制单人,BR4);展开匹配方式断言 `包含/不包含/等于`(BR4)。 | ||
| 191 | + - `::scope select shows 全部用户 only`——范围下拉仅「全部用户」(占位 demo,D2)。 | ||
| 192 | + - `::Enter in value triggers onSearch`——查询值框聚焦回车 → `onSearch` 被调一次(BR7)。 | ||
| 193 | + - `::click 搜索 calls onSearch / click 清空 calls onClear`——点「搜索」→ `onSearch`;点「清空」→ `onClear`(BR7/BR10)。 | ||
| 194 | + - `::changing selects/value calls respective onChange`——改下拉/输入触发 `onChangeQueryField`/`onChangeMatchType`/`onChangeQueryValue`。 | ||
| 195 | + - `::more toggle ▾ is placeholder (no extra callback)`——「▾」点击不触发查询回调(占位,D3)。 | ||
| 196 | +- [ ] **2. 实现最小代码**:`frontend/src/pages/usr/UserList/UserFilterBar.tsx`(签名见关键签名;options 来自 T2 常量;`Input` `onPressEnter`→`onSearch`)+ `UserList.module.css` 筛选栏样式(语义色用 `var(--color-*)`:白底 `--color-form-bg-edit`、下边线 `--color-border`、搜索按钮主色 `--color-primary`)。 | ||
| 197 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- UserFilterBar` | ||
| 198 | +- [ ] **4. commit**:`feat(usr): 用户列表筛选栏 UserFilterBar REQ-USR-003` | ||
| 199 | + | ||
| 200 | +### T5 — UserTable 表格(列/序号/作废只读/行双击/单选/受控分页/空态,BR1/BR6/BR11/BR12/BR14/D8)(jsdom 组件测) | ||
| 201 | +- **测试先行类型**:jsdom 组件测试 | ||
| 202 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/UserTable.test.tsx`(`renderShell`): | ||
| 203 | + - `::renders 11 column headers in order`——断言列头文案逐字、顺序与合同级常量列表一致(序号…制单日期)。 | ||
| 204 | + - `::serial number is page-aware`——传 `pageNum=2, pageSize=10` + 2 行,断言首行序号为 `11`(`(2-1)*10+0+1`,BR1)。 | ||
| 205 | + - `::作废 column renders 否/是 read-only`——`iIsVoid:0`→「否」、`1`→「是」;点击作废单元不触发任何 onChange/写动作(BR6)。 | ||
| 206 | + - `::double click row navigates via onRowDoubleClick`——双击某数据行 → `onRowDoubleClick` 收到该行(含 `id`,BR12)。 | ||
| 207 | + - `::controlled pagination reflects current/pageSize/total + showTotal`——传 `total=37,pageNum=1,pageSize=10`,断言分页显示「共 37 条记录」、当前页 1;点下一页/改每页条数 → `onChangePage` 收到新 `(pageNum,pageSize)`(改 pageSize 期望回第 1 页由页面/hook 处理,本组件原样上报,BR11)。 | ||
| 208 | + - `::empty rows shows Empty 暂无匹配的用户`——`rows=[]` → 渲染「暂无匹配的用户」(BR14)。 | ||
| 209 | + - `::radio rowSelection single-select does not affect query`——选中某行 → `onSelectRow` 收到该行 key,不触发取数/查询(spec D8;本组件不持查询态,断言仅回调)。 | ||
| 210 | +- [ ] **2. 实现最小代码**:`frontend/src/pages/usr/UserList/columns.tsx`(`buildUserColumns`,序号/作废 render)+ `frontend/src/pages/usr/UserList/UserTable.tsx`(AntD `Table` 受控分页/rowSelection/onRow 双击/空态 locale)+ `UserList.module.css` 表格样式(表头 `--color-table-header-bg`/`--color-table-header-fg`、行文字 `--color-table-row-fg`、hover `--color-table-row-bg-hover`、选中 `--color-table-row-bg-selected`、网格线 `--color-border`)。 | ||
| 211 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- UserTable` | ||
| 212 | +- [ ] **4. commit**:`feat(usr): 用户列表表格 UserTable 与列定义 REQ-USR-003` | ||
| 213 | + | ||
| 214 | +### T6 — UserToolbar 工具栏(刷新/新增导航/导出中禁用/齿轮占位,BR8/BR9/BR13/D7/D10)(jsdom 组件测) | ||
| 215 | +- **测试先行类型**:jsdom 组件测试 | ||
| 216 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/UserToolbar.test.tsx`(`renderShell`): | ||
| 217 | + - `::renders 刷新/新增/导出Excel/设置 buttons`——可见四个工具项(文案逐字 + 对应图标)。 | ||
| 218 | + - `::click 刷新 calls onRefresh / click 新增 calls onAdd`——点刷新→`onRefresh`;点新增→`onAdd`(BR8/BR13)。 | ||
| 219 | + - `::click 导出Excel calls onExport`——点导出→`onExport`(BR9)。 | ||
| 220 | + - `::exporting disables 导出Excel and shows loading`——`exporting=true` 时「导出Excel」禁用且 loading,再点不再触发 `onExport`(BR9)。 | ||
| 221 | + - `::gear setting is placeholder (no callback)`——点齿轮无任何业务回调(占位,D7)。 | ||
| 222 | +- [ ] **2. 实现最小代码**:`frontend/src/pages/usr/UserList/UserToolbar.tsx`(签名见关键签名;图标 `ReloadOutlined`/`PlusCircleOutlined`/`FileExcelOutlined`/`SettingOutlined`)+ `UserList.module.css` 工具栏深色底(scoped 局部装饰,非语义 token,D10)。 | ||
| 223 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- UserToolbar` | ||
| 224 | +- [ ] **4. commit**:`feat(usr): 用户列表工具栏 UserToolbar REQ-USR-003` | ||
| 225 | + | ||
| 226 | +### T7 — UserListPage 页面集成 + 路由接线(状态机贯通 + 导航 + 错误重试,BR3/BR7/BR8/BR9/BR12/BR13/BR15)(jsdom 组件测) | ||
| 227 | +- **测试先行类型**:jsdom 组件测试 | ||
| 228 | +- [ ] **1. 写失败测试**:`frontend/tests/unit/UserListPage.test.tsx`(`renderShell`,`vi.mock('../../src/api/usrApi')` 桩 `listUsers`;用 `LocationProbe` + `/usr/users/new`、`/usr/users/:id` 哨兵路由验导航): | ||
| 229 | + - `::initial load renders rows from listUsers (default query)`——挂载即调 `listUsers(DEFAULT_QUERY)`、渲染返回行(BR2/initialLoading→success)。 | ||
| 230 | + - `::search with value submits queryValue and shows results`——填查询值「李」点搜索 → `listUsers` 以 `queryValue:'李', pageNum:1` 调用、渲染结果(BR7/BR3)。 | ||
| 231 | + - `::empty response shows 暂无匹配的用户`——桩空 `records` → 空态(BR14)。 | ||
| 232 | + - `::error response shows error placeholder with 点击重试; retry calls refresh`——桩 reject `ApiError(-1)` → 可见「加载失败,点击重试」;点重试再次取数(spec § 4)。 | ||
| 233 | + - `::新增 navigates to /usr/users/new`——点工具栏「新增」→ URL/哨兵到 `/usr/users/new`(BR13)。 | ||
| 234 | + - `::double click row navigates to /usr/users/:id`——桩返回含 `id` 的行,双击 → URL/哨兵到 `/usr/users/{id}`(BR12)。 | ||
| 235 | + - `::refresh keeps current page`——翻到第 2 页后点刷新 → `listUsers` 以 `pageNum:2` 调用(BR8)。 | ||
| 236 | + - `::response pageNum echo syncs pagination`——请求越界 `pageNum`、桩响应回 `pageNum=最后一页`,断言分页当前页跟随响应(BR15)。 | ||
| 237 | +- [ ] **2. 实现最小代码**:`frontend/src/pages/usr/UserList/index.tsx`(`UserListPage`:`useUserList` + `useNavigate`,装配 `UserToolbar`/`UserFilterBar`/`UserTable`,错误态渲染「加载失败,点击重试」→`refresh`,新增/行双击导航);改 `frontend/src/router/index.tsx`——把 `/usr/users` 的 `UserListPlaceholder` 替换为 `UserListPage`(移除占位组件,import 真实页;`/usr/users/new`、`/usr/users/:id` 仍为 FE-04 占位,不动)。 | ||
| 238 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:unit -- UserListPage router`(确认 FE-02 `router.test.tsx` 不回归——若其断言 `/usr/users` 渲染占位 `data-testid`,需同步更新该断言为真实页可定位元素,并在 commit 说明) | ||
| 239 | +- [ ] **4. commit**:`feat(usr): 用户列表页面集成与路由接线 UserListPage REQ-USR-003` | ||
| 240 | + | ||
| 241 | +### T8 — E2E 用户列表关键旅程(Playwright) | ||
| 242 | +- **测试先行类型**:Playwright E2E | ||
| 243 | +- [ ] **1. 写失败测试**:`frontend/tests/e2e/userlist.spec.ts`(沿用 `shell.spec.ts` 的登录桩;`page.route('**/api/usr/users**')` 按用例返回不同体): | ||
| 244 | + - `::enter user list renders rows`——桩非空 `records`,从主页「常用操作 > 用户列表」进入 `/usr/users`,断言表格出现某用户名行 + 分页「共 N 条记录」。 | ||
| 245 | + - `::empty result shows 暂无匹配的用户`——桩空 `records`,断言空态文案。 | ||
| 246 | + - `::search by value triggers query`——填查询值点「搜索」,断言请求携带 `queryValue`(用 `page.waitForRequest('**/api/usr/users**')` 校验 URL 含 `queryValue`)并渲染结果。 | ||
| 247 | + - `::pagination next page refetches`——桩 `total>pageSize`,点下一页,断言请求 `pageNum=2`。 | ||
| 248 | + - `::double click row navigates to user detail`——双击行 → URL 到 `/usr/users/{id}`(FE-04 占位即可,本 FE 不验单据内容)。 | ||
| 249 | + - `::error response shows retry`——桩 5xx,断言「加载失败,点击重试」可见;点重试(改桩为成功)后渲染行。 | ||
| 250 | +- [ ] **2. 实现最小代码**:补任何为可测性需要的最小 `data-testid`(仅 Playwright 无法稳定定位时,如 `user-table`/`btn-search`/`btn-refresh`/`userlist-error`)。沿用 `playwright.config.ts`。 | ||
| 251 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run test:e2e -- userlist` | ||
| 252 | +- [ ] **4. commit**:`test(usr): 用户列表 E2E 关键旅程 REQ-USR-003` | ||
| 253 | + | ||
| 254 | +### T9 — 全量门禁回归 + 收尾(chore) | ||
| 255 | +- **测试先行类型**:无新增测试(全量验证) | ||
| 256 | +- [ ] **1. 写失败测试**:无。 | ||
| 257 | +- [ ] **2. 实现最小代码**:修 lint / build(`tsc --noEmit`)/ 类型问题;确认语义色全部 `var(--color-*)`、无硬编码 hex/rgba(工具栏深色装饰 scoped 例外,D10);确认无 `TBD/TODO/【人工填写】`;确认 FE-01/FE-02 既有单测/E2E 不回归(尤其 `router.test.tsx`、`shell.spec.ts`)。 | ||
| 258 | +- [ ] **3. 子会话验证 PASS**:`cd frontend && npm run lint && npm run build && npm run test:unit && npm run test:e2e` 全绿。 | ||
| 259 | +- [ ] **4. commit**:`chore(usr): FE-03 门禁回归通过 REQ-USR-003` | ||
| 260 | + | ||
| 261 | +--- | ||
| 262 | + | ||
| 263 | +## 完成判据(Definition of Done) | ||
| 264 | + | ||
| 265 | +1. `/usr/users` 渲染真实 `UserListPage`(复刻原型 `#screen-userlist` 的工具栏/筛选栏/用户表格/分页),数据/筛选/分页真实对接 `GET /api/usr/users`(spec § 1/§ 2 / D1)。 | ||
| 266 | +2. 状态机覆盖并测试固化:`initialLoading`(T3/T7)、`loading`(T3)、`success`(T3/T5/T7)、`empty`(T3/T5/T7)、`error`(T3/T7)、`exporting`(T3/T6)(spec § 3)。 | ||
| 267 | +3. 业务规则 BR1~BR15 在 hook/组件/E2E 有断言:BR1(T5)、BR2(T3/T4)、BR3(T3/T7)、BR4(T4)、BR5(只读:全程仅 GET,T1/T3)、BR6(T5)、BR7(T3/T4/T7)、BR8(T3/T6/T7)、BR9(T3/T6)、BR10(T3/T4)、BR11(T3/T5)、BR12(T5/T7/T8)、BR13(T6/T7/T8)、BR14(T3/T5/T7/T8)、BR15(T3/T7)(spec § 5)。 | ||
| 268 | +4. 列定义/字段映射对齐 REQ 输出表 1 + 原型 thead + `UserVO`(11 列顺序/表头/作废只读,spec § 6 / D9);`员工名`/`部门` 中文键在 api 层归一为 `employeeName`/`departmentName`(D-PLAN-2)。 | ||
| 269 | +5. API 经 `usrApi.listUsers` → `request.ts`(拆 `Result`、`ApiError` 分流、被动 401 统一登出复用 FE-02),页面不散用 axios(docs/04 § 2.3);列表态在页面 hook 不进 Redux(docs/04 § 2.2 / spec D6)。 | ||
| 270 | +6. 错误码分流文案对齐 spec § 4(42201 warning+重置重查、40001 error 保留条件、网络兜底 error、空态不报错)。 | ||
| 271 | +7. 导出为前端零依赖 CSV(UTF-8 BOM Blob 下载),不杜撰后端导出端点、不新增 npm 依赖(spec D5 / D-PLAN-1)。 | ||
| 272 | +8. 语义色只用 `var(--color-*)`,AntD `colorPrimary` 沿用 FE-01 `ConfigProvider`;工具栏深色底 scoped 装饰不新增全局 token、不挪用语义 token(spec § 7 / D10)。 | ||
| 273 | +9. 全部落点在 `frontend/**`,无 `backend/` / `sql/` / `scripts/` 改动;改 `router/index.tsx`(占位换真实页)、`usrApi.ts`/`types.ts`(增列表契约)属共享骨架,已在《模块完成报告》留痕。 | ||
| 274 | +10. 门禁全绿:`npm run lint` / `npm run build` / `npm run test:unit` / `npm run test:e2e`(docs/04 § 零)。 | ||
| 275 | + | ||
| 276 | +## 自审记录 | ||
| 277 | + | ||
| 278 | +- **占位符扫描**:本计划无 `【人工填写:】` / `TBD` / `TODO` 真实占位(正文 `TBD/TODO` 仅作为「禁止出现的字样」被引用)。 | ||
| 279 | +- **spec coverage**:§ 1 关联 REQ/原型/路由/落地目录 → 架构 + T1/T7(路由接线);§ 2 组件树 → T4(FilterBar)/T5(Table+列)/T6(Toolbar)/T7(Page 装配);§ 3 状态机 6 态 → 见 DoD 第 2 条;§ 4 端点/错误码 → T1(api)/T3(错误码分流)/T7(错误占位重试);§ 5 BR1-BR15 → 见 DoD 第 3 条逐条映射;§ 6 列定义/字段映射 → T5/columns + T1(归一);§ 7 tokens → T4/T5/T6/T9;§ 8 decisions D1-D10 已落实于架构/合同级常量/任务(D1 真实对接=全任务;D2 范围占位=T4;D3 更多占位=T4;D4 pageSize=T2/T5;D5 导出=T2/T3/T6 → 细化为 D-PLAN-1;D6 本地 hook=T3;D7 齿轮占位=T6;D8 单选=T5;D9 中文键=T1 → 细化为 D-PLAN-2;D10 工具栏深色=T6)。本计划新增/细化 D-PLAN-1、D-PLAN-2 见下。 | ||
| 280 | +- **本计划细化决策**: | ||
| 281 | + - **D-PLAN-1(导出实现具体化 spec D5)**:spec D5 允许「用前端库(如 xlsx/SheetJS)」且为 medium 置信度并写明「实现期如后端补导出端点可切换」。经核 `frontend/package.json`,**项目未声明 `xlsx`/SheetJS 依赖**,且现有前端无新增 npm 依赖先例;为不引入新依赖、不杜撰后端端点,采用**零依赖 CSV 导出**(拉当前条件命中结果 → UTF-8 BOM CSV → Blob + `<a download>` 下载),文件名 `用户列表.csv`。导出语义(导出当前查询命中结果、过程禁用按钮、成功/失败提示)与 BR9 完全一致。置信度 medium。 | ||
| 282 | + - **D-PLAN-2(中文键归一具体化 spec D9)**:spec D9 允许「在 api 层做一次到 `employeeName`/`departmentName` 的别名映射后供组件用」。本计划锁定该路径:`usrApi.listUsers` 内对 `PageResult.records` 逐项把 `员工名→employeeName`、`部门→departmentName` 归一,组件/列定义统一用 ASCII 键(避免 TS 中文键访问与 lint 摩擦);列渲染语义与 docs/05 契约不变。置信度 high。 | ||
| 283 | +- **类型一致性**:`UserVO`(ASCII 键,跨 T1/T3/T5/T7 一致)、`PageResult<T>`、`UserListQuery`、`DEFAULT_QUERY`、`QUERY_FIELD_OPTIONS`/`MATCH_TYPE_OPTIONS`/`SCOPE_OPTIONS`/`PAGE_SIZE_OPTIONS`、错误码常量(42201/40001/-1)、列表表头文案与顺序、`listUsers`/`useUserList`/`UserToolbar`/`UserFilterBar`/`UserTable`/`buildUserColumns`/`buildUserCsv`/`downloadCsv` 签名跨 T1-T9 一致;路由 path(`/usr/users` / `/usr/users/new` / `/usr/users/:id`)与 FE-02 合同级常量一致;`ApiError`/`TOKEN_STORAGE_KEY`/`NETWORK_ERROR_CODE` 复用 FE-01 `request.ts`。 | ||
| 284 | +- **作用域自审**:所有 `impl_file` / `test_file` 均以 `frontend/` 开头;无 `backend/` / `sql/` / `scripts/` 落点。改 `src/router/index.tsx`、`src/api/usrApi.ts`、`src/api/types.ts` 属 FE 共享骨架扩展(非新阶段越界),已在架构段与 DoD 第 9 条登记留痕要求。 |