Kaynağa Gözat

merge conflict

abpcoder 10 saat önce
ebeveyn
işleme
1287c2b588
62 değiştirilmiş dosya ile 1953 ekleme ve 579 silme
  1. 59 1
      src/api/modules/study.ts
  2. 5 1
      src/common/routes.ts
  3. 4 1
      src/components/ie-page/ie-page.vue
  4. 5 1
      src/components/ie-safe-toolbar/ie-safe-toolbar.vue
  5. 71 66
      src/components/ie-table/ie-table.vue
  6. 187 24
      src/composables/useExam.ts
  7. 2 2
      src/composables/useQuestionBook.ts
  8. 0 17
      src/hooks/useDebounce.ts
  9. 116 107
      src/main.ts
  10. 12 0
      src/pages.json
  11. 3 1
      src/pagesMain/pages/index/components/index-guide.vue
  12. 1 1
      src/pagesMain/pages/index/components/index-news.vue
  13. 59 0
      src/pagesStudy/components/ie-exam-record-item.vue
  14. 2 2
      src/pagesStudy/components/knowledge-table.vue
  15. 10 8
      src/pagesStudy/components/paper-work-item.vue
  16. 1 1
      src/pagesStudy/components/practice-table.vue
  17. 0 4
      src/pagesStudy/components/question-book-item.vue
  18. 95 0
      src/pagesStudy/components/vhs-exam-item.vue
  19. 69 0
      src/pagesStudy/components/vhs-exam-record-item.vue
  20. 12 8
      src/pagesStudy/components/video-table.vue
  21. 19 26
      src/pagesStudy/pages/exam-start/components/exam-stats-card.vue
  22. 23 4
      src/pagesStudy/pages/exam-start/components/exam-subtitle.vue
  23. 41 26
      src/pagesStudy/pages/exam-start/components/question-item.vue
  24. 1 1
      src/pagesStudy/pages/exam-start/components/question-parse.vue
  25. 3 3
      src/pagesStudy/pages/exam-start/components/question-title.vue
  26. 75 8
      src/pagesStudy/pages/exam-start/exam-start.vue
  27. 38 0
      src/pagesStudy/pages/index/compoentns/ie-exam.vue
  28. 9 3
      src/pagesStudy/pages/index/compoentns/index-banner.vue
  29. 3 8
      src/pagesStudy/pages/index/compoentns/index-menu.vue
  30. 146 0
      src/pagesStudy/pages/index/compoentns/index-practice-entry.vue
  31. 69 0
      src/pagesStudy/pages/index/compoentns/vhs-exam.vue
  32. 19 101
      src/pagesStudy/pages/index/index.vue
  33. 2 2
      src/pagesStudy/pages/knowledge-practice-detail/knowledge-practice-detail.vue
  34. 6 5
      src/pagesStudy/pages/knowledge-practice-history/knowledge-practice-history.vue
  35. 0 1
      src/pagesStudy/pages/simulation-analysis/components/exam-stat.vue
  36. 30 31
      src/pagesStudy/pages/simulation-analysis/simulation-analysis.vue
  37. 11 59
      src/pagesStudy/pages/study-history/components/exam-history.vue
  38. 57 0
      src/pagesStudy/pages/study-history/components/ie-exam-history-student.vue
  39. 18 0
      src/pagesStudy/pages/study-history/components/ie-exam-history-teacher.vue
  40. 66 0
      src/pagesStudy/pages/study-history/components/ie-exam-history.vue
  41. 3 1
      src/pagesStudy/pages/study-history/components/knowledge-history-student.vue
  42. 1 1
      src/pagesStudy/pages/study-history/components/knowledge-history.vue
  43. 1 1
      src/pagesStudy/pages/study-history/components/practice-history.vue
  44. 32 0
      src/pagesStudy/pages/study-history/components/vhs-exam-history-student.vue
  45. 93 0
      src/pagesStudy/pages/study-history/components/vhs-exam-history-teacher.vue
  46. 44 0
      src/pagesStudy/pages/study-history/components/vhs-exam-history.vue
  47. 8 5
      src/pagesStudy/pages/study-history/study-history.vue
  48. 102 0
      src/pagesStudy/pages/video/index/index.vue
  49. 138 0
      src/pagesStudy/pages/video/play/play.vue
  50. 4 2
      src/pagesStudy/pages/wrong-book/wrong-book.vue
  51. 8 8
      src/pagesSystem/pages/bind-profile/bind-profile.vue
  52. 9 9
      src/pagesSystem/pages/bind-teacher-profile/bind-teacher-profile.vue
  53. 1 1
      src/pagesSystem/pages/edit-profile/edit-profile.vue
  54. 4 1
      src/static/theme/theme.module.scss
  55. 10 2
      src/static/theme/var.scss
  56. 3 0
      src/store/userStore.ts
  57. 107 3
      src/types/study.ts
  58. 6 9
      src/types/transfer.ts
  59. 2 2
      src/uni_modules/mp-html/components/mp-html/node/node.vue
  60. 19 3
      src/uni_modules/uv-collapse/components/uv-collapse-item/uv-collapse-item.vue
  61. 6 7
      src/uni_modules/uv-tabs/components/uv-tabs/uv-tabs.vue
  62. 3 1
      src/uni_modules/uv-tags/components/uv-tags/uv-tags.vue

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

@@ -1,6 +1,6 @@
 import { ApiResponse, ApiResponseList } from "@/types";
 import flyio from "../flyio";
-import type { Batch, ClassKnowledgeRecord, DirectedSchool, Examinee, ExamPaper, ExamPaperSubmit, FavoriteQuestion, FavoriteQuestionListRequestDTO, GetExamPaperRequestDTO, Knowledge, KnowledgeListRequestDTO, KnowledgeRecord, OpenExamineeRequestDTO, PaperWork, PaperWorkRecord, PaperWorkRecordDetail, PaperWorkRecordQuery, PracticeHistory, PracticeRecord, SimulatedRecord, SimulationExamSubject, SimulationTestInfo, StudentExamRecord, StudentPlanStudyRecord, StudentSubject, StudentVideoRecord, StudyPlan, Subject, SubjectListRequestDTO, TeachClass, VideoStudy, WrongBookQuestion, WrongBookQuestionRequestDTO } from "@/types/study";
+import type { Batch, ClassKnowledgeRecord, DirectedSchool, Examinee, ExamPaper, ExamPaperSubmit, FavoriteQuestion, FavoriteQuestionListRequestDTO, GetExamPaperRequestDTO, Knowledge, KnowledgeListRequestDTO, KnowledgeRecord, OpenExamineeRequestDTO, PaperWork, PaperWorkRecord, PaperWorkRecordDetail, PaperWorkRecordQuery, PracticeHistory, PracticeRecord, SimulatedRecord, SimulationExamSubject, SimulationTestInfo, StudentExamRecord, StudentPlanStudyRecord, StudentSubject, StudentVideoRecord, StudyPlan, Subject, SubjectListRequestDTO, TeachClass, VHSPaper, VHSPaperListRequestDTO, VideoCourse, VideoCourseKnowledge, VideoCoursePlayInfo, VideoCourseRecordDTO, VideoCourseRequestDTO, VideoCourseSubject, VideoCourseSubjectRequestDTO, VideoStudy, WrongBookQuestion, WrongBookQuestionRequestDTO } from "@/types/study";
 import { EnumPaperWorkState } from "@/common/enum";
 
 /**
@@ -116,6 +116,14 @@ export function getExamineeResult(examineeId: number) {
   return flyio.get('/front/exam/loadExaminee', { examineeId }) as Promise<ApiResponse<Examinee>>;
 }
 
+/**
+ * 对口升学-获取真题&模拟试卷
+ * @param params 
+ * @returns 
+ */
+export function getVHSPaperList(params: VHSPaperListRequestDTO) {
+  return flyio.get('/front/paper/list', params) as Promise<ApiResponse<VHSPaper[]>>;
+}
 
 /**
  * 获取模拟考试信息
@@ -350,3 +358,53 @@ export function getWrongBookList(params: WrongBookQuestionRequestDTO) {
 export function deleteWrongQuestion(questionId: number) {
   return flyio.post('/front/v2/wrongBook/deleteWrongQuestion', null, { params: { questionId } }) as Promise<ApiResponse<any>>;
 }
+
+
+/**
+ * 获取视频课程科目列表
+ * @param params 
+ * @returns 
+ */
+export function getVideoCourseSubjects(params: VideoCourseSubjectRequestDTO) {
+  return flyio.get('/front/videoCourse/subjects', params) as Promise<ApiResponseList<VideoCourseSubject>>;
+}
+
+/**
+ * 获取视频课程知识点
+ * @param params 
+ * @returns 
+ */
+export function getVideoCourseKnowledges(subject: number) {
+  return flyio.get('/front/videoCourse/knowledges', { subject }) as Promise<ApiResponseList<VideoCourseKnowledge[]>>;
+}
+
+/**
+ * 获取视频课程列表
+ * @param params 
+ * @returns 
+ */
+export function getVideoCourseList(params: VideoCourseRequestDTO) {
+  return flyio.get('/front/videoCourse/video/list', params) as Promise<ApiResponseList<VideoCourse>>;
+}
+
+/**
+ * 获取视频课程播放信息
+ * @param videoId 
+ * @returns 
+ */
+export function getVideoCoursePlayInfo(videoId: string) {
+  return flyio.get('/front/comm/vod/getVideoPlayInfo', { videoId }) as Promise<ApiResponse<VideoCoursePlayInfo>>;
+}
+
+/**
+ * 保存视频课程学习记录
+ * @param params 
+ * @returns 
+ */
+export function saveVideoCourseRecord(params: VideoCourseRecordDTO) {
+  return flyio.post('/front/videoCourse/saveWatchRecord', null, { params }, {
+    headers: {
+      'Content-Type': 'application/www-form-urlencoded'
+    }
+  }) as Promise<ApiResponse<any>>;
+}

+ 5 - 1
src/common/routes.ts

