Bootstrap

鸿蒙 OS 开发单词打卡 APP 项目实战 20240922 笔记和源码分享

配套有完整的录播课, 需要的私信.
在这里插入图片描述

零基础入门级别, 有点前端基础都能学会.

效果截图:
在这里插入图片描述

代码截图:
在这里插入图片描述

页面完整代码:

import { AnswerStatus } from '../enums/AnswerStatus'
import { PracticeStatus } from '../enums/PracticeStatus'
import { getRandomQuestions, Question } from '../model/Question'
import { promptAction } from '@kit.ArkUI'
import { OptionButton } from '../components/OptionButton'
import { StatItem } from '../components/StatItem'
import { ResultDialog } from '../components/ResultDialog'
import { trustedAppService } from '@kit.DeviceSecurityKit'

@Entry
@Component
struct PracticePage {
  // 练习状态
  @State status: PracticeStatus = PracticeStatus.STOPPED
  // 题目个数
  @State totalQuestion: number = 3
  // 题目数组
  @State questions: Question[] = getRandomQuestions(this.totalQuestion)
  // 当前题目的索引
  @State currentIndex: number = 0
  // 用户选中的选项
  @State selectedOption: string = ""
  // 作答状态
  @State answerStatus: AnswerStatus = AnswerStatus.Answering
  // 已作答个数
  @State answeredCount: number = 0
  // 答对的个数
  @State rightCount: number = 0
  // 控制定时器
  timerController = new TextTimerController()
  // 总用时时间
  @State totalTime: number = 0
  // 自定义的弹窗组件控制器
  dialogController: CustomDialogController = new CustomDialogController({
    builder: ResultDialog({
      answeredCount: this.answeredCount,
      rightCount: this.rightCount,
      totalTime: this.totalTime,
      onStartFunc: () => {
        this.status = PracticeStatus.RUNNING
        this.timerController.start()
      },
      onCloseFunc: () => {
        this.questions = getRandomQuestions(this.totalQuestion)
        this.currentIndex = 0
        this.answeredCount = 0
        this.rightCount = 0
        this.totalTime = 0
        this.timerController.reset()
        this.answerStatus = AnswerStatus.Answering
        this.status = PracticeStatus.STOPPED
      },
    }),
    customStyle: true, // 使用自定义样式, 否则那个 x 出不来
    autoCancel: false, // 点击空白区域不会被自动关闭
  })

  // 统计准确率
  getRightPercent() {
    if (this.rightCount === 0) {
      return "0%"
    }
    return `${((this.rightCount / this.answeredCount) * 100).toFixed()}%`
  }

  // 停止练习
  stopPractice() {
    this.status = PracticeStatus.STOPPED
    this.timerController.pause()
    this.dialogController.open()
  }

