shenrongliang
2025-04-16 a25aa103c05348214df4eea6b091fac654dfa20d
easegen-front/src/views/myCourse/index.vue
@@ -1,5 +1,5 @@
<template>
  <!-- 搜索 -->
  <!-- 搜索区域 -->
  <ContentWrap>
    <el-form
      class="-mb-15px"
@@ -18,7 +18,7 @@
        />
      </el-form-item>
      <el-form-item>
        <el-button @click="handleQuery">
        <el-button @click="handleQuery" type="primary">
          <Icon icon="ep:search" class="mr-5px" />
          {{t('table.search')}}
        </el-button>
@@ -30,30 +30,25 @@
    </el-form>
  </ContentWrap>
  <!-- 列表 -->
  <!-- 视频列表 -->
  <ContentWrap>
    <el-table v-loading="loading" :data="list">
      <el-table-column :label="t('myCourse.videoCode')" align="center" prop="id" />
      <el-table-column :label="t('myCourse.videoName')" align="center" prop="name" />
<!--      <el-table-column :label="t('myCourse.duration')" align="center" prop="duration">-->
<!--        <template #default="scope">-->
<!--          {{ formatDuration(scope.row.duration) }}-->
<!--        </template>-->
<!--      </el-table-column>-->
      <el-table-column label="排队个数" align="center" prop="pos" >
    <el-table v-loading="loading" :data="list" style="width: 100%">
      <el-table-column :label="t('myCourse.videoCode')" align="center" prop="id" width="100" />
      <el-table-column :label="t('myCourse.videoName')" align="center" prop="name" min-width="150" />
      <el-table-column label="排队个数" align="center" prop="pos" width="100">
        <template #default="scope">
         <span v-if="scope.row.pos==0">视频正在合成...</span>
          <span v-if="scope.row.pos==0">视频正在合成...</span>
          <span v-else>{{ scope.row.pos }}</span>
        </template>
      </el-table-column>
      <el-table-column label="进度" align="center" prop="progressVideo">
      <el-table-column label="进度" align="center" prop="progressVideo" width="100">
        <template #default="scope">
          <span v-if="scope.row.status==2">100%</span>
          <span v-else>{{ calculateProgress(scope.row.progressVideo) }}%</span>
        </template>
      </el-table-column>
      <el-table-column :label="t('myCourse.courseName')" align="center" prop="courseName">
      <el-table-column :label="t('myCourse.courseName')" align="center" prop="courseName" min-width="150">
        <template #default="scope">
          <el-link type="primary" @click="goDetail(scope.row.courseId)">{{ scope.row.courseName }}</el-link>
        </template>
