HarmonyOS APP<玩转React>开源教程十五:首页完整实现

第15次:首页完整实现

经过前面的学习,我们已经开发了 HeroBanner、ModuleCard 组件以及 TutorialService、ProgressService 服务。本次课程将整合所有内容,完成首页的完整实现。


首页效果

学习目标

  • 掌握页面数据加载流程
  • 实现继续学习区域
  • 完成推荐模块横向滚动
  • 实现快捷入口功能
  • 完成首页的完整开发

15.1 首页数据加载流程

加载时序

复制代码
aboutToAppear()
    │
    ├── StorageUtil.init()      // 初始化存储
    │
    ├── initTheme()             // 初始化主题
    │
    ├── TutorialService.init()  // 初始化教程服务
    │
    ├── loadModules()           // 加载模块数据
    │
    ├── loadProgress()          // 加载用户进度
    │
    └── isLoading = false       // 完成加载

实现代码

typescript 复制代码
@Entry
@Component
struct Index {
  @State currentTab: number = 0;
  @State modules: LearningModule[] = [];
  @State progress: UserProgress = DEFAULT_USER_PROGRESS;
  @State isLoading: boolean = true;
  @StorageLink('isDarkMode') isDarkMode: boolean = false;

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

  private async initAndLoadData(): Promise<void> {
    try {
      // 1. 初始化存储
      await StorageUtil.init(getContext(this));
      
      // 2. 初始化主题
      initTheme(getContext(this));
      
      // 3. 初始化教程服务
      TutorialService.init();
      
      // 4. 加载模块数据
      this.modules = TutorialService.getAllModules();
      
      // 5. 加载用户进度
      this.progress = await ProgressService.loadProgress();
      
      // 6. 初始化其他服务
      SearchService.init(this.modules);
      BadgeService.init(this.modules);
      
      console.info('[Index] Data loaded successfully');
    } catch (error) {
      console.error('[Index] Failed to load data:', error);
    } finally {
      this.isLoading = false;
    }
  }
}

页面显示时刷新

typescript 复制代码
onPageShow(): void {
  // 从其他页面返回时刷新进度
  this.refreshProgress();
}

private async refreshProgress(): Promise<void> {
  try {
    this.progress = await ProgressService.loadProgress();
  } catch (error) {
    console.error('[Index] Failed to refresh progress:', error);
  }
}

15.2 加载状态处理

加载视图

typescript 复制代码
@Builder
LoadingView() {
  Column() {
    LoadingProgress()
      .width(48)
      .height(48)
      .color(this.isDarkMode ? '#61DAFB' : '#0077b6')

    Text('加载中...')
      .fontSize(14)
      .fontColor(this.isDarkMode ? '#d1d5db' : '#495057')
      .margin({ top: 16 })
  }
  .width('100%')
  .height('100%')
  .justifyContent(FlexAlign.Center)
}

条件渲染

typescript 复制代码
build() {
  Column() {
    if (this.isLoading) {
      this.LoadingView()
    } else {
      // 主内容
      Tabs({ barPosition: BarPosition.End, index: this.currentTab }) {
        // Tab 内容...
      }
    }
  }
  .width('100%')
  .height('100%')
  .backgroundColor(this.isDarkMode ? '#1a1a2e' : '#f8f9fa')
}

15.3 继续学习区域

功能设计

当用户有学习记录时,显示"继续学习"卡片,点击可直接跳转到上次学习的课程。

实现代码

typescript 复制代码
@Builder
ContinueLearningSection() {
  Column() {
    Text('继续学习')
      .fontSize(18)
      .fontWeight(FontWeight.Bold)
      .fontColor(this.isDarkMode ? '#ffffff' : '#1a1a2e')
      .margin({ bottom: 12 })

    Row() {
      Text('📖')
        .fontSize(32)

      Column() {
        Text('上次学习')
          .fontSize(14)
          .fontColor(this.isDarkMode ? '#d1d5db' : '#495057')
        Text(this.progress.currentLesson ?? '')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor(this.isDarkMode ? '#ffffff' : '#1a1a2e')
          .margin({ top: 4 })
      }
      .alignItems(HorizontalAlign.Start)
      .margin({ left: 12 })
      .layoutWeight(1)

      Text('继续 →')
        .fontSize(14)
        .fontColor(this.isDarkMode ? '#61DAFB' : '#0077b6')
    }
    .width('100%')
    .padding(16)
    .backgroundColor(this.isDarkMode ? '#282c34' : '#ffffff')
    .borderRadius(16)
    .shadow({
      radius: 8,
      color: this.isDarkMode ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.05)',
      offsetY: 2
    })
    .onClick(() => {
      this.navigateToLastLesson();
    })
  }
  .width('100%')
  .padding({ left: 16, right: 16, top: 20 })
  .alignItems(HorizontalAlign.Start)
}

