Files
web_template/web/dist/music1/index.html
Geliebte 10979351f8 feat: 默认语言改为英语 + 导航页添加i18n三语支持
- 导航页(index.html)添加完整i18n引擎,支持zh-CN/zh-TW/en
- 导航页添加语言切换按钮(EN/中/繁)
- 所有20个前端模板默认语言从zh-CN改为en
- 导航页SEO meta标签改为英文
2026-09-09 19:48:45 +08:00

2531 lines
102 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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">&#8962;</span><span class="label" data-i18n="nav_home">首页</span>
</div>
<div class="nav-link" data-nav="discover">
<span class="ico">&#9906;</span><span class="label" data-i18n="nav_discover">发现</span>
</div>
<div class="nav-link" data-nav="hot">
<span class="ico">&#128293;</span><span class="label" data-i18n="nav_hot">热门</span>
</div>
<div class="nav-link" data-nav="new">
<span class="ico">&#127881;</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)">&#9835;</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)">&#9836;</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)">&#9834;</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)">&#9833;</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="后退">&#8249;</button>
<button class="nav-btn" id="navForward" title="前进">&#8250;</button>
</div>
<div class="search-wrap" id="topSearchWrap">
<span class="search-ico">&#8981;</span>
<input type="text" class="search-input" id="topSearchInput" data-i18n-placeholder="search_placeholder" placeholder="搜索歌曲、歌手、专辑...">
<span class="search-clear" id="topSearchClear">&#10005;</span>
</div>
<div class="topbar-right">
<div class="lang-switcher" id="langSwitcher">
<button class="lang-btn" id="langBtn" onclick="toggleLangDropdown()">
<span class="ico">&#127760;</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">&#10003;</span><span>简体中文</span>
</div>
<div class="lang-option" data-lang="zh-TW" onclick="switchLang('zh-TW')">
<span class="check">&#10003;</span><span>繁體中文</span>
</div>
<div class="lang-option" data-lang="en" onclick="switchLang('en')">
<span class="check">&#10003;</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>&#9654;</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> &#8594;</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">&#9825;</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> &#8594;</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">&#8981;</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">&#9660;</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">&#9825;</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)">
&#9835;
</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">&#9825;</div>
</div>
<!-- 中间控制 -->
<div class="player-center">
<div class="player-controls">
<button class="player-btn shuffle-btn" id="shuffleBtn" data-i18n-title="ctrl_shuffle" title="随机播放">&#128256;</button>
<button class="player-btn" id="prevBtn" data-i18n-title="ctrl_prev" title="上一首">&#9664;&#9664;</button>
<button class="player-btn play-btn" id="playBtn" data-i18n-title="ctrl_play" title="播放/暂停">&#9654;</button>
<button class="player-btn" id="nextBtn" data-i18n-title="ctrl_next" title="下一首">&#9654;&#9654;</button>
<button class="player-btn repeat-btn" id="repeatBtn" data-i18n-title="ctrl_repeat" title="循环播放">&#128257;</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="音量">&#128266;</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="展开详情">&#8597;</button>
</div>
</div>
<!-- ==================== 歌曲详情弹窗 ==================== -->
<div class="modal-overlay" id="modalOverlay">
<div class="modal" id="modal">
<button class="modal-close" id="modalClose">&#10005;</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: '这首歌曲是纯音乐,暂无歌词 &#127932;',
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: '這首歌曲是純音樂,暫無歌詞 &#127932;',
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 &#127932;',
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 'en';
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
}
/* 根据歌曲标题哈希生成渐变封面 */
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 = {'流行':'&#9835;','摇滚':'&#127896;','民谣':'&#127928;','电子':'&#128300;','古典':'&#127932;','嘻哈':'&#127908;'};
return map[cat]||'&#9835;';
}
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||'&#10003;'}</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 = {'流行':'&#127925;','摇滚':'&#127896;','民谣':'&#127928;','电子':'&#128300;','古典':'&#127932;','嘻哈':'&#127908;'};
const all = [{id:'',name:t('cat_all'),icon:'&#127311;'}].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||'&#9835;'}</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">&#127925;</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">&#9654;</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?'&#9829;':'&#9825;'}</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">&#127881;</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">&#9654;</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">&#8981;</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">&#9654;</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?'&#9829;':'&#9825;'}</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 = '&#9825;'; }
showToast(t('toast_cancel_like'), '&#9825;');
}else{
state.player.liked.add(id);
if(btn){ btn.classList.add('liked'); btn.innerHTML = '&#9829;'; }
showToast(t('toast_add_like'), '&#9829;');
}
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':''}>&#8249;</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':''}>&#8250;</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 = '&#10074;&#10074;';
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, '&#9654;');
}
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 = '&#10074;&#10074;';
document.getElementById('visualizer').classList.remove('paused');
startProgress();
}else{
btn.innerHTML = '&#9654;';
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 = '&#9829;';
}else{
btn.classList.remove('liked');
btn.innerHTML = '&#9825;';
}
}
/* 进度条点击跳转 */
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 = '&#128263;';
else if(pct<0.5) btn.innerHTML = '&#128265;';
else btn.innerHTML = '&#128266;';
}
/* ============================================================
歌曲详情弹窗
============================================================ */
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?'&#128293; '+escapeHtml(t('section_hot')):''} ${detail.is_new?'&#127881; '+escapeHtml(t('section_new')):''} ${detail.category_name?'&#8226; '+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">&#9654; ${escapeHtml(t('modal_play_btn'))}</button>
<button class="modal-action-btn secondary" id="modalLikeBtn">${isLiked?'&#9829; '+escapeHtml(t('modal_liked_btn')):'&#9825; '+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 ? ('&#9829; '+escapeHtml(t('modal_liked_btn'))) : ('&#9825; '+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'), '&#128256;');
});
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'), '&#128257;');
});
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)?'&#9829;':'&#9825;';
}
});
});
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'), '&#8981;');
}
});
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, '&#9835;');
});
});
document.querySelector('.upgrade-btn').addEventListener('click', ()=>{
showToast(t('toast_upgrade'), '&#9733;');
});
}
/* ============================================================
滚动揭示动画
============================================================ */
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>