AI_Painting_V2.0/src/components/AudioPlayer/index.vue
WangLeo 2d12c5a20b feat: 新增 Music 音乐生成平台,遵循 Platform Descriptor 模式
基于旧项目 ai_music_v2.0 迁移,与 Painting/Video 统一架构:HTTP 轮询 + suanli 后端、
API 驱动配置、mode 独立 ref 驱动控件显隐。新增 AudioPlayer/CustomSlider 通用组件,
dialogBox/set.vue/taskPolling/modelApi 完成集成适配。
2026-06-12 19:20:18 +08:00

268 lines
7.4 KiB
Vue

<template>
<div class="audio-placeholder" :style="{ backgroundImage: `url(${backgroundImage})` }">
<div class="audio-left">
<img :src="coverImage" alt="音频封面" class="audio-cover" />
</div>
<div class="audio-center">
<div class="audio-title">{{ audioTitle }}</div>
<div class="audio-progress">
<div class="progress-bar" @click="handleProgressClick">
<div class="progress-fill" :style="{ width: progressPercentage + '%' }"></div>
</div>
<div class="progress-time">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</div>
</div>
</div>
<div class="audio-right">
<div class="play-button" @click.stop="handlePlayPause">
<svg v-if="!isPlaying" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 5V19L19 12L8 5Z" fill="white"/>
</svg>
<svg v-else width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" fill="white"/>
</svg>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import background1 from '@/assets/display/background1.png'
import background2 from '@/assets/display/background2.png'
import background3 from '@/assets/display/background3.png'
import background4 from '@/assets/display/background4.png'
import image1 from '@/assets/display/image1.png'
import image2 from '@/assets/display/image2.png'
import image3 from '@/assets/display/image3.png'
import image4 from '@/assets/display/image4.png'
const props = defineProps({
audioUrl: { type: String, default: '' },
audioTitle: { type: String, default: '我的音乐' },
cardIndex: { type: Number, default: 0 }
})
const emit = defineEmits(['play', 'pause', 'ended'])
const backgroundImages = [background1, background2, background3, background4]
const coverImages = [image1, image2, image3, image4]
const backgroundImage = computed(() => backgroundImages[props.cardIndex % 4])
const coverImage = computed(() => coverImages[props.cardIndex % 4])
const audioRef = ref(null)
const isPlaying = ref(false)
const isPlayPending = ref(false)
const currentTime = ref(0)
const duration = ref(0)
let timeupdateHandler = null
let loadedmetadataHandler = null
let endedHandler = null
const progressPercentage = computed(() => {
if (duration.value === 0) return 0
return (currentTime.value / duration.value) * 100
})
const formatTime = (seconds) => {
if (!seconds || isNaN(seconds)) return '00:00'
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
}
const handleProgressClick = (event) => {
if (!audioRef.value || duration.value === 0) return
const rect = event.currentTarget.getBoundingClientRect()
const clickX = event.clientX - rect.left
const percentage = Math.max(0, Math.min(1, clickX / rect.width))
const newTime = percentage * duration.value
audioRef.value.currentTime = newTime
currentTime.value = newTime
}
const setupAudio = (url) => {
if (audioRef.value) {
audioRef.value.pause()
audioRef.value.removeEventListener('timeupdate', timeupdateHandler)
audioRef.value.removeEventListener('loadedmetadata', loadedmetadataHandler)
audioRef.value.removeEventListener('ended', endedHandler)
audioRef.value.src = ''
audioRef.value.load()
audioRef.value = null
}
isPlaying.value = false
currentTime.value = 0
duration.value = 0
if (!url) return
audioRef.value = new Audio(url)
audioRef.value.crossOrigin = 'anonymous'
timeupdateHandler = () => {
if (audioRef.value) currentTime.value = audioRef.value.currentTime
}
loadedmetadataHandler = () => {
if (audioRef.value) duration.value = audioRef.value.duration || 0
}
endedHandler = () => {
isPlaying.value = false
currentTime.value = 0
emit('ended')
}
audioRef.value.addEventListener('timeupdate', timeupdateHandler)
audioRef.value.addEventListener('loadedmetadata', loadedmetadataHandler)
audioRef.value.addEventListener('ended', endedHandler)
}
const handlePlayPause = () => {
if (!audioRef.value) {
setupAudio(props.audioUrl)
}
if (!audioRef.value) return
if (isPlaying.value) {
audioRef.value.pause()
isPlaying.value = false
isPlayPending.value = false
emit('pause')
} else if (!isPlayPending.value) {
isPlayPending.value = true
audioRef.value.play().then(() => {
isPlaying.value = true
isPlayPending.value = false
emit('play')
}).catch((error) => {
console.error('播放失败:', error)
isPlaying.value = false
isPlayPending.value = false
})
}
}
onMounted(() => { if (props.audioUrl) setupAudio(props.audioUrl) })
onUnmounted(() => {
if (audioRef.value) {
audioRef.value.pause()
audioRef.value.removeEventListener('timeupdate', timeupdateHandler)
audioRef.value.removeEventListener('loadedmetadata', loadedmetadataHandler)
audioRef.value.removeEventListener('ended', endedHandler)
audioRef.value.src = ''
audioRef.value.load()
audioRef.value = null
}
})
watch(() => props.audioUrl, (newUrl, oldUrl) => {
if (newUrl === oldUrl) return
setupAudio(newUrl)
})
</script>
<style lang="less" scoped>
.audio-placeholder {
width: 100%;
height: 100%;
min-height: 80px;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
border-radius: 8px;
display: flex;
align-items: center;
gap: 16px;
padding: 12px 16px;
cursor: pointer;
.audio-left {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
.audio-cover {
width: 56px;
height: 56px;
border-radius: 5px;
object-fit: cover;
}
}
.audio-center {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
gap: 16px;
min-width: 0;
.audio-title {
color: white;
font-family: "Microsoft YaHei";
font-size: 14px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.audio-progress {
display: flex;
flex-direction: column;
gap: 4px;
.progress-bar {
width: 100%;
height: 4px;
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
overflow: hidden;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
height: 6px;
background: rgba(255, 255, 255, 0.4);
}
.progress-fill {
height: 100%;
background: white;
border-radius: 2px;
transition: width 0.1s ease;
}
}
.progress-time {
color: rgba(255, 255, 255, 0.8);
font-family: "Microsoft YaHei";
font-size: 11px;
font-weight: 400;
}
}
}
.audio-right {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
.play-button {
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(10px);
transition: all 0.3s ease;
&:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
svg { width: 20px; height: 20px; fill: white; }
}
}
}
</style>