// 跳转到上次学习的课程
private navigateToLastLesson(): void {
  const lessonId = this.progress.currentLesson;
  if (lessonId) {
    const moduleId = this.findModuleIdByLessonId(lessonId);
    if (moduleId) {
      router.pushUrl({
        url: 'pages/LessonDetail',
        params: { moduleId: moduleId, lessonId: lessonId }
      });
    }
  }
}

// 根据课程 ID 查找模块 ID
private findModuleIdByLessonId(lessonId: string): string | undefined {
  for (const module of this.modules) {
    const found = module.lessons.find(l => l.id === lessonId);
    if (found) {
      return module.id;
    }
  }
  return undefined;
}

条件显示

typescript 复制代码
@Builder
HomeContent() {
  Scroll() {
    Column() {
      // Hero Banner
      HeroBanner({
        completedLessons: this.progress.completedLessons.length,
        totalLessons: TutorialService.getTotalLessonCount(),
        streak: this.progress.learningStreak,
        onDailyQuestionTap: () => {
          router.pushUrl({ url: 'pages/QuizPage' });
        }
      })

      // 快捷入口
      this.QuickAccessSection()

      // 继续学习(仅当有学习记录时显示)
      if (this.progress.currentLesson) {
        this.ContinueLearningSection()
      }

      // 推荐模块
      this.RecommendedModulesSection()
    }
  }
  .width('100%')
  .height('100%')
  .scrollBar(BarState.Off)
}

15.4 快捷入口区域

功能设计

提供三个快捷入口:

  • 面试题库:跳转到面试题练习
  • 在线编程:跳转到代码练习
  • 成品下载:跳转到示例项目下载

实现代码

typescript 复制代码
@Builder
QuickAccessSection() {
  Column() {
    Text('快捷入口')
      .fontSize(18)
      .fontWeight(FontWeight.Bold)
      .fontColor(this.isDarkMode ? '#ffffff' : '#1a1a2e')
      .margin({ bottom: 12 })

    Row({ space: 12 }) {
      // 面试题库
      Column() {
        Text('🎯')
          .fontSize(32)
        Text('面试题库')
          .fontSize(13)
          .fontWeight(FontWeight.Medium)
          .fontColor(this.isDarkMode ? '#ffffff' : '#1a1a2e')
          .margin({ top: 6 })
        Text('海量面试真题')
          .fontSize(11)
          .fontColor(this.isDarkMode ? '#d1d5db' : '#495057')
          .margin({ top: 2 })
      }
      .layoutWeight(1)
      .padding(16)
      .backgroundColor(this.isDarkMode ? '#282c34' : '#ffffff')
      .borderRadius(16)
      .shadow({
        radius: 8,
        color: this.isDarkMode ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.05)',
        offsetY: 2
      })
      .onClick(() => {
        router.pushUrl({ url: 'pages/QuizBankPage' });
      })

      // 在线编程
      Column() {
        Text('💻')
          .fontSize(32)
        Text('在线编程')
          .fontSize(13)
          .fontWeight(FontWeight.Medium)
          .fontColor(this.isDarkMode ? '#ffffff' : '#1a1a2e')
          .margin({ top: 6 })
        Text('实战代码练习')
          .fontSize(11)
          .fontColor(this.isDarkMode ? '#d1d5db' : '#495057')
          .margin({ top: 2 })
      }
      .layoutWeight(1)
      .padding(16)
      .backgroundColor(this.isDarkMode ? '#282c34' : '#ffffff')
      .borderRadius(16)
      .shadow({
        radius: 8,
        color: this.isDarkMode ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.05)',
        offsetY: 2
      })
      .onClick(() => {
        router.pushUrl({ url: 'pages/CodePlayground' });
      })

      // 成品下载
      Column() {
        Text('📦')
          .fontSize(32)
        Text('成品下载')
          .fontSize(13)
          .fontWeight(FontWeight.Medium)
          .fontColor(this.isDarkMode ? '#ffffff' : '#1a1a2e')
          .margin({ top: 6 })
        Text('11个示例项目')
          .fontSize(11)
          .fontColor(this.isDarkMode ? '#d1d5db' : '#495057')
          .margin({ top: 2 })
      }
      .layoutWeight(1)
      .padding(16)
      .backgroundColor(this.isDarkMode ? '#282c34' : '#ffffff')
      .borderRadius(16)
      .shadow({
        radius: 8,
        color: this.isDarkMode ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.05)',
        offsetY: 2
      })
      .onClick(() => {
        router.pushUrl({ url: 'pages/DemoDownloadPage' });
      })
    }
    .width('100%')
  }
  .width('100%')
  .padding({ left: 16, right: 16, top: 20, bottom: 20 })
  .alignItems(HorizontalAlign.Start)
}

