Fill.vue 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  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. <el-button @click="identifyText">识别</el-button>
  42. <div class="correct-answer">
  43. <el-input
  44. v-for="(item, i) in data.answer.answer_list.filter(({ type }) => type === 'any_one')"
  45. :key="item.mark"
  46. v-model="item.value"
  47. @blur="handleTone(item.value, i)"
  48. >
  49. <span slot="prefix">{{ i + 1 }}.</span>
  50. </el-input>
  51. </div>
  52. </div>
  53. </template>
  54. </ModuleBase>
  55. </template>
  56. <script>
  57. import ModuleMixin from '../../common/ModuleMixin';
  58. import SoundRecord from '@/views/book/courseware/create/components/question/fill/components/SoundRecord.vue';
  59. import UploadAudio from '@/views/book/courseware/create/components/question/fill/components/UploadAudio.vue';
  60. import { getFillData, arrangeTypeList, fillFontList, fillTypeList } from '@/views/book/courseware/data/fill';
  61. import { addTone, handleToneValue } from '@/views/book/courseware/data/common';
  62. import { getRandomNumber } from '@/utils';
  63. import { GetStaticResources } from '@/api/app';
  64. export default {
  65. name: 'FillPage',
  66. components: {
  67. SoundRecord,
  68. UploadAudio,
  69. },
  70. mixins: [ModuleMixin],
  71. data() {
  72. return {
  73. data: getFillData(),
  74. fillTypeList,
  75. };
  76. },
  77. watch: {
  78. 'data.property.arrange_type': 'handleMindMap',
  79. 'data.property.fill_font': 'handleMindMap',
  80. 'data.vocabulary': {
  81. handler(val) {
  82. if (!val) return;
  83. this.data.word_list = val
  84. .split(/[\n\r]+/)
  85. .map((item) => item.split(' ').filter((s) => s))
  86. .flat()
  87. .map((content) => {
  88. return {
  89. content,
  90. mark: getRandomNumber(),
  91. };
  92. });
  93. },
  94. },
  95. },
  96. methods: {
  97. // 识别文本
  98. identifyText() {
  99. this.data.model_essay = [];
  100. this.data.answer.answer_list = [];
  101. this.data.content
  102. .split(/<(p|div)[^>]*>(.*?)<\/(p|div)>/g)
  103. .filter((s) => s && !s.match(/^(p|div)$/))
  104. .forEach((item) => {
  105. if (item.charCodeAt() === 10) return;
  106. let str = item
  107. // 去除所有的 font-size 样式
  108. .replace(/font-size:\s*\d+(\.\d+)?px;/gi, '')
  109. // 匹配 class 名为 rich-fill 的 span 标签和三个以上的_,并将它们组成数组
  110. .replace(/<span class="rich-fill".*?>(.*?)<\/span>|([_]{3,})/gi, '###$1$2###');
  111. this.data.model_essay.push(this.splitRichText(str));
  112. });
  113. },
  114. // 分割富文本
  115. splitRichText(str) {
  116. let _str = str;
  117. let start = 0;
  118. let index = 0;
  119. let arr = [];
  120. let matchNum = 0;
  121. while (index !== -1) {
  122. index = _str.indexOf('###', start);
  123. if (index === -1) break;
  124. matchNum += 1;
  125. arr.push({ content: _str.slice(start, index), type: 'text' });
  126. if (matchNum % 2 === 0 && arr.length > 0) {
  127. arr[arr.length - 1].type = 'input';
  128. arr[arr.length - 1].audio_answer_list = [];
  129. let mark = getRandomNumber();
  130. arr[arr.length - 1].mark = mark;
  131. let content = arr[arr.length - 1].content;
  132. // 设置答案数组
  133. let isUnderline = /^_{3,}$/.test(content);
  134. this.data.answer.answer_list.push({
  135. value: isUnderline ? '' : content,
  136. mark,
  137. type: isUnderline ? 'any_one' : 'only_one',
  138. });
  139. // 将 content 设置为空,为预览准备
  140. arr[arr.length - 1].content = '';
  141. }
  142. start = index + 3;
  143. }
  144. let last = _str.slice(start);
  145. if (last) {
  146. arr.push({ content: last, type: 'text' });
  147. }
  148. return arr;
  149. },
  150. handleTone(value, i) {
  151. if (!/^[a-zA-Z0-9\s]+$/.test(value)) return;
  152. this.data.answer.answer_list[i].value = 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. uploads(file_id) {
  165. this.data.audio_file_id = file_id;
  166. },
  167. deleteFiles() {
  168. this.data.audio_file_id = '';
  169. },
  170. // 自动生成音频
  171. handleMatic() {
  172. GetStaticResources('tool-TextToVoiceFile', {
  173. text: this.data.content.replace(/<[^>]+>/g, ''),
  174. })
  175. .then(({ status, file_id }) => {
  176. if (status === 1) {
  177. this.data.audio_file_id = file_id;
  178. }
  179. })
  180. .catch(() => {});
  181. },
  182. /**
  183. * @description 处理思维导图数据
  184. */
  185. handleMindMap() {
  186. const { fill_font, arrange_type } = this.data.property;
  187. const fontLabel = fillFontList.find((item) => item.value === fill_font)?.label || '';
  188. const arrangeLabel = arrangeTypeList.find((item) => item.value === arrange_type)?.label || '';
  189. this.data.mind_map.node_list = [
  190. {
  191. name: `${arrangeLabel}${fontLabel}填空组件`,
  192. },
  193. ];
  194. },
  195. },
  196. };
  197. </script>
  198. <style lang="scss" scoped>
  199. .fill-wrapper {
  200. display: flex;
  201. flex-direction: column;
  202. row-gap: 16px;
  203. align-items: flex-start;
  204. :deep .rich-wrapper {
  205. width: 100%;
  206. }
  207. .tips {
  208. font-size: 12px;
  209. color: #999;
  210. }
  211. .auto-matic,
  212. .upload-audio-play {
  213. :deep .upload-wrapper {
  214. margin-top: 0;
  215. }
  216. .audio-wrapper {
  217. :deep .audio-play {
  218. width: 16px;
  219. height: 16px;
  220. color: #000;
  221. background-color: initial;
  222. }
  223. :deep .audio-play.not-url {
  224. color: #a1a1a1;
  225. }
  226. :deep .voice-play {
  227. width: 16px;
  228. height: 16px;
  229. }
  230. }
  231. }
  232. .auto-matic {
  233. display: flex;
  234. flex-shrink: 0;
  235. column-gap: 12px;
  236. align-items: center;
  237. width: 200px;
  238. padding: 5px 12px;
  239. background-color: $fill-color;
  240. border-radius: 2px;
  241. .auto-btn {
  242. font-size: 16px;
  243. font-weight: 400;
  244. line-height: 22px;
  245. color: #1d2129;
  246. cursor: pointer;
  247. }
  248. }
  249. .correct-answer {
  250. display: flex;
  251. flex-wrap: wrap;
  252. gap: 8px;
  253. .el-input {
  254. width: 180px;
  255. :deep &__prefix {
  256. display: flex;
  257. align-items: center;
  258. color: $text-color;
  259. }
  260. }
  261. }
  262. }
  263. </style>