zhangjie 3 жил өмнө
parent
commit
2860fc103a
53 өөрчлөгдсөн 1425 нэмэгдсэн , 342 устгасан
  1. 4 2
      package.json
  2. 1 2
      src/api/examwork-course.js
  3. 3 3
      src/api/examwork-student.js
  4. 2 2
      src/api/login.js
  5. 3 3
      src/api/system-user.js
  6. BIN
      src/assets/icon-error.png
  7. 7 0
      src/components/ActivitySelect.vue
  8. 7 0
      src/components/CourseSelect.vue
  9. 1 0
      src/components/ExamRoomSelect.vue
  10. 12 1
      src/components/MinuteInput.vue
  11. 7 4
      src/components/VEditor/VEditor.vue
  12. 15 10
      src/constant/constants.js
  13. 4 1
      src/features/Login/Login.vue
  14. 3 0
      src/features/examwork/ActivityManagement/ActivityManagement.vue
  15. 36 2
      src/features/examwork/ActivityManagement/ActivityManagementDialog.vue
  16. 6 3
      src/features/examwork/CourseManagement/CourseManagement.vue
  17. 1 0
      src/features/examwork/CourseManagement/CoursePaperDialog.vue
  18. 1 0
      src/features/examwork/CourseManagement/PaperImportDialog.vue
  19. 515 209
      src/features/examwork/ExamManagement/ExamEdit.vue
  20. 3 0
      src/features/examwork/ExamManagement/ExamManagement.vue
  21. BIN
      src/features/examwork/ExamManagement/imgs/手机监考主机位-选中.png
  22. BIN
      src/features/examwork/ExamManagement/imgs/手机监考主机位.png
  23. BIN
      src/features/examwork/ExamManagement/imgs/手机监考辅机位-选中.png
  24. BIN
      src/features/examwork/ExamManagement/imgs/手机监考辅机位.png
  25. 3 0
      src/features/examwork/ExamStudentImport/ExamStudentImport.vue
  26. 2 2
      src/features/examwork/ExamStudentImport/ExamStudentImportDialog.vue
  27. 13 2
      src/features/examwork/ExamStudentManagement/ExamStudentManagement.vue
  28. 17 8
      src/features/examwork/ExamStudentManagement/ExamStudentManagementDialog.vue
  29. 3 0
      src/features/examwork/ImportExportTask/ImportExportTask.vue
  30. 3 0
      src/features/examwork/InvigilateManagement/InvigilateManagement.vue
  31. 9 1
      src/features/examwork/MarkResultManagement/MarkResultManagement.vue
  32. 92 0
      src/features/examwork/StudentManagement/HlsMedia.vue
  33. 3 0
      src/features/examwork/StudentManagement/StudentManagement.vue
  34. 83 50
      src/features/examwork/StudentManagement/StudentManagementDialog.vue
  35. 287 0
      src/features/examwork/StudentManagement/StudentMonitorRecord.vue
  36. 2 2
      src/features/invigilation/ExamInvigilation/ExamInvigilation.vue
  37. 18 7
      src/features/invigilation/RealtimeMonitoring/RealtimeMonitoring.vue
  38. 16 3
      src/features/invigilation/RealtimeMonitoring/VideoCommunication.vue
  39. 15 3
      src/features/invigilation/RealtimeMonitoring/WarningDetail.vue
  40. 0 1
      src/features/invigilation/StudentLogManage/StudentLogManage.vue
  41. 13 4
      src/features/system/OrgManagement/OrgManagementDialog.vue
  42. 3 0
      src/features/system/UserManagement/UserManagement.vue
  43. 90 5
      src/features/system/UserManagement/UserManagementDialog.vue
  44. 5 1
      src/filters/index.js
  45. 21 2
      src/plugins/axiosApp.js
  46. 7 0
      src/plugins/trtc.js
  47. 8 1
      src/router/index.js
  48. 6 1
      src/store/modules/user.js
  49. 2 2
      src/styles/global.css
  50. 3 0
      src/styles/icons.scss
  51. 4 0
      src/utils/utils.js
  52. 7 0
      src/views/Layout/Layout.vue
  53. 59 5
      yarn.lock

+ 4 - 2
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "exam-admin",
   "name": "exam-admin",
