du
2025-04-09 0ff2ee623c5ca0ffe6e21782f3d5706cce0d1b9b
字幕
已修改1个文件
453 ■■■■ 文件已修改
easegen-front/src/views/myCourse/index.vue 453 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easegen-front/src/views/myCourse/index.vue
@@ -31,7 +31,6 @@
  </ContentWrap>
  <!-- 列表 -->
  <!-- 列表 -->
  <ContentWrap>
    <el-table v-loading="loading" :data="list">
      <el-table-column :label="t('myCourse.videoCode')" align="center" prop="id" />
@@ -60,11 +59,6 @@
        width="120"
        :formatter="dateFormatter"
      />
<!--      <el-table-column :label="t('myCourse.progress')" align="center" prop="progress">-->
<!--        <template #default="scope">-->
<!--          <el-progress :percentage="getProgress(scope.row.status, scope.row.progress)" />-->
<!--        </template>-->
<!--      </el-table-column>-->
      <el-table-column :label="t('myCourse.SynthesisTime')" align="center">
        <template #default="scope">
          {{ calculateDuration(scope.row.createTime, scope.row.finishTime) }}
@@ -121,6 +115,13 @@
          </template>
          <el-button
            link
            type="primary"
            @click="openSubtitleDialog(scope.row.id)"
          >
            字幕
          </el-button>
          <el-button
            link
            type="danger"
            @click="handleDelete(scope.row.id)"
          >
@@ -137,8 +138,89 @@
      @pagination="getList"
    />
  </ContentWrap>
  <!-- 视频播放弹框 -->
  <videoDialog ref="videoRef" />
  <!-- 字幕生成弹框 -->
  <el-dialog
    v-model="subtitleDialogVisible"
    title="字幕生成"
    width="60%"
    @closed="resetSubtitleForm"
  >
    <el-form :model="subtitleForm" ref="subtitleFormRef">
      <el-row>
        <el-col :span="8">
          <el-form-item label="断句时间阈值" prop="timeThreshold" :rules="[
            { required: true, message: '请输入断句时间阈值', trigger: 'blur' },
            { pattern: /^\d+(\.\d+)?$/, message: '请输入有效数字', trigger: 'blur' }
          ]">
            <el-input v-model="subtitleForm.timeThreshold" placeholder="例如:0.05" clearable />
          </el-form-item>
        </el-col>
        <el-col :span="8">
          <el-form-item label="语言" prop="language" :rules="[
            { required: true, message: '请选择语言', trigger: 'change' }
          ]">
            <el-select v-model="subtitleForm.language" placeholder="请选择语言" clearable>
              <el-option label="中文" value="zh" />
              <el-option label="英文" value="en" />
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="8">
          <el-form-item>
            <el-button
              type="primary"
              @click="generateSubtitles"
              :loading="generating || polling"
            >
              生成字幕
            </el-button>
            <el-button
              type="primary"
              @click="triggerFileUpload"
            >
              上传SRT文件
              <input
                ref="fileInput"
                type="file"
                accept=".srt"
                style="display: none"
                @change="handleFileUpload"
              />
            </el-button>
          </el-form-item>
        </el-col>
      </el-row>
      <el-form-item label="字幕内容" prop="content" :rules="[
        { required: true, message: '请先生成或上传字幕内容', trigger: 'blur' }
      ]">
        <el-input
          v-model="subtitleForm.content"
          type="textarea"
          :rows="10"
          placeholder="字幕内容将显示在这里(SRT格式)"
          resize="none"
        />
      </el-form-item>
    </el-form>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="subtitleDialogVisible = false">取消</el-button>
        <el-button
          type="primary"
          @click="saveSubtitles"
          :loading="saving"
          :disabled="!subtitleForm.content"
        >
          保存字幕
        </el-button>
      </div>
    </template>
  </el-dialog>
