FillQuestion.vue 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. <template>
  2. <QuestionBase>
  3. <template #content>
  4. <div class="stem">
  5. <RichText v-model="data.stem" :font-size="18" placeholder="输入题干" />
  6. <el-input
  7. v-if="isEnable(data.property.is_enable_description)"
  8. v-model="data.description"
  9. rows="3"
  10. resize="none"
  11. type="textarea"
  12. placeholder="输入填空内容"
  13. />
  14. </div>
  15. <div class="content">
  16. <RichText
  17. ref="modelEssay"
  18. v-model="data.article"
  19. :is-fill="true"
  20. :toolbar="false"
  21. :wordlimit-num="false"
  22. placeholder="输入文段"
  23. @showContentmenu="showContentmenu"
  24. @hideContentmenu="hideContentmenu"
  25. />
  26. <div v-show="isShow" ref="contentmenu" :style="contentmenu" class="contentmenu">
  27. <SvgIcon icon-class="slice" size="16" @click="setFill" />
  28. <span class="button" @click="setFill">设为填空</span>
  29. <span class="line"></span>
  30. <SvgIcon icon-class="close-circle" size="16" @click="deleteFill" />
  31. <span class="button" @click="deleteFill">删除填空</span>
  32. </div>
  33. <el-button @click="identifyText">识别</el-button>
  34. <div v-if="data.answer.answer_list.length > 0" class="correct-answer">
  35. <div class="subtitle">正确答案</div>
  36. <el-input
  37. v-for="(item, i) in data.answer.answer_list.filter(({ type }) => type === 'any_one')"
  38. :key="item.mark"
  39. v-model="item.value"
  40. @blur="handleTone(item.value, i)"
  41. >
  42. <span slot="prefix">{{ i + 1 }}.</span>
  43. </el-input>
  44. </div>
  45. </div>
  46. </template>
  47. <template #property>
  48. <el-form :model="data.property">
  49. <el-form-item label="题号">
  50. <el-input v-model="data.property.question_number" />
  51. </el-form-item>
  52. <el-form-item label-width="45px">
  53. <el-radio
  54. v-for="{ value, label } in questionNumberTypeList"
  55. :key="value"
  56. v-model="data.other.question_number_type"
  57. :label="value"
  58. >
  59. {{ label }}
  60. </el-radio>
  61. </el-form-item>
  62. <el-form-item label="描述">
  63. <el-radio
  64. v-for="{ value, label } in switchOption"
  65. :key="value"
  66. v-model="data.property.is_enable_description"
  67. :label="value"
  68. >
  69. {{ label }}
  70. </el-radio>
  71. </el-form-item>
  72. <el-form-item label="分值">
  73. <el-radio
  74. v-for="{ value, label } in scoreTypeList"
  75. :key="value"
  76. v-model="data.property.score_type"
  77. :label="value"
  78. >
  79. {{ label }}
  80. </el-radio>
  81. </el-form-item>
  82. <el-form-item label-width="45px">
  83. <el-input-number
  84. v-model="data.property.score"
  85. :min="0"
  86. :step="data.property.score_type === scoreTypeList[0].value ? 1 : 0.1"
  87. />
  88. </el-form-item>
  89. </el-form>
  90. </template>
  91. </QuestionBase>
  92. </template>
  93. <script>
  94. import QuestionMixin from '../common/QuestionMixin.js';
  95. import { getRandomNumber } from '@/utils';
  96. import { addTone } from '@/views/exercise_questions/data/common';
  97. import { fillData, handleToneValue } from '@/views/exercise_questions/data/fill';
  98. export default {
  99. name: 'FillQuestion',
  100. mixins: [QuestionMixin],
  101. data() {
  102. return {
  103. isShow: false,
  104. contentmenu: {
  105. top: 0,
  106. left: 0,
  107. },
  108. data: JSON.parse(JSON.stringify(fillData)),
  109. };
  110. },
  111. created() {
  112. window.addEventListener('click', this.hideContentmenu);
  113. },
  114. beforeDestroy() {
  115. window.removeEventListener('click', this.hideContentmenu);
  116. },
  117. methods: {
  118. // 识别文本
  119. identifyText() {
  120. this.data.model_essay = [];
  121. this.data.answer.answer_list = [];
  122. this.data.article
  123. .split(/<p>(.*?)<\/p>/gi)
  124. .filter((item) => item)
  125. .forEach((item) => {
  126. if (item.charCodeAt() === 10) return;
  127. // 匹配 class 名为 rich-fill 的 span 标签和三个以上的_,并将它们组成数组
  128. let str = item.replace(/<span class="rich-fill".*?>(.*?)<\/span>|([_]{3,})/gi, '###$1$2###');
  129. this.data.model_essay.push(this.splitRichText(str));
  130. });
  131. },
  132. // 分割富文本
  133. splitRichText(str) {
  134. let _str = str;
  135. let start = 0;
  136. let index = 0;
  137. let arr = [];
  138. let matchNum = 0;
  139. while (index !== -1) {
  140. index = _str.indexOf('###', start);
  141. if (index === -1) break;
  142. matchNum += 1;
  143. arr.push({ content: _str.slice(start, index), type: 'text' });
  144. if (matchNum % 2 === 0 && arr.length > 0) {
  145. arr[arr.length - 1].type = 'input';
  146. let mark = getRandomNumber();
  147. arr[arr.length - 1].mark = mark;
  148. let content = arr[arr.length - 1].content;
  149. // 设置答案数组
  150. let isUnderline = /^_{3,}$/.test(content);
  151. this.data.answer.answer_list.push({
  152. value: isUnderline ? '' : content,
  153. mark,
  154. type: isUnderline ? 'any_one' : 'only_one',
  155. });
  156. // 将 content 设置为空,为预览准备
  157. arr[arr.length - 1].content = '';
  158. }
  159. start = index + 3;
  160. }
  161. let last = _str.slice(start);
  162. if (last) {
  163. arr.push({ content: last, type: 'text' });
  164. }
  165. return arr;
  166. },
  167. // 设置填空
  168. setFill() {
  169. this.$refs.modelEssay.setContent();
  170. this.hideContentmenu();
  171. },
  172. // 删除填空
  173. deleteFill() {
  174. this.$refs.modelEssay.deleteContent();
  175. this.hideContentmenu();
  176. },
  177. hideContentmenu() {
  178. this.isShow = false;
  179. },
  180. showContentmenu({ pixelsFromLeft, pixelsFromTop }) {
  181. this.isShow = true;
  182. this.contentmenu = {
  183. left: `${pixelsFromLeft + 14}px`,
  184. top: `${pixelsFromTop - 18}px`,
  185. };
  186. },
  187. handleTone(value, i) {
  188. if (!/^[a-zA-Z0-9\s]+$/.test(value)) return;
  189. this.data.answer.answer_list[i].value = value
  190. .trim()
  191. .split(/\s+/)
  192. .map((item) => {
  193. return handleToneValue(item);
  194. })
  195. .map((item) =>
  196. item.map(({ number, con }) => (number && con ? addTone(Number(number), con) : number || con || '')),
  197. )
  198. .filter((item) => item.length > 0)
  199. .join(' ');
  200. },
  201. },
  202. };
  203. </script>
  204. <style lang="scss" scoped>
  205. .content {
  206. position: relative;
  207. .el-button {
  208. margin-top: 8px;
  209. }
  210. .correct-answer {
  211. .subtitle {
  212. margin: 8px 0;
  213. font-size: 14px;
  214. color: #4e5969;
  215. }
  216. .el-input {
  217. width: 180px;
  218. :deep &__prefix {
  219. display: flex;
  220. align-items: center;
  221. color: $text-color;
  222. }
  223. + .el-input {
  224. margin-left: 8px;
  225. }
  226. }
  227. }
  228. .contentmenu {
  229. position: absolute;
  230. z-index: 999;
  231. display: flex;
  232. column-gap: 4px;
  233. align-items: center;
  234. padding: 4px 8px;
  235. font-size: 14px;
  236. background-color: #fff;
  237. border-radius: 2px;
  238. box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 10%);
  239. .svg-icon,
  240. .button {
  241. cursor: pointer;
  242. }
  243. .line {
  244. min-height: 16px;
  245. margin: 0 4px;
  246. }
  247. }
  248. }
  249. </style>