鸿蒙 HarmonyOS 待办清单应用开发实战 —— 基于 ArkTS + SQLite 的完整教程

鸿蒙 HarmonyOS 待办清单应用开发实战 ------ 基于 ArkTS + SQLite 的完整教程

一、前言

在快节奏的现代生活中,待办清单(Todo List) 是最基础也最实用的生产力工具之一。无论是学生管理学习任务、上班族跟踪工作进度,还是家庭主妇规划日常采购,一个清爽好用的待办清单应用都能让生活井井有条。

本文将带你从零到一,使用 HarmonyOS(鸿蒙系统)ArkTS 语言和 Stage 模型,结合 SQLite 关系型数据库,打造一个功能完整、交互流畅的待办清单应用。

全文约 10000 字,涵盖项目架构、数据库设计、CRUD 操作、UI 构建、状态管理、定时提醒、筛选过滤等方方面面,所有代码均来自真实可运行的鸿蒙项目,你可以直接对照学习并在 DevEco Studio 中验证。


二、项目概览

2.1 功能清单

在动手编码之前,我们先来明确这个待办清单应用需要实现哪些功能:

功能模块 详细功能 优先级
新增待办 填写标题、描述、选择优先级、设置截止日期 ⭐ 核心
查看列表 以卡片形式展示所有待办事项 ⭐ 核心
编辑待办 修改已有待办的标题、描述、优先级、截止日期 ⭐ 核心
删除待办 删除不再需要的待办事项(含确认弹窗) ⭐ 核心
完成切换 通过 Checkbox 切换待办的完成/未完成状态 ⭐ 核心
筛选过滤 按「全部」「待完成」「已完成」三种视图筛选 ⭐ 重要
优先级管理 高/中/低三级优先级,不同颜色标识 ⭐ 重要
截止日期 设置截止日期,逾期自动标记提醒 ⭐ 重要
定时提醒 每 30 秒检查是否有今天截止的未完成待办 ⭐ 加分
数据统计 顶部统计栏显示总计 / 待完成 / 已完成数量 ⭐ 加分
数据持久化 使用 SQLite 关系型数据库存储所有数据 ⭐ 核心

2.2 技术栈

技术 用途 说明
ArkTS 开发语言 基于 TypeScript 的声明式 UI 语言
Stage 模型 应用模型 HarmonyOS 3.0+ 主推的应用开发模型
@ohos.data.relationalStore 数据库 API 鸿蒙系统提供的 SQLite 关系型数据库接口
@State / @Builder 状态管理与 UI 构建 ArkTS 声明式 UI 的核心装饰器
CustomDialog 自定义弹窗 用于新增/编辑待办的对话框
setInterval / clearInterval 定时器 实现定时提醒检查
promptAction.showToast Toast 提示 操作结果的轻量反馈

三、项目结构

复制代码
Demo0528/
├── AppScope/                          # 应用级配置
│   └── app.json5                      # 应用全局配置
├── entry/                             # 主 Entry 模块
│   ├── src/main/
│   │   ├── ets/
│   │   │   ├── entryability/
│   │   │   │   └── EntryAbility.ets   # Ability 入口
│   │   │   └── pages/
│   │   │       ├── Index.ets          # 首页(入口引导页)
│   │   │       └── TodoList.ets       # 待办清单主页面(1082 行)
│   │   ├── module.json5               # 模块配置
│   │   └── resources/                 # 资源文件
│   ├── build-profile.json5
│   └── hvigorfile.ts
├── hvigor/                            # 构建配置
├── build-profile.json5                # 全局构建配置
└── hvigorfile.ts                      # 构建脚本

3.1 关键文件说明

  • EntryAbility.ets :应用的入口 Ability,在 onWindowStageCreate 中直接加载 pages/TodoList 作为首页。
  • Index.ets:一个简单的引导页,展示应用名称并提供"进入待办清单"按钮。实际运行时 EntryAbility 直接跳过了它,但在开发调试阶段可作为入口参考。
  • TodoList.ets:应用的核心文件,包含数据库管理、自定义弹窗、主页面 UI 构建等全部逻辑,共 1082 行。

3.2 EntryAbility 配置

typescript 复制代码
// EntryAbility.ets
onWindowStageCreate(windowStage: window.WindowStage): void {
  windowStage.loadContent('pages/TodoList', (err) => {
    if (err.code) {
      hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
      return;
    }
    hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
  });
}

这里直接将 TodoList 页面作为应用启动后的首页加载,跳过了 Index 引导页,用户打开应用就能直接看到待办清单。


四、数据持久化层设计

4.1 为什么选择 SQLite

在鸿蒙应用开发中,数据持久化方案主要有以下几种:

方案 适用场景 特点
Preferences(首选项) 键值对存储,适合少量配置 异步、轻量、不支持复杂查询
SQLite (relationalStore) 结构化数据,CRUD 操作频繁 关系型数据库,支持 SQL 查询、排序、过滤
分布式数据库 多设备数据协同 基于 SQLite,支持跨设备同步
文件存储 大文件、图片、日志 直接读写文件系统

对于待办清单这种需要频繁进行增删改查、排序、筛选的结构化数据场景,SQLite 是最合适的选择。鸿蒙系统提供了 @ohos.data.relationalStore API,封装了 SQLite 的底层操作,使用起来非常方便。

4.2 数据库表结构设计

typescript 复制代码
const DB_NAME: string = 'todo_app.db';
const TABLE_NAME: string = 'todo';
const DB_VERSION: number = 1;

数据库建表 SQL:

sql 复制代码
CREATE TABLE IF NOT EXISTS todo (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  title TEXT NOT NULL,
  description TEXT DEFAULT '',
  priority INTEGER DEFAULT 2,   -- 1=高, 2=中, 3=低
  dueDate TEXT DEFAULT '',
  isCompleted INTEGER DEFAULT 0, -- 0=未完成, 1=已完成
  createdTime TEXT DEFAULT ''
);

各字段说明:

字段 类型 说明
id INTEGER (PRIMARY KEY AUTOINCREMENT) 主键,自增
title TEXT (NOT NULL) 待办标题,必填
description TEXT (DEFAULT '') 待办描述,可选
priority INTEGER (DEFAULT 2) 优先级:1=高, 2=中, 3=低
dueDate TEXT (DEFAULT '') 截止日期,格式:yyyy-MM-dd
isCompleted INTEGER (DEFAULT 0) 完成状态:0=未完成, 1=已完成
createdTime TEXT (DEFAULT '') 创建时间,ISO 格式时间戳

4.3 数据模型(TypeScript 接口)

我们定义了两个接口来映射前后端的数据:

typescript 复制代码
// 完整的待办事项(含 id)
interface TodoItem {
  id: number;
  title: string;
  description: string;
  priority: Priority;
  dueDate: string;
  isCompleted: number;
  createdTime: string;
}

// 新增待办时的输入数据(不含 id 和 createdTime,由数据库自动生成)
interface InsertTodoItem {
  title: string;
  description: string;
  priority: Priority;
  dueDate: string;
  isCompleted: number;
}

将"完整数据"和"新增数据"分离的设计,可以让类型系统更好地帮助我们避免错误------新增时不需要传 idcreatedTime,这两个字段由数据库自动管理。

4.4 TodoDatabase 类 ------ 数据库管理核心

TodoDatabase 类封装了对 todo 表的所有操作,对外提供清晰的异步方法接口。

4.4.1 初始化与建表
typescript 复制代码
class TodoDatabase {
  private rdbStore: relationalStore.RdbStore | null = null;

