1.每一道题都从后端去请求数据
2.选择完之后请求数据拿到结果 反馈用户
3.记录错题 做完计算正确率
<template>
<view class="exercise-content">
<baseHead title="练习题" @toBack="goHome" />
<!-- 题目内容区域 -->
<view class="question-container" @touchstart="touchStart" @touchend="touchEnd">
<view
class="question-card"
:class="[slideDirection]"
:style="{ transform: `translateX(${translateX}px)`, transition: isAnimating ? 'transform 0.3s ease' : 'none' }"
>
<!-- 题目序号 -->
<view class="question-number">第{{ currentIndex }}题</view>
<!-- 题目内容 -->
<view class="question-content">
<text>{{ currentQuestion?.content }}</text>
</view>
<!-- 选项列表 -->
<view class="options-list">
<view
v-for="(option, index) in currentQuestion.options"
:key="index"
class="option-item"
:class="{ selected: selectedOption === option.optionKey }"
@click="selectOption(option.optionKey)"
>
<image
:src="getSpecImgUrl('problem/check.png')"
v-if="selectedOption === option.optionKey && selectedOption === correctOption"
mode="scaleToFill"
/>
<image :src="getSpecImgUrl('problem/fork.png')" v-else-if="selectedOption === option.optionKey && correctOption" mode="scaleToFill" />
<text v-else class="option-label">{{ option.optionKey }}</text>
<text class="option-text">{{ option.optionContent }}</text>
</view>
</view>
<!-- 下一题按钮 right_icon-->
<view class="next-btn">
<view @click="nextQuestion">
<text>下一题</text>
<image :src="getSpecImgUrl('problem/right_icon.png')" mode="scaleToFill" />
</view>
</view>
</view>
</view>
<view class="question-result" v-if="selectedOption && !isAnimating">
<view class="mr-40rpx">
<text class="label">答案:</text>
<text class="c-#087BFF">{{ correctOption }}</text>
</view>
<view>
<text class="label">您选择:</text>
<text :class="correctOption == selectedOption ? 'c-#087BFF' : 'c-#E75D5C'">{{ selectedOption }}</text>
</view>
</view>
<BottomOperation :height="100">
<view class="bottom-stats">
<view class="correct-count">
<image :src="getSpecImgUrl('problem/true_icon.png')" mode="scaleToFill" />
<text>{{ correctCount }}</text>
</view>
<view class="line"></view>
<view class="wrong-count">
<image :src="getSpecImgUrl('problem/false_icon.png')" mode="scaleToFill" />
<text>{{ wrongCount }}</text>
</view>
</view>
</BottomOperation>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { debounce } from '@/utils/index'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { getSpecImgUrl } from '@/config/app'
import baseHead from '@/components/base-head/base-head.vue'
import BottomOperation from '@/components/bottom-operation/bottom-operation.vue'
import { getQuestionApi, answerApi } from '@/api/modules/plate_management'
// 动画相关状态
const translateX = ref(0)
const isAnimating = ref(false)
const slideDirection = ref('')
const loading = ref(false)
const examId = ref()
const paramsVal = ref()
onLoad((options: any) => {
examId.value = options.examId
paramsVal.value = JSON.parse(options.params)
currentIndex.value = paramsVal.value.lastQuestionNum || 1
getQuestion()
})
// 获取选项
const getQuestion = () => {
loading.value = true
const params = {
examId: examId.value,
sortNum: currentIndex.value
}
getQuestionApi(params).then((res) => {
if (res.code == 200) {
currentQuestion.value = res.data
if (res.data.userAnswer && res.data.userAnswer.length > 0) {
selectedOption.value = res.data.userAnswer[0]
const current = res.data.options.find((val) => val.isCorrect)
correctOption.value = current.optionKey
}
}
loading.value = false
})
}
let timer
//作答
const answer = (optionKey) => {
const pramas = {
id: currentQuestion.value.id,
optionKey: optionKey
}
answerApi(pramas).then((res) => {
if (res.code == 200) {
correctOption.value = res.data.answer
if (correctOption.value == selectedOption.value) {
correctCount.value++
timer = setTimeout(() => {
nextQuestion()
}, 500)
} else {
wrongCount.value++
}
}
})
}
const nextResult = () => {
uni.navigateTo({
url: `/pages/exercise_result/index?correctCount=${correctCount.value}&wrongCount=${wrongCount.value}&bankId=${paramsVal.value.bankId}`
})
}
// 选择选项
const selectOption = (value) => {
if (selectedOption.value !== '' || loading.value) return
selectedOption.value = value
answer(value)
// 记录用户答案
userAnswers.value[currentQuestion.value.id] = value
}
// 当前题目索引
const currentIndex = ref(1)
// 已选择的选项
const selectedOption = ref('')
const correctOption = ref('') // 正确选项
// 正确题目数量
const correctCount = ref(0)
// 错误题目数量
const wrongCount = ref(0)
// 用户答案记录
const userAnswers = ref({})
// 计算当前题目
const currentQuestion = ref()
// 执行动画
const performAnimation = (direction, callback) => {
slideDirection.value = direction === 'next' ? 'slide-out-left' : 'slide-out-right'
isAnimating.value = true
// 等待出场动画结束
setTimeout(() => {
if (callback) callback()
// 切完题后再进场
slideDirection.value = direction === 'next' ? 'slide-in-right' : 'slide-in-left'
setTimeout(() => {
isAnimating.value = false
slideDirection.value = ''
}, 300) // 和CSS动画时长保持一致
}, 300)
}
// 下一题
const nextQuestion = () => {
if (!selectedOption.value) {
uni.showToast({
title: '当前题目未作答',
icon: 'none'
})
return
}
function foo() {
// 如果不是最后一题,执行动画并切换
if (!currentQuestion.value.isFinalQuestion) {
clearTimeout(timer)
if (loading.value) return
currentIndex.value++
performAnimation('next', () => {
selectedOption.value = ''
correctOption.value = ''
getQuestion()
})
} else {
nextResult()
}
}
const resultFoo = debounce(foo, 300)
resultFoo()
}
// 上一题
const prevQuestion = async () => {
if (currentIndex.value > 1) {
currentIndex.value--
selectedOption.value = ''
performAnimation('prev', async () => {
await getQuestion()
selectedOption.value = userAnswers.value[currentQuestion.value.id]
})
} else {
uni.showToast({
title: '前面没有题目啦~',
icon: 'none'
})
}
}
// 触摸开始位置
let startX = 0
// 触摸开始事件
const touchStart = (e) => {
startX = e.touches[0].clientX
}
// 触摸结束事件
const touchEnd = (e) => {
const endX = e.changedTouches[0].clientX
const diff = endX - startX
// 左滑:切换到下一题
if (diff < -50) {
if (!selectedOption.value) {
uni.showToast({
title: '当前题目未作答',
icon: 'none'
})
return
}
nextQuestion()
}
// 右滑:切换到上一题
if (diff > 50) {
prevQuestion()
}
}
const goHome = () => {
uni.switchTab({ url: `/pages/index/index` })
}
</script>
<style lang="scss" scoped>
.exercise-content {
min-height: 100vh;
background-color: #f5f5f6;
}
.question-container {
padding: 24rpx;
overflow: hidden; /* 确保动画不会溢出 */
}
.question-card {
background: #ffffff;
border-radius: 24rpx;
padding: 32rpx 34rpx;
will-change: transform; /* 优化动画性能 */
&.slide-left {
animation: fadeIn 0.3s ease;
}
&.slide-right {
animation: fadeIn 0.3s ease;
}
}
.question-card {
background: #ffffff;
border-radius: 24rpx;
padding: 32rpx 34rpx;
will-change: transform, opacity;
}
/* 出场 */
.slide-out-left {
animation: slideOutLeft 0.3s forwards ease;
}
.slide-out-right {
animation: slideOutRight 0.3s forwards ease;
}
/* 入场 */
.slide-in-left {
animation: slideInLeft 0.3s forwards ease;
}
.slide-in-right {
animation: slideInRight 0.3s forwards ease;
}
@keyframes slideOutLeft {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(-100%); opacity: 0; }
}
@keyframes slideOutRight {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
@keyframes slideInLeft {
from { transform: translateX(-100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideInRight {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
</style>
动画通过@keyframes 设置入场和出场动画