615 lines
15 KiB
Vue
615 lines
15 KiB
Vue
<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>
|