Skip to content

待办事项应用示例

这是一个完整的 HarmonyOS 待办事项应用,演示了状态管理、列表渲染、数据持久化等核心功能。

功能特性

  • 添加待办事项
  • 标记完成/未完成
  • 删除事项
  • 数据持久化(Preferences)
  • 列表动画

项目结构

todo-app/
├── entry/src/main/ets/
│   ├── entryability/
│   │   └── EntryAbility.ets
│   ├── pages/
│   │   └── Index.ets          # 主页面
│   ├── components/
│   │   ├── TodoItem.ets       # 待办项组件
│   │   └── AddTodo.ets        # 添加待办组件
│   └── model/
│       └── TodoModel.ets      # 数据模型
└── resources/

核心代码

1. 数据模型

typescript
// model/TodoModel.ets
import { preferences } from '@kit.ArkData'

@Observed
export class Todo {
  id: number
  title: string
  completed: boolean
  createdAt: number

  constructor(title: string) {
    this.id = Date.now()
    this.title = title
    this.completed = false
    this.createdAt = Date.now()
  }
}

export class TodoModel {
  private static pref: preferences.Preferences | null = null
  private static readonly KEY = 'todo_list'

  static async init(): Promise<void> {
    const context = getContext()
    TodoModel.pref = await preferences.getPreferences(context, 'todo_app')
  }

  static async loadTodos(): Promise<Todo[]> {
    if (!TodoModel.pref) return []
    
    const data = await TodoModel.pref.get(TodoModel.KEY, '[]') as string
    const arr = JSON.parse(data)
    return arr.map((item: object) => Object.assign(new Todo(''), item))
  }

  static async saveTodos(todos: Todo[]): Promise<void> {
    if (!TodoModel.pref) return
    
    await TodoModel.pref.put(TodoModel.KEY, JSON.stringify(todos))
    await TodoModel.pref.flush()
  }
}

2. 待办项组件

typescript
// components/TodoItem.ets
import { Todo } from '../model/TodoModel'

@Component
export struct TodoItemView {
  @ObjectLink todo: Todo
  onToggle: () => void = () => {}
  onDelete: () => void = () => {}

  build() {
    Row() {
      Row({ space: 12 }) {
        // 复选框
        Stack() {
          Circle()
            .width(24)
            .height(24)
            .stroke(this.todo.completed ? '#4CAF50' : '#CCCCCC')
            .strokeWidth(2)
            .fill(this.todo.completed ? '#4CAF50' : Color.Transparent)

          if (this.todo.completed) {
            Text('✓')
              .fontSize(14)
              .fontColor(Color.White)
          }
        }
        .width(24)
        .height(24)

        // 标题
        Text(this.todo.title)
          .fontSize(16)
          .fontColor(this.todo.completed ? '#999999' : '#333333')
          .decoration({
            type: this.todo.completed ? TextDecorationType.LineThrough : TextDecorationType.None
          })
          .layoutWeight(1)
      }
      .layoutWeight(1)

      // 删除按钮
      Button('删除')
        .fontSize(12)
        .fontColor('#FF4444')
        .backgroundColor(Color.Transparent)
        .onClick(() => this.onDelete())
    }
    .width('100%')
    .padding(16)
    .backgroundColor(Color.White)
    .borderRadius(8)
    .onClick(() => this.onToggle())
  }
}

3. 添加待办组件

typescript
// components/AddTodo.ets
@Component
export struct AddTodo {
  @State inputText: string = ''
  onAdd: (text: string) => void = () => {}

  build() {
    Row({ space: 12 }) {
      TextInput({ placeholder: '输入待办事项...', text: this.inputText })
        .placeholderColor('#999999')
        .height(48)
        .layoutWeight(1)
        .onChange((value) => {
          this.inputText = value
        })

      Button('添加')
        .width(80)
        .height(48)
        .backgroundColor('#007DFF')
        .enabled(this.inputText.length > 0)
        .onClick(() => {
          this.onAdd(this.inputText)
          this.inputText = ''
        })
    }
    .width('100%')
    .padding(16)
  }
}

4. 主页面

typescript
// pages/Index.ets
import { Todo, TodoModel } from '../model/TodoModel'
import { TodoItemView } from '../components/TodoItem'
import { AddTodo } from '../components/AddTodo'

@Entry
@Component
struct TodoPage {
  @State todos: Todo[] = []
  @State isLoading: boolean = true

  aboutToAppear() {
    this.loadTodos()
  }

  async loadTodos() {
    await TodoModel.init()
    this.todos = await TodoModel.loadTodos()
    this.isLoading = false
  }

  async saveTodos() {
    await TodoModel.saveTodos(this.todos)
  }

  addTodo(title: string) {
    const todo = new Todo(title)
    this.todos.unshift(todo)
    this.saveTodos()
  }

  toggleTodo(index: number) {
    this.todos[index].completed = !this.todos[index].completed
    this.saveTodos()
  }

  deleteTodo(index: number) {
    this.todos.splice(index, 1)
    this.saveTodos()
  }

  get completedCount(): number {
    return this.todos.filter(t => t.completed).length
  }

  build() {
    Column({ space: 16 }) {
      // 标题栏
      Row() {
        Text('待办事项')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)

        Text(`${this.completedCount}/${this.todos.length}`)
          .fontSize(14)
          .fontColor('#999999')
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      .padding(16)

      // 添加区域
      AddTodo({ onAdd: (text) => this.addTodo(text) })

      // 列表
      if (this.isLoading) {
        LoadingProgress()
          .width(40)
          .height(40)
        
        Text('加载中...')
          .fontSize(14)
          .fontColor('#999999')
      } else if (this.todos.length === 0) {
        Column({ space: 12 }) {
          Text('📝')
            .fontSize(48)
          
          Text('暂无待办事项')
            .fontSize(16)
            .fontColor('#999999')
          
          Text('点击上方添加按钮创建')
            .fontSize(14)
            .fontColor('#CCCCCC')
        }
        .layoutWeight(1)
        .justifyContent(FlexAlign.Center)
      } else {
        List({ space: 8 }) {
          ForEach(this.todos, (todo: Todo, index: number) => {
            ListItem() {
              TodoItemView({
                todo: todo,
                onToggle: () => this.toggleTodo(index),
                onDelete: () => this.deleteTodo(index)
              })
            }
            .transition({ type: TransitionType.All, opacity: 0 })
          }, (todo: Todo) => todo.id.toString())
        }
        .width('100%')
        .layoutWeight(1)
        .padding({ left: 16, right: 16 })
        .edgeEffect(EdgeEffect.Spring)
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
}

运行效果

  • 空状态显示提示信息
  • 添加事项后显示在列表顶部
  • 点击事项标记完成/未完成
  • 左滑或点击删除按钮删除
  • 数据自动保存,重启应用后恢复

学习要点

  1. @Observed + @ObjectLink:实现列表项的深度响应
  2. Preferences:轻量级数据持久化
  3. List + ForEach:高效列表渲染
  4. 组件拆分:AddTodo、TodoItemView 复用
  5. 状态提升:数据管理在页面层,通过回调传递操作