HarmonyOS APP<玩转React>开源教程二十二:每日一题功能

第22次:每日一题功能

每日一题是激励用户持续学习的有效方式。本次课程将实现每日一题功能,包括题目选择算法、答题界面和答案解析。


效果

学习目标

  • 掌握每日一题算法设计
  • 实现 QuizPage 页面布局
  • 设计选项交互效果
  • 完成答案解析显示
  • 实现每日一题完整功能

22.1 每日一题算法

题目选择逻辑

根据日期确定性地选择题目,确保同一天所有用户看到相同的题目:

typescript 复制代码
static getDailyQuestion(): DailyQuestion {
  const today = new Date().toISOString().split('T')[0];  // 'YYYY-MM-DD'
  const questions = TutorialData.getAllQuizQuestions();
  
  // 根据日期计算索引
  const dateSum = today.split('-').reduce((a, b) => a + parseInt(b), 0);
  const index = Math.abs(dateSum) % questions.length;
  
  const q = questions[index];
  return {
    date: today,
    question: q.question,
    moduleId: q.moduleId,
    lessonId: q.lessonId,
    answered: false
  };
}

DailyQuestion 结构

typescript 复制代码
interface DailyQuestion {
  date: string;           // 日期
  question: QuizItem;     // 题目
  moduleId: string;       // 所属模块
  lessonId: string;       // 所属课程
  answered: boolean;      // 是否已答
}

22.2 QuizPage 页面布局

页面结构

typescript 复制代码
@Entry
@Component
struct QuizPage {
  @State question: DailyQuestion | null = null;
  @State selectedAnswer: number = -1;
  @State isSubmitted: boolean = false;
  @State isCorrect: boolean = false;
  @StorageLink('isDarkMode') isDarkMode: boolean = false;

  aboutToAppear(): void {
    this.question = TutorialService.getDailyQuestion();
  }
}

页面头部

typescript 复制代码
@Builder
HeaderSection() {
  Row() {
    Text('←')
      .fontSize(24)
      .fontColor(this.isDarkMode ? '#ffffff' : '#1a1a2e')
      .onClick(() => router.back())

    Text('每日一题')
      .fontSize(18)
      .fontWeight(FontWeight.Bold)
      .fontColor(this.isDarkMode ? '#ffffff' : '#1a1a2e')
      .margin({ left: 16 })

    Blank()

    Text(this.question?.date ?? '')
      .fontSize(14)
      .fontColor(this.isDarkMode ? '#d1d5db' : '#495057')
  }
  .width('100%')
  .padding(16)
}

22.3 题目展示

题目卡片

typescript 复制代码
@Builder
QuestionCard() {
  Column() {
    Text('📝')
      .fontSize(48)
      .margin({ bottom: 16 })

    Text(this.question?.question.question ?? '')
      .fontSize(18)
      .fontWeight(FontWeight.Medium)
      .fontColor(this.isDarkMode ? '#ffffff' : '#1a1a2e')
      .textAlign(TextAlign.Center)
  }
  .width('100%')
  .padding(24)
  .backgroundColor(this.isDarkMode ? '#282c34' : '#ffffff')
  .borderRadius(16)
  .margin({ left: 16, right: 16, top: 16 })
}

22.4 选项交互设计

选项列表

typescript 复制代码
@Builder
OptionsSection() {
  Column({ space: 12 }) {
    ForEach(
      this.question?.question.options ?? [], 
      (option: string, index: number) => {
        this.OptionItem(option, index)
      }
    )
  }
  .width('100%')
  .padding({ left: 16, right: 16, top: 24 })
}

选项项组件

typescript 复制代码
@Builder
OptionItem(option: string, index: number) {
  Row() {
    // 选项字母
    Text(String.fromCharCode(65 + index))  // A, B, C, D
      .fontSize(16)
      .fontWeight(FontWeight.Bold)
      .fontColor(this.getOptionLetterColor(index))
      .width(32)
      .height(32)
      .textAlign(TextAlign.Center)
      .backgroundColor(this.getOptionBgColor(index))
      .borderRadius(16)

    // 选项内容
    Text(option)
      .fontSize(15)
      .fontColor(this.isDarkMode ? '#ffffff' : '#1a1a2e')
      .margin({ left: 12 })
      .layoutWeight(1)

    // 结果图标
    if (this.isSubmitted) {
      if (index === this.question?.question.correctAnswer) {
        Text('✓')
          .fontSize(18)
          .fontColor('#51cf66')
      } else if (index === this.selectedAnswer) {
        Text('✗')
          .fontSize(18)
          .fontColor('#ff6b6b')
      }
    }
  }
  .width('100%')
  .padding(16)
  .backgroundColor(this.getOptionCardBg(index))
  .borderRadius(12)
  .border({
    width: 2,
    color: this.getOptionBorderColor(index)
  })
  .onClick(() => {
    if (!this.isSubmitted) {
      this.selectedAnswer = index;
    }
  })
}

颜色计算方法

