Files
web_template/web/dist/music1/index.html

2149 lines
85 KiB
HTML
Raw 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)}
/* ---------- 内容容器 ---------- */
.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">主菜单</div>
<div class="nav-link active" data-nav="home">
<span class="ico">&#8962;</span><span class="label">首页</span>
</div>
<div class="nav-link" data-nav="discover">
<span class="ico">&#9906;</span><span class="label">发现</span>
</div>
<div class="nav-link" data-nav="hot">
<span class="ico">&#128293;</span><span class="label">热门</span>
</div>
<div class="nav-link" data-nav="new">
<span class="ico">&#127881;</span><span class="label">新歌</span>
</div>
</nav>
<div class="sidebar-playlists">
<div class="nav-section-title">我的歌单</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">我喜欢的音乐</div><div class="pl-desc">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">深夜电台</div><div class="pl-desc">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">运动节奏</div><div class="pl-desc">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">通勤路上</div><div class="pl-desc">64 首</div></div>
</div>
</div>
<div class="sidebar-footer">
<div class="footer-link">关于云音</div>
<div class="footer-link">隐私政策</div>
<div class="footer-link">服务条款</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" placeholder="搜索歌曲、歌手、专辑...">
<span class="search-clear" id="topSearchClear">&#10005;</span>
</div>
<div class="topbar-right">
<button class="upgrade-btn">升级会员</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">发现好音乐</h1>
<p class="hero-subtitle">沉浸暗夜音境,探索属于你的声音世界。从热门榜单到新歌速递,让每一次播放都成为一场旅程。</p>
<button class="hero-cta" id="heroCta">
<span>&#9654;</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>音乐分类</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>热门歌曲</h2>
<a class="section-link" id="hotSeeAll">查看全部 &#8594;</a>
</div>
<div class="hot-list" id="hotList">
<div class="hot-list-header">
<div class="col-center">#</div>
<div>标题</div>
<div class="col-album">专辑</div>
<div class="col-center">播放量</div>
<div class="col-center">时长</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>新歌速递</h2>
<a class="section-link" id="newSeeAll">查看全部 &#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>全部音乐</h2>
</div>
<div class="lib-toolbar">
<div class="lib-search">
<span class="ico">&#8981;</span>
<input type="text" id="libSearchInput" placeholder="搜索歌曲、歌手、专辑、标签...">
<span class="spinner" id="libSpinner"></span>
</div>
<div class="sort-wrap" id="sortWrap">
<button class="sort-btn" id="sortBtn">
<span id="sortLabel">默认排序</span><span class="arrow">&#9660;</span>
</button>
<div class="sort-menu" id="sortMenu">
<button data-sort="" class="active">默认排序</button>
<button data-sort="plays">按播放量</button>
<button data-sort="likes">按收藏量</button>
<button data-sort="newest">最新发布</button>
</div>
</div>
<div class="lib-info">共 <b id="libTotal">0</b> 首</div>
</div>
<div class="lib-list">
<div class="lib-list-header">
<div class="col-center">#</div>
<div>标题</div>
<div class="col-album">专辑</div>
<div class="col-cat">曲风</div>
<div class="col-center">播放量</div>
<div class="col-center 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">未在播放</div>
<div class="player-song-artist" id="playerArtist">选择一首歌曲开始</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" title="随机播放">&#128256;</button>
<button class="player-btn" id="prevBtn" title="上一首">&#9664;&#9664;</button>
<button class="player-btn play-btn" id="playBtn" title="播放/暂停">&#9654;</button>
<button class="player-btn" id="nextBtn" title="下一首">&#9654;&#9654;</button>
<button class="player-btn repeat-btn" id="repeatBtn" 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" 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" 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>
/* ============================================================
配置
============================================================ */
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(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 t = document.createElement('div');
t.className = 'toast';
t.innerHTML = `<span class="toast-ico">${ico||'&#10003;'}</span><span>${escapeHtml(msg)}</span>`;
wrap.appendChild(t);
requestAnimationFrame(()=>t.classList.add('show'));
setTimeout(()=>{
t.classList.remove('show');
setTimeout(()=>t.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:'全部',icon:'&#127311;'}].concat(cats);
wrap.innerHTML = all.map(c=>`
<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(c.name)}
</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">暂无热门歌曲</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">暂无新歌</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">没有找到匹配的歌曲</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('已取消收藏', '&#9825;');
}else{
state.player.liked.add(id);
if(btn){ btn.classList.add('liked'); btn.innerHTML = '&#9829;'; }
showToast('已添加到我喜欢', '&#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('正在播放:'+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);
// Header
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; 热门':''} ${detail.is_new?'&#127881; 新歌':''} ${detail.category_name?'&#8226; '+escapeHtml(detail.category_name):''}</div>
<h2 class="modal-title">${escapeHtml(detail.title)}</h2>
<div class="modal-artist">歌手:<span class="artist-name">${escapeHtml(detail.artist)}</span></div>
<div class="modal-meta">
<div class="meta-item">专辑:<b>${escapeHtml(detail.album||'未知')}</b></div>
<div class="meta-item">发行:<b>${escapeHtml(detail.release_date||'未知')}</b></div>
<div class="meta-item">语种:<b>${escapeHtml(detail.language||'未知')}</b></div>
<div class="meta-item">时长:<b>${formatDuration(detail.duration)}</b></div>
</div>
</div>
`;
// Body
let bodyHTML = '';
if(tags.length>0){
bodyHTML += '<div class="modal-tags">';
tags.forEach(t=>{ bodyHTML += `<span class="modal-tag">${escapeHtml(t)}</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">播放量</div></div>
<div class="modal-stat"><div class="stat-val">${formatNumber(detail.like_count)}</div><div class="stat-label">收藏量</div></div>
<div class="modal-stat"><div class="stat-val">${formatDuration(detail.duration)}</div><div class="stat-label">时长</div></div>
<div class="modal-stat"><div class="stat-val">${escapeHtml(detail.release_date||'-')}</div><div class="stat-label">发行日期</div></div>
</div>
`;
bodyHTML += '<div class="modal-lyrics-title"><span class="title-bar"></span>歌词</div>';
if(lyrics){
bodyHTML += `<div class="modal-lyrics">${escapeHtml(lyrics)}</div>`;
}else{
bodyHTML += '<div class="modal-lyrics"><div class="modal-lyrics-empty">这首歌曲是纯音乐,暂无歌词 &#127932;</div></div>';
}
bodyHTML += `
<div class="modal-actions">
<button class="modal-action-btn primary" id="modalPlayBtn">&#9654; 播放</button>
<button class="modal-action-btn secondary" id="modalLikeBtn">${isLiked?'&#9829; 已收藏':'&#9825; 收藏'}</button>
</div>
`;
document.getElementById('modalBody').innerHTML = bodyHTML;
// 绑定弹窗内按钮
document.getElementById('modalPlayBtn').addEventListener('click', ()=>{
playSong(detail);
});
document.getElementById('modalLikeBtn').addEventListener('click', (e)=>{
toggleLike(detail.id, null);
e.target.innerHTML = state.player.liked.has(detail.id)?'&#9829; 已收藏':'&#9825; 收藏';
});
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 = '按播放量';
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 = '最新发布';
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?'已开启随机播放':'已关闭随机播放', '&#128256;');
});
document.getElementById('repeatBtn').addEventListener('click', function(){
state.player.repeat = !state.player.repeat;
this.classList.toggle('active', state.player.repeat);
showToast(state.player.repeat?'已开启循环播放':'已关闭循环播放', '&#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('请先选择一首歌曲', '&#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'});
}
});
});
// Hero CTA
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('打开歌单:'+name, '&#9835;');
});
});
// 升级会员
document.querySelector('.upgrade-btn').addEventListener('click', ()=>{
showToast('会员功能即将上线,敬请期待', '&#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 = '晚上好';
if(h>=5 && h<12) greeting = '早上好';
else if(h>=12 && h<14) greeting = '中午好';
else if(h>=14 && h<18) greeting = '下午好';
else if(h>=18 && h<22) greeting = '晚上好';
else greeting = '夜深了';
document.getElementById('heroGreeting').textContent = greeting;
}
/* ============================================================
初始化
============================================================ */
async function init(){
setGreeting();
setVolume(0.7);
initEvents();
initRevealObserver();
// 并行加载首页数据
await loadHome();
// 加载全部音乐
loadLibrary();
}
// DOM 就绪后启动
if(document.readyState==='loading'){
document.addEventListener('DOMContentLoaded', init);
}else{
init();
}
</script>
</body>
</html>