@@ -62,94 +57,119 @@
        :label="t('table.createTime')"
        align="center"
        prop="createTime"
        width="120"
        width="160"
        :formatter="dateFormatter"
      />
      <el-table-column
        :label="t('myCourse.finishTime')"
        align="center"
        prop="finishTime"
        width="120"
        width="160"
        :formatter="dateFormatter"
      />
      <el-table-column :label="t('myCourse.SynthesisTime')" align="center">
      <el-table-column :label="t('myCourse.SynthesisTime')" align="center" width="120">
        <template #default="scope">
          {{ calculateDuration(scope.row.createTime, scope.row.finishTime) }}
        </template>
      </el-table-column>
      <el-table-column :label="t('myCourse.errorReason')" align="center" prop="errorReason">
      <el-table-column :label="t('myCourse.errorReason')" align="center" prop="errorReason" width="150">
        <template #default="scope">
          <el-tooltip :content="scope.row.errorReason || '--'" placement="top">
            <span>
              {{ scope.row.errorReason ? (scope.row.errorReason.length > 20 ? scope.row.errorReason.slice(0, 20) + '...' : scope.row.errorReason) : '--' }}
              {{ scope.row.errorReason ? (scope.row.errorReason.length > 10 ? scope.row.errorReason.slice(0, 10) + '...' : scope.row.errorReason) : '--' }}
            </span>
          </el-tooltip>
        </template>
      </el-table-column>
      <el-table-column :label="t('myCourse.status')" align="center" prop="status">
      <el-table-column :label="t('myCourse.status')" align="center" prop="status" width="120">
        <template #default="scope">
          <dict-tag v-if="scope.row.status==2 && scope.row.subtitlesAddStatus!=null" :type="DICT_TYPE.video_zi" :value="scope.row.subtitlesAddStatus" />
          <dict-tag v-else :type="DICT_TYPE.VIDEO_STATUS" :value="scope.row.status" />
        </template>
      </el-table-column>
      <el-table-column :label="t('table.action')" align="center" min-width="110" fixed="right">
      <el-table-column :label="t('table.action')" align="center" width="230" fixed="right">
        <template #default="scope">
          <template v-if="scope.row.status == 2">
            <el-button
              link
              type="primary"
              @click="openPreview(scope.row)"
            >
              {{t('myCourse.preview')}}
            </el-button>
            <el-button
              link
              type="primary"
              @click="handleDownload(scope.row.previewUrl,scope.row.courseName)"
            >
              {{t('myCourse.downloadVideo')}}
            </el-button>
          </template>
          <template v-if=" scope.row.status == 3">
            <el-button
              link
              type="warning"
              @click="reMegerMedia(scope.row.id)"
            >
              {{t('myCourse.resynthesize')}}
            </el-button>
          </template>
          <template v-if="scope.row.status == 2">
            <el-button
              link
              type="primary"
              @click="openSubtitleDialog(scope.row.id)"
            >
              字幕
            </el-button>
          </template>
          <template v-if="scope.row.subtitlesAddStatus == 2">
            <el-button
              link
              type="primary"
              @click="handleDownload(scope.row.videoUrl,scope.row.courseName)"
            >
             下载字幕合成视频
            </el-button>
          </template>
        <template v-if="scope.row.status == 2 || scope.row.status==3">
          <el-button
            link
            type="danger"
            @click="handleDelete(scope.row.id)"
          >
            {{ t('action.del') }}
          </el-button>
        </template>
          <el-button-group>
            <template v-if="scope.row.status == 2">
              <el-button
                type="text"
                @click="openPreview(scope.row)"
                plain
              >
                {{t('myCourse.preview')}}
              </el-button>
              <el-button
                type="text"
                @click="handleHeaderFooter(scope.row)"
                plain
              >
                片头片尾
              </el-button>
              <template v-if="scope.row.status == 2 || scope.row.status==3">
                <el-button
                  type="text"
                  @click="handleDelete(scope.row.id)"
                  plain
                >
                  {{ t('action.del') }}
                </el-button>
              </template>
              <template v-if="scope.row.status == 3">
                <el-button
                  type="text"
                  @click="reMegerMedia(scope.row.id)"
                  plain
                >
                  {{t('myCourse.resynthesize')}}
                </el-button>
              </template>
              <template v-if="scope.row.status == 2">
                <el-button
                  type="text"
                  @click="openSubtitleDialog(scope.row.id)"
                  plain
                >
                  字幕
                </el-button>
              </template>
            </template>
            <el-dropdown>
              <el-button type="text" plain>
                更多<el-icon class="el-icon--right"><arrow-down /></el-icon>
              </el-button>
              <template #dropdown>
                <el-dropdown-menu>
                  <el-dropdown-item
                    v-if="scope.row.subtitlesAddStatus == 2"
                    @click="handleDownload(scope.row.videoUrl, scope.row.courseName + '_字幕合成视频')"
                  >
                    <Icon class="mr-2" />下载字幕合成视频
                  </el-dropdown-item>
                  <el-dropdown-item
                    @click="handleDownload(scope.row.previewUrl, scope.row.courseName + '_视频')"
                  >
                    <Icon class="mr-2" />下载视频
                  </el-dropdown-item>
                  <el-dropdown-item
                    v-if="scope.row.compositeVideo!=null"
                    @click="handleDownload(scope.row.compositeVideo, scope.row.courseName + '_片头片尾视频')"
                  >
                    <Icon class="mr-2" />下载片头片尾视频
                  </el-dropdown-item>
                  <el-dropdown-item
                    v-if="scope.row.titles!=null && scope.row.trailer!=null"
                    @click="mergeHeaderFooter(scope.row.id)"
                  >
                    <Icon class="mr-2" />合成片头片尾视频
                  </el-dropdown-item>
                </el-dropdown-menu>
              </template>
            </el-dropdown>
          </el-button-group>
        </template>
      </el-table-column>
    </el-table>
    <!-- 分页 -->
    <Pagination
      :total="total"
