Topic.vue 8.5 KB

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