-  "version": "1.0.0",
+  "version": "1.1.0",
   "private": true,
   "private": true,
   "scripts": {
   "scripts": {
     "serve": "vue-cli-service serve",
     "serve": "vue-cli-service serve",
@@ -16,6 +16,7 @@
     "start": "vue-cli-service serve"
     "start": "vue-cli-service serve"
   },
   },
   "dependencies": {
   "dependencies": {
+    "@chenfengyuan/vue-qrcode": "1.0.2",
     "axios": "^0.19.2",
     "axios": "^0.19.2",
     "axios-progress-bar": "^1.2.0",
     "axios-progress-bar": "^1.2.0",
     "axios-retry": "^3.1.8",
     "axios-retry": "^3.1.8",
@@ -25,13 +26,14 @@
     "echarts": "^4.8.0",
     "echarts": "^4.8.0",
     "element-ui": "^2.13.2",
     "element-ui": "^2.13.2",
     "flv.js": "^1.5.0",
     "flv.js": "^1.5.0",
+    "hls.js": "^1.1.5",
     "js-cookie": "^2.2.1",
     "js-cookie": "^2.2.1",
     "js-md5": "^0.7.3",
     "js-md5": "^0.7.3",
     "lodash-es": "^4.17.15",
     "lodash-es": "^4.17.15",
     "moment": "^2.27.0",
     "moment": "^2.27.0",
     "query-string": "^6.13.1",
     "query-string": "^6.13.1",
     "register-service-worker": "^1.7.1",
     "register-service-worker": "^1.7.1",
-    "trtc-js-sdk": "^4.6.1",
+    "trtc-js-sdk": "^4.12.1",
     "vue": "^2.6.11",
     "vue": "^2.6.11",
     "vue-awesome": "^4.1.0",
     "vue-awesome": "^4.1.0",
     "vue-echarts": "^5.0.0-beta.0",
     "vue-echarts": "^5.0.0-beta.0",

+ 1 - 2
src/api/examwork-course.js

@@ -5,14 +5,13 @@ import { object2QueryString } from "@/utils/utils";
 export function searchCourses({
 export function searchCourses({
   id = "",
   id = "",
   examId = "",
   examId = "",
-  courseName = "",
   courseCode = "",
   courseCode = "",
   hasPaper = "",
   hasPaper = "",
   pageNumber = 1,
   pageNumber = 1,
   pageSize = 10,
   pageSize = 10,
 }) {
 }) {
   const data = pickBy(
   const data = pickBy(
-    { id, examId, courseCode, courseName, hasPaper, pageNumber, pageSize },
+    { id, examId, courseCode, hasPaper, pageNumber, pageSize },
     (v) => v !== ""
     (v) => v !== ""
   );
   );
   if (data.examId || data.id)
   if (data.examId || data.id)

+ 3 - 3
src/api/examwork-student.js

@@ -1,6 +1,6 @@
 import { httpApp } from "@/plugins/axiosIndex";
 import { httpApp } from "@/plugins/axiosIndex";
 import { pickBy } from "lodash-es";
 import { pickBy } from "lodash-es";
-import { object2QueryString, AESString } from "@/utils/utils";
+import { object2QueryString, encodePassword } from "@/utils/utils";
 
 
 export function searchStudents({
 export function searchStudents({
   enable,
   enable,
@@ -32,7 +32,7 @@ export function saveStudent({
   );
   );
   return httpApp.post("/api/admin/student/save", {
   return httpApp.post("/api/admin/student/save", {
     ...data,
     ...data,
-    ...(password.length > 0 ? { password: AESString(password) } : {}),
+    ...(password.length > 0 ? { password: encodePassword(password) } : {}),
   });
   });
 }
 }
 
 
@@ -43,7 +43,7 @@ export function toggleEnableStudent({ id, enable }) {
 export function resetStudentPassword({ id, password }) {
 export function resetStudentPassword({ id, password }) {
   return httpApp.post("/api/admin/student/updatePwd", {
   return httpApp.post("/api/admin/student/updatePwd", {
     id,
     id,
-    password: AESString(password),
+    password: encodePassword(password),
   });
   });
 }
 }
 
 

+ 2 - 2
src/api/login.js

@@ -1,10 +1,10 @@
 import { httpApp } from "@/plugins/axiosIndex";
 import { httpApp } from "@/plugins/axiosIndex";
-import { object2QueryString, AESString } from "@/utils/utils";
+import { object2QueryString, encodePassword } from "@/utils/utils";
 
 
 export function loginByUsername({ loginName, password, code }) {
 export function loginByUsername({ loginName, password, code }) {
   const data = {
   const data = {
     loginName,
     loginName,
-    password: AESString(password),
+    password: encodePassword(password),
     code,
     code,
   };
   };
   return httpApp.post("/api/admin/user/login/account", data, {
   return httpApp.post("/api/admin/user/login/account", data, {

+ 3 - 3
src/api/system-user.js

@@ -1,6 +1,6 @@
 import { httpApp } from "@/plugins/axiosIndex";
 import { httpApp } from "@/plugins/axiosIndex";
 import { pickBy } from "lodash-es";
 import { pickBy } from "lodash-es";
-import { object2QueryString, AESString } from "@/utils/utils";
+import { object2QueryString, encodePassword } from "@/utils/utils";
 
 
 export function searchUsers({
 export function searchUsers({
   orgId = "",
   orgId = "",
@@ -34,7 +34,7 @@ export function saveUser({
   );
   );
   return httpApp.post("/api/admin/user/save", {
   return httpApp.post("/api/admin/user/save", {
     ...data,
     ...data,
-    ...(password.length > 0 ? { password: AESString(password) } : {}),
+    ...(password.length > 0 ? { password: encodePassword(password) } : {}),
   });
   });
 }
 }
 
 
@@ -45,7 +45,7 @@ export function toggleEnableUser({ id, enable }) {
 export function resetUserPassword({ id, password }) {
 export function resetUserPassword({ id, password }) {
   return httpApp.post("/api/admin/user/updatePwd", {
   return httpApp.post("/api/admin/user/updatePwd", {
     id,
     id,
-    password: AESString(password),
+    password: encodePassword(password),
   });
   });
 }
 }
 
 

BIN
src/assets/icon-error.png


+ 7 - 0
src/components/ActivitySelect.vue

@@ -30,6 +30,10 @@ export default {
     value: String,
     value: String,
     examId: String,
     examId: String,
     styles: { type: String },
     styles: { type: String },
+    examRequired: {
+      type: Boolean,
+      default: false,
+    },
   },
   },
   data() {
   data() {
     return {
     return {
@@ -53,6 +57,9 @@ export default {
   },
   },
   methods: {
   methods: {
     async search(query) {
     async search(query) {
+      this.optionList = [];
+      if (this.examRequired && !this.examId) return;
+
       const res = await searchActivities({
       const res = await searchActivities({
         examId: this.examId,
         examId: this.examId,
         code: query,
         code: query,

+ 7 - 0
src/components/CourseSelect.vue

@@ -30,6 +30,10 @@ export default {
     value: String,
     value: String,
     examId: String,
     examId: String,
     styles: { type: String },
     styles: { type: String },
+    examRequired: {
+      type: Boolean,
+      default: false,
+    },
   },
   },
   data() {
   data() {
     return {
     return {
@@ -53,6 +57,9 @@ export default {
   },
   },
   methods: {
   methods: {
     async search(query) {
     async search(query) {
+      this.optionList = [];
+      if (this.examRequired && !this.examId) return;
+
       const res = await searchCourses({
       const res = await searchCourses({
         examId: this.examId,
         examId: this.examId,
         courseName: query,
         courseName: query,

+ 1 - 0
src/components/ExamRoomSelect.vue

@@ -55,6 +55,7 @@ export default {
   },
   },
   methods: {
   methods: {
     async search(query) {
     async search(query) {
+      this.optionList = [];
       if (!this.examId) return;
       if (!this.examId) return;
       const res = await this.$http.post(
       const res = await this.$http.post(
         "/api/admin/sys/exam/privilegeQuery?" +
         "/api/admin/sys/exam/privilegeQuery?" +

+ 12 - 1
src/components/MinuteInput.vue

@@ -1,5 +1,10 @@
 <template>
 <template>
-  <el-input v-model.number="minute" @change="watchMinute" @input="watchMinute">
+  <el-input
+    v-model.number="minute"
+    @change="watchMinute"
+    @input="watchMinute"
+    :maxlength="6"
+  >
     <template slot="append">分钟</template>
     <template slot="append">分钟</template>
   </el-input>
   </el-input>
 </template>
 </template>
@@ -30,7 +35,13 @@ export default {
   },
   },
   methods: {
   methods: {
     watchMinute() {
     watchMinute() {
+      // 负数要用blur
+      // if (this.minute === "-" || this.minute === "") {
+      //   return;
+      // }
+      this.minute = +this.minute;
       let v = this.minute * 60;
       let v = this.minute * 60;
+      // console.log(v, isFinite(v));
       if (!isFinite(v)) {
       if (!isFinite(v)) {
         v = 0;
         v = 0;
         this.minute = 0;
         this.minute = 0;

+ 7 - 4
src/components/VEditor/VEditor.vue

@@ -38,9 +38,9 @@ export default {
     renderRichText(JSON.parse(this.value), this.$refs.editor);
     renderRichText(JSON.parse(this.value), this.$refs.editor);
 
 
     this.$refs.editor.addEventListener("paste", (event) => {
     this.$refs.editor.addEventListener("paste", (event) => {
+      event.preventDefault();
       const clipboard = event.clipboardData || window.clipboardData;
       const clipboard = event.clipboardData || window.clipboardData;
-      if (!clipboard.files) {
-        event.preventDefault();
+      if (!clipboard.files || !clipboard.files.length) {
         let pasteText = clipboard.getData("text");
         let pasteText = clipboard.getData("text");
         document.execCommand("insertText", false, pasteText);
         document.execCommand("insertText", false, pasteText);
       }
       }
@@ -50,11 +50,14 @@ export default {
     emitJSON() {
     emitJSON() {
       if (this.$refs.editor.contentEditable) {
       if (this.$refs.editor.contentEditable) {
         const json = toJSON(this.$refs.editor);
         const json = toJSON(this.$refs.editor);
-        // this.$emit("input", json);
-        // this.$emit("change", json);
         this.$emit("result", json);
         this.$emit("result", json);
+        // this.$emit("input", json);
+        this.$emit("change", json);
       }
       }
     },
     },
+    getTextContent() {
+      return this.$refs.editor.textContent;
+    },
   },
   },
 };
 };
 </script>
 </script>

+ 15 - 10
src/constant/constants.js

@@ -9,7 +9,7 @@ if (!localStorage.getItem("deviceId")) {
 export const DEVICE_ID = localStorage.getItem("deviceId");
 export const DEVICE_ID = localStorage.getItem("deviceId");
 
 
 export const INVIGILATOR_IMPORT_TEMPLATE_DOWNLOAD_URL =
 export const INVIGILATOR_IMPORT_TEMPLATE_DOWNLOAD_URL =
-  "http://qmth-test.oss-cn-shenzhen.aliyuncs.com/file/考场监考老师导入.xlsx";
+  "https://qmth-test.oss-cn-shenzhen.aliyuncs.com/file/考场监考老师导入.xlsx";
 export const EXAM_STUDENT_IMPORT_TEMPLATE_DOWNLOAD_URL =
 export const EXAM_STUDENT_IMPORT_TEMPLATE_DOWNLOAD_URL =
   "https://cdn.online-exam.cn/frontend/template/考生导入模版.xlsx";
   "https://cdn.online-exam.cn/frontend/template/考生导入模版.xlsx";
 
 
@@ -35,6 +35,11 @@ export const BOOLEAN_INVERSE_TYPE = {
   1: "否",
   1: "否",
   0: "是",
   0: "是",
 };
 };
+// 通讯故障类型
+export const MONITOR_STATUS_TYPE = {
+  STOP: "是",
+  START: "否",
+};
 // 视频流类型
 // 视频流类型
 export const VIDEO_SOURCE_TYPE = {
 export const VIDEO_SOURCE_TYPE = {
   CLIENT_CAMERA: "电脑摄像头",
   CLIENT_CAMERA: "电脑摄像头",
@@ -48,15 +53,6 @@ export const STUDENT_ONLINE_STATUS = {
   EXAMING: "考试中",
   EXAMING: "考试中",
   BREAK_OFF: "通讯故障",
   BREAK_OFF: "通讯故障",
 };
 };
-// 推流通讯
-export const STUDENT_EXAM_STATUS = {
-  FIRST_PREPARE: "首次候考",
-  ANSWERING: "正在答题",
-  BREAK_OFF: "",
-  RESUME_PREPARE: "",
-  FINISHED: "",
-  PERSISTED: "",
-};
 // 违纪、缺考
 // 违纪、缺考
 export const STUDENT_BEHAVIOR_STATUS = {
 export const STUDENT_BEHAVIOR_STATUS = {
   0: "违纪",
   0: "违纪",
@@ -130,6 +126,15 @@ export const MONITOR_STATUS_SOURCE = {
   START: 1,
   START: 1,
   FINISH: 0,
   FINISH: 0,
 };
 };
+// 考试记录状态
+export const EXAM_RECORD_STATUS = {
+  FIRST_PREPARE: "首次候考",
+  ANSWERING: "正在答题",
+  BREAK_OFF: "已中断",
+  RESUME_PREPARE: "断点恢复候考",
+  FINISHED: "已结束考试",
+  PERSISTED: "数据已保存",
+};
 
 
 export const IMPORT_EXPORT_TASKS = [
 export const IMPORT_EXPORT_TASKS = [
   { code: "CALCULATE_EXAM_SCORE", name: "考试重新算分" },
   { code: "CALCULATE_EXAM_SCORE", name: "考试重新算分" },

+ 4 - 1
src/features/Login/Login.vue

@@ -87,7 +87,10 @@
     </div>
     </div>
     <div class="footer">
     <div class="footer">
       Copyright ©
       Copyright ©
-      <a href="https://www.qmth.com.cn" target="_blank">武汉启明软件</a> 2021.
+      <a href="http://www.qmth.com.cn" target="_blank">www.qmth.com.cn</a>, All
+      Rights Reserved.<a href="https://beian.miit.gov.cn/" target="_blank"
+        >鄂ICP备12000033号-10</a
+      >
     </div>
     </div>
   </div>
   </div>
 </template>
 </template>

+ 3 - 0
src/features/examwork/ActivityManagement/ActivityManagement.vue

@@ -137,6 +137,9 @@ export default {
       });
       });
       this.tableData = res.data.data.records;
       this.tableData = res.data.data.records;
       this.total = res.data.data.total;
       this.total = res.data.data.total;
+      if (this.total > 0 && this.tableData.length === 0) {
+        this.handleCurrentChange(this.currentPage - 1);
+      }
     },
     },
     handleCurrentChange(val) {
     handleCurrentChange(val) {
       this.currentPage = val;
       this.currentPage = val;

+ 36 - 2
src/features/examwork/ActivityManagement/ActivityManagementDialog.vue

@@ -153,6 +153,12 @@ export default {
         ],
         ],
         maxDurationSeconds: [
         maxDurationSeconds: [
           { required: true, message: "考试时长必填" },
           { required: true, message: "考试时长必填" },
+          {
+            type: "number",
+            min: 1,
+            max: 10000 * 60,
+            message: "必须在0到10000之间",
+          },
           {
           {
             validator: (rule, value) => {
             validator: (rule, value) => {
               return new Promise((resolve, reject) => {
               return new Promise((resolve, reject) => {
@@ -169,8 +175,36 @@ export default {
             message: "考试时长超出范围",
             message: "考试时长超出范围",
           },
           },
         ],
         ],
-        prepareSeconds: [{ required: true, message: "候考时间必填" }],
-        openingSeconds: [{ required: true, message: "迟到时长必填" }],
+        prepareSeconds: [
+          { required: true, message: "候考时间必填" },
+          {
+            type: "number",
+            min: 0,
+            max: 10000 * 60,
+            message: "必须在0到10000之间",
+          },
+        ],
+        openingSeconds: [
+          { required: true, message: "迟到时长必填" },
+          {
+            type: "number",
+            min: 0,
+            max: 10000 * 60,
+            message: "必须在0到10000之间",
+          },
+          {
+            validator: (rule, value) => {
+              return new Promise((resolve, reject) => {
+                if (value < this.form.maxDurationSeconds) {
+                  resolve();
+                } else {
+                  reject("reject");
+                }
+              });
+            },
+            message: "迟到时长不能大于考试时长",
+          },
+        ],
       },
       },
       loading: false,
       loading: false,
     };
     };

+ 6 - 3
src/features/examwork/CourseManagement/CourseManagement.vue

@@ -10,7 +10,7 @@
             <ExamSelect v-model="form.examId" />
             <ExamSelect v-model="form.examId" />
           </el-form-item>
           </el-form-item>
           <el-form-item label="科目名称">
           <el-form-item label="科目名称">
-            <CourseSelect :examId="form.examId" v-model="form.code" />
+            <CourseSelect :examId="form.examId" v-model="form.courseCode" />
           </el-form-item>
           </el-form-item>
           <el-form-item label="状态">
           <el-form-item label="状态">
             <StateSelect
             <StateSelect
@@ -120,7 +120,7 @@ export default {
     return {
     return {
       form: {
       form: {
         examId: "",
         examId: "",
-        code: "",
+        courseCode: "",
         hasPaper: null,
         hasPaper: null,
       },
       },
       rules: {
       rules: {
@@ -145,13 +145,16 @@ export default {
       }
       }
       const res = await searchCourses({
       const res = await searchCourses({
         examId: this.form.examId,
         examId: this.form.examId,
-        courseCode: this.form.code,
+        courseCode: this.form.courseCode,
         hasPaper: this.form.hasPaper,
         hasPaper: this.form.hasPaper,
         pageNumber: this.currentPage,
         pageNumber: this.currentPage,
         pageSize: this.pageSize,
         pageSize: this.pageSize,
       });
       });
       this.tableData = res.data.data.records;
       this.tableData = res.data.data.records;
       this.total = res.data.data.total;
       this.total = res.data.data.total;
+      if (this.total > 0 && this.tableData.length === 0) {
+        this.handleCurrentChange(this.currentPage - 1);
+      }
     },
     },
     handleCurrentChange(val) {
     handleCurrentChange(val) {
       this.currentPage = val;
       this.currentPage = val;

+ 1 - 0
src/features/examwork/CourseManagement/CoursePaperDialog.vue

@@ -57,6 +57,7 @@
             <span slot-scope="scope">
             <span slot-scope="scope">
               <el-input-number
               <el-input-number
                 :min="1"
                 :min="1"
+                :max="1000"
                 v-model.trim="scope.row.audioPlayCount"
                 v-model.trim="scope.row.audioPlayCount"
               ></el-input-number>
               ></el-input-number>
             </span>
             </span>

+ 1 - 0
src/features/examwork/CourseManagement/PaperImportDialog.vue

@@ -28,6 +28,7 @@
         <el-form-item label="音频播放次数">
         <el-form-item label="音频播放次数">
           <el-input-number
           <el-input-number
             :min="1"
             :min="1"
+            :max="1000"
             v-model.trim="form.audioPlayCount"
             v-model.trim="form.audioPlayCount"
           ></el-input-number>
           ></el-input-number>
         </el-form-item>
         </el-form-item>

+ 515 - 209
src/features/examwork/ExamManagement/ExamEdit.vue

@@ -20,12 +20,16 @@
           </el-row>
           </el-row>
           <el-row>
           <el-row>
             <el-form-item label="批次编码" prop="code">
             <el-form-item label="批次编码" prop="code">
-              <el-input :disabled="isEdit" v-model.trim="form.code"></el-input>
+              <el-input
+                :disabled="isEdit"
+                v-model.trim="form.code"
+                maxlength="30"
+              ></el-input>
             </el-form-item>
             </el-form-item>
           </el-row>
           </el-row>
           <el-row>
           <el-row>
             <el-form-item label="批次名称" prop="name">
             <el-form-item label="批次名称" prop="name">
-              <el-input v-model.trim="form.name"></el-input>
+              <el-input v-model.trim="form.name" maxlength="30"></el-input>
             </el-form-item>
             </el-form-item>
           </el-row>
           </el-row>
           <el-row>
           <el-row>
@@ -43,32 +47,37 @@
           </el-row>
           </el-row>
           <el-row v-if="!isModeAnytime">
           <el-row v-if="!isModeAnytime">
             <el-form-item label="候考时长(分钟)">
             <el-form-item label="候考时长(分钟)">
-              <MinuteInput v-model.trim="form.prepareSeconds"> </MinuteInput>
+              <MinuteInput
+                v-model.trim="form.prepareSeconds"
+                :min="0"
+                :max="10000"
+              >
+              </MinuteInput>
             </el-form-item>
             </el-form-item>
           </el-row>
           </el-row>
           <el-row>
           <el-row>
-            <el-form-item label="考试次数限制">
+            <el-form-item label="考试次数限制" prop="examCount">
               <el-input-number
               <el-input-number
                 v-model.number.trim="form.examCount"
                 v-model.number.trim="form.examCount"
-                :min="0"
+                :min="1"
+                :max="10000"
               ></el-input-number>
               ></el-input-number>
             </el-form-item>
             </el-form-item>
           </el-row>
           </el-row>
           <el-row>
           <el-row>
-            <el-form-item label="考试时长(分钟)">
+            <el-form-item label="考试时长(分钟)" prop="maxDurationSeconds">
               <MinuteInput v-model.trim="form.maxDurationSeconds">
               <MinuteInput v-model.trim="form.maxDurationSeconds">
               </MinuteInput>
               </MinuteInput>
             </el-form-item>
             </el-form-item>
           </el-row>
           </el-row>
           <el-row v-if="!isModeAnytime">
           <el-row v-if="!isModeAnytime">
-            <el-form-item label="迟到时长(分钟)">
+            <el-form-item label="迟到时长(分钟)" prop="openingSeconds">
               <MinuteInput v-model.trim="form.openingSeconds"> </MinuteInput>
               <MinuteInput v-model.trim="form.openingSeconds"> </MinuteInput>
             </el-form-item>
             </el-form-item>
           </el-row>
           </el-row>
           <el-row>
           <el-row>
-            <el-form-item label="冻结时间(分钟)">
-              <MinuteInput v-model.trim="form.minDurationSeconds">
-              </MinuteInput>
+            <el-form-item label="冻结时间(分钟)" prop="minDurationSeconds">
+              <MinuteInput v-model.trim="form.minDurationSeconds" />
             </el-form-item>
             </el-form-item>
           </el-row>
           </el-row>
           <el-row v-if="!isModeAnytime">
           <el-row v-if="!isModeAnytime">
@@ -101,12 +110,17 @@
             <el-form-item v-if="enableBreakProxy" label="断点次数">
             <el-form-item v-if="enableBreakProxy" label="断点次数">
               <el-input-number
               <el-input-number
                 v-model.trim="form.breakResumeCount"
                 v-model.trim="form.breakResumeCount"
-                :min="0"
+                :min="1"
+                :max="10000"
               ></el-input-number>
               ></el-input-number>
             </el-form-item>
             </el-form-item>
           </el-row>
           </el-row>
           <el-row>
           <el-row>
-            <el-form-item v-if="enableBreakProxy" label="断点时长(分钟)">
+            <el-form-item
+              v-if="enableBreakProxy"
+              prop="breakExpireSeconds"
+              label="断点时长(分钟)"
+            >
               <MinuteInput v-model.trim="form.breakExpireSeconds">
               <MinuteInput v-model.trim="form.breakExpireSeconds">
               </MinuteInput>
               </MinuteInput>
             </el-form-item>
             </el-form-item>
@@ -191,12 +205,11 @@
           :model="form"
           :model="form"
           :rules="rules"
           :rules="rules"
           label-width="180px"
           label-width="180px"
-          inline
           :disabled="disableEdit"
           :disabled="disableEdit"
         >
         >
           <el-row class="tab-invililation">
           <el-row class="tab-invililation">
             <h2>开考检测</h2>
             <h2>开考检测</h2>
-            <el-form-item label="">
+            <el-form-item label="" label-width="0px">
               <div class="d-flex flex-column tab-invililation-radio">
               <div class="d-flex flex-column tab-invililation-radio">
                 <el-radio v-model="form.entryAuthenticationPolicy" label="OFF">
                 <el-radio v-model="form.entryAuthenticationPolicy" label="OFF">
                   安全级别:<span style="color: #202b4b; font-size: 20px;">
                   安全级别:<span style="color: #202b4b; font-size: 20px;">
@@ -375,162 +388,136 @@
               </el-radio>
               </el-radio>
             </el-form-item>
             </el-form-item>
           </el-row>
           </el-row>
-          <!-- <el-row>
-            <el-form-item v-if="form.monitorProxy" label="是否需要视频回放">
-              <el-radio v-model="form.monitorRecord" :label="1">是 </el-radio>
-              <el-radio v-model="form.monitorRecord" :label="0">否 </el-radio>
-            </el-form-item>
-          </el-row> -->
-          <el-row>
-            <el-form-item v-if="form.monitorProxy" label="电脑&手机监控方案">
-              <el-checkbox-group v-model="form.monitorVideoSource">
-                <div class="d-flex">
-                  <div class="d-flex flex-column justify-content-between">
-                    <el-checkbox label="CLIENT_CAMERA"
-                      >电脑摄像头主机位</el-checkbox
-                    >
-                    <el-checkbox
-                      label="CLIENT_SCREEN"
-                      :disabled="
-                        !form.monitorVideoSource.includes('CLIENT_CAMERA')
-                      "
-                      :title="
-                        !form.monitorVideoSource.includes('CLIENT_CAMERA') &&
-                        '原因:先选择电脑摄像头主机位'
-                      "
-                      >电脑开启录屏</el-checkbox
-                    >
-                    <el-checkbox label="MOBILE_FIRST">手机主机位</el-checkbox>
-                    <el-checkbox
-                      :disabled="
-                        !form.monitorVideoSource.includes('MOBILE_FIRST')
-                      "
-                      :title="
-                        !form.monitorVideoSource.includes('MOBILE_FIRST') &&
-                        '原因:先选择手机主机位'
-                      "
-                      label="MOBILE_SECOND"
-                      >手机辅机位</el-checkbox
-                    >
-                    <span style="color: red; font-size: 12px;"
-                      >*主机位设备负责收音及播放监考提示</span
-                    >
-                  </div>
-                  <div
-                    style="
-                      margin-left: 20px;
-                      padding: 10px;
-                      font-size: 10px;
-                      background: #f0f4f9;
-                      position: relative;
-                    "
-                  >
-                    <div
-                      style="
-                        position: absolute;
-                        width: 108px;
-                        height: 40px;
-                        border-left: 1px dotted grey;
-                        border-bottom: 1px dotted grey;
-                        left: 35px;
-                        top: 115px;
-                        border-radius: 5px;
-                      "
-                      :class="
-                        form.monitorVideoSource.includes('MOBILE_SECOND') &&
-                        'enhance-left-bottom-line'
-                      "
-                    ></div>
-                    <div
-                      style="
-                        position: absolute;
-                        width: 108px;
-                        height: 40px;
-                        border-right: 1px dotted grey;
-                        border-top: 1px dotted grey;
-                        right: 35px;
-                        bottom: 122px;
-                        border-radius: 5px;
-                      "
-                      :class="
-                        form.monitorVideoSource.includes('MOBILE_FIRST') &&
-                        'enhance-right-bottom-line'
-                      "
-                    ></div>
-                    <div
-                      style="
-                        position: absolute;
-                        width: 2px;
-                        height: 10px;
-                        border-right: 1px dotted grey;
-                        left: 236px;
-                        top: 92px;
-                      "
-                      :class="
-                        form.monitorVideoSource.includes('CLIENT_CAMERA') &&
-                        'enhance-right-line'
-                      "
-                    ></div>
-                    <div class="d-flex">
-                      <div
-                        class="d-flex flex-column justify-content-center align-items-center"
-                        style="width: 52px !important;"
-                      >
-                        <div :class="monitorImgSrc('MOBILE_SECOND')" />
-                        <span style="font-size: 10px;">手机辅机位</span>
-                      </div>
-                      <div
-                        class="d-flex flex-column justify-content-center align-items-center"
-                        style="width: 350px !important;"
-                      >
-                        <div :class="monitorImgSrc('CLIENT_CAMERA')" />
-                        <span style="font-size: 10px;">电脑摄像头主机位</span>
-                      </div>
-                      <div style="width: 52px !important;"></div>
-                    </div>
-                    <div
-                      class="d-flex"
-                      style="margin-bottom: -60px; margin-top: -30px;"
-                    >
-                      <div style="width: 52px !important;"></div>
-                      <div
-                        class="d-flex flex-column justify-content-center align-items-center"
-                        style="width: 350px !important;"
-                      >
-                        <div :class="monitorImgSrc('CLIENT_SCREEN')" />
-                        <span style="font-size: 10px;">电脑开启录屏</span>
-                      </div>
-                      <div style="width: 52px !important;"></div>
-                    </div>
-                    <div class="d-flex">
-                      <div style="width: 52px !important;"></div>
-                      <div style="width: 350px !important;"></div>
-                      <div
-                        class="d-flex flex-column justify-content-center align-items-center"
-                        style="width: 52px !important;"
-                      >
-                        <div :class="monitorImgSrc('MOBILE_FIRST')" />
-                        <span style="font-size: 10px;">手机主机位</span>
-                      </div>
-                    </div>
-                  </div>
-                </div>
-              </el-checkbox-group>
-            </el-form-item>
-          </el-row>
+          <div
+            class="monitor-config"
+            v-if="form.monitorProxy && enablePrevilleges"
+          >
+            <div class="monitor-config-options">
+              <h3 class="monitor-title">监考直播方案配置:</h3>
+              <el-form-item label="电脑摄像头主机位">
+                <el-radio-group
+                  v-model="monitorVideoSource.CLIENT_CAMERA"
+                  @change="monitorClientCameraChange"
+                >
+                  <el-radio
+                    v-for="(val, key) in monitorTypes"
+                    :key="key"
+                    :label="key"
+                    >{{ val }}
+                  </el-radio>
+                </el-radio-group>
+              </el-form-item>
+              <el-form-item label="电脑操作录屏">
+                <el-radio-group
+                  v-model="monitorVideoSource.CLIENT_SCREEN"
+                  :disabled="monitorVideoSource.CLIENT_CAMERA !== '1'"
+                  :title="
+                    monitorVideoSource.CLIENT_CAMERA === '0' &&
+                    '原因:先选择电脑摄像头主机位'
+                  "
+                  @change="monitorClientScreenChange"
+                >
+                  <el-radio
+                    v-for="(val, key) in monitorTinyTypes"
+                    :key="key"
+                    :label="key"
+                    >{{ val }}
+                  </el-radio>
+                </el-radio-group>
+              </el-form-item>
+              <el-form-item label="手机监考主机位">
+                <el-radio-group
+                  v-model="monitorVideoSource.MOBILE_FIRST"
+                  @change="monitorMobileFirstChange"
+                >
+                  <el-radio
+                    v-for="(val, key) in monitorTypes"
+                    :key="key"
+                    :label="key"
+                    >{{ val }}
+                  </el-radio>
+                </el-radio-group>
+              </el-form-item>
+              <el-form-item label="手机监考辅机位">
+                <el-radio-group
+                  v-model="monitorVideoSource.MOBILE_SECOND"
+                  :disabled="monitorVideoSource.MOBILE_FIRST === '0'"
+                  :title="
+                    monitorVideoSource.MOBILE_FIRST === '0' &&
+                    '原因:先选择手机监考主机位'
+                  "
+                  @change="monitorVideoSourceChange"
+                >
+                  <el-radio
+                    v-for="(val, key) in monitorTypes"
+                    :key="key"
+                    :label="key"
+                    >{{ val }}
+                  </el-radio>
+                </el-radio-group>
+              </el-form-item>
+              <p class="tips-info tips-error">
+                *主机位设备负责收音及播放监考提示
+              </p>
+            </div>
+            <div class="monitor-config-view">
+              <div class="monitor-item monitor-client-camera">
+                <div
+                  :class="['monitor-item-img', monitorImgSrc('CLIENT_CAMERA')]"
+                />
+                <p class="monitor-item-desc">电脑摄像头主机位</p>
+              </div>
+              <div class="monitor-item monitor-client-screen">
+                <div
+                  :class="['monitor-item-img', monitorImgSrc('CLIENT_SCREEN')]"
+                />
+                <p class="monitor-item-desc">电脑开启录屏</p>
+              </div>
+              <div class="monitor-item monitor-mobile-first">
+                <div
+                  :class="['monitor-item-img', monitorImgSrc('MOBILE_FIRST')]"
+                />
+                <p class="monitor-item-desc">手机监考主机位</p>
+              </div>
+              <div class="monitor-item monitor-mobile-second">
+                <div
+                  :class="['monitor-item-img', monitorImgSrc('MOBILE_SECOND')]"
+                />
+                <p class="monitor-item-desc">手机监考辅机位</p>
+              </div>
+              <div
+                v-if="monitorVideoSource.MOBILE_SECOND !== '0'"
+                class="monitor-line-mobile-second"
+              >
+                <span></span>
+                <span></span>
+              </div>
+              <div
+                v-if="monitorVideoSource.MOBILE_FIRST !== '0'"
+                class="monitor-line-mobile-first"
+              ></div>
+              <div
+                v-if="monitorVideoSource.CLIENT_CAMERA !== '0'"
+                class="monitor-line-client-camera"
+              ></div>
+            </div>
+          </div>
         </el-form>
         </el-form>
       </el-tab-pane>
       </el-tab-pane>
 
 
       <el-tab-pane label="其他设置" name="third">
       <el-tab-pane label="其他设置" name="third">
         <el-form
         <el-form
+          ref="form3"
           :model="form"
           :model="form"
           label-width="170px"
           label-width="170px"
+          :rules="rules"
           inline
           inline
           :disabled="disableEdit"
           :disabled="disableEdit"
         >
         >
           <el-row>
           <el-row>
-            <el-form-item label="考试须知">
+            <el-form-item prop="preNotice" label="考试须知">
               <VEditor
               <VEditor
+                ref="preNoticeEditor"
                 :value="form.preNoticeClone"
                 :value="form.preNoticeClone"
                 style="width: 300px;"
                 style="width: 300px;"
                 @result="(v) => (form.preNotice = v)"
                 @result="(v) => (form.preNotice = v)"
@@ -539,14 +526,19 @@
           </el-row>
           </el-row>
           <el-row>
           <el-row>
             <el-form-item label="须知强制阅读时长">
             <el-form-item label="须知强制阅读时长">
-              <el-input v-model.trim="form.preNoticeStaySeconds">
-                <template slot="append">秒</template>
-              </el-input>
+              <el-input-number
+                :min="1"
+                :max="24 * 60"
+                step-strictly
+                :controls="false"
+                v-model.trim="form.preNoticeStaySeconds"
+              />(秒)
             </el-form-item>
             </el-form-item>
           </el-row>
           </el-row>
           <el-row>
           <el-row>
-            <el-form-item label="考后说明">
+            <el-form-item prop="postNotice" label="考后说明">
               <VEditor
               <VEditor
+                ref="postNoticeEditor"
                 :value="form.postNoticeClone"
                 :value="form.postNoticeClone"
                 style="width: 300px;"
                 style="width: 300px;"
                 @result="(v) => (form.postNotice = v)"
                 @result="(v) => (form.postNotice = v)"
@@ -559,9 +551,12 @@
               <el-radio v-model="form.enableIpLimit" :label="0">否</el-radio>
               <el-radio v-model="form.enableIpLimit" :label="0">否</el-radio>
             </el-form-item>
             </el-form-item>
           </el-row>
           </el-row>
-          <el-row>
-            <el-form-item label="IP段(*表示任意):">
-              <el-input v-model.trim="form.ipAllow"></el-input>
+          <el-row v-if="form.enableIpLimit">
+            <el-form-item label="IP段(*表示任意):" prop="ipAllow">
+              <el-input
+                v-model.trim="form.ipAllow"
+                placeholder="192.168.10.1, 192.168.11.*, 172.10.*.*"
+              ></el-input>
             </el-form-item>
             </el-form-item>
           </el-row>
           </el-row>
         </el-form>
         </el-form>
@@ -620,7 +615,7 @@ export default {
       set(v) {
       set(v) {
         if (v) {
         if (v) {
           this.form.breakResumeCount = 1;
           this.form.breakResumeCount = 1;
-          this.form.breakExpireSeconds = 0;
+          this.form.breakExpireSeconds = 60;
         } else {
         } else {
           this.form.breakResumeCount = null;
           this.form.breakResumeCount = null;
           this.form.breakExpireSeconds = null;
           this.form.breakExpireSeconds = null;
@@ -633,6 +628,9 @@ export default {
     disableEdit() {
     disableEdit() {
       return this.form.monitorStatus === "FINISHED";
       return this.form.monitorStatus === "FINISHED";
     },
     },
+    enablePrevilleges() {
+      return !!this.$store.state.user.orgInfo.enableMonitorRecord;
+    },
   },
   },
   watch: {
   watch: {
     "form.mode": {
     "form.mode": {
@@ -657,38 +655,11 @@ export default {
       handler(v) {
       handler(v) {
         if (!v) {
         if (!v) {
           this.form.monitorVideoSource = [];
           this.form.monitorVideoSource = [];
-          this.form.monitorRecord = 0;
+          this.form.monitorRecord = [];
         }
         }
         if (this.form.monitorVideoSource === null) {
         if (this.form.monitorVideoSource === null) {
           this.form.monitorVideoSource = [];
           this.form.monitorVideoSource = [];
-        }
-      },
-    },
-    "form.monitorVideoSource": {
-      immediate: true,
-      handler(v, ov) {
-        if (!v) {
-          this.form.monitorVideoSource = [];
-        }
-        if (
-          // 没动静,不修改,避免死循环
-          (v || []).includes("MOBILE_FIRST") !==
-            (ov || []).includes("MOBILE_FIRST") &&
-          !this.form.monitorVideoSource.includes("MOBILE_FIRST")
-        ) {
-          this.form.monitorVideoSource = this.form.monitorVideoSource.filter(
-            (v) => v !== "MOBILE_SECOND"
-          );
-        }
-        if (
-          // 没动静,不修改,避免死循环
-          (v || []).includes("CLIENT_CAMERA") !==
-            (ov || []).includes("CLIENT_CAMERA") &&
-          !this.form.monitorVideoSource.includes("CLIENT_CAMERA")
-        ) {
-          this.form.monitorVideoSource = this.form.monitorVideoSource.filter(
-            (v) => v !== "CLIENT_SCREEN"
-          );
+          this.form.monitorRecord = [];
         }
         }
       },
       },
     },
     },
@@ -721,9 +692,12 @@ export default {
         this.form.inProcessLivenessFixedRange = [0, 0];
         this.form.inProcessLivenessFixedRange = [0, 0];
       }
       }
       this.form.startEndTimeProxy = [this.form.startTime, this.form.endTime];
       this.form.startEndTimeProxy = [this.form.startTime, this.form.endTime];
-      this.form.monitorProxy = !!this.form.monitorVideoSource;
+      this.form.monitorRecord = this.form.monitorRecord || [];
+      this.form.monitorVideoSource = this.form.monitorVideoSource || [];
+      this.form.monitorProxy = !!this.form.monitorVideoSource.length;
       this.form.preNoticeClone = this.form.preNotice;
       this.form.preNoticeClone = this.form.preNotice;
       this.form.postNoticeClone = this.form.postNotice;
       this.form.postNoticeClone = this.form.postNotice;
+      this.parseMonitorVideoSource();
     }
     }
 
 
     // sleep
     // sleep
@@ -773,15 +747,76 @@ export default {
         inProcessLivenessFixedRange: [0, 0],
         inProcessLivenessFixedRange: [0, 0],
         inProcessLivenessJudgePolicy: "ALL",
         inProcessLivenessJudgePolicy: "ALL",
         monitorProxy: false,
         monitorProxy: false,
-        monitorRecord: 0,
+        monitorRecord: [],
         monitorVideoSource: [],
         monitorVideoSource: [],
         ipAllow: "",
         ipAllow: "",
       },
       },
       rules: {
       rules: {
         mode: { required: true, message: "必填" },
         mode: { required: true, message: "必填" },
-        name: { required: true, message: "必填" },
-        code: { required: true, message: "必填" },
+        name: [
+          { required: true, message: "必填" },
+          { type: "string", min: 1, max: 30, message: "长度必须在1到30之间" },
+        ],
+        code: [
+          { required: true, message: "必填" },
+          { type: "string", min: 1, max: 30, message: "长度必须在1到30之间" },
+        ],
         startEndTimeProxy: { required: true, message: "必填" },
         startEndTimeProxy: { required: true, message: "必填" },
+        examCount: [
+          { required: true, message: "必填" },
+          { type: "number", min: 1, message: "必须大于0" },
+        ],
+        maxDurationSeconds: [
+          { required: true, message: "必填" },
+          { type: "number", min: 1, message: "必须大于0" },
+        ],
+        openingSeconds: {
+          validator: (rule, value) => {
+            // console.log(value);
+            return new Promise((resolve, reject) => {
+              if (
+                !this.form.isModeAnytime &&
+                this.form.maxDurationSeconds < value
+              ) {
+                reject("迟到时长必须小于考试时长");
+              } else {
+                resolve();
+              }
+            });
+          },
+        },
+        minDurationSeconds: {
+          trigger: "change",
+          validator: (rule, value) => {
+            // console.log(value);
+            return new Promise((resolve, reject) => {
+              if (this.form.maxDurationSeconds < value) {
+                reject("冻结时长必须小于考试时长");
+              } else {
+                resolve();
+              }
+            });
+          },
+        },
+        breakExpireSeconds: {
+          trigger: "change",
+          validator: (rule, value) => {
+            // console.log(value);
+            return new Promise((resolve, reject) => {
+              if (this.enableBreakProxy) {
+                if (this.form.monitorRecord.length && value > 25 * 60) {
+                  reject("开启回放情况下,断点时长不得超过25分钟");
+                } else if (!value) {
+                  reject("请设置断点时长");
+                } else {
+                  resolve();
+                }
+              } else {
+                resolve();
+              }
+            });
+          },
+        },
         inProcessLivenessFixedRange: {
         inProcessLivenessFixedRange: {
           validator: (rule, value) => {
           validator: (rule, value) => {
             return new Promise((resolve, reject) => {
             return new Promise((resolve, reject) => {
@@ -812,6 +847,94 @@ export default {
           },
           },
           // message: "格式错误",
           // message: "格式错误",
         },
         },
+        ipAllow: {
+          validator: (rule, value) => {
+            // console.log(value);
+            return new Promise((resolve, reject) => {
+              if (!this.form.enableIpLimit) {
+                resolve();
+                return;
+              }
+              const ips = value.split(",").map((v) => v.trim());
+              // console.log(ips);
+              const checkIpSeg = (seg, idx) => {
+                if (idx < 2) {
+                  if (!/^\d+$/.test(seg)) {
+                    return false;
+                  }
+                } else if (idx >= 2 && idx < 4) {
+                  if (!(/^\d+$/.test(seg) || /^\*$/.test(seg))) {
+                    return false;
+                  }
+                }
+                return true;
+              };
+              if (
+                ips.every(
+                  (v) =>
+                    v.split(".").length === 4 && v.split(".").every(checkIpSeg)
+                )
+              ) {
+                resolve();
+              } else {
+                reject("格式不匹配");
+              }
+            });
+          },
+        },
+        postNotice: {
+          trigger: "change",
+          validator: (rule, value) => {
+            // console.log(value);
+            return new Promise((resolve, reject) => {
+              if (
+                value &&
+                this.$refs.postNoticeEditor.getTextContent().length > 1000
+              ) {
+                reject("考后说明最多允许输入1000个字符");
+              } else {
+                resolve();
+              }
+            });
+          },
+        },
+        preNotice: {
+          trigger: "change",
+          validator: (rule, value) => {
+            // console.log(value);
+            return new Promise((resolve, reject) => {
+              if (
+                value &&
+                this.$refs.preNoticeEditor.getTextContent().length > 1000
+              ) {
+                reject("考试须知最多允许输入1000个字符");
+              } else {
+                resolve();
+              }
+            });
+          },
+        },
+      },
+      sources: [
+        "MOBILE_FIRST",
+        "MOBILE_SECOND",
+        "CLIENT_CAMERA",
+        "CLIENT_SCREEN",
+      ],
+      monitorTypes: {
+        0: "关闭",
+        1: "直播",
+        2: "直播+回放",
+      },
+      monitorTinyTypes: {
+        0: "关闭",
+        1: "直播",
+      },
+      monitorVideoSource: {
+        CLIENT_CAMERA: "0",
+        CLIENT_SCREEN: "0",
+        MOBILE_FIRST: "0",
+        MOBILE_SECOND: "0",
       },
       },
       orgSetting: null,
       orgSetting: null,
       loading: false,
       loading: false,
@@ -819,7 +942,7 @@ export default {
   },
   },
   methods: {
   methods: {
     monitorImgSrc(monitorSource) {
     monitorImgSrc(monitorSource) {
-      const selected = this.form.monitorVideoSource.includes(monitorSource);
+      const selected = this.monitorVideoSource[monitorSource] !== "0";
       const selectedStr = selected ? "-selected" : "";
       const selectedStr = selected ? "-selected" : "";
       if (monitorSource === "MOBILE_FIRST") {
       if (monitorSource === "MOBILE_FIRST") {
         return `mobile-first-img${selectedStr}`;
         return `mobile-first-img${selectedStr}`;
@@ -836,11 +959,52 @@ export default {
 
 
       return "";
       return "";
     },
     },
+    monitorClientCameraChange(val) {
+      if (val === "0" || val === "2") {
+        this.monitorVideoSource.CLIENT_SCREEN = "0";
+      }
+
+      this.monitorVideoSourceChange();
+    },
+    monitorClientScreenChange() {
+      this.monitorVideoSourceChange();
+    },
+    monitorMobileFirstChange(val) {
+      if (val === "0") this.monitorVideoSource.MOBILE_SECOND = "0";
+      this.monitorVideoSourceChange();
+    },
+    monitorVideoSourceChange() {
+      let monitorVideoSource = [],
+        monitorRecord = [];
+      this.sources.forEach((source) => {
+        if (this.monitorVideoSource[source] === "0") return;
+        if (this.monitorVideoSource[source] === "2") {
+          monitorRecord.push(source);
+        }
+        monitorVideoSource.push(source);
+      });
+
+      this.form.monitorVideoSource = monitorVideoSource;
+      this.form.monitorRecord = monitorRecord;
+    },
+    parseMonitorVideoSource() {
+      this.sources.forEach((source) => {
+        const hasRecord = this.form.monitorRecord.includes(source);
+        const hasSource = this.form.monitorVideoSource.includes(source);
+        if (hasRecord) {
+          this.monitorVideoSource[source] = "2";
+        } else if (hasSource) {
+          this.monitorVideoSource[source] = "1";
+        } else {
+          this.monitorVideoSource[source] = "0";
+        }
+      });
+    },
     async save() {
     async save() {
       try {
       try {
         await this.$refs.form1.validate();
         await this.$refs.form1.validate();
         await this.$refs.form2.validate();
         await this.$refs.form2.validate();
-        // await this.$refs.form3.validate();
+        await this.$refs.form3.validate();
       } catch (error) {
       } catch (error) {
         console.log("校验失败", error);
         console.log("校验失败", error);
         this.$notify({
         this.$notify({
@@ -909,6 +1073,79 @@ export default {
   }
   }
 }
 }
 
 
+.monitor-config {
+  &-options {
+    display: inline-block;
+    vertical-align: top;
+    width: 470px;
+
+    .el-form-item {
+      margin-bottom: 10px;
+    }
+  }
+
+  &-view {
+    position: relative;
+    display: inline-block;
+    vertical-align: top;
+    width: 498px;
+    height: 257px;
+    background: #f0f4f9;
+    border-radius: 6px;
+    border: 1px solid #e8edf3;
+  }
+}
+.monitor-title {
+  height: 20px;
+  font-size: 14px;
+  font-weight: 600;
+  color: #202b4b;
+  line-height: 20px;
+  margin-bottom: 20px;
+  padding-left: 20px;
+
+  &::before {
+    content: "";
+    display: inline-block;
+    vertical-align: middle;
+    width: 4px;
+    height: 10px;
+    background: #1886fe;
+    border-radius: 2px;
+    margin-right: 10px;
+  }
+}
+
+.monitor-item {
+  position: absolute;
+  text-align: center;
+
+  &-img {
+    margin: 0 auto;
+  }
+  &-desc {
+    font-size: 12px;
+    line-height: 1;
+    margin: 5px 0 0;
+  }
+}
+.monitor-client-camera {
+  left: 171px;
+  top: 21px;
+}
+.monitor-client-screen {
+  left: 124px;
+  bottom: 20px;
+}
+.monitor-mobile-first {
+  left: 20px;
+  top: 20px;
+}
+.monitor-mobile-second {
+  bottom: 20px;
+  right: 20px;
+}
+
 .mobile-first-img {
 .mobile-first-img {
   background-image: url(./imgs/手机监考主机位.png);
   background-image: url(./imgs/手机监考主机位.png);
   background-repeat: no-repeat;
   background-repeat: no-repeat;
@@ -956,18 +1193,87 @@ export default {
     background-image: url(./imgs/电脑操作录屏-选中.png);
     background-image: url(./imgs/电脑操作录屏-选中.png);
   }
   }
 }
 }
+.monitor-line-point {
+  content: "";
+  display: block;
+  position: absolute;
+  width: 5px;
+  height: 5px;
+  border: 1px solid #c1cbdb;
+  border-radius: 50%;
+  background: #f0f4f9;
+}
+.monitor-line-client-camera {
+  position: absolute;
+  width: 0;
+  height: 23px;
+  border-left: 1px solid #c1cbdb;
+  top: 89px;
+  left: 219px;
 
 
-.enhance-left-bottom-line {
-  border-left-style: solid !important;
-  border-bottom-style: solid !important;
+  &::before {
+    @extend .monitor-line-point;
+    left: -3px;
+    top: 0;
+  }
+  &::after {
+    @extend .monitor-line-point;
+    left: -3px;
+    bottom: 0;
+  }
 }
 }
+.monitor-line-mobile-first {
+  position: absolute;
+  width: 56px;
+  height: 28px;
+  bottom: 96px;
+  left: 63px;
+  border-left: 1px solid #c1cbdb;
+  border-bottom: 1px solid #c1cbdb;
 
 
-.enhance-right-bottom-line {
-  border-right-style: solid !important;
-  border-top-style: solid !important;
+  &::before {
+    @extend .monitor-line-point;
+    left: -3px;
+    top: 0;
+  }
+  &::after {
+    @extend .monitor-line-point;
+    right: 0;
+    bottom: -3px;
+  }
 }
 }
+.monitor-line-mobile-second {
+  span:nth-of-type(1) {
+    display: block;
+    position: absolute;
+    width: 35px;
+    height: 63px;
+    bottom: 92px;
+    right: 139px;
+    border-right: 1px solid #c1cbdb;
+    border-bottom: 1px solid #c1cbdb;
 
 
-.enhance-right-line {
-  border-right-style: solid !important;
+    &::before {
+      @extend .monitor-line-point;
+      left: 0;
+      bottom: -3px;
+    }
+  }
+  span:nth-of-type(2) {
+    display: block;
+    position: absolute;
+    width: 78px;
+    height: 23px;
+    top: 100px;
+    right: 61px;
+    border-right: 1px solid #c1cbdb;
+    border-top: 1px solid #c1cbdb;
+
+    &::after {
+      @extend .monitor-line-point;
+      right: -3px;
+      bottom: 0;
+    }
+  }
 }
 }
 </style>
 </style>

+ 3 - 0
src/features/examwork/ExamManagement/ExamManagement.vue

@@ -202,6 +202,9 @@ export default {
       });
       });
       this.tableData = res.data.data.records;
       this.tableData = res.data.data.records;
       this.total = res.data.data.total;
       this.total = res.data.data.total;
+      if (this.total > 0 && this.tableData.length === 0) {
+        this.handleCurrentChange(this.currentPage - 1);
+      }
     },
     },
     handleCurrentChange(val) {
     handleCurrentChange(val) {
       this.currentPage = val;
       this.currentPage = val;

BIN
src/features/examwork/ExamManagement/imgs/手机监考主机位-选中.png


BIN
src/features/examwork/ExamManagement/imgs/手机监考主机位.png


BIN
src/features/examwork/ExamManagement/imgs/手机监考辅机位-选中.png


BIN
src/features/examwork/ExamManagement/imgs/手机监考辅机位.png


+ 3 - 0
src/features/examwork/ExamStudentImport/ExamStudentImport.vue

@@ -142,6 +142,9 @@ export default {
       });
       });
       this.tableData = res.data.data.records;
       this.tableData = res.data.data.records;
       this.total = res.data.data.total;
       this.total = res.data.data.total;
+      if (this.total > 0 && this.tableData.length === 0) {
+        this.handleCurrentChange(this.currentPage - 1);
+      }
     },
     },
     handleCurrentChange(val) {
     handleCurrentChange(val) {
       this.currentPage = val;
       this.currentPage = val;

+ 2 - 2
src/features/examwork/ExamStudentImport/ExamStudentImportDialog.vue

@@ -1,7 +1,7 @@
 <template>
 <template>
   <el-dialog
   <el-dialog
     ref="dialog"
     ref="dialog"
-    :title="'导入' + '考生'"
+    title="导入考生"
     width="540px"
     width="540px"
     :visible.sync="visible"
     :visible.sync="visible"
     @close="closeDialog"
     @close="closeDialog"
@@ -15,7 +15,7 @@
     >
     >
       <el-row>
       <el-row>
         <el-form-item label="选择文件">
         <el-form-item label="选择文件">
-          <input @change="selectFile" type="file" />
+          <input @change="selectFile" type="file" accept=".xlsx,.xls" />
         </el-form-item>
         </el-form-item>
         <el-form-item label="">
         <el-form-item label="">
           {{ uploadingPercent ? ` 上传中(${uploadingPercent}%)` : "" }}
           {{ uploadingPercent ? ` 上传中(${uploadingPercent}%)` : "" }}

+ 13 - 2
src/features/examwork/ExamStudentManagement/ExamStudentManagement.vue

@@ -10,13 +10,21 @@
             <ExamSelect v-model="form.examId" />
             <ExamSelect v-model="form.examId" />
           </el-form-item>
           </el-form-item>
           <el-form-item label="场次代码">
           <el-form-item label="场次代码">
-            <ActivitySelect :examId="form.examId" v-model="form.activityId" />
+            <ActivitySelect
+              :examId="form.examId"
+              v-model="form.activityId"
+              exam-required
+            />
           </el-form-item>
           </el-form-item>
           <el-form-item label="考场名称">
           <el-form-item label="考场名称">
             <ExamRoomSelect :examId="form.examId" v-model="form.roomCode" />
             <ExamRoomSelect :examId="form.examId" v-model="form.roomCode" />
           </el-form-item>
           </el-form-item>
           <el-form-item label="科目">
           <el-form-item label="科目">
-            <CourseSelect :examId="form.examId" v-model="form.courseCode" />
+            <CourseSelect
+              :examId="form.examId"
+              v-model="form.courseCode"
+              exam-required
+            />
           </el-form-item>
           </el-form-item>
           <el-form-item label="姓名">
           <el-form-item label="姓名">
             <el-input v-model.trim="form.name"></el-input>
             <el-input v-model.trim="form.name"></el-input>
@@ -218,6 +226,9 @@ export default {
       });
       });
       this.tableData = res.data.data.records;
       this.tableData = res.data.data.records;
       this.total = res.data.data.total;
       this.total = res.data.data.total;
+      if (this.total > 0 && this.tableData.length === 0) {
+        this.handleCurrentChange(this.currentPage - 1);
+      }
     },
     },
     handleCurrentChange(val) {
     handleCurrentChange(val) {
       this.currentPage = val;
       this.currentPage = val;

+ 17 - 8
src/features/examwork/ExamStudentManagement/ExamStudentManagementDialog.vue

@@ -19,7 +19,7 @@
         </el-form-item>
         </el-form-item>
       </el-row>
       </el-row>
       <el-row>
       <el-row>
-        <el-form-item label="场次代码">
+        <el-form-item label="场次代码" prop="examActivityId">
           <ActivitySelect
           <ActivitySelect
             :examId="form.examId"
             :examId="form.examId"
             v-model="form.examActivityId"
             v-model="form.examActivityId"
@@ -28,7 +28,7 @@
         </el-form-item>
         </el-form-item>
       </el-row>
       </el-row>
       <el-row>
       <el-row>
-        <el-form-item label="考场名称">
+        <el-form-item label="考场名称" prop="roomCode">
           <ExamRoomSelect
           <ExamRoomSelect
             :examId="form.examId"
             :examId="form.examId"
             v-model="form.roomCode"
             v-model="form.roomCode"
@@ -46,23 +46,27 @@
         </el-form-item>
         </el-form-item>
       </el-row>
       </el-row>
       <el-row>
       <el-row>
-        <el-form-item label="姓名">
-          <el-input v-model.trim="form.name"></el-input>
+        <el-form-item label="姓名" prop="name">
+          <el-input v-model.trim="form.name" maxlength="30"></el-input>
         </el-form-item>
         </el-form-item>
       </el-row>
       </el-row>
       <el-row>
       <el-row>
-        <el-form-item label="证件号">
-          <el-input :disabled="isEdit" v-model.trim="form.identity"></el-input>
+        <el-form-item label="证件号" prop="identity">
+          <el-input
+            :disabled="isEdit"
+            v-model.trim="form.identity"
+            maxlength="30"
+          ></el-input>
         </el-form-item>
         </el-form-item>
       </el-row>
       </el-row>
       <el-row>
       <el-row>
         <el-form-item label="年级">
         <el-form-item label="年级">
-          <el-input v-model.trim="form.grade"></el-input>
+          <el-input v-model.trim="form.grade" maxlength="30"></el-input>
         </el-form-item>
         </el-form-item>
       </el-row>
       </el-row>
       <el-row>
       <el-row>
         <el-form-item label="教学班级">
         <el-form-item label="教学班级">
-          <el-input v-model.trim="form.classNo"></el-input>
+          <el-input v-model.trim="form.classNo" maxlength="30"></el-input>
         </el-form-item>
         </el-form-item>
       </el-row>
       </el-row>
       <el-row>
       <el-row>
@@ -111,7 +115,12 @@ export default {
         classNo: "",
         classNo: "",
       },
       },
       rules: {
       rules: {
+        examId: { required: true, message: "必填" },
+        examActivityId: { required: true, message: "必填" },
+        roomCode: { required: true, message: "必填" },
         courseCode: { required: true, message: "必填" },
         courseCode: { required: true, message: "必填" },
+        name: { required: true, message: "必填" },
+        identity: { required: true, message: "必填" },
       },
       },
       loading: false,
       loading: false,
     };
     };

+ 3 - 0
src/features/examwork/ImportExportTask/ImportExportTask.vue

@@ -135,6 +135,9 @@ export default {
       });
       });
       this.tableData = res.data.data.records;
       this.tableData = res.data.data.records;
       this.total = res.data.data.total;
       this.total = res.data.data.total;
+      if (this.total > 0 && this.tableData.length === 0) {
+        this.handleCurrentChange(this.currentPage - 1);
+      }
     },
     },
     handleCurrentChange(val) {
     handleCurrentChange(val) {
       this.currentPage = val;
       this.currentPage = val;

+ 3 - 0
src/features/examwork/InvigilateManagement/InvigilateManagement.vue

@@ -149,6 +149,9 @@ export default {
       });
       });
       this.tableData = res.data.data.records;
       this.tableData = res.data.data.records;
       this.total = res.data.data.total;
       this.total = res.data.data.total;
+      if (this.total > 0 && this.tableData.length === 0) {
+        this.handleCurrentChange(this.currentPage - 1);
+      }
     },
     },
     handleCurrentChange(val) {
     handleCurrentChange(val) {
       this.currentPage = val;
       this.currentPage = val;

+ 9 - 1
src/features/examwork/MarkResultManagement/MarkResultManagement.vue

@@ -13,10 +13,15 @@
             <ActivitySelect
             <ActivitySelect
               :examId="form.examId"
               :examId="form.examId"
               v-model="form.examActivityId"
               v-model="form.examActivityId"
+              exam-required
             />
             />
           </el-form-item>
           </el-form-item>
           <el-form-item label="科目">
           <el-form-item label="科目">
-            <CourseSelect :examId="form.examId" v-model="form.courseCode" />
+            <CourseSelect
+              :examId="form.examId"
+              v-model="form.courseCode"
+              exam-required
+            />
           </el-form-item>
           </el-form-item>
           <el-form-item label="姓名">
           <el-form-item label="姓名">
             <el-input v-model.trim="form.name"></el-input>
             <el-input v-model.trim="form.name"></el-input>
@@ -152,6 +157,9 @@ export default {
       });
       });
       this.tableData = res.data.data.records;
       this.tableData = res.data.data.records;
       this.total = res.data.data.total;
       this.total = res.data.data.total;
+      if (this.total > 0 && this.tableData.length === 0) {
+        this.handleCurrentChange(this.currentPage - 1);
+      }
     },
     },
     handleCurrentChange(val) {
     handleCurrentChange(val) {
       this.currentPage = val;
       this.currentPage = val;

+ 92 - 0
src/features/examwork/StudentManagement/HlsMedia.vue

@@ -0,0 +1,92 @@
+<template>
+  <div class="hls-media">
+    <video
+      ref="VideoMedia"
+      muted
+      controls
+      controlslist="nodownload"
+      disablePictureInPicture
+    ></video>
+  </div>
+</template>
+
+<script>
+import Hls from "hls.js";
+
+export default {
+  name: "hls-media",
+  props: {
+    url: {
+      type: String,
+    },
+  },
+  data() {
+    return {
+      player: null,
+      hls: null,
+    };
+  },
+  mounted() {
+    this.initVideo();
+  },
+  methods: {
+    initVideo() {
+      if (!this.url) return;
+      if (!Hls.isSupported()) return;
+
+      this.hls = new Hls();
+      this.hls.loadSource(this.url);
+      this.hls.attachMedia(this.$refs.VideoMedia);
+      this.player = this.$refs.VideoMedia;
+      this.player.play();
+
+      this.hls.on(Hls.Events.ERROR, (event, data) => {
+        if (!data.fatal) {
+          switch (data.type) {
+            case Hls.ErrorTypes.NETWORK_ERROR:
+              console.log("网络问题,准备重试!");
+              this.hls.startLoad();
+              break;
+            case Hls.ErrorTypes.MEDIA_ERROR:
+              console.log("媒体问题,准备重试!");
+              this.hls.recoverMediaError();
+              break;
+            default:
+              console.log("未知问题,无法播放!");
+              this.$emit("error");
+              this.hls.destroy();
+              break;
+          }
+        }
+      });
+    },
+    playPlayer() {
+      this.player.play();
+    },
+    pausePlayer() {
+      this.player.pause();
+    },
+    destroyPlayer() {
+      if (!this.player) return;
+      this.player.pause();
+      this.player = null;
+      this.hls.destroy();
+      this.hls = null;
+    },
+  },
+  beforeDestroy() {
+    this.destroyPlayer();
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.hls-media {
+  height: 100%;
+  video {
+    display: block;
+    width: 100%;
+    height: 100%;
+  }
+}
+</style>

+ 3 - 0
src/features/examwork/StudentManagement/StudentManagement.vue

@@ -165,6 +165,9 @@ export default {
       //       "https://ecs-test-static.qmth.com.cn/org_logo/0/1597046412749.png")
       //       "https://ecs-test-static.qmth.com.cn/org_logo/0/1597046412749.png")
       // );
       // );
       this.total = res.data.data.total;
       this.total = res.data.data.total;
+      if (this.total > 0 && this.tableData.length === 0) {
+        this.handleCurrentChange(this.currentPage - 1);
+      }
     },
     },
     handleCurrentChange(val) {
     handleCurrentChange(val) {
       this.currentPage = val;
       this.currentPage = val;

+ 83 - 50
src/features/examwork/StudentManagement/StudentManagementDialog.vue

@@ -1,56 +1,75 @@
 <template>
 <template>
-  <el-dialog
-    ref="dialog"
-    title="考试记录"
-    width="600px"
-    :visible.sync="visible"
-    @close="closeDialog"
-  >
-    <el-form
-      :model="form"
-      ref="form"
-      :rules="rules"
-      label-position="right"
-      label-width="120px"
-      inline
+  <div>
+    <el-dialog
+      ref="dialog"
+      title="考试记录"
+      width="760px"
+      :visible.sync="visible"
+      @close="closeDialog"
     >
     >
-      <el-form-item label="批次名称" prop="examId">
-        <ExamSelect v-model="form.examId" />
-      </el-form-item>
-      <el-button type="primary" @click="handleCurrentChange(0)">查询</el-button>
-    </el-form>
+      <el-form
+        :model="form"
+        ref="form"
+        :rules="rules"
+        label-position="right"
+        label-width="120px"
+        inline
+      >
+        <el-form-item label="批次名称" prop="examId">
+          <ExamSelect v-model="form.examId" />
+        </el-form-item>
+        <el-button type="primary" @click="handleCurrentChange(0)"
+          >查询</el-button
+        >
+      </el-form>
 
 
-    <el-table :data="tableData" stripe style="width: 100%;">
-      <el-table-column width="100" label="姓名">
-        <span slot-scope="scope">{{ scope.row.name }}</span>
-      </el-table-column>
-      <el-table-column width="180" label="证件号">
-        <span slot-scope="scope">{{ scope.row.identity }}</span>
-      </el-table-column>
-      <el-table-column label="批次名称">
-        <span slot-scope="scope">{{ scope.row.examName }}</span>
-      </el-table-column>
-      <el-table-column width="100" label="课程">
-        <span slot-scope="scope">{{ scope.row.courseName }}</span>
-      </el-table-column>
-      <el-table-column width="100" label="状态">
-        <span slot-scope="scope">{{ scope.row.status }}</span>
-      </el-table-column>
-    </el-table>
-    <div class="page float-right">
-      <el-pagination
-        background
-        @current-change="handleCurrentChange"
-        :current-page="currentPage"
-        :page-size="pageSize"
-        :page-sizes="[10, 20, 50, 100, 200, 300]"
-        @size-change="handleSizeChange"
-        layout="total, sizes, prev, pager, next, jumper"
-        :total="total"
-      />
-    </div>
-    <div class="my-2"></div>
-  </el-dialog>
+      <el-table :data="tableData" stripe style="width: 100%;">
+        <el-table-column width="100" label="姓名">
+          <span slot-scope="scope">{{ scope.row.name }}</span>
+        </el-table-column>
+        <el-table-column width="180" label="证件号">
+          <span slot-scope="scope">{{ scope.row.identity }}</span>
+        </el-table-column>
+        <el-table-column label="批次名称">
+          <span slot-scope="scope">{{ scope.row.examName }}</span>
+        </el-table-column>
+        <el-table-column width="100" label="课程">
+          <span slot-scope="scope">{{ scope.row.courseName }}</span>
+        </el-table-column>
+        <el-table-column width="100" label="状态">
+          <span slot-scope="scope">{{
+            scope.row.status | examRecordStatusFilter
+          }}</span>
+        </el-table-column>
+        <el-table-column width="100" label="操作">
+          <template slot-scope="scope">
+            <el-button
+              v-if="scope.row.monitorRecord && scope.row.monitorRecord.length"
+              size="mini"
+              type="primary"
+              plain
+              @click="openMonitorRecord(scope.row)"
+            >
+              监考回放
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="page float-right">
+        <el-pagination
+          background
+          @current-change="handleCurrentChange"
+          :current-page="currentPage"
+          :page-size="pageSize"
+          :page-sizes="[10, 20, 50, 100, 200, 300]"
+          @size-change="handleSizeChange"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="total"
+        />
+      </div>
+      <div class="my-2"></div>
+    </el-dialog>
+  </div>
 </template>
 </template>
 
 
 <script>
 <script>
@@ -65,6 +84,7 @@ export default {
       this.tableData = [];
       this.tableData = [];
       this.pageSize = 10;
       this.pageSize = 10;
       this.total = 0;
       this.total = 0;
+      this.form.examId = "";
     },
     },
   },
   },
   data() {
   data() {
@@ -114,6 +134,19 @@ export default {
       });
       });
       this.tableData = res.data.data.records;
       this.tableData = res.data.data.records;
       this.total = res.data.data.total;
       this.total = res.data.data.total;
+      if (this.total > 0 && this.tableData.length === 0) {
+        this.handleCurrentChange(this.currentPage - 1);
+      }
+    },
+    // monitor record
+    openMonitorRecord(row) {
+      console.log(row);
+      window.sessionStorage.setItem("record", JSON.stringify(row));
+      window.open(
+        this.$router.resolve({
+          name: "StudentMonitorRecord",
+        }).href
+      );
     },
     },
   },
   },
 };
 };

+ 287 - 0
src/features/examwork/StudentManagement/StudentMonitorRecord.vue

@@ -0,0 +1,287 @@
+<template>
+  <div class="student-monitor-record">
+    <div class="record-header">
+      <div class="header-info">
+        <span class="header-info-name">
+          <i class="icon icon-user-act"></i>
+          {{ info.name }}
+        </span>
+        <span class="header-info-item"> 证件号:{{ info.identity }} </span>
+        <span class="header-info-item"> 课程:{{ info.courseName }} </span>
+        <span class="header-info-item"> 批次名称:{{ info.examName }} </span>
+        <span class="header-info-item">
+          考试时间:{{ info.firstStartTime | datetimeFilter }}
+        </span>
+      </div>
+      <div>
+        <el-button
+          v-for="source in info.monitorRecord"
+          :key="source.videoSource"
+          :type="
+            curSource.videoSource === source.videoSource ? 'primary' : 'info'
+          "
+          @click="switchVideo(source)"
+          >{{ videoSourceInfo[source.monitorKey] }}</el-button
+        >
+      </div>
+    </div>
+    <div class="record-body">
+      <div class="record-video">
+        <HlsMedia
+          v-if="curSource.videoUrl"
+          :url="curSource.videoUrl"
+          :key="curSource.videoUrl"
+          @error="videoError"
+        />
+        <div v-else class="record-video-none">
+          <div>
+            <i class="el-icon-video-camera-solid"></i>
+            <p>暂无视频</p>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="record-footer">
+      <i class="icon icon-error"></i>
+      <span
+        >提示:无法正常播放,请<i
+          class="color-primary record-qr"
+          @click="modalIsShow = true"
+          >扫描二维码</i
+        >。</span
+      >
+    </div>
+    <!-- qr-code -->
+    <el-dialog
+      custom-class="record-code-dialog"
+      :visible.sync="modalIsShow"
+      width="280px"
+      append-to-body
+      destroy-on-close
+    >
+      <div class="record-close" @click="modalIsShow = false">
+        <i class="el-icon-close"></i>
+      </div>
+      <div class="record-qrcode">
+        <vue-qrcode
+          :value="curSource.videoUrl"
+          :options="{ width: 200, margin: 0 }"
+          tag="img"
+        ></vue-qrcode>
+        <p class="record-tips">请扫描二维码</p>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import VueQrcode from "@chenfengyuan/vue-qrcode";
+import HlsMedia from "./HlsMedia";
+
+export default {
+  name: "StudentMonitorRecord",
+  components: { VueQrcode, HlsMedia },
+  data() {
+    return {
+      info: {
+        name: "",
+        identity: "",
+        examName: "",
+        courseName: "",
+        monitorRecord: [
+          {
+            videoSource: "",
+            videoUrl: "",
+          },
+        ],
+      },
+      videoSourceInfo: {
+        CLIENT_CAMERA: "电脑摄像头",
+        CLIENT_SCREEN: "电脑录屏",
+        MOBILE_FIRST: "手机主机位",
+        MOBILE_SECOND: "手机辅机位",
+      },
+      curSource: { videoSource: "", videoUrl: "" },
+      modalIsShow: false,
+    };
+  },
+  created() {
+    const recordStr = window.sessionStorage.getItem("record");
+    if (!recordStr) {
+      this.$message.error("数据丢失,请关闭页面!");
+      return;
+    }
+    this.info = JSON.parse(recordStr);
+    this.info.monitorRecord = this.info.monitorRecord.map((item) => {
+      return {
+        ...item,
+        monitorKey: this.getVideoMonitorKey(item.videoSource),
+      };
+    });
+    this.switchVideo(this.info.monitorRecord[0]);
+  },
+  methods: {
+    getVideoMonitorKey(videoSource) {
+      return Object.keys(this.videoSourceInfo).find((key) =>
+        videoSource.includes(key.toLowerCase())
+      );
+    },
+    switchVideo(source) {
+      this.curSource = source;
+    },
+    videoError() {
+      this.$message({
+        type: "error",
+        message: "视频解析错误,您可以尝试扫描二维码,通过手机观看。",
+        showClose: true,
+        duration: 10 * 1000,
+      });
+    },
+    videoLoad(event) {
+      console.log(event);
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.student-monitor-record {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  z-index: auto;
+  top: 0;
+  left: 0;
+  background-color: #f0f4f9;
+  color: #626a82;
+  font-weight: bold;
+}
+.record-header {
+  position: absolute;
+  height: 56px;
+  width: 100%;
+  left: 0;
+  top: 0;
+  background-color: #fff;
+  z-index: 9;
+  padding: 12px 20px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+
+  .header-info-name {
+    font-size: 18px;
+    vertical-align: middle;
+  }
+
+  .icon-user-act {
+    margin-top: -3px;
+    margin-right: 8px;
+  }
+
+  .header-info-item {
+    padding: 0 12px;
+    vertical-align: middle;
+
+    &:not(:last-child) {
+      border-right: 1px solid #abb8c9;
+    }
+  }
+
+  .el-button--info {
+    border-color: #f0f4f9;
+    background: #f0f4f9;
+    border-radius: 6px;
+    color: #273a62;
+
+    &:hover {
+      border-color: #1886fe;
+      background: #1886fe;
+      color: #fff;
+    }
+  }
+}
+.record-body {
+  padding: 76px 20px 70px;
+  height: 100%;
+}
+.record-video {
+  background: #000000;
+  border-radius: 6px;
+  height: 100%;
+  overflow: hidden;
+
+  &-none {
+    height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: space-around;
+    text-align: center;
+    color: #333;
+
+    i {
+      font-size: 80px;
+    }
+  }
+}
+.record-footer {
+  position: absolute;
+  width: 100%;
+  left: 0;
+  bottom: 34px;
+  text-align: center;
+  .icon {
+    margin-right: 10px;
+    margin-top: -1px;
+  }
+
+  .record-qr {
+    color: #1886fe;
+    text-decoration: underline;
+    font-style: normal;
+    cursor: pointer;
+    &:hover {
+      color: mix(#333, #1886fe, 20%);
+    }
+  }
+}
+</style>
+<style lang="scss">
+.record-code-dialog {
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  transform: translate(-50%, -50%);
+  margin: 0 !important;
+
+  .el-dialog__header,
+  .el-dialog__footer {
+    display: none;
+  }
+  .el-dialog__body {
+    position: relative;
+    padding: 40px 40px 30px 40px;
+    text-align: center;
+    font-size: 12px;
+  }
+  .record-close {
+    position: absolute;
+    height: 20px;
+    width: 20px;
+    font-size: 16px;
+    line-height: 20px;
+    top: 8px;
+    right: 8px;
+    z-index: 9;
+    color: #959fb1;
+    cursor: pointer;
+
+    &:hover {
+      color: #fe5863;
+    }
+  }
+  .record-tips {
+    margin: 20px 0 0;
+  }
+}
+</style>

+ 2 - 2
src/features/invigilation/ExamInvigilation/ExamInvigilation.vue

@@ -235,8 +235,8 @@
     <div class="invigilation-list">
     <div class="invigilation-list">
       <h3>实时监控台</h3>
       <h3>实时监控台</h3>
       <invigilation-student
       <invigilation-student
-        v-for="(item, index) in students"
-        :key="index"
+        v-for="item in students"
+        :key="item.examStudentId"
         :data="item"
         :data="item"
       ></invigilation-student>
       ></invigilation-student>
     </div>
     </div>

+ 18 - 7
src/features/invigilation/RealtimeMonitoring/RealtimeMonitoring.vue

@@ -94,16 +94,16 @@
               ></el-option>
               ></el-option>
             </el-select>
             </el-select>
           </el-form-item>
           </el-form-item>
-          <el-form-item>
+          <el-form-item v-for="source in viewingAngles" :key="source.code">
             <el-select
             <el-select
-              v-model="filter.monitorStatusSource"
-              placeholder="通讯故障"
+              v-model="monitorStatusFilter[source.filterParam]"
+              :placeholder="`${source.name}通讯故障`"
               clearable
               clearable
             >
             >
               <el-option
               <el-option
-                v-for="(val, key) in BOOLEAN_TYPE"
+                v-for="(val, key) in MONITOR_STATUS_TYPE"
                 :key="key"
                 :key="key"
-                :value="key * 1"
+                :value="key"
                 :label="val"
                 :label="val"
               ></el-option>
               ></el-option>
             </el-select>
             </el-select>
@@ -311,12 +311,12 @@ import SummaryLine from "../common/SummaryLine";
 import handleRollupDialog from "./handleRollupDialog";
 import handleRollupDialog from "./handleRollupDialog";
 import TextClock from "../common/TextClock";
 import TextClock from "../common/TextClock";
 import {
 import {
-  BOOLEAN_TYPE,
   VIDEO_SOURCE_TYPE,
   VIDEO_SOURCE_TYPE,
   BOOLEAN_INVERSE_TYPE,
   BOOLEAN_INVERSE_TYPE,
   STUDENT_ONLINE_STATUS,
   STUDENT_ONLINE_STATUS,
   CLIENT_WEBSOCKET_STATUS,
   CLIENT_WEBSOCKET_STATUS,
   MONITOR_STATUS_SOURCE,
   MONITOR_STATUS_SOURCE,
+  MONITOR_STATUS_TYPE,
 } from "@/constant/constants";
 } from "@/constant/constants";
 import { mapState, mapMutations, mapActions } from "vuex";
 import { mapState, mapMutations, mapActions } from "vuex";
 
 
@@ -345,12 +345,13 @@ export default {
         maxWarningCount: undefined,
         maxWarningCount: undefined,
         minWarningCount: undefined,
         minWarningCount: undefined,
       },
       },
-      BOOLEAN_TYPE,
+      monitorStatusFilter: {},
       VIDEO_SOURCE_TYPE,
       VIDEO_SOURCE_TYPE,
       BOOLEAN_INVERSE_TYPE,
       BOOLEAN_INVERSE_TYPE,
       STUDENT_ONLINE_STATUS,
       STUDENT_ONLINE_STATUS,
       CLIENT_WEBSOCKET_STATUS,
       CLIENT_WEBSOCKET_STATUS,
       MONITOR_STATUS_SOURCE,
       MONITOR_STATUS_SOURCE,
+      MONITOR_STATUS_TYPE,
       hasNewWarning: false,
       hasNewWarning: false,
       loopRunning: false,
       loopRunning: false,
       loopSetTs: [],
       loopSetTs: [],
@@ -417,16 +418,25 @@ export default {
       if (!examBatch) return;
       if (!examBatch) return;
       this.filter.examId = examBatch.id;
       this.filter.examId = examBatch.id;
       this.curExamBatch = examBatch;
       this.curExamBatch = examBatch;
+      const monitorStatusFilter = {};
       if (examBatch.monitorVideoSource) {
       if (examBatch.monitorVideoSource) {
         this.viewingAngles = examBatch.monitorVideoSource
         this.viewingAngles = examBatch.monitorVideoSource
           .split(",")
           .split(",")
           .map((item) => {
           .map((item) => {
+            const filterParam = this.videoSourceStatusParams[item].replace(
+              "Source",
+              ""
+            );
+
+            monitorStatusFilter[filterParam] = null;
             return {
             return {
               code: item,
               code: item,
               name: this.VIDEO_SOURCE_TYPE[item],
               name: this.VIDEO_SOURCE_TYPE[item],
               param: this.videoSourceStatusParams[item],
               param: this.videoSourceStatusParams[item],
+              filterParam,
             };
             };
           });
           });
+        this.monitorStatusFilter = monitorStatusFilter;
       } else {
       } else {
         this.viewingAngles = [];
         this.viewingAngles = [];
       }
       }
@@ -466,6 +476,7 @@ export default {
     async getList() {
     async getList() {
       const datas = {
       const datas = {
         ...this.filter,
         ...this.filter,
+        ...this.monitorStatusFilter,
         pageNumber: this.current,
         pageNumber: this.current,
         pageSize: this.size,
         pageSize: this.size,
       };
       };

+ 16 - 3
src/features/invigilation/RealtimeMonitoring/VideoCommunication.vue

@@ -107,7 +107,11 @@
 </template>
 </template>
 
 
 <script>
 <script>
-import { createClient, createStream } from "@/plugins/trtc";
+import {
+  checkSystemRequirements,
+  createClient,
+  createStream,
+} from "@/plugins/trtc";
 import {
 import {
   communicationList,
   communicationList,
   communicationCalling,
   communicationCalling,
@@ -200,16 +204,17 @@ export default {
       this.userMonitor = res.data.data;
       this.userMonitor = res.data.data;
       this.client = createClient({
       this.client = createClient({
         mode: "live",
         mode: "live",
-        sdkAppId: this.userMonitor.appId,
+        sdkAppId: this.userMonitor.appId * 1,
         userId: this.userMonitor.monitorUserId,
         userId: this.userMonitor.monitorUserId,
         userSig: this.userMonitor.monitorUserSig,
         userSig: this.userMonitor.monitorUserSig,
+        useStringRoomId: true,
       });
       });
     },
     },
     async getLocalMedia(isVideo) {
     async getLocalMedia(isVideo) {
       const localStream = createStream({
       const localStream = createStream({
         userId: this.userMonitor.monitorUserId,
         userId: this.userMonitor.monitorUserId,
         audio: true,
         audio: true,
-        video: isVideo,
+        video: !!isVideo,
       });
       });
       const errorTips = {
       const errorTips = {
         NotFoundError: "找不到硬件设备,请确保硬件设备已经正常插入。",
         NotFoundError: "找不到硬件设备,请确保硬件设备已经正常插入。",
@@ -218,6 +223,7 @@ export default {
           "暂时无法访问摄像头/麦克风,请确保当前没有其他应用请求访问摄像头/麦克风,并重试。",
           "暂时无法访问摄像头/麦克风,请确保当前没有其他应用请求访问摄像头/麦克风,并重试。",
         OverConstrainedError: "设备异常",
         OverConstrainedError: "设备异常",
         AbortError: "设备异常",
         AbortError: "设备异常",
+        RtcError: "无应答客户端",
       };
       };
 
 
       let initLocalStreamResult = true;
       let initLocalStreamResult = true;
@@ -230,6 +236,13 @@ export default {
       return initLocalStreamResult && localStream;
       return initLocalStreamResult && localStream;
     },
     },
     async answer(student, isVideo) {
     async answer(student, isVideo) {
+      const result = await checkSystemRequirements().catch(() => {
+        this.$message.error(
+          `您的浏览器不支持当前音视频通讯版本。建议使用最新版的chrome浏览器!`
+        );
+      });
+      if (!result) return;
+
       if (this.holding) return;
       if (this.holding) return;
       this.holding = true;
       this.holding = true;
 
 

+ 15 - 3
src/features/invigilation/RealtimeMonitoring/WarningDetail.vue

@@ -295,7 +295,11 @@
 </template>
 </template>
 
 
 <script>
 <script>
-import { createClient, createStream } from "@/plugins/trtc";
+import {
+  checkSystemRequirements,
+  createClient,
+  createStream,
+} from "@/plugins/trtc";
 import {
 import {
   invigilateDetail,
   invigilateDetail,
   invigilateFinish,
   invigilateFinish,
@@ -628,16 +632,17 @@ export default {
       this.userMonitor = res.data.data;
       this.userMonitor = res.data.data;
       this.client = createClient({
       this.client = createClient({
         mode: "live",
         mode: "live",
-        sdkAppId: this.userMonitor.appId,
+        sdkAppId: this.userMonitor.appId * 1,
         userId: this.userMonitor.monitorUserId,
         userId: this.userMonitor.monitorUserId,
         userSig: this.userMonitor.monitorUserSig,
         userSig: this.userMonitor.monitorUserSig,
+        useStringRoomId: true,
       });
       });
     },
     },
     async getLocalMedia(isVideo) {
     async getLocalMedia(isVideo) {
       const localStream = createStream({
       const localStream = createStream({
         userId: this.userMonitor.monitorUserId,
         userId: this.userMonitor.monitorUserId,
         audio: true,
         audio: true,
-        video: isVideo,
+        video: !!isVideo,
       });
       });
       const errorTips = {
       const errorTips = {
         NotFoundError: "找不到硬件设备,请确保硬件设备正常。",
         NotFoundError: "找不到硬件设备,请确保硬件设备正常。",
@@ -658,6 +663,13 @@ export default {
       return initLocalStreamResult && localStream;
       return initLocalStreamResult && localStream;
     },
     },
     async answer(isVideo) {
     async answer(isVideo) {
+      const result = await checkSystemRequirements().catch(() => {
+        this.$message.error(
+          `您的浏览器不支持当前音视频通讯版本。建议使用最新版的chrome浏览器!`
+        );
+      });
+      if (!result) return;
+
       // 客户端两路视频公用一个userId:
       // 客户端两路视频公用一个userId:
       // main:有音频,有视频
       // main:有音频,有视频
       // auxiliary:无音频,有视频
       // auxiliary:无音频,有视频

+ 0 - 1
src/features/invigilation/StudentLogManage/StudentLogManage.vue

@@ -12,7 +12,6 @@
               v-model="filter.examId"
               v-model="filter.examId"
               placeholder="请选择批次"
               placeholder="请选择批次"
               @change="examChange"
               @change="examChange"
-              clearable
             >
             >
               <el-option
               <el-option
                 v-for="item in examBatchs"
                 v-for="item in examBatchs"

+ 13 - 4
src/features/system/OrgManagement/OrgManagementDialog.vue

@@ -2,7 +2,7 @@
   <el-dialog
   <el-dialog
     ref="dialog"
     ref="dialog"
     :title="(isEdit ? '编辑' : '新增') + '机构'"
     :title="(isEdit ? '编辑' : '新增') + '机构'"
-    width="480px"
+    width="560px"
     :visible.sync="visible"
     :visible.sync="visible"
     @close="closeDialog"
     @close="closeDialog"
   >
   >
@@ -11,7 +11,7 @@
       ref="form"
       ref="form"
       :rules="rules"
       :rules="rules"
       label-position="right"
       label-position="right"
-      label-width="140px"
+      label-width="130px"
     >
     >
       <el-row>
       <el-row>
         <el-form-item label="机构名称" prop="name">
         <el-form-item label="机构名称" prop="name">
@@ -119,8 +119,17 @@ export default {
       visible: false,
       visible: false,
       form: {},
       form: {},
       rules: {
       rules: {
-        name: { required: true, message: "必填" },
-        code: { required: true, message: "必填" },
+        name: {
+          required: true,
+          max: 100,
+          message: "机构名称不能超过100个字符",
+          trigger: "change",
+        },
+        code: {
+          required: true,
+          pattern: new RegExp(`^[a-zA-Z0-9_-]{3,100}$`),
+          message: `机构编码只能由数字、字母、短横线和下划线组成,长度3-100个字符`,
+        },
       },
       },
       loading: false,
       loading: false,
     };
     };

+ 3 - 0
src/features/system/UserManagement/UserManagement.vue

@@ -166,6 +166,9 @@ export default {
       });
       });
       this.tableData = res.data.data.records;
       this.tableData = res.data.data.records;
       this.total = res.data.data.total;
       this.total = res.data.data.total;
+      if (this.total > 0 && this.tableData.length === 0) {
+        this.handleCurrentChange(this.currentPage - 1);
+      }
     },
     },
     handleCurrentChange(val) {
     handleCurrentChange(val) {
       this.currentPage = val;
       this.currentPage = val;

+ 90 - 5
src/features/system/UserManagement/UserManagementDialog.vue

@@ -41,10 +41,23 @@
           <el-input
           <el-input
             class="pull_length"
             class="pull_length"
             v-model="form.password"
             v-model="form.password"
+            type="password"
+            autocomplete="new-password"
             placeholder="密码"
             placeholder="密码"
           />
           />
         </el-form-item>
         </el-form-item>
       </el-row>
       </el-row>
+      <el-row>
+        <el-form-item label="再次输入密码" prop="passwordConfirm">
+          <el-input
+            class="pull_length"
+            v-model="form.passwordConfirm"
+            type="password"
+            autocomplete="new-password"
+            placeholder="再次输入密码"
+          />
+        </el-form-item>
+      </el-row>
       <el-row>
       <el-row>
         <el-form-item label="角色" prop="roleCode">
         <el-form-item label="角色" prop="roleCode">
           <RoleSelect v-model="form.roleCode" :multiple="true" />
           <RoleSelect v-model="form.roleCode" :multiple="true" />
@@ -92,11 +105,78 @@ export default {
       return this.user.id;
       return this.user.id;
     },
     },
     rules() {
     rules() {
+      const that = this;
       return {
       return {
-        loginName: [{ required: true, message: "登录名必填" }],
-        name: [{ required: true, message: "姓名必填" }],
-        password: [{ required: !this.isEdit, message: "密码必填" }],
+        loginName: [
+          {
+            required: true,
+            message: "登录名必填",
+            whitespace: true,
+          },
+          { min: 1, max: 20, message: "最长为20个字符" },
+        ],
+        name: [
+          { required: true, message: "姓名必填", whitespace: true },
+          { min: 1, max: 20, message: "最长为20个字符" },
+        ],
+        password: [
+          { required: !this.isEdit, message: "密码必填" },
+          {
+            required: !this.isEdit,
+            min: 1,
+            max: 16,
+            message: "最长为16个字符",
+          },
+          {
+            required: !this.isEdit,
+            type: "string",
+            pattern: /^[a-z0-9_]+$/i,
+            message: "仅支持大小写英文字母、数字和下划线",
+          },
+        ],
+        passwordConfirm: [
+          { required: !this.isEdit, message: "密码必填" },
+          {
+            required: !this.isEdit,
+            type: "string",
+            pattern: /^[a-z0-9_]+$/i,
+            message: "仅支持大小写英文字母、数字和下划线",
+          },
+          {
+            type: "string",
+            trigger: "blur",
+            validator: function (rule, value, callback) {
+              console.log(that.isEdit, value, that.form.password);
+              if (
+                that.isEdit &&
+                (that.form.password === undefined ||
+                  that.form.password.trim().length === 0) &&
+                (value === undefined || value.trim().length === 0)
+              ) {
+                console.log("object");
+                callback();
+                return;
+              }
+              console.log("pos");
+              if (value !== that.form.password) {
+                callback(new Error("与密码不一致"));
+              } else {
+                callback();
+              }
+            },
+            message: "与密码不一致",
+          },
+        ],
         roleCode: [{ required: true, message: "角色必填" }],
         roleCode: [{ required: true, message: "角色必填" }],
+        mobileNumber: [
+          { required: false, pattern: /^\d*$/i, message: "手机号必须是数字" },
+          {
+            required: false,
+            min: 0,
+            max: 11,
+            message: "最长为11个数字",
+          },
+        ],
       };
       };
     },
     },
   },
   },
@@ -109,7 +189,13 @@ export default {
   },
   },
   watch: {
   watch: {
     user(val) {
     user(val) {
+      this.resetForm(val);
+    },
+  },
+  methods: {
+    resetForm(val) {
       let tmp = { ...val };
       let tmp = { ...val };
+      // console.log({ user: this.user, tmp, val });
       if (!tmp.id) {
       if (!tmp.id) {
         tmp = {
         tmp = {
           orgId: this.$store.state.user.orgId,
           orgId: this.$store.state.user.orgId,
@@ -123,9 +209,8 @@ export default {
       }
       }
       this.form = tmp;
       this.form = tmp;
     },
     },
-  },
-  methods: {
     openDialog() {
     openDialog() {
+      this.resetForm(this.user);
       this.visible = true;
       this.visible = true;
     },
     },
     closeDialog() {
     closeDialog() {

+ 5 - 1
src/filters/index.js

@@ -1,6 +1,6 @@
 import Vue from "vue";
 import Vue from "vue";
 import { dateFormatForAPI } from "@/utils/utils";
 import { dateFormatForAPI } from "@/utils/utils";
-import { APPROVE_STATUS } from "@/constant/constants";
+import { APPROVE_STATUS, EXAM_RECORD_STATUS } from "@/constant/constants";
 
 
 Vue.filter("booleanYesNoFilter", function (val) {
 Vue.filter("booleanYesNoFilter", function (val) {
   if (val === null) return "无";
   if (val === null) return "无";
@@ -60,3 +60,7 @@ Vue.filter("zeroOneApproveStatusFilter", function (val) {
   if (val === null) return "";
   if (val === null) return "";
   return APPROVE_STATUS[val];
   return APPROVE_STATUS[val];
 });
 });
+Vue.filter("examRecordStatusFilter", function (val) {
+  if (val === null) return "";
+  return EXAM_RECORD_STATUS[val];
+});

+ 21 - 2
src/plugins/axiosApp.js

@@ -7,8 +7,9 @@ import { notifyInvalidTokenThrottled } from "./axiosNotice";
 import { getToken, removeToken, getSessionId } from "../auth/auth";
 import { getToken, removeToken, getSessionId } from "../auth/auth";
 import axiosRetry from "axios-retry";
 import axiosRetry from "axios-retry";
 import { PLATFORM, DEVICE_ID } from "@/constant/constants";
 import { PLATFORM, DEVICE_ID } from "@/constant/constants";
-import { Notification } from "element-ui";
+import { Notification, MessageBox } from "element-ui";
 import CryptoJS from "crypto-js";
 import CryptoJS from "crypto-js";
+import router from "@/router";
 
 
 // console.log(btoa(CryptoJS.SHA1("pWWQ0qyaXL8QHni4ig9YiWYTKr8UVQd4")));
 // console.log(btoa(CryptoJS.SHA1("pWWQ0qyaXL8QHni4ig9YiWYTKr8UVQd4")));
 // console.log(
 // console.log(
@@ -78,7 +79,7 @@ _axiosApp.interceptors.request.use(
     return Promise.reject(error);
     return Promise.reject(error);
   }
   }
 );
 );
-
+let unauthMsgBoxIsShow = false;
 // Add a response interceptor
 // Add a response interceptor
 _axiosApp.interceptors.response.use(
 _axiosApp.interceptors.response.use(
   (response) => {
   (response) => {
@@ -127,6 +128,24 @@ _axiosApp.interceptors.response.use(
 
 
     if (status != 200) {
     if (status != 200) {
       const data = error.response.data;
       const data = error.response.data;
+
+      if (status === 500 && data && data.code === 500012) {
+        if (unauthMsgBoxIsShow) return error;
+        unauthMsgBoxIsShow = true;
+        MessageBox.confirm("身份验证失效,请重新登录!", "错误提示", {
+          type: "warning",
+          closeOnClickModal: false,
+          closeOnPressEscape: false,
+          showClose: false,
+          callback: (action) => {
+            unauthMsgBoxIsShow = false;
+            if (action !== "confirm") return;
+            router.push({ name: "Login" });
+          },
+        });
+        return Promise.reject(error);
+      }
+
       if (data && data.message) {
       if (data && data.message) {
         if (showErrorMessage) {
         if (showErrorMessage) {
           Notification({
           Notification({

+ 7 - 0
src/plugins/trtc.js

@@ -2,6 +2,13 @@ import TRTC from "trtc-js-sdk";
 // 输出INFO以上日志等级
 // 输出INFO以上日志等级
 TRTC.Logger.setLogLevel(TRTC.Logger.LogLevel.WARN);
 TRTC.Logger.setLogLevel(TRTC.Logger.LogLevel.WARN);
 
 
+export const checkSystemRequirements = async () => {
+  const checkResult = await TRTC.checkSystemRequirements();
+  console.log(checkResult);
+  if (!checkResult.result) return Promise.reject(checkResult.detail);
+  return Promise.resolve(true);
+};
+
 export const createClient = (options) => {
 export const createClient = (options) => {
   return TRTC.createClient(options);
   return TRTC.createClient(options);
 };
 };

+ 8 - 1
src/router/index.js

@@ -178,6 +178,14 @@ const routes = [
       },
       },
     ],
     ],
   },
   },
+  {
+    path: "/exam/student-monitor-record",
+    name: "StudentMonitorRecord",
+    component: () =>
+      import(
+        /* webpackChunkName: "record" */ "../features/examwork/StudentManagement/StudentMonitorRecord.vue"
+      ),
+  },
   {
   {
     path: "/invigilation",
     path: "/invigilation",
     name: "Invigilation",
     name: "Invigilation",
@@ -211,7 +219,6 @@ router.beforeEach((to, from, next) => {
   if (to.path) {
   if (to.path) {
     window._hmt.push(["_trackPageview", to.fullPath]);
     window._hmt.push(["_trackPageview", to.fullPath]);
   }
   }
-
   const token = getToken();
   const token = getToken();
   if (isNil(token) && to.path.includes("/login") === false) {
   if (isNil(token) && to.path.includes("/login") === false) {
     router.push("/login?redirectTo=" + encodeURI(to.fullPath));
     router.push("/login?redirectTo=" + encodeURI(to.fullPath));

+ 6 - 1
src/store/modules/user.js

@@ -8,6 +8,7 @@ const user = {
   state: {
   state: {
     loginName: "",
     loginName: "",
     name: "",
     name: "",
+    orgInfo: {},
   },
   },
 
 
   mutations: {
   mutations: {
@@ -22,7 +23,11 @@ const user = {
     [LOGIN_BY_USERNAME]({ commit }, userInfo) {
     [LOGIN_BY_USERNAME]({ commit }, userInfo) {
       return loginByUsername(userInfo).then((response) => {
       return loginByUsername(userInfo).then((response) => {
         const data = response.data.data;
         const data = response.data.data;
-        commit("SET_USER", { ...data.account, roleCodes: data.roleCodes });
+        commit("SET_USER", {
+          ...data.account,
+          roleCodes: data.roleCodes,
+          orgInfo: data.orgInfo,
+        });
         setToken(data.accessToken);
         setToken(data.accessToken);
         setSessionId(data.sessionId);
         setSessionId(data.sessionId);
       });
       });

+ 2 - 2
src/styles/global.css

@@ -109,6 +109,6 @@ body {
   background-color: #ffffff;
   background-color: #ffffff;
 }
 }
 
 
-input[type=file] {
+input[type="file"] {
   overflow: hidden;
   overflow: hidden;
-}
+}

+ 3 - 0
src/styles/icons.scss

@@ -68,6 +68,9 @@
   &-logout {
   &-logout {
     background-image: url(../assets/icon-logout.png);
     background-image: url(../assets/icon-logout.png);
   }
   }
+  &-error {
+    background-image: url(../assets/icon-error.png);
+  }
 
 
   &-reexam {
   &-reexam {
     background-image: url(../assets/icon-reexam.png);
     background-image: url(../assets/icon-reexam.png);

+ 4 - 0
src/utils/utils.js

@@ -38,6 +38,10 @@ export function AESString(content) {
   return encrypted.toString();
   return encrypted.toString();
 }
 }
 
 
+export function encodePassword(content) {
+  return window.btoa(content);
+}
+
 export function object2QueryString(obj) {
 export function object2QueryString(obj) {
   return queryString.stringify(obj);
   return queryString.stringify(obj);
 }
 }

+ 7 - 0
src/views/Layout/Layout.vue

@@ -39,6 +39,13 @@ export default {
     };
     };
   },
   },
   created() {
   created() {
+    // 禁止通过非session共享的方式打开系统页面
+    if (!this.$store.state.user || !this.$store.state.user["id"]) {
+      this.$message.error("请重新登录!");
+      this.$router.replace({ name: "Login" });
+      return;
+    }
+
     this.getMenu();
     this.getMenu();
   },
   },
   methods: {
   methods: {

+ 59 - 5
yarn.lock

@@ -1308,6 +1308,13 @@
     lodash "^4.17.13"
     lodash "^4.17.13"
     to-fast-properties "^2.0.0"
     to-fast-properties "^2.0.0"
 
 
+"@chenfengyuan/vue-qrcode@1.0.2":
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/@chenfengyuan/vue-qrcode/-/vue-qrcode-1.0.2.tgz#37d71902e166e1ae58176bd6cb9c40905c1b0949"
+  integrity sha512-hwy1d4YMJAyEh+V7dLPG8eAKACRvugzSB4ylwb6QNqo84KHTF50/5EJcBYdUhTRPfAqrxG0i6jDAXONWOGyQbQ==
+  dependencies:
+    qrcode "^1.4.4"
+
 "@cnakazawa/watch@^1.0.3":
 "@cnakazawa/watch@^1.0.3":
   version "1.0.3"
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef"
   resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef"
@@ -4533,6 +4540,11 @@ diffie-hellman@^5.0.0:
     miller-rabin "^4.0.0"
     miller-rabin "^4.0.0"
     randombytes "^2.0.0"
     randombytes "^2.0.0"
 
 
+dijkstrajs@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.2.tgz#2e48c0d3b825462afe75ab4ad5e829c8ece36257"
+  integrity sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==
+
 dir-glob@^2.0.0, dir-glob@^2.2.2:
 dir-glob@^2.0.0, dir-glob@^2.2.2:
   version "2.2.2"
   version "2.2.2"
   resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4"
   resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4"
@@ -4754,6 +4766,11 @@ emojis-list@^3.0.0:
   resolved "https://registry.npm.taobao.org/emojis-list/download/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
   resolved "https://registry.npm.taobao.org/emojis-list/download/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
   integrity sha1-VXBmIEatKeLpFucariYKvf9Pang=
   integrity sha1-VXBmIEatKeLpFucariYKvf9Pang=
 
 
+encode-utf8@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda"
+  integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==
+
 encodeurl@~1.0.2:
 encodeurl@~1.0.2:
   version "1.0.2"
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
   resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
@@ -5957,6 +5974,11 @@ highlight.js@^9.6.0:
   resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.7.tgz#0e54555beb031894c8f220b3c0eb4253c366406c"
   resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.7.tgz#0e54555beb031894c8f220b3c0eb4253c366406c"
   integrity sha512-6AgA4zXNQZbgvhuJQhL+8JEd+XiPyzRxHpoRnhU084/ZoLqvMRjFQ3eSXvcUlLpLByA++TDYiFr0r4CHRHRJBQ==
   integrity sha512-6AgA4zXNQZbgvhuJQhL+8JEd+XiPyzRxHpoRnhU084/ZoLqvMRjFQ3eSXvcUlLpLByA++TDYiFr0r4CHRHRJBQ==
 
 
+hls.js@^1.1.5:
+  version "1.1.5"
+  resolved "https://registry.npmmirror.com/hls.js/-/hls.js-1.1.5.tgz#923a8a8cfdf09542578696d47c8ae435da981ffd"
+  integrity sha512-mQX5TSNtJEzGo5HPpvcQgCu+BWoKDQM6YYtg/KbgWkmVAcqOCvSTi0SuqG2ZJLXxIzdnFcKU2z7Mrw/YQWhPOA==
+
 hmac-drbg@^1.0.0:
 hmac-drbg@^1.0.0:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
   resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@@ -8872,6 +8894,11 @@ pn@^1.1.0:
   resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
   resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
   integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
   integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
 
 
+pngjs@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
+  integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
+
 pnp-webpack-plugin@^1.6.4:
 pnp-webpack-plugin@^1.6.4:
   version "1.6.4"
   version "1.6.4"
   resolved "https://registry.npm.taobao.org/pnp-webpack-plugin/download/pnp-webpack-plugin-1.6.4.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpnp-webpack-plugin%2Fdownload%2Fpnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149"
   resolved "https://registry.npm.taobao.org/pnp-webpack-plugin/download/pnp-webpack-plugin-1.6.4.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpnp-webpack-plugin%2Fdownload%2Fpnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149"
@@ -9443,6 +9470,16 @@ q@^1.1.2:
   resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
   resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
   integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
   integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
 
 
+qrcode@^1.4.4:
+  version "1.5.0"
+  resolved "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.0.tgz#95abb8a91fdafd86f8190f2836abbfc500c72d1b"
+  integrity sha512-9MgRpgVc+/+47dFvQeD6U2s0Z92EsKzcHogtum4QB+UNd025WOJSHvn/hjk9xmzj7Stj95CyUAs31mrjxliEsQ==
+  dependencies:
+    dijkstrajs "^1.0.1"
+    encode-utf8 "^1.0.3"
+    pngjs "^5.0.0"
+    yargs "^15.3.1"
+
 qs@6.7.0:
 qs@6.7.0:
   version "6.7.0"
   version "6.7.0"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
@@ -11040,10 +11077,10 @@ trim-right@^1.0.1:
   resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
   resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
   integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=
   integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=
 
 
-trtc-js-sdk@^4.6.1:
-  version "4.6.1"
-  resolved "https://registry.npm.taobao.org/trtc-js-sdk/download/trtc-js-sdk-4.6.1.tgz?cache=0&sync_timestamp=1597029484476&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ftrtc-js-sdk%2Fdownload%2Ftrtc-js-sdk-4.6.1.tgz#7792522eacdfebbcb169f2943ccf877f6bdb28bb"
-  integrity sha1-d5JSLqzf67yxafKUPM+Hf2vbKLs=
+trtc-js-sdk@^4.12.1:
+  version "4.12.1"
+  resolved "https://registry.npmmirror.com/trtc-js-sdk/-/trtc-js-sdk-4.12.1.tgz#8be2ab460d830d14e4196c63509b1c86d4ea29ce"
+  integrity sha512-eXYH6uuZQNPn6UmKod0EquwQtoPPVdsb36ruOXMknn6KsnHWzsh8SSbAp1UH9nBdPJQI41lM64XeLjuAQADOpw==
 
 
 tryer@^1.0.0:
 tryer@^1.0.0:
   version "1.0.1"
   version "1.0.1"
@@ -12054,7 +12091,7 @@ yargs-parser@^13.1.2:
     camelcase "^5.0.0"
     camelcase "^5.0.0"
     decamelize "^1.2.0"
     decamelize "^1.2.0"
 
 
-yargs-parser@^18.1.1:
+yargs-parser@^18.1.1, yargs-parser@^18.1.2:
   version "18.1.3"
   version "18.1.3"
   resolved "https://registry.npm.taobao.org/yargs-parser/download/yargs-parser-18.1.3.tgz?cache=0&sync_timestamp=1587068056050&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fyargs-parser%2Fdownload%2Fyargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
   resolved "https://registry.npm.taobao.org/yargs-parser/download/yargs-parser-18.1.3.tgz?cache=0&sync_timestamp=1587068056050&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fyargs-parser%2Fdownload%2Fyargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
   integrity sha1-vmjEl1xrKr9GkjawyHA2L6sJp7A=
   integrity sha1-vmjEl1xrKr9GkjawyHA2L6sJp7A=
@@ -12111,6 +12148,23 @@ yargs@^15.0.0:
     y18n "^4.0.0"
     y18n "^4.0.0"
     yargs-parser "^18.1.1"
     yargs-parser "^18.1.1"
 
 
+yargs@^15.3.1:
+  version "15.4.1"
+  resolved "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
+  integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
+  dependencies:
+    cliui "^6.0.0"
+    decamelize "^1.2.0"
+    find-up "^4.1.0"
+    get-caller-file "^2.0.1"
+    require-directory "^2.1.1"
+    require-main-filename "^2.0.0"
+    set-blocking "^2.0.0"
+    string-width "^4.2.0"
+    which-module "^2.0.0"
+    y18n "^4.0.0"
+    yargs-parser "^18.1.2"
+
 yorkie@^2.0.0:
 yorkie@^2.0.0:
   version "2.0.0"
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/yorkie/-/yorkie-2.0.0.tgz#92411912d435214e12c51c2ae1093e54b6bb83d9"
   resolved "https://registry.yarnpkg.com/yorkie/-/yorkie-2.0.0.tgz#92411912d435214e12c51c2ae1093e54b6bb83d9"