FE-03 用户列表与查询 — 任务级 TDD 计划(前端)
阶段:前端(frontend)。作用域:
frontend/**(页面 / api / 类型 / 样式 / 测试)。禁止写backend/**/sql/**/scripts/**。 上游 SSoT:specdocs/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。 复用资产: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)。 本计划告诉 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-02AppLayout的<Outlet/>内):纵向组合UserToolbar+UserFilterBar+UserTable+ 内置分页,状态由页面本地 hookuseUserList持有(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
Table(rowKey="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→ AntDEmpty「暂无匹配的用户」不报错(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)为页面就近态,用
useUserListhook +useState本地管理,不进 Redux;登录态复用 FE-01authSlice(本页不读写)。 -
请求封装 / 错误处理(docs/04 § 2.3 / § 2.4):取数走
usrApi.listUsers→request.tsAxios 实例(响应拦截器已拆Result:code=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/icons(ReloadOutlined/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.ts、renderShell.tsx);E2E Playwright(沿用playwright.config.ts,page.route桩**/api/usr/users**)。 - 命令(docs/04 § 零):build
npm run build;lintnpm run lint;unitnpm run test:unit;e2enpm run test:e2e。子会话验证用cd frontend && npm run test:unit -- <文件名片段>。 - 提交格式:
<type>(<scope>): <subject> REQ-USR-003。scope 统一用usr(业务模块名,CLAUDE.md § Git);subject 业务类(feat/fix/test)带REQ-USR-003后缀。每个任务在其 commit 行注明 REQ tag。
合同级常量(跨 task 必须一致)
-
API 路径 / 方法:
GET /api/usr/users(usrApi内传/usr/users,request.tsbaseURL=/api已含前缀;query 参数走 axiosparams)。 -
查询字段枚举(
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 常量);401由request.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| 只读:0→否、1→是(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/:id(navigate('/usr/users/' + row.id),BR12)。 -
localStorage token 键:
TOKEN_STORAGE_KEY = 'xly_erp_token'(FE-01request.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/05UserVO以中文键名员工名/部门给出(后端关联职员表派生);前端在 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-01as unknown as Promise<...>桥接模式)。
-
-
页面 hook(
pages/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拉当前条件命中结果(按total在pageSize≤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 Blob→URL.createObjectURL→ 触发<a download>(jsdom 下可桩URL.createObjectURL/a.click验证调用)。
-
-
组件 props(
pages/usr/UserList/):-
UserToolbar({ onRefresh(): void; onAdd(): void; onExport(): void; exporting: boolean; loading?: boolean }):刷新/新增/导出/齿轮;导出中导出Excel置loading且禁用(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下拉 + 查询值Input(onPressEnter→onSearch,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 }):AntDTablerowKey="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为 AntDEmpty「暂无匹配的用户」(BR14)。 -
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)。
-
-
列定义(
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+ AntDApp/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模式)。listUsersapi 测桩底层request实例(同usrApi.test.ts)。 -
Playwright E2E:
page.route桩**/api/usr/users**(先返回非空records验渲染/翻页/行双击;再用单独用例桩空records验空态、桩 5xx 验错误重试);先经登录桩落地外壳再进/usr/users(沿用shell.spec.ts的stubBackend/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.ts的vi.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.getresolve{ 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(新增listUsers,request.get('/usr/users',{params})+ records 中文键归一)。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- usrApi(含 FE-01usrApi既有用例不回归) - 4. commit:
feat(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:0与1),断言 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===10、PAGE_SIZE_OPTIONS末项为100、QUERY_FIELD_OPTIONS含 8 项首项用户名、MATCH_TYPE_OPTIONS为['包含','不包含','等于']。
-
- 2. 实现最小代码:
frontend/src/pages/usr/UserList/constants.ts(枚举/默认/pageSize/错误码/文案常量,见合同级常量)+frontend/src/pages/usr/UserList/exportUtils.ts(buildUserCsv/downloadCsv,签名见关键签名)。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- exportUtils - 4. commit:
feat(usr): 用户列表页面常量与前端 CSV 导出工具 REQ-USR-003
T3 — useUserList 列表查询 hook(状态机:initialLoading/loading/success/empty/error/exporting)(jsdom hook 测)
-
测试先行类型:jsdom 组件测试(
renderHook,vi.mock('../../src/api/usrApi')) - 1. 写失败测试:
frontend/tests/unit/useUserList.test.tsx:-
::mounts with default query and loads first page (initialLoading→success)——listUsersresolve 非空records,断言挂载即以DEFAULT_QUERY调用listUsers、loading由 true→false、list/total/query.pageNum同步(BR2/§ 3 initialLoading/success)。 -
::empty records sets empty state without error——resolverecords:[],total:0,断言list为空、error===null、无message.error(BR14/§ 3 empty)。 -
::search resets to page 1 and refetches with current filters——改queryField/queryValue后search(),断言以pageNum:1+ 当前条件再次调用(BR7)。 -
::refresh keeps current query and page——先翻到第 2 页,refresh()断言以当前pageNum(2)+ 当前条件调用,不回第 1 页、不重置条件(BR8)。 -
::clear resets to DEFAULT_QUERY then refetches——改条件后clear(),断言query回DEFAULT_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——listUsersrejectnew ApiError(40001,...),断言error置位、query未变、不自动重查(spec § 4 / 40001)。 -
::ApiError 42201 warns and refetches at page 1——rejectApiError(42201),断言重置pageNum=1(与 pageSize 收敛)后重查(spec § 4 / 42201 兜底)。 -
::network error (code -1) sets error state——rejectApiError(-1),断言error置位(错误占位由页面消费,spec § 4)。 -
::exportExcel toggles exporting and downloads——桩listUsers返回结果集 + 桩downloadCsv,调exportExcel(),断言exportingtrue→false、downloadCsv被调(BR9/§ 3 exporting);reject 时message.error('导出失败')。 > 取数响应回显(BR15):success 用例额外断言query.pageNum/pageSize跟随响应PageResult同步(即使请求 pageNum 越界,前端信任响应回显)。message断言可桩antdApp.useApp().message或vi.mock('antd', ...)暴露message,TDD 期定一处。
-
- 2. 实现最小代码:
frontend/src/pages/usr/UserList/useUserList.ts(签名见关键签名;内部useState持query/list/total/loading/error/exporting,useEffect挂载取数;错误码分流按合同级常量;导出复用 T2exportUtils)。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- useUserList - 4. commit:
feat(usr): 用户列表查询 hook useUserList 状态机 REQ-USR-003
T4 — UserFilterBar 筛选栏(默认值/枚举 options/回车搜索/清空,BR2/BR3/BR4/BR7/BR10/D2/D3)(jsdom 组件测)
- 测试先行类型:jsdom 组件测试
- 1. 写失败测试:
frontend/tests/unit/UserFilterBar.test.tsx(renderShell提供 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 常量;InputonPressEnter→onSearch)+UserList.module.css筛选栏样式(语义色用var(--color-*):白底--color-form-bg-edit、下边线--color-border、搜索按钮主色--color-primary)。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- UserFilterBar - 4. commit:
feat(usr): 用户列表筛选栏 UserFilterBar REQ-USR-003
T5 — UserTable 表格(列/序号/作废只读/行双击/单选/受控分页/空态,BR1/BR6/BR11/BR12/BR14/D8)(jsdom 组件测)
- 测试先行类型:jsdom 组件测试
- 1. 写失败测试:
frontend/tests/unit/UserTable.test.tsx(renderShell):-
::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.tsx(buildUserColumns,序号/作废 render)+frontend/src/pages/usr/UserList/UserTable.tsx(AntDTable受控分页/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. 子会话验证 PASS:
cd frontend && npm run test:unit -- UserTable - 4. commit:
feat(usr): 用户列表表格 UserTable 与列定义 REQ-USR-003
T6 — UserToolbar 工具栏(刷新/新增导航/导出中禁用/齿轮占位,BR8/BR9/BR13/D7/D10)(jsdom 组件测)
- 测试先行类型:jsdom 组件测试
- 1. 写失败测试:
frontend/tests/unit/UserToolbar.test.tsx(renderShell):-
::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. 子会话验证 PASS:
cd frontend && npm run test:unit -- UserToolbar - 4. commit:
feat(usr): 用户列表工具栏 UserToolbar REQ-USR-003
T7 — UserListPage 页面集成 + 路由接线(状态机贯通 + 导航 + 错误重试,BR3/BR7/BR8/BR9/BR12/BR13/BR15)(jsdom 组件测)
- 测试先行类型:jsdom 组件测试
- 1. 写失败测试:
frontend/tests/unit/UserListPage.test.tsx(renderShell,vi.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——填查询值「李」点搜索 →listUsers以queryValue:'李', pageNum:1调用、渲染结果(BR7/BR3)。 -
::empty response shows 暂无匹配的用户——桩空records→ 空态(BR14)。 -
::error response shows error placeholder with 点击重试; retry calls refresh——桩 rejectApiError(-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 页后点刷新 →listUsers以pageNum:2调用(BR8)。 -
::response pageNum echo syncs pagination——请求越界pageNum、桩响应回pageNum=最后一页,断言分页当前页跟随响应(BR15)。
-
- 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 占位,不动)。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- UserListPage router(确认 FE-02router.test.tsx不回归——若其断言/usr/users渲染占位data-testid,需同步更新该断言为真实页可定位元素,并在 commit 说明) - 4. commit:
feat(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. 子会话验证 PASS:
cd frontend && npm run test:e2e -- userlist - 4. commit:
test(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.tsx、shell.spec.ts)。 - 3. 子会话验证 PASS:
cd frontend && npm run lint && npm run build && npm run test:unit && npm run test:e2e全绿。 - 4. commit:
chore(usr): FE-03 门禁回归通过 REQ-USR-003
完成判据(Definition of Done)
-
/usr/users渲染真实UserListPage(复刻原型#screen-userlist的工具栏/筛选栏/用户表格/分页),数据/筛选/分页真实对接GET /api/usr/users(spec § 1/§ 2 / D1)。 - 状态机覆盖并测试固化:
initialLoading(T3/T7)、loading(T3)、success(T3/T5/T7)、empty(T3/T5/T7)、error(T3/T7)、exporting(T3/T6)(spec § 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)。
- 列定义/字段映射对齐 REQ 输出表 1 + 原型 thead +
UserVO(11 列顺序/表头/作废只读,spec § 6 / D9);员工名/部门中文键在 api 层归一为employeeName/departmentName(D-PLAN-2)。 - API 经
usrApi.listUsers→request.ts(拆Result、ApiError分流、被动 401 统一登出复用 FE-02),页面不散用 axios(docs/04 § 2.3);列表态在页面 hook 不进 Redux(docs/04 § 2.2 / spec D6)。 - 错误码分流文案对齐 spec § 4(42201 warning+重置重查、40001 error 保留条件、网络兜底 error、空态不报错)。
- 导出为前端零依赖 CSV(UTF-8 BOM Blob 下载),不杜撰后端导出端点、不新增 npm 依赖(spec D5 / D-PLAN-1)。
- 语义色只用
var(--color-*),AntDcolorPrimary沿用 FE-01ConfigProvider;工具栏深色底 scoped 装饰不新增全局 token、不挪用语义 token(spec § 7 / D10)。 - 全部落点在
frontend/**,无backend//sql//scripts/改动;改router/index.tsx(占位换真实页)、usrApi.ts/types.ts(增列表契约)属共享骨架,已在《模块完成报告》留痕。 - 门禁全绿:
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。
-
D-PLAN-1(导出实现具体化 spec D5):spec D5 允许「用前端库(如 xlsx/SheetJS)」且为 medium 置信度并写明「实现期如后端补导出端点可切换」。经核
-
类型一致性:
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-01request.ts。 -
作用域自审:所有
impl_file/test_file均以frontend/开头;无backend//sql//scripts/落点。改src/router/index.tsx、src/api/usrApi.ts、src/api/types.ts属 FE 共享骨架扩展(非新阶段越界),已在架构段与 DoD 第 9 条登记留痕要求。