shmily1213 1 viikko sitten
vanhempi
commit
410a622284
70 muutettua tiedostoa jossa 2524 lisäystä ja 1157 poistoa
  1. 1 33
      index.html
  2. 1 0
      package.json
  3. 17 0
      src/App.vue
  4. 1 1
      src/api/flyio.ts
  5. 11 0
      src/common/enum.ts
  6. 37 27
      src/common/modules/mx-block-index-all-test-config.js
  7. 1 1
      src/common/mxConst.js
  8. 38 23
      src/components/ie-button/ie-button.vue
  9. 6 2
      src/components/ie-popup-toolbar/ie-popup-toolbar.vue
  10. 64 0
      src/components/ie-popup/ie-popup.vue
  11. 3 0
      src/components/ie-sms/ie-captcha.vue
  12. 8 2
      src/components/mx-tabs-swiper/mx-tabs-swiper.vue
  13. 183 60
      src/composables/useExam.ts
  14. 2 0
      src/composables/useExamType.ts
  15. 1 1
      src/config.ts
  16. 0 1
      src/hooks/useTransferPage.ts
  17. 11 0
      src/main.ts
  18. 53 47
      src/pagesMain/pages/index/components/index-banner.vue
  19. 3 3
      src/pagesMain/pages/me/components/me-info.vue
  20. 6 2
      src/pagesMain/pages/splash/splash.vue
  21. 13 13
      src/pagesMain/pages/volunteer/volunteer.vue
  22. 1 1
      src/pagesOther/pages/college-library/components/college-item.vue
  23. 61 0
      src/pagesOther/pages/topic-center/wrong-book/components/datetime-picker.vue
  24. 30 19
      src/pagesOther/pages/topic-center/wrong-book/components/wrong-book-list.vue
  25. 27 13
      src/pagesOther/pages/topic-center/wrong-book/wrong-book.vue
  26. 3 2
      src/pagesStudy/components/exam-record-item.vue
  27. 27 2
      src/pagesStudy/components/knowledge-tree-node.vue
  28. 107 0
      src/pagesStudy/pages/exam-start/components/exam-mode.vue
  29. 60 0
      src/pagesStudy/pages/exam-start/components/exam-navbar.vue
  30. 225 0
      src/pagesStudy/pages/exam-start/components/exam-stats-card.vue
  31. 27 0
      src/pagesStudy/pages/exam-start/components/exam-subtitle.vue
  32. 136 0
      src/pagesStudy/pages/exam-start/components/exam-swiper.vue
  33. 92 0
      src/pagesStudy/pages/exam-start/components/exam-toolbar.vue
  34. 429 0
      src/pagesStudy/pages/exam-start/components/question-item copy.vue
  35. 24 411
      src/pagesStudy/pages/exam-start/components/question-item.vue
  36. 273 0
      src/pagesStudy/pages/exam-start/components/question-options.vue
  37. 45 0
      src/pagesStudy/pages/exam-start/components/question-parse.vue
  38. 53 0
      src/pagesStudy/pages/exam-start/components/question-result.vue
  39. 64 0
      src/pagesStudy/pages/exam-start/components/question-title.vue
  40. 2 13
      src/pagesStudy/pages/exam-start/components/question-wrap.vue
  41. 103 345
      src/pagesStudy/pages/exam-start/exam-start.vue
  42. 1 3
      src/pagesStudy/pages/index/compoentns/index-banner.vue
  43. 11 14
      src/pagesStudy/pages/index/index.vue
  44. 29 16
      src/pagesStudy/pages/knowledge-practice-detail/knowledge-practice-detail.vue
  45. 1 1
      src/pagesStudy/pages/knowledge-practice-history/knowledge-practice-history.vue
  46. 8 3
      src/pagesStudy/pages/knowledge-practice/knowledge-practice.vue
  47. 1 2
      src/pagesStudy/pages/simulation-analysis/components/exam-stat.vue
  48. 7 4
      src/pagesStudy/pages/simulation-analysis/simulation-analysis.vue
  49. 7 3
      src/pagesStudy/pages/simulation-start/simulation-start.vue
  50. 7 1
      src/pagesStudy/pages/study-history/components/knowledge-history-student.vue
  51. 24 6
      src/pagesStudy/pages/targeted-add/targeted-add.vue
  52. 16 38
      src/pagesStudy/pages/targeted-setting/targeted-setting.vue
  53. BIN
      src/pagesStudy/static/image/icon-more.png
  54. 3 3
      src/pagesSystem/pages/bind-profile/bind-profile.vue
  55. 3 2
      src/pagesSystem/pages/school-select/school-select.vue
  56. 31 0
      src/preload.js
  57. 40 13
      src/store/userStore.ts
  58. 5 2
      src/types/index.ts
  59. 5 11
      src/types/injectionSymbols.ts
  60. 24 5
      src/types/study.ts
  61. 33 1
      src/types/transfer.ts
  62. 3 1
      src/types/user.ts
  63. 5 1
      src/uni_modules/uni-calendar/components/uni-calendar/uni-calendar-item.vue
  64. 5 1
      src/uni_modules/uni-calendar/components/uni-calendar/uni-calendar.vue
  65. 1 1
      src/uni_modules/uv-cell/components/uv-cell/uv-cell.vue
  66. 1 1
      src/uni_modules/uv-icon/components/uv-icon/uv-icon.vue
  67. 1 0
      src/uni_modules/uv-image/components/uv-image/uv-image.vue
  68. 1 0
      src/uni_modules/uv-popup/components/uv-popup/uv-popup.vue
  69. 1 1
      src/utils/uni-tool.ts
  70. 1 1
      vite.config.js

+ 1 - 33
index.html

@@ -26,38 +26,6 @@
   <div id="app"><!--app-html--></div>
   <script type="module" src="/src/main.ts"></script>
 </body>
-<script>
-  document.addEventListener('UniAppJSBridgeReady', function () {
-    uni.webView.postMessage({
-      data: {
-        action: 'setPlatform'
-      }
-    });
-    window.backup = (from) => {
-      if (from === 'backbutton') {
-        const routes = getCurrentPages();
-        const route = routes[routes.length - 1].route;
-        if ([
-          'pagesMain/pages/index/index',
-          'pagesMain/pages/volunteer/volunteer',
-          'pagesMain/pages/me/me'
-        ].includes(route)) {
-          uni.webView.postMessage({
-            data: {
-              action: 'quit'
-            }
-          });
-        } else {
-          uni.navigateBack();
-        }
-      }
-    };
-    // 默认为h5平台
-    window.platform = 'h5';
-    window.setPlatform = (platform) => {
-      window.platform = platform;
-    };
-  });
-</script>
+<script></script>
 
 </html>

+ 1 - 0
package.json

@@ -6,6 +6,7 @@
     "dev:h5": "uni",
     "dev:mp-weixin": "uni -p mp-weixin",
     "build:custom": "uni build -p",
+    "build:h5": "uni build -p h5",
     "build": "uni build",
     "build:mp-weixin": "uni build -p mp-weixin"
   },

+ 17 - 0
src/App.vue

