2026-06-01-FE-03.md 42.2 KB

FE-03 用户列表与查询 — 任务级 TDD 计划(前端)

阶段:前端(frontend)。作用域:frontend/**(页面 / api / 类型 / 样式 / 测试)。禁止backend/** / sql/** / scripts/**。 上游 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-003GET /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。 复用资产:FE-01(api/request.ts 已建 Axios 实例 + Result 拆包 + ApiError + TOKEN_STORAGE_KEYapi/types.ts)、FE-02(AppLayout 外壳 + RequireAuth 守卫 + 标签栈 useTabStack + 路由表 router/index.tsx 已挂 /usr/users 占位 UserListPlaceholder)。 本计划告诉 TDD 执行者做什么 / 文件边界 / 测试意图 / props 与类型契约 / 完成判据;具体代码由红-绿-提交循环产出,不在此 dump 整组件 / 类型文件内容。 本 FE 为只读查询:仅 GET /api/usr/users,无任何写副作用(spec § 5 BR5)。查询匹配语义、敏感字段过滤、分页越界回退等真伪裁决全部在后端,前端只采集条件 + 发起分页查询 + 依响应渲染表格 / 分页 / 空态 / 错误态 + 导航跳转。


Goal(目标)

把 FE-02 在 router/index.tsx 留下的 /usr/users 占位(UserListPlaceholder)替换为真实「用户列表与查询」页面,复刻原型 #screen-userlist 的布局与交互语义(工具栏 / 筛选栏 / 用户表格 / 分页栏),数据/筛选/分页真实对接 GET /api/usr/users

  • 页面容器 UserListPage(路由 /usr/users,渲染于 FE-02 AppLayout<Outlet/> 内):纵向组合 UserToolbar + UserFilterBar + UserTable + 内置分页,状态由页面本地 hook useUserList 持有(spec § 8 D6,不进 Redux)。
  • API 封装api/usrApi.ts 新增 listUsers(query): Promise<PageResult<UserVO>>(页面只调封装方法,不散用 axios,docs/04 § 2.3);api/types.ts 新增 UserVO / PageResult<T> / UserListQuery 契约。
  • 状态机 ≥6 态(spec § 3):initialLoading / loading(查询·翻页·刷新进行中)/ success / empty / error / exporting,均有测试固化。
  • 业务规则 BR1~BR15 在组件层 / hook 层 / E2E 有断言(spec § 5)。
  • 工具栏:刷新(保持当前条件+当前页重取,BR8)/ 新增(navigate('/usr/users/new'),BR13)/ 导出Excel(导出当前条件命中结果,BR9 / D5 见下方决策 D-PLAN-1)/ 设置齿轮(占位,spec D7)。
  • 筛选栏:用户范围下拉(占位不传后端参数,spec D2)/ 查询字段下拉(默认「用户名」,BR2/BR4)/ 匹配方式下拉(默认「包含」,BR2/BR4)/ 查询值输入(空为全部,BR3,回车触发搜索 BR7)/ 更多「▾」(占位,spec D3)/ 搜索(回第 1 页查询,BR7)/ 清空(重置默认并全量查询,BR10)。
  • 用户表格:AntD TablerowKey="id",服务端受控分页,rowSelection={{type:'radio'}} 单选标记仅服务进单据不参与查询 spec D8,行双击 → navigate('/usr/users/'+id) BR12,「作废」列只读 0/1→否/是 BR6,「序号」列按当前页计算 BR1)。
  • 分页:AntD Table 内置 pagination 受控(current/pageSize/total/showSizeChanger/showTotal),pageSize 默认 10、可选 [10,20,50,100](spec D4);切页/改每页条数重取(改每页条数回第 1 页,BR11);越界由后端回退、前端按响应 pageNum 回显(BR15)。
  • 空态 / 错误态:空 records → AntD Empty「暂无匹配的用户」不报错(BR14);非 0 code / 网络异常 → 错误占位 +「点击重试」+ message 文案(spec § 4 错误码表);被动 401 由 request.ts 拦截器统一跳 /login(FE-02 D11,本页不重复处理)。
  • 语义色只用 var(--color-*);工具栏深色底为页面局部装饰,scoped 保留,不新增全局 token、不挪用语义 token(spec § 7 / D10)。

Architecture(架构 / 分层)

遵循 docs/04 § 2.1,落点全在 frontend/**新增/改动文件:

frontend/
├── src/
│   ├── api/types.ts                                      # 【改】新增 UserVO / PageResult<T> / UserListQuery 契约(沿用 FE-01 既有类型,不破坏)
│   ├── api/usrApi.ts                                     # 【改】新增 listUsers(query): Promise<PageResult<UserVO>>(GET /api/usr/users)
│   ├── pages/usr/UserList/index.tsx                      # 【新增】UserListPage 页面容器:组合工具栏/筛选栏/表格+分页;持 useUserList 态;导航跳转
│   ├── pages/usr/UserList/useUserList.ts                 # 【新增】列表查询 hook(list/total/loading/error/query/exporting + 动作)
│   ├── pages/usr/UserList/UserToolbar.tsx                # 【新增】深色工具条:刷新/新增/导出Excel/设置齿轮
│   ├── pages/usr/UserList/UserFilterBar.tsx              # 【新增】筛选栏:范围/查询字段/匹配方式/查询值/更多/搜索/清空
│   ├── pages/usr/UserList/UserTable.tsx                  # 【新增】AntD Table:列定义/序号/作废只读/行双击/单选 rowSelection/受控分页
│   ├── pages/usr/UserList/columns.tsx                    # 【新增】列定义 + 表头文案 + 作废渲染(与 UserVO 字段映射,spec § 6)
│   ├── pages/usr/UserList/constants.ts                   # 【新增】合同级常量:查询字段/匹配方式枚举、默认 query、pageSize 选项、文案
│   ├── pages/usr/UserList/exportUtils.ts                 # 【新增】前端导出工具(CSV+BOM Blob 下载,D-PLAN-1)
│   └── pages/usr/UserList/UserList.module.css            # 【新增】页面 scoped 样式:语义色用 var(--color-*);工具栏深色底局部装饰(D10)
├── src/router/index.tsx                                  # 【改】把 /usr/users 占位 UserListPlaceholder 替换为真实 UserListPage(属 FE 共享骨架,留痕)
└── tests/
    ├── unit/usrApi.userlist.test.ts                      # 【新增】listUsers 透传 query 到 GET /usr/users 并返回 PageResult(沿用 usrApi.test.ts 桩模式)
    ├── unit/useUserList.test.tsx                         # 【新增】hook 取数/翻页/刷新/搜索/清空/空/错误/导出态(renderHook)
    ├── unit/UserFilterBar.test.tsx                       # 【新增】默认值/枚举 options/回车搜索/清空回调(BR2/BR4/BR7/BR10)
    ├── unit/UserTable.test.tsx                           # 【新增】列渲染/序号/作废只读/行双击导航/受控分页/空 Empty(BR1/BR6/BR11/BR12/BR14)
    ├── unit/UserToolbar.test.tsx                         # 【新增】刷新/新增导航/导出中禁用/齿轮占位(BR8/BR9/BR13/D7)
    ├── unit/UserListPage.test.tsx                        # 【新增】页面集成:挂载默认查询/搜索/翻页/错误重试/空态(状态机 + BR3/BR7/BR15)
    └── e2e/userlist.spec.ts                              # 【新增】E2E:进入用户列表→渲染行/空态/搜索/翻页/行双击进单据/错误重试
  • 跨阶段/跨模块:本 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」。
  • 状态管理(docs/04 § 2.2 / spec D6):列表查询态(list/total/loading/error/query/exporting)为页面就近态,用 useUserList hook + useState 本地管理,不进 Redux;登录态复用 FE-01 authSlice(本页不读写)。
  • 请求封装 / 错误处理(docs/04 § 2.3 / § 2.4):取数走 usrApi.listUsersrequest.ts Axios 实例(响应拦截器已拆 Resultcode=0 返回 data,非 0 抛 ApiError,被动 401 统一登出 FE-02 D11);页面/hook 捕获 ApiError 按 code 分流 message 文案 + 错误占位。
  • Design Tokens(docs/04 § 2.1 / spec § 7):筛选栏/表格/分页/空态/错误/成功/警告等语义色只用 var(--color-*);工具栏深色底为页面局部装饰,scoped 在 UserList.module.css,不新增全局 token、不挪用语义 token(spec § 7 / D10,与 FE-02 § 8 D9 一致)。

Tech Stack(技术栈,源自 docs/04 § 零 + FE-01/FE-02 骨架)

  • 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/iconsReloadOutlined / PlusCircleOutlined / FileExcelOutlined / SettingOutlined / SearchOutlined)。
  • 不新增 npm 依赖:导出用零依赖 CSV(UTF-8 BOM + Blob + <a download>),不引入 xlsx/SheetJS(D-PLAN-1)。
  • 测试:单测 Vitest(jsdom)+ @testing-library/react|jest-dom|user-event(沿用 tests/setup.tsrenderShell.tsx);E2E Playwright(沿用 playwright.config.tspage.route**/api/usr/users**)。
  • 命令(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 -- <文件名片段>
  • 提交格式:<type>(<scope>): <subject> REQ-USR-003scope 统一用 usr(业务模块名,CLAUDE.md § Git);subject 业务类(feat/fix/test)带 REQ-USR-003 后缀。每个任务在其 commit 行注明 REQ tag。

合同级常量(跨 task 必须一致)

  • API 路径 / 方法GET /api/usr/usersusrApi 内传 /usr/usersrequest.ts baseURL=/api 已含前缀;query 参数走 axios params)。
  • 查询字段枚举QUERY_FIELD_OPTIONS,对齐 REQ 输入表 1「显示来源」/ docs/05):用户名 / 员工名 / 用户号 / 部门 / 用户类型 / 作废 / 登录日期 / 制单人逐字一致,原样作为 queryField 提交值,前端不映射,匹配语义由后端裁决)。默认 用户名(BR2)。
  • 匹配方式枚举MATCH_TYPE_OPTIONS):包含 / 不包含 / 等于(原样作为 matchType 提交值)。默认 包含(BR2)。
  • 用户范围下拉SCOPE_OPTIONS,占位 demo,spec D2):仅 全部用户 一项;选中「全部用户」时不向后端传任何额外参数(不杜撰 docs/05 未定义的「范围」参数)。
  • 默认查询 DEFAULT_QUERY: UserListQuery{ queryField: '用户名', matchType: '包含', queryValue: '', pageNum: 1, pageSize: 10 }(BR2/BR3,pageSize 默认 10 对齐 docs/05,spec D4)。queryValue 为空字符串时提交时省略或传空(后端按全量处理,BR3)。
  • pageSize 选项PAGE_SIZE_OPTIONS):[10, 20, 50, 100](上限 100 对齐 docs/05 / REQ 边界,spec D4;不采用原型 demo 的 10000)。
  • 错误码常量(对齐 docs/05 § REQ-USR-003):ERR_PAGE_INVALID = 42201(分页参数非法)、ERR_QUERY_INVALID = 40001(查询参数校验失败);网络/超时/5xx 经 request.ts 映射为 NETWORK_ERROR_CODE = -1(复用 FE-01 常量);401request.ts 统一处理(本页不分流)。
  • 表格列定义(中文表头,逐字一致,对齐原型 thead + REQ 输出表 1 + spec § 6),列顺序固定: | 列序 | 表头文案 | 数据字段(UserVO) | 渲染 | |---|---|---|---| | 0 | (单选列,无表头) | —(rowSelection 渲染) | radio 单选标记(spec D8) | | 1 | 序号 | —(前端生成) | (pageNum-1)*pageSize + index + 1(BR1) | | 2 | 用户名 | sUserName | 文本(双击进单据主标识) | | 3 | 员工名 | employeeName(映射自 docs/05 员工名,D-PLAN-2) | 文本,可空 | | 4 | 用户号 | sUserNo | 文本,可空 | | 5 | 部门 | departmentName(映射自 docs/05 部门,D-PLAN-2) | 文本,可空 | | 6 | 用户类型 | sUserType | 文本(已中文) | | 7 | 语言 | sLanguage | 文本 | | 8 | 作废 | iIsVoid | 只读:01(BR6,Tag 或禁用 Checkbox,点击无效) | | 9 | 登录日期 | tLastLoginDate | 日期时间文本,可空 | | 10 | 制单人 | sCreator | 文本 | | 11 | 制单日期 | tCreateDate | 日期时间文本 |
  • 静态文案(逐字一致,复刻原型 / spec): | 用途 | 文案 | |---|---| | 工具栏刷新 | 刷新 | | 工具栏新增 | 新增 | | 工具栏导出 | 导出Excel | | 搜索按钮 | 搜索 | | 清空按钮 | 清空(含原型 前缀,文案以「清空」为可定位文本) | | 空态文案 | 暂无匹配的用户(BR14) | | 错误占位文案 | 加载失败,点击重试(spec § 4) | | 分页统计文案 | 共 {total} 条记录showTotal,total 来自 PageResult.total,BR1/§ 3 success/empty) | | 导出成功提示 | 导出成功message.success,BR9) | | 导出失败提示 | 导出失败message.error,BR9) | | 42201 提示 | 分页参数有误,已重置为第 1 页message.warning,spec § 4) | | 40001 提示 | 查询条件有误,请检查后重试message.error,spec § 4) | | 网络/5xx 提示 | 加载失败,请稍后重试message.error,spec § 4) |
  • 路由 path(导航目标,FE-02 已注册):新增 → /usr/users/new(BR13);行双击修改 → /usr/users/:idnavigate('/usr/users/' + row.id),BR12)。
  • localStorage token 键TOKEN_STORAGE_KEY = 'xly_erp_token'(FE-01 request.ts 已导出,本页不直接读写)。

