| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404 |
- <template>
- <ModuleBase :type="data.type">
- <template #content>
- <!-- eslint-disable max-len -->
- <div class="fill-wrapper">
- <RichText
- ref="richText"
- v-model="data.content"
- :is-fill="true"
- toolbar="fontselect fontsizeselect forecolor backcolor | underline | bold italic strikethrough alignleft aligncenter alignright"
- :wordlimit-num="false"
- :font-size="data?.unified_attrib?.font_size"
- :font-family="data?.unified_attrib?.font"
- />
- <el-input
- v-if="data.property.fill_type === fillTypeList[1].value"
- v-model="data.vocabulary"
- type="textarea"
- :autosize="{ minRows: 2, maxRows: 4 }"
- resize="none"
- placeholder="请输入词汇,用于选词填空"
- />
- <span class="tips">在需要加空的内容处插入 3 个或以上的下划线“_”。</span>
- <div v-if="data.audio_file_id">
- <SoundRecord :wav-blob.sync="data.audio_file_id" />
- </div>
- <template v-else>
- <div :class="['upload-audio-play']">
- <UploadAudio
- v-if="data.property.audio_generation_method === 'upload'"
- :file-id="data.audio_file_id"
- :show-upload="!data.audio_file_id"
- @upload="uploads"
- @deleteFile="deleteFiles"
- />
- <div v-else-if="data.property.audio_generation_method === 'auto'" class="auto-matic" @click="handleMatic">
- <SvgIcon icon-class="voiceprint-line" class="record" />
- <span class="auto-btn">{{ data.audio_file_id ? '已生成' : '生成音频' }}</span
- >{{ data.audio_file_id ? '成功' : '' }}
- </div>
- <SoundRecord v-else :wav-blob.sync="data.audio_file_id" />
- </div>
- </template>
- <div>
- <el-button @click="identifyText">识别</el-button>
- <el-button @click="openMultilingual">多语言</el-button>
- </div>
- <div class="correct-answer">
- <el-input
- v-for="(item, i) in data.answer.answer_list.filter(({ type }) => type === 'any_one')"
- :key="item.mark"
- v-model="item.value"
- @blur="handleTone(item.value, i)"
- >
- <span slot="prefix">{{ i + 1 }}.</span>
- </el-input>
- </div>
- </div>
- <el-divider v-if="isEnable(data.property.view_pinyin)" content-position="left">拼音效果</el-divider>
- <template v-if="isEnable(data.property.view_pinyin)">
- <div v-for="(item, i) in data.model_essay" :key="i" class="pinyin-text-list">
- <template v-for="(li, j) in item">
- <PinyinText
- v-if="li.type === 'text'"
- :key="`${i}-${j}`"
- ref="PinyinText"
- :paragraph-list="li.paragraph_list"
- :pinyin-position="data.property.pinyin_position"
- @fillCorrectPinyin="fillCorrectPinyin($event, i, j, 'model_essay')"
- />
- </template>
- </div>
- </template>
- <MultilingualFill
- :visible.sync="multilingualVisible"
- :text="data.content"
- :translations="data.multilingual"
- @SubmitTranslation="handleMultilingualTranslation"
- />
- </template>
- </ModuleBase>
- </template>
- <script>
- import ModuleMixin from '../../common/ModuleMixin';
- import SoundRecord from '@/views/book/courseware/create/components/question/fill/components/SoundRecord.vue';
- import UploadAudio from '@/views/book/courseware/create/components/question/fill/components/UploadAudio.vue';
- import PinyinText from '@/components/PinyinText.vue';
- import { getFillData, arrangeTypeList, fillFontList, fillTypeList } from '@/views/book/courseware/data/fill';
- import { addTone, handleToneValue } from '@/views/book/courseware/data/common';
- import { getRandomNumber } from '@/utils';
- import { GetStaticResources } from '@/api/app';
- export default {
- name: 'FillPage',
- components: {
- SoundRecord,
- UploadAudio,
- PinyinText,
- },
- mixins: [ModuleMixin],
- data() {
- return {
- data: getFillData(),
- fillTypeList,
- };
- },
- watch: {
- 'data.property.arrange_type': 'handleMindMap',
- 'data.property.fill_font': 'handleMindMap',
- 'data.vocabulary': {
- handler(val) {
- if (!val) return;
- this.data.word_list = val
- .split(/[\n\r]+/)
- .map((item) => item.split(/\s+/).filter((s) => s))
- .flat()
- .map((content) => {
- return {
- content,
- mark: getRandomNumber(),
- };
- });
- },
- },
- 'data.property': {
- handler() {
- this.handleViewPinyin();
- },
- deep: true,
- },
- },
- methods: {
- // 识别文本
- identifyText() {
- this.data.model_essay = [];
- this.data.answer.answer_list = [];
- this.data.content
- .split(/<(p|div)[^>]*>(.*?)<\/(p|div)>/g)
- .filter((s) => s && !s.match(/^(p|div)$/))
- .forEach((item) => {
- if (item.charCodeAt() === 10) return;
- let str = item
- // 去除所有的 font-size 样式
- .replace(/font-size:\s*\d+(\.\d+)?px;/gi, '')
- // 匹配 class 名为 rich-fill 的 span 标签和三个以上的_,并将它们组成数组
- .replace(/<span class="rich-fill".*?>(.*?)<\/span>|([_]{3,})/gi, '###$1$2###');
- this.data.model_essay.push(this.splitRichText(str));
- });
- this.handleViewPinyin();
- },
- /**
- * 分割富文本
- * @param {String} str 富文本字符串
- * @returns {Array} 分割后的数组
- */
- splitRichText(str) {
- const parts = String(str).split(/###/g);
- const arr = [];
- for (let i = 0; i < parts.length; i++) {
- let content = parts[i] ?? '';
- // 偶数索引为普通文本段
- if (i % 2 === 0) {
- if (content === '') continue; // 跳过空文本块
- // 判断 content 最前面是否是标签
- const isStartWithTag = /^<[^>]+>/.test(content);
- if (!isStartWithTag) {
- content = this.setTag(i, parts, content);
- }
- arr.push({
- content,
- type: 'text',
- paragraph_list: [],
- paragraph_list_parameter: {
- text: '',
- pinyin_proofread_word_list: [],
- },
- });
- continue;
- }
- // 奇数索引为输入段(被 ### 包裹的内容)
- const isUnderline = /^_{3,}$/.test(content);
- const mark = getRandomNumber();
- arr.push({
- content: isUnderline ? '' : content,
- type: 'input',
- audio_answer_list: [],
- mark,
- });
- // 同步更新答案列表
- this.data.answer.answer_list.push({
- value: isUnderline ? '' : content,
- mark,
- type: isUnderline ? 'any_one' : 'only_one',
- });
- // 将 content 设置为空,为预览准备
- arr[arr.length - 1].content = '';
- }
- return arr;
- },
- /**
- * 设置前一个标签
- * @param {Number} index 当前索引
- * @param {Array} parts 分割后的数组
- * @param {String} content 当前内容
- * @returns {String} 包含前一个标签的内容
- */
- setTag(index, parts, content) {
- let _content = content;
- const isEndWithTag = /<\/[^>]+>$/.test(_content);
- let startTag = '';
- for (let j = index - 1; j >= 0; j--) {
- const prev = parts[j] ?? '';
- const m = prev.match(/^<[^>]+>/);
- if (m) {
- startTag = m[0];
- }
- }
- _content = `${startTag}${_content}`;
- if (!isEndWithTag) {
- let tag = startTag.match(/^<([^>\s]+).*?>/);
- tag = tag ? tag[1] : 'span';
- _content += `</${tag}>`;
- }
- return _content;
- },
- handleTone(value, i) {
- if (!/^[a-zA-Z0-9\s]+$/.test(value)) return;
- this.data.answer.answer_list[i].value = value
- .trim()
- .split(/\s+/)
- .map((item) => {
- return handleToneValue(item);
- })
- .map((item) =>
- item.map(({ number, con }) => (number && con ? addTone(Number(number), con) : number || con || '')),
- )
- .filter((item) => item.length > 0)
- .join(' ');
- },
- uploads(file_id) {
- this.data.audio_file_id = file_id;
- },
- deleteFiles() {
- this.data.audio_file_id = '';
- },
- // 自动生成音频
- handleMatic() {
- GetStaticResources('tool-TextToVoiceFile', {
- text: this.data.content.replace(/<[^>]+>/g, ''),
- })
- .then(({ status, file_id }) => {
- if (status === 1) {
- this.data.audio_file_id = file_id;
- }
- })
- .catch(() => {});
- },
- /**
- * @description 处理思维导图数据
- */
- handleMindMap() {
- const { fill_font, arrange_type } = this.data.property;
- const fontLabel = fillFontList.find((item) => item.value === fill_font)?.label || '';
- const arrangeLabel = arrangeTypeList.find((item) => item.value === arrange_type)?.label || '';
- this.data.mind_map.node_list = [
- {
- name: `${arrangeLabel}${fontLabel}填空组件`,
- },
- ];
- },
- handleViewPinyin() {
- if (!this.isEnable(this.data.property.view_pinyin)) {
- this.data.model_essay.forEach((item) => {
- item.forEach((option) => {
- option.paragraph_list = [];
- option.paragraph_list_parameter = {
- text: '',
- pinyin_proofread_word_list: [],
- };
- });
- });
- return;
- }
- if (
- this.data.model_essay.length > 0 &&
- this.data.model_essay[0].length > 0 &&
- this.data.model_essay[0][0].paragraph_list.length > 0
- ) {
- return;
- }
- this.data.model_essay.forEach((item, i) => {
- item.forEach((option, j) => {
- const text = option.content.replace(/<[^>]+>/g, '');
- if (!text) return;
- option.paragraph_list_parameter.text = text;
- this.createParsedTextInfoPinyin(text, i, j, 'model_essay');
- });
- });
- },
- },
- };
- </script>
- <style lang="scss" scoped>
- .fill-wrapper {
- display: flex;
- flex-direction: column;
- row-gap: 16px;
- align-items: flex-start;
- :deep .rich-wrapper {
- width: 100%;
- }
- .tips {
- font-size: 12px;
- color: #999;
- }
- .auto-matic,
- .upload-audio-play {
- :deep .upload-wrapper {
- margin-top: 0;
- }
- .audio-wrapper {
- :deep .audio-play {
- width: 16px;
- height: 16px;
- color: #000;
- background-color: initial;
- }
- :deep .audio-play.not-url {
- color: #a1a1a1;
- }
- :deep .voice-play {
- width: 16px;
- height: 16px;
- }
- }
- }
- .auto-matic {
- display: flex;
- flex-shrink: 0;
- column-gap: 12px;
- align-items: center;
- width: 200px;
- padding: 5px 12px;
- background-color: $fill-color;
- border-radius: 2px;
- .auto-btn {
- font-size: 16px;
- font-weight: 400;
- line-height: 22px;
- color: #1d2129;
- cursor: pointer;
- }
- }
- .correct-answer {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- .el-input {
- width: 180px;
- :deep &__prefix {
- display: flex;
- align-items: center;
- color: $text-color;
- }
- }
- }
- }
- .pinyin-text-list {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- }
- </style>
|