Files
web_template/web/dist/app3/index.html
2026-07-10 10:07:19 +00:00

1640 lines
72 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>应用集市 - 发现好应用,点亮新生活</title>
<style>
/* ==================== CSS Variables ==================== */
:root {
--coral: #ff6b6b;
--mint: #4ecdc4;
--sky: #45b7d8;
--amber: #ffa94d;
--purple: #9775fa;
--pink: #f783ac;
--green: #51cf66;
--bg-start: #f0f4ff;
--bg-end: #e8f5e9;
--card-bg: #ffffff;
--text-dark: #2d3748;
--text-mid: #4a5568;
--text-light: #a0aec0;
--shadow-soft: 0 4px 24px rgba(0,0,0,0.06);
--shadow-hover: 0 16px 48px rgba(0,0,0,0.12);
--radius-sm: 12px;
--radius-md: 16px;
--radius-lg: 20px;
--radius-xl: 24px;
--radius-pill: 999px;
--bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
/* ==================== Reset & Base ==================== */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: var(--font);
color: var(--text-dark);
background: linear-gradient(160deg, var(--bg-start) 0%, var(--bg-end) 100%);
min-height: 100vh;
overflow-x: hidden;
line-height: 1.6;
}
a { color: inherit; text-decoration: none; }
button { font-family: inherit; cursor: pointer; border: none; outline: none; }
input { font-family: inherit; outline: none; }
img { max-width: 100%; display: block; }
::selection { background: rgba(78,205,196,0.3); }
/* ==================== Background Decorations ==================== */
.bg-decor { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.blob {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.25;
animation: floatBlob 20s ease-in-out infinite;
}
.blob-1 { width: 400px; height: 400px; background: var(--coral); top: -120px; left: -100px; }
.blob-2 { width: 500px; height: 500px; background: var(--mint); bottom: -180px; right: -120px; animation-delay: -5s; }
.blob-3 { width: 300px; height: 300px; background: var(--sky); top: 35%; right: 5%; animation-delay: -10s; }
.blob-4 { width: 350px; height: 350px; background: var(--purple); top: 60%; left: 10%; animation-delay: -15s; }
.dots-pattern {
position: absolute; inset: 0;
background-image: radial-gradient(circle, rgba(69,183,216,0.08) 1.5px, transparent 1.5px);
background-size: 28px 28px;
}
@keyframes floatBlob {
0%, 100% { transform: translate(0,0) scale(1); }
33% { transform: translate(40px,-30px) scale(1.08); }
66% { transform: translate(-30px,20px) scale(0.95); }
}
/* ==================== Container ==================== */
.container { max-width: 1400px; margin: 0 auto; padding: 0 24px; position: relative; z-index: 1; }
/* ==================== Header ==================== */
.header {
position: sticky; top: 0; z-index: 100;
background: rgba(255,255,255,0.75);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border-bottom: 1px solid rgba(255,255,255,0.6);
transition: box-shadow 0.3s var(--ease-out);
}
.header.scrolled { box-shadow: 0 2px 20px rgba(0,0,0,0.06); }
.header-inner {
max-width: 1400px; margin: 0 auto; padding: 14px 24px;
display: flex; align-items: center; gap: 32px;
}
.logo {
font-size: 26px; font-weight: 800; white-space: nowrap;
background: linear-gradient(135deg, var(--coral), var(--mint), var(--sky));
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: -0.5px;
}
.logo::before { content: "✦ "; -webkit-text-fill-color: initial; }
.nav { display: flex; gap: 6px; }
.nav-link {
padding: 8px 18px; border-radius: var(--radius-pill);
font-size: 15px; font-weight: 500; color: var(--text-mid);
transition: all 0.3s var(--bounce);
position: relative;
}
.nav-link:hover { background: rgba(78,205,196,0.12); color: var(--mint); transform: translateY(-2px); }
.nav-link.active {
background: linear-gradient(135deg, var(--coral), var(--pink));
color: #fff; box-shadow: 0 4px 12px rgba(255,107,107,0.35);
}
.header-search { margin-left: auto; display: flex; align-items: center; gap: 8px; }
.search-pill-wrap {
display: flex; align-items: center; gap: 8px;
background: rgba(255,255,255,0.9);
border: 2px solid rgba(78,205,196,0.2);
border-radius: var(--radius-pill);
padding: 6px 6px 6px 18px;
transition: all 0.3s var(--bounce);
}
.search-pill-wrap:focus-within {
border-color: var(--mint);
box-shadow: 0 4px 16px rgba(78,205,196,0.2);
transform: scale(1.02);
}
.search-pill-wrap svg { width: 18px; height: 18px; color: var(--text-light); flex-shrink: 0; }
.search-pill {
border: none; background: transparent; font-size: 14px; width: 200px;
color: var(--text-dark);
}
.search-pill::placeholder { color: var(--text-light); }
.search-go {
padding: 8px 20px; border-radius: var(--radius-pill); font-size: 14px; font-weight: 600;
background: linear-gradient(135deg, var(--mint), var(--sky));
color: #fff; transition: all 0.3s var(--bounce);
}
.search-go:hover { transform: scale(1.08); box-shadow: 0 6px 16px rgba(78,205,196,0.4); }
.search-go:active { transform: scale(0.95); }
/* ==================== Hero ==================== */
.hero {
position: relative; max-width: 1400px; margin: 32px auto; padding: 0 24px; z-index: 1;
}
.hero-banner {
position: relative; border-radius: var(--radius-xl); overflow: hidden;
padding: 72px 48px; text-align: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 30%, #f093fb 60%, #f5576c 100%);
}
.hero-banner::before {
content: ""; position: absolute; inset: 0;
background: radial-gradient(circle at 20% 30%, rgba(255,255,255,0.15) 0%, transparent 40%),
radial-gradient(circle at 80% 70%, rgba(255,255,255,0.1) 0%, transparent 40%);
}
.hero-blob {
position: absolute; border-radius: 50%; filter: blur(40px); opacity: 0.35;
animation: floatBlob 12s ease-in-out infinite;
}
.hero-blob-1 { width: 200px; height: 200px; background: #fff; top: -60px; right: 10%; }
.hero-blob-2 { width: 150px; height: 150px; background: #4ecdc4; bottom: -40px; left: 15%; animation-delay: -4s; }
.hero-shape {
position: absolute; opacity: 0.15; animation: floatBlob 10s ease-in-out infinite;
}
.hero-shape-1 { width: 60px; height: 60px; border-radius: 30%; background: #fff; top: 20%; left: 8%; transform: rotate(20deg); }
.hero-shape-2 { width: 40px; height: 40px; border-radius: 50%; background: #ffd700; top: 60%; right: 12%; animation-delay: -3s; }
.hero-shape-3 { width: 80px; height: 12px; border-radius: 6px; background: #fff; bottom: 15%; right: 25%; transform: rotate(-15deg); animation-delay: -6s; }
.hero-content { position: relative; z-index: 2; }
.hero-badge {
display: inline-block; padding: 6px 20px; border-radius: var(--radius-pill);
background: rgba(255,255,255,0.2); backdrop-filter: blur(10px);
color: #fff; font-size: 14px; font-weight: 600; margin-bottom: 20px;
border: 1px solid rgba(255,255,255,0.3);
}
.hero-title {
font-size: 48px; font-weight: 800; color: #fff; line-height: 1.3; margin-bottom: 16px;
text-shadow: 0 2px 20px rgba(0,0,0,0.15); letter-spacing: -1px;
}
.hero-subtitle { font-size: 18px; color: rgba(255,255,255,0.9); margin-bottom: 36px; max-width: 560px; margin-left: auto; margin-right: auto; }
.hero-btn {
display: inline-flex; align-items: center; gap: 10px;
padding: 16px 40px; border-radius: var(--radius-pill);
background: #fff; color: var(--coral); font-size: 18px; font-weight: 700;
transition: all 0.4s var(--bounce);
box-shadow: 0 8px 28px rgba(0,0,0,0.2);
}
.hero-btn:hover { transform: translateY(-4px) scale(1.05); box-shadow: 0 14px 40px rgba(0,0,0,0.25); }
.hero-btn:active { transform: translateY(-2px) scale(1.02); }
.hero-btn svg { width: 22px; height: 22px; }
.hero-stats { display: flex; justify-content: center; gap: 48px; margin-top: 40px; }
.hero-stat { text-align: center; }
.hero-stat-num { font-size: 32px; font-weight: 800; color: #fff; }
.hero-stat-label { font-size: 13px; color: rgba(255,255,255,0.8); margin-top: 4px; }
/* ==================== Section Title ==================== */
.section { padding: 48px 0; position: relative; z-index: 1; }
.section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 32px; }
.section-title-wrap { display: flex; align-items: center; gap: 12px; }
.section-icon {
width: 40px; height: 40px; border-radius: var(--radius-sm);
display: flex; align-items: center; justify-content: center; font-size: 20px;
color: #fff; flex-shrink: 0;
}
.section-title { font-size: 28px; font-weight: 800; color: var(--text-dark); letter-spacing: -0.5px; }
.section-subtitle { font-size: 14px; color: var(--text-light); margin-top: 4px; }
.section-more {
font-size: 14px; font-weight: 600; color: var(--mint);
display: flex; align-items: center; gap: 4px; transition: all 0.3s var(--bounce);
}
.section-more:hover { gap: 8px; color: var(--sky); }
/* ==================== Category Pills ==================== */
.categories-section { padding: 32px 0 16px; position: relative; z-index: 1; }
.categories-wrap {
display: flex; flex-wrap: wrap; gap: 12px; justify-content: center;
}
.cat-pill {
padding: 10px 24px; border-radius: var(--radius-pill);
font-size: 15px; font-weight: 600; cursor: pointer;
background: var(--card-bg); color: var(--text-mid);
border: 2px solid transparent;
transition: all 0.35s var(--bounce);
box-shadow: var(--shadow-soft);
display: inline-flex; align-items: center; gap: 8px;
}
.cat-pill:hover { transform: translateY(-4px) scale(1.05); box-shadow: var(--shadow-hover); }
.cat-pill.active { color: #fff; border-color: rgba(255,255,255,0.3); transform: translateY(-2px); }
.cat-pill .cat-emoji { font-size: 18px; }
/* ==================== App Card (shared) ==================== */
.app-card {
background: var(--card-bg); border-radius: var(--radius-lg);
padding: 20px; box-shadow: var(--shadow-soft);
transition: all 0.4s var(--bounce); cursor: pointer;
position: relative; overflow: hidden;
display: flex; flex-direction: column;
}
.app-card::after {
content: ""; position: absolute; top: 0; right: 0;
width: 80px; height: 80px; border-radius: 50%;
background: var(--card-accent, var(--mint)); opacity: 0;
filter: blur(30px); transition: opacity 0.4s; pointer-events: none;
transform: translate(30px,-30px);
}
.app-card:hover {
transform: translateY(-10px) rotate(-1deg);
box-shadow: var(--shadow-hover);
}
.app-card:hover::after { opacity: 0.15; }
.app-card:active { transform: translateY(-4px) rotate(-0.5deg); }
.app-icon-wrap {
width: 64px; height: 64px; border-radius: 18px;
display: flex; align-items: center; justify-content: center;
font-size: 28px; font-weight: 700; color: #fff;
flex-shrink: 0; position: relative; overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.app-icon-wrap::after {
content: ""; position: absolute; inset: 0;
background: linear-gradient(135deg, rgba(255,255,255,0.3) 0%, transparent 50%);
}
.app-icon-img { width: 100%; height: 100%; object-fit: cover; border-radius: 18px; }
.app-card-top { display: flex; gap: 14px; align-items: flex-start; margin-bottom: 12px; }
.app-card-info { flex: 1; min-width: 0; }
.app-card-name {
font-size: 16px; font-weight: 700; color: var(--text-dark);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 2px;
}
.app-card-dev {
font-size: 12px; color: var(--text-light);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.app-card-desc {
font-size: 13px; color: var(--text-mid); line-height: 1.5;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
overflow: hidden; margin-bottom: 12px; flex: 1;
}
.app-card-bottom {
display: flex; align-items: center; justify-content: space-between; margin-top: auto;
}
.app-card-meta { display: flex; flex-direction: column; gap: 2px; }
.stars { display: flex; gap: 1px; font-size: 13px; line-height: 1; }
.star-full { color: #ffc94d; }
.star-half {
color: #ffc94d;
background: linear-gradient(90deg, #ffc94d 50%, #e2e8f0 50%);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
.star-empty { color: #e2e8f0; }
.app-card-rating-text { font-size: 12px; color: var(--text-light); margin-top: 2px; }
.app-card-downloads { font-size: 11px; color: var(--text-light); }
.btn-download {
padding: 8px 18px; border-radius: var(--radius-pill);
font-size: 13px; font-weight: 600; color: #fff;
background: linear-gradient(135deg, var(--mint), var(--sky));
transition: all 0.3s var(--bounce); white-space: nowrap;
box-shadow: 0 3px 10px rgba(78,205,196,0.3);
}
.btn-download:hover { transform: scale(1.1); box-shadow: 0 6px 16px rgba(78,205,196,0.4); }
.btn-download:active { transform: scale(0.95); }
.btn-download.coral { background: linear-gradient(135deg, var(--coral), var(--pink)); box-shadow: 0 3px 10px rgba(255,107,107,0.3); }
.btn-download.coral:hover { box-shadow: 0 6px 16px rgba(255,107,107,0.4); }
/* ==================== Featured Section ==================== */
.featured-grid {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px;
}
.featured-card {
background: var(--card-bg); border-radius: var(--radius-xl);
overflow: hidden; box-shadow: var(--shadow-soft);
transition: all 0.4s var(--bounce); cursor: pointer; position: relative;
}
.featured-card:hover { transform: translateY(-12px) rotate(-1.5deg); box-shadow: var(--shadow-hover); }
.featured-card-top {
padding: 28px 24px 20px; position: relative; overflow: hidden;
}
.featured-card-top::before {
content: ""; position: absolute; inset: 0; opacity: 0.08;
background: var(--card-grad, linear-gradient(135deg, var(--coral), var(--mint)));
}
.featured-card-top-inner { position: relative; z-index: 1; display: flex; gap: 16px; align-items: center; }
.featured-card .app-icon-wrap { width: 72px; height: 72px; border-radius: 20px; font-size: 32px; }
.featured-card-name { font-size: 20px; font-weight: 800; color: var(--text-dark); margin-bottom: 4px; }
.featured-card-dev { font-size: 13px; color: var(--text-light); }
.featured-card-body { padding: 0 24px 20px; }
.featured-card-desc {
font-size: 14px; color: var(--text-mid); line-height: 1.6;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
margin-bottom: 16px;
}
.featured-card-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 16px; }
.tag-pill {
padding: 4px 12px; border-radius: var(--radius-pill);
font-size: 12px; font-weight: 600;
}
.featured-card-footer {
display: flex; align-items: center; justify-content: space-between;
padding: 0 24px 24px;
}
.featured-card-stats { display: flex; gap: 20px; }
.featured-stat { display: flex; flex-direction: column; }
.featured-stat-val { font-size: 18px; font-weight: 700; color: var(--text-dark); }
.featured-stat-label { font-size: 11px; color: var(--text-light); }
.featured-card .btn-download { padding: 10px 24px; font-size: 14px; }
/* ==================== New Section ==================== */
.new-scroll-wrap {
display: flex; gap: 20px; overflow-x: auto; padding: 8px 4px 20px;
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
}
.new-scroll-wrap::-webkit-scrollbar { height: 8px; }
.new-scroll-wrap::-webkit-scrollbar-track { background: rgba(0,0,0,0.04); border-radius: 4px; }
.new-scroll-wrap::-webkit-scrollbar-thumb { background: var(--mint); border-radius: 4px; }
.new-card {
flex: 0 0 280px; scroll-snap-align: start;
background: var(--card-bg); border-radius: var(--radius-lg);
padding: 20px; box-shadow: var(--shadow-soft);
transition: all 0.4s var(--bounce); cursor: pointer; position: relative;
}
.new-card:hover { transform: translateY(-8px) rotate(1deg); box-shadow: var(--shadow-hover); }
.new-badge {
position: absolute; top: 14px; right: 14px;
padding: 4px 12px; border-radius: var(--radius-pill);
background: linear-gradient(135deg, var(--coral), #ff8787);
color: #fff; font-size: 11px; font-weight: 700; letter-spacing: 0.5px;
box-shadow: 0 3px 10px rgba(255,107,107,0.3);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.08); }
}
/* ==================== Library Section ==================== */
.library-toolbar {
display: flex; flex-wrap: wrap; gap: 16px; align-items: center; margin-bottom: 28px;
background: var(--card-bg); padding: 16px 20px; border-radius: var(--radius-lg);
box-shadow: var(--shadow-soft);
}
.library-search {
display: flex; align-items: center; gap: 8px;
background: rgba(240,244,255,0.8); border-radius: var(--radius-pill);
padding: 8px 18px; flex: 1; min-width: 200px;
border: 2px solid transparent; transition: all 0.3s var(--bounce);
}
.library-search:focus-within { border-color: var(--mint); background: #fff; }
.library-search svg { width: 18px; height: 18px; color: var(--text-light); flex-shrink: 0; }
.library-search input { border: none; background: transparent; font-size: 14px; width: 100%; color: var(--text-dark); }
.library-search input::placeholder { color: var(--text-light); }
.sort-select {
padding: 10px 20px; border-radius: var(--radius-pill); font-size: 14px; font-weight: 600;
background: linear-gradient(135deg, rgba(151,117,250,0.1), rgba(78,205,196,0.1));
color: var(--text-dark); border: 2px solid rgba(151,117,250,0.15);
cursor: pointer; transition: all 0.3s var(--bounce);
appearance: none; -webkit-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239775fa' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat; background-position: right 16px center;
padding-right: 40px;
}
.sort-select:hover { border-color: var(--purple); transform: translateY(-2px); }
.sort-select option { background: #fff; color: var(--text-dark); }
.library-count { font-size: 14px; color: var(--text-light); }
.library-count strong { color: var(--text-dark); }
.app-grid {
display: grid; grid-template-columns: repeat(5, 1fr); gap: 20px;
}
/* ==================== Skeleton ==================== */
.skeleton-card {
background: var(--card-bg); border-radius: var(--radius-lg); padding: 20px;
box-shadow: var(--shadow-soft);
}
.skel-shimmer {
background: linear-gradient(90deg, #f0f0f5 0%, #e8e8f0 30%, #f5f5fa 50%, #e8e8f0 70%, #f0f0f5 100%);
background-size: 200% 100%; animation: shimmer 1.5s infinite linear;
border-radius: 8px;
}
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
.skel-icon { width: 64px; height: 64px; border-radius: 18px; }
.skel-line { height: 12px; margin-bottom: 8px; }
.skel-line.short { width: 60%; }
.skel-line.mid { width: 80%; }
/* ==================== Empty State ==================== */
.empty-state {
grid-column: 1 / -1; text-align: center; padding: 60px 20px;
color: var(--text-light);
}
.empty-state-icon { font-size: 56px; margin-bottom: 16px; opacity: 0.5; }
.empty-state-text { font-size: 16px; }
/* ==================== Pagination ==================== */
.pagination {
display: flex; justify-content: center; align-items: center; gap: 8px; margin-top: 36px;
}
.page-btn {
min-width: 40px; height: 40px; border-radius: var(--radius-sm);
background: var(--card-bg); color: var(--text-mid); font-size: 14px; font-weight: 600;
box-shadow: var(--shadow-soft); transition: all 0.3s var(--bounce);
display: flex; align-items: center; justify-content: center; padding: 0 12px;
}
.page-btn:hover { transform: translateY(-3px); box-shadow: var(--shadow-hover); color: var(--mint); }
.page-btn.active {
background: linear-gradient(135deg, var(--coral), var(--pink));
color: #fff; box-shadow: 0 4px 12px rgba(255,107,107,0.35);
}
.page-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
.page-ellipsis { color: var(--text-light); padding: 0 4px; }
/* ==================== Modal ==================== */
.modal-overlay {
position: fixed; inset: 0; z-index: 1000;
background: rgba(45,55,72,0.4);
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
display: flex; align-items: center; justify-content: center;
padding: 24px; opacity: 0; pointer-events: none;
transition: opacity 0.3s var(--ease-out);
}
.modal-overlay.active { opacity: 1; pointer-events: auto; }
.modal {
background: #fff; border-radius: var(--radius-xl);
max-width: 720px; width: 100%; max-height: 88vh; overflow-y: auto;
box-shadow: 0 24px 80px rgba(0,0,0,0.2);
transform: scale(0.8) translateY(40px); opacity: 0;
transition: all 0.5s var(--bounce);
}
.modal-overlay.active .modal { transform: scale(1) translateY(0); opacity: 1; }
.modal::-webkit-scrollbar { width: 8px; }
.modal::-webkit-scrollbar-track { background: transparent; }
.modal::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.1); border-radius: 4px; }
.modal-header {
position: relative; padding: 32px 32px 24px; overflow: hidden;
}
.modal-header::before {
content: ""; position: absolute; inset: 0; opacity: 0.06;
background: var(--modal-grad, linear-gradient(135deg, var(--coral), var(--sky)));
}
.modal-header-inner { position: relative; z-index: 1; display: flex; gap: 20px; align-items: flex-start; }
.modal .app-icon-wrap { width: 88px; height: 88px; border-radius: 22px; font-size: 38px; }
.modal-app-name { font-size: 26px; font-weight: 800; color: var(--text-dark); margin-bottom: 6px; }
.modal-app-dev { font-size: 14px; color: var(--text-light); margin-bottom: 10px; }
.modal-rating-row { display: flex; align-items: center; gap: 8px; }
.modal-rating-num { font-size: 18px; font-weight: 700; color: #ffc94d; }
.modal-rating-text { font-size: 13px; color: var(--text-light); }
.modal-close {
position: absolute; top: 20px; right: 20px; z-index: 2;
width: 40px; height: 40px; border-radius: 50%;
background: rgba(255,255,255,0.9); color: var(--text-mid);
font-size: 22px; display: flex; align-items: center; justify-content: center;
transition: all 0.3s var(--bounce); box-shadow: var(--shadow-soft);
}
.modal-close:hover { transform: rotate(90deg) scale(1.1); background: var(--coral); color: #fff; }
.modal-body { padding: 0 32px 32px; }
.modal-section { margin-bottom: 24px; }
.modal-section-title {
font-size: 15px; font-weight: 700; color: var(--text-dark); margin-bottom: 12px;
display: flex; align-items: center; gap: 6px;
}
.modal-desc { font-size: 14px; color: var(--text-mid); line-height: 1.7; }
.modal-tags { display: flex; flex-wrap: wrap; gap: 8px; }
.modal-features { display: flex; flex-direction: column; gap: 10px; }
.modal-feature {
display: flex; align-items: center; gap: 10px; font-size: 14px; color: var(--text-mid);
}
.modal-feature-check {
width: 24px; height: 24px; border-radius: 50%; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
background: linear-gradient(135deg, var(--green), var(--mint)); color: #fff; font-size: 12px;
}
.modal-stats-grid {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px;
}
.modal-stat-item {
background: rgba(240,244,255,0.6); border-radius: var(--radius-md);
padding: 14px; text-align: center;
}
.modal-stat-val { font-size: 16px; font-weight: 700; color: var(--text-dark); margin-bottom: 2px; }
.modal-stat-label { font-size: 12px; color: var(--text-light); }
.modal-screenshots { display: flex; gap: 12px; overflow-x: auto; padding-bottom: 8px; }
.modal-screenshots::-webkit-scrollbar { height: 6px; }
.modal-screenshots::-webkit-scrollbar-thumb { background: var(--mint); border-radius: 3px; }
.screenshot-item {
flex: 0 0 200px; height: 360px; border-radius: var(--radius-md);
overflow: hidden; box-shadow: var(--shadow-soft);
}
.screenshot-item img { width: 100%; height: 100%; object-fit: cover; }
.screenshot-placeholder {
width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
font-size: 14px; color: rgba(255,255,255,0.7); font-weight: 600;
}
.modal-download-btn {
width: 100%; padding: 16px; border-radius: var(--radius-md);
font-size: 17px; font-weight: 700; color: #fff;
background: linear-gradient(135deg, var(--coral), var(--pink), var(--purple));
background-size: 200% 100%; transition: all 0.4s var(--bounce);
box-shadow: 0 6px 20px rgba(255,107,107,0.3);
display: flex; align-items: center; justify-content: center; gap: 10px;
}
.modal-download-btn:hover {
transform: scale(1.03); background-position: 100% 0;
box-shadow: 0 10px 28px rgba(255,107,107,0.4);
}
.modal-download-btn:active { transform: scale(0.98); }
.modal-download-btn svg { width: 22px; height: 22px; }
.modal-loading { padding: 60px; text-align: center; color: var(--text-light); }
.modal-error { padding: 60px; text-align: center; color: var(--coral); }
/* ==================== Footer ==================== */
.footer {
margin-top: 60px; position: relative; z-index: 1;
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
color: rgba(255,255,255,0.7); overflow: hidden;
}
.footer::before {
content: ""; position: absolute; top: 0; left: 0; right: 0; height: 4px;
background: linear-gradient(90deg, var(--coral), var(--amber), var(--green), var(--mint), var(--sky), var(--purple));
}
.footer-inner {
max-width: 1400px; margin: 0 auto; padding: 48px 24px 24px;
display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 40px;
}
.footer-brand .footer-logo {
font-size: 26px; font-weight: 800; margin-bottom: 12px;
background: linear-gradient(135deg, var(--coral), var(--mint), var(--sky));
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
.footer-brand p { font-size: 14px; line-height: 1.7; max-width: 300px; }
.footer-col h4 { font-size: 16px; font-weight: 700; color: #fff; margin-bottom: 16px; }
.footer-col a { display: block; font-size: 14px; padding: 6px 0; transition: all 0.3s var(--ease-out); }
.footer-col a:hover { color: var(--mint); transform: translateX(4px); }
.footer-bottom {
max-width: 1400px; margin: 0 auto; padding: 20px 24px;
border-top: 1px solid rgba(255,255,255,0.1);
text-align: center; font-size: 13px; color: rgba(255,255,255,0.5);
}
/* ==================== Scroll to Top ==================== */
.scroll-top {
position: fixed; bottom: 32px; right: 32px; z-index: 200;
width: 52px; height: 52px; border-radius: 50%;
background: linear-gradient(135deg, var(--mint), var(--sky));
color: #fff; font-size: 24px;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 8px 24px rgba(78,205,196,0.4);
opacity: 0; pointer-events: none; transform: scale(0.5);
transition: all 0.4s var(--bounce);
}
.scroll-top.show { opacity: 1; pointer-events: auto; transform: scale(1); }
.scroll-top:hover { transform: scale(1.15) rotate(-5deg); }
/* ==================== Toast ==================== */
.toast-container { position: fixed; top: 80px; right: 32px; z-index: 2000; display: flex; flex-direction: column; gap: 12px; }
.toast {
padding: 14px 24px; border-radius: var(--radius-md); font-size: 14px; font-weight: 600;
color: #fff; box-shadow: 0 8px 28px rgba(0,0,0,0.15);
transform: translateX(120%); opacity: 0; transition: all 0.4s var(--bounce);
display: flex; align-items: center; gap: 8px;
}
.toast.show { transform: translateX(0); opacity: 1; }
.toast-success { background: linear-gradient(135deg, var(--green), var(--mint)); }
.toast-error { background: linear-gradient(135deg, var(--coral), #ff8787); }
.toast-info { background: linear-gradient(135deg, var(--sky), var(--purple)); }
/* ==================== Reveal Animation ==================== */
.reveal {
opacity: 0; transform: translateY(40px) scale(0.96);
transition: all 0.7s var(--bounce);
}
.reveal.visible { opacity: 1; transform: translateY(0) scale(1); }
.reveal:nth-child(2) { transition-delay: 0.08s; }
.reveal:nth-child(3) { transition-delay: 0.16s; }
.reveal:nth-child(4) { transition-delay: 0.24s; }
.reveal:nth-child(5) { transition-delay: 0.32s; }
/* ==================== Responsive ==================== */
@media (max-width: 1200px) {
.app-grid { grid-template-columns: repeat(3, 1fr); }
.featured-grid { grid-template-columns: repeat(2, 1fr); }
.footer-inner { grid-template-columns: 2fr 1fr 1fr; }
.footer-col:last-child { display: none; }
}
@media (max-width: 900px) {
.hero-title { font-size: 36px; }
.hero-stats { gap: 28px; }
.section-title { font-size: 24px; }
.nav { display: none; }
.search-pill { width: 140px; }
}
@media (max-width: 768px) {
.app-grid { grid-template-columns: repeat(2, 1fr); }
.featured-grid { grid-template-columns: 1fr; }
.hero-banner { padding: 48px 24px; }
.hero-title { font-size: 28px; }
.hero-subtitle { font-size: 15px; }
.hero-stats { gap: 20px; }
.hero-stat-num { font-size: 24px; }
.header-inner { gap: 16px; padding: 12px 16px; }
.logo { font-size: 22px; }
.header-search { flex: 1; }
.search-pill { width: 100%; }
.container { padding: 0 16px; }
.section { padding: 32px 0; }
.library-toolbar { flex-direction: column; align-items: stretch; }
.modal-stats-grid { grid-template-columns: repeat(2, 1fr); }
.footer-inner { grid-template-columns: 1fr 1fr; gap: 24px; }
.footer-brand { grid-column: 1 / -1; }
.scroll-top { bottom: 20px; right: 20px; width: 44px; height: 44px; font-size: 20px; }
.toast-container { right: 16px; left: 16px; top: 70px; }
.toast { transform: translateY(-120%); }
.toast.show { transform: translateY(0); }
}
@media (max-width: 480px) {
.app-grid { grid-template-columns: 1fr; }
.hero-title { font-size: 24px; }
.hero-btn { padding: 14px 28px; font-size: 16px; }
.modal { border-radius: var(--radius-lg); }
.modal-header { padding: 24px 20px 16px; }
.modal-body { padding: 0 20px 20px; }
.modal .app-icon-wrap { width: 64px; height: 64px; border-radius: 18px; font-size: 28px; }
.modal-app-name { font-size: 20px; }
}
</style>
</head>
<body>
<!-- ==================== Background Decorations ==================== -->
<div class="bg-decor">
<div class="blob blob-1"></div>
<div class="blob blob-2"></div>
<div class="blob blob-3"></div>
<div class="blob blob-4"></div>
<div class="dots-pattern"></div>
</div>
<!-- ==================== Header ==================== -->
<header class="header" id="header">
<div class="header-inner">
<a href="#hero" class="logo">应用集市</a>
<nav class="nav">
<a href="#hero" class="nav-link active" data-target="hero">首页</a>
<a href="#library" class="nav-link" data-target="library">发现</a>
<a href="#featured" class="nav-link" data-target="featured">精选</a>
<a href="#new" class="nav-link" data-target="new">最新</a>
</nav>
<div class="header-search">
<div class="search-pill-wrap">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="text" id="header-search" class="search-pill" placeholder="搜索应用...">
</div>
<button class="search-go" id="header-search-btn">搜索</button>
</div>
</div>
</header>
<!-- ==================== Hero ==================== -->
<section class="hero" id="hero">
<div class="hero-banner">
<div class="hero-blob hero-blob-1"></div>
<div class="hero-blob hero-blob-2"></div>
<div class="hero-shape hero-shape-1"></div>
<div class="hero-shape hero-shape-2"></div>
<div class="hero-shape hero-shape-3"></div>
<div class="hero-content">
<span class="hero-badge">✨ 全新体验 · 海量应用</span>
<h1 class="hero-title">发现好应用,点亮新生活</h1>
<p class="hero-subtitle">精选优质应用,覆盖生活方方面面,让你的每一天都更加高效、有趣、美好</p>
<button class="hero-btn" id="hero-explore-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
开始探索
</button>
<div class="hero-stats">
<div class="hero-stat">
<div class="hero-stat-num" id="stat-apps">10万+</div>
<div class="hero-stat-label">优质应用</div>
</div>
<div class="hero-stat">
<div class="hero-stat-num" id="stat-downloads">5亿+</div>
<div class="hero-stat-label">累计下载</div>
</div>
<div class="hero-stat">
<div class="hero-stat-num" id="stat-users">800万+</div>
<div class="hero-stat-label">活跃用户</div>
</div>
</div>
</div>
</div>
</section>
<!-- ==================== Categories ==================== -->
<section class="categories-section">
<div class="container">
<div class="section-header">
<div class="section-title-wrap">
<div class="section-icon" style="background:linear-gradient(135deg,var(--amber),var(--coral))">🏷</div>
<div>
<h2 class="section-title">应用分类</h2>
<p class="section-subtitle">找到你感兴趣的领域</p>
</div>
</div>
</div>
<div class="categories-wrap" id="categories-wrap">
<div class="skel-shimmer" style="width:120px;height:42px;border-radius:999px"></div>
<div class="skel-shimmer" style="width:100px;height:42px;border-radius:999px"></div>
<div class="skel-shimmer" style="width:110px;height:42px;border-radius:999px"></div>
<div class="skel-shimmer" style="width:90px;height:42px;border-radius:999px"></div>
<div class="skel-shimmer" style="width:130px;height:42px;border-radius:999px"></div>
</div>
</div>
</section>
<!-- ==================== Featured ==================== -->
<section class="section" id="featured">
<div class="container">
<div class="section-header">
<div class="section-title-wrap">
<div class="section-icon" style="background:linear-gradient(135deg,var(--coral),var(--pink))">★</div>
<div>
<h2 class="section-title">精选好应用</h2>
<p class="section-subtitle">编辑团队精心挑选</p>
</div>
</div>
<a href="#library" class="section-more" data-target="library">查看全部 →</a>
</div>
<div class="featured-grid" id="featured-grid">
<div class="skeleton-card"><div style="display:flex;gap:16px;margin-bottom:16px"><div class="skel-shimmer skel-icon" style="width:72px;height:72px;border-radius:20px"></div><div style="flex:1"><div class="skel-shimmer skel-line" style="width:60%;height:16px"></div><div class="skel-shimmer skel-line short" style="margin-top:8px"></div></div></div><div class="skel-shimmer skel-line mid"></div><div class="skel-shimmer skel-line mid"></div></div>
<div class="skeleton-card"><div style="display:flex;gap:16px;margin-bottom:16px"><div class="skel-shimmer skel-icon" style="width:72px;height:72px;border-radius:20px"></div><div style="flex:1"><div class="skel-shimmer skel-line" style="width:60%;height:16px"></div><div class="skel-shimmer skel-line short" style="margin-top:8px"></div></div></div><div class="skel-shimmer skel-line mid"></div><div class="skel-shimmer skel-line mid"></div></div>
<div class="skeleton-card"><div style="display:flex;gap:16px;margin-bottom:16px"><div class="skel-shimmer skel-icon" style="width:72px;height:72px;border-radius:20px"></div><div style="flex:1"><div class="skel-shimmer skel-line" style="width:60%;height:16px"></div><div class="skel-shimmer skel-line short" style="margin-top:8px"></div></div></div><div class="skel-shimmer skel-line mid"></div><div class="skel-shimmer skel-line mid"></div></div>
</div>
</div>
</section>
<!-- ==================== New Apps ==================== -->
<section class="section" id="new">
<div class="container">
<div class="section-header">
<div class="section-title-wrap">
<div class="section-icon" style="background:linear-gradient(135deg,var(--green),var(--mint))">✦</div>
<div>
<h2 class="section-title">新鲜上架</h2>
<p class="section-subtitle">最新上架的优质应用</p>
</div>
</div>
<a href="#library" class="section-more" data-target="library">查看全部 →</a>
</div>
<div class="new-scroll-wrap" id="new-scroll-wrap">
<div class="skeleton-card" style="flex:0 0 280px"><div class="skel-shimmer skel-icon"></div><div class="skel-shimmer skel-line" style="width:60%;height:16px;margin-top:12px"></div><div class="skel-shimmer skel-line short" style="margin-top:8px"></div><div class="skel-shimmer skel-line mid" style="margin-top:12px"></div></div>
<div class="skeleton-card" style="flex:0 0 280px"><div class="skel-shimmer skel-icon"></div><div class="skel-shimmer skel-line" style="width:60%;height:16px;margin-top:12px"></div><div class="skel-shimmer skel-line short" style="margin-top:8px"></div><div class="skel-shimmer skel-line mid" style="margin-top:12px"></div></div>
<div class="skeleton-card" style="flex:0 0 280px"><div class="skel-shimmer skel-icon"></div><div class="skel-shimmer skel-line" style="width:60%;height:16px;margin-top:12px"></div><div class="skel-shimmer skel-line short" style="margin-top:8px"></div><div class="skel-shimmer skel-line mid" style="margin-top:12px"></div></div>
<div class="skeleton-card" style="flex:0 0 280px"><div class="skel-shimmer skel-icon"></div><div class="skel-shimmer skel-line" style="width:60%;height:16px;margin-top:12px"></div><div class="skel-shimmer skel-line short" style="margin-top:8px"></div><div class="skel-shimmer skel-line mid" style="margin-top:12px"></div></div>
</div>
</div>
</section>
<!-- ==================== Library ==================== -->
<section class="section" id="library">
<div class="container">
<div class="section-header">
<div class="section-title-wrap">
<div class="section-icon" style="background:linear-gradient(135deg,var(--sky),var(--purple))">⊞</div>
<div>
<h2 class="section-title">应用大全</h2>
<p class="section-subtitle">探索所有应用</p>
</div>
</div>
</div>
<div class="library-toolbar">
<div class="library-search">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="text" id="library-search" placeholder="搜索应用名称...">
</div>
<select class="sort-select" id="sort-select">
<option value="">默认排序</option>
<option value="rating">评分最高</option>
<option value="downloads">下载最多</option>
<option value="newest">最新上架</option>
</select>
<div class="library-count" id="library-count">加载中...</div>
</div>
<div class="app-grid" id="app-grid">
<div class="skeleton-card"><div style="display:flex;gap:14px;margin-bottom:12px"><div class="skel-shimmer skel-icon"></div><div style="flex:1"><div class="skel-shimmer skel-line" style="width:70%"></div><div class="skel-shimmer skel-line short" style="margin-top:6px"></div></div></div><div class="skel-shimmer skel-line mid"></div><div class="skel-shimmer skel-line mid"></div></div>
<div class="skeleton-card"><div style="display:flex;gap:14px;margin-bottom:12px"><div class="skel-shimmer skel-icon"></div><div style="flex:1"><div class="skel-shimmer skel-line" style="width:70%"></div><div class="skel-shimmer skel-line short" style="margin-top:6px"></div></div></div><div class="skel-shimmer skel-line mid"></div><div class="skel-shimmer skel-line mid"></div></div>
<div class="skeleton-card"><div style="display:flex;gap:14px;margin-bottom:12px"><div class="skel-shimmer skel-icon"></div><div style="flex:1"><div class="skel-shimmer skel-line" style="width:70%"></div><div class="skel-shimmer skel-line short" style="margin-top:6px"></div></div></div><div class="skel-shimmer skel-line mid"></div><div class="skel-shimmer skel-line mid"></div></div>
<div class="skeleton-card"><div style="display:flex;gap:14px;margin-bottom:12px"><div class="skel-shimmer skel-icon"></div><div style="flex:1"><div class="skel-shimmer skel-line" style="width:70%"></div><div class="skel-shimmer skel-line short" style="margin-top:6px"></div></div></div><div class="skel-shimmer skel-line mid"></div><div class="skel-shimmer skel-line mid"></div></div>
<div class="skeleton-card"><div style="display:flex;gap:14px;margin-bottom:12px"><div class="skel-shimmer skel-icon"></div><div style="flex:1"><div class="skel-shimmer skel-line" style="width:70%"></div><div class="skel-shimmer skel-line short" style="margin-top:6px"></div></div></div><div class="skel-shimmer skel-line mid"></div><div class="skel-shimmer skel-line mid"></div></div>
</div>
<div class="pagination" id="pagination"></div>
</div>
</section>
<!-- ==================== Modal ==================== -->
<div class="modal-overlay" id="modal-overlay">
<div class="modal" id="modal">
<div class="modal-loading" id="modal-loading">加载中...</div>
</div>
</div>
<!-- ==================== Footer ==================== -->
<footer class="footer">
<div class="footer-inner">
<div class="footer-brand">
<div class="footer-logo">应用集市</div>
<p>发现好应用,点亮新生活。致力于为用户提供优质、安全、便捷的应用下载服务。</p>
</div>
<div class="footer-col">
<h4>关于我们</h4>
<a href="#">公司介绍</a>
<a href="#">加入我们</a>
<a href="#">联系方式</a>
<a href="#">隐私政策</a>
</div>
<div class="footer-col">
<h4>帮助中心</h4>
<a href="#">常见问题</a>
<a href="#">使用指南</a>
<a href="#">意见反馈</a>
<a href="#">投诉建议</a>
</div>
<div class="footer-col">
<h4>合作伙伴</h4>
<a href="#">开发者入驻</a>
<a href="#">广告合作</a>
<a href="#">商务洽谈</a>
<a href="#">开放平台</a>
</div>
</div>
<div class="footer-bottom">
<p>© 2024 应用集市 · All rights reserved · 仅供学习交流使用</p>
</div>
</footer>
<!-- ==================== Scroll to Top ==================== -->
<button class="scroll-top" id="scroll-top" aria-label="回到顶部">↑</button>
<!-- ==================== Toast Container ==================== -->
<div class="toast-container" id="toast-container"></div>
<script>
(function() {
'use strict';
// ==================== Config ====================
var API_BASE = '';
// ==================== Category Colors ====================
var CAT_COLORS = [
{ bg: 'linear-gradient(135deg,#ffe8e8,#ffd0d0)', text: '#ff6b6b', solid: '#ff6b6b', emoji: '📱' },
{ bg: 'linear-gradient(135deg,#e8faf5,#d0f5ed)', text: '#4ecdc4', solid: '#4ecdc4', emoji: '🎮' },
{ bg: 'linear-gradient(135deg,#e8f4fc,#d0ecfa)', text: '#45b7d8', solid: '#45b7d8', emoji: '🛒' },
{ bg: 'linear-gradient(135deg,#fff5e8,#ffe8d0)', text: '#ffa94d', solid: '#ffa94d', emoji: '📚' },
{ bg: 'linear-gradient(135deg,#f3e8ff,#e8d0ff)', text: '#9775fa', solid: '#9775fa', emoji: '🎨' },
{ bg: 'linear-gradient(135deg,#e8fff0,#d0fae0)', text: '#51cf66', solid: '#51cf66', emoji: '🏃' },
{ bg: 'linear-gradient(135deg,#ffe8f5,#ffd0e8)', text: '#f783ac', solid: '#f783ac', emoji: '🎵' },
{ bg: 'linear-gradient(135deg,#e8f8ff,#d0f0ff)', text: '#3bc9db', solid: '#3bc9db', emoji: '💼' },
{ bg: 'linear-gradient(135deg,#fff8e8,#ffedc0)', text: '#fcc419', solid: '#fcc419', emoji: '🍳' },
{ bg: 'linear-gradient(135deg,#f0e8ff,#e0d0ff)', text: '#845ef7', solid: '#845ef7', emoji: '🔬' },
{ bg: 'linear-gradient(135deg,#e8fffa,#d0fff5)', text: '#20c997', solid: '#20c997', emoji: '✈️' },
{ bg: 'linear-gradient(135deg,#ffe8e8,#ffe0d0)', text: '#fa5252', solid: '#fa5252', emoji: '📷' },
];
var TAG_COLORS = [
{ bg: '#ffe8e8', text: '#ff6b6b' },
{ bg: '#e8faf5', text: '#4ecdc4' },
{ bg: '#e8f4fc', text: '#45b7d8' },
{ bg: '#fff5e8', text: '#ffa94d' },
{ bg: '#f3e8ff', text: '#9775fa' },
{ bg: '#e8fff0', text: '#51cf66' },
{ bg: '#ffe8f5', text: '#f783ac' },
{ bg: '#e8f8ff', text: '#3bc9db' },
];
// ==================== State ====================
var state = {
categories: [],
featuredApps: [],
newApps: [],
library: {
list: [], total: 0, page: 1, pageSize: 20,
categoryId: null, keyword: '', sort: '', loading: false
}
};
// ==================== Utility Functions ====================
function formatNumber(num) {
if (num === undefined || num === null || isNaN(num)) return '0';
num = Number(num);
if (num >= 100000000) return (num / 100000000).toFixed(1).replace(/\.0$/, '') + '亿';
if (num >= 10000) return (num / 10000).toFixed(1).replace(/\.0$/, '') + '万';
return num.toString();
}
function generateGradient(name) {
var hash = 0;
if (!name) name = 'A';
for (var i = 0; i < name.length; i++) {
hash = ((hash << 5) - hash) + name.charCodeAt(i);
hash |= 0;
}
var h = Math.abs(hash);
var hue1 = h % 360;
var hue2 = (hue1 + 40 + (Math.floor(h / 360) % 80)) % 360;
var hue3 = (hue2 + 50 + (Math.floor(h / 129600) % 60)) % 360;
return 'linear-gradient(135deg, hsl(' + hue1 + ',75%,68%), hsl(' + hue2 + ',72%,62%), hsl(' + hue3 + ',70%,58%))';
}
function parseJSON(str, fallback) {
if (!str) return fallback || [];
if (Array.isArray(str)) return str;
if (typeof str === 'object') return str;
try { return JSON.parse(str); } catch(e) { return fallback || []; }
}
function renderStars(rating) {
rating = parseFloat(rating) || 0;
var full = Math.floor(rating);
var hasHalf = (rating % 1) >= 0.25 && (rating % 1) < 0.75;
var extraFull = (rating % 1) >= 0.75 ? 1 : 0;
if (extraFull) { full++; hasHalf = false; }
var html = '';
for (var i = 0; i < 5; i++) {
if (i < full) {
html += '<span class="star star-full">★</span>';
} else if (i === full && hasHalf) {
html += '<span class="star star-half">★</span>';
} else {
html += '<span class="star star-empty">★</span>';
}
}
return html;
}
function debounce(fn, delay) {
var timer = null;
return function() {
var ctx = this, args = arguments;
clearTimeout(timer);
timer = setTimeout(function() { fn.apply(ctx, args); }, delay);
};
}
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/[&<>"']/g, function(c) {
return { '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;' }[c];
});
}
function showToast(msg, type) {
type = type || 'info';
var container = document.getElementById('toast-container');
var toast = document.createElement('div');
toast.className = 'toast toast-' + type;
var icon = type === 'success' ? '✓' : (type === 'error' ? '✕' : 'ℹ');
toast.innerHTML = '<span>' + icon + '</span><span>' + escapeHtml(msg) + '</span>';
container.appendChild(toast);
setTimeout(function() { toast.classList.add('show'); }, 10);
setTimeout(function() {
toast.classList.remove('show');
setTimeout(function() { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 400);
}, 3000);
}
function getCatColor(index) {
return CAT_COLORS[index % CAT_COLORS.length];
}
function getTagColor(index) {
return TAG_COLORS[index % TAG_COLORS.length];
}
function renderAppIcon(app, size) {
size = size || 64;
var radius = Math.round(size * 0.28);
if (app.icon && (app.icon.indexOf('http') === 0 || app.icon.indexOf('/') === 0)) {
return '<div class="app-icon-wrap" style="width:' + size + 'px;height:' + size + 'px;border-radius:' + radius + 'px"><img src="' + escapeHtml(app.icon) + '" class="app-icon-img" alt="' + escapeHtml(app.name) + '" loading="lazy"></div>';
}
var grad = generateGradient(app.name);
var letter = (app.name || '?').charAt(0).toUpperCase();
return '<div class="app-icon-wrap" style="background:' + grad + ';width:' + size + 'px;height:' + size + 'px;border-radius:' + radius + 'px">' + escapeHtml(letter) + '</div>';
}
// ==================== API ====================
function fetchData(path) {
return fetch(API_BASE + path)
.then(function(res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.json();
})
.then(function(json) {
if (json.code !== 0) throw new Error(json.message || 'API error');
return json.data;
})
.catch(function(err) {
console.error('Fetch error [' + path + ']:', err.message);
return null;
});
}
// ==================== Render: Categories ====================
function renderCategories() {
var wrap = document.getElementById('categories-wrap');
if (!state.categories.length) {
wrap.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><div class="empty-state-icon">📭</div><div class="empty-state-text">暂无分类数据</div></div>';
return;
}
var html = '';
var allActive = state.library.categoryId === null ? ' active' : '';
var allColor = state.library.categoryId === null
? 'background:linear-gradient(135deg,var(--coral),var(--pink));color:#fff'
: '';
html += '<button class="cat-pill' + allActive + '" data-cat-id="" style="' + allColor + '"><span class="cat-emoji">🌟</span>全部</button>';
state.categories.forEach(function(cat, i) {
var c = getCatColor(i);
var isActive = state.library.categoryId == cat.id;
var style = isActive
? 'background:' + c.solid + ';color:#fff'
: 'color:' + c.text;
var emoji = cat.icon || c.emoji;
if (cat.icon && cat.icon.indexOf('http') !== 0 && cat.icon.length <= 4) {
emoji = cat.icon;
} else if (cat.icon && cat.icon.indexOf('http') === 0) {
emoji = c.emoji;
}
html += '<button class="cat-pill' + (isActive ? ' active' : '') + '" data-cat-id="' + cat.id + '" style="' + style + '"><span class="cat-emoji">' + emoji + '</span>' + escapeHtml(cat.name) + '</button>';
});
wrap.innerHTML = html;
wrap.querySelectorAll('.cat-pill').forEach(function(btn) {
btn.addEventListener('click', function() {
var catId = btn.getAttribute('data-cat-id');
handleCategoryClick(catId ? parseInt(catId) : null);
});
});
}
// ==================== Render: Featured ====================
function renderFeatured() {
var grid = document.getElementById('featured-grid');
if (!state.featuredApps.length) {
grid.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><div class="empty-state-icon">✨</div><div class="empty-state-text">暂无精选应用</div></div>';
return;
}
var html = '';
state.featuredApps.forEach(function(app, i) {
var grad = generateGradient(app.name);
var tags = parseJSON(app.tags, []);
var tagsHtml = '';
tags.slice(0, 3).forEach(function(tag, j) {
var tc = getTagColor(j);
tagsHtml += '<span class="tag-pill" style="background:' + tc.bg + ';color:' + tc.text + '">' + escapeHtml(tag) + '</span>';
});
html += '<div class="featured-card reveal" data-app-id="' + app.id + '" style="--card-grad:' + grad + '">'
+ '<div class="featured-card-top">'
+ '<div class="featured-card-top-inner">'
+ renderAppIcon(app, 72)
+ '<div style="flex:1;min-width:0">'
+ '<div class="featured-card-name">' + escapeHtml(app.name) + '</div>'
+ '<div class="featured-card-dev">' + escapeHtml(app.developer || '未知开发者') + '</div>'
+ '</div>'
+ '</div>'
+ '</div>'
+ '<div class="featured-card-body">'
+ '<p class="featured-card-desc">' + escapeHtml(app.description || '暂无描述') + '</p>'
+ '<div class="featured-card-tags">' + tagsHtml + '</div>'
+ '</div>'
+ '<div class="featured-card-footer">'
+ '<div class="featured-card-stats">'
+ '<div class="featured-stat"><div class="featured-stat-val">' + (app.rating ? app.rating.toFixed(1) : '-') + '</div><div class="featured-stat-label">评分</div></div>'
+ '<div class="featured-stat"><div class="featured-stat-val">' + formatNumber(app.downloads) + '</div><div class="featured-stat-label">下载</div></div>'
+ '</div>'
+ '<button class="btn-download coral" data-download-id="' + app.id + '">获取</button>'
+ '</div>'
+ '</div>';
});
grid.innerHTML = html;
grid.querySelectorAll('.featured-card').forEach(function(card) {
card.addEventListener('click', function(e) {
if (e.target.classList.contains('btn-download')) return;
openAppDetail(card.getAttribute('data-app-id'));
});
});
grid.querySelectorAll('[data-download-id]').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
var app = state.featuredApps.find(function(a) { return a.id == btn.getAttribute('data-download-id'); });
if (app) handleDownload(app);
});
});
observeReveal();
}
// ==================== Render: New ====================
function renderNew() {
var wrap = document.getElementById('new-scroll-wrap');
if (!state.newApps.length) {
wrap.innerHTML = '<div class="empty-state" style="width:100%"><div class="empty-state-icon">🆕</div><div class="empty-state-text">暂无新上架应用</div></div>';
return;
}
var html = '';
state.newApps.forEach(function(app) {
html += '<div class="new-card reveal" data-app-id="' + app.id + '">'
+ '<div class="new-badge">NEW</div>'
+ '<div class="app-card-top">' + renderAppIcon(app, 56) + ''
+ '<div class="app-card-info">'
+ '<div class="app-card-name">' + escapeHtml(app.name) + '</div>'
+ '<div class="app-card-dev">' + escapeHtml(app.developer || '未知') + '</div>'
+ '</div>'
+ '</div>'
+ '<p class="app-card-desc">' + escapeHtml(app.description || '暂无描述') + '</p>'
+ '<div class="app-card-bottom">'
+ '<div class="app-card-meta">'
+ '<div class="stars">' + renderStars(app.rating) + '</div>'
+ '<div class="app-card-downloads">' + formatNumber(app.downloads) + ' 下载</div>'
+ '</div>'
+ '<button class="btn-download" data-download-id="' + app.id + '">下载</button>'
+ '</div>'
+ '</div>';
});
wrap.innerHTML = html;
wrap.querySelectorAll('.new-card').forEach(function(card) {
card.addEventListener('click', function(e) {
if (e.target.classList.contains('btn-download')) return;
openAppDetail(card.getAttribute('data-app-id'));
});
});
wrap.querySelectorAll('[data-download-id]').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
var app = state.newApps.find(function(a) { return a.id == btn.getAttribute('data-download-id'); });
if (app) handleDownload(app);
});
});
observeReveal();
}
// ==================== Render: Library ====================
function renderLibrary() {
var grid = document.getElementById('app-grid');
var count = document.getElementById('library-count');
count.innerHTML = '共 <strong>' + state.library.total + '</strong> 款应用';
if (state.library.loading) {
var skelHtml = '';
for (var s = 0; s < 10; s++) {
skelHtml += '<div class="skeleton-card"><div style="display:flex;gap:14px;margin-bottom:12px"><div class="skel-shimmer skel-icon"></div><div style="flex:1"><div class="skel-shimmer skel-line" style="width:70%"></div><div class="skel-shimmer skel-line short" style="margin-top:6px"></div></div></div><div class="skel-shimmer skel-line mid"></div><div class="skel-shimmer skel-line mid"></div></div>';
}
grid.innerHTML = skelHtml;
document.getElementById('pagination').innerHTML = '';
return;
}
if (!state.library.list.length) {
grid.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🔍</div><div class="empty-state-text">没有找到相关应用,试试其他关键词吧</div></div>';
document.getElementById('pagination').innerHTML = '';
return;
}
var html = '';
state.library.list.forEach(function(app) {
var dlClass = app.is_new ? ' coral' : '';
html += '<div class="app-card reveal" data-app-id="' + app.id + '">'
+ '<div class="app-card-top">' + renderAppIcon(app, 56) + ''
+ '<div class="app-card-info">'
+ '<div class="app-card-name">' + escapeHtml(app.name) + '</div>'
+ '<div class="app-card-dev">' + escapeHtml(app.developer || '未知') + '</div>'
+ '</div>'
+ '</div>'
+ '<p class="app-card-desc">' + escapeHtml(app.description || '暂无描述') + '</p>'
+ '<div class="app-card-bottom">'
+ '<div class="app-card-meta">'
+ '<div class="stars">' + renderStars(app.rating) + '</div>'
+ '<div class="app-card-rating-text">' + (app.rating ? app.rating.toFixed(1) : '-') + ' · ' + formatNumber(app.downloads) + ' 下载</div>'
+ '</div>'
+ '<button class="btn-download' + dlClass + '" data-download-id="' + app.id + '">下载</button>'
+ '</div>'
+ '</div>';
});
grid.innerHTML = html;
grid.querySelectorAll('.app-card').forEach(function(card) {
card.addEventListener('click', function(e) {
if (e.target.classList.contains('btn-download')) return;
openAppDetail(card.getAttribute('data-app-id'));
});
});
grid.querySelectorAll('[data-download-id]').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
var app = state.library.list.find(function(a) { return a.id == btn.getAttribute('data-download-id'); });
if (app) handleDownload(app);
});
});
renderPagination();
observeReveal();
}
// ==================== Render: Pagination ====================
function renderPagination() {
var wrap = document.getElementById('pagination');
var total = state.library.total;
var page = state.library.page;
var pageSize = state.library.pageSize;
var totalPages = Math.ceil(total / pageSize);
if (totalPages <= 1) { wrap.innerHTML = ''; return; }
var html = '';
if (page > 1) {
html += '<button class="page-btn" data-page="' + (page - 1) + '">‹ 上一页</button>';
}
var start = Math.max(1, page - 2);
var end = Math.min(totalPages, page + 2);
if (start > 1) {
html += '<button class="page-btn" data-page="1">1</button>';
if (start > 2) html += '<span class="page-ellipsis">...</span>';
}
for (var i = start; i <= end; i++) {
html += '<button class="page-btn' + (i === page ? ' active' : '') + '" data-page="' + i + '">' + i + '</button>';
}
if (end < totalPages) {
if (end < totalPages - 1) html += '<span class="page-ellipsis">...</span>';
html += '<button class="page-btn" data-page="' + totalPages + '">' + totalPages + '</button>';
}
if (page < totalPages) {
html += '<button class="page-btn" data-page="' + (page + 1) + '">下一页 ›</button>';
}
wrap.innerHTML = html;
wrap.querySelectorAll('.page-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var p = parseInt(btn.getAttribute('data-page'));
if (p && p !== state.library.page) {
handlePageChange(p);
}
});
});
}
// ==================== Render: Modal ====================
function renderModalContent(app) {
var grad = generateGradient(app.name);
var tags = parseJSON(app.tags, []);
var features = parseJSON(app.features, []);
var screenshots = parseJSON(app.screenshots, []);
var tagsHtml = '';
tags.forEach(function(tag, i) {
var tc = getTagColor(i);
tagsHtml += '<span class="tag-pill" style="background:' + tc.bg + ';color:' + tc.text + '">' + escapeHtml(tag) + '</span>';
});
var featuresHtml = '';
features.forEach(function(feat) {
featuresHtml += '<div class="modal-feature"><div class="modal-feature-check">✓</div><span>' + escapeHtml(feat) + '</span></div>';
});
var screenshotsHtml = '';
screenshots.forEach(function(ss, i) {
if (typeof ss === 'string' && ss.indexOf('http') === 0) {
screenshotsHtml += '<div class="screenshot-item"><img src="' + escapeHtml(ss) + '" alt="截图' + (i+1) + '" loading="lazy"></div>';
} else {
var ssGrad = generateGradient(app.name + '_ss_' + i);
screenshotsHtml += '<div class="screenshot-item"><div class="screenshot-placeholder" style="background:' + ssGrad + '">' + escapeHtml(app.name) + '</div></div>';
}
});
var platformText = app.platform || '多平台';
var priceText = app.price || '免费';
return '<div class="modal-header" style="--modal-grad:' + grad + '">'
+ '<button class="modal-close" id="modal-close">✕</button>'
+ '<div class="modal-header-inner">'
+ renderAppIcon(app, 88)
+ '<div style="flex:1;min-width:0">'
+ '<h3 class="modal-app-name">' + escapeHtml(app.name) + '</h3>'
+ '<div class="modal-app-dev">' + escapeHtml(app.developer || '未知开发者') + '</div>'
+ '<div class="modal-rating-row">'
+ '<span class="stars" style="font-size:16px">' + renderStars(app.rating) + '</span>'
+ '<span class="modal-rating-num">' + (app.rating ? app.rating.toFixed(1) : '-') + '</span>'
+ '<span class="modal-rating-text">' + formatNumber(app.reviews) + ' 评价</span>'
+ '</div>'
+ '</div>'
+ '</div>'
+ '</div>'
+ '<div class="modal-body">'
+ '<div class="modal-section">'
+ '<div class="modal-section-title">📝 应用介绍</div>'
+ '<p class="modal-desc">' + escapeHtml(app.description || '暂无描述') + '</p>'
+ '</div>'
+ (tags.length ? '<div class="modal-section"><div class="modal-section-title">🏷 标签</div><div class="modal-tags">' + tagsHtml + '</div></div>' : '')
+ (features.length ? '<div class="modal-section"><div class="modal-section-title">✨ 特色功能</div><div class="modal-features">' + featuresHtml + '</div></div>' : '')
+ '<div class="modal-section">'
+ '<div class="modal-section-title">📊 应用信息</div>'
+ '<div class="modal-stats-grid">'
+ '<div class="modal-stat-item"><div class="modal-stat-val">' + formatNumber(app.downloads) + '</div><div class="modal-stat-label">下载量</div></div>'
+ '<div class="modal-stat-item"><div class="modal-stat-val">' + escapeHtml(app.size || '-') + '</div><div class="modal-stat-label">大小</div></div>'
+ '<div class="modal-stat-item"><div class="modal-stat-val">' + escapeHtml(app.version || '-') + '</div><div class="modal-stat-label">版本</div></div>'
+ '<div class="modal-stat-item"><div class="modal-stat-val">' + escapeHtml(app.update_date || '-') + '</div><div class="modal-stat-label">更新日期</div></div>'
+ '</div>'
+ '</div>'
+ (screenshots.length ? '<div class="modal-section"><div class="modal-section-title">🖼 应用截图</div><div class="modal-screenshots">' + screenshotsHtml + '</div></div>' : '')
+ '<div style="display:flex;gap:12px;margin-top:8px">'
+ '<button class="modal-download-btn" id="modal-download-btn">'
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>'
+ '立即下载'
+ '</button>'
+ '</div>'
+ '<div style="text-align:center;margin-top:12px;font-size:13px;color:var(--text-light)">' + escapeHtml(platformText) + ' · ' + escapeHtml(priceText) + '</div>'
+ '</div>';
}
function renderModalSkeleton() {
return '<div class="modal-loading">'
+ '<div class="skel-shimmer" style="width:80px;height:80px;border-radius:22px;margin:0 auto 20px"></div>'
+ '<div class="skel-shimmer skel-line" style="width:50%;height:20px;margin:0 auto 12px"></div>'
+ '<div class="skel-shimmer skel-line short" style="margin:0 auto"></div>'
+ '<div class="skel-shimmer skel-line mid" style="margin-top:20px"></div>'
+ '<div class="skel-shimmer skel-line mid"></div>'
+ '</div>';
}
// ==================== Actions ====================
function loadHome() {
fetchData('/api/v1/app/home').then(function(data) {
if (data) {
if (data.categories) state.categories = data.categories;
if (data.featured) state.featuredApps = data.featured;
if (data.new) state.newApps = data.new;
} else {
// Fallback: fetch individually
return Promise.all([
fetchData('/api/v1/app/categories'),
fetchData('/api/v1/app/featured?limit=8'),
fetchData('/api/v1/app/new?limit=4')
]).then(function(results) {
if (results[0]) state.categories = results[0];
if (results[1]) state.featuredApps = results[1];
if (results[2]) state.newApps = results[2];
});
}
}).then(function() {
renderCategories();
renderFeatured();
renderNew();
});
}
function loadLibrary() {
state.library.loading = true;
renderLibrary();
var params = [];
if (state.library.categoryId) params.push('category_id=' + state.library.categoryId);
if (state.library.keyword) params.push('keyword=' + encodeURIComponent(state.library.keyword));
if (state.library.sort) params.push('sort=' + state.library.sort);
params.push('page=' + state.library.page);
params.push('page_size=' + state.library.pageSize);
fetchData('/api/v1/app/apps?' + params.join('&')).then(function(data) {
state.library.loading = false;
if (data) {
state.library.list = data.list || [];
state.library.total = data.total || 0;
state.library.page = data.page || state.library.page;
} else {
state.library.list = [];
state.library.total = 0;
}
renderLibrary();
});
}
function openAppDetail(id) {
var overlay = document.getElementById('modal-overlay');
var modal = document.getElementById('modal');
overlay.classList.add('active');
document.body.style.overflow = 'hidden';
modal.innerHTML = renderModalSkeleton();
fetchData('/api/v1/app/apps/' + id).then(function(app) {
if (app) {
modal.innerHTML = renderModalContent(app);
var closeBtn = document.getElementById('modal-close');
if (closeBtn) closeBtn.addEventListener('click', closeAppDetail);
var dlBtn = document.getElementById('modal-download-btn');
if (dlBtn) dlBtn.addEventListener('click', function() { handleDownload(app); });
} else {
modal.innerHTML = '<div class="modal-error"><div style="font-size:48px;margin-bottom:16px">😵</div>加载失败,请稍后重试<br><button class="btn-download" style="margin-top:20px" onclick="closeAppDetail()">关闭</button></div>';
}
});
}
function closeAppDetail() {
var overlay = document.getElementById('modal-overlay');
overlay.classList.remove('active');
document.body.style.overflow = '';
}
function handleDownload(app) {
showToast('开始下载 ' + (app.name || '应用') + '...', 'success');
}
function handleCategoryClick(catId) {
state.library.categoryId = catId;
state.library.page = 1;
renderCategories();
loadLibrary();
scrollToSection('library');
}
function handleSortChange(sort) {
state.library.sort = sort;
state.library.page = 1;
loadLibrary();
}
function handlePageChange(page) {
state.library.page = page;
loadLibrary();
scrollToSection('library');
}
function handleSearch(keyword) {
state.library.keyword = keyword;
state.library.page = 1;
loadLibrary();
}
function scrollToSection(id) {
var el = document.getElementById(id);
if (el) {
var offset = 80;
var top = el.getBoundingClientRect().top + window.pageYOffset - offset;
window.scrollTo({ top: top, behavior: 'smooth' });
}
}
// ==================== IntersectionObserver ====================
var revealObserver = null;
function observeReveal() {
if (!revealObserver) {
revealObserver = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
revealObserver.unobserve(entry.target);
}
});
}, { threshold: 0.08, rootMargin: '0px 0px -40px 0px' });
}
document.querySelectorAll('.reveal:not(.visible)').forEach(function(el) {
revealObserver.observe(el);
});
}
// ==================== Event Listeners ====================
function setupEventListeners() {
// Header search
var headerSearch = document.getElementById('header-search');
var headerSearchBtn = document.getElementById('header-search-btn');
var debouncedSearch = debounce(function(val) {
document.getElementById('library-search').value = val;
handleSearch(val);
}, 300);
headerSearch.addEventListener('input', function() {
debouncedSearch(this.value);
});
headerSearch.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
document.getElementById('library-search').value = this.value;
handleSearch(this.value);
scrollToSection('library');
}
});
headerSearchBtn.addEventListener('click', function() {
var val = headerSearch.value;
document.getElementById('library-search').value = val;
handleSearch(val);
scrollToSection('library');
});
// Library search
var librarySearch = document.getElementById('library-search');
var debouncedLibrarySearch = debounce(function(val) {
handleSearch(val);
}, 300);
librarySearch.addEventListener('input', function() {
debouncedLibrarySearch(this.value);
});
// Sort select
document.getElementById('sort-select').addEventListener('change', function() {
handleSortChange(this.value);
});
// Nav links
document.querySelectorAll('.nav-link').forEach(function(link) {
link.addEventListener('click', function(e) {
e.preventDefault();
var target = link.getAttribute('data-target');
if (target) {
scrollToSection(target);
document.querySelectorAll('.nav-link').forEach(function(l) { l.classList.remove('active'); });
link.classList.add('active');
}
});
});
// Section more links
document.querySelectorAll('.section-more').forEach(function(link) {
link.addEventListener('click', function(e) {
e.preventDefault();
var target = link.getAttribute('data-target');
if (target) scrollToSection(target);
});
});
// Hero explore button
document.getElementById('hero-explore-btn').addEventListener('click', function() {
scrollToSection('library');
});
// Modal overlay click to close
document.getElementById('modal-overlay').addEventListener('click', function(e) {
if (e.target === this) closeAppDetail();
});
// ESC to close modal
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeAppDetail();
});
// Scroll header shadow + scroll-to-top + scrollspy
var scrollTop = document.getElementById('scroll-top');
var header = document.getElementById('header');
var sections = ['hero', 'featured', 'new', 'library'];
window.addEventListener('scroll', function() {
var sy = window.pageYOffset;
header.classList.toggle('scrolled', sy > 10);
scrollTop.classList.toggle('show', sy > 400);
// Scrollspy
var current = 'hero';
for (var i = 0; i < sections.length; i++) {
var el = document.getElementById(sections[i]);
if (el && el.getBoundingClientRect().top <= 120) {
current = sections[i];
}
}
document.querySelectorAll('.nav-link').forEach(function(l) {
l.classList.toggle('active', l.getAttribute('data-target') === current);
});
});
// Scroll to top
scrollTop.addEventListener('click', function() {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
}
// ==================== Init ====================
function init() {
setupEventListeners();
loadHome();
loadLibrary();
observeReveal();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// Expose closeAppDetail for inline onclick
window.closeAppDetail = closeAppDetail;
})();
</script>
</body>
</html>