| | |
| | | <span v-if="saveTime">{{ saveTime }} {{ t('courseCenter.saved') }}</span> |
| | | <el-button size="small" @click="saveSubmit('save')">{{ t('common.save') }}</el-button> |
| | | <el-button type="primary" size="small" @click="saveSubmit('')">{{ |
| | | t('courseCenter.composeViode') |
| | | }}</el-button> |
| | | t('courseCenter.composeViode') |
| | | }}</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="template-main"> |
| | | <div class="template-box template-left"> |
| | | <div class="page"> |
| | | <div |
| | | >{{ t('courseCenter.page') }}:({{ PPTArr ? PPTArr.length : 1 }}){{ |
| | | >{{ t('courseCenter.page') }}:({{ PPTArr ? PPTArr.length : 1 }}){{ |
| | | t('courseCenter.pageTitle') |
| | | }}</div |
| | | > |
| | |
| | | /> |
| | | <!-- 数字人(有则显示) --> |
| | | <el-image |
| | | v-if="element.showDigitalHuman" |
| | | v-if="element.digitalHuman?.show && element.digitalHuman?.host" |
| | | class="host" |
| | | :style="{ |
| | | width: PPTpositon.w * (thumViewSize.width / 800) + 'px', |
| | | height: PPTpositon.h * (thumViewSize.height / 450) + 'px', |
| | | top: PPTpositon.y * (thumViewSize.width / 800) + 'px', |
| | | left: PPTpositon.x * (thumViewSize.height / 450) + 'px' |
| | | width: element.digitalHuman.w * (thumViewSize.width / 800) + 'px', |
| | | height: element.digitalHuman.h * (thumViewSize.height / 450) + 'px', |
| | | top: element.digitalHuman.y * (thumViewSize.width / 800) + 'px', |
| | | left: element.digitalHuman.x * (thumViewSize.height / 450) + 'px' |
| | | }" |
| | | :src="selectHost?.pictureUrl" |
| | | :src="element.digitalHuman.host.pictureUrl" |
| | | /> |
| | | <div class="list-index" :style="element.isActive ? 'background: #409eff' : ''"> |
| | | {{ index + 1 }} |
| | |
| | | </div> |
| | | </div> |
| | | <div class="left-upload-setting" v-if="!showLeftList"> |
| | | <!-- <img src="" alt=""> --> |
| | | <div>ppt{{ t('courseCenter.analyzing') }}...</div> |
| | | <el-progress :percentage="percentagePPT" /> |
| | | <el-button @click="cancelAnalyze">{{ t('common.cancel') }}</el-button> |
| | |
| | | class="main-image-box" |
| | | :style="{ width: viewSize.width + 'px', height: viewSize.height + 'px',position: 'relative' }" |
| | | > |
| | | |
| | | <!-- 背景(必显示) --> |
| | | <el-image |
| | | v-show="selectPPT.pictureUrl && selectPPT.showDigitalHuman==false" |
| | | v-show="selectPPT.pictureUrl && selectPPT.digitalHuman.show==0" |
| | | class="background1" |
| | | :src="selectPPT.pictureUrl" |
| | | style="z-index: 2" |
| | | /> |
| | | <el-image |
| | | v-show="selectPPT.pictureUrl && selectPPT.showDigitalHuman==true" |
| | | v-show="selectPPT.pictureUrl && selectPPT.digitalHuman.show==1" |
| | | class="background1" |
| | | :src="selectPPT.pictureUrl" |
| | | style="z-index: 1" |
| | | /> |
| | | |
| | | <!-- 画中画 --> |
| | | <Vue3DraggableResizable |
| | | v-if="selectPPT.innerPicture && selectPPT.innerPicture.src" |
| | |
| | | > |
| | | <el-image class="ppt-bg" :src="selectPPT.innerPicture.src" /> |
| | | <el-icon |
| | | v-if="PPTpositon.active" |
| | | v-if="selectPPT.innerPicture.active" |
| | | size="20" |
| | | color="#409eff" |
| | | style="position: absolute; top: 5px; right: 5px; z-index: 4" |
| | |
| | | <Delete /> |
| | | </el-icon> |
| | | </Vue3DraggableResizable> |
| | | <Vue3DraggableResizable |
| | | v-if="selectPPT.showDigitalHuman==true" |
| | | :parent="false" |
| | | :lockAspectRatio="true" |
| | | :minW="350" |
| | | :initW="PPTpositon.w" |
| | | :initH="PPTpositon.h" |
| | | @drag-move="onDragMove" |
| | | v-model:x="PPTpositon.x" |
| | | v-model:y="PPTpositon.y" |
| | | v-model:w="PPTpositon.w" |
| | | v-model:h="PPTpositon.h" |
| | | v-model:active="PPTpositon.active" |
| | | :draggable="true" |
| | | :resizable="true" |
| | | @activated="print('activated')" |
| | | @deactivated="print('deactivated')" |
| | | @drag-start="print('drag-start')" |
| | | @resize-start="print('resize-start')" |
| | | @dragging="print('dragging')" |
| | | @resizing="print('resizing')" |
| | | @drag-end="print('drag-end')" |
| | | @resize-end="print('resize-end')" |
| | | style="z-index: 4" |
| | | > |
| | | <!-- {{PPTpositon.w}}{{PPTpositon.h}}--> |
| | | <el-image |
| | | class="minddle-host-image" |
| | | :src="selectHost ? selectHost.pictureUrl : ''" |
| | | /> |
| | | |
| | | <el-icon |
| | | v-if="PPTpositon.active" |
| | | size="20" |
| | | color="#409eff" |
| | | style="position: absolute; top: 5px; right: 5px; z-index: 4" |
| | | @click.stop="deleteDigitalHuman" |
| | | > |
| | | <Delete /> |
| | | </el-icon> |
| | | </Vue3DraggableResizable> |
| | | <!-- 数字人 --> |
| | | <Vue3DraggableResizable |
| | | v-if="selectPPT.showDigitalHuman==false" |
| | | v-if="selectPPT.digitalHuman.show==1 && selectPPT.digitalHuman?.host" |
| | | :parent="false" |
| | | :lockAspectRatio="true" |
| | | :minW="350" |
| | | :initW="PPTpositon.w" |
| | | :initH="PPTpositon.h" |
| | | :initW="selectPPT.digitalHuman.w" |
| | | :initH="selectPPT.digitalHuman.h" |
| | | @drag-move="onDragMove" |
| | | v-model:x="PPTpositon.x" |
| | | v-model:y="PPTpositon.y" |
| | | v-model:w="PPTpositon.w" |
| | | v-model:h="PPTpositon.h" |
| | | v-model:active="PPTpositon.active" |
| | | v-model:x="selectPPT.digitalHuman.x" |
| | | v-model:y="selectPPT.digitalHuman.y" |
| | | v-model:w="selectPPT.digitalHuman.w" |
| | | v-model:h="selectPPT.digitalHuman.h" |
| | | v-model:active="selectPPT.digitalHuman.active" |
| | | :draggable="true" |
| | | :resizable="true" |
| | | @activated="print('activated')" |
| | | @deactivated="print('deactivated')" |
| | | @drag-start="print('drag-start')" |
| | | @resize-start="print('resize-start')" |
| | | @dragging="print('dragging')" |
| | | @resizing="print('resizing')" |
| | | @drag-end="print('drag-end')" |
| | | @resize-end="print('resize-end')" |
| | | style="z-index: 4" |
| | | > |
| | | <el-image |
| | | class="minddle-host-image" |
| | | :src="selectPPT.digitalHuman.host.pictureUrl" |
| | | /> |
| | | <el-icon |
| | | v-if="selectPPT.digitalHuman.active" |
| | | size="20" |
| | | color="#409eff" |
| | | style="position: absolute; top: 5px; right: 5px; z-index: 4" |
| | | @click.stop="deleteDigitalHuman" |
| | | > |
| | | <Delete /> |
| | | </el-icon> |
| | | </Vue3DraggableResizable> |
| | | <Vue3DraggableResizable |
| | | v-if="selectPPT.digitalHuman.show==0 && selectPPT.digitalHuman?.host" |
| | | :parent="false" |
| | | :lockAspectRatio="true" |
| | | :minW="350" |
| | | :initW="selectPPT.digitalHuman.w" |
| | | :initH="selectPPT.digitalHuman.h" |
| | | @drag-move="onDragMove" |
| | | v-model:x="selectPPT.digitalHuman.x" |
| | | v-model:y="selectPPT.digitalHuman.y" |
| | | v-model:w="selectPPT.digitalHuman.w" |
| | | v-model:h="selectPPT.digitalHuman.h" |
| | | v-model:active="selectPPT.digitalHuman.active" |
| | | :draggable="true" |
| | | :resizable="true" |
| | | @activated="print('activated')" |
| | |
| | | @resize-end="print('resize-end')" |
| | | style="z-index: 1" |
| | | > |
| | | <!-- {{PPTpositon.w}}{{PPTpositon.h}}--> |
| | | <el-image |
| | | class="minddle-host-image" |
| | | :src="selectHost ? selectHost.pictureUrl : ''" |
| | | :src="selectPPT.digitalHuman.host.pictureUrl" |
| | | /> |
| | | |
| | | <el-icon |
| | | v-if="PPTpositon.active" |
| | | v-if="selectPPT.digitalHuman.active" |
| | | size="20" |
| | | color="#409eff" |
| | | style="position: absolute; top: 5px; right: 5px; z-index: 4" |
| | |
| | | </div> |
| | | <div class="voice-main"> |
| | | <el-text class="mx-1" type="primary" size="small">{{ |
| | | t('courseCenter.oralBroadcastingContent') |
| | | }}</el-text> |
| | | <!-- <div class="voice-item">--> |
| | | <!-- <span--> |
| | | <!-- :class="selectPPT.driverType == item.itemValue ? 'active-item' : ''"--> |
| | | <!-- v-for="(item, index) in driveType"--> |
| | | <!-- :key="index"--> |
| | | <!-- @click="driveTypeChange(item)"--> |
| | | <!-- >{{ item.name }}</span--> |
| | | <!-- >--> |
| | | <!-- </div>--> |
| | | t('courseCenter.oralBroadcastingContent') |
| | | }}</el-text> |
| | | <div class="media-box"> |
| | | <el-button type="primary" :icon="Mic" size="small" @click="openSelect">{{ |
| | | selectPPT.selectAudio ? selectPPT.selectAudio.name : t('courseCenter.notSelect') |
| | | }}</el-button> |
| | | selectPPT.selectAudio ? selectPPT.selectAudio.name : t('courseCenter.notSelect') |
| | | }}</el-button> |
| | | <el-button |
| | | type="success" |
| | | :icon="Headset" |
| | |
| | | </div> |
| | | <div v-if="selectPPT.driverType == 1" style="position: relative"> |
| | | <div class="middle-textarea"> |
| | | <!-- <el-input |
| | | v-model="selectPPT.pptRemark" |
| | | ref="textareaRef" |
| | | @select="handlePptRemarkSelection" |
| | | :rows="5" |
| | | type="textarea" |
| | | :placeholder="t('common.inputText') + t('courseCenter.oralBroadcastingContent')" |
| | | show-word-limit |
| | | maxlength="1200" |
| | | resize="none" |
| | | /> --> |
| | | <Editor style="height: 196px; overflow-y: hidden;" v-model="selectPPT.pptRemark" :defaultConfig="editorConfig" mode="simple" @on-created="handleCreated" /> |
| | | </div> |
| | | <div class="tool-box"> |
| | | <div class="tool-btn"> |
| | | <!-- 新增智能讲稿按钮 --> |
| | | <!-- <el-button type="primary" size="small" @click="openScriptRewriter">{{--> |
| | | <!-- t('courseCenter.intelligentSpeech')--> |
| | | <!-- }}</el-button>--> |
| | | <!-- <el-button type="primary" @click="openReplaceDialog" size="small">{{--> |
| | | <!-- t('courseCenter.batchReplace')--> |
| | | <!-- }}</el-button>--> |
| | | <!-- <el-button type="primary" size="small" @click="handleWord">{{--> |
| | | <!-- t('courseCenter.polyphonicCharacters')--> |
| | | <!-- }}</el-button>--> |
| | | <!-- <el-dropdown placement="bottom" @command="handleNumber" style="margin: 0 12px;">--> |
| | | <!-- <el-button type="primary" size="small">{{ t('courseCenter.number') }}</el-button>--> |
| | | <!-- <template #dropdown>--> |
| | | <!-- <el-dropdown-menu>--> |
| | | <!-- <el-dropdown-item command="读数字">读数字</el-dropdown-item>--> |
| | | <!-- <el-dropdown-item command="读数值">读数值</el-dropdown-item>--> |
| | | <!-- </el-dropdown-menu>--> |
| | | <!-- </template>--> |
| | | <!-- </el-dropdown>--> |
| | | <!-- <el-dropdown placement="bottom" @command="handleBreak">--> |
| | | <!-- <el-button type="primary" size="small">{{ t('courseCenter.pause') }}</el-button>--> |
| | | <!-- <template #dropdown>--> |
| | | <!-- <el-dropdown-menu>--> |
| | | <!-- <el-dropdown-item command="0.5秒">0.5秒</el-dropdown-item>--> |
| | | <!-- <el-dropdown-item command="1秒">1秒</el-dropdown-item>--> |
| | | <!-- <el-dropdown-item command="2秒">2秒</el-dropdown-item>--> |
| | | <!-- </el-dropdown-menu>--> |
| | | <!-- </template>--> |
| | | <!-- </el-dropdown>--> |
| | | <div></div> |
| | | </div> |
| | | <el-button type="primary" :icon="VideoPlay" size="small" @click="createAudio">{{ |
| | | t('courseCenter.tryListening') |
| | | }}</el-button> |
| | | t('courseCenter.tryListening') |
| | | }}</el-button> |
| | | </div> |
| | | <div class="audio-play" v-if="showAudioPlay"> |
| | | <div>{{ t('courseCenter.listeningTrial') }}...</div> |
| | |
| | | > |
| | | <template #trigger> |
| | | <el-button type="primary" :icon="Upload">{{ |
| | | t('courseCenter.uploadAudio') |
| | | }}</el-button> |
| | | t('courseCenter.uploadAudio') |
| | | }}</el-button> |
| | | </template> |
| | | </el-upload> |
| | | </el-tooltip> |
| | |
| | | class="template-item" |
| | | v-for="(template, index) in templates" |
| | | :key="index" |
| | | @click="chooseTemplate(template)" |
| | | @click="handleTemplateSelection(template)" |
| | | > |
| | | <div class="list-index" :style="template.isActive ? 'background: #409eff' : ''"> |
| | | {{ index + 1 }} |
| | |
| | | <!-- 背景设置 --> |
| | | <div class="template-box template-right" v-if="showHeadImageTool"> |
| | | <div class="image-setting"> |
| | | <!-- 上传图片成功后,将当前场景的背景修改为上传的图片url--> |
| | | |
| | | <div>{{ t('courseCenter.uploadImage') }}</div> |
| | | <UploadImg v-model="selectPPT.pictureUrl" :limit="1" /> |
| | | </div> |
| | |
| | | <!-- 画中画设置 --> |
| | | <div class="template-box template-right" v-if="showInnerPictureTool"> |
| | | <div class="image-setting"> |
| | | <!-- 上传图片成功后,将当前场景的画中画修改为上传的图片url--> |
| | | |
| | | <div>{{ t('courseCenter.uploadImage') }}</div> |
| | | <UploadImg v-model="selectPPT.innerPicture.src" :limit="1" /> |
| | | </div> |
| | | </div> |
| | | <div class="template-box template-right" v-if="showImageSet"> |
| | | <div class="image-setting"> |
| | | <div>{{ t('courseCenter.imageProperties') }}</div> |
| | | <div class="img-setting"> |
| | | <span class="setting-label">{{ t('courseCenter.position') }}</span> |
| | | X <el-input v-model="PPTpositon.x" type="number" :min="20" :max="460" /> Y |
| | | <el-input v-model="PPTpositon.y" type="number" :min="20" :max="180" /> |
| | | </div> |
| | | <div class="img-setting"> |
| | | <span class="setting-label">{{ t('courseCenter.hierarchicalStructure') }}</span> |
| | | <el-input v-model="PPTpositon.depth" type="number" :min="0" :max="999" /> |
| | | </div> |
| | | <div class="img-setting"> |
| | | <span class="setting-label">{{ t('courseCenter.size') }}</span> |
| | | W <el-input v-model="PPTpositon.w" type="number" :min="20" :max="760" /> H |
| | | <el-input v-model="PPTpositon.h" type="number" :min="20" :max="360" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="template-box template-tool"> |
| | |
| | | <AudioSelect ref="audioSelect" @success="selectAudio" /> |
| | | <mergeWarningDialog ref="warningDialog" /> |
| | | <ReplaceDialog ref="replaceDialog" :ppt-arr="PPTArr" @submit="handleReplacement" /> |
| | | <!-- 添加智能讲稿组件 --> |
| | | <Rewriter |
| | | ref="rewriterRef" |
| | | :image-url="currentImageUrl" |
| | | :title="''" |
| | | :content="selectPPT.pptRemark" |
| | | @confirm="handleRewritten" |
| | | /> |
| | | <!-- 多音字 --> |
| | | <el-dialog v-model="dialogVisible" title="点击需要纠正的多音字,选择正确的发音" width="500" @close="dialogVisible = false"> |
| | | <!-- 多音字 --> |
| | | <el-dialog v-model="dialogVisible" title="点击需要纠正的多音字,选择正确的发音" width="500" @close="dialogVisible = false"> |
| | | <el-tag v-for="(item, index) in textList" :key="index" type="primary" effect="dark" style="margin-right: 10px;cursor: pointer;" @click="handleTag(item)"> |
| | | {{ item }} |
| | | {{ item }} |
| | | </el-tag> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { ref, reactive, onMounted } from 'vue' |
| | | import { ref, reactive, onMounted, computed, watch, nextTick, shallowRef } from 'vue' |
| | | import draggable from 'vuedraggable' |
| | | //引入人脸识别方法 |
| | | import { useFaceDetection } from '@/utils/HaveFace' |
| | | import Vue3DraggableResizable from 'vue3-draggable-resizable' |
| | | import 'vue3-draggable-resizable/dist/Vue3DraggableResizable.css' |
| | | import { useFaceDetection } from '@/utils/HaveFace' |
| | | import { config } from '@/config/axios/config' |
| | | import { genFileId } from 'element-plus' |
| | | import type { UploadRawFile } from 'element-plus' |
| | |
| | | import uploadExplain from './uploadExplain.vue' |
| | | import AudioSelect from './audioSelect.vue' |
| | | import mergeWarningDialog from './mergeWarningDialog.vue' |
| | | import ReplaceDialog from './replaceDialog.vue' // 引入批量替换组件 |
| | | import Rewriter from './rewriter.vue' |
| | | import ReplaceDialog from './replaceDialog.vue' |
| | | import template from '@/assets/imgs/template.png' |
| | | import templateActive from '@/assets/imgs/template-active.png' |
| | | import user from '@/assets/imgs/user.png' |
| | |
| | | import innerPicture from '@/assets/imgs/inner-picture.png' |
| | | import innerPictureActive from '@/assets/imgs/inner-picture-active.png' |
| | | import { TemplateApi } from '@/api/digitalcourse/template' |
| | | const { t } = useI18n() // 国际化 |
| | | //用户信息 |
| | | const { t } = useI18n() |
| | | import { useUserStore } from '@/store/modules/user' |
| | | import { |
| | | Edit, |
| | |
| | | Mic, |
| | | Headset, |
| | | Delete, |
| | | QuestionFilled, |
| | | VideoPlay, |
| | | CopyDocument |
| | | } from '@element-plus/icons-vue' |
| | | import { generateUUID } from '@/utils' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { cloneDeep } from 'lodash-es' |
| | | // 富文本编辑器 |
| | | import '@wangeditor/editor/dist/css/style.css' // 引入 css |
| | | import { Editor } from '@wangeditor/editor-for-vue' |
| | | import '@wangeditor/editor/dist/css/style.css' |
| | | import { Boot } from '@wangeditor/editor' |
| | | import TitleBlack from './title-black/index.js'; // 这个路径根据你的配置来 |
| | | import TitleBlack from './title-black/index.js' |
| | | Boot.registerModule(TitleBlack) |
| | | //多音字 |
| | | import { polyphonic } from 'pinyin-pro'; |
| | | //编辑器内容转换ssml |
| | | import { useEditorHtml } from '@/hooks/web/useEditorHtml'; |
| | | import {coursesDelete} from "@/api/pptTemplate"; |
| | | import { polyphonic } from 'pinyin-pro' |
| | | import { useEditorHtml } from '@/hooks/web/useEditorHtml' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | |
| | | const editorHtml = useEditorHtml() |
| | | const router = useRouter() // 路由 |
| | | const route = useRoute() // |
| | | //用户信息 |
| | | const router = useRouter() |
| | | const route = useRoute() |
| | | const userStore = useUserStore() |
| | | const userId = computed(() => userStore.user.id) |
| | | const message = useMessage() |
| | | |
| | | const isEditing = ref(false) |
| | | const inputRef = ref(null) |
| | | const editName = ref('') |
| | | |
| | | //当前是否存在人脸 |
| | | const IsHaveFace = ref(false) |
| | | //当前是否完成ppt人脸校验 |
| | |
| | | nextTick(() => { |
| | | inputRef.value.focus() |
| | | }) |
| | | }; |
| | | const onDragMove = (evt, data) => { |
| | | console.log(evt) |
| | | console.log(data) |
| | | } |
| | | |
| | | // 限制坐标 |
| | | if (data.x < -100) { |
| | | data.x = -100; // 可以设置最小坐标为 -100 |
| | | } |
| | | if (data.y < -100) { |
| | | data.y = -100; // 可以设置最小坐标为 -100 |
| | | } |
| | | }; |
| | | // 保存编辑后的名称 |
| | | const saveEdit = () => { |
| | | isEditing.value = false |
| | | courseInfo.value.name = editName.value |
| | | } |
| | | let humanId = 0 |
| | | const print = (val) => { |
| | | console.log(val) |
| | | } |
| | | const viewSize = reactive({ |
| | | width: 800, |
| | | height: (800 * 9) / 16 |
| | | }) |
| | | |
| | | //课程基本信息 |
| | | const thumViewSize = reactive({ |
| | | width: 152, |
| | | height: (152 * 9) / 16 |
| | | }) |
| | | |
| | | const digitalHumanSize = reactive({ |
| | | width: 640, |
| | | height: 360 |
| | | }) |
| | | |
| | | const scaleRatio = computed(() => ({ |
| | | width: courseInfo.value.width / viewSize.width, |
| | | height: courseInfo.value.height / viewSize.height |
| | | })) |
| | | |
| | | const courseInfo = ref({ |
| | | id: 0, |
| | | accountId: userId.value, |
| | |
| | | name: '未命名草稿', |
| | | duration: 0, |
| | | status: 0, |
| | | pageMode: 2,//ppt课件视频 |
| | | pageMode: 2, |
| | | matting: 1, |
| | | width: 1920, |
| | | height: 1080 |
| | | }) |
| | | // 当比例改变时更新宽度和高度 |
| | | |
| | | watch( |
| | | () => courseInfo.value.aspect, |
| | | (newAspect) => { |
| | |
| | | } |
| | | ) |
| | | |
| | | const editName = ref(courseInfo.value.name) |
| | | const viewSize = reactive({ |
| | | width: 800, |
| | | height: (800 * 9) / 16 |
| | | }) |
| | | const thumViewSize = reactive({ |
| | | width: 152, |
| | | height: (152 * 9) / 16 |
| | | }) |
| | | const digitalHumanSize = reactive({ |
| | | width: 640, |
| | | height: 360 |
| | | }) |
| | | // 添加缩放比例计算 |
| | | const scaleRatio = computed(() => ({ |
| | | width: courseInfo.value.width / viewSize.width, // 1920/800 = 2.4 |
| | | height: courseInfo.value.height / viewSize.height // 1080/450 = 2.4 |
| | | })) |
| | | |
| | | const PPTpositon = reactive({ |
| | | x: viewSize.width - digitalHumanSize.width, |
| | | y: viewSize.height - digitalHumanSize.height, |
| | | w: digitalHumanSize.width, |
| | | h: digitalHumanSize.height, |
| | | depth: 0, |
| | | active: false |
| | | }) |
| | | |
| | | const componentsInfo = reactive({ |
| | | width: PPTpositon.w / 5, |
| | | height: PPTpositon.h / 4, |
| | | marginLeft: PPTpositon.x / 4, |
| | | top: PPTpositon.y / 4.5, |
| | | depth: PPTpositon.depth |
| | | }) |
| | | //背景设置 |
| | | const showHeadImageTool = ref(false) |
| | | //数字人设置 |
| | | const showDigitalHumanTool = ref(false) |
| | | //模板设置 |
| | | const showTemplateTool = ref(false) |
| | | //画中画设置 |
| | | const showInnerPictureTool = ref(false) |
| | | //图片属性 |
| | | const showImageSet = ref(false) |
| | | //是否将模板应用到所有页面 |
| | | const applyAllTemplate = ref(false) |
| | | |
| | | const xScale = viewSize.width / thumViewSize.width |
| | | // const yScale = viewSize.height / thumViewSize.height |
| | | //左侧ppt数字人位置 |
| | | const leftWidth = computed(() => { |
| | | return PPTpositon.w / xScale + 'px' |
| | | }) |
| | | const leftHeight = computed(() => { |
| | | return PPTpositon.h / xScale + 'px' |
| | | }) |
| | | const leftTop = computed(() => { |
| | | return PPTpositon.y / xScale + 'px' |
| | | }) |
| | | const leftLeft = computed(() => { |
| | | return PPTpositon.x / xScale + 'px' |
| | | }) |
| | | const print = (val) => { |
| | | console.log(val) |
| | | } |
| | | const state = reactive({ |
| | | dragging: false |
| | | }) |
| | | |
| | | //预设模板 |
| | | const TEMPLATE_PRESETS = ref([]) |
| | | const templates = ref([]) |
| | | |
| | | const selectTemplate = ref([]) |
| | | |
| | | //数字人tab |
| | | const tabs1 = [ |
| | | { |
| | | itemName: t('courseCenter.model'), |
| | |
| | | itemValue: '2' |
| | | } |
| | | ] |
| | | |
| | | const tabs1Click = (item) => { |
| | | tabs1ActiveNum.value = item.itemValue |
| | | getList() |
| | | } |
| | | |
| | | const tabs2Click = (item) => { |
| | | tabs2ActiveNum.value = item.itemValue |
| | | getList() |
| | | } |
| | | |
| | | const tabs3Click = (item) => { |
| | | tabs3ActiveNum.value = item.itemValue |
| | | getList() |
| | | } |
| | | //驱动类型 |
| | | const selectDriveType = ref({ |
| | | name: t('courseCenter.textDriven'), |
| | | itemValue: 1, |
| | | isActive: true |
| | | }) |
| | | const driveType = reactive([ |
| | | { |
| | | name: t('courseCenter.textDriven'), |
| | | itemValue: 1, |
| | | isActive: true |
| | | }, |
| | | { |
| | | name: t('courseCenter.soundDriven'), |
| | | isActive: false, |
| | | itemValue: 2 |
| | | } |
| | | ]) |
| | | const driveTypeChange = (item) => { |
| | | selectPPT.value.driverType = item.itemValue |
| | | } |
| | | //右侧设置 |
| | | |
| | | const rightTools = reactive([ |
| | | { |
| | | name: t('courseCenter.template'), |
| | |
| | | isActive: false |
| | | } |
| | | ]) |
| | | |
| | | const showHeadImageTool = ref(false) |
| | | const showDigitalHumanTool = ref(false) |
| | | const showTemplateTool = ref(false) |
| | | const showInnerPictureTool = ref(false) |
| | | const applyAllTemplate = ref(false) |
| | | |
| | | const handleTemplateSelection = (template) => { |
| | | ElMessageBox.confirm( |
| | | '是否要将此模板应用到所有页面?', |
| | | '应用模板', |
| | | { |
| | | confirmButtonText: '应用到所有', |
| | | cancelButtonText: '仅当前页', |
| | | type: 'warning', |
| | | } |
| | | ).then(() => { |
| | | // 用户点击"应用到所有" |
| | | applyAllTemplate.value = true |
| | | chooseTemplate(template) |
| | | }).catch(() => { |
| | | // 用户点击"仅当前页"或关闭对话框 |
| | | applyAllTemplate.value = false |
| | | chooseTemplate(template) |
| | | }) |
| | | } |
| | | const handleChangeTool = (item) => { |
| | | rightTools.forEach((child) => { |
| | | if (child.name == item.name) { |
| | |
| | | child.isActive = false |
| | | } |
| | | }) |
| | | if (item.name == t('courseCenter.background')) { |
| | | showHeadImageTool.value = true |
| | | showTemplateTool.value = false |
| | | showDigitalHumanTool.value = false |
| | | showInnerPictureTool.value = false |
| | | } else if (item.name == t('courseCenter.digitalPeople')) { |
| | | showHeadImageTool.value = false |
| | | showTemplateTool.value = false |
| | | showDigitalHumanTool.value = true |
| | | showInnerPictureTool.value = false |
| | | } else if (item.name == t('courseCenter.template')) { |
| | | showHeadImageTool.value = false |
| | | showTemplateTool.value = true |
| | | showDigitalHumanTool.value = false |
| | | showInnerPictureTool.value = false |
| | | } else if (item.name == t('courseCenter.pictureInPicture')) { |
| | | showHeadImageTool.value = false |
| | | showTemplateTool.value = false |
| | | showDigitalHumanTool.value = false |
| | | showInnerPictureTool.value = true |
| | | } |
| | | |
| | | showHeadImageTool.value = item.name === t('courseCenter.background') |
| | | showTemplateTool.value = item.name === t('courseCenter.template') |
| | | showDigitalHumanTool.value = item.name === t('courseCenter.digitalPeople') |
| | | showInnerPictureTool.value = item.name === t('courseCenter.pictureInPicture') |
| | | } |
| | | |
| | | const PPTArr = ref() |
| | | //ppt解析进度 |
| | | const PPTArr = ref([]) |
| | | const percentagePPT = ref(0) |
| | | const showLeftList = ref(true) |
| | | |
| | | const selectPPT = ref({ |
| | | pictureUrl: '', |
| | | innerPicture: { |
| | | //定义画中画对象,属性与数字人相同 |
| | | name: '画中画', |
| | | src: '', |
| | | cover: '', |
| | |
| | | originWidth: 0, |
| | | originHeight: 0, |
| | | category: 1, |
| | | depth: 1, //画中画1-100 |
| | | depth: 1, |
| | | top: 0, |
| | | marginLeft: 0, |
| | | businessId: generateUUID(), |
| | |
| | | pptRemark: '', |
| | | driverType: 1, |
| | | uploadAudioUrl: '', |
| | | fileList: [] as any, |
| | | fileList: [], |
| | | selectAudio: { |
| | | id: '', |
| | | code: '', |
| | | name: '' |
| | | }, |
| | | showDigitalHuman: true |
| | | }) //选择的PPT |
| | | const checked5 = ref(false) |
| | | const options = [ |
| | | { |
| | | value: '1', |
| | | label: '16:9' |
| | | }, |
| | | { |
| | | value: '2', |
| | | label: '9:16' |
| | | digitalHuman: { |
| | | show: true, |
| | | x: viewSize.width - digitalHumanSize.width, |
| | | y: viewSize.height - digitalHumanSize.height, |
| | | w: digitalHumanSize.width, |
| | | h: digitalHumanSize.height, |
| | | active: false, |
| | | host: null |
| | | } |
| | | ] |
| | | }) |
| | | |
| | | const videoText = ref(0) |
| | | const videoDuration = ref('') |
| | | |
| | | watch( |
| | | () => PPTArr.value, |
| | | (val) => { |
| | | if (!val) return |
| | | |
| | | videoText.value = val.reduce((prev, curr) => { |
| | | if (!curr.pptRemark) return prev |
| | | const plainText = curr.pptRemark.replace(/<[^>]+>/g, '') |
| | | return prev + plainText.length |
| | | }, 0) |
| | | |
| | | let videoTime = (videoText.value / 200) * 60 |
| | | videoDuration.value = formateVideoTime(Math.ceil(videoTime)) |
| | | }, |
| | | { deep: true } |
| | | ) |
| | | |
| | | const formateVideoTime = (times: any) => { |
| | | let hours: any = parseInt(`${times / 60 / 60}`) |
| | | let restMinutes: any = parseInt(`${(times / 60) % 60}`) |
| | | let seconds: any = parseInt(`${times % 60}`) |
| | | |
| | | if (hours < 10) hours = '0' + hours |
| | | if (restMinutes < 10) restMinutes = '0' + restMinutes |
| | | if (seconds < 10) seconds = '0' + seconds |
| | | |
| | | return hours + ':' + restMinutes + ':' + seconds |
| | | } |
| | | |
| | | const uploadRef = ref() |
| | | const headers = { |
| | | Accept: 'application/json, text/plain, */*', |
| | | Authorization: 'Bearer ' + getAccessToken(), |
| | | 'tenant-id': getTenantId() |
| | | } |
| | | //上传文件对象 |
| | | |
| | | const uploadFileObj = reactive({ |
| | | filename: '', |
| | | size: 0, |
| | |
| | | extInfo: '{"addMode":true,"docType":1,"pptNotes":true,"pptContent":false,"notesPolish":false}', |
| | | resolveType: 1 |
| | | }) |
| | | |
| | | //智能讲稿组件begin |
| | | //添加ref |
| | | const rewriterRef = ref() |
| | |
| | | uploadRef.value!.handleStart(file) |
| | | } |
| | | |
| | | // 上传相关的处理函数 |
| | | const handleChange = (files) => { |
| | | // 获取文件扩展名 |
| | | const extension = files.name.split('.').pop().toLowerCase() |
| | | |
| | | // 设置文档类型 1:ppt 2:pdf |
| | | uploadFileObj.docType = extension === 'pdf' ? 2 : 1 |
| | | uploadFileObj.filename = files.name |
| | | uploadFileObj.size = files.size |
| | |
| | | console.error('Upload error:', err) |
| | | } |
| | | |
| | | //上传音频 |
| | | const uploadAudioRef = ref() |
| | | const handleAudioSuccess = (rawFile) => { |
| | | console.log('--------', rawFile) |
| | | message.success('上传成功!') |
| | | selectPPT.value.uploadAudioUrl = rawFile.data |
| | | // uploadAudioRef.value!.clearFiles(); |
| | | selectPPT.value.fileList = [ |
| | | { |
| | | name: uploadAudioFile.value, |
| | | url: rawFile.data |
| | | } |
| | | ] |
| | | } |
| | | const uploadAudioFile = ref() |
| | | const handleAudioChange = (files) => { |
| | | uploadAudioFile.value = files.name |
| | | } |
| | | //ppt上传说明回调 |
| | | const uploadSubmit = (uploadForm) => { |
| | | console.log('-------ppt上传说明', uploadForm) |
| | | pptTemplateApi.createPPT(uploadFileObj).then((res) => { |
| | | if (res) { |
| | | //将课程名称修改为附件名称 |
| | | courseInfo.value.name = uploadFileObj.filename.split('.').slice(0, -1).join('.') |
| | | editName.value = courseInfo.value.name |
| | | schedulePPT(res) |
| | | } |
| | | }) |
| | | } |
| | | //解析ppt |
| | | |
| | | const chooseTemplate = (currTemplate) => { |
| | | selectTemplate.value = cloneDeep(currTemplate) |
| | | templates.value.forEach((item) => { |
| | | item.isActive = false |
| | | }) |
| | | currTemplate.isActive = true |
| | | applyTemplate() |
| | | } |
| | | |
| | | const schedulePPTTimer = ref() |
| | | const schedulePPT = (id) => { |
| | | percentagePPT.value = 0 |
| | |
| | | pptTemplateApi.getSchedule(id).then((res) => { |
| | | if (res && typeof res == 'string') { |
| | | const progress = Number(res) |
| | | // 添加解析失败的判断 |
| | | if (progress < 0) { |
| | | clearInterval(schedulePPTTimer.value) |
| | | showLeftList.value = true |
| | |
| | | } |
| | | percentagePPT.value = parseInt(`${progress * 100}`) |
| | | } else if (res && res.length > 0) { |
| | | console.log('courseInfo', courseInfo.value) |
| | | res.forEach((item) => { |
| | | item.isActive = false |
| | | item.isChecked = false |
| | |
| | | item.businessId = generateUUID() |
| | | item.width = courseInfo.value.width |
| | | item.height = courseInfo.value.height |
| | | |
| | | // 初始化独立的数字人配置 |
| | | item.digitalHuman = { |
| | | show: true, |
| | | x: viewSize.width - digitalHumanSize.width, |
| | | y: viewSize.height - digitalHumanSize.height, |
| | | w: digitalHumanSize.width, |
| | | h: digitalHumanSize.height, |
| | | active: false, |
| | | host: selectHost.value |
| | | } |
| | | |
| | | let ppturl = item.pictureUrl |
| | | //是否展示背景 如果展示背景,则背景等于模板背景,否则为空 |
| | | if (selectTemplate.value.showBackground) { |
| | | item.pictureUrl = selectTemplate.value.bgImage |
| | | } else { |
| | | item.pictureUrl = '' |
| | | } |
| | | // 如果展示背景,并且展示ppt,则ppt放到画中画 |
| | | // console.log('selectTemplate',selectTemplate.value) |
| | | // console.log('selectTemplate.value.pptW',selectTemplate.value.pptW |
| | | // ,'selectTemplate.value.pptH',selectTemplate.value.pptH |
| | | // ) |
| | | |
| | | if (item.pictureUrl && selectTemplate.value.showPpt) { |
| | | item.innerPicture = { |
| | | name: '画中画', |
| | |
| | | entityType: 0, |
| | | entityId: '0' |
| | | } |
| | | } |
| | | //如果不展示背景,则ppt放到背景,画中画为空 |
| | | else if (!item.pictureUrl && selectTemplate.value.showPpt) { |
| | | } else if (!item.pictureUrl && selectTemplate.value.showPpt) { |
| | | item.pictureUrl = ppturl |
| | | item.innerPicture = { |
| | | src: '', |
| | |
| | | category: 1 |
| | | } |
| | | } |
| | | // 初始化是否展示数字人 |
| | | item.showDigitalHuman = true |
| | | // 数字人位置和尺寸也需要缩放 |
| | | console.log(selectTemplate.value) |
| | | PPTpositon.w = selectTemplate.value.humanW |
| | | PPTpositon.h = selectTemplate.value.humanH |
| | | PPTpositon.x = selectTemplate.value.humanX |
| | | PPTpositon.y = selectTemplate.value.humanY |
| | | }) |
| | | |
| | | PPTArr.value = res |
| | | console.log('PPTArr.value', PPTArr.value) |
| | | PPTArr.value[0].isActive = true |
| | | selectPPT.value = PPTArr.value[0] |
| | | console.log('selectPPT.value', selectPPT.value) |
| | | showLeftList.value = true |
| | | clearInterval(schedulePPTTimer.value) |
| | | |
| | |
| | | }) |
| | | }, 2000) |
| | | } |
| | | //视频总字数、时长 |
| | | const videoText = ref(0) |
| | | const videoDuration = ref('') |
| | | watch( |
| | | () => PPTArr.value, |
| | | (val) => { |
| | | if (!val) { |
| | | return |
| | | } |
| | | // 计算总字数 - 修改为去除SSML标签后再计算长度 |
| | | videoText.value = val.reduce((prev, curr) => { |
| | | if (!curr.pptRemark) return prev; |
| | | |
| | | // 去除所有SSML标签,只保留文本内容 |
| | | const plainText = curr.pptRemark; |
| | | return prev + plainText.length; |
| | | }, 0) |
| | | //视频时长换算 |
| | | let videoTime = (videoText.value / 200) * 60 |
| | | videoDuration.value = formateVideoTime(Math.ceil(videoTime)) |
| | | }, |
| | | { deep: true } |
| | | ) |
| | | //视频时长换算 |
| | | const formateVideoTime = (times: any) => { |
| | | let hours: any = parseInt(`${times / 60 / 60}`) // 计算小时数 |
| | | let restMinutes: any = parseInt(`${(times / 60) % 60}`) // 分钟数取余,得到剩余分钟数 |
| | | let seconds: any = parseInt(`${times % 60}`) // 将剩余分钟数转换为秒数 |
| | | if (hours < 10) { |
| | | hours = '0' + hours |
| | | } |
| | | if (restMinutes < 10) { |
| | | restMinutes = '0' + restMinutes |
| | | } |
| | | if (seconds < 10) { |
| | | seconds = '0' + seconds |
| | | } |
| | | return hours + ':' + restMinutes + ':' + seconds |
| | | } |
| | | //取消解析ppt |
| | | const cancelAnalyze = () => { |
| | | showLeftList.value = true |
| | | clearInterval(schedulePPTTimer.value) |
| | | } |
| | | |
| | | const copyDocument = (item, index) => { |
| | | ElMessageBox.confirm( |
| | | '是否复制该页面?', |
| | |
| | | cancelButtonText: '否', |
| | | type: 'warning', |
| | | } |
| | | ) |
| | | .then(() => { |
| | | let copyItem = cloneDeep(item) |
| | | copyItem.id = generateUUID() |
| | | copyItem.isActive = false |
| | | PPTArr.value.splice(index + 1, 0, copyItem) |
| | | ).then(() => { |
| | | let copyItem = cloneDeep(item) |
| | | copyItem.id = generateUUID() |
| | | copyItem.isActive = false |
| | | // 深拷贝数字人配置 |
| | | copyItem.digitalHuman = {...item.digitalHuman} |
| | | PPTArr.value.splice(index + 1, 0, copyItem) |
| | | }).catch(() => { |
| | | ElMessage({ |
| | | type: 'info', |
| | | message: '已取消复制', |
| | | }) |
| | | .catch(() => { |
| | | ElMessage({ |
| | | type: 'info', |
| | | message: '已取消复制', |
| | | }) |
| | | }) |
| | | |
| | | }) |
| | | } |
| | | |
| | | const deleteDocument = (item) => { |
| | | ElMessageBox.confirm( |
| | | '是否删除该页面?', |
| | |
| | | cancelButtonText: '否', |
| | | type: 'warning', |
| | | } |
| | | ) |
| | | .then(() => { |
| | | PPTArr.value = PPTArr.value.filter((child) => child.id !== item.id) |
| | | }).catch(() => { |
| | | ElMessage({ |
| | | type: 'info', |
| | | message: '已取消删除', |
| | | }) |
| | | ).then(() => { |
| | | PPTArr.value = PPTArr.value.filter((child) => child.id !== item.id) |
| | | }).catch(() => { |
| | | ElMessage({ |
| | | type: 'info', |
| | | message: '已取消删除', |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | } |
| | | const deleteDigitalHuman = () => { |
| | | selectPPT.value.showDigitalHuman = false |
| | | selectPPT.value.digitalHuman.show = false |
| | | } |
| | | //删除画中画 |
| | | |
| | | const deleteInnerPicture = () => { |
| | | selectPPT.value.innerPicture.src = '' |
| | | } |
| | | //删除多个ppt |
| | | |
| | | const deleteMore = () => { |
| | | let selected = PPTArr.value.filter((child) => child.isChecked == true) |
| | | if (selected.length == 0) { |
| | | message.warning('请先选择要删除的ppt') |
| | | } else { |
| | | let newPPTArr = PPTArr.value.filter((child) => child.isChecked !== true) |
| | | PPTArr.value = newPPTArr |
| | | PPTArr.value = PPTArr.value.filter((child) => child.isChecked !== true) |
| | | } |
| | | } |
| | | /** 查询数字人列表 */ |
| | | const hostList = ref() |
| | | const loading = ref(true) // 列表的加载中 |
| | | |
| | | const hostList = ref([]) |
| | | const loading = ref(true) |
| | | const total = ref(0) |
| | | const queryParams = reactive({ |
| | | pageNo: 1, |
| | |
| | | gender: '', |
| | | posture: '' |
| | | }) |
| | | |
| | | const selectHost = ref(null) |
| | | |
| | | const getList = async () => { |
| | | loading.value = true |
| | | try { |
| | |
| | | queryParams.gender = tabs2ActiveNum.value |
| | | queryParams.posture = tabs3ActiveNum.value |
| | | queryParams.status = 0 |
| | | |
| | | let data = await pptTemplateApi.pageList(queryParams) |
| | | //如果数字人列表 data为空 则切换type再查询一次 |
| | | |
| | | if (data.list.length == 0) { |
| | | queryParams.type = tabs1ActiveNum.value == '0' ? '1' : '0' |
| | | tabs1ActiveNum.value = queryParams.type |
| | | data = await pptTemplateApi.pageList(queryParams) |
| | | if (data.list.length == 0) { |
| | | //如果还是没有,则提示没有有效的数字人,请联系管理员 |
| | | message.error('没有有效的数字人,请联系管理员') |
| | | return |
| | | } |
| | | } |
| | | |
| | | data.list.forEach((item) => { |
| | | item.isActive = false |
| | | }) |
| | | |
| | | hostList.value = data.list |
| | | humanId=hostList.value[0].id |
| | | selectHost.value = hostList.value[0] |
| | | // 切换数字人姿势条件时,修改数字人在ppt的位置 |
| | | initHumanPositon(selectHost.value) |
| | | if (hostList.value.length > 0 && !selectHost.value) { |
| | | selectHost.value = hostList.value[0] |
| | | // 更新当前选中PPT的数字人 |
| | | if (selectPPT.value) { |
| | | selectPPT.value.digitalHuman.host = selectHost.value |
| | | } |
| | | } |
| | | |
| | | total.value = data.total |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | const choosePPt = (item) => { |
| | | PPTArr.value.forEach((child) => { |
| | | if (child.id == item.id) { |
| | | child.isActive = true |
| | | } else { |
| | | child.isActive = false |
| | | } |
| | | child.isActive = child.id === item.id |
| | | }) |
| | | selectPPT.value = item |
| | | } |
| | | const selectHost = ref() // 选择的数字人 |
| | | |
| | | const chooseHost = (item) => { |
| | | console.log(item) |
| | | //将数字人id变成全局变量 |
| | | humanId=item.id |
| | | hostList.value.forEach((el) => { |
| | | if (el.id == item.id) { |
| | | el.isActive = true |
| | | } else { |
| | | el.isActive = false |
| | | } |
| | | el.isActive = el.id === item.id |
| | | }) |
| | | selectHost.value = item |
| | | // 点击数字人列表中的图像时,修改数字人在ppt的位置 |
| | | initHumanPositon(item) |
| | | } |
| | | // 根据数字人的不同姿势初始化其在ppt的位置 |
| | | const initHumanPositon = (data) => { |
| | | console.log(digitalHumanSize) |
| | | if (data.posture === 1) { |
| | | PPTpositon.x = viewSize.width - digitalHumanSize.width |
| | | PPTpositon.y = viewSize.height - digitalHumanSize.height |
| | | PPTpositon.w = digitalHumanSize.width |
| | | PPTpositon.h = digitalHumanSize.height |
| | | } else if (data.posture === 2) { |
| | | PPTpositon.x = viewSize.width - digitalHumanSize.width |
| | | PPTpositon.y = viewSize.height - digitalHumanSize.height |
| | | PPTpositon.w = digitalHumanSize.width |
| | | PPTpositon.h = digitalHumanSize.height |
| | | |
| | | // 只更新当前选中的PPT页的数字人 |
| | | if (selectPPT.value) { |
| | | selectPPT.value.digitalHuman.host = item |
| | | initHumanPositon(item, selectPPT.value.digitalHuman) |
| | | } |
| | | } |
| | | //打开弹框 |
| | | |
| | | const initHumanPositon = (hostData, digitalHuman) => { |
| | | if (hostData.posture === 1) { |
| | | digitalHuman.x = viewSize.width - digitalHuman.w |
| | | digitalHuman.y = viewSize.height - digitalHuman.h |
| | | } else if (hostData.posture === 2) { |
| | | digitalHuman.x = viewSize.width - digitalHuman.w |
| | | digitalHuman.y = viewSize.height - digitalHuman.h |
| | | } |
| | | } |
| | | |
| | | const audioSelect = ref() |
| | | const audioSelectData = ref() |
| | | |
| | | const openSelect = () => { |
| | | audioSelect.value.open() |
| | | } |
| | | |
| | | const selectAudio = (data) => { |
| | | console.log(data) |
| | | audioSelectData.value = data |
| | | // selectPPT.value.selectAudio = data[0] |
| | | // 遍历所有场景,应用相同的声音模型 |
| | | if (data==undefined){ |
| | | selectPPT.value.selectAudio.name='' |
| | | return {} |
| | | }else { |
| | | if (data == undefined) { |
| | | selectPPT.value.selectAudio.name = '' |
| | | } else { |
| | | PPTArr.value.forEach((scene) => { |
| | | scene.selectAudio = data[0] |
| | | }) |
| | | } |
| | | |
| | | } |
| | | //生成课程id |
| | | |
| | | const coursesCreate = () => { |
| | | const params = { |
| | | accountId: userId.value |
| | | } |
| | | pptTemplateApi.coursesCreate(params).then((res) => { |
| | | console.log(res) |
| | | if (res) { |
| | | courseInfo.value.id = res |
| | | } |
| | | }) |
| | | } |
| | | |
| | | //获取保存时间 |
| | | const saveTime = ref() |
| | | |
| | | const getSaveTime = () => { |
| | | const date = new Date() |
| | | let h: any = date.getHours() //hour |
| | | let m: any = date.getMinutes() //minute |
| | | let s: any = date.getSeconds() //second |
| | | if (h < 10) { |
| | | h = '0' + h |
| | | } |
| | | if (m < 10) { |
| | | m = '0' + m |
| | | } |
| | | if (s < 10) { |
| | | s = '0' + s |
| | | } |
| | | let h: any = date.getHours() |
| | | let m: any = date.getMinutes() |
| | | let s: any = date.getSeconds() |
| | | |
| | | if (h < 10) h = '0' + h |
| | | if (m < 10) m = '0' + m |
| | | if (s < 10) s = '0' + s |
| | | |
| | | return h + ':' + m + ':' + s |
| | | } |
| | | |
| | | const warningDialog = ref() |
| | | |
| | | // 语速 音量 |
| | | const voiceData = reactive({ |
| | | show: false, |
| | | speechRate: 1, |
| | |
| | | 2: '2' |
| | | } |
| | | }) |
| | | const removeHtmlTags= (html) =>{ |
| | | const parser = new DOMParser(); |
| | | const doc = parser.parseFromString(html, 'text/html'); |
| | | return doc.body.textContent || ""; |
| | | |
| | | const removeHtmlTags = (html) => { |
| | | const parser = new DOMParser() |
| | | const doc = parser.parseFromString(html, 'text/html') |
| | | return doc.body.textContent || "" |
| | | } |
| | | |
| | | const saveSubmit = async (type) => { |
| | | // 检查场景是否为空 |
| | | if (!PPTArr.value || PPTArr.value.length === 0) { |
| | | message.warning('场景为空,请先上传PPT!') |
| | | return false |
| | | } |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | //人脸校验 |
| | | while(!IsEndCheckFace.value){} //一个空循环,主要为了避免极端情况下当用户点击保存按钮或者视频合成按钮时,人脸校验未完成的问题 |
| | |
| | | thumbnail: '', |
| | | subtitlesStyle: '{}' |
| | | } |
| | | // if(type == "save"){ |
| | | Reflect.set(saveSubmitForm, 'id', courseInfo.value.id) |
| | | // }else{ |
| | | // Reflect.set(saveSubmitForm, "courseMediaId", courseInfo.value.id); |
| | | // } |
| | | |
| | | //组装数据 |
| | | const scenes: any = [] |
| | | saveSubmitForm.id = courseInfo.value.id |
| | | |
| | | const scenes = [] |
| | | const pageInfo = { |
| | | docInfo: { |
| | | docType: 1, |
| | | fileName: uploadFileObj.filename, |
| | | fileSize: uploadFileObj.size |
| | | }, |
| | | scenes: [] as any[] |
| | | scenes: [] |
| | | } |
| | | |
| | | let thumbnail = '' |
| | | const { name, pictureUrl, code, type: digitalHumanType } = selectHost.value // 解构以避免循环引用 |
| | | const digitalHumanComponents = { |
| | | name, |
| | | src: pictureUrl, |
| | | cover: pictureUrl, |
| | | width: PPTpositon.w * scaleRatio.value.width, |
| | | height: PPTpositon.h * scaleRatio.value.height, |
| | | originWidth: PPTpositon.w * scaleRatio.value.width, |
| | | originHeight: PPTpositon.h * scaleRatio.value.height, |
| | | category: 2, // 1: PPT, 2: 数字人, 3: 其他 |
| | | depth: componentsInfo.depth, |
| | | top: PPTpositon.y * scaleRatio.value.height, |
| | | marginLeft: PPTpositon.x * scaleRatio.value.width, |
| | | entityId: code, |
| | | entityType: digitalHumanType, // 如果是数字人,则是数字人类型 0: 普通, 1: 专属 |
| | | businessId: generateUUID(), |
| | | digitbotType: tabs1ActiveNum.value, |
| | | matting: 1, |
| | | marker: 1 |
| | | } |
| | | |
| | | let pageNum = 1 |
| | | if (PPTArr.value && PPTArr.value.length > 0) { |
| | | console.log('开始处理PPTArr数据') |
| | | PPTArr.value.forEach((item, index) => { |
| | | console.log(`处理第 ${index + 1} 个场景`) |
| | | try { |
| | | pageInfo.scenes.push(item.businessId) |
| | | if (pageNum == 1) { |
| | | thumbnail = item.pictureUrl |
| | | pageNum++ |
| | | } |
| | | console.log(item) |
| | | |
| | | const innerPictureCom = item.innerPicture |
| | | console.log('innerPictureCom:', JSON.stringify(innerPictureCom)) |
| | | console.log(item.pptRemark) |
| | | item.pptRemark = removeHtmlTags(item.pptRemark) |
| | | // item.pptRemark = editorRef.value.getText() |
| | | // item.pptRemark=item.pptRemark.replace(/<[^>]+>/g, '') |
| | | const formatItem = { |
| | | background: { |
| | | backgroundType: item.backgroundType, |
| | | entityId: '', |
| | | width: courseInfo.value.width, |
| | | height: courseInfo.value.height, |
| | | depth: 0, |
| | | src: item.pictureUrl, |
| | | cover: item.pictureUrl, |
| | | originWidth: item.width, |
| | | originHeight: item.height, |
| | | color: '#ffffff', |
| | | pptRemark: item.pptRemark |
| | | }, |
| | | hasPerson: item.showDigitalHuman==true? 1 : 2, |
| | | components: [ |
| | | { |
| | | ...cloneDeep(digitalHumanComponents), // 深拷贝 |
| | | status: item.showDigitalHuman ? 0 : 1 |
| | | }, |
| | | ...(item.innerPicture?.src |
| | | ? [ |
| | | { |
| | | ...cloneDeep(item.innerPicture), |
| | | // 保存时放大画中画的尺寸和位置 |
| | | width: item.innerPicture.width * scaleRatio.value.width, |
| | | height: item.innerPicture.height * scaleRatio.value.height, |
| | | top: item.innerPicture.top * scaleRatio.value.height, |
| | | marginLeft: item.innerPicture.marginLeft * scaleRatio.value.width, |
| | | category: 1, |
| | | id: undefined |
| | | } |
| | | ] |
| | | : []) |
| | | ], |
| | | driverType: item.driverType, |
| | | duration: '', |
| | | orderNo: index + 1, |
| | | textDriver: { |
| | | pitch: '', |
| | | speed: '', |
| | | speech_rate: voiceData.speechRate, |
| | | volume: voiceData.volume, |
| | | smartSpeed: '', |
| | | textJson: item.pptRemark, |
| | | }, |
| | | audioDriver: { |
| | | fileName: item.fileList && item.fileList[0]?.name, |
| | | audioId: '', |
| | | audioUrl: item.uploadAudioUrl, |
| | | useVideoBackgroundAudio: '' |
| | | }, |
| | | voice: { |
| | | voiceId:audioSelectData.value == undefined ? null : audioSelectData.value[0].id, |
| | | entityId: item.selectAudio && item.selectAudio.code, |
| | | tonePitch: '', |
| | | voiceType: item.selectAudio && item.selectAudio.voiceType, |
| | | speechRate: '', |
| | | name: item.selectAudio && item.selectAudio.name |
| | | }, |
| | | businessId: item.businessId |
| | | } |
| | | scenes.push(formatItem) |
| | | } catch (error) { |
| | | console.error(`处理第 ${index + 1} 个场景时出错:`, error) |
| | | //抛出异常 |
| | | throw error |
| | | PPTArr.value.forEach((item, index) => { |
| | | try { |
| | | pageInfo.scenes.push(item.businessId) |
| | | if (index === 0) { |
| | | thumbnail = item.pictureUrl |
| | | } |
| | | }) |
| | | } |
| | | console.log('pageInfo:', JSON.stringify(pageInfo)) |
| | | console.log('thumbnail:', thumbnail) |
| | | |
| | | try { |
| | | saveSubmitForm.pageInfo = JSON.stringify(pageInfo) |
| | | saveSubmitForm.thumbnail = thumbnail |
| | | saveSubmitForm.scenes = cloneDeep(scenes) |
| | | console.log('saveSubmitForm:', cloneDeep(saveSubmitForm)) |
| | | } catch (error) { |
| | | console.error('保存表单数据时出错:', error) |
| | | } |
| | | item.pptRemark = removeHtmlTags(item.pptRemark) |
| | | |
| | | const formatItem = { |
| | | background: { |
| | | backgroundType: item.backgroundType, |
| | | entityId: '', |
| | | width: courseInfo.value.width, |
| | | height: courseInfo.value.height, |
| | | depth: 0, |
| | | src: item.pictureUrl, |
| | | cover: item.pictureUrl, |
| | | originWidth: item.width, |
| | | originHeight: item.height, |
| | | color: '#ffffff', |
| | | pptRemark: item.pptRemark |
| | | }, |
| | | hasPerson: item.digitalHuman?.show ? 1 : 2, |
| | | components: [ |
| | | { |
| | | name: item.digitalHuman?.host?.name, |
| | | src: item.digitalHuman?.host?.pictureUrl, |
| | | cover: item.digitalHuman?.host?.pictureUrl, |
| | | width: item.digitalHuman?.w * scaleRatio.value.width, |
| | | height: item.digitalHuman?.h * scaleRatio.value.height, |
| | | originWidth: item.digitalHuman?.w * scaleRatio.value.width, |
| | | originHeight: item.digitalHuman?.h * scaleRatio.value.height, |
| | | category: 2, |
| | | depth: 0, |
| | | top: item.digitalHuman?.y * scaleRatio.value.height, |
| | | marginLeft: item.digitalHuman?.x * scaleRatio.value.width, |
| | | entityId: item.digitalHuman?.host?.code, |
| | | entityType: item.digitalHuman?.host?.type, |
| | | businessId: generateUUID(), |
| | | digitbotType: tabs1ActiveNum.value, |
| | | matting: 1, |
| | | marker: 1, |
| | | status: item.digitalHuman?.show ? 0 : 1 |
| | | }, |
| | | ...(item.innerPicture?.src ? [{ |
| | | ...cloneDeep(item.innerPicture), |
| | | width: item.innerPicture.width * scaleRatio.value.width, |
| | | height: item.innerPicture.height * scaleRatio.value.height, |
| | | top: item.innerPicture.top * scaleRatio.value.height, |
| | | marginLeft: item.innerPicture.marginLeft * scaleRatio.value.width, |
| | | category: 1, |
| | | id: undefined |
| | | }] : []) |
| | | ], |
| | | driverType: item.driverType, |
| | | duration: '', |
| | | orderNo: index + 1, |
| | | textDriver: { |
| | | pitch: '', |
| | | speed: '', |
| | | speech_rate: voiceData.speechRate, |
| | | volume: voiceData.volume, |
| | | smartSpeed: '', |
| | | textJson: item.pptRemark, |
| | | }, |
| | | audioDriver: { |
| | | fileName: item.fileList && item.fileList[0]?.name, |
| | | audioId: '', |
| | | audioUrl: item.uploadAudioUrl, |
| | | useVideoBackgroundAudio: '' |
| | | }, |
| | | voice: { |
| | | voiceId: audioSelectData.value == undefined ? null : audioSelectData.value[0].id, |
| | | entityId: item.selectAudio && item.selectAudio.code, |
| | | tonePitch: '', |
| | | voiceType: item.selectAudio && item.selectAudio.voiceType, |
| | | speechRate: '', |
| | | name: item.selectAudio && item.selectAudio.name |
| | | }, |
| | | businessId: item.businessId |
| | | } |
| | | |
| | | scenes.push(formatItem) |
| | | } catch (error) { |
| | | console.error(`处理第 ${index + 1} 个场景时出错:`, error) |
| | | throw error |
| | | } |
| | | }) |
| | | |
| | | saveSubmitForm.pageInfo = JSON.stringify(pageInfo) |
| | | saveSubmitForm.thumbnail = thumbnail |
| | | saveSubmitForm.scenes = cloneDeep(scenes) |
| | | |
| | | if (type == 'save') { |
| | | try { |
| | | const res = await pptTemplateApi.coursesSave(stringifySafely(saveSubmitForm)) |
| | | const res = await pptTemplateApi.coursesSave(JSON.stringify(saveSubmitForm)) |
| | | if (res) { |
| | | message.success('保存成功!') |
| | | saveTime.value = getSaveTime() |
| | | return true // 返回保存成功标志 |
| | | return true |
| | | } |
| | | return false |
| | | } catch (error) { |
| | |
| | | return false |
| | | } |
| | | } else { |
| | | // 合成视频前先保存 |
| | | try { |
| | | const saveResult = await saveSubmit('save') |
| | | if (!saveResult) { |
| | |
| | | return |
| | | } |
| | | |
| | | // 校验场景数据 |
| | | let warningStrArr: any = [] |
| | | let warningStrArr = [] |
| | | for (let i = 0; i < PPTArr.value.length; i++) { |
| | | const item = PPTArr.value[i] |
| | | console.log(item) |
| | |
| | | return |
| | | } |
| | | if (item.driverType == 1) { |
| | | // 去除所有HTML标签和SSML标签后再判断内容是否为空 |
| | | const plainText = item.pptRemark ? item.pptRemark.replace(/<[^>]+>/g, '') : '' |
| | | if (!plainText || plainText.trim() === '') { |
| | | warningStrArr.push( |
| | | `场景<span style="color: red; font-weight: bold;">${i + 1}</span>无有效的口播内容` |
| | | ) |
| | | } |
| | | else { |
| | | //判断去除标签后的内容长度是否超过2000字 |
| | | if (plainText.length > 2000) { |
| | | warningStrArr.push( |
| | | `场景<span style="color: red; font-weight: bold;">${i + 1}</span>口播内容超过2000字,请减少或拆分场景` |
| | | ) |
| | | } |
| | | } else if (plainText.length > 2000) { |
| | | warningStrArr.push( |
| | | `场景<span style="color: red; font-weight: bold;">${i + 1}</span>口播内容超过2000字,请减少或拆分场景` |
| | | ) |
| | | } |
| | | } |
| | | } |
| | |
| | | return |
| | | } |
| | | |
| | | // 合成视频 |
| | | try { |
| | | const res = await pptTemplateApi.megerMedia(saveSubmitForm) |
| | | if (res) { |
| | |
| | | } |
| | | } |
| | | } |
| | | function stringifySafely(obj) { |
| | | const seen = new WeakSet() |
| | | return JSON.stringify(obj, (key, value) => { |
| | | if (typeof value === 'object' && value !== null) { |
| | | if (seen.has(value)) { |
| | | return // 循环引用时返回 undefined |
| | | } |
| | | seen.add(value) |
| | | } |
| | | return value |
| | | }) |
| | | } |
| | | //定时保存 |
| | | // const saveTimer = ref() |
| | | // const saveInter = () => { |
| | | // if (saveTimer.value) { |
| | | // clearInterval(saveTimer.value) |
| | | // } |
| | | // saveTimer.value = setInterval(() => { |
| | | // saveSubmit('save') |
| | | // }, 60000) |
| | | // } |
| | | //生成试听 |
| | | const showAudioPlay = ref(false) //显示试听 |
| | | const showAudioPlay1 = ref(false) //显示试听 |
| | | //显示声音驱动的文件播放弹框 |
| | | const startAudioPlay = ref(false) |
| | | const textareaRef = ref() |
| | | const selectTextarea = ref('') |
| | | //上传音频文件超出限制后的提示 |
| | | const audioExceed = () => { |
| | | message.warning('最多上传一个声音驱动文件!') |
| | | } |
| | | const currentAudio = ref() |
| | | |
| | | const handlePptRemarkSelection = () => { |
| | | if (textareaRef.value) { |
| | | const textarea = textareaRef.value.$el.querySelector('textarea') |
| | | if (textarea) { |
| | | // 使用事件对象中的选中文本 |
| | | selectTextarea.value = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd) |
| | | console.log('选中的文本:', selectTextarea.value) |
| | | } |
| | | } |
| | | } |
| | | //富文本编辑器 -start |
| | | // 编辑器实例,必须用 shallowRef |
| | | const editorRef = shallowRef() |
| | | const editorConfig = { placeholder: '请输入内容...' } |
| | | const handleCreated = (editor) => { |
| | | editorRef.value = editor // editor 实例 |
| | | } |
| | | //停顿 |
| | | const handleBreak = (e) => { |
| | | //节点插入 |
| | | const node = { |
| | | type: 'title-black', |
| | | children: [{ text: e }] |
| | | } |
| | | editorRef.value.restoreSelection() // 恢复选区 |
| | | editorRef.value.insertNode(node); |
| | | editorRef.value.move(1); |
| | | } |
| | | //数字 |
| | | const handleNumber = (e) => { |
| | | editorRef.value.focus() |
| | | selectTextarea.value = editorRef.value.getSelectionText() |
| | | let reg = /^\d+$/ |
| | | if (!selectTextarea.value || selectTextarea.value.length == 0 || !reg.test(selectTextarea.value)) { |
| | | message.warning('请先选中需指定读法的数字') |
| | | return false |
| | | } |
| | | //节点插入 |
| | | const node = { |
| | | type: 'number-value', |
| | | numberVal: selectTextarea.value, |
| | | children: [{ text: e }] |
| | | } |
| | | editorRef.value.restoreSelection() // 恢复选区 |
| | | editorRef.value.insertNode(node); |
| | | editorRef.value.move(1); |
| | | } |
| | | //多音字 |
| | | const dialogVisible = ref(false) |
| | | const textList = ref([]) |
| | | const handleWord = () => { |
| | | editorRef.value.focus() |
| | | selectTextarea.value = editorRef.value.getSelectionText() |
| | | if (!selectTextarea.value) { |
| | | message.warning('请先选中需指定读法的文本') |
| | | return false |
| | | } |
| | | if (selectTextarea.value.length > 1) { |
| | | message.warning('只能选择一个字') |
| | | return false |
| | | } |
| | | let textPinyin = polyphonic(selectTextarea.value, { toneType: 'num', type: 'array' })[0]; |
| | | if (textPinyin.length > 1) { |
| | | textList.value = textPinyin; |
| | | dialogVisible.value = true |
| | | } else { |
| | | message.warning(`${selectTextarea.value}不是多音字`) |
| | | } |
| | | } |
| | | const handleTag = (name) => { |
| | | dialogVisible.value = false |
| | | //节点插入 |
| | | const node = { |
| | | type: 'text-value', |
| | | textVal: selectTextarea.value, |
| | | children: [{ text: name }] |
| | | } |
| | | editorRef.value.restoreSelection() // 恢复选区 |
| | | editorRef.value.insertNode(node); |
| | | editorRef.value.move(1); |
| | | } |
| | | const createAudio = async () => { |
| | | // 获取编辑器文本内容 |
| | | const text = editorRef.value.getText(); |
| | | |
| | | // 检查文本是否为空 |
| | | if (!text) { |
| | | message.warning('请输入需要试听文本的内容…'); |
| | | return false; |
| | | } |
| | | |
| | | // 截取文本长度不超过 100 |
| | | const truncatedText = text.length > 100 ? text.substring(0, 100) : text; |
| | | console.log(audioSelectData.value) |
| | | if (audioSelectData.value == undefined) { |
| | | const params = { |
| | | text: truncatedText, |
| | | humanId: humanId, |
| | | voiceId: null, |
| | | }; |
| | | try { |
| | | // 显示音频播放加载状态 |
| | | showAudioPlay1.value = true; |
| | | |
| | | // 调用 API 创建音频 |
| | | const res = await pptTemplateApi.createAudio(params); |
| | | |
| | | // 检查响应是否有效且无错误 |
| | | if (res && !res.error) { |
| | | console.log(res); |
| | | // 隐藏加载状态,显示音频播放状态 |
| | | showAudioPlay1.value = false; |
| | | showAudioPlay.value = true; |
| | | |
| | | // 初始化 Audio 对象 |
| | | currentAudio.value = new Audio(res); |
| | | |
| | | // 添加播放结束事件监听器 |
| | | currentAudio.value.addEventListener('ended', () => { |
| | | showAudioPlay.value = false; |
| | | currentAudio.value = null; |
| | | }); |
| | | |
| | | // 播放音频 |
| | | currentAudio.value.play(); |
| | | } else { |
| | | // 响应无效或有错误,隐藏加载状态 |
| | | showAudioPlay1.value = false; |
| | | } |
| | | } catch (error) { |
| | | // 捕获请求错误,隐藏加载状态并打印错误信息 |
| | | console.error('API 请求失败:', error); |
| | | showAudioPlay1.value = false; |
| | | } |
| | | }else { |
| | | const params = { |
| | | text: truncatedText, |
| | | humanId: null, |
| | | voiceId: audioSelectData.value[0].id, |
| | | }; |
| | | try { |
| | | // 显示音频播放加载状态 |
| | | showAudioPlay1.value = true; |
| | | |
| | | // 调用 API 创建音频 |
| | | const res = await pptTemplateApi.createAudio(params); |
| | | |
| | | // 检查响应是否有效且无错误 |
| | | if (res && !res.error) { |
| | | console.log(res); |
| | | // 隐藏加载状态,显示音频播放状态 |
| | | showAudioPlay1.value = false; |
| | | showAudioPlay.value = true; |
| | | |
| | | // 初始化 Audio 对象 |
| | | currentAudio.value = new Audio(res); |
| | | |
| | | // 添加播放结束事件监听器 |
| | | currentAudio.value.addEventListener('ended', () => { |
| | | showAudioPlay.value = false; |
| | | currentAudio.value = null; |
| | | }); |
| | | |
| | | // 播放音频 |
| | | currentAudio.value.play(); |
| | | } else { |
| | | // 响应无效或有错误,隐藏加载状态 |
| | | showAudioPlay1.value = false; |
| | | } |
| | | } catch (error) { |
| | | // 捕获请求错误,隐藏加载状态并打印错误信息 |
| | | console.error('API 请求失败:', error); |
| | | showAudioPlay1.value = false; |
| | | } |
| | | } |
| | | // 构建请求参数 |
| | | |
| | | |
| | | |
| | | } |
| | | //取消试听 |
| | | const pauseAudio = () => { |
| | | currentAudio.value.pause() |
| | | currentAudio.value = null |
| | | showAudioPlay.value = false |
| | | } |
| | | //声音驱动的文件 |
| | | const currentAudioFile = ref() |
| | | //声音驱动的文件播放 |
| | | const audioPlay = (file) => { |
| | | // 确保 file.response.data 是一个有效的 URL |
| | | if (!file.response.data) { |
| | | message.error('未获取到文件') |
| | | return |
| | | } |
| | | // 停止当前播放的音频(如果存在) |
| | | if (currentAudioFile.value) { |
| | | currentAudioFile.value.pause() |
| | | currentAudioFile.value.currentTime = 0 // 重置播放位置 |
| | | } |
| | | |
| | | // 创建新的 Audio 实例 |
| | | const audio = new Audio(file.response.data) |
| | | currentAudioFile.value = audio |
| | | |
| | | // 监听播放结束事件 |
| | | audio.addEventListener('ended', () => { |
| | | cancelAudio() |
| | | }) |
| | | |
| | | // 开始播放 |
| | | startAudioPlay.value = true |
| | | audio.play() |
| | | } |
| | | //取消声音驱动的文件播放 |
| | | const cancelAudio = () => { |
| | | if (currentAudioFile.value) { |
| | | currentAudioFile.value.pause() |
| | | // 可选:重置播放位置 |
| | | currentAudioFile.value.currentTime = 0 |
| | | currentAudioFile.value = null |
| | | } |
| | | startAudioPlay.value = false |
| | | } |
| | | //返回 |
| | | const goBack = () => { |
| | | console.log(PPTArr.value,courseInfo.value.id) |
| | | if (!PPTArr.value) { |
| | | // 删除当前课程 |
| | | pptTemplateApi.coursesDelete(courseInfo.value.id).then((res) => { |
| | | console.log(res) |
| | | }) |
| | | } |
| | | router.go(-1) |
| | | } |
| | | const getCourseDetail = (id) => { |
| | | pptTemplateApi.coursesDetail(id).then((res) => { |
| | | console.log(res) |
| | | if (res) { |
| | | //回显数据处理 |
| | | // 课程基本信息 |
| | | courseInfo.value = res |
| | | if (res.scenes && res.scenes.length > 0) { |
| | | //左侧数据列表 |
| | | res.scenes.forEach((item) => { |
| | | item.isActive = false |
| | | item.isChecked = false |
| | | item.pictureUrl = item.background.src |
| | | item.pptRemark = editorHtml.parseElemHtml(item.background.pptRemark); |
| | | item.backgroundType = item.background.backgroundType |
| | | item.width = item.background.width |
| | | item.height = item.background.height |
| | | item.showDigitalHuman = item.components.find((p) => p.category == 2).status == 0 //根据数字人模板的status判断是否显示数字人 |
| | | // 画中画位置和尺寸缩小 |
| | | const innerPicture = item.components.find((p) => p.category == 1) |
| | | if (innerPicture) { |
| | | item.innerPicture = { |
| | | ...innerPicture, |
| | | width: innerPicture.width / scaleRatio.value.width, |
| | | height: innerPicture.height / scaleRatio.value.height, |
| | | top: innerPicture.top / scaleRatio.value.height, |
| | | marginLeft: innerPicture.marginLeft / scaleRatio.value.width |
| | | } |
| | | } |
| | | }) |
| | | PPTArr.value = res.scenes |
| | | PPTArr.value[0].isActive = true |
| | | selectPPT.value = PPTArr.value[0] |
| | | console.log('getCourseDetail selectPPT.value:', selectPPT.value) |
| | | // selectPPT.value.uploadAudioUrl = PPTArr.value[0].audioDriver?.audioUrl; |
| | | // selectPPT.value.selectAudio = PPTArr.value[0].voice; |
| | | // 遍历所有场景,应用相同的声音模型 |
| | | PPTArr.value.forEach((scene, index) => { |
| | | //如果res.scenes[index] 有voice且不为空 |
| | | if (res.scenes[index].voice) { |
| | | scene.selectAudio = res.scenes[index].voice |
| | | scene.selectAudio.code = res.scenes[index].voice.entityId |
| | | scene.selectAudio.id = res.scenes[index].voice.voiceId |
| | | } |
| | | scene.uploadAudioUrl = res.scenes[index].audioDriver?.audioUrl |
| | | }) |
| | | if (PPTArr.value[0].audioDriver?.fileName && PPTArr.value[0].audioDriver?.audioUrl) { |
| | | selectPPT.value.fileList = [ |
| | | { |
| | | name: PPTArr.value[0].audioDriver?.fileName, |
| | | url: PPTArr.value[0].audioDriver?.audioUrl |
| | | } |
| | | ] |
| | | } |
| | | //选择的数字人信息 |
| | | const hostInfo = res.scenes[0].components.find((component) => component.category === 2) |
| | | // 先在当前数字人列表中查找 |
| | | let foundHost = hostList.value.find(item => item.code === hostInfo.entityId) |
| | | |
| | | // 如果在当前列表中没找到,且当前是公共数字人列表,则切换到我的数字人列表重新获取 |
| | | if (!foundHost && tabs1ActiveNum.value === '0') { |
| | | // 保存公共数字人列表的第一个数字人作为默认值 |
| | | const defaultPublicHost = hostList.value[0] |
| | | |
| | | // 切换到"我的"数字人 |
| | | tabs1ActiveNum.value = '1' |
| | | // 重新获取数字人列表 |
| | | getList().then(() => { |
| | | // 在新列表中查找 |
| | | foundHost = hostList.value.find(item => item.code === hostInfo.entityId) |
| | | |
| | | // 如果在"我的"数字人中也没找到,则使用默认公共数字人 |
| | | if (!foundHost) { |
| | | tabs1ActiveNum.value = '0' // 切回公共数字人tab |
| | | foundHost = defaultPublicHost // 使用之前保存的默认公共数字人 |
| | | message.warning('未找到原数字人,已使用默认公共数字人替代') |
| | | } |
| | | |
| | | // 设置选中的数字人 |
| | | selectHost.value = foundHost || hostList.value[0] |
| | | }) |
| | | } else { |
| | | // 设置选中的数字人 |
| | | selectHost.value = foundHost || hostList.value[0] |
| | | } |
| | | |
| | | // 设置选中的数字人 |
| | | selectHost.value = foundHost || hostList.value[0] |
| | | if (hostInfo) { |
| | | PPTpositon.w = hostInfo.width / scaleRatio.value.width |
| | | PPTpositon.h = hostInfo.height / scaleRatio.value.height |
| | | PPTpositon.x = hostInfo.marginLeft / scaleRatio.value.width |
| | | PPTpositon.y = hostInfo.top / scaleRatio.value.height |
| | | } |
| | | // selectHost.value.name = hostInfo.name; |
| | | // selectHost.value.pictureUrl = hostInfo.src; |
| | | // selectHost.value.id = hostInfo.entityId; |
| | | //数字人位置信息 |
| | | componentsInfo.width = hostInfo.width |
| | | componentsInfo.height = hostInfo.height |
| | | componentsInfo.top = hostInfo.top |
| | | componentsInfo.marginLeft = hostInfo.marginLeft |
| | | componentsInfo.depth = hostInfo.depth |
| | | //数字人类型 |
| | | tabs1ActiveNum.value = hostInfo.digitbotType |
| | | driveType.forEach((child) => { |
| | | if (child.name == hostInfo.driverType) { |
| | | selectDriveType.value = child |
| | | } |
| | | }) |
| | | //选择声音信息 |
| | | const voiceInfo = res.scenes[0].voice |
| | | audioSelectData.value = [ |
| | | { |
| | | id: voiceInfo.voiceId, |
| | | entityId: voiceInfo.entityId, |
| | | name: voiceInfo.name |
| | | } |
| | | ] |
| | | } |
| | | //上传文件信息 |
| | | const pageInfo = res.pageInfo ? JSON.parse(res.pageInfo) : '' |
| | | uploadFileObj.filename = pageInfo ? pageInfo.docInfo.fileName : '' |
| | | uploadFileObj.size = pageInfo ? pageInfo.docInfo.fileSize : '' |
| | | |
| | | //应用模板 这里用户可能已经调整了模板,所以这里不应用模板 |
| | | // applyTemplate() |
| | | } |
| | | }) |
| | | } |
| | | //选择模板 |
| | | const chooseTemplate = (currTemplate) => { |
| | | selectTemplate.value = cloneDeep(currTemplate) |
| | | console.log('选择的模板信息:', currTemplate) |
| | | console.log(templates.value) |
| | | templates.value.forEach((item) => { |
| | | item.isActive = false |
| | | }) |
| | | currTemplate.isActive = true |
| | | applyTemplate() |
| | | } |
| | | |
| | | const applyTemplate = (ppt = null) => { |
| | | const template = selectTemplate.value |
| | | console.log('template', template) |
| | | const pptList = applyAllTemplate.value ? PPTArr.value : [selectPPT.value] |
| | | |
| | | // 数字人是统一生效的,先处理 |
| | | console.log(template) |
| | | pptList.forEach((item) => { |
| | | // 保存原始ppt图片 |
| | | const originalPPT = item.innerPicture?.src || item.pictureUrl |
| | | console.log('originalPPT', originalPPT) |
| | | |
| | | if (template.showBackground) { |
| | | item.pictureUrl = template.bgImage |
| | | if (template.showPpt) { |
| | |
| | | item.innerPicture.src = '' |
| | | } |
| | | |
| | | item.showDigitalHuman = template.showDigitalHuman |
| | | // 添加同步宽高的逻辑 |
| | | const targetTemplate = selectTemplate.value |
| | | console.log(PPTArr) |
| | | item.digitalHuman.show = template.showDigitalHuman |
| | | item.digitalHuman.w = template.humanW |
| | | item.digitalHuman.h = template.humanH |
| | | item.digitalHuman.x = template.humanX |
| | | item.digitalHuman.y = template.humanY |
| | | |
| | | PPTArr.value.forEach((otherItem) => { |
| | | if (otherItem.templateId === item.templateId) { |
| | | otherItem.width = item.width |
| | |
| | | } |
| | | }) |
| | | }) |
| | | |
| | | // 数字人位置也需要缩放 |
| | | PPTpositon.w = selectTemplate.value.humanW |
| | | PPTpositon.h = selectTemplate.value.humanH |
| | | PPTpositon.x = selectTemplate.value.humanX |
| | | PPTpositon.y = selectTemplate.value.humanY |
| | | } |
| | | |
| | | const replaceDialog = ref(null) |
| | | |
| | | // 打开弹出框 |
| | | const openReplaceDialog = () => { |
| | | replaceDialog.value.open() |
| | | } |
| | | |
| | | // 处理提交的替换规则 |
| | | const escapeRegExp = (string) => { |
| | | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // 转义正则中的特殊字符 |
| | | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') |
| | | } |
| | | |
| | | const handleReplacement = (replacements) => { |
| | | PPTArr.value.forEach((item) => { |
| | | if (item.pptRemark) { |
| | | replacements.forEach((replacement) => { |
| | | const fromEscaped = escapeRegExp(replacement.from) // 转义特殊字符 |
| | | const regExp = new RegExp(fromEscaped, 'g') // 使用转义后的字符串构造正则表达式 |
| | | const fromEscaped = escapeRegExp(replacement.from) |
| | | const regExp = new RegExp(fromEscaped, 'g') |
| | | item.pptRemark = item.pptRemark.replace(regExp, replacement.to) |
| | | }) |
| | | } |
| | |
| | | message.success('批量替换成功!') |
| | | } |
| | | |
| | | const showAudioPlay = ref(false) |
| | | const showAudioPlay1 = ref(false) |
| | | const startAudioPlay = ref(false) |
| | | const textareaRef = ref() |
| | | const selectTextarea = ref('') |
| | | |
| | | const audioExceed = () => { |
| | | message.warning('最多上传一个声音驱动文件!') |
| | | } |
| | | |
| | | const currentAudio = ref() |
| | | |
| | | const createAudio = async () => { |
| | | const text = editorRef.value.getText() |
| | | if (!text) { |
| | | message.warning('请输入需要试听文本的内容…') |
| | | return false |
| | | } |
| | | |
| | | const truncatedText = text.length > 100 ? text.substring(0, 100) : text |
| | | |
| | | const params = { |
| | | text: truncatedText, |
| | | humanId: selectPPT.value.digitalHuman?.host?.id || null, |
| | | voiceId: audioSelectData.value == undefined ? null : audioSelectData.value[0].id, |
| | | } |
| | | |
| | | try { |
| | | showAudioPlay1.value = true |
| | | const res = await pptTemplateApi.createAudio(params) |
| | | |
| | | if (res && !res.error) { |
| | | showAudioPlay1.value = false |
| | | showAudioPlay.value = true |
| | | |
| | | currentAudio.value = new Audio(res) |
| | | currentAudio.value.addEventListener('ended', () => { |
| | | showAudioPlay.value = false |
| | | currentAudio.value = null |
| | | }) |
| | | |
| | | currentAudio.value.play() |
| | | } else { |
| | | showAudioPlay1.value = false |
| | | } |
| | | } catch (error) { |
| | | console.error('API 请求失败:', error) |
| | | showAudioPlay1.value = false |
| | | } |
| | | } |
| | | |
| | | const pauseAudio = () => { |
| | | currentAudio.value.pause() |
| | | currentAudio.value = null |
| | | showAudioPlay.value = false |
| | | } |
| | | |
| | | const currentAudioFile = ref() |
| | | |
| | | const audioPlay = (file) => { |
| | | if (!file.response.data) { |
| | | message.error('未获取到文件') |
| | | return |
| | | } |
| | | |
| | | if (currentAudioFile.value) { |
| | | currentAudioFile.value.pause() |
| | | currentAudioFile.value.currentTime = 0 |
| | | } |
| | | |
| | | const audio = new Audio(file.response.data) |
| | | currentAudioFile.value = audio |
| | | |
| | | audio.addEventListener('ended', () => { |
| | | cancelAudio() |
| | | }) |
| | | |
| | | startAudioPlay.value = true |
| | | audio.play() |
| | | } |
| | | |
| | | const cancelAudio = () => { |
| | | if (currentAudioFile.value) { |
| | | currentAudioFile.value.pause() |
| | | currentAudioFile.value.currentTime = 0 |
| | | currentAudioFile.value = null |
| | | } |
| | | startAudioPlay.value = false |
| | | } |
| | | |
| | | const goBack = () => { |
| | | if (!PPTArr.value) { |
| | | pptTemplateApi.coursesDelete(courseInfo.value.id).then((res) => { |
| | | console.log(res) |
| | | }) |
| | | } |
| | | router.go(-1) |
| | | } |
| | | |
| | | const editorRef = shallowRef() |
| | | const editorConfig = { placeholder: '请输入内容...' } |
| | | |
| | | const handleCreated = (editor) => { |
| | | editorRef.value = editor |
| | | } |
| | | |
| | | const dialogVisible = ref(false) |
| | | const textList = ref([]) |
| | | |
| | | const handleWord = () => { |
| | | editorRef.value.focus() |
| | | selectTextarea.value = editorRef.value.getSelectionText() |
| | | if (!selectTextarea.value) { |
| | | message.warning('请先选中需指定读法的文本') |
| | | return false |
| | | } |
| | | if (selectTextarea.value.length > 1) { |
| | | message.warning('只能选择一个字') |
| | | return false |
| | | } |
| | | let textPinyin = polyphonic(selectTextarea.value, { toneType: 'num', type: 'array' })[0] |
| | | if (textPinyin.length > 1) { |
| | | textList.value = textPinyin |
| | | dialogVisible.value = true |
| | | } else { |
| | | message.warning(`${selectTextarea.value}不是多音字`) |
| | | } |
| | | } |
| | | |
| | | const handleTag = (name) => { |
| | | dialogVisible.value = false |
| | | const node = { |
| | | type: 'text-value', |
| | | textVal: selectTextarea.value, |
| | | children: [{ text: name }] |
| | | } |
| | | editorRef.value.restoreSelection() |
| | | editorRef.value.insertNode(node) |
| | | editorRef.value.move(1) |
| | | } |
| | | const onDragMove = (evt, data) => { |
| | | console.log(evt) |
| | | console.log(data) |
| | | |
| | | // 限制坐标 |
| | | if (data.x < -100) { |
| | | data.x = -100; // 可以设置最小坐标为 -100 |
| | | } |
| | | if (data.y < -100) { |
| | | data.y = -100; // 可以设置最小坐标为 -100 |
| | | } |
| | | }; |
| | | |
| | | const getCourseDetail = async (id) => { |
| | | const res = await pptTemplateApi.coursesDetail(id) |
| | | if (res) { |
| | | courseInfo.value = res |
| | | |
| | | if (res.scenes && res.scenes.length > 0) { |
| | | res.scenes.forEach((item) => { |
| | | item.isActive = false |
| | | item.isChecked = false |
| | | item.pictureUrl = item.background.src |
| | | item.pptRemark = editorHtml.parseElemHtml(item.background.pptRemark) |
| | | item.backgroundType = item.background.backgroundType |
| | | item.width = item.background.width |
| | | item.height = item.background.height |
| | | |
| | | const hostInfo = item.components.find((p) => p.category == 2) |
| | | if (hostInfo) { |
| | | item.digitalHuman = { |
| | | show: hostInfo.status === 0, |
| | | x: hostInfo.marginLeft / scaleRatio.value.width, |
| | | y: hostInfo.top / scaleRatio.value.height, |
| | | w: hostInfo.width / scaleRatio.value.width, |
| | | h: hostInfo.height / scaleRatio.value.height, |
| | | active: false, |
| | | host: { |
| | | ...hostList.value.find(h => h.code === hostInfo.entityId), |
| | | code: hostInfo.entityId, |
| | | type: hostInfo.entityType |
| | | } |
| | | } |
| | | } |
| | | |
| | | const innerPicture = item.components.find((p) => p.category == 1) |
| | | if (innerPicture) { |
| | | item.innerPicture = { |
| | | ...innerPicture, |
| | | width: innerPicture.width / scaleRatio.value.width, |
| | | height: innerPicture.height / scaleRatio.value.height, |
| | | top: innerPicture.top / scaleRatio.value.height, |
| | | marginLeft: innerPicture.marginLeft / scaleRatio.value.width |
| | | } |
| | | } |
| | | }) |
| | | |
| | | PPTArr.value = res.scenes |
| | | PPTArr.value[0].isActive = true |
| | | selectPPT.value = PPTArr.value[0] |
| | | |
| | | PPTArr.value.forEach((scene, index) => { |
| | | if (res.scenes[index].voice) { |
| | | scene.selectAudio = res.scenes[index].voice |
| | | scene.selectAudio.code = res.scenes[index].voice.entityId |
| | | scene.selectAudio.id = res.scenes[index].voice.voiceId |
| | | } |
| | | scene.uploadAudioUrl = res.scenes[index].audioDriver?.audioUrl |
| | | }) |
| | | |
| | | if (PPTArr.value[0].audioDriver?.fileName && PPTArr.value[0].audioDriver?.audioUrl) { |
| | | selectPPT.value.fileList = [ |
| | | { |
| | | name: PPTArr.value[0].audioDriver?.fileName, |
| | | url: PPTArr.value[0].audioDriver?.audioUrl |
| | | } |
| | | ] |
| | | } |
| | | |
| | | // 设置音频选择数据 |
| | | const firstScene = res.scenes[0] |
| | | if (firstScene.voice) { |
| | | audioSelectData.value = [ |
| | | { |
| | | id: firstScene.voice.voiceId, |
| | | entityId: firstScene.voice.entityId, |
| | | name: firstScene.voice.name |
| | | } |
| | | ] |
| | | } |
| | | } |
| | | |
| | | const pageInfo = res.pageInfo ? JSON.parse(res.pageInfo) : '' |
| | | uploadFileObj.filename = pageInfo ? pageInfo.docInfo.fileName : '' |
| | | uploadFileObj.size = pageInfo ? pageInfo.docInfo.fileSize : '' |
| | | } |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | let data = await TemplateApi.getTemplatePage(queryParams) |
| | | TEMPLATE_PRESETS.value = data.list.map((item) => { |
| | | return { |
| | | ...item, |
| | | showBackground: item.showBackground === 1, |
| | | showDigitalHuman: item.showDigitalHuman === 1, |
| | | showPpt: item.showPpt === 1 |
| | | } |
| | | }) |
| | | if (templates.value.length > 0) selectTemplate.value = cloneDeep(templates.value[0]) |
| | | TEMPLATE_PRESETS.value = data.list.map((item) => ({ |
| | | ...item, |
| | | showBackground: item.showBackground === 1, |
| | | showDigitalHuman: item.showDigitalHuman === 1, |
| | | showPpt: item.showPpt === 1 |
| | | })) |
| | | |
| | | templates.value = TEMPLATE_PRESETS.value.map((template) => cloneDeep(template)) |
| | | selectTemplate.value = cloneDeep(templates.value[0]) |
| | | // console.log('onMounted selectTemplate.value',selectTemplate.value) |
| | | |
| | | await getList() |
| | | console.log(route.query.id) |
| | | |
| | | if (route.query.id) { |
| | | await getCourseDetail(route.query.id) |
| | | // saveInter() // 启动定时保存 |
| | | } else { |
| | | coursesCreate() |
| | | } |
| | | }) |
| | | |
| | | onUnmounted(() => { |
| | | // clearInterval(saveTimer.value) |
| | | console.log(schedulePPTTimer.value) |
| | | clearInterval(schedulePPTTimer.value) |
| | | if (schedulePPTTimer.value) { |
| | | clearInterval(schedulePPTTimer.value) |
| | | } |
| | | if (currentAudioFile.value) { |
| | | currentAudioFile.value.removeEventListener('ended', cancelAudio) |
| | | currentAudioFile.value = null |
| | | } |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .pages { |
| | | height: 100%; |
| | |
| | | margin: 0; |
| | | |
| | | div { |
| | | // height: 30px; |
| | | padding: 5px 10px; |
| | | margin: 0; |
| | | line-height: 30px; |
| | |
| | | |
| | | .list { |
| | | position: relative; |
| | | height: calc(152px * 9 / 16); // 使用缩略图的固定高度 |
| | | height: calc(152px * 9 / 16); |
| | | margin: 20px 0; |
| | | box-sizing: content-box; |
| | | |
| | |
| | | border-radius: 5px; |
| | | } |
| | | |
| | | // 确保背景图片填充整个容器 |
| | | .background { |
| | | position: absolute; |
| | | width: 100%; |
| | |
| | | |
| | | .ppt-bg { |
| | | z-index: 2; |
| | | // width: 152px; |
| | | // height: 100%; |
| | | } |
| | | |
| | | .host { |
| | |
| | | width: 56%; |
| | | background-color: #fff; |
| | | box-shadow: 0 3px 6px rgb(175 175 175 / 16%); |
| | | flex-grow: 1; // 确保中间区域可以自适应高度 |
| | | flex-grow: 1; |
| | | flex-direction: column; |
| | | justify-content: flex-start; |
| | | |
| | |
| | | |
| | | .main-image-box { |
| | | position: relative; |
| | | // width: 760px; |
| | | // height: 360px; |
| | | border: 1px solid #ebeef5; |
| | | box-sizing: content-box; |
| | | } |
| | |
| | | justify-content: space-between; |
| | | padding: 10px; |
| | | |
| | | .voice-item { |
| | | width: 180px; |
| | | height: 30px; |
| | | overflow: hidden; |
| | | cursor: pointer; |
| | | background-color: #c9c9c9; |
| | | border-radius: 12px; |
| | | |
| | | span { |
| | | display: inline-block; |
| | | width: 50%; |
| | | height: 30px; |
| | | line-height: 30px; |
| | | text-align: center; |
| | | } |
| | | |
| | | .active-item { |
| | | color: #fff; |
| | | background-color: #409eff; |
| | | } |
| | | } |
| | | |
| | | .media-box { |
| | | display: flex; |
| | | align-items: center; |
| | | |
| | | .mic { |
| | | display: flex; |
| | | align-items: center; |
| | | width: 50px; |
| | | justify-content: space-around; |
| | | padding: 5px 10px; |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | z-index: 1; /* 背景在底层 */ |
| | | z-index: 1; |
| | | width: 100%; |
| | | height: 100%; |
| | | background-color: #f0f1fa; /* 设置底色 */ |
| | | background-color: #f0f1fa; |
| | | } |
| | | |
| | | .host-name { |
| | |
| | | } |
| | | |
| | | .ppt-bg { |
| | | z-index: 2; /* 图片在背景之上 */ |
| | | z-index: 2; |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | |
| | | } |
| | | .ppt-bg { |
| | | position: absolute; |
| | | z-index: 2; /* 图片在背景之上 */ |
| | | z-index: 2; |
| | | } |
| | | |
| | | .human-image { |
| | | position: absolute; |
| | | z-index: 3; /* 图片在背景之上 */ |
| | | z-index: 3; |
| | | } |
| | | } |
| | | } |
| | |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | z-index: 1; /* 背景在底层 */ |
| | | z-index: 1; |
| | | width: 100%; |
| | | height: 100%; |
| | | background-color: #f0f1fa; /* 设置底色 */ |
| | | background-color: #f0f1fa; |
| | | } |
| | | |
| | | .template-tool { |
| | |
| | | bottom: 0; |
| | | } |
| | | |
| | | /* 滚动条样式 */ |
| | | ::-webkit-scrollbar { |
| | | width: 4px; |
| | | } |
| | | |
| | | /* 滑块样式 */ |
| | | ::-webkit-scrollbar-thumb { |
| | | background-color: #888; |
| | | border-radius: 6px; |
| | | } |
| | | |
| | | /* 滚动条轨道样式 */ |
| | | ::-webkit-scrollbar-track { |
| | | background-color: #f2f2f2; |
| | | border-radius: 6px; |
| | | } |
| | | |
| | | .voice-card { |
| | | z-index: 1000 !important; // 添加更高的z-index确保在最顶层 |
| | | z-index: 1000 !important; |
| | | } |
| | | |
| | | .voice-card :deep(.el-card__body) { |