shenrongliang
2025-05-23 ccb4a8b96d68e2546ade9752ee5173c4a3e4e000
easegen-front/src/views/File/index.vue
@@ -199,6 +199,15 @@
// 新增按钮操作
const handleAdd = () => {
  form.id = undefined
  form.name = ''
  form.url = ''
  form.type = ''
  form.createBy = undefined
  dialogTitle.value = '新增文件'
  if (formRef.value) {
    formRef.value.resetFields()
  }
  open.value = true
}
@@ -239,6 +248,7 @@
    open.value = false
    if (res) {
      message.success(form.id ? "修改成功" : "新增成功")
      reset()
      getList()
    }
  } catch (error) {
easegen-front/src/views/Login/Login.vue
@@ -76,6 +76,7 @@
  float: left;
  width: 50%;
  margin-top: 4vh;
  margin-left: 60px;
}
.Left-img img{
easegen-front/src/views/chooseTemplate/audioSelect.vue
@@ -203,7 +203,6 @@
const useOriginalSound = () => {
  // 清除选中的音频
  selectList.value = null;
  // 清除列表中所有选中状态
  if (audioList.value) {
    audioList.value.forEach(item => {
@@ -222,7 +221,7 @@
    currentlyPlaying.value.isPlay = false;
    currentlyPlaying.value = null;
  }
  getList();
  // 关闭对话框
  audioSelectVisible.value = false;
easegen-front/src/views/chooseTemplate/index.vue
@@ -31,19 +31,17 @@
      </div>
      <div class="top-right">
        <span v-if="saveTime">{{ saveTime }} {{ t('courseCenter.saved') }}</span>
        <!-- 保存按钮 -->
        <el-button size="small" @click="saveSubmit('save')" :loading="SaveLoading" >{{ t('common.save') }}</el-button>
        <!-- 合成视频 -->
        <el-button type="primary" size="small" @click="saveSubmit('')" :loading="MakeLoading" >{{
          t('courseCenter.composeViode')
        }}</el-button>
        <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>
      </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
          >
@@ -112,15 +110,15 @@
                    />
                    <!-- 数字人(有则显示) -->
                    <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 }}
@@ -154,7 +152,6 @@
          </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>
@@ -179,19 +176,21 @@
              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"
@@ -217,7 +216,7 @@
              >
                <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"
@@ -226,60 +225,59 @@
                  <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')"
@@ -292,14 +290,12 @@
                @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"
@@ -364,21 +360,12 @@
        </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"
@@ -389,55 +376,15 @@
        </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>
@@ -469,8 +416,8 @@
            >
              <template #trigger>
                <el-button type="primary" :icon="Upload">{{
                  t('courseCenter.uploadAudio')
                }}</el-button>
                    t('courseCenter.uploadAudio')
                  }}</el-button>
              </template>
            </el-upload>
          </el-tooltip>
@@ -544,7 +491,7 @@
            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 }}
@@ -559,8 +506,6 @@
      <!-- 背景设置 -->
      <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>
@@ -568,29 +513,8 @@
      <!-- 画中画设置 -->
      <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">
@@ -611,28 +535,35 @@
    <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>
    <el-dialog v-model="dialogVisible1" title="提示" width="500px" style="height: 150px">
      <p style="margin-bottom: 20px;font-size: 18px">是否要将此模板应用到所有页面</p>
      <span class="dialog-footer">
        <el-button @click="handleTemplateSelection1(1)">应用当前页</el-button>
        <el-button type="primary" @click="handleTemplateSelection1(2)">应用所有页</el-button>
      </span>
    </el-dialog>
    <el-dialog v-model="dialogVisible2" title="提示" width="500px" style="height: 150px">
      <p style="margin-bottom: 20px;font-size: 18px">是否要将此数字人应用到所有页面</p>
      <span class="dialog-footer">
        <el-button @click="chooseHost1(1)">应用当前页</el-button>
        <el-button type="primary" @click="chooseHost1(2)">应用所有页</el-button>
      </span>
    </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 { ElMessage, ElMessageBox } from 'element-plus'
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'
@@ -641,8 +572,7 @@
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'
@@ -652,8 +582,7 @@
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,
@@ -662,40 +591,37 @@
  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 { useFaceDetection } from '@/utils/HaveFace'
