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

1240 lines
71 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>CLOUD PLAY — 云端游戏 即开即玩</title>
<style>
/* ============================ Reset & Variables ============================ */
:root{
--bg-0:#0d1117; --bg-1:#161b22; --bg-2:#1c2433;
--blue:#3b82f6; --blue-l:#60a5fa; --blue-d:#2563eb;
--green:#10b981; --green-d:#059669;
--purple:#8b5cf6; --pink:#ec4899; --amber:#f59e0b;
--text:#e6edf3; --text-dim:#9ba6b3; --text-faint:#6b7684;
--glass:rgba(255,255,255,.04); --glass-2:rgba(255,255,255,.07);
--border:rgba(255,255,255,.08); --border-2:rgba(255,255,255,.16);
--radius:16px; --radius-sm:12px; --radius-lg:22px;
--shadow:0 8px 32px rgba(0,0,0,.45);
--shadow-soft:0 4px 20px rgba(0,0,0,.3);
--maxw:1320px;
--ease:cubic-bezier(.22,.61,.36,1);
}
*{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;
background:var(--bg-0); color:var(--text);
line-height:1.6; -webkit-font-smoothing:antialiased; overflow-x:hidden;
min-height:100vh;
}
body::before{
content:""; position:fixed; inset:0; z-index:-2;
background:
radial-gradient(900px 600px at 12% -5%, rgba(59,130,246,.16), transparent 60%),
radial-gradient(800px 500px at 95% 8%, rgba(139,92,246,.13), transparent 55%),
radial-gradient(700px 600px at 50% 120%, rgba(16,185,129,.10), transparent 55%),
linear-gradient(180deg,#0d1117 0%,#11161f 40%,#161b22 100%);
}
a{color:inherit;text-decoration:none}
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
img{display:block;max-width:100%}
ul{list-style:none}
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:rgba(255,255,255,.12);border-radius:10px}
::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.22)}
::selection{background:rgba(59,130,246,.35)}
/* ============================ Layout helpers ============================ */
.wrap{max-width:var(--maxw);margin:0 auto;padding:0 28px}
.section{padding:64px 0 24px}
.section-head{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;margin-bottom:28px;flex-wrap:wrap}
.section-title{font-size:26px;font-weight:800;letter-spacing:-.02em;display:flex;align-items:center;gap:12px}
.section-title .bar{width:5px;height:26px;border-radius:4px;background:linear-gradient(180deg,var(--blue),var(--purple))}
.section-sub{color:var(--text-dim);font-size:14px;margin-top:6px;font-weight:400}
.link-more{color:var(--blue-l);font-size:14px;font-weight:600;display:inline-flex;align-items:center;gap:6px;transition:gap .25s var(--ease)}
.link-more:hover{gap:10px}
/* ============================ Header ============================ */
header{
position:fixed;top:0;left:0;right:0;z-index:100;
backdrop-filter:blur(18px) saturate(140%); -webkit-backdrop-filter:blur(18px) saturate(140%);
background:rgba(13,17,23,.72); border-bottom:1px solid var(--border);
transition:background .3s var(--ease),border-color .3s var(--ease);
}
header.scrolled{background:rgba(13,17,23,.9);border-color:var(--border-2)}
.nav{max-width:var(--maxw);margin:0 auto;padding:0 28px;height:68px;display:flex;align-items:center;gap:34px}
.logo{font-size:21px;font-weight:900;letter-spacing:.02em;white-space:nowrap;
background:linear-gradient(100deg,var(--blue-l),var(--purple) 60%,var(--pink));
-webkit-background-clip:text;background-clip:text;color:transparent;display:flex;align-items:center;gap:10px}
.logo .dot{width:11px;height:11px;border-radius:50%;background:linear-gradient(135deg,var(--green),var(--blue));
box-shadow:0 0 14px rgba(16,185,129,.7);animation:pulse 2.4s ease-in-out infinite}
@keyframes pulse{0%,100%{transform:scale(1);opacity:1}50%{transform:scale(1.25);opacity:.75}}
.nav-links{display:flex;gap:6px}
.nav-links a{padding:8px 14px;border-radius:10px;font-size:14.5px;font-weight:500;color:var(--text-dim);
transition:all .25s var(--ease);position:relative}
.nav-links a:hover{color:var(--text);background:var(--glass-2)}
.nav-links a.active{color:var(--text);background:var(--glass-2)}
.nav-links a.active::after{content:"";position:absolute;left:14px;right:14px;bottom:2px;height:2px;border-radius:2px;
background:linear-gradient(90deg,var(--blue),var(--purple))}
.nav-right{margin-left:auto;display:flex;align-items:center;gap:14px}
.search-box{display:flex;align-items:center;gap:8px;background:var(--glass-2);border:1px solid var(--border);
border-radius:12px;padding:9px 14px;width:240px;transition:all .3s var(--ease)}
.search-box:focus-within{border-color:rgba(59,130,246,.55);box-shadow:0 0 0 3px rgba(59,130,246,.15);width:300px}
.search-box svg{width:17px;height:17px;color:var(--text-faint);flex-shrink:0}
.search-box input{background:none;border:none;outline:none;color:var(--text);font-size:14px;width:100%;font-family:inherit}
.search-box input::placeholder{color:var(--text-faint)}
.btn-signin{padding:9px 18px;border-radius:11px;font-size:14px;font-weight:600;color:#fff;white-space:nowrap;
background:linear-gradient(135deg,var(--blue),var(--blue-d));box-shadow:0 4px 16px rgba(59,130,246,.4);
transition:transform .25s var(--ease),box-shadow .25s var(--ease)}
.btn-signin:hover{transform:translateY(-2px);box-shadow:0 8px 24px rgba(59,130,246,.55)}
.menu-toggle{display:none;width:40px;height:40px;border-radius:10px;align-items:center;justify-content:center;background:var(--glass-2);border:1px solid var(--border)}
.menu-toggle span{display:block;width:18px;height:2px;background:var(--text);position:relative;transition:.3s}
.menu-toggle span::before,.menu-toggle span::after{content:"";position:absolute;left:0;width:18px;height:2px;background:var(--text);transition:.3s}
.menu-toggle span::before{top:-6px}.menu-toggle span::after{top:6px}
/* ============================ Hero ============================ */
.hero{position:relative;padding:130px 0 70px;overflow:hidden}
.hero-mesh{position:absolute;inset:0;z-index:-1;overflow:hidden}
.hero-mesh .blob{position:absolute;border-radius:50%;filter:blur(70px);opacity:.55;animation:float 14s ease-in-out infinite}
.hero-mesh .b1{width:480px;height:480px;background:radial-gradient(circle,var(--blue),transparent 70%);top:-120px;left:-60px}
.hero-mesh .b2{width:420px;height:420px;background:radial-gradient(circle,var(--purple),transparent 70%);top:40px;right:8%;animation-delay:-4s}
.hero-mesh .b3{width:380px;height:380px;background:radial-gradient(circle,var(--green),transparent 70%);bottom:-140px;left:40%;animation-delay:-8s}
@keyframes float{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(40px,-30px) scale(1.08)}66%{transform:translate(-30px,25px) scale(.95)}}
.hero-grid-bg{position:absolute;inset:0;z-index:-1;opacity:.4;
background-image:linear-gradient(rgba(255,255,255,.035) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.035) 1px,transparent 1px);
background-size:48px 48px;mask-image:radial-gradient(ellipse 80% 60% at 50% 30%,#000,transparent 75%);-webkit-mask-image:radial-gradient(ellipse 80% 60% at 50% 30%,#000,transparent 75%)}
.hero-inner{display:grid;grid-template-columns:1.15fr .85fr;gap:50px;align-items:center}
.hero-badge{display:inline-flex;align-items:center;gap:8px;padding:7px 14px;border-radius:999px;font-size:12.5px;font-weight:600;
background:rgba(59,130,246,.12);border:1px solid rgba(59,130,246,.3);color:var(--blue-l);margin-bottom:22px}
.hero-badge .live{width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 10px var(--green);animation:pulse 1.8s infinite}
.hero h1{font-size:60px;line-height:1.05;font-weight:900;letter-spacing:-.03em;margin-bottom:22px}
.hero h1 .grad{background:linear-gradient(100deg,var(--blue-l),var(--purple) 55%,var(--pink));-webkit-background-clip:text;background-clip:text;color:transparent}
.hero p.lead{font-size:17px;color:var(--text-dim);max-width:520px;margin-bottom:34px}
.hero-cta{display:flex;gap:16px;align-items:center;flex-wrap:wrap}
.btn-cta{display:inline-flex;align-items:center;gap:10px;padding:15px 30px;border-radius:14px;font-size:16px;font-weight:700;color:#fff;
background:linear-gradient(135deg,var(--blue),var(--purple));box-shadow:0 8px 30px rgba(59,130,246,.45),0 0 0 0 rgba(59,130,246,.4);
transition:transform .3s var(--ease),box-shadow .3s var(--ease);animation:glow 2.6s ease-in-out infinite}
@keyframes glow{0%,100%{box-shadow:0 8px 30px rgba(59,130,246,.45),0 0 0 0 rgba(59,130,246,.5)}50%{box-shadow:0 8px 36px rgba(139,92,246,.55),0 0 0 14px rgba(59,130,246,0)}}
.btn-cta:hover{transform:translateY(-3px)}
.btn-ghost{display:inline-flex;align-items:center;gap:10px;padding:15px 26px;border-radius:14px;font-size:15px;font-weight:600;
background:var(--glass-2);border:1px solid var(--border-2);color:var(--text);transition:all .3s var(--ease)}
.btn-ghost:hover{background:var(--glass);border-color:rgba(255,255,255,.28);transform:translateY(-3px)}
.hero-stats{display:flex;gap:30px;margin-top:40px}
.hero-stats .st{display:flex;flex-direction:column}
.hero-stats .num{font-size:26px;font-weight:800;background:linear-gradient(100deg,var(--blue-l),var(--green));-webkit-background-clip:text;background-clip:text;color:transparent}
.hero-stats .lbl{font-size:13px;color:var(--text-faint);margin-top:2px}
/* Hero preview card */
.hero-preview{position:relative;perspective:1200px}
.hero-card{position:relative;border-radius:var(--radius-lg);overflow:hidden;border:1px solid var(--border-2);
box-shadow:0 30px 80px rgba(0,0,0,.5);transform:rotateY(-9deg) rotateX(4deg);transition:transform .6s var(--ease);background:var(--bg-1)}
.hero-card:hover{transform:rotateY(-4deg) rotateX(2deg)}
.hero-card .hc-cover{height:300px;position:relative;background:linear-gradient(135deg,#1e3a5f,#3b1d6e)}
.hero-card .hc-cover::after{content:"";position:absolute;inset:0;background:linear-gradient(180deg,transparent 40%,rgba(13,17,23,.9))}
.hero-card .hc-body{padding:22px;position:relative;margin-top:-70px;z-index:2}
.hero-card .hc-tag{display:inline-block;font-size:11px;font-weight:700;padding:4px 10px;border-radius:6px;
background:rgba(16,185,129,.18);color:var(--green);border:1px solid rgba(16,185,129,.3);margin-bottom:12px}
.hero-card .hc-title{font-size:22px;font-weight:800;margin-bottom:8px}
.hero-card .hc-meta{display:flex;gap:16px;color:var(--text-dim);font-size:13px;margin-bottom:16px}
.hero-card .play-bar{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-radius:12px;
background:var(--glass);border:1px solid var(--border)}
.hero-card .pb-left{display:flex;align-items:center;gap:10px;font-size:13px;color:var(--text-dim)}
.hero-card .pb-btn{width:42px;height:42px;border-radius:50%;display:flex;align-items:center;justify-content:center;
background:linear-gradient(135deg,var(--green),var(--green-d));box-shadow:0 4px 16px rgba(16,185,129,.5);transition:transform .3s}
.hero-card .pb-btn:hover{transform:scale(1.1)}
.hero-card .pb-btn svg{width:16px;height:16px;margin-left:2px;fill:#fff}
/* ============================ Category pills ============================ */
.cat-row{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:8px}
.cat-pill{padding:10px 20px;border-radius:999px;font-size:14px;font-weight:600;color:var(--text-dim);
background:var(--glass);border:1px solid var(--border);transition:all .3s var(--ease);white-space:nowrap}
.cat-pill:hover{color:var(--text);border-color:var(--border-2);background:var(--glass-2)}
.cat-pill.active{color:#fff;background:linear-gradient(135deg,var(--blue),var(--purple));border-color:transparent;
box-shadow:0 6px 20px rgba(59,130,246,.4)}
/* ============================ Cards common ============================ */
.card{background:var(--glass);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;
transition:transform .35s var(--ease),border-color .35s var(--ease),box-shadow .35s var(--ease);position:relative}
.card:hover{transform:translateY(-6px);border-color:var(--border-2);box-shadow:var(--shadow)}
.cover{position:relative;overflow:hidden;background:var(--bg-2)}
.cover .cv-art{position:absolute;inset:0;transition:transform .5s var(--ease)}
.cover:hover .cv-art{transform:scale(1.08)}
.cover .cv-glow{position:absolute;inset:0;background:linear-gradient(180deg,transparent 45%,rgba(0,0,0,.55));opacity:0;transition:opacity .35s}
.cover:hover .cv-glow{opacity:1}
.play-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .35s var(--ease);z-index:3}
.cover:hover .play-overlay{opacity:1}
.play-overlay .po-btn{width:56px;height:56px;border-radius:50%;display:flex;align-items:center;justify-content:center;
background:linear-gradient(135deg,var(--green),var(--green-d));box-shadow:0 8px 28px rgba(16,185,129,.6);
transform:scale(.7);transition:transform .35s var(--ease)}
.cover:hover .play-overlay .po-btn{transform:scale(1)}
.play-overlay .po-btn svg{width:22px;height:22px;margin-left:3px;fill:#fff}
.cover .cv-badge{position:absolute;top:12px;left:12px;z-index:4;font-size:11px;font-weight:700;padding:4px 9px;border-radius:6px;letter-spacing:.02em}
.badge-hot{background:linear-gradient(135deg,var(--pink),#f43f5e);color:#fff;box-shadow:0 3px 12px rgba(244,63,94,.5)}
.badge-new{background:linear-gradient(135deg,var(--green),var(--green-d));color:#fff;box-shadow:0 3px 12px rgba(16,185,129,.5)}
.cover .cv-rating{position:absolute;top:12px;right:12px;z-index:4;font-size:12px;font-weight:700;padding:4px 10px;border-radius:7px;
background:rgba(0,0,0,.55);backdrop-filter:blur(6px);color:var(--amber);display:flex;align-items:center;gap:4px}
/* Generated cover art */
.gcover{display:flex;align-items:flex-end;padding:16px;color:rgba(255,255,255,.95)}
.gcover .gc-watermark{position:absolute;font-size:96px;font-weight:900;opacity:.16;top:-10px;right:-6px;line-height:1;letter-spacing:-.04em}
.gcover .gc-orb1{position:absolute;width:120px;height:120px;border-radius:50%;background:rgba(255,255,255,.14);filter:blur(8px);top:-30px;left:-20px}
.gcover .gc-orb2{position:absolute;width:80px;height:80px;border-radius:50%;background:rgba(0,0,0,.2);filter:blur(6px);bottom:-20px;right:30px}
.gcover .gc-cat{position:relative;font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;opacity:.85;padding:3px 9px;border-radius:6px;background:rgba(0,0,0,.28);backdrop-filter:blur(4px)}
/* ============================ Hot section ============================ */
.hot-grid{display:grid;grid-template-columns:1.7fr 1fr 1fr;grid-template-rows:auto auto;gap:18px}
.hot-grid .card.big{grid-row:span 2;display:flex;flex-direction:column}
.hot-grid .card.big .cover{flex:1;min-height:380px}
.hot-grid .card:not(.big) .cover{height:150px}
.card-body{padding:16px 18px}
.card-title{font-size:16px;font-weight:700;margin-bottom:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.card.big .card-title{font-size:22px;white-space:normal}
.card-meta{display:flex;align-items:center;gap:12px;color:var(--text-dim);font-size:12.5px;flex-wrap:wrap}
.card-meta .dot{width:3px;height:3px;border-radius:50%;background:var(--text-faint)}
.card-desc{color:var(--text-dim);font-size:13px;margin-top:8px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.card.big .card-desc{font-size:14px;-webkit-line-clamp:3;margin-top:10px}
.card-tags{display:flex;gap:6px;flex-wrap:wrap;margin-top:10px}
.tag-mini{font-size:11px;color:var(--text-dim);padding:3px 8px;border-radius:6px;background:var(--glass-2);border:1px solid var(--border)}
/* ============================ New section (horizontal scroll) ============================ */
.scroll-x{display:flex;gap:16px;overflow-x:auto;padding:6px 4px 16px;scroll-snap-type:x mandatory;scroll-padding-left:4px}
.scroll-x::-webkit-scrollbar{height:6px}
.scroll-x .card{min-width:240px;max-width:240px;scroll-snap-align:start}
.scroll-x .card .cover{height:140px}
/* ============================ Library ============================ */
.lib-bar{display:flex;gap:14px;flex-wrap:wrap;align-items:center;margin-bottom:26px}
.lib-search{display:flex;align-items:center;gap:9px;background:var(--glass-2);border:1px solid var(--border);border-radius:12px;padding:11px 16px;flex:1;min-width:220px;transition:all .3s var(--ease)}
.lib-search:focus-within{border-color:rgba(59,130,246,.5);box-shadow:0 0 0 3px rgba(59,130,246,.12)}
.lib-search svg{width:17px;height:17px;color:var(--text-faint);flex-shrink:0}
.lib-search input{background:none;border:none;outline:none;color:var(--text);font-size:14px;width:100%;font-family:inherit}
.lib-search input::placeholder{color:var(--text-faint)}
.lib-cats{display:flex;gap:8px;flex-wrap:wrap}
.lib-sort{position:relative}
.lib-sort select{appearance:none;-webkit-appearance:none;padding:11px 38px 11px 16px;border-radius:12px;font-size:14px;font-weight:600;
background:var(--glass-2);border:1px solid var(--border);color:var(--text);cursor:pointer;font-family:inherit;transition:all .3s var(--ease)}
.lib-sort select:hover{border-color:var(--border-2)}
.lib-sort::after{content:"";position:absolute;right:15px;top:50%;width:8px;height:8px;border-right:2px solid var(--text-dim);border-bottom:2px solid var(--text-dim);transform:translateY(-65%) rotate(45deg);pointer-events:none}
.game-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:18px}
.game-grid .card .cover{height:165px}
.game-grid .card-body{padding:14px 15px}
/* Pagination */
.pager{display:flex;justify-content:center;align-items:center;gap:8px;margin-top:38px;flex-wrap:wrap}
.pager button{min-width:40px;height:40px;padding:0 12px;border-radius:11px;font-size:14px;font-weight:600;color:var(--text-dim);
background:var(--glass);border:1px solid var(--border);transition:all .25s var(--ease)}
.pager button:hover:not(:disabled){color:var(--text);border-color:var(--border-2);background:var(--glass-2)}
.pager button.active{color:#fff;background:linear-gradient(135deg,var(--blue),var(--purple));border-color:transparent;box-shadow:0 4px 16px rgba(59,130,246,.4)}
.pager button:disabled{opacity:.35;cursor:not-allowed}
.pager .dots{color:var(--text-faint);padding:0 4px}
.lib-info{text-align:center;color:var(--text-faint);font-size:13px;margin-top:14px}
/* ============================ Skeleton ============================ */
.skeleton{background:var(--glass);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
.sk-shimmer{position:relative;background:var(--bg-2);overflow:hidden}
.sk-shimmer::after{content:"";position:absolute;inset:0;transform:translateX(-100%);
background:linear-gradient(90deg,transparent,rgba(255,255,255,.07),transparent);animation:shimmer 1.5s infinite}
@keyframes shimmer{100%{transform:translateX(100%)}}
.sk-cover{height:165px}
.sk-line{height:13px;border-radius:6px;margin:10px 14px}
/* ============================ Stars ============================ */
.stars{position:relative;display:inline-block;font-size:14px;line-height:1;letter-spacing:2px;font-family:Arial,sans-serif}
.stars .bg{color:rgba(255,255,255,.18)}
.stars .fg{position:absolute;left:0;top:0;overflow:hidden;white-space:nowrap;
background:linear-gradient(90deg,var(--amber),#fbbf24);-webkit-background-clip:text;background-clip:text;color:transparent}
/* ============================ Modal ============================ */
.modal-mask{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;padding:24px;
background:rgba(7,10,16,.7);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
opacity:0;visibility:hidden;transition:opacity .35s var(--ease),visibility .35s}
.modal-mask.open{opacity:1;visibility:visible}
.modal{width:100%;max-width:860px;max-height:90vh;overflow:hidden;border-radius:var(--radius-lg);
background:var(--bg-1);border:1px solid var(--border-2);box-shadow:0 40px 100px rgba(0,0,0,.6);
transform:translateY(30px) scale(.97);opacity:0;transition:transform .4s var(--ease),opacity .4s;display:flex;flex-direction:column}
.modal-mask.open .modal{transform:translateY(0) scale(1);opacity:1}
.modal-header{position:relative;height:230px;flex-shrink:0;overflow:hidden}
.modal-header .mh-art{position:absolute;inset:0}
.modal-header .mh-art::after{content:"";position:absolute;inset:0;background:linear-gradient(180deg,rgba(22,27,34,.2) 0%,rgba(22,27,34,.6) 60%,var(--bg-1) 100%)}
.modal-close{position:absolute;top:16px;right:16px;z-index:5;width:38px;height:38px;border-radius:11px;display:flex;align-items:center;justify-content:center;
background:rgba(0,0,0,.5);backdrop-filter:blur(8px);border:1px solid var(--border);transition:all .25s var(--ease)}
.modal-close:hover{background:rgba(0,0,0,.7);transform:rotate(90deg)}
.modal-close svg{width:18px;height:18px;stroke:var(--text)}
.modal-header-content{position:absolute;left:0;right:0;bottom:0;padding:26px 30px;z-index:3}
.modal-badges{display:flex;gap:8px;margin-bottom:12px}
.modal-title{font-size:32px;font-weight:900;letter-spacing:-.02em;margin-bottom:10px}
.modal-sub{display:flex;align-items:center;gap:14px;flex-wrap:wrap;color:var(--text-dim);font-size:13.5px}
.modal-body{overflow-y:auto;padding:26px 30px 30px}
.modal-row{display:grid;grid-template-columns:1.5fr 1fr;gap:30px;margin-bottom:26px}
.modal-desc h4{font-size:15px;font-weight:700;margin-bottom:10px;color:var(--text)}
.modal-desc p{color:var(--text-dim);font-size:14px;line-height:1.75}
.modal-features{margin-top:18px}
.modal-features h4{font-size:15px;font-weight:700;margin-bottom:12px}
.feat-list{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.feat-item{display:flex;align-items:center;gap:9px;font-size:13.5px;color:var(--text-dim)}
.feat-item .ck{width:20px;height:20px;border-radius:6px;flex-shrink:0;display:flex;align-items:center;justify-content:center;
background:rgba(16,185,129,.16);color:var(--green)}
.feat-item .ck svg{width:12px;height:12px;stroke:var(--green);stroke-width:3}
.modal-info{background:var(--glass);border:1px solid var(--border);border-radius:var(--radius);padding:20px}
.info-row{display:flex;justify-content:space-between;gap:12px;padding:9px 0;border-bottom:1px solid var(--border);font-size:13.5px}
.info-row:last-child{border-bottom:none}
.info-row .k{color:var(--text-faint);white-space:nowrap}
.info-row .v{color:var(--text);font-weight:600;text-align:right}
.modal-tags{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:24px}
.tag-pill{font-size:12.5px;font-weight:600;padding:6px 13px;border-radius:999px;
background:rgba(139,92,246,.12);border:1px solid rgba(139,92,246,.3);color:#c4b5fd}
.modal-cta{display:flex;gap:14px;align-items:center;flex-wrap:wrap;padding-top:6px}
.btn-play-big{display:inline-flex;align-items:center;gap:10px;padding:15px 36px;border-radius:14px;font-size:16px;font-weight:700;color:#fff;
background:linear-gradient(135deg,var(--green),var(--green-d));box-shadow:0 8px 30px rgba(16,185,129,.5);
transition:transform .3s var(--ease),box-shadow .3s var(--ease)}
.btn-play-big:hover{transform:translateY(-3px);box-shadow:0 12px 40px rgba(16,185,129,.65)}
.btn-play-big svg{width:18px;height:18px;fill:#fff}
.btn-fav{width:54px;height:54px;border-radius:14px;display:flex;align-items:center;justify-content:center;
background:var(--glass-2);border:1px solid var(--border-2);transition:all .3s var(--ease)}
.btn-fav:hover{background:var(--glass);border-color:rgba(255,255,255,.3);transform:translateY(-3px)}
.btn-fav svg{width:22px;height:22px;stroke:var(--text-dim);fill:none;stroke-width:2}
.modal-sk{height:420px;display:flex;flex-direction:column}
.modal-sk .sk-shimmer{height:230px;border-radius:0}
.modal-sk .sk-line.w60{width:60%;margin:24px 30px 0}
.modal-sk .sk-line.w90{width:90%}
.modal-sk .sk-line.w40{width:40%}
/* ============================ Footer ============================ */
footer{margin-top:80px;border-top:1px solid var(--border);background:rgba(13,17,23,.6)}
.foot{max-width:var(--maxw);margin:0 auto;padding:54px 28px 30px;display:grid;grid-template-columns:1.6fr 1fr 1fr 1fr;gap:40px}
.foot-brand .logo{margin-bottom:16px}
.foot-brand p{color:var(--text-dim);font-size:14px;max-width:320px;line-height:1.7}
.foot-soc{display:flex;gap:10px;margin-top:20px}
.foot-soc a{width:38px;height:38px;border-radius:10px;display:flex;align-items:center;justify-content:center;background:var(--glass-2);border:1px solid var(--border);transition:all .3s var(--ease)}
.foot-soc a:hover{background:var(--glass);border-color:var(--border-2);transform:translateY(-3px)}
.foot-soc svg{width:18px;height:18px;stroke:var(--text-dim);fill:none;stroke-width:1.8}
.foot-col h5{font-size:14px;font-weight:700;margin-bottom:16px;color:var(--text)}
.foot-col a{display:block;color:var(--text-dim);font-size:13.5px;padding:6px 0;transition:color .25s}
.foot-col a:hover{color:var(--blue-l)}
.foot-bottom{max-width:var(--maxw);margin:0 auto;padding:20px 28px;border-top:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px}
.foot-bottom p{color:var(--text-faint);font-size:13px}
/* ============================ Toast ============================ */
.toast-wrap{position:fixed;top:88px;right:24px;z-index:300;display:flex;flex-direction:column;gap:10px}
.toast{padding:14px 20px;border-radius:12px;font-size:14px;font-weight:600;color:#fff;background:rgba(28,36,51,.95);
border:1px solid var(--border-2);backdrop-filter:blur(12px);box-shadow:var(--shadow);display:flex;align-items:center;gap:10px;
transform:translateX(120%);transition:transform .4s var(--ease)}
.toast.show{transform:translateX(0)}
.toast.success{border-left:3px solid var(--green)}
.toast .ti{width:20px;height:20px;flex-shrink:0;color:var(--green)}
/* ============================ Reveal animation ============================ */
.reveal{opacity:0;transform:translateY(28px);transition:opacity .7s var(--ease),transform .7s var(--ease)}
.reveal.in{opacity:1;transform:none}
/* ============================ Empty state ============================ */
.empty{text-align:center;padding:70px 20px;color:var(--text-faint)}
.empty svg{width:64px;height:64px;margin:0 auto 18px;opacity:.4}
.empty p{font-size:15px}
/* ============================ Responsive ============================ */
@media(max-width:1100px){
.hero-inner{grid-template-columns:1fr;gap:40px}
.hero-preview{max-width:480px}
.game-grid{grid-template-columns:repeat(3,1fr)}
.hot-grid{grid-template-columns:1fr 1fr}
.hot-grid .card.big{grid-row:span 1;grid-column:span 2}
.hot-grid .card.big .cover{min-height:260px}
.foot{grid-template-columns:1fr 1fr;gap:30px}
}
@media(max-width:760px){
.wrap{padding:0 18px}
.nav{padding:0 18px;gap:16px}
.nav-links,.search-box{display:none}
.menu-toggle{display:flex}
.hero{padding:110px 0 50px}
.hero h1{font-size:40px}
.hero p.lead{font-size:15px}
.hero-stats{gap:20px}
.section{padding:46px 0 16px}
.section-title{font-size:22px}
.game-grid{grid-template-columns:repeat(2,1fr);gap:14px}
.hot-grid{grid-template-columns:1fr}
.hot-grid .card.big{grid-column:span 1}
.modal-row{grid-template-columns:1fr;gap:20px}
.feat-list{grid-template-columns:1fr}
.modal-title{font-size:24px}
.modal-header{height:190px}
.foot{grid-template-columns:1fr;gap:28px}
.lib-bar{flex-direction:column;align-items:stretch}
.lib-search{width:100%}
}
</style>
</head>
<body>
<!-- ============================ HEADER ============================ -->
<header id="header">
<nav class="nav">
<a href="#top" class="logo"><span class="dot"></span>CLOUD PLAY</a>
<ul class="nav-links">
<li><a href="#top" class="active" data-nav="home">首页</a></li>
<li><a href="#library" data-nav="browse">浏览</a></li>
<li><a href="#hot" data-nav="hot">热门</a></li>
<li><a href="#new" data-nav="new">新游</a></li>
</ul>
<div class="nav-right">
<div class="search-box">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input type="text" id="navSearch" placeholder="搜索游戏...">
</div>
<button class="btn-signin">登录</button>
<button class="menu-toggle" aria-label="菜单"><span></span></button>
</div>
</nav>
</header>
<!-- ============================ HERO ============================ -->
<section class="hero" id="top">
<div class="hero-grid-bg"></div>
<div class="hero-mesh"><span class="blob b1"></span><span class="blob b2"></span><span class="blob b3"></span></div>
<div class="wrap hero-inner">
<div class="hero-text">
<span class="hero-badge"><span class="live"></span>云端串流 · 零延迟 · 4K 画质</span>
<h1>云端游戏<br><span class="grad">即开即玩</span></h1>
<p class="lead">无需下载,无需高端硬件。在任意设备上畅玩 3A 大作,从手机到电视,进度无缝衔接。强大的云端算力,让每一帧都丝滑流畅。</p>
<div class="hero-cta">
<button class="btn-cta" onclick="document.getElementById('hot').scrollIntoView({behavior:'smooth'})">
<svg width="18" height="18" viewBox="0 0 24 24" fill="#fff"><path d="M8 5v14l11-7z"/></svg>
立即体验
</button>
<a href="#library" class="btn-ghost">浏览全部游戏</a>
</div>
<div class="hero-stats">
<div class="st"><span class="num" id="statGames">0</span><span class="lbl">款游戏</span></div>
<div class="st"><span class="num">99.9%</span><span class="lbl">服务可用率</span></div>
<div class="st"><span class="num">&lt;20ms</span><span class="lbl">平均延迟</span></div>
</div>
</div>
<div class="hero-preview">
<div class="hero-card">
<div class="hc-cover" id="heroCover"></div>
<div class="hc-body">
<span class="hc-tag">正在热玩</span>
<h3 class="hc-title" id="heroTitle">加载中...</h3>
<div class="hc-meta">
<span id="heroCat">—</span>
<span>·</span>
<span id="heroDev">—</span>
</div>
<div class="play-bar">
<div class="pb-left">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v20M2 12h20"/></svg>
云端启动 · 无需下载
</div>
<button class="pb-btn" id="heroPlay"><svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg></button>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ============================ CATEGORY FILTER ============================ -->
<section class="section" style="padding-top:24px">
<div class="wrap">
<div class="cat-row" id="catRow">
<button class="cat-pill active" data-cat="0">全部</button>
</div>
</div>
</section>
<!-- ============================ HOT GAMES ============================ -->
<section class="section" id="hot">
<div class="wrap">
<div class="section-head reveal">
<div>
<h2 class="section-title"><span class="bar"></span>热门游戏</h2>
<p class="section-sub">当下最受玩家追捧的云端大作</p>
</div>
<a href="#library" class="link-more">查看全部 <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m9 18 6-6-6-6"/></svg></a>
</div>
<div class="hot-grid" id="hotGrid"></div>
</div>
</section>
<!-- ============================ NEW GAMES ============================ -->
<section class="section" id="new">
<div class="wrap">
<div class="section-head reveal">
<div>
<h2 class="section-title"><span class="bar"></span>最新上线</h2>
<p class="section-sub">新鲜上架,第一时间云端畅玩</p>
</div>
</div>
<div class="scroll-x" id="newScroll"></div>
</div>
</section>
<!-- ============================ GAME LIBRARY ============================ -->
<section class="section" id="library">
<div class="wrap">
<div class="section-head reveal">
<div>
<h2 class="section-title"><span class="bar"></span>全部游戏</h2>
<p class="section-sub">探索完整的云端游戏库</p>
</div>
</div>
<div class="lib-bar reveal">
<div class="lib-search">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input type="text" id="libSearch" placeholder="搜索游戏名称...">
</div>
<div class="lib-cats" id="libCats"></div>
<div class="lib-sort">
<select id="libSort">
<option value="">默认排序</option>
<option value="rating">评分最高</option>
<option value="plays">最受欢迎</option>
<option value="newest">最新上线</option>
</select>
</div>
</div>
<div class="game-grid" id="gameGrid"></div>
<div class="lib-info" id="libInfo"></div>
<div class="pager" id="pager"></div>
</div>
</section>
<!-- ============================ FOOTER ============================ -->
<footer>
<div class="foot">
<div class="foot-brand">
<a href="#top" class="logo"><span class="dot"></span>CLOUD PLAY</a>
<p>新一代云端游戏平台,让任何人都能在任何设备上畅玩 3A 大作。告别硬件限制,拥抱无限可能。</p>
<div class="foot-soc">
<a href="javascript:void(0)" aria-label="微博"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/></svg></a>
<a href="javascript:void(0)" aria-label="微信"><svg viewBox="0 0 24 24"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg></a>
<a href="javascript:void(0)" aria-label="B站"><svg viewBox="0 0 24 24"><rect x="3" y="6" width="18" height="14" rx="3"/><path d="M8 3l4 3 4-3"/></svg></a>
</div>
</div>
<div class="foot-col">
<h5>平台</h5>
<a href="#top">首页</a><a href="#library">游戏库</a><a href="#hot">热门排行</a><a href="#new">最新上线</a>
</div>
<div class="foot-col">
<h5>支持</h5>
<a href="javascript:void(0)">帮助中心</a><a href="javascript:void(0)">设备要求</a><a href="javascript:void(0)">网络诊断</a><a href="javascript:void(0)">联系我们</a>
</div>
<div class="foot-col">
<h5>关于</h5>
<a href="javascript:void(0)">关于我们</a><a href="javascript:void(0)">服务条款</a><a href="javascript:void(0)">隐私政策</a><a href="javascript:void(0)">合作伙伴</a>
</div>
</div>
<div class="foot-bottom">
<p>© 2026 CLOUD PLAY. 保留所有权利。</p>
<p>由云端算力驱动 · 即开即玩</p>
</div>
</footer>
<!-- ============================ MODAL ============================ -->
<div class="modal-mask" id="modalMask">
<div class="modal" id="modal"></div>
</div>
<!-- ============================ TOAST ============================ -->
<div class="toast-wrap" id="toastWrap"></div>
<script>
"use strict";
/* ===========================================================================
CLOUD PLAY — 现代云游戏平台前端逻辑
所有 API 调用使用 fetch;当后端不可用时回退到内置 Mock 数据以保证模板可独立预览
=========================================================================== */
const API = "/api/v1/game";
/* ---------- 内置 Mock 数据(与后端 seed 一致,用于离线/预览回退) ---------- */
const MOCK = {
categories: [
{id:1,name:"动作冒险",icon:"action",sort:1},
{id:2,name:"角色扮演",icon:"rpg",sort:2},
{id:3,name:"策略模拟",icon:"strategy",sort:3},
{id:4,name:"休闲益智",icon:"casual",sort:4},
{id:5,name:"体育竞速",icon:"sports",sort:5},
{id:6,name:"射击对战",icon:"shooter",sort:6},
],
games: [
{id:1,name:"暗影纪元:觉醒",description:"开放世界动作冒险巨作。探索广袤的奇幻大陆,与远古巨龙激战,揭开失落文明的秘密。流畅的战斗系统,震撼的视觉效果,超过 100 小时的主线剧情等你体验。",category_id:1,category_name:"动作冒险",cover:"",developer:"Phoenix Studio",publisher:"Nexus Games",rating:4.9,plays:890000,downloads:560000,size:"45.2 GB",version:"v2.1.0",release_date:"2026-03-15",platform:"PC / PS5 / Xbox",tags:'["开放世界","巨龙","史诗"]',screenshots:'["广袤大陆远景","巨龙激战","失落文明遗迹"]',features:'["无缝开放世界","实时战斗系统","动态天气","多结局剧情"]',is_hot:true,is_new:false,sort:1,status:1},
{id:2,name:"忍者之刃:影遁",description:"硬核动作游戏,融合忍术与暗杀。在战国时代的日本,扮演最后的影忍,用速度和技巧消灭敌人。像素级精准操作,爽快连招系统。",category_id:1,category_name:"动作冒险",cover:"",developer:"Shadow Forge",publisher:"Nexus Games",rating:4.7,plays:450000,downloads:280000,size:"18.5 GB",version:"v1.3.2",release_date:"2026-01-20",platform:"PC / Switch",tags:'["忍术","硬核","连招"]',screenshots:'["战国场景","忍术特效"]',features:'["精准格挡系统","忍术技能树","无UI沉浸模式"]',is_hot:true,is_new:false,sort:2,status:1},
{id:3,name:"深海迷踪",description:"水下探险生存游戏。在神秘的深海世界中采集资源、建造基地、发现远古遗迹。当心隐藏在黑暗中的深海巨兽。",category_id:1,category_name:"动作冒险",cover:"",developer:"Abyss Team",publisher:"Ocean Interactive",rating:4.5,plays:320000,downloads:190000,size:"22.0 GB",version:"v1.0.5",release_date:"2026-05-01",platform:"PC / PS5",tags:'["生存","水下","探索"]',screenshots:'["深海基地","巨兽遭遇"]',features:'["深海生态系统","基地建造","多人协作"]',is_hot:false,is_new:true,sort:3,status:1},
{id:4,name:"星域编年史",description:"科幻 MMORPG 巨作。跨越银河系的史诗冒险,选择你的种族和职业,与千万玩家共同书写星际历史。自由度极高的技能搭配和装备系统。",category_id:2,category_name:"角色扮演",cover:"",developer:"Galaxy Works",publisher:"Nexus Games",rating:4.8,plays:1200000,downloads:890000,size:"80.0 GB",version:"v5.0.1",release_date:"2025-11-10",platform:"PC / Mobile",tags:'["MMORPG","科幻","多人"]',screenshots:'["银河舰队","星际战场"]',features:'["6大种族12职业","星际舰队战","玩家经济系统","跨服竞技"]',is_hot:true,is_new:false,sort:1,status:1},
{id:5,name:"幻灵大陆:重生",description:"经典日式 RPG 回归。手绘风格画面,感人至深的剧情,回合制战斗系统全新升级。收集 200+ 幻灵伙伴,组建你的最强队伍。",category_id:2,category_name:"角色扮演",cover:"",developer:"Dreamlight",publisher:"Square Nexus",rating:4.7,plays:670000,downloads:450000,size:"12.8 GB",version:"v3.2.0",release_date:"2026-02-28",platform:"PC / Switch / Mobile",tags:'["JRPG","回合制","收集"]',screenshots:'["手绘场景","幻灵队伍"]',features:'["200+幻灵收集","手绘动画","分支剧情","宠物养成"]',is_hot:false,is_new:false,sort:2,status:1},
{id:6,name:"修仙问道录",description:"国风仙侠开放世界 RPG。从凡人修炼至飞升仙界,体验完整的修仙体系。御剑飞行、炼丹炼器、渡劫飞升,感受东方仙侠之美。",category_id:2,category_name:"角色扮演",cover:"",developer:"云海工作室",publisher:"仙界互娱",rating:4.6,plays:980000,downloads:720000,size:"35.0 GB",version:"v2.0.0",release_date:"2026-04-01",platform:"PC / Mobile",tags:'["仙侠","开放世界","国风"]',screenshots:'["御剑飞行","渡劫场景"]',features:'["修仙境界体系","御剑飞行","炼丹炼器","宗门争霸"]',is_hot:true,is_new:true,sort:3,status:1},
{id:7,name:"帝国黎明:文明崛起",description:"4X 策略巨作。从石器时代到星际文明,带领你的民族走过万年历史。外交、战争、科技、文化,多种胜利方式。支持最多 12 人联机。",category_id:3,category_name:"策略模拟",cover:"",developer:"Strategy Labs",publisher:"Nexus Games",rating:4.8,plays:560000,downloads:340000,size:"28.0 GB",version:"v4.1.0",release_date:"2026-01-05",platform:"PC / Mac",tags:'["4X","文明","策略"]',screenshots:'["文明版图","星际时代"]',features:'["万年文明演进","12人联机","随机地图生成","科技树系统"]',is_hot:true,is_new:false,sort:1,status:1},
{id:8,name:"星际拓荒者",description:"太空殖民模拟经营。建立你的星际帝国,开发星球资源,研究科技,与外星文明交流或对抗。沙盒式自由玩法。",category_id:3,category_name:"策略模拟",cover:"",developer:"Cosmic Dev",publisher:"Space Games",rating:4.5,plays:380000,downloads:210000,size:"15.0 GB",version:"v1.5.0",release_date:"2026-03-20",platform:"PC",tags:'["模拟","太空","经营"]',screenshots:'["星球殖民","星际帝国"]',features:'["星球殖民","科技研究","外交系统","沙盒模式"]',is_hot:false,is_new:false,sort:2,status:1},
{id:9,name:"农场物语:四季",description:"治愈系农场模拟。种田、养殖、钓鱼、采矿、交友。四季变换的美景,温馨的村庄生活。支持 4 人联机合作。",category_id:3,category_name:"策略模拟",cover:"",developer:"Green Valley",publisher:"Harvest Games",rating:4.7,plays:720000,downloads:520000,size:"8.5 GB",version:"v2.3.0",release_date:"2026-02-14",platform:"PC / Switch / Mobile",tags:'["农场","治愈","联机"]',screenshots:'["四季农场","村庄生活"]',features:'["四季农场经营","4人联机","村民社交","节日活动"]',is_hot:true,is_new:false,sort:3,status:1},
{id:10,name:"方块消消乐:无尽版",description:"经典三消玩法全新升级。数百个精心设计的关卡,丰富的道具和障碍,每周更新限时活动。轻松上手,难以放下。",category_id:4,category_name:"休闲益智",cover:"",developer:"Fun Factory",publisher:"Casual Games",rating:4.6,plays:3500000,downloads:2800000,size:"320 MB",version:"v8.0.0",release_date:"2026-04-15",platform:"Mobile / PC",tags:'["三消","休闲","限时活动"]',screenshots:'["关卡界面","道具特效"]',features:'["500+关卡","每周活动","社交排行榜","离线可玩"]',is_hot:true,is_new:false,sort:1,status:1},
{id:11,name:"脑洞大师",description:"创意脑筋急转弯合集。1000+ 脑洞题目,考验你的逆向思维和创意。每关都有意想不到的解法,让你大呼过瘾。",category_id:4,category_name:"休闲益智",cover:"",developer:"Brain Box",publisher:"Puzzle Games",rating:4.4,plays:1800000,downloads:1200000,size:"150 MB",version:"v3.0.0",release_date:"2026-05-10",platform:"Mobile",tags:'["益智","脑筋急转弯","创意"]',screenshots:'["题目界面","成就收集"]',features:'["1000+题目","每日挑战","提示系统","成就收集"]',is_hot:false,is_new:true,sort:2,status:1},
{id:12,name:"音符跳跃",description:"音乐节奏游戏。跟随节拍跳跃,感受音乐与操作的完美融合。100+ 授权热门曲目,多种难度可选。",category_id:4,category_name:"休闲益智",cover:"",developer:"Rhythm Works",publisher:"Music Games",rating:4.5,plays:920000,downloads:680000,size:"680 MB",version:"v2.1.0",release_date:"2026-03-01",platform:"Mobile / PC",tags:'["音乐","节奏","手速"]',screenshots:'["节奏谱面","曲目列表"]',features:'["100+授权曲目","多难度","自定义谱面","全球排名"]',is_hot:false,is_new:false,sort:3,status:1},
{id:13,name:"极速狂飙:霓虹之城",description:"赛博朋克风格竞速游戏。在霓虹闪烁的未来都市飙车,改装你的超跑,挑战极限赛道。支持 16 人在线竞速。",category_id:5,category_name:"体育竞速",cover:"",developer:"Turbo Studio",publisher:"Racing Games",rating:4.7,plays:680000,downloads:430000,size:"55.0 GB",version:"v2.0.0",release_date:"2026-04-20",platform:"PC / PS5 / Xbox",tags:'["竞速","赛博朋克","改装"]',screenshots:'["霓虹都市","极限赛道"]',features:'["16人在线","车辆改装","开放城市","动态天气"]',is_hot:true,is_new:false,sort:1,status:1},
{id:14,name:"绿茵传奇 2026",description:"次世代足球模拟。真实球员授权,精细到每个动作的动作捕捉。全新战术系统,经理模式深度运营。电竞赛事指定用游。",category_id:5,category_name:"体育竞速",cover:"",developer:"Sports Pro",publisher:"Champion Games",rating:4.6,plays:1100000,downloads:820000,size:"65.0 GB",version:"v2026.1",release_date:"2026-01-01",platform:"PC / PS5 / Xbox / Switch",tags:'["足球","模拟","电竞"]',screenshots:'["比赛画面","经理模式"]',features:'["真实球员授权","经理模式","在线对战","UT卡牌"]',is_hot:false,is_new:false,sort:2,status:1},
{id:15,name:"战术先锋:零点行动",description:"5v5 战术 FPS 对战。精准枪械手感,丰富的战术道具,公平竞技无付费增强。排位赛、休闲赛、僵尸模式应有尽有。",category_id:6,category_name:"射击对战",cover:"",developer:"Tactical Ops",publisher:"Nexus Games",rating:4.8,plays:2500000,downloads:1800000,size:"40.0 GB",version:"v6.0",release_date:"2025-12-01",platform:"PC / Console",tags:'["FPS","5v5","电竞"]',screenshots:'["战术对战","僵尸模式"]',features:'["精准枪械手感","战术道具系统","排位竞技","僵尸模式"]',is_hot:true,is_new:false,sort:1,status:1},
{id:16,name:"星际战甲:远征",description:"PvE 合作射击 RPG。扮演星际战士,穿着可定制战甲,使用超能力和先进武器,与队友一起完成史诗任务。50+ 战甲可选。",category_id:6,category_name:"射击对战",cover:"",developer:"Digital Forge",publisher:"Space Games",rating:4.7,plays:1300000,downloads:950000,size:"50.0 GB",version:"v3.5.0",release_date:"2026-02-10",platform:"PC / PS5 / Xbox",tags:'["PvE","战甲","合作"]',screenshots:'["战甲展示","合作任务"]',features:'["50+可定制战甲","4人合作","开放世界任务","免费游玩"]',is_hot:true,is_new:false,sort:2,status:1},
{id:17,name:"荒野求生:大逃杀",description:"100 人大逃杀。跳伞、搜装备、缩圈、活到最后。多种地图和模式,真实弹道物理。战术竞技的极致体验。",category_id:6,category_name:"射击对战",cover:"",developer:"Battle Royal",publisher:"Nexus Games",rating:4.6,plays:3200000,downloads:2500000,size:"35.0 GB",version:"v12.0",release_date:"2026-03-01",platform:"PC / Mobile / Console",tags:'["大逃杀","100人","战术"]',screenshots:'["跳伞开局","决赛圈"]',features:'["100人对战","多地图模式","真实弹道","免费游玩"]',is_hot:true,is_new:true,sort:3,status:1},
]
};
/* ---------- 状态 ---------- */
const state = {
categories: [],
libCategory: 0,
libKeyword: "",
libSort: "",
libPage: 1,
libPageSize: 10,
libTotal: 0,
};
/* ===========================================================================
工具函数
=========================================================================== */
function $(sel, root){ return (root||document).querySelector(sel); }
function $$(sel, root){ return Array.from((root||document).querySelectorAll(sel)); }
// 数字格式化:890000 -> "89万"
function formatNumber(n){
n = Number(n)||0;
if(n >= 10000){
return (n/10000).toFixed(n%10000===0?0:1).replace(/\.0$/,'') + "万";
}
return String(n);
}
// 安全解析 JSON 字符串数组
function parseJSON(str, fallback){
if(Array.isArray(str)) return str;
if(typeof str !== "string" || !str) return fallback||[];
try{ const v = JSON.parse(str); return Array.isArray(v)? v : (fallback||[]); }
catch(e){ return fallback||[]; }
}
// 字符串 hash -> 用于生成稳定封面配色
function hashStr(s){
let h = 0;
for(let i=0;i<s.length;i++){ h = (h<<5)-h + s.charCodeAt(i); h |= 0; }
return Math.abs(h);
}
const PALETTES = [
["#3b82f6","#8b5cf6"],["#10b981","#3b82f6"],["#8b5cf6","#ec4899"],
["#f59e0b","#ef4444"],["#06b6d4","#3b82f6"],["#ec4899","#8b5cf6"],
["#14b8a6","#06b6d4"],["#f43f5e","#f59e0b"],["#6366f1","#a855f7"],
["#0ea5e9","#22c55e"],["#d946ef","#6366f1"],["#f97316","#eab308"],
];
// 生成封面渐变
function coverGradient(name){
const p = PALETTES[hashStr(name||"x") % PALETTES.length];
return `linear-gradient(135deg, ${p[0]} 0%, ${p[1]} 100%)`;
}
// 生成封面艺术 HTML(无外部图片时的占位)
function coverArt(game, hidden){
const grad = coverGradient(game.name);
const ch = (game.name||"?").replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g,"").charAt(0) || "?";
const disp = hidden ? 'display:none;' : '';
return `<div class="cv-art gcover" style="${disp}background:${grad}">
<span class="gc-orb1"></span><span class="gc-orb2"></span>
<span class="gc-watermark">${ch}</span>
<span class="gc-cat">${game.category_name||""}</span>
</div>`;
}
// 封面区域(如果有 cover URL 用图片,否则用生成艺术)
function coverHTML(game, opts){
opts = opts||{};
let inner;
if(game.cover){
inner = `<img class="cv-art" src="${game.cover}" alt="${game.name}" style="width:100%;height:100%;object-fit:cover" onerror="this.style.display='none';this.nextElementSibling.style.display=''">` + coverArt(game, true);
} else {
inner = coverArt(game);
}
let badges = "";
if(game.is_hot) badges += `<span class="cv-badge badge-hot">HOT</span>`;
else if(game.is_new) badges += `<span class="cv-badge badge-new">NEW</span>`;
if(game.rating){
badges += `<span class="cv-rating">★ ${Number(game.rating).toFixed(1)}</span>`;
}
const play = opts.noPlay ? "" : `<div class="play-overlay"><div class="po-btn"><svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg></div></div>`;
return `${inner}<div class="cv-glow"></div>${badges}${play}`;
}
// 星级评分 HTML(支持小数)
function starsHTML(rating){
rating = Number(rating)||0;
const pct = Math.max(0, Math.min(100, (rating/5)*100));
return `<span class="stars" style="--pct:${pct}%">
<span class="bg">★★★★★</span>
<span class="fg" style="width:${pct}%">★★★★★</span>
</span>`;
}
// 防抖
function debounce(fn, wait){
let t;
return function(){
const ctx = this, args = arguments;
clearTimeout(t);
t = setTimeout(()=> fn.apply(ctx, args), wait||300);
};
}
// Toast 提示
function toast(msg, type){
const wrap = $("#toastWrap");
const el = document.createElement("div");
el.className = "toast " + (type||"success");
el.innerHTML = `<svg class="ti" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6 9 17l-5-5"/></svg><span>${msg}</span>`;
wrap.appendChild(el);
requestAnimationFrame(()=> el.classList.add("show"));
setTimeout(()=>{
el.classList.remove("show");
setTimeout(()=> el.remove(), 400);
}, 2600);
}
/* ===========================================================================
API 层(fetch 优先,失败回退 Mock)
=========================================================================== */
async function apiGet(path, params){
const url = new URL(API + path, location.origin);
if(params){ Object.keys(params).forEach(k=>{ if(params[k]!==undefined&&params[k]!==""&&params[k]!==null) url.searchParams.set(k, params[k]); }); }
try{
const res = await fetch(url.toString(), {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;
throw new Error(json && json.message || "接口异常");
}catch(e){
// 回退到 Mock
return mockRoute(path, params);
}
}
// Mock 路由:模拟后端响应
function mockRoute(path, params){
params = params||{};
if(path === "/home"){
return {
categories: MOCK.categories,
hot: MOCK.games.filter(g=>g.is_hot).sort((a,b)=>b.plays-a.plays).slice(0,8),
new: MOCK.games.filter(g=>g.is_new).slice(0,4),
};
}
if(path === "/categories"){
return MOCK.categories;
}
if(path === "/hot"){
let limit = parseInt(params.limit)||8;
return MOCK.games.filter(g=>g.is_hot).sort((a,b)=>b.plays-a.plays).slice(0,limit);
}
if(path === "/new"){
let limit = parseInt(params.limit)||4;
return MOCK.games.filter(g=>g.is_new).slice(0,limit);
}
if(path === "/games"){
let list = MOCK.games.slice();
if(params.category_id){ const cid = parseInt(params.category_id); list = list.filter(g=>g.category_id===cid); }
if(params.keyword){ const kw = params.keyword.toLowerCase(); list = list.filter(g=> (g.name||"").toLowerCase().includes(kw) || (g.description||"").toLowerCase().includes(kw)); }
const sort = params.sort;
if(sort==="rating") list.sort((a,b)=>b.rating-a.rating);
else if(sort==="plays") list.sort((a,b)=>b.plays-a.plays);
else if(sort==="newest") list.sort((a,b)=> new Date(b.release_date)-new Date(a.release_date));
else list.sort((a,b)=>a.sort-b.sort);
const total = list.length;
const page = parseInt(params.page)||1;
const pageSize = parseInt(params.page_size)||12;
const start = (page-1)*pageSize;
return { list: list.slice(start, start+pageSize), total, page, page_size: pageSize };
}
// /games/:id
const m = path.match(/^\/games\/(\d+)$/);
if(m){
const id = parseInt(m[1]);
return MOCK.games.find(g=>g.id===id) || null;
}
return null;
}
/* ===========================================================================
渲染:分类
=========================================================================== */
function renderCategoryPills(cats){
const row = $("#catRow");
const all = `<button class="cat-pill active" data-cat="0">全部</button>`;
row.innerHTML = all + cats.map(c=>`<button class="cat-pill" data-cat="${c.id}">${c.name}</button>`).join("");
$$(".cat-pill", row).forEach(btn=>{
btn.addEventListener("click", ()=>{
$$(".cat-pill", row).forEach(b=>b.classList.remove("active"));
btn.classList.add("active");
state.libCategory = parseInt(btn.dataset.cat);
state.libPage = 1;
// 同步底部库分类
$$("#libCats .cat-pill").forEach(b=>{
b.classList.toggle("active", b.dataset.cat === btn.dataset.cat);
});
loadLibrary();
document.getElementById("library").scrollIntoView({behavior:"smooth"});
});
});
// 底部库分类按钮
const libCats = $("#libCats");
libCats.innerHTML = `<button class="cat-pill active" data-cat="0">全部</button>` + cats.map(c=>`<button class="cat-pill" data-cat="${c.id}">${c.name}</button>`).join("");
$$(".cat-pill", libCats).forEach(btn=>{
btn.addEventListener("click", ()=>{
$$(".cat-pill", libCats).forEach(b=>b.classList.remove("active"));
btn.classList.add("active");
state.libCategory = parseInt(btn.dataset.cat);
state.libPage = 1;
// 同步顶部分类
$$("#catRow .cat-pill").forEach(b=>{
b.classList.toggle("active", b.dataset.cat === btn.dataset.cat);
});
loadLibrary();
});
});
}
/* ===========================================================================
渲染:热门游戏(首图大,其余小)
=========================================================================== */
function renderHot(games){
const grid = $("#hotGrid");
if(!games || !games.length){ grid.innerHTML = emptyHTML("暂无热门游戏"); return; }
const big = games[0];
const rest = games.slice(1, 5);
let html = `<div class="card big" data-id="${big.id}">
<div class="cover">${coverHTML(big)}</div>
<div class="card-body">
<h3 class="card-title">${big.name}</h3>
<div class="card-meta">
<span>${big.category_name||""}</span><span class="dot"></span>
<span>${big.developer||""}</span><span class="dot"></span>
<span>${formatNumber(big.plays)} 游玩</span>
</div>
<p class="card-desc">${big.description||""}</p>
<div class="card-tags">${parseJSON(big.tags).slice(0,4).map(t=>`<span class="tag-mini">${t}</span>`).join("")}</div>
</div>
</div>`;
rest.forEach(g=>{
html += `<div class="card" data-id="${g.id}">
<div class="cover">${coverHTML(g)}</div>
<div class="card-body">
<h3 class="card-title">${g.name}</h3>
<div class="card-meta">
<span>${g.category_name||""}</span><span class="dot"></span>
<span>${formatNumber(g.plays)} 游玩</span>
</div>
</div>
</div>`;
});
grid.innerHTML = html;
bindCardClicks(grid);
}
/* ===========================================================================
渲染:新游(横向滚动)
=========================================================================== */
function renderNew(games){
const scroll = $("#newScroll");
if(!games || !games.length){ scroll.innerHTML = emptyHTML("暂无新游"); return; }
scroll.innerHTML = games.map(g=>`
<div class="card" data-id="${g.id}">
<div class="cover">${coverHTML(g)}</div>
<div class="card-body">
<h3 class="card-title">${g.name}</h3>
<div class="card-meta">
<span>${g.category_name||""}</span><span class="dot"></span>
<span>${g.release_date||""}</span>
</div>
</div>
</div>`).join("");
bindCardClicks(scroll);
}
/* ===========================================================================
渲染:游戏库网格
=========================================================================== */
function renderLibrary(data){
const grid = $("#gameGrid");
const info = $("#libInfo");
if(!data || !data.list || !data.list.length){
grid.innerHTML = "";
info.textContent = "没有找到匹配的游戏";
renderPager(0,1);
return;
}
grid.innerHTML = data.list.map(g=>`
<div class="card" data-id="${g.id}">
<div class="cover">${coverHTML(g)}</div>
<div class="card-body">
<h3 class="card-title">${g.name}</h3>
<div class="card-meta">
<span>${g.category_name||""}</span><span class="dot"></span>
<span>${formatNumber(g.plays)} 游玩</span>
</div>
</div>
</div>`).join("");
bindCardClicks(grid);
state.libTotal = data.total||0;
info.textContent = `共 ${data.total} 款游戏 · 第 ${data.page} / ${Math.max(1, Math.ceil(data.total/(data.page_size||state.libPageSize)))} 页`;
renderPager(data.total, data.page, data.page_size||state.libPageSize);
}
// 卡片点击 -> 打开详情
function bindCardClicks(root){
$$(".card[data-id]", root).forEach(card=>{
card.addEventListener("click", ()=> openModal(parseInt(card.dataset.id)));
});
}
// 骨架屏
function skeletonGrid(n, big){
n = n||10;
let html = "";
for(let i=0;i<n;i++){
html += `<div class="skeleton"><div class="sk-shimmer sk-cover"></div>
<div class="sk-shimmer sk-line" style="width:70%"></div>
<div class="sk-shimmer sk-line" style="width:45%"></div></div>`;
}
return html;
}
function skeletonHot(){
return `<div class="skeleton" style="grid-row:span 2"><div class="sk-shimmer" style="height:380px"></div>
<div class="sk-shimmer sk-line" style="width:60%"></div><div class="sk-shimmer sk-line" style="width:80%"></div></div>` +
[1,2,3,4].map(()=>`<div class="skeleton"><div class="sk-shimmer" style="height:150px"></div><div class="sk-shimmer sk-line" style="width:70%"></div></div>`).join("");
}
function emptyHTML(text){
return `<div class="empty" style="grid-column:1/-1">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 15V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h9"/><path d="m16 19 2 2 4-4"/></svg>
<p>${text}</p></div>`;
}
/* ===========================================================================
分页
=========================================================================== */
function renderPager(total, page, pageSize){
const pager = $("#pager");
pageSize = pageSize || state.libPageSize;
const pages = Math.max(1, Math.ceil(total/pageSize));
if(pages <= 1){ pager.innerHTML = ""; return; }
let html = `<button data-page="prev" ${page<=1?"disabled":""}>‹</button>`;
const showPages = [];
if(pages <= 7){
for(let i=1;i<=pages;i++) showPages.push(i);
} else {
showPages.push(1);
if(page > 3) showPages.push("...");
const start = Math.max(2, page-1), end = Math.min(pages-1, page+1);
for(let i=start;i<=end;i++) showPages.push(i);
if(page < pages-2) showPages.push("...");
showPages.push(pages);
}
showPages.forEach(p=>{
if(p==="...") html += `<span class="dots">···</span>`;
else html += `<button class="${p===page?'active':''}" data-page="${p}">${p}</button>`;
});
html += `<button data-page="next" ${page>=pages?"disabled":""}>›</button>`;
pager.innerHTML = html;
$$("button[data-page]", pager).forEach(btn=>{
btn.addEventListener("click", ()=>{
const p = btn.dataset.page;
let next = page;
if(p==="prev") next = page-1;
else if(p==="next") next = page+1;
else next = parseInt(p);
if(next<1 || next>pages || next===page) return;
state.libPage = next;
loadLibrary(true);
});
});
}
/* ===========================================================================
数据加载
=========================================================================== */
async function loadHome(){
// 骨架屏
$("#hotGrid").innerHTML = skeletonHot();
$("#newScroll").innerHTML = [1,2,3,4].map(()=>`<div class="skeleton"><div class="sk-shimmer" style="height:140px"></div><div class="sk-shimmer sk-line" style="width:70%"></div></div>`).join("");
const data = await apiGet("/home");
if(data){
state.categories = data.categories||[];
renderCategoryPills(state.categories);
$("#statGames").textContent = MOCK.games.length; // 预览用计数
animateCounter("#statGames", MOCK.games.length);
renderHot(data.hot||[]);
renderNew(data.new||[]);
renderHeroCard(data.hot&&data.hot[0]);
observeReveal();
}
}
// Hero 卡片填充
function renderHeroCard(game){
if(!game) return;
const cover = $("#heroCover");
if(game.cover && /^https?:\/\//.test(game.cover)){
cover.innerHTML = `<img src="${game.cover}" alt="${game.name}" style="width:100%;height:100%;object-fit:cover">`;
} else {
cover.style.background = coverGradient(game.name);
cover.innerHTML = `<span style="position:absolute;font-size:140px;font-weight:900;opacity:.18;top:-10px;right:-6px;color:#fff">${(game.name||"?").charAt(0)}</span>`;
}
$("#heroTitle").textContent = game.name;
$("#heroCat").textContent = game.category_name||"";
$("#heroDev").textContent = game.developer||"";
$("#heroPlay").onclick = ()=> openModal(game.id);
}
async function loadLibrary(keepScroll){
const grid = $("#gameGrid");
if(!keepScroll) grid.innerHTML = skeletonGrid(10);
else grid.style.opacity = ".4";
const data = await apiGet("/games", {
category_id: state.libCategory || undefined,
keyword: state.libKeyword || undefined,
sort: state.libSort || undefined,
page: state.libPage,
page_size: state.libPageSize,
});
grid.style.opacity = "1";
renderLibrary(data);
}
// 数字滚动动画
function animateCounter(sel, target){
const el = $(sel);
if(!el) return;
let cur = 0;
const dur = 1200, t0 = performance.now();
function step(t){
const p = Math.min(1, (t-t0)/dur);
const eased = 1 - Math.pow(1-p, 3);
el.textContent = Math.round(target*eased) + "+";
if(p<1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
/* ===========================================================================
详情 Modal
=========================================================================== */
async function openModal(id){
const mask = $("#modalMask");
const modal = $("#modal");
// 骨架屏
modal.innerHTML = `<div class="modal-sk">
<div class="sk-shimmer"></div>
<div class="sk-shimmer sk-line w60" style="height:24px"></div>
<div class="sk-shimmer sk-line w40"></div>
<div class="sk-shimmer sk-line w90"></div>
<div class="sk-shimmer sk-line w90"></div>
<div class="sk-shimmer sk-line w60"></div>
</div>`;
mask.classList.add("open");
document.body.style.overflow = "hidden";
const game = await apiGet("/games/"+id);
if(!game){ modal.innerHTML = `<div class="empty" style="padding:80px"><p>游戏不存在</p></div>`; return; }
renderModal(game);
}
function renderModal(g){
const modal = $("#modal");
const tags = parseJSON(g.tags);
const features = parseJSON(g.features);
const headerArt = g.cover
? `<img src="${g.cover}" alt="${g.name}" style="width:100%;height:100%;object-fit:cover" onerror="this.style.display='none';this.parentNode.querySelectorAll('[data-fb]').forEach(function(e){e.style.display=''})"><div data-fb style="position:absolute;inset:0;background:${coverGradient(g.name)};display:none"></div><span data-fb style="position:absolute;font-size:200px;font-weight:900;opacity:.14;top:-30px;right:10px;color:#fff;display:none">${(g.name||"?").charAt(0)}</span>`
: `<div style="position:absolute;inset:0;background:${coverGradient(g.name)}"></div>
<span style="position:absolute;font-size:200px;font-weight:900;opacity:.14;top:-30px;right:10px;color:#fff">${(g.name||"?").charAt(0)}</span>`;
const badges = [];
if(g.is_hot) badges.push(`<span class="cv-badge badge-hot">热门</span>`);
if(g.is_new) badges.push(`<span class="cv-badge badge-new">新游</span>`);
modal.innerHTML = `
<div class="modal-header">
<button class="modal-close" id="modalClose" aria-label="关闭">
<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="M18 6 6 18M6 6l12 12"/></svg>
</button>
<div class="mh-art">${headerArt}</div>
<div class="modal-header-content">
<div class="modal-badges">${badges.join("")}</div>
<h2 class="modal-title">${g.name}</h2>
<div class="modal-sub">
<span>${g.category_name||""}</span>
<span>·</span>
<span>${starsHTML(g.rating)} ${Number(g.rating).toFixed(1)}</span>
<span>·</span>
<span>${formatNumber(g.plays)} 游玩</span>
<span>·</span>
<span>${g.developer||""}</span>
</div>
</div>
</div>
<div class="modal-body">
<div class="modal-tags">${tags.map(t=>`<span class="tag-pill"># ${t}</span>`).join("")}</div>
<div class="modal-row">
<div class="modal-desc">
<h4>游戏简介</h4>
<p>${g.description||"暂无简介"}</p>
<div class="modal-features">
<h4>核心特性</h4>
<div class="feat-list">
${features.map(f=>`<div class="feat-item"><span class="ck"><svg viewBox="0 0 24 24" fill="none"><path d="M20 6 9 17l-5-5"/></svg></span>${f}</div>`).join("")}
</div>
</div>
</div>
<div class="modal-info">
<div class="info-row"><span class="k">开发商</span><span class="v">${g.developer||"—"}</span></div>
<div class="info-row"><span class="k">发行商</span><span class="v">${g.publisher||"—"}</span></div>
<div class="info-row"><span class="k">游戏大小</span><span class="v">${g.size||"—"}</span></div>
<div class="info-row"><span class="k">当前版本</span><span class="v">${g.version||"—"}</span></div>
<div class="info-row"><span class="k">发行日期</span><span class="v">${g.release_date||"—"}</span></div>
<div class="info-row"><span class="k">支持平台</span><span class="v">${g.platform||"—"}</span></div>
<div class="info-row"><span class="k">下载量</span><span class="v">${formatNumber(g.downloads)}</span></div>
</div>
</div>
<div class="modal-cta">
<button class="btn-play-big" id="modalPlay">
<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
开始游戏
</button>
<button class="btn-fav" id="modalFav" aria-label="收藏">
<svg viewBox="0 0 24 24"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.51 4.04 3 5.5l7 7Z"/></svg>
</button>
<span style="color:var(--text-faint);font-size:13px">云端启动 · 无需下载安装</span>
</div>
</div>`;
$("#modalClose").addEventListener("click", closeModal);
$("#modalPlay").addEventListener("click", ()=> toast("正在云端启动 " + g.name + ",请稍候…", "success"));
$("#modalFav").addEventListener("click", function(){
const svg = this.querySelector("svg");
const active = svg.getAttribute("data-on")==="1";
if(active){ svg.setAttribute("data-on","0"); svg.style.fill="none"; svg.style.stroke="var(--text-dim)"; toast("已取消收藏"); }
else { svg.setAttribute("data-on","1"); svg.style.fill="var(--pink)"; svg.style.stroke="var(--pink)"; toast("已加入收藏"); }
});
}
function closeModal(){
const mask = $("#modalMask");
mask.classList.remove("open");
document.body.style.overflow = "";
}
/* ===========================================================================
滚动揭示动画 (IntersectionObserver)
=========================================================================== */
let io;
function observeReveal(){
if(!io){
io = new IntersectionObserver((entries)=>{
entries.forEach(e=>{
if(e.isIntersecting){ e.target.classList.add("in"); io.unobserve(e.target); }
});
}, {threshold:0.12, rootMargin:"0px 0px -40px 0px"});
}
$$(".reveal:not(.in)").forEach(el=> io.observe(el));
// 为卡片网格容器添加 reveal
$("#hotGrid").classList.add("reveal");
$("#newScroll").classList.add("reveal");
$("#gameGrid").classList.add("reveal");
$$(".reveal:not(.in)").forEach(el=> io.observe(el));
}
/* ===========================================================================
事件绑定 & 初始化
=========================================================================== */
function bindEvents(){
// 顶部搜索 -> 同步库搜索并滚动
const navSearch = $("#navSearch");
const libSearch = $("#libSearch");
navSearch.addEventListener("input", debounce(()=>{
state.libKeyword = navSearch.value.trim();
libSearch.value = state.libKeyword;
state.libPage = 1;
loadLibrary();
if(state.libKeyword) document.getElementById("library").scrollIntoView({behavior:"smooth"});
}, 300));
// 库搜索(防抖 300ms)
libSearch.addEventListener("input", debounce(()=>{
state.libKeyword = libSearch.value.trim();
navSearch.value = state.libKeyword;
state.libPage = 1;
loadLibrary();
}, 300));
// 排序
$("#libSort").addEventListener("change", function(){
state.libSort = this.value;
state.libPage = 1;
loadLibrary();
});
// Header 滚动效果
const header = $("#header");
window.addEventListener("scroll", ()=>{
if(window.scrollY > 20) header.classList.add("scrolled");
else header.classList.remove("scrolled");
}, {passive:true});
// 导航高亮
const navMap = {top:"home", library:"browse", hot:"hot", new:"new"};
window.addEventListener("scroll", ()=>{
let current = "home";
const sections = ["top","hot","new","library"];
for(let i=sections.length-1;i>=0;i--){
const el = document.getElementById(sections[i]);
if(el && el.getBoundingClientRect().top <= 120){ current = navMap[sections[i]]; break; }
}
$$(".nav-links a").forEach(a=> a.classList.toggle("active", a.dataset.nav === current));
}, {passive:true});
// Modal 背景点击关闭
$("#modalMask").addEventListener("click", e=>{
if(e.target === e.currentTarget) closeModal();
});
// ESC 关闭
document.addEventListener("keydown", e=>{
if(e.key === "Escape" && $("#modalMask").classList.contains("open")) closeModal();
});
// 移动端菜单(简单滚动到库)
$(".menu-toggle").addEventListener("click", ()=>{
document.getElementById("library").scrollIntoView({behavior:"smooth"});
});
// 平滑滚动锚点
$$('.nav-links a[href^="#"], a.btn-ghost[href^="#"], .foot-col a[href^="#"]').forEach(a=>{
a.addEventListener("click", e=>{
const href = a.getAttribute("href");
if(href && href.length>1 && document.querySelector(href)){
e.preventDefault();
document.querySelector(href).scrollIntoView({behavior:"smooth"});
}
});
});
}
// 启动
document.addEventListener("DOMContentLoaded", ()=>{
bindEvents();
loadHome();
loadLibrary();
observeReveal();
});
</script>
</body>
</html>