🎉 initial commit

This commit is contained in:
2025-09-14 16:49:40 +08:00
commit ee0fa309ca
176 changed files with 16837 additions and 0 deletions

View 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>