From 9b132082d298012458e01376ca28647fae204b63 Mon Sep 17 00:00:00 2001 From: sion123 <450702724@qq.com> Date: Thu, 5 Mar 2026 22:58:31 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web-gold/src/components/VoiceSelector.vue | 167 ++- .../app/web-gold/src/composables/useTTS.js | 2 +- .../web-gold/src/views/kling/IdentifyFace.vue | 1238 ++++++++++------- .../kling/components/TextGeneratePopup.vue | 515 +++++++ .../views/kling/components/TimelinePanel.vue | 253 ++-- .../kling/stores/useDigitalHumanStore.ts | 65 +- .../service/DigitalHumanTaskServiceImpl.java | 12 +- 7 files changed, 1514 insertions(+), 738 deletions(-) create mode 100644 frontend/app/web-gold/src/views/kling/components/TextGeneratePopup.vue diff --git a/frontend/app/web-gold/src/components/VoiceSelector.vue b/frontend/app/web-gold/src/components/VoiceSelector.vue index a0de85b083..bdd5bfa5b7 100644 --- a/frontend/app/web-gold/src/components/VoiceSelector.vue +++ b/frontend/app/web-gold/src/components/VoiceSelector.vue @@ -96,13 +96,14 @@ const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE const voiceStore = useVoiceCopyStore() -const emit = defineEmits(['select']) +const emit = defineEmits(['select', 'audioGenerated']) let player = null const playerContainer = ref(null) const audioUrl = ref('') const currentVoiceName = ref('') const isPlayerInitializing = ref(false) +const currentAudioBase64 = ref('') // 保存当前音频的 base64 数据 // 默认封面图片(音频波形图标) const defaultCover = `data:image/svg+xml;base64,${btoa(` @@ -182,9 +183,7 @@ const handleVoiceChange = (value, option) => { } const handleSynthesize = () => { - if (!selectedVoiceId.value) return - // 防止在播放器初始化过程中重复点击 - if (isPlayerInitializing.value) return + if (!selectedVoiceId.value || isPlayerInitializing.value) return const voice = userVoiceCards.value.find(v => v.id === selectedVoiceId.value) if (!voice) return @@ -209,12 +208,11 @@ const handlePlayVoiceSample = (voice) => { (data) => { const url = data.audioUrl || data.objectUrl if (!url) return + currentAudioBase64.value = data.audioBase64 || '' initPlayer(url) }, - (error) => { - // 音频播放失败,静默处理 - }, - { autoPlay: false } // 禁用自动播放,由 APlayer 控制 + undefined, // 错误静默处理 + { autoPlay: false } ) } @@ -266,6 +264,14 @@ const initPlayer = (url) => { player.on('canplay', () => { isPlayerInitializing.value = false + // 发送音频时长和 base64 数据给父组件 + const durationMs = Math.floor(player.audio.duration * 1000) + if (durationMs > 0) { + emit('audioGenerated', { + durationMs, + audioBase64: currentAudioBase64.value + }) + } }) } catch (e) { console.error('APlayer 初始化失败:', e) @@ -276,29 +282,21 @@ const initPlayer = (url) => { }) } -/** - * 下载音频 - */ const downloadAudio = () => { if (!audioUrl.value) return - const filename = `${currentVoiceName.value || '语音合成'}.mp3` const link = document.createElement('a') link.href = audioUrl.value - link.download = filename + link.download = `${currentVoiceName.value || '语音合成'}.mp3` document.body.appendChild(link) link.click() document.body.removeChild(link) } -/** - * 销毁播放器 - */ const destroyPlayer = () => { isPlayerInitializing.value = false if (player) { try { - // 先暂停播放,防止销毁过程中出错 player.pause() player.destroy() } catch (e) { @@ -306,8 +304,7 @@ const destroyPlayer = () => { } player = null } - // 只对 blob URL 调用 revokeObjectURL - if (audioUrl.value && audioUrl.value.startsWith('blob:')) { + if (audioUrl.value?.startsWith('blob:')) { URL.revokeObjectURL(audioUrl.value) } audioUrl.value = '' @@ -338,25 +335,25 @@ onBeforeUnmount(() => { .voice-selector-wrapper { display: flex; flex-direction: column; - gap: 16px; + gap: 14px; } -/* 音色卡片 */ +/* 音色卡片 - 柔和风格 */ .voice-card { - background: linear-gradient(135deg, rgba(59, 130, 246, 0.05) 0%, rgba(139, 92, 246, 0.05) 100%); - border: 1px solid rgba(59, 130, 246, 0.15); - border-radius: 12px; - padding: 16px; - transition: all 0.3s ease; + background: rgba(59, 130, 246, 0.03); + border: 1px solid rgba(59, 130, 246, 0.1); + border-radius: 10px; + padding: 14px; + transition: all 0.25s ease; &:hover { - border-color: rgba(59, 130, 246, 0.3); - box-shadow: 0 4px 12px rgba(59, 130, 246, 0.1); + border-color: rgba(59, 130, 246, 0.18); + background: rgba(59, 130, 246, 0.05); } &.has-audio { - border-color: rgba(59, 130, 246, 0.4); - background: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, rgba(139, 92, 246, 0.08) 100%); + border-color: rgba(59, 130, 246, 0.2); + background: rgba(59, 130, 246, 0.06); } } @@ -365,40 +362,40 @@ onBeforeUnmount(() => { display: flex; align-items: center; gap: 10px; - margin-bottom: 14px; + margin-bottom: 12px; } .header-icon { - width: 28px; - height: 28px; - border-radius: 8px; - background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); + width: 26px; + height: 26px; + border-radius: 6px; + background: rgba(59, 130, 246, 0.1); display: flex; align-items: center; justify-content: center; - color: white; + color: #3b82f6; svg { - width: 16px; - height: 16px; + width: 14px; + height: 14px; } } .header-title { - font-size: 14px; + font-size: 13px; font-weight: 500; color: var(--color-text); } .header-badge { margin-left: auto; - padding: 2px 10px; - background: rgba(59, 130, 246, 0.1); - border-radius: 12px; - font-size: 12px; + padding: 2px 8px; + background: rgba(59, 130, 246, 0.08); + border-radius: 10px; + font-size: 11px; color: #3b82f6; font-weight: 500; - max-width: 120px; + max-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -407,7 +404,7 @@ onBeforeUnmount(() => { /* 卡片主体 */ .voice-card-body { display: flex; - gap: 12px; + gap: 10px; align-items: stretch; } @@ -416,73 +413,73 @@ onBeforeUnmount(() => { :deep(.ant-select) { width: 100%; - height: 40px; + height: 36px; .ant-select-selector { - height: 40px !important; - border-radius: 10px !important; - border-color: rgba(59, 130, 246, 0.2) !important; - background: rgba(255, 255, 255, 0.8) !important; - transition: all 0.3s ease !important; + height: 36px !important; + border-radius: 8px !important; + border-color: rgba(59, 130, 246, 0.12) !important; + background: rgba(255, 255, 255, 0.9) !important; + transition: all 0.2s ease !important; &:hover { - border-color: #3b82f6 !important; + border-color: rgba(59, 130, 246, 0.25) !important; } } &.ant-select-focused .ant-select-selector { - border-color: #3b82f6 !important; - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important; + border-color: rgba(59, 130, 246, 0.3) !important; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.06) !important; } .ant-select-selection-item { - line-height: 38px !important; + line-height: 34px !important; + font-size: 13px; } .ant-select-selection-placeholder { - line-height: 38px !important; + line-height: 34px !important; + font-size: 13px; } } } .select-arrow { - color: #3b82f6; + color: #94a3b8; transition: transform 0.3s ease; } -/* 合成按钮 */ +/* 合成按钮 - 柔和风格 */ .synthesize-btn { - height: 40px; - padding: 0 20px; - border-radius: 10px; + height: 36px; + padding: 0 16px; + border-radius: 8px; border: none; - background: linear-gradient(135deg, #e2e8f0 0%, #cbd5e1 100%); + background: rgba(59, 130, 246, 0.08); color: #64748b; font-weight: 500; display: flex; align-items: center; gap: 6px; - transition: all 0.3s ease; + transition: all 0.25s ease; white-space: nowrap; &:hover:not(:disabled) { - background: linear-gradient(135deg, #cbd5e1 0%, #94a3b8 100%); - transform: translateY(-1px); + background: rgba(59, 130, 246, 0.12); + color: #475569; } &:disabled { - opacity: 0.6; + opacity: 0.5; cursor: not-allowed; } &.btn-active { - background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); - color: white; - box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); + background: rgba(59, 130, 246, 0.15); + color: #3b82f6; &:hover:not(:disabled) { - background: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%); - box-shadow: 0 6px 16px rgba(59, 130, 246, 0.4); + background: rgba(59, 130, 246, 0.2); } } @@ -493,46 +490,46 @@ onBeforeUnmount(() => { /* 播放器区域 */ .player-section { - background: rgba(255, 255, 255, 0.6); - border-radius: 12px; - padding: 14px; - border: 1px solid rgba(59, 130, 246, 0.1); + background: rgba(59, 130, 246, 0.03); + border-radius: 10px; + padding: 12px; + border: 1px solid rgba(59, 130, 246, 0.08); } .aplayer-container { :deep(.aplayer) { - border-radius: 10px; + border-radius: 8px; box-shadow: none; - border: 1px solid rgba(0, 0, 0, 0.06); + border: 1px solid rgba(0, 0, 0, 0.04); .aplayer-body { - border-radius: 10px; + border-radius: 8px; } } } .player-actions { - margin-top: 10px; + margin-top: 8px; display: flex; justify-content: flex-end; } .download-btn { - color: #3b82f6; + color: #94a3b8; font-size: 12px; padding: 4px 8px; height: auto; transition: all 0.2s ease; &:hover { - color: #2563eb; - background: rgba(59, 130, 246, 0.1); + color: #3b82f6; + background: rgba(59, 130, 246, 0.06); } } /* 动画 */ .slide-fade-enter-active { - transition: all 0.3s ease-out; + transition: all 0.25s ease-out; } .slide-fade-leave-active { @@ -540,12 +537,12 @@ onBeforeUnmount(() => { } .slide-fade-enter-from { - transform: translateY(-10px); + transform: translateY(-8px); opacity: 0; } .slide-fade-leave-to { - transform: translateY(-10px); + transform: translateY(-8px); opacity: 0; } diff --git a/frontend/app/web-gold/src/composables/useTTS.js b/frontend/app/web-gold/src/composables/useTTS.js index cbf2e36dc9..c049906bb7 100644 --- a/frontend/app/web-gold/src/composables/useTTS.js +++ b/frontend/app/web-gold/src/composables/useTTS.js @@ -250,7 +250,7 @@ export function useTTS(options = {}) { // 处理 Base64 音频 if (res.data?.audioBase64) { const { blob, objectUrl } = decodeBase64Audio(res.data.audioBase64, res.data.format) - const audioData = { blob, objectUrl, format: res.data.format } + const audioData = { blob, objectUrl, format: res.data.format, audioBase64: res.data.audioBase64 } cacheAudio(cacheKey, audioData) resetPreviewState() if (opts.autoPlay !== false) playCachedAudio(audioData) diff --git a/frontend/app/web-gold/src/views/kling/IdentifyFace.vue b/frontend/app/web-gold/src/views/kling/IdentifyFace.vue index ad94c0d52c..f4bce64326 100644 --- a/frontend/app/web-gold/src/views/kling/IdentifyFace.vue +++ b/frontend/app/web-gold/src/views/kling/IdentifyFace.vue @@ -1,265 +1,235 @@