15.5 推荐模块横向滚动

功能设计

展示前 5 个推荐模块,支持横向滚动浏览。

实现代码

typescript 复制代码
@Builder
RecommendedModulesSection() {
  Column() {
    Row() {
      Text('推荐模块')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.isDarkMode ? '#ffffff' : '#1a1a2e')

      Blank()

      Text('查看全部 →')
        .fontSize(14)
        .fontColor(this.isDarkMode ? '#61DAFB' : '#0077b6')
        .onClick(() => {
          this.currentTab = 1;  // 切换到课程 Tab
        })
    }
    .width('100%')
    .margin({ bottom: 12 })

    // 横向滚动模块卡片
    Scroll() {
      Row({ space: 12 }) {
        ForEach(this.modules.slice(0, 5), (module: LearningModule) => {
          ModuleCard({
            module: module,
            progress: ProgressService.getCompletionPercentage(module, this.progress),
            isLocked: false,
            onTap: () => {
              router.pushUrl({
                url: 'pages/ModuleDetail',
                params: { moduleId: module.id }
              });
            }
          })
        })
      }
      .padding({ right: 16 })
    }
    .scrollable(ScrollDirection.Horizontal)
    .scrollBar(BarState.Off)
  }
  .width('100%')
  .padding({ left: 16, top: 20 })
  .alignItems(HorizontalAlign.Start)
}

横向滚动要点

  1. Scroll 组件 :设置 scrollable(ScrollDirection.Horizontal)
  2. 隐藏滚动条scrollBar(BarState.Off)
  3. 右侧留白 :Row 内部 padding({ right: 16 })

15.6 完整首页代码

Index.ets 完整实现

typescript 复制代码
/**
 * 首页
 * 底部 5 Tab 导航 + Hero Banner + 模块轮播
 */
import { router } from '@kit.ArkUI';
import { initTheme } from '../common/ThemeUtil';
import { StorageUtil } from '../common/StorageUtil';
import { TutorialService } from '../services/TutorialService';
import { ProgressService } from '../services/ProgressService';
import { SearchService } from '../services/SearchService';
import { BadgeService } from '../services/BadgeService';
import { LearningModule, UserProgress, DEFAULT_USER_PROGRESS } from '../models/Models';
import { HeroBanner } from '../components/HeroBanner';
import { ModuleCard } from '../components/ModuleCard';

@Entry
@Component
struct Index {
  @State currentTab: number = 0;
  @State modules: LearningModule[] = [];
  @State progress: UserProgress = DEFAULT_USER_PROGRESS;
  @State isLoading: boolean = true;
  @StorageLink('isDarkMode') isDarkMode: boolean = false;

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

  onPageShow(): void {
    this.refreshProgress();
  }

  private async initAndLoadData(): Promise<void> {
    try {
      await StorageUtil.init(getContext(this));
      initTheme(getContext(this));
      TutorialService.init();
      this.modules = TutorialService.getAllModules();
      this.progress = await ProgressService.loadProgress();
      SearchService.init(this.modules);
      BadgeService.init(this.modules);
    } catch (error) {
      console.error('[Index] Failed to load data:', error);
    } finally {
      this.isLoading = false;
    }
  }

  private async refreshProgress(): Promise<void> {
    try {
      this.progress = await ProgressService.loadProgress();
    } catch (error) {
      console.error('[Index] Failed to refresh progress:', error);
    }
  }