  async init(context: Context): Promise<void> {
    if (this.rdbStore) {
      return; // 避免重复初始化
    }
    const config: relationalStore.StoreConfig = {
      name: DB_NAME,
      securityLevel: relationalStore.SecurityLevel.S1,
    };
    this.rdbStore = await relationalStore.getRdbStore(context, config);
    this.rdbStore.version = DB_VERSION;
    await this.createTable();
    console.info('[TodoDB] 数据库初始化成功');
  }

  private async createTable(): Promise<void> {
    const sql = `CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      title TEXT NOT NULL,
      description TEXT DEFAULT '',
      priority INTEGER DEFAULT ${Priority.MEDIUM},
      dueDate TEXT DEFAULT '',
      isCompleted INTEGER DEFAULT 0,
      createdTime TEXT DEFAULT ''
    )`;
    await this.rdbStore!.executeSql(sql);
    console.info('[TodoDB] 数据表创建/验证完成');
  }
}

这里有几个设计要点:

  1. 单例模式 :全局只创建一个 TodoDatabase 实例(const todoDB = new TodoDatabase()),避免重复打开数据库连接。
  2. 幂等建表 :使用 CREATE TABLE IF NOT EXISTS,无论应用启动多少次,建表操作都是安全的。
  3. 延迟初始化init() 方法需要 Context 参数,在组件 aboutToAppear 生命周期中调用,确保获取到正确的应用上下文。
4.4.2 查询所有待办(带排序)
typescript 复制代码
async queryAll(): Promise<TodoItem[]> {
  const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
  predicates.orderByAsc('isCompleted');   // 未完成的排在前面
  predicates.orderByAsc('priority');      // 优先级高的排在前面
  predicates.orderByAsc('dueDate');       // 截止日期早的排在前面
  const resultSet = await this.rdbStore!.query(predicates);
  // ... 遍历 resultSet 构造 TodoItem 数组
}

排序策略非常讲究用户体验:

  • 第一排序(isCompleted 升序):未完成(0)排在已完成(1)前面,用户最关心的是还需要完成的事情。
  • 第二排序(priority 升序):高优先级(1)排在中(2)前,中排在低(3)前,确保紧急事项优先展示。
  • 第三排序(dueDate 升序):截止日期早的排在前面,进一步细化排序。

这种"三级排序"策略确保了列表的展示顺序总是:未完成的先按优先级排,同级内按截止日期排,最后才是已完成项

4.4.3 新增待办
typescript 复制代码
async insert(item: InsertTodoItem): Promise<number> {
  if (!item.title.trim()) {
    throw new Error('标题不能为空');
  }
  const value: relationalStore.ValuesBucket = {
    title: item.title.trim(),
    description: item.description.trim(),
    priority: item.priority,
    dueDate: item.dueDate,
    isCompleted: item.isCompleted,
    createdTime: new Date().toISOString(),
  };
  const rowId = await this.rdbStore.insert(TABLE_NAME, value);
  return rowId;
}
  • 输入校验:在数据库层就做空标题校验,双重保障数据完整性。
  • 自动时间戳createdTime 在插入时自动生成,无需调用方传入。
  • 修剪空白trim() 去除用户输入的首尾空格。
4.4.4 更新与切换完成状态
typescript 复制代码
async update(item: TodoItem): Promise<void> {
  if (!item.title.trim()) {
    throw new Error('标题不能为空');
  }
  const value: relationalStore.ValuesBucket = {
    title: item.title.trim(),
    description: item.description.trim(),
    priority: item.priority,
    dueDate: item.dueDate,
    isCompleted: item.isCompleted,
  };
  const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
  predicates.equalTo('id', item.id);
  await this.rdbStore.update(value, predicates);
}

async toggleCompleted(id: number, currentStatus: number): Promise<void> {
  const value: relationalStore.ValuesBucket = {
    isCompleted: currentStatus === 0 ? 1 : 0,
  };
  const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
  predicates.equalTo('id', id);
  await this.rdbStore.update(value, predicates);
}

update 用于编辑待办时更新全部字段,而 toggleCompleted 专门用于 Checkbox 点击------它只看当前状态,做了 0→1 或 1→0 的翻转,无需调用方自己计算新状态。

4.4.5 删除待办
typescript 复制代码
async delete(id: number): Promise<void> {
  const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
  predicates.equalTo('id', id);
  await this.rdbStore.delete(predicates);
}
4.4.6 查询今天截止的未完成任务(提醒用)
typescript 复制代码
async queryDueTodayActive(): Promise<TodoItem[]> {
  const today = this.getTodayStr();
  const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
  predicates.equalTo('isCompleted', 0);
  predicates.equalTo('dueDate', today);
  const resultSet = await this.rdbStore.query(predicates);
  // ... 遍历 resultSet 构造列表
}

private getTodayStr(): string {
  const d = new Date();
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  return `${y}-${m}-${day}`;
}

这个方法专为定时提醒功能设计,只查询"今天截止且未完成"的待办事项。数据库层面的过滤比在应用层过滤效率高得多,特别是当数据量较大时。


五、枚举与工具函数

5.1 优先级枚举

typescript 复制代码
enum Priority {
  HIGH = 1,
  MEDIUM = 2,
  LOW = 3,
}

Priority 枚举的值同时是数据库中的 priority 字段值,1 表示高优先级、2 表示中、3 表示低。这样设计的好处是:排序时直接按数值升序就能从高到低排列,无需额外映射。

对应的显示函数:

typescript 复制代码
function getPriorityLabel(p: Priority): string {
  switch (p) {
    case Priority.HIGH: return '高';
    case Priority.MEDIUM: return '中';
    case Priority.LOW: return '低';
    default: return '中';
  }
}

function getPriorityColor(p: Priority): ResourceColor {
  switch (p) {
    case Priority.HIGH: return '#FF3B30';   // iOS 红色
    case Priority.MEDIUM: return '#FF9500';  // iOS 橙色
    case Priority.LOW: return '#007AFF';     // iOS 蓝色
    default: return '#007AFF';
  }
}

三种优先级的配色致敬了 iOS 系统级的语义颜色:

优先级 颜色 色值 语义
红色 #FF3B30 紧急、警告、需要立即处理
橙色 #FF9500 重要但不紧急
蓝色 #007AFF 常规、待定、可延后

5.2 筛选枚举

typescript 复制代码
enum FilterType {
  ALL = 0,      // 全部
  ACTIVE = 1,   // 待完成
  COMPLETED = 2,// 已完成
}

function getFilterLabel(filter: FilterType): string {
  switch (filter) {
    case FilterType.ALL: return '全部';
    case FilterType.ACTIVE: return '待完成';
    case FilterType.COMPLETED: return '已完成';
    default: return '全部';
  }
}

5.3 日期工具函数

typescript 复制代码
/** 将 yyyy-MM-dd 格式日期转为中文显示 */
function formatDate(dateStr: string): string {
  if (!dateStr) return '无截止日期';
  const parts = dateStr.split('-');
  if (parts.length !== 3) return dateStr;
  return `${parts[0]}年${parseInt(parts[1], 10)}月${parseInt(parts[2], 10)}日`;
}

/** 判断是否逾期(日期小于今天) */
function isOverdue(dateStr: string): boolean {
  if (!dateStr) return false;
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  const due = new Date(dateStr);
  due.setHours(0, 0, 0, 0);
  return due.getTime() < today.getTime();
}

/** 判断是否今天截止 */
function isDueToday(dateStr: string): boolean {
  if (!dateStr) return false;
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  const due = new Date(dateStr);
  due.setHours(0, 0, 0, 0);
  return due.getTime() === today.getTime();
}

