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:
2026-03-16 02:41:26 +08:00
parent 52c3b5489d
commit 110fe62404
8 changed files with 632 additions and 736 deletions

View File

@@ -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 {