Files
web_template/web/dist/game1/index.html
Geliebte 10979351f8 feat: 默认语言改为英语 + 导航页添加i18n三语支持
- 导航页(index.html)添加完整i18n引擎,支持zh-CN/zh-TW/en
- 导航页添加语言切换按钮(EN/中/繁)
- 所有20个前端模板默认语言从zh-CN改为en
- 导航页SEO meta标签改为英文
2026-09-09 19:48:45 +08:00

2064 lines
88 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}
/* ============================================================
语言切换器
============================================================ */
.lang-switcher{position:relative;margin-left:8px}
.lang-btn{
display:flex;align-items:center;gap:6px;padding:8px 14px;border-radius:var(--radius-sm);
font-size:13px;font-weight:600;color:var(--text-dim);cursor:pointer;
background:rgba(0,0,0,.3);border:1px solid var(--border);
transition:all .25s;
}
.lang-btn:hover{color:var(--cyan);background:rgba(0,240,255,.06);border-color:var(--cyan);box-shadow:0 0 10px rgba(0,240,255,.2)}
.lang-btn svg{width:16px;height:16px}
.lang-dropdown{
position:absolute;top:calc(100% + 8px);right:0;min-width:160px;z-index:200;
background:rgba(10,10,20,.96);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
border:1px solid var(--border);border-radius:var(--radius-sm);
box-shadow:0 12px 40px rgba(0,0,0,.6);
opacity:0;transform:translateY(-8px);pointer-events:none;
transition:all .25s;
}
.lang-dropdown.open{opacity:1;transform:none;pointer-events:auto}
.lang-option{
display:flex;align-items:center;gap:10px;padding:12px 16px;
font-size:14px;color:var(--text-dim);cursor:pointer;transition:all .2s;
}
.lang-option:hover{background:rgba(0,240,255,.08);color:var(--cyan)}
.lang-option.active{color:var(--cyan);font-weight:600}
.lang-option .check{width:16px;height:16px;opacity:0;transition:opacity .2s}
.lang-option.active .check{opacity:1}
/* ============================================================
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" data-i18n="nav_home"></a>
<a href="#library" data-scroll="library" data-i18n="nav_library"></a>
<a href="#hot" data-scroll="hot" data-i18n="nav_hot"></a>
<a href="#new" data-scroll="new" data-i18n="nav_new"></a>
</nav>
<div class="header-search">
<span class="icon">&#128269;</span>
<input type="text" id="globalSearch" data-i18n-placeholder="search_placeholder" autocomplete="off">
<span class="clear-btn" id="globalSearchClear">&times;</span>
</div>
<div class="lang-switcher" id="langSwitcher">
<button class="lang-btn" id="langBtn" onclick="toggleLangDropdown()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15 15 0 0 1 0 20 15 15 0 0 1 0-20z"/></svg>
<span id="langLabel">中文</span>
</button>
<div class="lang-dropdown" id="langDropdown">
<div class="lang-option active" data-lang="zh-CN" onclick="switchLang('zh-CN')">
<svg class="check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
中文
</div>
<div class="lang-option" data-lang="zh-TW" onclick="switchLang('zh-TW')">
<svg class="check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
繁體中文
</div>
<div class="lang-option" data-lang="en" onclick="switchLang('en')">
<svg class="check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
English
</div>
</div>
</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> <span data-i18n="hero_tag"></span></div>
<h1>
<span class="l1 glitch" data-i18n="hero_title_line1" data-text=""></span><br>
<span class="l2 glitch" data-i18n="hero_title_line2" data-text=""></span>
</h1>
<p data-i18n="hero_desc"></p>
<div class="hero-actions">
<button class="btn btn-primary" onclick="document.getElementById('library').scrollIntoView({behavior:'smooth'})" data-i18n="hero_cta_explore"></button>
<button class="btn btn-magenta" onclick="document.getElementById('hot').scrollIntoView({behavior:'smooth'})" data-i18n="hero_cta_hot"></button>
</div>
<div class="hero-stats">
<div class="hero-stat"><div class="num" data-count="12860">0</div><div class="label" data-i18n="stats_games"></div></div>
<div class="hero-stat"><div class="num" data-count="5800000">0</div><div class="label" data-i18n="stats_players"></div></div>
<div class="hero-stat"><div class="num" data-count="248">0</div><div class="label" data-i18n="stats_categories"></div></div>
<div class="hero-stat"><div class="num" data-count="99" data-suffix=".9%">0</div><div class="label" data-i18n="stats_rating"></div></div>
</div>
</div>
<div class="scroll-hint" data-i18n="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 data-i18n="section_categories"></span> <span class="en">/ CATEGORIES</span></div>
</div>
<div class="section-sub" data-i18n="section_categories_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 data-i18n="section_hot"></span> <span class="en">/ TRENDING NOW</span></div>
</div>
<div class="section-sub">&#128293; <span data-i18n="section_hot_sub"></span></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 data-i18n="section_new"></span> <span class="en">/ FRESH DROPS</span></div>
</div>
<div class="section-sub">&#10024; <span data-i18n="section_new_sub"></span></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 data-i18n="section_library"></span> <span class="en">/ GAME LIBRARY</span></div>
</div>
<div class="section-sub">&#128270; <span data-i18n="section_library_sub"></span></div>
</div>
<div class="lib-toolbar reveal d1">
<div class="lib-search">
<span class="icon">&#128269;</span>
<input type="text" id="libSearch" data-i18n-placeholder="lib_search_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" data-i18n="sort_default"></span>
<span class="arrow">&#9660;</span>
</button>
<div class="sort-menu" id="sortMenu">
<button class="active" data-sort="" data-i18n="sort_default"></button>
<button data-sort="rating" data-i18n="sort_rating"></button>
<button data-sort="plays" data-i18n="sort_plays"></button>
<button data-sort="newest" data-i18n="sort_newest"></button>
</div>
</div>
<div class="lib-info"><span data-i18n="lib_total_prefix"></span> <b id="libTotal">0</b> <span data-i18n="lib_total_suffix"></span></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 data-i18n="footer_brand_desc"></p>
</div>
<div class="footer-col">
<h4 data-i18n="footer_platform"></h4>
<a href="#home" data-i18n="nav_home"></a>
<a href="#library" data-i18n="nav_library"></a>
<a href="#hot" data-i18n="footer_hot_ranking"></a>
<a href="#new" data-i18n="footer_new_delivery"></a>
</div>
<div class="footer-col">
<h4 data-i18n="footer_support"></h4>
<a href="javascript:void(0)" data-i18n="footer_help_center"></a>
<a href="javascript:void(0)" data-i18n="footer_faq"></a>
<a href="javascript:void(0)" data-i18n="footer_feedback"></a>
<a href="javascript:void(0)" data-i18n="footer_dev_join"></a>
</div>
<div class="footer-col">
<h4 data-i18n="footer_about"></h4>
<a href="javascript:void(0)" data-i18n="footer_about_us"></a>
<a href="javascript:void(0)" data-i18n="footer_terms"></a>
<a href="javascript:void(0)" data-i18n="footer_privacy"></a>
<a href="javascript:void(0)" data-i18n="footer_contact"></a>
</div>
</div>
<div class="footer-bottom">
<span>&copy; 2077 NEON ARCADE. ALL RIGHTS RESERVED. // <span data-i18n="footer_encrypted"></span> <span class="ic">&#9679;</span> <span data-i18n="footer_quantum_stable"></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 — 应用逻辑
============================================================ */
/* ============================================================
I18N — 国际化
============================================================ */
const I18N_LOCALES = {
'zh-CN': {
nav_home: '首页',
nav_library: '游戏库',
nav_hot: '热门',
nav_new: '新游',
search_placeholder: '搜索游戏...',
hero_tag: 'NEXT-GEN PLATFORM // 2077 EDITION',
hero_title_line1: '次世代',
hero_title_line2: '游戏平台',
hero_desc: '潜入霓虹深渊,探索数万款次世代游戏。从开放世界到硬核竞技,从独立精品到 3A 巨制 —— 你的赛博冒险,由此启程。',
hero_cta_explore: '立即探索 ▶',
hero_cta_hot: '查看热门',
stats_games: '游戏数量',
stats_players: '注册玩家',
stats_categories: '游戏分类',
stats_rating: '好评率',
scroll_hint: 'SCROLL',
section_categories: '游戏分类',
section_categories_sub: '选择你心仪的类型,开启专属旅程',
section_hot: '热门推荐',
section_hot_sub: '全平台热度飙升中',
section_new: '新游上架',
section_new_sub: '最新发布,抢先体验',
section_library: '游戏库',
section_library_sub: '浏览全部游戏,发现你的下一款心头好',
lib_search_placeholder: '搜索游戏名称...',
sort_default: '默认排序',
sort_rating: '评分最高',
sort_plays: '最多游玩',
sort_newest: '最新上架',
lib_total_prefix: '共',
lib_total_suffix: '款游戏',
cat_all: '全部',
empty_hot: '暂无热门游戏',
empty_new: '暂无新游',
empty_no_match: '未找到匹配的游戏,换个关键词试试',
empty_load_fail: '加载失败,请稍后重试',
toast_load_fail: '游戏库加载失败',
loading_categories: '加载分类中...',
modal_tags_label: '标签 / TAGS',
modal_tags_empty: '暂无标签',
modal_features_label: '核心特色 / FEATURES',
modal_features_empty: '暂无特色信息',
modal_stats_label: '数据统计 / STATS',
modal_stat_plays: '游玩次数',
modal_stat_downloads: '下载量',
modal_stat_rating: '评分',
modal_stat_size: '游戏大小',
modal_details_label: '详细信息 / DETAILS',
modal_dev: '开发商',
modal_pub: '发行商',
modal_platform: '平台',
modal_version: '版本',
modal_release: '发行日期',
modal_size: '游戏大小',
modal_screenshots_label: '游戏截图 / SCREENSHOTS',
modal_screenshot_alt: '截图',
modal_desc_empty: '暂无简介',
modal_not_found: '游戏信息不存在',
modal_load_error: '加载失败,请关闭后重试',
modal_btn_play: '开始游戏',
modal_btn_fav: '收藏',
modal_btn_download: '下载',
toast_launching: '正在启动',
toast_fav_added: '已加入收藏夹',
toast_downloading: '正在下载',
footer_brand_desc: '次世代赛博朋克游戏平台,汇聚全球顶尖游戏作品。在霓虹与代码的交织中,重新定义你的游戏体验。',
footer_platform: '平台',
footer_hot_ranking: '热门排行',
footer_new_delivery: '新游速递',
footer_support: '支持',
footer_help_center: '帮助中心',
footer_faq: '常见问题',
footer_feedback: '反馈建议',
footer_dev_join: '开发者入驻',
footer_about: '关于',
footer_about_us: '关于我们',
footer_terms: '用户协议',
footer_privacy: '隐私政策',
footer_contact: '联系我们',
footer_encrypted: '数据流加密',
footer_quantum_stable: '量子链路稳定',
mock_action_adventure: '动作冒险',
mock_rpg: '角色扮演',
mock_shooter: '射击竞技',
mock_strategy: '策略经营',
mock_racing: '赛车竞速',
mock_sports: '体育模拟',
mock_horror: '恐怖生存',
mock_open_world: '开放世界',
mock_indie: '独立精品',
mock_sandbox: '沙盒建造'
},
'en': {
nav_home: 'Home',
nav_library: 'Library',
nav_hot: 'Trending',
nav_new: 'New',
search_placeholder: 'Search games...',
hero_tag: 'NEXT-GEN PLATFORM // 2077 EDITION',
hero_title_line1: 'Next-Gen',
hero_title_line2: 'Game Platform',
hero_desc: 'Dive into the neon abyss and explore tens of thousands of next-gen games. From open worlds to hardcore competition, from indie gems to AAA blockbusters — your cyber adventure starts here.',
hero_cta_explore: 'Explore Now ▶',
hero_cta_hot: 'View Trending',
stats_games: 'Total Games',
stats_players: 'Registered Players',
stats_categories: 'Categories',
stats_rating: 'Positive Rating',
scroll_hint: 'SCROLL',
section_categories: 'Game Categories',
section_categories_sub: 'Pick your favorite genre and start your journey',
section_hot: 'Trending Now',
section_hot_sub: 'Hot picks across the platform',
section_new: 'Fresh Drops',
section_new_sub: 'Latest releases, get in early',
section_library: 'Game Library',
section_library_sub: 'Browse all games and discover your next favorite',
lib_search_placeholder: 'Search game title...',
sort_default: 'Default',
sort_rating: 'Top Rated',
sort_plays: 'Most Played',
sort_newest: 'Newest',
lib_total_prefix: 'Total',
lib_total_suffix: 'games',
cat_all: 'All',
empty_hot: 'No trending games yet',
empty_new: 'No new games yet',
empty_no_match: 'No matching games found, try different keywords',
empty_load_fail: 'Failed to load, please try again later',
toast_load_fail: 'Failed to load game library',
loading_categories: 'Loading categories...',
modal_tags_label: 'TAGS',
modal_tags_empty: 'No tags available',
modal_features_label: 'FEATURES',
modal_features_empty: 'No feature information available',
modal_stats_label: 'STATS',
modal_stat_plays: 'Plays',
modal_stat_downloads: 'Downloads',
modal_stat_rating: 'Rating',
modal_stat_size: 'Size',
modal_details_label: 'DETAILS',
modal_dev: 'Developer',
modal_pub: 'Publisher',
modal_platform: 'Platform',
modal_version: 'Version',
modal_release: 'Release Date',
modal_size: 'Size',
modal_screenshots_label: 'SCREENSHOTS',
modal_screenshot_alt: 'Screenshot',
modal_desc_empty: 'No description available',
modal_not_found: 'Game information not found',
modal_load_error: 'Failed to load, please close and try again',
modal_btn_play: 'Play Now',
modal_btn_fav: 'Favorite',
modal_btn_download: 'Download',
toast_launching: 'Launching',
toast_fav_added: 'Added to favorites',
toast_downloading: 'Downloading',
footer_brand_desc: 'A next-gen cyberpunk game platform bringing together top games worldwide. Redefine your gaming experience at the intersection of neon and code.',
footer_platform: 'Platform',
footer_hot_ranking: 'Top Charts',
footer_new_delivery: 'New Releases',
footer_support: 'Support',
footer_help_center: 'Help Center',
footer_faq: 'FAQ',
footer_feedback: 'Feedback',
footer_dev_join: 'Developer Portal',
footer_about: 'About',
footer_about_us: 'About Us',
footer_terms: 'Terms of Service',
footer_privacy: 'Privacy Policy',
footer_contact: 'Contact Us',
footer_encrypted: 'Data Encrypted',
footer_quantum_stable: 'Quantum Link Stable',
mock_action_adventure: 'Action Adventure',
mock_rpg: 'RPG',
mock_shooter: 'Shooter',
mock_strategy: 'Strategy',
mock_racing: 'Racing',
mock_sports: 'Sports',
mock_horror: 'Horror Survival',
mock_open_world: 'Open World',
mock_indie: 'Indie',
mock_sandbox: 'Sandbox'
},
'zh-TW': {
nav_home: '首頁',
nav_library: '遊戲庫',
nav_hot: '熱門',
nav_new: '新遊',
search_placeholder: '搜尋遊戲...',
hero_tag: 'NEXT-GEN PLATFORM // 2077 EDITION',
hero_title_line1: '次世代',
hero_title_line2: '遊戲平台',
hero_desc: '潛入霓虹深淵,探索數萬款次世代遊戲。從開放世界到硬核競技,從獨立精品到 3A 巨製 —— 你的賽博冒險,由此啟程。',
hero_cta_explore: '立即探索 ▶',
hero_cta_hot: '查看熱門',
stats_games: '遊戲數量',
stats_players: '註冊玩家',
stats_categories: '遊戲分類',
stats_rating: '好評率',
scroll_hint: 'SCROLL',
section_categories: '遊戲分類',
section_categories_sub: '選擇你心儀的類型,開啟專屬旅程',
section_hot: '熱門推薦',
section_hot_sub: '全平台熱度飆升中',
section_new: '新遊上架',
section_new_sub: '最新發布,搶先體驗',
section_library: '遊戲庫',
section_library_sub: '瀏覽全部遊戲,發現你的下一款心頭好',
lib_search_placeholder: '搜尋遊戲名稱...',
sort_default: '預設排序',
sort_rating: '評分最高',
sort_plays: '最多遊玩',
sort_newest: '最新上架',
lib_total_prefix: '共',
lib_total_suffix: '款遊戲',
cat_all: '全部',
empty_hot: '暫無熱門遊戲',
empty_new: '暫無新遊',
empty_no_match: '未找到匹配的遊戲,換個關鍵詞試試',
empty_load_fail: '載入失敗,請稍後重試',
toast_load_fail: '遊戲庫載入失敗',
loading_categories: '載入分類中...',
modal_tags_label: '標籤 / TAGS',
modal_tags_empty: '暫無標籤',
modal_features_label: '核心特色 / FEATURES',
modal_features_empty: '暫無特色資訊',
modal_stats_label: '數據統計 / STATS',
modal_stat_plays: '遊玩次數',
modal_stat_downloads: '下載量',
modal_stat_rating: '評分',
modal_stat_size: '遊戲大小',
modal_details_label: '詳細資訊 / DETAILS',
modal_dev: '開發商',
modal_pub: '發行商',
modal_platform: '平台',
modal_version: '版本',
modal_release: '發行日期',
modal_size: '遊戲大小',
modal_screenshots_label: '遊戲截圖 / SCREENSHOTS',
modal_screenshot_alt: '截圖',
modal_desc_empty: '暫無簡介',
modal_not_found: '遊戲資訊不存在',
modal_load_error: '載入失敗,請關閉後重試',
modal_btn_play: '開始遊戲',
modal_btn_fav: '收藏',
modal_btn_download: '下載',
toast_launching: '正在啟動',
toast_fav_added: '已加入收藏夾',
toast_downloading: '正在下載',
footer_brand_desc: '次世代賽博朋克遊戲平台,匯聚全球頂尖遊戲作品。在霓虹與程式碼的交織中,重新定義你的遊戲體驗。',
footer_platform: '平台',
footer_hot_ranking: '熱門排行',
footer_new_delivery: '新遊速遞',
footer_support: '支援',
footer_help_center: '幫助中心',
footer_faq: '常見問題',
footer_feedback: '回饋建議',
footer_dev_join: '開發者入驻',
footer_about: '關於',
footer_about_us: '關於我們',
footer_terms: '使用者協議',
footer_privacy: '隱私政策',
footer_contact: '聯絡我們',
footer_encrypted: '資料流加密',
footer_quantum_stable: '量子鏈路穩定',
mock_action_adventure: '動作冒險',
mock_rpg: '角色扮演',
mock_shooter: '射擊競技',
mock_strategy: '策略經營',
mock_racing: '賽車競速',
mock_sports: '體育模擬',
mock_horror: '恐怖生存',
mock_open_world: '開放世界',
mock_indie: '獨立精品',
mock_sandbox: '沙盒建造'
}
};
const LANG_LABELS = {'zh-CN':'中文','en':'EN','zh-TW':'繁中'};
let currentLang = (function(){
try{
var u=new URLSearchParams(location.search).get('lang');
if(u&&I18N_LOCALES[u]) return u;
}catch(e){}
try{var s=localStorage.getItem('lang');if(s&&I18N_LOCALES[s]) return s;}catch(e){}
var b=navigator.language||navigator.userLanguage||'';
if(b.startsWith('zh-TW')||b.startsWith('zh-Hant')) return 'zh-TW';
if(b.startsWith('zh')) return 'en';
return 'en';
})();
function t(key){return (I18N_LOCALES[currentLang]&&I18N_LOCALES[currentLang][key])||(I18N_LOCALES['zh-CN'][key])||key;}
function applyI18n(){
document.querySelectorAll('[data-i18n]').forEach(function(el){
var key=el.getAttribute('data-i18n');
el.textContent=t(key);
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(function(el){
var key=el.getAttribute('data-i18n-placeholder');
el.placeholder=t(key);
});
document.documentElement.lang=currentLang==='zh-TW'?'zh-TW':currentLang==='zh-CN'?'zh-CN':'en';
var lbl=document.getElementById('langLabel');
if(lbl) lbl.textContent=LANG_LABELS[currentLang]||currentLang;
document.querySelectorAll('.lang-option').forEach(function(opt){
opt.classList.toggle('active',opt.getAttribute('data-lang')===currentLang);
});
document.title='NEON ARCADE · '+t('hero_title_line2');
}
function switchLang(lang){
if(!I18N_LOCALES[lang]) return;
currentLang=lang;
try{localStorage.setItem('lang',lang);}catch(e){}
applyI18n();
if(typeof onLangChange==='function') onLangChange(lang);
document.getElementById('langDropdown').classList.remove('open');
}
window.switchLang = switchLang;
function toggleLangDropdown(){
document.getElementById('langDropdown').classList.toggle('open');
}
window.toggleLangDropdown = toggleLangDropdown;
document.addEventListener('click',function(e){
var sw=document.getElementById('langSwitcher');
if(sw&&!sw.contains(e.target)) document.getElementById('langDropdown').classList.remove('open');
});
let onLangChange = null;
/* ============================================================
配置
============================================================ */
const CONFIG = {
apiBase: '/api/v1/game',
pageSize: 15,
useMock: true
};
/* ============================================================
应用状态
============================================================ */
const state = {
categories: [],
hotGames: [],
newGames: [],
library: { list: [], total: 0, page: 1, pageSize: CONFIG.pageSize },
filters: { category_id: '', keyword: '', sort: '' },
modalLoading: false
};
/* ============================================================
工具函数
============================================================ */
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();
}
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;
}
function showToast(msg){
const tEl = document.getElementById('toast');
document.getElementById('toastMsg').textContent = msg;
tEl.classList.add('show');
clearTimeout(tEl._timer);
tEl._timer = setTimeout(()=>tEl.classList.remove('show'), 3000);
}
window.showToast = showToast;
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,nameKey:'mock_action_adventure',icon:'&#9876;',sort:1},
{id:2,nameKey:'mock_rpg',icon:'&#129332;',sort:2},
{id:3,nameKey:'mock_shooter',icon:'&#127919;',sort:3},
{id:4,nameKey:'mock_strategy',icon:'&#9823;',sort:4},
{id:5,nameKey:'mock_racing',icon:'&#127950;',sort:5},
{id:6,nameKey:'mock_sports',icon:'&#9917;',sort:6},
{id:7,nameKey:'mock_horror',icon:'&#128123;',sort:7},
{id:8,nameKey:'mock_open_world',icon:'&#127758;',sort:8},
{id:9,nameKey:'mock_indie',icon:'&#10024;',sort:9},
{id:10,nameKey:'mock_sandbox',icon:'&#129529;',sort:10}
];
const namePool = {
'zh-CN': [
'霓虹深渊','赛博纪元','量子破碎','暗影协议','虚空回响','龙裔传说','星尘远征','机械觉醒',
'末日余晖','深渊行者','星际拓荒','幽冥战场','永恒之刃','幻境迷踪','霓虹追猎','代码洪流',
'银河战纪','暗网潜行','机甲风暴','灵魂收割','时空裂隙','深渊领主','霓虹之城','量子跃迁',
'废土余生','神域降临','暗夜行者','星辰变奏','机械之心','虚空使者','末日方舟','龙魂觉醒',
'幻影特工','星际迷航','深渊之眼','霓虹武士','量子迷宫','暗影猎手','银河传说','机甲纪元'
],
'en': [
'Neon Abyss','Cyber Era','Quantum Break','Shadow Protocol','Void Echo','Dragon Legacy','Stellar Expedition','Mech Awakening',
'Doom Afterglow','Abyss Walker','Star Frontier','Phantom Battlefield','Eternal Blade','Phantom Trace','Neon Hunt','Code Flood',
'Galaxy Chronicle','Darknet Infiltration','Mech Storm','Soul Reaper','Time Rift','Abyss Lord','Neon City','Quantum Leap',
'Wasteland Life','Divine Descent','Nightwalker','Star Variations','Heart of Mech','Void Emissary','Doomsday Ark','Dragon Soul Rising',
'Shadow Agent','Star Trek','Eye of the Abyss','Neon Samurai','Quantum Maze','Shadow Hunter','Galaxy Legend','Mech Era'
],
'zh-TW': [
'霓虹深淵','賽博紀元','量子破碎','暗影協議','虛空迴響','龍裔傳說','星塵遠征','機械覺醒',
'末日餘暉','深淵行者','星際拓荒','幽冥戰場','永恆之刃','幻境迷蹤','霓虹追獵','代碼洪流',
'銀河戰紀','暗網潛行','機甲風暴','靈魂收割','時空裂隙','深淵領主','霓虹之城','量子躍遷',
'廢土餘生','神域降臨','暗夜行者','星辰變奏','機械之心','虛空使者','末日方舟','龍魂覺醒',
'幻影特工','星際迷航','深淵之眼','霓虹武士','量子迷宮','暗影獵手','銀河傳說','機甲紀元'
]
};
const descPool = {
'zh-CN': [
'在霓虹笼罩的未来都市中,你将扮演一名边缘行者,穿梭于现实与虚拟的边界,揭开一个足以颠覆整个世界的惊天阴谋。',
'探索广袤无垠的开放世界,与巨龙缔结契约,书写属于你的史诗传奇。无缝地图、实时天气、动态生态,沉浸感拉满。',
'硬核射击体验,次世代画面引擎打造的电影级视觉效果。多人对战、战术配合,在硝烟弥漫的战场中证明你的实力。',
'建造你的赛博帝国,管理资源、研发科技、扩张版图。从街头小卒到地下之王,每一步决策都将改变命运。',
'生存恐怖杰作,在诡异的废弃空间站中寻找真相。有限的弹药、无尽的恐惧,你能活到黎明吗?',
'极速狂飙,感受引擎的轰鸣与风的速度。上百辆授权跑车、全球知名赛道,还原最真实的竞速快感。'
],
'en': [
'In a neon-lit future metropolis, play as an edge runner navigating between reality and the virtual world, uncovering a conspiracy that could shake the entire world.',
'Explore a vast open world, forge pacts with dragons, and write your own epic saga. Seamless maps, real-time weather, and dynamic ecosystems for maximum immersion.',
'Hardcore shooting experience with cinematic visuals powered by a next-gen engine. Multiplayer battles and tactical teamwork to prove your skills on the battlefield.',
'Build your cyber empire, manage resources, research technologies, and expand your territory. Every decision from street punk to underground king changes your destiny.',
'A survival horror masterpiece set in an eerie abandoned space station. Limited ammo, endless fear — can you survive until dawn?',
'Feel the roar of engines and the rush of speed. Hundreds of licensed sports cars and world-famous tracks deliver the most authentic racing experience.'
],
'zh-TW': [
'在霓虹籠罩的未來都市中,你將扮演一名邊緣行者,穿梭於現實與虛擬的邊界,揭開一個足以顛覆整個世界的驚天陰謀。',
'探索廣袤無垠的開放世界,與巨龍締結契約,書寫屬於你的史詩傳奇。無縫地圖、即時天氣、動態生態,沉浸感拉滿。',
'硬核射擊體驗,次世代畫面引擎打造的電影級視覺效果。多人對戰、戰術配合,在硝煙瀰漫的戰場中證明你的實力。',
'建造你的賽博帝國,管理資源、研發科技、擴張版圖。從街頭小卒到地下之王,每一步決策都將改變命運。',
'生存恐怖傑作,在詭異的廢棄太空站中尋找真相。有限的彈藥、無盡的恐懼,你能活到黎明嗎?',
'極速狂飆,感受引擎的轟鳴與風的速度。上百輛授權跑車、全球知名賽道,還原最真實的競速快感。'
]
};
const tagPool = {
'zh-CN': ['开放世界','多人在线','单人剧情','PVP','PVE','沙盒','硬核','多人合作','次世代','光线追踪','MOD支持','云存档','手柄支持','中文配音','免费游玩'],
'en': ['Open World','Multiplayer','Story Mode','PVP','PVE','Sandbox','Hardcore','Co-op','Next-Gen','Ray Tracing','MOD Support','Cloud Save','Controller','Dubbed','Free to Play'],
'zh-TW': ['開放世界','多人線上','單人劇情','PVP','PVE','沙盒','硬核','多人合作','次世代','光線追蹤','MOD支援','雲端存檔','手把支援','中文配音','免費遊玩']
};
const featPool = {
'zh-CN': ['无缝开放世界','实时战斗系统','动态天气系统','物理破坏引擎','光线追踪渲染','多人在线合作','分支剧情选择','角色自定义系统','MOD 创意工坊','跨平台云存档'],
'en': ['Seamless Open World','Real-Time Combat','Dynamic Weather','Physics Destruction Engine','Ray Tracing Render','Online Co-op Branching Story Choices','Character Customization','MOD Workshop','Cross-Platform Cloud Save'],
'zh-TW': ['無縫開放世界','即時戰鬥系統','動態天氣系統','物理破壞引擎','光線追蹤渲染','多人線上合作','分支劇情選擇','角色自訂系統','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 lang = currentLang;
const names = namePool[lang] || namePool['zh-CN'];
const descs = descPool[lang] || descPool['zh-CN'];
const tags = tagPool[lang] || tagPool['zh-CN'];
const feats = featPool[lang] || featPool['zh-CN'];
const name = names[(id-1) % names.length] + (id > names.length ? ' ' + ['II','III','IV','V','ZERO','REBORN','RELOADED'][id % 7] : '');
const selectedTags = [];
const tagCount = randInt(2,4);
const shuffled = [...tags].sort(()=>Math.random()-.5);
for(let i=0;i<tagCount;i++) selectedTags.push(shuffled[i]);
const selectedFeats = [];
const fShuf = [...feats].sort(()=>Math.random()-.5);
for(let i=0;i<randInt(3,5);i++) selectedFeats.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(descs),
category_id: cat.id,
category_name: t(cat.nameKey),
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(selectedTags),
screenshots,
features: JSON.stringify(selectedFeats),
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:'',nameKey:null,name:t('cat_all'),icon:'&#127311;'}].concat(cats.map(c=>({
...c,
name: c.nameKey ? t(c.nameKey) : (c.name || '')
})));
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);
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(t('empty_hot')); 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(t('empty_new')); 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(t('empty_no_match')); 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)">${t('loading_categories')}</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(t('empty_load_fail'));
showToast(t('toast_load_fail'));
} 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">${t('modal_not_found')}</div>`; return; }
renderModal(game);
}catch(e){
modal.innerHTML = `<div class="empty-state" style="padding:60px">${t('modal_load_error')}</div>`;
}
}
window.openModal = openModal;
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||t('modal_desc_empty')}</p>
<div class="modal-section-label">${t('modal_tags_label')}</div>
<div class="modal-tags">
${tags.length ? tags.map(tg=>`<span class="modal-tag">${tg}</span>`).join('') : `<span style="color:var(--text-mute)">${t('modal_tags_empty')}</span>`}
</div>
<div class="modal-section-label">${t('modal_features_label')}</div>
<ul class="modal-features">
${features.length ? features.map(f=>`<li>${f}</li>`).join('') : `<li style="color:var(--text-mute)">${t('modal_features_empty')}</li>`}
</ul>
<div class="modal-section-label">${t('modal_stats_label')}</div>
<div class="modal-stats">
<div class="stat-box"><div class="v">${formatNumber(game.plays)}</div><div class="l">${t('modal_stat_plays')}</div></div>
<div class="stat-box"><div class="v">${formatNumber(game.downloads)}</div><div class="l">${t('modal_stat_downloads')}</div></div>
<div class="stat-box"><div class="v">${rating.toFixed(1)}</div><div class="l">${t('modal_stat_rating')}</div></div>
<div class="stat-box"><div class="v">${game.size||'-'}</div><div class="l">${t('modal_stat_size')}</div></div>
</div>
<div class="modal-section-label">${t('modal_details_label')}</div>
<div class="modal-meta">
<div class="meta-item"><span class="k">${t('modal_dev')}</span><span class="v">${game.developer||'-'}</span></div>
<div class="meta-item"><span class="k">${t('modal_pub')}</span><span class="v">${game.publisher||'-'}</span></div>
<div class="meta-item"><span class="k">${t('modal_platform')}</span><span class="v">${game.platform||'-'}</span></div>
<div class="meta-item"><span class="k">${t('modal_version')}</span><span class="v">${game.version||'-'}</span></div>
<div class="meta-item"><span class="k">${t('modal_release')}</span><span class="v">${game.release_date||'-'}</span></div>
<div class="meta-item"><span class="k">${t('modal_size')}</span><span class="v">${game.size||'-'}</span></div>
</div>
${shots.length ? `
<div class="modal-section-label">${t('modal_screenshots_label')}</div>
<div class="modal-shots">
${shots.map((s,i)=>`<img src="${shotImage(game,i)}" alt="${t('modal_screenshot_alt')}${i+1}" loading="lazy" onerror="shotFallback(this, ${game.id}, ${i})">`).join('')}
</div>` : ''}
<div class="modal-actions">
<button class="btn btn-primary" onclick="showToast('${t('toast_launching')} '+${JSON.stringify(game.name)}+'... ▶')">${t('modal_btn_play')}</button>
<button class="btn" onclick="showToast('${t('toast_fav_added')}')">${t('modal_btn_fav')}</button>
<button class="btn btn-magenta" onclick="showToast('${t('toast_downloading')} ${game.size||''}...')">${t('modal_btn_download')}</button>
</div>
</div>
`;
}
function closeModal(){
document.getElementById('modalOverlay').classList.remove('open');
document.body.style.overflow = '';
}
window.closeModal = closeModal;
/* ============================================================
交互事件
============================================================ */
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();
});
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();
});
}
/* ============================================================
滚动揭示动画
============================================================ */
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));
}
/* ============================================================
语言切换回调:重新加载所有数据
============================================================ */
onLangChange = function(lang){
MOCK.allGames.length = 0;
for(let i=1;i<=48;i++) MOCK.allGames.push(MOCK.detail(i));
state.filters = { category_id: '', keyword: '', sort: '' };
state.library.page = 1;
document.getElementById('globalSearch').value = '';
document.getElementById('libSearch').value = '';
document.getElementById('globalSearchClear').style.display = 'none';
document.getElementById('sortLabel').textContent = t('sort_default');
document.querySelectorAll('.sort-menu button').forEach(b=>{
b.classList.toggle('active', !b.dataset.sort);
});
document.querySelectorAll('.cat-pill').forEach(b=>b.classList.remove('active'));
loadHome();
loadLibrary();
};
/* ============================================================
初始化
============================================================ */
async function init(){
applyI18n();
initEvents();
observeReveal();
initCountUp();
await loadHome();
await loadLibrary();
}
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>