feat: 全站20个模板添加国际化(i18n)支持 - 简体中文/繁体中文/英文三语切换

- 所有18套前端模板 + 管理后台共20个HTML文件添加i18n引擎
- 前端JavaScript方案:I18N_LOCALES翻译字典 + data-i18n属性 + t()函数调用
- 语言切换器UI:下拉菜单选择,匹配各模板主题风格
- 语言持久化:localStorage存储 + URL参数覆盖 + 浏览器语言自动检测
- onLangChange回调:语言切换时动态重新渲染所有内容
- 支持语言:zh-CN(简体中文) / zh-TW(繁体中文) / en(English)
This commit is contained in:
2026-09-09 16:58:40 +08:00
parent 69c59f4fbd
commit a84129330e
20 changed files with 9343 additions and 3855 deletions

View File

@@ -94,6 +94,34 @@ img { max-width: 100%; display: block; }
.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;
@@ -383,17 +411,38 @@ img { max-width: 100%; display: block; }
<div class="header-inner">
<a href="#" class="logo" onclick="goHome(event)">
<span class="logo-mark">&#10010;</span>
<span>应用商店</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')">首页</a>
<a href="#" class="nav-link" data-nav="discover" onclick="navTo(event,'discover')">发现</a>
<a href="#" class="nav-link" data-nav="ranking" onclick="navTo(event,'ranking')">排行榜</a>
<a href="#" class="nav-link" data-nav="new" onclick="navTo(event,'new')">新上架</a>
<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" placeholder="搜索应用、游戏..." autocomplete="off">
<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>
@@ -401,11 +450,11 @@ img { max-width: 100%; display: block; }
<!-- ======================= HERO ======================= -->
<section class="hero">
<div class="container">
<span class="hero-eyebrow">全新体验 · 精选推荐</span>
<h1>发现精彩应用</h1>
<p>探索海量优质应用与游戏,从效率工具到娱乐大作,总有一款适合你。</p>
<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>
@@ -427,10 +476,10 @@ img { max-width: 100%; display: block; }
<div class="container">
<div class="section-head">
<div>
<h2 class="section-title">精选推荐</h2>
<p class="section-sub">编辑团队为你精心挑选</p>
<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)">查看全部 &rsaquo;</a>
<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>
@@ -445,10 +494,10 @@ img { max-width: 100%; display: block; }
<div class="container">
<div class="section-head">
<div>
<h2 class="section-title">新上架</h2>
<p class="section-sub">最新加入的应用</p>
<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)">查看全部 &rsaquo;</a>
<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">
@@ -466,26 +515,26 @@ img { max-width: 100%; display: block; }
<div class="container">
<div class="section-head">
<div>
<h2 class="section-title">全部应用</h2>
<p class="section-sub">浏览所有应用</p>
<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" placeholder="搜索应用名称..." autocomplete="off">
<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">默认排序</span>
<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 class="check">&#10003;</span></div>
<div class="sort-item" data-sort="rating" onclick="setSort('rating')">评分最高 <span class="check">&#10003;</span></div>
<div class="sort-item" data-sort="downloads" onclick="setSort('downloads')">下载最多 <span class="check">&#10003;</span></div>
<div class="sort-item" data-sort="newest" onclick="setSort('newest')">最新发布 <span class="check">&#10003;</span></div>
<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>
@@ -499,36 +548,36 @@ img { max-width: 100%; display: block; }
<div class="footer-inner">
<div class="footer-top">
<div class="footer-brand">
<div class="logo"><span class="logo-mark">&#10010;</span><span>应用商店</span></div>
<p>发现、下载并体验海量优质应用与游戏。我们致力于为用户提供安全、丰富的应用生态。</p>
<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>探索</h5>
<a href="#" onclick="navTo(event,'home')">首页</a>
<a href="#" onclick="navTo(event,'discover')">发现</a>
<a href="#" onclick="navTo(event,'ranking')">排行榜</a>
<a href="#" onclick="navTo(event,'new')">新上架</a>
<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>关于</h5>
<a href="#">关于我们</a>
<a href="#">隐私政策</a>
<a href="#">服务条款</a>
<a href="#">联系方式</a>
<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>开发者</h5>
<a href="#">开发者中心</a>
<a href="#">上架指南</a>
<a href="#">API 文档</a>
<a href="#">合作申请</a>
<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>&copy; 2026 应用商店. 保留所有权利.</span>
<span>仅供学习展示用途</span>
<span data-i18n="footer_copyright">&copy; 2026 应用商店. 保留所有权利.</span>
<span data-i18n="footer_disclaimer">仅供学习展示用途</span>
</div>
</div>
</footer>
@@ -536,7 +585,7 @@ img { max-width: 100%; display: block; }
<!-- ======================= MODAL ======================= -->
<div class="modal-backdrop" id="modalBackdrop" onclick="closeModalOnBackdrop(event)">
<div class="modal" id="appModal">
<button class="modal-close" onclick="closeModal()" aria-label="关闭">&times;</button>
<button class="modal-close" onclick="closeModal()" aria-label="关闭" data-i18n-aria="modal_close">&times;</button>
<div class="modal-body" id="modalBody"></div>
</div>
</div>
@@ -546,6 +595,253 @@ img { max-width: 100%; display: block; }
<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 '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.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');
});
/* =========================================================================
配置 & 工具函数
========================================================================= */
@@ -555,15 +851,15 @@ var PAGE_SIZE = 12;
/* 状态 */
var state = {
categories: [],
currentCat: '', // 当前分类 id('' 表示全部)
currentCat: '',
sort: '',
keyword: '',
page: 1,
total: 0,
cache: {} // app id -> app detail cache
cache: {}
};
/* ---- 工具:格式化大数字 56000000 -> "5600万" ---- */
/* ---- 工具:格式化大数字 ---- */
function formatNum(n) {
if (n == null || isNaN(n)) return '0';
n = Number(n);
@@ -572,17 +868,23 @@ function formatNum(n) {
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 full = Math.floor(rating);
var half = (rating - full) >= 0.5 ? 1 : 0;
var empty = 5 - full - half;
var s = '';
for (var i = 0; i < full; i++) s += '\u2605';
if (half) s += '\u2605'; // 简化用实心星表示半星区域
for (var j = 0; j < empty; j++) s += '\u2606';
// 全实心更美观,这里全部用实心 + 透明度控制空星
var html = '';
for (var k = 0; k < 5; k++) {
var fill = Math.max(0, Math.min(1, rating - k));
@@ -618,7 +920,6 @@ function gradFromName(name) {
}
function initials(name) {
name = name || '?';
// 取首字符(兼容中文/英文)
return name.charAt(0);
}
@@ -637,7 +938,7 @@ function isIconURL(icon) {
return v.indexOf('/uploads/') === 0 || v.indexOf('http://') === 0 || v.indexOf('https://') === 0;
}
/* ---- 工具:生成应用图标 HTML(有 URL 用真实图片,否则渐变占位) ---- */
/* ---- 工具:生成应用图标 HTML ---- */
function appIconHTML(app, cls) {
var grad = gradFromName(app.name);
var clsAttr = 'app-icon' + (cls ? ' ' + cls : '');
@@ -672,7 +973,7 @@ function showToast(msg) {
}
/* =========================================================================
Mock 数据(当后端 API 不可用时自动回退使用)
Mock 数据
========================================================================= */
var MOCK = (function () {
var catDefs = [
@@ -687,6 +988,36 @@ var MOCK = (function () {
{ 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: '免费' },
@@ -715,7 +1046,8 @@ var MOCK = (function () {
];
var apps = appDefs.map(function (a, i) {
var catName = (catDefs.find(function (c) { return c.id === a.cat; }) || {}).name || '应用';
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,
@@ -733,7 +1065,7 @@ var MOCK = (function () {
platform: a.plat,
price: a.price,
tags: JSON.stringify(a.tags),
screenshots: JSON.stringify([a.name + ' 截图 1', a.name + ' 截图 2', a.name + ' 截图 3']),
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),
@@ -745,7 +1077,7 @@ var MOCK = (function () {
function ok(data) { return { code: 0, message: 'ok', data: data }; }
return {
categories: function () { return ok(catDefs.slice()); },
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));
@@ -778,7 +1110,7 @@ var MOCK = (function () {
},
home: function () {
return ok({
categories: catDefs.slice(),
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);
@@ -789,7 +1121,7 @@ var MOCK = (function () {
})();
/* =========================================================================
API 调用层(先尝试真实后端,失败则回退 mock)
API 调用层
========================================================================= */
function apiGet(path, useMockFn) {
var url = API_BASE + path;
@@ -803,8 +1135,7 @@ function apiGet(path, useMockFn) {
return res;
})
.catch(function (err) {
// 回退到 mock
console.warn('[API] 请求失败,使用本地数据:', url, err.message);
console.warn('[API] Request failed, using local data:', url, err.message);
return useMockFn();
});
}
@@ -838,7 +1169,7 @@ function fetchDetail(id) {
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>全部</button>';
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>';
@@ -849,13 +1180,11 @@ function renderCategories(cats) {
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' });
}
@@ -865,33 +1194,31 @@ function selectCat(catId) {
========================================================================= */
function renderFeatured(list) {
var grid = document.getElementById('featuredGrid');
if (!list || !list.length) { grid.innerHTML = '<div class="empty-state" style="grid-column:1/-1;"><p>暂无精选应用</p></div>'; return; }
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">编辑精选</span>'
+ '<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; ' + formatNum(app.downloads) + ' 下载</div>'
+ '<button class="feature-btn" onclick="event.stopPropagation();openApp(' + app.id + ')">查看详情</button>'
+ '<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">推荐</span>'
+ '<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>';
}
});
// 补齐到至少 3 个(如果不足)
grid.innerHTML = html;
observeFadeIn();
}
@@ -901,7 +1228,7 @@ function renderFeatured(list) {
========================================================================= */
function renderNew(list) {
var wrap = document.getElementById('newScroll');
if (!list || !list.length) { wrap.innerHTML = '<div class="empty-state"><p>暂无新应用</p></div>'; return; }
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);
@@ -929,7 +1256,7 @@ function renderAppGrid(list) {
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>没有找到匹配的应用</p></div>';
+ '<p>' + esc(t('empty_apps')) + '</p></div>';
return;
}
var html = '';
@@ -943,7 +1270,7 @@ function renderAppGrid(list) {
+ '<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 + ')">下载</button>'
+ '<button class="ac-dl" onclick="event.stopPropagation();openApp(' + app.id + ')">' + esc(t('download')) + '</button>'
+ '</div></div>';
});
grid.innerHTML = html;
@@ -1019,8 +1346,8 @@ function toggleSort(e) {
function setSort(sort) {
state.sort = sort;
state.page = 1;
var labels = { '': '默认排序', 'rating': '评分最高', 'downloads': '下载最多', 'newest': '最新发布' };
document.getElementById('sortLabel').textContent = labels[sort] || '默认排序';
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);
});
@@ -1068,7 +1395,6 @@ var debouncedSearch = debounce(function (kw) {
========================================================================= */
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>'
@@ -1077,11 +1403,10 @@ function openApp(id) {
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>应用信息不存在</p></div>'; return; }
if (!app) { body.innerHTML = '<div class="empty-state"><p>' + esc(t('empty_app_detail')) + '</p></div>'; return; }
state.cache[id] = app;
renderModal(app);
});
@@ -1094,7 +1419,7 @@ function renderModal(app) {
var features = parseJSON(app.features, []);
var screenshots = parseJSON(app.screenshots, []);
var tagsHtml = tags.map(function (t) { return '<span class="tag-pill">' + esc(t) + '</span>'; }).join('');
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);
@@ -1108,31 +1433,31 @@ function renderModal(app) {
+ '<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">(' + formatNum(app.reviews) + ' 评分)</span></div>'
+ '<span class="mh-price">' + esc(app.price || '免费') + '</span>'
+ '<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, "\\'") + '\')">下载</button>'
+ '<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">' + formatNum(app.downloads) + '</div><div class="ms-label">下载量</div></div>'
+ '<div class="modal-stat"><div class="ms-val">' + formatNum(app.reviews) + '</div><div class="ms-label">评分数</div></div>'
+ '<div class="modal-stat"><div class="ms-val">' + esc(app.size || '-') + '</div><div class="ms-label">大小</div></div>'
+ '<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>应用介绍</h4><p>' + esc(app.description) + '</p></div>'
+ (tags.length ? '<div class="modal-section"><h4>标签</h4><div class="tag-row">' + tagsHtml + '</div></div>' : '')
+ (features.length ? '<div class="modal-section"><h4>功能特性</h4><ul class="feature-list">' + featHtml + '</ul></div>' : '')
+ (screenshots.length ? '<div class="modal-section"><h4>应用截图</h4><div class="shots-row">' + shotsHtml + '</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>版本:</b>' + esc(app.version || '-') + '</span>'
+ '<span><b>更新日期:</b>' + esc(app.update_date || '-') + '</span>'
+ '<span><b>平台:</b>' + esc(app.platform || '-') + '</span>'
+ '<span><b>分类:</b>' + esc(app.category_name || '-') + '</span>'
+ '<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('\u5f00\u59cb\u4e0b\u8f7d\u300c' + name + '\u300d'); // 开始下载「name」
showToast(t('toast_downloading') + name + t('toast_downloading_end'));
}
function closeModal() {
@@ -1183,7 +1508,7 @@ function navTo(e, key) {
} else if (key === 'ranking') {
state.sort = 'downloads';
state.page = 1;
document.getElementById('sortLabel').textContent = '下载最多';
document.getElementById('sortLabel').textContent = t('sort_downloads');
document.querySelectorAll('#sortMenu .sort-item').forEach(function (it) {
it.classList.toggle('active', it.getAttribute('data-sort') === 'downloads');
});
@@ -1198,11 +1523,26 @@ function exploreNow(e) { if (e) e.preventDefault(); document.getElementById('lib
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;
@@ -1214,21 +1554,18 @@ function init() {
debouncedSearch(kw);
});
// 首次加载应用库
loadLibrary();
// 加载首页数据(分类 + 精选 + 新上架)
// 尝试 home 聚合接口,失败则分别请求
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(renderCategories);
fetchFeatured(5).then(renderFeatured);
fetchNew(4).then(renderNew);
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); });
});
}