diff --git a/frontend/app/web-gold/src/components/TopNav.vue b/frontend/app/web-gold/src/components/TopNav.vue index 66166ec059..48d4dc17f8 100644 --- a/frontend/app/web-gold/src/components/TopNav.vue +++ b/frontend/app/web-gold/src/components/TopNav.vue @@ -1,60 +1,37 @@ diff --git a/frontend/app/web-gold/src/components/VoiceSelector.vue b/frontend/app/web-gold/src/components/VoiceSelector.vue index 4cd9f64ee9..cc3474729b 100644 --- a/frontend/app/web-gold/src/components/VoiceSelector.vue +++ b/frontend/app/web-gold/src/components/VoiceSelector.vue @@ -188,7 +188,8 @@ const handlePlayVoiceSample = (voice) => { }, (error) => { console.error('音频播放失败', error) - } + }, + { autoPlay: false } // 禁用自动播放,由 APlayer 控制 ) } diff --git a/frontend/app/web-gold/src/composables/useTTS.js b/frontend/app/web-gold/src/composables/useTTS.js index 4a01745d00..cf383a62f6 100644 --- a/frontend/app/web-gold/src/composables/useTTS.js +++ b/frontend/app/web-gold/src/composables/useTTS.js @@ -215,8 +215,10 @@ export function useTTS(options = {}) { * @param {Object} voice - 音色对象 * @param {Function} onSuccess - 成功回调 * @param {Function} onError - 错误回调 + * @param {Object} options - 选项 + * @param {boolean} options.autoPlay - 是否自动播放(默认 true) */ - async function playVoiceSample(voice, onSuccess, onError) { + async function playVoiceSample(voice, onSuccess, onError, options = { autoPlay: true }) { if (!voice) return if (previewLoadingVoiceId.value === voice.id || playingPreviewVoiceId.value === voice.id) { return @@ -237,7 +239,11 @@ export function useTTS(options = {}) { const cachedAudio = previewAudioCache.get(cacheKey) if (cachedAudio) { - playCachedAudio(cachedAudio, resetPreviewState) + if (options.autoPlay !== false) { + playCachedAudio(cachedAudio, resetPreviewState) + } else { + resetPreviewState() + } onSuccess && onSuccess(cachedAudio) return } @@ -260,19 +266,23 @@ export function useTTS(options = {}) { if (res.data?.audioUrl) { resetPreviewState() - playAudioPreview(res.data.audioUrl, { - revokeOnEnd: true, - onEnded: function() { - URL.revokeObjectURL(res.data.audioUrl) - } - }) + if (options.autoPlay !== false) { + playAudioPreview(res.data.audioUrl, { + revokeOnEnd: true, + onEnded: function() { + URL.revokeObjectURL(res.data.audioUrl) + } + }) + } onSuccess?.(res.data) } else if (res.data?.audioBase64) { const audioData = await decodeAndCacheBase64(res.data.audioBase64, res.data.format, cacheKey) resetPreviewState() - playCachedAudio(audioData, function() { - URL.revokeObjectURL(audioData.objectUrl) - }) + if (options.autoPlay !== false) { + playCachedAudio(audioData, function() { + URL.revokeObjectURL(audioData.objectUrl) + }) + } onSuccess?.(audioData) } else { message.error('试听失败') diff --git a/frontend/app/web-gold/src/router/index.js b/frontend/app/web-gold/src/router/index.js index cc2e6b5ab1..983d9bafb0 100644 --- a/frontend/app/web-gold/src/router/index.js +++ b/frontend/app/web-gold/src/router/index.js @@ -10,7 +10,7 @@ const navConfig = [ order: 1, items: [ { name: '对标分析', path: 'content-style/benchmark', icon: 'grid', component: () => import('../views/content-style/Benchmark.vue') }, - { name: '文案创作', path: 'content-style/copywriting', icon: 'text', component: () => import('../views/content-style/Copywriting.vue') }, + // { name: '文案创作', path: 'content-style/copywriting', icon: 'text', component: () => import('../views/content-style/Copywriting.vue') }, { name: '热点趋势', path: 'trends/forecast', icon: 'text', component: () => import('../views/trends/Forecast.vue') }, { name: '智能体', path: 'agents', icon: 'robot', component: () => import('../views/agents/Agents.vue') }, ] @@ -32,14 +32,14 @@ const navConfig = [ { name: '任务中心', path: 'system/task-management/:type', icon: 'video', component: () => import('../views/system/task-management/layout/TaskLayout.vue'), requiresAuth: true, params: { type: 'mix-task' } }, ] }, - { - group: '系统', - order: 4, - requiresAuth: true, - items: [ - { name: '风格设置', path: 'system/style-settings', icon: 'text', component: () => import('../views/system/style-settings/index.vue') }, - ] - } + // { + // group: '系统', + // order: 4, + // requiresAuth: true, + // items: [ + // { name: '风格设置', path: 'system/style-settings', icon: 'text', component: () => import('../views/system/style-settings/index.vue') }, + // ] + // } ] // 导航图标定义 @@ -144,23 +144,34 @@ const router = createRouter({ routes, }) +// 白名单路由(无需登录) +const WHITE_LIST = ['/login'] + // 路由守卫 router.beforeEach(async (to, from, next) => { const userStore = useUserStore() - const authenToken = tokenManager.getAccessToken() + const token = tokenManager.getAccessToken() - if (to.meta.requiresAuth && !authenToken) { + // 1. 白名单路由直接放行 + if (WHITE_LIST.includes(to.path)) { + // 已登录访问登录页 → 跳转首页 + if (token) { + next({ path: '/', replace: true }) + return + } + next() + return + } + + // 2. 非白名单路由,必须有 token + if (!token) { next({ path: '/login', query: { redirect: to.fullPath } }) return } - if (to.path === '/login' && authenToken) { - next({ path: '/content-style/benchmark', replace: true }) - return - } - - if (authenToken && !userStore.isLoggedIn) { - userStore.fetchUserInfo() + // 3. 有 token 但未加载用户信息 → 加载 + if (!userStore.isLoggedIn) { + await userStore.fetchUserInfo() } next() diff --git a/frontend/app/web-gold/src/views/kling/hooks/pipeline/useSimplePipeline.ts b/frontend/app/web-gold/src/views/kling/hooks/pipeline/useSimplePipeline.ts index 48e50fd0d8..547f9914f5 100644 --- a/frontend/app/web-gold/src/views/kling/hooks/pipeline/useSimplePipeline.ts +++ b/frontend/app/web-gold/src/views/kling/hooks/pipeline/useSimplePipeline.ts @@ -113,8 +113,17 @@ export function useSimplePipeline(options: PipelineOptions) { * 运行完整流程(到 ready 状态) */ async function run(params: PipelineParams): Promise { - // 重置状态 - reset() + // 重置上下文数据,但保持状态在即将开始工作的状态 + context.value = { ...INITIAL_CONTEXT } + error.value = null + history.value = ['idle'] + + // 立即设置忙碌状态,让 UI 显示 loading + // 根据是否有上传文件决定初始状态 + const initialState: PipelineState = params.videoFile && !params.selectedVideo + ? 'uploading' + : 'recognizing' + setState(initialState) try { // 保存参数到上下文 @@ -126,20 +135,28 @@ export function useSimplePipeline(options: PipelineOptions) { // 步骤1: 上传视频(如果是上传模式) if (params.videoFile && !params.selectedVideo) { - const fileId = await executeStep('uploading', () => - options.uploadVideo(params.videoFile!) - ) - context.value.videoFileId = fileId + try { + const fileId = await options.uploadVideo(params.videoFile!) + context.value.videoFileId = fileId + } catch (err) { + setError(err as Error) + throw err + } } else if (params.selectedVideo) { context.value.videoFileId = params.selectedVideo.fileId } // 步骤2: 识别人脸 - const recognizeData = params.selectedVideo - ? await options.recognizeFromLibrary(params.selectedVideo) - : await options.recognizeUploaded(context.value.videoFileId!) - - await executeStep('recognizing', async () => recognizeData) + setState('recognizing') + let recognizeData + try { + recognizeData = params.selectedVideo + ? await options.recognizeFromLibrary(params.selectedVideo) + : await options.recognizeUploaded(context.value.videoFileId!) + } catch (err) { + setError(err as Error) + throw err + } context.value.sessionId = recognizeData.sessionId context.value.faceId = recognizeData.faceId @@ -148,9 +165,14 @@ export function useSimplePipeline(options: PipelineOptions) { context.value.videoDurationMs = recognizeData.duration || 0 // 步骤3: 生成音频 - const audioData = await executeStep('generating', () => - options.generateAudio(params.text, params.voice, params.speechRate) - ) + setState('generating') + let audioData + try { + audioData = await options.generateAudio(params.text, params.voice, params.speechRate) + } catch (err) { + setError(err as Error) + throw err + } context.value.audioBase64 = audioData.audioBase64 context.value.audioFormat = audioData.format || 'mp3' @@ -160,17 +182,17 @@ export function useSimplePipeline(options: PipelineOptions) { setState('validating') const videoDurationMs = context.value.videoDurationMs ?? 0 if (context.value.audioDurationMs > videoDurationMs) { - throw new Error( - `校验失败:音频时长(${(context.value.audioDurationMs / 1000).toFixed(1)}秒) 超过人脸时长(${(videoDurationMs / 1000).toFixed(1)}秒)` - ) + const errorMsg = `校验失败:音频时长(${(context.value.audioDurationMs / 1000).toFixed(1)}秒) 超过人脸时长(${(videoDurationMs / 1000).toFixed(1)}秒)` + setError(new Error(errorMsg)) + return } context.value.validationPassed = true // 到达 ready 状态 setState('ready') - } catch (err) { - // 错误已在 executeStep 中处理 + } catch { + // 错误已在各步骤中处理 } } diff --git a/frontend/app/web-gold/src/views/kling/hooks/useDigitalHumanGeneration.ts b/frontend/app/web-gold/src/views/kling/hooks/useDigitalHumanGeneration.ts index dd87b331ed..e9e2c107e6 100644 --- a/frontend/app/web-gold/src/views/kling/hooks/useDigitalHumanGeneration.ts +++ b/frontend/app/web-gold/src/views/kling/hooks/useDigitalHumanGeneration.ts @@ -12,7 +12,7 @@ import type { IdentifyResult, Video, } from '../types/identify-face' -import { identifyUploadedVideo } from '@/api/kling' +import { identifyUploadedVideo, uploadAndIdentifyVideo } from '@/api/kling' import { useUpload } from '@/composables/useUpload' export function useDigitalHumanGeneration() { @@ -90,6 +90,7 @@ export function useDigitalHumanGeneration() { } if (hasSelectedVideo) { + // 从素材库选择:调用识别接口 const res = await identifyUploadedVideo(hasSelectedVideo) as { success: boolean; data: { sessionId: string; faceId: string | null; startTime: number; endTime: number } @@ -100,35 +101,18 @@ export function useDigitalHumanGeneration() { identifyResult.value.faceStartTime = res.data.startTime || 0 identifyResult.value.faceEndTime = res.data.endTime || 0 } else { + // 上传新视频:使用 uploadAndIdentifyVideo 完成上传+识别 const file = hasUploadFile! - let coverBase64 = null - try { - const { extractVideoCover } = await import('@/utils/video-cover') - const cover = await extractVideoCover(file, { maxWidth: 800, quality: 0.8 }) - coverBase64 = cover.base64 - } catch { - // 封面提取失败不影响主流程 + const res = await uploadAndIdentifyVideo(file) as { + success: boolean; + data: { fileId: string; sessionId: string; faceId: string | null; startTime: number; endTime: number } } - const fileId = await upload(file, { - fileCategory: 'video', - groupId: null, - coverBase64, - onStart: function() {}, - onProgress: function() {}, - onSuccess: function() {}, - onError: function(err: Error) { - message.error(err.message || '上传失败') - } - }) - - identifyResult.value.videoFileId = fileId - // 上传后需要再调用识别接口获取人脸信息 - // 暂时清空,等待后续识别 - identifyResult.value.sessionId = '' - identifyResult.value.faceId = '' - identifyResult.value.faceStartTime = 0 - identifyResult.value.faceEndTime = 0 + identifyResult.value.videoFileId = res.data.fileId + identifyResult.value.sessionId = res.data.sessionId + identifyResult.value.faceId = res.data.faceId || '' + identifyResult.value.faceStartTime = res.data.startTime || 0 + identifyResult.value.faceEndTime = res.data.endTime || 0 } return { ...identifyResult.value } diff --git a/frontend/app/web-gold/src/views/kling/hooks/useVoiceGeneration.ts b/frontend/app/web-gold/src/views/kling/hooks/useVoiceGeneration.ts index 51cdf3b67e..656979f9fa 100644 --- a/frontend/app/web-gold/src/views/kling/hooks/useVoiceGeneration.ts +++ b/frontend/app/web-gold/src/views/kling/hooks/useVoiceGeneration.ts @@ -71,7 +71,6 @@ export function useVoiceGeneration(): UseVoiceGeneration { audioState.value.generated = audioData audioState.value.durationMs = await parseAudioDuration(audioData.audioBase64) - message.success('配音生成成功!') } else { throw new Error(res.msg || '配音生成失败') }