typescript 复制代码
private getOptionBorderColor(index: number): string {
  if (this.isSubmitted) {
    if (index === this.question?.question.correctAnswer) {
      return '#51cf66';  // 正确答案绿色
    }
    if (index === this.selectedAnswer) {
      return '#ff6b6b';  // 错误选择红色
    }
  }
  if (index === this.selectedAnswer) {
    return '#61DAFB';    // 选中蓝色
  }
  return 'transparent';
}

22.5 答案解析显示

解析区域

typescript 复制代码
@Builder
ExplanationSection() {
  if (this.isSubmitted && this.question) {
    Column() {
      Row() {
        Text(this.isCorrect ? '🎉' : '💡')
          .fontSize(24)
        Text(this.isCorrect ? '回答正确!' : '答案解析')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(this.isDarkMode ? '#ffffff' : '#1a1a2e')
          .margin({ left: 8 })
      }
      .margin({ bottom: 12 })

      Text(this.question.question.explanation)
        .fontSize(14)
        .fontColor(this.isDarkMode ? '#e6e6e6' : '#333333')
        .lineHeight(22)
    }
    .width('100%')
    .padding(16)
    .margin({ left: 16, right: 16, top: 24 })
    .backgroundColor(this.isCorrect 
      ? (this.isDarkMode ? 'rgba(81,207,102,0.15)' : 'rgba(81,207,102,0.1)')
      : (this.isDarkMode ? 'rgba(97,218,251,0.15)' : 'rgba(97,218,251,0.1)'))
    .borderRadius(12)
    .alignItems(HorizontalAlign.Start)
  }
}

22.6 提交按钮

按钮实现

typescript 复制代码
@Builder
SubmitButton() {
  Button(this.isSubmitted ? '返回首页' : '提交答案')
    .width('90%')
    .height(48)
    .fontSize(16)
    .fontWeight(FontWeight.Medium)
    .backgroundColor(this.selectedAnswer === -1 && !this.isSubmitted
      ? (this.isDarkMode ? '#3d3d5c' : '#e9ecef')
      : '#61DAFB')
    .fontColor(this.selectedAnswer === -1 && !this.isSubmitted
      ? (this.isDarkMode ? '#9ca3af' : '#6c757d')
      : '#1a1a2e')
    .borderRadius(24)
    .margin({ top: 24 })
    .enabled(this.selectedAnswer !== -1 || this.isSubmitted)
    .onClick(() => {
      if (this.isSubmitted) {
        router.back();
      } else {
        this.submitAnswer();
      }
    })
}

提交逻辑

typescript 复制代码
private submitAnswer(): void {
  if (this.question && this.selectedAnswer !== -1) {
    this.isCorrect = this.selectedAnswer === this.question.question.correctAnswer;
    this.isSubmitted = true;
    
    // 更新连续学习天数
    ProgressService.updateStreak();
  }
}

22.7 完整页面代码

typescript 复制代码
@Entry
@Component
struct QuizPage {
  @State question: DailyQuestion | null = null;
  @State selectedAnswer: number = -1;
  @State isSubmitted: boolean = false;
  @State isCorrect: boolean = false;
  @StorageLink('isDarkMode') isDarkMode: boolean = false;

  aboutToAppear(): void {
    this.question = TutorialService.getDailyQuestion();
  }

  build() {
    Column() {
      this.HeaderSection()
      
      Scroll() {
        Column() {
          this.QuestionCard()
          this.OptionsSection()
          this.ExplanationSection()
          this.SubmitButton()
        }
        .padding({ bottom: 40 })
      }
      .layoutWeight(1)
      .scrollBar(BarState.Off)
    }
    .width('100%')
    .height('100%')
    .backgroundColor(this.isDarkMode ? '#1a1a2e' : '#f8f9fa')
  }

  // ... Builder 方法
}

本次课程小结

✅ 掌握了每日一题算法设计

✅ 实现了 QuizPage 页面布局

✅ 设计了选项交互效果

✅ 完成了答案解析显示


下次预告

第23次:面试题库功能 - 实现分类题库和练习模式

相关推荐
Coovally AI模型快速验证1 小时前
2.5GB 塞进浏览器:Mistral 开源实时语音识别,延迟不到半秒
人工智能·5g·计算机视觉·开源·语音识别
还是大剑师兰特1 小时前
Vue3 + Element Plus 日期选择器:开始 / 结束时间,结束时间不超过今天
前端·javascript·vue.js
2501_945837431 小时前
OpenClaw 核心:开源 AI 执行网关的技术内核
人工智能·开源
Robbie丨Yang1 小时前
前端工程构建优化实践指南
前端
Irene19911 小时前
前端序列化和反序列化总结(JSON.stringify 和 JSON.parse 的局限,自定义通用的安全序列化工具类)
前端
老星*1 小时前
Home Assistant:开源智能家居平台,打造全屋智能的中枢神经
开源·智能家居
Saga Two2 小时前
Vue实现核心原理
前端·javascript·vue.js
i建模2 小时前
开源AI memory项目一览表
人工智能·开源
进击monkey2 小时前
2026 企业知识库首选:PandaWiki AGPL-3.0 开源,无绑定+合规+降本三不误
人工智能·开源·ai知识库