<template> <el-row :gutter="20"> <el-col :span="6"> <el-card> <template #header> <div> <span>考生信息</span> </div> </template> <p v-for="(userItem, userIndex) in userInfo" :key="userIndex"> <template v-if="userItem.type === 'datetimerange'"> <div> <span>{{ userItem.label }}:</span> <span>{{ _.join(userData[userItem.prop], "——") }}</span> </div> </template> <template v-else-if="userItem.type === 'text'"> <div> <span>{{ userItem.label }}:</span> <span>{{ userData[userItem.prop] }}</span> </div> </template> <template v-else-if="userItem.type === 'img'"> <div> <span>{{ userItem.label }}:</span> <el-image style="width: 20px; height: 20px;" :src="userData[userItem.prop]" v-if="_.trim(userData[userItem.prop])" fit="fill" :preview-src-list="[userData[userItem.prop]]" preview-teleported> </el-image> <span v-else></span> </div> </template> </p> <template #footer> <el-button type="primary" @click="connectTeacher">联系老师</el-button> <el-button type="primary" @click="startExam">考前检测</el-button> <el-button type="primary" @click="startCheckExam">开始考试</el-button> <el-button type="primary" @click="stopCheckExam">结束考试</el-button> </template> </el-card> </el-col> <el-col :span="6"> <el-card> <template #header> <div> <span>实时作弊信息</span> <!-- <el-button type="primary" @click="getzuobiList">获取作弊信息</el-button> --> </div> </template> <p v-for="(zuobiItem, zuobiIndex) in zuobiPList" :key="zuobiIndex"> <span> {{ zuobiItem.label }} </span>: <span> {{ (zuobiObj[zuobiItem.prop] || []).length }} </span> </p> </el-card> <el-card> <template #header> <div> <span>作弊饼状图</span> <!-- <el-button type="primary" @click="getzuobiList">获取作弊信息</el-button> --> </div> </template> <div id="studentZuobiEcharts"></div> </el-card> </el-col> <el-col :span="12"> <video ref="videoEL" class="canvasClass" playsinline></video> </el-col> </el-row> </template> <script> import * as echarts from 'echarts'; import _ from "lodash"; import { getUser, //获取user信息 startCheck,//开始检测 stopCheck,//停止检测 getzuobi,//获取作弊信息 } from "@/api/student"; import { ElMessage, ElMessageBox } from 'element-plus' import io from 'socket.io-client'; export default { name: "student", components: {}, data() { return { _: _, userId: "", // 学生姓名、学号,考试类型、考试科目、考试时间段 userInfo: [ { label: "学校名称", prop: "xuexiaomingcheng", type: "text" }, { label: "学校代号", prop: "xuexiaodaihao", type: "text" }, { label: "专业名称", prop: "zhuanyemingcheng", type: "text" }, { label: "专业代号", prop: "zhuanyedaihao", type: "text" }, { label: "年级", prop: "nianji", type: "text" }, { label: "班级", prop: "banji", type: "text" }, { label: "学生姓名", prop: "xueshengxingming", type: "text" }, { label: "学号", prop: "xuehao", type: "text" }, { label: "考生图片", prop: "kaoshengtupian", type: "img" }, { label: "考试类型", prop: "kaoshileixing", type: "text" }, { label: "考试科目", prop: "kaoshikemu", type: "text" }, { label: "考试时间段", prop: "kaoshishijianduan", type: "datetimerange" }, ], userData: {}, socket: null, zuobiPList: [ { label: "提问次数", prop: "qustion" }, { label: "回答次数", prop: "answer" }, { label: "讲话次数", prop: "talk" }, { label: "举手次数", prop: "jushou" } ], zuobiList: [], zuobiObj: {}, zuobiInterval: null, studentEcharts: null, }; }, watch: {}, computed: {}, async mounted() { this.userId = _.get(this.$route, ["params", "id"], "") await this.getUser() await this.initWebSocket(); let echartsDom = document.getElementById('studentZuobiEcharts'); this.studentEcharts = echarts.init(echartsDom) this.zuobiInterval = setInterval(async () => { await this.getzuobiList() }, 3000); }, methods: { async getUser() { let res = await getUser({ id: this.userId }) this.userData = res.list[0] }, async initWebSocket() { this.socket = io('http://localhost:5000/ws/video', { query: { id: this.userId, isAdmin: "0" } }); // 替换为你的后端服务器地址 this.socket.on('connect', () => { console.log('Connected to server'); }); console.log(78444, `studentMsg${this.userId}`) this.socket.on(`studentMsg${this.userId}`, (data) => { console.log(78875454477, data) let msg = _.get(data, ['data'], "") let answer = _.split(msg, "@@@") ElMessage({ message: `老师回答:${answer[1]}`, type: "success", }); // 在这里处理接收到的学生端谈话信息 }); }, async connectTeacher() { ElMessageBox.prompt(``, `提出疑问`, { confirmButtonText: '发送', cancelButtonText: '取消' }) .then(({ value }) => { this.socket.emit('sendMsg', { userId: this.userId, data: value }); ElMessage({ type: 'success', message: `已提出疑问:${value}`, }) }) .catch(() => { }) }, // 开始检测 async startCheckExam() { startCheck() }, // 考试结束 async stopCheckExam() { stopCheck() this.stopVideoStream() this.closeWebSocket() }, async startExam() { let that = this if ('webkitSpeechRecognition' in window) { console.log("Speech Recognition Supported"); // 创建一个新的SpeechRecognition对象 let recognition = new webkitSpeechRecognition(); // 设置语言,例如中文('zh-CN') recognition.lang = 'zh-CN'; // 是否连续听写,默认为false, 连续为true recognition.continuous = false; // 是否在结果中包含中间的识别结果,默认为false recognition.interimResults = false; // 开始监听语音输入 recognition.start(); // 当有语音识别结果时触发此事件 recognition.onresult = function (event) { let result = event.results[event.results.length - 1][0].transcript; console.log('用户说了: ' + result); ElMessage({ message: `您正在说说:${result}`, type: "error", }); that.socket.emit('talk', { userId: that.userId, data: result }); // 在这里处理识别到的文字,比如显示在页面上或执行其他操作 }; // 如果语音识别服务遇到错误,会触发此事件 recognition.onerror = function (event) { console.error('语音识别错误:', event.error); }; // 用户结束说话后停止监听 recognition.onend = function () { console.log('语音识别已停止'); // 可根据需要决定是否重新开始监听 recognition.start(); }; } try { let device = {} let devices = await navigator.mediaDevices.enumerateDevices() for (let key in devices) { if (devices[key].kind === 'videoinput') { device = devices[key] break } } let stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: { sourceId: device.deviceId, // 把对应的 摄像头ID 放到这里 width: 600, height: 600, } }) // 摄像头开启成功 this.$refs['videoEL'].srcObject = stream this.$refs['videoEL'].play() this.sendVideoFrames(); } catch (error) { ElMessage.error(`摄像头开启失败,请检查摄像头是否可用!${error}`) } }, sendVideoFrames() { let that = this let video = this.$refs['videoEL']; let canvas = document.createElement('canvas'); let ctx = canvas.getContext('2d'); canvas.width = video.offsetWidth; canvas.height = video.offsetHeight; let sendFrame = () => { ctx.drawImage(video, 0, 0, canvas.width, canvas.height); let dataURL = canvas.toDataURL('image/jpeg', 0.3); that.socket.emit('video', { userId: this.userId, data: dataURL }); }; setInterval(sendFrame, 1000 / 30); // 30 FPS }, stopVideoStream() { if (this.videoStream) { this.videoStream.getTracks().forEach(track => track.stop()); } }, closeWebSocket() { if (this.socket && this.socket.connected) { this.socket.disconnect(); } }, // 获取作弊信息 async getzuobiList() { let res = await getzuobi() this.zuobiList = res.list this.zuobiObj = _.groupBy(this.zuobiList, 'type') console.log(78444, this.zuobiObj) let dataLocal = [] for (let key in this.zuobiObj) { let value = this.zuobiObj[key] dataLocal.push({ value: value.length, name: key }) } let option = { tooltip: { trigger: 'item' }, legend: { top: '1%', left: 'center' }, series: [ { name: '作弊次数', type: 'pie', radius: ['40%', '70%'], avoidLabelOverlap: false, itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 }, label: { show: false, position: 'center' }, emphasis: { label: { show: true, fontSize: 40, fontWeight: 'bold' } }, labelLine: { show: false }, data: dataLocal } ] }; this.studentEcharts.setOption(option) } }, beforeUnmount() { this.stopVideoStream(); this.closeWebSocket(); clearInterval(this.zuobiInterval) }, }; </script> <style scoped> .canvasClass { position: relative; width: 600px; height: 600px; } #studentZuobiEcharts { width: 400px; height: 400px; } </style>