项目2 · React 数据管理工作台

从一张静态表格出发,补齐真实管理台需要的请求状态、查询、分页、编辑和批量操作。

约 10 小时
学习重点
  • 区分远程数据、查询条件和临时选择三类状态,安排各自的生命周期与归属
  • 用可注入延迟和故障的 mock API 练习加载、失败、重试与空数据分支
  • 把排序、分页、筛选和搜索组合成可推导的查询,并在条件变化时重置页码
  • 完成详情抽屉、表单校验和批量删除确认,理解异步提交与界面反馈的配合
页面 / 接口清单
  • 01数据列表页:员工记录表格、排序表头、结果统计和请求状态
  • 02查询栏:关键词搜索、状态筛选、部门筛选、排序字段/方向与每页条数
  • 03分页器:上一页、下一页、当前页/总页数和边界禁用
  • 04详情抽屉:查看完整记录、进入编辑、保存后同步列表
  • 05编辑表单:姓名、邮箱、薪资、入职日期等字段校验与异步保存
  • 06批量操作:当前页多选、批量删除确认、取消和删除结果提示
  • 07mock 接口:延迟、随机失败、列表查询、更新与批量删除
验收清单
0 / 8(0%)

页面启动调用 mockApi.list,成功渲染 20 条样例数据;表格列、状态文案和结果总数正确。

加载时显示明确状态;failRate=1 时出现失败提示和重试按钮,调回 0 后重试可恢复;无结果时显示空状态。

关键词、状态、部门可同时生效;排序字段和方向可切换;任一条件变化页码重置为 1,结果计数准确。

上一页/下一页在首尾禁用,页码和总页数随筛选结果变化;修改每页条数后回到第 1 页。

点击「查看」打开抽屉并展示完整字段;支持关闭、Esc 关闭和焦点落在可操作元素上,不阻塞表格浏览。

姓名必填、邮箱格式、薪资非负、入职日期必填;校验失败不提交,成功后抽屉和表格同步更新。

当前页可多选,批量操作条显示数量;点击删除先弹确认框,取消不改变数据,确认后调用接口并刷新列表。

solution 下 npm run test 全绿;mock 契约、数据模型、组件职责和 README 启动步骤与实现一致。

README(项目目录)
# React 数据管理工作台(p2-data-workbench)

一个带真实管理台骨架的数据列表:mock API 提供延迟与故障注入,页面补齐加载、失败、重试、空数据、排序、分页、条件筛选、详情抽屉、编辑校验和批量删除确认。

重点练习「远程数据、查询条件、临时选择」三类状态各归其位,而不是把所有东西塞进一个 `useState`。

## 学习目标

- 区分远程数据、查询条件和当前选择,明确各自的更新时机。
- 用可注入 `latency` / `failRate` 的 mock API 稳定复现请求状态。
- 把排序、分页、筛选、搜索收进一个查询 reducer,条件变化统一回到第 1 页。
- 完成详情抽屉、表单校验、异步保存和批量删除确认。
- 用行为测试覆盖成功、失败、重试、组合查询、编辑和批量操作。

## 环境要求

- Node.js 22.22.2+(推荐 24 LTS)
- npm 10+

## 快速开始

### 运行起点

```bash
cd starter
npm install --no-audit --no-fund
npm run dev
```

起点是一张静态表格,`src/App.jsx` 顶部用 `TODO m1`–`TODO m8` 标出了缺口。

### 运行参考实现

```bash
cd solution
npm install --no-audit --no-fund
npm run dev
```

### 运行行为测试

```bash
cd solution
npm install --no-audit --no-fund
npm run test
```

测试文件在项目根目录 `tests/`,由 `solution/vitest.config.js` 以项目根为测试根加载。测试会自己创建隔离的 mock API 实例,不会污染默认内存数据。

### 构建检查

```bash
cd starter
npm install --no-audit --no-fund
npm run build
```

`solution` 同样可执行 `npm run build`。

## 目录结构

```text
p2-data-workbench/
├── README.md
├── starter/                      # 可运行起点:静态表格 + TODO
│   ├── index.html
│   ├── package.json
│   ├── vite.config.js
│   └── src/
│       ├── App.jsx
│       ├── main.jsx
│       └── styles.css
├── solution/                     # 完整参考实现
│   ├── index.html
│   ├── package.json
│   ├── vite.config.js
│   ├── vitest.config.js
│   └── src/
│       ├── App.jsx
│       ├── format.js
│       ├── main.jsx
│       ├── styles.css
│       ├── api/
│       │   ├── mockApi.js
│       │   └── seedRecords.js
│       ├── state/
│       │   └── queryReducer.js
│       └── components/
│           ├── ConfirmDialog.jsx
│           ├── DataTable.jsx
│           ├── DetailDrawer.jsx
│           ├── Pagination.jsx
│           ├── RecordForm.jsx
│           ├── RequestState.jsx
│           └── Toolbar.jsx
├── tests/
│   └── app.test.jsx              # Vitest + Testing Library 行为测试
├── docs/
│   ├── api.md                    # mockApi 契约与故障注入
│   └── data.md                   # 记录、查询与请求状态模型
└── fixtures/
    └── records.json              # 20 条示例员工记录
```

