|
@@ -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>
|