nxdt-uniapp/pages/myExercise/exercises.vue

605 lines
20 KiB
Vue
Raw Normal View History

2025-01-16 17:36:46 +08:00
<template>
<view>
<Navbar title="练习" :showBack="false" />
<div class="content">
<div class="top-wrapper">
<div class="left">
<div style="color: #1989fa">正确{{ correctNum }}</div>
<div style="color: #ff4d4f">错误{{ errorNum }}</div>
<div>用时{{ timeCount }}</div>
</div>
<div class="right">
<span style="color: #1989fa">{{ currentIndex + 1 }}</span>
/{{ questionList.length }}
</div>
</div>
<!-- 题目内容 -->
<div class="question-wrapper">
<div class="question-list" v-for="(item, index) in questionList" :key="item.id" v-show="index == currentIndex">
<div class="question-type">
<div class="line"></div>
<div class="type">
<div v-if="item.questionType == 1">单选题</div>
<div v-else-if="item.questionType == 2">多选题</div>
<div v-else-if="item.questionType == 3">判断题</div>
</div>
</div>
<div class="question-title">{{ index + 1 }}. {{ item.content }}</div>
<div class="question-img-list">
<div class="img" v-for="(img, imgIndex) in item.questionPictureVoList">
<u-image
:key="imgIndex"
:src="img.url"
:showLoading="true"
width="60px"
height="60px"
@click="handleImg(img.url)"
/>
</div>
</div>
<div class="question-item">
<div v-if="item.questionType == 1 || item.questionType == 3">
<!-- 单选题 -->
<u-radio-group v-model="item.radioValue" placement="column">
<u-radio
:customStyle="{ marginBottom: '8px' }"
v-for="(question, index) in item.questionAnswerVoList"
:key="index"
:label="question.options + '. ' + question.answerOptions"
:name="question.options + '. ' + question.answerOptions"
:disabled="question.disabled"
@change="radioChange(question)"
>
<div class="radio-item">
<div class="radio-name">{{ question.options + '. ' + question.answerOptions }}</div>
<div class="question-img-list">
<div class="img" v-for="(img, imgIndex) in question.questionPictureVoList">
<u-image
:key="imgIndex"
:src="img.url"
:showLoading="true"
width="60px"
height="60px"
@click="handleImg(img.url)"
/>
</div>
</div>
</div>
</u-radio>
</u-radio-group>
</div>
<div v-if="item.questionType == 2">
<!-- 多选题 -->
<u-checkbox-group v-model="item.checkboxValue" placement="column">
<div v-for="(question, index) in item.questionAnswerVoList">
<u-checkbox
:customStyle="{ marginBottom: '8px' }"
:key="index"
:label="question.options + '. ' + question.answerOptions"
:name="question.options + '. ' + question.answerOptions"
:disabled="question.disabled"
@change="radioChange(question)"
></u-checkbox>
<div class="radio-item">
<div class="question-img-list" style="margin: 10px">
<div class="img" v-for="(img, imgIndex) in question.questionPictureVoList">
<u-image
:key="imgIndex"
:src="img.url"
:showLoading="true"
width="60px"
height="60px"
@click="handleImg(img.url)"
/>
</div>
</div>
</div>
</div>
</u-checkbox-group>
</div>
<div style="padding: 10px">
<u-button
type="primary"
size="small"
shape="circle"
@click="handleOption(item)"
v-show="!item.chooseAnswer"
>
确定选择
</u-button>
</div>
<div v-if="item.chooseAnswer">
<div style="color: #5ac725" v-if="item.chooseAnswer == item.correctAnswer">回答正确</div>
<div style="color: #ff4d4f" v-else>回答错误</div>
</div>
<div class="answer-wrap" v-if="item.chooseAnswer">
<div class="answer-item">
<div>正确答案</div>
<div style="color: #5ac725">{{ item.correctAnswer1 }}</div>
</div>
<div class="answer-line"></div>
<div class="answer-item">
<div>已选答案</div>
<div :style="{ color: item.chooseAnswer == item.correctAnswer ? '#5ac725' : '#ff4d4f' }">
{{ item.chooseAnswer1 }}
</div>
</div>
</div>
<!-- 解析 -->
<div v-if="item.chooseAnswer">
<div style="margin: 20px 0 10px; font-weight: 800">解析</div>
<div class="analysis">{{ item.analysis }}</div>
</div>
</div>
</div>
</div>
<!-- 底部固定按钮, 上一题, 下一题, 结束练习 -->
<div class="box"></div>
<div class="bottom-wrapper">
<u-button
class="btn"
type="primary"
size="small"
shape="circle"
@click="handlePrev"
:disabled="currentIndex == 0"
>
上一题
</u-button>
<u-button
class="btn"
type="primary"
size="small"
shape="circle"
@click="handleNext"
:disabled="
currentIndex == questionList.length - 1 ||
(questionList[currentIndex] && questionList[currentIndex].chooseAnswer == '')
"
>
下一题
</u-button>
<u-button class="btn" type="primary" size="small" shape="circle" @click="showModal = true">结束练习</u-button>
</div>
</div>
<!-- 弹框 -->
<u-modal
:show="showModal"
title="温馨提示"
content="确定退出本次练习吗?"
:showCancelButton="true"
@cancel="showModal = false"
@confirm="handleEnd"
>
<view class="slot-content">确定退出本次练习吗</view>
</u-modal>
<u-modal :show="showModal2" title="答题结果" @confirm="goBack">
<view class="slot-content">
<div class="container">
<div>答题数{{ correctNum + errorNum }}</div>
<div style="margin: 0 20px">
正确数
<span style="color: #5ac725">{{ correctNum }}</span>
</div>
<div>
正确率{{ correctNum + errorNum == 0 ? 0 : ((correctNum / (correctNum + errorNum)) * 100).toFixed(2) }}%
</div>
</div>
<u-line dashed :hairline="false" margin="20px 0"></u-line>
<div class="container">
<div>用时</div>
<div>{{ timeCount }}</div>
</div>
</view>
</u-modal>
</view>
</template>
<script>
import { getQuestion, updateAnswer, getTestQuestion, endPractice } from '@/api/educationTraining'
import config from '@/config'
export default {
data() {
return {
isAddLoading: false,
userId: uni.getStorageSync('userInfo').userId,
opt: {},
currentIndex: 0, // 当前题目索引
questionList: [], // 试题列表
endOfExamination: false, // 是否结束考试
correctNum: 0, // 正确题数
errorNum: 0, // 错误题数
time: 0, // 用时
timeCount: '00:00:00',
timer: null, // 计时器id
showModal: false,
showModal2: false
}
},
onLoad(opt) {
this.opt = JSON.parse(opt.params)
console.log('🚀 ~ onLoad ~ this.opt:', this.opt)
this.getQuestionList()
},
methods: {
// 获取题目列表
async getQuestionList() {
let res = {}
if (this.opt.isAutoExercises) {
const params = {
practiceId: this.opt.practiceId,
practiceType: this.opt.practiceType,
lastPracticeQuestionId: this.opt.lastPracticeQuestionId || ''
}
console.log('🚀 ~ getQuestionList ~ params:', params)
res = await getTestQuestion(params)
} else {
res = await getQuestion({ examId: this.opt.id })
}
console.log('🚀 ~ 题目列表 ~ res:', res)
if (this.opt.practiceType == 1) {
// 过滤出包含 lastPracticeQuestionId 的题目, lastPracticeQuestionId 是未答题目所有id拼接的字符串
this.opt.lastPracticeQuestionId = this.opt.lastPracticeQuestionId.split(',')
res.data = res.data.filter(item => this.opt.lastPracticeQuestionId.includes(item.questionId))
}
this.questionList = res.data
this.questionList.forEach(item => {
if (item.questionPictureVoList && item.questionPictureVoList.length > 0) {
item.questionPictureVoList.forEach(img => {
img.url = config.fileUrl2 + img.pictureUrl.replace(/\\/g, '/')
// img.url = 'https://cdn.uviewui.com/uview/swiper/1.jpg'
})
}
//
if (item.questionAnswerVoList && item.questionAnswerVoList.length > 0) {
// 添加一个 radiovalue 字段,用于记录用户选择的答案
if (item.questionType == 1 || item.questionType == 3) {
item.radioValue = ''
} else if (item.questionType == 2) {
item.checkboxValue = []
}
item.chooseAnswer = ''
item.questionAnswerVoList.forEach(answer => {
if (answer.questionPictureVoList && answer.questionPictureVoList.length > 0) {
// 图片路径拼接
answer.questionPictureVoList.forEach(opt => {
opt.url = config.fileUrl2 + opt.pictureUrl.replace(/\\/g, '/')
// opt.url = 'https://cdn.uviewui.com/uview/swiper/1.jpg'
})
}
})
}
})
console.log('🚀 ~ getQuestionList ~ this.题目列表:', this.questionList)
this.startTimer()
},
// 上一题
handlePrev() {
this.currentIndex--
},
// 下一题
handleNext() {
this.currentIndex++
},
// 结束练习
async handleEnd() {
console.log('🚀 结束练习')
try {
if (this.isAddLoading) {
// 提示
uni.showToast({
title: '正在提交中...',
icon: 'none'
})
return
}
this.isAddLoading = true
// 计算总分
let totalScore = 0
this.questionList.forEach(item => {
if (item.scorePerQuestion) {
totalScore += Number(item.scorePerQuestion)
}
})
// 找到 this.questionList 中 chooseAnswer 最后一个有值的 questionId
// const questionId = this.questionList.filter(item => item.chooseAnswer).pop().questionId
// 过滤出所有未答题的questionId 拼接为一个字符串
const questionId = this.questionList
.filter(item => !item.chooseAnswer)
.map(item => item.questionId)
.join(',')
// errorQuestionList 错题列表
const errorQuestionList = this.questionList.filter(
item => item.chooseAnswer && item.chooseAnswer != item.correctAnswer
)
// correctQuestionList 正确题列表
const correctQuestionList = this.questionList.filter(
item => item.chooseAnswer && item.chooseAnswer == item.correctAnswer
)
clearInterval(this.timer)
this.showModal = false
// loading
uni.showLoading({
title: '提交中...',
mask: true
})
if (this.opt.isAutoExercises) {
const params = {
practiceId: this.opt.practiceId, // 题库id
practiceType: this.opt.practiceType, // 题库类型
personId: this.userId, // 用户id
practiceNum: this.correctNum + this.errorNum, // 练习题数
correctNum: this.correctNum, // 正确题数
errorNum: this.errorNum, // 错误题数
practiceTime: this.time, // 练习时间
lastPracticeQuestionId: questionId, // 最后一题id
errorQuestionList, // 错题列表
correctQuestionList // 正确题列表
}
console.log('🚀 ~ handleEnd ~ params:', params)
const res = await endPractice(params)
console.log('🚀 ~ handleEnd ~ res:', res)
} else {
let scoringRete = 0
if (this.correctNum + this.errorNum != 0) {
scoringRete = ((this.correctNum / (this.correctNum + this.errorNum)) * 100).toFixed(2)
}
const params = {
userId: this.userId,
examPaperId: this.opt.id,
scoringRete,
answerTime: this.time,
score: totalScore,
selectAnswerList: this.questionList
}
console.log('🚀 ~ handleEnd ~ params:', params)
const res = await updateAnswer(params)
console.log('🚀 ~ handleEnd ~ res:', res)
}
this.showModal2 = true
uni.hideLoading()
this.isAddLoading = false
} catch (error) {
console.log('🚀 ~ handleEnd ~ error:', error)
this.isAddLoading = false
}
},
// 返回
goBack() {
uni.navigateBack()
},
// 开启计时器
startTimer() {
this.timer = setInterval(() => {
this.time++
// 时间格式 00:00:00
const hour = Math.floor(this.time / 3600)
const minute = Math.floor((this.time % 3600) / 60)
const second = this.time % 60
this.timeCount = `${hour < 10 ? '0' + hour : hour}:${minute < 10 ? '0' + minute : minute}:${
second < 10 ? '0' + second : second
}`
}, 1000)
},
// 点击预览
handleImg(img) {
uni.previewImage({
urls: [img]
})
},
// 选择选项
handleOption(item) {
console.log('🚀 ~ handleOption ~ item:', item)
if (item.questionType == 1 || item.questionType == 3) {
if (!item.radioValue) {
uni.showToast({
title: '请选择答案',
icon: 'none'
})
return
}
console.log('🚀 ~ handleOption ~ 确认选择-单选:', item)
item.chooseAnswer = String(item.radioValue.split('')[0].charCodeAt() - 65)
item.correctAnswer1 = String.fromCharCode(65 + Number(item.correctAnswer))
item.chooseAnswer1 = String.fromCharCode(65 + Number(item.chooseAnswer))
item.questionAnswerVoList.forEach(answer => {
answer.disabled = true
})
// 判断答案是否正确
if (item.chooseAnswer == item.correctAnswer) {
this.correctNum++
if (item.questionType == 1) {
item.scorePerQuestion = this.opt.singleScore
} else if (item.questionType == 3) {
item.scorePerQuestion = this.opt.judgeScore
}
} else {
this.errorNum++
item.scorePerQuestion = '0'
}
} else if (item.questionType == 2) {
if (item.checkboxValue.length == 0) {
uni.showToast({
title: '请选择答案',
icon: 'none'
})
return
}
console.log('🚀 ~ handleOption ~ 确认选择-多选:', item)
let chooseAnswer = item.checkboxValue.map(item => item.split('')[0].charCodeAt() - 65)
chooseAnswer.sort((a, b) => a - b)
console.log('🚀 ~ handleOption ~ chooseAnswer:', chooseAnswer)
item.chooseAnswer = chooseAnswer.join(',')
console.log('🚀 ~ handleOption ~ item.chooseAnswer:', item.chooseAnswer)
item.correctAnswer1 = item.correctAnswer
.split(',')
.map(item => String.fromCharCode(65 + Number(item)))
.join(',')
item.chooseAnswer1 = chooseAnswer.map(item => String.fromCharCode(65 + Number(item))).join(',')
item.questionAnswerVoList.forEach(answer => {
answer.disabled = true
})
// 判断答案是否正确
if (item.chooseAnswer == item.correctAnswer) {
this.correctNum++
item.scorePerQuestion = this.opt.multipleScore
} else {
this.errorNum++
item.scorePerQuestion = '0'
}
}
},
// 单选题选择
radioChange(e) {
console.log('radioChange', e)
},
// 禁用手势返回
onBackPress(options) {
console.log(options)
if (options.from == 'backbutton') {
// 来自手势返回
console.log('手势返回')
// 返回为 true 时,不会执行返回操作,可以自定义返回逻辑
// 返回为 false 或者不返回时,则执行默认返回操作
return true
}
// 返回为 false 或者不返回时,则执行默认返回操作
return false
}
}
}
</script>
<style lang="scss">
.content {
padding: 20px;
word-wrap: break-word;
word-break: break-all;
.bottom-wrapper {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 10px;
background-color: #fff;
display: flex;
justify-content: space-around;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
.btn {
margin: 0 15px;
}
}
.box {
height: 50px;
}
.top-wrapper {
height: 50px;
padding: 0 10px;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #fafafa;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
.left {
display: flex;
}
}
.question-wrapper {
margin-top: 30px;
.question-type {
display: flex;
justify-content: flex-start;
align-items: center;
.line {
width: 3px;
height: 15px;
background-color: #1989fa;
}
.type {
padding: 0 10px;
}
}
.question-title {
margin: 10px 0;
font-size: 15px;
font-weight: 800;
color: #333;
}
.question-img-list {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
/* margin-top: 10px; */
}
.img {
margin-right: 10px;
width: 60px;
height: 60px;
}
.radio-item {
margin-left: 10px;
display: flex;
flex-direction: column;
align-items: flex-start;
.radio-name {
margin-bottom: 5px;
}
}
.question-item {
margin-top: 20px;
}
.answer-wrap {
margin-top: 20px;
display: flex;
justify-content: space-around;
background-color: #edf2f7;
border-radius: 8px;
padding: 10px;
.answer-item {
display: flex;
flex-direction: column;
align-items: center;
div {
margin-bottom: 5px;
}
}
.answer-line {
width: 1px;
height: 45px;
background-color: #ccc;
}
}
.analysis {
padding: 10px;
background-color: #f5f5f5;
border-radius: 8px;
min-height: 30px;
}
}
}
/deep/ .u-radio {
align-items: flex-start !important;
}
.container {
display: flex;
align-items: center;
justify-content: space-between;
}
</style>