nxdt-uniapp/pages/myExam/examination.vue

775 lines
24 KiB
Vue
Raw Normal View History

2025-01-16 17:36:46 +08:00
<template>
<view>
<div v-if="!isEnd">
<Navbar title="考试" :showBack="false" />
<div class="content">
<div class="top-wrapper">
<div class="time">
<div>计时器</div>
<div>
<u-count-down
class="count-down"
ref="countDown"
:autoStart="false"
:time="time"
@change="changeCountDown"
/>
</div>
</div>
<div>
<span style="color: #1989fa">{{ currentIndex + 1 }}/{{ questionList.length }}</span>
</div>
</div>
<div class="center-wrapper">
<div class="answer-wrapper">
<div
class="item-wrapper"
v-for="(item, index) in questionList"
:key="index"
v-show="item.isShow"
@click="handleQuestionNumber(item, index)"
>
<div class="answer-item" :class="{ isActive: currentIndex == index, isAnswer: item.selectAnswer != '' }">
{{ index + 1 }}
</div>
</div>
</div>
<div class="unfold" @click="handleUnfold">
{{ this.isRotating ? '收起' : '展开' }}
<u-icon v-if="!this.isRotating" name="arrow-down-fill" size="10" />
<u-icon v-else name="arrow-up-fill" size="10" />
</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">单选题{{ opt.singleScore || 0 }}</div>
<div v-else-if="item.questionType == 2">多选题{{ opt.multipleScore || 0 }}</div>
<div v-else-if="item.questionType == 3">判断题{{ opt.judgeScore || 0 }}</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, item)"
>
<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, item)"
></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>
</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 || currentIndex == questionList.length"
>
下一题
</u-button>
<u-button class="btn" type="primary" size="small" shape="circle" @click="handleSubmit"> </u-button>
</div>
</div>
<u-toast ref="uToast"></u-toast>
<u-modal
:show="showModal"
title="提示"
:content="content"
showCancelButton
@confirm="confirm"
@cancel="showModal = false"
/>
</div>
<div v-else>
<endOfExamination :states="endOfExamination" :isStudyTask="opt.isStudyTask" :isTrain="opt.isTrain" />
</div>
</view>
</template>
<script>
import config from '@/config'
import endOfExamination from './endOfExamination.vue'
import { getExamQuestion, submitAnswer, getQuestion, updateAnswer } from '@/api/educationTraining'
export default {
components: { endOfExamination },
data() {
return {
isAddLoading: false,
userId: uni.getStorageSync('userInfo').userId,
opt: {},
currentIndex: 0, // 当前题目索引
questionList: [], // 试题列表
endOfExamination: false, // 是否结束考试
correctNum: 0, // 正确题数
errorNum: 0, // 错误题数
totalScore: 0, // 总得分
timeCount: '00:00:00',
timer: null, // 计时器id
showModal: false,
showModal2: false,
showModal: false,
content: '是否确认提交?',
// 时间
time: 0,
// 答题时间
answerTime: 0,
// 试题列表
// 答题卡
answerSheet: [],
// 是否展开
isRotating: false,
// 答题结束
isEnd: false,
// 考后结算
endOfExamination: {
totalScore: 0, // 总分
score: 0, // 分数
isPass: '', // 是否及格
examType: '', // 考试类型
submitTime: '', // 交卷时间
answerSheet: [], // 答题卡
correctNum: 0, // 正确题数
errorNum: 0, // 错误题数
examTime: '', // 考试用时
totalNum: 0 // 总题数
}
}
},
onLoad(opt) {
this.opt = JSON.parse(opt.params)
this.time = Number(this.opt.examTime) * 60 * 1000
console.log('🚀 ~ onLoad ~ opt:', this.opt)
this.getQuestionList()
},
mounted() {
// this.getList()
},
// 未正常提交, 离开页面自动提交
onUnload() {
if (!this.isEnd) {
this.handleConfirmSubmit()
}
},
methods: {
// 获取题目列表
async getQuestionList() {
this.questionList = []
let res = {}
if (this.opt.isStudyTask) {
res = await getQuestion({ examId: this.opt.id })
} else {
const params = {
examPaperId: this.opt.examPaperId,
type: 1,
practiceType: 3
}
res = await getExamQuestion(params)
}
console.log('🚀 ~ 题目列表 ~ res:', res)
this.questionList = res.data
this.questionList.sort((a, b) => a.questionType - b.questionType)
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.selectAnswer = ''
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.getList()
},
// 获取列表
getList() {
this.questionList.forEach((item, index) => {
this.$set(item, 'isShow', index < 7)
this.$set(item, 'isActive', false)
this.$set(item, 'isShortAnswerValue', '')
if (item.options) {
item.options.forEach(option => {
this.$set(option, 'isActive', false)
})
}
})
console.log('🚀 ~ this.questionList.forEach ~ this.questionList:', this.questionList)
this.$refs.countDown.start()
},
radioChange(question, item) {
// console.log('🚀 ~ radioChange ~ item:', item)
console.log('🚀 ~ radioChange ~ question:', question)
setTimeout(() => {
this.handleOption(item)
}, 200)
},
// 上一题
handlePrev() {
if (this.currentIndex > 0) {
this.currentIndex--
if (this.currentIndex <= 6) {
this.isRotating = false
this.questionList.forEach((item, index) => {
if (index > 6) {
this.$set(item, 'isShow', false)
}
})
}
}
},
// 下一题
handleNext() {
if (this.currentIndex < this.questionList.length - 1) {
this.currentIndex++
if (this.currentIndex > 6) {
this.handleUnfold2()
}
}
},
// 点击预览
handleImg(img) {
uni.previewImage({
urls: [img]
})
},
// 选择选项
handleOption(item) {
console.log('🚀 ~ handleOption ~ item:', item)
if (item.questionType == 1 || item.questionType == 3) {
console.log('🚀 ~ handleOption ~ 确认选择-单选:', item)
item.selectAnswer = String(item.radioValue.split('')[0].charCodeAt() - 65)
item.correctAnswer1 = String.fromCharCode(65 + Number(item.correctAnswer))
item.chooseAnswer1 = String.fromCharCode(65 + Number(item.selectAnswer))
} else if (item.questionType == 2) {
console.log('🚀 ~ handleOption ~ 确认选择-多选:', item)
let selectAnswer = item.checkboxValue.map(item => item.split('')[0].charCodeAt() - 65)
console.log('🚀 ~ handleOption ~ selectAnswer:', selectAnswer)
selectAnswer.sort((a, b) => a - b)
item.selectAnswer = selectAnswer.join(',')
console.log('🚀 ~ handleOption ~ item.selectAnswer:', item.selectAnswer)
item.correctAnswer1 = item.correctAnswer
.split(',')
.map(item => String.fromCharCode(65 + Number(item)))
.join(',')
item.chooseAnswer1 = selectAnswer.map(item => String.fromCharCode(65 + Number(item))).join(',')
}
},
changeCountDown(time) {
// console.log('🚀 ~ changeCountDown ~ time:', time)
this.answerTime =
this.time - (time.days * 24 * 60 * 60 + time.hours * 60 * 60 + time.minutes * 60 + time.seconds) * 1000
if (this.answerTime == this.time) {
// 提示: 时间结束, 自动提交
this.$refs.uToast.show({
message: '考试时间结束, 系统将自动提交',
duration: 1000
})
this.handleConfirmSubmit()
setTimeout(() => {
this.isEnd = true
}, 1000)
}
},
// 展开
handleUnfold() {
this.questionList.forEach((item, index) => {
if (index > 6) {
this.$set(item, 'isShow', !item.isShow)
}
})
this.isRotating = !this.isRotating
},
handleUnfold2() {
this.questionList.forEach((item, index) => {
if (index > 6) {
this.$set(item, 'isShow', true)
}
})
this.isRotating = true
},
// 点击题号
handleQuestionNumber(item, index) {
console.log('🚀 ~ handleQuestionNumber ~ item:', item)
this.currentIndex = index
},
confirm() {
if (this.isAddLoading) {
// 提示
this.$refs.uToast.show({
message: '正在提交中...',
duration: 1000
})
return
}
this.isAddLoading = true
this.handleConfirmSubmit()
},
// 提交试卷
handleSubmit() {
if (this.questionList.some(item => item.selectAnswer == '')) {
this.showModal = true
this.content = '您还有题目未答, 是否确认提交?'
} else {
this.content = '是否确认提交?'
this.showModal = true
}
},
// 格式化时间
formatTime(seconds) {
const minutes = Math.floor(seconds / 60)
const remainingSeconds = seconds % 60
const formattedMinutes = String(minutes).padStart(2, '0')
const formattedSeconds = String(remainingSeconds).padStart(2, '0')
return `${formattedMinutes}:${formattedSeconds}`
},
async handleConfirmSubmit() {
try {
uni.showLoading({
title: '提交中...',
mask: true
})
console.log('🚀 ~ handleSubmit ~ 提交:')
// 提交试卷
this.$refs.countDown.pause()
// 计算使用时间
this.endOfExamination.examTime = this.formatTime(this.answerTime / 1000)
// 获取当前时间
const date = new Date()
this.endOfExamination.submitTime = this.formatDate(date)
// 计算正确题数, 错误题数, 每题得分, 总分
this.questionList.forEach(item => {
if (item.selectAnswer == item.correctAnswer) {
this.correctNum++
if (item.questionType == 1) {
item.scorePerQuestion = this.opt.singleScore
} else if (item.questionType == 2) {
item.scorePerQuestion = this.opt.multipleScore
} else if (item.questionType == 3) {
item.scorePerQuestion = this.opt.judgeScore
}
} else {
this.errorNum++
item.scorePerQuestion = '0'
}
if (item.scorePerQuestion) {
this.totalScore += Number(item.scorePerQuestion)
}
})
this.endOfExamination.score = this.totalScore // 总得分
this.endOfExamination.totalScore = this.opt.totalScore // 总分
// 考试类型
this.endOfExamination.examType = this.opt.name
// 计算答对题数
this.endOfExamination.correctNum = this.correctNum
// 计算答错题数
this.endOfExamination.errorNum = this.errorNum
// 生成答题卡
this.endOfExamination.answerSheet = this.generateAnswerSheet()
// 是否及格
// this.endOfExamination.isPass = this.endOfExamination.score >= this.passScore ? '及格' : '不及格'
this.endOfExamination.isPass = this.endOfExamination.score >= this.opt.passScore ? '及格' : '不及格'
console.log('🚀 ~ handleConfirmSubmit ~ this.endOfExamination:', this.endOfExamination)
// 交卷
let scoringRete = 0
scoringRete = ((this.correctNum / (this.correctNum + this.errorNum)) * 100).toFixed(2)
if (scoringRete == 'NaN' || scoringRete == 'Infinity' || scoringRete == '0.00') {
scoringRete = 0
}
if (this.opt.isStudyTask) {
const params = {
taskId: this.opt.taskId,
type: this.opt.type,
userId: this.userId,
examPaperId: this.opt.id,
id: this.opt.id,
scoringRete,
answerTime: this.answerTime / 1000,
score: this.totalScore,
selectAnswerList: this.questionList
}
console.log('🚀 ~ handleEnd ~ params:', params)
const res = await updateAnswer(params)
console.log('🚀 ~ handleEnd ~ res:', res)
if (res.code == 200) {
this.isEnd = true
uni.hideLoading()
}
this.isAddLoading = false
} else {
const params = {
taskId: this.opt.taskId,
type: this.opt.type,
userId: this.userId,
examPaperId: this.opt.examPaperId,
id: this.opt.examPaperId,
score: this.totalScore,
scoreRate: scoringRete,
answerTime: this.answerTime / 1000,
answerList: this.questionList
}
console.log('🚀 ~ handleConfirmSubmit ~ params:', params)
const res = await submitAnswer(params)
console.log('🚀 ~ handleConfirmSubmit ~ res:', res)
if (res.code == 200) {
this.isEnd = true
uni.hideLoading()
}
this.isAddLoading = false
}
} catch (error) {
console.log('🚀 ~ handleConfirmSubmit ~ error:', error)
// 提示
this.$refs.uToast.show({
message: '提交失败, 请重新提交',
duration: 1000
})
uni.hideLoading()
this.isAddLoading = false
}
},
// 格式化时间
formatDate(date) {
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const hours = date.getHours()
const minutes = date.getMinutes().toString().padStart(2, '0')
const seconds = date.getSeconds().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
},
// 生成答题卡
generateAnswerSheet() {
return this.questionList.map(item => {
return {
id: item.questionId,
isAnswer: item.selectAnswer == item.correctAnswer ? true : false,
isUnAnswer: item.selectAnswer == ''
}
})
},
onBackPress(options) {
console.log(options)
if (options.from == 'backbutton') {
// 来自手势返回
console.log('手势返回')
// 返回为 true 时,不会执行返回操作,可以自定义返回逻辑
// 返回为 false 或者不返回时,则执行默认返回操作
return true
}
// 返回为 false 或者不返回时,则执行默认返回操作
return false
}
}
}
</script>
<style lang="scss" scoped>
.content {
padding: 0 20px;
font-size: 13px;
word-wrap: break-word; // 自动换行
word-break: break-all; // 强制换行
.top-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
.time {
display: flex;
justify-content: flex-start;
}
}
.center-wrapper {
display: flex;
justify-content: space-between;
.unfold {
height: 50px;
line-height: 50px;
text-align: center;
display: flex;
justify-content: center;
}
.answer-wrapper {
width: 86%;
max-height: 150px;
overflow: auto;
display: flex;
justify-content: flex-start;
align-items: flex-start;
flex-wrap: wrap;
.item-wrapper {
padding: 5px 5px 0 0;
width: 12%;
height: 45px;
display: flex;
justify-content: center;
align-items: center;
.answer-item {
width: 33px;
height: 33px;
line-height: 33px;
text-align: center;
border-radius: 5px;
background: #f4f9fe;
color: #333;
&.isAnswer {
background: #a9e08f;
}
&.isActive {
background: #1989fa;
}
}
}
}
}
.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;
}
}
/* .question-wrapper {
margin-top: 20px;
.question-title {
font-weight: 500;
font-size: 18px;
color: #08428d;
}
.option-item {
margin-top: 10px;
padding: 10px;
border-radius: 5px;
background: #f4f9fe;
color: #333;
&.isActive {
background: #e4f5de;
}
}
} */
.box {
height: 50px;
}
.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;
}
}
}
::v-deep .u-count-down__text {
color: #1989fa;
font-size: 13px;
}
::v-deep .u-modal__content {
flex-direction: column;
align-items: center;
}
</style>