关键签名(首次出现处给出,跨 task 一致)

  • 类型契约api/types.ts,新增;沿用 FE-01 既有 AuthUser/LoginPayload 等不动):
    • 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 }。 > docs/05 UserVO 以中文键名 员工名/部门 给出(后端关联职员表派生);前端在 api 层做一次别名映射 员工名→employeeName部门→departmentName,组件/列定义统一用 ASCII 键(spec D9 / D-PLAN-2)。映射在 usrApi.listUsers 内对 records 逐项归一。
    • PageResult<T>{ records: T[]; total: number; pageNum: number; pageSize: number }(docs/04 § 1.4 / § 3.2)。
    • UserListQuery{ queryField?: string; matchType?: string; queryValue?: string; pageNum: number; pageSize: number }(提交给 GET /api/usr/users 的 query;queryValue 空时省略,BR3)。
  • API 封装api/usrApi.ts,新增方法):
    • 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<...> 桥接模式)。
  • 页面 hookpages/usr/UserList/useUserList.ts):
    • 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>; }
    • 挂载时以 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)。
    • exportExcel():置 exporting=true → 调 listUsers 拉当前条件命中结果(按 totalpageSize≤100 约束内一次或分批取,D-PLAN-1)→ 生成 CSV Blob 下载 → message.success('导出成功') / 失败 message.error('导出失败')exporting=false(BR9)。
  • 导出工具pages/usr/UserList/exportUtils.ts):
    • buildUserCsv(rows: UserVO[]): string——按列定义顺序与中文表头生成 CSV 文本(含表头行,作废 0/1→否/是,空值→空串)。
    • downloadCsv(filename: string, csv: string): void——前置 UTF-8 BOM()→ new BlobURL.createObjectURL → 触发 <a download>(jsdom 下可桩 URL.createObjectURL/a.click 验证调用)。
  • 组件 propspages/usr/UserList/):
    • UserToolbar{ onRefresh(): void; onAdd(): void; onExport(): void; exporting: boolean; loading?: boolean }):刷新/新增/导出/齿轮;导出中 导出Excelloading 且禁用(BR9);齿轮无动作(spec D7)。
    • UserFilterBar{ query: UserListQuery; onChangeQueryField(v): void; onChangeMatchType(v): void; onChangeQueryValue(v): void; onSearch(): void; onClear(): void }):渲染 SCOPE_OPTIONS/QUERY_FIELD_OPTIONS/MATCH_TYPE_OPTIONS 下拉 + 查询值 InputonPressEnteronSearch,BR7)+ 搜索/清空按钮;更多「▾」占位(spec D3)。
    • 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),空 rowslocale.emptyText 为 AntD Empty「暂无匹配的用户」(BR14)。
    • UserListPage(default export,无 props):useUserList() + useNavigate();装配 UserToolbar/UserFilterBar/UserTableonAddnavigate('/usr/users/new')(BR13);onRowDoubleClicknavigate('/usr/users/'+row.id)(BR12);错误态渲染错误占位 +「点击重试」→ refresh()(spec § 4)。
  • 列定义pages/usr/UserList/columns.tsx):
    • buildUserColumns(opts: { pageNum: number; pageSize: number }): ColumnsType<UserVO>——返回上表 1~11 列(序号列用 render:(_,__,index)=>(pageNum-1)*pageSize+index+1,BR1;作废列 render:(v)=> v===1 ? '是' : '否',只读 BR6)。

