彻底抽离music组件和路由
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
export default function MusicLoadingIndicator({
|
||||
text,
|
||||
size = 'md',
|
||||
className = '',
|
||||
}: {
|
||||
text?: string;
|
||||
size?: 'sm' | 'md';
|
||||
className?: string;
|
||||
}) {
|
||||
const iconSize = size === 'sm' ? 'w-4 h-4' : 'w-5 h-5';
|
||||
const textSize = size === 'sm' ? 'text-xs' : 'text-sm';
|
||||
|
||||
return (
|
||||
<div className={`flex items-center justify-center gap-3 text-zinc-400 ${className}`}>
|
||||
<div className="flex items-end gap-1.5">
|
||||
{[0, 1, 2].map((index) => (
|
||||
<svg
|
||||
key={index}
|
||||
className={`${iconSize} text-green-400`}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
style={{ animation: `music-note-bounce 0.9s ease-in-out ${index * 0.14}s infinite` }}
|
||||
>
|
||||
<path d="M12 3v11.55A3.98 3.98 0 0010 14c-2.21 0-4 1.34-4 3s1.79 3 4 3 4-1.34 4-3V8h4V3h-6z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
{text ? <span className={`${textSize} font-medium tracking-wide`}>{text}</span> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import type { MusicSource } from '@/lib/music/types';
|
||||
|
||||
interface MusicSidebarDrawerProps {
|
||||
currentSource: MusicSource;
|
||||
isOpen: boolean;
|
||||
pathname: string | null;
|
||||
onClose: () => void;
|
||||
onNavigate: (href: string) => void;
|
||||
}
|
||||
|
||||
const musicNavItems = [
|
||||
{ key: 'rankings', label: '排行榜', href: '/music/rankings', icon: 'M3 4h18M8 8h13M3 12h18M8 16h13M3 20h18' },
|
||||
{ key: 'search', label: '搜索', icon: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z' },
|
||||
{ key: 'my-playlists', label: '我的歌单', href: '/music/my-playlists', icon: 'M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z' },
|
||||
];
|
||||
|
||||
export default function MusicSidebarDrawer({
|
||||
currentSource,
|
||||
isOpen,
|
||||
pathname,
|
||||
onClose,
|
||||
onNavigate,
|
||||
}: MusicSidebarDrawerProps) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const navigate = (href: string) => {
|
||||
onClose();
|
||||
onNavigate(href);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[95]">
|
||||
<button
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm animate-in fade-in duration-300"
|
||||
onClick={onClose}
|
||||
aria-label="关闭菜单"
|
||||
/>
|
||||
<aside className="absolute left-0 top-0 flex h-full w-72 max-w-[85vw] flex-col border-r border-white/10 bg-zinc-950/80 backdrop-blur-xl px-5 py-6 shadow-[20px_0_40px_rgba(0,0,0,0.5)] animate-in slide-in-from-left duration-300 ease-out">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-2xl bg-gradient-to-br from-green-400 to-emerald-600 text-white shadow-lg shadow-green-500/20">
|
||||
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M18 3a1 1 0 00-1.196-.98l-10 2A1 1 0 006 5v9.114A4.369 4.369 0 005 14c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V7.82l8-1.6v5.894A4.37 4.37 0 0015 12c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-lg font-bold text-white tracking-wide">音乐菜单</div>
|
||||
<div className="text-[10px] uppercase tracking-wider text-green-400/80">Music Navigation</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full bg-white/5 text-zinc-400 transition-all hover:bg-white/10 hover:text-white hover:rotate-90"
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="space-y-3 relative z-10">
|
||||
{musicNavItems.map((item) => {
|
||||
const href = item.key === 'search' ? `/music/search?source=${currentSource}` : item.href ?? '/music/rankings';
|
||||
const active = item.key === 'rankings'
|
||||
? pathname?.startsWith('/music/rankings') || pathname === '/music'
|
||||
: pathname?.startsWith(`/music/${item.key}`);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.key}
|
||||
onClick={() => navigate(href)}
|
||||
className={`group flex w-full items-center gap-4 rounded-2xl px-4 py-3.5 text-left transition-all duration-300 ${
|
||||
active
|
||||
? 'bg-gradient-to-r from-green-500/20 to-emerald-500/5 text-white shadow-inner border border-green-500/20'
|
||||
: 'bg-transparent text-zinc-400 hover:bg-white/5 hover:text-white border border-transparent'
|
||||
}`}
|
||||
>
|
||||
<div className={`flex items-center justify-center transition-colors ${active ? 'text-green-400' : 'text-zinc-500 group-hover:text-white'}`}>
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d={item.icon} />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-sm font-semibold">{item.label}</span>
|
||||
{active && (
|
||||
<div className="ml-auto w-1.5 h-1.5 rounded-full bg-green-400 shadow-[0_0_8px_rgba(74,222,128,0.8)] animate-pulse" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="mt-auto relative z-10 pt-6">
|
||||
<div className="absolute top-0 inset-x-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="group flex w-full items-center gap-4 rounded-2xl border border-white/5 bg-white/5 px-4 py-3.5 text-left text-zinc-400 transition-all duration-300 hover:bg-white/10 hover:text-white hover:border-white/10 hover:shadow-lg"
|
||||
>
|
||||
<div className="flex items-center justify-center text-zinc-500 group-hover:text-white transition-colors">
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-sm font-semibold">返回主页</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="pointer-events-none absolute -left-20 -top-20 h-64 w-64 rounded-full bg-green-500/10 blur-[80px]" />
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
'use client';
|
||||
|
||||
import { addMusicSongToPlaylist, playMusicLater, playMusicSong } from '@/lib/music/actions';
|
||||
import { SourcePill } from '@/lib/music/shared';
|
||||
import type { Song } from '@/lib/music/types';
|
||||
|
||||
export default function SongList({ songs }: { songs: Song[] }) {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{songs.map((song, index) => (
|
||||
<div
|
||||
key={`${song.platform}-${song.id}-${index}`}
|
||||
className="grid grid-cols-[40px_1fr_auto_auto] md:grid-cols-[50px_2fr_1fr_auto_auto] gap-2 px-3 py-3 rounded-lg cursor-pointer transition-all hover:bg-white/5"
|
||||
>
|
||||
<div className="text-center text-zinc-500 dark:text-zinc-300 text-sm" onClick={() => playMusicSong(song, index)}>
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="min-w-0" onClick={() => playMusicSong(song, index)}>
|
||||
<div className="text-sm font-medium text-white truncate">{song.name}</div>
|
||||
<div className="text-xs text-zinc-500 truncate md:hidden">{song.artist}</div>
|
||||
</div>
|
||||
<div className="hidden md:block text-sm text-zinc-400 truncate" onClick={() => playMusicSong(song, index)}>
|
||||
{song.artist}
|
||||
</div>
|
||||
<div className="flex items-center" onClick={() => playMusicSong(song, index)}>
|
||||
<SourcePill source={song.platform} />
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center gap-0.5 leading-none">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
addMusicSongToPlaylist(song);
|
||||
}}
|
||||
className="text-zinc-500 hover:text-red-500 transition-colors p-0.5"
|
||||
title="添加到歌单"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
playMusicLater(song);
|
||||
}}
|
||||
className="text-zinc-500 hover:text-green-500 transition-colors p-0.5"
|
||||
title="稍后播放"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-9-9 9 9 0 019 9z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user