</template>
<script lang="ts" setup>
import { DICT_TYPE } from '@/utils/dict'
@@ -147,21 +229,43 @@
import * as pptTemplateApi from '@/api/pptTemplate'
import { useRouter } from 'vue-router'
import videoDialog from "./videoDialog.vue"
const router = useRouter() // 路由
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
import { getAccessToken, getTenantId } from "@/utils/auth"
import axios from 'axios'
import { config } from '@/config/axios/config'
const router = useRouter()
const message = useMessage()
const { t } = useI18n()
const polling = ref(false)
let pollingTimer: number | null = null
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
// 视频列表相关数据
const loading = ref(true)
const total = ref(0)
const list = ref([])
const queryParams = reactive({
  pageNo: 1,
  pageSize: 20,
  name: undefined
})
const queryFormRef = ref() // 搜索的表单
const queryFormRef = ref()
/** 查询列表 */
// 视频预览相关
const videoRef = ref()
// 字幕弹框相关
const subtitleDialogVisible = ref(false)
const subtitleFormRef = ref()
const fileInput = ref<HTMLInputElement | null>(null)
const subtitleForm = reactive({
  videoId: null as number | null,
  timeThreshold: '0.05',
  language: 'zh',
  content: ''
})
const generating = ref(false)
const saving = ref(false)
// 获取视频列表
const getList = async () => {
  loading.value = true
  try {
@@ -173,54 +277,51 @@
  }
}
/** 搜索按钮操作 */
// 搜索视频
const handleQuery = () => {
  queryParams.pageNo = 1
  getList()
}
/** 重置按钮操作 */
// 重置搜索
const resetQuery = () => {
  queryFormRef.value.resetFields()
  handleQuery()
}
/** 预览按钮*/
const videoRef = ref()
// 预览视频
const openPreview = (row) => {
  if(row){
    videoRef.value.open(row.previewUrl, row.subtitlesVttUrl);
    videoRef.value.open(row.previewUrl, row.subtitlesVttUrl)
  }
}
/** 删除按钮操作 */
// 删除视频
const handleDelete = async (id: number) => {
  try {
    // 删除的二次确认
    await message.delConfirm()
    // 发起删除
    await pptTemplateApi.deleteMyCourse(id)
    message.success(t('common.delSuccess'))
    // 刷新列表
    await getList()
  } catch {}
}
/** 下载按钮操作 */
// 下载文件
const handleDownload = (url, courseName) => {
  //如果url为空,则提示未找到资源文件
  if (!url) {
    message.warning("未找到资源文件!");
    return;
    message.warning("未找到资源文件!")
    return
  }
  const link = document.createElement('a');
  link.href = url;
  link.download = courseName;
  link.target = '_blank'; // 强制新标签页下载
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
};
  const link = document.createElement('a')
  link.href = url
  link.download = courseName
  link.target = '_blank'
  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link)
}
// 跳转到课程详情
const goDetail = (id) => {
  pptTemplateApi.coursesDetail(id).then((res) => {
    if (!res) {
@@ -228,73 +329,261 @@
      return
    }
    if (res.pageMode === 2 || res.pageMode === 0) {
      router.push({ path: '/chooseTemplate/index', query: { id } });
      router.push({ path: '/chooseTemplate/index', query: { id } })
    } else if (res.pageMode === 3) {
      router.push({ path: '/chooseTemplate/speakvideo', query: { id } });
      router.push({ path: '/chooseTemplate/speakvideo', query: { id } })
    }
  })
}
/** 格式化视频时长 */
// 格式化视频时长
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 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 reMegerMedia = async (id: number) => {
  try {
    // 开启 loading
    loading.value = true
    // 重新合成视频
    const res = await pptTemplateApi.reMegerMedia({ id });
    console.log("---------", res);
    const res = await pptTemplateApi.reMegerMedia({ id })
    if (res) {
      message.success("合成视频任务提交成功,请到我的视频中查看!");
      message.success("合成视频任务提交成功,请到我的视频中查看!")
    }
  } catch (error) {
    console.error(error)
  } finally {
    getList()
    loading.value = false
  }
}
// 计算合成耗时
const calculateDuration = (createTime: string, finishTime: string) => {
  if (!createTime || !finishTime) return '未完成'
  const start = new Date(createTime).getTime()
  const end = new Date(finishTime).getTime()
  const duration = (end - start) / 1000
  const hrs = Math.floor(duration / 3600)
  const mins = Math.floor((duration % 3600) / 60)
  const secs = Math.floor(duration % 60)
  return `${hrs > 0 ? `${hrs}时` : ''}${mins > 0 ? `${mins}分` : ''}${secs}秒`
}
// 打开字幕弹框
const openSubtitleDialog = (videoId: number) => {
  subtitleForm.videoId = videoId
  subtitleDialogVisible.value = true
}
// 重置字幕表单
const resetSubtitleForm = () => {
  subtitleFormRef.value?.resetFields()
  subtitleForm.videoId = null
  subtitleForm.content = ''
}
// 触发文件上传
const triggerFileUpload = () => {
  fileInput.value?.click()
}
// 处理文件上传
const handleFileUpload = async (event: Event) => {
  const input = event.target as HTMLInputElement
  if (!input.files?.length) return
  const file = input.files[0]
  if (!file.name.endsWith('.srt')) {
    message.warning('请上传SRT格式的字幕文件')
    return
  }
  try {
    const content = await readFileAsText(file)
    subtitleForm.content = content
    message.success('字幕文件上传成功')
  } catch (error) {
    message.error('读取字幕文件失败')
    console.error(error)
  } finally {
    input.value = ''
  }
}
// 读取文件为文本
const readFileAsText = (file: File): Promise<string> => {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.onload = (e) => resolve(e.target?.result as string)
    reader.onerror = (e) => reject(e)
    reader.readAsText(file)
  })
}
// 生成字幕
const generateSubtitles = async () => {
  try {
    await subtitleFormRef.value.validateField(['timeThreshold', 'language'])
    if (!subtitleForm.videoId) {
      message.warning('视频ID不能为空')
      return
    }
    generating.value = true
    const params = {
      videoId: subtitleForm.videoId,
      timeThreshold: parseFloat(subtitleForm.timeThreshold),
      language: subtitleForm.language
    }
    await pptTemplateApi.generateSubtitles(params)
    message.success('字幕生成任务已开始')
    const maxAttempts = 20
    const interval = 3000
    let attempts = 0
    const poll = async () => {
      polling.value = true
      attempts++
      try {
        const videoDetail = await pptTemplateApi.myCourseDetail(subtitleForm.videoId!)
        console.log('轮询结果:', videoDetail)
        if (videoDetail.subtitlesStatus === 2) {
          if (videoDetail.subtitlesUrl) {
            try {
              const response = await fetch(videoDetail.subtitlesUrl)
              if (response.ok) {
                const srtContent = await response.text()
                subtitleForm.content = srtContent
              }
            } catch (error) {
              console.error('Error fetching SRT file:', error)
            }
          } else if (videoDetail.subtitlesContent) {
            subtitleForm.content = videoDetail.subtitlesContent
          }
          message.success('字幕生成成功')
          stopPolling()
        } else if (videoDetail.subtitlesStatus === 3) {
          message.error(`字幕生成失败: ${videoDetail.errorReason || '未知原因'}`)
          stopPolling()
        } else if (attempts >= maxAttempts) {
          message.warning('字幕生成超时,请稍后手动检查')
          stopPolling()
        } else {
          pollingTimer = window.setTimeout(poll, interval)
        }
      } catch (error) {
        console.error('轮询出错:', error)
        if (attempts >= maxAttempts) {
          message.error('字幕状态检查超时')
          stopPolling()
        } else {
          pollingTimer = window.setTimeout(poll, interval)
        }
      }
    }
    poll()
  } catch (error) {
    console.error(error);
    console.error('生成字幕出错:', error)
    message.error(`生成字幕失败: ${error.message || '未知错误'}`)
    stopPolling()
  } finally {
    generating.value = false
  }
}
const saveSubtitles = async () => {
  try {
    saving.value = true
    // 1. 将字幕内容转换为SRT格式
    const srtContent = formatToSrt(subtitleForm.content)
    // 2. 创建Blob对象表示SRT文件
    const blob = new Blob([srtContent], { type: 'text/plain' })
    const file = new File([blob], 'subtitles.srt', { type: 'text/plain' })
    // 3. 创建FormData并添加文件
    const formData = new FormData()
    formData.append('file', file)
    // 4. 上传文件 - 使用 axios 替代 request
    const uploadResponse = await axios({
      url: config.base_url+'/infra/file/upload',
      method: 'post',
      data: formData,
      headers: {
        'Content-Type': 'multipart/form-data',
        'Authorization': `Bearer ${getAccessToken()}`,
        'tenant-id': getTenantId()
      }
    })
    // 5. 调用保存字幕接口
    const params = {
      id: subtitleForm.videoId,
      subtitlesUrl: uploadResponse.data.data,
    }
    await pptTemplateApi.saveSubtitles(params)
    message.success('字幕保存成功')
    subtitleDialogVisible.value = false
    // 刷新列表
    getList();
    // 无论成功还是失败,都需要关闭 loading
    loading.value = false;
    getList()
  } catch (error) {
    console.error('保存字幕失败:', error)
    message.error(`保存字幕失败: ${error.message || '未知错误'}`)
  } finally {
    saving.value = false
  }
}
/** 根据创建时间和完成时间计算合成耗时 */
const calculateDuration = (createTime: string, finishTime: string) => {
  if (!createTime || !finishTime) return '未完成';
  const start = new Date(createTime).getTime();
  const end = new Date(finishTime).getTime();
  const duration = (end - start) / 1000; // 转换为秒
  const hrs = Math.floor(duration / 3600);
  const mins = Math.floor((duration % 3600) / 60);
  const secs = Math.floor(duration % 60);
  return `${hrs > 0 ? `${hrs}时` : ''}${mins > 0 ? `${mins}分` : ''}${secs}秒`;
}
/** 获取合成进度,关联状态 */
const getProgress = (status: number, completionPercentage: number) => {
  if (status === 2) {
    return 100; // 已完成,进度固定为100%
  } else if (status === 0) {
    return 0; // 未启动,进度固定为0%
  } else {
    return completionPercentage || 0; // 合成中或合成失败,按实际进度展示
// 将文本内容格式化为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}/)) {
    return content
  }
  const lines = content.split('\n').filter(line => line.trim())
  let srtContent = ''
  lines.forEach((line, index) => {
    srtContent += `${index + 1}\n`
    srtContent += `00:00:${String(index).padStart(2, '0')},000 --> 00:00:${String(index + 1).padStart(2, '0')},000\n`
    srtContent += `${line}\n\n`
  })
  return srtContent
}
/** 初始化 **/
onMounted(async () => {
  await getList()
// 停止轮询
const stopPolling = () => {
  if (pollingTimer) {
    clearTimeout(pollingTimer)
    pollingTimer = null
  }
  polling.value = false
}
// 清理定时器
onBeforeUnmount(() => {
  stopPolling()
})
// 初始化
onMounted(() => {
  getList()
})
</script>