Selaa lähdekoodia

完成环境检测

Michael Wang 3 vuotta sitten
vanhempi
commit
23aeb0344f
2 muutettua tiedostoa jossa 211 lisäystä ja 271 poistoa
  1. 210 270
      src/features/OnlineExam/CheckComputer.vue
  2. 1 1
      src/utils/nativeMethods.ts

+ 210 - 270
src/features/OnlineExam/CheckComputer.vue

@@ -1,20 +1,19 @@
 <script setup lang="ts">
 import moment from "moment";
 import VueQrcode from "@chenfengyuan/vue-qrcode";
-import { onMounted, onUnmounted } from "vue";
+import { onMounted, onUnmounted, watch } from "vue";
 import { useTimers } from "@/setups/useTimers";
-// import PulseLoader from "vue-spinner/src/PulseLoader.vue";
-// import {
-//   openWS,
-//   closeWsWithoutReconnect,
-// } from "@/features/OnlineExam/Examing/ws";
-
-// FIXME: 开启摄像头和websocket库
+import { Close, Checkmark } from "@vicons/ionicons5";
+import { closeMediaStream, getMediaStream } from "@/utils/camera";
+import { useWXSocket } from "./Examing/setups/useWXSocket";
+import { store } from "@/store/store";
+import { httpApp } from "@/plugins/axiosApp";
 
 const emit = defineEmits<{ (e: "on-close"): void }>();
 
+const { addTimeout, addInterval } = useTimers();
+
 const show = $ref(true);
-// const CLOCK_RATE_TIMEOUT = 10;
 
 let current = $ref(1);
 // @ts-expect-error chrome支持,但还有浏览器不支持,所以没进类型定义
@@ -59,18 +58,6 @@ const wechat = $ref({
   examRecordDataId: null,
 });
 
-// ...mapState([
-//   "questionQrCode",
-//   "questionQrCodeScanned",
-//   "questionAnswerFileUrl",
-// ]),
-
-// timeCurrent() {
-//   return moment(this.nowDate)
-//     .utcOffset("+08:00")
-//     .format("YYYY-MM-DD HH:mm:ssZZ");
-// },
-
 const step1Status = $computed(() => {
   return network.downlinkStatus && network.rrtStatus;
 });
@@ -99,113 +86,158 @@ const step5Status = $computed(() => {
   return wechat.qrScanned && wechat.uploadStatus;
 });
 
-// watch(
-//   () => questionQrCodeScanned,
-//   () => {
-//     this.wechat.qrScanned = true;
-//     this.wechat.qrScannedResolved = true;
-//   }
-// );
-// watch(
-//   () => questionAnswerFileUrl,
-//   (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;
-//             this.wechat.uploadResolved = true;
-//             this.wechat.uploadStatus = true;
-//             q.saved = true;
-//             if (acknowledgeStatus === "CONFIRMED")
-//               this.$Message.info({
-//                 content: "小程序作答已更新",
-//                 duration: 5,
-//                 closable: true,
-//               });
-//           })
-//           .catch(() => {
-//             this.$Message.error({
-//               content: "更新小程序答案失败!",
-//               duration: 15,
-//               closable: true,
-//             });
-//           });
-//       }
-//     }
-//   }
-// );
-
+//#region 时钟处理
 let nowDate: number = $ref(0);
-const { addInterval } = useTimers();
 addInterval(() => (nowDate = Date.now()), 1000);
 console.log(nowDate);
