11  第10章 综合实战:待办事项应用

🚀 第10章 综合实战

用 ArkTS 构建一个功能完整的 Todo 待办事项应用

11.1 项目概览

本章将把前9章所学知识综合运用,从零开始构建一个功能完整的 待办事项(Todo) 应用。

增删改查

添加、删除、完成待办事项

🔍

筛选过滤

全部/进行中/已完成三种视图

💾

状态管理

@State/@Link 响应式驱动 UI

🎨

自定义样式

动画过渡,美观界面

11.2 10.1 应用架构设计

📐 三层架构

🎨 UI层 TodoItem组件 · FilterBar组件 · InputBar组件 · StatBar组件
⬇ 数据绑定 / 事件回调
⚙️ 逻辑层 TodoViewModel · 过滤逻辑 · 统计计算 · 持久化调用
⬇ 读写
💿 数据层 TodoModel · AppStorage · LocalStorage · 接口定义

11.3 10.2 数据模型定义

📌 接口 & 类型定义
// model/TodoModel.ets
export interface TodoItem {
  id: string;
  text: string;
  done: boolean;
  createdAt: number;
}

export type FilterType = 'all' | 'active' | 'done';

// 生成唯一ID
export function genId(): string {
  return Date.now().toString(36) + Math.random().toString(36).slice(2);
}

11.4 10.3 ViewModel 实现

// viewmodel/TodoViewModel.ets
import { TodoItem, FilterType, genId } from '../model/TodoModel';

@Observed
export class TodoViewModel {
  @Track items: TodoItem[] = [];
  @Track filter: FilterType = 'all';

  // 添加待办
  add(text: string): void {
    if (!text.trim()) return;
    this.items.unshift({ id: genId(), text: text.trim(), done: false, createdAt: Date.now() });
  }

  // 切换完成状态
  toggle(id: string): void {
    const item = this.items.find(i => i.id === id);
    if (item) item.done = !item.done;
  }

  // 删除待办
  remove(id: string): void {
    this.items = this.items.filter(i => i.id !== id);
  }

  // 过滤列表
  get filtered(): TodoItem[] {
    return this.items.filter(i => {
      if (this.filter === 'active') return !i.done;
      if (this.filter === 'done')  return i.done;
      return true;
    });
  }

  get activeCount(): number { return this.items.filter(i => !i.done).length; }
  get doneCount():  number { return this.items.filter(i => i.done).length; }
}

11.5 10.4 子组件实现

11.5.1 10.4.1 输入栏组件

// components/InputBar.ets
@Component
export struct InputBar {
  @Link inputText: string;
  onAdd?: () => void;

  build() {
    Row() {
      TextInput({ placeholder: '输入待办事项…', text: this.inputText })
        .layoutWeight(1)
        .height(44)
        .borderRadius(8)
        .onChange(v => { this.inputText = v; })
        .onSubmit(() => { this.onAdd?.(); })

      Button('添加')
        .height(44)
        .backgroundColor('#C02020')
        .onClick(() => { this.onAdd?.(); })
    }
    .width('100%')
    .gap(8)
  }
}

11.5.2 10.4.2 待办项组件

// components/TodoItemComp.ets
@Component
export struct TodoItemComp {
  @ObjectLink item: TodoItem;
  onToggle?: (id: string) => void;
  onDelete?: (id: string) => void;

  build() {
    Row() {
      Checkbox()
        .select(this.item.done)
        .onChange(() => { this.onToggle?.(this.item.id); })

      Text(this.item.text)
        .layoutWeight(1)
        .decoration({ type: this.item.done ? TextDecorationType.LineThrough : TextDecorationType.None })
        .fontColor(this.item.done ? '#aaa' : '#333')

      Button('🗑', { type: ButtonType.Normal })
        .fontColor('#e57373')
        .backgroundColor(Color.Transparent)
        .onClick(() => { this.onDelete?.(this.item.id); })
    }
    .width('100%')
    .padding(12)
    .backgroundColor(Color.White)
    .borderRadius(8)
    .margin({ bottom: 4 })
    .transition(TransitionEffect.OPACITY)
  }
}