@@ -210,7 +230,6 @@
                @change="handleFileUpload"
              />
            </el-button>
            <!-- 当查看字幕按钮转圈时,字幕视频合成也转圈 -->
            <el-button
              type="primary"
              @click="downloadSubtitles"
@@ -250,9 +269,7 @@
          >
            保存字幕
          </el-button>
        </div>
      </el-form-item>
      <el-form-item label="预览视频" v-if="subtitleForm.subtitlesAddStatus==2">
        <div style="width: 100%;">
@@ -268,15 +285,56 @@
      </el-form-item>
    </el-form>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="subtitleDialogVisible = false">关 闭</el-button>
      </div>
    </template>
  </el-dialog>
  <!-- 片头片尾设置弹框 -->
  <el-dialog
    v-model="headerFooterDialogVisible"
    title="片头片尾设置"
    width="50%"
  >
    <el-form :model="headerFooterForm" label-width="120px">
      <el-form-item label="片头视频">
        <UploadFile v-model="headerFooterForm.titles" :fileType="['mp4']" :limit="1" @on-success="handleFileSuccess('audition', $event)"/>
      </el-form-item>
      <el-form-item label="片尾视频">
        <UploadFile v-model="headerFooterForm.trailer" :fileType="['mp4']" :limit="1" @on-success="handleFileSuccess1('audition', $event)"/>
      </el-form-item>
    </el-form>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="headerFooterDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="applyHeaderFooter" :loading="applyingHeaderFooter">应用</el-button>
      </div>
    </template>
  </el-dialog>
  <el-dialog
    v-model="dialogVisible"
    title="视频合成中"
    width="50%"
  >
    <el-form :model="formData1" label-width="120px">
      <el-form-item  label="视频格式">
        <el-select v-model="formData1.isvideo">
          <el-option :disabled="formData1.videoUrl==null" label="字幕视频" :value="1" />
          <el-option label="原视频" :value="2" />
        </el-select>
      </el-form-item>
    </el-form>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="hecheng" :loading="applyingHeaderFooter">合成</el-button>
      </div>
    </template>
  </el-dialog>
