From 0ff2ee623c5ca0ffe6e21782f3d5706cce0d1b9b Mon Sep 17 00:00:00 2001
From: du <13220750630.163.com>
Date: 星期三, 09 四月 2025 15:42:26 +0800
Subject: [PATCH] 字幕

---
 easegen-front/src/views/myCourse/index.vue |  453 ++++++++++++++++++++++++++++++++++++++++++++++----------
 1 files changed, 371 insertions(+), 82 deletions(-)

diff --git a/easegen-front/src/views/myCourse/index.vue b/easegen-front/src/views/myCourse/index.vue
index c34c3e6..0977127 100644
--- a/easegen-front/src/views/myCourse/index.vue
+++ b/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="瀛楀箷鍐呭灏嗘樉绀哄湪杩欓噷锛圫RT鏍煎紡锛�"
+          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('璇蜂笂浼燬RT鏍煎紡鐨勫瓧骞曟枃浠�')
+    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; // 鍚堟垚涓垨鍚堟垚澶辫触锛屾寜瀹為檯杩涘害灞曠ず
+// 灏嗘枃鏈唴瀹规牸寮忓寲涓篠RT鏍煎紡
+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>

--
Gitblit v1.9.3