YNUtdPlatform/pages/YNEduApp/exam/examination.vue

779 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view>
<u-navbar leftIcon="" title="考试" :placeholder="true" />
<div class="content" @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd">
<div class="top-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 }}</span>
/{{ questionList.length }}
</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: item.isActive, currentActive: currentIndex == index }">
{{ index + 1 }}
</div>
</div>
</div>
<div class="unfold" @click="handleUnfold">
{{ 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>
<!-- 题目 -->
<div class="question-wrapper" v-for="(item, index) in questionList" :key="index" v-show="index == currentIndex">
<div class="question-type-wrapper">
<div class="line" />
<div class="question-type">
<div v-if="item.examType == 1">单选题({{ item.questionScore }}分)</div>
<div v-if="item.examType == 2">多选题({{ item.questionScore }}分)</div>
<div v-if="item.examType == 3">判断题({{ item.questionScore }}分)</div>
</div>
</div>
<div class="question">{{ currentIndex + 1 }}. {{ item.examTopic }}</div>
<u--image
v-if="item.examTopicUrl"
:showLoading="true"
:src="fileUrl + item.examTopicUrl || ''"
width="60px"
height="60px"
style="margin-bottom: 10px"
@click="clickImg(fileUrl + item.examTopicUrl)"
/>
<div class="options">
<div class="option-wrapper" v-for="(option, optionIndex) in item.listOption" :key="optionIndex">
<div
class="option"
:class="{ isActive: option.isActive }"
@click="handleSelectOption(item, index, option, optionIndex)"
>
<div class="option-item">{{ option.optionIdent }}.</div>
<div class="option-content">{{ option.optionContent }}</div>
</div>
<u--image
v-if="option.optionUrl"
:showLoading="true"
:src="fileUrl + option.optionUrl || ''"
width="60px"
height="60px"
style="margin-bottom: 10px"
@click="clickImg(fileUrl + option.optionUrl)"
/>
</div>
</div>
</div>
<!-- 底部按钮 -->
<div class="bottom-btn">
<div class="btn" v-show="currentIndex != 0">
<u-button size="small" shape="circle" text="上一题" @click="currentIndex--" />
</div>
<div class="btn" v-if="currentIndex !== questionList.length - 1">
<u-button type="primary" size="small" shape="circle" text="下一题" @click="currentIndex++" />
</div>
<div class="btn">
<u-button type="primary" size="small" shape="circle" text="交 卷" @click="openConfirmModal" />
</div>
</div>
</div>
<!-- 提交弹框 -->
<u-modal
:show="showConfirmModal"
title="提示"
showCancelButton
@cancel="showConfirmModal = false"
@confirm="handleConfirmSubmit"
>
<view class="slot-content">
<view v-if="unDoCount > 0">
本场考试还有
<span style="color: #1989fa; margin: 0 5px">{{ unDoCount }}</span>
题尚未完成
</view>
<view style="text-align: center">确定交卷?</view>
</view>
</u-modal>
<u-toast ref="uToast"></u-toast>
</view>
</template>
<script>
import { pathToBase64, base64ToPath } from 'image-tools'
import face from '@/uni_modules/mcc-face/index.js'
import {
getExamQuestionList,
insertQuestionAnswerById,
commitExamByRecordId,
getFaceRecognition,
updStudyDurationExamPractice
} from '@/api/eduApp'
import config from '@/config'
export default {
data() {
return {
isLoading: false,
// 考试id
examId: '',
// 考试记录id
recordId: '',
// 切屏次数
screenCount: 0,
// 允许切屏次数
switchCount: 0,
showConfirmModal: false,
// 考试时间
time: 0,
examTime: 0,
random1: 0,
random2: 0,
// 答题时间
answerTime: 0,
currentIndex: 0,
// 是否展开
isRotating: false,
// 未做题目数
unDoCount: 0,
examNum: 0, // 考试次数
examCount: 1, // 1: 不限次 2: 及格终止 3: 自定义
examCustom: 0, // 自定义次数
results: 0, // 考试结果
studyId: '', // 学习id
// 题目列表
questionList: [],
fileUrl: config.fileUrl,
score: 0,
passScore: 0,
isHide: false,
failCount: 3,
startX: 0, // 起始触摸点X坐标
endX: 0 // 结束触摸点X坐标
}
},
onLoad(opt) {
opt = JSON.parse(opt.params)
console.log('🚀 ~ onLoad ~ opt考试中--:', opt)
this.examId = opt.examId
this.switchCount = Number(opt.switchCount)
this.examNum = opt.examNum
this.examCount = opt.examCount
this.examCustom = opt.examCustom
this.studyId = opt.studyId || ''
this.score = opt.score
this.passScore = opt.passScore
},
onShow() {
this.isHide = false
// setTimeout(() => {
// if (this.screenCount > this.switchCount) {
// this.$refs.uToast.show({
// message: '切屏次数已达上限, 系统将自动提交',
// duration: 1000
// })
// this.handleConfirmSubmit()
// }
// }, 1000)
},
onHide() {
// this.screenCount++
// console.log('🚀 ~ onHide ~ this.screenCount:', this.screenCount)
setTimeout(() => {
if (!this.isHide) {
this.handleConfirmSubmit()
}
}, 500)
},
mounted() {
this.getList()
},
// onUnload() {
// console.log('🚀 ~ onUnload ~ 页面关闭')
// this.handleConfirmSubmit()
// },
methods: {
// 获取列表
async getList() {
try {
const params = {
examId: this.examId
}
this.$verificationToken()
uni.request({
url: config.baseUrl + '/exam-student/studentExam/getExamQuestionList',
method: 'post',
data: params,
header: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: uni.getStorageSync('access_token')
},
success: res => {
console.log('🚀 ~ getList ~ res:', res)
res = res.data
const data = res.data
this.questionList = data.examPaperData
this.recordId = data.recordId
this.examTime = data.answerTime
this.time = Number(data.answerTime) * 60 * 1000
console.log('🚀 ~ getList ~ this.time:', this.time)
console.log('🚀 ~ getList ~ this.questionList:', this.questionList)
this.random1 = (data.answerTime / 3) * 60 + Math.ceil(Math.random() * 20) + 20
this.random2 = (data.answerTime / 1.5) * 60 + Math.ceil(Math.random() * 30) + 30
console.log('🚀 ~ getList ~ this.random1:', this.random1, this.random2)
if (this.questionList.length > 0) {
this.questionList.forEach((item, index) => {
this.$set(item, 'isShow', index < 7)
this.$set(item, 'isActive', false)
if (item.listOption) {
item.listOption.forEach(option => {
this.$set(option, 'isActive', false)
})
}
})
console.log('🚀 ~ this.questionList.forEach ~ this.questionList:', this.questionList)
setTimeout(() => {
this.$refs.countDown.start()
}, 100)
}
},
fail: err => {
console.log(err)
}
})
} catch (error) {
console.log('🚀 ~ getList ~ error:', error)
}
},
changeCountDown(time) {
// console.log('🚀 ~ changeCountDown ~ time:', time)
// 等时间赋值后再开始计时
if (this.time == 0 && !this.isLoading) return
this.answerTime =
this.time - (time.days * 24 * 60 * 60 + time.hours * 60 * 60 + time.minutes * 60 + time.seconds) * 1000
// console.log('🚀 ~ changeCountDown ~ this.answerTime:', this.answerTime)
// console.log(
// '🚀 ~ changeCountDown ~ this.answerTime:',
// this.time,
// this.answerTime / 1000,
// this.random1,
// this.random2
// )
// console.log('🚀 ~ changeCountDown ~:', this.answerTime / 1000 == this.random1)
// console.log('🚀 ~ changeCountDown ~:', this.answerTime / 1000 == this.random2)
if (this.answerTime / 1000 == this.random1) {
console.log('🚀 ~ changeCountDown ~ 人脸识别')
// this.openFaceScan() // Android 人脸识别
this.openPhotograph() // IOS 拍照识别
} else if (this.answerTime / 1000 == this.random2) {
// this.openFaceScan()
this.openPhotograph()
} else if (this.answerTime == this.time) {
// 提示: 时间结束, 自动提交
this.$refs.uToast.show({
message: '考试时间结束, 系统将自动提交',
duration: 1000
})
this.handleConfirmSubmit()
}
},
// 点击题号
handleQuestionNumber(item, index) {
console.log('🚀 ~ handleQuestionNumber ~ item:', item)
this.currentIndex = index
},
// 展开
handleUnfold() {
this.questionList.forEach((item, index) => {
if (index > 6) {
this.$set(item, 'isShow', !item.isShow)
}
})
this.isRotating = !this.isRotating
console.log('🚀 ~ this.questionList.forEach ~ this.isRotating:', this.isRotating)
},
handleSelectOption(item, index, option, optionIndex) {
let selectAnswer = ''
console.log('🚀 ~ handleSelectOption ~ option:', item, option, optionIndex)
item.isActive = true
// 如果是单选题与判断题 则只能选中一个 多选题可以选中多个
if (item.examType == 1 || item.examType == 3) {
this.$set(option, 'isActive', true)
item.listOption.forEach((option, optIndex) => {
// 除了当前选中的其他都设置为未选中
if (optIndex != optionIndex) {
this.$set(option, 'isActive', false)
}
})
selectAnswer = option.optionIdent
} else {
this.$set(option, 'isActive', !option.isActive)
// 如果所有选项都是未选中状态, 则题目也是未选中状态
let isActive = item.listOption.some(option => option.isActive)
item.isActive = isActive
// 将点击的选项的 optionIdent 'ABC' 拼接成字符串
item.listOption.forEach(option => {
if (option.isActive) {
selectAnswer += option.optionIdent
}
})
}
const params = {
recordId: this.recordId,
questionId: item.id,
selectAnswer
}
if (item.examType == 3) {
params.selectAnswer = selectAnswer == 'A' ? '对' : '错'
}
console.log('🚀 ~ handleSelectOption ~ params:', params)
// insertQuestionAnswerById(params)
this.$verificationToken()
uni.request({
url: config.baseUrl + '/exam-student/studentExam/insertQuestionAnswerById',
method: 'post',
data: params,
header: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: uni.getStorageSync('access_token')
},
success: res => {
console.log('🚀 ~ handleSelectOption ~ res:', res)
}
})
},
openConfirmModal() {
this.unDoCount = this.questionList.filter(item => !item.isActive).length
this.showConfirmModal = true
},
// 确认提交
handleConfirmSubmit() {
console.log('🚀 ~ 提交-->', this.isLoading)
if (this.isLoading) {
uni.showToast({
title: '正在提交中, 请稍后',
icon: 'none'
})
return
}
this.isLoading = true
// 停止计时
this.$refs.countDown.pause()
const params = {
userId: uni.getStorageSync('userId'),
recordId: this.recordId,
examId: this.examId,
// 时间 转换为分钟, 不足一分钟算一分钟
answerTime: Math.ceil(this.answerTime / 60000)
}
// const res = await commitExamByRecordId(params)
this.$verificationToken()
uni.request({
url: config.baseUrl + '/exam-student/studentExam/commitExamByRecordId',
method: 'post',
data: params,
header: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: uni.getStorageSync('access_token')
},
success: res => {
console.log('🚀 ~ handleConfirmSubmit ~ res:', res)
res = res.data
const params2 = {
examId: this.examId,
examGrade: res.examGrade,
examResult: res.examResult,
gradeAverage: res.gradeAverage,
answerTime: Math.ceil(this.answerTime / 60000),
examTime: this.examTime,
questionCount: this.questionList.length,
switchCount: this.switchCount,
examNum: this.examNum + 1,
examCount: this.examCount,
examCustom: this.examCustom,
results: res.examResult,
studyId: this.studyId || '',
score: this.score,
passScore: this.passScore
}
uni.navigateTo({
url: `/pages/YNEduApp/exam/examinationDetails?params=${JSON.stringify(params2)}`
})
setTimeout(() => {
this.showConfirmModal = false
}, 1000)
}
})
if (this.studyId) {
// updStudyDurationExamPractice({ userId: params.userId, studyId: this.studyId })
this.updStudyDurationExamPractice({ userId: params.userId, studyId: this.studyId })
}
console.log('🚀 ~ handleConfirmSubmit ~ params:', params, res)
// this.showConfirmModal = false
// const params2 = {
// examId: this.examId,
// examGrade: res.examGrade,
// examResult: res.examResult,
// gradeAverage: res.gradeAverage,
// answerTime: Math.ceil(this.answerTime / 60000),
// examTime: this.examTime,
// questionCount: this.questionList.length,
// switchCount: this.switchCount,
// examNum: this.examNum,
// examCount: this.examCount,
// examCustom: this.examCustom,
// results: res.examResult,
// studyId: this.studyId || ''
// }
// uni.navigateTo({
// url: `/pages/YNEduApp/exam/examinationDetails?params=${JSON.stringify(params2)}`
// })
},
// 人脸识别
// async openFaceScan() {
// face.open(['a'], e => {
// console.log('🚀 ~ e-人脸识别:', e)
// face.close()
// let params = {
// userId: uni.getStorageSync('userId'),
// img: e
// }
// params = JSON.stringify(params)
// this.getFaceRecognition(params)
// })
// },
// 拍照录入
openPhotograph() {
this.isHide = true
setTimeout(() => {
this.$refs.countDown.pause()
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['camera'],
success: res => {
this.isHide = false
let url = ''
console.log('🚀 ~ res-拍照:', res)
this.imgToBase64(res.tempFilePaths[0]).then(base64 => {
url = base64
console.log('🚀 ~ this.imgToBase64 ~ base64:', url)
this.getFaceRecognition({ userId: uni.getStorageSync('userId'), img: url })
})
},
fail: err => {
this.isHide = false
console.log('🚀 ~ err:', err)
}
})
}, 10)
},
imgToBase64(data) {
return new Promise((resolve, reject) => {
pathToBase64(data)
.then(base64 => {
resolve(base64)
})
.catch(error => {
console.error(error)
reject(error)
})
})
},
// 发送请求
getFaceRecognition(params) {
console.log('🚀 ~ getFaceRecognition ~ params:', params)
this.$verificationToken()
uni.request({
url: config.baseUrl + '/exam-student/personalCenter/getFaceRecognition',
method: 'POST',
header: {
'content-type': 'application/json',
Authorization: uni.getStorageSync('access_token')
},
data: params,
success: res => {
console.log('🚀 ~ openFaceScan ~ res-人脸识别:', res)
res = res.data
console.log('🚀 ~ openFaceScan ~ res-人脸识别:', res.code)
if (res.code == 200) {
this.$refs.uToast.show({
message: '人脸识别成功',
duration: 1000
})
this.failCount = 3
// 继续计时
this.$refs.countDown.start()
} else {
this.failCount--
if (this.failCount == 0) {
this.$refs.uToast.show({
message: '人脸识别失败, 即将结束考试',
duration: 1500
})
setTimeout(() => {
this.handleConfirmSubmit()
}, 1000)
} else {
this.$refs.uToast.show({
message: '人脸识别失败, 请重新录入, 剩余次数: ' + this.failCount + '次',
duration: 1500
})
setTimeout(() => {
this.openPhotograph()
}, 1700)
}
}
},
fail(err) {
console.log('🚀 ~ openFaceScan ~ 人脸识别失败', err)
this.$refs.uToast.show({
message: err.errMsg,
duration: 1000
})
}
})
},
clickImg(url) {
this.isHide = true
uni.previewImage({
urls: [url]
})
// this.screenCount--
},
updStudyDurationExamPractice(params) {
this.$verificationToken()
uni.request({
url: config.baseUrl + '/exam-student/student/updStudyDurationExamPractice',
method: 'post',
data: params,
header: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: uni.getStorageSync('access_token')
},
fail: err => {
console.log(err)
}
})
},
touchStart(event) {
this.startX = 0
this.endX = 0
console.log('🚀 ~ 开始 ~ event:', event)
// 记录触摸开始的位置
this.startX = event.touches[0].clientX
console.log('🚀 ~ touchStart ~ this.startX:', this.startX)
},
touchMove(event) {
console.log('🚀 ~ 移动 ~ event:', event)
// 在触摸过程中可以获取当前触摸点位置
this.endX = event.touches[0].clientX
console.log('🚀 ~ touchMove ~ this.endX:', this.endX)
},
touchEnd() {
// 判断滑动方向
if (this.startX != 0 && this.endX != 0 && this.startX - this.endX > 10) {
// 左滑
console.log('向左滑动')
this.currentIndex++
} else if (this.startX != 0 && this.endX != 0 && this.endX - this.startX > 10) {
// 右滑
console.log('向右滑动')
this.currentIndex--
}
if (this.currentIndex < 0) {
this.currentIndex = 0
} else if (this.currentIndex > this.questionList.length - 1) {
this.currentIndex = this.questionList.length - 1
}
}
},
onBackPress(options) {
console.log(options)
if (options.from == 'backbutton') {
// 来自手势返回
console.log('手势返回')
// 返回为 true 时,不会执行返回操作,可以自定义返回逻辑
// 返回为 false 或者不返回时,则执行默认返回操作
return true
}
// 返回为 false 或者不返回时,则执行默认返回操作
return false
}
}
</script>
<style lang="scss" scoped>
.content {
width: 100vw;
height: 90vh;
.top-content {
background: #fff;
padding: 10px;
border-radius: 5px;
}
.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%;
display: flex;
justify-content: flex-start;
align-items: center;
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;
&.isActive {
background: #1989fa;
}
&.currentActive {
background: #f4c14a;
}
}
}
}
}
.question-wrapper {
padding: 10px;
.question-type-wrapper {
margin: 20px 0;
display: flex;
justify-content: flex-start;
align-items: center;
.line {
width: 2px;
height: 11px;
background: #1989fa;
margin-right: 3px;
}
.question-type {
color: #8a8a8a;
}
}
.question {
font-weight: 800;
font-size: 15px;
color: #333333;
}
}
.options {
margin-top: 10px;
margin-bottom: 30px;
max-height: 330px;
overflow: auto;
.option-wrapper {
max-height: 200px;
overflow: auto;
.option {
display: flex;
justify-content: flex-start;
align-items: center;
margin-top: 10px;
margin-bottom: 5px;
background: #f4f9fe;
border-radius: 5px;
&.isActive {
background: #8cbff1;
color: #fff;
}
.option-item {
width: 33px;
height: 33px;
line-height: 33px;
text-align: center;
color: #333;
}
.option-content {
margin-left: 10px;
color: #333;
}
}
}
}
.bottom-btn {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: #fff;
padding: 15px 0;
display: flex;
justify-content: flex-end;
align-items: center;
.btn {
width: 100px;
margin-right: 10px;
}
}
}
::v-deep .u-count-down__text {
font-weight: 700;
color: #1989fa;
}
</style>