import {coursesDelete} from "@/api/pptTemplate";
import { tr } from 'element-plus/es/locale'
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 dialogVisible1=ref(false)
const dialogVisible2=ref(false)
const isEditing = ref(false)
const inputRef = ref(null)
//保存的加载动画
const SaveLoading = ref(false)
//视频合成的加载动画
const MakeLoading = ref(false)
const editName = ref('')
const applyAllHost = ref(false)
//当前是否存在人脸
const IsHaveFace = ref(false)
//当前是否完成ppt人脸校验
const IsEndCheckFace = ref(true)
// 切换到编辑模式
const toggleEdit = () => {
  isEditing.value = true
@@ -703,27 +629,62 @@
  nextTick(() => {
    inputRef.value.focus()
  })
};
const onDragMove = (evt, data) => {
  console.log(evt)
  console.log(data)
}
const hostValue=ref({})
const chooseHost = (item) => {
  dialogVisible2.value = true
  hostValue.value = item
}
const chooseHost1 = (index) => {
  if (index==1){
    applyAllHost.value = false
    applyHostToPages(hostValue.value)
    dialogVisible2.value = false
  }else if(index==2){
    applyAllHost.value = true
    applyHostToPages(hostValue.value)
    dialogVisible2.value = false
  }
}
const applyHostToPages = (host) => {
  hostList.value.forEach((el) => {
    el.isActive = el.id === host.id
  })
  // 限制坐标
  if (data.x < -100) {
    data.x = -100; // 可以设置最小坐标为 -100
  }
  if (data.y < -100) {
    data.y = -100; // 可以设置最小坐标为 -100
  }
};
// 保存编辑后的名称
  const pagesToUpdate = applyAllHost.value ? PPTArr.value : [selectPPT.value]
  pagesToUpdate.forEach(page => {
    page.digitalHuman.host = host
    initHumanPositon(host, page.digitalHuman)
  })
}
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,
@@ -731,12 +692,12 @@
  name: '未命名草稿',
  duration: 0,
  status: 0,
  pageMode: 2,//ppt课件视频
  pageMode: 2,
  matting: 1,
  width: 1920,
  height: 1080
})
// 当比例改变时更新宽度和高度
watch(
  () => courseInfo.value.aspect,
  (newAspect) => {
@@ -745,83 +706,14 @@
  }
)
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'),
@@ -859,40 +751,22 @@
    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'),