-
+const CLOCK_RATE_TIMEOUT = 10;
+let start: moment.Moment, end: moment.Moment;
+void fetch("/oe-web/", { method: "HEAD" }).then((e) => {
+  start = moment(e.headers.get("date"));
+});
+addTimeout(() => {
+  void fetch("/oe-web/", { method: "HEAD" }).then((e) => {
+    // 可能已经离开这个页面了
+    if (isUnmounted) return;
+    end = moment(e.headers.get("date"));
+    time.clockRateStateResolved = true;
+    // @ts-expect-error 不方便给clockRateDiff定义 number | null
+    time.clockRateDiff = end.diff(start, "seconds") - CLOCK_RATE_TIMEOUT;
+    time.clockRateStatus = end.diff(start, "seconds") < CLOCK_RATE_TIMEOUT + 2;
+  });
+}, CLOCK_RATE_TIMEOUT * 1000);
+
+let isUnmounted = false;
+onUnmounted(() => (isUnmounted = true));
+//#endregion 时钟处理
+
+//#region websocket
+// 初始化wxSocket的前提
+store.exam.WEIXIN_ANSWER_ENABLED = true;
+useWXSocket();
 onMounted(async () => {
-  // openWS({});
-  // const fetchQR = async () => {
-  //   const examRecordDataId = this.$store.state.user.id;
-  //   const response = await this.$http.post(
-  //     "/api/ecs_oe_student/examControl/getQrCode",
-  //     {
-  //       examRecordDataId,
-  //       order: 1,
-  //       transferFileType: "AUDIO",
-  //       testEnv: true,
-  //     }
-  //   );
-  //   this.wechat.qrValue = response.data;
-  //   const trueExamRecordDataId = decodeURIComponent(response.data).match(
-  //     /&examRecordDataId=(\d+)/
-  //   )[1];
-  //   this.wechat.examRecordDataId = trueExamRecordDataId;
-  // };
-  // if (this.wechat.qrValue) {
-  //   this.getQRCodeTimeout = setTimeout(() => {
-  //     fetchQR();
-  //   }, 3000);
-  // }
-  // let start, end;
-  // fetch("/oe-web/login", { Method: "HEAD" }).then((e) => {
-  //   start = moment(e.headers.get("date"));
-  // });
-  // this.checkClockRateTimeout = setTimeout(() => {
-  //   fetch("/oe-web/login", { Method: "HEAD" }).then((e) => {
-  //     // 可能已经离开这个页面了
-  //     if (!this.time) return;
-  //     end = moment(e.headers.get("date"));
-  //     this.time.clockRateStateResolved = true;
-  //     this.time.clockRateDiff = end.diff(start, "seconds") - CLOCK_RATE_TIMEOUT;
-  //     this.time.clockRateStatus =
-  //       end.diff(start, "seconds") < CLOCK_RATE_TIMEOUT + 2;
-  //   });
-  // }, CLOCK_RATE_TIMEOUT * 1000);
-  // await this.openCamera();
+  const examRecordDataId = store.user.id;
+  const response = await httpApp.post(
+    "/api/ecs_oe_student/examControl/getQrCode",
+    {
+      examRecordDataId,
+      order: 1,
+      transferFileType: "AUDIO",
+      testEnv: true,
+    }
+  );
+  wechat.qrValue = response.data;
+  const trueExamRecordDataId = decodeURIComponent(wechat.qrValue).match(
+    /&examRecordDataId=(\d+)/
+  )![1];
+  // @ts-expect-error
+  wechat.examRecordDataId = trueExamRecordDataId;
 });
+// websocket 会在unmounted时自动关闭
 
