This commit is contained in:
fengchuanhn@gmail.com
2026-05-18 10:18:49 +08:00
parent beb5eaeb42
commit 6cc305f5e8
5 changed files with 253 additions and 5 deletions

View File

@@ -10,6 +10,7 @@ import {
listCloneVoices,
deleteCloneVoice,
} from "../../services/soundPromptStorage.js";
import { previewVoice } from "../../services/voicePreview.js";
import VoiceCloneEditDialog from "./VoiceCloneEditDialog.vue";
const workflow = useWorkflowStore();
@@ -21,6 +22,12 @@ const cloneVoices = ref([]);
const cloneLoading = ref(false);
const editDialogRef = ref(null);
const previewingKey = ref(null);
const previewPanelKey = ref(null);
const previewAudioSrc = ref("");
const previewError = ref("");
const audioRef = ref(null);
const filteredSystemVoices = computed(() =>
filterSystemVoices(voiceCategory.value),
);
@@ -39,9 +46,22 @@ watch(voiceManageModalVisible, (open) => {
activeTab.value = "system";
voiceCategory.value = "putonghua";
loadCloneVoices();
} else {
stopPreview();
}
});
function stopPreview() {
previewingKey.value = null;
previewPanelKey.value = null;
previewAudioSrc.value = "";
previewError.value = "";
if (audioRef.value) {
audioRef.value.pause();
audioRef.value.removeAttribute("src");
}
}
function onClose() {
workflow.closeVoiceManageDialog();
}
@@ -64,12 +84,62 @@ function onDeleteClone(record) {
if (selectedVoiceId.value === record.id) {
workflow.selectVoice("sys_Cherry");
}
if (previewPanelKey.value === record.id) {
stopPreview();
}
loadCloneVoices();
}
function stripVoiceIdPrefix(id) {
return String(id || "").replace(/^cosyvoice-v3-flash-/, "");
}
async function runPreview(opts) {
previewError.value = "";
previewingKey.value = opts.listKey;
const result = await previewVoice(opts);
previewingKey.value = null;
if (!result.ok) {
previewError.value = result.message;
return;
}
previewPanelKey.value = opts.listKey;
previewAudioSrc.value = result.audioSrc;
requestAnimationFrame(() => {
audioRef.value?.play()?.catch(() => {});
});
}
function onPreviewSystem(voice) {
runPreview({
listKey: voice.id,
title: voice.title,
aliyunVoiceId: voice.aliyunVoiceId,
});
}
function onPreviewClone(item) {
const voiceId = String(item.content?.aliyunVoiceId || "").trim();
if (!voiceId) {
previewError.value = "请先配置音色 ID 后再试听";
return;
}
runPreview({
listKey: item.id,
title: item.title,
aliyunVoiceId: voiceId,
});
}
function isPreviewing(key) {
return previewingKey.value === key;
}
function showPreviewPlayer(key) {
return previewPanelKey.value === key && previewAudioSrc.value;
}
</script>
<template>
@@ -82,6 +152,8 @@ function stripVoiceIdPrefix(id) {
class="voice-manage-dialog"
@hide="onClose"
>
<audio ref="audioRef" class="hidden" :src="previewAudioSrc || undefined" />
<Tabs v-model:value="activeTab">
<TabList>
<Tab value="system">系统音色</Tab>
@@ -113,11 +185,33 @@ function stripVoiceIdPrefix(id) {
</div>
<p class="mt-2 text-xs text-slate-500">音色ID: {{ voice.aliyunVoiceId }}</p>
</div>
<Button
label="选择"
size="small"
icon="pi pi-check"
@click="onSelectVoice(voice.id)"
<div class="flex shrink-0 gap-1">
<Button
label="试听"
size="small"
severity="secondary"
icon="pi pi-volume-up"
:loading="isPreviewing(voice.id)"
:disabled="Boolean(previewingKey && !isPreviewing(voice.id))"
@click="onPreviewSystem(voice)"
/>
<Button
label="选择"
size="small"
icon="pi pi-check"
@click="onSelectVoice(voice.id)"
/>
</div>
</div>
<div
v-if="showPreviewPlayer(voice.id)"
class="mt-2 rounded-lg border border-blue-500/30 bg-blue-500/10 px-2 py-2"
>
<audio
:src="previewAudioSrc"
controls
class="h-8 w-full"
autoplay
/>
</div>
</div>
@@ -170,6 +264,18 @@ function stripVoiceIdPrefix(id) {
</p>
</div>
<div class="flex shrink-0 gap-1">
<Button
label="试听"
size="small"
severity="secondary"
icon="pi pi-volume-up"
:loading="isPreviewing(item.id)"
:disabled="
!item.content?.aliyunVoiceId ||
Boolean(previewingKey && !isPreviewing(item.id))
"
@click="onPreviewClone(item)"
/>
<Button label="选择" size="small" @click="onSelectVoice(item.id)" />
<Button
icon="pi pi-pencil"
@@ -186,12 +292,21 @@ function stripVoiceIdPrefix(id) {
@click="onDeleteClone(item)"
/>
</div>
</div>
<div
v-if="showPreviewPlayer(item.id)"
class="mt-2 rounded-lg border border-blue-500/30 bg-blue-500/10 px-2 py-2"
>
<audio :src="previewAudioSrc" controls class="h-8 w-full" autoplay />
</div>
</div>
</div>
</TabPanel>
</TabPanels>
</Tabs>
<p v-if="previewError" class="mt-2 text-sm text-red-400">{{ previewError }}</p>
<VoiceCloneEditDialog ref="editDialogRef" @saved="loadCloneVoices" />
</Dialog>
</template>