测试栈说明

  • jsdom 组件 / hook / api 单测(Vitest + RTL):组件测用 renderShell(已存在,Provider + 真实 store + MemoryRouter + AntD App/ConfigProvider);导航断言用 LocationProbe(复用 HomePage.test.tsxuseLocation 探针模式)+ 路由哨兵(/usr/users/new/usr/users/:id 哨兵元素)。useUserListrenderHook,对 usrApi.listUsersvi.mock('../../src/api/usrApi') 桩(沿用 usrApi.test.tsvi.mock 模式)。listUsers api 测桩底层 request 实例(同 usrApi.test.ts)。
  • Playwright E2Epage.route**/api/usr/users**(先返回非空 records 验渲染/翻页/行双击;再用单独用例桩空 records 验空态、桩 5xx 验错误重试);先经登录桩落地外壳再进 /usr/users(沿用 shell.spec.tsstubBackend/login 模式,可抽到本 spec 内)。不依赖真实后端起服。
  • 可测性:优先语义查询(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)。

任务列表(每个 task = red → green → 子会话验证 → commit)

硬护栏:以下每个 impl_file / test_file 均以 frontend/ 开头;无任何 backend/ / sql/ / scripts/ 落点。 提交 scope 统一 usr;REQ tag 统一 REQ-USR-003

