当前位置: 首页 > news >正文

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 设置入场和出场动画

http://www.dtcms.com/a/363042.html

相关文章:

  • Nginx的反向代理与正向代理及其location的配置说明
  • 久等啦!Tigshop O2O多门店JAVA/PHP版本即将上线!
  • SpringBoot3 + Netty + Vue3 实现消息推送(最新)
  • B树和B+树,聚簇索引和非聚簇索引
  • 云计算学习100天-第44天-部署邮件服务器
  • vscode炒股插件-韭菜盒子AI版
  • 小白H5制作教程!一分钟学会制作企业招聘H5页面
  • Linux 环境配置 muduo 网络库详细步骤
  • WPF 开发必备技巧:TreeView 自动展开全攻略
  • gbase8s之导出mysql导入gbase8s
  • WebSocket STOMP协议服务端给客户端发送ERROR帧
  • 串口服务器技术详解:2025年行业标准与应用指南
  • 大文件稳定上传:Spring Boot + MinIO 断点续传实践
  • DevOps部署与监控
  • WPF中的DataContext以及常见的绑定方式
  • Zynq开发实践(FPGA之流水线和冻结)
  • FPGA入门-分频器
  • 【Python - 基础 - 工具】解决pycharm“No Python interpreter configured for the project”问题
  • 【踩坑随笔】VScode+ESP-IDF头文件标红但能正常运行
  • 广播电视制作领域,什么是SMPTE标准?
  • vscode使用black对python代码进行格式化
  • 2025年了,学C#上位机需要什么条件
  • Day33 网络编程:OSI/TCP/IP模型、协议族与UDP编程
  • 虚拟继承:破解菱形继承之谜
  • Redis核心数据类型解析——string篇
  • Linux驱动开发学习笔记
  • 【C++框架#1】gflags 和 gtest 安装使用
  • 情况三:已经 add ,并且也 commit 了
  • 10 51单片机之DS1302实时时钟
  • 2025 年普通人还可以期待 NFT 交易市场吗?