Topic.vue 8.2 KB

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