T1 — 类型契约 + listUsers API 封装(GET /api/usr/users,中文键归一 D-PLAN-2)(jsdom api 测)

  • 测试先行类型:jsdom 组件测试(api 单测)
  • 1. 写失败测试frontend/tests/unit/usrApi.userlist.test.ts(沿用 usrApi.test.tsvi.mock('../../src/api/request') 桩底层实例):
    • ::listUsers gets /usr/users with query params——调 listUsers({ queryField:'用户名', matchType:'包含', queryValue:'李', pageNum:2, pageSize:20 }),断言 request.get'/usr/users' + { params: { ...含传入字段 } } 被调用。
    • ::listUsers omits empty queryValue——queryValue:'' 时 params 不含 queryValue(或传空,按实现一处定,与 BR3 一致;测试断言后端不收到非空 queryValue)。
    • ::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)。
  • 2. 实现最小代码frontend/src/api/types.ts(新增 UserVO/PageResult<T>/UserListQuery,签名见关键签名)+ frontend/src/api/usrApi.ts(新增 listUsersrequest.get('/usr/users',{params}) + records 中文键归一)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- usrApi(含 FE-01 usrApi 既有用例不回归)
  • 4. commitfeat(usr): 用户列表查询 API 与类型契约 listUsers REQ-USR-003

