123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415 |
- <template>
- <div class="student-track-record">
- <div v-if="!propData" class="student-track-head box-justify">
- <h2 class="student-track-title">轨迹回放</h2>
- <el-button size="mini" icon="el-icon-arrow-left" @click="goBack"
- >返回列表</el-button
- >
- </div>
- <div class="student-track-info box-justify">
- <div class="student-track-info-left">
- <i class="icon icon-theme"></i>{{ info.examName }}
- </div>
- <div class="student-track-info-list">
- <span class="student-track-info-item">
- <span>姓名:</span>
- <span class="color-primary">{{ info.name }}</span>
- </span>
- <span class="student-track-info-item">
- <span>证件号:</span>
- <span class="color-primary">{{ info.identity }}</span>
- </span>
- <span class="student-track-info-item">
- <span>科目(代码):</span>
- <span class="color-primary"
- >{{ info.courseName }}({{ info.courseCode }})</span
- >
- </span>
- <span class="student-track-info-item">
- <span>考试时间:</span>
- <span class="color-primary">{{
- info.firstStartTime | datetimeFilter
- }}</span>
- </span>
- </div>
- </div>
- <div class="student-track-body">
- <el-row :gutter="40" type="flex">
- <el-col :span="12">
- <div class="warning-track">
- <h3 class="warning-track-title">
- <i class="icon icon-track"></i>考试轨迹
- </h3>
- <div
- class="warning-track-item"
- v-for="log in examStudentLogList"
- :key="log.id"
- >
- <div
- :class="[
- 'warning-track-type',
- log.viewType === 'common' ? 'type-common' : 'type-exception',
- ]"
- >
- <i :class="['icon', `icon-track-${log.viewType}`]"></i>
- </div>
- <div class="warning-track-body">
- <div class="warning-track-info">
- <div class="warning-track-info-title">
- <template v-if="log.type === 'BREACH_HANDLE'">
- <span v-if="log.breachLogStatus">已撤销</span>
- </template>
- <h3>{{ log.title }}</h3>
- <b
- v-if="log.ipChange"
- :class="{ 'color-danger': log.ipChange }"
- >[ip变动]</b
- >
- </div>
- <template v-if="log.desc">
- <template v-if="log.type === 'MESSAGE'">
- <p v-if="log.msgType === 'AUDIO'">
- <audio
- class="qm-audio"
- :src="log.content"
- controls
- ></audio>
- </p>
- <template v-else-if="log.msgType === 'MEDIA'">
- <p v-for="(cont, index) in log.desc" :key="index">
- {{ cont }}
- </p>
- </template>
- <p v-else>{{ log.desc }}</p>
- </template>
- <p v-else>{{ log.desc }}</p>
- <p v-if="log.formUserName">
- 发送人:{{ log.formUserName }}
- </p>
- </template>
- <p v-if="log.endTime">
- 时间段:
- <span v-if="log.startTime">{{ log.startTime }} ~ </span>
- <span>{{ log.endTime }}</span>
- </p>
- <p v-if="log.durationTime">
- 持续时长约:{{ log.durationTime }}
- </p>
- <p v-if="log.ip" :class="{ 'color-danger': log.ipChange }">
- ip:{{ log.ip }},属地:{{ log.province }}{{ log.city }}
- </p>
- </div>
- <ul class="warning-track-media" v-if="log.photos">
- <li v-for="(photo, pindex) in log.photos" :key="pindex">
- <img :src="photo" @click="toViewImg(photo)" />
- </li>
- </ul>
- </div>
- </div>
- </div>
- </el-col>
- <el-col :span="12">
- <el-form inline>
- <el-form-item>
- <el-select v-model="filter.monitorRecord" placeholder="视频源">
- <el-option
- v-for="item in monitorRecordList"
- :key="item.code"
- :value="item.code"
- :label="item.name"
- >
- </el-option>
- </el-select>
- </el-form-item>
- <el-form-item>
- <el-button type="primary" @click="toPage(1)">查询</el-button>
- </el-form-item>
- </el-form>
- <table class="table">
- <tr>
- <th>录制起点</th>
- <th>录制终点</th>
- <th>操作</th>
- </tr>
- <tr v-for="item in videoRocordList" :key="item.videoUrl">
- <td>{{ item.startTime | datetimeFilter }}</td>
- <td>{{ item.endTime | datetimeFilter }}</td>
- <td class="td-link">
- <span @click="toDetail(item)">视频回放</span>
- </td>
- </tr>
- </table>
- <div class="part-page">
- <el-pagination
- background
- layout="prev, pager, next,total,sizes,jumper"
- :current-page="current"
- :total="total"
- :page-size.sync="size"
- @size-change="toPage(1)"
- @current-change="toPage"
- >
- </el-pagination>
- </div>
- </el-col>
- </el-row>
- </div>
- <!-- image-preview -->
- <simple-image-preview
- :cur-image="curImage"
- ref="SimpleImagePreview"
- ></simple-image-preview>
- <!-- StudentMonitorRecordDialog -->
- <student-monitor-record-dialog
- ref="StudentMonitorRecordDialog"
- :videoSource="curVideoSource"
- @video-ended="videoEnded"
- ></student-monitor-record-dialog>
- </div>
- </template>
- <script>
- import { VIDEO_SOURCE_TYPE } from "@/constant/constants";
- import { searchStudentTrackRecord } from "@/api/examwork-student";
- import SimpleImagePreview from "@/components/imagePreview/SimpleImagePreview";
- import StudentMonitorRecordDialog from "./StudentMonitorRecordDialog";
- import {
- formatDate,
- timeNumberToText,
- objTypeOf,
- objAssign,
- } from "@/utils/utils";
- export default {
- name: "StudentTrackRecord",
- components: { SimpleImagePreview, StudentMonitorRecordDialog },
- props: {
- propData: {
- type: Object,
- default: null,
- },
- },
- data() {
- return {
- filter: {
- examRecordId: "",
- monitorRecord: "",
- log: true,
- },
- info: {
- examId: "",
- examName: "",
- examStudentId: "",
- examRecordId: "",
- courseCode: "",
- courseName: "",
- identity: "",
- name: "",
- firstStartTime: null,
- },
- VIDEO_SOURCE_TYPE,
- monitorRecordList: [],
- videoRocordList: [],
- current: 1,
- total: 0,
- size: 24,
- examStudentLogList: [],
- curImage: { imgSrc: "" },
- curVideoSource: {},
- };
- },
- mounted() {
- let studentTrackMonitorRecord = null;
- if (this.propData) {
- this.filter.examRecordId = this.propData.examRecordId;
- studentTrackMonitorRecord = this.propData.monitorRecord;
- } else {
- this.filter.examRecordId = this.$route.params.examRecordId;
- studentTrackMonitorRecord = window.sessionStorage.getItem(
- "studentTrackMonitorRecord"
- );
- }
- if (!studentTrackMonitorRecord) {
- this.$message.error("数据丢失,请退出本页");
- return;
- }
- this.monitorRecordList = studentTrackMonitorRecord
- .split(",")
- .map((item) => {
- return {
- name: VIDEO_SOURCE_TYPE[item],
- code: item,
- };
- });
- this.filter.monitorRecord = this.monitorRecordList[0].code;
- this.initData();
- },
- methods: {
- async initData() {
- await this.getList();
- this.filter.log = false;
- },
- async getList() {
- const datas = {
- ...this.filter,
- pageNumber: this.current,
- pageSize: this.size,
- };
- const res = await searchStudentTrackRecord(datas);
- this.info = objAssign(this.info, res.data.data);
- const { records, total } =
- res.data.data.teStudentExamRecordVideoMessageIPage;
- this.videoRocordList = records;
- this.total = total;
- if (datas.log) {
- this.examStudentLogList = this.parseStudentLogs(
- res.data.data.teExamStudentLogList
- );
- }
- },
- toPage(page) {
- this.current = page;
- this.getList();
- },
- parseStudentLogs(examStudentLogList) {
- const statusTypes = {
- common: [
- "FIRST_START",
- "RESUME_START",
- "IN_PROCESS",
- "PREPARE",
- "ANSWERING",
- "BREAK_OFF",
- "RESUME_PREPARE",
- "FINISHED",
- "FIRST_PREPARE",
- ],
- warning: [
- "FACE_COUNT_ERROR",
- "FACE_COMPARE_ERROR",
- "EYE_CLOSE_ERROR",
- "LIVENESS_ACTION_ERROR",
- "REALNESS",
- "NONE",
- // exception:
- "NET_TIME_OUT",
- "MACHING_STOP",
- "NET_TIME_BREAK",
- "SOFTWARE_STOP",
- "POWER_CUT",
- "BREACH_HANDLE",
- "BREACH_REVOKE",
- "OTHER",
- ],
- };
- const transformInfo = {
- FIRST_START: "身份识别",
- ANSWERING: "进入考试",
- RESUME_START: "身份识别",
- };
- let statusTypeMap = {};
- Object.keys(statusTypes).map((k) => {
- statusTypes[k].map((item) => {
- statusTypeMap[item] = k;
- });
- });
- const dateTimeFormat = "YYYY/MM/DD HH:mm:ss";
- const logs = examStudentLogList.map((item) => {
- let info = { ...item };
- info.endTime = formatDate(dateTimeFormat, new Date(info.createTime));
- info.viewType = statusTypeMap[info.type] || "common";
- // 文字消息提示
- if (info.msgType) {
- info.type = "MESSAGE";
- info.title = info.msgTypeStr;
- if (info.msgType === "MEDIA") {
- info.endTime = null;
- info.desc = info.content.split("\r\n");
- } else {
- info.desc = info.content;
- }
- return info;
- }
- const content = info.info.split(/【|】/);
- if (content.length === 3) {
- info.title = content[1];
- info.desc = content[2];
- } else {
- info.title = transformInfo[info.type] || content[0];
- }
- if (info.title.trim() === "断点续考") {
- info.desc = info.remark;
- }
- if (info.remark && info.remark.includes('{"')) {
- info.remark = JSON.parse(info.remark);
- if (info.remark["MIN_CREATE_TIME"]) {
- info.startTime = formatDate(
- dateTimeFormat,
- new Date(info.remark["MIN_CREATE_TIME"])
- );
- info.durationTime = timeNumberToText(
- info.createTime - info.remark["MIN_CREATE_TIME"]
- );
- }
- let photos = [];
- Object.keys(info.remark).map((key) => {
- if (key.includes("PHOTO")) {
- const kPhotos =
- objTypeOf(info.remark[key]) === "array"
- ? info.remark[key]
- : [info.remark[key]];
- photos = [...photos, ...kPhotos];
- }
- });
- info.photos = photos;
- } else if (info.updateTime) {
- info.startTime = formatDate(
- dateTimeFormat,
- new Date(info.createTime)
- );
- info.endTime = formatDate(dateTimeFormat, new Date(info.updateTime));
- info.durationTime = timeNumberToText(
- info.updateTime - info.createTime
- );
- }
- // 撤销违纪的特别处理
- if (item.type === "BREACH_HANDLE" && item.title.includes('{"')) {
- const tinfo = JSON.parse(item.title);
- info.breachLogStatus = tinfo.breachLogStatus;
- }
- return info;
- });
- return logs;
- },
- toViewImg(photo) {
- this.curImage = { imgSrc: photo };
- this.$refs.SimpleImagePreview.open();
- },
- toDetail(row) {
- this.curVideoSource = row;
- this.$refs.StudentMonitorRecordDialog.open();
- },
- videoEnded(videoSource) {
- const pos = this.videoRocordList.findIndex(
- (item) => item.videoUrl === videoSource.videoUrl
- );
- if (pos >= this.videoRocordList.length - 1) return;
- this.curVideoSource = this.videoRocordList[pos + 1];
- this.$message.info("即将播放下一个视频!");
- },
- goBack() {
- window.history.go(-1);
- },
- },
- };
- </script>
|