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

1624 lines
66 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>NEON ARCADE · 次世代游戏平台</title>
<style>
/* ============================================================
NEON ARCADE — 赛博朋克霓虹风游戏平台
单文件模板,零外部依赖
============================================================ */
/* ---------- 设计变量 ---------- */
:root{
--bg:#0a0a0f;
--bg-2:#0e0e18;
--bg-card:#10101c;
--bg-card-2:#131322;
--cyan:#00f0ff;
--magenta:#ff0080;
--purple:#8b00ff;
--green:#39ff14;
--yellow:#ffe600;
--text:#e8e8f0;
--text-dim:#8a8aa0;
--text-mute:#555570;
--border:rgba(0,240,255,.18);
--border-magenta:rgba(255,0,128,.25);
--glow-cyan:0 0 6px rgba(0,240,255,.7),0 0 16px rgba(0,240,255,.45);
--glow-magenta:0 0 6px rgba(255,0,128,.7),0 0 16px rgba(255,0,128,.45);
--glow-purple:0 0 6px rgba(139,0,255,.7),0 0 16px rgba(139,0,255,.45);
--radius:12px;
--radius-sm:8px;
--font:'Segoe UI','Microsoft YaHei','PingFang SC',system-ui,sans-serif;
--mono:'Consolas','Courier New',monospace;
}
/* ---------- 重置 ---------- */
*{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:smooth}
body{
font-family:var(--font);
background:var(--bg);
color:var(--text);
overflow-x:hidden;
line-height:1.6;
min-height:100vh;
}
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}
::selection{background:var(--magenta);color:#fff}
/* 滚动条 */
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-track{background:#0a0a12}
::-webkit-scrollbar-thumb{background:linear-gradient(var(--cyan),var(--purple));border-radius:6px}
::-webkit-scrollbar-thumb:hover{background:linear-gradient(var(--magenta),var(--purple))}
/* ---------- 背景层 ---------- */
.bg-layers{position:fixed;inset:0;z-index:-1;overflow:hidden;pointer-events:none}
/* 动态渐变 */
.bg-gradient{
position:absolute;inset:-20%;
background:
radial-gradient(circle at 15% 20%,rgba(0,240,255,.10),transparent 45%),
radial-gradient(circle at 85% 15%,rgba(255,0,128,.10),transparent 45%),
radial-gradient(circle at 50% 85%,rgba(139,0,255,.10),transparent 50%);
animation:bgFloat 18s ease-in-out infinite alternate;
}
@keyframes bgFloat{
0%{transform:translate(0,0) scale(1)}
50%{transform:translate(-3%,2%) scale(1.05)}
100%{transform:translate(2%,-2%) scale(1)}
}
/* 网格 */
.bg-grid{
position:absolute;inset:0;
background-image:
linear-gradient(rgba(0,240,255,.045) 1px,transparent 1px),
linear-gradient(90deg,rgba(0,240,255,.045) 1px,transparent 1px);
background-size:48px 48px;
mask-image:radial-gradient(ellipse at center,#000 30%,transparent 80%);
-webkit-mask-image:radial-gradient(ellipse at center,#000 30%,transparent 80%);
animation:gridMove 30s linear infinite;
}
@keyframes gridMove{from{background-position:0 0}to{background-position:48px 48px}}
/* 扫描线 */
.bg-scanlines{
position:absolute;inset:0;
background:repeating-linear-gradient(0deg,
rgba(0,0,0,0) 0px,rgba(0,0,0,0) 2px,
rgba(0,240,255,.022) 3px,rgba(0,0,0,0) 4px);
pointer-events:none;
}
/* 噪点 */
.bg-noise{
position:absolute;inset:0;opacity:.04;mix-blend-mode:overlay;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* ---------- 通用 ---------- */
.container{max-width:1440px;margin:0 auto;padding:0 24px}
.section{padding:64px 0;position:relative}
.section-head{display:flex;align-items:flex-end;justify-content:space-between;margin-bottom:32px;flex-wrap:wrap;gap:16px}
.section-title{
font-size:30px;font-weight:800;letter-spacing:1px;
display:flex;align-items:center;gap:14px;
}
.section-title .bar{
width:5px;height:30px;border-radius:3px;
background:linear-gradient(var(--cyan),var(--magenta));
box-shadow:var(--glow-cyan);
}
.section-title .en{
font-family:var(--mono);font-size:13px;color:var(--cyan);
letter-spacing:3px;text-transform:uppercase;font-weight:400;
text-shadow:0 0 8px rgba(0,240,255,.5);
}
.section-sub{color:var(--text-dim);font-size:14px}
/* 霓虹文字 */
.neon-cyan{color:var(--cyan);text-shadow:var(--glow-cyan)}
.neon-magenta{color:var(--magenta);text-shadow:var(--glow-magenta)}
.neon-purple{color:var(--purple);text-shadow:var(--glow-purple)}
/* 故障文字 */
.glitch{position:relative;display:inline-block}
.glitch::before,.glitch::after{
content:attr(data-text);position:absolute;top:0;left:0;width:100%;
overflow:hidden;
}
.glitch::before{color:var(--magenta);z-index:-1;animation:glitchTop 3s infinite linear alternate-reverse}
.glitch::after{color:var(--cyan);z-index:-2;animation:glitchBot 2.5s infinite linear alternate-reverse}
@keyframes glitchTop{
0%,90%,100%{transform:translate(0,0);clip-path:inset(0 0 100% 0)}
92%{transform:translate(-3px,-2px);clip-path:inset(0 0 55% 0)}
94%{transform:translate(3px,1px);clip-path:inset(0 0 40% 0)}
96%{transform:translate(-2px,-1px);clip-path:inset(0 0 70% 0)}
}
@keyframes glitchBot{
0%,88%,100%{transform:translate(0,0);clip-path:inset(100% 0 0 0)}
90%{transform:translate(3px,2px);clip-path:inset(60% 0 0 0)}
93%{transform:translate(-3px,-1px);clip-path:inset(45% 0 0 0)}
95%{transform:translate(2px,1px);clip-path:inset(30% 0 0 0)}
}
/* 按钮 */
.btn{
display:inline-flex;align-items:center;gap:8px;
padding:12px 28px;border-radius:var(--radius-sm);
font-weight:700;font-size:15px;letter-spacing:1px;
position:relative;overflow:hidden;transition:.3s;
border:1px solid var(--cyan);color:var(--cyan);
background:rgba(0,240,255,.06);
text-shadow:0 0 6px rgba(0,240,255,.6);
box-shadow:0 0 0 rgba(0,240,255,0);
}
.btn:hover{
background:rgba(0,240,255,.16);
box-shadow:0 0 18px rgba(0,240,255,.5),inset 0 0 14px rgba(0,240,255,.12);
transform:translateY(-2px);
}
.btn-magenta{border-color:var(--magenta);color:var(--magenta);background:rgba(255,0,128,.06);text-shadow:0 0 6px rgba(255,0,128,.6)}
.btn-magenta:hover{background:rgba(255,0,128,.16);box-shadow:0 0 18px rgba(255,0,128,.5),inset 0 0 14px rgba(255,0,128,.12)}
.btn-primary{
background:linear-gradient(135deg,var(--magenta),var(--purple));
border:none;color:#fff;letter-spacing:2px;
box-shadow:0 0 16px rgba(139,0,255,.5);
}
.btn-primary:hover{box-shadow:0 0 28px rgba(255,0,128,.6),0 0 18px rgba(139,0,255,.6);transform:translateY(-2px)}
.btn::after{
content:"";position:absolute;top:0;left:-120%;width:60%;height:100%;
background:linear-gradient(90deg,transparent,rgba(255,255,255,.18),transparent);
transform:skewX(-20deg);transition:.6s;
}
.btn:hover::after{left:130%}
/* ============================================================
HEADER
============================================================ */
.header{
position:fixed;top:0;left:0;right:0;z-index:100;
backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);
background:rgba(10,10,15,.72);
border-bottom:1px solid var(--border);
transition:.3s;
}
.header.scrolled{background:rgba(10,10,15,.92);border-bottom-color:var(--border-magenta)}
.header-inner{display:flex;align-items:center;gap:32px;height:68px}
.logo{
font-family:var(--mono);font-size:22px;font-weight:900;letter-spacing:2px;
display:flex;align-items:center;gap:10px;white-space:nowrap;
}
.logo .dot{
width:10px;height:10px;border-radius:50%;background:var(--magenta);
box-shadow:var(--glow-magenta);animation:pulse 1.6s infinite;
}
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(.8)}}
.logo .t1{color:var(--cyan);text-shadow:var(--glow-cyan)}
.logo .t2{color:var(--magenta);text-shadow:var(--glow-magenta)}
.nav{display:flex;gap:6px}
.nav a{
padding:8px 16px;border-radius:var(--radius-sm);font-size:15px;font-weight:600;
color:var(--text-dim);transition:.25s;position:relative;
}
.nav a:hover{color:var(--cyan);text-shadow:0 0 8px rgba(0,240,255,.5)}
.nav a.active{color:var(--cyan);background:rgba(0,240,255,.08);text-shadow:0 0 8px rgba(0,240,255,.6)}
.nav a.active::after{
content:"";position:absolute;bottom:2px;left:50%;transform:translateX(-50%);
width:18px;height:2px;background:var(--cyan);box-shadow:var(--glow-cyan);border-radius:2px;
}
.header-search{flex:1;max-width:380px;margin-left:auto;position:relative}
.header-search input{
width:100%;padding:10px 16px 10px 40px;border-radius:var(--radius-sm);
background:rgba(0,0,0,.4);border:1px solid var(--border);color:var(--text);
font-size:14px;transition:.3s;
}
.header-search input::placeholder{color:var(--text-mute)}
.header-search input:focus{outline:none;border-color:var(--cyan);box-shadow:0 0 12px rgba(0,240,255,.3)}
.header-search .icon{
position:absolute;left:13px;top:50%;transform:translateY(-50%);
color:var(--cyan);font-size:16px;opacity:.7;
}
.header-search .clear-btn{
position:absolute;right:10px;top:50%;transform:translateY(-50%);
color:var(--text-mute);font-size:14px;display:none;cursor:pointer;
}
.header-search .clear-btn:hover{color:var(--magenta)}
.mobile-toggle{display:none;font-size:24px;color:var(--cyan);cursor:pointer}
/* ============================================================
HERO
============================================================ */
.hero{
min-height:100vh;display:flex;align-items:center;justify-content:center;
position:relative;text-align:center;padding:120px 24px 80px;
}
.hero::before{
content:"";position:absolute;inset:0;
background:radial-gradient(ellipse at center,transparent 0%,rgba(10,10,15,.6) 80%);
pointer-events:none;
}
.hero-content{position:relative;z-index:2;max-width:900px}
.hero-tag{
display:inline-flex;align-items:center;gap:10px;
padding:6px 18px;border:1px solid var(--border-magenta);border-radius:30px;
font-family:var(--mono);font-size:13px;color:var(--magenta);
letter-spacing:2px;margin-bottom:28px;
background:rgba(255,0,128,.06);text-shadow:0 0 6px rgba(255,0,128,.5);
}
.hero-tag .blink{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);animation:pulse 1.4s infinite}
.hero h1{
font-size:clamp(40px,8vw,84px);font-weight:900;line-height:1.1;
letter-spacing:2px;margin-bottom:24px;
}
.hero h1 .l1{color:#fff;text-shadow:0 0 20px rgba(0,240,255,.4)}
.hero h1 .l2{
background:linear-gradient(135deg,var(--cyan),var(--magenta),var(--purple));
-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;
filter:drop-shadow(0 0 18px rgba(255,0,128,.5));
}
.hero p{
font-size:clamp(15px,2vw,19px);color:var(--text-dim);
max-width:640px;margin:0 auto 40px;letter-spacing:1px;
}
.hero-actions{display:flex;gap:18px;justify-content:center;flex-wrap:wrap}
.hero-stats{
display:flex;gap:48px;justify-content:center;margin-top:56px;flex-wrap:wrap;
}
.hero-stat{text-align:center}
.hero-stat .num{
font-size:36px;font-weight:900;font-family:var(--mono);
background:linear-gradient(135deg,var(--cyan),var(--magenta));
-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;
}
.hero-stat .label{font-size:13px;color:var(--text-dim);letter-spacing:2px;text-transform:uppercase}
/* 浮动装饰框 */
.hero-deco{position:absolute;font-family:var(--mono);font-size:12px;color:rgba(0,240,255,.25);pointer-events:none}
.hero-deco.d1{top:18%;left:6%;animation:floatY 6s ease-in-out infinite}
.hero-deco.d2{top:24%;right:7%;animation:floatY 7s ease-in-out infinite reverse}
.hero-deco.d3{bottom:16%;left:9%;animation:floatY 5s ease-in-out infinite}
.hero-deco.d4{bottom:22%;right:8%;animation:floatY 8s ease-in-out infinite reverse}
@keyframes floatY{0%,100%{transform:translateY(0)}50%{transform:translateY(-14px)}}
/* 滚动提示 */
.scroll-hint{
position:absolute;bottom:30px;left:50%;transform:translateX(-50%);
color:var(--text-mute);font-size:12px;letter-spacing:2px;
display:flex;flex-direction:column;align-items:center;gap:8px;
animation:floatY 2s ease-in-out infinite;
}
.scroll-hint .arrow{width:1px;height:30px;background:linear-gradient(var(--cyan),transparent)}
/* ============================================================
CATEGORIES
============================================================ */
.categories-wrap{margin-bottom:8px}
.cat-scroll{
display:flex;gap:12px;overflow-x:auto;padding:6px 2px 14px;
scroll-behavior:smooth;
}
.cat-scroll::-webkit-scrollbar{height:4px}
.cat-pill{
flex:0 0 auto;padding:10px 22px;border-radius:30px;
border:1px solid var(--border);background:rgba(0,0,0,.3);
color:var(--text-dim);font-size:14px;font-weight:600;white-space:nowrap;
transition:.3s;display:flex;align-items:center;gap:8px;
}
.cat-pill .ico{font-size:16px}
.cat-pill:hover{border-color:var(--cyan);color:var(--cyan);box-shadow:0 0 12px rgba(0,240,255,.25)}
.cat-pill.active{
border-color:var(--magenta);color:#fff;
background:linear-gradient(135deg,rgba(255,0,128,.2),rgba(139,0,255,.2));
box-shadow:0 0 16px rgba(255,0,128,.4);
text-shadow:0 0 6px rgba(255,0,128,.6);
}
/* ============================================================
GAME CARDS
============================================================ */
.games-grid{
display:grid;gap:22px;
grid-template-columns:repeat(5,1fr);
}
.games-grid.cols-hot{grid-template-columns:repeat(4,1fr)}
.new-scroll{
display:flex;gap:20px;overflow-x:auto;padding:6px 2px 20px;scroll-behavior:smooth;
}
.new-scroll::-webkit-scrollbar{height:6px}
.new-card{flex:0 0 auto;width:300px}
.game-card{
background:linear-gradient(160deg,var(--bg-card),var(--bg-card-2));
border:1px solid var(--border);border-radius:var(--radius);
overflow:hidden;cursor:pointer;position:relative;
transition:.35s cubic-bezier(.2,.8,.2,1);
display:flex;flex-direction:column;
}
.game-card::before{
content:"";position:absolute;inset:0;border-radius:var(--radius);
padding:1px;background:linear-gradient(135deg,var(--cyan),transparent 40%,var(--magenta));
-webkit-mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);
-webkit-mask-composite:xor;mask-composite:exclude;
opacity:0;transition:.35s;pointer-events:none;
}
.game-card:hover{
transform:translateY(-6px);
border-color:transparent;
box-shadow:0 12px 36px rgba(0,240,255,.18),0 0 22px rgba(139,0,255,.14);
}
.game-card:hover::before{opacity:1}
.game-card .cover{
position:relative;aspect-ratio:3/4;overflow:hidden;background:#0a0a14;
}
.game-card .cover img{width:100%;height:100%;object-fit:cover;transition:.5s}
.game-card:hover .cover img{transform:scale(1.08)}
.game-card .cover-grad{
position:absolute;inset:0;
background:linear-gradient(to top,rgba(10,10,28,.96) 8%,transparent 55%);
}
.game-card .badges{position:absolute;top:10px;left:10px;display:flex;gap:6px;z-index:2}
.badge{
font-size:10px;font-weight:700;padding:3px 9px;border-radius:4px;
letter-spacing:1px;font-family:var(--mono);text-transform:uppercase;
}
.badge.hot{background:rgba(255,0,128,.85);color:#fff;box-shadow:0 0 8px rgba(255,0,128,.6)}
.badge.new{background:rgba(57,255,20,.85);color:#000;box-shadow:0 0 8px rgba(57,255,20,.5)}
.badge.rating{background:rgba(255,230,0,.85);color:#000}
.game-card .rating-float{
position:absolute;top:10px;right:10px;z-index:2;
background:rgba(0,0,0,.65);backdrop-filter:blur(4px);
padding:4px 10px;border-radius:6px;border:1px solid var(--border);
font-size:13px;font-weight:700;color:var(--yellow);
display:flex;align-items:center;gap:3px;
}
.game-card .card-body{padding:14px 14px 16px;flex:1;display:flex;flex-direction:column}
.game-card .card-title{
font-size:15px;font-weight:700;margin-bottom:6px;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.game-card:hover .card-title{color:var(--cyan)}
.game-card .card-cat{font-size:11px;color:var(--text-mute);margin-bottom:10px;letter-spacing:1px;font-family:var(--mono)}
.game-card .card-tags{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px;min-height:22px}
.mini-tag{font-size:10px;padding:2px 8px;border-radius:4px;background:rgba(0,240,255,.08);color:var(--cyan);border:1px solid rgba(0,240,255,.15)}
.game-card .card-foot{
margin-top:auto;display:flex;justify-content:space-between;align-items:center;
font-size:12px;color:var(--text-dim);padding-top:8px;border-top:1px solid rgba(255,255,255,.04);
}
.game-card .card-foot .plays{display:flex;align-items:center;gap:5px}
.game-card .card-foot .plays .ic{color:var(--magenta)}
/* 新游横滑卡片 */
.new-card .game-card .cover{aspect-ratio:16/10}
.new-card .game-card .release-date{font-size:11px;color:var(--cyan);font-family:var(--mono)}
/* ============================================================
LIBRARY 工具栏
============================================================ */
.lib-toolbar{
display:flex;gap:16px;align-items:center;flex-wrap:wrap;margin-bottom:28px;
padding:18px 20px;border-radius:var(--radius);
background:rgba(10,10,20,.5);border:1px solid var(--border);
}
.lib-search{flex:1;min-width:220px;position:relative}
.lib-search input{
width:100%;padding:10px 16px 10px 40px;border-radius:var(--radius-sm);
background:rgba(0,0,0,.4);border:1px solid var(--border);color:var(--text);font-size:14px;transition:.3s;
}
.lib-search input:focus{outline:none;border-color:var(--cyan);box-shadow:0 0 12px rgba(0,240,255,.3)}
.lib-search .icon{position:absolute;left:13px;top:50%;transform:translateY(-50%);color:var(--cyan);opacity:.7}
.lib-search .spinner{position:absolute;right:14px;top:50%;transform:translateY(-50%);width:16px;height:16px;border:2px solid rgba(0,240,255,.2);border-top-color:var(--cyan);border-radius:50%;animation:spin .7s linear infinite;display:none}
.lib-search .spinner.show{display:block}
@keyframes spin{to{transform:translateY(-50%) rotate(360deg)}}
.sort-wrap{position:relative}
.sort-btn{
display:flex;align-items:center;gap:10px;padding:10px 18px;border-radius:var(--radius-sm);
background:rgba(0,0,0,.4);border:1px solid var(--border);color:var(--text);font-size:14px;transition:.3s;
}
.sort-btn:hover{border-color:var(--cyan);box-shadow:0 0 10px rgba(0,240,255,.2)}
.sort-btn .arrow{font-size:10px;transition:.3s}
.sort-wrap.open .sort-btn .arrow{transform:rotate(180deg)}
.sort-menu{
position:absolute;top:calc(100% + 8px);right:0;min-width:160px;z-index:20;
background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-sm);
padding:6px;opacity:0;visibility:hidden;transform:translateY(-8px);transition:.25s;
box-shadow:0 8px 28px rgba(0,0,0,.6);
}
.sort-wrap.open .sort-menu{opacity:1;visibility:visible;transform:translateY(0)}
.sort-menu button{
display:block;width:100%;text-align:left;padding:9px 14px;border-radius:6px;
font-size:13px;color:var(--text-dim);transition:.2s;
}
.sort-menu button:hover{background:rgba(0,240,255,.08);color:var(--cyan)}
.sort-menu button.active{background:rgba(255,0,128,.1);color:var(--magenta)}
.lib-info{font-size:13px;color:var(--text-dim);font-family:var(--mono)}
.lib-info b{color:var(--cyan)}
/* ============================================================
PAGINATION
============================================================ */
.pagination{display:flex;justify-content:center;align-items:center;gap:8px;margin-top:40px;flex-wrap:wrap}
.page-btn{
min-width:40px;height:40px;padding:0 12px;border-radius:8px;
background:rgba(0,0,0,.4);border:1px solid var(--border);color:var(--text-dim);
font-size:14px;font-weight:600;transition:.25s;display:flex;align-items:center;justify-content:center;
}
.page-btn:hover:not(:disabled){border-color:var(--cyan);color:var(--cyan);box-shadow:0 0 10px rgba(0,240,255,.25)}
.page-btn.active{
background:linear-gradient(135deg,var(--magenta),var(--purple));
border-color:transparent;color:#fff;box-shadow:0 0 14px rgba(139,0,255,.5);
}
.page-btn:disabled{opacity:.35;cursor:not-allowed}
.page-ellipsis{color:var(--text-mute);padding:0 4px}
/* ============================================================
SKELETON
============================================================ */
.skel-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
.skel-cover{aspect-ratio:3/4;background:linear-gradient(110deg,#13131f 30%,#1a1a2e 50%,#13131f 70%);background-size:200% 100%;animation:shimmer 1.4s infinite}
.skel-line{height:12px;border-radius:4px;background:linear-gradient(110deg,#13131f 30%,#1a1a2e 50%,#13131f 70%);background-size:200% 100%;animation:shimmer 1.4s infinite;margin:10px 14px}
.skel-line.short{width:50%}
.skel-line.mid{width:75%}
@keyframes shimmer{from{background-position:200% 0}to{background-position:-200% 0}}
/* ============================================================
MODAL
============================================================ */
.modal-overlay{
position:fixed;inset:0;z-index:1000;
background:rgba(5,5,10,.75);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);
display:flex;align-items:center;justify-content:center;padding:24px;
opacity:0;visibility:hidden;transition:.3s;
}
.modal-overlay.open{opacity:1;visibility:visible}
.modal{
width:100%;max-width:920px;max-height:90vh;overflow-y:auto;
background:linear-gradient(160deg,var(--bg-card),#0c0c16);
border:1px solid var(--border-magenta);border-radius:var(--radius);
box-shadow:0 0 50px rgba(139,0,255,.3),0 0 80px rgba(0,240,255,.1);
transform:scale(.92) translateY(20px);transition:.35s cubic-bezier(.2,.8,.2,1);
position:relative;
}
.modal-overlay.open .modal{transform:scale(1) translateY(0)}
.modal::-webkit-scrollbar{width:6px}
.modal-close{
position:absolute;top:16px;right:16px;z-index:10;
width:38px;height:38px;border-radius:50%;
background:rgba(0,0,0,.6);border:1px solid var(--border);
font-size:20px;color:var(--text-dim);transition:.25s;
display:flex;align-items:center;justify-content:center;
}
.modal-close:hover{border-color:var(--magenta);color:var(--magenta);box-shadow:0 0 12px rgba(255,0,128,.4);transform:rotate(90deg)}
.modal-hero{position:relative;height:280px;overflow:hidden}
.modal-hero img{width:100%;height:100%;object-fit:cover}
.modal-hero-grad{position:absolute;inset:0;background:linear-gradient(to top,var(--bg-card) 5%,transparent 60%),linear-gradient(to right,rgba(10,10,20,.6),transparent)}
.modal-hero-info{position:absolute;bottom:24px;left:32px;right:32px;z-index:2}
.modal-cat-tag{font-family:var(--mono);font-size:12px;color:var(--cyan);letter-spacing:2px;text-transform:uppercase;margin-bottom:8px;text-shadow:0 0 8px rgba(0,240,255,.5)}
.modal-title{font-size:34px;font-weight:900;line-height:1.15;margin-bottom:10px;text-shadow:0 0 18px rgba(0,0,0,.8)}
.modal-rating-row{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.stars{color:var(--yellow);font-size:18px;letter-spacing:2px;text-shadow:0 0 8px rgba(255,230,0,.4)}
.stars .empty{color:var(--text-mute);text-shadow:none}
.rating-num{font-size:20px;font-weight:800;color:var(--yellow)}
.modal-body{padding:28px 32px 36px}
.modal-desc{color:var(--text-dim);font-size:15px;line-height:1.8;margin-bottom:24px}
.modal-section-label{
font-family:var(--mono);font-size:12px;color:var(--cyan);letter-spacing:3px;
text-transform:uppercase;margin-bottom:14px;display:flex;align-items:center;gap:10px;
}
.modal-section-label::before{content:"";width:14px;height:2px;background:var(--cyan);box-shadow:var(--glow-cyan)}
.modal-section-label::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,var(--border),transparent)}
.modal-tags{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:24px}
.modal-tag{
padding:6px 16px;border-radius:20px;font-size:13px;
background:rgba(0,240,255,.06);border:1px solid rgba(0,240,255,.2);color:var(--cyan);
transition:.25s;
}
.modal-tag:hover{background:rgba(0,240,255,.15);box-shadow:0 0 10px rgba(0,240,255,.3)}
.modal-features{margin-bottom:24px}
.modal-features li{
padding:10px 0 10px 28px;position:relative;color:var(--text);font-size:14px;
border-bottom:1px solid rgba(255,255,255,.04);
}
.modal-features li::before{
content:"▹";position:absolute;left:6px;top:10px;color:var(--magenta);
text-shadow:0 0 6px rgba(255,0,128,.6);font-size:16px;
}
.modal-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:28px}
.stat-box{
text-align:center;padding:16px 10px;border-radius:var(--radius-sm);
background:rgba(0,0,0,.3);border:1px solid var(--border);
}
.stat-box .v{font-size:22px;font-weight:800;font-family:var(--mono);color:var(--cyan);text-shadow:0 0 8px rgba(0,240,255,.4)}
.stat-box .l{font-size:11px;color:var(--text-mute);letter-spacing:1px;margin-top:4px}
.modal-meta{display:grid;grid-template-columns:repeat(2,1fr);gap:12px 32px;margin-bottom:28px}
.meta-item{display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid rgba(255,255,255,.04);font-size:14px}
.meta-item .k{color:var(--text-mute)}
.meta-item .v{color:var(--text);font-weight:600}
.modal-shots{display:flex;gap:12px;overflow-x:auto;padding-bottom:8px;margin-bottom:28px}
.modal-shots img{flex:0 0 auto;width:340px;height:190px;object-fit:cover;border-radius:var(--radius-sm);border:1px solid var(--border);transition:.3s}
.modal-shots img:hover{border-color:var(--cyan);box-shadow:0 0 14px rgba(0,240,255,.3)}
.modal-actions{display:flex;gap:14px;flex-wrap:wrap}
.modal-actions .btn{flex:1;min-width:200px;justify-content:center;padding:16px}
/* 模态骨架 */
.modal-skel{padding:32px}
.modal-skel .skel-line{margin:14px 0;height:14px}
.modal-skel .skel-line.title{height:30px;width:60%}
.modal-skel .skel-cover{height:240px;border-radius:var(--radius);margin-bottom:24px}
/* ============================================================
FOOTER
============================================================ */
.footer{
border-top:1px solid var(--border);margin-top:40px;
padding:50px 0 30px;background:rgba(8,8,14,.6);
}
.footer-grid{display:grid;grid-template-columns:2fr 1fr 1fr 1fr;gap:40px;margin-bottom:36px}
.footer-brand .logo{margin-bottom:16px;font-size:20px}
.footer-brand p{color:var(--text-mute);font-size:13px;line-height:1.7;max-width:300px}
.footer-col h4{font-size:14px;font-weight:700;margin-bottom:16px;color:var(--cyan);letter-spacing:1px}
.footer-col a{display:block;color:var(--text-mute);font-size:13px;padding:5px 0;transition:.2s}
.footer-col a:hover{color:var(--magenta);text-shadow:0 0 6px rgba(255,0,128,.4);padding-left:6px}
.footer-bottom{
display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px;
padding-top:24px;border-top:1px solid rgba(255,255,255,.05);
color:var(--text-mute);font-size:12px;font-family:var(--mono);
}
.footer-bottom .ic{color:var(--cyan)}
/* ============================================================
滚动动画
============================================================ */
.reveal{opacity:0;transform:translateY(40px);transition:.7s cubic-bezier(.2,.8,.2,1)}
.reveal.in{opacity:1;transform:translateY(0)}
.reveal.d1{transition-delay:.08s}
.reveal.d2{transition-delay:.16s}
.reveal.d3{transition-delay:.24s}
.reveal.d4{transition-delay:.32s}
/* 空状态 */
.empty-state{text-align:center;padding:60px 20px;color:var(--text-mute)}
.empty-state .ico{font-size:48px;margin-bottom:16px;opacity:.4}
.empty-state p{font-size:15px}
/* 错误提示 */
.toast{
position:fixed;bottom:30px;left:50%;transform:translateX(-50%) translateY(100px);
background:rgba(20,5,15,.95);border:1px solid var(--magenta);border-radius:10px;
padding:14px 24px;color:var(--magenta);font-size:14px;z-index:2000;
box-shadow:0 0 20px rgba(255,0,128,.4);transition:.4s;opacity:0;
display:flex;align-items:center;gap:10px;
}
.toast.show{transform:translateX(-50%) translateY(0);opacity:1}
/* ============================================================
响应式
============================================================ */
@media(max-width:1200px){
.games-grid{grid-template-columns:repeat(4,1fr)}
.games-grid.cols-hot{grid-template-columns:repeat(3,1fr)}
}
@media(max-width:900px){
.games-grid,.games-grid.cols-hot{grid-template-columns:repeat(3,1fr)}
.footer-grid{grid-template-columns:1fr 1fr}
.modal-stats{grid-template-columns:repeat(2,1fr)}
.modal-meta{grid-template-columns:1fr}
.nav{display:none}
.mobile-toggle{display:block}
.nav.mobile-open{
display:flex;flex-direction:column;position:absolute;top:68px;left:0;right:0;
background:rgba(10,10,15,.98);padding:16px;border-bottom:1px solid var(--border);gap:4px;
}
.nav.mobile-open a{width:100%}
.header-search{max-width:none}
.hero-stats{gap:28px}
}
@media(max-width:640px){
.games-grid,.games-grid.cols-hot{grid-template-columns:repeat(2,1fr)}
.section{padding:48px 0}
.section-title{font-size:24px}
.container{padding:0 16px}
.modal-hero{height:200px}
.modal-title{font-size:24px}
.modal-body{padding:20px}
.new-card{width:240px}
.footer-grid{grid-template-columns:1fr}
.lib-toolbar{flex-direction:column;align-items:stretch}
.modal-stats{grid-template-columns:repeat(2,1fr)}
.header-inner{gap:12px}
.logo{font-size:18px}
}
/* 加载圈 */
.loading-center{display:flex;justify-content:center;align-items:center;padding:60px}
.neon-loader{
width:50px;height:50px;border-radius:50%;
border:3px solid rgba(0,240,255,.15);
border-top-color:var(--cyan);border-right-color:var(--magenta);
animation:spin .9s linear infinite;
box-shadow:0 0 16px rgba(0,240,255,.3);
}
</style>
</head>
<body>
<!-- 背景层 -->
<div class="bg-layers">
<div class="bg-gradient"></div>
<div class="bg-grid"></div>
<div class="bg-scanlines"></div>
<div class="bg-noise"></div>
</div>
<!-- HEADER -->
<header class="header" id="header">
<div class="container header-inner">
<a href="#home" class="logo">
<span class="dot"></span>
<span class="t1">NEON</span> <span class="t2">ARCADE</span>
</a>
<nav class="nav" id="nav">
<a href="#home" class="active" data-scroll="home">首页</a>
<a href="#library" data-scroll="library">游戏库</a>
<a href="#hot" data-scroll="hot">热门</a>
<a href="#new" data-scroll="new">新游</a>
</nav>
<div class="header-search">
<span class="icon">&#128269;</span>
<input type="text" id="globalSearch" placeholder="搜索游戏..." autocomplete="off">
<span class="clear-btn" id="globalSearchClear">&times;</span>
</div>
<span class="mobile-toggle" id="mobileToggle">&#9776;</span>
</div>
</header>
<!-- HERO -->
<section class="hero" id="home">
<div class="hero-deco d1">[ SYS:ONLINE ]<br>// NEURAL_LINK<br>// 0xFF0080</div>
<div class="hero-deco d2">{ STATUS: READY }<br>// FPS:999<br>// PING:0ms</div>
<div class="hero-deco d3">&lt; protocol_v9 &gt;<br>// ENCRYPTED<br>// SECURE</div>
<div class="hero-deco d4">[ DATA_STREAM ]<br>// 1.21GW<br>// STABLE</div>
<div class="hero-content">
<div class="hero-tag"><span class="blink"></span> NEXT-GEN PLATFORM // 2077 EDITION</div>
<h1>
<span class="l1 glitch" data-text="次世代">次世代</span><br>
<span class="l2 glitch" data-text="游戏平台">游戏平台</span>
</h1>
<p>潜入霓虹深渊,探索数万款次世代游戏。从开放世界到硬核竞技,从独立精品到 3A 巨制 —— 你的赛博冒险,由此启程。</p>
<div class="hero-actions">
<button class="btn btn-primary" onclick="document.getElementById('library').scrollIntoView({behavior:'smooth'})">立即探索 &#9654;</button>
<button class="btn btn-magenta" onclick="document.getElementById('hot').scrollIntoView({behavior:'smooth'})">查看热门</button>
</div>
<div class="hero-stats">
<div class="hero-stat"><div class="num" data-count="12860">0</div><div class="label">游戏数量</div></div>
<div class="hero-stat"><div class="num" data-count="5800000">0</div><div class="label">注册玩家</div></div>
<div class="hero-stat"><div class="num" data-count="248">0</div><div class="label">游戏分类</div></div>
<div class="hero-stat"><div class="num" data-count="99" data-suffix=".9%">0</div><div class="label">好评率</div></div>
</div>
</div>
<div class="scroll-hint">SCROLL <div class="arrow"></div></div>
</section>
<!-- CATEGORIES -->
<section class="section" id="categories" style="padding-top:32px">
<div class="container">
<div class="section-head reveal">
<div>
<div class="section-title"><span class="bar"></span>游戏分类 <span class="en">/ CATEGORIES</span></div>
</div>
<div class="section-sub">选择你心仪的类型,开启专属旅程</div>
</div>
<div class="categories-wrap reveal d1">
<div class="cat-scroll" id="catScroll"></div>
</div>
</div>
</section>
<!-- HOT GAMES -->
<section class="section" id="hot">
<div class="container">
<div class="section-head reveal">
<div>
<div class="section-title"><span class="bar"></span>热门推荐 <span class="en">/ TRENDING NOW</span></div>
</div>
<div class="section-sub">&#128293; 全平台热度飙升中</div>
</div>
<div class="games-grid cols-hot reveal d1" id="hotGrid"></div>
</div>
</section>
<!-- NEW GAMES -->
<section class="section" id="new">
<div class="container">
<div class="section-head reveal">
<div>
<div class="section-title"><span class="bar"></span>新游上架 <span class="en">/ FRESH DROPS</span></div>
</div>
<div class="section-sub">&#10024; 最新发布,抢先体验</div>
</div>
<div class="new-scroll reveal d1" id="newScroll"></div>
</div>
</section>
<!-- GAME LIBRARY -->
<section class="section" id="library">
<div class="container">
<div class="section-head reveal">
<div>
<div class="section-title"><span class="bar"></span>游戏库 <span class="en">/ GAME LIBRARY</span></div>
</div>
<div class="section-sub">&#128270; 浏览全部游戏,发现你的下一款心头好</div>
</div>
<div class="lib-toolbar reveal d1">
<div class="lib-search">
<span class="icon">&#128269;</span>
<input type="text" id="libSearch" placeholder="搜索游戏名称..." autocomplete="off">
<div class="spinner" id="libSpinner"></div>
</div>
<div class="sort-wrap" id="sortWrap">
<button class="sort-btn" id="sortBtn">
<span id="sortLabel">默认排序</span>
<span class="arrow">&#9660;</span>
</button>
<div class="sort-menu" id="sortMenu">
<button class="active" data-sort="">默认排序</button>
<button data-sort="rating">评分最高</button>
<button data-sort="plays">最多游玩</button>
<button data-sort="newest">最新上架</button>
</div>
</div>
<div class="lib-info">共 <b id="libTotal">0</b> 款游戏</div>
</div>
<div class="games-grid reveal d2" id="libGrid"></div>
<div class="pagination" id="pagination"></div>
</div>
</section>
<!-- FOOTER -->
<footer class="footer">
<div class="container">
<div class="footer-grid">
<div class="footer-brand">
<a href="#home" class="logo">
<span class="dot"></span>
<span class="t1">NEON</span> <span class="t2">ARCADE</span>
</a>
<p>次世代赛博朋克游戏平台,汇聚全球顶尖游戏作品。在霓虹与代码的交织中,重新定义你的游戏体验。</p>
</div>
<div class="footer-col">
<h4>平台</h4>
<a href="#home">首页</a>
<a href="#library">游戏库</a>
<a href="#hot">热门排行</a>
<a href="#new">新游速递</a>
</div>
<div class="footer-col">
<h4>支持</h4>
<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="footer-col">
<h4>关于</h4>
<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="footer-bottom">
<span>&copy; 2077 NEON ARCADE. ALL RIGHTS RESERVED. // 数据流加密 <span class="ic">&#9679;</span> 量子链路稳定</span>
<span>POWERED BY <span class="neon-cyan">CYBER_CORE v9.0</span></span>
</div>
</div>
</footer>
<!-- MODAL -->
<div class="modal-overlay" id="modalOverlay">
<div class="modal" id="modal"></div>
</div>
<!-- TOAST -->
<div class="toast" id="toast"><span>&#9888;</span><span id="toastMsg"></span></div>
<script>
/* ============================================================
NEON ARCADE — 应用逻辑
============================================================ */
/* ---------- 配置 ---------- */
const CONFIG = {
apiBase: '/api/v1/game', // 可改为完整域名
pageSize: 15,
useMock: true // 无后端时自动使用模拟数据(fetch 失败后回退)
};
/* ---------- 应用状态 ---------- */
const state = {
categories: [],
hotGames: [],
newGames: [],
library: { list: [], total: 0, page: 1, pageSize: CONFIG.pageSize },
filters: { category_id: '', keyword: '', sort: '' },
modalLoading: false
};
/* ============================================================
工具函数
============================================================ */
/* 大数格式化:890000 -> 89万 */
function formatNumber(n){
if(n == null) return '0';
n = Number(n);
if(n >= 100000000) return (n/100000000).toFixed(1).replace(/\.0$/,'') + '亿';
if(n >= 10000) return (n/10000).toFixed(1).replace(/\.0$/,'') + '万';
return n.toLocaleString();
}
/* 安全解析 JSON 字符串 */
function parseJSON(str, fallback){
if(Array.isArray(str)) return str;
if(typeof str !== 'string') return fallback || [];
try{ const v = JSON.parse(str); return Array.isArray(v) ? v : (fallback||[]); }
catch(e){ return fallback || []; }
}
/* 防抖 */
function debounce(fn, delay){
let t;
return function(...args){
clearTimeout(t);
t = setTimeout(()=>fn.apply(this,args), delay);
};
}
/* 星级渲染 */
function renderStars(rating){
rating = parseFloat(rating) || 0;
const full = Math.floor(rating);
const half = (rating - full) >= 0.5;
let html = '';
for(let i=0;i<5;i++){
if(i < full) html += '★';
else if(i === full && half) html += '★';
else html += '<span class="empty">★</span>';
}
return html;
}
/* Toast 提示 */
function showToast(msg){
const t = document.getElementById('toast');
document.getElementById('toastMsg').textContent = msg;
t.classList.add('show');
clearTimeout(t._timer);
t._timer = setTimeout(()=>t.classList.remove('show'), 3000);
}
/* 生成封面占位图(SVG data URI,避免外部依赖) */
function placeholderImage(game){
const palettes = [
['#ff0080','#8b00ff'],['#00f0ff','#0066ff'],['#8b00ff','#ff0080'],
['#00f0ff','#39ff14'],['#ff0080','#ffe600'],['#0066ff','#8b00ff'],
['#39ff14','#00f0ff'],['#ffe600','#ff0080']
];
const idx = (game.id||0) % palettes.length;
const [c1,c2] = palettes[idx];
const name = (game.name||'UNKNOWN').slice(0,8);
const seed = (game.id||1) * 137;
const shapes = Array.from({length:6},(_,i)=>{
const x = (seed * (i+1) * 7) % 100;
const y = (seed * (i+3) * 11) % 100;
const r = 8 + (seed * (i+1)) % 20;
return `<circle cx="${x}" cy="${y}" r="${r}" fill="white" opacity="0.06"/>`;
}).join('');
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="533" viewBox="0 0 400 533">
<defs><linearGradient id="g${game.id}" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="${c1}"/><stop offset="1" stop-color="${c2}"/>
</linearGradient></defs>
<rect width="400" height="533" fill="url(#g${game.id})" opacity="0.35"/>
<rect width="400" height="533" fill="#0a0a0f" opacity="0.55"/>
${shapes}
<text x="200" y="250" fill="white" font-size="42" font-weight="900" text-anchor="middle" font-family="sans-serif" opacity="0.9">${name}</text>
<text x="200" y="290" fill="${c1}" font-size="16" text-anchor="middle" font-family="monospace" opacity="0.7">NEON ARCADE</text>
</svg>`;
return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
}
function coverImage(game){
if(game.cover) return game.cover;
return placeholderImage(game);
}
function coverFallback(img, id){
img.onerror=null;
img.src=placeholderImage({id:id, name:img.alt||'UNKNOWN'});
}
/* 截图占位图 */
function shotPlaceholder(game, idx){
const colors = ['#00f0ff','#ff0080','#8b00ff','#39ff14','#ffe600'];
const c = colors[(idx+game.id) % colors.length];
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="680" height="380" viewBox="0 0 680 380">
<rect width="680" height="380" fill="#0e0e18"/>
<rect width="680" height="380" fill="${c}" opacity="0.12"/>
<line x1="0" y1="0" x2="680" y2="380" stroke="${c}" stroke-width="1" opacity="0.2"/>
<line x1="680" y1="0" x2="0" y2="380" stroke="${c}" stroke-width="1" opacity="0.2"/>
<text x="340" y="200" fill="${c}" font-size="20" text-anchor="middle" font-family="monospace" opacity="0.5">SCREENSHOT_${idx+1}</text>
</svg>`;
return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
}
function shotImage(game, idx){
const url = (game.screenshots && parseJSON(game.screenshots)[idx]);
if(url) return url;
return shotPlaceholder(game, idx);
}
function shotFallback(img, gameId, idx){
img.onerror=null;
img.src=shotPlaceholder({id:gameId}, idx);
}
/* ============================================================
模拟数据(后端不可用时自动回退)
============================================================ */
const MOCK = (function(){
const catData = [
{id:1,name:'动作冒险',icon:'&#9876;',sort:1},
{id:2,name:'角色扮演',icon:'&#129332;',sort:2},
{id:3,name:'射击竞技',icon:'&#127919;',sort:3},
{id:4,name:'策略经营',icon:'&#9823;',sort:4},
{id:5,name:'赛车竞速',icon:'&#127950;',sort:5},
{id:6,name:'体育模拟',icon:'&#9917;',sort:6},
{id:7,name:'恐怖生存',icon:'&#128123;',sort:7},
{id:8,name:'开放世界',icon:'&#127758;',sort:8},
{id:9,name:'独立精品',icon:'&#10024;',sort:9},
{id:10,name:'沙盒建造',icon:'&#129529;',sort:10}
];
const namePool = [
'霓虹深渊','赛博纪元','量子破碎','暗影协议','虚空回响','龙裔传说','星尘远征','机械觉醒',
'末日余晖','深渊行者','星际拓荒','幽冥战场','永恒之刃','幻境迷踪','霓虹追猎','代码洪流',
'银河战纪','暗网潜行','机甲风暴','灵魂收割','时空裂隙','深渊领主','霓虹之城','量子跃迁',
'废土余生','神域降临','暗夜行者','星辰变奏','机械之心','虚空使者','末日方舟','龙魂觉醒',
'幻影特工','星际迷航','深渊之眼','霓虹武士','量子迷宫','暗影猎手','银河传说','机甲纪元'
];
const descPool = [
'在霓虹笼罩的未来都市中,你将扮演一名边缘行者,穿梭于现实与虚拟的边界,揭开一个足以颠覆整个世界的惊天阴谋。',
'探索广袤无垠的开放世界,与巨龙缔结契约,书写属于你的史诗传奇。无缝地图、实时天气、动态生态,沉浸感拉满。',
'硬核射击体验,次世代画面引擎打造的电影级视觉效果。多人对战、战术配合,在硝烟弥漫的战场中证明你的实力。',
'建造你的赛博帝国,管理资源、研发科技、扩张版图。从街头小卒到地下之王,每一步决策都将改变命运。',
'生存恐怖杰作,在诡异的废弃空间站中寻找真相。有限的弹药、无尽的恐惧,你能活到黎明吗?',
'极速狂飙,感受引擎的轰鸣与风的速度。上百辆授权跑车、全球知名赛道,还原最真实的竞速快感。'
];
const tagPool = ['开放世界','多人在线','单人剧情','PVP','PVE','沙盒','硬核','多人合作','次世代','光线追踪','MOD支持','云存档','手柄支持','中文配音','免费游玩'];
const featPool = ['无缝开放世界','实时战斗系统','动态天气系统','物理破坏引擎','光线追踪渲染','多人在线合作','分支剧情选择','角色自定义系统','MOD 创意工坊','跨平台云存档'];
const devs = ['Cyber Dynamics','Neon Forge','Quantum Labs','Void Interactive','Stellar Games','Phantom Studio','Iron Byte','Hyperion Entertainment'];
const plats = ['PC / PS5 / Xbox','PC / PS5','PC / Switch','全平台','PC / Mobile','PS5 独占'];
function rand(arr){ return arr[Math.floor(Math.random()*arr.length)]; }
function randInt(a,b){ return Math.floor(Math.random()*(b-a+1))+a; }
function makeGame(id){
const cat = catData[id % catData.length];
const name = namePool[(id-1) % namePool.length] + (id > namePool.length ? ' ' + ['II','III','IV','V','ZERO','REBORN','RELOADED'][id % 7] : '');
const tags = [];
const tagCount = randInt(2,4);
const shuffled = [...tagPool].sort(()=>Math.random()-.5);
for(let i=0;i<tagCount;i++) tags.push(shuffled[i]);
const feats = [];
const fShuf = [...featPool].sort(()=>Math.random()-.5);
for(let i=0;i<randInt(3,5);i++) feats.push(fShuf[i]);
const screenshots = JSON.stringify([0,1,2].map(i=>shotImage({id,name},i)));
const isHot = id <= 12;
const isNew = id > 35;
return {
id, name,
description: rand(descPool),
category_id: cat.id,
category_name: cat.name,
cover: coverImage({id,name}),
developer: rand(devs),
publisher: rand(devs),
rating: (3.5 + Math.random()*1.5).toFixed(1),
plays: randInt(10000, 9800000),
downloads: randInt(5000, 5000000),
size: randInt(8,95) + '.' + randInt(0,9) + ' GB',
version: 'v' + randInt(1,5) + '.' + randInt(0,9) + '.' + randInt(0,9),
release_date: '20' + randInt(20,26) + '-' + String(randInt(1,12)).padStart(2,'0') + '-' + String(randInt(1,28)).padStart(2,'0'),
platform: rand(plats),
tags: JSON.stringify(tags),
screenshots,
features: JSON.stringify(feats),
is_hot: isHot,
is_new: isNew,
sort: id,
status: 1
};
}
const allGames = [];
for(let i=1;i<=48;i++) allGames.push(makeGame(i));
return {
categories: catData,
allGames,
home(){
return {
categories: catData,
hot: allGames.filter(g=>g.is_hot).slice(0,8),
new: allGames.filter(g=>g.is_new).slice(0,4)
};
},
hot(limit){ return allGames.filter(g=>g.is_hot).slice(0, limit||8); },
news(limit){ return allGames.filter(g=>g.is_new).slice(0, limit||4); },
query({category_id, keyword, sort, page, page_size}){
let list = [...allGames];
if(category_id) list = list.filter(g=>g.category_id == category_id);
if(keyword){
const kw = keyword.toLowerCase();
list = list.filter(g=> g.name.toLowerCase().includes(kw) || (g.description||'').toLowerCase().includes(kw));
}
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));
const total = list.length;
const ps = page_size || 15;
const pg = page || 1;
const start = (pg-1)*ps;
return { list: list.slice(start, start+ps), total, page: pg, page_size: ps };
},
detail(id){ return allGames.find(g=>g.id == id) || null; }
};
})();
/* ============================================================
API 层(fetch + mock 回退)
============================================================ */
async function apiGet(path, params){
let url = CONFIG.apiBase + path;
if(params){
const qs = new URLSearchParams();
Object.entries(params).forEach(([k,v])=>{ if(v!=='' && v!=null) qs.set(k,v); });
const s = qs.toString();
if(s) url += (url.includes('?')?'&':'?') + s;
}
try{
const res = await fetch(url, {headers:{'Accept':'application/json'}});
if(!res.ok) throw new Error('HTTP '+res.status);
const json = await res.json();
if(json.code !== 0) throw new Error(json.message || 'API error');
return json.data;
}catch(e){
// 回退到模拟数据
CONFIG.useMock = true;
throw e;
}
}
/* 带模拟回退的封装 */
async function fetchHome(){
try{ return await apiGet('/home'); }
catch(e){ return MOCK.home(); }
}
async function fetchCategories(){
try{ const d = await apiGet('/categories'); return Array.isArray(d)?d:(d.categories||[]); }
catch(e){ return MOCK.categories; }
}
async function fetchHot(limit){
try{ return await apiGet('/hot',{limit}); }
catch(e){ return MOCK.hot(limit); }
}
async function fetchNew(limit){
try{ return await apiGet('/new',{limit}); }
catch(e){ return MOCK.news(limit); }
}
async function fetchGames(params){
try{ return await apiGet('/games', params); }
catch(e){ return MOCK.query(params); }
}
async function fetchDetail(id){
try{ return await apiGet('/games/'+id); }
catch(e){ return MOCK.detail(id); }
}
/* ============================================================
渲染:分类
============================================================ */
function renderCategories(cats){
const wrap = document.getElementById('catScroll');
const all = [{id:'',name:'全部',icon:'&#127311;'}].concat(cats);
wrap.innerHTML = all.map(c=>`
<button class="cat-pill ${state.filters.category_id==c.id?'active':''}" data-cat="${c.id}">
<span class="ico">${c.icon||'&#127918;'}</span>${c.name}
</button>
`).join('');
wrap.querySelectorAll('.cat-pill').forEach(btn=>{
btn.addEventListener('click', ()=>{
const cid = btn.dataset.cat;
state.filters.category_id = cid;
state.library.page = 1;
wrap.querySelectorAll('.cat-pill').forEach(b=>b.classList.remove('active'));
btn.classList.add('active');
loadLibrary();
document.getElementById('library').scrollIntoView({behavior:'smooth'});
});
});
}
/* ============================================================
渲染:游戏卡片
============================================================ */
function gameCardHTML(game, opts){
opts = opts || {};
const tags = parseJSON(game.tags).slice(0,2);
const coverCls = opts.wide ? '' : '';
return `
<div class="game-card ${opts.wide?'new-card':''}" data-id="${game.id}">
<div class="cover">
${game.is_hot ? '<span class="badge hot">HOT</span>' : ''}
${game.is_new ? '<span class="badge new">NEW</span>' : ''}
<div class="rating-float">&#9733; ${parseFloat(game.rating||0).toFixed(1)}</div>
<img src="${coverImage(game)}" alt="${game.name}" loading="lazy" onerror="coverFallback(this, ${game.id})">
<div class="cover-grad"></div>
</div>
<div class="card-body">
<div class="card-title">${game.name}</div>
<div class="card-cat">${game.category_name||''}</div>
<div class="card-tags">${tags.map(t=>`<span class="mini-tag">${t}</span>`).join('')}</div>
<div class="card-foot">
<span class="plays"><span class="ic">&#9654;</span> ${formatNumber(game.plays)}</span>
${opts.release ? `<span class="release-date">${game.release_date||''}</span>` : `<span>${game.size||''}</span>`}
</div>
</div>
</div>
`;
}
function renderHot(games){
const grid = document.getElementById('hotGrid');
if(!games.length){ grid.innerHTML = emptyHTML('暂无热门游戏'); return; }
grid.innerHTML = games.map(g=>gameCardHTML(g)).join('');
bindCardClicks(grid);
}
function renderNew(games){
const wrap = document.getElementById('newScroll');
if(!games.length){ wrap.innerHTML = emptyHTML('暂无新游'); return; }
wrap.innerHTML = games.map(g=>gameCardHTML(g,{wide:true,release:true})).join('');
bindCardClicks(wrap);
}
function renderLibrary(data){
const grid = document.getElementById('libGrid');
document.getElementById('libTotal').textContent = formatNumber(data.total);
if(!data.list.length){ grid.innerHTML = emptyHTML('未找到匹配的游戏,换个关键词试试'); return; }
grid.innerHTML = data.list.map(g=>gameCardHTML(g)).join('');
bindCardClicks(grid);
renderPagination(data);
}
function emptyHTML(msg){
return `<div class="empty-state" style="grid-column:1/-1"><div class="ico">&#128270;</div><p>${msg}</p></div>`;
}
function bindCardClicks(container){
container.querySelectorAll('.game-card').forEach(card=>{
card.addEventListener('click', ()=>openModal(card.dataset.id));
});
}
/* ============================================================
渲染:分页
============================================================ */
function renderPagination(data){
const wrap = document.getElementById('pagination');
const {page, page_size, total} = data;
const totalPages = Math.ceil(total / page_size) || 1;
if(totalPages <= 1){ wrap.innerHTML = ''; return; }
let html = '';
// 上一页
html += `<button class="page-btn" ${page<=1?'disabled':''} data-page="${page-1}">&laquo;</button>`;
// 页码逻辑
const pages = [];
if(totalPages <= 7){
for(let i=1;i<=totalPages;i++) pages.push(i);
} else {
pages.push(1);
if(page > 4) pages.push('...');
const start = Math.max(2, page-1);
const end = Math.min(totalPages-1, page+1);
for(let i=start;i<=end;i++) pages.push(i);
if(page < totalPages-3) pages.push('...');
pages.push(totalPages);
}
pages.forEach(p=>{
if(p === '...') html += '<span class="page-ellipsis">···</span>';
else html += `<button class="page-btn ${p===page?'active':''}" data-page="${p}">${p}</button>`;
});
// 下一页
html += `<button class="page-btn" ${page>=totalPages?'disabled':''} data-page="${page+1}">&raquo;</button>`;
wrap.innerHTML = html;
wrap.querySelectorAll('.page-btn').forEach(btn=>{
btn.addEventListener('click', ()=>{
if(btn.disabled) return;
const p = parseInt(btn.dataset.page);
if(p === state.library.page) return;
state.library.page = p;
loadLibrary(true);
});
});
}
/* ============================================================
骨架屏
============================================================ */
function skeletonGrid(count, wide){
const items = [];
for(let i=0;i<count;i++){
if(wide){
items.push(`<div class="skel-card new-card" style="width:300px">
<div class="skel-cover" style="aspect-ratio:16/10"></div>
<div class="skel-line mid"></div><div class="skel-line short"></div>
</div>`);
} else {
items.push(`<div class="skel-card">
<div class="skel-cover"></div>
<div class="skel-line mid"></div><div class="skel-line short"></div>
</div>`);
}
}
return items.join('');
}
function skeletonPagination(){
let html = '';
for(let i=0;i<5;i++) html += '<div class="skel-line" style="width:40px;height:40px;border-radius:8px"></div>';
return html;
}
/* ============================================================
数据加载
============================================================ */
async function loadHome(){
// 分类
document.getElementById('catScroll').innerHTML = '<div style="padding:10px;color:var(--text-mute)">加载分类中...</div>';
// 热门骨架
document.getElementById('hotGrid').innerHTML = skeletonGrid(8);
// 新游骨架
document.getElementById('newScroll').innerHTML = skeletonGrid(4, true);
const [cats, home] = await Promise.all([fetchCategories(), fetchHome()]);
state.categories = cats || home.categories || [];
renderCategories(state.categories);
state.hotGames = home.hot || [];
state.newGames = home.new || [];
renderHot(state.hotGames);
renderNew(state.newGames);
observeReveal();
}
let libLoading = false;
async function loadLibrary(scrollTop){
if(libLoading) return;
libLoading = true;
const grid = document.getElementById('libGrid');
grid.innerHTML = skeletonGrid(state.library.pageSize);
document.getElementById('pagination').innerHTML = skeletonPagination();
try{
const data = await fetchGames({
category_id: state.filters.category_id,
keyword: state.filters.keyword,
sort: state.filters.sort,
page: state.library.page,
page_size: state.library.pageSize
});
state.library.list = data.list || [];
state.library.total = data.total || 0;
state.library.page = data.page || state.library.page;
state.library.pageSize = data.page_size || state.library.pageSize;
renderLibrary(state.library);
}catch(e){
grid.innerHTML = emptyHTML('加载失败,请稍后重试');
showToast('游戏库加载失败');
} finally {
libLoading = false;
if(scrollTop){
document.getElementById('library').scrollIntoView({behavior:'smooth'});
}
}
}
/* ============================================================
模态框
============================================================ */
async function openModal(id){
const overlay = document.getElementById('modalOverlay');
const modal = document.getElementById('modal');
overlay.classList.add('open');
document.body.style.overflow = 'hidden';
// 骨架
modal.innerHTML = `
<div class="modal-skel">
<div class="skel-cover"></div>
<div class="skel-line title"></div>
<div class="skel-line"></div>
<div class="skel-line mid"></div>
<div class="skel-line"></div>
<div class="skel-line short"></div>
</div>`;
try{
const game = await fetchDetail(id);
if(!game){ modal.innerHTML = '<div class="empty-state" style="padding:60px">游戏信息不存在</div>'; return; }
renderModal(game);
}catch(e){
modal.innerHTML = '<div class="empty-state" style="padding:60px">加载失败,请关闭后重试</div>';
}
}
function renderModal(game){
const modal = document.getElementById('modal');
const tags = parseJSON(game.tags);
const features = parseJSON(game.features);
const shots = parseJSON(game.screenshots);
const rating = parseFloat(game.rating)||0;
modal.innerHTML = `
<button class="modal-close" onclick="closeModal()">&times;</button>
<div class="modal-hero">
<img src="${coverImage(game)}" alt="${game.name}" onerror="coverFallback(this, ${game.id})">
<div class="modal-hero-grad"></div>
<div class="modal-hero-info">
<div class="modal-cat-tag">${game.category_name||''} // ID:${game.id}</div>
<h2 class="modal-title">${game.name}</h2>
<div class="modal-rating-row">
<span class="stars">${renderStars(rating)}</span>
<span class="rating-num">${rating.toFixed(1)}</span>
${game.is_hot?'<span class="badge hot">HOT</span>':''}
${game.is_new?'<span class="badge new">NEW</span>':''}
</div>
</div>
</div>
<div class="modal-body">
<p class="modal-desc">${game.description||'暂无简介'}</p>
<div class="modal-section-label">标签 / TAGS</div>
<div class="modal-tags">
${tags.length ? tags.map(t=>`<span class="modal-tag">${t}</span>`).join('') : '<span style="color:var(--text-mute)">暂无标签</span>'}
</div>
<div class="modal-section-label">核心特色 / FEATURES</div>
<ul class="modal-features">
${features.length ? features.map(f=>`<li>${f}</li>`).join('') : '<li style="color:var(--text-mute)">暂无特色信息</li>'}
</ul>
<div class="modal-section-label">数据统计 / STATS</div>
<div class="modal-stats">
<div class="stat-box"><div class="v">${formatNumber(game.plays)}</div><div class="l">游玩次数</div></div>
<div class="stat-box"><div class="v">${formatNumber(game.downloads)}</div><div class="l">下载量</div></div>
<div class="stat-box"><div class="v">${rating.toFixed(1)}</div><div class="l">评分</div></div>
<div class="stat-box"><div class="v">${game.size||'-'}</div><div class="l">游戏大小</div></div>
</div>
<div class="modal-section-label">详细信息 / DETAILS</div>
<div class="modal-meta">
<div class="meta-item"><span class="k">开发商</span><span class="v">${game.developer||'-'}</span></div>
<div class="meta-item"><span class="k">发行商</span><span class="v">${game.publisher||'-'}</span></div>
<div class="meta-item"><span class="k">平台</span><span class="v">${game.platform||'-'}</span></div>
<div class="meta-item"><span class="k">版本</span><span class="v">${game.version||'-'}</span></div>
<div class="meta-item"><span class="k">发行日期</span><span class="v">${game.release_date||'-'}</span></div>
<div class="meta-item"><span class="k">游戏大小</span><span class="v">${game.size||'-'}</span></div>
</div>
${shots.length ? `
<div class="modal-section-label">游戏截图 / SCREENSHOTS</div>
<div class="modal-shots">
${shots.map((s,i)=>`<img src="${shotImage(game,i)}" alt="截图${i+1}" loading="lazy" onerror="shotFallback(this, ${game.id}, ${i})">`).join('')}
</div>` : ''}
<div class="modal-actions">
<button class="btn btn-primary" onclick="showToast('正在启动 ${game.name.replace(/'/g,'')}... &#9654;')">开始游戏</button>
<button class="btn" onclick="showToast('已加入收藏夹')">收藏</button>
<button class="btn btn-magenta" onclick="showToast('正在下载 ${game.size||''}...')">下载</button>
</div>
</div>
`;
}
function closeModal(){
document.getElementById('modalOverlay').classList.remove('open');
document.body.style.overflow = '';
}
window.closeModal = closeModal;
window.showToast = showToast;
/* ============================================================
交互事件
============================================================ */
function initEvents(){
// 滚动头部样式
window.addEventListener('scroll', ()=>{
const h = document.getElementById('header');
if(window.scrollY > 40) h.classList.add('scrolled');
else h.classList.remove('scrolled');
});
// 移动端菜单
document.getElementById('mobileToggle').addEventListener('click', ()=>{
document.getElementById('nav').classList.toggle('mobile-open');
});
// 导航点击平滑滚动 + 高亮
document.querySelectorAll('[data-scroll]').forEach(a=>{
a.addEventListener('click', e=>{
e.preventDefault();
document.getElementById('nav').classList.remove('mobile-open');
document.querySelectorAll('.nav a').forEach(n=>n.classList.remove('active'));
a.classList.add('active');
const target = document.getElementById(a.dataset.scroll);
if(target) target.scrollIntoView({behavior:'smooth'});
});
});
// 全局搜索 -> 跳转游戏库并搜索
const globalSearch = document.getElementById('globalSearch');
const globalClear = document.getElementById('globalSearchClear');
globalSearch.addEventListener('input', debounce(()=>{
const kw = globalSearch.value.trim();
state.filters.keyword = kw;
state.library.page = 1;
document.getElementById('libSearch').value = kw;
if(kw){
loadLibrary();
document.getElementById('library').scrollIntoView({behavior:'smooth'});
}
globalClear.style.display = kw ? 'block' : 'none';
}, 400));
globalClear.addEventListener('click', ()=>{
globalSearch.value = '';
globalClear.style.display = 'none';
state.filters.keyword = '';
state.library.page = 1;
document.getElementById('libSearch').value = '';
loadLibrary();
});
// 游戏库搜索(带 spinner)
const libSearch = document.getElementById('libSearch');
const libSpinner = document.getElementById('libSpinner');
libSearch.addEventListener('input', debounce(()=>{
libSpinner.classList.add('show');
state.filters.keyword = libSearch.value.trim();
state.library.page = 1;
// 同步全局搜索
document.getElementById('globalSearch').value = state.filters.keyword;
document.getElementById('globalSearchClear').style.display = state.filters.keyword ? 'block':'none';
loadLibrary().then(()=>libSpinner.classList.remove('show'));
}, 450));
// 排序下拉
const sortWrap = document.getElementById('sortWrap');
const sortBtn = document.getElementById('sortBtn');
const sortMenu = document.getElementById('sortMenu');
sortBtn.addEventListener('click', e=>{
e.stopPropagation();
sortWrap.classList.toggle('open');
});
document.addEventListener('click', e=>{
if(!sortWrap.contains(e.target)) sortWrap.classList.remove('open');
});
sortMenu.querySelectorAll('button').forEach(btn=>{
btn.addEventListener('click', ()=>{
sortMenu.querySelectorAll('button').forEach(b=>b.classList.remove('active'));
btn.classList.add('active');
state.filters.sort = btn.dataset.sort;
state.library.page = 1;
document.getElementById('sortLabel').textContent = btn.textContent;
sortWrap.classList.remove('open');
loadLibrary();
});
});
// 模态框关闭
document.getElementById('modalOverlay').addEventListener('click', e=>{
if(e.target.id === 'modalOverlay') closeModal();
});
document.addEventListener('keydown', e=>{
if(e.key === 'Escape') closeModal();
});
}
/* ============================================================
滚动揭示动画 (IntersectionObserver)
============================================================ */
let revealObserver;
function observeReveal(){
if(!revealObserver){
revealObserver = new IntersectionObserver((entries)=>{
entries.forEach(entry=>{
if(entry.isIntersecting){
entry.target.classList.add('in');
revealObserver.unobserve(entry.target);
}
});
}, {threshold:0.08, rootMargin:'0px 0px -50px 0px'});
}
document.querySelectorAll('.reveal:not(.in)').forEach(el=>revealObserver.observe(el));
}
/* ============================================================
数字滚动动画
============================================================ */
function animateCount(el){
const target = parseInt(el.dataset.count);
const suffix = el.dataset.suffix || '';
const duration = 1800;
const start = performance.now();
function tick(now){
const p = Math.min((now - start)/duration, 1);
const eased = 1 - Math.pow(1-p, 3);
let val = Math.floor(target * eased);
el.textContent = formatNumber(val) + suffix;
if(p < 1) requestAnimationFrame(tick);
else el.textContent = formatNumber(target) + suffix;
}
requestAnimationFrame(tick);
}
function initCountUp(){
const obs = new IntersectionObserver((entries)=>{
entries.forEach(entry=>{
if(entry.isIntersecting){
animateCount(entry.target);
obs.unobserve(entry.target);
}
});
}, {threshold:0.5});
document.querySelectorAll('[data-count]').forEach(el=>obs.observe(el));
}
/* ============================================================
初始化
============================================================ */
async function init(){
initEvents();
observeReveal();
initCountUp();
await loadHome();
await loadLibrary();
}
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>