Просмотр исходного кода

Merge branch 'master' of http://49.234.186.218:9000/root/ieplus-app

jinxia.mo 1 месяц назад
Родитель
Сommit
fc6fd50851
100 измененных файлов с 3292 добавлено и 1419 удалено
  1. 5 1
      package.json
  2. 0 1
      src/api/flyio.ts
  3. 112 4
      src/api/modules/study.ts
  4. 6 2
      src/api/modules/system.ts
  5. 87 2
      src/common/enum.ts
  6. 1 1
      src/components/ie-image/ie-image.vue
  7. 18 5
      src/components/ie-page/ie-page.vue
  8. 11 6
      src/components/ie-picker/ie-picker.vue
  9. 2 2
      src/components/ie-popup-toolbar/ie-popup-toolbar.vue
  10. 6 2
      src/components/ie-popup/ie-popup.vue
  11. 100 100
      src/components/mx-tabs-swiper/mx-tabs-swiper.vue
  12. 40 18
      src/composables/useCalendar.ts
  13. 190 76
      src/composables/useExam.ts
  14. 15 7
      src/composables/useExamType.ts
  15. 5 1
      src/config.ts
  16. 46 0
      src/hooks/useAuth.ts
  17. 1 1
      src/hooks/useSms.ts
  18. 29 0
      src/main.ts
  19. 1 1
      src/manifest.json
  20. 32 2
      src/pages.json
  21. 0 1
      src/pagesMain/pages/index/components/index-banner.vue
  22. 1 1
      src/pagesMain/pages/index/components/index-popup.vue
  23. 14 8
      src/pagesMain/pages/index/index.vue
  24. 53 22
      src/pagesMain/pages/me/components/me-info.vue
  25. 0 7
      src/pagesMain/pages/me/components/me-menu copy.vue
  26. 23 33
      src/pagesMain/pages/me/components/me-menu.vue
  27. 1 1
      src/pagesMain/pages/me/me.vue
  28. 13 3
      src/pagesMain/pages/splash/splash.vue
  29. 93 87
      src/pagesOther/pages/personal-center/setting/setting.vue
  30. 6 4
      src/pagesOther/pages/video-center/index/components/video-page-layout.vue
  31. 4 3
      src/pagesStudy/components/exam-record-item.vue
  32. 45 41
      src/pagesStudy/components/knowledge-tree-node.vue
  33. 6 15
      src/pagesStudy/components/knowledge-tree.vue
  34. 104 0
      src/pagesStudy/components/paper-work-item.vue
  35. 7 8
      src/pagesStudy/components/practice-table.vue
  36. 10 11
      src/pagesStudy/components/teacher-class-view.vue
  37. 2 8
      src/pagesStudy/components/video-table.vue
  38. 12 3
      src/pagesStudy/pages/exam-start/components/exam-navbar.vue
  39. 1 1
      src/pagesStudy/pages/exam-start/components/exam-stats-card.vue
  40. 14 12
      src/pagesStudy/pages/exam-start/components/question-item.vue
  41. 23 17
      src/pagesStudy/pages/exam-start/components/question-options.vue
  42. 3 3
      src/pagesStudy/pages/exam-start/components/question-parse.vue
  43. 4 4
      src/pagesStudy/pages/exam-start/components/question-title.vue
  44. 28 18
      src/pagesStudy/pages/exam-start/exam-start.vue
  45. 52 23
      src/pagesStudy/pages/homework/homework.vue
  46. 8 1
      src/pagesStudy/pages/index/compoentns/index-banner.vue
  47. 20 4
      src/pagesStudy/pages/index/compoentns/index-test.vue
  48. 19 15
      src/pagesStudy/pages/index/index.vue
  49. 4 3
      src/pagesStudy/pages/knowledge-practice-detail/knowledge-practice-detail.vue
  50. 35 27
      src/pagesStudy/pages/knowledge-practice-history/knowledge-practice-history.vue
  51. 33 21
      src/pagesStudy/pages/knowledge-practice/knowledge-practice.vue
  52. 6 1
      src/pagesStudy/pages/simulation-analysis/components/exam-stat.vue
  53. 15 4
      src/pagesStudy/pages/simulation-analysis/simulation-analysis.vue
  54. 6 0
      src/pagesStudy/pages/simulation-entry/simulation-entry.vue
  55. 1 1
      src/pagesStudy/pages/simulation-start/simulation-start.vue
  56. 84 0
      src/pagesStudy/pages/study-exam-simulated-class/study-exam-simulated-class.vue
  57. 84 0
      src/pagesStudy/pages/study-exam-simulated-detail/study-exam-simulated-detail.vue
  58. 85 0
      src/pagesStudy/pages/study-exam-simulated-student/study-exam-simulated-student.vue
  59. 252 0
      src/pagesStudy/pages/study-history/components/exam-history-paperwork.vue
  60. 74 0
      src/pagesStudy/pages/study-history/components/exam-history-simulated.vue
  61. 20 11
      src/pagesStudy/pages/study-history/components/exam-history-student.vue
  62. 4 166
      src/pagesStudy/pages/study-history/components/exam-history-teacher.vue
  63. 1 1
      src/pagesStudy/pages/study-history/components/knowledge-history-student.vue
  64. 21 29
      src/pagesStudy/pages/study-history/components/knowledge-history-teacher.vue
  65. 1 1
      src/pagesStudy/pages/study-history/components/knowledge-history.vue
  66. 1 4
      src/pagesStudy/pages/study-history/components/practice-history-student.vue
  67. 25 29
      src/pagesStudy/pages/study-history/components/practice-history-teacher.vue
  68. 8 8
      src/pagesStudy/pages/study-history/components/practice-history.vue
  69. 12 8
      src/pagesStudy/pages/study-history/components/student-stat-table.vue
  70. 1 1
      src/pagesStudy/pages/study-history/components/video-history-student.vue
  71. 44 14
      src/pagesStudy/pages/study-history/components/video-history-teacher.vue
  72. 6 6
      src/pagesStudy/pages/study-history/components/video-history.vue
  73. 24 3
      src/pagesStudy/pages/study-knowledge-detail/study-knowledge-detail.vue
  74. 2 2
      src/pagesStudy/pages/study-practice-detail/study-practice-detail.vue
  75. 20 2
      src/pagesStudy/pages/study-video-detail/study-video-detail.vue
  76. 64 0
      src/pagesStudy/pages/textbooks-practice-history/textbooks-practice-history.vue
  77. 86 0
      src/pagesStudy/pages/textbooks-practice/textbooks-practice.vue
  78. BIN
      src/pagesStudy/static/image/icon-physics.png
  79. BIN
      src/pagesStudy/static/image/icon-politics.png
  80. 243 131
      src/pagesSystem/pages/bind-profile/bind-profile copy.vue
  81. 146 131
      src/pagesSystem/pages/bind-profile/bind-profile.vue
  82. 282 0
      src/pagesSystem/pages/bind-teacher-profile/bind-teacher-profile.vue
  83. 65 53
      src/pagesSystem/pages/edit-profile/edit-profile.vue
  84. 0 80
      src/pagesSystem/pages/edit-teacher-profile/edit-teacher-profile.vue
  85. 34 9
      src/pagesSystem/pages/login/login.vue
  86. 11 37
      src/pagesSystem/pages/phone-verify/phone-verify.vue
  87. BIN
      src/static/personal/avatar_default.png
  88. BIN
      src/static/personal/bg-vip-card.png
  89. BIN
      src/static/personal/icon-role.png
  90. BIN
      src/static/personal/icon-vip.png
  91. BIN
      src/static/personal/icon-vip2.png
  92. 8 0
      src/static/style/tailwind.scss
  93. 1 1
      src/static/theme/var.scss
  94. 1 8
      src/store/appStore.ts
  95. 52 9
      src/store/userStore.ts
  96. 17 5
      src/types/index.ts
  97. 115 23
      src/types/study.ts
  98. 6 0
      src/types/system.ts
  99. 4 0
      src/types/transfer.ts
  100. 15 4
      src/types/user.ts

+ 5 - 1
package.json

@@ -39,10 +39,13 @@
     "@dcloudio/uni-components": "3.0.0-4070620250821001",
     "@dcloudio/uni-h5": "3.0.0-4070620250821001",
     "@dcloudio/uni-mp-weixin": "3.0.0-4070620250821001",
+    "@rojer/katex-mini": "^1.3.4",
     "@vueuse/core": "^11.2.0",
     "echarts": "^6.0.0",
     "flyio": "^0.6.14",
+    "katex": "^0.16.25",
     "lodash": "^4.17.21",
+    "mp-html": "^2.5.1",
     "pinia": "2",
     "pinia-plugin-persistedstate": "^4.3.0",
     "uqrcodejs": "^4.0.7",
@@ -68,5 +71,6 @@
     "vite": "5.2.8",
     "vite-plugin-compression": "^0.5.1",
     "vite-plugin-uni-polyfill": "^0.1.0"
-  }
+  },
+  "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
 }

+ 0 - 1
src/api/flyio.ts

@@ -3,7 +3,6 @@ import Fly from "flyio/dist/npm/wx"
 import config from "@/config";
 import { useUserStore } from '@/store/userStore';
 const { serverBaseUrl } = config;
