瀏覽代碼

巡考大屏制作

zhangjie 2 年之前
父節點
當前提交
a28ffa5920

二進制
src/assets/bg-invi-body.png


二進制
src/assets/bg-invi-head.png


+ 271 - 0
src/features/invigilation/ExamInvigilation/ExamInvigilationFull.vue

@@ -0,0 +1,271 @@
+<template>
+  <div class="exam-invigilation-full invi-container">
+    <div class="invi-header">
+      <h1 class="invi-title">{{ summary.name }}在线考试数据监控中心</h1>
+      <text-clock
+        :show-icon="false"
+        :show-week="false"
+        :show-apm="false"
+      ></text-clock>
+    </div>
+    <div class="invi-body">
+      <div class="invi-monitor">
+        <div
+          v-for="item in students"
+          :key="item.examStudentId"
+          class="invi-monitor-item"
+        >
+          <invigilation-student
+            ref="InvigilationStudent"
+            :data="item"
+            show-type="patrol"
+            :max-retry-count="1"
+            detail-name="MORE"
+            @muted-change="videoAllMuted"
+          ></invigilation-student>
+        </div>
+      </div>
+      <div class="invi-main">
+        <div class="invi-summary">
+          <div class="invi-summary-item">
+            <h4>在线考试人数</h4>
+            <p>{{ summary.onlineCount }}</p>
+          </div>
+          <div class="invi-summary-item">
+            <h4>考试中人数</h4>
+            <p>{{ summary.examingCount }}</p>
+          </div>
+          <div class="invi-summary-item">
+            <h4>待考人数</h4>
+            <p>{{ summary.waitingCount }}</p>
+          </div>
+          <div class="invi-summary-item">
+            <h4>预警总数</h4>
+            <p>{{ summary.warnCount }}</p>
+          </div>
+          <div class="invi-summary-item">
+            <h4>违纪总数</h4>
+            <p>{{ summary.breachCount }}</p>
+          </div>
+        </div>
+        <div class="invi-map"></div>
+      </div>
+      <div class="invi-analysis">
+        <div class="invi-analysis-item">
+          <div class="invi-analysis-box invi-types">
+            <h2 class="invi-analysis-title">预警类型排行</h2>
+            <div class="invi-types-table">
+              <table class="invi-table">
+                <colgroup>
+                  <col width="70" />
+                  <col width="120" />
+                  <col width="20" />
+                </colgroup>
+                <tr v-for="item in inviTypesList" :key="item.id">
+                  <td>{{ item.type }}</td>
+                  <td>
+                    <el-progress
+                      :stroke-width="6"
+                      :show-text="false"
+                      :percentage="item.percentage"
+                    ></el-progress>
+                  </td>
+                  <td>{{ item.warnCount }}</td>
+                </tr>
+              </table>
+            </div>
+            <div class="invi-types-chart invi-chart">
+              <div class="invi-chart-title">预警类型占比</div>
+              <vue-charts
+                v-if="inviTypesChartOption"
+                :options="inviTypesChartOption"
+                autoresize
+              ></vue-charts>
+              <p class="chart-none" v-else>暂无数据</p>
+            </div>
+          </div>
+        </div>
+        <div class="invi-analysis-item">
+          <div class="invi-analysis-box invi-area">
+            <h2 class="invi-analysis-title">预警地区排行</h2>
+            <div class="invi-area-table">
+              <table class="invi-table">
+                <colgroup>
+                  <col width="40" />
+                  <col width="150" />
+                  <col width="30" />
+                </colgroup>
+                <tr v-for="item in inviAreaList" :key="item.id">
+                  <td>{{ item.province }}</td>
+                  <td>
+                    <el-progress
+                      :stroke-width="6"
+                      :show-text="false"
+                      :percentage="item.percentage"
+                    ></el-progress>
+                  </td>
+                  <td>{{ item.warnCount }}</td>
+                </tr>
+              </table>
+            </div>
+            <div class="invi-area-chart invi-chart">
+              <div class="invi-chart-title">考生分布</div>
+              <vue-charts
+                v-if="inviAreaChartOption"
+                :options="inviAreaChartOption"
+                autoresize
+              ></vue-charts>
+              <p class="chart-none" v-else>暂无数据</p>
+            </div>
+          </div>
+        </div>
+        <div class="invi-analysis-item">
+          <div class="invi-analysis-box invi-time">
+            <h2 class="invi-analysis-title">考生在线/预警时间趋势</h2>
+            <div class="invi-time-chart invi-chart">
+              <vue-charts
+                v-if="inviTimeChartOption"
+                :options="inviTimeChartOption"
+                autoresize
+              ></vue-charts>
+              <p class="chart-none" v-else>暂无数据</p>
+            </div>
+          </div>
+        </div>
+        <div class="invi-analysis-item">
+          <div class="invi-analysis-box invi-student">
+            <h2 class="invi-analysis-title">考试动态预警</h2>
+            <div class="invi-student-table">
+              <table class="invi-table">
+                <colgroup>
+                  <col width="60" />
+                  <col width="60" />
+                  <col width="140" />
+                  <col width="100" />
+                  <col width="60" />
+                </colgroup>
+                <tbody>
+                  <tr>
+                    <th>时间</th>
+                    <th>姓名</th>
+                    <th>证件号</th>
+                    <th>预警类型</th>
+                    <th>处理状态</th>
+                  </tr>
+                  <tr v-for="item in inviStudentList" :key="item.id">
+                    <td>{{ item.createTime }}</td>
+                    <td>
+                      <div class="td-cont" style="width: 60px;">
+                        {{ item.name }}
+                      </div>
+                    </td>
+                    <td>{{ item.identity }}</td>
+                    <td>
+                      <div class="td-cont" style="width: 140px;">
+                        {{ item.info }}
+                      </div>
+                    </td>
+                    <td>{{ item.approveStatus }}</td>
+                  </tr>
+                </tbody>
+              </table>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import TextClock from "../common/TextClock";
+import InvigilationStudent from "../common/InvigilationStudent";
+import VueCharts from "@/plugins/VueCharts";
+import {
+  getInviTypesOption,
+  getInviAreaOption,
+  getInviTimeOption,
+} from "./chartOpt";
+
+import {
+  students,
+  summary,
+  inviTypesList,
+  inviTimeList,
+  inviAreaList,
+  inviMapList,
+  inviStudentList,
+} from "./datas";
+
+export default {
+  name: "exam-invigilation-full",
+  components: { TextClock, InvigilationStudent, VueCharts },
+  data() {
+    return {
+      schoolName: "",
+      students: [],
+      summary: {
+        onlineCount: "",
+        waitingCount: "",
+        examingCount: "",
+        breachCount: "",
+        warnCount: "",
+        id: "",
+        name: "",
+        code: "",
+      },
+      inviMapList: [],
+      inviTypesList: [],
+      inviTypesChartOption: null,
+      inviAreaList: [],
+      inviAreaChartOption: null,
+      inviTimeList: [],
+      inviTimeChartOption: null,
+      inviStudentList: [],
+    };
+  },
+  mounted() {
+    this.students = students;
+    this.summary = summary;
+    this.inviMapList = inviMapList;
+    this.parseInviTypesList(inviTypesList);
+    this.parseInviAreaList(inviAreaList);
+    this.parseInviTimeList(inviTimeList);
+    this.inviStudentList = inviStudentList;
+  },
+  methods: {
+    videoAllMuted() {
+      this.$refs.InvigilationStudent.forEach((refInst) => {
+        refInst.mutedPlayer(true);
+      });
+    },
+    getMaxNum(nums) {
+      return Math.max.apply(null, nums);
+    },
+    parseInviTypesList(dataList) {
+      const maxNum = this.getMaxNum(dataList.map((item) => item.warnCount));
+      this.inviTypesList = dataList.map((item) => {
+        return {
+          ...item,
+          percentage: Math.floor((100 * item.warnCount) / maxNum),
+        };
+      });
+      this.inviTypesChartOption = getInviTypesOption(this.inviTypesList);
+    },
+    parseInviAreaList(dataList) {
+      const maxNum = this.getMaxNum(dataList.map((item) => item.warnCount));
+      this.inviAreaList = dataList.map((item) => {
+        return {
+          ...item,
+          percentage: Math.floor((100 * item.warnCount) / maxNum),
+        };
+      });
+      this.inviAreaChartOption = getInviAreaOption(this.inviAreaList);
+    },
+    parseInviTimeList(dataList) {
+      this.inviTimeList = dataList;
+      this.inviTimeChartOption = getInviTimeOption(dataList);
+    },
+  },
+};
+</script>

