605 lines
20 KiB
Vue
605 lines
20 KiB
Vue
<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>
|