Browse Source

privilige add

zhangjie 4 years ago
parent
commit
9b0648b709
27 changed files with 1687 additions and 465 deletions
  1. 34 16
      src/api/invigilation.js
  2. 4 0
      src/api/system-user.js
  3. 10 0
      src/constant/constants.js
  4. 9 69
      src/features/invigilation/InvigilationDetail/InvigilationDetail.vue
  5. 72 108
      src/features/invigilation/OnlinePatrol/OnlinePatrol.vue
  6. 2 1
      src/features/invigilation/RealtimeMonitoring/ExamBatchDialog.vue
  7. 146 110
      src/features/invigilation/RealtimeMonitoring/RealtimeMonitoring.vue
  8. 160 0
      src/features/invigilation/RealtimeMonitoring/StudentBreachDialog.vue
  9. 297 36
      src/features/invigilation/RealtimeMonitoring/WainingDetail.vue
  10. 124 0
      src/features/invigilation/RealtimeMonitoring/handleRollupDialog.vue
  11. 33 18
      src/features/invigilation/ReexamApply/ApplyReexamDialog.vue
  12. 45 5
      src/features/invigilation/ReexamApply/ReexamApply.vue
  13. 1 3
      src/features/invigilation/ReexamChecked/ReexamChecked.vue
  14. 6 3
      src/features/invigilation/ReexamPending/CheckReexamDialog.vue
  15. 18 4
      src/features/invigilation/ReexamPending/ReexamPending.vue
  16. 1 1
      src/features/invigilation/WainingManage/WainingManage.vue
  17. 205 0
      src/features/invigilation/common/MultipleSelectStudent.vue
  18. 113 0
      src/features/invigilation/common/SummaryLine.vue
  19. 41 0
      src/features/invigilation/common/TextClock.vue
  20. 1 0
      src/router/index.js
  21. 147 46
      src/styles/base.scss
  22. 68 0
      src/styles/element-ui-custom.scss
  23. 3 0
      src/styles/icons.scss
  24. 41 0
      src/utils/utils.js
  25. 76 13
      src/views/Layout/Layout.vue
  26. 23 25
      src/views/Layout/components/NavBar.vue
  27. 7 7
      src/views/Layout/components/menu.js

+ 34 - 16
src/api/invigilation.js

@@ -18,6 +18,15 @@ export function invigilateVideoList(datas) {
   );
 }
 
