Topic.vue 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. view
  2. <template>
  3. <Container
  4. :scrollX="true"
  5. :scrollY="false"
  6. :scroll-into-view="`item-${nowIndex}`"
  7. :title="title"
  8. @onSafeAreaChange="onSafeAreaChange"
  9. >
  10. <template v-for="(item, parindex) in topics.ques" :key="parindex">
  11. <view
  12. :id="`item-${parindex}`"
  13. v-if="parindex === nowIndex"
  14. class="topic-item"
  15. :style="{
  16. width: `${safeArea.width}px`,
  17. height: `${safeArea.height}px`,
  18. flexShrink: 0, // 解决宽度无效问题
  19. }"
  20. >
  21. <!-- 头部 -->
  22. <view class="topic-header">
  23. <view class="topic-header-left">
  24. <view class="topic-type">
  25. {{
  26. item.mode || (item.anslist?.length > 1 ? "多选题" : "单选题")
  27. }}
  28. </view>
  29. <view class="topic-count">
  30. 第{{ parindex + 1 }}题/共{{ topics.ques.length }}题
  31. </view>
  32. </view>
  33. <view class="star-icon" @tap="handleStar(item)">
  34. <text
  35. class="iconfont"
  36. :class="starIcon === 'star' ? 'icon-star' : 'icon-star-filled'"
  37. ></text>
  38. </view>
  39. </view>
  40. <!-- 问题内容 -->
  41. <view class="topic-content">
  42. <view class="question-text">{{ item.title }}</view>
  43. <questions
  44. v-for="(question, index) in item.questions"
  45. :key="index"
  46. :answer-list="
  47. Array.isArray(item.anslist) ? item.anslist : [item.anslist]
  48. "
  49. :index="index"
  50. :select-count="selectIndexList[parindex] || []"
  51. :question="question"
  52. :parindex="parindex"
  53. :mode="mode"
  54. @select="handleSelect"
  55. @show-answer="(index) => setShowAnswer(index, parindex)"
  56. />
  57. </view>
  58. <!-- 答案展示 -->
  59. <view
  60. v-if="showAnswer[parindex] && mode === 'practice'"
  61. class="answer-section"
  62. >
  63. <view class="answer-content">
  64. <view class="answer-row">
  65. <view class="answer-item border-r-primary">
  66. 正确答案:
  67. <text class="answer-text">{{ getRightAnswer(parindex) }}</text>
  68. </view>
  69. <view class="answer-item">
  70. 我的答案:
  71. <text class="answer-text">{{ getMyAnswer(parindex) }}</text>
  72. </view>
  73. </view>
  74. </view>
  75. </view>
  76. <!-- 底部按钮 -->
  77. <view class="button-group">
  78. <button
  79. v-if="parindex >= 1 && parindex < topics.ques.length - 1"
  80. class="prev-btn"
  81. @tap="handlePrevPage(item, parindex)"
  82. >
  83. 上一题
  84. </button>
  85. <button
  86. v-if="parindex < topics.ques.length - 1"
  87. class="next-btn"
  88. @tap="handleNextPage(item, parindex)"
  89. >
  90. 下一题
  91. </button>
  92. </view>
  93. </view>
  94. </template>
  95. </Container>
  96. </template>
  97. <script setup>
  98. import { ref, onMounted } from "vue";
  99. import Questions from "./Questions.vue";
  100. import Container from "../Container/Container.vue";
  101. const TopicMapList = ["A", "B", "C", "D", "E"];
  102. const safeArea = ref({});
  103. // Props 定义
  104. const props = defineProps({
  105. topics: {
  106. type: Object,
  107. default: () => ({}),
  108. },
  109. onStar: {
  110. type: Function,
  111. default: null,
  112. },
  113. type: {
  114. type: String,
  115. default: "radio",
  116. },
  117. mode: {
  118. type: String,
  119. default: "practice", // practice: 练习模式, exam: 考试模式
  120. },
  121. title: String
  122. });
  123. // Emits 定义
  124. const emit = defineEmits(["prevPage", "nextPage", "answerChange"]);
  125. // 响应式数据
  126. const nowIndex = ref(0);
  127. const starIcon = ref("star");
  128. const showAnswer = ref([]);
  129. const selectIndexList = ref([]);
  130. // 生命周期钩子
  131. onMounted(() => {
  132. const systemInfo = uni.getSystemInfoSync();
  133. selectIndexList.value = Array(props.topics.ques.length)
  134. .fill()
  135. .map(() => []);
  136. showAnswer.value = Array(props.topics.ques.length).fill(false);
  137. });
  138. // 方法
  139. const handleStar = (item) => {
  140. if (!props.onStar) return;
  141. props.onStar(item).then((res) => {
  142. if (!res) return;
  143. starIcon.value = starIcon.value === "star" ? "star-2" : "star";
  144. });
  145. };
  146. const handleSelect = (value, parindex) => {
  147. const arr = selectIndexList.value[parindex];
  148. const currentTopic = props.topics.ques[parindex];
  149. const isSingleChoice =
  150. !Array.isArray(currentTopic.anslist) || currentTopic.anslist.length === 1;
  151. // 如果点击已选中的选项,则取消选择
  152. if (arr.includes(value)) {
  153. selectIndexList.value[parindex] = arr.filter((item) => item !== value);
  154. emit("answerChange", {
  155. questionIndex: parindex,
  156. answers: selectIndexList.value[parindex],
  157. isSingleChoice,
  158. });
  159. return;
  160. }
  161. // 如果是考试模式
  162. if (props.mode === "exam") {
  163. // 如果是单选题,直接替换
  164. if (isSingleChoice) {
  165. selectIndexList.value[parindex] = [value];
  166. } else {
  167. // 多选题直接添加
  168. arr.push(value);
  169. }
  170. emit("answerChange", {
  171. questionIndex: parindex,
  172. answers: selectIndexList.value[parindex],
  173. isSingleChoice,
  174. });
  175. return;
  176. }
  177. // 练习模式逻辑
  178. const max = Array.isArray(currentTopic.anslist)
  179. ? currentTopic.anslist.length
  180. : 1;
  181. // 如果已达到最大选择数,则不允许继续选择
  182. if (arr.length >= max) {
  183. return;
  184. }
  185. // 如果是单选题,直接替换
  186. if (max === 1) {
  187. selectIndexList.value[parindex] = [value];
  188. } else {
  189. // 添加新选择
  190. arr.push(value);
  191. }
  192. // 如果是练习模式,且即将完成所有选择,则显示答案
  193. if (props.mode === "practice" && max - 1 === arr.length) {
  194. emit("showAnswer", true);
  195. }
  196. };
  197. const setShowAnswer = (value, parindex) => {
  198. showAnswer.value[parindex] = value;
  199. };
  200. const getRightAnswer = (index) => {
  201. const topic = props.topics.ques[index];
  202. const answers = Array.isArray(topic.anslist)
  203. ? topic.anslist
  204. : [topic.anslist];
  205. return answers
  206. .map((value) => {
  207. const question = topic.questions.find((q) => q.value === value);
  208. return question ? TopicMapList[topic.questions.indexOf(question)] : "";
  209. })
  210. .join("");
  211. };
  212. const getMyAnswer = (parindex) => {
  213. const topic = props.topics.ques[parindex];
  214. return (selectIndexList.value[parindex] || [])
  215. .map((value) => {
  216. const question = topic.questions.find((q) => q.value === value);
  217. return question ? TopicMapList[topic.questions.indexOf(question)] : "";
  218. })
  219. .join("");
  220. };
  221. const handlePrevPage = (item, index) => {
  222. nowIndex.value = index - 1;
  223. emit("prevPage", item, index);
  224. };
  225. const handleNextPage = (item, index) => {
  226. nowIndex.value = index + 1;
  227. emit("nextPage", item, index);
  228. };
  229. const onSafeAreaChange = (s) => {
  230. safeArea.value = s;
  231. };
  232. </script>
  233. <style lang="scss" scoped>
  234. @import "@/uni.scss";
  235. .topic-container {
  236. width: 100vw;
  237. overflow: hidden;
  238. position: relative;
  239. }
  240. .topic-item {
  241. display: flex;
  242. flex-direction: column;
  243. gap: 12px;
  244. position: relative;
  245. box-sizing: border-box;
  246. }
  247. .topic-header {
  248. display: flex;
  249. align-items: center;
  250. justify-content: space-between;
  251. }
  252. .topic-header-left {
  253. display: flex;
  254. gap: 8px;
  255. align-items: center;
  256. }
  257. .topic-type {
  258. border: 1px solid $uni-primary;
  259. padding: 0 8px;
  260. border-radius: 6px;
  261. color: $uni-primary;
  262. font-weight: 600;
  263. font-size: 14px;
  264. }
  265. .topic-count {
  266. color: #333;
  267. font-size: 14px;
  268. }
  269. .topic-content {
  270. display: flex;
  271. flex-direction: column;
  272. gap: 8px;
  273. }
  274. .question-text {
  275. font-weight: bold;
  276. font-size: 14px;
  277. white-space: normal;
  278. }
  279. .answer-section {
  280. flex: 1;
  281. border-radius: 16rpx;
  282. border: 1px solid #ddd;
  283. padding: 24rpx;
  284. display: flex;
  285. flex-direction: column;
  286. gap: 32rpx;
  287. overflow: scroll;
  288. }
  289. .answer-row {
  290. font-size: 14px;
  291. display: flex;
  292. align-items: center;
  293. gap: 12px;
  294. }
  295. .answer-item {
  296. display: flex;
  297. gap: 8px;
  298. align-items: center;
  299. padding-right: 12px;
  300. }
  301. .border-r-primary {
  302. border-right: 2px solid $uni-primary;
  303. }
  304. .answer-text {
  305. color: $uni-primary;
  306. }
  307. .button-group {
  308. display: flex;
  309. gap: 8px;
  310. position: sticky;
  311. bottom: 0;
  312. margin-top: auto;
  313. }
  314. .prev-btn {
  315. flex: 1;
  316. background-color: $uni-primary-light;
  317. color: $uni-primary;
  318. }
  319. .next-btn {
  320. flex: 1;
  321. background-color: $uni-primary;
  322. color: #fff;
  323. }
  324. </style>