wyling 3 лет назад
Родитель
Сommit
f0220130c4

+ 4 - 0
src/hooks/index.ts

@@ -84,4 +84,8 @@ export class RouterBus {
   goMockTest = () => {
     this.router.push({ path: "/mockTest", query: this.route.query });
   };
+  /** 错题重阅 */
+  goWrongReview = (id: number) => {
+    this.router.push({ path: "/wrongReview", query: this.route.query });
+  };
 }

+ 2 - 2
src/views/cashOut/index.vue

@@ -37,7 +37,7 @@
         <th>头像</th>
         <th>昵称</th>
         <th>代理等级</th>
-        <th>分成比例</th>
+        <!-- <th>分成比例</th> -->
         <th>分成金额</th>
       </tr>
       <tr
@@ -51,7 +51,7 @@
         </td>
         <td>{{ item.nickName }}</td>
         <td>{{ item.hierarchy }}</td>
-        <td>{{ item.percentage }}</td>
+        <!-- <td>{{ item.percentage }}</td> -->
         <td>{{ item.profitPrice }}</td>
       </tr>
     </table>

+ 3 - 0
src/views/collection/components/list.vue

@@ -14,6 +14,7 @@
                 v-for="(item, index) in list"
                 :key="index"
                 :disabled="isChoose"
+                @click="goWrongReview(item.id)"
               >
                 <van-cell
                   class="cell-box"
@@ -179,6 +180,8 @@ const useQuestionList = (type: CollectionAndWrongType.type) => {
 <script setup lang="ts">
 import { ref, watch } from "vue";
 
+const { goWrongReview } = new RouterBus();
+
 interface Props {
   type: "wrong" | "collection";
 }

+ 322 - 0
src/views/wrongReview/hooks.ts

@@ -0,0 +1,322 @@
+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";
+import { CollectionModel } from "@/dataModel/collection";
+import { RouterBus } from "@/hooks";
+import { Notify } from "vant";
+
+//答题模式切换
+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,
+    });
+    subjectList.value = res.list;
+    subjectTotal.value = res.total;
+  });
+  //加载下一页数据
+  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 wrongModel = new CollectionModel("wrong");
+  const collectionModel = new CollectionModel("collection");
+  const {
+    route: { query },
+  } = new RouterBus();
+
+  /** 批量新增收藏 */
+  const addsCullection = async (ids: number[]) => {
+    const questionList = ids.map((id) => {
+      return {
+        carType: query.vehicle as CollectionAndWrongType.CarType,
+        km: query.name as CollectionAndWrongType.Km,
+        questionId: id,
+      };
+    });
+    collectionModel.adds(questionList);
+  };
+
+  /** 收藏当前题目 */
+  const addCurrentQuestion = async () => {
+    const res = await collectionModel.adds([
+      {
+        carType: query.vehicle as CollectionAndWrongType.CarType,
+        km: query.name as CollectionAndWrongType.Km,
+        questionId: currentSubject.value.id,
+      },
+    ]);
+    if (res.data == 1) {
+      currentSubject.value.isCollection = true;
+      Notify({ type: "success", message: "收藏成功" });
+    } else {
+      currentSubject.value.isCollection = true;
+      Notify({ type: "primary", message: res.msg });
+    }
+  };
+  /**
+   * 选择答案后进行校验
+   */
+  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)
+    ) {
+      //答案正确
+      currentSubject.value.isTrue = true;
+      trueNum.value++;
+      if (isJumpNext.value) {
+        nextTick(() => {
+          nextSubject();
+        });
+      }
+    } else {
+      //答案错误
+      wrongModel.adds([
+        {
+          carType: query.vehicle as CollectionAndWrongType.CarType,
+          km: query.name as CollectionAndWrongType.Km,
+          questionId: currentSubject.value.id,
+        },
+      ]);
+      currentSubject.value.isTrue = false;
+      falseNum.value++;
+    }
+  };
+
+  return {
+    trueNum,
+    falseNum,
+    isJumpNext,
+    userAnswerChange,
+    addCurrentQuestion,
+  };
+};
+
+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++;
+    } else {
+      if (nextBtnState.value) {
+        //禁用下一题按钮
+        nextBtnState.value = false;
+        //题目数量不足加载数据
+        await loadNewSubject();
+        //启用按钮
+        nextBtnState.value = true;
+      }
+    }
+  };
+
+  /** 展示上一题  */
+  const lastSubject = () => {
+    if (currentSubjectIndex.value > 0) {
+      currentSubjectIndex.value--;
+    }
+  };
+
+  const {
+    trueNum,
+    falseNum,
+    isJumpNext,
+    userAnswerChange,
+    addCurrentQuestion,
+  } = useSubjectCheck(currentSubject, nextSubject);
+
+  return {
+    currentSubject,
+    currentSubjectIndex,
+    subjectTotal,
+    nextSubject,
+    lastSubject,
+    trueNum,
+    falseNum,
+    isJumpNext,
+    userAnswerChange,
+    addCurrentQuestion,
+  };
+};

