HarmonyOS 5开发从入门到精通(十四):待办事项应用实战(下)

HarmonyOS 5开发从入门到精通(十四):待办事项应用实战(下)

本章将继续完善待办事项应用,添加任务编辑、分类管理、提醒设置等高级功能,让应用更加实用和完整。

一、任务编辑功能

1.1 编辑页面设计

创建任务编辑页面,支持修改任务标题、描述、截止时间等信息。

// pages/EditTask.ets
import router from '@ohos.router';
import { TaskModel } from '../model/TaskModel';
import { StorageUtil } from '../utils/StorageUtil';

@Entry
@Component
struct EditTask {
  @State task: TaskModel = new TaskModel('');
  @State title: string = '';
  @State description: string = '';
  @State dueDate: string = '';
  @State showDatePicker: boolean = false;

  aboutToAppear() {
    const params = router.getParams() as { task: TaskModel };
    if (params?.task) {
      this.task = params.task;
      this.title = this.task.title;
      this.description = this.task.description || '';
      this.dueDate = this.task.dueDate || '';
    }
  }

  // 保存任务
  async saveTask() {
    if (this.title.trim() === '') {
      promptAction.showToast({ message: '请输入任务标题' });
      return;
    }

    this.task.title = this.title.trim();
    this.task.description = this.description;
    this.task.dueDate = this.dueDate;
    this.task.updateTime = new Date().getTime();

    // 保存到本地存储
    await StorageUtil.updateTask(this.task);
    
    // 返回上一页并传递更新标识
    router.back({ url: 'pages/Index', params: { refresh: true } });
  }

  // 选择截止日期
  selectDueDate() {
    this.showDatePicker = true;
  }

