这个前端代码的剩余时间倒计时有点问题,过一段时间之后就会卡死<template>
<div class="container learnstyle-page" @contextmenu.prevent>
<el-card class="box-card">
<div slot="header" class="clearfix" style="font-size: 32px;">
<span style="margin-left: 2%; font-weight: bold;">
{{testPaperInfo.name}}
<sys-select v-model="testPaperInfo.paperType" dict-type="paper_type" showType="text" />
(共{{testPaperInfo.questionNumber}}题 合计{{testPaperInfo.mark}}分)
</span>
<span style="float: right; color: red; margin-right: 2%;">考试剩余时间: {{timeValue}}</span>
</div>
<question-info
ref="questionInfoRef"
:showNumberTitle="false"
@openWritingBoard="openWritingBoard"
:timeOutFlag="timeOutFlag"
commitButtonText="提交试卷"
@afterCommit="afterCommit"
:questionInfoAnswerList="testPaperInfoQuestionList">
</question-info>
</el-card>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import questionInfo from '@/components/question-info'
import { isEmpty } from '@/utils/system-utils'
export default {
name: 'exam',
components: {questionInfo},
data () {
return {
studentExamTime: 0, // 学生考试用时
studentExamTimer: null,
examTime: null,
examTimeKey: 'examTime:',
// timeInterval: null,
timeTarget: null,
endTime: null,
timeOutFlag: false, // 是否考试时间到
openAnswerFlag: false,
leaveNumber: 0,
currentQuestion: {},
testPaperInfoQuestionList: [], // 存储用户答题答案及试题答案解析
currentPage: 0,
timeValue: '',
pageSize: 1,
totalCount: 0,
openWritingBoardFlag: false,
questionTypeList: [],
serverTime: null, // 服务器时间
timeDiff: 0, // 服务器时间与本地时间差值
isCommit: false,
isFullscreen: true, // 标记是否处于全屏状态
blurTimer: null, // 失去焦点计时器
blurCount: 0, // 失去焦点次数
maxBlurCount: 3, // 最大允许失去焦点次数
warningCount: 0, // 警告次数
maxWarningCount: 5, // 最大警告次数
isPageBlurred: false, // 页面是否失去焦点
blurStartTime: null, // 失去焦点开始时间
warningSubmit: false
}
},
computed: {
...mapGetters({
testPaperInfo: 'common/getTestPaperInfo'
})
},
beforeCreate () {
},
mounted () {
this.startListener()
this.axios.get(this.$httpApi.httpUrl('/student/testPaperInfo/systemTime'), {
params: {}
}).then(response => {
this.isCommit = false
this.serverTime = response.data.data
// 计算服务器时间与本地时间的差值
this.timeDiff = this.serverTime - new Date().getTime()
if (this.testPaperInfo.examTime) {
let examTimeObj = sessionStorage.getItem(this.examTimeKey + this.testPaperInfo.id)
if (examTimeObj) {
this.examTime = parseInt(examTimeObj)
} else {
this.examTime = this.testPaperInfo.examTime * 60 * 1000
}
let now = this.getCurrentTime()
if (!isEmpty(this.testPaperInfo.enterDate)) {
now = new Date(this.testPaperInfo.enterDate).getTime()
}
this.endTime = now + this.examTime
if (this.testPaperInfo.endTime) {
this.endTime = new Date(this.testPaperInfo.endTime).getTime()
}
this.timeTarget = setInterval(() => {
this.startTime(); // 开始倒计时
}, 1000)
}
this.studentExamTimer = setInterval(() => {
this.startCountStudentExamTime() // 开始计算考试时间
}, 1000)
this.getPaperQuestionList()
})
},
beforeRouteLeave(to, from, next) {
// 用户确认离开,提交试卷
// this.$refs.questionInfoRef.commitQuestion(true)
next()
},
destroyed() {
sessionStorage.removeItem(this.examTimeKey + this.testPaperInfo.id)
clearInterval(this.timeTarget)
clearInterval(this.studentExamTimer)
this.endListener()
},
methods: {
startListener () {
// 添加全屏变化事件监听
document.addEventListener('fullscreenchange', this.fullscreenChangeHandler)
document.addEventListener('webkitfullscreenchange', this.fullscreenChangeHandler)
document.addEventListener('mozfullscreenchange', this.fullscreenChangeHandler)
document.addEventListener('MSFullscreenChange', this.fullscreenChangeHandler)
// 禁用鼠标右键
document.addEventListener('contextmenu', this.preventRightClick)
document.addEventListener('keydown', this.preventDefault);
},
endListener () {
// 移除全屏变化事件监听
document.removeEventListener('fullscreenchange', this.fullscreenChangeHandler)
document.removeEventListener('webkitfullscreenchange', this.fullscreenChangeHandler)
document.removeEventListener('mozfullscreenchange', this.fullscreenChangeHandler)
document.removeEventListener('MSFullscreenChange', this.fullscreenChangeHandler)
// 移除右键禁用事件监听
document.removeEventListener('contextmenu', this.preventRightClick);
document.removeEventListener('keydown', this.preventDefault);
},
// 处理页面失去焦点
handlePageBlur() {
if (this.isCommit) return;
this.isPageBlurred = true;
this.blurStartTime = new Date().getTime();
// 启动计时器,如果失去焦点超过一定时间则认为切屏
if (this.blurTimer) {
clearTimeout(this.blurTimer);
}
this.blurTimer = setTimeout(() => {
if (this.isPageBlurred && !this.isCommit) {
this.handlePotentialScreenSwitch();
}
}, 500);
},
// 处理页面获得焦点
handlePageFocus() {
if (this.isCommit) return;
this.isPageBlurred = false;
if (this.blurTimer) {
clearTimeout(this.blurTimer);
this.blurTimer = null;
}
// 如果页面失去焦点时间超过阈值,增加切屏计数
if (this.blurStartTime) {
const blurDuration = new Date().getTime() - this.blurStartTime;
if (blurDuration > 1000) { // 失去焦点超过2秒
this.blurCount++;
this.showBlurWarning();
}
this.blurStartTime = null;
}
},
// 处理页面可见性变化
handleVisibilityChange() {
if (document.hidden) {
this.handlePageBlur();
} else {
this.handlePageFocus();
}
// 调用原有的 visibilityChangeEvent 方法
this.visibilityChangeEvent();
},
handleback () {
history.pushState(null, null, location.href)
},
// 处理可能的切屏行为
handlePotentialScreenSwitch() {
this.blurCount++;
if (!this.warningSubmit) {
this.autoCommitForScreenSwitch();
}
// 如果切屏次数超过限制,自动提交试卷
// if (this.blurCount >= this.maxBlurCount) {
// this.autoCommitForScreenSwitch();
// }
},
// 显示切屏警告
showBlurWarning() {
this.warningCount++;
this.$message({
// message: `检测到切屏行为,这是第 ${this.blurCount} 次。请注意考试纪律!`,
message: `检测到切屏行为,将自动提交试卷!`,
type: 'warning',
duration: 3000
});
// 如果警告次数过多,自动提交试卷
if (this.warningCount >= this.maxWarningCount) {
this.autoCommitForWarnings();
this.warningSubmit = true
}
},
// 因切屏次数过多自动提交试卷
autoCommitForScreenSwitch() {
this.$alert('检测到切屏行为,系统将自动提交试卷。', '考试结束', {
confirmButtonText: '确定',
type: 'error',
callback: () => {
this.$refs.questionInfoRef.commitQuestion(true);
}
});
},
// 因警告次数过多自动提交试卷
autoCommitForWarnings() {
this.$alert('检测到多次违规操作,系统将自动提交试卷。', '考试结束', {
confirmButtonText: '确定',
type: 'error',
callback: () => {
this.$refs.questionInfoRef.commitQuestion(true);
}
});
},
preventDefault (e) {
// 禁用常见的切屏快捷键
if (
// F5刷新
e.keyCode === 116 ||
// Ctrl+R
(e.ctrlKey && e.keyCode === 82) ||
// Ctrl+F5
(e.ctrlKey && e.keyCode === 116) ||
// ESC键
e.keyCode === 27 ||
// Win键(左右)
e.keyCode === 91 || e.keyCode === 92 ||
// Alt+Tab
(e.altKey && e.keyCode === 9) ||
// Ctrl+Alt+Del
(e.ctrlKey && e.altKey && e.keyCode === 46) ||
// Ctrl+Alt+.
(e.ctrlKey && e.altKey && e.keyCode === 190) ||
// Alt+F4
(e.altKey && e.keyCode === 115) ||
// 禁用F1、F12
// (e.keyCode === 112 || e.keyCode === 123) ||
// 禁用Ctrl+W(关闭窗口)
(e.ctrlKey && e.keyCode === 87) ||
// 禁用Win+Tab
((e.keyCode === 91 || e.keyCode === 92) && e.keyCode === 9) ||
//
(e.altKey && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) ||
//
(e.key === 'Backspace' && e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA')
) {
e.preventDefault();
e.stopPropagation();
this.warningCount++;
return false;
}
// 禁用Alt键单独按下(防止Alt+Tab)
if (e.altKey && e.keyCode !== 9) {
e.preventDefault();
e.stopPropagation();
this.warningCount++;
return false;
}
},
handleTouchEnd(event) {
if (!this.disableSwipe) return;
const touchEndX = event.changedTouches[0].clientX;
const touchEndY = event.changedTouches[0].clientY;
const diffX = touchEndX - this.touchStartX;
const diffY = touchEndY - this.touchStartY;
// 判断是否是水平滑动(前进/后退手势)
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 30) {
event.preventDefault();
if (diffX > 0) {
console.log('右滑后退已被阻止');
} else {
console.log('左滑前进已被阻止');
}
}
},
// 处理触摸移动事件
handleTouchMove(event) {
if (this.disableSwipe) {
event.preventDefault();
console.log('触摸滑动已被阻止');
}
},
// 处理鼠标按下事件
handleMouseDown(event) {
this.mouseDownButton = event.button;
// 检测侧键(前进/后退)
if (this.disableMouseButtons && (event.button === 3 || event.button === 4)) {
event.preventDefault();
const buttonName = event.button === 3 ? '后退' : '前进';
console.log(`鼠标${buttonName}键按下已被阻止`);
}
},
// 处理鼠标释放事件
handleMouseUp(event) {
// 检测侧键(前进/后退)
if (this.disableMouseButtons && (event.button === 3 || event.button === 4)) {
event.preventDefault();
const buttonName = event.button === 3 ? '后退' : '前进';
console.log(`鼠标${buttonName}键释放已被阻止`);
}
this.mouseDownButton = null;
},
// 阻止右键菜单
preventRightClick(event) {
event.preventDefault();
return false;
},
keyDownHandler (event) {
console.log('keyDownHandler', event.keyCode)
if (event.key === 'Escape' && this.isFullscreen) {
event.preventDefault()
this.$confirm('退出全屏将自动提交试卷,是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 用户确认退出,提交试卷
this.$refs.questionInfoRef.commitQuestion(true)
}).catch(() => {
// 用户取消退出,重新进入全屏
})
}
},
// 全屏变化事件处理
fullscreenChangeHandler() {
// 检查是否退出了全屏模式
const isCurrentlyFullscreen = document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement;
// 如果之前是全屏状态,现在不是全屏状态,说明用户手动退出了全屏
if (this.isFullscreen && !isCurrentlyFullscreen && !this.isCommit) {
this.$refs.questionInfoRef.commitQuestion(true)
}
this.isFullscreen = !!isCurrentlyFullscreen
},
// 处理用户手动退出全屏的情况
handleManualExitFullscreen() {
// 如果试卷还未提交,则提示用户并自动提交
if (!this.isCommit) {
this.$confirm('退出全屏将自动提交试卷,是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 用户确认退出,提交试卷
this.$refs.questionInfoRef.commitQuestion(true)
}).catch(() => {
// 用户取消退出,重新进入全屏
this.enterFullscreen()
})
}
},
// 进入全屏模式
enterFullscreen() {
const element = document.documentElement;
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen();
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
}
this.isFullscreen = true
},
// 退出全屏模式
exitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
this.isFullscreen = false
},
// 获取当前准确时间(基于服务器时间)
getCurrentTime() {
return new Date().getTime() + this.timeDiff
},
openWritingBoard () {
this.openWritingBoardFlag = true
},
startCountStudentExamTime () {
this.studentExamTime++
},
// 监听页面离开事件
visibilityChangeEvent (e) {
// 考试中请勿离开当前页面,系统将自动提交
if (!this.isCommit) {
this.$refs.questionInfoRef.commitQuestion(true)
}
},
startTime() {
if (this.timeValue === '0:00:00' || this.timeValue === '00:00:00') {
clearInterval(this.timeTarget)
this.$notify({
title: '警告',
duration: 3000,
message: '考试时间到,系统将自动提交本次考试',
type: 'warning'
})
setTimeout(() => {
this.timeOutFlag = true
}, 100)
return false
}
let nowTime = this.getCurrentTime();
let chaTime = this.endTime - nowTime;
//倒计时的时间
if (chaTime >= 0) {
let d = Math.floor(chaTime / 1000 / 60 / 60 / 24);
let h = Math.floor(chaTime / 1000 / 60 / 60 % 24);
if (h <= 9) {
h = 0 + '' + h
}
let m = Math.floor(chaTime / 1000 / 60 % 60);
if (m <= 9) {
m = 0 + '' + m
}
let s = Math.floor(chaTime / 1000 % 60);
if (s <= 9) {
s = 0 + '' + s
}
if (d > 0) {
this.timeValue = d + '天' + h + '时' + m + '分' + s + '秒'
} else {
this.timeValue = h + ':' + m + ':' + s
}
}
this.examTime = this.examTime - 100
// 缓存考试时间,防止刷新页面考试时间仍然不变的问题
sessionStorage.setItem(this.examTimeKey + this.testPaperInfo.id, this.examTime)
},
selectQuestion (index) {
},
// 答题卡
openAnswerSheet () {
this.openAnswerFlag = true
},
// 获取试卷试题列表
getPaperQuestionList () {
this.axios.get(this.$httpApi.httpUrl('/student/testPaperInfo/selectPaperQuestionById'), {
params: {
id: this.testPaperInfo.id
}
}).then(response => {
this.testPaperInfoQuestionList = response.data.data
this.testPaperInfoQuestionList.forEach(item =>{
let questionType = item.questionType
if (questionType !== 1 && questionType !== 6) {
this.$set(item, 'studentAnswer', [])
// item.studentAnswer = [] 此方式会导致v-model 无法绑定变量
} else {
this.$set(item, 'studentAnswer', '')
}
})
})
},
afterCommit (questionAnswerParam) {
if (this.isCommit) {
return
}
this.isCommit = true
clearInterval(this.studentExamTimer)
let params = {
examTime: this.studentExamTime,
testPaperInfoId: this.testPaperInfo.id,
questionAnswerList: questionAnswerParam
}
this.axios.post(this.$httpApi.httpUrl('/student/testPaperInfo/commitPaper'), params)
.then(response => {
// 提交成功后退出全屏模式
this.exitFullscreen();
this.endListener()
this.$message.success('提交成功')
this.$router.push('examHistory')
})
}
},
filters: {
}
}
</script>
<style scoped>
.container.learnstyle-page {
width: 100%;
padding: 0 8%;
margin-top: 1%;
}
.exam_time {
color: red;
font-size: 24px;
position: relative;
left: 30%;
bottom: 80%
}
.back{
color:#ca0000;
cursor: pointer;
}
.text {
font-size: 14px;
}
.oper-btn {
position: relative;
left: 35%;
margin-top: 50px;
}
.radio-item {
line-height: 40px; margin-left: 20px
}
.item {
margin-bottom: 18px;
}
.clearfix:before,
.clearfix:after {
display: table;
content: "";
}
.clearfix:after {
clear: both
}
.box-card p {display: inline}
.box-card {
width: 100%;
}
.box-card >>> p {display: inline-block}
</style>