SelectPreview.vue 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. <!-- eslint-disable vue/no-v-html -->
  2. <template>
  3. <div class="select-preview" :style="[getAreaStyle(), getComponentStyle()]">
  4. <SerialNumberPosition v-if="isEnable(data.property.sn_display_mode)" :property="data.property" />
  5. <div class="main">
  6. <ul
  7. class="option-list"
  8. :style="{ flexDirection: data.property.arrange_type === arrangeTypeList[0].value ? 'row' : 'column' }"
  9. >
  10. <li
  11. v-for="({ content, mark, multilingual, paragraph_list, rich_text_list }, i) in data.option_list"
  12. :key="mark"
  13. :style="{ cursor: disabled ? 'not-allowed' : 'pointer', borderColor: data.unified_attrib?.topic_color }"
  14. :class="['option-item', { active: isAnswer(mark) }, ...computedAnswerClass(mark)]"
  15. @click="selectAnswer(mark)"
  16. >
  17. <span :class="[isSingle ? 'radio' : 'checkbox']">
  18. <SvgIcon icon-class="check-mark" width="10" height="7" />
  19. </span>
  20. <span
  21. v-if="!('enable_serial' in data.property) || isEnable(data.property.enable_serial)"
  22. class="serial-number"
  23. >
  24. {{ computeOptionNumber(i, data.property.option_serial_type) }}
  25. </span>
  26. <PinyinText
  27. v-if="isEnable(data.property.view_pinyin)"
  28. class="content"
  29. :paragraph-list="paragraph_list"
  30. :rich-text-list="rich_text_list"
  31. :pinyin-position="data.property.pinyin_position"
  32. :body-styles="getBodyStyles()"
  33. :is-preview="true"
  34. />
  35. <span
  36. v-else
  37. class="content rich-text"
  38. :style="{ fontSize: type === typeList[0] ? '12pt' : '', ...getBodyStyles() }"
  39. v-html="convertText(sanitizeHTML(content))"
  40. ></span>
  41. <div v-if="showLang" class="lang">
  42. {{ multilingual.find((item) => item.type === getLang())?.translation }}
  43. </div>
  44. </li>
  45. </ul>
  46. <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @judgeCorrect="judgeCorrect" @retry="retry" />
  47. </div>
  48. <AnswerCorrect
  49. :answer-correct="data?.answer_correct"
  50. :visible.sync="visibleAnswerCorrect"
  51. @closeAnswerCorrect="closeAnswerCorrect"
  52. />
  53. <AnswerAnalysis
  54. :visible.sync="visibleAnswerAnalysis"
  55. :answer-list="data.answer_list"
  56. :analysis-list="data.analysis_list"
  57. :is-check-correct="isCheckCorrect"
  58. @closeAnswerAnalysis="closeAnswerAnalysis"
  59. >
  60. <ul
  61. slot="right-answer"
  62. class="option-list"
  63. :style="{ flexDirection: data.property.arrange_type === arrangeTypeList[0].value ? 'row' : 'column' }"
  64. >
  65. <li
  66. v-for="({ content, mark, multilingual, paragraph_list, rich_text_list }, i) in data.option_list"
  67. :key="mark"
  68. :style="{ cursor: disabled ? 'not-allowed' : 'pointer', borderColor: data.unified_attrib?.topic_color }"
  69. :class="['option-item', ...computedAnswerClass(mark, 'answer-analysis')]"
  70. @click="selectAnswer(mark)"
  71. >
  72. <span :class="[isSingle ? 'radio' : 'checkbox']">
  73. <SvgIcon icon-class="check-mark" width="10" height="7" />
  74. </span>
  75. <span
  76. v-if="!('enable_serial' in data.property) || isEnable(data.property.enable_serial)"
  77. class="serial-number"
  78. >
  79. {{ computeOptionNumber(i, data.property.option_serial_type) }}
  80. </span>
  81. <PinyinText
  82. v-if="isEnable(data.property.view_pinyin)"
  83. class="content"
  84. :paragraph-list="paragraph_list"
  85. :rich-text-list="rich_text_list"
  86. :pinyin-position="data.property.pinyin_position"
  87. :body-styles="getBodyStyles()"
  88. :is-preview="true"
  89. />
  90. <span
  91. v-else
  92. class="content rich-text"
  93. :style="{ fontSize: type === typeList[0] ? '12pt' : '', ...getBodyStyles() }"
  94. v-html="convertText(sanitizeHTML(content))"
  95. ></span>
  96. <div v-if="showLang" class="lang">
  97. {{ multilingual.find((item) => item.type === getLang())?.translation }}
  98. </div>
  99. </li>
  100. </ul>
  101. </AnswerAnalysis>
  102. </div>
  103. </template>
  104. <script>
  105. import PreviewMixin from '../common/PreviewMixin';
  106. import { getSelectData, arrangeTypeList, selectTypeList } from '@/views/book/courseware/data/select';
  107. import { computeOptionNumber } from '@/views/book/courseware/data/common';
  108. export default {
  109. name: 'SelectPreview',
  110. mixins: [PreviewMixin],
  111. data() {
  112. return {
  113. data: getSelectData(),
  114. arrangeTypeList,
  115. computeOptionNumber,
  116. };
  117. },
  118. computed: {
  119. isSingle() {
  120. return this.data.property?.select_type === selectTypeList[0].value;
  121. },
  122. },
  123. watch: {
  124. 'answer.answer_list': {
  125. handler(val) {
  126. if (val.length === 0) {
  127. this.answer.is_right = false;
  128. return;
  129. }
  130. if (val.length !== this.data.answer.answer_list.length) {
  131. this.answer.is_right = false;
  132. return;
  133. }
  134. this.answer.is_right = val.every((item) => this.data.answer.answer_list.includes(item));
  135. },
  136. deep: true,
  137. },
  138. },
  139. methods: {
  140. /**
  141. * 判断选项是否被选中
  142. * @param {string} mark - 选项的标识
  143. */
  144. isAnswer(mark) {
  145. return this.answer.answer_list.includes(mark);
  146. },
  147. selectAnswer(mark) {
  148. if (this.disabled) return;
  149. const list = this.answer.answer_list || [];
  150. const isSelected = list.includes(mark);
  151. if (this.isSingle) {
  152. this.answer.answer_list = isSelected ? [] : [mark];
  153. return;
  154. }
  155. this.answer.answer_list = isSelected ? list.filter((item) => item !== mark) : list.concat(mark);
  156. },
  157. /**
  158. * 计算选项的类名
  159. * @param {string} mark - 选项的标识
  160. * @param {string} type - 计算类名的类型,默认为 'default',可选值为 'answer-analysis'
  161. * @returns {Array} - 选项的类名数组
  162. */
  163. computedAnswerClass(mark, type = 'default') {
  164. if (!this.isJudgingRightWrong && !this.isShowRightAnswer) {
  165. return [];
  166. }
  167. let isHas = this.answer.answer_list.includes(mark); // 用户是否已选中
  168. let isRight = this.data.answer.answer_list.includes(mark); // 是否是正确答案
  169. let answerClass = [];
  170. if (this.isShowRightAnswer && type === 'answer-analysis') {
  171. answerClass = isRight ? ['answer-right'] : [];
  172. }
  173. // 判断是否是正确答案
  174. if (isHas && this.isJudgingRightWrong && type !== 'answer-analysis') {
  175. answerClass = isRight ? ['answer-right'] : ['wrong'];
  176. }
  177. return answerClass;
  178. },
  179. retry() {
  180. this.isJudgingRightWrong = false;
  181. this.isShowRightAnswer = false;
  182. this.answer.answer_list = [];
  183. },
  184. /**
  185. * 获取无文本内容的数据结构,用于保存为个人模板时的样式模板
  186. */
  187. getNoTextContentData() {
  188. let noTextContentData = JSON.parse(JSON.stringify(this.data));
  189. const resetFieldMap = {
  190. analysis_list: [],
  191. answer_list: [],
  192. };
  193. Object.assign(noTextContentData, resetFieldMap);
  194. noTextContentData.option_list.forEach((item) => {
  195. item.content = '';
  196. item.multilingual = [];
  197. item.paragraph_list = [];
  198. item.paragraph_list_parameter = {
  199. text: '',
  200. pinyin_proofread_word_list: [],
  201. };
  202. });
  203. if (noTextContentData.answer) {
  204. noTextContentData.answer.answer_list = [];
  205. noTextContentData.answer.reference_answer = '';
  206. }
  207. return noTextContentData;
  208. },
  209. },
  210. };
  211. </script>
  212. <style lang="scss" scoped>
  213. @use '@/styles/mixin.scss' as *;
  214. .select-preview {
  215. @include preview-base;
  216. .option-list {
  217. display: flex;
  218. flex-flow: column wrap;
  219. gap: 16px;
  220. .option-item {
  221. display: flex;
  222. flex: 1;
  223. flex-wrap: wrap;
  224. column-gap: 8px;
  225. align-items: center;
  226. padding: 12px 24px;
  227. color: #706f78;
  228. cursor: pointer;
  229. background-color: $content-color;
  230. border: 1px solid $content-color;
  231. border-radius: 4px;
  232. .lang {
  233. // width: 100%;
  234. padding-left: 52px;
  235. }
  236. .radio,
  237. .checkbox {
  238. display: flex;
  239. align-items: center;
  240. justify-content: center;
  241. width: 14px;
  242. height: 14px;
  243. margin-right: 8px;
  244. cursor: pointer;
  245. background-color: #fff;
  246. border: 1px solid #dcdfe6;
  247. border-radius: 2px;
  248. .svg-icon {
  249. display: none;
  250. }
  251. }
  252. .radio {
  253. border-radius: 50%;
  254. }
  255. .serial-number {
  256. font-size: 16pt;
  257. }
  258. .content {
  259. flex: 1;
  260. }
  261. &.active {
  262. color: #34343a;
  263. background-color: $main-active-color;
  264. .checkbox {
  265. color: #fff;
  266. background-color: $light-main-color;
  267. .svg-icon {
  268. display: block;
  269. }
  270. }
  271. .radio {
  272. background-color: #fff;
  273. box-shadow: inset 0 0 0 4px $light-main-color;
  274. }
  275. }
  276. &.answer-right {
  277. background-color: #e8f7f2;
  278. &::after {
  279. font-size: 14px;
  280. color: $right-color;
  281. white-space: nowrap;
  282. content: '正确';
  283. }
  284. }
  285. &.wrong {
  286. margin: 1px 0;
  287. background-color: #ffd4d9;
  288. &::after {
  289. font-size: 14px;
  290. color: #ff455e;
  291. white-space: nowrap;
  292. content: '错误';
  293. }
  294. }
  295. }
  296. }
  297. }
  298. </style>