重要细节 :日期比较时一定要用 setHours(0, 0, 0, 0) 将时间归零,否则 new Date() 包含当前时分秒,直接比较 getTime() 会导致逻辑错误。例如,当前时间是 2025 年 6 月 5 日 14:30,new Date('2025-06-05').getTime() 是当天 00:00:00 的时间戳,如果不归零,new Date() 的 14:30 比 00:00 大,就会错误地认为 2025-06-05 是"已过去"的日期。


六、自定义弹窗 ------ TodoEditDialog

新增和编辑待办使用同一个自定义弹窗组件 TodoEditDialog,通过 editItem 参数区分两种模式。

6.1 组件定义与状态

typescript 复制代码
@CustomDialog
struct TodoEditDialog {
  controller: CustomDialogController;
  editItem?: TodoItem;  // 编辑模式传入;新增模式为 undefined

  onConfirm: (title: string, description: string, priority: Priority, dueDate: string) => void;

  @State title: string = '';
  @State description: string = '';
  @State priority: Priority = Priority.MEDIUM;
  @State dueDate: string = '';
  @State titleError: string = '';
  @State showDatePicker: boolean = false;
  @State pickYear: number = new Date().getFullYear();
  @State pickMonth: number = new Date().getMonth() + 1;
  @State pickDay: number = new Date().getDate();
}

关键设计思路:

  • 双向复用 :新增和编辑使用同一个弹窗组件,通过 editItem 是否为空区分模式。弹窗标题和确认按钮文案也随模式变化。
  • onConfirm 回调:将确认操作委托给父组件处理,符合"子组件负责 UI,父组件负责逻辑"的职责分离原则。
  • 本地状态 :弹窗内部的 titledescriptionprioritydueDate 等状态独立于主页面,在弹窗关闭时自动释放。

6.2 生命周期:aboutToAppear

typescript 复制代码
aboutToAppear(): void {
  if (this.editItem) {
    // 编辑模式:用已有数据填充表单
    this.title = this.editItem.title;
    this.description = this.editItem.description;
    this.priority = this.editItem.priority;
    this.dueDate = this.editItem.dueDate;
    // 解析已有日期到年份/月份/日期选择器
    if (this.dueDate) {
      const parts = this.dueDate.split('-');
      if (parts.length === 3) {
        this.pickYear = parseInt(parts[0], 10);
        this.pickMonth = parseInt(parts[1], 10);
        this.pickDay = parseInt(parts[2], 10);
      }
    }
  }
  // 新增模式保持默认值
}

aboutToAppear(类似 React 的 componentDidMount)中初始化表单数据,如果是编辑模式就将已有数据填入各字段,如果是新增模式就保持默认状态。

6.3 弹窗 UI 布局

弹窗的 UI 从上到下依次是:

复制代码
┌──────────────────────────────────────┐
│           编辑待办 / 新增待办            │  ← 标题
│  ┌────────────────────────────────┐  │
│  │  请输入待办标题 *               │  │  ← 标题输入(TextInput)
│  └────────────────────────────────┘  │
│  ┌────────────────────────────────┐  │
│  │  描述(可选)                   │  │  ← 描述输入(TextArea)
│  └────────────────────────────────┘  │
│                                       │
│  优先级    ○ 高   ○ 中   ○ 低         │  ← 三选一 Radio
│                                       │
│  截止日期   2025年6月5日   📅   ✕     │  ← 日期选择
│  ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐       │
│  │  年 [2025] 月 [06] 日 [05] │       │  ← 日期选择器(展开后)
│  └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘       │
│                                       │
│  ┌──────────┐  ┌──────────┐          │
│  │   取消    │  │  保存/添加 │          │  ← 底部按钮
│  └──────────┘  └──────────┘          │
└──────────────────────────────────────┘
标题输入
typescript 复制代码
TextInput({ placeholder: '请输入待办标题 *', text: this.title })
  .height(44)
  .fontSize(15)
  .placeholderColor('#C7C7CC')
  .onChange((val: string) => {
    this.title = val;
    this.titleError = '';  // 用户重新输入时清除错误提示
  })
  .borderRadius(10)
  .backgroundColor('#F2F2F7')
  .padding({ left: 12, right: 12 })
  • 标题输入框使用 #F2F2F7 浅灰背景,营造 iOS 系统风格的输入框质感。
  • 10px 圆角柔和视觉。
  • onChange 中同时清除错误提示,让错误提示在用户修正时即时消失。
错误提示
typescript 复制代码
if (this.titleError) {
  Text(this.titleError)
    .fontSize(12)
    .fontColor('#FF3B30')
    .width('100%')
    .margin({ top: 4, bottom: 0 })
}

错误提示仅在 titleError 非空时显示,不影响正常状态下的布局。

优先级选择
typescript 复制代码
Row() {
  Text('优先级').fontSize(15).fontColor('#3A3A3C')
  Blank()
  ForEach([Priority.HIGH, Priority.MEDIUM, Priority.LOW], (p: Priority) => {
    Row() {
      Radio({ value: `${p}`, group: 'priority' })
        .checked(this.priority === p)
        .width(18).height(18)
        .onChange(() => { this.priority = p; })
      Text(getPriorityLabel(p))
        .fontSize(14)
        .fontColor(getPriorityColor(p))
        .margin({ left: 4 })
    }
    .margin({ left: p === Priority.HIGH ? 0 : 12 })
  })
}
  • 使用 Radio 单选组件,同一个 group 名称确保三选一互斥。
  • 文字颜色使用对应的优先级颜色,让视觉和语义统一。
  • ForEach 遍历枚举值,保证代码的简洁性和扩展性。
日期选择器

日期选择器是本应用中最复杂的 UI 组件之一。它采用"折叠式"设计------默认只显示日期文字和按钮,点击 📅 按钮后展开年/月/日输入框:

typescript 复制代码
// 日期显示行
Row() {
  Text('截止日期').fontSize(15).fontColor('#3A3A3C')
  Blank()
  Text(this.dueDate ? formatDate(this.dueDate) : '选择日期')
    .fontSize(14)
    .fontColor(this.dueDate ? '#1C1C1E' : '#C7C7CC')
    .margin({ right: 8 })
  Button() { Text('📅').fontSize(16) }
    .width(36).height(36).borderRadius(18)
    .backgroundColor('#F2F2F7')
    .onClick(() => { this.showDatePicker = !this.showDatePicker; })
  if (this.dueDate) {
    Button() { Text('✕').fontSize(14).fontColor('#8E8E93') }
      .width(36).height(36).borderRadius(18)
      .backgroundColor('#F2F2F7')
      .margin({ left: 6 })
      .onClick(() => { this.dueDate = ''; this.showDatePicker = false; })
  }
}

// 展开的日期选择器
if (this.showDatePicker) {
  Column() {
    Row() {
      // 年输入
      Column() {
        Text('年').fontSize(12).fontColor('#8E8E93')
        TextInput({ text: `${this.pickYear}` })
          .height(36).width(80)
          .onChange((val: string) => {
            const n = parseInt(val, 10);
            if (!isNaN(n) && n >= 2020 && n <= 2099) { this.pickYear = n; }
          })
      }
      // 月输入(类似)
      // 日输入(类似)
    }
    // 确认日期按钮
    Button('确定日期')
      .onClick(() => {
        this.dueDate = `${this.pickYear}-${String(this.pickMonth).padStart(2, '0')}-${String(this.pickDay).padStart(2, '0')}`;
        this.showDatePicker = false;
      })
  }
}

