1240 lines
71 KiB
HTML
1240 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>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"><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&¶ms[k]!==""&¶ms[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>
|