|
|
|
@ -1,22 +1,28 @@
|
|
|
|
|
<template>
|
|
|
|
|
<a-layout>
|
|
|
|
|
<!-- 页面布局的头部区域,设置文字颜色为白色 -->
|
|
|
|
|
<a-layout-header class="header" style="color: #fff">
|
|
|
|
|
<!-- v-if="examDetail.exam" 是为了防止 异步请求时页面渲染的时候还没有拿到这个值而报错, 下面多处这个判断都是这个道理 -->
|
|
|
|
|
<!-- v-if="examDetail.exam" 用于防止异步请求时页面渲染还没拿到 examDetail.exam 值而报错,下面多处类似判断同理 -->
|
|
|
|
|
<span style="font-size:25px;margin-left: 0px;" v-if="examDetail.exam">
|
|
|
|
|
<!-- 使用 a-avatar 组件展示考试相关头像,通过管道过滤器 imgSrcFilter 处理图片地址,同时展示考试名称和描述 -->
|
|
|
|
|
<a-avatar slot="avatar" size="large" shape="circle" :src="examDetail.exam.examAvatar | imgSrcFilter"/>
|
|
|
|
|
{{ examDetail.exam.examName }}
|
|
|
|
|
<span style="font-size:15px;">{{ examDetail.exam.examDescription }} </span>
|
|
|
|
|
</span>
|
|
|
|
|
<span style="float: right;">
|
|
|
|
|
<!-- 根据 recordDetail.examRecord 存在与否,展示考试得分和参加考试时间信息 -->
|
|
|
|
|
<span style="margin-right: 40px; font-size: 20px" v-if="recordDetail.examRecord">
|
|
|
|
|
考试得分:<span style="color: red">{{ recordDetail.examRecord.examJoinScore }}</span> 分
|
|
|
|
|
<span style="font-size:15px;">参加考试时间:{{ recordDetail.examRecord.examJoinDate }}</span>
|
|
|
|
|
</span>
|
|
|
|
|
<!-- 展示用户头像 -->
|
|
|
|
|
<a-avatar class="avatar" size="small" :src="avatar()"/>
|
|
|
|
|
<!-- 展示用户昵称 -->
|
|
|
|
|
<span style="margin-left: 12px">{{ nickname() }}</span>
|
|
|
|
|
</span>
|
|
|
|
|
</a-layout-header>
|
|
|
|
|
<a-layout>
|
|
|
|
|
<!-- 页面布局的侧边栏区域,设置了宽度、背景、滚动等样式属性 -->
|
|
|
|
|
<a-layout-sider width="190" :style="{background: '#444',overflow: 'auto', height: '100vh', position: 'fixed', left: 0 }">
|
|
|
|
|
<a-menu
|
|
|
|
|
mode="inline"
|
|
|
|
@ -24,14 +30,18 @@
|
|
|
|
|
:defaultOpenKeys="['question_radio', 'question_check', 'question_judge']"
|
|
|
|
|
:style="{ height: '100%', borderRight: 0 }"
|
|
|
|
|
>
|
|
|
|
|
<!-- 单选题菜单部分 -->
|
|
|
|
|
<a-sub-menu key="question_radio">
|
|
|
|
|
<!-- 根据 examDetail.exam 存在与否展示菜单标题,包含图标和每题分值信息 -->
|
|
|
|
|
<span slot="title" v-if="examDetail.exam"><a-icon type="check-circle" theme="twoTone"/>单选题(每题{{ examDetail.exam.examScoreRadio }}分)</span>
|
|
|
|
|
<!-- 循环渲染单选题菜单项,点击菜单项触发 getQuestionDetail 方法,根据 resultsMap 中对应题目结果显示正确或错误图标 -->
|
|
|
|
|
<a-menu-item v-for="(item, index) in examDetail.radioIds" :key="item" @click="getQuestionDetail(item)">
|
|
|
|
|
<a-icon type="check" v-if="resultsMap.get(item)==='True'"/>
|
|
|
|
|
<a-icon type="close" v-if="resultsMap.get(item)==='False'"/>
|
|
|
|
|
题目{{ index + 1 }}
|
|
|
|
|
</a-menu-item>
|
|
|
|
|
</a-sub-menu>
|
|
|
|
|
<!-- 多选题菜单部分,逻辑与单选题类似 -->
|
|
|
|
|
<a-sub-menu key="question_check">
|
|
|
|
|
<span slot="title" v-if="examDetail.exam"><a-icon type="check-square" theme="twoTone"/>多选题(每题{{ examDetail.exam.examScoreCheck }}分)</span>
|
|
|
|
|
<a-menu-item v-for="(item, index) in examDetail.checkIds" :key="item" @click="getQuestionDetail(item)">
|
|
|
|
@ -40,6 +50,7 @@
|
|
|
|
|
题目{{ index + 1 }}
|
|
|
|
|
</a-menu-item>
|
|
|
|
|
</a-sub-menu>
|
|
|
|
|
<!-- 判断题菜单部分,逻辑与单选题类似 -->
|
|
|
|
|
<a-sub-menu key="question_judge">
|
|
|
|
|
<span slot="title" v-if="examDetail.exam"><a-icon type="like" theme="twoTone"/>判断题(每题{{ examDetail.exam.examScoreJudge }}分)</span>
|
|
|
|
|
<a-menu-item v-for="(item, index) in examDetail.judgeIds" :key="item" @click="getQuestionDetail(item)">
|
|
|
|
@ -53,21 +64,24 @@
|
|
|
|
|
<a-layout :style="{ marginLeft: '200px' }">
|
|
|
|
|
<a-layout-content :style="{ margin: '24px 16px 0',height: '84vh', overflow: 'initial' }">
|
|
|
|
|
<div :style="{ padding: '24px', background: '#fff',height: '84vh'}">
|
|
|
|
|
<!-- 如果当前没有题目(currentQuestion 为空),显示欢迎查看考试情况及操作提示语 -->
|
|
|
|
|
<span v-if="currentQuestion === ''" style="font-size: 30px;font-family: Consolas">欢迎查看本次考试情况,点击左侧题目编号可以查看答题详情</span>
|
|
|
|
|
<span v-if="currentQuestion !== ''">
|
|
|
|
|
<span v-if="currentQuestion!== ''">
|
|
|
|
|
<!-- 展示当前题目类型和题目名称(使用 v-html 渲染可能包含 HTML 标签的题目名称) -->
|
|
|
|
|
<strong>{{ currentQuestion.type }} </strong> <p v-html="currentQuestion.name"></p>
|
|
|
|
|
<!-- 根据 questionRight 计算属性判断当前题目用户是否答对,显示相应提示信息 -->
|
|
|
|
|
<strong style="color: green;" v-if="questionRight">本题您答对啦!</strong>
|
|
|
|
|
<strong style="color: red;" v-if="!questionRight">本题您答错啦!</strong>
|
|
|
|
|
</span>
|
|
|
|
|
<br><br>
|
|
|
|
|
<!-- 单选题和判断题 --> <!-- key不重复只需要在一个for循环中保证即可 -->
|
|
|
|
|
<!-- 单选题和判断题的选项组,通过 v-model 双向绑定选择值,根据题目类型进行显示 -->
|
|
|
|
|
<a-radio-group v-model="radioValue" v-if="currentQuestion.type === '单选题' || currentQuestion.type === '判断题'">
|
|
|
|
|
<a-radio v-for="option in currentQuestion.options" :key="option.questionOptionId" :style="optionStyle" :value="option.questionOptionId">
|
|
|
|
|
{{ option.questionOptionContent }}
|
|
|
|
|
</a-radio>
|
|
|
|
|
</a-radio-group>
|
|
|
|
|
|
|
|
|
|
<!-- 题目出错的时候才显示这块 -->
|
|
|
|
|
<!-- 当题目答错且当前题目不为空且为单选题或判断题时,显示正确答案的选项组 -->
|
|
|
|
|
<div v-if="!questionRight && currentQuestion!=='' && (currentQuestion.type === '单选题' || currentQuestion.type === '判断题')">
|
|
|
|
|
<span style="color: red;"><br/>正确答案是:<br/></span>
|
|
|
|
|
<a-radio-group v-model="radioRightValue">
|
|
|
|
@ -77,14 +91,14 @@
|
|
|
|
|
</a-radio-group>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 多选题 -->
|
|
|
|
|
<!-- 多选题的选项组,通过 v-model 双向绑定选择值,根据题目类型进行显示 -->
|
|
|
|
|
<a-checkbox-group v-model="checkValues" v-if="currentQuestion.type === '多选题'">
|
|
|
|
|
<a-checkbox v-for="option in currentQuestion.options" :key="option.questionOptionId" :style="optionStyle" :value="option.questionOptionId">
|
|
|
|
|
{{ option.questionOptionContent }}
|
|
|
|
|
</a-checkbox>
|
|
|
|
|
</a-checkbox-group>
|
|
|
|
|
|
|
|
|
|
<!-- 题目出错的时候才显示这块 -->
|
|
|
|
|
<!-- 当题目答错且当前题目不为空且为多选题时,显示正确答案的选项组 -->
|
|
|
|
|
<div v-if="!questionRight && currentQuestion!=='' && currentQuestion.type === '多选题'">
|
|
|
|
|
<span style="color: red;"><br/>正确答案是:<br/></span>
|
|
|
|
|
<a-checkbox-group v-model="checkRightValues">
|
|
|
|
@ -105,10 +119,12 @@
|
|
|
|
|
</a-layout>
|
|
|
|
|
</a-layout>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
// 从指定路径导入获取考试详情、获取题目详情、获取考试记录详情相关的 API 函数
|
|
|
|
|
import { getExamDetail, getQuestionDetail, getExamRecordDetail } from '../../api/exam'
|
|
|
|
|
// 导入用户菜单组件
|
|
|
|
|
import UserMenu from '../../components/tools/UserMenu'
|
|
|
|
|
// 导入 Vuex 的 mapGetters 辅助函数,用于获取 Vuex 中的计算属性
|
|
|
|
|
import { mapGetters } from 'vuex'
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
@ -118,25 +134,25 @@ export default {
|
|
|
|
|
},
|
|
|
|
|
data () {
|
|
|
|
|
return {
|
|
|
|
|
// 考试详情对象
|
|
|
|
|
// 用于存放考试详情信息的对象,初始为空
|
|
|
|
|
examDetail: {},
|
|
|
|
|
// 考试记录详情对象
|
|
|
|
|
// 用于存放考试记录详情信息的对象,初始为空
|
|
|
|
|
recordDetail: {},
|
|
|
|
|
// 用户做过的问题都放到这个数组中,键为问题id, 值为currentQuestion(其属性answers属性用于存放答案选项地id或ids),,用于存放用户勾选的答案
|
|
|
|
|
// 以 Map 结构存储用户做过的题目信息,键为题目id,值为包含答案等信息的 currentQuestion 对象(其 answers 属性存放答案选项id或ids)
|
|
|
|
|
answersMap: {},
|
|
|
|
|
// 题目的正确答案
|
|
|
|
|
// 以 Map 结构存储题目正确答案信息,键为题目id,对应的值为正确答案相关内容
|
|
|
|
|
answersRightMap: {},
|
|
|
|
|
// 题目的作答结果(正确或错误)
|
|
|
|
|
// 以 Map 结构存储题目作答结果信息(如 'True' 表示正确,'False' 表示错误),键为题目id,对应的值为作答结果
|
|
|
|
|
resultsMap: {},
|
|
|
|
|
// 当前用户的问题
|
|
|
|
|
// 当前显示的题目信息对象,初始为空字符串
|
|
|
|
|
currentQuestion: '',
|
|
|
|
|
// 单选或判断题的绑定值,用于从answersMap中初始化做题状态
|
|
|
|
|
// 单选或判断题的绑定值,用于从 answersMap 中初始化做题状态,初始为空
|
|
|
|
|
radioValue: '',
|
|
|
|
|
// 单选题的正确答案,用于从answersRightMap中初始化做题状态
|
|
|
|
|
// 单选题的正确答案绑定值,用于从 answersRightMap 中初始化正确答案状态,初始为空
|
|
|
|
|
radioRightValue: '',
|
|
|
|
|
// 多选题的绑定值,用于从answersMap中初始化做题状态
|
|
|
|
|
// 多选题的绑定值,用于从 answersMap 中初始化做题状态,初始为空数组
|
|
|
|
|
checkValues: [],
|
|
|
|
|
// 多选题的绑定值,用于从answersRightMap中初始化做题状态
|
|
|
|
|
// 多选题的正确答案绑定值,用于从 answersRightMap 中初始化正确答案状态,初始为空数组
|
|
|
|
|
checkRightValues: [],
|
|
|
|
|
optionStyle: {
|
|
|
|
|
display: 'block',
|
|
|
|
@ -148,42 +164,44 @@ export default {
|
|
|
|
|
},
|
|
|
|
|
computed: {
|
|
|
|
|
/**
|
|
|
|
|
* 当前题目用户是否作答正确
|
|
|
|
|
* */
|
|
|
|
|
* 计算属性,用于判断当前题目用户是否作答正确,通过对比 resultsMap 中对应题目结果是否为 'True' 来确定
|
|
|
|
|
*/
|
|
|
|
|
questionRight () {
|
|
|
|
|
return this.resultsMap !== '' && this.resultsMap.get(this.currentQuestion.id) === 'True'
|
|
|
|
|
return this.resultsMap!== '' && this.resultsMap.get(this.currentQuestion.id) === 'True'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
mounted () {
|
|
|
|
|
// 在组件挂载时,初始化 answersMap、answersRightMap、resultsMap 为新的 Map 对象
|
|
|
|
|
this.answersMap = new Map()
|
|
|
|
|
this.answersRightMap = new Map()
|
|
|
|
|
this.resultsMap = new Map()
|
|
|
|
|
const that = this
|
|
|
|
|
// 从后端获取考试的详细信息,渲染到考试详情里,需要加个延时,要不拿不到参数
|
|
|
|
|
const that = this;
|
|
|
|
|
// 调用 getExamDetail 函数从后端获取考试详细信息,并根据返回结果进行处理,此处提到需加延时(可能实际代码中还需补充相关逻辑)
|
|
|
|
|
getExamDetail(this.$route.params.exam_id)
|
|
|
|
|
.then(res => {
|
|
|
|
|
if (res.code === 0) {
|
|
|
|
|
// 赋值考试对象
|
|
|
|
|
// 如果获取成功,将返回的考试详情数据赋值给 examDetail 对象
|
|
|
|
|
that.examDetail = res.data
|
|
|
|
|
return res.data
|
|
|
|
|
} else {
|
|
|
|
|
// 如果获取失败,通过通知组件显示错误信息
|
|
|
|
|
this.$notification.error({
|
|
|
|
|
message: '获取考试详情失败',
|
|
|
|
|
description: res.msg
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
// 查看考试记录详情,渲染到前端界面
|
|
|
|
|
// 调用 getExamRecordDetail 函数从后端获取考试记录详情信息,并根据返回结果进行处理
|
|
|
|
|
getExamRecordDetail(this.$route.params.record_id)
|
|
|
|
|
.then(res => {
|
|
|
|
|
if (res.code === 0) {
|
|
|
|
|
console.log(res.data)
|
|
|
|
|
// 赋值考试对象
|
|
|
|
|
that.recordDetail = res.data
|
|
|
|
|
// 赋值用户的作答答案
|
|
|
|
|
that.objToMap()
|
|
|
|
|
return res.data
|
|
|
|
|
console.log(res.data);
|
|
|
|
|
// 如果获取成功,将返回的考试记录详情数据赋值给 recordDetail 对象,并调用 objToMap 方法处理相关数据
|
|
|
|
|
that.recordDetail = res.data;
|
|
|
|
|
that.objToMap();
|
|
|
|
|
return res.data;
|
|
|
|
|
} else {
|
|
|
|
|
// 如果获取失败,通过通知组件显示错误信息
|
|
|
|
|
this.$notification.error({
|
|
|
|
|
message: '获取考试记录详情失败',
|
|
|
|
|
description: res.msg
|
|
|
|
@ -192,51 +210,52 @@ export default {
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
methods: {
|
|
|
|
|
// 从全局变量中获取用户昵称和头像,
|
|
|
|
|
// 使用 mapGetters 辅助函数从 Vuex 中获取用户昵称和头像相关的计算属性
|
|
|
|
|
...mapGetters(['nickname', 'avatar']),
|
|
|
|
|
/**
|
|
|
|
|
* 把后端传过来的对象Object转换成Map
|
|
|
|
|
**/
|
|
|
|
|
* 方法用于将后端传过来的对象数据转换为 Map 结构,分别处理 answersMap、answersRightMap、resultsMap 的赋值
|
|
|
|
|
*/
|
|
|
|
|
objToMap () {
|
|
|
|
|
for (const item in this.recordDetail.answersMap) {
|
|
|
|
|
this.answersMap.set(item, this.recordDetail.answersMap[item])
|
|
|
|
|
this.answersMap.set(item, this.recordDetail.answersMap[item]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const item in this.recordDetail.answersRightMap) {
|
|
|
|
|
this.answersRightMap.set(item, this.recordDetail.answersRightMap[item])
|
|
|
|
|
this.answersRightMap.set(item, this.recordDetail.answersRightMap[item]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const item in this.recordDetail.resultsMap) {
|
|
|
|
|
this.resultsMap.set(item, this.recordDetail.resultsMap[item])
|
|
|
|
|
this.resultsMap.set(item, this.recordDetail.resultsMap[item]);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
getQuestionDetail (questionId) {
|
|
|
|
|
// 问题切换时从后端拿到问题详情,渲染到前端content中
|
|
|
|
|
const that = this
|
|
|
|
|
// 清空问题绑定的值
|
|
|
|
|
this.radioValue = ''
|
|
|
|
|
this.radioRightValue = ''
|
|
|
|
|
this.checkValues = []
|
|
|
|
|
this.checkRightValues = []
|
|
|
|
|
const that = this;
|
|
|
|
|
// 问题切换时,先清空单选、单选题正确答案、多选、多选题正确答案的绑定值
|
|
|
|
|
this.radioValue = '';
|
|
|
|
|
this.radioRightValue = '';
|
|
|
|
|
this.checkValues = [];
|
|
|
|
|
this.checkRightValues = [];
|
|
|
|
|
// 调用 getQuestionDetail 函数从后端获取指定题目id的题目详情信息,并根据返回结果进行处理
|
|
|
|
|
getQuestionDetail(questionId)
|
|
|
|
|
.then(res => {
|
|
|
|
|
if (res.code === 0) {
|
|
|
|
|
// 赋值当前考试对象
|
|
|
|
|
that.currentQuestion = res.data
|
|
|
|
|
// 查看用户是不是已经做过这道题又切换回来的,answersMap中查找,能找到这个题目id对应的值数组不为空说明用户做过这道题
|
|
|
|
|
// 如果获取成功,将返回的题目详情数据赋值给 currentQuestion 对象
|
|
|
|
|
that.currentQuestion = res.data;
|
|
|
|
|
// 检查用户是否之前做过这道题(通过 answersMap 判断)
|
|
|
|
|
if (that.answersMap.get(that.currentQuestion.id)) {
|
|
|
|
|
// 说明之前做过这道题了
|
|
|
|
|
// 如果做过,根据题目类型初始化对应的绑定值(单选/判断题取对应答案,多选题进行数组拷贝赋值)
|
|
|
|
|
if (that.currentQuestion.type === '单选题' || that.currentQuestion.type === '判断题') {
|
|
|
|
|
that.radioValue = that.answersMap.get(that.currentQuestion.id)[0]
|
|
|
|
|
that.radioRightValue = that.answersRightMap.get(that.currentQuestion.id)[0]
|
|
|
|
|
that.radioValue = that.answersMap.get(that.currentQuestion.id)[0];
|
|
|
|
|
that.radioRightValue = that.answersRightMap.get(that.currentQuestion.id)[0];
|
|
|
|
|
} else if (that.currentQuestion.type === '多选题') {
|
|
|
|
|
// 数组是引用类型,因此需要进行拷贝,千万不要直接赋值
|
|
|
|
|
Object.assign(that.checkValues, that.answersMap.get(that.currentQuestion.id))
|
|
|
|
|
Object.assign(that.checkRightValues, that.answersRightMap.get(that.currentQuestion.id))
|
|
|
|
|
// 因为数组是引用类型,这里使用 Object.assign 进行拷贝,避免直接赋值带来的问题
|
|
|
|
|
Object.assign(that.checkValues, that.answersMap.get(that.currentQuestion.id));
|
|
|
|
|
Object.assign(that.checkRightValues, that.answersRightMap.get(that.currentQuestion.id));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return res.data
|
|
|
|
|
return res.data;
|
|
|
|
|
} else {
|
|
|
|
|
// 如果获取题目详情失败,通过通知组件显示错误信息
|
|
|
|
|
this.$notification.error({
|
|
|
|
|
message: '获取问题详情失败',
|
|
|
|
|
description: res.msg
|
|
|
|
|