瀏覽代碼

引入环境变量,新增开通会员页面,跳转普通用户权限

wyling 3 年之前
父節點
當前提交
30ded92e65

+ 5 - 0
package-lock.json

@@ -455,6 +455,11 @@
       "resolved": "https://registry.nlark.com/csstype/download/csstype-2.6.17.tgz",
       "integrity": "sha1-TPMOuH4dGgBdi2UQ+VKSQT9qHA4="
     },
+    "dayjs": {
+      "version": "1.10.7",
+      "resolved": "https://registry.nlark.com/dayjs/download/dayjs-1.10.7.tgz",
+      "integrity": "sha1-LPX5Gt0oEWdIRAhmoKHSbzps5Gg="
+    },
     "deepmerge": {
       "version": "4.2.2",
       "resolved": "https://registry.npm.taobao.org/deepmerge/download/deepmerge-4.2.2.tgz",

+ 1 - 0
package.json

@@ -8,6 +8,7 @@
   },
   "dependencies": {
     "axios": "^0.21.1",
+    "dayjs": "^1.10.7",
     "howler": "^2.2.3",
     "marked": "^3.0.4",
     "mockjs": "^1.1.0",

+ 1 - 0
src/App.vue

@@ -22,6 +22,7 @@ export default defineComponent({
   -moz-user-select: none;
   -ms-user-select: none;
   user-select: none;
+  box-sizing: border-box;
 }
 #app::-webkit-scrollbar {
   width: 0px;

+ 4 - 2
src/api/modules/auth.ts

@@ -20,8 +20,10 @@ interface loginRes {
  */
 export async function login(code: LocationQueryValue | LocationQueryValue[]) {
   let res = await request({
-    // url: "/login/code/test",
-    url: "/login/code",
+    url:
+      import.meta.env.MODE === "development"
+        ? "/login/code/test"
+        : "/login/code",
     method: "post",
     headers: {
       isToken: false,

+ 18 - 11
src/api/modules/pay.ts

@@ -1,6 +1,8 @@
 import request from "../request";
 import * as API from "@/api";
 import store from "@/store";
+import { Toast } from "vant";
+import router from "@/route";
 
 declare let WeixinJSBridge: any;
 
@@ -21,12 +23,13 @@ function onBridgeReady(payConfig: PayConfig) {
       if (res.err_msg == "get_brand_wcpay_request:ok") {
         // 使用以上方式判断前端返回,微信团队郑重提示:
         //res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
-        console.log("支付成功");
         let userDataRes = await API.userInfo();
         store.commit("setUserData", {
           ...store.getters.getUserData,
           expireTime: userDataRes.data.data.expireTime,
         });
+        Toast.success("支付成功");
+        router.push("/home");
       }
     }
   );
@@ -52,20 +55,12 @@ export async function prepareOrder(dictCode: number) {
 }
 
 /**
- * 获取支付配置,并调起微信支付
+ * 循环支付,并调起微信支付
  */
 export async function loopPrepareOrder() {
-  // let res = await request({
-  //   url: "/student/wx/prepareOrder-test",
-  //   method: "post",
-  // });
-
   let res = await request({
-    url: "/student/wx/prepareOrder",
+    url: "/student/wx/prepareOrder-test",
     method: "post",
-    data: {
-      dictCode: 33,
-    },
   });
 
   if (res.data.code === 200) {
@@ -74,3 +69,15 @@ export async function loopPrepareOrder() {
     }
   }
 }
+
+/**
+ * 获取vip价格字典
+ * @returns
+ */
+export async function getVipPrice() {
+  const res = await request({
+    url: `/system/dict/data/type/vip_type_price`,
+  });
+
+  return res.data;
+}

+ 1 - 1
src/hooks/index.ts

@@ -9,4 +9,4 @@ export const useExpireTime = () => {
   return {
     expireTime,
   };
-};
+};

+ 14 - 4
src/main.ts

@@ -4,17 +4,27 @@ import store from "./store";
 import router from "./route";
 import Vant from "vant";
 import "vant/lib/index.css";
-import './utils/rem'
-import components from './components'
+import "./utils/rem";
+import components from "./components";
 
 import VConsole from "vconsole";
-new VConsole()
+if (import.meta.env.MODE === "development") new VConsole();
 
 const app = createApp(App);
 app.use(store);
 app.use(router);
-app.use(components)
+app.use(components);
 app.use(Vant);
 app.mount("#app");
 
+declare let WeixinJSBridge: any;
 
+function onBridgeReady() {
+  WeixinJSBridge.call("hideToolbar");
+}
+
+if (typeof WeixinJSBridge == "undefined") {
+  document.addEventListener("WeixinJSBridgeReady", onBridgeReady, false);
+} else {
+  onBridgeReady();
+}

+ 30 - 0
src/route/guard.ts

@@ -0,0 +1,30 @@
+import { Router } from "vue-router";
+import store from "@/store";
+import dayjs from "dayjs";
+import { Toast } from "vant";
+const guard = (router: Router) => {
+  router.beforeEach(async (to, from, next) => {
+    const userTime = dayjs(store.getters.getUserData.expireTime).valueOf();
+    const currentTime = dayjs().valueOf();
+    switch (to.path) {
+      case "/exercise":
+        if (userTime - currentTime > 0) {
+          next();
+        } else {
+          Toast.fail("会员到期");
+        }
+        break;
+      case "/mockTest":
+        if (userTime - currentTime > 0) {
+          next();
+        } else {
+          Toast.fail("会员到期");
+        }
+        break;
+      default:
+        next();
+        break;
+    }
+  });
+};
+export default guard;

+ 8 - 8
src/route/index.ts

@@ -1,15 +1,14 @@
-import { createRouter, createWebHashHistory, createWebHistory } from "vue-router";
+import { createRouter, createWebHistory } from "vue-router";
+import guard from "./guard";
 
-// 导入library文件夹下的所有组件
-// 批量导入需要使用一个函数 require.context(dir,deep,matching)
-// 参数:1. 目录  2. 是否加载子目录  3. 加载的正则匹配
+// 导入views文件夹下的所有组件
 const modules = import.meta.glob("../views/**/index.vue");
 // 匹配到的文件名数组
-let aotuRoutes: any = [];
+let aotuRoutes: any[] = [];
 //根据名字查找路由项目
 const aotuRoutesFind = (name: String) => {
   let list: any = [];
-  //递归查询所路由,子路由;添加至list
+  //递归查询所路由,子路由;添加至list
   const recursionPush = (routes: any) => {
     routes.forEach((item: any) => {
       list.push(item);
@@ -52,8 +51,6 @@ for (const path in modules) {
   });
 }
 
-console.log(aotuRoutesFind("initMockTest-default"));
-
 // 创建路由实例并传递 `routes` 配置
 const router = createRouter({
   history: createWebHistory(),
@@ -66,4 +63,7 @@ const router = createRouter({
   ],
 });
 
+//注入路由守卫
+guard(router);
+
 export default router;

+ 7 - 0
src/route/routerPush.ts

@@ -0,0 +1,7 @@
+import router from ".";
+
+/**
+ * 会员购买页
+ * @returns
+ */
+export const goBuyVip = () => router.push("/buyVip");

+ 151 - 41
src/views/buyVip/index.vue

@@ -1,58 +1,168 @@
 <template>
-  <div class="login-box">
-    <!-- 登陆中 -->
-    <div v-if="loginState == 0">
-      <van-empty description="登陆中"></van-empty>
+  <m-nav-bar title="开通会员" :transparent="true" />
+  <div class="buyvip-box">
+    <img class="top-img" :src="topImg" alt="头部背景" />
+    <div class="info-warn">您还未开通完整版系统,付费购买后享受更多功能</div>
+    <div class="item-box">
+      <van-cell class="cell-title" title="选择充值方式" />
+      <div class="radio-box">
+        <div
+          v-for="(item, index) in goodsList"
+          :class="{ select: index === goodsIndex }"
+          @click="goodsIndex = index"
+        >
+          <span>{{ item.dictLabel }}</span>
+          <span>¥{{ item.dictValue.split(",")[0] }}</span>
+          <span>原价{{ item.dictValue.split(",")[1] }}元</span>
+        </div>
+      </div>
     </div>
-    <!-- 登陆成功 -->
-    <div v-else-if="loginState == 1">
-      <van-empty description="登陆成功"></van-empty>
+    <div class="item-box">
+      <van-cell class="cell-title" title="使用说明" />
+      <div class="readme">
+        <span>1. 有效期内拥有VIP权限.</span>
+        <span>2. 过期后可以使用免费用户的权限.</span>
+        <span>3. 可以续费.</span>
+      </div>
     </div>
-    <!-- 登陆失败 -->
-    <div v-else-if="loginState == 2">
-      <van-empty image="network" description="登录失败"></van-empty>
+    <div class="button-box">
+      <div class="button" @click="buy">立即开通</div>
     </div>
+    <div style="height: 60px"></div>
   </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from "vue";
-export default defineComponent({
-  name: "login",
-});
-</script>
-
 <script lang="ts" setup>
-import { useRoute, useRouter } from "vue-router";
-import * as API from "@/api";
+import topImg from "./top-background.png";
 import { ref } from "vue";
-import { useStore } from "vuex";
-const router = useRouter();
-const route = useRoute();
-const store = useStore();
-const loginState = ref(0);
-
-API.login(route.query.code).then(async (res) => {
-  if (res.code == 200) {
-    window.localStorage.setItem("token", res.data.token);
-    window.localStorage.setItem(
-      "userData",
-      JSON.stringify(res.data.wxUserInfo)
-    );
-    let userDataRes = await API.userInfo();
-    window.localStorage.setItem("expireTime", userDataRes.data.data.expireTime);
-    loginState.value = 1;
-    router.push("/home/test");
-  } else {
-    loginState.value = 2;
-  }
+import { getVipPrice, prepareOrder } from "@/api";
+const goodsIndex = ref(0);
+const goodsList = ref<any[]>([]);
+getVipPrice().then((res) => {
+  goodsList.value = res.data;
 });
+const buy = () => {
+  prepareOrder(goodsList.value[goodsIndex.value].dictCode);
+};
 </script>
 
 <style scoped lang="scss">
-.login-box {
+.buyvip-box {
+  width: 100%;
+  margin-top: -50px;
+  min-height: 100vh;
   display: flex;
-  justify-content: center;
   align-items: center;
+  flex-direction: column;
+  background: #ffdda6;
+  .top-img {
+    width: 100%;
+  }
+  .info-warn {
+    width: 345px;
+    height: 49px;
+    background: #ffffff;
+    border-radius: 10px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    font-size: 13px;
+    font-family: PingFang SC;
+    font-weight: 400;
+    color: #91540f;
+    margin-top: -70px;
+    margin-bottom: 10px;
+  }
+  .item-box {
+    width: 345px;
+    background: #ffffff;
+    border-radius: 10px;
+    padding: 15px;
+    margin-bottom: 10px;
+    .cell-title {
+      font-size: 16px;
+      font-family: PingFang SC;
+      font-weight: bold;
+      line-height: 20px;
+      color: #0a1a33;
+      margin-left: -10px;
+      margin-bottom: 15px;
+    }
+  }
+  .readme {
+    display: flex;
+    flex-direction: column;
+    font-size: 13px;
+    line-height: 20px;
+    span {
+      margin-bottom: 5px;
+    }
+  }
+  .radio-box {
+    display: flex;
+    justify-content: space-around;
+    align-items: center;
+    .select {
+      background: #fcf1de;
+    }
+    div {
+      width: 98px;
+      font-size: 20px;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: space-around;
+      background: #ffffff;
+      border: 1px solid #faca93;
+      height: 124px;
+      border-radius: 5px;
+      span {
+        font-family: PingFang SC;
+        &:nth-of-type(1) {
+          font-size: 13px;
+          font-weight: bold;
+          color: #91540f;
+        }
+        &:nth-of-type(2) {
+          font-size: 30px;
+          font-weight: bold;
+          color: #ff4d53;
+        }
+        &:nth-of-type(3) {
+          font-size: 11px;
+          font-weight: 400;
+          color: #8a9099;
+          text-decoration: line-through;
+        }
+      }
+    }
+  }
+  .button-box {
+    position: fixed;
+    background-color: #ffffff;
+    bottom: 0;
+    width: 375px;
+    height: 60px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    .button {
+      width: 152px;
+      height: 40px;
+      background: #faca93;
+      border-radius: 30px;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      font-size: 15px;
+      font-family: PingFang SC;
+      font-weight: 500;
+      color: #91540f;
+      letter-spacing: 0px;
+      &:active {
+        background-color: red;
+      }
+    }
+  }
 }
 </style>

二進制
src/views/buyVip/top-background.png


+ 2 - 1
src/views/home/children/test/components/userData.vue

@@ -7,7 +7,7 @@
       </div>
     </template>
     <template #right>
-      <div class="vip-box">
+      <div class="vip-box" @click="goBuyVip">
         <m-icon class="vip-icon" type="huiyuan" />
         <span>{{ expireTime ? expireTime : "开通会员" }}</span>
       </div>
@@ -20,6 +20,7 @@ import { useExpireTime } from "@/hooks";
 </script>
 
 <script lang="ts" setup>
+import { goBuyVip } from "@/route/routerPush";
 const { expireTime } = useExpireTime();
 </script>
 

+ 263 - 6
src/views/mockTest/components/hooks.ts

@@ -1,11 +1,268 @@
-import { defineEmits } from "vue";
+import {
+  ref,
+  watch,
+  onBeforeMount,
+  Ref,
+  computed,
+  nextTick,
+  ComputedRef,
+} from "vue";
+import * as API from "@/api";
+import { Howl, Howler } from "howler";
+import { useRoute } from "vue-router";
 
-export const useNext = () => {
-  const emits = defineEmits(["next"]);
-  const gotoNextPage = () => {
-    emits("next");
+//答题模式切换
+export function useTopicMode() {
+  const answerTypeList = ref([
+    { name: "顺序练习" },
+    { name: "随机练习" },
+    { name: "背题模式" },
+  ]);
+  const currentType = ref(0);
+  const typeParams = ref({
+    order: true, //顺序练习
+    answerShow: false, //背题模式
+  });
+  watch(currentType, (currentVal) => {
+    switch (currentVal) {
+      case 0:
+        typeParams.value.order = true;
+        typeParams.value.answerShow = false;
+        break;
+      case 1:
+        typeParams.value.order = false;
+        typeParams.value.answerShow = false;
+        break;
+      case 2:
+        typeParams.value.order = true;
+        typeParams.value.answerShow = true;
+        break;
+    }
+  });
+  return {
+    answerTypeList,
+    currentType,
+    typeParams,
+  };
+}
+
+//语音设置
+export function useAudioSet(currentSubject: ComputedRef<any>) {
+  const aotuPlayFlag = ref(false);
+
+  let sound: Howl;
+  /**
+   * 播放音频
+   * @param audioUrl
+   */
+  const audioPlay = (audioUrl: string | string[]) => {
+    audioPause();
+    sound = new Howl({
+      src: audioUrl,
+    });
+    sound.once("load", function () {
+      sound.play();
+    });
+    if (typeof audioUrl === "object") {
+      sound.once("end", () => {
+        sound = new Howl({
+          src: audioUrl[1],
+        });
+        sound.once("load", function () {
+          sound.play();
+        });
+      });
+    }
+  };
+
+  /**
+   * 读题
+   */
+  const subjectAudioPlay = (
+    type: "读题" | "读官方解释" | "读技巧解释" | "读题+答案"
+  ) => {
+    switch (type) {
+      case "读题":
+        audioPlay(currentSubject.value.issuemp3);
+        break;
+      case "读官方解释":
+        audioPlay(currentSubject.value.explainjsmp3);
+        break;
+      case "读技巧解释":
+        audioPlay(currentSubject.value.explainMp3);
+        break;
+      case "读题+答案":
+        audioPlay([
+          currentSubject.value.issuemp3,
+          currentSubject.value.answermp3,
+        ]);
+        break;
+      default:
+        break;
+    }
+  };
+
+  /**
+   * 停止播放
+   */
+  const audioPause = () => {
+    sound && sound.pause();
+  };
+
+  //音频模块end
+  const aotuPlaySet = () => {
+    aotuPlayFlag.value = !aotuPlayFlag.value;
+    aotuPlayFlag.value ? subjectAudioPlay("读题") : audioPause();
+  };
+
+  //自动读题
+  watch(currentSubject, () => {
+    if (aotuPlayFlag.value) subjectAudioPlay("读题"); //自动读题
+  });
+
+  return {
+    aotuPlayFlag,
+    aotuPlaySet,
+    subjectAudioPlay,
+  };
+}
+
+const useSubjectList = () => {
+  const subjectList = ref<any[]>([]); //题目列表
+  const subjectTotal = ref(0); //题目总数
+  const pageNum = ref(1); //当前请求页码
+  const pageSize = ref(100); //当前请求每页数据
+  const query = useRoute().query; //路由query参数
+  onBeforeMount(async () => {
+    const res = await API.getTopicList({
+      ...query,
+      pageNum: pageNum.value,
+      pageSize: pageSize.value,
+      isRand: true,
+    });
+    subjectList.value = res.list;
+    subjectTotal.value = 100;
+  });
+  //加载下一页数据
+  const loadNewSubject = async () => {
+    if (subjectList.value.length == subjectTotal.value) return;
+    pageNum.value++;
+    const res = await API.getTopicList({
+      ...query,
+      pageNum: pageNum.value,
+      pageSize: pageSize.value,
+    });
+    subjectList.value = subjectList.value.concat(res.list);
   };
+  const currentSubjectIndex = ref(0); //当前题目下标
+  //当前题目内容
+  const currentSubject = computed(() => {
+    return subjectList.value[currentSubjectIndex.value];
+  });
+  return {
+    subjectList,
+    subjectTotal,
+    loadNewSubject,
+    currentSubject,
+    currentSubjectIndex,
+  };
+};
+
+const useSubjectCheck = (
+  currentSubject: ComputedRef<any>,
+  nextSubject: () => Promise<void>
+) => {
+  const trueNum = ref(0); //正确数量
+  const falseNum = ref(0); //错误数量
+  const isJumpNext = ref(false); //答对跳转下一题
+
+  /**
+   * 选择答案后进行校验
+   */
+  const userAnswerChange = () => {
+    currentSubject.value.optsBack = currentSubject.value.opts.map(
+      (val: String) => {
+        let status;
+        if (currentSubject.value.answer.includes(val)) {
+          status = 1;
+        } else {
+          status = 0;
+        }
+        if (currentSubject.value.userAnswer.includes(val)) {
+          status += 2;
+        }
+        return { opt: val, status };
+      }
+    );
+    if (
+      JSON.stringify(currentSubject.value.answer) ==
+      JSON.stringify(currentSubject.value.userAnswer)
+    ) {
+      console.log("答案正确");
+      currentSubject.value.isTrue = true;
+      trueNum.value++;
+      if (isJumpNext.value) {
+        nextTick(() => {
+          nextSubject();
+        });
+      }
+    } else {
+      console.log("错误");
+      currentSubject.value.isTrue = false;
+      falseNum.value++;
+    }
+  };
+
+  return {
+    trueNum,
+    falseNum,
+    isJumpNext,
+    userAnswerChange,
+  };
+};
+
+export const useSubjectShowLogic = () => {
+  const {
+    subjectList,
+    subjectTotal,
+    loadNewSubject,
+    currentSubject,
+    currentSubjectIndex,
+  } = useSubjectList(); //获取题目列表
+
+  const nextBtnState = ref(true); //下一题数据请求锁
+  
+  /**
+   * 展示下一题
+   */
+  const nextSubject = async () => {
+    if (currentSubjectIndex.value < subjectList.value.length - 1) {
+      currentSubjectIndex.value++;
+    }
+  };
+  /**
+   * 展示上一题
+   */
+  const lastSubject = () => {
+    if (currentSubjectIndex.value > 0) {
+      currentSubjectIndex.value--;
+    }
+  };
+
+  const { trueNum, falseNum, isJumpNext, userAnswerChange } = useSubjectCheck(
+    currentSubject,
+    nextSubject
+  );
+
   return {
-    gotoNextPage,
+    currentSubject,
+    currentSubjectIndex,
+    subjectTotal,
+    nextSubject,
+    lastSubject,
+    trueNum,
+    falseNum,
+    isJumpNext,
+    userAnswerChange,
   };
 };

+ 40 - 260
src/views/mockTest/components/startTest.vue

@@ -21,26 +21,33 @@
   <div class="divider" />
   <!-- 题目模块 -->
   <!-- 题目预加载 -->
-  <m-empty v-if="!currentAnswer" />
+  <m-empty v-if="!currentSubject" />
   <!-- 题目预加载end -->
   <div class="problem-box" v-else>
     <!-- 题目内容 -->
     <div class="problem">
-      <span class="type">{{ topicType(currentAnswer.type) }}</span>
-      <span class="text">{{ currentAnswer.explain }}</span>
-      <img v-if="currentAnswer.image" :src="currentAnswer.image" class="img"/>
+      <span class="type">{{ currentSubject.type }}</span>
+      <span class="text">{{ currentSubject.explain }}</span>
+      <van-image
+        v-if="currentSubject.image"
+        :src="currentSubject.image"
+        class="img"
+      >
+        <template v-slot:loading>
+          <van-loading type="spinner" size="20" />
+        </template>
+      </van-image>
     </div>
     <!-- 选择内容 -->
     <div v-if="true">
       <!-- 单选 -->
       <van-radio-group
-        v-model="currentAnswer.userAnswer"
-        v-if="currentAnswer.type < 2"
-        @change="userAnswerChange"
+        v-model="currentSubject.userAnswer"
+        v-if="currentSubject.type != '多选题'"
         icon-size="35px"
       >
         <van-radio
-          v-for="(item, index) in currentAnswer.opts"
+          v-for="(item, index) in currentSubject.opts"
           :key="Number(index)"
           :name="item"
           class="answer"
@@ -54,9 +61,12 @@
       </van-radio-group>
       <!-- 多选 -->
       <div v-else>
-        <van-checkbox-group v-model="currentAnswer.userAnswer" icon-size="35px">
+        <van-checkbox-group
+          v-model="currentSubject.userAnswer"
+          icon-size="35px"
+        >
           <van-checkbox
-            v-for="(item, index) in currentAnswer.opts"
+            v-for="(item, index) in currentSubject.opts"
             :key="Number(index)"
             :name="item"
             class="answer"
@@ -68,21 +78,13 @@
             </template>
           </van-checkbox>
         </van-checkbox-group>
-        <van-button
-          round
-          type="primary"
-          class="checkbox-btn"
-          :disabled="currentAnswer.userAnswer.length == 0"
-          @click="userAnswerChange"
-          >确定</van-button
-        >
       </div>
     </div>
     <!-- 展示答题后选择内容 -->
     <div v-else-if="false">
       <div>
         <div
-          v-for="(item, index) in currentAnswer.optsBack"
+          v-for="(item, index) in currentSubject.optsBack"
           :key="Number(index)"
           class="answer-box"
         >
@@ -114,7 +116,7 @@
         </div>
       </div>
       <div class="checkbox-answer">
-        答案: {{ currentAnswer.answer.toString() }}
+        答案: {{ currentSubject.answer.toString() }}
       </div>
     </div>
     <!-- 展示答题后选择内容end -->
@@ -132,7 +134,7 @@
       <m-icon type="a-dtda" size="25px" />
       <span>读题+答案</span>
     </div> -->
-    <div class="function-item" @click="issueAudioPlay">
+    <div class="function-item" @click="subjectAudioPlay('读题')">
       <m-icon type="duti" size="25px" />
       <span>读题</span>
     </div>
@@ -150,52 +152,9 @@
     </div> -->
   </div>
   <!-- 功能选择列表End -->
-  <!-- 技巧讲解 -->
-  <van-overlay :show="skillsShow" @click="skillsShow = false" z-index="10">
-    <div class="skills-box" @click.stop>
-      <div class="skills">
-        <div class="title">技巧讲解</div>
-        <img
-          src="https://t1-1305573081.file.myqcloud.com/gif/2.gif"
-          class="img"
-        />
-        <van-divider class="divider">本题速记口诀</van-divider>
-        <div class="text">题目以“拘役”结尾.答对;以“徒刑”结尾.答错</div>
-        <div class="btn">
-          <span @click="skillsShow = false">关闭</span>
-          <span @click="JQexplainAudioPlay">语音重播</span>
-        </div>
-      </div>
-    </div>
-  </van-overlay>
-  <!-- 技巧讲解end -->
-  <!-- 官方解释 -->
-  <van-overlay :show="officialShow" @click="officialShow = false" z-index="10">
-    <div class="skills-box" @click.stop>
-      <div class="skills">
-        <div class="title">官方解释</div>
-        <div class="text">
-          1、申请城市公交车、大型货车、无轨电车或者有轨电车准驾车型的,在20周岁以上,50周岁以下;2、申请大型客车准驾车型的,在26周岁以上,50周岁以下;3、申请中型客车准驾车型的,在21周岁以上,50周岁以下;4、申请牵引车准驾车型的,在24周岁以上,50周岁以下。
-        </div>
-        <div class="btn">
-          <span @click="officialShow = false">关闭</span>
-          <span @click="explainAudioPlay">语音重播</span>
-        </div>
-      </div>
-    </div>
-  </van-overlay>
-  <!-- 官方解释end -->
-  <!-- 音频模块 -->
-  <audio ref="audio"></audio>
-  <!-- 音频模块end -->
   <!-- 题目模块end -->
   <!-- 设置操作栏 -->
   <van-popup v-model:show="setShow" position="bottom">
-    <van-cell center title="答对跳转下一题">
-      <template #right-icon>
-        <van-switch v-model="isJump" size="24" />
-      </template>
-    </van-cell>
     <van-cell center title="答题音效提示">
       <template #right-icon>
         <van-switch v-model="isSoundEffect" size="24" />
@@ -207,14 +166,14 @@
   <!-- <div style="height: 40px"></div> -->
   <!-- <m-button class="submitButton" text="交卷" /> -->
   <van-tabbar placeholder route>
-    <van-tabbar-item @click="currentAnswerIndexBack"
+    <van-tabbar-item @click="lastSubject"
       >上一题
       <template #icon>
         <m-icon type="shangyiti" />
       </template>
     </van-tabbar-item>
     <van-tabbar-item
-      >{{ idIndex }}/{{ total }}
+      >{{ currentSubjectIndex + 1 }}/{{ subjectTotal }}
       <template #icon>
         <m-icon type="zongtishu" />
       </template>
@@ -225,7 +184,7 @@
         <m-icon type="jiaojuan" />
       </template>
     </van-tabbar-item>
-    <van-tabbar-item @click="currentAnswerIndexGo"
+    <van-tabbar-item @click="nextSubject"
       >下一题
       <template #icon>
         <m-icon type="xiayiti" />
@@ -236,18 +195,10 @@
 </template>
 
 <script lang="ts" setup>
-import * as Api from "@/api";
 import { useRoute, useRouter } from "vue-router";
-import {
-  ref,
-  watch,
-  computed,
-  reactive,
-  onBeforeMount,
-  nextTick,
-  defineEmits,
-} from "vue";
+import { ref, defineEmits } from "vue";
 import { Dialog } from "vant";
+import { useSubjectShowLogic, useTopicMode, useAudioSet } from "./hooks";
 const router = useRouter();
 const route = useRoute();
 const onClickLeft = () => {
@@ -266,198 +217,27 @@ const gotoTest = () => {
     .catch(() => {});
 };
 
-//答题模式选择逻辑
-const answerTypeList = ref([
-  { name: "顺序练习" },
-  { name: "随机练习" },
-  { name: "背题模式" },
-]);
-const currentType = ref(0);
-const typeParams = ref({
-  order: true, //顺序练习
-  answerShow: false, //背题模式
-});
-watch(currentType, (currentVal) => {
-  switch (currentVal) {
-    case 0:
-      typeParams.value.order = true;
-      typeParams.value.answerShow = false;
-      break;
-    case 1:
-      typeParams.value.order = false;
-      typeParams.value.answerShow = false;
-      break;
-    case 2:
-      typeParams.value.order = true;
-      typeParams.value.answerShow = true;
-      break;
-  }
-});
-
-//自动读题
-const aotuPlayFlag = ref(false);
-const aotuPlaySet = () => {
-  aotuPlayFlag.value = !aotuPlayFlag.value;
-  if (aotuPlayFlag.value) issueAudioPlay();
-  if (!aotuPlayFlag.value) audioPause();
-};
-onBeforeMount(() => {
-  watch(currentAnswerIndex, () => {
-    if (aotuPlayFlag.value) issueAudioPlay(); //自动读题
-  });
-});
-//答题模式选择逻辑end
-
 //题目展示逻辑
-const topicList = ref([]);
-console.log(topicList);
-
-//API请求接口数据
-onBeforeMount(async () => {
-  const topicListApiRes = await Api.getTopicList({
-    ...route.query,
-    pageNum: 1,
-    pageSize: 100,
-    isRand: true,
-  });
-  topicList.value = topicListApiRes.list;
-  idIndex.value = currentAnswerIndex.value;
-  total.value = 100;
-});
-
-//当前题目数据
-const currentAnswerIndex = ref(0);
-const currentAnswer: any = computed(() => {
-  return topicList.value[currentAnswerIndex.value];
-});
-
-//上一题,下一题
-const currentAnswerIndexBack = () => {
-  if (topicList.value.length == 0) return;
-  currentAnswerIndex.value =
-    (currentAnswerIndex.value - 1 + topicList.value.length) %
-    topicList.value.length;
-};
-const currentAnswerIndexGo = () => {
-  if (topicList.value.length == 0) return;
-  currentAnswerIndex.value =
-    (currentAnswerIndex.value + 1) % topicList.value.length;
-};
-
-//选择答案后
-const userAnswerChange = () => {
-  currentAnswer.value.optsBack = currentAnswer.value.opts.map((val: String) => {
-    let status;
-    if (currentAnswer.value.answer.includes(val)) {
-      status = 1;
-    } else {
-      status = 0;
-    }
-    if (currentAnswer.value.userAnswer.includes(val)) {
-      status += 2;
-    }
-    return { opt: val, status };
-  });
-  if (
-    JSON.stringify(currentAnswer.value.answer) ==
-    JSON.stringify(currentAnswer.value.userAnswer)
-  ) {
-    console.log("答案正确");
-    currentAnswer.value.isTrue = true;
-    trueNum.value++;
-    if (isJump.value)
-      setTimeout(() => {
-        currentAnswerIndexGo();
-      }, 200);
-  } else {
-    console.log("错误");
-    currentAnswer.value.isTrue = false;
-    falseNum.value++;
-  }
-};
-const topicType = (type: Number) => {
-  switch (type) {
-    case 0:
-      return "判断题";
-    case 1:
-      return "单选题";
-    case 2:
-      return "多选题";
-  }
-};
-//题目展示逻辑end
-
-//技巧讲解
-const skillsShow = ref(false); //显示技巧讲解
-//技巧讲解end
-
-//官方解释
-const officialShow = ref(false); //显示官方解释
-//官方解释end
+const {
+  currentSubject,
+  currentSubjectIndex,
+  subjectTotal,
+  nextSubject,
+  lastSubject,
+  trueNum,
+  falseNum,
+  isJumpNext,
+  userAnswerChange,
+} = useSubjectShowLogic();
 
 //设置操作栏
 const setShow = ref(false); //显示设置栏
-const isJump = ref(false); //答对跳转下一题
 const isSoundEffect = ref(true); //答题音效
 //设置操作栏end
 
 //音频模块
-const audio = ref();
-
-/**
- * 读题
- */
-const issueAudioPlay = () => {
-  audio.value.src = "https://t1-1305573081.file.myqcloud.com/issue/issue1.mp3";
-  audio.value.play();
-};
-
-/**
- * 读答案
- */
-const answerAudioPlay = () => {
-  audio.value.src =
-    "https://t1-1305573081.file.myqcloud.com/answer/answer1.mp3";
-  audio.value.play();
-};
-
-/**
- * 读官方解释
- */
-const explainAudioPlay = () => {
-  audio.value.src =
-    "https://t1-1305573081.file.myqcloud.com/explainjs/explainJS1.mp3";
-  audio.value.play();
-};
-
-/**
- * 读技巧解释
- */
-const JQexplainAudioPlay = () => {
-  audio.value.src = "https://t1-1305573081.file.myqcloud.com/mp3/explain1.mp3";
-  audio.value.play();
-};
-
-/**
- * 停止播放
- */
-const audioPause = () => {
-  audio.value.pause();
-};
-
+const { subjectAudioPlay } = useAudioSet(currentSubject);
 //音频模块end
-
-//记录模块
-const trueNum = ref(0); //答对数量
-const falseNum = ref(0); //答错数量
-const idIndex = ref(0); //当前题目标志位
-const total = ref(0); //题目总数量
-onBeforeMount(() => {
-  watch(currentAnswerIndex, () => {
-    idIndex.value = currentAnswerIndex.value;
-  });
-});
-//记录模块end
 </script>
 
 <style lang="scss" scoped>