@@ -95,7 +95,11 @@ export const routes = {
   /**
    * 课程学习
    */
-  pageCourseStudy: '/pagesOther/pages/video-center/index/index',
+  pageCourseStudy: '/pagesStudy/pages/video/index/index',
+  /**
+ * 视频播放
+ */
+  pageCourseVideoPlay: '/pagesStudy/pages/video/play/play',
   /**
    * 组卷作业
    */

+ 4 - 1
src/components/ie-page/ie-page.vue

@@ -111,7 +111,10 @@ onUnload(() => {
 }
 
 .is-fixed {
-  height: 1px;
+  height: 100vh;
+  .ie-page-content {
+    height: 100%;
+  }
 }
 
 .ie-fixed-bottom {

+ 5 - 1
src/components/ie-safe-toolbar/ie-safe-toolbar.vue

@@ -1,7 +1,7 @@
 <template>
   <view class="min-h-62" :style="{ minHeight: minHeight + 'px' }">
     <view class="safe-area-inset-bottom fixed left-0 bottom-0 right-0 z-10 box-border" :class="{ 'shadow-box': shadow }"
-      :style="{ height: minHeight + 'px' }">
+      :style="{ height: minHeight + 'px', backgroundColor: bgColor }">
       <slot></slot>
     </view>
   </view>
@@ -22,6 +22,10 @@ const props = defineProps({
   shadow: {
     type: Boolean,
     default: true
+  },
+  bgColor: {
+    type: String,
+    default: '#FFFFFF'
   }
 });
 const minHeight = computed(() => {

+ 71 - 66
src/components/ie-table/ie-table.vue

@@ -1,119 +1,124 @@
 <template>
-    <view class="">
-        <view class="table-header">
-            <view class="table-header-cell" v-for="item in tableColumns" :key="item.prop" :style="getHeaderStyle(item)">
-                {{ item.label }}
-            </view>
-        </view>
-        <view class="table-body">
-            <block v-if="data.length">
-                <view class="table-row" :class="{ 'sibling-border-top': getTableConfig.border }"
-                      v-for="(row, index) in data"
-                      :key="getRowKey(row, index)" @click="handleRowClick(row)">
-                    <view class="table-row-cell" v-for="item in tableColumns" :key="item.prop"
-                          :style="getCellStyle(item)">
-                        <view v-if="item.type === 'index'">
-                            {{ index + 1 }}
-                        </view>
-                        <template v-else-if="item.slot">
-                            <slot :name="item.slot" :item="row" :index="index"></slot>
-                        </template>
-                        <text v-else>{{ getCellValue(row, item.prop) }}</text>
-                    </view>
-                </view>
-            </block>
-            <view v-else class="no-data">{{ getTableConfig.emptyText }}</view>
-        </view>
+  <view class="w-full" :class="{ 'h-full flex flex-col': headerFixed }">
+    <view class="table-header">
+      <view class="table-header-cell" v-for="item in tableColumns" :key="item.prop" :style="getHeaderStyle(item)">
+        {{ item.label }}
+      </view>
     </view>
+    <scroll-view :class="{ 'flex-1 min-h-1': headerFixed }" scroll-y>
+      <view class="table-body">
+        <block v-if="data.length">
+          <view class="table-row" :class="{ 'sibling-border-top': getTableConfig.border }" v-for="(row, index) in data"
+            :key="getRowKey(row, index)" @click="handleRowClick(row)">
+            <view class="table-row-cell" v-for="item in tableColumns" :key="item.prop" :style="getCellStyle(item)">
+              <view v-if="item.type === 'index'">
+                {{ index + 1 }}
+              </view>
+              <template v-else-if="item.slot">
+                <slot :name="item.slot" :item="row" :index="index"></slot>
+              </template>
+              <text v-else>{{ getCellValue(row, item.prop) }}</text>
+            </view>
+          </view>
+        </block>
+        <view v-else class="no-data">{{ getTableConfig.emptyText }}</view>
+      </view>
+    </scroll-view>
+  </view>
 </template>
 
 <script lang="ts" setup generic="T extends Record<string, any>">
-import {TableColumnConfig, TableConfig} from '@/types';
-import {CSSProperties} from 'vue';
+import { TableColumnConfig, TableConfig } from '@/types';
+import { CSSProperties } from 'vue';
 
 // 使用泛型定义props
 interface Props<T> {
-    tableConfig: TableConfig;
-    tableColumns: TableColumnConfig[];
-    data: T[];
-    cellStyle: CSSProperties;
-    headerStyle: CSSProperties;
+  tableConfig: TableConfig;
+  tableColumns: TableColumnConfig[];
+  data: T[];
+  cellStyle: CSSProperties;
+  headerStyle: CSSProperties;
+  headerFixed?: boolean;
 }
 
 const props = defineProps<Props<any>>();
 
 // 使用泛型定义emits
 const emit = defineEmits<{
-    rowClick: [row: T]
+  rowClick: [row: T]
 }>();
 
 const getTableConfig = computed(() => {
-    return {
-        ...{
-            border: true,
-            stripe: false,
-            emptyText: '暂无数据',
-            loading: false,
-            rowKey: 'id'
-        },
-        ...props.tableConfig
-    };
+  return {
+    ...{
+      border: true,
+      stripe: false,
+      emptyText: '暂无数据',
+      loading: false,
+      rowKey: 'id'
+    },
+    ...props.tableConfig
+  };
 });
 
 const getHeaderStyle = (item: TableColumnConfig) => {
-    return {
-        flex: item.flex ? item.flex : 1,
-        minWidth: '1px',
-        textAlign: item.headerAlign ? item.headerAlign : 'center',
-        ...props.headerStyle
-    };
+  return {
+    flex: item.flex ? item.flex : 1,
+    minWidth: '1px',
+    textAlign: item.headerAlign ? item.headerAlign : 'center',
+    ...props.headerStyle
+  };
 };
 
 const getCellStyle = (item: TableColumnConfig) => {
-    return {
-        flex: item.flex ? item.flex : 1,
-        minWidth: '1px',
-        flexShrink: 0,
-        width: '100%',
-        textAlign: item.align ? item.align : 'center',
-        ...props.cellStyle
-    };
+  return {
+    flex: item.flex ? item.flex : 1,
+    minWidth: '1px',
+    flexShrink: 0,
+    width: '100%',
+    textAlign: item.align ? item.align : 'center',
+    ...props.cellStyle
+  };
 };
 
 const handleRowClick = (row: any) => {
-    emit('rowClick', row);
+  emit('rowClick', row);
 };
 
 // 安全地获取行key
 const getRowKey = (row: any, index: number) => {
-    const rowKey = getTableConfig.value.rowKey;
-    return row[rowKey] || index;
+  const rowKey = getTableConfig.value.rowKey;
+  return row[rowKey] || index;
 };
 
 // 安全地获取单元格值
 const getCellValue = (row: any, prop: string) => {
-    return row[prop] || props.tableConfig.defaultValue || '';
+  return row[prop] || props.tableConfig.defaultValue || '';
 };
 </script>
 
 <style lang="scss" scoped>
 .table-header {
-    @apply flex items-center bg-[#EBF9FF] rounded-5 overflow-hidden;
+  @apply w-full flex items-center bg-[#EBF9FF] rounded-5 overflow-hidden;
 }
 
 .table-header-cell {
-    @apply flex-1 px-20 py-20 text-30 text-fore-light;
+  @apply flex-1 px-20 py-20 text-30 text-fore-light;
 }
 
 .table-row {
-    @apply flex items-center;
+  @apply flex items-center;
 }
 
 .table-row-cell {
-    @apply px-20 py-20 text-28 text-fore-title;
+  @apply px-20 py-20 text-28 text-fore-title;
 }
 
 .no-data {
-    @apply mt-16 bg-[#F6F8FA] text-center py-50 text-26 text-fore-light rounded-5;
+  @apply mt-16 bg-[#F6F8FA] text-center py-50 text-26 text-fore-light rounded-5;
+}
+
+.fixed {
+  @apply sticky top-0 z-1;
 }
 </style>

+ 187 - 24
src/composables/useExam.ts

@@ -12,7 +12,7 @@ const getNow = (): number => {
   // #ifdef MP-WEIXIN
   return Date.now();
   // #endif
-  
+
   // #ifndef MP-WEIXIN
   if (typeof performance !== 'undefined' && performance.now) {
     return performance.now();
@@ -31,7 +31,7 @@ const requestAnimFrame = (() => {
     return setTimeout(() => callback(Date.now()), 1000 / 60) as unknown as number;
   };
   // #endif
-  
+
   // #ifndef MP-WEIXIN
   if (typeof requestAnimationFrame !== 'undefined') {
     return requestAnimationFrame;
@@ -52,7 +52,7 @@ const cancelAnimFrame = (() => {
     clearTimeout(id as unknown as NodeJS.Timeout);
   };
   // #endif
-  
+
   // #ifndef MP-WEIXIN
   if (typeof cancelAnimationFrame !== 'undefined') {
     return cancelAnimationFrame;
@@ -71,7 +71,7 @@ const cancelAnimFrame = (() => {
 export const decodeHtmlEntities = (str: string): string => {
   if (!str) return str;
 
-  // 音标和常用 HTML 实体映射表
+  // 标准 HTML 实体映射表
   const entityMap: Record<string, string> = {
     // 音标相关 - 锐音符 (acute)
     'aacute': 'á',
@@ -101,10 +101,9 @@ export const decodeHtmlEntities = (str: string): string => {
     'ntilde': 'ñ',
     'atilde': 'ã',
     'otilde': 'õ',
+    'aelig': 'æ',
+    'eth': 'ð',
     // 其他常用实体
-    'amp': '&',
-    'lt': '<',
-    'gt': '>',
     'quot': '"',
     'apos': "'",
     'nbsp': '\u00A0',
@@ -114,29 +113,188 @@ export const decodeHtmlEntities = (str: string): string => {
     'mdash': '—',
     'ndash': '–',
     'hellip': '…',
+    'darr': '↓',
+    'uarr': '↑',
+    'rarr': '→',
+    'larr': '←',
+    'harr': '↔',
+    'crarr': '↵',
+    'lArr': '⇐',
+    'rArr': '⇒',
+    'hArr': '⇔',
+    'laquo': '«',
+    'raquo': '»',
+    'ldquo': '“',
+    'rdquo': '”',
+    'lsquo': '‘',
+    'rsquo': '’',
+    'lsaquo': '‹',
+    'rsaquo': '›',
     // 数学符号
+    'amp': '&',
+    'minus': '−',
+    'plus': '+',
     'times': '×',
     'divide': '÷',
     'plusmn': '±',
+    'deg': '°',
+    'radic': '√',
+    'isin': '∈',
+    'infin': '∞',
+    'there4': '∴',
+    'because': '∵',
+    'forall': '∀',
+    'exists': '∃',
+    'nexists': '∄',
+    'notin': '∉',
+    'ni': '∋',
+    'empty': '∅',
+    'prod': '∏',
+    'sum': '∑',
+    'prop': '∝',
+    'sim': '∼',
+    'simeq': '≃',
+    'approx': '≈',
+    'asymp': '≈',
+    'neq': '≠',
+    'equiv': '≡',
+    'ne': '≠',
+    'le': '≤',
+    'ge': '≥',
+    'll': '≪',
+    'gg': '≫',
+    'prec': '≺',
+    'succ': '≻',
+    'preceq': '≼',
+    'succeq': '≽',
+    'sub': '⊂',
+    'sube': '⊆',
+    'subset': '⊂',
+    'subseteq': '⊆',
+    'sup': '⊃',
+    'supe': '⊇',
+    'supset': '⊃',
+    'supseteq': '⊇',
+    'perp': '⊥',
+    'parallel': '∥',
+    'wedge': '∧',
+    'vee': '∨',
+    'cap': '∩',
+    'cup': '∪',
+    'int': '∫',
+    'oint': '∮',
+    'angle': '∠',
+    'ang': '∠',
+    'mid': '∣',
+    'vdash': '⊢',
+    'dashv': '⊣',
+    'models': '⊨',
+    'vDash': '⊩',
+    'Vdash': '⊫',
+    'VDash': '⊬',
+    'Oslash': 'Ø',
+    'ordm': 'º',
+    'not': '¬',
+    'prime': '′',
+    // 排版符号
+    'middot': '·',
+    'bull': '•',
+    'sect': '§',
+    'para': '¶',
+    'macr': '¯',
+    'micro': 'µ',
+    // 货币符号
+    'yen': '¥',
+    'euro': '€',
+    'pound': '£',
+    'cent': '¢',
+    // 希腊字母
+    'alpha': 'α',
+    'beta': 'β',
+    'gamma': 'γ',
+    'delta': 'δ',
+    'epsilon': 'ε',
+    'zeta': 'ζ',
+    'eta': 'η',
+    'theta': 'θ',
+    'iota': 'ι',
+    'kappa': 'κ',
+    'lambda': 'λ',
+    'mu': 'μ',
+    'nu': 'ν',
+    'xi': 'ξ',
+    'omicron': 'ο',
+    'pi': 'π',
+    'rho': 'ρ',
+    'sigma': 'σ',
+    'tau': 'τ',
+    'upsilon': 'υ',
+    'phi': 'φ',
+    'chi': 'χ',
+    'psi': 'ψ',
+    'omega': 'ω',
+    'Omega': 'Ω',
+    'thetasym': 'ϑ',
+    'upsih': 'ϒ',
+    'piv': 'ϖ',
   };
 
-  // 处理命名实体(如 &iacute;)
-  // 使用 [a-z0-9] 以支持包含数字的实体名称
-  let result = str.replace(/&([a-z0-9]+);/gi, (match, entity) => {
+  let result = str;
+
+  // 清除所有的特殊空格实体
+  result = result.replaceAll("&zwj;", "")
+    .replaceAll("&nbsp;", "")
+    .replaceAll("&zwnj;", "")
+    .replaceAll("&thinsp;", "")
+    .replaceAll("&emsp;", "")
+    .replaceAll("&ensp;", "")
+    .replaceAll("&shy;", "")
+    .replaceAll("&#8203;", "");
+
+  // 0. 保护真正的 HTML 标签,转义文本中的 < 和 >
+  // 策略:< 后面如果不是字母或 /(标签开始),则转义为 &lt;
+  // 这样可以处理 x<2、1<x<5 等数学表达式
+  // result = result.replace(/<(?![a-zA-Z/])/g, '&lt;');  // < 后面不是字母或 / 的转义
+  // result = result.replace(/(?<![a-zA-Z0-9])>/g, '&gt;'); // > 前面不是字母数字的转义(避免破坏标签)
+
+  // 1. 处理非标准的上标实体:&sup数字; → <sup>数字</sup>
+  // 例如:&sup2; → <sup>2</sup>, &sup123; → <sup>123</sup>
+  result = result.replace(/&sup(\d+);/gi, (match, digits) => {
+    return `<sup>${digits}</sup>`;
+  });
+
+  // 2. 处理非标准的下标实体:&sub数字; → <sub>数字</sub>
+  // 例如:&sub2; → <sub>2</sub>, &sub123; → <sub>123</sub>
+  result = result.replace(/&sub(\d+);/gi, (match, digits) => {
+    return `<sub>${digits}</sub>`;
+  });
+
+  // 3. 处理标准命名实体(如 &iacute;、&there4;)
+  // 注意:正则需要支持字母和数字的组合
+  result = result.replace(/&([a-z0-9]+);/gi, (match, entity) => {
     const lowerEntity = entity.toLowerCase();
     if (entityMap[lowerEntity]) {
       return entityMap[lowerEntity];
     }
-    return match; // 如果找不到映射,保持原样
+    return match;
   });
 
-  // 处理数字实体(如 &#237; 或 &#xED;)
+  // 4. 处理十进制数字实体(如 &#178; → ²
   result = result.replace(/&#(\d+);/g, (match, num) => {
-    return String.fromCharCode(parseInt(num, 10));
+    const code = parseInt(num, 10);
+    if (!isNaN(code)) {
+      return String.fromCharCode(code);
+    }
+    return match;
   });
 
+  // 5. 处理十六进制数字实体(如 &#xB2; → ²)
   result = result.replace(/&#x([0-9a-f]+);/gi, (match, hex) => {
-    return String.fromCharCode(parseInt(hex, 16));
+    const code = parseInt(hex, 16);
+    if (!isNaN(code)) {
+      return String.fromCharCode(code);
+    }
+    return match;
   });
 
   return result;
@@ -384,7 +542,7 @@ export const useExam = () => {
     answer1 = answer1 || '';
     answer2 = answer2 || '';
     if ([EnumQuestionType.SINGLE_CHOICE, EnumQuestionType.JUDGMENT].includes(typeId)) {
-      return answer1.includes(answers[0]);
+      return answer1.trim() === answers[0];
     } else if ([EnumQuestionType.MULTIPLE_CHOICE].includes(typeId)) {
       return answers.length === answer1.length && answers.every(item => answer1.includes(item));
     } else {
@@ -430,7 +588,9 @@ export const useExam = () => {
     if (currentQuestion.value.activeSubIndex < currentQuestion.value.subQuestions.length - 1) {
       currentQuestion.value.activeSubIndex++;
     } else {
-      currentIndex.value++;
+      const lastIndex = currentIndex.value + 1;
+      questionList.value[lastIndex].activeSubIndex = 0;
+      currentIndex.value = lastIndex;
     }
   }
   // 上一题
@@ -442,7 +602,9 @@ export const useExam = () => {
       if (currentQuestion.value.activeSubIndex > 0) {
         currentQuestion.value.activeSubIndex--;
       } else {
-        currentIndex.value--;
+        const prevIndex = currentIndex.value - 1;
+        questionList.value[prevIndex].activeSubIndex = questionList.value[prevIndex].subQuestions.length - 1;
+        currentIndex.value = prevIndex;
       }
     } else {
       if (currentIndex.value > 0) {
@@ -494,12 +656,12 @@ export const useExam = () => {
   // 开始计时
   const startTiming = () => {
     startCount();
-    
+
     // 记录开始时间戳(毫秒)
     if (practiceStartTime === 0) {
       practiceStartTime = getNow();
     }
-    
+
     // 使用 requestAnimFrame 更新显示,更流畅且性能更好
     // 兼容微信小程序环境
     const updatePracticeDuration = () => {
@@ -511,20 +673,20 @@ export const useExam = () => {
         animationFrameId = requestAnimFrame(updatePracticeDuration);
       }
     };
-    
+
     // 开始动画帧循环
     animationFrameId = requestAnimFrame(updatePracticeDuration);
   }
   // 停止计时
   const stopTiming = () => {
     stopCount();
-    
+
     // 取消动画帧
     if (animationFrameId !== null) {
       cancelAnimFrame(animationFrameId);
       animationFrameId = null;
     }
-    
+
     // 如果正在计时,累加经过的时间
     if (practiceStartTime > 0) {
       const elapsed = (getNow() - practiceStartTime) / 1000;
@@ -625,6 +787,7 @@ export const useExam = () => {
     const transerQuestion = (item: Study.ExamineeQuestion, index: number): Study.Question => {
       return {
         ...item,
+        title: decodeHtmlEntities(item.title || ''),
         // 处理没有题型的大题,统一作为阅读题
         typeId: (item.typeId === null || item.typeId === undefined) ? EnumQuestionType.OTHER : item.typeId,
         answers: item.answers || [],
@@ -647,7 +810,8 @@ export const useExam = () => {
         virtualIndex: 0,
         duration: 0,
         activeSubIndex: 0,
-        hasSubQuestions: item.subQuestions?.length > 0
+        hasSubQuestions: item.subQuestions && item.subQuestions.length > 0,
+        typeTitle: item.typeTitle
       } as Study.Question
     }
     questionList.value = transerQuestions(list.map((item, index) => transerQuestion(item, index)));
@@ -712,7 +876,6 @@ export const useExam = () => {
   watch([() => currentIndex.value, () => currentQuestion.value?.activeSubIndex], (val) => {
     const qs = questionList.value[val[0]];
     virtualCurrentIndex.value = qs.index + qs.offset + val[1];
-    console.log(virtualCurrentIndex.value, 777)
   }, {
     immediate: false
   });

+ 2 - 2
src/composables/useQuestionBook.ts

@@ -61,7 +61,7 @@ const useQuestionBook = () => {
    */
   const cancelCollect = (id: number): Promise<boolean> => {
     return new Promise((resolve, reject) => {
-      uni.$ie.showConfirm({
+      uni.$ie.showModal({
         title: '提示',
         content: '确定取消收藏吗?',
       }).then(async confirm => {
@@ -99,7 +99,7 @@ const useQuestionBook = () => {
   }
   const deleteWrongQuestion = (id: number): Promise<boolean> => {
     return new Promise(async (resolve, reject) => {
-      uni.$ie.showConfirm({
+      uni.$ie.showModal({
         title: '提示',
         content: '确定删除错题吗?',
       }).then(async confirm => {

+ 0 - 17
src/hooks/useDebounce.ts

@@ -66,21 +66,4 @@ export function useDebounce<T extends (...args: any[]) => any>(
   };
   
   return debounced;
-}
-
-/**
- * 用于Vue组件中的防抖钩子
- * @param value 需要防抖的值
- * @param wait 等待时间(毫秒)
- * @returns 防抖后的值
- */
-export function useDebouncedValue<T>(value: T, wait: number): T {
-  let debouncedValue = value;
-  let timeoutId: ReturnType<typeof setTimeout> | null = null;
-  
-  // 简单实现,实际项目中可能需要结合ref或reactive使用
-  // 完整实现可能需要:import { ref, watch } from 'vue';
-  // 这里仅提供一个基础版本
-  
-  return debouncedValue;
 }

+ 116 - 107
src/main.ts

@@ -9,7 +9,7 @@ import './preload'
 import tool from '@/utils/uni-tool'
 import * as Pinia from 'pinia';
 import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
-import {useImage} from '@/hooks/useImage';
+import { useImage } from '@/hooks/useImage';
 
 // #ifndef VUE3
 import Vue from 'vue'
@@ -19,127 +19,136 @@ Vue.config.productionTip = false
 Vue.use(uvUiTools)
 App.mpType = 'app'
 const app = new Vue({
-    ...App
+  ...App
 })
 app.$mount()
 // #endif
 
 // #ifdef VUE3
-import {createSSRApp} from 'vue'
+import { createSSRApp } from 'vue'
 import "./static/style/tailwind.scss";
 
 export function createApp() {
-    const app = createSSRApp(App)
-    app.use(uvUiTools)
+  const app = createSSRApp(App)
+  app.use(uvUiTools)
 
-    uni.$ie = tool;
+  uni.$ie = tool;
 
-    uni.$uv.setConfig({
-        props: {
-            loadingPage: {
-                loadingText: {default: ''},
-                image: {default: '/static/logo/loading1.gif'},
-                class: {default: 'mx-loading-page'}
-            },
-            navbar: {
-                placeholder: {default: true},
-                clickHover: {default: true},
-                statusBarHeight: {default: 0}
-            },
-            statusBar: {
-                statusBarHeight: {default: 0}
-            },
-            tabs: {
-                activeStyle: {default: () => ({color: 'var(--primary-color)'})}
-            },
-            steps: {
-                activeColor: {default: 'var(--primary-color)'}
-            },
-            search: {
-                color: {default: 'var(--main-color)'},
-                actionStyle: {default: () => ({color: 'var(--primary-color)'})}
-            },
-            empty: {
-                icon: {default: '/static/icon-empty.png'},
-                height: {default: 140},
-                width: {default: 140},
-                text: {default: '暂无相关数据'}
-            },
-            icon: {
-                customClass: {
-                    default: ''
-                }
-            },
-            popup: {
-                theme: {
-                    default: 'theme-ie'
-                }
-            },
-            image: {
-                customClass: {
-                    default: ''
-                }
-            },
-            cell: {
-                disableHover: {
-                    default: false
-                }
-            },
-            collapseItem: {
-                padding: {
-                    default: '12px 15px;'
-                }
-            },
-            input: {
-                fontSize: {default: '30rpx'},
-                disabledColor: {default: 'var(--back-light)'},
-                customStyle: {
-                    default: () => ({
-                        height: '30px',
-                        paddingLeft: '40rpx',
-                        paddingRight: '40rpx',
-                        borderRadius: '24rpx'
-                    })
-                }
-            }
+  uni.$uv.setConfig({
+    props: {
+      loadingPage: {
+        loadingText: { default: '' },
+        image: { default: '/static/logo/loading1.gif' },
+        class: { default: 'mx-loading-page' }
+      },
+      navbar: {
+        placeholder: { default: true },
+        clickHover: { default: true },
+        statusBarHeight: { default: 0 }
+      },
+      statusBar: {
+        statusBarHeight: { default: 0 }
+      },
+      tabs: {
+        activeStyle: { default: () => ({ color: 'var(--primary-color)' }) },
+        animationEnabled: { default: true } // 滚动时是否带有动画
+      },
+      steps: {
+        activeColor: { default: 'var(--primary-color)' }
+      },
+      search: {
+        color: { default: 'var(--main-color)' },
+        actionStyle: { default: () => ({ color: 'var(--primary-color)' }) }
+      },
+      empty: {
+        icon: { default: '/static/icon-empty.png' },
+        height: { default: 140 },
+        width: { default: 140 },
+        text: { default: '暂无相关数据' }
+      },
+      icon: {
+        customClass: {
+          default: ''
         }
-    })
-
-    const {resolvePath} = useImage();
-    uni.$zp = {
-        config: {
-            'default-page-size': 20,
-            'refresher-title-style': {
-                fontSize: '28rpx'
-            },
-            'loading-more-title-custom-style': {
-                fontSize: '26rpx'
-            },
-            // 底部安全区域以placeholder形式实现
-            'use-safe-area-placeholder': true
-            // 'empty-view-img-style': {
-            //   width: '364rpx',
-            //   height: '252rpx'
-            // },
-            // 'empty-view-img': resolvePath('/pagesStudy/static/image/icon-empty.png'),
-            // 'empty-view-title-style': {
-            //   color: '#B3B3B3',
-            //   fontSize: '30rpx',
-            //   marginTop: '40rpx'
-            // },
-            // 'empty-view-style': {
-            //   marginTop: '-200rpx'
-            // }
+      },
+      popup: {
+        theme: {
+          default: 'theme-ie'
+        }
+      },
+      image: {
+        customClass: {
+          default: ''
+        }
+      },
+      cell: {
+        disableHover: {
+          default: false
+        }
+      },
+      collapseItem: {
+        padding: {
+          default: '12px 15px;'
+        },
+        data: { default: null },
+        lazy: { default: false },
+        load: { default: null }
+      },
+      input: {
+        fontSize: { default: '30rpx' },
+        disabledColor: { default: 'var(--back-light)' },
+        customStyle: {
+          default: () => ({
+            height: '30px',
+            paddingLeft: '40rpx',
+            paddingRight: '40rpx',
+            borderRadius: '24rpx'
+          })
         }
+      },
+      tags: {
+        customClass: {
+          default: ''
+        }
+      }
     }
+  })
 
-    const pinia = Pinia.createPinia();
-    app.use(pinia);
-    pinia.use(piniaPluginPersistedstate);
-
-    return {
-        app
+  const { resolvePath } = useImage();
+  uni.$zp = {
+    config: {
+      'default-page-size': 20,
+      'refresher-title-style': {
+        fontSize: '28rpx'
+      },
+      'loading-more-title-custom-style': {
+        fontSize: '26rpx'
+      },
+      // 底部安全区域以placeholder形式实现
+      'use-safe-area-placeholder': true
+      // 'empty-view-img-style': {
+      //   width: '364rpx',
+      //   height: '252rpx'
+      // },
+      // 'empty-view-img': resolvePath('/pagesStudy/static/image/icon-empty.png'),
+      // 'empty-view-title-style': {
+      //   color: '#B3B3B3',
+      //   fontSize: '30rpx',
+      //   marginTop: '40rpx'
+      // },
+      // 'empty-view-style': {
+      //   marginTop: '-200rpx'
+      // }
     }
+  }
+
+  const pinia = Pinia.createPinia();
+  app.use(pinia);
+  pinia.use(piniaPluginPersistedstate);
+
+  return {
+    app
+  }
 }
 
 // #endif

+ 12 - 0
src/pages.json

@@ -387,6 +387,18 @@
           "style": {
             "navigationBarTitleText": ""
           }
+        },
+        {
+          "path": "pages/video/index/index",
+          "style": {
+            "navigationBarTitleText": ""
+          }
+        },
+        {
+          "path": "pages/video/play/play",
+          "style": {
+            "navigationBarTitleText": ""
+          }
         }
       ]
     }

+ 3 - 1
src/pagesMain/pages/index/components/index-guide.vue

@@ -70,7 +70,9 @@ const loadData = () => {
     newsList.value = res.rows;
   });
 }
-loadData();
+onShow(() => {
+  loadData();
+});
 </script>
 <style lang="scss" scoped>
 .wrap {

+ 1 - 1
src/pagesMain/pages/index/components/index-news.vue

@@ -29,7 +29,7 @@ const loadData = async () => {
   newsList.value = rows;
 }
 
-onLoad(() => {
+onShow(() => {
   loadData();
 });
 </script>

+ 59 - 0
src/pagesStudy/components/ie-exam-record-item.vue

@@ -0,0 +1,59 @@
+<template>
+  <view class="bg-white">
+    <view class="text-30 text-fore-title">{{ data.name }}</view>
+    <view class="mt-32 flex items-center justify-between">
+      <view class="text-24 text-fore-light">提交时间: {{ data.date || '-' }}</view>
+      <view class="text-28 text-primary flex items-center gap-x-4" @click="handleDetail">
+        <text>{{ isFinished ? '查看分析' : '继续考试' }}</text>
+        <uv-icon name="arrow-right" size="14" color="var(--primary-color)" />
+      </view>
+    </view>
+    <view
+      class="mt-20 border border-solid border-[#FEF6DA] bg-[#FFFBEB] rounded-5 py-20 px-16 flex items-center justify-between">
+      <view class="text-24 text-[#F59E0B]">考试科目:{{ data.subjectName }}</view>
+      <view class="text-24 text-[#F59E0B]">卷面得分:{{ getScore }}</view>
+    </view>
+  </view>
+</template>
+<script lang="ts" setup>
+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<any, Transfer.SimulationAnalysisPageOptions | Transfer.ExamAnalysisPageOptions>();
+const props = defineProps<{
+  data: Study.SimulatedRecord;
+}>();
+const isFinished = computed(() => {
+  return props.data.state === EnumSimulatedRecordStatus.SUBMIT;
+});
+const getScore = computed(() => {
+  if (props.data.score === undefined || props.data.score === null) {
+    return '-';
+  }
+  return props.data.score;
+})
+const handleDetail = () => {
+  if (isFinished.value) {
+    transferTo('/pagesStudy/pages/simulation-analysis/simulation-analysis', {
+      data: {
+        examineeId: props.data.id,
+        paperType: EnumPaperType.SIMULATED
+      }
+    });
+  } else {
+    transferTo('/pagesStudy/pages/exam-start/exam-start', {
+      data: {
+        // name: '模拟考试-' + props.data.subjectName,
+        paperType: EnumPaperType.SIMULATED,
+        readonly: false,
+        simulationInfo: {
+          examineeId: props.data.id,
+          name: props.data.subjectName,
+        }
+      }
+    });
+  }
+};
+</script>
+<style lang="scss" scoped></style>

+ 2 - 2
src/pagesStudy/components/knowledge-table.vue

@@ -1,6 +1,6 @@
 <template>
-  <view class="p-30">
-    <ie-table :table-columns="tableColumns" :table-config="tableConfig" :data="data">
+  <view class="h-full p-30 box-border">
+    <ie-table :table-columns="tableColumns" :table-config="tableConfig" :data="data" :header-fixed="true">
       <template #name="{ item }">
         <view class="">
           <text class="leading-38">{{ item.name }}</text>

+ 10 - 8
src/pagesStudy/components/paper-work-item.vue

@@ -3,7 +3,7 @@
     <view class="border-bottom flex items-center justify-between py-32 px-25 leading-27">
       <view class="text-28 text-fore-light flex items-center">
         <view class="w-12 h-12 rounded-full bg-[#E5E5E5]"></view>
-        <view class="ml-10">{{ publishTime }}{{  `${data.publishUser ? `-${data.publishUser}` : ''}` }}</view>
+        <view class="ml-10">{{ publishTime }}{{ `${data.publishUser ? `-${data.publishUser}` : ''}` }}</view>
       </view>
       <view :class="['text-28', data.state === EnumPaperWorkState.NOT_COMPLETED ? 'text-warning' : 'text-success']">
         {{ data.state === EnumPaperWorkState.NOT_COMPLETED ? '未完成' : '已完成' }}
@@ -93,14 +93,16 @@ const handleStart = () => {
     relateId: props.data.id,
     directed: props.data.directed
   }).then(res => {
+    const pageOptions: Transfer.ExamAnalysisPageOptions = {
+      paperType: EnumPaperType.TEST,
+      readonly: false,
+      simulationInfo: {
+        examineeId: res.data.examineeId,
+        name: batchName.value
+      },
+    }
     transferTo('/pagesStudy/pages/exam-start/exam-start', {
-      data: {
-        name: '组卷作业-' + batchName.value,
-        paperType: EnumPaperType.TEST,
-        simulationInfo: {
-          examineeId: res.data.examineeId
-        },
-      } as Transfer.ExamAnalysisPageOptions,
+      data: pageOptions,
     });
   }).catch(err => {
     console.error(err);

+ 1 - 1
src/pagesStudy/components/practice-table.vue

@@ -61,7 +61,7 @@
       </ie-table>
     </view>
     <ie-popup ref="calendarPopupRef" :showToolbar="false" v-if="canOpenCalendar">
-      <view class="h-[480px]">
+      <view class="h-[420px]">
         <view class="h-108 flex items-center justify-center border-0 border-b border-solid border-border">
           <view :class="['w-38 h-38 rounded-full flex items-center justify-center transition-all duration-200', prevButtonClass]" @click="canGoPrev && !loading ? handlePrev() : null">
             <uv-icon name="arrow-left" size="12" :color="canGoPrev && !loading ? '#808080' : '#CCCCCC'" />

+ 0 - 4
src/pagesStudy/components/question-book-item.vue

@@ -68,10 +68,6 @@ import type { Study } from '@/types';
 
 
 const props = defineProps({
-  showKnowledge: {
-    type: Boolean,
-    default: false
-  },
   showAnswer: {
     type: Boolean,
     default: false

+ 95 - 0
src/pagesStudy/components/vhs-exam-item.vue

@@ -0,0 +1,95 @@
+<template>
+  <view class="bg-white rounded-5">
+    <view class="px-30 pt-32 pb-20">
+      <view class="text-28 text-fore-title font-bold ellipsis-2">{{ data.paperName }}</view>
+      <view class="flex items-center gap-16 mt-20">
+        <view class="tag-item bg-[#FFFBEB] text-[#F97316]">{{ data.subjectName }}</view>
+        <view class="tag-item bg-back text-fore-light">{{ type === 0 ? '公共课' : '专业课' }}</view>
+      </view>
+    </view>
+    <uv-line color="#F6F8FA" />
+    <view class="px-30 py-20 flex items-center justify-between">
+      <view class="text-24 text-fore-light">
+        <text>共</text>
+        <text class="text-primary">{{ data.number }}</text>
+        <text>道题,总分</text>
+        <text class="text-primary">{{ data.fenshu }}</text>
+        <text>分</text>
+      </view>
+      <view class="px-20 py-8 border border-solid rounded-full text-24 text-white"
+        :class="[isFinished ? 'bg-success border-success' : 'bg-primary border-primary']" @click="handleStartExam">
+        <text>{{ isFinished ? '查看分析' : '开始考试' }}</text>
+      </view>
+    </view>
+  </view>
+</template>
+<script lang="ts" setup>
+import { EnumPaperType, EnumSimulatedRecordStatus, EnumUserRole } from '@/common/enum';
+import { Study, Transfer } from '@/types';
+import { useTransferPage } from '@/hooks/useTransferPage';
+import { useAuth } from '@/hooks/useAuth';
+import { getOpenExaminee } from '@/api/modules/study';
+
+const { hasPermission } = useAuth();
+const { transferTo } = useTransferPage();
+const props = defineProps<{
+  data: Study.VHSPaper;
+  type?: number
+}>();
+const isFinished = computed(() => {
+  return props.data.status === EnumSimulatedRecordStatus.SUBMIT;
+});
+
+const handleStartExam = () => {
+  const hasAuth = hasPermission([EnumUserRole.VIP]);
+  if (hasAuth) {
+    if (isFinished.value) {
+      transferTo('/pagesStudy/pages/simulation-analysis/simulation-analysis', {
+        data: {
+          examineeId: props.data.examineeId,
+          paperType: EnumPaperType.SIMULATED
+        }
+      });
+    } else {
+      console.log(props.data)
+      // return
+      // const pageOptions: Transfer.ExamAnalysisPageOptions = {
+      //   paperType: EnumPaperType.SIMULATED,
+      //   readonly: false,
+      //   simulationInfo: {
+      //     name: props.data.paperName,
+      //     // 难受
+      //     examineeId: props.data.id,
+      //   }
+      // }
+      // transferTo('/pagesStudy/pages/exam-start/exam-start', {
+      //   data: pageOptions,
+      // });
+      getOpenExaminee({
+        paperType: EnumPaperType.SIMULATED,
+        relateId: props.data.id
+      }).then(res => {
+        const pageOptions: Transfer.ExamAnalysisPageOptions = {
+          paperType: EnumPaperType.SIMULATED,
+          readonly: false,
+          simulationInfo: {
+            examineeId: res.data.examineeId,
+            name: props.data.paperName,
+          },
+        }
+        transferTo('/pagesStudy/pages/exam-start/exam-start', {
+          data: pageOptions,
+        });
+      }).catch(err => {
+        console.error(err);
+      });
+    }
+  }
+}
+
+</script>
+<style lang="scss" scoped>
+.tag-item {
+  @apply text-24 rounded-4 px-10 py-6 w-fit;
+}
+</style>

+ 69 - 0
src/pagesStudy/components/vhs-exam-record-item.vue

@@ -0,0 +1,69 @@
+<template>
+  <view class="bg-white rounded-5">
+    <view class="px-30 pt-32 pb-20">
+      <view class="text-28 text-fore-title font-bold ellipsis-2">{{ data.name }}</view>
+      <view class="flex items-center gap-16 mt-20">
+        <view class="tag-item bg-[#FFFBEB] text-[#F97316]">{{ data.subjectName }}</view>
+        <view class="tag-item bg-back text-fore-light">{{ data.subjectGroup }}</view>
+      </view>
+    </view>
+    <uv-line color="#F6F8FA" />
+    <view class="px-30 py-20 flex items-center justify-between">
+      <view class="text-24 text-fore-light">
+        <template v-if="data.score !== null">
+          <text>得分:</text>
+          <text class="text-primary">{{ data.score }}</text>
+          <text>分</text>
+        </template>
+      </view>
+      <view class="px-20 py-8 border border-solid rounded-full text-24 text-white"
+        :class="[isFinished ? 'bg-success border-success' : 'bg-primary border-primary']" @click="handleStartExam">
+        <text>{{ isFinished ? '查看分析' : '继续考试' }}</text>
+      </view>
+    </view>
+  </view>
+</template>
+<script lang="ts" setup>
+import { EnumPaperType, EnumSimulatedRecordStatus } from '@/common/enum';
+import { Study, Transfer } from '@/types';
+import { useTransferPage } from '@/hooks/useTransferPage';
+
+const { transferTo } = useTransferPage();
+const props = defineProps<{
+  data: Study.SimulatedRecord;
+  type?: number
+}>();
+const isFinished = computed(() => {
+  return props.data.state === EnumSimulatedRecordStatus.SUBMIT;
+});
+
+const handleStartExam = () => {
+  if (isFinished.value) {
+    transferTo('/pagesStudy/pages/simulation-analysis/simulation-analysis', {
+      data: {
+        examineeId: props.data.id,
+        paperType: EnumPaperType.SIMULATED
+      }
+    });
+  } else {
+    const pageOptions: Transfer.ExamAnalysisPageOptions = {
+      paperType: EnumPaperType.SIMULATED,
+      readonly: false,
+      simulationInfo: {
+        name: props.data.name,
+        // 难受
+        examineeId: props.data.id,
+      }
+    }
+    transferTo('/pagesStudy/pages/exam-start/exam-start', {
+      data: pageOptions,
+    });
+  }
+}
+
+</script>
+<style lang="scss" scoped>
+.tag-item {
+  @apply text-24 rounded-4 px-10 py-6 w-fit;
+}
+</style>

+ 12 - 8
src/pagesStudy/components/video-table.vue

@@ -12,14 +12,8 @@
     </view>
     <view class="mx-30">
       <ie-table :table-columns="tableColumns" :data="data">
-        <template #name="{ item }">
-          <view class="flex items-center justify-center">
-            <ie-image :src="item.avatar" class="w-60 h-60 bg-back" :round="999" />
-            <text class="ml-10 flex-1 min-w-1 ellipsis-1">{{ item.name }}</text>
-          </view>
-        </template>
         <template #duration="{ item }">
-          <text>{{ Math.round(item.study / 60) }}分钟</text>
+          <text>{{ getStudyDuration(item) }}</text>
         </template>
       </ie-table>
     </view>
@@ -64,6 +58,16 @@ const tableColumns = ref<TableColumnConfig[]>([
     slot: 'duration',
   }
 ])
-const data = computed(() => props.data.list || [])
+const data = computed(() => props.data.list || []);
+
+const getStudyDuration = (item: Study.VideoStudyRecord) => {
+  const time = Number(item.study) / 60;
+  if (time > 1) {
+    return Math.round(time) + '分钟';
+  } else {
+    // 显示秒
+    return Number(item.study) + '秒';
+  }
+}
 </script>
 <style lang="scss" scoped></style>

+ 19 - 26
src/pagesStudy/pages/exam-start/components/exam-stats-card.vue

@@ -35,30 +35,23 @@
           <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 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 flatQuestionList" :key="j"
+                    class="aspect-square flex items-center justify-center" @click="hanadleNavigate(qs, 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.isDone,
+                        'is-not-know': !isViewMode && qs.isNotKnow,
+                        'is-mark': !isViewMode && qs.isMark,
+                        'is-correct': isViewMode && qs.isCorrect,
+                        'is-incorrect': isViewMode && !qs.isCorrect,
+                      }">
+                      <text class="z-1 font-bold text-32">{{ qs.virtualIndex + 1 }}</text>
+                      <ie-image v-if="qs.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 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>
                 </view>
               </scroll-view>
             </view>
@@ -89,7 +82,7 @@ const props = defineProps<{
   readonly?: boolean;
 }>();
 const examData = inject(EXAM_DATA) || {} as ReturnType<typeof useExam>;
-const { doneCount, virtualTotalCount, groupedQuestionList, questionTypeDesc, reset, startTiming, stopTiming, changeIndex } = examData;
+const { doneCount, virtualTotalCount, groupedQuestionList, flatQuestionList, questionTypeDesc, reset, startTiming, stopTiming, changeIndex } = examData;
 const examPageOptions = inject(EXAM_PAGE_OPTIONS) || {} as Transfer.ExamAnalysisPageOptions;
 const isViewMode = computed(() => {
   return examPageOptions?.readonly || false;
@@ -136,7 +129,7 @@ const hanadleNavigate = (question: Study.Question, index: number) => {
   if (question.isSubQuestion) {
     changeIndex(question.parentIndex || 0, question.subIndex || 0);
   } else {
-    changeIndex(index);
+    changeIndex(question.index);
   }
 }
 </script>
@@ -219,4 +212,4 @@ const hanadleNavigate = (question: Study.Question, index: number) => {
 .is-incorrect {
   @apply text-[#FF5B5C] border-[#FEEDE9] bg-[#FEEDE9];
 }
-</style>
+</style>

+ 23 - 4
src/pagesStudy/pages/exam-start/components/exam-subtitle.vue

@@ -8,20 +8,39 @@
   </view>
 </template>
 <script lang="ts" setup>
+import { useUserStore } from '@/store/userStore';
 import { Transfer } from '@/types';
 import { EXAM_PAGE_OPTIONS, EXAM_DATA } from '@/types/injectionSymbols';
 import { useExam } from '@/composables/useExam';
 
+const userStore = useUserStore();
 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;
+  if (examPageOptions && examPageOptions.practiceInfo) {
+    const { practiceInfo: { directed, questionType, name } } = examPageOptions;
+    let titlePrefix = '';
+    if (userStore.isVHS) {
+      if (questionType === 0) {
+        titlePrefix = '知识点练习';
+      } else if (questionType === 2) {
+        titlePrefix = '必刷题';
+      }
+    } else {
+      if (directed) {
+        titlePrefix = '定向刷题';
+      } else {
+        titlePrefix = '全量刷题';
+      }
+    }
+    return titlePrefix + '-' + name;
+  } else if (examPageOptions && examPageOptions.simulationInfo) {
+    const { simulationInfo: { name } } = examPageOptions;
+    return '模拟考试' + '-' + name || '模拟考试';
   }
-  return '';
+  return '知识点练习';
 });
 </script>
 <style lang="scss" scoped></style>

+ 41 - 26
src/pagesStudy/pages/exam-start/components/question-item.vue

@@ -7,19 +7,24 @@
       <question-parse :question="question" />
     </template>
     <view v-else class="mt-20">
-      <scroll-view class="w-full h-fit sticky top-0 py-10 z-1" scroll-x :scroll-into-view="scrollIntoView"
-        :scroll-left="scrollLeft">
-        <view class="flex items-center px-20 gap-x-20">
-          <view class="px-40 py-8 rounded-full" :id="`sub_question_${getNo(index)}`"
-            :class="[index === question.activeSubIndex ? 'bg-[#EBF9FF] text-primary font-bold' : 'bg-back']"
-            v-for="(subQuestion, index) in question.subQuestions" @click="setSubQuestionIndex(index)">
+      <uv-tabs ref="tabRef" :current="question.activeSubIndex" keyName="label"
+        :animationEnabled="true" :itemStyle="tabItemStyle" :list="tabs" :scrollable="true" lineHeight="0"
+        @change="handleTabChange">
+        <template #default="{ data: { item, index } }">
+          <view class="px-40 py-8 rounded-full"
+            :class="[index === question.activeSubIndex ? 'bg-primary-light text-primary' : 'bg-back']">
             {{ getNo(index) }}
           </view>
+        </template>
+      </uv-tabs>
+      <view v-if="question.subQuestions.length" class="mt-20">
+        <view v-for="subQuestion in question.subQuestions" :key="subQuestion.subIndex">
+          <question-item v-show="subQuestion.subIndex === question.activeSubIndex" :question="subQuestion" />
         </view>
-      </scroll-view>
-      <view v-if="question.subQuestions[question.activeSubIndex]" class="mt-20">
-        <question-item :question="question.subQuestions[question.activeSubIndex]" />
       </view>
+      <!-- <view v-if="question.subQuestions[question.activeSubIndex]" class="mt-20">
+        <question-item :question="question.subQuestions[question.activeSubIndex]" />
+      </view> -->
     </view>
   </view>
 </template>
@@ -28,41 +33,51 @@ 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 QuestionItem from './question-item.vue';
 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 props = defineProps<{
+  question: Study.Question;
+}>();
+
 const examPageOptions = inject(EXAM_PAGE_OPTIONS) || {} as Transfer.ExamAnalysisPageOptions;
 const examData = inject(EXAM_DATA) || {} as ReturnType<typeof useExam>;
-const { subQuestionIndex, setSubQuestionIndex } = examData;
+const { subQuestionIndex, setSubQuestionIndex, currentIndex } = examData;
 const examAutoSubmit = inject(EXAM_AUTO_SUBMIT);
 
 const scrollIntoView = ref('')
 const scrollLeft = ref(0);
-
-watch(subQuestionIndex, (val) => {
-  scrollIntoView.value = '';
-  nextTick(() => {
-    scrollIntoView.value = `sub_question_${val}`;
-    if (props.question.subQuestions && props.question.subQuestions.length > 0) {
-      if (props.question.subQuestions[0].subIndex === val) {
-        scrollLeft.value = -1;
-        setTimeout(() => {
-          scrollLeft.value = 0;
-        }, 0);
-      }
+const autoScroll = ref(false);
+const tabRef = ref();
+const tabs = computed(() => {
+  return props.question.subQuestions.map((item, index) => {
+    return {
+      label: props.question.index + props.question.offset + index + 1,
+      value: item.subIndex
     }
   });
+});
+const tabItemStyle = {
+  padding: '0 5px',
+};
+
+watch(() => currentIndex.value, (val) => {
+  if (currentIndex.value === props.question.index) {
+    nextTick(() => {
+      tabRef.value?.init();
+    });
+  }
 }, {
   immediate: false
 });
-
-const props = defineProps<{
-  question: Study.Question;
-}>();
 const getNo = (subIndex: number) => {
   return props.question.index + props.question.offset + subIndex + 1;
 }
+const handleTabChange = (e: any) => {
+  setSubQuestionIndex(e.index);
+}
 </script>
 <style lang="scss" scoped></style>

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

@@ -1,7 +1,7 @@
 <template>
   <view v-if="(isReadOnly) || (!isReadOnly && question.showParse)" class="mt-40">
     <!-- 主观题的答案在这里显示,其他题型在 question-result 面板显示 -->
-    <view v-if="isOnlySubjective" class="mb-20">
+    <view class="mb-20">
       <view class="text-30 text-fore-title font-bold">答案</view>
       <view class="mt-10 text-26 text-fore-light">
         <mp-html :content="decodeHtmlEntities(question.answer2 || '略')" />

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

@@ -1,7 +1,7 @@
 <template>
   <view class="question-title">
     <view v-if="question.typeId && !isSubQuestion" class="question-type">
-      <text>{{ questionTypeDesc[question.typeId as EnumQuestionType] }}</text>
+      <text>{{ question.typeTitle || questionTypeDesc[question.typeId as EnumQuestionType] }}</text>
       <!-- 考试模式下显示分数 -->
       <text v-if="showScore">({{ getScore }}分)</text>
     </view>
@@ -48,8 +48,8 @@ const getScore = computed(() => {
 });
 const getQuestionTitle = () => {
   if (isSubQuestion.value) {
-    const prefix = questionTypeDesc[props.question.typeId as EnumQuestionType].slice(0, 2);
-    return `[${prefix}]`;
+    const prefix = questionTypeDesc[props.question.typeId as EnumQuestionType].slice(0, 3);
+    return `[${props.question.typeTitle || prefix}]`;
   }
   return '';
 };

+ 75 - 8
src/pagesStudy/pages/exam-start/exam-start.vue

@@ -2,7 +2,7 @@
   <ie-page :fix-height="true" :safe-area-inset-bottom="false">
     <block v-if="isReady">
       <exam-navbar :total-exam-time="totalExamTime" @left-click="handleLeftClick" @right-click="handleRightClick" />
-      <exam-subtitle :total-exam-time="totalExamTime" />
+      <exam-subtitle />
       <exam-swiper @submit="beforeSubmit" />
       <exam-toolbar ref="examToolbarRef" @submit="beforeSubmit" />
     </block>
@@ -78,7 +78,9 @@ const totalExamTime = ref<number>(0);
 const hasShowSubmitConfirm = ref(false);
 const examineeId = ref<number | undefined>(undefined);
 const paperData = ref<Study.ExamPaper>({} as Study.ExamPaper);
-const examToolbarRef = ref();
+// 是否确认退出
+const confirmQuit = ref(false);
+const confirmShowing = ref(false);
 /**
  * 自动提交
  */
@@ -111,6 +113,7 @@ const isReadOnly = computed(() => {
 });
 const handleLeftClick = () => {
   if (!isReady.value || isReadOnly.value) {
+    confirmQuit.value = true;
     transferBack();
     return;
   }
@@ -133,6 +136,7 @@ const beforeQuit = () => {
   }
   stopTime();
   const msg = paperType === EnumPaperType.PRACTICE ? '当前练习未完成,确认退出?' : '当前考试未完成,确认退出?';
+  confirmShowing.value = true;
   uni.$ie.showModal({
     title: '提示',
     content: msg,
@@ -140,6 +144,8 @@ const beforeQuit = () => {
     if (confirm) {
       handleSubmit(true);
     } else {
+      confirmQuit.value = false;
+      confirmShowing.value = false;
       startTime();
     }
   });
@@ -180,7 +186,7 @@ const handleSubmit = (tempSave: boolean = false) => {
   const msg = tempSave ? '保存中...' : '提交中...';
   uni.$ie.showLoading(msg);
   setTimeout(async () => {
-    const params = {
+    const params: Study.ExamPaperSubmit = {
       ...paperData.value,
       questions: questionList.value.map(item => {
         return {
@@ -199,7 +205,7 @@ const handleSubmit = (tempSave: boolean = false) => {
       // examineeId: examineerData.value.examineeId,
       isDone: tempSave ? isAllDone.value : true,
       duration: practiceDuration.value
-    } as Study.ExamPaperSubmit;
+    };
     console.log('提交试卷参数', params)
     await commitExamineePaper(params);
     if (isSimulationExam.value || isTestExam.value) {
@@ -207,6 +213,8 @@ const handleSubmit = (tempSave: boolean = false) => {
         setTimeout(async () => {
           uni.$ie.hideLoading();
           await nextTick();
+          confirmQuit.value = true;
+          confirmShowing.value = false;
           transferTo('/pagesStudy/pages/simulation-analysis/simulation-analysis', {
             data: {
               examineeId: examineeId.value,
@@ -217,6 +225,8 @@ const handleSubmit = (tempSave: boolean = false) => {
         }, 2500);
       } else {
         uni.$ie.hideLoading();
+        confirmQuit.value = true;
+        confirmShowing.value = false;
         nextTick(() => {
           transferBack();
         });
@@ -226,18 +236,23 @@ const handleSubmit = (tempSave: boolean = false) => {
         setTimeout(async () => {
           uni.$ie.hideLoading();
           await nextTick();
+          confirmQuit.value = true;
+          confirmShowing.value = false;
           transferTo('/pagesStudy/pages/knowledge-practice-detail/knowledge-practice-detail', {
             data: {
               paperType: prevData.value.paperType,
               examineeId: examineeId.value,
               name: prevData.value.practiceInfo?.name,
-              directed: prevData.value.practiceInfo?.directed
+              directed: prevData.value.practiceInfo?.directed,
+              questionType: prevData.value.practiceInfo?.questionType
             } as Transfer.PracticeResultPageOptions,
             type: 'redirectTo'
           });
         }, 2500);
       } else {
         uni.$ie.hideLoading();
+        confirmQuit.value = true;
+        confirmShowing.value = false;
         nextTick(() => {
           transferBack();
         });
@@ -276,6 +291,7 @@ const restoreQuestion = (savedQuestion: Study.ExamineeQuestion[], fullQuestion:
   }
   return fullQuestion;
 }
+// 1、加载知识点练习数据
 const loadPracticeData = async () => {
   const { paperType, readonly, practiceInfo } = prevData.value;
   let data: Study.Examinee | null = null;
@@ -285,11 +301,16 @@ const loadPracticeData = async () => {
       data = res.data;
     }
   } else {
-    const res = await getOpenExaminee({
+    const params = {
       paperType: paperType,
       relateId: practiceInfo?.relateId,
-      directed: practiceInfo?.directed || false
-    });
+    } as Study.OpenExamineeRequestDTO;
+    if (userStore.isVHS) {
+      params.questionType = practiceInfo?.questionType;
+    } else {
+      params.directed = practiceInfo?.directed || false;
+    }
+    const res = await getOpenExaminee(params);
     data = res.data || {};
   }
 
@@ -302,6 +323,7 @@ const loadPracticeData = async () => {
   totalExamTime.value = Number.MAX_SAFE_INTEGER;
   combinePaperData(data, paperType);
 }
+// 2、加载模拟考试数据
 const loadExamData = async () => {
   const { paperType, readonly, simulationInfo } = prevData.value;
   let data: Study.Examinee;
@@ -322,6 +344,31 @@ const loadExamData = async () => {
     combinePaperData(data, paperType);
   }
 }
+// 3、加载对口升学试卷数据
+// const loadVHSPaperData = async () => {
+//   const { paperType, readonly, simulationInfo } = prevData.value;
+//   let data: Study.Examinee;
+//   if (simulationInfo?.examineeId) {
+//     if (readonly) {
+//       const res = await getExamineeResult(simulationInfo.examineeId);
+//       data = res.data;
+//     } else {
+//       const params = {
+//         paperType: paperType,
+//         relateId: simulationInfo?.examineeId,
+//       } as Study.OpenExamineeRequestDTO;
+//       const res = await getOpenExaminee(params);
+//       data = res.data || {};
+//     }
+//     if (!data) {
+//       uni.$ie.hideLoading();
+//       transferBack();
+//       return;
+//     }
+//     totalExamTime.value = data.paperInfo?.time || Number.MAX_SAFE_INTEGER;
+//     combinePaperData(data, paperType);
+//   }
+// }
 const combinePaperData = async (examinee: Study.Examinee, paperType: EnumPaperType) => {
   examineeId.value = examinee.examineeId;
   if (examinee.paperId) {
@@ -383,12 +430,32 @@ const loadData = async () => {
   if (paperType === EnumPaperType.PRACTICE || paperType === EnumPaperType.COURSE) {
     loadPracticeData();
   } else if (paperType === EnumPaperType.SIMULATED || paperType === EnumPaperType.TEST) {
+    // if (paperType === EnumPaperType.SIMULATED && userStore.isVHS) {
+    //   loadVHSPaperData();
+    // } else {
+    //   loadExamData();
+    // }
     loadExamData();
   }
 };
 onLoad(() => {
   console.log(prevData.value)
   loadData();
+  uni.addInterceptor('navigateBack', {
+    invoke: (e) => {
+      if (confirmShowing.value) {
+        return false;
+      }
+      if (confirmQuit.value) {
+        return e;
+      }
+      handleLeftClick();
+      return false;
+    }
+  })
+});
+onUnload(() => {
+  uni.removeInterceptor('navigateBack');
 });
 </script>
 

+ 38 - 0
src/pagesStudy/pages/index/compoentns/ie-exam.vue

@@ -0,0 +1,38 @@
+<template>
+  <view>
+    <view class="h-16 bg-back my-32"></view>
+    <index-test :directed-school="directedSchool" />
+    <view v-if="list.length > 0" class="px-30 pb-30 bg-back">
+      <view class="h-94 flex items-center justify-center">
+        <view class="h-0 w-160 border-0 border-b border-dashed border-border"></view>
+        <ie-image src="/pagesStudy/static/image/icon-ear-left.png" customClass="w-20 h-26 ml-26 mr-30" />
+        <view class="text-30 text-fore-title font-bold">考试记录</view>
+        <ie-image src="/pagesStudy/static/image/icon-ear-right.png" customClass="w-20 h-26 ml-26 mr-30" />
+        <view class="h-0 w-160 border-0 border-b border-dashed border-border"></view>
+      </view>
+      <view v-for="value in list" :key="value.id" class="rounded-10 mb-20 bg-white px-20 py-38">
+        <ie-exam-record-item :data="value" />
+      </view>
+    </view>
+  </view>
+</template>
+<script lang="ts" setup>
+import IndexTest from './index-test.vue';
+import IeExamRecordItem from '@/pagesStudy/components/ie-exam-record-item.vue';
+import { getSimulatedRecord } from '@/api/modules/study';
+import { Study } from '@/types';
+
+const props = defineProps<{
+  directedSchool: Study.DirectedSchool;
+}>();
+
+const list = ref<Study.SimulatedRecord[]>([]);
+const loadData = async () => {
+  const { data } = await getSimulatedRecord();
+  list.value = data || [];
+}
+onShow(() => {
+  loadData();
+});
+</script>
+<style lang="scss" scoped></style>

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

@@ -1,8 +1,8 @@
 <template>
   <view class="mx-30 mt-40">
     <!-- <ie-image :is-oss="true" src="/banner/index-banner-4.png" :round="10" customClass="w-full h-178" mode="widthFix" /> -->
-    <view class="flex gap-x-30">
-      <view class="flex-1 rounded-12 bg-[#F0FFF2] py-40 pl-22 pr-8 flex items-center" @click="handleOpenPlan">
+    <view class="grid grid-cols-2 gap-x-30">
+      <view class="rounded-12 bg-[#F0FFF2] py-40 pl-22 pr-8 flex items-center" @click="handleOpenPlan">
         <!-- /pagesStudy/pages/study-plan-edit/study-plan-edit -->
         <view class="flex-1">
           <view class="text-30 text-fore-title font-bold flex items-center">
@@ -13,7 +13,7 @@
         </view>
         <ie-image :is-oss="true" src="/study-bg3.png" customClass="w-92 h-92" />
       </view>
-      <view class="flex-1 rounded-12 bg-[#FFF6F0] py-40 pl-22 pr-8 flex items-center" @click="handleTest">
+      <view v-if="showCoursePractice" class="rounded-12 bg-[#FFF6F0] py-40 pl-22 pr-8 flex items-center" @click="handleTest">
         <view class="flex-1">
           <view class="text-30 text-fore-title font-bold flex items-center">
             <text class="mr-2">教材同步练习</text>
@@ -31,10 +31,16 @@ import { useTransferPage } from '@/hooks/useTransferPage';
 import { getStudyPlan, getDirectedSchool } from '@/api/modules/study';
 import { OPEN_VIP_POPUP } from '@/types/injectionSymbols';
 import { useUserStore } from '@/store/userStore';
+import { EnumExamType } from '@/common/enum';
 
 const { transferTo } = useTransferPage();
 const userStore = useUserStore();
 const openVipPopup = inject(OPEN_VIP_POPUP);
+
+const showCoursePractice = computed(() => {
+  return userStore.getExamType !== EnumExamType.VHS;
+});
+
 const handleOpenPlan = async () => {
   const { data } = await getStudyPlan();
   if (data) {

+ 3 - 8
src/pagesStudy/pages/index/compoentns/index-menu.vue

@@ -11,6 +11,7 @@
 <script lang="ts" setup>
 import { useUserStore } from '@/store/userStore';
 import { useTransferPage } from '@/hooks/useTransferPage';
+import { EnumExamType } from '@/common/enum';
 
 const { transferTo, routes } = useTransferPage();
 const userStore = useUserStore();
@@ -30,7 +31,7 @@ const menus = computed(() => [
     label: '组卷作业',
     icon: '/menu/menu-exam.png',
     pageUrl: routes.pageHomework,
-    visible: userStore.isStudent
+    visible: userStore.isStudent && (userStore.getExamType !== EnumExamType.VHS)
   },
   {
     label: '收藏夹',
@@ -52,13 +53,7 @@ const menus = computed(() => [
   }
 ])
 const navigateTo = (menu: MenuItem) => {
-  if (menu.label === '错题本') {
-    transferTo(routes.pageWrongBook, {
-      data: {}
-    });
-  } else {
-    transferTo(menu.pageUrl);
-  }
+  transferTo(menu.pageUrl);
 }
 </script>
 <style lang="scss" scoped>

+ 146 - 0
src/pagesStudy/pages/index/compoentns/index-practice-entry.vue

@@ -0,0 +1,146 @@
+<template>
+  <view>
+    <view v-if="!isVHS" class="mx-30 mt-20 flex items-center bg-[#E3F4FA] rounded-8 py-16 px-16 gap-x-40">
+      <view class="text-24 text-[#34B0D7] flex-1">
+        <text v-if="!hasDirectedSchool">你还未开启定向学习,快来设置吧!</text>
+        <view v-else class="flex items-center">
+          <text class="flex-shrink-0">定向:</text>
+          <text class="min-w-1 ellipsis-1">{{ firstDirectedSchool.universityName }}</text>
+          <uv-icon name="arrow-right" size="14" color="#0DACF5"></uv-icon>
+          <text class="flex-shrink-0">{{ firstDirectedSchool.majorName }}</text>
+        </view>
+      </view>
+      <view class="text-24 text-white bg-gradient-to-r from-[#26C5F7] to-[#0DACF5] rounded-full px-18 py-6"
+        @click="handleSetting">{{ hasDirectedSchool ? '已开启' : '去开启' }}</view>
+    </view>
+    <view class="mx-30 mt-20">
+      <view class="grid grid-cols-2 items-center gap-x-28">
+        <template v-if="isVHS">
+          <view class="bg-gradient-to-r from-[#0088FE] to-[#31A0FC] flex-1 rounded-15 relative overflow-hidden">
+            <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>
+              <view class="mt-32 w-200 h-56 flex items-center justify-center rounded-full text-26 text-primary bg-white"
+                @click="handlePracticeKnowledge">
+                开始练习
+              </view>
+            </view>
+            <ie-image :is-oss="true" src="/study-bg13.png" custom-class="absolute bottom-0 left-0 w-full h-full z-0"
+              mode="aspectFill" />
+          </view>
+          <!-- <view class="bg-gradient-to-r from-[#32B5FD] to-[#79DCFD] flex-1 rounded-15 relative overflow-hidden">
+            <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>
+              <view class="mt-32 w-200 h-56 flex items-center justify-center rounded-full text-26 text-primary bg-white"
+                @click="handlePracticeMustDo">
+                开始练习
+              </view>
+            </view>
+            <ie-image :is-oss="true" src="/study-bg13.png" custom-class="absolute bottom-0 left-0 w-full h-full z-0"
+              mode="aspectFill" />
+          </view> -->
+        </template>
+        <template v-else>
+          <view class="bg-gradient-to-r from-[#0088FE] to-[#31A0FC] flex-1 rounded-15 relative overflow-hidden">
+            <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>
+              <view class="mt-32 w-200 h-56 flex items-center justify-center rounded-full text-26 text-primary bg-white"
+                @click="handlePracticeAll">
+                开始练习
+              </view>
+            </view>
+            <ie-image :is-oss="true" src="/study-bg13.png" custom-class="absolute bottom-0 left-0 w-full h-full z-0"
+              mode="aspectFill" />
+          </view>
+          <view class="bg-gradient-to-r from-[#32B5FD] to-[#79DCFD] flex-1 rounded-15 relative overflow-hidden">
+            <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>
+              <view class="mt-32 w-200 h-56 flex items-center justify-center rounded-full text-26 text-primary bg-white"
+                @click="handlePracticeDirected">
+                开始练习
+              </view>
+            </view>
+            <ie-image :is-oss="true" src="/study-bg13.png" custom-class="absolute bottom-0 left-0 w-full h-full z-0"
+              mode="aspectFill" />
+          </view>
+        </template>
+      </view>
+    </view>
+  </view>
+</template>
+<script lang="ts" setup>
+import { useTransferPage } from '@/hooks/useTransferPage';
+import { useUserStore } from '@/store/userStore';
+const { transferTo } = useTransferPage();
+const userStore = useUserStore();
+
+const { hasDirectedSchool, directedSchoolList, getExamType, isVHS } = storeToRefs(userStore);
+const firstDirectedSchool = computed(() => directedSchoolList.value[0] || {});
+
+const handlePracticeDirected = async () => {
+  if (!hasDirectedSchool.value) {
+    addTarget();
+    return;
+  }
+  const notice = firstDirectedSchool.value.notice;
+  if (notice) {
+    await uni.$ie.showModal({
+      title: '提示',
+      content: notice,
+      showCancel: false,
+      confirmText: '知道了'
+    });
+  } else {
+    transferTo('/pagesStudy/pages/knowledge-practice/knowledge-practice', {
+      data: {
+        isVHS: false,
+        directed: true
+      }
+    });
+  }
+}
+
+const handlePracticeAll = () => {
+  transferTo('/pagesStudy/pages/knowledge-practice/knowledge-practice', {
+    data: {
+      isVHS: false,
+      directed: false
+    }
+  });
+}
+
+const handlePracticeKnowledge = () => {
+  transferTo('/pagesStudy/pages/knowledge-practice/knowledge-practice', {
+    data: {
+      isVHS: true,
+      directed: false,
+      questionType: 0
+    }
+  });
+}
+
+const handlePracticeMustDo = () => {
+  transferTo('/pagesStudy/pages/knowledge-practice/knowledge-practice', {
+    data: {
+      isVHS: true,
+      directed: false,
+      questionType: 2
+    }
+  });
+}
+
+const handleSetting = async () => {
+  if (hasDirectedSchool.value) {
+    transferTo('/pagesStudy/pages/targeted-setting/targeted-setting');
+  } else {
+    addTarget();
+  }
+}
+const addTarget = () => {
+  transferTo('/pagesStudy/pages/targeted-add/targeted-add');
+}
+</script>
+<style lang="scss" scoped></style>

+ 69 - 0
src/pagesStudy/pages/index/compoentns/vhs-exam.vue

@@ -0,0 +1,69 @@
+<template>
+  <view>
+    <view class="h-16 bg-back my-32"></view>
+    <view class="px-30 pt-13 pb-24 text-32 text-fore-title font-bold">真题&模拟</view>
+    <view class="">
+      <view class="overflow-hidden bg-white sticky z-2" :style="{ top: baseStickyTop + 'px' }">
+        <view class="px-30 pt-24 pb-30 text-32 text-fore-title font-bold flex items-center gap-44 bg-back rounded-t-15">
+          <view :class="['relative text-fore-light', { 'is-active': current === 0 }]" @click="handleChange(0)">公共课
+          </view>
+          <view :class="['relative text-fore-light', { 'is-active': current === 1 }]" @click="handleChange(1)">专业课
+          </view>
+        </view>
+      </view>
+
+      <view v-if="list.length > 0" class="px-30 pb-24 bg-back flex flex-col gap-20 sticky z-1 "
+        :style="{ top: baseStickyTop + 20 + 'px' }">
+        <vhs-exam-item v-for="(item, index) in list" :key="index" :data="item" :type="current" />
+      </view>
+      <view v-else class="bg-white">
+        <z-paging-empty-view :empty-view-fixed="false" />
+      </view>
+    </view>
+  </view>
+</template>
+<script lang="ts" setup>
+import VhsExamItem from '@/pagesStudy/components/vhs-exam-item.vue';
+import { useTransferPage } from '@/hooks/useTransferPage';
+import { useNavbar } from '@/hooks/useNavbar';
+import { getVHSPaperList } from '@/api/modules/study';
+import { Study, Transfer } from '@/types';
+
+const { transferTo } = useTransferPage();
+const { baseStickyTop } = useNavbar();
+const list = ref<Study.VHSPaper[]>([]);
+const current = ref(0);
+const handleChange = (index: number) => {
+  current.value = index;
+  loadData();
+}
+const loadData = async () => {
+  list.value = [];
+  const { data } = await getVHSPaperList({
+    subjectId: current.value
+  });
+  list.value = data;
+}
+onShow(() => {
+  loadData();
+});
+</script>
+<style lang="scss" scoped>
+.is-active {
+  @apply text-fore-title;
+
+  &::after {
+    content: '';
+    display: block;
+    width: 20px;
+    height: 4px;
+    background: radial-gradient(0% 0% at 0% 0%, #31A0FC 0%, #70C8FD 100%);
+    position: absolute;
+    bottom: 0;
+    border-radius: 4rpx;
+    left: 50%;
+    transform: translateX(-50%);
+    bottom: -8rpx;
+  }
+}
+</style>

+ 19 - 101
src/pagesStudy/pages/index/index.vue

@@ -1,75 +1,31 @@
 <template>
   <ie-page ref="iePageRef" bg-color="#FFFFFF" safe-area-inset-bottom-color="#f2f3f7">
-    <view>
-      <ie-navbar>
-        <template #headerLeft>
-          <view class="flex items-center">
-            <uv-icon name="arrow-left" size="20px" color="#333" bold></uv-icon>
-            <ie-image :is-oss="true" src="/study-title.png" custom-class="ml-8 w-148 h-36" mode="heightFix" />
-            <view class="w-6 h-6 rounded-2 bg-black mx-12"></view>
-            <view>
-              <ie-dict :dict-name="EnumDictName.EXAM_TYPE" :dict-value="userStore.getExamType || '--'" />
-            </view>
-          </view>
-        </template>
-      </ie-navbar>
-      <view class="mx-30 mt-20 flex items-center bg-[#E3F4FA] rounded-8 py-16 px-16 gap-x-40">
-        <view class="text-24 text-[#34B0D7] flex-1">
-          <text v-if="!hasDirectedSchool">你还未开启定向学习,快来设置吧!</text>
-          <view v-else class="flex items-center">
-            <text class="flex-shrink-0">定向:</text>
-            <text class="min-w-1 ellipsis-1">{{ firstDirectedSchool.universityName }}</text>
-            <uv-icon name="arrow-right" size="14" color="#0DACF5"></uv-icon>
-            <text class="flex-shrink-0">{{ firstDirectedSchool.majorName }}</text>
-          </view>
-        </view>
-        <view class="text-24 text-white bg-gradient-to-r from-[#26C5F7] to-[#0DACF5] rounded-full px-18 py-6"
-          @click="handleSetting">{{ hasDirectedSchool ? '已开启' : '去开启' }}</view>
-      </view>
-      <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="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>
-              <view class="mt-32 w-200 h-56 flex items-center justify-center rounded-full text-26 text-primary bg-white"
-                @click="handlePracticeAll">
-                开始练习
-              </view>
-            </view>
-            <ie-image :is-oss="true" src="/study-bg13.png" custom-class="absolute bottom-0 left-0 w-full h-full z-0"
-              mode="aspectFill" />
-          </view>
-          <view class="bg-gradient-to-r from-[#32B5FD] to-[#79DCFD] flex-1 rounded-15 relative overflow-hidden">
-            <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>
-              <view class="mt-32 w-200 h-56 flex items-center justify-center rounded-full text-26 text-primary bg-white"
-                @click="handlePracticeDirected">
-                开始练习
-              </view>
-            </view>
-            <ie-image :is-oss="true" src="/study-bg13.png" custom-class="absolute bottom-0 left-0 w-full h-full z-0"
-              mode="aspectFill" />
+    <ie-navbar>
+      <template #headerLeft>
+        <view class="flex items-center">
+          <uv-icon name="arrow-left" size="20px" color="#333"></uv-icon>
+          <ie-image :is-oss="true" src="/study-title.png" custom-class="ml-8 w-148 h-36" mode="heightFix" />
+          <view class="w-6 h-6 rounded-2 bg-black mx-12"></view>
+          <view>
+            <ie-dict :dict-name="EnumDictName.EXAM_TYPE" :dict-value="userStore.getExamType || '--'" />
           </view>
         </view>
-      </view>
-      <index-menu />
-      <index-banner />
-      <block v-if="hasTestAndRecord">
-        <view class="h-16 bg-back my-32"></view>
-        <index-test :directed-school="firstDirectedSchool" />
-        <index-exam-record />
-      </block>
-    </view>
+      </template>
+    </ie-navbar>
+    <index-practice-entry />
+    <index-menu />
+    <index-banner />
+    <vhs-exam v-if="isVHS" :directed-school="firstDirectedSchool" />
+    <ie-exam v-else :directed-school="firstDirectedSchool" />
   </ie-page>
 </template>
 
 <script lang="ts" setup>
 import IndexMenu from './compoentns/index-menu.vue';
 import IndexBanner from './compoentns/index-banner.vue';
-import IndexTest from './compoentns/index-test.vue';
-import IndexExamRecord from './compoentns/index-exam-record.vue';
+import IeExam from './compoentns/ie-exam.vue';
+import VhsExam from './compoentns/vhs-exam.vue';
+import IndexPracticeEntry from './compoentns/index-practice-entry.vue';
 import { EnumDictName, EnumExamType, EnumUserRole } from '@/common/enum';
 import { useUserStore } from '@/store/userStore';
 import { useTransferPage } from '@/hooks/useTransferPage';
@@ -82,47 +38,9 @@ const { hasPermission } = useAuth();
 
 // 通过 ref 获取 ie-page 组件实例
 const iePageRef = ref<InstanceType<typeof IePage>>();
-const { hasDirectedSchool, directedSchoolList, hasTestAndRecord } = toRefs(userStore);
+const { hasDirectedSchool, directedSchoolList, getExamType, isVHS } = storeToRefs(userStore);
 const firstDirectedSchool = computed(() => directedSchoolList.value[0] || {});
 
-const handlePracticeAll = () => {
-  transferTo('/pagesStudy/pages/knowledge-practice/knowledge-practice', {
-    data: {
-      directed: false
-    }
-  });
-}
-const handlePracticeDirected = async () => {
-  if (!hasDirectedSchool.value) {
-    addTarget();
-    return;
-  }
-  const notice = firstDirectedSchool.value.notice;
-  if (notice) {
-    await uni.$ie.showModal({
-      title: '提示',
-      content: notice,
-      showCancel: false,
-      confirmText: '知道了'
-    });
-  } else {
-    transferTo('/pagesStudy/pages/knowledge-practice/knowledge-practice', {
-      data: {
-        directed: true
-      }
-    });
-  }
-}
-const handleSetting = async () => {
-  if (hasDirectedSchool.value) {
-    transferTo('/pagesStudy/pages/targeted-setting/targeted-setting');
-  } else {
-    addTarget();
-  }
-}
-const addTarget = () => {
-  transferTo('/pagesStudy/pages/targeted-add/targeted-add');
-}
 const loadData = async () => {
   await userStore.getDirectedSchoolList();
 }

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

@@ -1,5 +1,5 @@
 <template>
-  <ie-page bg-color="#F6F8FA" safe-area-inset-bottom-color="#FFFFFF" :fix-height="true">
+  <ie-page bg-color="#F6F8FA" :safe-area-inset-bottom="false" :fix-height="true">
     <ie-navbar :title="pageTitle" />
     <view v-if="examineeData" class="relative z-3 pt-30 pb-20 mx-30">
       <view class="bg-white rounded-15 px-20 pb-1">
@@ -25,7 +25,7 @@
       </view>
       <exam-stat :data="examineeData" :show-stats="false" @detail="handleQuestionDetail" />
     </view>
-    <ie-safe-toolbar :height="84" :shadow="false">
+    <ie-safe-toolbar :height="84" :shadow="false" bg-color="#FFFFFF">
       <view class="h-[84px] px-46 bg-white flex items-center justify-between gap-x-40">
         <view class="text-30 text-primary bg-back flex-1 py-22 rounded-full text-center h-fit" @click="handleStartPractice">
           继续刷题

+ 6 - 5
src/pagesStudy/pages/knowledge-practice-history/knowledge-practice-history.vue

@@ -15,7 +15,7 @@
             </view>
             <view class="mt-10 text-28">
               <text class=" text-fore-light">完成时间:</text>
-              <text class="text-fore-title">{{ item.endTime }}</text>
+              <text class="text-fore-title">{{ getTime(item.endTime) }}</text>
             </view>
           </view>
           <view class="text-24 text-white bg-primary w-fit px-40 py-12 rounded-full text-center"
@@ -34,6 +34,8 @@ import { getPracticeHistory } from '@/api/modules/study';
 import { Study } from '@/types';
 import { Transfer } from '@/types';
 import { EnumPaperType } from '@/common/enum';
+import { formatTime } from '@/utils/format';
+
 const { prevData, transferTo } = useTransferPage<{}, Transfer.PracticeResultPageOptions>();
 const { baseStickyTop } = useNavbar();
 const historyList = ref<Study.PracticeHistory[]>([]);
@@ -49,6 +51,9 @@ const handleViewHistory = (value: Study.PracticeHistory) => {
 }
 
 const pagingRef = ref();
+const getTime = (time: string) => {
+  return formatTime(time, 'yyyy-mm-dd hh:MM:ss');
+}
 const handleQuery = (pageNum: number, pageSize: number) => {
   getPracticeHistory({
     pageNum,
@@ -65,9 +70,5 @@ const handleQuery = (pageNum: number, pageSize: number) => {
     pagingRef.value.complete(false);
   });
 }
-onLoad(() => {
-  console.log(prevData.value)
-  // loadData();
-});
 </script>
 <style lang="scss" scoped></style>

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

@@ -131,7 +131,6 @@ const handleDetail = (item: Study.Question) => {
   emit('detail', item);
 }
 setQuestionList(props.data.questions);
-console.log(flatQuestionList.value, 111)
 </script>
 <style lang="scss" scoped>
 .question-item {

+ 30 - 31
src/pagesStudy/pages/simulation-analysis/simulation-analysis.vue

@@ -10,16 +10,18 @@
           <rate-chart :value="rightRate" />
           <view class="h-1 bg-[#E6E6E6] my-20"></view>
           <view>
-            <view class="my-20 flex items-center justify-between text-24">
-              <ie-image src="/pagesStudy/static/image/icon-house.png" custom-class="w-24 h-24" mode="aspectFill" />
-              <text class="ml-10 text-fore-light flex-1">考试院校</text>
-              <text class="text-fore-title">{{ examineeData.collegeName }}-{{ examineeData.majorName }}</text>
-            </view>
-            <view class="my-20 flex items-center justify-between text-24">
-              <ie-image src="/pagesStudy/static/image/icon-group.png" custom-class="w-24 h-24" mode="aspectFill" />
-              <text class="ml-10 text-fore-light flex-1">考试科目</text>
-              <text class="text-fore-title">{{ examineeData.subjectName }}</text>
-            </view>
+            <template v-if="!userStore.isVHS">
+              <view class="my-20 flex items-center justify-between text-24">
+                <ie-image src="/pagesStudy/static/image/icon-house.png" custom-class="w-24 h-24" mode="aspectFill" />
+                <text class="ml-10 text-fore-light flex-1">考试院校</text>
+                <text class="text-fore-title">{{ examineeData.collegeName }}-{{ examineeData.majorName }}</text>
+              </view>
+              <view class="my-20 flex items-center justify-between text-24">
+                <ie-image src="/pagesStudy/static/image/icon-group.png" custom-class="w-24 h-24" mode="aspectFill" />
+                <text class="ml-10 text-fore-light flex-1">考试科目</text>
+                <text class="text-fore-title">{{ examineeData.subjectName }}</text>
+              </view>
+            </template>
             <view class="my-20 flex items-center justify-between text-24">
               <ie-image src="/pagesStudy/static/image/icon-clock.png" custom-class="w-24 h-24" mode="aspectFill" />
               <text class="ml-10 text-fore-light flex-1">考试时长</text>
@@ -40,6 +42,7 @@ 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 { useUserStore } from '@/store/userStore';
 import { useAppStore } from '@/store/appStore';
 import { Study, Transfer } from '@/types';
 import { EnumPaperType, EnumQuestionType } from '@/common/enum';
@@ -47,19 +50,14 @@ import { EnumPaperType, EnumQuestionType } from '@/common/enum';
 const appStore = useAppStore();
 const { prevData, transferTo } = useTransferPage<Transfer.SimulationAnalysisPageOptions, Transfer.ExamAnalysisPageOptions>();
 const examineeData = ref<Study.Examinee>();
-  const titleMap = {
-  [EnumPaperType.PRACTICE]: '知识点练习',
-  [EnumPaperType.SIMULATED]: '模拟考试',
-  [EnumPaperType.TEST]: '组卷作业',
-}
-const readonlyTitleMap = {
-  [EnumPaperType.PRACTICE]: '练习解析',
-  [EnumPaperType.SIMULATED]: '考试解析',
-  [EnumPaperType.TEST]: '作业解析',
-}
+const userStore = useUserStore();
 
 const paperName = computed(() => {
-  return titleMap[prevData.value.paperType as keyof typeof titleMap] + '-' + examineeData.value?.subjectName;
+  if (userStore.isVHS) {
+    return examineeData.value?.name || examineeData.value?.subjectName || '';
+  } else {
+    return examineeData.value?.name || examineeData.value?.subjectName || '';
+  }
 });
 const rightRate = computed(() => {
   const { totalCount = 0, wrongCount = 0 } = examineeData.value || {};
@@ -83,17 +81,18 @@ const formatTime = (time: number) => {
 const handleDetail = (item: Study.Question) => {
   if (!examineeData.value) {
     return;
-  }  
-  transferTo('/pagesStudy/pages/exam-start/exam-start', {
-    data: {
+  }
+  const pageOptions: Transfer.ExamAnalysisPageOptions = {
+    readonly: true,
+    questionId: item.id,
+    paperType: prevData.value.paperType,
+    simulationInfo: {
       name: paperName.value,
-      readonly: true,
-      questionId: item.id,
-      paperType: prevData.value.paperType,
-      simulationInfo: {
-        examineeId: examineeData.value.examineeId
-      }
+      examineeId: examineeData.value.examineeId
     }
+  }
+  transferTo('/pagesStudy/pages/exam-start/exam-start', {
+    data: pageOptions,
   });
 };
 const loadData = async () => {
@@ -108,4 +107,4 @@ onLoad(() => {
 onPageScroll(() => { })
 </script>
 
-<style lang="scss" scoped></style>
+<style lang="scss" scoped></style>

+ 11 - 59
src/pagesStudy/pages/study-history/components/exam-history.vue

@@ -1,66 +1,18 @@
 <template>
-  <view class="flex-1 min-h-1 bg-white">
-    <view class="px-30 py-30 flex gap-x-20">
-      <view class="exam-type-item" :class="{ 'is-active': examType === EnumExamRecordType.SIMULATED }"
-        @click="handleChangeExamType(EnumExamRecordType.SIMULATED)">
-        <ie-image src="/pagesStudy/static/image/icon-exam-test.png" custom-class="w-64 h-60" />
-        <view class="exam-type-text">模拟仿真</view>
-      </view>
-      <view class="exam-type-item" :class="{ 'is-active': examType === EnumExamRecordType.HOMEWORK }"
-        @click="handleChangeExamType(EnumExamRecordType.HOMEWORK)">
-        <ie-image src="/pagesStudy/static/image/icon-exam-homework.png" custom-class="w-64 h-60" />
-        <view class="exam-type-text">组卷作业</view>
-      </view>
-    </view>
-    <exam-history-student v-if="isStudent" :exam-type="examType" />
-    <exam-history-teacher v-else :exam-type="examType" />
+  <view class="h-full relative bg-back">
+    <ie-exam-history v-if="getLocation === '湖南'" />
+    <vhs-exam-history v-if="getLocation === '河南'" />
   </view>
 </template>
 <script lang="ts" setup>
-defineOptions({
-  options: {
-    virtualHost: true
-  }
-});
-import { EnumExamRecordType } from '@/common/enum';
-import examHistoryStudent from './exam-history-student.vue';
-import examHistoryTeacher from './exam-history-teacher.vue';
+import IeExamHistory from './ie-exam-history.vue'
+import VhsExamHistory from './vhs-exam-history.vue'
 import { useUserStore } from '@/store/userStore';
-const { isStudent } = storeToRefs(useUserStore());
-const examType = ref(EnumExamRecordType.SIMULATED);
+const userStore = useUserStore();
+const { getLocation } = storeToRefs(userStore);
 
-const handleChangeExamType = (type: EnumExamRecordType) => {
-  examType.value = type;
-}
+onShow(() => {
+  console.log(2)
+});
 </script>
-<style lang="scss" scoped>
-.exam-type-item {
-  @apply flex-1 h-175 rounded-15 bg-[#F5F5F5] flex flex-col items-center justify-center gap-y-10;
-}
-
-.exam-type-text {
-  @apply text-28 text-fore-title font-bold;
-}
-
-.is-active {
-  @apply bg-[#E6F7FF] relative;
-
-  &::after {
-    content: "";
-    display: block;
-    position: absolute;
-    bottom: -9px;
-    left: 50%;
-    transform: translateX(-50%);
-    width: 0;
-    height: 0;
-    border-left: 14px solid transparent;
-    border-right: 14px solid transparent;
-    border-top: 10px solid #E6F7FF;
-  }
-
-  .exam-type-text {
-    @apply text-primary;
-  }
-}
-</style>
+<style lang="scss" scoped></style>

+ 57 - 0
src/pagesStudy/pages/study-history/components/ie-exam-history-student.vue

@@ -0,0 +1,57 @@
+<template>
+  <view class="pb-30">
+    <view v-if="examType === EnumExamRecordType.SIMULATED" class="px-30">
+      <view class="sibling-border-top px-20 py-30" v-for="(item, index) in simulatedRecordList" :key="index">
+        <ie-exam-record-item :data="item" />
+      </view>
+    </view>
+    <template v-else>
+      <template v-if="paperWorkRecordList.length > 0">
+        <view class="sibling-border-top" v-for="(item, index) in paperWorkRecordList" :key="index">
+          <paper-work-item :data="item" />
+        </view>
+      </template>
+      <view v-else class="mt-50 mx-30 bg-[#F6F8FA] text-center py-50 text-26 text-fore-light rounded-5">暂无数据</view>
+    </template>
+  </view>
+</template>
+<script lang="ts" setup>
+import { getSimulatedRecord, getPaperWorkList } from '@/api/modules/study';
+import { EnumExamRecordType, EnumPaperWorkState } from '@/common/enum';
+import IeExamRecordItem from '@/pagesStudy/components/ie-exam-record-item.vue';
+import { Study } from '@/types';
+import PaperWorkItem from '@/pagesStudy/components/paper-work-item.vue';
+const props = defineProps({
+  examType: {
+    type: String,
+    default: 'test'
+  }
+});
+const simulatedRecordList = ref<Study.SimulatedRecord[]>([]);
+const paperWorkRecordList = ref<Study.PaperWork[]>([]);
+const loadData = async (type: string) => {
+  simulatedRecordList.value = [];
+  paperWorkRecordList.value = [];
+  if (type === EnumExamRecordType.SIMULATED) {
+    const { data } = await getSimulatedRecord();
+    simulatedRecordList.value = data;
+  } else {
+    const { rows } = await getPaperWorkList({ state: EnumPaperWorkState.COMPLETED });
+    paperWorkRecordList.value = rows;
+  }
+}
+
+watch(() => props.examType, (newVal) => {
+  loadData(newVal);
+}, {
+  immediate: false
+});
+onShow(() => {
+  console.log(44)
+});
+onMounted(() => {
+  console.log(4)
+  loadData(props.examType);
+});
+</script>
+<style lang="scss" scoped></style>

+ 18 - 0
src/pagesStudy/pages/study-history/components/ie-exam-history-teacher.vue

@@ -0,0 +1,18 @@
+<template>
+  <view class="p-30 pt-50">
+    <exam-history-simulated v-if="examType === 'simulated'" />
+    <exam-history-paperwork v-else />
+  </view>
+</template>
+<script lang="ts" setup>
+import examHistorySimulated from './exam-history-simulated.vue';
+import examHistoryPaperwork from './exam-history-paperwork.vue';
+
+const props = defineProps({
+  examType: {
+    type: String,
+    default: 'test'
+  }
+});
+</script>
+<style lang="scss" scoped></style>

+ 66 - 0
src/pagesStudy/pages/study-history/components/ie-exam-history.vue

@@ -0,0 +1,66 @@
+<template>
+  <view class="h-full bg-white flex flex-col">
+    <view class="px-30 py-20 flex gap-x-20">
+      <view class="exam-type-item" :class="{ 'is-active': examType === EnumExamRecordType.SIMULATED }"
+        @click="handleChangeExamType(EnumExamRecordType.SIMULATED)">
+        <ie-image src="/pagesStudy/static/image/icon-exam-test.png" custom-class="w-64 h-60" />
+        <view class="exam-type-text">模拟仿真</view>
+      </view>
+      <view class="exam-type-item" :class="{ 'is-active': examType === EnumExamRecordType.HOMEWORK }"
+        @click="handleChangeExamType(EnumExamRecordType.HOMEWORK)">
+        <ie-image src="/pagesStudy/static/image/icon-exam-homework.png" custom-class="w-64 h-60" />
+        <view class="exam-type-text">组卷作业</view>
+      </view>
+    </view>
+    <scroll-view class="flex-1 min-h-1" scroll-y>
+      <ie-exam-history-student v-if="isStudent" :exam-type="examType" />
+      <ie-exam-history-teacher v-else :exam-type="examType" />
+    </scroll-view>
+  </view>
+</template>
+<script lang="ts" setup>
+import { EnumExamRecordType } from '@/common/enum';
+import IeExamHistoryStudent from './ie-exam-history-student.vue';
+import IeExamHistoryTeacher from './ie-exam-history-teacher.vue';
+import { useUserStore } from '@/store/userStore';
+const { isStudent } = storeToRefs(useUserStore());
+const examType = ref(EnumExamRecordType.SIMULATED);
+
+const handleChangeExamType = (type: EnumExamRecordType) => {
+  examType.value = type;
+}
+onShow(() => {
+  console.log(3)
+});
+</script>
+<style lang="scss" scoped>
+.exam-type-item {
+  @apply flex-1 h-175 rounded-15 bg-[#F5F5F5] flex flex-col items-center justify-center gap-y-10;
+}
+
+.exam-type-text {
+  @apply text-28 text-fore-title font-bold;
+}
+
+.is-active {
+  @apply bg-[#E6F7FF] relative;
+
+  &::after {
+    content: "";
+    display: block;
+    position: absolute;
+    bottom: -9px;
+    left: 50%;
+    transform: translateX(-50%);
+    width: 0;
+    height: 0;
+    border-left: 14px solid transparent;
+    border-right: 14px solid transparent;
+    border-top: 10px solid #E6F7FF;
+  }
+
+  .exam-type-text {
+    @apply text-primary;
+  }
+}
+</style>

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

@@ -1,5 +1,7 @@
 <template>
-  <knowledge-table :data="dataList" />
+  <view class="h-full">
+    <knowledge-table :data="dataList" />
+  </view>
 </template>
 <script lang="ts" setup>
 import { getKnowledgeRecord } from '@/api/modules/study';

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

@@ -1,5 +1,5 @@
 <template>
-  <view class="bg-white">
+  <view class="bg-white h-full">
     <knowledge-history-student v-if="isStudent" />
     <teacher-class-view v-else>
       <template #default="{ teachClass }">

+ 1 - 1
src/pagesStudy/pages/study-history/components/practice-history.vue

@@ -1,5 +1,5 @@
 <template>
-  <view class="flex-1 min-h-1 bg-white">
+  <view class="h-full bg-white">
     <practice-history-student v-if="isStudent" />
     <teacher-class-view v-else>
       <template #default="{ teachClass }">

+ 32 - 0
src/pagesStudy/pages/study-history/components/vhs-exam-history-student.vue

@@ -0,0 +1,32 @@
+<template>
+  <view class="flex flex-col gap-20 px-30 pb-20 bg-back h-fit">
+    <template v-if="simulatedRecordList.length > 0">
+      <view class="" v-for="(item, index) in simulatedRecordList" :key="index">
+        <vhs-exam-record-item :data="item" />
+      </view>
+    </template>
+    <view v-else class="mt-200">
+      <z-paging-empty-view :empty-view-fixed="false" />
+    </view>
+  </view>
+</template>
+<script lang="ts" setup>
+import { getSimulatedRecord } from '@/api/modules/study';
+import VhsExamRecordItem from '@/pagesStudy/components/vhs-exam-record-item.vue';
+import { Study } from '@/types';
+
+const simulatedRecordList = ref<Study.SimulatedRecord[]>([]);
+const paperWorkRecordList = ref<Study.PaperWork[]>([]);
+const loadData = async () => {
+  simulatedRecordList.value = [];
+  paperWorkRecordList.value = [];
+  const { data } = await getSimulatedRecord();
+  simulatedRecordList.value = data;
+  console.log(data, 123)
+}
+
+onShow(() => {
+  loadData();
+});
+</script>
+<style lang="scss" scoped></style>

+ 93 - 0
src/pagesStudy/pages/study-history/components/vhs-exam-history-teacher.vue

@@ -0,0 +1,93 @@
+<template>
+  <view class="h-full bg-white">
+    <teacher-class-view>
+      <template #default="{ teachClass }">
+        <view class="">
+          <ie-table :tableColumns="tableColumns" :data="tableData" :cellStyle="cellStyle" :headerFixed="true">
+            <template #name="{ item }">
+              <text class="font-bold">{{ item.name }}</text>
+            </template>
+            <template #total="{ item }">
+              <text class="font-bold">{{ item.value }}/{{ item.total }}</text>
+            </template>
+            <template #action="{ item }">
+              <text class="text-30 text-primary font-bold" @click="handleRowClick(item)">查看</text>
+            </template>
+          </ie-table>
+        </view>
+      </template>
+    </teacher-class-view>
+  </view>
+</template>
+<script lang="ts" setup>
+import teacherClassView from '@/pagesStudy/components/teacher-class-view.vue';
+import examHistorySimulated from './exam-history-simulated.vue';
+import examHistoryPaperwork from './exam-history-paperwork.vue';
+import { Study } from '@/types';
+import { TableColumnConfig } from '@/types';
+import { CSSProperties } from 'vue';
+import { useTransferPage } from '@/hooks/useTransferPage';
+import { getClassExamRecord } from '@/api/modules/study';
+const { transferTo } = useTransferPage();
+
+const props = defineProps({
+  examType: {
+    type: String,
+    default: 'test'
+  }
+});
+const cellStyle: CSSProperties = {
+  padding: '30rpx 20rpx'
+}
+const mockData = {
+  name: '1班',
+  total: 100,
+  value: 100,
+  rate: 100,
+}
+const tableData = ref<Study.StudentExamRecord[]>(new Array(20).fill(mockData));
+const tableColumns: TableColumnConfig[] = [
+  {
+    prop: 'name',
+    label: '班级',
+    flex: 1,
+    slot: 'name',
+    headerAlign: 'center',
+    align: 'center'
+  },
+  {
+    prop: 'total',
+    label: '做卷情况',
+    flex: 1,
+    slot: 'total'
+  },
+  {
+    prop: 'action',
+    label: '操作',
+    flex: 1,
+    slot: 'action'
+  }
+];
+
+const handleRowClick = (row: Study.StudentExamRecord) => {
+  transferTo('/pagesStudy/pages/study-exam-simulated-class/study-exam-simulated-class', {
+    data: {
+      classId: row.id,
+      name: row.name
+    }
+  });
+}
+
+const loadData = async () => {
+  uni.$ie.showLoading();
+  await getClassExamRecord({}).then(res => {
+    tableData.value = res.data;
+  });
+  uni.$ie.hideLoading();
+}
+
+onMounted(() => {
+  // loadData();
+})
+</script>
+<style lang="scss" scoped></style>

+ 44 - 0
src/pagesStudy/pages/study-history/components/vhs-exam-history.vue

@@ -0,0 +1,44 @@
+<template>
+  <view class="h-full">
+    <vhs-exam-history-student v-if="isStudent" />
+    <vhs-exam-history-teacher v-else />
+  </view>
+</template>
+<script lang="ts" setup>
+import VhsExamHistoryStudent from './vhs-exam-history-student.vue';
+import VhsExamHistoryTeacher from './vhs-exam-history-teacher.vue';
+import { useUserStore } from '@/store/userStore';
+
+const { isStudent } = storeToRefs(useUserStore());
+</script>
+<style lang="scss" scoped>
+.exam-type-item {
+  @apply flex-1 h-175 rounded-15 bg-[#F5F5F5] flex flex-col items-center justify-center gap-y-10;
+}
+
+.exam-type-text {
+  @apply text-28 text-fore-title font-bold;
+}
+
+.is-active {
+  @apply bg-[#E6F7FF] relative;
+
+  &::after {
+    content: "";
+    display: block;
+    position: absolute;
+    bottom: -9px;
+    left: 50%;
+    transform: translateX(-50%);
+    width: 0;
+    height: 0;
+    border-left: 14px solid transparent;
+    border-right: 14px solid transparent;
+    border-top: 10px solid #E6F7FF;
+  }
+
+  .exam-type-text {
+    @apply text-primary;
+  }
+}
+</style>

+ 8 - 5
src/pagesStudy/pages/study-history/study-history.vue

@@ -1,15 +1,17 @@
 <template>
   <ie-page bg-color="#FFFFFF" :fix-height="true">
     <ie-navbar title="学习记录" />
-    <view class="bg-white sticky z-1" :style="{ top: baseStickyTop + 'px' }">
+    <view class="bg-white" :style="{ top: baseStickyTop + 'px' }">
       <uv-tabs :list="list" :current="current" :activeStyle="activeStyle" :inactiveStyle="inactiveStyle"
         :scrollable="false" @change="handleTabChange"></uv-tabs>
     </view>
     <view class="h-20 bg-[#F6F8FA]"></view>
-    <knowledge-history v-if="current === 0" />
-    <exam-history v-if="current === 1" />
-    <practice-history v-if="current === 2" />
-    <video-history v-if="current === 3" />
+    <scroll-view class="flex-1 min-h-1 box-border" scroll-y>
+      <knowledge-history v-if="current === 0" />
+      <exam-history v-if="current === 1" />
+      <practice-history v-if="current === 2" />
+      <video-history v-if="current === 3" />
+    </scroll-view>
   </ie-page>
 </template>
 
@@ -37,6 +39,7 @@ const inactiveStyle = {
 const handleTabChange = (e: any) => {
   current.value = e.index;
 }
+onShow(() => { });
 </script>
 
 <style lang="scss" scoped></style>

+ 102 - 0
src/pagesStudy/pages/video/index/index.vue

@@ -0,0 +1,102 @@
+<template>
+  <ie-page>
+    <z-paging ref="paging" v-model="knowledgeList" :auto="false" :loading-more-enabled="false"
+      :safe-area-inset-bottom="true" :hide-no-more-by-limit="10" @query="handleQuery">
+      <template #top>
+        <ie-navbar title="视频课程" />
+        <uv-tabs :current="current" keyName="label" :list="subjects" :scrollable="false" @change="handleTabChange" />
+        <uv-line margin="0" />
+      </template>
+      <view class="h-[8px] bg-back" />
+      <uv-collapse ref="collapse" :border="false">
+        <uv-collapse-item v-for="item in knowledgeList" :key="item.code" :data="item" :duration="200" :lazy="true" :load="handleLoad">
+          <template #title="{ expanded, loading }">
+            <view class="flex items-center justify-between">
+              <view class="flex items-center gap-10">
+                <uv-icon name="arrow-right" size="14" color="#888" custom-class="transition-transform duration-300"
+                  :class="{ 'rotate-90': expanded }" />
+                <uv-loading-icon v-if="loading" mode="spinner" size="16"></uv-loading-icon>
+                <view>{{ item.label }}</view>
+              </view>
+            </view>
+          </template>
+          <template #right-icon>
+            <view></view>
+          </template>
+          <template #default="{ expanded }">
+            <view class="pl-20">
+              <view v-for="child in item.children" :key="child.name" class="flex items-center gap-20 mb-20">
+                <ie-image :src="child.img" custom-class="w-100 h-64" />
+                <view class="flex-1 min-w-1 ellipsis-1 text-28 text-fore-title">{{ child.name }}</view>
+                <view
+                  class="px-20 py-8 border border-solid border-primary rounded-full text-24 text-primary flex items-center gap-8"
+                  @click.stop="handlePlay(item, child)">
+                  <uv-icon name="play-circle" size="16" color="var(--primary-color)" />
+                  <text>播放</text>
+                </view>
+              </view>
+            </view>
+          </template>
+        </uv-collapse-item>
+      </uv-collapse>
+    </z-paging>
+  </ie-page>
+</template>
+
+<script lang="ts" setup>
+import { getVideoCourseSubjects, getVideoCourseKnowledges, getVideoCourseList } from '@/api/modules/study';
+import type { Study } from '@/types';
+import { useTransferPage } from '@/hooks/useTransferPage';
+
+const { transferTo, routes } = useTransferPage();
+const paging = ref<ZPagingInstance>();
+const subjects = ref<Study.VideoCourseSubject[]>([]);
+const current = ref(0);
+const knowledgeList = ref<Study.VideoCourseKnowledge[]>([]);
+
+const handleQuery = (page: number, size: number) => {
+  getVideoCourseKnowledges(subjects.value[current.value].code).then(res => {
+    paging.value?.completeByNoMore(res.rows, true);
+  });
+}
+const handleTabChange = (e: any) => {
+  current.value = e.index;
+  paging.value?.reload();
+}
+const handlePlay = (parent: Study.VideoCourseKnowledge, data: Study.VideoCourse) => {
+  transferTo(routes.pageCourseVideoPlay, {
+    data: {
+      knowledge: parent,
+      video: data,
+    }
+  });
+}
+const handleLoad = async (data: Study.VideoCourseKnowledge, callback: () => {}) => {
+  const queryParams = {
+    pageNum: 1,
+    pageSize: 1000,
+    subject: subjects.value[current.value].code,
+    knowledge: data.code,
+  } as Study.VideoCourseRequestDTO;
+  getVideoCourseList(queryParams).then(res => {
+    data.children = res.rows;
+    callback();
+  });
+}
+const loadData = async () => {
+  const queryParams = {
+    pageNum: 1,
+    pageSize: 1000,
+    type: 1,
+  } as Study.VideoCourseSubjectRequestDTO;
+  getVideoCourseSubjects(queryParams).then(res => {
+    subjects.value = res.rows;
+    setTimeout(() => {
+      paging.value?.reload();
+    }, 100);
+  });
+}
+onLoad(() => {
+  loadData();
+});
+</script>

+ 138 - 0
src/pagesStudy/pages/video/play/play.vue

@@ -0,0 +1,138 @@
+<template>
+  <ie-page bg-color="#F6F8FA">
+    <ie-navbar :title="pageTitle" />
+    <view class="">
+      <view class="h-[200px] bg-black">
+        <video v-if="playUrl" id="player" class="w-full h-full" :src="playUrl" :poster="coverUrl" object-fit="fill"
+          controls enable-play-gesture play-btn-position="center" :picture-in-picture-mode="[]"
+          :show-background-playback-button="false" @timeupdate="handleTimeUpdate" @ended="handlePlayEnd">
+        </video>
+      </view>
+      <view class="flex items-center gap-20 px-20 py-16 bg-white">
+        <text class="text-28 text-fore-light">倍速播放</text>
+        <uv-tags v-for="item in rateList" :key="item" :text="`x${item}`" size="mini" type="primary"
+          :plain="currentRate !== item" @click="handleRate(item)" custom-class="w-[28px] text-center">
+        </uv-tags>
+      </view>
+      <view class="bg-white">
+        <uv-cell-group>
+          <uv-cell v-for="item in videoList" :key="item.aliId" :title="item.name">
+            <template #title>
+              <view class="flex items-center gap-20" @click="handlePlay(item)">
+                <ie-image :src="item.img" custom-class="w-100 h-64" />
+                <view class="flex-1 min-w-1 ellipsis-1 text-28"
+                  :class="[currentVideo?.aliId === item.aliId ? 'text-primary' : 'text-fore-title']">
+                  {{ item.name }}
+                </view>
+                <view class="text-24 text-primary flex items-center">
+                  <uv-icon :name="currentVideo?.aliId === item.aliId ? 'play-circle-fill' : 'play-circle'" size="20"
+                    color="var(--primary-color)" />
+                </view>
+              </view>
+            </template>
+          </uv-cell>
+        </uv-cell-group>
+      </view>
+    </view>
+  </ie-page>
+</template>
+
+<script lang="ts" setup>
+import { useTransferPage } from '@/hooks/useTransferPage';
+import type { Study } from '@/types';
+import { getVideoCoursePlayInfo, saveVideoCourseRecord } from '@/api/modules/study';
+
+let videoContext: UniApp.VideoContext;
+const rateList = ref<number[]>([0.5, 1, 1.25, 1.5, 2]);
+const { prevData, transferBack } = useTransferPage();
+const currentRate = ref<number>(1);
+const currentVideo = ref<Study.VideoCourse>();
+const duration = ref(0);
+const currentTime = ref(0);
+
+const videoList = computed(() => {
+  const { knowledge } = prevData.value as { knowledge: Study.VideoCourseKnowledge };
+  return knowledge?.children || [];
+});
+const videoIndex = computed(() => {
+  return videoList.value.findIndex(item => item.aliId === currentVideo.value?.aliId);
+});
+const pageTitle = computed(() => {
+  return videoList.value[videoIndex.value]?.name || '';
+});
+const playUrl = ref('');
+const coverUrl = ref('');
+const handleRate = (item: number) => {
+  if (videoContext) {
+    currentRate.value = item;
+    videoContext.playbackRate(item);
+  }
+}
+const handleTimeUpdate = (e: any) => {
+  const newTime = Number(e.detail.currentTime);
+  if (newTime > currentTime.value && (newTime - currentTime.value > 5)) {
+    saveRecord(newTime, duration.value);
+    currentTime.value = newTime;
+  }
+  if (!isNaN(e.detail.duration)) {
+    duration.value = Number(e.detail.duration);
+  }
+}
+const handlePlayEnd = () => {
+  saveRecord(duration.value, duration.value);
+}
+const saveRecord = (current: number, duration: number) => {
+  if (current > 0 && duration > 0) {
+    const percentage = ((current / duration) * 100).toFixed(2);
+    saveVideoCourseRecord({
+      sectionId: currentVideo.value!.aliId,
+      duration: Math.floor(duration),
+      percent: percentage,
+      type: currentVideo.value!.aliIdType
+    }).then(res => {
+      console.log(res);
+    });
+  }
+}
+const handlePlay = async (item: Study.VideoCourse) => {
+  currentVideo.value = item;
+  // 等待播放暂停
+  videoContext.pause();
+  await new Promise(resolve => setTimeout(resolve, 150));
+  currentTime.value = 0;
+  duration.value = 0;
+  await loadData();
+  setTimeout(() => {
+    videoContext.play();
+  }, 0);
+}
+const loadData = async () => {
+  if (currentVideo.value) {
+    const aliId = currentVideo.value?.aliId || '';
+    if (aliId.includes('https') || aliId.includes('http')) {
+      playUrl.value = aliId;
+    } else {
+      uni.$ie.showLoading();
+      await getVideoCoursePlayInfo(aliId).then(res => {
+        playUrl.value = res.data.palyUrl;
+        coverUrl.value = res.data.coverUrl;
+      }).finally(() => {
+        uni.$ie.hideLoading();
+      });
+    }
+  }
+}
+
+onLoad(() => {
+  currentVideo.value = prevData.value.video;
+  loadData();
+});
+onUnload(() => {
+  saveRecord(currentTime.value, duration.value);
+});
+onReady(() => {
+  videoContext = uni.createVideoContext('player');
+});
+</script>
+
+<style lang="scss"></style>

+ 4 - 2
src/pagesStudy/pages/wrong-book/wrong-book.vue

@@ -122,10 +122,12 @@ const handleChange = (e: any) => {
 const loadData = () => {
   getStudentSubject().then(res => {
     tabs.value = res.data;
-    paging.value?.reload();
+    setTimeout(() => {
+      paging.value?.reload();
+    }, 0);
   });
 }
-onLoad(() => {
+onMounted(() => {
   loadData();
 });
 </script>

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

@@ -5,7 +5,7 @@
       <content-card title="个人信息">
         <uv-form-item label="姓名" prop="name" borderBottom required>
           <uv-input v-model="form.nickName" border="none" placeholder="请输入姓名" placeholderClass="text-[15px]"
-            font-size="30rpx" :custom-style="customStyle">
+            font-size="15px" :custom-style="customStyle">
           </uv-input>
         </uv-form-item>
         <uv-form-item label="所在省份" prop="location" borderBottom required>
@@ -31,7 +31,7 @@
             :disabled="!examTypeForm.examType" :placeholder="pickerPlaceholder" :custom-style="customStyle"
             key-label="dictLabel" key-value="dictValue"></ie-picker>
         </uv-form-item>
-        <uv-form-item label="单招年份" prop="year" required>
+        <uv-form-item label="毕业年份" prop="year" required>
           <ie-picker ref="pickerRef" v-model="examTypeForm.endYear" :list="endYearList"
             :disabled="!examTypeForm.examType" :placeholder="pickerPlaceholder" :custom-style="customStyle"
             key-label="dictLabel" key-value="dictValue" @click="handlePreCheck('endYear')"></ie-picker>
@@ -40,7 +40,7 @@
       </content-card>
       <content-card title="邀请信息">
         <uv-form-item label="邀请码" prop="form.inviteCode">
-          <uv-input v-model="form.inviteCode" border="none" placeholder="请输入邀请码(非必填)" font-size="30rpx"
+          <uv-input v-model="form.inviteCode" border="none" placeholder="请输入邀请码(非必填)" font-size="15px"
             :custom-style="customStyle">
           </uv-input>
         </uv-form-item>
@@ -49,27 +49,27 @@
       <content-card v-if="showCulture" title="文化素质">
         <uv-form-item label="语文" prop="form.scores.chinese" borderBottom :required="isScoreRequired">
           <uv-input v-model.number="scoresForm.chinese" border="none" type="number" :placeholder="inputPlaceholder"
-            font-size="30rpx" :custom-style="customStyle">
+            font-size="15px" :custom-style="customStyle">
           </uv-input>
         </uv-form-item>
         <uv-form-item label="数学" prop="form.score.mathematics" borderBottom :required="isScoreRequired">
           <uv-input v-model.number="scoresForm.mathematics" border="none" type="number" :placeholder="inputPlaceholder"
-            font-size="30rpx" :custom-style="customStyle">
+            font-size="15px" :custom-style="customStyle">
           </uv-input>
         </uv-form-item>
         <uv-form-item label="外语" prop="form.scores.foreign" borderBottom :required="isScoreRequired">
           <uv-input v-model.number="scoresForm.foreign" border="none" type="number" :placeholder="inputPlaceholder"
-            font-size="30rpx" :custom-style="customStyle">
+            font-size="15px" :custom-style="customStyle">
           </uv-input>
         </uv-form-item>
         <uv-form-item label="物理" prop="form.scores.physics" borderBottom :required="isScoreRequired">
           <uv-input v-model.number="scoresForm.physics" border="none" type="number" :placeholder="inputPlaceholder"
-            font-size="30rpx" :custom-style="customStyle">
+            font-size="15px" :custom-style="customStyle">
           </uv-input>
         </uv-form-item>
         <uv-form-item label="政治" prop="form.scores.political" :required="isScoreRequired">
           <uv-input v-model.number="scoresForm.political" border="none" type="number" :placeholder="inputPlaceholder"
-            font-size="30rpx" :custom-style="customStyle">
+            font-size="15px" :custom-style="customStyle">
           </uv-input>
         </uv-form-item>
       </content-card>

+ 9 - 9
src/pagesSystem/pages/bind-teacher-profile/bind-teacher-profile.vue

@@ -5,7 +5,7 @@
       <content-card title="个人信息">
         <uv-form-item label="姓名" prop="name" borderBottom required>
           <uv-input v-model="form.nickName" border="none" placeholder="请输入姓名" placeholderClass="text-30"
-            font-size="30rpx" :custom-style="customStyle">
+            font-size="15px" :custom-style="customStyle">
           </uv-input>
         </uv-form-item>
         <uv-form-item label="所在省份" prop="location" borderBottom required>
@@ -33,7 +33,7 @@
               <ie-image src="/static/image/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
             </template></ie-picker>
         </uv-form-item>
-        <uv-form-item label="单招年份" prop="year" required>
+        <uv-form-item label="毕业年份" prop="year" required>
           <ie-picker ref="pickerRef" v-model="examTypeForm.endYear" :list="endYearList"
             :disabled="!examTypeForm.examType || disabledEdit" :placeholder="pickerPlaceholder"
             :custom-style="customStyle" key-label="dictLabel" key-value="dictValue">
@@ -46,33 +46,33 @@
       <content-card v-if="showCulture" title="文化素质">
         <uv-form-item label="语文" prop="form.scores.chinese" borderBottom :required="isScoreRequired">
           <uv-input v-model.number="scoresForm.chinese" border="none" type="number" :placeholder="inputPlaceholder"
-            font-size="30rpx" :custom-style="customStyle" :readonly="disabledEdit">
+            font-size="15px" :custom-style="customStyle" :readonly="disabledEdit">
           </uv-input>
         </uv-form-item>
         <uv-form-item label="数学" prop="form.score.mathematics" borderBottom :required="isScoreRequired">
           <uv-input v-model.number="scoresForm.mathematics" border="none" type="number" :placeholder="inputPlaceholder"
-            font-size="30rpx" :custom-style="customStyle" :readonly="disabledEdit">
+            font-size="15px" :custom-style="customStyle" :readonly="disabledEdit">
           </uv-input>
           <ie-image v-if="disabledEdit" slot="right" src="/static/image/icon-lock.png" custom-class="w-24 h-30"
             mode="aspectFill" />
         </uv-form-item>
         <uv-form-item label="外语" prop="form.scores.foreign" borderBottom :required="isScoreRequired">
           <uv-input v-model.number="scoresForm.foreign" border="none" type="number" :placeholder="inputPlaceholder"
-            font-size="30rpx" :custom-style="customStyle" :readonly="disabledEdit">
+            font-size="15px" :custom-style="customStyle" :readonly="disabledEdit">
           </uv-input>
           <ie-image v-if="disabledEdit" slot="right" src="/static/image/icon-lock.png" custom-class="w-24 h-30"
             mode="aspectFill" />
         </uv-form-item>
         <uv-form-item label="物理" prop="form.scores.physics" borderBottom :required="isScoreRequired">
           <uv-input v-model.number="scoresForm.physics" border="none" type="number" :placeholder="inputPlaceholder"
-            font-size="30rpx" :custom-style="customStyle" :readonly="disabledEdit">
+            font-size="15px" :custom-style="customStyle" :readonly="disabledEdit">
           </uv-input>
           <ie-image v-if="disabledEdit" slot="right" src="/static/image/icon-lock.png" custom-class="w-24 h-30"
             mode="aspectFill" />
         </uv-form-item>
         <uv-form-item label="政治" prop="form.scores.political" :required="isScoreRequired">
           <uv-input v-model.number="scoresForm.political" border="none" type="number" :placeholder="inputPlaceholder"
-            font-size="30rpx" :custom-style="customStyle" :readonly="disabledEdit">
+            font-size="15px" :custom-style="customStyle" :readonly="disabledEdit">
           </uv-input>
           <ie-image v-if="disabledEdit" slot="right" src="/static/image/icon-lock.png" custom-class="w-24 h-30"
             mode="aspectFill" />
@@ -81,14 +81,14 @@
 
       <content-card v-if="showSchoolInfo" title="学校信息">
         <uv-form-item label="学校名称" prop="form.campusName" borderBottom>
-          <uv-input v-model="form.campusName" border="none" placeholder="" placeholderClass="text-30" font-size="30rpx"
+          <uv-input v-model="form.campusName" border="none" placeholder="" placeholderClass="text-30" font-size="15px"
             :custom-style="customStyle" readonly>
           </uv-input>
           <ie-image slot="right" src="/static/image/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
         </uv-form-item>
         <uv-form-item label="所在班级" prop="form.campusClassName">
           <uv-input v-model="form.campusClassName" border="none" placeholder="" placeholderClass="text-30"
-            font-size="30rpx" :custom-style="customStyle" readonly>
+            font-size="15px" :custom-style="customStyle" readonly>
           </uv-input>
           <ie-image slot="right" src="/static/image/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
         </uv-form-item>

+ 1 - 1
src/pagesSystem/pages/edit-profile/edit-profile.vue

@@ -33,7 +33,7 @@
             </view>
             <ie-image slot="right" src="/static/image/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
           </uv-form-item>
-          <uv-form-item label="单招年份" prop="name">
+          <uv-form-item label="毕业年份" prop="name">
             <uv-input v-model="form.endYear" border="none" placeholder="" placeholderClass="text-[15px]" font-size="15px"
               :custom-style="customStyle" readonly>
             </uv-input>

+ 4 - 1
src/static/theme/theme.module.scss

@@ -1,3 +1,6 @@
+// 导入主题变量
+@import './var.scss';
+
 // 生成颜色阶梯的函数
 @function generate-color-scale($color) {
   $scale: ();
@@ -29,7 +32,7 @@
 
 @each $name, $color in $themes {
   $color-scale: generate-color-scale($color);
-  $light-color: map-get($themes, $name + '-light');
+  $light-color: map-get($theme-lights, $name);
   .theme-#{$name} {
     // 生成所有颜色阶梯的CSS变量
     @each $step, $value in $color-scale {

+ 10 - 2
src/static/theme/var.scss

@@ -1,5 +1,13 @@
 $ie: #31A0FC;
-$ie-light: #E5F1ED;
+$ie-light: #EBF9FF;
 
-$themes: map-merge((), ('ie': $ie));
+// 主题主色映射
+$themes: (
+  'ie': $ie
+);
+
+// 主题浅色映射
+$theme-lights: (
+  'ie': $ie-light
+);
 

+ 3 - 0
src/store/userStore.ts

@@ -128,6 +128,9 @@ export const useUserStore = defineStore('ie-user', {
     },
     isHN(): boolean {
       return this.getLocation == '湖南'
+    },
+    isVHS(): boolean {
+      return this.getExamType === EnumExamType.VHS;
     }
   },
   actions: {

+ 107 - 3
src/types/study.ts

@@ -185,6 +185,8 @@ export interface ExamineeQuestion {
   subQuestions: ExamineeQuestion[];
   parse?: string;
   totalScore: number;
+  // 原始题型名称
+  typeTitle?: string;
 }
 export interface Examinee {
   examineeId: number;
@@ -296,10 +298,14 @@ export interface Question extends QuestionState {
   duration: number;
   // 是否有子题
   hasSubQuestions: boolean;
+  // 原始题型名称
+  typeTitle?: string;
 }
 
 export interface SubjectListRequestDTO {
-  directed: boolean;
+  directed?: boolean;
+  questionType?: number;
+  subjectType?: number;
 }
 
 export interface KnowledgeListRequestDTO {
@@ -310,9 +316,10 @@ export interface KnowledgeListRequestDTO {
 export interface OpenExamineeRequestDTO {
   paperType: string,
   relateId?: number,
-  directed: boolean,
+  directed?: boolean,
   subjectId?: number,
-  testType?: string
+  testType?: string;
+  questionType?: number;
 }
 
 export interface GetExamPaperRequestDTO {
@@ -385,6 +392,8 @@ export interface SimulatedRecord {
   // 
   subjectName: string;
   state: EnumSimulatedRecordStatus;
+  //
+  subjectGroup: string;
 }
 
 /**
@@ -431,6 +440,7 @@ export interface PracticeHistory {
   endTime: string;
   examineeId: number;
   paperName: string;
+  questionType?: number;
 }
 
 /**
@@ -457,6 +467,38 @@ export interface PaperWork {
   batchName: string;
 }
 
+export interface VHSPaperListRequestDTO {
+  subjectId: number;
+}
+
+export interface VHSPaper {
+  collect: boolean;
+  createBy: string | null;
+  createTime: string;
+  directKey: string;
+  examineeId: string | null;
+  examineeTypes: string;
+  fenshu: number;
+  filename: string;
+  id: number;
+  locations: string;
+  number: string | null;
+  ospath: string | null;
+  paperInfo: string | null;
+  paperName: string;
+  paperSource: number;
+  paperType: string;
+  relateId: number;
+  remark: string | null;
+  status: number;
+  subjectId: number;
+  subjectName: string;
+  tiid: string;
+  updateBy: string | null;
+  updateTime: string | null;
+  year: string | null;
+}
+
 
 /**
  * 收藏题目列表请求参数
@@ -608,3 +650,65 @@ export interface WrongBookQuestion {
   options: string[];
   answers: string[];
 }
+
+/**
+ * 视频课程科目请求参数
+ */
+export interface VideoCourseSubjectRequestDTO {
+  pageNum: number;
+  pageSize: number;
+  type: number;
+}
+/**
+ * 视频课程科目
+ */
+export interface VideoCourseSubject {
+  code: number;
+  label: string;
+}
+/**
+ * 视频课程知识点
+ */
+export interface VideoCourseKnowledge {
+  code: number;
+  label: string;
+  children: VideoCourse[];
+}
+/**
+ * 视频课程请求参数
+ */
+export interface VideoCourseRequestDTO {
+  pageNum: number;
+  pageSize: number;
+  subject: number;
+  knowledge: number;
+}
+/**
+ * 视频课程
+ */
+export interface VideoCourse {
+  aliId: string;
+  aliIdType: number;
+  img: string;
+  name: string;
+}
+/**
+ * 视频课程播放信息
+ */
+export interface VideoCoursePlayInfo {
+  coverUrl: string;
+  duration: string;
+  palyUrl: string;
+  title: string;
+  videoId: string;
+}
+
+/**
+ * 视频课程学习记录
+ */
+export interface VideoCourseRecordDTO {
+  sectionId: string;
+  duration: number;
+  percent: string;
+  type: number;
+}

+ 6 - 9
src/types/transfer.ts

@@ -10,6 +10,8 @@ export interface PracticeResultPageOptions {
   name: string;
   directed: boolean;
   paperType: EnumPaperType;
+  isVHS?: boolean; // 是否是对口升学
+  questionType?: number; // 对口升学用来区分是知识点还是必刷题
 }
 
 /**
@@ -18,12 +20,13 @@ export interface PracticeResultPageOptions {
  */
 export interface ExamAnalysisPageOptions {
   paperType: EnumPaperType;
-  name: string;
+  // name: string;
   questionId?: number;
-  readonly?: boolean;
+  readonly: boolean;
   // 模拟考试
   simulationInfo?: {
     examineeId: number;
+    name: string;
   };
   // 知识点练习、教材同步练、组卷作业
   practiceInfo?: {
@@ -31,6 +34,7 @@ export interface ExamAnalysisPageOptions {
     relateId: number;
     directed: boolean; // 知识点 id
     examineeId?: number;
+    questionType?: number; // 对口升学用来区分是知识点还是必刷题
   };
 }
 
@@ -40,11 +44,4 @@ export interface ExamAnalysisPageOptions {
 export interface SimulationAnalysisPageOptions {
   examineeId: number;
   paperType: EnumPaperType;
-}
-
-export interface UniversityPickerPageOptions {
-  title?: string;
-  fromVoluntary?: boolean;
-  selectedUniversityId?: string|number;
-  selectedMajorId?: string|number;
 }

+ 2 - 2
src/uni_modules/mp-html/components/mp-html/node/node.vue

@@ -70,10 +70,10 @@
       
       <!-- 富文本 -->
       <!-- #ifdef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
-      <rich-text v-else-if="!n.c&&(n.l||!handler.isInline(n.name, n.attrs.style))" :id="n.attrs.id" :style="n.f" :user-select="opts[4]" :nodes="[n]" />
+      <rich-text v-else-if="!n.c&&(n.l||!handler.isInline(n.name, n.attrs.style))" style="display: inline-block;" :id="n.attrs.id" :style="n.f" :user-select="opts[4]" :nodes="[n]" />
       <!-- #endif -->
       <!-- #ifndef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
-      <rich-text v-else-if="!n.c" :id="n.attrs.id" :style="'display:inline;'+n.f" :preview="false" :selectable="opts[4]" :user-select="opts[4]" :nodes="[n]" />
+      <rich-text v-else-if="!n.c" :id="n.attrs.id" :style="'display:inline;'+n.f"  style="display: inline-block;" :preview="false" :selectable="opts[4]" :user-select="opts[4]" :nodes="[n]" />
       <!-- #endif -->
       <!-- 继续递归 -->
       <view v-else-if="n.c===2" :id="n.attrs.id" :class="'_block _'+n.name+' '+n.attrs.class" :style="n.f+';'+n.attrs.style">

+ 19 - 3
src/uni_modules/uv-collapse/components/uv-collapse-item/uv-collapse-item.vue

@@ -8,14 +8,14 @@
 			:isLink="isLink"
 			:clickable="clickable"
 			:border="false"
-			@click="clickHandler"
+			@click="loadContent"
 			:arrowDirection="expanded ? 'up' : 'down'"
 			:disabled="disabled"
 		>
 			<!-- Vue 3 插槽语法,支持所有平台包括微信小程序 -->
 			<template v-slot:title>
 				<view>
-					<slot name="title" :expanded="expanded"></slot>
+					<slot name="title" :expanded="expanded" :loading="loading"></slot>
 				</view>
 			</template>
 			<template v-slot:icon>
@@ -91,7 +91,8 @@
 				parentData: {
 					accordion: false,
 					border: false
-				}
+				},
+        loading: false,
 			};
 		},
 		watch: {
@@ -195,6 +196,21 @@
 				})
 				// #endif
 			},
+      async loadContent() {
+        if (this.lazy && this.load) {
+          if (!this.inited) {
+            this.loading = true;
+            setTimeout(async () => {
+              await this.load(this.data, this.clickHandler);
+              this.loading = false;
+            }, 300);
+          } else {
+            this.clickHandler();
+          }
+        } else {
+          this.clickHandler();
+        }
+      },
 			// 点击collapsehead头部
 			async clickHandler() {
 				if (this.disabled || this.animating) return

+ 6 - 7
src/uni_modules/uv-tabs/components/uv-tabs/uv-tabs.vue

@@ -6,7 +6,7 @@
 				<scroll-view
 					:scroll-x="scrollable"
 					:scroll-left="scrollLeft"
-					scroll-with-animation
+					:scroll-with-animation="animationEnabled"
 					class="uv-tabs__wrapper__scroll-view"
 					:show-scrollbar="false"
 					ref="uv-tabs__wrapper__scroll-view"
@@ -266,8 +266,9 @@
 				// 此处为屏幕宽度
 				const windowWidth = this.$uv.sys().windowWidth
 				// 将活动的tabs-item移动到屏幕正中间,实际上是对scroll-view的移动
-				let scrollLeft = offsetLeft - (this.tabsRect.width - tabRect.rect.width) / 2 - (windowWidth - this.tabsRect
-					.right) / 2 + this.tabsRect.left / 2
+				// let scrollLeft = offsetLeft - (this.tabsRect.width - tabRect.rect.width) / 2 - (windowWidth - this.tabsRect
+				// 	.right) / 2 + this.tabsRect.left / 2
+        let scrollLeft = offsetLeft + tabRect.rect.width / 2 - this.tabsRect.width / 2;
 				// 这里做一个限制,限制scrollLeft的最大值为整个scroll-view宽度减去tabs组件的宽度
 				scrollLeft = Math.min(scrollLeft, this.scrollViewWidth - this.tabsRect.width)
 				this.scrollLeft = Math.max(0, scrollLeft)
@@ -289,9 +290,7 @@
 					})
 					// 获取了tabs的尺寸之后,设置滑块的位置
 					this.setLineLeft()
-					if(this.innerCurrent !== 0 || this.innerCurrent === 0 && !this.firstTime) {
-						this.setScrollLeft()
-					}
+					this.setScrollLeft()
 				})
 			},
 			// 获取导航菜单的尺寸
@@ -393,4 +392,4 @@
 			}
 		}
 	}
-</style>
+</style>

+ 3 - 1
src/uni_modules/uv-tags/components/uv-tags/uv-tags.vue

@@ -7,7 +7,7 @@
         <view class="uv-tags-wrapper">
             <view
                 class="uv-tags"
-                :class="[`uv-tags--${shape}`, !plain && `uv-tags--${type}`, plain && `uv-tags--${type}--plain`, `uv-tags--${size}`,`uv-tags--${size}--${closePlace}`, plain && plainFill && `uv-tags--${type}--plain--fill`]"
+                :class="[`uv-tags--${shape}`, !plain && `uv-tags--${type}`, plain && `uv-tags--${type}--plain`, `uv-tags--${size}`,`uv-tags--${size}--${closePlace}`, plain && plainFill && `uv-tags--${type}--plain--fill`, customClass]"
                 @tap.stop="clickHandler"
                 :style="[{
 					marginRight: closable&& closePlace=='right-top' ? '10px' : 0,
@@ -222,6 +222,8 @@ export default {
     }
 
     &__text {
+      flex: 1;
+      text-align: center;;
         &--tiny {
             font-size: 10px;
             line-height: 10px;