HarmonyOS APP<玩转React>开源教程二十:收藏功能实现

第20次:收藏功能实现

收藏功能让用户可以标记感兴趣的课程,方便后续快速访问。本次课程将完整实现收藏功能,包括服务层、状态管理和收藏页面。


项目效果





学习目标

  • 掌握 BookmarkService 设计
  • 学会收藏状态管理
  • 实现收藏列表持久化
  • 完成收藏页面开发
  • 实现收藏功能全流程

20.1 收藏数据模型

Bookmark 结构

typescript 复制代码
interface Bookmark {
  lessonId: string;    // 课程 ID
  moduleId: string;    // 所属模块 ID
  addedAt: string;     // 添加时间(ISO 格式)
}

存储键

typescript 复制代码
// Constants.ets
export class StorageKeys {
  static readonly BOOKMARKS: string = 'bookmarks';
}

20.2 BookmarkService 设计

服务结构

typescript 复制代码
export class BookmarkService {
  // 内存缓存
  private static cachedBookmarks: Bookmark[] = [];

  // 加载收藏
  static async loadBookmarks(): Promise<Bookmark[]>;
  
  // 添加收藏
  static async addBookmark(lessonId: string, moduleId: string): Promise<void>;
  
  // 移除收藏
  static async removeBookmark(lessonId: string): Promise<void>;
  
  // 切换收藏状态
  static async toggleBookmark(lessonId: string, moduleId: string): Promise<boolean>;
  
  // 检查是否已收藏
  static isBookmarked(lessonId: string): boolean;
}

加载收藏

typescript 复制代码
static async loadBookmarks(): Promise<Bookmark[]> {
  try {
    const bookmarks = await StorageUtil.getObject<Bookmark[]>(
      StorageKeys.BOOKMARKS,
      []
    );
    BookmarkService.cachedBookmarks = bookmarks;
    return bookmarks;
  } catch (error) {
    console.error('[BookmarkService] Failed to load bookmarks:', error);
    return [];
  }
}

保存收藏

typescript 复制代码
private static async saveBookmarks(bookmarks: Bookmark[]): Promise<void> {
  try {
    await StorageUtil.setObject(StorageKeys.BOOKMARKS, bookmarks);
    BookmarkService.cachedBookmarks = bookmarks;
  } catch (error) {
    console.error('[BookmarkService] Failed to save bookmarks:', error);
  }
}

20.3 收藏操作实现

添加收藏

typescript 复制代码
static async addBookmark(lessonId: string, moduleId: string): Promise<void> {
  const bookmarks = await BookmarkService.loadBookmarks();

  // 检查是否已收藏,避免重复
  if (bookmarks.some(b => b.lessonId === lessonId)) {
    return;
  }

  // 创建新收藏
  const newBookmark: Bookmark = {
    lessonId,
    moduleId,
    addedAt: new Date().toISOString()
  };

  bookmarks.push(newBookmark);
  await BookmarkService.saveBookmarks(bookmarks);
}

移除收藏

typescript 复制代码
static async removeBookmark(lessonId: string): Promise<void> {
  const bookmarks = await BookmarkService.loadBookmarks();
  const filtered = bookmarks.filter(b => b.lessonId !== lessonId);
  await BookmarkService.saveBookmarks(filtered);
}

切换收藏状态

typescript 复制代码
static async toggleBookmark(lessonId: string, moduleId: string): Promise<boolean> {
  const isCurrentlyBookmarked = BookmarkService.isBookmarked(lessonId);

  if (isCurrentlyBookmarked) {
    await BookmarkService.removeBookmark(lessonId);
    return false;  // 返回新状态:未收藏
  } else {
    await BookmarkService.addBookmark(lessonId, moduleId);
    return true;   // 返回新状态:已收藏
  }
}

检查收藏状态

typescript 复制代码
// 同步方法,使用缓存
static isBookmarked(lessonId: string): boolean {
  return BookmarkService.cachedBookmarks.some(b => b.lessonId === lessonId);
}

20.4 收藏分组功能

按模块分组

typescript 复制代码
static getBookmarksByModule(): Map<string, Bookmark[]> {
  const grouped = new Map<string, Bookmark[]>();

  for (const bookmark of BookmarkService.cachedBookmarks) {
    const moduleBookmarks = grouped.get(bookmark.moduleId) ?? [];
    moduleBookmarks.push(bookmark);
    grouped.set(bookmark.moduleId, moduleBookmarks);
  }

  return grouped;
}

获取收藏数量

typescript 复制代码
static getBookmarkCount(): number {
  return BookmarkService.cachedBookmarks.length;
}

清空收藏

typescript 复制代码
static async clearBookmarks(): Promise<void> {
  await BookmarkService.saveBookmarks([]);
}

20.5 收藏页面实现

页面结构

