项目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 只记录当前详情,不复制任务对象。

Command Palette

Search for a command to run...