This commit is contained in:
2025-07-15 10:30:42 +08:00
parent 232b68313d
commit c6a45a9fff
9 changed files with 232 additions and 139 deletions

View File

@ -1,4 +1,4 @@
import { post } from '@/utils/request'
import { post, get } from '@/utils/request'
/**
* 试卷列表
@ -7,3 +7,9 @@ import { post } from '@/utils/request'
export function listPaper(userId, examId) {
return post('/exam/api/paper/paper/paging', { current: 1, size: 5, params: { userId: userId, examId: examId } })
}
// 准备考试
export function getRegExamList() {
return get(`/exam/api/exam/registration/getRegExamList?userId=${JSON.parse(localStorage.getItem("user")).id}&finishState=0`)
}

View File

@ -115,14 +115,12 @@ export const asyncRoutes = [
roles: ['student', 'sa']
},
children: [
{
path: 'exam',
component: () => import('@/views/paper/exam/list'),
name: 'ExamOnline',
meta: { title: '在线考试', noCache: true, icon: 'guide' }
meta: { title: '模拟考试', noCache: true, icon: 'guide' }
},
{
path: 'exam/prepare/:examId',
component: () => import('@/views/paper/exam/preview'),
@ -130,7 +128,12 @@ export const asyncRoutes = [
meta: { title: '准备考试', noCache: true, activeMenu: '/my/exam' },
hidden: true
},
{
path: 'exam/prepareexam',
component: () => import('@/views/paper/exam/prepareexam'),
name: 'PrepareExam',
meta: { title: '准备考试', noCache: true, icon: 'log' }
},
{
path: 'exam/result/:id',
component: () => import('@/views/paper/exam/result'),
@ -138,14 +141,12 @@ export const asyncRoutes = [
meta: { title: '考试结果', noCache: true, activeMenu: '/online/exam' },
hidden: true
},
{
path: 'exam/records',
component: () => import('@/views/user/exam/my'),
name: 'ListMyExam',
meta: { title: '我的成绩', noCache: true, icon: 'results' }
},
{
path: 'book/list/:examId',
component: () => import('@/views/user/book'),
@ -153,7 +154,6 @@ export const asyncRoutes = [
meta: { title: '考试错题', noCache: true, activeMenu: '/my/exam/records' },
hidden: true
},
{
path: 'book/training/:examId',
component: () => import('@/views/user/book/train'),
@ -161,7 +161,6 @@ export const asyncRoutes = [
meta: { title: '错题训练', noCache: true, activeMenu: '/my/exam/records' },
hidden: true
}
]
},

View File

@ -1,6 +1,7 @@
import { login, reg, logout, getInfo } from '@/api/user'
import { getToken, setToken, removeToken } from '@/utils/auth'
import router, { resetRouter } from '@/router'
import Cookies from 'js-cookie'
const state = {
token: getToken(),
@ -43,8 +44,15 @@ const actions = {
return new Promise((resolve, reject) => {
login({ username: username.trim(), password: password }).then(response => {
const { data } = response
console.log(data)
commit('SET_TOKEN', data.token)
setToken(data.token)
localStorage.setItem("user", JSON.stringify({
id: data.id,
cardNo: data.userName,
realName: data.realName,
phone: data.phone,
}));
resolve()
}).catch(error => {
reject(error)

View File

@ -171,3 +171,15 @@ export function post(url, data = {}) {
})
})
}
export function get(url, data = {}) {
return new Promise((resolve, reject) => {
instance.get(url, { params: data })
.then(response => {
resolve(response)
}, err => {
reject(err)
})
})
}

View File

@ -134,7 +134,7 @@ export default {
filterText: '',
treeLoading: false,
dateValues: [],
timeValues: undefined,
timeValues: [],
//
repoList: [],
//

View File

@ -1,6 +1,9 @@
<template>
<data-table ref="pagingTable" :options="options" :list-query="listQuery">
<template #filter-content>
<el-select v-model="listQuery.params.examType" class="filter-item" placeholder="考试类型" clearable>
<el-option v-for="item in examTypes" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-select v-model="listQuery.params.openType" class="filter-item" placeholder="开放类型" clearable>
<el-option v-for="item in openTypes" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
@ -13,6 +16,12 @@
<template #data-columns>
<el-table-column label="考试名称" prop="title" />
<el-table-column label="考试类型" align="center">
<template v-slot="scope">
{{ scope.row.examType == 1 ? '模拟考试' : '' }}
{{ scope.row.examType == 2 ? '正式考试' : '' }}
</template>
</el-table-column>
<el-table-column label="开放类型" align="center">
<template v-slot="scope">
{{ scope.row.openType | examOpenType }}
</template>
@ -52,6 +61,16 @@ export default {
components: { DataTable },
data() {
return {
examTypes: [
{
value: 1,
label: '模拟考试'
},
{
value: 2,
label: '正式考试'
}
],
openTypes: [
{
value: 1,

View File

@ -1,33 +1,23 @@
<template>
<div class="app-container">
<el-row :gutter="24">
<el-col :span="24">
<el-card style="margin-bottom: 10px">
距离考试结束还有
<exam-timer v-model="paperData.leftSeconds" @timeout="doHandler()" />
<el-button :loading="loading" style="float: right; margin-top: -10px" type="primary" icon="el-icon-plus"
@click="handHandExam()">
{{ handleText }}
</el-button>
</el-card>
</el-col>
<el-col :span="5" :xs="24" style="margin-bottom: 10px">
<el-card class="content-h">
<p class="card-title">答题卡</p>
<el-row :gutter="24" class="card-line" style="padding-left: 10px">
<el-tag type="info">未作答</el-tag>
<el-tag type="success">已作答</el-tag>
</el-row>
<div v-if="paperData.radioList !== undefined && paperData.radioList.length > 0">
<p class="card-title">单选题</p>
<el-row :gutter="24" class="card-line">
@ -35,7 +25,6 @@
@click="handSave(item)"> {{ item.sort + 1 }}</el-tag>
</el-row>
</div>
<div v-if="paperData.multiList !== undefined && paperData.multiList.length > 0">
<p class="card-title">多选题</p>
<el-row :gutter="24" class="card-line">
@ -43,7 +32,6 @@
@click="handSave(item)">{{ item.sort + 1 }}</el-tag>
</el-row>
</div>
<div v-if="paperData.judgeList !== undefined && paperData.judgeList.length > 0">
<p class="card-title">判断题</p>
<el-row :gutter="24" class="card-line">
@ -51,13 +39,9 @@
@click="handSave(item)">{{ item.sort + 1 }}</el-tag>
</el-row>
</div>
</el-card>
</el-col>
<el-col :span="19" :xs="24">
<el-card class="qu-content content-h">
<p v-if="quData.content">{{ quData.sort + 1 }}.{{ quData.content }}</p>
<p v-if="quData.image != null && quData.image != ''">
@ -72,9 +56,7 @@
</el-radio>
</el-radio-group>
</div>
<div v-if="quData.quType === 2">
<el-checkbox-group v-model="multiValue">
<el-checkbox v-for="item in quData.answerList" :key="item.id" :label="item.id">{{ item.abc }}.{{
item.content }}
@ -83,34 +65,24 @@
</div>
</el-checkbox>
</el-checkbox-group>
</div>
<div style="margin-top: 20px">
<el-button v-if="showPrevious" type="primary" icon="el-icon-back" @click="handPrevious()">
上一题
</el-button>
<el-button v-if="showNext" type="warning" icon="el-icon-right" @click="handNext()">
下一题
</el-button>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
import { paperDetail, quDetail, handExam, fillAnswer } from '@/api/paper/exam'
import { Loading } from 'element-ui'
import ExamTimer from '@/views/paper/exam/components/ExamTimer'
export default {
name: 'ExamProcess',
components: { ExamTimer },
@ -144,7 +116,9 @@ export default {
//
multiValue: [],
// ID
answeredIds: []
answeredIds: [],
switchCount: 0,
maxSwitchAllowed: 3
}
},
created() {
@ -154,97 +128,86 @@ export default {
this.fetchData(id)
}
},
mounted() {
this.setupAntiCheat();
},
methods: {
//
cardItemClass(answered, quId) {
if (quId === this.cardItem.quId) {
return 'warning'
}
if (answered) {
return 'success'
}
if (!answered) {
return 'info'
}
},
/**
* 统计有多少题没答的
* @returns {number}
*/
countNotAnswered() {
let notAnswered = 0
this.paperData.radioList.forEach(item => {
if (!item.answered) {
notAnswered += 1
}
})
this.paperData.multiList.forEach(item => {
if (!item.answered) {
notAnswered += 1
}
})
this.paperData.judgeList.forEach(item => {
if (!item.answered) {
notAnswered += 1
}
})
return notAnswered
},
/**
* 下一题
*/
handNext() {
this.requestFullScreen();
const index = this.cardItem.sort + 1
this.handSave(this.allItem[index])
},
/**
* 上一题
*/
handPrevious() {
this.requestFullScreen();
const index = this.cardItem.sort - 1
this.handSave(this.allItem[index])
},
doHandler() {
//
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
window.removeEventListener('blur', this.handleBlur);
this.handleText = '正在交卷,请等待...'
this.loading = true
const params = { id: this.paperId }
handExam(params).then(() => {
this.$message({
message: '试卷提交成功,即将进入试卷详情!',
type: 'success'
})
this.$router.push({ name: 'ShowExam', params: { id: this.paperId } })
})
},
//
handHandExam() {
const that = this
//
this.handSave(this.cardItem, () => {
const notAnswered = that.countNotAnswered()
let msg = '确认要交卷吗?'
if (notAnswered > 0) {
msg = '您还有' + notAnswered + '题未作答,确认要交卷吗?'
}
that.$confirm(msg, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
@ -259,7 +222,6 @@ export default {
})
})
},
//
handSave(item, callback) {
if (item.id === this.allItem[0].id) {
@ -267,21 +229,17 @@ export default {
} else {
this.showPrevious = true
}
//
const last = this.allItem.length - 1
if (item.id === this.allItem[last].id) {
this.showNext = false
} else {
this.showNext = true
}
const answers = this.multiValue
if (this.radioValue !== '') {
answers.push(this.radioValue)
}
const params = { paperId: this.paperId, quId: this.cardItem.quId, answers: answers, answer: '' }
fillAnswer(params).then(() => {
//
@ -289,17 +247,14 @@ export default {
//
this.cardItem.answered = true
}
//
if (callback) {
callback()
}
//
this.fetchQuData(item)
})
},
//
fetchQuData(item) {
//
@ -307,10 +262,8 @@ export default {
text: '拼命加载中',
background: 'rgba(0, 0, 0, 0.7)'
})
//
this.cardItem = item
//
const params = { paperId: this.paperId, quId: item.quId }
quDetail(params).then(response => {
@ -318,30 +271,25 @@ export default {
this.quData = response.data
this.radioValue = ''
this.multiValue = []
//
this.quData.answerList.forEach((item) => {
if ((this.quData.quType === 1 || this.quData.quType === 3) && item.checked) {
this.radioValue = item.id
}
if (this.quData.quType === 2 && item.checked) {
this.multiValue.push(item.id)
}
})
//
loading.close()
})
},
//
fetchData(id) {
const params = { id: id }
paperDetail(params).then(response => {
//
this.paperData = response.data
//
if (this.paperData.radioList && this.paperData.radioList.length > 0) {
this.cardItem = this.paperData.radioList[0]
@ -350,26 +298,67 @@ export default {
} else if (this.paperData.judgeList && this.paperData.judgeList.length > 0) {
this.cardItem = this.paperData.judgeList[0]
}
const that = this
this.paperData.radioList.forEach(item => {
that.allItem.push(item)
})
this.paperData.multiList.forEach(item => {
that.allItem.push(item)
})
this.paperData.judgeList.forEach(item => {
that.allItem.push(item)
})
//
this.fetchQuData(this.cardItem)
})
},
requestFullScreen() {
const element = document.documentElement;
console.log(element.requestFullscreen)
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen();
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
}
},
setupAntiCheat() {
document.addEventListener('visibilitychange', this.handleVisibilityChange);
window.addEventListener('blur', this.handleBlur);
},
handleVisibilityChange() {
if (document.hidden) {
this.handleScreenSwitch();
}
},
handleBlur() {
this.handleScreenSwitch();
},
handleScreenSwitch() {
this.switchCount++;
if (this.switchCount >= this.maxSwitchAllowed) {
alert('您已超过最大切屏次数,考试将自动提交!');
this.doHandler();
} else {
alert(`警告:请勿切换窗口!剩余允许次数:${this.maxSwitchAllowed - this.switchCount}`);
}
},
},
beforeDestroy() {
exitFullScreen();
//
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
window.removeEventListener('blur', this.handleBlur);
}
}
function exitFullScreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
</script>