-console.log(serverBaseUrl)
 const requestConfig = {
   baseURL: serverBaseUrl,
   timeout: 30000,

+ 112 - 4
src/api/modules/study.ts

@@ -1,6 +1,7 @@
 import { ApiResponse, ApiResponseList } from "@/types";
 import flyio from "../flyio";
-import { DirectedSchool, Examinee, ExamPaper, ExamPaperSubmit, GetExamPaperRequestDTO, Knowledge, KnowledgeListRequestDTO, KnowledgeRecord, OpenExamineeRequestDTO, PracticeHistory, PracticeRecord, SimulatedRecord, SimulationExamSubject, SimulationTestInfo, StudyPlan, Subject, SubjectListRequestDTO, VideoStudyRecord } from "@/types/study";
+import { Batch, ClassKnowledgeRecord, DirectedSchool, Examinee, ExamPaper, ExamPaperSubmit, GetExamPaperRequestDTO, Knowledge, KnowledgeListRequestDTO, KnowledgeRecord, OpenExamineeRequestDTO, PaperWork, PaperWorkRecord, PaperWorkRecordDetail, PaperWorkRecordQuery, PracticeHistory, PracticeRecord, SimulatedRecord, SimulationExamSubject, SimulationTestInfo, StudentExamRecord, StudentPlanStudyRecord, StudentVideoRecord, StudyPlan, Subject, SubjectListRequestDTO, TeachClass, VideoStudy } from "@/types/study";
+import { EnumPaperWorkState } from "@/common/enum";
 
 /**
  * 获取学习计划
@@ -68,6 +69,15 @@ export function getKnowledgeList(params: KnowledgeListRequestDTO) {
   return flyio.get('/front/paper/knowledge', params) as Promise<ApiResponse<Knowledge[]>>;
 }
 
+/**
+ * 获取教材同步知识点
+ * @param params 
+ * @returns 
+ */
+export function getTextbooksKnowledgeList() {
+  return flyio.get('/front/paper/courseKnowledge', {}) as Promise<ApiResponse<Knowledge[]>>;
+}
+
 /**
  * 开卷
  * @param params 
@@ -166,7 +176,7 @@ export function getPlanStudyRecord(params: { year: number, month: number }) {
  * @returns 
  */
 export function getVideoStudyRecord() {
-  return flyio.get('/front/student/record/video') as Promise<ApiResponse<VideoStudyRecord>>;
+  return flyio.get('/front/student/record/video') as Promise<ApiResponse<VideoStudy>>;
 }
 
 
@@ -203,6 +213,104 @@ export function correctQuestion(params: { questionid: number, remark: string })
  * @param params 
  * @returns 
  */
-export function getPracticeHistory(params: { directed: boolean }) {
-  return flyio.get('/front/student/record/practice', params) as Promise<ApiResponseList<PracticeHistory>>;
+export function getPracticeHistory({pageNum, pageSize}: {pageNum: number, pageSize: number}) {
+  return flyio.get('/front/student/record/practice', {
+    pageNum,
+    pageSize
+  }) as Promise<ApiResponseList<PracticeHistory>>;
+}
+
+/**
+ * 获取教材同步练习记录
+ * @param params 
+ * @returns 
+ */
+export function getTextbooksPracticeHistory() {
+  return flyio.get('/front/student/record/coursePractice', {}) as Promise<ApiResponseList<PracticeHistory>>;
+}
+
+export function getPaperWorkList(parmas: {state?: EnumPaperWorkState}) {
+  return flyio.get('/front/student/record/test', parmas) as Promise<ApiResponseList<PaperWork>>;
+}
+
+export function getPaperWorkDetail(id: number) {
+  return flyio.get(`/front/student/record/test/${id}`) as Promise<ApiResponse<Examinee>>;
+}
+
+export function getPaperWorkStatistic() {
+  return flyio.get('/front/student/paperStats', {}) as Promise<ApiResponse<Record<string, number>[]>>;
+}
+
+// 老师端 API
+/**
+ * 获取教学班级列表
+ */
+export function getTeachClassList(params: any) {
+  return flyio.get('/front/teacher/classes', params) as Promise<ApiResponse<TeachClass[]>>;
+}
+
+export function getClassKnowledgeRecord(params: any) {
+  return flyio.get('/front/teacher/record/knowledge', params) as Promise<ApiResponse<ClassKnowledgeRecord>>;
+}
+
+export function getStudentKnowledgeRecord(params: any) {
+  return flyio.get(`/front/teacher/record/knowledge/${params.recordId}`, params) as Promise<ApiResponseList<KnowledgeRecord>>;
+}
+
+export function getClassPlanStudyRecord(params: any) {
+  return flyio.get('/front/teacher/record/planStudy', params) as Promise<ApiResponseList<StudentPlanStudyRecord>>;
+}
+
+export function getStudentPracticeRecord(params: any) {
+  return flyio.get(`/front/teacher/record/planStudy/${params.recordId}`, params) as Promise<ApiResponse<PracticeRecord>>;
+}
+
+export function getClassVideoStudyRecord(params: any) {
+  return flyio.get('/front/teacher/record/video', params) as Promise<ApiResponseList<StudentVideoRecord>>;
+}
+
+export function getStudentVideoStudyRecord(params: any) {
+  return flyio.get(`/front/teacher/record/video/${params.recordId}`, params) as Promise<ApiResponse<VideoStudy>>;
+}
+
+export function getClassExamRecord(params: any) {
+  return flyio.get('/front/teacher/record/simulated', params) as Promise<ApiResponse<StudentExamRecord[]>>;
+}
+
+export function getStudentExamRecord(params: any) {
+  return flyio.get(`/front/teacher/record/simulated/${params.classId}`, params) as Promise<ApiResponse<StudentExamRecord[]>>;
+}
+
+export function getStudentExamSubjectRecord(params: any) {
+  return flyio.get(`/front/teacher/record/simulated/subject/${params.studentId}`, params) as Promise<ApiResponse<StudentExamRecord[]>>;
+}
+
+export function getStudentExamSubjectDetail(params: any) {
+  return flyio.get('/front/teacher/record/simulated/knowledge', params) as Promise<ApiResponse<StudentExamRecord[]>>;
+}
+
+export function getBatchList(params: any) {
+  return flyio.get('/front/teacher/batchs', params) as Promise<ApiResponse<Batch[]>>;
+}
+
+/**
+ * 获取教师科目列表
+ * @param params 
+ * @returns 
+ */
+export function getTeacherSubjectList(params: any) {
+  return flyio.get('/front/teacher/subjects', params) as Promise<ApiResponse<Subject[]>>;
+}
+
+export function getTeacherTestRecord(params: any) {
+  return flyio.get('/front/teacher/record/test', params) as Promise<ApiResponse<PaperWorkRecord[]>>;
+}
+
+
+export function getTeacherTestRecordDetail(params: any) {
+  return flyio.get('/front/teacher/record/test/detail', params) as Promise<ApiResponse<PaperWorkRecordDetail[]>>;
+}
+
+export function getTeacherTestRecordCondition(params: any) {
+  return flyio.get('/front/teacher/record/test/cond', params) as Promise<ApiResponse<PaperWorkRecordQuery>>;
 }

+ 6 - 2
src/api/modules/system.ts

@@ -1,5 +1,5 @@
 import flyio from "../flyio";
-import { ApiCaptchaResponse, ApiResponse, DictItem, ConfigItem } from "@/types";
+import { ApiCaptchaResponse, ApiResponse, DictItem, ConfigItem, System, ApiResponseList } from "@/types";
 import { SmsRequestDTO } from "@/types/user";
 
 /**
@@ -54,7 +54,11 @@ export function sendSms(params: SmsRequestDTO) {
  * @returns 
  */
 export function getProvinces() {
-  return flyio.get('/front/user/provinces') as Promise<ApiResponse<DictItem[]>>;
+  // return flyio.get('/front/user/provinces') as Promise<ApiResponse<DictItem[]>>;
+  const params = {
+    columns: 'id,full_name,parent_id,has_children'
+  }
+  return flyio.get('/system/area/list/tree', { params }) as Promise<ApiResponseList<System.ProvinceItem>>;
 }
 
 /**

+ 87 - 2
src/common/enum.ts

@@ -184,7 +184,11 @@ export enum EnumBindScene {
   /**
    * 登录绑定
    */
-  LOGIN_BIND = 'login_bind'
+  LOGIN_BIND = 'login_bind',
+  /**
+   * 老师或者代理商完善信息
+   */
+  IMPROVE = 'improve'
 }
 
 export enum EnumExamType {
@@ -241,7 +245,15 @@ export enum EnumPaperType {
   /**
    * 考试
    */
-  SIMULATED = 'Simulated'
+  SIMULATED = 'Simulated',
+  /**
+   * 教材同步练习
+   */
+  COURSE = 'Course',
+  /**
+   * 测试卷
+   */
+  TEST = 'Test'
 }
 
 export enum EnumReviewMode {
@@ -253,4 +265,77 @@ export enum EnumReviewMode {
    * 答完一题就评卷
    */
   DURING_ANSWER = 2
+}
+
+export enum EnumUserRole {
+  /**
+   * 普通用户
+   */
+  NORMAL = 'normal',
+  /**
+   * 游客
+   */
+  GUEST = 'guest',
+  /**
+   * 会员
+   */
+  VIP = 'vip',
+  /**
+   * 代理商
+   */
+  AGENT = 'agent',
+  /**
+   * 教师
+   */
+  TEACHER = 'teacher'
+}
+
+export enum EnumEvent {
+  /**
+   * 打开VIP弹窗
+   */
+  OPEN_VIP_POPUP = 'OPEN_VIP_POPUP'
+}
+
+/**
+ * 卡类型
+ */
+export enum CardType {
+  VIP = 1,
+  DEPT = 2,
+  PLATFORM = 6,
+  /**
+   * 体验卡
+   */
+  EXPERIENCE = 9
+}
+
+export enum EnumPaperWorkState {
+  /**
+   * 未完成
+   */
+  NOT_COMPLETED = 2,
+  /**
+   * 已完成
+   */
+  COMPLETED = 4
+}
+
+export enum EnumPaperBuildType {
+  /**
+   * 定向智能
+   */
+  EXACT_INTELLIGENT = 'ExactIntelligent',
+  /**
+   * 全量智能
+   */
+  FULL_INTELLIGENT = 'FullIntelligent',
+  /**
+   * 定向手动
+   */
+  EXACT_HAND = 'ExactHand',
+  /**
+   * 全量手动
+   */
+  FULL_HAND = 'FullHand'
 }

+ 1 - 1
src/components/ie-image/ie-image.vue

@@ -37,7 +37,7 @@ const props = defineProps({
   },
   bgColor: {
     type: String,
-    default: '#F3F5F6'
+    default: ''
   }
 });
 const imageSrc = computed(() => {

+ 18 - 5
src/components/ie-page/ie-page.vue

@@ -17,6 +17,7 @@
 <script lang="ts" setup>
 import { CLOSE_VIP_POPUP, OPEN_VIP_POPUP } from '@/types/injectionSymbols';
 import VipPopup from './components/vip-popup.vue';
+import { EnumEvent } from '@/common/enum';
 defineOptions({
   name: 'ie-page',
   options: {
@@ -66,14 +67,26 @@ defineExpose({
   showVipPopup,
   closeVipPopup
 });
+const addListener = () => {
+  console.log('监听')
+  uni.$on(EnumEvent.OPEN_VIP_POPUP, showVipPopup);
+}
+const removeListener = () => {
+  console.log('取消监听')
+  uni.$off(EnumEvent.OPEN_VIP_POPUP);
+}
 onMounted(() => {
-  // console.log('ie-page mounted, vipPopupRef.value:', vipPopupRef.value);
-  // uni.$on('showVipPopup', showVipPopup);
-  // uni.$on('closeVipPopup', closeVipPopup);
+  addListener();
 });
 onBeforeUnmount(() => {
-  // uni.$off('showVipPopup', showVipPopup);
-  // uni.$off('closeVipPopup', closeVipPopup);
+  removeListener();
+});
+
+onHide(() => {
+  removeListener();
+});
+onUnload(() => {
+  removeListener();
 });
 </script>
 

+ 11 - 6
src/components/ie-picker/ie-picker.vue

@@ -1,13 +1,13 @@
 <template>
   <view class="w-full" @click="handleClick">
-    <view class="flex items-center gap-x-10" :style="customStyle">
-      <view v-if="matchValue || customLabel" class="flex-1 text-fore-title" :style="getValueStyle"
-        :class="{ 'text-[#c0c4cc]': disabled || readonly }">
+    <view class="flex items-center gap-x-6" :style="customStyle">
+      <view v-if="matchValue || customLabel" class="text-fore-title flex-1 min-w-1 ellipsis-1" :style="getValueStyle"
+        :class="{ 'text-[#dce4f6]': disabled || readonly }">
         <slot :label="label">
           {{ label }}
         </slot>
       </view>
-      <view v-else class="flex-1 text-[#c0c4cc]" :style="getPlaceholderStyle">{{ placeholder }}</view>
+      <view v-else class=" text-[#c0c4cc]" :style="getPlaceholderStyle">{{ placeholder }}</view>
       <slot name="right">
         <view v-if="!readonly && showArrow" class="transition-all duration-300">
           <uv-icon :name="icon" size="15" color="#B3B3B3" />
@@ -123,6 +123,7 @@ const init = () => {
     matchValue.value = true;
     return;
   }
+
   if (modelValue.value !== null && modelValue.value !== undefined && modelValue.value !== '') {
     const index = props.list.findIndex(item => item[props.keyValue] == modelValue.value);
     if (index !== -1) {
@@ -132,8 +133,8 @@ const init = () => {
     } else {
       // 如果找不到匹配项,重置状态
       defaultIndex.value = [0];
-      label.value = props.placeholder;
-      matchValue.value = false;
+      label.value = modelValue.value + '';
+      matchValue.value = true;
     }
   } else {
     defaultIndex.value = [0];
@@ -182,6 +183,10 @@ const handleClick = () => {
   if (props.disabled) {
     return;
   }
+  if (!props.list.length) {
+    uni.$ie.showToast('暂无数据');
+    return;
+  }
   init();
   isOpen.value = true;
   pickerRef.value.open();

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

@@ -1,7 +1,7 @@
 <template>
-  <view class="flex items-center justify-between pt-20">
+  <view class="flex items-center justify-center pt-20">
     <view v-if="showCancel" class="px-46 py-20 text-28 text-fore-light" @click="handleCancel">{{ cancelText }}</view>
-    <text class="text-30 text-fore-title font-bold">{{ title }}</text>
+    <text class="text-30 text-fore-title font-bold flex-1 text-center">{{ title }}</text>
     <view v-if="showConfirm" class="px-46 py-20 text-28 text-fore-title" @click="handleConfirm">{{ confirmText }}</view>
   </view>
 </template>

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

@@ -7,8 +7,10 @@
       <!-- #endif -->
       <uv-popup ref="popupRef" :mode="mode" :round="round" popup-class="theme-ie"
         :close-on-click-overlay="closeOnClickOverlay" @close="handleClose">
-        <ie-popup-toolbar :title="title" :cancelText="cancelText" :confirmText="confirmText" :showCancel="showCancel"
-          :showConfirm="showConfirm" @cancel="handleCancel" @confirm="handleConfirm" />
+        <template v-if="showToolbar">
+          <ie-popup-toolbar :title="title" :cancelText="cancelText" :confirmText="confirmText" :showCancel="showCancel"
+            :showConfirm="showConfirm" @cancel="handleCancel" @confirm="handleConfirm" />
+        </template>
         <view class="popup-content">
           <slot></slot>
         </view>
@@ -28,6 +30,7 @@ type Props = {
   mode: 'bottom' | 'center' | 'top';
   round: number;
   closeOnClickOverlay: boolean;
+  showToolbar: boolean;
   showCancel: boolean;
   showConfirm: boolean;
 }
@@ -38,6 +41,7 @@ const props = withDefaults(defineProps<Props>(), {
   mode: 'bottom',
   round: 16,
   closeOnClickOverlay: true,
+  showToolbar: true,
   showCancel: true,
   showConfirm: true
 });

+ 100 - 100
src/components/mx-tabs-swiper/mx-tabs-swiper.vue

@@ -1,65 +1,66 @@
 <template>
-    <!-- NOTE:min-h-1在使用useElementSize时非常重要 -->
-    <view ref="container" class="h-full fx-col flex-1 min-h-1">
-        <uv-tabs :current="current" :list="tabs" :key-name="keyName" v-bind="tabBindings"
-                 class="bg-white bd-b-1" @change="handleTabChange">
-            <template v-if="$slots.tab" #default="scope">
-                <slot name="tab" v-bind="scope"/>
-            </template>
-        </uv-tabs>
-        <uv-line v-if="border"/>
-        <view>
-          <slot name="header"></slot>
-        </view>
-        <view class="flex-1 min-h-1">
-          <!-- :style="{height: swiperHeight+'px'}" -->
-          <swiper :current="current" class="h-full" v-bind="swiperBindings"
-                @change="handleSwiperChange">
-            <swiper-item v-for="t in tabs" :key="t.name">
-                <!-- 延迟渲染 -->
-                <!-- 如果配置相同的template可共享卡槽 -->
-                <!-- 内容如果要支持滚动,请使用css class - .tabs-swiper-content -->
-                <keep-alive v-if="lazy">
-                    <slot v-if="t.show" :name="t.template||template||t.name" v-bind="t"/>
-                </keep-alive>
-                <slot v-else :name="t.template||template||t.name" v-bind="t"/>
-            </swiper-item>
+  <!-- NOTE:min-h-1在使用useElementSize时非常重要 -->
+  <view ref="container" class="h-full fx-col flex-1 min-h-1">
+    <uv-tabs :current="current" :list="tabs" :key-name="keyName" v-bind="tabBindings" class="bg-white bd-b-1"
+      @change="handleTabChange">
+      <template v-if="$slots.tab" #default="scope">
+        <slot name="tab" v-bind="scope" />
+      </template>
+    </uv-tabs>
+    <uv-line v-if="border" />
+    <view>
+      <slot name="header"></slot>
+    </view>
+    <view class="flex-1 min-h-1 relative">
+      <!-- :style="{height: swiperHeight+'px'}" -->
+      <view class="absolute inset-0">
+        <swiper :current="current" class="h-full" v-bind="swiperBindings" @change="handleSwiperChange">
+          <swiper-item v-for="t in tabs" :key="t.name">
+            <!-- 延迟渲染 -->
+            <!-- 如果配置相同的template可共享卡槽 -->
+            <!-- 内容如果要支持滚动,请使用css class - .tabs-swiper-content -->
+            <keep-alive v-if="lazy">
+              <slot v-if="t.show" :name="t.template || template || t.name" v-bind="t" />
+            </keep-alive>
+            <slot v-else :name="t.template || template || t.name" v-bind="t" />
+          </swiper-item>
         </swiper>
-        </view>
+      </view>
     </view>
+  </view>
 </template>
 
 <script setup>
-import {ref, computed, watch} from 'vue'
+import { ref, computed, watch } from 'vue'
 import _ from "lodash";
-import {createPropDefine} from "@/utils";
-import {useElementSize} from "@vueuse/core";
+import { createPropDefine } from "@/utils";
+import { useElementSize } from "@vueuse/core";
 
 const props = defineProps({
-    // support synchronization
-    modelValue: createPropDefine(0, Number),
-    tabs: createPropDefine([], Array),
-    keyName: createPropDefine('name'),
-    // other options of uv-tabs
-    tabOptions: createPropDefine(null, Object),
-    // other uni-app swiper options
-    swiperOptions: createPropDefine(null, Object),
-    // support lazy rendering, default is true
-    lazy: createPropDefine(true, Boolean),
-    // whether cache all tabs? if not use cacheSize, higher priority than cacheSize
-    cacheAll: createPropDefine(false, Boolean),
-    // cached accessed tab. <=0 means cache all.
-    cacheSize: createPropDefine(5, Number),
-    // tab content will render after delay time, recommended for complex dynamic content.
-    // related to lazy, default is true
-    delay: createPropDefine(true, Boolean),
-    preload: createPropDefine(true, Boolean), // 一般情况下preload会非常平滑,但有大请求时可以禁用
-    // bigger than the animation time。 default 400ms.
-    delayTime: createPropDefine(400, Number),
-    border: createPropDefine(false, Boolean),
-    tabsHeight: createPropDefine(44, Number),
-    // 统一指定模板,优先级低于tab.template,因为有时候让tab指定template会破坏原数据结构
-    template: createPropDefine('')
+  // support synchronization
+  modelValue: createPropDefine(0, Number),
+  tabs: createPropDefine([], Array),
+  keyName: createPropDefine('name'),
+  // other options of uv-tabs
+  tabOptions: createPropDefine(null, Object),
+  // other uni-app swiper options
+  swiperOptions: createPropDefine(null, Object),
+  // support lazy rendering, default is true
+  lazy: createPropDefine(true, Boolean),
+  // whether cache all tabs? if not use cacheSize, higher priority than cacheSize
+  cacheAll: createPropDefine(false, Boolean),
+  // cached accessed tab. <=0 means cache all.
+  cacheSize: createPropDefine(5, Number),
+  // tab content will render after delay time, recommended for complex dynamic content.
+  // related to lazy, default is true
+  delay: createPropDefine(true, Boolean),
+  preload: createPropDefine(true, Boolean), // 一般情况下preload会非常平滑,但有大请求时可以禁用
+  // bigger than the animation time。 default 400ms.
+  delayTime: createPropDefine(400, Number),
+  border: createPropDefine(false, Boolean),
+  tabsHeight: createPropDefine(44, Number),
+  // 统一指定模板,优先级低于tab.template,因为有时候让tab指定template会破坏原数据结构
+  template: createPropDefine('')
 })
 const emits = defineEmits(['update:modelValue', 'change'])
 
@@ -67,69 +68,68 @@ const emits = defineEmits(['update:modelValue', 'change'])
 const current = ref(0)
 const cachedTabs = ref([])
 const container = ref(null)
-const {height} = useElementSize(container)
+const { height } = useElementSize(container)
 const swiperHeight = computed(() => height.value - props.tabsHeight - (props.border ? 1 : 0))
 
 const tabBindings = computed(() => {
-    // make some default options for u-tabs here
-    return {
-        scrollable: props.tabs.length > 4,
-        itemStyle: {height: props.tabsHeight + 'px'},
-        ...props.tabOptions
-    }
+  // make some default options for u-tabs here
+  return {
+    scrollable: props.tabs.length > 4,
+    itemStyle: { height: props.tabsHeight + 'px' },
+    ...props.tabOptions
+  }
 })
 const swiperBindings = computed(() => {
-    // make some default options for uni-app swiper here
-    return {
-        ...props.swiperOptions
-    }
+  // make some default options for uni-app swiper here
+  return {
+    ...props.swiperOptions
+  }
 })
 
 // 保持localCurrent与props.current同步,即props优先级更高
-watch(() => props.modelValue, (val) => current.value = val, {immediate: true})
+watch(() => props.modelValue, (val) => current.value = val, { immediate: true })
 
 watch([current, () => props.tabs], ([current]) => {
-    const {cacheAll, cacheSize, tabs, delay, preload, delayTime} = props
-    if (!tabs.length) return // no data, wait for tabs ready.
+  const { cacheAll, cacheSize, tabs, delay, preload, delayTime } = props
+  if (!tabs.length) return // no data, wait for tabs ready.
 
-    // delay control, first tab must render immediately.
-    const effectDelay = cachedTabs.value.length && delay ? delayTime : 0
+  // delay control, first tab must render immediately.
+  const effectDelay = cachedTabs.value.length && delay ? delayTime : 0
 
-    // cache tabs, keep accessed sequence.
-    const next = [current]
-    if (preload) {
-        // keep current -1 +1 in cache list while preload=true.
-        // this will make swiper action much-much smooth.
-        if (current + 1 < tabs.length) next.unshift(current + 1)
-        if (current - 1 >= 0) next.unshift(current - 1)
-    }
-    _.pull(cachedTabs.value, ...next)
-    cachedTabs.value.push(...next)
-    while (!cacheAll && cacheSize && cachedTabs.value.length > cacheSize) {
-        cachedTabs.value.shift()
-    }
+  // cache tabs, keep accessed sequence.
+  const next = [current]
+  if (preload) {
+    // keep current -1 +1 in cache list while preload=true.
+    // this will make swiper action much-much smooth.
+    if (current + 1 < tabs.length) next.unshift(current + 1)
+    if (current - 1 >= 0) next.unshift(current - 1)
+  }
+  _.pull(cachedTabs.value, ...next)
+  cachedTabs.value.push(...next)
+  while (!cacheAll && cacheSize && cachedTabs.value.length > cacheSize) {
+    cachedTabs.value.shift()
+  }
 
-    // reset show property of all tabs
-    if (effectDelay) {
-        setTimeout(() => {
-            tabs.forEach((t, i) => t.show = cachedTabs.value.includes(i))
-        }, effectDelay)
-    } else {
-        tabs.forEach((t, i) => t.show = cachedTabs.value.includes(i))
-    }
-}, {immediate: true})
+  // reset show property of all tabs
+  if (effectDelay) {
+    setTimeout(() => {
+      tabs.forEach((t, i) => t.show = cachedTabs.value.includes(i))
+    }, effectDelay)
+  } else {
+    tabs.forEach((t, i) => t.show = cachedTabs.value.includes(i))
+  }
+}, { immediate: true })
 
-const handleTabChange = function ({index}) {
-    current.value = index
-    emits('update:modelValue', current.value)
-    emits('change', current.value)
+const handleTabChange = function ({ index }) {
+  current.value = index
+  emits('update:modelValue', current.value)
+  emits('change', current.value)
 }
 const handleSwiperChange = function (e) {
-    current.value = e.detail.current
-    emits('update:modelValue', current.value)
-    emits('change', current.value)
+  current.value = e.detail.current
+  emits('update:modelValue', current.value)
+  emits('change', current.value)
 }
 </script>
 
-<style>
-</style>
+<style></style>

+ 40 - 18
src/composables/useCalendar.ts

@@ -1,7 +1,9 @@
 import { ref, computed, watch } from 'vue';
-import { getPlanStudyRecord } from '@/api/modules/study';
+import { getPlanStudyRecord, getStudentPracticeRecord } from '@/api/modules/study';
+import { useUserStore } from '@/store/userStore';
 // @ts-ignore
 import CalendarUtil from '@/uni_modules/uni-calendar/components/uni-calendar/util.js';
+import { PracticeRecord } from '@/types/study';
 
 export interface PracticeStatistics {
   list: PracticeData[],
@@ -18,7 +20,8 @@ export interface PracticeData {
 
 export function useCalendar() {
   const calendarUtil = new CalendarUtil();
-  const globalStudentId = ref(0);
+  const { isStudent } = storeToRefs(useUserStore());
+  const globalRecordId = ref(0);
   const selected = ref<PracticeData[]>([]);
   const statistics = ref<PracticeStatistics>({
     list: [],
@@ -73,7 +76,6 @@ export function useCalendar() {
   // 4. 统计数据会从返回的练习数据中自动计算,无需额外处理
   // ========================================================
   const fetchPracticeData = async (year: number, month: number): Promise<PracticeStatistics> => {
-    console.log('请求数据', year, month, globalStudentId.value)
     let practiceData: PracticeStatistics = {
       list: [],
       rate: 0,
@@ -81,19 +83,37 @@ export function useCalendar() {
       total: 0
     };
 
-    const { data } = await getPlanStudyRecord({
-      year,
-      month
-    });
-    practiceData.list = data.list.map(item => ({
-      date: item.date,
-      info: item.rate ? Number(item.rate) : 0,
-      questionNum: item.study ? Number(item.study) : 0
-    })).sort((a, b) => a.date.localeCompare(b.date));
-    practiceData.rate = data.rate ? Number(data.rate) : 0;
-    practiceData.studyDays = data.studyDays ? Number(data.studyDays) : 0;
-    practiceData.total = data.total ? Number(data.total) : 0;
-    console.log('response', practiceData)
+    let resData: PracticeRecord = {
+      list: [],
+      rate: 0,
+      studyDays: 0,
+      total: 0
+    };
+    if (isStudent.value) {
+      const { data } = await getPlanStudyRecord({
+        year,
+        month
+      });
+      resData = data;
+    } else {
+      // 老师查看学生刷题记录
+      const { data } = await getStudentPracticeRecord({
+        recordId: globalRecordId.value,
+        year,
+        month
+      });
+      resData = data;
+    }
+    if (resData && Array.isArray(resData.list)) {
+      practiceData.list = resData.list.map(item => ({
+        date: item.date,
+        info: item.rate ? Number(item.rate) : 0,
+        questionNum: item.study ? Number(item.study) : 0
+      })).sort((a, b) => a.date.localeCompare(b.date));
+    }
+    practiceData.rate = resData.rate ? Number(resData.rate) : 0;
+    practiceData.studyDays = resData.studyDays ? Number(resData.studyDays) : 0;
+    practiceData.total = resData.total ? Number(resData.total) : 0;
     return practiceData;
   };
 
@@ -196,9 +216,11 @@ export function useCalendar() {
   };
 
   // 初始化数据 - 默认按年份模式初始化
-  const init = async (studentId: number) => {
+  const init = async (recordId?: number) => {
     const today = new Date();
-    globalStudentId.value = studentId;
+    if (recordId) {
+      globalRecordId.value = recordId;
+    }
     await updateCalendarData(today.getFullYear(), 0);
   };
 

+ 190 - 76
src/composables/useExam.ts

@@ -3,6 +3,85 @@ import { getPaper } from '@/api/modules/study';
 import { Study } from "@/types";
 import { Question } from "@/types/study";
 
+/**
+ * @description 解码 HTML 实体
+ * 由于 uv-parse 的 decodeEntity 只支持有限的实体,需要手动解码音标等特殊实体
+ * 使用手动映射表,兼容所有 uni-app 平台(包括小程序)
+ */
+export const decodeHtmlEntities = (str: string): string => {
+  if (!str) return str;
+
+  // 音标和常用 HTML 实体映射表
+  const entityMap: Record<string, string> = {
+    // 音标相关 - 锐音符 (acute)
+    'aacute': 'á',
+    'eacute': 'é',
+    'iacute': 'í',
+    'oacute': 'ó',
+    'uacute': 'ú',
+    // 音标相关 - 重音符 (grave)
+    'agrave': 'à',
+    'egrave': 'è',
+    'igrave': 'ì',
+    'ograve': 'ò',
+    'ugrave': 'ù',
+    // 音标相关 - 扬抑符 (circumflex)
+    'acirc': 'â',
+    'ecirc': 'ê',
+    'icirc': 'î',
+    'ocirc': 'ô',
+    'ucirc': 'û',
+    // 音标相关 - 分音符 (umlaut/diaeresis)
+    'auml': 'ä',
+    'euml': 'ë',
+    'iuml': 'ï',
+    'ouml': 'ö',
+    'uuml': 'ü',
+    // 音标相关 - 波浪符 (tilde)
+    'ntilde': 'ñ',
+    'atilde': 'ã',
+    'otilde': 'õ',
+    // 其他常用实体
+    'amp': '&',
+    'lt': '<',
+    'gt': '>',
+    'quot': '"',
+    'apos': "'",
+    'nbsp': '\u00A0',
+    'copy': '©',
+    'reg': '®',
+    'trade': '™',
+    'mdash': '—',
+    'ndash': '–',
+    'hellip': '…',
+    // 数学符号
+    'times': '×',
+    'divide': '÷',
+    'plusmn': '±',
+  };
+
+  // 处理命名实体(如 &iacute;)
+  // 使用 [a-z0-9] 以支持包含数字的实体名称
+  let result = str.replace(/&([a-z0-9]+);/gi, (match, entity) => {
+    const lowerEntity = entity.toLowerCase();
+    if (entityMap[lowerEntity]) {
+      return entityMap[lowerEntity];
+    }
+    return match; // 如果找不到映射,保持原样
+  });
+
+  // 处理数字实体(如 &#237; 或 &#xED;)
+  result = result.replace(/&#(\d+);/g, (match, num) => {
+    return String.fromCharCode(parseInt(num, 10));
+  });
+
+  result = result.replace(/&#x([0-9a-f]+);/gi, (match, hex) => {
+    return String.fromCharCode(parseInt(hex, 16));
+  });
+
+  return result;
+}
+
 export const useExam = () => {
   const questionTypeDesc: Record<EnumQuestionType, string> = {
     [EnumQuestionType.SINGLE_CHOICE]: '单选题',
@@ -28,7 +107,13 @@ export const useExam = () => {
     EnumQuestionType.OTHER
   ];
   let interval: NodeJS.Timeout | null = null;
+  let animationFrameId: number | null = null; // requestAnimationFrame 的 ID
+  let countStart: number = 0;
+  let countTime: number = 0;
   const countDownCallback = ref<() => void>(() => { });
+  // 练习计时相关变量
+  let practiceStartTime: number = 0; // 练习开始时间戳(毫秒)
+  let practiceAccumulatedTime: number = 0; // 累计的练习时间(秒)
   // 练习时长
   const practiceDuration = ref<number>(0);
   const formatPracticeDuration = computed(() => {
@@ -173,7 +258,8 @@ export const useExam = () => {
     if (subQuestionIndex.value >= currentQuestion.value.subQuestions.length) {
       return null;
     }
-    return currentQuestion.value.subQuestions[subQuestionIndex.value];
+    // return currentQuestion.value.subQuestions[subQuestionIndex.value];
+    return currentQuestion.value.subQuestions[currentQuestion.value.activeSubIndex];
   });
   /// 总题量,不区分子题,等同于接口返回的题目列表
   const totalCount = computed(() => {
@@ -203,6 +289,20 @@ export const useExam = () => {
   const notDoneCount = computed(() => {
     return virtualTotalCount.value - doneCount.value;
   });
+  watch(() => virtualCurrentIndex.value, (newVal, oldVal) => {
+    updateQuestionDuration(oldVal);
+  });
+  // 更新单个题目做题时间
+  const updateQuestionDuration = (index: number, continueCount = true) => {
+    const question = flatQuestionList.value[index];
+    const time = stopCount();
+    question.duration += time;
+    // 每次结算后都清空累计时长,避免多次提交或多次 stop 导致重复累加
+    clearCount();
+    if (continueCount) {
+      startCount();
+    }
+  }
   /// 包含子题的题目计算整体做题进度
   const calcProgress = (qs: Study.Question): number => {
     if (qs.subQuestions && qs.subQuestions.length > 0) {
@@ -256,54 +356,19 @@ export const useExam = () => {
   }
   // 是否可以切换上一题
   const prevEnable = computed(() => {
-    // return currentIndex.value > 0;
-    if (currentQuestion.value) {
-      if (currentQuestion.value.isSubQuestion) {
-        return subQuestionIndex.value > 0;
-      } else {
-        return currentIndex.value > 0;
-      }
-    }
-    return false;
+    return currentIndex.value > 0 || currentQuestion.value.activeSubIndex > 0;
   });
   // 是否可以切换下一题
   const nextEnable = computed(() => {
-    if (currentQuestion.value) {
-      if (currentQuestion.value.isSubQuestion) {
-        console.log(5, subQuestionIndex.value < currentQuestion.value.subQuestions.length - 1)
-        return subQuestionIndex.value < currentQuestion.value.subQuestions.length - 1;
-      } else {
-        if (currentQuestion.value.subQuestions && currentQuestion.value.subQuestions.length > 0) {
-          console.log(subQuestionIndex.value, currentQuestion.value.subQuestions.length - 1)
-          // 子题可以切换
-          return subQuestionIndex.value < currentQuestion.value.subQuestions.length - 1;
-        } else {
-          // 大题可以切换
-          return currentIndex.value < questionList.value.length - 1;
-        }
-      }
-    }
-    console.log(7, currentQuestion.value)
-    return false;
+    return currentIndex.value < questionList.value.length - 1 || currentQuestion.value.activeSubIndex < currentQuestion.value.subQuestions.length - 1;
   });
   // 下一题
   const nextQuestion = () => {
     if (!nextEnable.value) {
       return;
     }
-    // if (currentIndex.value < questionList.value.length - 1) {
-    //   currentIndex.value++;
-    //   setTimeout(() => {
-    //     subQuestionIndex.value = 0;
-    //   }, 300);
-    // }
-    if (currentQuestion.value.subQuestions && currentQuestion.value.subQuestions.length > 0) {
-      if (subQuestionIndex.value < currentQuestion.value.subQuestions.length - 1) {
-        subQuestionIndex.value++;
-      } else {
-        currentIndex.value++;
-        subQuestionIndex.value = 0;
-      }
+    if (currentQuestion.value.activeSubIndex < currentQuestion.value.subQuestions.length - 1) {
+      currentQuestion.value.activeSubIndex++;
     } else {
       currentIndex.value++;
     }
@@ -313,26 +378,15 @@ export const useExam = () => {
     if (!prevEnable.value) {
       return;
     }
-    if (currentIndex.value > 0) {
-      // currentIndex.value--;
-      // setTimeout(() => {
-      //   subQuestionIndex.value = 0;
-      // }, 300);
-    }
     if (currentQuestion.value.subQuestions && currentQuestion.value.subQuestions.length > 0) {
-      if (subQuestionIndex.value > 0) {
-        subQuestionIndex.value--;
+      if (currentQuestion.value.activeSubIndex > 0) {
+        currentQuestion.value.activeSubIndex--;
       } else {
         currentIndex.value--;
       }
     } else {
       if (currentIndex.value > 0) {
         currentIndex.value--;
-        // 如果上一个题是子题,那么,默认选中最后一个子题
-        const prevQuestion = questionList.value[currentIndex.value - 1];
-        if (prevQuestion && prevQuestion.subQuestions && prevQuestion.subQuestions.length > 0) {
-          subQuestionIndex.value = prevQuestion.subQuestions.length - 1;
-        }
       }
     }
   }
@@ -368,26 +422,72 @@ export const useExam = () => {
     setTimeout(() => {
       currentIndex.value = index;
       if (!subIndex !== undefined) {
-        subQuestionIndex.value = subIndex || 0;
+        if (subIndex !== undefined) {
+          questionList.value[index].activeSubIndex = subIndex || 0;
+        }
       }
       setTimeout(() => {
         swiperDuration.value = 300;
       }, 0);
     }, 0);
   }
-  const changeSubIndex = (index: number) => {
-    subQuestionIndex.value = index;
-  }
   // 开始计时
   const startTiming = () => {
-    interval = setInterval(() => {
-      practiceDuration.value += 1;
-    }, 1000);
+    startCount();
+    
+    // 记录开始时间戳(毫秒)
+    if (practiceStartTime === 0) {
+      practiceStartTime = performance.now();
+    }
+    
+    // 使用 requestAnimationFrame 更新显示,更流畅且性能更好
+    const updatePracticeDuration = () => {
+      if (practiceStartTime > 0) {
+        // 计算实际经过的时间(秒)
+        const elapsed = (performance.now() - practiceStartTime) / 1000;
+        practiceDuration.value = Math.floor(practiceAccumulatedTime + elapsed);
+        // 继续下一帧更新
+        animationFrameId = requestAnimationFrame(updatePracticeDuration);
+      }
+    };
+    
+    // 开始动画帧循环
+    animationFrameId = requestAnimationFrame(updatePracticeDuration);
   }
   // 停止计时
   const stopTiming = () => {
-    interval && clearInterval(interval);
-    interval = null;
+    stopCount();
+    
+    // 取消动画帧
+    if (animationFrameId !== null) {
+      cancelAnimationFrame(animationFrameId);
+      animationFrameId = null;
+    }
+    
+    // 如果正在计时,累加经过的时间
+    if (practiceStartTime > 0) {
+      const elapsed = (performance.now() - practiceStartTime) / 1000;
+      practiceAccumulatedTime += elapsed;
+      practiceDuration.value = Math.floor(practiceAccumulatedTime);
+      practiceStartTime = 0;
+    }
+  }
+  const startCount = () => {
+    if (countStart === 0) {
+      countStart = performance.now();
+    }
+  }
+  const stopCount = () => {
+    // 如果当前没有在计时(countStart 为 0),说明已经 stop 过了,直接返回累计时长,避免重复累加
+    if (countStart === 0) {
+      return countTime;
+    }
+    countTime += (performance.now() - countStart);
+    countStart = 0;
+    return countTime;
+  }
+  const clearCount = () => {
+    countTime = 0;
   }
   // 开始倒计时
   const startCountdown = () => {
@@ -411,6 +511,8 @@ export const useExam = () => {
   }
   const setPracticeDuration = (duration: number) => {
     practiceDuration.value = duration;
+    practiceAccumulatedTime = duration;
+    practiceStartTime = 0;
   }
   const setCountDownCallback = (callback: () => void) => {
     countDownCallback.value = callback;
@@ -418,6 +520,8 @@ export const useExam = () => {
   const setDuration = (duration: number) => {
     examDuration.value = duration;
     practiceDuration.value = duration;
+    practiceAccumulatedTime = duration;
+    practiceStartTime = 0;
   }
   /// 整理题目结构
   const transerQuestions = (arr: Study.Question[]) => {
@@ -465,8 +569,10 @@ export const useExam = () => {
         answers: item.answers || [],
         subQuestions: item.subQuestions?.map(transerQuestion) || [],
         options: item.options?.map((option, index) => {
+          // 移除选项编号(如 A.)并解码 HTML 实体(如 &iacute; → í)
+          const cleanedOption = option.replace(/[A-Z]\./g, '').replace(/\s/g, ' ');
           return {
-            name: option,
+            name: decodeHtmlEntities(cleanedOption),
             no: orders[index],
             id: index,
             isAnswer: false,
@@ -477,7 +583,10 @@ export const useExam = () => {
         totalScore: item.totalScore || 0,
         offset: 0,
         index: index,
-        virtualIndex: 0
+        virtualIndex: 0,
+        duration: 0,
+        activeSubIndex: 0,
+        hasSubQuestions: item.subQuestions?.length > 0
       } as Study.Question
     }
     questionList.value = transerQuestions(list.map((item, index) => transerQuestion(item, index)));
@@ -499,7 +608,8 @@ export const useExam = () => {
             isMark: false,
             isNotKnow: false,
             hasParsed: false,
-            showParse: false
+            showParse: false,
+            duration: 0, // 清空做题时长
           }
         }),
         options: item.options.map(option => {
@@ -511,34 +621,37 @@ export const useExam = () => {
             isMissed: false,
             isIncorrect: false
           }
-        })
+        }),
+        duration: 0, // 清空做题时长
       }
     });
-    console.log(questionList.value)
     changeIndex(0);
     practiceDuration.value = 0;
+    practiceAccumulatedTime = 0;
+    practiceStartTime = 0;
     examDuration.value = 0;
     interval && clearInterval(interval);
     interval = null;
+    if (animationFrameId !== null) {
+      cancelAnimationFrame(animationFrameId);
+      animationFrameId = null;
+    }
   }
   /// 设置子题下标
   const setSubQuestionIndex = (index: number) => {
-    subQuestionIndex.value = index;
+    currentQuestion.value.activeSubIndex = index;
   }
   // 切换阅卷模式
   const setPracticeSettings = (settings: Study.PracticeSettings) => {
     practiceSettings.value = settings;
   }
-  // watch(() => currentIndex.value, (val) => {
-  //   subQuestionIndex.value = 0;
-  // }, {
-  //   immediate: false
-  // });
-  watch([() => currentIndex.value, () => subQuestionIndex.value], (val) => {
-    console.log('currentIndex.value', currentIndex.value)
-    console.log('subQuestionIndex.value', subQuestionIndex.value)
+  const submit = () => {
+    updateQuestionDuration(virtualCurrentIndex.value, false);
+  }
+  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
   });
@@ -584,6 +697,7 @@ export const useExam = () => {
     setQuestionList,
     changeIndex,
     reset,
+    submit,
     isQuestionCorrect,
     isOptionCorrect,
     setPracticeSettings,

+ 15 - 7
src/composables/useExamType.ts

@@ -1,7 +1,7 @@
 import { EnumDictName } from "@/common/enum";
 import { useDictStore } from "@/store/dictStore";
 import { useAppStore } from "@/store/appStore";
-import { DictItem } from "@/types";
+import { DictItem, System } from "@/types";
 import { getExamMajors, getExamTypes, getGraduateYears, getProvinces } from "@/api/modules/system";
 import { StudentExamInfo, UserInfo } from "@/types/user";
 const dictStore = useDictStore();
@@ -9,13 +9,13 @@ const appStore = useAppStore();
 
 export const useExamType = () => {
   const form = ref<Partial<Pick<StudentExamInfo, 'location' | 'examType' | 'endYear' | 'majorType'>>>({})
-  const provinceList = ref<DictItem[]>([]);
+  const provinceList = ref<System.ProvinceItem[]>([]);
   const examTypeList = ref<DictItem[]>([]);
   const examMajorList = ref<DictItem[]>([]);
   const endYearList = ref<DictItem[]>([]);
   const loadProvinceData = async () => {
-    const { data } = await getProvinces();
-    provinceList.value = data;
+    const { rows } = await getProvinces();
+    provinceList.value = rows;
   }
   const loadExamTypeData = async () => {
     if (form.value.location) {
@@ -43,12 +43,20 @@ export const useExamType = () => {
     }
   }
   watch(() => form.value.location, (val) => {
-    form.value.examType = '';
-    form.value.majorType = '';
-    form.value.endYear = undefined;
+    console.log('监听到省份数据', val)
+    if (examTypeList.value.length) {
+      form.value.examType = undefined;
+    }
+    if (examMajorList.value.length) {
+      form.value.majorType = '';
+    }
+    if (endYearList.value.length) {
+     form.value.endYear = undefined; 
+    }
     if (val) {
       loadExamTypeData();
     }
+    loadGraduateYearData();
   });
   watch(() => form.value.examType, (val) => {
     if (val) {

+ 5 - 1
src/config.ts

@@ -6,8 +6,12 @@ const config = {
   serverBaseUrl: '',
   paySiteUrl: '',
   responseErrorCatch: true,
+  defaultOrg: {
+    contactPhone: '400-1797-985',
+    logo: '',
+    orgName: '单招一卡通',
+  }
 };
-console.log(process.env.IE_ENV)
 export const env = {
   development: {
     serverBaseUrl: 'https://dz.shineking.top/prod-api',

+ 46 - 0
src/hooks/useAuth.ts

@@ -0,0 +1,46 @@
+import { EnumEvent, EnumUserRole, EnumUserType } from "@/common/enum";
+import { useUserStore } from "@/store/userStore";
+import IePage from "@/components/ie-page/ie-page.vue";
+
+export const useAuth = () => {
+  const userStore = useUserStore();
+  const { userInfo, isLogin, isVip, isStudent } = storeToRefs(userStore);
+  const hasPermission = (roles: EnumUserRole[] = []) => {
+    if (roles.length === 0) {
+      return true;
+    }
+    const hasAuth = roles.some(role => {
+      if (role === EnumUserRole.VIP) {
+        return isVip.value;
+      }
+      if (role === EnumUserRole.NORMAL) {
+        return isStudent.value && !isVip.value;
+      }
+      if (role === EnumUserRole.GUEST) {
+        return !isLogin.value;
+      }
+      if (role === EnumUserRole.TEACHER) {
+        return userInfo.value.userType === EnumUserType.TEACHER;
+      }
+      if (role === EnumUserRole.AGENT) {
+        return userInfo.value.userType === EnumUserType.AGENT;
+      }
+      return true;
+    });
+    if (!hasAuth && roles.includes(EnumUserRole.VIP)) {
+      uni.$emit(EnumEvent.OPEN_VIP_POPUP);
+      return false;
+    }
+    return hasAuth;
+  }
+
+  const checkVipPermission = () => {
+    if (!isVip.value) {
+
+    }
+  }
+
+  return {
+    hasPermission
+  }
+}

+ 1 - 1
src/hooks/useSms.ts

@@ -14,7 +14,7 @@ export const useSms = () => {
   const { setItem, getItem, removeItem } = useUniStore();
 
   // 状态常量定义
-  const SMS_COUNTDOWN_TIME = 60; // 倒计时时长(秒)
+  const SMS_COUNTDOWN_TIME = import.meta.env.DEV ? 5 : 60; // 倒计时时长(秒)
   const STORAGE_KEYS = {
     LAST_TIME: 'ie-smsLastTime',
     WAIT_TIME: 'ie-smsWaitTime'

+ 29 - 0
src/main.ts

@@ -10,6 +10,7 @@ import { useRequest } from '@/utils/request'
 import tool from '@/utils/uni-tool'
 import * as Pinia from 'pinia';
 import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
+import { useImage } from '@/hooks/useImage';
 
 // #ifndef VUE3
 import Vue from 'vue'
@@ -94,6 +95,34 @@ export function createApp() {
     }
   })
 
+  const { resolvePath } = useImage();
+  uni.$zp = {
+    config: {
+      'default-page-size': 20,
+      'refresher-title-style': {
+        fontSize: '28rpx'
+      },
+      'loading-more-title-custom-style': {
+        fontSize: '26rpx'
+      }
+      // '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'
+      // }
+    }
+  }
+
+  console.log(uni.$zp)
+
   const pinia = Pinia.createPinia();
   app.use(pinia);
   pinia.use(piniaPluginPersistedstate);

+ 1 - 1
src/manifest.json

@@ -42,7 +42,7 @@
     },
     "quickapp" : {},
     "mp-weixin" : {
-        "appid" : "wxa4d6c494e8f5d450",
+        "appid" : "wxadcab19e07ceef0f",
         "setting" : {
             "urlCheck" : false,
             "minified" : true,

+ 32 - 2
src/pages.json

@@ -543,13 +543,13 @@
           }
         },
         {
-          "path": "pages/edit-student-profile/edit-student-profile",
+          "path": "pages/edit-profile/edit-profile",
           "style": {
             "navigationBarTitleText": ""
           }
         },
         {
-          "path": "pages/edit-teacher-profile/edit-teacher-profile",
+          "path": "pages/bind-teacher-profile/bind-teacher-profile",
           "style": {
             "navigationBarTitleText": ""
           }
@@ -667,6 +667,36 @@
           "style": {
             "navigationBarTitleText": ""
           }
+        },
+        {
+          "path": "pages/textbooks-practice/textbooks-practice",
+          "style": {
+            "navigationBarTitleText": ""
+          }
+        },
+        {
+          "path": "pages/textbooks-practice-history/textbooks-practice-history",
+          "style": {
+            "navigationBarTitleText": ""
+          }
+        },
+        {
+          "path": "pages/study-exam-simulated-class/study-exam-simulated-class",
+          "style": {
+            "navigationBarTitleText": ""
+          }
+        },
+        {
+          "path": "pages/study-exam-simulated-student/study-exam-simulated-student",
+          "style": {
+            "navigationBarTitleText": ""
+          }
+        },
+        {
+          "path": "pages/study-exam-simulated-detail/study-exam-simulated-detail",
+          "style": {
+            "navigationBarTitleText": ""
+          }
         }
       ]
     }

+ 0 - 1
src/pagesMain/pages/index/components/index-banner.vue

@@ -43,7 +43,6 @@ const navigateTo = async (item: MenuItem) => {
   }
 }
 const validMenus = computed(() => {
-  console.log(userStore.isAuditor)
   const menus: MenuItem[] = [
     {
       name: '学习备考',

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

@@ -18,7 +18,7 @@
               <view class="flex items-center px-42 bg-back rounded-15 h-104 box-border">
                 <view class="flex-shrink-0">所在省份</view>
                 <ie-picker ref="pickerRef" v-model="form.location" :list="provinceList" :customStyle="customStyle"
-                  placeholder="请选择" key-label="dictLabel" key-value="dictValue">
+                  placeholder="请选择" key-label="areaName" key-value="shortName">
                 </ie-picker>
               </view>
               <view class="mt-30 flex items-center px-42 bg-back rounded-15 h-104 box-border">

+ 14 - 8
src/pagesMain/pages/index/index.vue

@@ -3,7 +3,7 @@
     <ie-navbar transparent bg-color="#FFFFFF" :placeholder="false" custom-back :click-hover="false">
       <template #headerLeft>
         <view class="flex items-center">
-          <view class="text-36 text-fore-title font-bold">单招一卡通</view>
+          <view class="text-36 text-fore-title font-bold">{{ orgName }}</view>
           <view class="w-6 h-6 rounded-2 bg-black mx-12"></view>
           <view>升学备考好帮手</view>
           <view v-if="userStore.getLocation" class="ml-10 flex items-center gap-x-4" @click="handleChangeLocation">
@@ -18,7 +18,6 @@
       <index-banner />
       <index-guide @detail="handleDetail" />
       <index-news @detail="handleDetail" />
-
     </view>
     <template #tabbar>
       <ie-tabbar :active="0" />
@@ -44,6 +43,7 @@ const { baseStickyTop } = useNavbar();
 const scrollTop = ref(0);
 const isHide = ref(false);
 const userStore = useUserStore();
+const orgName = computed(() => userStore.orgInfo.orgName);
 
 const handleDetail = async (id: number | string, title?: string) => {
   if (('' + id).includes(',')) {
@@ -70,7 +70,12 @@ const checkProvinceInfo = () => {
   }
 }
 const checkTeacherInfo = async () => {
-  const isTeacherInfoComplete = await userStore.checkInfoComplete();
+  await userStore.checkInfoComplete();
+}
+const reloadUserInfo = async () => {
+  if (userStore.isLogin) {
+    await userStore.getUserInfo();
+  }
 }
 const handleChangeLocation = () => {
   if (userStore.isLogin) {
@@ -87,17 +92,18 @@ onPageScroll((e) => {
   }
 });
 onShow(() => {
-  setTimeout(() => {
-    checkProvinceInfo();
-    checkTeacherInfo();
-  }, 500);
-  isHide.value = false;
   setTimeout(() => {
     uni.pageScrollTo({
       scrollTop: scrollTop.value,
       duration: 0
     });
   }, 0);
+  setTimeout(() => {
+    checkProvinceInfo();
+    checkTeacherInfo();
+    reloadUserInfo();
+  }, 500);
+  isHide.value = false;
 });
 </script>
 

+ 53 - 22
src/pagesMain/pages/me/components/me-info.vue

@@ -1,37 +1,42 @@
 <template>
   <view class="mx-30 mt-80">
     <view class="flex items-center justify-between gap-x-20" @click="handleHeaderClick">
-      <ie-image :src="avatar" custom-class="w-128 h-128" :round="999" />
+      <ie-image :src="avatar" custom-class="w-134 h-134" :round="999" />
       <view class="flex-1 min-w-1">
         <view class="text-40 text-fore-title flex items-center gap-x-20">
           <text class="font-bold">{{ nickName }}</text>
-          <ie-image v-if="isVip" src="/static/personal/vip_tag.png" custom-class="w-100 h-36" />
+          <view v-if="isLogin" class="bg-[#EBF4FC] pl-6 pr-20 py-6 rounded-20 flex items-center">
+            <ie-image src="/static/personal/icon-role.png" custom-class="w-30 h-30" mode="aspectFit" />
+            <text class="ml-10 text-20 text-primary">{{ roleDesc }}</text>
+          </view>
         </view>
-        <view v-if="phonenumber" class="text-30 text-fore-subcontent">{{ phonenumber }}</view>
+        <view v-if="!isLogin" class="mt-8 text-34 text-[#666666]">注册/登录</view>
+        <view v-else-if="phonenumber" class="mt-8 text-30 text-fore-subcontent">{{ phonenumber }}</view>
       </view>
       <view>
         <ie-image src="/static/personal/setting.png" custom-class="w-48 h-48" @click.stop="handleSettingClick" />
       </view>
     </view>
-    <!-- <view class="my-30 flex items-center text-center">
-      <view class="flex-1">
-        <view class="text-30 text-fore-title font-bold">0</view>
-        <view class="mt-10 text-26 text-fore-subcontent">做题数量</view>
-      </view>
-      <view class="flex-1">
-        <view class="text-30 text-fore-title font-bold">0</view>
-        <view class="mt-10 text-26 text-fore-subcontent">视频观看时长</view>
-      </view>
-      <view class="flex-1">
-        <view class="text-30 text-fore-title font-bold">0</view>
-        <view class="mt-10 text-26 text-fore-subcontent">登录次数</view>
-      </view>
-    </view> -->
-    <view v-if="isVip" class="mt-30 relative">
-      <ie-image src="/static/personal/buy_vip.png" custom-class="w-full h-96" />
-      <view class="absolute left-100 right-20 top-0 h-full flex items-center justify-between">
-        <view class="text-26 text-fore-title">已开通会员,享受权益中</view>
-        <view class="text-26 text-fore-subcontent">{{ vipInfo.outDate }} 到期</view>
+
+    <view v-if="isStudent" class="mt-50 h-184 rounded-15 relative bg-gradient-to-r from-[#253045] to-[#141D2F] overflow-hidden">
+      <ie-image src="/static/personal/bg-vip-card.png"
+        custom-class="w-276 h-full absolute left-1/2 top-0 -translate-x-1/2 z-0" mode="heightFix" />
+      <view class="h-full box-border px-45 flex items-center justify-between relative z-1">
+        <view class="flex-1 min-w-1">
+          <view class="flex items-center justify-between">
+            <ie-image src="/static/personal/icon-vip.png" custom-class="w-210 h-46" />
+            <view v-if="!isVip" class="flex items-center justify-center px-20 py-10 rounded-22"
+              style="background: radial-gradient( 0% 0% at 0% 0%, #FEE8BD 0%, #EFCC8D 100%);" @click="handleVip">
+              <text class="mr-4 text-24 text-[#532F12] font-bold leading-23">开通会员</text>
+              <uv-icon name="arrow-right" size="13" color="#532F12" />
+            </view>
+          </view>
+          <view class="mt-16 text-26 text-[#FEECCB]">{{ isVip ? `使用有效期至 ${vipInfo.outDate}` : '开通VIP会员,享受更多专属权益' }}
+          </view>
+        </view>
+        <view class="shrink-0">
+          <ie-image v-if="isVip" src="/static/personal/icon-vip2.png" custom-class="w-160 h-168" mode="heightFix" />
+        </view>
       </view>
     </view>
   </view>
@@ -45,7 +50,29 @@ const avatar = computed(() => userStore.avatar);
 const nickName = computed(() => userStore.nickName);
 const phonenumber = computed(() => userStore.anonymousPhoneNumber);
 const isVip = computed(() => userStore.isVip);
+const isStudent = computed(() => userStore.isStudent);
+const isLogin = computed(() => userStore.isLogin);
+const isExperienceVip = computed(() => userStore.isExperienceVip);
 const vipInfo = computed(() => userStore.vipInfo);
+const roleDesc = computed(() => {
+  if (isLogin.value && !isVip && !userStore.isStudent) {
+    return '普通会员';
+  }
+  if (isVip.value) {
+    if (isExperienceVip.value) {
+      return '体验会员';
+    }
+    return 'VIP会员';
+  }
+  if (userStore.isAgent) {
+    return '内部账号';
+  }
+  if (userStore.isTeacher) {
+    return '教师账号';
+  }
+  return '普通会员';
+});
+
 const handleHeaderClick = async () => {
   // 不询问直接跳转登录
   const isLogin = await userStore.checkLogin({ askToLogin: false });
@@ -55,6 +82,10 @@ const handleHeaderClick = async () => {
     }, 500)
   }
 }
+const handleVip = () => {
+  transferTo('/pagesSystem/pages/card-verify/card-verify')
+}
+
 const handleSettingClick = async () => {
   transferTo('/pagesOther/pages/personal-center/setting/setting');
 }

+ 0 - 7
src/pagesMain/pages/me/components/me-menu copy.vue

@@ -1,7 +0,0 @@
-<template>
-  <view></view>
-</template>
-<script lang="ts" setup>
-
-</script>
-<style lang="scss" scoped></style>

+ 23 - 33
src/pagesMain/pages/me/components/me-menu.vue

@@ -9,40 +9,30 @@
         </view>
       </view>
       <view class="mt-40 text-30 text-fore-title font-bold">其他功能</view>
+      <view class="mt-16 shadow-card rounded-8 py-10 bg-white">
+        <uv-cell-group :border="false">
+          <uv-cell isLink :cellStyle="cellStyle"
+            @click="handleNavigate('/pagesSystem/pages/edit-profile/edit-profile', '基本资料')">
+            <template #title>
+              <view class="flex items-center gap-x-10">
+                <ie-image src="/static/personal/icon_jibenziliao@2x.png" custom-class="w-34 h-34" />
+                <text class="text-30 text-fore-subtitle">基本资料</text>
+              </view>
+            </template>
+          </uv-cell>
+          <uv-cell isLink :cellStyle="cellStyle" @click="handleQuestion" :border="false">
+            <template #title>
+              <view class="flex items-center gap-x-10">
+                <uv-icon name="question-circle" size="16" color="#888888" />
+                <text class="text-30 text-fore-subtitle">常见问题</text>
+              </view>
+            </template>
+          </uv-cell>
+        </uv-cell-group>
+      </view>
     </view>
-    <view class="-mt-10 rounded-8 py-20">
-      <uv-cell-group :border="false">
-        <uv-cell isLink :cellStyle="cellStyle"
-          @click="handleNavigate('/pagesSystem/pages/edit-student-profile/edit-student-profile', '基本资料')">
-          <template #title>
-            <view class="flex items-center gap-x-10">
-              <ie-image src="/static/personal/icon_jibenziliao@2x.png" custom-class="w-34 h-34" />
-              <text class="text-30 text-fore-subtitle">基本资料</text>
-            </view>
-          </template>
-
-        </uv-cell>
-        <!-- <uv-cell isLink :cellStyle="cellStyle"
-          @click="handleNavigate('/pagesOther/pages/personal-center/change-pwd/change-pwd', '修改密码')">
-          <template #title>
-            <view class="flex items-center gap-x-10">
-              <ie-image src="/static/personal/icon_password@2x.png" custom-class="w-36 h-36" />
-              <text class="text-30 text-fore-subtitle">修改密码</text>
-            </view>
-          </template>
-        </uv-cell> -->
-        <uv-cell isLink :cellStyle="cellStyle" @click="handleQuestion">
-          <template #title>
-            <view class="flex items-center gap-x-10">
-              <uv-icon name="question-circle" size="16" color="#888888" />
-              <text class="text-30 text-fore-subtitle">常见问题</text>
-            </view>
-          </template>
-        </uv-cell>
-      </uv-cell-group>
-    </view>
-    <view v-if="userStore.isLogin" class="mt-80 mb-40 w-400 mx-auto">
-      <uv-button type="error" size="medium" shape="circle" text="退出登陆" plain @click="handleLogout"></uv-button>
+    <view v-if="userStore.isLogin" class="mt-80 mb-40 px-30">
+      <ie-button type="primary" custom-class="w-full" @click="handleLogout">退出登陆</ie-button>
     </view>
   </view>
 </template>

+ 1 - 1
src/pagesMain/pages/me/me.vue

@@ -1,6 +1,6 @@
 <template>
   <ie-page>
-    <ie-image :is-oss="true" src="/volunteer/page-bg.png" custom-class="w-full h-auto absolute top-0 left-0 -z-1" />
+    <ie-image :is-oss="true" src="/me/page-bg.png" custom-class="w-full h-auto absolute top-0 left-0 -z-1" />
     <me-info />
     <me-menu />
     <template #tabbar>

+ 13 - 3
src/pagesMain/pages/splash/splash.vue

@@ -16,6 +16,9 @@ import { useAppStore } from '@/store/appStore';
 import { useAppConfig } from '@/hooks/useAppConfig';
 import { useUserStore } from '@/store/userStore';
 import { useTransferPage } from '@/hooks/useTransferPage';
+import { load } from '@/utils/loadFont';
+
+const splashTimeout = 1200;
 const appStore = useAppStore();
 const userStore = useUserStore();
 const { transferTo } = useTransferPage();
@@ -32,12 +35,19 @@ const handleLoad = () => {
     });
   }
   // 执行初始化的操作:预加载字典、提前校验token是否有效等
-  appStore.init().then(() => {
-    setTimeout(() => {
+  appStore.init().then(async () => {
+    try {
+      const usedTime = await load();
+      if (usedTime < splashTimeout) {
+        await new Promise(resolve => setTimeout(resolve, splashTimeout - usedTime));
+      }
+    } catch (error) {
+      console.error('初始化失败: ', error);
+    } finally {
       transferTo('/pagesMain/pages/index/index', {
         type: 'reLaunch'
       });
-    }, 1200);
+    }
   });
 };
 onLoad(() => {

+ 93 - 87
src/pagesOther/pages/personal-center/setting/setting.vue

@@ -1,104 +1,110 @@
 <template>
-    <view class="page-content">
-        <mx-nav-bar title="关于"/>
-        <view class="fx-col fx-cen-cen py-80">
-            <uv-image src="/static/logo.png" width="80" height="auto" mode="widthFix"/>
-        </view>
-        <uv-cell-group class="bg-white !flex-none">
-            <uv-cell v-for="s in settings" v-bind="s" @click="s.handler()"/>
-        </uv-cell-group>
+  <view class="page-content">
+    <mx-nav-bar title="关于" />
+    <view class="fx-col fx-cen-cen py-80">
+      <ie-image src="/static/logo.png" custom-class="w-160 h-160" mode="widthFix" />
     </view>
+    <uv-cell-group class="bg-white !flex-none">
+      <uv-cell v-for="s in settings" v-bind="s" @click="s.handler()" />
+    </uv-cell-group>
+  </view>
 </template>
 
 <script setup>
-import {onMounted, reactive} from 'vue'
-import {useTransfer} from "@/hooks/useTransfer";
-import {useCacheStore} from "@/hooks/useCacheStore";
-import {useEnvStore} from "@/hooks/useEnvStore";
-import {confirmAsync} from "@/utils/uni-helper";
-import {useUserStore} from "@/hooks/useUserStore";
-import {sizeFormat} from "@/utils";
+import { onMounted, reactive } from 'vue'
+import { useTransfer } from "@/hooks/useTransfer";
+import { useCacheStore } from "@/hooks/useCacheStore";
+import { useEnvStore } from "@/hooks/useEnvStore";
+import { confirmAsync } from "@/utils/uni-helper";
+import { useUserStore } from "@/hooks/useUserStore";
+import { sizeFormat } from "@/utils";
+import { useUserStore as newUserStore } from '@/store/userStore';
 
-const {transferToProtocolUser, transferToProtocolPrivacy, transferToIndex, cleanAllTransferCacheData} = useTransfer()
-const {getSize: getCacheSize, gcCache} = useCacheStore()
-const {systemInfo} = useEnvStore()
-const {LogoutPhysical, GetInfo, isLogin} = useUserStore()
-
-const settings = reactive([
-    {
-        title: '服务协议',
-        icon: '/static/personal/icon-yonghuxieyi@2x.png',
-        isLink: true,
-        handler: () => transferToProtocolUser()
-    },
-    {
-        title: '隐私政策',
-        icon: '/static/personal/icon-yinxixieyi@2x.png',
-        isLink: true,
-        handler: () => transferToProtocolPrivacy()
-    },
-    {
-        title: '清除缓存', //icon_cache
-        icon: '/static/personal/icon_cache.png',
-        rightIcon: 'reload',
-        isLink: true,
-        handler: () => {
-            cleanAllTransferCacheData() // 清理页面大对象
-            GetInfo() // GetInfo也会清除缓存,借机也更新一下用户信息
-            calculateCacheSize()
-        }
-    },
-    {
-        title: '当前版本',
-        icon: '/static/personal/icon-dangqianbanben@2x.png',
-        value: systemInfo.value.appVersion,
-        handler: () => {
-        }
-    },
-    {
-        title: '注销账号',
-        icon: 'trash',
-        isLink: true,
-        handler: async () => {
-          if (!isLogin.value) {
-            const msg = '当前未登录'
-            await confirmAsync(msg)
-            return;
-          }
-            const msg = '是否注销账号,注销后卡号将不能再注册使用系统且永久失效。请谨慎操作!'
-            await confirmAsync(msg)
-            await LogoutPhysical()
-            transferToIndex()
-        }
-    },
-    {
-        title: 'APP备案号',
-        icon: 'empty-permission',
-        value: '湘ICP备18012964号-8A',
-        handler: () => {
-        }
-    },
-    {
-        title: '客服电话',
-        icon: 'server-man',
-        value: '400-1797-985',
-        handler: () => {
-        }
+const { transferToProtocolUser, transferToProtocolPrivacy, transferToIndex, cleanAllTransferCacheData } = useTransfer()
+const { getSize: getCacheSize, gcCache } = useCacheStore()
+const { systemInfo } = useEnvStore()
+const { LogoutPhysical, GetInfo, isLogin } = useUserStore()
+const { orgInfo } = newUserStore()
+const cacheSize = ref(0)
+const settings = computed(() => [
+  {
+    title: '服务协议',
+    icon: '/static/personal/icon-yonghuxieyi@2x.png',
+    isLink: true,
+    handler: () => transferToProtocolUser()
+  },
+  {
+    title: '隐私政策',
+    icon: '/static/personal/icon-yinxixieyi@2x.png',
+    isLink: true,
+    handler: () => transferToProtocolPrivacy()
+  },
+  {
+    title: '清除缓存', //icon_cache
+    icon: '/static/personal/icon_cache.png',
+    rightIcon: 'reload',
+    isLink: true,
+    value: cacheSize.value,
+    handler: () => {
+      cleanAllTransferCacheData() // 清理页面大对象
+      if (isLogin.value) {
+        GetInfo() // GetInfo也会清除缓存,借机也更新一下用户信息
+      }
+      calculateCacheSize()
+    }
+  },
+  {
+    title: '当前版本',
+    icon: '/static/personal/icon-dangqianbanben@2x.png',
+    value: systemInfo.value.appVersion,
+    handler: () => {
+    }
+  },
+  {
+    title: '注销账号',
+    icon: 'trash',
+    isLink: true,
+    handler: async () => {
+      if (!isLogin.value) {
+        const msg = '当前未登录'
+        await confirmAsync(msg)
+        return;
+      }
+      const msg = '是否注销账号,注销后卡号将不能再注册使用系统且永久失效。请谨慎操作!'
+      await confirmAsync(msg)
+      await LogoutPhysical()
+      transferToIndex()
     }
+  },
+  {
+    title: 'APP备案号',
+    icon: 'empty-permission',
+    value: '湘ICP备18012964号-8A',
+    handler: () => {
+    }
+  },
+  {
+    title: '客服电话',
+    icon: 'server-man',
+    value: orgInfo.contactPhone,
+    handler: () => {
+    }
+  }
 ])
 
-onMounted(() => calculateCacheSize())
-
 const calculateCacheSize = function () {
-    gcCache()
-    let size = getCacheSize()
-    settings[2].value = sizeFormat(size)
+  gcCache()
+  const size = getCacheSize()
+  cacheSize.value = sizeFormat(size)
 }
+onMounted(() => {
+  calculateCacheSize()
+});
 </script>
 
 <style scoped>
 ::v-deep .uv-cell__body {
-    font-size: 16px;
-    padding: 12px 15px;
+  font-size: 16px;
+  padding: 12px 15px;
 }
 </style>

+ 6 - 4
src/pagesOther/pages/video-center/index/components/video-page-layout.vue

@@ -33,6 +33,10 @@ import { empty } from "@/uni_modules/uv-ui-tools/libs/function/test";
 import { useTransfer } from "@/hooks/useTransfer";
 import mxConst from "@/common/mxConst";
 import { useUserStore } from '@/store/userStore';
+import { useAuth } from '@/hooks/useAuth';
+import { EnumUserRole } from '@/common/enum';
+const { hasPermission } = useAuth();
+
 const userStore = useUserStore();
 const openVipPopup = inject(OPEN_VIP_POPUP);
 const props = defineProps({
@@ -90,8 +94,8 @@ const handleLeafClick = async ({ node, deep }) => {
 const handleVideoPlay = async (node) => {
   // 尝试传递更多数据,不使用playVideo,启用usingCache
   // playVideo(node.aliId, node.aliIdType, node.name)
-  const isVip = await userStore.checkVip();
-  if (isVip) {
+  const hasAuth = hasPermission([EnumUserRole.VIP, EnumUserRole.AGENT, EnumUserRole.TEACHER]);
+  if (hasAuth) {
     const nodeWithKnowledge = list.value.find(n => n.children?.includes(node))
     transferTo(mxConst.routes.videoPlay, {
       aliId: node.aliId,
@@ -99,8 +103,6 @@ const handleVideoPlay = async (node) => {
       title: node.name,
       node: nodeWithKnowledge
     }, null, true)
-  } else {
-    openVipPopup();
   }
 
 }

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

@@ -2,8 +2,8 @@
   <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-26 text-fore-light">提交时间: {{ data.date || '-' }}</view>
-      <view class="text-26 text-primary flex items-center gap-x-4" @click="handleDetail">
+      <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>
@@ -37,7 +37,8 @@ const handleDetail = () => {
   if (isFinished.value) {
     transferTo('/pagesStudy/pages/simulation-analysis/simulation-analysis', {
       data: {
-        examineeId: props.data.id
+        examineeId: props.data.id,
+        paperType: EnumPaperType.SIMULATED
       }
     });
   } else {

+ 45 - 41
src/pagesStudy/components/knowledge-tree-node.vue

@@ -1,15 +1,15 @@
 <template>
   <view class="knowledge-tree-node">
-    <view class="min-h-[68px]" @click.stop="handleClick">
-      <view class="flex items-center border-0 border-b border-solid border-[#E6E6E6] py-26">
+    <view class="" @click.stop="handleClick">
+      <view class="flex items-center border-0 border-b border-solid border-[#E6E6E6] py-20">
         <view class="flex-1 min-w-1 flex items-center">
           <uv-icon v-if="!nodeData.isLeaf" name="arrow-right" size="14" color="#888"
             :custom-class="['mr-16 transition-transform duration-300', nodeData.isExpanded ? 'rotate-90' : '']" />
           <view>
             <view class="block text-28 text-fore-title font-bold ellipsis-1">{{ nodeData.name }}</view>
-            <view class="mt-4 text-24 text-fore-light flex items-center">
+            <view class="mt-8 text-24 text-fore-light flex items-center">
               <progress class="w-100 rounded-full overflow-hidden" :percent="getProgressPercent(nodeData)"
-                :show-text="false" activeColor="#31a0fc" backgroundColor="#E3F4FA" />
+                :show-text="false" activeColor="#31a0fc" backgroundColor="#efefef" />
               <text class="ml-10 text-primary">{{ nodeData.finishedCount }}</text>
               <text>/{{ nodeData.questionCount }}道</text>
               <text class="ml-10">正确率</text>
@@ -26,12 +26,11 @@
     </view>
 
     <!-- 子节点容器 -->
-    <view v-if="nodeData.children && nodeData.children.length > 0"
-      :class="['ml-40 overflow-hidden transition-all duration-300 h-0']"
-      :style="{ height: nodeData.actualHeight + 'px' }">
+    <view v-if="nodeData.children && nodeData.children.length > 0" :id="`knowledge-children-${nodeData.id}`"
+      :class="['ml-80 overflow-hidden transition-all duration-300', { 'is-measuring': isMeasuringHeight }]"
+      :style="{ height: measuredHeight + 'px' }">
       <knowledge-tree-node v-for="child in nodeData.children" :key="child.name" :node-data="child"
-        :parent-data="nodeData" @node-click="handleNodeClick" @update-height="handleUpdateHeight"
-        @start-practice="handleChildStartPractice">
+        :parent-data="nodeData" @node-click="handleNodeClick" @start-practice="handleChildStartPractice">
         <template #default>
           <slot></slot>
         </template>
@@ -54,9 +53,10 @@ const props = defineProps({
     default: null
   }
 });
-
+const isMeasuringHeight = ref(false);
+const measuredHeight = ref(0);
 // 定义 emits
-const emit = defineEmits(['nodeClick', 'updateHeight', 'startPractice']);
+const emit = defineEmits(['nodeClick', 'startPractice']);
 
 // 计算子节点高度
 const calculateChildrenHeight = (node: Study.KnowledgeNode): number => {
@@ -78,28 +78,38 @@ const calculateChildrenHeight = (node: Study.KnowledgeNode): number => {
   return height;
 };
 
+const getRect = (selector: string) => {
+  return new Promise((resolve: (rect: { top: number, height: number }) => void) => {
+    const query = uni.createSelectorQuery();
+    query.select(selector).boundingClientRect(function (rect) {
+      resolve(rect as { top: number, height: number });
+    }).exec();
+  });
+}
+
+const measureHeight = () => {
+  isMeasuringHeight.value = true;
+  nextTick(() => {
+    getRect(`#knowledge-children-${props.nodeData.id}`).then((res: any) => {
+      isMeasuringHeight.value = false;
+      setTimeout(() => {
+        nextTick(() => {
+          measuredHeight.value = res?.height ?? 0;
+        });
+      }, 0);
+    });
+  });
+}
+
 // 处理节点点击
 const handleClick = () => {
 
   // 切换展开状态
   props.nodeData.isExpanded = !props.nodeData.isExpanded;
-
-  // 计算并设置实际高度
   if (props.nodeData.isExpanded) {
-    props.nodeData.actualHeight = calculateChildrenHeight(props.nodeData);
+    measureHeight();
   } else {
-    props.nodeData.actualHeight = 0;
-  }
-
-  // 通知父组件节点被点击
-  emit('nodeClick', {
-    node: props.nodeData,
-    parent: props.parentData
-  });
-
-  // 如果有父节点,通知父节点更新高度
-  if (props.parentData) {
-    emit('updateHeight', props.parentData);
+    measuredHeight.value = 0;
   }
 };
 
@@ -119,20 +129,6 @@ const handleNodeClick = (eventData: { node: Study.KnowledgeNode; parent: Study.K
   emit('nodeClick', eventData);
 };
 
-// 处理高度更新事件
-const handleUpdateHeight = (parentNode: Study.KnowledgeNode) => {
-
-  // 重新计算父节点高度
-  if (parentNode.isExpanded) {
-    parentNode.actualHeight = calculateChildrenHeight(parentNode);
-  }
-
-  // 继续向上传递高度更新事件
-  if (props.parentData) {
-    emit('updateHeight', props.parentData);
-  }
-};
-
 const getCorrectRate = (rate: number): number => {
   if (!rate) {
     return 0;
@@ -152,4 +148,12 @@ const getProgressPercent = (nodeData: Study.KnowledgeNode): number => {
 };
 </script>
 
-<style lang="scss" scoped></style>
+<style lang="scss" scoped>
+.is-measuring {
+  opacity: 0;
+  position: fixed;
+  z-index: -1;
+  transform: translateX(-100%);
+  height: auto !important;
+}
+</style>

+ 6 - 15
src/pagesStudy/components/knowledge-tree.vue

@@ -1,7 +1,7 @@
 <template>
   <view class="knowledge-tree">
     <knowledge-tree-node v-for="item in initializedData" :key="item.id" :node-data="item"
-      @node-click="handleNodeClick" @update-height="handleUpdateHeight" @start-practice="handleStartPractice">
+      @node-click="handleNodeClick" @start-practice="handleStartPractice">
       <template #default>
         <slot></slot>
       </template>
@@ -12,14 +12,14 @@
 import * as Study from '@/types/study';
 import KnowledgeTreeNode from './knowledge-tree-node.vue';
 // 定义 emits
-const emit = defineEmits(['update:treeData', 'nodeClick', 'startPractice']);
+const emit = defineEmits(['nodeClick', 'startPractice']);
 
 // 定义 props
 const props = defineProps({
   treeData: {
     type: Array as PropType<Study.KnowledgeNode[]>,
     default: () => []
-  }
+  },
 });
 
 // 初始化后的数据
@@ -33,22 +33,19 @@ const ensureItemProperties = (item: Study.KnowledgeNode) => {
   if (item.isLeaf === undefined) {
     item.isLeaf = !item.children || item.children.length === 0;
   }
-  if (item.actualHeight === undefined) {
-    item.actualHeight = 0;
-  }
   // 递归处理子项
   item.children?.forEach(child => ensureItemProperties(child));
 }
 
 // 初始化数据
 const initializeData = (sourceData: Study.KnowledgeNode[]): Study.KnowledgeNode[] => {
-  return sourceData.map(item => {
+  return sourceData.map((item, index) => {
+    const oldItem = initializedData.value[index];
     const children = initializeData(item.children || []);
     return {
       ...item,
-      isExpanded: item.isExpanded ?? false,
+      isExpanded: item.id === oldItem?.id ? oldItem?.isExpanded ?? false : false,
       isLeaf: !item.children || item.children.length === 0,
-      actualHeight: item.actualHeight ?? 0,
       children
     };
   });
@@ -60,12 +57,6 @@ const handleNodeClick = (eventData: { node: Study.KnowledgeNode; parent: Study.K
   emit('nodeClick', eventData);
 };
 
-// 处理高度更新事件
-const handleUpdateHeight = (node: Study.KnowledgeNode) => {
-  // 通知父组件数据已更新
-  emit('update:treeData', initializedData.value);
-};
-
 // 处理开始练习事件
 const handleStartPractice = (node: Study.KnowledgeNode) => {
   emit('startPractice', node);

+ 104 - 0
src/pagesStudy/components/paper-work-item.vue

@@ -0,0 +1,104 @@
+<template>
+  <view class="bg-white">
+    <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 }}</view>
+        <view class="ml-20">发布人: {{ data.publishUser }}</view>
+      </view>
+      <view :class="['text-28', data.state === EnumPaperWorkState.NOT_COMPLETED ? 'text-warning' : 'text-success']">
+        {{ data.state === EnumPaperWorkState.NOT_COMPLETED ? '未完成' : '已完成' }}
+      </view>
+    </view>
+    <view class="px-46 py-30 relative">
+      <view>
+        <text class="text-28 text-fore-title font-bold">{{ data.universityName }}-{{ data.majorName }}</text>
+        <text v-if="data.directed"
+          class="ml-10 bg-[#F0FDF4] text-[#22C55E] border border-solid border-[#22C55E] text-20 rounded-4 px-10 py-2">定向</text>
+      </view>
+      <view class="mt-40 ml-20 text-24 ">
+        <view>
+          <text class="text-fore-light">得分/总分:</text>
+          <text class="text-fore-title">{{ data.score || '-' }}/{{ data.total }}</text>
+        </view>
+        <view class="mt-14">
+          <text class="text-fore-light">科目/批次:</text>
+          <text class="text-fore-title">{{ batchName }}</text>
+        </view>
+        <view class="mt-14">
+          <text class="text-fore-light">所属校区:</text>
+          <text class="text-fore-title">{{ data.campusName }}</text>
+        </view>
+      </view>
+      <view v-if="data.state === EnumPaperWorkState.COMPLETED"
+        class="mt-20 bg-[#FFFBEB] py-18 px-20 rounded-5 text-24 text-[#F59E0B] flex items-center justify-between border border-solid border-[#FEF6DA]">
+        <text>提交时间:{{ data.endTime }}</text>
+        <text>做题时长:{{ formatSeconds(data.duration) }}</text>
+      </view>
+      <view class="absolute right-50 top-100">
+        <view v-if="data.state === EnumPaperWorkState.COMPLETED"
+          class="px-28 py-10 border border-solid border-primary rounded-full text-24 text-primary bg-[#EEF8FD]"
+          @click="handleDetail">详情
+        </view>
+        <view v-else class="text-24 text-white bg-primary px-28 py-10 rounded-full" @click="handleStart">去完成</view>
+      </view>
+    </view>
+  </view>
+</template>
+<script lang="ts" setup>
+import { EnumPaperType, EnumPaperWorkState } from '@/common/enum';
+import { useTransferPage } from '@/hooks/useTransferPage';
+import { getPaperWorkDetail, getOpenExaminee } from '@/api/modules/study';
+import { Study, Transfer } from '@/types';
+import { formatSeconds } from '@/utils/common';
+const { transferTo } = useTransferPage();
+const props = defineProps<{
+  data: Study.PaperWork;
+}>();
+
+const batchName = computed(() => {
+  const names = props.data.name.split('_');
+  return `${names[1]}(${names[0]})`
+});
+const publishTime = computed(() => {
+  return uni.$ie.formatTime(props.data.publishTime, 'yyyy年mm月dd日 hh:MM:ss');
+});
+
+const handleDetail = () => {
+  uni.$ie.showLoading();
+  getPaperWorkDetail(props.data.id).then(res => {
+    uni.$ie.hideLoading();
+    transferTo('/pagesStudy/pages/simulation-analysis/simulation-analysis', {
+      data: {
+        examineeId: res.data.examineeId,
+        paperType: EnumPaperType.TEST
+      }
+    });
+  }).catch(err => {
+    uni.$ie.hideLoading();
+    console.error(err);
+  });
+
+}
+
+const handleStart = () => {
+  getOpenExaminee({
+    paperType: EnumPaperType.TEST,
+    relateId: props.data.id,
+    directed: props.data.directed
+  }).then(res => {
+    transferTo('/pagesStudy/pages/exam-start/exam-start', {
+      data: {
+        name: '组卷作业-' + batchName.value,
+        paperType: EnumPaperType.TEST,
+        simulationInfo: {
+          examineeId: res.data.examineeId
+        },
+      } as Transfer.ExamAnalysisPageOptions,
+    });
+  }).catch(err => {
+    console.error(err);
+  });
+}
+</script>
+<style lang="scss" scoped></style>

+ 7 - 8
src/pagesStudy/components/practice-table.vue

@@ -114,14 +114,12 @@
   </view>
 </template>
 <script lang="ts" setup>
-import { nextTick } from 'vue';
 import { TableColumnConfig } from '@/types';
 import ieEchart from './ie-echart/ie-echart.vue';
 import { useCalendar } from '@/composables/useCalendar';
-import { getPlanStudyRecord } from '@/api/modules/study';
-import * as Study from '@/types';
+
 const props = defineProps<{
-  studentId: number;
+  recordId?: number;
 }>();
 // 使用 useCalendar composable
 const {
@@ -242,6 +240,7 @@ const options1 = computed(() => {
         type: 'pie',
         radius: ['60%', '80%'],
         startAngle: 90,
+        silent: true,
         label: { show: false },
         data: [
           {
@@ -300,10 +299,11 @@ const options2 = computed(() => {
         type: 'pie',
         radius: ['60%', '80%'],
         startAngle: 90,
+        silent: true,
         label: { show: false },
         data: [
           {
-            value: 75,
+            value: accuracy,
             itemStyle: {
               color: {
                 type: 'linear',
@@ -319,7 +319,7 @@ const options2 = computed(() => {
             }
           },
           {
-            value: 35,
+            value: 100 - accuracy,
             itemStyle: { color: '#EBF9FF' }
           }
         ]
@@ -449,12 +449,11 @@ const handleOpenCalendar = () => {
 onMounted(async () => {
   // 先初始化默认选中
   initializeDefaultSelection();
-  initCalendar(props.studentId);
   // 显示全屏loading
   uni.$ie.showLoading();
   try {
     // 初始化日历
-    // await initCalendar(props.studentId);
+    await initCalendar(props.recordId);
   } finally {
     // 隐藏loading
     uni.$ie.hideLoading();

+ 10 - 11
src/pagesStudy/components/teacher-class-view.vue

@@ -15,21 +15,20 @@
 </template>
 <script lang="ts" setup>
 import { TeachClass } from '@/types/study';
-const classId = ref<number>(0);
+import { getTeachClassList } from '@/api/modules/study';
+const classId = ref<number | null>(null);
 const classList = ref<TeachClass[]>([]);
 const selectedClass = ref<TeachClass>();
 const pickerRef = ref();
 const loadData = async () => {
-  // const res = await getClassHistory();
-  // 模拟数据
-  classList.value = [
-    { classId: 1, schoolId: 1, year: 2025, name: '班级1' },
-    { classId: 2, schoolId: 1, year: 2025, name: '班级2' },
-    { classId: 3, schoolId: 1, year: 2025, name: '班级3' },
-  ];
-  // 默认选中第一个
-  classId.value = classList.value[0].classId;
-  selectedClass.value = classList.value[0];
+  try {
+    const { data } = await getTeachClassList();
+    classList.value = data;
+    classId.value = classList.value[0].classId;
+    selectedClass.value = classList.value[0];
+  } catch (error) {
+    console.log(error);
+  }
 }
 onMounted(() => {
   loadData();

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

@@ -11,7 +11,7 @@
       </view>
     </view>
     <view class="mx-30">
-      <ie-table :table-columns="tableColumns" :data="data" @rowClick="handleRowClick">
+      <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" />
@@ -29,7 +29,7 @@
 import { TableColumnConfig } from '@/types';
 import * as Study from '@/types/study';
 const props = defineProps<{
-  data: Study.VideoStudyRecord;
+  data: Study.VideoStudy;
 }>();
 
 const stat = computed(() => [
@@ -65,11 +65,5 @@ const tableColumns = ref<TableColumnConfig[]>([
   }
 ])
 const data = computed(() => props.data.list || [])
-const emit = defineEmits<{
-  rowClick: [row: Study.StudentVideoRecord]
-}>();
-const handleRowClick = (row: Study.StudentVideoRecord) => {
-  emit('rowClick', row);
-}
 </script>
 <style lang="scss" scoped></style>

+ 12 - 3
src/pagesStudy/pages/exam-start/components/exam-navbar.vue

@@ -29,7 +29,16 @@ const { transferTo, transferBack } = useTransferPage();
 const examPageOptions = inject(EXAM_PAGE_OPTIONS) || {} as Transfer.ExamAnalysisPageOptions;
 const examData = inject(EXAM_DATA) || {} as ReturnType<typeof useExam>;
 const { formatPracticeDuration, practiceDuration } = examData;
-
+const titleMap = {
+  [EnumPaperType.PRACTICE]: '知识点练习',
+  [EnumPaperType.SIMULATED]: '模拟考试',
+  [EnumPaperType.TEST]: '组卷作业',
+}
+const readonlyTitleMap = {
+  [EnumPaperType.PRACTICE]: '练习解析',
+  [EnumPaperType.SIMULATED]: '考试解析',
+  [EnumPaperType.TEST]: '作业解析',
+}
 const isPractice = computed(() => {
   return examPageOptions?.paperType === EnumPaperType.PRACTICE;
 });
@@ -37,9 +46,9 @@ const pageTitle = computed(() => {
   if (examPageOptions) {
     const { name, readonly, paperType } = examPageOptions;
     if (readonly) {
-      return paperType === EnumPaperType.SIMULATED ? '考试解析' : '练习解析';
+      return readonlyTitleMap[paperType as keyof typeof readonlyTitleMap];
     }
-    return paperType === EnumPaperType.SIMULATED ? '模拟考试' : '知识点练习';
+    return titleMap[paperType as keyof typeof titleMap];
   }
   return '练习';
 });

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

@@ -89,7 +89,7 @@ const props = defineProps<{
   readonly?: boolean;
 }>();
 const examData = inject(EXAM_DATA) || {} as ReturnType<typeof useExam>;
-const { doneCount, virtualTotalCount, groupedQuestionList, questionTypeDesc, reset, startTiming, stopTiming, changeIndex, setSubQuestionIndex } = examData;
+const { doneCount, virtualTotalCount, groupedQuestionList, questionTypeDesc, reset, startTiming, stopTiming, changeIndex } = examData;
 const examPageOptions = inject(EXAM_PAGE_OPTIONS) || {} as Transfer.ExamAnalysisPageOptions;
 const isViewMode = computed(() => {
   return examPageOptions?.readonly || false;

+ 14 - 12
src/pagesStudy/pages/exam-start/components/question-item.vue

@@ -1,22 +1,24 @@
 <template>
   <view class="question-item">
     <question-title :question="question" />
-    <question-options :question="question" />
-    <question-result :question="question" />
-    <question-parse :question="question" />
-    <view v-if="question.subQuestions.length">
-      <scroll-view class="w-full h-fit sticky top-0 bg-white py-10 z-1" scroll-x :scroll-into-view="scrollIntoView"
+    <template v-if="!question.hasSubQuestions">
+      <question-options :question="question" />
+      <question-result :question="question" />
+      <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(subIndex)}`"
-            :class="[subIndex === subQuestionIndex ? 'bg-[#EBF9FF] text-primary font-bold' : 'bg-back']"
-            v-for="(subQuestion, subIndex) in question.subQuestions" @click="setSubQuestionIndex(subIndex)">
-            {{ getNo(subIndex) }}
+          <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)">
+            {{ getNo(index) }}
           </view>
         </view>
       </scroll-view>
-      <view v-if="currentSubQuestion" class="mt-20">
-        <question-item :question="currentSubQuestion" />
+      <view v-if="question.subQuestions[question.activeSubIndex]" class="mt-20">
+        <question-item :question="question.subQuestions[question.activeSubIndex]" />
       </view>
     </view>
   </view>
@@ -33,7 +35,7 @@ import { EXAM_DATA, EXAM_PAGE_OPTIONS, EXAM_AUTO_SUBMIT } from '@/types/injectio
 
 const examPageOptions = inject(EXAM_PAGE_OPTIONS) || {} as Transfer.ExamAnalysisPageOptions;
 const examData = inject(EXAM_DATA) || {} as ReturnType<typeof useExam>;
-const { subQuestionIndex, nextQuestion, isAllDone, currentSubQuestion, setSubQuestionIndex } = examData;
+const { subQuestionIndex, setSubQuestionIndex } = examData;
 const examAutoSubmit = inject(EXAM_AUTO_SUBMIT);
 
 const scrollIntoView = ref('')

+ 23 - 17
src/pagesStudy/pages/exam-start/components/question-options.vue

@@ -1,7 +1,7 @@
 <template>
   <view class="question-options">
-    <view class="question-option" v-for="option in question.options" :class="getStyleClass(option)" :key="option.id"
-      @click="handleSelect(option)">
+    <view class="question-option" v-for="option in question.options" :class="getStyleClass(option)"
+      :key="question.id + '_' + option.id" @click="handleSelect(option)">
       <template v-if="!isReadOnly">
         <view v-if="!isOnlySubjective" class="question-option-index">{{ option.no }}</view>
         <view v-else>
@@ -15,8 +15,7 @@
         <view v-else class="question-option-index">{{ option.no }}</view>
       </view>
       <view class="question-option-content">
-        <uv-parse :content="getOptionContent(option)" containerStyle="display:inline"
-          contentStyle="word-break:break-word;"></uv-parse>
+        <mp-html :content="option.name" />
       </view>
     </view>
     <!-- 添加不会选项 -->
@@ -33,14 +32,14 @@
       <ie-button type="primary" size="mini" :round="4" :shadow="false" custom-class="w-160"
         @click="handleSubmit">提交</ie-button>
     </view>
-    <view v-if="!isReadOnly && isOnlySubjective" class="mt-40 bg-[#EBF9FF] p-12 rounded-8">
+    <view v-if="showParseTip" class="mt-40 bg-[#EBF9FF] p-12 rounded-8">
       <view class="rounded-8 bg-white px-10 py-20 text-primary text-24 flex gap-x-6 items-center">
         <uv-icon name="info-circle" color="#31A0FC" size="16" />
         <text>请线下答题,查看解析对比后,选“会”或“不会”</text>
       </view>
       <view class="mt-30 mb-20 text-24 text-white bg-primary w-fit mx-auto px-20 py-12 rounded-full text-center"
         @click="handleShowParse">
-        查看解析
+        {{ question.showParse ? '收起解析' : '查看解析' }}
       </view>
     </view>
   </view>
@@ -78,6 +77,10 @@ const isMultipleChoice = computed(() => {
   return props.question.typeId === EnumQuestionType.MULTIPLE_CHOICE;
 });
 
+const showParseTip = computed(() => {
+  return !isReadOnly.value && isOnlySubjective.value;
+});
+
 const getStyleClass = (option: Study.QuestionOption) => {
   if (!isReadOnly.value) {
     return option.isSelected ? 'question-option-selected' : '';
@@ -111,11 +114,6 @@ const getStyleClass = (option: Study.QuestionOption) => {
   return customClass;
 };
 
-const getOptionContent = (option: Study.QuestionOption) => {
-  // sb 问题,浪费几个小时
-  return option.name.replace(/\s/g, ' ');
-}
-
 // 多选题要手动提交才能认为是作答结束
 const handleSubmit = () => {
   props.question.hasParsed = true;
@@ -129,25 +127,33 @@ const handleNotKnow = () => {
 }
 
 const handleNext = () => {
-  // 如果是正常的练习,默认下一题,如果是背题模式,需要根据是否自动下一题来决定
   console.log('handleNext')
+  // 如果已经完成所有题目,则自动提交
+  if (isAllDone.value) {
+    examAutoSubmit?.();
+    return;
+  }
+  // 如果是正常的练习,默认下一题,如果是背题模式,需要根据是否自动下一题来决定
   if (practiceSettings.value.reviewMode === EnumReviewMode.DURING_ANSWER) {
     console.log(1)
     if (practiceSettings.value.autoNext) {
       console.log(2)
       if (props.question.typeId !== EnumQuestionType.MULTIPLE_CHOICE) {
         console.log(3)
-        nextQuestion();
+        changeNextQuestion();
       }
     }
   } else {
-    nextQuestion();
-  }
-  if (isAllDone.value) {
-    examAutoSubmit?.();
+    changeNextQuestion();
   }
 }
 
+const changeNextQuestion = () => {
+  setTimeout(() => {
+    nextQuestion();
+  }, 250);
+}
+
 const handleSelect = async (option: Study.QuestionOption) => {
   if (isReadOnly.value) {
     return;

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

@@ -4,18 +4,18 @@
     <view v-if="isOnlySubjective" class="mb-20">
       <view class="text-30 text-fore-title font-bold">答案</view>
       <view class="mt-10 text-26 text-fore-light">
-        <uv-parse :content="question.answer2 || '略'"></uv-parse>
+        <mp-html :content="decodeHtmlEntities(question.answer2 || '略')" />
       </view>
     </view>
     <view class="text-30 text-fore-title font-bold">解析</view>
     <view class="mt-10 text-26 text-fore-light">
-      <uv-parse :content="question.parse || '暂无解析'"></uv-parse>
+      <mp-html :content="decodeHtmlEntities(question.parse || '暂无解析')" />
     </view>
   </view>
 </template>
 <script lang="ts" setup>
 import { EnumQuestionType, EnumReviewMode } from '@/common/enum';
-import { useExam } from '@/composables/useExam';
+import { useExam, decodeHtmlEntities } from '@/composables/useExam';
 import { Study, Transfer } from '@/types';
 import { EXAM_DATA, EXAM_PAGE_OPTIONS } from '@/types/injectionSymbols';
 

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

@@ -7,7 +7,7 @@
     </view>
     <text v-if="isSubQuestion" class="text-nowrap text-30">
       <text>{{ getQuestionTitle() }}</text>
-      <text v-if="isSimulation">({{ getScore }}分)</text>
+      <text v-if="isExam">({{ getScore }}分)</text>
       <text v-else>&nbsp;</text>
     </text>
     <uv-parse :content="question.title" containerStyle="display:inline"
@@ -27,15 +27,15 @@ const { questionTypeDesc } = examData;
 const props = defineProps<{
   question: Study.Question;
 }>();
-const isSimulation = computed(() => {
+const isExam = computed(() => {
   const { paperType } = examPageOptions;
-  return paperType === EnumPaperType.SIMULATED;
+  return paperType === EnumPaperType.SIMULATED || paperType === EnumPaperType.TEST;
 });
 const isSubQuestion = computed(() => {
   return props.question.isSubQuestion;
 });
 const showScore = computed(() => {
-  return isSimulation.value;
+  return isExam.value;
 });
 const getScore = computed(() => {
   if (props.question.subQuestions && props.question.subQuestions.length > 0) {

+ 28 - 18
src/pagesStudy/pages/exam-start/exam-start.vue

@@ -37,9 +37,9 @@ const userStore = useUserStore();
 // import { Examinee, ExamPaper, ExamPaperSubmit } from '@/types/study';
 const { prevData, transferBack, transferTo } = useTransferPage<Transfer.ExamAnalysisPageOptions, {}>();
 const examData = useExam();
-const { setQuestionList, questionList, flatQuestionList, setPracticeSettings, setSubQuestionIndex,
-  notDoneCount, isAllDone, nextQuestion, prevQuestion, nextQuestionQuickly, prevQuestionQuickly,
-  practiceDuration, startTiming, stopTiming, changeIndex, setDuration } = examData;
+const { setQuestionList, questionList, flatQuestionList, setSubQuestionIndex,
+  notDoneCount, isAllDone,
+  practiceDuration, startTiming, stopTiming, submit, changeIndex, setDuration } = examData;
 
 //
 const showSwiperTip = ref(false);
@@ -90,10 +90,17 @@ provide(EXAM_PAGE_OPTIONS, prevData.value);
 provide(EXAM_DATA, examData);
 provide(EXAM_AUTO_SUBMIT, autoSubmit);
 
-const isExam = computed(() => {
+const isPracticeExam = computed(() => {
+  return prevData.value.paperType === EnumPaperType.PRACTICE || prevData.value.paperType === EnumPaperType.COURSE;
+});
+
+const isSimulationExam = computed(() => {
   // prevData.value
   return prevData.value.paperType === EnumPaperType.SIMULATED;
 });
+const isTestExam = computed(() => {
+  return prevData.value.paperType === EnumPaperType.TEST;
+});
 
 const isReadOnly = computed(() => {
   return prevData.value.readonly || false;
@@ -139,7 +146,7 @@ const stopTime = () => {
 }
 
 
-const beforeSubmit = () => {
+const beforeSubmit = async () => {
   const text = notDoneCount.value > 0 ? `还有${notDoneCount.value}题未做,确认交卷?` : '是否确认交卷?';
   stopTime();
   uni.$ie.showModal({
@@ -159,10 +166,11 @@ const beforeSubmit = () => {
  * @param tempSave 是否临时保存
  */
 const handleSubmit = (tempSave: boolean = false) => {
-  console.log('handleSubmit', questionList.value)
+  // 执行完后续逻辑
+  submit();
   const msg = tempSave ? '保存中...' : '提交中...';
   uni.$ie.showLoading(msg);
-  setTimeout(() => {
+  setTimeout(async () => {
     const params = {
       ...paperData.value,
       questions: questionList.value.map(item => {
@@ -184,16 +192,17 @@ const handleSubmit = (tempSave: boolean = false) => {
       duration: practiceDuration.value
     } as Study.ExamPaperSubmit;
     console.log('提交试卷参数', params)
-    commitExamineePaper(params);
-    if (isExam.value) {
+    await commitExamineePaper(params);
+    if (isSimulationExam.value || isTestExam.value) {
       if (!tempSave) {
         setTimeout(async () => {
           uni.$ie.hideLoading();
           await nextTick();
           transferTo('/pagesStudy/pages/simulation-analysis/simulation-analysis', {
             data: {
-              examineeId: examineeId.value
-            },
+              examineeId: examineeId.value,
+              paperType: prevData.value.paperType
+            } as Transfer.SimulationAnalysisPageOptions,
             type: 'redirectTo'
           });
         }, 2500);
@@ -203,17 +212,18 @@ const handleSubmit = (tempSave: boolean = false) => {
           transferBack();
         });
       }
-    } else {
+    } else if (isPracticeExam.value) {
       if (!tempSave) {
         setTimeout(async () => {
           uni.$ie.hideLoading();
           await nextTick();
           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
-            },
+            } as Transfer.PracticeResultPageOptions,
             type: 'redirectTo'
           });
         }, 2500);
@@ -224,7 +234,7 @@ const handleSubmit = (tempSave: boolean = false) => {
         });
       }
     }
-  }, 1000);
+  }, 300);
 }
 
 /**
@@ -283,7 +293,7 @@ const loadPracticeData = async () => {
   totalExamTime.value = Number.MAX_SAFE_INTEGER;
   combinePaperData(data, paperType);
 }
-const loadSimulationData = async () => {
+const loadExamData = async () => {
   const { paperType, readonly, simulationInfo } = prevData.value;
   let data: Study.Examinee;
   if (simulationInfo?.examineeId) {
@@ -359,10 +369,10 @@ const handleGuideClose = () => {
 const loadData = async () => {
   uni.$ie.showLoading();
   const { paperType } = prevData.value;
-  if (paperType === EnumPaperType.PRACTICE) {
+  if (paperType === EnumPaperType.PRACTICE || paperType === EnumPaperType.COURSE) {
     loadPracticeData();
-  } else if (paperType === EnumPaperType.SIMULATED) {
-    loadSimulationData();
+  } else if (paperType === EnumPaperType.SIMULATED || paperType === EnumPaperType.TEST) {
+    loadExamData();
   }
 };
 onLoad(() => {

+ 52 - 23
src/pagesStudy/pages/homework/homework.vue

@@ -2,18 +2,18 @@
   <ie-page bg-color="#F6F8FA" :fix-height="true">
     <ie-navbar title="组卷作业" />
     <view class="bg-white">
-      <uv-tabs :list="list" :activeStyle="activeStyle" :inactiveStyle="inactiveStyle" :scrollable="false"
-        @change="handleTabChange"></uv-tabs>
+      <uv-tabs :current="currentTab" :list="tabList" :activeStyle="activeStyle" :inactiveStyle="inactiveStyle"
+        :scrollable="false" @change="handleTabChange"></uv-tabs>
     </view>
     <view class="flex-1 min-h-1 relative">
       <view class="absolute inset-0">
-        <z-paging ref="paging" :fixed="false" v-model="dataList" :safe-area-inset-bottom="true"
-          :use-safe-area-placeholder="true" :auto="false" empty-view-text="暂无发布组卷作业~"
+        <z-paging ref="pagingRef" :fixed="false" v-model="dataList" :safe-area-inset-bottom="true"
+          :use-safe-area-placeholder="true" :auto="false" bg-color="#F6F8FA" empty-view-text="暂无发布组卷作业~"
           :empty-view-img="resolvePath('/pagesStudy/static/image/icon-empty.png')" :empty-view-style="emptyViewStyle"
           :empty-view-img-style="emptyViewImgStyle" :empty-view-title-style="emptyViewTextStyle" @query="handleQuery">
-          <view v-if="dataList.length > 0" class="pt-10">
-            <view class="card-item" v-for="item in dataList" :key="item.id">
-              <activity-item :data="item" />
+          <view v-if="dataList.length > 0" class="">
+            <view class="mt-20" v-for="item in dataList" :key="item.id">
+              <paper-work-item :data="item" />
             </view>
           </view>
         </z-paging>
@@ -24,6 +24,11 @@
 
 <script lang="ts" setup>
 import { useImage } from '@/hooks/useImage';
+import PaperWorkItem from '@/pagesStudy/components/paper-work-item.vue';
+import { getPaperWorkList, getPaperWorkStatistic } from '@/api/modules/study';
+import { Study } from '@/types';
+import { EnumPaperWorkState } from '@/common/enum';
+
 const { resolvePath } = useImage();
 const activeStyle = {
   color: '#1A1A1A'
@@ -43,39 +48,63 @@ const emptyViewTextStyle = {
   fontSize: '30rpx',
   marginTop: '40rpx'
 }
-const list = [
+const tabList = ref([
   {
     name: '全部(0)',
-    value: 'all'
+    value: '全部',
+    state: undefined
   },
   {
     name: '未完成(0)',
-    value: 'special'
+    value: '未完成',
+    state: EnumPaperWorkState.NOT_COMPLETED
   },
   {
     name: '已完成(0)',
-    value: 'special'
+    value: '已完成',
+    state: EnumPaperWorkState.COMPLETED
   }
-]
-const dataList = ref<any[]>([]);
-const paging = ref<any>(null);
+])
+const dataList = ref<Study.PaperWork[]>([]);
+const pagingRef = ref();
+const currentTab = ref(0);
 const handleTabChange = (item: any) => {
-  console.log(item);
-  paging.value.reload();
+  currentTab.value = item.index;
+  pagingRef.value.reload();
 }
 const handleQuery = (page: number, pageSize: number) => {
   uni.$ie.showLoading();
-  setTimeout(() => {
+  const params = {} as { state?: EnumPaperWorkState };
+  const state = tabList.value[currentTab.value].state;
+  if (state) {
+    params.state = state;
+  }
+  getPaperWorkList(params).then(res => {
+    pagingRef.value.complete(res.rows);
+  }).catch(err => {
+    pagingRef.value.complete(false);
+  }).finally(() => {
     uni.$ie.hideLoading();
-    paging.value.complete([]);
-  }, 1000);
+  });
+
 }
-const click = (item: any) => {
-  console.log(item);
+const loadStatistic = () => {
+  getPaperWorkStatistic().then(res => {
+    tabList.value.forEach(item => {
+      const cate = res.data.find(c => c.hasOwnProperty(item.value))
+      if (cate) {
+        item.name = `${item.value}(${cate[item.value]})`;
+      }
+    });
+  });
 }
 
-onMounted(() => {
-  paging.value.reload();
+loadStatistic();
+
+onShow(() => {
+  nextTick(() => {
+    pagingRef.value.refresh();
+  });
 });
 </script>
 

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

@@ -30,7 +30,10 @@
 import { useTransferPage } from '@/hooks/useTransferPage';
 import { getStudyPlan, getDirectedSchool } from '@/api/modules/study';
 import { OPEN_VIP_POPUP } from '@/types/injectionSymbols';
+import { useUserStore } from '@/store/userStore';
+
 const { transferTo } = useTransferPage();
+const userStore = useUserStore();
 const openVipPopup = inject(OPEN_VIP_POPUP);
 const handleOpenPlan = async () => {
   const { data } = await getStudyPlan();
@@ -45,7 +48,11 @@ const handleOpenPlan = async () => {
   }
 };
 const handleTest = () => {
-
+  if (userStore.isVip || userStore.isTeacher) {
+    transferTo('/pagesStudy/pages/textbooks-practice/textbooks-practice');
+  } else {
+    openVipPopup?.();
+  }
 };
 </script>
 <style lang="scss" scoped></style>

+ 20 - 4
src/pagesStudy/pages/index/compoentns/index-test.vue

@@ -41,7 +41,13 @@
 import { useTransferPage } from '@/hooks/useTransferPage';
 import { DirectedSchool, SelectedUniversityMajor, SimulationTestInfo, SimulationTestOptions } from '@/types/study';
 import { getSimulationInfo } from '@/api/modules/study';
+import { useUserStore } from '@/store/userStore';
+
+const userStore = useUserStore();
+const { hasDirectedSchool, directedSchoolList } = toRefs(userStore);
+const firstDirectedSchool = computed(() => directedSchoolList.value[0] || {});
 const { transferTo } = useTransferPage();
+
 type Props = {
   directedSchool: DirectedSchool;
 }
@@ -60,10 +66,20 @@ const handleSelectCollege = () => {
     data: {}
   });
 }
-const handleStartTest = () => {
-  transferTo('/pagesStudy/pages/simulation-entry/simulation-entry', {
-    data: props.directedSchool
-  });
+const handleStartTest = async () => {
+  const notice = firstDirectedSchool.value.notice;
+  if (notice) {
+    await uni.$ie.showModal({
+      title: '提示',
+      content: notice,
+      showCancel: false,
+      confirmText: '知道了'
+    });
+  } else {
+    transferTo('/pagesStudy/pages/simulation-entry/simulation-entry', {
+      data: props.directedSchool
+    });
+  }
 }
 const loadData = () => {
   getSimulationInfo().then(res => {

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

@@ -68,13 +68,16 @@ 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 { EnumDictName, EnumExamType } from '@/common/enum';
+import { EnumDictName, EnumExamType, EnumUserRole } from '@/common/enum';
 import { useUserStore } from '@/store/userStore';
 import { useTransferPage } from '@/hooks/useTransferPage';
 import IePage from '@/components/ie-page/ie-page.vue';
+import { useAuth } from '@/hooks/useAuth';
 
 const { transferTo } = useTransferPage();
 const userStore = useUserStore();
+const { hasPermission } = useAuth();
+
 const hasTestAndRecord = computed(() => userStore.getExamType !== EnumExamType.VHS);
 // 通过 ref 获取 ie-page 组件实例
 const iePageRef = ref<InstanceType<typeof IePage>>();
@@ -93,23 +96,24 @@ const handlePracticeDirected = async () => {
     addTarget();
     return;
   }
-  transferTo('/pagesStudy/pages/knowledge-practice/knowledge-practice', {
-    data: {
-      directed: true
-    }
-  });
+  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) {
-    const notice = firstDirectedSchool.value.notice;
-    if (notice) {
-      await uni.$ie.showModal({
-        title: '提示',
-        content: notice,
-        showCancel: false,
-        confirmText: '知道了'
-      });
-    }
     transferTo('/pagesStudy/pages/targeted-setting/targeted-setting');
   } else {
     addTarget();

+ 4 - 3
src/pagesStudy/pages/knowledge-practice-detail/knowledge-practice-detail.vue

@@ -50,7 +50,7 @@ const { prevData, transferTo } = useTransferPage<Transfer.PracticeResultPageOpti
 const rightRate = computed(() => {
   const { totalCount = 0, wrongCount = 0 } = examineeData.value || {};
   const rate = (totalCount - wrongCount) / totalCount * 100;
-  return rate < 0.1 ? 0.1 : Number(rate.toFixed(1));
+  return Number(rate.toFixed(1));
 });
 const paperName = computed(() => {
   return '知识点练习-' + prevData.value.name;
@@ -81,7 +81,7 @@ const handleQuestionDetail = (item: Study.Question) => {
   transferTo('/pagesStudy/pages/exam-start/exam-start', {
     data: {
       name: paperName.value,
-      paperType: EnumPaperType.PRACTICE,
+      paperType: prevData.value.paperType || EnumPaperType.PRACTICE,
       readonly: true,
       questionId: item.id,
       practiceInfo: {
@@ -96,12 +96,13 @@ const handleQuestionDetail = (item: Study.Question) => {
 const handleStartPractice = () => {
   const { knowledgeId } = examineeData.value || {};
   if (!knowledgeId) {
+    console.error('knowledgeId is null');
     return;
   }
   transferTo('/pagesStudy/pages/exam-start/exam-start', {
     data: {
       name: paperName.value,
-      paperType: EnumPaperType.PRACTICE,
+      paperType: prevData.value.paperType || EnumPaperType.PRACTICE,
       practiceInfo: {
         name: prevData.value.name,
         relateId: knowledgeId,

+ 35 - 27
src/pagesStudy/pages/knowledge-practice-history/knowledge-practice-history.vue

@@ -1,25 +1,29 @@
 <template>
   <ie-page bg-color="#F6F8FA" :fix-height="true">
-    <ie-navbar title="刷题记录" />
-    <view class="mt-20">
-      <view v-for="(item, index) in historyList" :key="index"
-        class="bg-white px-40 py-30 flex items-center sibling-border-top">
-        <view class="flex-1">
-          <view class="text-28">
-            <text class=" text-fore-light">知识点:</text>
-            <text class="text-fore-title">{{ item.paperName }}</text>
+    <z-paging ref="pagingRef" v-model="historyList" :loading-more-enabled="false" @query="handleQuery">
+      <template #top>
+        <ie-navbar title="刷题记录" />
+      </template>
+      <view class="mt-20">
+        <view v-for="(item, index) in historyList" :key="index"
+          class="bg-white px-40 py-30 flex items-center sibling-border-top">
+          <view class="flex-1">
+            <view class="text-28">
+              <text class=" text-fore-light">知识点:</text>
+              <text class="text-fore-title">{{ item.paperName }}</text>
+            </view>
+            <view class="mt-10 text-28">
+              <text class=" text-fore-light">完成时间:</text>
+              <text class="text-fore-title">{{ item.endTime }}</text>
+            </view>
           </view>
-          <view class="mt-10 text-28">
-            <text class=" text-fore-light">完成时间:</text>
-            <text class="text-fore-title">{{ item.endTime }}</text>
+          <view class="text-24 text-white bg-primary w-fit px-40 py-12 rounded-full text-center"
+            @click="handleViewHistory(item)">
+            查看
           </view>
         </view>
-        <view class="text-24 text-white bg-primary w-fit px-40 py-12 rounded-full text-center"
-          @click="handleViewHistory(item)">
-          查看
-        </view>
       </view>
-    </view>
+    </z-paging>
   </ie-page>
 </template>
 <script lang="ts" setup>
@@ -28,37 +32,41 @@ import { useTransferPage } from '@/hooks/useTransferPage';
 import { getPracticeHistory } from '@/api/modules/study';
 import { Study } from '@/types';
 import { Transfer } from '@/types';
+import { EnumPaperType } from '@/common/enum';
 const { prevData, transferTo } = useTransferPage<{}, Transfer.PracticeResultPageOptions>();
 const { baseStickyTop } = useNavbar();
 const historyList = ref<Study.PracticeHistory[]>([]);
 const handleViewHistory = (value: Study.PracticeHistory) => {
   transferTo('/pagesStudy/pages/knowledge-practice-detail/knowledge-practice-detail', {
     data: {
+      paperType: EnumPaperType.PRACTICE,
       examineeId: value.examineeId,
       name: value.paperName,
       directed: value.directed === 1
     }
   });
 }
-const loadData = async () => {
-  uni.$ie.showLoading();
-  try {
-    const { rows } = await getPracticeHistory({
-      directed: true
-    });
-    historyList.value = rows.map(item => {
+
+const pagingRef = ref();
+const handleQuery = (pageNum: number, pageSize: number) => {
+  getPracticeHistory({
+    pageNum,
+    pageSize
+  }).then(res => {
+    historyList.value = res.rows.map(item => {
       return {
         ...item,
         endTime: uni.$uv.timeFormat(item.endTime, 'yyyy-mm-dd hh:MM:ss')
       } as Study.PracticeHistory;
     });
-  } finally {
-    uni.$ie.hideLoading();
-  }
+    pagingRef.value.complete(res.rows)
+  }).catch(() => {
+    pagingRef.value.complete(false);
+  });
 }
 onLoad(() => {
   console.log(prevData.value)
-  loadData();
+  // loadData();
 });
 </script>
 <style lang="scss" scoped></style>

+ 33 - 21
src/pagesStudy/pages/knowledge-practice/knowledge-practice.vue

@@ -1,17 +1,21 @@
 <template>
-  <ie-page ref="iePageRef">
-    <ie-navbar :title="pageTitle" />
-    <uv-tabs :list="subjectList" key-name="subjectName" @click="handleChangeTab" :scrollable="true"></uv-tabs>
-    <view class="px-30 py-16 bg-back">
-      <view class="flex items-center justify-end gap-x-4" @click="handleViewHistory">
-        <uv-icon name="clock" size="16" color="#31A0FC"></uv-icon>
-        <text class="text-28 text-primary">查看记录</text>
-        <uv-icon name="arrow-right" size="16" color="#31A0FC"></uv-icon>
+  <ie-page>
+    <z-paging ref="pagingRef" v-model="treeData" :loading-more-enabled="false" :auto="false" @query="loadKnowledgeList">
+      <template #top>
+        <ie-navbar :title="pageTitle" />
+        <uv-tabs :list="subjectList" key-name="subjectName" @click="handleChangeTab" :scrollable="true"></uv-tabs>
+        <view class="px-30 py-16 bg-back">
+          <view class="flex items-center justify-end gap-x-4" @click="handleViewHistory">
+            <uv-icon name="clock" size="16" color="#31A0FC"></uv-icon>
+            <text class="text-28 text-primary">查看记录</text>
+            <uv-icon name="arrow-right" size="16" color="#31A0FC"></uv-icon>
+          </view>
+        </view>
+      </template>
+      <view class="px-40">
+        <knowledgeTree :tree-data="treeData" @start-practice="handleStartPractice" />
       </view>
-    </view>
-    <view class="px-40">
-      <knowledgeTree :tree-data="treeData" @start-practice="handleStartPractice" />
-    </view>
+    </z-paging>
   </ie-page>
 </template>
 
@@ -21,12 +25,16 @@ import { useTransferPage } from '@/hooks/useTransferPage';
 import { getSubjectList, getKnowledgeList } from '@/api/modules/study';
 import knowledgeTree from '@/pagesStudy/components/knowledge-tree.vue';
 import * as Study from '@/types/study';
-import { EnumPaperType } from '@/common/enum';
+import { EnumPaperType, EnumUserRole } from '@/common/enum';
 import { useUserStore } from '@/store/userStore';
+import { useAuth } from '@/hooks/useAuth';
+
 const { prevData, transferTo } = useTransferPage();
 const currentSubjectIndex = ref<number>(-1);
 const userStore = useUserStore();
-const iePageRef = ref<InstanceType<typeof IePage>>();
+const pagingRef = ref();
+
+const { hasPermission } = useAuth();
 const pageTitle = computed(() => {
   if (prevData.value.directed) {
     return '定向刷题';
@@ -62,16 +70,18 @@ const loadKnowledgeList = async () => {
       directed: prevData.value.directed
     });
     treeData.value = data as Study.KnowledgeNode[];
+    pagingRef.value.complete(data);
   } catch (error) {
     console.log(error);
+    pagingRef.value.complete(false);
   } finally {
     uni.$ie.hideLoading();
   }
 }
 
 const handleStartPractice = async (node: Study.KnowledgeNode) => {
-  const isVip = await userStore.checkVip();
-  if (isVip) {
+  const hasAuth = hasPermission([EnumUserRole.VIP, EnumUserRole.AGENT, EnumUserRole.TEACHER]);
+  if (hasAuth) {
     transferTo('/pagesStudy/pages/exam-start/exam-start', {
       data: {
         name: '知识点练习-' + node.name,
@@ -83,15 +93,13 @@ const handleStartPractice = async (node: Study.KnowledgeNode) => {
         },
       }
     });
-  } else {
-    iePageRef.value?.showVipPopup();
   }
 }
 
 watch(() => currentSubjectIndex.value, () => {
-  loadKnowledgeList();
+  pagingRef.value.reload();
 }, {
-  immediate: true
+  immediate: false
 });
 
 const loadData = async () => {
@@ -107,7 +115,11 @@ onLoad(() => {
   loadData();
 });
 onShow(() => {
-  loadKnowledgeList();
+  nextTick(() => {
+    if (subjectList.value.length > 0) {
+      pagingRef.value.refresh();
+    }
+  });
 });
 </script>
 

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

@@ -15,7 +15,7 @@
         <view class="mt-5 text-28 text-fore-light">得分</view>
       </view>
       <view class="flex-1 text-center">
-        <view class="text-40 text-fore-title font-bold">{{ stats.rate }}%</view>
+        <view class="text-40 text-fore-title font-bold">{{ rightRate }}%</view>
         <view class="mt-5 text-28 text-fore-light">正确率</view>
       </view>
     </view>
@@ -74,6 +74,11 @@ const stats = computed(() => {
 const totalQuestions = computed(() => {
   return (flatQuestionList.value || []).length;
 });
+const rightRate = computed(() => {
+  const { totalCount = 0, wrongCount = 0 } = props.data || {};
+  return Math.max(Math.round((totalCount - wrongCount) / totalCount * 100), 1);
+});
+
 const filterTypes = ref<('correct' | 'incorrect' | 'unanswered')[]>(['correct', 'incorrect', 'unanswered']);
 const filteredQuestionList = computed(() => {
   const list: QuestionItem[] = [];

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

@@ -44,12 +44,23 @@ import { Study, Transfer } from '@/types';
 import { EnumPaperType, EnumQuestionType } from '@/common/enum';
 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 paperName = computed(() => {
-  return '模拟考试-' + examineeData.value?.subjectName;
+  return titleMap[prevData.value.paperType as keyof typeof titleMap] + '-' + examineeData.value?.subjectName;
 });
 const rightRate = computed(() => {
   const { totalCount = 0, wrongCount = 0 } = examineeData.value || {};
-  return Math.round((totalCount - wrongCount) / totalCount * 100) || 0;
+  return Math.max(Math.round((totalCount - wrongCount) / totalCount * 100), 1);
 });
 
 const formatTime = (time: number) => {
@@ -69,13 +80,13 @@ const formatTime = (time: number) => {
 const handleDetail = (item: Study.Question) => {
   if (!examineeData.value) {
     return;
-  }
+  }  
   transferTo('/pagesStudy/pages/exam-start/exam-start', {
     data: {
       name: paperName.value,
       readonly: true,
       questionId: item.id,
-      paperType: EnumPaperType.SIMULATED,
+      paperType: prevData.value.paperType,
       simulationInfo: {
         examineeId: examineeData.value.examineeId
       }

+ 6 - 0
src/pagesStudy/pages/simulation-entry/simulation-entry.vue

@@ -39,6 +39,8 @@ import iconChinese from '@/pagesStudy/static/image/icon-chinese.png';
 import iconMath from '@/pagesStudy/static/image/icon-math.png';
 import iconForeign from '@/pagesStudy/static/image/icon-foreign.png';
 import iconSkill from '@/pagesStudy/static/image/icon-skill.png';
+import iconPhysics from '@/pagesStudy/static/image/icon-physics.png';
+import iconPolitics from '@/pagesStudy/static/image/icon-politics.png';
 import { Study } from '@/types';
 const { transferTo, prevData } = useTransferPage();
 const subjects = ref<Study.SimulationExamSubject[]>([]);
@@ -62,6 +64,10 @@ const getIcon = (subjectName: string) => {
     return iconMath;
   } else if (subjectName === '外语' || /.语/.test(subjectName)) {
     return iconForeign;
+  } else if (subjectName === '物理') {
+    return iconPhysics;
+  } else if (subjectName === '政治') {
+    return iconPolitics;
   } else {
     return iconSkill;
   }

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

@@ -33,7 +33,7 @@
             </view>
             <view class="mt-40 flex items-center justify-between">
               <text class="text-28 text-fore-light">考试题型</text>
-              <text class="text-28 text-fore-title">{{ questionTypes }}</text>
+              <text class="text-28 text-fore-title">{{ questionTypes || '--' }}</text>
             </view>
           </view>
           <view class="mt-36 rounded-15 bg-back px-40 py-36 flex">

+ 84 - 0
src/pagesStudy/pages/study-exam-simulated-class/study-exam-simulated-class.vue

@@ -0,0 +1,84 @@
+<template>
+  <ie-page bg-color="#F6F8FA" :fix-height="true">
+    <ie-navbar :title="pageTitle" />
+    <view class="h-20"></view>
+    <view class="bg-white flex-1 min-h-1">
+      <view class="pt-30 px-30 text-30 text-fore-title font-bold">{{ prevData.name }}班的做卷详情</view>
+      <view class="p-30">
+        <ie-table :tableColumns="tableColumns" :data="tableData" :cellStyle="cellStyle">
+          <template #name="{ item }">
+            <text class="font-bold">{{ item.name }}</text>
+          </template>
+          <template #total="{ item }">
+            <text class="font-bold">{{ item.total }}</text>
+          </template>
+          <template #action="{ item }">
+            <text class="text-30 text-primary font-bold" @click="handleRowClick(item)">查看</text>
+          </template>
+        </ie-table>
+      </view>
+    </view>
+  </ie-page>
+</template>
+<script lang="ts" setup>
+import { useTransferPage } from '@/hooks/useTransferPage';
+import { Study, TableColumnConfig } from '@/types';
+import { getStudentExamRecord } from '@/api/modules/study';
+import { CSSProperties } from 'vue';
+
+const { prevData, transferTo } = useTransferPage();
+const cellStyle: CSSProperties = {
+  padding: '30rpx 20rpx'
+}
+const tableData = ref<Study.StudentExamRecord[]>([]);
+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 pageTitle = computed(() => {
+  return '模考-班级详情';
+});
+
+const handleRowClick = (row: Study.StudentExamRecord) => {
+  transferTo('/pagesStudy/pages/study-exam-simulated-student/study-exam-simulated-student', {
+    data: {
+      studentId: row.id,
+      name: row.name
+    }
+  });
+}
+const loadData = async () => {
+  uni.$ie.showLoading();
+  try {
+    const res = await getStudentExamRecord({
+      classId: prevData.value.classId,
+    });
+    tableData.value = res.data;
+  } finally {
+    uni.$ie.hideLoading();
+  }
+}
+onLoad(() => {
+  loadData();
+});
+</script>
+<style lang="scss" scoped></style>

+ 84 - 0
src/pagesStudy/pages/study-exam-simulated-detail/study-exam-simulated-detail.vue

@@ -0,0 +1,84 @@
+<template>
+  <ie-page bg-color="#F6F8FA" :fix-height="true">
+    <ie-navbar :title="pageTitle" />
+    <view class="h-20"></view>
+    <view class="bg-white flex-1 min-h-1">
+      <view class="pt-30 px-30 text-30 text-fore-title font-bold">{{ prevData.name }}的{{ prevData.subjectName }}试卷详情</view>
+      <view class="p-30">
+        <ie-table :tableColumns="tableColumns" :data="tableData" :cellStyle="cellStyle">
+          <template #name="{ item }">
+            <text class="font-bold">{{ item.name }}</text>
+          </template>
+          <template #total="{ item }">
+            <text class="font-bold">{{ item.total }}</text>
+          </template>
+          <template #rate="{ item }">
+            <text :class="[item.rate < 70 ? 'text-danger' : 'text-fore-title']">{{ item.rate }}%</text>
+          </template>
+        </ie-table>
+      </view>
+    </view>
+  </ie-page>
+</template>
+<script lang="ts" setup>
+import { useTransferPage } from '@/hooks/useTransferPage';
+import { Study, TableColumnConfig } from '@/types';
+import { getStudentExamSubjectDetail } from '@/api/modules/study';
+import { CSSProperties } from 'vue';
+
+const { prevData, transferTo } = useTransferPage();
+const cellStyle: CSSProperties = {
+  padding: '30rpx 20rpx'
+}
+const tableData = ref<Study.StudentExamRecord[]>([]);
+const tableColumns: TableColumnConfig[] = [
+  {
+    prop: 'name',
+    label: '状态',
+    flex: 1,
+    slot: 'name',
+    headerAlign: 'center',
+    align: 'center'
+  },
+  {
+    prop: 'total',
+    label: '做题量',
+    flex: 1,
+    slot: 'total'
+  },
+  {
+    prop: 'rate',
+    label: '正确率',
+    flex: 1,
+    slot: 'rate'
+  }
+];
+
+const pageTitle = computed(() => {
+  return '模考-试卷详情';
+});
+
+const handleRowClick = (row: Study.StudentExamRecord) => {
+  transferTo('/pagesStudy/pages/study-exam-simulated-detail/study-exam-simulated-detail', {
+    data: {
+      studentId: row.id,
+      name: row.name
+    }
+  });
+}
+const loadData = async () => {
+  uni.$ie.showLoading();
+  try {
+    const res = await getStudentExamSubjectDetail({
+      examineeId: prevData.value.examineeId,
+    });
+    tableData.value = res.data;
+  } finally {
+    uni.$ie.hideLoading();
+  }
+}
+onLoad(() => {
+  loadData();
+});
+</script>
+<style lang="scss" scoped></style>

+ 85 - 0
src/pagesStudy/pages/study-exam-simulated-student/study-exam-simulated-student.vue

@@ -0,0 +1,85 @@
+<template>
+  <ie-page bg-color="#F6F8FA" :fix-height="true">
+    <ie-navbar :title="pageTitle" />
+    <view class="h-20"></view>
+    <view class="bg-white flex-1 min-h-1">
+      <view class="pt-30 px-30 text-30 text-fore-title font-bold">{{ prevData.name }}的做卷详情</view>
+      <view class="p-30">
+        <ie-table :tableColumns="tableColumns" :data="tableData" :cellStyle="cellStyle">
+          <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>
+    </view>
+  </ie-page>
+</template>
+<script lang="ts" setup>
+import { useTransferPage } from '@/hooks/useTransferPage';
+import { Study, TableColumnConfig } from '@/types';
+import { getStudentExamSubjectRecord } from '@/api/modules/study';
+import { CSSProperties } from 'vue';
+
+const { prevData, transferTo } = useTransferPage();
+const cellStyle: CSSProperties = {
+  padding: '30rpx 20rpx'
+}
+const tableData = ref<Study.StudentExamRecord[]>([]);
+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 pageTitle = computed(() => {
+  return '模考-做卷详情';
+});
+
+const handleRowClick = (row: Study.StudentExamRecord) => {
+  transferTo('/pagesStudy/pages/study-exam-simulated-detail/study-exam-simulated-detail', {
+    data: {
+      examineeId: row.id,
+      name: prevData.value.name,
+      subjectName: row.name
+    }
+  });
+}
+const loadData = async () => {
+  uni.$ie.showLoading();
+  try {
+    const res = await getStudentExamSubjectRecord({
+      studentId: prevData.value.studentId,
+    });
+    tableData.value = res.data;
+  } finally {
+    uni.$ie.hideLoading();
+  }
+}
+onLoad(() => {
+  loadData();
+});
+</script>
+<style lang="scss" scoped></style>

+ 252 - 0
src/pagesStudy/pages/study-history/components/exam-history-paperwork.vue

@@ -0,0 +1,252 @@
+<template>
+  <view>
+    <view class="w-full flex mb-30 justify-between">
+      <view class="max-w-180">
+        <ie-picker ref="pickerRef" v-model="queryForm.buildType" :list="buildTypeList" placeholder="类型" title="类型"
+          icon="arrow-down" key-label="name" key-value="value" :fontSize="28"
+          :placeholder-style="placeholderStyle"></ie-picker>
+      </view>
+      <view class="max-w-180">
+        <ie-picker ref="pickerRef" v-model="queryForm.batchId" :list="batchList" placeholder="批次" title="批次"
+          icon="arrow-down" key-label="name" key-value="batchId" :fontSize="28"
+          :placeholder-style="placeholderStyle"></ie-picker>
+      </view>
+      <view class="max-w-180">
+        <ie-picker ref="pickerRef" v-model="queryForm.classId" :list="classList" placeholder="班级" title="班级"
+          icon="arrow-down" key-label="name" key-value="classId" :fontSize="28"
+          :placeholder-style="placeholderStyle"></ie-picker>
+      </view>
+      <view class="max-w-180">
+        <ie-picker ref="pickerRef" v-model="queryForm.subjectId" :list="subjectList" placeholder="科目" title="科目"
+          icon="arrow-down" key-label="subjectName" key-value="subjectId" :fontSize="28"
+          :placeholder-style="placeholderStyle"></ie-picker>
+      </view>
+    </view>
+    <ie-table :tableColumns="tableColumns" :data="tableData" :cellStyle="cellStyle">
+      <template #name="{ item }">
+        <text class="font-bold">{{ getStatusText(item.buildStatus) }}</text>
+      </template>
+      <template #total="{ item }">
+        <text class="font-bold">{{ item.count || 0 }}</text>
+      </template>
+      <template #status="{ item }">
+        <text class="text-30 text-primary font-bold" @click="handleRowClick(item)">查看</text>
+      </template>
+    </ie-table>
+  </view>
+  <ie-popup :title="popupTitle" ref="popupRef" mode="bottom" :showToolbar="false">
+    <view class="p-30 relative">
+      <view class="text-center text-30 font-bold">{{ popupTitle }}</view>
+      <uv-icon name="close" size="18" color="#333" class="absolute top-34 right-20" @click="popupRef.close()" />
+    </view>
+    <view class="h-[400px]">
+      <scroll-view scroll-y class="h-full">
+        <view class="px-20 py-4">
+          <ie-table :tableColumns="tableColumns2" :data="tableData2" :cellStyle="cellStyle" @rowClick="handleRowClick">
+            <template #status="{ item }">
+              <text class="text-30 text-primary font-bold" @click="handleRowClick(item)">查看</text>
+            </template>
+          </ie-table>
+        </view>
+      </scroll-view>
+    </view>
+  </ie-popup>
+</template>
+<script lang="ts" setup>
+import { Study, TableColumnConfig } from '@/types';
+import { CSSProperties } from 'vue';
+import { getBatchList, getTeachClassList, getTeacherSubjectList, getTeacherTestRecord, getTeacherTestRecordDetail, getTeacherTestRecordCondition } from '@/api/modules/study';
+import { EnumPaperBuildType, EnumPaperWorkState } from '@/common/enum';
+
+const cellStyle: CSSProperties = {
+  padding: '30rpx 20rpx'
+}
+const queryForm = ref<Partial<Study.PaperWorkRecordQuery>>({});
+const buildTypeList = ref([
+  {
+    name: '定向智能',
+    value: EnumPaperBuildType.EXACT_INTELLIGENT
+  },
+  {
+    name: '全量智能',
+    value: EnumPaperBuildType.FULL_INTELLIGENT
+  },
+  {
+    name: '定向手动',
+    value: EnumPaperBuildType.EXACT_HAND
+  },
+  {
+    name: '全量手动',
+    value: EnumPaperBuildType.FULL_HAND
+  }
+]);
+const buildStatusList = ref([
+  {
+    name: '未定向未组卷',
+    value: 10
+  },
+  {
+    name: '未组卷',
+    value: 20
+  },
+  {
+    name: '组卷未完成',
+    value: 30
+  },
+  {
+    name: '组卷已完成',
+    value: 40
+  }
+]);
+const popupTitle = ref('');
+const classList = ref<Study.TeachClass[]>([]);
+const batchList = ref<Study.Batch[]>([]);
+const subjectList = ref<Study.Subject[]>([]);
+const pickerRef = ref();
+const placeholderStyle = {
+  color: '#1A1A1A'
+}
+const tableData = ref<Study.PaperWorkRecord[]>([]);
+const tableColumns: TableColumnConfig[] = [
+  {
+    prop: 'name',
+    label: '状态',
+    flex: 1,
+    slot: 'name',
+    headerAlign: 'center',
+    align: 'center'
+  },
+  {
+    prop: 'total',
+    label: '人数',
+    flex: 1,
+    slot: 'total'
+  },
+  {
+    prop: 'status',
+    label: '操作',
+    flex: 1,
+    slot: 'status'
+  }
+];
+const tableColumns2: TableColumnConfig[] = [
+  {
+    prop: 'className',
+    label: '班级',
+    flex: 1,
+    headerAlign: 'center',
+    align: 'center'
+  },
+  {
+    prop: 'nickName',
+    label: '姓名',
+    flex: 1
+  },
+  {
+    prop: 'state',
+    label: '状态',
+    flex: 1,
+    headerAlign: 'right',
+    align: 'right',
+  }
+];
+const tableData2 = ref<Study.PaperWorkRecordDetail[]>([]);
+
+const getStatusText = (status: number | null) => {
+  if (!status) {
+    return '总人数';
+  }
+  const statusMap = {
+    10: '未定向未组卷',
+    20: '未组卷',
+    30: '组卷未完成',
+    40: '组卷已完成'
+  }
+  return statusMap[status as keyof typeof statusMap];
+}
+
+const popupRef = ref();
+const handleRowClick = async (row: Study.PaperWorkRecord) => {
+  if (!row.count) {
+    uni.$ie.showToast('暂无数据');
+    return;
+  }
+  uni.$ie.showLoading();
+  try {
+    const params = {
+      ...queryForm.value
+    } as Study.PaperWorkRecordQuery;
+    if (row.buildStatus !== null) {
+      params.buildStatus = row.buildStatus;
+    } else {
+      delete (params as any).buildStatus;
+    }
+    await getTeacherTestRecordDetail(params).then(res => {
+      tableData2.value = res.data;
+    });
+  } finally {
+    uni.$ie.hideLoading();
+  }
+  popupTitle.value = getStatusText(row.buildStatus);
+  popupRef.value.open();
+}
+
+const loadData = async () => {
+  const queryBatch = getBatchList({}).then(res => {
+    batchList.value = res.data;
+  });
+  const queryClass = getTeachClassList({}).then(res => {
+    classList.value = res.data;
+  });
+  const querySubject = getTeacherSubjectList({}).then(res => {
+    subjectList.value = res.data;
+  });
+  await Promise.all([queryBatch, queryClass, querySubject]);
+  const queryCondition = getTeacherTestRecordCondition({}).then(res => {
+    const { buildType, buildStatus, batchId, classId, subjectId } = res.data;
+    queryForm.value = {
+      buildType: buildType || buildTypeList.value[0].value,
+      buildStatus: buildStatus || buildStatusList.value[0].value,
+      batchId: batchId || batchList.value[0].batchId,
+      classId: classId || classList.value[0].classId,
+      subjectId: subjectId || subjectList.value[0].subjectId,
+    };
+  });
+  await Promise.all([queryCondition]);
+}
+
+const loadTestRecord = () => {
+  const params = {} as Study.PaperWorkRecordQuery;
+  const { buildType, buildStatus, batchId, classId, subjectId } = queryForm.value;
+  if (buildType !== undefined && buildType !== null) {
+    params.buildType = buildType;
+  }
+  if (batchId !== undefined && batchId !== null) {
+    params.batchId = batchId;
+  }
+  if (classId !== undefined && classId !== null) {
+    params.classId = classId;
+  }
+  if (subjectId !== undefined && subjectId !== null) {
+    params.subjectId = subjectId;
+  }
+  uni.$ie.showLoading();
+  getTeacherTestRecord(params).then(res => {
+    tableData.value = res.data;
+  }).finally(() => {
+    uni.$ie.hideLoading();
+  });
+}
+
+watch(() => queryForm.value, () => {
+  loadTestRecord();
+}, {
+  deep: true,
+  immediate: false
+});
+
+onMounted(() => {
+  loadData();
+});
+</script>
+<style lang="scss" scoped></style>

+ 74 - 0
src/pagesStudy/pages/study-history/components/exam-history-simulated.vue

@@ -0,0 +1,74 @@
+<template>
+  <view>
+    <ie-table :tableColumns="tableColumns" :data="tableData" :cellStyle="cellStyle">
+      <template #name="{ item }: { item: StudentExamRecord }">
+        <text class="font-bold">{{ item.name }}</text>
+      </template>
+      <template #total="{ item }: { item: StudentExamRecord }">
+        <text class="font-bold">{{ item.value }}/{{ item.total }}</text>
+      </template>
+      <template #action="{ item }: { item: StudentExamRecord }">
+        <text class="text-30 text-primary font-bold" @click="handleRowClick(item)">查看</text>
+      </template>
+    </ie-table>
+  </view>
+</template>
+<script lang="ts" setup>
+import { getClassExamRecord } from '@/api/modules/study';
+import { Study, TableColumnConfig, Transfer } from '@/types';
+import { StudentExamRecord } from '@/types/study';
+import { CSSProperties } from 'vue';
+import { useTransferPage } from '@/hooks/useTransferPage';
+const { transferTo } = useTransferPage();
+
+const cellStyle: CSSProperties = {
+  padding: '30rpx 20rpx'
+}
+
+const tableData = ref<StudentExamRecord[]>([]);
+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: 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>

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

@@ -1,32 +1,41 @@
 <template>
-  <view class="px-30 pb-30">
-    <view class="sibling-border-top px-20 py-30" v-for="(item, index) in dataList" :key="index">
-      <exam-record-item :data="item" />
+  <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">
+        <exam-record-item :data="item" />
+      </view>
     </view>
+    <template v-else>
+      <view class="sibling-border-top" v-for="(item, index) in paperWorkRecordList" :key="index">
+        <paper-work-item :data="item" />
+      </view>
+    </template>
   </view>
 </template>
 <script lang="ts" setup>
-import { getSimulatedRecord } from '@/api/modules/study';
-import { EnumExamRecordType } from '@/common/enum';
+import { getSimulatedRecord, getPaperWorkList } from '@/api/modules/study';
+import { EnumExamRecordType, EnumPaperWorkState } from '@/common/enum';
 import examRecordItem from '@/pagesStudy/components/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 dataList = ref<Study.SimulatedRecord[]>([]);
+const simulatedRecordList = ref<Study.SimulatedRecord[]>([]);
+const paperWorkRecordList = ref<Study.PaperWork[]>([]);
 const loadData = async (type: string) => {
-  dataList.value = [];
+  simulatedRecordList.value = [];
+  paperWorkRecordList.value = [];
   if (type === EnumExamRecordType.SIMULATED) {
     const { data } = await getSimulatedRecord();
-    dataList.value = data;
+    simulatedRecordList.value = data;
   } else {
-
-
+    const { rows } = await getPaperWorkList({ state: EnumPaperWorkState.COMPLETED });
+    paperWorkRecordList.value = rows;
   }
-
 }
 
 watch(() => props.examType, (newVal) => {

+ 4 - 166
src/pagesStudy/pages/study-history/components/exam-history-teacher.vue

@@ -1,51 +1,12 @@
 <template>
   <view class="p-30 pt-50">
-    <view class="w-fit flex gap-x-50">
-      <ie-picker ref="pickerRef" v-model="classId" :list="classList" placeholder="选择批次" title="选择批次" icon="arrow-down"
-        key-label="name" key-value="classId" @change="handleClassChange"
-        :placeholder-style="placeholderStyle"></ie-picker>
-
-      <ie-picker ref="pickerRef" v-model="classId" :list="classList" placeholder="选择班级" title="选择班级" icon="arrow-down"
-        key-label="name" key-value="classId" @change="handleClassChange"
-        :placeholder-style="placeholderStyle"></ie-picker>
-    </view>
-    <view class="mt-32 py-38 bg-back border border-solid border-border rounded-10 flex">
-      <view class="flex-1 text-center">
-        <view class="text-40 text-primary font-bold leading-30">64</view>
-        <view class="mt-24 text-28 text-fore-light leading-28">总人数</view>
-      </view>
-      <view class="flex-1 text-center">
-        <view class="text-40 text-[#22C55E] font-bold leading-30">27</view>
-        <view class="mt-24 text-28 text-fore-light leading-28">完成</view>
-      </view>
-      <view class="flex-1 text-center">
-        <view class="text-40 text-[#F59E0B] font-bold leading-30">10</view>
-        <view class="mt-24 text-28 text-fore-light leading-28">未完成</view>
-      </view>
-      <view class="flex-1 text-center">
-        <view class="text-40 text-[#F59E0B] font-bold leading-30">9</view>
-        <view class="mt-24 text-28 text-fore-light leading-28">未发送</view>
-      </view>
-    </view>
-    <view class="mt-70">
-      <ie-table :tableColumns="tableColumns" :data="tableData" :cellStyle="cellStyle" @rowClick="handleRowClick">
-        <template #name="{ item }: { item: StudentExamRecord }">
-          <text class="font-bold">{{ item.name }}</text>
-        </template>
-        <template #score="{ item }: { item: StudentExamRecord }">
-          <text class="font-bold">{{ item.score }}</text>
-        </template>
-        <template #status="{ item }: { item: StudentExamRecord }">
-          <text :class="getStatusClass(item.status)">{{ getStatusText(item.status) }}</text>
-        </template>
-      </ie-table>
-    </view>
+    <exam-history-simulated v-if="examType === 'simulated'" />
+    <exam-history-paperwork v-else />
   </view>
 </template>
 <script lang="ts" setup>
-import { TableColumnConfig } from '@/types';
-import { StudentExamRecord, TeachClass } from '@/types/study';
-import { CSSProperties } from 'vue';
+import examHistorySimulated from './exam-history-simulated.vue';
+import examHistoryPaperwork from './exam-history-paperwork.vue';
 
 const props = defineProps({
   examType: {
@@ -53,128 +14,5 @@ const props = defineProps({
     default: 'test'
   }
 });
-const classId = ref<number>(0);
-const classList = ref<TeachClass[]>([]);
-const pickerRef = ref();
-const placeholderStyle = {
-  color: '#1A1A1A'
-}
-const tableColumns: TableColumnConfig[] = [
-  {
-    prop: 'name',
-    label: '姓名',
-    flex: 1,
-    slot: 'name',
-    headerAlign: 'center',
-    align: 'center'
-  },
-  {
-    prop: 'score',
-    label: '得分',
-    flex: 1,
-    slot: 'score'
-  },
-  {
-    prop: 'status',
-    label: '状态',
-    flex: 1,
-    slot: 'status'
-  }
-]
-const tableData = ref<StudentExamRecord[]>([
-  {
-    id: 1,
-    name: '张三',
-    score: 100,
-    status: 1
-  },
-  {
-    id: 2,
-    name: '李四',
-    score: 90,
-    status: 2
-  },
-  {
-    id: 3,
-    name: '王五',
-    score: 80,
-    status: 1
-  },
-  {
-    id: 4,
-    name: '赵六',
-    score: 70,
-    status: 1
-  },
-  {
-    id: 5,
-    name: '孙七',
-    score: 60,
-    status: 2
-  },
-  {
-    id: 6,
-    name: '周八',
-    score: 50,
-    status: 2
-  },
-  {
-    id: 7,
-    name: '吴九',
-    score: 40,
-    status: 1
-  }
-])
-const cellStyle: CSSProperties = {
-  padding: '30rpx 20rpx'
-}
-const handleClassChange = (value: number, selectedItem: TeachClass) => {
-  classId.value = value;
-}
-
-// 处理表格行点击事件
-const handleRowClick = (row: StudentExamRecord) => {
-  console.log('点击了行:', row);
-  // 这里可以添加跳转到详情页或其他逻辑
-}
-
-// 获取状态文本
-const getStatusText = (status: number) => {
-  const statusMap = {
-    1: '已完成',
-    2: '未完成',
-    3: '未发送'
-  };
-  return statusMap[status as keyof typeof statusMap] || '未知';
-}
-
-// 获取状态样式类
-const getStatusClass = (status: number) => {
-  const classMap = {
-    1: 'text-[#22C55E] font-bold',
-    2: 'text-[#F59E0B] font-bold',
-    3: 'text-[#6B7280] font-bold'
-  };
-  return classMap[status as keyof typeof classMap] || 'text-gray-500';
-}
-
-const loadData = async () => {
-  // const res = await getClassHistory();
-  // console.log(res);
-  uni.$ie.showLoading();
-  setTimeout(() => {
-    uni.$ie.hideLoading();
-  }, 1000);
-}
-const loadClassList = async () => {
-  // const res = await getClassList();
-  // console.log(res);
-}
-
-watch(() => props.examType, (newVal) => {
-  loadData();
-}, {
-  immediate: true
-});
 </script>
 <style lang="scss" scoped></style>

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

@@ -15,7 +15,7 @@ const loadData = async () => {
       const rate = item.rate;
       return {
         ...item,
-        rate: rate < 0.1 ? 0.1 : Number(rate.toFixed(1))
+        rate: Number(rate.toFixed(1))
       }
     });
   } finally {

+ 21 - 29
src/pagesStudy/pages/study-history/components/knowledge-history-teacher.vue

@@ -1,11 +1,11 @@
 <template>
   <view class="mt-40 flex items-center justify-end gap-x-10">
     <text class="text-24 text-fore-light">平均正确率</text>
-    <text class="text-30 text-fore-title font-bold">80%</text>
+    <text class="text-30 text-fore-title font-bold">{{ averageRate.toFixed(1) }}%</text>
   </view>
   <view class="mt-10">
-    <progress :percent="80" stroke-width="12" activeColor="#68D119" backgroundColor="#F2F2F2" :border-radius="8"
-      class="custom-progress" />
+    <progress :percent="averageRate" stroke-width="12" activeColor="#68D119" backgroundColor="#F2F2F2"
+      :border-radius="8" class="custom-progress" />
   </view>
   <view class="mt-76">
     <student-stat-table :teach-class="teachClass" :data="studentStatData" @rowClick="handleRowClick" />
@@ -14,7 +14,8 @@
 <script lang="ts" setup>
 import { OPEN_KNOWLEDGE_DETAIL } from '@/types/injectionSymbols';
 import studentStatTable from './student-stat-table.vue';
-import { StudentStat, TeachClass } from '@/types/study';
+import { StudentPlanStudyRecord, TeachClass } from '@/types/study';
+import { getClassKnowledgeRecord } from '@/api/modules/study';
 const openKnowledgeDetail = inject(OPEN_KNOWLEDGE_DETAIL);
 const props = defineProps({
   teachClass: {
@@ -22,38 +23,29 @@ const props = defineProps({
     default: () => ({})
   }
 });
-const studentStatData = ref<StudentStat[]>([
-  {
-    id: 1,
-    name: '张三',
-    questionNum: 10,
-    dateNum: 10,
-    rate: 80
-  },
-  {
-    id: 2,
-    name: '李四',
-    questionNum: 10,
-    dateNum: 10,
-    rate: 80
-  }
-]);
+const studentStatData = ref<StudentPlanStudyRecord[]>([]);
+const averageRate = ref(0);
+
+const loadData = async () => {
+  getClassKnowledgeRecord({
+    classId: props.teachClass.classId,
+  }).then(res => {
+    studentStatData.value = res.data.list;
+    averageRate.value = res.data.rate;
+  })
+}
+
+const handleRowClick = (row: StudentPlanStudyRecord) => {
+  openKnowledgeDetail?.(row.id, row.name);
+}
+
 watch(() => props.teachClass, (newVal) => {
   if (newVal.classId) {
-    console.log('新的班级', newVal);
     loadData();
   }
 }, {
   deep: true,
   immediate: true
 });
-
-const loadData = async () => {
-  // const res = await getStudentStat(props.teachClass.classId);
-  // console.log(res);
-}
-const handleRowClick = (row: StudentStat) => {
-  openKnowledgeDetail?.(row.id, row.name);
-}
 </script>
 <style lang="scss" scoped></style>

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

@@ -21,7 +21,7 @@ const { isStudent } = storeToRefs(useUserStore());
 const openKnowledgeDetail = (id: number, name: string) => {
   transferTo('/pagesStudy/pages/study-knowledge-detail/study-knowledge-detail', {
     data: {
-      studentId: id,
+      recordId: id,
       name: name
     }
   });

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

@@ -1,12 +1,9 @@
 <template>
   <view>
-    <practice-table :student-id="studentId" />
+    <practice-table />
   </view>
 </template>
 <script lang="ts" setup>
 import practiceTable from '@/pagesStudy/components/practice-table.vue';
-import { useUserStore } from '@/store/userStore';
-const userStore = useUserStore();
-const studentId = ref(userStore.userInfo.userId);
 </script>
 <style lang="scss" scoped></style>

+ 25 - 29
src/pagesStudy/pages/study-history/components/practice-history-teacher.vue

@@ -1,56 +1,52 @@
 <template>
   <view>
-     <student-stat-table :teach-class="teachClass" :data="studentStatData" @row-click="handleRowClick" />
+    <student-stat-table :teach-class="teachClass" :data="studentStatData" @row-click="handleRowClick" />
   </view>
 </template>
 <script lang="ts" setup>
 import studentStatTable from './student-stat-table.vue';
-import { StudentStat, TeachClass } from '@/types/study';
+import { Study } from '@/types';
 import { OPEN_PRACTICE_DETAIL } from '@/types/injectionSymbols';
+import { getClassPlanStudyRecord } from '@/api/modules/study';
+
 const openPracticeDetail = inject(OPEN_PRACTICE_DETAIL);
 const props = defineProps({
   teachClass: {
-    type: Object as PropType<TeachClass>,
+    type: Object as PropType<Study.TeachClass>,
     default: () => ({})
   },
   currentType: {
     type: String,
     default: 'rate'
   },
-  currentTab: {
+  currentSort: {
     type: String,
     default: 'asc'
   }
 });
-const studentId = ref(0);
-const studentStatData = ref<StudentStat[]>([
-  {
-    id: 1,
-    name: '张三',
-    questionNum: 10,
-    dateNum: 10,
-    rate: 80
-  },
-  {
-    id: 2,
-    name: '李四',
-    questionNum: 10,
-    dateNum: 10,
-    rate: 80
-  }
-]);
+
+const studentStatData = ref<Study.StudentPracticeRecord[]>([]);
+
 const loadData = async () => {
-  console.log('loadData', props.teachClass, props.currentTab, props.currentType);
-  // const res = await getPracticeData(props.teachClass.classId);
-  // data.value = res.data;
+  uni.$ie.showLoading();
+  try {
+    const { rows } = await getClassPlanStudyRecord({
+      classId: props.teachClass.classId,
+      sortField: props.currentType,
+      asc: props.currentSort === 'asc'
+    });
+    studentStatData.value = rows;
+  } finally {
+    uni.$ie.hideLoading();
+  }
 }
 watchEffect(() => {
-  loadData();
+  if (props.teachClass.classId) {
+    loadData();
+  }
 });
-const handleRowClick = (row: StudentStat) => {
-  console.log('点击了行:', row);
+const handleRowClick = (row: Study.StudentPracticeRecord) => {
   openPracticeDetail?.(row.id, row.name);
 }
 </script>
-<style lang="scss" scoped>
-</style>
+<style lang="scss" scoped></style>

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

@@ -9,10 +9,10 @@
           </view>
         </view>
         <view class="mt-30 w-fit mx-auto">
-          <ie-tab :options="tabList" v-model="currentTab" />
+          <ie-tab :options="tabList" v-model="currentSort" />
         </view>
         <view class="mt-30">
-          <practice-history-teacher :teach-class="teachClass" :current-type="currentType" :current-tab="currentTab" />
+          <practice-history-teacher :teach-class="teachClass" :current-type="currentType" :current-sort="currentSort" />
         </view>
       </template>
     </teacher-class-view>
@@ -34,14 +34,14 @@ const typeList = ref([
   },
   {
     label: '按刷题天数',
-    value: 'practiceDays'
+    value: 'time'
   },
   {
     label: '按刷题题量',
-    value: 'questionNum'
+    value: 'total'
   }
 ])
-const currentTab = ref('asc');
+const currentSort = ref('asc');
 const tabList = ref([
   {
     label: '升序',
@@ -56,11 +56,11 @@ const { isStudent } = storeToRefs(useUserStore());
 const handleTypeChange = (value: string) => {
   currentType.value = value;
 }
-const openPracticeDetail = (id: number, name: string) => {
+const openPracticeDetail = (recordId: number, name: string) => {
   transferTo('/pagesStudy/pages/study-practice-detail/study-practice-detail', {
     data: {
-      studentId: id,
-      name: name
+      recordId,
+      name
     }
   });
 }

+ 12 - 8
src/pagesStudy/pages/study-history/components/student-stat-table.vue

@@ -7,19 +7,22 @@
           <text class="ml-10 flex-1 min-w-1 ellipsis-1">{{ item.name }}</text>
         </view>
       </template>
+      <template #rate="{ item }">
+        <text :class="[item.rate < 70 ? 'text-danger' : 'text-fore-title']">{{ item.rate }}%</text>
+      </template>
     </ie-table>
   </view>
 </template>
 <script lang="ts" setup>
 import { TableColumnConfig, TableConfig } from '@/types';
-import { TeachClass, StudentStat } from '@/types/study';
+import { TeachClass, StudentPlanStudyRecord } from '@/types/study';
 const props = defineProps({
   teachClass: {
     type: Object as PropType<TeachClass>,
     default: () => ({})
   },
   data: {
-    type: Array as PropType<StudentStat[]>,
+    type: Array as PropType<StudentPlanStudyRecord[]>,
     default: () => []
   }
 });
@@ -43,28 +46,29 @@ const tableColumns: TableColumnConfig[] = [
     slot: 'name'
   },
   {
-    prop: 'questionNum',
+    prop: 'total',
     label: '题量',
     flex: 0.6
   },
   {
-    prop: 'dateNum',
+    prop: 'time',
     label: '天数',
     flex: 0.6
   },
   {
     prop: 'rate',
-    label: '正确率'
+    label: '正确率',
+    slot: 'rate'
   }
 ]
 const emit = defineEmits<{
-  rowClick: [row: StudentStat]
+  rowClick: [row: StudentPlanStudyRecord]
 }>();
-const handleRowClick = (row: StudentStat) => {
+const handleRowClick = (row: StudentPlanStudyRecord) => {
   emit('rowClick', row);
 }
 // const openKnowledgeDetail = inject(OPEN_KNOWLEDGE_DETAIL);
-// const handleRowClick = (row: StudentStat) => {
+// const handleRowClick = (row: StudentPlanStudyRecord) => {
 //   openKnowledgeDetail?.(row.id, row.name);
 // }
 </script>

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

@@ -7,7 +7,7 @@
 import videoTable from '@/pagesStudy/components/video-table.vue';
 import { getVideoStudyRecord } from '@/api/modules/study';
 import * as Study from '@/types/study';
-const dataList = ref<Study.VideoStudyRecord>({} as Study.VideoStudyRecord);
+const dataList = ref<Study.VideoStudy>({} as Study.VideoStudy);
 const loading = ref(false);
 const loadData = async () => {
   loading.value = true;

+ 44 - 14
src/pagesStudy/pages/study-history/components/video-history-teacher.vue

@@ -11,9 +11,22 @@
   </view>
 </template>
 <script lang="ts" setup>
-import { TableColumnConfig } from '@/types';
-import { StudentVideoStat } from '@/types/study';
+import { Study, TableColumnConfig } from '@/types';
+import { StudentVideoRecord } from '@/types/study';
 import { OPEN_VIDEO_DETAIL } from '@/types/injectionSymbols';
+import { getClassVideoStudyRecord } from '@/api/modules/study';
+
+const props = defineProps({
+  teachClass: {
+    type: Object as PropType<Study.TeachClass>,
+    default: () => ({})
+  },
+  currentSort: {
+    type: String,
+    default: 'asc'
+  }
+});
+
 const openVideoDetail = inject(OPEN_VIDEO_DETAIL);
 const tableColumns = ref<TableColumnConfig[]>([
   {
@@ -28,27 +41,44 @@ const tableColumns = ref<TableColumnConfig[]>([
     flex: 1
   },
   {
-    prop: 'videoCount',
+    prop: 'total',
     label: '课时',
     flex: 1
   },
   {
-    prop: 'duration',
+    prop: 'value',
     label: '时长(分钟)',
     flex: 1
   }
 ])
-const data = ref<StudentVideoStat[]>([
-  {
-    id: 1,
-    name: '张三',
-    videoCount: 10,
-    duration: 100
-  }
-])
-const handleRowClick = (row: StudentVideoStat) => {
-  console.log('点击了行:', row);
+const data = ref<StudentVideoRecord[]>([])
+const handleRowClick = (row: StudentVideoRecord) => {
   openVideoDetail?.(row.id, row.name);
 }
+
+const loadData = async () => {
+  uni.$ie.showLoading();
+  try {
+    const { rows } = await getClassVideoStudyRecord({
+      classId: props.teachClass.classId,
+      asc: props.currentSort === 'asc'
+    });
+    data.value = rows.map(item => {
+      return {
+        ...item,
+        value: Math.round(item.value / 60)
+      }
+    });
+  } finally {
+    uni.$ie.hideLoading();
+  }
+}
+
+watchEffect(() => {
+  if (props.teachClass.classId) {
+    loadData();
+  }
+});
+
 </script>
 <style lang="scss" scoped></style>

+ 6 - 6
src/pagesStudy/pages/study-history/components/video-history.vue

@@ -4,9 +4,9 @@
     <teacher-class-view v-else>
       <template #default="{ teachClass }">
         <view class="mt-30 w-fit mx-auto">
-          <ie-tab :options="tabList" v-model="currentTab" />
+          <ie-tab :options="tabList" v-model="currentSort" />
         </view>
-        <video-history-teacher :teach-class="teachClass" />
+        <video-history-teacher :teach-class="teachClass" :current-sort="currentSort" />
       </template>
     </teacher-class-view>
   </view>
@@ -20,7 +20,7 @@ import { useTransferPage } from '@/hooks/useTransferPage';
 import { useUserStore } from '@/store/userStore';
 const { transferTo } = useTransferPage();
 const { isStudent } = storeToRefs(useUserStore());
-const currentTab = ref('asc');
+const currentSort = ref('asc');
 const tabList = ref([
   {
     label: '升序',
@@ -31,11 +31,11 @@ const tabList = ref([
     value: 'desc'
   }
 ])
-const openVideoDetail = (id: number, name: string) => {
+const openVideoDetail = (recordId: number, name: string) => {
   transferTo('/pagesStudy/pages/study-video-detail/study-video-detail', {
     data: {
-      studentId: id,
-      name: name
+      recordId,
+      name
     }
   });
 }

+ 24 - 3
src/pagesStudy/pages/study-knowledge-detail/study-knowledge-detail.vue

@@ -3,8 +3,8 @@
     <ie-navbar :title="pageTitle" />
     <view class="h-20"></view>
     <view class="bg-white flex-1 min-h-1">
-      <view class="pt-32 px-30 text-30 text-fore-title font-bold">{{ prevData.name }}的知识点记录</view>
-      <knowledge-table :student-id="studentId" />
+      <view class="pt-30 px-30 text-30 text-fore-title font-bold">{{ prevData.name }}的知识点记录</view>
+      <knowledge-table :data="tableList" />
     </view>
   </ie-page>
 </template>
@@ -12,9 +12,13 @@
 import knowledgeTable from '@/pagesStudy/components/knowledge-table.vue';
 import { useTransferPage } from '@/hooks/useTransferPage';
 import { useScroll } from '@/hooks/useScroll';
+import { getStudentKnowledgeRecord } from '@/api/modules/study';
+import { Study } from '@/types';
+
 const { prevData } = useTransferPage();
-const studentId = ref(prevData.value.studentId);
+const recordId = ref();
 const { scrollTop } = useScroll();
+const tableList = ref<Study.KnowledgeRecord[]>([]);
 
 const pageTitle = computed(() => {
   if (scrollTop.value > 45) {
@@ -22,5 +26,22 @@ const pageTitle = computed(() => {
   }
   return '学生知识点详情';
 });
+
+const loadData = async () => {
+  uni.$ie.showLoading();
+  try {
+    const { rows } = await getStudentKnowledgeRecord({
+      recordId: recordId.value,
+    });
+    tableList.value = rows;
+  } finally {
+    uni.$ie.hideLoading();
+  }
+
+}
+onLoad(() => {
+  recordId.value = prevData.value.recordId;
+  loadData();
+});
 </script>
 <style lang="scss" scoped></style>

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

@@ -4,7 +4,7 @@
     <view class="h-20"></view>
     <view class="bg-white flex-1 min-h-1">
       <view class="pt-32 px-30 text-30 text-fore-title font-bold">{{ prevData.name }}的刷题详情</view>
-      <practice-table :student-id="studentId" />
+      <practice-table :record-id="prevData.recordId" />
     </view>
   </ie-page>
 </template>
@@ -12,8 +12,8 @@
 import practiceTable from '@/pagesStudy/components/practice-table.vue';
 import { useTransferPage } from '@/hooks/useTransferPage';
 import { useScroll } from '@/hooks/useScroll';
+
 const { prevData } = useTransferPage();
-const studentId = ref(prevData.value.studentId);
 const { scrollTop } = useScroll();
 
 const pageTitle = computed(() => {

+ 20 - 2
src/pagesStudy/pages/study-video-detail/study-video-detail.vue

@@ -4,7 +4,7 @@
     <view class="h-20"></view>
     <view class="bg-white flex-1 min-h-1 relative">
       <view class="pt-30 px-32 text-30 text-fore-title font-bold">{{ prevData.name }}的视频观看统计</view>
-      <video-table :student-id="studentId" />
+      <video-table :data="data" />
     </view>
   </ie-page>
 </template>
@@ -12,9 +12,12 @@
 import videoTable from '@/pagesStudy/components/video-table.vue';
 import { useTransferPage } from '@/hooks/useTransferPage';
 import { useScroll } from '@/hooks/useScroll';
+import { Study } from '@/types';
+import { getStudentVideoStudyRecord } from '@/api/modules/study';
+
 const { prevData } = useTransferPage();
-const studentId = ref(prevData.value.studentId);
 const { scrollTop } = useScroll();
+const data = ref<Study.VideoStudy>({} as Study.VideoStudy);
 
 const pageTitle = computed(() => {
   if (scrollTop.value > 45) {
@@ -22,5 +25,20 @@ const pageTitle = computed(() => {
   }
   return '学生刷题详情';
 });
+
+const loadData = async () => {
+  uni.$ie.showLoading();
+  try {
+    const res = await getStudentVideoStudyRecord({
+      recordId: prevData.value.recordId,
+    });
+    data.value = res.data;
+  } finally {
+    uni.$ie.hideLoading();
+  }
+}
+onLoad(() => {
+  loadData();
+});
 </script>
 <style lang="scss" scoped></style>

+ 64 - 0
src/pagesStudy/pages/textbooks-practice-history/textbooks-practice-history.vue

@@ -0,0 +1,64 @@
+<template>
+  <ie-page bg-color="#F6F8FA" :fix-height="true">
+    <ie-navbar title="教材同步练习记录" />
+    <view class="mt-20">
+      <view v-for="(item, index) in historyList" :key="index"
+        class="bg-white px-40 py-30 flex items-center sibling-border-top">
+        <view class="flex-1">
+          <view class="text-28">
+            <text class=" text-fore-light">知识点:</text>
+            <text class="text-fore-title">{{ item.paperName }}</text>
+          </view>
+          <view class="mt-10 text-28">
+            <text class=" text-fore-light">完成时间:</text>
+            <text class="text-fore-title">{{ item.endTime }}</text>
+          </view>
+        </view>
+        <view class="text-24 text-white bg-primary w-fit px-40 py-12 rounded-full text-center"
+          @click="handleViewHistory(item)">
+          查看
+        </view>
+      </view>
+    </view>
+  </ie-page>
+</template>
+<script lang="ts" setup>
+import { useNavbar } from '@/hooks/useNavbar';
+import { useTransferPage } from '@/hooks/useTransferPage';
+import { getTextbooksPracticeHistory } from '@/api/modules/study';
+import { Study } from '@/types';
+import { Transfer } from '@/types';
+import { EnumPaperType } from '@/common/enum';
+const { prevData, transferTo } = useTransferPage<{}, Transfer.PracticeResultPageOptions>();
+const { baseStickyTop } = useNavbar();
+const historyList = ref<Study.PracticeHistory[]>([]);
+const handleViewHistory = (value: Study.PracticeHistory) => {
+  transferTo('/pagesStudy/pages/knowledge-practice-detail/knowledge-practice-detail', {
+    data: {
+      paperType: EnumPaperType.COURSE,
+      examineeId: value.examineeId,
+      name: value.paperName,
+      directed: value.directed === 1
+    } as Transfer.PracticeResultPageOptions,
+  });
+}
+const loadData = async () => {
+  uni.$ie.showLoading();
+  try {
+    const { rows } = await getTextbooksPracticeHistory();
+    historyList.value = rows.map(item => {
+      return {
+        ...item,
+        endTime: uni.$uv.timeFormat(item.endTime, 'yyyy-mm-dd hh:MM:ss')
+      } as Study.PracticeHistory;
+    });
+  } finally {
+    uni.$ie.hideLoading();
+  }
+}
+onLoad(() => {
+  console.log(prevData.value)
+  loadData();
+});
+</script>
+<style lang="scss" scoped></style>

+ 86 - 0
src/pagesStudy/pages/textbooks-practice/textbooks-practice.vue

@@ -0,0 +1,86 @@
+<template>
+  <ie-page>
+    <z-paging ref="pagingRef" v-model="treeData" :loading-more-enabled="false" :auto="false" @query="loadKnowledgeList">
+      <template #top>
+        <ie-navbar :title="pageTitle" />
+        <view class="px-30 py-16 bg-back">
+          <view class="flex items-center justify-end gap-x-4" @click="handleViewHistory">
+            <uv-icon name="clock" size="16" color="#31A0FC"></uv-icon>
+            <text class="text-28 text-primary">查看记录</text>
+            <uv-icon name="arrow-right" size="16" color="#31A0FC"></uv-icon>
+          </view>
+        </view>
+      </template>
+      <view class="px-40">
+        <knowledgeTree :tree-data="treeData" @start-practice="handleStartPractice" />
+      </view>
+    </z-paging>
+  </ie-page>
+</template>
+
+<script lang="ts" setup>
+import IePage from '@/components/ie-page/ie-page.vue';
+import { useTransferPage } from '@/hooks/useTransferPage';
+import { getTextbooksKnowledgeList } from '@/api/modules/study';
+import knowledgeTree from '@/pagesStudy/components/knowledge-tree.vue';
+import * as Study from '@/types/study';
+import { EnumPaperType, EnumUserRole } from '@/common/enum';
+import { useUserStore } from '@/store/userStore';
+import { useAuth } from '@/hooks/useAuth';
+
+const { prevData, transferTo } = useTransferPage();
+const userStore = useUserStore();
+const pagingRef = ref();
+const { hasPermission } = useAuth();
+
+const pageTitle = computed(() => {
+  return '教材同步练习';
+});
+
+const treeData = ref<Study.KnowledgeNode[]>([]);
+
+const handleViewHistory = () => {
+  transferTo('/pagesStudy/pages/textbooks-practice-history/textbooks-practice-history', {
+    data: {}
+  });
+}
+
+const loadKnowledgeList = async (page: number, size: number) => {
+  try {
+    uni.$ie.showLoading();
+    const { data } = await getTextbooksKnowledgeList();
+    treeData.value = data as Study.KnowledgeNode[];
+    pagingRef.value.complete(data);
+  } catch (error) {
+    console.log(error);
+    pagingRef.value.complete(false);
+  } finally {
+    uni.$ie.hideLoading();
+  }
+}
+
+const handleStartPractice = async (node: Study.KnowledgeNode) => {
+  const hasAuth = hasPermission([EnumUserRole.VIP, EnumUserRole.AGENT, EnumUserRole.TEACHER]);
+  if (hasAuth) {
+    transferTo('/pagesStudy/pages/exam-start/exam-start', {
+      data: {
+        name: '教材同步练习-' + node.name,
+        paperType: EnumPaperType.COURSE,
+        practiceInfo: {
+          name: node.name,
+          relateId: node.id
+        },
+      }
+    });
+  }
+}
+
+onShow(() => {
+  nextTick(() => {
+    // 下拉刷新数据
+    pagingRef.value?.refresh();
+  });
+});
+</script>
+
+<style></style>

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


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


+ 243 - 131
src/pagesSystem/pages/bind-profile/bind-profile copy.vue

@@ -2,31 +2,38 @@
   <ie-page bg-color="#F6F8FA" :safeAreaInsetBottom="false">
     <ie-navbar title="完善信息"></ie-navbar>
     <uv-form labelPosition="left" :model="form" labelWidth="70px" ref="formRef">
-      <content-card title="考生信息">
-        <uv-form-item label="学生姓名" prop="name" borderBottom required>
+      <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">
           </uv-input>
         </uv-form-item>
         <uv-form-item label="所在省份" prop="location" borderBottom required>
-          <ie-picker ref="pickerRef" v-model="examTypeForm.location" :list="provinceList" placeholder="选择省份"
-            :custom-style="customStyle" key-label="dictLabel" key-value="dictValue" :disabled="isProvinceDisabled"
-            :show-arrow="!isProvinceDisabled" @change="handleProvinceChange"></ie-picker>
+          <ie-picker ref="pickerRef" v-model="examTypeForm.location" :list="provinceList"
+            :placeholder="pickerPlaceholder" :custom-style="customStyle" key-label="dictLabel" key-value="dictValue"
+            :disabled="isProvinceDisabled">
+            <template v-if="isProvinceDisabled" #right>
+              <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="examType" borderBottom required>
           <ie-picker ref="pickerRef" v-model="examTypeForm.examType" :list="examTypeList" :disabled="isExamTypeDisabled"
-            placeholder="选择考生类别" :custom-style="customStyle" key-label="dictLabel" key-value="dictValue"
-            :show-arrow="!isExamTypeDisabled"></ie-picker>
+            :placeholder="pickerPlaceholder" :custom-style="customStyle" key-label="dictLabel" key-value="dictValue">
+            <template v-if="isExamTypeDisabled" #right>
+              <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 v-if="examTypeForm.examType === 'VHS'" label="专业类别" prop="majorType" borderBottom required>
           <ie-picker ref="pickerRef" v-model="examTypeForm.majorType" :list="examMajorList"
-            :disabled="!examTypeForm.examType" placeholder="选择专业类别" :custom-style="customStyle" key-label="dictLabel"
-            key-value="dictValue"></ie-picker>
+            :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="选择毕业年份" :custom-style="customStyle" key-label="dictLabel"
-            key-value="dictValue"></ie-picker>
+            :disabled="!examTypeForm.examType" :placeholder="pickerPlaceholder" :custom-style="customStyle"
+            key-label="dictLabel" key-value="dictValue"></ie-picker>
         </uv-form-item>
 
       </content-card>
@@ -38,46 +45,52 @@
         </uv-form-item>
       </content-card>
       <content-card v-if="showCulture" title="文化素质">
-        <uv-form-item label="语文" prop="form.scores.chinese" borderBottom :required="isImproveMode">
-          <uv-input v-model="scoresForm.chinese" border="none" type="number" placeholder="请输入" font-size="30rpx"
-            :custom-style="customStyle">
+        <uv-form-item label="语文" prop="form.scores.chinese" borderBottom :required="isBindMode">
+          <uv-input v-model.number="scoresForm.chinese" border="none" type="number" :placeholder="inputPlaceholder"
+            font-size="30rpx" :custom-style="customStyle">
           </uv-input>
         </uv-form-item>
-        <uv-form-item label="数学" prop="form.score.mathematics" borderBottom :required="isImproveMode">
-          <uv-input v-model="scoresForm.mathematics" border="none" type="number" placeholder="请输入" font-size="30rpx"
-            :custom-style="customStyle">
+        <uv-form-item label="数学" prop="form.score.mathematics" borderBottom :required="isBindMode">
+          <uv-input v-model.number="scoresForm.mathematics" border="none" type="number" :placeholder="inputPlaceholder"
+            font-size="30rpx" :custom-style="customStyle">
           </uv-input>
         </uv-form-item>
-        <uv-form-item label="外语" prop="form.scores.foreign" borderBottom :required="isImproveMode">
-          <uv-input v-model="scoresForm.foreign" border="none" type="number" placeholder="请输入" font-size="30rpx"
-            :custom-style="customStyle">
+        <uv-form-item label="外语" prop="form.scores.foreign" borderBottom :required="isBindMode">
+          <uv-input v-model.number="scoresForm.foreign" border="none" type="number" :placeholder="inputPlaceholder"
+            font-size="30rpx" :custom-style="customStyle">
           </uv-input>
         </uv-form-item>
-        <uv-form-item label="物理" prop="form.scores.physics" borderBottom :required="isImproveMode">
-          <uv-input v-model="scoresForm.physics" border="none" type="number" placeholder="请输入" font-size="30rpx"
-            :custom-style="customStyle">
+        <uv-form-item label="物理" prop="form.scores.physics" borderBottom :required="isBindMode">
+          <uv-input v-model.number="scoresForm.physics" border="none" type="number" :placeholder="inputPlaceholder"
+            font-size="30rpx" :custom-style="customStyle">
           </uv-input>
         </uv-form-item>
-        <uv-form-item label="政治" prop="form.scores.political" :required="isImproveMode">
-          <uv-input v-model="scoresForm.political" border="none" type="number" placeholder="请输入" font-size="30rpx"
-            :custom-style="customStyle">
+        <uv-form-item label="政治" prop="form.scores.political" :required="isBindMode">
+          <uv-input v-model.number="scoresForm.political" border="none" type="number" :placeholder="inputPlaceholder"
+            font-size="30rpx" :custom-style="customStyle">
           </uv-input>
         </uv-form-item>
       </content-card>
-      <content-card v-if="isImproveMode" title="学校信息">
-        <uv-form-item label="学校名称" prop="form.name" borderBottom :required="isImproveMode">
-          <ie-picker ref="pickerRef" v-model="form.schoolName" disabled placeholder="请选择就读学校"
-            :custom-style="customStyle" :custom-label="form.schoolName" @click="handleSchoolSelect"
-            :show-arrow="!isSchoolDisabled"></ie-picker>
+      <content-card v-if="isBindMode" title="学校信息">
+        <template #right>
+          <view class="text-26 text-primary underline" @click="handleNoSchool">没有我的学校班级?</view>
+        </template>
+        <uv-form-item label="学校名称" prop="form.name" borderBottom :required="isBindMode">
+          <ie-picker ref="pickerRef" v-model="form.schoolName" disabled :placeholder="pickerPlaceholder"
+            :custom-style="customStyle" :custom-label="form.schoolName" @click="handleSchoolSelect">
+            <template v-if="isSchoolDisabled" #right>
+              <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="form.name" :required="isImproveMode">
+        <uv-form-item label="所在班级" prop="form.name" :required="isBindMode">
           <ie-picker ref="pickerRef" v-model="form.classId" :list="classList" :disabled="!form.schoolId" title="选择班级"
-            placeholder="请选择所在班级" :custom-style="customStyle" key-label="name" key-value="classId"></ie-picker>
+            placeholder="请选择" :custom-style="customStyle" key-label="name" key-value="classId"></ie-picker>
         </uv-form-item>
       </content-card>
     </uv-form>
     <ie-safe-toolbar :height="84" :shadow="false">
-      <view class="px-30 py-16">
+      <view class="px-46 pt-24">
         <ie-button @click="handleSubmit">确认提交</ie-button>
       </view>
     </ie-safe-toolbar>
@@ -87,16 +100,24 @@
 <script lang="ts" setup>
 import ContentCard from '../../components/content-card.vue';
 import { useUserStore } from '@/store/userStore';
-import { registry, improve } from '@/api/modules/login';
+import { registry, improve, improveWithToken } from '@/api/modules/login';
 import { useTransferPage } from '@/hooks/useTransferPage';
 import { useExamType } from '@/composables/useExamType';
 import { useAppStore } from '@/store/appStore';
-import { BindCardInfo, ClassItem, SchoolItem, Scores } from '@/types/user';
+import { BindCardInfo, CardInfo, ClassItem, LoginInfo, RegisterInfo, SchoolItem, Scores, UserInfo } from '@/types/user';
 
 import { getClassList } from '@/api/modules/user';
+import { EnumBindScene, EnumExamRecordType, EnumExamType } from '@/common/enum';
+type PrevDataInfo = {
+  cardInfo: CardInfo;
+  userInfo: UserInfo;
+  registerInfo: RegisterInfo;
+  scene: EnumBindScene;
+  token: string; // 绑定已有账号时需要 token
+}
+
 const { form: examTypeForm, examTypeList, examMajorList, provinceList, endYearList } = useExamType();
 const userStore = useUserStore();
-const appStore = useAppStore();
 const { prevData, transferTo, transferBack } = useTransferPage();
 
 const form = ref<Partial<BindCardInfo>>({});
@@ -106,36 +127,55 @@ const customStyle = {
   paddingLeft: '26px'
 };
 
-const isImproveMode = computed(() => prevData.value.scene === 'phone_improve' || prevData.value.scene === 'card_improve');
-const isSchoolDisabled = computed(() => prevData.value.scene === 'card_improve' && prevData.value.card.assignSchoolId);
-const isProvinceDisabled = computed(() => prevData.value.scene === 'card_improve' && prevData.value.card.assignLocation);
-const isExamTypeDisabled = computed(() => (prevData.value.scene === 'card_improve' && prevData.value.card.assignExamType) || !examTypeForm.value.location);
-const handleProvinceChange = (val: string) => {
-  if (isProvinceDisabled.value) {
-    return;
-  }
-  form.value.examType = '';
-  form.value.majorType = '';
-}
+const isBindMode = computed(() => [EnumBindScene.LOGIN_BIND, EnumBindScene.REGISTER_BIND].includes(prevData.value.scene));
+const isSchoolDisabled = computed(() => isBindMode.value && prevData.value.cardInfo.assignSchoolId);
+const isProvinceDisabled = computed(() => isBindMode.value && prevData.value.cardInfo.assignLocation);
+const isExamTypeDisabled = computed(() => (isBindMode.value && prevData.value.cardInfo.assignExamType) || !examTypeForm.value.location);
+const contactPhone = computed(() => userStore.orgInfo.contactPhone);
+const inputPlaceholder = computed(() => {
+  return isBindMode.value ? '请输入(提交后不可修改)' : '请输入';
+});
+const pickerPlaceholder = computed(() => {
+  return isBindMode.value ? '请选择(提交后不可修改)' : '请选择';
+});
 
 const classList = ref<ClassItem[]>([]);
 const showCulture = computed(() => {
-  return examTypeForm.value.examType === 'OHS';
+  return examTypeForm.value.examType === EnumExamType.OHS;
 });
-const handleSchoolSelect = () => {
-  if (isSchoolDisabled.value) {
+const handleNoSchool = () => {
+  if (!contactPhone.value) {
+    uni.$ie.showToast('请联系客服处理');
     return;
   }
+  uni.showActionSheet({
+    title: '联系客服处理',
+    itemList: [`拨打电话:${contactPhone.value}`],
+    success: (res) => {
+      uni.makePhoneCall({
+        phoneNumber: contactPhone.value
+      });
+    }
+  });
+}
+const handleSchoolSelect = () => {
+  // if (isSchoolDisabled.value || !examTypeForm.value.examType) {
+  //   return;
+  // }
   transferTo('/pagesSystem/pages/school-select/school-select', {
-    data: form.value
+    data: {
+      examType: examTypeForm.value.examType,
+    }
   }).then(res => {
-    const school = res as SchoolItem;
-    form.value.schoolId = school.id;
-    form.value.schoolName = school.name;
-    console.log(form.value)
-    form.value.classId = undefined;
-    classList.value = [];
-    handleGetClassList();
+    if (res) {
+      const school = res as SchoolItem;
+      form.value.schoolId = school.id;
+      form.value.schoolName = school.name;
+      console.log(form.value)
+      form.value.classId = undefined;
+      classList.value = [];
+      handleGetClassList();
+    }
   });
 }
 const handleGetClassList = () => {
@@ -178,44 +218,44 @@ const loginValidate = () => {
     return false;
   }
   if (showCulture.value) {
-    if (isImproveMode.value) {
+    if (isBindMode.value) {
       if (!scoresForm.value.chinese || scoresForm.value.chinese < 0 || scoresForm.value.chinese > 100) {
         uni.$ie.showToast('请输入正确的语文成绩');
         return false;
       }
     }
-    if (isImproveMode.value) {
+    if (isBindMode.value) {
       if (!scoresForm.value.mathematics || scoresForm.value.mathematics < 0 || scoresForm.value.mathematics > 100) {
         uni.$ie.showToast('请输入正确的数学成绩');
         return false;
       }
     }
-    if (isImproveMode.value) {
+    if (isBindMode.value) {
       if (!scoresForm.value.foreign || scoresForm.value.foreign < 0 || scoresForm.value.foreign > 100) {
         uni.$ie.showToast('请输入正确的外语成绩');
         return false;
       }
     }
-    if (isImproveMode.value) {
+    if (isBindMode.value) {
       if (!scoresForm.value.physics || scoresForm.value.physics < 0 || scoresForm.value.physics > 100) {
         uni.$ie.showToast('请输入正确的物理成绩');
         return false;
       }
     }
-    if (isImproveMode.value) {
+    if (isBindMode.value) {
       if (!scoresForm.value.political || scoresForm.value.political < 0 || scoresForm.value.political > 100) {
         uni.$ie.showToast('请输入正确的政治成绩');
         return false;
       }
     }
   }
-  if (isImproveMode.value) {
+  if (isBindMode.value) {
     if (!form.value.schoolId) {
       uni.$ie.showToast('请选择学校');
       return false;
     }
   }
-  if (isImproveMode.value) {
+  if (isBindMode.value) {
     if (!form.value.classId) {
       uni.$ie.showToast('请选择班级');
       return false;
@@ -228,56 +268,89 @@ const handleSubmit = async () => {
   if (valid) {
     let params = {
       ...form.value,
+      ...examTypeForm.value,
       scores: scoresForm.value,
     };
+
+    // 接下来补充注册登录信息
     try {
-      if (isImproveMode.value) {
-        const { cardNo, password } = prevData.value;
-        params = {
-          ...params,
-          username: cardNo,
-          password,
-        }
-        console.log('params', params)
-        if (prevData.value.scene === 'card_improve') {
-          startLogin(params as BindCardInfo);
-        } else {
-          startBind(params as BindCardInfo)
-        }
+      if (prevData.value.scene === EnumBindScene.REGISTER) {
+        startRegister(params as BindCardInfo);
       } else {
-        const { mobile, password, code, uuid } = prevData.value;
-        params = {
-          ...params,
-          mobile,
-          password,
-          code,
-          uuid,
+        // startRegisterBind(params as BindCardInfo);
+        // params = {
+        //   ...params,
+        //   username: prevData.value.registerInfo.username,
+        //   password: prevData.value.registerInfo.password,
+        // };
+        console.log('初步提交信息:', params);
+        if (prevData.value.scene === EnumBindScene.LOGIN_BIND) {
+          startLoginBind(params as BindCardInfo);
+        } else {
+          startRegister(params as BindCardInfo);
         }
-        startLogin(params as BindCardInfo);
       }
+      // if (isBindMode.value) {
+      //   const { cardNo, password } = prevData.value;
+      //   params = {
+      //     ...params,
+      //     username: cardNo,
+      //     password,
+      //   }
+      //   console.log('params', params)
+      //   if (prevData.value.scene === 'card_improve') {
+      //     startRegister(params as BindCardInfo);
+      //   } else {
+      //     startRegisterBind(params as BindCardInfo)
+      //   }
+      // } else {
+      //   const { mobile, password, code, uuid } = prevData.value;
+      //   params = {
+      //     ...params,
+      //     mobile,
+      //     password,
+      //     code,
+      //     uuid,
+      //   }
+      //   startRegister(params as BindCardInfo);
+      // }
     } catch (error) {
       console.error(error)
     }
   }
 }
 
-const startBind = async (params: BindCardInfo) => {
+const startLoginBind = async (params: BindCardInfo) => {
   uni.$ie.showLoading();
-  await improve(params);
+  const token = prevData.value.token;
+  await improveWithToken(params, token);
   uni.$ie.hideLoading();
   uni.$ie.showSuccess('绑定成功');
-  userStore.getUserInfo();
-  goHome();
+  const userStore = useUserStore();
+  userStore.setToken(token);
+  setTimeout(() => {
+    userStore.getUserInfo();
+    goHome();
+  }, 50);
 }
 
-const startLogin = async (params: BindCardInfo) => {
+// const startRegisterBind = async (params: BindCardInfo) => {
+//   uni.$ie.showLoading();
+//   await improve(params);
+//   uni.$ie.hideLoading();
+//   uni.$ie.showSuccess('绑定成功');
+//   userStore.getUserInfo();
+//   goHome();
+// }
+
+const startRegister = async (params: BindCardInfo) => {
   uni.$ie.showLoading();
   const { token } = await registry(params);
   if (token) {
-    const isLogin = await userStore.login(token);
+    const { success } = await userStore.login(token);
     uni.$ie.hideLoading();
     uni.$ie.showSuccess('登录成功');
-    if (isLogin) {
+    if (success) {
       goHome();
     }
   }
@@ -288,53 +361,92 @@ const goHome = () => {
     transferTo('/pagesMain/pages/index/index', {
       type: 'reLaunch'
     });
-  }, 600);
+  }, 800);
 }
 
 const gatherInfo = () => {
+  console.log('数据预览:', prevData.value)
   // const { scene, card, phone, code, uuid } = prevData.value;
-  const { cardInfo, userInfo, loginInfo } = prevData.value;
-  console.log('prevData.value', prevData.value)
-  if (userStore.tempInfo?.location) {
+  let { cardInfo = {} as CardInfo, userInfo = {} as UserInfo, registerInfo = {} as RegisterInfo, scene, token } = prevData.value as PrevDataInfo;
+  form.value = {
+    code: registerInfo.code,
+    uuid: registerInfo.uuid,
+    mobile: registerInfo.mobile,
+    username: registerInfo.username,
+    password: registerInfo.password,
+  }
+  // 注册时,如果以游客身份使用过,则从临时信息中获取省份和考试类型
+  if (scene === EnumBindScene.REGISTER) {
+    console.log('自动填写临时信息:', userStore.tempInfo)
     examTypeForm.value.location = userStore.tempInfo?.location;
     setTimeout(() => {
       examTypeForm.value.examType = userStore.tempInfo!.examType;
-    }, 0)
-  }
-  console.log('examTypeForm', examTypeForm)
-  const { scene, phone } = loginInfo;
-  const { nickName, location, examType, majorType: userMajorType, endYear: userEndYear, scores } = userInfo;
-  const { assignLocation, assignExamType, majorType, endYear: cardEndYear, assignSchoolId, assignSchoolName, classId, } = cardInfo;
-  if (scene === 'card_improve') {
-    // 未登录用户卡注册登录
+    }, 0);
+  } else if ([EnumBindScene.LOGIN_BIND, EnumBindScene.REGISTER_BIND].includes(scene)) {
+    // 已有卡信息,补充信息
+    if (scene === EnumBindScene.LOGIN_BIND) {
+      form.value = {
+        ...form.value,
+        ...userInfo,
+      };
+    }
     form.value = {
-      nickName,
-      scores,
-      // 下面 3 个属性卡的优先级更高
-      location: assignLocation || location,
-      examType: assignExamType || examType,
-      endYear: cardEndYear || userEndYear,
-      majorType: majorType || userMajorType,
-      schoolId: assignSchoolId,
-      schoolName: assignSchoolName,
-      classId: classId,
-      mobile: phone
-      // code
+      ...form.value,
+      schoolName: cardInfo.assignSchoolName,
+      schoolId: cardInfo.assignSchoolId,
+      classId: cardInfo.classId,
+      //
+      // code: registerInfo.code,
+      // uuid: registerInfo.uuid,
+      // mobile: registerInfo.mobile,
+      // username: registerInfo.username,
+      // password: registerInfo.password,
     };
+    // 考生相关信息
+    examTypeForm.value.location = cardInfo.assignLocation || userInfo.location || '';
+    setTimeout(() => {
+      examTypeForm.value.examType = cardInfo.assignExamType || userInfo.examType || '';
+      examTypeForm.value.endYear = cardInfo.endYear || userInfo.endYear;
+    }, 0);
+    examTypeForm.value.majorType = userInfo.majorType || '';
+    // 
+    scoresForm.value = userInfo.scores || {};
     handleGetClassList();
-  } else if (scene === 'phone_improve') {
-    // 已登录用户绑卡
-    // const { nickName, location, examType, endYear, scores } = userStore.userInfo;
-    // const { nickName, location, examType, endYear, scores } = prevData.value.userInfo;
-    form.value = {
-      nickName,
-      location,
-      examType,
-      // endYear,
-      scores
-    };
-    scoresForm.value = scores;
   }
+  console.log('初始化整理的信息:')
+  console.log('form.value:', form.value)
+  console.log('examTypeForm.value:', examTypeForm.value)
+  console.log('scoresForm.value:', scoresForm.value)
+  // if (scene === EnumBindScene.LOGIN_BIND) {
+  // 未登录用户卡注册登录
+  // form.value = {
+  // nickName,
+  // scores,
+  // 下面 3 个属性卡的优先级更高
+  // location: assignLocation || location,
+  // examType: assignExamType || examType,
+  // endYear: cardEndYear || userEndYear,
+  // majorType: majorType || userMajorType,
+  // schoolId: assignSchoolId,
+  // schoolName: assignSchoolName,
+  // classId: classId,
+  // mobile: phone
+  // code
+  // };
+  // handleGetClassList();
+  // } else if (scene === 'phone_improve') {
+  // 已登录用户绑卡
+  // const { nickName, location, examType, endYear, scores } = userStore.userInfo;
+  // const { nickName, location, examType, endYear, scores } = prevData.value.userInfo;
+  // form.value = {
+  //   nickName,
+  //   location,
+  //   examType,
+  //   // endYear,
+  //   scores
+  // };
+  // scoresForm.value = scores;
+  // }
 }
 
 onLoad(() => {

+ 146 - 131
src/pagesSystem/pages/bind-profile/bind-profile.vue

@@ -2,15 +2,15 @@
   <ie-page bg-color="#F6F8FA" :safeAreaInsetBottom="false">
     <ie-navbar title="完善信息"></ie-navbar>
     <uv-form labelPosition="left" :model="form" labelWidth="70px" ref="formRef">
-      <content-card title="考生信息">
-        <uv-form-item label="学生姓名" prop="name" borderBottom required>
+      <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">
           </uv-input>
         </uv-form-item>
         <uv-form-item label="所在省份" prop="location" borderBottom required>
           <ie-picker ref="pickerRef" v-model="examTypeForm.location" :list="provinceList"
-            :placeholder="pickerPlaceholder" :custom-style="customStyle" key-label="dictLabel" key-value="dictValue"
+            :placeholder="pickerPlaceholder" :custom-style="customStyle" key-label="areaName" key-value="shortName"
             :disabled="isProvinceDisabled">
             <template v-if="isProvinceDisabled" #right>
               <ie-image src="/static/image/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
@@ -19,7 +19,8 @@
         </uv-form-item>
         <uv-form-item label="考生类别" prop="examType" borderBottom required>
           <ie-picker ref="pickerRef" v-model="examTypeForm.examType" :list="examTypeList" :disabled="isExamTypeDisabled"
-            :placeholder="pickerPlaceholder" :custom-style="customStyle" key-label="dictLabel" key-value="dictValue">
+            :placeholder="pickerPlaceholder" :custom-style="customStyle" key-label="dictLabel" key-value="dictValue"
+            @click="handlePreCheck('examType')" @change="handleExamTypeChange">
             <template v-if="isExamTypeDisabled" #right>
               <ie-image src="/static/image/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
             </template>
@@ -30,10 +31,10 @@
             :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"></ie-picker>
+            key-label="dictLabel" key-value="dictValue" @click="handlePreCheck('endYear')"></ie-picker>
         </uv-form-item>
 
       </content-card>
@@ -44,34 +45,36 @@
           </uv-input>
         </uv-form-item>
       </content-card>
+
       <content-card v-if="showCulture" title="文化素质">
-        <uv-form-item label="语文" prop="form.scores.chinese" borderBottom :required="isBindMode">
+        <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">
           </uv-input>
         </uv-form-item>
-        <uv-form-item label="数学" prop="form.score.mathematics" borderBottom :required="isBindMode">
+        <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">
           </uv-input>
         </uv-form-item>
-        <uv-form-item label="外语" prop="form.scores.foreign" borderBottom :required="isBindMode">
+        <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">
           </uv-input>
         </uv-form-item>
-        <uv-form-item label="物理" prop="form.scores.physics" borderBottom :required="isBindMode">
+        <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">
           </uv-input>
         </uv-form-item>
-        <uv-form-item label="政治" prop="form.scores.political" :required="isBindMode">
+        <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">
           </uv-input>
         </uv-form-item>
       </content-card>
-      <content-card v-if="isBindMode" title="学校信息">
+
+      <content-card v-if="showSchoolInfo" title="学校信息">
         <template #right>
           <view class="text-26 text-primary underline" @click="handleNoSchool">没有我的学校班级?</view>
         </template>
@@ -85,7 +88,12 @@
         </uv-form-item>
         <uv-form-item label="所在班级" prop="form.name" :required="isBindMode">
           <ie-picker ref="pickerRef" v-model="form.classId" :list="classList" :disabled="!form.schoolId" title="选择班级"
-            placeholder="请选择" :custom-style="customStyle" key-label="name" key-value="classId"></ie-picker>
+            placeholder="请选择" :custom-style="customStyle" key-label="name" key-value="classId"
+            @click="handlePreCheck('classId')">
+            <template v-if="isClassDisabled" #right>
+              <ie-image src="/static/image/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
+            </template>
+          </ie-picker>
         </uv-form-item>
       </content-card>
     </uv-form>
@@ -107,7 +115,7 @@ import { useAppStore } from '@/store/appStore';
 import { BindCardInfo, CardInfo, ClassItem, LoginInfo, RegisterInfo, SchoolItem, Scores, UserInfo } from '@/types/user';
 
 import { getClassList } from '@/api/modules/user';
-import { EnumBindScene, EnumExamRecordType, EnumExamType } from '@/common/enum';
+import { EnumBindScene, EnumExamRecordType, EnumExamType, EnumUserType } from '@/common/enum';
 type PrevDataInfo = {
   cardInfo: CardInfo;
   userInfo: UserInfo;
@@ -127,11 +135,15 @@ const customStyle = {
   paddingLeft: '26px'
 };
 
+// 普高-文化成绩必填,保存后锁定不可修改
+// 中职务-文化成绩不填,保存后可修改
+const isScoreRequired = computed(() => examTypeForm.value.examType === EnumExamType.OHS);
 const isBindMode = computed(() => [EnumBindScene.LOGIN_BIND, EnumBindScene.REGISTER_BIND].includes(prevData.value.scene));
-const isSchoolDisabled = computed(() => isBindMode.value && prevData.value.cardInfo.assignSchoolId);
-const isProvinceDisabled = computed(() => isBindMode.value && prevData.value.cardInfo.assignLocation);
-const isExamTypeDisabled = computed(() => (isBindMode.value && prevData.value.cardInfo.assignExamType) || !examTypeForm.value.location);
-
+const isSchoolDisabled = computed(() => isBindMode.value && !!prevData.value.cardInfo.assignSchoolId);
+const isClassDisabled = computed(() => isBindMode.value && !!prevData.value.cardInfo.classId);
+const isProvinceDisabled = computed(() => isBindMode.value && !!prevData.value.cardInfo.assignLocation);
+const isExamTypeDisabled = computed(() => (isBindMode.value && !!prevData.value.cardInfo.assignExamType));
+const contactPhone = computed(() => userStore.orgInfo.contactPhone);
 const inputPlaceholder = computed(() => {
   return isBindMode.value ? '请输入(提交后不可修改)' : '请输入';
 });
@@ -139,25 +151,37 @@ const pickerPlaceholder = computed(() => {
   return isBindMode.value ? '请选择(提交后不可修改)' : '请选择';
 });
 
+const showSchoolInfo = computed(() => {
+  return isBindMode.value;
+})
+
 const classList = ref<ClassItem[]>([]);
 const showCulture = computed(() => {
   return examTypeForm.value.examType === EnumExamType.OHS;
 });
 const handleNoSchool = () => {
+  if (!contactPhone.value) {
+    uni.$ie.showToast('请联系客服处理');
+    return;
+  }
   uni.showActionSheet({
     title: '联系客服处理',
-    itemList: ['拨打电话:400-1797-985'],
+    itemList: [`拨打电话:${contactPhone.value}`],
     success: (res) => {
       uni.makePhoneCall({
-        phoneNumber: '400-1797-985'
+        phoneNumber: contactPhone.value
       });
     }
   });
 }
 const handleSchoolSelect = () => {
-  // if (isSchoolDisabled.value || !examTypeForm.value.examType) {
-  //   return;
-  // }
+  if (isSchoolDisabled.value) {
+    return;
+  }
+  if (!examTypeForm.value.examType) {
+    uni.$ie.showToast('请选择考生类别');
+    return;
+  }
   transferTo('/pagesSystem/pages/school-select/school-select', {
     data: {
       examType: examTypeForm.value.examType,
@@ -184,6 +208,42 @@ const handleGetClassList = () => {
   });
 }
 
+const handlePreCheck = (type: string) => {
+  switch (type) {
+    case 'examType': {
+      if (isExamTypeDisabled.value) {
+        return true;
+      }
+      const { location } = examTypeForm.value;
+      if (!location) {
+        uni.$ie.showToast('请选择省份');
+        return false;
+      }
+      return true;
+    }
+    case 'endYear': {
+      const { examType } = examTypeForm.value;
+      const result = handlePreCheck('examType');
+      if (!result) {
+        return false;
+      }
+      if (!examType) {
+        uni.$ie.showToast('请选择考生类别');
+        return false;
+      }
+      return true;
+    }
+    case 'classId': {
+      const { schoolId } = form.value;
+      if (!schoolId) {
+        uni.$ie.showToast('请选择学校');
+        return false;
+      }
+      return true;
+    }
+  }
+}
+
 
 const loginValidate = () => {
   form.value = {
@@ -214,36 +274,60 @@ const loginValidate = () => {
     return false;
   }
   if (showCulture.value) {
-    if (isBindMode.value) {
-      if (!scoresForm.value.chinese || scoresForm.value.chinese < 0 || scoresForm.value.chinese > 100) {
-        uni.$ie.showToast('请输入正确的语文成绩');
+    if (isScoreRequired.value) {
+      if (!scoresForm.value.chinese) {
+        uni.$ie.showToast('请输入语文成绩');
         return false;
       }
     }
-    if (isBindMode.value) {
-      if (!scoresForm.value.mathematics || scoresForm.value.mathematics < 0 || scoresForm.value.mathematics > 100) {
-        uni.$ie.showToast('请输入正确的数学成绩');
+    if (scoresForm.value?.chinese && (scoresForm.value.chinese < 0 || scoresForm.value.chinese > 100)) {
+      uni.$ie.showToast('请输入正确的语文成绩');
+      return false;
+    }
+    // 
+    if (isScoreRequired.value) {
+      if (!scoresForm.value.mathematics) {
+        uni.$ie.showToast('请输入数学成绩');
         return false;
       }
     }
-    if (isBindMode.value) {
-      if (!scoresForm.value.foreign || scoresForm.value.foreign < 0 || scoresForm.value.foreign > 100) {
-        uni.$ie.showToast('请输入正确的外语成绩');
+    if (scoresForm.value?.mathematics && (scoresForm.value.mathematics < 0 || scoresForm.value.mathematics > 100)) {
+      uni.$ie.showToast('请输入正确的数学成绩');
+      return false;
+    }
+    //
+    if (isScoreRequired.value) {
+      if (!scoresForm.value.foreign) {
+        uni.$ie.showToast('请输入外语成绩');
         return false;
       }
     }
-    if (isBindMode.value) {
-      if (!scoresForm.value.physics || scoresForm.value.physics < 0 || scoresForm.value.physics > 100) {
-        uni.$ie.showToast('请输入正确的物理成绩');
+    if (scoresForm.value?.foreign && (scoresForm.value.foreign < 0 || scoresForm.value.foreign > 100)) {
+      uni.$ie.showToast('请输入正确的外语成绩');
+      return false;
+    }
+    // 
+    if (isScoreRequired.value) {
+      if (!scoresForm.value.physics) {
+        uni.$ie.showToast('请输入物理成绩');
         return false;
       }
     }
-    if (isBindMode.value) {
-      if (!scoresForm.value.political || scoresForm.value.political < 0 || scoresForm.value.political > 100) {
-        uni.$ie.showToast('请输入正确的政治成绩');
+    if (scoresForm.value?.physics && (scoresForm.value.physics < 0 || scoresForm.value.physics > 100)) {
+      uni.$ie.showToast('请输入正确的物理成绩');
+      return false;
+    }
+    //
+    if (isScoreRequired.value) {
+      if (!scoresForm.value.political) {
+        uni.$ie.showToast('请输入政治成绩');
         return false;
       }
     }
+    if (scoresForm.value?.political && (scoresForm.value.political < 0 || scoresForm.value.political > 100)) {
+      uni.$ie.showToast('请输入正确的政治成绩');
+      return false;
+    }
   }
   if (isBindMode.value) {
     if (!form.value.schoolId) {
@@ -259,6 +343,11 @@ const loginValidate = () => {
   }
   return true;
 }
+
+const handleExamTypeChange = () => {
+  scoresForm.value = {};
+}
+
 const handleSubmit = async () => {
   const valid = loginValidate();
   if (valid) {
@@ -270,46 +359,16 @@ const handleSubmit = async () => {
 
     // 接下来补充注册登录信息
     try {
+      console.log('初步提交信息:', prevData.value.scene, params);
       if (prevData.value.scene === EnumBindScene.REGISTER) {
         startRegister(params as BindCardInfo);
       } else {
-        // startRegisterBind(params as BindCardInfo);
-        // params = {
-        //   ...params,
-        //   username: prevData.value.registerInfo.username,
-        //   password: prevData.value.registerInfo.password,
-        // };
-        console.log('初步提交信息:', params);
         if (prevData.value.scene === EnumBindScene.LOGIN_BIND) {
           startLoginBind(params as BindCardInfo);
         } else {
           startRegister(params as BindCardInfo);
         }
       }
-      // if (isBindMode.value) {
-      //   const { cardNo, password } = prevData.value;
-      //   params = {
-      //     ...params,
-      //     username: cardNo,
-      //     password,
-      //   }
-      //   console.log('params', params)
-      //   if (prevData.value.scene === 'card_improve') {
-      //     startRegister(params as BindCardInfo);
-      //   } else {
-      //     startRegisterBind(params as BindCardInfo)
-      //   }
-      // } else {
-      //   const { mobile, password, code, uuid } = prevData.value;
-      //   params = {
-      //     ...params,
-      //     mobile,
-      //     password,
-      //     code,
-      //     uuid,
-      //   }
-      //   startRegister(params as BindCardInfo);
-      // }
     } catch (error) {
       console.error(error)
     }
@@ -319,34 +378,30 @@ const handleSubmit = async () => {
 const startLoginBind = async (params: BindCardInfo) => {
   uni.$ie.showLoading();
   const token = prevData.value.token;
-  await improveWithToken(params, token);
-  uni.$ie.hideLoading();
-  uni.$ie.showSuccess('绑定成功');
-  const userStore = useUserStore();
-  userStore.setToken(token);
-  setTimeout(() => {
-    userStore.getUserInfo();
-    goHome();
-  }, 50);
+  try {
+    await improveWithToken(params, token);
+    uni.$ie.hideLoading();
+    uni.$ie.showSuccess('绑定成功');
+    const userStore = useUserStore();
+    userStore.setToken(token);
+    setTimeout(() => {
+      userStore.getUserInfo();
+      goHome();
+    }, 50);
+  } catch (error) {
+    uni.$ie.hideLoading();
+    uni.$ie.showToast('绑定失败');
+  }
 }
 
-// const startRegisterBind = async (params: BindCardInfo) => {
-//   uni.$ie.showLoading();
-//   await improve(params);
-//   uni.$ie.hideLoading();
-//   uni.$ie.showSuccess('绑定成功');
-//   userStore.getUserInfo();
-//   goHome();
-// }
-
 const startRegister = async (params: BindCardInfo) => {
   uni.$ie.showLoading();
   const { token } = await registry(params);
   if (token) {
-    const isLogin = await userStore.login(token);
+    const { success } = await userStore.login(token);
     uni.$ie.hideLoading();
     uni.$ie.showSuccess('登录成功');
-    if (isLogin) {
+    if (success) {
       goHome();
     }
   }
@@ -362,7 +417,6 @@ const goHome = () => {
 
 const gatherInfo = () => {
   console.log('数据预览:', prevData.value)
-  // const { scene, card, phone, code, uuid } = prevData.value;
   let { cardInfo = {} as CardInfo, userInfo = {} as UserInfo, registerInfo = {} as RegisterInfo, scene, token } = prevData.value as PrevDataInfo;
   form.value = {
     code: registerInfo.code,
@@ -391,17 +445,12 @@ const gatherInfo = () => {
       schoolName: cardInfo.assignSchoolName,
       schoolId: cardInfo.assignSchoolId,
       classId: cardInfo.classId,
-      //
-      // code: registerInfo.code,
-      // uuid: registerInfo.uuid,
-      // mobile: registerInfo.mobile,
-      // username: registerInfo.username,
-      // password: registerInfo.password,
     };
     // 考生相关信息
     examTypeForm.value.location = cardInfo.assignLocation || userInfo.location || '';
+    console.log(examTypeForm.value.location)
     setTimeout(() => {
-      examTypeForm.value.examType = cardInfo.assignExamType || userInfo.examType || '';
+      examTypeForm.value.examType = cardInfo.assignExamType || userInfo.examType || undefined;
       examTypeForm.value.endYear = cardInfo.endYear || userInfo.endYear;
     }, 0);
     examTypeForm.value.majorType = userInfo.majorType || '';
@@ -409,40 +458,6 @@ const gatherInfo = () => {
     scoresForm.value = userInfo.scores || {};
     handleGetClassList();
   }
-  console.log('初始化整理的信息:')
-  console.log('form.value:', form.value)
-  console.log('examTypeForm.value:', examTypeForm.value)
-  console.log('scoresForm.value:', scoresForm.value)
-  // if (scene === EnumBindScene.LOGIN_BIND) {
-  // 未登录用户卡注册登录
-  // form.value = {
-  // nickName,
-  // scores,
-  // 下面 3 个属性卡的优先级更高
-  // location: assignLocation || location,
-  // examType: assignExamType || examType,
-  // endYear: cardEndYear || userEndYear,
-  // majorType: majorType || userMajorType,
-  // schoolId: assignSchoolId,
-  // schoolName: assignSchoolName,
-  // classId: classId,
-  // mobile: phone
-  // code
-  // };
-  // handleGetClassList();
-  // } else if (scene === 'phone_improve') {
-  // 已登录用户绑卡
-  // const { nickName, location, examType, endYear, scores } = userStore.userInfo;
-  // const { nickName, location, examType, endYear, scores } = prevData.value.userInfo;
-  // form.value = {
-  //   nickName,
-  //   location,
-  //   examType,
-  //   // endYear,
-  //   scores
-  // };
-  // scoresForm.value = scores;
-  // }
 }
 
 onLoad(() => {

+ 282 - 0
src/pagesSystem/pages/bind-teacher-profile/bind-teacher-profile.vue

@@ -0,0 +1,282 @@
+<template>
+  <ie-page bg-color="#F6F8FA" :safeAreaInsetBottom="false">
+    <ie-navbar title="完善信息" custom-back @left-click="handleBack" />
+    <uv-form labelPosition="left" :model="examTypeForm" labelWidth="70px" ref="formRef">
+      <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">
+          </uv-input>
+        </uv-form-item>
+        <uv-form-item label="所在省份" prop="location" borderBottom required>
+          <ie-picker ref="pickerRef" v-model="examTypeForm.location" :list="provinceList"
+            :placeholder="pickerPlaceholder" :custom-style="customStyle" key-label="areaName" key-value="shortName"
+            :disabled="disabledEdit">
+            <template v-if="disabledEdit" #right>
+              <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="examType" borderBottom required>
+          <ie-picker ref="pickerRef" v-model="examTypeForm.examType" :list="examTypeList"
+            :disabled="!examTypeForm.location || disabledEdit" :placeholder="pickerPlaceholder"
+            :custom-style="customStyle" key-label="dictLabel" key-value="dictValue" @change="handleExamTypeChange">
+            <template v-if="disabledEdit" #right>
+              <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 v-if="examTypeForm.examType === 'VHS'" label="专业类别" prop="majorType" borderBottom required>
+          <ie-picker ref="pickerRef" v-model="examTypeForm.majorType" :list="examMajorList"
+            :disabled="!examTypeForm.examType || disabledEdit" :placeholder="pickerPlaceholder"
+            :custom-style="customStyle" key-label="dictLabel" key-value="dictValue">
+            <template v-if="disabledEdit" #right>
+              <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>
+          <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">
+            <template v-if="disabledEdit" #right>
+              <ie-image src="/static/image/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
+            </template></ie-picker>
+        </uv-form-item>
+      </content-card>
+
+      <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">
+          </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">
+          </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">
+          </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">
+          </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">
+          </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>
+      </content-card>
+
+      <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"
+            :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>
+          </uv-input>
+          <ie-image slot="right" src="/static/image/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
+        </uv-form-item>
+      </content-card>
+    </uv-form>
+    <ie-safe-toolbar :height="84" :shadow="false">
+      <view class="px-30 py-16">
+        <ie-button @click="handleSubmit">确认提交</ie-button>
+      </view>
+    </ie-safe-toolbar>
+  </ie-page>
+</template>
+
+<script lang="ts" setup>
+import ContentCard from '@/pagesSystem/components/content-card.vue';
+import { useExamType } from '@/composables/useExamType';
+import { updateUserInfo } from '@/api/modules/login';
+import { useUserStore } from '@/store/userStore';
+import { ClassItem, Scores, UserInfo } from '@/types/user';
+import { useTransferPage } from '@/hooks/useTransferPage';
+import { EnumExamType, EnumUserType } from '@/common/enum';
+import { getClassList } from '@/api/modules/user';
+const { prevData, transferTo, transferBack } = useTransferPage();
+const { form: examTypeForm, examTypeList, examMajorList, provinceList, endYearList } = useExamType();
+const userStore = useUserStore();
+const userInfo = computed(() => userStore.userInfo);
+
+const classList = ref<ClassItem[]>([]);
+const customStyle = {
+  paddingLeft: '26px'
+};
+const inputPlaceholder = computed(() => {
+  return true ? '请输入(提交后不可修改)' : '请输入';
+});
+const pickerPlaceholder = computed(() => {
+  return true ? '请选择(提交后不可修改)' : '请选择';
+});
+
+const form = ref<UserInfo>({
+  ...userInfo.value,
+});
+const scoresForm = ref<Scores>({})
+examTypeForm.value.location = form.value.location;
+examTypeForm.value.examType = form.value.examType;
+examTypeForm.value.endYear = form.value.endYear;
+examTypeForm.value.majorType = form.value.majorType;
+scoresForm.value = form.value.scores || {}
+
+const disabledEdit = computed(() => {
+  return !!userInfo.value.location;
+})
+// 普高-文化成绩必填,保存后锁定不可修改
+// 中职务-文化成绩不填,保存后可修改
+const isScoreRequired = computed(() => examTypeForm.value.examType === EnumExamType.OHS);
+const showCulture = computed(() => {
+  return examTypeForm.value.examType === EnumExamType.OHS;
+});
+// 代理机构不显示学校
+const showSchoolInfo = computed(() => userStore.userInfo.userType !== EnumUserType.AGENT);
+
+const handleBack = () => {
+  if (disabledEdit.value) {
+    transferBack();
+    return;
+  }
+  const pages = getCurrentPages();
+  const page = pages[pages.length - 2];
+  // 如果是登录页,允许直接返回
+  if (page?.route === 'pagesSystem/pages/login/login') {
+    transferBack();
+    return;
+  }
+  uni.$ie.showToast('请先完善信息');
+};
+const loginValidate = () => {
+  form.value = {
+    ...form.value,
+    ...examTypeForm.value,
+  }
+  const { nickName, location, examType, endYear } = form.value;
+  if (!nickName || nickName.trim() === '') {
+    uni.$ie.showToast('请输入姓名');
+    return false;
+  }
+  if (!location || location.trim() === '') {
+    uni.$ie.showToast('请选择省份');
+    return false;
+  }
+  if (!examType || examType.trim() === '') {
+    uni.$ie.showToast('请选择考生类别');
+    return false;
+  }
+  if (examType === 'VHS') {
+    if (!form.value.majorType) {
+      uni.$ie.showToast('请选择专业类别');
+      return false;
+    }
+  }
+  if (!endYear) {
+    uni.$ie.showToast('请选择毕业年份');
+    return false;
+  }
+  if (showCulture.value) {
+    if (isScoreRequired.value) {
+      if (!scoresForm.value.chinese) {
+        uni.$ie.showToast('请输入语文成绩');
+        return false;
+      }
+    }
+    if (scoresForm.value?.chinese && (scoresForm.value.chinese < 0 || scoresForm.value.chinese > 100)) {
+      uni.$ie.showToast('请输入正确的语文成绩');
+      return false;
+    }
+    // 
+    if (isScoreRequired.value) {
+      if (!scoresForm.value.mathematics) {
+        uni.$ie.showToast('请输入数学成绩');
+        return false;
+      }
+    }
+    if (scoresForm.value?.mathematics && (scoresForm.value.mathematics < 0 || scoresForm.value.mathematics > 100)) {
+      uni.$ie.showToast('请输入正确的数学成绩');
+      return false;
+    }
+    //
+    if (isScoreRequired.value) {
+      if (!scoresForm.value.foreign) {
+        uni.$ie.showToast('请输入外语成绩');
+        return false;
+      }
+    }
+    if (scoresForm.value?.foreign && (scoresForm.value.foreign < 0 || scoresForm.value.foreign > 100)) {
+      uni.$ie.showToast('请输入正确的外语成绩');
+      return false;
+    }
+    // 
+    if (isScoreRequired.value) {
+      if (!scoresForm.value.physics) {
+        uni.$ie.showToast('请输入物理成绩');
+        return false;
+      }
+    }
+    if (scoresForm.value?.physics && (scoresForm.value.physics < 0 || scoresForm.value.physics > 100)) {
+      uni.$ie.showToast('请输入正确的物理成绩');
+      return false;
+    }
+    //
+    if (isScoreRequired.value) {
+      if (!scoresForm.value.political) {
+        uni.$ie.showToast('请输入政治成绩');
+        return false;
+      }
+    }
+    if (scoresForm.value?.political && (scoresForm.value.political < 0 || scoresForm.value.political > 100)) {
+      uni.$ie.showToast('请输入正确的政治成绩');
+      return false;
+    }
+  }
+  return true;
+}
+
+const handleExamTypeChange = () => {
+  scoresForm.value = {};
+}
+
+const handleSubmit = async () => {
+  console.log('handleSubmit', examTypeForm.value)
+  if (!loginValidate()) {
+    return;
+  }
+  const params = {
+    ...userStore.userInfo,
+    ...form.value,
+    scores: scoresForm.value,
+  } as UserInfo;
+  console.log(params)
+  uni.$ie.showLoading();
+  await updateUserInfo(params);
+  await userStore.getUserInfo();
+  uni.$ie.hideLoading();
+  uni.$ie.showToast('保存成功');
+  setTimeout(() => {
+    transferTo('/pagesMain/pages/index/index', {
+      type: 'reLaunch'
+    });
+  }, 800);
+};
+
+</script>
+
+<style></style>

+ 65 - 53
src/pagesSystem/pages/edit-student-profile/edit-student-profile.vue → src/pagesSystem/pages/edit-profile/edit-profile.vue

@@ -3,8 +3,8 @@
     <ie-navbar title="基本信息" />
     <view class="">
       <uv-form labelPosition="left" :model="form" labelWidth="70px" ref="formRef">
-        <content-card :title="userStore.isStudent ? '考生信息' : '个人信息'">
-          <uv-form-item :label="nameLabel" prop="name" borderBottom>
+        <content-card title="个人信息">
+          <uv-form-item label="姓名" prop="name" borderBottom>
             <uv-input v-model="form.nickName" border="none" :readonly="!userStore.isStudent" placeholder="请输入姓名"
               placeholderClass="text-30" font-size="30rpx" :custom-style="customStyle">
             </uv-input>
@@ -19,67 +19,68 @@
             <uv-input v-model="form.location" border="none" placeholder="" placeholderClass="text-30" font-size="30rpx"
               :custom-style="customStyle" readonly>
             </uv-input>
-            <ie-image slot="right" src="/static/image/icon-lock.png" custom-class="w-24 h-30"
-              mode="aspectFill" />
+            <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" borderBottom>
-            <view class="flex-1 pl-[26px]">
+            <view class="flex-1 pl-[26px] text-30">
               <ie-dict :dictName="EnumDictName.EXAM_TYPE" :dictValue="form.examType" />
             </view>
-            <ie-image slot="right" src="/static/image/icon-lock.png" custom-class="w-24 h-30"
-              mode="aspectFill" />
+            <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-30" font-size="30rpx"
               :custom-style="customStyle" readonly>
             </uv-input>
-            <ie-image slot="right" src="/static/image/icon-lock.png" custom-class="w-24 h-30"
-              mode="aspectFill" />
+            <ie-image slot="right" src="/static/image/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
           </uv-form-item>
         </content-card>
 
-        <content-card v-if="userStore.isStudent" title="文化素质">
+        <content-card title="文化素质">
           <uv-form-item label="语文" prop="name" borderBottom>
-            <uv-input v-model="scores.chinese" border="none" placeholder="请输入" placeholderClass="text-30"
+            <uv-input v-model="scores.chinese" border="none"
+              :placeholder="form.examType === EnumExamType.OHS ? '' : '请输入'" placeholderClass="text-30"
               font-size="30rpx" :custom-style="customStyle" :readonly="form.examType === EnumExamType.OHS">
             </uv-input>
-            <ie-image v-if="form.examType === EnumExamType.OHS" slot="right"
-              src="/static/image/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
+            <ie-image v-if="form.examType === EnumExamType.OHS" 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" borderBottom>
-            <uv-input v-model="scores.mathematics" border="none" placeholder="请输入" placeholderClass="text-30"
+            <uv-input v-model="scores.mathematics" border="none"
+              :placeholder="form.examType === EnumExamType.OHS ? '' : '请输入'" placeholderClass="text-30"
               font-size="30rpx" :custom-style="customStyle" :readonly="form.examType === EnumExamType.OHS">
             </uv-input>
-            <ie-image v-if="form.examType === EnumExamType.OHS" slot="right"
-              src="/static/image/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
+            <ie-image v-if="form.examType === EnumExamType.OHS" 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" :borderBottom="form.examType === EnumExamType.OHS">
-            <uv-input v-model="scores.foreign" border="none" placeholder="请输入" placeholderClass="text-30"
+          <uv-form-item label="外语" prop="name" :borderBottom="form.examType === EnumExamType.OHS || form.examType === EnumExamType.SVS">
+            <uv-input v-model="scores.foreign" border="none"
+              :placeholder="form.examType === EnumExamType.OHS ? '' : '请输入'" placeholderClass="text-30"
               font-size="30rpx" :custom-style="customStyle" :readonly="form.examType === EnumExamType.OHS">
             </uv-input>
-            <ie-image v-if="form.examType === EnumExamType.OHS" slot="right"
-              src="/static/image/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
+            <ie-image v-if="form.examType === EnumExamType.OHS" slot="right" src="/static/image/icon-lock.png"
+              custom-class="w-24 h-30" mode="aspectFill" />
           </uv-form-item>
-          <block v-if="[EnumExamType.OHS].includes(form.examType)">
+          <block v-if="[EnumExamType.OHS, EnumExamType.SVS].includes(form.examType)">
             <uv-form-item label="物理" prop="name" borderBottom>
-              <uv-input v-model="scores.physics" border="none" placeholder="请输入" placeholderClass="text-30"
+              <uv-input v-model="scores.physics" border="none"
+                :placeholder="form.examType === EnumExamType.OHS ? '' : '请输入'" placeholderClass="text-30"
                 font-size="30rpx" :custom-style="customStyle" :readonly="form.examType === EnumExamType.OHS">
               </uv-input>
-              <ie-image v-if="form.examType === EnumExamType.OHS" slot="right"
-                src="/static/image/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
+              <ie-image v-if="form.examType === EnumExamType.OHS" 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-input v-model="scores.political" border="none" placeholder="请输入" placeholderClass="text-30"
+              <uv-input v-model="scores.political" border="none"
+                :placeholder="form.examType === EnumExamType.OHS ? '' : '请输入'" placeholderClass="text-30"
                 font-size="30rpx" :custom-style="customStyle" :readonly="form.examType === EnumExamType.OHS">
               </uv-input>
-              <ie-image v-if="form.examType === EnumExamType.OHS" slot="right"
-                src="/static/image/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
+              <ie-image v-if="form.examType === EnumExamType.OHS" slot="right" src="/static/image/icon-lock.png"
+                custom-class="w-24 h-30" mode="aspectFill" />
             </uv-form-item>
           </block>
         </content-card>
 
-        <content-card v-if="userStore.isStudent && ([EnumExamType.OHS, EnumExamType.SVS].includes(form.examType))"
-          title="职业技能成绩">
+        <content-card v-if="([EnumExamType.OHS, EnumExamType.SVS].includes(form.examType))" title="职业技能成绩">
           <uv-form-item label="职业技能" prop="name">
             <uv-input v-model.number="scores.skill" border="none" placeholder="请输入" placeholderClass="text-30"
               font-size="30rpx" :custom-style="customStyle">
@@ -87,22 +88,37 @@
           </uv-form-item>
         </content-card>
 
-        <content-card v-if="userStore.isVip && userStore.isStudent" title="学校信息">
-          <uv-form-item label="学校名称" prop="form.name" borderBottom>
-            <uv-input v-model="form.schoolName" border="none" placeholder="" placeholderClass="text-30"
+        <template v-if="userStore.isStudent">
+          <content-card v-if="userStore.isVip" title="学校信息">
+            <uv-form-item label="学校名称" prop="form.name" borderBottom>
+              <uv-input v-model="form.schoolName" border="none" placeholder="" placeholderClass="text-30"
+                font-size="30rpx" :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.name">
+              <ie-picker ref="pickerRef" v-model="form.classId" :list="classList" title="选择班级" placeholder="请选择所在班级"
+                :custom-style="customStyle" key-label="name" key-value="classId"></ie-picker>
+            </uv-form-item>
+          </content-card>
+        </template>
+        <content-card v-else-if="userStore.isTeacher" title="学校信息">
+          <uv-form-item label="学校名称" prop="form.campusName" borderBottom>
+            <uv-input v-model="form.campusName" border="none" placeholder="" placeholderClass="text-30"
               font-size="30rpx" :custom-style="customStyle" readonly>
             </uv-input>
-            <ie-image slot="right" src="/static/image/icon-lock.png" custom-class="w-24 h-30"
-              mode="aspectFill" />
+            <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.name">
-            <ie-picker ref="pickerRef" v-model="form.classId" :list="classList" title="选择班级" placeholder="请选择所在班级"
-              :custom-style="customStyle" key-label="name" key-value="classId"></ie-picker>
+          <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>
+            </uv-input>
+            <ie-image slot="right" src="/static/image/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
           </uv-form-item>
         </content-card>
       </uv-form>
     </view>
-    <ie-safe-toolbar v-if="userStore.isStudent" :height="84" :shadow="false">
+    <ie-safe-toolbar :height="84" :shadow="false">
       <view class="px-30 py-16">
         <ie-button @click="handleSubmit">确认保存</ie-button>
       </view>
@@ -136,24 +152,20 @@ const userStore = useUserStore();
 const userInfo = computed(() => userStore.userInfo);
 const cardInfo = computed(() => userStore.card);
 const { classList, loadClassData } = useSchool();
-type SchoolInfo = {
-  schoolName: string;
-  classId: number | null;
-  className: string;
-  schoolId: number | null;
-}
-type UserProfile = Pick<UserInfo, 'nickName' | 'phonenumber' | 'location' | 'endYear' | 'examType'> & SchoolInfo;
-const form = ref<UserProfile>({
-  ...userInfo.value,
-  schoolName: cardInfo.value?.schoolName || '',
-  classId: cardInfo.value?.classId || null,
-  className: cardInfo.value?.className || '',
-  schoolId: cardInfo.value?.schoolId || null
+
+const form = ref<UserInfo>({
+  ...userInfo.value
 });
+if (userStore.isStudent) {
+  form.value.schoolName = cardInfo.value?.schoolName || '';
+  form.value.classId = cardInfo.value?.classId || undefined;
+  form.value.schoolClassName = cardInfo.value?.className || '';
+  form.value.schoolId = cardInfo.value?.schoolId || undefined
+}
+
 const scores = ref({
   ...userInfo.value.scores
 })
-const nameLabel = computed(() => userStore.isStudent ? '学生姓名' : '姓名');
 const customStyle = {
   paddingLeft: '26px'
 };

+ 0 - 80
src/pagesSystem/pages/edit-teacher-profile/edit-teacher-profile.vue

@@ -1,80 +0,0 @@
-<template>
-  <ie-page bg-color="#F6F8FA" :safeAreaInsetBottom="false">
-    <ie-navbar title="完善信息" custom-back @left-click="handleBack" />
-    <uv-form labelPosition="left" :model="examTypeForm" labelWidth="70px" ref="formRef">
-      <content-card title="基本信息">
-        <uv-form-item label="所在省份" prop="location" borderBottom required>
-          <ie-picker ref="pickerRef" v-model="examTypeForm.location" :list="provinceList" placeholder="选择省份"
-            :custom-style="customStyle" key-label="dictLabel" key-value="dictValue"></ie-picker>
-        </uv-form-item>
-        <uv-form-item label="考生类别" prop="examType" borderBottom required>
-          <ie-picker ref="pickerRef" v-model="examTypeForm.examType" :list="examTypeList"
-            :disabled="!examTypeForm.location" placeholder="选择考生类别" :custom-style="customStyle" key-label="dictLabel"
-            key-value="dictValue"></ie-picker>
-        </uv-form-item>
-        <uv-form-item label="毕业年份" prop="year" required>
-          <ie-picker ref="pickerRef" v-model="examTypeForm.endYear" :list="endYearList"
-            :disabled="!examTypeForm.examType" placeholder="选择毕业年份" :custom-style="customStyle" key-label="dictLabel"
-            key-value="dictValue"></ie-picker>
-        </uv-form-item>
-      </content-card>
-    </uv-form>
-    <ie-safe-toolbar :height="84" :shadow="false">
-      <view class="px-30 py-16">
-        <ie-button @click="handleSubmit">确认提交</ie-button>
-      </view>
-    </ie-safe-toolbar>
-  </ie-page>
-</template>
-
-<script lang="ts" setup>
-import ContentCard from '@/pagesSystem/components/content-card.vue';
-import { useExamType } from '@/composables/useExamType';
-import { updateUserInfo } from '@/api/modules/login';
-import { useUserStore } from '@/store/userStore';
-import { UserInfo } from '@/types/user';
-import { useTransferPage } from '@/hooks/useTransferPage';
-const { prevData, transferTo, transferBack } = useTransferPage();
-const { form: examTypeForm, examTypeList, examMajorList, provinceList, endYearList } = useExamType();
-const userStore = useUserStore();
-const customStyle = {
-  paddingLeft: '26px'
-};
-const handleBack = () => {
-  uni.$ie.showToast('请先完善信息');
-};
-const handleSubmit = async () => {
-  console.log('handleSubmit', examTypeForm.value)
-  const { location, examType, endYear } = examTypeForm.value;
-  if (!location) {
-    uni.$ie.showToast('请先选择所在省份');
-    return;
-  }
-  if (!examType) {
-    uni.$ie.showToast('请先选择考试类别');
-    return;
-  }
-  if (!endYear) {
-    uni.$ie.showToast('请先选择毕业年份');
-    return;
-  }
-  const params = {
-    ...userStore.userInfo,
-    location,
-    examType,
-    endYear,
-  } as UserInfo;
-  uni.$ie.showLoading();
-  await updateUserInfo(params);
-  await userStore.getUserInfo();
-  uni.$ie.hideLoading();
-  uni.$ie.showToast('保存成功');
-  setTimeout(() => {
-    transferTo('/pagesMain/pages/index/index', {
-      type: 'reLaunch'
-    });
-  }, 800);
-};
-</script>
-
-<style></style>

+ 34 - 9
src/pagesSystem/pages/login/login.vue

@@ -43,7 +43,7 @@
       </view>
       <view class="mt-42 ml-26">
         <uv-checkbox-group v-model="agreePrivacy">
-          <uv-checkbox name="true" shape="circle" label="记住密码" :labelSize="14" :iconSize="13" labelColor="#666666">
+          <uv-checkbox :name="true" shape="circle" label="记住密码" :labelSize="14" :iconSize="13" labelColor="#666666">
             <text class="text-28 text-fore-subcontent">已阅读并同意<text class="text-primary"
                 @click.stop="handleAgreePrivacy('user')">《用户协议》</text>和<text class="text-primary"
                 @click.stop="handleAgreePrivacy('privacy')">《隐私政策》</text></text>
@@ -60,12 +60,14 @@ import ieCaptcha from '@/components/ie-sms/ie-captcha.vue';
 import { useUserStore } from '@/store/userStore';
 import { useTransferPage } from '@/hooks/useTransferPage';
 import { validatePhone } from '@/hooks/useValidation';
+import { useAppConfig } from '@/hooks/useAppConfig';
 import { verifyCard } from '@/api/modules/user';
-import { EnumBindScene, EnumSmsApiType } from '@/common/enum';
+import { EnumBindScene, EnumSmsApiType, EnumUserType } from '@/common/enum';
 import { login } from '@/api/modules/login';
-import { LoginRequestDTO, MobileLoginResponseDTO } from '@/types/user';
+import { LoginRequestDTO, MobileLoginResponseDTO, UserInfo } from '@/types/user';
 const { transferBack, transferTo } = useTransferPage();
 const userStore = useUserStore();
+const { isSmsCaptchaEnable } = useAppConfig();
 
 const loginType = ref('phone');
 const phone = ref('');
@@ -146,8 +148,12 @@ const handleLogin = async () => {
   if (loginType.value === 'phone') {
     submitLogin();
   } else if (loginType.value === 'card') {
-    captchaRef.value.open();
-    userStore.rememberPwd = !!rememberPassword.value[0];
+    if (isSmsCaptchaEnable.value) {
+      captchaRef.value.open();
+    } else {
+      submitLogin();
+    }
+    userStore.rememberLoginInfo(!!rememberPassword.value[0], cardNo.value, cardPassword.value);
     // submitLogin();
   }
 }
@@ -190,7 +196,7 @@ const handleMobileLogin = async (params: LoginRequestDTO) => {
       }
     } else {
       if (res.token) {
-        userStore.login(res.token).then((success: boolean) => {
+        userStore.login(res.token).then(({ success, userInfo }) => {
           if (success) {
             transferBack(true);
           } else {
@@ -207,10 +213,15 @@ const handleCardLogin = (params: LoginRequestDTO) => {
   uni.$ie.showLoading();
   login(params).then(res => {
     if (res.token) {
-      userStore.login(res.token).then((success: boolean) => {
+      userStore.login(res.token).then(({ success, userInfo }) => {
         uni.$ie.hideLoading();
-        if (success) {
-          transferBack(true);
+        if (success && userInfo) {
+          const needComplete = needCompleteInfo(userInfo);
+          if (needComplete) {
+            transferTo('/pagesSystem/pages/bind-teacher-profile/bind-teacher-profile');
+          } else {
+            transferBack(true);
+          }
         } else {
           uni.$ie.showToast('登录失败')
         }
@@ -242,6 +253,11 @@ const handleCardLogin = (params: LoginRequestDTO) => {
 
 }
 
+const needCompleteInfo = (userInfo: UserInfo) => {
+  const { userType, location, examType, endYear } = userInfo;
+  return [EnumUserType.TEACHER, EnumUserType.AGENT].includes(userType) && (!location || !examType || !endYear);
+};
+
 const handleValid = (data: { code: string; uuid: string }) => {
   console.log(code.value, uuid.value);
   captchaRef.value.close();
@@ -251,6 +267,15 @@ const handleValid = (data: { code: string; uuid: string }) => {
 
 onLoad(() => {
   rememberPassword.value[0] = userStore.rememberPwd;
+  if (import.meta.env.DEV) {
+    agreePrivacy.value = [true];
+    phone.value = '17363958504';
+    password.value = '1234';
+  }
+  if (userStore.rememberPwd) {
+    cardNo.value = userStore.cardNo;
+    cardPassword.value = userStore.cardPassword;
+  }
 });
 </script>
 

+ 11 - 37
src/pagesSystem/pages/phone-verify/phone-verify.vue

@@ -22,9 +22,12 @@ import { useTransferPage } from '@/hooks/useTransferPage';
 import { EnumBindScene, EnumSmsApiType } from '@/common/enum';
 import { validateSms } from '@/api/modules/system';
 import { validatePhone } from '@/hooks/useValidation';
+import { useAppConfig } from '@/hooks/useAppConfig';
 import { getUserInfo, getUserInfoWithToken, login } from '@/api/modules/login';
 import { useUserStore } from '@/store/userStore';
 const { prevData, transferTo } = useTransferPage();
+const { isSmsCaptchaEnable } = useAppConfig();
+
 const form = ref({
   phone: '',
   password: '',
@@ -47,16 +50,6 @@ const checkHasReg = async () => {
     password: form.value.password,
   };
   const res = await login(params);
-  // const params = {
-  //   phone: form.value.phone,
-  //   code: form.value.code,
-  //   uuid: form.value.uuid,
-  //   scene: '',
-  //   type: 'phone',
-  //   card: prevData.value.card,
-  //   cardNo: prevData.value.cardNo,
-  //   password: prevData.value.password
-  // };
   if (res.data) {
     const { code, message } = res.data;
     if (code === 101) {
@@ -84,8 +77,6 @@ const checkHasReg = async () => {
       // 查询用户信息
       const { data } = await getUserInfoWithToken(res.token);
       console.log('用户已存在,用户信息:', data)
-      // params.scene = 'phone_improve';
-      // console.log(params)
       // 判断用户是否是 vip
       if (!!data.cardId) {
         uni.$ie.showToast('该手机号已绑定其他账号,请使用其他手机号');
@@ -94,6 +85,7 @@ const checkHasReg = async () => {
       // 不是 vip 则可以绑定
       const submitInfo = {
         scene: EnumBindScene.LOGIN_BIND,
+        token: res.token,
         userInfo: data,
         cardInfo: prevData.value.cardInfo,
         registerInfo: {
@@ -120,36 +112,18 @@ const handleSubmit = async () => {
     uni.$ie.showToast('请输入正确的手机号');
     return;
   }
-  if (!uuid && password) {
-    uni.$ie.showToast('请输入正确的验证码');
-    return;
+  // 开启了图形验证
+  if (isSmsCaptchaEnable.value) {
+    if (!uuid) {
+      uni.$ie.showToast('请输入正确的验证码');
+      return;
+    }
   }
-  if (!uuid || !password || password.trim() === '') {
+  if (!password || password.trim() === '') {
     uni.$ie.showToast('请输入验证码');
     return;
   }
-  // uni.$ie.showLoading();
   checkHasReg();
-  // validateSms({ mobile: phone, code: password }).then(() => {
-  //   uni.$ie.hideLoading();
-  //   const params = {
-  //     phone,
-  //     code: password,
-  //     uuid,
-  //     type: 'phone',
-  //     scene: 'card_improve',
-  //     card: prevData.value.card,
-  //     cardNo: prevData.value.cardNo,
-  //     password: prevData.value.password
-  //   };
-  //   console.log(params)
-  //   checkHasReg();
-  //   // transferTo('/pagesSystem/pages/bind-profile/bind-profile', {
-  //   //   data: params
-  //   // });
-  // }).catch(() => {
-  //   uni.$ie.hideLoading();
-  // });
 }
 onLoad(() => {
   console.log('收到上个页面的数据', prevData.value)

BIN
src/static/personal/avatar_default.png


BIN
src/static/personal/bg-vip-card.png


BIN
src/static/personal/icon-role.png


BIN
src/static/personal/icon-vip.png


BIN
src/static/personal/icon-vip2.png


+ 8 - 0
src/static/style/tailwind.scss

@@ -34,6 +34,14 @@
     box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.08);
   }
 
+  .shadow-card-dark {
+    box-shadow: 0px 0px 20px 0px rgba(0, 0, 0, 0.08);
+  }
+
+  .border-bottom {
+    border-bottom: 1px solid #E5E5E5;
+  }
+
   .sibling-border-top {
     &+.sibling-border-top {
       border-top: 1px solid #E5E5E5;

+ 1 - 1
src/static/theme/var.scss

@@ -14,7 +14,7 @@ $fore-placeholder: #B3B3B3;
 
 $border: #E6E6E6;
 
-$back: #f6f6f6;
+$back: #F6F8FA;
 $back-light: #f7f8fa;
 
 $warning: #f9ae3d;

+ 1 - 8
src/store/appStore.ts

@@ -16,7 +16,6 @@ export const useAppStore = defineStore('ie-app', {
       statusBarHeight: 0,
       sysInfo: null as UniApp.GetSystemInfoResult | null,
       appConfig: [] as ConfigItem[],
-      provinceList: [] as DictItem[],
     }
   },
   getters: {
@@ -52,7 +51,6 @@ export const useAppStore = defineStore('ie-app', {
      */
     async loadPreloadData() {
       await this.loadConfig();
-      // await this.loadProvince();
     },
     /**
      * 加载参数配置
@@ -72,11 +70,6 @@ export const useAppStore = defineStore('ie-app', {
       });
       this.appConfig = list;
     },
-    async loadProvince() {
-      const { data } = await getProvinces();
-      this.provinceList = data;
-      console.log(data)
-    },
     /**
      * 获取应用配置项的值
      * @param key 配置项的 key
@@ -99,7 +92,7 @@ export const useAppStore = defineStore('ie-app', {
       setItem: uni.setStorageSync,
     },
     // 不需要持久化的
-    // 'appConfig', 'provinceList'
+    // 'appConfig'
     omit: [],
   }
 });

+ 52 - 9
src/store/userStore.ts

@@ -2,6 +2,7 @@ import { defineStore } from 'pinia';
 import { useTransferPage } from '@/hooks/useTransferPage';
 import { ref, computed } from 'vue';
 import { getUserInfo } from '@/api/modules/login';
+import config from '@/config';
 
 import { Study, UserStoreState } from '@/types';
 import { UserInfo, VipCardInfo } from '@/types/user';
@@ -10,7 +11,7 @@ import defaultAvatar from '@/static/personal/avatar_default.png'
 
 // @ts-ignore
 import { useUserStore as useOldUserStore } from '@/hooks/useUserStore';
-import { EnumReviewMode, EnumUserType } from '@/common/enum';
+import { CardType, EnumReviewMode, EnumUserType } from '@/common/enum';
 import { OPEN_VIP_POPUP } from '@/types/injectionSymbols';
 import { getDirectedSchool, saveDirectedSchool } from '@/api/modules/study';
 const oldUserStore = useOldUserStore()
@@ -29,9 +30,14 @@ export const useUserStore = defineStore('ie-user', {
     isExamGuideShow: false,
     tempInfo: {
       location: '',
-      examType: '',
+      examType: undefined,
+    },
+    org: {
+      ...config.defaultOrg,
     },
     rememberPwd: false,
+    cardPassword: '',
+    cardNo: '',
     directedSchoolList: [],
     practiceSettings: {
       reviewMode: EnumReviewMode.AFTER_SUBMIT,
@@ -40,7 +46,7 @@ export const useUserStore = defineStore('ie-user', {
   }),
   getters: {
     isLogin(state: UserStoreState): boolean {
-      return !!state.accessToken && state.user !== null;
+      return !!state.accessToken && state.user !== null && Object.keys(state.user).length > 0;
     },
     userInfo(state: UserStoreState): UserInfo {
       return state.user || {} as UserInfo;
@@ -58,7 +64,18 @@ export const useUserStore = defineStore('ie-user', {
       return '';
     },
     isVip(): boolean {
-      return !!this.userInfo.cardId;
+      if (!this.card) {
+        return false;
+      }
+      const { outDate } = this.card;
+      return !!this.userInfo.cardId && new Date(outDate) > new Date();
+    },
+    isExperienceVip(): boolean {
+      if (!this.card) {
+        return false;
+      }
+      const { outDate } = this.card;
+      return this.card?.type === CardType.EXPERIENCE && new Date(outDate) > new Date();
     },
     vipInfo(state: UserStoreState): VipCardInfo {
       if (state.card) {
@@ -66,6 +83,9 @@ export const useUserStore = defineStore('ie-user', {
       }
       return {} as VipCardInfo;
     },
+    orgInfo(state: UserStoreState) {
+      return state.org;
+    },
     isStudent(): boolean {
       return this.userInfo.userType === EnumUserType.STUDENT;
     },
@@ -111,10 +131,16 @@ export const useUserStore = defineStore('ie-user', {
       try {
         this.accessToken = token;
         await oldUserStore.SyncToken(token);
-        await this.getUserInfo();;
-        return Promise.resolve(true);
+        const userInfo = await this.getUserInfo();;
+        return Promise.resolve({
+          success: true,
+          userInfo: userInfo
+        });
       } catch (error) {
-        return Promise.resolve(false);
+        return Promise.resolve({
+          success: false,
+          userInfo: null
+        });
       }
     },
     checkLogin({ askToLogin = true }: CheckLoginOptions = {}): Promise<boolean> {
@@ -158,7 +184,7 @@ export const useUserStore = defineStore('ie-user', {
       return new Promise((resolve, reject) => {
         if (this.needCompleteInfo && (!this.userInfo.location || !this.userInfo.examType || !this.userInfo.endYear)) {
           const { transferTo } = useTransferPage();
-          transferTo('/pagesSystem/pages/edit-teacher-profile/edit-teacher-profile').then(res => {
+          transferTo('/pagesSystem/pages/bind-teacher-profile/bind-teacher-profile').then(res => {
             resolve(res as boolean);
           }).catch(() => {
             resolve(false);
@@ -170,7 +196,7 @@ export const useUserStore = defineStore('ie-user', {
     },
     async getUserInfo() {
       const res = await getUserInfo();
-      const { data, isDefaultModifyPwd, isPasswordExpired, card } = res;
+      const { data, isDefaultModifyPwd, isPasswordExpired, card, org } = res;
       if (data) {
         this.user = data;
         if (card) {
@@ -179,6 +205,10 @@ export const useUserStore = defineStore('ie-user', {
         this.isDefaultModifyPwd = isDefaultModifyPwd;
         this.isPasswordExpired = isPasswordExpired;
       }
+      if (org) {
+        this.org = org;
+      }
+      return data;
     },
     /**
      * 保存定向学校列表
@@ -214,11 +244,24 @@ export const useUserStore = defineStore('ie-user', {
         });
       });
     },
+    rememberLoginInfo(remember: boolean, cardNo: string, cardPassword: string) {
+      this.rememberPwd = remember;
+      if (remember) {
+        this.cardNo = cardNo;
+        this.cardPassword = cardPassword;
+      } else {
+        this.cardNo = '';
+        this.cardPassword = '';
+      }
+    },
     logout() {
       this.accessToken = null;
       this.user = null;
       this.isDefaultModifyPwd = false;
       this.isPasswordExpired = false;
+      this.org = {
+        ...config.defaultOrg,
+      };
       oldUserStore.Logout();
       // const { transferTo } = useTransferPage();
       // setTimeout(() => {

+ 17 - 5
src/types/index.ts

@@ -2,8 +2,9 @@ import * as Study from "./study";
 import * as User from "./user";
 import * as News from "./news";
 import * as Transfer from "./transfer";
+import * as System from './system';
 import { VipCardInfo } from "./user";
-import { EnumReviewMode } from "@/common/enum";
+import { EnumExamMode, EnumExamType, EnumReviewMode } from "@/common/enum";
 
 /// 接口响应
 export interface ApiResponse<T> {
@@ -16,6 +17,12 @@ export interface ApiResponse<T> {
   user?: User.UserInfo;
   isDefaultModifyPwd?: boolean;
   isPasswordExpired?: boolean;
+  // 以下是机构信息
+  org?: {
+    contactPhone: string;
+    logo: string;
+    orgName: string;
+  }
 }
 
 export interface ApiCaptchaResponse {
@@ -38,7 +45,6 @@ export interface TreeData {
   children?: TreeData[];
   isExpanded?: boolean;
   isLeaf?: boolean;
-  actualHeight?: number;
 }
 
 export interface TableConfig {
@@ -68,7 +74,6 @@ export interface AppStoreState {
   statusBarHeight: number;
   sysInfo: UniApp.GetSystemInfoResult | null;
   appConfig: ConfigItem[];
-  provinceList: DictItem[];
 }
 
 export interface DictStoreState {
@@ -84,9 +89,16 @@ export interface UserStoreState {
   isExamGuideShow: boolean;
   tempInfo?: {
     location: string;
-    examType: string;
+    examType?: EnumExamType;
+  };
+  org: {
+    contactPhone: string;
+    logo: string;
+    orgName: string;
   };
   rememberPwd: boolean;
+  cardPassword: string;
+  cardNo: string;
   directedSchoolList: Study.DirectedSchool[];
   practiceSettings: Study.PracticeSettings;
 }
@@ -114,4 +126,4 @@ export interface ConfigItem {
 
 
 
-export { Study, User, News, Transfer };
+export { Study, User, News, Transfer, System };

+ 115 - 23
src/types/study.ts

@@ -1,4 +1,4 @@
-import { EnumReviewMode, EnumSimulatedRecordStatus } from "@/common/enum";
+import { EnumPaperBuildType, EnumPaperWorkState, EnumReviewMode, EnumSimulatedRecordStatus } from "@/common/enum";
 
 export interface TeachClass {
   classId: number;
@@ -16,26 +16,83 @@ export interface StudentStat {
   rate: number;
 }
 
-export interface StudentExamRecord {
+/**
+ * 班级知识点记录
+ */
+  export interface ClassKnowledgeRecord {
+  rate: number;
+  list: StudentPlanStudyRecord[];
+}
+
+/**
+ * 班级学生刷题记录
+ */
+export interface StudentPlanStudyRecord {
   id: number;
+  avatar?: string;
   name: string;
-  score: number;
-  status: number;
+  rate: number;
+  time: number;
+  total: number;
+  value: number;
 }
 
-export interface StudentVideoRecord {
+export interface StudentPracticeRecord {
   id: number;
+  avatar?: string;
   name: string;
-  date: string;
-  duration: number;
+  rate: number;
+  time: number;
+  total: number;
+  value: number;
 }
 
-export interface StudentVideoStat {
+export interface StudentExamRecord {
   id: number;
-  avatar?: string;
   name: string;
-  videoCount: number;
-  duration: number;
+  total: number;
+  value: number;
+  rate: number;
+}
+
+/**
+ * 组卷批次信息
+ */
+export interface Batch {
+  batchId: number;
+  name: string;
+  year: number;
+}
+
+/**
+ * 班级学生视频学习记录
+ */
+export interface StudentVideoRecord {
+  id: number;
+  name: string;
+  total: string;
+  value: number;
+  seq: number;
+}
+
+export interface PaperWorkRecord {
+  buildStatus: number | null;
+  count: number | null;
+}
+
+export interface PaperWorkRecordQuery {
+  buildStatus: number | null;
+  batchId: number | null;
+  buildType: EnumPaperBuildType;
+  classId: number | null;
+  subjectId: number | null;
+}
+
+export interface PaperWorkRecordDetail {
+  className: string;
+  nickName: string;
+  state: string;
+  studentId: number;
 }
 
 /**
@@ -93,7 +150,6 @@ export interface Knowledge {
 export type KnowledgeNode = Pick<Knowledge, 'id' | 'name' | 'status' | 'questionCount' | 'finishedCount' | 'finishedRatio'> & {
   isExpanded: boolean;
   isLeaf: boolean;
-  actualHeight: number;
   children: KnowledgeNode[];
 }
 
@@ -225,16 +281,21 @@ export interface Question extends QuestionState {
   subQuestions: Question[];
   totalScore: number;
   //
-  index: number; // 索引
-  offset: number; // 偏移量
+  index: number; // 当前题目在原始数组中的索引,如果是子题,则index和父题的 index 一致
+  offset: number; // 前面所有子题数量的偏移量
   isSubQuestion?: boolean; // 是否是子题
   //
-  parentIndex?: number; // 父题索引
+  parentIndex?: number; // 子题中父题在原始数组中的索引
   parentId?: number; // 父题ID
   parentTypeId?: number; // 父题类型
-  subIndex?: number; // 子题索引
+  subIndex?: number; // 子题在父题中的索引
   //
-  virtualIndex: number; // 虚拟索引
+  virtualIndex: number; // 在平铺数组中的虚拟索引
+  activeSubIndex: number; // 父题中正在展示的子题索引
+  // 单题做题时间
+  duration: number;
+  // 是否有子题
+  hasSubQuestions: boolean;
 }
 
 export interface SubjectListRequestDTO {
@@ -324,14 +385,22 @@ export interface SimulatedRecord {
   state: EnumSimulatedRecordStatus;
 }
 
-export interface VideoStudyRecord {
+/**
+ * 视频学习统计
+ */
+export interface VideoStudy {
   study: number;
   total: number;
-  list: {
-    name: string;
-    date: string;
-    study: string;
-  }[]
+  list: VideoStudyRecord[]
+}
+
+/**
+ * 视频学习记录
+ */
+export interface VideoStudyRecord {
+  name: string;
+  date: string;
+  study: string;
 }
 
 export interface PracticeRecord {
@@ -360,4 +429,27 @@ export interface PracticeHistory {
   endTime: string;
   examineeId: number;
   paperName: string;
+}
+
+/**
+ * 组卷作业
+ */
+export interface PaperWork {
+  id: number;
+  name: string;
+  subjectId: number;
+  total: number;
+  state: EnumPaperWorkState;
+  subjectName: string;
+  date: string;
+  campusName: string;
+  directed: boolean;
+  majorName: string;
+  publishUser: string;
+  publishTime: string;
+  schoolName: string;
+  score: number;
+  universityName: string;
+  endTime: string;
+  duration: number;
 }

+ 6 - 0
src/types/system.ts

@@ -0,0 +1,6 @@
+export type ProvinceItem = {
+  areaId: number;
+  areaName: string;
+  provinceName: string;
+  children: ProvinceItem[]
+}

+ 4 - 0
src/types/transfer.ts

@@ -9,6 +9,7 @@ export interface PracticeResultPageOptions {
   examineeId: number;
   name: string;
   directed: boolean;
+  paperType: EnumPaperType;
 }
 
 /**
@@ -20,9 +21,11 @@ export interface ExamAnalysisPageOptions {
   name: string;
   questionId?: number;
   readonly?: boolean;
+  // 模拟考试
   simulationInfo?: {
     examineeId: number;
   };
+  // 知识点练习、教材同步练、组卷作业
   practiceInfo?: {
     name: string;
     relateId: number;
@@ -36,4 +39,5 @@ export interface ExamAnalysisPageOptions {
  */
 export interface SimulationAnalysisPageOptions {
   examineeId: number;
+  paperType: EnumPaperType;
 }

+ 15 - 4
src/types/user.ts

@@ -1,4 +1,4 @@
-import { EnumExamType, EnumSmsType, EnumUserType } from "@/common/enum";
+import { CardType, EnumExamType, EnumSmsType, EnumUserType } from "@/common/enum";
 
 export interface LoginInfo {
   accessToken: string;
@@ -11,7 +11,7 @@ export interface StudentExamInfo {
   nickName: string;
   location: string;
   endYear: number;
-  examType: string;
+  examType?: EnumExamType;
   majorType?: string;
 }
 
@@ -57,7 +57,7 @@ export interface ClassItem {
 export interface CardInfo {
   agentId?: number;
   agentName?: string;
-  assignExamType?: string;
+  assignExamType?: EnumExamType;
   assignLocation?: string;
   assignSchoolId?: number;
   assignSchoolName?: string;
@@ -90,6 +90,8 @@ export interface RegisterInfo {
 
 export interface BindCardInfo extends RegisterInfo {
   cardNo?: string;
+  // 临时信息
+  schoolClassName?: string;
 }
 
 export interface Scores {
@@ -149,7 +151,13 @@ export interface UserInfo {
   userName: string;
   scores: Scores;
   userType: EnumUserType,
-  accountType: number
+  accountType: number,
+  schoolName: string;
+  schoolClassName: string;
+  classId?: number;
+  schoolId?: number;
+  campusClassName?: string;
+  campusName?: string;
 }
 
 export interface VipCardInfo {
@@ -161,8 +169,11 @@ export interface VipCardInfo {
   year: number; // 入学年份
   endYear: number; // 毕业年份
   outDate: string; // 到期时间
+  type: CardType;
 }
 
+export type UserRole = 'vip' | 'normal' | 'guest' | 'teacher' | 'agent' | 'auditor'
+
 // export interface BindCardInfo {
 //   cardNo: string;
 //   password: string;

Некоторые файлы не были показаны из-за большого количества измененных файлов