  build() {
    Column({ space: 0 }) {
      // 顶部导航栏
      Row() {
        Button('取消')
          .fontSize(16)
          .fontColor('#007AFF')
          .backgroundColor('transparent')
          .onClick(() => router.back())
        
        Text('编辑任务')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
        
        Button('保存')
          .fontSize(16)
          .fontColor('#007AFF')
          .backgroundColor('transparent')
          .onClick(() => this.saveTask())
      }
      .width('100%')
      .height(60)
      .padding({ left: 15, right: 15 })
      .backgroundColor('#FFFFFF')
      .border({ width: { bottom: 1 }, color: '#EEEEEE' })

      // 表单内容
      Scroll() {
        Column({ space: 20 }) {
          // 任务标题
          Column({ space: 8 }) {
            Text('任务标题')
              .fontSize(16)
              .fontColor('#333333')
            
            TextInput({ text: this.title, placeholder: '请输入任务标题' })
              .placeholderColor('#999999')
              .fontSize(16)
              .height(50)
              .backgroundColor('#FFFFFF')
              .borderRadius(8)
              .border({ width: 1, color: '#DDDDDD' })
              .padding({ left: 15, right: 15 })
          }

          // 任务描述
          Column({ space: 8 }) {
            Text('任务描述(可选)')
              .fontSize(16)
              .fontColor('#333333')
            
            TextArea({ text: this.description, placeholder: '请输入任务描述...' })
              .placeholderColor('#999999')
              .fontSize(16)
              .height(120)
              .backgroundColor('#FFFFFF')
              .borderRadius(8)
              .border({ width: 1, color: '#DDDDDD' })
              .padding(15)
          }

          // 截止日期
          Column({ space: 8 }) {
            Text('截止日期(可选)')
              .fontSize(16)
              .fontColor('#333333')
            
            Row() {
              Text(this.dueDate || '选择截止日期')
                .fontSize(16)
                .fontColor(this.dueDate ? '#333333' : '#999999')
                .layoutWeight(1)
              
              Image($r('app.media.calendar'))
                .width(20)
                .height(20)
            }
            .height(50)
            .backgroundColor('#FFFFFF')
            .borderRadius(8)
            .border({ width: 1, color: '#DDDDDD' })
            .padding({ left: 15, right: 15 })
            .onClick(() => this.selectDueDate())
          }
        }
        .padding(20)
      }
      .layoutWeight(1)

      // 日期选择器
      if (this.showDatePicker) {
        DatePicker({
          start: new Date(),
          end: new Date(2030, 11, 31),
          selected: this.dueDate ? new Date(this.dueDate) : new Date()
        })
        .width('100%')
        .height(300)
        .onChange((value: DatePickerResult) => {
          const date = new Date(value.year, value.month, value.day);
          this.dueDate = date.toISOString().split('T')[0];
          this.showDatePicker = false;
        })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
}

1.2 主页面添加编辑功能

在主页面中添加编辑跳转逻辑:

// pages/Index.ets 中修改TaskItem组件
TaskItem({
  task: item,
  onToggle: () => this.toggleTask(item.id),
  onEdit: () => this.editTask(item),
  onDelete: () => this.deleteTask(item.id)
})

// 添加编辑方法
editTask(task: TaskModel) {
  router.pushUrl({
    url: 'pages/EditTask',
    params: { task: task }
  });
}

// 监听页面返回刷新
onPageShow() {
  const params = router.getParams() as { refresh: boolean };
  if (params?.refresh) {
    this.loadTasks();
  }
}

二、分类管理功能

2.1 分类数据模型

// model/CategoryModel.ets
export class CategoryModel {
  id: string = '';
  name: string = '';
  color: string = '#007AFF';
  taskCount: number = 0;
  
  constructor(name: string, color?: string) {
    this.id = this.generateId();
    this.name = name;
    this.color = color || this.getRandomColor();
  }
  
  private generateId(): string {
    return Date.now().toString() + Math.random().toString(36).substr(2);
  }
  
  private getRandomColor(): string {
    const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#FD79A8'];
    return colors[Math.floor(Math.random() * colors.length)];
  }
}

2.2 分类管理页面

// pages/CategoryManager.ets
import router from '@ohos.router';
import { CategoryModel } from '../model/CategoryModel';
import { StorageUtil } from '../utils/StorageUtil';

@Entry
@Component
struct CategoryManager {
  @State categories: CategoryModel[] = [];
  @State newCategoryName: string = '';
  @State showAddDialog: boolean = false;

  aboutToAppear() {
    this.loadCategories();
  }

  async loadCategories() {
    this.categories = await StorageUtil.getCategories();
  }

  async addCategory() {
    if (this.newCategoryName.trim() === '') {
      promptAction.showToast({ message: '请输入分类名称' });
      return;
    }

    const newCategory = new CategoryModel(this.newCategoryName.trim());
    this.categories.push(newCategory);
    this.newCategoryName = '';
    this.showAddDialog = false;
    
    await StorageUtil.saveCategories(this.categories);
  }

  async deleteCategory(id: string) {
    this.categories = this.categories.filter(category => category.id !== id);
    await StorageUtil.saveCategories(this.categories);
  }

  build() {
    Column({ space: 0 }) {
      // 顶部导航栏
      Row() {
        Button('返回')
          .fontSize(16)
          .fontColor('#007AFF')
          .backgroundColor('transparent')
          .onClick(() => router.back())
        
        Text('分类管理')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
        
        Button('添加')
          .fontSize(16)
          .fontColor('#007AFF')
          .backgroundColor('transparent')
          .onClick(() => this.showAddDialog = true)
      }
      .width('100%')
      .height(60)
      .padding({ left: 15, right: 15 })
      .backgroundColor('#FFFFFF')
      .border({ width: { bottom: 1 }, color: '#EEEEEE' })

      // 分类列表
      if (this.categories.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)
      } else {
        List({ space: 1 }) {
          ForEach(this.categories, (category: CategoryModel) => {
            ListItem() {
              Row({ space: 12 }) {
                // 分类颜色标识
                Column()
                  .width(8)
                  .height(8)
                  .backgroundColor(category.color)
                  .borderRadius(4)
                
                Text(category.name)
                  .fontSize(16)
                  .fontColor('#333333')
                  .layoutWeight(1)
                
                Text(`${category.taskCount}个任务`)
                  .fontSize(14)
                  .fontColor('#999999')
                
                Button() {
                  Image($r('app.media.delete'))
                    .width(20)
                    .height(20)
                }
                .width(40)
                .height(40)
                .backgroundColor('transparent')
                .onClick(() => this.deleteCategory(category.id))
              }
              .width('100%')
              .height(60)
              .padding({ left: 20, right: 20 })
              .backgroundColor('#FFFFFF')
            }
          })
        }
        .width('100%')
        .layoutWeight(1)
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')

    // 添加分类弹窗
    if (this.showAddDialog) {
      Column() {
        Column({ space: 15 }) {
          Text('添加分类')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .textAlign(TextAlign.Center)
          
          TextInput({ text: this.newCategoryName, placeholder: '请输入分类名称' })
            .placeholderColor('#999999')
            .fontSize(16)
            .height(50)
            .backgroundColor('#FFFFFF')
            .borderRadius(8)
            .border({ width: 1, color: '#DDDDDD' })
            .padding({ left: 15, right: 15 })
          
          Row({ space: 15 }) {
            Button('取消')
              .layoutWeight(1)
              .height(45)
              .fontSize(16)
              .fontColor('#333333')
              .backgroundColor('#F0F0F0')
              .borderRadius(8)
              .onClick(() => this.showAddDialog = false)
            
            Button('确定')
              .layoutWeight(1)
              .height(45)
              .fontSize(16)
              .fontColor('#FFFFFF')
              .backgroundColor('#007AFF')
              .borderRadius(8)
              .onClick(() => this.addCategory())
          }
        }
        .padding(20)
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
      }
      .width('80%')
      .justifyContent(FlexAlign.Center)
      .alignItems(HorizontalAlign.Center)
      .backgroundColor('rgba(0,0,0,0.5)')
    }
  }
}

2.3 存储工具类扩展

// utils/StorageUtil.ets 添加分类相关方法
export class StorageUtil {
  private static readonly CATEGORY_KEY = 'todo_categories';
  
  // 保存分类列表
  static async saveCategories(categories: CategoryModel[]): Promise<void> {
    try {
      const prefs = await this.getPreferences();
      const categoriesJson = JSON.stringify(categories);
      await prefs.put(this.CATEGORY_KEY, categoriesJson);
      await prefs.flush();
    } catch (error) {
      console.error('保存分类失败:', error);
    }
  }
  
  // 获取分类列表
  static async getCategories(): Promise<CategoryModel[]> {
    try {
      const prefs = await this.getPreferences();
      const categoriesJson = await prefs.get(this.CATEGORY_KEY, '[]');
      return JSON.parse(categoriesJson);
    } catch (error) {
      console.error('获取分类失败:', error);
      return [];
    }
  }
}

三、提醒设置功能

3.1 提醒数据模型

// model/ReminderModel.ets
export class ReminderModel {
  id: string = '';
  taskId: string = '';
  type: 'time' | 'location' = 'time';
  time?: string;
  location?: string;
  repeat: 'none' | 'daily' | 'weekly' | 'monthly' = 'none';
  enabled: boolean = true;
  
  constructor(taskId: string) {
    this.id = this.generateId();
    this.taskId = taskId;
  }
  
  private generateId(): string {
    return Date.now().toString() + Math.random().toString(36).substr(2);
  }
}

3.2 提醒设置页面

// pages/ReminderSetting.ets
import router from '@ohos.router';
import { ReminderModel } from '../model/ReminderModel';
import { StorageUtil } from '../utils/StorageUtil';

@Entry
@Component
struct ReminderSetting {
  @State reminder: ReminderModel = new ReminderModel('');
  @State time: string = '';
  @State repeat: string = 'none';
  @State showTimePicker: boolean = false;
  @State showRepeatPicker: boolean = false;

  aboutToAppear() {
    const params = router.getParams() as { taskId: string };
    if (params?.taskId) {
      this.reminder.taskId = params.taskId;
      this.loadReminder();
    }
  }

  async loadReminder() {
    const reminders = await StorageUtil.getReminders();
    const existingReminder = reminders.find(r => r.taskId === this.reminder.taskId);
    if (existingReminder) {
      this.reminder = existingReminder;
      this.time = this.reminder.time || '';
      this.repeat = this.reminder.repeat;
    }
  }

  async saveReminder() {
    this.reminder.time = this.time;
    this.reminder.repeat = this.repeat;
    
    await StorageUtil.saveReminder(this.reminder);
    
    // 设置系统提醒
    await this.setSystemReminder();
    
    router.back({ url: 'pages/Index', params: { refresh: true } });
  }

  async setSystemReminder() {
    if (!this.time) return;
    
    const [hour, minute] = this.time.split(':').map(Number);
    const repeatDays = this.getRepeatDays();
    
    try {
      const reminderAgent = await reminderAgentManager.getReminderAgent();
      const reminderRequest = new reminderAgentManager.ReminderRequestAlarm(
        hour,
        minute,
        repeatDays
      );
      
      reminderRequest.title = '待办事项提醒';
      reminderRequest.content = '您有一个待办事项需要处理';
      
      await reminderAgent.publishReminder(reminderRequest);
    } catch (error) {
      console.error('设置提醒失败:', error);
    }
  }

  getRepeatDays(): number[] {
    switch (this.repeat) {
      case 'daily':
        return [1, 2, 3, 4, 5, 6, 7];
      case 'weekly':
        return [1, 2, 3, 4, 5];
      case 'monthly':
        return [1];
      default:
        return [];
    }
  }

  build() {
    Column({ space: 0 }) {
      // 顶部导航栏
      Row() {
        Button('取消')
          .fontSize(16)
          .fontColor('#007AFF')
          .backgroundColor('transparent')
          .onClick(() => router.back())
        
        Text('设置提醒')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
        
        Button('保存')
          .fontSize(16)
          .fontColor('#007AFF')
          .backgroundColor('transparent')
          .onClick(() => this.saveReminder())
      }
      .width('100%')
      .height(60)
      .padding({ left: 15, right: 15 })
      .backgroundColor('#FFFFFF')
      .border({ width: { bottom: 1 }, color: '#EEEEEE' })

      // 提醒设置内容
      Scroll() {
        Column({ space: 20 }) {
          // 提醒时间
          Column({ space: 8 }) {
            Text('提醒时间')
              .fontSize(16)
              .fontColor('#333333')
            
            Row() {
              Text(this.time || '选择提醒时间')
                .fontSize(16)
                .fontColor(this.time ? '#333333' : '#999999')
                .layoutWeight(1)
              
              Image($r('app.media.time'))
                .width(20)
                .height(20)
            }
            .height(50)
            .backgroundColor('#FFFFFF')
            .borderRadius(8)
            .border({ width: 1, color: '#DDDDDD' })
            .padding({ left: 15, right: 15 })
            .onClick(() => this.showTimePicker = true)
          }

          // 重复设置
          Column({ space: 8 }) {
            Text('重复提醒')
              .fontSize(16)
              .fontColor('#333333')
            
            Row() {
              Text(this.getRepeatText())
                .fontSize(16)
                .fontColor('#333333')
                .layoutWeight(1)
              
              Image($r('app.media.arrow'))
                .width(20)
                .height(20)
            }
            .height(50)
            .backgroundColor('#FFFFFF')
            .borderRadius(8)
            .border({ width: 1, color: '#DDDDDD' })
            .padding({ left: 15, right: 15 })
            .onClick(() => this.showRepeatPicker = true)
          }
        }
        .padding(20)
      }
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')

    // 时间选择器
    if (this.showTimePicker) {
      Column() {
        TimePicker()
          .width('100%')
          .height(300)
          .onChange((value: TimePickerResult) => {
            this.time = `${value.hour.toString().padStart(2, '0')}:${value.minute.toString().padStart(2, '0')}`;
            this.showTimePicker = false;
          })
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
    }

    // 重复选择器
    if (this.showRepeatPicker) {
      Column() {
        List({ space: 1 }) {
          ListItem() {
            Text('不重复')
              .fontSize(16)
              .fontColor('#333333')
              .onClick(() => {
                this.repeat = 'none';
                this.showRepeatPicker = false;
              })
          }
          .height(50)
          .padding({ left: 20, right: 20 })
          
          ListItem() {
            Text('每天')
              .fontSize(16)
              .fontColor('#333333')
              .onClick(() => {
                this.repeat = 'daily';
                this.showRepeatPicker = false;
              })
          }
          .height(50)
          .padding({ left: 20, right: 20 })
          
          ListItem() {
            Text('每周')
              .fontSize(16)
              .fontColor('#333333')
              .onClick(() => {
                this.repeat = 'weekly';
                this.showRepeatPicker = false;
              })
          }
          .height(50)
          .padding({ left: 20, right: 20 })
          
          ListItem() {
            Text('每月')
              .fontSize(16)
              .fontColor('#333333')
              .onClick(() => {
                this.repeat = 'monthly';
                this.showRepeatPicker = false;
              })
          }
          .height(50)
          .padding({ left: 20, right: 20 })
        }
      }
      .width('80%')
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
    }
  }

  getRepeatText(): string {
    switch (this.repeat) {
      case 'daily':
        return '每天';
      case 'weekly':
        return '每周';
      case 'monthly':
        return '每月';
      default:
        return '不重复';
    }
  }
}

3.3 权限配置

在module.json5中添加提醒权限:

{
  "requestPermissions": [
    {
      "name": "ohos.permission.PUBLISH_AGENT_REMINDER",
      "reason": "需要设置待办事项提醒"
    }
  ]
}

四、主页面功能集成

4.1 任务模型扩展

// model/TaskModel.ets 扩展字段
export class TaskModel {
  id: string = '';
  title: string = '';
  description: string = '';
  completed: boolean = false;
  category: string = '';
  priority: 'low' | 'medium' | 'high' = 'medium';
  dueDate: string = '';
  createTime: number = 0;
  updateTime: number = 0;
  hasReminder: boolean = false;
  
  constructor(title: string) {
    this.id = this.generateId();
    this.title = title;
    this.completed = false;
    this.createTime = new Date().getTime();
    this.updateTime = this.createTime;
  }
  
  private generateId(): string {
    return Date.now().toString() + Math.random().toString(36).substr(2);
  }
}

4.2 主页面功能扩展

// pages/Index.ets 继续完善功能
@Entry
@Component
struct Index {
  @State taskList: TaskModel[] = [];
  @State categories: CategoryModel[] = [];
  @State showCategoryManager: boolean = false;
  @State showReminderSetting: boolean = false;
  @State selectedTaskId: string = '';
  
  // 页面显示时加载数据
  aboutToAppear() {
    this.loadTasks();
    this.loadCategories();
  }
  
  // 加载分类
  async loadCategories() {
    this.categories = await StorageUtil.getCategories();
  }
  
  // 设置提醒
  setReminder(taskId: string) {
    this.selectedTaskId = taskId;
    this.showReminderSetting = true;
  }
  
  // 管理分类
  manageCategories() {
    this.showCategoryManager = true;
  }
  
  // 获取任务分类名称
  getCategoryName(categoryId: string): string {
    const category = this.categories.find(c => c.id === categoryId);
    return category?.name || '未分类';
  }
  
  // 获取任务分类颜色
  getCategoryColor(categoryId: string): string {
    const category = this.categories.find(c => c.id === categoryId);
    return category?.color || '#999999';
  }
  
  // 跳转到编辑页面
  editTask(task: TaskModel) {
    router.pushUrl({
      url: 'pages/EditTask',
      params: { task: task }
    });
  }
  
  // 监听页面返回刷新
  onPageShow() {
    const params = router.getParams() as { refresh: boolean };
    if (params?.refresh) {
      this.loadTasks();
      this.loadCategories();
    }
  }
  
  build() {
    Column({ space: 0 }) {
      // 顶部导航栏
      this.buildHeader()
      
      // 输入区域
      this.buildInputArea()
      
      // 筛选栏
      this.buildFilterBar()
      
      // 任务列表
      this.buildTaskList()
      
      // 底部操作栏
      this.buildActionBar()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
    
    // 分类管理弹窗
    if (this.showCategoryManager) {
      CategoryManager()
        .onClose(() => this.showCategoryManager = false)
    }
    
    // 提醒设置弹窗
    if (this.showReminderSetting) {
      ReminderSetting({
        taskId: this.selectedTaskId
      })
      .onClose(() => this.showReminderSetting = false)
    }
  }
  
  // 构建底部操作栏
  @Builder
  buildActionBar() {
    Row({ space: 20 }) {
      Button('管理分类')
        .layoutWeight(1)
        .height(45)
        .fontSize(16)
        .fontColor('#007AFF')
        .backgroundColor('#FFFFFF')
        .borderRadius(8)
        .border({ width: 1, color: '#007AFF' })
        .onClick(() => this.manageCategories())
      
      Button('设置提醒')
        .layoutWeight(1)
        .height(45)
        .fontSize(16)
        .fontColor('#FFFFFF')
        .backgroundColor('#007AFF')
        .borderRadius(8)
        .onClick(() => this.setReminder(''))
    }
    .padding(20)
    .backgroundColor('#F5F5F5')
  }
  
  // 构建任务列表(增强版)
  @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,
              categoryName: this.getCategoryName(item.category),
              categoryColor: this.getCategoryColor(item.category),
              onToggle: () => this.toggleTask(item.id),
              onEdit: () => this.editTask(item),
              onDelete: () => this.deleteTask(item.id),
              onSetReminder: () => this.setReminder(item.id)
            })
          }
        }, (item: TaskModel) => item.id)
      }
      .width('100%')
      .layoutWeight(1)
      .divider({ strokeWidth: 1, color: '#EEEEEE', startMargin: 20, endMargin: 20 })
    }
  }
}

