zhangjie пре 4 година
родитељ
комит
22a77a1164
30 измењених фајлова са 1546 додато и 18 уклоњено
  1. BIN
      public/img/avatars/1.jpg
  2. BIN
      public/img/avatars/2.jpg
  3. BIN
      public/img/avatars/3.jpg
  4. 4 0
      public/index.html
  5. 0 0
      src/assets/bg-split-line.png
  6. 15 0
      src/features/invigilation/ExamInvigilation/ExamInvigilation.vue
  7. 15 0
      src/features/invigilation/ExamReport/ExamReport.vue
  8. 15 0
      src/features/invigilation/InvigilationDetail/InvigilationDetail.vue
  9. 15 0
      src/features/invigilation/OnlinePatrol/OnlinePatrol.vue
  10. 15 0
      src/features/invigilation/ProgressDetail/ProgressDetail.vue
  11. 15 0
      src/features/invigilation/RealtimeMonitoring/RealtimeMonitoring.vue
  12. 15 0
      src/features/invigilation/ReexamApply/ReexamApply.vue
  13. 15 0
      src/features/invigilation/ReexamChecked/ReexamChecked.vue
  14. 15 0
      src/features/invigilation/ReexamPending/ReexamPending.vue
  15. 15 0
      src/features/invigilation/StudentLogManage/StudentLogManage.vue
  16. 249 0
      src/features/invigilation/VideoCommunication/VideoCommunication.vue
  17. 571 0
      src/features/invigilation/WainingManage/WainingDetail.vue
  18. 232 0
      src/features/invigilation/WainingManage/WainingManage.vue
  19. 1 0
      src/main.js
  20. 39 0
      src/plugins/RongIM.js
  21. 1 1
      src/plugins/element.js
  22. 3 0
      src/router/index.js
  23. 83 0
      src/router/invigilation.js
  24. 139 0
      src/styles/base.scss
  25. 2 1
      src/styles/element-variables.scss
  26. 1 0
      src/styles/global.css
  27. 24 0
      src/styles/icons.scss
  28. 27 1
      src/views/Layout/components/NavBar.vue
  29. 2 1
      src/views/Layout/components/SideBar.vue
  30. 18 14
      src/views/Layout/components/menu.js

BIN
public/img/avatars/1.jpg


BIN
public/img/avatars/2.jpg


BIN
public/img/avatars/3.jpg


+ 4 - 0
public/index.html

@@ -6,6 +6,10 @@
     <meta name="viewport" content="width=device-width,initial-scale=1.0" />
     <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
     <title>在线考试管理后台</title>
+    <!-- 未压缩版 -->
+    <script src="https://cdn.ronghub.com/RongIMLib-2.5.9.js"></script>
+    <!-- 压缩版 -->
+    <script src="https://cdn.ronghub.com/RongRTC-3.2.4.min.js"></script>
   </head>
   <body>
     <noscript>

+ 0 - 0
src/assets/icon-split-line.png → src/assets/bg-split-line.png


+ 15 - 0
src/features/invigilation/ExamInvigilation/ExamInvigilation.vue

@@ -0,0 +1,15 @@
+<template>
+  <div class="exam-invigilation">
+    exam-invigilation
+  </div>
+</template>
+
+<script>
+export default {
+  name: "exam-invigilation",
+  data() {
+    return {};
+  },
+  methods: {},
+};
+</script>

+ 15 - 0
src/features/invigilation/ExamReport/ExamReport.vue

@@ -0,0 +1,15 @@
+<template>
+  <div class="ExamReport">
+    ExamReport
+  </div>
+</template>
+
+<script>
+export default {
+  name: "ExamReport",
+  data() {
+    return {};
+  },
+  methods: {},
+};
+</script>

+ 15 - 0
src/features/invigilation/InvigilationDetail/InvigilationDetail.vue

@@ -0,0 +1,15 @@
+<template>
+  <div class="InvigilationDetail">
+    InvigilationDetail
+  </div>
+</template>
+
+<script>
+export default {
+  name: "InvigilationDetail",
+  data() {
+    return {};
+  },
+  methods: {},
+};
+</script>

+ 15 - 0
src/features/invigilation/OnlinePatrol/OnlinePatrol.vue

@@ -0,0 +1,15 @@
+<template>
+  <div class="online-patrol">
+    online-patrol
+  </div>
+</template>
+
+<script>
+export default {
+  name: "online-patrol",
+  data() {
+    return {};
+  },
+  methods: {},
+};
+</script>

+ 15 - 0
src/features/invigilation/ProgressDetail/ProgressDetail.vue

@@ -0,0 +1,15 @@
+<template>
+  <div class="ProgressDetail">
+    ProgressDetail
+  </div>
+</template>
+
+<script>
+export default {
+  name: "ProgressDetail",
+  data() {
+    return {};
+  },
+  methods: {},
+};
+</script>

+ 15 - 0
src/features/invigilation/RealtimeMonitoring/RealtimeMonitoring.vue

@@ -0,0 +1,15 @@
+<template>
+  <div class="RealtimeMonitoring">
+    RealtimeMonitoring
+  </div>
+</template>
+
+<script>
+export default {
+  name: "RealtimeMonitoring",
+  data() {
+    return {};
+  },
+  methods: {},
+};
+</script>

+ 15 - 0
src/features/invigilation/ReexamApply/ReexamApply.vue

@@ -0,0 +1,15 @@
+<template>
+  <div class="ReexamApply">
+    ReexamApply
+  </div>
+</template>
+
+<script>
+export default {
+  name: "ReexamApply",
+  data() {
+    return {};
+  },
+  methods: {},
+};
+</script>

+ 15 - 0
src/features/invigilation/ReexamChecked/ReexamChecked.vue

@@ -0,0 +1,15 @@
+<template>
+  <div class="ReexamChecked">
+    ReexamChecked
+  </div>
+</template>
+
+<script>
+export default {
+  name: "ReexamChecked",
+  data() {
+    return {};
+  },
+  methods: {},
+};
+</script>