@@ -919,6 +793,30 @@
    isActive: false
  }
])
const showHeadImageTool = ref(false)
const showDigitalHumanTool = ref(false)
const showTemplateTool = ref(false)
const showInnerPictureTool = ref(false)
const applyAllTemplate = ref(false)
const templateSelection=ref({})
const handleTemplateSelection = (template) => {
  dialogVisible1.value=true
  templateSelection.value=template
  console.log(templateSelection.value)
}
const handleTemplateSelection1 = (index) => {
  if (index==2){
    applyAllTemplate.value = true
    chooseTemplate(templateSelection.value)
    dialogVisible1.value=false
  }else if(index==1){
    applyAllTemplate.value = false
    chooseTemplate(templateSelection.value)
    dialogVisible1.value=false
  }
}
const handleChangeTool = (item) => {
  rightTools.forEach((child) => {
    if (child.name == item.name) {
@@ -927,39 +825,23 @@
      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 DeleteD = ref(false)
const selectPPT = ref({
  id:"",
  pictureUrl: '',
  innerPicture: {
    //定义画中画对象,属性与数字人相同
    name: '画中画',
    src: '',
    cover: '',
@@ -968,7 +850,7 @@
    originWidth: 0,
    originHeight: 0,
    category: 1,
    depth: 1, //画中画1-100
    depth: 1,
    top: 0,
    marginLeft: 0,
    businessId: generateUUID(),
@@ -978,32 +860,62 @@
  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,
@@ -1015,6 +927,7 @@
  extInfo: '{"addMode":true,"docType":1,"pptNotes":true,"pptContent":false,"notesPolish":false}',
  resolveType: 1
})
//智能讲稿组件begin
//添加ref
const rewriterRef = ref()
@@ -1025,6 +938,18 @@
  }
  return selectPPT.value?.pictureUrl || ''
})
//ppt人脸校验方法
const PPtIsHaveFace = async ()=>{
  IsEndCheckFace.value = false
  //添加ppt中人脸校验
  //向原始ppt添加数据,用作后续ppt中是否包含人脸的数据校验原始数据
  const InitPpt = PPTArr.value.map( (item)=>{
      return item.innerPicture.src
  } )
  const { detectFacesInImages } = useFaceDetection()
  IsHaveFace.value = await detectFacesInImages(InitPpt)
  IsEndCheckFace.value = true
}
// 打开智能讲稿弹窗
const openScriptRewriter = () => {
  if (!selectPPT.value?.pptRemark && !currentImageUrl.value) {
@@ -1045,12 +970,8 @@
  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
@@ -1069,37 +990,25 @@
  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
@@ -1111,7 +1020,6 @@
    pptTemplateApi.getSchedule(id).then((res) => {
      if (res && typeof res == 'string') {
        const progress = Number(res)
        // 添加解析失败的判断
        if (progress < 0) {
          clearInterval(schedulePPTTimer.value)
          showLeftList.value = true
@@ -1120,8 +1028,6 @@
        }
        percentagePPT.value = parseInt(`${progress * 100}`)
      } else if (res && res.length > 0) {
        console.log('解析成功', res)
        console.log('courseInfo', courseInfo.value)
        res.forEach((item) => {
          item.isActive = false
          item.isChecked = false
@@ -1132,18 +1038,25 @@
          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: '画中画',
@@ -1161,9 +1074,7 @@
              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: '',
@@ -1175,21 +1086,17 @@
              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]
        showLeftList.value = true
        clearInterval(schedulePPTTimer.value)
        //ppt人脸校验
        PPtIsHaveFace()
        //轮询保存课程
        /**
         * 后端数据库压力过大,暂时停止定时保存
@@ -1199,114 +1106,76 @@
    })
  }, 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(
    '是否复制这页PPT?',
    '是否复制该页面?',
    '提示',
    {
      confirmButtonText: '是',
      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(
    '是否删除这页PPT?',
    '是否删除该页面?',
    '提示',
    {
      confirmButtonText: '是',
      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)
    //已经进行过删除操作
    DeleteD.value = true
  }).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,
@@ -1315,6 +1184,9 @@
  gender: '',
  posture: ''
})
const selectHost = ref(null)
const getList = async () => {
  loading.value = true
  try {
@@ -1322,94 +1194,85 @@
    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
    }
  })
  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
// const chooseHost = (item) => {
//   hostList.value.forEach((el) => {
//     el.isActive = el.id === item.id
//   })
//
//   // 只更新当前选中的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
@@ -1421,41 +1284,23 @@
  })
}
//ppt人脸校验
const PPtIsHaveFace = async ()=>{
  //添加ppt中人脸校验
  //向原始ppt添加数据,用作后续ppt中是否包含人脸的数据校验原始数据
  const InitPpt = PPTArr.value.map( (item)=>{
      return item.innerPicture.src
  } )
  const { detectFacesInImages } = useFaceDetection()
  const IsHaveFace = await detectFacesInImages(InitPpt)
  return IsHaveFace
}
//获取保存时间
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,
@@ -1473,31 +1318,32 @@
    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 || ""
}
//传入 save 则代表保存,空字符传则是合成视频
const saveSubmit = async (type) => {
  if( type.length === 0 ){
    //此时为视频合成
    MakeLoading.value = true
    SaveLoading.value = true
  }else{
    //此时为保存
    SaveLoading.value = true
  }
  // 检查场景是否为空
  console.log( "是否删除", DeleteD.value )
  if (!PPTArr.value || PPTArr.value.length === 0) {
    message.warning('场景为空,请先上传PPT!')
    //关闭视频合成与保存按钮的loading动画
    MakeLoading.value = false
    SaveLoading.value = false
    return false
  }
  //人脸校验
  while(!IsEndCheckFace.value){} //一个空循环,主要为了避免极端情况下当用户点击保存按钮或者视频合成按钮时,人脸校验未完成的问题
  if( IsHaveFace.value && !DeleteD.value ){
    message.warning('当前ppt中存在人脸元素,为方便后续视频生成,请去除该元素')
    return
  }
  //保存课程
  let saveSubmitForm = {
    accountId: courseInfo.value.accountId,
@@ -1515,675 +1361,201 @@
    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))
        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)
        //关闭视频合成与保存按钮的loading动画
        MakeLoading.value = false
        SaveLoading.value = false
        //抛出异常
        throw error
  PPTArr.value.forEach((item, index) => {
    try {
      pageInfo.scenes.push(item.businessId)
      if (index === 0) {
        thumbnail = item.pictureUrl
      }
    })
  }
  try {
    saveSubmitForm.pageInfo = JSON.stringify(pageInfo)
    saveSubmitForm.thumbnail = thumbnail
    saveSubmitForm.scenes = cloneDeep(scenes)
    console.log('saveSubmitForm:', cloneDeep(saveSubmitForm))
  } catch (error) {
    //关闭视频合成与保存按钮的loading动画
    MakeLoading.value = false
    SaveLoading.value = false
    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') {
    //反正怎么走都会走save这一步,那就只在这一步进行一次人脸校验,如果后续合成视频按钮不再走保存,请将这一步也一并进行更改
    //主要因为wangEditor过于敏感
    const isHaveFace = await PPtIsHaveFace()
    if( isHaveFace ){
      message.warning('当前ppt中包含人脸元素, 为方便后续视频生成 ,请去除该元素')
      //关闭视频合成与保存按钮的loading动画
      MakeLoading.value = false
      SaveLoading.value = false
      return false
    if( DeleteD.value ){
      //如果进行过ppt删除操作则需要进行二次查看
      await PPtIsHaveFace()
      if( IsHaveFace.value ){
          message.warning('当前ppt中存在人脸元素,为方便后续视频生成,请去除该元素')
          return
      }
    }
    try {
      const res = await pptTemplateApi.coursesSave(stringifySafely(saveSubmitForm))
      const res = await pptTemplateApi.coursesSave(JSON.stringify(saveSubmitForm))
      if (res) {
        message.success('保存成功!')
        saveTime.value = getSaveTime()
        MakeLoading.value = false
        SaveLoading.value = false
        return true // 返回保存成功标志
        return true
      }
      return false
    } catch (error) {
      console.error('保存课程时出错:', error)
      message.error('保存失败,请重试')
      //关闭视频合成与保存按钮的loading动画
      MakeLoading.value = false
      SaveLoading.value = false
      return false
    }
  } else {
    // 合成视频前先保存
    try {
      const saveResult = await saveSubmit('save')
      if (!saveResult) {
        message.error('保存失败,请重试后再合成视频')
        //关闭视频合成与保存按钮的loading动画
        MakeLoading.value = false
        SaveLoading.value = false
        return
      }
      // 校验场景数据
      let warningStrArr: any = []
      let warningStrArr = []
      for (let i = 0; i < PPTArr.value.length; i++) {
        const item = PPTArr.value[i]
        console.log(item)
        console.log( "宽度", item.width )
        console.log( "高度", item.height )
        // 校验背景宽高
        if (!item.width || !item.height) {
          message.warning('背景尺寸无效,请检查宽高设置,或者重新选择模板')
          //关闭视频合成与保存按钮的loading动画
          MakeLoading.value = false
          SaveLoading.value = false
          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字,请减少或拆分场景`
            )
          }
        }
      }
      if (warningStrArr.length > 0) {
        warningDialog.value.open(warningStrArr.map((warning) => `<div>${warning}</div>`).join(''))
        //关闭视频合成与保存按钮的loading动画
        MakeLoading.value = false
        SaveLoading.value = false
        return
      }
      // 合成视频
      try {
        const res = await pptTemplateApi.megerMedia(saveSubmitForm)
        if (res) {
          //关闭视频合成与保存按钮的loading动画
          MakeLoading.value = false
          SaveLoading.value = false
          message.success('合成视频任务提交成功,请到我的视频中查看!')
        }
      } catch (error) {
        //关闭视频合成与保存按钮的loading动画
        MakeLoading.value = false
        SaveLoading.value = false
        console.error('合成视频失败:', error)
        message.error('合成视频失败,请重试')
      }
    } catch (error) {
      //关闭视频合成与保存按钮的loading动画
      MakeLoading.value = false
      SaveLoading.value = false
      console.error('保存或合成过程出错:', error)
      message.error('操作失败,请重试')
    }
  }
}
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) {
@@ -2210,10 +1582,12 @@
      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
@@ -2221,32 +1595,24 @@
      }
    })
  })
  // 数字人位置也需要缩放
  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)
      })
    }
@@ -2254,39 +1620,276 @@
  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%;
@@ -2353,7 +1956,6 @@
      margin: 0;
      div {
        // height: 30px;
        padding: 5px 10px;
        margin: 0;
        line-height: 30px;
@@ -2403,7 +2005,7 @@
      .list {
        position: relative;
        height: calc(152px * 9 / 16); // 使用缩略图的固定高度
        height: calc(152px * 9 / 16);
        margin: 20px 0;
        box-sizing: content-box;
@@ -2421,7 +2023,6 @@
          border-radius: 5px;
        }
        // 确保背景图片填充整个容器
        .background {
          position: absolute;
          width: 100%;
@@ -2436,8 +2037,6 @@
        .ppt-bg {
          z-index: 2;
          // width: 152px;
          // height: 100%;
        }
        .host {
@@ -2470,7 +2069,7 @@
    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;
@@ -2486,8 +2085,6 @@
      .main-image-box {
        position: relative;
        // width: 760px;
        // height: 360px;
        border: 1px solid #ebeef5;
        box-sizing: content-box;
      }
@@ -2518,39 +2115,9 @@
      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;
        }
      }
    }
@@ -2668,10 +2235,10 @@
          position: absolute;
          top: 0;
          left: 0;
          z-index: 1; /* 背景在底层 */
          z-index: 1;
          width: 100%;
          height: 100%;
          background-color: #f0f1fa; /* 设置底色 */
          background-color: #f0f1fa;
        }
        .host-name {
@@ -2689,7 +2256,7 @@
        }
        .ppt-bg {
          z-index: 2; /* 图片在背景之上 */
          z-index: 2;
          width: 100%;
          height: 100%;
        }
@@ -2744,12 +2311,12 @@
      }
      .ppt-bg {
        position: absolute;
        z-index: 2; /* 图片在背景之上 */
        z-index: 2;
      }
      .human-image {
        position: absolute;
        z-index: 3; /* 图片在背景之上 */
        z-index: 3;
      }
    }
  }
@@ -2758,10 +2325,10 @@
    position: absolute;
    top: 0;
    left: 0;
    z-index: 1; /* 背景在底层 */
    z-index: 1;
    width: 100%;
    height: 100%;
    background-color: #f0f1fa; /* 设置底色 */
    background-color: #f0f1fa;
  }
  .template-tool {
@@ -2798,25 +2365,22 @@
  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) {
@@ -2843,4 +2407,7 @@
    background-color: #1989fa;
  }
}
.dialog-footer{
  float: right;
}
</style>
easegen-front/src/views/digitalcourse/template/index.vue
@@ -74,7 +74,6 @@
      <el-table-column label="背景图片" align="center" prop="bgImage">
        <template #default="scope">
          <el-image
            style="width: 100px; height: 60px"
            :src="scope.row.bgImage"
            :preview-src-list="[scope.row.bgImage]"
            fit="cover"
@@ -85,7 +84,6 @@
      <el-table-column label="预览图片" align="center" prop="previewImage">
        <template #default="scope">
          <el-image
            style="width: 100px; height: 60px"
            :src="scope.row.previewImage"
            :preview-src-list="[scope.row.previewImage]"
            fit="cover"
easegen-front/src/views/myCourse/index.vue
@@ -534,6 +534,9 @@
const currentPlayUrl = ref('')
const videoRefs = ref<HTMLVideoElement[]>([])
//计时器
const Timer = ref()
// 获取视频列表
const getList = async () => {
  loading.value = true
@@ -1167,10 +1170,28 @@
  stopPolling()
})
//计时器轮询
const TimerList = ( () => {
  Timer.value = setInterval( ()=>{getList()}, 5000 )
} )
//清楚计时器
const ClearTimerList = ( () => {
  clearInterval(Timer.value)
  Timer.value = null
} )
// 初始化
onMounted(() => {
  //加载数据
  getList()
  //调用计时器
  TimerList()
})
//清楚计时器
onUnmounted( ()=>{
  ClearTimerList()
} )
// 播放预览
const playPreview = (type: 'titles' | 'trailer') => {
easegen-front/src/views/pptTemplateList/index.vue
@@ -116,7 +116,7 @@
          <!--          <el-icon size="20" color="#ffffff" style="margin-right: 5px" @click.stop="copyItem(item.id)">-->
          <!--            <CopyDocument />-->
          <!--          </el-icon>-->
          <el-icon size="20" color="#ffffff" style="margin-right: 5px" @click.stop="deleteItem(item.id)">
          <el-icon size="20" color="#ffcf49" style="margin-right: 5px" @click.stop="deleteItem(item.id)">
            <Delete />
          </el-icon>
        </div>
yudao-module-digitalcourse/yudao-module-digitalcourse-biz/src/main/java/cn/iocoder/yudao/module/digitalcourse/service/courses/CoursesServiceImpl.java
@@ -181,7 +181,7 @@
        // 根据传入的userid创建查询条件
        LambdaQueryWrapper<CoursesDO> queryWrapper = new LambdaQueryWrapper<CoursesDO>()
                .eq(CoursesDO::getCreator, Long.valueOf(userid));
        // 使用带有查询条件的分页查询
        PageResult<CoursesDO> pageResult = coursesMapper.selectPage(pageReqVO, queryWrapper);
@@ -297,12 +297,12 @@
    private void refreshCourseCache(String courseId) {
        // 获取课程完整信息
        AppCoursesUpdateReqVO courseInfo = getCourses(Long.parseLong(courseId));
        if (courseInfo == null || CollectionUtil.isEmpty(courseInfo.getScenes())) {
            log.error("课程信息不存在或场景为空,courseId: {}", courseId);
            return;
        }
        // 构建场景缓存数据
        Map<String, Map<String, String>> scenesMap = new HashMap<>();
        courseInfo.getScenes().forEach(scene -> {
@@ -311,7 +311,7 @@
            sceneData.put("background", scene.getBackground().getSrc());
            scenesMap.put(String.valueOf(scene.getOrderNo()), sceneData);
        });
        // 序列化并存储场景数据
        String sceneRedisKey = COURSE_SCENE_TEXT_KEY + courseId;
@@ -481,4 +481,4 @@
        }
    }
}
}
yudao-module-digitalcourse/yudao-module-digitalcourse-biz/src/main/java/cn/iocoder/yudao/module/digitalcourse/service/coursescenes/CourseScenesServiceImpl.java
@@ -168,7 +168,6 @@
    }
    @Override
    @Async
    public void batchRemoveCouseScenes(Long id) {
        System.out.println(System.currentTimeMillis()+"      删除开始");
        List<CourseScenesDO> courseScenesDOS = courseScenesMapper.selectList(new QueryWrapperX<CourseScenesDO>().lambda().eq(CourseScenesDO::getCourseId, id));
@@ -247,4 +246,4 @@
        return courseScenesMapper.selectPage(pageReqVO);
    }
}
}