音乐封面更改为黑胶留声机样式
This commit is contained in:
+193
-6
@@ -183,6 +183,74 @@ function AudioSpectrumCanvas({
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const VINYL_NEEDLE_SVG = `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 130"><path d="M21,21 C21,65 86,70 86,100" fill="none" stroke="rgba(0,0,0,0.28)" stroke-width="6" stroke-linecap="round"/><path d="M20,20 C20,65 85,70 85,100" fill="none" stroke="%23e0e0e0" stroke-width="4.5" stroke-linecap="round"/><path d="M19,20 C19,65 84,70 84,100" fill="none" stroke="%23fff" stroke-width="1.5" stroke-linecap="round"/><g transform="translate(85, 100) rotate(25)"><rect x="-6" y="0" width="12" height="18" rx="2" fill="%23ccc"/><rect x="-4" y="5" width="8" height="14" rx="1" fill="%23333"/><rect x="-2" y="16" width="4" height="5" rx="1" fill="%23d43c33"/></g><circle cx="20" cy="20" r="10" fill="%23f0f0f0" stroke="%23ccc" stroke-width="1"/><circle cx="20" cy="20" r="4" fill="%23fff"/><circle cx="20" cy="20" r="1.5" fill="%23999"/></svg>')`;
|
||||
|
||||
function VinylTurntable({
|
||||
cover,
|
||||
title,
|
||||
isPlaying,
|
||||
className = '',
|
||||
}: {
|
||||
cover?: string;
|
||||
title: string;
|
||||
isPlaying: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`relative mx-auto mt-12 mb-5 flex h-[280px] w-[280px] items-center justify-center md:mt-16 md:mb-8 md:h-[340px] md:w-[340px] ${className}`}
|
||||
aria-label="黑胶唱片机封面"
|
||||
>
|
||||
<div
|
||||
className="pointer-events-none absolute left-1/2 top-[-54px] z-20 h-[140px] w-[108px] drop-shadow-xl transition-transform duration-500"
|
||||
style={{
|
||||
marginLeft: '-20px',
|
||||
backgroundImage: VINYL_NEEDLE_SVG,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: 'contain',
|
||||
transformOrigin: '20px 20px',
|
||||
transform: isPlaying ? 'rotate(0deg)' : 'rotate(-30deg)',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="relative flex h-[246px] w-[246px] items-center justify-center overflow-hidden rounded-full md:h-[300px] md:w-[300px]"
|
||||
style={{
|
||||
background: 'conic-gradient(from 45deg, #070707 0%, #2b2b2b 10%, #101010 20%, #080808 32%, #242424 42%, #0b0b0b 55%, #1e1e1e 68%, #050505 80%, #2c2c2c 90%, #070707 100%)',
|
||||
boxShadow: '0 0 0 8px rgba(255,255,255,0.055), 0 20px 42px rgba(0,0,0,0.65), inset 0 0 28px rgba(255,255,255,0.045)',
|
||||
animation: 'vinyl-spin 20s linear infinite',
|
||||
animationPlayState: isPlaying ? 'running' : 'paused',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 rounded-full"
|
||||
style={{
|
||||
background: 'repeating-radial-gradient(circle, transparent 0, transparent 3px, rgba(255,255,255,0.055) 3px, rgba(255,255,255,0.055) 4px)',
|
||||
}}
|
||||
/>
|
||||
<div className="pointer-events-none absolute left-[18%] top-[10%] h-[42%] w-[22%] rotate-[-28deg] rounded-full bg-white/10 blur-md" />
|
||||
<div className="relative z-10 flex h-[158px] w-[158px] items-center justify-center overflow-hidden rounded-full border-[5px] border-black bg-zinc-800 md:h-[192px] md:w-[192px]">
|
||||
{cover ? (
|
||||
<img
|
||||
src={cover}
|
||||
alt={title}
|
||||
className="h-full w-full rounded-full object-cover"
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<svg className="h-14 w-14 text-zinc-500 md:h-16 md:w-16" 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 className="pointer-events-none absolute z-20 h-3 w-3 rounded-full border border-white/30 bg-zinc-950" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 扩展 Window 接口以支持 Document PiP API
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -216,6 +284,7 @@ export default function MusicPage() {
|
||||
const [showPlayer, setShowPlayer] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showLyrics, setShowLyrics] = useState(false);
|
||||
const [mobileLyricsView, setMobileLyricsView] = useState<'lyrics' | 'vinyl'>('lyrics');
|
||||
const [musicProxyEnabled, setMusicProxyEnabled] = useState(() => {
|
||||
if (typeof window === 'undefined') return true;
|
||||
return (window as any).RUNTIME_CONFIG?.MUSIC_PROXY_ENABLED !== false;
|
||||
@@ -1928,7 +1997,99 @@ export default function MusicPage() {
|
||||
return (
|
||||
<div className="music-theme min-h-screen bg-zinc-950 text-white">
|
||||
<>
|
||||
<style jsx>{`
|
||||
<style jsx global>{`
|
||||
@keyframes vinyl-spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.vinyl-player {
|
||||
position: relative;
|
||||
width: min(72vw, 300px);
|
||||
height: min(72vw, 300px);
|
||||
margin: 42px auto 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.vinyl-player {
|
||||
width: clamp(260px, 30vw, 360px);
|
||||
height: clamp(260px, 30vw, 360px);
|
||||
margin: 54px auto 28px;
|
||||
}
|
||||
}
|
||||
.vinyl-needle {
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
left: 50%;
|
||||
margin-left: -20px;
|
||||
width: 108px;
|
||||
height: 140px;
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 130"><path d="M21,21 C21,65 86,70 86,100" fill="none" stroke="rgba(0,0,0,0.28)" stroke-width="6" stroke-linecap="round"/><path d="M20,20 C20,65 85,70 85,100" fill="none" stroke="%23e0e0e0" stroke-width="4.5" stroke-linecap="round"/><path d="M19,20 C19,65 84,70 84,100" fill="none" stroke="%23fff" stroke-width="1.5" stroke-linecap="round"/><g transform="translate(85, 100) rotate(25)"><rect x="-6" y="0" width="12" height="18" rx="2" fill="%23ccc"/><rect x="-4" y="5" width="8" height="14" rx="1" fill="%23333"/><rect x="-2" y="16" width="4" height="5" rx="1" fill="%23d43c33"/></g><circle cx="20" cy="20" r="10" fill="%23f0f0f0" stroke="%23ccc" stroke-width="1"/><circle cx="20" cy="20" r="4" fill="%23fff"/><circle cx="20" cy="20" r="1.5" fill="%23999"/></svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
transform-origin: 20px 20px;
|
||||
transform: rotate(-30deg);
|
||||
transition: transform 0.5s cubic-bezier(0.3, 0, 0.1, 1);
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
filter: drop-shadow(0 8px 8px rgba(0,0,0,0.35));
|
||||
}
|
||||
.vinyl-player.playing .vinyl-needle {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
.vinyl-disc {
|
||||
position: relative;
|
||||
width: 88%;
|
||||
height: 88%;
|
||||
border-radius: 9999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: conic-gradient(from 45deg, #080808 0%, #2a2a2a 10%, #0f0f0f 20%, #111 40%, #303030 50%, #111 60%, #090909 80%, #2a2a2a 90%, #080808 100%);
|
||||
box-shadow: 0 0 0 8px rgba(255,255,255,0.045), 0 18px 36px rgba(0,0,0,0.55), inset 0 0 24px rgba(255,255,255,0.04);
|
||||
animation: vinyl-spin 20s linear infinite;
|
||||
animation-play-state: paused;
|
||||
}
|
||||
.vinyl-player.playing .vinyl-disc {
|
||||
animation-play-state: running;
|
||||
}
|
||||
.vinyl-disc::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
background: repeating-radial-gradient(transparent 0, transparent 3px, rgba(255,255,255,0.045) 3px, rgba(255,255,255,0.045) 4px), radial-gradient(circle at 35% 25%, rgba(255,255,255,0.14), transparent 28%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.vinyl-disc::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 9999px;
|
||||
background: #111;
|
||||
border: 2px solid rgba(255,255,255,0.25);
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
}
|
||||
.vinyl-cover {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 64%;
|
||||
height: 64%;
|
||||
border-radius: 9999px;
|
||||
object-fit: cover;
|
||||
border: 5px solid #0a0a0a;
|
||||
user-select: none;
|
||||
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.06);
|
||||
}
|
||||
.vinyl-cover-fallback {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #27272a;
|
||||
}
|
||||
@keyframes music-note-bounce {
|
||||
0%,
|
||||
100% {
|
||||
@@ -2564,7 +2725,7 @@ export default function MusicPage() {
|
||||
</button>
|
||||
<div className="flex flex-1 min-h-0 flex-col md:flex-row">
|
||||
{/* Cover / Meta */}
|
||||
<div className="relative h-32 md:h-auto md:w-[320px] lg:w-[360px] xl:w-[420px] bg-gradient-to-b from-zinc-800 to-zinc-900 shrink-0">
|
||||
<div className="relative h-32 md:h-auto md:w-[380px] lg:w-[430px] xl:w-[480px] bg-gradient-to-b from-zinc-800 to-zinc-900 shrink-0 overflow-hidden">
|
||||
{currentSong.pic && (
|
||||
<div className="absolute inset-0">
|
||||
<img
|
||||
@@ -2575,7 +2736,13 @@ export default function MusicPage() {
|
||||
</div>
|
||||
)}
|
||||
<div className="relative h-full flex flex-col items-center justify-center p-4 md:p-6 lg:p-8">
|
||||
<div className="w-16 h-16 md:w-40 md:h-40 lg:w-56 lg:h-56 rounded-xl lg:rounded-2xl overflow-hidden shadow-2xl mb-2 md:mb-4 lg:mb-6">
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setMobileLyricsView((view) => (view === 'lyrics' ? 'vinyl' : 'lyrics'));
|
||||
}}
|
||||
className="w-16 h-16 md:hidden rounded-xl overflow-hidden shadow-2xl mb-2"
|
||||
>
|
||||
{currentSong.pic ? (
|
||||
<img
|
||||
src={currentSong.pic}
|
||||
@@ -2584,19 +2751,39 @@ export default function MusicPage() {
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full bg-zinc-800 flex items-center justify-center">
|
||||
<svg className="w-8 h-8 md:w-16 md:h-16 lg:w-20 lg:h-20 text-zinc-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<svg className="w-8 h-8 text-zinc-600" 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>
|
||||
<h2 className="text-base md:text-lg lg:text-2xl font-bold text-white text-center mb-1 line-clamp-2">{currentSong.name}</h2>
|
||||
<div className="hidden md:block w-full">
|
||||
<VinylTurntable cover={currentSong.pic} title={currentSong.name} isPlaying={isPlaying} />
|
||||
</div>
|
||||
<h2 className="text-base md:text-xl lg:text-2xl font-bold text-white text-center mb-1 line-clamp-2">{currentSong.name}</h2>
|
||||
<p className="text-xs md:text-sm lg:text-base text-zinc-400 line-clamp-1 text-center">{currentSong.artist}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{mobileLyricsView === 'vinyl' && (
|
||||
<div className="relative md:hidden flex-1 min-h-0 overflow-hidden bg-gradient-to-b from-zinc-800 to-zinc-900">
|
||||
{currentSong.pic && (
|
||||
<div className="absolute inset-0">
|
||||
<img
|
||||
src={currentSong.pic}
|
||||
alt={currentSong.name}
|
||||
className="w-full h-full object-cover opacity-30 blur-xl"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative h-full flex items-center justify-center p-4">
|
||||
<VinylTurntable cover={currentSong.pic} title={currentSong.name} isPlaying={isPlaying} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Lyrics Content */}
|
||||
<div ref={lyricsContainerRef} className="flex-1 overflow-y-auto p-4 md:p-6 lg:p-8">
|
||||
<div ref={lyricsContainerRef} className={`${mobileLyricsView === 'vinyl' ? 'hidden md:block' : 'block'} flex-1 overflow-y-auto p-4 md:p-6 lg:p-8`}>
|
||||
{lyrics.length > 0 ? (
|
||||
<div className="space-y-4 md:space-y-5">
|
||||
{lyrics.map((line, index) => (
|
||||
|
||||
Reference in New Issue
Block a user