Browse Source

vhs - form init tmp save

abpcoder 4 weeks ago
parent
commit
0ec3a83c6d
41 changed files with 1223 additions and 469 deletions
  1. 8 0
      README.md
  2. 0 1
      package.json
  3. 4 2
      src/App.vue
  4. 3 1
      src/api/flyio.ts
  5. 8 0
      src/api/modules/system.ts
  6. 10 1
      src/api/modules/voluntary.ts
  7. 277 273
      src/common/enum.ts
  8. 8 0
      src/common/routes.ts
  9. 2 24
      src/global.d.ts
  10. 9 1
      src/hooks/useAppConfig.ts
  11. 7 4
      src/hooks/useAuth.ts
  12. 63 0
      src/hooks/useReport.ts
  13. 12 0
      src/pages.json
  14. 55 0
      src/pagesMain/pages/index/components/index-maintain-popup.vue
  15. 18 3
      src/pagesMain/pages/index/components/index-map.vue
  16. 1 1
      src/pagesMain/pages/index/components/index-popup.vue
  17. 10 1
      src/pagesMain/pages/index/index.vue
  18. 2 5
      src/pagesMain/pages/me/components/me-menu.vue
  19. 44 0
      src/pagesMain/pages/splash/components/maintain-popup.vue
  20. 43 18
      src/pagesMain/pages/splash/splash.vue
  21. 37 32
      src/pagesMain/pages/volunteer/components/volunteer-banner.vue
  22. 14 6
      src/pagesOther/components/ie-condition-dropdown/ie-condition-dropdown-popup.vue
  23. 1 1
      src/pagesOther/components/ie-condition-dropdown/ie-condition-dropdown.vue
  24. 2 2
      src/pagesOther/pages/skill/index/index.vue
  25. 3 3
      src/pagesOther/pages/university/detail/components/plan-enroll-list.vue
  26. 2 2
      src/pagesOther/pages/university/index/index.vue
  27. 2 2
      src/pagesOther/pages/voluntary/index/index.vue
  28. 2 2
      src/pagesOther/pages/voluntary/list/list.vue
  29. 2 2
      src/pagesStudy/components/vhs-exam-item.vue
  30. 229 23
      src/pagesStudy/pages/exam-start/exam-start.vue
  31. 38 26
      src/pagesStudy/pages/knowledge-practice-detail/knowledge-practice-detail.vue
  32. 2 2
      src/pagesStudy/pages/knowledge-practice/knowledge-practice.vue
  33. 5 5
      src/pagesStudy/pages/textbooks-practice/textbooks-practice.vue
  34. 67 0
      src/pagesSystem/pages/debug/debug.vue
  35. 66 0
      src/pagesSystem/pages/qa/qa.vue
  36. 29 0
      src/pagesSystem/pages/setting/setting.vue
  37. 92 4
      src/store/appStore.ts
  38. 27 2
      src/store/userStore.ts
  39. 1 0
      src/types/index.ts
  40. 17 0
      src/types/system.ts
  41. 1 20
      src/utils/uni-tool.ts

+ 8 - 0
README.md

@@ -0,0 +1,8 @@
+# IE-plus 项目
+
+## 微信小程序适配问题
+1、插槽
+
+- 不支持在父组件```for```循环中使用子组件的具名作用域插槽;
+
+- 在```for```循环中渲染同一个具名插槽时,只会渲染一个;

+ 0 - 1
package.json

@@ -58,7 +58,6 @@
     "@dcloudio/uni-h5": "3.0.0-4080720251210001",
     "@dcloudio/uni-mp-weixin": "3.0.0-4080720251210001",
     "@rojer/katex-mini": "^1.3.4",
-    "@vueuse/core": "^14.1.0",
     "echarts": "^6.0.0",
     "flyio": "^0.6.14",
     "katex": "^0.16.25",

+ 4 - 2
src/App.vue