@@ -20,4 +20,21 @@ export default {
 @import "@/uni_modules/uv-ui-tools/index.scss";
 @import '@/static/theme/theme.module.scss';
 @import "@/static/common.scss";
+
+.uni-modal {
+  border-radius: 6px;
+}
+
+.uni-modal__title {
+  font-weight: bold;
+}
+
+.uni-modal__bd {
+  padding-top: 50rpx;
+  padding-bottom: 50rpx;
+}
+
+.uni-modal__btn {
+  font-size: 16px;
+}
 </style>

+ 1 - 1
src/api/flyio.ts

@@ -6,7 +6,7 @@ const { serverBaseUrl } = config;
 console.log(serverBaseUrl)
 const requestConfig = {
   baseURL: serverBaseUrl,
-  timeout: 10000,
+  timeout: 30000,
   headers: {
     "Content-Type": "application/json",
   },

+ 11 - 0
src/common/enum.ts

@@ -242,4 +242,15 @@ export enum EnumPaperType {
    * 考试
    */
   SIMULATED = 'Simulated'
+}
+
+export enum EnumReviewMode {
+  /**
+   * 交卷后评卷
+   */
+  AFTER_SUBMIT = 1,
+  /**
+   * 答完一题就评卷
+   */
+  DURING_ANSWER = 2
 }

+ 37 - 27
src/common/modules/mx-block-index-all-test-config.js

@@ -1,31 +1,41 @@
 import widgets from '@/common/mx-block-widgets.js'
 
 export default {
-	indexAllTestBlocks: [{
-		...widgets.electiveTest,
-		satisfyStoreGetters: ['false']
-	}, {
-		...widgets.careerTest,
-		satisfyStoreGetters: ['false']
-	}, {
-		...widgets.electiveGuide,
-		satisfyStoreGetters: ['false']
-	},{
-		...widgets.hollandGuide
-	}, {
-		...widgets.mbtiGuide
-	}, {
-		...widgets.multiwayGuide,
-		satisfyStoreGetters: ['false']
-	}, {
-		...widgets.mentalHealthGuide
-	},{
-		...widgets.psychologyTest,
-		satisfyStoreGetters: ['isSenior', 'false'],
-		satisfyAny: false
-	}, {
-		...widgets.studyTest,
-		satisfyStoreGetters: ['isSenior', 'false'],
-		satisfyAny: false
-	}]
+  indexAllTestBlocks: [
+    {
+      ...widgets.electiveTest,
+      satisfyStoreGetters: ['false']
+    },
+    {
+      ...widgets.careerTest,
+      satisfyStoreGetters: ['false']
+    },
+    {
+      ...widgets.electiveGuide,
+      satisfyStoreGetters: ['false']
+    },
+    {
+      ...widgets.hollandGuide
+    },
+    {
+      ...widgets.mbtiGuide
+    },
+    {
+      ...widgets.multiwayGuide,
+      satisfyStoreGetters: ['false']
+    },
+    // {
+    //   ...widgets.mentalHealthGuide
+    // },
+    {
+      ...widgets.psychologyTest,
+      satisfyStoreGetters: ['isSenior', 'false'],
+      satisfyAny: false
+    },
+    {
+      ...widgets.studyTest,
+      satisfyStoreGetters: ['isSenior', 'false'],
+      satisfyAny: false
+    }
+  ]
 }

+ 1 - 1
src/common/mxConst.js

@@ -12,7 +12,7 @@ const consts = {
     keyCulturalExamType: '职高对口升学',
     propAppConfig: 'appConfig',
     routes: {
-        index: {url: '/pages/index/index', type: 'tab'},
+        index: {url: '/pagesMain/pages/index/index', type: 'tab'},
         portal: {url: '/pages/ie/portal', type: 'tab'},
         personalCenter: {url: '/pages/personal-center/index/index', type: 'tab'},
         login: '/pagesSystem/pages/login/login',

+ 38 - 23
src/components/ie-button/ie-button.vue

@@ -1,32 +1,36 @@
 <template>
   <button class="ie-button"
-    :class="['ie-button', `ie-button-${type}`, `ie-button-${size}`, customClass, { 'is-disabled': disabled }]"
-    :disabled="disabled" hover-class="button-hover" @click="handleClick">
+    :class="['ie-button', `ie-button-${type}`, `ie-button-${size}`, customClass, { 'is-disabled': disabled, 'has-shadow': hasShadow }]"
+    :disabled="disabled" hover-class="button-hover" :style="getStyle" @click="handleClick">
     <slot></slot>
   </button>
 </template>
 <script lang="ts" setup>
-import { PropType } from 'vue';
 
-const props = defineProps({
-  disabled: {
-    type: Boolean as PropType<boolean>,
-    default: false
-  },
-  type: {
-    type: String as PropType<'primary' | 'secondary' | 'info'>,
-    default: 'primary'
-  },
-  size: {
-    type: String as PropType<'large' | 'normal' | 'small' | 'mini'>,
-    default: 'large'
-  },
-  customClass: {
-    type: String as PropType<string>,
-    default: ''
+type Props = {
+  disabled: boolean;
+  type: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error';
+  size: 'large' | 'normal' | 'small' | 'mini';
+  customClass: string;
+  round: number;
+  shadow: boolean;
+}
+const props = withDefaults(defineProps<Props>(), {
+  disabled: false,
+  type: 'primary',
+  size: 'large',
+  customClass: '',
+  round: 999,
+  shadow: true
+});
+const hasShadow = computed(() => {
+  return props.shadow && props.type === 'primary';
+});
+const getStyle = computed(() => {
+  return {
+    borderRadius: props.round + 'px'
   }
 });
-
 const emit = defineEmits(['click']);
 const handleClick = () => {
   emit('click');
@@ -37,12 +41,15 @@ const handleClick = () => {
   height: fit-content !important;
   line-height: 1;
   font-weight: 800;
-  @apply relative rounded-full text-center;
+  @apply relative text-center;
+}
+
+.has-shadow {
+  box-shadow: 0 5px 8px 0px rgba(49, 160, 252, 0.6);
 }
 
 .ie-button-primary {
   background: linear-gradient(to right, #31A0FC, #0088FE);
-  box-shadow: 0 5px 8px 0px rgba(49, 160, 252, 0.6);
   @apply text-white
 }
 
@@ -50,6 +57,14 @@ const handleClick = () => {
   @apply bg-back text-fore-title;
 }
 
+.ie-button-success {
+  @apply bg-success text-white;
+}
+
+.ie-button-warning {
+  @apply bg-warning text-white;
+}
+
 .ie-button-large {
   @apply py-36 text-30;
 }
@@ -63,7 +78,7 @@ const handleClick = () => {
 }
 
 .ie-button-mini {
-  @apply py-20 text-24;
+  @apply py-22 text-24;
 }
 
 .ie-button::after {

+ 6 - 2
src/components/ie-popup-toolbar/ie-popup-toolbar.vue

@@ -1,8 +1,8 @@
 <template>
   <view class="flex items-center justify-between pt-20">
-    <view class="px-46 py-20 text-28 text-fore-light" @click="handleCancel">{{ cancelText }}</view>
+    <view v-if="showCancel" class="px-46 py-20 text-28 text-fore-light" @click="handleCancel">{{ cancelText }}</view>
     <text class="text-30 text-fore-title font-bold">{{ title }}</text>
-    <view class="px-46 py-20 text-28 text-fore-title" @click="handleConfirm">{{ confirmText }}</view>
+    <view v-if="showConfirm" class="px-46 py-20 text-28 text-fore-title" @click="handleConfirm">{{ confirmText }}</view>
   </view>
 </template>
 <script lang="ts" setup>
@@ -10,11 +10,15 @@ type ToolbarOption = {
   title: string;
   cancelText: string;
   confirmText: string;
+  showCancel: boolean;
+  showConfirm: boolean;
 }
 const props = withDefaults(defineProps<ToolbarOption>(), {
   title: '',
   cancelText: '取消',
   confirmText: '确认',
+  showCancel: true,
+  showConfirm: true,
 });
 const emit = defineEmits(['cancel', 'confirm']);
 const handleCancel = () => {

+ 64 - 0
src/components/ie-popup/ie-popup.vue

@@ -0,0 +1,64 @@
+<template>
+  <!-- #ifdef H5 -->
+  <teleport to="body">
+    <!-- #endif -->
+    <!-- #ifdef MP-WEIXIN -->
+    <root-portal externalClass="theme-ie">
+      <!-- #endif -->
+      <uv-popup ref="popupRef" :mode="mode" :round="round" popup-class="theme-ie"
+        :close-on-click-overlay="closeOnClickOverlay" @close="handleClose">
+        <ie-popup-toolbar :title="title" :cancelText="cancelText" :confirmText="confirmText" :showCancel="showCancel"
+          :showConfirm="showConfirm" @cancel="handleCancel" @confirm="handleConfirm" />
+        <view class="popup-content">
+          <slot></slot>
+        </view>
+      </uv-popup>
+      <!-- #ifdef MP-WEIXIN -->
+    </root-portal>
+    <!-- #endif -->
+    <!-- #ifdef H5 -->
+  </teleport>
+  <!-- #endif -->
+</template>
+<script lang="ts" setup>
+type Props = {
+  title: string;
+  cancelText: string;
+  confirmText: string;
+  mode: 'bottom' | 'center' | 'top';
+  round: number;
+  closeOnClickOverlay: boolean;
+  showCancel: boolean;
+  showConfirm: boolean;
+}
+const props = withDefaults(defineProps<Props>(), {
+  title: '',
+  cancelText: '取消',
+  confirmText: '确认',
+  mode: 'bottom',
+  round: 16,
+  closeOnClickOverlay: true,
+  showCancel: true,
+  showConfirm: true
+});
+const popupRef = ref();
+const emit = defineEmits(['cancel', 'confirm', 'close']);
+const handleClose = () => {
+  emit('close');
+}
+const handleCancel = () => {
+  emit('cancel');
+  close();
+}
+const handleConfirm = () => {
+  emit('confirm');
+}
+const open = () => {
+  popupRef.value.open();
+}
+const close = () => {
+  popupRef.value.close();
+}
+defineExpose({ open, close });
+</script>
+<style lang="scss" scoped></style>

+ 3 - 0
src/components/ie-sms/ie-captcha.vue

@@ -42,8 +42,11 @@ const debounceGetImage = useDebounce(getImage, 300);
 const open = async () => {
   codeModelValue.value = undefined;
   try {
+    uni.$ie.showLoading();
     await getImage();
+    uni.$ie.hideLoading();
   } catch (error) {
+    uni.$ie.hideLoading();
     uni.$ie.showToast('获取验证码失败');
   }
   popupRef.value.open();

+ 8 - 2
src/components/mx-tabs-swiper/mx-tabs-swiper.vue

@@ -1,6 +1,6 @@
 <template>
     <!-- NOTE:min-h-1在使用useElementSize时非常重要 -->
-    <view ref="container" class="fx-col flex-1 min-h-1">
+    <view ref="container" class="h-full fx-col flex-1 min-h-1">
         <uv-tabs :current="current" :list="tabs" :key-name="keyName" v-bind="tabBindings"
                  class="bg-white bd-b-1" @change="handleTabChange">
             <template v-if="$slots.tab" #default="scope">
@@ -8,7 +8,12 @@
             </template>
         </uv-tabs>
         <uv-line v-if="border"/>
-        <swiper :current="current" :style="{height: swiperHeight+'px'}" v-bind="swiperBindings"
+        <view>
+          <slot name="header"></slot>
+        </view>
+        <view class="flex-1 min-h-1">
+          <!-- :style="{height: swiperHeight+'px'}" -->
+          <swiper :current="current" class="h-full" v-bind="swiperBindings"
                 @change="handleSwiperChange">
             <swiper-item v-for="t in tabs" :key="t.name">
                 <!-- 延迟渲染 -->
@@ -20,6 +25,7 @@
                 <slot v-else :name="t.template||template||t.name" v-bind="t"/>
             </swiper-item>
         </swiper>
+        </view>
     </view>
 </template>
 

+ 183 - 60
src/composables/useExam.ts

@@ -1,4 +1,4 @@
-import { EnumQuestionType } from "@/common/enum";
+import { EnumQuestionType, EnumReviewMode } from "@/common/enum";
 import { getPaper } from '@/api/modules/study';
 import { Study } from "@/types";
 import { Question } from "@/types/study";
@@ -35,7 +35,11 @@ export const useExam = () => {
     const hours = Math.floor(practiceDuration.value / 3600);
     const minutes = Math.floor((practiceDuration.value % 3600) / 60);
     const seconds = practiceDuration.value % 60;
-    return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
+    if (hours > 0) {
+      return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
+    } else {
+      return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
+    }
   });
   // 考试时长
   const examDuration = ref<number>(0);
@@ -47,13 +51,20 @@ export const useExam = () => {
   });
   const swiperDuration = ref<number>(300);
   const questionList = ref<Study.Question[]>([]);
+  // 练习设置
+  const practiceSettings = ref<Study.PracticeSettings>({
+    reviewMode: EnumReviewMode.AFTER_SUBMIT,
+    autoNext: false
+  });
   // 收藏列表
   const favoriteList = ref<Study.Question[]>([]);
   // 不会列表
   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) {
@@ -62,41 +73,53 @@ export const useExam = () => {
       return acc + 1;
     }, 0);
   });
-  const subQuestionIndex = ref<number>(0);
   // 包含状态的问题列表
   const stateQuestionList = computed(() => {
-    function parseQuestion(qs: Study.Question) {
+    const parseQuestion = (qs: Study.Question) => {
       if (qs.subQuestions && qs.subQuestions.length > 0) {
-        qs.subQuestions.forEach((item, index) => {
-          item.isDone = isDone(item);
+        qs.isLeaf = false;
+        qs.subQuestions.forEach((subQs, index) => {
+          subQs.isDone = isDone(subQs);
+          subQs.isCorrect = isQuestionCorrect(subQs);
+          subQs.isNotAnswer = isQuestionNotAnswer(subQs);
+          subQs.isLeaf = true;
+          subQs.options.forEach(option => {
+            option.isCorrect = isOptionCorrect(subQs, option);
+            option.isSelected = isOptionSelected(subQs, option);
+            option.isMissed = !option.isSelected && option.isCorrect;
+            option.isIncorrect = !option.isCorrect && option.isSelected;
+          });
         });
       } else {
         qs.isSubQuestion = false;
+        qs.isDone = isDone(qs);
+        qs.isCorrect = isQuestionCorrect(qs);
+        qs.isNotAnswer = isQuestionNotAnswer(qs);
+        qs.isLeaf = true;
+        qs.options.forEach(option => {
+          option.isCorrect = isOptionCorrect(qs, option);
+          option.isSelected = isOptionSelected(qs, option);
+          option.isMissed = !option.isSelected && option.isCorrect;
+          option.isIncorrect = !option.isCorrect && option.isSelected;
+        });
       }
-      return {
-        ...qs,
-        isDone: isDone(qs)
-      }
+      return qs;
     }
-    console.log('重新计算')
     return questionList.value.map((item, index) => {
       return parseQuestion(item)
-      // return {
-      //   ...item,
-      //   // isDone: isDone(item)
-      // };
     });
   });
+  /// 扁平化题目列表,用于答题卡
   const flatQuestionList = computed(() => {
-    return questionList.value.flatMap(item => {
+    return stateQuestionList.value.flatMap(item => {
       if (item.subQuestions && item.subQuestions.length > 0) {
         return item.subQuestions.flat();
       }
       return item;
     });
   });
+  /// 按照题型分组,用于答题卡
   const groupedQuestionList = computed(() => {
-    // 状态:已做、未做、是否不会、是否标记,整体按照题型分组
     const state = questionTypeOrder.map(type => {
       return {
         type,
@@ -106,7 +129,6 @@ export const useExam = () => {
         }[]
       }
     });
-    const arr: Study.Question[] = [];
     flatQuestionList.value.forEach((qs, index) => {
       let group;
       if (qs.isSubQuestion) {
@@ -129,22 +151,37 @@ export const useExam = () => {
         });
       }
     });
-    console.log('group data', arr)
     return state;
   });
+  /// 是否全部做完
   const isAllDone = computed(() => {
-    // return questionList.value.every(q => isDone(q));
     return doneCount.value === virtualTotalCount.value;
   });
   // 当前下标
   const currentIndex = ref<number>(0);
+  // 子题下标
+  const subQuestionIndex = ref<number>(0);
+  // 当前题目
+  const currentQuestion = computed(() => {
+    return questionList.value[currentIndex.value];
+  });
+  // 当前子题
+  const currentSubQuestion = computed(() => {
+    if (currentQuestion.value.subQuestions.length === 0) {
+      return null;
+    }
+    if (subQuestionIndex.value >= currentQuestion.value.subQuestions.length) {
+      return null;
+    }
+    return currentQuestion.value.subQuestions[subQuestionIndex.value];
+  });
+  /// 总题量,不区分子题,等同于接口返回的题目列表
   const totalCount = computed(() => {
     return questionList.value.length;
   });
+  /// 已做题的数量 --> stateQuestionList
   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;
     let count = 0;
     for (let i = 0; i <= stateQuestionList.value.length - 1; i++) {
       const qs = stateQuestionList.value[i];
@@ -162,23 +199,26 @@ export const useExam = () => {
     }
     return count;
   });
+  /// 未做题的数量
   const notDoneCount = computed(() => {
     return virtualTotalCount.value - doneCount.value;
   });
-  // 包含子题的题目计算整体做题进度
+  /// 包含子题的题目计算整体做题进度
   const calcProgress = (qs: Study.Question): number => {
     if (qs.subQuestions && qs.subQuestions.length > 0) {
       return qs.subQuestions.reduce((acc, q) => acc + calcProgress(q), 0) / qs.subQuestions.length;
     }
     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));
-    // }
-    return 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.filter(item => !!item).length > 0) || !!qs.isNotKnow;
   }
-  const isQuestionCorrect = (qs: Study.ExamineeQuestion): boolean => {
+  /// 题目是否正确
+  const isQuestionCorrect = (qs: Study.Question): boolean => {
     let { answers, answer1, answer2, typeId } = qs;
     answers = answers?.filter(item => !!item) || [];
     answer1 = answer1 || '';
@@ -196,29 +236,93 @@ export const useExam = () => {
     }
     return false;
   };
+  /// 题目是否未作答
+  const isQuestionNotAnswer = (qs: Study.Question): boolean => {
+    if (qs.subQuestions && qs.subQuestions.length > 0) {
+      return qs.subQuestions.every(q => isQuestionNotAnswer(q));
+    }
+    return !qs.answers || qs.answers.length === 0;
+  }
+  /// 选项是否正确
   const isOptionCorrect = (question: Study.Question, option: Study.QuestionOption) => {
     const { answers, answer1, typeId } = question;
     if ([EnumQuestionType.SINGLE_CHOICE, EnumQuestionType.JUDGMENT].includes(typeId)) {
       return answer1?.includes(option.no);
     } else if ([EnumQuestionType.MULTIPLE_CHOICE].includes(typeId)) {
       return answer1?.includes(option.no);
-    } else if (typeId === EnumQuestionType.SUBJECTIVE) {
+    } else if ([EnumQuestionType.SUBJECTIVE, EnumQuestionType.SHORT_ANSWER, EnumQuestionType.ESSAY].includes(typeId)) {
       return answers?.includes(option.no) && option.no === 'A';
     }
     return false;
   }
+  /// 选项是否选中
+  const isOptionSelected = (question: Study.Question, option: Study.QuestionOption) => {
+    return question.answers.includes(option.no);
+  }
+  // 是否可以切换上一题
+  const prevEnable = computed(() => {
+    // return currentIndex.value > 0;
+    if (currentQuestion.value) {
+      if (currentQuestion.value.isSubQuestion) {
+        return subQuestionIndex.value > 0;
+      } else {
+        return currentIndex.value > 0;
+      }
+    }
+    return false;
+  });
+  // 是否可以切换下一题
+  const nextEnable = computed(() => {
+    if (currentQuestion.value) {
+      if (currentQuestion.value.isSubQuestion) {
+        return subQuestionIndex.value < currentQuestion.value.subQuestions.length - 1;
+      } else {
+        return currentIndex.value < questionList.value.length - 1;
+      }
+    }
+    return false;
+  });
+  // 下一题
   const nextQuestion = () => {
-    if (currentIndex.value >= questionList.value.length - 1) {
+    if (!nextEnable.value) {
       return;
     }
-    currentIndex.value++;
+    if (currentQuestion.value.subQuestions && currentQuestion.value.subQuestions.length > 0) {
+      if (subQuestionIndex.value < currentQuestion.value.subQuestions.length - 1) {
+        subQuestionIndex.value++;
+      } else {
+        currentIndex.value++;
+        subQuestionIndex.value = 0;
+      }
+    } else {
+      if (currentIndex.value < questionList.value.length - 1) {
+        currentIndex.value++;
+      }
+    }
   }
+  // 上一题
   const prevQuestion = () => {
-    if (currentIndex.value <= 0) {
+    if (!prevEnable.value) {
       return;
     }
-    currentIndex.value--;
+    if (currentQuestion.value.subQuestions && currentQuestion.value.subQuestions.length > 0) {
+      if (subQuestionIndex.value > 0) {
+        subQuestionIndex.value--;
+      } else {
+        currentIndex.value--;
+      }
+    } else {
+      if (currentIndex.value > 0) {
+        currentIndex.value--;
+        // 如果上一个题是子题,那么,默认选中最后一个子题
+        const prevQuestion = questionList.value[currentIndex.value - 1];
+        if (prevQuestion.subQuestions && prevQuestion.subQuestions.length > 0) {
+          subQuestionIndex.value = prevQuestion.subQuestions.length - 1;
+        }
+      }
+    }
   }
+  // 快速下一题
   const nextQuestionQuickly = () => {
     if (currentIndex.value >= questionList.value.length - 1) {
       return;
@@ -231,6 +335,7 @@ export const useExam = () => {
       }, 0);
     }, 0);
   }
+  // 快速上一题
   const prevQuestionQuickly = () => {
     if (currentIndex.value <= 0) {
       return;
@@ -243,6 +348,7 @@ export const useExam = () => {
       }, 0);
     }, 0);
   }
+  // 通过下标切换题目
   const changeIndex = (index: number) => {
     swiperDuration.value = 0;
     setTimeout(() => {
@@ -252,19 +358,22 @@ export const useExam = () => {
       }, 0);
     }, 0);
   }
+  const changeSubIndex = (index: number) => {
+    subQuestionIndex.value = index;
+  }
   // 开始计时
-  const startPracticeDuration = () => {
+  const startTiming = () => {
     interval = setInterval(() => {
       practiceDuration.value += 1;
     }, 1000);
   }
   // 停止计时
-  const stopPracticeDuration = () => {
+  const stopTiming = () => {
     interval && clearInterval(interval);
     interval = null;
   }
   // 开始倒计时
-  const startExamDuration = () => {
+  const startCountdown = () => {
     interval = setInterval(() => {
       if (examDuration.value <= 0) {
         console.log('停止倒计时')
@@ -293,7 +402,8 @@ export const useExam = () => {
     examDuration.value = duration;
     practiceDuration.value = duration;
   }
-  const processArray = (arr: Study.Question[]) => {
+  /// 整理题目结构
+  const transerQuestions = (arr: Study.Question[]) => {
     let offset = 0;
     return arr.map((item: Study.Question, index: number) => {
       const result = {
@@ -325,6 +435,7 @@ export const useExam = () => {
       return result;
     });
   }
+  // 将ExamineeQuestion转为Question
   const setQuestionList = (list: Study.ExamineeQuestion[]) => {
     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'];
     // 数据预处理
@@ -341,20 +452,20 @@ export const useExam = () => {
             name: option,
             no: orders[index],
             id: index,
-            isAnswer: false
+            isAnswer: false,
+            isCorrect: false,
+            isSelected: false
           } as Study.QuestionOption
         }) || [],
-        isDone: false,
-        isCorrect: isQuestionCorrect(item),
+        totalScore: item.totalScore || 0,
         offset: 0,
         index: index,
         virtualIndex: 0
       } as Study.Question
     }
-    console.log(list.map((item, index) => transerQuestion(item, index)), 777)
-    const arr: Study.Question[] = processArray(list.map((item, index) => transerQuestion(item, index)));
-    questionList.value = arr;
+    questionList.value = transerQuestions(list.map((item, index) => transerQuestion(item, index)));
   }
+  /// 重置题目状态
   const reset = () => {
     questionList.value = questionList.value.map(item => {
       return {
@@ -368,9 +479,17 @@ export const useExam = () => {
             answers: [],
             isMark: false,
             isNotKnow: false
-          } as Study.Question;
+          }
+        }),
+        options: item.options.map(option => {
+          return {
+            ...option,
+            isAnswer: false,
+            isCorrect: false,
+            isSelected: false
+          }
         })
-      } as Study.Question;
+      }
     });
     console.log(questionList.value)
     changeIndex(0);
@@ -379,27 +498,27 @@ export const useExam = () => {
     interval && clearInterval(interval);
     interval = null;
   }
+  /// 设置子题下标
   const setSubQuestionIndex = (index: number) => {
-    console.log(index, 1000)
     subQuestionIndex.value = index;
   }
-  watch(() => currentIndex.value, (val) => {
-    subQuestionIndex.value = 0;
-  }, {
-    immediate: false
-  });
+  // 切换阅卷模式
+  const setPracticeSettings = (settings: Study.PracticeSettings) => {
+    practiceSettings.value = settings;
+  }
+  // watch(() => currentIndex.value, (val) => {
+  //   subQuestionIndex.value = 0;
+  // }, {
+  //   immediate: false
+  // });
   watch([() => currentIndex.value, () => subQuestionIndex.value], (val) => {
     const qs = questionList.value[val[0]];
-    setTimeout(() => {
-      console.log(qs, val[1], 999)
-      console.log(11111, flatQuestionList.value[virtualCurrentIndex.value])
-    }, 500);
     virtualCurrentIndex.value = qs.index + qs.offset + val[1];
-
   }, {
     immediate: false
   });
   return {
+    practiceSettings,
     questionList,
     groupedQuestionList,
     stateQuestionList,
@@ -417,6 +536,9 @@ export const useExam = () => {
     doneCount,
     notDoneCount,
     questionTypeDesc,
+    currentSubQuestion,
+    prevEnable,
+    nextEnable,
     nextQuestion,
     prevQuestion,
     nextQuestionQuickly,
@@ -426,10 +548,10 @@ export const useExam = () => {
     examDuration,
     formatExamDuration,
     formatPracticeDuration,
-    startPracticeDuration,
-    stopPracticeDuration,
+    startTiming,
+    stopTiming,
     setDuration,
-    startExamDuration,
+    startCountdown,
     stopExamDuration,
     setExamDuration,
     setPracticeDuration,
@@ -438,6 +560,7 @@ export const useExam = () => {
     changeIndex,
     reset,
     isQuestionCorrect,
-    isOptionCorrect
+    isOptionCorrect,
+    setPracticeSettings,
   }
 }

+ 2 - 0
src/composables/useExamType.ts

@@ -19,8 +19,10 @@ export const useExamType = () => {
   }
   const loadExamTypeData = async () => {
     if (form.value.location) {
+      uni.$ie.showLoading();
       const { data } = await getExamTypes(form.value.location);
       examTypeList.value = data;
+      uni.$ie.hideLoading();
     }
 
   }

+ 1 - 1
src/config.ts

@@ -7,7 +7,7 @@ const config = {
   paySiteUrl: '',
   responseErrorCatch: true,
 };
-
+console.log(process.env.IE_ENV)
 export const env = {
   development: {
     serverBaseUrl: 'https://dz.shineking.top/prod-api',

+ 0 - 1
src/hooks/useTransferPage.ts

@@ -118,7 +118,6 @@ export const useTransferPage = <T1 = any, T2 = any>() => {
         });
       } else {
         const routeFunc = funcMap[type as keyof typeof funcMap];
-        console.log(routeFunc, type)
         routeFunc({
           url: nextUrl,
           fail: (err) => {

+ 11 - 0
src/main.ts

@@ -4,6 +4,7 @@ import uvUiTools from "@/uni_modules/uv-ui-tools";
 // #ifdef H5
 import "@/uni.webview.1.5.6"
 import './common/webview.bridge'
+import './preload'
 // #endif
 import { useRequest } from '@/utils/request'
 import tool from '@/utils/uni-tool'
@@ -79,6 +80,16 @@ export function createApp() {
         theme: {
           default: 'theme-ie'
         }
+      },
+      image: {
+        customClass: {
+          default: ''
+        }
+      },
+      cell: {
+        disableHover: {
+          default: false
+        }
       }
     }
   })

+ 53 - 47
src/pagesMain/pages/index/components/index-banner.vue

@@ -3,7 +3,7 @@
     <ie-image :is-oss="true" src="/banner/index-banner-1.png" custom-class="w-full min-h-264 overflow-hidden"
       :round="15" />
     <view class="pt-24 pb-40 bg-white grid grid-cols-4 gap-y-32 justify-items-center">
-      <view class="w-fit" v-for="item in menus" :key="item.name" @click="navigateTo(item)">
+      <view class="w-fit" v-for="item in validMenus" :key="item.name" @click="navigateTo(item)">
         <ie-image :is-oss="true" custom-class="w-auto h-82" :round="10" :src="item.icon" mode="heightFix" />
         <view class="text-26 text-fore-title">{{ item.name }}</view>
       </view>
@@ -25,53 +25,8 @@ type MenuItem = {
   pageUrl: string;
   navigateType?: Transfer.TransferType;
   noLogin?: boolean
+  visible?: boolean
 }
-const menus: MenuItem[] = [
-  {
-    name: '学习备考',
-    icon: '/menu/menu-study.png',
-    pageUrl: '/pagesStudy/pages/index/index',
-  },
-  {
-    name: '志愿填报',
-    icon: '/menu/menu-volunteer.png',
-    pageUrl: '/pagesMain/pages/volunteer/volunteer',
-    navigateType: 'switchTab',
-  },
-  {
-    name: '找院校',
-    icon: '/menu/menu-college.png',
-    pageUrl: '/pagesOther/pages/college-library/index/index',
-  },
-  {
-    name: '查专业',
-    icon: '/menu/menu-major.png',
-    pageUrl: '/pagesOther/pages/major-library/index/index',
-    noLogin: true
-  },
-  {
-    name: '看职业',
-    icon: '/menu/menu-work.png',
-    pageUrl: '/pagesOther/pages/vocation-library/index/index',
-    noLogin: true
-  },
-  {
-    name: '自我测评',
-    icon: '/menu/menu-test.png',
-    pageUrl: '/pagesOther/pages/test-center/index/index',
-  },
-  {
-    name: '单招资讯',
-    icon: '/menu/menu-news.png',
-    pageUrl: '/pagesOther/pages/news/index/index',
-    noLogin: true
-  },
-  // {
-  //   name: '专升本',
-  //   icon: '/menu/menu-upgrade.png',
-  //   pageUrl: '/pages/index/index',
-  // }
-]
 const navigateTo = async (item: MenuItem) => {
   const { pageUrl, navigateType, noLogin } = item;
   if (!noLogin) {
@@ -87,5 +42,56 @@ const navigateTo = async (item: MenuItem) => {
     });
   }
 }
+const validMenus = computed(() => {
+  console.log(userStore.isAuditor)
+  const menus: MenuItem[] = [
+    {
+      name: '学习备考',
+      icon: '/menu/menu-study.png',
+      pageUrl: '/pagesStudy/pages/index/index',
+      visible: !userStore.isAuditor,
+    },
+    {
+      name: '志愿填报',
+      icon: '/menu/menu-volunteer.png',
+      pageUrl: '/pagesMain/pages/volunteer/volunteer',
+      navigateType: 'switchTab',
+    },
+    {
+      name: '找院校',
+      icon: '/menu/menu-college.png',
+      pageUrl: '/pagesOther/pages/college-library/index/index',
+    },
+    {
+      name: '查专业',
+      icon: '/menu/menu-major.png',
+      pageUrl: '/pagesOther/pages/major-library/index/index',
+      noLogin: true
+    },
+    {
+      name: '看职业',
+      icon: '/menu/menu-work.png',
+      pageUrl: '/pagesOther/pages/vocation-library/index/index',
+      noLogin: true
+    },
+    {
+      name: '自我测评',
+      icon: '/menu/menu-test.png',
+      pageUrl: '/pagesOther/pages/test-center/index/index',
+    },
+    {
+      name: '单招资讯',
+      icon: '/menu/menu-news.png',
+      pageUrl: '/pagesOther/pages/news/index/index',
+      noLogin: true
+    },
+    // {
+    //   name: '专升本',
+    //   icon: '/menu/menu-upgrade.png',
+    //   pageUrl: '/pages/index/index',
+    // }
+  ]
+  return menus.filter(item => item.visible !== false);
+});
 </script>
 <style lang="scss" scoped></style>

+ 3 - 3
src/pagesMain/pages/me/components/me-info.vue

@@ -13,7 +13,7 @@
         <ie-image src="/static/personal/setting.png" custom-class="w-48 h-48" @click.stop="handleSettingClick" />
       </view>
     </view>
-    <view class="my-30 flex items-center text-center">
+    <!-- <view class="my-30 flex items-center text-center">
       <view class="flex-1">
         <view class="text-30 text-fore-title font-bold">0</view>
         <view class="mt-10 text-26 text-fore-subcontent">做题数量</view>
@@ -26,8 +26,8 @@
         <view class="text-30 text-fore-title font-bold">0</view>
         <view class="mt-10 text-26 text-fore-subcontent">登录次数</view>
       </view>
-    </view>
-    <view v-if="isVip" class="relative">
+    </view> -->
+    <view v-if="isVip" class="mt-30 relative">
       <ie-image src="/static/personal/buy_vip.png" custom-class="w-full h-96" />
       <view class="absolute left-100 right-20 top-0 h-full flex items-center justify-between">
         <view class="text-26 text-fore-title">已开通会员,享受权益中</view>

+ 6 - 2
src/pagesMain/pages/splash/splash.vue

@@ -1,7 +1,11 @@
 <template>
   <ie-page :fix-height="true">
     <view class="flex-1 min-h-1 h-full flex items-center justify-center">
-      <uv-image :src="imgLaunch" width="55vw" height="auto" mode="widthFix" class="-mt-300" />
+      <uv-image :src="imgLaunch" width="55vw" height="auto" mode="widthFix" bgColor="white" custom-class="-mt-300">
+        <template #loading>
+          <span></span>
+        </template>
+      </uv-image>
     </view>
   </ie-page>
 </template>
@@ -33,7 +37,7 @@ const handleLoad = () => {
       transferTo('/pagesMain/pages/index/index', {
         type: 'reLaunch'
       });
-    }, 1500);
+    }, 1200);
   });
 };
 onLoad(() => {

+ 13 - 13
src/pagesMain/pages/volunteer/volunteer.vue

@@ -3,7 +3,7 @@
     <ie-image :is-oss="true" src="/volunteer/page-bg.png" custom-class="w-full h-auto absolute top-0 left-0 -z-1" />
     <ie-image :is-oss="true" src="/volunteer/title-right.png" custom-class="w-400 h-auto absolute top-100 right-0" />
     <view class="pt-200 ml-30">
-      <ie-image :is-oss="true" src="/volunteer/title.png" custom-class="w-auto h-140 bg-back" mode="heightFix" />
+      <ie-image :is-oss="true" src="/volunteer/title.png" custom-class="w-fit h-140 bg-back" mode="heightFix" />
     </view>
     <view class="relative">
       <ie-image :is-oss="true" src="/volunteer/volunteer-bg.png" custom-class="w-full h-220" mode="scaleToFill" />
@@ -51,18 +51,18 @@ const menu: MenuItem[] = [
     icon: '/volunteer/single.png',
     pagePath: '/pagesOther/pages/ie/entry-single/entry-single'
   },
-  {
-    title: '模拟志愿分析',
-    desc: '精准分析你的志愿表',
-    icon: '/volunteer/analysis.png',
-    pagePath: '/pagesOther/pages/ie/entry-analysis/entry-analysis'
-  },
-  {
-    title: 'AI志愿',
-    desc: '精准推荐合理志愿,生成志愿表',
-    icon: '/volunteer/ai.png',
-    pagePath: '/pagesOther/pages/ie/entry-ai/entry-ai'
-  },
+  // {
+  //   title: '模拟志愿分析',
+  //   desc: '精准分析你的志愿表',
+  //   icon: '/volunteer/analysis.png',
+  //   pagePath: '/pagesOther/pages/ie/entry-analysis/entry-analysis'
+  // },
+  // {
+  //   title: 'AI志愿',
+  //   desc: '精准推荐合理志愿,生成志愿表',
+  //   icon: '/volunteer/ai.png',
+  //   pagePath: '/pagesOther/pages/ie/entry-ai/entry-ai'
+  // },
   {
     title: '测职业技能分',
     desc: '结合院校录取规则,快速测算',

+ 1 - 1
src/pagesOther/pages/college-library/components/college-item.vue

@@ -64,7 +64,7 @@ const bxTags = computed(() => {
         _.pull(tags, '双高')
         tags.push(bxType)
     }
-    return tags
+    return tags.filter(item => !item.includes('国家级'))
 })
 
 const isSpecialTag = (tag) => {

+ 61 - 0
src/pagesOther/pages/topic-center/wrong-book/components/datetime-picker.vue

@@ -0,0 +1,61 @@
+<template>
+  <view class="bg-white py-20 px-30 text-26 ">
+    <view class="h-50 p-10 flex items-center gap-x-10  border border-[#999999] border-solid rounded-6"
+      @click="handlePicker">
+      <uv-icon name="calendar" size="20" color="#999999" />
+      <text class="flex-1 text-center" :class="[startDate ? 'text-fore-title' : 'text-[#999999]']">
+        {{ startDate || '开始日期' }}
+      </text>
+      <text class="text-[#999999]">至</text>
+      <text class="flex-1 text-center" :class="[endDate ? 'text-fore-title' : 'text-[#999999]']">
+        {{ endDate || '结束日期' }}
+      </text>
+      <view class="w-44">
+        <uv-icon name="close-circle" size="20" color="#999999" v-if="startDate || endDate" @click.stop="handleClear" />
+      </view>
+    </view>
+  </view>
+  <ie-popup ref="popupRef" title="选择日期" @confirm="handleConfirm">
+    <uni-calendar ref="calendarRef" :insert="true" :lunar="false" :range="true" :showMonth="false"
+      :showExtraInfo="false" start-date="" end-date="" @change="handleChange"></uni-calendar>
+  </ie-popup>
+</template>
+<script lang="ts" setup>
+const emit = defineEmits<{
+  (e: 'change', value: { startDate: string, endDate: string }): void
+}>();
+
+const startDate = defineModel<string>('startDate', { required: true });
+const endDate = defineModel<string>('endDate', { required: true });
+
+const currentStartDate = ref('');
+const currentEndDate = ref('');
+const handleChange = (e: any) => {
+  const { before, after } = e.range;
+  if (before && after) {
+    currentStartDate.value = before;
+    currentEndDate.value = after;
+  }
+}
+const popupRef = ref();
+const handlePicker = () => {
+  popupRef.value.open()
+}
+const handleClear = () => {
+  currentStartDate.value = ''
+  currentEndDate.value = ''
+  updateDate();
+}
+const updateDate = () => {
+  startDate.value = currentStartDate.value;
+  endDate.value = currentEndDate.value;
+  setTimeout(() => {
+    emit('change', { startDate: startDate.value, endDate: endDate.value });
+  }, 0);
+}
+const handleConfirm = () => {
+  updateDate();
+  popupRef.value.close();
+}
+</script>
+<style lang="scss" scoped></style>

+ 30 - 19
src/pagesOther/pages/topic-center/wrong-book/components/wrong-book-list.vue

@@ -1,38 +1,49 @@
 <template>
-    <z-paging ref="paging" v-model="list" :default-page-size="5" @query="handleQuery">
-        <view class="p-30 fx-col gap-30">
-            <wrong-book-item v-for="item in list" :question="item" @delete="handleRefresh"/>
-        </view>
-    </z-paging>
+  <z-paging ref="paging" v-model="list" :default-page-size="5" @query="handleQuery">
+    <view class="p-30 fx-col gap-30">
+      <wrong-book-item v-for="item in list" :question="item" @delete="handleRefresh" />
+    </view>
+  </z-paging>
 </template>
 
 <script setup>
-import {ref} from 'vue'
-import {getWrongQuestions} from '@/api/webApi/paper.js';
+import { ref } from 'vue'
+import { getWrongQuestions } from '@/api/webApi/paper.js';
 import WrongBookItem from './wrong-book-item.vue';
-import {createPropDefine} from "@/utils";
-import {useQuestionTranslate} from "@/components/mx-question/useQuestionTranslate";
+import { createPropDefine } from "@/utils";
+import { useQuestionTranslate } from "@/components/mx-question/useQuestionTranslate";
 
 const props = defineProps({
-    subjectId: createPropDefine(0, [Number, String])
+  subjectId: createPropDefine(0, [Number, String]),
+  startDate: createPropDefine('', String),
+  endDate: createPropDefine('', String)
 })
 
 const list = ref([])
 const paging = ref(null)
 
 const handleQuery = function (pageNum, pageSize) {
-    getWrongQuestions({subjectId: props.subjectId, pageNum, pageSize})
-        .then(res => {
-            const rows = res.rows.map(useQuestionTranslate)
-            paging.value.completeByTotal(rows, res.total)
-        })
-        .catch(e => paging.value.complete(false))
+  uni.$ie.showLoading()
+  getWrongQuestions({ subjectId: props.subjectId, start: props.startDate, end: props.endDate, pageNum, pageSize })
+    .then(res => {
+      const rows = res.rows.map(useQuestionTranslate)
+      paging.value.completeByTotal(rows, res.total)
+    })
+    .catch(e => paging.value.complete(false))
+    .finally(() => {
+      uni.$ie.hideLoading()
+    })
 }
 
 const handleRefresh = function () {
-    paging.value.refresh() // 这会直接刷新至当前分页
+  paging.value.refresh() // 这会直接刷新至当前分页
 }
+const reload = () => {
+  paging.value.reload()
+}
+watch([() => props.startDate, () => props.endDate], () => {
+  reload()
+})
 </script>
 
-<style lang="scss" scoped>
-</style>
+<style lang="scss" scoped></style>

+ 27 - 13
src/pagesOther/pages/topic-center/wrong-book/wrong-book.vue

@@ -1,28 +1,42 @@
 <template>
-    <view class="page-content">
-        <mx-nav-bar title="错题本"/>
+  <view class="page-content">
+    <mx-nav-bar title="错题本" />
+    <view class="flex-1 min-h-1 relative">
+      <view class="absolute inset-0">
         <mx-tabs-swiper v-model="current" :tabs="subjects" :key-name="keyName" template="default" border>
-            <template #="subject">
-                <wrong-book-list :subject-id="subject[keyValue]"/>
-            </template>
+          <template #header>
+            <date-time-picker v-model:start-date="startDate" v-model:end-date="endDate" @change="handleChange" />
+          </template>
+          <template #="subject">
+            <wrong-book-list :subject-id="subject[keyValue]" :start-date="startDate" :end-date="endDate" />
+          </template>
         </mx-tabs-swiper>
+      </view>
     </view>
+  </view>
 </template>
 
 <script setup>
-import {watchEffect, ref} from 'vue'
-import {useTopicSubjects} from "@/pagesOther/pages/topic-center/hooks/useTopicSubjects";
+import { watchEffect, ref } from 'vue'
+import { useTopicSubjects } from "@/pagesOther/pages/topic-center/hooks/useTopicSubjects";
 import WrongBookList from "@/pagesOther/pages/topic-center/wrong-book/components/wrong-book-list.vue";
-import {useTransfer} from "@/hooks/useTransfer";
+import DateTimePicker from "@/pagesOther/pages/topic-center/wrong-book/components/datetime-picker.vue";
+import { useTransfer } from "@/hooks/useTransfer";
 import _ from "lodash";
 
-const {prevData} = useTransfer()
-const {subjects, keyName, keyValue} = useTopicSubjects()
+const { prevData } = useTransfer()
+const { subjects, keyName, keyValue } = useTopicSubjects()
 const current = ref(0)
 
 watchEffect(() => current.value = _.findIndex(subjects.value, s => s[keyValue] == prevData.value.subjectId))
-</script>
 
-<style lang="scss">
+const startDate = ref('')
+const endDate = ref('')
+
+const handleChange = (value) => {
+  startDate.value = value.startDate
+  endDate.value = value.endDate
+}
+</script>
 
-</style>
+<style lang="scss"></style>

+ 3 - 2
src/pagesStudy/components/exam-record-item.vue

@@ -16,11 +16,11 @@
   </view>
 </template>
 <script lang="ts" setup>
-import { Study } from '@/types';
+import { Study, Transfer } from '@/types';
 import { useTransferPage } from '@/hooks/useTransferPage';
 import { EnumPaperType, EnumSimulatedRecordStatus } from '@/common/enum';
 import { beginExaminee } from '@/api/modules/study';
-const { transferTo } = useTransferPage();
+const { transferTo } = useTransferPage<any, Transfer.SimulationAnalysisPageOptions | Transfer.ExamAnalysisPageOptions>();
 const props = defineProps<{
   data: Study.SimulatedRecord;
 }>();
@@ -45,6 +45,7 @@ const handleDetail = () => {
       data: {
         name: '模拟考试-' + props.data.subjectName,
         paperType: EnumPaperType.SIMULATED,
+        readonly: false,
         simulationInfo: {
           examineeId: props.data.id,
         }

+ 27 - 2
src/pagesStudy/components/knowledge-tree-node.vue

@@ -6,8 +6,15 @@
           <uv-icon v-if="!nodeData.isLeaf" name="arrow-right" size="14" color="#888"
             :custom-class="['mr-16 transition-transform duration-300', nodeData.isExpanded ? 'rotate-90' : '']" />
           <view>
-            <text class="block text-28 text-fore-title font-bold ellipsis-1">{{ nodeData.name }}</text>
-            <text class="mt-4 block text-24 text-fore-light">共{{ nodeData.questionCount || 0 }}道题</text>
+            <view class="block text-28 text-fore-title font-bold ellipsis-1">{{ nodeData.name }}</view>
+            <view class="mt-4 text-24 text-fore-light flex items-center">
+              <progress class="w-100 rounded-full overflow-hidden" :percent="getProgressPercent(nodeData)"
+                :show-text="false" activeColor="#31a0fc" backgroundColor="#E3F4FA" />
+              <text class="ml-10 text-primary">{{ nodeData.finishedCount }}</text>
+              <text>/{{ nodeData.questionCount }}道</text>
+              <text class="ml-10">正确率</text>
+              <text class="ml-10 text-primary font-bold">{{ getCorrectRate(nodeData.finishedRatio) }}%</text>
+            </view>
           </view>
         </view>
         <slot>
@@ -125,6 +132,24 @@ const handleUpdateHeight = (parentNode: Study.KnowledgeNode) => {
     emit('updateHeight', props.parentData);
   }
 };
+
+const getCorrectRate = (rate: number): number => {
+  if (!rate) {
+    return 0;
+  }
+  if (rate > 0 && rate < 0.1) {
+    return 0.1;
+  }
+  return rate;
+};
+
+const getProgressPercent = (nodeData: Study.KnowledgeNode): number => {
+  const { finishedCount, questionCount } = nodeData;
+  if (!finishedCount || !questionCount) {
+    return 0;
+  }
+  return Math.min(finishedCount / questionCount, 1);
+};
 </script>
 
 <style lang="scss" scoped></style>

+ 107 - 0
src/pagesStudy/pages/exam-start/components/exam-mode.vue

@@ -0,0 +1,107 @@
+<template>
+  <ie-popup ref="popupRef" title="模式切换" @confirm="handleConfirm" @close="beforeClose">
+    <view class="popup-content">
+      <view class="flex items-center gap-x-40 px-40 pt-0 pb-40 bg-white">
+        <view class="mode-card" :class="{ 'is-active': form.reviewMode === EnumReviewMode.AFTER_SUBMIT }"
+          @click="handleChangeMode(EnumReviewMode.AFTER_SUBMIT)">
+          <view class="text-32 font-bold h-60">练习</view>
+          <view class="flex-1 mt-4 text-26">答完一组题查看结果解析</view>
+        </view>
+        <view class="mode-card" :class="{ 'is-active': form.reviewMode === EnumReviewMode.DURING_ANSWER }"
+          @click="handleChangeMode(EnumReviewMode.DURING_ANSWER)">
+          <view class="text-32 font-bold h-60">背题</view>
+          <view class="flex-1 mt-4 text-26">答完一题直接出答案 左滑下一题</view>
+        </view>
+      </view>
+      <view class="h-16 bg-back"></view>
+      <view class="mx-40 pt-10">
+        <uv-cell-group :border="false">
+          <uv-cell title="答对直接下一题" :border="false">
+            <template v-slot:value>
+              <uv-switch v-model="form.autoNext" active-color="#31A0FC" inactive-color="#eeeeee"></uv-switch>
+            </template>
+          </uv-cell>
+          <uv-cell title="清空重来" :border="false" :isLink="true" :disableHover="true" @click="handleClear"></uv-cell>
+        </uv-cell-group>
+      </view>
+    </view>
+  </ie-popup>
+</template>
+<script lang="ts" setup>
+import { EnumReviewMode } from '@/common/enum';
+import { useExam } from '@/composables/useExam';
+import { Study, Transfer } from '@/types';
+import { EXAM_DATA, EXAM_PAGE_OPTIONS } from '@/types/injectionSymbols';
+import { useUserStore } from '@/store/userStore';
+
+const emit = defineEmits<{
+  (e: 'clear'): void;
+}>();
+const examPageOptions = inject(EXAM_PAGE_OPTIONS) || {} as Transfer.ExamAnalysisPageOptions;
+const examData = inject(EXAM_DATA) || {} as ReturnType<typeof useExam>;
+const { doneCount, startTiming, stopTiming, practiceSettings, setPracticeSettings, reset } = examData;
+
+const form = ref<Study.PracticeSettings>(practiceSettings.value);
+const popupRef = ref();
+const userStore = useUserStore();
+const open = () => {
+  form.value = JSON.parse(JSON.stringify(practiceSettings.value));
+  stopTiming();
+  popupRef.value.open();
+}
+const close = () => {
+  popupRef.value.close();
+}
+const beforeClose = () => {
+  startTiming();
+}
+defineExpose({
+  open,
+  close
+});
+
+const handleConfirm = () => {
+  userStore.setPracticeSettings(form.value);
+  setPracticeSettings(form.value);
+  close();
+}
+const handleChangeMode = (mode: EnumReviewMode) => {
+  form.value.reviewMode = mode;
+}
+const handleClear = () => {
+  if (doneCount.value <= 0) {
+    return;
+  }
+  stopTiming();
+  uni.$ie.showModal({
+    title: '重新作答',
+    content: '是否确认清空全部作答数据?',
+  }).then(confirm => {
+    if (confirm) {
+      close();
+      reset();
+      setTimeout(() => {
+        startTiming();
+      }, 300);
+    } else {
+      startTiming();
+    }
+  });
+}
+</script>
+<style lang="scss" scoped>
+.popup-content {
+  @apply py-30;
+}
+
+.mode-card {
+  height: 140rpx;
+  box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.04);
+  @apply flex-1 rounded-12 bg-white px-30 py-14 border border-solid border-white flex flex-col items-center justify-center text-fore-title text-left;
+}
+
+.is-active {
+  box-shadow: none;
+  @apply bg-[#E3F4FA] border-primary text-primary;
+}
+</style>

+ 60 - 0
src/pagesStudy/pages/exam-start/components/exam-navbar.vue

@@ -0,0 +1,60 @@
+<template>
+  <ie-navbar :title="pageTitle" custom-back @left-click="handleLeftClick">
+    <template #headerRight>
+      <view v-if="!isReadOnly" class="flex items-center gap-x-40 h-full">
+        <view class="text-28" :class="{ 'text-red-500': practiceDuration > totalExamTime }">
+          {{ formatPracticeDuration }}
+        </view>
+        <!-- 练习模式才有背题模式 -->
+        <view v-if="isPractice" class="px-10 h-full flex items-center" @click="handleMoreClick">
+          <ie-image src="/pagesStudy/static/image/icon-more.png" custom-class="w-38 h-auto" mode="widthFix" />
+        </view>
+      </view>
+      <view v-else class="text-28">用时:{{ formatPracticeDuration }}</view>
+    </template>
+  </ie-navbar>
+</template>
+<script lang="ts" setup>
+import { EnumPaperType } from '@/common/enum';
+import { Transfer } from '@/types';
+import { EXAM_PAGE_OPTIONS, EXAM_DATA } from '@/types/injectionSymbols';
+import { useExam } from '@/composables/useExam';
+import { useTransferPage } from '@/hooks/useTransferPage';
+
+const props = defineProps<{
+  totalExamTime: number;
+}>();
+
+const { transferTo, transferBack } = useTransferPage();
+const examPageOptions = inject(EXAM_PAGE_OPTIONS) || {} as Transfer.ExamAnalysisPageOptions;
+const examData = inject(EXAM_DATA) || {} as ReturnType<typeof useExam>;
+const { formatPracticeDuration, practiceDuration } = examData;
+
+const isPractice = computed(() => {
+  return examPageOptions?.paperType === EnumPaperType.PRACTICE;
+});
+const pageTitle = computed(() => {
+  if (examPageOptions) {
+    const { name, readonly, paperType } = examPageOptions;
+    if (readonly) {
+      return paperType === EnumPaperType.SIMULATED ? '考试解析' : '练习解析';
+    }
+    return paperType === EnumPaperType.SIMULATED ? '模拟考试' : '知识点练习';
+  }
+  return '练习';
+});
+const isReadOnly = computed(() => {
+  return examPageOptions?.readonly || false;
+});
+const emit = defineEmits<{
+  (e: 'left-click'): void;
+  (e: 'right-click'): void;
+}>();
+const handleLeftClick = () => {
+  emit('left-click');
+}
+const handleMoreClick = () => {
+  emit('right-click');
+}
+</script>
+<style lang="scss" scoped></style>

+ 225 - 0
src/pagesStudy/pages/exam-start/components/exam-stats-card.vue

@@ -0,0 +1,225 @@
+<template>
+  <!-- #ifdef H5 -->
+  <teleport to="body">
+    <!-- #endif -->
+    <!-- #ifdef MP-WEIXIN -->
+    <root-portal externalClass="theme-ie">
+      <!-- #endif -->
+      <uv-popup ref="popupRef" mode="bottom" :close-on-click-overlay="true" :closeable="false" :round="16">
+        <view class="theme-ie w-auto box-border bg-white">
+          <view class="popup-header">
+            <view class="popup-header-left">
+              <uv-icon name="calendar" size="26" />
+              <view class="popup-header-left-title">
+                <text>答题卡</text>
+                <view class="ml-20">
+                  <text class="text-30 text-primary">{{ doneCount }}</text>
+                  <text>/</text>
+                  <text class="text-30 text-fore-light">{{ virtualTotalCount }}</text>
+                </view>
+              </view>
+            </view>
+            <view class="popup-header-right">
+              <block v-if="!readonly">
+                <view class="stats-dot stats-dot-done">已答</view>
+                <view class="stats-dot stats-dot-not-done">未答</view>
+                <view class="stats-dot stats-dot-not-know">不会</view>
+              </block>
+              <block v-else>
+                <view class="stats-dot stats-dot-correct">答对</view>
+                <view class="stats-dot stats-dot-incorrect">答错</view>
+                <view class="stats-dot stats-dot-not-done">未答</view>
+              </block>
+            </view>
+          </view>
+          <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 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.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="{
+                            'is-done': !isViewMode && qs.question.isDone,
+                            'is-not-know': !isViewMode && qs.question.isNotKnow,
+                            'is-mark': !isViewMode && qs.question.isMark,
+                            'is-correct': isViewMode && qs.question.isCorrect,
+                            'is-incorrect': isViewMode && !qs.question.isCorrect,
+                          }">
+                          <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" />
+                        </view>
+                      </view>
+                    </view>
+                  </template>
+                </view>
+              </scroll-view>
+            </view>
+            <view v-if="!isViewMode" 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="handleSubmit">交卷</view>
+            </view>
+          </view>
+        </view>
+      </uv-popup>
+      <!-- #ifdef MP-WEIXIN -->
+    </root-portal>
+    <!-- #endif -->
+    <!-- #ifdef H5 -->
+  </teleport>
+  <!-- #endif -->
+</template>
+
+<script lang="ts" setup>
+import { useExam } from '@/composables/useExam';
+import { Study, Transfer } from '@/types';
+import { EXAM_DATA } from '@/types/injectionSymbols';
+import { EXAM_PAGE_OPTIONS } from '@/types/injectionSymbols';
+const props = defineProps<{
+  readonly?: boolean;
+}>();
+const examData = inject(EXAM_DATA) || {} as ReturnType<typeof useExam>;
+const { doneCount, virtualTotalCount, groupedQuestionList, questionTypeDesc, reset, startTiming, stopTiming, changeIndex, setSubQuestionIndex } = examData;
+const examPageOptions = inject(EXAM_PAGE_OPTIONS) || {} as Transfer.ExamAnalysisPageOptions;
+const isViewMode = computed(() => {
+  return examPageOptions?.readonly || false;
+});
+
+const popupRef = ref();
+const open = () => {
+  popupRef.value.open();
+}
+const close = () => {
+  popupRef.value.close();
+}
+defineExpose({
+  open,
+  close
+});
+const handleReset = () => {
+  if (doneCount.value <= 0) {
+    return;
+  }
+  stopTiming();
+  uni.$ie.showModal({
+    title: '重新作答',
+    content: '是否确认清空全部作答数据?',
+  }).then(confirm => {
+    if (confirm) {
+      close();
+      reset();
+      setTimeout(() => {
+        startTiming();
+      }, 300);
+    } else {
+      startTiming();
+    }
+  });
+}
+const emit = defineEmits<{
+  (e: 'submit'): void;
+}>();
+const handleSubmit = () => {
+  emit('submit');
+}
+const hanadleNavigate = (question: Study.Question, index: number) => {
+  if (question.isSubQuestion) {
+    changeIndex(question.parentIndex || 0);
+    setTimeout(() => {
+      setSubQuestionIndex(question.subIndex || 0);
+    });
+  } else {
+    changeIndex(index);
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.popup-header {
+  @apply px-30 h-120 flex items-center justify-between;
+}
+
+.popup-header-left {
+  @apply flex items-center;
+}
+
+.popup-header-left-title {
+  @apply flex items-center text-30 text-fore-title font-bold;
+}
+
+.popup-header-right {
+  @apply flex items-center gap-x-60;
+}
+
+.stats-dot {
+  @apply relative text-22 text-fore-light;
+
+  &::before {
+    @apply content-[''] absolute top-6 -left-30 w-18 h-18 rounded-full;
+  }
+
+  &.stats-dot-done {
+    &::before {
+      @apply bg-primary;
+    }
+  }
+
+  &.stats-dot-not-done {
+    &::before {
+      @apply w-14 h-14 border-2 border-solid border-back;
+    }
+  }
+
+  &.stats-dot-not-know {
+    &::before {
+      @apply bg-back;
+    }
+  }
+
+  &.stats-dot-correct {
+    &::before {
+      @apply bg-[#2CC6A0];
+    }
+  }
+
+  &.stats-dot-incorrect {
+    &::before {
+      @apply bg-[#FF5B5C];
+    }
+  }
+}
+
+.popup-content {
+  @apply h-[48vh] 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];
+}
+
+.is-correct {
+  @apply text-[#2CC6A0] border-[#E7FCF8] bg-[#E7FCF8];
+}
+
+.is-incorrect {
+  @apply text-[#FF5B5C] border-[#FEEDE9] bg-[#FEEDE9];
+}
+</style>

+ 27 - 0
src/pagesStudy/pages/exam-start/components/exam-subtitle.vue

@@ -0,0 +1,27 @@
+<template>
+  <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">{{ virtualCurrentIndex + 1 }}</text>/
+      <text class="text-28 text-fore-subtitle">{{ virtualTotalCount }}</text>
+    </view>
+  </view>
+</template>
+<script lang="ts" setup>
+import { Transfer } from '@/types';
+import { EXAM_PAGE_OPTIONS, EXAM_DATA } from '@/types/injectionSymbols';
+import { useExam } from '@/composables/useExam';
+
+const examPageOptions = inject(EXAM_PAGE_OPTIONS) || {} as Transfer.ExamAnalysisPageOptions;
+const examData = inject(EXAM_DATA) || {} as ReturnType<typeof useExam>;
+const { virtualCurrentIndex, virtualTotalCount } = examData;
+
+const pageSubtitle = computed(() => {
+  if (examPageOptions) {
+    const { name } = examPageOptions;
+    return name;
+  }
+  return '';
+});
+</script>
+<style lang="scss" scoped></style>

+ 136 - 0
src/pagesStudy/pages/exam-start/components/exam-swiper.vue

@@ -0,0 +1,136 @@
+<template>
+  <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">
+        <block v-for="(item, index) in questionList" :key="item.id">
+          <swiper-item class="h-full" v-show="showSwiperItem(index)">
+            <scroll-view v-if="showSwiperItem(index)" scroll-y class="question-wrap" :scroll-with-animation="true">
+              <view class="h-20"></view>
+              <view class="mx-40">
+                <question-item :question="item" />
+              </view>
+              <view class="h-40"></view>
+            </scroll-view>
+          </swiper-item>
+        </block>
+      </swiper>
+    </view>
+  </view>
+  <view class="btn-group flex items-center justify-evenly pt-10 pb-30">
+    <view class="">
+      <ie-button type="primary" size="mini" :round="4" :shadow="false" custom-class="w-180" :disabled="!prevEnable"
+        @click="handlePrevQuestion">上一题</ie-button>
+    </view>
+    <view class="" v-if="!isReadOnly">
+      <ie-button type="primary" size="mini" :round="4" :shadow="false" custom-class="w-160"
+        @click="submit">交卷</ie-button>
+    </view>
+    <view class="">
+      <ie-button type="primary" size="mini" :round="4" :shadow="false" custom-class="w-180" :disabled="!nextEnable"
+        @click="handleNextQuestion">下一题</ie-button>
+    </view>
+  </view>
+
+</template>
+
+<script lang="ts" setup>
+import { Study, Transfer } from '@/types';
+import QuestionItem from './question-item.vue';
+import { useExam } from '@/composables/useExam';
+import { EXAM_DATA, EXAM_PAGE_OPTIONS } from '@/types/injectionSymbols';
+const examData = inject(EXAM_DATA) || {} as ReturnType<typeof useExam>;
+const examPageOptions = inject(EXAM_PAGE_OPTIONS) || {} as Transfer.ExamAnalysisPageOptions;
+const { swiperDuration, currentIndex, subQuestionIndex, startTiming, stopTiming, questionList, prevEnable, nextEnable, prevQuestion, nextQuestion, changeIndex } = examData;
+const current = 0;
+// const props = defineProps<{
+//   question: Study.Question;
+//   readonly?: boolean;
+//   currentIndex: number;
+//   index: number;
+//   subQuestionIndex: number;
+// }>();
+
+const isReadOnly = computed(() => {
+  const { readonly } = examPageOptions;
+  return readonly
+});
+const showSwiperItem = (index: number) => {
+  return Math.abs(currentIndex.value - index) <= 2;
+};
+const isViewMode = computed(() => {
+  return isReadOnly.value || false;
+});
+const isAnimationFinish = ref(false);
+const transitionStartX = ref(null);
+const transitionEndX = ref(null);
+
+const emit = defineEmits<{
+  (e: 'changeSubQuestion', index: number): void;
+  (e: 'changeQuestion', question: Study.Question): void;
+  (e: 'submit'): void;
+}>();
+const handleChangeSubQuestion = (index: number) => {
+  emit('changeSubQuestion', index);
+}
+
+const handleChangeQuestion = (question: Study.Question) => {
+  emit('changeQuestion', question);
+}
+
+const handleSwiperChange = (e: any) => {
+  changeIndex(e.detail.current);
+};
+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 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) {
+    if (!isViewMode.value) {
+      submit();
+    }
+  }
+  isAnimationFinish.value = true;
+  transitionStartX.value = null;
+  transitionEndX.value = null;
+};
+
+const submit = () => {
+  emit('submit');
+}
+
+const handlePrevQuestion = () => {
+  prevQuestion();
+}
+const handleNextQuestion = () => {
+  nextQuestion();
+}
+</script>
+
+<style lang="scss" scoped>
+.question-wrap {
+  @apply h-full box-border;
+}
+
+.btn-group {
+  box-shadow: 0 -20px 10px 0 rgba(255, 255, 255, 0.9);
+  position: relative;
+  z-index: 1;
+}
+</style>

+ 92 - 0
src/pagesStudy/pages/exam-start/components/exam-toolbar.vue

@@ -0,0 +1,92 @@
+<template>
+  <ie-safe-toolbar :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="min-w-100 flex flex-col items-center" id="question-calendar-btn" @click="handleCalendar">
+        <view class="w-48 h-48 flex items-center justify-center">
+          <uv-icon name="calendar" size="26" />
+        </view>
+        <text class="mt-6 text-24 text-fore-subcontent">答题卡</text>
+      </view>
+      <view class="min-w-100 flex flex-col items-center" id="question-favorite-btn" @click="handleFavorite">
+        <view class="w-48 h-48 flex items-center justify-center">
+          <uv-icon v-if="currentQuestion.isFavorite" name="star-fill" color="#FF9A18" size="27" />
+          <uv-icon v-else name="star" size="25" />
+        </view>
+        <text class="mt-6 text-24 text-fore-subcontent">收藏</text>
+      </view>
+      <view class="min-w-100 flex flex-col items-center" id="question-mark-btn" @click="handleMark">
+        <view class="w-48 h-48 flex items-center justify-center">
+          <ie-image
+            :src="currentQuestion.isMark ? '/pagesStudy/static/image/icon-mark-active.png' : '/pagesStudy/static/image/icon-mark.png'"
+            custom-class="w-34 h-34" mode="aspectFill" />
+        </view>
+        <text class="mt-6 text-24 text-fore-subcontent">标记</text>
+      </view>
+      <view class="min-w-100 flex flex-col items-center" id="question-correct-btn" @click="handleCorrect">
+        <view class="w-48 h-48 flex items-center justify-center">
+          <uv-icon name="info-circle" size="22" />
+        </view>
+        <text class="mt-6 text-24 text-fore-subcontent">题目纠错</text>
+      </view>
+    </view>
+  </ie-safe-toolbar>
+  <exam-stats-card ref="examStatsCardRef" @submit="handleSubmit" />
+  <question-correct-popup ref="questionCorrectPopupRef" @close="startTiming" />
+</template>
+<script lang="ts" setup>
+import ExamStatsCard from './exam-stats-card.vue';
+import QuestionCorrectPopup from './question-correct-popup.vue';
+import { Study, Transfer } from '@/types';
+import { EXAM_PAGE_OPTIONS, EXAM_DATA } from '@/types/injectionSymbols';
+import { useExam } from '@/composables/useExam';
+import { collectQuestion, cancelCollectQuestion } from '@/api/modules/study';
+
+const examPageOptions = inject(EXAM_PAGE_OPTIONS) || {} as Transfer.ExamAnalysisPageOptions;
+const examData = inject(EXAM_DATA) || {} as ReturnType<typeof useExam>;
+const { stateQuestionList, currentIndex, subQuestionIndex, startTiming, stopTiming } = examData;
+
+const examStatsCardRef = ref<InstanceType<typeof ExamStatsCard>>();
+const currentQuestion = computed(() => {
+  const qs = stateQuestionList.value[currentIndex.value];
+  if (qs.subQuestions && qs.subQuestions.length > 0) {
+    return qs.subQuestions[subQuestionIndex.value] || {};
+  }
+  return qs || {};
+});
+const emit = defineEmits<{
+  (e: 'correct', question: Study.Question): void;
+  (e: 'favorite', question: Study.Question): void;
+  (e: 'mark', question: Study.Question): void;
+  (e: 'submit'): void;
+}>();
+
+const questionCorrectPopupRef = ref();
+const handleCorrect = () => {
+  stopTiming();
+  questionCorrectPopupRef.value.open(currentQuestion.value.id);
+}
+/**
+ * 收藏
+ */
+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 handleCalendar = () => {
+  examStatsCardRef.value?.open();
+}
+const handleSubmit = () => {
+  emit('submit');
+}
+</script>
+<style lang="scss" scoped></style>

+ 429 - 0
src/pagesStudy/pages/exam-start/components/question-item copy.vue

@@ -0,0 +1,429 @@
+<template>
+  <view class="question-item" :id="`qs_${question.id}`">
+    <view class="is-main-question">
+      <view v-if="question.typeId && !isSubQuestion" class="question-type">
+        {{ questionTypeDesc[question.typeId as EnumQuestionType] }}
+      </view>
+      <view class="question-content" :class="{ 'mt-30': isSubQuestion }">
+        <text v-if="question.isSubQuestion" 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)">
+          <template v-if="!readonly">
+            <view v-if="!isOnlySubjective" class="question-option-index">{{ option.no }}</view>
+            <view v-else>
+              <uv-icon name="info-circle" :color="isSelected(option) ? '#31A0FC' : '#999'" size="18" />
+            </view>
+          </template>
+          <view v-else>
+            <uv-icon v-if="isOptionCorrect(question, 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="getOptionContent(option)" containerStyle="display:inline"
+              contentStyle="word-break:break-word;"></uv-parse>
+          </view>
+        </view>
+        <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 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-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>
+      <!-- 阅卷模式下显示答案 -->
+      <template v-if="readonly">
+        <view v-if="question.subQuestions.length === 0" 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 v-if="!isOnlySubjective" 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 v-if="!isOnlySubjective" 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 v-if="isOnlySubjective" class="text-left mt-10 px-20">
+            <uv-parse :content="'参考答案:' + question.answer2"></uv-parse>
+          </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>
+        </view>
+      </view>
+    </view>
+    <view v-if="question.subQuestions.length" class="is-sub-question">
+      <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="changeSubQuestion(subIndex)">
+            {{ question.index + question.offset + subIndex + 1 }}
+          </view>
+        </view>
+      </scroll-view>
+      <view v-if="subQuestion">
+        <question-item :question="subQuestion" :parent-question="question" :readonly="readonly" :is-sub-question="true"
+          :index="index" :total="question.subQuestions.length" @select="handleSelectOption"
+          @notKnow="handleSelectNotKnow" />
+      </view>
+    </view>
+  </view>
+</template>
+
+<script lang="ts" setup>
+import { Study } from '@/types';
+import { useExam } from '@/composables/useExam';
+import { EnumQuestionType } from '@/common/enum';
+import { NEXT_QUESTION, PREV_QUESTION, NEXT_QUESTION_QUICKLY, PREV_QUESTION_QUICKLY, SHOW_SUBMIT_CONFIRM, IS_ALL_DONE } from '@/types/injectionSymbols';
+const { questionTypeDesc, isOptionCorrect } = useExam();
+const props = defineProps<{
+  question: Study.Question;
+  parentQuestion?: Study.Question;
+  readonly?: boolean;
+  isSubQuestion?: boolean;
+  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 isAllDone = inject(IS_ALL_DONE);
+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;
+  (e: 'changeQuestion', question: Study.Question): void;
+}>();
+const isOnlySubjective = computed(() => {
+  return [EnumQuestionType.SUBJECTIVE, EnumQuestionType.SHORT_ANSWER, EnumQuestionType.ESSAY].includes(props.question.typeId);
+});
+const getStyleClass = (option: Study.QuestionOption) => {
+  if (!props.readonly) {
+    return isSelected(option) ? 'question-option-selected' : '';
+  }
+  let customClass = '';
+  let { answers, answer1 } = props.question;
+  answers = answers?.filter(item => item !== ' ') || [];
+  answer1 = answer1 || ''
+  if ([EnumQuestionType.SINGLE_CHOICE, EnumQuestionType.JUDGMENT].includes(props.question.typeId)) {
+    if (answer1.includes(option.no)) {
+      customClass = 'question-option-correct';
+    } else if (answers.includes(option.no)) {
+      customClass = 'question-option-incorrect';
+    }
+  } else if ([EnumQuestionType.MULTIPLE_CHOICE].includes(props.question.typeId)) {
+    // 我选择的答案
+    if (answers.includes(option.no)) {
+      if (answer1.includes(option.no)) {
+        customClass = 'question-option-correct';
+      } else {
+        customClass = 'question-option-incorrect';
+      }
+    } else {
+      // 漏选的答案
+      if (answer1.includes(option.no)) {
+        customClass = 'question-option-miss';
+      }
+    }
+  }
+  // console.log(props.question, option)
+  return customClass;
+};
+
+const isOptionIncorrect = (option: Study.QuestionOption) => {
+  const { answers, answer1 } = props.question;
+  return answers.includes(option.no) && !answer1.includes(option.no);
+}
+const getQuestionTitle = () => {
+  if (props.isSubQuestion) {
+    const prefix = questionTypeDesc[props.question.typeId as EnumQuestionType].slice(0, 2);
+    return `[${prefix}]`;
+  }
+  return '';
+};
+const getOptionContent = (option: Study.QuestionOption) => {
+  // sb 问题,浪费几个小时
+  return option.name.replace(/\s/g, ' ');
+}
+const handleShowParse = () => {
+  props.question.showParse = !props.question.showParse;
+}
+const handleNotKnow = () => {
+  props.question.answers = [];
+  props.question.isNotKnow = !props.question.isNotKnow;
+  checkIsDone();
+  if (props.isSubQuestion) {
+    emit('select', props.question);
+  } else {
+    changeQuestion();
+  }
+}
+const handleSelect = (option: Study.QuestionOption) => {
+  if (props.readonly) {
+    return;
+  }
+  if ([
+    EnumQuestionType.JUDGMENT,
+    EnumQuestionType.SINGLE_CHOICE,
+    EnumQuestionType.SUBJECTIVE,
+    EnumQuestionType.SHORT_ANSWER,
+    EnumQuestionType.ESSAY,
+    EnumQuestionType.ANALYSIS
+  ].includes(props.question.typeId)) {
+    props.question.answers = [option.no];
+    // nextQuestion?.();
+
+  } else if (props.question.typeId === EnumQuestionType.MULTIPLE_CHOICE) {
+    if (props.question.answers.includes(option.no)) {
+      props.question.answers = props.question.answers.filter(item => item !== option.no);
+    } else {
+      props.question.answers.push(option.no);
+    }
+  }
+
+  props.question.isNotKnow = false;
+  checkIsDone(props.question);
+  if (props.question.isSubQuestion) {
+    // 同时检查父题是否已完成
+    checkIsDone(props.parentQuestion);
+  }
+  if (props.isSubQuestion) {
+    emit('select', props.question);
+  } else {
+    changeQuestion()
+  }
+}
+
+const checkIsDone = (question?: Study.Question) => {
+  if (!question) {
+    return;
+  }
+  if (question?.subQuestions && question?.subQuestions.length > 0) {
+    question.isDone = question.subQuestions.every(q => q.answers.length > 0 || q.isNotKnow);
+  } else {
+    question.isDone = question.answers.length > 0 || question.isNotKnow;
+  }
+}
+// 子题选中方法
+const handleSelectOption = () => {
+  findNotDoneSubQuestion();
+}
+const handleSelectNotKnow = () => {
+  findNotDoneSubQuestion();
+}
+// 查找是否有子题没有做,有的话就自动滚动到目标处
+const findNotDoneSubQuestion = () => {
+  console.log(props)
+  if (props.question.subQuestions.length - 1 === props.subQuestionIndex) {
+    changeQuestion();
+  } else {
+    changeSubQuestion((props.subQuestionIndex ?? 0) + 1);
+  }
+  // const notDoneSubQuestion = props.question.subQuestions.find(q => !q.isDone);
+  // if (notDoneSubQuestion) {
+  //   if (notDoneSubQuestion.subIndex !== undefined) {
+  //     changeSubQuestion(notDoneSubQuestion.subIndex);
+  //   }
+  // } else {
+  //   // 是否当前是最后一个子题
+  //   if (props.question.subQuestions.length - 1 === props.subQuestionIndex) {
+  //     changeQuestion();
+  //   } else {
+  //     changeSubQuestion(props.subQuestionIndex ?? 0 + 1);
+  //   }
+  // }
+}
+const changeSubQuestion = (index: number) => {
+  emit('changeSubQuestion', index);
+}
+const changeQuestion = () => {
+  emit('changeQuestion', props.question);
+}
+
+const isSelected = (option: Study.QuestionOption) => {
+  const { typeId, answers } = props.question;
+  if ([
+    EnumQuestionType.JUDGMENT,
+    EnumQuestionType.SINGLE_CHOICE,
+    EnumQuestionType.SUBJECTIVE,
+    EnumQuestionType.SHORT_ANSWER,
+    EnumQuestionType.ESSAY,
+    EnumQuestionType.ANALYSIS
+  ].includes(typeId)) {
+    return answers.includes(option.no);
+  } else if (typeId === EnumQuestionType.MULTIPLE_CHOICE) {
+    return answers.includes(option.no);
+  }
+  return false;
+}
+</script>
+
+<style lang="scss" scoped>
+.answer-wrap {
+  box-shadow: 0 0 10px 0px rgba(0, 0, 0, 0.06);
+}
+
+.prefix {
+  @apply relative pl-20 before:content-[''] before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:w-[3px] before:h-[15px] before:bg-primary before:rounded-full;
+}
+
+.is-sub-question {
+  @apply px-0;
+
+  .question-options {
+    @apply mt-0;
+  }
+}
+
+.is-main-question {
+  @apply px-40;
+
+  >.question-options {
+    @apply mt-40;
+  }
+}
+
+.question-item {
+  .question-type {
+    @apply mb-20 text-32 text-fore-subtitle font-bold;
+  }
+
+  .sub-question-type {
+    @apply text-28 text-fore-light font-bold;
+  }
+
+  .question-content {
+    @apply text-32 text-fore-title break-words;
+  }
+
+  .question-options {
+
+    .question-option {
+      @apply flex items-center px-30 py-24 bg-back rounded-8 border border-none border-transparent;
+
+      .question-option-index {
+        @apply w-40 h-40 rounded-full bg-transparent text-30 text-fore-light font-bold flex items-center justify-center flex-shrink-0;
+      }
+
+      .question-option-content {
+        @apply text-28 text-fore-title ml-20 flex-1 min-w-0;
+      }
+    }
+
+    .question-option-selected {
+      @apply bg-[#b5eaff8e];
+
+      .question-option-index {
+        @apply bg-primary text-white;
+      }
+
+      .question-option-content {
+        @apply text-primary;
+      }
+    }
+
+    .question-option-not-know {
+      @apply bg-[#b5eaff8e];
+
+      .question-option-content {
+        @apply text-primary;
+      }
+    }
+
+    .question-option-correct,
+    .question-option-miss {
+      @apply bg-[#E7FCF8] border-[#E7FCF8] text-[#2CC6A0];
+
+      .question-option-index {
+        @apply text-[#2CC6A0];
+      }
+
+      .question-option-content {
+        @apply text-[#2CC6A0];
+      }
+    }
+
+    .question-option-miss {
+      @apply relative overflow-hidden;
+
+      &::before {
+        content: '';
+        position: absolute;
+        right: -56rpx;
+        top: 15rpx;
+        width: 180rpx;
+        height: 36rpx;
+        background: rgba(255, 91, 92, 0.2);
+        transform: rotate(30deg);
+        box-shadow: 0 2rpx 4rpx rgba(255, 91, 92, 0.1);
+      }
+
+      &::after {
+        content: '漏选';
+        position: absolute;
+        right: -8rpx;
+        top: 14rpx;
+        width: 100rpx;
+        height: 32rpx;
+        color: #FF5B5C;
+        font-size: 20rpx;
+        // font-weight: bold;
+        transform: rotate(30deg);
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        line-height: 1;
+      }
+    }
+
+    .question-option-incorrect {
+      @apply bg-[#FEEDE9] border-[#FEEDE9] text-[#FF5B5C];
+
+      .question-option-index {
+        @apply text-[#FF5B5C];
+      }
+
+      .question-option-content {
+        @apply text-[#FF5B5C];
+      }
+
+    }
+
+    .question-option+.question-option {
+      @apply mt-24;
+    }
+  }
+}
+</style>

+ 24 - 411
src/pagesStudy/pages/exam-start/components/question-item.vue

@@ -1,430 +1,43 @@
 <template>
-  <view class="question-item" :id="`qs_${question.id}`">
-    <view class="is-main-question">
-      <view v-if="question.typeId && !isSubQuestion" class="question-type">
-        {{ questionTypeDesc[question.typeId as EnumQuestionType] }}
-      </view>
-      <view class="question-content" :class="{ 'mt-30': isSubQuestion }">
-        <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)">
-          <template v-if="!readonly">
-            <view v-if="!isOnlySubjective" class="question-option-index">{{ option.no }}</view>
-            <view v-else>
-              <uv-icon name="info-circle" :color="isSelected(option) ? '#31A0FC' : '#999'" size="18" />
-            </view>
-          </template>
-          <view v-else>
-            <uv-icon v-if="isOptionCorrect(question, 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="getOptionContent(option)" containerStyle="display:inline"
-              contentStyle="word-break:break-word;"></uv-parse>
-          </view>
-        </view>
-        <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 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-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>
-      <!-- 阅卷模式下显示答案 -->
-      <template v-if="readonly">
-        <view v-if="question.subQuestions.length === 0" 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 v-if="!isOnlySubjective" 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 v-if="!isOnlySubjective" 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 v-if="isOnlySubjective" class="text-left mt-10 px-20">
-            <uv-parse :content="'参考答案:' + question.answer2"></uv-parse>
-          </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>
-        </view>
-      </view>
-    </view>
-    <view v-if="question.subQuestions.length" class="is-sub-question">
+  <view class="question-item">
+    <question-title :question="question" />
+    <question-options :question="question" />
+    <question-result :question="question" />
+    <question-parse :question="question" />
+    <view v-if="question.subQuestions.length">
       <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="changeSubQuestion(subIndex)">
+            v-for="(subQuestion, subIndex) in question.subQuestions" @click="setSubQuestionIndex(subIndex)">
             {{ question.index + question.offset + subIndex + 1 }}
           </view>
         </view>
       </scroll-view>
-      <view v-if="subQuestion">
-        <question-item :question="subQuestion" :parent-question="question" :readonly="readonly" :is-sub-question="true"
-          :index="index" :total="question.subQuestions.length" @select="handleSelectOption"
-          @notKnow="handleSelectNotKnow" />
+      <view v-if="currentSubQuestion" class="mt-20">
+        <question-item :question="currentSubQuestion" />
       </view>
     </view>
   </view>
 </template>
-
 <script lang="ts" setup>
-import { Study } from '@/types';
+import QuestionTitle from './question-title.vue';
+import QuestionOptions from './question-options.vue';
+import questionResult from './question-result.vue';
+import QuestionParse from './question-parse.vue';
+import { EnumQuestionType, EnumReviewMode } from '@/common/enum';
 import { useExam } from '@/composables/useExam';
-import { EnumQuestionType } from '@/common/enum';
-import { NEXT_QUESTION, PREV_QUESTION, NEXT_QUESTION_QUICKLY, PREV_QUESTION_QUICKLY, SHOW_SUBMIT_CONFIRM, IS_ALL_DONE } from '@/types/injectionSymbols';
-const { questionTypeDesc, isOptionCorrect } = useExam();
-const props = defineProps<{
-  question: Study.Question;
-  parentQuestion?: Study.Question;
-  readonly?: boolean;
-  isSubQuestion?: boolean;
-  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 isAllDone = inject(IS_ALL_DONE);
-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;
-  (e: 'changeQuestion', question: Study.Question): void;
-}>();
-const isOnlySubjective = computed(() => {
-  return [EnumQuestionType.SUBJECTIVE, EnumQuestionType.SHORT_ANSWER, EnumQuestionType.ESSAY].includes(props.question.typeId);
-});
-const getStyleClass = (option: Study.QuestionOption) => {
-  if (!props.readonly) {
-    return isSelected(option) ? 'question-option-selected' : '';
-  }
-  let customClass = '';
-  let { answers, answer1 } = props.question;
-  answers = answers?.filter(item => item !== ' ') || [];
-  answer1 = answer1 || ''
-  if ([EnumQuestionType.SINGLE_CHOICE, EnumQuestionType.JUDGMENT].includes(props.question.typeId)) {
-    if (answer1.includes(option.no)) {
-      customClass = 'question-option-correct';
-    } else if (answers.includes(option.no)) {
-      customClass = 'question-option-incorrect';
-    }
-  } else if ([EnumQuestionType.MULTIPLE_CHOICE].includes(props.question.typeId)) {
-    // 我选择的答案
-    if (answers.includes(option.no)) {
-      if (answer1.includes(option.no)) {
-        customClass = 'question-option-correct';
-      } else {
-        customClass = 'question-option-incorrect';
-      }
-    } else {
-      // 漏选的答案
-      if (answer1.includes(option.no)) {
-        customClass = 'question-option-miss';
-      }
-    }
-  }
-  // console.log(props.question, option)
-  return customClass;
-};
-
-const isOptionIncorrect = (option: Study.QuestionOption) => {
-  const { answers, answer1 } = props.question;
-  return answers.includes(option.no) && !answer1.includes(option.no);
-}
-const getQuestionTitle = () => {
-  if (props.isSubQuestion) {
-    const prefix = questionTypeDesc[props.question.typeId as EnumQuestionType].slice(0, 2);
-    return `[${prefix}]`;
-  }
-  return '';
-};
-const getOptionContent = (option: Study.QuestionOption) => {
-  // sb 问题,浪费几个小时
-  return option.name.replace(/\s/g, ' ');
-}
-const handleShowParse = () => {
-  props.question.showParse = !props.question.showParse;
-}
-const handleNotKnow = () => {
-  props.question.answers = [];
-  props.question.isNotKnow = !props.question.isNotKnow;
-  checkIsDone();
-  if (props.isSubQuestion) {
-    emit('select', props.question);
-  } else {
-    changeQuestion();
-  }
-}
-const handleSelect = (option: Study.QuestionOption) => {
-  if (props.readonly) {
-    return;
-  }
-  if ([
-    EnumQuestionType.JUDGMENT,
-    EnumQuestionType.SINGLE_CHOICE,
-    EnumQuestionType.SUBJECTIVE,
-    EnumQuestionType.SHORT_ANSWER,
-    EnumQuestionType.ESSAY,
-    EnumQuestionType.ANALYSIS
-  ].includes(props.question.typeId)) {
-    props.question.answers = [option.no];
-    // nextQuestion?.();
+import { Study, Transfer } from '@/types';
+import { EXAM_DATA, EXAM_PAGE_OPTIONS, EXAM_AUTO_SUBMIT } from '@/types/injectionSymbols';
 
-  } else if (props.question.typeId === EnumQuestionType.MULTIPLE_CHOICE) {
-    if (props.question.answers.includes(option.no)) {
-      props.question.answers = props.question.answers.filter(item => item !== option.no);
-    } else {
-      props.question.answers.push(option.no);
-    }
-  }
+const examPageOptions = inject(EXAM_PAGE_OPTIONS) || {} as Transfer.ExamAnalysisPageOptions;
+const examData = inject(EXAM_DATA) || {} as ReturnType<typeof useExam>;
+const { subQuestionIndex, nextQuestion, isAllDone, currentSubQuestion, setSubQuestionIndex } = examData;
+const examAutoSubmit = inject(EXAM_AUTO_SUBMIT);
 
-  props.question.isNotKnow = false;
-  checkIsDone(props.question);
-  if (props.question.isSubQuestion) {
-    // 同时检查父题是否已完成
-    checkIsDone(props.parentQuestion);
-  }
-  console.log(333)
-  if (props.isSubQuestion) {
-    emit('select', props.question);
-  } else {
-    changeQuestion()
-  }
-}
 
-const checkIsDone = (question?: Study.Question) => {
-  if (!question) {
-    return;
-  }
-  if (question?.subQuestions && question?.subQuestions.length > 0) {
-    question.isDone = question.subQuestions.every(q => q.answers.length > 0 || q.isNotKnow);
-  } else {
-    question.isDone = question.answers.length > 0 || question.isNotKnow;
-  }
-}
-// 子题选中方法
-const handleSelectOption = () => {
-  findNotDoneSubQuestion();
-}
-const handleSelectNotKnow = () => {
-  findNotDoneSubQuestion();
-}
-// 查找是否有子题没有做,有的话就自动滚动到目标处
-const findNotDoneSubQuestion = () => {
-  console.log(props)
-  if (props.question.subQuestions.length - 1 === props.subQuestionIndex) {
-    changeQuestion();
-  } else {
-    changeSubQuestion((props.subQuestionIndex ?? 0) + 1);
-  }
-  // const notDoneSubQuestion = props.question.subQuestions.find(q => !q.isDone);
-  // if (notDoneSubQuestion) {
-  //   if (notDoneSubQuestion.subIndex !== undefined) {
-  //     changeSubQuestion(notDoneSubQuestion.subIndex);
-  //   }
-  // } else {
-  //   // 是否当前是最后一个子题
-  //   if (props.question.subQuestions.length - 1 === props.subQuestionIndex) {
-  //     changeQuestion();
-  //   } else {
-  //     changeSubQuestion(props.subQuestionIndex ?? 0 + 1);
-  //   }
-  // }
-}
-const changeSubQuestion = (index: number) => {
-  emit('changeSubQuestion', index);
-}
-const changeQuestion = () => {
-  emit('changeQuestion', props.question);
-}
-
-const isSelected = (option: Study.QuestionOption) => {
-  const { typeId, answers } = props.question;
-  if ([
-    EnumQuestionType.JUDGMENT,
-    EnumQuestionType.SINGLE_CHOICE,
-    EnumQuestionType.SUBJECTIVE,
-    EnumQuestionType.SHORT_ANSWER,
-    EnumQuestionType.ESSAY,
-    EnumQuestionType.ANALYSIS
-  ].includes(typeId)) {
-    return answers.includes(option.no);
-  } else if (typeId === EnumQuestionType.MULTIPLE_CHOICE) {
-    return answers.includes(option.no);
-  }
-  return false;
-}
+const props = defineProps<{
+  question: Study.Question;
+}>();
 </script>
-
-<style lang="scss" scoped>
-.answer-wrap {
-  box-shadow: 0 0 10px 0px rgba(0, 0, 0, 0.06);
-}
-
-.prefix {
-  @apply relative pl-20 before:content-[''] before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:w-[3px] before:h-[15px] before:bg-primary before:rounded-full;
-}
-
-.is-sub-question {
-  @apply px-0;
-
-  .question-options {
-    @apply mt-0;
-  }
-}
-
-.is-main-question {
-  @apply px-40;
-
-  >.question-options {
-    @apply mt-40;
-  }
-}
-
-.question-item {
-  .question-type {
-    @apply my-20 text-32 text-fore-subtitle font-bold;
-  }
-
-  .sub-question-type {
-    @apply text-28 text-fore-light font-bold;
-  }
-
-  .question-content {
-    @apply text-32 text-fore-title break-words;
-  }
-
-  .question-options {
-
-    .question-option {
-      @apply flex items-center px-30 py-24 bg-back rounded-8 border border-none border-transparent;
-
-      .question-option-index {
-        @apply w-40 h-40 rounded-full bg-transparent text-30 text-fore-light font-bold flex items-center justify-center flex-shrink-0;
-      }
-
-      .question-option-content {
-        @apply text-28 text-fore-title ml-20 flex-1 min-w-0;
-      }
-    }
-
-    .question-option-selected {
-      @apply bg-[#b5eaff8e];
-
-      .question-option-index {
-        @apply bg-primary text-white;
-      }
-
-      .question-option-content {
-        @apply text-primary;
-      }
-    }
-
-    .question-option-not-know {
-      @apply bg-[#b5eaff8e];
-
-      .question-option-content {
-        @apply text-primary;
-      }
-    }
-
-    .question-option-correct,
-    .question-option-miss {
-      @apply bg-[#E7FCF8] border-[#E7FCF8] text-[#2CC6A0];
-
-      .question-option-index {
-        @apply text-[#2CC6A0];
-      }
-
-      .question-option-content {
-        @apply text-[#2CC6A0];
-      }
-    }
-
-    .question-option-miss {
-      @apply relative overflow-hidden;
-
-      &::before {
-        content: '';
-        position: absolute;
-        right: -56rpx;
-        top: 15rpx;
-        width: 180rpx;
-        height: 36rpx;
-        background: rgba(255, 91, 92, 0.2);
-        transform: rotate(30deg);
-        box-shadow: 0 2rpx 4rpx rgba(255, 91, 92, 0.1);
-      }
-
-      &::after {
-        content: '漏选';
-        position: absolute;
-        right: -8rpx;
-        top: 14rpx;
-        width: 100rpx;
-        height: 32rpx;
-        color: #FF5B5C;
-        font-size: 20rpx;
-        // font-weight: bold;
-        transform: rotate(30deg);
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        line-height: 1;
-      }
-    }
-
-    .question-option-incorrect {
-      @apply bg-[#FEEDE9] border-[#FEEDE9] text-[#FF5B5C];
-
-      .question-option-index {
-        @apply text-[#FF5B5C];
-      }
-
-      .question-option-content {
-        @apply text-[#FF5B5C];
-      }
-
-    }
-
-    .question-option+.question-option {
-      @apply mt-24;
-    }
-  }
-}
-</style>
+<style lang="scss" scoped></style>

+ 273 - 0
src/pagesStudy/pages/exam-start/components/question-options.vue

@@ -0,0 +1,273 @@
+<template>
+  <view class="question-options">
+    <view class="question-option" v-for="option in question.options" :class="getStyleClass(option)" :key="option.id"
+      @click="handleSelect(option)">
+      <template v-if="!isReadOnly">
+        <view v-if="!isOnlySubjective" class="question-option-index">{{ option.no }}</view>
+        <view v-else>
+          <uv-icon name="info-circle" :color="option.isSelected ? '#31A0FC' : '#999'" size="18" />
+        </view>
+      </template>
+      <view v-else>
+        <uv-icon v-if="option.isCorrect" name="checkmark-circle-fill" color="#2CC6A0" size="22" />
+        <uv-icon v-else-if="!option.isCorrect && option.isSelected" 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="getOptionContent(option)" containerStyle="display:inline"
+          contentStyle="word-break:break-word;"></uv-parse>
+      </view>
+    </view>
+    <!-- 添加不会选项 -->
+    <view v-if="question.options.length && !isReadOnly && !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 class="mt-40" v-if="practiceSettings.reviewMode === EnumReviewMode.DURING_ANSWER && isMultipleChoice">
+      <ie-button type="primary" size="mini" :round="4" :shadow="false" custom-class="w-160"
+        @click="handleSubmit">提交</ie-button>
+    </view>
+    <view v-if="!isReadOnly && 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-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>
+</template>
+<script lang="ts" setup>
+import { EnumQuestionType, EnumReviewMode } from '@/common/enum';
+import { useExam } from '@/composables/useExam';
+import { Study, Transfer } from '@/types';
+import { EXAM_DATA, EXAM_PAGE_OPTIONS, EXAM_AUTO_SUBMIT } from '@/types/injectionSymbols';
+
+const examPageOptions = inject(EXAM_PAGE_OPTIONS) || {} as Transfer.ExamAnalysisPageOptions;
+const examData = inject(EXAM_DATA) || {} as ReturnType<typeof useExam>;
+const { practiceSettings, nextQuestion, isAllDone } = examData;
+const examAutoSubmit = inject(EXAM_AUTO_SUBMIT);
+
+const props = defineProps<{
+  question: Study.Question;
+}>();
+const isReadOnly = computed(() => {
+  const { readonly } = examPageOptions;
+  if (readonly) {
+    return true;
+  }
+  // 练习模式下,需要背题模式且题目已经做过且解析过
+  if (practiceSettings.value.reviewMode === EnumReviewMode.DURING_ANSWER && props.question.hasParsed) {
+    return true;
+  }
+  return false;
+});
+const isOnlySubjective = computed(() => {
+  return [EnumQuestionType.SUBJECTIVE, EnumQuestionType.SHORT_ANSWER, EnumQuestionType.ESSAY].includes(props.question.typeId);
+});
+const isMultipleChoice = computed(() => {
+  return props.question.typeId === EnumQuestionType.MULTIPLE_CHOICE;
+});
+
+const getStyleClass = (option: Study.QuestionOption) => {
+  if (!isReadOnly.value) {
+    return option.isSelected ? 'question-option-selected' : '';
+  }
+  let customClass = '';
+  let { answers, answer1 } = props.question;
+  answers = answers?.filter(item => item !== ' ') || [];
+  answer1 = answer1 || ''
+  if ([EnumQuestionType.SINGLE_CHOICE, EnumQuestionType.JUDGMENT].includes(props.question.typeId)) {
+    if (option.isCorrect) {
+      customClass = 'question-option-correct';
+    } else if (option.isIncorrect) {
+      customClass = 'question-option-incorrect';
+    }
+  } else if ([EnumQuestionType.MULTIPLE_CHOICE].includes(props.question.typeId)) {
+    // 我选择的答案
+    if (option.isSelected) {
+      if (option.isCorrect) {
+        customClass = 'question-option-correct';
+      } else {
+        customClass = 'question-option-incorrect';
+      }
+    } else {
+      // 漏选的答案
+      if (option.isMissed) {
+        customClass = 'question-option-miss';
+      }
+    }
+  }
+  // console.log(props.question, option)
+  return customClass;
+};
+
+const getOptionContent = (option: Study.QuestionOption) => {
+  // sb 问题,浪费几个小时
+  return option.name.replace(/\s/g, ' ');
+}
+
+// 多选题要手动提交才能认为是作答结束
+const handleSubmit = () => {
+  props.question.hasParsed = true;
+}
+
+const handleNotKnow = () => {
+  props.question.answers = [];
+  props.question.isNotKnow = !props.question.isNotKnow;
+  props.question.hasParsed = true;
+  handleNext();
+}
+
+const handleNext = () => {
+  // 如果是正常的练习,默认下一题,如果是背题模式,需要根据是否自动下一题来决定
+  if (practiceSettings.value.reviewMode === EnumReviewMode.DURING_ANSWER) {
+    if (practiceSettings.value.autoNext) {
+      nextQuestion();
+    }
+  } else {
+    nextQuestion();
+  }
+  if (isAllDone.value) {
+    examAutoSubmit?.();
+  }
+}
+
+const handleSelect = (option: Study.QuestionOption) => {
+  if (isReadOnly.value) {
+    return;
+  }
+  if ([
+    EnumQuestionType.JUDGMENT,
+    EnumQuestionType.SINGLE_CHOICE,
+    EnumQuestionType.SUBJECTIVE,
+    EnumQuestionType.SHORT_ANSWER,
+    EnumQuestionType.ESSAY,
+    EnumQuestionType.ANALYSIS
+  ].includes(props.question.typeId)) {
+    props.question.answers = [option.no];
+  } else if (props.question.typeId === EnumQuestionType.MULTIPLE_CHOICE) {
+    if (props.question.answers.includes(option.no)) {
+      props.question.answers = props.question.answers.filter(item => item !== option.no);
+    } else {
+      props.question.answers.push(option.no);
+    }
+  }
+  props.question.isNotKnow = false;
+  props.question.hasParsed = true;
+  // 多选题不自动切换下一题
+  if (props.question.typeId !== EnumQuestionType.MULTIPLE_CHOICE) {
+    handleNext();
+  }
+}
+
+const handleShowParse = () => {
+  props.question.showParse = !props.question.showParse;
+}
+</script>
+<style lang="scss" scoped>
+.question-options {
+  @apply mt-40;
+}
+
+.question-option {
+  @apply flex items-center px-30 py-24 bg-back rounded-8 border border-none border-transparent;
+
+  .question-option-index {
+    @apply w-40 h-40 rounded-full bg-transparent text-30 text-fore-light font-bold flex items-center justify-center flex-shrink-0;
+  }
+
+  .question-option-content {
+    @apply text-28 text-fore-title ml-20 flex-1 min-w-0;
+  }
+}
+
+.question-option-selected {
+  @apply bg-[#b5eaff8e];
+
+  .question-option-index {
+    @apply bg-primary text-white;
+  }
+
+  .question-option-content {
+    @apply text-primary;
+  }
+}
+
+.question-option-not-know {
+  @apply bg-[#b5eaff8e];
+
+  .question-option-content {
+    @apply text-primary;
+  }
+}
+
+.question-option-correct {
+  @apply bg-[#E7FCF8] border-[#E7FCF8] text-[#2CC6A0];
+
+  .question-option-index {
+    @apply text-[#2CC6A0];
+  }
+
+  .question-option-content {
+    @apply text-[#2CC6A0];
+  }
+}
+
+.question-option-miss {
+  @apply relative overflow-hidden;
+
+  &::before {
+    content: '';
+    position: absolute;
+    right: -56rpx;
+    top: 15rpx;
+    width: 180rpx;
+    height: 36rpx;
+    background: rgba(255, 91, 92, 0.2);
+    transform: rotate(30deg);
+    box-shadow: 0 2rpx 4rpx rgba(255, 91, 92, 0.1);
+  }
+
+  &::after {
+    content: '漏选';
+    position: absolute;
+    right: -8rpx;
+    top: 14rpx;
+    width: 100rpx;
+    height: 32rpx;
+    color: #FF5B5C;
+    font-size: 20rpx;
+    // font-weight: bold;
+    transform: rotate(30deg);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    line-height: 1;
+  }
+}
+
+.question-option-incorrect {
+  @apply bg-[#FEEDE9] border-[#FEEDE9] text-[#FF5B5C];
+
+  .question-option-index {
+    @apply text-[#FF5B5C];
+  }
+
+  .question-option-content {
+    @apply text-[#FF5B5C];
+  }
+
+}
+
+.question-option+.question-option {
+  @apply mt-24;
+}
+</style>

+ 45 - 0
src/pagesStudy/pages/exam-start/components/question-parse.vue

@@ -0,0 +1,45 @@
+<template>
+  <view v-if="(isReadOnly) || (!isReadOnly && question.showParse)" class="mt-40">
+    <!-- 主观题的答案在这里显示,其他题型在 question-result 面板显示 -->
+    <view v-if="isOnlySubjective" class="mb-20">
+      <view class="text-30 text-fore-title font-bold">答案</view>
+      <view class="mt-10 text-26 text-fore-light">
+        <uv-parse :content="question.answer2 || '略'"></uv-parse>
+      </view>
+    </view>
+    <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>
+    </view>
+  </view>
+</template>
+<script lang="ts" setup>
+import { EnumQuestionType, EnumReviewMode } from '@/common/enum';
+import { useExam } from '@/composables/useExam';
+import { Study, Transfer } from '@/types';
+import { EXAM_DATA, EXAM_PAGE_OPTIONS } from '@/types/injectionSymbols';
+
+const examPageOptions = inject(EXAM_PAGE_OPTIONS) || {} as Transfer.ExamAnalysisPageOptions;
+const examData = inject(EXAM_DATA) || {} as ReturnType<typeof useExam>;
+const { practiceSettings } = examData;
+
+const props = defineProps<{
+  question: Study.Question;
+}>();
+const isReadOnly = computed(() => {
+  const { readonly } = examPageOptions;
+  if (readonly) {
+    return true;
+  }
+  // 练习模式下,需要背题模式且题目已经做过且解析过
+  if (practiceSettings.value.reviewMode === EnumReviewMode.DURING_ANSWER && props.question.hasParsed) {
+    return true;
+  }
+  return false;
+});
+const isOnlySubjective = computed(() => {
+  return [EnumQuestionType.SUBJECTIVE, EnumQuestionType.SHORT_ANSWER, EnumQuestionType.ESSAY].includes(props.question.typeId);
+});
+
+</script>
+<style lang="scss" scoped></style>

+ 53 - 0
src/pagesStudy/pages/exam-start/components/question-result.vue

@@ -0,0 +1,53 @@
+<template>
+  <view v-if="isReadOnly && question.isLeaf"
+    class="question-result 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" :class="{ 'top-30': isOnlySubjective }" />
+    <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" :class="{ 'top-30': isOnlySubjective }" />
+    <view v-if="!isOnlySubjective" 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 v-if="!isOnlySubjective" 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>
+<script lang="ts" setup>
+import { EnumQuestionType, EnumReviewMode } from '@/common/enum';
+import { useExam } from '@/composables/useExam';
+import { Study, Transfer } from '@/types';
+import { EXAM_DATA, EXAM_PAGE_OPTIONS } from '@/types/injectionSymbols';
+
+const examPageOptions = inject(EXAM_PAGE_OPTIONS) || {} as Transfer.ExamAnalysisPageOptions;
+const examData = inject(EXAM_DATA) || {} as ReturnType<typeof useExam>;
+const { practiceSettings } = examData;
+
+const props = defineProps<{
+  question: Study.Question;
+}>();
+const isReadOnly = computed(() => {
+  const { readonly } = examPageOptions;
+  if (readonly) {
+    return true;
+  }
+  // 练习模式下,需要背题模式且题目已经做过且解析过
+  if (practiceSettings.value.reviewMode === EnumReviewMode.DURING_ANSWER && props.question.hasParsed) {
+    return true;
+  }
+  return false;
+});
+const isOnlySubjective = computed(() => {
+  return [EnumQuestionType.SUBJECTIVE, EnumQuestionType.SHORT_ANSWER, EnumQuestionType.ESSAY].includes(props.question.typeId);
+});
+</script>
+<style lang="scss" scoped>
+.question-result {
+  box-shadow: 0 0 10px 0px rgba(0, 0, 0, 0.06);
+}
+</style>

+ 64 - 0
src/pagesStudy/pages/exam-start/components/question-title.vue

@@ -0,0 +1,64 @@
+<template>
+  <view class="question-title">
+    <view v-if="question.typeId && !isSubQuestion" class="question-type">
+      <text>{{ questionTypeDesc[question.typeId as EnumQuestionType] }}</text>
+      <!-- 考试模式下显示分数 -->
+      <text v-if="showScore">({{ getScore }}分)</text>
+    </view>
+    <text v-if="isSubQuestion" class="text-nowrap text-30">
+      <text>{{ getQuestionTitle() }}</text>
+      <text v-if="isSimulation">({{ getScore }}分)</text>
+      <text v-else>&nbsp;</text>
+    </text>
+    <uv-parse :content="question.title" containerStyle="display:inline"
+      contentStyle="word-break:break-word;"></uv-parse>
+  </view>
+</template>
+<script lang="ts" setup>
+import { EnumPaperType, EnumQuestionType } from '@/common/enum';
+import { useExam } from '@/composables/useExam';
+import { Study, Transfer } from '@/types';
+import { EXAM_DATA, EXAM_PAGE_OPTIONS } from '@/types/injectionSymbols';
+
+const examPageOptions = inject(EXAM_PAGE_OPTIONS) || {} as Transfer.ExamAnalysisPageOptions;
+const examData = inject(EXAM_DATA) || {} as ReturnType<typeof useExam>;
+const { questionTypeDesc } = examData;
+
+const props = defineProps<{
+  question: Study.Question;
+}>();
+const isSimulation = computed(() => {
+  const { paperType } = examPageOptions;
+  return paperType === EnumPaperType.SIMULATED;
+});
+const isSubQuestion = computed(() => {
+  return props.question.isSubQuestion;
+});
+const showScore = computed(() => {
+  return isSimulation.value;
+});
+const getScore = computed(() => {
+  if (props.question.subQuestions && props.question.subQuestions.length > 0) {
+    return props.question.subQuestions.reduce((acc: number, curr: Study.Question) => {
+      return acc + curr.totalScore;
+    }, 0);
+  }
+  return props.question.totalScore;
+});
+const getQuestionTitle = () => {
+  if (isSubQuestion.value) {
+    const prefix = questionTypeDesc[props.question.typeId as EnumQuestionType].slice(0, 2);
+    return `[${prefix}]`;
+  }
+  return '';
+};
+</script>
+<style lang="scss" scoped>
+.question-title {
+  @apply text-32 text-fore-title break-words;
+}
+
+.question-type {
+  @apply mb-20 text-32 text-fore-subtitle font-bold;
+}
+</style>

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

@@ -1,9 +1,8 @@
 <template>
-  <scroll-view v-if="visible" scroll-y class="question-wrap" :scroll-into-view="scrollIntoView"
+  <scroll-view v-if="visible" scroll-y class="question-wrap"
     :scroll-with-animation="true">
     <view class="h-20"></view>
     <question-item :question="question" :readonly="readonly" :index="index" :subQuestionIndex="subQuestionIndex"
-      @update:question="emit('update:question', $event)" @scrollTo="handleScrollTo"
       @changeSubQuestion="handleChangeSubQuestion" @change-question="handleChangeQuestion" />
     <view class="h-20"></view>
   </scroll-view>
@@ -23,23 +22,13 @@ 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;
   (e: 'changeQuestion', question: Study.Question): void;
 }>();
 const handleChangeSubQuestion = (index: number) => {
   emit('changeSubQuestion', index);
 }
-const scrollIntoView = ref('');
-const handleScrollTo = (selector: string) => {
-  console.log('收到子组件滚动请求', selector)
-  scrollIntoView.value = '';
-  setTimeout(() => {
-    if (selector) {
-      scrollIntoView.value = selector;
-    }
-  }, 200);
-}
+
 const handleChangeQuestion = (question: Study.Question) => {
   emit('changeQuestion', question);
 }

+ 103 - 345
src/pagesStudy/pages/exam-start/exam-start.vue

@@ -1,143 +1,54 @@
 <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="!isReadOnly" class="" :class="{ 'text-red-500': practiceDuration > totalExamTime }">
-          {{ formatPracticeDuration }}
-        </view>
-        <view v-else class="text-28">用时:{{ formatPracticeDuration }}</view>
-      </template>
-    </ie-navbar>
     <block v-if="isReady">
-      <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">{{ virtualCurrentIndex + 1 }}</text>/
-          <text class="text-28 text-fore-subtitle">{{ virtualTotalCount }}</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">
-            <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"
-                  :subQuestionIndex="subQuestionIndex" :readonly="isReadOnly"
-                  @changeSubQuestion="handleChangeSubQuestion" @change-question="handleChangeQuestion" />
-              </swiper-item>
-            </block>
-          </swiper>
-        </view>
-      </view>
-      <ie-safe-toolbar :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" id="question-correct-btn" @click="handleCorrect">
-            <uv-icon name="info-circle" size="24" />
-          </view>
-          <view class="w-48 h-48 flex items-center justify-center" id="question-favorite-btn" @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" id="question-mark-btn" @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" id="question-calendar-btn" @click="handleCalendar">
-            <uv-icon name="calendar" size="28" />
-          </view>
-        </view>
-      </ie-safe-toolbar>
+      <exam-navbar :total-exam-time="totalExamTime" @left-click="handleLeftClick" @right-click="handleRightClick" />
+      <exam-subtitle />
+      <exam-swiper @submit="beforeSubmit" />
+      <exam-toolbar @submit="beforeSubmit" />
     </block>
   </ie-page>
-  <question-stats-popup ref="questionStatsPopupRef" :readonly="isReadOnly">
-    <template #title>
-      <view class="ml-20">
-        <text class="text-30 text-primary">{{ doneCount }}</text>
-        <text>/</text>
-        <text class="text-30 text-fore-light">{{ virtualTotalCount }}</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 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.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="{
-                      'is-done': !isReadOnly && qs.question.isDone,
-                      'is-not-know': !isReadOnly && qs.question.isNotKnow,
-                      'is-mark': !isReadOnly && qs.question.isMark,
-                      'is-correct': isReadOnly && qs.question.isCorrect,
-                      'is-incorrect': isReadOnly && !qs.question.isCorrect,
-                    }">
-                    <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" /> -->
-                  </view>
-                </view>
-              </view>
-            </template>
-          </view>
-        </scroll-view>
-      </view>
-      <view v-if="!isReadOnly" 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>
-  <question-correct-popup ref="questionCorrectPopupRef" @close="handleCorrectClose" />
   <fast-guide v-model:show="guideShow" :list="guideList" v-model:index="guideIndex"
     @close="handleGuideClose"></fast-guide>
   <question-swiper-tip :visible="showSwiperTip" @next="handleSwiperTipNext" />
+  <exam-mode ref="examModeRef" />
 </template>
 
 <script lang="ts" setup>
-import QuestionWrap from './components/question-wrap.vue';
-import QuestionStatsPopup from './components/question-stats-popup.vue';
-import QuestionProgress from './components/question-progress.vue';
-import QuestionCorrectPopup from './components/question-correct-popup.vue';
+import ExamNavbar from './components/exam-navbar.vue';
+import ExamSubtitle from './components/exam-subtitle.vue';
+import ExamSwiper from './components/exam-swiper.vue';
+import ExamToolbar from './components/exam-toolbar.vue';
+import ExamMode from './components/exam-mode.vue';
+
 import QuestionSwiperTip from './components/question-swiper-tip.vue';
 import { useTransferPage } from '@/hooks/useTransferPage';
 import { useUserStore } from '@/store/userStore';
 import { EnumPaperType, EnumQuestionType } from '@/common/enum';
-import { getOpenExaminee, getPaper, commitExamineePaper, collectQuestion, cancelCollectQuestion, beginExaminee, getExamineeResult } from '@/api/modules/study';
+import { getOpenExaminee, getPaper, commitExamineePaper, beginExaminee, getExamineeResult } from '@/api/modules/study';
 import { useExam } from '@/composables/useExam';
-import { Study } from '@/types';
-import { NEXT_QUESTION, PREV_QUESTION, NEXT_QUESTION_QUICKLY, PREV_QUESTION_QUICKLY, SHOW_SUBMIT_CONFIRM, IS_ALL_DONE } from '@/types/injectionSymbols';
+import { Study, Transfer } from '@/types';
+import {
+  EXAM_AUTO_SUBMIT,
+  EXAM_PAGE_OPTIONS,
+  EXAM_DATA
+} from '@/types/injectionSymbols';
 
 const userStore = useUserStore();
 // import { Examinee, ExamPaper, ExamPaperSubmit } from '@/types/study';
-const { prevData, transferBack, transferTo } = useTransferPage();
-const { setQuestionList, questionList, flatQuestionList, groupedQuestionList, questionTypeDesc,
-  currentIndex, totalCount, virtualCurrentIndex, virtualTotalCount, subQuestionIndex, setSubQuestionIndex,
-  doneCount,
-  notDoneCount, isAllDone, nextQuestion, prevQuestion, nextQuestionQuickly, prevQuestionQuickly, swiperDuration,
-  formatPracticeDuration, practiceDuration, startPracticeDuration, stopPracticeDuration, changeIndex, setDuration, reset } = useExam();
+const { prevData, transferBack, transferTo } = useTransferPage<Transfer.ExamAnalysisPageOptions, {}>();
+const examData = useExam();
+const { setQuestionList, questionList, flatQuestionList, setPracticeSettings, setSubQuestionIndex,
+  notDoneCount, isAllDone, nextQuestion, prevQuestion, nextQuestionQuickly, prevQuestionQuickly,
+  practiceDuration, startTiming, stopTiming, changeIndex, setDuration } = examData;
 
 //
 const showSwiperTip = ref(false);
 const guideShow = ref(false);
 const guideList = ref([
   {
-    target: '#question-correct-btn',
+    target: '#question-calendar-btn',
     position: 'top',
-    msg: '[题目纠错]\n点击题目纠错,帮助我们改进题目'
+    msg: '[答题卡]\n查看答题卡,掌握考试进度'
   },
   {
     target: '#question-favorite-btn',
@@ -150,45 +61,42 @@ const guideList = ref([
     msg: '[题目标记]\n标记的题目可以在答题卡中快速找到'
   },
   {
-    target: '#question-calendar-btn',
+    target: '#question-correct-btn',
     position: 'top',
-    msg: '[答题卡]\n查看答题卡,掌握考试进度'
+    msg: '[题目纠错]\n点击题目纠错,帮助我们改进题目'
   }
 ]);
 const guideIndex = ref(0);
-// 
-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 examineeId = ref<number | undefined>(undefined);
-// const examineerData = ref<Study.Examinee>({} as Study.Examinee);
 const paperData = ref<Study.ExamPaper>({} as Study.ExamPaper);
 
-provide(NEXT_QUESTION, nextQuestion);
-provide(PREV_QUESTION, prevQuestion);
-provide(NEXT_QUESTION_QUICKLY, nextQuestionQuickly);
-provide(PREV_QUESTION_QUICKLY, prevQuestionQuickly);
-provide(IS_ALL_DONE, isAllDone);
-
-const pageTitle = computed(() => {
-  if (isReadOnly.value) {
-    return '考试解析';
+/**
+ * 自动提交
+ */
+const autoSubmit = () => {
+  if (hasShowSubmitConfirm.value) {
+    return;
   }
-  return isExam.value ? '考试' : '练习';
-});
+  hasShowSubmitConfirm.value = true;
+  beforeSubmit();
+}
+
+provide(EXAM_PAGE_OPTIONS, prevData.value);
+provide(EXAM_DATA, examData);
+provide(EXAM_AUTO_SUBMIT, autoSubmit);
+
 const isExam = computed(() => {
+  // prevData.value
   return prevData.value.paperType === EnumPaperType.SIMULATED;
 });
-const pageSubtitle = computed(() => {
-  return prevData.value.name;
-});
+
 const isReadOnly = computed(() => {
-  return prevData.value.readonly;
+  return prevData.value.readonly || false;
 });
 const handleLeftClick = () => {
   if (!isReady.value || isReadOnly.value) {
@@ -197,9 +105,11 @@ const handleLeftClick = () => {
   }
   beforeQuit();
 };
-const handleSwiperChange = (e: any) => {
-  currentIndex.value = e.detail.current;
-};
+const examModeRef = ref();
+const handleRightClick = () => {
+  examModeRef.value.open();
+}
+
 const beforeQuit = () => {
   const { paperType } = prevData.value;
   if (!isReady.value || isReadOnly.value) {
@@ -218,92 +128,16 @@ const beforeQuit = () => {
     }
   });
 };
-const currentQuestion = computed(() => {
-  const qs = questionList.value[currentIndex.value];
-  if (qs.subQuestions && qs.subQuestions.length > 0) {
-    return qs.subQuestions[subQuestionIndex.value] || {};
-  }
-  return qs || {};
-});
-const hanadleNavigate = (question: Study.Question, index: number) => {
-  console.log(question, index)
-  if (question.isSubQuestion) {
-    changeIndex(question.parentIndex || 0);
-    setTimeout(() => {
-      setSubQuestionIndex(question.subIndex || 0);
-    });
-  } else {
-    changeIndex(index);
-  }
-}
-/// 问题纠错
-const questionCorrectPopupRef = ref();
-const handleCorrect = () => {
-  stopTime();
-  questionCorrectPopupRef.value.open(currentQuestion.value.id);
-}
-const handleCorrectClose = () => {
-  startTime();
-}
-/**
- * 收藏
- */
-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 = () => {
-  startPracticeDuration();
+  startTiming();
 }
 
 const stopTime = () => {
-  stopPracticeDuration();
+  stopTiming();
 }
 
-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) {
-    if (!isReadOnly.value) {
-      beforeSubmit();
-    }
-  }
-  isAnimationFinish.value = true;
-  transitionStartX.value = null;
-  transitionEndX.value = null;
-};
 
 const beforeSubmit = () => {
   const text = notDoneCount.value > 0 ? `还有${notDoneCount.value}题未做,确认交卷?` : '是否确认交卷?';
@@ -320,40 +154,6 @@ const beforeSubmit = () => {
   });
 }
 
-/**
- * 重新作答
- */
-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();
-  }
-}
-
 /**
  * 提交试卷
  * @param tempSave 是否临时保存
@@ -404,48 +204,29 @@ const handleSubmit = (tempSave: boolean = false) => {
         });
       }
     } else {
-      setTimeout(async () => {
+      if (!tempSave) {
+        setTimeout(async () => {
+          uni.$ie.hideLoading();
+          await nextTick();
+          transferTo('/pagesStudy/pages/knowledge-practice-detail/knowledge-practice-detail', {
+            data: {
+              examineeId: examineeId.value,
+              name: prevData.value.practiceInfo?.name,
+              directed: prevData.value.practiceInfo?.directed
+            },
+            type: 'redirectTo'
+          });
+        }, 2500);
+      } else {
         uni.$ie.hideLoading();
-        await nextTick();
-        transferTo('/pagesStudy/pages/knowledge-practice-detail/knowledge-practice-detail', {
-          data: {
-            examineeId: examineeId.value,
-            name: prevData.value.practiceInfo.name,
-            directed: prevData.value.practiceInfo.directed
-          },
-          type: 'redirectTo'
+        nextTick(() => {
+          transferBack();
         });
-      }, !tempSave ? 2500 : 0);
+      }
     }
-
   }, 1000);
 }
 
-const handleChangeSubQuestion = (index: number) => {
-  console.log(index, 111)
-  setSubQuestionIndex(index);
-}
-const handleChangeQuestion = (question: Study.Question) => {
-  if (isAllDone.value && !hasShowSubmitConfirm.value) {
-    autoSubmit();
-    return;
-  }
-  // 判断是多选还是单选
-  if (question.typeId === EnumQuestionType.MULTIPLE_CHOICE) {
-    // 多选题,选择不会时切换下一题,否则不自动切换
-    if (question.isNotKnow) {
-      nextQuestion?.();
-    } else {
-      return;
-    }
-  } else {
-    console.log(question, 222)
-    // if (question.isDone) {
-    //   nextQuestion?.();
-    // }
-    nextQuestion?.();
-  }
-}
 /**
  * 恢复上次做题历史数据
  * @param savedQuestion 上次做题历史数据
@@ -468,22 +249,31 @@ const restoreQuestion = (savedQuestion: Study.ExamineeQuestion[], fullQuestion:
       item.isFavorite = savedQs.isFavorite;
       item.isNotKnow = savedQs.isNotKnow;
       item.parse = savedQs.parse;
+      item.totalScore = savedQs.totalScore;
       if (item.subQuestions) {
-        console.log('对比', JSON.parse(JSON.stringify(savedQs.subQuestions)), JSON.parse(JSON.stringify(item.subQuestions)))
         restoreQuestion(savedQs.subQuestions, item.subQuestions);
       }
     }
   }
-  console.log(fullQuestion, 123)
   return fullQuestion;
 }
 const loadPracticeData = async () => {
-  const { paperType, practiceInfo } = prevData.value;
-  const { data } = await getOpenExaminee({
-    paperType: paperType,
-    relateId: practiceInfo.relateId,
-    directed: practiceInfo.directed
-  });
+  const { paperType, readonly, practiceInfo } = prevData.value;
+  let data: Study.Examinee | null = null;
+  if (readonly) {
+    if (practiceInfo?.examineeId) {
+      const res = await getExamineeResult(practiceInfo.examineeId);
+      data = res.data;
+    }
+  } else {
+    const res = await getOpenExaminee({
+      paperType: paperType,
+      relateId: practiceInfo?.relateId,
+      directed: practiceInfo?.directed || false
+    });
+    data = res.data || {};
+  }
+
   if (!data) {
     uni.$ie.hideLoading();
     transferBack();
@@ -496,20 +286,22 @@ const loadPracticeData = async () => {
 const loadSimulationData = async () => {
   const { paperType, readonly, simulationInfo } = prevData.value;
   let data: Study.Examinee;
-  if (readonly) {
-    const res = await getExamineeResult(simulationInfo.examineeId);
-    data = res.data;
-  } else {
-    const res = await beginExaminee(simulationInfo.examineeId);
-    data = res.data;
-  }
-  if (!data) {
-    uni.$ie.hideLoading();
-    transferBack();
-    return;
+  if (simulationInfo?.examineeId) {
+    if (readonly) {
+      const res = await getExamineeResult(simulationInfo.examineeId);
+      data = res.data;
+    } else {
+      const res = await beginExaminee(simulationInfo.examineeId);
+      data = res.data || {};
+    }
+    if (!data) {
+      uni.$ie.hideLoading();
+      transferBack();
+      return;
+    }
+    totalExamTime.value = data.paperInfo?.time || 0;
+    combinePaperData(data, paperType);
   }
-  totalExamTime.value = data.paperInfo?.time || 0;
-  combinePaperData(data, paperType);
 }
 const combinePaperData = async (examinee: Study.Examinee, paperType: EnumPaperType) => {
   examineeId.value = examinee.examineeId;
@@ -532,15 +324,13 @@ const combinePaperData = async (examinee: Study.Examinee, paperType: EnumPaperTy
     }
     setTimeout(() => {
       if (targetQuestion?.isSubQuestion) {
-        console.log(targetQuestion.subIndex, 888)
         setSubQuestionIndex(targetQuestion.subIndex || 0);
       }
     }, 50);
-    // const questionIndex = prevData.value.questionId ? paperData.value.questions.findIndex(item => item.id === prevData.value.questionId) : 0;
-    // changeIndex(questionIndex);
-    console.log(groupedQuestionList.value, 123, flatQuestionList.value)
     await new Promise(resolve => setTimeout(resolve, 50));
     await nextTick();
+    // 读取用户练习设置
+    // setPracticeSettings(userStore.practiceSettings);
     isReady.value = true;
     console.log('试卷信息', res)
     if (!userStore.isExamGuideShow) {
@@ -581,36 +371,4 @@ onLoad(() => {
 });
 </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];
-}
-
-.is-correct {
-  @apply text-[#2CC6A0] border-[#E7FCF8] bg-[#E7FCF8];
-}
-
-.is-incorrect {
-  @apply text-[#FF5B5C] border-[#FEEDE9] bg-[#FEEDE9];
-}
-</style>
+<style lang="scss" scoped></style>

+ 1 - 3
src/pagesStudy/pages/index/compoentns/index-banner.vue

@@ -45,9 +45,7 @@ const handleOpenPlan = async () => {
   }
 };
 const handleTest = () => {
-  if (openVipPopup) {
-    openVipPopup();
-  }
+
 };
 </script>
 <style lang="scss" scoped></style>

+ 11 - 14
src/pagesStudy/pages/index/index.vue

@@ -28,7 +28,6 @@
     <view class="mx-30 mt-20">
       <view class="flex items-center gap-x-28">
         <view class="bg-gradient-to-r from-[#0088FE] to-[#31A0FC] flex-1 rounded-15 relative overflow-hidden">
-          <!-- <view class="h-70 z-1 relative"></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>
@@ -72,8 +71,6 @@ import IndexExamRecord from './compoentns/index-exam-record.vue';
 import { EnumDictName, EnumExamType } from '@/common/enum';
 import { useUserStore } from '@/store/userStore';
 import { useTransferPage } from '@/hooks/useTransferPage';
-import { getDirectedSchool } from '@/api/modules/study';
-import { DirectedSchool } from '@/types/study';
 import IePage from '@/components/ie-page/ie-page.vue';
 
 const { transferTo } = useTransferPage();
@@ -81,10 +78,8 @@ const userStore = useUserStore();
 const hasTestAndRecord = computed(() => userStore.getExamType !== EnumExamType.VHS);
 // 通过 ref 获取 ie-page 组件实例
 const iePageRef = ref<InstanceType<typeof IePage>>();
-
-const directedSchoolData = ref<DirectedSchool[]>([]);
-const firstDirectedSchool = computed(() => directedSchoolData.value[0] || {});
-const hasDirectedSchool = computed(() => directedSchoolData.value.length > 0);
+const { hasDirectedSchool, directedSchoolList } = toRefs(userStore);
+const firstDirectedSchool = computed(() => directedSchoolList.value[0] || {});
 
 const handlePracticeAll = () => {
   transferTo('/pagesStudy/pages/knowledge-practice/knowledge-practice', {
@@ -95,7 +90,7 @@ const handlePracticeAll = () => {
 }
 const handlePracticeDirected = async () => {
   if (!hasDirectedSchool.value) {
-    uni.$ie.showToast('请先选择定向院校');
+    addTarget();
     return;
   }
   transferTo('/pagesStudy/pages/knowledge-practice/knowledge-practice', {
@@ -115,16 +110,18 @@ const handleSetting = async () => {
         confirmText: '知道了'
       });
     }
+    transferTo('/pagesStudy/pages/targeted-setting/targeted-setting');
+  } else {
+    addTarget();
   }
-  transferTo('/pagesStudy/pages/targeted-setting/targeted-setting');
+}
+const addTarget = () => {
+  transferTo('/pagesStudy/pages/targeted-add/targeted-add');
 }
 const loadData = async () => {
-  const res = await getDirectedSchool();
-  if (res.data) {
-    directedSchoolData.value = res.data;
-  }
+  await userStore.getDirectedSchoolList();
 }
-onShow(() => {
+onLoad(() => {
   loadData();
 });
 </script>

+ 29 - 16
src/pagesStudy/pages/knowledge-practice-detail/knowledge-practice-detail.vue

@@ -23,7 +23,7 @@
           </view>
         </view>
       </view>
-      <exam-stat :data="examineeData" :show-stats="false" @detail="handleDetail" />
+      <exam-stat :data="examineeData" :show-stats="false" @detail="handleQuestionDetail" />
     </view>
     <ie-safe-toolbar :height="84" :shadow="false">
       <view class="h-[84px] px-46 bg-white flex items-center justify-between gap-x-40">
@@ -44,19 +44,19 @@ import { useTransferPage } from '@/hooks/useTransferPage';
 import { Study, Transfer } from '@/types';
 import { getExamineeResult } from '@/api/modules/study';
 import { EnumPaperType } from '@/common/enum';
-const { prevData, transferTo } = useTransferPage<Transfer.PracticeResult, {}>();
+const { prevData, transferTo } = useTransferPage<Transfer.PracticeResultPageOptions, Transfer.ExamAnalysisPageOptions>();
 
 
 const rightRate = computed(() => {
   const { totalCount = 0, wrongCount = 0 } = examineeData.value || {};
-  return Math.round((totalCount - wrongCount) / totalCount * 100) || 0;
+  const rate = (totalCount - wrongCount) / totalCount * 100;
+  return rate < 0.1 ? 0.1 : Number(rate.toFixed(1));
 });
 const paperName = computed(() => {
   return '知识点练习-' + prevData.value.name;
 });
 const examineeData = ref<Study.Examinee>();
 const pageTitle = computed(() => {
-  // return prevData.value.directed ? '定向练习结果' : '全量练习结果';
   return '练习结果';
 });
 const formatTime = (time: number) => {
@@ -73,22 +73,31 @@ const formatTime = (time: number) => {
     return `${minutes}分${seconds}秒`;
   }
 };
-const handleDetail = (item: Study.Question) => {
+const handleQuestionDetail = (item: Study.Question) => {
   console.log(item, examineeData)
+  if (!examineeData.value) {
+    return;
+  }
   transferTo('/pagesStudy/pages/exam-start/exam-start', {
     data: {
       name: paperName.value,
-      paperType: EnumPaperType.SIMULATED,
+      paperType: EnumPaperType.PRACTICE,
       readonly: true,
       questionId: item.id,
-      simulationInfo: {
-        examineeId: examineeData.value?.examineeId,
-      }
+      practiceInfo: {
+        name: prevData.value.name,
+        relateId: examineeData.value.knowledgeId,
+        directed: prevData.value.directed,
+        examineeId: examineeData.value.examineeId
+      },
     }
   });
 }
 const handleStartPractice = () => {
   const { knowledgeId } = examineeData.value || {};
+  if (!knowledgeId) {
+    return;
+  }
   transferTo('/pagesStudy/pages/exam-start/exam-start', {
     data: {
       name: paperName.value,
@@ -96,20 +105,26 @@ const handleStartPractice = () => {
       practiceInfo: {
         name: prevData.value.name,
         relateId: knowledgeId,
-        directed: prevData.value.directed ? 1 : 0
+        directed: prevData.value.directed
       },
     }
   });
 }
 const handleViewAnalysis = () => {
+  if (!examineeData.value) {
+    return;
+  }
   transferTo('/pagesStudy/pages/exam-start/exam-start', {
     data: {
       name: paperName.value,
-      paperType: EnumPaperType.SIMULATED,
+      paperType: EnumPaperType.PRACTICE,
       readonly: true,
-      simulationInfo: {
-        examineeId: examineeData.value?.examineeId,
-      }
+      practiceInfo: {
+        name: prevData.value.name,
+        relateId: examineeData.value.knowledgeId,
+        directed: prevData.value.directed,
+        examineeId: examineeData.value.examineeId
+      },
     }
   });
 }
@@ -118,13 +133,11 @@ const loadData = async () => {
   try {
     const res = await getExamineeResult(prevData.value.examineeId);
     examineeData.value = res.data;
-    console.log(examineeData.value)
   } finally {
     uni.$ie.hideLoading();
   }
 }
 onLoad(() => {
-  console.log(prevData.value)
   loadData();
 });
 </script>

+ 1 - 1
src/pagesStudy/pages/knowledge-practice-history/knowledge-practice-history.vue

@@ -28,7 +28,7 @@ import { useTransferPage } from '@/hooks/useTransferPage';
 import { getPracticeHistory } from '@/api/modules/study';
 import { Study } from '@/types';
 import { Transfer } from '@/types';
-const { prevData, transferTo } = useTransferPage<{}, Transfer.PracticeResult>();
+const { prevData, transferTo } = useTransferPage<{}, Transfer.PracticeResultPageOptions>();
 const { baseStickyTop } = useNavbar();
 const historyList = ref<Study.PracticeHistory[]>([]);
 const handleViewHistory = (value: Study.PracticeHistory) => {

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

@@ -4,8 +4,8 @@
     <uv-tabs :list="subjectList" key-name="subjectName" @click="handleChangeTab" :scrollable="true"></uv-tabs>
     <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="clock" size="16" color="#31A0FC"></uv-icon>
+        <text class="text-28 text-primary">查看记录</text>
         <uv-icon name="arrow-right" size="16" color="#31A0FC"></uv-icon>
       </view>
     </view>
@@ -42,7 +42,6 @@ const currentSubjectId = computed(() => {
 const subjectList = ref<Study.Subject[]>([]);
 const treeData = ref<Study.KnowledgeNode[]>([]);
 const handleChangeTab = (item: any) => {
-  console.log(item)
   currentSubjectIndex.value = item.index;
 }
 const handleViewHistory = () => {
@@ -57,6 +56,7 @@ const loadKnowledgeList = async () => {
     return;
   }
   try {
+    uni.$ie.showLoading();
     const { data } = await getKnowledgeList({
       subjectId: currentSubjectId.value,
       directed: prevData.value.directed
@@ -64,6 +64,8 @@ const loadKnowledgeList = async () => {
     treeData.value = data as Study.KnowledgeNode[];
   } catch (error) {
     console.log(error);
+  } finally {
+    uni.$ie.hideLoading();
   }
 }
 
@@ -104,6 +106,9 @@ const loadData = async () => {
 onLoad(() => {
   loadData();
 });
+onShow(() => {
+  loadKnowledgeList();
+});
 </script>
 
 <style></style>

+ 1 - 2
src/pagesStudy/pages/simulation-analysis/components/exam-stat.vue

@@ -121,13 +121,12 @@ const handleFilter = (type: 'correct' | 'incorrect' | 'unanswered') => {
   } else {
     filterTypes.value.push(type);
   }
-  console.log(filterTypes.value)
 }
 const handleDetail = (item: Study.Question) => {
-  console.log(item, flatQuestionList.value)
   emit('detail', item);
 }
 setQuestionList(props.data.questions);
+console.log(flatQuestionList.value, 111)
 </script>
 <style lang="scss" scoped>
 .question-item {

+ 7 - 4
src/pagesStudy/pages/simulation-analysis/simulation-analysis.vue

@@ -27,7 +27,7 @@
             </view>
           </view>
         </view>
-        <exam-stat :data="examineeData" :show-stats="true" @detail="handleDetail"/>
+        <exam-stat :data="examineeData" :show-stats="true" @detail="handleDetail" />
         <score-stat :data="examineeData" />
       </view>
     </view>
@@ -40,9 +40,9 @@ import ExamStat from './components/exam-stat.vue';
 import ScoreStat from './components/score-stat.vue';
 import { getExamineeResult } from '@/api/modules/study';
 import { useTransferPage } from '@/hooks/useTransferPage';
-import { Study } from '@/types';
+import { Study, Transfer } from '@/types';
 import { EnumPaperType, EnumQuestionType } from '@/common/enum';
-const { prevData, transferTo } = useTransferPage();
+const { prevData, transferTo } = useTransferPage<Transfer.SimulationAnalysisPageOptions, Transfer.ExamAnalysisPageOptions>();
 const examineeData = ref<Study.Examinee>();
 const paperName = computed(() => {
   return '模拟考试-' + examineeData.value?.subjectName;
@@ -67,6 +67,9 @@ const formatTime = (time: number) => {
   }
 };
 const handleDetail = (item: Study.Question) => {
+  if (!examineeData.value) {
+    return;
+  }
   transferTo('/pagesStudy/pages/exam-start/exam-start', {
     data: {
       name: paperName.value,
@@ -74,7 +77,7 @@ const handleDetail = (item: Study.Question) => {
       questionId: item.id,
       paperType: EnumPaperType.SIMULATED,
       simulationInfo: {
-        examineeId: examineeData.value?.examineeId
+        examineeId: examineeData.value.examineeId
       }
     }
   });

+ 7 - 3
src/pagesStudy/pages/simulation-start/simulation-start.vue

@@ -38,7 +38,7 @@
           </view>
           <view class="mt-36 rounded-15 bg-back px-40 py-36 flex">
             <view class="flex-1 text-center">
-              <view class="text-40 text-fore-title font-bold">{{ questions.length }}</view>
+              <view class="text-40 text-fore-title font-bold">{{ questionCount }}</view>
               <view class="mt-10 text-28 text-fore-light">题量</view>
             </view>
             <view class="flex-1 text-center">
@@ -73,7 +73,12 @@ 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 questionCount = computed(() => {
+  return paperInfo.value.types.reduce((acc: number, curr: { count: number }) => {
+    return acc + curr.count;
+  }, 0);
+});
 const questions = ref<Study.ExamineeQuestion[]>([]);
 const universityInfo = computed(() => {
   return prevData.value.universityInfo;
@@ -82,7 +87,6 @@ const subjectInfo = computed(() => {
   return prevData.value.subjectInfo;
 });
 const handleStartTest = () => {
-  // transferTo('/pagesStudy/pages/simulation-analysis/simulation-analysis')
   transferTo('/pagesStudy/pages/exam-start/exam-start', {
     data: {
       paperType: EnumPaperType.SIMULATED,

+ 7 - 1
src/pagesStudy/pages/study-history/components/knowledge-history-student.vue

@@ -11,7 +11,13 @@ const loadData = async () => {
   uni.$ie.showLoading();
   try {
     const { rows } = await getKnowledgeRecord();
-    dataList.value = rows;
+    dataList.value = rows.map(item => {
+      const rate = item.rate;
+      return {
+        ...item,
+        rate: rate < 0.1 ? 0.1 : Number(rate.toFixed(1))
+      }
+    });
   } finally {
     uni.$ie.hideLoading();
   }

+ 24 - 6
src/pagesStudy/pages/targeted-add/targeted-add.vue

@@ -1,6 +1,6 @@
 <template>
   <ie-page bg-color="#F6F8FA" :fix-height="true">
-    <ie-navbar title="选择院校" />
+    <ie-navbar title="添加定向院校" />
     <view class="mt-16 bg-white py-10">
       <view class="">
         <uv-cell-group :border="false">
@@ -74,7 +74,10 @@
 import { useTransferPage } from '@/hooks/useTransferPage';
 import { DirectedSchool, SelectedUniversityMajor, University, UniversityMajor } from '@/types/study';
 import { getUniversityMajorList } from '@/api/modules/university';
-const { prevData, transferTo, transferBack } = useTransferPage();
+import { useUserStore } from '@/store/userStore';
+const userStore = useUserStore();
+const { transferTo } = useTransferPage();
+const { hasDirectedSchool, directedSchoolList } = toRefs(userStore);
 const form = ref<Partial<SelectedUniversityMajor>>({});
 const keyword = ref('');
 const cellStyle = {
@@ -94,7 +97,6 @@ const handleUniversitySelect = () => {
     }
   }).then((res: any) => {
     if (res) {
-      console.log(res)
       const university = res as University;
       if (university.code !== form.value.universityId) {
         selectedMajor.value = null;
@@ -136,7 +138,7 @@ const handleSelect = (item: any) => {
   selectedMajor.value = item;
 }
 
-const handleAdd = () => {
+const handleAdd = async () => {
   if (!form.value.universityId) {
     uni.$ie.showToast('请选择院校');
     return;
@@ -146,13 +148,25 @@ const handleAdd = () => {
     return;
   }
   // 检查数据是否已存在
-  const historyData = (prevData.value.historyData || []) as DirectedSchool[];
+  const historyData = directedSchoolList.value;
   const isExist = historyData.some(item => item.universityId === form.value.universityId && item.majorId === form.value.majorId);
   if (isExist) {
     uni.$ie.showToast('该院校专业已存在');
     return;
   }
-  transferBack(form.value);
+  uni.$ie.showLoading();
+  const params = {
+    ...form.value,
+    code: form.value.universityId
+  } as DirectedSchool;
+  await userStore.saveDirectedSchoolList([params, ...directedSchoolList.value]);
+  uni.$ie.hideLoading();
+  uni.$ie.showSuccess('保存成功');
+  setTimeout(() => {
+    transferTo('/pagesStudy/pages/targeted-setting/targeted-setting', {
+      type: 'redirectTo'
+    });
+  }, 600);
 }
 const isActive = (item: UniversityMajor) => {
   return selectedMajor.value && selectedMajor.value.id === item.id;
@@ -163,6 +177,10 @@ const loadMajorList = async (universityId: string) => {
   majorList.value = data;
   uni.$ie.hideLoading();
 }
+
+onLoad(() => {
+  handleUniversitySelect();
+});
 </script>
 
 <style lang="scss" scoped></style>

+ 16 - 38
src/pagesStudy/pages/targeted-setting/targeted-setting.vue

@@ -10,10 +10,6 @@
     </view>
     <view class="px-48 pt-52 pb-32 flex items-center justify-between">
       <view class="text-32 text-fore-title font-bold">我的定向院校</view>
-      <!-- <view v-if="hasDirectedSchool" class="flex items-center gap-x-4">
-        <text class="text-28 text-[#F59E0B]">设置</text>
-        <ie-image src="/pagesStudy/static/image/icon-edit-pen.png" custom-class="w-24 h-24" mode="aspectFill" />
-      </view> -->
     </view>
     <view class="relative">
       <view class="px-30">
@@ -72,41 +68,26 @@
 
 <script lang="ts" setup>
 import { useTransferPage } from '@/hooks/useTransferPage';
-import { DirectedSchool, SelectedUniversityMajor } from '@/types/study';
-import { getDirectedSchool, saveDirectedSchool } from '@/api/modules/study';
+import { DirectedSchool } from '@/types/study';
+import { useUserStore } from '@/store/userStore';
+
+const userStore = useUserStore();
 const { transferTo } = useTransferPage();
 const loading = ref(true);
-const directedSchoolList = ref<DirectedSchool[]>([]);
 const sortList = ref<DirectedSchool[]>([]);
 const touchHandle = ref(false)
-const hasDirectedSchool = computed(() => directedSchoolList.value.length > 0);
+const { directedSchoolList } = toRefs(userStore);
 const handleSetDirectedSchool = async (item: DirectedSchool, index: number) => {
   // 将该院校设置为第一个
-  directedSchoolList.value.splice(index, 1);
-  directedSchoolList.value.unshift(item);
-  save(directedSchoolList.value);
+  const list = [...directedSchoolList.value];
+  list.splice(index, 1);
+  list.unshift(item);
+  save(list);
 }
 const dragRef = ref();
 const handleAdd = () => {
   transferTo('/pagesStudy/pages/targeted-add/targeted-add', {
-    data: {
-      historyData: directedSchoolList.value
-    }
-  }).then(async res => {
-    if (res) {
-      uni.$ie.showLoading();
-      directedSchoolList.value.push({
-        ...res,
-        code: (res as SelectedUniversityMajor).universityId
-      } as DirectedSchool);
-      setTimeout(() => {
-        dragRef.value.push({
-          ...res,
-          code: (res as SelectedUniversityMajor).universityId
-        } as DirectedSchool);
-        uni.$ie.hideLoading();
-      }, 300);
-    }
+    data: {}
   });
 }
 
@@ -116,23 +97,20 @@ const changeSort = (e: any) => {
 }
 const save = async (list: DirectedSchool[]) => {
   uni.$ie.showLoading();
-  await saveDirectedSchool(list);
+  await userStore.saveDirectedSchoolList(list);
   await refresh();
   uni.$ie.hideLoading();
   uni.$ie.showSuccess('设置成功');
   loading.value = false;
 }
 const refresh = async () => {
-  const { data } = await getDirectedSchool();
-  directedSchoolList.value = data || [];
   sortList.value = [...directedSchoolList.value];
 }
-const loadData = async () => {
-  uni.$ie.showLoading();
-  await refresh();
-  uni.$ie.hideLoading();
-}
-loadData();
+onShow(() => {
+  nextTick(() => {
+    refresh();
+  });
+});
 </script>
 
 <style lang="scss" scoped></style>

BIN
src/pagesStudy/static/image/icon-more.png


+ 3 - 3
src/pagesSystem/pages/bind-profile/bind-profile.vue

@@ -155,9 +155,9 @@ const handleNoSchool = () => {
   });
 }
 const handleSchoolSelect = () => {
-  if (isSchoolDisabled.value) {
-    return;
-  }
+  // if (isSchoolDisabled.value || !examTypeForm.value.examType) {
+  //   return;
+  // }
   transferTo('/pagesSystem/pages/school-select/school-select', {
     data: form.value
   }).then(res => {

+ 3 - 2
src/pagesSystem/pages/school-select/school-select.vue

@@ -25,7 +25,7 @@
 import { getSchoolList } from '@/api/modules/user';
 import { SchoolItem } from '@/types/user';
 import { useTransferPage } from '@/hooks/useTransferPage';
-const { transferTo, transferBack } = useTransferPage();
+const { prevData, transferTo, transferBack } = useTransferPage();
 const keyword = ref<string>('');
 const list = ref<SchoolItem[]>([]);
 const paging = ref();
@@ -34,7 +34,8 @@ const handleQuery = (pageNum: number, pageSize: number) => {
   getSchoolList({
     keyword: keyword.value,
     pageNum,
-    pageSize
+    pageSize,
+    examType: prevData.value.examType,
   }).then(res => {
     paging.value.completeByTotal(res.rows, res.total);
   }).catch(e => {

+ 31 - 0
src/preload.js

@@ -0,0 +1,31 @@
+document.addEventListener('UniAppJSBridgeReady', function () {
+  uni.webView.postMessage({
+    data: {
+      action: 'setPlatform'
+    }
+  });
+  window.backup = (from) => {
+    if (from === 'backbutton') {
+      const routes = getCurrentPages();
+      const route = routes[routes.length - 1].route;
+      if ([
+        'pagesMain/pages/index/index',
+        'pagesMain/pages/volunteer/volunteer',
+        'pagesMain/pages/me/me'
+      ].includes(route)) {
+        uni.webView.postMessage({
+          data: {
+            action: 'quit'
+          }
+        });
+      } else {
+        uni.navigateBack();
+      }
+    }
+  };
+  // 默认为h5平台
+  window.platform = 'h5';
+  window.setPlatform = (platform) => {
+    window.platform = platform;
+  };
+});

+ 40 - 13
src/store/userStore.ts

@@ -3,15 +3,16 @@ import { useTransferPage } from '@/hooks/useTransferPage';
 import { ref, computed } from 'vue';
 import { getUserInfo } from '@/api/modules/login';
 
-import { UserStoreState } from '@/types';
+import { Study, UserStoreState } from '@/types';
 import { UserInfo, VipCardInfo } from '@/types/user';
 import tools from '@/utils/uni-tool';
 import defaultAvatar from '@/static/personal/avatar_default.png'
 
 // @ts-ignore
 import { useUserStore as useOldUserStore } from '@/hooks/useUserStore';
-import { EnumUserType } from '@/common/enum';
+import { EnumReviewMode, EnumUserType } from '@/common/enum';
 import { OPEN_VIP_POPUP } from '@/types/injectionSymbols';
+import { getDirectedSchool, saveDirectedSchool } from '@/api/modules/study';
 const oldUserStore = useOldUserStore()
 const themeColor = '#31A0FC';
 type CheckLoginOptions = {
@@ -30,7 +31,12 @@ export const useUserStore = defineStore('ie-user', {
       location: '',
       examType: '',
     },
-    rememberPwd: false
+    rememberPwd: false,
+    directedSchoolList: [],
+    practiceSettings: {
+      reviewMode: EnumReviewMode.AFTER_SUBMIT,
+      autoNext: false
+    }
   }),
   getters: {
     isLogin(state: UserStoreState): boolean {
@@ -69,6 +75,13 @@ export const useUserStore = defineStore('ie-user', {
     isAgent(): boolean {
       return this.userInfo.userType === EnumUserType.AGENT;
     },
+    isAuditor(): boolean {
+      return false;
+      return this.userInfo.accountType === 2;
+    },
+    hasDirectedSchool(): boolean {
+      return this.directedSchoolList.length > 0;
+    },
     /**
      * 需要完成信息完善的用户类型
      * @returns 
@@ -167,6 +180,24 @@ export const useUserStore = defineStore('ie-user', {
         this.isPasswordExpired = isPasswordExpired;
       }
     },
+    /**
+     * 保存定向学校列表
+     * @param list 定向学校列表
+     */
+    async saveDirectedSchoolList(list: Study.DirectedSchool[]) {
+      await saveDirectedSchool(list);
+      await this.getDirectedSchoolList();
+    },
+    /**
+     * 获取定向学校列表
+     */
+    async getDirectedSchoolList() {
+      const { data } = await getDirectedSchool();
+      this.directedSchoolList = data || [];
+    },
+    setPracticeSettings(settings: Study.PracticeSettings) {
+      this.practiceSettings = settings;
+    },
     askLogout() {
       return new Promise((resolve, reject) => {
         uni.$ie.showModal({
@@ -188,17 +219,13 @@ export const useUserStore = defineStore('ie-user', {
       this.user = null;
       this.isDefaultModifyPwd = false;
       this.isPasswordExpired = false;
-      this.tempInfo = {
-        location: '',
-        examType: ''
-      }
       oldUserStore.Logout();
-      const { transferTo } = useTransferPage();
-      setTimeout(() => {
-        transferTo('/pagesMain/pages/index/index', {
-          type: 'reLaunch'
-        })
-      }, 300);
+      // const { transferTo } = useTransferPage();
+      // setTimeout(() => {
+      //   transferTo('/pagesMain/pages/index/index', {
+      //     type: 'reLaunch'
+      //   })
+      // }, 300);
     },
   },
   persist: {

+ 5 - 2
src/types/index.ts

@@ -3,6 +3,7 @@ import * as User from "./user";
 import * as News from "./news";
 import * as Transfer from "./transfer";
 import { VipCardInfo } from "./user";
+import { EnumReviewMode } from "@/common/enum";
 
 /// 接口响应
 export interface ApiResponse<T> {
@@ -84,8 +85,10 @@ export interface UserStoreState {
   tempInfo?: {
     location: string;
     examType: string;
-  },
-  rememberPwd: boolean
+  };
+  rememberPwd: boolean;
+  directedSchoolList: Study.DirectedSchool[];
+  practiceSettings: Study.PracticeSettings;
 }
 
 /**

+ 5 - 11
src/types/injectionSymbols.ts

@@ -1,5 +1,7 @@
 import type { InjectionKey } from 'vue'
 import { StudyPlan, StudyPlanStats } from './study';
+import { Study, Transfer } from '.';
+import { useExam } from '@/composables/useExam';
 /**
  * 打开知识点记录详情
  */
@@ -16,17 +18,9 @@ export const OPEN_PRACTICE_DETAIL = Symbol('OPEN_PRACTICE_DETAIL') as InjectionK
 export const OPEN_VIDEO_DETAIL = Symbol('OPEN_VIDEO_DETAIL') as InjectionKey<(id: number, name: string) => void>;
 
 
-export const NEXT_QUESTION = Symbol('NEXT_QUESTION') as InjectionKey<() => void>;
-
-export const PREV_QUESTION = Symbol('PREV_QUESTION') as InjectionKey<() => void>;
-
-export const NEXT_QUESTION_QUICKLY = Symbol('NEXT_QUESTION_QUICKLY') as InjectionKey<() => void>;
-
-export const PREV_QUESTION_QUICKLY = Symbol('PREV_QUESTION_QUICKLY') as InjectionKey<() => void>;
-
-export const SHOW_SUBMIT_CONFIRM = Symbol('SHOW_SUBMIT_CONFIRM') as InjectionKey<Ref<boolean>>;
-export const IS_ALL_DONE = Symbol('IS_ALL_DONE') as InjectionKey<Ref<boolean>>;
-
+export const EXAM_AUTO_SUBMIT = Symbol('EXAM_AUTO_SUBMIT') as InjectionKey<() => void>;
+export const EXAM_PAGE_OPTIONS = Symbol('EXAM_PAGE_OPTIONS') as InjectionKey<Transfer.ExamAnalysisPageOptions>;
+export const EXAM_DATA = Symbol('EXAM_DATA') as InjectionKey<ReturnType<typeof useExam>>;
 /**
  * 学习计划
  */

+ 24 - 5
src/types/study.ts

@@ -1,4 +1,4 @@
-import { EnumSimulatedRecordStatus } from "@/common/enum";
+import { EnumReviewMode, EnumSimulatedRecordStatus } from "@/common/enum";
 
 export interface TeachClass {
   classId: number;
@@ -86,9 +86,11 @@ export interface Knowledge {
   status: number;
   questionCount: number;
   children: Knowledge[];
+  finishedCount: number;
+  finishedRatio: number;
 }
 
-export type KnowledgeNode = Pick<Knowledge, 'id' | 'name' | 'status' | 'questionCount'> & {
+export type KnowledgeNode = Pick<Knowledge, 'id' | 'name' | 'status' | 'questionCount' | 'finishedCount' | 'finishedRatio'> & {
   isExpanded: boolean;
   isLeaf: boolean;
   actualHeight: number;
@@ -102,7 +104,11 @@ export interface QuestionState {
   isNotAnswer?: boolean;
   isFavorite?: boolean;
   isCorrect?: boolean;
+  isLeaf?: boolean; // 是否是叶子节点
   progress?: number;
+  //
+  showParse?: boolean; // 是否显示解析
+  hasParsed?: boolean; // 是否已经解析过,用于背题模式
 }
 
 /**
@@ -122,6 +128,7 @@ export interface ExamineeQuestion {
   answers: string[];
   subQuestions: ExamineeQuestion[];
   parse?: string;
+  totalScore: number;
 }
 export interface Examinee {
   examineeId: number;
@@ -187,11 +194,23 @@ export interface ExamPaperSubmit {
   isDone?: boolean;
 }
 
-export interface QuestionOption {
+export interface QuestionOptionState {
+  isAnswer: boolean;
+  isCorrect: boolean;
+  isSelected: boolean;
+  isMissed: boolean;
+  isIncorrect: boolean;
+}
+
+export interface PracticeSettings {
+  reviewMode: EnumReviewMode;
+  autoNext: boolean;
+}
+
+export interface QuestionOption extends QuestionOptionState {
   id: number;
   no: string; // A, B, C, D
   name: string;
-  isAnswer: boolean;
 }
 
 export interface Question extends QuestionState {
@@ -204,6 +223,7 @@ export interface Question extends QuestionState {
   answer2: string;
   parse?: string;
   subQuestions: Question[];
+  totalScore: number;
   //
   index: number; // 索引
   offset: number; // 偏移量
@@ -215,7 +235,6 @@ export interface Question extends QuestionState {
   subIndex?: number; // 子题索引
   //
   virtualIndex: number; // 虚拟索引
-  showParse?: boolean; // 是否显示解析
 }
 
 export interface SubjectListRequestDTO {

+ 33 - 1
src/types/transfer.ts

@@ -1,7 +1,39 @@
+import { EnumPaperType } from "@/common/enum";
+
 export type TransferType = 'redirectTo' | 'reLaunch' | 'switchTab' | 'navigateTo' | 'navigateBack';
 
-export interface PracticeResult {
+/**
+ * 知识点练习结果
+ */
+export interface PracticeResultPageOptions {
   examineeId: number;
   name: string;
   directed: boolean;
+}
+
+/**
+ * 查看试卷分析
+ * 
+ */
+export interface ExamAnalysisPageOptions {
+  paperType: EnumPaperType;
+  name: string;
+  questionId?: number;
+  readonly?: boolean;
+  simulationInfo?: {
+    examineeId: number;
+  };
+  practiceInfo?: {
+    name: string;
+    relateId: number;
+    directed: boolean; // 知识点 id
+    examineeId?: number;
+  };
+}
+
+/**
+ * 查看模拟考试分析
+ */
+export interface SimulationAnalysisPageOptions {
+  examineeId: number;
 }

+ 3 - 1
src/types/user.ts

@@ -36,6 +36,7 @@ export interface SchoolListQueryDTO {
   keyword?: string;
   pageNum: number;
   pageSize: number;
+  examType?: string;
 }
 export interface SchoolItem {
   id: number;
@@ -147,7 +148,8 @@ export interface UserInfo {
   userId: number;
   userName: string;
   scores: Scores;
-  userType: EnumUserType
+  userType: EnumUserType,
+  accountType: number
 }
 
 export interface VipCardInfo {

+ 5 - 1
src/uni_modules/uni-calendar/components/uni-calendar/uni-calendar-item.vue

@@ -21,7 +21,7 @@
         'uni-calendar-item--week-mode-disabled': weeks.isWeekModeDisabled,
         'uni-calendar-item--not-current-month': !weeks.isCurrentMonth,
       }">{{ weeks.date }}</text>
-      <view class="uni-calendar-item__weeks-info-text-box">
+      <view v-if="showExtraInfo" class="uni-calendar-item__weeks-info-text-box">
         <view v-if="weeks.extraInfo && weeks.extraInfo.info" class="uni-calendar-item__weeks-info-text-box-content"
           :class="[weeks.extraInfo.info.isFinish ? 'uni-calendar-item__weeks-info-text-box--active' : 'uni-calendar-item__weeks-info-text-box--inactive']">
         </view>
@@ -63,6 +63,10 @@ export default {
     highlightToday: {
       type: Boolean,
       default: true
+    },
+    showExtraInfo: {
+      type: Boolean,
+      default: false
     }
   },
   computed: {

+ 5 - 1
src/uni_modules/uni-calendar/components/uni-calendar/uni-calendar.vue

@@ -86,7 +86,7 @@
           <view class="uni-calendar__weeks-item" v-for="(weeks, weeksIndex) in item" :key="weeksIndex">
             <slot name="calendar-item" :weeks="weeks" :calendar="calendar" :selected="selected" :lunar="lunar"
               :highlight-today="highlightToday" :week-index="weekIndex" :weeks-index="weeksIndex">
-              <calendar-item class="uni-calendar-item--hook" :weeks="weeks" :calendar="calendar" :selected="selected"
+              <calendar-item class="uni-calendar-item--hook" :weeks="weeks" :calendar="calendar" :selected="selected" :showExtraInfo="showExtraInfo"
                 :lunar="lunar" :highlight-today="highlightToday" @change="choiceDate" />
             </slot>
           </view>
@@ -197,6 +197,10 @@ export default {
     showToolbar: {
       type: Boolean,
       default: true
+    },
+    showExtraInfo: {
+      type: Boolean,
+      default: true
     }
   },
   data() {

+ 1 - 1
src/uni_modules/uv-cell/components/uv-cell/uv-cell.vue

@@ -1,6 +1,6 @@
 <template>
 	<view class="uv-cell" :class="[customClass]" :style="[$uv.addStyle(customStyle)]"
-		:hover-class="(!disabled && (clickable || isLink)) ? 'uv-cell--clickable' : ''" :hover-stay-time="250"
+		:hover-class="(!disabled && (clickable || isLink)) && !disableHover ? 'uv-cell--clickable' : ''" :hover-stay-time="250"
 		@click="clickHandler">
 		<view class="uv-cell__body" 
 			:class="[ center && 'uv-cell--center', size === 'large' && 'uv-cell__body--large']"

+ 1 - 1
src/uni_modules/uv-icon/components/uv-icon/uv-icon.vue

@@ -144,7 +144,7 @@
 		},
 		methods: {
 			clickHandler(e) {
-				this.$emit('click', this.index)
+				this.$emit('click', e)
 				// 是否阻止事件冒泡
 				this.stop && this.preventEvent(e)
 			}

+ 1 - 0
src/uni_modules/uv-image/components/uv-image/uv-image.vue

@@ -6,6 +6,7 @@
 		:duration="fade ? duration : 0"
 		:cell-child="cellChild"
 		:custom-style="wrapStyle"
+    :class="customClass"
 	>
 		<view
 			class="uv-image"

+ 1 - 0
src/uni_modules/uv-popup/components/uv-popup/uv-popup.vue

@@ -359,6 +359,7 @@ export default {
                 show: false,
                 type: this.mode
             })
+            this.$emit('close')
             clearTimeout(this.timer)
             // // 自定义关闭事件
             this.timer = setTimeout(() => {

+ 1 - 1
src/utils/uni-tool.ts

@@ -94,7 +94,7 @@ const defaultModalOptions: IModalOptions = {
   showCancel: true,
   showConfirm: true,
   cancelColor: '#000000',
-  confirmColor: '#576B95',
+  confirmColor: '#31a0fc',
   cancelText: '取消',
   confirmText: '确认'
 };

+ 1 - 1
vite.config.js

@@ -10,7 +10,7 @@ import uniPolyfill from 'vite-plugin-uni-polyfill';
 import viteCompression from "vite-plugin-compression";
 import { env as envConfig } from './src/config';
 // https://vitejs.dev/config/
-const env = JSON.parse(process.env.UNI_CUSTOM_DEFINE);
+const env = JSON.parse(process.env.UNI_CUSTOM_DEFINE || '{}');
 const mode = env.IE_ENV || 'development';
 const baseUrl = envConfig[mode]?.serverBaseUrl || '';
 console.log('当前模式:', mode);