Fill.vue 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. <template>
  2. <ModuleBase :type="data.type">
  3. <template #content>
  4. <!-- eslint-disable max-len -->
  5. <div class="fill-wrapper">
  6. <RichText
  7. v-model="data.content"
  8. :is-fill="true"
  9. toolbar="fontselect fontsizeselect forecolor backcolor | underline | bold italic strikethrough alignleft aligncenter alignright"
  10. :wordlimit-num="false"
  11. />
  12. <el-input
  13. v-if="data.property.fill_type === fillTypeList[1].value"
  14. v-model="data.vocabulary"
  15. type="textarea"
  16. :autosize="{ minRows: 2, maxRows: 4 }"
  17. resize="none"
  18. placeholder="请输入词汇,用于选词填空"
  19. />
  20. <span class="tips">在需要加空的内容处插入 3 个或以上的下划线“_”。</span>
  21. <div v-if="data.audio_file_id">
  22. <SoundRecord :wav-blob.sync="data.audio_file_id" />
  23. </div>
  24. <template v-else>
  25. <div :class="['upload-audio-play']">
  26. <UploadAudio
  27. v-if="data.property.audio_generation_method === 'upload'"
  28. :file-id="data.audio_file_id"
  29. :show-upload="!data.audio_file_id"
  30. @upload="uploads"
  31. @deleteFile="deleteFiles"
  32. />
  33. <div v-else-if="data.property.audio_generation_method === 'auto'" class="auto-matic" @click="handleMatic">
  34. <SvgIcon icon-class="voiceprint-line" class="record" />
  35. <span class="auto-btn">{{ data.audio_file_id ? '已生成' : '生成音频' }}</span
  36. >{{ data.audio_file_id ? '成功' : '' }}
  37. </div>
  38. <SoundRecord v-else :wav-blob.sync="data.audio_file_id" />
  39. </div>
  40. </template>
  41. <div>
  42. <el-button @click="identifyText">识别</el-button>
  43. <el-button @click="multilingualVisible = true">多语言</el-button>
  44. </div>
  45. <div class="correct-answer">
  46. <el-input
  47. v-for="(item, i) in data.answer.answer_list.filter(({ type }) => type === 'any_one')"
  48. :key="item.mark"
  49. v-model="item.value"
  50. @blur="handleTone(item.value, i)"
  51. >
  52. <span slot="prefix">{{ i + 1 }}.</span>
  53. </el-input>
  54. </div>
  55. </div>
  56. <MultilingualFill :visible.sync="multilingualVisible" :text="data.content" />
  57. </template>
  58. </ModuleBase>
  59. </template>
  60. <script>
  61. import ModuleMixin from '../../common/ModuleMixin';
  62. import SoundRecord from '@/views/book/courseware/create/components/question/fill/components/SoundRecord.vue';
  63. import UploadAudio from '@/views/book/courseware/create/components/question/fill/components/UploadAudio.vue';
  64. import MultilingualFill from '@/views/book/components/MultilingualFill.vue';
  65. import { getFillData, arrangeTypeList, fillFontList, fillTypeList } from '@/views/book/courseware/data/fill';
  66. import { addTone, handleToneValue } from '@/views/book/courseware/data/common';
  67. import { getRandomNumber } from '@/utils';
  68. import { GetStaticResources } from '@/api/app';
  69. export default {
  70. name: 'FillPage',
  71. components: {
  72. SoundRecord,
  73. UploadAudio,
  74. MultilingualFill,
  75. },
  76. mixins: [ModuleMixin],
  77. data() {
  78. return {
  79. data: getFillData(),
  80. fillTypeList,
  81. multilingualVisible: false,
  82. };
  83. },
  84. watch: {
  85. 'data.property.arrange_type': 'handleMindMap',
  86. 'data.property.fill_font': 'handleMindMap',
  87. 'data.vocabulary': {
  88. handler(val) {
  89. if (!val) return;
  90. this.data.word_list = val
  91. .split(/[\n\r]+/)
  92. .map((item) => item.split(' ').filter((s) => s))
  93. .flat()
  94. .map((content) => {
  95. return {
  96. content,
  97. mark: getRandomNumber(),
  98. };
  99. });
  100. },
  101. },
  102. },
  103. methods: {
  104. // 识别文本
  105. identifyText() {
  106. this.data.model_essay = [];
  107. this.data.answer.answer_list = [];
  108. this.data.content
  109. .split(/<(p|div)[^>]*>(.*?)<\/(p|div)>/g)
  110. .filter((s) => s && !s.match(/^(p|div)$/))
  111. .forEach((item) => {
  112. if (item.charCodeAt() === 10) return;
  113. let str = item
  114. // 去除所有的 font-size 样式
  115. .replace(/font-size:\s*\d+(\.\d+)?px;/gi, '')
  116. // 匹配 class 名为 rich-fill 的 span 标签和三个以上的_,并将它们组成数组
  117. .replace(/<span class="rich-fill".*?>(.*?)<\/span>|([_]{3,})/gi, '###$1$2###');
  118. this.data.model_essay.push(this.splitRichText(str));
  119. });
  120. },
  121. // 分割富文本
  122. splitRichText(str) {
  123. let _str = str;
  124. let start = 0;
  125. let index = 0;
  126. let arr = [];
  127. let matchNum = 0;
  128. while (index !== -1) {
  129. index = _str.indexOf('###', start);
  130. if (index === -1) break;
  131. matchNum += 1;
  132. arr.push({ content: _str.slice(start, index), type: 'text' });
  133. if (matchNum % 2 === 0 && arr.length > 0) {
  134. arr[arr.length - 1].type = 'input';
  135. arr[arr.length - 1].audio_answer_list = [];
  136. let mark = getRandomNumber();
  137. arr[arr.length - 1].mark = mark;
  138. let content = arr[arr.length - 1].content;
  139. // 设置答案数组
  140. let isUnderline = /^_{3,}$/.test(content);
  141. this.data.answer.answer_list.push({
  142. value: isUnderline ? '' : content,
  143. mark,
  144. type: isUnderline ? 'any_one' : 'only_one',
  145. });
  146. // 将 content 设置为空,为预览准备
  147. arr[arr.length - 1].content = '';
  148. }
  149. start = index + 3;
  150. }
  151. let last = _str.slice(start);
  152. if (last) {
  153. arr.push({ content: last, type: 'text' });
  154. }
  155. return arr;
  156. },
  157. handleTone(value, i) {
  158. if (!/^[a-zA-Z0-9\s]+$/.test(value)) return;
  159. this.data.answer.answer_list[i].value = value
  160. .trim()
  161. .split(/\s+/)
  162. .map((item) => {
  163. return handleToneValue(item);
  164. })
  165. .map((item) =>
  166. item.map(({ number, con }) => (number && con ? addTone(Number(number), con) : number || con || '')),
  167. )
  168. .filter((item) => item.length > 0)
  169. .join(' ');
  170. },
  171. uploads(file_id) {
  172. this.data.audio_file_id = file_id;
  173. },
  174. deleteFiles() {
  175. this.data.audio_file_id = '';
  176. },
  177. // 自动生成音频
  178. handleMatic() {
  179. GetStaticResources('tool-TextToVoiceFile', {
  180. text: this.data.content.replace(/<[^>]+>/g, ''),
  181. })
  182. .then(({ status, file_id }) => {
  183. if (status === 1) {
  184. this.data.audio_file_id = file_id;
  185. }
  186. })
  187. .catch(() => {});
  188. },
  189. /**
  190. * @description 处理思维导图数据
  191. */
  192. handleMindMap() {
  193. const { fill_font, arrange_type } = this.data.property;
  194. const fontLabel = fillFontList.find((item) => item.value === fill_font)?.label || '';
  195. const arrangeLabel = arrangeTypeList.find((item) => item.value === arrange_type)?.label || '';
  196. this.data.mind_map.node_list = [
  197. {
  198. name: `${arrangeLabel}${fontLabel}填空组件`,
  199. },
  200. ];
  201. },
  202. },
  203. };
  204. </script>
  205. <style lang="scss" scoped>
  206. .fill-wrapper {
  207. display: flex;
  208. flex-direction: column;
  209. row-gap: 16px;
  210. align-items: flex-start;
  211. :deep .rich-wrapper {
  212. width: 100%;
  213. }
  214. .tips {
  215. font-size: 12px;
  216. color: #999;
  217. }
  218. .auto-matic,
  219. .upload-audio-play {
  220. :deep .upload-wrapper {
  221. margin-top: 0;
  222. }
  223. .audio-wrapper {
  224. :deep .audio-play {
  225. width: 16px;
  226. height: 16px;
  227. color: #000;
  228. background-color: initial;
  229. }
  230. :deep .audio-play.not-url {
  231. color: #a1a1a1;
  232. }
  233. :deep .voice-play {
  234. width: 16px;
  235. height: 16px;
  236. }
  237. }
  238. }
  239. .auto-matic {
  240. display: flex;
  241. flex-shrink: 0;
  242. column-gap: 12px;
  243. align-items: center;
  244. width: 200px;
  245. padding: 5px 12px;
  246. background-color: $fill-color;
  247. border-radius: 2px;
  248. .auto-btn {
  249. font-size: 16px;
  250. font-weight: 400;
  251. line-height: 22px;
  252. color: #1d2129;
  253. cursor: pointer;
  254. }
  255. }
  256. .correct-answer {
  257. display: flex;
  258. flex-wrap: wrap;
  259. gap: 8px;
  260. .el-input {
  261. width: 180px;
  262. :deep &__prefix {
  263. display: flex;
  264. align-items: center;
  265. color: $text-color;
  266. }
  267. }
  268. }
  269. }
  270. </style>