Flex
2025-04-22 0db91dc48b76629787f0d92ff99917e4547f828b
easegen-front/src/views/chooseTemplate/index.vue
@@ -16,7 +16,7 @@
          @blur="saveEdit"
          @keydown.enter="saveEdit"
        />
        <!-- 如果不在编辑,显示文本 -->
        <div
          v-else
@@ -31,10 +31,8 @@
      </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" >{{
        <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>
@@ -128,7 +126,7 @@
                    <div class="icon-content">
                      <el-icon
                        size="20"
                        color="#FFA500"
                        color="#ffffff"
                        style="margin-right: 5px"
                        @click.stop="copyDocument(element, index)"
                      >
@@ -136,7 +134,7 @@
                      </el-icon>
                      <el-icon
                        size="20"
                        color="#FFA500"
                        color="#ffffff"
                        style="margin-right: 5px"
                        @click.stop="deleteDocument(element)"
                      >
@@ -256,7 +254,7 @@
                    class="minddle-host-image"
                    :src="selectHost ? selectHost.pictureUrl : ''"
                  />
                  <el-icon
                    v-if="PPTpositon.active"
                    size="20"
@@ -297,7 +295,7 @@
                  class="minddle-host-image"
                  :src="selectHost ? selectHost.pictureUrl : ''"
                />
                <el-icon
                  v-if="PPTpositon.active"
                  size="20"
@@ -560,7 +558,7 @@
      <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>
@@ -569,7 +567,7 @@
      <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>
