300 lines
9.2 KiB
TypeScript
300 lines
9.2 KiB
TypeScript
"use client"
|
||
|
||
import type React from "react"
|
||
|
||
import { useState, useRef, useEffect } from "react"
|
||
import { Button } from "@/components/ui/button"
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
DialogTrigger,
|
||
} from "@/components/ui/dialog"
|
||
import { Badge } from "@/components/ui/badge"
|
||
import { Eye, Edit, Play, Pause, Volume2, VolumeX } from "lucide-react"
|
||
import { Slider } from "@/components/ui/slider"
|
||
|
||
export type Song = {
|
||
id: string
|
||
name: string
|
||
composer: string
|
||
lyricist: string
|
||
duration: string
|
||
releaseDate?: string
|
||
status: string
|
||
image?: string
|
||
audioUrl?: string
|
||
description?: string
|
||
rarity?: string
|
||
cardType?: string
|
||
genre?: string
|
||
lyrics?: string
|
||
bpm?: string | number
|
||
}
|
||
|
||
type SongDetailDialogProps = {
|
||
song: Song
|
||
onEdit?: () => void
|
||
}
|
||
|
||
export function SongDetailDialog({ song, onEdit }: SongDetailDialogProps) {
|
||
const [open, setOpen] = useState(false)
|
||
const [isPlaying, setIsPlaying] = useState(false)
|
||
const [isLoading, setIsLoading] = useState(false)
|
||
const [volume, setVolume] = useState(80)
|
||
const [isMuted, setIsMuted] = useState(false)
|
||
|
||
const audioRef = useRef<HTMLAudioElement | null>(null)
|
||
|
||
// 初始化音频播放器
|
||
useEffect(() => {
|
||
if (open && !audioRef.current && song.audioUrl) {
|
||
audioRef.current = new Audio(song.audioUrl)
|
||
|
||
const audio = audioRef.current
|
||
|
||
// 设置事件监听器
|
||
audio.addEventListener('canplay', () => {
|
||
setIsLoading(false)
|
||
})
|
||
|
||
audio.addEventListener('ended', () => {
|
||
setIsPlaying(false)
|
||
})
|
||
|
||
audio.addEventListener('error', () => {
|
||
setIsPlaying(false)
|
||
setIsLoading(false)
|
||
})
|
||
|
||
// 设置音量
|
||
audio.volume = volume / 100
|
||
audio.muted = isMuted
|
||
}
|
||
|
||
// 关闭对话框时清理
|
||
if (!open && audioRef.current) {
|
||
const audio = audioRef.current
|
||
audio.pause()
|
||
audio.src = ''
|
||
audioRef.current = null
|
||
setIsPlaying(false)
|
||
}
|
||
|
||
return () => {
|
||
if (audioRef.current) {
|
||
const audio = audioRef.current
|
||
audio.pause()
|
||
audio.src = ''
|
||
|
||
audio.removeEventListener('canplay', () => {})
|
||
audio.removeEventListener('ended', () => {})
|
||
audio.removeEventListener('error', () => {})
|
||
}
|
||
}
|
||
}, [open, song.audioUrl, volume, isMuted])
|
||
|
||
// 监听音量变化
|
||
useEffect(() => {
|
||
if (audioRef.current) {
|
||
audioRef.current.volume = volume / 100
|
||
}
|
||
}, [volume])
|
||
|
||
// 监听静音状态
|
||
useEffect(() => {
|
||
if (audioRef.current) {
|
||
audioRef.current.muted = isMuted
|
||
}
|
||
}, [isMuted])
|
||
|
||
const togglePlay = (e: React.MouseEvent) => {
|
||
e.stopPropagation()
|
||
|
||
if (!song.audioUrl) return
|
||
|
||
if (isPlaying) {
|
||
audioRef.current?.pause()
|
||
setIsPlaying(false)
|
||
} else {
|
||
setIsLoading(true)
|
||
if (audioRef.current) {
|
||
audioRef.current.play()
|
||
.then(() => {
|
||
setIsPlaying(true)
|
||
setIsLoading(false)
|
||
})
|
||
.catch((error) => {
|
||
console.error('播放失败:', error)
|
||
setIsLoading(false)
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
// 切换静音状态
|
||
const toggleMute = () => {
|
||
setIsMuted(!isMuted)
|
||
}
|
||
|
||
// 处理音量变化
|
||
const handleVolumeChange = (values: number[]) => {
|
||
const newVolume = values[0]
|
||
setVolume(newVolume)
|
||
|
||
// 如果音量从0调高,则取消静音
|
||
if (newVolume > 0 && isMuted) {
|
||
setIsMuted(false)
|
||
}
|
||
// 如果音量调到0,则设置为静音
|
||
else if (newVolume === 0 && !isMuted) {
|
||
setIsMuted(true)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Dialog open={open} onOpenChange={setOpen}>
|
||
<DialogTrigger asChild>
|
||
<Button variant="ghost" size="icon" className="hover:bg-blue-50 hover:text-blue-600">
|
||
<Eye className="h-4 w-4" />
|
||
</Button>
|
||
</DialogTrigger>
|
||
<DialogContent className="sm:max-w-[550px]">
|
||
<DialogHeader>
|
||
<DialogTitle className="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-purple-600 to-pink-600">
|
||
歌曲详情
|
||
</DialogTitle>
|
||
<DialogDescription>查看歌曲的详细信息</DialogDescription>
|
||
</DialogHeader>
|
||
<div className="grid gap-6 py-4">
|
||
<div className="flex items-center gap-4">
|
||
<div className="relative h-24 w-24 rounded-lg overflow-hidden bg-gray-100 flex items-center justify-center group">
|
||
<img
|
||
src={song.image || "/placeholder.svg?height=96&width=96"}
|
||
alt={song.name}
|
||
className="object-cover h-full w-full"
|
||
/>
|
||
{song.audioUrl && (
|
||
<div
|
||
className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center cursor-pointer"
|
||
onClick={togglePlay}
|
||
>
|
||
{isLoading ? (
|
||
<div className="h-10 w-10 flex items-center justify-center">
|
||
<div className="h-6 w-6 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
||
</div>
|
||
) : isPlaying ? (
|
||
<Pause className="h-10 w-10 text-white" />
|
||
) : (
|
||
<Play className="h-10 w-10 text-white" />
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div>
|
||
<h3 className="text-lg font-bold">{song.name}</h3>
|
||
<p className="text-sm text-gray-500">ID: {song.id}</p>
|
||
<div className="flex gap-2 mt-2">
|
||
<Badge className={song.status === "已发布" ? "bg-green-500" : "bg-gray-500"}>{song.status}</Badge>
|
||
{song.rarity && <Badge className="bg-purple-500">{song.rarity}</Badge>}
|
||
{song.cardType && <Badge className="bg-blue-500">{song.cardType}</Badge>}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 音频控制器 - 仅当歌曲有音频URL且正在播放时显示 */}
|
||
{song.audioUrl && isPlaying && (
|
||
<div className="flex items-center space-x-4 bg-pink-50 p-3 rounded-lg">
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
className="h-7 w-7 rounded-full hover:bg-pink-100"
|
||
onClick={toggleMute}
|
||
>
|
||
{isMuted ? <VolumeX className="h-3.5 w-3.5 text-pink-500" /> : <Volume2 className="h-3.5 w-3.5 text-pink-500" />}
|
||
</Button>
|
||
<div className="flex-1">
|
||
<Slider
|
||
value={[volume]}
|
||
min={0}
|
||
max={100}
|
||
step={1}
|
||
onValueChange={handleVolumeChange}
|
||
className="h-1"
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="space-y-4">
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<h4 className="text-sm font-medium text-gray-500 mb-1">作曲</h4>
|
||
<p className="text-sm">{song.composer}</p>
|
||
</div>
|
||
<div>
|
||
<h4 className="text-sm font-medium text-gray-500 mb-1">作词</h4>
|
||
<p className="text-sm">{song.lyricist}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<h4 className="text-sm font-medium text-gray-500 mb-1">时长</h4>
|
||
<p className="text-sm">{song.duration}</p>
|
||
</div>
|
||
<div>
|
||
<h4 className="text-sm font-medium text-gray-500 mb-1">发布日期</h4>
|
||
<p className="text-sm">{song.releaseDate || "未发布"}</p>
|
||
</div>
|
||
</div>
|
||
|
||
{song.genre && (
|
||
<div>
|
||
<h4 className="text-sm font-medium text-gray-500 mb-1">风格</h4>
|
||
<p className="text-sm">{song.genre}</p>
|
||
</div>
|
||
)}
|
||
|
||
{song.description && (
|
||
<div>
|
||
<h4 className="text-sm font-medium text-gray-500 mb-1">描述</h4>
|
||
<p className="text-sm">{song.description}</p>
|
||
</div>
|
||
)}
|
||
|
||
{song.lyrics && (
|
||
<div>
|
||
<h4 className="text-sm font-medium text-gray-500 mb-1">歌词</h4>
|
||
<div className="text-sm max-h-40 overflow-y-auto p-2 bg-gray-50 rounded">
|
||
<pre className="whitespace-pre-wrap font-sans">{song.lyrics}</pre>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||
关闭
|
||
</Button>
|
||
{song.status !== "已发布" && onEdit && (
|
||
<Button
|
||
className="bg-gradient-to-r from-pink-500 to-purple-600 hover:from-pink-600 hover:to-purple-700"
|
||
onClick={() => {
|
||
setOpen(false)
|
||
onEdit()
|
||
}}
|
||
>
|
||
<Edit className="mr-2 h-4 w-4" />
|
||
编辑歌曲
|
||
</Button>
|
||
)}
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
)
|
||
}
|