配套有完整的录播课, 需要的私信.
零基础入门级别, 有点前端基础都能学会.
效果截图:
代码截图:
页面完整代码:
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)
}
代码比较多, 需要整套完整代码的可以私信我获取.