背景
在线上考试、测验等应用场景中,为了防止考生通过切屏、复制粘贴等手段作弊,我们可以在页面中嵌入一些防作弊机制。本篇博客将结合 Vue 3 + Element Plus,讲解如何封装一个简单的防作弊组件,包括检测切屏、禁用复制粘贴、右键菜单、以及新标签页的打开等功能。
组件功能需求
- 切屏检测:检测用户切屏次数,超过指定次数则自动提交答卷。
- 禁用复制粘贴:防止用户通过复制、剪切、粘贴作弊。
- 禁用右键:禁用右键菜单,防止用户使用开发者工具。
- 页面关闭提醒:在用户试图关闭或刷新页面时,发出警告。
实现过程
(1) 切屏检测
通过监听 visibilitychange
事件,当用户切换到其他页面时,document.hidden
的值会变为 true
,我们可以利用这一特性记录切屏次数。
(2) 禁用复制、粘贴、剪切
监听 copy
、paste
和 cut
事件,调用 preventDefault
阻止默认行为。
(3) 禁用右键菜单
监听 contextmenu
事件,并阻止默认行为。
(4) 页面关闭警告
使用 beforeunload
事件在页面刷新或关闭时提示用户确认操作。
代码
<template>
<div>
<slot></slot>
</div>
</template>
<script setup lang="ts">
import { ElMessageBox } from 'element-plus';
const props = defineProps<{
// 是否开启
isOpen: number;
}>();
const switchCount = ref(0); // 切屏次数
const maxSwitches = 4; // 最大切屏次数
const countdownTime = ref(10); // 倒计时时间(秒)
let timer: number; // 定时器ID
const emit = defineEmits(['submit-exam']); // 超出切屏次数提交方法
/**
* @description 显示警告弹窗
* @param {string} message 弹窗消息内容
* @param {Function} [callback] 可选的回调函数
* @return {Promise<void>}
*/
const showTips = (message: string, callback?: () => void): void => {
ElMessageBox.close();
ElMessageBox.confirm(message, 'Warning', {
confirmButtonText: 'OK',
confirmButtonClass: 'confirm-btn', // 自定义按钮类
// type: 'info',
icon: '',
showClose: false,
closeOnHashChange: true,
showCancelButton: false,
closeOnClickModal: false,
closeOnPressEscape: false,
distinguishCancelAndClose: true,
dangerouslyUseHTMLString: true,
}).then(() => {
if (callback) callback();
});
};
// 10s倒计时函数
const startCountdown = () => {
timer = window.setInterval(() => {
countdownTime.value--;
const countdownElem = document.getElementById('countdown');
if (countdownElem) {
countdownElem.textContent = countdownTime.value.toString();
}
if (countdownTime.value <= 0) {
ElMessageBox.close();
clearTimer();
emit('submit-exam');
}
}, 1000);
};
/**
* @description 禁用复制、剪切和粘贴功能
* @param {ClipboardEvent} event 剪贴板事件
* @return {void}
*/
const preventCopyPaste = (event: ClipboardEvent): void => {
event.preventDefault();
showTips('Copying, cutting, and pasting are disabled.');
};
/**
* @description 禁止打开新的标签页
* @param {KeyboardEvent} event 键盘事件
* @return {void}
*/
const preventNewTab = (event: KeyboardEvent): void => {
if ((event.ctrlKey || event.metaKey) && event.key === 't') {
event.preventDefault();
showTips('Opening new tabs is disabled.');
}
};
/**
* @description 禁用鼠标右键菜单
* @param {MouseEvent} event 鼠标事件
* @return {void}
*/
const disableRightClick = (event: MouseEvent): void => {
event.preventDefault();
};
/**
* @description 在用户试图关闭或重新加载页面之前发出警告
* @param {BeforeUnloadEvent} event 页面卸载事件
* @return {void}
*/
const warnBeforeUnload = (event: BeforeUnloadEvent): void => {
event.preventDefault();
event.returnValue = '';
};
/**
* @description 检测屏幕切换
* @return {void}
*/
const handleVisibilityChange = (): void => {
if (document.hidden) {
switchCount.value++;
if (switchCount.value >= maxSwitches) {
showTips(
`You have exceeded the allowed number of screen switches. The exam will be automatically submitted in <span id="countdown" class="num_color">${countdownTime.value}</span> seconds.`,
() => {
clearTimer(); // 清除计时器
emit('submit-exam');
}
);
startCountdown();
removeEventListeners();
} else {
showTips(
`You have switched screens <span class="num_color">${
switchCount.value
}</span> times. Maximum allowed is <span class="num_color">${maxSwitches - 1}</span>.`
);
}
}
};
/**
* @description 添加事件监听器
* @return {void}
*/
const addEventListeners = (): void => {
document.addEventListener('copy', preventCopyPaste);
document.addEventListener('cut', preventCopyPaste);
document.addEventListener('paste', preventCopyPaste);
document.addEventListener('keydown', preventNewTab);
document.addEventListener('contextmenu', disableRightClick);
window.addEventListener('beforeunload', warnBeforeUnload);
document.addEventListener('visibilitychange', handleVisibilityChange);
};
/**
* @description 移除事件监听器
* @return {void}
*/
const removeEventListeners = (): void => {
document.removeEventListener('copy', preventCopyPaste);
document.removeEventListener('cut', preventCopyPaste);
document.removeEventListener('paste', preventCopyPaste);
document.removeEventListener('keydown', preventNewTab);
document.removeEventListener('contextmenu', disableRightClick);
window.removeEventListener('beforeunload', warnBeforeUnload);
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
/**
* @description: 清除定时器
* @return {*}
*/
const clearTimer = () => {
if (timer) {
clearInterval(timer);
}
};
onMounted(() => {
if (!props.isOpen) {
clearTimer();
addEventListeners();
showTips(
`During the practice process, it is not allowed to switch away from the answering page. If you leave more than <span class="num_color">3</span> times, the answer sheet will be automatically submitted. Please answer carefully!`
);
}
});
onBeforeUnmount(() => {
clearTimer();
removeEventListeners();
});
</script>
<style lang="scss">
.el-button:focus {
outline: none;
}
.el-message-box {
max-width: 520px !important;
}
.el-message-box__btns {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
padding: 18px 0 6px 0;
border-top: 1px solid #ebeef5;
}
/* 自定义确认按钮样式 */
.confirm-btn {
background-color: transparent;
color: var(--el-color-primary);
border-color: transparent;
font-size: 18px !important;
}
.confirm-btn:hover {
background-color: transparent;
color: rgba(var(--el-color-primary-rgb), 0.78);
// opacity: 0.98;
border-color: transparent;
}
.el-message-box__title {
font-size: 18px !important;
font-weight: bold;
}
.el-message-box__message p {
padding: 24px 0 36px 0;
font-size: 18px !important;
line-height: 28px;
}
.num_color {
color: var(--el-color-primary);
}
</style>
调用组件
<template>
<AntiCheat @submit-exam="submitExam" :isOpen="isOpen">
<!-- 需要监听作弊的页面内容 -->
</AntiCheat>
</template>
<script lang="ts" setup>
import AntiCheat from '@/components/AntiCheat/index.vue';
const isOpen = ref(false); // 是否开启监听
/**
* @description: 切屏次数超过3次后的回调方法
* @return {*}
*/
const submitExam = () => {
submit();
};
</script>
总结
通过 Vue 3 的组合式 API 与 Element Plus 的弹窗组件,我们成功封装了一个具备切屏检测、禁用复制粘贴等防作弊功能的通用组件。这种组件在线上考试系统中非常实用。