- 导航页(index.html)添加完整i18n引擎,支持zh-CN/zh-TW/en - 导航页添加语言切换按钮(EN/中/繁) - 所有20个前端模板默认语言从zh-CN改为en - 导航页SEO meta标签改为英文
1421 lines
71 KiB
HTML
1421 lines
71 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>乐流 Music — 音乐无界,流光溢彩</title>
|
||
<style>
|
||
/* ============ 基础变量与重置 ============ */
|
||
: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}
|
||
|
||
/* Language switcher - gradient theme */
|
||
.lang-switcher{position:relative;flex-shrink:0}
|
||
.lang-btn{
|
||
display:flex;align-items:center;gap:6px;padding:7px 14px;border-radius:20px;
|
||
font-size:13px;font-weight:600;color:var(--text-soft);cursor:pointer;
|
||
background:var(--glass);border:1px solid var(--glass-border);
|
||
transition:all .3s;
|
||
}
|
||
.lang-btn:hover{color:#fff;background:var(--glass-strong);border-color:rgba(255,255,255,0.35)}
|
||
.lang-btn .ico{font-size:15px}
|
||
.lang-dropdown{
|
||
position:absolute;top:calc(100% + 8px);right:0;min-width:150px;z-index:1001;
|
||
background:rgba(20,16,50,0.85);backdrop-filter:blur(20px);border:1px solid var(--glass-border);
|
||
border-radius:var(--radius);padding:6px;
|
||
box-shadow:0 12px 40px rgba(0,0,0,0.5);
|
||
opacity:0;visibility:hidden;transform:translateY(-8px);transition:.25s;
|
||
}
|
||
.lang-dropdown.open{opacity:1;visibility:visible;transform:translateY(0)}
|
||
.lang-option{
|
||
display:flex;align-items:center;gap:10px;padding:9px 14px;border-radius:10px;
|
||
font-size:13px;color:var(--text-soft);cursor:pointer;transition:all .2s;
|
||
}
|
||
.lang-option:hover{background:rgba(108,92,231,0.15);color:#fff}
|
||
.lang-option.active{color:#fff;font-weight:700}
|
||
.lang-option .check{font-size:14px;opacity:0;transition:opacity .2s}
|
||
.lang-option.active .check{opacity:1}
|
||
|
||
/* ============ 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" data-i18n="nav_home">首页</a>
|
||
<a data-nav="explore" data-i18n="nav_explore">探索</a>
|
||
<a data-nav="hot" data-i18n="nav_hot">热门</a>
|
||
<a data-nav="new" data-i18n="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" data-i18n-placeholder="search_placeholder" placeholder="搜索歌曲、歌手、专辑…">
|
||
</div>
|
||
<div class="lang-switcher" id="langSwitcher">
|
||
<button class="lang-btn" id="langBtn" onclick="toggleLangDropdown()">
|
||
<span class="ico">🌐</span>
|
||
<span id="langLabel">中文</span>
|
||
</button>
|
||
<div class="lang-dropdown" id="langDropdown">
|
||
<div class="lang-option" data-lang="zh-CN" onclick="switchLang('zh-CN')">
|
||
<span class="check">✓</span><span>简体中文</span>
|
||
</div>
|
||
<div class="lang-option" data-lang="zh-TW" onclick="switchLang('zh-TW')">
|
||
<span class="check">✓</span><span>繁體中文</span>
|
||
</div>
|
||
<div class="lang-option" data-lang="en" onclick="switchLang('en')">
|
||
<span class="check">✓</span><span>English</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<button class="nav-icon-btn" data-i18n-title="nav_like_title" 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 data-i18n="hero_title">音乐无界,<span class="grad-text">流光溢彩</span></h1>
|
||
<p data-i18n="hero_subtitle">沉浸于色彩的律动,让每一个音符都化作光影。千万曲库,一键聆听,开启你的专属音乐宇宙。</p>
|
||
<div class="actions">
|
||
<button class="btn-grad" id="heroListenBtn" data-i18n="hero_listen">立即聆听</button>
|
||
<button class="btn-ghost" id="heroExploreBtn" data-i18n="hero_browse">浏览曲库</button>
|
||
</div>
|
||
<div class="hero-stats">
|
||
<div class="st"><b class="grad-text">1280万+</b><span data-i18n="stat_songs">在线曲库</span></div>
|
||
<div class="st"><b class="grad-text">320K</b><span data-i18n="stat_quality">无损音质</span></div>
|
||
<div class="st"><b class="grad-text" data-i18n="stat_daily_val">每日更新</b><span data-i18n="stat_daily">新歌首发</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><span data-i18n="section_genres">音乐分类</span></div>
|
||
<span class="sec-sub" data-i18n="see_all">查看全部 ›</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><span data-i18n="section_hot">热门榜单</span></div>
|
||
<span class="sec-sub" data-scroll="plaza" data-i18n="see_all">查看全部 ›</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><span data-i18n="section_new">新歌首发</span></div>
|
||
<span class="sec-sub" data-scroll="plaza" data-i18n="more_new">更多新歌 ›</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><span data-i18n="section_plaza">音乐广场</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="" data-i18n="sort_default">默认</div>
|
||
<div class="sort-btn" data-sort="plays" data-i18n="sort_plays">播放最多</div>
|
||
<div class="sort-btn" data-sort="likes" data-i18n="sort_likes">收藏最多</div>
|
||
<div class="sort-btn" data-sort="newest" data-i18n="sort_newest">最新发布</div>
|
||
</div>
|
||
<div class="plaza-search">
|
||
<div class="wrap">
|
||
<input id="plazaSearchInput" type="text" data-i18n-placeholder="plaza_search_placeholder" 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 data-i18n="footer_desc">音乐无界,流光溢彩。用色彩诠释旋律,用科技传递感动。</p>
|
||
</div>
|
||
<div class="foot-col">
|
||
<h4 data-i18n="footer_product">产品</h4>
|
||
<a data-i18n="footer_play">在线播放</a><a data-i18n="footer_recommend">个性推荐</a><a data-i18n="footer_playlist">歌单广场</a><a data-i18n="footer_lossless">无损音质</a>
|
||
</div>
|
||
<div class="foot-col">
|
||
<h4 data-i18n="footer_discover">发现</h4>
|
||
<a data-i18n="nav_hot">热门榜单</a><a data-i18n="section_new">新歌首发</a><a data-i18n="section_genres">分类浏览</a><a data-i18n="footer_artist">歌手专区</a>
|
||
</div>
|
||
<div class="foot-col">
|
||
<h4 data-i18n="footer_about">关于</h4>
|
||
<a data-i18n="footer_about_us">关于我们</a><a data-i18n="footer_terms">用户协议</a><a data-i18n="footer_privacy">隐私政策</a><a data-i18n="footer_contact">联系合作</a>
|
||
</div>
|
||
</div>
|
||
<div class="container foot-bottom">© 2026 乐流 Music · <span data-i18n="footer_subtitle">渐变流媒体风格模板 · 仅供学习演示</span></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" data-i18n="player_idle_title">未在播放</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>
|
||
/* ============================================================
|
||
i18n 国际化引擎
|
||
============================================================ */
|
||
const I18N_LOCALES = {
|
||
'zh-CN': {
|
||
title: '乐流 Music — 音乐无界,流光溢彩',
|
||
nav_home: '首页', nav_explore: '探索', nav_hot: '热门', nav_new: '新歌',
|
||
nav_like_title: '我的喜欢',
|
||
search_placeholder: '搜索歌曲、歌手、专辑…',
|
||
hero_title: '音乐无界,流光溢彩',
|
||
hero_subtitle: '沉浸于色彩的律动,让每一个音符都化作光影。千万曲库,一键聆听,开启你的专属音乐宇宙。',
|
||
hero_listen: '立即聆听', hero_browse: '浏览曲库',
|
||
stat_songs: '在线曲库', stat_quality: '无损音质', stat_daily_val: '每日更新', stat_daily: '新歌首发',
|
||
section_genres: '音乐分类', section_hot: '热门榜单', section_new: '新歌首发', section_plaza: '音乐广场',
|
||
see_all: '查看全部 ›', more_new: '更多新歌 ›',
|
||
sort_default: '默认', sort_plays: '播放最多', sort_likes: '收藏最多', sort_newest: '最新发布',
|
||
plaza_search_placeholder: '搜索歌曲…',
|
||
plaza_count: '共 ',
|
||
player_idle_title: '未在播放',
|
||
cat_all: '全部', cat_pop: '流行', cat_rock: '摇滚', cat_folk: '民谣',
|
||
cat_electronic: '电子', cat_hiphop: '嘻哈', cat_rnb: 'R&B', cat_classical: '古典', cat_jazz: '爵士',
|
||
empty_categories: '暂无分类', empty_hot: '暂无热门歌曲', empty_new: '暂无新歌',
|
||
empty_plaza: '没有找到相关歌曲,换个关键词试试吧',
|
||
toast_now_playing: '正在播放:', toast_select_song: '请先选择一首歌曲',
|
||
toast_cancel_like: '已取消收藏', toast_add_like: '已加入收藏',
|
||
toast_no_liked: '还没有收藏的歌曲,去点亮一颗心吧', toast_liked_count: '已收藏 ',
|
||
toast_no_songs: '暂无可播放的歌曲',
|
||
tracks_unit: '首',
|
||
modal_plays: '播放量', modal_likes: '收藏量', modal_duration: '时长',
|
||
modal_album_prefix: '专辑:《', modal_album_suffix: '》',
|
||
modal_lyrics: '歌词', modal_no_lyrics: '暂无歌词,纯音乐同样动人',
|
||
modal_play: '播放', modal_like: '收藏', modal_liked: '已收藏',
|
||
footer_desc: '音乐无界,流光溢彩。用色彩诠释旋律,用科技传递感动。',
|
||
footer_product: '产品', footer_discover: '发现', footer_about: '关于',
|
||
footer_play: '在线播放', footer_recommend: '个性推荐', footer_playlist: '歌单广场', footer_lossless: '无损音质',
|
||
footer_artist: '歌手专区',
|
||
footer_about_us: '关于我们', footer_terms: '用户协议', footer_privacy: '隐私政策', footer_contact: '联系合作',
|
||
footer_subtitle: '渐变流媒体风格模板 · 仅供学习演示',
|
||
},
|
||
'zh-TW': {
|
||
title: '樂流 Music — 音樂無界,流光溢彩',
|
||
nav_home: '首頁', nav_explore: '探索', nav_hot: '熱門', nav_new: '新歌',
|
||
nav_like_title: '我的喜歡',
|
||
search_placeholder: '搜尋歌曲、歌手、專輯…',
|
||
hero_title: '音樂無界,流光溢彩',
|
||
hero_subtitle: '沉浸於色彩的律動,讓每一個音符都化作光影。千萬曲庫,一鍵聆聽,開啟你的專屬音樂宇宙。',
|
||
hero_listen: '立即聆聽', hero_browse: '瀏覽曲庫',
|
||
stat_songs: '線上曲庫', stat_quality: '無損音質', stat_daily_val: '每日更新', stat_daily: '新歌首發',
|
||
section_genres: '音樂分類', section_hot: '熱門榜單', section_new: '新歌首發', section_plaza: '音樂廣場',
|
||
see_all: '查看全部 ›', more_new: '更多新歌 ›',
|
||
sort_default: '預設', sort_plays: '播放最多', sort_likes: '收藏最多', sort_newest: '最新發布',
|
||
plaza_search_placeholder: '搜尋歌曲…',
|
||
plaza_count: '共 ',
|
||
player_idle_title: '未在播放',
|
||
cat_all: '全部', cat_pop: '流行', cat_rock: '搖滾', cat_folk: '民謠',
|
||
cat_electronic: '電子', cat_hiphop: '嘻哈', cat_rnb: 'R&B', cat_classical: '古典', cat_jazz: '爵士',
|
||
empty_categories: '暫無分類', empty_hot: '暫無熱門歌曲', empty_new: '暫無新歌',
|
||
empty_plaza: '沒有找到相關歌曲,換個關鍵詞試試吧',
|
||
toast_now_playing: '正在播放:', toast_select_song: '請先選擇一首歌曲',
|
||
toast_cancel_like: '已取消收藏', toast_add_like: '已加入收藏',
|
||
toast_no_liked: '還沒有收藏的歌曲,去點亮一顆心吧', toast_liked_count: '已收藏 ',
|
||
toast_no_songs: '暫無可播放的歌曲',
|
||
tracks_unit: '首',
|
||
modal_plays: '播放量', modal_likes: '收藏量', modal_duration: '時長',
|
||
modal_album_prefix: '專輯:《', modal_album_suffix: '》',
|
||
modal_lyrics: '歌詞', modal_no_lyrics: '暫無歌詞,純音樂同樣動人',
|
||
modal_play: '播放', modal_like: '收藏', modal_liked: '已收藏',
|
||
footer_desc: '音樂無界,流光溢彩。用色彩詮釋旋律,用科技傳遞感動。',
|
||
footer_product: '產品', footer_discover: '發現', footer_about: '關於',
|
||
footer_play: '線上播放', footer_recommend: '個性推薦', footer_playlist: '歌單廣場', footer_lossless: '無損音質',
|
||
footer_artist: '歌手專區',
|
||
footer_about_us: '關於我們', footer_terms: '用戶協議', footer_privacy: '隱私政策', footer_contact: '聯繫合作',
|
||
footer_subtitle: '漸變流媒體風格模板 · 僅供學習演示',
|
||
},
|
||
'en': {
|
||
title: 'LeMusic — Music Without Borders',
|
||
nav_home: 'Home', nav_explore: 'Explore', nav_hot: 'Hot', nav_new: 'New',
|
||
nav_like_title: 'My Likes',
|
||
search_placeholder: 'Search songs, artists, albums…',
|
||
hero_title: 'Music Without Borders, Streaming in Color',
|
||
hero_subtitle: 'Immerse in the rhythm of colors, let every note become light. Millions of songs, one click to play,开启 your personal music universe.',
|
||
hero_listen: 'Listen Now', hero_browse: 'Browse Library',
|
||
stat_songs: 'Online Library', stat_quality: 'Lossless Audio', stat_daily_val: 'Daily Updates', stat_daily: 'New Releases',
|
||
section_genres: 'Genres', section_hot: 'Hot Charts', section_new: 'New Releases', section_plaza: 'Music Plaza',
|
||
see_all: 'See All ›', more_new: 'More New ›',
|
||
sort_default: 'Default', sort_plays: 'Most Played', sort_likes: 'Most Liked', sort_newest: 'Newest',
|
||
plaza_search_placeholder: 'Search songs…',
|
||
plaza_count: '',
|
||
player_idle_title: 'Nothing Playing',
|
||
cat_all: 'All', cat_pop: 'Pop', cat_rock: 'Rock', cat_folk: 'Folk',
|
||
cat_electronic: 'Electronic', cat_hiphop: 'Hip-Hop', cat_rnb: 'R&B', cat_classical: 'Classical', cat_jazz: 'Jazz',
|
||
empty_categories: 'No categories', empty_hot: 'No hot songs yet', empty_new: 'No new releases yet',
|
||
empty_plaza: 'No songs found, try different keywords',
|
||
toast_now_playing: 'Now playing: ', toast_select_song: 'Select a song first',
|
||
toast_cancel_like: 'Removed from liked', toast_add_like: 'Added to liked',
|
||
toast_no_liked: 'No liked songs yet, go show some love', toast_liked_count: 'Liked ',
|
||
toast_no_songs: 'No songs available',
|
||
tracks_unit: '',
|
||
modal_plays: 'Plays', modal_likes: 'Likes', modal_duration: 'Duration',
|
||
modal_album_prefix: 'Album: ', modal_album_suffix: '',
|
||
modal_lyrics: 'Lyrics', modal_no_lyrics: 'No lyrics available, instrumental music speaks for itself',
|
||
modal_play: 'Play', modal_like: 'Like', modal_liked: 'Liked',
|
||
footer_desc: 'Music without borders, streaming in color. Interpreting melody with color, delivering emotion with technology.',
|
||
footer_product: 'Product', footer_discover: 'Discover', footer_about: 'About',
|
||
footer_play: 'Online Play', footer_recommend: 'Recommendations', footer_playlist: 'Playlists', footer_lossless: 'Lossless Audio',
|
||
footer_artist: 'Artists',
|
||
footer_about_us: 'About Us', footer_terms: 'Terms', footer_privacy: 'Privacy', footer_contact: 'Contact',
|
||
footer_subtitle: 'Gradient streaming style template · For demo purposes only',
|
||
}
|
||
};
|
||
const LANG_LABELS = {'zh-CN':'中文','en':'EN','zh-TW':'繁中'};
|
||
const CATEGORY_NAME_KEY = {流行:'cat_pop',摇滚:'cat_rock',民谣:'cat_folk',电子:'cat_electronic',嘻哈:'cat_hiphop','R&B':'cat_rnb',古典:'cat_classical',爵士:'cat_jazz'};
|
||
let currentLang = (function(){
|
||
try{ var u=new URLSearchParams(location.search).get('lang'); if(u&&I18N_LOCALES[u]) return u; }catch(e){}
|
||
try{ var s=localStorage.getItem('music3_lang'); if(s&&I18N_LOCALES[s]) return s; }catch(e){}
|
||
var b=navigator.language||navigator.userLanguage||'';
|
||
if(b.startsWith('zh-TW')||b.startsWith('zh-Hant')) return 'zh-TW';
|
||
if(b.startsWith('zh')) return 'en';
|
||
return 'en';
|
||
})();
|
||
function t(key){ return (I18N_LOCALES[currentLang]&&I18N_LOCALES[currentLang][key])||(I18N_LOCALES['zh-CN'][key])||key; }
|
||
function applyI18n(){
|
||
document.querySelectorAll('[data-i18n]').forEach(function(el){ el.textContent=t(el.getAttribute('data-i18n')); });
|
||
document.querySelectorAll('[data-i18n-placeholder]').forEach(function(el){ el.placeholder=t(el.getAttribute('data-i18n-placeholder')); });
|
||
document.querySelectorAll('[data-i18n-title]').forEach(function(el){ el.title=t(el.getAttribute('data-i18n-title')); });
|
||
document.documentElement.lang=currentLang==='zh-TW'?'zh-TW':currentLang==='zh-CN'?'zh-CN':'en';
|
||
document.title=t('title');
|
||
var ll=document.getElementById('langLabel'); if(ll) ll.textContent=LANG_LABELS[currentLang]||currentLang;
|
||
document.querySelectorAll('.lang-option').forEach(function(opt){ opt.classList.toggle('active',opt.getAttribute('data-lang')===currentLang); });
|
||
}
|
||
function switchLang(lang){
|
||
if(!I18N_LOCALES[lang]) return;
|
||
currentLang=lang;
|
||
try{localStorage.setItem('music3_lang',lang);}catch(e){}
|
||
applyI18n();
|
||
if(typeof onLangChange==='function') onLangChange(lang);
|
||
document.getElementById('langDropdown').classList.remove('open');
|
||
}
|
||
function toggleLangDropdown(){ document.getElementById('langDropdown').classList.toggle('open'); }
|
||
document.addEventListener('click',function(e){ var sw=document.getElementById('langSwitcher'); if(sw&&!sw.contains(e.target)) document.getElementById('langDropdown').classList.remove('open'); });
|
||
|
||
/* ============================================================
|
||
乐流 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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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){
|
||
const grad = coverGradient(song.title+song.artist);
|
||
const pos = cls === '' ? 'position:absolute;inset:0;' : '';
|
||
if(song.cover){
|
||
return `<div class="${cls}" style="${pos}background:${grad};overflow:hidden"><img src="${escapeHtml(song.cover)}" alt="" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover" onerror="this.style.display='none'"></div>`;
|
||
}
|
||
return `<div class="${cls}" style="${pos}background:${grad}"></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=t('cat_all');all.dataset.id='';
|
||
bar.appendChild(all);
|
||
cats.sort((a,b)=>(a.sort||0)-(b.sort||0)).forEach(c=>{
|
||
const nameKey=CATEGORY_NAME_KEY[c.name];
|
||
const displayName=nameKey?t(nameKey):c.name;
|
||
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(displayName)}`;
|
||
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">'+t('empty_hot')+'</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">'+t('empty_new')+'</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=t('plaza_count')+data.total+' '+t('tracks_unit');
|
||
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>${t('empty_plaza')}</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">${t('modal_album_prefix')}${escapeHtml(s.album||'未知')}${t('modal_album_suffix')}· ${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>${t('modal_plays')}</span></div>
|
||
<div class="ms"><b class="grad-text">${formatNumber(s.like_count)}</b><span>${t('modal_likes')}</span></div>
|
||
<div class="ms"><b class="grad-text">${formatDuration(s.duration)}</b><span>${t('modal_duration')}</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>${t('modal_play')}
|
||
</button>
|
||
<button class="btn-like ${liked?'liked':''}" id="modalLikeBtn" title="${t('modal_like')}">
|
||
<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>${t('modal_lyrics')}</h5>
|
||
${lyrics.length?lyrics.map(l=>`<div class="lyrics-line">${escapeHtml(l)}</div>`).join(''):`<div class="lyrics-empty">${t('modal_no_lyrics')}</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(t('toast_cancel_like')); }
|
||
else{ likedSet.add(sid); this.classList.add('liked'); toast(t('toast_add_like')); 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(t('toast_now_playing')+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="" style="width:100%;height:100%;object-fit:cover" onerror="this.style.display='none';this.nextElementSibling.style.display='block'"><div class="cdiv" style="background:${coverGradient(s.title+s.artist)};display:none"></div>`; }
|
||
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(t('toast_select_song')); 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(t('toast_select_song')); return; }
|
||
const sid=parseInt(player.song.id,10);
|
||
if(likedSet.has(sid)){ likedSet.delete(sid); this.classList.remove('liked'); toast(t('toast_cancel_like')); }
|
||
else{ likedSet.add(sid); this.classList.add('liked'); toast(t('toast_add_like')); }
|
||
});
|
||
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(t('toast_no_songs'));
|
||
});
|
||
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(t('toast_no_liked')); return; }
|
||
toast(t('toast_liked_count')+likedSet.size+' '+t('tracks_unit'));
|
||
});
|
||
|
||
/* ---------- 滚动动画 ---------- */
|
||
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);
|
||
}
|
||
})();
|
||
|
||
/* ---------- Language Change Callback ---------- */
|
||
function onLangChange(lang){
|
||
applyI18n();
|
||
renderCategories();
|
||
renderHot();
|
||
renderNew();
|
||
renderPlaza();
|
||
}
|
||
|
||
/* ---------- 初始化 ---------- */
|
||
(async function init(){
|
||
applyI18n();
|
||
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>
|