FillQuestion.vue 8.3 KB

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