Files
web_template/web/dist/app1/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

1580 lines
81 KiB
HTML
Raw Permalink 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>应用商店 - 发现精彩应用</title>
<style>
/* ============ Reset & Base ============ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--blue: #0071e3;
--blue-light: #2997ff;
--blue-dark: #0058b0;
--bg: #ffffff;
--bg-soft: #f5f5f7;
--bg-softer: #fafafa;
--text: #1d1d1f;
--text-secondary: #6e6e73;
--text-tertiary: #86868b;
--border: #d2d2d7;
--border-soft: #e8e8ed;
--radius-sm: 10px;
--radius-md: 14px;
--radius-lg: 18px;
--radius-xl: 24px;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.03);
--shadow-md: 0 4px 16px rgba(0,0,0,0.06), 0 2px 6px rgba(0,0,0,0.04);
--shadow-lg: 0 12px 40px rgba(0,0,0,0.10), 0 4px 12px rgba(0,0,0,0.05);
--header-h: 60px;
--max-w: 1200px;
}
html { scroll-behavior: smooth; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', 'PingFang SC', 'Microsoft YaHei', sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a { color: inherit; text-decoration: none; }
button { font-family: inherit; cursor: pointer; border: none; background: none; }
img { max-width: 100%; display: block; }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #c7c7cc; border-radius: 8px; }
::-webkit-scrollbar-thumb:hover { background: #a1a1a6; }
.container { max-width: var(--max-w); margin: 0 auto; padding: 0 24px; }
/* ============ Header ============ */
.site-header {
position: sticky; top: 0; z-index: 100;
height: var(--header-h);
background: rgba(255,255,255,0.82);
backdrop-filter: saturate(180%) blur(20px);
-webkit-backdrop-filter: saturate(180%) blur(20px);
border-bottom: 1px solid var(--border-soft);
}
.header-inner {
height: 100%; max-width: var(--max-w); margin: 0 auto; padding: 0 24px;
display: flex; align-items: center; gap: 32px;
}
.logo {
font-size: 19px; font-weight: 700; letter-spacing: -0.3px;
display: flex; align-items: center; gap: 8px; white-space: nowrap;
}
.logo-mark {
width: 30px; height: 30px; border-radius: 8px;
background: linear-gradient(135deg, var(--blue-light), var(--blue));
display: grid; place-items: center; color: #fff; font-size: 16px;
box-shadow: 0 2px 8px rgba(0,113,227,0.35);
}
.main-nav { display: flex; gap: 4px; }
.nav-link {
padding: 8px 14px; border-radius: 980px; font-size: 14px; font-weight: 500;
color: var(--text-secondary); transition: all .25s ease; white-space: nowrap;
}
.nav-link:hover { background: var(--bg-soft); color: var(--text); }
.nav-link.active { color: var(--blue); background: rgba(0,113,227,0.08); }
.header-search {
margin-left: auto; position: relative; flex: 0 1 280px;
}
.header-search input {
width: 100%; height: 38px; border-radius: 980px;
border: 1px solid var(--border-soft); background: var(--bg-soft);
padding: 0 16px 0 38px; font-size: 14px; color: var(--text);
transition: all .25s ease; outline: none;
}
.header-search input::placeholder { color: var(--text-tertiary); }
.header-search input:focus { background: #fff; border-color: var(--blue-light); box-shadow: 0 0 0 3px rgba(0,113,227,0.12); }
.header-search svg { position: absolute; left: 13px; top: 50%; transform: translateY(-50%); color: var(--text-tertiary); }
@media (max-width: 720px) {
.main-nav { display: none; }
.header-search { flex: 1; }
}
/* ============ 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: 13px; font-weight: 500; color: var(--text-secondary);
border: 1px solid var(--border-soft); background: var(--bg-soft);
transition: all .25s ease;
}
.lang-btn:hover { background: #ededf0; color: var(--text); border-color: var(--border); }
.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: #fff; border: 1px solid var(--border-soft); border-radius: var(--radius-md);
box-shadow: var(--shadow-lg); 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: var(--radius-sm); font-size: 14px; color: var(--text-secondary);
cursor: pointer; transition: all .15s;
}
.lang-option:hover { background: var(--bg-soft); color: var(--text); }
.lang-option.active { color: var(--blue); font-weight: 600; }
.lang-option .check { width: 16px; height: 16px; opacity: 0; transition: opacity .2s; }
.lang-option.active .check { opacity: 1; }
/* ============ Hero ============ */
.hero {
padding: 72px 0 56px; text-align: center; position: relative; overflow: hidden;
background:
radial-gradient(120% 120% at 50% -20%, rgba(41,151,255,0.10), transparent 60%),
linear-gradient(180deg, #fbfbfd 0%, #ffffff 100%);
}
.hero::after {
content: ''; position: absolute; left: 50%; bottom: -60px; transform: translateX(-50%);
width: 600px; height: 200px; border-radius: 50%;
background: radial-gradient(closest-side, rgba(0,113,227,0.07), transparent);
filter: blur(10px);
}
.hero-eyebrow {
display: inline-block; font-size: 13px; font-weight: 600; color: var(--blue);
background: rgba(0,113,227,0.08); padding: 6px 14px; border-radius: 980px; margin-bottom: 20px;
}
.hero h1 {
font-size: clamp(34px, 5vw, 52px); font-weight: 700; letter-spacing: -1px; line-height: 1.1;
background: linear-gradient(180deg, #1d1d1f 0%, #43434a 100%);
-webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
}
.hero p {
font-size: clamp(16px, 2vw, 19px); color: var(--text-secondary); margin-top: 16px; max-width: 560px; margin-left: auto; margin-right: auto;
}
.hero-cta {
margin-top: 30px; display: inline-flex; align-items: center; gap: 8px;
height: 46px; padding: 0 26px; border-radius: 980px; font-size: 15px; font-weight: 600;
background: linear-gradient(135deg, var(--blue-light), var(--blue)); color: #fff;
box-shadow: 0 8px 20px rgba(0,113,227,0.30); transition: transform .2s ease, box-shadow .2s ease;
}
.hero-cta:hover { transform: translateY(-2px); box-shadow: 0 12px 28px rgba(0,113,227,0.38); }
.hero-cta:active { transform: translateY(0); }
/* ============ Section common ============ */
.section { padding: 56px 0; }
.section-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 28px; gap: 16px; }
.section-title { font-size: 26px; font-weight: 700; letter-spacing: -0.5px; }
.section-sub { font-size: 14px; color: var(--text-tertiary); margin-top: 4px; }
.section-more { font-size: 14px; color: var(--blue); font-weight: 500; transition: opacity .2s; white-space: nowrap; }
.section-more:hover { opacity: 0.7; }
/* ============ Category pills ============ */
.cat-bar { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 6px; margin: 0 -24px; padding-left: 24px; padding-right: 24px; -webkit-overflow-scrolling: touch; }
.cat-bar::-webkit-scrollbar { height: 0; }
.cat-pill {
flex-shrink: 0; height: 36px; padding: 0 16px; border-radius: 980px;
background: var(--bg-soft); color: var(--text-secondary); font-size: 14px; font-weight: 500;
display: inline-flex; align-items: center; gap: 6px; transition: all .25s ease; white-space: nowrap;
border: 1px solid transparent;
}
.cat-pill:hover { background: #ededf0; }
.cat-pill.active { background: var(--text); color: #fff; }
.cat-pill .cat-emoji { font-size: 15px; }
/* ============ Featured ============ */
.featured-grid { display: grid; grid-template-columns: 2fr 1fr; grid-template-rows: auto auto; gap: 20px; }
.feature-big { grid-row: span 2; min-height: 360px; border-radius: var(--radius-xl); overflow: hidden; position: relative; cursor: pointer; }
.feature-card { border-radius: var(--radius-lg); overflow: hidden; position: relative; cursor: pointer; background: var(--bg-soft); }
.feature-big, .feature-card { box-shadow: var(--shadow-sm); transition: transform .3s ease, box-shadow .3s ease; }
.feature-big:hover, .feature-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); }
.feature-bg { position: absolute; inset: 0; }
.feature-content { position: relative; padding: 28px; height: 100%; display: flex; flex-direction: column; justify-content: flex-end; color: #fff; }
.feature-card .feature-content { padding: 20px; }
.feature-content .ftag {
align-self: flex-start; font-size: 11px; font-weight: 700; letter-spacing: 0.5px; text-transform: uppercase;
background: rgba(255,255,255,0.25); backdrop-filter: blur(8px); padding: 4px 10px; border-radius: 980px; margin-bottom: 12px;
}
.feature-big h3 { font-size: 28px; font-weight: 700; letter-spacing: -0.5px; }
.feature-card h3 { font-size: 18px; font-weight: 700; }
.feature-content .fdesc { font-size: 14px; opacity: 0.9; margin-top: 6px; }
.feature-btn {
margin-top: 16px; align-self: flex-start; height: 34px; padding: 0 18px; border-radius: 980px;
background: rgba(255,255,255,0.95); color: var(--text); font-size: 13px; font-weight: 600;
transition: all .2s ease;
}
.feature-btn:hover { background: #fff; transform: scale(1.04); }
.feature-rating { display: flex; align-items: center; gap: 4px; font-size: 13px; margin-top: 8px; opacity: 0.95; }
.feature-rating .stars { color: #ffd33d; letter-spacing: 1px; }
@media (max-width: 820px) {
.featured-grid { grid-template-columns: 1fr; }
.feature-big { grid-row: span 1; min-height: 260px; }
}
/* ============ New apps horizontal ============ */
.h-scroll-wrap { position: relative; }
.h-scroll { display: flex; gap: 16px; overflow-x: auto; padding-bottom: 10px; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; }
.h-scroll::-webkit-scrollbar { height: 0; }
.new-card {
flex: 0 0 220px; scroll-snap-align: start; background: var(--bg-soft); border-radius: var(--radius-lg);
padding: 18px; cursor: pointer; transition: transform .25s ease, box-shadow .25s ease; position: relative;
}
.new-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); }
.new-badge {
position: absolute; top: 12px; right: 12px; font-size: 10px; font-weight: 700; letter-spacing: 0.5px;
background: var(--blue); color: #fff; padding: 3px 8px; border-radius: 6px;
}
.new-card .nc-head { display: flex; gap: 12px; align-items: center; }
.new-card .nc-name { font-size: 15px; font-weight: 600; }
.new-card .nc-dev { font-size: 12px; color: var(--text-tertiary); margin-top: 2px; }
.new-card .nc-desc { font-size: 13px; color: var(--text-secondary); margin-top: 12px; line-height: 1.45; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.new-card .nc-foot { display: flex; align-items: center; justify-content: space-between; margin-top: 14px; }
.new-card .nc-rate { font-size: 13px; color: var(--text-secondary); display: flex; align-items: center; gap: 3px; }
.new-card .nc-rate .stars { color: #ffd33d; }
/* ============ App Library ============ */
.library-controls {
display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin-bottom: 24px;
}
.lib-search { position: relative; flex: 1 1 220px; min-width: 200px; }
.lib-search input {
width: 100%; height: 42px; border-radius: var(--radius-md); border: 1px solid var(--border-soft);
background: var(--bg-soft); padding: 0 16px 0 40px; font-size: 14px; outline: none; transition: all .25s;
}
.lib-search input:focus { background: #fff; border-color: var(--blue-light); box-shadow: 0 0 0 3px rgba(0,113,227,0.12); }
.lib-search svg { position: absolute; left: 13px; top: 50%; transform: translateY(-50%); color: var(--text-tertiary); }
.sort-dropdown { position: relative; }
.sort-btn {
height: 42px; padding: 0 16px; border-radius: var(--radius-md); border: 1px solid var(--border-soft);
background: var(--bg-soft); font-size: 14px; font-weight: 500; color: var(--text); display: flex; align-items: center; gap: 6px; transition: all .25s;
}
.sort-btn:hover { background: #ededf0; }
.sort-menu {
position: absolute; top: calc(100% + 6px); right: 0; min-width: 160px; background: #fff;
border: 1px solid var(--border-soft); border-radius: var(--radius-md); box-shadow: var(--shadow-lg);
padding: 6px; opacity: 0; visibility: hidden; transform: translateY(-6px); transition: all .2s ease; z-index: 20;
}
.sort-menu.open { opacity: 1; visibility: visible; transform: translateY(0); }
.sort-item { padding: 9px 12px; border-radius: var(--radius-sm); font-size: 14px; color: var(--text-secondary); transition: all .15s; display: flex; align-items: center; justify-content: space-between; }
.sort-item:hover { background: var(--bg-soft); color: var(--text); }
.sort-item.active { color: var(--blue); font-weight: 600; }
.sort-item .check { opacity: 0; }
.sort-item.active .check { opacity: 1; }
.app-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 16px; }
@media (max-width: 1000px) { .app-grid { grid-template-columns: repeat(4, 1fr); } }
@media (max-width: 640px) { .app-grid { grid-template-columns: repeat(2, 1fr); } }
.app-card {
background: var(--bg-soft); border-radius: var(--radius-lg); padding: 16px; cursor: pointer;
transition: transform .25s ease, box-shadow .25s ease; display: flex; flex-direction: column;
}
.app-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); }
.app-card .ac-head { display: flex; gap: 12px; align-items: center; }
.app-card .ac-info { flex: 1; min-width: 0; }
.app-card .ac-name { font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.app-card .ac-cat { font-size: 12px; color: var(--text-tertiary); margin-top: 2px; }
.app-card .ac-desc { font-size: 12px; color: var(--text-secondary); margin-top: 12px; line-height: 1.45; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; flex: 1; }
.app-card .ac-foot { display: flex; align-items: center; justify-content: space-between; margin-top: 14px; }
.app-card .ac-rate { font-size: 12px; color: var(--text-secondary); display: flex; align-items: center; gap: 3px; }
.app-card .ac-rate .stars { color: #ffd33d; font-size: 11px; }
.app-card .ac-dl {
height: 28px; padding: 0 14px; border-radius: 980px; background: #fff; color: var(--blue);
font-size: 12px; font-weight: 600; border: 1px solid rgba(0,113,227,0.2); transition: all .2s ease;
}
.app-card .ac-dl:hover { background: var(--blue); color: #fff; border-color: var(--blue); }
/* App icon (CSS gradient) */
.app-icon {
width: 56px; height: 56px; border-radius: 14px; flex-shrink: 0; position: relative; overflow: hidden;
display: grid; place-items: center; color: #fff; font-size: 22px; font-weight: 700;
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
.app-icon::after { content: ''; position: absolute; inset: 0; background: linear-gradient(160deg, rgba(255,255,255,0.25), transparent 50%); }
.app-icon.sm { width: 44px; height: 44px; border-radius: 11px; font-size: 17px; }
.app-icon.xs { width: 36px; height: 36px; border-radius: 9px; font-size: 14px; }
.app-icon.lg { width: 88px; height: 88px; border-radius: 20px; font-size: 34px; }
/* ============ Pagination ============ */
.pagination { display: flex; justify-content: center; align-items: center; gap: 6px; margin-top: 36px; flex-wrap: wrap; }
.page-btn {
min-width: 38px; height: 38px; padding: 0 10px; border-radius: var(--radius-sm); font-size: 14px;
color: var(--text-secondary); background: var(--bg-soft); transition: all .2s; display: grid; place-items: center;
}
.page-btn:hover:not(:disabled) { background: #ededf0; color: var(--text); }
.page-btn.active { background: var(--blue); color: #fff; font-weight: 600; }
.page-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.page-ellipsis { padding: 0 4px; color: var(--text-tertiary); }
/* ============ Modal ============ */
.modal-backdrop {
position: fixed; inset: 0; z-index: 1000; background: rgba(0,0,0,0.4);
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
opacity: 0; visibility: hidden; transition: opacity .3s ease, visibility .3s ease;
display: grid; place-items: center; padding: 24px; overflow-y: auto;
}
.modal-backdrop.open { opacity: 1; visibility: visible; }
.modal {
background: #fff; border-radius: var(--radius-xl); max-width: 680px; width: 100%; margin: auto;
max-height: 90vh; overflow-y: auto; box-shadow: var(--shadow-lg);
transform: scale(0.95) translateY(10px); opacity: 0; transition: transform .35s cubic-bezier(0.22,1,0.36,1), opacity .35s ease;
}
.modal-backdrop.open .modal { transform: scale(1) translateY(0); opacity: 1; }
.modal-close {
position: sticky; top: 0; float: right; margin: 14px 14px 0 0; z-index: 5;
width: 32px; height: 32px; border-radius: 50%; background: rgba(0,0,0,0.05); color: var(--text-secondary);
display: grid; place-items: center; font-size: 18px; transition: all .2s;
}
.modal-close:hover { background: rgba(0,0,0,0.1); transform: rotate(90deg); }
.modal-body { padding: 0 32px 36px; }
.modal-hero { display: flex; gap: 20px; align-items: center; padding-top: 24px; }
.modal-hero .mh-info { flex: 1; min-width: 0; }
.modal-hero h2 { font-size: 24px; font-weight: 700; letter-spacing: -0.3px; }
.modal-hero .mh-dev { font-size: 14px; color: var(--text-secondary); margin-top: 4px; }
.modal-hero .mh-meta { display: flex; align-items: center; gap: 14px; margin-top: 10px; flex-wrap: wrap; }
.modal-hero .mh-rating { display: flex; align-items: center; gap: 4px; font-size: 14px; }
.modal-hero .mh-rating .stars { color: #ffd33d; letter-spacing: 1px; }
.modal-hero .mh-rating .rnum { font-weight: 600; }
.modal-hero .mh-rating .rcount { color: var(--text-tertiary); font-size: 13px; }
.modal-hero .mh-price { font-size: 13px; color: var(--text-secondary); }
.modal-dl-btn {
flex-shrink: 0; height: 40px; padding: 0 24px; border-radius: 980px; font-size: 15px; font-weight: 600;
background: linear-gradient(135deg, var(--blue-light), var(--blue)); color: #fff; transition: transform .2s;
}
.modal-dl-btn:hover { transform: scale(1.04); }
.modal-stats { display: grid; grid-template-columns: repeat(3,1fr); gap: 12px; margin-top: 24px; padding: 18px 0; border-top: 1px solid var(--border-soft); border-bottom: 1px solid var(--border-soft); }
.modal-stat { text-align: center; }
.modal-stat .ms-val { font-size: 18px; font-weight: 700; }
.modal-stat .ms-label { font-size: 12px; color: var(--text-tertiary); margin-top: 2px; }
.modal-section { margin-top: 26px; }
.modal-section h4 { font-size: 16px; font-weight: 700; margin-bottom: 10px; }
.modal-section p { font-size: 14px; color: var(--text-secondary); line-height: 1.6; }
.tag-row { display: flex; gap: 8px; flex-wrap: wrap; }
.tag-pill { font-size: 12px; font-weight: 500; color: var(--blue); background: rgba(0,113,227,0.08); padding: 5px 12px; border-radius: 980px; }
.feature-list { list-style: none; display: flex; flex-direction: column; gap: 10px; }
.feature-list li { font-size: 14px; color: var(--text-secondary); display: flex; align-items: flex-start; gap: 8px; }
.feature-list li::before { content: '\2713'; color: var(--blue); font-weight: 700; flex-shrink: 0; }
.shots-row { display: flex; gap: 12px; overflow-x: auto; padding-bottom: 6px; }
.shots-row::-webkit-scrollbar { height: 0; }
.shot-item { flex: 0 0 200px; height: 360px; border-radius: var(--radius-md); overflow: hidden; position: relative; }
.shot-item .shot-label { position: absolute; bottom: 12px; left: 12px; color: #fff; font-size: 12px; font-weight: 600; text-shadow: 0 1px 4px rgba(0,0,0,0.4); }
.modal-version { font-size: 13px; color: var(--text-tertiary); margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--border-soft); display: flex; gap: 24px; flex-wrap: wrap; }
.modal-version span b { color: var(--text-secondary); font-weight: 500; }
@media (max-width: 560px) {
.modal-body { padding: 0 20px 28px; }
.modal-hero { flex-direction: column; text-align: center; }
.modal-stats { grid-template-columns: repeat(3,1fr); }
}
/* ============ Skeleton ============ */
.skeleton { background: #ececee; border-radius: var(--radius-sm); position: relative; overflow: hidden; }
.skeleton::after {
content: ''; position: absolute; inset: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.65), transparent);
transform: translateX(-100%); animation: shimmer 1.4s infinite;
}
@keyframes shimmer { 100% { transform: translateX(100%); } }
.sk-card { background: var(--bg-soft); border-radius: var(--radius-lg); padding: 16px; }
.sk-line { height: 12px; border-radius: 6px; }
.sk-line.w70 { width: 70%; } .sk-line.w50 { width: 50%; } .sk-line.w90 { width: 90%; }
/* ============ Fade-in animation ============ */
.fade-in { opacity: 0; transform: translateY(24px); transition: opacity .6s ease, transform .6s ease; }
.fade-in.visible { opacity: 1; transform: translateY(0); }
/* ============ Footer ============ */
.site-footer { background: var(--bg-soft); border-top: 1px solid var(--border-soft); padding: 48px 0 32px; margin-top: 40px; }
.footer-inner { max-width: var(--max-w); margin: 0 auto; padding: 0 24px; }
.footer-top { display: flex; justify-content: space-between; gap: 32px; flex-wrap: wrap; }
.footer-brand { max-width: 280px; }
.footer-brand .logo { margin-bottom: 12px; }
.footer-brand p { font-size: 13px; color: var(--text-tertiary); line-height: 1.6; }
.footer-cols { display: flex; gap: 56px; flex-wrap: wrap; }
.footer-col h5 { font-size: 13px; font-weight: 600; margin-bottom: 12px; }
.footer-col a { display: block; font-size: 13px; color: var(--text-tertiary); padding: 4px 0; transition: color .2s; }
.footer-col a:hover { color: var(--blue); }
.footer-bottom { margin-top: 36px; padding-top: 20px; border-top: 1px solid var(--border-soft); display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; font-size: 12px; color: var(--text-tertiary); }
@media (max-width: 640px) { .footer-cols { gap: 32px; } }
/* ============ Toast ============ */
.toast { position: fixed; bottom: 32px; left: 50%; transform: translateX(-50%) translateY(20px); z-index: 2000; background: rgba(29,29,31,0.92); color: #fff; padding: 12px 22px; border-radius: var(--radius-md); font-size: 14px; opacity: 0; visibility: hidden; transition: all .3s ease; box-shadow: var(--shadow-lg); }
.toast.show { opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0); }
/* ============ Empty ============ */
.empty-state { text-align: center; padding: 60px 20px; color: var(--text-tertiary); }
.empty-state svg { margin-bottom: 16px; opacity: 0.5; }
.empty-state p { font-size: 15px; }
/* ============ Misc ============ */
.hidden { display: none !important; }
</style>
</head>
<body>
<!-- ======================= HEADER ======================= -->
<header class="site-header">
<div class="header-inner">
<a href="#" class="logo" onclick="goHome(event)">
<span class="logo-mark">&#10010;</span>
<span data-i18n="logo_name">应用商店</span>
</a>
<nav class="main-nav" id="mainNav">
<a href="#" class="nav-link active" data-nav="home" onclick="navTo(event,'home')" data-i18n="nav_home">首页</a>
<a href="#" class="nav-link" data-nav="discover" onclick="navTo(event,'discover')" data-i18n="nav_discover">发现</a>
<a href="#" class="nav-link" data-nav="ranking" onclick="navTo(event,'ranking')" data-i18n="nav_ranking">排行榜</a>
<a href="#" class="nav-link" data-nav="new" onclick="navTo(event,'new')" data-i18n="nav_new">新上架</a>
</nav>
<div class="header-search">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
<input type="text" id="headerSearch" data-i18n-placeholder="search_all" placeholder="搜索应用、游戏..." autocomplete="off">
</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>
</div>
</header>
<!-- ======================= HERO ======================= -->
<section class="hero">
<div class="container">
<span class="hero-eyebrow" data-i18n="hero_eyebrow">全新体验 · 精选推荐</span>
<h1 data-i18n="hero_title">发现精彩应用</h1>
<p data-i18n="hero_desc">探索海量优质应用与游戏,从效率工具到娱乐大作,总有一款适合你。</p>
<a href="#library" class="hero-cta" onclick="exploreNow(event)">
<span data-i18n="hero_cta">立即探索</span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
</a>
</div>
</section>
<!-- ======================= CATEGORY FILTER ======================= -->
<section class="section" style="padding-top:24px; padding-bottom:0;">
<div class="container">
<div class="cat-bar" id="catBar">
<button class="cat-pill skeleton" style="width:80px;height:36px;"></button>
<button class="cat-pill skeleton" style="width:90px;height:36px;"></button>
<button class="cat-pill skeleton" style="width:70px;height:36px;"></button>
</div>
</div>
</section>
<!-- ======================= FEATURED ======================= -->
<section class="section" id="featuredSection">
<div class="container">
<div class="section-head">
<div>
<h2 class="section-title" data-i18n="featured_title">精选推荐</h2>
<p class="section-sub" data-i18n="featured_sub">编辑团队为你精心挑选</p>
</div>
<a href="#" class="section-more" onclick="seeAllFeatured(event)" data-i18n="see_all">查看全部 &rsaquo;</a>
</div>
<div class="featured-grid" id="featuredGrid">
<div class="feature-big skeleton"></div>
<div class="feature-card skeleton" style="min-height:170px;"></div>
<div class="feature-card skeleton" style="min-height:170px;"></div>
</div>
</div>
</section>
<!-- ======================= NEW APPS ======================= -->
<section class="section" id="newSection">
<div class="container">
<div class="section-head">
<div>
<h2 class="section-title" data-i18n="new_title">新上架</h2>
<p class="section-sub" data-i18n="new_sub">最新加入的应用</p>
</div>
<a href="#" class="section-more" onclick="seeAllNew(event)" data-i18n="see_all">查看全部 &rsaquo;</a>
</div>
<div class="h-scroll-wrap">
<div class="h-scroll" id="newScroll">
<div class="new-card skeleton" style="height:200px;"></div>
<div class="new-card skeleton" style="height:200px;"></div>
<div class="new-card skeleton" style="height:200px;"></div>
<div class="new-card skeleton" style="height:200px;"></div>
</div>
</div>
</div>
</section>
<!-- ======================= APP LIBRARY ======================= -->
<section class="section" id="library">
<div class="container">
<div class="section-head">
<div>
<h2 class="section-title" data-i18n="library_title">全部应用</h2>
<p class="section-sub" data-i18n="library_sub">浏览所有应用</p>
</div>
</div>
<div class="library-controls">
<div class="lib-search">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
<input type="text" id="libSearch" data-i18n-placeholder="search_name" placeholder="搜索应用名称..." autocomplete="off">
</div>
<div class="sort-dropdown">
<button class="sort-btn" id="sortBtn" onclick="toggleSort(event)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 6h18M6 12h12M10 18h4"/></svg>
<span id="sortLabel" data-i18n="sort_default">默认排序</span>
<svg width="12" height="12" 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="sort-menu" id="sortMenu">
<div class="sort-item active" data-sort="" onclick="setSort('')"><span data-i18n="sort_default">默认排序</span> <span class="check">&#10003;</span></div>
<div class="sort-item" data-sort="rating" onclick="setSort('rating')"><span data-i18n="sort_rating">评分最高</span> <span class="check">&#10003;</span></div>
<div class="sort-item" data-sort="downloads" onclick="setSort('downloads')"><span data-i18n="sort_downloads">下载最多</span> <span class="check">&#10003;</span></div>
<div class="sort-item" data-sort="newest" onclick="setSort('newest')"><span data-i18n="sort_newest">最新发布</span> <span class="check">&#10003;</span></div>
</div>
</div>
</div>
<div class="app-grid" id="appGrid"></div>
<div class="pagination" id="pagination"></div>
</div>
</section>
<!-- ======================= FOOTER ======================= -->
<footer class="site-footer">
<div class="footer-inner">
<div class="footer-top">
<div class="footer-brand">
<div class="logo"><span class="logo-mark">&#10010;</span><span data-i18n="logo_name">应用商店</span></div>
<p data-i18n="footer_desc">发现、下载并体验海量优质应用与游戏。我们致力于为用户提供安全、丰富的应用生态。</p>
</div>
<div class="footer-cols">
<div class="footer-col">
<h5 data-i18n="footer_explore">探索</h5>
<a href="#" onclick="navTo(event,'home')" data-i18n="nav_home">首页</a>
<a href="#" onclick="navTo(event,'discover')" data-i18n="nav_discover">发现</a>
<a href="#" onclick="navTo(event,'ranking')" data-i18n="nav_ranking">排行榜</a>
<a href="#" onclick="navTo(event,'new')" data-i18n="nav_new">新上架</a>
</div>
<div class="footer-col">
<h5 data-i18n="footer_about">关于</h5>
<a href="#" data-i18n="footer_about_us">关于我们</a>
<a href="#" data-i18n="footer_privacy">隐私政策</a>
<a href="#" data-i18n="footer_terms">服务条款</a>
<a href="#" data-i18n="footer_contact">联系方式</a>
</div>
<div class="footer-col">
<h5 data-i18n="footer_dev">开发者</h5>
<a href="#" data-i18n="footer_dev_center">开发者中心</a>
<a href="#" data-i18n="footer_listing_guide">上架指南</a>
<a href="#" data-i18n="footer_api_docs">API 文档</a>
<a href="#" data-i18n="footer_cooperation">合作申请</a>
</div>
</div>
</div>
<div class="footer-bottom">
<span data-i18n="footer_copyright">&copy; 2026 应用商店. 保留所有权利.</span>
<span data-i18n="footer_disclaimer">仅供学习展示用途</span>
</div>
</div>
</footer>
<!-- ======================= MODAL ======================= -->
<div class="modal-backdrop" id="modalBackdrop" onclick="closeModalOnBackdrop(event)">
<div class="modal" id="appModal">
<button class="modal-close" onclick="closeModal()" aria-label="关闭" data-i18n-aria="modal_close">&times;</button>
<div class="modal-body" id="modalBody"></div>
</div>
</div>
<!-- ======================= TOAST ======================= -->
<div class="toast" id="toast"></div>
<script>
"use strict";
/* =========================================================================
i18n 国际化引擎
========================================================================= */
var I18N_LOCALES = {
'zh-CN': {
logo_name: '应用商店',
nav_home: '首页',
nav_discover: '发现',
nav_ranking: '排行榜',
nav_new: '新上架',
search_all: '搜索应用、游戏...',
search_name: '搜索应用名称...',
hero_eyebrow: '全新体验 · 精选推荐',
hero_title: '发现精彩应用',
hero_desc: '探索海量优质应用与游戏,从效率工具到娱乐大作,总有一款适合你。',
hero_cta: '立即探索',
cat_all: '全部',
featured_title: '精选推荐',
featured_sub: '编辑团队为你精心挑选',
featured_tag: '编辑精选',
featured_recommended: '推荐',
see_all: '查看全部',
view_detail: '查看详情',
new_title: '新上架',
new_sub: '最新加入的应用',
library_title: '全部应用',
library_sub: '浏览所有应用',
sort_default: '默认排序',
sort_rating: '评分最高',
sort_downloads: '下载最多',
sort_newest: '最新发布',
download: '下载',
downloads_label: '下载量',
reviews_label: '评分数',
size_label: '大小',
rating_count: '评分',
app_intro: '应用介绍',
app_tags: '标签',
app_features: '功能特性',
app_screenshots: '应用截图',
version_label: '版本:',
update_label: '更新日期:',
platform_label: '平台:',
category_label: '分类:',
empty_featured: '暂无精选应用',
empty_new: '暂无新应用',
empty_apps: '没有找到匹配的应用',
empty_app_detail: '应用信息不存在',
modal_close: '关闭',
toast_downloading: '开始下载「',
toast_downloading_end: '」',
footer_desc: '发现、下载并体验海量优质应用与游戏。我们致力于为用户提供安全、丰富的应用生态。',
footer_explore: '探索',
footer_about: '关于',
footer_about_us: '关于我们',
footer_privacy: '隐私政策',
footer_terms: '服务条款',
footer_contact: '联系方式',
footer_dev: '开发者',
footer_dev_center: '开发者中心',
footer_listing_guide: '上架指南',
footer_api_docs: 'API 文档',
footer_cooperation: '合作申请',
footer_copyright: '\u00a9 2026 应用商店. 保留所有权利.',
footer_disclaimer: '仅供学习展示用途'
},
'zh-TW': {
logo_name: '應用商店',
nav_home: '首頁',
nav_discover: '發現',
nav_ranking: '排行榜',
nav_new: '新上架',
search_all: '搜尋應用、遊戲...',
search_name: '搜尋應用名稱...',
hero_eyebrow: '全新體驗 · 精選推薦',
hero_title: '發現精彩應用',
hero_desc: '探索海量優質應用與遊戲,從效率工具到娛樂大作,總有一款適合你。',
hero_cta: '立即探索',
cat_all: '全部',
featured_title: '精選推薦',
featured_sub: '編輯團隊為你精心挑選',
featured_tag: '編輯精選',
featured_recommended: '推薦',
see_all: '查看全部',
view_detail: '查看详情',
new_title: '新上架',
new_sub: '最新加入的應用',
library_title: '全部應用',
library_sub: '瀏覽所有應用',
sort_default: '預設排序',
sort_rating: '評分最高',
sort_downloads: '下載最多',
sort_newest: '最新發佈',
download: '下載',
downloads_label: '下載量',
reviews_label: '評分數',
size_label: '大小',
rating_count: '評分',
app_intro: '應用介紹',
app_tags: '標籤',
app_features: '功能特性',
app_screenshots: '應用截圖',
version_label: '版本:',
update_label: '更新日期:',
platform_label: '平台:',
category_label: '分類:',
empty_featured: '暫無精選應用',
empty_new: '暫無新應用',
empty_apps: '沒有找到匹配的應用',
empty_app_detail: '應用資訊不存在',
modal_close: '關閉',
toast_downloading: '開始下載「',
toast_downloading_end: '」',
footer_desc: '發現、下載並體驗海量優質應用與遊戲。我們致力於為使用者提供安全、豐富的應用生態。',
footer_explore: '探索',
footer_about: '關於',
footer_about_us: '關於我們',
footer_privacy: '隱私政策',
footer_terms: '服務條款',
footer聯絡: '聯絡方式',
footer_dev: '開發者',
footer_dev_center: '開發者中心',
footer_listing_guide: '上架指南',
footer_api_docs: 'API 文件',
footer_cooperation: '合作申請',
footer_copyright: '\u00a9 2026 應用商店. 保留所有權利.',
footer_disclaimer: '僅供學習展示用途'
},
'en': {
logo_name: 'App Store',
nav_home: 'Home',
nav_discover: 'Discover',
nav_ranking: 'Rankings',
nav_new: 'New',
search_all: 'Search apps, games...',
search_name: 'Search app name...',
hero_eyebrow: 'All-New Experience · Curated Picks',
hero_title: 'Discover Amazing Apps',
hero_desc: 'Explore a vast collection of quality apps and games, from productivity tools to entertainment hits — there\'s something for everyone.',
hero_cta: 'Explore Now',
cat_all: 'All',
featured_title: 'Featured',
featured_sub: 'Handpicked by our editorial team',
featured_tag: 'Editor\'s Choice',
featured_recommended: 'Recommended',
see_all: 'See All',
view_detail: 'View Details',
new_title: 'New Arrivals',
new_sub: 'Recently added apps',
library_title: 'All Apps',
library_sub: 'Browse all apps',
sort_default: 'Default',
sort_rating: 'Top Rated',
sort_downloads: 'Most Downloaded',
sort_newest: 'Newest',
download: 'Get',
downloads_label: 'Downloads',
reviews_label: 'Ratings',
size_label: 'Size',
rating_count: 'ratings',
app_intro: 'App Description',
app_tags: 'Tags',
app_features: 'Features',
app_screenshots: 'Screenshots',
version_label: 'Version: ',
update_label: 'Updated: ',
platform_label: 'Platform: ',
category_label: 'Category: ',
empty_featured: 'No featured apps',
empty_new: 'No new apps',
empty_apps: 'No matching apps found',
empty_app_detail: 'App information not available',
modal_close: 'Close',
toast_downloading: 'Downloading "',
toast_downloading_end: '"',
footer_desc: 'Discover, download, and enjoy a vast collection of quality apps and games. We are dedicated to providing a safe and rich app ecosystem.',
footer_explore: 'Explore',
footer_about: 'About',
footer_about_us: 'About Us',
footer_privacy: 'Privacy Policy',
footer_terms: 'Terms of Service',
footer_contact: 'Contact',
footer_dev: 'Developers',
footer_dev_center: 'Developer Center',
footer_listing_guide: 'Listing Guide',
footer_api_docs: 'API Docs',
footer_cooperation: 'Partnerships',
footer_copyright: '\u00a9 2026 App Store. All rights reserved.',
footer_disclaimer: 'For demonstration purposes only'
}
};
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('appstore_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.querySelectorAll('[data-i18n-aria]').forEach(function(el) {
var key = el.getAttribute('data-i18n-aria');
el.setAttribute('aria-label', 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('appstore_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');
});
/* =========================================================================
配置 & 工具函数
========================================================================= */
var API_BASE = '/api/v1/app';
var PAGE_SIZE = 12;
/* 状态 */
var state = {
categories: [],
currentCat: '',
sort: '',
keyword: '',
page: 1,
total: 0,
cache: {}
};
/* ---- 工具:格式化大数字 ---- */
function formatNum(n) {
if (n == null || isNaN(n)) 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.toString();
}
function formatNumI18n(n) {
if (n == null || isNaN(n)) return '0';
n = Number(n);
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 n.toString();
}
if (n >= 100000000) return (n / 100000000).toFixed(1).replace(/\.0$/, '') + '亿';
if (n >= 10000) return (n / 10000).toFixed(1).replace(/\.0$/, '') + '万';
return n.toString();
}
/* ---- 工具:星级 HTML ---- */
function starsHtml(rating) {
rating = parseFloat(rating) || 0;
var html = '';
for (var k = 0; k < 5; k++) {
var fill = Math.max(0, Math.min(1, rating - k));
html += '<span style="display:inline-block;position:relative;font-size:inherit;">'
+ '<span style="color:#d8d8df;">\u2605</span>'
+ '<span style="position:absolute;left:0;top:0;overflow:hidden;width:' + (fill * 100) + '%;color:#ffd33d;">\u2605</span>'
+ '</span>';
}
return html;
}
/* ---- 工具:安全解析 JSON ---- */
function parseJSON(str, fallback) {
if (Array.isArray(str)) return str;
if (typeof str !== 'string') return fallback || [];
try { var v = JSON.parse(str); return v; }
catch (e) { return fallback || []; }
}
/* ---- 工具:用应用名 hash 生成稳定的渐变色 ---- */
function hashStr(s) {
s = s || 'A';
var h = 0;
for (var i = 0; i < s.length; i++) { h = ((h << 5) - h) + s.charCodeAt(i); h |= 0; }
return Math.abs(h);
}
function gradFromName(name) {
var h = hashStr(name);
var hue1 = h % 360;
var hue2 = (hue1 + 40 + (h % 80)) % 360;
var sat = 70 + (h % 20);
return 'linear-gradient(135deg, hsl(' + hue1 + ',' + sat + '%,60%), hsl(' + hue2 + ',' + (sat + 5) + '%,50%))';
}
function initials(name) {
name = name || '?';
return name.charAt(0);
}
/* ---- 工具:escape HTML ---- */
function esc(s) {
if (s == null) return '';
return String(s).replace(/[&<>"']/g, function (c) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
});
}
/* ---- 工具:判断 icon 字段是否为图片 URL ---- */
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;
}
/* ---- 工具:生成应用图标 HTML ---- */
function appIconHTML(app, cls) {
var grad = gradFromName(app.name);
var clsAttr = 'app-icon' + (cls ? ' ' + cls : '');
var iconURL = isIconURL(app.icon) ? String(app.icon).trim() : '';
if (iconURL) {
return '<div class="' + clsAttr + '" style="background:' + grad + ';">'
+ '<img src="' + esc(iconURL) + '" alt="' + esc(app.name) + '" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'block\'">'
+ '<span style="display:none">' + esc(initials(app.name)) + '</span>'
+ '</div>';
}
return '<div class="' + clsAttr + '" style="background:' + grad + ';">' + esc(initials(app.name)) + '</div>';
}
/* ---- 工具:debounce ---- */
function debounce(fn, wait) {
var t;
return function () {
var ctx = this, args = arguments;
clearTimeout(t);
t = setTimeout(function () { fn.apply(ctx, args); }, wait);
};
}
/* ---- 工具:toast ---- */
var toastTimer;
function showToast(msg) {
var el = document.getElementById('toast');
el.textContent = msg;
el.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { el.classList.remove('show'); }, 2200);
}
/* =========================================================================
Mock 数据
========================================================================= */
var MOCK = (function () {
var catDefs = [
{ id: 1, name: '推荐', icon: '\u2b50', sort: 1 },
{ id: 2, name: '游戏', icon: '\ud83c\udfae', sort: 2 },
{ id: 3, name: '效率', icon: '\u26a1', sort: 3 },
{ id: 4, name: '社交', icon: '\ud83d\udc65', sort: 4 },
{ id: 5, name: '工具', icon: '\ud83d\udee0', sort: 5 },
{ id: 6, name: '影音', icon: '\ud83c\udfa7', sort: 6 },
{ id: 7, name: '教育', icon: '\ud83d\udcda', sort: 7 },
{ id: 8, name: '生活', icon: '\ud83c\udfe1', sort: 8 },
{ id: 9, name: '财务', icon: '\ud83d\udcb0', sort: 9 }
];
var catDefsEN = [
{ id: 1, name: 'Recommended', icon: '\u2b50', sort: 1 },
{ id: 2, name: 'Games', icon: '\ud83c\udfae', sort: 2 },
{ id: 3, name: 'Productivity', icon: '\u26a1', sort: 3 },
{ id: 4, name: 'Social', icon: '\ud83d\udc65', sort: 4 },
{ id: 5, name: 'Tools', icon: '\ud83d\udee0', sort: 5 },
{ id: 6, name: 'Media', icon: '\ud83c\udfa7', sort: 6 },
{ id: 7, name: 'Education', icon: '\ud83d\udcda', sort: 7 },
{ id: 8, name: 'Lifestyle', icon: '\ud83c\udfe1', sort: 8 },
{ id: 9, name: 'Finance', icon: '\ud83d\udcb0', sort: 9 }
];
var catDefsTW = [
{ id: 1, name: '推薦', icon: '\u2b50', sort: 1 },
{ id: 2, name: '遊戲', icon: '\ud83c\udfae', sort: 2 },
{ id: 3, name: '效率', icon: '\u26a1', sort: 3 },
{ id: 4, name: '社交', icon: '\ud83d\udc65', sort: 4 },
{ id: 5, name: '工具', icon: '\ud83d\udee0', sort: 5 },
{ id: 6, name: '影音', icon: '\ud83c\udfa7', sort: 6 },
{ id: 7, name: '教育', icon: '\ud83d\udcda', sort: 7 },
{ id: 8, name: '生活', icon: '\ud83c\udfe1', sort: 8 },
{ id: 9, name: '財務', icon: '\ud83d\udcb0', sort: 9 }
];
function getLocalizedCats() {
if (currentLang === 'en') return catDefsEN;
if (currentLang === 'zh-TW') return catDefsTW;
return catDefs;
}
var appDefs = [
{ name: '极光笔记', dev: 'Aurora Studio', cat: 3, desc: '一款支持 Markdown 与思维导图的智能笔记应用,让灵感随手记录。', tags: ['笔记','Markdown','同步'], features: ['实时云端同步','支持思维导图','富文本与双链','端到端加密'], rating: 4.9, reviews: 128340, downloads: 56000000, size: '85 MB', ver: '3.2.1', date: '2026-06-20', plat: 'iOS / Android', price: '免费' },
{ name: '星海航海家', dev: 'Cosmos Games', cat: 2, desc: '在浩瀚星海中探索未知星球,建立你的星际舰队,征服宇宙。', tags: ['冒险','策略','科幻'], features: ['开放宇宙地图','多人实时对战','超过 200 种舰船','精美 3D 画面'], rating: 4.7, reviews: 89021, downloads: 23000000, size: '1.2 GB', ver: '2.0.5', date: '2026-07-01', plat: 'iOS / Android', price: '免费' },
{ name: '云听音乐', dev: 'CloudTune', cat: 6, desc: '高品质无损音乐播放器,千万曲库,智能推荐你的专属歌单。', tags: ['音乐','无损','推荐'], features: ['无损音质播放','AI 智能歌单','歌词逐字滚动','离线缓存'], rating: 4.8, reviews: 567200, downloads: 320000000, size: '120 MB', ver: '8.1.0', date: '2026-06-28', plat: 'iOS / Android', price: '免费' },
{ name: '时光日历', dev: 'TimeFlow', cat: 8, desc: '集日历、待办、习惯追踪于一体的时间管理工具,优雅而强大。', tags: ['日历','待办','效率'], features: ['自然语言输入','习惯打卡','多端同步','小组件支持'], rating: 4.6, reviews: 45120, downloads: 12000000, size: '45 MB', ver: '4.3.2', date: '2026-06-15', plat: 'iOS / Android', price: '免费' },
{ name: '像素工坊', dev: 'PixelArt Inc.', cat: 2, desc: '复古像素风格的沙盒建造游戏,发挥创意打造你的像素世界。', tags: ['沙盒','像素','创造'], features: ['无限创造模式','多人联机','丰富素材库','支持 MOD'], rating: 4.5, reviews: 234560, downloads: 89000000, size: '380 MB', ver: '1.9.0', date: '2026-06-22', plat: 'iOS / Android', price: '\u00a618' },
{ name: '速聊 Messenger', dev: 'QuickChat', cat: 4, desc: '极简安全的即时通讯应用,端到端加密,消息阅后即焚。', tags: ['聊天','加密','通讯'], features: ['端到端加密','阅后即焚','高清视频通话','多设备登录'], rating: 4.4, reviews: 312000, downloads: 450000000, size: '78 MB', ver: '6.0.3', date: '2026-07-05', plat: 'iOS / Android', price: '免费' },
{ name: '极简记账', dev: 'LedgerLab', cat: 9, desc: '一键记账,多维度图表分析,让每一笔收支清清楚楚。', tags: ['记账','理财','图表'], features: ['智能分类','多账本管理','预算提醒','数据导出'], rating: 4.7, reviews: 89300, downloads: 34000000, size: '52 MB', ver: '5.2.1', date: '2026-06-18', plat: 'iOS / Android', price: '免费' },
{ name: '番茄专注', dev: 'FocusLab', cat: 3, desc: '番茄工作法效率工具,白噪音伴你专注,告别拖延。', tags: ['番茄钟','专注','白噪音'], features: ['番茄计时','白噪音库','专注统计','森林养成'], rating: 4.8, reviews: 67800, downloads: 21000000, size: '38 MB', ver: '3.1.4', date: '2026-07-03', plat: 'iOS / Android', price: '免费' },
{ name: '星空摄影', dev: 'AstroShot', cat: 8, desc: '专业级手机摄影应用,AI 增强、长曝光、星空模式一应俱全。', tags: ['摄影','修图','AI'], features: ['AI 智能增强','星空拍摄模式','RAW 格式输出','专业参数调节'], rating: 4.6, reviews: 156000, downloads: 67000000, size: '95 MB', ver: '7.0.2', date: '2026-06-25', plat: 'iOS / Android', price: '免费' },
{ name: '编程学院', dev: 'CodeEdu', cat: 7, desc: '互动式编程学习平台,从零基础到全栈工程师,边学边练。', tags: ['编程','学习','在线'], features: ['互动编程练习','多语言课程','项目实战','在线编译器'], rating: 4.9, reviews: 98000, downloads: 15000000, size: '110 MB', ver: '4.0.0', date: '2026-06-30', plat: 'iOS / Android', price: '免费' },
{ name: '萌宠乐园', dev: 'PetWorld', cat: 2, desc: '养育你的虚拟萌宠,互动养成小游戏合集,治愈每一天。', tags: ['养成','休闲','治愈'], features: ['多种萌宠选择','互动小游戏','每日任务','社交访友'], rating: 4.5, reviews: 432000, downloads: 120000000, size: '260 MB', ver: '2.4.1', date: '2026-06-12', plat: 'iOS / Android', price: '免费' },
{ name: '文件管家', dev: 'FileMaster', cat: 5, desc: '全能文件管理工具,支持压缩解压、PDF 转换、无线传输。', tags: ['文件','管理','工具'], features: ['无线传输','压缩解压','PDF 工具','云盘挂载'], rating: 4.6, reviews: 76500, downloads: 43000000, size: '64 MB', ver: '6.3.0', date: '2026-06-20', plat: 'iOS / Android', price: '免费' },
{ name: '冥想空间', dev: 'MindPeace', cat: 8, desc: '引导式冥想与呼吸练习,缓解压力,改善睡眠质量。', tags: ['冥想','健康','助眠'], features: ['引导冥想课程','呼吸训练','睡眠白噪音','心情日记'], rating: 4.8, reviews: 54000, downloads: 9800000, size: '72 MB', ver: '3.0.5', date: '2026-07-02', plat: 'iOS / Android', price: '免费' },
{ name: '赛车竞速', dev: 'TurboGames', cat: 2, desc: '超真实 3D 赛车竞速,百辆豪车任你选,驰骋世界赛道。', tags: ['赛车','竞速','3D'], features: ['超百辆授权跑车','真实物理引擎','多人实时竞速','自定义改装'], rating: 4.4, reviews: 287000, downloads: 76000000, size: '1.5 GB', ver: '5.1.0', date: '2026-06-10', plat: 'iOS / Android', price: '免费' },
{ name: '翻译助手', dev: 'LingoPro', cat: 5, desc: '支持 100+ 语言的实时翻译,拍照翻译、语音对话即说即译。', tags: ['翻译','语言','工具'], features: ['100+ 语言互译','拍照翻译','语音实时对话','离线词库'], rating: 4.7, reviews: 198000, downloads: 99000000, size: '88 MB', ver: '9.0.1', date: '2026-06-27', plat: 'iOS / Android', price: '免费' },
{ name: '健身教练', dev: 'FitCoach', cat: 8, desc: 'AI 私人健身教练,定制训练计划,动作纠正,科学塑形。', tags: ['健身','运动','AI'], features: ['AI 动作识别','定制训练计划','卡路里追踪','社区挑战'], rating: 4.6, reviews: 123000, downloads: 55000000, size: '130 MB', ver: '4.2.0', date: '2026-06-19', plat: 'iOS / Android', price: '免费' },
{ name: '绘画大师', dev: 'ArtCanvas', cat: 5, desc: '专业数字绘画工具,支持图层、笔刷、矢量,创意无限。', tags: ['绘画','设计','创意'], features: ['200+ 专业笔刷','图层管理系统','矢量绘图','压感支持'], rating: 4.8, reviews: 67000, downloads: 18000000, size: '210 MB', ver: '2.7.3', date: '2026-07-04', plat: 'iOS / Android', price: '\u00a538' },
{ name: '天气通', dev: 'SkyCast', cat: 8, desc: '精准天气预报与空气质量监测,分钟级降水提醒。', tags: ['天气','预报','生活'], features: ['分钟级降水预报','15 天趋势','空气质量监测','灾害预警'], rating: 4.5, reviews: 345000, downloads: 210000000, size: '58 MB', ver: '7.5.2', date: '2026-06-23', plat: 'iOS / Android', price: '免费' },
{ name: '象棋大师', dev: 'BoardKing', cat: 2, desc: '中国象棋对弈平台,AI 难度可调,在线对战棋友遍布全球。', tags: ['象棋','棋牌','对战'], features: ['AI 多级难度','在线实时对战','棋谱复盘','残局挑战'], rating: 4.7, reviews: 89000, downloads: 33000000, size: '48 MB', ver: '3.4.1', date: '2026-06-14', plat: 'iOS / Android', price: '免费' },
{ name: '英语流利说', dev: 'EnglishPro', cat: 7, desc: 'AI 口语教练,情景对话练习,让你的英语脱口而出。', tags: ['英语','口语','学习'], features: ['AI 口语评分','情景对话','发音纠正','定制课程'], rating: 4.6, reviews: 256000, downloads: 88000000, size: '145 MB', ver: '8.2.0', date: '2026-06-26', plat: 'iOS / Android', price: '免费' },
{ name: '区块链钱包', dev: 'CryptoSafe', cat: 9, desc: '安全便捷的数字资产管理钱包,支持多链,DeFi 一站式。', tags: ['钱包','区块链','DeFi'], features: ['多链资产管理','硬件钱包支持','DeFi 聚合','安全审计'], rating: 4.3, reviews: 45000, downloads: 7600000, size: '92 MB', ver: '5.0.0', date: '2026-06-21', plat: 'iOS / Android', price: '免费' },
{ name: '飞行模拟器', dev: 'SkyPilot', cat: 2, desc: '高保真飞行模拟,全球机场与航线,体验机长的驾驶乐趣。', tags: ['模拟','飞行','3D'], features: ['全球真实机场','多种机型','实时天气','多视角驾驶'], rating: 4.5, reviews: 78000, downloads: 22000000, size: '2.1 GB', ver: '1.5.2', date: '2026-06-08', plat: 'iOS / Android', price: '\u00a068' },
{ name: '食谱大全', dev: 'CookMate', cat: 8, desc: '百万道菜谱,视频教学,根据冰箱食材智能推荐菜谱。', tags: ['美食','菜谱','生活'], features: ['百万菜谱库','视频教学','智能推荐','购物清单'], rating: 4.7, reviews: 167000, downloads: 61000000, size: '76 MB', ver: '6.1.0', date: '2026-06-24', plat: 'iOS / Android', price: '免费' },
{ name: '云盘同步', dev: 'CloudSync', cat: 5, desc: '安全可靠的云存储服务,自动备份照片文件,跨设备访问。', tags: ['云盘','备份','存储'], features: ['自动照片备份','大文件传输','端到端加密','多端同步'], rating: 4.4, reviews: 234000, downloads: 180000000, size: '68 MB', ver: '10.0.0', date: '2026-06-17', plat: 'iOS / Android', price: '免费' }
];
var apps = appDefs.map(function (a, i) {
var localizedCats = getLocalizedCats();
var catName = (localizedCats.find(function (c) { return c.id === a.cat; }) || {}).name || (getLocalizedCats()[0] || {}).name || '应用';
return {
id: i + 1,
name: a.name,
description: a.desc,
category_id: a.cat,
category_name: catName,
icon: '',
developer: a.dev,
rating: a.rating,
reviews: a.reviews,
downloads: a.downloads,
size: a.size,
version: a.ver,
update_date: a.date,
platform: a.plat,
price: a.price,
tags: JSON.stringify(a.tags),
screenshots: JSON.stringify([a.name + ' ' + (currentLang === 'en' ? 'Screenshot' : '截图') + ' 1', a.name + ' ' + (currentLang === 'en' ? 'Screenshot' : '截图') + ' 2', a.name + ' ' + (currentLang === 'en' ? 'Screenshot' : '截图') + ' 3']),
features: JSON.stringify(a.features),
is_featured: i < 5,
is_new: i >= (appDefs.length - 6),
sort: i + 1,
status: 1
};
});
function ok(data) { return { code: 0, message: 'ok', data: data }; }
return {
categories: function () { return ok(getLocalizedCats().slice()); },
featured: function (limit) {
limit = limit || 8;
return ok(apps.filter(function (a) { return a.is_featured; }).slice(0, limit));
},
newApps: function (limit) {
limit = limit || 4;
return ok(apps.filter(function (a) { return a.is_new; }).sort(function (x, y) {
return new Date(y.update_date) - new Date(x.update_date);
}).slice(0, limit));
},
apps: function (params) {
var cat = params.category_id, kw = (params.keyword || '').toLowerCase().trim(), sort = params.sort || '';
var page = parseInt(params.page) || 1, ps = parseInt(params.page_size) || PAGE_SIZE;
var list = apps.slice();
if (cat) list = list.filter(function (a) { return a.category_id == cat; });
if (kw) list = list.filter(function (a) {
return a.name.toLowerCase().indexOf(kw) > -1 || a.developer.toLowerCase().indexOf(kw) > -1;
});
if (sort === 'rating') list.sort(function (a, b) { return b.rating - a.rating; });
else if (sort === 'downloads') list.sort(function (a, b) { return b.downloads - a.downloads; });
else if (sort === 'newest') list.sort(function (a, b) { return new Date(b.update_date) - new Date(a.update_date); });
var total = list.length;
var start = (page - 1) * ps;
var paged = list.slice(start, start + ps);
return ok({ list: paged, total: total, page: page, page_size: ps });
},
detail: function (id) {
var a = apps.find(function (x) { return x.id == id; });
return a ? ok(a) : ok(null);
},
home: function () {
return ok({
categories: getLocalizedCats().slice(),
featured: apps.filter(function (a) { return a.is_featured; }).slice(0, 5),
new: apps.filter(function (a) { return a.is_new; }).sort(function (x, y) {
return new Date(y.update_date) - new Date(x.update_date);
}).slice(0, 4)
});
}
};
})();
/* =========================================================================
API 调用层
========================================================================= */
function apiGet(path, useMockFn) {
var url = API_BASE + path;
return fetch(url, { headers: { 'Accept': 'application/json' } })
.then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(function (res) {
if (res && typeof res.code !== 'undefined' && res.code !== 0) throw new Error(res.message || 'API error');
return res;
})
.catch(function (err) {
console.warn('[API] Request failed, using local data:', url, err.message);
return useMockFn();
});
}
function fetchCategories() {
return apiGet('/categories', function () { return MOCK.categories(); }).then(function (r) { return r.data; });
}
function fetchFeatured(limit) {
return apiGet('/featured?limit=' + (limit || 8), function () { return MOCK.featured(limit); }).then(function (r) { return r.data; });
}
function fetchNew(limit) {
return apiGet('/new?limit=' + (limit || 4), function () { return MOCK.newApps(limit); }).then(function (r) { return r.data; });
}
function fetchApps(params) {
var qs = [];
if (params.category_id) qs.push('category_id=' + params.category_id);
if (params.keyword) qs.push('keyword=' + encodeURIComponent(params.keyword));
if (params.sort) qs.push('sort=' + params.sort);
qs.push('page=' + (params.page || 1));
qs.push('page_size=' + (params.page_size || PAGE_SIZE));
var path = '/apps?' + qs.join('&');
return apiGet(path, function () { return MOCK.apps(params); }).then(function (r) { return r.data; });
}
function fetchDetail(id) {
return apiGet('/apps/' + id, function () { return MOCK.detail(id); }).then(function (r) { return r.data; });
}
/* =========================================================================
渲染:分类
========================================================================= */
function renderCategories(cats) {
state.categories = cats;
var bar = document.getElementById('catBar');
var html = '<button class="cat-pill active" data-cat="" onclick="selectCat(\'\')"><span class="cat-emoji">\u2728</span>' + esc(t('cat_all')) + '</button>';
cats.forEach(function (c) {
html += '<button class="cat-pill" data-cat="' + c.id + '" onclick="selectCat(' + c.id + ')">'
+ '<span class="cat-emoji">' + esc(c.icon || '\u25cf') + '</span>' + esc(c.name) + '</button>';
});
bar.innerHTML = html;
}
function selectCat(catId) {
state.currentCat = catId;
state.page = 1;
var pills = document.querySelectorAll('#catBar .cat-pill');
pills.forEach(function (p) {
p.classList.toggle('active', String(p.getAttribute('data-cat')) === String(catId));
});
loadLibrary();
var lib = document.getElementById('library');
if (lib) lib.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
/* =========================================================================
渲染:精选
========================================================================= */
function renderFeatured(list) {
var grid = document.getElementById('featuredGrid');
if (!list || !list.length) { grid.innerHTML = '<div class="empty-state" style="grid-column:1/-1;"><p>' + esc(t('empty_featured')) + '</p></div>'; return; }
var html = '';
list.forEach(function (app, idx) {
var grad = gradFromName(app.name);
if (idx === 0) {
html += '<div class="feature-big fade-in" onclick="openApp(' + app.id + ')">'
+ '<div class="feature-bg" style="background:' + grad + ';"></div>'
+ '<div class="feature-content">'
+ '<span class="ftag">' + esc(t('featured_tag')) + '</span>'
+ '<h3>' + esc(app.name) + '</h3>'
+ '<p class="fdesc">' + esc(app.description) + '</p>'
+ '<div class="feature-rating"><span class="stars">' + '\u2605'.repeat(5) + '</span> ' + app.rating + ' &middot; ' + formatNumI18n(app.downloads) + ' ' + esc(t('downloads_label')) + '</div>'
+ '<button class="feature-btn" onclick="event.stopPropagation();openApp(' + app.id + ')">' + esc(t('view_detail')) + '</button>'
+ '</div></div>';
} else {
html += '<div class="feature-card fade-in" onclick="openApp(' + app.id + ')">'
+ '<div class="feature-bg" style="background:' + grad + ';"></div>'
+ '<div class="feature-content">'
+ '<span class="ftag">' + esc(t('featured_recommended')) + '</span>'
+ '<h3>' + esc(app.name) + '</h3>'
+ '<p class="fdesc">' + esc(app.category_name) + '</p>'
+ '<div class="feature-rating"><span class="stars">' + '\u2605'.repeat(5) + '</span> ' + app.rating + '</div>'
+ '</div></div>';
}
});
grid.innerHTML = html;
observeFadeIn();
}
/* =========================================================================
渲染:新上架
========================================================================= */
function renderNew(list) {
var wrap = document.getElementById('newScroll');
if (!list || !list.length) { wrap.innerHTML = '<div class="empty-state"><p>' + esc(t('empty_new')) + '</p></div>'; return; }
var html = '';
list.forEach(function (app) {
var grad = gradFromName(app.name);
html += '<div class="new-card fade-in" onclick="openApp(' + app.id + ')">'
+ '<span class="new-badge">NEW</span>'
+ '<div class="nc-head">'
+ appIconHTML(app, 'sm')
+ '<div><div class="nc-name">' + esc(app.name) + '</div><div class="nc-dev">' + esc(app.developer) + '</div></div>'
+ '</div>'
+ '<p class="nc-desc">' + esc(app.description) + '</p>'
+ '<div class="nc-foot">'
+ '<span class="nc-rate"><span class="stars">' + '\u2605' + '</span>' + app.rating + '</span>'
+ '<span style="font-size:11px;color:var(--text-tertiary);">' + esc(app.size) + '</span>'
+ '</div></div>';
});
wrap.innerHTML = html;
observeFadeIn();
}
/* =========================================================================
渲染:应用库网格
========================================================================= */
function renderAppGrid(list) {
var grid = document.getElementById('appGrid');
if (!list || !list.length) {
grid.innerHTML = '<div class="empty-state" style="grid-column:1/-1;">'
+ '<svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>'
+ '<p>' + esc(t('empty_apps')) + '</p></div>';
return;
}
var html = '';
list.forEach(function (app) {
var grad = gradFromName(app.name);
html += '<div class="app-card fade-in" onclick="openApp(' + app.id + ')">'
+ '<div class="ac-head">'
+ appIconHTML(app, '')
+ '<div class="ac-info"><div class="ac-name">' + esc(app.name) + '</div><div class="ac-cat">' + esc(app.category_name) + '</div></div>'
+ '</div>'
+ '<p class="ac-desc">' + esc(app.description) + '</p>'
+ '<div class="ac-foot">'
+ '<span class="ac-rate"><span class="stars">' + '\u2605' + '</span>' + app.rating + '</span>'
+ '<button class="ac-dl" onclick="event.stopPropagation();openApp(' + app.id + ')">' + esc(t('download')) + '</button>'
+ '</div></div>';
});
grid.innerHTML = html;
observeFadeIn();
}
/* 骨架屏 */
function renderGridSkeleton() {
var grid = document.getElementById('appGrid');
var html = '';
for (var i = 0; i < 12; i++) {
html += '<div class="app-card sk-card">'
+ '<div class="ac-head"><div class="skeleton" style="width:56px;height:56px;border-radius:14px;"></div>'
+ '<div style="flex:1;"><div class="skeleton sk-line w70" style="height:14px;"></div><div class="skeleton sk-line w50" style="margin-top:8px;height:12px;"></div></div></div>'
+ '<div class="skeleton sk-line w90" style="margin-top:14px;height:12px;"></div>'
+ '<div class="skeleton sk-line w50" style="margin-top:6px;height:12px;"></div>'
+ '<div style="display:flex;justify-content:space-between;margin-top:14px;"><div class="skeleton sk-line" style="width:50px;height:12px;"></div><div class="skeleton" style="width:50px;height:28px;border-radius:980px;"></div></div>'
+ '</div>';
}
grid.innerHTML = html;
}
/* =========================================================================
分页
========================================================================= */
function renderPagination() {
var el = document.getElementById('pagination');
var total = state.total, ps = PAGE_SIZE, page = state.page;
var pages = Math.ceil(total / ps);
if (pages <= 1) { el.innerHTML = ''; return; }
var html = '';
html += '<button class="page-btn" onclick="goPage(' + (page - 1) + ')" ' + (page <= 1 ? 'disabled' : '') + '>&lsaquo;</button>';
var maxVisible = 5;
var startP = Math.max(1, page - 2);
var endP = Math.min(pages, startP + maxVisible - 1);
if (endP - startP < maxVisible - 1) startP = Math.max(1, endP - maxVisible + 1);
if (startP > 1) {
html += '<button class="page-btn" onclick="goPage(1)">1</button>';
if (startP > 2) html += '<span class="page-ellipsis">...</span>';
}
for (var p = startP; p <= endP; p++) {
html += '<button class="page-btn ' + (p === page ? 'active' : '') + '" onclick="goPage(' + p + ')">' + p + '</button>';
}
if (endP < pages) {
if (endP < pages - 1) html += '<span class="page-ellipsis">...</span>';
html += '<button class="page-btn" onclick="goPage(' + pages + ')">' + pages + '</button>';
}
html += '<button class="page-btn" onclick="goPage(' + (page + 1) + ')" ' + (page >= pages ? 'disabled' : '') + '>&rsaquo;</button>';
el.innerHTML = html;
}
function goPage(p) {
if (p < 1) return;
var pages = Math.ceil(state.total / PAGE_SIZE);
if (pages && p > pages) return;
state.page = p;
loadLibrary();
document.getElementById('library').scrollIntoView({ behavior: 'smooth', block: 'start' });
}
/* =========================================================================
排序
========================================================================= */
function toggleSort(e) {
if (e) e.stopPropagation();
var menu = document.getElementById('sortMenu');
menu.classList.toggle('open');
}
function setSort(sort) {
state.sort = sort;
state.page = 1;
var labelKey = { '': 'sort_default', 'rating': 'sort_rating', 'downloads': 'sort_downloads', 'newest': 'sort_newest' };
document.getElementById('sortLabel').textContent = t(labelKey[sort] || 'sort_default');
document.querySelectorAll('#sortMenu .sort-item').forEach(function (it) {
it.classList.toggle('active', it.getAttribute('data-sort') === sort);
});
document.getElementById('sortMenu').classList.remove('open');
loadLibrary();
}
document.addEventListener('click', function (e) {
var menu = document.getElementById('sortMenu');
var btn = document.getElementById('sortBtn');
if (menu.classList.contains('open') && !menu.contains(e.target) && !btn.contains(e.target)) {
menu.classList.remove('open');
}
});
/* =========================================================================
加载应用库
========================================================================= */
function loadLibrary() {
renderGridSkeleton();
fetchApps({
category_id: state.currentCat,
keyword: state.keyword,
sort: state.sort,
page: state.page,
page_size: PAGE_SIZE
}).then(function (data) {
state.total = data.total;
state.page = data.page;
renderAppGrid(data.list);
renderPagination();
});
}
/* =========================================================================
搜索(debounce 300ms)
========================================================================= */
var debouncedSearch = debounce(function (kw) {
state.keyword = kw;
state.page = 1;
loadLibrary();
}, 300);
/* =========================================================================
模态框:应用详情
========================================================================= */
function openApp(id) {
var body = document.getElementById('modalBody');
body.innerHTML = '<div style="padding-top:24px;">'
+ '<div style="display:flex;gap:20px;align-items:center;">'
+ '<div class="skeleton" style="width:88px;height:88px;border-radius:20px;"></div>'
+ '<div style="flex:1;"><div class="skeleton sk-line w50" style="height:24px;"></div><div class="skeleton sk-line w70" style="margin-top:10px;height:14px;"></div><div class="skeleton sk-line w40" style="margin-top:8px;height:14px;"></div></div>'
+ '</div></div>';
document.getElementById('modalBackdrop').classList.add('open');
document.body.style.overflow = 'hidden';
if (state.cache[id]) { renderModal(state.cache[id]); return; }
fetchDetail(id).then(function (app) {
if (!app) { body.innerHTML = '<div class="empty-state"><p>' + esc(t('empty_app_detail')) + '</p></div>'; return; }
state.cache[id] = app;
renderModal(app);
});
}
function renderModal(app) {
var body = document.getElementById('modalBody');
var grad = gradFromName(app.name);
var tags = parseJSON(app.tags, []);
var features = parseJSON(app.features, []);
var screenshots = parseJSON(app.screenshots, []);
var tagsHtml = tags.map(function (tag) { return '<span class="tag-pill">' + esc(tag) + '</span>'; }).join('');
var featHtml = features.map(function (f) { return '<li>' + esc(f) + '</li>'; }).join('');
var shotsHtml = screenshots.map(function (s, i) {
var sg = gradFromName(s + i);
return '<div class="shot-item" style="background:' + sg + ';"><span class="shot-label">' + esc(s) + '</span></div>';
}).join('');
body.innerHTML =
'<div class="modal-hero">'
+ appIconHTML(app, 'lg')
+ '<div class="mh-info">'
+ '<h2>' + esc(app.name) + '</h2>'
+ '<div class="mh-dev">' + esc(app.developer) + '</div>'
+ '<div class="mh-meta">'
+ '<div class="mh-rating"><span class="stars">' + starsHtml(app.rating) + '</span><span class="rnum">' + app.rating + '</span><span class="rcount">(' + formatNumI18n(app.reviews) + ' ' + esc(t('rating_count')) + ')</span></div>'
+ '<span class="mh-price">' + esc(app.price || t('download')) + '</span>'
+ '</div>'
+ '</div>'
+ '<button class="modal-dl-btn" onclick="downloadApp(' + app.id + ',\'' + esc(app.name).replace(/'/g, "\\'") + '\')">' + esc(t('download')) + '</button>'
+ '</div>'
+ '<div class="modal-stats">'
+ '<div class="modal-stat"><div class="ms-val">' + formatNumI18n(app.downloads) + '</div><div class="ms-label">' + esc(t('downloads_label')) + '</div></div>'
+ '<div class="modal-stat"><div class="ms-val">' + formatNumI18n(app.reviews) + '</div><div class="ms-label">' + esc(t('reviews_label')) + '</div></div>'
+ '<div class="modal-stat"><div class="ms-val">' + esc(app.size || '-') + '</div><div class="ms-label">' + esc(t('size_label')) + '</div></div>'
+ '</div>'
+ '<div class="modal-section"><h4>' + esc(t('app_intro')) + '</h4><p>' + esc(app.description) + '</p></div>'
+ (tags.length ? '<div class="modal-section"><h4>' + esc(t('app_tags')) + '</h4><div class="tag-row">' + tagsHtml + '</div></div>' : '')
+ (features.length ? '<div class="modal-section"><h4>' + esc(t('app_features')) + '</h4><ul class="feature-list">' + featHtml + '</ul></div>' : '')
+ (screenshots.length ? '<div class="modal-section"><h4>' + esc(t('app_screenshots')) + '</h4><div class="shots-row">' + shotsHtml + '</div></div>' : '')
+ '<div class="modal-version">'
+ '<span><b>' + esc(t('version_label')) + '</b>' + esc(app.version || '-') + '</span>'
+ '<span><b>' + esc(t('update_label')) + '</b>' + esc(app.update_date || '-') + '</span>'
+ '<span><b>' + esc(t('platform_label')) + '</b>' + esc(app.platform || '-') + '</span>'
+ '<span><b>' + esc(t('category_label')) + '</b>' + esc(app.category_name || '-') + '</span>'
+ '</div>';
}
function downloadApp(id, name) {
showToast(t('toast_downloading') + name + t('toast_downloading_end'));
}
function closeModal() {
document.getElementById('modalBackdrop').classList.remove('open');
document.body.style.overflow = '';
}
function closeModalOnBackdrop(e) {
if (e.target === document.getElementById('modalBackdrop')) closeModal();
}
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') closeModal();
});
/* =========================================================================
IntersectionObserver 淡入动画
========================================================================= */
var fadeObserver = null;
function observeFadeIn() {
if (!('IntersectionObserver' in window)) {
document.querySelectorAll('.fade-in').forEach(function (el) { el.classList.add('visible'); });
return;
}
if (!fadeObserver) {
fadeObserver = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
fadeObserver.unobserve(entry.target);
}
});
}, { threshold: 0.1, rootMargin: '0px 0px -40px 0px' });
}
document.querySelectorAll('.fade-in:not(.visible)').forEach(function (el) { fadeObserver.observe(el); });
}
/* =========================================================================
导航 & 交互
========================================================================= */
function navTo(e, key) {
if (e) e.preventDefault();
document.querySelectorAll('#mainNav .nav-link').forEach(function (l) { l.classList.remove('active'); });
var link = document.querySelector('#mainNav .nav-link[data-nav="' + key + '"]');
if (link) link.classList.add('active');
if (key === 'home') {
window.scrollTo({ top: 0, behavior: 'smooth' });
} else if (key === 'discover') {
document.getElementById('featuredSection').scrollIntoView({ behavior: 'smooth' });
} else if (key === 'ranking') {
state.sort = 'downloads';
state.page = 1;
document.getElementById('sortLabel').textContent = t('sort_downloads');
document.querySelectorAll('#sortMenu .sort-item').forEach(function (it) {
it.classList.toggle('active', it.getAttribute('data-sort') === 'downloads');
});
loadLibrary();
document.getElementById('library').scrollIntoView({ behavior: 'smooth' });
} else if (key === 'new') {
document.getElementById('newSection').scrollIntoView({ behavior: 'smooth' });
}
}
function goHome(e) { if (e) e.preventDefault(); navTo(e, 'home'); }
function exploreNow(e) { if (e) e.preventDefault(); document.getElementById('library').scrollIntoView({ behavior: 'smooth' }); }
function seeAllFeatured(e) { if (e) e.preventDefault(); document.getElementById('featuredSection').scrollIntoView({ behavior: 'smooth' }); }
function seeAllNew(e) { if (e) e.preventDefault(); document.getElementById('newSection').scrollIntoView({ behavior: 'smooth' }); }
/* =========================================================================
语言切换回调:重新渲染动态内容
========================================================================= */
var lastHomeData = null;
function onLangChange() {
applyI18n();
loadLibrary();
if (lastHomeData) {
renderCategories(lastHomeData.categories || []);
renderFeatured(lastHomeData.featured || []);
renderNew(lastHomeData.new || []);
}
}
/* =========================================================================
初始化
========================================================================= */
function init() {
applyI18n();
document.getElementById('headerSearch').addEventListener('input', function (e) {
var kw = e.target.value;
document.getElementById('libSearch').value = kw;
debouncedSearch(kw);
});
document.getElementById('libSearch').addEventListener('input', function (e) {
var kw = e.target.value;
document.getElementById('headerSearch').value = kw;
debouncedSearch(kw);
});
loadLibrary();
apiGet('/home', function () { return MOCK.home(); }).then(function (res) {
var d = res.data;
lastHomeData = d;
renderCategories(d.categories || []);
renderFeatured(d.featured || []);
renderNew(d.new || []);
}).catch(function () {
fetchCategories().then(function (cats) { lastHomeData = lastHomeData || {}; lastHomeData.categories = cats; renderCategories(cats); });
fetchFeatured(5).then(function (feat) { lastHomeData = lastHomeData || {}; lastHomeData.featured = feat; renderFeatured(feat); });
fetchNew(4).then(function (nw) { lastHomeData = lastHomeData || {}; lastHomeData.new = nw; renderNew(nw); });
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
</script>
</body>
</html>