功能优化

This commit is contained in:
2026-02-02 23:16:38 +08:00
parent 42567c457b
commit 409e976209
5 changed files with 76 additions and 62 deletions

View File

@@ -1,6 +1,6 @@
<template>
<div class="voice-selector">
<div v-if="displayedVoices.length === 0" class="empty-voices">
<div v-if="userVoiceCards.length === 0" class="empty-voices">
还没有配音可先在"配音管理"中上传
</div>
@@ -19,31 +19,52 @@
size="small"
:disabled="!selectedVoiceId"
:loading="previewLoadingVoiceId === selectedVoiceId"
@click="handlePreviewCurrentVoice"
@click="handleSynthesize"
>
<template #icon>
<SoundOutlined />
</template>
试听
合成
</a-button>
</div>
<!-- APlayer 播放器容器 -->
<div v-if="audioUrl" ref="playerContainer" class="aplayer-container"></div>
<!-- 下载按钮 -->
<a-button
v-if="audioUrl"
type="link"
size="small"
@click="downloadAudio"
class="download-link"
>
下载音频
</a-button>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
import { useVoiceCopyStore } from '@/stores/voiceCopy'
import { useTTS, TTS_PROVIDERS } from '@/composables/useTTS'
import APlayer from 'aplayer'
const props = defineProps({
synthText: {
type: String,
default: ''
},
speechRate: {
type: Number,
default: 1.0
}
})
const voiceStore = useVoiceCopyStore()
const emit = defineEmits(['select'])
// APlayer 实例
let player = null
const playerContainer = ref(null)
const audioUrl = ref('')
@@ -87,27 +108,21 @@ const defaultCover = `data:image/svg+xml;base64,${btoa(`
// 使用TTS Hook
const {
previewLoadingVoiceId,
playingPreviewVoiceId,
ttsText,
speechRate,
playVoiceSample,
setText,
setSpeechRate,
resetPreviewState
setSpeechRate
} = useTTS({
provider: TTS_PROVIDERS.SILICONFLOW
})
// 当前选中的音色ID
const selectedVoiceId = ref('')
// 从store数据构建音色列表
const userVoiceCards = computed(() =>
(voiceStore.profiles || []).map(profile => ({
id: `user-${profile.id}`,
rawId: profile.id,
name: profile.name || '未命名',
category:'',
category: '',
gender: profile.gender || 'female',
description: profile.note || '我的配音',
fileUrl: profile.fileUrl,
@@ -117,18 +132,14 @@ const userVoiceCards = computed(() =>
}))
)
const displayedVoices = computed(() => userVoiceCards.value)
// 转换为下拉框选项格式
const voiceOptions = computed(() =>
displayedVoices.value.map(voice => ({
userVoiceCards.value.map(voice => ({
value: voice.id,
label: voice.name,
data: voice // 保存完整数据
data: voice
}))
)
// 音色选择变化处理
const handleVoiceChange = (value, option) => {
const voice = option.data
selectedVoiceId.value = value
@@ -136,26 +147,33 @@ const handleVoiceChange = (value, option) => {
emit('select', voice)
}
// 试听当前选中的音色
const handlePreviewCurrentVoice = () => {
const handleSynthesize = () => {
if (!selectedVoiceId.value) return
const voice = displayedVoices.value.find(v => v.id === selectedVoiceId.value)
const voice = userVoiceCards.value.find(v => v.id === selectedVoiceId.value)
if (!voice) return
currentVoiceName.value = voice.name
handlePlayVoiceSample(voice)
}
// 监听 prop 变化,更新 TTS 参数
watch(() => props.synthText, (newText) => {
setText(newText || '')
}, { immediate: true })
watch(() => props.speechRate, (newRate) => {
setSpeechRate(newRate)
}, { immediate: true })
/**
* 处理音色试听
* 处理音色
*/
const handlePlayVoiceSample = (voice) => {
currentVoiceName.value = voice.name
playVoiceSample(
voice,
(data) => {
// 提取音频 URL
const url = data.audioUrl || data.objectUrl
if (!url) {
console.error('无效的音频数据格式', data)
@@ -186,14 +204,11 @@ const initPlayer = (url) => {
preload: 'auto',
volume: 0.7,
audio: [{
name: currentVoiceName.value || '语音试听',
artist: '试听',
name: currentVoiceName.value || '语音合成',
artist: '合成',
url: url,
cover: defaultCover
}],
options: {
showDownload: true
}
}]
})
player.on('ended', () => {
@@ -209,6 +224,20 @@ const initPlayer = (url) => {
})
}
/**
* 下载音频
*/
const downloadAudio = () => {
if (!audioUrl.value) return
const link = document.createElement('a')
link.href = audioUrl.value
link.download = `${currentVoiceName.value || '语音合成'}.mp3`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
/**
* 销毁播放器
*/
@@ -227,26 +256,7 @@ const destroyPlayer = () => {
}
}
/**
* 设置要试听的文本(供父组件调用)
* @param {string} text 要试听的文本
*/
const setPreviewText = (text) => {
setText(text)
}
/**
* 设置语速(供父组件调用)
* @param {number} rate 语速倍率
*/
const setPreviewSpeechRate = (rate) => {
setSpeechRate(rate)
}
defineExpose({
setPreviewText,
setPreviewSpeechRate
})
defineExpose({})
onMounted(async () => {
await voiceStore.refresh()
@@ -294,4 +304,11 @@ onBeforeUnmount(() => {
.aplayer-container {
margin-top: 12px;
}
/* 下载链接样式 */
.download-link {
margin-top: 8px;
padding: 0;
font-size: 12px;
}
</style>

View File

@@ -24,7 +24,11 @@
<!-- 音色选择 -->
<div class="section">
<h3>音色</h3>
<VoiceSelector ref="voiceSelectorRef" @select="handleVoiceSelect" />
<VoiceSelector
:synth-text="ttsText"
:speech-rate="speechRate"
@select="handleVoiceSelect"
/>
</div>
<!-- TTS 控制 -->
@@ -300,8 +304,6 @@ import FullWidthLayout from '@/layouts/components/FullWidthLayout.vue'
import { useIdentifyFaceController } from './hooks/useIdentifyFaceController'
const voiceStore = useVoiceCopyStore()
const voiceSelectorRef: any = ref(null)
const dragOver = ref(false)
// ==================== 初始化 Controller ====================
@@ -355,12 +357,6 @@ const {
onMounted(async () => {
await voiceStore.refresh()
// 设置VoiceSelector的试听文本和语速
if (voiceSelectorRef.value) {
voiceSelectorRef.value.setPreviewText(ttsText.value)
voiceSelectorRef.value.setPreviewSpeechRate(speechRate.value)
}
})
</script>