StudentTrackRecord.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. <template>
  2. <div class="student-track-record">
  3. <div v-if="!propData" class="student-track-head box-justify">
  4. <h2 class="student-track-title">轨迹回放</h2>
  5. <el-button size="mini" icon="el-icon-arrow-left" @click="goBack"
  6. >返回列表</el-button
  7. >
  8. </div>
  9. <div class="student-track-info box-justify">
  10. <div class="student-track-info-left">
  11. <i class="icon icon-theme"></i>{{ info.examName }}
  12. </div>
  13. <div class="student-track-info-list">
  14. <span class="student-track-info-item">
  15. <span>姓名:</span>
  16. <span class="color-primary">{{ info.name }}</span>
  17. </span>
  18. <span class="student-track-info-item">
  19. <span>证件号:</span>
  20. <span class="color-primary">{{ info.identity }}</span>
  21. </span>
  22. <span class="student-track-info-item">
  23. <span>科目(代码):</span>
  24. <span class="color-primary"
  25. >{{ info.courseName }}({{ info.courseCode }})</span
  26. >
  27. </span>
  28. <span class="student-track-info-item">
  29. <span>考试时间:</span>
  30. <span class="color-primary">{{
  31. info.firstStartTime | datetimeFilter
  32. }}</span>
  33. </span>
  34. </div>
  35. </div>
  36. <div class="student-track-body">
  37. <el-row :gutter="40" type="flex">
  38. <el-col :span="12">
  39. <div class="warning-track">
  40. <h3 class="warning-track-title">
  41. <i class="icon icon-track"></i>考试轨迹
  42. </h3>
  43. <div
  44. class="warning-track-item"
  45. v-for="log in examStudentLogList"
  46. :key="log.id"
  47. >
  48. <div
  49. :class="[
  50. 'warning-track-type',
  51. log.viewType === 'common' ? 'type-common' : 'type-exception',
  52. ]"
  53. >
  54. <i :class="['icon', `icon-track-${log.viewType}`]"></i>
  55. </div>
  56. <div class="warning-track-body">
  57. <div class="warning-track-info">
  58. <div class="warning-track-info-title">
  59. <template v-if="log.type === 'BREACH_HANDLE'">
  60. <span v-if="log.breachLogStatus">已撤销</span>
  61. </template>
  62. <h3>{{ log.title }}</h3>
  63. <b
  64. v-if="log.ipChange"
  65. :class="{ 'color-danger': log.ipChange }"
  66. >[ip变动]</b
  67. >
  68. </div>
  69. <template v-if="log.desc">
  70. <template v-if="log.type === 'MESSAGE'">
  71. <p v-if="log.msgType === 'AUDIO'">
  72. <audio
  73. class="qm-audio"
  74. :src="log.content"
  75. controls
  76. ></audio>
  77. </p>
  78. <template v-else-if="log.msgType === 'MEDIA'">
  79. <p v-for="(cont, index) in log.desc" :key="index">
  80. {{ cont }}
  81. </p>
  82. </template>
  83. <p v-else>{{ log.desc }}</p>
  84. </template>
  85. <p v-else>{{ log.desc }}</p>
  86. <p v-if="log.formUserName">
  87. 发送人:{{ log.formUserName }}
  88. </p>
  89. </template>
  90. <p v-if="log.endTime">
  91. 时间段:
  92. <span v-if="log.startTime">{{ log.startTime }} ~ </span>
  93. <span>{{ log.endTime }}</span>
  94. </p>
  95. <p v-if="log.durationTime">
  96. 持续时长约:{{ log.durationTime }}
  97. </p>
  98. <p v-if="log.ip" :class="{ 'color-danger': log.ipChange }">
  99. ip:{{ log.ip }},属地:{{ log.province }}{{ log.city }}
  100. </p>
  101. </div>
  102. <ul class="warning-track-media" v-if="log.photos">
  103. <li v-for="(photo, pindex) in log.photos" :key="pindex">
  104. <img :src="photo" @click="toViewImg(photo)" />
  105. </li>
  106. </ul>
  107. </div>
  108. </div>
  109. </div>
  110. </el-col>
  111. <el-col :span="12">
  112. <el-form inline>
  113. <el-form-item>
  114. <el-select v-model="filter.monitorRecord" placeholder="视频源">
  115. <el-option
  116. v-for="item in monitorRecordList"
  117. :key="item.code"
  118. :value="item.code"
  119. :label="item.name"
  120. >
  121. </el-option>
  122. </el-select>
  123. </el-form-item>
  124. <el-form-item>
  125. <el-button type="primary" @click="toPage(1)">查询</el-button>
  126. </el-form-item>
  127. </el-form>
  128. <table class="table">
  129. <tr>
  130. <th>录制起点</th>
  131. <th>录制终点</th>
  132. <th>操作</th>
  133. </tr>
  134. <tr v-for="item in videoRocordList" :key="item.videoUrl">
  135. <td>{{ item.startTime | datetimeFilter }}</td>
  136. <td>{{ item.endTime | datetimeFilter }}</td>
  137. <td class="td-link">
  138. <span @click="toDetail(item)">视频回放</span>
  139. </td>
  140. </tr>
  141. </table>
  142. <div class="part-page">
  143. <el-pagination
  144. background
  145. layout="prev, pager, next,total,sizes,jumper"
  146. :current-page="current"
  147. :total="total"
  148. :page-size.sync="size"
  149. @size-change="toPage(1)"
  150. @current-change="toPage"
  151. >
  152. </el-pagination>
  153. </div>
  154. </el-col>
  155. </el-row>
  156. </div>
  157. <!-- image-preview -->
  158. <simple-image-preview
  159. :cur-image="curImage"
  160. ref="SimpleImagePreview"
  161. ></simple-image-preview>
  162. <!-- StudentMonitorRecordDialog -->
  163. <student-monitor-record-dialog
  164. ref="StudentMonitorRecordDialog"
  165. :videoSource="curVideoSource"
  166. @video-ended="videoEnded"
  167. ></student-monitor-record-dialog>
  168. </div>
  169. </template>
  170. <script>
  171. import { VIDEO_SOURCE_TYPE } from "@/constant/constants";
  172. import { searchStudentTrackRecord } from "@/api/examwork-student";
  173. import SimpleImagePreview from "@/components/imagePreview/SimpleImagePreview";
  174. import StudentMonitorRecordDialog from "./StudentMonitorRecordDialog";
  175. import {
  176. formatDate,
  177. timeNumberToText,
  178. objTypeOf,
  179. objAssign,
  180. } from "@/utils/utils";
  181. export default {
  182. name: "StudentTrackRecord",
  183. components: { SimpleImagePreview, StudentMonitorRecordDialog },
  184. props: {
  185. propData: {
  186. type: Object,
  187. default: null,
  188. },
  189. },
  190. data() {
  191. return {
  192. filter: {
  193. examRecordId: "",
  194. monitorRecord: "",
  195. log: true,
  196. },
  197. info: {
  198. examId: "",
  199. examName: "",
  200. examStudentId: "",
  201. examRecordId: "",
  202. courseCode: "",
  203. courseName: "",
  204. identity: "",
  205. name: "",
  206. firstStartTime: null,
  207. },
  208. VIDEO_SOURCE_TYPE,
  209. monitorRecordList: [],
  210. videoRocordList: [],
  211. current: 1,
  212. total: 0,
  213. size: 24,
  214. examStudentLogList: [],
  215. curImage: { imgSrc: "" },
  216. curVideoSource: {},
  217. };
  218. },
  219. mounted() {
  220. let studentTrackMonitorRecord = null;
  221. if (this.propData) {
  222. this.filter.examRecordId = this.propData.examRecordId;
  223. studentTrackMonitorRecord = this.propData.monitorRecord;
  224. } else {
  225. this.filter.examRecordId = this.$route.params.examRecordId;
  226. studentTrackMonitorRecord = window.sessionStorage.getItem(
  227. "studentTrackMonitorRecord"
  228. );
  229. }
  230. if (!studentTrackMonitorRecord) {
  231. this.$message.error("数据丢失,请退出本页");
  232. return;
  233. }
  234. this.monitorRecordList = studentTrackMonitorRecord
  235. .split(",")
  236. .map((item) => {
  237. return {
  238. name: VIDEO_SOURCE_TYPE[item],
  239. code: item,
  240. };
  241. });
  242. this.filter.monitorRecord = this.monitorRecordList[0].code;
  243. this.initData();
  244. },
  245. methods: {
  246. async initData() {
  247. await this.getList();
  248. this.filter.log = false;
  249. },
  250. async getList() {
  251. const datas = {
  252. ...this.filter,
  253. pageNumber: this.current,
  254. pageSize: this.size,
  255. };
  256. const res = await searchStudentTrackRecord(datas);
  257. this.info = objAssign(this.info, res.data.data);
  258. const { records, total } =
  259. res.data.data.teStudentExamRecordVideoMessageIPage;
  260. this.videoRocordList = records;
  261. this.total = total;
  262. if (datas.log) {
  263. this.examStudentLogList = this.parseStudentLogs(
  264. res.data.data.teExamStudentLogList
  265. );
  266. }
  267. },
  268. toPage(page) {
  269. this.current = page;
  270. this.getList();
  271. },
  272. parseStudentLogs(examStudentLogList) {
  273. const statusTypes = {
  274. common: [
  275. "FIRST_START",
  276. "RESUME_START",
  277. "IN_PROCESS",
  278. "PREPARE",
  279. "ANSWERING",
  280. "BREAK_OFF",
  281. "RESUME_PREPARE",
  282. "FINISHED",
  283. "FIRST_PREPARE",
  284. ],
  285. warning: [
  286. "FACE_COUNT_ERROR",
  287. "FACE_COMPARE_ERROR",
  288. "EYE_CLOSE_ERROR",
  289. "LIVENESS_ACTION_ERROR",
  290. "REALNESS",
  291. "NONE",
  292. // exception:
  293. "NET_TIME_OUT",
  294. "MACHING_STOP",
  295. "NET_TIME_BREAK",
  296. "SOFTWARE_STOP",
  297. "POWER_CUT",
  298. "BREACH_HANDLE",
  299. "BREACH_REVOKE",
  300. "OTHER",
  301. ],
  302. };
  303. const transformInfo = {
  304. FIRST_START: "身份识别",
  305. ANSWERING: "进入考试",
  306. RESUME_START: "身份识别",
  307. };
  308. let statusTypeMap = {};
  309. Object.keys(statusTypes).map((k) => {
  310. statusTypes[k].map((item) => {
  311. statusTypeMap[item] = k;
  312. });
  313. });
  314. const dateTimeFormat = "YYYY/MM/DD HH:mm:ss";
  315. const logs = examStudentLogList.map((item) => {
  316. let info = { ...item };
  317. info.endTime = formatDate(dateTimeFormat, new Date(info.createTime));
  318. info.viewType = statusTypeMap[info.type] || "common";
  319. // 文字消息提示
  320. if (info.msgType) {
  321. info.type = "MESSAGE";
  322. info.title = info.msgTypeStr;
  323. if (info.msgType === "MEDIA") {
  324. info.endTime = null;
  325. info.desc = info.content.split("\r\n");
  326. } else {
  327. info.desc = info.content;
  328. }
  329. return info;
  330. }
  331. const content = info.info.split(/【|】/);
  332. if (content.length === 3) {
  333. info.title = content[1];
  334. info.desc = content[2];
  335. } else {
  336. info.title = transformInfo[info.type] || content[0];
  337. }
  338. if (info.title.trim() === "断点续考") {
  339. info.desc = info.remark;
  340. }
  341. if (info.remark && info.remark.includes('{"')) {
  342. info.remark = JSON.parse(info.remark);
  343. if (info.remark["MIN_CREATE_TIME"]) {
  344. info.startTime = formatDate(
  345. dateTimeFormat,
  346. new Date(info.remark["MIN_CREATE_TIME"])
  347. );
  348. info.durationTime = timeNumberToText(
  349. info.createTime - info.remark["MIN_CREATE_TIME"]
  350. );
  351. }
  352. let photos = [];
  353. Object.keys(info.remark).map((key) => {
  354. if (key.includes("PHOTO")) {
  355. const kPhotos =
  356. objTypeOf(info.remark[key]) === "array"
  357. ? info.remark[key]
  358. : [info.remark[key]];
  359. photos = [...photos, ...kPhotos];
  360. }
  361. });
  362. info.photos = photos;
  363. } else if (info.updateTime) {
  364. info.startTime = formatDate(
  365. dateTimeFormat,
  366. new Date(info.createTime)
  367. );
  368. info.endTime = formatDate(dateTimeFormat, new Date(info.updateTime));
  369. info.durationTime = timeNumberToText(
  370. info.updateTime - info.createTime
  371. );
  372. }
  373. // 撤销违纪的特别处理
  374. if (item.type === "BREACH_HANDLE" && item.title.includes('{"')) {
  375. const tinfo = JSON.parse(item.title);
  376. info.breachLogStatus = tinfo.breachLogStatus;
  377. }
  378. return info;
  379. });
  380. return logs;
  381. },
  382. toViewImg(photo) {
  383. this.curImage = { imgSrc: photo };
  384. this.$refs.SimpleImagePreview.open();
  385. },
  386. toDetail(row) {
  387. this.curVideoSource = row;
  388. this.$refs.StudentMonitorRecordDialog.open();
  389. },
  390. videoEnded(videoSource) {
  391. const pos = this.videoRocordList.findIndex(
  392. (item) => item.videoUrl === videoSource.videoUrl
  393. );
  394. if (pos >= this.videoRocordList.length - 1) return;
  395. this.curVideoSource = this.videoRocordList[pos + 1];
  396. this.$message.info("即将播放下一个视频!");
  397. },
  398. goBack() {
  399. window.history.go(-1);
  400. },
  401. },
  402. };
  403. </script>