T2 — 页面常量 + 导出工具(枚举/默认/pageSize/文案 + CSV 下载,D-PLAN-1)(jsdom 单测)

  • 测试先行类型:jsdom 组件测试(纯逻辑断言)
  • 1. 写失败测试frontend/tests/unit/UserToolbar.test.tsx 暂不依赖此,单独建 frontend/tests/unit/exportUtils.test.ts
    • ::buildUserCsv has header row and maps 作废 0/1——传 2 行(iIsVoid:01),断言 CSV 首行含中文表头(用户名/员工名/作废 等逐字),数据行作废列分别为 /,空值字段为空串。
    • ::downloadCsv triggers blob download with UTF-8 BOM——桩 URL.createObjectURL(vi.fn)与 HTMLAnchorElement.prototype.click,调 downloadCsv('users.csv', 'x'),断言 createObjectURL 收到的 Blob 内容以  开头且 click 被调一次。
    • (常量断言并入此文件或 useUserList.test.tsx,TDD 期定一处):DEFAULT_QUERY.queryField==='用户名'matchType==='包含'pageSize===10PAGE_SIZE_OPTIONS 末项为 100QUERY_FIELD_OPTIONS 含 8 项首项 用户名MATCH_TYPE_OPTIONS['包含','不包含','等于']
  • 2. 实现最小代码frontend/src/pages/usr/UserList/constants.ts(枚举/默认/pageSize/错误码/文案常量,见合同级常量)+ frontend/src/pages/usr/UserList/exportUtils.tsbuildUserCsv/downloadCsv,签名见关键签名)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- exportUtils
  • 4. commitfeat(usr): 用户列表页面常量与前端 CSV 导出工具 REQ-USR-003