4.3 任务项组件增强

// components/TaskItem.ets 增强版
@Component
export default struct TaskItem {
  private task: TaskModel;
  private categoryName: string;
  private categoryColor: string;
  private onToggle: () => void;
  private onEdit: () => void;
  private onDelete: () => void;
  private onSetReminder: () => void;
  
  build() {
    Row({ space: 12 }) {
      // 复选框
      Checkbox()
        .checked(this.task.completed)
        .width(24)
        .height(24)
        .onChange((checked: boolean) => {
          this.onToggle();
        })
      
      // 任务内容
      Column({ space: 4 }) {
        // 任务标题
        Text(this.task.title)
          .fontSize(16)
          .fontColor(this.task.completed ? '#999999' : '#333333')
          .decoration({ type: this.task.completed ? TextDecorationType.LineThrough : TextDecorationType.None })
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
        
        // 任务信息(分类和截止日期)
        Row({ space: 12 }) {
          if (this.categoryName !== '未分类') {
            Row({ space: 4 }) {
              Column()
                .width(6)
                .height(6)
                .backgroundColor(this.categoryColor)
                .borderRadius(3)
              
              Text(this.categoryName)
                .fontSize(12)
                .fontColor('#666666')
            }
          }
          
          if (this.task.dueDate) {
            Text(this.formatDueDate())
              .fontSize(12)
              .fontColor('#FF6B6B')
          }
        }
      }
      .layoutWeight(1)
      
      // 操作按钮组
      Row({ space: 8 }) {
        // 提醒按钮
        if (this.task.hasReminder) {
          Button() {
            Image($r('app.media.reminder'))
              .width(20)
              .height(20)
          }
          .width(40)
          .height(40)
          .backgroundColor('transparent')
          .onClick(() => this.onSetReminder())
        }
        
        // 编辑按钮
        Button() {
          Image($r('app.media.edit'))
            .width(20)
            .height(20)
        }
        .width(40)
        .height(40)
        .backgroundColor('transparent')
        .onClick(() => this.onEdit())
        
        // 删除按钮
        Button() {
          Image($r('app.media.delete'))
            .width(20)
            .height(20)
        }
        .width(40)
        .height(40)
        .backgroundColor('transparent')
        .onClick(() => this.onDelete())
      }
    }
    .width('100%')
    .height(70)
    .padding({ left: 20, right: 20 })
    .backgroundColor('#FFFFFF')
  }
  
