美化音乐界面
This commit is contained in:
@@ -3408,26 +3408,26 @@ export default function MusicClient({ children: _children }: { children?: React.
|
||||
{showSidebarDrawer && (
|
||||
<div className="fixed inset-0 z-[95]">
|
||||
<button
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm animate-in fade-in duration-300"
|
||||
onClick={() => setShowSidebarDrawer(false)}
|
||||
aria-label="关闭菜单"
|
||||
/>
|
||||
<aside className="absolute left-0 top-0 flex h-full w-72 max-w-[86vw] flex-col border-r border-white/10 bg-zinc-950/98 px-4 py-5 shadow-2xl">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<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-9 w-9 items-center justify-center rounded-full bg-white/10 text-green-500">
|
||||
<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-base font-bold text-white">音乐菜单</div>
|
||||
<div className="text-xs text-zinc-500">Music Navigation</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={() => setShowSidebarDrawer(false)}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full bg-white/10 text-zinc-400 hover:text-white"
|
||||
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" />
|
||||
@@ -3435,7 +3435,7 @@ export default function MusicClient({ children: _children }: { children?: React.
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="space-y-2">
|
||||
<nav className="space-y-3 relative z-10">
|
||||
{[
|
||||
{ key: 'rankings', label: '排行榜', href: '/music/rankings', icon: 'M3 4h18M8 8h13M3 12h18M8 16h13M3 20h18' },
|
||||
{ key: 'search', label: '搜索', href: `/music/search?source=${currentSource}`, icon: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z' },
|
||||
@@ -3451,33 +3451,46 @@ export default function MusicClient({ children: _children }: { children?: React.
|
||||
setShowSidebarDrawer(false);
|
||||
router.push(item.href);
|
||||
}}
|
||||
className={`flex w-full items-center gap-3 rounded-xl border px-3 py-3 text-left transition-all ${
|
||||
className={`group flex w-full items-center gap-4 rounded-2xl px-4 py-3.5 text-left transition-all duration-300 ${
|
||||
active
|
||||
? 'border-green-500/40 bg-green-500/15 text-white'
|
||||
: 'border-white/5 bg-white/5 text-zinc-400 hover:bg-white/10 hover:text-white'
|
||||
? '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'
|
||||
}`}
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d={item.icon} />
|
||||
</svg>
|
||||
<span className="text-sm font-medium">{item.label}</span>
|
||||
<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>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowSidebarDrawer(false);
|
||||
router.push('/');
|
||||
}}
|
||||
className="mt-auto flex w-full items-center gap-3 rounded-xl border border-white/10 bg-white/5 px-3 py-3 text-left text-zinc-400 transition-colors hover:bg-white/10 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" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
<span className="text-sm font-medium">返回主页</span>
|
||||
</button>
|
||||
<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={() => {
|
||||
setShowSidebarDrawer(false);
|
||||
router.push('/');
|
||||
}}
|
||||
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>
|
||||
|
||||
{/* Ambient Background Effect */}
|
||||
<div className="pointer-events-none absolute -left-20 -top-20 h-64 w-64 rounded-full bg-green-500/10 blur-[80px]" />
|
||||
</aside>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -9,6 +9,7 @@ export default function MusicRankingsPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [currentSource, setCurrentSource] = useState(normalizeSource(searchParams.get('source')));
|
||||
const [showSourceMenu, setShowSourceMenu] = useState(false);
|
||||
const [playlists, setPlaylists] = useState<Playlist[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -34,50 +35,106 @@ export default function MusicRankingsPage() {
|
||||
.finally(() => setLoading(false));
|
||||
}, [searchParams]);
|
||||
|
||||
const currentSourceLabel = musicSources.find(s => s.key === currentSource)?.label || '音源';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-6 flex items-center justify-between gap-3 border-b border-white/5 pb-3">
|
||||
<h2 className="shrink-0 text-xs font-mono text-white/50 tracking-widest">排行榜</h2>
|
||||
<div className="flex max-w-[calc(100vw-7rem)] justify-end overflow-x-auto rounded-full border border-white/10 bg-white/[0.04] p-1 shadow-inner shadow-black/20 md:max-w-none md:overflow-visible">
|
||||
{musicSources.map((source) => {
|
||||
const active = currentSource === source.key;
|
||||
return (
|
||||
<div className="mb-8 flex items-center justify-between border-b border-[rgba(0,0,0,0.08)] dark:border-[rgba(255,255,255,0.08)] pb-4 relative z-50">
|
||||
<h2 className="text-2xl font-bold text-black dark:text-white tracking-tight">热歌榜单</h2>
|
||||
|
||||
{/* Pixel-perfect macOS Source Switcher */}
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowSourceMenu((open) => !open)}
|
||||
className="flex items-center gap-1.5 px-2.5 py-1 rounded-[6px] bg-[rgba(0,0,0,0.04)] dark:bg-[rgba(255,255,255,0.08)] hover:bg-[rgba(0,0,0,0.08)] dark:hover:bg-[rgba(255,255,255,0.12)] border border-[rgba(0,0,0,0.04)] dark:border-[rgba(255,255,255,0.04)] shadow-sm transition-all"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={showSourceMenu}
|
||||
>
|
||||
<span className="text-[13px] text-[rgba(0,0,0,0.5)] dark:text-[rgba(255,255,255,0.5)]">音源</span>
|
||||
<span className="text-[13px] font-medium text-black dark:text-white">{currentSourceLabel}</span>
|
||||
<svg className="w-3.5 h-3.5 opacity-50 ml-0.5 text-black dark:text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l4-4 4 4m0 6l-4 4-4-4" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{showSourceMenu && (
|
||||
<>
|
||||
<button
|
||||
key={source.key}
|
||||
onClick={() => router.push(`/music/rankings?source=${source.key}`)}
|
||||
className={`shrink-0 rounded-full px-3 py-1.5 text-xs font-bold transition-all ${
|
||||
active
|
||||
? 'bg-gradient-to-r from-green-500 to-emerald-400 text-white shadow-lg shadow-green-500/25'
|
||||
: 'text-zinc-400 hover:bg-white/10 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{source.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
type="button"
|
||||
className="fixed inset-0 z-40 cursor-default"
|
||||
onClick={() => setShowSourceMenu(false)}
|
||||
aria-label="关闭菜单"
|
||||
/>
|
||||
<div className="absolute right-0 top-[calc(100%+6px)] z-50 w-32 p-1.5 overflow-hidden rounded-xl bg-[rgba(255,255,255,0.85)] dark:bg-[rgba(35,35,35,0.85)] backdrop-blur-2xl shadow-[0_8px_30px_rgba(0,0,0,0.12)] border border-[rgba(0,0,0,0.1)] dark:border-[rgba(255,255,255,0.15)] animate-in fade-in zoom-in-95 duration-100 origin-top-right" role="listbox">
|
||||
{musicSources.map((item) => {
|
||||
const active = currentSource === item.key;
|
||||
return (
|
||||
<button
|
||||
key={item.key}
|
||||
onClick={() => {
|
||||
setShowSourceMenu(false);
|
||||
router.push(`/music/rankings?source=${item.key}`);
|
||||
}}
|
||||
className="group flex w-full items-center justify-between rounded-md px-2 py-1.5 text-left text-[13px] text-black dark:text-white hover:bg-green-500 hover:text-white transition-none"
|
||||
role="option"
|
||||
aria-selected={active}
|
||||
>
|
||||
<span className="whitespace-nowrap">{item.label}</span>
|
||||
{active && (
|
||||
<svg className="h-3.5 w-3.5 text-black dark:text-white group-hover:text-white shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{loading ? <MusicLoadingIndicator className="py-8" /> : playlists.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{loading ? <MusicLoadingIndicator className="py-12" /> : playlists.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{playlists.map((playlist, index) => (
|
||||
<button
|
||||
key={playlist.id}
|
||||
onClick={() => router.push(`/music/rankings/${playlist.source || currentSource}/${encodeURIComponent(playlist.id)}?name=${encodeURIComponent(playlist.name)}`)}
|
||||
className="w-full text-left rounded-xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors px-4 py-3"
|
||||
className="group relative w-full text-left overflow-hidden rounded-2xl border border-white/5 bg-white/5 hover:bg-white/10 transition-all duration-300 p-4 hover:-translate-y-1 hover:shadow-xl backdrop-blur-sm"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-8 text-sm text-zinc-500 dark:text-zinc-300 font-mono shrink-0">{String(index + 1).padStart(2, '0')}</div>
|
||||
<div className="absolute top-0 right-0 -mt-2 -mr-2 w-16 h-16 bg-white/5 rounded-full blur-xl group-hover:bg-green-500/20 transition-colors duration-500"></div>
|
||||
<div className="flex items-center gap-4 relative z-10">
|
||||
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-white/5 text-lg font-bold text-zinc-400 font-mono shadow-inner group-hover:bg-green-500/20 group-hover:text-green-500 transition-colors">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-white/90 truncate">{playlist.name}</div>
|
||||
{playlist.updateFrequency ? <div className="text-xs text-zinc-500 mt-1 truncate">{playlist.updateFrequency}</div> : null}
|
||||
<div className="text-base font-semibold text-white/90 truncate group-hover:text-white transition-colors">{playlist.name}</div>
|
||||
{playlist.updateFrequency ? (
|
||||
<div className="text-xs text-zinc-500 mt-1 truncate group-hover:text-zinc-400 transition-colors flex items-center gap-1">
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{playlist.updateFrequency}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="text-zinc-600 group-hover:text-green-500 group-hover:translate-x-1 transition-all duration-300 shrink-0">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-zinc-500 shrink-0">›</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-xl border border-white/10 bg-white/5 p-6 text-center text-zinc-400">当前音源暂无排行榜</div>
|
||||
<div className="flex flex-col items-center justify-center rounded-2xl border border-dashed border-white/10 bg-white/5 p-12 text-center text-zinc-400">
|
||||
<svg className="mb-4 h-12 w-12 text-zinc-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3" />
|
||||
</svg>
|
||||
<div className="text-lg font-medium text-zinc-300 mb-1">暂无数据</div>
|
||||
<div className="text-sm text-zinc-500">当前音源暂无排行榜数据</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -53,79 +53,106 @@ export default function MusicSearchPage() {
|
||||
const currentSourceLabel = musicSources.find((item) => item.key === selectedSource)?.label || '音源';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-6 flex items-center gap-2">
|
||||
<div className="relative h-11 flex-1 rounded-xl border border-white/10 bg-white/10">
|
||||
<div className="pointer-events-none absolute inset-y-0 left-3 flex items-center">
|
||||
<svg className="h-4 w-4 text-zinc-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
||||
</div>
|
||||
<input value={keyword} onChange={(e) => setKeyword(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && submit()} className="h-full w-full border-0 bg-transparent pl-10 pr-28 text-sm text-white outline-none placeholder:text-zinc-500" placeholder="搜索歌曲或艺术家..." />
|
||||
<div className="absolute inset-y-0 right-2 flex items-center">
|
||||
<div className="relative h-8">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowSourceMenu((open) => !open)}
|
||||
className="flex h-8 min-w-[84px] items-center justify-between gap-1 rounded-lg border border-white/10 bg-zinc-900/90 pl-3 pr-2 text-xs font-medium text-white shadow-inner shadow-white/5 transition-colors hover:bg-zinc-800"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={showSourceMenu}
|
||||
aria-label="选择音源"
|
||||
>
|
||||
<span>{currentSourceLabel}</span>
|
||||
<svg className={`h-3.5 w-3.5 text-zinc-400 transition-transform ${showSourceMenu ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
{showSourceMenu && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="fixed inset-0 z-40 cursor-default"
|
||||
onClick={() => setShowSourceMenu(false)}
|
||||
aria-label="关闭音源菜单"
|
||||
/>
|
||||
<div className="absolute right-0 top-10 z-50 w-32 overflow-hidden rounded-xl border border-white/10 bg-zinc-900/95 p-1 shadow-2xl shadow-black/50 backdrop-blur-md" role="listbox">
|
||||
{musicSources.map((item) => {
|
||||
const active = item.key === selectedSource;
|
||||
return (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
onClick={() => changeSource(item.key)}
|
||||
className={`flex w-full items-center justify-between rounded-lg px-3 py-2 text-left text-xs transition-colors ${
|
||||
active ? 'bg-green-500/20 text-green-300' : 'text-zinc-300 hover:bg-white/10 hover:text-white'
|
||||
}`}
|
||||
role="option"
|
||||
aria-selected={active}
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
{active && (
|
||||
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="animate-in fade-in duration-500 relative z-10">
|
||||
<div className="mb-8 flex items-center gap-3 relative z-50">
|
||||
<div className="relative flex-1 flex items-center bg-[rgba(0,0,0,0.04)] dark:bg-[rgba(255,255,255,0.06)] rounded-[8px] pl-3 border border-[rgba(0,0,0,0.06)] dark:border-[rgba(255,255,255,0.06)] focus-within:border-green-500 focus-within:ring-0 transition-all shadow-sm">
|
||||
<svg className="w-4 h-4 opacity-40 text-black dark:text-white mr-2 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
||||
<input
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && submit()}
|
||||
className="w-full bg-transparent border-none outline-none focus:outline-none focus:ring-0 text-[14px] py-2 text-black dark:text-white placeholder-[rgba(0,0,0,0.4)] dark:placeholder-[rgba(255,255,255,0.4)]"
|
||||
placeholder="搜索歌曲或艺术家..."
|
||||
/>
|
||||
|
||||
<div className="w-px h-5 bg-[rgba(0,0,0,0.1)] dark:bg-[rgba(255,255,255,0.1)] mx-1 shrink-0" />
|
||||
|
||||
<div className="relative h-full flex items-center pr-1 shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowSourceMenu((open) => !open)}
|
||||
className="flex h-[32px] items-center gap-1.5 px-2.5 rounded-[6px] hover:bg-[rgba(0,0,0,0.06)] dark:hover:bg-[rgba(255,255,255,0.08)] transition-all"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={showSourceMenu}
|
||||
>
|
||||
<span className="text-[13px] font-medium text-black dark:text-white">{currentSourceLabel}</span>
|
||||
<svg className="w-3.5 h-3.5 opacity-50 text-black dark:text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l4-4 4 4m0 6l-4 4-4-4" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{showSourceMenu && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="fixed inset-0 z-40 cursor-default"
|
||||
onClick={() => setShowSourceMenu(false)}
|
||||
aria-label="关闭菜单"
|
||||
/>
|
||||
<div className="absolute right-0 top-[calc(100%+6px)] z-50 w-32 p-1.5 overflow-hidden rounded-xl bg-[rgba(255,255,255,0.85)] dark:bg-[rgba(35,35,35,0.85)] backdrop-blur-2xl shadow-[0_8px_30px_rgba(0,0,0,0.12)] border border-[rgba(0,0,0,0.1)] dark:border-[rgba(255,255,255,0.15)] animate-in fade-in zoom-in-95 duration-100 origin-top-right" role="listbox">
|
||||
{musicSources.map((item) => {
|
||||
const active = selectedSource === item.key;
|
||||
return (
|
||||
<button
|
||||
key={item.key}
|
||||
onClick={() => {
|
||||
setShowSourceMenu(false);
|
||||
changeSource(item.key);
|
||||
}}
|
||||
className="group flex w-full items-center justify-between rounded-md px-2 py-1.5 text-left text-[13px] text-black dark:text-white hover:bg-green-500 hover:text-white transition-none"
|
||||
role="option"
|
||||
aria-selected={active}
|
||||
>
|
||||
<span className="whitespace-nowrap">{item.label}</span>
|
||||
{active && (
|
||||
<svg className="h-3.5 w-3.5 text-black dark:text-white group-hover:text-white shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={submit} className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-green-600 text-white hover:bg-green-700" aria-label="搜索" title="搜索">
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 mb-8 border-b border-white/10 pb-4">
|
||||
<div className="flex items-center gap-4 min-w-0">
|
||||
<h2 className="text-2xl font-black text-white tracking-tight truncate max-w-md">
|
||||
{q ? `搜索: ${q}` : '发现音乐'}
|
||||
</h2>
|
||||
{songs.length > 0 && (
|
||||
<span className="px-2.5 py-1 text-xs font-bold bg-white/10 text-green-500 rounded-full border border-green-500/20 shrink-0">
|
||||
{songs.length} 首结果
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => playMusicList(songs, q ? `搜索: ${q}` : '搜索结果')}
|
||||
disabled={songs.length === 0}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-white/10 hover:bg-white/20 disabled:bg-white/5 disabled:opacity-50 disabled:cursor-not-allowed rounded-xl text-sm font-medium text-white transition-colors shrink-0"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z" /></svg>
|
||||
播放全部
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mb-6 border-b border-white/5 pb-2">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<h2 className="text-xl font-bold text-white/80 tracking-tight truncate max-w-md">{q ? `搜索: ${q}` : '搜索'}</h2>
|
||||
<span className="text-[10px] font-bold bg-white/10 px-2 py-0.5 rounded text-white shrink-0">{songs.length} 首歌曲</span>
|
||||
{loading ? (
|
||||
<MusicLoadingIndicator className="py-16" />
|
||||
) : q ? (
|
||||
<SongList songs={songs} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center rounded-3xl border border-dashed border-white/10 bg-white/5 py-20 text-center backdrop-blur-sm">
|
||||
<div className="w-20 h-20 mb-6 rounded-full bg-white/5 flex items-center justify-center shadow-inner">
|
||||
<svg className="h-10 w-10 text-zinc-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-zinc-300 mb-2">开始你的音乐探索</div>
|
||||
<div className="text-sm text-zinc-500 max-w-sm">在上方输入你想听的歌曲、歌手或专辑,我们为你搜罗全网好音乐。</div>
|
||||
</div>
|
||||
<button onClick={() => playMusicList(songs, q ? `搜索: ${q}` : '搜索结果')} disabled={songs.length === 0} className="px-3 py-1.5 bg-green-600 hover:bg-green-700 disabled:bg-zinc-700 disabled:cursor-not-allowed rounded-lg text-sm text-white shrink-0">播放全部</button>
|
||||
</div>
|
||||
{loading ? <MusicLoadingIndicator className="py-8" /> : q ? <SongList songs={songs} /> : <div className="rounded-xl border border-white/10 bg-white/5 p-8 text-center text-zinc-400">输入关键词开始搜索</div>}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user