T3 — useUserList 列表查询 hook(状态机:initialLoading/loading/success/empty/error/exporting)(jsdom hook 测)

  • 测试先行类型:jsdom 组件测试(renderHookvi.mock('../../src/api/usrApi')
  • 1. 写失败测试frontend/tests/unit/useUserList.test.tsx
    • ::mounts with default query and loads first page (initialLoading→success)——listUsers resolve 非空 records,断言挂载即以 DEFAULT_QUERY 调用 listUsersloading 由 true→false、list/total/query.pageNum 同步(BR2/§ 3 initialLoading/success)。
    • ::empty records sets empty state without error——resolve records:[],total:0,断言 list 为空、error===null、无 message.error(BR14/§ 3 empty)。
    • ::search resets to page 1 and refetches with current filters——改 queryField/queryValuesearch(),断言以 pageNum:1 + 当前条件再次调用(BR7)。
    • ::refresh keeps current query and page——先翻到第 2 页,refresh() 断言以当前 pageNum(2)+ 当前条件调用,不回第 1 页、不重置条件(BR8)。
    • ::clear resets to DEFAULT_QUERY then refetches——改条件后 clear(),断言 queryDEFAULT_QUERY(用户名/包含/空/page1)并以之取数(BR10)。
    • ::changePage refetch; changing pageSize resets to page 1——changePage(3, 10) 以 pageNum=3 取;changePage(1, 50)(改 pageSize)回第 1 页取(BR11)。
    • ::ApiError 40001 keeps filters and shows error, sets error state——listUsers reject new ApiError(40001,...),断言 error 置位、query 未变、不自动重查(spec § 4 / 40001)。
    • ::ApiError 42201 warns and refetches at page 1——reject ApiError(42201),断言重置 pageNum=1(与 pageSize 收敛)后重查(spec § 4 / 42201 兜底)。
    • ::network error (code -1) sets error state——reject ApiError(-1),断言 error 置位(错误占位由页面消费,spec § 4)。
    • ::exportExcel toggles exporting and downloads——桩 listUsers 返回结果集 + 桩 downloadCsv,调 exportExcel(),断言 exporting true→false、downloadCsv 被调(BR9/§ 3 exporting);reject 时 message.error('导出失败')。 > 取数响应回显(BR15):success 用例额外断言 query.pageNum/pageSize 跟随响应 PageResult 同步(即使请求 pageNum 越界,前端信任响应回显)。message 断言可桩 antd App.useApp().messagevi.mock('antd', ...) 暴露 message,TDD 期定一处。
  • 2. 实现最小代码frontend/src/pages/usr/UserList/useUserList.ts(签名见关键签名;内部 useStatequery/list/total/loading/error/exportinguseEffect 挂载取数;错误码分流按合同级常量;导出复用 T2 exportUtils)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- useUserList
  • 4. commitfeat(usr): 用户列表查询 hook useUserList 状态机 REQ-USR-003

T4 — UserFilterBar 筛选栏(默认值/枚举 options/回车搜索/清空,BR2/BR3/BR4/BR7/BR10/D2/D3)(jsdom 组件测)

  • 测试先行类型:jsdom 组件测试
  • 1. 写失败测试frontend/tests/unit/UserFilterBar.test.tsxrenderShell 提供 AntD App 上下文):
    • ::renders defaults 用户名 / 包含 and empty value——传 query=DEFAULT_QUERY,断言查询字段下拉显「用户名」、匹配方式显「包含」、查询值框为空(BR2)。
    • ::query field options match enum——展开查询字段下拉,断言 8 个选项逐字一致(用户名…制单人,BR4);展开匹配方式断言 包含/不包含/等于(BR4)。
    • ::scope select shows 全部用户 only——范围下拉仅「全部用户」(占位 demo,D2)。
    • ::Enter in value triggers onSearch——查询值框聚焦回车 → onSearch 被调一次(BR7)。
    • ::click 搜索 calls onSearch / click 清空 calls onClear——点「搜索」→ onSearch;点「清空」→ onClear(BR7/BR10)。
    • ::changing selects/value calls respective onChange——改下拉/输入触发 onChangeQueryField/onChangeMatchType/onChangeQueryValue
    • ::more toggle ▾ is placeholder (no extra callback)——「▾」点击不触发查询回调(占位,D3)。
  • 2. 实现最小代码frontend/src/pages/usr/UserList/UserFilterBar.tsx(签名见关键签名;options 来自 T2 常量;Input onPressEnteronSearch)+ UserList.module.css 筛选栏样式(语义色用 var(--color-*):白底 --color-form-bg-edit、下边线 --color-border、搜索按钮主色 --color-primary)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- UserFilterBar
  • 4. commitfeat(usr): 用户列表筛选栏 UserFilterBar REQ-USR-003

T5 — UserTable 表格(列/序号/作废只读/行双击/单选/受控分页/空态,BR1/BR6/BR11/BR12/BR14/D8)(jsdom 组件测)

  • 测试先行类型:jsdom 组件测试
  • 1. 写失败测试frontend/tests/unit/UserTable.test.tsxrenderShell):
    • ::renders 11 column headers in order——断言列头文案逐字、顺序与合同级常量列表一致(序号…制单日期)。
    • ::serial number is page-aware——传 pageNum=2, pageSize=10 + 2 行,断言首行序号为 11(2-1)*10+0+1,BR1)。
    • ::作废 column renders 否/是 read-only——iIsVoid:0→「否」、1→「是」;点击作废单元不触发任何 onChange/写动作(BR6)。
    • ::double click row navigates via onRowDoubleClick——双击某数据行 → onRowDoubleClick 收到该行(含 id,BR12)。
    • ::controlled pagination reflects current/pageSize/total + showTotal——传 total=37,pageNum=1,pageSize=10,断言分页显示「共 37 条记录」、当前页 1;点下一页/改每页条数 → onChangePage 收到新 (pageNum,pageSize)(改 pageSize 期望回第 1 页由页面/hook 处理,本组件原样上报,BR11)。
    • ::empty rows shows Empty 暂无匹配的用户——rows=[] → 渲染「暂无匹配的用户」(BR14)。
    • ::radio rowSelection single-select does not affect query——选中某行 → onSelectRow 收到该行 key,不触发取数/查询(spec D8;本组件不持查询态,断言仅回调)。
  • 2. 实现最小代码frontend/src/pages/usr/UserList/columns.tsxbuildUserColumns,序号/作废 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)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- UserTable
  • 4. commitfeat(usr): 用户列表表格 UserTable 与列定义 REQ-USR-003

