zhangjie 4 rokov pred
rodič
commit
13c61b6272
89 zmenil súbory, kde vykonal 3752 pridanie a 776 odobranie
  1. 4 3
      src/api/examwork-invigilate.js
  2. 12 3
      src/api/examwork-task.js
  3. 102 32
      src/api/invigilation.js
  4. BIN
      src/assets/bg-line-blue.png
  5. BIN
      src/assets/bg-line-cyan.png
  6. BIN
      src/assets/bg-line-yellow.png
  7. BIN
      src/assets/bg-liu.png
  8. BIN
      src/assets/icon-order-asc.png
  9. BIN
      src/assets/icon-order-desc.png
  10. BIN
      src/assets/icon-order-none.png
  11. BIN
      src/assets/icon-password.png
  12. 11 3
      src/components/ExamRoomSelect.vue
  13. 2 0
      src/components/ExamTypeSelect.vue
  14. 1 1
      src/components/InvigilatorSelect.vue
  15. 3 0
      src/components/RoleSelect.vue
  16. 69 0
      src/components/VEditor/VEditor.vue
  17. 66 0
      src/components/VEditor/changeMode.js
  18. 55 0
      src/components/VEditor/components/VMenu.vue
  19. 17 0
      src/components/VEditor/constants.js
  20. 8 0
      src/components/VEditor/main.js
  21. 95 0
      src/components/VEditor/renderJSON.js
  22. 154 0
      src/components/VEditor/toJSON.js
  23. 2 0
      src/components/registerComponents.js
  24. 62 12
      src/constant/constants.js
  25. 134 103
      src/features/Login/Login.vue
  26. 92 64
      src/features/examwork/ActivityManagement/ActivityEdit.vue
  27. 2 1
      src/features/examwork/ActivityManagement/ActivityManagement.vue
  28. 48 8
      src/features/examwork/ActivityManagement/ActivityManagementDialog.vue
  29. 5 2
      src/features/examwork/CourseManagement/CourseManagement.vue
  30. 7 1
      src/features/examwork/CourseManagement/CoursePaperDialog.vue
  31. 13 5
      src/features/examwork/CourseManagement/PaperImportDialog.vue
  32. 17 8
      src/features/examwork/ExamManagement/CopyExamDialog.vue
  33. 100 38
      src/features/examwork/ExamManagement/ExamEdit.vue
  34. 4 1
      src/features/examwork/ExamManagement/ExamManagement.vue
  35. 4 1
      src/features/examwork/ExamStudentImport/ExamStudentImport.vue
  36. 18 10
      src/features/examwork/ExamStudentImport/ExamStudentImportDialog.vue
  37. 6 2
      src/features/examwork/ExamStudentManagement/ExamStudentManagement.vue
  38. 20 7
      src/features/examwork/ExamStudentManagement/ExamStudentManagementDialog.vue
  39. 5 2
      src/features/examwork/ImportExportTask/ImportExportTask.vue
  40. 37 9
      src/features/examwork/InvigilateManagement/InvigilateImportDialog.vue
  41. 31 7
      src/features/examwork/InvigilateManagement/InvigilateManagement.vue
  42. 16 6
      src/features/examwork/InvigilateManagement/InvigilateManagementDialog.vue
  43. 4 1
      src/features/examwork/StudentManagement/StudentManagement.vue
  44. 2 1
      src/features/examwork/StudentManagement/StudentManagementDialog.vue
  45. 18 27
      src/features/invigilation/ExamInvigilation/ExamInvigilation.vue
  46. 92 0
      src/features/invigilation/ExamReport/BreachDetailDialog.vue
  47. 224 5
      src/features/invigilation/ExamReport/ExamReport.vue
  48. 108 0
      src/features/invigilation/ExamReport/ExceptionDetailDialog.vue
  49. 81 0
      src/features/invigilation/ExamReport/ReportAbsent.vue
  50. 82 0
      src/features/invigilation/ExamReport/ReportBreach.vue
  51. 82 0
      src/features/invigilation/ExamReport/ReportCancalBreach.vue
  52. 86 0
      src/features/invigilation/ExamReport/ReportException.vue
  53. 258 0
      src/features/invigilation/ExamReport/ReportOverview.vue
  54. 81 0
      src/features/invigilation/ExamReport/ReportReexam.vue
  55. 104 0
      src/features/invigilation/ExamReport/ReportStatistics.vue
  56. 94 43
      src/features/invigilation/InvigilationDetail/InvigilationDetail.vue
  57. 205 34
      src/features/invigilation/OnlinePatrol/OnlinePatrol.vue
  58. 72 24
      src/features/invigilation/ProgressDetail/ProgressDetail.vue
  59. 8 3
      src/features/invigilation/RealtimeMonitoring/ExamBatchDialog.vue
  60. 110 105
      src/features/invigilation/RealtimeMonitoring/RealtimeMonitoring.vue
  61. 1 1
      src/features/invigilation/RealtimeMonitoring/StudentBreachDialog.vue
  62. 24 15
      src/features/invigilation/RealtimeMonitoring/VideoCommunication.vue
  63. 32 26
      src/features/invigilation/RealtimeMonitoring/WainingDetail.vue
  64. 53 16
      src/features/invigilation/ReexamApply/ReexamApply.vue
  65. 48 16
      src/features/invigilation/ReexamChecked/ReexamChecked.vue
  66. 48 16
      src/features/invigilation/ReexamPending/ReexamPending.vue
  67. 1 0
      src/features/invigilation/StudentLogManage/StudentLogDetailDialog.vue
  68. 46 22
      src/features/invigilation/StudentLogManage/StudentLogManage.vue
  69. 80 47
      src/features/invigilation/WainingManage/WainingManage.vue
  70. 155 2
      src/features/invigilation/common/EchartRender.vue
  71. 12 5
      src/features/invigilation/common/InvigilationStudent.vue
  72. 1 1
      src/features/invigilation/common/RightOrWrong.vue
  73. 39 16
      src/features/invigilation/common/SummaryLine.vue
  74. 4 1
      src/features/system/OrgManagement/OrgManagement.vue
  75. 13 4
      src/features/system/OrgManagement/OrgManagementDialog.vue
  76. 4 2
      src/features/system/UserManagement/UserManagement.vue
  77. 30 6
      src/features/system/UserManagement/UserManagementDialog.vue
  78. 6 0
      src/filters/index.js
  79. 1 0
      src/plugins/VueCharts.js
  80. 6 1
      src/router/index.js
  81. 2 0
      src/store/index.js
  82. 51 0
      src/store/modules/invigilation.js
  83. 2 2
      src/store/modules/user.js
  84. 31 1
      src/styles/base.scss
  85. 27 1
      src/styles/element-ui-custom.scss
  86. 6 0
      src/styles/icons.scss
  87. 43 0
      src/views/Layout/Layout.vue
  88. 49 1
      src/views/Layout/components/SideBar.vue
  89. 4 2
      vue.config.js

+ 4 - 3
src/api/examwork-invigilate.js