@@ -630,7 +628,7 @@
<script lang="ts" setup>
import { ref, reactive, onMounted } 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 { config } from '@/config/axios/config'
@@ -679,10 +677,7 @@
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'
const editorHtml = useEditorHtml()
const router = useRouter() // 路由
const route = useRoute() //
@@ -692,10 +687,6 @@
const message = useMessage()
const isEditing = ref(false)
const inputRef = ref(null)
//保存的加载动画
const SaveLoading = ref(false)
//视频合成的加载动画
const MakeLoading = ref(false)
// 切换到编辑模式
const toggleEdit = () => {
  isEditing.value = true
@@ -707,7 +698,7 @@
const onDragMove = (evt, data) => {
  console.log(evt)
  console.log(data)
  // 限制坐标
  if (data.x < -100) {
    data.x = -100; // 可以设置最小坐标为 -100
@@ -722,7 +713,7 @@
  courseInfo.value.name = editName.value
}
let humanId = 0
//课程基本信息
const courseInfo = ref({
  id: 0,
@@ -744,7 +735,7 @@
    courseInfo.value.height = newAspect === '16:9' ? 1080 : 1920
  }
)
const editName = ref(courseInfo.value.name)
const viewSize = reactive({
  width: 800,
@@ -763,7 +754,7 @@
  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,
@@ -772,7 +763,7 @@
  depth: 0,
  active: false
})
const componentsInfo = reactive({
  width: PPTpositon.w / 5,
  height: PPTpositon.h / 4,
@@ -792,7 +783,7 @@
const showImageSet = ref(false)
//是否将模板应用到所有页面
const applyAllTemplate = ref(false)
const xScale = viewSize.width / thumViewSize.width
// const yScale = viewSize.height / thumViewSize.height
//左侧ppt数字人位置
@@ -814,13 +805,13 @@
const state = reactive({
  dragging: false
})
//预设模板
const TEMPLATE_PRESETS = ref([])
const templates = ref([])
const selectTemplate = ref([])
//数字人tab
const tabs1 = [
  {
@@ -949,14 +940,13 @@
    showInnerPictureTool.value = true
  }
}
const PPTArr = ref()
//ppt解析进度
const percentagePPT = ref(0)
const showLeftList = ref(true)
const selectPPT = ref({
  id:"",
  pictureUrl: '',
  innerPicture: {
    //定义画中画对象,属性与数字人相同
@@ -1044,18 +1034,18 @@
  file.uid = genFileId()
  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
}
const uploadExplainRef = ref()
const handleSuccess = (rawFile) => {
  message.success('上传成功!')
@@ -1063,12 +1053,12 @@
  uploadExplainRef.value.open()
  uploadRef.value!.clearFiles()
}
const handleError = (err) => {
  message.error('上传失败,请重试')
  console.error('Upload error:', err)
}
//上传音频
const uploadAudioRef = ref()
const handleAudioSuccess = (rawFile) => {
@@ -1120,7 +1110,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
@@ -1188,6 +1177,7 @@
        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)
        //轮询保存课程
@@ -1211,7 +1201,7 @@
    // 计算总字数 - 修改为去除SSML标签后再计算长度
    videoText.value = val.reduce((prev, curr) => {
      if (!curr.pptRemark) return prev;
      // 去除所有SSML标签,只保留文本内容
      const plainText = curr.pptRemark;
      return prev + plainText.length;
@@ -1244,48 +1234,13 @@
  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)
    })
    .catch(() => {
      ElMessage({
        type: 'info',
        message: '已取消复制',
      })
    })
  let copyItem = cloneDeep(item)
  copyItem.id = generateUUID()
  copyItem.isActive = false
  PPTArr.value.splice(index + 1, 0, copyItem)
}
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: '已取消删除',
      })
  })
  PPTArr.value = PPTArr.value.filter((child) => child.id !== item.id)
}
const deleteDigitalHuman = () => {
  selectPPT.value.showDigitalHuman = false
@@ -1407,7 +1362,7 @@
      scene.selectAudio = data[0]
    })
  }
}
//生成课程id
const coursesCreate = () => {
@@ -1415,26 +1370,13 @@
    accountId: userId.value
  }
  pptTemplateApi.coursesCreate(params).then((res) => {
    console.log(res)
    if (res) {
      courseInfo.value.id = res
    }
  })
}
//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 = () => {
@@ -1454,7 +1396,7 @@
  return h + ':' + m + ':' + s
}
const warningDialog = ref()
// 语速 音量
const voiceData = reactive({
  show: false,
@@ -1478,26 +1420,14 @@
  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
  }
  // 检查场景是否为空
  if (!PPTArr.value || PPTArr.value.length === 0) {
    message.warning('场景为空,请先上传PPT!')
    //关闭视频合成与保存按钮的loading动画
    MakeLoading.value = false
    SaveLoading.value = false
    return false
  }
  //保存课程
  let saveSubmitForm = {
    accountId: courseInfo.value.accountId,
@@ -1520,7 +1450,7 @@
  // }else{
  //   Reflect.set(saveSubmitForm, "courseMediaId", courseInfo.value.id);
  // }
  //组装数据
  const scenes: any = []
  const pageInfo = {
@@ -1552,6 +1482,7 @@
    matting: 1,
    marker: 1
  }
  let pageNum = 1
  if (PPTArr.value && PPTArr.value.length > 0) {
    console.log('开始处理PPTArr数据')
@@ -1564,9 +1495,10 @@
          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, '')
@@ -1635,78 +1567,54 @@
        scenes.push(formatItem)
      } catch (error) {
        console.error(`处理第 ${index + 1} 个场景时出错:`, error)
        //关闭视频合成与保存按钮的loading动画
        MakeLoading.value = false
        SaveLoading.value = false
        //抛出异常
        throw error
      }
    })
  }
  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) {
    //关闭视频合成与保存按钮的loading动画
    MakeLoading.value = false
    SaveLoading.value = false
    console.error('保存表单数据时出错:', error)
  }
  if (type == 'save') {
    //反正怎么走都会走save这一步,那就只在这一步进行一次人脸校验,如果后续合成视频按钮不再走保存,请将这一步也一并进行更改
    //主要因为wangEditor过于敏感
    const isHaveFace = await PPtIsHaveFace()
    if( isHaveFace ){
      message.warning('当前ppt中包含人脸元素, 为方便后续视频生成 ,请去除该元素')
      //关闭视频合成与保存按钮的loading动画
      MakeLoading.value = false
      SaveLoading.value = false
      return false
    }
    try {
      const res = await pptTemplateApi.coursesSave(stringifySafely(saveSubmitForm))
      if (res) {
        message.success('保存成功!')
        saveTime.value = getSaveTime()
        MakeLoading.value = false
        SaveLoading.value = false
        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 = []
      for (let i = 0; i < PPTArr.value.length; i++) {
        const item = PPTArr.value[i]
        console.log(item)
        // 校验背景宽高
        if (!item.width || !item.height) {
          message.warning('背景尺寸无效,请检查宽高设置,或者重新选择模板')
          //关闭视频合成与保存按钮的loading动画
          MakeLoading.value = false
          SaveLoading.value = false
          return
        }
        if (item.driverType == 1) {
@@ -1727,40 +1635,28 @@
          }
        }
      }
      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) => {
@@ -1795,7 +1691,7 @@
  message.warning('最多上传一个声音驱动文件!')
}
const currentAudio = ref()
const handlePptRemarkSelection = () => {
  if (textareaRef.value) {
    const textarea = textareaRef.value.$el.querySelector('textarea')
@@ -1806,7 +1702,6 @@
    }
  }
}
//富文本编辑器  -start
// 编辑器实例,必须用 shallowRef
const editorRef = shallowRef()
@@ -1881,13 +1776,13 @@
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)
@@ -1900,26 +1795,26 @@
    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 {
@@ -1940,26 +1835,26 @@
    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 {
@@ -1973,9 +1868,9 @@
    }
  }
  // 构建请求参数
}
//取消试听
const pauseAudio = () => {
@@ -1997,16 +1892,16 @@
    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()
@@ -2090,26 +1985,26 @@
        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]
          })