设计细节:

  1. 折叠式设计:日期选择器默认收起,不占用弹窗空间。只有用户点击 📅 按钮时才展开,这种渐进式披露(Progressive Disclosure)的设计模式让界面保持简洁。
  2. 清除按钮:当已有日期时,显示 ✕ 按钮一键清除日期,方便用户取消截止日期设置。
  3. 输入校验:年份限制在 2020-2099 年之间,月份限制在 1-12,日期根据年月动态计算最大值(支持闰年)。
底部操作按钮
typescript 复制代码
Row() {
  Button('取消')
    .height(44).borderRadius(22)
    .backgroundColor('#F2F2F7').fontColor('#007AFF')
    .layoutWeight(1)
    .onClick(() => { this.controller.close(); })

  Blank().width(16)

  Button(this.editItem ? '保存' : '添加')
    .height(44).borderRadius(22)
    .backgroundColor('#007AFF').fontColor('#FFFFFF')
    .layoutWeight(1)
    .onClick(() => {
      if (!this.title.trim()) {
        this.titleError = '标题不能为空';
        return;  // 阻止关闭弹窗
      }
      this.onConfirm(this.title.trim(), this.description.trim(), this.priority, this.dueDate);
      this.controller.close();
    })
}
  • layoutWeight(1):两个按钮等宽分布,视觉平衡。
  • 标题校验 :点击确认时先检查标题是否为空,为空则显示错误提示并 阻止弹窗关闭,用户修改后再次点击才能提交。
  • 文字联动:确认按钮的文字在新增和编辑模式下自动切换为"添加"和"保存"。

七、主页面 TodoList 的实现

TodoList 是整个应用的核心组件,包含数据管理、生命周期、UI 构建等全部逻辑。

7.1 组件状态

typescript 复制代码
@Entry
@Component
struct TodoList {
  @State todoList: TodoItem[] = [];           // 全部待办数据
  @State currentFilter: FilterType = FilterType.ALL;  // 当前筛选
  @State isLoading: boolean = true;           // 加载状态

  private addDialogController: CustomDialogController | null = null;
  private editDialogController: CustomDialogController | null = null;
  private reminderTimer: number = -1;
  private remindedToday: Set<number> = new Set();
}
  • @State todoList :驱动 UI 渲染的核心数据。每当数据发生变化(增、删、改、切换状态),loadData() 重新从数据库获取最新数据,@State 自动触发 UI 重新渲染。
  • @State currentFilter:当前选中的筛选条件,切换时自动刷新列表展示。
  • @State isLoading:用于控制加载中状态的显示。
  • remindedToday:Set 集合存储已提醒过的待办 ID,避免同一条待办重复提醒。

7.2 生命周期管理

typescript 复制代码
aboutToAppear(): void {
  this.initDatabase();
}

aboutToDisappear(): void {
  this.clearReminderTimer();
}

private async initDatabase(): Promise<void> {
  try {
    const context = getContext(this);
    await todoDB.init(context);
    await this.loadData();
    this.startReminderTimer();
  } catch (err) {
    console.error(`[TodoList] 数据库初始化失败: ${JSON.stringify(err)}`);
    promptAction.showToast({ message: '数据库初始化失败', duration: 2000 });
  } finally {
    this.isLoading = false;
  }
}

private async loadData(): Promise<void> {
  try {
    this.todoList = await todoDB.queryAll();
  } catch (err) {
    console.error(`[TodoList] 数据加载失败: ${JSON.stringify(err)}`);
    promptAction.showToast({ message: '数据加载失败', duration: 2000 });
  }
}

生命周期流程:

复制代码
aboutToAppear
    ↓
initDatabase ──→ 初始化 SQLite
    ↓
loadData ──→ 查询全部待办 → 更新 @State todoList
    ↓
startReminderTimer ──→ 启动定时提醒
    ↓
isLoading = false ──→ 渲染列表

这种"先加载数据,再渲染 UI"的时序设计,避免了白屏或空数据闪烁的问题。

7.3 定时提醒系统

定时提醒是本应用的一个亮眼功能,它可以在后台周期性检查是否有今天截止但未完成的待办,并通过 Toast 提醒用户。

typescript 复制代码
private startReminderTimer(): void {
  // 每 30 秒检查一次
  this.reminderTimer = setInterval(() => {
    this.checkDueReminder();
  }, 30000);
  // 启动后 1 秒立即检查一次,避免等待
  setTimeout(() => { this.checkDueReminder(); }, 1000);
}

private clearReminderTimer(): void {
  if (this.reminderTimer !== -1) {
    clearInterval(this.reminderTimer);
    this.reminderTimer = -1;
  }
}

private async checkDueReminder(): Promise<void> {
  try {
    const items = await todoDB.queryDueTodayActive();
    for (const item of items) {
      if (!this.remindedToday.has(item.id)) {
        this.remindedToday.add(item.id);
        // 延迟 500ms 弹出,避免阻塞 UI
        setTimeout(() => {
          promptAction.showToast({
            message: `⏰ 今天截止: ${item.title}`,
            duration: 3000,
          });
        }, 500);
      }
    }
  } catch (err) {
    console.error(`[TodoList] 提醒检查失败: ${JSON.stringify(err)}`);
  }
}

设计要点:

  1. 间隔 30 秒:频率适中,既不会太频繁消耗性能,也不会太久错过提醒。
  2. 立即首次检查:启动后 1 秒立即检查一次,让用户打开应用就能收到当天提醒,无需等待 30 秒。
  3. 去重机制remindedToday Set 确保同一条待办只提醒一次,避免重复打扰用户。
  4. 非阻断式:查询操作是异步的,延迟 500ms 弹出 Toast 也是异步的,不会阻塞主线程。
  5. 静默容错:提醒检查失败时只在控制台打印错误,不弹 Toast 干扰用户。

7.4 筛选逻辑

typescript 复制代码
getFilteredList(): TodoItem[] {
  if (this.currentFilter === FilterType.ALL) {
    return this.todoList;
  }
  const target = this.currentFilter === FilterType.COMPLETED ? 1 : 0;
  return this.todoList.filter(item => item.isCompleted === target);
}

getTotalCount(): number {
  return this.todoList.length;
}

getActiveCount(): number {
  return this.todoList.filter(item => item.isCompleted === 0).length;
}

getCompletedCount(): number {
  return this.todoList.filter(item => item.isCompleted === 1).length;
}

筛选和统计逻辑都基于 @State todoList 进行计算,ArkTS 的响应式系统会确保这些计算属性在数据变化时自动重新求值。

7.5 CRUD 操作

新增待办
typescript 复制代码
private async addTodo(title: string, description: string, priority: Priority, dueDate: string): Promise<void> {
  try {
    const data: InsertTodoItem = {
      title, description, priority, dueDate, isCompleted: 0,
    };
    await todoDB.insert(data);
    await this.loadData();  // 重新加载列表
    promptAction.showToast({ message: '✅ 添加成功', duration: 1500 });
  } catch (err) {
    promptAction.showToast({ message: `添加失败: ${(err as Error).message}`, duration: 2000 });
  }
}
编辑待办
typescript 复制代码
private async editTodo(item: TodoItem): Promise<void> {
  try {
    await todoDB.update(item);
    await this.loadData();
    promptAction.showToast({ message: '✅ 更新成功', duration: 1500 });
  } catch (err) {
    promptAction.showToast({ message: `更新失败: ${(err as Error).message}`, duration: 2000 });
  }
}
切换完成状态
typescript 复制代码
private async toggleTodo(id: number, currentStatus: number): Promise<void> {
  try {
    await todoDB.toggleCompleted(id, currentStatus);
    await this.loadData();
  } catch (err) {
    promptAction.showToast({ message: '状态切换失败', duration: 2000 });
  }
}
删除待办(带确认)
typescript 复制代码
private async deleteTodo(id: number): Promise<void> {
  try {
    await todoDB.delete(id);
    this.remindedToday.delete(id);  // 清除提醒记录
    await this.loadData();
    promptAction.showToast({ message: '🗑️ 已删除', duration: 1500 });
  } catch (err) {
    promptAction.showToast({ message: '删除失败', duration: 2000 });
  }
}

