refactor: replace ant-design components with shadcn/ui and update toast notifications
This commit migrates from Ant Design Vue components to Shadcn/Vue components across multiple files in the web-gold frontend application. Key changes include: - Replaced ant-design-vue imports with shadcn/ui components (Dialog, Button, Input, etc.) - Swapped ant-design-vue message/toast system for vue-sonner toast notifications - Updated icon usage from ant-design icons to lucide icons via @iconify/vue - Removed unused token refresh logic that was incorrectly implemented client-side - Applied consistent styling updates to match new component library The token refresh functionality was removed since it should be handled server-side through axios interceptors rather than client-side intervals.
This commit is contained in:
@@ -2,20 +2,13 @@
|
||||
<div class="voice-selector">
|
||||
<!-- 空状态 -->
|
||||
<div v-if="userVoiceCards.length === 0" class="empty-voices">
|
||||
<a-empty :image="simpleImage" description="还没有配音">
|
||||
<template #image>
|
||||
<div class="empty-icon">
|
||||
<svg viewBox="0 0 64 64" fill="none">
|
||||
<circle cx="32" cy="32" r="28" fill="#f1f5f9" stroke="#e2e8f0" stroke-width="2"/>
|
||||
<path d="M32 18C36.4183 18 40 21.5817 40 26V38C40 42.4183 36.4183 46 32 46C27.5817 46 24 42.4183 24 38V26C24 21.5817 27.5817 18 32 18Z" fill="#cbd5e1"/>
|
||||
<path d="M32 14V18M32 46V50M18 32H22M42 32H46" stroke="#94a3b8" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
<a-button type="primary" size="small" @click="$router.push('/voice-copy')">
|
||||
去创建配音
|
||||
</a-button>
|
||||
</a-empty>
|
||||
<div class="empty-icon">
|
||||
<Icon icon="lucide:mic-off" class="size-10 text-muted-foreground/40" />
|
||||
</div>
|
||||
<p class="text-muted-foreground mb-4">还没有配音</p>
|
||||
<Button @click="$router.push('/voice-copy')">
|
||||
去创建配音
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-else class="voice-selector-wrapper">
|
||||
@@ -25,23 +18,21 @@
|
||||
<span class="header-title">选择音色</span>
|
||||
<span class="voice-count">{{ userVoiceCards.length }} 个配音</span>
|
||||
</div>
|
||||
<a-button
|
||||
<Button
|
||||
v-if="selectedVoiceId"
|
||||
class="synthesize-btn"
|
||||
:disabled="isPlayerInitializing"
|
||||
:loading="previewLoadingVoiceId === selectedVoiceId"
|
||||
@click="handleSynthesize"
|
||||
>
|
||||
<template #icon>
|
||||
<SoundOutlined />
|
||||
</template>
|
||||
<Icon icon="lucide:volume-2" class="size-4" />
|
||||
合成试听
|
||||
</a-button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 卡片网格 -->
|
||||
<div class="voice-grid" :class="{ 'has-many': userVoiceCards.length > 4 }">
|
||||
<div
|
||||
<button
|
||||
v-for="voice in userVoiceCards"
|
||||
:key="voice.id"
|
||||
class="voice-card"
|
||||
@@ -52,15 +43,11 @@
|
||||
<div class="card-avatar">
|
||||
<div class="avatar-ring"></div>
|
||||
<div class="avatar-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="3" fill="currentColor"/>
|
||||
<path d="M12 2v4M12 18v4M2 12h4M18 12h4" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.5"/>
|
||||
<path d="M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
|
||||
</svg>
|
||||
<Icon icon="lucide:audio-waveform" class="size-6" />
|
||||
</div>
|
||||
<!-- 选中指示器 -->
|
||||
<div v-if="selectedVoiceId === voice.id" class="selected-indicator">
|
||||
<CheckOutlined />
|
||||
<Icon icon="lucide:check" class="size-3" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,7 +56,7 @@
|
||||
<div class="voice-name">{{ voice.name }}</div>
|
||||
<div class="voice-desc">{{ voice.description || '我的配音' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 播放器区域 -->
|
||||
@@ -78,20 +65,17 @@
|
||||
<div class="player-header">
|
||||
<div class="player-info">
|
||||
<div class="player-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="10" fill="currentColor" opacity="0.1"/>
|
||||
<path d="M10 8L16 12L10 16V8Z" fill="currentColor"/>
|
||||
</svg>
|
||||
<Icon icon="lucide:play-circle" class="size-8" />
|
||||
</div>
|
||||
<div class="player-meta">
|
||||
<div class="player-title">{{ currentVoiceName }}</div>
|
||||
<div class="player-label">合成预览</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-button type="text" size="small" @click="downloadAudio" class="download-btn">
|
||||
<template #icon><DownloadOutlined /></template>
|
||||
<Button variant="ghost" size="sm" @click="downloadAudio" class="download-btn">
|
||||
<Icon icon="lucide:download" class="size-4" />
|
||||
下载
|
||||
</a-button>
|
||||
</Button>
|
||||
</div>
|
||||
<div ref="playerContainer" class="aplayer-container"></div>
|
||||
</div>
|
||||
@@ -102,25 +86,25 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
|
||||
import { Empty, message } from 'ant-design-vue'
|
||||
import { SoundOutlined, DownloadOutlined, CheckOutlined } from '@ant-design/icons-vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useVoiceCopyStore } from '@/stores/voiceCopy'
|
||||
import { useTTS, TTS_PROVIDERS } from '@/composables/useTTS'
|
||||
import APlayer from 'aplayer'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
const props = defineProps({
|
||||
synthText: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: '',
|
||||
},
|
||||
speechRate: {
|
||||
type: Number,
|
||||
default: 1.0
|
||||
}
|
||||
default: 1.0,
|
||||
},
|
||||
})
|
||||
|
||||
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE
|
||||
|
||||
const voiceStore = useVoiceCopyStore()
|
||||
|
||||
const emit = defineEmits(['select', 'audioGenerated'])
|
||||
@@ -171,15 +155,15 @@ const {
|
||||
previewLoadingVoiceId,
|
||||
playVoiceSample,
|
||||
setText,
|
||||
setSpeechRate
|
||||
setSpeechRate,
|
||||
} = useTTS({
|
||||
provider: TTS_PROVIDERS.SILICONFLOW
|
||||
provider: TTS_PROVIDERS.SILICONFLOW,
|
||||
})
|
||||
|
||||
const selectedVoiceId = ref('')
|
||||
|
||||
const userVoiceCards = computed(() =>
|
||||
(voiceStore.profiles || []).map(profile => ({
|
||||
(voiceStore.profiles || []).map((profile) => ({
|
||||
id: `user-${profile.id}`,
|
||||
rawId: profile.id,
|
||||
name: profile.name || '未命名',
|
||||
@@ -189,8 +173,8 @@ const userVoiceCards = computed(() =>
|
||||
fileUrl: profile.fileUrl,
|
||||
transcription: profile.transcription || '',
|
||||
source: 'user',
|
||||
voiceId: profile.voiceId
|
||||
}))
|
||||
voiceId: profile.voiceId,
|
||||
})),
|
||||
)
|
||||
|
||||
const handleVoiceSelect = (voice) => {
|
||||
@@ -202,7 +186,7 @@ const handleVoiceSelect = (voice) => {
|
||||
const handleSynthesize = () => {
|
||||
if (!selectedVoiceId.value || isPlayerInitializing.value) return
|
||||
|
||||
const voice = userVoiceCards.value.find(v => v.id === selectedVoiceId.value)
|
||||
const voice = userVoiceCards.value.find((v) => v.id === selectedVoiceId.value)
|
||||
if (!voice) return
|
||||
|
||||
currentVoiceName.value = voice.name
|
||||
@@ -210,13 +194,21 @@ const handleSynthesize = () => {
|
||||
}
|
||||
|
||||
// 监听 prop 变化,更新 TTS 参数
|
||||
watch(() => props.synthText, (newText) => {
|
||||
setText(newText || '')
|
||||
}, { immediate: true })
|
||||
watch(
|
||||
() => props.synthText,
|
||||
(newText) => {
|
||||
setText(newText || '')
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(() => props.speechRate, (newRate) => {
|
||||
setSpeechRate(newRate)
|
||||
}, { immediate: true })
|
||||
watch(
|
||||
() => props.speechRate,
|
||||
(newRate) => {
|
||||
setSpeechRate(newRate)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const handlePlayVoiceSample = (voice) => {
|
||||
currentVoiceName.value = voice.name
|
||||
@@ -227,8 +219,10 @@ const handlePlayVoiceSample = (voice) => {
|
||||
if (!url) return
|
||||
initPlayer(url)
|
||||
},
|
||||
() => { /* 错误已在 useTTS 中处理 */ },
|
||||
{ autoPlay: false }
|
||||
() => {
|
||||
/* 错误已在 useTTS 中处理 */
|
||||
},
|
||||
{ autoPlay: false },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -249,7 +243,7 @@ const initPlayer = (url) => {
|
||||
try {
|
||||
// 检查容器是否存在
|
||||
if (!playerContainer.value) {
|
||||
message.error('播放器容器未就绪')
|
||||
toast.error('播放器容器未就绪')
|
||||
isPlayerInitializing.value = false
|
||||
audioUrl.value = ''
|
||||
return
|
||||
@@ -261,12 +255,14 @@ const initPlayer = (url) => {
|
||||
theme: '#3b82f6',
|
||||
volume: 0.7,
|
||||
loop: 'none',
|
||||
audio: [{
|
||||
name: currentVoiceName.value || '语音合成',
|
||||
artist: '合成',
|
||||
url: url,
|
||||
cover: defaultCover
|
||||
}]
|
||||
audio: [
|
||||
{
|
||||
name: currentVoiceName.value || '语音合成',
|
||||
artist: '合成',
|
||||
url: url,
|
||||
cover: defaultCover,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
player.on('ended', () => {
|
||||
@@ -284,13 +280,13 @@ const initPlayer = (url) => {
|
||||
if (durationMs > 0) {
|
||||
emit('audioGenerated', {
|
||||
durationMs,
|
||||
audioUrl: audioUrl.value // 使用 URL(性能优化)
|
||||
audioUrl: audioUrl.value, // 使用 URL(性能优化)
|
||||
})
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('APlayer 初始化失败:', e)
|
||||
message.error('播放器初始化失败,请重试')
|
||||
toast.error('播放器初始化失败,请重试')
|
||||
isPlayerInitializing.value = false
|
||||
audioUrl.value = ''
|
||||
}
|
||||
@@ -349,10 +345,6 @@ onBeforeUnmount(() => {
|
||||
|
||||
.empty-icon {
|
||||
margin-bottom: 12px;
|
||||
svg {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,6 +444,7 @@ onBeforeUnmount(() => {
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
border-color: #e2e8f0;
|
||||
@@ -496,11 +489,9 @@ onBeforeUnmount(() => {
|
||||
z-index: 1;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.selected-indicator {
|
||||
@@ -577,11 +568,9 @@ onBeforeUnmount(() => {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
color: #3b82f6;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.player-meta {
|
||||
|
||||
Reference in New Issue
Block a user