+ 197 - 0
src/features/invigilation/ExamInvigilation/chartOpt.js

@@ -0,0 +1,197 @@
+const animationIsOpen = true;
+
+export function getInviTypesOption(dataList) {
+  if (!dataList.length) return null;
+  const chartColor = [
+    "#2D99FF",
+    "#4346D3",
+    "#4D1D83",
+    "#5E2BBC",
+    "#6648FF",
+    "#F43469",
+    "#16CEB9",
+  ];
+
+  const seriesData = dataList.map((item) => {
+    return {
+      name: item.type,
+      value: item.warnCount,
+    };
+  });
+
+  return {
+    animation: animationIsOpen,
+    color: chartColor,
+    grid: {
+      top: "20%",
+      bottom: "10%",
+    },
+    tooltip: {
+      trigger: "item",
+      formatter: "{b}<br/>{d}%",
+    },
+    legend: {
+      show: false,
+    },
+    series: [
+      {
+        name: "",
+        type: "pie",
+        radius: ["55%", "85%"],
+        data: seriesData,
+        label: {
+          show: false,
+        },
+      },
+    ],
+  };
+}
+export function getInviAreaOption(dataList) {
+  if (!dataList.length) return null;
+
+  const chartColor = [
+    "#6648FF",
+    "#5E2BBC",
+    "#4D1D83",
+    "#4346D3",
+    "#2D99FF",
+    "#16CEB9",
+  ];
+
+  const seriesData = dataList.map((item) => {
+    return {
+      name: `${item.province},${item.warnCount}`,
+      value: item.warnCount,
+    };
+  });
+
+  return {
+    animation: animationIsOpen,
+    color: chartColor,
+    grid: {
+      top: "20%",
+      bottom: "10%",
+    },
+    tooltip: {
+      trigger: "item",
+      formatter: "{b}<br/>{d}%",
+    },
+    legend: {
+      show: false,
+    },
+    series: [
+      {
+        name: "占比",
+        type: "pie",
+        radius: ["55%", "85%"],
+        data: seriesData,
+        label: {
+          show: false,
+        },
+      },
+    ],
+  };
+}
+
+export function getInviTimeOption(dataList) {
+  if (!dataList.length) return;
+
+  return {
+    animation: animationIsOpen,
+    grid: {
+      show: true,
+      top: "22%",
+      bottom: "15%",
+      left: "8%",
+      right: "5%",
+      borderColor: "#292C38",
+    },
+    legend: {
+      show: true,
+      top: 17,
+      right: 20,
+      itemGap: 20,
+      itemWidth: 16,
+      itemHeight: 4,
+      icon: "roundRect",
+      textStyle: {
+        color: "rgba(225, 230, 239, 0.5)",
+        fontSize: 12,
+        padding: [0, 0, 0, 5],
+      },
+    },
+    tooltip: {
+      show: false,
+    },
+    xAxis: {
+      type: "category",
+      data: dataList.map((item) => item.hour),
+      interval: 5,
+      splitLine: {
+        show: false,
+      },
+      axisLine: {
+        lineStyle: {
+          color: "#292C38",
+        },
+      },
+      axisLabel: {
+        color: "#E1E6EF",
+        fontSize: 10,
+      },
+      axisTick: {
+        show: true,
+        inside: true,
+      },
+    },
+    yAxis: {
+      type: "value",
+      splitLine: {
+        show: true,
+        lineStyle: {
+          color: "#292C38",
+        },
+      },
+      axisLine: {
+        show: false,
+      },
+      axisLabel: {
+        fontSize: 10,
+        color: "#E1E6EF",
+      },
+      axisTick: {
+        show: false,
+      },
+    },
+    series: [
+      {
+        name: "在线",
+        type: "line",
+        smooth: true,
+        showSymbol: false,
+        data: dataList.map((item) => item.onlineCount),
+        itemStyle: {
+          color: "#16CEB9",
+        },
+        lineStyle: {
+          width: 4,
+          color: "#16CEB9",
+        },
+      },
+      {
+        name: "预警",
+        type: "line",
+        smooth: true,
+        showSymbol: false,
+        data: dataList.map((item) => item.warnCount),
+        itemStyle: {
+          color: "#F7517F",
+        },
+        lineStyle: {
+          width: 4,
+          color: "#F7517F",
+        },
+      },
+    ],
+  };
+}