confirmDelete(item: TodoItem): void {
  AlertDialog.show({
    title: '删除待办',
    message: `确定要删除「${item.title}」吗?`,
    autoCancel: true,
    primaryButton: { value: '取消', action: () => {} },
    secondaryButton: {
      value: '删除',
      fontColor: '#FF3B30',
      action: () => { this.deleteTodo(item.id); },
    },
  });
}

删除确认 :使用 AlertDialog.show 弹出系统级确认框。"删除"按钮使用红色(#FF3B30)传达危险操作的语义,让用户在做删除操作时多一次确认机会,避免误删。

7.6 UI 构建

整体布局
typescript 复制代码
build() {
  Column() {
    this.buildHeader()    // 顶部导航栏
    this.buildStatsBar()  // 统计栏
    this.buildFilterBar() // 筛选标签
    if (this.isLoading) {
      LoadingProgress()   // 加载中
    } else if (this.getFilteredList().length === 0) {
      this.buildEmptyState() // 空状态
    } else {
      this.buildTodoList()   // 待办列表
    }
    Blank().height(20)    // 底部安全区
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#F2F2F7')
}

页面结构分为五个区域,从上到下依次是:

复制代码
┌──────────────────────────────────┐
│  📋 我的待办          + 添加     │  ← 顶部导航栏(buildHeader)
├──────────────────────────────────┤
│  📊 总计    ⏳ 待完成    ✅ 已完成  │  ← 统计栏(buildStatsBar)
├──────────────────────────────────┤
│  [全部] [待完成] [已完成]         │  ← 筛选标签(buildFilterBar)
├──────────────────────────────────┤
│  ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐  │
│  │  待办卡片 1                │  │
│  │  待办卡片 2                │  │  ← 待办列表(buildTodoList)
│  │  待办卡片 3                │  │     或 空状态(buildEmptyState)
│  └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘  │
├──────────────────────────────────┤
│                                  │  ← 底部安全区
└──────────────────────────────────┘
顶部导航栏
typescript 复制代码
@Builder
buildHeader() {
  Row() {
    Text('📋 我的待办')
      .fontSize(22).fontWeight(FontWeight.Bold).fontColor('#1C1C1E')
    Blank()
    Button() {
      Text('+ 添加')
        .fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Medium)
    }
    .height(36).borderRadius(18).backgroundColor('#007AFF')
    .padding({ left: 16, right: 16 })
    .onClick(() => { this.openAddDialog(); })
  }
  .width('100%')
  .padding({ left: 20, right: 20, top: 56, bottom: 12 })
  .backgroundColor('#FFFFFF')
}
  • 顶部 padding top: 56:为状态栏让出空间(类似 iOS 的 Safe Area)。
  • iOS 风格导航栏:左侧标题,右侧操作按钮,符合移动端用户的操作习惯。
  • 圆角按钮:36px 高度 + 18px borderRadius = 胶囊按钮,风格现代。
统计栏
typescript 复制代码
@Builder
buildStatsBar() {
  Row() {
    this.buildStatItem('📊 总计', this.getTotalCount(), '#007AFF')
    Blank()
    this.buildStatItem('⏳ 待完成', this.getActiveCount(), '#FF9500')
    Blank()
    this.buildStatItem('✅ 已完成', this.getCompletedCount(), '#34C759')
  }
  .width('100%')
  .padding({ left: 20, right: 20, top: 12, bottom: 12 })
  .backgroundColor('#FFFFFF')
  .margin({ bottom: 8 })
}

@Builder
buildStatItem(label: string, count: number, color: ResourceColor) {
  Column() {
    Text(`${count}`)
      .fontSize(24).fontWeight(FontWeight.Bold).fontColor(color)
    Text(label)
      .fontSize(12).fontColor('#8E8E93').margin({ top: 2 })
  }
  .alignItems(HorizontalAlign.Center)
}

三个统计块等宽分布,使用 Blank() 在 Row 中均分空间。每个统计块的数字采用不同的颜色:

统计项 颜色 色值 含义
总计 蓝色 #007AFF 中性、信息
待完成 橙色 #FF9500 警告、需要注意
已完成 绿色 #34C759 完成、正向
筛选标签栏
typescript 复制代码
@Builder
buildFilterBar() {
  Row() {
    ForEach([FilterType.ALL, FilterType.ACTIVE, FilterType.COMPLETED], (filter: FilterType) => {
      Button() {
        Text(getFilterLabel(filter))
          .fontSize(14)
          .fontWeight(this.currentFilter === filter ? FontWeight.Medium : FontWeight.Regular)
          .fontColor(this.currentFilter === filter ? '#FFFFFF' : '#3A3A3C')
      }
      .height(32).borderRadius(16)
      .backgroundColor(this.currentFilter === filter ? '#007AFF' : '#E5E5EA')
      .padding({ left: 16, right: 16 }).margin({ right: 8 })
      .onClick(() => { this.currentFilter = filter; })
    })
  }
  .width('100%')
  .padding({ left: 20, right: 20, bottom: 8 })
}
  • 胶囊式标签:32px 高度 + 16px borderRadius,风格统一。
  • 选中态高亮:选中时蓝色背景白色文字,未选中时浅灰色背景深色文字,对比清晰。
  • 点击切换筛选条件 :仅修改 currentFilter 状态变量,不请求数据库,列表展示由 getFilteredList() 实时计算。
空状态
typescript 复制代码
@Builder
buildEmptyState() {
  Column() {
    Blank().layoutWeight(1)
    Text('📝').fontSize(64)
    Text('暂无待办事项').fontSize(16).fontColor('#8E8E93').margin({ top: 12 })
    Text('点击右上角「添加」按钮创建第一条待办')
      .fontSize(13).fontColor('#C7C7CC').margin({ top: 6 })
    Blank().layoutWeight(1)
  }
  .width('100%').layoutWeight(1)
}
  • 使用 Blank().layoutWeight(1) 将内容垂直居中。
  • 友好的文案引导用户操作,而不是让用户面对白屏不知所措。
  • 三个层级:Emoji → 主文案 → 引导文案,视觉重点明确。
待办列表
typescript 复制代码
@Builder
buildTodoList() {
  Scroll() {
    Column() {
      ForEach(this.getFilteredList(), (item: TodoItem) => {
        this.buildTodoCard(item)
        Blank().height(10)
      })
      Blank().height(40)  // 底部留白,便于最后一项滚动查看
    }
    .width('100%')
    .padding({ left: 16, right: 16 })
  }
  .layoutWeight(1)
  .width('100%')
}

外层 Scroll 包裹 Column,当待办项超出屏幕高度时用户可滚动查看。底部 40px 留白确保最后一项不会被底部工具栏遮挡。

7.7 待办卡片设计

每个待办事项都以卡片形式展示,这是整个应用 UI 设计的核心。

typescript 复制代码
@Builder
buildTodoCard(item: TodoItem) {
  Column() {
    Row() {
      // ─── 左侧:Checkbox ───
      Checkbox()
        .select(item.isCompleted === 1)
        .width(22).height(22)
        .shape(CheckBoxShape.ROUNDED_SQUARE)
        .selectedColor('#34C759')
        .onChange((val: boolean) => {
          this.toggleTodo(item.id, item.isCompleted);
        })

      Blank().width(12)

      // ─── 中间:标题 + 描述 + 标签 ───
      Column() {
        // 标题
        Text(item.title)
          .fontSize(16).fontWeight(FontWeight.Medium)
          .fontColor(item.isCompleted === 1 ? '#C7C7CC' : '#1C1C1E')
          .decoration({
            type: item.isCompleted === 1 ? TextDecorationType.LineThrough : TextDecorationType.None
          })
          .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })

        // 描述(可选)
        if (item.description) {
          Text(item.description)
            .fontSize(13).fontColor('#8E8E93')
            .margin({ top: 4 })
            .maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
        }

        Blank().height(6)

        // 标签行:优先级 + 截止日期 + 逾期标记
        Row() {
          // 优先级标签
          Text(getPriorityLabel(item.priority))
            .fontSize(11).fontColor('#FFFFFF')
            .backgroundColor(getPriorityColor(item.priority))
            .borderRadius(4)
            .padding({ left: 6, right: 6, top: 2, bottom: 2 })

          Blank().width(8)

          // 截止日期
          if (item.dueDate) {
            Text(formatDate(item.dueDate))
              .fontSize(12)
              .fontColor(
                isOverdue(item.dueDate) && item.isCompleted === 0 ? '#FF3B30'
                : isDueToday(item.dueDate) && item.isCompleted === 0 ? '#FF9500'
                : '#8E8E93'
              )
          }

          // 逾期标记
          if (isOverdue(item.dueDate) && item.isCompleted === 0) {
            Text('⚠️ 已逾期').fontSize(11).fontColor('#FF3B30').margin({ left: 6 })
          }

          // 今日截止标记
          if (isDueToday(item.dueDate) && item.isCompleted === 0) {
            Text('🔔 今天截止').fontSize(11).fontColor('#FF9500').margin({ left: 6 })
          }
        }
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)

      // ─── 右侧:编辑 + 删除按钮 ───
      Column() {
        Button() { Text('✏️').fontSize(14) }
          .width(32).height(32).borderRadius(16).backgroundColor('#F2F2F7')
          .onClick(() => { this.openEditDialog(item); })

        Blank().height(6)

        Button() { Text('🗑️').fontSize(14) }
          .width(32).height(32).borderRadius(16).backgroundColor('#FFF0F0')
          .onClick(() => { this.confirmDelete(item); })
      }
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%')
    .alignItems(VerticalAlign.Top)
    .padding({ left: 14, right: 14, top: 14, bottom: 14 })
  }
  .width('100%')
  .backgroundColor('#FFFFFF')
  .borderRadius(14)
  .shadow({ radius: 6, color: 'rgba(0,0,0,0.04)', offsetY: 2 })
}
卡片布局解析

