Files
web_template/web/dist/music3/index.html
2026-07-10 10:07:21 +00:00

1222 lines
57 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>
/* ============ 基础变量与重置 ============ */
:root{
--purple:#6c5ce7;
--pink:#e84393;
--blue:#0984e3;
--grad-main:linear-gradient(135deg,#6c5ce7 0%,#e84393 50%,#0984e3 100%);
--grad-warm:linear-gradient(135deg,#e84393,#fd79a8);
--grad-cool:linear-gradient(135deg,#0984e3,#6c5ce7);
--glass:rgba(255,255,255,0.10);
--glass-strong:rgba(255,255,255,0.18);
--glass-border:rgba(255,255,255,0.22);
--text:#ffffff;
--text-soft:rgba(255,255,255,0.78);
--text-mute:rgba(255,255,255,0.55);
--radius:18px;
--radius-lg:24px;
--shadow:0 8px 32px rgba(31,38,135,0.25);
--shadow-glow:0 0 24px rgba(108,92,231,0.55);
}
*{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:smooth}
body{
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;
color:var(--text);
min-height:100vh;
overflow-x:hidden;
background:#0f0c29;
position:relative;
}
/* 动态渐变网格背景 */
.bg-canvas{
position:fixed;inset:0;z-index:-2;
background:linear-gradient(-45deg,#6c5ce7,#e84393,#0984e3,#6c5ce7);
background-size:400% 400%;
animation:meshShift 22s ease infinite;
}
@keyframes meshShift{
0%{background-position:0% 50%}
50%{background-position:100% 50%}
100%{background-position:0% 50%}
}
/* 漂浮光斑 */
.blob{position:fixed;border-radius:50%;filter:blur(70px);opacity:0.55;z-index:-1;pointer-events:none;mix-blend-mode:screen}
.blob.b1{width:520px;height:520px;background:#6c5ce7;top:-120px;left:-80px;animation:float1 18s ease-in-out infinite}
.blob.b2{width:460px;height:460px;background:#e84393;top:30%;right:-100px;animation:float2 24s ease-in-out infinite}
.blob.b3{width:560px;height:560px;background:#0984e3;bottom:-160px;left:25%;animation:float3 30s ease-in-out infinite}
.blob.b4{width:380px;height:380px;background:#fd79a8;top:55%;left:10%;animation:float1 26s ease-in-out infinite reverse}
@keyframes float1{0%,100%{transform:translate(0,0) scale(1)}50%{transform:translate(80px,60px) scale(1.15)}}
@keyframes float2{0%,100%{transform:translate(0,0) scale(1)}50%{transform:translate(-90px,80px) scale(1.1)}}
@keyframes float3{0%,100%{transform:translate(0,0) scale(1)}50%{transform:translate(70px,-70px) scale(1.2)}}
/* 粒子 */
.particle{position:fixed;width:6px;height:6px;border-radius:50%;background:rgba(255,255,255,0.6);z-index:-1;pointer-events:none;animation:rise linear infinite}
@keyframes rise{0%{transform:translateY(100vh) scale(0);opacity:0}10%{opacity:1}90%{opacity:1}100%{transform:translateY(-10vh) scale(1.2);opacity:0}}
/* ============ 通用 ============ */
.container{max-width:1320px;margin:0 auto;padding:0 28px}
section{padding:48px 0}
.glass{
background:var(--glass);
backdrop-filter:blur(22px) saturate(140%);
-webkit-backdrop-filter:blur(22px) saturate(140%);
border:1px solid var(--glass-border);
border-radius:var(--radius);
}
.grad-text{
background:linear-gradient(90deg,#a29bfe,#fd79a8,#74b9ff);
-webkit-background-clip:text;background-clip:text;
-webkit-text-fill-color:transparent;color:transparent;
}
.btn-grad{
background:var(--grad-main);
background-size:200% 200%;
border:none;color:#fff;cursor:pointer;
padding:13px 30px;border-radius:30px;font-size:15px;font-weight:600;
transition:all .35s ease;
box-shadow:0 6px 20px rgba(108,92,231,0.45);
}
.btn-grad:hover{transform:translateY(-2px);box-shadow:0 10px 28px rgba(232,67,147,0.55);background-position:100% 50%}
.btn-grad:active{transform:translateY(0)}
a{text-decoration:none;color:inherit}
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-track{background:rgba(255,255,255,0.05)}
::-webkit-scrollbar-thumb{background:linear-gradient(#6c5ce7,#e84393);border-radius:6px}
/* ============ 头部导航 ============ */
header{
position:sticky;top:0;z-index:100;
background:rgba(15,12,41,0.35);
backdrop-filter:blur(20px) saturate(160%);
-webkit-backdrop-filter:blur(20px) saturate(160%);
border-bottom:1px solid rgba(255,255,255,0.10);
}
.nav-inner{display:flex;align-items:center;gap:24px;height:70px;max-width:1320px;margin:0 auto;padding:0 28px}
.logo{font-size:25px;font-weight:800;letter-spacing:.5px;white-space:nowrap}
.logo .dot{display:inline-block;width:9px;height:9px;border-radius:50%;background:var(--grad-main);margin:0 3px 3px 0;box-shadow:0 0 12px #e84393}
.nav-links{display:flex;gap:6px;margin-left:8px}
.nav-links a{
padding:8px 16px;border-radius:20px;font-size:15px;font-weight:500;
color:var(--text-soft);transition:all .3s;cursor:pointer;
}
.nav-links a:hover{color:#fff;background:rgba(255,255,255,0.10)}
.nav-links a.active{color:#fff;background:var(--grad-main);box-shadow:0 4px 14px rgba(108,92,231,0.5)}
.nav-search{
flex:1;max-width:420px;margin-left:auto;position:relative;
}
.nav-search .wrap{
position:relative;border-radius:30px;padding:1.5px;
background:linear-gradient(90deg,#6c5ce7,#e84393,#0984e3);
}
.nav-search input{
width:100%;height:42px;border:none;border-radius:29px;
background:rgba(15,12,41,0.55);color:#fff;
padding:0 20px 0 44px;font-size:14px;outline:none;
}
.nav-search input::placeholder{color:var(--text-mute)}
.nav-search .icon{
position:absolute;left:15px;top:50%;transform:translateY(-50%);
width:18px;height:18px;pointer-events:none;opacity:.7;
}
.nav-search .icon svg{width:100%;height:100%;fill:#fff}
.nav-icon-btn{
width:42px;height:42px;border-radius:50%;border:1px solid var(--glass-border);
background:var(--glass);display:grid;place-items:center;cursor:pointer;color:#fff;
transition:all .3s;
}
.nav-icon-btn:hover{background:var(--glass-strong);transform:scale(1.05)}
.nav-icon-btn svg{width:20px;height:20px;fill:#fff}
/* ============ Hero ============ */
.hero{
position:relative;margin:28px auto;border-radius:var(--radius-lg);
overflow:hidden;padding:64px 56px;min-height:340px;
display:flex;flex-direction:column;justify-content:center;
background:linear-gradient(120deg,rgba(108,92,231,0.35),rgba(232,67,147,0.30),rgba(9,132,227,0.35));
border:1px solid var(--glass-border);
}
.hero::before{
content:"";position:absolute;inset:0;
background:radial-gradient(circle at 20% 30%,rgba(255,255,255,0.25),transparent 40%),
radial-gradient(circle at 80% 70%,rgba(232,67,147,0.4),transparent 45%);
animation:heroGlow 10s ease-in-out infinite alternate;
}
@keyframes heroGlow{0%{opacity:.6;transform:scale(1)}100%{opacity:1;transform:scale(1.1)}}
.hero-shape{position:absolute;border-radius:50%;filter:blur(2px);opacity:.5}
.hero-shape.s1{width:120px;height:120px;background:linear-gradient(135deg,#fd79a8,#e84393);top:-30px;right:18%;animation:float1 12s ease-in-out infinite}
.hero-shape.s2{width:80px;height:80px;background:linear-gradient(135deg,#74b9ff,#0984e3);bottom:-20px;right:8%;animation:float2 9s ease-in-out infinite}
.hero-shape.s3{width:60px;height:60px;background:linear-gradient(135deg,#a29bfe,#6c5ce7);top:40%;right:30%;animation:float3 14s ease-in-out infinite}
.hero h1{font-size:46px;font-weight:800;line-height:1.2;position:relative;z-index:2;letter-spacing:1px}
.hero p{margin-top:16px;font-size:17px;color:var(--text-soft);position:relative;z-index:2;max-width:520px}
.hero .actions{margin-top:30px;display:flex;gap:16px;position:relative;z-index:2}
.hero .btn-ghost{
padding:13px 28px;border-radius:30px;border:1px solid var(--glass-border);
background:rgba(255,255,255,0.08);color:#fff;font-size:15px;font-weight:600;cursor:pointer;transition:all .3s;
}
.hero .btn-ghost:hover{background:rgba(255,255,255,0.18);transform:translateY(-2px)}
.hero-stats{display:flex;gap:30px;margin-top:34px;position:relative;z-index:2}
.hero-stats .st b{font-size:26px;font-weight:800;display:block}
.hero-stats .st span{font-size:13px;color:var(--text-mute)}
/* ============ 区块标题 ============ */
.sec-head{display:flex;align-items:flex-end;justify-content:space-between;margin-bottom:26px}
.sec-title{font-size:28px;font-weight:800;display:flex;align-items:center;gap:12px}
.sec-title .bar{width:6px;height:28px;border-radius:4px;background:var(--grad-main)}
.sec-sub{font-size:14px;color:var(--text-mute);cursor:pointer;transition:color .3s}
.sec-sub:hover{color:#fff}
/* ============ 分类筛选 ============ */
.cat-bar{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:8px}
.cat-pill{
padding:9px 20px;border-radius:22px;font-size:14px;font-weight:600;cursor:pointer;
background:var(--glass);border:1px solid var(--glass-border);color:var(--text-soft);
transition:all .3s;user-select:none;
}
.cat-pill:hover{color:#fff;transform:translateY(-2px)}
.cat-pill.active{
color:#fff;background:var(--grad-main);background-size:200% 200%;
border-color:transparent;box-shadow:0 6px 18px rgba(108,92,231,0.5);
animation:gradMove 4s ease infinite;
}
@keyframes gradMove{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
/* ============ 热门榜单 ============ */
.hot-grid{display:grid;grid-template-columns:1.4fr 1fr 1fr;gap:22px}
.hot-feature{position:relative;border-radius:var(--radius-lg);overflow:hidden;min-height:380px;cursor:pointer;
display:flex;align-items:flex-end;padding:26px;transition:transform .4s}
.hot-feature:hover{transform:translateY(-4px)}
.hot-feature .cover{position:absolute;inset:0;z-index:0;transition:transform .6s}
.hot-feature:hover .cover{transform:scale(1.06)}
.hot-feature .scrim{position:absolute;inset:0;background:linear-gradient(to top,rgba(0,0,0,0.7),rgba(0,0,0,0.1) 60%,transparent);z-index:1}
.hot-feature .info{position:relative;z-index:2}
.rank-badge{
position:absolute;top:18px;left:18px;z-index:3;
width:46px;height:46px;border-radius:14px;display:grid;place-items:center;
font-size:22px;font-weight:800;color:#fff;
background:var(--grad-main);box-shadow:0 6px 16px rgba(0,0,0,0.35);
}
.rank-badge.gold{background:linear-gradient(135deg,#ffd700,#ff9f43)}
.rank-badge.silver{background:linear-gradient(135deg,#dfe6e9,#b2bec3);color:#2d3436}
.rank-badge.bronze{background:linear-gradient(135deg,#e17055,#d63031)}
.hot-feature h3{font-size:24px;font-weight:800;margin-bottom:6px;text-shadow:0 2px 10px rgba(0,0,0,0.4)}
.hot-feature .meta{font-size:14px;color:rgba(255,255,255,0.85)}
.play-overlay{
position:absolute;top:50%;left:50%;transform:translate(-50%,-50%) scale(0.6);z-index:3;
width:64px;height:64px;border-radius:50%;background:rgba(255,255,255,0.92);
display:grid;place-items:center;opacity:0;transition:all .35s;
}
.hot-feature:hover .play-overlay,.song-card:hover .play-overlay{opacity:1;transform:translate(-50%,-50%) scale(1)}
.play-overlay svg{width:26px;height:26px;fill:#e84393;margin-left:3px}
.hot-side{display:flex;flex-direction:column;gap:16px}
.hot-row{display:flex;gap:16px;align-items:center;padding:14px;border-radius:var(--radius);background:var(--glass);border:1px solid var(--glass-border);cursor:pointer;transition:all .3s;position:relative;overflow:hidden}
.hot-row:hover{background:var(--glass-strong);transform:translateX(4px)}
.hot-row .mini-cover{width:60px;height:60px;border-radius:12px;flex-shrink:0;position:relative}
.hot-row .rank{font-size:24px;font-weight:800;width:30px;text-align:center;color:rgba(255,255,255,0.85);flex-shrink:0}
.hot-row .t-info{flex:1;min-width:0}
.hot-row .t-info h4{font-size:15px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.hot-row .t-info p{font-size:12px;color:var(--text-mute);margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* ============ 新歌首发 ============ */
.new-scroll{display:flex;gap:20px;overflow-x:auto;padding:6px 4px 16px;scroll-snap-type:x mandatory}
.new-scroll::-webkit-scrollbar{height:6px}
.new-card{
flex:0 0 230px;scroll-snap-align:start;cursor:pointer;
border-radius:var(--radius);overflow:hidden;position:relative;
background:var(--glass);border:1px solid var(--glass-border);
transition:transform .35s;
}
.new-card:hover{transform:translateY(-6px)}
.new-card .nc-cover{height:230px;position:relative;overflow:hidden}
.new-badge{
position:absolute;top:12px;left:12px;z-index:2;
padding:4px 12px;border-radius:12px;font-size:11px;font-weight:800;letter-spacing:1px;
background:linear-gradient(135deg,#00b894,#00cec9);color:#fff;
box-shadow:0 4px 12px rgba(0,184,148,0.5);
}
.new-card .nc-body{padding:14px 16px 18px}
.new-card .nc-body h4{font-size:15px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.new-card .nc-body p{font-size:12px;color:var(--text-mute);margin-top:5px}
.new-card .play-overlay{width:52px;height:52px}
.new-card .play-overlay svg{width:22px;height:22px}
/* ============ 音乐广场 ============ */
.plaza-toolbar{display:flex;flex-wrap:wrap;gap:14px;align-items:center;margin-bottom:24px}
.sort-group{display:flex;gap:8px;flex-wrap:wrap}
.sort-btn{
padding:8px 18px;border-radius:20px;font-size:13px;font-weight:600;cursor:pointer;
background:var(--glass);border:1px solid var(--glass-border);color:var(--text-soft);transition:all .3s;
}
.sort-btn:hover{color:#fff}
.sort-btn.active{color:#fff;background:var(--grad-cool);border-color:transparent;box-shadow:0 4px 12px rgba(9,132,227,0.45)}
.plaza-search{margin-left:auto;position:relative}
.plaza-search .wrap{position:relative;border-radius:24px;padding:1.5px;background:linear-gradient(90deg,#6c5ce7,#0984e3)}
.plaza-search input{width:240px;height:40px;border:none;border-radius:23px;background:rgba(15,12,41,0.6);color:#fff;padding:0 18px;font-size:14px;outline:none}
.plaza-search input::placeholder{color:var(--text-mute)}
.song-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:22px}
.song-card{
border-radius:var(--radius);overflow:hidden;cursor:pointer;
background:var(--glass);border:1px solid var(--glass-border);
transition:transform .35s,box-shadow .35s;position:relative;
}
.song-card:hover{transform:translateY(-6px);box-shadow:0 16px 36px rgba(0,0,0,0.3)}
.song-card .sc-cover{height:100%;aspect-ratio:1;position:relative;overflow:hidden}
.song-card .sc-body{padding:12px 14px 16px}
.song-card .sc-body h4{font-size:14px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.song-card .sc-body p{font-size:12px;color:var(--text-mute);margin-top:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.song-card .sc-stats{display:flex;gap:12px;margin-top:8px;font-size:11px;color:var(--text-mute)}
.song-card .sc-stats span{display:flex;align-items:center;gap:3px}
.song-card .sc-stats svg{width:12px;height:12px;fill:currentColor}
.tag-mini{position:absolute;top:10px;right:10px;z-index:2;padding:3px 9px;border-radius:10px;font-size:10px;font-weight:700}
.tag-mini.hot{background:linear-gradient(135deg,#e84393,#fd79a8);color:#fff}
.tag-mini.new{background:linear-gradient(135deg,#00b894,#00cec9);color:#fff}
.song-card .play-overlay{width:50px;height:50px}
.song-card .play-overlay svg{width:21px;height:21px}
/* 分页 */
.pagination{display:flex;justify-content:center;align-items:center;gap:8px;margin-top:40px;flex-wrap:wrap}
.page-btn{
min-width:40px;height:40px;padding:0 12px;border-radius:12px;cursor:pointer;
background:var(--glass);border:1px solid var(--glass-border);color:var(--text-soft);
font-size:14px;font-weight:600;transition:all .3s;display:grid;place-items:center;
}
.page-btn:hover:not(:disabled){color:#fff;background:var(--glass-strong)}
.page-btn.active{background:var(--grad-main);color:#fff;border-color:transparent;box-shadow:0 4px 14px rgba(108,92,231,0.5)}
.page-btn:disabled{opacity:.35;cursor:not-allowed}
.page-info{font-size:13px;color:var(--text-mute);margin:0 10px}
/* ============ 骨架屏 ============ */
.skeleton{position:relative;overflow:hidden;background:rgba(255,255,255,0.08);border-radius:12px}
.skeleton::after{
content:"";position:absolute;inset:0;
background:linear-gradient(90deg,transparent,rgba(255,255,255,0.25),transparent);
transform:translateX(-100%);animation:shimmer 1.6s infinite;
}
@keyframes shimmer{100%{transform:translateX(100%)}}
.sk-card{border-radius:var(--radius);overflow:hidden;background:var(--glass);border:1px solid var(--glass-border)}
.sk-card .sk-cover{aspect-ratio:1}
.sk-card .sk-line{height:12px;margin:12px 14px}
.sk-card .sk-line.short{width:50%}
/* ============ 歌曲详情弹窗 ============ */
.modal-mask{
position:fixed;inset:0;z-index:1000;display:none;
background:rgba(15,12,41,0.55);backdrop-filter:blur(10px);
align-items:center;justify-content:center;padding:24px;
opacity:0;transition:opacity .3s;
}
.modal-mask.show{display:flex;opacity:1}
.modal{
width:min(880px,100%);max-height:88vh;overflow-y:auto;border-radius:var(--radius-lg);
background:rgba(20,16,50,0.78);backdrop-filter:blur(30px) saturate(160%);
border:1px solid var(--glass-border);box-shadow:0 24px 60px rgba(0,0,0,0.5);
transform:scale(.9) translateY(20px);transition:transform .35s cubic-bezier(.2,.9,.3,1.2);
position:relative;
}
.modal-mask.show .modal{transform:scale(1) translateY(0)}
.modal-close{
position:absolute;top:18px;right:18px;z-index:5;
width:40px;height:40px;border-radius:50%;border:1px solid var(--glass-border);
background:rgba(0,0,0,0.3);color:#fff;cursor:pointer;display:grid;place-items:center;transition:all .3s;
}
.modal-close:hover{background:rgba(232,67,147,0.5);transform:rotate(90deg)}
.modal-top{display:flex;gap:30px;padding:40px 40px 28px}
.modal-cover{width:240px;height:240px;border-radius:var(--radius);flex-shrink:0;position:relative;overflow:hidden;box-shadow:0 16px 40px rgba(0,0,0,0.4)}
.modal-cover .pulse{position:absolute;inset:0;border-radius:var(--radius);box-shadow:0 0 0 0 rgba(255,255,255,0.4);animation:pulse 2.5s infinite}
@keyframes pulse{0%{box-shadow:0 0 0 0 rgba(255,255,255,0.4)}70%{box-shadow:0 0 0 24px rgba(255,255,255,0)}100%{box-shadow:0 0 0 0 rgba(255,255,255,0)}}
.modal-info{flex:1;min-width:0;padding-top:10px}
.modal-info .m-cat{display:inline-block;padding:4px 14px;border-radius:14px;font-size:12px;font-weight:600;background:var(--grad-cool);color:#fff;margin-bottom:14px}
.modal-info h2{font-size:32px;font-weight:800;line-height:1.2}
.modal-info .m-artist{font-size:17px;color:var(--text-soft);margin-top:10px}
.modal-info .m-album{font-size:14px;color:var(--text-mute);margin-top:6px}
.modal-tags{display:flex;flex-wrap:wrap;gap:8px;margin-top:18px}
.modal-tags .tg{padding:5px 13px;border-radius:14px;font-size:12px;font-weight:600;color:#fff;background:var(--grad-main);background-size:200% 200%;animation:gradMove 5s ease infinite}
.modal-stats{display:flex;gap:28px;margin-top:22px}
.modal-stats .ms b{font-size:22px;font-weight:800;display:block}
.modal-stats .ms span{font-size:12px;color:var(--text-mute)}
.modal-actions{display:flex;gap:14px;margin-top:26px;align-items:center}
.btn-play-glow{
background:var(--grad-main);background-size:200% 200%;border:none;color:#fff;cursor:pointer;
padding:14px 36px;border-radius:30px;font-size:16px;font-weight:700;
display:flex;align-items:center;gap:10px;animation:gradMove 4s ease infinite;
box-shadow:0 0 24px rgba(232,67,147,0.6),0 6px 20px rgba(108,92,231,0.5);transition:transform .3s;
}
.btn-play-glow:hover{transform:scale(1.04)}
.btn-play-glow svg{width:18px;height:18px;fill:#fff}
.btn-like{
width:48px;height:48px;border-radius:50%;border:1px solid var(--glass-border);
background:var(--glass);cursor:pointer;display:grid;place-items:center;transition:all .3s;
}
.btn-like:hover{background:var(--glass-strong);transform:scale(1.08)}
.btn-like svg{width:22px;height:22px;fill:none;stroke:#fff;stroke-width:2;transition:all .3s}
.btn-like.liked svg{fill:#e84393;stroke:#e84393}
.modal-body{padding:0 40px 40px}
.lyrics-block{
margin-top:8px;padding:24px 28px;border-radius:var(--radius);
background:rgba(255,255,255,0.06);border:1px solid var(--glass-border);
max-height:300px;overflow-y:auto;
}
.lyrics-block h5{font-size:14px;font-weight:700;color:var(--text-soft);margin-bottom:14px;display:flex;align-items:center;gap:8px}
.lyrics-block h5 .bar{width:4px;height:16px;border-radius:3px;background:var(--grad-warm)}
.lyrics-line{font-size:15px;line-height:2.1;color:var(--text-soft);transition:color .3s}
.lyrics-line:hover{color:#fff}
.lyrics-empty{color:var(--text-mute);font-style:italic}
/* ============ 底部迷你播放器 ============ */
.mini-player{
position:fixed;left:50%;bottom:18px;transform:translateX(-50%) translateY(120%);
width:min(980px,calc(100% - 36px));z-index:200;
border-radius:var(--radius);overflow:hidden;
background:rgba(20,16,50,0.72);backdrop-filter:blur(24px) saturate(160%);
border:1px solid var(--glass-border);box-shadow:0 12px 40px rgba(0,0,0,0.45);
transition:transform .45s cubic-bezier(.2,.9,.3,1.2);
display:flex;align-items:center;gap:16px;padding:12px 18px;
}
.mini-player.show{transform:translateX(-50%) translateY(0)}
.mp-cover{width:54px;height:54px;border-radius:12px;flex-shrink:0;overflow:hidden;position:relative}
.mp-cover img,.mp-cover .cdiv{width:100%;height:100%;object-fit:cover}
.mp-info{flex:0 0 220px;min-width:0}
.mp-info h4{font-size:14px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mp-info p{font-size:12px;color:var(--text-mute);margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mp-controls{display:flex;align-items:center;gap:8px}
.mp-btn{width:38px;height:38px;border-radius:50%;border:none;background:transparent;color:#fff;cursor:pointer;display:grid;place-items:center;transition:all .3s}
.mp-btn:hover{background:rgba(255,255,255,0.12)}
.mp-btn svg{width:20px;height:20px;fill:#fff}
.mp-play{width:46px;height:46px;background:var(--grad-main);box-shadow:0 4px 14px rgba(108,92,231,0.5)}
.mp-play:hover{transform:scale(1.06);background:var(--grad-main)}
.mp-progress{flex:1;display:flex;align-items:center;gap:12px;min-width:0}
.mp-time{font-size:12px;color:var(--text-mute);flex-shrink:0;width:40px;text-align:center}
.mp-bar{flex:1;height:6px;border-radius:4px;background:rgba(255,255,255,0.15);position:relative;cursor:pointer;overflow:hidden}
.mp-bar-fill{position:absolute;left:0;top:0;height:100%;width:0%;border-radius:4px;background:var(--grad-main);transition:width .2s linear}
.mp-bar-fill::after{content:"";position:absolute;right:-5px;top:50%;transform:translateY(-50%);width:12px;height:12px;border-radius:50%;background:#fff;box-shadow:0 0 8px rgba(232,67,147,0.8);opacity:0;transition:opacity .2s}
.mp-bar:hover .mp-bar-fill::after{opacity:1}
.mp-right{display:flex;align-items:center;gap:6px;flex-shrink:0}
.mp-like{width:38px;height:38px;border-radius:50%;border:none;background:transparent;color:#fff;cursor:pointer;display:grid;place-items:center;transition:all .3s}
.mp-like:hover{background:rgba(255,255,255,0.12)}
.mp-like svg{width:20px;height:20px;fill:none;stroke:#fff;stroke-width:2;transition:all .3s}
.mp-like.liked svg{fill:#e84393;stroke:#e84393}
/* ============ 页脚 ============ */
footer{
margin-top:60px;padding:50px 0 110px;position:relative;
background:linear-gradient(180deg,transparent,rgba(108,92,231,0.25) 40%,rgba(232,67,147,0.2));
border-top:1px solid var(--glass-border);
}
.foot-inner{display:flex;flex-wrap:wrap;gap:40px;justify-content:space-between}
.foot-col h4{font-size:16px;font-weight:700;margin-bottom:16px}
.foot-col a{display:block;color:var(--text-soft);font-size:14px;margin-bottom:10px;transition:color .3s;cursor:pointer}
.foot-col a:hover{color:#fff}
.foot-brand .logo{font-size:24px}
.foot-brand p{color:var(--text-mute);font-size:13px;margin-top:14px;max-width:280px;line-height:1.7}
.foot-bottom{text-align:center;margin-top:40px;padding-top:24px;border-top:1px solid rgba(255,255,255,0.1);color:var(--text-mute);font-size:13px}
/* ============ 滚动动画 ============ */
.reveal{opacity:0;transform:translateY(30px);transition:opacity .7s ease,transform .7s ease}
.reveal.in{opacity:1;transform:translateY(0)}
/* ============ 提示 ============ */
.toast{
position:fixed;top:90px;left:50%;transform:translateX(-50%) translateY(-20px);
background:rgba(20,16,50,0.9);backdrop-filter:blur(14px);border:1px solid var(--glass-border);
padding:12px 24px;border-radius:30px;font-size:14px;z-index:2000;opacity:0;pointer-events:none;
transition:all .35s;box-shadow:var(--shadow);
}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
.empty-state{text-align:center;padding:60px 20px;color:var(--text-mute);grid-column:1/-1}
.empty-state svg{width:64px;height:64px;opacity:.4;margin-bottom:16px}
/* ============ 响应式 ============ */
@media(max-width:1100px){
.song-grid{grid-template-columns:repeat(3,1fr)}
.hot-grid{grid-template-columns:1fr 1fr}
.hero h1{font-size:38px}
.modal-top{flex-direction:column;align-items:center;text-align:center}
.modal-tags{justify-content:center}
.modal-stats{justify-content:center}
.modal-actions{justify-content:center}
}
@media(max-width:760px){
.container{padding:0 18px}
.nav-links{display:none}
.song-grid{grid-template-columns:repeat(2,1fr)}
.hot-grid{grid-template-columns:1fr}
.hero{padding:40px 26px;min-height:280px}
.hero h1{font-size:30px}
.hero-stats{gap:18px}
.sec-title{font-size:22px}
.modal-top{padding:30px 24px 20px;gap:20px}
.modal-cover{width:180px;height:180px}
.modal-info h2{font-size:24px}
.modal-body{padding:0 24px 30px}
.mp-info{flex:0 0 130px}
.mp-right .mp-btn:not(.mp-like){display:none}
.plaza-search{margin-left:0;width:100%}
.plaza-search input{width:100%}
}
</style>
</head>
<body>
<!-- 动态背景 -->
<div class="bg-canvas"></div>
<div class="blob b1"></div>
<div class="blob b2"></div>
<div class="blob b3"></div>
<div class="blob b4"></div>
<!-- 头部 -->
<header>
<div class="nav-inner">
<div class="logo"><span class="dot"></span>乐流 <span class="grad-text">Music</span></div>
<nav class="nav-links">
<a class="active" data-nav="home">首页</a>
<a data-nav="explore">探索</a>
<a data-nav="hot">热门</a>
<a data-nav="new">新歌</a>
</nav>
<div class="nav-search">
<span class="icon"><svg viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27a6.5 6.5 0 1 0-.7.7l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0A4.5 4.5 0 1 1 14 9.5 4.5 4.5 0 0 1 9.5 14z"/></svg></span>
<input id="navSearchInput" type="text" placeholder="搜索歌曲、歌手、专辑…">
</div>
<button class="nav-icon-btn" title="我的喜欢" id="navLikeBtn">
<svg viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
</button>
</div>
</header>
<!-- Hero -->
<section class="hero container reveal">
<div class="hero-shape s1"></div>
<div class="hero-shape s2"></div>
<div class="hero-shape s3"></div>
<h1>音乐无界,<span class="grad-text">流光溢彩</span></h1>
<p>沉浸于色彩的律动,让每一个音符都化作光影。千万曲库,一键聆听,开启你的专属音乐宇宙。</p>
<div class="actions">
<button class="btn-grad" id="heroListenBtn">立即聆听</button>
<button class="btn-ghost" id="heroExploreBtn">浏览曲库</button>
</div>
<div class="hero-stats">
<div class="st"><b class="grad-text">1280万+</b><span>在线曲库</span></div>
<div class="st"><b class="grad-text">320K</b><span>无损音质</span></div>
<div class="st"><b class="grad-text">每日更新</b><span>新歌首发</span></div>
</div>
</section>
<!-- 分类筛选 -->
<section class="container reveal" id="secCat" style="padding-top:8px">
<div class="sec-head">
<div class="sec-title"><span class="bar"></span>音乐分类</div>
<span class="sec-sub">查看全部 ›</span>
</div>
<div class="cat-bar" id="catBar">
<div class="cat-pill skeleton" style="width:80px;height:38px;border-radius:22px"></div>
<div class="cat-pill skeleton" style="width:70px;height:38px;border-radius:22px"></div>
<div class="cat-pill skeleton" style="width:90px;height:38px;border-radius:22px"></div>
<div class="cat-pill skeleton" style="width:75px;height:38px;border-radius:22px"></div>
</div>
</section>
<!-- 热门榜单 -->
<section class="container reveal" id="secHot">
<div class="sec-head">
<div class="sec-title"><span class="bar"></span>热门榜单</div>
<span class="sec-sub" data-scroll="plaza">查看全部 ›</span>
</div>
<div class="hot-grid" id="hotGrid">
<div class="sk-card"><div class="sk-cover skeleton" style="aspect-ratio:1.4"></div></div>
<div style="display:flex;flex-direction:column;gap:16px">
<div class="skeleton" style="height:88px;border-radius:18px"></div>
<div class="skeleton" style="height:88px;border-radius:18px"></div>
</div>
<div style="display:flex;flex-direction:column;gap:16px">
<div class="skeleton" style="height:88px;border-radius:18px"></div>
<div class="skeleton" style="height:88px;border-radius:18px"></div>
</div>
</div>
</section>
<!-- 新歌首发 -->
<section class="container reveal" id="secNew">
<div class="sec-head">
<div class="sec-title"><span class="bar"></span>新歌首发</div>
<span class="sec-sub" data-scroll="plaza">更多新歌 ›</span>
</div>
<div class="new-scroll" id="newScroll">
<div class="new-card"><div class="nc-cover skeleton"></div></div>
<div class="new-card"><div class="nc-cover skeleton"></div></div>
<div class="new-card"><div class="nc-cover skeleton"></div></div>
<div class="new-card"><div class="nc-cover skeleton"></div></div>
</div>
</section>
<!-- 音乐广场 -->
<section class="container reveal" id="plaza">
<div class="sec-head">
<div class="sec-title"><span class="bar"></span>音乐广场</div>
<span class="sec-sub" id="plazaCount">—</span>
</div>
<div class="plaza-toolbar">
<div class="sort-group" id="sortGroup">
<div class="sort-btn active" data-sort="">默认</div>
<div class="sort-btn" data-sort="plays">播放最多</div>
<div class="sort-btn" data-sort="likes">收藏最多</div>
<div class="sort-btn" data-sort="newest">最新发布</div>
</div>
<div class="plaza-search">
<div class="wrap">
<input id="plazaSearchInput" type="text" placeholder="搜索歌曲…">
</div>
</div>
</div>
<div class="song-grid" id="songGrid"></div>
<div class="pagination" id="pagination"></div>
</section>
<!-- 页脚 -->
<footer>
<div class="container foot-inner">
<div class="foot-col foot-brand">
<div class="logo"><span class="dot"></span>乐流 <span class="grad-text">Music</span></div>
<p>音乐无界,流光溢彩。用色彩诠释旋律,用科技传递感动。</p>
</div>
<div class="foot-col">
<h4>产品</h4>
<a>在线播放</a><a>个性推荐</a><a>歌单广场</a><a>无损音质</a>
</div>
<div class="foot-col">
<h4>发现</h4>
<a>热门榜单</a><a>新歌首发</a><a>分类浏览</a><a>歌手专区</a>
</div>
<div class="foot-col">
<h4>关于</h4>
<a>关于我们</a><a>用户协议</a><a>隐私政策</a><a>联系合作</a>
</div>
</div>
<div class="container foot-bottom">© 2026 乐流 Music · 渐变流媒体风格模板 · 仅供学习演示</div>
</footer>
<!-- 详情弹窗 -->
<div class="modal-mask" id="modalMask">
<div class="modal" id="modalBox"></div>
</div>
<!-- 迷你播放器 -->
<div class="mini-player" id="miniPlayer">
<div class="mp-cover" id="mpCover"></div>
<div class="mp-info">
<h4 id="mpTitle">未在播放</h4>
<p id="mpArtist">—</p>
</div>
<div class="mp-controls">
<button class="mp-btn" id="mpPrev" title="上一首"><svg viewBox="0 0 24 24"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg></button>
<button class="mp-btn mp-play" id="mpPlay" title="播放/暂停"><svg id="mpPlayIcon" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg></button>
<button class="mp-btn" id="mpNext" title="下一首"><svg viewBox="0 0 24 24"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg></button>
</div>
<div class="mp-progress">
<span class="mp-time" id="mpCur">0:00</span>
<div class="mp-bar" id="mpBar"><div class="mp-bar-fill" id="mpFill"></div></div>
<span class="mp-time" id="mpDur">0:00</span>
</div>
<div class="mp-right">
<button class="mp-btn" title="音量"><svg viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0 0 14 7.97v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/></svg></button>
<button class="mp-like" id="mpLike" title="喜欢"><svg viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg></button>
</div>
</div>
<!-- 提示 -->
<div class="toast" id="toast"></div>
<script>
/* ============================================================
乐流 Music — 渐变流媒体风格 单文件模板
全部 API 使用 fetch,失败时回退到内置 Mock 数据以保证模板可独立预览
============================================================ */
/* ---------- 工具函数 ---------- */
function hashString(str){
let h=0;str=str||'music';
for(let i=0;i<str.length;i++){h=((h<<5)-h)+str.charCodeAt(i);h|=0;}
return Math.abs(h);
}
function coverGradient(seed){
const h=hashString(seed);
const h1=h%360, h2=(h1+50+ (h%80))%360, h3=(h1+180)%360;
const a1=120+(h%30), a2=a1+60;
return `linear-gradient(${a1%360}deg, hsl(${h1},85%,62%), hsl(${h2},80%,55%) ${50+(h%20)}%, hsl(${h3},75%,48%))`;
}
function formatDuration(sec){
sec=parseInt(sec||0,10);
const m=Math.floor(sec/60), s=sec%60;
return m+':'+(s<10?'0':'')+s;
}
function formatNumber(n){
n=parseInt(n||0,10);
if(n>=100000000) return (n/100000000).toFixed(1).replace(/\.0$/,'')+'亿';
if(n>=10000) return (n/10000).toFixed(1).replace(/\.0$/,'')+'万';
return String(n);
}
function parseTags(t){
if(!t) return [];
if(Array.isArray(t)) return t;
try{ const v=JSON.parse(t); return Array.isArray(v)?v:[v]; }catch(e){ return String(t).split(',').map(s=>s.trim()).filter(Boolean); }
}
function escapeHtml(s){
return String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
function debounce(fn,wait){
let t;return function(){const ctx=this,args=arguments;clearTimeout(t);t=setTimeout(()=>fn.apply(ctx,args),wait);};
}
let toastTimer;
function toast(msg){
const el=document.getElementById('toast');
el.textContent=msg;el.classList.add('show');
clearTimeout(toastTimer);
toastTimer=setTimeout(()=>el.classList.remove('show'),2200);
}
/* ---------- API 层(fetch + Mock 回退) ---------- */
const API_BASE='/api/v1/music';
async function fetchJSON(url){
try{
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 && json.code===0) return json.data;
if(Array.isArray(json)) return json; // 兼容直接返回数组
if(json && json.data!==undefined) return json.data;
return json;
}catch(e){
return null; // 由调用方决定回退
}
}
/* ---------- 内置 Mock 数据 ---------- */
const MOCK = (function(){
const cats=[
{id:1,name:'流行',icon:'🎤',sort:1},
{id:2,name:'摇滚',icon:'🎸',sort:2},
{id:3,name:'民谣',icon:'🍂',sort:3},
{id:4,name:'电子',icon:'⚡',sort:4},
{id:5,name:'嘻哈',icon:'🎧',sort:5},
{id:6,name:'R&B',icon:'💜',sort:6},
{id:7,name:'古典',icon:'🎻',sort:7},
{id:8,name:'爵士',icon:'🎷',sort:8}
];
const titles=['星河漫游','霓虹之夜','夏日絮语','蓝调心跳','迷雾森林','月光奏鸣','流光成河','逆光飞翔','深夜电台','城市孤岛','云上漫步','时间胶囊','碎裂星光','潮汐之梦','未寄出的信','风的形状','琥珀色黄昏','无人区玫瑰','银河铁道','碎银几两','脉冲星','雨后初晴','极光之下','旧时光','孤勇者','无名之辈','盛夏光年','漂流瓶','像素心跳','永恒回响'];
const artists=['林夕颜','陈未眠','苏沐辰','周予安','何以琛','顾南风','白洛因','江晚吟','沈清辞','叶知秋','萧亦然','楚子航'];
const albums=['幻境','余烬','潮汐','萤火','破晓','深渊之上','霓虹','未完成','琥珀','回声'];
const langs=['华语','英语','日语','韩语'];
const tagPool=['治愈','深夜','跑步','通勤','雨天','旅行','怀旧','燃','慵懒','浪漫','独立','氛围'];
const lyricSamples=[
'当星光坠入海面\n我把心事折成纸船\n漂向你不知道的远方\n风替我说出那句晚安\n\n时间是一场缓慢的雪\n覆盖所有来不及的告别\n而我在流光里\n依然记得你的轮廓',
'霓虹闪烁的街角\n影子比人更孤单\n耳机里循环的旋律\n是我不敢拨出的号码\n\n城市很大 大到装不下想念\n夜色很深 深得藏住了疲倦\n我们都在假装 假装一切都好',
'你说夏天会很长\n可蝉鸣转眼就停了\n冰棍化成甜腻的水\n像我们没说出口的话\n\n青春是一场不散场的电影\n散场后我们各自走远\n回头时只剩\n一地碎银般的月光',
'月光落在琴键上\n像你轻声的叹息\n我把思念谱成前奏\n却写不出副歌的结局\n\n如果声音会老去\n就让这首永远年轻\n在每个失眠的夜里\n替我陪着你'
];
function pick(a){return a[Math.floor(Math.random()*a.length)];}
function rand(min,max){return Math.floor(Math.random()*(max-min+1))+min;}
const songs=[];
for(let i=0;i<titles.length;i++){
const cat=cats[i%cats.length];
const isHot=i<10;
const isNew=i>=titles.length-8;
songs.push({
id:i+1,
title:titles[i],
artist:artists[i%artists.length],
album:albums[i%albums.length],
category_id:cat.id,
category_name:cat.name,
cover:'',
duration:rand(180,320),
play_count:rand(50000,98000000),
like_count:rand(1000,5600000),
lyrics:lyricSamples[i%lyricSamples.length],
release_date:'202'+rand(3,6)+'-'+String(rand(1,12)).padStart(2,'0')+'-'+String(rand(1,28)).padStart(2,'0'),
language:langs[i%langs.length],
tags:JSON.stringify([pick(tagPool),pick(tagPool),pick(tagPool)]),
is_hot:isHot,
is_new:isNew,
sort:i+1,
status:1
});
}
return {cats,songs};
})();
/* 模拟分页/筛选 */
function mockSongs(query){
let list=MOCK.songs.slice();
if(query.category_id){ list=list.filter(s=>s.category_id==query.category_id); }
if(query.keyword){ const k=query.keyword.toLowerCase(); list=list.filter(s=>(s.title+s.artist+s.album).toLowerCase().includes(k)); }
if(query.sort==='plays') list.sort((a,b)=>b.play_count-a.play_count);
else if(query.sort==='likes') list.sort((a,b)=>b.like_count-a.like_count);
else if(query.sort==='newest') list.sort((a,b)=>(b.release_date>'').toString().localeCompare('')||(b.release_date||'').localeCompare(a.release_date||''));
const total=list.length;
const page=parseInt(query.page||1,10), ps=parseInt(query.page_size||10,10);
const start=(page-1)*ps;
return {list:list.slice(start,start+ps), total, page, page_size:ps};
}
/* 统一取数:优先 API,失败回退 Mock */
async function apiHome(){
const d=await fetchJSON(API_BASE+'/home');
if(d && d.categories && d.hot && d.new) return d;
return {
categories:MOCK.cats,
hot:MOCK.songs.filter(s=>s.is_hot).slice(0,8),
new:MOCK.songs.filter(s=>s.is_new).slice(0,4)
};
}
async function apiCategories(){
const d=await fetchJSON(API_BASE+'/categories');
if(Array.isArray(d)&&d.length) return d;
return MOCK.cats;
}
async function apiSongs(query){
const qs=new URLSearchParams();
if(query.category_id)qs.set('category_id',query.category_id);
if(query.keyword)qs.set('keyword',query.keyword);
if(query.sort)qs.set('sort',query.sort);
qs.set('page',query.page||1);
qs.set('page_size',query.page_size||10);
const d=await fetchJSON(API_BASE+'/songs?'+qs.toString());
if(d && d.list) return d;
return mockSongs(query);
}
async function apiSongDetail(id){
const d=await fetchJSON(API_BASE+'/songs/'+id);
if(d && d.id) return d;
return MOCK.songs.find(s=>s.id==id) || MOCK.songs[0];
}
/* ---------- 渲染:封面元素 ---------- */
function coverHTML(song,cls){
if(song.cover){
return `<div class="${cls}" style="background:url('${escapeHtml(song.cover)}') center/cover"></div>`;
}
return `<div class="${cls}" style="background:${coverGradient(song.title+song.artist)}"></div>`;
}
/* ---------- 渲染:分类 ---------- */
let currentCat='';
async function renderCategories(){
const cats=await apiCategories();
const bar=document.getElementById('catBar');
bar.innerHTML='';
const all=document.createElement('div');
all.className='cat-pill active';all.textContent='全部';all.dataset.id='';
bar.appendChild(all);
cats.sort((a,b)=>(a.sort||0)-(b.sort||0)).forEach(c=>{
const el=document.createElement('div');
el.className='cat-pill';el.dataset.id=c.id;
el.innerHTML=`<span style="margin-right:5px">${c.icon||'🎵'}</span>${escapeHtml(c.name)}`;
bar.appendChild(el);
});
bar.querySelectorAll('.cat-pill').forEach(p=>{
p.addEventListener('click',()=>{
bar.querySelectorAll('.cat-pill').forEach(x=>x.classList.remove('active'));
p.classList.add('active');
currentCat=p.dataset.id;
plazaState.page=1;
renderPlaza();
});
});
}
/* ---------- 渲染:热门 ---------- */
async function renderHot(){
const home=await apiHome();
const hot=home.hot||[];
const grid=document.getElementById('hotGrid');
if(!hot.length){ grid.innerHTML='<div class="empty-state">暂无热门歌曲</div>'; return; }
const top=hot[0];
let side1='', side2='';
for(let i=1;i<hot.length && i<5;i++){
side1+=hotRowHTML(hot[i],i+1);
}
for(let i=5;i<hot.length && i<9;i++){
side2+=hotRowHTML(hot[i],i+1);
}
grid.innerHTML=`
<div class="hot-feature" data-id="${top.id}">
${coverHTML(top,'cover')}
<div class="scrim"></div>
<div class="rank-badge gold">1</div>
<div class="play-overlay"><svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg></div>
<div class="info">
<h3>${escapeHtml(top.title)}</h3>
<div class="meta">${escapeHtml(top.artist)} · ${formatNumber(top.play_count)}次播放</div>
</div>
</div>
<div class="hot-side">${side1}</div>
<div class="hot-side">${side2}</div>`;
bindSongClicks(grid);
}
function hotRowHTML(s,rank){
return `<div class="hot-row" data-id="${s.id}">
<div class="rank">${rank}</div>
${coverHTML(s,'mini-cover')}
<div class="t-info">
<h4>${escapeHtml(s.title)}</h4>
<p>${escapeHtml(s.artist)} · ${formatDuration(s.duration)}</p>
</div>
${s.is_new?'<div class="tag-mini new">NEW</div>':''}
</div>`;
}
/* ---------- 渲染:新歌 ---------- */
async function renderNew(){
const home=await apiHome();
const news=home.new||[];
const sc=document.getElementById('newScroll');
if(!news.length){ sc.innerHTML='<div class="empty-state">暂无新歌</div>'; return; }
sc.innerHTML=news.map(s=>`
<div class="new-card" data-id="${s.id}">
<div class="nc-cover">
<div class="new-badge">NEW</div>
${coverHTML(s,'')}
<div class="play-overlay"><svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg></div>
</div>
<div class="nc-body">
<h4>${escapeHtml(s.title)}</h4>
<p>${escapeHtml(s.artist)}</p>
</div>
</div>`).join('');
bindSongClicks(sc);
}
/* ---------- 渲染:音乐广场 ---------- */
const plazaState={keyword:'',sort:'',page:1,page_size:10,total:0,loading:false};
async function renderPlaza(){
if(plazaState.loading) return;
plazaState.loading=true;
const grid=document.getElementById('songGrid');
// 骨架
let sk='';
for(let i=0;i<10;i++) sk+=`<div class="sk-card"><div class="sk-cover skeleton"></div><div class="sk-line skeleton"></div><div class="sk-line short skeleton"></div></div>`;
grid.innerHTML=sk;
const data=await apiSongs({category_id:currentCat,keyword:plazaState.keyword,sort:plazaState.sort,page:plazaState.page,page_size:plazaState.page_size});
plazaState.total=data.total||0;
document.getElementById('plazaCount').textContent=`共 ${data.total} 首`;
if(!data.list.length){
grid.innerHTML=`<div class="empty-state">
<svg viewBox="0 0 24 24" fill="rgba(255,255,255,0.4)"><path d="M15.5 14h-.79l-.28-.27a6.5 6.5 0 1 0-.7.7l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0A4.5 4.5 0 1 1 14 9.5 4.5 4.5 0 0 1 9.5 14z"/></svg>
<div>没有找到相关歌曲,换个关键词试试吧</div></div>`;
}else{
grid.innerHTML=data.list.map(s=>songCardHTML(s)).join('');
bindSongClicks(grid);
}
renderPagination();
plazaState.loading=false;
}
function songCardHTML(s){
return `<div class="song-card" data-id="${s.id}">
<div class="sc-cover">
${coverHTML(s,'')}
${s.is_hot?'<div class="tag-mini hot">HOT</div>':(s.is_new?'<div class="tag-mini new">NEW</div>':'')}
<div class="play-overlay"><svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg></div>
</div>
<div class="sc-body">
<h4>${escapeHtml(s.title)}</h4>
<p>${escapeHtml(s.artist)}</p>
<div class="sc-stats">
<span><svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>${formatNumber(s.play_count)}</span>
<span><svg viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>${formatNumber(s.like_count)}</span>
<span>${formatDuration(s.duration)}</span>
</div>
</div>
</div>`;
}
function renderPagination(){
const total=plazaState.total, ps=plazaState.page_size, cur=plazaState.page;
const pages=Math.ceil(total/ps)||1;
const el=document.getElementById('pagination');
if(pages<=1){ el.innerHTML=`<span class="page-info">第 ${cur} / ${pages} 页</span>`; return; }
let html=`<button class="page-btn" data-page="${cur-1}" ${cur<=1?'disabled':''}>‹</button>`;
const win=2;
for(let p=1;p<=pages;p++){
if(p===1||p===pages||Math.abs(p-cur)<=win){
html+=`<button class="page-btn ${p===cur?'active':''}" data-page="${p}">${p}</button>`;
}else if(Math.abs(p-cur)===win+1){
html+=`<span class="page-info">…</span>`;
}
}
html+=`<button class="page-btn" data-page="${cur+1}" ${cur>=pages?'disabled':''}>›</button>`;
html+=`<span class="page-info">第 ${cur}/${pages} 页 · 共 ${total} 首</span>`;
el.innerHTML=html;
el.querySelectorAll('.page-btn[data-page]').forEach(b=>{
b.addEventListener('click',()=>{
if(b.disabled) return;
plazaState.page=parseInt(b.dataset.page,10);
renderPlaza();
document.getElementById('plaza').scrollIntoView({behavior:'smooth',block:'start'});
});
});
}
/* ---------- 歌曲点击绑定 ---------- */
function bindSongClicks(scope){
scope.querySelectorAll('[data-id]').forEach(el=>{
el.addEventListener('click',()=>openModal(el.dataset.id));
});
}
/* ---------- 详情弹窗 ---------- */
const likedSet=new Set();
async function openModal(id){
const mask=document.getElementById('modalMask');
const box=document.getElementById('modalBox');
box.innerHTML=`<div style="padding:60px;text-align:center;color:rgba(255,255,255,0.5)">加载中…</div>`;
mask.classList.add('show');
document.body.style.overflow='hidden';
const s=await apiSongDetail(id);
const tags=parseTags(s.tags);
const lyrics=(s.lyrics||'').split('\n').filter(l=>l.trim()!=='');
const liked=likedSet.has(parseInt(s.id,10));
box.innerHTML=`
<button class="modal-close" id="modalClose">
<svg viewBox="0 0 24 24" width="20" height="20" fill="#fff"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
<div class="modal-top">
<div class="modal-cover">
${coverHTML(s,'')}
<div class="pulse"></div>
</div>
<div class="modal-info">
<span class="m-cat">${escapeHtml(s.category_name||'音乐')}</span>
<h2>${escapeHtml(s.title)}</h2>
<div class="m-artist">${escapeHtml(s.artist)}</div>
<div class="m-album">专辑:《${escapeHtml(s.album||'未知')}》· ${escapeHtml(s.language||'')} · ${escapeHtml(s.release_date||'')}</div>
<div class="modal-tags">
${tags.map(t=>`<span class="tg">#${escapeHtml(t)}</span>`).join('')||'<span class="tg">#音乐</span>'}
</div>
<div class="modal-stats">
<div class="ms"><b class="grad-text">${formatNumber(s.play_count)}</b><span>播放量</span></div>
<div class="ms"><b class="grad-text">${formatNumber(s.like_count)}</b><span>收藏量</span></div>
<div class="ms"><b class="grad-text">${formatDuration(s.duration)}</b><span>时长</span></div>
</div>
<div class="modal-actions">
<button class="btn-play-glow" id="modalPlayBtn">
<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>播放
</button>
<button class="btn-like ${liked?'liked':''}" id="modalLikeBtn" title="收藏">
<svg viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
</button>
</div>
</div>
</div>
<div class="modal-body">
<div class="lyrics-block">
<h5><span class="bar"></span>歌词</h5>
${lyrics.length?lyrics.map(l=>`<div class="lyrics-line">${escapeHtml(l)}</div>`).join(''):'<div class="lyrics-empty">暂无歌词,纯音乐同样动人</div>'}
</div>
</div>`;
document.getElementById('modalClose').addEventListener('click',closeModal);
mask.addEventListener('click',function(e){ if(e.target===mask) closeModal(); });
document.getElementById('modalPlayBtn').addEventListener('click',()=>{
playSong(s);
closeModal();
});
document.getElementById('modalLikeBtn').addEventListener('click',function(){
const sid=parseInt(s.id,10);
if(likedSet.has(sid)){ likedSet.delete(sid); this.classList.remove('liked'); toast('已取消收藏'); }
else{ likedSet.add(sid); this.classList.add('liked'); toast('已加入收藏'); updateMiniLike(); }
});
}
function closeModal(){
document.getElementById('modalMask').classList.remove('show');
document.body.style.overflow='';
}
document.addEventListener('keydown',e=>{ if(e.key==='Escape') closeModal(); });
/* ---------- 迷你播放器 ---------- */
const player={
song:null, playing:false, cur:0, timer:null, queue:[]
};
function playSong(song){
player.song=song;
player.cur=0;
player.playing=true;
// 构建简易队列(当前广场列表)
player.queue=lastPlazaList.length?lastPlazaList:[song];
updateMiniUI();
startProgress();
document.getElementById('miniPlayer').classList.add('show');
toast('正在播放:'+song.title);
}
let lastPlazaList=[];
function updateMiniUI(){
const s=player.song;if(!s) return;
const cov=document.getElementById('mpCover');
if(s.cover){ cov.innerHTML=`<img src="${escapeHtml(s.cover)}" alt="">`; }
else{ cov.innerHTML=`<div class="cdiv" style="background:${coverGradient(s.title+s.artist)}"></div>`; }
document.getElementById('mpTitle').textContent=s.title;
document.getElementById('mpArtist').textContent=s.artist;
document.getElementById('mpDur').textContent=formatDuration(s.duration);
document.getElementById('mpCur').textContent='0:00';
document.getElementById('mpFill').style.width='0%';
setPlayIcon(player.playing);
updateMiniLike();
}
function updateMiniLike(){
const btn=document.getElementById('mpLike');
if(player.song && likedSet.has(parseInt(player.song.id,10))) btn.classList.add('liked');
else btn.classList.remove('liked');
}
function setPlayIcon(playing){
const ic=document.getElementById('mpPlayIcon');
ic.innerHTML=playing?'<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>':'<path d="M8 5v14l11-7z"/>';
}
function startProgress(){
clearInterval(player.timer);
player.timer=setInterval(()=>{
if(!player.playing||!player.song) return;
player.cur+=1;
if(player.cur>=player.song.duration){
player.cur=0;
nextSong();
return;
}
const pct=(player.cur/player.song.duration)*100;
document.getElementById('mpFill').style.width=pct+'%';
document.getElementById('mpCur').textContent=formatDuration(player.cur);
},1000);
}
function togglePlay(){
if(!player.song){ toast('请先选择一首歌曲'); return; }
player.playing=!player.playing;
setPlayIcon(player.playing);
if(player.playing) startProgress(); else clearInterval(player.timer);
}
function nextSong(){
if(!player.queue.length) return;
const idx=player.queue.findIndex(s=>s.id===(player.song&&player.song.id));
const next=player.queue[(idx+1)%player.queue.length];
if(next) playSong(next);
}
function prevSong(){
if(!player.queue.length) return;
const idx=player.queue.findIndex(s=>s.id===(player.song&&player.song.id));
const prev=player.queue[(idx-1+player.queue.length)%player.queue.length];
if(prev) playSong(prev);
}
document.getElementById('mpPlay').addEventListener('click',togglePlay);
document.getElementById('mpNext').addEventListener('click',nextSong);
document.getElementById('mpPrev').addEventListener('click',prevSong);
document.getElementById('mpLike').addEventListener('click',function(){
if(!player.song){ toast('请先选择一首歌曲'); return; }
const sid=parseInt(player.song.id,10);
if(likedSet.has(sid)){ likedSet.delete(sid); this.classList.remove('liked'); toast('已取消收藏'); }
else{ likedSet.add(sid); this.classList.add('liked'); toast('已加入收藏'); }
});
document.getElementById('mpBar').addEventListener('click',function(e){
if(!player.song) return;
const rect=this.getBoundingClientRect();
const pct=(e.clientX-rect.left)/rect.width;
player.cur=Math.floor(pct*player.song.duration);
document.getElementById('mpFill').style.width=(pct*100)+'%';
document.getElementById('mpCur').textContent=formatDuration(player.cur);
});
/* ---------- 搜索 ---------- */
const navSearchDebounced=debounce(function(val){
if(val){
plazaState.keyword=val; plazaState.page=1; currentCat='';
document.querySelectorAll('#catBar .cat-pill').forEach(x=>x.classList.remove('active'));
document.querySelector('#catBar .cat-pill').classList.add('active');
renderPlaza();
document.getElementById('plaza').scrollIntoView({behavior:'smooth'});
}
},300);
document.getElementById('navSearchInput').addEventListener('input',function(){
navSearchDebounced(this.value.trim());
});
const plazaSearchDebounced=debounce(function(val){
plazaState.keyword=val; plazaState.page=1; renderPlaza();
},300);
document.getElementById('plazaSearchInput').addEventListener('input',function(){
plazaSearchDebounced(this.value.trim());
});
/* ---------- 排序 ---------- */
document.getElementById('sortGroup').addEventListener('click',function(e){
const btn=e.target.closest('.sort-btn'); if(!btn) return;
this.querySelectorAll('.sort-btn').forEach(b=>b.classList.remove('active'));
btn.classList.add('active');
plazaState.sort=btn.dataset.sort; plazaState.page=1; renderPlaza();
});
/* ---------- 导航 / 按钮 ---------- */
document.querySelectorAll('.nav-links a').forEach(a=>{
a.addEventListener('click',()=>{
document.querySelectorAll('.nav-links a').forEach(x=>x.classList.remove('active'));
a.classList.add('active');
const t=a.dataset.nav;
const map={home:'secCat',explore:'plaza',hot:'secHot',new:'secNew'};
if(map[t]) document.getElementById(map[t]).scrollIntoView({behavior:'smooth'});
});
});
document.getElementById('heroListenBtn').addEventListener('click',async()=>{
const home=await apiHome();
if(home.hot&&home.hot[0]) playSong(home.hot[0]);
else toast('暂无可播放的歌曲');
});
document.getElementById('heroExploreBtn').addEventListener('click',()=>{
document.getElementById('plaza').scrollIntoView({behavior:'smooth'});
});
document.querySelectorAll('[data-scroll]').forEach(el=>{
el.addEventListener('click',()=>{ document.getElementById(el.dataset.scroll).scrollIntoView({behavior:'smooth'}); });
});
document.getElementById('navLikeBtn').addEventListener('click',()=>{
if(!likedSet.size){ toast('还没有收藏的歌曲,去点亮一颗心吧'); return; }
toast('已收藏 '+likedSet.size+' 首歌曲');
});
/* ---------- 滚动动画 ---------- */
const io=new IntersectionObserver(entries=>{
entries.forEach(en=>{ if(en.isIntersecting){ en.target.classList.add('in'); io.unobserve(en.target); } });
},{threshold:0.12});
document.querySelectorAll('.reveal').forEach(el=>io.observe(el));
/* ---------- 背景粒子 ---------- */
(function spawnParticles(){
const n=14;
for(let i=0;i<n;i++){
const p=document.createElement('div');
p.className='particle';
p.style.left=Math.random()*100+'%';
p.style.width=p.style.height=(3+Math.random()*7)+'px';
p.style.animationDuration=(12+Math.random()*16)+'s';
p.style.animationDelay=(-Math.random()*20)+'s';
p.style.background=['rgba(255,255,255,0.6)','rgba(253,121,168,0.6)','rgba(116,185,255,0.6)'][i%3];
document.body.appendChild(p);
}
})();
/* ---------- 初始化 ---------- */
(async function init(){
renderCategories();
renderHot();
renderNew();
await renderPlaza();
// 记录初始广场列表
setTimeout(()=>{
const ids=[...document.querySelectorAll('#songGrid [data-id]')].map(e=>parseInt(e.dataset.id,10));
if(ids.length){
Promise.all(ids.map(id=>apiSongDetail(id))).then(list=>{ lastPlazaList=list; });
}
},800);
})();
</script>
</body>
</html>