- 所有18套前端模板 + 管理后台共20个HTML文件添加i18n引擎 - 前端JavaScript方案:I18N_LOCALES翻译字典 + data-i18n属性 + t()函数调用 - 语言切换器UI:下拉菜单选择,匹配各模板主题风格 - 语言持久化:localStorage存储 + URL参数覆盖 + 浏览器语言自动检测 - onLangChange回调:语言切换时动态重新渲染所有内容 - 支持语言:zh-CN(简体中文) / zh-TW(繁体中文) / en(English)
2075 lines
96 KiB
HTML
2075 lines
96 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}
|
|
|
|
/* Language switcher - vinyl theme */
|
|
.lang-switcher{position:relative;flex-shrink:0}
|
|
.lang-btn{
|
|
display:flex;align-items:center;gap:6px;padding:6px 12px;
|
|
border-radius:var(--radius);font-family:var(--mono);font-size:0.78rem;
|
|
color:var(--text-secondary);cursor:pointer;
|
|
background:rgba(26,20,16,0.6);border:1px solid var(--border-dim);
|
|
transition:all 0.3s;letter-spacing:1px;
|
|
}
|
|
.lang-btn:hover{color:var(--gold);border-color:var(--border-warm)}
|
|
.lang-btn .ico{font-size:14px}
|
|
.lang-dropdown{
|
|
position:absolute;top:calc(100% + 8px);right:0;min-width:150px;z-index:1001;
|
|
background:var(--bg-card);border:1px solid var(--border-warm);border-radius:var(--radius);
|
|
padding:6px;box-shadow:var(--shadow-deep),var(--glow-gold);
|
|
opacity:0;visibility:hidden;transform:translateY(-8px);transition:.25s;
|
|
}
|
|
.lang-dropdown.open{opacity:1;visibility:visible;transform:translateY(0)}
|
|
.lang-option{
|
|
display:flex;align-items:center;gap:10px;padding:9px 14px;border-radius:4px;
|
|
font-size:0.82rem;color:var(--text-secondary);cursor:pointer;transition:all 0.2s;
|
|
}
|
|
.lang-option:hover{background:rgba(212,160,23,0.08);color:var(--gold)}
|
|
.lang-option.active{color:var(--gold);font-weight:bold}
|
|
.lang-option .check{font-size:13px;opacity:0;transition:opacity 0.2s}
|
|
.lang-option.active .check{opacity:1}
|
|
|
|
.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" data-i18n="site_name">黑胶电台</div>
|
|
<div class="logo-sub">VINYL RADIO</div>
|
|
</div>
|
|
</div>
|
|
<nav class="nav">
|
|
<a class="nav-link active" data-scroll="hero-section" data-i18n="nav_home">首页</a>
|
|
<a class="nav-link" data-scroll="library-section" data-i18n="nav_library">唱片架</a>
|
|
<a class="nav-link" data-scroll="hot-section" data-i18n="nav_hot">排行榜</a>
|
|
<a class="nav-link" data-scroll="new-section" data-i18n="nav_new">新碟</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" data-i18n-placeholder="search_placeholder" placeholder="搜索歌曲、歌手...">
|
|
</div>
|
|
<div class="lang-switcher" id="langSwitcher">
|
|
<button class="lang-btn" id="langBtn" onclick="toggleLangDropdown()">
|
|
<span class="ico">🌐</span>
|
|
<span id="langLabel">中文</span>
|
|
</button>
|
|
<div class="lang-dropdown" id="langDropdown">
|
|
<div class="lang-option" data-lang="zh-CN" onclick="switchLang('zh-CN')">
|
|
<span class="check">✓</span><span>简体中文</span>
|
|
</div>
|
|
<div class="lang-option" data-lang="zh-TW" onclick="switchLang('zh-TW')">
|
|
<span class="check">✓</span><span>繁體中文</span>
|
|
</div>
|
|
<div class="lang-option" data-lang="en" onclick="switchLang('en')">
|
|
<span class="check">✓</span><span>English</span>
|
|
</div>
|
|
</div>
|
|
</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>♫</span><span>♪</span><span>♩</span><span>♬</span>
|
|
</div>
|
|
<div class="container hero-inner">
|
|
<div class="hero-content">
|
|
<div class="hero-tag" data-i18n="hero_tag">经典之声 · 原音重现</div>
|
|
<h1 class="hero-title"><span data-i18n="hero_title_1">聆听</span><span class="accent" data-i18n="hero_title_2">经典</span><span data-i18n="hero_title_3">之声</span><br><span data-i18n="hero_title_4">重温黑胶岁月</span></h1>
|
|
<p class="hero-subtitle" data-i18n="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>
|
|
<span data-i18n="hero_play">开始播放</span>
|
|
</button>
|
|
<button class="btn-ghost" data-scroll="library-section">
|
|
<span data-i18n="hero_browse">浏览曲库</span>
|
|
<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" data-i18n="stat_records">唱片收藏</div></div>
|
|
<div class="hero-stat"><div class="num">—</div><div class="label" data-i18n="stat_artists">艺术家</div></div>
|
|
<div class="hero-stat"><div class="num">—</div><div class="label" data-i18n="stat_plays">总播放量</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" data-i18n="vinyl_label">黑胶电台</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" data-i18n="section_genres">音乐分类</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" data-i18n="section_hot">热门金曲</div>
|
|
<div class="section-subtitle">HOT CHART</div>
|
|
</div>
|
|
</div>
|
|
<a class="section-more" data-scroll="library-section"><span data-i18n="see_all">查看全部</span> →</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" data-i18n="section_new">新碟速递</div>
|
|
<div class="section-subtitle">NEW RELEASES</div>
|
|
</div>
|
|
</div>
|
|
<a class="section-more" data-scroll="library-section"><span data-i18n="see_all">查看全部</span> →</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" data-i18n="section_library">音乐图书馆</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" data-i18n-placeholder="search_placeholder_lib" placeholder="搜索歌曲、歌手、专辑...">
|
|
</div>
|
|
<select class="sort-select" id="sort-select">
|
|
<option value="" data-i18n="sort_default">默认排序</option>
|
|
<option value="plays" data-i18n="sort_plays">播放量最高</option>
|
|
<option value="likes" data-i18n="sort_likes">收藏最多</option>
|
|
<option value="newest" data-i18n="sort_newest">最新发布</option>
|
|
</select>
|
|
<div class="library-count" id="library-count">— <span data-i18n="tracks_unit">首歌曲</span></div>
|
|
</div>
|
|
<div class="library-table" id="library-table">
|
|
<div class="library-table-head">
|
|
<div>#</div><div></div><div data-i18n="col_song">歌曲</div><div data-i18n="col_artist">艺术家</div><div data-i18n="col_plays">播放量</div><div style="text-align:right" data-i18n="col_duration">时长</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" data-i18n="site_name">黑胶电台</div>
|
|
</div>
|
|
<p class="footer-desc" data-i18n="footer_desc">在数字洪流中,留存一份模拟的温度。每一道纹路,都是时光的低语。</p>
|
|
</div>
|
|
<div class="footer-col">
|
|
<h4 data-i18n="footer_explore">探索</h4>
|
|
<ul>
|
|
<li><a data-scroll="hot-section" data-i18n="nav_hot">热门金曲</a></li>
|
|
<li><a data-scroll="new-section" data-i18n="section_new">新碟速递</a></li>
|
|
<li><a data-scroll="library-section" data-i18n="section_library">音乐图书馆</a></li>
|
|
<li><a data-scroll="category-section" data-i18n="section_genres">分类浏览</a></li>
|
|
</ul>
|
|
</div>
|
|
<div class="footer-col">
|
|
<h4 data-i18n="footer_about">关于</h4>
|
|
<ul>
|
|
<li><a data-i18n="footer_about_us">关于我们</a></li>
|
|
<li><a data-i18n="footer_terms">使用条款</a></li>
|
|
<li><a data-i18n="footer_privacy">隐私政策</a></li>
|
|
<li><a data-i18n="footer_copyright">版权声明</a></li>
|
|
</ul>
|
|
</div>
|
|
<div class="footer-col">
|
|
<h4 data-i18n="footer_connect">连接</h4>
|
|
<ul>
|
|
<li><a data-i18n="footer_feedback">意见反馈</a></li>
|
|
<li><a data-i18n="footer_business">商务合作</a></li>
|
|
<li><a data-i18n="footer_join">加入我们</a></li>
|
|
<li><a data-i18n="footer_api">开发者API</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div class="footer-bottom">
|
|
<div class="footer-copy">© 2026 VINYL RADIO · <span data-i18n="site_name">黑胶电台</span> · <span data-i18n="footer_rights">保留所有权利</span></div>
|
|
<div class="footer-decor">♫ ♪ ♩ ♬</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" data-i18n="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">♫</span>
|
|
<span id="toast-text">提示</span>
|
|
</div>
|
|
|
|
<script>
|
|
/* ============================================================
|
|
i18n 国际化引擎
|
|
============================================================ */
|
|
const I18N_LOCALES = {
|
|
'zh-CN': {
|
|
title: '黑胶电台 - 复古音乐平台', site_name: '黑胶电台',
|
|
nav_home: '首页', nav_library: '唱片架', nav_hot: '排行榜', nav_new: '新碟',
|
|
search_placeholder: '搜索歌曲、歌手...', search_placeholder_lib: '搜索歌曲、歌手、专辑...',
|
|
hero_tag: '经典之声 · 原音重现',
|
|
hero_title_1: '聆听', hero_title_2: '经典', hero_title_3: '之声', hero_title_4: '重温黑胶岁月',
|
|
hero_subtitle: '在数字时代里,保留一份模拟的温暖。每一张唱片,都是一段值得珍藏的时光记忆。',
|
|
hero_play: '开始播放', hero_browse: '浏览曲库',
|
|
stat_records: '唱片收藏', stat_artists: '艺术家', stat_plays: '总播放量',
|
|
vinyl_label: '黑胶电台',
|
|
section_genres: '音乐分类', section_hot: '热门金曲', section_new: '新碟速递', section_library: '音乐图书馆',
|
|
see_all: '查看全部',
|
|
col_song: '歌曲', col_artist: '艺术家', col_plays: '播放量', col_duration: '时长',
|
|
sort_default: '默认排序', sort_plays: '播放量最高', sort_likes: '收藏最多', sort_newest: '最新发布',
|
|
tracks_unit: '首歌曲', lyrics_title: '歌词', no_lyrics: '暂无歌词',
|
|
ctrl_play: '播放/暂停', ctrl_expand: '展开', ctrl_collapse: '收起',
|
|
cat_all: '全部', cat_pop: '流行', cat_rock: '摇滚', cat_jazz: '爵士',
|
|
cat_classical: '古典', cat_folk: '民谣', cat_electronic: '电子', cat_rnb: 'R&B', cat_hiphop: '嘻哈',
|
|
empty_categories: '暂无分类', empty_hot: '暂无热门歌曲', empty_new: '暂无新碟', empty_library: '未找到匹配的歌曲',
|
|
toast_now_playing: '正在播放: ', toast_cannot_load: '无法加载歌曲信息', toast_loading: '歌曲加载中,请稍候...',
|
|
toast_no_songs: '暂无可播放的歌曲', toast_select_song: '请先选择一首歌曲',
|
|
toast_cancel_like: '已取消收藏', toast_add_like: '已加入收藏',
|
|
toast_no_liked: '还没有收藏的歌曲,去点亮一颗心吧', toast_liked_count: '已收藏 ',
|
|
footer_desc: '在数字洪流中,留存一份模拟的温度。每一道纹路,都是时光的低语。',
|
|
footer_explore: '探索', footer_about: '关于', footer_connect: '连接',
|
|
footer_about_us: '关于我们', footer_terms: '使用条款', footer_privacy: '隐私政策', footer_copyright: '版权声明',
|
|
footer_feedback: '意见反馈', footer_business: '商务合作', footer_join: '加入我们', footer_api: '开发者API',
|
|
footer_rights: '保留所有权利',
|
|
},
|
|
'zh-TW': {
|
|
title: '黑膠電台 - 復古音樂平台', site_name: '黑膠電台',
|
|
nav_home: '首頁', nav_library: '唱片架', nav_hot: '排行榜', nav_new: '新碟',
|
|
search_placeholder: '搜尋歌曲、歌手...', search_placeholder_lib: '搜尋歌曲、歌手、專輯...',
|
|
hero_tag: '經典之聲 · 原音重現',
|
|
hero_title_1: '聆聽', hero_title_2: '經典', hero_title_3: '之聲', hero_title_4: '重溫黑膠歲月',
|
|
hero_subtitle: '在數位時代裡,保留一份類比的溫度。每一張唱片,都是一段值得珍藏的時光記憶。',
|
|
hero_play: '開始播放', hero_browse: '瀏覽曲庫',
|
|
stat_records: '唱片收藏', stat_artists: '藝術家', stat_plays: '總播放量',
|
|
vinyl_label: '黑膠電台',
|
|
section_genres: '音樂分類', section_hot: '熱門金曲', section_new: '新碟速遞', section_library: '音樂圖書館',
|
|
see_all: '查看全部',
|
|
col_song: '歌曲', col_artist: '藝術家', col_plays: '播放量', col_duration: '時長',
|
|
sort_default: '預設排序', sort_plays: '播放量最高', sort_likes: '收藏最多', sort_newest: '最新發布',
|
|
tracks_unit: '首歌曲', lyrics_title: '歌詞', no_lyrics: '暫無歌詞',
|
|
ctrl_play: '播放/暫停', ctrl_expand: '展開', ctrl_collapse: '收起',
|
|
cat_all: '全部', cat_pop: '流行', cat_rock: '搖滾', cat_jazz: '爵士',
|
|
cat_classical: '古典', cat_folk: '民謠', cat_electronic: '電子', cat_rnb: 'R&B', cat_hiphop: '嘻哈',
|
|
empty_categories: '暫無分類', empty_hot: '暫無熱門歌曲', empty_new: '暫無新碟', empty_library: '未找到匹配的歌曲',
|
|
toast_now_playing: '正在播放: ', toast_cannot_load: '無法載入歌曲資訊', toast_loading: '歌曲載入中,請稍候...',
|
|
toast_no_songs: '暫無可播放的歌曲', toast_select_song: '請先選擇一首歌曲',
|
|
toast_cancel_like: '已取消收藏', toast_add_like: '已加入收藏',
|
|
toast_no_liked: '還沒有收藏的歌曲,去點亮一顆心吧', toast_liked_count: '已收藏 ',
|
|
footer_desc: '在數位洪流中,留存一份類比的溫度。每一道紋路,都是時光的低語。',
|
|
footer_explore: '探索', footer_about: '關於', footer_connect: '連接',
|
|
footer_about_us: '關於我們', footer_terms: '使用條款', footer_privacy: '隱私政策', footer_copyright: '版權聲明',
|
|
footer_feedback: '意見回饋', footer_business: '商務合作', footer_join: '加入我們', footer_api: '開發者API',
|
|
footer_rights: '保留所有權利',
|
|
},
|
|
'en': {
|
|
title: 'Vinyl Radio - Retro Music Platform', site_name: 'Vinyl Radio',
|
|
nav_home: 'Home', nav_library: 'Library', nav_hot: 'Charts', nav_new: 'New',
|
|
search_placeholder: 'Search songs, artists...', search_placeholder_lib: 'Search songs, artists, albums...',
|
|
hero_tag: 'Classic Sound · Pure Audio',
|
|
hero_title_1: 'Listen to', hero_title_2: 'Classic', hero_title_3: 'Sounds', hero_title_4: 'Relive the Vinyl Era',
|
|
hero_subtitle: 'In the digital age, preserve the warmth of analog. Every record is a cherished memory of time.',
|
|
hero_play: 'Start Playing', hero_browse: 'Browse Library',
|
|
stat_records: 'Records', stat_artists: 'Artists', stat_plays: 'Total Plays',
|
|
vinyl_label: 'Vinyl Radio',
|
|
section_genres: 'Genres', section_hot: 'Hot Chart', section_new: 'New Releases', section_library: 'Music Library',
|
|
see_all: 'See All',
|
|
col_song: 'Song', col_artist: 'Artist', col_plays: 'Plays', col_duration: 'Duration',
|
|
sort_default: 'Default', sort_plays: 'Most Played', sort_likes: 'Most Liked', sort_newest: 'Newest',
|
|
tracks_unit: 'tracks', lyrics_title: 'Lyrics', no_lyrics: 'No lyrics available',
|
|
ctrl_play: 'Play/Pause', ctrl_expand: 'Expand', ctrl_collapse: 'Collapse',
|
|
cat_all: 'All', cat_pop: 'Pop', cat_rock: 'Rock', cat_jazz: 'Jazz',
|
|
cat_classical: 'Classical', cat_folk: 'Folk', cat_electronic: 'Electronic', cat_rnb: 'R&B', cat_hiphop: 'Hip-Hop',
|
|
empty_categories: 'No categories', empty_hot: 'No hot songs yet', empty_new: 'No new releases yet', empty_library: 'No songs match your search',
|
|
toast_now_playing: 'Now playing: ', toast_cannot_load: 'Cannot load song info', toast_loading: 'Loading song, please wait...',
|
|
toast_no_songs: 'No songs available', toast_select_song: 'Select a song first',
|
|
toast_cancel_like: 'Removed from liked', toast_add_like: 'Added to liked',
|
|
toast_no_liked: 'No liked songs yet, go show some love', toast_liked_count: 'Liked ',
|
|
footer_desc: 'In the digital flood, preserve a touch of analog warmth. Every groove is a whisper of time.',
|
|
footer_explore: 'Explore', footer_about: 'About', footer_connect: 'Connect',
|
|
footer_about_us: 'About Us', footer_terms: 'Terms of Service', footer_privacy: 'Privacy Policy', footer_copyright: 'Copyright',
|
|
footer_feedback: 'Feedback', footer_business: 'Business', footer_join: 'Join Us', footer_api: 'Developer API',
|
|
footer_rights: 'All Rights Reserved',
|
|
}
|
|
};
|
|
const LANG_LABELS = {'zh-CN':'中文','en':'EN','zh-TW':'繁中'};
|
|
const CATEGORY_NAME_KEY = {流行:'cat_pop',摇滚:'cat_rock',爵士:'cat_jazz',古典:'cat_classical',民谣:'cat_folk',电子:'cat_electronic','R&B':'cat_rnb',嘻哈:'cat_hiphop'};
|
|
let currentLang = (function(){
|
|
try{ var u=new URLSearchParams(location.search).get('lang'); if(u&&I18N_LOCALES[u]) return u; }catch(e){}
|
|
try{ var s=localStorage.getItem('music2_lang'); if(s&&I18N_LOCALES[s]) return s; }catch(e){}
|
|
var b=navigator.language||navigator.userLanguage||'';
|
|
if(b.startsWith('zh-TW')||b.startsWith('zh-Hant')) return 'zh-TW';
|
|
if(b.startsWith('zh')) return 'zh-CN';
|
|
return 'en';
|
|
})();
|
|
function t(key){ return (I18N_LOCALES[currentLang]&&I18N_LOCALES[currentLang][key])||(I18N_LOCALES['zh-CN'][key])||key; }
|
|
function applyI18n(){
|
|
document.querySelectorAll('[data-i18n]').forEach(function(el){ el.textContent=t(el.getAttribute('data-i18n')); });
|
|
document.querySelectorAll('[data-i18n-placeholder]').forEach(function(el){ el.placeholder=t(el.getAttribute('data-i18n-placeholder')); });
|
|
document.querySelectorAll('[data-i18n-title]').forEach(function(el){ el.title=t(el.getAttribute('data-i18n-title')); });
|
|
document.documentElement.lang=currentLang==='zh-TW'?'zh-TW':currentLang==='zh-CN'?'zh-CN':'en';
|
|
document.title=t('title');
|
|
var ll=document.getElementById('langLabel'); if(ll) ll.textContent=LANG_LABELS[currentLang]||currentLang;
|
|
document.querySelectorAll('.lang-option').forEach(function(opt){ opt.classList.toggle('active',opt.getAttribute('data-lang')===currentLang); });
|
|
}
|
|
function switchLang(lang){
|
|
if(!I18N_LOCALES[lang]) return;
|
|
currentLang=lang;
|
|
try{localStorage.setItem('music2_lang',lang);}catch(e){}
|
|
applyI18n();
|
|
if(typeof onLangChange==='function') onLangChange(lang);
|
|
document.getElementById('langDropdown').classList.remove('open');
|
|
}
|
|
function toggleLangDropdown(){ document.getElementById('langDropdown').classList.toggle('open'); }
|
|
document.addEventListener('click',function(e){ var sw=document.getElementById('langSwitcher'); if(sw&&!sw.contains(e.target)) document.getElementById('langDropdown').classList.remove('open'); });
|
|
|
|
/* ============================================================
|
|
* 黑胶电台 - 复古音乐平台
|
|
* 单文件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) {
|
|
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">' + t('empty_categories') + '</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>' + t('cat_all') + '</div>';
|
|
cats.forEach(function(c) {
|
|
const nameKey = CATEGORY_NAME_KEY[c.name];
|
|
const displayName = nameKey ? t(nameKey) : c.name;
|
|
html += '<div class="category-tab" data-cat-id="' + c.id + '">' +
|
|
'<span class="cat-icon">' + (c.icon || '\u266A') + '</span>' +
|
|
'<span>' + displayName + '</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">' + t('empty_hot') + '</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) + '">' +
|
|
(song.cover ? '<img src="' + song.cover + '" alt="" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover" onerror="this.style.display=\'none\'">' : '') +
|
|
'<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">' + t('empty_new') + '</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) + '">' +
|
|
(song.cover ? '<img src="' + song.cover + '" alt="" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover" onerror="this.style.display=\'none\'">' : '') +
|
|
'<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">' + t('empty_library') + '</div></div>';
|
|
countEl.textContent = '0 ' + t('tracks_unit');
|
|
renderPagination(0, 1, state.library.page_size);
|
|
return;
|
|
}
|
|
|
|
countEl.textContent = data.total + ' ' + t('tracks_unit');
|
|
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) + '">' + (song.cover ? '<img src="' + song.cover + '" alt="" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover" onerror="this.style.display=\'none\'">' : '') + '</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(t('toast_cannot_load')); 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 = t('no_lyrics');
|
|
lyricsEl.classList.add('empty');
|
|
}
|
|
|
|
// Cover on vinyl label
|
|
const vinylLarge = document.getElementById('np-vinyl-large');
|
|
vinylLarge.style.background = 'radial-gradient(circle at center, #c0392b 0%, #c0392b 14%, #0a0806 14.5%, #1a1410 16%, #0a0806 17%, #15100a 100%)';
|
|
var oldVinylImg = vinylLarge.querySelector('img.vinyl-cover-img');
|
|
if (oldVinylImg) oldVinylImg.remove();
|
|
if (song.cover) {
|
|
var vinylImg = document.createElement('img');
|
|
vinylImg.className = 'vinyl-cover-img';
|
|
vinylImg.src = song.cover;
|
|
vinylImg.alt = '';
|
|
vinylImg.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;object-fit:cover;border-radius:50%;z-index:1';
|
|
vinylImg.onerror = function(){ this.style.display='none'; };
|
|
vinylLarge.insertBefore(vinylImg, vinylLarge.firstChild);
|
|
}
|
|
|
|
// Show panel
|
|
const np = document.getElementById('now-playing');
|
|
np.classList.add('active', 'playing');
|
|
updatePlayIcon();
|
|
|
|
// Start progress simulation
|
|
startPlayback();
|
|
|
|
showToast(t('toast_now_playing') + 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(t('toast_no_songs'));
|
|
}
|
|
});
|
|
|
|
// 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);
|
|
});
|
|
}
|
|
|
|
/* ==================== Language Change Callback ==================== */
|
|
function onLangChange(lang){
|
|
applyI18n();
|
|
if(state.categories.length>0) renderCategories(state.categories);
|
|
if(state.hotSongs.length>0) renderHot(state.hotSongs);
|
|
if(state.newSongs.length>0) renderNew(state.newSongs);
|
|
if(state.library.list.length>0 || state.library.total>0){
|
|
renderLibrary({list:state.library.list,total:state.library.total,page:state.library.page,page_size:state.library.page_size});
|
|
}
|
|
}
|
|
|
|
/* ==================== Init ==================== */
|
|
async function init() {
|
|
applyI18n();
|
|
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>
|