feat: 优化

This commit is contained in:
2026-02-02 22:36:20 +08:00
parent 33b748915d
commit 5cee704132
5 changed files with 108 additions and 16 deletions

View File

@@ -23,6 +23,7 @@
"@tailwindcss/vite": "^4.1.14",
"ai": "^6.0.39",
"ant-design-vue": "^4.2.6",
"aplayer": "^1.10.1",
"dayjs": "^1.11.18",
"markdown-it": "^14.1.0",
"path-to-regexp": "^6.3.0",

View File

@@ -27,19 +27,32 @@
试听
</a-button>
</div>
<!-- APlayer 播放器容器 -->
<div v-if="audioUrl" ref="playerContainer" class="aplayer-container"></div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { useVoiceCopyStore } from '@/stores/voiceCopy'
import { useTTS, TTS_PROVIDERS } from '@/composables/useTTS'
import APlayer from 'aplayer'
const voiceStore = useVoiceCopyStore()
const emit = defineEmits(['select'])
// 使用TTS Hook默认使用Qwen供应商
// APlayer 实例
let player = null
const playerContainer = ref(null)
const audioUrl = ref('')
const currentVoiceName = ref('')
// 默认封面图片Base64 SVG
const defaultCover = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIGZpbGw9IiMzY4MmY2IiBmaWxsLW9wYWNpdHk9IjAuMSIvPjxwL3N2Zz4='
// 使用TTS Hook
const {
previewLoadingVoiceId,
playingPreviewVoiceId,
@@ -87,6 +100,7 @@ const voiceOptions = computed(() =>
const handleVoiceChange = (value, option) => {
const voice = option.data
selectedVoiceId.value = value
currentVoiceName.value = voice.name
emit('select', voice)
}
@@ -97,27 +111,90 @@ const handlePreviewCurrentVoice = () => {
const voice = displayedVoices.value.find(v => v.id === selectedVoiceId.value)
if (!voice) return
currentVoiceName.value = voice.name
handlePlayVoiceSample(voice)
}
/**
* 处理音色试听
* 使用Hook提供的playVoiceSample方法
*/
const handlePlayVoiceSample = (voice) => {
currentVoiceName.value = voice.name
playVoiceSample(
voice,
(audioData) => {
// 成功回调
console.log('音频播放成功', audioData)
(data) => {
// 提取音频 URL
const url = data.audioUrl || data.objectUrl
if (!url) {
console.error('无效的音频数据格式', data)
return
}
initPlayer(url)
},
(error) => {
// 错误回调
console.error('音频播放失败', error)
}
)
}
/**
* 初始化 APlayer
*/
const initPlayer = (url) => {
destroyPlayer()
audioUrl.value = url
nextTick(() => {
if (!playerContainer.value) return
player = new APlayer({
container: playerContainer.value,
autoplay: true,
theme: '#3b82f6',
preload: 'auto',
volume: 0.7,
audio: [{
name: currentVoiceName.value || '语音试听',
artist: '试听',
url: url,
cover: defaultCover
}],
options: {
showDownload: true
}
})
player.on('ended', () => {
if (audioUrl.value?.startsWith('blob:')) {
URL.revokeObjectURL(audioUrl.value)
}
audioUrl.value = ''
})
player.on('error', (e) => {
console.error('APlayer 播放错误:', e)
})
})
}
/**
* 销毁播放器
*/
const destroyPlayer = () => {
if (player) {
try {
player.destroy()
} catch (e) {
console.error('销毁播放器失败:', e)
}
player = null
}
if (audioUrl.value) {
URL.revokeObjectURL(audioUrl.value)
audioUrl.value = ''
}
}
/**
* 设置要试听的文本(供父组件调用)
* @param {string} text 要试听的文本
@@ -142,6 +219,10 @@ defineExpose({
onMounted(async () => {
await voiceStore.refresh()
})
onBeforeUnmount(() => {
destroyPlayer()
})
</script>
<style scoped>
@@ -176,4 +257,9 @@ onMounted(async () => {
height: 32px;
white-space: nowrap;
}
/* APlayer 容器样式 */
.aplayer-container {
margin-top: 12px;
}
</style>

View File

@@ -3,6 +3,7 @@ import { createPinia } from 'pinia'
import Antd from 'ant-design-vue'
import 'normalize.css'
import 'ant-design-vue/dist/reset.css'
import 'aplayer/dist/APlayer.min.css'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import 'dayjs/locale/zh-cn';

View File

@@ -12,6 +12,7 @@
"description": "",
"dependencies": {
"@types/node": "^25.0.6",
"aplayer": "^1.10.1",
"axios": "^1.12.2",
"github-markdown-css": "^5.8.1",
"localforage": "^1.10.0",

View File

@@ -499,9 +499,9 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
log.info("[previewVoice][试听voiceConfigId={}, voiceId={}, userId={}]",
voiceConfigId, reqVO.getVoiceId(), userId);
String voiceId = null;
String fileUrl = null;
String referenceText = null;
String voiceId;
String fileUrl;
String referenceText;
// 1. 通过语音URL合成
if (StrUtil.isNotBlank(reqVO.getFileUrl()) && StrUtil.isNotBlank(reqVO.getTranscriptionText())) {
@@ -510,6 +510,7 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
? reqVO.getFileUrl()
: fileApi.presignGetUrl(rawFileUrl, PRESIGN_URL_EXPIRATION_SECONDS);
referenceText = reqVO.getTranscriptionText();
voiceId = null;
}
// 2. 用户配音
else if (voiceConfigId != null) {
@@ -518,8 +519,10 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
throw exception(VOICE_NOT_EXISTS, "配音不存在");
}
if (StrUtil.isNotBlank(voice.getVoiceId())) {
voiceId = voice.getVoiceId();
if (StrUtil.isNotBlank(voiceId)) {
fileUrl = null;
referenceText = null;
} else {
FileDO fileDO = fileMapper.selectById(voice.getFileId());
if (fileDO == null) {
@@ -538,14 +541,14 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
if (StrUtil.isBlank(voiceId)) {
throw exception(VOICE_NOT_EXISTS, "系统配音音色ID不能为空");
}
fileUrl = null;
referenceText = null;
}
// 统一处理:使用前端传入的 inputText否则使用默认试听文本
String finalText = StrUtil.blankToDefault(reqVO.getInputText(), getPreviewText());
String instruction = reqVO.getInstruction();
Float speechRate = reqVO.getSpeechRate() != null ? reqVO.getSpeechRate() : 1.0f;
Float volume = reqVO.getVolume() != null ? reqVO.getVolume() : 0f;
Float speechRate = ObjectUtil.defaultIfNull(reqVO.getSpeechRate(), 1.0f);
Float volume = ObjectUtil.defaultIfNull(reqVO.getVolume(), 0f);
String audioFormat = StrUtil.blankToDefault(reqVO.getAudioFormat(), "mp3");
// 缓存