Bladeren bron

update to monitor

171250600 5 jaren geleden
bovenliggende
commit
353e71f144

+ 6 - 0
src/router/exam.ts

@@ -18,6 +18,12 @@ const routes: Array<RouteConfig> = [
         name: 'exam-pane',
         component: () =>
           import(/* webpackChunkName: "exam" */ '@/views/exam/ExamPane.vue')
+      },
+      {
+        path: 'photoGet',
+        name: 'photo-get',
+        component: () =>
+          import(/* webpackChunkName: "exam" */ '@/views/exam/PhotoGet.vue')
       }
     ]
   }

+ 1 - 1
src/store/index.ts

@@ -12,7 +12,7 @@ export type RootState = {
 
 export const state = (): RootState => ({
   user: (null as unknown) as UserSerializer,
-  token: ''
+  token: 'temp'
 })
 
 export const mutations: MutationTree<RootState> = {

+ 1 - 1
src/views/exam/BeforeExam.vue

@@ -4,7 +4,7 @@
       <template #extra>
         <v-row justify="end" no-gutters>
           <router-link
-            :to="{ name: 'exam-pane' }"
+            :to="{ name: 'photo-get' }"
             style="text-decoration: none;"
           >
             <v-btn color="success" class="mr-2">进入评测</v-btn>

+ 464 - 12
src/views/exam/ExamPane.vue

@@ -1,5 +1,11 @@
 <template>
-  <div>
+  <div
+    tabIndex="-1"
+    @blur="monitor()"
+    ref="focusOn"
+    @focus="stopTimeCount()"
+    @resize="isFullScreen()"
+  >
     <v-navigation-drawer app permanent right class="elevation-5">
       <div
         class="flex-column d-flex justify-space-between"
@@ -76,16 +82,41 @@
             </div>
           </div>
         </div>
-        <div style="height: 20vh;width: 80%" class="align-self-center">
+        <div class="d-flex justify-center">
+          <video ref="videos" v-show="true" width="150" height="120"></video>
+          <video ref="video" v-show="false" width="640" height="480"></video>
+          <el-upload
+            class="avatar-uploader"
+            action
+            :show-file-list="false"
+            :with-credentials="false"
+            :http-request="selectPicUpload"
+          >
+            <canvas
+              name="file"
+              v-show="false"
+              width="640"
+              height="480"
+              ref="canvas"
+            ></canvas>
+          </el-upload>
           <!--          预留给反作弊-->
         </div>
         <div class="flex-shrink-0">
           <div class="pa-2 px-6 text--secondary justify-space-between d-flex">
             <v-dialog v-model="exitDialog" persistent max-width="290">
               <template v-slot:activator="{ on, attrs }">
-                <v-btn color="error" v-bind="attrs" v-on="on">退出评测</v-btn>
+                <v-btn
+                  color="error"
+                  v-bind="attrs"
+                  v-on="on"
+                  @click="changeFlagTo0"
+                  @mousedown="preventBlur($event)"
+                >
+                  退出评测
+                </v-btn>
               </template>
-              <v-card>
+              <v-card @mousedown="preventBlur($event)">
                 <v-card-title class="headline">
                   确认离开评测?
                 </v-card-title>
@@ -95,11 +126,21 @@
                   <v-btn
                     color="success darken-1"
                     text
-                    @click="exitDialog = false"
+                    @click="
+                      exitDialog = false
+                      changeFlagTo1()
+                      focusAuto()
+                    "
+                    @mousedown="preventBlur($event)"
                   >
                   </v-btn>
-                  <v-btn color="error darken-1" text @click="leaveExam">
+                  <v-btn
+                    color="error darken-1"
+                    text
+                    @click="leaveExam"
+                    @mousedown="preventBlur($event)"
+                  >
                   </v-btn>
                 </v-card-actions>
@@ -107,9 +148,17 @@
             </v-dialog>
             <v-dialog v-model="submitDialog" persistent max-width="290">
               <template v-slot:activator="{ on, attrs }">
-                <v-btn color="success" v-bind="attrs" v-on="on">提交试卷</v-btn>
+                <v-btn
+                  color="success"
+                  v-bind="attrs"
+                  v-on="on"
+                  @click="changeFlagTo0"
+                  @mousedown="preventBlur($event)"
+                >
+                  提交试卷
+                </v-btn>
               </template>
-              <v-card>
+              <v-card @mousedown="preventBlur($event)">
                 <v-card-title class="headline">
                   确认提交试卷?
                 </v-card-title>
@@ -119,11 +168,21 @@
                   <v-btn
                     color="success darken-1"
                     text
-                    @click="submitDialog = false"
+                    @click="
+                      submitDialog = false
+                      changeFlagTo1()
+                      focusAuto()
+                    "
+                    @mousedown="preventBlur($event)"
                   >
                   </v-btn>
-                  <v-btn color="error darken-1" text @click="submitAnswer">
+                  <v-btn
+                    color="error darken-1"
+                    text
+                    @click="submitAnswer"
+                    @mousedown="preventBlur($event)"
+                  >
                   </v-btn>
                 </v-card-actions>
@@ -146,6 +205,7 @@
               :total-question-num="codeQuestionNum"
               @nextQuestion="nextQuestion"
               @prevQuestion="prevQuestion"
+              @mousedown="preventBlur($event)"
             >
               <template #index>
                 <v-chip class="align-self-center"
@@ -194,7 +254,11 @@
             v-if="currentQuestionIndex >= 0"
             class="d-flex justify-center px-10 py-10"
           >
-            <v-btn color="info" @click="nextQuestion" :disabled="fetchingStem"
+            <v-btn
+              color="info"
+              @click="nextQuestion"
+              :disabled="fetchingStem"
+              @mousedown="preventBlur($event)"
               >下一题</v-btn
             >
           </div>
@@ -214,6 +278,78 @@
       提交失败,请重试
     </v-snackbar>
     <v-snackbar v-model="endTimeChanged" absolute>评测已延长</v-snackbar>
+    <template>
+      <div v-if="visible">
+        <v-dialog v-model="visible" persistent max-width="390">
+          <v-card>
+            <v-card-title class="headline"> </v-card-title>
+            <v-card-text
+              >您已切屏或改变页面大小,违规三次或持续十秒后将被判定为作弊!请立刻点击确认按钮返回考试,否则15s后也将按作弊论处。</v-card-text
+            >
+            <v-card-actions>
+              <v-spacer></v-spacer>
+              <v-btn
+                color="success darken-1"
+                text
+                @click="
+                  changeVisibleFalse()
+                  changeFlagTo1()
+                  stopTimeCount()
+                  focusAuto()
+                "
+                @mousedown="preventBlur($event)"
+              >
+                确认
+              </v-btn>
+            </v-card-actions>
+          </v-card>
+        </v-dialog>
+      </div>
+    </template>
+    <template>
+      <div>
+        <v-dialog v-model="visible1" persistent max-width="390">
+          <v-card>
+            <v-card-title class="headline"> </v-card-title>
+            <v-card-text>您已违反考试纪律,您已被认定为作弊</v-card-text>
+            <v-card-actions>
+              <v-spacer></v-spacer>
+              <v-btn
+                color="success darken-1"
+                text
+                @click="leaveExam()"
+                @mousedown="preventBlur($event)"
+              >
+                确认
+              </v-btn>
+            </v-card-actions>
+          </v-card>
+        </v-dialog>
+      </div>
+    </template>
+    <template>
+      <div>
+        <v-dialog v-model="visible2" persistent max-width="390">
+          <v-card>
+            <v-card-title class="headline"> </v-card-title>
+            <v-card-text
+              >您已切换ip地址,违反考试纪律,已被强制退出考试!</v-card-text
+            >
+            <v-card-actions>
+              <v-spacer></v-spacer>
+              <v-btn
+                color="success darken-1"
+                text
+                @click="leaveExam()"
+                @mousedown="preventBlur($event)"
+              >
+                确认
+              </v-btn>
+            </v-card-actions>
+          </v-card>
+        </v-dialog>
+      </div>
+    </template>
   </div>
 </template>
 
@@ -238,6 +374,9 @@ import { getQuestionStem } from '@/api/question'
 import { objectiveJudge } from '@/api/judge'
 import dayjs from 'dayjs'
 import { TIME_FORMAT } from '@/utils/common'
+import InputFocus from '@/views/exam/components/InputFocus.vue'
+import axios from 'axios'
+import qs from 'qs'
 
 export default Vue.extend({
   name: 'ExamPane',
@@ -274,7 +413,23 @@ export default Vue.extend({
       objectiveQuestionNum: 0,
       codeQuestionNum: 0,
       codeQuestionsReady: false,
-      endTimeChanged: false
+      endTimeChanged: false,
+
+      time: 0,
+      allowNum: 3,
+      interval: 0,
+      flag: 1,
+      vCardFlag: 1,
+      visible: false,
+      visible1: false,
+      visible2: false,
+      isCameraFlag: true,
+      photoNums: 0,
+      notMatchNum: 0,
+      hrep: 'http://localhost:8080/detect',
+      gethrep: 'http://localhost:8080/matching',
+      ipSave: 'http://localhost:8080/saveIP',
+      ipDetect: 'http://localhost:8080/detectIP'
     }
   },
   computed: {
@@ -286,6 +441,294 @@ export default Vue.extend({
     }
   },
   methods: {
+    test() {
+      alert(this.allowNum)
+    },
+
+    getUserIp() {
+      let ip = localStorage.getItem('Ip')
+      let id = this.$store.getters.getId
+      ip = id + '~~' + ip
+      console.log('ipget')
+      axios
+        .get(this.ipDetect, {
+          params: { ip },
+          withCredentials: false
+        })
+        .then((res) => {
+          console.log(res)
+          if (res.data == 0) {
+            this.visible2 = true
+          }
+          //this.imageUrl = res.data.url;
+          // this.backgroundUrl = res.data.backgroundUrl;
+        })
+        .catch((err) => {
+          console.log(err)
+        })
+      //console.log(localStorage.getItem('Ip'));
+    },
+
+    saveIP() {
+      let ip = localStorage.getItem('Ip')
+      let id = this.$store.getters.getId
+      ip = id + '~~' + ip
+
+      axios
+        .post(
+          this.ipSave,
+          qs.stringify({
+            key: 'file',
+            value: ip
+          }),
+          {
+            withCredentials: false
+          }
+        )
+        .then((res) => {
+          //console.log(res.data)
+        })
+        .catch((err) => {
+          console.log(err)
+        })
+    },
+
+    getUserIpAuto() {
+      let i = setInterval(this.getUserIp, 1000)
+    },
+
+    preventBlur(event: any) {
+      event.preventDefault()
+    },
+
+    changeFlagTo1() {
+      this.vCardFlag = 1
+    },
+
+    changeFlagTo0() {
+      this.vCardFlag = 0
+    },
+
+    changeExamFlagTo1() {
+      ;(InputFocus as any).ExamFlag = 1
+    },
+
+    changeExamFlagTo0() {
+      ;(InputFocus as any).ExamFlag = 0
+    },
+
+    focusAuto() {
+      if ((InputFocus as any).cheatFlag == 1) {
+        ;(InputFocus as any).cheatFlag = 0
+        this.monitor()
+      }
+      const fo = this.$refs.focusOn
+      let intval = setInterval(function() {
+        if (
+          (InputFocus as any).InputFlag == 1 &&
+          (InputFocus as any).codeFlag == 1
+        ) {
+          ;(fo as any).focus()
+        }
+      }, 100)
+    },
+
+    changeVisibleFalse() {
+      this.visible = false
+      this.focusAuto()
+    },
+
+    timeWatch() {
+      this.time++
+      if (this.time > 10) {
+        this.time = 0
+        clearInterval(this.interval)
+        this.visible1 = true
+      }
+    },
+
+    timeCount() {
+      this.interval = setInterval(this.timeWatch, 1000)
+    },
+
+    stopTimeCount() {
+      clearInterval(this.interval)
+      this.time = 0
+      this.focusAuto()
+      ;(InputFocus as any).ExamFlag = 1
+    },
+
+    monitor() {
+      let _this = this
+      if (
+        this.allowNum < 1 &&
+        this.flag == 1 &&
+        this.vCardFlag == 1 &&
+        (InputFocus as any).InputFlag == 1
+      ) {
+        setTimeout(function() {
+          if ((InputFocus as any).codeFlag == 1) {
+            _this.flag = _this.flag - 1
+            _this.visible1 = true
+          }
+        }, 100)
+      } else if (
+        this.flag == 1 &&
+        this.vCardFlag == 1 &&
+        (InputFocus as any).InputFlag == 1
+      ) {
+        setTimeout(function() {
+          if ((InputFocus as any).codeFlag == 1) {
+            _this.allowNum = _this.allowNum - 1
+            //alert(y);
+            _this.visible = true
+            _this.changeFlagTo0()
+            _this.timeCount()
+          }
+        }, 100)
+      }
+      //document.getElementById("demo").innerHTML = y;
+    },
+
+    isFullScreen() {
+      if (window.outerHeight == screen.availHeight) {
+        if (window.outerWidth == screen.availWidth) {
+          this.stopTimeCount()
+          this.visible = false
+          this.flag = 1
+        }
+      } else {
+        this.flag = 0
+        if (this.allowNum < 1) {
+          this.visible1 = true
+        } else {
+          this.allowNum = this.allowNum - 1
+          this.visible = true
+          this.timeCount()
+        }
+      }
+    },
+
+    getMedia() {
+      // H5调用电脑摄像头API
+      navigator.mediaDevices
+        .getUserMedia({
+          audio: true, //开启音频
+          video: true //开启视频
+        })
+        .then((success) => {
+          var videoTracks = success.getVideoTracks()
+          console.log(success)
+          console.log(videoTracks)
+          // 摄像头开启成功
+          ;(this.$refs['video'] as any).srcObject = success
+          ;(this.$refs['videos'] as any).srcObject = success
+          this.isCameraFlag = false
+          // 实时拍照效果
+          ;(this.$refs['video'] as any).play()
+          ;(this.$refs['videos'] as any).play()
+          this.interval = setInterval(this.photoCount, 1000)
+        })
+        .catch((error) => {
+          console.error('摄像头开启失败,请检查摄像头是否可用!')
+          this.isCameraFlag = true
+        })
+    },
+
+    photoCount() {
+      this.photoNums = this.photoNums + 1
+      if (this.photoNums <= 10) {
+        this.selectPicUpload()
+      } else {
+        this.photoNums = 0
+        this.getMatchingResult()
+      }
+    },
+
+    getMatchingResult() {
+      let b = -1
+      let id = this.$store.getters.getId
+      axios
+        .get(this.gethrep, {
+          withCredentials: false,
+          params: {
+            id
+          }
+        })
+        .then((res) => {
+          console.log('res' + res)
+          b = res.data
+          console.log('b:' + b)
+        })
+        .catch((err) => {
+          console.log('err')
+          console.log(err)
+          console.log(err.data)
+          b = err.data
+          /*不知道为什么数据会出现在error里,目前就直接从中获取了。
+           */
+          console.log('eb:' + b)
+          if (b == 0) {
+            this.notMatchNum = this.notMatchNum + 1
+            console.log('nmn:' + this.notMatchNum)
+            if (this.notMatchNum >= 5) {
+              this.leaveExam()
+            }
+          } else if (b == 1) {
+            console.log('nmn:' + this.notMatchNum)
+            this.notMatchNum = 0
+          }
+        })
+      //console.log("b:"+b);
+      /*if(b==0){
+        this.notMatchNum = this.notMatchNum+1;
+        console.log("nmn:"+this.notMatchNum);
+        if(this.notMatchNum>=5){
+          this.leaveExam();
+        }
+
+      }
+      else if(b==1){
+        console.log("nmn:"+this.notMatchNum);
+        this.notMatchNum = 0;*/
+      //}
+    },
+
+    selectPicUpload() {
+      let ctx = (this.$refs['canvas'] as any).getContext('2d')
+      // 把当前视频帧内容渲染到canvas上
+      ctx.drawImage(this.$refs['video'], 0, 0, 640, 480)
+      let imgBase64 = (this.$refs['canvas'] as any).toDataURL('image/png', 0.7)
+      let id = this.$store.getters.getId
+      imgBase64 = id + '~~' + this.photoNums + '~~' + imgBase64
+      //let fd = new FormData(); //参数的格式是formData格式的
+      //fd.append("image", imgBase64); //参数
+      //for (var value of fd.values()) {
+      //    console.log(value);
+      //}
+      axios
+        .post(
+          this.hrep,
+          qs.stringify({
+            key: 'file',
+            value: imgBase64
+          }),
+          {
+            withCredentials: false
+            //headers: {'Content-Type': 'multipart/form-data;boundary = ' + new Date().getTime()
+            //}
+          }
+        )
+        .then((res) => {
+          //console.log(res.data)
+          //this.imageUrl = res.data.url;
+          // this.backgroundUrl = res.data.backgroundUrl;
+        })
+        .catch((err) => {
+          console.log(err)
+        })
+    },
+
     questionType(): QuestionType {
       return this.currentQuestion.type
     },
@@ -436,6 +879,15 @@ export default Vue.extend({
   },
   mounted() {
     this.fetchData()
+    ;(this.$refs.focusOn as any).focus()
+    this.getUserIp()
+    this.isFullScreen()
+    this.getMedia()
+    this.saveIP()
+    this.getUserIpAuto()
+    window.onresize = () => {
+      this.isFullScreen()
+    }
   }
 })
 </script>

+ 216 - 0
src/views/exam/PhotoGet.vue

@@ -0,0 +1,216 @@
+<template>
+  <v-container>
+    <page-header title="照片收集" :breadcrumb="breadcrumb">
+      <template #extra>
+        <v-row justify="end" no-gutters>
+          <router-link
+            :to="{ name: 'exam-pane' }"
+            style="text-decoration: none;"
+          >
+            <v-btn color="success" class="mr-2">进入评测</v-btn>
+          </router-link>
+          <router-link to="/student/exams" style="text-decoration: none">
+            <v-btn class="ml-2">返回</v-btn>
+          </router-link>
+        </v-row>
+      </template>
+    </page-header>
+    <div class="d-flex justify-center">
+      <video ref="video" width="640" height="480"></video>
+      <el-upload
+        class="avatar-uploader"
+        action
+        :show-file-list="false"
+        :with-credentials="false"
+        :http-request="selectPicUpload"
+      >
+        <canvas
+          name="file"
+          v-show="false"
+          width="640"
+          height="480"
+          ref="canvas"
+        ></canvas>
+      </el-upload>
+    </div>
+    <template>
+      <div>
+        <v-dialog v-model="visible" persistent max-width="590">
+          <v-card>
+            <v-card-title class="headline">考试须知: </v-card-title>
+            <v-card-text>
+              请认真阅读以下内容,否则因此造成的考试无效、被判定为作弊等情况无法挽回。
+            </v-card-text>
+            <v-card-text>
+              1.因浏览器限制,本次考试请考生在chrome上进行,否则可能导致无法顺利完成编程题的答写。
+            </v-card-text>
+            <v-card-text>
+              2.本次考试过程中,系统将会开启您的摄像头,请确认打开您的摄像头,并在考试过程中与数据收集过程中保证面部完整出现在摄像头中,否则可能会导致您面部数据出现问题。
+            </v-card-text>
+            <v-card-text>
+              3.本次考试过程中,考生不得随意切屏或改变浏览器大小(浏览器必须以最大模式开启),一旦切屏或缩小浏览器三次以上(大于三次),或保持切屏状态或浏览器缩小状态15s以上,将立刻被按照作弊处理。
+            </v-card-text>
+            <v-card-text>
+              4.本次考试过程中,考生不得更改自身ip地址,即不得考试中途更换网络,否则按作弊论处,请考生在考试前保证网络畅通。
+            </v-card-text>
+            <v-card-text>
+              5.
+              我方承诺,在考试过程中所拍摄的照片,仅将被用于在线监考,绝不用于其他用途。
+            </v-card-text>
+            <v-card-actions>
+              <v-spacer></v-spacer>
+              <v-btn color="success darken-1" text @click="changeVisible()">
+                同意
+              </v-btn>
+            </v-card-actions>
+          </v-card>
+        </v-dialog>
+      </div>
+    </template>
+
+    <template>
+      <div>
+        <v-dialog v-model="visible1" persistent max-width="390">
+          <v-card>
+            <v-card-title class="headline">摄像头权限获取 </v-card-title>
+            <v-card-text>本页面将获取您的摄像头权限</v-card-text>
+            <v-card-actions>
+              <v-spacer></v-spacer>
+              <v-btn
+                color="success darken-1"
+                text
+                @click="
+                  changeVisible1()
+                  getMedia()
+                "
+              >
+                同意
+              </v-btn>
+            </v-card-actions>
+          </v-card>
+        </v-dialog>
+      </div>
+    </template>
+  </v-container>
+</template>
+
+<script lang="ts">
+import Vue from 'vue'
+import PageHeader from '@/views/components/PageHeader.vue'
+import { ExamQuestionSerializer, ExamSerializer } from '@/api/types'
+import { getExamById, getExamCodeQuestions, getExamQuestions } from '@/api/exam'
+import { TIME_FORMAT, timeLength } from '@/utils/common'
+import dayjs from 'dayjs'
+import axios from 'axios'
+import qs from 'qs'
+
+export default Vue.extend({
+  name: 'PhotoGet',
+  //$axios : any,
+  components: {
+    PageHeader
+  },
+
+  data() {
+    return {
+      isCameraFlag: true,
+      interval: 0,
+      hrep: 'http://localhost:8080/upload',
+      photoNums: 0,
+      ip: '',
+      visible: true,
+      visible1: false
+    }
+  },
+  mounted() {
+    this.getUserIp()
+  },
+  methods: {
+    changeVisible() {
+      if (this.visible == true) {
+        this.visible = false
+        this.changeVisible1()
+      }
+    },
+
+    changeVisible1() {
+      if (this.visible1 == true) {
+        this.visible1 = false
+      } else {
+        this.visible1 = true
+      }
+    },
+
+    getUserIp() {
+      //this.ip = localStorage.getItem('Ip');
+      console.log(localStorage.getItem('Ip'))
+    },
+
+    getMedia() {
+      // H5调用电脑摄像头API
+      navigator.mediaDevices
+        .getUserMedia({
+          audio: true, //开启音频
+          video: true //开启视频
+        })
+        .then((success) => {
+          var videoTracks = success.getVideoTracks()
+          console.log(success)
+          console.log(videoTracks)
+          // 摄像头开启成功
+          ;(this.$refs['video'] as any).srcObject = success
+          this.isCameraFlag = false
+          // 实时拍照效果
+          ;(this.$refs['video'] as any).play()
+          this.interval = setInterval(this.photoCount, 1000)
+        })
+        .catch((error) => {
+          console.error('摄像头开启失败,请检查摄像头是否可用!')
+          this.isCameraFlag = true
+        })
+    },
+
+    photoCount() {
+      this.photoNums = this.photoNums + 1
+      if (this.photoNums <= 200) {
+        this.selectPicUpload()
+      }
+    },
+
+    selectPicUpload() {
+      let ctx = (this.$refs['canvas'] as any).getContext('2d')
+      // 把当前视频帧内容渲染到canvas上
+      ctx.drawImage(this.$refs['video'], 0, 0, 640, 480)
+      let imgBase64 = (this.$refs['canvas'] as any).toDataURL('image/png', 0.7)
+      let id = this.$store.getters.getId
+      //console.log(id)
+      imgBase64 = id + '~~' + this.photoNums + '~~' + imgBase64
+      //let fd = new FormData(); //参数的格式是formData格式的
+      //fd.append("image", imgBase64); //参数
+      //for (var value of fd.values()) {
+      //    console.log(value);
+      //}
+      axios
+        .post(
+          this.hrep,
+          qs.stringify({
+            key: 'file',
+            value: imgBase64
+          }),
+          {
+            withCredentials: false
+            //headers: {'Content-Type': 'multipart/form-data;boundary = ' + new Date().getTime()
+            //}
+          }
+        )
+        .then((res) => {
+          console.log(res.data)
+          //this.imageUrl = res.data.url;
+          // this.backgroundUrl = res.data.backgroundUrl;
+        })
+    }
+  }
+})
+</script>
+
+<style scoped></style>

+ 35 - 1
src/views/exam/components/BlankPane.vue

@@ -14,6 +14,11 @@
       v-model="answer"
       class="pa-10"
       label="请输入答案"
+      @mousedown="
+        changeInputFlagTo0()
+        changeExamFlagTo0()
+      "
+      @blur="InputMonitor()"
     ></v-text-field>
   </div>
 </template>
@@ -22,6 +27,7 @@
 import Vue, { PropType } from 'vue'
 import MarkdownText from '@/views/exam/components/MarkdownText.vue'
 import { QuestionSerializer } from '@/api/types'
+import InputFocus from '@/views/exam/components/InputFocus.vue'
 export default Vue.extend({
   name: 'BlankPane',
   components: {
@@ -44,7 +50,35 @@ export default Vue.extend({
       this.answer = ''
     }
   },
-  methods: {},
+  methods: {
+    test() {
+      alert('yes!')
+    },
+
+    changeInputFlagTo0() {
+      ;(InputFocus as any).InputFlag = 0
+      //alert(InputFocus.InputFlag);
+    },
+    changeInputFlagTo1() {
+      ;(InputFocus as any).InputFlag = 1
+    },
+    changeExamFlagTo0() {
+      ;(InputFocus as any).ExamFlag = 0
+    },
+
+    InputMonitor() {
+      let _this = this
+      setTimeout(function() {
+        if ((InputFocus as any).ExamFlag == 0) {
+          ;(InputFocus as any).InputFlag = 1
+          ;(InputFocus as any).cheatFlag = 1
+          _this.$emit('focusAuto')
+        } else {
+          ;(InputFocus as any).InputFlag = 1
+        }
+      }, 500)
+    }
+  },
   beforeUpdate() {
     this.$emit('saveAnswer', this.answer)
   }

+ 29 - 3
src/views/exam/components/ChoicePane.vue

@@ -32,10 +32,14 @@
                   :input-value="active"
                   on-icon="mdi-radiobox-marked"
                   off-icon="mdi-radiobox-blank"
+                  @mousedown="changeInputFlagTo0()"
                 >
                 </v-checkbox>
               </v-list-item-action>
-              <v-list-item-content class="py-0">
+              <v-list-item-content
+                class="py-0"
+                @mousedown="changeInputFlagTo0()"
+              >
                 <markdown-text :raw="choice.labelText"></markdown-text>
               </v-list-item-content>
             </template>
@@ -49,9 +53,17 @@
           <v-list-item :key="choice.label" :value="choice.label">
             <template v-slot:default="{ active }">
               <v-list-item-action>
-                <v-checkbox hide-details :input-value="active"> </v-checkbox>
+                <v-checkbox
+                  hide-details
+                  :input-value="active"
+                  @mousedown="changeInputFlagTo0()"
+                >
+                </v-checkbox>
               </v-list-item-action>
-              <v-list-item-content class="py-0">
+              <v-list-item-content
+                class="py-0"
+                @mousedown="changeInputFlagTo0()"
+              >
                 <markdown-text :raw="choice.labelText"></markdown-text>
               </v-list-item-content>
             </template>
@@ -69,6 +81,7 @@ import MarkdownText from '@/views/exam/components/MarkdownText.vue'
 import { QuestionSerializer, OptionLabelTextPair } from '@/api/types'
 
 import { clone } from 'ramda'
+import InputFocus from '@/views/exam/components/InputFocus.vue'
 export default Vue.extend({
   name: 'ChoicePane',
   components: {
@@ -111,6 +124,19 @@ export default Vue.extend({
     }
   },
   methods: {
+    changeInputFlagTo0() {
+      //alert((InputFocus as any).InputFlag);
+      ;(InputFocus as any).InputFlag = 0
+      setTimeout(this.changeInputFlagTo1, 50)
+    },
+    changeInputFlagTo1() {
+      ;(InputFocus as any).InputFlag = 1
+    },
+
+    preventBlur(event: any) {
+      event.preventDefault()
+    },
+
     init() {
       if (this.question.type === 'true_false') {
         this.question.options?.set('true', '')

+ 48 - 1
src/views/exam/components/CodeEditor.vue

@@ -1,5 +1,11 @@
 <template>
-  <div style="height: 100%; width: 100%;"></div>
+  <div
+    style="height: 100%; width: 100%;"
+    @mousedown="
+      changeCodeFlagTo0()
+      changeExamFlagTo0()
+    "
+  ></div>
 </template>
 
 <script lang="ts">
@@ -8,6 +14,7 @@ import * as Monaco from 'monaco-editor'
 import { editor } from 'monaco-editor'
 import ICodeEditor = editor.ICodeEditor
 import ITextModel = editor.ITextModel
+import InputFocus from '@/views/exam/components/InputFocus.vue'
 
 export default Vue.extend({
   name: 'CodeEditor',
@@ -69,6 +76,37 @@ export default Vue.extend({
   },
 
   methods: {
+    test() {
+      alert('??')
+    },
+
+    changeCodeFlagTo0() {
+      ;(InputFocus as any).codeFlag = 0
+      console.log('change to 0')
+      //alert(InputFocus.codeFlag);
+    },
+    changeCodeFlagTo1() {
+      ;(InputFocus as any).codeFlag = 1
+      console.log('change to 1')
+    },
+    changeExamFlagTo0() {
+      ;(InputFocus as any).ExamFlag = 0
+    },
+
+    CodeMonitor() {
+      //this.test();
+      let _this = this
+      setTimeout(function() {
+        if ((InputFocus as any).ExamFlag == 0) {
+          _this.changeCodeFlagTo1()
+          ;(InputFocus as any).cheatFlag = 1
+          _this.$emit('focusAuto')
+        } else {
+          ;(InputFocus as any).codeFlag = 1
+        }
+      }, 500)
+    },
+
     initMonaco() {
       const options = {
         value: this.value,
@@ -85,6 +123,15 @@ export default Vue.extend({
           this.$emit('change', value)
         }
       })
+
+      this.editor.onDidBlurEditorText(() => {
+        this.CodeMonitor()
+      })
+
+      this.editor.onDidFocusEditorText(() => {
+        this.changeCodeFlagTo0()
+        this.changeExamFlagTo0()
+      })
     },
     setValue(val: string) {
       this.editor.setValue(val)

+ 138 - 9
src/views/exam/components/CodePane.vue

@@ -4,9 +4,27 @@
       <div class="flex-grow-1 d-flex flex-column justify-space-between">
         <div>
           <v-tabs v-model="tab" class="pa-0 ma-0 elevation-1" grow>
-            <v-tab>题目描述</v-tab>
-            <v-tab>运行结果</v-tab>
-            <v-tab>提交记录</v-tab>
+            <v-tab
+              @mousedown="
+                changeExamFlagTo1()
+                preventBlur($event)
+              "
+              >题目描述</v-tab
+            >
+            <v-tab
+              @mousedown="
+                changeExamFlagTo1()
+                preventBlur($event)
+              "
+              >运行结果</v-tab
+            >
+            <v-tab
+              @mousedown="
+                changeExamFlagTo1()
+                preventBlur($event)
+              "
+              >提交记录</v-tab
+            >
           </v-tabs>
           <v-tabs-items v-model="tab">
             <v-tab-item>
@@ -84,6 +102,10 @@
                           small
                           outlined
                           @click="onShowCode(item.submitAnswer)"
+                          @mousedown="
+                            changeExamFlagTo1()
+                            preventBlur($event)
+                          "
                           >详情</v-btn
                         >
                         <v-tooltip top>
@@ -96,6 +118,10 @@
                               x-small
                               outlined
                               @click="onOpenRollbackDialog(item.submitAnswer)"
+                              @mousedown="
+                                changeInputFlagTo0()
+                                changeExamFlagTo0()
+                              "
                               v-bind="attrs"
                               v-on="on"
                             >
@@ -117,8 +143,26 @@
             <span class="mx-2"> 编程题</span>
             <slot name="index"></slot>
           </span>
-          <v-btn color="info" small @click="prevQuestion">上一题</v-btn>
-          <v-btn color="info" small @click="nextQuestion">下一题</v-btn>
+          <v-btn
+            color="info"
+            small
+            @click="prevQuestion"
+            @mousedown="
+              changeExamFlagTo1()
+              preventBlur($event)
+            "
+            >上一题</v-btn
+          >
+          <v-btn
+            color="info"
+            small
+            @click="nextQuestion"
+            @mousedown="
+              changeExamFlagTo1()
+              preventBlur($event)
+            "
+            >下一题</v-btn
+          >
         </div>
       </div>
       <v-divider vertical class="mx-1"></v-divider>
@@ -136,6 +180,15 @@
                 style="width: 120px"
                 class="ma-0 pa-0"
                 solo
+                @mousedown="
+                  changeInputFlagTo0()
+                  changeExamFlagTo0()
+                "
+                @change="
+                  changeExamFlagTo1()
+                  InputMonitor()
+                "
+                @blur="InputMonitor()"
               >
               </v-select>
             </div>
@@ -147,6 +200,10 @@
             :disabled="running"
             :loading="running"
             outlined
+            @mousedown="
+              changeExamFlagTo1()
+              preventBlur($event)
+            "
             >保存测试</v-btn
           >
         </div>
@@ -156,7 +213,15 @@
         ></code-editor>
       </div>
     </div>
-    <v-snackbar v-model="error" top color="warning" absolute
+    <v-snackbar
+      v-model="error"
+      top
+      color="warning"
+      absolute
+      @mousedown="
+        changeExamFlagTo1()
+        preventBlur($event)
+      "
       >提交失败</v-snackbar
     >
     <v-dialog v-if="showCode" v-model="showCode" max-width="800">
@@ -165,17 +230,35 @@
         <markdown-text :raw="codeDetailContent" class="mx-5"></markdown-text>
       </v-card>
     </v-dialog>
-    <v-dialog max-width="300" v-model="rollbackCodeDialog">
+    <v-dialog
+      max-width="300"
+      v-model="rollbackCodeDialog"
+      @blur="InputMonitor()"
+    >
       <v-card>
         <v-card-title>确认恢复代码</v-card-title>
         <v-card-actions class="justify-end">
-          <v-btn outlined color="success darken-1" @click="onRollback">
+          <v-btn
+            outlined
+            color="success darken-1"
+            @click="onRollback"
+            @mousedown="
+              preventBlur($event)
+              changeExamFlagTo1()
+              InputMonitor()
+            "
+          >
             确定
           </v-btn>
           <v-btn
             outlined
             color="error darken-1"
             @click="rollbackCodeDialog = false"
+            @mousedown="
+              preventBlur($event)
+              changeExamFlagTo1()
+              InputMonitor()
+            "
           >
             取消
           </v-btn>
@@ -201,6 +284,7 @@ import {
 import { codeJudge } from '@/api/judge'
 import { getExamQuestionSubmitRecord } from '@/api/record'
 import dayjs from 'dayjs'
+import InputFocus from '@/views/exam/components/InputFocus.vue'
 
 interface CodeJudgeInfo extends CodeJudgeResult {
   id: ID
@@ -249,10 +333,19 @@ export default Vue.extend({
       showCode: false,
       codeDetailContent: '',
       rollbackCodeDialog: false,
-      rollbackAnswer: ''
+      rollbackAnswer: '',
+
+      editFlag: 0
     }
   },
   watch: {
+    dialogVisible() {
+      if (this.rollbackCodeDialog) {
+        this.changeInputFlagTo0()
+        this.changeExamFlagTo0()
+      }
+    },
+
     currentLang(val, oldVal) {
       if (oldVal !== '' && val !== oldVal) {
         const newCode =
@@ -282,6 +375,42 @@ export default Vue.extend({
     this.updateSubmitRecords()
   },
   methods: {
+    test() {
+      alert('???')
+    },
+
+    preventBlur(event: any) {
+      event.preventDefault()
+    },
+
+    changeInputFlagTo0() {
+      ;(InputFocus as any).InputFlag = 0
+      //alert((InputFocus as any).InputFlag);
+    },
+    changeInputFlagTo1() {
+      ;(InputFocus as any).InputFlag = 1
+    },
+    changeExamFlagTo0() {
+      ;(InputFocus as any).ExamFlag = 0
+    },
+    changeExamFlagTo1() {
+      ;(InputFocus as any).ExamFlag = 1
+    },
+
+    InputMonitor() {
+      //this.test();
+      let _this = this
+      setTimeout(function() {
+        if ((InputFocus as any).ExamFlag == 0) {
+          ;(InputFocus as any).InputFlag = 1
+          ;(InputFocus as any).cheatFlag = 1
+          _this.$emit('focusAuto')
+        } else {
+          ;(InputFocus as any).InputFlag = 1
+        }
+      }, 500)
+    },
+
     onShowCode(content: string) {
       content = '```\n' + content.replace('\r', '') + '\n```'
       this.codeDetailContent = content

+ 13 - 0
src/views/exam/components/InputFocus.vue

@@ -0,0 +1,13 @@
+<template> </template>
+
+<script>
+export default {
+  name: 'InputFocus',
+  InputFlag: 1,
+  codeFlag: 1,
+  ExamFlag: 0,
+  cheatFlag: 0
+}
+</script>
+
+<style scoped></style>

+ 1 - 1
vue.config.js

@@ -6,7 +6,7 @@ module.exports = {
   devServer: {
     proxy: {
       '^/api': {
-        // target: 'http://localhost:5000/',
+        //target: 'http://localhost:5000/',
         target: 'http://47.111.95.164:8080/',
         ws: true,
         changeOrigin: true