Jelajahi Sumber

适配子题和主观题

shmily1213 2 minggu lalu
induk
melakukan
32a4cbce63

+ 131 - 26
src/composables/useExam.ts

@@ -53,13 +53,39 @@ export const useExam = () => {
   const notKnowList = ref<Study.Question[]>([]);
   // 重点标记列表
   const markList = ref<Study.Question[]>([]);
+  const virtualCurrentIndex = ref<number>(0);
+  const virtualTotalCount = computed(() => {
+    return questionList.value.reduce((acc, item) => {
+      if (item.subQuestions && item.subQuestions.length > 0) {
+        return acc + item.subQuestions.length;
+      }
+      return acc + 1;
+    }, 0);
+  });
+  const subQuestionIndex = ref<number>(0);
   // 包含状态的问题列表
   const stateQuestionList = computed(() => {
-    return questionList.value.map(item => {
+    function parseQuestion(qs: Study.Question, parentIndex: number) {
+      if (qs.subQuestions && qs.subQuestions.length > 0) {
+        qs.subQuestions.forEach((item, index) => {
+          item.isSubQuestion = true;
+          item.parentId = qs.id;
+          item.parentTypeId = qs.typeId;
+          item.subIndex = index;
+          item.parentIndex = parentIndex;
+          item.isDone = isDone(item);
+        });
+      } else {
+        qs.isSubQuestion = false;
+      }
       return {
-        ...item,
-        isDone: isDone(item)
+        ...qs,
+        isDone: isDone(qs)
       }
+    }
+    console.log('重新计算')
+    return questionList.value.map((item, index) => {
+      return parseQuestion(item, index)
     });
   });
   const groupedQuestionList = computed(() => {
@@ -73,17 +99,40 @@ export const useExam = () => {
         }[]
       }
     });
-    for (let i = 0; i <= stateQuestionList.value.length - 1; i++) {
-      const qs = stateQuestionList.value[i];
-      state.forEach(item => {
-        qs.progress = calcProgress(qs);
-        if (qs.typeId === item.type) {
-          item.list.push({
+    let offset = 0;
+    function addQuestion(qs: Study.Question, index: number) {
+      if (qs.subQuestions && qs.subQuestions.length > 0) {
+        qs.subQuestions.forEach((subQs, subIndex) => {
+          offset++;
+          addQuestion(subQs, index + subIndex);
+        });
+      } else {
+        let group;
+        if (qs.isSubQuestion) {
+          group = state.find(item => item.type === qs.parentTypeId);
+        } else {
+          group = state.find(item => item.type === qs.typeId);
+        }
+        if (group) {
+          group.list.push({
             question: qs,
-            index: i
+            index
+          });
+        } else {
+          state.push({
+            type: qs.typeId,
+            list: [{
+              question: qs,
+              index
+            }]
           });
         }
-      });
+      }
+    }
+    console.log(stateQuestionList.value.length)
+    for (let i = 0; i <= stateQuestionList.value.length - 1; i++) {
+      const qs = stateQuestionList.value[i];
+      addQuestion(qs, i + offset);
     }
     return state;
   });
@@ -98,17 +147,26 @@ export const useExam = () => {
   const doneCount = computed(() => {
     // 有答案的或者不会做的,都认为是做了
     // return groupedQuestionList.value.reduce((acc, item) => acc + item.list.filter(q => q.question.isDone || q.question.isNotKnow).length, 0);
-    return stateQuestionList.value.filter(q => q.isDone || q.isNotKnow).length;
+    // return stateQuestionList.value.filter(q => q.isDone || q.isNotKnow).length;
+    let count = 0;
+    for (let i = 0; i <= stateQuestionList.value.length - 1; i++) {
+      const qs = stateQuestionList.value[i];
+      if (qs.isDone || qs.isNotKnow) {
+        count++;
+      }
+      if (qs.subQuestions && qs.subQuestions.length > 0) {
+        qs.subQuestions.forEach(subQs => {
+          if (subQs.isDone || subQs.isNotKnow) {
+            count++;
+          }
+        });
+      }
+    }
+    return count;
   });
   const notDoneCount = computed(() => {
-    return stateQuestionList.value.length - doneCount.value;
+    return virtualTotalCount.value - doneCount.value;
   });
-  // const notKnowCount = computed(() => {
-  //   return stateQuestionList.value.filter(q => q.isNotKnow).length;
-  // });
-  // const markCount = computed(() => {
-  //   return stateQuestionList.value.filter(q => q.isMark).length;
-  // });
   // 包含子题的题目计算整体做题进度
   const calcProgress = (qs: Study.Question): number => {
     if (qs.subQuestions && qs.subQuestions.length > 0) {
@@ -117,9 +175,9 @@ export const useExam = () => {
     return qs.isDone ? 100 : 0;
   }
   const isDone = (qs: Study.Question): boolean => {
-    if (qs.subQuestions && qs.subQuestions.length > 0) {
-      return qs.subQuestions.every(q => isDone(q));
-    }
+    // if (qs.subQuestions && qs.subQuestions.length > 0) {
+    //   return qs.subQuestions.every(q => isDone(q));
+    // }
     return qs.answers && qs.answers.filter(item => !!item).length > 0 || !!qs.isNotKnow;
   }
   const isCorrect = (qs: Study.ExamineeQuestion): boolean => {
@@ -224,13 +282,13 @@ export const useExam = () => {
     const orders = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
     // 数据预处理
     // 1、给每个项目补充额外字段
-    const parseQuestion = (item: Study.ExamineeQuestion): Study.Question => {
+    const transerQuestion = (item: Study.ExamineeQuestion): Study.Question => {
       return {
         ...item,
         // 处理没有题型的大题,统一作为阅读题
         typeId: (item.typeId === null || item.typeId === undefined) ? EnumQuestionType.OTHER : item.typeId,
         answers: item.answers || [],
-        subQuestions: item.subQuestions?.map(parseQuestion) || [],
+        subQuestions: item.subQuestions?.map(transerQuestion) || [],
         options: item.options?.map((option, index) => {
           return {
             name: option,
@@ -244,8 +302,29 @@ export const useExam = () => {
       } as Study.Question
     }
     const arr: Study.Question[] = list.map(item => {
-      return parseQuestion(item);
+      return transerQuestion(item);
     });
+    const qsList: Study.Question[] = [];
+    const parseQuestion = (qs: Study.Question) => {
+      if (qs.subQuestions && qs.subQuestions.length > 0) {
+        qs.isSubQuestion = true;
+        qs.isDone = isDone(qs);
+        qs.subQuestions.forEach((item, index) => {
+          item.parentId = qs.id;
+          item.parentTypeId = qs.typeId;
+          item.subIndex = index;
+          // parseQuestion(item);
+        });
+      } else {
+        qsList.push({
+          ...qs,
+          isSubQuestion: false,
+          isDone: isDone(qs)
+        });
+      }
+    };
+    // arr.forEach(parseQuestion);
+    // return qsList;
     questionList.value = arr;
   }
   const reset = () => {
@@ -254,15 +333,37 @@ export const useExam = () => {
         ...item,
         answers: [],
         isMark: false,
-        isNotKnow: false
+        isNotKnow: false,
+        subQuestions: item.subQuestions.map(subItem => {
+          return {
+            ...subItem,
+            answers: [],
+            isMark: false,
+            isNotKnow: false
+          } as Study.Question;
+        })
       } as Study.Question;
     });
+    console.log(questionList.value)
     changeIndex(0);
     practiceDuration.value = 0;
     examDuration.value = 0;
     interval && clearInterval(interval);
     interval = null;
   }
+  const setSubQuestionIndex = (index: number) => {
+    subQuestionIndex.value = index;
+  }
+  watch(() => currentIndex.value, (val) => {
+    subQuestionIndex.value = 0;
+  }, {
+    immediate: false
+  });
+  watch([() => currentIndex.value, () => subQuestionIndex.value], (val) => {
+    virtualCurrentIndex.value = val[0] + val[1];
+  }, {
+    immediate: false
+  });
   return {
     questionList,
     groupedQuestionList,
@@ -273,6 +374,10 @@ export const useExam = () => {
     currentIndex,
     isAllDone,
     totalCount,
+    virtualCurrentIndex,
+    virtualTotalCount,
+    subQuestionIndex,
+    setSubQuestionIndex,
     doneCount,
     notDoneCount,
     questionTypeDesc,

+ 12 - 0
src/pages.json

@@ -577,6 +577,18 @@
             "navigationBarTitleText": ""
           }
         },
+        {
+          "path": "pages/knowledge-practice-history/knowledge-practice-history",
+          "style": {
+            "navigationBarTitleText": ""
+          }
+        },
+        {
+          "path": "pages/knowledge-practice-detail/knowledge-practice-detail",
+          "style": {
+            "navigationBarTitleText": ""
+          }
+        },
         {
           "path": "pages/exam-start/exam-start",
           "style": {

+ 83 - 31
src/pagesStudy/pages/exam-start/components/question-item.vue

@@ -5,47 +5,66 @@
         {{ questionTypeDesc[question.typeId as EnumQuestionType] }}
       </view>
       <view class="question-content" :class="{ 'mt-30': isSubQuestion }">
-        <uv-parse :content="getQuestionTitle()"></uv-parse>
+        <text class="text-nowrap text-30">{{ getQuestionTitle() }} &nbsp;</text>
+        <uv-parse :content="question.title" containerStyle="display:inline"
+          contentStyle="word-break:break-word;"></uv-parse>
       </view>
       <view class="question-options">
         <view class="question-option" v-for="option in question.options" :class="getStyleClass(option)" :key="option.id"
           @click="handleSelect(option)">
-          <view v-if="!readonly" class="question-option-index">{{ option.no }}</view>
+          <template v-if="!readonly">
+            <view v-if="!isOnlySubjective" class="question-option-index">{{ option.no }}</view>
+            <view v-else>
+              <uv-icon name="info-circle" :color="question.isNotKnow ? '#31A0FC' : '#999'" size="18" />
+            </view>
+          </template>
           <view v-else>
             <uv-icon v-if="isOptionCorrect(option)" name="checkmark-circle-fill" color="#2CC6A0" size="22" />
             <uv-icon v-else-if="isOptionIncorrect(option)" name="close-circle-fill" color="#FF5B5C" size="22" />
             <view v-else class="question-option-index">{{ option.no }}</view>
           </view>
           <view class="question-option-content">
-            <uv-parse :content="option.name"></uv-parse>
+            <uv-parse :content="getOptionContent(option)" containerStyle="display:inline"
+              contentStyle="word-break:break-word;"></uv-parse>
           </view>
         </view>
-        <view v-if="question.options.length && !readonly" class="question-option"
+        <view v-if="question.options.length && !readonly && !isOnlySubjective" class="question-option"
           :class="{ 'question-option-not-know': question.isNotKnow }" @click="handleNotKnow">
           <view class="question-option-index">
             <uv-icon name="info-circle" :color="question.isNotKnow ? '#31A0FC' : '#999'" size="18" />
           </view>
           <view class="question-option-content text-fore-light">不会</view>
         </view>
-      </view>
-      <view v-if="readonly" class="answer-wrap mt-40 rounded-8 pt-60 pb-40 flex items-center text-center relative">
-        <ie-image v-if="question.isCorrect" src="/pagesStudy/static/image/icon-answer-correct.png"
-          class="absolute top-0 left-1/2 -translate-x-1/2 w-222 h-64" />
-        <ie-image v-else src="/pagesStudy/static/image/icon-answer-incorrect.png"
-          class="absolute top-0 left-1/2 -translate-x-1/2 w-240 h-64" />
-        <view class="flex-1">
-          <view class="text-34 text-[#2CC6A0] font-bold">{{ question.answer1 }}</view>
-          <view class="mt-4 text-26">正确答案</view>
-        </view>
-        <view class="h-40 w-1 bg-back"></view>
-        <view class="flex-1">
-          <view class="text-34 font-bold" :class="[question.isCorrect ? 'text-[#2CC6A0]' : 'text-[#FF5B5C]']">
-            {{ question.answers.join('') || (question.isNotKnow ? '不会' : '未答') }}
+        <view v-if="!readonly && isOnlySubjective" class="mt-40 bg-[#EBF9FF] p-12 rounded-8">
+          <view class="rounded-8 bg-white px-10 py-20 text-primary text-24 flex gap-x-6 items-center">
+            <uv-icon name="info-circle" color="#31A0FC" size="16" />
+            <text>主观题请线下答题,查看解析对比后,选“会”或“不会”</text>
           </view>
-          <view class="mt-4 text-26">我的答案</view>
+          <view class="mt-30 mb-20 text-24 text-white bg-primary w-fit mx-auto px-20 py-12 rounded-full text-center" @click="handleShowParse">
+            查看解析</view>
         </view>
       </view>
-      <view v-if="question.parse" class="mt-40">
+      <!-- 阅卷模式下显示答案 -->
+      <template v-if="readonly">
+        <view class="answer-wrap mt-40 rounded-8 pt-60 pb-40 flex items-center text-center relative">
+          <ie-image v-if="question.isCorrect" src="/pagesStudy/static/image/icon-answer-correct.png"
+            class="absolute top-0 left-1/2 -translate-x-1/2 w-222 h-64" />
+          <ie-image v-else src="/pagesStudy/static/image/icon-answer-incorrect.png"
+            class="absolute top-0 left-1/2 -translate-x-1/2 w-240 h-64" />
+          <view class="flex-1">
+            <view class="text-34 text-[#2CC6A0] font-bold">{{ question.answer1 }}</view>
+            <view class="mt-4 text-26">正确答案</view>
+          </view>
+          <view class="h-40 w-1 bg-back"></view>
+          <view class="flex-1">
+            <view class="text-34 font-bold" :class="[question.isCorrect ? 'text-[#2CC6A0]' : 'text-[#FF5B5C]']">
+              {{ question.answers.join('') || (question.isNotKnow ? '不会' : '未答') }}
+            </view>
+            <view class="mt-4 text-26">我的答案</view>
+          </view>
+        </view>
+      </template>
+      <view v-if="(readonly && question.parse) || (!readonly && question.showParse)" class="mt-40">
         <view class="text-30 text-fore-title font-bold">解析</view>
         <view class="mt-10 text-26 text-fore-light">
           <uv-parse :content="question.parse || '暂无解析'"></uv-parse>
@@ -53,9 +72,20 @@
       </view>
     </view>
     <view v-if="question.subQuestions.length" class="is-sub-question">
-      <question-item :question="subQuestion" :readonly="readonly" v-for="(subQuestion, index) in question.subQuestions"
-        :key="subQuestion.id" :is-sub-question="true" :index="index" :total="question.subQuestions.length"
-        @update:question="handleSubQuestionUpdate" @select="handleSelectOption" @notKnow="handleSelectNotKnow" />
+      <scroll-view class="w-full h-fit sticky top-0 bg-white py-10 z-1" scroll-x>
+        <view class="flex items-center px-20 gap-x-20">
+          <view class="px-40 py-8 rounded-full"
+            :class="[subIndex === subQuestionIndex ? 'bg-[#EBF9FF] text-primary font-bold' : 'bg-back']"
+            v-for="(subQuestion, subIndex) in question.subQuestions" @click="handleSubQuestionClick(subIndex)">
+            {{ index + subIndex + 1 }}
+          </view>
+        </view>
+      </scroll-view>
+      <view v-if="subQuestion">
+        <question-item :question="subQuestion" :readonly="readonly" :is-sub-question="true" :index="index"
+          :total="question.subQuestions.length" @update:question="handleSubQuestionUpdate" @select="handleSelectOption"
+          @notKnow="handleSelectNotKnow" />
+      </view>
     </view>
   </view>
 </template>
@@ -70,19 +100,28 @@ const props = defineProps<{
   question: Study.Question;
   readonly?: boolean;
   isSubQuestion?: boolean;
-  index?: number;
+  index: number;
   total?: number;
+  subQuestionIndex?: number;
 }>();
 const nextQuestion = inject(NEXT_QUESTION);
 const prevQuestion = inject(PREV_QUESTION);
 const nextQuestionQuickly = inject(NEXT_QUESTION_QUICKLY);
 const prevQuestionQuickly = inject(PREV_QUESTION_QUICKLY);
+const subQuestion = computed(() => {
+  return props.question.subQuestions[props.subQuestionIndex ?? 0];
+});
 const emit = defineEmits<{
   (e: 'update:question', question: Study.Question): void;
   (e: 'select', question: Study.Question): void;
   (e: 'notKnow', question: Study.Question): void;
   (e: 'scrollTo', selector: string): void;
+  (e: 'changeSubQuestion', index: number): void;
+  (e: 'selectSubQuestion', index: number): void;
 }>();
+const isOnlySubjective = computed(() => {
+  return props.question.typeId === EnumQuestionType.SUBJECTIVE;
+});
 const getStyleClass = (option: Study.QuestionOption) => {
   if (!props.readonly) {
     return isSelected(option) ? 'question-option-selected' : '';
@@ -129,12 +168,18 @@ const isOptionIncorrect = (option: Study.QuestionOption) => {
 const getQuestionTitle = () => {
   if (props.isSubQuestion) {
     const prefix = questionTypeDesc[props.question.typeId as EnumQuestionType].slice(0, 2);
-    const qsOrder = props.total && props.total > 1 ? `${props.index! + 1}.` : '';
-    return `[${prefix}] ${qsOrder} ${props.question.title}`;
+    return `[${prefix}]`;
   }
-  return props.question.title;
+  return '';
 };
-
+const getOptionContent = (option: Study.QuestionOption) => {
+  // sb 问题,浪费几个小时
+  return option.name.replace(/\s/g, ' ');
+}
+const handleShowParse = () => {
+  props.question.showParse = !props.question.showParse;
+  emit('update:question', props.question);
+}
 const handleNotKnow = () => {
   props.question.answers = [];
   props.question.isNotKnow = !props.question.isNotKnow;
@@ -183,6 +228,9 @@ const handleSelect = (option: Study.QuestionOption) => {
     changeQuestion();
   }
 }
+const handleSubQuestionClick = (index: number) => {
+  emit('changeSubQuestion', index);
+}
 const handleSubQuestionUpdate = (question: Study.Question) => {
   emit('update:question', question);
   checkIsDone();
@@ -206,10 +254,14 @@ const handleSelectNotKnow = () => {
 // 查找是否有子题没有做,有的话就自动滚动到目标处
 const findNotDoneSubQuestion = () => {
   const notDoneSubQuestion = props.question.subQuestions.find(q => !q.isDone);
+  console.log(notDoneSubQuestion)
   if (notDoneSubQuestion) {
     const selector = `qs_${notDoneSubQuestion.id}`;
     // 通知父组件滚动
-    emit('scrollTo', selector);
+    // emit('scrollTo', selector);
+    if (notDoneSubQuestion.subIndex !== undefined) {
+      emit('changeSubQuestion', notDoneSubQuestion.subIndex);
+    }
   } else {
     // 否则切换到下一题
     changeQuestion();
@@ -284,7 +336,7 @@ const isSelected = (option: Study.QuestionOption) => {
   }
 
   .question-content {
-    @apply text-32 text-fore-title;
+    @apply text-32 text-fore-title break-words;
   }
 
   .question-options {
@@ -297,7 +349,7 @@ const isSelected = (option: Study.QuestionOption) => {
       }
 
       .question-option-content {
-        @apply text-28 text-fore-title ml-20;
+        @apply text-28 text-fore-title ml-20 flex-1 min-w-0;
       }
     }
 

+ 8 - 2
src/pagesStudy/pages/exam-start/components/question-wrap.vue

@@ -2,8 +2,9 @@
   <scroll-view v-if="visible" scroll-y class="question-wrap" :scroll-into-view="scrollIntoView"
     :scroll-with-animation="true">
     <view class="h-20"></view>
-    <question-item :question="question" :readonly="readonly" @update:question="emit('update:question', $event)"
-      @scrollTo="handleScrollTo" />
+    <question-item :question="question" :readonly="readonly" :index="index" :subQuestionIndex="subQuestionIndex"
+      @update:question="emit('update:question', $event)" @scrollTo="handleScrollTo"
+      @changeSubQuestion="handleChangeSubQuestion" />
     <view class="h-20"></view>
   </scroll-view>
 </template>
@@ -16,13 +17,18 @@ const props = defineProps<{
   readonly?: boolean;
   currentIndex: number;
   index: number;
+  subQuestionIndex: number;
 }>();
 const visible = computed(() => {
   return Math.abs(props.currentIndex - props.index) <= 2;
 });
 const emit = defineEmits<{
   (e: 'update:question', question: Study.Question): void;
+  (e: 'changeSubQuestion', index: number): void;
 }>();
+const handleChangeSubQuestion = (index: number) => {
+  emit('changeSubQuestion', index);
+}
 const scrollIntoView = ref('');
 const handleScrollTo = (selector: string) => {
   console.log('收到子组件滚动请求', selector)

+ 34 - 14
src/pagesStudy/pages/exam-start/exam-start.vue

@@ -12,8 +12,8 @@
       <view 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>
+          <text class="text-34 text-primary font-bold">{{ virtualCurrentIndex + 1 }}</text>/
+          <text class="text-28 text-fore-subtitle">{{ virtualTotalCount }}</text>
         </view>
       </view>
       <view class="flex-1 min-h-1 relative">
@@ -23,8 +23,9 @@
             @animationfinish="handleSwiperAnimationFinish">
             <block v-for="(item, index) in questionList" :key="item.id">
               <swiper-item class="h-full" v-show="Math.abs(currentIndex - index) <= 2">
-                <question-wrap :question="item" :currentIndex="currentIndex" :index="index" :readonly="isReadOnly"
-                  @update:question="handleUpdateQuestion" />
+                <question-wrap :question="item" :currentIndex="currentIndex" :index="index"
+                  :subQuestionIndex="subQuestionIndex" :readonly="isReadOnly" @update:question="handleUpdateQuestion"
+                  @changeSubQuestion="handleChangeSubQuestion" />
               </swiper-item>
             </block>
           </swiper>
@@ -64,10 +65,12 @@
         <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-30 p-30">
+              <view class="h-70 bg-back px-20 leading-70 text-fore-subcontent sticky top-0 z-1">
+                {{ questionTypeDesc[item.type] }}
+              </view>
+              <view class="grid grid-cols-5 place-items-center gap-x-20 gap-y-30 p-30 relative z-0">
                 <view v-for="(qs, j) in item.list" :key="j" class="aspect-square flex items-center justify-center"
-                  @click="hanadleNavigate(qs.index)">
+                  @click="hanadleNavigate(qs.question, qs.index)">
                   <view
                     class="w-74 h-74 rounded-full flex items-center justify-center bg-white border border-solid border-border relative"
                     :class="{
@@ -80,7 +83,8 @@
                     <text class="z-1 font-bold text-32">{{ 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 v-if="!isReadOnly && !qs.question.isNotKnow" :progress="qs.question.progress || 0" />
+                    <!-- <question-progress v-if="!isReadOnly && !qs.question.isNotKnow"
+                      :progress="qs.question.progress || 0" /> -->
                   </view>
                 </view>
               </view>
@@ -120,8 +124,10 @@ import { NEXT_QUESTION, PREV_QUESTION, NEXT_QUESTION_QUICKLY, PREV_QUESTION_QUIC
 const userStore = useUserStore();
 // import { Examinee, ExamPaper, ExamPaperSubmit } from '@/types/study';
 const { prevData, transferBack } = useTransferPage();
-const { setQuestionList, questionList, groupedQuestionList, questionTypeDesc, currentIndex,
-  totalCount, doneCount, notDoneCount, isAllDone, nextQuestion, prevQuestion, nextQuestionQuickly, prevQuestionQuickly, swiperDuration,
+const { setQuestionList, questionList, groupedQuestionList, questionTypeDesc,
+  currentIndex, totalCount, virtualCurrentIndex, virtualTotalCount, subQuestionIndex, setSubQuestionIndex,
+  doneCount,
+  notDoneCount, isAllDone, nextQuestion, prevQuestion, nextQuestionQuickly, prevQuestionQuickly, swiperDuration,
   formatPracticeDuration, practiceDuration, startPracticeDuration, stopPracticeDuration, changeIndex, setDuration, reset } = useExam();
 
 provide(NEXT_QUESTION, nextQuestion);
@@ -210,10 +216,21 @@ const beforeQuit = () => {
   });
 };
 const currentQuestion = computed(() => {
-  return questionList.value[currentIndex.value] || {};
+  const qs = questionList.value[currentIndex.value];
+  if (qs.subQuestions && qs.subQuestions.length > 0) {
+    return qs.subQuestions[subQuestionIndex.value] || {};
+  }
+  return qs || {};
 });
-const hanadleNavigate = (index: number) => {
-  changeIndex(index);
+const hanadleNavigate = (question: Study.Question, index: number) => {
+  if (question.isSubQuestion) {
+    changeIndex(question.parentIndex || 0);
+    setTimeout(() => {
+      setSubQuestionIndex(question.subIndex || 0);
+    });
+  } else {
+    changeIndex(index);
+  }
 }
 /// 问题纠错
 const questionCorrectPopupRef = ref();
@@ -369,6 +386,10 @@ const handleUpdateQuestion = (question: Study.Question) => {
     autoSubmit();
   }
 }
+const handleChangeSubQuestion = (index: number) => {
+  console.log(index, 123)
+  setSubQuestionIndex(index);
+}
 /**
  * 恢复上次做题历史数据
  * @param savedQuestion 上次做题历史数据
@@ -462,7 +483,6 @@ const combinePaperData = async (examinee: Study.Examinee, paperType: EnumPaperTy
         startTime();
       }
     }
-    console.log(isReadOnly.value)
   }
 }
 const handleSwiperTipNext = () => {

+ 0 - 6
src/pagesStudy/pages/index/index.vue

@@ -41,12 +41,6 @@
             mode="aspectFill" />
         </view>
         <view class="bg-gradient-to-r from-[#32B5FD] to-[#79DCFD] flex-1 rounded-15 relative overflow-hidden">
-          <!-- <view class="mx-30 h-70 z-1 relative flex items-center gap-x-6" @click="handleSetting">
-            <view class="w-fit ellipsis-1 text-[#F59E0B]">
-              <text class="text-24 ">{{ firstDirectedSchool.universityName || '请选择定向院校' }}</text>
-            </view>
-            <ie-image src="/pagesStudy/static/image/icon-edit-pen.png" custom-class="w-28 h-28" mode="aspectFill" />
-          </view> -->
           <view class="mt-30 p-30 z-1 relative">
             <view class="text-30 text-white font-bold">定向刷题</view>
             <view class="mt-8 text-24 text-white">紧扣考纲,精准练习</view>

+ 107 - 0
src/pagesStudy/pages/knowledge-practice-detail/knowledge-practice-detail.vue

@@ -0,0 +1,107 @@
+<template>
+  <ie-page bg-color="#F6F8FA" :fix-height="true">
+    <ie-navbar :title="pageTitle" />
+    <view class="relative z-3 pt-30 pb-20 mx-30">
+      <view class="bg-white rounded-15 px-20 pb-1">
+        <rate-chart :value="rightRate" />
+        <view class="h-1 bg-[#E6E6E6] my-20"></view>
+        <view>
+          <view class="my-20 flex items-center justify-between text-24">
+            <ie-image src="/pagesStudy/static/image/icon-house.png" custom-class="w-24 h-24" mode="aspectFill" />
+            <text class="ml-10 text-fore-light flex-1">练习科目</text>
+            <text class="text-fore-title">{{ examineeData.collegeName }}-{{ examineeData.majorName }}</text>
+          </view>
+          <view class="my-20 flex items-center justify-between text-24">
+            <ie-image src="/pagesStudy/static/image/icon-group.png" custom-class="w-24 h-24" mode="aspectFill" />
+            <text class="ml-10 text-fore-light flex-1">练习知识点</text>
+            <text class="text-fore-title">{{ examineeData.subjectName || '-' }}</text>
+          </view>
+          <view class="my-20 flex items-center justify-between text-24">
+            <ie-image src="/pagesStudy/static/image/icon-clock.png" custom-class="w-24 h-24" mode="aspectFill" />
+            <text class="ml-10 text-fore-light flex-1">练习时长</text>
+            <text class="text-fore-title">{{ formatTime(examineeData.duration) || '-' }}</text>
+          </view>
+        </view>
+      </view>
+      <exam-stat :data="examineeData" :show-stats="false" />
+    </view>
+    <ie-safe-toolbar :height="84" :shadow="false">
+      <view class="h-[84px] px-46 bg-white flex items-center justify-between gap-x-40">
+        <!-- <view class="flex flex-col items-center justify-center">
+          <uv-icon name="clock" size="22" color="#31A0FC"></uv-icon>
+          <view class="text-28">记录</view>
+        </view> -->
+        <view class="text-30 text-primary bg-back flex-1 py-22 rounded-full text-center" @click="handleStartPractice">
+          继续刷题
+        </view>
+        <view class="text-30 text-white bg-primary flex-1 py-22 rounded-full text-center" @click="handleViewAnalysis">
+          查看解析
+        </view>
+      </view>
+    </ie-safe-toolbar>
+  </ie-page>
+</template>
+<script lang="ts" setup>
+import RateChart from '@/pagesStudy/pages/simulation-analysis/components/rate-chart.vue';
+import ExamStat from '@/pagesStudy/pages/simulation-analysis/components/exam-stat.vue';
+import { useNavbar } from '@/hooks/useNavbar';
+import { useTransferPage } from '@/hooks/useTransferPage';
+import { Study } from '@/types';
+import { EnumPaperType } from '@/common/enum';
+const { prevData, transferTo } = useTransferPage();
+const { baseStickyTop } = useNavbar();
+
+const rightRate = computed(() => {
+  const { totalCount = 0, wrongCount = 0 } = examineeData.value;
+  return Math.round((totalCount - wrongCount) / totalCount * 100) || 0;
+});
+
+const examineeData = ref<Study.Examinee>({} as Study.Examinee);
+const pageTitle = computed(() => {
+  return prevData.value.isDirected ? '定向练习结果' : '全量练习结果';
+});
+const formatTime = (time: number) => {
+  if (!time) {
+    return '';
+  }
+  const hours = Math.floor(time / 3600);
+  const minutes = Math.floor((time % 3600) / 60);
+  const seconds = time % 60;
+
+  if (hours >= 1) {
+    return `${hours}时${minutes}分${seconds}秒`;
+  } else {
+    return `${minutes}分${seconds}秒`;
+  }
+};
+const handleStartPractice = () => {
+  transferTo('/pagesStudy/pages/exam-start/exam-start', {
+    data: {
+      name: '知识点练习-' + examineeData.value.name,
+      paperType: EnumPaperType.PRACTICE,
+    }
+  });
+}
+const handleViewAnalysis = () => {
+  // transferTo('/pagesStudy/pages/simulation-analysis/simulation-analysis', {
+  //   data: {
+  //     examineeId: examineeData.value.id
+  //   }
+  // });
+}
+const loadData = async () => {
+  // uni.$ie.showLoading();
+  try {
+    // const res = await getKnowledgePracticeHistory();
+    // console.log(res);
+  } finally {
+    // setTimeout(() => {
+    //   uni.$ie.hideLoading();
+    // }, 1000);
+  }
+}
+onLoad(() => {
+  loadData();
+});
+</script>
+<style lang="scss" scoped></style>

+ 55 - 0
src/pagesStudy/pages/knowledge-practice-history/knowledge-practice-history.vue

@@ -0,0 +1,55 @@
+<template>
+  <ie-page bg-color="#F6F8FA" :fix-height="true">
+    <ie-navbar title="刷题记录" />
+    <view class="mt-20">
+      <view v-for="value in historyList" :key="value.id"
+        class="bg-white px-40 py-30 flex items-center sibling-border-top">
+        <view class="flex-1">
+          <view class="text-28">
+            <text class=" text-fore-light">知识点:</text>
+            <text class="text-fore-title">{{ value.name }}</text>
+          </view>
+          <view class="mt-10 text-28">
+            <text class=" text-fore-light">完成时间:</text>
+            <text class="text-fore-title">{{ value.time }}</text>
+          </view>
+        </view>
+        <view class="text-24 text-white bg-primary w-fit px-40 py-12 rounded-full text-center"
+          @click="handleViewHistory(value)">
+          查看
+        </view>
+      </view>
+    </view>
+  </ie-page>
+</template>
+<script lang="ts" setup>
+import IePage from '@/components/ie-page/ie-page.vue';
+import { useNavbar } from '@/hooks/useNavbar';
+import { useTransferPage } from '@/hooks/useTransferPage';
+const { transferTo } = useTransferPage();
+const { baseStickyTop } = useNavbar();
+const historyList = ref<any[]>([{ id: 1, name: '知识点1', time: '2021-01-01' }, { id: 2, name: '知识点2', time: '2021-01-02' }]);
+const handleViewHistory = (value: any) => {
+  transferTo('/pagesStudy/pages/knowledge-practice-detail/knowledge-practice-detail', {
+    data: {
+      id: value.id,
+      isDirected: value.isDirected
+    }
+  });
+}
+const loadData = async () => {
+  uni.$ie.showLoading();
+  try {
+    // const res = await getKnowledgePracticeHistory();
+    // console.log(res);
+  } finally {
+    setTimeout(() => {
+      uni.$ie.hideLoading();
+    }, 400);
+  }
+}
+onLoad(() => {
+  loadData();
+});
+</script>
+<style lang="scss" scoped></style>

+ 10 - 2
src/pagesStudy/pages/knowledge-practice/knowledge-practice.vue

@@ -2,7 +2,13 @@
   <ie-page ref="iePageRef">
     <ie-navbar :title="pageTitle" />
     <uv-tabs :list="subjectList" key-name="subjectName" @click="handleChangeTab" :scrollable="true"></uv-tabs>
-    <view class="h-16 bg-back"></view>
+    <view class="px-30 py-16 bg-back">
+      <view class="flex items-center justify-end gap-x-4" @click="handleViewHistory">
+        <uv-icon name="clock" size="17" color="#31A0FC"></uv-icon>
+        <text class="text-30 text-primary">查看记录</text>
+        <uv-icon name="arrow-right" size="16" color="#31A0FC"></uv-icon>
+      </view>
+    </view>
     <view class="px-40">
       <knowledgeTree :tree-data="treeData" @start-practice="handleStartPractice" />
     </view>
@@ -39,7 +45,9 @@ const handleChangeTab = (item: any) => {
   console.log(item)
   currentSubjectIndex.value = item.index;
 }
-
+const handleViewHistory = () => {
+  transferTo('/pagesStudy/pages/knowledge-practice-history/knowledge-practice-history');
+}
 const loadKnowledgeList = async () => {
   if (!currentSubjectId.value) {
     return;

+ 4 - 3
src/pagesStudy/pages/simulation-analysis/components/exam-stat.vue

@@ -1,7 +1,7 @@
 <template>
-  <view class="px-16 py-20 bg-white rounded-15 mt-20">
+  <view class="px-16 py-26 bg-white rounded-15 mt-20">
     <view class="text-30 text-fore-title font-bold">答题情况</view>
-    <view class="mt-20 flex items-center">
+    <view v-if="showStats" class="mt-20 flex items-center">
       <view class="flex-1 text-center">
         <view class="text-40 text-fore-title font-bold">{{ totalQuestions }}</view>
         <view class="mt-5 text-28 text-fore-light">题量</view>
@@ -19,7 +19,7 @@
         <view class="mt-5 text-28 text-fore-light">正确率</view>
       </view>
     </view>
-    <view class="h-1 bg-[#E6E6E6] my-20"></view>
+    <view v-if="showStats" class="h-1 bg-[#E6E6E6] my-20"></view>
     <view class="mt-40 flex items-center justify-end gap-x-30">
       <view class="flex items-center gap-x-12" :class="{ 'is-filter': !filterTypes.includes('correct') }"
         @click="handleFilter('correct')">
@@ -53,6 +53,7 @@ import { Study } from '@/types';
 const { transferTo } = useTransferPage();
 const props = defineProps<{
   data: Study.Examinee;
+  showStats: boolean;
 }>();
 const paperInfo = computed(() => {
   return props.data.paperInfo || {};

+ 1 - 1
src/pagesStudy/pages/simulation-analysis/simulation-analysis.vue

@@ -27,7 +27,7 @@
             </view>
           </view>
         </view>
-        <exam-stat :data="examineeData" />
+        <exam-stat :data="examineeData" :show-stats="true" />
         <score-stat :data="examineeData" />
       </view>
     </view>

+ 7 - 0
src/types/study.ts

@@ -203,6 +203,13 @@ export interface Question extends QuestionState {
   answer2: string;
   parse?: string;
   subQuestions: Question[];
+  //
+  parentIndex?: number; // 父题索引
+  parentId?: number; // 父题ID
+  parentTypeId?: number; // 父题类型
+  isSubQuestion?: boolean; // 是否是子题
+  subIndex?: number; // 子题索引
+  showParse?: boolean; // 是否显示解析
 }
 
 export interface SubjectListRequestDTO {

+ 4 - 2
src/uni_modules/uv-parse/components/uv-parse/uv-parse.vue

@@ -2,7 +2,7 @@
   <view id="_root" :class="(selectable?'_select ':'')+'_root'" :style="containerStyle">
     <slot v-if="!nodes[0]" />
     <!-- #ifndef APP-PLUS-NVUE -->
-    <node v-else :childs="nodes" :opts="[lazyLoad,loadingImg,errorImg,showImgMenu,selectable]" name="span" />
+    <node v-else :childs="nodes" :opts="[lazyLoad,loadingImg,errorImg,showImgMenu,selectable]" name="span" :style="contentStyle" />
     <!-- #endif -->
     <!-- #ifdef APP-PLUS-NVUE -->
     <web-view ref="web" src="/uni_modules/uv-parse/static/app-plus/uv-parse/local.html" :style="'margin-top:-2px;height:' + height + 'px'" @onPostMessage="_onMessage" />
@@ -100,7 +100,8 @@ export default {
       default: true
     },
     tagStyle: Object,
-    useAnchor: [Boolean, Number]
+    useAnchor: [Boolean, Number],
+    contentStyle: String
   },
   // #ifdef VUE3
   emits: ['load', 'ready', 'imgtap', 'linktap', 'play', 'error'],
@@ -488,6 +489,7 @@ export default {
   overflow-x: auto;
   overflow-y: hidden;
   -webkit-overflow-scrolling: touch;
+  /* word-break: break-word; */
 }
 
 /* 长按复制 */