  // 格式化截止日期
  private formatDueDate(): string {
    if (!this.task.dueDate) return '';
    
    const dueDate = new Date(this.task.dueDate);
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    
    const diffTime = dueDate.getTime() - today.getTime();
    const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
    
    if (diffDays === 0) {
      return '今天到期';
    } else if (diffDays === 1) {
      return '明天到期';
    } else if (diffDays === -1) {
      return '昨天到期';
    } else if (diffDays < 0) {
      return `已过期${Math.abs(diffDays)}天`;
    } else {
      return `${diffDays}天后到期`;
    }
  }
}

五、存储工具类完整版

// utils/StorageUtil.ets 完整版
import preferences from '@ohos.data.preferences';
import { TaskModel } from '../model/TaskModel';
import { CategoryModel } from '../model/CategoryModel';
import { ReminderModel } from '../model/ReminderModel';

export class StorageUtil {
  private static readonly TASK_KEY = 'todo_tasks';
  private static readonly CATEGORY_KEY = 'todo_categories';
  private static readonly REMINDER_KEY = 'todo_reminders';
  
  // 获取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.TASK_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.TASK_KEY, '[]');
      const tasks = JSON.parse(tasksJson);
      return tasks.map((task: any) => {
        const model = new TaskModel(task.title);
        model.id = task.id;
        model.title = task.title;
        model.description = task.description || '';
        model.completed = task.completed || false;
        model.category = task.category || '';
        model.priority = task.priority || 'medium';
        model.dueDate = task.dueDate || '';
        model.createTime = task.createTime || 0;
        model.updateTime = task.updateTime || 0;
        model.hasReminder = task.hasReminder || false;
        return model;
      });
    } catch (error) {
      console.error('获取任务失败:', error);
      return [];
    }
  }
  
  // 更新单个任务
  static async updateTask(task: TaskModel): Promise<void> {
    try {
      const tasks = await this.getTasks();
      const index = tasks.findIndex(t => t.id === task.id);
      if (index !== -1) {
        tasks[index] = task;
        await this.saveTasks(tasks);
      }
    } catch (error) {
      console.error('更新任务失败:', error);
    }
  }
  
  // 删除单个任务
  static async deleteTask(id: string): Promise<void> {
    try {
      const tasks = await this.getTasks();
      const filteredTasks = tasks.filter(task => task.id !== id);
      await this.saveTasks(filteredTasks);
    } catch (error) {
      console.error('删除任务失败:', error);
    }
  }
  
  // ========== 分类相关方法 ==========
  
  // 保存分类列表
  static async saveCategories(categories: CategoryModel[]): Promise<void> {
    try {
      const prefs = await this.getPreferences();
      const categoriesJson = JSON.stringify(categories);
      await prefs.put(this.CATEGORY_KEY, categoriesJson);
      await prefs.flush();
    } catch (error) {
      console.error('保存分类失败:', error);
    }
  }
  
  // 获取分类列表
  static async getCategories(): Promise<CategoryModel[]> {
    try {
      const prefs = await this.getPreferences();
      const categoriesJson = await prefs.get(this.CATEGORY_KEY, '[]');
      return JSON.parse(categoriesJson);
    } catch (error) {
      console.error('获取分类失败:', error);
      return [];
    }
  }
  
  // ========== 提醒相关方法 ==========
  
  // 保存提醒
  static async saveReminder(reminder: ReminderModel): Promise<void> {
    try {
      const prefs = await this.getPreferences();
      const reminders = await this.getReminders();
      const index = reminders.findIndex(r => r.id === reminder.id);
      
      if (index !== -1) {
        reminders[index] = reminder;
      } else {
        reminders.push(reminder);
      }
      
      const remindersJson = JSON.stringify(reminders);
      await prefs.put(this.REMINDER_KEY, remindersJson);
      await prefs.flush();
      
      // 更新任务提醒状态
      const tasks = await this.getTasks();
      const taskIndex = tasks.findIndex(t => t.id === reminder.taskId);
      if (taskIndex !== -1) {
        tasks[taskIndex].hasReminder = reminder.enabled;
        await this.saveTasks(tasks);
      }
    } catch (error) {
      console.error('保存提醒失败:', error);
    }
  }
  
  // 获取提醒列表
  static async getReminders(): Promise<ReminderModel[]> {
    try {
      const prefs = await this.getPreferences();
      const remindersJson = await prefs.get(this.REMINDER_KEY, '[]');
      return JSON.parse(remindersJson);
    } catch (error) {
      console.error('获取提醒失败:', error);
      return [];
    }
  }
  
  // 删除提醒
  static async deleteReminder(id: string): Promise<void> {
    try {
      const prefs = await this.getPreferences();
      const reminders = await this.getReminders();
      const filteredReminders = reminders.filter(r => r.id !== id);
      
      const remindersJson = JSON.stringify(filteredReminders);
      await prefs.put(this.REMINDER_KEY, remindersJson);
      await prefs.flush();
    } catch (error) {
      console.error('删除提醒失败:', error);
    }
  }
  
  // 清空所有数据
  static async clearAll(): Promise<void> {
    try {
      const prefs = await this.getPreferences();
      await prefs.delete(this.TASK_KEY);
      await prefs.delete(this.CATEGORY_KEY);
      await prefs.delete(this.REMINDER_KEY);
      await prefs.flush();
    } catch (error) {
      console.error('清空数据失败:', error);
    }
  }
}