+ 513 - 0
src/views/wrongReview/index.vue

@@ -0,0 +1,513 @@
+<template>
+  <!-- 导航栏 -->
+  <van-nav-bar
+    :title="query.vehicle + '>' + query.name"
+    left-arrow
+    @click-left="back"
+    fixed
+    placeholder
+  >
+    <template #right>
+      <m-icon type="shezhi" @click="setShow = true" />
+    </template>
+  </van-nav-bar>
+  <!-- 导航栏end -->
+  <!-- 答题模式选择 -->
+  <div class="answerType">
+    <span
+      v-for="(answerType, index) in answerTypeList"
+      :key="index"
+      :class="{ selected: currentType == index }"
+      @click="currentType = index"
+      >{{ answerType.name }}</span
+    >
+    <span :class="{ selected: aotuPlayFlag }" @click="aotuPlaySet"
+      >自动读题</span
+    >
+  </div>
+  <!-- 答题模式选择end -->
+  <!-- 分割线 -->
+  <div class="divider" />
+  <!-- 题目模块 -->
+  <!-- 题目预加载 -->
+  <m-empty v-if="!currentSubject" />
+  <!-- 题目预加载end -->
+  <div class="problem-box" v-else>
+    <!-- 题目内容 -->
+    <div class="problem">
+      <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="typeParams.answerShow">
+      <div>
+        <div
+          class="answer-box"
+          v-for="(item, index) in currentSubject.opts"
+          :key="Number(index)"
+        >
+          <div class="choose-icon">
+            {{ String.fromCharCode(65 + Number(index)) }}
+          </div>
+          <span
+            class="answer-text"
+            :class="{ true: currentSubject.answer.includes(item) }"
+          >
+            {{ item }}
+          </span>
+        </div>
+      </div>
+      <div class="checkbox-answer">
+        答案: {{ currentSubject.answer.toString() }}
+      </div>
+    </div>
+    <!-- 背题模式展示end -->
+    <!-- 选择内容 -->
+    <div v-else-if="currentSubject.isTrue === null">
+      <!-- 单选 -->
+      <van-radio-group
+        v-model="currentSubject.userAnswer"
+        v-if="currentSubject.type != '多选题'"
+        @change="userAnswerChange"
+        icon-size="35px"
+      >
+        <van-radio
+          v-for="(item, index) in currentSubject.opts"
+          :key="Number(index)"
+          :name="item"
+          class="answer"
+          >{{ item }}
+          <template #icon="props">
+            <div class="choose-icon" :class="{ selected: props.checked }">
+              {{ String.fromCharCode(65 + Number(index)) }}
+            </div>
+          </template>
+        </van-radio>
+      </van-radio-group>
+      <!-- 多选 -->
+      <div v-else>
+        <van-checkbox-group
+          v-model="currentSubject.userAnswer"
+          icon-size="35px"
+        >
+          <van-checkbox
+            v-for="(item, index) in currentSubject.opts"
+            :key="Number(index)"
+            :name="item"
+            class="answer"
+            >{{ item }}
+            <template #icon="props">
+              <div class="choose-icon" :class="{ selected: props.checked }">
+                {{ String.fromCharCode(65 + Number(index)) }}
+              </div>
+            </template>
+          </van-checkbox>
+        </van-checkbox-group>
+        <van-button
+          round
+          type="primary"
+          class="checkbox-btn"
+          :disabled="currentSubject.userAnswer.length == 0"
+          @click="userAnswerChange"
+          >确定</van-button
+        >
+      </div>
+    </div>
+    <!-- 展示答题后选择内容 -->
+    <div v-else>
+      <div>
+        <div
+          v-for="(item, index) in currentSubject.optsBack"
+          :key="Number(index)"
+          class="answer-box"
+        >
+          <div
+            class="choose-icon"
+            :class="{ iconTrue: item.status % 2 !== 0 }"
+            v-if="item.status < 2"
+          >
+            {{ String.fromCharCode(65 + Number(index)) }}
+          </div>
+          <m-icon
+            v-else-if="item.status == 3"
+            type="dui"
+            size="30px"
+            style="margin-left: 5px"
+          />
+          <m-icon
+            v-else-if="item.status == 2"
+            size="30px"
+            type="cuo"
+            style="margin-left: 5px"
+          />
+          <span
+            class="answer-text"
+            :class="{ true: item.status % 2 !== 0, false: item.status == 2 }"
+          >
+            {{ item.opt }}
+          </span>
+        </div>
+      </div>
+      <div class="checkbox-answer">
+        答案: {{ currentSubject.answer.toString() }}
+      </div>
+    </div>
+    <!-- 展示答题后选择内容end -->
+  </div>
+  <!-- 选择内容End -->
+  <!-- 分割线 -->
+  <van-divider />
+  <!-- 功能选择列表 -->
+  <div class="function-list">
+    <div class="function-item" @click="addCurrentQuestion">
+      <m-icon
+        :type="currentSubject?.isCollection ? 'shoucanghuang' : 'shoucanghui'"
+        size="25px"
+      />
+      <span>收藏</span>
+    </div>
+    <div class="function-item" @click="subjectAudioPlay('读题+答案')">
+      <m-icon type="a-dtda" size="25px" />
+      <span>读题+答案</span>
+    </div>
+    <div class="function-item" @click="subjectAudioPlay('读题')">
+      <m-icon type="duti" size="25px" />
+      <span>读题</span>
+    </div>
+    <div class="function-item" @click="skillsShow = true">
+      <m-icon type="jqjj" size="25px" />
+      <span>技巧讲解</span>
+    </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="currentSubject.explainGif" class="img" />
+        <van-divider class="divider">本题速记口诀</van-divider>
+        <div class="text">{{ currentSubject.explainJq }}</div>
+        <div class="btn">
+          <span @click="skillsShow = false">关闭</span>
+          <span @click="subjectAudioPlay('读技巧解释')">语音重播</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">
+          {{ currentSubject.explainJs }}
+        </div>
+        <div class="btn">
+          <span @click="officialShow = false">关闭</span>
+          <span @click="subjectAudioPlay('读官方解释')">语音重播</span>
+        </div>
+      </div>
+    </div>
+  </van-overlay>
+  <!-- 官方解释end -->
+  <!-- 题目模块end -->
+  <!-- 设置操作栏 -->
+  <van-popup v-model:show="setShow" position="bottom">
+    <van-cell center title="答对跳转下一题">
+      <template #right-icon>
+        <van-switch v-model="isJumpNext" size="24" />
+      </template>
+    </van-cell>
+    <van-cell center title="答题音效提示">
+      <template #right-icon>
+        <van-switch v-model="isSoundEffect" size="24" />
+      </template>
+    </van-cell>
+  </van-popup>
+  <!-- 设置操作栏end -->
+  <!-- 底部操作栏 -->
+  <van-tabbar placeholder route>
+    <van-tabbar-item @click="lastSubject"
+      >上一题
+      <template #icon>
+        <m-icon type="shangyiti" />
+      </template>
+    </van-tabbar-item>
+    <van-tabbar-item
+      >{{ trueNum }}
+      <template #icon>
+        <m-icon type="dui" />
+      </template>
+    </van-tabbar-item>
+    <van-tabbar-item
+      >{{ falseNum }}
+      <template #icon>
+        <m-icon type="cuo" />
+      </template>
+    </van-tabbar-item>
+    <van-tabbar-item
+      >{{ currentSubjectIndex + 1 }}/{{ subjectTotal }}
+      <template #icon>
+        <m-icon type="zongtishu" />
+      </template>
+    </van-tabbar-item>
+    <van-tabbar-item @click="officialShow = true"
+      >官方解释
+      <template #icon>
+        <m-icon type="gfjs" />
+      </template>
+    </van-tabbar-item>
+    <van-tabbar-item @click="nextSubject"
+      >下一题
+      <template #icon>
+        <m-icon type="xiayiti" />
+      </template>
+    </van-tabbar-item>
+  </van-tabbar>
+  <!-- 底部操作栏end -->
+</template>
+
+<script lang="ts" setup>
+import { computed, ref } from "vue";
+import { useTopicMode, useAudioSet, useSubjectShowLogic } from "./hooks";
+import { RouterBus } from "@/hooks";
+const {
+  router: { back },
+  route: { query },
+} = new RouterBus();
+
+//答题模式选择逻辑
+const { answerTypeList, currentType, typeParams } = useTopicMode();
+
+const skillsShow = ref(false); //显示技巧讲解
+const officialShow = ref(false); //显示官方解释
+
+//设置操作栏
+const setShow = ref(false); //显示设置栏
+const isSoundEffect = ref(true); //答题音效
+
+//题目展示逻辑
+const {
+  currentSubject,
+  currentSubjectIndex,
+  subjectTotal,
+  nextSubject,
+  lastSubject,
+  trueNum,
+  falseNum,
+  isJumpNext,
+  userAnswerChange,
+  addCurrentQuestion,
+} = useSubjectShowLogic();
+
+//音频模块
+const { aotuPlayFlag, subjectAudioPlay, aotuPlaySet } =
+  useAudioSet(currentSubject);
+</script>
+
+<style lang="scss" scoped>
+.parsing-img {
+  width: 100%;
+  margin-top: 10px;
+}
+.function-list {
+  width: 100%;
+  font-size: 13px;
+  display: flex;
+  justify-content: space-around;
+  padding: 15px;
+  box-sizing: border-box;
+  .function-item {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    font-size: 13px;
+    font-weight: 400;
+    color: #8a9099;
+    span {
+      margin-top: 5px;
+    }
+  }
+}
+.answerType {
+  width: 100%;
+  font-size: 13px;
+  display: flex;
+  justify-content: space-around;
+  padding: 15px;
+  box-sizing: border-box;
+  span {
+    border-radius: 20px;
+    padding: 3px 10px;
+    background-color: #b8c0cc;
+    color: #ffffff;
+  }
+  .selected {
+    background-color: #498ef5;
+  }
+}
+.divider {
+  width: 100%;
+  height: 10px;
+  background-color: #f2f3f5;
+}
+.problem-box {
+  font-size: 17px;
+  padding: 15px;
+  .problem {
+    .type {
+      width: 47px;
+      height: 24px;
+      background: #498ef5;
+      border-radius: 10px 10px 0px 10px;
+      font-size: 11px;
+      padding: 2px 7px;
+      margin-right: 5px;
+    }
+    .text {
+      font-family: PingFang SC;
+      font-weight: 400;
+      color: #0a1a33;
+      letter-spacing: 0.3px;
+    }
+    .img {
+      width: 100%;
+      margin-top: 10px;
+    }
+  }
+  .answer {
+    margin-top: 25px;
+  }
+  .answer-box {
+    display: flex;
+    margin-top: 25px;
+    align-items: center;
+    .iconTrue {
+      background-color: #01c18d;
+    }
+    .answer-text {
+      margin-left: 10px;
+    }
+    .true {
+      color: #01c18d;
+    }
+    .false {
+      color: #ff4d53;
+    }
+  }
+
+  .choose-icon {
+    width: 30px;
+    height: 30px;
+    border-radius: 50%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    font-size: 17px;
+    box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.16);
+    box-sizing: border-box;
+    margin-left: 5px;
+    margin-top: 1px;
+  }
+  .selected {
+    background-color: #498ef5;
+  }
+  .checkbox-btn {
+    width: 266px;
+    height: 40px;
+    margin: auto;
+    margin-top: 25px;
+    left: 50%;
+    transform: translateX(-50%);
+  }
+  .checkbox-answer {
+    padding: 8px 10px;
+    background-color: #f2f3f5;
+    margin-top: 25px;
+  }
+}
+.skills-box {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+  .skills {
+    width: 290px;
+    background: #ffffff;
+    box-shadow: 0px 0px 8px rgba(124, 129, 136, 0.16);
+    border-radius: 10px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding: 20px 16px;
+    box-sizing: border-box;
+    .title {
+      font-size: 15px;
+      font-family: PingFang SC;
+      font-weight: bold;
+      line-height: 21px;
+      color: #0a1a33;
+    }
+    .img {
+      width: 258px;
+      height: 129px;
+      border: 1px solid #e8e8e8;
+      margin-top: 16px;
+    }
+    .divider {
+      margin-top: 20px;
+      color: #0a1a33;
+      background: #ffffff;
+    }
+    .text {
+      font-size: 13px;
+      font-family: PingFang SC;
+      font-weight: 400;
+      line-height: 19px;
+      color: #5c6066;
+      margin-top: 10px;
+    }
+    .btn {
+      width: 100%;
+      display: flex;
+      justify-content: space-between;
+      padding: 0 40px;
+      box-sizing: border-box;
+      margin-top: 20px;
+      span {
+        width: 76px;
+        height: 30px;
+        border-radius: 15px;
+        font-size: 13px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        &:active {
+          background-color: #afaaaa;
+          filter: brightness(50%);
+        }
+        &:nth-of-type(1) {
+          border: 1px solid #707070;
+          color: #5c6066;
+        }
+        &:nth-of-type(2) {
+          background: #498ef5;
+          border: 1px solid #498ef5;
+          color: #ffffff;
+        }
+      }
+    }
+  }
+}
+</style>