Default Changelist

feature/qzw
秦泽旺 1 month ago
parent 3e515b8e04
commit 6fb6bc2f76

@ -1,30 +1,57 @@
// 定义一个名为 PERMISSION_ENUM 的常量对象,用于列举各种权限相关的枚举信息。
// 该对象的每个属性键(如 'add'、'delete' 等)对应一种权限操作,属性值又是一个包含 'key' 和 'label' 的对象,
// 'key' 与属性键相同,用于在代码逻辑中作为唯一标识来区分不同的权限操作,
// 'label' 则是对应的权限操作的中文描述,方便在界面显示或者给开发人员查看时更直观地理解该权限的含义。
const PERMISSION_ENUM = {
// 'add' 权限,代表新增操作,其对应的标识 'key' 为 'add',中文描述 'label' 为 '新增',在权限管理系统中可用于判断用户是否具有新增数据等相关权限。
'add': { key: 'add', label: '新增' },
// 'delete' 权限,对应删除操作,'key' 为 'delete''label' 为 '删除',用于检查用户是否有权限删除相应资源等场景。
'delete': { key: 'delete', label: '删除' },
// 'edit' 权限,表示修改操作,'key' 为 'edit''label' 为 '修改',可用于判断用户是否能够对数据等进行修改操作的权限验证。
'edit': { key: 'edit', label: '修改' },
// 'query' 权限,意味着查询操作,'key' 为 'query''label' 为 '查询',例如在数据查询功能的权限控制方面会用到这个权限标识。
'query': { key: 'query', label: '查询' },
// 'get' 权限,通常可理解为获取详情操作,'key' 为 'get''label' 为 '详情',用于判断用户是否有权查看具体数据详情等情况。
'get': { key: 'get', label: '详情' },
// 'enable' 权限,代表启用操作,'key' 为 'enable''label' 为 '启用',在涉及功能启用、数据启用等权限控制场景中使用。
'enable': { key: 'enable', label: '启用' },
// 'disable' 权限,对应禁用操作,'key' 为 'disable''label' 为 '禁用',用于验证用户是否具备禁用相关功能或数据的权限。
'disable': { key: 'disable', label: '禁用' },
// 'import' 权限,表示导入操作,'key' 为 'import''label' 为 '导入',比如在数据导入功能的权限管理中依靠这个权限标识来判断权限。
'import': { key: 'import', label: '导入' },
// 'export' 权限,意味着导出操作,'key' 为 'export''label' 为 '导出',用于控制用户是否有权限导出数据等相关操作的权限判断。
'export': { key: 'export', label: '导出' }
}
function plugin (Vue) {
// 检查插件是否已经被安装过,如果已经安装(通过判断 plugin.installed 属性是否为 true则直接返回避免重复安装。
if (plugin.installed) {
return
}
// 如果 Vue.prototype 上不存在 $auth 属性,就通过 Object.defineProperties 方法来为其定义该属性。
// 这里使用 defineProperties 可以更精细地控制属性的描述符(如可枚举性、可配置性、读写特性等)。
!Vue.prototype.$auth && Object.defineProperties(Vue.prototype, {
$auth: {
// 定义 $auth 属性的读取器getter函数当在 Vue 实例上访问 $auth 属性时,会执行这个函数来获取相应的值。
get () {
// 将当前的 Vue 实例保存到 _this 变量中,方便在后续的内部函数中访问实例上的其他属性和方法,
// 例如访问 Vuex 的 store 实例等(通过 _this.$store
const _this = this
// 返回一个函数,这个函数接收一个参数 permissions它应该是一个表示权限的字符串格式可能类似 'permission.action'。
return (permissions) => {
// 将传入的 permissions 参数按照 '.' 进行分割,得到一个包含两个元素的数组,
// 第一个元素 permission 表示权限的类型(如 'add'、'delete' 等),第二个元素 action 表示具体的操作行为(对应权限类型下的具体操作)。
const [permission, action] = permissions.split('.')
// 通过 Vue 实例的 $store.getters 访问 Vuex 中定义的 getters然后获取 roles.permissions
// 这里的 roles.permissions 应该是存储了用户所拥有的所有权限信息的列表(通常是一个数组,每个元素包含权限相关的详细信息)。
const permissionList = _this.$store.getters.roles.permissions
// 在权限列表中查找与传入的 permission 类型匹配的权限对象,通过比较 permissionId 属性来确定。
return permissionList.find((val) => {
return val.permissionId === permission
}).actionList.findIndex((val) => {
})
// 在上一步找到的权限对象中,查找其 actionList应该是包含该权限下所有具体操作行为的列表中是否存在传入的 action 操作行为,
// 通过 findIndex 方法查找,如果找到则返回该操作行为在列表中的索引,若索引大于 -1表示找到了该操作行为即用户具有对应的权限返回 true否则返回 false。
.actionList.findIndex((val) => {
return val === action
}) > -1
}
@ -32,15 +59,21 @@ function plugin (Vue) {
}
})
// 类似地,如果 Vue.prototype 上不存在 $enum 属性,就通过 Object.defineProperties 方法为其定义该属性。
!Vue.prototype.$enum && Object.defineProperties(Vue.prototype, {
$enum: {
// 定义 $enum 属性的读取器getter函数当在 Vue 实例上访问 $enum 属性时,执行此函数来获取相应的值。
get () {
// const _this = this;
// 返回一个函数,这个函数接收一个参数 val用于根据传入的参数来查找 PERMISSION_ENUM 中对应的枚举值。
return (val) => {
// 先将 PERMISSION_ENUM 赋值给 result后续会根据传入的 val 参数逐步在这个对象中查找对应的子属性。
let result = PERMISSION_ENUM
// 如果传入的 val 参数存在,就按照 '.' 分割成数组,然后遍历每个元素 v在 result 对象中查找对应的子属性。
val && val.split('.').forEach(v => {
// 如果当前的 result 对象存在并且包含对应的子属性 v则更新 result 为该子属性的值,否则将 result 设置为 null表示未找到对应的枚举值。
result = result && result[v] || null
})
// 最后返回查找后的结果,即对应的权限枚举值(如果找到的话)或者 null如果未找到
return result
}
}
@ -48,4 +81,6 @@ function plugin (Vue) {
})
}
// 将定义好的 plugin 函数导出,方便在其他模块中引入使用,通常是在 Vue 应用的入口文件或者插件注册相关的代码中,
// 通过 Vue.use(plugin) 的方式来注册这个插件,使其为 Vue 实例添加 $auth 和 $enum 等便捷的属性和功能。
export default plugin

@ -1,20 +1,27 @@
<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;">
<!-- 同样基于 examDetail.exam 存在的情况下展示考试限时信息此处为倒计时相关显示 -->
<span style="margin-right: 60px; font-size: 20px" v-if="examDetail.exam">{{ examDetail.exam.examTimeLimit }} </span>
<!-- 点击交卷按钮触发 finishExam 方法 -->
<a-button type="danger" ghost style="margin-right: 60px;" @click="finishExam()"></a-button>
<!-- 展示用户头像 -->
<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"
@ -22,13 +29,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 方法 -->
<a-menu-item v-for="(item, index) in examDetail.radioIds" :key="item" @click="getQuestionDetail(item)">
<!-- 如果用户已经做过这道题通过 answersMap 判断则显示查看图标 -->
<a-icon type="eye" theme="twoTone" twoToneColor="#52c41a" v-if="answersMap.get(item)"/>
题目{{ 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)">
@ -36,6 +48,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)">
@ -48,16 +61,18 @@
<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-show="currentQuestion === ''" style="font-size: 30px;font-family: Consolas"></span>
<!-- 展示当前题目类型和题目名称使用 v-html 渲染可能包含 HTML 标签的题目名称 -->
<strong>{{ currentQuestion.type }} </strong> <p v-html="currentQuestion.name"></p>
<!-- 单选题和判断题 --> <!-- key不重复只需要在一个for循环中保证即可 -->
<!-- 单选题和判断题的选项组通过 v-if 判断题目类型进行显示绑定 change 事件 onRadioChange使用 v-model 双向绑定单选或判断题的选择值 -->
<a-radio-group @change="onRadioChange" 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>
<!-- 多选题 -->
<!-- 多选题的选项组类似单选/判断题选项组逻辑绑定 change 事件 onCheckChange使用 v-model 双向绑定多选题的选择值 -->
<a-checkbox-group @change="onCheckChange" 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 }}
@ -72,10 +87,12 @@
</a-layout>
</a-layout>
</template>
<script>
// API
import { getExamDetail, getQuestionDetail, finishExam } from '../../api/exam'
//
import UserMenu from '../../components/tools/UserMenu'
// Vuex mapGetters Vuex
import { mapGetters } from 'vuex'
export default {
@ -85,15 +102,15 @@ export default {
},
data () {
return {
//
//
examDetail: {},
// id, currentQuestion(answersidids),
// Map id currentQuestion answers idids
answersMap: {},
//
//
currentQuestion: '',
// answersMap
// answersMap
radioValue: '',
// answersMap
// answersMap
checkValues: [],
optionStyle: {
display: 'block',
@ -104,16 +121,18 @@ export default {
}
},
mounted () {
// answersMap Map
this.answersMap = new Map()
const that = this
//
// getExamDetail
getExamDetail(this.$route.params.id)
.then(res => {
if (res.code === 0) {
//
// examDetail
that.examDetail = res.data
return res.data
} else {
//
this.$notification.error({
message: '获取考试详情失败',
description: res.msg
@ -122,31 +141,32 @@ export default {
})
},
methods: {
// ,
// 使 mapGetters Vuex
...mapGetters(['nickname', 'avatar']),
getQuestionDetail (questionId) {
// content
const that = this
//
//
this.radioValue = ''
this.checkValues = []
// getQuestionDetail id
getQuestionDetail(questionId)
.then(res => {
if (res.code === 0) {
//
// currentQuestion
that.currentQuestion = res.data
// answersMapid
// answersMap
if (that.answersMap.get(that.currentQuestion.id)) {
//
// /
if (that.currentQuestion.type === '单选题' || that.currentQuestion.type === '判断题') {
that.radioValue = that.answersMap.get(that.currentQuestion.id)[0]
} else if (that.currentQuestion.type === '多选题') {
//
// 使 Object.assign
Object.assign(that.checkValues, that.answersMap.get(that.currentQuestion.id))
}
}
return res.data
} else {
//
this.$notification.error({
message: '获取问题详情失败',
description: res.msg
@ -155,21 +175,21 @@ export default {
})
},
/**
* 单选题勾选是触发的变化事件
* @param e
* 单选题勾选时触发的变化事件处理函数
* @param e 事件对象包含选中的选项值等信息
*/
onRadioChange (e) {
const userOptions = []
userOptions.push(e.target.value)
//
// answersMap idid
this.answersMap.set(this.currentQuestion.id, userOptions)
},
/**
* 多选题触发的变化事件
* @param checkedValues
* 多选题触发的变化事件处理函数
* @param checkedValues 选中的选项值数组
*/
onCheckChange (checkedValues) {
//
// answersMap id
this.answersMap.set(this.currentQuestion.id, checkedValues)
},
_strMapToObj (strMap) {
@ -180,26 +200,27 @@ export default {
return obj
},
/**
*map转换为json
* Map 对象转换为 JSON 字符串的函数
*/
_mapToJson (map) {
return JSON.stringify(this._strMapToObj(map))
},
/**
* 结束考试并交卷
* 结束考试并交卷的函数
*/
finishExam () {
// Todo:answersMap
// Todo: answersMap
finishExam(this.$route.params.id, this._mapToJson(this.answersMap))
.then(res => {
if (res.code === 0) {
//
//
this.$notification.success({
message: '考卷提交成功!'
})
this.$router.push('/list/exam-record-list')
return res.data
} else {
//
this.$notification.error({
message: '交卷失败!',
description: res.msg

@ -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>&nbsp;&nbsp;
<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!== ''">
<!-- 展示当前题目类型和题目名称使用 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(answersidids),
// Map id currentQuestion answers idids
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'
}
},
mounted () {
// answersMapanswersRightMapresultsMap 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 结构分别处理 answersMapanswersRightMapresultsMap 的赋值
*/
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
// answersMapid
// 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

@ -1,18 +1,27 @@
<template>
<div>
<!-- 使用 a-card 组件创建一个卡片式布局设置了上边距并且无边框标题为参加过的考试 -->
<a-card style="margin-top: 24px" :bordered="false" title="参加过的考试">
<!-- 在卡片的额外插槽通常用于放置一些操作按钮等元素中添加一个输入搜索框 -->
<div slot="extra">
<a-input-search style="margin-left: 16px; width: 272px;"/>
</div>
<!-- 使用 a-list 组件创建一个列表设置了较大的尺寸 -->
<a-list size="large">
<!-- 循环遍历 data 数组中的每个元素代表每条考试记录信息来生成列表项每个列表项对应一次考试记录 -->
<a-list-item :key="index" v-for="(item, index) in data">
<!-- 使用 a-list-item-meta 组件来展示列表项的元信息如头像标题描述等 -->
<a-list-item-meta :description="item.exam.examDescription">
<!-- 在头像插槽中使用 a-avatar 组件展示考试相关的头像通过管道过滤器 imgSrcFilter 处理图片地址设置了较大尺寸和方形形状 -->
<a-avatar slot="avatar" size="large" shape="square" :src="item.exam.examAvatar | imgSrcFilter"/>
<!-- 在标题插槽中展示考试名称 -->
<a slot="title">{{ item.exam.examName }}</a>
</a-list-item-meta>
<!-- 在操作插槽中添加一个查看考试详情的链接点击时会触发 viewExamRecordDetail 方法并传入对应的考试记录信息 -->
<div slot="actions">
<a @click="viewExamRecordDetail(item.examRecord)"></a>
</div>
<!-- 自定义的列表内容区域用于展示更多考试记录相关详细信息 -->
<div class="list-content">
<div class="list-content-item">
<span>Owner</span>
@ -29,53 +38,57 @@
</div>
</a-list-item>
</a-list>
</a-card>
</div>
</template>
<script>
// HeadInfo
import HeadInfo from '../../components/tools/HeadInfo'
// API
import { getExamRecordList } from '../../api/exam'
export default {
//
// 'ExamRecordList'
name: 'ExamRecordList',
components: {
HeadInfo
},
data () {
return {
// data
data: {}
}
},
methods: {
/**
* 根据考试记录的id拿到本次考试的详情并查看
* @param record 考试详情的记录
* 方法用于根据传入的考试记录信息跳转到对应的考试详情页面查看所有题目的详细情况
* @param record 考试详情的记录对象包含了如考试id考试记录id等关键信息用于构建跳转路径
*/
viewExamRecordDetail (record) {
//
// 使 $router.resolve idid
const routeUrl = this.$router.resolve({
path: `/exam/record/${record.examId}/${record.examRecordId}`
})
//
// window.open
window.open(routeUrl.href, '_blank')
}
},
mounted () {
//
// getExamRecordList
getExamRecordList().then(res => {
if (res.code === 0) {
// 0 data
this.data = res.data
} else {
// msg
this.$notification.error({
message: '获取考试记录失败',
description: res.msg
})
}
}).catch(err => {
//
//
this.$notification.error({
message: '获取考试记录失败',
description: err.message
@ -86,26 +99,40 @@ export default {
</script>
<style lang="less" scoped>
/* 针对类名为 ant-avatar-lg 的元素设置样式,通常这个类可能是来自某个 UI 组件库(比如 Ant Design中定义的大尺寸头像相关的类 */
.ant-avatar-lg {
/* 设置元素的宽度为 48 像素,用于控制头像在页面上显示的横向尺寸大小 */
width: 48px;
/* 设置元素的高度为 48 像素,配合宽度一起确定头像的整体尺寸外观 */
height: 48px;
/* 设置元素内部文本的行高为 48 像素,行高与元素高度相等时,文本在垂直方向上能实现居中对齐效果(前提是文本的其他布局相关属性设置合适),常用于让头像中的文字(如果有)垂直居中显示 */
line-height: 48px;
}
/* 针对类名为 list-content-item 的元素设置样式,从命名来看可能是用于自定义的列表内容项相关的样式类 */
.list-content-item {
/* 设置元素的文本颜色为 rgba 格式表示的颜色值,这里是带有 45% 透明度的黑色,使得文本颜色看起来相对淡一些,呈现出一种弱化显示的视觉效果,常用于次要信息的展示等场景 */
color: rgba(0, 0, 0,.45);
/* 将元素设置为行内块级元素显示方式,行内块级元素兼具了行内元素(可以和其他行内元素在同一行显示)和块级元素(可以设置宽、高、内外边距等盒模型属性)的特点,方便对列表内容项进行布局排版以及尺寸控制等操作 */
display: inline-block;
/* 设置元素在垂直方向上与同属一行的其他元素进行居中对齐,确保列表内容项在垂直方向上排列整齐,视觉上更加美观协调 */
vertical-align: middle;
/* 设置元素内文本的字体大小为 14 像素,确定文本的字号大小 */
font-size: 14px;
/* 设置元素的左边距为 40 像素,让列表内容项与左侧的其他元素或者其他列表内容项之间保持一定的间隔距离,增强视觉层次感和区分度 */
margin-left: 40px;
span {
/* 针对类名为 list-content-item 的元素内部的 <span> 标签元素设置行高为 20 像素,用于控制 <span> 标签内文本在垂直方向上的间距等显示效果,使其与父元素(.list-content-item中其他文本的显示风格有所区分或者更符合特定的布局需求 */
line-height: 20px;
}
p {
/* 设置 <p> 段落元素的顶部外边距为 4 像素,使得段落与上方的元素之间产生一定的空白间隔,避免文本过于紧凑,提升可读性和视觉效果 */
margin-top: 4px;
/* 清除 <p> 段落元素底部的外边距,防止出现多余的空白间距,保证后续元素(如果有)能按照预期紧密排列或者与其他元素的间距符合设计要求 */
margin-bottom: 0;
/* 设置 <p> 段落元素内部文本的行高为 22 像素,用于控制段落文本在垂直方向上的排版效果,合适的行高有助于提高文本的阅读舒适度和整体美观度 */
line-height: 22px;
}
}

@ -1,29 +1,39 @@
<template>
<!-- 使用 a-card 组件创建一个卡片式布局设置无边框样式 -->
<a-card :bordered="false">
<!-- 创建一个具有 id "toolbar" div 容器用于放置操作按钮作为工具条区域 -->
<div id="toolbar">
<!-- 创建一个 "新建" 按钮按钮类型为 "primary"通常表示主要操作按钮的样式带有 "plus" 图标点击按钮时调用 $refs.createExamModal.create() 方法可能用于触发创建新考试相关的操作 -->
<a-button type="primary" icon="plus" @click="$refs.createExamModal.create()"></a-button>&nbsp;
<!-- 创建一个 "刷新" 按钮按钮类型为 "primary"带有 "reload" 图标点击按钮时调用 loadAll() 方法用于重新加载数据 -->
<a-button type="primary" icon="reload" @click="loadAll()"></a-button>
</div>
<!-- 使用 BootstrapTable 组件可能是基于 Bootstrap 样式的表格组件具体功能依赖其自身实现通过 ref 属性给组件实例添加引用标识方便后续在 JavaScript 代码中通过 this.$refs 进行访问同时传入表头数据和相关配置选项等属性 -->
<BootstrapTable
ref="table"
:columns="columns"
:data="tableData"
:options="options"
/>
<!-- ref是为了方便用this.$refs.modal直接引用下同 -->
<!-- 使用 step-by-step-exam-modal 组件自定义模态框组件用于逐步创建考试相关操作具体功能由该组件内部定义同样通过 ref 属性添加引用标识并且监听该组件的 @ok 事件当事件触发时调用 handleOk 方法 -->
<step-by-step-exam-modal ref="createExamModal" @ok="handleOk" />
<!-- 这里的详情需要传进去 -->
<!-- 使用 exam-edit-modal 组件自定义模态框组件用于编辑考试相关信息具体功能由组件内部定义通过 ref 属性添加引用标识监听 @ok 事件并在触发时调用 handleOk 方法这里编辑考试详情时可能需要传入相关数据进行处理 -->
<exam-edit-modal ref="editExamModal" @ok="handleOk" />
<!-- 更新考试封面图片 -->
<!-- 使用 update-avatar-modal 组件自定义模态框组件用于更新考试封面图片相关操作具体功能由组件内部定义通过 ref 属性添加引用标识监听 @ok 事件并在触发时调用 handleOk 方法 -->
<update-avatar-modal ref="updateAvatarModal" @ok="handleOk" />
</a-card>
</template>
<script>
// bootstrap-table
import '../../plugins/bootstrap-table'
// API
import { getExamAll } from '../../api/exam'
// StepByStepExamModal
import StepByStepExamModal from './modules/StepByStepExamModal'
// ExamEditModal
import ExamEditModal from './modules/ExamEditModal'
// UpdateAvatarModal
import UpdateAvatarModal from '@views/list/modules/UpdateAvatarModal'
export default {
@ -34,27 +44,32 @@ export default {
StepByStepExamModal
},
data () {
const that = this // 便bootstrap-tablemethods
const that = this; // 便 bootstrap-table
return {
//
//
columns: [
{
// ""
title: '序号',
//
field: 'serial',
// 1 1
formatter: function (value, row, index) {
return index + 1 // 1
return index + 1;
}
},
{
title: '封面',
field: 'avatar',
width: 50,
// value "exam-avatar" div
formatter: (value, row) => {
return '<div class="exam-avatar">' + value + '</div>'
return '<div class="exam-avatar">' + value + '</div>';
},
events: {
// "exam-avatar" that.handleAvatarEdit(row) row
'click.exam-avatar': function (e, value, row, index) {
that.handleAvatarEdit(row)
that.handleAvatarEdit(row);
}
}
},
@ -83,33 +98,43 @@ export default {
title: '操作',
field: 'action',
width: '150px',
// "" "" HTML
formatter: (value, row) => {
return '<button type="button" class="btn btn-success view-exam">详情</button>' +
'&nbsp;&nbsp;' +
'<button type="button" class="btn btn-success edit-exam">编辑</button>'
'<button type="button" class="btn btn-success edit-exam">编辑</button>';
},
events: {
// "view-exam" that.handleSub(row) row
'click.view-exam': function (e, value, row, index) {
that.handleSub(row)
that.handleSub(row);
},
// "edit-exam" that.handleEdit(row) row
'click.edit-exam': function (e, value, row, index) {
that.handleEdit(row)
that.handleEdit(row);
}
}
}
],
tableData: [], // bootstrap-table
// custom bootstrap-table
// bootstrap-table
tableData: [],
// bootstrap-table
options: {
//
search: true,
// /
showColumns: true,
// Excel
showExport: true,
//
pagination: true,
// HTML id id "toolbar" div
toolbar: '#toolbar',
//
//
advancedSearch: true,
idTable: 'advancedTable'
// http://www.itxst.com/bootstrap-table-events/tutorial.html
// 使
idTable: 'advancedTable',
// 使
// onClickRow: that.clickRow,
// onClickCell: that.clickCell, //
// onDblClickCell: that.dblClickCell //
@ -117,50 +142,55 @@ export default {
}
},
mounted () {
this.loadAll() //
// loadAll()
this.loadAll();
},
methods: {
handleEdit (record) {
// Todo:
console.log('开始编辑啦')
console.log(record)
this.$refs.editExamModal.edit(record)
// Todo:
console.log('开始编辑啦');
console.log(record);
this.$refs.editExamModal.edit(record);
},
handleAvatarEdit (record) {
// Todo:
console.log('开始更新封面啦')
console.log(record)
this.$refs.updateAvatarModal.edit(record)
// Todo:
console.log('开始更新封面啦');
console.log(record);
this.$refs.updateAvatarModal.edit(record);
},
handleSub (record) {
//
// console.log(record)
// this.$refs.modalView.edit(record)
//
// console.log(record);
// this.$refs.modalView.edit(record);
//
// $router.resolve 使 record.id
const routeUrl = this.$router.resolve({
path: `/exam/${record.id}`
})
//
window.open(routeUrl.href, '_blank')
});
// 使 window.open
window.open(routeUrl.href, '_blank');
},
handleOk () {
this.loadAll()
// @ok
this.loadAll();
},
loadAll () {
const that = this
const that = this;
// getExamAll()
getExamAll()
.then(res => {
if (res.code === 0) {
that.tableData = res.data
that.$refs.table._initTable()
// 0 tableData _initTable()
that.tableData = res.data;
that.$refs.table._initTable();
} else {
// "" msg
that.$notification.error({
message: '获取全部考试的列表失败',
description: res.msg
})
});
}
})
});
}
}
}

Loading…
Cancel
Save