+ 15 - 0
src/features/invigilation/ReexamPending/ReexamPending.vue

@@ -0,0 +1,15 @@
+<template>
+  <div class="ReexamPending">
+    ReexamPending
+  </div>
+</template>
+
+<script>
+export default {
+  name: "ReexamPending",
+  data() {
+    return {};
+  },
+  methods: {},
+};
+</script>

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

@@ -0,0 +1,15 @@
+<template>
+  <div class="StudentLogManage">
+    StudentLogManage
+  </div>
+</template>
+
+<script>
+export default {
+  name: "StudentLogManage",
+  data() {
+    return {};
+  },
+  methods: {},
+};
+</script>

+ 249 - 0
src/features/invigilation/VideoCommunication/VideoCommunication.vue

@@ -0,0 +1,249 @@
+<template>
+  <div class="video-communication">
+    <div class="part-box-head">
+      <div class="part-box-head-left"><h1>通话申请列表</h1></div>
+      <div class="part-box-head-right">
+        <el-radio-group size="small" v-model="pageType">
+          <el-radio-button label="0">待处理</el-radio-button>
+          <el-radio-button label="1">已处理</el-radio-button>
+        </el-radio-group>
+      </div>
+    </div>
+    <div class="student-list">
+      <el-row :gutter="20">
+        <el-col :md="6" :lg="6" :xl="4" v-for="item in students" :key="item.id">
+          <div class="student-item">
+            <div class="student-cover">
+              <img :src="item.stdAvatar" :alt="item.stdName" />
+            </div>
+            <h4 class="student-name">{{ item.stdName }}</h4>
+            <el-button round type="success" @click="answer(item, 0)"
+              >语音通话</el-button
+            >
+            <br />
+            <el-button round type="primary" @click="answer(item, 1)"
+              >视频通话</el-button
+            >
+          </div>
+        </el-col>
+      </el-row>
+    </div>
+
+    <!-- 通话弹出层 -->
+    <el-dialog
+      custom-class="communication-dialog"
+      :visible.sync="dialogVisible"
+      width="600px"
+      :show-close="false"
+      :close-on-press-escape="false"
+      :close-on-click-modal="false"
+      append-to-body
+      fullscreen
+    >
+      <div class="communication-box">
+        <div class="communication-host">
+          <video src="" ref="VideoHost" v-show="videoHostReady"></video>
+        </div>
+        <div class="communication-guest">
+          <video src="" ref="VideoGuest" v-show="videoGuestReady"></video>
+        </div>
+        <div class="communication-action">
+          <el-button round type="danger" @click="hangup">结束通话</el-button>
+        </div>
+        <div class="communication-info">
+          <span>当前网络状态良好</span>
+          <span>持续时长:{{ durationTime }}</span>
+        </div>
+      </div>
+      <span slot="footer" class="dialog-footer"> </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { initRongRTC, connectIM } from "@/plugins/RongIM";
+
+export default {
+  name: "VideoCommunication",
+  data() {
+    return {
+      pageType: "0",
+      dialogVisible: false,
+      students: [
+        {
+          id: 1,
+          roomId: "1",
+          stdAvatar: "/img/avatars/1.jpg",
+          stdName: "邹朵朵",
+        },
+        {
+          id: 2,
+          roomId: "2",
+          stdAvatar: "/img/avatars/2.jpg",
+          stdName: "罗图图",
+        },
+        {
+          id: 3,
+          roomId: "3",
+          stdAvatar: "/img/avatars/3.jpg",
+          stdName: "于梁",
+        },
+      ],
+      durationTime: "00:10:23",
+      token:
+        "iy7OAXkkr+ooBirzYIT8BC7ePbpkBY7eRygkv+h3Iok=@tlem.cn.rongnav.com;tlem.cn.rongcfg.com",
+      rongUserId: null,
+      rongRTC: null,
+      room: null,
+      stream: null,
+      streamUser: null,
+      curStreamType: null,
+      videoHostReady: false,
+      videoGuestReady: false,
+    };
+  },
+  mounted() {
+    // this.initRong();
+  },
+  methods: {
+    async initRong() {
+      this.rongUserId = await connectIM(this.token).catch((error) => {
+        console.log(error);
+      });
+      if (!this.rongUserId) {
+        this.$message.error("初始化错误!");
+        return;
+      }
+      this.rongRTC = initRongRTC({ debug: true });
+    },
+    async answer(student, isVideo) {
+      this.dialogVisible = true;
+
+      const { Room, Stream, StreamType } = this.rongRTC;
+      // 初始化房间
+      this.room = new Room({
+        id: student.roomId,
+        joined: (user) => {
+          console.log(user);
+        },
+        left: (user) => {
+          console.log(user);
+        },
+      });
+      // 加入房间
+      let joinRoomSuccess = true;
+      await this.room.join({ id: this.rongUserId }).catch((error) => {
+        console.log(error);
+        joinRoomSuccess = false;
+      });
+      if (!joinRoomSuccess) return;
+
+      // 初始化stream
+      // TODO:后加入房间的人,是否会执行先加入房间的人的published回调
+      this.stream = new Stream({
+        published: (user) => {
+          console.log(user);
+          this.stream.subscribe(user).then((user) => {
+            let {
+              stream: { mediaStream },
+            } = user;
+            this.$refs.VideoGuest.srcObject = mediaStream;
+            this.$refs.VideoGuest.autoplay = true;
+            this.videoGuestReady = true;
+          });
+        },
+        unpublished: (user) => {
+          this.stream.unsubscribe(user);
+        },
+      });
+
+      // 按需发布本地音视频
+      this.curStreamType = isVideo
+        ? StreamType.AUDIO_AND_VIDEO
+        : StreamType.AUDIO;
+      const localStream = await this.stream.get().catch((error) => {
+        console.log(error);
+        this.$message.error("获取本地音视频失败!");
+      });
+      if (!localStream) return;
+
+      const streamUser = {
+        id: this.rongUserId,
+        stream: {
+          tag: "RongCloudRTC",
+          type: this.curStreamType,
+          mediaStream: localStream.mediaStream,
+        },
+      };
+      const streamPublishResult = await this.stream
+        .publish(streamUser)
+        .catch((error) => {
+          console.log(error);
+        });
+      if (isVideo && streamPublishResult) {
+        this.$refs.VideoHost.autoplay = true;
+        this.$refs.VideoHost.srcObject = localStream.mediaStream;
+        this.videoHostReady = true;
+      }
+    },
+    async hangup() {
+      const streamUser = {
+        id: this.rongUserId,
+        stream: {
+          tag: "RongCloudRTC",
+          type: this.curStreamType,
+        },
+      };
+      await this.stream.unpublish(streamUser).catch((error) => {
+        console.log(error);
+      });
+      await this.room.leave().catch((error) => {
+        console.log(error);
+      });
+
+      this.dialogVisible = false;
+      this.initData();
+    },
+    initData() {
+      this.room = null;
+      this.stream = null;
+      this.streamUser = null;
+      this.curStreamType = null;
+      this.videoHostReady = false;
+      this.videoGuestReady = false;
+    },
+  },
+  beforeDestroy() {
+    if (this.rongRTC) this.rongRTC.destroy();
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.student-list {
+  .student-item {
+    padding: 20px;
+    background: #fff;
+    border-radius: 10px;
+    text-align: center;
+    .el-button {
+      margin-bottom: 12px;
+    }
+  }
+  .student-cover {
+    max-height: 200px;
+    border-radius: 6px;
+    overflow: hidden;
+    img {
+      display: block;
+      width: 100%;
+    }
+  }
+  .student-name {
+    margin: 20px 0;
+    font-size: 18px;
+    line-height: 25px;
+    color: #202b4b;
+  }
+}
+</style>

+ 571 - 0
src/features/invigilation/WainingManage/WainingDetail.vue

@@ -0,0 +1,571 @@
+<template>
+  <div class="warning-detail">
+    <div class="warning-detail-head">
+      <div class="warning-detail-title">
+        <h2>预警详情</h2>
+        <el-button size="mini" icon="el-icon-arrow-left">返回列表</el-button>
+      </div>
+      <div class="warning-detail-student">
+        <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>
+          </div>
+          <div class="student-head-right">
+            <el-button
+              size="mini"
+              type="primary"
+              icon="el-icon-arrow-left"
+            ></el-button>
+            <el-button
+              size="mini"
+              type="primary"
+              icon="el-icon-arrow-right"
+            ></el-button>
+          </div>
+        </div>
+        <div class="student-views">
+          <div class="student-avatar">
+            <img :src="curStudent.stdAvatar" alt="" />
+          </div>
+          <div class="student-video">
+            <div class="student-video-item">
+              <video
+                src=""
+                ref="FirstViewVideo"
+                v-show="firstViewVideoReady"
+              ></video>
+              <div class="student-video-none" v-show="!firstViewVideoReady">
+                <i class="el-icon-video-camera-solid"></i>
+              </div>
+            </div>
+            <div class="student-video-item">
+              <video
+                src=""
+                ref="ThreeViewVideo"
+                v-show="threeViewVideoReady"
+              ></video>
+              <div class="student-video-none" v-show="!threeViewVideoReady">
+                <i class="el-icon-video-camera-solid"></i>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="student-exception">
+          <ul>
+            <li v-for="(item, index) in exceptionList" :key="index">
+              <i>{{ index + 1 }}</i>
+              <h4>{{ item.title }}</h4>
+              <p>{{ item.desc }}</p>
+            </li>
+          </ul>
+        </div>
+      </div>
+    </div>
+
+    <div class="warning-detail-body">
+      <div class="warning-body-head clear-float">
+        <div class="warning-body-head-action">
+          <h3>考试轨迹</h3>
+          <el-button type="primary" icon="icon icon-view"></el-button>
+          <el-button type="primary" icon="icon icon-text"></el-button>
+          <el-button type="primary" icon="icon icon-audio"></el-button>
+          <el-popover
+            class="warning-body-head-call"
+            placement="bottom-start"
+            trigger="click"
+          >
+            <el-button type="success" @click="answer(0)">语音通话</el-button>
+            <el-button type="primary" @click="answer(1)">视频通话</el-button>
+            <el-button type="primary" slot="reference">实时通话</el-button>
+          </el-popover>
+        </div>
+        <div class="warning-body-head-info">
+          <p><span>系统预警</span><span>1次</span></p>
+          <p><span>陌生人脸</span><span>1次</span></p>
+          <p><span>异常处理</span><span>1次</span></p>
+          <p><span></span><span>违纪状态:正常</span></p>
+          <el-button type="danger" icon="icon icon-stop">违纪处理</el-button>
+          <el-button type="warning" icon="icon icon-forbide"
+            >强制收卷</el-button
+          >
+        </div>
+      </div>
+      <div class="warning-body-main"></div>
+    </div>
+
+    <!-- 通话弹出层 -->
+    <el-dialog
+      custom-class="communication-dialog"
+      :visible.sync="dialogVisible"
+      width="600px"
+      :show-close="false"
+      :close-on-press-escape="false"
+      :close-on-click-modal="false"
+      append-to-body
+      fullscreen
+    >
+      <div class="communication-box">
+        <div class="communication-host">
+          <video src="" ref="VideoHost" v-show="videoHostReady"></video>
+        </div>
+        <div class="communication-guest">
+          <video src="" ref="VideoGuest" v-show="videoGuestReady"></video>
+        </div>
+        <div class="communication-action">
+          <el-button round type="danger" @click="hangup">结束通话</el-button>
+        </div>
+        <div class="communication-info">
+          <span>当前网络状态良好</span>
+          <span>持续时长:{{ durationTime }}</span>
+        </div>
+      </div>
+      <span slot="footer" class="dialog-footer"> </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { initRongRTC, connectIM } from "@/plugins/RongIM";
+
+export default {
+  name: "warning-detail",
+  data() {
+    return {
+      exceptionList: [
+        {
+          title: "身份验证不通过",
+          desc: "系统发现该考生身份识别多次不通过,请监考老师关注!",
+        },
+        {
+          title: "疑似:有违规动作",
+          desc:
+            "系统发现该考生至少有多次以上持续1分钟以上的违规动作,且违规动作的持续时间已超出合理范围,请监考老师关注!",
+        },
+        {
+          title: "疑似:启用虚拟摄像头",
+          desc: "系统检测到考生可能使用了虚拟摄像头,请监考老师关注!",
+        },
+      ],
+      curStudent: {
+        stdAvatar: "/img/avatars/2.jpg",
+        stdCardNo: "000000000000000008",
+        stdName: "张龙龙",
+        subjectName: "大学英语",
+        subjectCode: "10006",
+        liveUrlFirst: "",
+        liveUrlThree: "",
+        roomId: "1",
+      },
+      firstViewVideoReady: false,
+      threeViewVideoReady: false,
+      // communication
+      durationTime: "00:10:23",
+      token:
+        "iy7OAXkkr+ooBirzYIT8BC7ePbpkBY7eRygkv+h3Iok=@tlem.cn.rongnav.com;tlem.cn.rongcfg.com",
+      rongUserId: null,
+      rongRTC: null,
+      room: null,
+      stream: null,
+      streamUser: null,
+      curStreamType: null,
+      videoHostReady: false,
+      videoGuestReady: false,
+      dialogVisible: false,
+    };
+  },
+  methods: {
+    async initRong() {
+      this.rongUserId = await connectIM(this.token).catch((error) => {
+        console.log(error);
+      });
+      if (!this.rongUserId) {
+        this.$message.error("初始化错误!");
+        return;
+      }
+      this.rongRTC = initRongRTC({ debug: true });
+    },
+    initVideo() {
+      const { Stream } = this.rongRTC;
+      // 初始化stream
+      this.stream = new Stream({
+        published: (user) => {
+          console.log(user);
+        },
+        unpublished: (user) => {
+          this.stream.unsubscribe(user);
+        },
+      });
+
+      this.stream.subscribe({ liveUrl: this.liveUrlFirst }).then((user) => {
+        let {
+          stream: { mediaStream },
+        } = user;
+        this.$refs.FirstViewVideo.srcObject = mediaStream;
+        this.$refs.FirstViewVideo.autoplay = true;
+        this.firstViewVideoReady = true;
+      });
+
+      this.stream.subscribe({ liveUrl: this.liveUrlThree }).then((user) => {
+        let {
+          stream: { mediaStream },
+        } = user;
+        this.$refs.ThreeViewVideo.srcObject = mediaStream;
+        this.$refs.ThreeViewVideo.autoplay = true;
+        this.threeViewVideoReady = true;
+      });
+    },
+    closeVideo() {
+      this.stream.unsubscribe({ liveUrl: this.liveUrlFirst });
+      this.stream.unsubscribe({ liveUrl: this.liveUrlThree });
+      this.firstViewVideoReady = false;
+      this.threeViewVideoReady = false;
+    },
+    async answer(isVideo) {
+      this.closeVideo();
+      this.dialogVisible = true;
+
+      const { Room, Stream, StreamType } = this.rongRTC;
+      // 初始化房间
+      this.room = new Room({
+        id: this.curStudent.roomId,
+        joined: (user) => {
+          console.log(user);
+        },
+        left: (user) => {
+          console.log(user);
+        },
+      });
+      // 加入房间
+      let joinRoomSuccess = true;
+      await this.room.join({ id: this.rongUserId }).catch((error) => {
+        console.log(error);
+        joinRoomSuccess = false;
+      });
+      if (!joinRoomSuccess) return;
+
+      // 初始化stream
+      // TODO:后加入房间的人,是否会执行先加入房间的人的published回调
+      this.stream = new Stream({
+        published: (user) => {
+          console.log(user);
+          this.stream.subscribe(user).then((user) => {
+            let {
+              stream: { mediaStream },
+            } = user;
+            this.$refs.VideoGuest.srcObject = mediaStream;
+            this.$refs.VideoGuest.autoplay = true;
+            this.videoGuestReady = true;
+          });
+        },
+        unpublished: (user) => {
+          this.stream.unsubscribe(user);
+        },
+      });
+
+      // 按需发布本地音视频
+      this.curStreamType = isVideo
+        ? StreamType.AUDIO_AND_VIDEO
+        : StreamType.AUDIO;
+      const localStream = await this.stream.get().catch((error) => {
+        console.log(error);
+        this.$message.error("获取本地音视频失败!");
+      });
+      if (!localStream) return;
+
+      const streamUser = {
+        id: this.rongUserId,
+        stream: {
+          tag: "RongCloudRTC",
+          type: this.curStreamType,
+          mediaStream: localStream.mediaStream,
+        },
+      };
+      const streamPublishResult = await this.stream
+        .publish(streamUser)
+        .catch((error) => {
+          console.log(error);
+        });
+      if (isVideo && streamPublishResult) {
+        this.$refs.VideoHost.autoplay = true;
+        this.$refs.VideoHost.srcObject = localStream.mediaStream;
+        this.videoHostReady = true;
+      }
+    },
+    async hangup() {
+      const streamUser = {
+        id: this.rongUserId,
+        stream: {
+          tag: "RongCloudRTC",
+          type: this.curStreamType,
+        },
+      };
+      await this.stream.unpublish(streamUser).catch((error) => {
+        console.log(error);
+      });
+      await this.room.leave().catch((error) => {
+        console.log(error);
+      });
+
+      this.dialogVisible = false;
+      this.initData();
+    },
+    initData() {
+      this.room = null;
+      this.stream = null;
+      this.streamUser = null;
+      this.curStreamType = null;
+      this.videoHostReady = false;
+      this.videoGuestReady = false;
+      this.initVideo();
+    },
+  },
+  beforeDestroy() {
+    if (this.rongRTC) this.rongRTC.destroy();
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.warning-detail {
+  &-head {
+    margin: -30px -30px 0;
+    padding: 30px;
+    background: #fff;
+    border: 1px solid #f0f4f9;
+  }
+  &-title {
+    overflow: hidden;
+    padding-bottom: 20px;
+    border-bottom: 1px solid #f0f4f9;
+    > h2 {
+      float: left;
+      font-weight: 600;
+      font-size: 18px;
+      line-height: 28px;
+      margin: 0;
+    }
+    > .el-button {
+      float: right;
+    }
+  }
+}
+.student-head {
+  padding: 20px 0;
+  overflow: hidden;
+  &-left {
+    float: left;
+    > p {
+      display: inline-block;
+      vertical-align: middle;
+      margin: 0;
+      line-height: 28px;
+      height: 28px;
+      margin-right: 15px;
+
+      &:first-child {
+        margin-right: 10px;
+      }
+
+      > span {
+        color: #626a82;
+
+        &:last-child {
+          color: #202b4b;
+          margin-left: 5px;
+          font-weight: 600;
+        }
+      }
+    }
+  }
+  &-right {
+    float: right;
+  }
+}
+.student-views {
+  height: 240px;
+  margin: 0 -15px;
+}
+.student-avatar {
+  padding: 0 15px;
+  width: 210px;
+  height: 100%;
+  float: left;
+  > img {
+    display: block;
+    width: 100%;
+    height: 100%;
+    border-radius: 6px;
+  }
+}
+.student-video {
+  margin-left: 210px;
+  height: 240px;
+  font-size: 0;
+
+  &-item {
+    display: inline-block;
+    vertical-align: top;
+    font-size: 14px;
+    padding: 0 15px;
+    width: 50%;
+    height: 100%;
+    > video {
+      display: block;
+      width: 100%;
+      height: 100%;
+      border-radius: 6px;
+      box-shadow: 0 0 1px #333;
+      background: #606060;
+    }
+  }
+
+  &-none {
+    height: 100%;
+    background: #606060;
+    border-radius: 6px;
+    font-size: 50px;
+    text-align: center;
+    padding-top: 90px;
+    color: #202b4b;
+  }
+}
+.student-exception {
+  padding-top: 30px;
+  overflow: auto;
+  ul,
+  li {
+    margin: 0;
+    padding: 0;
+  }
+  ul {
+    white-space: nowrap;
+    font-size: 0;
+  }
+  li {
+    display: inline-block;
+    vertical-align: top;
+    font-size: 14px;
+    position: relative;
+    padding-left: 38px;
+    width: 400px;
+
+    > i {
+      display: block;
+      position: absolute;
+      font-size: 29px;
+      line-height: 1;
+      color: #d9dfe8;
+      top: 3px;
+      left: 5px;
+      z-index: 9;
+      font-style: normal;
+
+      &::after {
+        content: "";
+        display: block;
+        position: absolute;
+        background: #fff;
+        width: 15px;
+        height: 15px;
+        transform: rotate(45deg);
+        bottom: -2px;
+        right: -10px;
+        z-index: 9;
+      }
+    }
+
+    &:not(:last-child):after {
+      content: "";
+      display: block;
+      position: absolute;
+      background-image: url(../../../assets/bg-split-line.png);
+      background-position: 100% 100%;
+      width: 30px;
+      height: 52px;
+      top: 6px;
+      right: 10px;
+      z-index: 9;
+    }
+    h4 {
+      font-size: 16px;
+      font-weight: 600;
+      color: #202b4b;
+      line-height: 22px;
+      margin-bottom: 4px;
+    }
+    p {
+      margin: 0;
+      width: 304px;
+      font-weight: 400;
+      color: #626a82;
+      line-height: 20px;
+      white-space: normal;
+    }
+  }
+}
+.warning-detail-body {
+  margin-top: 30px;
+}
+.warning-body-head {
+  background: #fff;
+  border-bottom: 1px solid #f0f4f9;
+  padding: 20px;
+  &-action {
+    float: left;
+    > h3 {
+      line-height: 32px;
+      display: inline-block;
+      vertical-align: middle;
+      font-size: 18px;
+      font-weight: 600;
+      margin: 0 30px 0 0;
+    }
+
+    i.icon {
+      margin: 0;
+    }
+  }
+  &-call {
+    margin: 0 10px;
+  }
+  &-info {
+    float: right;
+    > p {
+      margin: 0;
+      height: 32px;
+      line-height: 32px;
+      display: inline-block;
+      vertical-align: middle;
+      margin-right: 20px;
+      &::before {
+        content: "";
+        display: inline-block;
+        vertical-align: middle;
+        width: 8px;
+        height: 8px;
+        border: 2px solid #fe5863;
+        margin-right: 6px;
+        margin-top: -3px;
+        border-radius: 50%;
+      }
+
+      > span {
+        color: #626a82;
+
+        &:last-child {
+          color: #202b4b;
+          font-weight: 600;
+          margin-left: 8px;
+        }
+      }
+    }
+  }
+}
+.warning-body-main {
+  min-height: 400px;
+  background: #fff;
+}
+</style>

+ 232 - 0
src/features/invigilation/WainingManage/WainingManage.vue

@@ -0,0 +1,232 @@
+<template>
+  <div class="WainingManage">
+    <div class="part-box-head">
+      <div class="part-box-head-left"><h1>预警提醒</h1></div>
+      <div class="part-box-head-right">
+        <span>考场名称:哈尔滨工业大学第十一考场</span>
+      </div>
+    </div>
+    <div class="part-box part-box-filter">
+      <el-form
+        class="part-box-filter-form"
+        ref="FilterForm"
+        label-position="left"
+        inline
+      >
+        <el-form-item>
+          <el-select
+            v-model="filter.batchId"
+            placeholder="请选择批次"
+            clearable
+          >
+            <el-option
+              v-for="item in batchs"
+              :key="item.id"
+              :value="item.id"
+              :label="item.name"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-select v-model="filter.examId" placeholder="请选择考场" clearable>
+            <el-option
+              v-for="item in exams"
+              :key="item.id"
+              :value="item.id"
+              :label="item.name"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-select
+            v-model="filter.subjectId"
+            placeholder="请选择科目"
+            clearable
+          >
+            <el-option
+              v-for="item in subjects"
+              :key="item.id"
+              :value="item.id"
+              :label="item.name"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-select
+            v-model="filter.auditStatus"
+            placeholder="审阅状态"
+            clearable
+          >
+            <el-option
+              v-for="item in subjects"
+              :key="item.id"
+              :value="item.id"
+              :label="item.name"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-input
+            v-model.trim="filter.content"
+            placeholder="姓名/证件号"
+            clearable
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="陌生人脸">
+          <el-input-number
+            style="width: 52px;"
+            v-model.trim="filter.strangePersonDown"
+            placeholder="下限"
+            :controls="false"
+          ></el-input-number>
+          <span>-</span>
+          <el-input-number
+            style="width: 52px;"
+            v-model.trim="filter.strangePersonUp"
+            placeholder="上限"
+            :controls="false"
+          ></el-input-number>
+        </el-form-item>
+        <el-form-item label="异常处理">
+          <el-input-number
+            style="width: 52px;"
+            v-model.trim="filter.exceptionDown"
+            placeholder="下限"
+            :controls="false"
+          ></el-input-number>
+          <span>-</span>
+          <el-input-number
+            style="width: 52px;"
+            v-model.trim="filter.exceptionUp"
+            placeholder="上限"
+            :controls="false"
+          ></el-input-number>
+        </el-form-item>
+        <el-form-item label="预警数">
+          <el-input-number
+            style="width: 52px;"
+            v-model.trim="filter.warningDown"
+            placeholder="下限"
+            :controls="false"
+          ></el-input-number>
+          <span>-</span>
+          <el-input-number
+            style="width: 52px;"
+            v-model.trim="filter.warningUp"
+            placeholder="上限"
+            :controls="false"
+          ></el-input-number>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="toPage(1)">查询</el-button>
+        </el-form-item>
+      </el-form>
+      <div class="part-box-filter-action">
+        <el-button type="primary" icon="icon icon-clean" @click="cleanUnread"
+          >清除未阅</el-button
+        >
+        <el-button type="danger" icon="icon icon-warning" @click="batchAction"
+          >批量处理违纪</el-button
+        >
+      </div>
+    </div>
+
+    <el-table ref="TableList" :data="dataList" border stripe>
+      <el-table-column prop="batchName" label="批次"></el-table-column>
+      <el-table-column prop="examName" label="场次"></el-table-column>
+      <el-table-column prop="examroom" label="考场"> </el-table-column>
+      <el-table-column prop="examId" label="考试ID"></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="subjectName" label="科目名称"></el-table-column>
+      <el-table-column prop="subjectCode" label="科目(代码)"></el-table-column>
+      <el-table-column prop="strangeNumber" label="陌生人脸"></el-table-column>
+      <el-table-column
+        prop="exceptionNumber"
+        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="auditStatus" label="审阅状态"></el-table-column>
+      <el-table-column label="操作">
+        <template slot-scope="scope">
+          <el-button
+            class="btn-table-icon"
+            type="primary"
+            icon="icon icon-view"
+            @click="toDetail(scope.row)"
+            >详情</el-button
+          >
+        </template>
+      </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"
+        @current-change="toPage"
+      >
+      </el-pagination>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "WainingManage",
+  data() {
+    return {
+      filter: {
+        batchId: null,
+        examId: null,
+        subjectId: null,
+        auditStatus: null,
+        content: "",
+        strangePersonUp: null,
+        strangePersonDown: null,
+        exceptionUp: null,
+        exceptionDown: null,
+        warningUp: null,
+        warningDown: null,
+      },
+      current: 1,
+      total: 0,
+      size: 10,
+      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: "未阅",
+        },
+      ],
+    };
+  },
+  methods: {
+    getList() {},
+    toPage() {},
+    cleanUnread() {},
+    batchAction() {},
+    toDetail(row) {
+      console.log(row);
+      this.$router.push({ name: "WainingDetail", params: { id: row.id } });
+    },
+  },
+};
+</script>

+ 1 - 0
src/main.js

@@ -22,6 +22,7 @@ import "./utils/monitors";
 import "./styles/bootstrap.scss";
 import "./styles/global.css";
 import "./styles/icons.scss";
+import "./styles/base.scss";
 // styles end
 
 // 可以回退到上次route的状态,不重新执行生命周期函数

+ 39 - 0
src/plugins/RongIM.js

@@ -0,0 +1,39 @@
+window.RongIMLib.RongIMClient.init("3argexb63s9ie");
+// 连接状态监听器
+window.RongIMClient.setConnectionStatusListener({
+  onChanged: function (status) {
+    console.log(status);
+  },
+});
+// 消息监听器
+window.RongIMClient.setOnReceiveMessageListener({
+  onReceived: function (message) {
+    console.log(message);
+  },
+});
+
+export const initRongRTC = (options) => {
+  return new window.RongRTC({
+    RongIMLib: window.RongIMLib,
+    ...options,
+  });
+};
+
+export const connectIM = (token) => {
+  return new Promise((resolve, reject) => {
+    window.RongIMClient.connect(token, {
+      onSuccess: (userId) => {
+        console.log("connect success, userId:" + userId);
+        resolve(userId);
+      },
+      onTokenIncorrect: () => {
+        reject({ type: "token", msg: "token unvalid" });
+        console.log("token unvalid");
+      },
+      onError: (errorCode) => {
+        console.log(errorCode);
+        reject({ type: "error", msg: errorCode });
+      },
+    });
+  });
+};

+ 1 - 1
src/plugins/element.js

@@ -2,4 +2,4 @@ import Vue from "vue";
 import Element from "element-ui";
 import "../styles/element-variables.scss";
 
-Vue.use(Element, { size: "medium" });
+Vue.use(Element, { size: "small" });

+ 3 - 0
src/router/index.js

@@ -5,6 +5,8 @@ import VueRouter from "vue-router";
 import Home from "../views/Home/Home.vue";
 import Layout from "@/views/Layout/Layout.vue";
 
+import invigilation from "./invigilation";
+
 // ignore NavigationDuplicated. https://github.com/vuejs/vue-router/issues/2881
 const originalPush = VueRouter.prototype.push;
 VueRouter.prototype.push = function push(location, onResolve, onReject) {
@@ -41,6 +43,7 @@ const routes = [
         path: "",
         component: Home,
       },
+      ...invigilation,
     ],
   },
   {

+ 83 - 0
src/router/invigilation.js

@@ -0,0 +1,83 @@
+import ExamInvigilation from "../features/invigilation/ExamInvigilation/ExamInvigilation";
+import OnlinePatrol from "../features/invigilation/OnlinePatrol/OnlinePatrol";
+import RealtimeMonitoring from "../features/invigilation/RealtimeMonitoring/RealtimeMonitoring";
+import InvigilationDetail from "../features/invigilation/InvigilationDetail/InvigilationDetail";
+import WainingManage from "../features/invigilation/WainingManage/WainingManage";
+import WainingDetail from "../features/invigilation/WainingManage/WainingDetail";
+import ReexamApply from "../features/invigilation/ReexamApply/ReexamApply";
+import ProgressDetail from "../features/invigilation/ProgressDetail/ProgressDetail";
+import VideoCommunication from "../features/invigilation/VideoCommunication/VideoCommunication";
+import ReexamPending from "../features/invigilation/ReexamPending/ReexamPending";
+import ReexamChecked from "../features/invigilation/ReexamChecked/ReexamChecked";
+import ExamReport from "../features/invigilation/ExamReport/ExamReport";
+import StudentLogManage from "../features/invigilation/StudentLogManage/StudentLogManage";
+
+const routes = [
+  {
+    path: "/invigilation/exam-invigilation",
+    name: "ExamInvigilation",
+    component: ExamInvigilation,
+  },
+  {
+    path: "/invigilation/online-patrol",
+    name: "OnlinePatrol",
+    component: OnlinePatrol,
+  },
+  {
+    path: "/invigilation/realtime-monitoring",
+    name: "RealtimeMonitoring",
+    component: RealtimeMonitoring,
+  },
+  {
+    path: "/invigilation/invigilation-detail",
+    name: "InvigilationDetail",
+    component: InvigilationDetail,
+  },
+  {
+    path: "/invigilation/waining-manage",
+    name: "WainingManage",
+    component: WainingManage,
+  },
+  {
+    path: "/invigilation/waining-detail/:id",
+    name: "WainingDetail",
+    component: WainingDetail,
+  },
+  {
+    path: "/invigilation/reexam-apply",
+    name: "ReexamApply",
+    component: ReexamApply,
+  },
+  {
+    path: "/invigilation/progress-detail",
+    name: "ProgressDetail",
+    component: ProgressDetail,
+  },
+  {
+    path: "/invigilation/video-communication",
+    name: "VideoCommunication",
+    component: VideoCommunication,
+  },
+  {
+    path: "/invigilation/reexam-pending",
+    name: "ReexamPending",
+    component: ReexamPending,
+  },
+  {
+    path: "/invigilation/reexam-checked",
+    name: "ReexamChecked",
+    component: ReexamChecked,
+  },
+  {
+    path: "/invigilation/exam-report",
+    name: "ExamReport",
+    component: ExamReport,
+  },
+  {
+    path: "/invigilation/student-log-manage",
+    name: "StudentLogManage",
+    component: StudentLogManage,
+  },
+];
+
+export default routes;

+ 139 - 0
src/styles/base.scss

@@ -0,0 +1,139 @@
+.part-box {
+  padding: 20px;
+  margin-bottom: 20px;
+  background-color: #fff;
+  border-radius: 6px;
+
+  &-head {
+    margin-bottom: 20px;
+    line-height: 32px;
+
+    &::after {
+      content: "";
+      display: block;
+      clear: both;
+      visibility: hidden;
+    }
+
+    &-left {
+      float: left;
+
+      > h1 {
+        color: #202b4b;
+        font-size: 18px;
+      }
+    }
+    &-right {
+      float: right;
+    }
+  }
+
+  &-filter {
+    padding: 20px 20px 10px;
+    display: flex;
+    &-form {
+      .el-form-item {
+        margin-bottom: 10px;
+      }
+      .el-form-item__label {
+        margin-bottom: 0;
+      }
+    }
+    &-action {
+      padding-bottom: 10px;
+      white-space: nowrap;
+      display: flex;
+      align-items: flex-end;
+    }
+  }
+}
+.part-page {
+  margin-top: 20px;
+  text-align: right;
+}
+
+.clear-float {
+  &::after {
+    content: "";
+    display: block;
+    clear: both;
+    visibility: hidden;
+  }
+}
+
+// communication-dialog
+.communication-dialog {
+  background-color: transparent;
+
+  .communication-box {
+    width: 600px;
+    height: 720px;
+    background: #000;
+    margin: 0 auto;
+    position: relative;
+    border-radius: 6px;
+
+    .communication-host {
+      position: relative;
+      height: 100%;
+    }
+    .communication-guest {
+      position: absolute;
+      width: 90px;
+      height: 120px;
+      border-radius: 6px;
+      top: 10px;
+      right: 10px;
+      z-index: 99;
+    }
+    .communication-action {
+      position: absolute;
+      width: 100%;
+      bottom: 40px;
+      left: 0;
+      z-index: 99;
+      text-align: center;
+    }
+    .communication-info {
+      position: absolute;
+      width: 100%;
+      bottom: 0;
+      height: 40px;
+      line-height: 40px;
+      padding: 0 20px;
+      color: #fff;
+
+      span {
+        display: block;
+        float: right;
+
+        &:first-child {
+          float: left;
+        }
+      }
+    }
+  }
+  // element
+  .el-dialog__footer,
+  .el-dialog__header {
+    display: none;
+  }
+}
+
+// element-custom ------------->
+// el-button
+.el-button {
+  .icon {
+    margin-right: 5px;
+    width: 14px;
+    height: 14px;
+  }
+  .icon-view {
+    height: 10px;
+  }
+  span {
+    display: inline-block;
+    vertical-align: middle;
+    line-height: 1;
+  }
+}

+ 2 - 1
src/styles/element-variables.scss

@@ -3,7 +3,8 @@ Write your variables here. All available variables can be
 found in element-ui/packages/theme-chalk/src/common/var.scss.
 For example, to overwrite the theme color:
 */
-$--color-primary: teal;
+$--color-primary: #1886fe;
+$--color-success: #1cd1a1;
 
 /* icon font path, required */
 $--font-path: "~element-ui/lib/theme-chalk/fonts";

+ 1 - 0
src/styles/global.css

@@ -2,6 +2,7 @@
 
 body {
   margin: 0;
+  font-size: 14px;
 }
 
 .qm-primary-text {

+ 24 - 0
src/styles/icons.scss

@@ -31,7 +31,31 @@
   &-invigilation-act {
     background-image: url(../assets/icon-invigilation-act.png);
   }
+  &-warning {
+    background-image: url(../assets/icon-warning.png);
+  }
+  &-warning-act {
+    background-image: url(../assets/icon-warning-act.png);
+  }
 
+  &-clean {
+    background-image: url(../assets/icon-clean.png);
+  }
+  &-stop {
+    background-image: url(../assets/icon-stop.png);
+  }
+  &-forbide {
+    background-image: url(../assets/icon-forbide.png);
+  }
+  &-view {
+    background-image: url(../assets/icon-view.png);
+  }
+  &-text {
+    background-image: url(../assets/icon-text.png);
+  }
+  &-audio {
+    background-image: url(../assets/icon-audio.png);
+  }
   &-logout {
     background-image: url(../assets/icon-logout.png);
   }

+ 27 - 1
src/views/Layout/components/NavBar.vue

@@ -47,7 +47,13 @@
 </template>
 
 <script>
-import { headerMenuConfig } from "./menu";
+import {
+  baseMenuConfig,
+  userMenuConfig,
+  businessMenuConfig,
+  invigilationMenuConfig,
+  headerMenuConfig,
+} from "./menu";
 
 export default {
   name: "NavBar",
@@ -60,8 +66,28 @@ export default {
     return {
       navs: headerMenuConfig,
       curNav: "",
+      modaleNavs: {
+        base: baseMenuConfig,
+        user: userMenuConfig,
+        business: businessMenuConfig,
+        invigilation: invigilationMenuConfig,
+      },
     };
   },
+  mounted() {
+    const curRouterName = 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 });
+    });
+  },
   methods: {
     toPage(nav) {
       this.curNav = nav.name;

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

@@ -43,11 +43,12 @@ export default {
   },
   data() {
     return {
-      curRouterName: this.$router.name,
+      curRouterName: this.$route.name,
     };
   },
   methods: {
     toRouter(nav) {
+      this.curRouterName = nav.name;
       this.$router.push({ name: nav.name });
     },
   },

+ 18 - 14
src/views/Layout/components/menu.js

@@ -126,68 +126,72 @@ const invigilationMenuConfig = [
     children: [
       {
         title: "考情监控",
-        name: "Base",
+        name: "ExamInvigilation",
       },
       {
         title: "在线巡考",
-        name: "Base",
+        name: "OnlinePatrol",
       },
     ],
   },
   {
     title: "监考管理",
-    name: "Base",
+    name: "monitoring",
     icon: "icon-invigilation",
     children: [
       {
         title: "实时监控台",
-        name: "Base",
+        name: "RealtimeMonitoring",
       },
       {
         title: "监考明细管理",
-        name: "Base",
+        name: "InvigilationDetail",
       },
       {
         title: "预警提醒",
-        name: "Base",
+        name: "WainingManage",
       },
       {
         title: "重考申请",
-        name: "Base",
+        name: "ReexamApply",
       },
       {
         title: "进度查询",
-        name: "Base",
+        name: "ProgressDetail",
+      },
+      {
+        title: "视频互动",
+        name: "VideoCommunication",
       },
     ],
   },
   {
     title: "重考审批",
-    name: "Base",
+    name: "Reexam",
     icon: "icon-reexam",
     children: [
       {
         title: "重考待审",
-        name: "Base",
+        name: "ReexamPending",
       },
       {
         title: "重考已审",
-        name: "Base",
+        name: "ReexamChecked",
       },
     ],
   },
   {
     title: "查询统计",
-    name: "Base",
+    name: "Query",
     icon: "icon-invigilation",
     children: [
       {
         title: "考情综合报表分析",
-        name: "Base",
+        name: "ExamReport",
       },
       {
         title: "考生端日志管理",
-        name: "Base",
+        name: "StudentLogManage",
       },
     ],
   },