音乐封面更改为黑胶留声机样式

This commit is contained in:
mtvpls
2026-05-22 11:32:52 +08:00
rodzic a7707dc991
commit cb3fb65348
+193 -6
Wyświetl plik
@@ -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) => (