🎉 initial commit
This commit is contained in:
614
src/pages/quiz/parent-quiz.vue
Normal file
614
src/pages/quiz/parent-quiz.vue
Normal file
@@ -0,0 +1,614 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user