@@ -4,8 +4,10 @@ import { checkUpdate } from "@/utils/update.ts";
 export default {
   onLaunch: function () {
     console.log('App Launch')
-    const appStore = useAppStore();
-    appStore.init();
+    // const appStore = useAppStore();
+    // if (!appStore.isMaintaining) {
+    //   appStore.init();
+    // }
     // #ifdef MP-WEIXIN
     checkUpdate();
     // #endif

+ 3 - 1
src/api/flyio.ts

@@ -62,6 +62,7 @@ fly.interceptors.response.use(
       const { code, msg } = result;
       if (code === 401) {
         logout();
+        return Promise.reject({ code: 401, msg: '未授权,请重新登录', result });
       } else {
         if (code !== 200) {
           uni.$ie.showToast(msg);
@@ -71,7 +72,8 @@ fly.interceptors.response.use(
         }
       }
     } catch (err) {
-      return Promise.reject(result);
+      // 响应数据格式异常,reject 错误对象而不是 result
+      return Promise.reject(err);
     }
   },
   function (err: Error, promise: Promise<any>) {

+ 8 - 0
src/api/modules/system.ts

@@ -102,3 +102,11 @@ export function getCaptchaImage() {
 export function validateSms(params: SmsRequestDTO) {
   return flyio.post('/front/comm/validateSms', null, { params }) as Promise<ApiResponse<any>>;
 }
+
+/**
+ * 获取系统公告
+ * @returns 
+ */
+export function getSystemNotice(noticeType: number) {
+  return flyio.get('/front/news/getSystemNotice', { noticeType, status: '0' }) as Promise<ApiResponseList<System.SystemNotice>>;
+}

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

@@ -1,4 +1,4 @@
-import {ApiResponse, DictItem} from "@/types";
+import type {ApiResponse, ApiResponseList, DictItem, News} from "@/types";
 import flyio from "../flyio";
 import {
     EnrollRule,
@@ -289,3 +289,12 @@ export function sortVoluntaryByUniversity(universityIdList: string[]) {
     // 按传入院校id顺序,保存排序优先级
     return flyio.post('/voluntary/sortVoluntaryByUniversity', {universityIdList})
 }
+
+
+/**
+ * 获取志愿填报规则
+ * @returns 
+ */
+export function getVoluntaryRule() {
+  return flyio.get('/front/news/getMainListV2') as Promise<ApiResponseList<News.Guide>>
+}

+ 277 - 273
src/common/enum.ts

@@ -2,170 +2,174 @@
  * 应用配置项的 key
  */
 export enum EnumAppConfigKey {
-    /**
-     * 短信验证码是否开启图形验证
-     */
-    SMS_CAPTCHA_ENABLE = 'sys.sms.captchaEnabled'
+  /**
+   * 短信验证码是否开启图形验证
+   */
+  SMS_CAPTCHA_ENABLE = 'sys.sms.captchaEnabled',
+  /**
+   * app 是否开启日志上报
+   */
+  APP_LOG_REPORT_ENABLE = 'app.log.report.enable'
 }
 
 /**
  * 短信发送类型
  */
 export enum EnumSmsApiType {
-    /**
-     * 无需校验 无需token 发送短信-适用于登录
-     */
-    NO_VALIDATION_NO_TOKEN = 'NoValidationNoToken',
-    /**
-     * 需校验 需要 token 发送短信-适用于注册
-     */
-    NO_TOKEN = 'NoToken',
-    /**
-     * 普通发送短信
-     */
-    NORMAL = 'normal'
+  /**
+   * 无需校验 无需token 发送短信-适用于登录
+   */
+  NO_VALIDATION_NO_TOKEN = 'NoValidationNoToken',
+  /**
+   * 需校验 需要 token 发送短信-适用于注册
+   */
+  NO_TOKEN = 'NoToken',
+  /**
+   * 普通发送短信
+   */
+  NORMAL = 'normal'
 }
 
 /**
  * 短信类型
  */
 export enum EnumSmsType {
-    /**
-     * code
-     */
-    CODE = 'CODE',
-    /**
-     * ecard
-     */
-    ECARD = 'ECARD',
-    /**
-     * password
-     */
-    PASSWORD = 'PASSWORD'
+  /**
+   * code
+   */
+  CODE = 'CODE',
+  /**
+   * ecard
+   */
+  ECARD = 'ECARD',
+  /**
+   * password
+   */
+  PASSWORD = 'PASSWORD'
 }
 
 export enum EnumDictName {
-    /**
-     * 考生类别
-     */
-    EXAM_TYPE = 'exam_type'
+  /**
+   * 考生类别
+   */
+  EXAM_TYPE = 'exam_type'
 }
 
 export enum STATIC_PAGE_PATH {
-    /**
-     * 登录
-     */
-    LOGIN = '/pagesSystem/pages/login/login',
-    /**
-     * 注册
-     */
-    REGISTER = '/pagesSystem/pages/register/register',
-    /**
-     * 找回密码
-     */
-    FIND_PASSWORD = '/pagesSystem/pages/find-password/find-password',
-    /**
-     * 修改密码
-     */
-    CHANGE_PASSWORD = '/pagesSystem/pages/change-password/change-password',
-    /**
-     * 绑定手机号
-     */
-    BIND_PHONE = '/pagesSystem/pages/bind-phone/bind-phone'
+  /**
+   * 登录
+   */
+  LOGIN = '/pagesSystem/pages/login/login',
+  /**
+   * 注册
+   */
+  REGISTER = '/pagesSystem/pages/register/register',
+  /**
+   * 找回密码
+   */
+  FIND_PASSWORD = '/pagesSystem/pages/find-password/find-password',
+  /**
+   * 修改密码
+   */
+  CHANGE_PASSWORD = '/pagesSystem/pages/change-password/change-password',
+  /**
+   * 绑定手机号
+   */
+  BIND_PHONE = '/pagesSystem/pages/bind-phone/bind-phone'
 }
 
 export enum EnumExamMode {
-    /**
-     * 练习
-     */
-    PRACTICE = 1,
-    /**
-     * 考试
-     */
-    EXAM = 2
+  /**
+   * 练习
+   */
+  PRACTICE = 1,
+  /**
+   * 考试
+   */
+  EXAM = 2
 }
 
 /**
  * 题目类型
  */
 export enum EnumQuestionType {
-    /**
-     * 单选
-     */
-    SINGLE_CHOICE = 1,
-    /**
-     * 多选
-     */
-    MULTIPLE_CHOICE = 2,
-    /**
-     * 判断
-     */
-    JUDGMENT = 3,
-    /**
-     * 填空
-     */
-    FILL_IN_THE_BLANK = 4,
-    /**
-     * 主观题
-     */
-    SUBJECTIVE = 5,
-    /**
-     * 简答
-     */
-    SHORT_ANSWER = 6,
-    /**
-     * 问答题
-     */
-    ESSAY = 7,
-    /**
-     * 分析题
-     */
-    ANALYSIS = 8,
+  /**
+   * 单选
+   */
+  SINGLE_CHOICE = 1,
+  /**
+   * 多选
+   */
+  MULTIPLE_CHOICE = 2,
+  /**
+   * 判断
+   */
+  JUDGMENT = 3,
+  /**
+   * 填空
+   */
+  FILL_IN_THE_BLANK = 4,
+  /**
+   * 主观题
+   */
+  SUBJECTIVE = 5,
+  /**
+   * 简答
+   */
+  SHORT_ANSWER = 6,
+  /**
+   * 问答题
+   */
+  ESSAY = 7,
+  /**
+   * 分析题
+   */
+  ANALYSIS = 8,
 
-    /**
-     * 阅读题
-     */
-    OTHER = 99
+  /**
+   * 阅读题
+   */
+  OTHER = 99
 }
 
 /**
  * 用户类型
  */
 export enum EnumUserType {
-    /**
-     * 系统用户
-     */
-    SYSTEM = '00',
-    /**
-     * 学生
-     */
-    STUDENT = '01',
-    /**
-     * 教师
-     */
-    TEACHER = '11',
-    /**
-     * 代理商
-     */
-    AGENT = '10',
-    /**
-     * 机构
-     */
-    AGENCY = '12'
+  /**
+   * 系统用户
+   */
+  SYSTEM = '00',
+  /**
+   * 学生
+   */
+  STUDENT = '01',
+  /**
+   * 教师
+   */
+  TEACHER = '11',
+  /**
+   * 代理商
+   */
+  AGENT = '10',
+  /**
+   * 机构
+   */
+  AGENCY = '12'
 }
 
 /**
  * 考试类型
  */
 export enum EnumExamRecordType {
-    /**
-     * 模拟考试
-     */
-    SIMULATED = 'simulated',
-    /**
-     * 组卷作业
-     */
-    HOMEWORK = 'homework'
+  /**
+   * 模拟考试
+   */
+  SIMULATED = 'simulated',
+  /**
+   * 组卷作业
+   */
+  HOMEWORK = 'homework'
 }
 
 
@@ -173,192 +177,192 @@ export enum EnumExamRecordType {
  * 绑定场景
  */
 export enum EnumBindScene {
-    /**
-     * 注册
-     */
-    REGISTER = 'register',
-    /**
-     * 注册绑定
-     */
-    REGISTER_BIND = 'register_bind',
-    /**
-     * 登录绑定
-     */
-    LOGIN_BIND = 'login_bind',
-    /**
-     * 老师或者代理商完善信息
-     */
-    IMPROVE = 'improve'
+  /**
+   * 注册
+   */
+  REGISTER = 'register',
+  /**
+   * 注册绑定
+   */
+  REGISTER_BIND = 'register_bind',
+  /**
+   * 登录绑定
+   */
+  LOGIN_BIND = 'login_bind',
+  /**
+   * 老师或者代理商完善信息
+   */
+  IMPROVE = 'improve'
 }
 
 export enum EnumExamType {
-    /**
-     * 职高对口升学
-     */
-    VHS = 'VHS',
-    /**
-     * 单招(应届普高)
-     */
-    OHS = 'OHS',
-    /**
-     * 单招(中职)
-     */
-    SVS = 'SVS'
+  /**
+   * 职高对口升学
+   */
+  VHS = 'VHS',
+  /**
+   * 单招(应届普高)
+   */
+  OHS = 'OHS',
+  /**
+   * 单招(中职)
+   */
+  SVS = 'SVS'
 }
 
 export enum EnumSimulatedRecordStatus {
-    /**
-     * 空卷
-     */
-    INIT = 1,
-    /**
-     * 签到
-     */
-    SIGN = 2,
-    /**
-     * 考试
-     */
-    EXAM = 3,
-    /**
-     * 交卷
-     */
-    SUBMIT = 4,
-    /**
-     * 阅卷
-     */
-    REVIEW = 5,
-    /**
-     * 发布
-     */
-    PUBLISH = 6,
-    /**
-     * 关闭
-     */
-    CLOSE = 7,
+  /**
+   * 空卷
+   */
+  INIT = 1,
+  /**
+   * 签到
+   */
+  SIGN = 2,
+  /**
+   * 考试
+   */
+  EXAM = 3,
+  /**
+   * 交卷
+   */
+  SUBMIT = 4,
+  /**
+   * 阅卷
+   */
+  REVIEW = 5,
+  /**
+   * 发布
+   */
+  PUBLISH = 6,
+  /**
+   * 关闭
+   */
+  CLOSE = 7,
 }
 
 export enum EnumPaperType {
-    /**
-     * 练习
-     */
-    PRACTICE = 'Practice',
-    /**
-     * 考试
-     */
-    SIMULATED = 'Simulated',
-    /**
-     * 教材同步练习
-     */
-    COURSE = 'Course',
-    /**
-     * 测试卷
-     */
-    TEST = 'Test'
+  /**
+   * 练习
+   */
+  PRACTICE = 'Practice',
+  /**
+   * 考试
+   */
+  SIMULATED = 'Simulated',
+  /**
+   * 教材同步练习
+   */
+  COURSE = 'Course',
+  /**
+   * 测试卷
+   */
+  TEST = 'Test'
 }
 
 export enum EnumReviewMode {
-    /**
-     * 交卷后评卷
-     */
-    AFTER_SUBMIT = 1,
-    /**
-     * 答完一题就评卷
-     */
-    DURING_ANSWER = 2
+  /**
+   * 交卷后评卷
+   */
+  AFTER_SUBMIT = 1,
+  /**
+   * 答完一题就评卷
+   */
+  DURING_ANSWER = 2
 }
 
 export enum EnumUserRole {
-    /**
-     * 普通用户
-     */
-    NORMAL = 'normal',
-    /**
-     * 游客
-     */
-    GUEST = 'guest',
-    /**
-     * 会员
-     */
-    VIP = 'vip',
-    /**
-     * 代理商
-     */
-    AGENT = 'agent',
-    /**
-     * 教师
-     */
-    TEACHER = 'teacher'
+  /**
+   * 普通用户
+   */
+  NORMAL = 'normal',
+  /**
+   * 游客
+   */
+  GUEST = 'guest',
+  /**
+   * 会员
+   */
+  VIP = 'vip',
+  /**
+   * 代理商
+   */
+  AGENT = 'agent',
+  /**
+   * 教师
+   */
+  TEACHER = 'teacher'
 }
 
 export enum EnumEvent {
-    /**
-     * 打开VIP弹窗
-     */
-    OPEN_VIP_POPUP = 'OPEN_VIP_POPUP'
+  /**
+   * 打开VIP弹窗
+   */
+  OPEN_VIP_POPUP = 'OPEN_VIP_POPUP'
 }
 
 /**
  * 卡类型
  */
 export enum CardType {
-    VIP = 1,
-    DEPT = 2,
-    PLATFORM = 6,
-    /**
-     * 体验卡
-     */
-    EXPERIENCE = 9
+  VIP = 1,
+  DEPT = 2,
+  PLATFORM = 6,
+  /**
+   * 体验卡
+   */
+  EXPERIENCE = 9
 }
 
 export enum EnumPaperWorkState {
-    /**
-     * 未完成
-     */
-    NOT_COMPLETED = 2,
-    /**
-     * 已完成
-     */
-    COMPLETED = 4
+  /**
+   * 未完成
+   */
+  NOT_COMPLETED = 2,
+  /**
+   * 已完成
+   */
+  COMPLETED = 4
 }
 
 export enum EnumPaperBuildType {
-    /**
-     * 定向智能
-     */
-    EXACT_INTELLIGENT = 'ExactIntelligent',
-    /**
-     * 全量智能
-     */
-    FULL_INTELLIGENT = 'FullIntelligent',
-    /**
-     * 定向手动
-     */
-    EXACT_HAND = 'ExactHand',
-    /**
-     * 全量手动
-     */
-    FULL_HAND = 'FullHand'
+  /**
+   * 定向智能
+   */
+  EXACT_INTELLIGENT = 'ExactIntelligent',
+  /**
+   * 全量智能
+   */
+  FULL_INTELLIGENT = 'FullIntelligent',
+  /**
+   * 定向手动
+   */
+  EXACT_HAND = 'ExactHand',
+  /**
+   * 全量手动
+   */
+  FULL_HAND = 'FullHand'
 }
 
 export enum EnumBrochureType {
-    INTRODUCTION = 1, // 学校概况
-    ENROLL_RULE = 2, // 招生简章
-    EXAM_TIME = 3, // 考试大纲
-    WORK = 4, // 就业
-    OTHER_RULE = 5 // 其它
+  INTRODUCTION = 1, // 学校概况
+  ENROLL_RULE = 2, // 招生简章
+  EXAM_TIME = 3, // 考试大纲
+  WORK = 4, // 就业
+  OTHER_RULE = 5 // 其它
 }
 
 export enum EnumSimulatePickType {
-    DANGER = 0,
-    NORMAL = 1,
-    SAFETY = 2
+  DANGER = 0,
+  NORMAL = 1,
+  SAFETY = 2
 }
 
 export enum EnumScoreLock {
-    // 待锁定状态
-    locking = -1,
-    // 正常状态
-    unlock = 0,
-    // 已锁定状态
-    locked = 1
+  // 待锁定状态
+  locking = -1,
+  // 正常状态
+  unlock = 0,
+  // 已锁定状态
+  locked = 1
 }

+ 8 - 0
src/common/routes.ts

@@ -177,6 +177,14 @@ export const routes = {
    * 查位次
    */
   pageRanking: '/pagesOther/pages/ranking/ranking',
+  /**
+   * 常见问题
+   */
+  pageQa: '/pagesSystem/pages/qa/qa',
+  /**
+   * 测评中心
+   */
+  pageTestCenter: '/pagesOther/pages/test-center/index/index',
 } as const;
 
 export type Routes = keyof typeof routes;

+ 2 - 24
src/global.d.ts

@@ -13,30 +13,8 @@ declare global {
    * 微信小程序 API 接口
    */
   interface Wx {
-    /**
-     * 打开浏览器
-     * @param options - 配置选项
-     * @param options.url - 要打开的网址
-     * @param options.success - 成功回调
-     * @param options.fail - 失败回调
-     */
-    openBrowser?: (options: {
-      url: string;
-      success?: () => void;
-      fail?: (err: any) => void;
-    }) => void;
-  }
-  
-  /**
-   * 支付宝小程序 API 接口
-   */
-  interface My {
-    /**
-     * 打开浏览器
-     * @param options - 配置选项
-     * @param options.url - 要打开的网址
-     */
-    openBrowser?: (options: { url: string }) => void;
+    exitMiniProgram: (options?: { success?: () => void; fail?: (err: any) => void; complete?: () => void }) => void;
+    restartMiniProgram: (options?: { path?: string; success?: () => void; fail?: (err: any) => void; complete?: () => void }) => void;
   }
   
   /**

+ 9 - 1
src/hooks/useAppConfig.ts

@@ -9,7 +9,15 @@ export const useAppConfig = () => {
   const isSmsCaptchaEnable = computed(() => {
     return appStore.getAppConfig(EnumAppConfigKey.SMS_CAPTCHA_ENABLE) === 'true';
   });
+  /**
+  * app 是否开启日志上报
+  */
+  const isAppLogReportEnable = computed(() => {
+    return appStore.getAppConfig(EnumAppConfigKey.APP_LOG_REPORT_ENABLE) === 'true';
+  });
+
   return {
-    isSmsCaptchaEnable
+    isSmsCaptchaEnable,
+    isAppLogReportEnable
   }
 }

+ 7 - 4
src/hooks/useAuth.ts

@@ -34,13 +34,16 @@ export const useAuth = () => {
     return hasAuth;
   }
 
+  /**
+   * 检查是否具有VIP权限,开了 VIP 的学生、教师、代理商都有VIP权限
+   * @returns 是否具有VIP权限
+   */
   const checkVipPermission = () => {
-    if (!isVip.value) {
-
-    }
+    return hasPermission([EnumUserRole.VIP, EnumUserRole.AGENT, EnumUserRole.TEACHER]);
   }
 
   return {
-    hasPermission
+    hasPermission,
+    checkVipPermission
   }
 }

+ 63 - 0
src/hooks/useReport.ts

@@ -0,0 +1,63 @@
+import { useUserStore } from "@/store/userStore";
+import { useEnv } from "./useEnv";
+import { useTransferPage } from "./useTransferPage";
+import { useAppConfig } from "./useAppConfig";
+
+export const events = {
+  ExamStartEnter: 'exam-start-enter',
+
+  ExamStartLoadSuccess: 'exam-start-load-success',
+  ExamStartLoadSlow: 'exam-start-load-slow',
+  ExamStartLoadError: 'exam-start-load-error',
+
+  ExamStartCombineData: 'exam-start-combine-data',
+  ExamStartCombineDataSuccess: 'exam-start-combine-data-success',
+  ExamStartCombineDataError: 'exam-start-combine-data-error',
+
+  ExamStartGetPaperSuccess: 'exam-start-get-paper-success',
+  ExamStartGetPaperSlow: 'exam-start-get-paper-slow',
+  ExamStartGetPaperError: 'exam-start-get-paper-error',
+
+  ExamStartBeginExamineeSlow: 'exam-start-begin-examinee-slow',
+  ExamStartBeginExamineeSuccess: 'exam-start-begin-examinee-success',
+  ExamStartBeginExamineeError: 'exam-start-begin-examinee-error',
+
+  ExamStartInitError: 'exam-start-init-error',
+  ExamStartSubmitSuccess: 'exam-start-submit-success',
+  ExamStartSubmitError: 'exam-start-submit-error',
+  ExamStartExit: 'exam-start-exit',
+  PracticeDetailEnter: 'practice-detail-enter',
+} as const;
+
+type EventType = typeof events[keyof typeof events];
+
+const { isAppLogReportEnable } = useAppConfig();
+const userStore = useUserStore();
+const { platform } = useEnv();
+
+export const useReport = () => {
+  const { prevData } = useTransferPage();
+  const report = (event: EventType, extraData: any = {}) => {
+    if (isAppLogReportEnable.value) {
+      uni.report(event, getReportData(extraData));
+    }
+  }
+  const getReportData = (extraData: any = {}) => {
+    return {
+      pageOptions: prevData.value,
+      userInfo: {
+        userName: userStore.userInfo.userName,
+        phonenumber: userStore.userInfo.phonenumber,
+      },
+      platform: platform.value,
+      extraData,
+      createTime: Date.now()
+    }
+  }
+
+  return {
+    report,
+    getReportData,
+    events
+  }
+}

+ 12 - 0
src/pages.json

@@ -314,6 +314,18 @@
           "style": {
             "navigationBarTitleText": ""
           }
+        },
+        {
+          "path": "pages/qa/qa",
+          "style": {
+            "navigationBarTitleText": ""
+          }
+        },
+        {
+          "path": "pages/debug/debug",
+          "style": {
+            "navigationBarTitleText": ""
+          }
         }
       ]
     },

+ 55 - 0
src/pagesMain/pages/index/components/index-maintain-popup.vue

@@ -0,0 +1,55 @@
+<template>
+  <ie-popup title="" mode="center" ref="popupRef" :close-on-click-overlay="false" :showToolbar="false">
+    <view class="w-[88vw] relative">
+      <ie-image :is-oss="true" src="/study-bg15.png" custom-class="absolute top-0 left-0 w-full h-392 z-0"
+        mode="aspectFill" />
+      <view class="text-center my-40 text-[16px] text-fore-title font-bold">系统维护公告</view>
+      <view class="px-30 mt-30 text-[15px] whitespace-pre leading-[24px]">
+        <view>{{ maintainTitle }}</view>
+        <view>维护开始时间:<text class="text-primary">{{ maintainBeginTime }}</text></view>
+        <view>维护结束时间:<text class="text-primary">{{ maintainEndTime }}</text></view>
+        <view>{{ maintainContent }}</view>
+      </view>
+      <view class="px-30 my-40">
+        <uv-button type="primary" shape="circle" @click="handleConfirm">知道了</uv-button>
+      </view>
+    </view>
+  </ie-popup>
+</template>
+<script lang="ts" setup>
+import { useAppStore } from '@/store/appStore';
+
+const appStore = useAppStore();
+const popupRef = ref();
+let callback: ((value: void | PromiseLike<void>) => void) | null = null;
+const maintainTitle = computed(() => {
+  return appStore.maintainNotice?.title || '';
+});
+const maintainContent = computed(() => {
+  return appStore.maintainNotice?.content || '';
+});
+const maintainBeginTime = computed(() => {
+  return appStore.maintainNotice?.beginTime || '';
+});
+const maintainEndTime = computed(() => {
+  return appStore.maintainNotice?.endTime || '';
+});
+const handleConfirm = () => {
+  appStore.maintainNotice!.read = true;
+  if (callback !== null) {
+    callback();
+  }
+  close();
+}
+const open = () => {
+  return new Promise((resolve) => {
+    callback = resolve;
+    popupRef.value.open();
+  });
+}
+const close = () => {
+  popupRef.value.close();
+}
+defineExpose({ open, close })
+</script>
+<style lang="scss" scoped></style>

+ 18 - 3
src/pagesMain/pages/index/components/index-map.vue

@@ -16,7 +16,7 @@
           style="margin-bottom: -18rpx">
           第{{ i + 1 }}步
         </view>
-        <view class="w-full bg-back-light rounded-10 flex flex-col items-center pt-36 pb-28"
+        <view class="w-full bg-back-light rounded-10 flex flex-col items-center pt-36 pb-22"
           style="box-shadow: 1px 2px 0px 0px #DCF8BC;">
           <view>
             <view class="text-24 text-fore-title flex items-center gap-12">
@@ -37,6 +37,7 @@
 import { routes } from "@/common/routes";
 import { useTransferPage } from "@/hooks/useTransferPage";
 import { useUserStore } from "@/store/userStore";
+import { getVoluntaryRule } from "@/api/modules/voluntary";
 
 interface SiteMap {
   title: string;
@@ -47,6 +48,7 @@ interface SiteMap {
 
 const userStore = useUserStore()
 const { transferTo } = useTransferPage()
+const ruleRefId = ref('');
 
 const goSimulate = async () => {
   const isLogin = await userStore.checkLogin();
@@ -66,11 +68,11 @@ const goSimulate = async () => {
 const maps = computed<SiteMap[]>(() => [{
   title: '本省规则',
   desc: '填志愿不踩坑',
-  pagePath: routes.newsDetail + '?id=' + (userStore.isHN ? 1065 : 1078)
+  pagePath: ruleRefId.value ? routes.newsDetail + '?id=' + ruleRefId.value : ''
 }, {
   title: '自我评价',
   desc: '了解自身优势',
-  pagePath: ''
+  pagePath: routes.pageTestCenter
 }, {
   title: '职业规划',
   desc: '锁定职业方向',
@@ -103,6 +105,19 @@ const handleMap = (m: SiteMap) => {
   if (!m.pagePath) return
   transferTo(m.pagePath)
 }
+const loadData = () => {
+  getVoluntaryRule().then(res => {
+    if (res.rows.length) {
+      ruleRefId.value = res.rows[0].refIds;
+    } else {
+      ruleRefId.value = '';
+    }
+  });
+}
+
+onLoad(() => {
+  loadData();
+});
 </script>
 
 <style scoped></style>

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

@@ -1,5 +1,5 @@
 <template>
-  <ie-popup title="" mode="bottom" ref="popupRef" :showToolbar="false">
+  <ie-popup title="" mode="bottom" ref="popupRef" :close-on-click-overlay="false" :showToolbar="false">
     <view class="popup-content relative">
       <ie-image :is-oss="true" src="/study-bg15.png" custom-class="absolute top-0 left-0 w-full h-392 z-0"
         mode="aspectFill" />

+ 10 - 1
src/pagesMain/pages/index/index.vue

@@ -25,6 +25,7 @@
     <template #tabbar>
       <ie-tabbar :active="0" />
       <index-popup ref="popupRef" />
+      <index-maintain-popup ref="maintainPopupRef" />
     </template>
   </ie-page>
 </template>
@@ -35,13 +36,16 @@ import IndexGuide from './components/index-guide.vue';
 import IndexNews from './components/index-news.vue';
 import IndexMap from "./components/index-map.vue";
 import indexPopup from './components/index-popup.vue';
+import indexMaintainPopup from './components/index-maintain-popup.vue';
 
 import { useUserStore } from '@/store/userStore';
 // @ts-ignore
 import { useTransferPage } from '@/hooks/useTransferPage';
+import { useAppStore } from '@/store/appStore';
 import { useNavbar } from '@/hooks/useNavbar';
 import { onPageShow } from '@dcloudio/uni-app';
 
+const appStore = useAppStore();
 const { routes, transferTo } = useTransferPage();
 const { baseStickyTop } = useNavbar();
 const scrollTop = ref(0);
@@ -96,7 +100,12 @@ onPageScroll((e) => {
     scrollTop.value = e.scrollTop;
   }
 });
-onMounted(() => {
+const maintainPopupRef = ref();
+onMounted(async () => {
+  if (appStore.hasMaintain && !appStore.maintainNotice!.read) {
+    await uni.$uv.sleep(500);
+    await maintainPopupRef.value?.open();
+  }
   checkProvinceInfo();
   if (userStore.isLogin) {
     checkInfo();

+ 2 - 5
src/pagesMain/pages/me/components/me-menu.vue

@@ -109,11 +109,8 @@ const handleNavigate = async (pagePath: string, title: string) => {
   }
 }
 const handleQuestion = async () => {
-  transferTo(routes.pageWebview, {
-    data: {
-      title: '常见问题',
-      url: config.faqUrl
-    }
+  transferTo(routes.pageQa, {
+    data: {}
   });
 }
 const handleLogout = async () => {

+ 44 - 0
src/pagesMain/pages/splash/components/maintain-popup.vue

@@ -0,0 +1,44 @@
+<template>
+  <ie-popup title="" mode="center" ref="popupRef" :close-on-click-overlay="false" :showToolbar="false">
+    <view class="w-[88vw] relative">
+      <ie-image :is-oss="true" src="/study-bg15.png" custom-class="absolute top-0 left-0 w-full h-392 z-0"
+        mode="aspectFill" />
+      <view class="text-center my-40 text-[16px] text-fore-title font-bold">系统维护中</view>
+      <view class="px-30 mt-30 text-[15px] whitespace-pre leading-[24px]">
+        <view>预计结束时间:<text class="text-primary">{{ endTime }}</text></view>
+        <view>系统维护期间暂停使用,敬请谅解!</view>
+      </view>
+      <view class="px-30 my-40">
+        <uv-button type="primary" shape="circle" @click="handleConfirm">知道了</uv-button>
+      </view>
+    </view>
+  </ie-popup>
+</template>
+<script lang="ts" setup>
+import { useAppStore } from '@/store/appStore';
+
+const appStore = useAppStore();
+const popupRef = ref();
+let callback: ((value: void | PromiseLike<void>) => void) | null = null;
+const endTime = computed(() => {
+  return appStore.maintainNotice?.endTime || '';
+});
+const handleConfirm = () => {
+  appStore.maintainNotice!.read = true;
+  if (callback !== null) {
+    callback();
+  }
+  close();
+}
+const open = () => {
+  return new Promise((resolve) => {
+    callback = resolve;
+    popupRef.value.open();
+  });
+}
+const close = () => {
+  popupRef.value.close();
+}
+defineExpose({ open, close })
+</script>
+<style lang="scss" scoped></style>

+ 43 - 18
src/pagesMain/pages/splash/splash.vue

@@ -7,49 +7,74 @@
         </template>
       </uv-image>
     </view>
+    <maintain-popup ref="maintainPopupRef" />
   </ie-page>
 </template>
 
 <script lang="ts" setup>
 import config from '@/config';
+import { useEnv } from '@/hooks/useEnv';
 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';
+import MaintainPopup from './components/maintain-popup.vue';
 
+const { platform } = useEnv();
 const splashTimeout = 1200;
 const appStore = useAppStore();
-const userStore = useUserStore();
 const { transferTo, routes } = useTransferPage();
+
 // #ifdef H5
 uni.hideTabBar();
 // #endif
 const imgLaunch = computed(() => {
   return config.ossUrl + '/launch.png'
 });
+const maintainPopupRef = ref();
 const handleLoad = () => {
   if (typeof window !== 'undefined' && window !== null && window.platform === 'wap2app') {
     window.webviewBridge.getStatusBarHeight().then(height => {
       appStore.statusBarHeight = height;
     });
   }
-  // 执行初始化的操作:预加载字典、提前校验token是否有效等
-  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'
+  appStore.getMaintainNotice().then(async (isMaintaining) => {
+    // 执行初始化的操作:预加载字典、提前校验token是否有效等
+    if (!isMaintaining) {
+      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'
+          });
+          // transferTo('/pagesStudy/pages/knowledge-practice/knowledge-practice', {
+          //   type: 'reLaunch'
+          // });
+        }
       });
-      // transferTo('/pagesStudy/pages/knowledge-practice/knowledge-practice', {
-      //   type: 'reLaunch'
-      // });
+    } else {
+      setTimeout(() => {
+        nextTick(() => {
+          maintainPopupRef.value?.open().then(() => {
+            // #ifdef H5
+            if (platform.value === 'wap2app') {
+              plus.runtime.quit();
+            } else {
+              window.close();
+            }
+            // #endif
+            // #ifdef MP-WEIXIN
+            wx.exitMiniProgram();
+            // #endif
+          });
+        });
+      }, 800);
+
     }
   });
 };

+ 37 - 32
src/pagesMain/pages/volunteer/components/volunteer-banner.vue

@@ -1,55 +1,60 @@
 <template>
-    <view class="px-28 flex gap-18">
-        <volunteer-banner-item :title="mainEntry.title" :desc="mainEntry.desc" desc-bg-color="#A8F496"
-                               bg-src="/volunteer/index/banner_rate.png" class="flex-1" custom-class="h-342"
-                               @click="goRateCalc"/>
-        <view class="flex-1 flex flex-col justify-between">
-            <volunteer-banner-item :title="secondaryEntry.title" :desc="secondaryEntry.desc" desc-bg-color="#F8ECA6"
-                                   bg-src="/volunteer/index/banner_skill.png" custom-class="h-162"
-                                   @click="goSkillCalc"/>
-            <volunteer-banner-item title="志愿表" desc="我的报考意向" desc-bg-color="#DDD6FF"
-                                   bg-src="/volunteer/index/banner_list.png" custom-class="h-162"
-                                   @click="goVoluntaryList"/>
-        </view>
+  <view class="px-28 flex gap-18">
+    <volunteer-banner-item :title="mainEntry.title" :desc="mainEntry.desc" desc-bg-color="#A8F496"
+      bg-src="/volunteer/index/banner_rate.png" class="flex-1" custom-class="h-342" @click="goRateCalc" />
+    <view class="flex-1 flex flex-col justify-between">
+      <volunteer-banner-item :title="secondaryEntry.title" :desc="secondaryEntry.desc" desc-bg-color="#F8ECA6"
+        bg-src="/volunteer/index/banner_skill.png" custom-class="h-162" @click="goSkillCalc" />
+      <volunteer-banner-item title="志愿表" desc="我的报考意向" desc-bg-color="#DDD6FF" bg-src="/volunteer/index/banner_list.png"
+        custom-class="h-162" @click="goVoluntaryList" />
     </view>
+  </view>
 </template>
 
 <script setup lang="ts">
 import VolunteerBannerItem from "@/pagesMain/pages/volunteer/components/volunteer-banner-item.vue";
-import {useTransferPage} from "@/hooks/useTransferPage";
-import {routes} from "@/common/routes";
-import {useUserStore} from "@/store/userStore";
+import { useTransferPage } from "@/hooks/useTransferPage";
+import { routes } from "@/common/routes";
+import { useUserStore } from "@/store/userStore";
 
-const {transferTo} = useTransferPage()
+const { transferTo } = useTransferPage()
 const userStore = useUserStore()
 
 const mainEntry = computed(() => userStore.isVHS ? {
-    title: '填志愿',
-    desc: '大数据分析+智能算法'
+  title: '填志愿',
+  desc: '大数据分析+智能算法'
 } : {
-    title: '测录取概率',
-    desc: '院校录取风险评估'
+  title: '测录取概率',
+  desc: '院校录取风险评估'
 })
 
 const secondaryEntry = computed(() => userStore.isVHS ? {
-    title: '查位次',
-    desc: '查历年等效分'
+  title: '查位次',
+  desc: '查历年等效分'
 } : {
-    title: '测技能分',
-    desc: '精准定位'
+  title: '测技能分',
+  desc: '精准定位'
 })
 
-const goRateCalc = function () {
-    transferTo(userStore.isVHS ? routes.VHSIndex : routes.voluntaryIndex)
+const goRateCalc = async () => {
+  transferTo(userStore.isVHS ? routes.VHSIndex : routes.voluntaryIndex)
 }
-const goSkillCalc = function () {
-    transferTo(userStore.isVHS ? routes.pageRanking : routes.skillIndex)
+const goSkillCalc = async () => {
+  if (userStore.isVHS) {
+    const isLogin = await userStore.checkLogin();
+    if (isLogin) {
+      transferTo(routes.pageRanking)
+    }
+  } else {
+    transferTo(routes.skillIndex)
+  }
 }
-const goVoluntaryList = function () {
+const goVoluntaryList = async () => {
+  const isLogin = await userStore.checkLogin();
+  if (isLogin) {
     transferTo(userStore.isVHS ? routes.VHSList : routes.voluntaryList)
+  }
 }
 </script>
 
-<style scoped>
-
-</style>
+<style scoped></style>

+ 14 - 6
src/pagesOther/components/ie-condition-dropdown/ie-condition-dropdown-popup.vue

@@ -2,9 +2,12 @@
     <uv-popup ref="popup" mode="top" :safe-area-inset-bottom="false" :offset-top="relTop" @change="handleChange">
         <scroll-view scroll-y style="max-height: 35vh;">
             <view class="w-screen px-30 py-10 box-border" @touchmove.stop>
-                <component :is="comp" v-model="model[config.key]" placement="column" icon-placement="right">
-                    <component :is="itemComp" v-for="i in list" :name="getValue(i)" :label="getLabel(i)"/>
-                </component>
+                <uv-checkbox-group v-if="multiple" v-model="model[config.key]" placement="column" icon-placement="right">
+                    <uv-checkbox v-for="i in list" :name="getValue(i)" :label="getLabel(i)"/>
+                </uv-checkbox-group>
+                <uv-radio-group v-else v-model="model[config.key]" placement="column" icon-placement="right">
+                    <uv-radio v-for="i in list" :name="getValue(i)" :label="getLabel(i)"/>
+                </uv-radio-group>
             </view>
         </scroll-view>
         <ie-bottom-buttons left-type="primary" left="重置" :right="right" class="p-30"
@@ -13,7 +16,6 @@
 </template>
 
 <script setup>
-import {useElementBounding} from "@vueuse/core";
 import {deepClone, sleep} from "@/uni_modules/uv-ui-tools/libs/function";
 import IeBottomButtons from "@/pagesOther/components/ie-bottom-buttons/ie-bottom-buttons.vue";
 import UvCheckboxGroup from "@/uni_modules/uv-checkbox/components/uv-checkbox-group/uv-checkbox-group.vue";
@@ -25,6 +27,9 @@ import {useInjectSearchModel} from "@/pagesOther/components/ie-condition/useSear
 import {
     useInjectConditionDropdownPopup
 } from "@/pagesOther/components/ie-condition-dropdown/useConditionDropdownPopupInjection";
+// #ifdef H5
+import {useElementBounding} from "@vueuse/core";
+// #endif
 
 const popup = ref(null)
 const {bottom, container} = useInjectConditionDropdownPopup()
@@ -36,8 +41,6 @@ const condition = ref(null)
 const list = computed(() => condition.value?.list || [])
 const config = computed(() => condition.value?.config || {})
 const multiple = computed(() => config.value?.multiple)
-const comp = computed(() => multiple.value ? UvCheckboxGroup : UvRadioGroup)
-const itemComp = computed(() => multiple.value ? UvCheckbox : UvRadio)
 
 const right = computed(() => {
     if (!multiple.value) return '确定'
@@ -46,12 +49,17 @@ const right = computed(() => {
     return `确定(${cur.length})`
 })
 
+// #ifdef H5
 // TODO: 相对位置不精准
 // baseTop 理论上应该找当前容器的值,但目前筛选器是紧贴swiper容器的,所以取的是筛选器的顶部
 // 如果以后有定位不对的情况,请修正该值
 const {top: baseTop} = useElementBounding(container)
 const {bottom: baseBottom} = useElementBounding(bottom)
 const relTop = computed(() => baseBottom.value - baseTop.value - 1)
+// #endif
+// #ifdef MP-WEIXIN
+const refTop = ref(44 - 1)
+// #endif
 
 const handleReset = () => {
     // 重置本地model状态

+ 1 - 1
src/pagesOther/components/ie-condition-dropdown/ie-condition-dropdown.vue

@@ -10,8 +10,8 @@
 
 <script setup>
 /*TODO:暂不要给ie-condition-dropdown套uv-form,会导致zIndex失效,待改善*/
-import {findAncestorComponentByName} from "@/utils/uni-helper";
 import {createPropDefine} from "@/utils";
+import {findAncestorComponentByName} from "@/utils/uni-helper";
 import {useInjectSearchModel} from "@/pagesOther/components/ie-condition/useSearchModelInjection";
 import {
     useProvideConditionDropdownPopup

+ 2 - 2
src/pagesOther/pages/skill/index/index.vue

@@ -45,7 +45,7 @@ import {getSkillRules, postSkillRules} from "@/api/modules/voluntary";
 import {useAuth} from "@/hooks/useAuth";
 import {EnumUserRole} from "@/common/enum";
 
-const {hasPermission} = useAuth();
+const {checkVipPermission} = useAuth();
 const emptyImg = computed(() => config.ossUrl + '/volunteer/voluntary/index/empty_data.png')
 
 const form = ref<InstanceType<typeof VoluntaryForm>>()
@@ -91,7 +91,7 @@ const handleSelect = async () => {
 }
 
 const handleSubmit = async () => {
-    const hasAuth = hasPermission([EnumUserRole.VIP]);
+  const hasAuth = checkVipPermission();
     if (!hasAuth) {
         return;
     }

+ 3 - 3
src/pagesOther/pages/university/detail/components/plan-enroll-list.vue

@@ -75,7 +75,7 @@ import {useAuth} from "@/hooks/useAuth";
 import {EnumUserRole} from "@/common/enum";
 import {useUserStore} from "@/store/userStore";
 
-const {hasPermission} = useAuth();
+const {checkVipPermission} = useAuth();
 
 const props = withDefaults(defineProps<{
     mode: HistoryMode;
@@ -152,7 +152,7 @@ const handleRuleClick = (descriptor: IPlanEnrollDescriptor, history: IPlanEnroll
 }
 
 const handleAddVoluntary = async (item: IPlanEnrollHistory) => {
-    const hasAuth = hasPermission([EnumUserRole.VIP]);
+  const hasAuth = checkVipPermission();
     if (!hasAuth) {
         return;
     }
@@ -162,7 +162,7 @@ const handleAddVoluntary = async (item: IPlanEnrollHistory) => {
 }
 
 const handleRateVoluntary = (item: IPlanEnrollHistory) => {
-    const hasAuth = hasPermission([EnumUserRole.VIP]);
+    const hasAuth = checkVipPermission();
     if (!hasAuth) {
         return;
     }

+ 2 - 2
src/pagesOther/pages/university/index/index.vue

@@ -21,7 +21,7 @@ import { useTransferPage } from "@/hooks/useTransferPage";
 import { useAuth } from "@/hooks/useAuth";
 import { EnumUserRole } from "@/common/enum";
 
-const { hasPermission } = useAuth();
+const { checkVipPermission } = useAuth();
 const { transferTo, routes } = useTransferPage();
 const current = ref(0);
 const tabs = ref<SwiperTabItem[]>([{
@@ -36,7 +36,7 @@ const handleChangeSwiper = function (e: any) {
   current.value = e.detail.current;
 }
 const handleDetail = (u: University.University) => {
-  const hasAuth = hasPermission([EnumUserRole.VIP]);
+  const hasAuth = checkVipPermission();
   if (!hasAuth) {
     return;
   }

+ 2 - 2
src/pagesOther/pages/voluntary/index/index.vue

@@ -37,7 +37,7 @@ import {getRenderRules, postRenderRules} from "@/api/modules/voluntary";
 import { useAuth } from "@/hooks/useAuth";
 import { EnumUserRole } from "@/common/enum";
 
-const { hasPermission } = useAuth();
+const { checkVipPermission } = useAuth();
 const emptyImg = computed(() => config.ossUrl + '/volunteer/voluntary/index/empty_data.png')
 
 const form = ref<InstanceType<typeof VoluntaryForm>>()
@@ -87,7 +87,7 @@ const processSelected = async (picked: SelectedUniversityMajor) => {
 }
 
 const handleSubmit = async () => {
-    const hasAuth = hasPermission([EnumUserRole.VIP]);
+    const hasAuth = checkVipPermission();
     if (!hasAuth) {
         return;
     }

+ 2 - 2
src/pagesOther/pages/voluntary/list/list.vue

@@ -50,7 +50,7 @@ interface ActionItem {
     disabled?: boolean;
 }
 
-const {hasPermission} = useAuth();
+const {checkVipPermission} = useAuth();
 const {transferTo} = useTransferPage()
 const list = ref<VoluntaryRecord[]>([])
 const paging = ref<ZPagingInstance>()
@@ -166,7 +166,7 @@ const handleDeleted = () => {
 }
 
 const handleAdd = async () => {
-    const hasAuth = hasPermission([EnumUserRole.VIP]);
+    const hasAuth = checkVipPermission();
     if (!hasAuth) {
         return;
     }

+ 2 - 2
src/pagesStudy/components/vhs-exam-item.vue

@@ -30,7 +30,7 @@ import { useTransferPage } from '@/hooks/useTransferPage';
 import { useAuth } from '@/hooks/useAuth';
 import { getOpenExaminee } from '@/api/modules/study';
 
-const { hasPermission } = useAuth();
+const { checkVipPermission } = useAuth();
 const { transferTo } = useTransferPage();
 const props = defineProps<{
   data: Study.VHSPaper;
@@ -41,7 +41,7 @@ const isFinished = computed(() => {
 });
 
 const handleStartExam = () => {
-  const hasAuth = hasPermission([EnumUserRole.VIP]);
+  const hasAuth = checkVipPermission();
   if (hasAuth) {
     if (isFinished.value) {
       transferTo('/pagesStudy/pages/simulation-analysis/simulation-analysis', {

+ 229 - 23
src/pagesStudy/pages/exam-start/exam-start.vue

@@ -32,7 +32,13 @@ import {
   EXAM_PAGE_OPTIONS,
   EXAM_DATA
 } from '@/types/injectionSymbols';
+import { useAppStore } from '@/store/appStore';
+import { useEnv } from '@/hooks/useEnv';
+import { useReport } from '@/hooks/useReport';
 
+const { report, events } = useReport();
+const appStore = useAppStore();
+const { platform } = useEnv();
 const userStore = useUserStore();
 // import { Examinee, ExamPaper, ExamPaperSubmit } from '@/types/study';
 const { prevData, transferBack, transferTo } = useTransferPage<Transfer.ExamAnalysisPageOptions, {}>();
@@ -207,10 +213,14 @@ const handleSubmit = (tempSave: boolean = false) => {
       duration: practiceDuration.value
     };
     console.log('提交试卷参数', params)
+    const start = Date.now();
     await commitExamineePaper(params);
+    const costTime = Date.now() - start;
+    report(events.ExamStartSubmitSuccess, { time: costTime });
     if (isSimulationExam.value || isTestExam.value) {
       if (!tempSave) {
         setTimeout(async () => {
+          report(events.ExamStartExit);
           uni.$ie.hideLoading();
           await nextTick();
           confirmQuit.value = true;
@@ -234,6 +244,7 @@ const handleSubmit = (tempSave: boolean = false) => {
     } else if (isPracticeExam.value) {
       if (!tempSave) {
         setTimeout(async () => {
+          report(events.ExamStartExit);
           uni.$ie.hideLoading();
           await nextTick();
           confirmQuit.value = true;
@@ -250,6 +261,7 @@ const handleSubmit = (tempSave: boolean = false) => {
           });
         }, 2500);
       } else {
+        report(events.ExamStartExit);
         uni.$ie.hideLoading();
         confirmQuit.value = true;
         confirmShowing.value = false;
@@ -291,6 +303,135 @@ const restoreQuestion = (savedQuestion: Study.ExamineeQuestion[], fullQuestion:
   }
   return fullQuestion;
 }
+/**
+ * 带超时检测的 getOpenExaminee 请求
+ * 在 3s, 5s, 10s, 15s, 20s 这 5 个时间点检测超时并上报
+ * 每个时间点如果请求还未返回,都会上报一次
+ */
+const getOpenExamineeWithTimeoutCheck = async (params: Study.OpenExamineeRequestDTO) => {
+  // 超时时间档次(单位:毫秒)
+  const timeoutLevels = [3000, 5000, 10000, 15000, 20000];
+  // 存储所有定时器ID,用于清理
+  const timers: number[] = [];
+  // 请求是否已完成
+  let isCompleted = false;
+
+  // 创建超时检测定时器
+  timeoutLevels.forEach((timeout) => {
+    const timer = setTimeout(() => {
+      // 如果请求已完成,不进行上报
+      if (isCompleted) {
+        return;
+      }
+      // 上报超时数据,超时时间单位为秒
+      // 每个时间点如果请求还未返回,都会上报一次
+      report(events.ExamStartLoadSlow, { time: timeout });
+    }, timeout) as unknown as number;
+    timers.push(timer);
+  });
+
+  try {
+    // 执行请求
+    const result = await getOpenExaminee(params);
+    // 标记请求已完成
+    isCompleted = true;
+    // 清除所有定时器
+    timers.forEach((timer) => clearTimeout(timer));
+    return result;
+  } catch (error) {
+    // 请求失败时也要清除定时器
+    isCompleted = true;
+    timers.forEach((timer) => clearTimeout(timer));
+    throw error;
+  }
+};
+
+/**
+ * 带超时检测的 getPaper 请求
+ * 在 3s, 5s, 10s, 15s, 20s 这 5 个时间点检测超时并上报
+ * 每个时间点如果请求还未返回,都会上报一次
+ */
+const getPaperWithTimeoutCheck = async (params: Study.GetExamPaperRequestDTO) => {
+  // 超时时间档次(单位:毫秒)
+  const timeoutLevels = [3000, 5000, 10000, 15000, 20000];
+  // 存储所有定时器ID,用于清理
+  const timers: number[] = [];
+  // 请求是否已完成
+  let isCompleted = false;
+
+  // 创建超时检测定时器
+  timeoutLevels.forEach((timeout) => {
+    const timer = setTimeout(() => {
+      // 如果请求已完成,不进行上报
+      if (isCompleted) {
+        return;
+      }
+      // 上报超时数据,超时时间单位为秒
+      // 每个时间点如果请求还未返回,都会上报一次
+      report(events.ExamStartGetPaperSlow, { time: timeout });
+    }, timeout) as unknown as number;
+    timers.push(timer);
+  });
+
+  try {
+    // 执行请求
+    const result = await getPaper(params);
+    // 标记请求已完成
+    isCompleted = true;
+    // 清除所有定时器
+    timers.forEach((timer) => clearTimeout(timer));
+    return result;
+  } catch (error) {
+    // 请求失败时也要清除定时器
+    isCompleted = true;
+    timers.forEach((timer) => clearTimeout(timer));
+    throw error;
+  }
+};
+
+/**
+ * 带超时检测的 beginExaminee 请求
+ * 在 3s, 5s, 10s, 15s, 20s 这 5 个时间点检测超时并上报
+ * 每个时间点如果请求还未返回,都会上报一次
+ */
+const beginExamineeWithTimeoutCheck = async (examineeId: number) => {
+  // 超时时间档次(单位:毫秒)
+  const timeoutLevels = [3000, 5000, 10000, 15000, 20000];
+  // 存储所有定时器ID,用于清理
+  const timers: number[] = [];
+  // 请求是否已完成
+  let isCompleted = false;
+
+  // 创建超时检测定时器
+  timeoutLevels.forEach((timeout) => {
+    const timer = setTimeout(() => {
+      // 如果请求已完成,不进行上报
+      if (isCompleted) {
+        return;
+      }
+      // 上报超时数据,超时时间单位为秒
+      // 每个时间点如果请求还未返回,都会上报一次
+      report(events.ExamStartBeginExamineeSlow, { time: timeout });
+    }, timeout) as unknown as number;
+    timers.push(timer);
+  });
+
+  try {
+    // 执行请求
+    const result = await beginExaminee(examineeId);
+    // 标记请求已完成
+    isCompleted = true;
+    // 清除所有定时器
+    timers.forEach((timer) => clearTimeout(timer));
+    return result;
+  } catch (error) {
+    // 请求失败时也要清除定时器
+    isCompleted = true;
+    timers.forEach((timer) => clearTimeout(timer));
+    throw error;
+  }
+};
+
 // 1、加载知识点练习数据
 const loadPracticeData = async () => {
   const { paperType, readonly, practiceInfo } = prevData.value;
@@ -301,17 +442,25 @@ const loadPracticeData = async () => {
       data = res.data;
     }
   } else {
-    const params = {
-      paperType: paperType,
-      relateId: practiceInfo?.relateId,
-    } as Study.OpenExamineeRequestDTO;
-    if (userStore.isVHS) {
-      params.questionType = practiceInfo?.questionType;
-    } else {
-      params.directed = practiceInfo?.directed || false;
+    try {
+      const params = {
+        paperType: paperType,
+        relateId: practiceInfo?.relateId,
+      } as Study.OpenExamineeRequestDTO;
+      if (userStore.isVHS) {
+        params.questionType = practiceInfo?.questionType;
+      } else {
+        params.directed = practiceInfo?.directed || false;
+      }
+      const start = Date.now();
+      const res = await getOpenExamineeWithTimeoutCheck(params);
+      data = res.data || {};
+      const costTime = Date.now() - start;
+      console.log('开卷用时:', costTime);
+      report(events.ExamStartLoadSuccess, { time: costTime, request: params, response: res.data || {} });
+    } catch (error) {
+      report(events.ExamStartLoadError, { error: error });
     }
-    const res = await getOpenExaminee(params);
-    data = res.data || {};
   }
 
   if (!data) {
@@ -332,8 +481,16 @@ const loadExamData = async () => {
       const res = await getExamineeResult(simulationInfo.examineeId);
       data = res.data;
     } else {
-      const res = await beginExaminee(simulationInfo.examineeId);
-      data = res.data || {};
+      try {
+        const start = Date.now();
+        const res = await beginExamineeWithTimeoutCheck(simulationInfo.examineeId);
+        data = res.data || {};
+        const costTime = Date.now() - start;
+        report(events.ExamStartBeginExamineeSuccess, { time: costTime, request: { examineeId: simulationInfo.examineeId }, response: res.data || {} });
+      } catch (error) {
+        report(events.ExamStartBeginExamineeError, { error: error });
+        throw error;
+      }
     }
     if (!data) {
       uni.$ie.hideLoading();
@@ -371,16 +528,49 @@ const loadExamData = async () => {
 // }
 const combinePaperData = async (examinee: Study.Examinee, paperType: EnumPaperType) => {
   examineeId.value = examinee.examineeId;
-  if (examinee.paperId) {
-    const res = await getPaper({
+  report(events.ExamStartCombineData, {
+    envUser: localStorage.getItem('ie-user'),
+    envApp: localStorage.getItem('ie-app'),
+  });
+
+  if (!examinee.paperId) {
+    report(events.ExamStartGetPaperError, {
+      error: '获取试卷数据失败,没有paperId',
+      examineeId: examinee.examineeId
+    });
+    return;
+  }
+
+  // 第一步:请求试卷数据(单独 try-catch)
+  let res;
+  try {
+    const start = Date.now();
+    res = await getPaperWithTimeoutCheck({
       type: paperType,
       id: examinee.paperId
     });
+    const costTime = Date.now() - start;
+    report(events.ExamStartGetPaperSuccess, {
+      time: costTime,
+      paperId: examinee.paperId,
+      paperType: paperType
+    });
+  } catch (error: any) {
+    // 请求失败的错误上报
+    report(events.ExamStartGetPaperError, {
+      error: error
+    });
+    throw error; // 继续抛出,让外层处理
+  }
+
+  // 第二步:处理和组装数据(单独 try-catch)
+  try {
     paperData.value = res.data;
     paperData.value.questions = restoreQuestion(examinee.questions, res.data.questions);
     console.log('初始化数据', paperData.value.questions)
     setQuestionList(paperData.value.questions);
     setDuration(examinee.duration || 0);
+
     await nextTick();
     const targetQuestion = flatQuestionList.value.find(item => item.id === prevData.value.questionId);
     if (targetQuestion) {
@@ -412,6 +602,16 @@ const combinePaperData = async (examinee: Study.Examinee, paperType: EnumPaperTy
         startTime();
       }
     }
+    report(events.ExamStartCombineDataSuccess, {
+      paperId: examinee.paperId,
+      paperType: paperType
+    });
+  } catch (error: any) {
+    // 数据处理失败的错误上报
+    report(events.ExamStartCombineDataError, {
+      error: error
+    });
+    throw error; // 继续抛出,让外层处理
   }
 }
 const handleSwiperTipNext = () => {
@@ -427,19 +627,25 @@ const handleGuideClose = () => {
 const loadData = async () => {
   uni.$ie.showLoading();
   const { paperType } = prevData.value;
-  if (paperType === EnumPaperType.PRACTICE || paperType === EnumPaperType.COURSE) {
-    loadPracticeData();
-  } else if (paperType === EnumPaperType.SIMULATED || paperType === EnumPaperType.TEST) {
-    // if (paperType === EnumPaperType.SIMULATED && userStore.isVHS) {
-    //   loadVHSPaperData();
-    // } else {
-    //   loadExamData();
-    // }
-    loadExamData();
+  try {
+    if (paperType === EnumPaperType.PRACTICE || paperType === EnumPaperType.COURSE) {
+      loadPracticeData();
+    } else if (paperType === EnumPaperType.SIMULATED || paperType === EnumPaperType.TEST) {
+      loadExamData();
+    }
+  } catch (error) {
+    report(events.ExamStartInitError, { error });
+    uni.$ie.showToast('加载失败,请稍后重试');
+    setTimeout(() => {
+      uni.$ie.hideLoading();
+      transferBack();
+    }, 1000);
   }
 };
+
 onLoad(() => {
   console.log(prevData.value)
+  report(events.ExamStartEnter);
   loadData();
   uni.addInterceptor('navigateBack', {
     invoke: (e) => {

+ 38 - 26
src/pagesStudy/pages/knowledge-practice-detail/knowledge-practice-detail.vue

@@ -27,10 +27,12 @@
     </view>
     <ie-safe-toolbar :height="84" :shadow="false" bg-color="#FFFFFF">
       <view class="h-[84px] px-46 bg-white flex items-center justify-between gap-x-40">
-        <view class="text-30 text-primary bg-back flex-1 py-22 rounded-full text-center h-fit" @click="handleStartPractice">
+        <view class="text-30 text-primary bg-back flex-1 py-22 rounded-full text-center h-fit"
+          @click="handleStartPractice">
           继续刷题
         </view>
-        <view class="text-30 text-white bg-primary flex-1 py-22 rounded-full text-center h-fit" @click="handleViewAnalysis">
+        <view class="text-30 text-white bg-primary flex-1 py-22 rounded-full text-center h-fit"
+          @click="handleViewAnalysis">
           查看解析
         </view>
       </view>
@@ -41,11 +43,13 @@
 import RateChart from '@/pagesStudy/pages/simulation-analysis/components/rate-chart.vue';
 import ExamStat from '@/pagesStudy/pages/simulation-analysis/components/exam-stat.vue';
 import { useTransferPage } from '@/hooks/useTransferPage';
-import { Study, Transfer } from '@/types';
+import type { Study, Transfer } from '@/types';
 import { getExamineeResult } from '@/api/modules/study';
 import { EnumPaperType } from '@/common/enum';
-const { prevData, transferTo } = useTransferPage<Transfer.PracticeResultPageOptions, Transfer.ExamAnalysisPageOptions>();
+import { useReport } from '@/hooks/useReport';
 
+const { prevData, transferTo } = useTransferPage<Transfer.PracticeResultPageOptions, Transfer.ExamAnalysisPageOptions>();
+const { report, events } = useReport();
 
 const rightRate = computed(() => {
   const { totalCount = 0, wrongCount = 0 } = examineeData.value || {};
@@ -80,7 +84,7 @@ const handleQuestionDetail = (item: Study.Question) => {
   }
   transferTo('/pagesStudy/pages/exam-start/exam-start', {
     data: {
-      name: paperName.value,
+      // name: paperName.value,
       paperType: prevData.value.paperType || EnumPaperType.PRACTICE,
       readonly: true,
       questionId: item.id,
@@ -88,7 +92,9 @@ const handleQuestionDetail = (item: Study.Question) => {
         name: prevData.value.name,
         relateId: examineeData.value.knowledgeId,
         directed: prevData.value.directed,
-        examineeId: examineeData.value.examineeId
+        examineeId: examineeData.value.examineeId,
+        // 对口升学
+        questionType: prevData.value.questionType
       },
     }
   });
@@ -99,34 +105,38 @@ const handleStartPractice = () => {
     console.error('knowledgeId is null');
     return;
   }
+  const pageOptions: Transfer.ExamAnalysisPageOptions = {
+    paperType: prevData.value.paperType || EnumPaperType.PRACTICE,
+    readonly: false,
+    practiceInfo: {
+      name: prevData.value.name,
+      relateId: knowledgeId,
+      directed: prevData.value.directed,
+      questionType: prevData.value.questionType
+    },
+  }
   transferTo('/pagesStudy/pages/exam-start/exam-start', {
-    data: {
-      name: paperName.value,
-      paperType: prevData.value.paperType || EnumPaperType.PRACTICE,
-      practiceInfo: {
-        name: prevData.value.name,
-        relateId: knowledgeId,
-        directed: prevData.value.directed
-      },
-    }
+    data: pageOptions
   });
 }
 const handleViewAnalysis = () => {
   if (!examineeData.value) {
     return;
   }
+  const pageOptions: Transfer.ExamAnalysisPageOptions = {
+    paperType: prevData.value.paperType || EnumPaperType.PRACTICE,
+    readonly: true,
+    practiceInfo: {
+      name: prevData.value.name,
+      relateId: examineeData.value.knowledgeId,
+      directed: prevData.value.directed,
+      examineeId: examineeData.value.examineeId,
+      // 对口升学
+      questionType: prevData.value.questionType
+    },
+  }
   transferTo('/pagesStudy/pages/exam-start/exam-start', {
-    data: {
-      name: paperName.value,
-      paperType: EnumPaperType.PRACTICE,
-      readonly: true,
-      practiceInfo: {
-        name: prevData.value.name,
-        relateId: examineeData.value.knowledgeId,
-        directed: prevData.value.directed,
-        examineeId: examineeData.value.examineeId
-      },
-    }
+    data: pageOptions
   });
 }
 const loadData = async () => {
@@ -139,6 +149,8 @@ const loadData = async () => {
   }
 }
 onLoad(() => {
+  report(events.PracticeDetailEnter)
+  console.log(prevData.value)
   loadData();
 });
 </script>

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

@@ -36,7 +36,7 @@ const currentSubjectIndex = ref<number>(-1);
 const userStore = useUserStore();
 const pagingRef = ref<ZPagingInstance>();
 
-const { hasPermission } = useAuth();
+const { checkVipPermission } = useAuth();
 const pageTitle = computed(() => {
   const { isVHS, directed, questionType } = prevData.value;
   if (isVHS) {
@@ -96,7 +96,7 @@ const loadKnowledgeList = async () => {
 }
 
 const handleStartPractice = async (node: Study.KnowledgeNode) => {
-  const hasAuth = hasPermission([EnumUserRole.VIP, EnumUserRole.AGENT, EnumUserRole.TEACHER]);
+  const hasAuth = checkVipPermission();
   if (hasAuth) {
     transferTo('/pagesStudy/pages/exam-start/exam-start', {
       data: {

+ 5 - 5
src/pagesStudy/pages/textbooks-practice/textbooks-practice.vue

@@ -30,8 +30,8 @@ import { useAuth } from '@/hooks/useAuth';
 
 const { prevData, transferTo } = useTransferPage();
 const userStore = useUserStore();
-const pagingRef = ref();
-const { hasPermission } = useAuth();
+const pagingRef = ref<ZPagingInstance>();
+const { checkVipPermission } = useAuth();
 
 const pageTitle = computed(() => {
   return '教材同步练习';
@@ -50,17 +50,17 @@ const loadKnowledgeList = async (page: number, size: number) => {
     uni.$ie.showLoading();
     const { data } = await getTextbooksKnowledgeList();
     treeData.value = data as Study.KnowledgeNode[];
-    pagingRef.value.complete(data);
+    pagingRef.value?.complete(data);
   } catch (error) {
     console.log(error);
-    pagingRef.value.complete(false);
+    pagingRef.value?.complete(false);
   } finally {
     uni.$ie.hideLoading();
   }
 }
 
 const handleStartPractice = async (node: Study.KnowledgeNode) => {
-  const hasAuth = hasPermission([EnumUserRole.VIP, EnumUserRole.AGENT, EnumUserRole.TEACHER]);
+  const hasAuth = checkVipPermission();
   if (hasAuth) {
     transferTo('/pagesStudy/pages/exam-start/exam-start', {
       data: {

+ 67 - 0
src/pagesSystem/pages/debug/debug.vue

@@ -0,0 +1,67 @@
+<template>
+  <ie-page bg-color="#F6F8FA">
+    <ie-navbar title="调试" />
+    <view class="p-20">
+      <view class="my-20">用户数据:</view>
+      <view class="bg-white">
+        <textarea class="p-20 h-400 break-all" v-model="userJson" maxlength="-1"></textarea>
+      </view>
+    </view>
+    <view class="m-20">
+      <uv-button type="primary" @click="handleLoad">加载</uv-button>
+    </view>
+    <view class="p-20">
+      <view class="my-20">app数据:</view>
+      <view class="bg-white">
+        <textarea class="p-20 h-400 break-all" v-model="appJson" maxlength="-1"></textarea>
+      </view>
+    </view>
+    <view class="m-20">
+      <uv-button type="primary" @click="handleLoad">加载</uv-button>
+    </view>
+    <view class="m-20">
+      <uv-button type="success" @click="handleToHome">进入首页</uv-button>
+    </view>
+  </ie-page>
+</template>
+
+<script lang="ts" setup>
+import { useUserStore } from '@/store/userStore';
+import { useAppStore } from '@/store/appStore';
+
+const userStore = useUserStore();
+const appStore = useAppStore();
+
+const userJson = ref('');
+const appJson = ref('');
+
+const handleLoad = () => {
+  if (userJson.value) {
+    try {
+      userStore.loadFromLocal(userJson.value);
+      uni.$ie.showToast('加载成功');
+    } catch (error) {
+      uni.$ie.showToast('加载失败');
+    }
+  }
+}
+const handleLoadApp = () => {
+  if (appJson.value) {
+    try {
+      appStore.loadFromLocal(appJson.value);
+      uni.$ie.showToast('加载成功');
+    } catch (error) {
+      uni.$ie.showToast('加载失败');
+    }
+  }
+}
+
+const handleToHome = () => {
+  uni.reLaunch({
+    url: '/pagesMain/pages/index/index',
+    type: 'reLaunch'
+  });
+}
+</script>
+
+<style lang="scss"></style>

+ 66 - 0
src/pagesSystem/pages/qa/qa.vue

@@ -0,0 +1,66 @@
+<template>
+  <ie-page>
+    <ie-navbar title="常见问题" />
+    <view class="p-30">
+      <view class="item">1. 如何开通单招一卡通VIP?</view>
+      <view class="content">VIP有两种开通方式:① 线上开通会员:下载单招一卡通APP,注册账号成为新用户,点击右下角进入“我的”,点击开通VIP,购买成功后,当前账号由普通用户升级为会员用户,自动开通会员功能,若未显示VIP权限,需退出当前账号重新登录生效。②线下购买会员实体卡:下载单招一卡通APP,直接输入会员卡卡号和密码登录,完善信息后,会员即生效。</view>
+      <view class="item">2. 单招一卡通VIP会员权益介绍</view>
+      <view class="content">同学你好!VIP会员权益包括:智能填报,院校、专业、职业查询,权威测评,精品课程,分类题库和资讯,八大会员特权,助你轻松备考升学!</view>
+
+      <view class="item">3. 单招卡如何使用(绑定)</view>
+      <view class="content">打开单招一卡通APP,输入卡号和密码进行登录,根据系统提示进行绑定。</view>
+
+      <view class="item">4. 开通VIP后我能使用多久?</view>
+      <view class="content">本卡使用有效期至学生毕业年份单招录取结束止。</view>
+
+      <view class="item">5. 开通会员卡或购买会员卡,可以退换吗?</view>
+      <view class="content">不记名、不挂失,一经出售,不退换。</view>
+
+      <view class="item">6. 会员卡实物卡遗失可补吗?</view>
+      <view class="content">不可以,收到卡后请妥善保管好。</view>
+
+      <view class="item">7. 如何修改密码?密码忘记了怎么办?</view>
+      <view class="content">打开APP,右下角点击我的,点击修改密码。如果忘记密码,在登录框点击忘记密码,根据提示填写即可修改密码。</view>
+
+      <view class="item">8. 怎么更换绑定的手机号?</view>
+      <view class="content">您可以打开APP,右下角点击我的,点击基本信息,然后点击变更手机号进行更换绑定。</view>
+
+      <view class="item">9. 功能为什么无法正常使用?</view>
+      <view class="content">同学,建议您检查网络设置、退出登录或重新安装单招一卡通APP试一下,若仍无法解决,请联系客服提交意见并配图说明,我们会尽快为您解决!</view>
+
+      <view class="item">10. 为什么我根据条件筛选出来的结果没有一所院校?</view>
+      <view class="content">单招一卡通会依照用户的条件,智能推荐与用户所选项目相匹配的院校与专业。如果出现无院校推荐的情况,即说明没有院校符合您的筛选条件,建议清空筛选条件。</view>
+
+      <view class="item">11. 免费注册后如何绑定志愿卡?</view>
+      <view class="content">打开APP,右下角点击我的--绑定会员卡,按系统提示填写即可。</view>
+
+      <view class="item">12. 省份和学校填写错误怎么办?</view>
+      <view class="content">单招卡省份、学校确定之后是无法进行修改的,如您在使用途中遇到问题可以拨打单招一卡通客服热线4001797985。</view>
+
+      <view class="item">13. 线上购卡高考年份选错了怎么办?</view>
+      <view class="content">单招年份确定之后是无法进行修改的,如您在使用途中遇到问题可以拨打单招一卡通客服热线4001797985。</view>
+    </view>
+  </ie-page>
+</template>
+
+<script setup>
+
+</script>
+
+<style lang="scss" scoped>
+.title {
+  @apply text-center text-[18px] text-fore-title font-bold my-30;
+}
+
+.subtitle {
+  @apply text-[15px] text-fore-title my-10;
+}
+
+.item {
+  @apply text-[15px] text-fore-title font-medium;
+}
+
+.content {
+  @apply text-[14px] text-fore-tip mt-10 mb-30;
+}
+</style>

+ 29 - 0
src/pagesSystem/pages/setting/setting.vue

@@ -38,6 +38,35 @@ const settings = computed(() => [
       transferTo(routes.pagePrivacyPolicy);
     }
   },
+  {
+    title: '清除数据',
+    icon: 'empty-page',
+    isLink: true,
+    value: '',
+    handler: () => {
+      // 清除所有本地数据,重启 app
+      uni.$ie.showModal({
+        title: '提示',
+        content: '清除数据后需要重新登录\n是否继续?',
+      }).then(confirm => {
+        if (confirm) {
+          uni.clearStorageSync();
+          setTimeout(() => {
+            // 再次尝试清除,防止有补偿机制
+            uni.clearStorageSync();
+            // #ifdef H5
+            window.location.href = '/h5/';
+            // #endif
+            // #ifdef MP-WEIXIN
+            wx.restartMiniProgram({
+              path: '/pagesMain/pages/splash/splash'
+            });
+            // #endif
+          }, 100);
+        }
+      })
+    }
+  },
   {
     title: '当前版本',
     icon: 'tags',

+ 92 - 4
src/store/appStore.ts

@@ -1,9 +1,10 @@
 import { defineStore } from 'pinia';
-import { AppStoreState, ConfigItem, DictItem, PickerItem } from '@/types';
-import { getConfig, getProvinces } from '@/api/modules/system';
+import type { AppStoreState, ConfigItem, DictItem, PickerItem, System } from '@/types';
+import { getConfig, getProvinces, getSystemNotice } from '@/api/modules/system';
 import { useDictStore } from '@/store/dictStore';
 import { EnumDictName } from '@/common/enum';
 import { useUserStore } from '@/store/userStore';
+
 const preloadDicts: string[] = [
   EnumDictName.EXAM_TYPE
 ];
@@ -15,7 +16,8 @@ export const useAppStore = defineStore('ie-app', {
       activeTabbar: 0,
       statusBarHeight: 0,
       systemInfo: null as UniApp.GetSystemInfoResult | null,
-      appConfig: [] as ConfigItem[]
+      appConfig: [] as ConfigItem[],
+      maintainNotice: null
     }
   },
   getters: {
@@ -31,6 +33,12 @@ export const useAppStore = defineStore('ie-app', {
     isMp(): boolean {
       return this.sysInfo.uniPlatform === 'mp-weixin';
     },
+    hasMaintain(): boolean {
+      return this.maintainNotice !== null;
+    },
+    isMaintaining(): boolean {
+      return isBetweenTime(this.maintainNotice?.beginTime, this.maintainNotice?.endTime);
+    },
   },
   actions: {
     async init() {
@@ -84,7 +92,45 @@ export const useAppStore = defineStore('ie-app', {
     getAppConfig(key: string): string | undefined {
       return this.appConfig.find(item => item.configKey === key)?.configValue;
     },
-
+    async getMaintainNotice() {
+      try {
+        const { rows } = await getSystemNotice(2);
+        if (rows && rows.length) {
+          const notice = extractMaintenanceContent(rows[0].noticeContent);
+          if (notice) {
+            const { title, beginTime, endTime, content } = notice;
+            this.maintainNotice = {
+              title,
+              beginTime,
+              endTime,
+              content,
+              read: this.maintainNotice?.read || false
+            };
+            return Promise.resolve(isBetweenTime(beginTime, endTime));
+          }
+          return Promise.resolve(false);
+        } else {
+          this.maintainNotice = null;
+          return Promise.resolve(false);
+        }
+      } catch (error) {
+        console.error('获取维护公告失败:', error);
+        return Promise.resolve(false);
+      }
+    },
+    loadFromLocal(state: string) {
+      const value = JSON.parse(state);
+      if (value) {
+        const { appConfig, activeTabbar, statusBarHeight, sysInfo, isInitialized } = value;
+        this.appConfig = appConfig || [];
+        this.activeTabbar = activeTabbar || 0;
+        this.statusBarHeight = statusBarHeight || 0;
+        this.systemInfo = sysInfo || null;
+        this.isInitialized = isInitialized || false;
+        return true;
+      }
+      return false;
+    },
     setActiveTabbar(index: number) {
       this.activeTabbar = index;
     },
@@ -102,3 +148,45 @@ export const useAppStore = defineStore('ie-app', {
     omit: [],
   }
 });
+
+// 将中文格式转换为标准格式
+const convertToStandardFormat = (dateStr: string): string => {
+  // 使用正则提取各部分并补零
+  const match = dateStr.match(/(\d{4})年(\d{1,2})月(\d{1,2})日(\d{1,2}):(\d{1,2}):(\d{1,2})/);
+  if (!match) {
+    return dateStr
+      .replace(/年/g, '-')
+      .replace(/月/g, '-')
+      .replace(/日/g, ' ');
+  }
+
+  // 对月、日、时、分、秒补零
+  const [, year, month, day, hour, minute, second] = match;
+  return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')} ${hour.padStart(2, '0')}:${minute.padStart(2, '0')}:${second.padStart(2, '0')}`;
+};
+
+function extractMaintenanceContent(htmlString: string): System.MaintainNotice | null {
+  const htmlContent = htmlString.replaceAll("</p><p>", "\n").replaceAll("</p>", "").replaceAll("<br>", "").replaceAll("<p>", "");
+  // 使用正则表达式匹配维护时间范围(完整的一次匹配)
+  const timeMatch = htmlContent.match(/维护开始时间:(\d{4}年\d{1,2}月\d{1,2}日\d{1,2}:\d{1,2}:\d{1,2})\n维护结束时间:(\d{4}年\d{1,2}月\d{1,2}日\d{1,2}:\d{1,2}:\d{1,2})/);
+  if (timeMatch) {
+    // 提取开始时间和结束时间
+    const startTimeStr = timeMatch[1];
+    const endTimeStr = timeMatch[2];
+    const beginTime = convertToStandardFormat(startTimeStr);
+    const endTime = convertToStandardFormat(endTimeStr);
+    const rows = htmlContent.split('\n');
+    return { title: rows[0], content: rows[3], beginTime, endTime, read: false };
+  }
+  return null;
+}
+
+const isBetweenTime = (beginTime?: string, endTime?: string) => {
+  const now = Date.now();
+  if (beginTime && endTime) {
+    beginTime = beginTime.replaceAll('-', '/');
+    endTime = endTime.replaceAll('-', '/');
+    return new Date(beginTime).getTime() <= now && new Date(endTime).getTime() >= now;
+  }
+  return false;
+}

+ 27 - 2
src/store/userStore.ts

@@ -6,7 +6,7 @@ import type { Study, UserStoreState } from '@/types';
 import type { UserInfo, VipCardInfo } from '@/types/user';
 import tools from '@/utils/uni-tool';
 import defaultAvatar from '@/static/personal/avatar_default.png'
-import {CardType, EnumBindScene, EnumExamType, EnumReviewMode, EnumScoreLock, EnumUserType} from '@/common/enum';
+import { CardType, EnumBindScene, EnumExamType, EnumReviewMode, EnumScoreLock, EnumUserType } from '@/common/enum';
 import { getDirectedSchool, saveDirectedSchool } from '@/api/modules/study';
 import { usePay } from '@/hooks/usePay';
 import { getUserBindCard } from '@/api/modules/user';
@@ -357,7 +357,32 @@ export const useUserStore = defineStore('ie-user', {
           });
         }
       });
-    }
+    },
+    loadFromLocal(state: string) {
+      const value = JSON.parse(state);
+      if (value) {
+        const { accessToken, user, card, isDefaultModifyPwd, isPasswordExpired,
+          org, rememberPwd, cardNo, cardPassword, directedSchoolList, practiceSettings,
+          isExamGuideShow,
+          tempInfo,
+        } = value;
+        this.accessToken = accessToken;
+        this.user = user;
+        this.card = card;
+        this.isDefaultModifyPwd = isDefaultModifyPwd;
+        this.isPasswordExpired = isPasswordExpired;
+        this.org = org;
+        this.rememberPwd = rememberPwd;
+        this.cardNo = cardNo;
+        this.cardPassword = cardPassword;
+        this.directedSchoolList = directedSchoolList;
+        this.practiceSettings = practiceSettings;
+        this.isExamGuideShow = isExamGuideShow;
+        this.tempInfo = tempInfo;
+        return true;
+      }
+      return false;
+    },
   },
   persist: {
     storage: {

+ 1 - 0
src/types/index.ts

@@ -82,6 +82,7 @@ export interface AppStoreState {
   statusBarHeight: number;
   systemInfo: UniApp.GetSystemInfoResult | null;
   appConfig: ConfigItem[];
+  maintainNotice: System.MaintainNotice | null;
 }
 
 export interface DictStoreState {

+ 17 - 0
src/types/system.ts

@@ -3,4 +3,21 @@ export type ProvinceItem = {
   areaName: string;
   provinceName: string;
   children: ProvinceItem[]
+}
+
+export type SystemNotice = {
+  id: number;
+  noticeTitle: string;
+  noticeContent: string;
+  noticeType: string;
+  status: string;
+  createdAt: string;
+}
+
+export interface MaintainNotice {
+  title: string;
+  content: string;
+  beginTime: string;
+  endTime: string;
+  read: boolean;
 }

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

@@ -209,28 +209,9 @@ const tool: IeTool = {
     })
     // #endif
 
-    // #ifdef MP-ALIPAY
-    // 支付宝小程序
-    if (my.openBrowser) {
-      my.openBrowser({ url: url })
-    } else {
-      // 降级处理
-      uni.setClipboardData({
-        data: url,
-        success: () => {
-          uni.showModal({
-            title: '提示',
-            content: '链接已复制,请在浏览器中打开',
-            showCancel: false
-          })
-        }
-      })
-    }
-    // #endif
-
     // 其他平台,如App、快应用等,可以根据需要补充
     // 对于不支持直接打开浏览器的平台,使用降级方案
-    // #ifndef H5 || MP-WEIXIN || MP-ALIPAY
+    // #ifndef H5 || MP-WEIXIN
     uni.setClipboardData({
       data: url,
       success: () => {