- 所有18套前端模板 + 管理后台共20个HTML文件添加i18n引擎 - 前端JavaScript方案:I18N_LOCALES翻译字典 + data-i18n属性 + t()函数调用 - 语言切换器UI:下拉菜单选择,匹配各模板主题风格 - 语言持久化:localStorage存储 + URL参数覆盖 + 浏览器语言自动检测 - onLangChange回调:语言切换时动态重新渲染所有内容 - 支持语言:zh-CN(简体中文) / zh-TW(繁体中文) / en(English)
2531 lines
102 KiB
HTML
2531 lines
102 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>云音 Music · 暗黑播放器</title>
|
||
<style>
|
||
/* ============================================================
|
||
云音 Music — 暗黑播放器风音乐平台
|
||
单文件模板,零外部依赖
|
||
============================================================ */
|
||
|
||
/* ---------- 设计变量 ---------- */
|
||
:root{
|
||
--bg:#0d0d0d;
|
||
--bg-2:#121212;
|
||
--bg-3:#181818;
|
||
--bg-card:#1a1a1a;
|
||
--bg-glass:rgba(255,255,255,.04);
|
||
--bg-glass-hover:rgba(255,255,255,.07);
|
||
--green:#1db954;
|
||
--green-light:#1ed760;
|
||
--green-dark:#169c46;
|
||
--green-glow:0 0 20px rgba(29,185,84,.35);
|
||
--green-glow-strong:0 0 30px rgba(29,185,84,.5),0 0 60px rgba(29,185,84,.2);
|
||
--text:#ffffff;
|
||
--text-dim:#b3b3b3;
|
||
--text-mute:#6a6a6a;
|
||
--border:rgba(255,255,255,.08);
|
||
--border-hover:rgba(255,255,255,.15);
|
||
--radius:12px;
|
||
--radius-sm:8px;
|
||
--radius-lg:16px;
|
||
--sidebar-w:240px;
|
||
--player-h:90px;
|
||
--font:'Segoe UI','Microsoft YaHei','PingFang SC','Helvetica Neue',system-ui,sans-serif;
|
||
}
|
||
|
||
/* ---------- 重置 ---------- */
|
||
*{margin:0;padding:0;box-sizing:border-box}
|
||
html{scroll-behavior:smooth}
|
||
body{
|
||
font-family:var(--font);
|
||
background:var(--bg);
|
||
color:var(--text);
|
||
overflow-x:hidden;
|
||
line-height:1.6;
|
||
min-height:100vh;
|
||
-webkit-font-smoothing:antialiased;
|
||
}
|
||
a{color:inherit;text-decoration:none}
|
||
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
|
||
img{display:block;max-width:100%}
|
||
ul,ol{list-style:none}
|
||
::selection{background:var(--green);color:#fff}
|
||
|
||
/* 滚动条 */
|
||
::-webkit-scrollbar{width:12px;height:12px}
|
||
::-webkit-scrollbar-track{background:var(--bg-2)}
|
||
::-webkit-scrollbar-thumb{background:#3a3a3a;border-radius:6px;border:3px solid var(--bg-2)}
|
||
::-webkit-scrollbar-thumb:hover{background:var(--green-dark)}
|
||
|
||
/* ============================================================
|
||
布局
|
||
============================================================ */
|
||
.app{display:flex;min-height:100vh}
|
||
|
||
/* ============================================================
|
||
侧边栏
|
||
============================================================ */
|
||
.sidebar{
|
||
position:fixed;left:0;top:0;bottom:0;width:var(--sidebar-w);z-index:100;
|
||
background:#000;
|
||
display:flex;flex-direction:column;
|
||
padding:24px 0 0;
|
||
border-right:1px solid var(--border);
|
||
}
|
||
.sidebar-logo{
|
||
display:flex;align-items:center;gap:12px;
|
||
padding:0 24px 24px;
|
||
}
|
||
.sidebar-logo .logo-icon{
|
||
width:40px;height:40px;border-radius:50%;
|
||
background:linear-gradient(135deg,var(--green),var(--green-dark));
|
||
display:flex;align-items:center;justify-content:center;
|
||
font-size:22px;color:#000;font-weight:900;
|
||
box-shadow:var(--green-glow);
|
||
flex-shrink:0;
|
||
}
|
||
.sidebar-logo .logo-text{font-size:20px;font-weight:800;letter-spacing:-.5px}
|
||
.sidebar-logo .logo-text span{color:var(--green)}
|
||
|
||
.sidebar-nav{padding:0 12px}
|
||
.sidebar-nav .nav-section-title{
|
||
font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1px;
|
||
color:var(--text-mute);padding:16px 12px 8px;
|
||
}
|
||
.sidebar-nav .nav-link{
|
||
display:flex;align-items:center;gap:16px;
|
||
padding:10px 12px;border-radius:var(--radius-sm);
|
||
color:var(--text-dim);font-size:14px;font-weight:600;
|
||
transition:color .2s,background .2s;
|
||
cursor:pointer;position:relative;
|
||
}
|
||
.sidebar-nav .nav-link:hover{color:var(--text)}
|
||
.sidebar-nav .nav-link.active{color:var(--text)}
|
||
.sidebar-nav .nav-link.active::before{
|
||
content:'';position:absolute;left:0;top:50%;transform:translateY(-50%);
|
||
width:3px;height:20px;background:var(--green);border-radius:0 2px 2px 0;
|
||
}
|
||
.sidebar-nav .nav-link .ico{font-size:18px;width:24px;text-align:center;flex-shrink:0}
|
||
|
||
.sidebar-playlists{flex:1;overflow-y:auto;padding:8px 12px 16px}
|
||
.sidebar-playlists .nav-section-title{padding:16px 12px 8px}
|
||
.playlist-item{
|
||
display:flex;align-items:center;gap:12px;
|
||
padding:8px 12px;border-radius:var(--radius-sm);
|
||
cursor:pointer;transition:background .2s;
|
||
}
|
||
.playlist-item:hover{background:var(--bg-glass)}
|
||
.playlist-item .pl-cover{
|
||
width:40px;height:40px;border-radius:4px;flex-shrink:0;
|
||
display:flex;align-items:center;justify-content:center;
|
||
font-size:18px;
|
||
}
|
||
.playlist-item .pl-info{flex:1;min-width:0}
|
||
.playlist-item .pl-name{font-size:13px;font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.playlist-item .pl-desc{font-size:11px;color:var(--text-mute);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
|
||
.sidebar-footer{
|
||
padding:16px 24px;border-top:1px solid var(--border);
|
||
}
|
||
.sidebar-footer .footer-link{
|
||
font-size:11px;color:var(--text-mute);display:block;padding:4px 0;
|
||
}
|
||
.sidebar-footer .footer-link:hover{color:var(--text-dim)}
|
||
|
||
/* ============================================================
|
||
主内容区
|
||
============================================================ */
|
||
.main{
|
||
margin-left:var(--sidebar-w);
|
||
flex:1;min-width:0;
|
||
padding-bottom:calc(var(--player-h) + 24px);
|
||
}
|
||
|
||
/* ---------- 顶部栏 ---------- */
|
||
.topbar{
|
||
position:sticky;top:0;z-index:50;
|
||
display:flex;align-items:center;gap:16px;
|
||
padding:16px 32px;
|
||
background:rgba(13,13,13,.85);
|
||
backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
|
||
border-bottom:1px solid var(--border);
|
||
}
|
||
.topbar-nav{display:flex;align-items:center;gap:8px}
|
||
.topbar-nav .nav-btn{
|
||
width:32px;height:32px;border-radius:50%;
|
||
background:rgba(0,0,0,.6);
|
||
display:flex;align-items:center;justify-content:center;
|
||
font-size:16px;color:var(--text-dim);
|
||
transition:color .2s,transform .2s;
|
||
}
|
||
.topbar-nav .nav-btn:hover{color:var(--text);transform:scale(1.05)}
|
||
|
||
.search-wrap{
|
||
flex:1;max-width:440px;position:relative;
|
||
}
|
||
.search-wrap .search-input{
|
||
width:100%;padding:10px 16px 10px 44px;
|
||
border-radius:var(--radius-sm);
|
||
background:var(--bg-glass);
|
||
border:1px solid var(--border);
|
||
color:var(--text);font-size:14px;
|
||
transition:border-color .2s,background .2s,box-shadow .2s;
|
||
}
|
||
.search-wrap .search-input::placeholder{color:var(--text-mute)}
|
||
.search-wrap .search-input:focus{
|
||
outline:none;border-color:var(--green);
|
||
background:var(--bg-glass-hover);
|
||
box-shadow:0 0 0 3px rgba(29,185,84,.15);
|
||
}
|
||
.search-wrap .search-ico{
|
||
position:absolute;left:14px;top:50%;transform:translateY(-50%);
|
||
font-size:18px;color:var(--text-mute);pointer-events:none;
|
||
}
|
||
.search-wrap .search-clear{
|
||
position:absolute;right:12px;top:50%;transform:translateY(-50%);
|
||
width:22px;height:22px;border-radius:50%;
|
||
background:var(--bg-3);color:var(--text-dim);
|
||
display:none;align-items:center;justify-content:center;
|
||
font-size:13px;cursor:pointer;
|
||
}
|
||
.search-wrap.has-text .search-clear{display:flex}
|
||
|
||
.topbar-right{display:flex;align-items:center;gap:16px;margin-left:auto}
|
||
.topbar-right .upgrade-btn{
|
||
padding:8px 20px;border-radius:20px;
|
||
background:var(--bg-glass);border:1px solid var(--border);
|
||
color:var(--text);font-size:13px;font-weight:600;
|
||
transition:transform .2s,background .2s;
|
||
}
|
||
.topbar-right .upgrade-btn:hover{transform:scale(1.05);background:var(--bg-glass-hover)}
|
||
.topbar-right .user-avatar{
|
||
width:34px;height:34px;border-radius:50%;
|
||
background:linear-gradient(135deg,#535353,#333);
|
||
display:flex;align-items:center;justify-content:center;
|
||
font-size:14px;font-weight:700;color:var(--text);
|
||
cursor:pointer;border:2px solid var(--green);
|
||
transition:box-shadow .2s;
|
||
}
|
||
.topbar-right .user-avatar:hover{box-shadow:var(--green-glow)}
|
||
|
||
/* ---------- 语言切换器 ---------- */
|
||
.lang-switcher{position:relative}
|
||
.lang-btn{
|
||
display:flex;align-items:center;gap:6px;padding:8px 14px;border-radius:20px;
|
||
font-size:13px;font-weight:600;color:var(--text-dim);cursor:pointer;
|
||
background:var(--bg-glass);border:1px solid var(--border);
|
||
transition:all .25s;
|
||
}
|
||
.lang-btn:hover{color:var(--text);background:var(--bg-glass-hover);border-color:var(--border-hover)}
|
||
.lang-btn .ico{font-size:15px}
|
||
.lang-dropdown{
|
||
position:absolute;top:calc(100% + 8px);right:0;min-width:160px;z-index:1001;
|
||
background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-sm);
|
||
padding:6px;box-shadow:0 12px 40px rgba(0,0,0,.6);
|
||
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:10px 14px;border-radius:6px;
|
||
font-size:13px;color:var(--text-dim);cursor:pointer;transition:all .2s;
|
||
}
|
||
.lang-option:hover{background:rgba(29,185,84,.1);color:var(--green)}
|
||
.lang-option.active{color:var(--green);font-weight:700}
|
||
.lang-option .check{font-size:14px;opacity:0;transition:opacity .2s}
|
||
.lang-option.active .check{opacity:1}
|
||
|
||
/* ---------- 内容容器 ---------- */
|
||
.content{padding:0 32px}
|
||
.section{margin-bottom:48px}
|
||
.section-header{
|
||
display:flex;align-items:center;justify-content:space-between;
|
||
margin-bottom:20px;
|
||
}
|
||
.section-title{
|
||
font-size:24px;font-weight:800;letter-spacing:-.5px;
|
||
display:flex;align-items:center;gap:10px;
|
||
}
|
||
.section-title .title-bar{
|
||
width:4px;height:24px;background:var(--green);border-radius:2px;
|
||
box-shadow:var(--green-glow);
|
||
}
|
||
.section-link{
|
||
font-size:12px;font-weight:700;color:var(--text-mute);
|
||
text-transform:uppercase;letter-spacing:1px;
|
||
transition:color .2s;
|
||
}
|
||
.section-link:hover{color:var(--green)}
|
||
|
||
/* ============================================================
|
||
Hero Banner
|
||
============================================================ */
|
||
.hero{
|
||
margin:24px 32px 32px;
|
||
border-radius:var(--radius-lg);
|
||
padding:48px 40px;
|
||
position:relative;overflow:hidden;
|
||
background:
|
||
linear-gradient(135deg,rgba(29,185,84,.25) 0%,rgba(18,18,18,.9) 60%),
|
||
linear-gradient(45deg,#1a1a2e,#16213e,#0d1b2a);
|
||
display:flex;flex-direction:column;justify-content:center;
|
||
min-height:260px;
|
||
}
|
||
.hero::before{
|
||
content:'';position:absolute;top:-50%;right:-10%;
|
||
width:400px;height:400px;border-radius:50%;
|
||
background:radial-gradient(circle,rgba(29,185,84,.15),transparent 70%);
|
||
pointer-events:none;
|
||
}
|
||
.hero::after{
|
||
content:'';position:absolute;bottom:-30%;left:20%;
|
||
width:300px;height:300px;border-radius:50%;
|
||
background:radial-gradient(circle,rgba(29,185,84,.08),transparent 70%);
|
||
pointer-events:none;
|
||
}
|
||
.hero-greeting{font-size:14px;color:var(--green);font-weight:600;margin-bottom:8px;position:relative;z-index:1}
|
||
.hero-title{font-size:42px;font-weight:900;letter-spacing:-1px;margin-bottom:12px;position:relative;z-index:1}
|
||
.hero-subtitle{font-size:16px;color:var(--text-dim);max-width:500px;position:relative;z-index:1}
|
||
.hero-cta{
|
||
display:inline-flex;align-items:center;gap:8px;
|
||
margin-top:24px;padding:12px 28px;
|
||
border-radius:24px;
|
||
background:var(--green);color:#000;
|
||
font-size:14px;font-weight:700;
|
||
transition:transform .2s,box-shadow .2s;
|
||
position:relative;z-index:1;width:fit-content;
|
||
}
|
||
.hero-cta:hover{transform:scale(1.05);box-shadow:var(--green-glow-strong)}
|
||
|
||
/* ============================================================
|
||
分类筛选
|
||
============================================================ */
|
||
.cat-scroll{
|
||
display:flex;gap:10px;overflow-x:auto;padding:4px 0 8px;
|
||
scrollbar-width:none;-ms-overflow-style:none;
|
||
}
|
||
.cat-scroll::-webkit-scrollbar{display:none}
|
||
.cat-pill{
|
||
display:inline-flex;align-items:center;gap:8px;
|
||
padding:8px 18px;border-radius:20px;
|
||
background:var(--bg-glass);border:1px solid var(--border);
|
||
color:var(--text-dim);font-size:13px;font-weight:600;
|
||
white-space:nowrap;transition:all .2s;
|
||
}
|
||
.cat-pill:hover{background:var(--bg-glass-hover);color:var(--text);border-color:var(--border-hover)}
|
||
.cat-pill.active{
|
||
background:var(--green);color:#000;border-color:var(--green);
|
||
box-shadow:var(--green-glow);
|
||
}
|
||
.cat-pill .pill-ico{font-size:15px}
|
||
|
||
/* ============================================================
|
||
热门歌曲列表
|
||
============================================================ */
|
||
.hot-list{
|
||
background:var(--bg-glass);
|
||
border:1px solid var(--border);
|
||
border-radius:var(--radius-lg);
|
||
overflow:hidden;
|
||
}
|
||
.hot-list-header{
|
||
display:grid;
|
||
grid-template-columns:50px 1fr 200px 120px 100px 60px;
|
||
gap:16px;padding:12px 20px;
|
||
border-bottom:1px solid var(--border);
|
||
font-size:11px;font-weight:700;text-transform:uppercase;
|
||
letter-spacing:1px;color:var(--text-mute);
|
||
}
|
||
.hot-list-header .col-center{text-align:center}
|
||
.hot-track{
|
||
display:grid;
|
||
grid-template-columns:50px 1fr 200px 120px 100px 60px;
|
||
gap:16px;padding:10px 20px;
|
||
align-items:center;
|
||
border-radius:0;
|
||
border-bottom:1px solid rgba(255,255,255,.03);
|
||
transition:background .15s;
|
||
cursor:pointer;position:relative;
|
||
}
|
||
.hot-track:last-child{border-bottom:none}
|
||
.hot-track:hover{background:var(--bg-glass-hover)}
|
||
.hot-track.playing{background:rgba(29,185,84,.08)}
|
||
.hot-track.playing .track-title{color:var(--green)}
|
||
|
||
.track-rank{
|
||
font-size:16px;font-weight:700;color:var(--text-dim);
|
||
text-align:center;font-variant-numeric:tabular-nums;
|
||
position:relative;
|
||
}
|
||
.track-rank.top-3{color:var(--green)}
|
||
.hot-track:hover .track-rank .rank-num{display:none}
|
||
.hot-track:hover .track-rank .play-icon{display:flex}
|
||
.track-rank .play-icon{
|
||
display:none;align-items:center;justify-content:center;
|
||
color:var(--text);font-size:14px;
|
||
}
|
||
|
||
.track-info{display:flex;align-items:center;gap:12px;min-width:0}
|
||
.track-cover{
|
||
width:44px;height:44px;border-radius:4px;flex-shrink:0;
|
||
display:flex;align-items:center;justify-content:center;
|
||
font-size:20px;position:relative;overflow:hidden;
|
||
}
|
||
.track-cover::after{
|
||
content:'';position:absolute;inset:0;
|
||
background:linear-gradient(135deg,rgba(255,255,255,.1),transparent);
|
||
}
|
||
.track-meta{min-width:0;flex:1}
|
||
.track-title{
|
||
font-size:14px;font-weight:600;color:var(--text);
|
||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
||
}
|
||
.track-artist{
|
||
font-size:12px;color:var(--text-dim);
|
||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
||
}
|
||
.track-album{
|
||
font-size:13px;color:var(--text-dim);
|
||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
||
}
|
||
.track-plays{
|
||
font-size:13px;color:var(--text-dim);text-align:center;
|
||
font-variant-numeric:tabular-nums;
|
||
}
|
||
.track-duration{
|
||
font-size:13px;color:var(--text-dim);text-align:center;
|
||
font-variant-numeric:tabular-nums;
|
||
}
|
||
.track-like-btn{
|
||
text-align:center;font-size:16px;color:var(--text-mute);
|
||
transition:color .2s,transform .2s;
|
||
}
|
||
.track-like-btn:hover{color:var(--green);transform:scale(1.2)}
|
||
.track-like-btn.liked{color:var(--green)}
|
||
|
||
/* ============================================================
|
||
新歌速递卡片网格
|
||
============================================================ */
|
||
.new-grid{
|
||
display:grid;
|
||
grid-template-columns:repeat(auto-fill,minmax(180px,1fr));
|
||
gap:20px;
|
||
}
|
||
.new-card{
|
||
background:var(--bg-glass);
|
||
border:1px solid var(--border);
|
||
border-radius:var(--radius);
|
||
padding:16px;
|
||
transition:background .2s,transform .2s,border-color .2s;
|
||
cursor:pointer;position:relative;overflow:hidden;
|
||
}
|
||
.new-card:hover{
|
||
background:var(--bg-glass-hover);
|
||
border-color:var(--border-hover);
|
||
transform:translateY(-4px);
|
||
}
|
||
.new-card .cover-wrap{
|
||
position:relative;margin-bottom:14px;border-radius:var(--radius-sm);overflow:hidden;
|
||
}
|
||
.new-card .card-cover{
|
||
width:100%;aspect-ratio:1;border-radius:var(--radius-sm);
|
||
display:flex;align-items:center;justify-content:center;
|
||
font-size:40px;position:relative;overflow:hidden;
|
||
}
|
||
.new-card .card-cover::after{
|
||
content:'';position:absolute;inset:0;
|
||
background:linear-gradient(160deg,rgba(255,255,255,.12),transparent 50%);
|
||
}
|
||
.new-card .badge-new{
|
||
position:absolute;top:8px;left:8px;
|
||
padding:3px 10px;border-radius:4px;
|
||
background:var(--green);color:#000;
|
||
font-size:10px;font-weight:800;letter-spacing:1px;
|
||
box-shadow:var(--green-glow);
|
||
}
|
||
.new-card .play-overlay{
|
||
position:absolute;bottom:8px;right:8px;
|
||
width:44px;height:44px;border-radius:50%;
|
||
background:var(--green);color:#000;
|
||
display:flex;align-items:center;justify-content:center;
|
||
font-size:20px;
|
||
opacity:0;transform:translateY(8px);
|
||
transition:opacity .2s,transform .2s,box-shadow .2s;
|
||
box-shadow:var(--green-glow);
|
||
}
|
||
.new-card:hover .play-overlay{opacity:1;transform:translateY(0)}
|
||
.new-card .play-overlay:hover{transform:scale(1.1)}
|
||
.new-card .card-title{
|
||
font-size:14px;font-weight:700;color:var(--text);
|
||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
||
margin-bottom:4px;
|
||
}
|
||
.new-card .card-artist{
|
||
font-size:12px;color:var(--text-dim);
|
||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
||
}
|
||
.new-card .card-date{
|
||
font-size:11px;color:var(--text-mute);margin-top:6px;
|
||
}
|
||
|
||
/* ============================================================
|
||
全部音乐(库)
|
||
============================================================ */
|
||
.lib-toolbar{
|
||
display:flex;gap:16px;align-items:center;flex-wrap:wrap;margin-bottom:24px;
|
||
padding:16px 20px;border-radius:var(--radius);
|
||
background:var(--bg-glass);border:1px solid var(--border);
|
||
}
|
||
.lib-search{flex:1;min-width:220px;position:relative}
|
||
.lib-search input{
|
||
width:100%;padding:10px 16px 10px 40px;border-radius:var(--radius-sm);
|
||
background:rgba(0,0,0,.4);border:1px solid var(--border);color:var(--text);font-size:14px;
|
||
transition:border-color .2s,box-shadow .2s;
|
||
}
|
||
.lib-search input::placeholder{color:var(--text-mute)}
|
||
.lib-search input:focus{outline:none;border-color:var(--green);box-shadow:0 0 0 3px rgba(29,185,84,.15)}
|
||
.lib-search .ico{position:absolute;left:13px;top:50%;transform:translateY(-50%);color:var(--text-mute);font-size:16px;pointer-events:none}
|
||
.lib-search .spinner{
|
||
position:absolute;right:14px;top:50%;transform:translateY(-50%);
|
||
width:16px;height:16px;border:2px solid rgba(29,185,84,.2);border-top-color:var(--green);
|
||
border-radius:50%;animation:spin .7s linear infinite;display:none;
|
||
}
|
||
.lib-search .spinner.show{display:block}
|
||
@keyframes spin{to{transform:translateY(-50%) rotate(360deg)}}
|
||
|
||
.sort-wrap{position:relative}
|
||
.sort-btn{
|
||
display:flex;align-items:center;gap:10px;padding:10px 18px;border-radius:var(--radius-sm);
|
||
background:rgba(0,0,0,.4);border:1px solid var(--border);color:var(--text-dim);font-size:14px;
|
||
transition:border-color .2s,color .2s;
|
||
}
|
||
.sort-btn:hover{border-color:var(--green);color:var(--text)}
|
||
.sort-btn .arrow{font-size:10px;transition:transform .25s}
|
||
.sort-wrap.open .sort-btn .arrow{transform:rotate(180deg)}
|
||
.sort-menu{
|
||
position:absolute;top:calc(100% + 8px);right:0;min-width:160px;z-index:20;
|
||
background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-sm);
|
||
padding:6px;opacity:0;visibility:hidden;transform:translateY(-8px);transition:.25s;
|
||
box-shadow:0 8px 32px rgba(0,0,0,.6);
|
||
}
|
||
.sort-wrap.open .sort-menu{opacity:1;visibility:visible;transform:translateY(0)}
|
||
.sort-menu button{
|
||
display:block;width:100%;text-align:left;padding:9px 14px;border-radius:6px;
|
||
font-size:13px;color:var(--text-dim);transition:background .15s,color .15s;
|
||
}
|
||
.sort-menu button:hover{background:rgba(29,185,84,.1);color:var(--green)}
|
||
.sort-menu button.active{background:rgba(29,185,84,.15);color:var(--green);font-weight:700}
|
||
|
||
.lib-info{font-size:13px;color:var(--text-dim);white-space:nowrap}
|
||
.lib-info b{color:var(--green);font-weight:700}
|
||
|
||
.lib-list{
|
||
background:var(--bg-glass);border:1px solid var(--border);
|
||
border-radius:var(--radius-lg);overflow:hidden;
|
||
}
|
||
.lib-list-header{
|
||
display:grid;
|
||
grid-template-columns:50px 1fr 180px 100px 80px 80px 50px;
|
||
gap:16px;padding:12px 20px;
|
||
border-bottom:1px solid var(--border);
|
||
font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:var(--text-mute);
|
||
}
|
||
.lib-list-header .col-center{text-align:center}
|
||
.lib-track{
|
||
display:grid;
|
||
grid-template-columns:50px 1fr 180px 100px 80px 80px 50px;
|
||
gap:16px;padding:10px 20px;align-items:center;
|
||
border-bottom:1px solid rgba(255,255,255,.03);
|
||
transition:background .15s;cursor:pointer;
|
||
}
|
||
.lib-track:last-child{border-bottom:none}
|
||
.lib-track:hover{background:var(--bg-glass-hover)}
|
||
.lib-track.playing{background:rgba(29,185,84,.08)}
|
||
.lib-track.playing .track-title{color:var(--green)}
|
||
.lib-track .track-cat{
|
||
font-size:12px;color:var(--text-dim);
|
||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
||
}
|
||
.lib-track .track-likes{
|
||
font-size:12px;color:var(--text-dim);text-align:center;font-variant-numeric:tabular-nums;
|
||
}
|
||
.lib-empty{
|
||
padding:60px 20px;text-align:center;color:var(--text-mute);
|
||
}
|
||
.lib-empty .empty-ico{font-size:48px;margin-bottom:12px;opacity:.5}
|
||
.lib-empty .empty-text{font-size:14px}
|
||
|
||
/* ============================================================
|
||
分页
|
||
============================================================ */
|
||
.pagination{display:flex;justify-content:center;align-items:center;gap:8px;margin-top:32px;flex-wrap:wrap}
|
||
.page-btn{
|
||
min-width:40px;height:40px;padding:0 12px;border-radius:8px;
|
||
background:var(--bg-glass);border:1px solid var(--border);color:var(--text-dim);
|
||
font-size:14px;font-weight:600;transition:all .25s;
|
||
display:flex;align-items:center;justify-content:center;
|
||
}
|
||
.page-btn:hover:not(:disabled){border-color:var(--green);color:var(--green);box-shadow:0 0 12px rgba(29,185,84,.2)}
|
||
.page-btn.active{
|
||
background:var(--green);border-color:var(--green);color:#000;
|
||
box-shadow:var(--green-glow);
|
||
}
|
||
.page-btn:disabled{opacity:.3;cursor:not-allowed}
|
||
.page-ellipsis{color:var(--text-mute);padding:0 4px;font-size:14px}
|
||
|
||
/* ============================================================
|
||
骨架屏
|
||
============================================================ */
|
||
.skel-shimmer{
|
||
background:linear-gradient(110deg,#1a1a1a 30%,#2a2a2a 50%,#1a1a1a 70%);
|
||
background-size:200% 100%;animation:shimmer 1.4s infinite;
|
||
}
|
||
@keyframes shimmer{from{background-position:200% 0}to{background-position:-200% 0}}
|
||
|
||
.skel-track{
|
||
display:grid;grid-template-columns:50px 1fr 200px 120px 100px 60px;
|
||
gap:16px;padding:10px 20px;align-items:center;
|
||
}
|
||
.skel-track .skel-box{height:44px;border-radius:4px}
|
||
.skel-track .skel-rank{height:20px;width:30px;border-radius:4px;margin:0 auto}
|
||
.skel-track .skel-line{height:14px;border-radius:4px}
|
||
.skel-track .skel-line.short{width:60%}
|
||
.skel-track .skel-line.mid{width:80%}
|
||
|
||
.skel-card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:20px}
|
||
.skel-card{background:var(--bg-glass);border:1px solid var(--border);border-radius:var(--radius);padding:16px}
|
||
.skel-card .skel-cover{width:100%;aspect-ratio:1;border-radius:var(--radius-sm);margin-bottom:14px}
|
||
.skel-card .skel-line{height:14px;border-radius:4px;margin-bottom:8px}
|
||
|
||
.skel-pagination{display:flex;justify-content:center;gap:8px;margin-top:32px}
|
||
.skel-pagination .skel-box{width:40px;height:40px;border-radius:8px}
|
||
|
||
/* ============================================================
|
||
底部播放器栏
|
||
============================================================ */
|
||
.player-bar{
|
||
position:fixed;bottom:0;left:0;right:0;z-index:200;
|
||
height:var(--player-h);
|
||
background:rgba(0,0,0,.92);
|
||
backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
|
||
border-top:1px solid var(--border);
|
||
display:flex;align-items:center;
|
||
padding:0 24px;gap:24px;
|
||
}
|
||
.player-bar::before{
|
||
content:'';position:absolute;top:0;left:0;right:0;height:1px;
|
||
background:linear-gradient(90deg,transparent,rgba(29,185,84,.3),transparent);
|
||
}
|
||
|
||
/* 当前歌曲信息 */
|
||
.player-current{
|
||
display:flex;align-items:center;gap:14px;
|
||
width:280px;min-width:0;flex-shrink:0;
|
||
}
|
||
.player-cover{
|
||
width:56px;height:56px;border-radius:6px;flex-shrink:0;
|
||
display:flex;align-items:center;justify-content:center;
|
||
font-size:24px;position:relative;overflow:hidden;
|
||
box-shadow:0 4px 12px rgba(0,0,0,.4);
|
||
}
|
||
.player-cover::after{
|
||
content:'';position:absolute;inset:0;
|
||
background:linear-gradient(160deg,rgba(255,255,255,.1),transparent);
|
||
}
|
||
.player-song-info{min-width:0;flex:1}
|
||
.player-song-title{
|
||
font-size:14px;font-weight:600;color:var(--text);
|
||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
||
}
|
||
.player-song-artist{
|
||
font-size:12px;color:var(--text-dim);
|
||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
||
}
|
||
.player-like{
|
||
font-size:18px;color:var(--text-mute);cursor:pointer;
|
||
transition:color .2s,transform .2s;flex-shrink:0;
|
||
}
|
||
.player-like:hover{color:var(--text);transform:scale(1.15)}
|
||
.player-like.liked{color:var(--green)}
|
||
|
||
/* 中间控制区 */
|
||
.player-center{
|
||
flex:1;display:flex;flex-direction:column;align-items:center;gap:8px;
|
||
max-width:722px;margin:0 auto;
|
||
}
|
||
.player-controls{display:flex;align-items:center;gap:20px}
|
||
.player-btn{
|
||
font-size:16px;color:var(--text-dim);
|
||
transition:color .2s,transform .2s;
|
||
display:flex;align-items:center;justify-content:center;
|
||
}
|
||
.player-btn:hover{color:var(--text);transform:scale(1.1)}
|
||
.player-btn.play-btn{
|
||
width:36px;height:36px;border-radius:50%;
|
||
background:var(--text);color:#000;font-size:16px;
|
||
display:flex;align-items:center;justify-content:center;
|
||
transition:transform .15s,box-shadow .2s;
|
||
}
|
||
.player-btn.play-btn:hover{transform:scale(1.08);box-shadow:0 0 20px rgba(255,255,255,.3)}
|
||
.player-btn.shuffle-btn.active,.player-btn.repeat-btn.active{color:var(--green)}
|
||
|
||
/* 进度条 */
|
||
.player-progress{
|
||
display:flex;align-items:center;gap:12px;width:100%;
|
||
}
|
||
.player-time{
|
||
font-size:11px;color:var(--text-dim);font-variant-numeric:tabular-nums;
|
||
min-width:40px;text-align:center;
|
||
}
|
||
.progress-bar{
|
||
flex:1;height:4px;border-radius:2px;
|
||
background:rgba(255,255,255,.1);
|
||
position:relative;cursor:pointer;overflow:hidden;
|
||
transition:height .15s;
|
||
}
|
||
.progress-bar:hover{height:6px}
|
||
.progress-fill{
|
||
height:100%;background:var(--green);border-radius:2px;
|
||
width:0%;position:relative;transition:width .1s linear;
|
||
}
|
||
.progress-bar:hover .progress-fill{background:var(--green-light)}
|
||
.progress-fill::after{
|
||
content:'';position:absolute;right:-6px;top:50%;transform:translateY(-50%);
|
||
width:12px;height:12px;border-radius:50%;background:#fff;
|
||
opacity:0;transition:opacity .15s;
|
||
}
|
||
.progress-bar:hover .progress-fill::after{opacity:1}
|
||
|
||
/* 右侧控制区 */
|
||
.player-right{
|
||
display:flex;align-items:center;gap:12px;
|
||
width:280px;justify-content:flex-end;flex-shrink:0;
|
||
}
|
||
.visualizer{
|
||
display:flex;align-items:flex-end;gap:2px;height:24px;margin-right:4px;
|
||
}
|
||
.visualizer .bar{
|
||
width:3px;background:var(--green);border-radius:2px;
|
||
animation:vis-pulse .8s ease-in-out infinite alternate;
|
||
opacity:.7;
|
||
}
|
||
.visualizer .bar:nth-child(1){animation-delay:0s;height:30%}
|
||
.visualizer .bar:nth-child(2){animation-delay:.15s;height:60%}
|
||
.visualizer .bar:nth-child(3){animation-delay:.3s;height:90%}
|
||
.visualizer .bar:nth-child(4){animation-delay:.45s;height:50%}
|
||
.visualizer.paused .bar{animation-play-state:paused;opacity:.3}
|
||
@keyframes vis-pulse{
|
||
0%{height:20%}
|
||
100%{height:100%}
|
||
}
|
||
|
||
.player-right .player-btn{font-size:15px}
|
||
.volume-wrap{display:flex;align-items:center;gap:8px}
|
||
.volume-slider{
|
||
width:100px;height:4px;border-radius:2px;
|
||
background:rgba(255,255,255,.1);position:relative;cursor:pointer;
|
||
transition:height .15s;
|
||
}
|
||
.volume-slider:hover{height:6px}
|
||
.volume-fill{
|
||
height:100%;background:var(--text);border-radius:2px;width:70%;
|
||
transition:width .1s;
|
||
}
|
||
.volume-slider:hover .volume-fill{background:var(--green-light)}
|
||
|
||
.player-expand{
|
||
font-size:16px;color:var(--text-dim);cursor:pointer;
|
||
transition:color .2s,transform .2s;
|
||
}
|
||
.player-expand:hover{color:var(--text);transform:scale(1.1)}
|
||
|
||
/* ============================================================
|
||
歌曲详情弹窗
|
||
============================================================ */
|
||
.modal-overlay{
|
||
position:fixed;inset:0;z-index:500;
|
||
background:rgba(0,0,0,.8);
|
||
backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
|
||
display:flex;align-items:center;justify-content:center;
|
||
padding:24px;
|
||
opacity:0;visibility:hidden;transition:.3s;
|
||
}
|
||
.modal-overlay.open{opacity:1;visibility:visible}
|
||
.modal{
|
||
width:100%;max-width:880px;max-height:88vh;overflow-y:auto;
|
||
background:linear-gradient(160deg,var(--bg-3),var(--bg-2));
|
||
border:1px solid var(--border);border-radius:var(--radius-lg);
|
||
box-shadow:0 0 60px rgba(29,185,84,.15),0 20px 60px rgba(0,0,0,.6);
|
||
transform:scale(.92) translateY(20px);transition:.35s cubic-bezier(.2,.8,.2,1);
|
||
position:relative;
|
||
}
|
||
.modal-overlay.open .modal{transform:scale(1) translateY(0)}
|
||
.modal::-webkit-scrollbar{width:6px}
|
||
.modal::-webkit-scrollbar-track{background:transparent}
|
||
.modal::-webkit-scrollbar-thumb{background:rgba(255,255,255,.1);border-radius:3px}
|
||
|
||
.modal-close{
|
||
position:absolute;top:16px;right:16px;z-index:10;
|
||
width:38px;height:38px;border-radius:50%;
|
||
background:rgba(0,0,0,.5);border:1px solid var(--border);
|
||
font-size:20px;color:var(--text-dim);transition:.25s;
|
||
display:flex;align-items:center;justify-content:center;
|
||
}
|
||
.modal-close:hover{background:rgba(0,0,0,.8);color:var(--text);transform:rotate(90deg)}
|
||
|
||
.modal-header{
|
||
display:flex;gap:32px;padding:40px;
|
||
background:linear-gradient(180deg,rgba(29,185,84,.08),transparent);
|
||
border-bottom:1px solid var(--border);
|
||
}
|
||
.modal-cover{
|
||
width:232px;height:232px;border-radius:var(--radius);
|
||
flex-shrink:0;display:flex;align-items:center;justify-content:center;
|
||
font-size:80px;position:relative;overflow:hidden;
|
||
box-shadow:0 12px 40px rgba(0,0,0,.5);
|
||
}
|
||
.modal-cover::after{
|
||
content:'';position:absolute;inset:0;
|
||
background:linear-gradient(160deg,rgba(255,255,255,.12),transparent 50%);
|
||
}
|
||
.modal-cover .cover-glow{
|
||
position:absolute;inset:-20px;border-radius:50%;
|
||
background:radial-gradient(circle,rgba(29,185,84,.2),transparent 70%);
|
||
z-index:-1;filter:blur(20px);
|
||
}
|
||
.modal-header-info{flex:1;display:flex;flex-direction:column;justify-content:flex-end;min-width:0}
|
||
.modal-badge{
|
||
display:inline-flex;align-items:center;gap:6px;
|
||
font-size:12px;font-weight:600;color:var(--green);
|
||
margin-bottom:8px;
|
||
}
|
||
.modal-title{
|
||
font-size:36px;font-weight:900;letter-spacing:-1px;
|
||
margin-bottom:8px;line-height:1.2;
|
||
}
|
||
.modal-artist{font-size:16px;color:var(--text-dim);margin-bottom:16px}
|
||
.modal-artist .artist-name{color:var(--text);font-weight:600}
|
||
.modal-meta{display:flex;flex-wrap:wrap;gap:16px;font-size:13px;color:var(--text-mute)}
|
||
.modal-meta .meta-item{display:flex;align-items:center;gap:6px}
|
||
.modal-meta .meta-item b{color:var(--text-dim);font-weight:600}
|
||
|
||
.modal-body{padding:32px 40px 40px}
|
||
.modal-tags{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:28px}
|
||
.modal-tag{
|
||
padding:5px 14px;border-radius:16px;
|
||
background:rgba(29,185,84,.1);border:1px solid rgba(29,185,84,.2);
|
||
color:var(--green);font-size:12px;font-weight:600;
|
||
}
|
||
.modal-stats{
|
||
display:flex;gap:32px;padding:20px 0;
|
||
border-top:1px solid var(--border);border-bottom:1px solid var(--border);
|
||
margin-bottom:28px;
|
||
}
|
||
.modal-stat{display:flex;flex-direction:column;gap:4px}
|
||
.modal-stat .stat-val{font-size:24px;font-weight:800;color:var(--text)}
|
||
.modal-stat .stat-val.green{color:var(--green)}
|
||
.modal-stat .stat-label{font-size:12px;color:var(--text-mute)}
|
||
|
||
.modal-lyrics-title{
|
||
font-size:16px;font-weight:700;margin-bottom:16px;
|
||
display:flex;align-items:center;gap:8px;
|
||
}
|
||
.modal-lyrics-title .title-bar{width:3px;height:18px;background:var(--green);border-radius:2px}
|
||
.modal-lyrics{
|
||
font-size:15px;line-height:2.2;color:var(--text-dim);
|
||
white-space:pre-wrap;word-break:break-word;
|
||
padding:20px;border-radius:var(--radius);
|
||
background:var(--bg-glass);border:1px solid var(--border);
|
||
max-height:320px;overflow-y:auto;
|
||
}
|
||
.modal-lyrics::-webkit-scrollbar{width:4px}
|
||
.modal-lyrics::-webkit-scrollbar-thumb{background:rgba(255,255,255,.1);border-radius:2px}
|
||
.modal-lyrics-empty{
|
||
color:var(--text-mute);font-style:italic;text-align:center;padding:20px;
|
||
}
|
||
.modal-actions{
|
||
display:flex;gap:12px;margin-top:28px;
|
||
}
|
||
.modal-action-btn{
|
||
display:flex;align-items:center;gap:8px;
|
||
padding:12px 28px;border-radius:24px;
|
||
font-size:14px;font-weight:700;
|
||
transition:transform .2s,box-shadow .2s;
|
||
}
|
||
.modal-action-btn.primary{
|
||
background:var(--green);color:#000;
|
||
}
|
||
.modal-action-btn.primary:hover{transform:scale(1.05);box-shadow:var(--green-glow-strong)}
|
||
.modal-action-btn.secondary{
|
||
background:var(--bg-glass);border:1px solid var(--border);color:var(--text);
|
||
}
|
||
.modal-action-btn.secondary:hover{background:var(--bg-glass-hover);border-color:var(--border-hover)}
|
||
|
||
/* ============================================================
|
||
滚动揭示动画
|
||
============================================================ */
|
||
.reveal{
|
||
opacity:0;transform:translateY(24px);
|
||
transition:opacity .6s ease,transform .6s ease;
|
||
}
|
||
.reveal.visible{opacity:1;transform:translateY(0)}
|
||
|
||
/* ============================================================
|
||
Toast
|
||
============================================================ */
|
||
.toast-wrap{
|
||
position:fixed;top:80px;right:24px;z-index:999;
|
||
display:flex;flex-direction:column;gap:8px;pointer-events:none;
|
||
}
|
||
.toast{
|
||
padding:12px 20px;border-radius:var(--radius-sm);
|
||
background:var(--bg-card);border:1px solid var(--border);
|
||
color:var(--text);font-size:13px;font-weight:600;
|
||
box-shadow:0 8px 32px rgba(0,0,0,.4);
|
||
opacity:0;transform:translateX(100%);
|
||
transition:opacity .3s,transform .3s;
|
||
display:flex;align-items:center;gap:10px;
|
||
}
|
||
.toast.show{opacity:1;transform:translateX(0)}
|
||
.toast .toast-ico{font-size:16px;color:var(--green)}
|
||
|
||
/* ============================================================
|
||
响应式
|
||
============================================================ */
|
||
@media(max-width:1024px){
|
||
:root{--sidebar-w:72px}
|
||
.sidebar-logo .logo-text{display:none}
|
||
.sidebar-nav .nav-link .label,.sidebar-nav .nav-section-title,.sidebar-playlists,.sidebar-footer{display:none}
|
||
.sidebar-nav .nav-link{justify-content:center;padding:12px}
|
||
.hot-list-header,.hot-track{grid-template-columns:40px 1fr 100px 80px}
|
||
.hot-list-header .col-album,.hot-track .track-album,
|
||
.hot-list-header .col-like,.hot-track .track-like-btn{display:none}
|
||
.lib-list-header,.lib-track{grid-template-columns:40px 1fr 120px 80px}
|
||
.lib-list-header .col-album,.lib-track .track-album,
|
||
.lib-list-header .col-cat,.lib-track .track-cat,
|
||
.lib-list-header .col-likes,.lib-track .track-likes,
|
||
.lib-list-header .col-like,.lib-track .track-like-btn{display:none}
|
||
}
|
||
@media(max-width:768px){
|
||
.player-current{width:200px}
|
||
.player-right{width:auto}
|
||
.visualizer,.volume-wrap{display:none}
|
||
.player-expand{display:none}
|
||
.modal-header{flex-direction:column;align-items:center;text-align:center;gap:20px;padding:24px}
|
||
.modal-cover{width:180px;height:180px;font-size:60px}
|
||
.modal-title{font-size:24px}
|
||
.content{padding:0 16px}
|
||
.hero{margin:16px;border-radius:var(--radius);padding:32px 24px}
|
||
.hero-title{font-size:28px}
|
||
.topbar{padding:12px 16px}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="app">
|
||
|
||
<!-- ==================== 侧边栏 ==================== -->
|
||
<aside class="sidebar">
|
||
<div class="sidebar-logo">
|
||
<div class="logo-icon">Y</div>
|
||
<div class="logo-text">云音<span>Music</span></div>
|
||
</div>
|
||
<nav class="sidebar-nav">
|
||
<div class="nav-section-title label" data-i18n="sidebar_menu">主菜单</div>
|
||
<div class="nav-link active" data-nav="home">
|
||
<span class="ico">⌂</span><span class="label" data-i18n="nav_home">首页</span>
|
||
</div>
|
||
<div class="nav-link" data-nav="discover">
|
||
<span class="ico">⚲</span><span class="label" data-i18n="nav_discover">发现</span>
|
||
</div>
|
||
<div class="nav-link" data-nav="hot">
|
||
<span class="ico">🔥</span><span class="label" data-i18n="nav_hot">热门</span>
|
||
</div>
|
||
<div class="nav-link" data-nav="new">
|
||
<span class="ico">🎉</span><span class="label" data-i18n="nav_new">新歌</span>
|
||
</div>
|
||
</nav>
|
||
<div class="sidebar-playlists">
|
||
<div class="nav-section-title" data-i18n="my_playlists">我的歌单</div>
|
||
<div class="playlist-item">
|
||
<div class="pl-cover" style="background:linear-gradient(135deg,#1db954,#0d4d2a)">♫</div>
|
||
<div class="pl-info"><div class="pl-name" data-i18n="pl_liked">我喜欢的音乐</div><div class="pl-desc" data-i18n="pl_liked_count">256 首</div></div>
|
||
</div>
|
||
<div class="playlist-item">
|
||
<div class="pl-cover" style="background:linear-gradient(135deg,#535353,#1a1a1a)">♬</div>
|
||
<div class="pl-info"><div class="pl-name" data-i18n="pl_night">深夜电台</div><div class="pl-desc" data-i18n="pl_night_count">48 首</div></div>
|
||
</div>
|
||
<div class="playlist-item">
|
||
<div class="pl-cover" style="background:linear-gradient(135deg,#8b5cf6,#3b0764)">♪</div>
|
||
<div class="pl-info"><div class="pl-name" data-i18n="pl_sport">运动节奏</div><div class="pl-desc" data-i18n="pl_sport_count">32 首</div></div>
|
||
</div>
|
||
<div class="playlist-item">
|
||
<div class="pl-cover" style="background:linear-gradient(135deg,#f59e0b,#78350f)">♩</div>
|
||
<div class="pl-info"><div class="pl-name" data-i18n="pl_commute">通勤路上</div><div class="pl-desc" data-i18n="pl_commute_count">64 首</div></div>
|
||
</div>
|
||
</div>
|
||
<div class="sidebar-footer">
|
||
<div class="footer-link" data-i18n="footer_about">关于云音</div>
|
||
<div class="footer-link" data-i18n="footer_privacy">隐私政策</div>
|
||
<div class="footer-link" data-i18n="footer_terms">服务条款</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- ==================== 主内容区 ==================== -->
|
||
<main class="main">
|
||
|
||
<!-- 顶部栏 -->
|
||
<div class="topbar">
|
||
<div class="topbar-nav">
|
||
<button class="nav-btn" id="navBack" title="后退">‹</button>
|
||
<button class="nav-btn" id="navForward" title="前进">›</button>
|
||
</div>
|
||
<div class="search-wrap" id="topSearchWrap">
|
||
<span class="search-ico">⌕</span>
|
||
<input type="text" class="search-input" id="topSearchInput" data-i18n-placeholder="search_placeholder" placeholder="搜索歌曲、歌手、专辑...">
|
||
<span class="search-clear" id="topSearchClear">✕</span>
|
||
</div>
|
||
<div class="topbar-right">
|
||
<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>
|
||
<button class="upgrade-btn" data-i18n="btn_upgrade">升级会员</button>
|
||
<div class="user-avatar" title="个人中心">U</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Hero Banner -->
|
||
<section class="hero reveal" id="heroSection">
|
||
<div class="hero-greeting" id="heroGreeting">下午好</div>
|
||
<h1 class="hero-title" data-i18n="hero_title">发现好音乐</h1>
|
||
<p class="hero-subtitle" data-i18n="hero_subtitle">沉浸暗夜音境,探索属于你的声音世界。从热门榜单到新歌速递,让每一次播放都成为一场旅程。</p>
|
||
<button class="hero-cta" id="heroCta">
|
||
<span>▶</span> <span data-i18n="hero_cta">立即聆听</span>
|
||
</button>
|
||
</section>
|
||
|
||
<div class="content">
|
||
|
||
<!-- 分类筛选 -->
|
||
<section class="section reveal" id="catSection">
|
||
<div class="section-header">
|
||
<h2 class="section-title"><span class="title-bar"></span><span data-i18n="section_category">音乐分类</span></h2>
|
||
</div>
|
||
<div class="cat-scroll" id="catScroll"></div>
|
||
</section>
|
||
|
||
<!-- 热门歌曲 -->
|
||
<section class="section reveal" id="hotSection">
|
||
<div class="section-header">
|
||
<h2 class="section-title"><span class="title-bar"></span><span data-i18n="section_hot">热门歌曲</span></h2>
|
||
<a class="section-link" id="hotSeeAll"><span data-i18n="see_all">查看全部</span> →</a>
|
||
</div>
|
||
<div class="hot-list" id="hotList">
|
||
<div class="hot-list-header">
|
||
<div class="col-center">#</div>
|
||
<div data-i18n="col_title">标题</div>
|
||
<div class="col-album" data-i18n="col_album">专辑</div>
|
||
<div class="col-center" data-i18n="col_plays">播放量</div>
|
||
<div class="col-center" data-i18n="col_duration">时长</div>
|
||
<div class="col-center col-like">♡</div>
|
||
</div>
|
||
<div id="hotListBody"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 新歌速递 -->
|
||
<section class="section reveal" id="newSection">
|
||
<div class="section-header">
|
||
<h2 class="section-title"><span class="title-bar"></span><span data-i18n="section_new">新歌速递</span></h2>
|
||
<a class="section-link" id="newSeeAll"><span data-i18n="see_all">查看全部</span> →</a>
|
||
</div>
|
||
<div class="new-grid" id="newGrid"></div>
|
||
</section>
|
||
|
||
<!-- 全部音乐 -->
|
||
<section class="section reveal" id="allSection">
|
||
<div class="section-header">
|
||
<h2 class="section-title"><span class="title-bar"></span><span data-i18n="section_all">全部音乐</span></h2>
|
||
</div>
|
||
<div class="lib-toolbar">
|
||
<div class="lib-search">
|
||
<span class="ico">⌕</span>
|
||
<input type="text" id="libSearchInput" data-i18n-placeholder="search_placeholder_lib" placeholder="搜索歌曲、歌手、专辑、标签...">
|
||
<span class="spinner" id="libSpinner"></span>
|
||
</div>
|
||
<div class="sort-wrap" id="sortWrap">
|
||
<button class="sort-btn" id="sortBtn">
|
||
<span id="sortLabel" data-i18n="sort_default">默认排序</span><span class="arrow">▼</span>
|
||
</button>
|
||
<div class="sort-menu" id="sortMenu">
|
||
<button data-sort="" class="active" data-i18n="sort_default">默认排序</button>
|
||
<button data-sort="plays" data-i18n="sort_plays">按播放量</button>
|
||
<button data-sort="likes" data-i18n="sort_likes">按收藏量</button>
|
||
<button data-sort="newest" data-i18n="sort_newest">最新发布</button>
|
||
</div>
|
||
</div>
|
||
<div class="lib-info"><span data-i18n="lib_total_prefix">共 </span><b id="libTotal">0</b> <span data-i18n="lib_total_suffix"> 首</span></div>
|
||
</div>
|
||
<div class="lib-list">
|
||
<div class="lib-list-header">
|
||
<div class="col-center">#</div>
|
||
<div data-i18n="col_title">标题</div>
|
||
<div class="col-album" data-i18n="col_album">专辑</div>
|
||
<div class="col-cat" data-i18n="col_genre">曲风</div>
|
||
<div class="col-center" data-i18n="col_plays">播放量</div>
|
||
<div class="col-center col-likes" data-i18n="col_likes">收藏</div>
|
||
<div class="col-center col-like">♡</div>
|
||
</div>
|
||
<div id="libListBody"></div>
|
||
</div>
|
||
<div class="pagination" id="pagination"></div>
|
||
</section>
|
||
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- ==================== 底部播放器栏 ==================== -->
|
||
<div class="player-bar" id="playerBar">
|
||
<!-- 当前歌曲 -->
|
||
<div class="player-current">
|
||
<div class="player-cover" id="playerCover" style="background:linear-gradient(135deg,#535353,#1a1a1a)">
|
||
♫
|
||
</div>
|
||
<div class="player-song-info">
|
||
<div class="player-song-title" id="playerTitle" data-i18n="player_idle_title">未在播放</div>
|
||
<div class="player-song-artist" id="playerArtist" data-i18n="player_idle_artist">选择一首歌曲开始</div>
|
||
</div>
|
||
<div class="player-like" id="playerLike">♡</div>
|
||
</div>
|
||
|
||
<!-- 中间控制 -->
|
||
<div class="player-center">
|
||
<div class="player-controls">
|
||
<button class="player-btn shuffle-btn" id="shuffleBtn" data-i18n-title="ctrl_shuffle" title="随机播放">🔀</button>
|
||
<button class="player-btn" id="prevBtn" data-i18n-title="ctrl_prev" title="上一首">◀◀</button>
|
||
<button class="player-btn play-btn" id="playBtn" data-i18n-title="ctrl_play" title="播放/暂停">▶</button>
|
||
<button class="player-btn" id="nextBtn" data-i18n-title="ctrl_next" title="下一首">▶▶</button>
|
||
<button class="player-btn repeat-btn" id="repeatBtn" data-i18n-title="ctrl_repeat" title="循环播放">🔁</button>
|
||
</div>
|
||
<div class="player-progress">
|
||
<span class="player-time" id="currentTime">0:00</span>
|
||
<div class="progress-bar" id="progressBar">
|
||
<div class="progress-fill" id="progressFill"></div>
|
||
</div>
|
||
<span class="player-time" id="totalTime">0:00</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧控制 -->
|
||
<div class="player-right">
|
||
<div class="visualizer paused" id="visualizer">
|
||
<div class="bar"></div>
|
||
<div class="bar"></div>
|
||
<div class="bar"></div>
|
||
<div class="bar"></div>
|
||
</div>
|
||
<button class="player-btn" id="volumeBtn" data-i18n-title="ctrl_volume" title="音量">🔊</button>
|
||
<div class="volume-wrap">
|
||
<div class="volume-slider" id="volumeSlider">
|
||
<div class="volume-fill" id="volumeFill"></div>
|
||
</div>
|
||
</div>
|
||
<button class="player-expand" id="expandBtn" data-i18n-title="ctrl_expand" title="展开详情">↕</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ==================== 歌曲详情弹窗 ==================== -->
|
||
<div class="modal-overlay" id="modalOverlay">
|
||
<div class="modal" id="modal">
|
||
<button class="modal-close" id="modalClose">✕</button>
|
||
<div class="modal-header" id="modalHeader"></div>
|
||
<div class="modal-body" id="modalBody"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ==================== Toast ==================== -->
|
||
<div class="toast-wrap" id="toastWrap"></div>
|
||
|
||
<script>
|
||
/* ============================================================
|
||
i18n 国际化引擎
|
||
============================================================ */
|
||
const I18N_LOCALES = {
|
||
'zh-CN': {
|
||
title: '云音 Music · 暗黑播放器',
|
||
sidebar_menu: '主菜单',
|
||
nav_home: '首页',
|
||
nav_discover: '发现',
|
||
nav_hot: '热门',
|
||
nav_new: '新歌',
|
||
my_playlists: '我的歌单',
|
||
pl_liked: '我喜欢的音乐',
|
||
pl_liked_count: '256 首',
|
||
pl_night: '深夜电台',
|
||
pl_night_count: '48 首',
|
||
pl_sport: '运动节奏',
|
||
pl_sport_count: '32 首',
|
||
pl_commute: '通勤路上',
|
||
pl_commute_count: '64 首',
|
||
footer_about: '关于云音',
|
||
footer_privacy: '隐私政策',
|
||
footer_terms: '服务条款',
|
||
search_placeholder: '搜索歌曲、歌手、专辑...',
|
||
search_placeholder_lib: '搜索歌曲、歌手、专辑、标签...',
|
||
btn_upgrade: '升级会员',
|
||
hero_greeting_morning: '早上好',
|
||
hero_greeting_noon: '中午好',
|
||
hero_greeting_afternoon: '下午好',
|
||
hero_greeting_evening: '晚上好',
|
||
hero_greeting_night: '夜深了',
|
||
hero_title: '发现好音乐',
|
||
hero_subtitle: '沉浸暗夜音境,探索属于你的声音世界。从热门榜单到新歌速递,让每一次播放都成为一场旅程。',
|
||
hero_cta: '立即聆听',
|
||
section_category: '音乐分类',
|
||
section_hot: '热门歌曲',
|
||
section_new: '新歌速递',
|
||
section_all: '全部音乐',
|
||
see_all: '查看全部',
|
||
col_title: '标题',
|
||
col_album: '专辑',
|
||
col_plays: '播放量',
|
||
col_duration: '时长',
|
||
col_genre: '曲风',
|
||
col_likes: '收藏',
|
||
sort_default: '默认排序',
|
||
sort_plays: '按播放量',
|
||
sort_likes: '按收藏量',
|
||
sort_newest: '最新发布',
|
||
lib_total_prefix: '共',
|
||
lib_total_suffix: '首',
|
||
player_idle_title: '未在播放',
|
||
player_idle_artist: '选择一首歌曲开始',
|
||
ctrl_shuffle: '随机播放',
|
||
ctrl_prev: '上一首',
|
||
ctrl_play: '播放/暂停',
|
||
ctrl_next: '下一首',
|
||
ctrl_repeat: '循环播放',
|
||
ctrl_volume: '音量',
|
||
ctrl_expand: '展开详情',
|
||
cat_all: '全部',
|
||
cat_pop: '流行',
|
||
cat_rock: '摇滚',
|
||
cat_folk: '民谣',
|
||
cat_electronic: '电子',
|
||
cat_classical: '古典',
|
||
cat_hiphop: '嘻哈',
|
||
empty_hot: '暂无热门歌曲',
|
||
empty_new: '暂无新歌',
|
||
empty_search: '没有找到匹配的歌曲',
|
||
modal_artist_label: '歌手:',
|
||
modal_album_label: '专辑:',
|
||
modal_release_label: '发行:',
|
||
modal_language_label: '语种:',
|
||
modal_duration_label: '时长:',
|
||
modal_unknown: '未知',
|
||
modal_plays: '播放量',
|
||
modal_likes: '收藏量',
|
||
modal_duration: '时长',
|
||
modal_release_date: '发行日期',
|
||
modal_lyrics_title: '歌词',
|
||
modal_no_lyrics: '这首歌曲是纯音乐,暂无歌词 🎼',
|
||
modal_play_btn: '播放',
|
||
modal_like_btn: '收藏',
|
||
modal_liked_btn: '已收藏',
|
||
toast_now_playing: '正在播放:',
|
||
toast_cancel_like: '已取消收藏',
|
||
toast_add_like: '已添加到我喜欢',
|
||
toast_shuffle_on: '已开启随机播放',
|
||
toast_shuffle_off: '已关闭随机播放',
|
||
toast_repeat_on: '已开启循环播放',
|
||
toast_repeat_off: '已关闭循环播放',
|
||
toast_open_playlist: '打开歌单:',
|
||
toast_upgrade: '会员功能即将上线,敬请期待',
|
||
toast_select_song: '请先选择一首歌曲',
|
||
tracks_unit: '首',
|
||
},
|
||
'zh-TW': {
|
||
title: '雲音 Music · 暗黑播放器',
|
||
sidebar_menu: '主選單',
|
||
nav_home: '首頁',
|
||
nav_discover: '發現',
|
||
nav_hot: '熱門',
|
||
nav_new: '新歌',
|
||
my_playlists: '我的歌單',
|
||
pl_liked: '我喜歡的音樂',
|
||
pl_liked_count: '256 首',
|
||
pl_night: '深夜電台',
|
||
pl_night_count: '48 首',
|
||
pl_sport: '運動節奏',
|
||
pl_sport_count: '32 首',
|
||
pl_commute: '通勤路上',
|
||
pl_commute_count: '64 首',
|
||
footer_about: '關於雲音',
|
||
footer_privacy: '隱私政策',
|
||
footer_terms: '服務條款',
|
||
search_placeholder: '搜尋歌曲、歌手、專輯...',
|
||
search_placeholder_lib: '搜尋歌曲、歌手、專輯、標籤...',
|
||
btn_upgrade: '升級會員',
|
||
hero_greeting_morning: '早上好',
|
||
hero_greeting_noon: '中午好',
|
||
hero_greeting_afternoon: '下午好',
|
||
hero_greeting_evening: '晚上好',
|
||
hero_greeting_night: '夜深了',
|
||
hero_title: '發現好音樂',
|
||
hero_subtitle: '沉浸暗夜音境,探索屬於你的聲音世界。從熱門榜單到新歌速遞,讓每一次播放都成為一場旅程。',
|
||
hero_cta: '立即聆聽',
|
||
section_category: '音樂分類',
|
||
section_hot: '熱門歌曲',
|
||
section_new: '新歌速遞',
|
||
section_all: '全部音樂',
|
||
see_all: '查看全部',
|
||
col_title: '標題',
|
||
col_album: '專輯',
|
||
col_plays: '播放量',
|
||
col_duration: '時長',
|
||
col_genre: '曲風',
|
||
col_likes: '收藏',
|
||
sort_default: '預設排序',
|
||
sort_plays: '按播放量',
|
||
sort_likes: '按收藏量',
|
||
sort_newest: '最新發布',
|
||
lib_total_prefix: '共',
|
||
lib_total_suffix: '首',
|
||
player_idle_title: '未在播放',
|
||
player_idle_artist: '選擇一首歌曲開始',
|
||
ctrl_shuffle: '隨機播放',
|
||
ctrl_prev: '上一首',
|
||
ctrl_play: '播放/暫停',
|
||
ctrl_next: '下一首',
|
||
ctrl_repeat: '循環播放',
|
||
ctrl_volume: '音量',
|
||
ctrl_expand: '展開詳情',
|
||
cat_all: '全部',
|
||
cat_pop: '流行',
|
||
cat_rock: '搖滾',
|
||
cat_folk: '民謠',
|
||
cat_electronic: '電子',
|
||
cat_classical: '古典',
|
||
cat_hiphop: '嘻哈',
|
||
empty_hot: '暫無熱門歌曲',
|
||
empty_new: '暫無新歌',
|
||
empty_search: '沒有找到匹配的歌曲',
|
||
modal_artist_label: '歌手:',
|
||
modal_album_label: '專輯:',
|
||
modal_release_label: '發行:',
|
||
modal_language_label: '語種:',
|
||
modal_duration_label: '時長:',
|
||
modal_unknown: '未知',
|
||
modal_plays: '播放量',
|
||
modal_likes: '收藏量',
|
||
modal_duration: '時長',
|
||
modal_release_date: '發行日期',
|
||
modal_lyrics_title: '歌詞',
|
||
modal_no_lyrics: '這首歌曲是純音樂,暫無歌詞 🎼',
|
||
modal_play_btn: '播放',
|
||
modal_like_btn: '收藏',
|
||
modal_liked_btn: '已收藏',
|
||
toast_now_playing: '正在播放:',
|
||
toast_cancel_like: '已取消收藏',
|
||
toast_add_like: '已添加到我喜歡',
|
||
toast_shuffle_on: '已開啟隨機播放',
|
||
toast_shuffle_off: '已關閉隨機播放',
|
||
toast_repeat_on: '已開啟循環播放',
|
||
toast_repeat_off: '已關閉循環播放',
|
||
toast_open_playlist: '打開歌單:',
|
||
toast_upgrade: '會員功能即將上線,敬請期待',
|
||
toast_select_song: '請先選擇一首歌曲',
|
||
tracks_unit: '首',
|
||
},
|
||
'en': {
|
||
title: 'YunYin Music · Dark Player',
|
||
sidebar_menu: 'MENU',
|
||
nav_home: 'Home',
|
||
nav_discover: 'Discover',
|
||
nav_hot: 'Hot',
|
||
nav_new: 'New',
|
||
my_playlists: 'MY PLAYLISTS',
|
||
pl_liked: 'Liked Music',
|
||
pl_liked_count: '256 tracks',
|
||
pl_night: 'Late Night Radio',
|
||
pl_night_count: '48 tracks',
|
||
pl_sport: 'Workout Beats',
|
||
pl_sport_count: '32 tracks',
|
||
pl_commute: 'Commute Mix',
|
||
pl_commute_count: '64 tracks',
|
||
footer_about: 'About YunYin',
|
||
footer_privacy: 'Privacy Policy',
|
||
footer_terms: 'Terms of Service',
|
||
search_placeholder: 'Search songs, artists, albums...',
|
||
search_placeholder_lib: 'Search songs, artists, albums, tags...',
|
||
btn_upgrade: 'Upgrade',
|
||
hero_greeting_morning: 'Good morning',
|
||
hero_greeting_noon: 'Good afternoon',
|
||
hero_greeting_afternoon: 'Good afternoon',
|
||
hero_greeting_evening: 'Good evening',
|
||
hero_greeting_night: 'Late night',
|
||
hero_title: 'Discover Great Music',
|
||
hero_subtitle: 'Immerse in the dark audio realm and explore the sound world that belongs to you. From hot charts to new releases, every play becomes a journey.',
|
||
hero_cta: 'Listen Now',
|
||
section_category: 'Genres',
|
||
section_hot: 'Hot Songs',
|
||
section_new: 'New Releases',
|
||
section_all: 'All Music',
|
||
see_all: 'See All',
|
||
col_title: 'TITLE',
|
||
col_album: 'ALBUM',
|
||
col_plays: 'PLAYS',
|
||
col_duration: 'DURATION',
|
||
col_genre: 'GENRE',
|
||
col_likes: 'LIKES',
|
||
sort_default: 'Default',
|
||
sort_plays: 'By Plays',
|
||
sort_likes: 'By Likes',
|
||
sort_newest: 'Newest',
|
||
lib_total_prefix: '',
|
||
lib_total_suffix: ' tracks',
|
||
player_idle_title: 'Nothing Playing',
|
||
player_idle_artist: 'Pick a song to start',
|
||
ctrl_shuffle: 'Shuffle',
|
||
ctrl_prev: 'Previous',
|
||
ctrl_play: 'Play/Pause',
|
||
ctrl_next: 'Next',
|
||
ctrl_repeat: 'Repeat',
|
||
ctrl_volume: 'Volume',
|
||
ctrl_expand: 'Expand Details',
|
||
cat_all: 'All',
|
||
cat_pop: 'Pop',
|
||
cat_rock: 'Rock',
|
||
cat_folk: 'Folk',
|
||
cat_electronic: 'Electronic',
|
||
cat_classical: 'Classical',
|
||
cat_hiphop: 'Hip-Hop',
|
||
empty_hot: 'No hot songs yet',
|
||
empty_new: 'No new releases yet',
|
||
empty_search: 'No songs match your search',
|
||
modal_artist_label: 'Artist: ',
|
||
modal_album_label: 'Album: ',
|
||
modal_release_label: 'Released: ',
|
||
modal_language_label: 'Language: ',
|
||
modal_duration_label: 'Duration: ',
|
||
modal_unknown: 'Unknown',
|
||
modal_plays: 'Plays',
|
||
modal_likes: 'Likes',
|
||
modal_duration: 'Duration',
|
||
modal_release_date: 'Release Date',
|
||
modal_lyrics_title: 'Lyrics',
|
||
modal_no_lyrics: 'This is an instrumental track, no lyrics available 🎼',
|
||
modal_play_btn: 'Play',
|
||
modal_like_btn: 'Like',
|
||
modal_liked_btn: 'Liked',
|
||
toast_now_playing: 'Now playing: ',
|
||
toast_cancel_like: 'Removed from Liked',
|
||
toast_add_like: 'Added to Liked',
|
||
toast_shuffle_on: 'Shuffle on',
|
||
toast_shuffle_off: 'Shuffle off',
|
||
toast_repeat_on: 'Repeat on',
|
||
toast_repeat_off: 'Repeat off',
|
||
toast_open_playlist: 'Opening playlist: ',
|
||
toast_upgrade: 'Premium coming soon, stay tuned',
|
||
toast_select_song: 'Select a song first',
|
||
tracks_unit: '',
|
||
}
|
||
};
|
||
|
||
const LANG_LABELS = {'zh-CN':'中文','en':'EN','zh-TW':'繁中'};
|
||
|
||
const CATEGORY_NAME_KEY = {流行:'cat_pop',摇滚:'cat_rock',民谣:'cat_folk',电子:'cat_electronic',古典:'cat_classical',嘻哈:'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('music_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){
|
||
var key=el.getAttribute('data-i18n');
|
||
el.textContent=t(key);
|
||
});
|
||
document.querySelectorAll('[data-i18n-placeholder]').forEach(function(el){
|
||
var key=el.getAttribute('data-i18n-placeholder');
|
||
el.placeholder=t(key);
|
||
});
|
||
document.querySelectorAll('[data-i18n-title]').forEach(function(el){
|
||
var key=el.getAttribute('data-i18n-title');
|
||
el.title=t(key);
|
||
});
|
||
document.documentElement.lang=currentLang==='zh-TW'?'zh-TW':currentLang==='zh-CN'?'zh-CN':'en';
|
||
document.title = t('title');
|
||
document.getElementById('langLabel').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('music_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');
|
||
});
|
||
|
||
/* ============================================================
|
||
配置
|
||
============================================================ */
|
||
const CONFIG = {
|
||
apiBase: '/api/v1/music',
|
||
useMock: false,
|
||
};
|
||
|
||
/* ============================================================
|
||
模拟数据(API 不可用时回退)
|
||
============================================================ */
|
||
const MOCK = (function(){
|
||
const categories = [
|
||
{id:1,name:'流行',icon:'pop',sort:1},
|
||
{id:2,name:'摇滚',icon:'rock',sort:2},
|
||
{id:3,name:'民谣',icon:'folk',sort:3},
|
||
{id:4,name:'电子',icon:'electronic',sort:4},
|
||
{id:5,name:'古典',icon:'classical',sort:5},
|
||
{id:6,name:'嘻哈',icon:'hiphop',sort:6},
|
||
];
|
||
const songs = [
|
||
{id:1,title:'星海彼岸',artist:'林夕音',album:'星海彼岸',category_id:1,category_name:'流行',cover:'',duration:245,play_count:98000000,like_count:850000,lyrics:'越过星海的距离\n我在彼岸等你\n时光折叠成纸鹤\n飞向你的心里',release_date:'2026-06-15',language:'华语',tags:'["抒情","钢琴","治愈"]',is_hot:true,is_new:true,sort:1,status:1},
|
||
{id:2,title:'霓虹城市',artist:'Echo 乐队',album:'都市光影',category_id:1,category_name:'流行',cover:'',duration:198,play_count:67000000,like_count:520000,lyrics:'霓虹灯下的城市\n我们擦肩而过\n每一盏灯都是一个故事',release_date:'2026-05-20',language:'华语',tags:'["都市","电子流行","动感"]',is_hot:true,is_new:false,sort:2,status:1},
|
||
{id:3,title:'夏日晚风',artist:'苏念',album:'夏日纪',category_id:1,category_name:'流行',cover:'',duration:213,play_count:45000000,like_count:380000,lyrics:'夏天的晚风轻轻吹\n带着栀子花的香味\n我们骑着单车\n追着夕阳跑',release_date:'2026-07-01',language:'华语',tags:'["夏日","清新","轻快"]',is_hot:false,is_new:true,sort:3,status:1},
|
||
{id:4,title:'燃烧的翅膀',artist:'狂焰乐队',album:'烈火纪元',category_id:2,category_name:'摇滚',cover:'',duration:312,play_count:56000000,like_count:680000,lyrics:'展开燃烧的翅膀\n冲破所有阻挡\n就算坠落也要\n飞向太阳的方向',release_date:'2026-04-10',language:'华语',tags:'["硬摇滚","热血","励志"]',is_hot:true,is_new:false,sort:1,status:1},
|
||
{id:5,title:'破碎的镜',artist:'黑曜石',album:'反射',category_id:2,category_name:'摇滚',cover:'',duration:267,play_count:34000000,like_count:420000,lyrics:'破碎的镜中\n映出无数个自己\n哪一个才是真实',release_date:'2026-03-15',language:'华语',tags:'["另类摇滚","深沉","思考"]',is_hot:true,is_new:false,sort:2,status:1},
|
||
{id:6,title:'自由之路',artist:'远征者',album:'无界',category_id:2,category_name:'摇滚',cover:'',duration:289,play_count:28000000,like_count:310000,lyrics:'踏上自由之路\n油门踩到底\n风在耳边呼啸',release_date:'2026-06-28',language:'华语',tags:'["朋克","自由","公路"]',is_hot:false,is_new:true,sort:3,status:1},
|
||
{id:7,title:'故乡的云',artist:'陈木然',album:'远方',category_id:3,category_name:'民谣',cover:'',duration:256,play_count:78000000,like_count:920000,lyrics:'故乡的云\n慢慢地飘\n带走我的思念\n留在远方的家',release_date:'2026-05-01',language:'华语',tags:'["乡愁","吉他","温暖"]',is_hot:true,is_new:false,sort:1,status:1},
|
||
{id:8,title:'老街时光',artist:'江南月',album:'巷子里的歌',category_id:3,category_name:'民谣',cover:'',duration:224,play_count:42000000,like_count:560000,lyrics:'老街的石板路\n刻着岁月的纹\n阿婆的摇椅\n还在门前摇晃',release_date:'2026-04-22',language:'华语',tags:'["怀旧","口琴","叙事"]',is_hot:false,is_new:false,sort:2,status:1},
|
||
{id:9,title:'山野间',artist:'风之声',album:'自然录',category_id:3,category_name:'民谣',cover:'',duration:198,play_count:31000000,like_count:410000,lyrics:'山野间的风\n带着青草的味道\n溪水潺潺流过\n鸟儿在歌唱',release_date:'2026-07-05',language:'华语',tags:'["自然","竹笛","宁静"]',is_hot:false,is_new:true,sort:3,status:1},
|
||
{id:10,title:'赛博梦境',artist:'Neon Pulse',album:'Digital Dreams',category_id:4,category_name:'电子',cover:'',duration:345,play_count:89000000,like_count:750000,lyrics:'',release_date:'2026-06-18',language:'纯音乐',tags:'["合成器","赛博朋克","氛围"]',is_hot:true,is_new:true,sort:1,status:1},
|
||
{id:11,title:'脉冲星',artist:'Synthwave Lab',album:'Cosmic Frequencies',category_id:4,category_name:'电子',cover:'',duration:278,play_count:52000000,like_count:480000,lyrics:'',release_date:'2026-05-10',language:'纯音乐',tags:'["电子舞曲","太空","律动"]',is_hot:true,is_new:false,sort:2,status:1},
|
||
{id:12,title:'数据洪流',artist:'CodeBeat',album:'Binary',category_id:4,category_name:'电子',cover:'',duration:234,play_count:36000000,like_count:320000,lyrics:'',release_date:'2026-07-08',language:'纯音乐',tags:'["Glitch","科技","实验"]',is_hot:false,is_new:true,sort:3,status:1},
|
||
{id:13,title:'月光奏鸣曲 (改编)',artist:'李云朗',album:'古典新声',category_id:5,category_name:'古典',cover:'',duration:420,play_count:65000000,like_count:880000,lyrics:'',release_date:'2026-03-20',language:'纯音乐',tags:'["钢琴","贝多芬","改编"]',is_hot:true,is_new:false,sort:1,status:1},
|
||
{id:14,title:'春之圆舞曲',artist:'维也纳爱乐',album:'四季',category_id:5,category_name:'古典',cover:'',duration:387,play_count:43000000,like_count:560000,lyrics:'',release_date:'2026-04-05',language:'纯音乐',tags:'["管弦乐","圆舞曲","经典"]',is_hot:false,is_new:false,sort:2,status:1},
|
||
{id:15,title:'二泉映月',artist:'华彦钧',album:'中国传统名曲',category_id:5,category_name:'古典',cover:'',duration:512,play_count:38000000,like_count:620000,lyrics:'',release_date:'2026-01-15',language:'纯音乐',tags:'["二胡","传统","名曲"]',is_hot:false,is_new:false,sort:3,status:1},
|
||
{id:16,title:'街头诗人',artist:'MC 锋芒',album:'地下之声',category_id:6,category_name:'嘻哈',cover:'',duration:187,play_count:72000000,like_count:650000,lyrics:'我是街头诗人\n用韵脚写日记\n每一句都是生活\n每一个节拍都是心跳',release_date:'2026-06-25',language:'华语',tags:'["说唱","街头","真实"]',is_hot:true,is_new:true,sort:1,status:1},
|
||
{id:17,title:'不眠夜',artist:'Trap Master',album:'Midnight',category_id:6,category_name:'嘻哈',cover:'',duration:165,play_count:48000000,like_count:430000,lyrics:'Midnight city lights\nWe do not sleep tonight\nBeat drops heavy\nFlow so tight',release_date:'2026-05-15',language:'英语',tags:'["Trap","夜店","律动"]',is_hot:true,is_new:false,sort:2,status:1},
|
||
{id:18,title:'逐梦',artist:'新生代',album:'破茧',category_id:6,category_name:'嘻哈',cover:'',duration:198,play_count:35000000,like_count:390000,lyrics:'从地下到舞台\n每一步都不容易\n但梦想从不骗人\n只要你敢追',release_date:'2026-07-03',language:'华语',tags:'["励志","Flow","新生代"]',is_hot:false,is_new:true,sort:3,status:1},
|
||
];
|
||
|
||
return {
|
||
home(){ return {categories, hot: songs.filter(s=>s.is_hot).slice(0,8), new: songs.filter(s=>s.is_new).slice(0,4)}; },
|
||
categories(){ return categories; },
|
||
hot(limit){ return songs.filter(s=>s.is_hot).sort((a,b)=>b.play_count-a.play_count).slice(0,limit||8); },
|
||
news(limit){ return songs.filter(s=>s.is_new).sort((a,b)=>new Date(b.release_date)-new Date(a.release_date)).slice(0,limit||4); },
|
||
query(params){
|
||
let list = songs.filter(s=>s.status===1);
|
||
if(params.category_id){ list = list.filter(s=>s.category_id==params.category_id); }
|
||
if(params.keyword){
|
||
const kw = params.keyword.toLowerCase();
|
||
list = list.filter(s=>s.title.toLowerCase().includes(kw)||s.artist.toLowerCase().includes(kw)||s.album.toLowerCase().includes(kw)||s.tags.toLowerCase().includes(kw));
|
||
}
|
||
const sort = params.sort||'';
|
||
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 ps = params.page_size||12;
|
||
const pg = params.page||1;
|
||
const start = (pg-1)*ps;
|
||
return {list:list.slice(start,start+ps),total,page:pg,page_size:ps};
|
||
},
|
||
detail(id){ return songs.find(s=>s.id==id)||null; },
|
||
};
|
||
})();
|
||
|
||
/* ============================================================
|
||
全局状态
|
||
============================================================ */
|
||
const state = {
|
||
categories: [],
|
||
hotSongs: [],
|
||
newSongs: [],
|
||
library: {
|
||
list: [], total: 0, page: 1, page_size: 8,
|
||
keyword: '', sort: '', category_id: '',
|
||
},
|
||
player: {
|
||
currentSong: null,
|
||
isPlaying: false,
|
||
currentTime: 0,
|
||
duration: 0,
|
||
volume: 0.7,
|
||
liked: new Set(),
|
||
shuffle: false,
|
||
repeat: false,
|
||
progressTimer: null,
|
||
},
|
||
};
|
||
|
||
/* ============================================================
|
||
工具函数
|
||
============================================================ */
|
||
function formatNumber(n){
|
||
if(n==null||isNaN(n)) return '0';
|
||
n = Number(n);
|
||
if(currentLang==='en'){
|
||
if(n>=1000000000) return (n/1000000000).toFixed(1).replace(/\.0$/,'')+'B';
|
||
if(n>=1000000) return (n/1000000).toFixed(1).replace(/\.0$/,'')+'M';
|
||
if(n>=1000) return (n/1000).toFixed(1).replace(/\.0$/,'')+'K';
|
||
return String(n);
|
||
}
|
||
if(n>=100000000) return (n/100000000).toFixed(1).replace(/\.0$/,'')+'亿';
|
||
if(n>=10000) return (n/10000).toFixed(1).replace(/\.0$/,'')+'万';
|
||
if(n>=1000) return n.toLocaleString();
|
||
return String(n);
|
||
}
|
||
|
||
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 parseJSON(str, fallback){
|
||
if(!str) return fallback||[];
|
||
try{ return JSON.parse(str); }
|
||
catch(e){ return fallback||[]; }
|
||
}
|
||
|
||
function escapeHtml(str){
|
||
if(str==null) return '';
|
||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||
}
|
||
|
||
/* 根据歌曲标题哈希生成渐变封面 */
|
||
function coverGradient(song){
|
||
const str = (song && (song.title||song.artist||'music'))+'';
|
||
let hash = 0;
|
||
for(let i=0;i<str.length;i++){
|
||
hash = str.charCodeAt(i) + ((hash<<5)-hash);
|
||
hash = hash & hash;
|
||
}
|
||
const hue1 = Math.abs(hash)%360;
|
||
const hue2 = (hue1+60)%360;
|
||
const hue3 = (hue1+120)%360;
|
||
return `linear-gradient(135deg,hsl(${hue1},65%,45%),hsl(${hue2},60%,35%),hsl(${hue3},55%,25%))`;
|
||
}
|
||
|
||
/* 封面图标(根据曲风选择) */
|
||
function coverIcon(song){
|
||
const cat = (song&&song.category_name)||'';
|
||
const map = {'流行':'♫','摇滚':'🎘','民谣':'🎸','电子':'🔬','古典':'🎼','嘻哈':'🎤'};
|
||
return map[cat]||'♫';
|
||
}
|
||
|
||
function debounce(fn, delay){
|
||
let timer = null;
|
||
return function(){
|
||
const ctx = this, args = arguments;
|
||
clearTimeout(timer);
|
||
timer = setTimeout(()=>fn.apply(ctx,args), delay);
|
||
};
|
||
}
|
||
|
||
function showToast(msg, ico){
|
||
const wrap = document.getElementById('toastWrap');
|
||
const el = document.createElement('div');
|
||
el.className = 'toast';
|
||
el.innerHTML = `<span class="toast-ico">${ico||'✓'}</span><span>${escapeHtml(msg)}</span>`;
|
||
wrap.appendChild(el);
|
||
requestAnimationFrame(()=>el.classList.add('show'));
|
||
setTimeout(()=>{
|
||
el.classList.remove('show');
|
||
setTimeout(()=>el.remove(),300);
|
||
},2400);
|
||
}
|
||
|
||
/* ============================================================
|
||
API 层
|
||
============================================================ */
|
||
async function apiGet(path, params){
|
||
let url = CONFIG.apiBase + path;
|
||
if(params){
|
||
const qs = new URLSearchParams();
|
||
Object.entries(params).forEach(([k,v])=>{ if(v!==''&&v!=null) qs.set(k,v); });
|
||
const s = qs.toString();
|
||
if(s) url += (url.includes('?')?'&':'?')+s;
|
||
}
|
||
const res = await fetch(url, {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;
|
||
}
|
||
|
||
async function fetchHome(){
|
||
try{ return await apiGet('/home'); }
|
||
catch(e){ CONFIG.useMock = true; return MOCK.home(); }
|
||
}
|
||
async function fetchCategories(){
|
||
try{ const d = await apiGet('/categories'); return Array.isArray(d)?d:(d.categories||[]); }
|
||
catch(e){ return MOCK.categories(); }
|
||
}
|
||
async function fetchHot(limit){
|
||
try{ return await apiGet('/hot',{limit}); }
|
||
catch(e){ return MOCK.hot(limit); }
|
||
}
|
||
async function fetchNew(limit){
|
||
try{ return await apiGet('/new',{limit}); }
|
||
catch(e){ return MOCK.news(limit); }
|
||
}
|
||
async function fetchSongs(params){
|
||
try{ return await apiGet('/songs',params); }
|
||
catch(e){ return MOCK.query(params); }
|
||
}
|
||
async function fetchSongDetail(id){
|
||
try{ return await apiGet('/songs/'+id); }
|
||
catch(e){ return MOCK.detail(id); }
|
||
}
|
||
|
||
/* ============================================================
|
||
渲染:分类
|
||
============================================================ */
|
||
function renderCategories(cats){
|
||
state.categories = cats;
|
||
const wrap = document.getElementById('catScroll');
|
||
const icons = {'流行':'🎵','摇滚':'🎘','民谣':'🎸','电子':'🔬','古典':'🎼','嘻哈':'🎤'};
|
||
const all = [{id:'',name:t('cat_all'),icon:'🅏'}].concat(cats);
|
||
wrap.innerHTML = all.map(c=>{
|
||
const nameKey = CATEGORY_NAME_KEY[c.name];
|
||
const displayName = nameKey ? t(nameKey) : c.name;
|
||
return `
|
||
<button class="cat-pill ${state.library.category_id==c.id?'active':''}" data-cat="${c.id}">
|
||
<span class="pill-ico">${icons[c.name]||c.icon||'♫'}</span>${escapeHtml(displayName)}
|
||
</button>`;
|
||
}).join('');
|
||
wrap.querySelectorAll('.cat-pill').forEach(btn=>{
|
||
btn.addEventListener('click', ()=>{
|
||
const cid = btn.dataset.cat;
|
||
state.library.category_id = cid;
|
||
state.library.page = 1;
|
||
wrap.querySelectorAll('.cat-pill').forEach(b=>b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
loadLibrary();
|
||
document.getElementById('allSection').scrollIntoView({behavior:'smooth',block:'start'});
|
||
});
|
||
});
|
||
}
|
||
|
||
/* ============================================================
|
||
渲染:热门歌曲列表
|
||
============================================================ */
|
||
function renderHotList(songs){
|
||
const body = document.getElementById('hotListBody');
|
||
if(!songs||songs.length===0){
|
||
body.innerHTML = '<div class="lib-empty"><div class="empty-ico">🎵</div><div class="empty-text">'+escapeHtml(t('empty_hot'))+'</div></div>';
|
||
return;
|
||
}
|
||
body.innerHTML = songs.map((s,i)=>{
|
||
const grad = coverGradient(s);
|
||
const isPlaying = state.player.currentSong && state.player.currentSong.id===s.id;
|
||
const isLiked = state.player.liked.has(s.id);
|
||
return `
|
||
<div class="hot-track ${isPlaying?'playing':''}" data-id="${s.id}">
|
||
<div class="track-rank ${i<3?'top-3':''}">
|
||
<span class="rank-num">${i+1}</span>
|
||
<span class="play-icon">▶</span>
|
||
</div>
|
||
<div class="track-info">
|
||
<div class="track-cover" style="background:${grad||'#333'}">${s.cover?`<img src="${escapeHtml(s.cover)}" alt="" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover" onerror="this.style.display='none'">`:''}${coverIcon(s)}</div>
|
||
<div class="track-meta">
|
||
<div class="track-title">${escapeHtml(s.title)}</div>
|
||
<div class="track-artist">${escapeHtml(s.artist)}</div>
|
||
</div>
|
||
</div>
|
||
<div class="track-album">${escapeHtml(s.album||'')}</div>
|
||
<div class="track-plays">${formatNumber(s.play_count)}</div>
|
||
<div class="track-duration">${formatDuration(s.duration)}</div>
|
||
<div class="track-like-btn ${isLiked?'liked':''}" data-like="${s.id}">${isLiked?'♥':'♡'}</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
bindTrackEvents(body);
|
||
}
|
||
|
||
/* ============================================================
|
||
渲染:新歌卡片
|
||
============================================================ */
|
||
function renderNewGrid(songs){
|
||
const grid = document.getElementById('newGrid');
|
||
if(!songs||songs.length===0){
|
||
grid.innerHTML = '<div class="lib-empty" style="grid-column:1/-1"><div class="empty-ico">🎉</div><div class="empty-text">'+escapeHtml(t('empty_new'))+'</div></div>';
|
||
return;
|
||
}
|
||
grid.innerHTML = songs.map(s=>{
|
||
const grad = coverGradient(s);
|
||
const isPlaying = state.player.currentSong && state.player.currentSong.id===s.id;
|
||
return `
|
||
<div class="new-card" data-id="${s.id}">
|
||
<div class="cover-wrap">
|
||
<div class="card-cover" style="background:${grad||'#333'}">${s.cover?`<img src="${escapeHtml(s.cover)}" alt="" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover" onerror="this.style.display='none'">`:''}${coverIcon(s)}</div>
|
||
${s.is_new?'<span class="badge-new">NEW</span>':''}
|
||
<div class="play-overlay">▶</div>
|
||
</div>
|
||
<div class="card-title">${escapeHtml(s.title)}</div>
|
||
<div class="card-artist">${escapeHtml(s.artist)}</div>
|
||
<div class="card-date">${escapeHtml(s.release_date||'')}</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
grid.querySelectorAll('.new-card').forEach(card=>{
|
||
card.addEventListener('click', ()=>{
|
||
const id = card.dataset.id;
|
||
const song = state.newSongs.find(s=>String(s.id)===String(id)) || state.hotSongs.find(s=>String(s.id)===String(id));
|
||
if(song) playSong(song);
|
||
});
|
||
});
|
||
}
|
||
|
||
/* ============================================================
|
||
渲染:全部音乐列表
|
||
============================================================ */
|
||
function renderLibrary(data){
|
||
state.library.list = data.list||[];
|
||
state.library.total = data.total||0;
|
||
document.getElementById('libTotal').textContent = formatNumber(state.library.total);
|
||
const body = document.getElementById('libListBody');
|
||
if(state.library.list.length===0){
|
||
body.innerHTML = '<div class="lib-empty"><div class="empty-ico">⌕</div><div class="empty-text">'+escapeHtml(t('empty_search'))+'</div></div>';
|
||
document.getElementById('pagination').innerHTML = '';
|
||
return;
|
||
}
|
||
body.innerHTML = state.library.list.map((s,i)=>{
|
||
const grad = coverGradient(s);
|
||
const isPlaying = state.player.currentSong && state.player.currentSong.id===s.id;
|
||
const isLiked = state.player.liked.has(s.id);
|
||
return `
|
||
<div class="lib-track ${isPlaying?'playing':''}" data-id="${s.id}">
|
||
<div class="track-rank">
|
||
<span class="rank-num">${(state.library.page-1)*state.library.page_size+i+1}</span>
|
||
<span class="play-icon">▶</span>
|
||
</div>
|
||
<div class="track-info">
|
||
<div class="track-cover" style="background:${grad||'#333'}">${s.cover?`<img src="${escapeHtml(s.cover)}" alt="" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover" onerror="this.style.display='none'">`:''}${coverIcon(s)}</div>
|
||
<div class="track-meta">
|
||
<div class="track-title">${escapeHtml(s.title)}</div>
|
||
<div class="track-artist">${escapeHtml(s.artist)}</div>
|
||
</div>
|
||
</div>
|
||
<div class="track-album">${escapeHtml(s.album||'')}</div>
|
||
<div class="track-cat">${escapeHtml(s.category_name||'')}</div>
|
||
<div class="track-plays">${formatNumber(s.play_count)}</div>
|
||
<div class="track-likes">${formatNumber(s.like_count)}</div>
|
||
<div class="track-like-btn ${isLiked?'liked':''}" data-like="${s.id}">${isLiked?'♥':'♡'}</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
bindTrackEvents(body);
|
||
renderPagination(data);
|
||
}
|
||
|
||
/* 绑定歌曲行事件 */
|
||
function bindTrackEvents(container){
|
||
container.querySelectorAll('[data-id]').forEach(row=>{
|
||
row.addEventListener('click', (e)=>{
|
||
if(e.target.closest('[data-like]')) return;
|
||
const id = row.dataset.id;
|
||
const song = findSongById(id);
|
||
if(song) playSong(song);
|
||
});
|
||
row.addEventListener('dblclick', ()=>{
|
||
const id = row.dataset.id;
|
||
const song = findSongById(id);
|
||
if(song) openSongModal(song);
|
||
});
|
||
});
|
||
container.querySelectorAll('[data-like]').forEach(btn=>{
|
||
btn.addEventListener('click', (e)=>{
|
||
e.stopPropagation();
|
||
const id = parseInt(btn.dataset.like);
|
||
toggleLike(id, btn);
|
||
});
|
||
});
|
||
}
|
||
|
||
function findSongById(id){
|
||
const all = [].concat(state.hotSongs, state.newSongs, state.library.list);
|
||
return all.find(s=>String(s.id)===String(id));
|
||
}
|
||
|
||
function toggleLike(id, btn){
|
||
if(state.player.liked.has(id)){
|
||
state.player.liked.delete(id);
|
||
if(btn){ btn.classList.remove('liked'); btn.innerHTML = '♡'; }
|
||
showToast(t('toast_cancel_like'), '♡');
|
||
}else{
|
||
state.player.liked.add(id);
|
||
if(btn){ btn.classList.add('liked'); btn.innerHTML = '♥'; }
|
||
showToast(t('toast_add_like'), '♥');
|
||
}
|
||
if(state.player.currentSong && state.player.currentSong.id===id){
|
||
updatePlayerLikeBtn();
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
分页
|
||
============================================================ */
|
||
function renderPagination(data){
|
||
const wrap = document.getElementById('pagination');
|
||
const total = data.total||0;
|
||
const ps = data.page_size||state.library.page_size;
|
||
const current = data.page||state.library.page;
|
||
const totalPages = Math.ceil(total/ps);
|
||
if(totalPages<=1){ wrap.innerHTML = ''; return; }
|
||
|
||
let html = '';
|
||
html += `<button class="page-btn" data-page="${current-1}" ${current<=1?'disabled':''}>‹</button>`;
|
||
|
||
const maxVisible = 5;
|
||
let start = Math.max(1, current-2);
|
||
let end = Math.min(totalPages, start+maxVisible-1);
|
||
if(end-start < maxVisible-1) start = Math.max(1, end-maxVisible+1);
|
||
|
||
if(start>1){
|
||
html += `<button class="page-btn" data-page="1">1</button>`;
|
||
if(start>2) html += `<span class="page-ellipsis">...</span>`;
|
||
}
|
||
for(let i=start;i<=end;i++){
|
||
html += `<button class="page-btn ${i===current?'active':''}" data-page="${i}">${i}</button>`;
|
||
}
|
||
if(end<totalPages){
|
||
if(end<totalPages-1) html += `<span class="page-ellipsis">...</span>`;
|
||
html += `<button class="page-btn" data-page="${totalPages}">${totalPages}</button>`;
|
||
}
|
||
|
||
html += `<button class="page-btn" data-page="${current+1}" ${current>=totalPages?'disabled':''}>›</button>`;
|
||
wrap.innerHTML = html;
|
||
|
||
wrap.querySelectorAll('.page-btn[data-page]').forEach(btn=>{
|
||
btn.addEventListener('click', ()=>{
|
||
if(btn.disabled) return;
|
||
const p = parseInt(btn.dataset.page);
|
||
if(p>=1 && p<=totalPages && p!==current){
|
||
state.library.page = p;
|
||
loadLibrary(true);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/* ============================================================
|
||
骨架屏
|
||
============================================================ */
|
||
function skeletonHotList(count){
|
||
let html = '';
|
||
for(let i=0;i<count;i++){
|
||
html += `<div class="skel-track">
|
||
<div class="skel-rank skel-shimmer"></div>
|
||
<div style="display:flex;gap:12px;align-items:center">
|
||
<div class="skel-box skel-shimmer" style="width:44px;height:44px"></div>
|
||
<div style="flex:1">
|
||
<div class="skel-line skel-shimmer mid"></div>
|
||
<div class="skel-line skel-shimmer short" style="margin-top:6px"></div>
|
||
</div>
|
||
</div>
|
||
<div class="skel-line skel-shimmer" style="width:80%"></div>
|
||
<div class="skel-box skel-shimmer" style="width:60px;height:14px;margin:0 auto"></div>
|
||
<div class="skel-box skel-shimmer" style="width:40px;height:14px;margin:0 auto"></div>
|
||
<div class="skel-box skel-shimmer" style="width:16px;height:16px;margin:0 auto"></div>
|
||
</div>`;
|
||
}
|
||
return html;
|
||
}
|
||
|
||
function skeletonNewGrid(count){
|
||
let html = '';
|
||
for(let i=0;i<count;i++){
|
||
html += `<div class="skel-card">
|
||
<div class="skel-cover skel-shimmer"></div>
|
||
<div class="skel-line skel-shimmer" style="width:80%"></div>
|
||
<div class="skel-line skel-shimmer" style="width:50%"></div>
|
||
</div>`;
|
||
}
|
||
return html;
|
||
}
|
||
|
||
function skeletonLibList(count){
|
||
let html = '';
|
||
for(let i=0;i<count;i++){
|
||
html += `<div class="skel-track" style="grid-template-columns:50px 1fr 180px 100px 80px 80px 50px">
|
||
<div class="skel-rank skel-shimmer"></div>
|
||
<div style="display:flex;gap:12px;align-items:center">
|
||
<div class="skel-box skel-shimmer" style="width:44px;height:44px"></div>
|
||
<div style="flex:1">
|
||
<div class="skel-line skel-shimmer mid"></div>
|
||
<div class="skel-line skel-shimmer short" style="margin-top:6px"></div>
|
||
</div>
|
||
</div>
|
||
<div class="skel-line skel-shimmer" style="width:70%"></div>
|
||
<div class="skel-box skel-shimmer" style="width:50px;height:14px;margin:0 auto"></div>
|
||
<div class="skel-box skel-shimmer" style="width:60px;height:14px;margin:0 auto"></div>
|
||
<div class="skel-box skel-shimmer" style="width:40px;height:14px;margin:0 auto"></div>
|
||
<div class="skel-box skel-shimmer" style="width:16px;height:16px;margin:0 auto"></div>
|
||
</div>`;
|
||
}
|
||
return html;
|
||
}
|
||
|
||
function skeletonPagination(){
|
||
return `<div class="skel-pagination"><div class="skel-box skel-shimmer"></div><div class="skel-box skel-shimmer"></div><div class="skel-box skel-shimmer"></div></div>`;
|
||
}
|
||
|
||
/* ============================================================
|
||
加载数据
|
||
============================================================ */
|
||
async function loadHome(){
|
||
document.getElementById('hotListBody').innerHTML = skeletonHotList(6);
|
||
document.getElementById('newGrid').innerHTML = skeletonNewGrid(4);
|
||
const data = await fetchHome();
|
||
if(data.categories) renderCategories(data.categories);
|
||
else await loadCategories();
|
||
if(data.hot){ state.hotSongs = data.hot; renderHotList(data.hot); }
|
||
if(data.new){ state.newSongs = data.new; renderNewGrid(data.new); }
|
||
}
|
||
|
||
async function loadCategories(){
|
||
const cats = await fetchCategories();
|
||
renderCategories(cats);
|
||
}
|
||
|
||
async function loadHot(limit){
|
||
document.getElementById('hotListBody').innerHTML = skeletonHotList(6);
|
||
const songs = await fetchHot(limit||8);
|
||
state.hotSongs = songs;
|
||
renderHotList(songs);
|
||
}
|
||
|
||
async function loadNew(limit){
|
||
document.getElementById('newGrid').innerHTML = skeletonNewGrid(4);
|
||
const songs = await fetchNew(limit||4);
|
||
state.newSongs = songs;
|
||
renderNewGrid(songs);
|
||
}
|
||
|
||
async function loadLibrary(scrollTop){
|
||
const body = document.getElementById('libListBody');
|
||
body.innerHTML = skeletonLibList(state.library.page_size);
|
||
document.getElementById('pagination').innerHTML = skeletonPagination();
|
||
const data = await fetchSongs({
|
||
category_id: state.library.category_id,
|
||
keyword: state.library.keyword,
|
||
sort: state.library.sort,
|
||
page: state.library.page,
|
||
page_size: state.library.page_size,
|
||
});
|
||
renderLibrary(data);
|
||
if(scrollTop){
|
||
document.getElementById('allSection').scrollIntoView({behavior:'smooth',block:'start'});
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
播放器逻辑
|
||
============================================================ */
|
||
function playSong(song){
|
||
state.player.currentSong = song;
|
||
state.player.currentTime = 0;
|
||
state.player.duration = song.duration||0;
|
||
state.player.isPlaying = true;
|
||
|
||
const grad = coverGradient(song);
|
||
const cover = document.getElementById('playerCover');
|
||
cover.style.background = grad||'linear-gradient(135deg,#535353,#1a1a1a)';
|
||
cover.innerHTML = (song.cover?`<img src="${escapeHtml(song.cover)}" alt="" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover" onerror="this.style.display='none'">`:'') + coverIcon(song);
|
||
document.getElementById('playerTitle').textContent = song.title;
|
||
document.getElementById('playerArtist').textContent = song.artist;
|
||
document.getElementById('totalTime').textContent = formatDuration(song.duration);
|
||
document.getElementById('currentTime').textContent = '0:00';
|
||
document.getElementById('progressFill').style.width = '0%';
|
||
|
||
document.getElementById('playBtn').innerHTML = '❚❚';
|
||
document.getElementById('visualizer').classList.remove('paused');
|
||
|
||
updatePlayerLikeBtn();
|
||
|
||
document.querySelectorAll('.hot-track,.lib-track').forEach(row=>{
|
||
row.classList.toggle('playing', String(row.dataset.id)===String(song.id));
|
||
});
|
||
|
||
startProgress();
|
||
|
||
showToast(t('toast_now_playing')+song.title, '▶');
|
||
}
|
||
|
||
function togglePlay(){
|
||
if(!state.player.currentSong){
|
||
if(state.hotSongs.length>0){ playSong(state.hotSongs[0]); }
|
||
return;
|
||
}
|
||
state.player.isPlaying = !state.player.isPlaying;
|
||
const btn = document.getElementById('playBtn');
|
||
if(state.player.isPlaying){
|
||
btn.innerHTML = '❚❚';
|
||
document.getElementById('visualizer').classList.remove('paused');
|
||
startProgress();
|
||
}else{
|
||
btn.innerHTML = '▶';
|
||
document.getElementById('visualizer').classList.add('paused');
|
||
stopProgress();
|
||
}
|
||
}
|
||
|
||
function startProgress(){
|
||
stopProgress();
|
||
state.player.progressTimer = setInterval(()=>{
|
||
if(!state.player.isPlaying) return;
|
||
state.player.currentTime += 1;
|
||
if(state.player.currentTime >= state.player.duration){
|
||
state.player.currentTime = 0;
|
||
if(state.player.repeat){
|
||
}else{
|
||
nextSong();
|
||
return;
|
||
}
|
||
}
|
||
updateProgressUI();
|
||
},1000);
|
||
}
|
||
|
||
function stopProgress(){
|
||
if(state.player.progressTimer){
|
||
clearInterval(state.player.progressTimer);
|
||
state.player.progressTimer = null;
|
||
}
|
||
}
|
||
|
||
function updateProgressUI(){
|
||
const pct = state.player.duration>0 ? (state.player.currentTime/state.player.duration)*100 : 0;
|
||
document.getElementById('progressFill').style.width = pct+'%';
|
||
document.getElementById('currentTime').textContent = formatDuration(state.player.currentTime);
|
||
}
|
||
|
||
function nextSong(){
|
||
const all = [].concat(state.hotSongs, state.newSongs, state.library.list);
|
||
if(all.length===0) return;
|
||
if(state.player.shuffle){
|
||
const idx = Math.floor(Math.random()*all.length);
|
||
playSong(all[idx]);
|
||
return;
|
||
}
|
||
const currentId = state.player.currentSong ? state.player.currentSong.id : -1;
|
||
const idx = all.findIndex(s=>s.id===currentId);
|
||
const nextIdx = (idx+1)%all.length;
|
||
playSong(all[nextIdx]);
|
||
}
|
||
|
||
function prevSong(){
|
||
const all = [].concat(state.hotSongs, state.newSongs, state.library.list);
|
||
if(all.length===0) return;
|
||
const currentId = state.player.currentSong ? state.player.currentSong.id : -1;
|
||
const idx = all.findIndex(s=>s.id===currentId);
|
||
const prevIdx = (idx-1+all.length)%all.length;
|
||
playSong(all[prevIdx]);
|
||
}
|
||
|
||
function updatePlayerLikeBtn(){
|
||
const btn = document.getElementById('playerLike');
|
||
if(state.player.currentSong && state.player.liked.has(state.player.currentSong.id)){
|
||
btn.classList.add('liked');
|
||
btn.innerHTML = '♥';
|
||
}else{
|
||
btn.classList.remove('liked');
|
||
btn.innerHTML = '♡';
|
||
}
|
||
}
|
||
|
||
/* 进度条点击跳转 */
|
||
function seekProgress(e){
|
||
if(!state.player.currentSong) return;
|
||
const bar = document.getElementById('progressBar');
|
||
const rect = bar.getBoundingClientRect();
|
||
const pct = Math.max(0, Math.min(1, (e.clientX-rect.left)/rect.width));
|
||
state.player.currentTime = Math.floor(pct*state.player.duration);
|
||
updateProgressUI();
|
||
}
|
||
|
||
/* 音量控制 */
|
||
function setVolume(pct){
|
||
pct = Math.max(0, Math.min(1, pct));
|
||
state.player.volume = pct;
|
||
document.getElementById('volumeFill').style.width = (pct*100)+'%';
|
||
const btn = document.getElementById('volumeBtn');
|
||
if(pct===0) btn.innerHTML = '🔇';
|
||
else if(pct<0.5) btn.innerHTML = '🔉';
|
||
else btn.innerHTML = '🔊';
|
||
}
|
||
|
||
/* ============================================================
|
||
歌曲详情弹窗
|
||
============================================================ */
|
||
async function openSongModal(song){
|
||
let detail = song;
|
||
try{ const d = await fetchSongDetail(song.id); if(d) detail = d; }catch(e){}
|
||
|
||
const grad = coverGradient(detail);
|
||
const tags = parseJSON(detail.tags, []);
|
||
const lyrics = (detail.lyrics||'').trim();
|
||
const isLiked = state.player.liked.has(detail.id);
|
||
|
||
document.getElementById('modalHeader').innerHTML = `
|
||
<div class="modal-cover" style="background:${grad||'#333'}">
|
||
<div class="cover-glow"></div>
|
||
${detail.cover?`<img src="${escapeHtml(detail.cover)}" alt="" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover" onerror="this.style.display='none'">`:''}
|
||
${coverIcon(detail)}
|
||
</div>
|
||
<div class="modal-header-info">
|
||
<div class="modal-badge">${detail.is_hot?'🔥 '+escapeHtml(t('section_hot')):''} ${detail.is_new?'🎉 '+escapeHtml(t('section_new')):''} ${detail.category_name?'• '+escapeHtml(detail.category_name):''}</div>
|
||
<h2 class="modal-title">${escapeHtml(detail.title)}</h2>
|
||
<div class="modal-artist">${escapeHtml(t('modal_artist_label'))}<span class="artist-name">${escapeHtml(detail.artist)}</span></div>
|
||
<div class="modal-meta">
|
||
<div class="meta-item">${escapeHtml(t('modal_album_label'))}<b>${escapeHtml(detail.album||t('modal_unknown'))}</b></div>
|
||
<div class="meta-item">${escapeHtml(t('modal_release_label'))}<b>${escapeHtml(detail.release_date||t('modal_unknown'))}</b></div>
|
||
<div class="meta-item">${escapeHtml(t('modal_language_label'))}<b>${escapeHtml(detail.language||t('modal_unknown'))}</b></div>
|
||
<div class="meta-item">${escapeHtml(t('modal_duration_label'))}<b>${formatDuration(detail.duration)}</b></div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
let bodyHTML = '';
|
||
if(tags.length>0){
|
||
bodyHTML += '<div class="modal-tags">';
|
||
tags.forEach(tag=>{ bodyHTML += `<span class="modal-tag">${escapeHtml(tag)}</span>`; });
|
||
bodyHTML += '</div>';
|
||
}
|
||
bodyHTML += `
|
||
<div class="modal-stats">
|
||
<div class="modal-stat"><div class="stat-val green">${formatNumber(detail.play_count)}</div><div class="stat-label">${escapeHtml(t('modal_plays'))}</div></div>
|
||
<div class="modal-stat"><div class="stat-val">${formatNumber(detail.like_count)}</div><div class="stat-label">${escapeHtml(t('modal_likes'))}</div></div>
|
||
<div class="modal-stat"><div class="stat-val">${formatDuration(detail.duration)}</div><div class="stat-label">${escapeHtml(t('modal_duration'))}</div></div>
|
||
<div class="modal-stat"><div class="stat-val">${escapeHtml(detail.release_date||'-')}</div><div class="stat-label">${escapeHtml(t('modal_release_date'))}</div></div>
|
||
</div>
|
||
`;
|
||
bodyHTML += '<div class="modal-lyrics-title"><span class="title-bar"></span>'+escapeHtml(t('modal_lyrics_title'))+'</div>';
|
||
if(lyrics){
|
||
bodyHTML += `<div class="modal-lyrics">${escapeHtml(lyrics)}</div>`;
|
||
}else{
|
||
bodyHTML += '<div class="modal-lyrics"><div class="modal-lyrics-empty">'+t('modal_no_lyrics')+'</div></div>';
|
||
}
|
||
bodyHTML += `
|
||
<div class="modal-actions">
|
||
<button class="modal-action-btn primary" id="modalPlayBtn">▶ ${escapeHtml(t('modal_play_btn'))}</button>
|
||
<button class="modal-action-btn secondary" id="modalLikeBtn">${isLiked?'♥ '+escapeHtml(t('modal_liked_btn')):'♡ '+escapeHtml(t('modal_like_btn'))}</button>
|
||
</div>
|
||
`;
|
||
document.getElementById('modalBody').innerHTML = bodyHTML;
|
||
|
||
document.getElementById('modalPlayBtn').addEventListener('click', ()=>{
|
||
playSong(detail);
|
||
});
|
||
document.getElementById('modalLikeBtn').addEventListener('click', (e)=>{
|
||
toggleLike(detail.id, null);
|
||
const liked = state.player.liked.has(detail.id);
|
||
e.target.innerHTML = liked ? ('♥ '+escapeHtml(t('modal_liked_btn'))) : ('♡ '+escapeHtml(t('modal_like_btn')));
|
||
});
|
||
|
||
document.getElementById('modalOverlay').classList.add('open');
|
||
document.body.style.overflow = 'hidden';
|
||
}
|
||
|
||
function closeModal(){
|
||
document.getElementById('modalOverlay').classList.remove('open');
|
||
document.body.style.overflow = '';
|
||
}
|
||
|
||
/* ============================================================
|
||
事件绑定
|
||
============================================================ */
|
||
function initEvents(){
|
||
const topSearch = document.getElementById('topSearchInput');
|
||
const topSearchWrap = document.getElementById('topSearchWrap');
|
||
const topSearchClear = document.getElementById('topSearchClear');
|
||
const debouncedSearch = debounce(()=>{
|
||
state.library.keyword = topSearch.value.trim();
|
||
state.library.page = 1;
|
||
document.getElementById('libSearchInput').value = topSearch.value;
|
||
loadLibrary(true);
|
||
},300);
|
||
topSearch.addEventListener('input', ()=>{
|
||
topSearchWrap.classList.toggle('has-text', topSearch.value.length>0);
|
||
debouncedSearch();
|
||
});
|
||
topSearchClear.addEventListener('click', ()=>{
|
||
topSearch.value = '';
|
||
topSearchWrap.classList.remove('has-text');
|
||
state.library.keyword = '';
|
||
state.library.page = 1;
|
||
document.getElementById('libSearchInput').value = '';
|
||
loadLibrary();
|
||
});
|
||
|
||
const libSearch = document.getElementById('libSearchInput');
|
||
const libSpinner = document.getElementById('libSpinner');
|
||
const debouncedLibSearch = debounce(()=>{
|
||
state.library.keyword = libSearch.value.trim();
|
||
state.library.page = 1;
|
||
document.getElementById('topSearchInput').value = libSearch.value;
|
||
document.getElementById('topSearchWrap').classList.toggle('has-text', libSearch.value.length>0);
|
||
loadLibrary();
|
||
},300);
|
||
libSearch.addEventListener('input', ()=>{
|
||
libSpinner.classList.add('show');
|
||
debouncedLibSearch();
|
||
setTimeout(()=>libSpinner.classList.remove('show'),300);
|
||
});
|
||
|
||
const sortWrap = document.getElementById('sortWrap');
|
||
const sortBtn = document.getElementById('sortBtn');
|
||
const sortMenu = document.getElementById('sortMenu');
|
||
const sortLabel = document.getElementById('sortLabel');
|
||
sortBtn.addEventListener('click', (e)=>{
|
||
e.stopPropagation();
|
||
sortWrap.classList.toggle('open');
|
||
});
|
||
document.addEventListener('click', ()=>sortWrap.classList.remove('open'));
|
||
sortMenu.querySelectorAll('button').forEach(btn=>{
|
||
btn.addEventListener('click', ()=>{
|
||
const sort = btn.dataset.sort;
|
||
state.library.sort = sort;
|
||
state.library.page = 1;
|
||
sortLabel.textContent = btn.textContent;
|
||
sortMenu.querySelectorAll('button').forEach(b=>b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
sortWrap.classList.remove('open');
|
||
loadLibrary();
|
||
});
|
||
});
|
||
|
||
document.getElementById('hotSeeAll').addEventListener('click', ()=>{
|
||
state.library.sort = 'plays';
|
||
state.library.page = 1;
|
||
sortLabel.textContent = t('sort_plays');
|
||
sortMenu.querySelectorAll('button').forEach(b=>b.classList.toggle('active', b.dataset.sort==='plays'));
|
||
loadLibrary(true);
|
||
});
|
||
document.getElementById('newSeeAll').addEventListener('click', ()=>{
|
||
state.library.sort = 'newest';
|
||
state.library.page = 1;
|
||
sortLabel.textContent = t('sort_newest');
|
||
sortMenu.querySelectorAll('button').forEach(b=>b.classList.toggle('active', b.dataset.sort==='newest'));
|
||
loadLibrary(true);
|
||
});
|
||
|
||
document.getElementById('playBtn').addEventListener('click', togglePlay);
|
||
document.getElementById('nextBtn').addEventListener('click', nextSong);
|
||
document.getElementById('prevBtn').addEventListener('click', prevSong);
|
||
document.getElementById('shuffleBtn').addEventListener('click', function(){
|
||
state.player.shuffle = !state.player.shuffle;
|
||
this.classList.toggle('active', state.player.shuffle);
|
||
showToast(state.player.shuffle?t('toast_shuffle_on'):t('toast_shuffle_off'), '🔀');
|
||
});
|
||
document.getElementById('repeatBtn').addEventListener('click', function(){
|
||
state.player.repeat = !state.player.repeat;
|
||
this.classList.toggle('active', state.player.repeat);
|
||
showToast(state.player.repeat?t('toast_repeat_on'):t('toast_repeat_off'), '🔁');
|
||
});
|
||
|
||
document.getElementById('playerLike').addEventListener('click', function(){
|
||
if(!state.player.currentSong) return;
|
||
toggleLike(state.player.currentSong.id, null);
|
||
updatePlayerLikeBtn();
|
||
document.querySelectorAll('[data-like]').forEach(btn=>{
|
||
if(parseInt(btn.dataset.like)===state.player.currentSong.id){
|
||
btn.classList.toggle('liked', state.player.liked.has(state.player.currentSong.id));
|
||
btn.innerHTML = state.player.liked.has(state.player.currentSong.id)?'♥':'♡';
|
||
}
|
||
});
|
||
});
|
||
|
||
document.getElementById('progressBar').addEventListener('click', seekProgress);
|
||
|
||
const volumeSlider = document.getElementById('volumeSlider');
|
||
let volumeDragging = false;
|
||
function handleVolume(e){
|
||
const rect = volumeSlider.getBoundingClientRect();
|
||
const pct = Math.max(0, Math.min(1, (e.clientX-rect.left)/rect.width));
|
||
setVolume(pct);
|
||
}
|
||
volumeSlider.addEventListener('mousedown', (e)=>{
|
||
volumeDragging = true;
|
||
handleVolume(e);
|
||
});
|
||
document.addEventListener('mousemove', (e)=>{
|
||
if(volumeDragging) handleVolume(e);
|
||
});
|
||
document.addEventListener('mouseup', ()=>{ volumeDragging = false; });
|
||
document.getElementById('volumeBtn').addEventListener('click', ()=>{
|
||
if(state.player.volume>0){
|
||
setVolume(0);
|
||
}else{
|
||
setVolume(0.7);
|
||
}
|
||
});
|
||
|
||
document.getElementById('expandBtn').addEventListener('click', ()=>{
|
||
if(state.player.currentSong){
|
||
openSongModal(state.player.currentSong);
|
||
}else{
|
||
showToast(t('toast_select_song'), '⌕');
|
||
}
|
||
});
|
||
|
||
document.getElementById('modalClose').addEventListener('click', closeModal);
|
||
document.getElementById('modalOverlay').addEventListener('click', (e)=>{
|
||
if(e.target===e.currentTarget) closeModal();
|
||
});
|
||
document.addEventListener('keydown', (e)=>{
|
||
if(e.key==='Escape') closeModal();
|
||
if(e.key===' '&&e.target.tagName!=='INPUT'){
|
||
e.preventDefault();
|
||
togglePlay();
|
||
}
|
||
});
|
||
|
||
document.querySelectorAll('.sidebar-nav .nav-link').forEach(link=>{
|
||
link.addEventListener('click', ()=>{
|
||
document.querySelectorAll('.sidebar-nav .nav-link').forEach(l=>l.classList.remove('active'));
|
||
link.classList.add('active');
|
||
const nav = link.dataset.nav;
|
||
const map = {home:'heroSection',discover:'catSection',hot:'hotSection',new:'newSection'};
|
||
if(map[nav]){
|
||
document.getElementById(map[nav]).scrollIntoView({behavior:'smooth',block:'start'});
|
||
}
|
||
});
|
||
});
|
||
|
||
document.getElementById('heroCta').addEventListener('click', ()=>{
|
||
if(state.hotSongs.length>0){
|
||
playSong(state.hotSongs[0]);
|
||
}else{
|
||
document.getElementById('hotSection').scrollIntoView({behavior:'smooth'});
|
||
}
|
||
});
|
||
|
||
document.getElementById('navBack').addEventListener('click', ()=>history.back());
|
||
document.getElementById('navForward').addEventListener('click', ()=>history.forward());
|
||
|
||
document.querySelectorAll('.playlist-item').forEach(item=>{
|
||
item.addEventListener('click', ()=>{
|
||
const name = item.querySelector('.pl-name').textContent;
|
||
showToast(t('toast_open_playlist')+name, '♫');
|
||
});
|
||
});
|
||
|
||
document.querySelector('.upgrade-btn').addEventListener('click', ()=>{
|
||
showToast(t('toast_upgrade'), '★');
|
||
});
|
||
}
|
||
|
||
/* ============================================================
|
||
滚动揭示动画
|
||
============================================================ */
|
||
function initRevealObserver(){
|
||
const observer = new IntersectionObserver((entries)=>{
|
||
entries.forEach(entry=>{
|
||
if(entry.isIntersecting){
|
||
entry.target.classList.add('visible');
|
||
observer.unobserve(entry.target);
|
||
}
|
||
});
|
||
}, {threshold:0.1, rootMargin:'0px 0px -50px 0px'});
|
||
|
||
document.querySelectorAll('.reveal').forEach(el=>observer.observe(el));
|
||
}
|
||
|
||
/* ============================================================
|
||
问候语
|
||
============================================================ */
|
||
function setGreeting(){
|
||
const h = new Date().getHours();
|
||
let greeting = t('hero_greeting_evening');
|
||
if(h>=5 && h<12) greeting = t('hero_greeting_morning');
|
||
else if(h>=12 && h<14) greeting = t('hero_greeting_noon');
|
||
else if(h>=14 && h<18) greeting = t('hero_greeting_afternoon');
|
||
else if(h>=18 && h<22) greeting = t('hero_greeting_evening');
|
||
else greeting = t('hero_greeting_night');
|
||
document.getElementById('heroGreeting').textContent = greeting;
|
||
}
|
||
|
||
/* ============================================================
|
||
语言切换回调:重新渲染动态内容
|
||
============================================================ */
|
||
function onLangChange(lang){
|
||
applyI18n();
|
||
setGreeting();
|
||
if(state.categories.length>0) renderCategories(state.categories);
|
||
if(state.hotSongs.length>0) renderHotList(state.hotSongs);
|
||
if(state.newSongs.length>0) renderNewGrid(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});
|
||
}
|
||
if(state.player.currentSong){
|
||
document.getElementById('playerTitle').textContent = state.player.currentSong.title;
|
||
document.getElementById('playerArtist').textContent = state.player.currentSong.artist;
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
初始化
|
||
============================================================ */
|
||
async function init(){
|
||
applyI18n();
|
||
setGreeting();
|
||
setVolume(0.7);
|
||
initEvents();
|
||
initRevealObserver();
|
||
|
||
await loadHome();
|
||
loadLibrary();
|
||
}
|
||
|
||
if(document.readyState==='loading'){
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
}else{
|
||
init();
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|