|
@@ -0,0 +1,572 @@
|
|
|
+<template>
|
|
|
+ <div style="margin: 30px; text-align: left;">
|
|
|
+ <Steps :current="current" size="small">
|
|
|
+ <Step title="时钟"></Step>
|
|
|
+ <Step title="网速"></Step>
|
|
|
+ <Step title="摄像头"></Step>
|
|
|
+ <Step title="声音"></Step>
|
|
|
+ <Step title="微信小程序"></Step>
|
|
|
+ <Step title="检测结果"></Step>
|
|
|
+ </Steps>
|
|
|
+
|
|
|
+ <div v-if="current === 0" class="section">
|
|
|
+ <div class="list">
|
|
|
+ <table>
|
|
|
+ <tbody class="list-row">
|
|
|
+ <tr class="list-header qm-primary-strong-text">
|
|
|
+ <td class="first-td">检查项</td>
|
|
|
+ <td>结果</td>
|
|
|
+ </tr>
|
|
|
+
|
|
|
+ <tr>
|
|
|
+ <td>电脑当前时间</td>
|
|
|
+ <td>{{ timeCurrent }} (北京时间)</td>
|
|
|
+ </tr>
|
|
|
+ <!-- <tr>
|
|
|
+ <td>电脑是否时间准确</td>
|
|
|
+ <td>{{ timeDifference }}</td>
|
|
|
+ </tr> -->
|
|
|
+ <tr>
|
|
|
+ <td>电脑时区</td>
|
|
|
+ <td>{{ timeZone }}</td>
|
|
|
+ </tr>
|
|
|
+ <tr>
|
|
|
+ <td>电脑时钟频率</td>
|
|
|
+ <td>{{ clockRateResult }}</td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="current === 1" class="section">
|
|
|
+ <div class="list">
|
|
|
+ <table>
|
|
|
+ <tbody class="list-row">
|
|
|
+ <tr class="list-header qm-primary-strong-text">
|
|
|
+ <td class="first-td">检查项</td>
|
|
|
+ <td>结果</td>
|
|
|
+ </tr>
|
|
|
+
|
|
|
+ <tr>
|
|
|
+ <td>电脑当前下载速度</td>
|
|
|
+ <td>{{ network.downlink }}</td>
|
|
|
+ </tr>
|
|
|
+ <tr>
|
|
|
+ <td>电脑当前网络延迟</td>
|
|
|
+ <td>{{ network.rrt }}</td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-show="current === 2" class="section" style="text-align: center">
|
|
|
+ <div>
|
|
|
+ <div style="display: flex;">
|
|
|
+ <video
|
|
|
+ id="video"
|
|
|
+ ref="video"
|
|
|
+ :width="400"
|
|
|
+ :height="300"
|
|
|
+ autoplay
|
|
|
+ ></video>
|
|
|
+
|
|
|
+ <div style="margin-left: 50px; margin-top: 100px;">
|
|
|
+ <Button type="warning" @click="camera = '没有摄像头'">
|
|
|
+ 摄像头没有启用
|
|
|
+ </Button>
|
|
|
+ <div style="width: 30px; height: 30px;"></div>
|
|
|
+ <Button type="primary" @click="camera = 'OK'">
|
|
|
+ 摄像头正常工作
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="list">
|
|
|
+ <table>
|
|
|
+ <tbody class="list-row">
|
|
|
+ <tr class="list-header qm-primary-strong-text">
|
|
|
+ <td class="first-td">检查项</td>
|
|
|
+ <td>结果</td>
|
|
|
+ </tr>
|
|
|
+
|
|
|
+ <tr>
|
|
|
+ <td>摄像头没有启用</td>
|
|
|
+ <td>{{ camera }}</td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-show="current === 3" class="section" style="text-align: center">
|
|
|
+ <div>
|
|
|
+ <div style="display: flex; margin-bottom: 30px;">
|
|
|
+ <audio
|
|
|
+ src="https://ecs-static.qmth.com.cn/check-audio.mp3"
|
|
|
+ controls
|
|
|
+ nodownload
|
|
|
+ />
|
|
|
+
|
|
|
+ <div style="margin-left: 30px; display: flex; ">
|
|
|
+ <Button
|
|
|
+ type="warning"
|
|
|
+ @click="sound = '没有声音'"
|
|
|
+ title="或者听不到声音"
|
|
|
+ >
|
|
|
+ 不能播放声音
|
|
|
+ </Button>
|
|
|
+ <div style="width: 30px; height: 30px;"></div>
|
|
|
+ <Button type="primary" @click="sound = 'OK'">
|
|
|
+ 能够播放声音
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="list">
|
|
|
+ <table>
|
|
|
+ <tbody class="list-row">
|
|
|
+ <tr class="list-header qm-primary-strong-text">
|
|
|
+ <td class="first-td">检查项</td>
|
|
|
+ <td>结果</td>
|
|
|
+ </tr>
|
|
|
+
|
|
|
+ <tr>
|
|
|
+ <td>声音</td>
|
|
|
+ <td>{{ sound }}</td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-show="current === 4" class="section">
|
|
|
+ <div>
|
|
|
+ <div style="display: flex;">
|
|
|
+ <div>
|
|
|
+ <div v-if="wechat.qrValue" style="display: flex">
|
|
|
+ <qrcode
|
|
|
+ :value="wechat.qrValue"
|
|
|
+ :options="{ width: 200 }"
|
|
|
+ style="margin-left: -10px;"
|
|
|
+ ></qrcode>
|
|
|
+ <div style="margin-top: 10px;">
|
|
|
+ <div>
|
|
|
+ 请使用<span style="font-weight: 900; color: #1E90FF;"
|
|
|
+ >微信</span
|
|
|
+ >扫描二维码后,在微信小程序上录音,并上传文件。
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ v-if="wechat.qrScanned"
|
|
|
+ style="margin-top: 30px; font-size: 30px;"
|
|
|
+ >
|
|
|
+ {{ this.wechat.studentAnswer ? "已上传" : "已扫描" }}
|
|
|
+ <Icon type="md-checkmark" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-else>正在获取二维码...</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div
|
|
|
+ class="audio-answer audio-answer-line-height"
|
|
|
+ style="margin-top: 20px ;"
|
|
|
+ >
|
|
|
+ <span class="audio-answer-line-height">答案:</span>
|
|
|
+ <audio
|
|
|
+ class="audio-answer-line-height"
|
|
|
+ v-if="this.wechat.studentAnswer"
|
|
|
+ controls
|
|
|
+ controlsList="nodownload"
|
|
|
+ :src="this.wechat.studentAnswer"
|
|
|
+ />
|
|
|
+ <span v-else class="audio-answer-line-height">未上传文件</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div style=" margin-top: 30px; display: flex; margin-bottom: 30px;">
|
|
|
+ <Button
|
|
|
+ type="warning"
|
|
|
+ @click="wechat.upload = '不能正确扫描'"
|
|
|
+ title="或者上传不成功"
|
|
|
+ >
|
|
|
+ 不能正确扫描,或者上传不成功
|
|
|
+ </Button>
|
|
|
+ <div style="width: 30px; height: 30px;"></div>
|
|
|
+ <Button type="primary" @click="wechat.upload = 'OK'">
|
|
|
+ 能够扫描,并上传成功
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="list">
|
|
|
+ <table>
|
|
|
+ <tbody class="list-row">
|
|
|
+ <tr class="list-header qm-primary-strong-text">
|
|
|
+ <td class="first-td">检查项</td>
|
|
|
+ <td>结果</td>
|
|
|
+ </tr>
|
|
|
+
|
|
|
+ <tr>
|
|
|
+ <td>扫描二维码并上传录音</td>
|
|
|
+ <td>{{ wechat.upload }}</td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-show="current === 5" class="section" style="text-align: center">
|
|
|
+ <div class="list">
|
|
|
+ <table>
|
|
|
+ <tbody class="list-row">
|
|
|
+ <tr class="list-header qm-primary-strong-text">
|
|
|
+ <td class="first-td">检查项</td>
|
|
|
+ <td>结果</td>
|
|
|
+ </tr>
|
|
|
+
|
|
|
+ <tr>
|
|
|
+ <td>时钟</td>
|
|
|
+ <td>{{ step1Status }}</td>
|
|
|
+ </tr>
|
|
|
+ <tr>
|
|
|
+ <td>网速</td>
|
|
|
+ <td>{{ step2Status }}</td>
|
|
|
+ </tr>
|
|
|
+ <tr>
|
|
|
+ <td>摄像头</td>
|
|
|
+ <td>{{ step3Status }}</td>
|
|
|
+ </tr>
|
|
|
+ <tr>
|
|
|
+ <td>声音</td>
|
|
|
+ <td>{{ step4Status }}</td>
|
|
|
+ </tr>
|
|
|
+ <tr>
|
|
|
+ <td>微信小程序</td>
|
|
|
+ <td>{{ step5Status }}</td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div style="margin-top: 30px; text-align: center;">
|
|
|
+ <Button type="primary" @click="previous" :disabled="current === 0">
|
|
|
+ 上一步
|
|
|
+ </Button>
|
|
|
+ <div style="width: 30px; height: 1px; display: inline-block;"></div>
|
|
|
+ <Button type="primary" @click="next" :disabled="current === 5">
|
|
|
+ 下一步
|
|
|
+ </Button>
|
|
|
+ <div style="width: 30px; height: 1px; display: inline-block;"></div>
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ @click="() => this.$router.back()"
|
|
|
+ v-if="current === 5"
|
|
|
+ >
|
|
|
+ 返回考试列表
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import moment from "moment";
|
|
|
+import VueQrcode from "@chenfengyuan/vue-qrcode";
|
|
|
+import {
|
|
|
+ openWS,
|
|
|
+ closeWsWithoutReconnect,
|
|
|
+ getQRCode,
|
|
|
+} from "@/features/OnlineExam/Examing/ws";
|
|
|
+import { createNamespacedHelpers } from "vuex";
|
|
|
+const { mapState } = createNamespacedHelpers("examingHomeModule");
|
|
|
+
|
|
|
+const CLOCK_RATE_TIMEOUT = 10;
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: "check-computer",
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ current: 0,
|
|
|
+ // status: ["process", "wait", "wait", "wait", "wait"],
|
|
|
+ nowDate: Date.now(),
|
|
|
+ timeDifference: 0,
|
|
|
+ timeZone:
|
|
|
+ new Date().getTimezoneOffset() / 60 === -8
|
|
|
+ ? "OK"
|
|
|
+ : "请将电脑设置为北京时区",
|
|
|
+ clockRateResult: "检测中",
|
|
|
+ network: {
|
|
|
+ downlink:
|
|
|
+ navigator.connection.downlink > 0.5
|
|
|
+ ? "OK"
|
|
|
+ : `下载速度不佳: ${navigator.connection.downlink}Mb`,
|
|
|
+ rrt:
|
|
|
+ navigator.connection.rtt < 1000
|
|
|
+ ? "OK"
|
|
|
+ : `网络延迟较大${navigator.connection.rtt}`,
|
|
|
+ },
|
|
|
+ camera: "待确认",
|
|
|
+ sound: "待确认",
|
|
|
+ wechat: {
|
|
|
+ qrValue: " ",
|
|
|
+ upload: "待确认",
|
|
|
+ qrScanned: false,
|
|
|
+ studentAnswer: null,
|
|
|
+ examRecordDataId: null,
|
|
|
+ },
|
|
|
+ };
|
|
|
+ },
|
|
|
+ async created() {
|
|
|
+ this.getNowInterval = setInterval(() => {
|
|
|
+ this.nowDate = Date.now();
|
|
|
+ }, 1000);
|
|
|
+ openWS({ examRecordDataId: this.$store.state.user.id });
|
|
|
+ if (!getQRCode(1, "AUDIO", { testEnv: true })) {
|
|
|
+ setTimeout(() => {
|
|
|
+ getQRCode(1, "AUDIO", { testEnv: true });
|
|
|
+ }, 3000);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async mounted() {
|
|
|
+ let start, end;
|
|
|
+ const networkStart = performance.now();
|
|
|
+ fetch("/oe/login", { Method: "HEAD" }).then(e => {
|
|
|
+ start = moment(e.headers.get("date"));
|
|
|
+ const networkEnd = performance.now();
|
|
|
+
|
|
|
+ this.timeDifference =
|
|
|
+ Math.abs(moment().diff(start) - (networkEnd - networkStart) / 2) <
|
|
|
+ 60 * 1000
|
|
|
+ ? "OK"
|
|
|
+ : "与服务器时间差异超过60秒";
|
|
|
+ });
|
|
|
+ this.checkClockRateTimeout = setTimeout(() => {
|
|
|
+ fetch("/oe/login", { Method: "HEAD" }).then(e => {
|
|
|
+ end = moment(e.headers.get("date"));
|
|
|
+ this.clockRateResult =
|
|
|
+ end.diff(start, "seconds") < CLOCK_RATE_TIMEOUT + 2
|
|
|
+ ? "OK"
|
|
|
+ : "时钟过慢";
|
|
|
+ });
|
|
|
+ }, CLOCK_RATE_TIMEOUT * 1000);
|
|
|
+
|
|
|
+ await this.openCamera();
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ clearInterval(this.getNowInterval);
|
|
|
+ clearTimeout(this.checkClockRateTimeout);
|
|
|
+ closeWsWithoutReconnect();
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ previous() {
|
|
|
+ if (this.current >= 0) {
|
|
|
+ this.current -= 1;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ next() {
|
|
|
+ if (this.current < 5) {
|
|
|
+ this.current += 1;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async openCamera() {
|
|
|
+ const video = this.$refs.video;
|
|
|
+ if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
|
+ try {
|
|
|
+ console.log("启动摄像头");
|
|
|
+ const stream = await navigator.mediaDevices.getUserMedia({
|
|
|
+ video: {
|
|
|
+ facingMode: "user",
|
|
|
+ // width: 400,
|
|
|
+ // height: this.showRecognizeButton ? 300 : 250
|
|
|
+ },
|
|
|
+ });
|
|
|
+ if (stream) {
|
|
|
+ video.srcObject = stream;
|
|
|
+ try {
|
|
|
+ await video.play();
|
|
|
+ } catch (error) {
|
|
|
+ this.$Message.error({
|
|
|
+ content: "摄像头没有正常启用",
|
|
|
+ duration: 15,
|
|
|
+ closable: true,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.$Message.error({
|
|
|
+ content: "没有可用的视频流",
|
|
|
+ duration: 15,
|
|
|
+ closable: true,
|
|
|
+ });
|
|
|
+ window._hmt.push([
|
|
|
+ "_trackEvent",
|
|
|
+ "摄像头框",
|
|
|
+ "摄像头状态",
|
|
|
+ "没有可用的视频流",
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ this.$Message.error({
|
|
|
+ content: "无法启用摄像头",
|
|
|
+ duration: 15,
|
|
|
+ closable: true,
|
|
|
+ });
|
|
|
+ window._hmt.push([
|
|
|
+ "_trackEvent",
|
|
|
+ "摄像头框",
|
|
|
+ "摄像头状态",
|
|
|
+ "无法启用摄像头",
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.$Message.error({
|
|
|
+ content: "没有找到可用的摄像头",
|
|
|
+ duration: 15,
|
|
|
+ closable: true,
|
|
|
+ });
|
|
|
+ window._hmt.push([
|
|
|
+ "_trackEvent",
|
|
|
+ "摄像头框",
|
|
|
+ "摄像头状态",
|
|
|
+ "没有找到可用的摄像头",
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ ...mapState([
|
|
|
+ "questionQrCode",
|
|
|
+ "questionQrCodeScanned",
|
|
|
+ "questionAnswerFileUrl",
|
|
|
+ ]),
|
|
|
+ timeCurrent() {
|
|
|
+ return moment(this.nowDate)
|
|
|
+ .utcOffset("+08:00")
|
|
|
+ .format("YYYY-MM-DD HH:mm:ssZZ");
|
|
|
+ },
|
|
|
+ step1Status() {
|
|
|
+ // if (this.current === 0) {
|
|
|
+ // return "process";
|
|
|
+ // }
|
|
|
+ // return this.timeDifference === "OK" &&
|
|
|
+ return this.timeZone === "OK" && this.clockRateResult === "OK"
|
|
|
+ ? "OK"
|
|
|
+ : "错误";
|
|
|
+ },
|
|
|
+ step2Status() {
|
|
|
+ // if (this.current <= 1) {
|
|
|
+ // return "process";
|
|
|
+ // }
|
|
|
+ return this.network.downlink === "OK" && this.network.rrt === "OK"
|
|
|
+ ? "OK"
|
|
|
+ : "错误";
|
|
|
+ },
|
|
|
+ step3Status() {
|
|
|
+ // if (this.current <= 2) {
|
|
|
+ // return "process";
|
|
|
+ // }
|
|
|
+ // return this.camera === "OK" ? "OK" : "错误";
|
|
|
+ return this.camera;
|
|
|
+ },
|
|
|
+ step4Status() {
|
|
|
+ // if (this.current <= 3) {
|
|
|
+ // return "process";
|
|
|
+ // }
|
|
|
+ // return this.sound === "OK" ? "OK" : "错误";
|
|
|
+ return this.sound;
|
|
|
+ },
|
|
|
+ step5Status() {
|
|
|
+ // if (this.current <= 4) {
|
|
|
+ // return "process";
|
|
|
+ // }
|
|
|
+ // return this.wechat.upload === "OK" ? "OK" : "错误";
|
|
|
+ return this.wechat.upload;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ questionQrCode(value) {
|
|
|
+ // console.log(this.examQuestion.studentAnswer);
|
|
|
+ // console.log("watch", value);
|
|
|
+ this.wechat.qrValue = value.qrCode;
|
|
|
+ this.wechat.examRecordDataId = decodeURIComponent(value.qrCode).match(
|
|
|
+ /&examRecordDataId=(\d+)&/
|
|
|
+ )[1];
|
|
|
+ },
|
|
|
+ questionQrCodeScanned() {
|
|
|
+ // console.log(this.examQuestion.studentAnswer);
|
|
|
+ // console.log("watch", value);
|
|
|
+ this.wechat.qrScanned = true;
|
|
|
+ },
|
|
|
+ questionAnswerFileUrl(value) {
|
|
|
+ // console.log(this.examQuestion.studentAnswer);
|
|
|
+ // console.log("watch", value);
|
|
|
+ const examRecordDataId = this.wechat.examRecordDataId;
|
|
|
+ for (const q of value) {
|
|
|
+ if (!q.saved) {
|
|
|
+ let acknowledgeStatus = "CONFIRMED";
|
|
|
+
|
|
|
+ this.$http
|
|
|
+ .post(
|
|
|
+ "/api/ecs_oe_student/examControl/saveUploadedFileAcknowledgeStatus",
|
|
|
+ {
|
|
|
+ examRecordDataId,
|
|
|
+ filePath: q.fileUrl,
|
|
|
+ order: q.order,
|
|
|
+ acknowledgeStatus,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ .then(() => {
|
|
|
+ this.wechat.studentAnswer = q.fileUrl;
|
|
|
+ q.saved = true;
|
|
|
+ if (acknowledgeStatus === "CONFIRMED")
|
|
|
+ this.$Message.info({
|
|
|
+ content: "小程序作答已更新",
|
|
|
+ duration: 5,
|
|
|
+ closable: true,
|
|
|
+ });
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ this.$Message.error({
|
|
|
+ content: "更新小程序答案失败!",
|
|
|
+ duration: 15,
|
|
|
+ closable: true,
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+ components: {
|
|
|
+ qrcode: VueQrcode,
|
|
|
+ },
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.section {
|
|
|
+ margin-top: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.list {
|
|
|
+ border: 1px solid #eeeeee;
|
|
|
+ border-radius: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.list table {
|
|
|
+ width: 100%;
|
|
|
+ border-collapse: collapse !important;
|
|
|
+ border-spacing: 0;
|
|
|
+}
|
|
|
+.list td {
|
|
|
+ border: 1px solid #eeeeee;
|
|
|
+ border-radius: 6px;
|
|
|
+ border-collapse: separate !important;
|
|
|
+ padding: 10px;
|
|
|
+}
|
|
|
+.list .first-td {
|
|
|
+ width: 50%;
|
|
|
+}
|
|
|
+</style>
|