FillPreview.vue 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. <!-- eslint-disable vue/no-v-html -->
  2. <template>
  3. <div class="fill-preview">
  4. <div class="stem">
  5. <span class="question-number" :style="{ fontSize: data.property.stem_question_number }">
  6. {{ data.property.question_number }}.
  7. </span>
  8. <span v-html="sanitizeHTML(data.stem)"></span>
  9. </div>
  10. <div v-if="isEnable(data.property.is_enable_description)" class="description">
  11. <span v-for="(text, i) in descriptionList" :key="i" class="description-item" @click="selectedDescription(text)">
  12. {{ text }}
  13. </span>
  14. </div>
  15. <div class="fill-wrapper">
  16. <p v-for="(item, i) in modelEssay" :key="i">
  17. <template v-for="(li, j) in item">
  18. <span v-if="li.type === 'text'" :key="j" v-html="sanitizeHTML(li.content)"></span>
  19. <template v-if="li.type === 'input'">
  20. <el-input
  21. :key="j"
  22. v-model="li.content"
  23. :disabled="disabled"
  24. :class="[...computedAnswerClass(li.mark)]"
  25. :style="[{ width: Math.max(80, li.content.length * 21.3) + 'px' }]"
  26. @focus="handleInputFocus(i, j)"
  27. @blur="handleTone(li.content, i, j)"
  28. />
  29. <span v-show="computedAnswerText(li.mark).length > 0" :key="`answer-${j}`" class="right-answer">
  30. {{ computedAnswerText(li.mark) }}
  31. </span>
  32. </template>
  33. </template>
  34. </p>
  35. </div>
  36. </div>
  37. </template>
  38. <script>
  39. import PreviewMixin from './components/PreviewMixin';
  40. import { addTone } from '@/views/exercise_questions/data/common';
  41. import { handleToneValue } from '@/views/exercise_questions/data/fill';
  42. export default {
  43. name: 'FillPreview',
  44. mixins: [PreviewMixin],
  45. data() {
  46. return {
  47. modelEssay: [],
  48. inputFocus: false,
  49. focusPostion: {
  50. i: -1,
  51. j: -1,
  52. },
  53. };
  54. },
  55. computed: {
  56. descriptionList() {
  57. return this.data.description.split(/\s+/).filter((item) => item);
  58. },
  59. },
  60. watch: {
  61. 'data.model_essay': {
  62. handler(val) {
  63. if (!val) return;
  64. this.modelEssay = JSON.parse(JSON.stringify(val));
  65. },
  66. deep: true,
  67. immediate: true,
  68. },
  69. modelEssay: {
  70. handler(val) {
  71. if (!val) return;
  72. this.answer.answer_list = val
  73. .map((item) => {
  74. return item
  75. .map(({ type, content, mark }) => {
  76. if (type === 'input') {
  77. return {
  78. value: content,
  79. mark,
  80. };
  81. }
  82. })
  83. .filter((item) => item);
  84. })
  85. .flat();
  86. },
  87. deep: true,
  88. immediate: true,
  89. },
  90. isJudgingRightWrong(val) {
  91. if (!val) return;
  92. this.answer.answer_list.forEach(({ mark, value }) => {
  93. this.modelEssay.forEach((item) => {
  94. item.forEach((li) => {
  95. if (li.mark === mark) {
  96. li.content = value;
  97. }
  98. });
  99. });
  100. });
  101. },
  102. },
  103. created() {
  104. document.addEventListener('click', this.handleBlur);
  105. document.addEventListener('keydown', this.handleBlurTab);
  106. },
  107. beforeDestroy() {
  108. document.removeEventListener('click', this.handleBlur);
  109. document.removeEventListener('keydown', this.handleBlurTab);
  110. },
  111. methods: {
  112. /**
  113. * 处理点击失焦
  114. * @param {MouseEvent} e
  115. */
  116. handleBlur(e) {
  117. if (e.target.tagName === 'INPUT') return;
  118. this.inputFocus = false;
  119. },
  120. /**
  121. * 处理 tab 键失焦
  122. * @param {KeyboardEvent} e
  123. */
  124. handleBlurTab(e) {
  125. if (e.key !== 'Tab') return;
  126. this.inputFocus = false;
  127. },
  128. /**
  129. * 处理输入框聚焦
  130. * @param {number} i
  131. * @param {number} j
  132. */
  133. handleInputFocus(i, j) {
  134. this.inputFocus = true;
  135. this.focusPostion = {
  136. i,
  137. j,
  138. };
  139. },
  140. /**
  141. * 选中描述
  142. * @param {string} text 描述文本
  143. */
  144. selectedDescription(text) {
  145. if (!this.inputFocus) return;
  146. const { i, j } = this.focusPostion;
  147. this.modelEssay[i][j].content = text;
  148. this.inputFocus = false;
  149. },
  150. handleTone(value, i, j) {
  151. if (!/^[a-zA-Z0-9\s]+$/.test(value)) return;
  152. this.modelEssay[i][j].content = value
  153. .trim()
  154. .split(/\s+/)
  155. .map((item) => {
  156. return handleToneValue(item);
  157. })
  158. .map((item) =>
  159. item.map(({ number, con }) => (number && con ? addTone(Number(number), con) : number || con || '')),
  160. )
  161. .filter((item) => item.length > 0)
  162. .join(' ');
  163. },
  164. /**
  165. * 计算答题对错选项字体颜色
  166. * @param {string} mark 选项标识
  167. */
  168. computedAnswerClass(mark) {
  169. if (!this.isJudgingRightWrong && !this.isShowRightAnswer) {
  170. return '';
  171. }
  172. let selectOption = this.answer.answer_list.find((item) => item.mark === mark);
  173. let answerOption = this.data.answer.answer_list.find((item) => item.mark === mark);
  174. if (!selectOption) return '';
  175. let selectValue = selectOption.value;
  176. let answerValue = answerOption.value;
  177. let type = answerOption.type;
  178. let classList = [];
  179. let isRight =
  180. (type === 'only_one' && selectValue === answerValue) ||
  181. (type === 'any_one' && answerValue.split('/').includes(selectValue));
  182. if (this.isJudgingRightWrong) {
  183. isRight ? classList.push('right') : classList.push('wrong');
  184. }
  185. if (this.isShowRightAnswer && !isRight) {
  186. classList.push('show-right-answer');
  187. }
  188. return classList;
  189. },
  190. /**
  191. * 计算正确答案文本
  192. * @param {string} mark 选项标识
  193. */
  194. computedAnswerText(mark) {
  195. if (!this.isShowRightAnswer) return '';
  196. let selectOption = this.answer.answer_list.find((item) => item.mark === mark);
  197. let answerOption = this.data.answer.answer_list.find((item) => item.mark === mark);
  198. if (!selectOption) return '';
  199. let selectValue = selectOption.value;
  200. let answerValue = answerOption.value;
  201. let type = answerOption.type;
  202. let isRight =
  203. (type === 'only_one' && selectValue === answerValue) ||
  204. (type === 'any_one' && answerValue.split('/').includes(selectValue));
  205. if (isRight) return '';
  206. return `(${answerValue})`;
  207. },
  208. },
  209. };
  210. </script>
  211. <style lang="scss" scoped>
  212. @use '@/styles/mixin.scss' as *;
  213. .fill-preview {
  214. @include preview;
  215. .description {
  216. display: flex;
  217. flex-wrap: wrap;
  218. gap: 4px 8px;
  219. &-item {
  220. padding: 4px 8px;
  221. cursor: pointer;
  222. background-color: #e9edf7;
  223. border-radius: 4px;
  224. &:hover,
  225. &:active {
  226. color: $light-main-color;
  227. }
  228. }
  229. }
  230. .fill-wrapper {
  231. font-size: 16pt;
  232. .el-input {
  233. display: inline-flex;
  234. align-items: center;
  235. width: 120px;
  236. margin: 0 2px;
  237. &.right {
  238. :deep input.el-input__inner {
  239. color: $right-color;
  240. }
  241. }
  242. &.wrong {
  243. :deep input.el-input__inner {
  244. color: $error-color;
  245. }
  246. }
  247. & + .right-answer {
  248. position: relative;
  249. left: -4px;
  250. display: inline-block;
  251. height: 32px;
  252. line-height: 28px;
  253. vertical-align: bottom;
  254. border-bottom: 1px solid $font-color;
  255. }
  256. :deep input.el-input__inner {
  257. padding: 0;
  258. font-size: 16pt;
  259. color: $font-color;
  260. text-align: center;
  261. background-color: #fff;
  262. border-width: 0;
  263. border-bottom: 1px solid $font-color;
  264. border-radius: 0;
  265. }
  266. }
  267. }
  268. }
  269. </style>