每张卡片从左到右分为三个区域:

复制代码
┌────────────────────────────────────────────────────┐
│  ┌────┐  ┌────────────────────┐  ┌────┐            │
│  │ ✅  │  │  购买生活用品       │  │ ✏️ │            │
│  │     │  │  去超市买牛奶和面包  │  │ 🗑️ │            │
│  │     │  │  中  2025年6月5日   │  └────┘            │
│  └────┘  └────────────────────┘                     │
└────────────────────────────────────────────────────┘

左侧 Checkbox

  • 使用 Checkbox 组件,采用 ROUNDED_SQUARE 样式(圆角方形),比纯圆形更现代。
  • 选中色为 #34C759(iOS 绿色),与完成态的语义一致。
  • 完成状态通过 .select() 控制,点击通过 onChange 回调处理。

中间内容区

  • 标题 :已完成项使用灰色(#C7C7CC)加删除线(LineThrough),视觉上明确标记"已划掉"。
  • 描述:可选,最多显示 2 行,超出显示省略号。
  • 标签行:包含优先级标签、截止日期、逾期/今日截止标记。

右侧操作区

  • 编辑按钮:浅灰背景的 ✏️ 图标,位置固定,方便用户快速定位。
  • 删除按钮 :浅红背景(#FFF0F0)的 🗑️ 图标,红色背景暗示危险操作。
卡片样式
typescript 复制代码
.backgroundColor('#FFFFFF')
.borderRadius(14)
.shadow({ radius: 6, color: 'rgba(0,0,0,0.04)', offsetY: 2 })
  • 白色背景 + 14px 圆角 + 柔和阴影,是典型的 iOS 卡片风格。
  • 阴影参数 color: 'rgba(0,0,0,0.04)' 透明度极低,营造"悬浮但不刺眼"的视觉效果。
  • 卡片与卡片之间使用 10px 间距(在 ForEach 循环中插入 Blank().height(10))。
日期颜色语义

截止日期的颜色根据剩余时间动态变化:

状态 颜色 色值 文案
正常(还有时间) 灰色 #8E8E93 2025年6月5日
今天截止 橙色 #FF9500 🔔 今天截止
已逾期 红色 #FF3B30 ⚠️ 已逾期

这种颜色即信息的设计让用户一眼就能识别待办的紧急程度,无需仔细阅读日期数据。

7.8 对话框打开控制

typescript 复制代码
openAddDialog(): void {
  this.addDialogController = new CustomDialogController({
    builder: TodoEditDialog({
      editItem: undefined,  // 新增模式
      onConfirm: (title, description, priority, dueDate) => {
        this.addTodo(title, description, priority, dueDate);
      },
    }),
    autoCancel: true,    // 点击弹窗外区域自动关闭
    customStyle: true,   // 自定义样式
    cornerRadius: 20,
    width: '90%',
  });
  this.addDialogController.open();
}

openEditDialog(item: TodoItem): void {
  this.editDialogController = new CustomDialogController({
    builder: TodoEditDialog({
      editItem: item,  // 编辑模式,传入已有数据
      onConfirm: (title, description, priority, dueDate) => {
        this.editTodo({
          id: item.id, title, description, priority, dueDate,
          isCompleted: item.isCompleted,
          createdTime: item.createdTime,
        });
      },
    }),
    autoCancel: true,
    customStyle: true,
    cornerRadius: 20,
    width: '90%',
  });
  this.editDialogController.open();
}

每次打开弹窗时都创建一个新的 CustomDialogController 实例,这样可以确保每次打开都是"干净"的状态。


八、页面路由配置

8.1 main_pages.json

json 复制代码
{
  "src": [
    "pages/TodoList"
  ]
}

由于 EntryAbility 直接加载 TodoList 页面,路由表中只需注册这一个页面即可。

8.2 Index.ets(引导页)

虽然入口直接跳过了 Index 页面,但它依然保留在项目中作为独立的引导入口:

typescript 复制代码
@Entry
@Component
struct Index {
  build() {
    Column() {
      Text('📋 待办清单应用')
        .fontSize(28).fontWeight(FontWeight.Bold).fontColor('#1a1a2e')
      Text('管理您的日常任务')
        .fontSize(14).fontColor('#666666')
        .margin({ top: 8, bottom: 40 })
      Button('进入待办清单')
        .type(ButtonType.Capsule)
        .width(220).height(48)
        .backgroundColor('#007aff')
        .fontColor(Color.White).fontSize(16).fontWeight(FontWeight.Medium)
        .onClick(() => {
          router.pushUrl({ url: 'pages/TodoList' });
        })
    }
    .width('100%').height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .backgroundColor('#f5f5f5')
  }
}

这个页面作为一个简洁的启动屏,展示了应用名称和一句描述,用户点击按钮后跳转到待办清单页面。


九、UI 设计深度解析

9.1 整体设计风格

本应用采用了 iOS 风格的 UI 设计语言,主要体现在以下方面:

设计元素 应用中的体现
卡片式布局 每个待办事项都是一个白色圆角卡片
大面积留白 列表项之间 10px 间距,卡片内 14px padding
柔和阴影 卡片的 shadow 透明度仅 4%,非常克制
系统字体 使用系统默认字体,无衬线
蓝色主色调 #007AFF 是 iOS 系统蓝,用于导航和选中态
底部安全区 20px 底部留白适配全面屏

9.2 颜色体系

复制代码
主色(导航/按钮/选中):#007AFF   ------ 可靠、专业
成功(完成状态):     #34C759   ------ 完成、正向
警告(待完成):       #FF9500   ------ 需要注意
危险(删除/逾期):    #FF3B30   ------ 紧急、不可逆
文字主色:            #1C1C1E   ------ 高对比度
文字次要:            #8E8E93   ------ 说明文字
文字禁用:            #C7C7CC   ------ 已完成的标题
页面背景:            #F2F2F7   ------ 浅灰,突出卡片
卡片背景:            #FFFFFF   ------ 纯白

这套配色完全遵循了 iOS Human Interface Guidelines 的颜色体系,确保了系统级的一致性和良好的可读性。

9.3 圆角系统

复制代码
弹窗外圆角:  20px  (最外层)
卡片圆角:    14px  (待办卡片)
输入框圆角:  10px  (弹窗中的输入框)
按钮圆角:    18px/16px/22px  (胶囊风格)
复选框圆角:  ROUNDED_SQUARE  (圆角方形)

圆角值的大小与组件的"层次"相关:越靠外的组件圆角越大,形成嵌套的视觉节奏。

9.4 字体层级

元素 字号 字重 颜色
导航标题 22px Bold #1C1C1E
统计数字 24px Bold 语义色
待办标题 16px Medium 完成态 #C7C7CC / 未完成 #1C1C1E
待办描述 13px Regular #8E8E93
标签文字 11px Regular #FFFFFF
筛选文字 14px Medium/Regular 选中 #FFFFFF / 未选 #3A3A3C
统计标签 12px Regular #8E8E93
空状态提示 16px/13px Regular #8E8E93 / #C7C7CC

字号的递进关系(11 → 12 → 13 → 14 → 16 → 22 → 24)形成了清晰的视觉层级,用户一眼就能区分标题、正文和辅助信息。


十、状态管理与数据流

10.1 单向数据流

ArkTS 采用单向数据流的设计模式,数据的流动方向是:

复制代码
用户操作 → 事件回调 → 状态变量更新 → UI 重新渲染

具体到本应用:

复制代码
用户点击 Checkbox
    ↓
onChange 回调 → toggleTodo(id, status)
    ↓
todoDB.toggleCompleted(id, status) → SQLite UPDATE
    ↓
loadData() → todoDB.queryAll() → @State todoList 更新
    ↓
UI 自动重新渲染(标题颜色变化、完成态切换)

10.2 @State 驱动的响应式更新

ArkTS 中 @State 装饰器的核心特性:

  1. 自动追踪 :当 @State 变量的值发生变化时,框架自动记录哪些组件依赖了该变量。
  2. 最小化更新:只重新渲染依赖了发生变化的状态变量的组件,而非整个页面。
  3. 同步更新:状态变量的修改和 UI 的重新渲染在同一个帧内完成,不会出现"闪烁"。

在本应用中,@State todoList: TodoItem[] 是唯一的"数据源",所有 UI 展示(列表、统计、筛选)都基于这个数组进行计算。

10.3 数据流示意图

复制代码
                    ┌─────────────┐
                    │  TodoList   │
                    │  @State     │
                    │  todoList   │ ←──── 数据源
                    └──────┬──────┘
          ┌────────────────┼────────────────┐
          │                │                 │
          ▼                ▼                 ▼
    ┌──────────┐   ┌────────────┐   ┌──────────────┐
    │ 列表展示  │   │ 统计栏     │   │ 筛选条件     │
    │ (Filtered)│   │ (Counts)   │   │ (currentFilter)
    └──────────┘   └────────────┘   └──────────────┘
          │
          ▼
    ┌──────────┐
    │ 数据库    │
    │ SQLite   │
    └──────────┘

10.4 数据持久化策略

本应用的数据持久化采用"读-写-读"模式:

  1. :应用启动时 aboutToAppearinitDatabaseloadData → 全量读取到内存。
  2. :每次增/删/改操作后,立即重新 loadData 刷新内存数据。
  3. 内存即界面@State todoList 持有内存中的全量数据,UI 展示直接基于内存数据计算。

这种策略的优点:

  • 实现简单:无需设计复杂的状态管理方案。
  • 数据一致:每次操作后立即从数据库刷新,保证 UI 与数据库同步。
  • 适合小数据量:待办清单通常不会超过几百条记录,全量加载性能完全 OK。

但当数据量极大时(如数万条),可以考虑引入分页加载和增量更新策略。


十一、异常处理与用户体验

11.1 多层次的异常处理

本应用在三个层面做了异常处理:

数据库层
typescript 复制代码
async insert(item: InsertTodoItem): Promise<number> {
  if (!item.title.trim()) {
    throw new Error('标题不能为空');
  }
  // ...
}

数据库层校验数据合法性,抛出业务语义的错误。

业务逻辑层
typescript 复制代码
private async addTodo(...): Promise<void> {
  try {
    await todoDB.insert(data);
    await this.loadData();
    promptAction.showToast({ message: '✅ 添加成功', duration: 1500 });
  } catch (err) {
    promptAction.showToast({ message: `添加失败: ${(err as Error).message}`, duration: 2000 });
  }
}

业务逻辑层捕获数据库层的异常,将技术错误转化为用户可读的 Toast 提示。

UI 层
typescript 复制代码
onClick(() => {
  if (!this.title.trim()) {
    this.titleError = '标题不能为空';
    return;
  }
  this.onConfirm(...);
  this.controller.close();
})

UI 层面进行输入校验,在提交前拦截空标题,通过红色错误文字即时反馈。

11.2 用户反馈设计

操作 反馈方式 反馈内容
新增成功 Toast ✅ 添加成功
编辑成功 Toast ✅ 更新成功
删除成功 Toast 🗑️ 已删除
删除操作 AlertDialog 确认框 确定要删除「xxx」吗?
空标题提交 红色文字提示 标题不能为空
数据库初始化失败 Toast 数据库初始化失败
数据加载失败 Toast 数据加载失败
今天截止提醒 Toast ⏰ 今天截止: xxx

所有的用户操作都有明确的反馈,无论是成功、失败还是需要用户确认,都不会让用户陷入"操作没反应"的困惑中。

11.3 加载与空状态

  • 加载中 :首次进入应用时显示 LoadingProgress 旋转加载指示器,告知用户"数据正在加载"。
  • 空数据:当没有任何待办事项时,显示友好的空状态界面,引导用户创建第一条待办。
  • 加载完成:加载完成后才展示列表,避免数据闪烁。

十二、性能优化

12.1 数据库查询优化

  • 索引利用id 字段是主键,具有自动索引。isCompleteddueDate 虽然没有显式创建索引,但在数据量不大的场景下性能足够。
  • 分次查询 :提醒检查使用专门的 queryDueTodayActive 方法(带 isCompleted=0 AND dueDate=today 过滤条件),而非查询全部数据后再在内存中过滤。

12.2 UI 渲染优化

  • ForEach 列表渲染 :ArkTS 的 ForEach 组件会对列表项进行键值追踪,当数据变化时只重新渲染发生变化的项,而非整个列表。
  • 条件渲染 :使用 if/else 条件渲染加载状态、空状态和列表,避免同时渲染不需要的组件。
  • maxLines + Ellipsis:标题和描述设置了最大行数和省略号,防止长文本撑破卡片布局。

12.3 内存管理

  • 定时器清理 :在 aboutToDisappear 生命周期中清理 setInterval,避免组件销毁后定时器仍然运行。
  • 弹窗控制器释放:每次关闭弹窗后,控制器自动释放相关资源。
  • remindedToday 清理 :删除待办时同步清除 remindedToday 中的记录,避免内存泄漏。

十三、常见问题与排错

13.1 数据库初始化失败

现象:应用启动后显示"数据库初始化失败"Toast。

可能原因

  1. Context 获取不正确:getContext(this) 必须在组件生命周期内调用。
  2. 存储权限不足:检查 module.json5 中是否配置了存储权限。
  3. 数据库文件损坏:尝试卸载应用后重新安装。

13.2 数据操作后列表不刷新

现象:新增或编辑待办后,列表没有变化。

检查步骤

  1. 确认 loadData()addTodo / editTodo / deleteTodo 方法中被调用。
  2. 确认 todoList 变量使用了 @State 装饰器。
  3. 检查控制台是否有错误日志。
typescript 复制代码
private async addTodo(...): Promise<void> {
  try {
    await todoDB.insert(data);
    await this.loadData();  // ← 关键:重新加载数据
    promptAction.showToast({ message: '✅ 添加成功', duration: 1500 });
  } catch (err) { ... }
}

13.3 提醒没有弹出

现象:设置了今天截止的待办,但没有收到 Toast 提醒。

检查

  1. 确认待办的 dueDate 格式正确(yyyy-MM-dd,如 2025-06-05)。
  2. 确认待办的 isCompleted 为 0(未完成)。
  3. 确认当前设备的时间设置正确。
  4. 检查控制台是否有 [TodoList] 提醒检查失败 的日志。

13.4 日期选择器验证不通过

现象:输入年份或月份后,确定日期按钮无反应。

原因 :日期选择器的输入框 onChange 中有校验逻辑,超出范围的数字会被忽略。

typescript 复制代码
.onChange((val: string) => {
  const n = parseInt(val, 10);
  if (!isNaN(n) && n >= 2020 && n <= 2099) {
    this.pickYear = n;
  }
  // 如果输入不合法,值不会改变
})

13.5 弹窗样式异常

现象:弹窗显示为系统默认样式,没有圆角和自定义布局。

检查

  1. 确认 CustomDialogController 配置了 customStyle: true
  2. 确认弹窗组件使用了 @CustomDialog 装饰器。
  3. 检查弹窗组件的 build() 方法中是否使用了正确的布局容器。

十四、扩展与改进方向

14.1 功能扩展

本应用已经实现了一个待办清单的核心功能,未来可以从以下方向继续扩展:

扩展方向 实现思路 难度
分类/标签 增加 category 字段,支持按类别筛选 ⭐⭐
提醒通知 使用 @ohos.backgroundTaskManager 实现后台推送通知 ⭐⭐⭐
数据导出 将待办数据导出为 CSV 或 JSON 文件 ⭐⭐
主题切换 支持深色模式、自定义主题色 ⭐⭐
排序自定义 允许用户按创建时间、优先级、截止日期等自定义排序 ⭐⭐
搜索功能 在列表上方增加搜索栏,实时过滤 ⭐⭐
子任务 每个待办项下可以添加多个子任务 ⭐⭐⭐
重复任务 支持每日/每周/每月重复的周期性待办 ⭐⭐⭐
云同步 使用华为云或自定义后端实现多设备数据同步 ⭐⭐⭐⭐

14.2 代码架构优化

当前版本将所有代码集中在 TodoList.ets 文件中(1082 行),虽然在小项目中可以接受,但随着功能增加,建议进行模块拆分:

复制代码
ets/pages/
├── TodoList.ets               # 主页面(UI + 状态管理)
├── model/
│   ├── TodoItem.ts            # 数据模型接口
│   ├── Priority.ts            # 优先级枚举
│   └── FilterType.ts          # 筛选枚举
├── database/
│   └── TodoDatabase.ts        # 数据库操作类
├── component/
│   └── TodoEditDialog.ets     # 自定义弹窗组件
└── utils/
    └── dateUtils.ts           # 日期工具函数

14.3 性能优化建议

  • 虚拟列表 :当待办数量超过 100 条时,建议使用 LazyForEach 替代 ForEach 实现虚拟滚动。
  • 增量加载:首次只加载最近 50 条数据,用户滚动到底部时自动加载更多。
  • 防抖搜索:搜索功能中,使用防抖(debounce)减少频繁查询。
  • 数据库索引 :为 isCompleteddueDate 字段添加显式索引,加快查询速度。

十五、总结

15.1 知识点回顾

本文从零到一实现了一个完整的鸿蒙待办清单应用,覆盖了以下核心知识点:

知识点 详细内容
ArkTS 组件体系 @Entry, @Component, @CustomDialog, @Builder, build()
状态管理 @State 装饰器驱动的响应式 UI
SQLite 数据库 @ohos.data.relationalStore 的增删改查操作
数据模型设计 TodoItem / InsertTodoItem 接口分离
自定义弹窗 CustomDialog + 自定义样式 + 回调通信
表单校验 输入前校验 + 错误提示展示
列表渲染 ForEach + 卡片式布局
筛选功能 枚举 + 内存过滤
定时任务 setInterval + 生命周期管理
日期处理 格式化、比较、逾期/今日判断
交互反馈 Toast + AlertDialog 双层反馈
异常处理 try/catch + 用户友好的错误提示

15.2 设计哲学回顾

一个好的待办清单应用,应该做到以下几点:

  1. 启动即用:打开应用直接看到待办列表,零等待、零操作。
  2. 操作即得:新增、编辑、删除、切换状态都有即时反馈,不用等待网络请求。
  3. 一目了然:颜色、图标、文字共同传达信息,用户无需阅读细节就能判断紧急程度。
  4. 无情提醒:定时检查逾期和今日截止任务,不让用户错过重要事项。
  5. 轻松管理:筛选功能让用户在不同场景下只关注自己需要的待办。

15.3 结语

鸿蒙生态正处于快速发展的阶段,

相关推荐
Pocker_Spades_A1 小时前
[鸿蒙PC命令行移植适配]移植rust三方库peep到鸿蒙PC的完整实践
华为·rust·harmonyos
EterNity_TiMe_1 小时前
[鸿蒙PC命令行移植适配]移植rust三方库grex到鸿蒙PC的完整实践
华为·rust·harmonyos
EterNity_TiMe_2 小时前
[鸿蒙PC命令行移植适配]移植rust三方库lsd到鸿蒙PC的完整实践
华为·rust·harmonyos
IT大白鼠2 小时前
BGP协议概述:定义、机制与应用
网络·网络协议·华为
●VON2 小时前
AtomGit Flutter鸿蒙客户端:API客户端与网络层
flutter·华为·架构·跨平台·harmonyos·鸿蒙
不喝水就会渴2 小时前
HarmonyOS 动画实战:从「喵屿」看提醒与删除动效的三种实现
华为·交互·动画·harmonyos·鸿蒙
weixin_604236673 小时前
华为二层交换机 企业完整正式版配置
运维·服务器·华为·华为交换机命令
互联网散修3 小时前
鸿蒙实战:基于Navigation自定义转场动画 —— 一镜到底
华为·harmonyos·转场动画·一镜到底
禁默3 小时前
[鸿蒙PC命令行移植适配]移植rust三方库tealdeer到鸿蒙PC的完整实践
华为·rust·harmonyos