  build() {
    Column() {
      // 统计面板
      Column() {
        // 准确率
        StatItem({
          icon: $r("app.media.ic_accuracy"),
          name: "准确率",
          fontColor: Color.Black,
        }) {
          Text(this.getRightPercent())
            .width(100)
            .textAlign(TextAlign.Center)
        }

        // 进度
        StatItem({
          icon: $r("app.media.ic_progress"),
          name: "进度",
          fontColor: Color.Black,
        }) {
          Progress({ value: this.answeredCount, total: this.totalQuestion })
            .width(100)
        }

        // 题目个数
        StatItem({
          icon: $r("app.media.ic_count"),
          name: "个数",
          fontColor: Color.Black,
        }) {
          Button(this.totalQuestion.toString())
            .width(100)
            .height(25)
            .backgroundColor("#EBEBEB")
            .enabled(this.status === PracticeStatus.STOPPED)
            .onClick(() => {
              TextPickerDialog.show({
                range: ["5", "10", "20", "50", "100"],
                value: this.totalQuestion.toString(), // 默认值
                onAccept: (result) => {
                  this.totalQuestion = parseInt(result.value.toString())
                  this.questions = getRandomQuestions(this.totalQuestion)
                }
              })
            })
        }

        // 计时
        StatItem({
          icon: $r("app.media.ic_timer"),
          name: "用时",
          fontColor: Color.Black,
        }) {
          Row() {
            TextTimer({ controller: this.timerController })
              .onTimer((utc, elapsedTime) => {
                this.totalTime = elapsedTime
              })
          }.width(100)
          .justifyContent(FlexAlign.Center)
        }
      }.statBgStyle()

      // 题目
      Column() {
        Text(this.questions[this.currentIndex].word).wordStyle()
        Text(this.questions[this.currentIndex].sentence).sentenceStyle()
      }

      // 选项
      Column({ space: 15 }) {
        ForEach(
          this.questions[this.currentIndex].options,
          (item: string) => {
            OptionButton({
              option: item,
              answer: this.questions[this.currentIndex].answer,
              selectedOption: this.selectedOption,
              answerStatus: this.answerStatus,
            })
              .enabled(this.answerStatus === AnswerStatus.Answering)
              .onClick(() => {
                // 判断练习状态
                if (this.status !== PracticeStatus.RUNNING) {
                  promptAction.showToast({ message: "请先点击开始测试按钮" })
                  return
                }

                // 先将答题状态改为已作答
                this.answerStatus = AnswerStatus.Answered

                // 判断答案是否正确
                this.selectedOption = item
                this.answeredCount++
                if (this.questions[this.currentIndex].answer === this.selectedOption) {
                  this.rightCount++
                }

                // 判断题目状态
                if (this.currentIndex < this.questions.length - 1) {
                  setTimeout(() => {
                    this.currentIndex++
                    this.answerStatus = AnswerStatus.Answering
                  }, 500)
                } else {
                  // 停止测试
                  this.stopPractice()
                }
              })
          },
          (item: string) => this.questions[this.currentIndex].word + "_" + item,
        )
      }

      // 控制按钮
      Row({ space: 20 }) {
        Button("停止测试").controlButtonStyle(
          Color.Transparent,
          this.status === PracticeStatus.STOPPED ? Color.Gray : Color.Black,
          this.status === PracticeStatus.STOPPED ? Color.Gray : Color.Black,
        ).enabled(this.status !== PracticeStatus.STOPPED)
          .onClick(() => this.stopPractice())

        Button(this.status === PracticeStatus.RUNNING ? "暂停测试" : "开始测试")
          .controlButtonStyle(
            this.status === PracticeStatus.RUNNING ? "#666666" : Color.Black,
            this.status === PracticeStatus.RUNNING ? "#666666" : Color.Black,
            Color.White,
          )
          .stateEffect(false)
          .onClick(() => {
            if (this.status === PracticeStatus.RUNNING) {
              // 暂停测试
              this.status = PracticeStatus.PAUSED
              this.timerController.pause()
            } else {
              // 开始测试
              this.status = PracticeStatus.RUNNING
              this.timerController.start()
            }
          })
      }
    }.practiceBgStyle()
  }
}

// 页面背景
@Extend(Column)
function practiceBgStyle() {
  .width("100%")
  .height("100%")
  .backgroundImage($r("app.media.img_practice_bg"))
  .backgroundImageSize({ width: "100%", height: "100%" })
  .justifyContent(FlexAlign.SpaceEvenly)
}

// 统计面板背景
@Styles
function statBgStyle() {
  .backgroundColor(Color.White)
  .width("90%")
  .borderRadius(10)
  .padding(20)
}

// 单词样式
@Extend(Text)
function wordStyle() {
  .fontSize(50)
  .fontWeight(FontWeight.Bold)
}

// 例句样式
@Extend(Text)
function sentenceStyle() {
  .height(40)
  .fontSize(16)
  .fontColor("#9BA1A5")
  .fontWeight(FontWeight.Medium)
  .width("80%")
  .textAlign(TextAlign.Center)
}

// 控制按钮样式
@Extend(Button)
function controlButtonStyle(
  bgColor: ResourceColor,
  borderColor: ResourceColor,
  fontColor: ResourceColor,
) {
  .fontSize(16)
  .borderWidth(1)
  .backgroundColor(bgColor)
  .borderColor(borderColor)
  .fontColor(fontColor)
}

选项按钮组件完整代码:

import { AnswerStatus } from '../enums/AnswerStatus'
import { OptionStatus } from '../enums/OptionStatus'

@Component
export struct OptionButton {
  // 选项内容
  option: string = ""
  // 答案
  answer: string = ""
  // 选项状态
  @State optionStatus: OptionStatus = OptionStatus.DEFAULT
  // 用户选中的选项
  @Prop selectedOption: string = ""
  // 属性
  @Prop @Watch("onAnswerStatusChange") answerStatus: AnswerStatus = AnswerStatus.Answering

  // 监听器方法
  onAnswerStatusChange() {
    if (this.option === this.answer) {
      // 答案正确
      this.optionStatus = OptionStatus.RIGHT
    } else {
      if (this.option === this.selectedOption) {
        // 如果当前选项按钮是被选中但错误的按钮
        this.optionStatus = OptionStatus.ERROR
      } else {
        this.optionStatus = OptionStatus.DEFAULT
      }
    }
  }

