123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 |
- <script setup lang="ts">
- import OnlineExamFaceCheckModal from "./OnlineExamFaceCheckModal.vue";
- import CommittmentDialog from "./CommittmentDialog.vue";
- import { OnlineExam } from "@/types/student-client";
- import CheckComputer from "./CheckComputer.vue";
- import { onMounted, onUnmounted, watch } from "vue";
- import { httpApp } from "@/plugins/axiosApp";
- import router from "@/router";
- import { checkExamInProgress } from "../UserLogin/useExamInProgress";
- import { store } from "@/store/store";
- import { closeMediaStream } from "@/utils/camera";
- import { getKey } from "@/utils/utils";
- import { AxiosError } from "axios";
- import { doDec, doEnc } from "@/utils/encDec";
- const { course } = defineProps<{ course: OnlineExam }>();
- const emit = defineEmits<{ (e: "on-unselect-course"): void }>();
- /** 采用状态机,根据每一步的状态来执行初始化,以及跳转
- * 驱动步骤向前分两类:
- * 1. 自动跳转在watch里面
- * 2. 用户事件跳转,通过组件注册事件
- */
- const STEPS = [
- "COMMITTMENT",
- "CHECK_ENV_1",
- "CHECK_ENV_2",
- "CHECK_FACE",
- "CHECK_FACE_COMMITTMENT",
- ] as const;
- let curentStep = $ref(-1);
- // 为了页面在跳过此步骤是不出现此步骤的页面,所以加上这个变量来控制modal.show
- let showCheckEnv1 = $ref(false);
- watch(
- () => curentStep,
- async (step, oldStep) => {
- logger({
- cnl: ["server", "console"],
- act: "StartExamModal",
- ext: {
- currentStep: STEPS[step],
- oldStep: STEPS[oldStep],
- },
- });
- if (STEPS[step] === "COMMITTMENT") {
- if (!course.showUndertaking) curentStep++;
- }
- if (STEPS[step] === "CHECK_ENV_1") {
- if (!(await isGetCheckEnvOk())) {
- emit("on-unselect-course");
- return;
- }
- if (!serverWantCheckEnv) curentStep += 2;
- showCheckEnv1 = true;
- }
- if (STEPS[step] === "CHECK_FACE") {
- if (course.faceEnable) {
- if (!store.user.photoPath) {
- logger({ cnl: ["server"], act: "无底照" });
- $message.info(
- "本场考试需要进行人脸检测,但是您没有上传底照,请联系老师!"
- );
- emit("on-unselect-course");
- return true;
- }
- if (!(await isGetFaceLivenessOk())) {
- emit("on-unselect-course");
- return true;
- }
- } else {
- curentStep++;
- await enterExam();
- }
- }
- }
- );
- function applyCommittmentResult(isCommitted: boolean) {
- if (!isCommitted) {
- emit("on-unselect-course");
- } else {
- curentStep++;
- }
- }
- let serverWantCheckEnv = $ref(false);
- function skipCheckEnv() {
- curentStep += 2;
- }
- function doCheckEnv() {
- curentStep++;
- }
- async function isIPOk(): Promise<boolean> {
- logger({ cnl: ["server"], act: "正在检测IP合法性..." });
- try {
- const ipLimit = (
- await httpApp.get("/api/ecs_exam_work/exam/ipLimit/" + course.examId)
- ).data;
- if (ipLimit.limited) {
- logger({ cnl: ["server"], act: "IP受限,请到中心指定地点进行考试!" });
- $message.error("IP受限,请到中心指定地点进行考试!");
- return false;
- }
- } catch (error) {
- $message.error("查询IP限制出错!");
- logger({ cnl: ["server"], act: "查询IP限制出错!" });
- return false;
- }
- return true;
- }
- async function isGetCheckEnvOk(): Promise<boolean> {
- try {
- let checkEnv = await httpApp.get(
- "/api/ecs_exam_work/exam/getExamPropertyFromCacheByStudentSession/" +
- course.examId +
- `/CHECK_ENVIRONMENT`
- );
- if (checkEnv.data.CHECK_ENVIRONMENT === "true") {
- serverWantCheckEnv = true;
- }
- } catch (error) {
- logger({
- cnl: ["server"],
- act: "获取CHECK_ENVIRONMENT失败",
- });
- $message.warning("网络错误,请重试!");
- // 获取不到流程必要的参数,就中断流程
- return false;
- }
- return true;
- }
- async function isGetFaceLivenessOk(): Promise<boolean> {
- let faceLiveness = null;
- try {
- faceLiveness = await httpApp.get(
- "/api/ecs_exam_work/exam/identificationOfLivingEnabled/" + course.examId
- );
- } catch (error) {
- $message.error("查询考试的人脸检测设置属性出错!");
- logger({ cnl: ["server"], act: "查询考试的人脸检测设置属性出错!" });
- return false;
- }
- if (faceLiveness.data) {
- logger({ cnl: ["server"], act: "正在检测底照是否满足活体检测标准..." });
- let checkBasePhoto;
- try {
- checkBasePhoto = (
- await httpApp.get(
- "/api/ecs_oe_student/examFaceLivenessVerify/checkFaceLiveness" +
- `?${store.user.token}` // 考生采用相同的机器考试,使用不同的请求的缓存
- )
- ).data;
- if (!checkBasePhoto.success) {
- $message.error("您上传的底照不符合活体检测的要求,请联系老师!");
- logger({
- cnl: ["server"],
- act: "您上传的底照不符合活体检测的要求,请联系老师!",
- });
- return false;
- }
- } catch (error) {
- $message.error("网络错误,请稍后重试!");
- logger({
- cnl: ["server"],
- act: "查询检测底照是否满足活体检测标准的接口出错!",
- possibleError: error,
- });
- return false;
- }
- }
- return true;
- }
- onMounted(async () => {
- if (!(await isIPOk())) {
- emit("on-unselect-course");
- return true;
- }
- if (await checkExamInProgress().catch(() => true)) {
- // 检测到有断点,或者断点有异常,退出当前流程
- emit("on-unselect-course");
- return;
- }
- curentStep = 0;
- });
- function onFacePassed(isRealStudent: boolean) {
- if (isRealStudent) {
- void enterExam();
- } else {
- if (!course.faceCheck) curentStep++;
- }
- }
- function onFaceClosedByUser() {
- emit("on-unselect-course");
- }
- function disagreeCommittment() {
- emit("on-unselect-course");
- }
- let passedAllChecks = false;
- async function enterExam() {
- let hstate = { examStudentId: course.examStudentId };
- /** 为了代码加密混淆效果更好,隐藏在线考试开始考试的启动逻辑,将在线考试启动接口调用放在该组件内部 */
- if (course.examType === "ONLINE") {
- const timestamp = Date.now();
- const rawStr = JSON.stringify({
- examStudentId: course.examStudentId,
- timestamp,
- });
- const key = getKey(timestamp);
- let encryptParams = "";
- encryptParams = doEnc(rawStr, key);
- try {
- let res = await httpApp.post<string>(
- "/api/ecs_oe_student/examControl/online/startExam",
- encryptParams,
- {
- "axios-retry": { retries: 4 },
- noErrorMessage: true,
- headers: {
- timestamp,
- "Content-Type": "text/plain",
- accept: "text/plain",
- },
- responseType: "text",
- transitional: { forcedJSONParsing: false },
- }
- );
- res.data = doDec(res.data, key);
- const newRes: {
- courseName: string;
- examRecordDataId: number;
- } = JSON.parse(res.data);
- Object.assign(hstate, {
- examRecordDataId: newRes.examRecordDataId,
- courseName: encodeURIComponent(newRes.courseName),
- });
- } catch (error) {
- // console.log(error, error.response, error.isAxiosError);
- if ("isAxiosError" in <any>error) {
- const ne = <AxiosError>error;
- const jsonRes = JSON.parse(<string>ne.response?.data ?? "null");
- const desc: string = jsonRes.desc;
- if (desc) {
- $message.error(desc);
- logger({ cnl: ["server"], act: "开考失败", dtl: desc });
- } else {
- logger({
- cnl: ["server"],
- act: "开考失败",
- dtl: "开考失败,并且无错误消息",
- possibleError: error,
- });
- }
- } else {
- logger({
- cnl: ["server"],
- act: "开考失败",
- dtl: "非接口失败",
- possibleError: error,
- });
- }
- return;
- }
- }
- passedAllChecks = true;
- await router.push({
- name: "OnlineExamOverview",
- params: { examId: course.examId },
- query: hstate,
- });
- }
- onUnmounted(() => {
- /** TODO: 摄像头关闭时机,需要完善 */
- if (!passedAllChecks && course.faceEnable) {
- // debug级别备选。因为初期上线,摄像头比较容易出错,所以保留此日志
- logger({
- cnl: ["server", "console"],
- act: "关闭sharedtream",
- dtl: "!passedAllChecks",
- });
- closeMediaStream();
- } else {
- logger({
- cnl: ["server", "console"],
- act: "保留sharedtream",
- dtl: "passedAllChecks",
- });
- }
- });
- </script>
- <template>
- <CommittmentDialog
- v-if="STEPS[curentStep] === 'COMMITTMENT'"
- ref="committmentDialogRef"
- :content="course.undertaking"
- @onCommitResult="applyCommittmentResult"
- />
- <n-modal
- v-if="STEPS[curentStep] === 'CHECK_ENV_1'"
- :show="showCheckEnv1"
- :closable="false"
- preset="card"
- title="进行环境检测"
- style="width: 600px"
- >
- <div>
- 环境检测可以检测电脑的硬件配置、网络速度和常用操作。环境检测不通过的话,可能影响考试的正常进行。
- </div>
- <div class="tw-flex tw-justify-center tw-mt-2">
- <n-button
- type="success"
- style="margin-right: 5px; min-width: 120px"
- @click="skipCheckEnv"
- >
- 跳过检测
- </n-button>
- <n-button
- type="success"
- style="margin-right: 5px; min-width: 120px"
- @click="doCheckEnv"
- >
- 进行检测
- </n-button>
- </div>
- </n-modal>
- <CheckComputer
- v-if="STEPS[curentStep] === 'CHECK_ENV_2'"
- @onClose="curentStep++"
- />
- <OnlineExamFaceCheckModal
- v-if="STEPS[curentStep] === 'CHECK_FACE'"
- @passed="onFacePassed"
- @close="onFaceClosedByUser"
- />
- <n-modal
- v-if="STEPS[curentStep] === 'CHECK_FACE_COMMITTMENT'"
- :show="true"
- :closable="false"
- preset="card"
- title="郑重承诺"
- style="width: 600px"
- >
- <div>
- 我承诺由本人参加考试,并且同意接受考试监控系统信息审核,一经发现作弊,立即取消本门课程考试成绩。
- </div>
- <div class="tw-flex tw-justify-center tw-mt-2">
- <n-button
- type="success"
- style="margin-right: 5px; min-width: 120px"
- @click="disagreeCommittment"
- >
- 取消
- </n-button>
- <n-button
- type="success"
- style="margin-right: 5px; min-width: 120px"
- @click="enterExam"
- >
- 确定
- </n-button>
- </div>
- </n-modal>
- </template>
|