typescript 复制代码
@Entry
@Component
struct BookmarkPage {
  @State bookmarks: Bookmark[] = [];
  @State isLoading: boolean = true;
  @StorageLink('isDarkMode') isDarkMode: boolean = false;

  aboutToAppear(): void {
    this.loadBookmarks();
  }

  private async loadBookmarks(): Promise<void> {
    this.bookmarks = await BookmarkService.loadBookmarks();
    this.isLoading = false;
  }
}

空状态展示

typescript 复制代码
@Builder
EmptyState() {
  Column() {
    Text('📚')
      .fontSize(64)
    Text('暂无收藏')
      .fontSize(18)
      .fontColor(this.isDarkMode ? '#d1d5db' : '#495057')
      .margin({ top: 16 })
    Text('点击课程旁的 ☆ 添加收藏')
      .fontSize(14)
      .fontColor(this.isDarkMode ? '#9ca3af' : '#6c757d')
      .margin({ top: 8 })
  }
  .width('100%')
  .height('100%')
  .justifyContent(FlexAlign.Center)
}

收藏列表

typescript 复制代码
@Builder
BookmarkList() {
  List() {
    ForEach(this.bookmarks, (bookmark: Bookmark) => {
      ListItem() {
        this.BookmarkItem(bookmark)
      }
      .margin({ bottom: 8 })
    }, (bookmark: Bookmark) => bookmark.lessonId)
  }
  .width('100%')
  .layoutWeight(1)
  .padding({ left: 16, right: 16, top: 12 })
  .scrollBar(BarState.Off)
}

收藏项组件

typescript 复制代码
@Builder
BookmarkItem(bookmark: Bookmark) {
  Row() {
    // 获取课程信息
    Column() {
      Text(this.getLessonTitle(bookmark))
        .fontSize(15)
        .fontWeight(FontWeight.Medium)
        .fontColor(this.isDarkMode ? '#ffffff' : '#1a1a2e')
      
      Text(this.getModuleTitle(bookmark))
        .fontSize(12)
        .fontColor(this.isDarkMode ? '#d1d5db' : '#495057')
        .margin({ top: 4 })
    }
    .alignItems(HorizontalAlign.Start)
    .layoutWeight(1)

    // 取消收藏按钮
    Text('★')
      .fontSize(20)
      .fontColor('#fcc419')
      .onClick(() => this.removeBookmark(bookmark))

    // 箭头
    Text('›')
      .fontSize(18)
      .fontColor(this.isDarkMode ? '#9ca3af' : '#6c757d')
      .margin({ left: 8 })
  }
  .width('100%')
  .padding(16)
  .backgroundColor(this.isDarkMode ? '#282c34' : '#ffffff')
  .borderRadius(12)
  .onClick(() => {
    router.pushUrl({
      url: 'pages/LessonDetail',
      params: { 
        moduleId: bookmark.moduleId, 
        lessonId: bookmark.lessonId 
      }
    });
  })
}

// 获取课程标题
private getLessonTitle(bookmark: Bookmark): string {
  const lesson = TutorialService.getLessonById(
    bookmark.moduleId, 
    bookmark.lessonId
  );
  return lesson?.title ?? '未知课程';
}

// 获取模块标题
private getModuleTitle(bookmark: Bookmark): string {
  const module = TutorialService.getModuleById(bookmark.moduleId);
  return module?.title ?? '未知模块';
}

// 移除收藏
private async removeBookmark(bookmark: Bookmark): Promise<void> {
  await BookmarkService.removeBookmark(bookmark.lessonId);
  this.bookmarks = this.bookmarks.filter(
    b => b.lessonId !== bookmark.lessonId
  );
}

20.6 完整服务代码

typescript 复制代码
/**
 * 收藏管理服务
 */
import { StorageUtil } from '../common/StorageUtil';
import { StorageKeys } from '../common/Constants';
import { Bookmark } from '../models/Models';

export class BookmarkService {
  private static cachedBookmarks: Bookmark[] = [];

  static async loadBookmarks(): Promise<Bookmark[]> {
    try {
      const bookmarks = await StorageUtil.getObject<Bookmark[]>(
        StorageKeys.BOOKMARKS,
        []
      );
      BookmarkService.cachedBookmarks = bookmarks;
      return bookmarks;
    } catch (error) {
      console.error('[BookmarkService] Failed to load bookmarks:', error);
      return [];
    }
  }

  private static async saveBookmarks(bookmarks: Bookmark[]): Promise<void> {
    try {
      await StorageUtil.setObject(StorageKeys.BOOKMARKS, bookmarks);
      BookmarkService.cachedBookmarks = bookmarks;
    } catch (error) {
      console.error('[BookmarkService] Failed to save bookmarks:', error);
    }
  }

  static getCachedBookmarks(): Bookmark[] {
    return BookmarkService.cachedBookmarks;
  }