</template>
<script lang="ts" setup>
import { DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
@@ -287,13 +345,20 @@
import { getAccessToken, getTenantId } from "@/utils/auth"
import axios from 'axios'
import { config } from '@/config/axios/config'
import {videoMeger} from "@/api/pptTemplate";
import {createVideo, createVideoMeger, videoMeger} from "@/api/pptTemplate";
import { ArrowDown } from '@element-plus/icons-vue'
const router = useRouter()
const message = useMessage()
const { t } = useI18n()
const polling = ref(false)
let pollingTimer: number | null = null
//合成片头片尾视频
const dialogVisible = ref(false)
const formData1 = reactive({
  isvideo: 2,
  id: null as number | null
})
// 视频列表相关数据
const loading = ref(true)
const total = ref(0)
@@ -316,11 +381,34 @@
  videoId: null as number | null,
  timeThreshold: '0.05',
  language: 'zh',
  content: ''
  content: '',
  subtitlesUrl: '',
  videoUrl: '',
  courseName: '',
  subtitlesAddStatus: null
})
const generating = ref(false)
const saving = ref(false)
// 片头片尾弹框相关
const headerFooterDialogVisible = ref(false)
const headerFooterForm = reactive({
  id: null as number | null,
  titles: '',
  trailer: '',
})
const applyingHeaderFooter = ref(false)
const handleFileSuccess = (fileType,response) => {
  if (fileType === 'audition') {
    headerFooterForm.titles = response.data
  }
};
const handleFileSuccess1 = (fileType,response) => {
  if (fileType === 'audition') {
    headerFooterForm.trailer = response.data
  }
};
// 获取视频列表
const getList = async () => {
  loading.value = true
@@ -363,15 +451,15 @@
}
// 下载文件
const handleDownload = (url, courseName) => {
const handleDownload = (url, filename) => {
  if (!url) {
    message.warning("未找到资源文件!")
    return
  }
  // window.open(url, '_blank');
  const link = document.createElement('a')
  link.href = url
  link.download = courseName
  link.download = filename || 'download'
  link.target = '_blank'
  document.body.appendChild(link)
  link.click()
@@ -393,16 +481,8 @@
  })
}
// 格式化视频时长
const formatDuration = (milliseconds: number) => {
  const seconds = Math.floor(milliseconds / 1000)
  const hrs = Math.floor(seconds / 3600)
  const mins = Math.floor((seconds % 3600) / 60)
  const secs = seconds % 60
  return `${hrs > 0 ? `${hrs}时` : ''}${mins > 0 ? `${mins}分` : ''}${secs}秒`
}
// 计算进度
const calculateProgress = (progressStr: number) => {
const calculateProgress = (progressStr) => {
  if (!progressStr || typeof progressStr !== 'string') return 0;
  const parts = progressStr.split('/');
@@ -433,7 +513,7 @@
}
// 计算合成耗时
const calculateDuration = (createTime: string, finishTime: string) => {
const calculateDuration = (createTime, finishTime) => {
  if (!createTime || !finishTime) return '未完成'
  const start = new Date(createTime).getTime()
@@ -450,36 +530,38 @@
// 打开字幕弹框
const openSubtitleDialog = async (videoId: number) => {
  try {
    subtitleDialogVisible.value = true
    subtitleForm.videoId = videoId
    const videoDetail = await pptTemplateApi.myCourseDetail(videoId)
    // 立即获取视频详情检查字幕状态
    subtitleForm.subtitlesAddStatus=videoDetail.subtitlesAddStatus
    console.log('视频详情:', videoDetail)
    subtitleForm.subtitlesAddStatus = videoDetail.subtitlesAddStatus
    subtitleForm.courseName = videoDetail.courseName
    if (videoDetail.subtitlesAddStatus === 2) {
      subtitleForm.videoUrl = videoDetail.videoUrl || ''
      generating.value=false
      polling.value=false
    }else if (videoDetail.subtitlesAddStatus === 1) {
      generating.value = false
      polling.value = false
    } else if (videoDetail.subtitlesAddStatus === 1) {
      subtitleForm.videoUrl = ''
      generating.value=true
      polling.value=true
      generating.value = true
      polling.value = true
    }else {
      subtitleForm.videoUrl = videoDetail.videoUrl || ''
      generating.value = false
      polling.value = false
    }
    if (videoDetail.subtitlesStatus === 2) { // 2 表示字幕已生成
    if (videoDetail.subtitlesStatus === 2) {
      generating.value = false
      polling.value = false
      if (videoDetail.subtitlesUrl) {
        subtitleForm.subtitlesUrl = videoDetail.subtitlesUrl
        subtitleForm.courseName=videoDetail.courseName
        generating.value=false
        polling.value=false
        try {
          // 尝试从URL获取字幕内容
          const response = await fetch(videoDetail.subtitlesUrl)
          if (response.ok) {
            const srtContent = await response.text()
            subtitleForm.content = srtContent
          } else {
            // 如果URL不可用,检查是否有直接的字幕内容
            subtitleForm.content = videoDetail.subtitlesContent || ''
          }
        } catch (error) {
@@ -487,30 +569,25 @@
          subtitleForm.content = videoDetail.subtitlesContent || ''
        }
      } else if (videoDetail.subtitlesContent) {
        // 直接使用字幕内容
        subtitleForm.content = videoDetail.subtitlesContent
      }
    } else if (videoDetail.subtitlesStatus === 3) {
      // 字幕未生成或生成失败,清空内容
      generating.value = false
      polling.value = false
      subtitleForm.content = ''
    }else if (videoDetail.subtitlesStatus === 1) {
      generating.value=true
      polling.value=true
    } else if (videoDetail.subtitlesStatus === 1) {
      generating.value = true
      polling.value = true
      subtitleForm.content = ''
    }else{
      generating.value = false
      polling.value = false
    }
  } catch (error) {
    console.error('获取视频详情失败:', error)
    message.error('获取视频详情失败,请重试')
    subtitleDialogVisible.value = false
  }
  console.log('视频详情:', generating)
}
// 重置字幕表单
const resetSubtitleForm = () => {
  subtitleFormRef.value?.resetFields()
  subtitleForm.videoId = null
  subtitleForm.content = ''
}
// 触发文件上传
@@ -555,7 +632,7 @@
const generateSubtitles = async () => {
  try {
    await subtitleFormRef.value.validateField(['timeThreshold', 'language'])
    console.log(subtitleForm)
    if (!subtitleForm.videoId) {
      message.warning('视频ID不能为空')
      return
@@ -568,8 +645,10 @@
      sentenceGap: parseFloat(subtitleForm.timeThreshold),
      lang: subtitleForm.language
    }
    await pptTemplateApi.generateSubtitles(params)
    message.success(subtitleForm.courseName+' '+'字幕生成任务已开始')
    const maxAttempts = 20000
    const interval = 3000
    let attempts = 0
@@ -580,7 +659,7 @@
      try {
        const videoDetail = await pptTemplateApi.myCourseDetail(subtitleForm.videoId!)
        console.log('轮询结果:', videoDetail)
        if (videoDetail.subtitlesStatus === 2) {
          if (videoDetail.subtitlesUrl) {
            try {
@@ -598,7 +677,6 @@
          message.success(subtitleForm.courseName+' '+'字幕生成成功')
          stopPolling()
        } else if (videoDetail.subtitlesStatus === 3) {
          // message.error(`字幕生成失败: ${videoDetail.errorReason || '未知原因'}`)
          stopPolling()
        } else if (attempts >= maxAttempts) {
          message.warning(subtitleForm.courseName+' '+'字幕生成超时,请稍后手动检查')
@@ -620,13 +698,13 @@
    poll()
  } catch (error) {
    console.error(subtitleForm.courseName+' '+'生成字幕出错:', error)
    // message.error(`生成字幕失败: ${error.message || '未知错误'}`)
    stopPolling()
  } finally {
    generating.value = false
  }
}
// 保存字幕
const saveSubtitles = async () => {
  try {
    saving.value = true
@@ -642,7 +720,7 @@
    const formData = new FormData()
    formData.append('file', file)
    // 4. 上传文件 - 使用 axios 替代 request
    // 4. 上传文件
    const uploadResponse = await axios({
      url: config.base_url+'/infra/file/upload',
      method: 'post',
@@ -673,6 +751,7 @@
    saving.value = false
  }
}
// 将文本内容格式化为SRT格式
const formatToSrt = (content: string): string => {
  if (content.trim().match(/^\d+\s+\d{2}:\d{2}:\d{2},\d{3}\s-->\s\d{2}:\d{2}:\d{2},\d{3}/)) {
@@ -691,18 +770,9 @@
  return srtContent
}
// 停止轮询
const stopPolling = () => {
  if (pollingTimer) {
    clearTimeout(pollingTimer)
    pollingTimer = null
  }
  polling.value = false
}
//字幕视频合成
// 字幕视频合成
const downloadSubtitles = async () => {
  try {
    // 判断字幕内容是否为空
    if (!subtitleForm.content.trim()) {
      message.warning('请先生成或上传字幕内容')
      return
@@ -725,15 +795,14 @@
      try {
        const videoDetail = await pptTemplateApi.myCourseDetail(subtitleForm.videoId!)
        console.log('轮询字幕视频合成结果:', videoDetail)
        if (videoDetail.subtitlesAddStatus === 2) {
          message.success(subtitleForm.courseName+' '+'字幕视频合成成功')
          if (videoDetail.previewUrl) {
            subtitleForm.content = '' // 清空当前字幕内容
            subtitleForm.content = ''
            stopPolling()
            subtitleDialogVisible.value = false
            getList() // 刷新列表
            getList()
          }
        } else if (videoDetail.subtitlesAddStatus === 3) {
          message.error(subtitleForm.courseName+' '+`字幕视频合成失败: ${videoDetail.errorReason || '未知原因'}`)
@@ -764,6 +833,104 @@
    generating.value = false
  }
}
// 停止轮询
const stopPolling = () => {
  if (pollingTimer) {
    clearTimeout(pollingTimer)
    pollingTimer = null
  }
  polling.value = false
}
// 处理片头片尾按钮点击
const handleHeaderFooter =async (row) => {
  console.log(row)
  headerFooterForm.id = row.id
  let details= await pptTemplateApi.myCourseDetail(row.id)
  console.log(details)
  headerFooterForm.titles = details.titles || ''
  headerFooterForm.trailer = details.trailer || ''
  headerFooterDialogVisible.value = true
}
// 应用片头片尾设置
const applyHeaderFooter = async () => {
  try {
    console.log('应用片头片尾设置:', headerFooterForm)
    const title = await pptTemplateApi.createVideo(headerFooterForm)
    console.log('创建视频标题:', title)
    if (title) {
      message.success('片头片尾设置成功')
      headerFooterDialogVisible.value = false
      getList()
    }
  } catch (error) {
    console.error('片头片尾设置出错:', error)
    message.error('片头片尾设置出错')
  } finally {
    applyingHeaderFooter.value = false
  }
}
//合成片头片尾视频
const mergeHeaderFooter = async (id: number) => {
  try {
    let details= await pptTemplateApi.myCourseDetail(id)
    formData1.value=details
    dialogVisible.value = true
    console.log(formData1.value)
  }
  catch (error) {
    console.error(error)
  }
}
//合成片头片尾
const hecheng = async () => {
  try {
    console.log(formData1.value)
    applyingHeaderFooter.value = true
    let obj={}
    if (formData1.isvideo=='2'){
      obj={
        id:formData1.value.id,
        titles:formData1.value.titles,
        trailer:formData1.value.trailer,
        courseName:formData1.value.courseName,
        videoUrl:null,
        previewUrl:formData1.value.previewUrl
      }
      const res = await pptTemplateApi.createVideoMeger(obj)
      if (res) {
        message.success('视频合成成功')
        applyingHeaderFooter.value = true
        dialogVisible.value = false
        getList()
      }
    }else if (formData1.isvideo=='1'){
      obj={
        id:formData1.value.id,
        titles:formData1.value.titles,
        trailer:formData1.value.trailer,
        courseName:formData1.value.courseName,
        videoUrl:formData1.value.courseName,
        previewUrl:null
      }
      const res = await pptTemplateApi.createVideoMeger(obj)
      if (res) {
        message.success('视频合成成功')
        applyingHeaderFooter.value = true
        dialogVisible.value = false
        getList()
      }
    }
    // loading.value = true
    //
  }
  catch (error) {
    console.error(error)
  }
}
// 清理定时器
onBeforeUnmount(() => {
  stopPolling()
@@ -774,6 +941,7 @@
  getList()
})
</script>
<style scoped>
.textarea-wrapper {
  position: relative;
@@ -781,20 +949,15 @@
}
.scroll-outside {
  /* 隐藏默认滚动条 */
  overflow: hidden;
}
.scroll-outside .el-textarea__inner {
  /* 显示自定义滚动条 */
  overflow-y: auto;
  /* 确保滚动条不会挤压内容 */
  padding-right: 0;
  /* 可选:增加右边距为滚动条留出空间 */
  margin-right: 16px;
}
/* 可选:自定义滚动条样式 */
.scroll-outside .el-textarea__inner::-webkit-scrollbar {
  width: 8px;
}
@@ -803,4 +966,14 @@
  background: #c0c4cc;
  border-radius: 4px;
}
.el-button-group {
  display: flex;
  align-items: center;
  gap: 4px;
}
.el-dropdown {
  margin-left: 0;
}
</style>