Michael Wang 3 жил өмнө
parent
commit
55d4351e3a

+ 3 - 0
src/features/OnlineExam/OnlineExamHome.vue

@@ -1,6 +1,7 @@
 <script setup lang="ts">
 import { httpApp } from "@/plugins/axiosApp";
 import { ExamType, OnlineExam } from "@/types/student-client";
+import { closeMediaStream } from "@/utils/camera";
 import { onMounted } from "vue";
 import OnlineExamList from "./OnlineExamList.vue";
 
@@ -12,6 +13,8 @@ let courses: OnlineExam[] = $ref([]);
 let endCourses: OnlineExam[] = $ref([]);
 
 onMounted(async () => {
+  // router.back 时关闭摄像头
+  closeMediaStream();
   logger({
     cnl: ["local", "server"],
     pgu: "AUTO",

+ 252 - 0
src/features/OnlineExam/OnlineExamOverview/OnlineExamOverview.vue

@@ -0,0 +1,252 @@
+<script setup lang="ts">
+import { httpApp } from "@/plugins/axiosApp";
+import router from "@/router";
+import { useTimers } from "@/setups/useTimers";
+import { PaperStruct } from "@/types/student-client";
+import { tryLimit } from "@/utils/tryLimit";
+import moment from "moment";
+import { onMounted } from "vue";
+import { useRoute } from "vue-router";
+
+const route = useRoute();
+const examId = route.params.examId;
+const examStudentId = route.query.examStudentId;
+
+const TOTAL_READ_TIME = 120;
+const FORCE_READ_TIME = import.meta.env.DEV ? 1 : 10;
+
+let loading = $ref(true);
+
+let beforeExamRemark = $ref("");
+let startInfo: { courseName: string } | null = $ref(null);
+let paperStruct: PaperStruct | null = $ref(null);
+let paperTotalScore = $ref(0);
+let examRecordDataId = -1;
+
+const { addInterval } = useTimers();
+let remainTime = $ref(Number.MAX_SAFE_INTEGER);
+let forceTime = $ref(Number.MAX_SAFE_INTEGER);
+let tryHowManySeconds = $ref(120);
+addInterval(() => {
+  remainTime--;
+  forceTime--;
+  tryHowManySeconds--;
+  if (remainTime <= 0) {
+    goToPaper();
+  }
+}, 1000);
+
+const remainTimeFormatted = $computed(() =>
+  moment.utc(remainTime * 1000).format("HH:mm:ss")
+);
+
+onMounted(async () => {
+  $message.destroyAll();
+  logger({ cnl: ["server"], pgn: "在线考试概览页面", act: "进入页面" });
+
+  while (tryHowManySeconds > 0) {
+    const { limitResult } = await tryLimit({
+      action: "createExam",
+      limit: 100,
+    });
+    logger({
+      cnl: ["server"],
+      pgu: "AUTO",
+      key: "开考限流API call",
+      act: "后台创建考试记录-抢名额",
+      dtl: limitResult ? "成功" : "失败",
+      ext: { tryHowManySeconds },
+    });
+    // 抢到名额后脱离循环
+    if (limitResult) break;
+  }
+  if (tryHowManySeconds <= 0) {
+    $message.warning("网络繁忙,请稍后再试!");
+    router.back();
+    return;
+  }
+
+  try {
+    const exam = await httpApp.get(
+      "/api/ecs_exam_work/exam/getExamPropertyFromCacheByStudentSession/" +
+        examId +
+        `/BEFORE_EXAM_REMARK`,
+      { "axios-retry": { retries: 4 }, noErrorMessage: true }
+    );
+    beforeExamRemark = exam.data.BEFORE_EXAM_REMARK || "";
+    const res = await httpApp.get<{
+      courseName: string;
+      examRecordDataId: number;
+    }>(
+      "/api/ecs_oe_student/examControl/startExam?examStudentId=" +
+        examStudentId,
+      { "axios-retry": { retries: 4 }, noErrorMessage: true }
+    );
+
+    // const res = { data: { examRecordDataId: 8066803, courseName: "test" } };
+
+    startInfo = res.data;
+    examRecordDataId = res.data.examRecordDataId;
+
+    const paperStructRes = await httpApp.get(
+      "/api/ecs_oe_student/examRecordPaperStruct/getExamRecordPaperStruct?examRecordDataId=" +
+        examRecordDataId,
+      { "axios-retry": { retries: 4 }, noErrorMessage: true }
+    );
+    paperStruct = paperStructRes.data;
+
+    paperTotalScore =
+      paperStruct!.defaultPaper.questionGroupList
+        .map((q) => q.groupScore)
+        .reduce((p, c) => p + c * 1000, 0) / 1000;
+
+    logger({ cnl: ["server"], pgn: "在线考试概览页面", act: "开考成功" });
+    loading = false;
+    remainTime = TOTAL_READ_TIME;
+    forceTime = FORCE_READ_TIME;
+  } catch (e) {
+    $message.warning("网络异常,请重试!");
+    logger({
+      cnl: ["server"],
+      pgn: "在线考试概览页面",
+      act: "获取考试概览信息异常",
+      possibleError: e,
+    });
+    router.back();
+    return;
+  }
+});
+function gotoPaperClicked() {
+  logger({ cnl: ["server"], pgn: "在线考试概览页面", act: "点击开始考试" });
+  goToPaper();
+}
+
+function goToPaper() {
+  void router.replace(
+    `/online-exam/exam/${examId}/examRecordData/${examRecordDataId}/order/1`
+  );
+}
+</script>
+
+<template>
+  <div v-if="!loading" id="exam-overview" class="container">
+    <div class="instructions">
+      <h1 class="tw-text-2xl">考试说明</h1>
+      <div style="text-align: left; padding-bottom: 20px; font-size: 16px">
+        <p v-html="beforeExamRemark"></p>
+        <!-- <p>{{"测试".repeat(500)}}</p> -->
+      </div>
+      <n-button
+        type="success"
+        :disabled="forceTime > 0"
+        style="display: inline-block; width: 100%"
+        @click="gotoPaperClicked"
+      >
+        接受以上条款,开始考试(倒计时:
+        <span class="animated infinite pulse"> {{ remainTimeFormatted }} </span
+        >)
+      </n-button>
+    </div>
+
+    <div class="exam-detail">
+      <h3 class="">科目:{{ startInfo?.courseName }}</h3>
+      <br />
+      <h4 class="">试卷概览(总分:{{ paperTotalScore }})</h4>
+      <br />
+      <ul class="list-group">
+        <li
+          v-for="(questionsGroup, index) in paperStruct?.defaultPaper
+            .questionGroupList"
+          :key="questionsGroup.groupName"
+          class="list-group-item"
+        >
+          {{ index + 1 }}、{{ questionsGroup.groupName }}
+          <small class="pull-right">
+            (共{{ questionsGroup.questionWrapperList.length }}题,共{{
+              questionsGroup.groupScore
+            }}分)
+          </small>
+        </li>
+      </ul>
+      <div>
+        <img style="width: 100%; padding-top: 40px" src="./good-wish.png" />
+      </div>
+    </div>
+  </div>
+
+  <div v-else class="tw-text-center tw-my-4 tw-text-lg">
+    正在等待数据返回...({{ tryHowManySeconds }}s)
+  </div>
+</template>
+
+<style scoped>
+.container {
+  display: grid;
+  grid-template-columns: 1fr 300px;
+  width: 100vw;
+  height: 100vh;
+  overflow: auto;
+}
+
+.instructions {
+  display: grid;
+  grid-template-rows: 40px minmax(200px, auto) 1fr;
+  padding: 40px 20px;
+}
+
+.exam-detail {
+  padding: 40px 20px;
+  background-color: #f5f5f5;
+
+  text-align: left;
+}
+
+.list-group {
+  list-style: none;
+}
+
+.list-group li {
+  border-bottom: 1px solid #eeeeee;
+  padding-top: 10px;
+}
+.pull-right {
+  text-align: right;
+  float: right;
+}
+
+.animated {
+  animation-duration: 1s;
+  animation-fill-mode: both;
+  display: inline-block;
+}
+
+.animated.infinite {
+  animation-iteration-count: infinite;
+}
+
+@keyframes pulse {
+  from {
+    transform: scale3d(1, 1, 1);
+  }
+
+  50% {
+    transform: scale3d(1.05, 1.05, 1.05);
+  }
+
+  to {
+    transform: scale3d(1, 1, 1);
+  }
+}
+
+.pulse {
+  color: rgb(5, 88, 88);
+  animation-name: pulse;
+}
+</style>
+
+<style>
+#exam-overview img {
+  max-width: 100%;
+  height: auto !important;
+}
+</style>

BIN
src/features/OnlineExam/OnlineExamOverview/good-wish.png


+ 3 - 0
src/plugins/axiosApp.ts

@@ -75,6 +75,7 @@ _axiosApp.interceptors.response.use(
       pgu: "AUTO",
       aul: config?.url,
       aus: error.response?.status,
+      emsg: error.response?.data?.desc,
     });
     logger({
       key: "网络请求response error",
@@ -84,6 +85,8 @@ _axiosApp.interceptors.response.use(
       aus: error.response?.status,
       lvl: "debug",
       ejn: JSON.stringify(error),
+      emsg: error.response?.data?.desc,
+      possibleError: error,
     });
     if (config?.setGlobalMask) {
       store.decreaseGlobalMaskCount("axios");

+ 10 - 0
src/plugins/axiosNoAuth.ts

@@ -48,6 +48,16 @@ _axiosNoAuth.interceptors.response.use(
       cnl: ["console", "local", "server"],
       pgu: "AUTO",
       aul: config?.url,
+      emsg: error.response?.data?.desc,
+    });
+    logger({
+      key: "网络请求response error",
+      cnl: ["console", "local", "server"],
+      pgu: "AUTO",
+      lvl: "debug",
+      aul: config?.url,
+      emsg: error.response?.data?.desc,
+      possibleError: error,
     });
     if (config?.["axios-retry"]) {
       // @ts-expect-error lastRequestTime 并没有声明在它公共的api上

+ 6 - 0
src/router/index.ts

@@ -2,6 +2,7 @@ import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
 import UserLogin from "@/features/UserLogin/UserLogin.vue";
 import MainLayout from "@/components/MainLayout/MainLayout.vue";
 import OnlineExam from "@/features/OnlineExam/OnlineExamHome.vue";
+import OnlineExamOverview from "@/features/OnlineExam/OnlineExamOverview/OnlineExamOverview.vue";
 import WelcomePage from "@/features/WelcomePage/WelcomePage.vue";
 import ChangePassword from "@/features/ChangePassword/ChangePassword.vue";
 import SiteMessage from "@/features/SiteMessage/SiteMessage.vue";
@@ -66,6 +67,11 @@ const routes: RouteRecordRaw[] = [
       },
     ],
   },
+  {
+    path: "/online-exam/exam/:examId/overview",
+    name: "OnlineExamOverview",
+    component: OnlineExamOverview,
+  },
   {
     path: "/:pathMatch(.*)*",
     name: "NotFound",

+ 5 - 1
src/utils/logger.ts

@@ -86,10 +86,14 @@ export default function createLog(detail: LogDetail) {
   } else if (!isNil(detail.possibleError)) {
     possibleErrorFields = { ejn: JSON.stringify(detail.possibleError) };
   }
-  const newDetail = Object.assign(
+
+  const newDetail: Record<string, string> = Object.assign(
     defaultFileds,
     omit(detail, "ext"),
     detail.ext,
+    detail.pgu === "AUTO"
+      ? { pgu: location.href.replace(location.origin, "") }
+      : {},
     possibleErrorFields
   );
   // FIXME: 后期设置条件开启非log级别的日志,此时全部打回。