  static async addBookmark(lessonId: string, moduleId: string): Promise<void> {
    const bookmarks = await BookmarkService.loadBookmarks();
    if (bookmarks.some(b => b.lessonId === lessonId)) {
      return;
    }
    const newBookmark: Bookmark = {
      lessonId,
      moduleId,
      addedAt: new Date().toISOString()
    };
    bookmarks.push(newBookmark);
    await BookmarkService.saveBookmarks(bookmarks);
  }

  static async removeBookmark(lessonId: string): Promise<void> {
    const bookmarks = await BookmarkService.loadBookmarks();
    const filtered = bookmarks.filter(b => b.lessonId !== lessonId);
    await BookmarkService.saveBookmarks(filtered);
  }

  static async toggleBookmark(lessonId: string, moduleId: string): Promise<boolean> {
    const isCurrentlyBookmarked = BookmarkService.isBookmarked(lessonId);
    if (isCurrentlyBookmarked) {
      await BookmarkService.removeBookmark(lessonId);
      return false;
    } else {
      await BookmarkService.addBookmark(lessonId, moduleId);
      return true;
    }
  }

  static isBookmarked(lessonId: string): boolean {
    return BookmarkService.cachedBookmarks.some(b => b.lessonId === lessonId);
  }

  static getBookmarksByModule(): Map<string, Bookmark[]> {
    const grouped = new Map<string, Bookmark[]>();
    for (const bookmark of BookmarkService.cachedBookmarks) {
      const moduleBookmarks = grouped.get(bookmark.moduleId) ?? [];
      moduleBookmarks.push(bookmark);
      grouped.set(bookmark.moduleId, moduleBookmarks);
    }
    return grouped;
  }

  static getBookmarkCount(): number {
    return BookmarkService.cachedBookmarks.length;
  }

  static async clearBookmarks(): Promise<void> {
    await BookmarkService.saveBookmarks([]);
  }
}

20.7 在其他页面集成

课程详情页

typescript 复制代码
// LessonDetail.ets
@State isBookmarked: boolean = false;

aboutToAppear(): void {
  this.isBookmarked = BookmarkService.isBookmarked(this.lessonId);
}

// 收藏按钮
Text(this.isBookmarked ? '★' : '☆')
  .fontSize(24)
  .fontColor(this.isBookmarked ? '#fcc419' : '#9ca3af')
  .onClick(async () => {
    this.isBookmarked = await BookmarkService.toggleBookmark(
      this.lessonId, 
      this.moduleId
    );
  })

模块详情页

typescript 复制代码
// ModuleDetail.ets
LessonItem({
  lesson: lesson,
  isBookmarked: BookmarkService.isBookmarked(lesson.id),
  onBookmarkTap: async () => {
    await BookmarkService.toggleBookmark(lesson.id, module.id);
    this.bookmarkVersion++;  // 触发刷新
  }
})

本次课程小结

通过本次课程,你已经:

✅ 掌握了 BookmarkService 设计

✅ 学会了收藏状态管理

✅ 实现了收藏列表持久化

✅ 完成了收藏页面开发

✅ 实现了收藏功能全流程


课后练习

  1. 添加排序功能:支持按添加时间排序

  2. 添加批量操作:支持批量删除收藏

  3. 添加导出功能:导出收藏列表


下次预告

第21次:测验服务层实现

我们将开发测验功能:

  • QuizService 设计
  • 测验数据结构
  • 答案验证逻辑
  • 分数计算

进入测验功能开发!

相关推荐
晓得迷路了2 小时前
栗子前端技术周刊第 121 期 - Vitest 4.1、Nuxt 4.4、Next.js 16.2...
前端·javascript·vite
kyle~2 小时前
Electron桌面容器
前端·javascript·electron
隔壁小邓2 小时前
vue如何拆分业务逻辑
前端·javascript·vue.js
En^_^Joy2 小时前
Ajax与Axios:现代前端异步请求指南
前端·javascript·ajax
Cobyte2 小时前
来,实现一个 Mini Claude Code:从底层理解 AI Agent
前端·aigc·ai编程
SuperEugene2 小时前
Vue3 + Element Plus 表单校验实战:规则复用、自定义校验、提示语统一,告别混乱避坑|表单与表格规范篇
开发语言·前端·javascript·vue.js·前端框架
&&月弥2 小时前
三大开源消息队列(Kafka、RabbitMQ、RocketMQ)使用教程
kafka·开源·rabbitmq
酉鬼女又兒2 小时前
零基础快速入门前端JavaScript 浏览器环境输入输出语句全解析:从弹框交互到控制台调试(可用于备赛蓝桥杯Web应用开发赛道)
前端·javascript·职场和发展·蓝桥杯·js
清汤饺子2 小时前
搞懂 Cursor 后,我一行代码都不敲了《实战篇》
前端·javascript·后端