Files
zhuzi-uniapp/src/pages/quiz/parent-quiz.vue
2025-09-14 16:49:47 +08:00

615 lines
15 KiB
Vue
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.
<route lang="json">
{
"style": {
"navigationBarTitleText": "家长答题",
"navigationBarBackgroundColor": "#2D5E3E",
"navigationBarTextStyle": "white"
}
}
</route>
<template>
<view class="quiz-container">
<!-- 答题进度和倒计时 -->
<view class="quiz-header">
<view class="progress-info">
<text class="question-number">{{ currentQuestionIndex + 1 }}/{{ totalQuestions }}</text>
<view class="progress-bar">
<view
class="progress-fill"
:style="{ width: `${progressWidth}%` }"
/>
</view>
</view>
<view class="timer-container" :class="{ warning: timeLeft <= 10 }">
<uni-icons type="clock" size="16" color="#FFFFFF" />
<text class="timer-text">{{ timeLeft }}s</text>
</view>
</view>
<!-- 题目卡片 -->
<ZhuziCard v-if="currentQuestion" class="question-card">
<view class="question-content">
<!-- 题目类型和分值 -->
<view class="question-meta">
<text class="question-type">{{ currentQuestion.type === 'single' ? '单选题' : '多选题' }}</text>
<text class="question-score">{{ currentQuestion.score }}</text>
</view>
<!-- 题目标题 -->
<view class="question-title">
<text>{{ currentQuestion.title }}</text>
</view>
<!-- 选项列表 -->
<view class="options-container">
<view
v-for="option in currentQuestion.options"
:key="option.id"
class="option-item"
:class="{
selected: selectedAnswers.includes(option.id),
correct: showAnswer && currentQuestion.correctAnswers.includes(option.id),
incorrect: showAnswer && selectedAnswers.includes(option.id) && !currentQuestion.correctAnswers.includes(option.id),
}"
@tap="selectOption(option.id)"
>
<view class="option-content">
<view class="option-icon">
<view
v-if="currentQuestion.type === 'single'"
class="radio-icon"
:class="{ checked: selectedAnswers.includes(option.id) }"
/>
<view
v-else
class="checkbox-icon"
:class="{ checked: selectedAnswers.includes(option.id) }"
>
<uni-icons v-if="selectedAnswers.includes(option.id)" type="checkmarkempty" size="14" color="#FFFFFF" />
</view>
</view>
<text class="option-text">{{ option.text }}</text>
</view>
</view>
</view>
<!-- 多选题确认按钮 -->
<AncientButton
v-if="currentQuestion.type === 'multiple' && !showAnswer"
class="confirm-btn"
:disabled="selectedAnswers.length === 0"
@click="confirmAnswer"
>
确认答案
</AncientButton>
</view>
</ZhuziCard>
<!-- 答题反馈 -->
<view v-if="showFeedback" class="feedback-overlay" @tap="nextQuestion">
<view class="feedback-content">
<uni-icons
:type="isCorrect ? 'checkmarkempty' : 'closeempty'"
:size="80"
:color="isCorrect ? '#32CD32' : '#FF4444'"
class="feedback-icon"
/>
<text class="feedback-text">{{ isCorrect ? '回答正确!' : '很遗憾,答错了' }}</text>
<text v-if="isCorrect" class="score-text">+{{ currentQuestion?.score }}</text>
<text class="next-hint">点击任意处继续</text>
</view>
</view>
<!-- 超时提示 -->
<view v-if="showTimeout" class="timeout-overlay" @tap="nextQuestion">
<view class="timeout-content">
<uni-icons type="clock" size="80" color="#FF6B6B" />
<text class="timeout-text">时间到了</text>
<text class="timeout-hint">点击任意处继续</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import type { QuizQuestion } from '@/mocks/quiz'
import { computed, onUnmounted, ref } from 'vue'
import { mockParentQuestions } from '@/mocks/quiz'
// 响应式数据
const questions = ref<QuizQuestion[]>([...mockParentQuestions])
const currentQuestionIndex = ref(0)
const selectedAnswers = ref<string[]>([])
const showAnswer = ref(false)
const showFeedback = ref(false)
const showTimeout = ref(false)
const timeLeft = ref(30)
const totalScore = ref(0)
const correctCount = ref(0)
const timer = ref<number | null>(null)
const startTime = ref<number>(Date.now())
// 计算属性
const totalQuestions = computed(() => questions.value.length)
const currentQuestion = computed(() => questions.value[currentQuestionIndex.value])
const progressWidth = computed(() => ((currentQuestionIndex.value + 1) / totalQuestions.value) * 100)
const isCorrect = ref(false)
// 开始倒计时
function startTimer() {
if (timer.value)
clearInterval(timer.value)
timeLeft.value = 30
timer.value = setInterval(() => {
timeLeft.value--
if (timeLeft.value <= 0) {
// 时间到了
handleTimeout()
}
}, 1000) as unknown as number
}
// 处理超时
function handleTimeout() {
if (timer.value) {
clearInterval(timer.value)
timer.value = null
}
showTimeout.value = true
// 2秒后自动进入下一题
setTimeout(() => {
nextQuestion()
}, 2000)
}
// 选择选项
function selectOption(optionId: string) {
if (showAnswer.value || showFeedback.value || showTimeout.value)
return
if (currentQuestion.value?.type === 'single') {
selectedAnswers.value = [optionId]
// 单选题立即确认答案
setTimeout(() => {
confirmAnswer()
}, 300)
}
else {
// 多选题切换选择状态
const index = selectedAnswers.value.indexOf(optionId)
if (index > -1) {
selectedAnswers.value.splice(index, 1)
}
else {
selectedAnswers.value.push(optionId)
}
}
}
// 确认答案
function confirmAnswer() {
if (showAnswer.value || !currentQuestion.value)
return
if (timer.value) {
clearInterval(timer.value)
timer.value = null
}
showAnswer.value = true
// 检查答案是否正确
const correctAnswers = currentQuestion.value.correctAnswers
const isAnswerCorrect = selectedAnswers.value.length === correctAnswers.length
&& selectedAnswers.value.every(answer => correctAnswers.includes(answer))
isCorrect.value = isAnswerCorrect
if (isAnswerCorrect) {
totalScore.value += currentQuestion.value.score
correctCount.value++
}
// 显示反馈
setTimeout(() => {
showAnswer.value = false
showFeedback.value = true
// 3秒后自动进入下一题
setTimeout(() => {
if (showFeedback.value) {
nextQuestion()
}
}, 3000)
}, 1500)
}
// 下一题
function nextQuestion() {
showFeedback.value = false
showTimeout.value = false
selectedAnswers.value = []
if (currentQuestionIndex.value < totalQuestions.value - 1) {
currentQuestionIndex.value++
startTimer()
}
else {
// 答题完成,跳转到结果页
finishQuiz()
}
}
// 完成答题
function finishQuiz() {
const endTime = Date.now()
const timeSpent = Math.floor((endTime - startTime.value) / 1000)
const result = {
type: 'parent' as const,
totalScore: totalScore.value,
correctCount: correctCount.value,
totalQuestions: totalQuestions.value,
timeSpent,
questions: questions.value,
}
// 保存结果到本地存储
uni.setStorageSync('quizResult', result)
// 跳转到结果页
uni.redirectTo({
url: '/pages/quiz/result',
})
}
// 监听页面返回
function handleBackPress() {
uni.showModal({
title: '确认退出',
content: '答题尚未完成,确定要退出吗?',
success: (res) => {
if (res.confirm) {
if (timer.value) {
clearInterval(timer.value)
timer.value = null
}
uni.navigateBack()
}
},
})
return true // 阻止默认返回行为
}
// 页面加载时开始答题
onLoad(() => {
// 随机打乱题目顺序
questions.value = questions.value.sort(() => Math.random() - 0.5)
startTime.value = Date.now()
startTimer()
})
// 页面卸载时清理定时器
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value)
timer.value = null
}
})
// 监听页面隐藏和显示
onHide(() => {
if (timer.value) {
clearInterval(timer.value)
timer.value = null
}
})
onShow(() => {
if (!showFeedback.value && !showTimeout.value && !showAnswer.value && timeLeft.value > 0) {
startTimer()
}
})
// 监听返回按钮
uni.onBackPress?.(handleBackPress)
</script>
<style lang="scss" scoped>
.quiz-container {
min-height: 100vh;
background: linear-gradient(to bottom, #e6f3e6 0%, #d4f0d4 100%);
padding: 32rpx;
.quiz-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
.progress-info {
flex: 1;
margin-right: 32rpx;
.question-number {
font-size: 28rpx;
font-weight: bold;
color: #2d5e3e;
margin-bottom: 16rpx;
display: block;
}
.progress-bar {
height: 8rpx;
background: rgba(45, 94, 62, 0.2);
border-radius: 4rpx;
overflow: hidden;
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #2d5e3e 0%, #daa520 100%);
transition: width 0.3s ease;
}
}
}
.timer-container {
display: flex;
align-items: center;
gap: 8rpx;
background: #2d5e3e;
padding: 12rpx 20rpx;
border-radius: 20rpx;
transition: all 0.3s ease;
&.warning {
background: #ff4444;
animation: pulse 1s infinite;
}
.timer-text {
font-size: 28rpx;
font-weight: bold;
color: #ffffff;
}
}
}
.question-card {
margin-bottom: 32rpx;
.question-content {
.question-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
.question-type {
background: rgba(45, 94, 62, 0.1);
color: #2d5e3e;
font-size: 24rpx;
padding: 8rpx 16rpx;
border-radius: 16rpx;
font-weight: bold;
}
.question-score {
background: linear-gradient(135deg, #daa520 0%, #ffd700 100%);
color: white;
font-size: 24rpx;
font-weight: bold;
padding: 8rpx 16rpx;
border-radius: 16rpx;
}
}
.question-title {
font-size: 32rpx;
font-weight: bold;
color: #2d5e3e;
line-height: 1.5;
margin-bottom: 48rpx;
}
.options-container {
.option-item {
margin-bottom: 24rpx;
.option-content {
display: flex;
align-items: center;
padding: 32rpx;
background: rgba(255, 255, 255, 0.8);
border: 2rpx solid transparent;
border-radius: 16rpx;
transition: all 0.3s ease;
cursor: pointer;
.option-icon {
margin-right: 24rpx;
.radio-icon {
width: 40rpx;
height: 40rpx;
border: 3rpx solid #cccccc;
border-radius: 50%;
position: relative;
transition: all 0.3s ease;
&.checked {
border-color: #daa520;
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 20rpx;
height: 20rpx;
background: #daa520;
border-radius: 50%;
transform: translate(-50%, -50%);
}
}
}
.checkbox-icon {
width: 40rpx;
height: 40rpx;
border: 3rpx solid #cccccc;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
&.checked {
background: #daa520;
border-color: #daa520;
}
}
}
.option-text {
flex: 1;
font-size: 28rpx;
color: #2f4f4f;
line-height: 1.5;
}
}
&.selected .option-content {
background: rgba(218, 165, 32, 0.1);
border-color: #daa520;
}
&.correct .option-content {
background: rgba(50, 205, 50, 0.1);
border-color: #32cd32;
}
&.incorrect .option-content {
background: rgba(255, 68, 68, 0.1);
border-color: #ff4444;
}
}
}
.confirm-btn {
width: 100%;
margin-top: 32rpx;
}
}
}
// 反馈弹窗
.feedback-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
.feedback-content {
background: white;
border-radius: 24rpx;
padding: 64rpx 48rpx;
text-align: center;
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.2);
animation: slideInUp 0.4s ease-out;
.feedback-icon {
margin-bottom: 32rpx;
}
.feedback-text {
display: block;
font-size: 32rpx;
font-weight: bold;
color: #2d5e3e;
margin-bottom: 16rpx;
}
.score-text {
display: block;
font-size: 28rpx;
color: #daa520;
font-weight: bold;
margin-bottom: 24rpx;
}
.next-hint {
font-size: 24rpx;
color: #696969;
}
}
}
// 超时弹窗
.timeout-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 68, 68, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
.timeout-content {
text-align: center;
animation: pulse 0.6s ease-in-out;
.timeout-text {
display: block;
font-size: 40rpx;
font-weight: bold;
color: white;
margin: 32rpx 0 16rpx;
text-shadow: 2rpx 2rpx 4rpx rgba(0, 0, 0, 0.3);
}
.timeout-hint {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.9);
}
}
}
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
@keyframes slideInUp {
from {
transform: translateY(100rpx);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
</style>