@@ -3,13 +3,14 @@ import { pickBy } from "lodash-es";
 import { object2QueryString } from "@/utils/utils";
 
 export function searchInvigilators({
+  examId = "",
   roomCode = "",
   userId = "",
   pageNumber = 1,
   pageSize = 10,
 }) {
   const data = pickBy(
-    { roomCode, userId, pageNumber, pageSize },
+    { examId, roomCode, userId, pageNumber, pageSize },
     (v) => v !== ""
   );
   return httpApp.post(
@@ -17,7 +18,7 @@ export function searchInvigilators({
   );
 }
 
-export function saveInvigilator({ roomCode = "", userIds = "" }) {
-  const data = pickBy({ roomCode, userIds }, (v) => v !== "");
+export function saveInvigilator({ examId = "", roomCode = "", userIds = "" }) {
+  const data = pickBy({ examId, roomCode, userIds }, (v) => v !== "");
   return httpApp.post("/api/admin/invigilateUser/save", data);
 }

+ 12 - 3
src/api/examwork-task.js

@@ -2,8 +2,16 @@ import { httpApp } from "@/plugins/axiosIndex";
 import { pickBy } from "lodash-es";
 import { object2QueryString } from "@/utils/utils";
 
-export function searchTasks({ type = "", pageNumber = 1, pageSize = 10 }) {
-  const data = pickBy({ type, pageNumber, pageSize }, (v) => v !== "");
+export function searchTasks({
+  entityId = "",
+  type = "",
+  pageNumber = 1,
+  pageSize = 10,
+}) {
+  const data = pickBy(
+    { entityId, type, pageNumber, pageSize },
+    (v) => v !== ""
+  );
   return httpApp.post("/api/admin/task/query?" + object2QueryString(data));
 }
 
@@ -44,8 +52,9 @@ export function importExamStudent({ examId, fileName, file, md5 }) {
   });
 }
 
-export function importInvigilator({ fileName, file, md5 }) {
+export function importInvigilator({ examId, fileName, file, md5 }) {
   const form = new FormData();
+  form.append("examId", examId);
   form.append("fileName", fileName);
   form.append("file", file);
   return httpApp.post("/api/admin/invigilateUser/import", form, {

+ 102 - 32
src/api/invigilation.js

@@ -2,6 +2,14 @@ import { httpApp } from "@/plugins/axiosIndex";
 import { pickBy } from "lodash-es";
 import { object2QueryString } from "@/utils/utils";
 
+// monitor key
+export function getUserMonitorKey(recordId) {
+  return httpApp.post(
+    "/api/admin/monitor/getMonitorKey?" + object2QueryString({ recordId }),
+    {}
+  );
+}
+
 // realtime-monitoring
 export function invigilateList(datas) {
   const data = pickBy(datas, (v) => v !== "");
@@ -10,6 +18,13 @@ export function invigilateList(datas) {
     {}
   );
 }
+export function invigilateCount(datas) {
+  const data = pickBy(datas, (v) => v !== "");
+  return httpApp.post(
+    "/api/admin/exam/list/count?" + object2QueryString(data),
+    {}
+  );
+}
 export function invigilateVideoList(datas) {
   const data = pickBy(datas, (v) => v !== "");
   return httpApp.post(
@@ -17,6 +32,12 @@ export function invigilateVideoList(datas) {
     {}
   );
 }
+export function monitorCallCount(examId) {
+  return httpApp.post(
+    "/api/admin/monitor/call/count?" + object2QueryString({ examId }),
+    {}
+  );
+}
 
 // online-patrol
 export function patrolList(datas) {
@@ -37,22 +58,23 @@ export function invigilateExamFinish(examId) {
   return httpApp.post("/api/admin/invigilate/exam/finish?examId=" + examId, {});
 }
 
-// 考试列表
-export function examList(datas) {
+// 监考老师::事实监控台-考试批次列表
+export function examMonitorBatchList(datas) {
   const data = pickBy(datas, (v) => v !== "");
-  return httpApp.post("/api/admin/exam/list?" + object2QueryString(data), {});
+  return httpApp.post("/api/admin/exam/query?" + object2QueryString(data), {});
 }
 // 监考老师::考试批次列表
-export function examBatchList(datas) {
-  const data = pickBy(datas, (v) => v !== "");
-  return httpApp.post(
-    "/api/admin/sys/exam/query?" + object2QueryString(data),
-    {}
-  );
+export function examBatchList(userId) {
+  const paramQuery = userId ? `?userId=${userId}` : "";
+  return httpApp.post("/api/admin/sys/exam/query" + paramQuery);
+}
+// 监考老师::根据权限获取场次和考场接口
+export function examActivityRoomList(examId) {
+  return httpApp.post("/api/admin/sys/exam/privilegeQuery?examId=" + examId);
 }
 // 考试属性统计接口
 export function examPropCount(examId) {
-  return httpApp.post("/api/admin/exam/prop/count?examId=" + examId, {});
+  return httpApp.post("/api/admin/exam/prop/count?examId=" + examId);
 }
 
 export function communicationList({
@@ -90,25 +112,6 @@ export function invigilationHistoryList(datas) {
     "/api/admin/invigilate/history/list?" + object2QueryString(data),
     {}
   );
-
-  // {
-  //   "breachStatus": 0,
-  //   "examActivityId": 0,
-  //   "examId": 0,
-  //   "examRecordId": 0,
-  //   "examStudentId": 0,
-  //   "exceptionCount": 0,
-  //   "finishType": "",
-  //   "identity": "",
-  //   "multipleFaceCount": 0,
-  //   "name": "",
-  //   "roomCode": "",
-  //   "roomName": "",
-  //   "status": "",
-  //   "statusCode": "",
-  //   "updateTime": "",
-  //   "warningCount": 0
-  // }
 }
 
 // waining-manage
@@ -119,14 +122,22 @@ export function invigilationWarningList(datas) {
     {}
   );
 }
-// TODO:批量处理违纪
-export function batchInvigilation(datas) {
+export function invigilationWarningCount(datas) {
   const data = pickBy(datas, (v) => v !== "");
   return httpApp.post(
-    "/api/admin/invigilate/warn/list?" + object2QueryString(data),
+    "/api/admin/invigilate/warn/notify?" + object2QueryString(data),
     {}
   );
 }
+// TODO:批量处理违纪
+export function batchInvigilation(datas) {
+  const data = pickBy(datas, (v) => v !== "");
+  return Promise.resolve(data);
+  // return httpApp.post(
+  //   "/api/admin/invigilate/warn/list?" + object2QueryString(data),
+  //   {}
+  // );
+}
 // TODO:清除未阅
 export function clearInvigilationUnreadWarningList(datas) {
   const data = pickBy(datas, (v) => v !== "");
@@ -182,6 +193,14 @@ export function reexamPendingList(datas) {
     {}
   );
 }
+export function reexamPendingCount(datas) {
+  const data = pickBy(datas, (v) => v !== "");
+  return httpApp.post(
+    "/api/admin/invigilate/reexam/list_not_done_notify?" +
+      object2QueryString(data),
+    {}
+  );
+}
 export function checkReexamApply(datas) {
   const data = pickBy(datas, (v) => v !== "");
   return httpApp.post("/api/admin/invigilate/reexam/auditing", data);
@@ -204,3 +223,54 @@ export function progressDetailList(datas) {
     {}
   );
 }
+
+// exam-report
+// report-overview
+// reexam-checked
+export function reportOverviewData(datas) {
+  const data = pickBy(datas, (v) => v !== "");
+  return httpApp.post(
+    "/api/admin/report/exam_view?" + object2QueryString(data),
+    {}
+  );
+}
+// report-statistics
+export function reportStatisticsData(datas) {
+  const data = pickBy(datas, (v) => v !== "");
+  return httpApp.post(
+    "/api/admin/report/exam_view_count?" + object2QueryString(data),
+    {}
+  );
+}
+// report-absent
+export function reportAbsentData(datas) {
+  const data = pickBy(datas, (v) => v !== "");
+  return httpApp.post(
+    "/api/admin/report/exam_deficiency_list?" + object2QueryString(data),
+    {}
+  );
+}
+// report-exception
+export function reportExceptionData(datas) {
+  const data = pickBy(datas, (v) => v !== "");
+  return httpApp.post(
+    "/api/admin/report/exam_exception_list?" + object2QueryString(data),
+    {}
+  );
+}
+export function reportExceptionDetail(datas) {
+  // examStudentId
+  const data = pickBy(datas, (v) => v !== "");
+  return httpApp.post(
+    "/api/admin/report/exam_exception_list_detail?" + object2QueryString(data),
+    {}
+  );
+}
+// report-reexam
+export function reportReexamData(datas) {
+  const data = pickBy(datas, (v) => v !== "");
+  return httpApp.post(
+    "/api/admin/report/exam_reexam_list?" + object2QueryString(data),
+    {}
+  );
+}

BIN
src/assets/bg-line-blue.png


BIN
src/assets/bg-line-cyan.png


BIN
src/assets/bg-line-yellow.png


BIN
src/assets/bg-liu.png


BIN
src/assets/icon-order-asc.png


BIN
src/assets/icon-order-desc.png


BIN
src/assets/icon-order-none.png


BIN
src/assets/icon-password.png


+ 11 - 3
src/components/ExamRoomSelect.vue

@@ -26,6 +26,7 @@ import { object2QueryString } from "@/utils/utils";
 export default {
   name: "ExamRoomSelect",
   props: {
+    examId: String,
     value: [String, Array],
     styles: { type: String },
   },
@@ -45,15 +46,22 @@ export default {
         this.selected = val;
       },
     },
+    examId: {
+      immediate: true,
+      handler() {
+        this.search();
+      },
+    },
   },
   methods: {
     async search(query) {
+      if (!this.examId) return;
       const res = await this.$http.post(
-        "/api/admin/sys/examRoom/query?" +
-          object2QueryString({ roomName: query })
+        "/api/admin/sys/exam/privilegeQuery?" +
+          object2QueryString({ examId: this.examId, roomName: query })
       );
       // console.log(res.data);
-      this.optionList = res.data.data;
+      this.optionList = res.data.data.examRooms;
     },
     select() {
       this.$emit("input", this.selected);

+ 2 - 0
src/components/ExamTypeSelect.vue

@@ -6,6 +6,7 @@
     @change="select"
     :style="styles"
     clearable
+    :disabled="disabled"
   >
     <el-option
       v-for="item in optionList"
@@ -27,6 +28,7 @@ export default {
       default: "",
     },
     styles: { type: String },
+    disabled: { type: Boolean, default: false },
   },
   data() {
     return {

+ 1 - 1
src/components/InvigilatorSelect.vue

@@ -31,7 +31,7 @@ export default {
   data() {
     return {
       optionList: [],
-      selected: "",
+      selected: null,
     };
   },
   async created() {

+ 3 - 0
src/components/RoleSelect.vue

@@ -48,6 +48,9 @@ export default {
   },
   methods: {
     select() {
+      if (this.selected.length > 1) {
+        this.selected = this.selected.splice(1);
+      }
       this.$emit("input", this.selected);
       this.$emit("change", this.selected);
     },

+ 69 - 0
src/components/VEditor/VEditor.vue

@@ -0,0 +1,69 @@
+<template>
+  <div>
+    <VMenu />
+    <div
+      :id="'ved' + _uid"
+      ref="editor"
+      class="v-editor"
+      :data-placeholder="placeholder"
+      contenteditable
+      @blur="emitJSON"
+    ></div>
+  </div>
+</template>
+
+<script>
+import VMenu from "./components/VMenu.vue";
+import { renderRichText } from "./renderJSON";
+import { toJSON } from "./toJSON";
+
+export default {
+  name: "VEditor",
+  components: {
+    VMenu,
+  },
+  props: {
+    placeholder: { type: String, default: "请输入..." },
+    value: {
+      type: String,
+      default: () => `{}`,
+    },
+  },
+  watch: {
+    value() {
+      renderRichText(JSON.parse(this.value), this.$refs.editor);
+    },
+  },
+  mounted() {
+    renderRichText(JSON.parse(this.value), this.$refs.editor);
+  },
+  methods: {
+    emitJSON() {
+      if (this.$refs.editor.contentEditable) {
+        const json = toJSON(this.$refs.editor);
+        this.$emit("input", json);
+        this.$emit("change", json);
+      }
+    },
+  },
+};
+</script>
+
+<style>
+.v-editor,
+.sourceView {
+  border: 1px solid grey;
+  border-radius: 5px;
+  height: 300px;
+  padding: 5px;
+  overflow: scroll;
+}
+
+.sourceView {
+  margin: -5px;
+}
+
+.v-editor[contenteditable="true"]:empty:not(:focus)::before {
+  content: attr(data-placeholder);
+}
+</style>

+ 66 - 0
src/components/VEditor/changeMode.js

@@ -0,0 +1,66 @@
+const { toJSON } = require("./toJSON");
+
+let bToSource = false;
+let bToJSON = false;
+
+/**
+ *
+ * @param {string} type
+ * @param {HTMLDivElement} edt
+ */
+export function setDocMode(type, edt) {
+  if (type === "HTML") {
+    // if (bToSource === true) return;
+    bToJSON = false;
+    bToSource = !bToSource;
+  }
+  if (type === "JSON") {
+    if (bToSource === true) return;
+    bToSource = false;
+    bToJSON = !bToJSON;
+  }
+
+  let oContent;
+  if (bToSource) {
+    oContent = document.createTextNode(edt.innerHTML);
+    edt.innerHTML = "";
+    edt.contentEditable = false;
+    let oPre = document.createElement("pre");
+
+    oPre.className = "sourceView";
+    oPre.contentEditable = true;
+    oPre.style = "overflow: scroll; white-space: normal;";
+    oPre.appendChild(oContent);
+    edt.appendChild(oPre);
+    // 似乎chrome不支持将默认分段改为p
+    document.execCommand("defaultParagraphSeparator", false, "div");
+  } else if (bToJSON) {
+    const je = document.createElement("div");
+    je.style =
+      "position: absolute; top: 0; left: 0; background: beige; width: 100%; padding: 5px; z-index: 1000000";
+
+    oContent = document.createTextNode(toJSON(edt));
+
+    let oPre = document.createElement("pre");
+
+    // oPre.className = "sourceView";
+    edt.contentEditable = false;
+    oPre.style = "overflow: scroll;";
+    oPre.appendChild(oContent);
+    oPre.innerHTML = "双击关闭\n\n\n" + oPre.innerHTML;
+    je.appendChild(oPre);
+    document.body.appendChild(je);
+
+    je.addEventListener("dblclick", () => {
+      document.body.removeChild(je);
+      bToJSON = false;
+      edt.contentEditable = true;
+    });
+  } else {
+    oContent = document.createRange();
+    oContent.selectNodeContents(edt.firstChild);
+    edt.innerHTML = oContent.toString();
+    edt.contentEditable = true;
+  }
+  edt.focus();
+}

+ 55 - 0
src/components/VEditor/components/VMenu.vue

@@ -0,0 +1,55 @@
+<template>
+  <div class="edit-menus" style="display: flex; gap: 10px;">
+    <!-- 由于v-model会re-render,这里无法redo了 -->
+    <!-- <img
+      class="intLink"
+      title="Undo"
+      @click="execCommand('undo')"
+      src="data:image/gif;base64,R0lGODlhFgAWAOMKADljwliE33mOrpGjuYKl8aezxqPD+7/I19DV3NHa7P///////////////////////yH5BAEKAA8ALAAAAAAWABYAAARR8MlJq7046807TkaYeJJBnES4EeUJvIGapWYAC0CsocQ7SDlWJkAkCA6ToMYWIARGQF3mRQVIEjkkSVLIbSfEwhdRIH4fh/DZMICe3/C4nBQBADs="
+    /> -->
+    <img
+      class="intLink"
+      title="Bold"
+      @click="execCommand('bold')"
+      src="data:image/gif;base64,R0lGODlhFgAWAID/AMDAwAAAACH5BAEAAAAALAAAAAAWABYAQAInhI+pa+H9mJy0LhdgtrxzDG5WGFVk6aXqyk6Y9kXvKKNuLbb6zgMFADs="
+    />
+    <img
+      class="intLink"
+      title="Italic"
+      @click="execCommand('italic')"
+      src="data:image/gif;base64,R0lGODlhFgAWAKEDAAAAAF9vj5WIbf///yH5BAEAAAMALAAAAAAWABYAAAIjnI+py+0Po5x0gXvruEKHrF2BB1YiCWgbMFIYpsbyTNd2UwAAOw=="
+    />
+    <img
+      class="intLink"
+      title="Underline"
+      @click="execCommand('underline')"
+      src="data:image/gif;base64,R0lGODlhFgAWAKECAAAAAF9vj////////yH5BAEAAAIALAAAAAAWABYAAAIrlI+py+0Po5zUgAsEzvEeL4Ea15EiJJ5PSqJmuwKBEKgxVuXWtun+DwxCCgA7"
+    />
+    <!-- <span class="intLink" title="显示源码" @click="setDocMode('HTML')">
+      &lt;/&gt;
+    </span> -->
+    <span class="intLink" @click="setDocMode('JSON')" title="to JSON">{ }</span>
+  </div>
+</template>
+
+<script>
+import { setDocMode } from "../changeMode";
+
+export default {
+  name: "VMenu",
+  methods: {
+    setDocMode(type) {
+      setDocMode(type, this.$parent.$refs.editor);
+    },
+    execCommand(command) {
+      document.execCommand(command);
+    },
+  },
+};
+</script>
+
+<style>
+.intLink {
+  cursor: default;
+}
+</style>

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 17 - 0
src/components/VEditor/constants.js


+ 8 - 0
src/components/VEditor/main.js

@@ -0,0 +1,8 @@
+import Vue from "vue";
+import App from "./App.vue";
+
+Vue.config.productionTip = false;
+
+new Vue({
+  render: (h) => h(App),
+}).$mount("#app");

+ 95 - 0
src/components/VEditor/renderJSON.js

@@ -0,0 +1,95 @@
+// const _text_styles_ = ["bold", "underline", "italic", "sup", "sub"];
+
+export function renderRichText(body, container) {
+  let sections = body.sections || [];
+  let nodes = [];
+  sections.forEach((section) => {
+    nodes.push(renderSection(section));
+  });
+  if (container != undefined) {
+    // container.classList.add("rich-text");
+    while (container.hasChildNodes()) {
+      container.removeChild(container.lastChild);
+    }
+    nodes.forEach((node) => {
+      container.appendChild(node);
+    });
+  }
+
+  return nodes;
+}
+
+function renderSection(section) {
+  let blocks = section.blocks || [];
+  let inline = blocks.length > 1;
+  let node = document.createElement("div");
+  blocks.forEach((block) => {
+    node.appendChild(renderBlock(block, inline));
+  });
+  return node;
+}
+
+function renderBlock(block, inline) {
+  // let node = document.createElement('span')
+  // let classList = node.classList
+  let node;
+  if (block.type === "text") {
+    // classList.add('text')
+    // if (block.param != undefined) {
+    //     _text_styles_.forEach(style => {
+    //         if (block.param[style] === true) {
+    //             classList.add(style)
+    //         }
+    //     })
+    // }
+    if (block.param) {
+      let uNode, bNode, iNode;
+      if (block.param.underline) {
+        uNode = document.createElement("u");
+      }
+      if (block.param.bold) {
+        bNode = document.createElement("b");
+      }
+      if (block.param.italic) {
+        iNode = document.createElement("i");
+      }
+      // 将不为空的元素依次append
+      node = [uNode, bNode, iNode]
+        .filter((v) => v)
+        .reduceRight((p, c) => {
+          c.appendChild(p);
+          return c;
+        });
+
+      let childNode = node;
+
+      for (let i = 0; i < 3; i++) {
+        if (childNode && childNode.hasChildNodes()) {
+          childNode = childNode.childNodes[0];
+        }
+      }
+
+      childNode.textContent = block.value;
+    } else {
+      node = document.createTextNode(block.value);
+    }
+  } else if (block.type === "image") {
+    // classList.add("image");
+    // classList.add("loading");
+    node = document.createElement("img");
+    if (inline === true) {
+      node.classList.add("inline");
+    }
+    node.src = block.value;
+    // img.onload = function () {
+    //   this.parentNode.classList.remove("loading");
+    // };
+  } else if (block.type === "audio") {
+    // classList.add("audio");
+    // let audio = node.appendChild(new Audio());
+    // audio.src = block.value;
+    // audio.controls = true;
+  }
+
+  return node;
+}

+ 154 - 0
src/components/VEditor/toJSON.js

@@ -0,0 +1,154 @@
+import { flattenDeep } from "lodash-es";
+
+let res = [];
+
+/**
+ *
+ * @param {HTMLDivElement} editor
+ */
+export function toJSON(editor) {
+  // console.log(editor.innerHTML);
+  res = [];
+  for (const e of [...editor.childNodes]) {
+    toSection(e);
+  }
+  const newRes = [];
+  for (let section of res) {
+    section = flattenElement(section);
+
+    let newSection = [];
+    for (const b of section) {
+      const toB = toBlock(b);
+      if (toB) newSection.push(toB);
+    }
+    if (newSection.length === 0) {
+      // 空行特殊处理
+      newSection = [{ type: "text", value: " ", param: null }];
+    }
+    newRes.push({ blocks: newSection });
+  }
+  res = { sections: newRes };
+  // console.log(res);
+  // console.log(JSON.stringify(res));
+  // console.log(JSON.stringify(res, null, 2));
+  return JSON.stringify(res, null, 2);
+}
+
+/**
+ *
+ * @param {Node} e
+ */
+function toSection(e) {
+  if (e.nodeType == Node.ELEMENT_NODE && e.nodeName === "DIV") {
+    res.push(e.childNodes);
+  } else if (e.nodeType == Node.TEXT_NODE) {
+    res.push([e]);
+  } else {
+    console.log("toSection: 非div, 非TEXT", e);
+  }
+}
+
+/**
+ *
+ * @param {Node[]} section
+ */
+function flattenElement(section) {
+  section = [...section];
+  // console.log(section);
+  for (let i = 0; i < section.length; i++) {
+    if (section[i].hasChildNodes()) {
+      section[i] = [...section[i].childNodes];
+
+      for (let j = 0; j < section[i].length; j++) {
+        if (section[i][j].hasChildNodes()) {
+          section[i][j] = [...section[i][j].childNodes];
+          for (let k = 0; k < section[i][j].length; k++) {
+            if (section[i][j][k].hasChildNodes()) {
+              section[i][j][k] = [...section[i][j][k].childNodes];
+            }
+          }
+        }
+      }
+    }
+  }
+  section = flattenDeep(section);
+  return section;
+}
+
+/**
+ *
+ * @param {Node} e
+ * @param {String} tag
+ */
+function checkAncestorElementTag(e, tag) {
+  for (let i = 0; i < 3; i++) {
+    if (e.parentElement.nodeName === tag) {
+      return true;
+    } else {
+      e = e.parentElement;
+    }
+  }
+  return false;
+}
+
+/**
+ *
+ * @param {Node} e
+ */
+function toBlock(e) {
+  let block = {};
+  if (e.nodeType === Node.TEXT_NODE) {
+    block.type = "text";
+    block.value = e.textContent;
+    block.param = {};
+    // block.param.italic =
+    //   window.getComputedStyle(e.parentElement).fontStyle === "italic";
+    // block.param.bold =
+    //   window.getComputedStyle(e.parentElement).fontWeight > 400;
+    // block.param.underline =
+    //   window.getComputedStyle(e.parentElement).textDecorationLine ===
+    //   "underline";
+
+    block.param.italic = checkAncestorElementTag(e, "I");
+    block.param.bold = checkAncestorElementTag(e, "B");
+    block.param.underline = checkAncestorElementTag(e, "U");
+
+    // console.log(block.param);
+    const allFalse = Object.values(block.param).every((v) => v === false);
+    // console.log(allFalse);
+    if (allFalse) {
+      block.param = null;
+    }
+    // } else if (e.nodeType == Node.ELEMENT_NODE && e.nodeName === "SPAN") {
+    //   block.type = "text";
+    //   block.value = e.textContent;
+    //   block.param = {};
+  } else if (e.nodeType == Node.ELEMENT_NODE && e.nodeName === "U") {
+    block.type = "text";
+    block.value = e.textContent;
+    block.param = { underline: true };
+  } else if (e.nodeType == Node.ELEMENT_NODE && e.nodeName === "B") {
+    block.type = "text";
+    block.value = e.textContent;
+    block.param = { bold: true };
+  } else if (e.nodeType == Node.ELEMENT_NODE && e.nodeName === "I") {
+    block.type = "text";
+    block.value = e.textContent;
+    block.param = { italic: true };
+  } else if (e.nodeType == Node.ELEMENT_NODE && e.nodeName === "IMG") {
+    block.type = "image";
+    block.value = e.src;
+    block.param = { width: e.width, height: e.height };
+  } else if (e.nodeType == Node.ELEMENT_NODE && e.nodeName === "BR") {
+    block.type = "text";
+    block.value = "";
+    block.param = null;
+  } else {
+    console.log("toBlock: 非法", e);
+  }
+
+  if (block.type === "text" && block.value === "") {
+    block = null; // 返回null的block,从json中剔除
+  }
+  return block;
+}

+ 2 - 0
src/components/registerComponents.js

@@ -1,6 +1,8 @@
 import Vue from "vue";
 // import upperFirst from "lodash/upperFirst";
 // import camelCase from "lodash/camelCase";
+import VEditor from "./VEditor/VEditor";
+Vue.component("VEditor", VEditor);
 
 const requireComponent = require.context(
   // The relative path of the components folder

+ 62 - 12
src/constant/constants.js

@@ -11,33 +11,83 @@ export const INVIGILATOR_IMPORT_TEMPLATE_DOWNLOAD_URL =
 export const EXAM_STUDENT_IMPORT_TEMPLATE_DOWNLOAD_URL =
   "http://qmth-test.oss-cn-shenzhen.aliyuncs.com/file/考生导入_在线考试.xlsx";
 
-export const FINISH_TYPE = {
+// 交卷方式
+export const STUDENT_FINISH_EXAM_TYPE = {
   MANUAL: "手动收卷",
   AUTO: "正常交卷",
   BREACH: "强制收卷",
   INTERRUPT: "系统清卷",
 };
-
-export const REEXAM_TYPE = {
-  0: "批次内",
-  1: "换批次",
+// 是 / 否
+export const BOOLEAN_TYPE = {
+  0: "",
+  1: "",
 };
-
+// 考生在线状态
+export const STUDENT_ONLINE_STATUS = {
+  FIRST_PREPARE: "待考",
+  EXAMING: "考试中",
+  BREAK_OFF: "异常",
+};
+// 推流通讯
+export const STUDENT_EXAM_STATUS = {
+  FIRST_PREPARE: "首次候考",
+  ANSWERING: "正在答题",
+  BREAK_OFF: "",
+  RESUME_PREPARE: "",
+  FINISHED: "",
+  PERSISTED: "",
+};
+// 违纪、缺考
+export const STUDENT_BEHAVIOR_STATUS = {
+  0: "正常",
+  1: "违纪",
+  2: "缺考",
+};
+// 违规类型
 export const BREACH_REASON_TYPE = {
-  0: "批次内12",
-  1: "换批次1",
+  0: "夹带抄袭",
+  1: "左顾右盼",
+  2: "考中携带违规物品",
+  3: "他人替考",
+  4: "他人协助作答",
+  5: "考中使用违规(远程协助、直播等)软件",
+  6: "其他",
 };
-
+// 违规撤销原因
 export const BREACH_REPEAL_TYPE = {
-  0: "批次内334",
-  1: "换批次555",
+  0: "软件误操作",
+  1: "违规事实不符",
+  2: "其他",
 };
-
+// 审阅状态
+export const APPROVE_STATUS = {
+  0: "未审阅",
+  1: "已审阅",
+};
+// 重考方式
+export const REEXAM_TYPE = {
+  0: "批次内",
+  1: "换批次",
+};
+// 重考原因
 export const REEXAM_REASON = {
   EXCEPTION_TIME_OUT: "异常处理时效过期",
   BREAK_TIME_OUT: "断点续考次数用完",
   INVIGILATE_MISS: "监考人员误操作",
 };
+// 通讯状态
+export const CLIENT_WEBSOCKET_STATUS = {
+  ON_LINE: 1,
+  OFF_LINE: 0,
+};
+// 推流通讯
+export const MONITOR_STATUS_SOURCE = {
+  INIT: 1,
+  STOP: 0,
+  START: 1,
+  FINISH: 0,
+};
 
 export const IMPORT_EXPORT_TASKS = [
   { code: "CALCULATE_EXAM_SCORE", name: "考试重新算分" },

+ 134 - 103
src/features/Login/Login.vue

@@ -7,82 +7,91 @@
         在线考试管理系统
       </h2> -->
       <div class="form-container">
-        <div class="text-center">
-          <img :src="schoolLogo" class="school-logo" alt="学校logo" />
-          <div class="text-center title">在线考试平台后台管理系统</div>
-        </div>
-        <el-form ref="form" :model="user" label-width="0">
-          <div class="form-items">
-            <el-row class="form-item">
-              <el-col>
-                <el-form-item
-                  prop="username"
-                  :rules="[{ required: true, message: '用户名不能为空' }]"
-                >
-                  <div class="form-line">
-                    <el-input
-                      prefix-icon="el-icon-user"
-                      placeholder="请输入用户名"
-                      v-model="user.username"
-                    ></el-input>
-                  </div>
-                </el-form-item>
-              </el-col>
-            </el-row>
-            <el-row class="form-item">
-              <el-col>
-                <el-form-item
-                  prop="password"
-                  :rules="[{ required: true, message: '密码不能为空' }]"
+        <div class="form-body">
+          <div class="text-center">
+            <div class="school-logo">
+              <img :src="schoolLogo" alt="学校logo" />
+            </div>
+            <h1 class="text-center login-title">在线考试平台后台管理系统</h1>
+          </div>
+          <el-form ref="form" :model="user" label-width="0">
+            <div class="form-items">
+              <el-row class="form-item">
+                <el-col>
+                  <el-form-item
+                    prop="username"
+                    :rules="[{ required: true, message: '用户名不能为空' }]"
+                  >
+                    <div class="form-line">
+                      <el-input
+                        size="large"
+                        prefix-icon="icon icon-user"
+                        placeholder="请输入用户名"
+                        v-model="user.username"
+                        clearable
+                      ></el-input>
+                    </div>
+                  </el-form-item>
+                </el-col>
+              </el-row>
+              <el-row class="form-item">
+                <el-col>
+                  <el-form-item
+                    prop="password"
+                    :rules="[{ required: true, message: '密码不能为空' }]"
+                  >
+                    <div class="form-line">
+                      <el-input
+                        size="large"
+                        prefix-icon="icon icon-password"
+                        type="password"
+                        placeholder="请输入密码"
+                        v-model="user.password"
+                        clearable
+                      ></el-input>
+                    </div>
+                  </el-form-item>
+                </el-col>
+              </el-row>
+              <el-row class="form-item error-info">
+                <el-col>
+                  <el-form-item>
+                    <div v-show="user.errorInfo">
+                      <i class="el-icon-error"></i>{{ user.errorInfo }}
+                    </div>
+                  </el-form-item>
+                </el-col>
+              </el-row>
+              <!-- <el-row class="form-item">
+                <el-col>
+                  <el-form-item>
+                    <el-checkbox class="checkbox">记住账号</el-checkbox>
+                  </el-form-item>
+                </el-col>
+              </el-row> -->
+              <el-row class="form-item">
+                <el-button
+                  size="large"
+                  type="primary"
+                  class="submit-btn"
+                  @click="submitBtn"
+                  :loading="loading"
                 >
-                  <div class="form-line">
-                    <el-input
-                      prefix-icon="el-icon-lock"
-                      type="password"
-                      placeholder="请输入密码"
-                      v-model="user.password"
-                    ></el-input>
-                  </div>
-                </el-form-item>
-              </el-col>
-            </el-row>
-            <el-row class="form-item error-info">
-              <el-col>
-                <el-form-item>
-                  <div v-show="user.errorInfo">
-                    <i class="el-icon-error"></i>{{ user.errorInfo }}
-                  </div>
-                </el-form-item>
-              </el-col>
-            </el-row>
-            <!-- <el-row class="form-item">
-              <el-col>
-                <el-form-item>
-                  <el-checkbox class="checkbox">记住账号</el-checkbox>
-                </el-form-item>
-              </el-col>
+                  登 录
+                </el-button>
+              </el-row>
+            </div>
+            <!-- <el-row class="tips">
+              <a href="/" class="link">
+                立即注册
+              </a>
+              <span class="line">|</span>
+              <a href="/" class="link">
+                忘记密码
+              </a>
             </el-row> -->
-            <el-row class="form-item">
-              <el-button
-                type="primary"
-                class="submit-btn"
-                size="small"
-                @click="submitBtn"
-              >
-                登 录
-              </el-button>
-            </el-row>
-          </div>
-          <!-- <el-row class="tips">
-            <a href="/" class="link">
-              立即注册
-            </a>
-            <span class="line">|</span>
-            <a href="/" class="link">
-              忘记密码
-            </a>
-          </el-row> -->
-        </el-form>
+          </el-form>
+        </div>
       </div>
     </div>
     <div class="footer">Copyright © www.qmth.com.cn, All Rights Reserved.</div>
@@ -105,6 +114,7 @@ export default {
         password: "",
         errorInfo: "",
       },
+      loading: false,
     };
   },
   async created() {
@@ -116,6 +126,7 @@ export default {
       this.$refs["form"].validate(async (valid) => {
         if (valid) {
           try {
+            this.loading = true;
             await this.$store.dispatch(LOGIN_BY_USERNAME, {
               loginName: this.user.username,
               password: this.user.password,
@@ -133,6 +144,8 @@ export default {
           } catch (error) {
             // console.log(error?.response?.data?.message);
             this.user.errorInfo = error?.response?.data?.message || "";
+          } finally {
+            this.loading = false;
           }
         }
       });
@@ -179,11 +192,39 @@ export default {
     display: flex;
     justify-content: center;
     flex-direction: column;
-    padding: 30px 40px;
+    width: 400px;
+    border: 6px solid rgba(0, 0, 0, 0.1);
+    border-radius: 20px;
+  }
+  .form-body {
+    padding: 55px 56px 50px;
     background-color: #fff;
-    border-radius: 6px;
-    box-shadow: 1px 1px 2px #eee;
+    border-radius: 20px;
   }
+  .school-logo {
+    height: 60px;
+    position: relative;
+
+    > img {
+      display: block;
+      position: absolute;
+      margin: auto;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      max-height: 100%;
+      max-width: 100%;
+    }
+  }
+  .login-title {
+    color: #626a82;
+    font-size: 18px;
+    font-weight: 700;
+    line-height: 25px;
+    margin: 6px 0 40px;
+  }
+
   .el-form-item {
     margin-bottom: 15px;
   }
@@ -205,14 +246,7 @@ export default {
     }
   }
   .el-input {
-    width: 240px;
-    // FIXME: 此处css没有生效
-    input {
-      border: none;
-      margin: 0;
-      padding-left: 10px;
-      font-size: 13px;
-    }
+    width: 100%;
   }
   .input-icon {
     color: #999;
@@ -221,13 +255,9 @@ export default {
     margin-left: 5px;
   }
   .submit-btn {
-    margin-bottom: 25px;
     width: 100%;
-    background: #3080fe;
-    border-radius: 28px;
-  }
-  .tips {
   }
+
   .link {
     color: #999;
     text-decoration: none;
@@ -254,19 +284,6 @@ export default {
     }
   }
 }
-.school-logo {
-  text-align: center;
-  height: 60px;
-  width: 200px;
-  object-fit: cover;
-}
-.title {
-  color: #626a82;
-  font-size: 18px;
-  font-weight: 700;
-
-  margin-bottom: 30px;
-}
 .form-item {
   margin-bottom: 5px;
 }
@@ -282,5 +299,19 @@ export default {
   font-size: 12px;
   text-align: center;
   width: 100%;
+  color: #fff;
+}
+</style>
+<style lang="scss">
+.user-login {
+  .el-input--prefix {
+    .el-input__inner {
+      padding-left: 40px;
+    }
+    .el-input__prefix {
+      padding-top: 4px;
+      left: 16px;
+    }
+  }
 }
 </style>

+ 92 - 64
src/features/examwork/ActivityManagement/ActivityEdit.vue

@@ -19,7 +19,7 @@
       <el-form
         :model="item"
         :ref="'form' + index"
-        :rules="rules"
+        :rules="rules(index)"
         label-position="right"
         inline
         v-for="(item, index) in form"
@@ -44,7 +44,7 @@
             >
             </el-date-picker>
           </el-form-item>
-          <el-form-item label="考试时长" prop="prepareSeconds">
+          <el-form-item label="考试时长" prop="maxDurationSeconds">
             <MinuteInput
               v-model="item.maxDurationSeconds"
               style="width: 125px;"
@@ -53,14 +53,21 @@
           <el-form-item label="候考时间" prop="prepareSeconds">
             <MinuteInput v-model="item.prepareSeconds" style="width: 125px;" />
           </el-form-item>
-          <el-form-item label="迟到时长" prop="prepareSeconds">
+          <el-form-item label="迟到时长" prop="openingSeconds">
             <MinuteInput v-model="item.openingSeconds" style="width: 125px;" />
           </el-form-item>
+          <el-form-item label="操作">
+            <el-button type="primary" @click="removeActivity(index)" size="mini"
+              >删 除</el-button
+            >
+          </el-form-item>
         </div>
       </el-form>
     </el-row>
     <el-row class="d-flex justify-content-center">
-      <el-button type="primary" @click="submitForm">保 存</el-button>
+      <el-button type="primary" @click="submitForm" :loading="loading"
+        >保 存</el-button
+      >
       <el-button type="primary" @click="addActivity">新 增</el-button>
       <el-button @click="() => this.$router.back()">取 消</el-button>
     </el-row>
@@ -97,7 +104,48 @@ export default {
           maxDurationSeconds: 0,
         },
       ],
-      rules: {
+
+      exam: {},
+      activity: {},
+      loading: false,
+    };
+  },
+  async created() {
+    try {
+      this.exam = (await getExamDetail({ id: this.examId }))?.data.data;
+      this.form[0].prepareSeconds = this.exam.prepareSeconds;
+      this.form[0].openingSeconds = this.exam.openingSeconds;
+      this.form[0].maxDurationSeconds = this.exam.maxDurationSeconds;
+    } catch (error) {
+      console.log(error);
+      this.$notify({ type: "error", title: "获取考试详情失败" });
+    }
+
+    // if (this.isEdit) {
+    //   try {
+    //     this.activity = (
+    //       await getActivityDetail({ id: this.activityId })
+    //     )?.data.data.records[0];
+    //     this.form = this.activity;
+    //   } catch (error) {
+    //     console.log(error);
+    //     this.$notify({ type: "error", title: "获取场次详情失败" });
+    //   }
+    // } else {
+    // this.form = {
+    //   id: "",
+    //   startTime: null,
+    //   finishTime: null,
+    //   prepareSeconds: 0,
+    //   openingSeconds: 0,
+    //   maxDurationSeconds: 0,
+    //   enable: 0,
+    // };
+    // }
+  },
+  methods: {
+    rules(index) {
+      return {
         startTime: [
           { required: true, message: "开始时间必填" },
           {
@@ -117,22 +165,6 @@ export default {
                 }
               });
             },
-            // type: "date",
-            // asyncValidator: (rule, value) => {
-            //   console.log(value);
-            //   return new Promise((resolve, reject) => {
-            //     if (
-            //       moment(value).isBetween(
-            //         moment(that.exam.startTime),
-            //         moment(that.exam.endTime)
-            //       )
-            //     ) {
-            //       resolve(); // reject with error message
-            //     } else {
-            //       reject();
-            //     }
-            //   });
-            // },
             message: "场次的开始时间不在考试的时间范围",
           },
         ],
@@ -141,6 +173,14 @@ export default {
           {
             validator: (rule, value) => {
               return new Promise((resolve, reject) => {
+                if (
+                  moment(this.form[index].finishTime) -
+                    moment(this.form[index].startTime) <=
+                  0
+                ) {
+                  reject("考试结束时间要大于考试开始时间");
+                  return;
+                }
                 if (
                   moment(value).isBetween(
                     moment(this.exam.startTime),
@@ -150,56 +190,36 @@ export default {
                   )
                 ) {
                   resolve(); // reject with error message
+                } else {
+                  reject("场次的交卷时间不在考试的时间范围");
+                }
+              });
+            },
+          },
+        ],
+        maxDurationSeconds: [
+          { required: true, message: "考试时长必填" },
+          {
+            validator: (rule, value) => {
+              return new Promise((resolve, reject) => {
+                if (
+                  moment(this.form[index].finishTime) -
+                    moment(this.form[index].startTime) >=
+                  value * 1000
+                ) {
+                  resolve();
                 } else {
                   reject("reject");
                 }
               });
             },
-            message: "场次的交卷时间不在考试的时间范围",
+            message: "考试时长超出范围",
           },
         ],
-        maxDurationSeconds: [{ required: true, message: "考试时长必填" }],
         prepareSeconds: [{ required: true, message: "候考时间必填" }],
         openingSeconds: [{ required: true, message: "迟到时长必填" }],
-      },
-      exam: {},
-      activity: {},
-    };
-  },
-  async created() {
-    try {
-      this.exam = (await getExamDetail({ id: this.examId }))?.data.data;
-      this.form[0].prepareSeconds = this.exam.prepareSeconds;
-      this.form[0].openingSeconds = this.exam.openingSeconds;
-      this.form[0].maxDurationSeconds = this.exam.maxDurationSeconds;
-    } catch (error) {
-      console.log(error);
-      this.$notify({ type: "error", title: "获取考试详情失败" });
-    }
-
-    // if (this.isEdit) {
-    //   try {
-    //     this.activity = (
-    //       await getActivityDetail({ id: this.activityId })
-    //     )?.data.data.records[0];
-    //     this.form = this.activity;
-    //   } catch (error) {
-    //     console.log(error);
-    //     this.$notify({ type: "error", title: "获取场次详情失败" });
-    //   }
-    // } else {
-    // this.form = {
-    //   id: "",
-    //   startTime: null,
-    //   finishTime: null,
-    //   prepareSeconds: 0,
-    //   openingSeconds: 0,
-    //   maxDurationSeconds: 0,
-    //   enable: 0,
-    // };
-    // }
-  },
-  methods: {
+      };
+    },
     addActivity() {
       this.form.push({
         id: "",
@@ -210,6 +230,9 @@ export default {
         maxDurationSeconds: this.exam.maxDurationSeconds,
       });
     },
+    removeActivity(index) {
+      this.form.splice(index, 1);
+    },
     async submitForm() {
       let data = [];
       for (let i = 0; i < this.form.length; i++) {
@@ -232,8 +255,13 @@ export default {
         }
       }
 
-      await saveActivities(data);
-      this.$notify({ title: "保存成功", type: "success" });
+      try {
+        this.loading = true;
+        await saveActivities(data);
+        this.$notify({ title: "保存成功", type: "success" });
+      } finally {
+        this.loading = false;
+      }
     },
   },
 };

+ 2 - 1
src/features/examwork/ActivityManagement/ActivityManagement.vue

@@ -4,7 +4,7 @@
       <el-form-item label="场次代码">
         <el-input v-model.trim="form.code"></el-input>
       </el-form-item>
-      <el-button @click="searchForm">查询</el-button>
+      <el-button @click="handleCurrentChange(0)">查询</el-button>
       <el-button @click="add">新增</el-button>
       <!-- <el-button>导入</el-button> -->
     </el-form>
@@ -61,6 +61,7 @@
     </el-table>
     <div class="page float-right">
       <el-pagination
+        background
         @current-change="handleCurrentChange"
         :current-page="currentPage"
         :page-size="pageSize"

+ 48 - 8
src/features/examwork/ActivityManagement/ActivityManagementDialog.vue

@@ -34,7 +34,7 @@
         </el-form-item>
       </el-row>
       <el-row>
-        <el-form-item label="考试时长" prop="prepareSeconds">
+        <el-form-item label="考试时长" prop="maxDurationSeconds">
           <MinuteInput v-model="form.maxDurationSeconds" />
         </el-form-item>
       </el-row>
@@ -44,7 +44,7 @@
         </el-form-item>
       </el-row>
       <el-row>
-        <el-form-item label="迟到时长" prop="prepareSeconds">
+        <el-form-item label="迟到时长" prop="openingSeconds">
           <MinuteInput v-model="form.openingSeconds" />
         </el-form-item>
       </el-row>
@@ -57,7 +57,9 @@
         </el-form-item>
       </el-row>
       <el-row class="d-flex justify-content-center">
-        <el-button type="primary" @click="submitForm">保 存</el-button>
+        <el-button type="primary" @click="submitForm" :loading="loading"
+          >保 存</el-button
+        >
         <el-button @click="closeDialog">取 消</el-button>
       </el-row>
     </el-form>
@@ -123,6 +125,14 @@ export default {
           {
             validator: (rule, value) => {
               return new Promise((resolve, reject) => {
+                if (
+                  moment(this.form.finishTime) - moment(this.form.startTime) <=
+                  0
+                ) {
+                  reject("考试结束时间要大于考试开始时间");
+                  return;
+                }
+
                 if (
                   moment(value).isBetween(
                     moment(this.exam.startTime),
@@ -132,18 +142,35 @@ export default {
                   )
                 ) {
                   resolve(); // reject with error message
+                } else {
+                  reject("场次的交卷时间不在考试的时间范围");
+                }
+              });
+            },
+          },
+        ],
+        maxDurationSeconds: [
+          { required: true, message: "考试时长必填" },
+          {
+            validator: (rule, value) => {
+              return new Promise((resolve, reject) => {
+                if (
+                  moment(this.form.finishTime) - moment(this.form.startTime) >=
+                  value * 1000
+                ) {
+                  resolve();
                 } else {
                   reject("reject");
                 }
               });
             },
-            message: "场次的交卷时间不在考试的时间范围",
+            message: "考试时长超出范围",
           },
         ],
-        maxDurationSeconds: [{ required: true, message: "考试时长必填" }],
         prepareSeconds: [{ required: true, message: "候考时间必填" }],
         openingSeconds: [{ required: true, message: "迟到时长必填" }],
       },
+      loading: false,
     };
   },
   watch: {
@@ -177,14 +204,27 @@ export default {
       this.visible = false;
     },
     async submitForm() {
+      try {
+        await this.$refs.form.validate();
+      } catch (error) {
+        console.log(error);
+        return;
+      }
+
       let data = this.form;
       data = { ...data, examId: this.examId };
       if (this.isEdit) {
         data = { ...data, id: this.activity.id };
       }
-      await saveActivity(data);
-      this.$emit("reload");
-      this.closeDialog();
+      try {
+        this.loading = true;
+        await saveActivity(data);
+        this.$emit("reload");
+        this.$notify({ title: "保存成功", type: "success" });
+        this.closeDialog();
+      } finally {
+        this.loading = false;
+      }
     },
   },
 };

+ 5 - 2
src/features/examwork/CourseManagement/CourseManagement.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="course-management">
     <div class="part-box-head">
-      <div class="part-box-head-left"><h1>预警提醒</h1></div>
+      <div class="part-box-head-left"><h1>调卷规则</h1></div>
     </div>
     <div class="part-filter">
       <div class="part-filter-form">
@@ -22,7 +22,9 @@
             />
           </el-form-item>
           <el-form-item>
-            <el-button type="primary" @click="searchForm">查询</el-button>
+            <el-button type="primary" @click="handleCurrentChange(0)"
+              >查询</el-button
+            >
           </el-form-item>
         </el-form>
         <div class="part-filter-form-action">
@@ -77,6 +79,7 @@
     </el-table>
     <div class="part-page">
       <el-pagination
+        background
         @current-change="handleCurrentChange"
         :current-page="currentPage"
         :page-size="pageSize"

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

@@ -77,7 +77,9 @@
         </el-form-item>
       </el-row>
       <el-row class="d-flex justify-content-center">
-        <el-button type="primary" @click="submitForm">保 存</el-button>
+        <el-button type="primary" @click="submitForm" :loading="loading">
+          保 存
+        </el-button>
         <el-button @click="closeDialog">取 消</el-button>
       </el-row>
     </el-form>
@@ -104,6 +106,7 @@ export default {
       rules: {},
       refreshCourse: {},
       papers: [],
+      loading: false,
     };
   },
   watch: {
@@ -143,6 +146,7 @@ export default {
     },
     async submitForm() {
       try {
+        this.loading = true;
         await saveCourse({
           examId: this.refreshCourse.examId,
           courseCode: this.refreshCourse.courseCode,
@@ -165,6 +169,8 @@ export default {
         console.log(error);
         this.initData();
         this.$notify({ title: "保存失败", type: "warning" });
+      } finally {
+        this.loading = false;
       }
     },
   },

+ 13 - 5
src/features/examwork/CourseManagement/PaperImportDialog.vue

@@ -46,7 +46,9 @@
         </el-form-item>
       </el-row>
       <el-row class="d-flex justify-content-center">
-        <el-button type="primary" @click="submitForm">保 存</el-button>
+        <el-button type="primary" @click="submitForm" :loading="loading"
+          >保 存</el-button
+        >
         <el-button @click="closeDialog">取 消</el-button>
       </el-row>
     </el-form>
@@ -67,6 +69,7 @@ export default {
       visible: false,
       form: {},
       rules: {},
+      loading: false,
     };
   },
   watch: {
@@ -112,10 +115,15 @@ export default {
 
       let data = this.form;
 
-      await importPaper({ ...data, examId: this.examId, md5 });
-      this.$emit("reload");
-      this.$notify({ title: "导入任务已成功启动", type: "success" });
-      this.closeDialog();
+      try {
+        this.loading = true;
+        await importPaper({ ...data, examId: this.examId, md5 });
+        this.$emit("reload");
+        this.$notify({ title: "导入任务已成功启动", type: "success" });
+        this.closeDialog();
+      } finally {
+        this.loading = false;
+      }
     },
   },
 };

+ 17 - 8
src/features/examwork/ExamManagement/CopyExamDialog.vue

@@ -33,7 +33,9 @@
         </el-form-item>
       </el-row>
       <el-row class="d-flex justify-content-center">
-        <el-button type="primary" @click="submitForm">保 存</el-button>
+        <el-button type="primary" @click="submitForm" :loading="loading"
+          >保 存</el-button
+        >
         <el-button @click="closeDialog">取 消</el-button>
       </el-row>
     </el-form>
@@ -55,6 +57,7 @@ export default {
         code: "",
       },
       rules: {},
+      loading: false,
     };
   },
   watch: {
@@ -70,13 +73,19 @@ export default {
       this.visible = false;
     },
     async submitForm() {
-      await copyExam({
-        sourceId: this.exam.id,
-        code: this.form.code,
-        name: this.form.name,
-      });
-      this.$emit("reload");
-      this.closeDialog();
+      try {
+        this.loading = true;
+        await copyExam({
+          sourceId: this.exam.id,
+          code: this.form.code,
+          name: this.form.name,
+        });
+        this.$emit("reload");
+        this.$notify({ title: "复制成功", type: "success" });
+        this.closeDialog();
+      } finally {
+        this.loading = false;
+      }
     },
   },
 };

+ 100 - 38
src/features/examwork/ExamManagement/ExamEdit.vue

@@ -11,21 +11,24 @@
         >
           <el-row>
             <el-form-item label="考试模式" prop="mode">
-              <ExamTypeSelect v-model="form.mode"></ExamTypeSelect>
+              <ExamTypeSelect
+                :disabled="isEdit"
+                v-model="form.mode"
+              ></ExamTypeSelect>
             </el-form-item>
           </el-row>
           <el-row>
-            <el-form-item label="批次编码">
-              <el-input v-model.trim="form.code"></el-input>
+            <el-form-item label="批次编码" prop="code">
+              <el-input :disabled="isEdit" v-model.trim="form.code"></el-input>
             </el-form-item>
           </el-row>
           <el-row>
-            <el-form-item label="批次名称">
+            <el-form-item label="批次名称" prop="name">
               <el-input v-model.trim="form.name"></el-input>
             </el-form-item>
           </el-row>
           <el-row>
-            <el-form-item label="考试时间">
+            <el-form-item label="考试时间" prop="startEndTimeProxy">
               <el-date-picker
                 v-model="form.startEndTimeProxy"
                 type="datetimerange"
@@ -36,7 +39,7 @@
               </el-date-picker>
             </el-form-item>
           </el-row>
-          <el-row>
+          <el-row v-if="!isModeAnytime">
             <el-form-item label="候考时长(分钟)">
               <MinuteInput v-model.trim="form.prepareSeconds"> </MinuteInput>
             </el-form-item>
@@ -52,7 +55,7 @@
               </MinuteInput>
             </el-form-item>
           </el-row>
-          <el-row>
+          <el-row v-if="!isModeAnytime">
             <el-form-item label="迟到时长(分钟)">
               <MinuteInput v-model.trim="form.openingSeconds"> </MinuteInput>
             </el-form-item>
@@ -63,7 +66,7 @@
               </MinuteInput>
             </el-form-item>
           </el-row>
-          <el-row>
+          <el-row v-if="!isModeAnytime">
             <el-form-item label="启用集中收卷">
               <el-radio v-model="form.forceFinish" :label="1">是</el-radio>
               <el-radio v-model="form.forceFinish" :label="0">否</el-radio>
@@ -71,27 +74,31 @@
           </el-row>
           <el-row>
             <el-form-item label="启用开考口令">
-              <el-radio v-model="form.enableShortCode" :label="1">是</el-radio>
-              <el-radio v-model="form.enableShortCode" :label="0">否</el-radio>
+              <el-radio v-model="enableShortCodeProxy" :label="true"
+                >是</el-radio
+              >
+              <el-radio v-model="enableShortCodeProxy" :label="false"
+                >否</el-radio
+              >
               <el-input
-                v-if="form.enableShortCode"
+                v-if="enableShortCodeProxy"
                 v-model.trim="form.shortCode"
               ></el-input>
             </el-form-item>
           </el-row>
           <el-row>
             <el-form-item label="是否允许断点续考">
-              <el-radio v-model="form.enableBreak" :label="1">是</el-radio>
-              <el-radio v-model="form.enableBreak" :label="0">否</el-radio>
+              <el-radio v-model="enableBreakProxy" :label="true">是</el-radio>
+              <el-radio v-model="enableBreakProxy" :label="false">否</el-radio>
             </el-form-item>
           </el-row>
           <el-row>
-            <el-form-item v-if="form.enableBreak" label="断点次数">
+            <el-form-item v-if="enableBreakProxy" label="断点次数">
               <el-input v-model.trim="form.breakResumeCount"></el-input>
             </el-form-item>
           </el-row>
           <el-row>
-            <el-form-item v-if="form.enableBreak" label="断点时长(分钟)">
+            <el-form-item v-if="enableBreakProxy" label="断点时长(分钟)">
               <MinuteInput v-model.trim="form.breakExpireSeconds">
               </MinuteInput>
             </el-form-item>
@@ -104,18 +111,24 @@
           </el-row>
           <el-row>
             <el-form-item label="取分策略">
-              <el-radio v-model="form.recordSelectStrategy" label="LATEST">
+              <el-radio
+                v-model="form.recordSelectStrategy"
+                :disabled="isEdit"
+                label="LATEST"
+              >
                 最后一次提交
               </el-radio>
               <el-radio
                 v-model="form.recordSelectStrategy"
                 label="HIGHEST_OBJECTIVE_SCORE"
+                :disabled="isEdit"
               >
                 客观分最高
               </el-radio>
               <el-radio
                 v-model="form.recordSelectStrategy"
                 label="HIGHEST_TOTAL_SCORE"
+                :disabled="isEdit"
               >
                 总分最高
               </el-radio>
@@ -298,14 +311,14 @@
           <el-row>
             <el-form-item v-if="form.monitorProxy" label="电脑&手机监控方案">
               <el-checkbox-group v-model="form.monitorVideoSource">
-                <el-checkbox label="client_camera"
+                <el-checkbox label="CLIENT_CAMERA"
                   >电脑摄像头为主机位</el-checkbox
                 >
-                <el-checkbox label="client_screen">电脑开启录频</el-checkbox>
-                <el-checkbox label="mobile_first">手机主机位</el-checkbox>
+                <el-checkbox label="CLIENT_SCREEN">电脑开启录频</el-checkbox>
+                <el-checkbox label="MOBILE_FIRST">手机主机位</el-checkbox>
                 <el-checkbox
-                  :disabled="!form.monitorVideoSource.includes('mobile_first')"
-                  label="mobile_second"
+                  :disabled="!form.monitorVideoSource.includes('MOBILE_FIRST')"
+                  label="MOBILE_SECOND"
                   >手机辅机位</el-checkbox
                 >
               </el-checkbox-group>
@@ -319,7 +332,7 @@
         <el-form :model="form" label-width="170px" inline>
           <el-row>
             <el-form-item label="考试须知">
-              <el-input v-model.trim="form.preNotice"></el-input>
+              <VEditor v-model="form.preNotice" style="width: 300px;" />
             </el-form-item>
           </el-row>
           <el-row>
@@ -331,7 +344,7 @@
           </el-row>
           <el-row>
             <el-form-item label="考后说明">
-              <el-input v-model.trim="form.postNotice"></el-input>
+              <VEditor v-model="form.postNotice" style="width: 300px;" />
             </el-form-item>
           </el-row>
           <el-row>
@@ -350,7 +363,9 @@
     </el-tabs>
 
     <div class="tab-footer">
-      <el-button type="primary" @click="save">保存</el-button>
+      <el-button type="primary" @click="save" :loading="loading"
+        >保存</el-button
+      >
       <el-button @click="cancel">取消</el-button>
     </div>
   </div>
@@ -372,6 +387,35 @@ export default {
     isEdit() {
       return !!this.examId;
     },
+    isModeAnytime() {
+      return this.form.mode === "ANYTIME";
+    },
+    enableShortCodeProxy: {
+      get() {
+        return !!this.form.shortCode;
+      },
+      set(v) {
+        if (v) {
+          this.form.shortCode = "123";
+        } else {
+          this.form.shortCode = null;
+        }
+      },
+    },
+    enableBreakProxy: {
+      get() {
+        return !!(this.form.breakResumeCount || this.form.breakExpireSeconds);
+      },
+      set(v) {
+        if (v) {
+          this.form.breakResumeCount = 1;
+          this.form.breakExpireSeconds = 0;
+        } else {
+          this.form.breakResumeCount = null;
+          this.form.breakExpireSeconds = null;
+        }
+      },
+    },
   },
   watch: {
     "form.startEndTimeProxy": {
@@ -401,12 +445,12 @@ export default {
         }
         if (
           // 没动静,不修改,避免死循环
-          (v || []).includes("mobile_first") !==
-            (ov || []).includes("mobile_first") &&
-          !this.form.monitorVideoSource.includes("mobile_first")
+          (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"
+            (v) => v !== "MOBILE_SECOND"
           );
         }
       },
@@ -438,9 +482,7 @@ export default {
         openingSeconds: 0,
         minDurationSeconds: 0,
         forceFinish: 1,
-        enableShortCode: 1,
         shortCode: "",
-        enableBreak: 1,
         breakResumeCount: 0,
         breakExpireSeconds: 0,
         reexamAuditing: 0,
@@ -462,26 +504,41 @@ export default {
       },
       rules: {
         mode: { required: true, message: "必填" },
+        name: { required: true, message: "必填" },
+        code: { required: true, message: "必填" },
+        startEndTimeProxy: { required: true, message: "必填" },
         inProcessLivenessFixedRange: {
           validator: (rule, value) => {
             return new Promise((resolve, reject) => {
               const isNull = value === null;
               if (
-                isNull ||
+                (this.form.inProcessLivenessVerify === 0 && isNull) ||
                 (value.length === 2 &&
                   isNumber(value[0]) &&
                   isNumber(value[1]) &&
-                  value[0] < value[1])
+                  value[0] < value[1] &&
+                  value[0] > 0 &&
+                  value[1] < this.form.minDurationSeconds)
               ) {
-                resolve(); // reject with error message
+                resolve();
+                return;
+              }
+              if (this.form.inProcessLivenessVerify && value[0] <= 0) {
+                reject("开始时间必须大于0");
+              } else if (
+                this.form.inProcessLivenessVerify &&
+                value[1] >= this.form.minDurationSeconds
+              ) {
+                reject("结束时间必须小于冻结时间");
               } else {
-                reject("reject");
+                reject("格式错误");
               }
             });
           },
-          message: "格式错误",
+          // message: "格式错误",
         },
       },
+      loading: false,
     };
   },
   methods: {
@@ -495,9 +552,14 @@ export default {
         return;
       }
 
-      await saveExam(this.form);
-      this.$notify({ title: "保存成功", type: "success" });
-      this.$router.back();
+      try {
+        this.loading = true;
+        await saveExam(this.form);
+        this.$notify({ title: "保存成功", type: "success" });
+        this.$router.back();
+      } finally {
+        this.loading = false;
+      }
     },
     cancel() {
       this.$router.back();

+ 4 - 1
src/features/examwork/ExamManagement/ExamManagement.vue

@@ -19,7 +19,9 @@
             <StateSelect v-model="form.enableState"></StateSelect>
           </el-form-item>
           <el-form-item>
-            <el-button type="primary" @click="searchForm">查询</el-button>
+            <el-button type="primary" @click="handleCurrentChange(0)"
+              >查询</el-button
+            >
           </el-form-item>
         </el-form>
         <div class="part-filter-form-action">
@@ -109,6 +111,7 @@
     </el-table>
     <div class="part-page">
       <el-pagination
+        background
         @current-change="handleCurrentChange"
         :current-page="currentPage"
         :page-size="pageSize"

+ 4 - 1
src/features/examwork/ExamStudentImport/ExamStudentImport.vue

@@ -11,7 +11,9 @@
             <ExamSelect v-model="form.examId" />
           </el-form-item>
           <el-form-item>
-            <el-button type="primary" @click="searchForm">查询</el-button>
+            <el-button type="primary" @click="handleCurrentChange(0)"
+              >查询</el-button
+            >
           </el-form-item>
         </el-form>
 
@@ -79,6 +81,7 @@
     </el-table>
     <div class="part-page">
       <el-pagination
+        background
         @current-change="handleCurrentChange"
         :current-page="currentPage"
         :page-size="pageSize"

+ 18 - 10
src/features/examwork/ExamStudentImport/ExamStudentImportDialog.vue

@@ -19,7 +19,9 @@
         </el-form-item>
       </el-row>
       <el-row class="d-flex justify-content-center">
-        <el-button type="primary" @click="submitForm">导入</el-button>
+        <el-button type="primary" @click="submitForm" :loading="loading"
+          >导入</el-button
+        >
         <el-button @click="closeDialog">取消</el-button>
       </el-row>
     </el-form>
@@ -49,6 +51,7 @@ export default {
         fileName: "",
       },
       rules: {},
+      loading: false,
     };
   },
   methods: {
@@ -76,15 +79,20 @@ export default {
       const ab = await blobToArray(this.form.file);
       const md5 = MD5(ab);
 
-      await importExamStudent({
-        examId: this.examId,
-        file: this.form.file,
-        fileName: this.form.fileName,
-        md5,
-      });
-      this.$emit("reload");
-      this.$notify({ title: "导入任务已成功启动", type: "success" });
-      this.closeDialog();
+      try {
+        this.loading = true;
+        await importExamStudent({
+          examId: this.examId,
+          file: this.form.file,
+          fileName: this.form.fileName,
+          md5,
+        });
+        this.$emit("reload");
+        this.$notify({ title: "导入任务已成功启动", type: "success" });
+        this.closeDialog();
+      } finally {
+        this.loading = false;
+      }
     },
   },
 };

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

@@ -13,7 +13,7 @@
             <ActivitySelect :examId="form.examId" v-model="form.activityId" />
           </el-form-item>
           <el-form-item label="考场名称">
-            <ExamRoomSelect v-model="form.roomCode" />
+            <ExamRoomSelect :examId="form.examId" v-model="form.roomCode" />
           </el-form-item>
           <el-form-item label="科目">
             <CourseSelect :examId="form.examId" v-model="form.courseCode" />
@@ -31,7 +31,9 @@
             <el-input v-model.trim="form.classNo"></el-input>
           </el-form-item>
           <el-form-item>
-            <el-button type="primary" @click="searchForm">查询</el-button>
+            <el-button type="primary" @click="handleCurrentChange(0)"
+              >查询</el-button
+            >
           </el-form-item>
         </el-form>
 
@@ -109,6 +111,7 @@
     </el-table>
     <div class="part-page">
       <el-pagination
+        background
         @current-change="handleCurrentChange"
         :current-page="currentPage"
         :page-size="pageSize"
@@ -222,6 +225,7 @@ export default {
       await toggleEnableExamStudentArray(
         rows.map((v) => ({ id: v.id, enable }))
       );
+      this.$notify({ title: "操作成功", type: "success" });
       this.searchForm();
     },
   },

+ 20 - 7
src/features/examwork/ExamStudentManagement/ExamStudentManagementDialog.vue

@@ -29,7 +29,11 @@
       </el-row>
       <el-row>
         <el-form-item label="考场名称">
-          <ExamRoomSelect v-model="form.roomCode" styles="width: 100%" />
+          <ExamRoomSelect
+            :examId="form.examId"
+            v-model="form.roomCode"
+            styles="width: 100%"
+          />
         </el-form-item>
       </el-row>
       <el-row>
@@ -48,7 +52,7 @@
       </el-row>
       <el-row>
         <el-form-item label="证件号">
-          <el-input v-model.trim="form.identity"></el-input>
+          <el-input :disabled="isEdit" v-model.trim="form.identity"></el-input>
         </el-form-item>
       </el-row>
       <el-row>
@@ -70,7 +74,9 @@
         </el-form-item>
       </el-row>
       <el-row class="d-flex justify-content-center">
-        <el-button type="primary" @click="submitForm">保 存</el-button>
+        <el-button type="primary" @click="submitForm" :loading="loading"
+          >保 存</el-button
+        >
         <el-button @click="closeDialog">取 消</el-button>
       </el-row>
     </el-form>
@@ -87,7 +93,7 @@ export default {
   },
   computed: {
     isEdit() {
-      return this.examStudent.id;
+      return !!this.examStudent.id;
     },
   },
   data() {
@@ -107,6 +113,7 @@ export default {
       rules: {
         courseCode: { required: true, message: "必填" },
       },
+      loading: false,
     };
   },
   watch: {
@@ -147,9 +154,15 @@ export default {
       if (this.isEdit) {
         data = { ...data, id: this.examStudent.id };
       }
-      await saveExamStudent(data);
-      this.$emit("reload");
-      this.closeDialog();
+      try {
+        this.loading = true;
+        await saveExamStudent(data);
+        this.$emit("reload");
+        this.$notify({ title: "保存成功", type: "success" });
+        this.closeDialog();
+      } finally {
+        this.loading = false;
+      }
     },
   },
 };

+ 5 - 2
src/features/examwork/ImportExportTask/ImportExportTask.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="import-export-task">
     <div class="part-box-head">
-      <div class="part-box-head-left"><h1>预警提醒</h1></div>
+      <div class="part-box-head-left"><h1>导入导出任务</h1></div>
     </div>
     <div class="part-filter">
       <div class="part-filter-form">
@@ -11,7 +11,9 @@
             <TaskSelect v-model="form.type" />
           </el-form-item>
           <el-form-item>
-            <el-button type="primary" @click="searchForm">查询</el-button>
+            <el-button type="primary" @click="handleCurrentChange(0)"
+              >查询</el-button
+            >
           </el-form-item>
         </el-form>
       </div>
@@ -69,6 +71,7 @@
     </el-table>
     <div class="part-page">
       <el-pagination
+        background
         @current-change="handleCurrentChange"
         :current-page="currentPage"
         :page-size="pageSize"

+ 37 - 9
src/features/examwork/InvigilateManagement/InvigilateImportDialog.vue

@@ -19,7 +19,9 @@
         </el-form-item>
       </el-row>
       <el-row class="d-flex justify-content-center">
-        <el-button type="primary" @click="submitForm">导入</el-button>
+        <el-button type="primary" @click="submitForm" :loading="loading"
+          >导入</el-button
+        >
         <el-button @click="closeDialog">取消</el-button>
       </el-row>
     </el-form>
@@ -32,6 +34,9 @@ import MD5 from "js-md5";
 
 export default {
   name: "InvigilateImportDialog",
+  props: {
+    examId: String,
+  },
   data() {
     return {
       visible: false,
@@ -40,8 +45,25 @@ export default {
         fileName: "",
       },
       rules: {},
+      loading: false,
     };
   },
+  watch: {
+    examId: {
+      immediate: true,
+      handler() {
+        this.form = {
+          processPaper: false,
+          processAnswer: false,
+          objectiveShuffle: false,
+          optionShuffle: false,
+          audioPlayCount: 0,
+          file: "",
+          fileName: "",
+        };
+      },
+    },
+  },
   methods: {
     openDialog() {
       this.visible = true;
@@ -67,14 +89,20 @@ export default {
       const ab = await blobToArray(this.form.file);
       const md5 = MD5(ab);
 
-      await importInvigilator({
-        file: this.form.file,
-        fileName: this.form.fileName,
-        md5,
-      });
-      this.$emit("reload");
-      this.$notify({ title: "导入任务已成功启动", type: "success" });
-      this.closeDialog();
+      try {
+        this.loading = true;
+        await importInvigilator({
+          examId: this.examId,
+          file: this.form.file,
+          fileName: this.form.fileName,
+          md5,
+        });
+        this.$emit("reload");
+        this.$notify({ title: "导入任务已成功启动", type: "success" });
+        this.closeDialog();
+      } finally {
+        this.loading = false;
+      }
     },
   },
 };

+ 31 - 7
src/features/examwork/InvigilateManagement/InvigilateManagement.vue

@@ -5,17 +5,22 @@
     </div>
     <div class="part-filter">
       <div class="part-filter-form">
-        <el-form :model="form" inline>
-          <!-- <el-form-item v-if="$store.state.user.orgId === null" label="机构">
-        <OrgSelect v-model="form.orgId"></OrgSelect>
-      </el-form-item> -->
+        <el-form ref="form" :model="form" :rules="rules" inline>
+          <el-form-item label="批次名称" prop="examId">
+            <ExamSelect v-model="form.examId" />
+          </el-form-item>
           <el-form-item label="考场">
-            <ExamRoomSelect v-model="form.roomCode"></ExamRoomSelect>
+            <ExamRoomSelect
+              :examId="form.examId"
+              v-model="form.roomCode"
+            ></ExamRoomSelect>
           </el-form-item>
           <el-form-item label="监考老师">
             <InvigilatorSelect v-model="form.userId"></InvigilatorSelect>
           </el-form-item>
-          <el-button type="primary" @click="searchForm">查询</el-button>
+          <el-button type="primary" @click="handleCurrentChange(0)"
+            >查询</el-button
+          >
         </el-form>
         <div class="part-filter-form-action">
           <el-button
@@ -43,6 +48,12 @@
       <el-table-column width="100" label="ID">
         <span slot-scope="scope">{{ scope.row.id }}</span>
       </el-table-column> -->
+      <el-table-column label="批次ID">
+        <span slot-scope="scope">{{ scope.row.examId }}</span>
+      </el-table-column>
+      <el-table-column label="批次名称">
+        <span slot-scope="scope">{{ scope.row.examName }}</span>
+      </el-table-column>
       <el-table-column width="200" label="考场编码">
         <span slot-scope="scope">{{ scope.row.roomCode }}</span>
       </el-table-column>
@@ -62,6 +73,7 @@
     </el-table>
     <div class="page float-right">
       <el-pagination
+        background
         @current-change="handleCurrentChange"
         :current-page="currentPage"
         :page-size="pageSize"
@@ -102,9 +114,13 @@ export default {
   data() {
     return {
       form: {
+        examId: "",
         roomCode: "",
         userId: "",
       },
+      rules: {
+        examId: [{ required: true, message: "批次必选" }],
+      },
       tableData: [],
       currentPage: 1,
       pageSize: 10,
@@ -119,6 +135,7 @@ export default {
   methods: {
     async searchForm() {
       const res = await searchInvigilators({
+        examId: this.form.examId,
         userId: this.form.userId,
         roomCode: this.form.roomCode,
         pageNumber: this.currentPage,
@@ -140,7 +157,14 @@ export default {
       this.selectedUser = user;
       this.$refs.theDialog.openDialog();
     },
-    importDialog() {
+    async importDialog() {
+      try {
+        const valid = await this.$refs.form.validate();
+        if (!valid) return;
+      } catch (error) {
+        console.log(error);
+        return;
+      }
       this.$refs.theDialog2.openDialog();
     },
     exportInvigilate() {

+ 16 - 6
src/features/examwork/InvigilateManagement/InvigilateManagementDialog.vue

@@ -16,11 +16,12 @@
       <el-row>
         <el-form-item label="考场">
           <ExamRoomSelect
+            :examId="user.examId"
             v-model="form.roomCode"
             style="width: 100%;"
           ></ExamRoomSelect>
         </el-form-item>
-        <el-form-item label="考场">
+        <el-form-item label="监考老师">
           <InvigilatorSelect
             v-model="form.userIds"
             style="width: 100%;"
@@ -30,7 +31,9 @@
       </el-row>
 
       <el-row class="d-flex justify-content-center">
-        <el-button type="primary" @click="submitForm">保 存</el-button>
+        <el-button type="primary" @click="submitForm" :loading="loading"
+          >保 存</el-button
+        >
         <el-button @click="closeDialog">取 消</el-button>
       </el-row>
     </el-form>
@@ -50,7 +53,7 @@ export default {
       handler(val) {
         this.form.roomCode = val.roomCode;
         const u = val.userId || "";
-        this.form.userIds = u.split(",");
+        this.form.userIds = u.split(",").filter((v) => v);
       },
     },
   },
@@ -62,6 +65,7 @@ export default {
         userIds: [],
       },
       rules: {},
+      loading: false,
     };
   },
   methods: {
@@ -73,9 +77,15 @@ export default {
     },
     async submitForm() {
       let data = this.form;
-      await saveInvigilator(data);
-      this.$emit("reload");
-      this.closeDialog();
+      try {
+        this.loading = true;
+        await saveInvigilator({ examId: this.user.examId, ...data });
+        this.$emit("reload");
+        this.$notify({ title: "保存成功", type: "success" });
+        this.closeDialog();
+      } finally {
+        this.loading = false;
+      }
     },
   },
 };

+ 4 - 1
src/features/examwork/StudentManagement/StudentManagement.vue

@@ -16,7 +16,9 @@
             <StateSelect v-model="form.enable"></StateSelect>
           </el-form-item>
           <el-form-item>
-            <el-button type="primary" @click="searchForm">查询</el-button>
+            <el-button type="primary" @click="handleCurrentChange(0)"
+              >查询</el-button
+            >
           </el-form-item>
         </el-form>
       </div>
@@ -94,6 +96,7 @@
     </el-table>
     <div class="part-page">
       <el-pagination
+        background
         @current-change="handleCurrentChange"
         :current-page="currentPage"
         :page-size="pageSize"

+ 2 - 1
src/features/examwork/StudentManagement/StudentManagementDialog.vue

@@ -17,7 +17,7 @@
       <el-form-item label="批次名称" prop="examId">
         <ExamSelect v-model="form.examId" />
       </el-form-item>
-      <el-button type="primary" @click="searchForm">查询</el-button>
+      <el-button type="primary" @click="handleCurrentChange(0)">查询</el-button>
     </el-form>
 
     <el-table :data="tableData" stripe style="width: 100%;">
@@ -39,6 +39,7 @@
     </el-table>
     <div class="page float-right">
       <el-pagination
+        background
         @current-change="handleCurrentChange"
         :current-page="currentPage"
         :page-size="pageSize"

+ 18 - 27
src/features/invigilation/ExamInvigilation/ExamInvigilation.vue

@@ -124,7 +124,7 @@
               </h3>
               <echart-render
                 :chart-data="orgWarningData"
-                chart-type="pie"
+                chart-type="pieAnnulus"
                 v-if="chartDataReady"
               ></echart-render>
             </div>
@@ -144,7 +144,7 @@
               </h3>
               <echart-render
                 :chart-data="typeWarningData"
-                chart-type="pie"
+                chart-type="pieAnnulus"
                 v-if="chartDataReady"
               ></echart-render>
             </div>
@@ -206,30 +206,6 @@
         :key="item.examStudentId"
         :data="item"
       ></invigilation-student>
-      <div class="invigilation-student invigilation-student-warning">
-        <div class="student-video"></div>
-        <div class="student-info">
-          <h6><span>刘西西</span><i class="icon icon-net-break"></i></h6>
-          <p><span>证件号:</span><span>000000000000000008</span></p>
-          <p><span>答题进度:</span><span>20%</span></p>
-          <div class="student-time">
-            <i class="el-icon-alarm-clock"></i>
-            <span>50:32:15</span>
-          </div>
-        </div>
-      </div>
-      <div class="invigilation-student invigilation-student-netbreak">
-        <div class="student-video"></div>
-        <div class="student-info">
-          <h6><span>刘西西</span><i class="icon icon-net-break"></i></h6>
-          <p><span>证件号:</span><span>000000000000000008</span></p>
-          <p><span>答题进度:</span><span>20%</span></p>
-          <div class="student-time">
-            <i class="el-icon-alarm-clock"></i>
-            <span>50:32:15</span>
-          </div>
-        </div>
-      </div>
     </div>
   </div>
 </template>
@@ -360,7 +336,22 @@ export default {
         type: "light",
       },
       chartDataReady: false,
-      students: [],
+      students: [
+        {
+          name: "刘西西",
+          identity: "000000000000000008",
+          progress: "52%",
+          warning: false,
+          netbreak: true,
+        },
+        {
+          name: "刘西西",
+          identity: "000000000000000008",
+          progress: "52%",
+          warning: true,
+          netbreak: false,
+        },
+      ],
     };
   },
   computed: {

+ 92 - 0
src/features/invigilation/ExamReport/BreachDetailDialog.vue

@@ -0,0 +1,92 @@
+<template>
+  <el-dialog
+    class="exception-detail-dialog"
+    :visible.sync="modalIsShow"
+    title="违纪明细"
+    width="1000px"
+    top="94px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @open="visibleChange"
+  >
+    <el-table ref="TableList" :data="dataList">
+      <el-table-column
+        prop="creatTime"
+        label="违纪处理/撤销违纪"
+      ></el-table-column>
+      <el-table-column prop="type" label="操作人"></el-table-column>
+      <el-table-column prop="type" label="异常处理结束时间"></el-table-column>
+      <el-table-column prop="content" label="类型/原因"></el-table-column>
+      <el-table-column prop="content" label="详情备注"></el-table-column>
+    </el-table>
+    <div class="part-page">
+      <el-pagination
+        background
+        layout="prev, pager, next,total,sizes,jumper"
+        :current-page="current"
+        :total="total"
+        :page-size="size"
+        @size-change="toPage(1)"
+        @current-change="toPage"
+      >
+      </el-pagination>
+    </div>
+    <div slot="footer"></div>
+  </el-dialog>
+</template>
+
+<script>
+// import { updateCourse } from "../api";
+
+export default {
+  name: "exception-detail-dialog",
+  props: {
+    detailId: {
+      type: [String, Number],
+      required: true,
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      current: 1,
+      total: 0,
+      size: 10,
+      dataList: [
+        {
+          id: 1,
+          creatTime: "2020-08-02 15:12:24",
+          type: "进入考试",
+          content: "MAC地址:",
+        },
+        {
+          id: 2,
+          creatTime: "2020-08-02 15:12:24",
+          type: "网络断开",
+          content: "系统自动重试2次,网络已恢复",
+        },
+        {
+          id: 3,
+          creatTime: "2020-08-02 15:12:24",
+          type: "重新登录",
+          content: "MAC地址:",
+        },
+      ],
+    };
+  },
+  methods: {
+    visibleChange() {
+      this.getList();
+    },
+    getList() {},
+    toPage() {},
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+  },
+};
+</script>

+ 224 - 5
src/features/invigilation/ExamReport/ExamReport.vue

@@ -1,15 +1,234 @@
 <template>
-  <div class="ExamReport">
-    ExamReport
+  <div class="exam-report">
+    <div class="exam-report-head">
+      <h1>考情综合报表分析</h1>
+      <div class="part-filter-form">
+        <el-form ref="FilterForm" label-position="left" inline>
+          <el-form-item>
+            <el-select
+              v-model="filter.examId"
+              placeholder="请选择批次"
+              @change="examChange"
+            >
+              <el-option
+                v-for="item in examBatchs"
+                :key="item.id"
+                :value="item.id"
+                :label="item.name"
+              ></el-option>
+            </el-select>
+          </el-form-item>
+          <el-form-item>
+            <el-select
+              v-model="filter.roomCode"
+              placeholder="请选择考场"
+              clearable
+            >
+              <el-option
+                v-for="item in examRooms"
+                :key="item.roomCode"
+                :value="item.roomCode"
+                :label="item.roomName"
+              ></el-option>
+            </el-select>
+          </el-form-item>
+          <el-form-item>
+            <el-select
+              v-model="filter.courseCode"
+              placeholder="请选择科目"
+              clearable
+            >
+              <el-option
+                v-for="item in examCourses"
+                :key="item.courseCode"
+                :value="item.courseCode"
+                :label="item.courseName"
+              ></el-option>
+            </el-select>
+          </el-form-item>
+          <el-form-item>
+            <el-input
+              v-model.trim="filter.name"
+              placeholder="姓名/证件号"
+              clearable
+            ></el-input>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="search">查询</el-button>
+          </el-form-item>
+        </el-form>
+        <div class="part-filter-form-action">
+          <el-button type="primary" icon="icon icon-download" @click="toExport"
+            >导出统计报表</el-button
+          >
+        </div>
+      </div>
+      <div class="exam-report-tabs">
+        <el-button
+          size="medium"
+          v-for="item in tabs"
+          :key="item.key"
+          :type="item.key === curTab.key ? 'primary' : 'default'"
+          round
+          @click="selectTab(item)"
+          >{{ item.name }}</el-button
+        >
+      </div>
+    </div>
+    <div class="exam-report-body">
+      <component
+        :is="curComponent"
+        :filter="filter"
+        ref="ReportDetail"
+      ></component>
+    </div>
   </div>
 </template>
 
 <script>
+import { examBatchList, examActivityRoomList } from "@/api/invigilation";
+import ReportOverview from "./ReportOverview";
+import ReportStatistics from "./ReportStatistics";
+import ReportAbsent from "./ReportAbsent";
+import ReportException from "./ReportException";
+import ReportReexam from "./ReportReexam";
+import ReportBreach from "./ReportBreach";
+import ReportCancalBreach from "./ReportCancalBreach";
+
 export default {
-  name: "ExamReport",
+  name: "exam-report",
+  components: {
+    ReportOverview,
+    ReportStatistics,
+    ReportAbsent,
+    ReportException,
+    ReportReexam,
+    ReportBreach,
+    ReportCancalBreach,
+  },
   data() {
-    return {};
+    return {
+      filter: {
+        examId: null,
+        roomCode: null,
+        courseCode: null,
+        name: "",
+      },
+      examBatchs: [],
+      examActivities: [],
+      examRooms: [],
+      examCourses: [],
+      curTab: { key: "" },
+      tabs: [
+        {
+          key: "overview",
+          name: "考试概况",
+        },
+        {
+          key: "statistics",
+          name: "情况统计",
+        },
+        {
+          key: "absent",
+          name: "缺考名单",
+        },
+        {
+          key: "exception",
+          name: "异常处理明细",
+        },
+        {
+          key: "reexam",
+          name: "重考处理明细",
+        },
+        {
+          key: "breach",
+          name: "违纪名单",
+        },
+        {
+          key: "cancal-breach",
+          name: "撤销违纪名单",
+        },
+      ],
+    };
+  },
+  computed: {
+    curComponent() {
+      return this.curTab.key ? `report-${this.curTab.key}` : null;
+    },
+  },
+  mounted() {
+    this.initData();
+  },
+  methods: {
+    async initData() {
+      await this.getExamBatchList();
+      this.filter.examId = this.examBatchs[0] && this.examBatchs[0].id;
+      this.getExamActivityRoomList();
+      this.curTab = { ...this.tabs[0] };
+    },
+    search() {
+      this.$refs.ReportDetail.getData();
+    },
+    selectTab(tab) {
+      this.curTab = { ...tab };
+    },
+    async getExamBatchList() {
+      const res = await examBatchList();
+      this.examBatchs = res.data.data;
+    },
+    async getExamActivityRoomList() {
+      const res = await examActivityRoomList(this.filter.examId);
+      this.examActivities = res.data.data.examActivitys;
+      this.examRooms = res.data.data.examRooms;
+      this.examCourses = res.data.data.examCourses;
+    },
+    examChange() {
+      this.filter.examActivityId = null;
+      this.filter.roomCode = null;
+      this.filter.courseCode = null;
+      this.getExamActivityRoomList();
+    },
+    toExport() {},
   },
-  methods: {},
 };
 </script>
+
+<style lang="scss" scoped>
+.exam-report-head {
+  background: #fff;
+  margin: -30px -30px 30px -30px;
+  padding: 30px;
+
+  > h1 {
+    font-size: 18px;
+    font-weight: 600;
+    color: #202b4b;
+    line-height: 25px;
+    height: 45px;
+    border-bottom: 1px solid #f0f4f9;
+    margin: 0;
+  }
+  .part-filter-form {
+    border-bottom: 1px solid #f0f4f9;
+    padding: 20px 0 10px;
+  }
+  .exam-report-tabs {
+    padding-top: 20px;
+
+    .el-button--default {
+      background: #f0f4f9;
+      color: #626a82;
+      border-color: #f0f4f9;
+
+      &:hover {
+        color: #202b4b;
+      }
+    }
+
+    button:focus {
+      border: none;
+      outline: none;
+    }
+  }
+}
+</style>

+ 108 - 0
src/features/invigilation/ExamReport/ExceptionDetailDialog.vue

@@ -0,0 +1,108 @@
+<template>
+  <el-dialog
+    class="exception-detail-dialog"
+    :visible.sync="modalIsShow"
+    title="异常处理明细"
+    width="1000px"
+    top="94px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @open="visibleChange"
+  >
+    <el-table ref="TableList" :data="dataList">
+      <el-table-column
+        prop="creatTime"
+        label="异常处理开始时间"
+      ></el-table-column>
+      <el-table-column prop="type" label="异常处理结束时间"></el-table-column>
+      <el-table-column prop="content" label="异常原因"></el-table-column>
+      <el-table-column
+        prop="content"
+        label="持续时长(单位:分钟)"
+      ></el-table-column>
+    </el-table>
+    <div class="part-page">
+      <el-pagination
+        background
+        layout="prev, pager, next,total,sizes,jumper"
+        :current-page="current"
+        :total="total"
+        :page-size="size"
+        @size-change="toPage(1)"
+        @current-change="toPage"
+      >
+      </el-pagination>
+    </div>
+    <div slot="footer"></div>
+  </el-dialog>
+</template>
+
+<script>
+import { reportExceptionDetail } from "@/api/invigilation";
+
+export default {
+  name: "exception-detail-dialog",
+  props: {
+    detailId: {
+      type: [String, Number],
+      required: true,
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      current: 1,
+      total: 0,
+      size: 10,
+      dataList: [
+        {
+          id: 1,
+          creatTime: "2020-08-02 15:12:24",
+          type: "进入考试",
+          content: "MAC地址:",
+        },
+        {
+          id: 2,
+          creatTime: "2020-08-02 15:12:24",
+          type: "网络断开",
+          content: "系统自动重试2次,网络已恢复",
+        },
+        {
+          id: 3,
+          creatTime: "2020-08-02 15:12:24",
+          type: "重新登录",
+          content: "MAC地址:",
+        },
+      ],
+    };
+  },
+  methods: {
+    visibleChange() {
+      this.toPage(1);
+    },
+    async getList() {
+      const datas = {
+        examStudentId: this.detailId,
+        pageNumber: this.current - 1,
+        pageSize: this.size,
+      };
+
+      const res = await reportExceptionDetail(datas);
+
+      this.dataList = res.data.data.records;
+      this.total = res.data.data.records.total;
+    },
+    toPage(page) {
+      this.current = page;
+      this.getList();
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+  },
+};
+</script>

+ 81 - 0
src/features/invigilation/ExamReport/ReportAbsent.vue

@@ -0,0 +1,81 @@
+<template>
+  <div class="report-absent">
+    <el-table ref="TableList" :data="dataList">
+      <el-table-column type="index" label="排序"></el-table-column>
+      <el-table-column prop="examName" label="批次名称(ID)"></el-table-column>
+      <el-table-column prop="examActivity" label="场次ID"></el-table-column>
+      <el-table-column prop="examroom" label="考场名称(代码)">
+        <span slot-scope="scope"
+          >{{ scope.row.roomName }}({{ scope.row.roomCode }})</span
+        >
+      </el-table-column>
+      <el-table-column prop="identity" label="证件号"></el-table-column>
+      <el-table-column prop="name" label="姓名"></el-table-column>
+      <el-table-column prop="courseCode" label="科目名称(代码)">
+        <span slot-scope="scope"
+          >{{ scope.row.courseName }}({{ scope.row.courseCode }})</span
+        ></el-table-column
+      >
+    </el-table>
+    <div class="part-page">
+      <el-pagination
+        background
+        layout="prev, pager, next,total,sizes,jumper"
+        :current-page="current"
+        :total="total"
+        :page-size="size"
+        @size-change="toPage(1)"
+        @current-change="toPage"
+      >
+      </el-pagination>
+    </div>
+  </div>
+</template>
+
+<script>
+import { reportAbsentData } from "@/api/invigilation";
+
+export default {
+  name: "report-absent",
+  props: {
+    filter: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      current: 1,
+      total: 0,
+      size: 10,
+      dataList: [],
+    };
+  },
+  mounted() {
+    this.getData();
+  },
+  methods: {
+    async getList() {
+      const datas = {
+        ...this.filter,
+        pageNumber: this.current - 1,
+        pageSize: this.size,
+      };
+
+      const res = await reportAbsentData(datas);
+
+      this.dataList = res.data.data.records;
+      this.total = res.data.data.records.total;
+    },
+    toPage(page) {
+      this.current = page;
+      this.getList();
+    },
+    getData() {
+      this.toPage(1);
+    },
+  },
+};
+</script>

+ 82 - 0
src/features/invigilation/ExamReport/ReportBreach.vue

@@ -0,0 +1,82 @@
+<template>
+  <div class="report-breach">
+    <el-table ref="TableList" :data="dataList">
+      <el-table-column type="index" label="排序"></el-table-column>
+      <el-table-column prop="examName" label="批次名称(ID)"></el-table-column>
+      <el-table-column prop="examActivity" label="场次ID"></el-table-column>
+      <el-table-column prop="examroom" label="考场名称(代码)">
+        <span slot-scope="scope"
+          >{{ scope.row.roomName }}({{ scope.row.roomCode }})</span
+        >
+      </el-table-column>
+      <el-table-column prop="identity" label="证件号"></el-table-column>
+      <el-table-column prop="name" label="姓名"></el-table-column>
+      <el-table-column prop="courseCode" label="科目名称(代码)">
+        <span slot-scope="scope"
+          >{{ scope.row.courseName }}({{ scope.row.courseCode }})</span
+        ></el-table-column
+      >
+      <el-table-column prop="name" label="违纪/正常"></el-table-column>
+    </el-table>
+    <div class="part-page">
+      <el-pagination
+        background
+        layout="prev, pager, next,total,sizes,jumper"
+        :current-page="current"
+        :total="total"
+        :page-size="size"
+        @size-change="toPage(1)"
+        @current-change="toPage"
+      >
+      </el-pagination>
+    </div>
+  </div>
+</template>
+
+<script>
+import { reexamCheckedList } from "@/api/invigilation";
+
+export default {
+  name: "report-breach",
+  props: {
+    filter: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      current: 1,
+      total: 0,
+      size: 10,
+      dataList: [],
+    };
+  },
+  mounted() {
+    this.getData();
+  },
+  methods: {
+    async getList() {
+      const datas = {
+        ...this.filter,
+        pageNumber: this.current - 1,
+        pageSize: this.size,
+      };
+
+      const res = await reexamCheckedList(datas);
+
+      this.dataList = res.data.data.records;
+      this.total = res.data.data.records.total;
+    },
+    toPage(page) {
+      this.current = page;
+      this.getList();
+    },
+    getData() {
+      this.toPage(1);
+    },
+  },
+};
+</script>

+ 82 - 0
src/features/invigilation/ExamReport/ReportCancalBreach.vue

@@ -0,0 +1,82 @@
+<template>
+  <div class="report-cancel-breach">
+    <el-table ref="TableList" :data="dataList">
+      <el-table-column type="index" label="排序"></el-table-column>
+      <el-table-column prop="examName" label="批次名称(ID)"></el-table-column>
+      <el-table-column prop="examActivity" label="场次ID"></el-table-column>
+      <el-table-column prop="examroom" label="考场名称(代码)">
+        <span slot-scope="scope"
+          >{{ scope.row.roomName }}({{ scope.row.roomCode }})</span
+        >
+      </el-table-column>
+      <el-table-column prop="identity" label="证件号"></el-table-column>
+      <el-table-column prop="name" label="姓名"></el-table-column>
+      <el-table-column prop="courseCode" label="科目名称(代码)">
+        <span slot-scope="scope"
+          >{{ scope.row.courseName }}({{ scope.row.courseCode }})</span
+        ></el-table-column
+      >
+      <el-table-column prop="name" label="违纪/正常"></el-table-column>
+    </el-table>
+    <div class="part-page">
+      <el-pagination
+        background
+        layout="prev, pager, next,total,sizes,jumper"
+        :current-page="current"
+        :total="total"
+        :page-size="size"
+        @size-change="toPage(1)"
+        @current-change="toPage"
+      >
+      </el-pagination>
+    </div>
+  </div>
+</template>
+
+<script>
+import { reexamCheckedList } from "@/api/invigilation";
+
+export default {
+  name: "report-cancel-breach",
+  props: {
+    filter: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  mounted() {
+    this.getData();
+  },
+  data() {
+    return {
+      current: 1,
+      total: 0,
+      size: 10,
+      dataList: [],
+    };
+  },
+  methods: {
+    async getList() {
+      const datas = {
+        ...this.filter,
+        pageNumber: this.current - 1,
+        pageSize: this.size,
+      };
+
+      const res = await reexamCheckedList(datas);
+
+      this.dataList = res.data.data.records;
+      this.total = res.data.data.records.total;
+    },
+    toPage(page) {
+      this.current = page;
+      this.getList();
+    },
+    getData() {
+      this.toPage(1);
+    },
+  },
+};
+</script>

+ 86 - 0
src/features/invigilation/ExamReport/ReportException.vue

@@ -0,0 +1,86 @@
+<template>
+  <div class="report-exception">
+    <el-table ref="TableList" :data="dataList">
+      <el-table-column type="index" label="排序"></el-table-column>
+      <el-table-column prop="examName" label="批次名称(ID)"></el-table-column>
+      <el-table-column prop="examActivity" label="场次ID"></el-table-column>
+      <el-table-column prop="examroom" label="考场名称(代码)">
+        <span slot-scope="scope"
+          >{{ scope.row.roomName }}({{ scope.row.roomCode }})</span
+        >
+      </el-table-column>
+      <el-table-column prop="identity" label="证件号"></el-table-column>
+      <el-table-column prop="name" label="姓名"></el-table-column>
+      <el-table-column prop="courseCode" label="科目名称(代码)">
+        <span slot-scope="scope"
+          >{{ scope.row.courseName }}({{ scope.row.courseCode }})</span
+        ></el-table-column
+      >
+      <el-table-column prop="name" label="次数"></el-table-column>
+      <el-table-column
+        prop="name"
+        label="累积持续时长(单位:分钟)"
+      ></el-table-column>
+    </el-table>
+    <div class="part-page">
+      <el-pagination
+        background
+        layout="prev, pager, next,total,sizes,jumper"
+        :current-page="current"
+        :total="total"
+        :page-size="size"
+        @size-change="toPage(1)"
+        @current-change="toPage"
+      >
+      </el-pagination>
+    </div>
+  </div>
+</template>
+
+<script>
+import { reportExceptionData } from "@/api/invigilation";
+
+export default {
+  name: "report-exception",
+  props: {
+    filter: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      current: 1,
+      total: 0,
+      size: 10,
+      dataList: [],
+    };
+  },
+  mounted() {
+    this.getData();
+  },
+  methods: {
+    async getList() {
+      const datas = {
+        ...this.filter,
+        pageNumber: this.current - 1,
+        pageSize: this.size,
+      };
+
+      const res = await reportExceptionData(datas);
+
+      this.dataList = res.data.data.records;
+      this.total = res.data.data.records.total;
+    },
+    toPage(page) {
+      this.current = page;
+      this.getList();
+    },
+    getData() {
+      this.toPage(1);
+    },
+  },
+};
+</script>

+ 258 - 0
src/features/invigilation/ExamReport/ReportOverview.vue

@@ -0,0 +1,258 @@
+<template>
+  <div class="report-overview">
+    <el-row :gutter="20" type="flex">
+      <el-col :span="6">
+        <div class="overview-yk part-box">
+          <div class="overview-yk-info">
+            <h5>应考</h5>
+            <p>8000</p>
+            <p>(单位:科次)</p>
+          </div>
+        </div>
+      </el-col>
+      <el-col :span="5">
+        <div class="overview-detail">
+          <div class="overview-detail-item part-box">
+            <i class="overview-detail-line line-blue"></i>
+            <h5>实考 (科次)</h5>
+            <p>4852</p>
+          </div>
+          <div class="overview-detail-item part-box">
+            <i class="overview-detail-line line-yellow"></i>
+            <h5>缺考 (科次)</h5>
+            <p>745</p>
+          </div>
+          <div class="overview-detail-item part-box">
+            <i class="overview-detail-line line-cyan"></i>
+            <h5>未完成 (科次)</h5>
+            <p>2450</p>
+          </div>
+        </div>
+      </el-col>
+      <el-col :span="13">
+        <div class="overview-progress overview-chart part-box">
+          <h3 class="overview-part-title">
+            <span>考试总体进度</span>
+            <el-popover
+              placement="top-start"
+              width="200"
+              trigger="hover"
+              content="这是一段内容,这是一段内容,这是一段内容,这是一段内容。"
+            >
+              <i class="el-icon-question" slot="reference"></i>
+            </el-popover>
+          </h3>
+          <echart-render
+            :chart-data="progressData"
+            chart-type="pie"
+            v-if="chartDataReady"
+          ></echart-render>
+        </div>
+      </el-col>
+    </el-row>
+    <div class="overview-distribution overview-chart part-box">
+      <h3 class="overview-part-title">
+        <span>考试科次时间分布</span>
+        <el-popover
+          placement="top-start"
+          width="200"
+          trigger="hover"
+          content="这是一段内容,这是一段内容,这是一段内容,这是一段内容。"
+        >
+          <i class="el-icon-question" slot="reference"></i>
+        </el-popover>
+      </h3>
+      <echart-render
+        :chart-data="distributionData"
+        chart-type="lineMark"
+        v-if="chartDataReady"
+      ></echart-render>
+    </div>
+  </div>
+</template>
+
+<script>
+import { reportOverviewData } from "@/api/invigilation";
+import EchartRender from "../common/EchartRender";
+
+export default {
+  name: "report-overview",
+  components: { EchartRender },
+  props: {
+    filter: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      infos: {},
+      chartDataReady: true,
+      progressData: [
+        {
+          name: "实考",
+          count: 60,
+        },
+        {
+          name: "缺考",
+          count: 10,
+        },
+        {
+          name: "未完成",
+          count: 40,
+        },
+      ],
+      distributionData: [
+        {
+          name: "6月1号",
+          count: 13,
+        },
+        {
+          name: "6月2号",
+          count: 16,
+        },
+        {
+          name: "6月3号",
+          count: 20,
+        },
+        {
+          name: "6月4号",
+          count: 16,
+        },
+        {
+          name: "6月5号",
+          count: 10,
+        },
+        {
+          name: "6月6号",
+          count: 16,
+        },
+      ],
+    };
+  },
+  mounted() {
+    // this.getData();
+  },
+  methods: {
+    async getData() {
+      const datas = {
+        ...this.filter,
+      };
+
+      const res = await reportOverviewData(datas);
+
+      this.infos = res.data.data;
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.overview-yk {
+  height: 100%;
+  min-width: 258px;
+  min-height: 258px;
+  position: relative;
+
+  .overview-yk-info {
+    width: 258px;
+    height: 258px;
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    margin-left: -129px;
+    margin-top: -129px;
+    text-align: center;
+    background-image: url(../../../assets/bg-liu.png);
+    background-size: 100% 100%;
+    background-repeat: no-repeat;
+    color: #ffffff;
+
+    > h5 {
+      font-size: 16px;
+      font-weight: 600;
+      line-height: 22px;
+      margin-top: 69px;
+      margin-bottom: 0;
+    }
+
+    > p:nth-of-type(1) {
+      font-size: 56px;
+      font-weight: 600;
+      line-height: 78px;
+      margin: 0;
+    }
+    > p:nth-of-type(2) {
+      font-size: 12px;
+      font-weight: 400;
+      line-height: 17px;
+    }
+  }
+}
+.overview-detail-item {
+  position: relative;
+  padding-bottom: 16px;
+
+  &:last-child {
+    margin-bottom: 0;
+  }
+  > h5 {
+    font-size: 14px;
+    font-weight: 400;
+    color: #626a82;
+    line-height: 21px;
+    margin: 0;
+  }
+  > p {
+    font-size: 32px;
+    font-weight: 600;
+    color: #202b4b;
+    line-height: 45px;
+    margin: 0;
+  }
+}
+.overview-detail-line {
+  position: absolute;
+  width: 41px;
+  height: 13px;
+  top: 50%;
+  right: 20px;
+  margin-top: -7px;
+  z-index: auto;
+  background-size: 100% 100%;
+
+  &.line-blue {
+    background-image: url(../../../assets/bg-line-blue.png);
+  }
+  &.line-yellow {
+    background-image: url(../../../assets/bg-line-yellow.png);
+  }
+  &.line-cyan {
+    background-image: url(../../../assets/bg-line-cyan.png);
+  }
+}
+.overview-part-title {
+  font-size: 16px;
+  font-weight: 500;
+  color: #202b4b;
+  line-height: 22px;
+}
+.overview-progress {
+  height: 346px;
+  margin-bottom: 0;
+}
+.overview-distribution {
+  margin-top: 20px;
+  height: 326px;
+}
+.overview-chart {
+  position: relative;
+  padding-bottom: 5px;
+  .overview-part-title {
+    position: absolute;
+    top: 20px;
+  }
+}
+</style>

+ 81 - 0
src/features/invigilation/ExamReport/ReportReexam.vue

@@ -0,0 +1,81 @@
+<template>
+  <div class="report-reexam">
+    <el-table ref="TableList" :data="dataList">
+      <el-table-column type="index" label="排序"></el-table-column>
+      <el-table-column prop="examName" label="批次名称(ID)"></el-table-column>
+      <el-table-column prop="examActivity" label="场次ID"></el-table-column>
+      <el-table-column prop="examroom" label="考场名称(代码)">
+        <span slot-scope="scope"
+          >{{ scope.row.roomName }}({{ scope.row.roomCode }})</span
+        >
+      </el-table-column>
+      <el-table-column prop="identity" label="证件号"></el-table-column>
+      <el-table-column prop="name" label="姓名"></el-table-column>
+      <el-table-column prop="courseCode" label="科目名称(代码)">
+        <span slot-scope="scope"
+          >{{ scope.row.courseName }}({{ scope.row.courseCode }})</span
+        ></el-table-column
+      >
+    </el-table>
+    <div class="part-page">
+      <el-pagination
+        background
+        layout="prev, pager, next,total,sizes,jumper"
+        :current-page="current"
+        :total="total"
+        :page-size="size"
+        @size-change="toPage(1)"
+        @current-change="toPage"
+      >
+      </el-pagination>
+    </div>
+  </div>
+</template>
+
+<script>
+import { reportReexamData } from "@/api/invigilation";
+
+export default {
+  name: "report-reexam",
+  props: {
+    filter: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      current: 1,
+      total: 0,
+      size: 10,
+      dataList: [],
+    };
+  },
+  mounted() {
+    this.getData();
+  },
+  methods: {
+    async getList() {
+      const datas = {
+        ...this.filter,
+        pageNumber: this.current - 1,
+        pageSize: this.size,
+      };
+
+      const res = await reportReexamData(datas);
+
+      this.dataList = res.data.data.records;
+      this.total = res.data.data.records.total;
+    },
+    toPage(page) {
+      this.current = page;
+      this.getList();
+    },
+    getData() {
+      this.toPage(1);
+    },
+  },
+};
+</script>

+ 104 - 0
src/features/invigilation/ExamReport/ReportStatistics.vue

@@ -0,0 +1,104 @@
+<template>
+  <div class="report-statistics">
+    <el-table ref="TableList" :data="dataList">
+      <el-table-column type="index" label="排序"></el-table-column>
+      <el-table-column prop="examName" label="批次名称(ID)"></el-table-column>
+      <el-table-column prop="examActivity" label="场次ID"></el-table-column>
+      <el-table-column prop="examroom" label="考场名称(代码)">
+        <span slot-scope="scope"
+          >{{ scope.row.roomName }}({{ scope.row.roomCode }})</span
+        >
+      </el-table-column>
+      <el-table-column prop="name" label="应考(科次)">
+        <template slot-scope="scope">
+          <span class="color-primary">{{ scope.row.name }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="courseCode" label="实考(科次)">
+        <template slot-scope="scope">
+          <span class="color-success">{{ scope.row.name }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="reasom" label="缺考(科次)">
+        <template slot-scope="scope">
+          <span class="color-danger">{{ scope.row.name }}</span>
+        </template>
+      </el-table-column>
+    </el-table>
+    <div class="part-page">
+      <div class="page-info stat-page-info">
+        共计<span>应考8000</span>科次,<span>实考748分</span>科次,<span>缺考7854</span>科次
+      </div>
+      <el-pagination
+        background
+        layout="prev, pager, next,total,sizes,jumper"
+        :current-page="current"
+        :total="total"
+        :page-size="size"
+        @size-change="toPage(1)"
+        @current-change="toPage"
+      >
+      </el-pagination>
+    </div>
+  </div>
+</template>
+
+<script>
+import { reportStatisticsData } from "@/api/invigilation";
+
+export default {
+  name: "report-statistics",
+  props: {
+    filter: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      current: 1,
+      total: 0,
+      size: 10,
+      dataList: [],
+    };
+  },
+  mounted() {
+    this.getData();
+  },
+  methods: {
+    async getList() {
+      const datas = {
+        ...this.filter,
+        pageNumber: this.current - 1,
+        pageSize: this.size,
+      };
+
+      const res = await reportStatisticsData(datas);
+
+      this.dataList = res.data.data.records;
+      this.total = res.data.data.records.total;
+    },
+    toPage(page) {
+      this.current = page;
+      this.getList();
+    },
+    getData() {
+      this.toPage(1);
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.stat-page-info {
+  color: #626a82;
+
+  span {
+    color: #202b4b;
+    margin: 0 5px;
+    font-weight: 600;
+  }
+}
+</style>

+ 94 - 43
src/features/invigilation/InvigilationDetail/InvigilationDetail.vue

@@ -16,10 +16,10 @@
             <el-select
               v-model="filter.examId"
               placeholder="请选择批次"
-              clearable
+              @change="examChange"
             >
               <el-option
-                v-for="item in batchs"
+                v-for="item in examBatchs"
                 :key="item.id"
                 :value="item.id"
                 :label="item.name"
@@ -33,24 +33,24 @@
               clearable
             >
               <el-option
-                v-for="item in exams"
+                v-for="item in examActivities"
                 :key="item.id"
                 :value="item.id"
-                :label="item.name"
+                :label="item.code"
               ></el-option>
             </el-select>
           </el-form-item>
           <el-form-item>
             <el-select
-              v-model="filter.examActivityId"
+              v-model="filter.roomCode"
               placeholder="请选择考场"
               clearable
             >
               <el-option
-                v-for="item in exams"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="item in examRooms"
+                :key="item.roomCode"
+                :value="item.roomCode"
+                :label="item.roomName"
               ></el-option>
             </el-select>
           </el-form-item>
@@ -61,10 +61,10 @@
               clearable
             >
               <el-option
-                v-for="item in subjects"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="item in examCourses"
+                :key="item.courseCode"
+                :value="item.courseCode"
+                :label="item.courseName"
               ></el-option>
             </el-select>
           </el-form-item>
@@ -82,20 +82,20 @@
               clearable
             >
               <el-option
-                v-for="item in subjects"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="(val, key) in STUDENT_FINISH_EXAM_TYPE"
+                :key="key"
+                :value="key"
+                :label="val"
               ></el-option>
             </el-select>
           </el-form-item>
           <el-form-item>
             <el-select v-model="filter.status" placeholder="筛选状态" clearable>
               <el-option
-                v-for="item in subjects"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="(val, key) in STUDENT_ONLINE_STATUS"
+                :key="key"
+                :value="key * 1"
+                :label="val"
               ></el-option>
             </el-select>
           </el-form-item>
@@ -106,10 +106,10 @@
               clearable
             >
               <el-option
-                v-for="item in subjects"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="(val, key) in STUDENT_BEHAVIOR_STATUS"
+                :key="key"
+                :value="key * 1"
+                :label="val"
               ></el-option>
             </el-select>
           </el-form-item>
@@ -177,19 +177,19 @@
     </div>
 
     <el-table ref="TableList" :data="dataList">
-      <el-table-column prop="batchName" label="批次"></el-table-column>
-      <el-table-column prop="examName" label="场次"></el-table-column>
+      <el-table-column prop="examName" label="批次"></el-table-column>
+      <el-table-column prop="examActivityCode" label="场次"></el-table-column>
       <el-table-column prop="roomName" label="考场"> </el-table-column>
       <el-table-column prop="examId" label="考试ID"></el-table-column>
       <el-table-column prop="identity" label="证件号"></el-table-column>
       <el-table-column prop="name" label="姓名"></el-table-column>
-      <el-table-column prop="subjectName" label="联系电话"></el-table-column>
-      <el-table-column prop="courseNameCode" label="科目(代码)">
+      <el-table-column prop="mobileNumber" label="联系电话"></el-table-column>
+      <el-table-column prop="courseNameCode" label="科目(代码)">
       </el-table-column>
       <el-table-column prop="status" label="状态"></el-table-column>
       <el-table-column prop="finishType" label="交卷方式">
         <template slot-scope="scope">
-          <div>{{ FINISH_TYPE[scope.row.finishType] }}</div>
+          <div>{{ STUDENT_FINISH_EXAM_TYPE[scope.row.finishType] }}</div>
         </template>
       </el-table-column>
       <el-table-column
@@ -198,7 +198,13 @@
       ></el-table-column>
       <el-table-column prop="exceptionCount" label="异常处理"></el-table-column>
       <el-table-column prop="warningCount" label="预警数"></el-table-column>
-      <el-table-column prop="breachStatus" label="违纪"></el-table-column>
+      <el-table-column prop="breachStatus" label="违纪">
+        <template slot-scope="scope">
+          <span :class="{ 'color-danger': scope.row.breachStatus }">
+            {{ scope.row.breachStatus | zeroOneYesNoFilter }}
+          </span>
+        </template>
+      </el-table-column>
       <el-table-column label="操作">
         <template slot-scope="scope">
           <el-button
@@ -218,6 +224,7 @@
         :current-page="current"
         :total="total"
         :page-size="size"
+        @size-change="toPage(1)"
         @current-change="toPage"
       >
       </el-pagination>
@@ -226,8 +233,16 @@
 </template>
 
 <script>
-import { invigilationHistoryList } from "@/api/invigilation";
-import { FINISH_TYPE } from "@/constant/constants";
+import {
+  examBatchList,
+  examActivityRoomList,
+  invigilationHistoryList,
+} from "@/api/invigilation";
+import {
+  STUDENT_FINISH_EXAM_TYPE,
+  STUDENT_ONLINE_STATUS,
+  STUDENT_BEHAVIOR_STATUS,
+} from "@/constant/constants";
 import SummaryLine from "../common/SummaryLine";
 
 export default {
@@ -238,28 +253,42 @@ export default {
       filter: {
         examId: "",
         examActivityId: null,
+        roomCode: null,
         courseCode: null,
         auditStatus: null,
         name: "",
-        maxMultipleFaceCount: null,
-        minMultipleFaceCount: null,
-        maxExceptionCount: null,
-        minExceptionCount: null,
-        maxWarningCount: null,
-        minWarningCount: null,
+        maxMultipleFaceCount: undefined,
+        minMultipleFaceCount: undefined,
+        maxExceptionCount: undefined,
+        minExceptionCount: undefined,
+        maxWarningCount: undefined,
+        minWarningCount: undefined,
       },
+      STUDENT_FINISH_EXAM_TYPE,
+      STUDENT_ONLINE_STATUS,
+      STUDENT_BEHAVIOR_STATUS,
       showAdvancedFilter: false,
-      FINISH_TYPE,
       current: 1,
       total: 0,
       size: 10,
-      batchs: [],
-      exams: [],
-      subjects: [],
+      examBatchs: [],
+      examActivities: [],
+      examRooms: [],
+      examCourses: [],
       dataList: [],
+      userId: this.$store.state.user.id,
     };
   },
+  mounted() {
+    this.initData();
+  },
   methods: {
+    async initData() {
+      await this.getExamBatchList();
+      this.filter.examId = this.examBatchs[0] && this.examBatchs[0].id;
+      this.toPage(1);
+      this.getExamActivityRoomList();
+    },
     async getList() {
       const datas = {
         ...this.filter,
@@ -276,6 +305,28 @@ export default {
       this.current = page;
       this.getList();
     },
+    async getExamBatchList() {
+      const user = this.$store.state.user;
+      const userId =
+        user.roleCodes.includes("INVIGILATE") ||
+        user.roleCodes.includes("INSPECTION")
+          ? user.id
+          : null;
+      const res = await examBatchList(userId);
+      this.examBatchs = res.data.data;
+    },
+    async getExamActivityRoomList() {
+      const res = await examActivityRoomList(this.filter.examId);
+      this.examActivities = res.data.data.examActivitys;
+      this.examRooms = res.data.data.examRooms;
+      this.examCourses = res.data.data.examCourses;
+    },
+    examChange() {
+      this.filter.examActivityId = null;
+      this.filter.roomCode = null;
+      this.filter.courseCode = null;
+      this.getExamActivityRoomList();
+    },
     changeFilter() {
       this.showAdvancedFilter = !this.showAdvancedFilter;
       if (!this.showAdvancedFilter) {

+ 205 - 34
src/features/invigilation/OnlinePatrol/OnlinePatrol.vue

@@ -10,10 +10,10 @@
             <el-select
               v-model="filter.examId"
               placeholder="请选择批次"
-              clearable
+              @change="examChange"
             >
               <el-option
-                v-for="item in batchs"
+                v-for="item in examBatchs"
                 :key="item.id"
                 :value="item.id"
                 :label="item.name"
@@ -27,10 +27,10 @@
               clearable
             >
               <el-option
-                v-for="item in exams"
+                v-for="item in examActivities"
                 :key="item.id"
                 :value="item.id"
-                :label="item.name"
+                :label="item.code"
               ></el-option>
             </el-select>
           </el-form-item>
@@ -41,34 +41,34 @@
               clearable
             >
               <el-option
-                v-for="item in exams"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="item in examRooms"
+                :key="item.roomCode"
+                :value="item.roomCode"
+                :label="item.roomName"
               ></el-option>
             </el-select>
           </el-form-item>
           <el-form-item>
-            <el-select v-model="filter.status" placeholder="筛选状态" clearable>
+            <el-select v-model="filter.status" placeholder="考试状态" clearable>
               <el-option
-                v-for="item in subjects"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="(val, key) in STUDENT_ONLINE_STATUS"
+                :key="key"
+                :value="key"
+                :label="val"
               ></el-option>
             </el-select>
           </el-form-item>
           <el-form-item>
             <el-select
-              v-model="filter.clientWebsocketStatus"
+              v-model="filter.monitorStatusSource"
               placeholder="通讯故障"
               clearable
             >
               <el-option
-                v-for="item in subjects"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="(val, key) in BOOLEAN_TYPE"
+                :key="key"
+                :value="key * 1"
+                :label="val"
               ></el-option>
             </el-select>
           </el-form-item>
@@ -140,14 +140,14 @@
     </div>
 
     <el-table ref="TableList" :data="dataList">
-      <el-table-column prop="roomName" label="考场(代码)">
+      <el-table-column prop="roomName" label="考场(代码)">
         <span slot-scope="scope"
           >{{ scope.row.roomName }}({{ scope.row.roomCode }})</span
         >
       </el-table-column>
       <el-table-column prop="identity" label="证件号"></el-table-column>
       <el-table-column prop="name" label="姓名"></el-table-column>
-      <el-table-column prop="courseName" label="科目(代码)">
+      <el-table-column prop="courseName" label="科目(代码)">
         <span slot-scope="scope"
           >{{ scope.row.courseName }}({{ scope.row.courseCode }})</span
         >
@@ -162,7 +162,13 @@
         label="陌生人脸"
       ></el-table-column>
       <el-table-column prop="warningCount" label="预警数"></el-table-column>
-      <el-table-column prop="breachStatus" label="违纪"></el-table-column>
+      <el-table-column prop="breachStatus" label="违纪">
+        <template slot-scope="scope">
+          <span :class="{ 'color-danger': scope.row.breachStatus }">
+            {{ scope.row.breachStatus | zeroOneYesNoFilter }}
+          </span>
+        </template>
+      </el-table-column>
       <el-table-column label="操作">
         <template slot-scope="scope">
           <el-button
@@ -182,22 +188,43 @@
         :current-page="current"
         :total="total"
         :page-size="size"
+        @size-change="toPage(1)"
         @current-change="toPage"
       >
       </el-pagination>
     </div>
 
     <div class="patrol-analysis part-box">
+      <div class="patrol-analysis-legend">
+        <div
+          class="legend-item"
+          v-for="item in statInfo"
+          :key="item.key"
+          @click="orderChange(item)"
+        >
+          <i class="legend-item-symbol"></i>
+          <i class="legend-item-name">{{ item.name }}</i>
+          <i
+            :class="['legend-item-order', `legend-item-order-${item.order}`]"
+          ></i>
+        </div>
+      </div>
       <echart-render
         :chart-data="statData"
         chart-type="barGroup"
+        v-if="statDataReady"
       ></echart-render>
     </div>
   </div>
 </template>
 
 <script>
-import { patrolList } from "@/api/invigilation";
+import {
+  patrolList,
+  examBatchList,
+  examActivityRoomList,
+} from "@/api/invigilation";
+import { BOOLEAN_TYPE, STUDENT_ONLINE_STATUS } from "@/constant/constants";
 import EchartRender from "../common/EchartRender";
 import SummaryLine from "../common/SummaryLine";
 
@@ -207,26 +234,49 @@ export default {
   data() {
     return {
       filter: {
-        examId: null,
-        roomCode: null,
+        examId: "",
         examActivityId: null,
-        clientWebsocketStatus: null,
+        roomCode: null,
+        monitorStatusSource: null,
+        status: null,
         name: "",
-        maxMultipleFaceCount: null,
-        minMultipleFaceCount: null,
-        maxExceptionCount: null,
-        minExceptionCount: null,
-        maxWarningCount: null,
-        minWarningCount: null,
+        maxMultipleFaceCount: undefined,
+        minMultipleFaceCount: undefined,
+        maxExceptionCount: undefined,
+        minExceptionCount: undefined,
+        maxWarningCount: undefined,
+        minWarningCount: undefined,
       },
+      BOOLEAN_TYPE,
+      STUDENT_ONLINE_STATUS,
       current: 1,
       total: 30,
       size: 10,
-      batchs: [],
-      exams: [],
-      subjects: [],
+      examBatchs: [],
+      examActivities: [],
+      examRooms: [],
+      examCourses: [],
       dataList: [],
+      userId: this.$store.state.user.id,
       examPropData: {},
+      statDataReady: true,
+      statInfo: [
+        {
+          name: "已登录",
+          order: "none",
+          key: "login",
+        },
+        {
+          name: "通讯故障",
+          order: "none",
+          key: "error",
+        },
+        {
+          name: "预警数",
+          order: "none",
+          key: "waiting",
+        },
+      ],
       statData: [
         {
           name: "苏州市第一中学",
@@ -303,7 +353,17 @@ export default {
       ],
     };
   },
+  mounted() {
+    this.initData();
+  },
   methods: {
+    async initData() {
+      await this.getExamBatchList();
+      this.filter.examId = this.examBatchs[0] ? this.examBatchs[0].id : "";
+      if (!this.filter.examId) return;
+      this.toPage(1);
+      this.getExamActivityRoomList();
+    },
     async getList() {
       const datas = {
         ...this.filter,
@@ -320,6 +380,28 @@ export default {
       this.current = page;
       this.getList();
     },
+    async getExamBatchList() {
+      const user = this.$store.state.user;
+      const userId =
+        user.roleCodes.includes("INVIGILATE") ||
+        user.roleCodes.includes("INSPECTION")
+          ? user.id
+          : null;
+      const res = await examBatchList(userId);
+      this.examBatchs = res.data.data;
+    },
+    async getExamActivityRoomList() {
+      const res = await examActivityRoomList(this.filter.examId);
+      this.examActivities = res.data.data.examActivitys;
+      this.examRooms = res.data.data.examRooms;
+      this.examCourses = res.data.data.examCourses;
+    },
+    examChange() {
+      this.filter.examActivityId = null;
+      this.filter.roomCode = null;
+      this.filter.courseCode = null;
+      this.getExamActivityRoomList();
+    },
     toDetail(row) {
       console.log(row);
       this.$router.push({
@@ -327,6 +409,28 @@ export default {
         params: { recordId: row.examRecordId },
       });
     },
+    orderChange(statItem) {
+      const orderInfo = {
+        none: "desc",
+        desc: "asc",
+        asc: "desc",
+      };
+
+      this.statInfo.map((elem) => {
+        if (elem.key !== statItem.key) elem.order = "none";
+      });
+      statItem.order = orderInfo[statItem.order];
+
+      this.statDataReady = false;
+      this.$nextTick(() => {
+        this.statData.sort((a, b) => {
+          return statItem.order === "desc"
+            ? b[statItem.key] - a[statItem.key]
+            : a[statItem.key] - b[statItem.key];
+        });
+        this.statDataReady = true;
+      });
+    },
   },
 };
 </script>
@@ -335,5 +439,72 @@ export default {
 .patrol-analysis {
   height: 355px;
   margin-top: 20px;
+  position: relative;
+}
+.patrol-analysis-legend {
+  position: absolute;
+  top: 20px;
+  right: 20px;
+  z-index: 99;
+
+  .legend-item {
+    display: inline-block;
+    vertical-align: middle;
+    color: #626a82;
+    font-size: 0;
+    cursor: pointer;
+
+    &:not(:last-child) {
+      margin-right: 40px;
+    }
+    &:hover {
+      color: #202b4b;
+    }
+
+    > i {
+      display: inline-block;
+      font-size: 14px;
+      vertical-align: middle;
+      font-style: normal;
+    }
+
+    &-symbol {
+      height: 8px;
+      width: 8px;
+      border-radius: 50%;
+    }
+    &-name {
+      margin: 0 8px;
+    }
+    &-order {
+      width: 6px;
+      height: 8px;
+      background-image: url(../../../assets/icon-order-none.png);
+      background-size: 100% 100%;
+
+      &-desc {
+        background-image: url(../../../assets/icon-order-desc.png);
+      }
+      &-asc {
+        background-image: url(../../../assets/icon-order-asc.png);
+      }
+    }
+
+    &:nth-of-type(1) {
+      .legend-item-symbol {
+        background: #5fc9fa;
+      }
+    }
+    &:nth-of-type(2) {
+      .legend-item-symbol {
+        background: #fe5863;
+      }
+    }
+    &:nth-of-type(3) {
+      .legend-item-symbol {
+        background: #feca57;
+      }
+    }
+  }
 }
 </style>

+ 72 - 24
src/features/invigilation/ProgressDetail/ProgressDetail.vue

@@ -10,10 +10,10 @@
             <el-select
               v-model="filter.examId"
               placeholder="请选择批次"
-              clearable
+              @change="examChange"
             >
               <el-option
-                v-for="item in batchs"
+                v-for="item in examBatchs"
                 :key="item.id"
                 :value="item.id"
                 :label="item.name"
@@ -27,10 +27,10 @@
               clearable
             >
               <el-option
-                v-for="item in exams"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="item in examRooms"
+                :key="item.roomCode"
+                :value="item.roomCode"
+                :label="item.roomName"
               ></el-option>
             </el-select>
           </el-form-item>
@@ -41,10 +41,10 @@
               clearable
             >
               <el-option
-                v-for="item in subjects"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="item in examCourses"
+                :key="item.courseCode"
+                :value="item.courseCode"
+                :label="item.courseName"
               ></el-option>
             </el-select>
           </el-form-item>
@@ -61,7 +61,12 @@
         </el-form>
       </div>
       <div class="part-filter-info">
-        <div class="part-filter-info-main summary-line">
+        <summary-line
+          class="part-filter-info-main"
+          data-type="progress"
+          :exam-id="filter.examId"
+        ></summary-line>
+        <!-- <div class="part-filter-info-main summary-line">
           <p class="summary-line-item">
             <i class="icon icon-users"></i>
             <span class="line-name">全部应考</span>
@@ -69,7 +74,7 @@
           </p>
           <p class="summary-line-item">
             <i class="icon icon-rate"></i>
-            <span class="lin-name">完成率</span><span>20人</span>
+            <span class="line-name">完成率</span><span>20人</span>
           </p>
           <p class="summary-line-item">
             <i class="line-point line-point-success"></i>
@@ -84,10 +89,11 @@
             <span>5人</span>
           </p>
           <p class="summary-line-item">
-            <i class="line-point line-point-danger"></i><span>未完成</span
-            ><span>3人</span>
+            <i class="line-point line-point-danger"></i>
+            <span class="line-name">未完成</span>
+            <span>3人</span>
           </p>
-        </div>
+        </div> -->
         <div class="part-filter-info-sub">
           <el-button type="primary" icon="icon icon-upload" @click="toExport"
             >导出查询结果</el-button
@@ -97,10 +103,13 @@
     </div>
 
     <el-table ref="TableList" :data="dataList">
-      <el-table-column prop="roomCode" label="批次名称(代码)">
+      <el-table-column prop="examName" label="批次名称(代码)">
       </el-table-column>
-      <el-table-column prop="examName" label="场次名称(代码)"></el-table-column>
-      <el-table-column prop="roomName" label="考场名称(代码)">
+      <el-table-column
+        prop="examActivityCode"
+        label="场次名称(代码)"
+      ></el-table-column>
+      <el-table-column prop="roomName" label="考场名称(代码)">
         <span slot-scope="scope"
           >{{ scope.row.roomName }}({{ scope.row.roomCode }})</span
         >
@@ -108,7 +117,7 @@
       <el-table-column prop="identity" label="证件号"></el-table-column>
       <el-table-column prop="name" label="姓名"></el-table-column>
       <el-table-column prop="mobileNumber" label="联系电话"></el-table-column>
-      <el-table-column prop="courseName" label="科目(代码)">
+      <el-table-column prop="courseName" label="科目(代码)">
         <span slot-scope="scope"
           >{{ scope.row.courseName }}({{ scope.row.courseCode }})</span
         >
@@ -126,6 +135,7 @@
         :current-page="current"
         :total="total"
         :page-size="size"
+        @size-change="toPage(1)"
         @current-change="toPage"
       >
       </el-pagination>
@@ -134,14 +144,20 @@
 </template>
 
 <script>
-import { progressDetailList } from "@/api/invigilation";
+import {
+  examBatchList,
+  examActivityRoomList,
+  progressDetailList,
+} from "@/api/invigilation";
+import SummaryLine from "../common/SummaryLine";
 
 export default {
   name: "progress-detail",
+  components: { SummaryLine },
   data() {
     return {
       filter: {
-        examId: null,
+        examId: "",
         roomCode: null,
         courseCode: null,
         name: "",
@@ -149,13 +165,23 @@ export default {
       current: 1,
       total: 0,
       size: 10,
-      batchs: [],
-      exams: [],
-      subjects: [],
+      examBatchs: [],
+      examActivities: [],
+      examRooms: [],
+      examCourses: [],
       dataList: [],
     };
   },
+  mounted() {
+    this.initData();
+  },
   methods: {
+    async initData() {
+      await this.getExamBatchList();
+      this.filter.examId = this.examBatchs[0] && this.examBatchs[0].id;
+      this.toPage(1);
+      this.getExamActivityRoomList();
+    },
     async getList() {
       const datas = {
         ...this.filter,
@@ -172,6 +198,28 @@ export default {
       this.current = page;
       this.getList();
     },
+    async getExamBatchList() {
+      const user = this.$store.state.user;
+      const userId =
+        user.roleCodes.includes("INVIGILATE") ||
+        user.roleCodes.includes("INSPECTION")
+          ? user.id
+          : null;
+      const res = await examBatchList(userId);
+      this.examBatchs = res.data.data;
+    },
+    async getExamActivityRoomList() {
+      const res = await examActivityRoomList(this.filter.examId);
+      this.examActivities = res.data.data.examActivitys;
+      this.examRooms = res.data.data.examRooms;
+      this.examCourses = res.data.data.examCourses;
+    },
+    examChange() {
+      this.filter.examActivityId = null;
+      this.filter.roomCode = null;
+      this.filter.courseCode = null;
+      this.getExamActivityRoomList();
+    },
     toDetail(row) {
       console.log(row);
       this.$router.push({ name: "WainingDetail", params: { id: row.id } });

+ 8 - 3
src/features/invigilation/RealtimeMonitoring/ExamBatchDialog.vue

@@ -21,7 +21,7 @@
 </template>
 
 <script>
-import { examBatchList } from "@/api/invigilation";
+import { examMonitorBatchList } from "@/api/invigilation";
 
 export default {
   name: "exam-batch-dialog",
@@ -30,6 +30,7 @@ export default {
       dialogVisible: false,
       examBatchId: null,
       examBatchs: [],
+      userId: this.$store.state.user.id,
     };
   },
   mounted() {
@@ -37,8 +38,12 @@ export default {
   },
   methods: {
     async getExamList() {
-      const res = await examBatchList({ pageNumber: 0, pageSize: 100 });
-      this.examBatchs = res.data.data.map((item) => {
+      const res = await examMonitorBatchList({
+        userId: this.userId,
+        pageNumber: 0,
+        pageSize: 100,
+      });
+      this.examBatchs = res.data.data.records.map((item) => {
         return {
           ...item,
           label: `${item.name}【${item.startTime} - ${item.endTime}】`,

+ 110 - 105
src/features/invigilation/RealtimeMonitoring/RealtimeMonitoring.vue

@@ -2,20 +2,10 @@
   <div class="realtime-monitoring">
     <div class="realtime-top clear-float">
       <p>考场名称:{{ curExamBatch.name }}</p>
-      <div
-        class="el-select el-select--small"
-        @click="$refs.ExamBatchDialog.open()"
-      >
-        <div class="el-input el-input--small el-input--suffix">
-          <div class="el-input__inner">{{ curExamBatch.label }}</div>
-          <span class="el-input__suffix">
-            <span class="el-input__suffix-inner"
-              ><i
-                class="el-select__caret el-input__icon el-icon-arrow-up"
-              ></i></span
-          ></span>
-        </div>
-      </div>
+      <p class="realtime-top-select" @click="$refs.ExamBatchDialog.open()">
+        <span>{{ curExamBatch.label || "请选择批次" }}</span>
+        <i class="el-icon-caret-bottom"></i>
+      </p>
       <text-clock></text-clock>
     </div>
 
@@ -84,20 +74,20 @@
               clearable
             >
               <el-option
-                v-for="item in batchs"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="(val, key) in BOOLEAN_TYPE"
+                :key="key"
+                :value="key * 1"
+                :label="val"
               ></el-option>
             </el-select>
           </el-form-item>
           <el-form-item>
             <el-select v-model="filter.status" placeholder="考试状态" clearable>
               <el-option
-                v-for="item in exams"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="(val, key) in STUDENT_ONLINE_STATUS"
+                :key="key"
+                :value="key"
+                :label="val"
               ></el-option>
             </el-select>
           </el-form-item>
@@ -108,10 +98,10 @@
               clearable
             >
               <el-option
-                v-for="item in exams"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="(val, key) in BOOLEAN_TYPE"
+                :key="key"
+                :value="key * 1"
+                :label="val"
               ></el-option>
             </el-select>
           </el-form-item>
@@ -189,29 +179,37 @@
       <el-table-column prop="name" label="姓名"></el-table-column>
       <el-table-column prop="courseName" label="科目名称"></el-table-column>
       <el-table-column prop="courseCode" label="科目代码"></el-table-column>
-      <el-table-column prop="subjectCode" label="剩余时间"></el-table-column>
-      <el-table-column prop="paperDownload" label="试题下载"></el-table-column>
+      <el-table-column prop="remainTime" label="剩余时间"></el-table-column>
+      <el-table-column prop="paperDownload" label="试题下载">
+        <span slot-scope="scope">
+          {{ BOOLEAN_TYPE[scope.row.paperDownload] }}
+        </span>
+      </el-table-column>
       <el-table-column prop="status" label="考试状态"></el-table-column>
       <el-table-column prop="progress" label="进度"></el-table-column>
-      <el-table-column prop="clientCommunicationStatus" label="通讯">
+      <el-table-column prop="clientWebsocketStatus" label="通讯">
         <template slot-scope="scope">
           <right-or-wrong
-            :status="scope.row.clientCommunicationStatus"
+            :status="CLIENT_WEBSOCKET_STATUS[scope.row.clientWebsocketStatus]"
           ></right-or-wrong>
         </template>
       </el-table-column>
-      <el-table-column prop="subjectCode" label="推流通讯">
+      <el-table-column prop="monitorStatusSource" label="推流通讯">
         <template slot-scope="scope">
           <right-or-wrong
-            :status="scope.row.clientCommunicationStatus"
+            :status="MONITOR_STATUS_SOURCE[scope.row.monitorStatusSource]"
           ></right-or-wrong>
         </template>
       </el-table-column>
       <el-table-column prop="clientCurrentIp" label="IP"></el-table-column>
-      <el-table-column prop="updateTime" label="更新时间"></el-table-column>
+      <el-table-column prop="updateTime" label="更新时间">
+        <span slot-scope="scope">
+          {{ scope.row.updateTime | datetimeFilter }}
+        </span>
+      </el-table-column>
       <el-table-column prop="warningCount" label="预警数"></el-table-column>
       <el-table-column prop="breachStatus" label="违纪"></el-table-column>
-      <el-table-column label="操作">
+      <el-table-column label="操作" width="100">
         <template slot-scope="scope">
           <el-button
             class="btn-table-icon"
@@ -264,6 +262,7 @@ import {
   invigilateList,
   invigilateVideoList,
   invigilateExamFinish,
+  monitorCallCount,
 } from "@/api/invigilation";
 import ExamBatchDialog from "./ExamBatchDialog";
 import RightOrWrong from "../common/RightOrWrong";
@@ -271,6 +270,12 @@ import InvigilationStudent from "../common/InvigilationStudent";
 import SummaryLine from "../common/SummaryLine";
 import handleRollupDialog from "./handleRollupDialog";
 import TextClock from "../common/TextClock";
+import {
+  BOOLEAN_TYPE,
+  STUDENT_ONLINE_STATUS,
+  CLIENT_WEBSOCKET_STATUS,
+  MONITOR_STATUS_SOURCE,
+} from "@/constant/constants";
 
 export default {
   name: "realtime-monitoring",
@@ -293,6 +298,10 @@ export default {
         maxWarningCount: undefined,
         minWarningCount: undefined,
       },
+      BOOLEAN_TYPE,
+      STUDENT_ONLINE_STATUS,
+      CLIENT_WEBSOCKET_STATUS,
+      MONITOR_STATUS_SOURCE,
       hasNewWarning: false,
       communicationCount: 0,
       curExamBatch: {},
@@ -306,53 +315,23 @@ export default {
       exams: [],
       subjects: [],
       pageType: "0",
-      dataList: [
+      dataList: [],
+      videoList: [
         {
-          breachStatus: 0,
-          clientCommunicationStatus: 0,
-          clientCurrentIp: "192.168.10.12",
-          courseCode: "F001",
-          courseName: "数学",
-          examActivityId: 0,
-          examId: 111,
-          examRecordId: 222,
-          examStudentId: 333,
-          identity: "000000000000008",
-          monitorStatusSource: "",
-          name: "楚一一",
-          paperDownload: 0,
-          progress: 0,
-          roomCode: "123",
-          roomName: "第一教师",
-          status: "1",
-          statusCode: "1",
-          updateTime: "2020-12-12",
-          warningCount: 0,
+          name: "刘西西",
+          identity: "000000000000000008",
+          progress: "52%",
+          warning: false,
+          netbreak: true,
         },
         {
-          breachStatus: 0,
-          clientCommunicationStatus: 1,
-          clientCurrentIp: "192.168.10.12",
-          courseCode: "F001",
-          courseName: "数学",
-          examActivityId: 0,
-          examId: 111,
-          examRecordId: 222,
-          examStudentId: 333,
-          identity: "000000000000008",
-          monitorStatusSource: "",
-          name: "楚一一",
-          paperDownload: 0,
-          progress: 0,
-          roomCode: "123",
-          roomName: "第一教师",
-          status: "1",
-          statusCode: "1",
-          updateTime: "2020-12-12",
-          warningCount: 0,
+          name: "刘西西",
+          identity: "000000000000000008",
+          progress: "52%",
+          warning: true,
+          netbreak: false,
         },
       ],
-      videoList: [],
       viewingAngles: [
         {
           code: "1",
@@ -370,23 +349,23 @@ export default {
     };
   },
   mounted() {
-    window.inviligateWaining = (id) => {
-      console.log(id);
-    };
-    this.$notify({
-      duration: 0,
-      dangerouslyUseHTMLString: true,
-      customClass: "msg-monitor-magbox",
-      position: "bottom-right",
-      offset: 50,
-      message: `
-        <div class="msg-monitor">
-          <span class="msg-monitor-icon"><i class="icon icon-warning"></i></span>
-          <span>注意:<b>张三意识</b>发现违纪,</span>
-          <span class="msg-monitor-action" onclick="window.inviligateWaining(12)">立即处理</span>
-        </div>
-      `,
-    });
+    // window.inviligateWaining = (id) => {
+    //   console.log(id);
+    // };
+    // this.$notify({
+    //   duration: 5,
+    //   dangerouslyUseHTMLString: true,
+    //   customClass: "msg-monitor-magbox",
+    //   position: "bottom-right",
+    //   offset: 50,
+    //   message: `
+    //     <div class="msg-monitor">
+    //       <span class="msg-monitor-icon"><i class="icon icon-warning"></i></span>
+    //       <span>注意:<b>张三意识</b>发现违纪,</span>
+    //       <span class="msg-monitor-action" onclick="window.inviligateWaining(12)">立即处理</span>
+    //     </div>
+    //   `,
+    // });
   },
   methods: {
     async getList() {
@@ -414,11 +393,17 @@ export default {
       this.current = page;
       this.getList();
     },
+    async getMonitorCallCount() {
+      if (!this.filter.examId) return;
+      const res = await monitorCallCount(this.filter.examId);
+      this.communicationCount = res.data.data.count || 0;
+    },
     examChange(examBatch) {
       if (!examBatch) return;
       this.filter.examId = examBatch.id;
       this.curExamBatch = examBatch;
-      // this.toPage(1);
+      this.toPage(1);
+      this.getMonitorCallCount();
     },
     pageTypeChange(pageType) {
       this.pageType = pageType;
@@ -444,11 +429,16 @@ export default {
       this.getList();
     },
     async finishInvigilationExam() {
-      const result = await this.$confirm("确定要结束监考吗?", {
-        confirmButtonText: "确定",
-        cancelButtonText: "取消",
-        type: "confirm",
-      }).catch(() => {});
+      const result = await this.$confirm(
+        "确定要结束监考吗?",
+        "结束监考确认提醒",
+        {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          iconClass: "el-icon-warning",
+          customClass: "el-message-box__error",
+        }
+      ).catch(() => {});
 
       if (!result) return;
 
@@ -484,6 +474,7 @@ export default {
   border-radius: 6px;
   color: #fff;
   margin-bottom: 30px;
+  line-height: 32px;
 
   &::before {
     content: "";
@@ -497,19 +488,33 @@ export default {
     background-size: 100% 100%;
   }
 
-  > * {
+  > p {
     float: left;
-    line-height: 32px;
     margin: 0;
   }
-  .el-select {
-    min-width: 200px;
-  }
   > p:first-child {
     margin-right: 40px;
     min-width: 150px;
   }
-  > p:last-child {
+  .realtime-top-select {
+    display: inline-block;
+    position: relative;
+    height: 32px;
+    line-height: 32px;
+    border-radius: 6px;
+    min-width: 200px;
+    background-color: #fff;
+    color: #1886fe;
+    padding: 0 26px 0 12px;
+    cursor: pointer;
+
+    > i {
+      position: absolute;
+      right: 8px;
+      top: 9px;
+    }
+  }
+  .text-clock {
     float: right;
     font-size: 12px;
     opacity: 0.8;

+ 1 - 1
src/features/invigilation/RealtimeMonitoring/StudentBreachDialog.vue

@@ -28,7 +28,7 @@
         <span>{{ modalForm.examStudentName }}</span>
       </div>
       <div class="student-info-item">
-        <span>科目(代码):</span>
+        <span>科目(代码):</span>
         <span>{{ modalForm.courseNameCode }}</span>
       </div>
     </div>

+ 24 - 15
src/features/invigilation/RealtimeMonitoring/VideoCommunication.vue

@@ -36,6 +36,7 @@
         :current-page="current"
         :total="total"
         :page-size="size"
+        @size-change="toPage(1)"
         @current-change="toPage"
       >
       </el-pagination>
@@ -70,7 +71,11 @@
 
 <script>
 import { createClient, createStream } from "@/plugins/trtc";
-import { communicationList, communicationOver } from "@/api/invigilation";
+import {
+  communicationList,
+  communicationOver,
+  getUserMonitorKey,
+} from "@/api/invigilation";
 
 const stdAvatars = [
   "/img/avatars/1.jpg",
@@ -82,6 +87,7 @@ export default {
   name: "video-communication",
   data() {
     return {
+      examActivityId: this.$route.params.examActivityId,
       pageType: "0",
       dialogVisible: false,
       students: [
@@ -98,26 +104,15 @@ export default {
       setT: null,
       durationTime: "00:10:23",
       appId: "1400411036",
-      videoUserSig:
-        "eJyrVgrxCdYrSy1SslIy0jNQ0gHzM1NS80oy0zLBwsUGZlDh4pTsxIKCzBQlK0MTAwMTQ0MDYzOITGpFQWZRKlDc1NTUyMDAACJakpkLFrM0NzI0NLKEqi3OTAeaWmoeWZAd6O1h7B4YbhJoXOYdkeNmEebm5Z1R5JxXEmVhbOZkYWFQkZvjV26rVAsAYQkvlQ__",
-      videoUserId: "s06",
+      userMonitor: {},
       client: null,
       localStream: null,
     };
   },
   mounted() {
-    this.initClinet();
     this.getCommunicationList();
   },
   methods: {
-    async initClinet() {
-      this.client = createClient({
-        mode: "live",
-        sdkAppId: this.appId,
-        userId: this.videoUserId,
-        userSig: this.videoUserSig,
-      });
-    },
     async getCommunicationList() {
       if (this.setT) clearTimeout(this.setT);
 
@@ -129,6 +124,7 @@ export default {
       this.students = res.data.data.records
         .filter((item) => item.callStatus === "START")
         .map((item, index) => {
+          // TODO:用户头像临时处理方法
           const lindex = index % 3;
           return {
             ...item,
@@ -151,7 +147,18 @@ export default {
       this.current = page;
       this.getCommunicationList();
     },
+    async initClient(examRecordId) {
+      const res = await getUserMonitorKey(examRecordId);
+      this.userMonitor = res.data.data;
+      this.client = createClient({
+        mode: "live",
+        sdkAppId: this.appId,
+        userId: this.userMonitor.monitorUserId,
+        userSig: this.userMonitor.monitorUserSig,
+      });
+    },
     async answer(student, isVideo) {
+      await this.initClient(student.examRecordId);
       // 结束学生的通话申请
       await communicationOver({ recordId: student.examRecordId });
 
@@ -179,7 +186,7 @@ export default {
       let roomJoinResult = true;
       await this.client
         .join({
-          roomId: student.monitorKey,
+          roomId: this.userMonitor.monitorKey,
           role: "audience",
           // role: "anchor"
         })
@@ -199,7 +206,7 @@ export default {
 
       // 初始化stream
       this.localStream = createStream({
-        userId: this.videoUserId,
+        userId: this.userMonitor.monitorUserId,
         audio: true,
         video: isVideo,
       });
@@ -242,6 +249,8 @@ export default {
       if (!result) return;
 
       this.client.off("*");
+      this.client = null;
+      this.userMonitor = {};
       this.dialogVisible = false;
     },
   },

+ 32 - 26
src/features/invigilation/RealtimeMonitoring/WainingDetail.vue

@@ -6,7 +6,7 @@
         <el-button size="mini" icon="el-icon-arrow-left" @click="goBack"
           >返回列表</el-button
         >
-        <el-button
+        <!-- <el-button
           @click="initSubscribeVideo"
           type="primary"
           size="mini"
@@ -19,7 +19,7 @@
           size="mini"
           icon="el-icon-arrow-left"
           >关闭视频</el-button
-        >
+        > -->
       </div>
       <div class="warning-detail-student">
         <div class="student-head">
@@ -32,7 +32,7 @@
               <span>证件号:</span><span>{{ detailInfo.identity }}</span>
             </p>
             <p>
-              <span>科目(代码):</span
+              <span>科目(代码):</span
               ><span>{{ detailInfo.courseNameCode }}</span>
             </p>
           </div>
@@ -122,7 +122,7 @@
             ><span>{{ detailInfo.exceptionCount }}次</span>
           </p>
           <p class="summary-line-item">
-            <span></span><span>违纪状态:{{ detailInfo.status }}</span>
+            <span></span><span>违纪状态:{{ detailInfo.breachStatus }}</span>
           </p>
           <el-button type="danger" icon="icon icon-stop" @click="toBreach"
             >违纪处理</el-button
@@ -250,7 +250,11 @@
 import { createClient, createStream } from "@/plugins/trtc";
 import { invigilateFinish } from "@/api/invigilation";
 import FlvMedia from "../common/FlvMedia";
-import { invigilateDetail, warningStudentDetail } from "@/api/invigilation";
+import {
+  invigilateDetail,
+  warningStudentDetail,
+  getUserMonitorKey,
+} from "@/api/invigilation";
 import StudentBreachDialog from "./StudentBreachDialog";
 
 const test = {
@@ -330,12 +334,12 @@ export default {
       },
       firstViewVideo: {
         id: "111",
-        src: "http://live.qmth.com.cn/live/8888_mobile_first.flv",
+        src: "",
         name: "第一视角",
       },
       secondViewVideo: {
         id: "222",
-        src: "http://live.qmth.com.cn/live/8888_mobile_second.flv",
+        src: "",
         name: "第二视角",
       },
       firstViewVideoReady: false,
@@ -343,9 +347,7 @@ export default {
       // communication
       durationTime: "00:10:23",
       appId: "1400411036",
-      videoUserSig:
-        "eJyrVgrxCdYrSy1SslIy0jNQ0gHzM1NS80oy0zLBwsUGZlDh4pTsxIKCzBQlK0MTAwMTQ0MDYzOITGpFQWZRKlDc1NTUyMDAACJakpkLFrM0NzI0NLKEqi3OTAeaWmoeWZAd6O1h7B4YbhJoXOYdkeNmEebm5Z1R5JxXEmVhbOZkYWFQkZvjV26rVAsAYQkvlQ__",
-      videoUserId: "s06",
+      userMonitor: {},
       client: null,
       localStream: null,
       dialogVisible: false,
@@ -354,18 +356,9 @@ export default {
     };
   },
   mounted() {
-    // this.initClinet();
-    // this.initData();
+    this.initData();
   },
   methods: {
-    initClinet() {
-      this.client = createClient({
-        mode: "live",
-        sdkAppId: this.appId,
-        userId: this.videoUserId,
-        userSig: this.videoUserSig,
-      });
-    },
     async initData() {
       this.getInvigilateDetail();
       const res = await warningStudentDetail({ recordId: this.recordId });
@@ -382,8 +375,7 @@ export default {
     },
     async getInvigilateDetail() {
       const res = await invigilateDetail(this.recordId);
-      this.detailInfo = res;
-      console.log(res);
+      this.detailInfo = res.data.data;
     },
     toBreach() {
       this.curDetail = {
@@ -427,10 +419,22 @@ export default {
       this.firstViewVideoReady = false;
       this.secondViewVideoReady = false;
     },
+    async initClient(examRecordId) {
+      const res = await getUserMonitorKey(examRecordId);
+      this.userMonitor = res.data.data;
+      this.client = createClient({
+        mode: "live",
+        sdkAppId: this.appId,
+        userId: this.userMonitor.monitorUserId,
+        userSig: this.userMonitor.monitorUserSig,
+      });
+    },
     async answer(isVideo) {
       this.closeSubscribeVideo();
-      this.dialogVisible = true;
 
+      await this.initClient(this.recordId);
+
+      this.dialogVisible = true;
       // 添加远程用户视频发布监听
       this.client.on("stream-added", (event) => {
         console.log(event);
@@ -461,7 +465,7 @@ export default {
       let roomJoinResult = true;
       await this.client
         .join({
-          roomId: this.curStudent.roomId,
+          roomId: this.userMonitor.monitorKey,
           role: "audience",
         })
         .catch((error) => {
@@ -480,7 +484,7 @@ export default {
 
       // 初始化stream
       this.localStream = createStream({
-        userId: this.videoUserId,
+        userId: this.userMonitor.monitorUserId,
         audio: true,
         video: isVideo,
       });
@@ -526,8 +530,10 @@ export default {
       if (!result) return;
 
       this.client.off("*");
+      this.client = null;
+      this.userMonitor = {};
+
       this.dialogVisible = false;
-      this.localStream = null;
       this.isWaiting = true;
       this.initSubscribeVideo();
     },

+ 53 - 16
src/features/invigilation/ReexamApply/ReexamApply.vue

@@ -11,10 +11,10 @@
             <el-select
               v-model="filter.examId"
               placeholder="请选择批次"
-              clearable
+              @change="examChange"
             >
               <el-option
-                v-for="item in batchs"
+                v-for="item in examBatchs"
                 :key="item.id"
                 :value="item.id"
                 :label="item.name"
@@ -28,10 +28,10 @@
               clearable
             >
               <el-option
-                v-for="item in exams"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="item in examRooms"
+                :key="item.roomCode"
+                :value="item.roomCode"
+                :label="item.roomName"
               ></el-option>
             </el-select>
           </el-form-item>
@@ -42,10 +42,10 @@
               clearable
             >
               <el-option
-                v-for="item in subjects"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="item in examCourses"
+                :key="item.courseCode"
+                :value="item.courseCode"
+                :label="item.courseName"
               ></el-option>
             </el-select>
           </el-form-item>
@@ -76,7 +76,7 @@
     >
       <el-table-column type="selection" width="55" align="center">
       </el-table-column>
-      <el-table-column prop="roomName" label="考场名称(代码)">
+      <el-table-column prop="roomName" label="考场名称(代码)">
         <span slot-scope="scope"
           >{{ scope.row.roomName }}({{ scope.row.roomCode }})</span
         >
@@ -104,6 +104,7 @@
         :current-page="current"
         :total="total"
         :page-size="size"
+        @size-change="toPage(1)"
         @current-change="toPage"
       >
       </el-pagination>
@@ -119,7 +120,11 @@
 </template>
 
 <script>
-import { reexamApplyList } from "@/api/invigilation";
+import {
+  examBatchList,
+  examActivityRoomList,
+  reexamApplyList,
+} from "@/api/invigilation";
 import ApplyReexamDialog from "./ApplyReexamDialog";
 
 export default {
@@ -129,16 +134,17 @@ export default {
     return {
       filter: {
         examId: null,
-        examroom: null,
+        roomCode: null,
         courseCode: null,
         name: "",
       },
       current: 1,
       total: 0,
       size: 10,
-      batchs: [],
-      exams: [],
-      subjects: [],
+      examBatchs: [],
+      examActivities: [],
+      examRooms: [],
+      examCourses: [],
       dataList: [
         {
           courseCode: "F001",
@@ -184,7 +190,16 @@ export default {
       multipleSelection: [],
     };
   },
+  mounted() {
+    this.initData();
+  },
   methods: {
+    async initData() {
+      await this.getExamBatchList();
+      this.filter.examId = this.examBatchs[0] && this.examBatchs[0].id;
+      this.toPage(1);
+      this.getExamActivityRoomList();
+    },
     async getList() {
       const datas = {
         ...this.filter,
@@ -201,6 +216,28 @@ export default {
       this.current = page;
       this.getList();
     },
+    async getExamBatchList() {
+      const user = this.$store.state.user;
+      const userId =
+        user.roleCodes.includes("INVIGILATE") ||
+        user.roleCodes.includes("INSPECTION")
+          ? user.id
+          : null;
+      const res = await examBatchList(userId);
+      this.examBatchs = res.data.data;
+    },
+    async getExamActivityRoomList() {
+      const res = await examActivityRoomList(this.filter.examId);
+      this.examActivities = res.data.data.examActivitys;
+      this.examRooms = res.data.data.examRooms;
+      this.examCourses = res.data.data.examCourses;
+    },
+    examChange() {
+      this.filter.examActivityId = null;
+      this.filter.roomCode = null;
+      this.filter.courseCode = null;
+      this.getExamActivityRoomList();
+    },
     handleSelectionChange(val) {
       this.multipleSelection = val;
     },

+ 48 - 16
src/features/invigilation/ReexamChecked/ReexamChecked.vue

@@ -9,11 +9,12 @@
           <el-form-item>
             <el-select
               v-model="filter.examId"
-              placeholder="请选择原批次"
+              placeholder="请选择批次"
+              @change="examChange"
               clearable
             >
               <el-option
-                v-for="item in batchs"
+                v-for="item in examBatchs"
                 :key="item.id"
                 :value="item.id"
                 :label="item.name"
@@ -27,10 +28,10 @@
               clearable
             >
               <el-option
-                v-for="item in exams"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="item in examRooms"
+                :key="item.roomCode"
+                :value="item.roomCode"
+                :label="item.roomName"
               ></el-option>
             </el-select>
           </el-form-item>
@@ -41,10 +42,10 @@
               clearable
             >
               <el-option
-                v-for="item in subjects"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="item in examCourses"
+                :key="item.courseCode"
+                :value="item.courseCode"
+                :label="item.courseName"
               ></el-option>
             </el-select>
           </el-form-item>
@@ -98,7 +99,7 @@
     </div>
 
     <el-table ref="TableList" :data="dataList">
-      <el-table-column prop="examroom" label="考场(代码)">
+      <el-table-column prop="examroom" label="考场(代码)">
         <span slot-scope="scope"
           >{{ scope.row.roomName }}({{ scope.row.roomCode }})</span
         >
@@ -134,6 +135,7 @@
         :current-page="current"
         :total="total"
         :page-size="size"
+        @size-change="toPage(1)"
         @current-change="toPage"
       >
       </el-pagination>
@@ -148,7 +150,11 @@
 </template>
 
 <script>
-import { reexamCheckedList } from "@/api/invigilation";
+import {
+  examBatchList,
+  examActivityRoomList,
+  reexamCheckedList,
+} from "@/api/invigilation";
 import { REEXAM_REASON } from "@/constant/constants";
 import CheckReexamDialog from "../ReexamPending/CheckReexamDialog";
 
@@ -159,7 +165,7 @@ export default {
     return {
       filter: {
         examId: null,
-        examroom: null,
+        roomCode: null,
         courseCode: null,
         name: "",
         applyName: "",
@@ -170,16 +176,26 @@ export default {
       current: 1,
       total: 0,
       size: 10,
-      batchs: [],
-      exams: [],
-      subjects: [],
+      examBatchs: [],
+      examActivities: [],
+      examRooms: [],
+      examCourses: [],
       applyTime: "",
       dataList: [],
       curReexam: {},
       multipleSelection: [],
     };
   },
+  mounted() {
+    this.initData();
+  },
   methods: {
+    async initData() {
+      await this.getExamBatchList();
+      this.filter.examId = this.examBatchs[0] && this.examBatchs[0].id;
+      this.toPage(1);
+      this.getExamActivityRoomList();
+    },
     async getList() {
       const datas = {
         ...this.filter,
@@ -196,6 +212,22 @@ export default {
       this.current = page;
       this.getList();
     },
+    async getExamBatchList() {
+      const res = await examBatchList();
+      this.examBatchs = res.data.data;
+    },
+    async getExamActivityRoomList() {
+      const res = await examActivityRoomList(this.filter.examId);
+      this.examActivities = res.data.data.examActivitys;
+      this.examRooms = res.data.data.examRooms;
+      this.examCourses = res.data.data.examCourses;
+    },
+    examChange() {
+      this.filter.examActivityId = null;
+      this.filter.roomCode = null;
+      this.filter.courseCode = null;
+      this.getExamActivityRoomList();
+    },
     applyTimeChange(vals) {
       const dvals = vals || [];
       this.filter.reasonStartTime = dvals[0] || null;

+ 48 - 16
src/features/invigilation/ReexamPending/ReexamPending.vue

@@ -9,11 +9,12 @@
           <el-form-item>
             <el-select
               v-model="filter.examId"
-              placeholder="请选择原批次"
+              placeholder="请选择批次"
+              @change="examChange"
               clearable
             >
               <el-option
-                v-for="item in batchs"
+                v-for="item in examBatchs"
                 :key="item.id"
                 :value="item.id"
                 :label="item.name"
@@ -27,10 +28,10 @@
               clearable
             >
               <el-option
-                v-for="item in exams"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="item in examRooms"
+                :key="item.roomCode"
+                :value="item.roomCode"
+                :label="item.roomName"
               ></el-option>
             </el-select>
           </el-form-item>
@@ -41,10 +42,10 @@
               clearable
             >
               <el-option
-                v-for="item in subjects"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="item in examCourses"
+                :key="item.courseCode"
+                :value="item.courseCode"
+                :label="item.courseName"
               ></el-option>
             </el-select>
           </el-form-item>
@@ -106,7 +107,7 @@
 
     <el-table ref="TableList" :data="dataList">
       <!-- <el-table-column type="selection" width="55"> </el-table-column> -->
-      <el-table-column prop="examroom" label="考场(代码)">
+      <el-table-column prop="examroom" label="考场(代码)">
         <span slot-scope="scope"
           >{{ scope.row.roomName }}({{ scope.row.roomCode }})</span
         >
@@ -139,6 +140,7 @@
         :current-page="current"
         :total="total"
         :page-size="size"
+        @size-change="toPage(1)"
         @current-change="toPage"
       >
       </el-pagination>
@@ -153,7 +155,11 @@
 </template>
 
 <script>
-import { reexamPendingList } from "@/api/invigilation";
+import {
+  examBatchList,
+  examActivityRoomList,
+  reexamPendingList,
+} from "@/api/invigilation";
 import { REEXAM_REASON } from "@/constant/constants";
 import CheckReexamDialog from "./CheckReexamDialog";
 
@@ -164,7 +170,7 @@ export default {
     return {
       filter: {
         examId: null,
-        examroom: null,
+        roomCode: null,
         courseCode: null,
         name: "",
         applyName: "",
@@ -175,9 +181,10 @@ export default {
       current: 1,
       total: 0,
       size: 10,
-      batchs: [],
-      exams: [],
-      subjects: [],
+      examBatchs: [],
+      examActivities: [],
+      examRooms: [],
+      examCourses: [],
       applyTime: "",
       dataList: [
         {
@@ -200,7 +207,16 @@ export default {
       multipleSelection: [],
     };
   },
+  mounted() {
+    this.initData();
+  },
   methods: {
+    async initData() {
+      await this.getExamBatchList();
+      this.filter.examId = this.examBatchs[0] && this.examBatchs[0].id;
+      this.toPage(1);
+      this.getExamActivityRoomList();
+    },
     async getList() {
       const datas = {
         ...this.filter,
@@ -217,6 +233,22 @@ export default {
       this.current = page;
       this.getList();
     },
+    async getExamBatchList() {
+      const res = await examBatchList();
+      this.examBatchs = res.data.data;
+    },
+    async getExamActivityRoomList() {
+      const res = await examActivityRoomList(this.filter.examId);
+      this.examActivities = res.data.data.examActivitys;
+      this.examRooms = res.data.data.examRooms;
+      this.examCourses = res.data.data.examCourses;
+    },
+    examChange() {
+      this.filter.examActivityId = null;
+      this.filter.roomCode = null;
+      this.filter.courseCode = null;
+      this.getExamActivityRoomList();
+    },
     applyTimeChange(vals) {
       const dvals = vals || [];
       this.filter.reasonStartTime = dvals[0] || null;

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

@@ -22,6 +22,7 @@
         :current-page="current"
         :total="total"
         :page-size="size"
+        @size-change="toPage(1)"
         @current-change="toPage"
       >
       </el-pagination>

+ 46 - 22
src/features/invigilation/StudentLogManage/StudentLogManage.vue

@@ -9,12 +9,12 @@
         <el-form ref="FilterForm" label-position="left" inline>
           <el-form-item>
             <el-select
-              v-model="filter.examId"
+              v-model="filter.taskId"
               placeholder="请选择考试任务"
               clearable
             >
               <el-option
-                v-for="item in batchs"
+                v-for="item in examTasks"
                 :key="item.id"
                 :value="item.id"
                 :label="item.name"
@@ -23,12 +23,13 @@
           </el-form-item>
           <el-form-item>
             <el-select
-              v-model="filter.batchId"
+              v-model="filter.examId"
               placeholder="请选择批次"
+              @change="examChange"
               clearable
             >
               <el-option
-                v-for="item in exams"
+                v-for="item in examBatchs"
                 :key="item.id"
                 :value="item.id"
                 :label="item.name"
@@ -37,29 +38,29 @@
           </el-form-item>
           <el-form-item>
             <el-select
-              v-model="filter.examroom"
+              v-model="filter.roomCode"
               placeholder="请选择考场"
               clearable
             >
               <el-option
-                v-for="item in exams"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="item in examRooms"
+                :key="item.roomCode"
+                :value="item.roomCode"
+                :label="item.roomName"
               ></el-option>
             </el-select>
           </el-form-item>
           <el-form-item>
             <el-select
-              v-model="filter.subjectId"
+              v-model="filter.courseCode"
               placeholder="请选择科目"
               clearable
             >
               <el-option
-                v-for="item in subjects"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="item in examCourses"
+                :key="item.courseCode"
+                :value="item.courseCode"
+                :label="item.courseName"
               ></el-option>
             </el-select>
           </el-form-item>
@@ -81,10 +82,13 @@
       <el-table-column prop="batchName" label="排序"></el-table-column>
       <el-table-column prop="batchName" label="批次名称(ID)"></el-table-column>
       <el-table-column prop="examName" label="场次ID"></el-table-column>
-      <el-table-column prop="examName" label="考场名称(代码)"></el-table-column>
+      <el-table-column
+        prop="examName"
+        label="考场名称(代码)"
+      ></el-table-column>
       <el-table-column prop="stdCardNo" label="证件号"></el-table-column>
       <el-table-column prop="stdName" label="姓名"></el-table-column>
-      <el-table-column prop="subjectCode" label="科目(代码)">
+      <el-table-column prop="subjectCode" label="科目(代码)">
         <template slot-scope="scope">
           <span>{{ scope.row.subjectName }}({{ scope.row.subjectCode }})</span>
         </template>
@@ -108,6 +112,7 @@
         :current-page="current"
         :total="total"
         :page-size="size"
+        @size-change="toPage(1)"
         @current-change="toPage"
       >
       </el-pagination>
@@ -121,6 +126,7 @@
 </template>
 
 <script>
+import { examBatchList, examActivityRoomList } from "@/api/invigilation";
 import StudentLogDetailDialog from "./StudentLogDetailDialog";
 
 export default {
@@ -129,18 +135,20 @@ export default {
   data() {
     return {
       filter: {
+        taskId: null,
         examId: null,
-        batchId: null,
-        examroom: null,
-        subjectId: null,
+        roomCode: null,
+        courseCode: null,
         content: "",
       },
       current: 1,
       total: 0,
       size: 10,
-      batchs: [],
-      exams: [],
-      subjects: [],
+      examTasks: [],
+      examBatchs: [],
+      examActivities: [],
+      examRooms: [],
+      examCourses: [],
       dataList: [
         {
           id: 1,
@@ -165,6 +173,22 @@ export default {
   methods: {
     getList() {},
     toPage() {},
+    async getExamBatchList() {
+      const res = await examBatchList();
+      this.examBatchs = res.data.data;
+    },
+    async getExamActivityRoomList() {
+      const res = await examActivityRoomList(this.filter.examId);
+      this.examActivities = res.data.data.examActivitys;
+      this.examRooms = res.data.data.examRooms;
+      this.examCourses = res.data.data.examCourses;
+    },
+    examChange() {
+      this.filter.examActivityId = null;
+      this.filter.roomCode = null;
+      this.filter.courseCode = null;
+      this.getExamActivityRoomList();
+    },
     toDetail(row) {
       console.log(row);
       this.curLogId = row.id;

+ 80 - 47
src/features/invigilation/WainingManage/WainingManage.vue

@@ -13,10 +13,10 @@
             <el-select
               v-model="filter.examId"
               placeholder="请选择批次"
-              clearable
+              @change="examChange"
             >
               <el-option
-                v-for="item in batchs"
+                v-for="item in examBatchs"
                 :key="item.id"
                 :value="item.id"
                 :label="item.name"
@@ -30,10 +30,10 @@
               clearable
             >
               <el-option
-                v-for="item in exams"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="item in examRooms"
+                :key="item.roomCode"
+                :value="item.roomCode"
+                :label="item.roomName"
               ></el-option>
             </el-select>
           </el-form-item>
@@ -44,10 +44,10 @@
               clearable
             >
               <el-option
-                v-for="item in subjects"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="item in examCourses"
+                :key="item.courseCode"
+                :value="item.courseCode"
+                :label="item.courseName"
               ></el-option>
             </el-select>
           </el-form-item>
@@ -58,10 +58,10 @@
               clearable
             >
               <el-option
-                v-for="item in subjects"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
+                v-for="(val, key) in APPROVE_STATUS"
+                :key="key"
+                :value="key"
+                :label="val"
               ></el-option>
             </el-select>
           </el-form-item>
@@ -125,51 +125,47 @@
           <el-button type="primary" icon="icon icon-clean" @click="cleanUnread"
             >清除未阅</el-button
           >
-          <el-button type="danger" icon="icon icon-warning" @click="batchAction"
+          <!-- <el-button type="danger" icon="icon icon-warning" @click="batchAction"
             >批量处理违纪</el-button
-          >
+          > -->
         </div>
       </div>
     </div>
 
-    <!-- <div class="part-box-head">
-      <div class="part-box-head-left"><h1>预警提醒</h1></div>
-    </div>
-    <div class="part-filter">
-      <div class="part-filter-form">
-
-        <div class="part-filter-form-action">
-
-        </div>
-      </div>
-    </div> -->
-
     <el-table
       ref="TableList"
       :data="dataList"
       @selection-change="handleSelectionChange"
     >
       <el-table-column type="selection" width="55" align="center" />
-      <el-table-column prop="examId" label="批次"></el-table-column>
-      <el-table-column prop="examName" label="场次"></el-table-column>
+      <el-table-column prop="examName" label="批次"></el-table-column>
+      <el-table-column prop="examActivityCode" label="场次"></el-table-column>
       <el-table-column prop="roomName" label="考场"> </el-table-column>
       <el-table-column prop="examId" label="考试ID"></el-table-column>
       <el-table-column prop="identity" label="证件号"></el-table-column>
       <el-table-column prop="name" label="姓名"></el-table-column>
-      <el-table-column prop="subjectName" label="科目名称"></el-table-column>
-      <el-table-column prop="courseName" label="科目(代码)">
-        <span slot-scope="scope"
-          >{{ scope.row.courseName }}({{ scope.row.courseCode }})</span
-        >
-      </el-table-column>
+      <el-table-column prop="courseName" label="科目名称"></el-table-column>
+      <el-table-column prop="courseCode" label="科目代码"> </el-table-column>
       <el-table-column
         prop="multipleFaceCount"
         label="陌生人脸"
       ></el-table-column>
       <el-table-column prop="exceptionCount" label="异常处理"></el-table-column>
       <el-table-column prop="warningCount" label="预警数"></el-table-column>
-      <el-table-column prop="breachStatus" label="是否违纪"></el-table-column>
-      <el-table-column prop="approveStatus" label="审阅状态"></el-table-column>
+      <el-table-column prop="breachStatus" label="是否违纪">
+        <template slot-scope="scope">
+          <span :class="{ 'color-danger': scope.row.breachStatus }">
+            {{ scope.row.breachStatus | zeroOneYesNoFilter }}
+          </span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="approveStatus" label="审阅状态">
+        <template slot-scope="scope">
+          <span :class="{ 'color-danger': !scope.row.approveStatus }">
+            {{ scope.row.approveStatus | zeroOneApproveStatusFilter }}
+          </span>
+        </template>
+      </el-table-column>
       <el-table-column label="操作">
         <template slot-scope="scope">
           <el-button
@@ -189,6 +185,7 @@
         :current-page="current"
         :total="total"
         :page-size="size"
+        @size-change="toPage(1)"
         @current-change="toPage"
       >
       </el-pagination>
@@ -198,11 +195,14 @@
 
 <script>
 import {
+  examBatchList,
+  examActivityRoomList,
   invigilationWarningList,
   batchInvigilation,
   clearInvigilationUnreadWarningList,
 } from "@/api/invigilation";
 
+import { APPROVE_STATUS } from "@/constant/constants";
 export default {
   name: "WainingManage",
   data() {
@@ -213,24 +213,35 @@ export default {
         courseCode: null,
         approveStatus: null,
         name: "",
-        maxMultipleFaceCount: null,
-        minMultipleFaceCount: null,
-        maxExceptionCount: null,
-        minExceptionCount: null,
-        maxWarningCount: null,
-        minWarningCount: null,
+        maxMultipleFaceCount: undefined,
+        minMultipleFaceCount: undefined,
+        maxExceptionCount: undefined,
+        minExceptionCount: undefined,
+        maxWarningCount: undefined,
+        minWarningCount: undefined,
       },
+      APPROVE_STATUS,
       multipleSelection: [],
       current: 1,
       total: 0,
       size: 10,
-      batchs: [],
-      exams: [],
-      subjects: [],
+      examBatchs: [],
+      examActivities: [],
+      examRooms: [],
+      examCourses: [],
       dataList: [],
     };
   },
+  mounted() {
+    this.initData();
+  },
   methods: {
+    async initData() {
+      await this.getExamBatchList();
+      this.filter.examId = this.examBatchs[0] && this.examBatchs[0].id;
+      this.toPage(1);
+      this.getExamActivityRoomList();
+    },
     async getList() {
       const datas = {
         ...this.filter,
@@ -247,6 +258,28 @@ export default {
       this.current = page;
       this.getList();
     },
+    async getExamBatchList() {
+      const user = this.$store.state.user;
+      const userId =
+        user.roleCodes.includes("INVIGILATE") ||
+        user.roleCodes.includes("INSPECTION")
+          ? user.id
+          : null;
+      const res = await examBatchList(userId);
+      this.examBatchs = res.data.data;
+    },
+    async getExamActivityRoomList() {
+      const res = await examActivityRoomList(this.filter.examId);
+      this.examActivities = res.data.data.examActivitys;
+      this.examRooms = res.data.data.examRooms;
+      this.examCourses = res.data.data.examCourses;
+    },
+    examChange() {
+      this.filter.examActivityId = null;
+      this.filter.roomCode = null;
+      this.filter.courseCode = null;
+      this.getExamActivityRoomList();
+    },
     handleSelectionChange(val) {
       this.multipleSelection = val;
     },

+ 155 - 2
src/features/invigilation/common/EchartRender.vue

@@ -23,7 +23,11 @@ export default {
       type: String,
       required: true,
       validator(value) {
-        return ["bar", "pie", "line", "barGroup"].indexOf(value) !== -1;
+        return (
+          ["bar", "pieAnnulus", "line", "barGroup", "pie", "lineMark"].indexOf(
+            value
+          ) !== -1
+        );
       },
     },
     rendererType: {
@@ -207,7 +211,103 @@ export default {
         ],
       };
     },
-    getPieOption(datas) {
+    getLineMarkOption(datas) {
+      if (!datas.length) return;
+
+      return {
+        animation: this.animationIsOpen,
+        grid: {
+          top: "18%",
+          bottom: "12%",
+          left: "5%",
+          right: "5%",
+        },
+        tooltip: {
+          show: false,
+        },
+        xAxis: {
+          type: "category",
+          data: datas.map((item) => item.name),
+          splitLine: {
+            show: true,
+            lineStyle: {
+              color: "#F0F4F9",
+            },
+          },
+          axisLine: {
+            lineStyle: {
+              color: "#BDC8D9",
+            },
+          },
+          axisLabel: {
+            color: "#8C94AC",
+            fontSize: 14,
+            margin: 14,
+          },
+          axisTick: {
+            show: false,
+          },
+        },
+        yAxis: {
+          type: "value",
+          splitLine: {
+            show: true,
+            lineStyle: {
+              color: "#F0F4F9",
+            },
+          },
+          axisLine: {
+            show: false,
+          },
+          axisLabel: {
+            fontSize: 14,
+            color: "#8C94AC",
+            // formatter: function (value) {
+            //   return value + "%";
+            // },
+          },
+          axisTick: {
+            show: false,
+          },
+        },
+        series: [
+          {
+            name: "数量",
+            type: "line",
+            symbolSize: 8,
+            data: datas.map((item) => item.count),
+            itemStyle: {
+              color: "#3A93FB",
+            },
+            lineStyle: {
+              color: "#3A93FB",
+            },
+            markLine: {
+              symbol: "none",
+              data: [
+                {
+                  type: "average",
+                  lineStyle: {
+                    color: "#626A82",
+                  },
+                  label: {
+                    position: "insideEndTop",
+                    formatter: "平均科次:{c}",
+                  },
+                  emphasis: {
+                    lineStyle: {
+                      type: "dotted",
+                      color: "#1886FE",
+                    },
+                  },
+                },
+              ],
+            },
+          },
+        ],
+      };
+    },
+    getPieAnnulusOption(datas) {
       if (!datas.dataList.length) return;
 
       const colors = {
@@ -283,6 +383,58 @@ export default {
         ],
       };
     },
+    getPieOption(datas) {
+      if (!datas.length) return;
+
+      const seriesData = datas.map((item) => {
+        return {
+          name: item.name,
+          value: item.count,
+        };
+      });
+
+      return {
+        animation: this.animationIsOpen,
+        color: ["#1886FE", "#FECA57", "#5FC9FA", "#FE5863", "#dd7755"],
+        grid: {
+          top: "20%",
+          bottom: "1%",
+        },
+        tooltip: {
+          show: false,
+        },
+        legend: {
+          show: true,
+          bottom: "1%",
+          itemGap: 20,
+          itemWidth: 8,
+          itemHeight: 8,
+          icon: "circle",
+          textStyle: {
+            color: "#8C94AC",
+            fontSize: 14,
+            padding: [0, 0, 0, 5],
+          },
+        },
+        series: [
+          {
+            name: "占比",
+            type: "pie",
+            radius: [0, "60%"],
+            data: seriesData,
+            itemStyle: {
+              borderWidth: 2,
+              borderColor: "#fff",
+            },
+            label: {
+              fontSize: 14,
+              color: "#626A82",
+              formatter: "{b}:{d}%",
+            },
+          },
+        ],
+      };
+    },
     getBarOption(datas) {
       if (!datas.dataList.length) return;
       const colors = {
@@ -409,6 +561,7 @@ export default {
           },
         },
         legend: {
+          show: false,
           data: legendData,
           top: "2%",
           right: 0,

+ 12 - 5
src/features/invigilation/common/InvigilationStudent.vue

@@ -6,9 +6,6 @@
         :live-url="data.monitorLiveUrl"
         v-if="data.monitorLiveUrl"
       ></flv-media>
-      <div class="student-video-none" v-else>
-        <i class="el-icon-video-camera-solid"></i>
-      </div>
     </div>
     <div class="student-info">
       <h6>
@@ -18,9 +15,11 @@
       <p>
         <span>证件号:</span><span>{{ data.identity }}</span>
       </p>
-      <p><span>答题进度:</span><span>20%</span></p>
+      <p>
+        <span>答题进度:</span><span>{{ data.progress }}</span>
+      </p>
       <div class="student-time">
-        <i class="el-icon-alarm-clock"></i>
+        <i class="icon icon-alarm-clock"></i>
         <span>50:32:15</span>
       </div>
     </div>
@@ -50,6 +49,7 @@ export default {
         "invigilation-student",
         {
           "invigilation-student-warning": this.data.warning,
+          "invigilation-student-netbreak": this.data.netbreak,
         },
       ];
     },
@@ -150,9 +150,16 @@ export default {
     border-radius: 12px;
     background: #abb8c9;
     color: #fff;
+    line-height: 16px;
     font-size: 12px;
     > i {
       margin-right: 5px;
+      width: 12px;
+      height: 12px;
+    }
+    > span {
+      display: inline-block;
+      vertical-align: middle;
     }
   }
 

+ 1 - 1
src/features/invigilation/common/RightOrWrong.vue

@@ -7,7 +7,7 @@ export default {
   name: "right-or-wrong",
   props: {
     status: {
-      type: Boolean,
+      type: [Boolean, Number],
     },
   },
   computed: {

+ 39 - 16
src/features/invigilation/common/SummaryLine.vue

@@ -20,13 +20,13 @@
         <span class="line-name" slot="reference">{{ item.name }}</span>
       </el-popover>
       <span class="line-name" v-else>{{ item.name }}</span>
-      <span>{{ examPropData[item.param] }}</span>
+      <span>{{ examPropData[item.param] }}{{ item.unit }}</span>
     </p>
   </div>
 </template>
 
 <script>
-// import { examPropCount } from "@/api/invigilation";
+import { examPropCount } from "@/api/invigilation";
 
 const paramInfo = {
   all: {
@@ -34,42 +34,56 @@ const paramInfo = {
     param: "allCount",
     desc: "参加考试的全部考生。",
     icon: "users",
+    unit: "人",
   },
-  login: {
-    name: "已登录",
-    param: "loginCount",
-    desc: "已成功登录考生端的考生。",
-    pointType: "info",
+  finish: {
+    name: "完成率",
+    param: "completionRate",
+    desc: "",
+    icon: "rate",
+    unit: "%",
   },
   prepare: {
     name: "已待考",
     param: "prepareCount",
     desc: "已进入待考界面等待开考的考生。",
     pointType: "success",
+    unit: "人",
   },
   exam: {
     name: "考试中",
-    param: "notComplete",
+    param: "examCount",
     desc: "正在答题的考生。",
     pointType: "primary",
+    unit: "人",
   },
   complete: {
     name: "已交卷",
     param: "alreadyComplete",
     desc: "",
     pointType: "danger",
+    unit: "人",
   },
   trouble: {
     name: "通讯故障",
-    param: "trouble",
+    param: "clientCommunicationStatusCount",
     desc:
       "考生端出现断网、断电、软硬件故障等异常导致考生端与监考端无法正常连接的考生。",
     pointType: "danger",
+    unit: "人",
+  },
+  unfinish: {
+    name: "未完成",
+    param: "notComplete",
+    desc: "",
+    pointType: "danger",
+    unit: "人",
   },
 };
 const types = {
-  trouble: ["all", "login", "prepare", "exam", "trouble"],
-  complete: ["all", "login", "prepare", "exam", "complete"],
+  trouble: ["all", "prepare", "exam", "trouble"],
+  complete: ["all", "prepare", "exam", "complete"],
+  progress: ["all", "finish", "prepare", "unfinish"],
 };
 
 export default {
@@ -83,12 +97,20 @@ export default {
       type: String,
       required: true,
       validator: (val) => {
-        return ["trouble", "complete"].includes(val);
+        return ["trouble", "complete", "progress"].includes(val);
       },
     },
   },
-  created() {
-    this.initData();
+  // mounted() {
+  //   this.initData();
+  // },
+  watch: {
+    examId: {
+      immediate: true,
+      handler() {
+        this.initData();
+      },
+    },
   },
   data() {
     return {
@@ -98,8 +120,9 @@ export default {
   },
   methods: {
     async initData() {
-      // const examPropData = await examPropCount(this.examId).catch(() => {});
-      // this.examPropData = examPropData || {};
+      if (!this.examId) return;
+      const res = await examPropCount(this.examId).catch(() => {});
+      this.examPropData = res.data.data || {};
       this.paramList = types[this.dataType].map((item) => {
         let info = paramInfo[item];
         return {

+ 4 - 1
src/features/system/OrgManagement/OrgManagement.vue

@@ -16,7 +16,9 @@
             <StateSelect v-model="form.enableState"></StateSelect>
           </el-form-item>
           <el-form-item>
-            <el-button type="primary" @click="searchForm">查询</el-button>
+            <el-button type="primary" @click="handleCurrentChange(0)"
+              >查询</el-button
+            >
           </el-form-item>
         </el-form>
         <div class="part-filter-form-action">
@@ -76,6 +78,7 @@
     </el-table>
     <div class="part-page">
       <el-pagination
+        background
         @current-change="handleCurrentChange"
         :current-page="currentPage"
         :page-size="pageSize"

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

@@ -90,7 +90,9 @@
         </el-form-item>
       </el-row>
       <el-row class="d-flex justify-content-center">
-        <el-button type="primary" @click="submitForm">保 存</el-button>
+        <el-button type="primary" @click="submitForm" :loading="loading"
+          >保 存</el-button
+        >
         <el-button @click="closeDialog">取 消</el-button>
       </el-row>
     </el-form>
@@ -116,6 +118,7 @@ export default {
       visible: false,
       form: {},
       rules: {},
+      loading: false,
     };
   },
   watch: {
@@ -153,9 +156,15 @@ export default {
       if (this.isEdit) {
         data = { ...data, id: this.org.id };
       }
-      await saveOrg(data);
-      this.$emit("reload");
-      this.closeDialog();
+      try {
+        this.loading = true;
+        await saveOrg(data);
+        this.$emit("reload");
+        this.$notify({ title: "保存成功", type: "success" });
+        this.closeDialog();
+      } finally {
+        this.loading = false;
+      }
     },
   },
 };

+ 4 - 2
src/features/system/UserManagement/UserManagement.vue

@@ -22,7 +22,9 @@
             <StateSelect v-model="form.enableState"></StateSelect>
           </el-form-item>
           <el-form-item>
-            <el-button type="primary" @click="searchForm">查询</el-button>
+            <el-button type="primary" @click="handleCurrentChange(0)"
+              >查询</el-button
+            >
           </el-form-item>
         </el-form>
         <div class="part-filter-form-action">
@@ -35,7 +37,6 @@
     </div>
 
     <el-table :data="tableData">
-      <el-table-column type="selection" width="42" />
       <el-table-column width="100" label="ID">
         <span slot-scope="scope">{{ scope.row.id }}</span>
       </el-table-column>
@@ -95,6 +96,7 @@
 
     <div class="part-page">
       <el-pagination
+        background
         @current-change="handleCurrentChange"
         :current-page="currentPage"
         :page-size="pageSize"

+ 30 - 6
src/features/system/UserManagement/UserManagementDialog.vue

@@ -47,7 +47,7 @@
       </el-row>
       <el-row>
         <el-form-item label="角色" prop="roleCode">
-          <RoleSelect v-model="form.roleCode" multiple />
+          <RoleSelect v-model="form.roleCode" :multiple="true" />
         </el-form-item>
       </el-row>
       <el-row>
@@ -68,7 +68,9 @@
         </el-form-item>
       </el-row>
       <el-row class="d-flex justify-content-center">
-        <el-button type="primary" @click="submitForm">保 存</el-button>
+        <el-button type="primary" @click="submitForm" :loading="loading"
+          >保 存</el-button
+        >
         <el-button @click="closeDialog">取 消</el-button>
       </el-row>
     </el-form>
@@ -89,12 +91,20 @@ export default {
     isEdit() {
       return this.user.id;
     },
+    rules() {
+      return {
+        loginName: [{ required: true, message: "登录名必填" }],
+        name: [{ required: true, message: "姓名必填" }],
+        password: [{ required: !this.isEdit, message: "密码必填" }],
+        roleCode: [{ required: true, message: "角色必填" }],
+      };
+    },
   },
   data() {
     return {
       visible: false,
       form: {},
-      rules: {},
+      loading: false,
     };
   },
   watch: {
@@ -122,13 +132,27 @@ export default {
       this.visible = false;
     },
     async submitForm() {
+      try {
+        const valid = await this.$refs.form.validate();
+        if (!valid) return;
+      } catch (error) {
+        console.log(error);
+        return;
+      }
+
       let data = this.form;
       if (this.isEdit) {
         data = { ...data, id: this.user.id };
       }
-      await saveUser(data);
-      this.$emit("reload");
-      this.closeDialog();
+      try {
+        this.loading = true;
+        await saveUser(data);
+        this.$emit("reload");
+        this.$notify({ title: "保存成功", type: "success" });
+        this.closeDialog();
+      } finally {
+        this.loading = false;
+      }
     },
   },
 };

+ 6 - 0
src/filters/index.js

@@ -1,5 +1,6 @@
 import Vue from "vue";
 import { dateFormatForAPI } from "@/utils/utils";
+import { APPROVE_STATUS } from "@/constant/constants";
 
 Vue.filter("booleanYesNoFilter", function (val) {
   if (val === null) return "无";
@@ -54,3 +55,8 @@ Vue.filter("modeFilter", function (val) {
   if (val === null) return "无";
   return { TOGETHER: "集中统一", ANYTIME: "随到随考" }[val];
 });
+
+Vue.filter("zeroOneApproveStatusFilter", function (val) {
+  if (val === null) return "";
+  return APPROVE_STATUS[val];
+});

+ 1 - 0
src/plugins/VueCharts.js

@@ -9,6 +9,7 @@ import "echarts/lib/component/title";
 import "echarts/lib/component/legend";
 import "echarts/lib/component/tooltip";
 import "echarts/lib/component/dataZoom";
+import "echarts/lib/component/markLine";
 // import "echarts/lib/component/geo";
 // import "echarts/lib/component/visualMap";
 

+ 6 - 1
src/router/index.js

@@ -34,6 +34,11 @@ Vue.use(VueRouter);
 // }
 
 const routes = [
+  {
+    path: "/",
+    name: "Index",
+    redirect: { name: "Login" },
+  },
   {
     path: "/home",
     component: Layout,
@@ -169,7 +174,7 @@ const routes = [
       import(/* webpackChunkName: "Login" */ "../features/Login/Login.vue"),
   },
   {
-    path: "/*",
+    path: "*",
     name: "404",
     component: () =>
       import(/* webpackChunkName: "default" */ "../views/404.vue"),

+ 2 - 0
src/store/index.js

@@ -2,6 +2,7 @@ import Vue from "vue";
 import Vuex from "vuex";
 import createPersistedState from "vuex-persistedstate";
 import user from "./modules/user";
+import invigilation from "./modules/invigilation";
 
 Vue.use(Vuex);
 
@@ -9,6 +10,7 @@ export default new Vuex.Store({
   plugins: [createPersistedState({ storage: window.sessionStorage })],
   modules: {
     user,
+    invigilation,
   },
   state: {
     isFullScreen: false,

+ 51 - 0
src/store/modules/invigilation.js

@@ -0,0 +1,51 @@
+import {
+  invigilateCount,
+  invigilationWarningCount,
+  reexamPendingCount,
+} from "@/api/invigilation";
+
+const state = {
+  navTips: {
+    RealtimeMonitoring: {
+      val: "",
+      classes: "nav-item-tips-popover",
+    },
+    WainingManage: {
+      val: "",
+      classes: "nav-item-tips-num",
+    },
+    ReexamPending: {
+      val: "",
+      classes: "nav-item-tips-num",
+    },
+  },
+};
+
+const mutations = {
+  setRealtimeMonitoring(state, val) {
+    state.navTips.RealtimeMonitoring.val = val ? "考试" : null;
+  },
+  setWainingManage(state, val) {
+    state.navTips.WainingManage.val = val;
+  },
+  setReexamPending(state, val) {
+    state.navTips.ReexamPending.val = val;
+  },
+};
+
+const actions = {
+  async fetchRealtimeMonitoringCount({ commit }, datas) {
+    const res = await invigilateCount(datas);
+    commit("setRealtimeMonitoring", res.data.data.count);
+  },
+  async fetchWainingManageCount({ commit }, datas) {
+    const res = await invigilationWarningCount(datas);
+    commit("setWainingManage", res.data.data.count);
+  },
+  async fetchReexamPendingCount({ commit }, datas) {
+    const res = await reexamPendingCount(datas);
+    commit("setReexamPending", res.data.data.count);
+  },
+};
+
+export default { namespaced: true, state, mutations, actions };

+ 2 - 2
src/store/modules/user.js

@@ -1,6 +1,6 @@
 import { loginByUsername, logout } from "@/api/login";
 // import { removeKeyToken, setKeyToken } from "@/auth/auth";
-import { omit } from "lodash-es";
+// import { omit } from "lodash-es";
 import { LOGIN_BY_USERNAME, LOG_OUT, FED_LOG_OUT } from "../action-types";
 import { setToken } from "@/auth/auth";
 
@@ -25,7 +25,7 @@ const user = {
     [LOGIN_BY_USERNAME]({ commit }, userInfo) {
       return loginByUsername(userInfo).then((response) => {
         const data = response.data.data;
-        commit("SET_USER", omit(data.account, ["token", "uid"]));
+        commit("SET_USER", { ...data.account, roleCodes: data.roleCodes });
         setToken(data.accessToken);
       });
     },

+ 31 - 1
src/styles/base.scss

@@ -37,6 +37,17 @@ body {
   font-size: 14px;
 }
 
+// fomate
+.color-primay {
+  color: #1886fe;
+}
+.color-success {
+  color: #1cd1a1;
+}
+.color-danger {
+  color: #fe5863;
+}
+
 // part-box
 .part-box {
   padding: 20px;
@@ -126,6 +137,21 @@ body {
 .part-page {
   margin-top: 20px;
   text-align: right;
+  &::after {
+    content: "";
+    display: block;
+    clear: both;
+    visibility: hidden;
+  }
+
+  .page-info {
+    float: left;
+    line-height: 32px;
+  }
+  .el-pagination {
+    float: right;
+    padding: 0;
+  }
 }
 
 .clear-float {
@@ -188,6 +214,7 @@ body {
 // summary-line
 .summary-line {
   font-size: 0;
+  min-height: 32px;
   padding: 6px 10px 6px 0;
   &-item {
     font-size: 14px;
@@ -284,7 +311,10 @@ body {
 .msg-monitor-magbox {
   width: 400px;
   .el-notification__closeBtn {
-    top: 28px;
+    top: 25px;
+  }
+  .el-notification__content {
+    margin: 0;
   }
 }
 

+ 27 - 1
src/styles/element-ui-custom.scss

@@ -9,7 +9,14 @@
     border-radius: 6px;
     border-color: #e8edf3;
     background-color: #f0f4f9;
-    padding-left: 12px;
+  }
+}
+.el-input-group--append {
+  .el-input__inner {
+    border-radius: 6px 0 0 6px;
+  }
+  .el-input-group__append {
+    border-color: #e8edf3;
   }
 }
 .el-input-number {
@@ -87,6 +94,19 @@
     .el-checkbox {
       margin-bottom: 0;
     }
+    .btn-table-icon {
+      padding: 5px 12px 6px;
+      font-size: 14px;
+      &.el-button--primary {
+        background-color: #5fc9fa;
+        border-color: #5fc9fa;
+
+        &:hover {
+          background-color: mix(#fff, #5fc9fa, 10%);
+          border-color: mix(#fff, #5fc9fa, 10%);
+        }
+      }
+    }
   }
 }
 
@@ -143,6 +163,12 @@
       padding-right: 10px;
     }
   }
+  &__editor {
+    height: 32px;
+    &.el-input .el-input__inner {
+      height: 32px;
+    }
+  }
 }
 
 // el-dialog

+ 6 - 0
src/styles/icons.scss

@@ -72,6 +72,9 @@
   &-reexam {
     background-image: url(../assets/icon-reexam.png);
   }
+  &-password {
+    background-image: url(../assets/icon-password.png);
+  }
   &-seal {
     background-image: url(../assets/icon-seal.png);
   }
@@ -115,6 +118,9 @@
   &-exam-detail {
     background-image: url(../assets/icon-exam-detail.png);
   }
+  &-alarm-clock {
+    background-image: url(../assets/icon-alarm-clock.png);
+  }
   &-net-break {
     background-image: url(../assets/icon-net-break.png);
   }

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

@@ -17,6 +17,7 @@ import NavBar from "./components/NavBar.vue";
 import { sysMenu } from "@/api/system-user";
 import localMenu from "./components/menu";
 import { deepCopy } from "@/utils/utils";
+import { mapMutations } from "vuex";
 
 export default {
   name: "Layout",
@@ -37,9 +38,16 @@ export default {
     this.getMenu();
   },
   methods: {
+    ...mapMutations("inviligation", [
+      "fetchRealtimeMonitoringCount",
+      "fetchWainingManageCount",
+      "fetchReexamPendingCount",
+    ]),
     navChange(name) {
       const nav = this.navs.find((item) => item.name === name);
       this.curMenus = nav.children || [];
+
+      // if (name === "Invigilation") this.initNavTips();
     },
     async getMenu() {
       const res = await sysMenu();
@@ -107,6 +115,41 @@ export default {
       this.validRoutes.push("Home");
       return clearNoFixed(localNavs);
     },
+    initNavTips() {
+      let validNav = {
+        RealtimeMonitoring: {
+          valid: false,
+          func: this.fetchRealtimeMonitoringCount,
+        },
+        WainingManage: {
+          valid: false,
+          func: this.fetchWainingManageCount,
+        },
+        ReexamPending: {
+          valid: false,
+          func: this.fetchReexamPendingCount,
+        },
+      };
+      const validNavNames = Object.keys(validNav);
+      this.menus.forEach((nav) => {
+        nav.children.forEach((subnav) => {
+          if (validNavNames.includes(subnav.name)) {
+            validNav[subnav.name].valid = true;
+          }
+        });
+      });
+
+      const user = this.$store.state.user;
+      const datas =
+        user.roleCodes.includes("INVIGILATE") ||
+        user.roleCodes.includes("INSPECTION")
+          ? { userId: user.id }
+          : {};
+
+      validNavNames.forEach((item) => {
+        if (validNav[item].valid) validNav[item].func.call(this, datas);
+      });
+    },
   },
 };
 </script>

+ 49 - 1
src/views/Layout/components/SideBar.vue

@@ -19,7 +19,11 @@
           >
             <p>
               <span>{{ subnav.title }}</span>
-              <span></span>
+              <span
+                v-if="navHasTips(subnav) && navTips[subnav.name].val"
+                :class="['nav-item-tips', navTips[subnav.name].classes]"
+                >{{ navTips[subnav.name].val }}</span
+              >
             </p>
           </li>
         </ul>
@@ -30,6 +34,7 @@
 
 <script>
 import ScrollBar from "./ScrollBar";
+import { mapState } from "vuex";
 
 export default {
   name: "SideBar",
@@ -46,6 +51,7 @@ export default {
     return {};
   },
   computed: {
+    ...mapState("invigilation", ["navTips"]),
     curRouterName() {
       return this.$route.meta.relate || this.$route.name;
     },
@@ -55,6 +61,9 @@ export default {
       // this.curRouterName = nav.name;
       this.$router.push({ name: nav.name });
     },
+    navHasTips(nav) {
+      return Object.keys(this.navTips).includes(nav.name);
+    },
   },
 };
 </script>
@@ -110,5 +119,44 @@ export default {
     color: #1886fe;
     font-weight: 600;
   }
+  .nav-sub-item {
+    span {
+      display: inline-block;
+      vertical-align: middle;
+    }
+
+    span.nav-item-tips {
+      height: 18px;
+      line-height: 18px;
+      padding: 0 6px;
+      color: #fff;
+      font-size: 12px;
+      border-radius: 3px;
+      margin-left: 10px;
+    }
+
+    span.nav-item-tips-num {
+      background: #5fc9fa;
+    }
+    span.nav-item-tips-popover {
+      background: #ff9f43;
+      position: relative;
+      margin-left: 15px;
+
+      &::before {
+        content: "";
+        display: block;
+        position: absolute;
+        left: -5px;
+        top: 50%;
+        margin-top: -4px;
+        width: 0;
+        height: 0;
+        border-style: solid;
+        border-width: 4px;
+        border-color: transparent #ff9f43 #ff9f43 transparent;
+      }
+    }
+  }
 }
 </style>

+ 4 - 2
vue.config.js

@@ -1,11 +1,13 @@
 let proxy = {
   "/api": {
-    target: "http://192.168.10.36:6001/",
+    target: "http://192.168.10.26:6001/",
+    // target: "http://192.168.10.36:6001/",
     // target: "http://192.168.10.86:6001/",
     changeOrigin: true,
   },
   "/file": {
-    target: "http://192.168.10.36:6001/",
+    target: "http://192.168.10.26:6001/",
+    // target: "http://192.168.10.36:6001/",
     // target: "http://192.168.10.86:6001/",
     changeOrigin: true,
   },

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov