音乐封面更改为黑胶留声机样式
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
|
// 扩展 Window 接口以支持 Document PiP API
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -216,6 +284,7 @@ export default function MusicPage() {
|
|||||||
const [showPlayer, setShowPlayer] = useState(false);
|
const [showPlayer, setShowPlayer] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [showLyrics, setShowLyrics] = useState(false);
|
const [showLyrics, setShowLyrics] = useState(false);
|
||||||
|
const [mobileLyricsView, setMobileLyricsView] = useState<'lyrics' | 'vinyl'>('lyrics');
|
||||||
const [musicProxyEnabled, setMusicProxyEnabled] = useState(() => {
|
const [musicProxyEnabled, setMusicProxyEnabled] = useState(() => {
|
||||||
if (typeof window === 'undefined') return true;
|
if (typeof window === 'undefined') return true;
|
||||||
return (window as any).RUNTIME_CONFIG?.MUSIC_PROXY_ENABLED !== false;
|
return (window as any).RUNTIME_CONFIG?.MUSIC_PROXY_ENABLED !== false;
|
||||||
@@ -1928,7 +1997,99 @@ export default function MusicPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="music-theme min-h-screen bg-zinc-950 text-white">
|
<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 {
|
@keyframes music-note-bounce {
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
@@ -2564,7 +2725,7 @@ export default function MusicPage() {
|
|||||||
</button>
|
</button>
|
||||||
<div className="flex flex-1 min-h-0 flex-col md:flex-row">
|
<div className="flex flex-1 min-h-0 flex-col md:flex-row">
|
||||||
{/* Cover / Meta */}
|
{/* 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 && (
|
{currentSong.pic && (
|
||||||
<div className="absolute inset-0">
|
<div className="absolute inset-0">
|
||||||
<img
|
<img
|
||||||
@@ -2575,7 +2736,13 @@ export default function MusicPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="relative h-full flex flex-col items-center justify-center p-4 md:p-6 lg:p-8">
|
<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 ? (
|
{currentSong.pic ? (
|
||||||
<img
|
<img
|
||||||
src={currentSong.pic}
|
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">
|
<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" />
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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>
|
<p className="text-xs md:text-sm lg:text-base text-zinc-400 line-clamp-1 text-center">{currentSong.artist}</p>
|
||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* 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 ? (
|
{lyrics.length > 0 ? (
|
||||||
<div className="space-y-4 md:space-y-5">
|
<div className="space-y-4 md:space-y-5">
|
||||||
{lyrics.map((line, index) => (
|
{lyrics.map((line, index) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user