@@ -2117,7 +2012,7 @@
          // 设置选中的数字人
          selectHost.value = foundHost || hostList.value[0]
        }
        // 设置选中的数字人
        selectHost.value = foundHost || hostList.value[0]
        if (hostInfo) {
@@ -2156,7 +2051,7 @@
      const pageInfo = res.pageInfo ? JSON.parse(res.pageInfo) : ''
      uploadFileObj.filename = pageInfo ? pageInfo.docInfo.fileName : ''
      uploadFileObj.size = pageInfo ? pageInfo.docInfo.fileSize : ''
      //应用模板 这里用户可能已经调整了模板,所以这里不应用模板
      // applyTemplate()
    }
@@ -2173,11 +2068,11 @@
  currTemplate.isActive = true
  applyTemplate()
}
const applyTemplate = (ppt = null) => {
  const template = selectTemplate.value
  const pptList = applyAllTemplate.value ? PPTArr.value : [selectPPT.value]
  // 数字人是统一生效的,先处理
  console.log(template)
  pptList.forEach((item) => {
@@ -2209,7 +2104,7 @@
      item.pictureUrl = originalPPT
      item.innerPicture.src = ''
    }
    item.showDigitalHuman = template.showDigitalHuman
    // 添加同步宽高的逻辑
    const targetTemplate = selectTemplate.value
@@ -2221,26 +2116,26 @@
      }
    })
  })
  // 数字人位置也需要缩放
  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, '\\$&') // 转义正则中的特殊字符
}
const handleReplacement = (replacements) => {
  PPTArr.value.forEach((item) => {
    if (item.pptRemark) {
@@ -2253,7 +2148,7 @@
  })
  message.success('批量替换成功!')
}
onMounted(async () => {
  let data = await TemplateApi.getTemplatePage(queryParams)
  TEMPLATE_PRESETS.value = data.list.map((item) => {
@@ -2292,13 +2187,13 @@
  height: 100%;
  background-color: #f5f7fa;
}
.minddle-host-image {
  z-index: 5;
  width: 100%;
  height: 100%;
}
.template-top {
  display: flex;
  height: 60px;
@@ -2308,50 +2203,50 @@
  border: 1px solid #ebeef5;
  box-shadow: 0 3px 6px rgb(175 175 175 / 16%);
  justify-content: space-between;
  .top-left {
    display: flex;
    align-items: center;
    .top-icon {
      display: flex;
      align-items: center;
    }
    .back-text {
      margin-right: 20px;
      margin-left: 10px;
      cursor: pointer;
    }
    span {
      margin: 0 25px;
    }
  }
  .top-right {
    span {
      margin: 0 20px;
    }
  }
}
.template-main {
  display: flex;
  height: calc(100% - 82px);
  padding: 10px;
  justify-content: space-around;
  .template-left {
    position: relative;
    width: 180px;
    background-color: #fff;
    border: 1px solid #ebeef5;
    box-shadow: 0 3px 6px rgb(175 175 175 / 16%);
    .page {
      margin: 0;
      div {
        // height: 30px;
        padding: 5px 10px;
@@ -2359,7 +2254,7 @@
        line-height: 30px;
        border-bottom: 1px solid #ebeef5;
      }
      .line {
        width: 30px;
        height: 3px;
@@ -2367,12 +2262,12 @@
        margin: 0;
        background-color: aqua;
      }
      .upload-demo {
        text-align: center;
      }
    }
    .left-upload-setting {
      display: flex;
      height: calc(100% - 86px);
@@ -2381,32 +2276,32 @@
      flex-direction: column;
      justify-content: center;
      align-items: center;
      div {
        line-height: 40px;
      }
      ::v-deep(.el-progress-bar) {
        width: 180px;
      }
      .el-button {
        margin: 20px 0;
      }
    }
    .image-list {
      height: calc(100% - 70px);
      padding: 10px;
      overflow: hidden auto;
      border-bottom: 1px solid #ebeef5;
      .list {
        position: relative;
        height: calc(152px * 9 / 16); // 使用缩略图的固定高度
        margin: 20px 0;
        box-sizing: content-box;
        .list-index {
          position: absolute;
          top: 10px;
@@ -2420,7 +2315,7 @@
          background: #122121;
          border-radius: 5px;
        }
        // 确保背景图片填充整个容器
        .background {
          position: absolute;
@@ -2433,18 +2328,18 @@
          height: 100%;
          background-color: rgba(0, 0, 0, 0);
        }
        .ppt-bg {
          z-index: 2;
          // width: 152px;
          // height: 100%;
        }
        .host {
          position: absolute;
          z-index: 3;
        }
        .icon-content {
          position: absolute;
          top: 0;
@@ -2456,7 +2351,7 @@
        }
      }
    }
    .page-btn {
      position: absolute;
      bottom: 10px;
@@ -2464,7 +2359,7 @@
      padding: 0 10px;
    }
  }
  .template-middle {
    display: flex;
    width: 56%;
@@ -2473,17 +2368,17 @@
    flex-grow: 1; // 确保中间区域可以自适应高度
    flex-direction: column;
    justify-content: flex-start;
    .middle-top {
      padding: 5px 20px;
    }
    .main-box {
      display: flex;
      padding: 10px 20px;
      border: 1px solid #ebeef5;
      justify-content: center;
      .main-image-box {
        position: relative;
        // width: 760px;
@@ -2491,20 +2386,20 @@
        border: 1px solid #ebeef5;
        box-sizing: content-box;
      }
      .list {
        position: relative;
        display: flex;
        width: 95%;
        justify-content: center;
      }
      .ppt-bg {
        z-index: 2;
        width: 100%;
        height: 100%;
      }
      .host {
        position: absolute;
        right: 0;
@@ -2512,12 +2407,12 @@
        width: 300px;
      }
    }
    .voice-main {
      display: flex;
      justify-content: space-between;
      padding: 10px;
      .voice-item {
        width: 180px;
        height: 30px;
@@ -2525,7 +2420,7 @@
        cursor: pointer;
        background-color: #c9c9c9;
        border-radius: 12px;
        span {
          display: inline-block;
          width: 50%;
@@ -2533,17 +2428,17 @@
          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;
@@ -2553,30 +2448,30 @@
        }
      }
    }
    .audio-upload {
      display: flex;
      align-items: center;
      justify-content: center;
      height: 200px;
    }
    .middle-textarea {
      padding: 5px 20px;
    }
    .tool-box {
      display: flex;
      padding: 10px;
      border-top: 1px solid #ebeef5;
      justify-content: space-between;
      .tool-btn {
        display: flex;
        align-items: center;
      }
    }
    .audio-play {
      position: absolute;
      top: 0;
@@ -2593,25 +2488,25 @@
      flex-direction: column;
    }
  }
  .template-right {
    position: relative;
    width: 20%;
    background-color: #fff;
    box-shadow: 0 3px 6px rgb(175 175 175 / 16%);
    .tabs-1 {
      display: flex;
      justify-content: space-around;
      padding: 10px 30px;
      border-bottom: 1px solid #ebeef5;
      .tabs-item {
        width: 30px;
        font-size: 14px;
        text-align: center;
        cursor: pointer;
        span {
          display: block;
          width: 30px;
@@ -2621,12 +2516,12 @@
        }
      }
    }
    .tabs-2 {
      display: flex;
      padding: 10px;
      justify-content: space-around;
      div {
        width: 60px;
        height: 30px;
@@ -2635,13 +2530,13 @@
        cursor: pointer;
        border-radius: 5px;
      }
      .tabs-active {
        color: #fff !important;
        background-color: #409eff;
      }
    }
    .apply-all {
      position: absolute;
      bottom: 80px;
@@ -2649,12 +2544,12 @@
      width: 100%;
      justify-content: center;
    }
    .host-list {
      height: 80%;
      overflow-y: auto;
      border-top: 1px solid #ebeef5;
      .host-item {
        position: relative;
        display: inline-block;
@@ -2663,7 +2558,7 @@
        margin: 5px 0;
        margin-left: 10px;
        cursor: pointer;
        .background {
          position: absolute;
          top: 0;
@@ -2673,7 +2568,7 @@
          height: 100%;
          background-color: #f0f1fa; /* 设置底色 */
        }
        .host-name {
          position: absolute;
          bottom: 10px;
@@ -2687,7 +2582,7 @@
          background: rgb(225 225 225 / 70%);
          border-radius: 5px;
        }
        .ppt-bg {
          z-index: 2; /* 图片在背景之上 */
          width: 100%;
@@ -2696,31 +2591,31 @@
      }
    }
  }
  .image-setting {
    padding: 10px 20px;
    .img-setting {
      display: flex;
      align-items: center;
      line-height: 40px;
      .setting-label {
        width: 120px;
      }
      ::v-deep(.el-input) {
        width: 170px;
        margin-left: 10px;
      }
    }
  }
  .template-list {
    height: 90%;
    overflow-y: auto;
    border-top: 1px solid #ebeef5;
    .template-item {
      position: relative;
      display: inline-block;
@@ -2746,14 +2641,14 @@
        position: absolute;
        z-index: 2; /* 图片在背景之上 */
      }
      .human-image {
        position: absolute;
        z-index: 3; /* 图片在背景之上 */
      }
    }
  }
  .background {
    position: absolute;
    top: 0;
@@ -2763,25 +2658,25 @@
    height: 100%;
    background-color: #f0f1fa; /* 设置底色 */
  }
  .template-tool {
    width: 60px;
    padding: 10px;
    background-color: #fff;
    box-shadow: 0 3px 6px rgb(175 175 175 / 16%);
    .tool-item {
      display: flex;
      padding: 10px 20px;
      cursor: pointer;
      flex-direction: column;
      align-items: center;
      img {
        width: 32px;
        height: 32px;
      }
      .tool-name {
        width: 60px;
        margin-top: 6px;
@@ -2792,50 +2687,50 @@
    }
  }
}
::v-deep(.el-pagination) {
  position: absolute;
  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确保在最顶层
}
.voice-card :deep(.el-card__body) {
  padding: 0;
}
.speech-slider {
  &:deep(.el-slider__bar) {
    display: none;
  }
  &:deep(.el-slider__runway) {
    height: 2px;
  }
  &:deep(.el-slider__button-wrapper) {
    top: -17px;
  }
  &:deep(.el-slider__marks-stop) {
    top: -5px;
    width: 12px;
@@ -2843,4 +2738,4 @@
    background-color: #1989fa;
  }
}
</style>
</style>