wyling007 %!s(int64=3) %!d(string=hai) anos
pai
achega
c06617af75

+ 1 - 1
.prettierrc.js

@@ -1,6 +1,6 @@
 module.exports = {
 	semi: true, // 使用分号, 默认true
-	singleQuote: true, // 使用单引号, 默认false(在jsx中配置无效, 默认都是双引号)
+	singleQuote: false, // 使用单引号, 默认false(在jsx中配置无效, 默认都是双引号)
 	bracketSpacing: true, // 对象中的空格 默认true
 	tabWidth: 2, // tab缩进大小,默认为2
 	useTabs: true, // 使用tab缩进,默认false

+ 3 - 0
.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+	"typescript.tsdk": "node_modules/typescript/lib"
+}

+ 55 - 28
src/App.vue

@@ -1,37 +1,64 @@
 <template>
-  <router-view></router-view>
+	<router-view v-slot="{ Component }">
+		<component :is="Component" />
+	</router-view>
 </template>
 
 <script lang="ts">
-import { defineComponent } from "vue";
+	import { defineComponent } from 'vue';
 
-export default defineComponent({
-  name: "App",
-});
+	export default defineComponent({
+		name: 'App',
+	});
 </script>
 
 <style lang="scss">
-* {
-  margin: 0;
-  padding: 0;
-  box-sizing: border-box;
-}
-#app::-webkit-scrollbar {
-  width: 0px;
-  height: 0px;
-}
-/* 可以设置不同的进入和离开动画   */
-/* 设置持续时间和动画函数        */
-.slide-left-enter,
-.slide-right-leave-active {
-  opacity: 0;
-  -webkit-transform: translate(30px, 0);
-  transform: translate(30px, 0);
-}
-.slide-left-leave-active,
-.slide-right-enter {
-  opacity: 0;
-  -webkit-transform: translate(-30px, 0);
-  transform: translate(-30px, 0);
-}
+	* {
+		margin: 0;
+		padding: 0;
+		box-sizing: border-box;
+	}
+	#app::-webkit-scrollbar {
+		width: 0px;
+		height: 0px;
+	}
+	/* 可以设置不同的进入和离开动画   */
+	/* 设置持续时间和动画函数        */
+	/* .slide-left-enter,
+	.slide-right-leave-active {
+		opacity: 0;
+		-webkit-transform: translate(30px, 0);
+		transform: translate(30px, 0);
+	}
+	.slide-left-leave-active,
+	.slide-right-enter {
+		opacity: 0;
+		-webkit-transform: translate(-30px, 0);
+		transform: translate(-30px, 0);
+	} */
+
+	.slide-right-enter-active,
+	.slide-right-leave-active,
+	.slide-left-enter-active,
+	.slide-left-leave-active {
+		will-change: transform;
+		transition: all 250ms;
+		position: absolute;
+	}
+	.slide-right-enter {
+		opacity: 0;
+		transform: translate3d(-100%, 0, 0);
+	}
+	.slide-right-leave-active {
+		opacity: 0;
+		transform: translate3d(100%, 0, 0);
+	}
+	.slide-left-enter {
+		opacity: 0;
+		transform: translate3d(100%, 0, 0);
+	}
+	.slide-left-leave-active {
+		opacity: 0;
+		transform: translate3d(-100%, 0, 0);
+	}
 </style>

+ 12 - 14
src/api/request.ts

@@ -1,25 +1,23 @@
-import axios from "axios";
-import store from "@/store";
+import axios from 'axios';
+import store from '@/store';
 
 const request = axios.create({
-  baseURL: import.meta.env.MODE === "development" ? "/dev-api" : "/prod-api",
+	baseURL: import.meta.env.MODE === 'development' ? '/dev-api' : '/prod-api',
 });
 
 request.interceptors.request.use((config) => {
-  // 是否需要设置 token
-  if (config.headers.isToken !== false) {
-    config.headers["Authorization"] = "Bearer " + store.getters.getToken; // 让每个请求携带自定义token 请根据实际情况自行修改
-  }
-  return config;
+	// 是否需要设置 token
+	if (config.headers.isToken !== false) {
+		config.headers['Authorization'] = 'Bearer ' + store.getters.getToken; // 让每个请求携带自定义token 请根据实际情况自行修改
+	}
+	return config;
 });
 
 request.interceptors.response.use((res) => {
-  if (res.data.code === 401) {
-    location.replace(
-      `https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx67ca1b8c9816ef28&redirect_uri=${location.href}&response_type=code&scope=snsapi_userinfo&state=LOGIN#wechat_redirect`
-    );
-  }
-  return res;
+	if (res.data.code === 401) {
+		location.replace(`https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx67ca1b8c9816ef28&redirect_uri=${location.href}&response_type=code&scope=snsapi_userinfo&state=LOGIN#wechat_redirect`);
+	}
+	return res;
 });
 
 export default request;

+ 431 - 0
src/components/m-exercise/index.vue

@@ -0,0 +1,431 @@
+<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" z-index="10">
+		<div class="skills-box">
+			<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" z-index="10">
+		<div class="skills-box">
+			<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 { ref, defineProps } from 'vue';
+	import { useTopicMode, useAudioSet, useSubjectShowLogic } from '@/hooks/exercise';
+	import { RouterBus } from '@/hooks';
+	const {
+		router: { back },
+		route: { query },
+	} = new RouterBus();
+
+	const props = defineProps<{
+		listType: ExerciseType.ListType;
+	}>();
+
+	//答题模式选择逻辑
+	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(props.listType);
+
+	//音频模块
+	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>

