Files
web_template/web/dist/music2/index.html
2026-07-10 10:07:20 +00:00

1887 lines
81 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>黑胶电台 - 复古音乐平台</title>
<style>
/* ==================== CSS Reset & Variables ==================== */
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg-dark:#1a1410;
--bg-warm:#2a2018;
--bg-card:#332619;
--gold:#d4a017;
--gold-light:#e8b942;
--gold-dim:#9a7412;
--cream:#f5e6c8;
--red:#c0392b;
--red-dark:#8b0000;
--text-primary:#f5e6c8;
--text-secondary:#c4a877;
--text-muted:#8a7560;
--border-warm:rgba(212,160,23,0.3);
--border-dim:rgba(212,160,23,0.12);
--shadow-warm:0 4px 20px rgba(0,0,0,0.5);
--shadow-deep:0 8px 40px rgba(0,0,0,0.7);
--glow-gold:0 0 25px rgba(212,160,23,0.35);
--radius:6px;
--serif:Georgia,'Times New Roman',serif;
--mono:'Courier New',Courier,monospace;
}
html{scroll-behavior:smooth;font-size:16px}
body{
font-family:var(--serif);
color:var(--text-primary);
min-height:100vh;
background:
radial-gradient(ellipse at 20% 0%,rgba(212,160,23,0.06) 0%,transparent 50%),
radial-gradient(ellipse at 80% 100%,rgba(192,57,43,0.05) 0%,transparent 50%),
linear-gradient(160deg,var(--bg-dark) 0%,var(--bg-warm) 50%,var(--bg-dark) 100%);
background-attachment:fixed;
overflow-x:hidden;
position:relative;
}
/* Subtle texture overlay */
body::before{
content:'';position:fixed;inset:0;z-index:0;pointer-events:none;
background:
repeating-linear-gradient(90deg,rgba(0,0,0,0.015) 0px,rgba(0,0,0,0.015) 1px,transparent 1px,transparent 3px),
repeating-linear-gradient(0deg,rgba(0,0,0,0.01) 0px,rgba(0,0,0,0.01) 1px,transparent 1px,transparent 4px);
}
a{color:inherit;text-decoration:none}
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
input,select{font-family:inherit}
ul{list-style:none}
.container{max-width:1200px;margin:0 auto;padding:0 24px;position:relative;z-index:1}
/* ==================== Scrollbar ==================== */
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-track{background:var(--bg-dark)}
::-webkit-scrollbar-thumb{background:var(--gold-dim);border-radius:5px;border:2px solid var(--bg-dark)}
::-webkit-scrollbar-thumb:hover{background:var(--gold)}
/* ==================== Header ==================== */
.header{
position:sticky;top:0;z-index:100;
background:
linear-gradient(180deg,rgba(26,20,16,0.98) 0%,rgba(42,32,24,0.95) 100%);
border-bottom:1px solid var(--border-warm);
box-shadow:0 2px 20px rgba(0,0,0,0.6);
}
.header::after{
content:'';position:absolute;bottom:-1px;left:0;right:0;height:1px;
background:linear-gradient(90deg,transparent,var(--gold),transparent);
opacity:0.5;
}
.header-inner{
display:flex;align-items:center;justify-content:space-between;
height:68px;gap:24px;
}
.logo{display:flex;align-items:center;gap:10px;flex-shrink:0}
.logo-icon{
width:36px;height:36px;border-radius:50%;
background:radial-gradient(circle,#c0392b 0%,#c0392b 25%,#0a0806 26%,#0a0806 100%);
position:relative;
animation:spin 8s linear infinite;
box-shadow:0 0 12px rgba(0,0,0,0.6),inset 0 0 6px rgba(0,0,0,0.4);
}
.logo-icon::after{
content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
width:4px;height:4px;border-radius:50%;background:var(--gold);
}
.logo-text{
font-family:var(--serif);font-size:1.35rem;font-weight:bold;
color:var(--gold);letter-spacing:2px;
text-shadow:0 1px 3px rgba(0,0,0,0.5);
}
.logo-sub{font-size:0.6rem;color:var(--text-muted);letter-spacing:3px;text-transform:uppercase}
.nav{display:flex;gap:4px;align-items:center}
.nav-link{
padding:8px 16px;font-size:0.95rem;color:var(--text-secondary);
border-radius:var(--radius);transition:all 0.3s ease;
position:relative;letter-spacing:1px;
}
.nav-link:hover{color:var(--gold);background:rgba(212,160,23,0.08)}
.nav-link.active{color:var(--gold)}
.nav-link.active::after{
content:'';position:absolute;bottom:-2px;left:50%;transform:translateX(-50%);
width:60%;height:2px;background:var(--gold);border-radius:1px;
}
.search-bar{
display:flex;align-items:center;gap:8px;
background:rgba(26,20,16,0.8);
border:1px solid var(--border-warm);
border-radius:var(--radius);
padding:6px 14px;
transition:all 0.3s ease;
min-width:220px;
}
.search-bar:focus-within{border-color:var(--gold);box-shadow:var(--glow-gold)}
.search-bar svg{width:16px;height:16px;color:var(--text-muted);flex-shrink:0}
.search-bar input{
background:none;border:none;outline:none;color:var(--cream);
font-size:0.9rem;width:100%;padding:4px 0;
}
.search-bar input::placeholder{color:var(--text-muted);font-style:italic}
.header-fm{
font-family:var(--mono);font-size:0.7rem;color:var(--gold-dim);
letter-spacing:1px;border-left:1px solid var(--border-dim);padding-left:12px;
display:flex;flex-direction:column;align-items:flex-end;
}
.header-fm .eq{display:flex;gap:2px;align-items:flex-end;height:14px;margin-top:2px}
.header-fm .eq span{
width:2px;background:var(--gold);border-radius:1px;
animation:eqBounce 0.8s ease-in-out infinite;
}
.header-fm .eq span:nth-child(1){height:6px;animation-delay:0s}
.header-fm .eq span:nth-child(2){height:10px;animation-delay:0.15s}
.header-fm .eq span:nth-child(3){height:4px;animation-delay:0.3s}
.header-fm .eq span:nth-child(4){height:8px;animation-delay:0.1s}
.header-fm .eq span:nth-child(5){height:5px;animation-delay:0.25s}
/* ==================== Hero ==================== */
.hero{
position:relative;padding:60px 0 80px;overflow:hidden;
}
.hero-bg-notes{position:absolute;inset:0;pointer-events:none;opacity:0.06;font-size:8rem}
.hero-bg-notes span{position:absolute;font-family:var(--serif);color:var(--gold)}
.hero-bg-notes span:nth-child(1){top:10%;left:5%}
.hero-bg-notes span:nth-child(2){top:60%;right:8%;font-size:6rem}
.hero-bg-notes span:nth-child(3){bottom:5%;left:15%;font-size:5rem}
.hero-bg-notes span:nth-child(4){top:20%;right:20%;font-size:7rem}
.hero-inner{
display:flex;align-items:center;justify-content:space-between;gap:60px;
}
.hero-content{flex:1;max-width:540px}
.hero-tag{
display:inline-flex;align-items:center;gap:8px;
font-family:var(--mono);font-size:0.75rem;color:var(--gold);
letter-spacing:3px;text-transform:uppercase;
border:1px solid var(--border-warm);border-radius:20px;
padding:4px 16px;margin-bottom:24px;
background:rgba(212,160,23,0.05);
}
.hero-tag::before{content:'●';color:var(--red);animation:blink 2s infinite}
.hero-title{
font-family:var(--serif);font-size:3.2rem;font-weight:bold;
line-height:1.15;letter-spacing:2px;margin-bottom:16px;
background:linear-gradient(180deg,var(--cream) 0%,var(--gold) 100%);
-webkit-background-clip:text;-webkit-text-fill-color:transparent;
background-clip:text;
}
.hero-title .accent{color:var(--red);-webkit-text-fill-color:var(--red);font-style:italic}
.hero-subtitle{
font-size:1.05rem;color:var(--text-secondary);line-height:1.7;
margin-bottom:32px;font-style:italic;
}
.hero-actions{display:flex;gap:16px;align-items:center;flex-wrap:wrap}
.btn-primary{
display:inline-flex;align-items:center;gap:10px;
padding:14px 32px;font-size:1rem;font-family:var(--serif);
letter-spacing:2px;color:var(--bg-dark);
background:linear-gradient(135deg,var(--gold-light),var(--gold));
border-radius:var(--radius);border:1px solid var(--gold-light);
box-shadow:0 4px 15px rgba(212,160,23,0.3);
transition:all 0.3s ease;font-weight:bold;
}
.btn-primary:hover{
transform:translateY(-2px);
box-shadow:0 6px 25px rgba(212,160,23,0.5);
background:linear-gradient(135deg,var(--gold),var(--gold-light));
}
.btn-primary svg{width:18px;height:18px}
.btn-ghost{
display:inline-flex;align-items:center;gap:8px;
padding:14px 28px;font-size:0.95rem;font-family:var(--serif);
letter-spacing:1px;color:var(--cream);
border:1px solid var(--border-warm);border-radius:var(--radius);
background:rgba(212,160,23,0.05);
transition:all 0.3s ease;
}
.btn-ghost:hover{border-color:var(--gold);background:rgba(212,160,23,0.12);color:var(--gold)}
.hero-stats{display:flex;gap:32px;margin-top:40px}
.hero-stat{text-align:left}
.hero-stat .num{font-family:var(--serif);font-size:1.8rem;color:var(--gold);font-weight:bold}
.hero-stat .label{font-size:0.78rem;color:var(--text-muted);letter-spacing:1px;text-transform:uppercase}
/* Hero Vinyl Display */
.hero-vinyl-wrap{
position:relative;width:360px;height:360px;flex-shrink:0;
display:flex;align-items:center;justify-content:center;
}
.hero-vinyl{
width:320px;height:320px;border-radius:50%;position:relative;
background:radial-gradient(circle at center,
#c0392b 0%,#c0392b 14%,
#0a0806 14.5%,#1a1410 16%,
#0a0806 17%,#15100a 100%);
box-shadow:0 0 80px rgba(0,0,0,0.8),inset 0 0 40px rgba(0,0,0,0.5),0 0 0 6px rgba(212,160,23,0.08);
animation:spin 6s linear infinite;
}
.hero-vinyl::before{
content:'';position:absolute;inset:17%;border-radius:50%;
background:repeating-radial-gradient(circle at center,
transparent 0,transparent 2px,
rgba(255,255,255,0.025) 2px,rgba(255,255,255,0.025) 3px);
}
.hero-vinyl::after{
content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
width:8px;height:8px;border-radius:50%;background:var(--bg-dark);
box-shadow:0 0 0 2px rgba(212,160,23,0.4);
}
.vinyl-label{
position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
width:26%;height:26%;border-radius:50%;
background:radial-gradient(circle,#c0392b 0%,#8b0000 80%);
display:flex;flex-direction:column;align-items:center;justify-content:center;
z-index:2;color:var(--cream);text-align:center;
}
.vinyl-label .lbl-title{font-family:var(--serif);font-size:0.65rem;font-weight:bold;letter-spacing:1px}
.vinyl-label .lbl-sub{font-family:var(--mono);font-size:0.5rem;color:var(--gold-light);margin-top:2px}
.vinyl-label .lbl-rpm{font-family:var(--mono);font-size:0.45rem;color:rgba(245,230,200,0.6);margin-top:4px}
/* Tonearm */
.tonearm{
position:absolute;top:-5%;right:-8%;width:180px;height:8px;
transform-origin:top right;transform:rotate(32deg);z-index:3;
}
.tonearm-base{
position:absolute;top:-12px;right:-12px;width:28px;height:28px;border-radius:50%;
background:radial-gradient(circle,var(--gold),var(--gold-dim));
box-shadow:0 2px 8px rgba(0,0,0,0.5);
}
.tonearm-stick{
position:absolute;top:10px;right:4px;width:170px;height:3px;
background:linear-gradient(90deg,var(--gold-dim),var(--gold),var(--gold-dim));
border-radius:2px;
}
.tonearm-head{
position:absolute;top:6px;left:2px;width:16px;height:10px;
background:linear-gradient(135deg,#8a7560,#5c4a3a);border-radius:2px;
box-shadow:0 2px 4px rgba(0,0,0,0.4);
}
/* ==================== Section Common ==================== */
.section{padding:40px 0;position:relative}
.section-header{
display:flex;align-items:flex-end;justify-content:space-between;
margin-bottom:28px;gap:16px;
}
.section-title-wrap{display:flex;align-items:center;gap:14px}
.section-icon{
width:42px;height:42px;border-radius:50%;flex-shrink:0;
display:flex;align-items:center;justify-content:center;
background:radial-gradient(circle,rgba(212,160,23,0.15),rgba(212,160,23,0.05));
border:1px solid var(--border-warm);
}
.section-icon svg{width:22px;height:22px;color:var(--gold)}
.section-title{
font-family:var(--serif);font-size:1.6rem;font-weight:bold;
color:var(--cream);letter-spacing:2px;
}
.section-subtitle{font-size:0.8rem;color:var(--text-muted);letter-spacing:2px;text-transform:uppercase;font-family:var(--mono)}
.section-more{font-size:0.85rem;color:var(--text-secondary);transition:color 0.3s}
.section-more:hover{color:var(--gold)}
.section-divider{
height:1px;margin:0 0 28px;
background:linear-gradient(90deg,var(--gold) 0%,transparent 50%);
opacity:0.3;
}
/* ==================== Categories ==================== */
.categories{padding:30px 0 20px}
.category-tabs{
display:flex;gap:10px;flex-wrap:wrap;
}
.category-tab{
display:inline-flex;align-items:center;gap:8px;
padding:8px 20px;font-size:0.92rem;font-family:var(--serif);
letter-spacing:1px;color:var(--text-secondary);
background:linear-gradient(180deg,rgba(51,38,25,0.6),rgba(42,32,24,0.6));
border:1px solid var(--border-dim);border-radius:var(--radius);
transition:all 0.3s ease;cursor:pointer;
position:relative;overflow:hidden;
}
.category-tab .cat-icon{font-size:1rem;color:var(--gold-dim)}
.category-tab:hover{
border-color:var(--border-warm);color:var(--cream);
background:linear-gradient(180deg,rgba(61,46,30,0.8),rgba(51,38,25,0.8));
}
.category-tab.active{
color:var(--bg-dark);background:linear-gradient(135deg,var(--gold-light),var(--gold));
border-color:var(--gold);font-weight:bold;box-shadow:0 2px 12px rgba(212,160,23,0.3);
}
.category-tab.active .cat-icon{color:var(--bg-dark)}
/* ==================== Hot Songs ==================== */
.hot-songs{padding:40px 0}
.chart-list{
display:flex;flex-direction:column;gap:2px;
background:linear-gradient(180deg,rgba(42,32,24,0.5),rgba(26,20,16,0.5));
border:1px solid var(--border-dim);border-radius:var(--radius);
padding:12px;box-shadow:var(--shadow-warm);
}
.chart-item{
display:flex;align-items:center;gap:16px;padding:12px 16px;
border-radius:var(--radius);transition:all 0.3s ease;
cursor:pointer;position:relative;
}
.chart-item:hover{
background:rgba(212,160,23,0.08);
box-shadow:inset 0 0 0 1px var(--border-warm);
}
.chart-rank{
font-family:var(--serif);font-size:1.8rem;font-weight:bold;font-style:italic;
width:42px;text-align:center;flex-shrink:0;color:var(--text-muted);
transition:all 0.3s;
}
.chart-item:nth-child(1) .chart-rank{color:var(--red);font-size:2rem}
.chart-item:nth-child(2) .chart-rank{color:var(--gold);font-size:2rem}
.chart-item:nth-child(3) .chart-rank{color:#cd7f32;font-size:2rem}
.chart-item:hover .chart-rank{color:var(--cream)}
.chart-cover{
width:52px;height:52px;border-radius:var(--radius);flex-shrink:0;
position:relative;overflow:hidden;
box-shadow:0 2px 8px rgba(0,0,0,0.4);
display:flex;align-items:center;justify-content:center;
}
.chart-cover::after{
content:'';position:absolute;inset:0;border-radius:var(--radius);
box-shadow:inset 0 0 0 1px rgba(255,255,255,0.08);
}
.chart-info{flex:1;min-width:0}
.chart-title{
font-family:var(--serif);font-size:1.02rem;color:var(--cream);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
transition:color 0.3s;
}
.chart-item:hover .chart-title{color:var(--gold)}
.chart-artist{
font-size:0.82rem;color:var(--text-muted);margin-top:3px;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.chart-meta{
display:flex;align-items:center;gap:16px;flex-shrink:0;
}
.chart-plays{
font-family:var(--mono);font-size:0.78rem;color:var(--text-muted);
display:flex;align-items:center;gap:6px;
}
.chart-duration{
font-family:var(--mono);font-size:0.78rem;color:var(--text-muted);
min-width:40px;text-align:right;
}
.chart-play-btn{
width:34px;height:34px;border-radius:50%;
display:flex;align-items:center;justify-content:center;
border:1px solid var(--border-warm);
transition:all 0.3s;opacity:0;
}
.chart-item:hover .chart-play-btn{opacity:1}
.chart-play-btn:hover{background:var(--gold);color:var(--bg-dark);border-color:var(--gold)}
.chart-play-btn svg{width:14px;height:14px}
.chart-badge{
font-family:var(--mono);font-size:0.6rem;padding:2px 6px;border-radius:3px;
letter-spacing:1px;
}
.chart-badge.hot{background:rgba(192,57,43,0.2);color:var(--red);border:1px solid rgba(192,57,43,0.3)}
/* ==================== New Songs (Album Sleeves) ==================== */
.new-songs{padding:40px 0}
.album-grid{
display:grid;grid-template-columns:repeat(4,1fr);gap:24px;
}
.album-card{
cursor:pointer;transition:all 0.4s ease;
position:relative;
}
.album-sleeve{
position:relative;aspect-ratio:1;border-radius:var(--radius);
overflow:hidden;box-shadow:var(--shadow-warm);
transition:all 0.4s ease;
}
.album-sleeve::after{
content:'';position:absolute;inset:0;
box-shadow:inset 0 0 0 1px rgba(255,255,255,0.1),inset 0 -40px 40px -20px rgba(0,0,0,0.5);
border-radius:var(--radius);pointer-events:none;
}
.album-cover-bg{
position:absolute;inset:0;
display:flex;align-items:center;justify-content:center;
}
.album-cover-text{
font-family:var(--serif);font-size:0.7rem;color:rgba(245,230,200,0.3);
letter-spacing:2px;text-align:center;padding:12px;
text-transform:uppercase;
}
.album-card:hover .album-sleeve{
transform:translateY(-6px);
box-shadow:0 12px 40px rgba(0,0,0,0.6),0 0 30px rgba(212,160,23,0.2);
}
.new-stamp{
position:absolute;top:10px;right:10px;z-index:3;
font-family:var(--mono);font-size:0.65rem;font-weight:bold;
color:var(--red);letter-spacing:1px;
border:2px solid var(--red);border-radius:3px;
padding:2px 8px;background:rgba(26,20,16,0.7);
transform:rotate(8deg);
text-shadow:0 0 4px rgba(192,57,43,0.3);
}
.album-info{padding:12px 4px 0}
.album-title{
font-family:var(--serif);font-size:0.95rem;color:var(--cream);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
transition:color 0.3s;
}
.album-card:hover .album-title{color:var(--gold)}
.album-artist{
font-size:0.8rem;color:var(--text-muted);margin-top:3px;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.album-date{
font-family:var(--mono);font-size:0.7rem;color:var(--gold-dim);margin-top:4px;
}
/* ==================== Music Library ==================== */
.library{padding:40px 0 60px}
.library-controls{
display:flex;gap:16px;align-items:center;flex-wrap:wrap;
margin-bottom:24px;
}
.library-search{
display:flex;align-items:center;gap:8px;flex:1;min-width:240px;
background:rgba(26,20,16,0.6);
border:1px solid var(--border-warm);border-radius:var(--radius);
padding:8px 14px;transition:all 0.3s;
}
.library-search:focus-within{border-color:var(--gold);box-shadow:var(--glow-gold)}
.library-search svg{width:16px;height:16px;color:var(--text-muted);flex-shrink:0}
.library-search input{
background:none;border:none;outline:none;color:var(--cream);
font-size:0.9rem;width:100%;padding:4px 0;
}
.library-search input::placeholder{color:var(--text-muted);font-style:italic}
.sort-select{
padding:8px 16px;font-size:0.88rem;font-family:var(--serif);
color:var(--cream);background:rgba(26,20,16,0.6);
border:1px solid var(--border-warm);border-radius:var(--radius);
outline:none;cursor:pointer;
appearance:none;
background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23d4a017' stroke-width='2'><polyline points='6 9 12 15 18 9'/></svg>");
background-repeat:no-repeat;background-position:right 10px center;
padding-right:32px;letter-spacing:1px;
}
.sort-select option{background:var(--bg-warm);color:var(--cream)}
.library-count{
font-family:var(--mono);font-size:0.8rem;color:var(--text-muted);
letter-spacing:1px;
}
.library-table{
background:linear-gradient(180deg,rgba(42,32,24,0.4),rgba(26,20,16,0.4));
border:1px solid var(--border-dim);border-radius:var(--radius);
overflow:hidden;box-shadow:var(--shadow-warm);
}
.library-table-head{
display:grid;grid-template-columns:50px 60px 1fr 1fr 100px 80px 50px;
gap:12px;padding:12px 20px;
border-bottom:1px solid var(--border-warm);
font-family:var(--mono);font-size:0.72rem;color:var(--text-muted);
letter-spacing:2px;text-transform:uppercase;
}
.library-row{
display:grid;grid-template-columns:50px 60px 1fr 1fr 100px 80px 50px;
gap:12px;padding:10px 20px;align-items:center;
border-bottom:1px solid rgba(212,160,23,0.06);
transition:all 0.3s;cursor:pointer;
}
.library-row:last-child{border-bottom:none}
.library-row:hover{
background:rgba(212,160,23,0.06);
box-shadow:inset 3px 0 0 var(--gold);
}
.lib-rank{font-family:var(--mono);font-size:0.85rem;color:var(--text-muted);text-align:center}
.lib-cover{
width:44px;height:44px;border-radius:4px;overflow:hidden;
position:relative;box-shadow:0 2px 6px rgba(0,0,0,0.3);
}
.lib-cover::after{
content:'';position:absolute;inset:0;
box-shadow:inset 0 0 0 1px rgba(255,255,255,0.08);
border-radius:4px;
}
.lib-title{font-size:0.92rem;color:var(--cream);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.lib-title .lib-tag{
display:inline-block;font-family:var(--mono);font-size:0.55rem;
padding:1px 5px;border-radius:2px;margin-left:6px;vertical-align:middle;
letter-spacing:1px;
}
.lib-tag.hot{background:rgba(192,57,43,0.2);color:var(--red);border:1px solid rgba(192,57,43,0.3)}
.lib-tag.new{background:rgba(212,160,23,0.15);color:var(--gold);border:1px solid rgba(212,160,23,0.3)}
.lib-artist{font-size:0.82rem;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.lib-plays{font-family:var(--mono);font-size:0.78rem;color:var(--text-secondary)}
.lib-duration{font-family:var(--mono);font-size:0.78rem;color:var(--text-muted);text-align:right}
.lib-action{
width:30px;height:30px;border-radius:50%;
display:flex;align-items:center;justify-content:center;
border:1px solid var(--border-dim);transition:all 0.3s;
}
.library-row:hover .lib-action{border-color:var(--gold);color:var(--gold)}
.lib-action svg{width:14px;height:14px}
/* Pagination */
.pagination{
display:flex;gap:6px;justify-content:center;align-items:center;
margin-top:28px;flex-wrap:wrap;
}
.page-btn{
min-width:38px;height:38px;padding:0 10px;
display:flex;align-items:center;justify-content:center;
font-family:var(--serif);font-size:0.92rem;color:var(--text-secondary);
border:1px solid var(--border-dim);border-radius:var(--radius);
background:rgba(42,32,24,0.4);transition:all 0.3s;cursor:pointer;
}
.page-btn:hover:not(:disabled){border-color:var(--border-warm);color:var(--cream);background:rgba(212,160,23,0.08)}
.page-btn.active{
background:linear-gradient(135deg,var(--gold-light),var(--gold));
color:var(--bg-dark);border-color:var(--gold);font-weight:bold;
}
.page-btn:disabled{opacity:0.3;cursor:not-allowed}
.page-btn svg{width:14px;height:14px}
.page-ellipsis{color:var(--text-muted);padding:0 4px}
/* ==================== Skeleton Loading ==================== */
.skeleton{
background:linear-gradient(90deg,
rgba(42,32,24,0.5) 25%,
rgba(212,160,23,0.08) 50%,
rgba(42,32,24,0.5) 75%);
background-size:200% 100%;
animation:shimmer 1.5s infinite;
border-radius:4px;
}
@keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
.skel-chart-item{display:flex;align-items:center;gap:16px;padding:12px 16px}
.skel-chart-rank{width:42px;height:30px}
.skel-chart-cover{width:52px;height:52px;border-radius:6px}
.skel-chart-info{flex:1}
.skel-line{height:12px;margin-bottom:6px}
.skel-line.short{width:60%}
.skel-album-card{}
.skel-album-cover{aspect-ratio:1;border-radius:6px}
.skel-album-title{height:14px;margin-top:12px;width:80%}
.skel-album-artist{height:10px;margin-top:6px;width:50%}
.skel-lib-row{display:grid;grid-template-columns:50px 60px 1fr 1fr 100px 80px 50px;gap:12px;padding:10px 20px;align-items:center}
.skel-lib-cover{width:44px;height:44px;border-radius:4px}
/* Empty state */
.empty-state{
text-align:center;padding:60px 20px;color:var(--text-muted);
}
.empty-state .empty-icon{font-size:3rem;margin-bottom:16px;opacity:0.3}
.empty-state .empty-text{font-size:1rem;font-style:italic}
/* ==================== Now Playing Panel ==================== */
.now-playing{
position:fixed;bottom:0;left:0;right:0;z-index:200;
background:linear-gradient(180deg,rgba(26,20,16,0.98),rgba(42,32,24,0.98));
border-top:1px solid var(--border-warm);
box-shadow:0 -8px 40px rgba(0,0,0,0.6);
transform:translateY(100%);transition:transform 0.5s cubic-bezier(0.4,0,0.2,1);
}
.now-playing.active{transform:translateY(0)}
.now-playing.expanded{height:80vh}
.np-bar{
display:flex;align-items:center;gap:20px;padding:14px 24px;
transition:all 0.3s;
}
.now-playing.expanded .np-bar{display:none}
.np-vinyl-mini{
width:56px;height:56px;border-radius:50%;flex-shrink:0;position:relative;
background:radial-gradient(circle,#c0392b 0%,#c0392b 20%,#0a0806 21%,#15100a 100%);
animation:spin 4s linear infinite;
animation-play-state:paused;
box-shadow:0 2px 10px rgba(0,0,0,0.5);
}
.now-playing.playing .np-vinyl-mini{animation-play-state:running}
.np-vinyl-mini::before{
content:'';position:absolute;inset:20%;border-radius:50%;
background:repeating-radial-gradient(circle,transparent 0,transparent 2px,rgba(255,255,255,0.03) 2px,rgba(255,255,255,0.03) 3px);
}
.np-vinyl-mini::after{
content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
width:5px;height:5px;border-radius:50%;background:var(--gold);
}
.np-info-mini{flex:1;min-width:0}
.np-title-mini{
font-family:var(--serif);font-size:1rem;color:var(--cream);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.np-artist-mini{font-size:0.8rem;color:var(--text-muted);margin-top:2px}
.np-progress-mini{
display:flex;align-items:center;gap:10px;flex-shrink:0;
}
.np-time{font-family:var(--mono);font-size:0.72rem;color:var(--text-muted);min-width:36px}
.np-progress-bar{
width:200px;height:4px;border-radius:2px;
background:rgba(212,160,23,0.15);position:relative;cursor:pointer;
overflow:hidden;
}
.np-progress-fill{
height:100%;border-radius:2px;
background:linear-gradient(90deg,var(--gold-dim),var(--gold));
width:0%;transition:width 0.3s linear;
position:relative;
}
.np-progress-fill::after{
content:'';position:absolute;right:-4px;top:50%;transform:translateY(-50%);
width:10px;height:10px;border-radius:50%;background:var(--gold);
box-shadow:0 0 8px rgba(212,160,23,0.5);
}
.np-controls-mini{display:flex;align-items:center;gap:8px;flex-shrink:0}
.np-btn{
width:38px;height:38px;border-radius:50%;
display:flex;align-items:center;justify-content:center;
border:1px solid var(--border-dim);transition:all 0.3s;color:var(--cream);
}
.np-btn:hover{border-color:var(--gold);color:var(--gold);background:rgba(212,160,23,0.08)}
.np-btn svg{width:16px;height:16px}
.np-btn.play{
width:46px;height:46px;
background:linear-gradient(135deg,var(--gold-light),var(--gold));
color:var(--bg-dark);border-color:var(--gold);
}
.np-btn.play:hover{background:linear-gradient(135deg,var(--gold),var(--gold-light))}
.np-btn.play svg{width:20px;height:20px}
.np-expand-btn{
width:38px;height:38px;border-radius:50%;
display:flex;align-items:center;justify-content:center;
border:1px solid var(--border-dim);transition:all 0.3s;color:var(--text-secondary);
}
.np-expand-btn:hover{border-color:var(--gold);color:var(--gold)}
.np-expand-btn svg{width:18px;height:18px;transition:transform 0.3s}
.now-playing.expanded .np-expand-btn svg{transform:rotate(180deg)}
/* Expanded panel */
.np-expanded{
display:none;height:100%;flex-direction:row;gap:0;
}
.now-playing.expanded .np-expanded{display:flex}
.np-left{
flex:0 0 320px;display:flex;align-items:center;justify-content:center;
padding:30px;position:relative;
background:radial-gradient(circle at center,rgba(212,160,23,0.05),transparent);
}
.np-vinyl-large{
width:240px;height:240px;border-radius:50%;position:relative;
background:radial-gradient(circle at center,
#c0392b 0%,#c0392b 14%,
#0a0806 14.5%,#1a1410 16%,
#0a0806 17%,#15100a 100%);
box-shadow:0 0 60px rgba(0,0,0,0.8),inset 0 0 30px rgba(0,0,0,0.5),0 0 0 4px rgba(212,160,23,0.08);
animation:spin 5s linear infinite;
animation-play-state:paused;
}
.now-playing.playing .np-vinyl-large{animation-play-state:running}
.np-vinyl-large::before{
content:'';position:absolute;inset:17%;border-radius:50%;
background:repeating-radial-gradient(circle,transparent 0,transparent 2px,rgba(255,255,255,0.025) 2px,rgba(255,255,255,0.025) 3px);
}
.np-vinyl-large::after{
content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
width:7px;height:7px;border-radius:50%;background:var(--bg-dark);
box-shadow:0 0 0 2px rgba(212,160,23,0.4);
}
.np-vinyl-large .vinyl-label{
position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
width:26%;height:26%;border-radius:50%;
background:radial-gradient(circle,#c0392b,#8b0000);
display:flex;flex-direction:column;align-items:center;justify-content:center;
color:var(--cream);text-align:center;z-index:2;
}
.np-vinyl-large .vinyl-label .lbl-title{font-family:var(--serif);font-size:0.6rem;font-weight:bold;letter-spacing:1px;max-width:90%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.np-vinyl-large .vinyl-label .lbl-sub{font-family:var(--mono);font-size:0.45rem;color:var(--gold-light);margin-top:2px}
.np-right{
flex:1;display:flex;flex-direction:column;padding:30px 30px 20px;overflow:hidden;
}
.np-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:16px}
.np-song-title{
font-family:var(--serif);font-size:1.6rem;color:var(--cream);font-weight:bold;
letter-spacing:1px;
}
.np-song-meta{font-size:0.85rem;color:var(--text-muted);margin-top:4px}
.np-song-meta .sep{margin:0 8px;color:var(--gold-dim)}
.np-tags{display:flex;gap:6px;flex-wrap:wrap;margin-top:8px}
.np-tag{
font-family:var(--mono);font-size:0.65rem;padding:2px 8px;border-radius:3px;
background:rgba(212,160,23,0.1);color:var(--gold);border:1px solid var(--border-dim);
letter-spacing:1px;
}
.np-lyrics-wrap{
flex:1;overflow-y:auto;margin:16px 0;
padding:16px 20px;
background:rgba(26,20,16,0.4);border-radius:var(--radius);
border:1px solid var(--border-dim);
}
.np-lyrics-title{
font-family:var(--mono);font-size:0.7rem;color:var(--gold-dim);
letter-spacing:2px;text-transform:uppercase;margin-bottom:12px;
display:flex;align-items:center;gap:8px;
}
.np-lyrics-title::before{content:'';width:3px;height:12px;background:var(--gold);border-radius:1px}
.np-lyrics{
font-family:var(--serif);font-size:0.95rem;color:var(--text-secondary);
line-height:2;white-space:pre-wrap;font-style:italic;
}
.np-lyrics.empty{color:var(--text-muted);font-style:italic;opacity:0.5}
.np-footer{display:flex;align-items:center;gap:16px}
.np-progress-large{flex:1;display:flex;align-items:center;gap:12px}
.np-progress-large .np-progress-bar{width:100%;height:5px}
.np-controls-large{display:flex;align-items:center;gap:10px}
/* ==================== Footer ==================== */
.footer{
border-top:1px solid var(--border-warm);
background:linear-gradient(180deg,rgba(26,20,16,0.6),rgba(10,8,6,0.8));
padding:50px 0 24px;margin-top:40px;position:relative;
}
.footer::before{
content:'';position:absolute;top:-1px;left:0;right:0;height:1px;
background:linear-gradient(90deg,transparent,var(--gold),transparent);
opacity:0.4;
}
.footer-inner{display:grid;grid-template-columns:2fr 1fr 1fr 1fr;gap:40px;margin-bottom:32px}
.footer-brand .logo-text{font-size:1.2rem}
.footer-desc{font-size:0.82rem;color:var(--text-muted);line-height:1.7;margin-top:12px;font-style:italic;max-width:300px}
.footer-col h4{font-family:var(--serif);font-size:0.95rem;color:var(--gold);margin-bottom:12px;letter-spacing:1px}
.footer-col ul li{margin-bottom:8px}
.footer-col a{font-size:0.82rem;color:var(--text-secondary);transition:color 0.3s}
.footer-col a:hover{color:var(--gold)}
.footer-bottom{
border-top:1px solid var(--border-dim);padding-top:20px;
display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px;
}
.footer-copy{font-family:var(--mono);font-size:0.72rem;color:var(--text-muted);letter-spacing:1px}
.footer-decor{font-size:1.2rem;color:var(--gold-dim);opacity:0.4;letter-spacing:4px}
/* ==================== Animations ==================== */
@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
@keyframes eqBounce{0%,100%{transform:scaleY(0.4)}50%{transform:scaleY(1)}}
@keyframes blink{0%,100%{opacity:1}50%{opacity:0.3}}
@keyframes fadeInUp{from{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}
/* Scroll reveal */
.reveal{opacity:0;transform:translateY(30px);transition:opacity 0.7s ease,transform 0.7s ease}
.reveal.visible{opacity:1;transform:translateY(0)}
/* Toast */
.toast{
position:fixed;top:80px;right:24px;z-index:300;
padding:12px 20px;border-radius:var(--radius);
background:linear-gradient(135deg,rgba(42,32,24,0.98),rgba(51,38,25,0.98));
border:1px solid var(--gold);color:var(--cream);
font-size:0.88rem;box-shadow:var(--shadow-deep),var(--glow-gold);
transform:translateX(120%);transition:transform 0.4s ease;
display:flex;align-items:center;gap:10px;
}
.toast.show{transform:translateX(0)}
.toast .toast-icon{color:var(--gold)}
/* ==================== Responsive ==================== */
@media(max-width:980px){
.hero-inner{flex-direction:column-reverse;text-align:center}
.hero-content{max-width:100%}
.hero-stats{justify-content:center}
.hero-actions{justify-content:center}
.hero-vinyl-wrap{width:280px;height:280px}
.hero-vinyl{width:240px;height:240px}
.album-grid{grid-template-columns:repeat(2,1fr)}
.footer-inner{grid-template-columns:1fr 1fr}
.library-table-head,.library-row{grid-template-columns:40px 50px 1fr 80px 40px}
.lib-artist,.lib-plays{display:none}
.np-progress-bar{width:120px}
}
@media(max-width:680px){
.header-inner{height:auto;flex-wrap:wrap;padding:10px 0;gap:10px}
.nav{order:3;width:100%;justify-content:center;flex-wrap:wrap}
.search-bar{min-width:160px;flex:1}
.header-fm{display:none}
.hero-title{font-size:2rem}
.hero-vinyl-wrap{width:220px;height:220px}
.hero-vinyl{width:200px;height:200px}
.album-grid{grid-template-columns:1fr 1fr}
.footer-inner{grid-template-columns:1fr}
.chart-meta{flex-direction:column;gap:6px}
.library-table-head{display:none}
.library-row{grid-template-columns:40px 1fr 40px;gap:8px;padding:8px 12px}
.lib-cover,.lib-rank,.lib-title,.lib-action{display:flex}
.lib-artist,.lib-plays,.lib-duration{display:none}
.lib-title{flex-direction:column;align-items:flex-start;gap:2px}
.np-bar{flex-wrap:wrap;gap:10px;padding:10px 16px}
.np-progress-bar{width:100%;order:5}
.np-progress-mini{width:100%;order:5}
.np-expanded{flex-direction:column}
.np-left{flex:0 0 auto}
.np-vinyl-large{width:160px;height:160px}
.now-playing.expanded{height:90vh}
.tonearm{display:none}
}
</style>
</head>
<body>
<!-- ==================== Header ==================== -->
<header class="header">
<div class="container header-inner">
<div class="logo">
<div class="logo-icon"></div>
<div>
<div class="logo-text">黑胶电台</div>
<div class="logo-sub">VINYL RADIO</div>
</div>
</div>
<nav class="nav">
<a class="nav-link active" data-scroll="hero-section">首页</a>
<a class="nav-link" data-scroll="library-section">唱片架</a>
<a class="nav-link" data-scroll="hot-section">排行榜</a>
<a class="nav-link" data-scroll="new-section">新碟</a>
</nav>
<div class="search-bar">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="text" id="header-search" placeholder="搜索歌曲、歌手...">
</div>
<div class="header-fm">
<span>FM 88.8</span>
<div class="eq"><span></span><span></span><span></span><span></span><span></span></div>
</div>
</div>
</header>
<!-- ==================== Hero ==================== -->
<section class="hero" id="hero-section">
<div class="hero-bg-notes">
<span>&#9835;</span><span>&#9834;</span><span>&#9833;</span><span>&#9836;</span>
</div>
<div class="container hero-inner">
<div class="hero-content">
<div class="hero-tag">经典之声 · 原音重现</div>
<h1 class="hero-title">聆听<span class="accent">经典</span>之声<br>重温黑胶岁月</h1>
<p class="hero-subtitle">在数字时代里,保留一份模拟的温暖。<br>每一张唱片,都是一段值得珍藏的时光记忆。</p>
<div class="hero-actions">
<button class="btn-primary" id="hero-play-btn">
<svg viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
开始播放
</button>
<button class="btn-ghost" data-scroll="library-section">
浏览曲库
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
</button>
</div>
<div class="hero-stats" id="hero-stats">
<div class="hero-stat"><div class="num">—</div><div class="label">唱片收藏</div></div>
<div class="hero-stat"><div class="num">—</div><div class="label">艺术家</div></div>
<div class="hero-stat"><div class="num">—</div><div class="label">总播放量</div></div>
</div>
</div>
<div class="hero-vinyl-wrap">
<div class="tonearm">
<div class="tonearm-base"></div>
<div class="tonearm-stick"></div>
<div class="tonearm-head"></div>
</div>
<div class="hero-vinyl">
<div class="vinyl-label">
<div class="lbl-title">黑胶电台</div>
<div class="lbl-sub">SIDE A</div>
<div class="lbl-rpm">33⅓ RPM</div>
</div>
</div>
</div>
</div>
</section>
<!-- ==================== Categories ==================== -->
<section class="categories" id="category-section">
<div class="container">
<div class="section-header reveal">
<div class="section-title-wrap">
<div class="section-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
</div>
<div>
<div class="section-title">音乐分类</div>
<div class="section-subtitle">GENRES</div>
</div>
</div>
</div>
<div class="category-tabs" id="category-tabs">
<div class="skeleton" style="width:80px;height:36px;border-radius:6px"></div>
<div class="skeleton" style="width:80px;height:36px;border-radius:6px"></div>
<div class="skeleton" style="width:80px;height:36px;border-radius:6px"></div>
<div class="skeleton" style="width:80px;height:36px;border-radius:6px"></div>
</div>
</div>
</section>
<!-- ==================== Hot Songs ==================== -->
<section class="hot-songs" id="hot-section">
<div class="container">
<div class="section-header reveal">
<div class="section-title-wrap">
<div class="section-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></svg>
</div>
<div>
<div class="section-title">热门金曲</div>
<div class="section-subtitle">HOT CHART</div>
</div>
</div>
<a class="section-more" data-scroll="library-section">查看全部 →</a>
</div>
<div class="section-divider"></div>
<div class="chart-list" id="hot-list">
<!-- skeleton items -->
<div class="skel-chart-item"><div class="skeleton skel-chart-rank"></div><div class="skeleton skel-chart-cover"></div><div class="skel-chart-info"><div class="skeleton skel-line"></div><div class="skeleton skel-line short"></div></div></div>
<div class="skel-chart-item"><div class="skeleton skel-chart-rank"></div><div class="skeleton skel-chart-cover"></div><div class="skel-chart-info"><div class="skeleton skel-line"></div><div class="skeleton skel-line short"></div></div></div>
<div class="skel-chart-item"><div class="skeleton skel-chart-rank"></div><div class="skeleton skel-chart-cover"></div><div class="skel-chart-info"><div class="skeleton skel-line"></div><div class="skeleton skel-line short"></div></div></div>
</div>
</div>
</section>
<!-- ==================== New Songs ==================== -->
<section class="new-songs" id="new-section">
<div class="container">
<div class="section-header reveal">
<div class="section-title-wrap">
<div class="section-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
</div>
<div>
<div class="section-title">新碟速递</div>
<div class="section-subtitle">NEW RELEASES</div>
</div>
</div>
<a class="section-more" data-scroll="library-section">查看全部 →</a>
</div>
<div class="section-divider"></div>
<div class="album-grid" id="new-grid">
<div class="skel-album-card"><div class="skeleton skel-album-cover"></div><div class="skeleton skel-album-title"></div><div class="skeleton skel-album-artist"></div></div>
<div class="skel-album-card"><div class="skeleton skel-album-cover"></div><div class="skeleton skel-album-title"></div><div class="skeleton skel-album-artist"></div></div>
<div class="skel-album-card"><div class="skeleton skel-album-cover"></div><div class="skeleton skel-album-title"></div><div class="skeleton skel-album-artist"></div></div>
<div class="skel-album-card"><div class="skeleton skel-album-cover"></div><div class="skeleton skel-album-title"></div><div class="skeleton skel-album-artist"></div></div>
</div>
</div>
</section>
<!-- ==================== Music Library ==================== -->
<section class="library" id="library-section">
<div class="container">
<div class="section-header reveal">
<div class="section-title-wrap">
<div class="section-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
</div>
<div>
<div class="section-title">音乐图书馆</div>
<div class="section-subtitle">MUSIC LIBRARY</div>
</div>
</div>
</div>
<div class="section-divider"></div>
<div class="library-controls reveal">
<div class="library-search">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="text" id="library-search" placeholder="搜索歌曲、歌手、专辑...">
</div>
<select class="sort-select" id="sort-select">
<option value="">默认排序</option>
<option value="plays">播放量最高</option>
<option value="likes">收藏最多</option>
<option value="newest">最新发布</option>
</select>
<div class="library-count" id="library-count">— 首歌曲</div>
</div>
<div class="library-table" id="library-table">
<div class="library-table-head">
<div>#</div><div></div><div>歌曲</div><div>艺术家</div><div>播放量</div><div style="text-align:right">时长</div><div></div>
</div>
<div id="library-rows">
<div class="skel-lib-row"><div class="skeleton" style="width:20px;height:14px"></div><div class="skeleton skel-lib-cover"></div><div class="skeleton skel-line"></div><div class="skeleton skel-line short"></div><div class="skeleton" style="width:60px;height:12px"></div><div class="skeleton" style="width:30px;height:12px"></div><div></div></div>
<div class="skel-lib-row"><div class="skeleton" style="width:20px;height:14px"></div><div class="skeleton skel-lib-cover"></div><div class="skeleton skel-line"></div><div class="skeleton skel-line short"></div><div class="skeleton" style="width:60px;height:12px"></div><div class="skeleton" style="width:30px;height:12px"></div><div></div></div>
<div class="skel-lib-row"><div class="skeleton" style="width:20px;height:14px"></div><div class="skeleton skel-lib-cover"></div><div class="skeleton skel-line"></div><div class="skeleton skel-line short"></div><div class="skeleton" style="width:60px;height:12px"></div><div class="skeleton" style="width:30px;height:12px"></div><div></div></div>
</div>
</div>
<div class="pagination" id="pagination"></div>
</div>
</section>
<!-- ==================== Footer ==================== -->
<footer class="footer">
<div class="container">
<div class="footer-inner">
<div class="footer-brand">
<div class="logo">
<div class="logo-icon"></div>
<div class="logo-text">黑胶电台</div>
</div>
<p class="footer-desc">在数字洪流中,留存一份模拟的温度。每一道纹路,都是时光的低语。</p>
</div>
<div class="footer-col">
<h4>探索</h4>
<ul>
<li><a data-scroll="hot-section">热门金曲</a></li>
<li><a data-scroll="new-section">新碟速递</a></li>
<li><a data-scroll="library-section">音乐图书馆</a></li>
<li><a data-scroll="category-section">分类浏览</a></li>
</ul>
</div>
<div class="footer-col">
<h4>关于</h4>
<ul>
<li><a>关于我们</a></li>
<li><a>使用条款</a></li>
<li><a>隐私政策</a></li>
<li><a>版权声明</a></li>
</ul>
</div>
<div class="footer-col">
<h4>连接</h4>
<ul>
<li><a>意见反馈</a></li>
<li><a>商务合作</a></li>
<li><a>加入我们</a></li>
<li><a>开发者API</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<div class="footer-copy">&copy; 2026 VINYL RADIO · 黑胶电台 · 保留所有权利</div>
<div class="footer-decor">&#9835; &#9834; &#9833; &#9836;</div>
</div>
</div>
</footer>
<!-- ==================== Now Playing Panel ==================== -->
<div class="now-playing" id="now-playing">
<!-- Compact bar -->
<div class="np-bar">
<div class="np-vinyl-mini"></div>
<div class="np-info-mini">
<div class="np-title-mini" id="np-title">—</div>
<div class="np-artist-mini" id="np-artist">—</div>
</div>
<div class="np-progress-mini">
<span class="np-time" id="np-current">0:00</span>
<div class="np-progress-bar" id="np-progress-bar">
<div class="np-progress-fill" id="np-progress-fill"></div>
</div>
<span class="np-time" id="np-duration">0:00</span>
</div>
<div class="np-controls-mini">
<button class="np-btn play" id="np-play-btn" title="播放/暂停">
<svg id="np-play-icon" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
</button>
<button class="np-expand-btn" id="np-expand-btn" title="展开">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</button>
</div>
</div>
<!-- Expanded panel -->
<div class="np-expanded">
<div class="np-left">
<div class="np-vinyl-large" id="np-vinyl-large">
<div class="vinyl-label">
<div class="lbl-title" id="np-vinyl-label-title">—</div>
<div class="lbl-sub" id="np-vinyl-label-sub">SIDE A</div>
</div>
</div>
</div>
<div class="np-right">
<div class="np-header">
<div>
<div class="np-song-title" id="np-title-large">—</div>
<div class="np-song-meta">
<span id="np-artist-large">—</span>
<span class="sep">|</span>
<span id="np-album-large">—</span>
<span class="sep">|</span>
<span id="np-date-large">—</span>
</div>
<div class="np-tags" id="np-tags"></div>
</div>
<button class="np-expand-btn" id="np-collapse-btn" title="收起">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 15 12 9 6 15"/></svg>
</button>
</div>
<div class="np-lyrics-wrap">
<div class="np-lyrics-title">歌词</div>
<div class="np-lyrics" id="np-lyrics">暂无歌词</div>
</div>
<div class="np-footer">
<div class="np-progress-large">
<span class="np-time" id="np-current-large">0:00</span>
<div class="np-progress-bar" id="np-progress-bar-large">
<div class="np-progress-fill" id="np-progress-fill-large"></div>
</div>
<span class="np-time" id="np-duration-large">0:00</span>
</div>
<div class="np-controls-large">
<button class="np-btn play" id="np-play-btn-large" title="播放/暂停">
<svg id="np-play-icon-large" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toast">
<span class="toast-icon">&#9835;</span>
<span id="toast-text">提示</span>
</div>
<script>
/* ============================================================
* 黑胶电台 - 复古音乐平台
* 单文件HTML模板
* ============================================================ */
/* ==================== Mock Data (fallback when API unavailable) ==================== */
const MOCK_CATEGORIES = [
{id:1, name:'流行', icon:'\u266A', sort:1},
{id:2, name:'摇滚', icon:'\u266B', sort:2},
{id:3, name:'爵士', icon:'\u266C', sort:3},
{id:4, name:'古典', icon:'\u2669', sort:4},
{id:5, name:'民谣', icon:'\u266A', sort:5},
{id:6, name:'电子', icon:'\u266B', sort:6},
{id:7, name:'R&B', icon:'\u266C', sort:7},
{id:8, name:'嘻哈', icon:'\u2669', sort:8},
];
const SONG_DATA = [
{title:'夜的钢琴曲五', artist:'石进', cat:'古典', lyrics:'夜色温柔地降临\n月光洒在黑白键上\n每一个音符\n都是心底最深的诉说\n\n钢琴声在寂静中回响\n像是一场无声的雨\n落在记忆的湖面\n泛起层层涟漪'},
{title:'夏日最后的玫瑰', artist:'陈绮贞', cat:'民谣', lyrics:'夏日最后的玫瑰\n在风中轻轻摇曳\n你说要记住这一刻\n因为秋天就要来了\n\n蝉鸣声渐渐远去\n夕阳染红了天边\n我们坐在屋顶\n看星星一颗颗亮起'},
{title:'时光旅行者', artist:'李志', cat:'摇滚', lyrics:'如果我能回到过去\n我会对那个少年说什么\n时光是一条单行道\n我们都是旅行者\n\n穿过城市的霓虹\n穿过雨夜的街头\n时光旅行者\n永远在路上'},
{title:'雨中漫步', artist:'张悬', cat:'民谣', lyrics:'下雨了\n我撑起伞走在街上\n雨水打湿了鞋尖\n却淋不湿心情\n\n每一滴雨都是一个故事\n落在屋檐上\n落在花瓣上\n落在我心里'},
{title:'星空下的誓言', artist:'宋冬野', cat:'民谣', lyrics:'那年夏天\n我们躺在草地上\n数着天上的星星\n许下了一生的诺言\n\n星空下\n你说永远不会离开\n星空下\n我信了'},
{title:'旧时光', artist:'赵雷', cat:'民谣', lyrics:'老街口的梧桐树\n又到了落叶的季节\n那些旧时光\n像泛黄的照片\n\n骑着单车的少年\n已经长大成人\n旧时光啊\n你慢些走'},
{title:'蓝色的梦', artist:'陈粒', cat:'流行', lyrics:'我做了一个蓝色的梦\n梦里有一片海\n海风吹过脸庞\n带着咸咸的味道\n\n蓝色的梦里\n没有忧愁\n蓝色的梦里\n只有自由'},
{title:'咖啡馆的午后', artist:'花粥', cat:'民谣', lyrics:'午后的阳光\n透过玻璃窗\n洒在木质桌面上\n咖啡冒着热气\n\n书翻到第三页\n猫在脚边睡着了\n时间在这里\n走得很慢很慢'},
{title:'秋日私语', artist:'理查德·克莱德曼', cat:'古典', lyrics:'秋天的风\n轻轻吹过树林\n金色的叶子\n在空中旋转\n\n这是一首秋日私语\n诉说着季节的故事\n诉说着时光的温柔'},
{title:'月光奏鸣曲', artist:'贝多芬', cat:'古典', lyrics:'月光如水\n倾泻在琴键上\n升c小调的忧郁\n在夜色中流淌\n\n第一乐章的沉思\n第二乐章的温柔\n第三乐章的激昂\n月光下的奏鸣'},
{title:'城市夜归人', artist:'海龟先生', cat:'摇滚', lyrics:'末班地铁\n载着疲惫的灵魂\n城市的灯火\n在窗外闪烁\n\n夜归人啊\n你在为谁奔波\n夜归人啊\n你的家在哪里'},
{title:'风中的承诺', artist:'草东没有派对', cat:'摇滚', lyrics:'风吹过荒野\n吹散了我们的承诺\n你说等风停了\n就回来找我\n\n风一直没有停\n你一直没有回来\n我在风中站了很久\n最后也成了风'},
{title:'记忆的碎片', artist:'万能青年旅店', cat:'摇滚', lyrics:'记忆是一面碎了的镜子\n每一片都映着不同的画面\n有的在笑\n有的在哭\n\n我捡起一片又一片\n拼不出完整的从前\n记忆的碎片\n割伤了指尖'},
{title:'花房姑娘', artist:'崔健', cat:'摇滚', lyrics:'你带我走进你的花房\n那里有各种奇异的花\n你说你最热爱这些花\n虽然你不能拥有它\n\n我就要回到老地方\n我就要走在老路上'},
{title:'远方的呼唤', artist:'逃跑计划', cat:'摇滚', lyrics:'远方在呼唤我\n像母亲在唤归家的孩子\n背上行囊\n踏上未知的旅途\n\n远方的山\n远方的水\n远方的人\n都在等我'},
{title:'午夜电台', artist:'GALA', cat:'摇滚', lyrics:'午夜的电台\n播放着老歌\n主持人的声音\n低沉而温暖\n\n一个人戴着耳机\n躺在床上\n听别人的故事\n想自己的心事'},
{title:'街角的唱片店', artist:'马頔', cat:'民谣', lyrics:'街角有一家唱片店\n门口挂着风铃\n推门进去\n满是黑胶的味道\n\n老板戴着老花镜\n在唱机旁打盹\n一只猫趴在唱片堆上\n看着来来往往的人'},
{title:'老唱片的回忆', artist:'陈鸿宇', cat:'民谣', lyrics:'翻开积灰的唱片柜\n抽出一张老唱片\n针尖落下的瞬间\n时光倒流\n\n那是父亲年轻时\n最爱的旋律\n如今我也到了\n那个年纪'},
{title:'黑胶岁月', artist:'刘昊霖', cat:'流行', lyrics:'转盘在旋转\n唱针在滑动\n黑胶的纹路里\n藏着旧日时光\n\n没有跳曲\n没有快进\n只有慢慢地\n听完一面再翻过来'},
{title:'留声机的呢喃', artist:'谢春花', cat:'民谣', lyrics:'角落里的留声机\n蒙上了一层灰\n摇动把手\n它又唱了起来\n\n那沙沙的声音\n像是在呢喃\n讲述着一个\n很久以前的故事'},
{title:'黄昏的列车', artist:'程璧', cat:'民谣', lyrics:'黄昏的列车\n缓缓驶出站台\n窗外的风景\n一幕幕后退\n\n不知道终点在哪里\n只想一直坐下去\n看夕阳沉入山后\n看星星升起来'},
{title:'雾中的灯塔', artist:'好妹妹乐队', cat:'民谣', lyrics:'海雾弥漫\n看不见远方的船\n只有灯塔的光\n一闪一闪\n\n它守着这片海\n守了一百年\n无论风雨\n从未熄灭'},
{title:'海边的吉他', artist:'痛仰', cat:'摇滚', lyrics:'坐在海边的礁石上\n弹着一把旧吉他\n海浪打着节拍\n风在和声\n\n没有听众\n也不需要听众\n大海是最好的观众\n它从不吝啬掌声'},
{title:'落叶的季节', artist:'新裤子', cat:'摇滚', lyrics:'又到了落叶的季节\n满地金黄\n踩上去沙沙作响\n像是在鼓掌\n\n秋天是收获的季节\n也是告别的季节\n叶子落了\n明年还会长出来'},
{title:'冬日的暖阳', artist:'赵雷', cat:'民谣', lyrics:'冬日的暖阳\n照在老墙上\n一只猫趴在墙头\n眯着眼睛\n\n世界很安静\n时间很慢\n这一刻\n什么都可以不想'},
{title:'春日序曲', artist:'久石让', cat:'古典', lyrics:'冰雪消融\n溪水潺潺\n第一朵花开了\n春天来了\n\n鸟儿在枝头歌唱\n孩子在草地上奔跑\n万物复苏\n生机盎然'},
{title:'夏日终曲', artist:'坂本龙一', cat:'古典', lyrics:'夏天要走了\n蝉鸣声渐弱\n最后一缕热风\n带走了喧嚣\n\n静静地\n听一首夏日的终曲\n然后\n迎接秋天'},
{title:'秋之回忆', artist:'肖邦', cat:'古典', lyrics:'秋天的回忆\n像一片落叶\n旋转着飘落\n轻轻地触碰大地\n\n每一条脉络\n都是一段故事\n黄色是温暖\n红色是热烈'},
{title:'冬眠', artist:'陈粒', cat:'流行', lyrics:'冬天来了\n我想冬眠\n像一只熊\n躲进树洞\n\n不做梦\n不醒来\n等到春天来了\n再伸出头看看'},
{title:'永恒的旋律', artist:'莫扎特', cat:'古典', lyrics:'有些旋律\n穿越了几个世纪\n依然动人\n\n它们不属于某个时代\n而属于永恒\n在每一个需要安慰的夜晚\n轻轻响起'},
{title:'霓虹深处', artist:'草东没有派对', cat:'摇滚', lyrics:'城市的霓虹灯\n闪烁不停\n深处有一条巷子\n没有灯\n\n那里有一家酒吧\n卖便宜的酒\n放老旧的歌\n来的人都不说话'},
{title:'潮汐', artist:'坂本龙一', cat:'古典', lyrics:'潮起潮落\n日复一日\n月亮牵着大海的手\n跳一支永恒的舞\n\n涨潮时\n浪花亲吻沙滩\n退潮时\n留下贝壳和回忆'},
];
const TAG_POOL = ['经典','治愈','怀旧','温暖','安静','深夜','公路','雨天','咖啡','阅读','纯音乐','现场','循环','安眠'];
const ALBUMS = ['时光机','黑胶记忆','留声岁月','深夜电台','城市日记','四季歌','旧曲新编','原音重现','无声电影','街角往事'];
function generateMockSongs() {
return SONG_DATA.map((s, i) => {
const cat = MOCK_CATEGORIES.find(c => c.name === s.cat) || MOCK_CATEGORIES[0];
const playCount = Math.floor(Math.random() * 98000000) + 100000;
const likeCount = Math.floor(playCount * (Math.random() * 0.06 + 0.01));
const duration = Math.floor(Math.random() * 200) + 120;
const releaseDate = `20${String(Math.floor(Math.random()*16)+10)}-${String(Math.floor(Math.random()*12)+1).padStart(2,'0')}-${String(Math.floor(Math.random()*28)+1).padStart(2,'0')}`;
const tagCount = Math.floor(Math.random() * 3) + 1;
const tags = [];
const pool = [...TAG_POOL];
for (let t = 0; t < tagCount; t++) {
const idx = Math.floor(Math.random() * pool.length);
tags.push(pool.splice(idx, 1)[0]);
}
return {
id: i + 1,
title: s.title,
artist: s.artist,
album: ALBUMS[Math.floor(Math.random() * ALBUMS.length)],
category_id: cat.id,
category_name: cat.name,
cover: '',
duration: duration,
play_count: playCount,
like_count: likeCount,
lyrics: s.lyrics,
release_date: releaseDate,
language: '中文',
tags: JSON.stringify(tags),
is_hot: i < 12,
is_new: i >= SONG_DATA.length - 8,
sort: i + 1,
status: 1,
};
});
}
let MOCK_SONGS = generateMockSongs();
/* ==================== API Layer ==================== */
const API_BASE = '/api/v1/music';
async function apiGet(path) {
try {
const res = await fetch(API_BASE + path, {
headers: { 'Accept': 'application/json' },
});
if (!res.ok) throw new Error('HTTP ' + res.status);
const json = await res.json();
if (json.code !== 0) throw new Error(json.message || 'API error');
return json.data;
} catch (e) {
console.warn('[API fallback]', path, e.message);
return mockApi(path);
}
}
function mockApi(path) {
if (path === '/home') {
return {
categories: MOCK_CATEGORIES,
hot: [...MOCK_SONGS].sort((a,b) => b.play_count - a.play_count).slice(0, 8),
new: [...MOCK_SONGS].filter(s => s.is_new).slice(0, 4),
};
}
if (path === '/categories') return MOCK_CATEGORIES;
if (path.startsWith('/hot')) {
const m = path.match(/limit=(\d+)/);
const limit = m ? parseInt(m[1]) : 8;
return [...MOCK_SONGS].sort((a,b) => b.play_count - a.play_count).slice(0, limit);
}
if (path.startsWith('/new')) {
const m = path.match(/limit=(\d+)/);
const limit = m ? parseInt(m[1]) : 4;
return [...MOCK_SONGS].filter(s => s.is_new).slice(0, limit);
}
if (path.startsWith('/songs/')) {
const id = parseInt(path.split('/')[2]);
return MOCK_SONGS.find(s => s.id === id) || null;
}
if (path.startsWith('/songs')) {
const url = new URL('http://x' + path);
const params = url.searchParams;
const categoryId = params.get('category_id');
const keyword = params.get('keyword') || '';
const sort = params.get('sort') || '';
const page = parseInt(params.get('page')) || 1;
const pageSize = parseInt(params.get('page_size')) || 10;
let list = [...MOCK_SONGS];
if (categoryId) list = list.filter(s => String(s.category_id) === String(categoryId));
if (keyword) {
const kw = keyword.toLowerCase();
list = list.filter(s =>
s.title.toLowerCase().includes(kw) ||
s.artist.toLowerCase().includes(kw) ||
(s.album && s.album.toLowerCase().includes(kw))
);
}
if (sort === 'plays') list.sort((a,b) => b.play_count - a.play_count);
else if (sort === 'likes') list.sort((a,b) => b.like_count - a.like_count);
else if (sort === 'newest') list.sort((a,b) => new Date(b.release_date) - new Date(a.release_date));
const total = list.length;
const start = (page - 1) * pageSize;
list = list.slice(start, start + pageSize);
return { list, total, page, page_size: pageSize };
}
return null;
}
/* ==================== State ==================== */
const state = {
categories: [],
hotSongs: [],
newSongs: [],
library: { list: [], total: 0, page: 1, page_size: 10 },
currentCategory: null,
searchKeyword: '',
sortBy: '',
nowPlaying: null,
isPlaying: false,
currentTime: 0,
playTimer: null,
};
/* ==================== Utility Functions ==================== */
function formatDuration(sec) {
if (!sec || sec < 0) return '0:00';
const m = Math.floor(sec / 60);
const s = Math.floor(sec % 60);
return m + ':' + String(s).padStart(2, '0');
}
function formatNumber(num) {
if (num == null) return '0';
if (num >= 100000000) return (num / 100000000).toFixed(1).replace(/\.0$/, '') + '亿';
if (num >= 10000) return (num / 10000).toFixed(1).replace(/\.0$/, '') + '万';
return String(num);
}
function parseTags(tagsStr) {
if (!tagsStr) return [];
try {
const parsed = JSON.parse(tagsStr);
return Array.isArray(parsed) ? parsed : [];
} catch (e) {
return [];
}
}
function hashString(str) {
let hash = 0;
if (!str) return 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash);
}
function generateCover(title) {
const hash = hashString(title || 'unknown');
const palettes = [
['#3a1f0f', '#8B4513', '#D2691E'],
['#4B3621', '#8B6914', '#D4A017'],
['#4A0E0E', '#8B0000', '#C0392B'],
['#1C1C1C', '#3D2B1F', '#5C4033'],
['#2F1B14', '#5C3317', '#8B4513'],
['#1a1410', '#4B3621', '#8B7355'],
['#3D0C02', '#800020', '#B22222'],
['#0f1419', '#2a3d2a', '#556B2F'],
['#2B1B17', '#5C2E1E', '#8B5E3C'],
['#1a0f0a', '#3a2a1a', '#8B6914'],
];
const p = palettes[hash % palettes.length];
const angle = (hash % 180) + 90;
return 'linear-gradient(' + angle + 'deg, ' + p[0] + ', ' + p[1] + ', ' + p[2] + ')';
}
function getCoverBg(song) {
if (song && song.cover) {
return 'background-image:url(\'' + song.cover + '\');background-size:cover;background-position:center;';
}
return 'background:' + generateCover(song ? song.title : '') + ';';
}
function debounce(fn, delay) {
let timer;
return function() {
const ctx = this, args = arguments;
clearTimeout(timer);
timer = setTimeout(function() { fn.apply(ctx, args); }, delay);
};
}
function showToast(msg) {
const toast = document.getElementById('toast');
document.getElementById('toast-text').textContent = msg;
toast.classList.add('show');
clearTimeout(toast._timer);
toast._timer = setTimeout(function() { toast.classList.remove('show'); }, 2500);
}
/* ==================== Render: Categories ==================== */
function renderCategories(cats) {
const container = document.getElementById('category-tabs');
if (!cats || cats.length === 0) {
container.innerHTML = '<div style="color:var(--text-muted);padding:10px">暂无分类</div>';
return;
}
cats.sort((a,b) => (a.sort||0) - (b.sort||0));
let html = '<div class="category-tab active" data-cat-id=""><span class="cat-icon">\u266B</span>全部</div>';
cats.forEach(function(c) {
html += '<div class="category-tab" data-cat-id="' + c.id + '">' +
'<span class="cat-icon">' + (c.icon || '\u266A') + '</span>' +
'<span>' + c.name + '</span></div>';
});
container.innerHTML = html;
container.querySelectorAll('.category-tab').forEach(function(tab) {
tab.addEventListener('click', function() {
container.querySelectorAll('.category-tab').forEach(function(t) { t.classList.remove('active'); });
tab.classList.add('active');
state.currentCategory = tab.dataset.catId || null;
state.library.page = 1;
loadLibrary();
document.getElementById('library-section').scrollIntoView({ behavior: 'smooth' });
});
});
}
/* ==================== Render: Hot Songs ==================== */
function renderHot(songs) {
const container = document.getElementById('hot-list');
if (!songs || songs.length === 0) {
container.innerHTML = '<div class="empty-state"><div class="empty-icon">\u266B</div><div class="empty-text">暂无热门歌曲</div></div>';
return;
}
let html = '';
songs.forEach(function(song, idx) {
const rank = String(idx + 1).padStart(2, '0');
html += '<div class="chart-item" data-song-id="' + song.id + '">' +
'<div class="chart-rank">' + rank + '</div>' +
'<div class="chart-cover" style="' + getCoverBg(song) + '">' +
'<div style="position:absolute;width:10px;height:10px;border-radius:50%;background:rgba(0,0,0,0.4);top:50%;left:50%;transform:translate(-50%,-50%)"></div>' +
'</div>' +
'<div class="chart-info">' +
'<div class="chart-title">' + song.title + '</div>' +
'<div class="chart-artist">' + song.artist + ' \u00B7 ' + (song.album || '单曲') + '</div>' +
'</div>' +
'<div class="chart-meta">' +
(song.is_hot ? '<span class="chart-badge hot">HOT</span>' : '') +
'<div class="chart-plays">' +
'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>' +
formatNumber(song.play_count) +
'</div>' +
'<div class="chart-duration">' + formatDuration(song.duration) + '</div>' +
'<button class="chart-play-btn"><svg viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg></button>' +
'</div>' +
'</div>';
});
container.innerHTML = html;
container.querySelectorAll('.chart-item').forEach(function(item) {
item.addEventListener('click', function() {
playSong(parseInt(item.dataset.songId));
});
});
}
/* ==================== Render: New Songs ==================== */
function renderNew(songs) {
const container = document.getElementById('new-grid');
if (!songs || songs.length === 0) {
container.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><div class="empty-icon">\u266C</div><div class="empty-text">暂无新碟</div></div>';
return;
}
let html = '';
songs.forEach(function(song) {
const tags = parseTags(song.tags);
const tagText = tags.length > 0 ? tags[0] : (song.category_name || '音乐');
html += '<div class="album-card" data-song-id="' + song.id + '">' +
'<div class="album-sleeve">' +
'<div class="new-stamp">NEW</div>' +
'<div class="album-cover-bg" style="' + getCoverBg(song) + '">' +
'<div class="album-cover-text">' + tagText + '</div>' +
'</div>' +
'</div>' +
'<div class="album-info">' +
'<div class="album-title">' + song.title + '</div>' +
'<div class="album-artist">' + song.artist + '</div>' +
'<div class="album-date">' + (song.release_date || '') + '</div>' +
'</div>' +
'</div>';
});
container.innerHTML = html;
container.querySelectorAll('.album-card').forEach(function(card) {
card.addEventListener('click', function() {
playSong(parseInt(card.dataset.songId));
});
});
}
/* ==================== Render: Library ==================== */
function renderLibrary(data) {
const rowsContainer = document.getElementById('library-rows');
const countEl = document.getElementById('library-count');
if (!data || !data.list || data.list.length === 0) {
rowsContainer.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><div class="empty-icon">\u2669</div><div class="empty-text">未找到匹配的歌曲</div></div>';
countEl.textContent = '0 首歌曲';
renderPagination(0, 1, state.library.page_size);
return;
}
countEl.textContent = data.total + ' 首歌曲';
state.library.total = data.total;
let html = '';
data.list.forEach(function(song, idx) {
const rank = String((data.page - 1) * data.page_size + idx + 1).padStart(2, '0');
const badges = [];
if (song.is_hot) badges.push('<span class="lib-tag hot">HOT</span>');
if (song.is_new) badges.push('<span class="lib-tag new">NEW</span>');
html += '<div class="library-row" data-song-id="' + song.id + '">' +
'<div class="lib-rank">' + rank + '</div>' +
'<div class="lib-cover" style="' + getCoverBg(song) + '"></div>' +
'<div class="lib-title">' + song.title + badges.join('') + '</div>' +
'<div class="lib-artist">' + song.artist + '</div>' +
'<div class="lib-plays">' + formatNumber(song.play_count) + '</div>' +
'<div class="lib-duration">' + formatDuration(song.duration) + '</div>' +
'<div class="lib-action"><svg viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg></div>' +
'</div>';
});
rowsContainer.innerHTML = html;
rowsContainer.querySelectorAll('.library-row').forEach(function(row) {
row.addEventListener('click', function() {
playSong(parseInt(row.dataset.songId));
});
});
renderPagination(data.total, data.page, data.page_size);
}
/* ==================== Render: Pagination ==================== */
function renderPagination(total, page, pageSize) {
const container = document.getElementById('pagination');
const totalPages = Math.ceil(total / pageSize);
if (totalPages <= 1) { container.innerHTML = ''; return; }
let html = '';
html += '<button class="page-btn" ' + (page <= 1 ? 'disabled' : '') + ' data-page="' + (page - 1) + '">' +
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg></button>';
const maxVisible = 5;
let startPage = Math.max(1, page - Math.floor(maxVisible / 2));
let endPage = Math.min(totalPages, startPage + maxVisible - 1);
if (endPage - startPage < maxVisible - 1) startPage = Math.max(1, endPage - maxVisible + 1);
if (startPage > 1) {
html += '<button class="page-btn" data-page="1">1</button>';
if (startPage > 2) html += '<span class="page-ellipsis">...</span>';
}
for (let i = startPage; i <= endPage; i++) {
html += '<button class="page-btn ' + (i === page ? 'active' : '') + '" data-page="' + i + '">' + i + '</button>';
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) html += '<span class="page-ellipsis">...</span>';
html += '<button class="page-btn" data-page="' + totalPages + '">' + totalPages + '</button>';
}
html += '<button class="page-btn" ' + (page >= totalPages ? 'disabled' : '') + ' data-page="' + (page + 1) + '">' +
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg></button>';
container.innerHTML = html;
container.querySelectorAll('.page-btn[data-page]').forEach(function(btn) {
if (btn.disabled) return;
btn.addEventListener('click', function() {
const p = parseInt(btn.dataset.page);
if (p && p !== state.library.page) {
state.library.page = p;
loadLibrary();
document.getElementById('library-section').scrollIntoView({ behavior: 'smooth' });
}
});
});
}
/* ==================== Render: Hero Stats ==================== */
function renderHeroStats(songs) {
if (!songs || songs.length === 0) return;
const totalPlays = songs.reduce(function(sum, s) { return sum + (s.play_count || 0); }, 0);
const artists = new Set(songs.map(function(s) { return s.artist; }));
const statsEl = document.getElementById('hero-stats');
const nums = statsEl.querySelectorAll('.num');
if (nums.length >= 3) {
nums[0].textContent = songs.length + '+';
nums[1].textContent = artists.size + '+';
nums[2].textContent = formatNumber(totalPlays);
}
}
/* ==================== Now Playing ==================== */
async function playSong(songId) {
const song = await apiGet('/songs/' + songId);
if (!song) { showToast('无法加载歌曲信息'); return; }
state.nowPlaying = song;
state.currentTime = 0;
state.isPlaying = true;
// Update compact bar
document.getElementById('np-title').textContent = song.title;
document.getElementById('np-artist').textContent = song.artist;
document.getElementById('np-duration').textContent = formatDuration(song.duration);
document.getElementById('np-current').textContent = '0:00';
// Update expanded panel
document.getElementById('np-title-large').textContent = song.title;
document.getElementById('np-artist-large').textContent = song.artist;
document.getElementById('np-album-large').textContent = song.album || '单曲';
document.getElementById('np-date-large').textContent = song.release_date || '';
document.getElementById('np-duration-large').textContent = formatDuration(song.duration);
document.getElementById('np-current-large').textContent = '0:00';
document.getElementById('np-vinyl-label-title').textContent = song.title;
// Tags
const tags = parseTags(song.tags);
const tagsEl = document.getElementById('np-tags');
if (tags.length > 0) {
tagsEl.innerHTML = tags.map(function(t) { return '<span class="np-tag">' + t + '</span>'; }).join('');
} else {
tagsEl.innerHTML = '<span class="np-tag">' + (song.category_name || '音乐') + '</span>';
}
// Lyrics
const lyricsEl = document.getElementById('np-lyrics');
if (song.lyrics && song.lyrics.trim()) {
lyricsEl.textContent = song.lyrics;
lyricsEl.classList.remove('empty');
} else {
lyricsEl.textContent = '暂无歌词';
lyricsEl.classList.add('empty');
}
// Cover on vinyl label
const vinylLarge = document.getElementById('np-vinyl-large');
if (song.cover) {
vinylLarge.style.background = 'url(\'' + song.cover + '\') center/cover';
} else {
vinylLarge.style.background = 'radial-gradient(circle at center, #c0392b 0%, #c0392b 14%, #0a0806 14.5%, #1a1410 16%, #0a0806 17%, #15100a 100%)';
}
// Show panel
const np = document.getElementById('now-playing');
np.classList.add('active', 'playing');
updatePlayIcon();
// Start progress simulation
startPlayback();
showToast('正在播放: ' + song.title);
}
function togglePlay() {
if (!state.nowPlaying) return;
state.isPlaying = !state.isPlaying;
const np = document.getElementById('now-playing');
if (state.isPlaying) {
np.classList.add('playing');
startPlayback();
} else {
np.classList.remove('playing');
stopPlayback();
}
updatePlayIcon();
}
function updatePlayIcon() {
const iconPlay = '<polygon points="5 3 19 12 5 21 5 3"/>';
const iconPause = '<rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>';
const svg = state.isPlaying ? iconPause : iconPlay;
document.getElementById('np-play-icon').innerHTML = svg;
document.getElementById('np-play-icon-large').innerHTML = svg;
}
function startPlayback() {
stopPlayback();
if (!state.nowPlaying) return;
state.playTimer = setInterval(function() {
if (!state.isPlaying || !state.nowPlaying) return;
state.currentTime += 1;
if (state.currentTime >= state.nowPlaying.duration) {
state.currentTime = 0;
state.isPlaying = false;
stopPlayback();
updatePlayIcon();
document.getElementById('now-playing').classList.remove('playing');
}
updateProgress();
}, 1000);
}
function stopPlayback() {
if (state.playTimer) {
clearInterval(state.playTimer);
state.playTimer = null;
}
}
function updateProgress() {
if (!state.nowPlaying) return;
const pct = (state.currentTime / state.nowPlaying.duration) * 100;
const timeStr = formatDuration(state.currentTime);
document.getElementById('np-progress-fill').style.width = pct + '%';
document.getElementById('np-progress-fill-large').style.width = pct + '%';
document.getElementById('np-current').textContent = timeStr;
document.getElementById('np-current-large').textContent = timeStr;
}
function seekProgress(e) {
if (!state.nowPlaying) return;
const bar = e.currentTarget;
const rect = bar.getBoundingClientRect();
const pct = (e.clientX - rect.left) / rect.width;
state.currentTime = Math.floor(pct * state.nowPlaying.duration);
updateProgress();
}
/* ==================== Data Loading ==================== */
async function loadHome() {
const data = await apiGet('/home');
if (data) {
if (data.categories) { state.categories = data.categories; renderCategories(data.categories); }
if (data.hot) { state.hotSongs = data.hot; renderHot(data.hot); }
if (data.new) { state.newSongs = data.new; renderNew(data.new); }
// Combine all for stats
const allSongs = [].concat(data.hot || [], data.new || []);
if (allSongs.length > 0) renderHeroStats(allSongs);
}
}
async function loadLibrary() {
const params = new URLSearchParams();
if (state.currentCategory) params.set('category_id', state.currentCategory);
if (state.searchKeyword) params.set('keyword', state.searchKeyword);
if (state.sortBy) params.set('sort', state.sortBy);
params.set('page', state.library.page);
params.set('page_size', state.library.page_size);
// Show skeleton
const rowsEl = document.getElementById('library-rows');
rowsEl.innerHTML = '';
for (let i = 0; i < 5; i++) {
rowsEl.innerHTML += '<div class="skel-lib-row"><div class="skeleton" style="width:20px;height:14px"></div><div class="skeleton skel-lib-cover"></div><div class="skeleton skel-line"></div><div class="skeleton skel-line short"></div><div class="skeleton" style="width:60px;height:12px"></div><div class="skeleton" style="width:30px;height:12px"></div><div></div></div>';
}
const data = await apiGet('/songs?' + params.toString());
renderLibrary(data);
}
/* ==================== Event Bindings ==================== */
function bindEvents() {
// Header search → library search
const headerSearch = document.getElementById('header-search');
headerSearch.addEventListener('input', debounce(function(e) {
state.searchKeyword = e.target.value.trim();
state.library.page = 1;
document.getElementById('library-search').value = state.searchKeyword;
loadLibrary();
}, 300));
// Library search
const librarySearch = document.getElementById('library-search');
librarySearch.addEventListener('input', debounce(function(e) {
state.searchKeyword = e.target.value.trim();
state.library.page = 1;
headerSearch.value = state.searchKeyword;
loadLibrary();
}, 300));
// Sort
document.getElementById('sort-select').addEventListener('change', function(e) {
state.sortBy = e.target.value;
state.library.page = 1;
loadLibrary();
});
// Play/pause buttons
document.getElementById('np-play-btn').addEventListener('click', togglePlay);
document.getElementById('np-play-btn-large').addEventListener('click', togglePlay);
// Expand/collapse
document.getElementById('np-expand-btn').addEventListener('click', function() {
document.getElementById('now-playing').classList.toggle('expanded');
});
document.getElementById('np-collapse-btn').addEventListener('click', function() {
document.getElementById('now-playing').classList.remove('expanded');
});
// Progress bar seek
document.getElementById('np-progress-bar').addEventListener('click', seekProgress);
document.getElementById('np-progress-bar-large').addEventListener('click', seekProgress);
// Hero play button → play first hot song
document.getElementById('hero-play-btn').addEventListener('click', function() {
if (state.hotSongs && state.hotSongs.length > 0) {
playSong(state.hotSongs[0].id);
} else {
showToast('歌曲加载中,请稍候...');
}
});
// Nav scroll links
document.querySelectorAll('[data-scroll]').forEach(function(el) {
el.addEventListener('click', function(e) {
const target = el.dataset.scroll;
const el2 = document.getElementById(target);
if (el2) {
e.preventDefault();
el2.scrollIntoView({ behavior: 'smooth' });
// Update active nav
document.querySelectorAll('.nav-link').forEach(function(n) { n.classList.remove('active'); });
const navLink = document.querySelector('.nav-link[data-scroll="' + target + '"]');
if (navLink) navLink.classList.add('active');
}
});
});
}
/* ==================== Intersection Observer ==================== */
function setupScrollReveal() {
const observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
}, { threshold: 0.1, rootMargin: '0px 0px -50px 0px' });
document.querySelectorAll('.reveal').forEach(function(el) {
observer.observe(el);
});
// Nav active state on scroll
const sections = ['hero-section', 'hot-section', 'new-section', 'library-section'];
const navObserver = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
const id = entry.target.id;
document.querySelectorAll('.nav-link').forEach(function(n) {
n.classList.toggle('active', n.dataset.scroll === id);
});
}
});
}, { threshold: 0.3 });
sections.forEach(function(id) {
const el = document.getElementById(id);
if (el) navObserver.observe(el);
});
}
/* ==================== Init ==================== */
async function init() {
bindEvents();
setupScrollReveal();
// Load home data (categories, hot, new)
await loadHome();
// Load library (first page)
await loadLibrary();
}
// Start when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
</script>
</body>
</html>