+// online-patrol
+export function patrolList(datas) {
+  const data = pickBy(datas, (v) => v !== "");
+  return httpApp.post(
+    "/api/admin/invigilate/patrol/list?" + object2QueryString(data),
+    {}
+  );
+}
+
 // 强制/手动交卷接口
 export function invigilateFinish(datas) {
   const data = pickBy(datas, (v) => v !== "");
@@ -27,12 +36,6 @@ export function invigilateFinish(datas) {
 export function invigilateExamFinish(examId) {
   return httpApp.post("/api/admin/invigilate/exam/finish?examId=" + examId, {});
 }
-export function invigilateDetail(recordId) {
-  return httpApp.post(
-    "/api/admin/invigilate/list/detail?examRecordId=" + recordId,
-    {}
-  );
-}
 
 // 考试列表
 export function examList(datas) {
@@ -52,16 +55,6 @@ export function examPropCount(examId) {
   return httpApp.post("/api/admin/exam/prop/count?examId=" + examId, {});
 }
 
-export function warningStudentDetail({ recordId }) {
-  const data = {
-    recordId,
-  };
-  return httpApp.post(
-    "/api/admin/monitor/call/query?" + object2QueryString(data),
-    {}
-  );
-}
-
 export function communicationList({
   examActivityId,
   pageNumber = 1,
@@ -143,6 +136,31 @@ export function clearInvigilationUnreadWarningList(datas) {
   );
 }
 
+// warning-detail
+// 获取当前学生直播视频流
+export function warningStudentDetail({ recordId }) {
+  const data = {
+    recordId,
+  };
+  return httpApp.post(
+    "/api/admin/monitor/call/query?" + object2QueryString(data),
+    {}
+  );
+}
+// 获取预警详情信息
+export function invigilateDetail(recordId) {
+  return httpApp.post(
+    "/api/admin/invigilate/list/detail?examRecordId=" + recordId,
+    {}
+  );
+}
+// 学生违纪处理
+export function updateBreachInfo(datas) {
+  const data = pickBy(datas, (v) => v !== "");
+
+  return httpApp.post("/api/admin/invigilate/breach", data);
+}
+
 // reexam-apply
 export function reexamApplyList(datas) {
   const data = pickBy(datas, (v) => v !== "");

+ 4 - 0
src/api/system-user.js

@@ -48,3 +48,7 @@ export function resetUserPassword({ id, password }) {
     password: AESString(password),
   });
 }
+
+export function sysMenu() {
+  return httpApp.post("/api/admin/sys/getMenu", {});
+}

+ 10 - 0
src/constant/constants.js

@@ -26,6 +26,16 @@ export const REEXAM_TYPE = {
   1: "换批次",
 };
 
+export const BREACH_REASON_TYPE = {
+  0: "批次内12",
+  1: "换批次1",
+};
+
+export const BREACH_REPEAL_TYPE = {
+  0: "批次内334",
+  1: "换批次555",
+};
+
 export const REEXAM_REASON = {
   EXCEPTION_TIME_OUT: "异常处理时效过期",
   BREAK_TIME_OUT: "断点续考次数用完",

+ 9 - 69
src/features/invigilation/InvigilationDetail/InvigilationDetail.vue

@@ -168,69 +168,11 @@
       </div>
 
       <div class="part-filter-info">
-        <div class="part-filter-info-main summary-line">
-          <p class="summary-line-item">
-            <i class="icon icon-users"></i>
-            <el-popover
-              placement="top-start"
-              width="200"
-              trigger="hover"
-              content="全部应考:参加考试的全部考生。"
-            >
-              <span class="line-name" slot="reference">全部应考</span>
-            </el-popover>
-            <span>{{ examPropData.allCount }}人</span>
-          </p>
-          <p class="summary-line-item">
-            <i class="line-point line-point-info"></i>
-            <el-popover
-              placement="top-start"
-              width="200"
-              trigger="hover"
-              content="已登录:已成功登录考生端的考生。"
-            >
-              <span class="line-name" slot="reference">已登录</span>
-            </el-popover>
-            <span>{{ examPropData.loginCount }}人</span>
-          </p>
-          <p class="summary-line-item">
-            <i class="line-point line-point-success"></i>
-            <el-popover
-              placement="top-start"
-              width="200"
-              trigger="hover"
-              content="已待考:已进入待考界面等待开考的考生。"
-            >
-              <span class="line-name" slot="reference">已待考</span>
-            </el-popover>
-            <span>{{ examPropData.prepareCount }}人</span>
-          </p>
-          <p class="summary-line-item">
-            <i class="line-point line-point-primary"></i>
-            <el-popover
-              placement="top-start"
-              width="200"
-              trigger="hover"
-              content="考试中:正在答题的考生。"
-            >
-              <span class="line-name" slot="reference">考试中</span>
-            </el-popover>
-            <span>{{ examPropData.notComplete }}人</span>
-          </p>
-          <p class="summary-line-item">
-            <i class="line-point line-point-danger"></i>
-            <!-- <el-popover
-              placement="top-start"
-              width="200"
-              trigger="hover"
-              content="已交卷:考生交卷成功,结束考试。"
-            >
-              <span class="line-name" slot="reference">已交卷</span>
-            </el-popover> -->
-            <span class="line-name">已交卷</span>
-            <span>{{ examPropData.alreadyComplete }}人</span>
-          </p>
-        </div>
+        <summary-line
+          class="part-filter-info-main"
+          data-type="complete"
+          :exam-id="filter.examId"
+        ></summary-line>
       </div>
     </div>
 
@@ -284,15 +226,17 @@
 </template>
 
 <script>
-import { invigilationHistoryList, examPropCount } from "@/api/invigilation";
+import { invigilationHistoryList } from "@/api/invigilation";
 import { FINISH_TYPE } from "@/constant/constants";
+import SummaryLine from "../common/SummaryLine";
 
 export default {
   name: "invigilation-detail",
+  components: { SummaryLine },
   data() {
     return {
       filter: {
-        examId: null,
+        examId: "",
         examActivityId: null,
         courseCode: null,
         auditStatus: null,
@@ -313,7 +257,6 @@ export default {
       exams: [],
       subjects: [],
       dataList: [],
-      examPropData: {},
     };
   },
   methods: {
@@ -333,9 +276,6 @@ export default {
       this.current = page;
       this.getList();
     },
-    async getExamPropCount() {
-      this.examPropData = await examPropCount(this.filter.examId);
-    },
     changeFilter() {
       this.showAdvancedFilter = !this.showAdvancedFilter;
       if (!this.showAdvancedFilter) {

+ 72 - 108
src/features/invigilation/OnlinePatrol/OnlinePatrol.vue

@@ -8,7 +8,7 @@
         <el-form ref="FilterForm" label-position="left" inline>
           <el-form-item>
             <el-select
-              v-model="filter.batchId"
+              v-model="filter.examId"
               placeholder="请选择批次"
               clearable
             >
@@ -22,7 +22,7 @@
           </el-form-item>
           <el-form-item>
             <el-select
-              v-model="filter.examroom"
+              v-model="filter.examActivityId"
               placeholder="请选择场次"
               clearable
             >
@@ -36,7 +36,7 @@
           </el-form-item>
           <el-form-item>
             <el-select
-              v-model="filter.examroom"
+              v-model="filter.roomCode"
               placeholder="请选择考场"
               clearable
             >
@@ -49,11 +49,7 @@
             </el-select>
           </el-form-item>
           <el-form-item>
-            <el-select
-              v-model="filter.subjectId"
-              placeholder="筛选状态"
-              clearable
-            >
+            <el-select v-model="filter.status" placeholder="筛选状态" clearable>
               <el-option
                 v-for="item in subjects"
                 :key="item.id"
@@ -64,7 +60,7 @@
           </el-form-item>
           <el-form-item>
             <el-select
-              v-model="filter.auditStatus"
+              v-model="filter.clientWebsocketStatus"
               placeholder="通讯故障"
               clearable
             >
@@ -78,22 +74,22 @@
           </el-form-item>
           <el-form-item>
             <el-input
-              v-model.trim="filter.content"
-              placeholder="申请人姓名"
+              v-model.trim="filter.name"
+              placeholder="姓名/证件号"
               clearable
             ></el-input>
           </el-form-item>
           <el-form-item label="陌生人脸">
             <el-input-number
               style="width: 52px;"
-              v-model.trim="filter.strangePersonDown"
+              v-model.trim="filter.minMultipleFaceCount"
               placeholder="下限"
               :controls="false"
             ></el-input-number>
             <span class="line-split">-</span>
             <el-input-number
               style="width: 52px;"
-              v-model.trim="filter.strangePersonUp"
+              v-model.trim="filter.maxMultipleFaceCount"
               placeholder="上限"
               :controls="false"
             ></el-input-number>
@@ -101,14 +97,14 @@
           <el-form-item label="异常处理">
             <el-input-number
               style="width: 52px;"
-              v-model.trim="filter.exceptionDown"
+              v-model.trim="filter.minExceptionCount"
               placeholder="下限"
               :controls="false"
             ></el-input-number>
             <span class="line-split">-</span>
             <el-input-number
               style="width: 52px;"
-              v-model.trim="filter.exceptionUp"
+              v-model.trim="filter.maxExceptionCount"
               placeholder="上限"
               :controls="false"
             ></el-input-number>
@@ -116,14 +112,14 @@
           <el-form-item label="预警数">
             <el-input-number
               style="width: 52px;"
-              v-model.trim="filter.warningDown"
+              v-model.trim="filter.minWarningCount"
               placeholder="下限"
               :controls="false"
             ></el-input-number>
             <span class="line-split">-</span>
             <el-input-number
               style="width: 52px;"
-              v-model.trim="filter.warningUp"
+              v-model.trim="filter.maxWarningCount"
               placeholder="上限"
               :controls="false"
             ></el-input-number>
@@ -135,55 +131,38 @@
       </div>
 
       <div class="part-filter-info">
-        <div class="part-filter-info-main summary-line">
-          <p class="summary-line-item">
-            <i class="icon icon-users"></i
-            ><span class="line-name">全部应考</span><span>50人</span>
-          </p>
-          <p class="summary-line-item">
-            <i class="line-point line-point-info"></i
-            ><span class="line-name">已登录</span><span>5人</span>
-          </p>
-          <p class="summary-line-item">
-            <i class="line-point line-point-success"></i
-            ><span class="line-name">已待考</span><span>3人</span>
-          </p>
-          <p class="summary-line-item">
-            <i class="line-point line-point-primary"></i
-            ><span class="line-name">考试中</span><span>2人</span>
-          </p>
-          <p class="summary-line-item">
-            <i class="line-point line-point-danger"></i>
-            <el-popover
-              placement="top-start"
-              width="200"
-              trigger="hover"
-              content="这是一段内容,这是一段内容,这是一段内容,这是一段内容。"
-            >
-              <span class="line-name" slot="reference">通讯故障</span>
-            </el-popover>
-            <span>1人</span>
-          </p>
-        </div>
+        <summary-line
+          class="part-filter-info-main"
+          data-type="trouble"
+          :exam-id="filter.examId"
+        ></summary-line>
       </div>
     </div>
 
     <el-table ref="TableList" :data="dataList">
-      <el-table-column prop="examroom" 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>
+      <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="科目(代码)">
+        <span slot-scope="scope"
+          >{{ scope.row.courseName }}({{ scope.row.courseCode }})</span
+        >
+      </el-table-column>
       <el-table-column prop="subjectCode" label="剩余时间"></el-table-column>
       <el-table-column prop="subjectCode" label="进度"></el-table-column>
       <el-table-column prop="subjectCode" label="通讯"></el-table-column>
-      <el-table-column prop="subjectCode" label="更新时间"></el-table-column>
+      <el-table-column prop="updateTime" label="更新时间"></el-table-column>
+      <el-table-column prop="exceptionCount" label="异常处理"></el-table-column>
       <el-table-column
-        prop="exceptionNumber"
-        label="异常处理"
+        prop="multipleFaceCount"
+        label="陌生人脸"
       ></el-table-column>
-      <el-table-column prop="strangeNumber" label="陌生人脸"></el-table-column>
-      <el-table-column prop="warningNumber" label="预警数"></el-table-column>
-      <el-table-column prop="isDiscipline" 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 label="操作">
         <template slot-scope="scope">
           <el-button
@@ -218,25 +197,27 @@
 </template>
 
 <script>
+import { patrolList } from "@/api/invigilation";
 import EchartRender from "../common/EchartRender";
+import SummaryLine from "../common/SummaryLine";
 
 export default {
   name: "online-patrol",
-  components: { EchartRender },
+  components: { EchartRender, SummaryLine },
   data() {
     return {
       filter: {
-        batchId: null,
-        examroom: null,
-        subjectId: null,
-        auditStatus: null,
-        content: "",
-        strangePersonUp: null,
-        strangePersonDown: null,
-        exceptionUp: null,
-        exceptionDown: null,
-        warningUp: null,
-        warningDown: null,
+        examId: null,
+        roomCode: null,
+        examActivityId: null,
+        clientWebsocketStatus: null,
+        name: "",
+        maxMultipleFaceCount: null,
+        minMultipleFaceCount: null,
+        maxExceptionCount: null,
+        minExceptionCount: null,
+        maxWarningCount: null,
+        minWarningCount: null,
       },
       current: 1,
       total: 30,
@@ -244,40 +225,8 @@ export default {
       batchs: [],
       exams: [],
       subjects: [],
-      dataList: [
-        {
-          id: 1,
-          batchName: "第一批次",
-          examName: "第一场次",
-          examroom: "第一考场",
-          examId: "123456",
-          stdCardNo: "000000000000000008",
-          stdName: "张龙龙",
-          subjectName: "大学英语",
-          subjectCode: "10006",
-          strangeNumber: "0",
-          exceptionNumber: "2",
-          warningNumber: "2",
-          isDiscipline: "",
-          auditStatus: "未阅",
-        },
-        {
-          id: 2,
-          batchName: "第一批次",
-          examName: "第一场次",
-          examroom: "第一考场",
-          examId: "123456",
-          stdCardNo: "000000000000000008",
-          stdName: "张龙龙",
-          subjectName: "大学英语",
-          subjectCode: "10006",
-          strangeNumber: "0",
-          exceptionNumber: "2",
-          warningNumber: "2",
-          isDiscipline: "",
-          auditStatus: "未阅",
-        },
-      ],
+      dataList: [],
+      examPropData: {},
       statData: [
         {
           name: "苏州市第一中学",
@@ -355,13 +304,28 @@ export default {
     };
   },
   methods: {
-    getList() {},
-    toPage() {},
-    cleanUnread() {},
-    batchAction() {},
+    async getList() {
+      const datas = {
+        ...this.filter,
+        pageNumber: this.current - 1,
+        pageSize: this.size,
+      };
+
+      const res = await patrolList(datas);
+
+      this.dataList = res.data.data.records;
+      this.total = res.data.data.records.total;
+    },
+    toPage(page) {
+      this.current = page;
+      this.getList();
+    },
     toDetail(row) {
       console.log(row);
-      this.$router.push({ name: "WainingDetail", params: { id: row.id } });
+      this.$router.push({
+        name: "WainingDetail",
+        params: { recordId: row.examRecordId },
+      });
     },
   },
 };

+ 2 - 1
src/features/invigilation/RealtimeMonitoring/ExamBatchDialog.vue

@@ -4,7 +4,6 @@
     :visible.sync="dialogVisible"
     width="600px"
     title="请选择考试批次"
-    :show-close="false"
     :close-on-press-escape="false"
     :close-on-click-modal="false"
     append-to-body
@@ -46,6 +45,8 @@ export default {
         };
       });
 
+      this.examBatchId = this.examBatchs[0] && this.examBatchs[0].id;
+
       this.$emit("confirm", this.examBatchs[0]);
     },
     cancel() {

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

@@ -1,20 +1,13 @@
 <template>
   <div class="realtime-monitoring">
     <div class="realtime-top clear-float">
-      <p v-if="examName">考场名称:{{ examName }}</p>
+      <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">
-          <input
-            type="text"
-            readonly="readonly"
-            autocomplete="off"
-            placeholder="请选择批次"
-            v-model="curExamBatch.label"
-            class="el-input__inner"
-          />
+          <div class="el-input__inner">{{ curExamBatch.label }}</div>
           <span class="el-input__suffix">
             <span class="el-input__suffix-inner"
               ><i
@@ -23,7 +16,7 @@
           ></span>
         </div>
       </div>
-      <p>现在是2020年6月2日 星期二 上午 09:30:12</p>
+      <text-clock></text-clock>
     </div>
 
     <div class="part-box-head">
@@ -31,85 +24,41 @@
         <h1>实时监控台</h1>
       </div>
       <div class="part-box-head-right">
-        <el-radio-group
-          size="small"
-          v-model="pageType"
-          @change="pageTypeChange"
+        <div
+          :class="[
+            'realtime-switch',
+            { 'realtime-switch-warning': hasNewWarning },
+          ]"
         >
-          <el-radio-button label="0">
-            <i class="el-icon-s-fold"></i>列表</el-radio-button
+          <div
+            :class="[
+              'realtime-switch-item',
+              { 'realtime-switch-item-act': pageType === '0' },
+            ]"
+            @click="pageTypeChange('0')"
           >
-          <el-radio-button label="1"
-            ><i class="el-icon-video-camera"></i> 视频</el-radio-button
+            <i class="el-icon-s-fold"></i><span>列表</span>
+          </div>
+          <div
+            :class="[
+              'realtime-switch-item',
+              { 'realtime-switch-item-act': pageType === '1' },
+            ]"
+            @click="pageTypeChange('1')"
           >
-        </el-radio-group>
+            <i class="el-icon-video-camera"></i><span>视频</span>
+          </div>
+        </div>
       </div>
     </div>
 
     <div class="part-filter">
       <div class="part-filter-info">
-        <div class="part-filter-info-main summary-line">
-          <p class="summary-line-item">
-            <i class="icon icon-users"></i>
-            <el-popover
-              placement="top-start"
-              width="200"
-              trigger="hover"
-              content="全部应考:参加考试的全部考生。"
-            >
-              <span class="line-name" slot="reference">全部应考</span>
-            </el-popover>
-            <span>{{ examPropData.allCount }}人</span>
-          </p>
-          <p class="summary-line-item">
-            <i class="line-point line-point-info"></i>
-            <el-popover
-              placement="top-start"
-              width="200"
-              trigger="hover"
-              content="已登录:已成功登录考生端的考生。"
-            >
-              <span class="line-name" slot="reference">已登录</span>
-            </el-popover>
-            <span>{{ examPropData.loginCount }}人</span>
-          </p>
-          <p class="summary-line-item">
-            <i class="line-point line-point-success"></i>
-            <el-popover
-              placement="top-start"
-              width="200"
-              trigger="hover"
-              content="已待考:已进入待考界面等待开考的考生。"
-            >
-              <span class="line-name" slot="reference">已待考</span>
-            </el-popover>
-            <span>{{ examPropData.prepareCount }}人</span>
-          </p>
-          <p class="summary-line-item">
-            <i class="line-point line-point-primary"></i>
-            <el-popover
-              placement="top-start"
-              width="200"
-              trigger="hover"
-              content="考试中:正在答题的考生。"
-            >
-              <span class="line-name" slot="reference">考试中</span>
-            </el-popover>
-            <span>{{ examPropData.notComplete }}人</span>
-          </p>
-          <p class="summary-line-item">
-            <i class="line-point line-point-danger"></i>
-            <el-popover
-              placement="top-start"
-              width="200"
-              trigger="hover"
-              content="通讯故障:考生端出现断网、断电、软硬件故障等异常导致考生端与监考端无法正常连接的考生。"
-            >
-              <span class="line-name" slot="reference">通讯故障</span>
-            </el-popover>
-            <span>1人</span>
-          </p>
-        </div>
+        <summary-line
+          class="part-filter-info-main"
+          data-type="trouble"
+          :exam-id="filter.examId"
+        ></summary-line>
         <div class="part-filter-info-sub">
           <el-button
             icon="el-icon-phone-outline"
@@ -228,7 +177,8 @@
       @selection-change="handleSelectionChange"
       v-if="pageType === '0'"
     >
-      <el-table-column type="selection" width="55"> </el-table-column>
+      <el-table-column type="selection" width="55" align="center">
+      </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>
@@ -261,7 +211,7 @@
     <div class="invigilation-student-list" v-else>
       <div
         class="invigilation-student-item"
-        v-for="item in dataList"
+        v-for="item in videoList"
         :key="item.examStudentId"
       >
         <invigilation-student :data="item"></invigilation-student>
@@ -280,10 +230,17 @@
       </el-pagination>
     </div>
 
+    <!-- 考试批次选择 -->
     <exam-batch-dialog
       @confirm="examChange"
       ref="ExamBatchDialog"
     ></exam-batch-dialog>
+    <!-- 手动收卷 -->
+    <handle-rollup-dialog
+      :data-list="multipleSelection"
+      @modified="rollupOver"
+      ref="handleRollupDialog"
+    ></handle-rollup-dialog>
   </div>
 </template>
 
@@ -291,20 +248,27 @@
 import {
   invigilateList,
   invigilateVideoList,
-  invigilateFinish,
   invigilateExamFinish,
-  examPropCount,
 } from "@/api/invigilation";
 import ExamBatchDialog from "./ExamBatchDialog";
 import InvigilationStudent from "../common/InvigilationStudent";
+import SummaryLine from "../common/SummaryLine";
+import handleRollupDialog from "./handleRollupDialog";
+import TextClock from "../common/TextClock";
 
 export default {
   name: "realtime-monitoring",
-  components: { ExamBatchDialog, InvigilationStudent },
+  components: {
+    ExamBatchDialog,
+    InvigilationStudent,
+    SummaryLine,
+    handleRollupDialog,
+    TextClock,
+  },
   data() {
     return {
       filter: {
-        examId: null,
+        examId: "",
         paperDownload: null,
         status: null,
         monitorStatusSource: null,
@@ -312,10 +276,9 @@ export default {
         maxWarningCount: undefined,
         minWarningCount: undefined,
       },
-      examName: "",
+      hasNewWarning: false,
       curExamBatch: {},
       curViewingAngle: {},
-      examPropData: {},
       current: 1,
       total: 0,
       size: 10,
@@ -325,7 +288,31 @@ export default {
       exams: [],
       subjects: [],
       pageType: "0",
-      dataList: [],
+      dataList: [
+        {
+          breachStatus: 0,
+          clientCommunicationStatus: "12",
+          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,
+        },
+      ],
+      videoList: [],
       viewingAngles: [
         {
           code: "1",
@@ -354,28 +341,31 @@ export default {
       let res = null;
       if (this.pageType === "0") {
         res = await invigilateList(datas);
+        this.dataList = res.data.data.records.map((item) => {
+          item.label = `${item.identity} ${item.courseName}(${item.courseCode}) ${item.name}`;
+          return item;
+        });
       } else {
         res = await invigilateVideoList(datas);
+        this.videoList = res.data.data.records;
       }
 
-      this.dataList = res.data.data.records;
       this.total = res.data.data.records.total;
     },
     toPage(page) {
       this.current = page;
       this.getList();
     },
-    async getExamPropCount() {
-      this.examPropData = await examPropCount(this.filter.examId);
-    },
     examChange(examBatch) {
       if (!examBatch) return;
       this.filter.examId = examBatch.id;
       this.curExamBatch = examBatch;
-      this.toPage(1);
+      // this.toPage(1);
     },
-    pageTypeChange() {
+    pageTypeChange(pageType) {
+      this.pageType = pageType;
       this.toPage(1);
+      this.multipleSelection = [];
     },
     handleSelectionChange(val) {
       console.log(val);
@@ -385,20 +375,15 @@ export default {
       this.curViewingAngle = data;
     },
     async finishInvigilation() {
-      const result = await this.$confirm("确定要手动收卷吗?", {
-        confirmButtonText: "确定",
-        cancelButtonText: "取消",
-        type: "confirm",
-      }).catch(() => {});
-
-      if (!result) return;
-
-      await invigilateFinish({ examRecordId: "", type: false });
-      this.toPage(1);
-      this.$message({
-        type: "success",
-        message: "操作成功!",
-      });
+      if (!this.multipleSelection.length) {
+        this.$message.error("请先选择数据!");
+        return;
+      }
+      this.$refs.handleRollupDialog.open();
+    },
+    rollupOver() {
+      this.multipleSelection = [];
+      this.getList();
     },
     async finishInvigilationExam() {
       const result = await this.$confirm("确定要结束监考吗?", {
@@ -456,8 +441,12 @@ export default {
     line-height: 32px;
     margin: 0;
   }
+  .el-select {
+    min-width: 200px;
+  }
   > p:first-child {
     margin-right: 40px;
+    min-width: 150px;
   }
   > p:last-child {
     float: right;
@@ -465,6 +454,53 @@ export default {
     opacity: 0.8;
   }
 }
+.realtime-switch {
+  font-size: 0;
+  &-warning {
+    .realtime-switch-item {
+      &::before {
+        content: "";
+        display: block;
+        position: absolute;
+        width: 10px;
+        height: 10px;
+        top: -5px;
+        right: -5px;
+        border-radius: 50%;
+        border: 2px solid #fff;
+        background: #fe5863;
+        z-index: 9;
+      }
+    }
+  }
+
+  &-item {
+    display: inline-block;
+    vertical-align: top;
+    font-size: 12px;
+    color: #8c94ac;
+    background: #fff;
+    line-height: 18px;
+    padding: 5px 14px;
+    position: relative;
+    cursor: pointer;
+
+    > i {
+      margin-right: 5px;
+    }
+    &:first-child {
+      border-radius: 6px 0px 0px 6px;
+    }
+    &:last-child {
+      border-radius: 0px 6px 6px 0px;
+    }
+
+    &-act {
+      color: #fff;
+      background: #5fc9fa;
+    }
+  }
+}
 .invigilation-student-list {
   background: #ffffff;
   border-radius: 6px;

+ 160 - 0
src/features/invigilation/RealtimeMonitoring/StudentBreachDialog.vue

@@ -0,0 +1,160 @@
+<template>
+  <el-dialog
+    class="student-breach-dialog"
+    :visible.sync="dialogVisible"
+    width="640px"
+    title="违纪处理"
+    :close-on-press-escape="false"
+    :close-on-click-modal="false"
+    append-to-body
+    @open="opened"
+  >
+    <div class="tips-info tips-error">
+      <i class="el-icon-warning"></i>
+      <p v-if="modalForm.status">
+        本操作记录考生的违规事实,并对考生进行违纪处理,请监考老师仔细核对好考生的信息,谨慎操作!
+      </p>
+      <p v-else>
+        本操作将撤销考生撤销违纪处理,记录撤销的原因。请监考老师仔细核对好考生的信息,谨慎操作!
+      </p>
+    </div>
+    <div class="student-info">
+      <div class="student-info-item">
+        <span>证件号:</span>
+        <span>{{ modalForm.identity }}</span>
+      </div>
+      <div class="student-info-item">
+        <span>姓名:</span>
+        <span>{{ modalForm.examStudentName }}</span>
+      </div>
+      <div class="student-info-item">
+        <span>科目(代码):</span>
+        <span>{{ modalForm.courseNameCode }}</span>
+      </div>
+    </div>
+    <el-form
+      ref="modalFormComp"
+      :model="modalForm"
+      :rules="rules"
+      label-width="100px"
+    >
+      <el-form-item prop="type" :label="reasonTitle">
+        <el-select v-model="modalForm.type" placeholder="请选择" clearable>
+          <el-option
+            v-for="item in typeList"
+            :key="item.key"
+            :value="item.key"
+            :label="item.val"
+          ></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item :label="descTitle">
+        <el-input
+          type="textarea"
+          v-model.trim="modalForm.description"
+          placeholder="请输入"
+          :rows="5"
+          :maxlength="200"
+          show-word-limit
+        ></el-input>
+      </el-form-item>
+    </el-form>
+
+    <div slot="footer" class="dialog-footer">
+      <el-button @click="cancel" plain>取消</el-button>
+      <el-button type="primary" @click="confirm" :disabled="isSubmit"
+        >确认</el-button
+      >
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { updateBreachInfo } from "@/api/invigilation";
+import { BREACH_REASON_TYPE, BREACH_REPEAL_TYPE } from "@/constant/constants";
+
+const initModalForm = {
+  examStudentName: "",
+  identity: "",
+  courseNameCode: "",
+  description: "",
+  examRecordId: 0,
+  status: 0,
+  type: "",
+};
+
+export default {
+  name: "student-breach-dialog",
+  props: {
+    instance: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      dialogVisible: false,
+      isSubmit: false,
+      reasonTitle: "",
+      descTitle: "",
+      typeList: [],
+      modalForm: {},
+      rules: {
+        type: [
+          {
+            required: true,
+            message: "请选择违规类型",
+            trigger: "change",
+          },
+        ],
+      },
+    };
+  },
+  methods: {
+    opened() {
+      this.modalForm = Object.assign({}, initModalForm, this.instance);
+      this.reasonTitle = this.modalForm.status ? "违规类型:" : "撤销原因:";
+      this.descTitle = this.modalForm.status ? "违规描述:" : "原因详述:";
+      this.rules.type[0].message = this.modalForm.status
+        ? "请选择违规类型"
+        : "请选择撤销原因";
+      const options = this.modalForm.status
+        ? BREACH_REASON_TYPE
+        : BREACH_REPEAL_TYPE;
+      let typeList = [];
+      Object.entries(options).map(([key, val]) => {
+        typeList.push({
+          key,
+          val,
+        });
+      });
+      this.typeList = typeList;
+      this.$nextTick(() => {
+        this.$refs.modalFormComp.clearValidate();
+      });
+    },
+    cancel() {
+      this.dialogVisible = false;
+    },
+    open() {
+      this.dialogVisible = true;
+    },
+    async confirm() {
+      const valid = await this.$refs.modalFormComp.validate().catch(() => {});
+      if (!valid) return;
+
+      if (this.isSubmit) return;
+      this.isSubmit = true;
+      const result = await updateBreachInfo(this.modalForm).catch(() => {});
+
+      this.isSubmit = false;
+      if (!result) return;
+
+      this.$emit("modified");
+      this.cancel();
+    },
+  },
+};
+</script>

+ 297 - 36
src/features/invigilation/RealtimeMonitoring/WainingDetail.vue

@@ -25,9 +25,16 @@
         <div class="student-head">
           <div class="student-head-left">
             <p><i class="icon icon-user-act"></i></p>
-            <p><span>姓名:</span><span>张龙龙</span></p>
-            <p><span>证件号:</span><span>000000000000000008</span></p>
-            <p><span>科目(代码):</span><span>大学语文(1001)</span></p>
+            <p>
+              <span>姓名:</span><span>{{ detailInfo.examStudentName }}</span>
+            </p>
+            <p>
+              <span>证件号:</span><span>{{ detailInfo.identity }}</span>
+            </p>
+            <p>
+              <span>科目(代码):</span
+              ><span>{{ detailInfo.courseNameCode }}</span>
+            </p>
           </div>
           <div class="student-head-right">
             <el-button
@@ -44,7 +51,7 @@
         </div>
         <div class="student-views">
           <div class="student-avatar">
-            <img :src="curStudent.stdAvatar" alt="" />
+            <img :src="detailInfo.stdAvatar" alt="" />
           </div>
           <div class="student-video">
             <div class="student-video-item">
@@ -71,10 +78,10 @@
         </div>
         <div class="student-exception">
           <ul>
-            <li v-for="(item, index) in exceptionList" :key="index">
+            <li v-for="(item, index) in detailInfo.exceptionInfos" :key="index">
               <i>{{ index + 1 }}</i>
-              <h4>{{ item.title }}</h4>
-              <p>{{ item.desc }}</p>
+              <h4>{{ item.remark }}</h4>
+              <p>{{ item.info }}</p>
             </li>
           </ul>
         </div>
@@ -101,28 +108,107 @@
         <div class="warning-body-head-info summary-line">
           <p class="summary-line-item">
             <i class="line-point line-point-danger"></i
-            ><span class="line-name">系统预警</span><span>1次</span>
+            ><span class="line-name">系统预警</span
+            ><span>{{ detailInfo.warningCount }}次</span>
           </p>
           <p class="summary-line-item">
             <i class="line-point line-point-danger"></i
-            ><span class="line-name">陌生人脸</span><span>1次</span>
+            ><span class="line-name">陌生人脸</span
+            ><span>{{ detailInfo.multipleFaceCount }}次</span>
           </p>
           <p class="summary-line-item">
             <i class="line-point line-point-danger"></i
-            ><span class="line-name">异常处理</span><span>1次</span>
+            ><span class="line-name">异常处理</span
+            ><span>{{ detailInfo.exceptionCount }}次</span>
           </p>
           <p class="summary-line-item">
-            <span></span><span>违纪状态:正常</span>
+            <span></span><span>违纪状态:{{ detailInfo.status }}</span>
           </p>
-          <el-button type="danger" icon="icon icon-stop">违纪处理</el-button>
-          <el-button type="warning" icon="icon icon-forbide"
+          <el-button type="danger" icon="icon icon-stop" @click="toBreach"
+            >违纪处理</el-button
+          >
+          <el-button type="warning" icon="icon icon-forbide" @click="toFinish"
             >强制收卷</el-button
           >
         </div>
       </div>
-      <div class="warning-body-main"></div>
+      <div class="warning-body-main">
+        <div class="warning-history">
+          <div class="warning-history-info">
+            <h3>进入考试</h3>
+            <p>时间段:08:00:01</p>
+          </div>
+          <div class="warning-history-type type-common">
+            <i class="icon icon-current-step"></i>
+          </div>
+          <div class="warning-history-media">
+            <ul class="media-list">
+              <li></li>
+            </ul>
+          </div>
+        </div>
+        <div class="warning-history">
+          <div class="warning-history-info">
+            <h3>违纪预警</h3>
+            <p>时间段:09:10:20 ~ 09:20:36</p>
+            <p>持续时长约:10分钟16秒</p>
+          </div>
+          <div class="warning-history-type type-exception">
+            <i class="icon icon-warning-act"></i>
+          </div>
+          <div class="warning-history-media">
+            <ul class="media-list">
+              <li></li>
+              <li></li>
+              <li></li>
+              <li></li>
+              <li></li>
+              <li></li>
+              <li></li>
+              <li></li>
+            </ul>
+          </div>
+        </div>
+        <div class="warning-history">
+          <div class="warning-history-info">
+            <h3>异常处理</h3>
+            <h5>类型:网络故障</h5>
+            <p>时间段:09:10:20 ~ 09:20:36</p>
+            <p>持续时长约:10分钟16秒</p>
+          </div>
+          <div class="warning-history-type type-exception">
+            <i class="icon icon-net-break"></i>
+          </div>
+          <div class="warning-history-media">
+            <ul class="media-list">
+              <li></li>
+            </ul>
+          </div>
+        </div>
+        <div class="warning-history">
+          <div class="warning-history-info">
+            <h3>异常处理</h3>
+            <h5>类型:网络故障</h5>
+            <p>时间段:09:10:20 ~ 09:20:36</p>
+            <p>持续时长约:10分钟16秒</p>
+          </div>
+          <div class="warning-history-type type-exception">
+            <i class="icon icon-net-break"></i>
+          </div>
+          <div class="warning-history-media">
+            <ul class="media-list">
+              <li></li>
+            </ul>
+          </div>
+        </div>
+      </div>
     </div>
 
+    <student-breach-dialog
+      :instance="curDetail"
+      ref="StudentBreachDialog"
+    ></student-breach-dialog>
+
     <!-- 通话弹出层 -->
     <el-dialog
       custom-class="communication-dialog"
@@ -162,31 +248,78 @@
 
 <script>
 import { createClient, createStream } from "@/plugins/trtc";
+import { invigilateFinish } from "@/api/invigilation";
 import FlvMedia from "../common/FlvMedia";
 import { invigilateDetail, warningStudentDetail } from "@/api/invigilation";
+import StudentBreachDialog from "./StudentBreachDialog";
+
+const test = {
+  breachStatus: 0,
+  courseNameCode: "大学语文(1001)",
+  examActivityId: 0,
+  examId: 0,
+  examRecordId: 11111,
+  examStudentId: 0,
+  examStudentName: "张一三",
+  exceptionCount: 0,
+  exceptionInfos: [
+    {
+      createTime: "",
+      info: "系统发现该考生身份识别多次不通过,请监考老师关注!",
+      remark: "身份验证不通过",
+      type: "",
+    },
+    {
+      createTime: "",
+      info:
+        "系统发现该考生至少有多次以上持续1分钟以上的违规动作,且违规动作的持续时间已超出合理范围,请监考老师关注!",
+      remark: "疑似:有违规动作",
+      type: "",
+    },
+    {
+      createTime: "",
+      info: "系统检测到考生可能使用了虚拟摄像头,请监考老师关注!",
+      remark: "疑似:启用虚拟摄像头",
+      type: "",
+    },
+  ],
+  stdAvatar: "/img/avatars/2.jpg",
+  identity: "000000000000000008",
+  multipleFaceCount: 0,
+  roomCode: "111",
+  roomName: "第一考场",
+  status: "正常",
+  statusCode: "0124",
+  studentLogs: [
+    {
+      createTime: "",
+      info: "",
+      remark: "",
+      type: "",
+    },
+  ],
+  warningCount: 0,
+  warningInfos: [
+    {
+      approveStatus: 0,
+      createTime: "",
+      info: "",
+      level: "",
+      remark: "",
+      type: "",
+    },
+  ],
+};
 
 export default {
   name: "warning-detail",
-  components: { FlvMedia },
+  components: { FlvMedia, StudentBreachDialog },
   data() {
     return {
-      recordId: "32145907927482368",
-      // recordId: this.$route.params.recordId,
-      exceptionList: [
-        {
-          title: "身份验证不通过",
-          desc: "系统发现该考生身份识别多次不通过,请监考老师关注!",
-        },
-        {
-          title: "疑似:有违规动作",
-          desc:
-            "系统发现该考生至少有多次以上持续1分钟以上的违规动作,且违规动作的持续时间已超出合理范围,请监考老师关注!",
-        },
-        {
-          title: "疑似:启用虚拟摄像头",
-          desc: "系统检测到考生可能使用了虚拟摄像头,请监考老师关注!",
-        },
-      ],
+      // recordId: "32145907927482368",
+      recordId: this.$route.params.recordId,
+      detailInfo: test,
+      curDetail: {},
       curStudent: {
         stdAvatar: "/img/avatars/2.jpg",
         stdCardNo: "000000000000000008",
@@ -221,8 +354,8 @@ export default {
     };
   },
   mounted() {
-    this.initClinet();
-    this.initData();
+    // this.initClinet();
+    // this.initData();
   },
   methods: {
     initClinet() {
@@ -249,8 +382,43 @@ export default {
     },
     async getInvigilateDetail() {
       const res = await invigilateDetail(this.recordId);
+      this.detailInfo = res;
       console.log(res);
     },
+    toBreach() {
+      this.curDetail = {
+        examStudentName: this.detailInfo.examStudentName,
+        identity: this.detailInfo.identity,
+        courseNameCode: this.detailInfo.courseNameCode,
+        description: "",
+        examRecordId: this.detailInfo.examRecordId,
+        status: 1,
+        type: "",
+      };
+      this.$refs.StudentBreachDialog.open();
+    },
+    async toFinish() {
+      const result = await this.$confirm(
+        "试卷若被强制回收,考试再无法重置继续完成考试,请您慎重选择!您确定要强制回收改考试试卷吗?",
+        "强制收卷确认提醒",
+        {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          iconClass: "el-icon-warning",
+          customClass: "el-message-box__error",
+        }
+      ).catch(() => {});
+
+      if (!result) return;
+
+      await invigilateFinish({
+        examRecordId: this.detailInfo.examRecordId,
+        type: true,
+      });
+      this.$message.success("强制收卷成功!");
+      this.goBack();
+    },
+    // video relative
     initSubscribeVideo() {
       if (this.firstViewVideo.id) this.firstViewVideoReady = true;
       if (this.secondViewVideo.id) this.secondViewVideoReady = true;
@@ -569,9 +737,10 @@ export default {
 }
 .warning-detail-body {
   margin-top: 30px;
+  border-radius: 6px;
+  background: #fff;
 }
 .warning-body-head {
-  background: #fff;
   border-bottom: 1px solid #f0f4f9;
   padding: 20px;
   &-action {
@@ -594,15 +763,107 @@ export default {
   }
   &-info {
     float: right;
+    padding: 0;
     > p {
       height: 32px;
       line-height: 32px;
       margin-right: 20px;
     }
+    .el-button {
+      vertical-align: middle;
+    }
   }
 }
 .warning-body-main {
   min-height: 400px;
-  background: #fff;
+  padding: 15px;
+}
+.warning-history {
+  display: flex;
+  min-height: 130px;
+  align-items: center;
+  &-info {
+    padding: 15px 30px 15px 0;
+    text-align: right;
+    width: 210px;
+    flex-shrink: 0;
+    > h3 {
+      font-size: 16px;
+      font-weight: 600;
+      color: #202b4b;
+      line-height: 22px;
+      margin-bottom: 4px;
+    }
+
+    > h5 {
+      font-size: 13px;
+      font-weight: 600;
+      color: #626a82;
+      line-height: 18px;
+      margin-bottom: 4px;
+    }
+
+    > p {
+      font-size: 12px;
+      font-weight: 400;
+      color: #626a82;
+      line-height: 17px;
+      margin: 0;
+    }
+  }
+
+  &-type {
+    position: relative;
+    width: 36px;
+    height: 36px;
+    padding-top: 5px;
+    text-align: center;
+    border-radius: 50%;
+    border: 2px solid #1cd1a1;
+    background-color: #fff;
+    z-index: 9;
+    flex-shrink: 0;
+    &.type-exception {
+      border-color: #fe5863;
+    }
+    > i {
+      width: 18px;
+      height: 17px;
+    }
+    i.icon-current-step {
+      width: 12px;
+    }
+  }
+
+  &-media {
+    padding: 10px 0 10px 20px;
+    position: relative;
+    z-index: 8;
+
+    &::before {
+      content: "";
+      display: block;
+      position: absolute;
+      height: 100%;
+      border-left: 1px solid #f0f4f9;
+      left: -18px;
+      top: 0;
+    }
+  }
+  .media-list {
+    padding: 0;
+    margin: 0;
+    list-style: none;
+
+    li {
+      display: inline-block;
+      vertical-align: top;
+      width: 160px;
+      height: 100px;
+      margin: 5px 10px;
+      border-radius: 6px;
+      background-color: #626a82;
+    }
+  }
 }
 </style>

+ 124 - 0
src/features/invigilation/RealtimeMonitoring/handleRollupDialog.vue

@@ -0,0 +1,124 @@
+<template>
+  <el-dialog
+    class="handle-rollup-dialog"
+    :visible.sync="dialogVisible"
+    width="640px"
+    title="手动收卷"
+    :close-on-press-escape="false"
+    :close-on-click-modal="false"
+    append-to-body
+    @open="opened"
+    @close="closed"
+  >
+    <div class="tips-info tips-error">
+      <i class="el-icon-warning"></i>
+      <p>
+        系统检测到如下考生正在进行异常处理尚未交卷,请监考老师勾选需要手动收卷的考生!
+        注意:被收卷的考生无法继续完成考试,请谨慎操作!
+      </p>
+    </div>
+    <el-form
+      ref="modalFormComp"
+      :model="modalForm"
+      :rules="rules"
+      label-width="120px"
+    >
+      <el-form-item prop="ids" label="证件号/姓名:">
+        <multiple-select-student
+          data-key="examRecordId"
+          :data-list="dataList"
+          :seleted-ids="modalForm.ids"
+          @selected="idsChange"
+          ref="MultipleSelectStudent"
+          v-if="dataReady"
+        ></multiple-select-student>
+      </el-form-item>
+    </el-form>
+
+    <div slot="footer" class="dialog-footer">
+      <el-button @click="cancel" plain>取消</el-button>
+      <el-button type="primary" @click="confirm" :disabled="isSubmit"
+        >确认</el-button
+      >
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import MultipleSelectStudent from "../common/MultipleSelectStudent";
+import { invigilateFinish } from "@/api/invigilation";
+
+export default {
+  name: "handle-rollup-dialog",
+  components: { MultipleSelectStudent },
+  props: {
+    dataList: {
+      type: Array,
+      default() {
+        return [];
+      },
+    },
+  },
+  data() {
+    const idsValidator = (rule, value, callback) => {
+      if (!value || !value.length) {
+        callback(new Error("请选择学生"));
+      } else {
+        callback();
+      }
+    };
+    return {
+      dialogVisible: false,
+      dataReady: false,
+      isSubmit: false,
+      modalForm: {
+        ids: [],
+      },
+      rules: {
+        ids: [
+          {
+            required: true,
+            validator: idsValidator,
+            trigger: "change",
+          },
+        ],
+      },
+    };
+  },
+  methods: {
+    opened() {
+      this.modalForm.ids = this.dataList.map((item) => item.examRecordId);
+      this.dataReady = true;
+    },
+    closed() {
+      this.dataReady = false;
+    },
+    cancel() {
+      this.dialogVisible = false;
+    },
+    open() {
+      this.dialogVisible = true;
+    },
+    idsChange(ids) {
+      this.modalForm.ids = ids;
+    },
+    async confirm() {
+      const valid = await this.$refs.modalFormComp.validate().catch(() => {});
+      if (!valid) return;
+
+      if (this.isSubmit) return;
+      this.isSubmit = true;
+      const result = await invigilateFinish({
+        examRecordId: this.modalForm.ids,
+        type: true,
+      }).catch(() => {});
+
+      this.isSubmit = false;
+      if (!result) return;
+
+      this.$emit("modified");
+      this.cancel();
+    },
+  },
+};
+</script>

+ 33 - 18
src/features/invigilation/ReexamApply/ApplyReexamDialog.vue

@@ -8,38 +8,34 @@
     :close-on-click-modal="false"
     append-to-body
     @open="visibleChange"
+    @close="closed"
   >
     <el-form
       ref="modalFormComp"
       :model="modalForm"
       :rules="rules"
-      label-width="100px"
+      label-width="120px"
     >
       <el-form-item prop="model" label="重考方式:">
         <el-select v-model="modalForm.model" placeholder="请选择" clearable>
           <el-option
             v-for="(val, key) in REEXAM_TYPE"
             :key="key"
-            :value="key"
+            :value="key * 1"
             :label="val"
           ></el-option>
         </el-select>
       </el-form-item>
       <el-form-item prop="examRecordId" label="证件号/姓名:">
-        <el-select
-          v-model="modalForm.examRecordId"
-          placeholder="请选择"
-          :multiple="students.length > 1"
-          :readonly="students.length === 1"
-          clearable
-        >
-          <el-option
-            v-for="item in students"
-            :key="item.examRecordId"
-            :value="item.examRecordId"
-            :label="item.label"
-          ></el-option>
-        </el-select>
+        <multiple-select-student
+          data-key="examRecordId"
+          :data-list="dataList"
+          :seleted-ids="modalForm.examRecordId"
+          :show-add="true"
+          @selected="idsChange"
+          ref="MultipleSelectStudent"
+          v-if="dataReady"
+        ></multiple-select-student>
       </el-form-item>
       <el-form-item prop="reason" label="重考原因:">
         <el-select v-model="modalForm.reason" placeholder="请选择" clearable>
@@ -57,6 +53,7 @@
           v-model.trim="modalForm.remark"
           placeholder="请输入"
           :maxlength="200"
+          :rows="5"
           show-word-limit
           clearable
         ></el-input>
@@ -65,7 +62,9 @@
 
     <div slot="footer" class="dialog-footer">
       <el-button @click="cancel" plain>取消</el-button>
-      <el-button type="primary" @click="confirm">确认</el-button>
+      <el-button type="primary" @click="confirm" :disabled="isSubmit"
+        >确认</el-button
+      >
     </div>
   </el-dialog>
 </template>
@@ -73,16 +72,18 @@
 <script>
 import { applyReexam } from "@/api/invigilation";
 import { REEXAM_TYPE, REEXAM_REASON } from "@/constant/constants";
+import MultipleSelectStudent from "../common/MultipleSelectStudent";
 
 const initModalForm = {
   examRecordId: [],
   model: 0,
-  reason: "",
+  reason: null,
   remark: "",
 };
 
 export default {
   name: "apply-reexam-dialog",
+  components: { MultipleSelectStudent },
   props: {
     students: {
       type: Array,
@@ -90,6 +91,12 @@ export default {
         return [];
       },
     },
+    dataList: {
+      type: Array,
+      default() {
+        return [];
+      },
+    },
   },
   data() {
     const recordIdValidator = (rule, value, callback) => {
@@ -101,6 +108,7 @@ export default {
     };
     return {
       dialogVisible: false,
+      dataReady: false,
       isSubmit: false,
       modalForm: { ...initModalForm },
       REEXAM_TYPE,
@@ -136,6 +144,10 @@ export default {
       this.modalForm.examRecordId = this.students.map(
         (item) => item.examRecordId
       );
+      this.dataReady = true;
+    },
+    closed() {
+      this.dataReady = false;
     },
     cancel() {
       this.dialogVisible = false;
@@ -143,6 +155,9 @@ export default {
     open() {
       this.dialogVisible = true;
     },
+    idsChange(ids) {
+      this.modalForm.examRecordId = ids;
+    },
     async confirm() {
       const valid = await this.$refs.modalFormComp.validate().catch(() => {});
       if (!valid) return;

+ 45 - 5
src/features/invigilation/ReexamApply/ReexamApply.vue

@@ -74,7 +74,8 @@
       :data="dataList"
       @selection-change="handleSelectionChange"
     >
-      <el-table-column type="selection" width="55"> </el-table-column>
+      <el-table-column type="selection" width="55" align="center">
+      </el-table-column>
       <el-table-column prop="roomName" label="考场名称(代码)">
         <span slot-scope="scope"
           >{{ scope.row.roomName }}({{ scope.row.roomCode }})</span
@@ -110,6 +111,7 @@
 
     <apply-reexam-dialog
       :students="selectStudents"
+      :data-list="dataList"
       @modified="applyFinish"
       ref="ApplyReexamDialog"
     ></apply-reexam-dialog>
@@ -137,7 +139,47 @@ export default {
       batchs: [],
       exams: [],
       subjects: [],
-      dataList: [],
+      dataList: [
+        {
+          courseCode: "F001",
+          courseName: "数学",
+          examActivityId: 11,
+          examId: 111,
+          examRecordId: 1111,
+          examStudentId: 11111,
+          identity: "000000000015",
+          name: "张示框",
+          reexamId: 1,
+          roomCode: "111",
+          roomName: "第一考场",
+        },
+        {
+          courseCode: "F001",
+          courseName: "数学",
+          examActivityId: 22,
+          examId: 222,
+          examRecordId: 2222,
+          examStudentId: 22222,
+          identity: "000000000016",
+          name: "张设置",
+          reexamId: 2,
+          roomCode: "111",
+          roomName: "第一考场",
+        },
+        {
+          courseCode: "F001",
+          courseName: "数学",
+          examActivityId: 33,
+          examId: 333,
+          examRecordId: 3333,
+          examStudentId: 33333,
+          identity: "000000000017",
+          name: "张看得",
+          reexamId: 3,
+          roomCode: "111",
+          roomName: "第一考场",
+        },
+      ],
       selectStudents: [],
       multipleSelection: [],
     };
@@ -152,9 +194,7 @@ export default {
 
       const res = await reexamApplyList(datas);
 
-      this.dataList = res.data.data.records.map((item) => {
-        item.label = `${item.identity} ${item.courseName}(${item.courseCode}) ${item.name}`;
-      });
+      this.dataList = res.data.data.records;
       this.total = res.data.data.records.total;
     },
     toPage(page) {

+ 1 - 3
src/features/invigilation/ReexamChecked/ReexamChecked.vue

@@ -189,9 +189,7 @@ export default {
 
       const res = await reexamCheckedList(datas);
 
-      this.dataList = res.data.data.records.map((item) => {
-        item.label = `${item.identity} ${item.courseName}(${item.courseCode}) ${item.name}`;
-      });
+      this.dataList = res.data.data.records;
       this.total = res.data.data.records.total;
     },
     toPage(page) {

+ 6 - 3
src/features/invigilation/ReexamPending/CheckReexamDialog.vue

@@ -13,16 +13,17 @@
       ref="modalFormComp"
       :model="modalForm"
       :rules="rules"
-      label-width="100px"
+      label-width="120px"
     >
       <el-form-item prop="reexamId" label="证件号/姓名:">
-        <el-input v-model="modalForm.label" :readonly="!isEdit"></el-input>
+        <el-input v-model="modalForm.name" readonly></el-input>
       </el-form-item>
       <el-form-item prop="reason" label="重考原因:">
         <el-select
           v-model="modalForm.reason"
           placeholder="请选择"
-          :readonly="!isEdit"
+          :disabled="!isEdit"
+          clearable
         >
           <el-option
             v-for="(val, key) in REEXAM_REASON"
@@ -38,6 +39,7 @@
           v-model.trim="modalForm.remark"
           placeholder="请输入"
           :maxlength="200"
+          :rows="5"
           show-word-limit
           readonly
         ></el-input>
@@ -51,6 +53,7 @@
           v-model.trim="modalForm.auditingSuggest"
           placeholder="请输入"
           :maxlength="200"
+          :rows="5"
           show-word-limit
           :readonly="!isEdit"
           :clearable="isEdit"

+ 18 - 4
src/features/invigilation/ReexamPending/ReexamPending.vue

@@ -179,7 +179,23 @@ export default {
       exams: [],
       subjects: [],
       applyTime: "",
-      dataList: [],
+      dataList: [
+        {
+          courseCode: "F001",
+          courseName: "数学",
+          examActivityId: 11,
+          examId: 111,
+          examRecordId: 1111,
+          examStudentId: 11111,
+          identity: "000000000015",
+          name: "张示框",
+          reexamId: 1,
+          roomCode: "111",
+          roomName: "第一考场",
+          remark: "这是remark",
+          status: 1,
+        },
+      ],
       curReexam: {},
       multipleSelection: [],
     };
@@ -194,9 +210,7 @@ export default {
 
       const res = await reexamPendingList(datas);
 
-      this.dataList = res.data.data.records.map((item) => {
-        item.label = `${item.identity} ${item.courseName}(${item.courseCode}) ${item.name}`;
-      });
+      this.dataList = res.data.data.records;
       this.total = res.data.data.records.total;
     },
     toPage(page) {

+ 1 - 1
src/features/invigilation/WainingManage/WainingManage.vue

@@ -137,7 +137,7 @@
       :data="dataList"
       @selection-change="handleSelectionChange"
     >
-      <el-table-column type="selection" width="42" />
+      <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="roomName" label="考场"> </el-table-column>

+ 205 - 0
src/features/invigilation/common/MultipleSelectStudent.vue

@@ -0,0 +1,205 @@
+<template>
+  <div class="multiple-select-student">
+    <div class="selected-list">
+      <div
+        class="selected-item"
+        v-for="(item, sindex) in seletedList"
+        :key="item.key"
+      >
+        <i
+          class="selected-remove el-icon-close"
+          @click="removeSelectedStudent(sindex)"
+        ></i>
+        <p class="selected-label">{{ item.label }}</p>
+      </div>
+    </div>
+    <div class="selected-add" v-if="showAdd" @click="toSelect">
+      <i class="el-icon-plus"></i>
+      <span>选择考生</span>
+    </div>
+
+    <el-dialog
+      class="student-list-dialog"
+      :visible.sync="modalIsShow"
+      title="选择考生"
+      width="640px"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+      append-to-body
+    >
+      <div class="student-list">
+        <div
+          :class="['student-item', { 'student-item-selected': item.selected }]"
+          v-for="item in studentList"
+          :key="item.key"
+          @click="switchSelectStudent(item)"
+        >
+          {{ item.label }}
+        </div>
+      </div>
+      <div slot="footer">
+        <el-button type="primary" @click="confirm">确认</el-button>
+        <el-button @click="closeDialog">取消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "multiple-select-student",
+  props: {
+    dataKey: {
+      type: String,
+      required: true,
+    },
+    dataList: {
+      type: Array,
+      default() {
+        return [];
+      },
+    },
+    seletedIds: {
+      type: Array,
+      default() {
+        return [];
+      },
+    },
+    showAdd: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  mounted() {
+    this.initData();
+  },
+  data() {
+    return {
+      seletedList: [],
+      studentList: [],
+      newSeletedIds: [],
+      modalIsShow: false,
+    };
+  },
+  methods: {
+    initData() {
+      this.studentList = this.dataList.map((item) => {
+        return {
+          ...item,
+          key: item[this.dataKey],
+          label: `${item.identity}  ${item.courseName}(${item.courseCode})  ${item.name}`,
+          selected: this.seletedIds.includes(item[this.dataKey]),
+        };
+      });
+      this.updateSelectedStudent();
+    },
+    updateSelectedStudent() {
+      this.seletedList = this.studentList
+        .filter((item) => item.selected)
+        .map((item) => {
+          return { ...item };
+        });
+      this.newSeletedIds = this.seletedList.map((item) => item[this.dataKey]);
+    },
+    initSelectedStudent() {
+      this.studentList.map((item) => {
+        item.selected = this.newSeletedIds.includes(item[this.dataKey]);
+      });
+    },
+    removeSelectedStudent(sindex) {
+      if (this.seletedList.length <= 1 && !this.showAdd) {
+        this.$message.error("当前只剩下一条信息,不可再删!");
+        return;
+      }
+      this.seletedList.splice(sindex, 1);
+      this.newSeletedIds = this.seletedList.map((item) => item[this.dataKey]);
+      this.$emit("selected", this.newSeletedIds);
+    },
+    switchSelectStudent(student) {
+      student.selected = !student.selected;
+    },
+    toSelect() {
+      this.initSelectedStudent();
+      this.modalIsShow = true;
+    },
+    closeDialog() {
+      this.modalIsShow = false;
+    },
+    confirm() {
+      this.modalIsShow = false;
+      this.updateSelectedStudent();
+      this.$emit("selected", this.newSeletedIds);
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.multiple-select-student {
+  .selected-list {
+    background: #f0f4f9;
+    border-radius: 6px;
+    border: 1px solid #e8edf3;
+    padding: 10px;
+    min-height: 180px;
+  }
+  .selected-item {
+    position: relative;
+    display: inline-block;
+    background: #fff;
+    padding: 6px 15px 6px 30px;
+    border-radius: 6px;
+    color: #202b4b;
+    margin-bottom: 4px;
+    line-height: 20px;
+
+    > .selected-remove {
+      position: absolute;
+      display: block;
+      font-size: 14px;
+      color: rgba(189, 200, 218, 1);
+      left: 8px;
+      top: 9px;
+      cursor: pointer;
+
+      &:hover {
+        color: #fe5863;
+      }
+    }
+    > .selected-label {
+      margin: 0;
+      font-weight: 400;
+      white-space: pre-wrap;
+    }
+  }
+  .selected-add {
+    font-weight: 600;
+    color: #1886fe;
+    cursor: pointer;
+    > i {
+      margin-right: 5px;
+    }
+  }
+}
+.student-list-dialog {
+  .student-item {
+    border: 1px dashed #e0e0e0;
+    padding: 10px;
+    border-radius: 6px;
+    margin-bottom: 10px;
+    color: #999;
+    white-space: pre-wrap;
+    cursor: pointer;
+
+    &:hover {
+      border-color: #1886fe;
+    }
+
+    &-selected {
+      border-style: solid;
+      border-color: #1886fe;
+      color: #1886fe;
+    }
+  }
+}
+</style>

+ 113 - 0
src/features/invigilation/common/SummaryLine.vue

@@ -0,0 +1,113 @@
+<template>
+  <div class="summary-line">
+    <p class="summary-line-item" v-for="item in paramList" :key="item.param">
+      <i :class="['icon', `icon-${item.icon}`]" v-if="item.icon"></i>
+      <i
+        :class="[
+          'line-point',
+          item.pointType && `line-point-${item.pointType}`,
+        ]"
+        v-else
+      ></i>
+      <el-popover
+        placement="bottom-start"
+        popper-class="summary-line-popover"
+        width="300"
+        trigger="hover"
+        :content="item.content"
+        v-if="item.desc"
+      >
+        <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>
+    </p>
+  </div>
+</template>
+
+<script>
+// import { examPropCount } from "@/api/invigilation";
+
+const paramInfo = {
+  all: {
+    name: "全部应考",
+    param: "allCount",
+    desc: "参加考试的全部考生。",
+    icon: "users",
+  },
+  login: {
+    name: "已登录",
+    param: "loginCount",
+    desc: "已成功登录考生端的考生。",
+    pointType: "info",
+  },
+  prepare: {
+    name: "已待考",
+    param: "prepareCount",
+    desc: "已进入待考界面等待开考的考生。",
+    pointType: "success",
+  },
+  exam: {
+    name: "考试中",
+    param: "notComplete",
+    desc: "正在答题的考生。",
+    pointType: "primary",
+  },
+  complete: {
+    name: "已交卷",
+    param: "alreadyComplete",
+    desc: "",
+    pointType: "danger",
+  },
+  trouble: {
+    name: "通讯故障",
+    param: "trouble",
+    desc:
+      "考生端出现断网、断电、软硬件故障等异常导致考生端与监考端无法正常连接的考生。",
+    pointType: "danger",
+  },
+};
+const types = {
+  trouble: ["all", "login", "prepare", "exam", "trouble"],
+  complete: ["all", "login", "prepare", "exam", "complete"],
+};
+
+export default {
+  name: "summary-line",
+  props: {
+    examId: {
+      type: String,
+      required: true,
+    },
+    dataType: {
+      type: String,
+      required: true,
+      validator: (val) => {
+        return ["trouble", "complete"].includes(val);
+      },
+    },
+  },
+  created() {
+    this.initData();
+  },
+  data() {
+    return {
+      paramList: [],
+      examPropData: {},
+    };
+  },
+  methods: {
+    async initData() {
+      // const examPropData = await examPropCount(this.examId).catch(() => {});
+      // this.examPropData = examPropData || {};
+      this.paramList = types[this.dataType].map((item) => {
+        let info = paramInfo[item];
+        return {
+          ...info,
+          content: `${info.name}:${info.desc}`,
+        };
+      });
+    },
+  },
+};
+</script>

+ 41 - 0
src/features/invigilation/common/TextClock.vue

@@ -0,0 +1,41 @@
+<template>
+  <p class="text-clock">现在是{{ text }}</p>
+</template>
+
+<script>
+import { formatDate } from "@/utils/utils";
+
+export default {
+  name: "text-clock",
+  data() {
+    return {
+      text: "",
+      setT: null,
+      week: ["日", "一", "二", "三", "四", "五", "六"],
+    };
+  },
+  mounted() {
+    this.parseDate();
+  },
+  methods: {
+    parseDate() {
+      const now = new Date();
+      const timeStr = formatDate("YYYY年M月D日_mm:ss", now).split("_");
+      const weekDay = `星期${this.week[now.getDay()]}`;
+
+      const hourNum = now.getHours();
+      const val = hourNum > 12 ? hourNum - 12 : hourNum;
+      const hour = ("00" + val).substr(("" + val).length);
+      const apm = hourNum < 12 ? "上午" : "下午";
+      this.text = `${timeStr[0]} ${weekDay} ${apm} ${hour}:${timeStr[1]}`;
+
+      this.setT = setTimeout(() => {
+        this.parseDate();
+      }, 1000);
+    },
+  },
+  beforeDestroy() {
+    if (this.setT) clearTimeout(this.setT);
+  },
+};
+</script>

+ 1 - 0
src/router/index.js

@@ -164,6 +164,7 @@ const routes = [
   },
   {
     path: "/*",
+    name: "404",
     component: () =>
       import(/* webpackChunkName: "default" */ "../views/404.vue"),
   },

+ 147 - 46
src/styles/base.scss

@@ -6,6 +6,37 @@ input:-moz-placeholder {
   font-weight: 400;
   color: #8c94ac;
 }
+/* browse style */
+::-webkit-scrollbar {
+  width: 8px;
+  height: 8px;
+  background: transparent;
+}
+::-webkit-scrollbar-button {
+  display: none;
+}
+::-webkit-scrollbar-track {
+  background: transparent;
+}
+::-webkit-scrollbar-thumb {
+  border-radius: 8px;
+  background: #999;
+}
+::-webkit-scrollbar-corner {
+  background: transparent;
+}
+::-webkit-scrollbar-resizer {
+  background: transparent;
+}
+
+body {
+  font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
+    "Microsoft YaHei", Arial, sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  font-size: 14px;
+}
+
 // part-box
 .part-box {
   padding: 20px;
@@ -111,6 +142,28 @@ input:-moz-placeholder {
   height: 100% !important;
   width: 100% !important;
 }
+// tips-info
+.tips-info {
+  position: relative;
+  padding-left: 30px;
+
+  > i {
+    position: absolute;
+    display: block;
+    left: 0;
+    top: 3px;
+    font-size: 20px;
+  }
+  > p {
+    margin: 0;
+    line-height: 20px;
+    font-size: 14px;
+    font-weight: 400;
+  }
+}
+.tips-error {
+  color: rgba(254, 88, 99, 1);
+}
 
 // :fullscreen
 .app-fullscreen {
@@ -124,6 +177,69 @@ input:-moz-placeholder {
     padding: 0;
   }
 }
+// summary-line
+.summary-line {
+  font-size: 0;
+  padding: 6px 10px 6px 0;
+  &-item {
+    font-size: 14px;
+    display: inline-block;
+    vertical-align: middle;
+    margin: 0 30px 0 0;
+    line-height: 20px;
+    color: #202b4b;
+
+    > .icon {
+      margin-right: 8px;
+      margin-top: -2px;
+    }
+
+    span.line-name {
+      color: #626a82;
+      margin-right: 8px;
+    }
+    > span:last-child {
+      color: #202b4b;
+      font-weight: 600;
+    }
+
+    i.line-point {
+      display: inline-block;
+      vertical-align: middle;
+      width: 8px;
+      height: 8px;
+      border: 2px solid #202b4b;
+      margin-right: 6px;
+      margin-top: -3px;
+      border-radius: 50%;
+    }
+
+    i.line-point-primary {
+      border-color: #1886fe;
+    }
+    i.line-point-info {
+      border-color: #5fcafc;
+    }
+    i.line-point-success {
+      border-color: #1cd1a1;
+    }
+    i.line-point-danger {
+      border-color: #fe5863;
+    }
+  }
+}
+.el-popover.summary-line-popover {
+  background: #ff9f43;
+  box-shadow: 0px 0px 6px 0px rgba(255, 158, 59, 0.5);
+  border-radius: 6px;
+  padding: 6px 10px;
+  color: #fff;
+  font-size: 12px;
+  line-height: 17px;
+  .popper__arrow::after {
+    border-bottom-color: #ff9f43 !important;
+  }
+}
 
 // communication-dialog
 .communication-dialog {
@@ -229,55 +345,40 @@ input:-moz-placeholder {
   }
 }
 
-// summary-line
-.summary-line {
-  font-size: 0;
-  padding: 6px 10px 6px 0;
-  &-item {
-    font-size: 14px;
-    display: inline-block;
-    vertical-align: middle;
-    margin: 0 30px 0 0;
-    line-height: 20px;
-    color: #202b4b;
-
-    > .icon {
-      margin-right: 8px;
-      margin-top: -2px;
-    }
-
-    span.line-name {
-      color: #626a82;
-      margin-right: 8px;
-    }
-    > span:last-child {
-      color: #202b4b;
-      font-weight: 600;
-    }
+// handle-rollup-dialog
+.handle-rollup-dialog {
+  .tips-info {
+    margin-bottom: 30px;
+  }
+}
 
-    i.line-point {
+// student-breach-dialog
+.student-breach-dialog {
+  .student-info {
+    margin: 30px 0 20px;
+    &-item {
       display: inline-block;
-      vertical-align: middle;
-      width: 8px;
-      height: 8px;
-      border: 2px solid #202b4b;
-      margin-right: 6px;
-      margin-top: -3px;
-      border-radius: 50%;
-    }
+      vertical-align: top;
 
-    i.line-point-primary {
-      border-color: #1886fe;
-    }
-    i.line-point-info {
-      border-color: #5fcafc;
-    }
-    i.line-point-success {
-      border-color: #1cd1a1;
-    }
-    i.line-point-danger {
-      border-color: #fe5863;
+      &:not(:last-child) {
+        margin-right: 20px;
+        color: #626a82;
+      }
+      span:nth-of-type(2) {
+        color: #202b4b;
+        font-weight: 600;
+      }
     }
   }
 }
-// element-custom ------------->
+
+// exam-batch-dialog
+.exam-batch-dialog {
+  .el-radio {
+    color: #202b4b;
+    margin-bottom: 20px;
+    margin-right: 0;
+    line-height: 20px;
+    font-weight: 400;
+  }
+}

+ 68 - 0
src/styles/element-ui-custom.scss

@@ -20,6 +20,17 @@
   }
 }
 
+// datepicker
+.el-date-editor.el-input__inner {
+  border-radius: 6px;
+  border-color: #e8edf3;
+  background-color: #f0f4f9;
+
+  .el-range-input {
+    background-color: #f0f4f9;
+  }
+}
+
 // .el-button
 .el-button {
   border-radius: 6px;
@@ -71,6 +82,12 @@
   // .el-table__row.row-danger {
   //   color: $--color-danger;
   // }
+
+  .cell {
+    .el-checkbox {
+      margin-bottom: 0;
+    }
+  }
 }
 
 // el-pagination
@@ -123,3 +140,54 @@
     }
   }
 }
+
+// el-dialog
+.el-dialog {
+  background: #ffffff;
+  border-radius: 10px;
+  color: #202b4b;
+  .el-dialog__header {
+    padding: 16px 20px;
+    border-bottom: 1px solid rgba(240, 244, 249, 1);
+  }
+  .el-dialog__body {
+    padding: 30px;
+  }
+  .el-dialog__footer {
+    .el-button {
+      width: 83px;
+    }
+  }
+}
+
+// el-message-box
+.el-message-box {
+  color: #202b4b;
+  border-radius: 10px;
+}
+.el-message-box__error {
+  width: 540px;
+  .el-message-box__header {
+    padding: 16px 20px;
+    border-bottom: 1px solid rgba(240, 244, 249, 1);
+  }
+  .el-message-box__content {
+    padding: 30px;
+    min-height: 140px;
+  }
+  .el-message-box__btns {
+    .el-button {
+      width: 83px;
+    }
+  }
+  .el-message-box__message {
+    padding-left: 50px;
+  }
+  .el-icon-warning {
+    color: #fe5863;
+    font-size: 32px !important;
+    top: 0;
+    left: 0;
+    transform: none;
+  }
+}

+ 3 - 0
src/styles/icons.scss

@@ -98,4 +98,7 @@
   &-full-screen {
     background-image: url(../assets/icon-full-screen.png);
   }
+  &-current-step {
+    background-image: url(../assets/icon-current-step.png);
+  }
 }

+ 41 - 0
src/utils/utils.js

@@ -53,3 +53,44 @@ export function downloadFileURL(url, name) {
   link.click();
   document.body.removeChild(link);
 }
+
+export function objTypeOf(obj) {
+  const toString = Object.prototype.toString;
+  const map = {
+    "[object Boolean]": "boolean",
+    "[object Number]": "number",
+    "[object String]": "string",
+    "[object Function]": "function",
+    "[object Array]": "array",
+    "[object Date]": "date",
+    "[object RegExp]": "regExp",
+    "[object Undefined]": "undefined",
+    "[object Null]": "null",
+    "[object Object]": "object",
+  };
+  return map[toString.call(obj)];
+}
+
+export function formatDate(format = "YYYY/MM/DD HH:mm:ss", date = new Date()) {
+  if (objTypeOf(date) !== "date") return;
+  const options = {
+    "Y+": date.getFullYear(),
+    "M+": date.getMonth() + 1,
+    "D+": date.getDate(),
+    "H+": date.getHours(),
+    "m+": date.getMinutes(),
+    "s+": date.getSeconds(),
+  };
+  Object.entries(options).map(([key, val]) => {
+    if (new RegExp("(" + key + ")").test(format)) {
+      const zeros = key === "Y+" ? "0000" : "00";
+      const value = (zeros + val).substr(("" + val).length);
+      format = format.replace(RegExp.$1, value);
+    }
+  });
+  return format;
+}
+
+export function deepCopy(obj) {
+  return JSON.parse(JSON.stringify(obj));
+}

+ 76 - 13
src/views/Layout/Layout.vue

@@ -1,7 +1,11 @@
 <template>
   <div class="app-wrapper">
-    <nav-bar @on-nav-change="navChange" />
-    <side-bar class="sidebar-container" :menus="curMenus" />
+    <nav-bar :navs="navs" @on-nav-change="navChange" v-if="navs.length" />
+    <side-bar
+      class="sidebar-container"
+      :menus="curMenus"
+      v-if="curMenus.length"
+    />
     <div class="main-container">
       <app-main />
       <app-footer />
@@ -14,11 +18,9 @@ import AppMain from "./components/AppMain.vue";
 import AppFooter from "./components/AppFooter.vue";
 import SideBar from "./components/SideBar.vue";
 import NavBar from "./components/NavBar.vue";
-import {
-  systemMenuConfig,
-  businessMenuConfig,
-  invigilationMenuConfig,
-} from "./components/menu";
+import { sysMenu } from "@/api/system-user";
+import localMenu from "./components/menu";
+import { deepCopy } from "@/utils/utils";
 
 export default {
   name: "Layout",
@@ -31,16 +33,77 @@ export default {
   data() {
     return {
       curMenus: [],
-      navs: {
-        systemMenuConfig,
-        businessMenuConfig,
-        invigilationMenuConfig,
-      },
+      navs: [],
+      validRoutes: [],
     };
   },
+  created() {
+    this.getMenu();
+  },
   methods: {
     navChange(name) {
-      this.curMenus = this.navs[`${name}MenuConfig`];
+      const nav = this.navs.find((item) => item.name === name);
+      this.curMenus = nav.children || [];
+    },
+    async getMenu() {
+      const res = await sysMenu();
+      this.navs = this.menusToTree(res.data.data);
+
+      if (this.$route.name === "Home") {
+        this.$router.replace({
+          name: this.navs[0].children[0].children[0].name,
+        });
+      } else {
+        if (!this.validRoutes.includes(this.$route.name)) {
+          this.$router.replace({
+            name: "404",
+          });
+          return;
+        }
+      }
+    },
+    buildLocalNavs() {
+      const dmenu = deepCopy(localMenu);
+      let localNavs = dmenu.headerMenuConfig;
+      localNavs.map((item) => {
+        item.children = dmenu[`${item.name.toLowerCase()}MenuConfig`];
+      });
+      return localNavs;
+    },
+    menusToTree(menus) {
+      let localNavs = this.buildLocalNavs();
+      const validRoutes = menus.map((menu) => menu.url);
+      const anchorNav = (navs) => {
+        let navHasAnchor = false;
+        navs.forEach((item) => {
+          let isFixed = validRoutes.includes(item.name);
+          if (item.children && item.children.length) {
+            isFixed = anchorNav(item.children);
+          }
+          item.fixed = isFixed;
+          navHasAnchor = navHasAnchor || isFixed;
+        });
+        return navHasAnchor;
+      };
+      anchorNav(localNavs);
+
+      const clearNoFixed = (navs) => {
+        let list = [];
+        navs.forEach((nav) => {
+          if (nav["fixed"]) {
+            let navItem = { title: nav.title, name: nav.name };
+            if (nav.icon) navItem.icon = nav.icon;
+            if (nav["children"])
+              navItem.children = clearNoFixed(nav["children"]);
+            list.push(navItem);
+          }
+        });
+        return list;
+      };
+
+      this.validRoutes = validRoutes;
+      this.validRoutes.push("Home");
+      return clearNoFixed(localNavs);
     },
   },
 };

+ 23 - 25
src/views/Layout/components/NavBar.vue

@@ -53,15 +53,16 @@
 </template>
 
 <script>
-import {
-  systemMenuConfig,
-  businessMenuConfig,
-  invigilationMenuConfig,
-  headerMenuConfig,
-} from "./menu";
-
 export default {
   name: "NavBar",
+  props: {
+    navs: {
+      type: Array,
+      default() {
+        return [];
+      },
+    },
+  },
   computed: {
     username() {
       return this.$store.state.user.name;
@@ -75,33 +76,30 @@ export default {
   },
   data() {
     return {
-      navs: headerMenuConfig,
       curNav: "",
-      modaleNavs: {
-        systemMenuConfig,
-        business: businessMenuConfig,
-        invigilation: invigilationMenuConfig,
-      },
       isFullscreen: false,
     };
   },
   mounted() {
-    const curRouterName = this.$route.meta.relate || this.$route.name;
-    Object.keys(this.modaleNavs).forEach((mkey) => {
-      if (this.curNav) return;
-      let curRouter = null;
-      this.modaleNavs[mkey].forEach((item) => {
-        item.children.forEach((elem) => {
-          if (elem.name === curRouterName) curRouter = elem;
-        });
-      });
-
-      if (curRouter) this.toPage({ name: mkey });
-    });
+    this.actCurNav();
     this.registFullscreenChange();
     this.$store.commit("setIsFullScreen", this.checkDocIsFullscreen());
   },
   methods: {
+    actCurNav() {
+      const curRouterName = this.$route.meta.relate || this.$route.name;
+      this.navs.forEach((nav) => {
+        if (this.curNav) return;
+        let curRouter = null;
+        nav.children.forEach((item) => {
+          item.children.forEach((elem) => {
+            if (elem.name === curRouterName) curRouter = elem;
+          });
+        });
+
+        if (curRouter) this.toPage(nav);
+      });
+    },
     toPage(nav) {
       this.curNav = nav.name;
       this.$emit("on-nav-change", nav.name);

+ 7 - 7
src/views/Layout/components/menu.js

@@ -5,17 +5,17 @@
 const headerMenuConfig = [
   {
     title: "系统管理",
-    name: "system",
+    name: "System",
     icon: "icon-base",
   },
   {
     title: "考务管理",
-    name: "business",
+    name: "Exam",
     icon: "icon-business",
   },
   {
     title: "监考管理",
-    name: "invigilation",
+    name: "Invigilation",
     icon: "icon-invigilation",
   },
 ];
@@ -38,7 +38,7 @@ const systemMenuConfig = [
   },
 ];
 
-const businessMenuConfig = [
+const examMenuConfig = [
   {
     title: "考务管理",
     name: "Exam",
@@ -49,7 +49,7 @@ const businessMenuConfig = [
         name: "ExamManagement",
       },
       {
-        title: "考生信息",
+        title: "考生管理",
         name: "ExamStudentManagement",
       },
       {
@@ -151,9 +151,9 @@ const invigilationMenuConfig = [
   },
 ];
 
-export {
+export default {
   headerMenuConfig,
   systemMenuConfig,
-  businessMenuConfig,
+  examMenuConfig,
   invigilationMenuConfig,
 };