+ 301 - 0
src/features/invigilation/ExamInvigilation/datas.js

@@ -0,0 +1,301 @@
+export const students = [
+  {
+    seq: null,
+    examName: "20220506",
+    examActivityCode: "1",
+    examId: "255274328714129408",
+    examActivityId: "255274329125171200",
+    examStudentId: "1522766012435914753",
+    examRecordId: "259318134736424960",
+    identity: "42011406159",
+    roomCode: "001",
+    roomName: "测试001",
+    name: "test06159",
+    courseName: "测试",
+    courseCode: "A0002",
+    paperDownload: 1,
+    status: "已待考",
+    statusCode: "FIRST_PREPARE",
+    progress: 0.0,
+    warningCount: 0,
+    clientWebsocketStatus: "OFF_LINE",
+    cameraMonitorStatusSource: null,
+    screenMonitorStatusSource: null,
+    mobileFirstMonitorStatusSource: null,
+    mobileSecondMonitorStatusSource: null,
+    clientCurrentIp: "192.168.10.136",
+    breachStatus: 1,
+    monitorLiveUrl: "oe_test_259318134736424960_client_camera",
+    updateTime: 1652767129478,
+    remainTime: "00:00:00",
+    warningNew: 0,
+    monitorVideoSource: "CLIENT_CAMERA",
+    monitorRecord: null,
+  },
+  {
+    seq: null,
+    examName: "20220506",
+    examActivityCode: "1",
+    examId: "255274328714129408",
+    examActivityId: "255274329125171200",
+    examStudentId: "1522766012473663489",
+    examRecordId: "259318142240034817",
+    identity: "42011406194",
+    roomCode: "001",
+    roomName: "测试001",
+    name: "test06194",
+    courseName: "测试",
+    courseCode: "A0002",
+    paperDownload: 1,
+    status: "已待考",
+    statusCode: "FIRST_PREPARE",
+    progress: 0.0,
+    warningCount: 0,
+    clientWebsocketStatus: "OFF_LINE",
+    cameraMonitorStatusSource: null,
+    screenMonitorStatusSource: null,
+    mobileFirstMonitorStatusSource: null,
+    mobileSecondMonitorStatusSource: null,
+    clientCurrentIp: "192.168.10.136",
+    breachStatus: 1,
+    monitorLiveUrl: "oe_test_259318142240034817_client_camera",
+    updateTime: 1652767129158,
+    remainTime: "00:00:00",
+    warningNew: 0,
+    monitorVideoSource: "CLIENT_CAMERA",
+    monitorRecord: null,
+  },
+  {
+    seq: null,
+    examName: "20220506",
+    examActivityCode: "1",
+    examId: "255274328714129408",
+    examActivityId: "255274329125171200",
+    examStudentId: "1522766012318474241",
+    examRecordId: "259318111927799808",
+    identity: "42011406050",
+    roomCode: "001",
+    roomName: "测试001",
+    name: "test06050",
+    courseName: "测试",
+    courseCode: "A0002",
+    paperDownload: 1,
+    status: "已待考",
+    statusCode: "FIRST_PREPARE",
+    progress: 0.0,
+    warningCount: 0,
+    clientWebsocketStatus: "OFF_LINE",
+    cameraMonitorStatusSource: null,
+    screenMonitorStatusSource: null,
+    mobileFirstMonitorStatusSource: null,
+    mobileSecondMonitorStatusSource: null,
+    clientCurrentIp: "192.168.10.136",
+    breachStatus: 1,
+    monitorLiveUrl: "oe_test_259318111927799808_client_camera",
+    updateTime: 1652767119001,
+    remainTime: "00:00:00",
+    warningNew: 0,
+    monitorVideoSource: "CLIENT_CAMERA",
+    monitorRecord: null,
+  },
+  {
+    seq: null,
+    examName: "20220506",
+    examActivityCode: "1",
+    examId: "255274328714129408",
+    examActivityId: "255274329125171200",
+    examStudentId: "1522766012553355267",
+    examRecordId: "259318157805096960",
+    identity: "42011406269",
+    roomCode: "001",
+    roomName: "测试001",
+    name: "test06269",
+    courseName: "测试",
+    courseCode: "A0002",
+    paperDownload: 1,
+    status: "已待考",
+    statusCode: "FIRST_PREPARE",
+    progress: 0.0,
+    warningCount: 0,
+    clientWebsocketStatus: "OFF_LINE",
+    cameraMonitorStatusSource: null,
+    screenMonitorStatusSource: null,
+    mobileFirstMonitorStatusSource: null,
+    mobileSecondMonitorStatusSource: null,
+    clientCurrentIp: "192.168.10.136",
+    breachStatus: 1,
+    monitorLiveUrl: "oe_test_259318157805096960_client_camera",
+    updateTime: 1652767140219,
+    remainTime: "00:00:00",
+    warningNew: 0,
+    monitorVideoSource: "CLIENT_CAMERA",
+    monitorRecord: null,
+  },
+];
+
+export const summary = {
+  onlineCount: 65432,
+  waitingCount: 6532,
+  examingCount: 1342,
+  breachCount: 1112,
+  warnCount: 4453,
+  id: "1",
+  name: "武汉大学",
+  code: "whdx",
+};
+
+export const inviTypesList = [
+  {
+    type: "违规动作",
+    warnCount: 320,
+  },
+  {
+    type: "替考",
+    warnCount: 98,
+  },
+  {
+    type: "求助第三方",
+    warnCount: 58,
+  },
+  {
+    type: "采用照片",
+    warnCount: 54,
+  },
+  {
+    type: "虚拟摄像头",
+    warnCount: 30,
+  },
+  {
+    type: "切出考试窗口",
+    warnCount: 17,
+  },
+  {
+    type: "雷同卷",
+    warnCount: 4,
+  },
+];
+
+let inviTimeList = [];
+(function () {
+  let pre = "08:";
+  let baset = 10;
+  for (let i = 0; i < 30; i++) {
+    inviTimeList.push({
+      hour: `${pre}${baset + i}`,
+      onlineCount: Math.floor(100 + Math.random() * 500),
+      warnCount: Math.floor(Math.random() * 100),
+    });
+  }
+})();
+
+export { inviTimeList };
+
+export const inviAreaList = [
+  {
+    country: "中国",
+    province: "河南",
+    warnCount: 1147,
+  },
+  {
+    country: "中国",
+    province: "江西",
+    warnCount: 498,
+  },
+  {
+    country: "中国",
+    province: "湖南",
+    warnCount: 461,
+  },
+  {
+    country: "中国",
+    province: "湖北",
+    warnCount: 349,
+  },
+  {
+    country: "中国",
+    province: "河北",
+    warnCount: 332,
+  },
+];
+
+export const inviMapList = [
+  {
+    country: "国家",
+    province: "省份",
+    onlineCount: 120,
+    warnCount: 10,
+  },
+];
+
+export const inviStudentList = [
+  {
+    examId: "1",
+    examStudentId: "1",
+    name: "丰玉",
+    identity: "42000198710082825",
+    info: "疑似违规动作",
+    level: "预警等级",
+    warningId: "1",
+    remark: "1",
+    examRecordId: "1",
+    roomCode: "1",
+    createTime: "17:06:12",
+    approveStatus: "已处理",
+  },
+  {
+    examId: "1",
+    examStudentId: "2",
+    name: "司莎寒",
+    identity: "42000198710082825",
+    info: "疑似替考",
+    level: "预警等级",
+    warningId: "1",
+    remark: "1",
+    examRecordId: "1",
+    roomCode: "1",
+    createTime: "17:06:12",
+    approveStatus: "已处理",
+  },
+  {
+    examId: "1",
+    examStudentId: "3",
+    name: "伦珊桂",
+    identity: "42000198710082825",
+    info: "疑似求助第三方",
+    level: "预警等级",
+    warningId: "1",
+    remark: "1",
+    examRecordId: "1",
+    roomCode: "1",
+    createTime: "14:27:56",
+    approveStatus: "已处理",
+  },
+  {
+    examId: "1",
+    examStudentId: "4",
+    name: "牛山",
+    identity: "42000198710082825",
+    info: "疑似采用照片",
+    level: "预警等级",
+    warningId: "1",
+    remark: "1",
+    examRecordId: "1",
+    roomCode: "1",
+    createTime: "17:06:12",
+    approveStatus: "已处理",
+  },
+  {
+    examId: "1",
+    examStudentId: "5",
+    name: "孙康启",
+    identity: "42000198710082825",
+    info: "疑似采用照片或虚拟电饭锅手动阀牢固树立",
+    level: "预警等级",
+    warningId: "1",
+    remark: "1",
+    examRecordId: "1",
+    roomCode: "1",
+    createTime: "09:12:50",
+    approveStatus: "已处理",
+  },
+];

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