T6 — UserToolbar 工具栏(刷新/新增导航/导出中禁用/齿轮占位,BR8/BR9/BR13/D7/D10)(jsdom 组件测)

  • 测试先行类型:jsdom 组件测试
  • 1. 写失败测试frontend/tests/unit/UserToolbar.test.tsxrenderShell):
    • ::renders 刷新/新增/导出Excel/设置 buttons——可见四个工具项(文案逐字 + 对应图标)。
    • ::click 刷新 calls onRefresh / click 新增 calls onAdd——点刷新→onRefresh;点新增→onAdd(BR8/BR13)。
    • ::click 导出Excel calls onExport——点导出→onExport(BR9)。
    • ::exporting disables 导出Excel and shows loading——exporting=true 时「导出Excel」禁用且 loading,再点不再触发 onExport(BR9)。
    • ::gear setting is placeholder (no callback)——点齿轮无任何业务回调(占位,D7)。
  • 2. 实现最小代码frontend/src/pages/usr/UserList/UserToolbar.tsx(签名见关键签名;图标 ReloadOutlined/PlusCircleOutlined/FileExcelOutlined/SettingOutlined)+ UserList.module.css 工具栏深色底(scoped 局部装饰,非语义 token,D10)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- UserToolbar
  • 4. commitfeat(usr): 用户列表工具栏 UserToolbar REQ-USR-003

T7 — UserListPage 页面集成 + 路由接线(状态机贯通 + 导航 + 错误重试,BR3/BR7/BR8/BR9/BR12/BR13/BR15)(jsdom 组件测)

  • 测试先行类型:jsdom 组件测试
  • 1. 写失败测试frontend/tests/unit/UserListPage.test.tsxrenderShellvi.mock('../../src/api/usrApi')listUsers;用 LocationProbe + /usr/users/new/usr/users/:id 哨兵路由验导航):
    • ::initial load renders rows from listUsers (default query)——挂载即调 listUsers(DEFAULT_QUERY)、渲染返回行(BR2/initialLoading→success)。
    • ::search with value submits queryValue and shows results——填查询值「李」点搜索 → listUsersqueryValue:'李', pageNum:1 调用、渲染结果(BR7/BR3)。
    • ::empty response shows 暂无匹配的用户——桩空 records → 空态(BR14)。
    • ::error response shows error placeholder with 点击重试; retry calls refresh——桩 reject ApiError(-1) → 可见「加载失败,点击重试」;点重试再次取数(spec § 4)。
    • ::新增 navigates to /usr/users/new——点工具栏「新增」→ URL/哨兵到 /usr/users/new(BR13)。
    • ::double click row navigates to /usr/users/:id——桩返回含 id 的行,双击 → URL/哨兵到 /usr/users/{id}(BR12)。
    • ::refresh keeps current page——翻到第 2 页后点刷新 → listUserspageNum:2 调用(BR8)。
    • ::response pageNum echo syncs pagination——请求越界 pageNum、桩响应回 pageNum=最后一页,断言分页当前页跟随响应(BR15)。
  • 2. 实现最小代码frontend/src/pages/usr/UserList/index.tsxUserListPageuseUserList + useNavigate,装配 UserToolbar/UserFilterBar/UserTable,错误态渲染「加载失败,点击重试」→refresh,新增/行双击导航);改 frontend/src/router/index.tsx——把 /usr/usersUserListPlaceholder 替换为 UserListPage(移除占位组件,import 真实页;/usr/users/new/usr/users/:id 仍为 FE-04 占位,不动)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- UserListPage router(确认 FE-02 router.test.tsx 不回归——若其断言 /usr/users 渲染占位 data-testid,需同步更新该断言为真实页可定位元素,并在 commit 说明)
  • 4. commitfeat(usr): 用户列表页面集成与路由接线 UserListPage REQ-USR-003