+watch(
+  () => store.exam.questionQrCodeScanned,
+  () => {
+    wechat.qrScanned = true;
+    wechat.qrScannedResolved = true;
+  }
+);
+watch(
+  () => store.exam.questionAnswerFileUrl,
+  (value) => {
+    const examRecordDataId = wechat.examRecordDataId;
+    for (const q of value) {
+      if (!q.saved) {
+        let acknowledgeStatus = "CONFIRMED";
+
+        httpApp
+          .post(
+            "/api/ecs_oe_student/examControl/saveUploadedFileAcknowledgeStatus",
+            {
+              examRecordDataId,
+              filePath: q.fileUrl,
+              order: q.order,
+              acknowledgeStatus,
+            }
+          )
+          .then(() => {
+            // @ts-expect-error
+            wechat.studentAnswer = q.fileUrl;
+            wechat.uploadResolved = true;
+            wechat.uploadStatus = true;
+            q.saved = true;
+            if (acknowledgeStatus === "CONFIRMED")
+              $message.info("小程序作答已更新");
+          })
+          .catch(() => {
+            $message.error("更新小程序答案失败!");
+          });
+      }
+    }
+  }
+);
+//#endregion websocket
+
+//#region 摄像头处理
+onMounted(async () => {
+  await openCamera();
+});
+const video: HTMLVideoElement = $ref();
+async function openCamera() {
+  const stream = await getMediaStream();
+  video.srcObject = stream;
+  try {
+    await video.play();
+    camera.openCameraStatus = true;
+  } catch (error) {
+    if (error instanceof Error) {
+      if (error.name == "AbortError") {
+        logger({
+          cnl: ["server"],
+          pgu: "AUTO",
+          act: "video.paly",
+          dtl: "AbortError and retry",
+        });
+        await video.play();
+        logger({
+          cnl: ["server"],
+          pgu: "AUTO",
+          act: "摄像头没有正常启用: AbortError 重试成功",
+        });
+      } else if (error.name == "NotSupportedError") {
+        logger({
+          cnl: ["server"],
+          act: "摄像头没有正常启用",
+          pgu: "AUTO",
+          ejn: JSON.stringify(error),
+          ext: {
+            errorName: error.name,
+            errorMessage: error.message,
+            errorStack: error.stack,
+          },
+        });
+        $message.error("摄像头没有正常启用: " + error);
+      } else {
+        throw error;
+      }
+    } else {
+      logger({
+        cnl: ["server"],
+        pgu: "AUTO",
+        act: "video.play",
+        dtl: "not an Error",
+        stk: error + "",
+      });
+    }
+    throw error;
+  } finally {
+    camera.openCameraResolved = true;
+  }
+}
 onUnmounted(() => {
-  // clearTimeout(this.checkClockRateTimeout);
-  // clearTimeout(this.getQRCodeTimeout);
-  // if (this.$refs.video.srcObject) {
-  //   this.$refs.video.srcObject.getTracks().forEach(function (track) {
-  //     track.stop();
-  //   });
-  //   this.$refs.video.srcObject = null;
-  // }
-  // closeWsWithoutReconnect();
+  closeMediaStream();
 });
