Selaa lähdekoodia

合并练习和考试

shmily1213 3 viikkoa sitten
vanhempi
commit
3c50c29919

+ 10 - 1
src/api/modules/study.ts

@@ -177,4 +177,13 @@ export function getVideoStudyRecord() {
  */
 export function getSimulationExamSubjects() {
   return flyio.get('/front/exam/subjects') as Promise<ApiResponse<SimulationExamSubject[]>>;
-}
+}
+
+/**
+ * 开始考试
+ * @param paperId 
+ * @returns 
+ */
+export function beginExaminee(examineeId: number) {
+  return flyio.get('/front/exam/beginExaminee', { examineeId }) as Promise<ApiResponse<Examinee>>;
+} 

+ 42 - 0
src/common/enum.ts

@@ -200,4 +200,46 @@ export enum EnumExamType {
    * 单招(中职)
    */
   SVS = 'SVS'
+}
+
+export enum EnumSimulatedRecordStatus {
+  /**
+   * 空卷
+   */
+  INIT = 1,
+  /**
+   * 签到
+   */
+  SIGN = 2,
+  /**
+   * 考试
+   */
+  EXAM = 3,
+  /**
+   * 交卷
+   */
+  SUBMIT = 4,
+  /**
+   * 阅卷
+   */
+  REVIEW = 5,
+  /**
+   * 发布
+   */
+  PUBLISH = 6,
+  /**
+   * 关闭
+   */
+  CLOSE = 7,
+}
+
+export enum EnumPaperType {
+  /**
+   * 练习
+   */
+  PRACTICE = 'Practice',
+  /**
+   * 考试
+   */
+  SIMULATED = 'Simulated'
 }

+ 4 - 2
src/composables/useExam.ts

