从零开发一个鸿蒙待办事项 App

📅 2025-05-07 · ⏱ 30 分钟 · 实战

通过一个完整的待办事项 App 实战项目,串联 ArkTS 语法、ArkUI 组件、状态管理和数据持久化等核心知识点。

一、数据模型

先定义待办事项的数据结构:

interface TodoItem {
  id: number
  title: string
  completed: boolean
  createdAt: number  // 时间戳
}

二、主页面结构

import { preferences } from "@kit.ArkData"

@Entry
@Component
struct TodoPage {
  @State todos: TodoItem[] = []
  @State newTitle: string = ""
  @State filter: "all" | "active" | "done" = "all"
  private nextId: number = 1

  aboutToAppear() {
    this.loadTodos()
  }

  build() {
    Column() {
      // 顶部标题
      Text("我的待办")
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .width("100%")
        .padding({ left: 20, top: 20, bottom: 12 })

      // 输入区域
      Row() {
        TextInput({ placeholder: "添加新任务...", text: this.newTitle })
          .layoutWeight(1)
          .height(44)
          .onChange((v: string) => this.newTitle = v)
          .onSubmit(() => this.addTodo())
        Button("添加")
          .width(60)
          .height(44)
          .margin({ left: 8 })
          .onClick(() => this.addTodo())
      }
      .width("100%")
      .padding({ left: 20, right: 20 })

      // 筛选标签
      Row({ space: 8 }) {
        this.FilterTab("全部", "all")
        this.FilterTab("未完成", "active")
        this.FilterTab("已完成", "done")
      }
      .margin({ top: 16, left: 20 })

      // 列表
      List({ space: 8 }) {
        ForEach(this.filteredTodos(), (item: TodoItem) => {
          ListItem() {
            this.TodoRow(item)
          }
        })
      }
      .layoutWeight(1)
      .width("100%")
      .padding(16)

      // 统计
      Text(`${this.todos.filter(t => !t.completed).length} 项待完成`)
        .fontSize(13)
        .fontColor("#999")
        .padding({ bottom: 16 })
    }
    .width("100%")
    .height("100%")
    .backgroundColor("#f5f5f5")
  }
}

三、子组件:单条待办

@Builder
TodoRow(item: TodoItem) {
  Row() {
    Checkbox()
      .select(item.completed)
      .onChange((val: boolean) => {
        this.toggleTodo(item.id)
      })
    Text(item.title)
      .fontSize(16)
      .decoration({ type: item.completed ? TextDecorationType.LineThrough : TextDecorationType.None })
      .fontColor(item.completed ? "#ccc" : "#333")
      .layoutWeight(1)
      .margin({ left: 12 })
    Button("删除")
      .fontSize(12)
      .height(28)
      .backgroundColor("#ff4d4f")
      .onClick(() => this.removeTodo(item.id))
  }
  .width("100%")
  .padding(16)
  .backgroundColor(Color.White)
  .borderRadius(8)
}

四、核心逻辑

// 添加待办
addTodo() {
  if (!this.newTitle.trim()) return
  this.todos.unshift({
    id: this.nextId++,
    title: this.newTitle.trim(),
    completed: false,
    createdAt: Date.now()
  })
  this.newTitle = ""
  this.saveTodos()
}

// 切换完成状态
toggleTodo(id: number) {
  const idx = this.todos.findIndex(t => t.id === id)
  if (idx >= 0) {
    this.todos[idx].completed = !this.todos[idx].completed
    this.saveTodos()
  }
}

// 删除
removeTodo(id: number) {
  this.todos = this.todos.filter(t => t.id !== id)
  this.saveTodos()
}

// 筛选
filteredTodos(): TodoItem[] {
  if (this.filter === "active") return this.todos.filter(t => !t.completed)
  if (this.filter === "done") return this.todos.filter(t => t.completed)
  return this.todos
}

// 持久化
async saveTodos() {
  const ctx = getContext(this)
  const prefs = await preferences.getPreferences(ctx, "todos")
  await prefs.put("data", JSON.stringify(this.todos))
  await prefs.put("nextId", this.nextId.toString())
  await prefs.flush()
}

async loadTodos() {
  const ctx = getContext(this)
  const prefs = await preferences.getPreferences(ctx, "todos")
  const data = await prefs.get("data", "[]") as string
  this.todos = JSON.parse(data)
  this.nextId = Number(await prefs.get("nextId", "1"))
}

五、小结

通过这个实战项目,你学会了: