Files
hospital-rescue-app-v2/pages/course/quiz.uvue
T
2026-09-24 16:25:22 +08:00

397 lines
9.0 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" :nav-title="'顺序练习 · 第 ' + (cur + 1) + ' 题'"
@right-click="onMarkCurrent">
<template v-slot:right>
<x-icon :name="isMarked ? 'bookmark-fill' : 'bookmark-line'" :color="isMarked ? '#F59E0B' : '#6B7280'"
font-size="19"></x-icon>
</template>
<view class="hr-body">
<!-- ============ 进度 ============ -->
<view class="hr-row">
<view style="flex:1">
<hr-progress :percent="pct"></hr-progress>
</view>
<text class="qz-prog">{{ cur + 1 }} / {{ total }}</text>
</view>
<!-- ============ 题卡 ============ -->
<hr-card style="margin-top:10px;padding:16px 14px 16px 14px">
<text class="qz-stem">{{ q.stem }}</text>
<view class="qz-opts">
<view v-for="(o, i) in q.options" :key="i" :class="optClass(i)" @click="choose(i)">
<view :class="optKeyClass(i)">
<text :class="optKeyTextClass(i)">{{ o.key }}</text>
</view>
<text class="qz-opt-t">{{ o.text }}</text>
</view>
</view>
<view class="qz-acts">
<view class="hr-btn hr-btn-ghost qz-act" @click="onMarkCurrent">
<x-icon :name="isMarked ? 'bookmark-fill' : 'bookmark-line'"
:color="isMarked ? '#F59E0B' : '#1F2937'" font-size="16"></x-icon>
<text class="qz-act-t">{{ isMarked ? '已标记' : '标记本题' }}</text>
</view>
<view class="hr-btn hr-btn-ghost qz-act qz-act-last" @click="openCard">
<x-icon name="layout-grid-line" color="#1F2937" font-size="16"></x-icon>
<text class="qz-act-t">答题卡</text>
</view>
</view>
</hr-card>
<hr-tip style="margin-top:10px" theme="info" icon=""
text="答错的题目会自动进入错题本,可在课题功能页统一复习。"></hr-tip>
<!-- 作答反馈 -->
<view v-if="showAnswer" class="qz-feedback" :class="isRight ? 'qz-feedback-ok' : 'qz-feedback-err'">
<view class="qz-fb-ic" :class="isRight ? 'qz-fb-ic-ok' : 'qz-fb-ic-err'">
<text class="qz-fb-ic-t">{{ isRight ? '✓' : '✕' }}</text>
</view>
<view class="hr-flex1">
<text class="qz-fb-b">{{ isRight ? '回答正确' : '回答错误,已加入错题本' }}</text>
<text class="qz-fb-s">正确答案:{{ rightAnswer }}</text>
<text class="qz-fb-s">{{ q.analysis }}</text>
</view>
</view>
<view style="height:14px"></view>
</view>
<template v-slot:footer>
<view class="hr-fixedbar">
<view class="hr-btn hr-btn-ghost qz-foot-prev" @click="prev">
<text class="qz-foot-t">上一题</text>
</view>
<view class="hr-btn hr-btn-primary" style="flex:1" @click="next">
<text class="qz-foot-t2">{{ isLast ? '交卷并查看成绩' : '下一题' }}</text>
</view>
</view>
</template>
</hr-page>
</template>
<script lang="uts" setup>
import { ref, computed } from 'vue'
import { quizQuestions } from '@/mock/index.uts'
import { quizState, setAnswer, toggleMark, answeredCount } from '@/utils/quiz.uts'
import { relaunchTo, navTo } from '@/utils/nav.uts'
import { HrQuestion } from '@/mock/types.uts'
/** s03 答题界面 */
const total = quizQuestions.length
const cur = ref(0)
const refreshKey = ref(0)
const q = computed(() : HrQuestion => {
return quizQuestions[cur.value]
})
const selected = computed(() : number => {
const k = refreshKey.value
return quizState.answers[cur.value]
})
const isMarked = computed(() : boolean => {
const k = refreshKey.value
return quizState.marked[cur.value]
})
const isLast = computed(() : boolean => {
return cur.value == total - 1
})
const pct = computed(() : number => {
return Math.round((cur.value + 1) * 100 / total)
})
const showAnswer = computed(() : boolean => {
const k = refreshKey.value
return quizState.answers[cur.value] >= 0
})
const isRight = computed(() : boolean => {
const k = refreshKey.value
return quizState.answers[cur.value] == q.value.answer
})
const rightAnswer = computed(() : string => {
const o = q.value.options[q.value.answer]
return `${o.key}. ${o.text}`
})
// uni-app x 页面显示生命周期:从答题卡返回时同步当前题号与作答状态
onPageShow(() => {
cur.value = quizState.current
refreshKey.value = refreshKey.value + 1
})
function optClass(i : number) : string {
if (selected.value == i) {
if (i == q.value.answer) { return 'qz-opt qz-opt-ok' }
return 'qz-opt qz-opt-sel'
}
if (showAnswer.value && i == q.value.answer) { return 'qz-opt qz-opt-ok' }
return 'qz-opt'
}
function optKeyClass(i : number) : string {
if (selected.value == i) {
if (i == q.value.answer) { return 'qz-ok qz-ok-ok' }
return 'qz-ok qz-ok-sel'
}
if (showAnswer.value && i == q.value.answer) { return 'qz-ok qz-ok-ok' }
return 'qz-ok'
}
function optKeyTextClass(i : number) : string {
if (selected.value == i || (showAnswer.value && i == q.value.answer)) {
return 'qz-ok-t qz-ok-t-on'
}
return 'qz-ok-t'
}
function choose(i : number) : void {
if (selected.value >= 0) {
uni.showToast({ title: '本题已作答', icon: 'none' })
return
}
setAnswer(cur.value, i)
refreshKey.value = refreshKey.value + 1
if (i != q.value.answer) {
// 答错自动进错题本(mock 阶段仅提示)
uni.showToast({ title: '已加入错题本', icon: 'none' })
}
}
function onMarkCurrent() : void {
toggleMark(cur.value)
refreshKey.value = refreshKey.value + 1
uni.showToast({ title: isMarked.value ? '已标记本题' : '已取消标记', icon: 'none' })
}
function prev() : void {
if (cur.value == 0) {
uni.showToast({ title: '已是第一题', icon: 'none' })
return
}
cur.value = cur.value - 1
quizState.current = cur.value
refreshKey.value = refreshKey.value + 1
}
// ⚠ submit 被 next() 调用,必须先定义(UTS 不提升函数声明)
function submit() : void {
const done = answeredCount()
uni.showModal({
title: '交卷确认',
content: `已作答 ${done} / ${total} 题${done < total ? ',仍有题目未作答' : ''}。交卷后无法修改。`,
cancelText: '继续答题',
confirmText: '交卷',
success: (res) => {
if (res.confirm) {
quizState.submitted = true
uni.showToast({ title: '已交卷 · 得分 92', icon: 'success' })
setTimeout(() => {
relaunchTo('/pages/study/exams')
}, 700)
}
}
})
}
function next() : void {
if (isLast.value) {
submit()
return
}
cur.value = cur.value + 1
quizState.current = cur.value
refreshKey.value = refreshKey.value + 1
}
function openCard() : void {
quizState.current = cur.value
navTo('/pages/course/answer-card')
}
</script>
<style>
.qz-prog {
font-size: 25.8rpx;
font-weight: bold;
color: #6B7280;
margin-left: 20rpx;
}
.qz-stem {
font-size: 33.6rpx;
font-weight: bold;
color: #1F2937;
line-height: 1.6;
}
.qz-opts {
flex-direction: column;
margin-top: 30rpx;
}
.qz-opt {
display: flex;
flex-direction: row;
align-items: center;
min-height: 112rpx;
padding: 20rpx 26rpx 20rpx 26rpx;
border: 3rpx solid #E5E7EB;
border-radius: 24rpx;
background-color: #FFFFFF;
margin-bottom: 18rpx;
}
.qz-opt-sel {
border: 3rpx solid #2563EB;
background-color: #EFF6FF;
}
.qz-opt-ok {
border: 3rpx solid #10B981;
background-color: #ECFDF5;
}
.qz-ok {
width: 52rpx;
height: 52rpx;
border-radius: 26rpx;
border: 3rpx solid #E5E7EB;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-right: 22rpx;
}
.qz-ok-sel {
background-color: #2563EB;
border: 3rpx solid #2563EB;
}
.qz-ok-ok {
background-color: #10B981;
border: 3rpx solid #10B981;
}
.qz-ok-t {
font-size: 26.8rpx;
font-weight: bold;
color: #6B7280;
}
.qz-ok-t-on {
color: #FFFFFF;
}
.qz-opt-t {
flex: 1;
font-size: 30.2rpx;
color: #1F2937;
}
.qz-acts {
display: flex;
flex-direction: row;
margin-top: 30rpx;
}
.qz-act {
flex: 1;
height: 88rpx;
margin-right: 20rpx;
}
.qz-act-last {
margin-right: 0rpx;
}
.qz-act-t {
font-size: 30.2rpx;
font-weight: bold;
color: #1F2937;
margin-left: 12rpx;
}
.qz-feedback {
display: flex;
flex-direction: row;
align-items: flex-start;
margin-top: 20rpx;
padding: 22rpx 24rpx 22rpx 24rpx;
border-radius: 22rpx;
}
.qz-feedback-ok {
background-color: #ECFDF5;
border: 2rpx solid #A7F3D0;
}
.qz-feedback-err {
background-color: #FEF2F2;
border: 2rpx solid #FECACA;
}
.qz-fb-ic {
width: 52rpx;
height: 52rpx;
border-radius: 16rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.qz-fb-ic-ok {
background-color: #10B981;
}
.qz-fb-ic-err {
background-color: #DC2626;
}
.qz-fb-ic-t {
font-size: 29.2rpx;
font-weight: bold;
color: #FFFFFF;
}
.qz-fb-b {
font-size: 28rpx;
font-weight: bold;
color: #1F2937;
}
.qz-fb-s {
font-size: 24.6rpx;
color: #6B7280;
margin-top: 6rpx;
}
.qz-foot-prev {
flex: 0 0 220rpx;
margin-right: 18rpx;
}
.qz-foot-t {
font-size: 31.4rpx;
font-weight: bold;
color: #1F2937;
}
.qz-foot-t2 {
font-size: 32.4rpx;
font-weight: bold;
color: #FFFFFF;
}
</style>