@@ -74,7 +74,7 @@ export const useExam = () => {
       }
     });
     for (let i = 0; i <= stateQuestionList.value.length - 1; i++) {
-      const qs = questionList.value[i];
+      const qs = stateQuestionList.value[i];
       state.forEach(item => {
         qs.progress = calcProgress(qs);
         if (qs.typeId === item.type) {
@@ -121,10 +121,11 @@ export const useExam = () => {
     return qs.isDone ? 100 : 0;
   }
   const isDone = (qs: Study.Question): boolean => {
+    // console.log(qs.answers, qs.answers && qs.answers.filter(item => !!item).length > 0 || !!qs.isNotKnow)
     if (qs.subQuestions && qs.subQuestions.length > 0) {
       return qs.subQuestions.every(q => isDone(q));
     }
-    return qs.answers && qs.answers.length > 0 || !!qs.isNotKnow;
+    return qs.answers && qs.answers.filter(item => !!item).length > 0 || !!qs.isNotKnow;
   }
   const nextQuestion = () => {
     if (currentIndex.value >= questionList.value.length - 1) {
@@ -241,6 +242,7 @@ export const useExam = () => {
     const arr: Study.Question[] = list.map(item => {
       return parseQuestion(item);
     });
+    console.log('处理后的题目', arr)
     questionList.value = arr;
   }
   const reset = () => {

+ 37 - 9
src/pagesStudy/components/exam-record-item.vue

@@ -1,22 +1,50 @@
 <template>
   <view class="bg-white">
-    <view class="text-30 text-fore-title">长沙民政职业技术学院-大数据与会计</view>
-    <view class="mt-16 text-26 text-fore-light">卷面得分 210分</view>
-    <view class="mt-8 flex items-center justify-between">
-      <view class="text-26 text-fore-light">考试时长 2025-09-25 10:00:00</view>
-      <view class="text-26 text-primary flex items-center gap-x-4">
-        <text>继续考试</text>
+    <view class="text-30 text-fore-title">{{ data.name }}</view>
+    <view class="mt-32 flex items-center justify-between">
+      <view class="text-26 text-fore-light">提交时间: {{ data.date || '-' }}</view>
+      <view class="text-26 text-primary flex items-center gap-x-4" @click="handleDetail">
+        <text>{{ isFinished ? '查看分析' : '继续考试' }}</text>
         <uv-icon name="arrow-right" size="14" color="var(--primary-color)" />
       </view>
     </view>
     <view
       class="mt-20 border border-solid border-[#FEF6DA] bg-[#FFFBEB] rounded-5 py-20 px-16 flex items-center justify-between">
-      <view class="text-24 text-[#F59E0B]">语数外+职业技能</view>
-      <view class="text-24 text-[#F59E0B]">录取概率:-%</view>
+      <view class="text-24 text-[#F59E0B]">考试科目:{{ data.subjectName }}</view>
+      <view class="text-24 text-[#F59E0B]">卷面得分:{{ data.score || '-' }}</view>
     </view>
   </view>
 </template>
 <script lang="ts" setup>
-
+import { Study } from '@/types';
+import { useTransferPage } from '@/hooks/useTransferPage';
+import { EnumSimulatedRecordStatus } from '@/common/enum';
+import { beginExaminee } from '@/api/modules/study';
+const { transferTo } = useTransferPage();
+const props = defineProps<{
+  data: Study.SimulatedRecord;
+}>();
+const isFinished = computed(() => {
+  return props.data.state === EnumSimulatedRecordStatus.SUBMIT;
+});
+const handleDetail = () => {
+  if (isFinished.value) {
+    transferTo('/pagesStudy/pages/simulation-analysis/simulation-analysis', {
+      data: {
+        id: props.data.id
+      }
+    });
+  } else {
+    transferTo('/pagesStudy/pages/start-exam/start-exam', {
+      data: {
+        name: '模拟考试-' + props.data.subjectName,
+        paperType: 'Simulated',
+        simulationInfo: {
+          examineeId: props.data.id,
+        }
+      }
+    });
+  }
+};
 </script>
 <style lang="scss" scoped></style>

+ 14 - 5
src/pagesStudy/pages/index/compoentns/index-exam-record.vue

@@ -1,5 +1,5 @@
 <template>
-  <view class="px-30 bg-back">
+  <view v-if="list.length > 0" class="px-30 pb-30 bg-back">
     <view class="h-94 flex items-center justify-center">
       <view class="h-0 w-160 border-0 border-b border-dashed border-border"></view>
       <ie-image src="/pagesStudy/static/image/icon-ear-left.png" customClass="w-20 h-26 ml-26 mr-30" />
@@ -7,13 +7,22 @@
       <ie-image src="/pagesStudy/static/image/icon-ear-right.png" customClass="w-20 h-26 ml-26 mr-30" />
       <view class="h-0 w-160 border-0 border-b border-dashed border-border"></view>
     </view>
-    <view class="py-50 text-center text-24 text-fore-subcontent bg-white rounded-4">暂无数据</view>
-    <!-- <view class="rounded-15 mb-20 bg-white px-20 py-38">
-      <exam-record-item />
-    </view> -->
+    <view v-for="value in list" :key="value.id" class="rounded-15 mb-20 bg-white px-20 py-38">
+      <exam-record-item :data="value" />
+    </view>
   </view>
 </template>
 <script lang="ts" setup>
 import ExamRecordItem from '@/pagesStudy/components/exam-record-item.vue';
+import { getSimulatedRecord } from '@/api/modules/study';
+import { Study } from '@/types';
+const list = ref<Study.SimulatedRecord[]>([]);
+const loadData = async () => {
+  const { data } = await getSimulatedRecord();
+  list.value = data || [];
+}
+onLoad(() => {
+  loadData();
+});
 </script>
 <style lang="scss" scoped></style>

+ 0 - 1
src/pagesStudy/pages/index/compoentns/index-test.vue

@@ -67,7 +67,6 @@ const handleStartTest = () => {
 }
 const loadData = () => {
   getSimulationInfo().then(res => {
-    console.log(res)
     tabOptions.value = res.data.subjects.map(item => ({
       label: item,
       value: item

+ 1 - 1
src/pagesStudy/pages/index/index.vue

@@ -64,7 +64,7 @@
     <index-banner />
     <view class="h-16 bg-back my-32"></view>
     <index-test :directed-school="firstDirectedSchool" />
-    <!-- <index-exam-record /> -->
+    <index-exam-record />
   </ie-page>
 </template>
 

+ 8 - 6
src/pagesStudy/pages/knowledge-practice/knowledge-practice.vue

@@ -15,7 +15,7 @@ import { useTransferPage } from '@/hooks/useTransferPage';
 import { getSubjectList, getKnowledgeList } from '@/api/modules/study';
 import knowledgeTree from '@/pagesStudy/components/knowledge-tree.vue';
 import * as Study from '@/types/study';
-import { EnumExamMode } from '@/common/enum';
+import { EnumPaperType } from '@/common/enum';
 import { useUserStore } from '@/store/userStore';
 const { prevData, transferTo } = useTransferPage();
 const currentSubjectIndex = ref<number>(-1);
@@ -60,11 +60,13 @@ const handleStartPractice = async (node: Study.KnowledgeNode) => {
   if (isVip) {
     transferTo('/pagesStudy/pages/start-exam/start-exam', {
       data: {
-        name: node.name,
-        mode: EnumExamMode.PRACTICE,
-        paperType: 'Practice',
-        relateId: node.id,
-        directed: prevData.value.directed
+        name: '知识点练习-' + node.name,
+        paperType: EnumPaperType.PRACTICE,
+        practiceInfo: {
+          name: node.name,
+          relateId: node.id,
+          directed: prevData.value.directed
+        },
       }
     });
   } else {

+ 23 - 9
src/pagesStudy/pages/simulation-start/simulation-start.vue

@@ -29,11 +29,11 @@
             </view>
             <view class="mt-40 flex items-center justify-between">
               <text class="text-28 text-fore-light">考试标准</text>
-              <text class="text-28 text-fore-title">{{ examTime }}分钟,满分{{ totalScore }}分</text>
+              <text class="text-28 text-fore-title">{{ paperInfo.time }}分钟,满分{{ paperInfo.score }}分</text>
             </view>
             <view class="mt-40 flex items-center justify-between">
               <text class="text-28 text-fore-light">考试题型</text>
-              <text class="text-28 text-fore-title">单选、多选、判断</text>
+              <text class="text-28 text-fore-title">{{ questionTypes }}</text>
             </view>
           </view>
           <view class="mt-36 rounded-15 bg-back px-40 py-36 flex">
@@ -42,7 +42,7 @@
               <view class="mt-10 text-28 text-fore-light">题量</view>
             </view>
             <view class="flex-1 text-center">
-              <view class="text-40 text-fore-title font-bold">{{ totalScore }}</view>
+              <view class="text-40 text-fore-title font-bold">{{ paperInfo.score }}</view>
               <view class="mt-10 text-28 text-fore-light">总分</view>
             </view>
           </view>
@@ -63,9 +63,17 @@ const { prevData, transferTo } = useTransferPage();
 const userStore = useUserStore();
 
 const isReady = ref(false);
-const totalScore = ref(0);
-const examTime = ref(0);
-const questions = ref<Study.Question[]>([]);
+const paperInfo = ref<Study.ExamineePaperInfo>({
+  score: 0,
+  time: 0,
+  types: []
+});
+const questionTypes = computed(() => {
+  return paperInfo.value.types.reduce((acc: string, curr: { type: string }, index: number) => {
+    return acc + curr.type + (index < paperInfo.value.types.length - 1 ? "、" : "");
+  }, "");
+})
+const questions = ref<Study.ApiQuestion[]>([]);
 const universityInfo = computed(() => {
   return prevData.value.universityInfo;
 });
@@ -86,13 +94,19 @@ const loadData = async () => {
     uni.$ie.hideLoading();
     return;
   }
-  const { examineeId, paperId } = data;
+  const { paperId } = data;
+  paperInfo.value = {
+    ...data.paperInfo,
+    time: Math.round(data.paperInfo.time / 60)
+  }
   const { data: paperData } = await getPaper({ type: 'Simulated', id: paperId });
   if (paperData) {
-    console.log(paperData)
+    questions.value = paperData.questions;
     isReady.value = true;
   }
-  uni.$ie.hideLoading();
+  setTimeout(() => {
+    uni.$ie.hideLoading();
+  }, 300);
 }
 onLoad(() => {
   loadData();

+ 393 - 0
src/pagesStudy/pages/start-exam/start-exam copy.vue

@@ -0,0 +1,393 @@
+<template>
+  <ie-page :fix-height="true" :safe-area-inset-bottom="false">
+    <ie-navbar :title="pageTitle" custom-back @left-click="handleLeftClick">
+      <template v-if="isReady" #headerRight>
+        <view v-if="isExamMode" class="countdown-text" :class="{ 'text-red-500': examDuration < 30 }">
+          {{ formatExamDuration }}
+        </view>
+        <view v-else class="">{{ formatPracticeDuration }}</view>
+      </template>
+    </ie-navbar>
+    <view v-if="isReady" class="px-20 py-14 bg-back flex justify-between items-center gap-x-20">
+      <text class="flex-1 min-w-1 text-26 ellipsis-1">{{ pageSubtitle }}</text>
+      <view class="flex items-baseline">
+        <text class="text-34 text-primary font-bold">{{ currentIndex + 1 }}</text>/
+        <text class="text-28 text-fore-subtitle">{{ totalCount }}</text>
+      </view>
+    </view>
+    <view class="flex-1 min-h-1 relative">
+      <view class="absolute inset-0 ">
+        <swiper class="h-full" :disable-touch="false" :current="currentIndex" :duration="swiperDuration"
+          @change="handleSwiperChange" @transition="handleSwiperTransition"
+          @animationfinish="handleSwiperAnimationFinish">
+          <swiper-item class="h-full" v-for="(item, index) in questionList" :key="index">
+            <question-wrap :question="item" @update:question="handleUpdateQuestion" />
+          </swiper-item>
+        </swiper>
+      </view>
+    </view>
+    <ie-safe-toolbar v-if="isReady" :height="64" :shadow="false">
+      <view class="px-18 h-full flex items-center justify-around border-0 border-t border-solid border-[#EFEFEF]">
+        <view class="w-48 h-48 flex items-center justify-center" @click="handleFavorite">
+          <uv-icon v-if="currentQuestion.isFavorite" name="star-fill" color="#FF9A18" size="27" />
+          <uv-icon v-else name="star" size="27" />
+        </view>
+        <view class="w-48 h-48 flex items-center justify-center" @click="handleMark">
+          <ie-image
+            :src="currentQuestion.isMark ? '/pagesStudy/static/image/icon-mark-active.png' : '/pagesStudy/static/image/icon-mark.png'"
+            custom-class="w-38 h-38" mode="aspectFill" />
+        </view>
+        <view class="w-48 h-48 flex items-center justify-center" @click="handleCalendar">
+          <uv-icon name="calendar" size="28" />
+        </view>
+      </view>
+    </ie-safe-toolbar>
+  </ie-page>
+  <question-stats-popup ref="questionStatsPopupRef">
+    <template #title>
+      <view class="ml-20">
+        <text class="text-30 text-primary">{{ doneCount }}</text>
+        <text>/</text>
+        <text class="text-30 text-fore-light">{{ totalCount }}</text>
+      </view>
+    </template>
+    <view class="popup-content">
+      <view class="flex-1 min-h-1">
+        <scroll-view class="h-full" scroll-y>
+          <view v-for="(item, i) in groupedQuestionList" :key="i" class="">
+            <template v-if="item.list.length > 0">
+              <view class="h-70 bg-back px-20 leading-70 text-fore-subcontent">{{ questionTypeDesc[item.type] }}</view>
+              <view class="grid grid-cols-5 place-items-center gap-x-20 gap-y-20 p-30">
+                <view v-for="(qs, j) in item.list" :key="j" class="aspect-square flex items-center justify-center"
+                  @click="hanadleNavigate(qs.index)">
+                  <view
+                    class="w-74 h-74 rounded-full flex items-center justify-center bg-white border border-solid border-border relative"
+                    :class="{
+                      'is-done': qs.question.isDone,
+                      'is-not-know': qs.question.isNotKnow,
+                      'is-mark': qs.question.isMark
+                    }">
+                    <text class="z-1">{{ qs.index + 1 }}</text>
+                    <ie-image v-if="qs.question.isMark" src="/pagesStudy/static/image/icon-mark-active.png"
+                      custom-class="absolute -top-12 left-14 w-28 h-28 z-1" mode="aspectFill" />
+                    <question-progress :progress="qs.question.progress || 0" />
+                  </view>
+                </view>
+
+              </view>
+
+            </template>
+          </view>
+        </scroll-view>
+      </view>
+      <view class="h-150 bg-white flex items-center gap-x-120 px-40">
+        <view class="flex flex-col items-center gap-x-10" @click="handleReset">
+          <uv-icon name="reload" size="20" :color="doneCount > 0 ? '#999' : '#cccccc'" />
+          <text class="mt-4 text-20 text-subcontent" :class="{ 'text-fore-light': doneCount <= 0 }">重新作答</text>
+        </view>
+        <view class="flex-1 py-20 text-center rounded-full bg-primary text-white" @click="beforeSubmit">交卷</view>
+      </view>
+    </view>
+  </question-stats-popup>
+</template>
+
+<script lang="ts" setup>
+import QuestionItem from './components/question-item.vue';
+import QuestionWrap from './components/question-wrap.vue';
+import QuestionStatsPopup from './components/question-stats-popup.vue';
+import QuestionProgress from './components/question-progress.vue';
+import { useTransferPage } from '@/hooks/useTransferPage';
+import { EnumExamMode } from '@/common/enum';
+import { getOpenExaminee, getPaper, commitExamineePaper, collectQuestion, cancelCollectQuestion } from '@/api/modules/study';
+import { useExam } from '@/composables/useExam';
+import { Study } from '@/types';
+import { NEXT_QUESTION, PREV_QUESTION, NEXT_QUESTION_QUICKLY, PREV_QUESTION_QUICKLY } from '@/types/injectionSymbols';
+// import { Examinee, ExamPaper, ExamPaperSubmit } from '@/types/study';
+const { prevData, transferBack } = useTransferPage();
+const { setQuestionList, questionList, groupedQuestionList, stateQuestionList, questionTypeDesc, favoriteList, notKnowList, markList, currentIndex,
+  totalCount, doneCount, notDoneCount, notKnowCount, markCount, isAllDone, nextQuestion, prevQuestion, nextQuestionQuickly, prevQuestionQuickly, swiperDuration,
+  formatPracticeDuration, formatExamDuration, practiceDuration, startPracticeDuration, stopPracticeDuration,
+  examDuration, startExamDuration, stopExamDuration, setExamDuration, setCountDownCallback, changeIndex, setDuration, reset } = useExam();
+
+provide(NEXT_QUESTION, nextQuestion);
+provide(PREV_QUESTION, prevQuestion);
+provide(NEXT_QUESTION_QUICKLY, nextQuestionQuickly);
+provide(PREV_QUESTION_QUICKLY, prevQuestionQuickly);
+
+const isAnimationFinish = ref(false);
+const transitionStartX = ref(null);
+const transitionEndX = ref(null);
+const isReady = ref(false);
+// 自动提交只提醒1次
+const hasShowSubmitConfirm = ref(false);
+const examineerData = ref<Study.Examinee>({} as Study.Examinee);
+const paperData = ref<Study.ExamPaper>({} as Study.ExamPaper);
+const pageTitle = computed(() => {
+  const { mode } = prevData.value;
+  return mode === EnumExamMode.PRACTICE ? '练习' : '考试';
+});
+const pageSubtitle = computed(() => {
+  const { mode, name } = prevData.value;
+  return (mode === EnumExamMode.PRACTICE ? '知识点练习' : '考试') + '-' + name;
+});
+const isExamMode = computed(() => {
+  return prevData.value.mode === EnumExamMode.EXAM;
+});
+const handleLeftClick = () => {
+  if (!isReady.value) {
+    transferBack();
+    return;
+  }
+  beforeQuit();
+};
+const handleSwiperChange = (e: any) => {
+  currentIndex.value = e.detail.current;
+};
+const beforeQuit = () => {
+  const { mode } = prevData.value;
+  if (!isReady.value) {
+    return;
+  }
+  stopTime();
+  const msg = mode === EnumExamMode.PRACTICE ? '当前练习未完成,确认退出?' : '当前考试未完成,确认退出?';
+  uni.$ie.showModal({
+    title: '提示',
+    content: msg,
+  }).then(confirm => {
+    if (confirm) {
+      handleSubmit(true);
+    } else {
+      startTime();
+    }
+  });
+};
+const currentQuestion = computed(() => {
+  return questionList.value[currentIndex.value] || {};
+});
+const hanadleNavigate = (index: number) => {
+  changeIndex(index);
+}
+const handleFavorite = async () => {
+  if (!currentQuestion.value.isFavorite) {
+    await collectQuestion(currentQuestion.value.id);
+    currentQuestion.value.isFavorite = true;
+    uni.$ie.showToast('收藏成功');
+  } else {
+    await cancelCollectQuestion(currentQuestion.value.id);
+    currentQuestion.value.isFavorite = false;
+    uni.$ie.showToast('取消收藏成功');
+  }
+};
+
+const handleMark = () => {
+  currentQuestion.value.isMark = !currentQuestion.value.isMark;
+};
+const questionStatsPopupRef = ref();
+const handleCalendar = () => {
+  questionStatsPopupRef.value.open();
+};
+
+const handleSwiperTransition = (e: any) => {
+  if (currentIndex.value === questionList.value.length - 1) {
+    if (!transitionStartX.value) {
+      transitionStartX.value = e.detail.dx;
+    } else {
+      transitionEndX.value = e.detail.dx;
+    }
+    return;
+  }
+};
+
+const startTime = () => {
+  if (isExamMode.value) {
+    startExamDuration();
+  } else {
+    startPracticeDuration();
+  }
+}
+
+const stopTime = () => {
+  if (isExamMode.value) {
+    stopExamDuration();
+  } else {
+    stopPracticeDuration();
+  }
+}
+
+const handleSwiperAnimationFinish = (e: any) => {
+  if (transitionStartX.value == null || transitionEndX.value == null || currentIndex.value !== questionList.value.length - 1) {
+    isAnimationFinish.value = true;
+    transitionStartX.value = null;
+    transitionEndX.value = null;
+    return;
+  }
+  const offsetX = transitionEndX.value - transitionStartX.value;
+  if (offsetX < 0 && offsetX > -150) {
+    beforeSubmit();
+  }
+  isAnimationFinish.value = true;
+  transitionStartX.value = null;
+  transitionEndX.value = null;
+};
+
+const beforeSubmit = () => {
+  const text = notDoneCount.value > 0 ? `还有${notDoneCount.value}题未做,确认交卷?` : '是否确认交卷?';
+  stopTime();
+  uni.$ie.showModal({
+    title: '提示',
+    content: text,
+  }).then(confirm => {
+    if (confirm) {
+      handleSubmit(false);
+    } else {
+      startTime();
+    }
+  });
+}
+
+const handleReset = () => {
+  if (doneCount.value <= 0) {
+    return;
+  }
+  uni.$ie.showModal({
+    title: '重新作答',
+    content: '是否确认清空全部作答数据?',
+  }).then(confirm => {
+    if (confirm) {
+      questionStatsPopupRef.value.close();
+      reset();
+      setTimeout(() => {
+        startTime();
+      }, 300);
+    }
+  });
+}
+
+const autoSubmit = () => {
+  if (isAllDone.value) {
+    if (hasShowSubmitConfirm.value) {
+      return;
+    }
+    hasShowSubmitConfirm.value = true;
+    beforeSubmit();
+  }
+}
+
+const handleSubmit = (tempSave: boolean = false) => {
+  console.log('handleSubmit', questionList.value)
+  const msg = tempSave ? '保存中...' : '提交中...';
+  uni.$ie.showLoading(msg);
+  setTimeout(() => {
+    uni.$ie.hideLoading();
+    const params = {
+      ...paperData.value,
+      questions: questionList.value.map(item => {
+        return {
+          ...item,
+          isDone: tempSave ? item.isDone : true
+        };
+      }),
+      examineeId: examineerData.value.examineeId,
+      isDone: isAllDone.value,
+      duration: isExamMode.value ? examDuration.value : practiceDuration.value
+    } as Study.ExamPaperSubmit;
+    if (!isExamMode.value) {
+      params.duration = practiceDuration.value;
+    }
+    commitExamineePaper(params);
+    uni.navigateBack();
+  }, 1000);
+}
+
+const handleUpdateQuestion = (question: Study.Question) => {
+  if (currentIndex.value === questionList.value.length - 1) {
+    autoSubmit();
+  }
+}
+/**
+ * 恢复上次做题历史数据
+ * @param savedQuestion 上次做题历史数据
+ * @param fullQuestion 当前试卷数据
+ */
+const restoreQuestion = (savedQuestion: Study.ExamineeQuestion[], fullQuestion: Study.ApiQuestion[]) => {
+  if (!savedQuestion) {
+    return fullQuestion;
+  }
+  console.log(savedQuestion, fullQuestion)
+  for (const item of fullQuestion) {
+    const savedQs = savedQuestion.find(q => q.id === item.id);
+    if (savedQs) {
+      if (savedQs.answers) {
+        item.answers = savedQs.answers.filter(ans => ans.trim());
+      }
+      item.isMark = savedQs.isMark;
+      item.isFavorite = savedQs.isFavorite;
+      item.isNotKnow = savedQs.isNotKnow;
+      if (item.subQuestions) {
+        restoreQuestion(savedQs.subQuestions, item.subQuestions);
+      }
+    }
+  }
+  return fullQuestion;
+}
+const loadData = async () => {
+  uni.$ie.showLoading();
+  const { data } = await getOpenExaminee({
+    paperType: prevData.value.paperType,
+    relateId: prevData.value.relateId,
+    directed: prevData.value.directed
+  });
+  if (!data) {
+    uni.$ie.hideLoading();
+    transferBack();
+    return;
+  }
+  examineerData.value = data;
+  if (data.paperId) {
+    const res = await getPaper({
+      type: prevData.value.paperType,
+      id: data.paperId
+    });
+
+    uni.$ie.hideLoading();
+    paperData.value = res.data;
+    paperData.value.questions = restoreQuestion(data.questions, res.data.questions);
+    setQuestionList(paperData.value.questions);
+    setDuration(data.duration || 0);
+  }
+  isReady.value = true;
+  setCountDownCallback(() => {
+    handleSubmit(false);
+  });
+  startTime();
+};
+onLoad(() => {
+  loadData();
+});
+</script>
+
+<style lang="scss" scoped>
+.countdown-text {
+  height: 100%;
+  display: flex;
+  align-items: center;
+  font-weight: bold;
+  font-family: 'Courier New', Courier, monospace;
+}
+
+.popup-content {
+  @apply h-[42vh] flex flex-col;
+}
+
+.scroll-view {
+  @apply h-full;
+}
+
+.is-done {
+  @apply text-primary border-[#EBF9FF] bg-[#EBF9FF];
+}
+
+.is-not-know {
+  @apply text-fore-title border-[#F2F2F2] bg-[#F2F2F2];
+}
+</style>

+ 64 - 57
src/pagesStudy/pages/start-exam/start-exam.vue

@@ -2,10 +2,7 @@
   <ie-page :fix-height="true" :safe-area-inset-bottom="false">
     <ie-navbar :title="pageTitle" custom-back @left-click="handleLeftClick">
       <template v-if="isReady" #headerRight>
-        <view v-if="isExamMode" class="countdown-text" :class="{ 'text-red-500': examDuration < 30 }">
-          {{ formatExamDuration }}
-        </view>
-        <view v-else class="">{{ formatPracticeDuration }}</view>
+        <view class="" :class="{ 'text-red-500': practiceDuration > totalExamTime }">{{ formatPracticeDuration }}</view>
       </template>
     </ie-navbar>
     <view v-if="isReady" class="px-20 py-14 bg-back flex justify-between items-center gap-x-20">
@@ -20,7 +17,7 @@
         <swiper class="h-full" :disable-touch="false" :current="currentIndex" :duration="swiperDuration"
           @change="handleSwiperChange" @transition="handleSwiperTransition"
           @animationfinish="handleSwiperAnimationFinish">
-          <swiper-item class="h-full" v-for="(item, index) in questionList" :key="index">
+          <swiper-item class="h-full" v-for="(item, index) in stateQuestionList" :key="index">
             <question-wrap :question="item" @update:question="handleUpdateQuestion" />
           </swiper-item>
         </swiper>
@@ -73,9 +70,7 @@
                     <question-progress :progress="qs.question.progress || 0" />
                   </view>
                 </view>
-
               </view>
-
             </template>
           </view>
         </scroll-view>
@@ -97,8 +92,8 @@ import QuestionWrap from './components/question-wrap.vue';
 import QuestionStatsPopup from './components/question-stats-popup.vue';
 import QuestionProgress from './components/question-progress.vue';
 import { useTransferPage } from '@/hooks/useTransferPage';
-import { EnumExamMode } from '@/common/enum';
-import { getOpenExaminee, getPaper, commitExamineePaper, collectQuestion, cancelCollectQuestion } from '@/api/modules/study';
+import { EnumPaperType } from '@/common/enum';
+import { getOpenExaminee, getPaper, commitExamineePaper, collectQuestion, cancelCollectQuestion, beginExaminee } from '@/api/modules/study';
 import { useExam } from '@/composables/useExam';
 import { Study } from '@/types';
 import { NEXT_QUESTION, PREV_QUESTION, NEXT_QUESTION_QUICKLY, PREV_QUESTION_QUICKLY } from '@/types/injectionSymbols';
@@ -118,20 +113,20 @@ const isAnimationFinish = ref(false);
 const transitionStartX = ref(null);
 const transitionEndX = ref(null);
 const isReady = ref(false);
+// 考试规定时间
+const totalExamTime = ref<number>(0);
 // 自动提交只提醒1次
 const hasShowSubmitConfirm = ref(false);
-const examineerData = ref<Study.Examinee>({} as Study.Examinee);
+const examineeId = ref<number | undefined>(undefined);
+// const examineerData = ref<Study.Examinee>({} as Study.Examinee);
 const paperData = ref<Study.ExamPaper>({} as Study.ExamPaper);
 const pageTitle = computed(() => {
-  const { mode } = prevData.value;
-  return mode === EnumExamMode.PRACTICE ? '练习' : '考试';
+  const { paperType } = prevData.value;
+  return paperType === EnumPaperType.PRACTICE ? '练习' : '考试';
 });
 const pageSubtitle = computed(() => {
-  const { mode, name } = prevData.value;
-  return (mode === EnumExamMode.PRACTICE ? '知识点练习' : '考试') + '-' + name;
-});
-const isExamMode = computed(() => {
-  return prevData.value.mode === EnumExamMode.EXAM;
+  const { name } = prevData.value;
+  return name;
 });
 const handleLeftClick = () => {
   if (!isReady.value) {
@@ -144,12 +139,12 @@ const handleSwiperChange = (e: any) => {
   currentIndex.value = e.detail.current;
 };
 const beforeQuit = () => {
-  const { mode } = prevData.value;
+  const { paperType } = prevData.value;
   if (!isReady.value) {
     return;
   }
   stopTime();
-  const msg = mode === EnumExamMode.PRACTICE ? '当前练习未完成,确认退出?' : '当前考试未完成,确认退出?';
+  const msg = paperType === EnumPaperType.PRACTICE ? '当前练习未完成,确认退出?' : '当前考试未完成,确认退出?';
   uni.$ie.showModal({
     title: '提示',
     content: msg,
@@ -162,7 +157,7 @@ const beforeQuit = () => {
   });
 };
 const currentQuestion = computed(() => {
-  return questionList.value[currentIndex.value] || {};
+  return stateQuestionList.value[currentIndex.value] || {};
 });
 const hanadleNavigate = (index: number) => {
   changeIndex(index);
@@ -188,7 +183,7 @@ const handleCalendar = () => {
 };
 
 const handleSwiperTransition = (e: any) => {
-  if (currentIndex.value === questionList.value.length - 1) {
+  if (currentIndex.value === stateQuestionList.value.length - 1) {
     if (!transitionStartX.value) {
       transitionStartX.value = e.detail.dx;
     } else {
@@ -199,23 +194,15 @@ const handleSwiperTransition = (e: any) => {
 };
 
 const startTime = () => {
-  if (isExamMode.value) {
-    startExamDuration();
-  } else {
-    startPracticeDuration();
-  }
+  startPracticeDuration();
 }
 
 const stopTime = () => {
-  if (isExamMode.value) {
-    stopExamDuration();
-  } else {
-    stopPracticeDuration();
-  }
+  stopPracticeDuration();
 }
 
 const handleSwiperAnimationFinish = (e: any) => {
-  if (transitionStartX.value == null || transitionEndX.value == null || currentIndex.value !== questionList.value.length - 1) {
+  if (transitionStartX.value == null || transitionEndX.value == null || currentIndex.value !== stateQuestionList.value.length - 1) {
     isAnimationFinish.value = true;
     transitionStartX.value = null;
     transitionEndX.value = null;
@@ -274,33 +261,31 @@ const autoSubmit = () => {
 }
 
 const handleSubmit = (tempSave: boolean = false) => {
-  console.log('handleSubmit', questionList.value)
+  console.log('handleSubmit', stateQuestionList.value)
   const msg = tempSave ? '保存中...' : '提交中...';
   uni.$ie.showLoading(msg);
   setTimeout(() => {
     uni.$ie.hideLoading();
     const params = {
       ...paperData.value,
-      questions: questionList.value.map(item => {
+      questions: stateQuestionList.value.map(item => {
         return {
           ...item,
           isDone: tempSave ? item.isDone : true
         };
       }),
-      examineeId: examineerData.value.examineeId,
+      examineeId: examineeId.value,
+      // examineeId: examineerData.value.examineeId,
       isDone: isAllDone.value,
-      duration: isExamMode.value ? examDuration.value : practiceDuration.value
+      duration: practiceDuration.value
     } as Study.ExamPaperSubmit;
-    if (!isExamMode.value) {
-      params.duration = practiceDuration.value;
-    }
     commitExamineePaper(params);
     uni.navigateBack();
   }, 1000);
 }
 
 const handleUpdateQuestion = (question: Study.Question) => {
-  if (currentIndex.value === questionList.value.length - 1) {
+  if (currentIndex.value === stateQuestionList.value.length - 1) {
     autoSubmit();
   }
 }
@@ -330,36 +315,58 @@ const restoreQuestion = (savedQuestion: Study.ExamineeQuestion[], fullQuestion:
   }
   return fullQuestion;
 }
-const loadData = async () => {
-  uni.$ie.showLoading();
+const loadPracticeData = async () => {
+  const { paperType, practiceInfo } = prevData.value;
   const { data } = await getOpenExaminee({
-    paperType: prevData.value.paperType,
-    relateId: prevData.value.relateId,
-    directed: prevData.value.directed
+    paperType: paperType,
+    relateId: practiceInfo.relateId,
+    directed: practiceInfo.directed
   });
   if (!data) {
     uni.$ie.hideLoading();
     transferBack();
     return;
   }
-  examineerData.value = data;
-  if (data.paperId) {
+  combinePaperData(data, paperType);
+}
+const loadSimulationData = async () => {
+  const { paperType, simulationInfo } = prevData.value;
+  const { data } = await beginExaminee(simulationInfo.examineeId);
+  console.log('开卷信息', data)
+  if (!data) {
+    uni.$ie.hideLoading();
+    transferBack();
+    return;
+  }
+  totalExamTime.value = data.paperInfo.time;
+  combinePaperData(data, paperType);
+}
+const combinePaperData = async (examinee: Study.Examinee, paperType: EnumPaperType) => {
+  examineeId.value = examinee.examineeId;
+  if (examinee.paperId) {
     const res = await getPaper({
-      type: prevData.value.paperType,
-      id: data.paperId
+      type: paperType,
+      id: examinee.paperId
     });
-
+    console.log('试卷信息', res)
     uni.$ie.hideLoading();
     paperData.value = res.data;
-    paperData.value.questions = restoreQuestion(data.questions, res.data.questions);
+    paperData.value.questions = restoreQuestion(examinee.questions, res.data.questions);
     setQuestionList(paperData.value.questions);
-    setDuration(data.duration || 0);
+    setDuration(examinee.duration || 0);
+    isReady.value = true;
+    startTime();
+    console.log(stateQuestionList.value)
+  }
+}
+const loadData = async () => {
+  uni.$ie.showLoading();
+  const { paperType } = prevData.value;
+  if (paperType === EnumPaperType.PRACTICE) {
+    loadPracticeData();
+  } else {
+    loadSimulationData();
   }
-  isReady.value = true;
-  setCountDownCallback(() => {
-    handleSubmit(false);
-  });
-  startTime();
 };
 onLoad(() => {
   loadData();

+ 17 - 0
src/types/study.ts

@@ -1,3 +1,5 @@
+import { EnumSimulatedRecordStatus } from "@/common/enum";
+
 export interface TeachClass {
   classId: number;
   schoolId: number;
@@ -119,6 +121,17 @@ export interface Examinee {
   paperId: number;
   duration: number;
   questions: ExamineeQuestion[];
+  // 
+  paperInfo: ExamineePaperInfo;
+}
+export interface ExamineePaperInfo {
+  score: number;
+  time: number;
+  types: {
+    type: string;
+    count: number;
+    score: number;
+  }[]
 }
 
 /**
@@ -255,11 +268,15 @@ export interface KnowledgeRecord {
 }
 
 export interface SimulatedRecord {
+  id: number;
   rate: number;
   name: string;
   score: number;
   total: number;
   date: string;
+  // 
+  subjectName: string;
+  state: EnumSimulatedRecordStatus;
 }
 
 export interface VideoStudyRecord {