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 测试用例

  1. 添加任务测试: 输入任务内容,点击添加按钮 验证任务是否出现在列表中 验证输入框是否清空
  2. 标记完成测试: 点击任务复选框 验证任务是否显示删除线 验证已完成数量是否正确
  3. 删除任务测试: 点击任务删除按钮 验证任务是否从列表中移除 验证任务总数是否正确
  4. 数据持久化测试: 添加几个任务 关闭应用重新打开 验证任务数据是否保留
  5. 筛选功能测试: 添加已完成和未完成任务 点击不同筛选按钮 验证列表显示是否正确

八、本章总结

本章完成了待办事项应用的基础框架和核心功能,包括:

项目结构设计 - 合理的目录结构和模块划分

数据模型定义 - TaskModel类封装任务数据

主页面开发 - 完整的界面布局和交互逻辑

任务项组件 - 可复用的任务项组件

数据存储 - Preferences本地存储实现

筛选功能 - 按状态筛选任务列表

核心技术点

  • @State装饰器的状态管理
  • ForEach循环渲染列表
  • 自定义组件的props传递
  • 本地数据持久化存储
  • 条件渲染和样式切换
posted @ 2025-12-23 21:06  奇崽  阅读(0)  评论(0)    收藏  举报