ChengYuXuan 5 年 前
コミット
47aac6368a

+ 23 - 0
src/api/monitor.ts

@@ -0,0 +1,23 @@
+import { MONITOR_MODULE } from '@/api/prefix'
+import axios from 'axios'
+import { ID, ReceivedData } from '@/api/types'
+
+export const upload = (payload: string) => {
+  return axios.post<ReceivedData>(`${MONITOR_MODULE}/upload`, payload)
+}
+
+export const detect = (payload: string) => {
+  return axios.post<ReceivedData>(`${MONITOR_MODULE}/detect`, payload)
+}
+
+export const matching = (id: ID) => {
+  return axios.get<ReceivedData>(`${MONITOR_MODULE}/matching?id=${id}`)
+}
+
+export const saveIP = (payload: string) => {
+  return axios.post<ReceivedData>(`${MONITOR_MODULE}/saveIP`, payload)
+}
+
+export const detectIP = (ip: string) => {
+  return axios.get<ReceivedData>(`${MONITOR_MODULE}/detectIP?ip=${ip}`)
+}

+ 2 - 0
src/api/prefix.ts

@@ -6,3 +6,5 @@ export const USER_MODULE = BASE_URL + '/user'
 export const JUDGE_MODULE = BASE_URL + '/judge'
 export const REPORT_MODULE = BASE_URL + '/report'
 export const RECORD_MODULE = BASE_URL + '/record'
+
+export const MONITOR_MODULE = BASE_URL + '/monitor'

+ 1 - 1
src/views/components/PageHeader.vue

@@ -5,7 +5,7 @@
         <h1 class="text-h3">{{ title }}</h1>
         <v-breadcrumbs :items="breadcrumb" class="pl-0"></v-breadcrumbs>
       </v-col>
-      <v-col class="flex-shrink-0">
+      <v-col class="flex-shrink-0 pb-5">
         <slot name="extra"></slot>
       </v-col>
     </v-row>

+ 18 - 9
src/views/exam/BeforeExam.vue

@@ -4,20 +4,22 @@
       <template #extra>
         <v-row justify="end" no-gutters>
           <router-link
-            :to="{ name: 'photo-get' }"
+            :to="{ name: 'exam-pane' }"
             style="text-decoration: none;"
           >
-            <v-btn color="success" class="mr-2">进入评测</v-btn>
+            <v-btn
+              color="success"
+              class="mr-2"
+              v-if="(examInfo && !examInfo.antiCheating) || cameraReady"
+              >进入评测</v-btn
+            >
           </router-link>
-          <!--          <router-link to="/student/exams" style="text-decoration: none">-->
-          <!--            <v-btn class="ml-2" @click="onReturn">返回</v-btn>-->
-          <!--          </router-link>-->
           <v-btn class="ml-2" @click="onReturn">返回</v-btn>
         </v-row>
       </template>
     </page-header>
     <v-container>
-      <v-card class="mb-5" hover style="cursor: default" v-if="examInfo">
+      <v-card class="mb-5 pb-5" hover style="cursor: default" v-if="examInfo">
         <v-card-title class="text-h4">{{ examInfo.examName }}</v-card-title>
         <v-card-subtitle class="text-h5 black--text">评测信息</v-card-subtitle>
         <v-card-text class="text-body-1 black--text"
@@ -35,6 +37,10 @@
           客观题 {{ examQuestionsNum - codeQuestionsNum }} 道 编程题
           {{ codeQuestionsNum }} 道
         </v-card-text>
+        <photo-get
+          v-if="examInfo && examInfo.antiCheating"
+          @cameraReady="cameraReady = true"
+        ></photo-get>
       </v-card>
     </v-container>
   </v-container>
@@ -43,7 +49,8 @@
 <script lang="ts">
 import Vue from 'vue'
 import PageHeader from '@/views/components/PageHeader.vue'
-import { ExamQuestionSerializer, ExamSerializer } from '@/api/types'
+import PhotoGet from '@/views/exam/PhotoGet.vue'
+import { ExamSerializer } from '@/api/types'
 import { getExamById, getExamCodeQuestions, getExamQuestions } from '@/api/exam'
 import { TIME_FORMAT, timeLength } from '@/utils/common'
 import dayjs from 'dayjs'