  // 获取背景颜色
  getBgColor() {
    switch (this.optionStatus) {
      case OptionStatus.RIGHT:
        return "#1DBF7B"
      case OptionStatus.ERROR:
        return "#FA635F"
      default:
        return Color.White
    }
  }

  build() {
    Stack() {
      Button(this.option)
        .optionButtonStyle(
          this.getBgColor(), // 动态获取背景颜色
          this.optionStatus === OptionStatus.DEFAULT ? Color.Black : Color.White,
        )
      // 根据状态设置不同的图标
      if (this.optionStatus === OptionStatus.RIGHT) {
        Image($r("app.media.ic_right"))
          .width(22)
          .height(22)
          .offset({ x: 10 })
      } else if (this.optionStatus === OptionStatus.ERROR) {
        Image($r("app.media.ic_wrong"))
          .width(22)
          .height(22)
          .offset({ x: 10 })
      }
    }.alignContent(Alignment.Start)
  }
}

// 选项按钮样式
@Extend(Button)
function optionButtonStyle(bgColor: ResourceColor, fontColor: ResourceColor) {
  .width(240)
  .height(48)
  .fontSize(16)
  .type(ButtonType.Normal)
  .fontWeight(FontWeight.Medium)
  .borderRadius(8)
  .backgroundColor(bgColor)
  .fontColor(fontColor)
}

弹窗组件完整代码:

import { millisecondsToTimeStr } from '../utils/DateUtil'
import { StatItem } from './StatItem'

@CustomDialog
export struct ResultDialog {
  answeredCount: number = 0 // 已答题个数
  rightCount: number = 0 // 正确个数
  totalTime: number = 0 // 总计耗时
  // 再来一局开始执行的函数
  onStartFunc: () => void = () => {
  }
  // 在关闭弹窗时触发方法
  onCloseFunc: () => void = () => {
  }
  // 弹窗控制器
  controller: CustomDialogController = new CustomDialogController({
    builder: ResultDialog()
  })

  // 统计准确率
  getRightPercent() {
    if (this.rightCount === 0) {
      return "0%"
    }
    return `${((this.rightCount / this.answeredCount) * 100).toFixed()}%`
  }

  build() {
    Column({ space: 10 }) {
      // 右上角有个 X 的按钮
      Image($r("app.media.ic_close"))
        .width(25)
        .height(25)
        .alignSelf(ItemAlign.End)
        .onClick(() => {
          this.controller.close() // 关闭弹窗
          this.onCloseFunc() // 触发关闭的函数
        })

      // 主体内容
      Column({ space: 10 }) {
        // 图片
        Image($r("app.media.img_post"))
          .width("100%")
          .borderRadius(10)

        // 用时
        StatItem({
          icon: $r("app.media.ic_timer"),
          name: "用时",
          fontColor: Color.Black
        }) {
          Text(millisecondsToTimeStr(this.totalTime))
        }

        // 准确率
        StatItem({
          icon: $r("app.media.ic_accuracy"),
          name: "准确率",
          fontColor: Color.Black
        }) {
          Text(this.getRightPercent())
        }

        // 个数
        StatItem({
          icon: $r("app.media.ic_count"),
          name: "个数",
          fontColor: Color.Black
        }) {
          Text(this.answeredCount.toString())
        }

        // 分割线
        Divider()

        // 控制按钮
        Row({ space: 30 }) {
          Button("再来一局")
            .controlButtonStyle(
              Color.Transparent,
              Color.Black,
              Color.Black,
            )
            .onClick(() => {
              this.controller.close()
              this.onCloseFunc() // 先关闭
              this.onStartFunc() // 再启动
            })


          Button("登录打卡")
            .controlButtonStyle(
              Color.Black,
              Color.Black,
              Color.White,
            )
            .onClick(() => {
              this.controller.close()
              this.onCloseFunc() // 先关闭
              // TODO: 登录并打卡
            })
        }
      }
      .backgroundColor(Color.White)
      .width("100%")
      .padding(20)
      .borderRadius(10)
    }
    .backgroundColor(Color.Transparent)
    .width("80%")
  }
}

// 控制按钮样式
@Extend(Button)
function controlButtonStyle(
  bgColor: ResourceColor,
  borderColor: ResourceColor,
  fontColor: ResourceColor,
) {
  .fontSize(16)
  .borderWidth(1)
  .backgroundColor(bgColor)
  .borderColor(borderColor)
  .fontColor(fontColor)
}

代码比较多, 需要整套完整代码的可以私信我获取.

;