+//#endregion 摄像头处理
 
 function previous() {
   if (current > 1) {
@@ -217,96 +249,19 @@ function next() {
     current += 1;
   }
   if (current === 6) {
-    // window._hmt.push([
-    //   "_trackEvent",
-    //   "环境检测",
-    //   `网络: ${this.step1Status}; 时间: ${this.step2Status}; 摄像头: ${this.step3Status}; 声音: ${this.step4Status}; 小程序: ${this.step5Status};`,
-    // ]);
+    logger({
+      cnl: ["server"],
+      act: "环境检测",
+      ext: {
+        network: step1Status,
+        time: step2Status,
+        camera: step3Status,
+        sound: step4Status,
+        wechat: step5Status,
+      },
+    });
   }
 }
-
-// async function openCamera() {
-// const video = this.$refs.video;
-// if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
-//   try {
-//     console.log("启动摄像头");
-//     const stream = await navigator.mediaDevices.getUserMedia({
-//       video: {
-//         facingMode: "user",
-//         resizeMode: "crop-and-scale",
-//         width: 400,
-//         height: 300,
-//       },
-//     });
-//     if (stream) {
-//       video.srcObject = stream;
-//       try {
-//         await video.play();
-//         this.camera.openCameraStatus = true;
-//       } catch (error) {
-//         console.log("摄像头没有正常启用", error);
-//         this.$Message.error({
-//           content: "摄像头没有正常启用: " + error,
-//           duration: 15,
-//           closable: true,
-//         });
-//         window._hmt.push([
-//           "_trackEvent",
-//           "摄像头框-环境检测",
-//           "摄像头状态",
-//           "摄像头没有正常启用: " + error,
-//         ]);
-//       }
-//     } else {
-//       this.$Message.error({
-//         content: "没有可用的视频流",
-//         duration: 15,
-//         closable: true,
-//       });
-//       window._hmt.push([
-//         "_trackEvent",
-//         "摄像头框-环境检测",
-//         "摄像头状态",
-//         "没有可用的视频流",
-//       ]);
-//     }
-//   } catch (error) {
-//     console.log("无法启用摄像头", error);
-//     let errMsg;
-//     if (error.name || error.message) {
-//       errMsg = `${error.name} ${error.message}`;
-//     } else {
-//       errMsg = error;
-//     }
-//     this.$Message.error({
-//       content: "无法启用摄像头: " + errMsg,
-//       duration: 15,
-//       closable: true,
-//     });
-//     window._hmt.push([
-//       "_trackEvent",
-//       "摄像头框-环境检测",
-//       "摄像头状态",
-//       "无法启用摄像头" + errMsg,
-//     ]);
-//   } finally {
-//     this.camera.openCameraResolved = true;
-//   }
-// } else {
-//   this.$Message.error({
-//     content: "没有找到可用的摄像头",
-//     duration: 15,
-//     closable: true,
-//   });
-//   window._hmt.push([
-//     "_trackEvent",
-//     "摄像头框-环境检测",
-//     "摄像头状态",
-//     "没有找到可用的摄像头",
-//   ]);
-// }
-// this.camera.openCameraResolved = true;
-// }
 </script>
 
 <template>
@@ -342,13 +297,13 @@ function next() {
                 <td>{{ network.downlink }}Mb</td>
                 <td>
                   <div v-if="network.downlinkStatus">
-                    <n-icon class="pass-check" type="md-checkmark" />
+                    <n-icon class="pass-check" :component="Checkmark" />
                   </div>
                   <div v-else>
                     <n-icon
                       class="fail-cross"
                       title="下载速度不佳"
-                      type="md-close"
+                      :component="Close"
                     />
                   </div>
                 </td>
@@ -358,13 +313,13 @@ function next() {
                 <td>{{ network.rrt }}毫秒</td>
                 <td>
                   <div v-if="network.rrtStatus">
-                    <n-icon class="pass-check" type="md-checkmark" />
+                    <n-icon class="pass-check" :component="Checkmark" />
                   </div>
                   <div v-else>
                     <n-icon
                       class="fail-cross"
                       title="网络延迟较大"
-                      type="md-close"
+                      :component="Close"
                     />
                   </div>
                 </td>
@@ -384,27 +339,18 @@ function next() {
                 <td>状态</td>
               </tr>
 
-              <!-- <tr>
-              <td>电脑当前时间</td>
-              <td>{{ timeCurrent }} (北京时间)</td>
-              <td></td>
-            </tr> -->
-              <!-- <tr>
-              <td>电脑是否时间准确</td>
-              <td>{{ timeDifference }}</td>
-            </tr> -->
               <tr>
                 <td>电脑时区</td>
                 <td>{{ time.currentTimeZone }}</td>
                 <td>
                   <div v-if="time.timeZoneStatus">
-                    <n-icon class="pass-check" type="md-checkmark" />
+                    <n-icon class="pass-check" :component="Checkmark" />
                   </div>
                   <div v-else>
                     <n-icon
                       class="fail-cross"
                       title="请将电脑设置为北京时区"
-                      type="md-close"
+                      :component="Close"
                     />
                   </div>
                 </td>
@@ -416,7 +362,7 @@ function next() {
                     {{ (time.clockRateDiff ?? 0) > 3 ? "时钟过慢" : "正常" }}
                   </div>
                   <div v-else>
-                    <n-spin size="medium" />
+                    <n-spin size="small" />
                   </div>
                 </td>
                 <td>
@@ -424,17 +370,17 @@ function next() {
                     <n-icon
                       v-if="time.clockRateStatus"
                       class="pass-check"
-                      type="md-checkmark"
+                      :component="Checkmark"
                     />
                     <n-icon
                       v-else
                       class="fail-cross"
                       title="请更换电脑"
-                      type="md-close"
+                      :component="Close"
                     />
                   </div>
                   <div v-else>
-                    <n-spin size="medium" />
+                    <n-spin size="small" />
                   </div>
                 </td>
               </tr>
@@ -446,13 +392,7 @@ function next() {
       <div v-show="current === 3" key="3" class="section">
         <div>
           <div style="display: flex">
-            <video
-              id="video"
-              ref="video"
-              width="400"
-              height="300"
-              autoplay
-            ></video>
+            <video id="video" ref="video" width="400" height="300" autoplay />
 
             <div
               v-if="camera.openCameraResolved && camera.openCameraStatus"
@@ -497,24 +437,24 @@ function next() {
                     {{ camera.openCameraStatus ? "正常" : "请检查摄像头" }}
                   </div>
                   <div v-else>
-                    <n-spin size="medium" />
+                    <n-spin size="small" />
                   </div>
                 </td>
                 <td>
                   <div v-if="camera.openCameraResolved">
                     <div v-if="camera.openCameraStatus">
-                      <n-icon class="pass-check" type="md-checkmark" />
+                      <n-icon class="pass-check" :component="Checkmark" />
                     </div>
                     <div v-else>
                       <n-icon
                         class="fail-cross"
                         title="请检查摄像头"
-                        type="md-close"
+                        :component="Close"
                       />
                     </div>
                   </div>
                   <div v-else>
-                    <n-spin size="medium" />
+                    <n-spin size="small" />
                   </div>
                 </td>
               </tr>
@@ -530,7 +470,7 @@ function next() {
                     {{ camera.identityStatus ? "正常" : "请检查摄像头" }}
                   </div>
                   <div v-else>
-                    <n-spin size="medium" />
+                    <n-spin size="small" />
                   </div>
                 </td>
                 <td>
@@ -541,18 +481,18 @@ function next() {
                     "
                   >
                     <div v-if="camera.identityStatus">
-                      <n-icon class="pass-check" type="md-checkmark" />
+                      <n-icon class="pass-check" :component="Checkmark" />
                     </div>
                     <div v-else>
                       <n-icon
                         class="fail-cross"
                         title="请检查摄像头"
-                        type="md-close"
+                        :component="Close"
                       />
                     </div>
                   </div>
                   <div v-else>
-                    <n-spin size="medium" />
+                    <n-spin size="small" />
                   </div>
                 </td>
               </tr>
@@ -624,24 +564,24 @@ function next() {
                     {{ sound.downloadStatus ? "正常" : "出错" }}
                   </div>
                   <div v-else>
-                    <n-spin size="medium" />
+                    <n-spin size="small" />
                   </div>
                 </td>
                 <td>
                   <div v-if="sound.downloadResolved">
                     <div v-if="sound.downloadStatus">
-                      <n-icon class="pass-check" type="md-checkmark" />
+                      <n-icon class="pass-check" :component="Checkmark" />
                     </div>
                     <div v-else>
                       <n-icon
                         class="fail-cross"
                         title="下载出错"
-                        type="md-close"
+                        :component="Close"
                       />
                     </div>
                   </div>
                   <div v-else>
-                    <n-spin size="medium" />
+                    <n-spin size="small" />
                   </div>
                 </td>
               </tr>
@@ -652,24 +592,24 @@ function next() {
                     {{ sound.playedStatus ? "正常" : "出错" }}
                   </div>
                   <div v-else>
-                    <n-spin size="medium" />
+                    <n-spin size="small" />
                   </div>
                 </td>
                 <td>
                   <div v-if="sound.playedStatusResolved">
                     <div v-if="sound.playedStatus">
-                      <n-icon class="pass-check" type="md-checkmark" />
+                      <n-icon class="pass-check" :component="Checkmark" />
                     </div>
                     <div v-else>
                       <n-icon
                         class="fail-cross"
                         title="不能播放声音"
-                        type="md-close"
+                        :component="Close"
                       />
                     </div>
                   </div>
                   <div v-else>
-                    <n-spin size="medium" />
+                    <n-spin size="small" />
                   </div>
                 </td>
               </tr>
@@ -699,7 +639,7 @@ function next() {
                     style="margin-top: 30px; font-size: 30px"
                   >
                     {{ wechat.studentAnswer ? "已上传" : "已扫描" }}
-                    <n-icon type="md-checkmark" />
+                    <n-icon :component="Checkmark" />
                   </div>
                 </div>
               </div>
@@ -763,24 +703,24 @@ function next() {
                     {{ wechat.qrScanned ? "正常" : "出错" }}
                   </div>
                   <div v-else>
-                    <n-spin size="medium" />
+                    <n-spin size="small" />
                   </div>
                 </td>
                 <td>
                   <div v-if="wechat.qrScannedResolved">
                     <div v-if="wechat.qrScanned">
-                      <n-icon class="pass-check" type="md-checkmark" />
+                      <n-icon class="pass-check" :component="Checkmark" />
                     </div>
                     <div v-else>
                       <n-icon
                         class="fail-cross"
                         title="扫描出错"
-                        type="md-close"
+                        :component="Close"
                       />
                     </div>
                   </div>
                   <div v-else>
-                    <n-spin size="medium" />
+                    <n-spin size="small" />
                   </div>
                 </td>
               </tr>
@@ -792,20 +732,20 @@ function next() {
                     {{ wechat.uploadStatus ? "正常" : "出错" }}
                   </div>
                   <div v-else>
-                    <n-spin size="medium" />
+                    <n-spin size="small" />
                   </div>
                 </td>
                 <td>
                   <div v-if="wechat.uploadResolved">
                     <div v-if="wechat.uploadStatus">
-                      <n-icon class="pass-check" type="md-checkmark" />
+                      <n-icon class="pass-check" :component="Checkmark" />
                     </div>
                     <div v-else>
-                      <n-icon class="fail-cross" type="md-close" />
+                      <n-icon class="fail-cross" :component="Close" />
                     </div>
                   </div>
                   <div v-else>
-                    <n-spin size="medium" />
+                    <n-spin size="small" />
                   </div>
                 </td>
               </tr>
@@ -827,10 +767,10 @@ function next() {
                 <td>网速</td>
                 <td>
                   <div v-if="step1Status">
-                    <n-icon class="pass-check" type="md-checkmark" />
+                    <n-icon class="pass-check" :component="Checkmark" />
                   </div>
                   <div v-else>
-                    <n-icon class="fail-cross" type="md-close" />
+                    <n-icon class="fail-cross" :component="Close" />
                   </div>
                 </td>
               </tr>
@@ -839,14 +779,14 @@ function next() {
                 <td>
                   <div v-if="step2StatusResolved">
                     <div v-if="step2Status">
-                      <n-icon class="pass-check" type="md-checkmark" />
+                      <n-icon class="pass-check" :component="Checkmark" />
                     </div>
                     <div v-else>
-                      <n-icon class="fail-cross" type="md-close" />
+                      <n-icon class="fail-cross" :component="Close" />
                     </div>
                   </div>
                   <div v-else>
-                    <n-spin size="medium" />
+                    <n-spin size="small" />
                   </div>
                 </td>
               </tr>
@@ -855,10 +795,10 @@ function next() {
                 <td>
                   <div v-if="step3StatusResolved">
                     <div v-if="step3Status">
-                      <n-icon class="pass-check" type="md-checkmark" />
+                      <n-icon class="pass-check" :component="Checkmark" />
                     </div>
                     <div v-else>
-                      <n-icon class="fail-cross" type="md-close" />
+                      <n-icon class="fail-cross" :component="Close" />
                     </div>
                   </div>
                   <div v-else class="fail-cross">
@@ -871,10 +811,10 @@ function next() {
                 <td>
                   <div v-if="step4StatusResolved">
                     <div v-if="step4Status">
-                      <n-icon class="pass-check" type="md-checkmark" />
+                      <n-icon class="pass-check" :component="Checkmark" />
                     </div>
                     <div v-else>
-                      <n-icon class="fail-cross" type="md-close" />
+                      <n-icon class="fail-cross" :component="Close" />
                     </div>
                   </div>
                   <div v-else class="fail-cross">
@@ -887,10 +827,10 @@ function next() {
                 <td>
                   <div v-if="step5StatusResolved">
                     <div v-if="step5Status">
-                      <n-icon class="pass-check" type="md-checkmark" />
+                      <n-icon class="pass-check" :component="Checkmark" />
                     </div>
                     <div v-else>
-                      <n-icon class="fail-cross" type="md-close" />
+                      <n-icon class="fail-cross" :component="Close" />
                     </div>
                   </div>
                   <div v-else class="fail-cross">

+ 1 - 1
src/utils/nativeMethods.ts

@@ -84,7 +84,7 @@ export function execLocal(exeName: string): Promise<void> {
 
 /** 文件路径是否存在 */
 export function fileExists(file: string): boolean {
-  // FIXME
+  // FIXME: 在苹果电脑跳过检测
   if (navigator.userAgent.includes("Macintosh")) {
     return true;
   }