@@ -51,7 +58,8 @@ import dayjs from 'dayjs'
 export default Vue.extend({
   name: 'BeforeExam',
   components: {
-    PageHeader
+    PageHeader,
+    PhotoGet
   },
 
   data() {
@@ -77,7 +85,8 @@ export default Vue.extend({
       examLength: '',
       examQuestionsNum: 0,
       codeQuestionsNum: 0,
-      canEnter: false
+      canEnter: false,
+      cameraReady: false
     }
   },
   mounted() {

+ 43 - 36
src/views/exam/ExamPane.vue

@@ -85,21 +85,13 @@
         <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>
+          <canvas
+            name="file"
+            v-show="false"
+            width="640"
+            height="480"
+            ref="canvas"
+          ></canvas>
           <!--          预留给反作弊-->
         </div>
         <div class="flex-shrink-0">
@@ -374,7 +366,7 @@ 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 { InputFocus } from '@/views/exam/components/InputFocus.ts'
 import axios from 'axios'
 import qs from 'qs'
 
@@ -415,9 +407,13 @@ export default Vue.extend({
       codeQuestionsReady: false,
       endTimeChanged: false,
 
+      mediaTracks: [] as MediaStreamTrack[],
       time: 0,
       allowNum: 3,
-      interval: 0,
+      videoInterval: 0,
+      focusAutoInterval: 0,
+      timeCountInterval: 0,
+      ipInterval: 0,
       flag: 1,
       vCardFlag: 1,
       visible: false,
@@ -441,10 +437,6 @@ export default Vue.extend({
     }
   },
   methods: {
-    test() {
-      alert(this.allowNum)
-    },
-
     getUserIp() {
       let ip = localStorage.getItem('Ip')
       let id = this.$store.getters.getId
@@ -494,7 +486,7 @@ export default Vue.extend({
     },
 
     getUserIpAuto() {
-      let i = setInterval(this.getUserIp, 1000)
+      this.ipInterval = setInterval(this.getUserIp, 1000)
     },
 
     preventBlur(event: any) {
@@ -523,7 +515,7 @@ export default Vue.extend({
         this.monitor()
       }
       const fo = this.$refs.focusOn
-      let intval = setInterval(function() {
+      this.focusAutoInterval = setInterval(function() {
         if (
           (InputFocus as any).InputFlag == 1 &&
           (InputFocus as any).codeFlag == 1
@@ -542,17 +534,17 @@ export default Vue.extend({
       this.time++
       if (this.time > 10) {
         this.time = 0
-        clearInterval(this.interval)
+        clearInterval(this.timeCountInterval)
         this.visible1 = true
       }
     },
 
     timeCount() {
-      this.interval = setInterval(this.timeWatch, 1000)
+      this.timeCountInterval = setInterval(this.timeWatch, 1000)
     },
 
     stopTimeCount() {
-      clearInterval(this.interval)
+      clearInterval(this.timeCountInterval)
       this.time = 0
       this.focusAuto()
       ;(InputFocus as any).ExamFlag = 1
@@ -618,6 +610,7 @@ export default Vue.extend({
         })
         .then((success) => {
           var videoTracks = success.getVideoTracks()
+          this.mediaTracks = success.getTracks()
           console.log(success)
           console.log(videoTracks)
           // 摄像头开启成功
@@ -627,7 +620,7 @@ export default Vue.extend({
           // 实时拍照效果
           ;(this.$refs['video'] as any).play()
           ;(this.$refs['videos'] as any).play()
-          this.interval = setInterval(this.photoCount, 1000)
+          this.videoInterval = setInterval(this.photoCount, 1000)
         })
         .catch((error) => {
           console.error('摄像头开启失败,请检查摄像头是否可用!')
@@ -833,6 +826,7 @@ export default Vue.extend({
 
       getExamById(this.examId).then(({ data }) => {
         this.examInfo = data.result
+        this.initMonitor() // 根据考试信息进行是否监考初始化
         this.endTime = dayjs(this.examInfo.endTime, TIME_FORMAT).valueOf()
         this.currentTime = dayjs().valueOf()
         const startTime = dayjs(this.examInfo.startTime, TIME_FORMAT).valueOf()
@@ -875,19 +869,32 @@ export default Vue.extend({
           this.objectiveAnswers = new Map<ID, Record>()
         })
       })
+    },
+    initMonitor() {
+      if (this.examInfo.antiCheating) {
+        ;(this.$refs.focusOn as any).focus()
+        this.getUserIp()
+        this.isFullScreen()
+        this.getMedia()
+        this.saveIP()
+        this.getUserIpAuto()
+        window.onresize = () => {
+          this.isFullScreen()
+        }
+      }
     }
   },
   mounted() {
     this.fetchData()
-    ;(this.$refs.focusOn as any).focus()
-    this.getUserIp()
-    this.isFullScreen()
-    this.getMedia()
-    this.saveIP()
-    this.getUserIpAuto()
-    window.onresize = () => {
-      this.isFullScreen()
-    }
+  },
+  beforeDestroy() {
+    clearInterval(this.videoInterval)
+    clearInterval(this.timeCountInterval)
+    clearInterval(this.focusAutoInterval)
+    clearInterval(this.ipInterval)
+    this.mediaTracks.forEach((val) => {
+      val.stop()
+    })
   }
 })
 </script>

+ 29 - 46
src/views/exam/PhotoGet.vue

@@ -1,37 +1,15 @@
 <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>
+    <v-card-subtitle class="text-h5 black--text">照片采集</v-card-subtitle>
     <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>
+      <canvas
+        name="file"
+        v-show="false"
+        width="640"
+        height="480"
+        ref="canvas"
+      ></canvas>
     </div>
     <template>
       <div>
@@ -91,40 +69,38 @@
         </v-dialog>
       </div>
     </template>
-  </v-container>
+  </div>
 </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,
+      cameraReady: false,
       interval: 0,
       hrep: 'http://localhost:8080/upload',
       photoNums: 0,
       ip: '',
       visible: true,
-      visible1: false
+      visible1: false,
+      mediaTracks: [] as MediaStreamTrack[]
     }
   },
   mounted() {
     this.getUserIp()
   },
+  watch: {
+    cameraReady: function(val, oldVal) {
+      if (val && val === !oldVal) {
+        this.$emit('cameraReady')
+      }
+    }
+  },
   methods: {
     changeVisible() {
       if (this.visible == true) {
@@ -155,18 +131,19 @@ export default Vue.extend({
         })
         .then((success) => {
           var videoTracks = success.getVideoTracks()
+          this.mediaTracks = success.getTracks()
           console.log(success)
           console.log(videoTracks)
           // 摄像头开启成功
           ;(this.$refs['video'] as any).srcObject = success
-          this.isCameraFlag = false
+          this.cameraReady = true
           // 实时拍照效果
           ;(this.$refs['video'] as any).play()
           this.interval = setInterval(this.photoCount, 1000)
         })
         .catch((error) => {
           console.error('摄像头开启失败,请检查摄像头是否可用!')
-          this.isCameraFlag = true
+          this.cameraReady = false
         })
     },
 
@@ -209,6 +186,12 @@ export default Vue.extend({
           // this.backgroundUrl = res.data.backgroundUrl;
         })
     }
+  },
+  beforeDestroy() {
+    clearInterval(this.interval)
+    this.mediaTracks.forEach((val) => {
+      val.stop()
+    })
   }
 })
 </script>

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

@@ -27,7 +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'
+import { InputFocus } from '@/views/exam/components/InputFocus.ts'
 export default Vue.extend({
   name: 'BlankPane',
   components: {

+ 1 - 1
src/views/exam/components/ChoicePane.vue

@@ -81,7 +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'
+import { InputFocus } from '@/views/exam/components/InputFocus.ts'
 export default Vue.extend({
   name: 'ChoicePane',
   components: {

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

@@ -14,7 +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'
+import { InputFocus } from '@/views/exam/components/InputFocus.ts'
 
 export default Vue.extend({
   name: 'CodeEditor',

+ 1 - 1
src/views/exam/components/CodePane.vue

@@ -284,7 +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'
+import { InputFocus } from '@/views/exam/components/InputFocus.ts'
 
 interface CodeJudgeInfo extends CodeJudgeResult {
   id: ID

+ 6 - 0
src/views/exam/components/InputFocus.ts

@@ -0,0 +1,6 @@
+export const InputFocus = {
+  InputFlag: 1,
+  codeFlag: 1,
+  ExamFlag: 0,
+  cheatFlag: 0
+}

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

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

+ 9 - 2
vue.config.js

@@ -6,10 +6,17 @@ module.exports = {
   devServer: {
     proxy: {
       '^/api': {
-        //target: 'http://localhost:5000/',
-        target: 'http://47.111.95.164:8080/',
+        target: 'http://localhost:5000/',
+        // target: 'http://47.111.95.164:8080/',
         ws: true,
         changeOrigin: true
+      },
+      '^/api/monitor': {
+        target: 'http://localhost:3000',
+        // deploy target TODO
+        ws: true,
+        changeOrigin: true,
+        pathRewrite: '/'
       }
     },
     port: 8000