- 所有18套前端模板 + 管理后台共20个HTML文件添加i18n引擎 - 前端JavaScript方案:I18N_LOCALES翻译字典 + data-i18n属性 + t()函数调用 - 语言切换器UI:下拉菜单选择,匹配各模板主题风格 - 语言持久化:localStorage存储 + URL参数覆盖 + 浏览器语言自动检测 - onLangChange回调:语言切换时动态重新渲染所有内容 - 支持语言:zh-CN(简体中文) / zh-TW(繁体中文) / en(English)
1956 lines
73 KiB
HTML
1956 lines
73 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>APP HUB — 下一代应用中心</title>
|
|
<style>
|
|
/* ==================== RESET & BASE ==================== */
|
|
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
|
:root{
|
|
--bg-base:#0d0d0d;
|
|
--bg-surface:#141414;
|
|
--bg-card:rgba(255,255,255,0.04);
|
|
--bg-card-hover:rgba(255,255,255,0.07);
|
|
--border-glass:rgba(255,255,255,0.08);
|
|
--border-glass-hover:rgba(255,255,255,0.15);
|
|
--neon-green:#00ff88;
|
|
--neon-cyan:#00d4ff;
|
|
--neon-purple:#a855f7;
|
|
--text-primary:#e8e8e8;
|
|
--text-secondary:#999;
|
|
--text-muted:#666;
|
|
--font-mono:'SF Mono','Fira Code','Consolas','Courier New',monospace;
|
|
--font-sans:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;
|
|
--glow-green:0 0 20px rgba(0,255,136,0.35),0 0 40px rgba(0,255,136,0.15);
|
|
--glow-cyan:0 0 20px rgba(0,212,255,0.35),0 0 40px rgba(0,212,255,0.15);
|
|
}
|
|
html{scroll-behavior:smooth;font-size:16px}
|
|
body{
|
|
font-family:var(--font-sans);
|
|
background:var(--bg-base);
|
|
color:var(--text-primary);
|
|
min-height:100vh;
|
|
overflow-x:hidden;
|
|
line-height:1.6;
|
|
}
|
|
::selection{background:rgba(0,255,136,0.3);color:#fff}
|
|
::-webkit-scrollbar{width:8px;height:8px}
|
|
::-webkit-scrollbar-track{background:#0a0a0a}
|
|
::-webkit-scrollbar-thumb{background:rgba(0,255,136,0.2);border-radius:4px}
|
|
::-webkit-scrollbar-thumb:hover{background:rgba(0,255,136,0.4)}
|
|
a{color:inherit;text-decoration:none}
|
|
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
|
|
img{max-width:100%;display:block}
|
|
|
|
/* ==================== ANIMATED BACKGROUND ==================== */
|
|
.bg-layer{position:fixed;inset:0;z-index:-2;pointer-events:none;overflow:hidden}
|
|
.bg-grid{
|
|
position:absolute;inset:-50%;
|
|
background-image:
|
|
linear-gradient(rgba(0,255,136,0.04) 1px,transparent 1px),
|
|
linear-gradient(90deg,rgba(0,255,136,0.04) 1px,transparent 1px);
|
|
background-size:50px 50px;
|
|
animation:gridMove 25s linear infinite;
|
|
transform:perspective(500px) rotateX(0deg);
|
|
}
|
|
@keyframes gridMove{
|
|
0%{transform:translate(0,0)}
|
|
100%{transform:translate(50px,50px)}
|
|
}
|
|
.bg-glow{
|
|
position:absolute;
|
|
border-radius:50%;
|
|
filter:blur(120px);
|
|
opacity:0.15;
|
|
animation:glowPulse 8s ease-in-out infinite alternate;
|
|
}
|
|
.bg-glow.g1{width:600px;height:600px;background:var(--neon-green);top:-200px;left:-100px}
|
|
.bg-glow.g2{width:500px;height:500px;background:var(--neon-cyan);bottom:-100px;right:-100px;animation-delay:3s}
|
|
.bg-glow.g3{width:400px;height:400px;background:var(--neon-purple);top:40%;left:50%;animation-delay:5s}
|
|
@keyframes glowPulse{
|
|
0%{opacity:0.08;transform:scale(1)}
|
|
100%{opacity:0.18;transform:scale(1.15)}
|
|
}
|
|
.particles{position:fixed;inset:0;z-index:-1;pointer-events:none;overflow:hidden}
|
|
.particle{
|
|
position:absolute;
|
|
width:3px;height:3px;
|
|
border-radius:50%;
|
|
background:var(--neon-green);
|
|
box-shadow:0 0 6px var(--neon-green);
|
|
opacity:0;
|
|
animation:floatUp linear infinite;
|
|
}
|
|
@keyframes floatUp{
|
|
0%{opacity:0;transform:translateY(100vh) translateX(0)}
|
|
10%{opacity:0.6}
|
|
90%{opacity:0.6}
|
|
100%{opacity:0;transform:translateY(-10vh) translateX(50px)}
|
|
}
|
|
|
|
/* ==================== HEADER ==================== */
|
|
.header{
|
|
position:sticky;top:0;z-index:100;
|
|
background:rgba(13,13,13,0.8);
|
|
backdrop-filter:blur(20px);
|
|
-webkit-backdrop-filter:blur(20px);
|
|
border-bottom:1px solid var(--border-glass);
|
|
}
|
|
.header-inner{
|
|
max-width:1400px;margin:0 auto;
|
|
padding:0 32px;height:72px;
|
|
display:flex;align-items:center;gap:32px;
|
|
}
|
|
.logo{
|
|
font-family:var(--font-mono);
|
|
font-size:1.4rem;font-weight:700;
|
|
letter-spacing:2px;
|
|
color:var(--neon-green);
|
|
text-shadow:0 0 12px rgba(0,255,136,0.6),0 0 24px rgba(0,255,136,0.3);
|
|
display:flex;align-items:center;gap:8px;
|
|
white-space:nowrap;
|
|
}
|
|
.logo-dot{
|
|
width:10px;height:10px;border-radius:50%;
|
|
background:var(--neon-green);
|
|
box-shadow:0 0 10px var(--neon-green);
|
|
animation:blink 2s ease-in-out infinite;
|
|
}
|
|
@keyframes blink{0%,100%{opacity:1}50%{opacity:0.3}}
|
|
.nav{display:flex;gap:4px;flex:1}
|
|
.nav-link{
|
|
padding:8px 18px;
|
|
font-size:0.9rem;font-weight:500;
|
|
color:var(--text-secondary);
|
|
border-radius:8px;
|
|
transition:all 0.3s;
|
|
position:relative;
|
|
}
|
|
.nav-link:hover{color:var(--neon-green);background:rgba(0,255,136,0.06)}
|
|
.nav-link.active{color:var(--neon-green);background:rgba(0,255,136,0.1)}
|
|
.nav-link.active::after{
|
|
content:'';position:absolute;bottom:-2px;left:50%;transform:translateX(-50%);
|
|
width:20px;height:2px;background:var(--neon-green);
|
|
box-shadow:0 0 8px var(--neon-green);
|
|
}
|
|
.search-wrap{position:relative;width:320px;max-width:40vw}
|
|
.search-input{
|
|
width:100%;
|
|
padding:10px 16px 10px 40px;
|
|
background:rgba(255,255,255,0.05);
|
|
border:1px solid rgba(0,255,136,0.2);
|
|
border-radius:10px;
|
|
color:var(--text-primary);
|
|
font-size:0.875rem;
|
|
font-family:var(--font-sans);
|
|
transition:all 0.3s;
|
|
outline:none;
|
|
}
|
|
.search-input::placeholder{color:var(--text-muted)}
|
|
.search-input:focus{
|
|
border-color:var(--neon-green);
|
|
box-shadow:0 0 0 3px rgba(0,255,136,0.1),0 0 20px rgba(0,255,136,0.15);
|
|
background:rgba(0,255,136,0.03);
|
|
}
|
|
.search-icon{
|
|
position:absolute;left:13px;top:50%;transform:translateY(-50%);
|
|
width:16px;height:16px;
|
|
color:var(--neon-green);
|
|
pointer-events:none;
|
|
}
|
|
.mobile-toggle{display:none;font-size:1.5rem;color:var(--neon-green);padding:4px}
|
|
|
|
/* ==================== LANGUAGE SWITCHER ==================== */
|
|
.lang-switcher{position:relative;flex-shrink:0}
|
|
.lang-btn{
|
|
display:flex;align-items:center;gap:6px;padding:7px 12px;border-radius:980px;
|
|
font-size:0.8rem;font-weight:500;color:var(--text-secondary);
|
|
border:1px solid rgba(0,255,136,0.2);background:rgba(0,255,136,0.05);
|
|
transition:all .25s ease;
|
|
}
|
|
.lang-btn:hover{background:rgba(0,255,136,0.12);color:var(--neon-green);border-color:rgba(0,255,136,0.4)}
|
|
.lang-btn svg{width:16px;height:16px}
|
|
.lang-btn .chevron{width:12px;height:12px}
|
|
.lang-dropdown{
|
|
position:absolute;top:calc(100% + 6px);right:0;min-width:150px;
|
|
background:#141414;border:1px solid rgba(0,255,136,0.2);border-radius:12px;
|
|
box-shadow:0 8px 32px rgba(0,0,0,0.6);padding:6px;z-index:200;
|
|
opacity:0;visibility:hidden;transform:translateY(-6px);transition:all .2s ease;
|
|
}
|
|
.lang-dropdown.open{opacity:1;visibility:visible;transform:translateY(0)}
|
|
.lang-option{
|
|
display:flex;align-items:center;gap:8px;padding:9px 12px;
|
|
border-radius:8px;font-size:0.85rem;color:var(--text-secondary);
|
|
cursor:pointer;transition:all .15s;
|
|
}
|
|
.lang-option:hover{background:rgba(0,255,136,0.06);color:var(--text-primary)}
|
|
.lang-option.active{color:var(--neon-green);font-weight:600}
|
|
.lang-option .check{width:16px;height:16px;opacity:0;transition:opacity .2s}
|
|
.lang-option.active .check{opacity:1}
|
|
|
|
/* ==================== LAYOUT ==================== */
|
|
.container{max-width:1400px;margin:0 auto;padding:0 32px}
|
|
.section{padding:72px 0}
|
|
.section-header{
|
|
display:flex;align-items:center;justify-content:space-between;
|
|
margin-bottom:36px;flex-wrap:wrap;gap:16px;
|
|
}
|
|
.section-title-wrap{display:flex;align-items:center;gap:14px}
|
|
.section-title{
|
|
font-size:1.6rem;font-weight:700;
|
|
letter-spacing:1px;
|
|
}
|
|
.section-title .accent{color:var(--neon-green);text-shadow:0 0 12px rgba(0,255,136,0.4)}
|
|
.section-tag{
|
|
font-family:var(--font-mono);font-size:0.7rem;
|
|
color:var(--neon-cyan);
|
|
padding:3px 10px;
|
|
border:1px solid rgba(0,212,255,0.3);
|
|
border-radius:4px;
|
|
background:rgba(0,212,255,0.05);
|
|
text-transform:uppercase;letter-spacing:1px;
|
|
}
|
|
.section-line{
|
|
flex:1;height:1px;
|
|
background:linear-gradient(90deg,rgba(0,255,136,0.3),transparent);
|
|
min-width:60px;
|
|
}
|
|
|
|
/* ==================== HERO ==================== */
|
|
.hero{
|
|
position:relative;
|
|
padding:100px 0 80px;
|
|
overflow:hidden;
|
|
}
|
|
.hero-inner{max-width:1400px;margin:0 auto;padding:0 32px;position:relative;z-index:2}
|
|
.hero-badge{
|
|
display:inline-flex;align-items:center;gap:8px;
|
|
padding:6px 16px;
|
|
background:rgba(0,255,136,0.08);
|
|
border:1px solid rgba(0,255,136,0.25);
|
|
border-radius:20px;
|
|
font-family:var(--font-mono);font-size:0.75rem;
|
|
color:var(--neon-green);
|
|
margin-bottom:28px;
|
|
}
|
|
.hero-badge-dot{width:6px;height:6px;border-radius:50%;background:var(--neon-green);box-shadow:0 0 6px var(--neon-green);animation:blink 1.5s infinite}
|
|
.hero-title{
|
|
font-size:3.8rem;font-weight:800;
|
|
line-height:1.15;letter-spacing:2px;
|
|
margin-bottom:24px;
|
|
}
|
|
.hero-title .glow-green{
|
|
color:var(--neon-green);
|
|
text-shadow:0 0 20px rgba(0,255,136,0.5),0 0 40px rgba(0,255,136,0.3);
|
|
}
|
|
.hero-title .glow-cyan{
|
|
color:var(--neon-cyan);
|
|
text-shadow:0 0 20px rgba(0,212,255,0.5),0 0 40px rgba(0,212,255,0.3);
|
|
}
|
|
.hero-terminal{
|
|
background:rgba(0,0,0,0.5);
|
|
border:1px solid var(--border-glass);
|
|
border-left:3px solid var(--neon-green);
|
|
border-radius:8px;
|
|
padding:20px 24px;
|
|
font-family:var(--font-mono);
|
|
font-size:0.875rem;
|
|
max-width:560px;
|
|
margin-bottom:36px;
|
|
box-shadow:0 0 30px rgba(0,255,136,0.08);
|
|
}
|
|
.terminal-line{color:var(--text-secondary);margin-bottom:6px}
|
|
.terminal-line:last-child{margin-bottom:0}
|
|
.terminal-prompt{color:var(--neon-green)}
|
|
.terminal-text{color:var(--text-primary)}
|
|
.terminal-cursor{
|
|
display:inline-block;width:8px;height:14px;
|
|
background:var(--neon-green);
|
|
vertical-align:text-bottom;
|
|
animation:cursorBlink 1s step-end infinite;
|
|
box-shadow:0 0 6px var(--neon-green);
|
|
}
|
|
@keyframes cursorBlink{0%,50%{opacity:1}51%,100%{opacity:0}}
|
|
.hero-actions{display:flex;gap:16px;flex-wrap:wrap}
|
|
.btn-primary{
|
|
display:inline-flex;align-items:center;gap:10px;
|
|
padding:14px 32px;
|
|
background:linear-gradient(135deg,rgba(0,255,136,0.15),rgba(0,212,255,0.1));
|
|
border:1px solid var(--neon-green);
|
|
border-radius:10px;
|
|
color:var(--neon-green);
|
|
font-size:0.95rem;font-weight:600;
|
|
letter-spacing:1px;
|
|
transition:all 0.3s;
|
|
box-shadow:0 0 20px rgba(0,255,136,0.15);
|
|
}
|
|
.btn-primary:hover{
|
|
background:linear-gradient(135deg,rgba(0,255,136,0.25),rgba(0,212,255,0.15));
|
|
box-shadow:0 0 30px rgba(0,255,136,0.4),0 0 60px rgba(0,255,136,0.2);
|
|
transform:translateY(-2px);
|
|
}
|
|
.btn-secondary{
|
|
display:inline-flex;align-items:center;gap:10px;
|
|
padding:14px 32px;
|
|
background:rgba(255,255,255,0.04);
|
|
border:1px solid var(--border-glass-hover);
|
|
border-radius:10px;
|
|
color:var(--text-primary);
|
|
font-size:0.95rem;font-weight:600;
|
|
transition:all 0.3s;
|
|
}
|
|
.btn-secondary:hover{border-color:var(--neon-cyan);color:var(--neon-cyan);box-shadow:var(--glow-cyan)}
|
|
.hero-stats{
|
|
display:flex;gap:48px;margin-top:56px;
|
|
flex-wrap:wrap;
|
|
}
|
|
.hero-stat{text-align:left}
|
|
.hero-stat-num{
|
|
font-family:var(--font-mono);
|
|
font-size:2rem;font-weight:700;
|
|
color:var(--neon-green);
|
|
text-shadow:0 0 12px rgba(0,255,136,0.3);
|
|
}
|
|
.hero-stat-label{font-size:0.8rem;color:var(--text-muted);letter-spacing:1px;text-transform:uppercase}
|
|
|
|
/* ==================== CATEGORY PILLS ==================== */
|
|
.category-bar{
|
|
display:flex;gap:10px;flex-wrap:wrap;
|
|
padding:8px 0;
|
|
}
|
|
.cat-pill{
|
|
padding:8px 20px;
|
|
background:rgba(255,255,255,0.04);
|
|
border:1px solid var(--border-glass);
|
|
border-radius:24px;
|
|
color:var(--text-secondary);
|
|
font-size:0.875rem;font-weight:500;
|
|
transition:all 0.3s;
|
|
white-space:nowrap;
|
|
}
|
|
.cat-pill:hover{
|
|
border-color:rgba(0,255,136,0.4);
|
|
color:var(--text-primary);
|
|
background:rgba(0,255,136,0.05);
|
|
}
|
|
.cat-pill.active{
|
|
background:rgba(0,255,136,0.1);
|
|
border-color:var(--neon-green);
|
|
color:var(--neon-green);
|
|
box-shadow:0 0 15px rgba(0,255,136,0.2),inset 0 0 10px rgba(0,255,136,0.05);
|
|
text-shadow:0 0 8px rgba(0,255,136,0.5);
|
|
}
|
|
|
|
/* ==================== APP GRID & CARDS ==================== */
|
|
.app-grid{
|
|
display:grid;
|
|
grid-template-columns:repeat(5,1fr);
|
|
gap:20px;
|
|
}
|
|
@media(max-width:1100px){.app-grid{grid-template-columns:repeat(3,1fr)}}
|
|
@media(max-width:680px){.app-grid{grid-template-columns:repeat(2,1fr)}}
|
|
|
|
.app-card{
|
|
background:var(--bg-card);
|
|
border:1px solid var(--border-glass);
|
|
border-radius:14px;
|
|
padding:20px;
|
|
cursor:pointer;
|
|
transition:all 0.35s cubic-bezier(0.4,0,0.2,1);
|
|
position:relative;
|
|
overflow:hidden;
|
|
backdrop-filter:blur(10px);
|
|
}
|
|
.app-card::before{
|
|
content:'';position:absolute;inset:0;
|
|
background:linear-gradient(135deg,rgba(0,255,136,0) 0%,rgba(0,255,136,0.04) 100%);
|
|
opacity:0;transition:opacity 0.35s;
|
|
pointer-events:none;
|
|
}
|
|
.app-card:hover{
|
|
border-color:rgba(0,255,136,0.35);
|
|
background:var(--bg-card-hover);
|
|
transform:translateY(-4px);
|
|
box-shadow:0 8px 30px rgba(0,0,0,0.4),0 0 20px rgba(0,255,136,0.12);
|
|
}
|
|
.app-card:hover::before{opacity:1}
|
|
.card-top{display:flex;align-items:center;gap:14px;margin-bottom:14px}
|
|
.app-icon{
|
|
width:56px;height:56px;
|
|
border-radius:14px;
|
|
flex-shrink:0;
|
|
position:relative;
|
|
overflow:hidden;
|
|
}
|
|
.app-icon-img{width:100%;height:100%;object-fit:cover;border-radius:14px}
|
|
.app-icon-placeholder{
|
|
width:100%;height:100%;
|
|
display:flex;align-items:center;justify-content:center;
|
|
font-size:1.5rem;font-weight:700;
|
|
border-radius:14px;
|
|
border:2px solid;
|
|
position:relative;
|
|
}
|
|
.app-icon-placeholder::after{
|
|
content:'';position:absolute;inset:0;border-radius:12px;
|
|
box-shadow:inset 0 0 15px rgba(0,0,0,0.5);
|
|
pointer-events:none;
|
|
}
|
|
.card-name-row{flex:1;min-width:0}
|
|
.card-name{
|
|
font-size:0.95rem;font-weight:600;
|
|
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
|
margin-bottom:3px;
|
|
}
|
|
.card-dev{
|
|
font-size:0.75rem;color:var(--text-muted);
|
|
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
|
}
|
|
.card-cat{
|
|
font-family:var(--font-mono);
|
|
font-size:0.65rem;
|
|
color:var(--neon-cyan);
|
|
padding:2px 8px;
|
|
border:1px solid rgba(0,212,255,0.2);
|
|
border-radius:4px;
|
|
background:rgba(0,212,255,0.05);
|
|
margin-bottom:12px;
|
|
display:inline-block;
|
|
}
|
|
.card-desc{
|
|
font-size:0.78rem;color:var(--text-secondary);
|
|
line-height:1.5;
|
|
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
|
|
overflow:hidden;
|
|
margin-bottom:14px;
|
|
min-height:42px;
|
|
}
|
|
.card-bottom{
|
|
display:flex;align-items:center;justify-content:space-between;
|
|
padding-top:12px;
|
|
border-top:1px solid var(--border-glass);
|
|
}
|
|
.card-rating{display:flex;align-items:center;gap:5px}
|
|
.stars{display:flex;gap:1px}
|
|
.star{font-size:0.7rem;color:#333;transition:all 0.3s}
|
|
.star.filled{color:var(--neon-green);text-shadow:0 0 4px rgba(0,255,136,0.6)}
|
|
.star.half{position:relative;color:#333}
|
|
.star.half::before{content:'★';position:absolute;color:var(--neon-green);text-shadow:0 0 4px rgba(0,255,136,0.6);width:50%;overflow:hidden}
|
|
.rating-num{
|
|
font-family:var(--font-mono);
|
|
font-size:0.8rem;font-weight:600;
|
|
color:var(--neon-green);
|
|
}
|
|
.card-downloads{
|
|
font-family:var(--font-mono);
|
|
font-size:0.7rem;
|
|
color:var(--text-muted);
|
|
}
|
|
.card-downloads .dl-icon{color:var(--neon-cyan)}
|
|
.new-badge{
|
|
position:absolute;top:12px;right:12px;
|
|
font-family:var(--font-mono);font-size:0.6rem;font-weight:700;
|
|
padding:3px 8px;
|
|
background:rgba(0,255,136,0.15);
|
|
border:1px solid var(--neon-green);
|
|
color:var(--neon-green);
|
|
border-radius:4px;
|
|
text-shadow:0 0 6px rgba(0,255,136,0.5);
|
|
box-shadow:0 0 10px rgba(0,255,136,0.2);
|
|
letter-spacing:1px;
|
|
}
|
|
|
|
/* ==================== FEATURED CARDS ==================== */
|
|
.featured-grid{
|
|
display:grid;
|
|
grid-template-columns:repeat(4,1fr);
|
|
gap:20px;
|
|
}
|
|
@media(max-width:1100px){.featured-grid{grid-template-columns:repeat(3,1fr)}}
|
|
@media(max-width:680px){.featured-grid{grid-template-columns:repeat(2,1fr)}}
|
|
.featured-card{
|
|
background:linear-gradient(145deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02));
|
|
border:1px solid var(--border-glass);
|
|
border-radius:16px;
|
|
padding:24px;
|
|
cursor:pointer;
|
|
transition:all 0.35s;
|
|
position:relative;
|
|
overflow:hidden;
|
|
}
|
|
.featured-card::before{
|
|
content:'';position:absolute;top:0;left:0;right:0;height:2px;
|
|
background:linear-gradient(90deg,transparent,var(--neon-green),transparent);
|
|
opacity:0;transition:opacity 0.35s;
|
|
}
|
|
.featured-card:hover{
|
|
border-color:rgba(0,255,136,0.3);
|
|
transform:translateY(-4px);
|
|
box-shadow:0 10px 40px rgba(0,0,0,0.4),0 0 30px rgba(0,255,136,0.1);
|
|
}
|
|
.featured-card:hover::before{opacity:1}
|
|
.featured-card .card-top{margin-bottom:16px}
|
|
.featured-card .app-icon{width:64px;height:64px}
|
|
.featured-card .app-icon-placeholder{font-size:1.7rem}
|
|
.featured-card .card-name{font-size:1.05rem}
|
|
.featured-card .card-desc{font-size:0.82rem;min-height:48px}
|
|
.feat-badge{
|
|
position:absolute;top:14px;right:14px;
|
|
font-family:var(--font-mono);font-size:0.6rem;
|
|
padding:3px 8px;
|
|
background:rgba(0,212,255,0.1);
|
|
border:1px solid rgba(0,212,255,0.3);
|
|
color:var(--neon-cyan);
|
|
border-radius:4px;
|
|
letter-spacing:1px;
|
|
}
|
|
|
|
/* ==================== NEW APPS HORIZONTAL SCROLL ==================== */
|
|
.new-scroll{
|
|
display:flex;gap:18px;
|
|
overflow-x:auto;
|
|
padding:8px 4px 20px;
|
|
scroll-snap-type:x mandatory;
|
|
-webkit-overflow-scrolling:touch;
|
|
}
|
|
.new-scroll::-webkit-scrollbar{height:4px}
|
|
.new-scroll::-webkit-scrollbar-track{background:transparent}
|
|
.new-scroll::-webkit-scrollbar-thumb{background:rgba(0,255,136,0.2);border-radius:2px}
|
|
.new-card{
|
|
flex:0 0 300px;
|
|
scroll-snap-align:start;
|
|
background:var(--bg-card);
|
|
border:1px solid var(--border-glass);
|
|
border-radius:14px;
|
|
padding:20px;
|
|
cursor:pointer;
|
|
transition:all 0.3s;
|
|
position:relative;
|
|
}
|
|
.new-card:hover{
|
|
border-color:rgba(0,255,136,0.35);
|
|
transform:translateY(-3px);
|
|
box-shadow:0 6px 25px rgba(0,0,0,0.4),0 0 15px rgba(0,255,136,0.1);
|
|
}
|
|
|
|
/* ==================== LIBRARY TOOLBAR ==================== */
|
|
.library-toolbar{
|
|
display:flex;align-items:center;gap:14px;
|
|
margin-bottom:28px;flex-wrap:wrap;
|
|
}
|
|
.lib-search{
|
|
position:relative;flex:1;min-width:220px;
|
|
}
|
|
.lib-search .search-input{padding-left:40px}
|
|
.lib-sort{
|
|
display:flex;gap:6px;
|
|
background:rgba(255,255,255,0.03);
|
|
border:1px solid var(--border-glass);
|
|
border-radius:10px;
|
|
padding:4px;
|
|
}
|
|
.sort-btn{
|
|
padding:7px 16px;
|
|
font-size:0.8rem;font-weight:500;
|
|
color:var(--text-secondary);
|
|
border-radius:7px;
|
|
transition:all 0.3s;
|
|
white-space:nowrap;
|
|
}
|
|
.sort-btn:hover{color:var(--text-primary)}
|
|
.sort-btn.active{
|
|
background:rgba(0,255,136,0.1);
|
|
color:var(--neon-green);
|
|
box-shadow:0 0 10px rgba(0,255,136,0.15);
|
|
}
|
|
.lib-cat-filter{display:flex;gap:8px;flex-wrap:wrap}
|
|
.lib-cat-filter .cat-pill{padding:6px 16px;font-size:0.8rem}
|
|
|
|
/* ==================== PAGINATION ==================== */
|
|
.pagination{
|
|
display:flex;align-items:center;justify-content:center;gap:6px;
|
|
margin-top:40px;
|
|
}
|
|
.page-btn{
|
|
min-width:40px;height:40px;
|
|
display:flex;align-items:center;justify-content:center;
|
|
background:rgba(255,255,255,0.04);
|
|
border:1px solid var(--border-glass);
|
|
border-radius:8px;
|
|
font-family:var(--font-mono);font-size:0.85rem;
|
|
color:var(--text-secondary);
|
|
transition:all 0.3s;
|
|
padding:0 12px;
|
|
}
|
|
.page-btn:hover:not(:disabled){
|
|
border-color:rgba(0,255,136,0.4);
|
|
color:var(--neon-green);
|
|
background:rgba(0,255,136,0.05);
|
|
}
|
|
.page-btn.active{
|
|
background:rgba(0,255,136,0.1);
|
|
border-color:var(--neon-green);
|
|
color:var(--neon-green);
|
|
box-shadow:0 0 12px rgba(0,255,136,0.2);
|
|
}
|
|
.page-btn:disabled{opacity:0.3;cursor:not-allowed}
|
|
.page-info{
|
|
font-family:var(--font-mono);font-size:0.8rem;
|
|
color:var(--text-muted);
|
|
margin:0 12px;
|
|
}
|
|
|
|
/* ==================== SKELETON ==================== */
|
|
.skeleton-card{
|
|
background:var(--bg-card);
|
|
border:1px solid var(--border-glass);
|
|
border-radius:14px;
|
|
padding:20px;
|
|
}
|
|
.skel{background:rgba(255,255,255,0.04);border-radius:6px;position:relative;overflow:hidden}
|
|
.skel::after{
|
|
content:'';position:absolute;inset:0;
|
|
background:linear-gradient(90deg,transparent,rgba(0,255,136,0.06),transparent);
|
|
animation:shimmer 1.8s infinite;
|
|
}
|
|
@keyframes shimmer{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
|
|
.skel-icon{width:56px;height:56px;border-radius:14px;margin-bottom:14px}
|
|
.skel-line{height:12px;margin-bottom:8px}
|
|
.skel-line.short{width:60%}
|
|
.skel-line.mid{width:80%}
|
|
|
|
/* ==================== MODAL ==================== */
|
|
.modal-overlay{
|
|
position:fixed;inset:0;z-index:1000;
|
|
background:rgba(0,0,0,0.7);
|
|
backdrop-filter:blur(12px);
|
|
-webkit-backdrop-filter:blur(12px);
|
|
display:flex;align-items:center;justify-content:center;
|
|
padding:24px;
|
|
opacity:0;visibility:hidden;
|
|
transition:opacity 0.3s,visibility 0.3s;
|
|
}
|
|
.modal-overlay.open{opacity:1;visibility:visible}
|
|
.modal{
|
|
background:linear-gradient(145deg,#161616,#0f0f0f);
|
|
border:1px solid rgba(0,255,136,0.25);
|
|
border-radius:18px;
|
|
max-width:680px;width:100%;
|
|
max-height:88vh;overflow-y:auto;
|
|
padding:36px;
|
|
position:relative;
|
|
box-shadow:0 0 60px rgba(0,255,136,0.1),0 20px 60px rgba(0,0,0,0.6);
|
|
transform:scale(0.92) translateY(20px);
|
|
transition:transform 0.35s cubic-bezier(0.34,1.56,0.64,1);
|
|
}
|
|
.modal-overlay.open .modal{transform:scale(1) translateY(0)}
|
|
.modal::-webkit-scrollbar{width:6px}
|
|
.modal::-webkit-scrollbar-thumb{background:rgba(0,255,136,0.2);border-radius:3px}
|
|
.modal-close{
|
|
position:absolute;top:20px;right:20px;
|
|
width:36px;height:36px;
|
|
display:flex;align-items:center;justify-content:center;
|
|
background:rgba(255,255,255,0.05);
|
|
border:1px solid var(--border-glass);
|
|
border-radius:50%;
|
|
font-size:1.1rem;color:var(--text-secondary);
|
|
transition:all 0.3s;z-index:2;
|
|
}
|
|
.modal-close:hover{border-color:var(--neon-green);color:var(--neon-green);box-shadow:var(--glow-green);transform:rotate(90deg)}
|
|
.modal-header{display:flex;gap:20px;margin-bottom:24px;align-items:flex-start}
|
|
.modal-icon{
|
|
width:80px;height:80px;flex-shrink:0;
|
|
border-radius:18px;
|
|
position:relative;
|
|
}
|
|
.modal-icon .app-icon-placeholder{font-size:2rem;border-radius:18px}
|
|
.modal-icon .app-icon-img{border-radius:18px}
|
|
.modal-title-area{flex:1;min-width:0;padding-top:4px}
|
|
.modal-name{
|
|
font-size:1.5rem;font-weight:700;
|
|
margin-bottom:6px;
|
|
color:var(--text-primary);
|
|
text-shadow:0 0 12px rgba(0,255,136,0.2);
|
|
}
|
|
.modal-dev{font-size:0.85rem;color:var(--text-muted);margin-bottom:10px}
|
|
.modal-cat-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
.modal-rating-row{display:flex;align-items:center;gap:8px}
|
|
.modal-rating-row .stars .star{font-size:0.9rem}
|
|
.modal-rating-num{font-family:var(--font-mono);font-weight:700;color:var(--neon-green);font-size:1rem}
|
|
.modal-reviews{font-size:0.75rem;color:var(--text-muted)}
|
|
.modal-section{margin-bottom:24px}
|
|
.modal-section-title{
|
|
font-size:0.8rem;font-weight:600;
|
|
color:var(--neon-cyan);
|
|
text-transform:uppercase;letter-spacing:1.5px;
|
|
margin-bottom:10px;
|
|
display:flex;align-items:center;gap:8px;
|
|
}
|
|
.modal-section-title::before{
|
|
content:'';width:3px;height:14px;
|
|
background:var(--neon-cyan);
|
|
border-radius:2px;
|
|
box-shadow:0 0 6px var(--neon-cyan);
|
|
}
|
|
.modal-desc{font-size:0.9rem;color:var(--text-secondary);line-height:1.7}
|
|
.tag-row{display:flex;flex-wrap:wrap;gap:8px}
|
|
.tag-pill{
|
|
font-size:0.75rem;
|
|
padding:5px 14px;
|
|
background:rgba(0,255,136,0.06);
|
|
border:1px solid rgba(0,255,136,0.2);
|
|
border-radius:20px;
|
|
color:var(--neon-green);
|
|
transition:all 0.3s;
|
|
}
|
|
.tag-pill:hover{background:rgba(0,255,136,0.12);box-shadow:0 0 10px rgba(0,255,136,0.15)}
|
|
.feature-list{list-style:none;display:flex;flex-direction:column;gap:10px}
|
|
.feature-item{
|
|
display:flex;align-items:center;gap:10px;
|
|
font-size:0.875rem;color:var(--text-secondary);
|
|
padding:10px 14px;
|
|
background:rgba(255,255,255,0.03);
|
|
border:1px solid var(--border-glass);
|
|
border-radius:8px;
|
|
transition:all 0.3s;
|
|
}
|
|
.feature-item:hover{border-color:rgba(0,255,136,0.2);background:rgba(0,255,136,0.03)}
|
|
.feature-check{color:var(--neon-green);font-size:0.9rem;flex-shrink:0}
|
|
.modal-stats{
|
|
display:grid;
|
|
grid-template-columns:repeat(3,1fr);
|
|
gap:12px;
|
|
}
|
|
@media(max-width:520px){.modal-stats{grid-template-columns:repeat(2,1fr)}}
|
|
.stat-box{
|
|
background:rgba(255,255,255,0.03);
|
|
border:1px solid var(--border-glass);
|
|
border-radius:10px;
|
|
padding:14px;
|
|
text-align:center;
|
|
}
|
|
.stat-label{font-size:0.65rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px}
|
|
.stat-value{font-family:var(--font-mono);font-size:0.95rem;font-weight:600;color:var(--neon-green)}
|
|
.modal-download{
|
|
display:flex;align-items:center;justify-content:center;gap:10px;
|
|
width:100%;
|
|
padding:16px;
|
|
margin-top:8px;
|
|
background:linear-gradient(135deg,rgba(0,255,136,0.15),rgba(0,212,255,0.1));
|
|
border:1px solid var(--neon-green);
|
|
border-radius:12px;
|
|
color:var(--neon-green);
|
|
font-size:1rem;font-weight:700;
|
|
letter-spacing:1px;
|
|
transition:all 0.3s;
|
|
box-shadow:0 0 20px rgba(0,255,136,0.12);
|
|
}
|
|
.modal-download:hover{
|
|
background:linear-gradient(135deg,rgba(0,255,136,0.25),rgba(0,212,255,0.15));
|
|
box-shadow:0 0 35px rgba(0,255,136,0.35),0 0 60px rgba(0,255,136,0.15);
|
|
transform:translateY(-2px);
|
|
}
|
|
.modal-skel{padding:36px}
|
|
.modal-skel .skel{margin-bottom:16px}
|
|
|
|
/* ==================== FOOTER ==================== */
|
|
.footer{
|
|
border-top:1px solid var(--border-glass);
|
|
background:rgba(10,10,10,0.6);
|
|
padding:48px 0 32px;
|
|
margin-top:40px;
|
|
}
|
|
.footer-inner{
|
|
max-width:1400px;margin:0 auto;padding:0 32px;
|
|
display:flex;justify-content:space-between;align-items:flex-start;
|
|
gap:40px;flex-wrap:wrap;
|
|
}
|
|
.footer-brand{max-width:320px}
|
|
.footer-logo{
|
|
font-family:var(--font-mono);font-size:1.2rem;font-weight:700;
|
|
color:var(--neon-green);
|
|
text-shadow:0 0 10px rgba(0,255,136,0.4);
|
|
margin-bottom:12px;
|
|
}
|
|
.footer-desc{font-size:0.8rem;color:var(--text-muted);line-height:1.6}
|
|
.footer-links{display:flex;gap:60px;flex-wrap:wrap}
|
|
.footer-col h4{
|
|
font-size:0.8rem;font-weight:600;
|
|
color:var(--text-primary);
|
|
margin-bottom:14px;
|
|
letter-spacing:1px;
|
|
}
|
|
.footer-col a{
|
|
display:block;font-size:0.8rem;
|
|
color:var(--text-muted);
|
|
margin-bottom:8px;
|
|
transition:color 0.3s;
|
|
}
|
|
.footer-col a:hover{color:var(--neon-green)}
|
|
.footer-bottom{
|
|
max-width:1400px;margin:32px auto 0;padding:20px 32px 0;
|
|
border-top:1px solid var(--border-glass);
|
|
display:flex;justify-content:space-between;align-items:center;
|
|
flex-wrap:wrap;gap:12px;
|
|
}
|
|
.footer-copy{font-family:var(--font-mono);font-size:0.75rem;color:var(--text-muted)}
|
|
.footer-status{
|
|
display:flex;align-items:center;gap:8px;
|
|
font-family:var(--font-mono);font-size:0.75rem;
|
|
color:var(--neon-green);
|
|
}
|
|
.footer-status-dot{width:6px;height:6px;border-radius:50%;background:var(--neon-green);box-shadow:0 0 6px var(--neon-green);animation:blink 2s infinite}
|
|
|
|
/* ==================== REVEAL ANIMATION ==================== */
|
|
.reveal{opacity:0;transform:translateY(30px);transition:opacity 0.7s ease,transform 0.7s ease}
|
|
.reveal.visible{opacity:1;transform:translateY(0)}
|
|
|
|
/* ==================== EMPTY STATE ==================== */
|
|
.empty-state{
|
|
grid-column:1/-1;
|
|
text-align:center;padding:60px 20px;
|
|
color:var(--text-muted);
|
|
}
|
|
.empty-state-icon{font-size:3rem;margin-bottom:16px;opacity:0.3}
|
|
.empty-state-text{font-size:0.9rem}
|
|
|
|
/* ==================== TOAST ==================== */
|
|
.toast{
|
|
position:fixed;bottom:32px;left:50%;transform:translateX(-50%) translateY(100px);
|
|
background:rgba(13,13,13,0.95);
|
|
border:1px solid var(--neon-green);
|
|
border-radius:10px;
|
|
padding:12px 24px;
|
|
font-size:0.875rem;color:var(--neon-green);
|
|
box-shadow:0 0 20px rgba(0,255,136,0.2);
|
|
z-index:2000;
|
|
opacity:0;transition:all 0.3s;
|
|
backdrop-filter:blur(10px);
|
|
}
|
|
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
|
|
|
|
/* ==================== RESPONSIVE ==================== */
|
|
@media(max-width:900px){
|
|
.nav{display:none}
|
|
.mobile-toggle{display:block}
|
|
.hero-title{font-size:2.6rem}
|
|
.section{padding:56px 0}
|
|
.header-inner{padding:0 20px;gap:16px}
|
|
.container{padding:0 20px}
|
|
.hero-inner{padding:0 20px}
|
|
.hero-stats{gap:32px}
|
|
.hero-stat-num{font-size:1.6rem}
|
|
.footer-inner{padding:0 20px}
|
|
.footer-bottom{padding:20px 20px 0}
|
|
.modal{padding:24px}
|
|
.modal-header{flex-direction:column;align-items:center;text-align:center}
|
|
.modal-cat-row{justify-content:center}
|
|
}
|
|
@media(max-width:480px){
|
|
.hero-title{font-size:2rem}
|
|
.search-wrap{width:200px}
|
|
.library-toolbar{flex-direction:column;align-items:stretch}
|
|
.lib-sort{overflow-x:auto}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Animated Background -->
|
|
<div class="bg-layer">
|
|
<div class="bg-grid"></div>
|
|
<div class="bg-glow g1"></div>
|
|
<div class="bg-glow g2"></div>
|
|
<div class="bg-glow g3"></div>
|
|
</div>
|
|
<div class="particles" id="particles"></div>
|
|
|
|
<!-- Header -->
|
|
<header class="header">
|
|
<div class="header-inner">
|
|
<a href="#hero" class="logo"><span class="logo-dot"></span>APP HUB</a>
|
|
<nav class="nav" id="nav">
|
|
<a href="#hero" class="nav-link active" data-target="hero" data-i18n="nav_home">首页</a>
|
|
<a href="#library" class="nav-link" data-target="library" data-i18n="nav_explore">探索</a>
|
|
<a href="#featured" class="nav-link" data-target="featured" data-i18n="nav_featured">精选</a>
|
|
<a href="#new" class="nav-link" data-target="new" data-i18n="nav_new">新上架</a>
|
|
</nav>
|
|
<div class="search-wrap">
|
|
<svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
|
<input type="text" class="search-input" id="headerSearch" data-i18n-placeholder="search_placeholder" placeholder="搜索应用...">
|
|
</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>
|
|
<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M6 9l6 6 6-6"/></svg>
|
|
</button>
|
|
<div class="lang-dropdown" id="langDropdown">
|
|
<div class="lang-option" data-lang="zh-CN" onclick="switchLang('zh-CN')">
|
|
<svg class="check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
|
<span>简体中文</span>
|
|
</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="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
|
<span>繁體中文</span>
|
|
</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="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
|
<span>English</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button class="mobile-toggle" id="mobileToggle">☰</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Hero -->
|
|
<section class="hero" id="hero">
|
|
<div class="hero-inner">
|
|
<div class="hero-badge"><span class="hero-badge-dot"></span>SYSTEM ONLINE · v2.0.26</div>
|
|
<h1 class="hero-title">
|
|
<span class="glow-green" data-i18n="hero_line1">下一代</span><br>
|
|
<span class="glow-cyan" data-i18n="hero_line2">应用中心</span>
|
|
</h1>
|
|
<div class="hero-terminal">
|
|
<div class="terminal-line"><span class="terminal-prompt">$</span> <span class="terminal-text">init app_hub --mode=dark</span></div>
|
|
<div class="terminal-line"><span class="terminal-prompt">></span> <span class="terminal-text" data-i18n="hero_loading">loading app database...</span> <span class="terminal-cursor"></span></div>
|
|
<div class="terminal-line"><span class="terminal-prompt">></span> <span class="terminal-text" style="color:var(--neon-green)" data-i18n="hero_ready">ready · 18 apps indexed</span></div>
|
|
</div>
|
|
<div class="hero-actions">
|
|
<button class="btn-primary" onclick="scrollToSection('library')">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
|
<span data-i18n="hero_enter">进入商店</span>
|
|
</button>
|
|
<button class="btn-secondary" onclick="scrollToSection('featured')">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
|
<span data-i18n="hero_featured">精选推荐</span>
|
|
</button>
|
|
</div>
|
|
<div class="hero-stats">
|
|
<div class="hero-stat">
|
|
<div class="hero-stat-num" id="statApps">--</div>
|
|
<div class="hero-stat-label" data-i18n="stat_total_apps">应用总数</div>
|
|
</div>
|
|
<div class="hero-stat">
|
|
<div class="hero-stat-num" id="statCats">--</div>
|
|
<div class="hero-stat-label" data-i18n="stat_total_cats">分类数量</div>
|
|
</div>
|
|
<div class="hero-stat">
|
|
<div class="hero-stat-num" id="statDownloads">--</div>
|
|
<div class="hero-stat-label" data-i18n="stat_total_downloads">总下载量</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Category Filter -->
|
|
<section class="section" style="padding-top:0;padding-bottom:20px">
|
|
<div class="container">
|
|
<div class="section-header reveal">
|
|
<div class="section-title-wrap">
|
|
<h2 class="section-title"><span class="accent">//</span> <span data-i18n="section_categories">分类导航</span></h2>
|
|
<span class="section-tag">CATEGORIES</span>
|
|
</div>
|
|
<div class="section-line"></div>
|
|
</div>
|
|
<div class="category-bar" id="categoryBar">
|
|
<div class="skel" style="width:80px;height:36px;border-radius:24px"></div>
|
|
<div class="skel" style="width:80px;height:36px;border-radius:24px"></div>
|
|
<div class="skel" style="width:80px;height:36px;border-radius:24px"></div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Featured Apps -->
|
|
<section class="section" id="featured" style="padding-top:20px">
|
|
<div class="container">
|
|
<div class="section-header reveal">
|
|
<div class="section-title-wrap">
|
|
<h2 class="section-title"><span class="accent">//</span> <span data-i18n="section_featured">精选应用</span></h2>
|
|
<span class="section-tag">FEATURED</span>
|
|
</div>
|
|
<div class="section-line"></div>
|
|
</div>
|
|
<div class="featured-grid" id="featuredGrid">
|
|
<div class="skeleton-card"><div class="skel skel-icon"></div><div class="skel skel-line mid"></div><div class="skel skel-line short"></div><div class="skel skel-line"></div><div class="skel skel-line short"></div></div>
|
|
<div class="skeleton-card"><div class="skel skel-icon"></div><div class="skel skel-line mid"></div><div class="skel skel-line short"></div><div class="skel skel-line"></div><div class="skel skel-line short"></div></div>
|
|
<div class="skeleton-card"><div class="skel skel-icon"></div><div class="skel skel-line mid"></div><div class="skel skel-line short"></div><div class="skel skel-line"></div><div class="skel skel-line short"></div></div>
|
|
<div class="skeleton-card"><div class="skel skel-icon"></div><div class="skel skel-line mid"></div><div class="skel skel-line short"></div><div class="skel skel-line"></div><div class="skel skel-line short"></div></div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- New Apps -->
|
|
<section class="section" id="new" style="padding-top:20px">
|
|
<div class="container">
|
|
<div class="section-header reveal">
|
|
<div class="section-title-wrap">
|
|
<h2 class="section-title"><span class="accent">//</span> <span data-i18n="section_new">最新上架</span></h2>
|
|
<span class="section-tag">NEW</span>
|
|
</div>
|
|
<div class="section-line"></div>
|
|
</div>
|
|
<div class="new-scroll" id="newScroll">
|
|
<div class="skeleton-card" style="flex:0 0 300px"><div class="skel skel-icon"></div><div class="skel skel-line mid"></div><div class="skel skel-line short"></div></div>
|
|
<div class="skeleton-card" style="flex:0 0 300px"><div class="skel skel-icon"></div><div class="skel skel-line mid"></div><div class="skel skel-line short"></div></div>
|
|
<div class="skeleton-card" style="flex:0 0 300px"><div class="skel skel-icon"></div><div class="skel skel-line mid"></div><div class="skel skel-line short"></div></div>
|
|
<div class="skeleton-card" style="flex:0 0 300px"><div class="skel skel-icon"></div><div class="skel skel-line mid"></div><div class="skel skel-line short"></div></div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- App Library -->
|
|
<section class="section" id="library" style="padding-top:20px">
|
|
<div class="container">
|
|
<div class="section-header reveal">
|
|
<div class="section-title-wrap">
|
|
<h2 class="section-title"><span class="accent">//</span> <span data-i18n="section_library">应用库</span></h2>
|
|
<span class="section-tag">LIBRARY</span>
|
|
</div>
|
|
<div class="section-line"></div>
|
|
</div>
|
|
<div class="library-toolbar reveal">
|
|
<div class="lib-search">
|
|
<svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
|
<input type="text" class="search-input" id="libSearch" data-i18n-placeholder="search_lib" placeholder="搜索应用名称或开发者...">
|
|
</div>
|
|
<div class="lib-sort" id="libSort">
|
|
<button class="sort-btn active" data-sort="" data-i18n="sort_default">默认</button>
|
|
<button class="sort-btn" data-sort="rating" data-i18n="sort_rating">评分</button>
|
|
<button class="sort-btn" data-sort="downloads" data-i18n="sort_downloads">下载量</button>
|
|
<button class="sort-btn" data-sort="newest" data-i18n="sort_newest">最新</button>
|
|
</div>
|
|
</div>
|
|
<div class="lib-cat-filter reveal" id="libCatFilter" style="margin-bottom:24px"></div>
|
|
<div class="app-grid" id="libraryGrid">
|
|
<div class="skeleton-card"><div class="skel skel-icon"></div><div class="skel skel-line mid"></div><div class="skel skel-line short"></div><div class="skel skel-line"></div><div class="skel skel-line short"></div></div>
|
|
<div class="skeleton-card"><div class="skel skel-icon"></div><div class="skel skel-line mid"></div><div class="skel skel-line short"></div><div class="skel skel-line"></div><div class="skel skel-line short"></div></div>
|
|
<div class="skeleton-card"><div class="skel skel-icon"></div><div class="skel skel-line mid"></div><div class="skel skel-line short"></div><div class="skel skel-line"></div><div class="skel skel-line short"></div></div>
|
|
<div class="skeleton-card"><div class="skel skel-icon"></div><div class="skel skel-line mid"></div><div class="skel skel-line short"></div><div class="skel skel-line"></div><div class="skel skel-line short"></div></div>
|
|
<div class="skeleton-card"><div class="skel skel-icon"></div><div class="skel skel-line mid"></div><div class="skel skel-line short"></div><div class="skel skel-line"></div><div class="skel skel-line short"></div></div>
|
|
</div>
|
|
<div class="pagination" id="pagination"></div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Footer -->
|
|
<footer class="footer">
|
|
<div class="footer-inner">
|
|
<div class="footer-brand">
|
|
<div class="footer-logo">// APP HUB</div>
|
|
<p class="footer-desc" data-i18n="footer_desc">下一代应用下载中心,暗黑科技风格设计,为您提供最前沿的应用发现体验。安全、快速、极致。</p>
|
|
</div>
|
|
<div class="footer-links">
|
|
<div class="footer-col">
|
|
<h4 data-i18n="footer_product">产品</h4>
|
|
<a href="#featured" data-i18n="nav_featured">精选应用</a>
|
|
<a href="#new" data-i18n="nav_new">最新上架</a>
|
|
<a href="#library" data-i18n="section_library">应用库</a>
|
|
</div>
|
|
<div class="footer-col">
|
|
<h4 data-i18n="footer_categories">分类</h4>
|
|
<a href="#library" onclick="filterByCategory(1)" data-i18n="cat_social">社交通讯</a>
|
|
<a href="#library" onclick="filterByCategory(2)" data-i18n="cat_productivity">效率办公</a>
|
|
<a href="#library" onclick="filterByCategory(3)" data-i18n="cat_media">影音娱乐</a>
|
|
<a href="#library" onclick="filterByCategory(6)" data-i18n="cat_tools">工具开发</a>
|
|
</div>
|
|
<div class="footer-col">
|
|
<h4 data-i18n="footer_about">关于</h4>
|
|
<a href="#" data-i18n="footer_about_us">关于我们</a>
|
|
<a href="#" data-i18n="footer_privacy">隐私政策</a>
|
|
<a href="#" data-i18n="footer_terms">服务条款</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="footer-bottom">
|
|
<div class="footer-copy" data-i18n="footer_copyright">© 2026 APP HUB · All rights reserved</div>
|
|
<div class="footer-status"><span class="footer-status-dot"></span><span data-i18n="footer_status">SYSTEM OPERATIONAL</span></div>
|
|
</div>
|
|
</footer>
|
|
|
|
<!-- Modal -->
|
|
<div class="modal-overlay" id="modalOverlay">
|
|
<div class="modal" id="modal">
|
|
<button class="modal-close" onclick="closeModal()">✕</button>
|
|
<div id="modalContent">
|
|
<div class="modal-skel">
|
|
<div style="display:flex;gap:20px;margin-bottom:24px">
|
|
<div class="skel" style="width:80px;height:80px;border-radius:18px"></div>
|
|
<div style="flex:1">
|
|
<div class="skel skel-line mid" style="height:20px;margin-bottom:10px"></div>
|
|
<div class="skel skel-line short"></div>
|
|
</div>
|
|
</div>
|
|
<div class="skel skel-line"></div>
|
|
<div class="skel skel-line"></div>
|
|
<div class="skel skel-line short"></div>
|
|
<div class="skel skel-line"></div>
|
|
<div class="skel skel-line short"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toast -->
|
|
<div class="toast" id="toast"></div>
|
|
|
|
<script>
|
|
/* ==================== i18n ==================== */
|
|
var I18N_LOCALES = {
|
|
'zh-CN': {
|
|
nav_home: '首页',
|
|
nav_explore: '探索',
|
|
nav_featured: '精选',
|
|
nav_new: '新上架',
|
|
search_placeholder: '搜索应用...',
|
|
search_lib: '搜索应用名称或开发者...',
|
|
hero_line1: '下一代',
|
|
hero_line2: '应用中心',
|
|
hero_loading: 'loading app database...',
|
|
hero_ready: 'ready · 18 apps indexed',
|
|
hero_enter: '进入商店',
|
|
hero_featured: '精选推荐',
|
|
stat_total_apps: '应用总数',
|
|
stat_total_cats: '分类数量',
|
|
stat_total_downloads: '总下载量',
|
|
section_categories: '分类导航',
|
|
section_featured: '精选应用',
|
|
section_new: '最新上架',
|
|
section_library: '应用库',
|
|
cat_all: '全部',
|
|
cat_social: '社交通讯',
|
|
cat_productivity: '效率办公',
|
|
cat_media: '影音娱乐',
|
|
cat_tools: '工具开发',
|
|
sort_default: '默认',
|
|
sort_rating: '评分',
|
|
sort_downloads: '下载量',
|
|
sort_newest: '最新',
|
|
empty_featured: '暂无精选应用',
|
|
empty_new: '暂无新上架应用',
|
|
empty_library: '加载失败,请稍后重试',
|
|
empty_no_match: '未找到匹配的应用',
|
|
empty_tags: '暂无标签',
|
|
empty_features: '暂无特性信息',
|
|
modal_desc: '应用介绍',
|
|
modal_tags: '标签',
|
|
modal_features: '核心特性',
|
|
modal_screenshots: '应用截图',
|
|
modal_info: '应用信息',
|
|
modal_downloads: '下载量',
|
|
modal_size: '大小',
|
|
modal_version: '版本',
|
|
modal_update: '更新日期',
|
|
modal_platform: '平台',
|
|
modal_price: '价格',
|
|
modal_free: '免费',
|
|
modal_reviews: '评价',
|
|
modal_download: '下载应用',
|
|
modal_unknown_dev: '未知开发者',
|
|
modal_unknown_cat: '未分类',
|
|
modal_no_desc: '暂无描述',
|
|
footer_product: '产品',
|
|
footer_categories: '分类',
|
|
footer_about: '关于',
|
|
footer_about_us: '关于我们',
|
|
footer_privacy: '隐私政策',
|
|
footer_terms: '服务条款',
|
|
footer_desc: '下一代应用下载中心,暗黑科技风格设计,为您提供最前沿的应用发现体验。安全、快速、极致。',
|
|
footer_copyright: '\u00a9 2026 APP HUB \u00b7 All rights reserved',
|
|
footer_status: 'SYSTEM OPERATIONAL',
|
|
total_apps: '共 {n} 个应用'
|
|
},
|
|
'zh-TW': {
|
|
nav_home: '首頁',
|
|
nav_explore: '探索',
|
|
nav_featured: '精選',
|
|
nav_new: '新上架',
|
|
search_placeholder: '搜尋應用...',
|
|
search_lib: '搜尋應用名稱或開發者...',
|
|
hero_line1: '下一代',
|
|
hero_line2: '應用中心',
|
|
hero_loading: 'loading app database...',
|
|
hero_ready: 'ready \u00b7 18 apps indexed',
|
|
hero_enter: '進入商店',
|
|
hero_featured: '精選推薦',
|
|
stat_total_apps: '應用總數',
|
|
stat_total_cats: '分類數量',
|
|
stat_total_downloads: '總下載量',
|
|
section_categories: '分類導航',
|
|
section_featured: '精選應用',
|
|
section_new: '最新上架',
|
|
section_library: '應用庫',
|
|
cat_all: '全部',
|
|
cat_social: '社交通訊',
|
|
cat_productivity: '效率辦公',
|
|
cat_media: '影音娛樂',
|
|
cat_tools: '工具開發',
|
|
sort_default: '預設',
|
|
sort_rating: '評分',
|
|
sort_downloads: '下載量',
|
|
sort_newest: '最新',
|
|
empty_featured: '暫無精選應用',
|
|
empty_new: '暫無新上架應用',
|
|
empty_library: '載入失敗,請稍後重試',
|
|
empty_no_match: '未找到匹配的應用',
|
|
empty_tags: '暫無標籤',
|
|
empty_features: '暫無特性資訊',
|
|
modal_desc: '應用介紹',
|
|
modal_tags: '標籤',
|
|
modal_features: '核心特性',
|
|
modal_screenshots: '應用截圖',
|
|
modal_info: '應用資訊',
|
|
modal_downloads: '下載量',
|
|
modal_size: '大小',
|
|
modal_version: '版本',
|
|
modal_update: '更新日期',
|
|
modal_platform: '平台',
|
|
modal_price: '價格',
|
|
modal_free: '免費',
|
|
modal_reviews: '評價',
|
|
modal_download: '下載應用',
|
|
modal_unknown_dev: '未知開發者',
|
|
modal_unknown_cat: '未分類',
|
|
modal_no_desc: '暫無描述',
|
|
footer_product: '產品',
|
|
footer_categories: '分類',
|
|
footer_about: '關於',
|
|
footer_about_us: '關於我們',
|
|
footer_privacy: '隱私政策',
|
|
footer_terms: '服務條款',
|
|
footer_desc: '下一代應用下載中心,暗黑科技風格設計,為您提供最前沿的應用發現體驗。安全、快速、極致。',
|
|
footer_copyright: '\u00a9 2026 APP HUB \u00b7 All rights reserved',
|
|
footer_status: 'SYSTEM OPERATIONAL',
|
|
total_apps: '共 {n} 個應用'
|
|
},
|
|
'en': {
|
|
nav_home: 'Home',
|
|
nav_explore: 'Explore',
|
|
nav_featured: 'Featured',
|
|
nav_new: 'New',
|
|
search_placeholder: 'Search apps...',
|
|
search_lib: 'Search app name or developer...',
|
|
hero_line1: 'Next-Gen',
|
|
hero_line2: 'App Hub',
|
|
hero_loading: 'loading app database...',
|
|
hero_ready: 'ready \u00b7 18 apps indexed',
|
|
hero_enter: 'Enter Store',
|
|
hero_featured: 'Featured Picks',
|
|
stat_total_apps: 'Total Apps',
|
|
stat_total_cats: 'Categories',
|
|
stat_total_downloads: 'Total Downloads',
|
|
section_categories: 'Categories',
|
|
section_featured: 'Featured Apps',
|
|
section_new: 'New Arrivals',
|
|
section_library: 'App Library',
|
|
cat_all: 'All',
|
|
cat_social: 'Social',
|
|
cat_productivity: 'Productivity',
|
|
cat_media: 'Media & Entertainment',
|
|
cat_tools: 'Tools & Dev',
|
|
sort_default: 'Default',
|
|
sort_rating: 'Top Rated',
|
|
sort_downloads: 'Most Downloaded',
|
|
sort_newest: 'Newest',
|
|
empty_featured: 'No featured apps',
|
|
empty_new: 'No new apps',
|
|
empty_library: 'Load failed, please try again later',
|
|
empty_no_match: 'No matching apps found',
|
|
empty_tags: 'No tags',
|
|
empty_features: 'No features available',
|
|
modal_desc: 'Description',
|
|
modal_tags: 'Tags',
|
|
modal_features: 'Key Features',
|
|
modal_screenshots: 'Screenshots',
|
|
modal_info: 'App Info',
|
|
modal_downloads: 'Downloads',
|
|
modal_size: 'Size',
|
|
modal_version: 'Version',
|
|
modal_update: 'Updated',
|
|
modal_platform: 'Platform',
|
|
modal_price: 'Price',
|
|
modal_free: 'Free',
|
|
modal_reviews: 'Reviews',
|
|
modal_download: 'Download App',
|
|
modal_unknown_dev: 'Unknown Developer',
|
|
modal_unknown_cat: 'Uncategorized',
|
|
modal_no_desc: 'No description available',
|
|
footer_product: 'Product',
|
|
footer_categories: 'Categories',
|
|
footer_about: 'About',
|
|
footer_about_us: 'About Us',
|
|
footer_privacy: 'Privacy Policy',
|
|
footer_terms: 'Terms of Service',
|
|
footer_desc: 'Next-gen app download hub with dark tech-inspired design. Providing you with the most cutting-edge app discovery experience. Safe, fast, extreme.',
|
|
footer_copyright: '\u00a9 2026 APP HUB \u00b7 All rights reserved',
|
|
footer_status: 'SYSTEM OPERATIONAL',
|
|
total_apps: '{n} apps'
|
|
}
|
|
};
|
|
|
|
var LANG_LABELS = {'zh-CN': '中文', 'zh-TW': '繁中', 'en': 'EN'};
|
|
|
|
var 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('apphub_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 'zh-CN';
|
|
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';
|
|
document.getElementById('langLabel').textContent = LANG_LABELS[currentLang] || currentLang;
|
|
document.querySelectorAll('.lang-option').forEach(function(opt) {
|
|
opt.classList.toggle('active', opt.getAttribute('data-lang') === currentLang);
|
|
});
|
|
}
|
|
|
|
function switchLang(lang) {
|
|
if (!I18N_LOCALES[lang]) return;
|
|
currentLang = lang;
|
|
try { localStorage.setItem('apphub_lang', lang); } catch(e) {}
|
|
applyI18n();
|
|
if (typeof onLangChange === 'function') onLangChange(lang);
|
|
document.getElementById('langDropdown').classList.remove('open');
|
|
}
|
|
|
|
function toggleLangDropdown() {
|
|
document.getElementById('langDropdown').classList.toggle('open');
|
|
}
|
|
document.addEventListener('click', function(e) {
|
|
var sw = document.getElementById('langSwitcher');
|
|
if (sw && !sw.contains(e.target)) document.getElementById('langDropdown').classList.remove('open');
|
|
});
|
|
|
|
/* ==================== STATE ==================== */
|
|
const state = {
|
|
categories: [],
|
|
featured: [],
|
|
newApps: [],
|
|
library: { list: [], total: 0, page: 1, pageSize: 10 },
|
|
libCategory: 0,
|
|
libKeyword: '',
|
|
libSort: '',
|
|
loading: false
|
|
};
|
|
|
|
const API_BASE = '/api/v1/app';
|
|
|
|
/* ==================== API ==================== */
|
|
async function apiGet(path) {
|
|
try {
|
|
const res = await fetch(API_BASE + path);
|
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
const json = await res.json();
|
|
if (json.code === 0) return json.data;
|
|
throw new Error(json.message || t('empty_library'));
|
|
} catch (e) {
|
|
console.error('[API Error]', path, e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/* ==================== UTILS ==================== */
|
|
function formatNumber(n) {
|
|
if (n == null || isNaN(n)) return '0';
|
|
if (currentLang === 'en') {
|
|
if (n >= 1000000000) return (n / 1000000000).toFixed(1).replace(/\.0$/, '') + 'B';
|
|
if (n >= 1000000) return (n / 1000000).toFixed(1).replace(/\.0$/, '') + 'M';
|
|
if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'K';
|
|
return String(n);
|
|
}
|
|
if (n >= 100000000) return (n / 100000000).toFixed(1).replace(/\.0$/, '') + '\u4ebf';
|
|
if (n >= 10000) return (n / 10000).toFixed(1).replace(/\.0$/, '') + '\u4e07';
|
|
return String(n);
|
|
}
|
|
|
|
function hashString(str) {
|
|
let hash = 0;
|
|
for (let i = 0; i < str.length; i++) {
|
|
hash = ((hash << 5) - hash) + str.charCodeAt(i);
|
|
hash = hash & hash;
|
|
}
|
|
return Math.abs(hash);
|
|
}
|
|
|
|
function isIconURL(icon) {
|
|
if (!icon) return false;
|
|
var v = String(icon).trim();
|
|
return v.indexOf('/uploads/') === 0 || v.indexOf('http://') === 0 || v.indexOf('https://') === 0;
|
|
}
|
|
|
|
function generateIconHTML(app, size) {
|
|
var iconURL = isIconURL(app.icon) ? String(app.icon).trim() : '';
|
|
const hash = hashString(app.name || 'A');
|
|
const hue1 = hash % 360;
|
|
const hue2 = (hash * 7 + 120) % 360;
|
|
const initial = (app.name || '?').charAt(0);
|
|
const borderHue = hue1;
|
|
var imgHTML = iconURL ? '<img class="app-icon-img" src="' + escapeHtml(iconURL) + '" alt="' + escapeHtml(app.name) + '" loading="lazy" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'">' : '';
|
|
var placeholderStyle = 'background:linear-gradient(135deg,hsl(' + hue1 + ',60%,12%),hsl(' + hue2 + ',55%,18%));border-color:hsl(' + borderHue + ',80%,45%);box-shadow:0 0 12px hsla(' + borderHue + ',80%,50%,0.3);' + (iconURL ? 'display:none' : '');
|
|
var placeholderHTML = '<div class="app-icon-placeholder" style="' + placeholderStyle + '">' +
|
|
'<span style="color:hsl(' + hue1 + ',85%,60%);text-shadow:0 0 8px hsla(' + hue1 + ',85%,50%,0.5)">' + escapeHtml(initial) + '</span>' +
|
|
'</div>';
|
|
return imgHTML + placeholderHTML;
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
if (str == null) return '';
|
|
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
}
|
|
|
|
function parseJSONArr(str) {
|
|
try {
|
|
const arr = JSON.parse(str);
|
|
return Array.isArray(arr) ? arr : [];
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function starRatingHTML(rating) {
|
|
const r = parseFloat(rating) || 0;
|
|
let html = '';
|
|
for (let i = 1; i <= 5; i++) {
|
|
if (r >= i) {
|
|
html += '<span class="star filled">★</span>';
|
|
} else if (r >= i - 0.5) {
|
|
html += '<span class="star half">★</span>';
|
|
} else {
|
|
html += '<span class="star">★</span>';
|
|
}
|
|
}
|
|
return html;
|
|
}
|
|
|
|
function debounce(fn, delay) {
|
|
let timer = null;
|
|
return function () {
|
|
const ctx = this, args = arguments;
|
|
clearTimeout(timer);
|
|
timer = setTimeout(function () { fn.apply(ctx, args); }, delay);
|
|
};
|
|
}
|
|
|
|
function showToast(msg) {
|
|
const t = document.getElementById('toast');
|
|
t.textContent = msg;
|
|
t.classList.add('show');
|
|
clearTimeout(t._timer);
|
|
t._timer = setTimeout(function () { t.classList.remove('show'); }, 2500);
|
|
}
|
|
|
|
function scrollToSection(id) {
|
|
const el = document.getElementById(id);
|
|
if (el) el.scrollIntoView({ behavior: 'smooth' });
|
|
}
|
|
|
|
/* ==================== CARD RENDERERS ==================== */
|
|
function appCardHTML(app, opts) {
|
|
opts = opts || {};
|
|
const isFeatured = opts.featured;
|
|
const isNew = opts.newCard;
|
|
const cardClass = isFeatured ? 'featured-card' : (isNew ? 'new-card' : 'app-card');
|
|
|
|
let badge = '';
|
|
if (app.is_new && !isNew) {
|
|
badge = '<span class="new-badge">NEW</span>';
|
|
}
|
|
if (isFeatured) {
|
|
badge = '<span class="feat-badge">FEATURED</span>';
|
|
}
|
|
|
|
return '<div class="' + cardClass + ' reveal visible" onclick="openModal(' + app.id + ')">' +
|
|
badge +
|
|
'<div class="card-top">' +
|
|
'<div class="app-icon">' + generateIconHTML(app) + '</div>' +
|
|
'<div class="card-name-row">' +
|
|
'<div class="card-name">' + escapeHtml(app.name) + '</div>' +
|
|
'<div class="card-dev">' + escapeHtml(app.developer || t('modal_unknown_dev')) + '</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<span class="card-cat">' + escapeHtml(app.category_name || t('modal_unknown_cat')) + '</span>' +
|
|
'<p class="card-desc">' + escapeHtml(app.description || '') + '</p>' +
|
|
'<div class="card-bottom">' +
|
|
'<div class="card-rating">' +
|
|
'<div class="stars">' + starRatingHTML(app.rating) + '</div>' +
|
|
'<span class="rating-num">' + (parseFloat(app.rating) || 0).toFixed(1) + '</span>' +
|
|
'</div>' +
|
|
'<div class="card-downloads">' +
|
|
'<svg class="dl-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align:-1px"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> ' +
|
|
formatNumber(app.downloads) +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
}
|
|
|
|
/* ==================== CATEGORY BAR ==================== */
|
|
function renderCategoryBar() {
|
|
const bar = document.getElementById('categoryBar');
|
|
let html = '<button class="cat-pill active" data-cid="0" onclick="scrollToSection(\'library\')">' + escapeHtml(t('cat_all')) + '</button>';
|
|
state.categories.forEach(function (cat) {
|
|
html += '<button class="cat-pill" data-cid="' + cat.id + '" onclick="filterByCategory(' + cat.id + ')">' + escapeHtml(cat.name) + '</button>';
|
|
});
|
|
bar.innerHTML = html;
|
|
}
|
|
|
|
function renderLibCatFilter() {
|
|
const wrap = document.getElementById('libCatFilter');
|
|
let html = '<button class="cat-pill active" data-libcid="0">' + escapeHtml(t('cat_all')) + '</button>';
|
|
state.categories.forEach(function (cat) {
|
|
html += '<button class="cat-pill" data-libcid="' + cat.id + '">' + escapeHtml(cat.name) + '</button>';
|
|
});
|
|
wrap.innerHTML = html;
|
|
wrap.querySelectorAll('.cat-pill').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
wrap.querySelectorAll('.cat-pill').forEach(function (b) { b.classList.remove('active'); });
|
|
btn.classList.add('active');
|
|
state.libCategory = parseInt(btn.dataset.libcid, 10);
|
|
state.library.page = 1;
|
|
loadLibrary();
|
|
});
|
|
});
|
|
}
|
|
|
|
function filterByCategory(cid) {
|
|
state.libCategory = cid;
|
|
state.library.page = 1;
|
|
const wrap = document.getElementById('libCatFilter');
|
|
if (wrap) {
|
|
wrap.querySelectorAll('.cat-pill').forEach(function (b) {
|
|
b.classList.toggle('active', parseInt(b.dataset.libcid, 10) === cid);
|
|
});
|
|
}
|
|
scrollToSection('library');
|
|
loadLibrary();
|
|
}
|
|
|
|
/* ==================== FEATURED ==================== */
|
|
function renderFeatured() {
|
|
const grid = document.getElementById('featuredGrid');
|
|
if (!state.featured || state.featured.length === 0) {
|
|
grid.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🔍</div><div class="empty-state-text">' + esc(t('empty_featured')) + '</div></div>';
|
|
return;
|
|
}
|
|
let html = '';
|
|
state.featured.forEach(function (app) {
|
|
html += appCardHTML(app, { featured: true });
|
|
});
|
|
grid.innerHTML = html;
|
|
}
|
|
|
|
/* ==================== NEW APPS ==================== */
|
|
function renderNew() {
|
|
const scroll = document.getElementById('newScroll');
|
|
if (!state.newApps || state.newApps.length === 0) {
|
|
scroll.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🔍</div><div class="empty-state-text">' + esc(t('empty_new')) + '</div></div>';
|
|
return;
|
|
}
|
|
let html = '';
|
|
state.newApps.forEach(function (app) {
|
|
html += appCardHTML(app, { newCard: true });
|
|
});
|
|
scroll.innerHTML = html;
|
|
}
|
|
|
|
/* ==================== LIBRARY ==================== */
|
|
async function loadLibrary() {
|
|
if (state.loading) return;
|
|
state.loading = true;
|
|
const grid = document.getElementById('libraryGrid');
|
|
let skel = '';
|
|
for (let i = 0; i < 10; i++) {
|
|
skel += '<div class="skeleton-card"><div class="skel skel-icon"></div><div class="skel skel-line mid"></div><div class="skel skel-line short"></div><div class="skel skel-line"></div><div class="skel skel-line short"></div></div>';
|
|
}
|
|
grid.innerHTML = skel;
|
|
|
|
const params = new URLSearchParams();
|
|
if (state.libCategory) params.set('category_id', state.libCategory);
|
|
if (state.libKeyword) params.set('keyword', state.libKeyword);
|
|
if (state.libSort) params.set('sort', state.libSort);
|
|
params.set('page', state.library.page);
|
|
params.set('page_size', state.library.pageSize);
|
|
|
|
const data = await apiGet('/apps?' + params.toString());
|
|
state.loading = false;
|
|
|
|
if (!data || !data.list) {
|
|
grid.innerHTML = '<div class="empty-state"><div class="empty-state-icon">⚠</div><div class="empty-state-text">' + esc(t('empty_library')) + '</div></div>';
|
|
document.getElementById('pagination').innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
state.library.list = data.list;
|
|
state.library.total = data.total || 0;
|
|
state.library.page = data.page || 1;
|
|
state.library.pageSize = data.page_size || 10;
|
|
|
|
if (data.list.length === 0) {
|
|
grid.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🔍</div><div class="empty-state-text">' + esc(t('empty_no_match')) + '</div></div>';
|
|
document.getElementById('pagination').innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
data.list.forEach(function (app) {
|
|
html += appCardHTML(app);
|
|
});
|
|
grid.innerHTML = html;
|
|
renderPagination();
|
|
}
|
|
|
|
function renderPagination() {
|
|
const wrap = document.getElementById('pagination');
|
|
const total = state.library.total;
|
|
const pageSize = state.library.pageSize;
|
|
const page = state.library.page;
|
|
const totalPages = Math.ceil(total / pageSize);
|
|
|
|
if (totalPages <= 1) {
|
|
wrap.innerHTML = '<span class="page-info">' + t('total_apps').replace('{n}', total) + '</span>';
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
html += '<button class="page-btn" onclick="goToPage(' + (page - 1) + ')" ' + (page <= 1 ? 'disabled' : '') + '>‹</button>';
|
|
|
|
const maxVisible = 5;
|
|
let start = Math.max(1, page - Math.floor(maxVisible / 2));
|
|
let end = Math.min(totalPages, start + maxVisible - 1);
|
|
if (end - start < maxVisible - 1) start = Math.max(1, end - maxVisible + 1);
|
|
|
|
if (start > 1) {
|
|
html += '<button class="page-btn" onclick="goToPage(1)">1</button>';
|
|
if (start > 2) html += '<span class="page-info">...</span>';
|
|
}
|
|
for (let i = start; i <= end; i++) {
|
|
html += '<button class="page-btn ' + (i === page ? 'active' : '') + '" onclick="goToPage(' + i + ')">' + i + '</button>';
|
|
}
|
|
if (end < totalPages) {
|
|
if (end < totalPages - 1) html += '<span class="page-info">...</span>';
|
|
html += '<button class="page-btn" onclick="goToPage(' + totalPages + ')">' + totalPages + '</button>';
|
|
}
|
|
|
|
html += '<button class="page-btn" onclick="goToPage(' + (page + 1) + ')" ' + (page >= totalPages ? 'disabled' : '') + '>›</button>';
|
|
html += '<span class="page-info">' + page + ' / ' + totalPages + '</span>';
|
|
wrap.innerHTML = html;
|
|
}
|
|
|
|
function goToPage(p) {
|
|
const totalPages = Math.ceil(state.library.total / state.library.pageSize);
|
|
if (p < 1 || p > totalPages) return;
|
|
state.library.page = p;
|
|
loadLibrary();
|
|
document.getElementById('library').scrollIntoView({ behavior: 'smooth' });
|
|
}
|
|
|
|
/* ==================== MODAL ==================== */
|
|
async function openModal(appId) {
|
|
const overlay = document.getElementById('modalOverlay');
|
|
const content = document.getElementById('modalContent');
|
|
overlay.classList.add('open');
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
content.innerHTML =
|
|
'<div class="modal-skel">' +
|
|
'<div style="display:flex;gap:20px;margin-bottom:24px">' +
|
|
'<div class="skel" style="width:80px;height:80px;border-radius:18px"></div>' +
|
|
'<div style="flex:1">' +
|
|
'<div class="skel skel-line mid" style="height:20px;margin-bottom:10px"></div>' +
|
|
'<div class="skel skel-line short"></div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="skel skel-line"></div><div class="skel skel-line"></div><div class="skel skel-line short"></div>' +
|
|
'<div class="skel skel-line"></div><div class="skel skel-line short"></div>' +
|
|
'</div>';
|
|
|
|
const app = await apiGet('/apps/' + appId);
|
|
if (!app) {
|
|
content.innerHTML = '<div class="empty-state"><div class="empty-state-icon">⚠</div><div class="empty-state-text">' + esc(t('empty_library')) + '</div></div>';
|
|
return;
|
|
}
|
|
|
|
const tags = parseJSONArr(app.tags);
|
|
const features = parseJSONArr(app.features);
|
|
const screenshots = parseJSONArr(app.screenshots);
|
|
|
|
let tagsHTML = '';
|
|
if (tags.length) {
|
|
tagsHTML = tags.map(function (tag) { return '<span class="tag-pill">' + escapeHtml(tag) + '</span>'; }).join('');
|
|
} else {
|
|
tagsHTML = '<span class="tag-pill" style="opacity:0.5">' + t('empty_tags') + '</span>';
|
|
}
|
|
|
|
let featuresHTML = '';
|
|
if (features.length) {
|
|
featuresHTML = features.map(function (f) {
|
|
return '<li class="feature-item"><span class="feature-check">▸</span>' + escapeHtml(f) + '</li>';
|
|
}).join('');
|
|
} else {
|
|
featuresHTML = '<li class="feature-item" style="opacity:0.5"><span class="feature-check">▸</span>' + t('empty_features') + '</li>';
|
|
}
|
|
|
|
let screenshotsHTML = '';
|
|
if (screenshots.length) {
|
|
screenshotsHTML = '<div class="modal-section"><div class="modal-section-title">' + t('modal_screenshots') + '</div><div style="display:flex;gap:10px;overflow-x:auto;padding-bottom:8px">' +
|
|
screenshots.map(function (s) {
|
|
return '<div style="flex:0 0 auto;min-width:200px;height:120px;background:rgba(255,255,255,0.03);border:1px solid var(--border-glass);border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:0.75rem;color:var(--text-muted);padding:12px;text-align:center">' + escapeHtml(s) + '</div>';
|
|
}).join('') +
|
|
'</div></div>';
|
|
}
|
|
|
|
content.innerHTML =
|
|
'<div class="modal-header">' +
|
|
'<div class="app-icon modal-icon">' + generateIconHTML(app) + '</div>' +
|
|
'<div class="modal-title-area">' +
|
|
'<h2 class="modal-name">' + escapeHtml(app.name) + '</h2>' +
|
|
'<div class="modal-dev">' + escapeHtml(app.developer || t('modal_unknown_dev')) + '</div>' +
|
|
'<div class="modal-cat-row">' +
|
|
'<span class="card-cat">' + escapeHtml(app.category_name || t('modal_unknown_cat')) + '</span>' +
|
|
(app.is_new ? '<span class="new-badge" style="position:static">NEW</span>' : '') +
|
|
(app.is_featured ? '<span class="feat-badge" style="position:static">FEATURED</span>' : '') +
|
|
'</div>' +
|
|
'<div class="modal-rating-row" style="margin-top:8px">' +
|
|
'<div class="stars">' + starRatingHTML(app.rating) + '</div>' +
|
|
'<span class="modal-rating-num">' + (parseFloat(app.rating) || 0).toFixed(1) + '</span>' +
|
|
'<span class="modal-reviews">(' + formatNumber(app.reviews) + ' ' + t('modal_reviews') + ')</span>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="modal-section">' +
|
|
'<div class="modal-section-title">' + t('modal_desc') + '</div>' +
|
|
'<p class="modal-desc">' + escapeHtml(app.description || t('modal_no_desc')) + '</p>' +
|
|
'</div>' +
|
|
'<div class="modal-section">' +
|
|
'<div class="modal-section-title">' + t('modal_tags') + '</div>' +
|
|
'<div class="tag-row">' + tagsHTML + '</div>' +
|
|
'</div>' +
|
|
'<div class="modal-section">' +
|
|
'<div class="modal-section-title">' + t('modal_features') + '</div>' +
|
|
'<ul class="feature-list">' + featuresHTML + '</ul>' +
|
|
'</div>' +
|
|
screenshotsHTML +
|
|
'<div class="modal-section">' +
|
|
'<div class="modal-section-title">' + t('modal_info') + '</div>' +
|
|
'<div class="modal-stats">' +
|
|
'<div class="stat-box"><div class="stat-label">' + t('modal_downloads') + '</div><div class="stat-value">' + formatNumber(app.downloads) + '</div></div>' +
|
|
'<div class="stat-box"><div class="stat-label">' + t('modal_size') + '</div><div class="stat-value">' + escapeHtml(app.size || '-') + '</div></div>' +
|
|
'<div class="stat-box"><div class="stat-label">' + t('modal_version') + '</div><div class="stat-value">' + escapeHtml(app.version || '-') + '</div></div>' +
|
|
'<div class="stat-box"><div class="stat-label">' + t('modal_update') + '</div><div class="stat-value">' + escapeHtml(app.update_date || '-') + '</div></div>' +
|
|
'<div class="stat-box"><div class="stat-label">' + t('modal_platform') + '</div><div class="stat-value" style="font-size:0.8rem">' + escapeHtml(app.platform || '-') + '</div></div>' +
|
|
'<div class="stat-box"><div class="stat-label">' + t('modal_price') + '</div><div class="stat-value">' + escapeHtml(app.price || t('modal_free')) + '</div></div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<button class="modal-download" onclick="showToast(\'' + escapeHtml(t('modal_download')) + ' ' + escapeHtml(app.name) + ' ' + escapeHtml(app.version || '') + '\')">' +
|
|
'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>' +
|
|
t('modal_download') +
|
|
'</button>';
|
|
}
|
|
|
|
function closeModal() {
|
|
document.getElementById('modalOverlay').classList.remove('open');
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
/* ==================== PARTICLES ==================== */
|
|
function initParticles() {
|
|
const container = document.getElementById('particles');
|
|
const colors = ['#00ff88', '#00d4ff', '#00ff88', '#00d4ff', '#a855f7'];
|
|
for (let i = 0; i < 25; i++) {
|
|
const p = document.createElement('div');
|
|
p.className = 'particle';
|
|
p.style.left = Math.random() * 100 + '%';
|
|
p.style.animationDuration = (15 + Math.random() * 20) + 's';
|
|
p.style.animationDelay = (Math.random() * 20) + 's';
|
|
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
p.style.background = color;
|
|
p.style.boxShadow = '0 0 6px ' + color;
|
|
const size = 2 + Math.random() * 3;
|
|
p.style.width = size + 'px';
|
|
p.style.height = size + 'px';
|
|
container.appendChild(p);
|
|
}
|
|
}
|
|
|
|
/* ==================== INTERSECTION OBSERVER ==================== */
|
|
function initObserver() {
|
|
const observer = new IntersectionObserver(function (entries) {
|
|
entries.forEach(function (entry) {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add('visible');
|
|
}
|
|
});
|
|
}, { threshold: 0.08, rootMargin: '0px 0px -40px 0px' });
|
|
|
|
function observeAll() {
|
|
document.querySelectorAll('.reveal:not(.visible)').forEach(function (el) {
|
|
observer.observe(el);
|
|
});
|
|
}
|
|
observeAll();
|
|
window._observeAll = observeAll;
|
|
}
|
|
|
|
/* ==================== NAV SCROLLSPY ==================== */
|
|
function initNavScroll() {
|
|
const navLinks = document.querySelectorAll('.nav-link');
|
|
const sections = ['hero', 'featured', 'new', 'library'];
|
|
|
|
window.addEventListener('scroll', function () {
|
|
let current = 'hero';
|
|
const scrollY = window.scrollY + 120;
|
|
sections.forEach(function (id) {
|
|
const el = document.getElementById(id);
|
|
if (el && el.offsetTop <= scrollY) current = id;
|
|
});
|
|
navLinks.forEach(function (link) {
|
|
link.classList.toggle('active', link.dataset.target === current);
|
|
});
|
|
});
|
|
}
|
|
|
|
/* ==================== onLangChange ==================== */
|
|
function onLangChange() {
|
|
applyI18n();
|
|
renderCategoryBar();
|
|
renderLibCatFilter();
|
|
renderFeatured();
|
|
renderNew();
|
|
loadLibrary();
|
|
}
|
|
|
|
/* ==================== INIT ==================== */
|
|
async function init() {
|
|
applyI18n();
|
|
initParticles();
|
|
initObserver();
|
|
initNavScroll();
|
|
|
|
const headerSearch = document.getElementById('headerSearch');
|
|
const libSearch = document.getElementById('libSearch');
|
|
headerSearch.addEventListener('input', debounce(function (e) {
|
|
const val = e.target.value.trim();
|
|
libSearch.value = val;
|
|
state.libKeyword = val;
|
|
state.library.page = 1;
|
|
state.libSort = state.libSort || '';
|
|
loadLibrary();
|
|
scrollToSection('library');
|
|
}, 300));
|
|
|
|
libSearch.addEventListener('input', debounce(function (e) {
|
|
state.libKeyword = e.target.value.trim();
|
|
state.library.page = 1;
|
|
loadLibrary();
|
|
}, 300));
|
|
|
|
document.querySelectorAll('#libSort .sort-btn').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
document.querySelectorAll('#libSort .sort-btn').forEach(function (b) { b.classList.remove('active'); });
|
|
btn.classList.add('active');
|
|
state.libSort = btn.dataset.sort;
|
|
state.library.page = 1;
|
|
loadLibrary();
|
|
});
|
|
});
|
|
|
|
document.getElementById('modalOverlay').addEventListener('click', function (e) {
|
|
if (e.target === this) closeModal();
|
|
});
|
|
document.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Escape') closeModal();
|
|
});
|
|
|
|
document.getElementById('mobileToggle').addEventListener('click', function () {
|
|
const nav = document.getElementById('nav');
|
|
if (nav.style.display === 'flex') {
|
|
nav.style.display = '';
|
|
} else {
|
|
nav.style.display = 'flex';
|
|
nav.style.position = 'absolute';
|
|
nav.style.top = '72px';
|
|
nav.style.left = '0';
|
|
nav.style.right = '0';
|
|
nav.style.flexDirection = 'column';
|
|
nav.style.background = 'rgba(13,13,13,0.95)';
|
|
nav.style.padding = '16px';
|
|
nav.style.borderBottom = '1px solid var(--border-glass)';
|
|
nav.style.zIndex = '99';
|
|
}
|
|
});
|
|
|
|
const home = await apiGet('/home');
|
|
if (home) {
|
|
state.categories = home.categories || [];
|
|
state.featured = home.featured || [];
|
|
state.newApps = home.new || [];
|
|
|
|
renderCategoryBar();
|
|
renderLibCatFilter();
|
|
renderFeatured();
|
|
renderNew();
|
|
|
|
document.getElementById('statCats').textContent = state.categories.length;
|
|
const totalDownloads = (state.featured.concat(state.newApps)).reduce(function (sum, app) {
|
|
return sum + (app.downloads || 0);
|
|
}, 0);
|
|
document.getElementById('statApps').textContent = '18+';
|
|
document.getElementById('statDownloads').textContent = formatNumber(totalDownloads || 560000000);
|
|
|
|
if (window._observeAll) window._observeAll();
|
|
} else {
|
|
const cats = await apiGet('/categories');
|
|
if (cats) {
|
|
state.categories = cats;
|
|
renderCategoryBar();
|
|
renderLibCatFilter();
|
|
}
|
|
const featured = await apiGet('/featured?limit=8');
|
|
if (featured) {
|
|
state.featured = featured;
|
|
renderFeatured();
|
|
}
|
|
const newApps = await apiGet('/new?limit=4');
|
|
if (newApps) {
|
|
state.newApps = newApps;
|
|
renderNew();
|
|
}
|
|
document.getElementById('statCats').textContent = state.categories.length;
|
|
document.getElementById('statApps').textContent = '--';
|
|
document.getElementById('statDownloads').textContent = '--';
|
|
}
|
|
|
|
loadLibrary();
|
|
|
|
setTimeout(async function () {
|
|
const libData = await apiGet('/apps?page=1&page_size=1');
|
|
if (libData) {
|
|
document.getElementById('statApps').textContent = libData.total || '18+';
|
|
}
|
|
}, 500);
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
</script>
|
|
</body>
|
|
</html>
|