如何封装切屏检测与防作弊组件 —— Vue 3 + Element Plus 实践

背景

在线上考试、测验等应用场景中,为了防止考生通过切屏、复制粘贴等手段作弊,我们可以在页面中嵌入一些防作弊机制。本篇博客将结合 Vue 3 + Element Plus,讲解如何封装一个简单的防作弊组件,包括检测切屏、禁用复制粘贴、右键菜单、以及新标签页的打开等功能。

组件功能需求

  1. 切屏检测:检测用户切屏次数,超过指定次数则自动提交答卷。
  2. 禁用复制粘贴:防止用户通过复制、剪切、粘贴作弊。
  3. 禁用右键:禁用右键菜单,防止用户使用开发者工具。
  4. 页面关闭提醒:在用户试图关闭或刷新页面时,发出警告。

 实现过程

(1) 切屏检测
通过监听 visibilitychange 事件,当用户切换到其他页面时,document.hidden 的值会变为 true,我们可以利用这一特性记录切屏次数。

(2) 禁用复制、粘贴、剪切
监听 copypastecut 事件,调用 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 的弹窗组件,我们成功封装了一个具备切屏检测、禁用复制粘贴等防作弊功能的通用组件。这种组件在线上考试系统中非常实用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值