T8 — E2E 用户列表关键旅程(Playwright)

  • 测试先行类型:Playwright E2E
  • 1. 写失败测试frontend/tests/e2e/userlist.spec.ts(沿用 shell.spec.ts 的登录桩;page.route('**/api/usr/users**') 按用例返回不同体):
    • ::enter user list renders rows——桩非空 records,从主页「常用操作 > 用户列表」进入 /usr/users,断言表格出现某用户名行 + 分页「共 N 条记录」。
    • ::empty result shows 暂无匹配的用户——桩空 records,断言空态文案。
    • ::search by value triggers query——填查询值点「搜索」,断言请求携带 queryValue(用 page.waitForRequest('**/api/usr/users**') 校验 URL 含 queryValue)并渲染结果。
    • ::pagination next page refetches——桩 total>pageSize,点下一页,断言请求 pageNum=2
    • ::double click row navigates to user detail——双击行 → URL 到 /usr/users/{id}(FE-04 占位即可,本 FE 不验单据内容)。
    • ::error response shows retry——桩 5xx,断言「加载失败,点击重试」可见;点重试(改桩为成功)后渲染行。
  • 2. 实现最小代码:补任何为可测性需要的最小 data-testid(仅 Playwright 无法稳定定位时,如 user-table/btn-search/btn-refresh/userlist-error)。沿用 playwright.config.ts
  • 3. 子会话验证 PASScd frontend && npm run test:e2e -- userlist
  • 4. committest(usr): 用户列表 E2E 关键旅程 REQ-USR-003

T9 — 全量门禁回归 + 收尾(chore)

  • 测试先行类型:无新增测试(全量验证)
  • 1. 写失败测试:无。
  • 2. 实现最小代码:修 lint / build(tsc --noEmit)/ 类型问题;确认语义色全部 var(--color-*)、无硬编码 hex/rgba(工具栏深色装饰 scoped 例外,D10);确认无 TBD/TODO/【人工填写】;确认 FE-01/FE-02 既有单测/E2E 不回归(尤其 router.test.tsxshell.spec.ts)。
  • 3. 子会话验证 PASScd frontend && npm run lint && npm run build && npm run test:unit && npm run test:e2e 全绿。
  • 4. commitchore(usr): FE-03 门禁回归通过 REQ-USR-003

完成判据(Definition of Done)

  1. /usr/users 渲染真实 UserListPage(复刻原型 #screen-userlist 的工具栏/筛选栏/用户表格/分页),数据/筛选/分页真实对接 GET /api/usr/users(spec § 1/§ 2 / D1)。
  2. 状态机覆盖并测试固化:initialLoading(T3/T7)、loading(T3)、success(T3/T5/T7)、empty(T3/T5/T7)、error(T3/T7)、exporting(T3/T6)(spec § 3)。
  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)。
  4. 列定义/字段映射对齐 REQ 输出表 1 + 原型 thead + UserVO(11 列顺序/表头/作废只读,spec § 6 / D9);员工名/部门 中文键在 api 层归一为 employeeName/departmentName(D-PLAN-2)。
  5. API 经 usrApi.listUsersrequest.ts(拆 ResultApiError 分流、被动 401 统一登出复用 FE-02),页面不散用 axios(docs/04 § 2.3);列表态在页面 hook 不进 Redux(docs/04 § 2.2 / spec D6)。
  6. 错误码分流文案对齐 spec § 4(42201 warning+重置重查、40001 error 保留条件、网络兜底 error、空态不报错)。
  7. 导出为前端零依赖 CSV(UTF-8 BOM Blob 下载),不杜撰后端导出端点、不新增 npm 依赖(spec D5 / D-PLAN-1)。
  8. 语义色只用 var(--color-*),AntD colorPrimary 沿用 FE-01 ConfigProvider;工具栏深色底 scoped 装饰不新增全局 token、不挪用语义 token(spec § 7 / D10)。
  9. 全部落点在 frontend/**,无 backend/ / sql/ / scripts/ 改动;改 router/index.tsx(占位换真实页)、usrApi.ts/types.ts(增列表契约)属共享骨架,已在《模块完成报告》留痕。
  10. 门禁全绿:npm run lint / npm run build / npm run test:unit / npm run test:e2e(docs/04 § 零)。

自审记录

  • 占位符扫描:本计划无 【人工填写:】 / TBD / TODO 真实占位(正文 TBD/TODO 仅作为「禁止出现的字样」被引用)。
  • 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 见下。
  • 本计划细化决策
    • 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。
    • D-PLAN-2(中文键归一具体化 spec D9):spec D9 允许「在 api 层做一次到 employeeName/departmentName 的别名映射后供组件用」。本计划锁定该路径:usrApi.listUsers 内对 PageResult.records 逐项把 员工名→employeeName部门→departmentName 归一,组件/列定义统一用 ASCII 键(避免 TS 中文键访问与 lint 摩擦);列渲染语义与 docs/05 契约不变。置信度 high。
  • 类型一致性UserVO(ASCII 键,跨 T1/T3/T5/T7 一致)、PageResult<T>UserListQueryDEFAULT_QUERYQUERY_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
  • 作用域自审:所有 impl_file / test_file 均以 frontend/ 开头;无 backend/ / sql/ / scripts/ 落点。改 src/router/index.tsxsrc/api/usrApi.tssrc/api/types.ts 属 FE 共享骨架扩展(非新阶段越界),已在架构段与 DoD 第 9 条登记留痕要求。