11.6 10.5 主页面组装

// pages/Index.ets
import { TodoViewModel } from '../viewmodel/TodoViewModel';
import { InputBar } from '../components/InputBar';
import { TodoItemComp } from '../components/TodoItemComp';

@Entry
@Component
struct Index {
  @State vm: TodoViewModel = new TodoViewModel();
  @State inputText: string = '';

  build() {
    Column() {
      // 标题栏
      Text('📋 我的待办')
        .fontSize(24).fontWeight(FontWeight.Bold)
        .fontColor('#C02020').margin({ bottom: 16 })

      // 输入栏
      InputBar({ inputText: $inputText, onAdd: () => {
        this.vm.add(this.inputText);
        this.inputText = '';
      }})

      // 过滤标签
      Row() {
        ForEach(['all', 'active', 'done'],
          (f: string) => Button(f === 'all' ? '全部' : f === 'active' ? '进行中' : '已完成')
            .backgroundColor(this.vm.filter === f ? '#C02020' : '#eee')
            .fontColor(this.vm.filter === f ? Color.White : '#555')
            .layoutWeight(1)
            .onClick(() => { this.vm.filter = f as any; })
        )
      }.width('100%').gap(8).margin({ top: 12, bottom: 8 })

      // 待办列表
      List({ space: 4 }) {
        ForEach(this.vm.filtered, (item: TodoItem) =>
          ListItem() {
            TodoItemComp({ item,
              onToggle: id => this.vm.toggle(id),
              onDelete: id => this.vm.remove(id)
            })
          }
        , item => item.id)
      }.layoutWeight(1)

      // 统计栏
      Row() {
        Text(`待完成 ${this.vm.activeCount} 项`).fontColor('#777')
        Text(`已完成 ${this.vm.doneCount} 项`).fontColor('#4caf50')
      }.justifyContent(FlexAlign.SpaceBetween).width('100%').margin({ top: 12 })
    }
    .padding(20)
    .backgroundColor('#f5f5f5')
    .height('100%')
  }
}

11.7 10.6 交互演示

📋 Todo App 演示

    待完成 0 项 已完成 0 项

    👆 实时可用! 在上方演示中添加待办事项,切换筛选标签,体验与真实 ArkTS 应用相同的交互逻辑。

    11.8 10.7 知识点回顾

    🔤

    第2-3章

    变量声明、类型、控制流

    🔧

    第4章

    函数、箭头函数、可选参数

    📦

    第5章

    数组 filter / find / map

    🏗️

    第6章

    类、接口、getter

    🎨

    第7-8章

    @Component @State 装饰器 + UI组件

    🔄

    第9章

    @Link @ObjectLink 跨组件状态

    💡 下一步:将数据持久化到 AppStoragePreferences,让 Todo 列表在应用重启后依然存在!

    11.9 10.8 项目结构总览

    todo-app/
    ├── entry/src/main/ets/
    │   ├── entryability/
    │   │   └── EntryAbility.ets
    │   ├── model/
    │   │   └── TodoModel.ets          ← 数据接口定义
    │   ├── viewmodel/
    │   │   └── TodoViewModel.ets      ← 业务逻辑
    │   ├── components/
    │   │   ├── InputBar.ets           ← 输入栏组件
    │   │   └── TodoItemComp.ets       ← 待办项组件
    │   └── pages/
    │       └── Index.ets              ← 主页面(@Entry)
    └── entry/src/main/resources/
        └── base/profile/
            └── main_pages.json

    🎉 恭喜完成 ArkTS 互动教程!

    你已掌握 ArkTS 的核心知识体系,可以开始构建真实的 HarmonyOS 应用了!