Fill.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810
  1. <!-- eslint-disable vue/no-v-html -->
  2. <template>
  3. <ModuleBase ref="base" :type="data.type">
  4. <template #content>
  5. <!-- eslint-disable max-len -->
  6. <div class="fill-wrapper">
  7. <RichText
  8. v-if="property.isGetContent"
  9. ref="richText"
  10. v-model="data.content"
  11. :is-fill="true"
  12. toolbar="fontselect fontsizeselect forecolor backcolor | lineheight paragraphSpacing underline | bold italic strikethrough alignleft aligncenter alignright bullist numlist dotEmphasis"
  13. :wordlimit-num="false"
  14. :font-size="data?.unified_attrib?.font_size"
  15. :font-family="data?.unified_attrib?.font"
  16. :font-color="data?.unified_attrib?.text_color"
  17. @handleRichTextBlur="parsedContentPinyin"
  18. />
  19. <div v-if="data.property.fill_type === fillTypeList[1].value" class="select-vocabulary">
  20. <h5 class="title">选词列表:</h5>
  21. <el-button size="mini" @click="openAddWord">添加词汇</el-button>
  22. <ul class="word-list">
  23. <li v-for="(item, index) in data.word_list" :key="item.mark" class="word-item">
  24. <span v-html="sanitizeHTML(item.content)"></span>
  25. <el-button type="text" size="mini" class="delete-word" @click="removeWord(index)">
  26. <SvgIcon icon-class="delete-black" size="12" />
  27. </el-button>
  28. </li>
  29. </ul>
  30. </div>
  31. <span class="tips">在需要加空的内容处插入 3 个或以上的下划线“_”。</span>
  32. <div v-if="data.audio_file_id">
  33. <SoundRecord :wav-blob.sync="data.audio_file_id" />
  34. </div>
  35. <template v-else>
  36. <div :class="['upload-audio-play']">
  37. <UploadAudio
  38. v-if="data.property.audio_generation_method === 'upload'"
  39. :file-id="data.audio_file_id"
  40. :show-upload="!data.audio_file_id"
  41. @upload="uploads"
  42. @deleteFile="deleteFiles"
  43. />
  44. <div v-else-if="data.property.audio_generation_method === 'auto'" class="auto-matic" @click="handleMatic">
  45. <SvgIcon icon-class="voiceprint-line" class="record" />
  46. <span class="auto-btn">{{ data.audio_file_id ? '已生成' : '生成音频' }}</span
  47. >{{ data.audio_file_id ? '成功' : '' }}
  48. </div>
  49. <SoundRecord v-else :wav-blob.sync="data.audio_file_id" />
  50. </div>
  51. </template>
  52. <div>
  53. <el-button @click="parsedContentPinyin">识别</el-button>
  54. <el-button @click="openMultilingual">多语言</el-button>
  55. </div>
  56. <div v-if="data.answer.answer_list.length > 0" class="title">答案:</div>
  57. <div class="correct-answer">
  58. <el-input
  59. v-for="(item, i) in data.answer.answer_list.filter(({ type }) => type === 'any_one')"
  60. :key="item.mark"
  61. v-model="item.value"
  62. placeholder="多个答案可用‘/’分割"
  63. @blur="handleTone(item.value, i)"
  64. >
  65. <span slot="prefix">{{ i + 1 }}.</span>
  66. </el-input>
  67. </div>
  68. </div>
  69. <el-divider v-if="isEnable(data.property.view_pinyin)" content-position="left">
  70. <span>拼音效果</span>
  71. <el-button
  72. v-show="isEnable(data.property.view_pinyin)"
  73. type="text"
  74. icon="el-icon-refresh"
  75. title="刷新"
  76. class="refresh-pinyin-btn"
  77. @click.native="parsedContentPinyin"
  78. />
  79. </el-divider>
  80. <template v-if="isEnable(data.property.view_pinyin)">
  81. <div v-for="(item, i) in data.model_essay" :key="i" class="pinyin-text-list">
  82. <PinyinText
  83. :key="`pinyin-${i}`"
  84. ref="PinyinText"
  85. :rich-text-list="item.rich_text_list"
  86. :pinyin-position="data.property.pinyin_position"
  87. :body-styles="getBodyStyles()"
  88. @fillCorrectPinyin="fillCorrectPinyin($event, i, -1, 'model_essay')"
  89. />
  90. </div>
  91. </template>
  92. <MultilingualFill
  93. :visible.sync="multilingualVisible"
  94. :text="data.content"
  95. :translations="data.multilingual"
  96. @SubmitTranslation="handleMultilingualTranslation"
  97. />
  98. <AnswerAnalysisList
  99. v-if="data.answer_list?.length > 0 || data.analysis_list?.length > 0"
  100. :answer-list="data.answer_list"
  101. :analysis-list="data.analysis_list"
  102. :unified-attrib="data.unified_attrib"
  103. @updateAnswerAnalysisFileList="updateAnswerAnalysisFileList"
  104. @deleteAnswerAnalysis="deleteAnswerAnalysis"
  105. />
  106. <AddWord :visible.sync="visibleWord" @add-word="addWord" />
  107. </template>
  108. </ModuleBase>
  109. </template>
  110. <script>
  111. import ModuleMixin from '../../common/ModuleMixin';
  112. import SoundRecord from '@/views/book/courseware/create/components/question/fill/components/SoundRecord.vue';
  113. import UploadAudio from '@/views/book/courseware/create/components/question/fill/components/UploadAudio.vue';
  114. import PinyinText from '@/components/PinyinText.vue';
  115. import AddWord from './components/AddWord.vue';
  116. import { getFillData, arrangeTypeList, fillTypeList } from '@/views/book/courseware/data/fill';
  117. import { addTone, handleToneValue } from '@/views/book/courseware/data/common';
  118. import { getRandomNumber } from '@/utils';
  119. import { TextToAudioFile } from '@/api/app';
  120. import { sanitizeHTML } from '@/utils/common';
  121. import { PinyinBuild_OldFormat } from '@/api/book';
  122. export default {
  123. name: 'FillPage',
  124. components: {
  125. SoundRecord,
  126. UploadAudio,
  127. PinyinText,
  128. AddWord,
  129. },
  130. mixins: [ModuleMixin],
  131. data() {
  132. return {
  133. data: getFillData(),
  134. fillTypeList,
  135. sanitizeHTML,
  136. visibleWord: false,
  137. rich_text_list: [],
  138. };
  139. },
  140. watch: {
  141. 'data.property.arrange_type': 'handleMindMap',
  142. 'data.property.fill_font': 'handleMindMap',
  143. 'data.property': {
  144. handler() {
  145. this.parsedContentPinyin();
  146. },
  147. deep: true,
  148. },
  149. },
  150. methods: {
  151. async parsedContentPinyin() {
  152. PinyinBuild_OldFormat({
  153. text: this.data.content,
  154. is_first_sentence_first_hz_pinyin_first_char_upper_case:
  155. this.data.property.is_first_sentence_first_hz_pinyin_first_char_upper_case,
  156. is_fill_space: 'true',
  157. is_rich_text: 'true',
  158. }).then(({ rich_text }) => {
  159. if (rich_text) {
  160. this.rich_text_list = rich_text.text_list;
  161. this.data.model_essay = this.parseRichText();
  162. }
  163. });
  164. },
  165. parseRichText() {
  166. let text_list = this.rich_text_list || [];
  167. const preservedAnyOneAnswers = (this.data.answer.answer_list || [])
  168. .filter(({ type }) => type === 'any_one')
  169. .map(({ value }) => value);
  170. const preservedAnyOneState = { index: 0 };
  171. this.data.answer.answer_list = [];
  172. const arr = [];
  173. let totalText = '';
  174. let totalRichText = [];
  175. const openStyleTagStack = [];
  176. for (let i = 0; i < text_list.length; i++) {
  177. const textItem = text_list[i];
  178. const text = textItem.text || '';
  179. const isStyle = textItem.is_style === 'true' || textItem.is_style === true;
  180. if (isStyle) {
  181. const isRichFill = /class=\s*(\\?["'])rich-fill\1/.test(text);
  182. if (isRichFill) {
  183. if (totalText.length > 0) {
  184. arr.push({
  185. content: totalText,
  186. type: 'text',
  187. rich_text_list: totalRichText,
  188. });
  189. totalText = '';
  190. totalRichText = [];
  191. }
  192. let nextData = text_list[i + 1] || {};
  193. let nextTag = text_list[i + 2] || {};
  194. const mark = getRandomNumber();
  195. arr.push({
  196. content: text + (nextData.text || '') + (nextTag.text || ''),
  197. type: 'input',
  198. input: '',
  199. audio_answer_list: [],
  200. mark,
  201. rich_text_list: [textItem, nextData, nextTag],
  202. });
  203. this.data.answer.answer_list.push({
  204. value: nextData.text || '',
  205. mark,
  206. type: 'only_one',
  207. });
  208. // 跳过下一个文本和标签数据
  209. i += 2;
  210. } else {
  211. totalText += text;
  212. totalRichText = totalRichText.concat(textItem);
  213. this.syncOpenStyleTagStack(openStyleTagStack, textItem);
  214. }
  215. } else {
  216. const splitBlocks = this.splitTextItemByUnderline(textItem, preservedAnyOneAnswers, preservedAnyOneState);
  217. const currentOpenStyleTags = openStyleTagStack.map((tagItem) => ({ ...tagItem }));
  218. const firstBlockPrefix =
  219. totalText.length > 0
  220. ? this.buildFirstBlockStylePrefix(totalRichText, currentOpenStyleTags)
  221. : currentOpenStyleTags;
  222. // 样式标签单独成块会导致 PinyinText 的样式栈断开,需并入紧随其后的文本块。
  223. if (splitBlocks.length > 0) {
  224. if (totalText.length > 0) {
  225. splitBlocks[0].content = `${totalText}${splitBlocks[0].content || ''}`;
  226. }
  227. splitBlocks.forEach((block, blockIndex) => {
  228. const prevRichTextList = block.rich_text_list || [];
  229. block.rich_text_list = [
  230. ...(blockIndex === 0 ? firstBlockPrefix : currentOpenStyleTags),
  231. ...prevRichTextList,
  232. ];
  233. });
  234. totalText = '';
  235. totalRichText = [];
  236. } else if (totalText.length > 0) {
  237. arr.push({
  238. content: totalText,
  239. type: 'text',
  240. rich_text_list: firstBlockPrefix,
  241. });
  242. totalText = '';
  243. totalRichText = [];
  244. }
  245. arr.push(...splitBlocks);
  246. }
  247. }
  248. if (totalText.length > 0) {
  249. arr.push({
  250. content: totalText,
  251. type: 'text',
  252. rich_text_list: totalRichText,
  253. });
  254. }
  255. return arr;
  256. },
  257. getStyleTagName(tagText = '') {
  258. const trimmedText = String(tagText).trim();
  259. const closeMatch = trimmedText.match(/^<\/(\w+)>$/);
  260. if (closeMatch) return closeMatch[1].toLowerCase();
  261. const openMatch = trimmedText.match(/^<(\w+)([^>]*)>$/);
  262. if (openMatch) return openMatch[1].toLowerCase();
  263. return '';
  264. },
  265. isOpenStyleTag(tagItem = {}) {
  266. const tagText = String(tagItem?.text || '').trim();
  267. const isStyleTag = tagItem?.is_style === 'true' || tagItem?.is_style === true;
  268. if (!isStyleTag) return false;
  269. if (/^<\//.test(tagText)) return false;
  270. if (/^<br\s*\/?\s*>$/i.test(tagText)) return false;
  271. return /^<\w+[^>]*>$/.test(tagText);
  272. },
  273. syncOpenStyleTagStack(openStyleTagStack, styleTagItem) {
  274. const tagText = String(styleTagItem?.text || '').trim();
  275. const isStyleTag = styleTagItem?.is_style === 'true' || styleTagItem?.is_style === true;
  276. if (!isStyleTag) return;
  277. if (/^<br\s*\/?\s*>$/i.test(tagText)) return;
  278. const closeMatch = tagText.match(/^<\/(\w+)>$/);
  279. if (closeMatch) {
  280. const closeTagName = closeMatch[1].toLowerCase();
  281. for (let i = openStyleTagStack.length - 1; i >= 0; i--) {
  282. const tagName = this.getStyleTagName(openStyleTagStack[i]?.text);
  283. if (tagName === closeTagName) {
  284. openStyleTagStack.splice(i, 1);
  285. break;
  286. }
  287. }
  288. return;
  289. }
  290. if (this.isOpenStyleTag(styleTagItem)) {
  291. openStyleTagStack.push(styleTagItem);
  292. }
  293. },
  294. buildFirstBlockStylePrefix(transitionStyleTags = [], currentOpenStyleTags = []) {
  295. const transitionOpenTagNames = transitionStyleTags
  296. .filter((tagItem) => this.isOpenStyleTag(tagItem))
  297. .map((tagItem) => this.getStyleTagName(tagItem?.text));
  298. const missingOpenTags = currentOpenStyleTags.filter((tagItem) => {
  299. const tagName = this.getStyleTagName(tagItem?.text);
  300. return tagName && !transitionOpenTagNames.includes(tagName);
  301. });
  302. return [...missingOpenTags, ...transitionStyleTags];
  303. },
  304. /**
  305. * 根据文本中的连续下划线分割文本块,并将下划线部分转换为输入块
  306. * @param {Object} textItem 富文本中的一个文本项
  307. * @param {Array} preservedAnyOneAnswers 预先保存的 any_one 类型答案列表
  308. * @param {Object} preservedAnyOneState any_one 答案的当前索引状态
  309. */
  310. splitTextItemByUnderline(textItem, preservedAnyOneAnswers = [], preservedAnyOneState = { index: 0 }) {
  311. const text = textItem?.text || '';
  312. const matcher = /_{3,}/g;
  313. const blocks = [];
  314. let lastIndex = 0;
  315. let match = matcher.exec(text);
  316. while (match) {
  317. const underlineText = match[0] || '';
  318. const start = match.index;
  319. const end = start + underlineText.length;
  320. if (start > lastIndex) {
  321. const textContent = text.slice(lastIndex, start);
  322. blocks.push({
  323. content: textContent,
  324. type: 'text',
  325. rich_text_list: [
  326. {
  327. ...textItem,
  328. text: textContent,
  329. word_list: this.sliceWordListByTextRange(textItem.word_list, lastIndex, start),
  330. },
  331. ],
  332. });
  333. }
  334. const mark = getRandomNumber();
  335. blocks.push({
  336. content: underlineText,
  337. type: 'input',
  338. input: '',
  339. audio_answer_list: [],
  340. mark,
  341. rich_text_list: [
  342. {
  343. ...textItem,
  344. text: underlineText,
  345. word_list: this.sliceWordListByTextRange(textItem.word_list, start, end),
  346. },
  347. ],
  348. });
  349. this.data.answer.answer_list.push({
  350. value: preservedAnyOneAnswers[preservedAnyOneState.index] || '',
  351. mark,
  352. type: 'any_one',
  353. });
  354. preservedAnyOneState.index += 1;
  355. lastIndex = end;
  356. match = matcher.exec(text);
  357. }
  358. if (lastIndex < text.length) {
  359. const textContent = text.slice(lastIndex);
  360. blocks.push({
  361. content: textContent,
  362. type: 'text',
  363. rich_text_list: [
  364. {
  365. ...textItem,
  366. text: textContent,
  367. word_list: this.sliceWordListByTextRange(textItem.word_list, lastIndex, text.length),
  368. },
  369. ],
  370. });
  371. }
  372. if (blocks.length === 0) {
  373. return [
  374. {
  375. content: text,
  376. type: 'text',
  377. rich_text_list: [textItem],
  378. },
  379. ];
  380. }
  381. return blocks;
  382. },
  383. sliceWordListByTextRange(wordList = [], rangeStart = 0, rangeEnd = 0) {
  384. if (!Array.isArray(wordList) || rangeEnd <= rangeStart) return [];
  385. const result = [];
  386. let cursor = 0;
  387. wordList.forEach((wordItem) => {
  388. const wordText = wordItem?.text || '';
  389. const wordStart = cursor;
  390. const wordEnd = wordStart + wordText.length;
  391. cursor = wordEnd;
  392. const overlapStart = Math.max(rangeStart, wordStart);
  393. const overlapEnd = Math.min(rangeEnd, wordEnd);
  394. if (overlapStart >= overlapEnd) return;
  395. const relativeStart = overlapStart - wordStart;
  396. const relativeEnd = overlapEnd - wordStart;
  397. result.push(this.sliceWordItem(wordItem, relativeStart, relativeEnd));
  398. });
  399. return result;
  400. },
  401. sliceWordItem(wordItem, start, end) {
  402. const fullText = wordItem?.text || '';
  403. const slicedText = fullText.slice(start, end);
  404. const slicedWord = {
  405. ...wordItem,
  406. text: slicedText,
  407. };
  408. const sourcePinyinList = Array.isArray(wordItem?.pinyin_list) ? wordItem.pinyin_list : [];
  409. if (sourcePinyinList.length === fullText.length) {
  410. slicedWord.pinyin_list = sourcePinyinList.slice(start, end);
  411. slicedWord.pinyin = (slicedWord.pinyin_list || []).join(' ').trim();
  412. return slicedWord;
  413. }
  414. if (start === 0 && end === fullText.length) {
  415. slicedWord.pinyin_list = [...sourcePinyinList];
  416. return slicedWord;
  417. }
  418. slicedWord.pinyin_list = new Array(slicedText.length).fill('');
  419. slicedWord.pinyin = '';
  420. return slicedWord;
  421. },
  422. /**
  423. * 识别文本中
  424. * @param {Boolean} isUpdatePinyin 是否更新拼音,默认为 true
  425. */
  426. identifyText(isUpdatePinyin = true) {
  427. this.data.answer.answer_list = [];
  428. const content = this.data.content || '';
  429. // 使用 class 为 rich-fill 的 span 以及连续 3 个及以上下划线作为分割符
  430. if (!content || !content.match(/<span[^>]*class="[^"]*rich-fill[^"]*"[^>]*>(.*?)<\/span>|_{3,}/gi)) {
  431. this.data.model_essay = [
  432. [
  433. {
  434. content,
  435. type: 'text',
  436. paragraph_list: [],
  437. paragraph_list_parameter: { text: '', pinyin_proofread_word_list: [] },
  438. },
  439. ],
  440. ];
  441. return;
  442. }
  443. const splitSource = content.split(/\n|<br>/).map((item) => {
  444. // rich-fill 和 ___ 均转为统一占位符,交给 splitRichText 处理
  445. return this.splitRichText(
  446. item.replace(/<span[^>]*class="[^"]*rich-fill[^"]*"[^>]*>.*?<\/span>|_{3,}/gi, '###$&###'),
  447. );
  448. });
  449. this.data.model_essay = splitSource;
  450. if (isUpdatePinyin) this.handleViewPinyin();
  451. },
  452. /**
  453. * 分割富文本
  454. * @param {String} str 富文本字符串
  455. * @returns {Array} 分割后的数组
  456. */
  457. splitRichText(str) {
  458. const parts = String(str).split(/###/g);
  459. const arr = [];
  460. for (let i = 0; i < parts.length; i++) {
  461. let content = parts[i] ?? '';
  462. // 偶数索引为普通文本段
  463. if (i % 2 === 0) {
  464. if (content === '') continue; // 跳过空文本块
  465. // 判断 content 最前面是否是标签
  466. const isStartWithTag = /^<[^>]+>/.test(content);
  467. if (!isStartWithTag) {
  468. content = this.setTag(i, parts, content);
  469. }
  470. arr.push({
  471. content,
  472. type: 'text',
  473. paragraph_list: [],
  474. paragraph_list_parameter: {
  475. text: '',
  476. pinyin_proofread_word_list: [],
  477. },
  478. });
  479. continue;
  480. }
  481. // 奇数索引为输入段(被 ### 包裹的分割符)
  482. const separatorContent = content;
  483. const isUnderline = /^_{3,}$/.test(separatorContent);
  484. const richFillMatch = separatorContent.match(/^<span[^>]*class="[^"]*rich-fill[^"]*"[^>]*>(.*?)<\/span>$/i);
  485. const answerValue = isUnderline ? '' : richFillMatch ? richFillMatch[1] : separatorContent;
  486. const mark = getRandomNumber();
  487. arr.push({
  488. content: separatorContent,
  489. type: 'input',
  490. input: '',
  491. audio_answer_list: [],
  492. mark,
  493. paragraph_list: [],
  494. paragraph_list_parameter: {
  495. text: '',
  496. pinyin_proofread_word_list: [],
  497. },
  498. });
  499. // 同步更新答案列表
  500. this.data.answer.answer_list.push({
  501. value: answerValue,
  502. mark,
  503. type: isUnderline ? 'any_one' : 'only_one',
  504. });
  505. }
  506. return arr;
  507. },
  508. /**
  509. * 设置前一个标签
  510. * @param {Number} index 当前索引
  511. * @param {Array} parts 分割后的数组
  512. * @param {String} content 当前内容
  513. * @returns {String} 包含向前两个标签内容中最后一个html标签的内容
  514. */
  515. setTag(index, parts, content) {
  516. let i = index;
  517. if (i < 2) return content;
  518. let _content = content;
  519. const isEndWithTag = /<\/[^>]+>$/.test(_content); // 判断是否以标签结尾
  520. let startTag = '';
  521. const part = parts[i - 2] ?? '';
  522. const tagMatch = part.match(/<[^>]+>/g);
  523. if (tagMatch) {
  524. startTag = tagMatch[tagMatch.length - 1]; // 获取最后一个标签
  525. // 如果是 <br> 标签,继续往前找,直到找到非 <br> 标签或者没有标签为止
  526. while (startTag.toLowerCase() === '<br>') {
  527. const prevPart = parts[i - 3] ?? '';
  528. const prevTagMatch = prevPart.match(/<[^>]+>/g);
  529. if (prevTagMatch === null) {
  530. startTag = '';
  531. break;
  532. }
  533. if (prevTagMatch) {
  534. startTag = prevTagMatch[prevTagMatch.length - 1];
  535. i -= 1;
  536. } else {
  537. break;
  538. }
  539. }
  540. }
  541. _content = `${startTag}${_content}`;
  542. if (!isEndWithTag) {
  543. let tag = startTag.match(/^<([^>\s]+).*?>/);
  544. tag = tag ? tag[1] : 'span';
  545. _content += `</${tag}>`;
  546. }
  547. return _content;
  548. },
  549. handleTone(value, i) {
  550. if (!/^[a-zA-Z0-9\s]+$/.test(value)) return;
  551. this.data.answer.answer_list[i].value = value
  552. .trim()
  553. .split(/\s+/)
  554. .map((item) => {
  555. return handleToneValue(item);
  556. })
  557. .map((item) =>
  558. item.map(({ number, con }) => (number && con ? addTone(Number(number), con) : number || con || '')),
  559. )
  560. .filter((item) => item.length > 0)
  561. .join(' ');
  562. },
  563. uploads(file_id) {
  564. this.data.audio_file_id = file_id;
  565. },
  566. deleteFiles() {
  567. this.data.audio_file_id = '';
  568. },
  569. // 自动生成音频
  570. handleMatic() {
  571. TextToAudioFile({
  572. text: this.data.content.replace(/<[^>]+>/g, ''),
  573. voice_type: this.data.property.voice_type,
  574. emotion: this.data.property.emotion,
  575. speed_ratio: this.data.property.speed_ratio,
  576. })
  577. .then(({ status, file_id }) => {
  578. if (status === 1) {
  579. this.data.audio_file_id = file_id;
  580. }
  581. })
  582. .catch(() => {});
  583. },
  584. /**
  585. * @description 处理思维导图数据
  586. */
  587. handleMindMap() {
  588. const { arrange_type } = this.data.property;
  589. const arrangeLabel = arrangeTypeList.find((item) => item.value === arrange_type)?.label || '';
  590. this.data.mind_map.node_list = [
  591. {
  592. name: `${arrangeLabel}填空组件`,
  593. },
  594. ];
  595. },
  596. handleViewPinyin() {
  597. if (!this.isEnable(this.data.property.view_pinyin)) {
  598. return;
  599. }
  600. this.data.model_essay.forEach((item, i) => {
  601. item.forEach((option, j) => {
  602. const text = option.content;
  603. option.paragraph_list_parameter.text = text;
  604. this.createParsedTextInfoPinyin(text, i, j, 'model_essay');
  605. });
  606. });
  607. },
  608. openAddWord() {
  609. this.visibleWord = true;
  610. },
  611. /**
  612. * 添加词汇
  613. * @param {string} word 词汇内容
  614. */
  615. addWord(word) {
  616. if (!word) return;
  617. this.data.word_list.push({
  618. content: word,
  619. mark: getRandomNumber(),
  620. });
  621. },
  622. removeWord(index) {
  623. this.data.word_list.splice(index, 1);
  624. },
  625. getBodyStyles() {
  626. if (!this.$refs.richText) return {};
  627. return this.$refs.richText.getBodyInitialStyles();
  628. },
  629. },
  630. };
  631. </script>
  632. <style lang="scss" scoped>
  633. .fill-wrapper {
  634. display: flex;
  635. flex-direction: column;
  636. row-gap: 16px;
  637. align-items: flex-start;
  638. :deep .rich-wrapper {
  639. width: 100%;
  640. }
  641. .select-vocabulary {
  642. .title {
  643. margin: 0 0 8px;
  644. }
  645. .word-list {
  646. display: flex;
  647. flex-wrap: wrap;
  648. gap: 8px;
  649. margin-top: 8px;
  650. .word-item {
  651. display: flex;
  652. gap: 6px;
  653. align-items: center;
  654. padding: 4px 6px;
  655. border: $border;
  656. border-radius: 4px;
  657. :deep p {
  658. margin: 0;
  659. }
  660. .delete-word {
  661. display: flex;
  662. align-items: center;
  663. padding: 0;
  664. line-height: 1;
  665. }
  666. }
  667. }
  668. }
  669. .tips {
  670. font-size: 12px;
  671. color: #999;
  672. }
  673. .auto-matic,
  674. .upload-audio-play {
  675. :deep .upload-wrapper {
  676. margin-top: 0;
  677. }
  678. .audio-wrapper {
  679. :deep .audio-play {
  680. width: 16px;
  681. height: 16px;
  682. color: #000;
  683. background-color: initial;
  684. }
  685. :deep .audio-play.not-url {
  686. color: #a1a1a1;
  687. }
  688. :deep .voice-play {
  689. width: 16px;
  690. height: 16px;
  691. }
  692. }
  693. }
  694. .auto-matic {
  695. display: flex;
  696. flex-shrink: 0;
  697. column-gap: 12px;
  698. align-items: center;
  699. width: 200px;
  700. padding: 5px 12px;
  701. background-color: $fill-color;
  702. border-radius: 2px;
  703. .auto-btn {
  704. font-size: 16px;
  705. font-weight: 400;
  706. line-height: 22px;
  707. color: #1d2129;
  708. cursor: pointer;
  709. }
  710. }
  711. .correct-answer {
  712. display: flex;
  713. flex-wrap: wrap;
  714. gap: 8px;
  715. .el-input {
  716. width: 180px;
  717. :deep &__prefix {
  718. display: flex;
  719. align-items: center;
  720. color: $text-color;
  721. }
  722. }
  723. }
  724. }
  725. .pinyin-text-list {
  726. display: inline;
  727. :deep .pinyin-area {
  728. display: inline;
  729. }
  730. :deep .pinyin-area .rich-text-container,
  731. :deep .pinyin-area .pinyin-paragraph {
  732. display: inline;
  733. }
  734. }
  735. </style>