+ 3 - 0
src/components/m-exercise/types.d.ts

@@ -0,0 +1,3 @@
+declare namespace ExerciseType {
+	type ListType = 'normal' | 'free' | 'test' | 'wrong';
+}

+ 3 - 6
src/hooks/exercise/index.ts

@@ -4,8 +4,8 @@ export { useAudioSet } from './audio';
 import { useSubjectList } from './list';
 import { useSubjectCheck } from './wrong';
 
-export const useSubjectShowLogic = () => {
-	const { subjectList, subjectTotal, loadNewSubject, currentSubject, currentSubjectIndex } = useSubjectList(); //获取题目列表
+export const useSubjectShowLogic = (type: ExerciseType.ListType) => {
+	const { subjectList, subjectTotal, loadNewSubject, currentSubject, currentSubjectIndex } = useSubjectList(type); //获取题目列表
 
 	/**下一题数据请求锁 */
 	const nextBtnState = ref(true);
@@ -28,10 +28,7 @@ export const useSubjectShowLogic = () => {
 
 	/** 展示上一题  */
 	const lastSubject = () => {
-		currentSubjectIndex.value > 0 ?? currentSubjectIndex.value--;
-		// if (currentSubjectIndex.value > 0) {
-		// 	currentSubjectIndex.value--;
-		// }
+		currentSubjectIndex.value > 0 && currentSubjectIndex.value--;
 	};
 
 	const { trueNum, falseNum, isJumpNext, userAnswerChange, addCurrentQuestion } = useSubjectCheck(currentSubject, nextSubject);

+ 17 - 11
src/hooks/exercise/list.ts

@@ -3,18 +3,21 @@ import { useRoute } from 'vue-router';
 import * as API from '@/api';
 
 /**获取题目列表 */
-export const useSubjectList = () => {
+export const useSubjectList = (type: ExerciseType.ListType) => {
 	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,
-		});
+		const res = await API.getTopicList(
+			{
+				...query,
+				pageNum: pageNum.value,
+				pageSize: pageSize.value,
+			},
+			type !== 'free'
+		);
 		subjectList.value = res.list;
 		subjectTotal.value = res.total;
 	});
@@ -22,11 +25,14 @@ export const useSubjectList = () => {
 	const loadNewSubject = async () => {
 		if (subjectList.value.length == subjectTotal.value) return;
 		pageNum.value++;
-		const res = await API.getTopicList({
-			...query,
-			pageNum: pageNum.value,
-			pageSize: pageSize.value,
-		});
+		const res = await API.getTopicList(
+			{
+				...query,
+				pageNum: pageNum.value,
+				pageSize: pageSize.value,
+			},
+			type !== 'free'
+		);
 		subjectList.value = subjectList.value.concat(res.list);
 	};
 	const currentSubjectIndex = ref(0); //当前题目下标

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

@@ -1,322 +0,0 @@
-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,
-  };
-};

+ 3 - 509
src/views/exercise/index.vue

@@ -1,513 +1,7 @@
 <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 -->
+	<m-exercise listType="normal" />
 </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();
+<script></script>
 
-//答题模式选择逻辑
-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>
+<style lang="scss" scoped></style>

+ 0 - 281
src/views/exerciseFree/hooks.ts

@@ -1,281 +0,0 @@
-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 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,
-      },
-      false
-    );
-    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,
-      },
-      false
-    );
-    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++;
-    } 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 } = useSubjectCheck(
-    currentSubject,
-    nextSubject
-  );
-
-  return {
-    currentSubject,
-    currentSubjectIndex,
-    subjectTotal,
-    nextSubject,
-    lastSubject,
-    trueNum,
-    falseNum,
-    isJumpNext,
-    userAnswerChange,
-  };
-};

+ 3 - 505
src/views/exerciseFree/index.vue

@@ -1,509 +1,7 @@
 <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">
-      <m-icon type="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 -->
+	<m-exercise listType="free" />
 </template>
 
-<script lang="ts" setup>
-import { ref } from "vue";
-import { useTopicMode, useAudioSet, useSubjectShowLogic } from "./hooks";
-import { RouterBus } from "@/hooks";
-const {
-  router: { back },
-  route: { query },
-} = new RouterBus();
+<script></script>
 
-//答题模式选择逻辑
-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,
-} = 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>
+<style lang="scss" scoped></style>

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

@@ -1,322 +0,0 @@
-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,
-  };
-};

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

@@ -1,517 +1,7 @@
 <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 -->
+	<m-exercise listType="free" />
 </template>
 
-<script lang="ts" setup>
-import { computed, ref } from "vue";
-import {
-  useTopicMode,
-  useAudioSet,
-  useSubjectShowLogic,
-} from "@/hooks/exercise";
-import { RouterBus } from "@/hooks";
-const {
-  router: { back },
-  route: { query },
-} = new RouterBus();
+<script></script>
 
-//答题模式选择逻辑
-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>
+<style lang="scss" scoped></style>