## 组件职责

| 组件 | 职责 | 为什么拆出来 |
| --- | --- | --- |
| `App` | 持有查询、请求、选择和确认状态,触发 list / update / delete | 唯一知道远程数据与查询条件的位置,避免各组件各自请求 |
| `Toolbar` | 搜索、状态/部门筛选、排序、每页条数 | 只抛查询变化,不缓存结果,也不负责重置页码 |
| `DataTable` | 当前页表格、全选/单选、查看入口 | 只渲染传入的当前页,选择状态由 Set 派生 |
| `Pagination` | 页码信息与上一页/下一页 | 只处理翻页意图,边界由按钮 disabled 和接口页码双重兜底 |
| `DetailDrawer` | 查看详情与切换到编辑 | 局部流程留在抽屉,任务数据仍以 App 的 `selectedRecord` 为准 |
| `RecordForm` | 编辑草稿与字段校验 | 校验和请求解耦,保存失败时保留用户输入 |
| `ConfirmDialog` | 批量删除确认、Esc 与焦点 | 不可逆动作集中处理,避免散落在 App JSX |
| `RequestState` | loading / error / empty 三种请求分支 | 避免 App 堆多层三元表达式,状态文案集中维护 |
| `mockApi` | 内存数据、延迟、故障注入、查询/更新/删除 | 让异步分支可重复测试,不依赖真实网络 |

## 起点缺口

| 里程碑 | 起点状态 | 需要完成 |
| --- | --- | --- |
| m1 | 静态数组 | `mockApi.list()` + 异步表格 |
| m2 | 无请求状态 | loading / error / retry / empty |
| m3 | 无查询 | 搜索、状态、部门、排序组合 |
| m4 | 假分页 | 真分页、每页条数、边界禁用 |
| m5 | 无详情 | 详情抽屉、关闭与 Esc |
| m6 | 无编辑 | 表单校验、异步保存、列表同步 |
| m7 | 无批量 | 当前页多选、删除确认 |
| m8 | 无文档 | mock 契约、数据模型、组件职责 |

## 验收方式

逐项对照 `content/projects/p2-data-workbench.json` 的 `m1`–`m8`:

1. **m1 接口与表格**:首屏先加载,再显示 20 条记录;默认按入职时间降序,第一页 5 条。
2. **m2 请求状态**:在 `solution/src/api/mockApi.js` 把默认 `failRate` 改为 1,刷新出现失败提示;改回 0 点重试恢复。搜索无结果时显示空状态。
3. **m3 组合查询**:搜索「张」+ 状态「在职」+ 部门「研发」只剩张驰;切换排序字段后页码回到 1。
4. **m4 分页**:默认 4 页,上一页/下一页在边界禁用;每页条数改为 10 后回到第 1 页且总页数变化。
5. **m5 详情抽屉**:点「查看 孙悦」打开抽屉,Esc 或遮罩可关闭,焦点落在关闭按钮。
6. **m6 编辑校验**:清空姓名、输入非法邮箱时不能提交;修正后保存,抽屉和表格同时更新。
7. **m7 批量删除**:勾选当前页两条,点「批量删除」先弹确认;取消后总数不变,确认后总数减少并提示删除数量。
8. **m8 测试与审查**:`cd solution && npm run test` 全绿;`docs/api.md`、`docs/data.md` 与实现一致。

自动化测试覆盖:加载→成功、失败→重试成功、排序/分页/筛选组合、空结果、编辑校验与保存、批量删除确认。

## 建议实现顺序

1. 先写 `mockApi` 和 `seedRecords`,确认接口返回结构。
2. 搭 `queryReducer`,把所有查询条件变化统一处理。
3. 接 `App` 的请求 effect,再补 `RequestState`。
4. 做 `DataTable`、`Pagination`、`Toolbar`。
5. 最后做 `DetailDrawer`、`RecordForm`、`ConfirmDialog`。

字段、接口参数和请求状态说明见 `docs/data.md` 与 `docs/api.md`。
数据模型
Record = { id: string, name: string, email: string, department: '产品' | '研发' | '设计' | '运营' | '财务', role: string, status: 'active' | 'on_leave' | 'inactive', salary: number, location: string, joinedAt: 'YYYY-MM-DD' }。查询状态为 { page, pageSize, sortBy, sortOrder, filters: { keyword, status, department } };接口返回 { items, total, page, pageSize, totalPages }。远程数据只保留接口响应,当前行、选中集合和编辑草稿都是界面临时状态。

Command Palette

Search for a command to run...