  build() {
    Column() {
      if (this.isLoading) {
        this.LoadingView()
      } else {
        Tabs({ barPosition: BarPosition.End, index: this.currentTab }) {
          TabContent() {
            this.HomeContent()
          }
          .tabBar(this.TabBuilder('首页', '🏠', 0))

          TabContent() {
            this.CourseContent()
          }
          .tabBar(this.TabBuilder('课程', '📚', 1))

          TabContent() {
            this.SourceCodeContent()
          }
          .tabBar(this.TabBuilder('源码', '📖', 2))

          TabContent() {
            this.ProjectContent()
          }
          .tabBar(this.TabBuilder('项目', '🌟', 3))

          TabContent() {
            this.ProfileContent()
          }
          .tabBar(this.TabBuilder('我的', '👤', 4))
        }
        .onChange((index: number) => {
          this.currentTab = index;
        })
        .barHeight(60)
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor(this.isDarkMode ? '#1a1a2e' : '#f8f9fa')
  }

  @Builder
  LoadingView() {
    Column() {
      LoadingProgress()
        .width(48)
        .height(48)
        .color(this.isDarkMode ? '#61DAFB' : '#0077b6')
      Text('加载中...')
        .fontSize(14)
        .fontColor(this.isDarkMode ? '#d1d5db' : '#495057')
        .margin({ top: 16 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  @Builder
  TabBuilder(title: string, icon: string, index: number) {
    Column() {
      Text(icon)
        .fontSize(24)
      Text(title)
        .fontSize(12)
        .fontColor(this.currentTab === index 
          ? (this.isDarkMode ? '#61DAFB' : '#0077b6') 
          : (this.isDarkMode ? '#d1d5db' : '#495057'))
        .margin({ top: 4 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  @Builder
  HomeContent() {
    Scroll() {
      Column() {
        HeroBanner({
          completedLessons: this.progress.completedLessons.length,
          totalLessons: TutorialService.getTotalLessonCount(),
          streak: this.progress.learningStreak,
          onDailyQuestionTap: () => {
            router.pushUrl({ url: 'pages/QuizPage' });
          }
        })

        this.QuickAccessSection()

        if (this.progress.currentLesson) {
          this.ContinueLearningSection()
        }

        this.RecommendedModulesSection()
      }
    }
    .width('100%')
    .height('100%')
    .scrollBar(BarState.Off)
  }

  // ... 其他 Builder 方法
}

本次课程小结

通过本次课程,你已经:

✅ 掌握了页面数据加载流程

✅ 实现了继续学习区域

✅ 完成了推荐模块横向滚动

✅ 实现了快捷入口功能

✅ 完成了首页的完整开发


课后练习

  1. 添加下拉刷新:实现下拉刷新进度数据

  2. 添加骨架屏:加载时显示骨架屏而非 Loading

  3. 添加动画效果:为模块卡片添加入场动画


下次预告

第16次:课程列表页面

我们将开发课程 Tab 的完整内容:

  • 按难度分组展示
  • 模块列表项设计
  • 课程数量与时长显示
  • 列表滚动优化

进入课程学习功能开发!

相关推荐
云和数据.ChenGuang4 小时前
鸿蒙智联,极智共生:HarmonyOS与MiniMax智能体的融合新纪元
华为·harmonyos·鸿蒙
不爱吃糖的程序媛4 小时前
已有 Flutter 应用适配鸿蒙平台指导文档
flutter·华为·harmonyos
大雷神4 小时前
HarmonyOS APP<玩转React>开源教程十六:课程列表页面
harmonyos
wAIxiSeu4 小时前
开源项目分享——CLI-Anything
开源·github
弓.长.5 小时前
ReactNative for OpenHarmony项目鸿蒙化三方库:react-native-video — 视频播放组件
react native·音视频·harmonyos
进击monkey5 小时前
2026 年 AI Wiki 推荐:PandaWiki——AI 原生+开源私有化,企业级知识库最优解
人工智能·开源·ai知识库
霪霖笙箫5 小时前
真授之以渔:我是怎么从"想给文章配几张图",一步步做出一个可发布 skill 的
前端·人工智能·开源
yzin5 小时前
【源码】【react】useCallback、useMemo、memo 原理
前端·react.js
坚果派·白晓明5 小时前
在 Ubuntu 中搭建鸿蒙 PC 三方库交叉编译构建开发环境
ubuntu·华为·harmonyos