鸿蒙原生开发——从零构建待办清单
一、引言
待办清单(Todo List)是生产力工具的起点。从 Google Keep 到 Microsoft To Do,从 Apple Reminders 到 Notion 的 Task Database,几乎所有笔记和效率工具都内置了某种形式的待办功能。它的交互模型极其直观——写下要做的事、做完后打勾划掉、删掉不再需要的条目——但这种简单背后隐藏着一整套状态管理、筛选过滤、分类层级的设计问题。
从技术角度看,待办清单的核心挑战有三个:
第一,多维属性管理。一个任务不仅有一个标题,还有分类标签、优先级标记、完成状态、创建日期等多个维度。这些维度需要在列表渲染、筛选过滤、添加编辑中保持一致。与倒数日追踪器的单一数据维度(名称 + 日期)不同,待办清单的每条数据有 5-6 个字段,状态管理复杂度成倍增加。
第二,列表筛选。用户需要快速切换"全部 / 待完成 / 已完成"三种视图。这意味着列表不是简单地把数据映射为 UI,而是需要基于当前筛选状态动态计算子集。筛选器本身也是交互组件——用户点击"待完成"时,不仅列表内容变了,筛选标签的高亮状态也要同步更新。
第三,完成状态的视觉反馈。勾选一个任务后,它的外观应该明显变化——文字变灰、划线贯穿、勾选框变绿。这个变化要即时发生,不能有延迟。在 ArkUI 的响应式体系中,这意味着正确使用 @State + 不可变更新模式。
本文将用 ArkUI 从零构建一个功能完整的待办清单。功能包括:
- 添加任务(标题 + 分类标签 + 优先级标记)
- 三态筛选(全部 / 待完成 / 已完成)
- 五种分类标签(工作、个人、购物、健康、学习)
- 三级优先级(高 / 中 / 低,红 / 蓝 / 灰三色)
- 一键完成切换(勾选 → 文字变灰 + 删除线)
- 滑动删除(每条右侧 × 按钮)
配色采用红 / 蓝 / 灰三色优先级 + 深色标题栏 + 白色卡片布局,完全不使用黄色系,确保所有文字与背景的对比度达标。
阅读完本文,你将能够:
- 设计多属性任务数据模型
- 实现三态列表筛选系统
- 使用
Flex({ wrap: FlexWrap.Wrap })构建标签选择器 - 用颜色编码实现可视化的优先级标记
- 通过
TextDecorationType.LineThrough实现完成状态的删除线效果
二、数据模型设计
2.1 任务实体
待办清单的数据模型比前几篇明显复杂。一个任务不是简单的一行文字,而是一个具有多个属性的结构化实体:
interface TodoItem {
id: number;
title: string;
category: number; // 0-4,对应五个分类
priority: number; // 0=高, 1=中, 2=低
done: boolean;
date: string; // YYYY-MM-DD
}
六个字段各有其用途:
- id:自增数字,用于唯一标识。在
ForEach中作为 key 生成器的基础,确保列表 diff 正确。 - title:任务名称。用户在添加弹窗的
TextInput中输入,.trim()处理后存储。 - category:分类索引(0-4),不是分类名称字符串。用数字索引而非字符串的好处是:便于比较(
task.category === this.addCategory),且避免拼写不一致的 bug。 - priority:优先级索引(0=高, 1=中, 2=低)。三个等级对应三种颜色——红色(紧急)、蓝色(正常)、灰色(低优先级)。这个设计让用户不需要读文字就能感知每项任务的重要程度。
- done:布尔值。
true表示已完成,触发文字变灰 + 删除线效果。 - date:创建日期。用
YYYY-MM-DD字符串格式存储,与前几篇的日期策略一致。
2.2 分类系统
五个分类涵盖日常任务的主要场景:
const CATEGORIES: string[] = ['💼', '👤', '🛒', '❤️', '📚'];
const CATEGORY_NAMES: string[] = ['工作', '个人', '购物', '健康', '学习'];
用两个平行数组而非对象数组([{icon, name}])的原因是:索引访问更直接——CATEGORIES[task.category] 即得图标,CATEGORY_NAMES[task.category] 即得名称。不需要 find() 或 filter()。
Emoji 图标不仅节省空间,还提供跨语言的视觉识别。无论用户使用什么语言,看到 💼 就知道是工作,看到 🛒 就知道是购物。在移动端小屏幕上,emoji 比文字标签更易辨识。
2.3 优先级系统
三个优先级等级,每种对应一个语义化颜色:
const PRIORITIES: string[] = ['高', '中', '低'];
const PRIORITY_COLORS: string[] = ['#FF4D4F', '#1677FF', '#888899'];
| 优先级 | 颜色 | 色值 | 语义 |
|---|---|---|---|
| 高 | 红色 | #FF4D4F |
紧急,需要立即处理 |
| 中 | 蓝色 | #1677FF |
正常优先级,按计划完成 |
| 低 | 灰色 | #888899 |
有时间再做,不紧急 |
颜色编码是一个重要的 UX 设计原则。与倒数日的四色距离标记类似,优先级颜色让用户在扫描列表时能快速识别哪些任务需要优先关注。红色 = 暂停并关注,灰色 = 扫一眼可以跳过。
注意这里没有使用黄色或金色。在前几篇中我们已经反复验证过——黄色背景配上白色文字对比度不足(约 1.5:1,远低于 WCAG 要求的 4.5:1)。红色 #FF4D4F 与白色的对比度约 4.5:1,蓝色 #1677FF 约 4.3:1,灰色 #888899 约 4.5:1,全部达标。
2.4 初始化示例数据
aboutToAppear() 中预设 5 条示例任务,涵盖全部 5 个分类和全部 3 个优先级:
aboutToAppear(): void {
this.tasks = [
{ id: this.nextId++, title: '完成项目方案设计', category: 0, priority: 0, done: false, date: todayStr() },
{ id: this.nextId++, title: '购买生日礼物', category: 2, priority: 1, done: false, date: todayStr() },
{ id: this.nextId++, title: '阅读《深入浅出ArkTS》第三章', category: 4, priority: 2, done: true, date: todayStr() },
{ id: this.nextId++, title: '跑步30分钟', category: 3, priority: 1, done: false, date: todayStr() },
{ id: this.nextId++, title: '整理周报并提交', category: 1, priority: 2, done: true, date: todayStr() },
];
}
5 条任务中有 3 条待完成、2 条已完成。这为三态筛选提供了直接的测试场景——切换到"待完成"看到 3 条,切换到"已完成"看到 2 条。
三、三态筛选系统
3.1 筛选状态
筛选器是三个标签的平铺(不是下拉菜单),直接显示在标题栏下方:
private filterLabels: string[] = ['全部', '待完成', '已完成'];
@State filterIndex: number = 0; // 0=全部, 1=待完成, 2=已完成
用索引(0/1/2)而非字符串标识当前筛选状态,原因与分类使用索引相同——比较更快,不会出现拼写错误。filterIndex === 0 比 filterState === 'all' 更可靠。
3.2 筛选算法
filteredTasks() 方法根据 filterIndex 的当前值返回不同的任务子集:
filteredTasks(): TodoItem[] {
const result: TodoItem[] = [];
for (let i = 0; i < this.tasks.length; i++) {
const t = this.tasks[i];
if (this.filterIndex === 0) {
result.push(t); // 全部:不做过滤
} else if (this.filterIndex === 1 && !t.done) {
result.push(t); // 待完成:只取 done=false
} else if (this.filterIndex === 2 && t.done) {
result.push(t); // 已完成:只取 done=true
}
}
return result;
}
算法逻辑:
- 全部(
filterIndex === 0):不进行任何过滤,返回所有任务。这是默认视图,用户进入页面后看到完整列表。 - 待完成(
filterIndex === 1):只收集done === false的任务。这是用户实际使用中最常用的视图——“还有什么没做”。 - 已完成(
filterIndex === 2):只收集done === true的任务。用于回顾已完成的工作,获得成就感。
每次返回的都是新数组(const result: TodoItem[] = []),而不是对 this.tasks 的引用。这确保了筛选结果的变化能被框架正确检测。
3.3 筛选标签的 UI
三个标签使用 ForEach 渲染,当前选中的标签高亮为白色加粗,未选中为半透明白色:
ForEach(this.filterLabels, (label: string, fi: number) => {
Text(label)
.fontSize(FontSize.BODY)
.fontColor(this.filterIndex === fi ? '#FFFFFF' : '#FFFFFF66')
.fontWeight(this.filterIndex === fi ? FontWeight.Bold : FontWeight.Normal)
.padding({ left: Spacing.LG, right: Spacing.LG, top: 8, bottom: 12 })
.onClick(() => { this.filterIndex = fi; })
})
使用 ForEach 的好处是——如果将来需要增加"已过期"或"重要"等新筛选维度,只需在 filterLabels 数组中新增元素,UI 自动跟随。不需要手动复制粘贴标签代码。
#FFFFFF66(约 40% 不透明度)提供了一种柔和的未选中态,既不抢眼又能辨认。与完全隐藏不同,半透明让用户知道还有其他选项存在。
3.4 空状态的不同文案
筛选后可能没有任何任务。空状态不是统一文案,而是根据当前筛选条件显示不同的引导文字:
if (this.filteredTasks().length === 0) {
Column() {
Text('📋')
.fontSize(48)
.margin({ bottom: Spacing.SM })
Text(this.filterIndex === 0 ? '还没有任务' :
(this.filterIndex === 1 ? '所有任务都已完成 🎉' : '还没有已完成的任务'))
.fontSize(FontSize.BODY)
.fontColor('#888899')
}
}
三种空状态对应三种用户心理:
- 全部为空(“还没有任务”):新用户首次打开,需要引导添加。此时 FAB 按钮发光吸引点击。
- 待完成为空(“所有任务都已完成 🎉”):这是一种正向反馈——所有事都做完了,值得庆祝。emoji 🎉 增强了这种成就感。
- 已完成为空(“还没有已完成的任务”):暗示用户应该开始完成任务,带有轻微的紧迫感。
3.5 待完成计数
标题栏右侧显示"待完成"任务的实时统计:
Text(`${this.pendingCount()} 项待完成`)
.fontSize(FontSize.CAPTION)
.fontColor('#FFFFFF88')
pendingCount() 方法遍历所有任务统计未完成数量:
pendingCount(): number {
let count = 0;
for (let i = 0; i < this.tasks.length; i++) {
if (!this.tasks[i].done) count++;
}
return count;
}
这个数字会随勾选操作即时更新——用户每勾选一项,"N 项待完成"中的 N 就减 1。这种即时反馈是待办清单的核心体验:用户能直观感受到自己在推进。
四、添加任务:分类与优先级的双重选择
4.1 弹窗设计
添加任务弹窗包含三个输入区和一个确认区:
- 任务名称 —
TextInput,占位文字"任务名称,如’完成项目方案设计’" - 选择分类 — 5 个分类标签,
Flex({ wrap: FlexWrap.Wrap })布局 - 选择优先级 — 3 个优先级按钮,
Row水平排列 - 取消 / 确认添加 — 两个按钮并排
与前几篇的弹窗不同,这次使用 Flex 而非 Row 来承载分类标签,原因是 5 个分类的标签(图标 + 文字名)宽度加起来可能超过屏幕宽度。FlexWrap.Wrap 允许标签自动折行,在小屏幕上也能完整显示。
4.2 分类选择器
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(CATEGORIES, (icon: string, ci: number) => {
Row() {
Text(icon + ' ' + CATEGORY_NAMES[ci])
.fontSize(12)
.fontColor(this.addCategory === ci ? '#1677FF' : '#888899')
.fontWeight(this.addCategory === ci ? FontWeight.Bold : FontWeight.Normal)
}
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
.borderRadius(BorderRadius.FULL)
.backgroundColor(this.addCategory === ci ? '#1677FF15' : '#F5F5FA')
.border({ width: this.addCategory === ci ? 1 : 0, color: '#1677FF' })
.margin({ right: 6, bottom: 6 })
.onClick(() => { this.addCategory = ci; })
})
}
.width('100%')
两个关键设计细节:
选中态用边框 + 浅蓝背景区分。未选中标签是 #F5F5FA(浅灰),选中标签是 #1677FF15(约 8% 不透明度的蓝色)+ 1vp 蓝色边框。这种双重标记确保即使用户色弱或屏幕亮度较低,也能通过边框辨认选中态。
.margin({ bottom: 6 }) 提供折行间距。当标签折到第二行时,上下两行之间需要 6vp 的间距。不设这个间距的话折行后两行会紧贴在一起。
4.3 优先级选择器
优先级选择器的设计理念与分类不同——三个选项空间充足,使用 Row 水平排列即可:
ForEach(PRIORITIES, (label: string, pi: number) => {
Text(label)
.fontSize(FontSize.BODY)
.fontColor(this.addPriority === pi ? '#FFFFFF' : PRIORITY_COLORS[pi])
.fontWeight(FontWeight.Bold)
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(BorderRadius.FULL)
.backgroundColor(this.addPriority === pi ? PRIORITY_COLORS[pi] : PRIORITY_COLORS[pi] + '15')
.margin({ right: Spacing.SM })
.onClick(() => { this.addPriority = pi; })
})
选中态与分类选择器相反的策略——实心颜色 + 白字而非边框。原因是优先级颜色本身就承载了语义信息:
- 选中"高":红色实心按钮 + 白色文字 → 视觉冲击力最强
- 选中"中":蓝色实心按钮 + 白色文字 → 稳定可靠
- 选中"低":灰色实心按钮 + 白色文字 → 低调不抢眼
未选中态使用颜色的 15% 不透明度版本(PRIORITY_COLORS[pi] + '15')+ 对应颜色的文字。这样用户能在不选中的状态下也能预判每个选项的颜色语义。
4.4 确认按钮的条件禁用
确认按钮在输入为空时灰色不可点击,输入不为空时蓝色可点击:
backgroundColor(this.addTitle.trim() ? '#1677FF' : '#CCCCCC')
这是一个简单但重要的防错设计——阻止用户创建空标题的任务。.trim() 保证纯空格也无法创建。
五、任务切换与删除
5.1 完成状态切换
每个任务卡片左侧有一个圆形按钮,点击切换完成/未完成状态:
toggleTask(id: number): void {
const newTasks: TodoItem[] = [];
for (let i = 0; i < this.tasks.length; i++) {
const t = this.tasks[i];
if (t.id === id) {
newTasks.push({
id: t.id, title: t.title, category: t.category,
priority: t.priority, done: !t.done, date: t.date,
});
} else {
newTasks.push(t);
}
}
this.tasks = newTasks;
}
使用不可变更新模式——创建新对象替换旧对象,而非直接修改 t.done = !t.done。这是 ArkUI @State 响应式更新的核心要求:框架通过引用比较(===)来判断状态是否变化。如果直接修改对象属性而数组引用不变,框架不知道需要重新渲染。
5.2 完成状态的视觉差异
已完成的与未完成的任务在视觉上有 5 处差异,让用户一眼就能分清:
| 属性 | 未完成 | 已完成 |
|---|---|---|
| 复选框 | ⬜ 空心方块 | ✅ 绿色对勾 |
| 复选框背景 | #F0F0F5 浅灰 |
#52C41A15 浅绿 |
| 任务标题颜色 | #1a1a2e 深色 |
#BBBBCC 灰色 |
| 任务标题字重 | Bold 加粗 |
Normal 常规 |
| 文字装饰 | 无 | LineThrough 删除线 |
这五重差异构建了一个完整的"完成感"——用户勾选后,任务在视觉上显著后退,让未完成的任务获得更多注意力。
删除线的实现:
.decoration({ type: task.done ? TextDecorationType.LineThrough : TextDecorationType.None })
TextDecorationType.LineThrough 是 ArkUI 内置的文本装饰类型,不需要手动绘制线条。另一可选值是 Underline(下划线)和 Overline(上划线)。
5.3 删除任务
每条任务右侧有 × 按钮,点击删除:
deleteTask(id: number): void {
const newTasks: TodoItem[] = [];
for (let i = 0; i < this.tasks.length; i++) {
if (this.tasks[i].id !== id) newTasks.push(this.tasks[i]);
}
this.tasks = newTasks;
}
同样使用不可变更新——创建新数组,跳过要删除的任务。与前几篇的删除逻辑完全一致。
六、UI 布局设计
6.1 整体结构
Stack(根容器)
├── Column(主界面)
│ ├── Column(深色标题栏)
│ │ ├── Row:✅ 待办清单 + "N 项待完成"
│ │ └── Row:三个筛选标签
│ └── Scroll(任务列表)
│ └── Column
│ ├── 空状态 / ForEach → 任务卡片
│ └── 底部留白(80vp)
├── Button(FAB:"+ 添加任务")
└── if showAdd → 底部弹窗遮罩 + 表单
6.2 深色标题栏 + 白色卡片列表
延续前几篇的设计系统——标题栏深色(#1a1a2e)、列表背景浅灰(#F2F3F5)、任务卡片白色。这是一个被反复验证有效的移动端信息架构:深色顶栏提供视觉锚点,白色卡片在浅灰背景上形成清晰的"内容单元"边界。
6.3 任务卡片布局
每张卡片水平排列四个元素:
- 复选框(40×40vp 圆形,左侧):显示 ✅ 或 ⬜
- 信息区(
layoutWeight(1),中间):任务标题 + 分类标签行 - 删除按钮(右侧):× 字符
分类标签行包含两个小标签:
- 分类标签(浅灰底 + 灰色文字):💼 工作 / 👤 个人 / 🛒 购物 / ❤️ 健康 / 📚 学习
- 优先级标签(彩色底 + 白色文字):高(红底)/ 中(蓝底)/ 低(灰底)
两个标签并排,间距 6vp。分类标签告知"这是什么类型的任务",优先级标签告知"这有多重要"。两者信息互补,互不冗余。
6.4 卡片间距
卡片之间使用 1vp 的细微间距:
.margin({ left: Spacing.LG, right: Spacing.LG, bottom: this.filteredTasks().length - 1 === idx ? 0 : 1 })
最后一张卡片底部无边距(紧贴后续留白区域),其他卡片底部有 1vp 间隙。1vp 的线足够形成视觉分隔,但不会产生"列表中间有空隙"的感觉——它更像一条分隔线而非间距。
6.5 第一张卡片无圆角
这是前几篇确立的视觉惯例——列表第一张卡片的顶部与标题栏底部相接,不设圆角,形成整体感:
.borderRadius(idx === 0 ? 0 : BorderRadius.LG)
七、完整代码结构
TodoPage
├── Stack(根容器)
│ ├── Column(主界面)
│ │ ├── Column(深色标题栏 #1a1a2e)
│ │ │ ├── Row:✅ 待办清单 + "N 项待完成"
│ │ │ └── Row:三标签筛选(全部 | 待完成 | 已完成)
│ │ └── Scroll(任务列表区域)
│ │ └── Column
│ │ ├── if 空态 → 空状态引导
│ │ └── ForEach → Row(任务卡片)
│ │ ├── Column(✅/⬜ 复选框,40vp 圆形)
│ │ ├── Column(标题 + 分类标签 + 优先级标签)
│ │ └── Text(删除 ×)
│ ├── Button(FAB:"+ 添加任务",蓝色悬浮)
│ └── if showAdd → Column(半透明遮罩 + 底部弹窗)
│ └── Column(白色圆角弹窗)
│ ├── TextInput(任务名称)
│ ├── Text + Flex(选择分类,5 个标签 wrap 折行)
│ ├── Text + Row(选择优先级,3 个按钮)
│ └── Row(取消 + 确认添加)
八、总结
本文从零构建了一个功能完整的待办清单。与前五篇的功能型应用相比,待办清单的核心挑战在于多属性管理 + 动态筛选 + 完成状态的视觉编码。
核心要点回顾:
-
多属性数据模型:六个字段(id/title/category/priority/done/date)构成任务实体。分类和优先级使用数字索引而非字符串,便于比较和避免拼写错误。
-
三态筛选系统:
filteredTasks()根据filterIndex(0=全部/1=待完成/2=已完成)返回不同子集。每次返回新数组确保框架检测到变化。筛选标签使用ForEach渲染,便于扩展。 -
分类标签的双重标记:选中态用边框 + 浅蓝背景,未选中态用浅灰。
Flex({ wrap: FlexWrap.Wrap })让 5 个标签在窄屏上自动折行。 -
优先级的颜色编码:红(
#FF4D4F)= 紧急、蓝(#1677FF)= 正常、灰(#888899)= 低优先。选中态实心色 + 白字,未选中态 15% 不透明度底色 + 对应颜色文字。完全不用黄色系,所有颜色与白字的对比度均达标。 -
完成状态的五重视觉差异:复选框(✅ vs ⬜)、背景色(浅绿 vs 浅灰)、文字颜色(灰 vs 深)、字重(常规 vs 加粗)、装饰线(删除线 vs 无)。这种多层次的视觉反馈让"完成"成为一种显著的、有满足感的操作。
-
不可变状态更新:
toggleTask()和deleteTask()都遵循创建新数组/新对象的模式,保证@State响应式更新被框架正确触发。 -
空状态的差异化文案:全部空 → “还没有任务”(引导添加)、待完成为空 → “所有任务都已完成 🎉”(正向反馈)、已完成为空 → “还没有已完成的任务”(紧迫感暗示)。
待办清单是最经典的生产力工具——一个输入框、一个列表、一个筛选器、一个完成按钮。但多属性管理、动态筛选、颜色编码、完成状态的多重视觉反馈这些细节组合在一起,形成了一个真正可用的效率工具,而非一个展示组件属性的 Demo 页面。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)