question-item copy.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. <template>
  2. <view class="question-item" :id="`qs_${question.id}`">
  3. <view class="is-main-question">
  4. <view v-if="question.typeId && !isSubQuestion" class="question-type">
  5. {{ questionTypeDesc[question.typeId as EnumQuestionType] }}
  6. </view>
  7. <view class="question-content" :class="{ 'mt-30': isSubQuestion }">
  8. <text v-if="question.isSubQuestion" class="text-nowrap text-30">{{ getQuestionTitle() }} &nbsp;</text>
  9. <uv-parse :content="question.title" containerStyle="display:inline"
  10. contentStyle="word-break:break-word;"></uv-parse>
  11. </view>
  12. <view class="question-options">
  13. <view class="question-option" v-for="option in question.options" :class="getStyleClass(option)" :key="option.id"
  14. @click="handleSelect(option)">
  15. <template v-if="!readonly">
  16. <view v-if="!isOnlySubjective" class="question-option-index">{{ option.no }}</view>
  17. <view v-else>
  18. <uv-icon name="info-circle" :color="isSelected(option) ? '#31A0FC' : '#999'" size="18" />
  19. </view>
  20. </template>
  21. <view v-else>
  22. <uv-icon v-if="isOptionCorrect(question, option)" name="checkmark-circle-fill" color="#2CC6A0" size="22" />
  23. <uv-icon v-else-if="isOptionIncorrect(option)" name="close-circle-fill" color="#FF5B5C" size="22" />
  24. <view v-else class="question-option-index">{{ option.no }}</view>
  25. </view>
  26. <view class="question-option-content">
  27. <uv-parse :content="getOptionContent(option)" containerStyle="display:inline"
  28. contentStyle="word-break:break-word;"></uv-parse>
  29. </view>
  30. </view>
  31. <view v-if="question.options.length && !readonly && !isOnlySubjective" class="question-option"
  32. :class="{ 'question-option-not-know': question.isNotKnow }" @click="handleNotKnow">
  33. <view class="question-option-index">
  34. <uv-icon name="info-circle" :color="question.isNotKnow ? '#31A0FC' : '#999'" size="18" />
  35. </view>
  36. <view class="question-option-content text-fore-light">不会</view>
  37. </view>
  38. <view v-if="!readonly && isOnlySubjective" class="mt-40 bg-[#EBF9FF] p-12 rounded-8">
  39. <view class="rounded-8 bg-white px-10 py-20 text-primary text-24 flex gap-x-6 items-center">
  40. <uv-icon name="info-circle" color="#31A0FC" size="16" />
  41. <text>请线下答题,查看解析对比后,选“会”或“不会”</text>
  42. </view>
  43. <view class="mt-30 mb-20 text-24 text-white bg-primary w-fit mx-auto px-20 py-12 rounded-full text-center"
  44. @click="handleShowParse">
  45. 查看解析</view>
  46. </view>
  47. </view>
  48. <!-- 阅卷模式下显示答案 -->
  49. <template v-if="readonly">
  50. <view v-if="question.subQuestions.length === 0" class="answer-wrap mt-40 rounded-8 pt-60 pb-40 flex items-center text-center relative">
  51. <ie-image v-if="question.isCorrect" src="/pagesStudy/static/image/icon-answer-correct.png"
  52. class="absolute top-0 left-1/2 -translate-x-1/2 w-222 h-64" />
  53. <ie-image v-else src="/pagesStudy/static/image/icon-answer-incorrect.png"
  54. class="absolute top-0 left-1/2 -translate-x-1/2 w-240 h-64" />
  55. <view v-if="!isOnlySubjective" class="flex-1">
  56. <view class="text-34 text-[#2CC6A0] font-bold">{{ question.answer1 }}</view>
  57. <view class="mt-4 text-26">正确答案</view>
  58. </view>
  59. <view class="h-40 w-1 bg-back"></view>
  60. <view v-if="!isOnlySubjective" class="flex-1">
  61. <view class="text-34 font-bold" :class="[question.isCorrect ? 'text-[#2CC6A0]' : 'text-[#FF5B5C]']">
  62. {{ question.answers.join('') || (question.isNotKnow ? '不会' : '未作答') }}
  63. </view>
  64. <view class="mt-4 text-26">我的答案</view>
  65. </view>
  66. <view v-if="isOnlySubjective" class="text-left mt-10 px-20">
  67. <uv-parse :content="'参考答案:' + question.answer2"></uv-parse>
  68. </view>
  69. </view>
  70. </template>
  71. <view v-if="(readonly && question.parse) || (!readonly && question.showParse)" class="mt-40">
  72. <view class="text-30 text-fore-title font-bold">解析</view>
  73. <view class="mt-10 text-26 text-fore-light">
  74. <uv-parse :content="question.parse || '暂无解析'"></uv-parse>
  75. </view>
  76. </view>
  77. </view>
  78. <view v-if="question.subQuestions.length" class="is-sub-question">
  79. <scroll-view class="w-full h-fit sticky top-0 bg-white py-10 z-1" scroll-x>
  80. <view class="flex items-center px-20 gap-x-20">
  81. <view class="px-40 py-8 rounded-full"
  82. :class="[subIndex === subQuestionIndex ? 'bg-[#EBF9FF] text-primary font-bold' : 'bg-back']"
  83. v-for="(subQuestion, subIndex) in question.subQuestions" @click="changeSubQuestion(subIndex)">
  84. {{ question.index + question.offset + subIndex + 1 }}
  85. </view>
  86. </view>
  87. </scroll-view>
  88. <view v-if="subQuestion">
  89. <question-item :question="subQuestion" :parent-question="question" :readonly="readonly" :is-sub-question="true"
  90. :index="index" :total="question.subQuestions.length" @select="handleSelectOption"
  91. @notKnow="handleSelectNotKnow" />
  92. </view>
  93. </view>
  94. </view>
  95. </template>
  96. <script lang="ts" setup>
  97. import { Study } from '@/types';
  98. import { useExam } from '@/composables/useExam';
  99. import { EnumQuestionType } from '@/common/enum';
  100. import { NEXT_QUESTION, PREV_QUESTION, NEXT_QUESTION_QUICKLY, PREV_QUESTION_QUICKLY, SHOW_SUBMIT_CONFIRM, IS_ALL_DONE } from '@/types/injectionSymbols';
  101. const { questionTypeDesc, isOptionCorrect } = useExam();
  102. const props = defineProps<{
  103. question: Study.Question;
  104. parentQuestion?: Study.Question;
  105. readonly?: boolean;
  106. isSubQuestion?: boolean;
  107. index: number;
  108. total?: number;
  109. subQuestionIndex?: number;
  110. }>();
  111. const nextQuestion = inject(NEXT_QUESTION);
  112. const prevQuestion = inject(PREV_QUESTION);
  113. const nextQuestionQuickly = inject(NEXT_QUESTION_QUICKLY);
  114. const prevQuestionQuickly = inject(PREV_QUESTION_QUICKLY);
  115. const isAllDone = inject(IS_ALL_DONE);
  116. const subQuestion = computed(() => {
  117. return props.question.subQuestions[props.subQuestionIndex ?? 0];
  118. });
  119. const emit = defineEmits<{
  120. (e: 'update:question', question: Study.Question): void;
  121. (e: 'select', question: Study.Question): void;
  122. (e: 'notKnow', question: Study.Question): void;
  123. (e: 'scrollTo', selector: string): void;
  124. (e: 'changeSubQuestion', index: number): void;
  125. (e: 'selectSubQuestion', index: number): void;
  126. (e: 'changeQuestion', question: Study.Question): void;
  127. }>();
  128. const isOnlySubjective = computed(() => {
  129. // 除了单选、判断、多选题,其他题型都是主观题
  130. return ![EnumQuestionType.SINGLE_CHOICE, EnumQuestionType.JUDGMENT, EnumQuestionType.MULTIPLE_CHOICE].includes(props.question.typeId);
  131. });
  132. const getStyleClass = (option: Study.QuestionOption) => {
  133. if (!props.readonly) {
  134. return isSelected(option) ? 'question-option-selected' : '';
  135. }
  136. let customClass = '';
  137. let { answers, answer1 } = props.question;
  138. answers = answers?.filter(item => item !== ' ') || [];
  139. answer1 = answer1 || ''
  140. if ([EnumQuestionType.SINGLE_CHOICE, EnumQuestionType.JUDGMENT].includes(props.question.typeId)) {
  141. if (answer1.includes(option.no)) {
  142. customClass = 'question-option-correct';
  143. } else if (answers.includes(option.no)) {
  144. customClass = 'question-option-incorrect';
  145. }
  146. } else if ([EnumQuestionType.MULTIPLE_CHOICE].includes(props.question.typeId)) {
  147. // 我选择的答案
  148. if (answers.includes(option.no)) {
  149. if (answer1.includes(option.no)) {
  150. customClass = 'question-option-correct';
  151. } else {
  152. customClass = 'question-option-incorrect';
  153. }
  154. } else {
  155. // 漏选的答案
  156. if (answer1.includes(option.no)) {
  157. customClass = 'question-option-miss';
  158. }
  159. }
  160. }
  161. // console.log(props.question, option)
  162. return customClass;
  163. };
  164. const isOptionIncorrect = (option: Study.QuestionOption) => {
  165. const { answers, answer1 } = props.question;
  166. return answers.includes(option.no) && !answer1.includes(option.no);
  167. }
  168. const getQuestionTitle = () => {
  169. if (props.isSubQuestion) {
  170. const prefix = questionTypeDesc[props.question.typeId as EnumQuestionType].slice(0, 2);
  171. return `[${prefix}]`;
  172. }
  173. return '';
  174. };
  175. const getOptionContent = (option: Study.QuestionOption) => {
  176. // sb 问题,浪费几个小时
  177. return option.name.replace(/\s/g, ' ');
  178. }
  179. const handleShowParse = () => {
  180. props.question.showParse = !props.question.showParse;
  181. }
  182. const handleNotKnow = () => {
  183. props.question.answers = [];
  184. props.question.isNotKnow = !props.question.isNotKnow;
  185. checkIsDone();
  186. if (props.isSubQuestion) {
  187. emit('select', props.question);
  188. } else {
  189. changeQuestion();
  190. }
  191. }
  192. const handleSelect = (option: Study.QuestionOption) => {
  193. if (props.readonly) {
  194. return;
  195. }
  196. if ([
  197. EnumQuestionType.JUDGMENT,
  198. EnumQuestionType.SINGLE_CHOICE,
  199. EnumQuestionType.SUBJECTIVE,
  200. EnumQuestionType.SHORT_ANSWER,
  201. EnumQuestionType.ESSAY,
  202. EnumQuestionType.ANALYSIS
  203. ].includes(props.question.typeId)) {
  204. props.question.answers = [option.no];
  205. // nextQuestion?.();
  206. } else if (props.question.typeId === EnumQuestionType.MULTIPLE_CHOICE) {
  207. if (props.question.answers.includes(option.no)) {
  208. props.question.answers = props.question.answers.filter(item => item !== option.no);
  209. } else {
  210. props.question.answers.push(option.no);
  211. }
  212. }
  213. props.question.isNotKnow = false;
  214. checkIsDone(props.question);
  215. if (props.question.isSubQuestion) {
  216. // 同时检查父题是否已完成
  217. checkIsDone(props.parentQuestion);
  218. }
  219. if (props.isSubQuestion) {
  220. emit('select', props.question);
  221. } else {
  222. changeQuestion()
  223. }
  224. }
  225. const checkIsDone = (question?: Study.Question) => {
  226. if (!question) {
  227. return;
  228. }
  229. if (question?.subQuestions && question?.subQuestions.length > 0) {
  230. question.isDone = question.subQuestions.every(q => q.answers.length > 0 || q.isNotKnow);
  231. } else {
  232. question.isDone = question.answers.length > 0 || question.isNotKnow;
  233. }
  234. }
  235. // 子题选中方法
  236. const handleSelectOption = () => {
  237. findNotDoneSubQuestion();
  238. }
  239. const handleSelectNotKnow = () => {
  240. findNotDoneSubQuestion();
  241. }
  242. // 查找是否有子题没有做,有的话就自动滚动到目标处
  243. const findNotDoneSubQuestion = () => {
  244. console.log(props)
  245. if (props.question.subQuestions.length - 1 === props.subQuestionIndex) {
  246. changeQuestion();
  247. } else {
  248. changeSubQuestion((props.subQuestionIndex ?? 0) + 1);
  249. }
  250. // const notDoneSubQuestion = props.question.subQuestions.find(q => !q.isDone);
  251. // if (notDoneSubQuestion) {
  252. // if (notDoneSubQuestion.subIndex !== undefined) {
  253. // changeSubQuestion(notDoneSubQuestion.subIndex);
  254. // }
  255. // } else {
  256. // // 是否当前是最后一个子题
  257. // if (props.question.subQuestions.length - 1 === props.subQuestionIndex) {
  258. // changeQuestion();
  259. // } else {
  260. // changeSubQuestion(props.subQuestionIndex ?? 0 + 1);
  261. // }
  262. // }
  263. }
  264. const changeSubQuestion = (index: number) => {
  265. emit('changeSubQuestion', index);
  266. }
  267. const changeQuestion = () => {
  268. emit('changeQuestion', props.question);
  269. }
  270. const isSelected = (option: Study.QuestionOption) => {
  271. const { typeId, answers } = props.question;
  272. if ([
  273. EnumQuestionType.JUDGMENT,
  274. EnumQuestionType.SINGLE_CHOICE,
  275. EnumQuestionType.SUBJECTIVE,
  276. EnumQuestionType.SHORT_ANSWER,
  277. EnumQuestionType.ESSAY,
  278. EnumQuestionType.ANALYSIS
  279. ].includes(typeId)) {
  280. return answers.includes(option.no);
  281. } else if (typeId === EnumQuestionType.MULTIPLE_CHOICE) {
  282. return answers.includes(option.no);
  283. }
  284. return false;
  285. }
  286. </script>
  287. <style lang="scss" scoped>
  288. .answer-wrap {
  289. box-shadow: 0 0 10px 0px rgba(0, 0, 0, 0.06);
  290. }
  291. .prefix {
  292. @apply relative pl-20 before:content-[''] before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:w-[3px] before:h-[15px] before:bg-primary before:rounded-full;
  293. }
  294. .is-sub-question {
  295. @apply px-0;
  296. .question-options {
  297. @apply mt-0;
  298. }
  299. }
  300. .is-main-question {
  301. @apply px-40;
  302. >.question-options {
  303. @apply mt-40;
  304. }
  305. }
  306. .question-item {
  307. .question-type {
  308. @apply mb-20 text-32 text-fore-subtitle font-bold;
  309. }
  310. .sub-question-type {
  311. @apply text-28 text-fore-light font-bold;
  312. }
  313. .question-content {
  314. @apply text-32 text-fore-title break-words;
  315. }
  316. .question-options {
  317. .question-option {
  318. @apply flex items-center px-30 py-24 bg-back rounded-8 border border-none border-transparent;
  319. .question-option-index {
  320. @apply w-40 h-40 rounded-full bg-transparent text-30 text-fore-light font-bold flex items-center justify-center flex-shrink-0;
  321. }
  322. .question-option-content {
  323. @apply text-28 text-fore-title ml-20 flex-1 min-w-0;
  324. }
  325. }
  326. .question-option-selected {
  327. @apply bg-[#b5eaff8e];
  328. .question-option-index {
  329. @apply bg-primary text-white;
  330. }
  331. .question-option-content {
  332. @apply text-primary;
  333. }
  334. }
  335. .question-option-not-know {
  336. @apply bg-[#b5eaff8e];
  337. .question-option-content {
  338. @apply text-primary;
  339. }
  340. }
  341. .question-option-correct,
  342. .question-option-miss {
  343. @apply bg-[#E7FCF8] border-[#E7FCF8] text-[#2CC6A0];
  344. .question-option-index {
  345. @apply text-[#2CC6A0];
  346. }
  347. .question-option-content {
  348. @apply text-[#2CC6A0];
  349. }
  350. }
  351. .question-option-miss {
  352. @apply relative overflow-hidden;
  353. &::before {
  354. content: '';
  355. position: absolute;
  356. right: -56rpx;
  357. top: 15rpx;
  358. width: 180rpx;
  359. height: 36rpx;
  360. background: rgba(255, 91, 92, 0.2);
  361. transform: rotate(30deg);
  362. box-shadow: 0 2rpx 4rpx rgba(255, 91, 92, 0.1);
  363. }
  364. &::after {
  365. content: '漏选';
  366. position: absolute;
  367. right: -8rpx;
  368. top: 14rpx;
  369. width: 100rpx;
  370. height: 32rpx;
  371. color: #FF5B5C;
  372. font-size: 20rpx;
  373. // font-weight: bold;
  374. transform: rotate(30deg);
  375. display: flex;
  376. align-items: center;
  377. justify-content: center;
  378. line-height: 1;
  379. }
  380. }
  381. .question-option-incorrect {
  382. @apply bg-[#FEEDE9] border-[#FEEDE9] text-[#FF5B5C];
  383. .question-option-index {
  384. @apply text-[#FF5B5C];
  385. }
  386. .question-option-content {
  387. @apply text-[#FF5B5C];
  388. }
  389. }
  390. .question-option+.question-option {
  391. @apply mt-24;
  392. }
  393. }
  394. }
  395. </style>