View File

@ -3,27 +3,14 @@
<div>
<div v-if="breakShow" style=" cursor: pointer; padding: 20px 20px 0px 20px" @click="toExam">
<el-alert
:closable="false"
title="您有正在进行的考试,离线太久考试将被作废哦,点击此处可继续考试!"
type="error"
/>
<el-alert :closable="false" title="您有正在进行的模拟考试,离线太久考试将被作废哦,点击此处可继续考试!" type="error" />
</div>
<data-table
ref="pagingTable"
:options="options"
:list-query="listQuery"
>
<data-table ref="pagingTable" :options="options" :list-query="listQuery">
<template #filter-content>
<el-select v-model="listQuery.params.openType" class="filter-item" placeholder="开放类型" clearable>
<el-option
v-for="item in openTypes"
:key="item.value"
:label="item.label"
:value="item.value"
/>
<el-option v-for="item in openTypes" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-input v-model="listQuery.params.title" placeholder="搜索考试名称" style="width: 200px;" class="filter-item" />
@ -32,27 +19,16 @@
<template #data-columns>
<el-table-column
label="考试名称"
prop="title"
show-overflow-tooltip
/>
<el-table-column label="考试名称" prop="title" show-overflow-tooltip />
<el-table-column
label="考试类型"
align="center"
>
<el-table-column label="开放类型" align="center">
<template v-slot="scope">
{{ scope.row.openType | examOpenType }}
</template>
</el-table-column>
<el-table-column
label="考试时间"
width="220px"
align="center"
>
<el-table-column label="考试时间" width="220px" align="center">
<template v-slot="scope">
<span v-if="scope.row.timeLimit">
@ -63,10 +39,7 @@
</el-table-column>
<el-table-column
label="考试时长"
align="center"
>
<el-table-column label="考试时长" align="center">
<template v-slot="scope">
{{ scope.row.totalTime }}分钟
@ -74,24 +47,14 @@
</el-table-column>
<el-table-column
label="考试总分"
prop="totalScore"
align="center"
/>
<el-table-column label="考试总分" prop="totalScore" align="center" />
<el-table-column
label="及格线"
prop="qualifyScore"
align="center"
/>
<el-table-column label="及格线" prop="qualifyScore" align="center" />
<el-table-column
label="操作"
align="center"
>
<el-table-column label="操作" align="center">
<template v-slot="scope">
<el-button v-if="scope.row.state===0" icon="el-icon-caret-right" type="primary" size="mini" @click="handlePre(scope.row.id)">去考试</el-button>
<el-button v-if="scope.row.state === 0" icon="el-icon-caret-right" type="primary" size="mini"
@click="handlePre(scope.row.id)">去考试</el-button>
<el-button v-if="scope.row.state === 1" icon="el-icon-s-release" size="mini" disabled>已禁用</el-button>
<el-button v-if="scope.row.state === 2" icon="el-icon-s-fold" size="mini" disabled>待开始</el-button>
<el-button v-if="scope.row.state === 3" icon="el-icon-s-unfold" size="mini" disabled>已结束</el-button>
@ -132,9 +95,9 @@ export default {
current: 1,
size: 10,
params: {
examType: 1,
}
},
options: {
//
multi: false,
@ -145,7 +108,7 @@ export default {
},
created() {
this.check()
// this.check()
},
methods: {

View File

@ -0,0 +1,97 @@
<template>
<div class="app-container">
<el-row :gutter="24">
<el-col :span="24" style="margin-bottom: 20px">
<el-alert title="点击`开始考试`后将自动进入考试,请诚信考试!" type="error" style="margin-bottom: 10px" />
<el-card class="pre-exam">
<div><strong>考试名称</strong>{{ detailData.title }}</div>
<div style="color: red;"><strong>考试费用</strong>{{ detailData.examFee }}</div>
<div><strong>考试时长</strong>{{ detailData.totalTime }}分钟</div>
<div><strong>试卷总分</strong>{{ detailData.totalScore }}</div>
<div><strong>及格分数</strong>{{ detailData.qualifyScore }}</div>
<div><strong>考试日期</strong>{{ detailData.startDate }}{{ detailData.endDate }}</div>
<div><strong>考试时间</strong>{{ detailData.startTime }}{{ detailData.endTime }}</div>
<div><strong>考试人员</strong>{{ user.realName }}</div>
<div><strong>身份证</strong>{{ user.cardNo }}</div>
<div><strong>手机号</strong>{{ user.phone }}</div>
<div><strong>考试描述</strong>{{ detailData.content }}</div>
</el-card>
</el-col>
<el-col :span="24">
<el-button :loading="loading" type="primary" icon="el-icon-caret-right" @click="handleCreate">
开始考试
</el-button>
<el-button @click="handleBack">
返回
</el-button>
</el-col>
</el-row>
</div>
</template>
<script>
import { getRegExamList } from '@/api/paper/paper'
import { createPaper } from '@/api/paper/exam'
export default {
data() {
return {
user: JSON.parse(localStorage.getItem("user")),
detailData: {},
postForm: {
examId: '',
userId: JSON.parse(localStorage.getItem("user")).id,
},
loading: false,
}
},
created() {
this.fetchData()
},
methods: {
fetchData() {
getRegExamList().then(response => {
this.detailData = response.data[0] || {}
this.postForm.examId = this.detailData.examId
})
},
handleCreate() {
this.requestFullScreen();
const that = this
this.loading = true
createPaper(this.postForm).then(response => {
if (response.code === 0) {
setTimeout(() => {
that.loading = false
that.dialogVisible = false
that.$router.push({ name: 'StartExam', params: { id: response.data.id } })
}, 1000)
}
}).catch(() => {
this.loading = false
})
},
handleBack() {
this.$router.push({ name: 'ExamOnline' })
},
requestFullScreen() {
const element = document.documentElement;
console.log(element.requestFullscreen)
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen();
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
}
},
},
}
</script>
<style scoped>
.pre-exam div {
line-height: 42px;
color: #555555;
}
</style>