鸿蒙 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.RUNNINGthis.timerController.start()},onCloseFunc: () => {this.questions = getRandomQuestions(this.totalQuestion)this.currentIndex = 0this.answeredCount = 0this.rightCount = 0this.totalTime = 0this.timerController.reset()this.answerStatus = AnswerStatus.Answeringthis.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.STOPPEDthis.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 = itemthis.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.PAUSEDthis.timerController.pause()} else {// 开始测试this.status = PracticeStatus.RUNNINGthis.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)
}
代码比较多, 需要整套完整代码的可以私信我获取.