HarmonyOS 5开发从入门到精通(十三):待办事项应用实战(上)
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();
}
// 生成唯一ID
private 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')
}
// 构建标题栏
@Builder
buildHeader() {
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' })
}
// 构建输入区域
@Builder
buildInputArea() {
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')
}
// 构建筛选栏
@Builder
buildFilterBar() {
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' })
}
// 构建任务列表
@Builder
buildTaskList() {
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 })
}
}
// 构建底部统计
@Builder
buildFooter() {
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传递
- 本地数据持久化存储
- 条件渲染和样式切换
浙公网安备 33010602011771号