1222 lines
65 KiB
HTML
1222 lines
65 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>应用商店 - 发现精彩应用</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">✚</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)">查看全部 ›</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)">查看全部 ›</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">✓</span></div>
|
||
<div class="sort-item" data-sort="rating" onclick="setSort('rating')">评分最高 <span class="check">✓</span></div>
|
||
<div class="sort-item" data-sort="downloads" onclick="setSort('downloads')">下载最多 <span class="check">✓</span></div>
|
||
<div class="sort-item" data-sort="newest" onclick="setSort('newest')">最新发布 <span class="check">✓</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">✚</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>© 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="关闭">×</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 { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||
});
|
||
}
|
||
|
||
/* ---- 工具: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 + ' · ' + 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">'
|
||
+ '<div class="app-icon sm" style="background:' + grad + ';">' + esc(initials(app.name)) + '</div>'
|
||
+ '<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">'
|
||
+ '<div class="app-icon" style="background:' + grad + ';">' + esc(initials(app.name)) + '</div>'
|
||
+ '<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' : '') + '>‹</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' : '') + '>›</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">'
|
||
+ '<div class="app-icon lg" style="background:' + grad + ';">' + esc(initials(app.name)) + '</div>'
|
||
+ '<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>
|