项目1 · React 任务看板
用一份任务数据驱动三列看板,把组件拆分、状态设计、派生数据和可访问交互真正串起来。
约 8 小时
学习重点
- 按职责拆分看板、列、任务卡、筛选栏和详情面板,能说清每个组件的边界
- 用 useReducer 集中处理任务增删改和状态迁移,避免多份状态互相覆盖
- 从任务数组派生分列结果、计数、搜索和筛选结果,不维护重复数据
- 实现不依赖鼠标的移动按钮、可读标签和明确提示,让核心流程可键盘完成
页面 / 接口清单
- 01看板页:待办、进行中、已完成三列与每列任务计数
- 02任务卡:标题、优先级、描述摘要、详情、移动和删除入口
- 03筛选栏:关键词搜索、状态筛选、结果统计与一键清除
- 04任务详情面板:查看、编辑、删除当前任务
- 05新建任务表单:标题、描述、优先级与输入校验
- 06本地持久化:任务变更写入 localStorage,刷新页面后恢复
验收清单
0 / 7(0%)
三列由同一份任务数组按 status 派生;每列显示标题、数量和空列提示,组件拆分与职责说明一致。
表单可创建任务到待办列;空标题或全空格标题被拦截并显示错误,成功创建后输入清空。
点击任务可打开详情;编辑保存后卡片和详情同步更新;删除后任务消失,详情回到未选中状态。
关键词匹配标题或描述,状态筛选可与关键词组合;结果计数准确,无匹配时出现空结果提示和清除入口。
每张任务卡提供「左移」「右移」按钮,边界列对应按钮禁用;按钮名称包含任务名,焦点和状态变化可感知。
新增、编辑、移动、删除后刷新仍保留;存储为空时使用示例任务,JSON 损坏时安全回退且界面不崩溃。
solution 下 npm run test 全绿;按验收清单逐项操作,README、数据模型和组件职责表与实现一致。
README(项目目录)
# React 任务看板(p1-task-board)
一个三列任务看板:任务只存一份数组,列、计数、搜索和筛选全部派生;新增、编辑、删除、移动都经过同一个 reducer,并自动写入 localStorage。
这个项目的重点不是把页面堆满功能,而是练会「状态放在哪里、什么该派生、交互如何对键盘用户也成立」。
## 学习目标
- 按职责拆分看板、列、任务卡、筛选栏和详情面板。
- 用 `useReducer` 集中处理任务变更,避免多处 `setState` 互相覆盖。
- 只保留一份任务数据,列与筛选结果全部派生。
- 用按钮完成左移/右移,给任务卡和操作提供可读标签与状态反馈。
- 用 localStorage 做最小持久化,并处理空数据和损坏数据。
## 环境要求
- Node.js 22.22.2+(推荐 24 LTS)
- npm 10+
## 快速开始
### 运行起点
```bash
cd starter
npm install --no-audit --no-fund
npm run dev
```
打开终端提示的本地地址。起点能正常显示三列静态任务,缺口以 `TODO m1` 到 `TODO m6` 标在 `src/App.jsx` 顶部。
### 运行参考实现
```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` 会以项目根为测试根目录加载它们,因此测试描述同时就是本项目的验收描述。
### 构建检查
```bash
cd starter
npm install --no-audit --no-fund
npm run build
```
`solution` 同样可以执行 `npm run build`。
## 目录结构
```text
p1-task-board/
├── 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
│ ├── main.jsx
│ ├── state.js
│ ├── styles.css
│ └── components/
│ ├── BoardColumn.jsx
│ ├── EmptyState.jsx
│ ├── FilterBar.jsx
│ ├── TaskCard.jsx
│ ├── TaskDetail.jsx
│ └── TaskForm.jsx
├── tests/
│ └── app.test.jsx # Vitest + Testing Library 行为测试
├── docs/
│ ├── components.md # 组件职责与数据流
│ └── data.md # 任务结构与状态模型
└── fixtures/
└── seed-tasks.json # 6 条示例任务
```
## 起点缺口
| 里程碑 | 起点状态 | 需要完成 |
| --- | --- | --- |
| m1 | 三列各自过滤静态数组 | 抽出单一任务数组,按 status 派生列 |
| m2 | 无表单 | 新建任务、标题必填校验 |
| m3 | 详情只读 | 编辑、删除、保存后同步 |
| m4 | 无筛选 | 关键词 + 状态组合筛选、空结果态 |
| m5 | 无移动 | 左移/右移按钮、边界禁用 |
| m6 | 无持久化 | localStorage 读写与损坏回退 |
## 验收方式
逐项对照 `content/projects/p1-task-board.json` 的 `m1`–`m7`:
1. **m1 单源看板**:新建任务后只出现在一列;删除后三列计数同步变化。
2. **m2 新建与校验**:提交空标题或全空格标题时出现「请输入任务标题」,任务数不变。
3. **m3 详情、编辑与删除**:打开详情 → 编辑标题 → 保存,卡片和详情同时更新;删除后详情回到空态。
4. **m4 搜索与筛选**:搜索「接口」只剩匹配项;再选「进行中」结果进一步收敛;切到「待办」出现「没有匹配的任务」。
5. **m5 键盘移动**:用 Tab 聚焦「右移」并按 Enter,任务进入下一列;待办列的左移和已完成列的右移按钮禁用。
6. **m6 持久化**:新增或移动后刷新页面,数据仍在;在 DevTools 把 localStorage 键改成非法 JSON,刷新后回到示例任务且不白屏。
7. **m7 测试与走查**:`cd solution && npm run test` 全绿;README、docs 和实现一致。
自动化测试覆盖:localStorage 注入恢复、写后重新挂载、空结果、输入校验、筛选组合、键盘移动、详情编辑删除、损坏数据回退。
## 推荐实现顺序
1. 先做 `state.js`:任务数组、reducer、校验函数。
2. 再拆 `BoardColumn` / `TaskCard`,让列只接收已经分好组的任务。
3. 接 `TaskForm`,新建和编辑共用校验。
4. 最后补 `FilterBar`、`EmptyState`、`TaskDetail` 和 localStorage。
关键取舍和字段说明见 `docs/data.md` 与 `docs/components.md`。
数据模型
Task = { id: string, title: string, description: string, status: 'todo' | 'doing' | 'done', priority: 'low' | 'normal' | 'high', createdAt: string, updatedAt: string }。全部任务只保存在一个数组中,三列、状态计数和搜索结果都由 status、title、description 派生;selectedId 只记录当前详情,不复制任务对象。