六、项目配置完善

6.1 权限配置

// module.json5
{
  "requestPermissions": [
    {
      "name": "ohos.permission.DISTRIBUTED_DATASYNC",
      "reason": "需要存储待办事项数据"
    },
    {
      "name": "ohos.permission.PUBLISH_AGENT_REMINDER",
      "reason": "需要设置待办事项提醒"
    }
  ]
}

6.2 页面路由配置

// module.json5 中的 pages 配置
{
  "pages": [
    "pages/Index",
    "pages/EditTask",
    "pages/CategoryManager",
    "pages/ReminderSetting"
  ]
}

6.3 资源文件准备

在resources/base/media目录下准备以下图片资源:

  • empty.png(空状态图片)
  • delete.png(删除图标)
  • edit.png(编辑图标)
  • reminder.png(提醒图标)
  • calendar.png(日历图标)
  • time.png(时间图标)
  • arrow.png(箭头图标)

七、项目运行与调试

7.1 运行项目

  1. 在DevEco Studio中打开项目
  2. 连接HarmonyOS设备或启动模拟器
  3. 点击运行按钮(▶️)
  4. 等待应用安装并启动

7.2 功能测试清单

基础功能测试

  • 添加、删除、编辑任务
  • 标记任务完成状态
  • 数据持久化存储
  • 筛选功能正常

分类管理测试

  • 添加、删除分类
  • 任务关联分类显示
  • 分类颜色标识

提醒功能测试

  • 设置提醒时间
  • 选择重复周期
  • 系统提醒触发

界面交互测试

  • 页面跳转流畅
  • 弹窗显示正常
  • 按钮响应及时

八、本章总结

本章完成了待办事项应用的高级功能开发,包括:

任务编辑功能 - 支持修改任务标题、描述、截止时间

分类管理功能 - 自定义任务分类,支持颜色标识

提醒设置功能 - 时间提醒和重复提醒

数据存储增强 - 完整的本地存储方案

界面优化 - 更丰富的任务信息展示

核心技术点

  • 页面路由跳转与参数传递
  • 弹窗组件的使用
  • 日期和时间选择器
  • 系统提醒API调用
  • 复杂数据模型的管理

通过这个完整的待办事项应用实战项目,我们掌握了HarmonyOS应用开发的核心技能,包括界面开发、状态管理、数据存储、组件封装等。这些技能可以应用到各种类型的应用开发中,为后续的HarmonyOS开发打下坚实基础。

posted @ 2025-12-23 21:19  奇崽  阅读(0)  评论(0)    收藏  举报