uni-app 实现做练习题(每一题从后端接口请求切换动画记录错题)
1.每一道题都从后端去请求数据
2.选择完之后请求数据拿到结果 反馈用户
3.记录错题 做完计算正确率
<template><view class="exercise-content"><baseHead title="练习题" @toBack="goHome" /><!-- 题目内容区域 --><view class="question-container" @touchstart="touchStart" @touchend="touchEnd"><viewclass="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"><viewv-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.examIdparamsVal.value = JSON.parse(options.params)currentIndex.value = paramsVal.value.lastQuestionNum || 1getQuestion()
})// 获取选项
const getQuestion = () => {loading.value = trueconst params = {examId: examId.value,sortNum: currentIndex.value}getQuestionApi(params).then((res) => {if (res.code == 200) {currentQuestion.value = res.dataif (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.answerif (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) returnselectedOption.value = valueanswer(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 = falseslideDirection.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) returncurrentIndex.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].clientXconst 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 设置入场和出场动画