Files
web_template/web/dist/app1/index.html

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