@@ -30,7 +30,7 @@
         </div>
         <div class="student-info-detail">
           <el-button type="text" size="small" @click="toDetail"
-            >详情 <i class="el-icon-arrow-right"></i
+            >{{ detailName }} <i class="el-icon-arrow-right"></i
           ></el-button>
         </div>
       </div>
@@ -68,6 +68,10 @@ export default {
       type: Number,
       default: 3,
     },
+    detailName: {
+      type: String,
+      default: "详情",
+    },
   },
   data() {
     return {

+ 23 - 2
src/features/invigilation/common/TextClock.vue

@@ -1,6 +1,7 @@
 <template>
   <p class="text-clock">
-    <i class="icon icon-calendar-act"></i><span>{{ text }}</span>
+    <i v-if="showIcon" class="icon icon-calendar-act"></i
+    ><span>{{ text }}</span>
   </p>
 </template>
 
@@ -9,6 +10,20 @@ import { formatDate } from "@/utils/utils";
 
 export default {
   name: "text-clock",
+  props: {
+    showIcon: {
+      type: Boolean,
+      default: true,
+    },
+    showWeek: {
+      type: Boolean,
+      default: true,
+    },
+    showApm: {
+      type: Boolean,
+      default: true,
+    },
+  },
   data() {
     return {
       text: "",
@@ -22,15 +37,21 @@ export default {
   },
   methods: {
     parseDate() {
+      let infos = [];
       const now = new Date();
       const timeStr = formatDate("YYYY年M月D日_mm:ss", now).split("_");
+      infos.push(timeStr[0]);
       const weekDay = `星期${this.week[now.getDay()]}`;
+      if (this.showWeek) infos.push(weekDay);
 
       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]}`;
+      if (this.showApm) infos.push(apm);
+      infos.push(`${hour}:${timeStr[1]}`);
+
+      this.text = infos.join(" ");
     },
     start() {
       this.setT = setInterval(() => {

+ 8 - 0
src/router/index.js

@@ -194,6 +194,14 @@ const routes = [
         /* webpackChunkName: "record" */ "../features/examwork/StudentManagement/StudentMonitorRecord.vue"
       ),
   },
+  {
+    path: "/exam-invigilation-full",
+    name: "ExamInvigilationFull",
+    component: () =>
+      import(
+        /* webpackChunkName: "inspection" */ "../features/invigilation/ExamInvigilation/ExamInvigilationFull"
+      ),
+  },
   {
     path: "/invigilation",
     name: "Invigilation",

+ 307 - 0
src/styles/base.scss

@@ -1036,6 +1036,313 @@ body {
     height: auto;
   }
 }
+// invi-container
+.invi-container {
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  z-index: auto;
+  background-image: url(../assets/bg-invi-body.png);
+  background-repeat: no-repeat;
+  background-size: cover;
+  color: #fff;
+  min-height: 1080px;
+
+  .invi-header {
+    position: absolute;
+    width: 100%;
+    height: 65px;
+    top: 0;
+    left: 0;
+    z-index: 8;
+    background-image: url(../assets/bg-invi-head.png);
+    background-repeat: no-repeat;
+    background-size: 100% 100%;
+
+    .text-clock {
+      position: absolute;
+      bottom: 18px;
+      right: 20px;
+      font-size: 12px;
+    }
+  }
+  .invi-title {
+    margin: 0;
+    padding-top: 16px;
+    height: 100%;
+    font-size: 28px;
+    text-align: center;
+    line-height: 32px;
+    letter-spacing: 2px;
+    text-shadow: 0px 0px 10px rgba(45, 153, 255, 0.5);
+  }
+
+  .invi-body {
+    height: 100%;
+    padding-top: 65px;
+    overflow: hidden;
+    display: flex;
+    justify-content: space-between;
+    align-items: stretch;
+  }
+  .invi-monitor {
+    flex-shrink: 0;
+    flex-grow: 0;
+    width: 280px;
+    padding: 10px 20px;
+    &-item {
+      height: 25%;
+      padding: 10px 0;
+    }
+
+    .invigilation-student {
+      padding: 20px 20px 70px;
+      margin: 0;
+      height: 100%;
+      background: rgba(63, 67, 87, 0.3);
+      border-radius: 10px;
+      box-shadow: 0 0 2px rgba(255, 255, 255, 0.4);
+      overflow: hidden;
+      border: none;
+    }
+    .student-video {
+      margin-bottom: 0;
+      height: 100%;
+    }
+    .student-info {
+      position: absolute;
+      bottom: 20px;
+      left: 20px;
+      right: 20px;
+      z-index: 9;
+    }
+    .student-info-tips {
+      display: none;
+    }
+    .student-info-header {
+      margin-bottom: 0;
+    }
+    .student-info-content {
+      > p {
+        font-size: 12px;
+        line-height: 1;
+        font-weight: 600;
+        color: rgba(255, 255, 255, 0.5);
+      }
+      > p:nth-of-type(2) {
+        display: none;
+      }
+    }
+    .student-info-name {
+      font-size: 12px;
+      line-height: 1;
+      margin-bottom: 6px;
+      > i {
+        display: none;
+      }
+    }
+    .student-info-detail {
+      position: absolute;
+      top: 5px;
+      right: 0;
+      margin: 0;
+      z-index: 9;
+      box-shadow: inset 0px 0px 6px 0px #2d99ff;
+      border-radius: 4px;
+      border: 1px solid #2d99ff;
+      padding: 0 4px;
+      border-radius: 4px;
+      .el-button {
+        display: block;
+        line-height: 20px;
+        font-size: 12px;
+        margin-top: -1px;
+      }
+      i {
+        display: none;
+      }
+    }
+  }
+  .invi-main {
+    flex-grow: 2;
+    position: relative;
+  }
+  .invi-summary {
+    position: absolute;
+    width: 100%;
+    text-align: center;
+    z-index: 8;
+    top: 40px;
+
+    &-item {
+      display: inline-block;
+      vertical-align: top;
+      margin: 0 10px;
+      background: rgba(63, 67, 87, 0.3);
+      border-radius: 10px;
+      box-shadow: 0 0 2px rgba(255, 255, 255, 0.4);
+      overflow: hidden;
+      padding: 20px 10px;
+      width: 160px;
+
+      > h4 {
+        font-size: 12px;
+        line-height: 1;
+        margin-bottom: 8px;
+      }
+      > p {
+        font-size: 24px;
+        line-height: 1;
+        font-weight: bold;
+        text-shadow: 0px 0px 10px rgba(45, 153, 255, 0.5);
+        margin: 0;
+      }
+    }
+  }
+  .invi-analysis {
+    flex-shrink: 0;
+    flex-grow: 0;
+    width: 600px;
+    padding: 10px 20px;
+
+    &-item {
+      height: 25%;
+      padding: 10px 0;
+    }
+    &-box {
+      height: 100%;
+      background: rgba(63, 67, 87, 0.3);
+      border-radius: 10px;
+      box-shadow: 0 0 2px rgba(255, 255, 255, 0.4);
+      position: relative;
+    }
+    &-title {
+      position: absolute;
+      width: 180px;
+      height: 30px;
+      top: 8px;
+      left: 8px;
+      z-index: 8;
+      background: linear-gradient(90deg, #3f4357 0%, rgba(63, 67, 87, 0) 100%);
+      border-radius: 6px 0px 0px 6px;
+      padding: 9px 0 9px 12px;
+      margin: 0;
+      font-size: 12px;
+    }
+  }
+  .invi-types {
+    display: flex;
+    justify-content: space-between;
+    align-items: stretch;
+    padding: 8px;
+    &-table {
+      width: 50%;
+      padding-top: 50px;
+      overflow: hidden;
+    }
+    &-chart {
+      width: 50%;
+    }
+  }
+
+  .invi-area {
+    display: flex;
+    justify-content: space-between;
+    align-items: stretch;
+    padding: 8px;
+    &-table {
+      width: 50%;
+      padding-top: 50px;
+      overflow: hidden;
+    }
+    &-chart {
+      width: 50%;
+    }
+  }
+  .invi-types-chart,
+  .invi-area-chart {
+    position: relative;
+
+    .invi-chart-title {
+      position: absolute;
+      width: 100px;
+      height: 100px;
+      left: 50%;
+      top: 50%;
+      margin-top: -50px;
+      margin-left: -50px;
+      border-radius: 50%;
+      background: rgba(63, 67, 87, 0.3);
+      box-shadow: inset 0px 0px 10px 0px #3f4357;
+      font-size: 12px;
+      font-weight: 500;
+      color: rgba(225, 230, 239, 0.5);
+      line-height: 100px;
+      text-align: center;
+      letter-spacing: 1px;
+      z-index: 9;
+    }
+  }
+
+  .invi-time {
+    &-chart {
+      height: 100%;
+    }
+  }
+
+  .invi-student {
+    padding: 8px;
+
+    &-table {
+      padding-top: 50px;
+      padding-bottom: 8px;
+      overflow: hidden;
+      height: 100%;
+      td {
+        color: rgba(225, 230, 239, 1);
+      }
+
+      tr:nth-of-type(even) {
+        background: linear-gradient(
+          90deg,
+          #20242b 0%,
+          rgba(32, 36, 43, 0) 100%
+        );
+
+        td:first-child {
+          border-top-left-radius: 6px;
+          border-bottom-left-radius: 6px;
+        }
+      }
+    }
+  }
+
+  .invi-table {
+    width: 100%;
+    height: 100%;
+    border-spacing: 0;
+    border-collapse: collapse;
+    text-align: right;
+    font-size: 12px;
+    color: rgba(225, 230, 239, 0.5);
+
+    th,
+    td {
+      border: none;
+      font-weight: 400;
+      padding: 0 5px;
+    }
+    .td-cont {
+      display: inline-block;
+      width: 100%;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+  }
+}
 
 // flv-media
 .flv-media {