Rebuild redmini as offline mirror of the existing H5 (same globals.css classes).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
eric 2026-09-27 13:14:28 -05:00
parent bf7167c185
commit dbeac128bb
15 changed files with 25878 additions and 2169 deletions

View File

@ -1,36 +1,26 @@
# redmini · nomadro 小红书离线镜像(1:1 产品面)
# redmini · nomadro 小红书离线包 = 官网 H5 镜像
小红书 **miniTool 无法联网**。本目录是 [nomadweb.nomadro.com](https://nomadweb.nomadro.com) 的离线内容镜像:
**不再另起 UI。** 样式直接来自 `frontend/src/app/globals.css`(打包为 `nomadro.css`),DOM class 与 `localhost:3000` 首页一致:`navbar` / `hero` / `dest-card` / `visa-card` / `journey-strip` / `footer` …
- 三环导航:探索 / 连接 / 成长(与官网一致)
- 首页:Hero、统计、热门目的地、游民一天、游民心声
- 探索:目的地列表/详情、地图快照、签证、智能匹配、城市对比
- 连接:活动、社区讨论、心声
- 成长:游民学院课时、博客全文、工具箱、费用估算、FAQ
- 我的:收藏、最近浏览、旅居计划清单(本地 Storage)
数据为本机 mock 快照(无法联网)。完整登录/支付/RSVP 请用网页版。
数据来自 `backend/app/data/mock_data.py` 等官网同源 mock(打包进 `src/data.js`)。
## 打包上传
## 构建
```bash
cd redmini
# 若 mock 有更新,先重建 data.js(在仓库根目录):
# cd ../backend && python -c "..." # 或复用 scripts 导出
python scripts/build_data.py # 从 backend mock 导出 data.js
# nomadro.css 由仓库维护:从 frontend globals 同步时再跑 sanitize
python scripts/assemble.py
python scripts/static_scan.py # 禁止 fetch / XHR / WebSocket
python scripts/static_scan.py
python scripts/pack.py # -> dist/nomadro-redmini.zip
```
用小红书开发者工具上传 zip(根目录含 `index.html`)。
本地预览:打开 `release/index.html`(或 `tool/index.html`,需有 `tool/data.js`)。
## 目录
## 对照官网
| 路径 | 说明 |
|------|------|
| `src/data.js` | 离线数据包 |
| `tool/` | HTML / CSS / JS / xhs-bridge |
| `release/` | assemble 产物 |
| `scripts/` | assemble / static_scan / pack |
本地浏览器预览:直接打开 `tool/index.html`(需先把 `src/data.js` 拷到 `tool/`,或跑 assemble 后打开 `release/index.html`)。
| 官网 H5 | 离线包 |
|---------|--------|
| `frontend/src/app/globals.css` | `tool/nomadro.css` |
| Hero / Destinations / Visa / FAQ | 同名 class 渲染 |
| API JSON | `src/data.js` 内嵌 |

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -3,40 +3,49 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>nomadro · 数字游民</title>
<link rel="stylesheet" href="./styles.css" />
<meta name="theme-color" content="#0a0e17" />
<title>nomadro · 数字游民旅居指南</title>
<link rel="stylesheet" href="./nomadro.css" />
<style>
/* XHS offline shell — keep H5 look; hide web-only chrome that needs network */
.xhs-offline-banner {
position: sticky;
top: 0;
z-index: 100;
padding: 8px 16px;
text-align: center;
font-size: 12px;
color: var(--text-secondary);
background: rgba(17, 24, 39, 0.95);
border-bottom: var(--border-glass);
}
.xhs-offline-banner strong { color: var(--accent-3); }
#app-root { min-height: 100vh; }
.page-panel { display: none; }
.page-panel.is-active { display: block; }
.xhs-back {
position: fixed;
left: 12px;
bottom: 18px;
z-index: 90;
width: 44px;
height: 44px;
border-radius: 999px;
border: var(--border-glass);
background: var(--bg-card);
color: var(--text-primary);
box-shadow: var(--shadow-card);
display: none;
}
.xhs-back.is-on { display: block; }
</style>
</head>
<body>
<div id="app">
<header class="top-chrome" id="top-chrome">
<div class="top-left">
<button type="button" class="icon-btn is-hidden" id="btn-back" aria-label="返回">←</button>
<div>
<p class="top-brand" id="top-brand">nomadro</p>
<p class="top-sub" id="top-sub">离线镜像</p>
</div>
</div>
<button type="button" class="chip" id="btn-offline" title="离线说明">离线</button>
</header>
<main class="main" id="main"></main>
<nav class="tabbar" id="tabbar" aria-label="主导航">
<button type="button" class="tab is-active" data-tab="home"><span>🏠</span><i>首页</i></button>
<button type="button" class="tab" data-tab="explore"><span>🧭</span><i>探索</i></button>
<button type="button" class="tab" data-tab="connect"><span>🤝</span><i>连接</i></button>
<button type="button" class="tab" data-tab="grow"><span>🌱</span><i>成长</i></button>
<button type="button" class="tab" data-tab="mine"><span>👤</span><i>我的</i></button>
</nav>
</div>
<div class="sheet is-hidden" id="sheet" role="dialog" aria-modal="true">
<div class="sheet-mask" id="sheet-mask"></div>
<div class="sheet-panel">
<div class="sheet-handle"></div>
<div id="sheet-body"></div>
</div>
<div class="xhs-offline-banner">
<strong>离线镜像</strong> · 样式与结构同步官网 H5 · 数据为本机快照 · 完整互动请打开 nomadweb.nomadro.com
</div>
<div id="app-root"></div>
<button type="button" class="xhs-back" id="xhs-back" aria-label="返回">←</button>
<script src="./data.js"></script>
<script src="./xhs-bridge.js"></script>

12011
redmini/release/nomadro.css Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,277 +0,0 @@
:root {
--bg: #0b0a10;
--bg2: #14121c;
--card: rgba(255, 255, 255, 0.06);
--card2: rgba(255, 255, 255, 0.09);
--text: #f4f0ea;
--muted: #9a94a8;
--accent: #f0c674;
--accent2: #7dd3c7;
--danger: #f07178;
--border: rgba(255, 255, 255, 0.1);
--safe-b: env(safe-area-inset-bottom, 0px);
--safe-t: env(safe-area-inset-top, 0px);
--tab-h: 64px;
--top-h: 56px;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
-webkit-font-smoothing: antialiased;
-webkit-tap-highlight-color: transparent;
}
button, input, select, textarea { font: inherit; color: inherit; }
button { border: 0; background: none; cursor: pointer; }
.is-hidden { display: none !important; }
#app {
min-height: 100%;
min-height: 100vh;
padding-top: calc(var(--top-h) + var(--safe-t));
padding-bottom: calc(var(--tab-h) + var(--safe-b) + 8px);
}
.top-chrome {
position: fixed; top: 0; left: 0; right: 0; z-index: 40;
height: calc(var(--top-h) + var(--safe-t));
padding: var(--safe-t) 14px 0;
display: flex; align-items: center; justify-content: space-between;
background: rgba(11, 10, 16, 0.92);
backdrop-filter: blur(14px);
border-bottom: 1px solid var(--border);
}
.top-left { display: flex; align-items: center; gap: 8px; }
.top-brand { font-weight: 700; font-size: 16px; letter-spacing: 0.4px; }
.top-sub { font-size: 11px; color: var(--muted); margin-top: 1px; }
.icon-btn {
width: 34px; height: 34px; border-radius: 10px;
background: var(--card); color: var(--text); font-size: 16px;
}
.chip {
padding: 6px 10px; border-radius: 999px; font-size: 12px;
background: rgba(240, 198, 116, 0.15); color: var(--accent);
border: 1px solid rgba(240, 198, 116, 0.35);
}
.main { padding: 12px 14px 20px; max-width: 720px; margin: 0 auto; }
.tabbar {
position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
height: calc(var(--tab-h) + var(--safe-b));
padding: 6px 6px var(--safe-b);
display: flex; background: rgba(16, 14, 24, 0.96);
border-top: 1px solid var(--border);
backdrop-filter: blur(14px);
}
.tab {
flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px;
color: var(--muted); font-size: 10px; padding: 6px 0;
}
.tab span { font-size: 18px; line-height: 1; }
.tab.is-active { color: var(--accent); }
.hero {
position: relative; overflow: hidden; border-radius: 22px;
padding: 22px 18px 20px;
background:
radial-gradient(500px 220px at 0% 0%, rgba(240, 198, 116, 0.22), transparent 55%),
radial-gradient(420px 200px at 100% 0%, rgba(125, 211, 199, 0.16), transparent 50%),
linear-gradient(165deg, #15121d, #0e0c14 70%);
border: 1px solid var(--border);
margin-bottom: 16px;
}
.hero-badge {
display: inline-block; font-size: 11px; color: var(--accent2);
background: rgba(125, 211, 199, 0.12); padding: 4px 8px; border-radius: 999px;
margin-bottom: 10px;
}
.hero h1 { font-size: 28px; line-height: 1.2; font-weight: 760; margin-bottom: 8px; }
.hero p { color: var(--muted); font-size: 13px; line-height: 1.55; }
.hero-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
.btn {
display: inline-flex; align-items: center; justify-content: center;
padding: 10px 14px; border-radius: 999px; font-size: 13px; font-weight: 600;
background: var(--accent); color: #1a1520;
}
.btn.ghost {
background: transparent; color: var(--text);
border: 1px solid var(--border);
}
.btn.sm { padding: 7px 11px; font-size: 12px; }
.section { margin: 18px 0 8px; }
.section-head {
display: flex; align-items: baseline; justify-content: space-between;
margin-bottom: 10px;
}
.section-head h2 { font-size: 16px; font-weight: 700; }
.section-head .more { font-size: 12px; color: var(--accent); }
.stats {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin: 14px 0;
}
.stat {
background: var(--card); border: 1px solid var(--border); border-radius: 14px;
padding: 10px 6px; text-align: center;
}
.stat b { display: block; color: var(--accent); font-size: 16px; }
.stat span { display: block; margin-top: 3px; color: var(--muted); font-size: 10px; }
.ring-grid, .card-grid { display: grid; gap: 10px; }
.ring-grid { grid-template-columns: 1fr 1fr; }
.ring-card, .card {
background: var(--card); border: 1px solid var(--border); border-radius: 16px;
padding: 14px; text-align: left;
}
.ring-card .emoji, .card .emoji { font-size: 22px; display: block; margin-bottom: 8px; }
.ring-card strong, .card strong { display: block; font-size: 14px; margin-bottom: 4px; }
.ring-card span, .card .desc, .meta {
display: block; color: var(--muted); font-size: 12px; line-height: 1.45;
}
.list { display: flex; flex-direction: column; gap: 10px; }
.list-item {
display: flex; gap: 12px; align-items: flex-start;
background: var(--card); border: 1px solid var(--border); border-radius: 16px;
padding: 12px 14px; text-align: left; width: 100%;
}
.list-item .lead {
width: 42px; height: 42px; border-radius: 12px; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
background: var(--card2); font-size: 22px;
}
.list-item .body { flex: 1; min-width: 0; }
.list-item .title { font-size: 14px; font-weight: 650; }
.list-item .meta { margin-top: 4px; }
.fav-dot { color: var(--accent); font-size: 12px; margin-left: 6px; }
.chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.chip-btn {
padding: 7px 12px; border-radius: 999px; font-size: 12px;
background: var(--card); border: 1px solid var(--border); color: var(--muted);
}
.chip-btn.is-on { color: #1a1520; background: var(--accent); border-color: var(--accent); }
.detail-hero {
border-radius: 20px; padding: 18px; margin-bottom: 14px;
background: linear-gradient(160deg, rgba(240,198,116,.18), transparent 55%), var(--bg2);
border: 1px solid var(--border);
}
.detail-hero .emoji { font-size: 36px; }
.detail-hero h1 { font-size: 24px; margin: 8px 0 4px; }
.kv {
display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 12px 0;
}
.kv .cell {
background: var(--card); border-radius: 14px; padding: 12px; border: 1px solid var(--border);
}
.kv .k { color: var(--muted); font-size: 11px; }
.kv .v { display: block; margin-top: 4px; font-size: 16px; font-weight: 700; color: var(--accent); }
.hl { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0 14px; }
.hl span {
font-size: 12px; padding: 6px 10px; border-radius: 999px;
background: rgba(125, 211, 199, 0.1); color: #b7efe6;
}
.article {
background: var(--card); border: 1px solid var(--border); border-radius: 16px;
padding: 16px; line-height: 1.7; font-size: 14px; color: #e8e2f4;
white-space: pre-wrap;
}
.article h2, .article h3 { color: var(--text); margin: 14px 0 8px; font-size: 16px; }
.article p { margin: 0 0 10px; }
.article ul, .article ol { padding-left: 18px; margin-bottom: 10px; }
.article li { margin: 4px 0; }
.article table { width: 100%; border-collapse: collapse; font-size: 12px; margin: 10px 0; }
.article th, .article td { border: 1px solid var(--border); padding: 6px 8px; text-align: left; }
.form {
background: var(--card); border: 1px solid var(--border); border-radius: 16px; padding: 14px;
}
.field { margin-bottom: 12px; }
.field label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px; }
.field input, .field select, .field textarea {
width: 100%; padding: 10px 12px; border-radius: 12px;
background: rgba(0,0,0,.25); border: 1px solid var(--border);
}
.check-list { display: flex; flex-direction: column; gap: 8px; }
.check-item {
display: flex; align-items: center; gap: 10px;
background: var(--card); border: 1px solid var(--border); border-radius: 14px;
padding: 12px 14px; text-align: left; width: 100%;
}
.check-item.is-on { border-color: rgba(240,198,116,.5); background: rgba(240,198,116,.08); }
.check-box {
width: 20px; height: 20px; border-radius: 6px; border: 1.5px solid var(--muted);
display: flex; align-items: center; justify-content: center; font-size: 12px;
}
.check-item.is-on .check-box { background: var(--accent); border-color: var(--accent); color: #1a1520; }
.lifestyle {
display: flex; gap: 8px; overflow-x: auto; padding-bottom: 4px; -webkit-overflow-scrolling: touch;
}
.life-card {
min-width: 140px; flex-shrink: 0;
background: var(--card); border: 1px solid var(--border); border-radius: 16px;
padding: 12px; text-align: left;
}
.life-card.is-on { border-color: var(--accent); }
.life-detail {
margin-top: 10px; padding: 12px; border-radius: 14px;
background: rgba(240,198,116,.08); color: #e8dfc8; font-size: 13px; line-height: 1.55;
}
.note {
margin: 10px 0 14px; padding: 10px 12px; border-radius: 12px;
background: rgba(240,198,116,.1); color: #e8d7a8; font-size: 12px; line-height: 1.5;
}
.empty { text-align: center; color: var(--muted); padding: 28px 10px; font-size: 13px; }
.map-board {
position: relative; height: 220px; border-radius: 18px; overflow: hidden;
background:
radial-gradient(circle at 20% 40%, rgba(125,211,199,.15), transparent 30%),
radial-gradient(circle at 70% 35%, rgba(240,198,116,.12), transparent 28%),
linear-gradient(180deg, #17141f, #0f0d15);
border: 1px solid var(--border); margin-bottom: 12px;
}
.map-pin {
position: absolute; transform: translate(-50%, -100%);
font-size: 18px; filter: drop-shadow(0 2px 4px rgba(0,0,0,.5));
}
.map-pin span {
display: block; margin-top: 2px; font-size: 9px; color: var(--text);
background: rgba(0,0,0,.45); padding: 1px 4px; border-radius: 4px; white-space: nowrap;
}
.sheet { position: fixed; inset: 0; z-index: 60; }
.sheet-mask { position: absolute; inset: 0; background: rgba(0,0,0,.55); }
.sheet-panel {
position: absolute; left: 0; right: 0; bottom: 0;
max-height: 78%; overflow: auto;
background: #16131e; border-radius: 20px 20px 0 0;
padding: 10px 16px calc(20px + var(--safe-b));
border-top: 1px solid var(--border);
}
.sheet-handle {
width: 42px; height: 4px; border-radius: 99px; background: rgba(255,255,255,.2);
margin: 4px auto 14px;
}
.compare-pick { display: flex; gap: 8px; margin-bottom: 10px; }
.compare-pick select { flex: 1; padding: 10px; border-radius: 12px; background: rgba(0,0,0,.25); border: 1px solid var(--border); }
.compare-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.compare-table th, .compare-table td { border-bottom: 1px solid var(--border); padding: 10px 6px; text-align: left; }
.compare-table th { color: var(--muted); font-weight: 500; }
.quote {
background: var(--card); border-left: 3px solid var(--accent);
border-radius: 0 14px 14px 0; padding: 12px 14px; margin-bottom: 10px;
}
.quote p { font-size: 13px; line-height: 1.55; }
.quote .who { margin-top: 8px; color: var(--muted); font-size: 12px; }

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python3
"""Copy tool/ into release/ and embed src/data.js (no network)."""
"""Copy tool/ + src/data.js into release/ for XHS zip (H5 mirror)."""
from __future__ import annotations
import shutil
from pathlib import Path
@ -9,14 +10,30 @@ TOOL = ROOT / "tool"
SRC_DATA = ROOT / "src" / "data.js"
RELEASE = ROOT / "release"
FILES = (
"index.html",
"app.js",
"nomadro.css",
"xhs-bridge.js",
)
def main() -> int:
if RELEASE.exists():
shutil.rmtree(RELEASE)
RELEASE.mkdir(parents=True)
for name in ("index.html", "styles.css", "app.js", "xhs-bridge.js"):
shutil.copy2(TOOL / name, RELEASE / name)
for name in FILES:
src = TOOL / name
if not src.exists():
print(f"missing {src}")
return 1
shutil.copy2(src, RELEASE / name)
if not SRC_DATA.exists():
print(f"missing {SRC_DATA} — run: python scripts/build_data.py")
return 1
shutil.copy2(SRC_DATA, RELEASE / "data.js")
# keep a copy in tool/ for local preview
shutil.copy2(SRC_DATA, TOOL / "data.js")
print(f"assembled {RELEASE}")
return 0

View File

@ -34,13 +34,13 @@ def main() -> int:
"meetups": len(meetups),
"blog": len(m.BLOG_POSTS),
"faqs": len(m.FAQS),
"total_nomads": "35.6M",
"countries": 195,
"satisfaction": 87,
"avg_cost": sum(d["cost"] for d in m.DESTINATIONS) // max(1, len(m.DESTINATIONS)) // 100,
"active_today": 127,
},
"ticker": [
"🌏 全球数字游民正在路上",
"📋 签证政策实时整理(离线快照)",
"🎉 同城活动与社区讨论可先浏览",
"🎓 游民学院免费课时可学",
],
"ticker": m.TICKER_MESSAGES,
"rings": {
"explore": [
{"id": "destinations", "emoji": "🌴", "title": "目的地", "desc": "发现下一座城市"},

View File

@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""Sync frontend/src/app/globals.css -> redmini/tool/nomadro.css (Chrome 61 sanitize)."""
from __future__ import annotations
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
SRC = ROOT / "frontend" / "src" / "app" / "globals.css"
OUT = ROOT / "redmini" / "tool" / "nomadro.css"
def unwrap_layers(text: str) -> str:
while True:
m = re.search(r"@layer\s+[\w-]+\s*\{", text)
if not m:
break
start = m.start()
i = m.end()
depth = 1
while i < len(text) and depth:
if text[i] == "{":
depth += 1
elif text[i] == "}":
depth -= 1
i += 1
text = text[:start] + text[m.end() : i - 1] + text[i:]
return text
def main() -> int:
src = SRC.read_text(encoding="utf-8")
src = re.sub(
r"html:has\(\[data-ebook-root\]\)[^{]*\{(?:[^{}]|\{[^{}]*\})*\}",
"",
src,
)
src = re.sub(
r"html:has\(\[data-ebook-root\]\)\s+body\s*\{(?:[^{}]|\{[^{}]*\})*\}",
"",
src,
)
src = unwrap_layers(src)
src = src.replace("var(--font-outfit), ", "").replace(", var(--font-outfit)", "")
OUT.write_text(
"/* Mirror of frontend/src/app/globals.css — synced for XHS offline H5 */\n" + src,
encoding="utf-8",
)
print(f"wrote {OUT} ({OUT.stat().st_size} bytes) open={src.count('{')} close={src.count('}')}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -3,40 +3,49 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>nomadro · 数字游民</title>
<link rel="stylesheet" href="./styles.css" />
<meta name="theme-color" content="#0a0e17" />
<title>nomadro · 数字游民旅居指南</title>
<link rel="stylesheet" href="./nomadro.css" />
<style>
/* XHS offline shell — keep H5 look; hide web-only chrome that needs network */
.xhs-offline-banner {
position: sticky;
top: 0;
z-index: 100;
padding: 8px 16px;
text-align: center;
font-size: 12px;
color: var(--text-secondary);
background: rgba(17, 24, 39, 0.95);
border-bottom: var(--border-glass);
}
.xhs-offline-banner strong { color: var(--accent-3); }
#app-root { min-height: 100vh; }
.page-panel { display: none; }
.page-panel.is-active { display: block; }
.xhs-back {
position: fixed;
left: 12px;
bottom: 18px;
z-index: 90;
width: 44px;
height: 44px;
border-radius: 999px;
border: var(--border-glass);
background: var(--bg-card);
color: var(--text-primary);
box-shadow: var(--shadow-card);
display: none;
}
.xhs-back.is-on { display: block; }
</style>
</head>
<body>
<div id="app">
<header class="top-chrome" id="top-chrome">
<div class="top-left">
<button type="button" class="icon-btn is-hidden" id="btn-back" aria-label="返回">←</button>
<div>
<p class="top-brand" id="top-brand">nomadro</p>
<p class="top-sub" id="top-sub">离线镜像</p>
</div>
</div>
<button type="button" class="chip" id="btn-offline" title="离线说明">离线</button>
</header>
<main class="main" id="main"></main>
<nav class="tabbar" id="tabbar" aria-label="主导航">
<button type="button" class="tab is-active" data-tab="home"><span>🏠</span><i>首页</i></button>
<button type="button" class="tab" data-tab="explore"><span>🧭</span><i>探索</i></button>
<button type="button" class="tab" data-tab="connect"><span>🤝</span><i>连接</i></button>
<button type="button" class="tab" data-tab="grow"><span>🌱</span><i>成长</i></button>
<button type="button" class="tab" data-tab="mine"><span>👤</span><i>我的</i></button>
</nav>
</div>
<div class="sheet is-hidden" id="sheet" role="dialog" aria-modal="true">
<div class="sheet-mask" id="sheet-mask"></div>
<div class="sheet-panel">
<div class="sheet-handle"></div>
<div id="sheet-body"></div>
</div>
<div class="xhs-offline-banner">
<strong>离线镜像</strong> · 样式与结构同步官网 H5 · 数据为本机快照 · 完整互动请打开 nomadweb.nomadro.com
</div>
<div id="app-root"></div>
<button type="button" class="xhs-back" id="xhs-back" aria-label="返回">←</button>
<script src="./data.js"></script>
<script src="./xhs-bridge.js"></script>

12011
redmini/tool/nomadro.css Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,277 +0,0 @@
:root {
--bg: #0b0a10;
--bg2: #14121c;
--card: rgba(255, 255, 255, 0.06);
--card2: rgba(255, 255, 255, 0.09);
--text: #f4f0ea;
--muted: #9a94a8;
--accent: #f0c674;
--accent2: #7dd3c7;
--danger: #f07178;
--border: rgba(255, 255, 255, 0.1);
--safe-b: env(safe-area-inset-bottom, 0px);
--safe-t: env(safe-area-inset-top, 0px);
--tab-h: 64px;
--top-h: 56px;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
-webkit-font-smoothing: antialiased;
-webkit-tap-highlight-color: transparent;
}
button, input, select, textarea { font: inherit; color: inherit; }
button { border: 0; background: none; cursor: pointer; }
.is-hidden { display: none !important; }
#app {
min-height: 100%;
min-height: 100vh;
padding-top: calc(var(--top-h) + var(--safe-t));
padding-bottom: calc(var(--tab-h) + var(--safe-b) + 8px);
}
.top-chrome {
position: fixed; top: 0; left: 0; right: 0; z-index: 40;
height: calc(var(--top-h) + var(--safe-t));
padding: var(--safe-t) 14px 0;
display: flex; align-items: center; justify-content: space-between;
background: rgba(11, 10, 16, 0.92);
backdrop-filter: blur(14px);
border-bottom: 1px solid var(--border);
}
.top-left { display: flex; align-items: center; gap: 8px; }
.top-brand { font-weight: 700; font-size: 16px; letter-spacing: 0.4px; }
.top-sub { font-size: 11px; color: var(--muted); margin-top: 1px; }
.icon-btn {
width: 34px; height: 34px; border-radius: 10px;
background: var(--card); color: var(--text); font-size: 16px;
}
.chip {
padding: 6px 10px; border-radius: 999px; font-size: 12px;
background: rgba(240, 198, 116, 0.15); color: var(--accent);
border: 1px solid rgba(240, 198, 116, 0.35);
}
.main { padding: 12px 14px 20px; max-width: 720px; margin: 0 auto; }
.tabbar {
position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
height: calc(var(--tab-h) + var(--safe-b));
padding: 6px 6px var(--safe-b);
display: flex; background: rgba(16, 14, 24, 0.96);
border-top: 1px solid var(--border);
backdrop-filter: blur(14px);
}
.tab {
flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px;
color: var(--muted); font-size: 10px; padding: 6px 0;
}
.tab span { font-size: 18px; line-height: 1; }
.tab.is-active { color: var(--accent); }
.hero {
position: relative; overflow: hidden; border-radius: 22px;
padding: 22px 18px 20px;
background:
radial-gradient(500px 220px at 0% 0%, rgba(240, 198, 116, 0.22), transparent 55%),
radial-gradient(420px 200px at 100% 0%, rgba(125, 211, 199, 0.16), transparent 50%),
linear-gradient(165deg, #15121d, #0e0c14 70%);
border: 1px solid var(--border);
margin-bottom: 16px;
}
.hero-badge {
display: inline-block; font-size: 11px; color: var(--accent2);
background: rgba(125, 211, 199, 0.12); padding: 4px 8px; border-radius: 999px;
margin-bottom: 10px;
}
.hero h1 { font-size: 28px; line-height: 1.2; font-weight: 760; margin-bottom: 8px; }
.hero p { color: var(--muted); font-size: 13px; line-height: 1.55; }
.hero-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
.btn {
display: inline-flex; align-items: center; justify-content: center;
padding: 10px 14px; border-radius: 999px; font-size: 13px; font-weight: 600;
background: var(--accent); color: #1a1520;
}
.btn.ghost {
background: transparent; color: var(--text);
border: 1px solid var(--border);
}
.btn.sm { padding: 7px 11px; font-size: 12px; }
.section { margin: 18px 0 8px; }
.section-head {
display: flex; align-items: baseline; justify-content: space-between;
margin-bottom: 10px;
}
.section-head h2 { font-size: 16px; font-weight: 700; }
.section-head .more { font-size: 12px; color: var(--accent); }
.stats {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin: 14px 0;
}
.stat {
background: var(--card); border: 1px solid var(--border); border-radius: 14px;
padding: 10px 6px; text-align: center;
}
.stat b { display: block; color: var(--accent); font-size: 16px; }
.stat span { display: block; margin-top: 3px; color: var(--muted); font-size: 10px; }
.ring-grid, .card-grid { display: grid; gap: 10px; }
.ring-grid { grid-template-columns: 1fr 1fr; }
.ring-card, .card {
background: var(--card); border: 1px solid var(--border); border-radius: 16px;
padding: 14px; text-align: left;
}
.ring-card .emoji, .card .emoji { font-size: 22px; display: block; margin-bottom: 8px; }
.ring-card strong, .card strong { display: block; font-size: 14px; margin-bottom: 4px; }
.ring-card span, .card .desc, .meta {
display: block; color: var(--muted); font-size: 12px; line-height: 1.45;
}
.list { display: flex; flex-direction: column; gap: 10px; }
.list-item {
display: flex; gap: 12px; align-items: flex-start;
background: var(--card); border: 1px solid var(--border); border-radius: 16px;
padding: 12px 14px; text-align: left; width: 100%;
}
.list-item .lead {
width: 42px; height: 42px; border-radius: 12px; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
background: var(--card2); font-size: 22px;
}
.list-item .body { flex: 1; min-width: 0; }
.list-item .title { font-size: 14px; font-weight: 650; }
.list-item .meta { margin-top: 4px; }
.fav-dot { color: var(--accent); font-size: 12px; margin-left: 6px; }
.chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.chip-btn {
padding: 7px 12px; border-radius: 999px; font-size: 12px;
background: var(--card); border: 1px solid var(--border); color: var(--muted);
}
.chip-btn.is-on { color: #1a1520; background: var(--accent); border-color: var(--accent); }
.detail-hero {
border-radius: 20px; padding: 18px; margin-bottom: 14px;
background: linear-gradient(160deg, rgba(240,198,116,.18), transparent 55%), var(--bg2);
border: 1px solid var(--border);
}
.detail-hero .emoji { font-size: 36px; }
.detail-hero h1 { font-size: 24px; margin: 8px 0 4px; }
.kv {
display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 12px 0;
}
.kv .cell {
background: var(--card); border-radius: 14px; padding: 12px; border: 1px solid var(--border);
}
.kv .k { color: var(--muted); font-size: 11px; }
.kv .v { display: block; margin-top: 4px; font-size: 16px; font-weight: 700; color: var(--accent); }
.hl { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0 14px; }
.hl span {
font-size: 12px; padding: 6px 10px; border-radius: 999px;
background: rgba(125, 211, 199, 0.1); color: #b7efe6;
}
.article {
background: var(--card); border: 1px solid var(--border); border-radius: 16px;
padding: 16px; line-height: 1.7; font-size: 14px; color: #e8e2f4;
white-space: pre-wrap;
}
.article h2, .article h3 { color: var(--text); margin: 14px 0 8px; font-size: 16px; }
.article p { margin: 0 0 10px; }
.article ul, .article ol { padding-left: 18px; margin-bottom: 10px; }
.article li { margin: 4px 0; }
.article table { width: 100%; border-collapse: collapse; font-size: 12px; margin: 10px 0; }
.article th, .article td { border: 1px solid var(--border); padding: 6px 8px; text-align: left; }
.form {
background: var(--card); border: 1px solid var(--border); border-radius: 16px; padding: 14px;
}
.field { margin-bottom: 12px; }
.field label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px; }
.field input, .field select, .field textarea {
width: 100%; padding: 10px 12px; border-radius: 12px;
background: rgba(0,0,0,.25); border: 1px solid var(--border);
}
.check-list { display: flex; flex-direction: column; gap: 8px; }
.check-item {
display: flex; align-items: center; gap: 10px;
background: var(--card); border: 1px solid var(--border); border-radius: 14px;
padding: 12px 14px; text-align: left; width: 100%;
}
.check-item.is-on { border-color: rgba(240,198,116,.5); background: rgba(240,198,116,.08); }
.check-box {
width: 20px; height: 20px; border-radius: 6px; border: 1.5px solid var(--muted);
display: flex; align-items: center; justify-content: center; font-size: 12px;
}
.check-item.is-on .check-box { background: var(--accent); border-color: var(--accent); color: #1a1520; }
.lifestyle {
display: flex; gap: 8px; overflow-x: auto; padding-bottom: 4px; -webkit-overflow-scrolling: touch;
}
.life-card {
min-width: 140px; flex-shrink: 0;
background: var(--card); border: 1px solid var(--border); border-radius: 16px;
padding: 12px; text-align: left;
}
.life-card.is-on { border-color: var(--accent); }
.life-detail {
margin-top: 10px; padding: 12px; border-radius: 14px;
background: rgba(240,198,116,.08); color: #e8dfc8; font-size: 13px; line-height: 1.55;
}
.note {
margin: 10px 0 14px; padding: 10px 12px; border-radius: 12px;
background: rgba(240,198,116,.1); color: #e8d7a8; font-size: 12px; line-height: 1.5;
}
.empty { text-align: center; color: var(--muted); padding: 28px 10px; font-size: 13px; }
.map-board {
position: relative; height: 220px; border-radius: 18px; overflow: hidden;
background:
radial-gradient(circle at 20% 40%, rgba(125,211,199,.15), transparent 30%),
radial-gradient(circle at 70% 35%, rgba(240,198,116,.12), transparent 28%),
linear-gradient(180deg, #17141f, #0f0d15);
border: 1px solid var(--border); margin-bottom: 12px;
}
.map-pin {
position: absolute; transform: translate(-50%, -100%);
font-size: 18px; filter: drop-shadow(0 2px 4px rgba(0,0,0,.5));
}
.map-pin span {
display: block; margin-top: 2px; font-size: 9px; color: var(--text);
background: rgba(0,0,0,.45); padding: 1px 4px; border-radius: 4px; white-space: nowrap;
}
.sheet { position: fixed; inset: 0; z-index: 60; }
.sheet-mask { position: absolute; inset: 0; background: rgba(0,0,0,.55); }
.sheet-panel {
position: absolute; left: 0; right: 0; bottom: 0;
max-height: 78%; overflow: auto;
background: #16131e; border-radius: 20px 20px 0 0;
padding: 10px 16px calc(20px + var(--safe-b));
border-top: 1px solid var(--border);
}
.sheet-handle {
width: 42px; height: 4px; border-radius: 99px; background: rgba(255,255,255,.2);
margin: 4px auto 14px;
}
.compare-pick { display: flex; gap: 8px; margin-bottom: 10px; }
.compare-pick select { flex: 1; padding: 10px; border-radius: 12px; background: rgba(0,0,0,.25); border: 1px solid var(--border); }
.compare-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.compare-table th, .compare-table td { border-bottom: 1px solid var(--border); padding: 10px 6px; text-align: left; }
.compare-table th { color: var(--muted); font-weight: 500; }
.quote {
background: var(--card); border-left: 3px solid var(--accent);
border-radius: 0 14px 14px 0; padding: 12px 14px; margin-bottom: 10px;
}
.quote p { font-size: 13px; line-height: 1.55; }
.quote .who { margin-top: 8px; color: var(--muted); font-size: 12px; }