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 设计
  • 测验数据结构
  • 答案验证逻辑
  • 分数计算

进入测验功能开发!

相关推荐
天***88525 分钟前
Edge 浏览器离线绿色增强版+官方安装包,支持win7等系统
前端·edge
漫游的渔夫14 分钟前
别再直接 `json.loads` 了!AI 返回的 JSON 坑位指南
前端·人工智能
软件工程师文艺26 分钟前
从0到1:Claude Code如何用React构建CLI应用
前端·react.js·前端框架
Utopia^29 分钟前
Flutter 框架跨平台鸿蒙开发 - 旅行预算管家
flutter·华为·harmonyos
M ? A35 分钟前
Vue 迁移 React 实战:VuReact 一键自动化转换方案
前端·vue.js·经验分享·react.js·开源·自动化·vureact
yuki_uix36 分钟前
重排、重绘与合成——浏览器渲染性能的底层逻辑
前端·javascript·面试
李李李勃谦1 小时前
Flutter 框架跨平台鸿蒙开发 - 星空识别助手
flutter·华为·harmonyos
Mars酱1 小时前
1分钟编写贪吃蛇 | JSnake贪吃蛇单机版
java·后端·开源
李李李勃谦1 小时前
Flutter 框架跨平台鸿蒙开发 - 本地生活服务预约
flutter·华为·生活·harmonyos
沃尔威武1 小时前
调试黑科技:Chrome DevTools时间旅行调试实战
前端·科技·chrome devtools