HarmonyOS 5开发从入门到精通(十三):待办事项应用实战(上)
本章将通过一个完整的待办事项应用实战项目,综合运用前面章节学到的知识,包括界面搭建、状态管理、数据存储等核心技能。我们将分上下两篇完成这个项目,本篇主要完成项目基础框架和核心功能。
一、项目需求分析
1.1 核心功能规划
待办事项应用需要实现以下核心功能:
- ✅ 任务列表展示:显示所有待办事项
- ✅ 添加新任务:通过输入框添加新任务
- ✅ 标记完成/未完成:点击任务切换完成状态
- ✅ 删除任务:支持删除单个任务
- ✅ 数据持久化:应用关闭后数据不丢失
- ✅ 任务统计:显示已完成/未完成数量
1.2 技术栈选择
- UI框架:ArkTS声明式UI
- 状态管理:@State、@Link装饰器
- 数据存储:Preferences本地存储
- 布局组件:Column、Row、List、ListItem
- 交互组件:TextInput、Button、Checkbox
二、项目结构设计
2.1 目录结构
todo-app/
├── entry/
│ └── src/main/
│ ├── ets/
│ │ ├── entryability/ # 应用入口
│ │ ├── pages/ # 页面目录
│ │ │ ├── Index.ets # 主页面
│ │ │ └── AddTask.ets # 添加任务页面
│ │ ├── model/ # 数据模型
│ │ │ └── TaskModel.ets # 任务模型
│ │ ├── utils/ # 工具类
│ │ │ └── StorageUtil.ets # 存储工具
│ │ └── components/ # 自定义组件
│ │ └── TaskItem.ets # 任务项组件
│ └── resources/ # 资源文件
└── module.json5 # 项目配置
2.2 数据模型定义
// model/TaskModel.ets
export class TaskModel {id: string = ''; // 任务唯一标识title: string = ''; // 任务标题completed: boolean = false; // 是否完成createTime: number = 0; // 创建时间戳constructor(title: string) {this.id = this.generateId();this.title = title;this.completed = false;this.createTime = new Date().getTime();}// 生成唯一IDprivate generateId(): string {return Date.now().toString() + Math.random().toString(36).substr(2);}
}
三、主页面开发
3.1 页面布局设计
主页面采用经典的垂直布局结构:
Column
├── Header (标题栏)
├── InputArea (输入区域)
├── FilterBar (筛选栏)
├── TaskList (任务列表)
└── Footer (底部统计)
3.2 主页面代码实现
// pages/Index.ets
import { TaskModel } from '../model/TaskModel';
import { StorageUtil } from '../utils/StorageUtil';
import TaskItem from '../components/TaskItem';@Entry
@Component
struct Index {@State taskList: TaskModel[] = []; // 任务列表@State newTaskTitle: string = ''; // 新任务标题@State filterType: string = 'all'; // 筛选类型:all/active/completed// 页面显示时加载数据aboutToAppear() {this.loadTasks();}// 从本地存储加载任务async loadTasks() {const tasks = await StorageUtil.getTasks();this.taskList = tasks;}// 添加新任务addTask() {if (this.newTaskTitle.trim() === '') {promptAction.showToast({ message: '请输入任务内容' });return;}const newTask = new TaskModel(this.newTaskTitle.trim());this.taskList.push(newTask);this.newTaskTitle = ''; // 清空输入框this.saveTasks(); // 保存到本地}// 删除任务deleteTask(id: string) {this.taskList = this.taskList.filter(task => task.id !== id);this.saveTasks();}// 切换任务完成状态toggleTask(id: string) {const task = this.taskList.find(t => t.id === id);if (task) {task.completed = !task.completed;this.taskList = [...this.taskList]; // 触发UI更新this.saveTasks();}}// 保存任务到本地async saveTasks() {await StorageUtil.saveTasks(this.taskList);}// 获取筛选后的任务列表get filteredTasks(): TaskModel[] {switch (this.filterType) {case 'active':return this.taskList.filter(task => !task.completed);case 'completed':return this.taskList.filter(task => task.completed);default:return this.taskList;}}// 获取已完成任务数量get completedCount(): number {return this.taskList.filter(task => task.completed).length;}// 获取未完成任务数量get activeCount(): number {return this.taskList.filter(task => !task.completed).length;}build() {Column({ space: 0 }) {// 标题栏this.buildHeader()// 输入区域this.buildInputArea()// 筛选栏this.buildFilterBar()// 任务列表this.buildTaskList()// 底部统计this.buildFooter()}.width('100%').height('100%').backgroundColor('#F5F5F5')}// 构建标题栏@BuilderbuildHeader() {Row() {Text('待办事项').fontSize(24).fontWeight(FontWeight.Bold).fontColor('#333333').layoutWeight(1)}.width('100%').height(60).padding({ left: 20, right: 20 }).backgroundColor('#FFFFFF').border({ width: { bottom: 1 }, color: '#EEEEEE' })}// 构建输入区域@BuilderbuildInputArea() {Row({ space: 10 }) {TextInput({ text: this.newTaskTitle, placeholder: '添加新任务...' }).placeholderColor('#999999').fontSize(16).layoutWeight(1).height(40).backgroundColor('#FFFFFF').borderRadius(8).border({ width: 1, color: '#DDDDDD' }).padding({ left: 12, right: 12 }).onChange((value: string) => {this.newTaskTitle = value;}).onSubmit(() => {this.addTask();})Button('添加').width(60).height(40).fontSize(14).fontColor('#FFFFFF').backgroundColor('#007AFF').borderRadius(8).onClick(() => {this.addTask();})}.width('100%').padding({ left: 20, right: 20, top: 20, bottom: 20 }).backgroundColor('#F5F5F5')}// 构建筛选栏@BuilderbuildFilterBar() {Row({ space: 20 }) {Button('全部').fontSize(14).fontColor(this.filterType === 'all' ? '#007AFF' : '#666666').backgroundColor('transparent').onClick(() => {this.filterType = 'all';})Button('未完成').fontSize(14).fontColor(this.filterType === 'active' ? '#007AFF' : '#666666').backgroundColor('transparent').onClick(() => {this.filterType = 'active';})Button('已完成').fontSize(14).fontColor(this.filterType === 'completed' ? '#007AFF' : '#666666').backgroundColor('transparent').onClick(() => {this.filterType = 'completed';})}.width('100%').height(40).padding({ left: 20, right: 20 }).backgroundColor('#FFFFFF').border({ width: { bottom: 1 }, color: '#EEEEEE' })}// 构建任务列表@BuilderbuildTaskList() {if (this.filteredTasks.length === 0) {Column() {Image($r('app.media.empty')).width(120).height(120).margin({ bottom: 20 })Text('暂无任务').fontSize(16).fontColor('#999999')}.width('100%').height(200).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center).backgroundColor('#FFFFFF').margin({ top: 1 })} else {List({ space: 1 }) {ForEach(this.filteredTasks, (item: TaskModel) => {ListItem() {TaskItem({task: item,onToggle: () => this.toggleTask(item.id),onDelete: () => this.deleteTask(item.id)})}}, (item: TaskModel) => item.id)}.width('100%').layoutWeight(1).divider({ strokeWidth: 1, color: '#EEEEEE', startMargin: 20, endMargin: 20 })}}// 构建底部统计@BuilderbuildFooter() {Row() {Text(`已完成 ${this.completedCount} / 总计 ${this.taskList.length}`).fontSize(14).fontColor('#666666').layoutWeight(1)if (this.completedCount > 0) {Button('清除已完成').fontSize(14).fontColor('#FF3B30').backgroundColor('transparent').onClick(() => {this.taskList = this.taskList.filter(task => !task.completed);this.saveTasks();})}}.width('100%').height(50).padding({ left: 20, right: 20 }).backgroundColor('#FFFFFF').border({ width: { top: 1 }, color: '#EEEEEE' })}
}
四、任务项组件开发
4.1 任务项组件设计
任务项组件需要支持以下功能:
- 显示任务标题
- 显示完成状态(复选框)
- 支持左滑删除
- 点击切换完成状态
4.2 任务项组件代码
// components/TaskItem.ets
import { TaskModel } from '../model/TaskModel';@Component
export default struct TaskItem {private task: TaskModel; // 任务数据private onToggle: () => void; // 切换完成状态回调private onDelete: () => void; // 删除任务回调build() {Row({ space: 12 }) {// 复选框Checkbox().checked(this.task.completed).width(24).height(24).onChange((checked: boolean) => {this.onToggle();})// 任务标题Text(this.task.title).fontSize(16).fontColor(this.task.completed ? '#999999' : '#333333').decoration({ type: this.task.completed ? TextDecorationType.LineThrough : TextDecorationType.None }).layoutWeight(1).maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })// 删除按钮Button() {Image($r('app.media.delete')).width(20).height(20)}.width(40).height(40).backgroundColor('transparent').onClick(() => {this.onDelete();})}.width('100%').height(60).padding({ left: 20, right: 20 }).backgroundColor('#FFFFFF')}
}
五、数据存储工具类
5.1 存储工具设计
使用HarmonyOS的Preferences进行本地数据存储:
// utils/StorageUtil.ets
import preferences from '@ohos.data.preferences';
import { TaskModel } from '../model/TaskModel';export class StorageUtil {private static readonly STORAGE_KEY = 'todo_tasks';// 获取Preferences实例private static async getPreferences(): Promise<preferences.Preferences> {const context = getContext();return await preferences.getPreferences(context, {name: 'todo_app_data'});}// 保存任务列表static async saveTasks(tasks: TaskModel[]): Promise<void> {try {const prefs = await this.getPreferences();const tasksJson = JSON.stringify(tasks);await prefs.put(this.STORAGE_KEY, tasksJson);await prefs.flush();} catch (error) {console.error('保存任务失败:', error);}}// 获取任务列表static async getTasks(): Promise<TaskModel[]> {try {const prefs = await this.getPreferences();const tasksJson = await prefs.get(this.STORAGE_KEY, '[]');const tasks = JSON.parse(tasksJson);return tasks.map((task: any) => {const model = new TaskModel(task.title);model.id = task.id;model.completed = task.completed;model.createTime = task.createTime;return model;});} catch (error) {console.error('获取任务失败:', error);return [];}}// 清空所有任务static async clearTasks(): Promise<void> {try {const prefs = await this.getPreferences();await prefs.delete(this.STORAGE_KEY);await prefs.flush();} catch (error) {console.error('清空任务失败:', error);}}
}
六、项目配置
6.1 权限配置
在module.json5中添加存储权限:
{"requestPermissions": [{"name": "ohos.permission.DISTRIBUTED_DATASYNC","reason": "需要存储待办事项数据"}]
}
6.2 资源文件准备
在resources/base/media目录下准备以下图片资源:
- empty.png(空状态图片)
- delete.png(删除图标)
七、功能测试
7.1 测试用例
- 添加任务测试: 输入任务内容,点击添加按钮 验证任务是否出现在列表中 验证输入框是否清空
- 标记完成测试: 点击任务复选框 验证任务是否显示删除线 验证已完成数量是否正确
- 删除任务测试: 点击任务删除按钮 验证任务是否从列表中移除 验证任务总数是否正确
- 数据持久化测试: 添加几个任务 关闭应用重新打开 验证任务数据是否保留
- 筛选功能测试: 添加已完成和未完成任务 点击不同筛选按钮 验证列表显示是否正确
八、本章总结
本章完成了待办事项应用的基础框架和核心功能,包括:
✅ 项目结构设计 - 合理的目录结构和模块划分
✅ 数据模型定义 - TaskModel类封装任务数据
✅ 主页面开发 - 完整的界面布局和交互逻辑
✅ 任务项组件 - 可复用的任务项组件
✅ 数据存储 - Preferences本地存储实现
✅ 筛选功能 - 按状态筛选任务列表
核心技术点:
- @State装饰器的状态管理
- ForEach循环渲染列表
- 自定义组件的props传递
- 本地数据持久化存储
- 条件渲染和样式切换