From bf7167c18561e0fe45df74549eea58bc2101bc8b Mon Sep 17 00:00:00 2001
From: eric
Date: Sun, 27 Sep 2026 13:04:35 -0500
Subject: [PATCH] Rebuild redmini as full offline nomadro product mirror for
XHS.
Co-authored-by: Cursor
---
redmini/README.md | 31 +-
redmini/release/app.js | 831 +++++++++++++++++++++++++++++++--
redmini/release/data.js | 20 +-
redmini/release/index.html | 34 +-
redmini/release/styles.css | 289 +++++++++++-
redmini/release/xhs-bridge.js | 653 ++++++++++++++++++++++++--
redmini/scripts/build_data.py | 103 ++++
redmini/scripts/static_scan.py | 10 +-
redmini/src/data.js | 20 +-
redmini/tool/app.js | 831 +++++++++++++++++++++++++++++++--
redmini/tool/data.js | 1 +
redmini/tool/index.html | 34 +-
redmini/tool/styles.css | 289 +++++++++++-
redmini/tool/xhs-bridge.js | 653 ++++++++++++++++++++++++--
14 files changed, 3597 insertions(+), 202 deletions(-)
create mode 100644 redmini/scripts/build_data.py
create mode 100644 redmini/tool/data.js
diff --git a/redmini/README.md b/redmini/README.md
index f0bcaf6..764a664 100644
--- a/redmini/README.md
+++ b/redmini/README.md
@@ -1,25 +1,36 @@
-# redmini · nomadro 小红书小程序(纯前端离线)
+# redmini · nomadro 小红书离线镜像(1:1 产品面)
-参考 Desktop/redmini:小红书 **miniTool 无法联网**,本目录禁止 `fetch` / XHR / WebSocket。
+小红书 **miniTool 无法联网**。本目录是 [nomadweb.nomadro.com](https://nomadweb.nomadro.com) 的离线内容镜像:
-数据写死在 `src/data.js`,收藏仅存本地 Storage。
+- 三环导航:探索 / 连接 / 成长(与官网一致)
+- 首页:Hero、统计、热门目的地、游民一天、游民心声
+- 探索:目的地列表/详情、地图快照、签证、智能匹配、城市对比
+- 连接:活动、社区讨论、心声
+- 成长:游民学院课时、博客全文、工具箱、费用估算、FAQ
+- 我的:收藏、最近浏览、旅居计划清单(本地 Storage)
-## 打包
+数据来自 `backend/app/data/mock_data.py` 等官网同源 mock(打包进 `src/data.js`)。
+
+## 打包上传
```bash
cd redmini
+# 若 mock 有更新,先重建 data.js(在仓库根目录):
+# cd ../backend && python -c "..." # 或复用 scripts 导出
python scripts/assemble.py
-python scripts/static_scan.py
-python scripts/pack.py
+python scripts/static_scan.py # 禁止 fetch / XHR / WebSocket
+python scripts/pack.py # -> dist/nomadro-redmini.zip
```
-产出:`dist/nomadro-redmini.zip`(根目录含 `index.html`),用小红书开发者工具上传。
+用小红书开发者工具上传 zip(根目录含 `index.html`)。
## 目录
| 路径 | 说明 |
|------|------|
-| `src/data.js` | 离线目的地与提示文案 |
-| `tool/` | 源码 HTML/CSS/JS |
+| `src/data.js` | 离线数据包 |
+| `tool/` | HTML / CSS / JS / xhs-bridge |
| `release/` | assemble 产物 |
-| `scripts/` | assemble / scan / pack |
+| `scripts/` | assemble / static_scan / pack |
+
+本地浏览器预览:直接打开 `tool/index.html`(需先把 `src/data.js` 拷到 `tool/`,或跑 assemble 后打开 `release/index.html`)。
diff --git a/redmini/release/app.js b/redmini/release/app.js
index e22fbb2..0692e53 100644
--- a/redmini/release/app.js
+++ b/redmini/release/app.js
@@ -1,42 +1,805 @@
(function () {
- 'use strict';
- var data = window.NOMADRO_DATA || { destinations: [], tips: [] };
- var root = document.getElementById('app');
- var favKey = 'nomadro_redmini_favs';
+ "use strict";
- function render(favs) {
- favs = favs || [];
- var cards = (data.destinations || []).map(function (d) {
- var marked = favs.indexOf(d.slug) >= 0 ? ' · 已收藏' : '';
- return (
- '' +
- '' + d.name + '
' +
- '' + (d.country || '') + ' · ¥' + (d.cost || '—') + '/月 · ★' + (d.rating || '—') + '
' +
- '' + (d.tagline || '') + '
' +
- '' + (marked || '点按收藏(仅本地)') + '
' +
- ' '
- );
- }).join('');
- var tips = (data.tips || []).map(function (t) { return '' + t + ' '; }).join('');
- root.innerHTML =
- '' + (data.brand || 'nomadro') + '
' +
- '' + (data.tagline || '') + '
' +
- '' + (data.note || '') + '
' +
- cards +
- '';
+ var DATA = window.NOMADRO_DATA || {};
+ var Bridge = window.XhsBridge || window.NomadroXhs || null;
+ var FAV_KEY = "nomadro_xhs_favs";
+ var PLAN_KEY = "nomadro_xhs_plan";
+ var CHECK_KEY = "nomadro_xhs_checklist";
+ var RECENT_KEY = "nomadro_xhs_recent";
- Array.prototype.forEach.call(root.querySelectorAll('.card'), function (el) {
- el.addEventListener('click', function () {
- var slug = el.getAttribute('data-slug');
- var next = favs.slice();
- var i = next.indexOf(slug);
- if (i >= 0) next.splice(i, 1); else next.push(slug);
- window.NomadroXhs.setLocalData(favKey, next).then(function () { render(next); });
+ var state = {
+ tab: "home",
+ stack: [],
+ favs: [],
+ plan: [],
+ checklist: {},
+ recent: [],
+ region: "all",
+ lifeIdx: 0,
+ compareA: "",
+ compareB: "",
+ matchBudget: 6000,
+ matchClimate: "any",
+ matchRegion: "all",
+ };
+
+ var main = document.getElementById("main");
+ var topBrand = document.getElementById("top-brand");
+ var topSub = document.getElementById("top-sub");
+ var btnBack = document.getElementById("btn-back");
+ var tabbar = document.getElementById("tabbar");
+ var sheet = document.getElementById("sheet");
+ var sheetBody = document.getElementById("sheet-body");
+
+ function on(el, ev, fn) {
+ if (el) el.addEventListener(ev, fn);
+ }
+
+ function esc(s) {
+ return String(s == null ? "" : s)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+ }
+
+ function stars(n) {
+ var s = "";
+ var i;
+ for (i = 0; i < 5; i++) s += i < Math.round(n || 0) ? "★" : "☆";
+ return s;
+ }
+
+ function storageGet(key, fallback) {
+ if (Bridge && typeof Bridge.getLocalData === "function") {
+ return Bridge.getLocalData(key).then(function (v) {
+ return v == null ? fallback : v;
});
+ }
+ try {
+ var raw = localStorage.getItem(key);
+ return Promise.resolve(raw ? JSON.parse(raw) : fallback);
+ } catch (e) {
+ return Promise.resolve(fallback);
+ }
+ }
+
+ function storageSet(key, val) {
+ if (Bridge && typeof Bridge.setLocalData === "function") {
+ return Bridge.setLocalData(key, val);
+ }
+ try {
+ localStorage.setItem(key, JSON.stringify(val));
+ return Promise.resolve(true);
+ } catch (e) {
+ return Promise.resolve(false);
+ }
+ }
+
+ function destBySlug(slug) {
+ var list = DATA.destinations || [];
+ var i;
+ for (i = 0; i < list.length; i++) if (list[i].slug === slug) return list[i];
+ return null;
+ }
+
+ function isFav(slug) {
+ return state.favs.indexOf(slug) >= 0;
+ }
+
+ function toggleFav(slug) {
+ var i = state.favs.indexOf(slug);
+ if (i >= 0) state.favs.splice(i, 1);
+ else state.favs.push(slug);
+ storageSet(FAV_KEY, state.favs);
+ render();
+ }
+
+ function pushRecent(slug) {
+ var next = [slug].concat(state.recent.filter(function (s) { return s !== slug; })).slice(0, 8);
+ state.recent = next;
+ storageSet(RECENT_KEY, next);
+ }
+
+ function setChrome(title, sub, showBack) {
+ topBrand.textContent = title || "nomadro";
+ topSub.textContent = sub || "离线镜像";
+ if (showBack) btnBack.classList.remove("is-hidden");
+ else btnBack.classList.add("is-hidden");
+ }
+
+ function goTab(tab) {
+ state.tab = tab;
+ state.stack = [];
+ Array.prototype.forEach.call(tabbar.querySelectorAll(".tab"), function (btn) {
+ btn.classList.toggle("is-active", btn.getAttribute("data-tab") === tab);
+ });
+ render();
+ }
+
+ function openView(view, params) {
+ state.stack.push({ view: view, params: params || {} });
+ render();
+ }
+
+ function goBack() {
+ if (state.stack.length) {
+ state.stack.pop();
+ render();
+ return;
+ }
+ goTab("home");
+ }
+
+ function currentRoute() {
+ if (state.stack.length) return state.stack[state.stack.length - 1];
+ return { view: state.tab, params: {} };
+ }
+
+ function openSheet(html) {
+ sheetBody.innerHTML = html;
+ sheet.classList.remove("is-hidden");
+ }
+
+ function closeSheet() {
+ sheet.classList.add("is-hidden");
+ sheetBody.innerHTML = "";
+ }
+
+ function mdLite(src) {
+ var text = String(src || "");
+ text = esc(text);
+ text = text.replace(/^### (.*)$/gm, "$1 ");
+ text = text.replace(/^## (.*)$/gm, "$1 ");
+ text = text.replace(/\*\*(.+?)\*\*/g, "$1 ");
+ text = text.replace(/^- (.*)$/gm, "$1 ");
+ text = text.replace(/(.*<\/li>\n?)+/g, function (m) {
+ return "";
+ });
+ text = text.replace(/\n\n/g, "
");
+ return "
" + text + "
";
+ }
+
+ /* ---------- views ---------- */
+
+ function viewHome() {
+ setChrome("nomadro", "用一行代码环游世界", false);
+ var stats = DATA.stats || {};
+ var dests = (DATA.destinations || []).slice().sort(function (a, b) { return (b.rating || 0) - (a.rating || 0); }).slice(0, 4);
+ var voices = (DATA.testimonials || []).slice(0, 3);
+ var life = DATA.lifestyle || [];
+ var lifeActive = life[state.lifeIdx] || life[0];
+ var ticker = (DATA.ticker || []).join(" · ");
+
+ return (
+ '' +
+ '🌏 全球数字游民正在路上
' +
+ "用一行代码 环游世界 " +
+ "" + esc(DATA.offlineNote || "") + "
" +
+ '' +
+ '从城市开始 ' +
+ '🎯 智能匹配 ' +
+ "
" +
+ " " +
+ (ticker ? '' + esc(ticker) + "
" : "") +
+ '' +
+ '
' + (stats.destinations || "—") + " 目的地
" +
+ '
' + (stats.visas || "—") + " 签证
" +
+ '
' + (stats.meetups || "—") + " 活动
" +
+ '
' + (stats.blog || "—") + " 文章
" +
+ "
" +
+ '
⚡ 三环路径 ' +
+ '
' +
+ '🧭 探索 Explore 目的地 · 签证 · 匹配 ' +
+ '🤝 连接 Connect 活动 · 社区 · 心声 ' +
+ '🌱 成长 Grow 学院 · 博客 · 工具 ' +
+ '🗓️ 旅居计划 离线清单与收藏 ' +
+ "
" +
+ '
🌴 热门目的地 全部 → ' +
+ '
' + dests.map(destCard).join("") + "
" +
+ '
🗓️ 游民一天 ' +
+ '
' +
+ life.map(function (item, idx) {
+ return '' +
+ '' + esc(item.emoji) + " " + esc(item.title) + " ";
+ }).join("") +
+ "
" +
+ (lifeActive ? '
' + esc(lifeActive.body) + "
" : "") +
+ "
" +
+ '
🗣️ 游民心声 更多 → ' +
+ voices.map(function (t) {
+ return '
' + esc(t.avatar || "") + " " + esc(t.content) + '
' +
+ esc(t.author) + " · " + esc(t.role) + " · " + stars(t.rating) + "
";
+ }).join("") +
+ "
"
+ );
+ }
+
+ function destCard(d) {
+ return (
+ '' +
+ '' + esc(d.emoji || "🌍") + "
" +
+ '' + esc(d.name) +
+ (isFav(d.slug) ? '★ ' : "") +
+ '
' + esc(d.country) + " · ¥" + (d.cost || "—") + "/月 · ★" + (d.rating || "—") +
+ '
' + esc(d.tag || d.tagline || "") + "
"
+ );
+ }
+
+ function viewExploreHub() {
+ setChrome("探索", "Discover where to go", false);
+ var rings = (DATA.rings && DATA.rings.explore) || [];
+ return (
+ '探索环:发现去哪。离线可浏览目的地、签证,并做本地智能匹配。
' +
+ '' +
+ rings.map(function (r) {
+ return '' +
+ esc(r.emoji) + " " + esc(r.title) + " " + esc(r.desc) + " ";
+ }).join("") +
+ "
" +
+ '
🗺️ 游民地图快照 ' + mapBoard() + "
"
+ );
+ }
+
+ function mapBoard() {
+ var list = DATA.destinations || [];
+ var pins = list.map(function (d) {
+ var left = Math.max(8, Math.min(92, ((d.map_x || 500) / 900) * 100));
+ var top = Math.max(12, Math.min(88, ((d.map_y || 220) / 400) * 100));
+ return '' + esc(d.emoji || "📍") + "" + esc(d.name) + " ";
+ }).join("");
+ return '' + pins + "
";
+ }
+
+ function viewDestinations() {
+ setChrome("目的地", "Top nomad cities", true);
+ var regions = [
+ { id: "all", label: "全部" },
+ { id: "sea", label: "东南亚" },
+ { id: "europe", label: "欧洲" },
+ { id: "asia", label: "东亚" },
+ { id: "latam", label: "拉美" },
+ ];
+ var list = (DATA.destinations || []).filter(function (d) {
+ return state.region === "all" || d.region === state.region;
+ });
+ return (
+ '' +
+ regions.map(function (r) {
+ return '' + r.label + " ";
+ }).join("") +
+ "
" +
+ '' + list.map(destCard).join("") + "
"
+ );
+ }
+
+ function viewDestination(params) {
+ var d = destBySlug(params.slug);
+ if (!d) return '未找到城市
';
+ pushRecent(d.slug);
+ setChrome(d.name, d.country, true);
+ var fav = isFav(d.slug);
+ return (
+ '' +
+ '
' + esc(d.emoji || "🌍") + "
" +
+ "
" + esc(d.name) + " " +
+ '
' + esc(d.tag) + " · " + esc(d.nomads_count || "") + " 游民
" +
+ "
" + esc(d.description) + "
" +
+ '
' +
+ '' + (fav ? "取消收藏" : "收藏城市") + " " +
+ '加入计划 ' +
+ "
" +
+ "
" +
+ '' +
+ '
月生活费
¥' + (d.cost || "—") + " " +
+ '
网速
' + (d.speed || "—") + " Mbps " +
+ '
均温
' + (d.temperature || "—") + "°C " +
+ '
评分
' + (d.rating || "—") + " " +
+ "
" +
+ '' + (d.highlights || []).map(function (h) { return "" + esc(h) + " "; }).join("") + "
" +
+ '
接下来 ' +
+ '' +
+ '📋 签证指南 查远程友好签证 ' +
+ '🎉 同城活动 先线下遇见 ' +
+ "
"
+ );
+ }
+
+ function viewVisas() {
+ setChrome("签证指南", "Remote-friendly visas", true);
+ return (
+ '离线快照,政策可能变动。出发前请再核对官网。
' +
+ '' +
+ (DATA.visas || []).map(function (v) {
+ return '
' + esc(v.flag || "🛂") + '
' +
+ '
' + esc(v.name) + " · " + esc(v.badge) + "
" +
+ '
' + esc(v.duration) + " · 收入 " + esc(v.income_req) + " · 审批 " + esc(v.approval_time) + "
" +
+ '
' + esc(v.extra) + " · 难度 " + esc(v.difficulty_label) + "
" +
+ "
";
+ }).join("") +
+ "
"
+ );
+ }
+
+ function viewMatcher() {
+ setChrome("智能匹配", "Find your city", true);
+ var scored = scoreDestinations();
+ return (
+ '" +
+ '
推荐结果 ' +
+ scored.slice(0, 6).map(function (item) {
+ var d = item.d;
+ return '
' +
+ '' + esc(d.emoji) + '
' + esc(d.name) +
+ ' · 匹配 ' + item.score + '
¥' + d.cost + "/月 · " + d.speed + "Mbps · ★" + d.rating +
+ "
";
+ }).join("") +
+ "
"
+ );
+ }
+
+ function opt(v, label, cur) {
+ return '" + label + " ";
+ }
+
+ function scoreDestinations() {
+ var budget = Number(state.matchBudget) || 999999;
+ return (DATA.destinations || []).map(function (d) {
+ var score = 50;
+ if (d.cost <= budget) score += 25;
+ else score -= Math.min(30, (d.cost - budget) / 400);
+ if (state.matchRegion !== "all" && d.region !== state.matchRegion) score -= 40;
+ var t = d.temperature || 20;
+ if (state.matchClimate === "warm" && t >= 24) score += 15;
+ if (state.matchClimate === "mild" && t >= 16 && t < 24) score += 15;
+ if (state.matchClimate === "cool" && t <= 16) score += 15;
+ score += (d.rating || 0) * 3;
+ score += Math.min(15, (d.speed || 0) / 15);
+ return { d: d, score: Math.round(score) };
+ }).sort(function (a, b) { return b.score - a.score; });
+ }
+
+ function viewCompare() {
+ setChrome("城市对比", "Side by side", true);
+ var list = DATA.destinations || [];
+ if (!state.compareA && list[0]) state.compareA = list[0].slug;
+ if (!state.compareB && list[1]) state.compareB = list[1].slug;
+ var a = destBySlug(state.compareA) || list[0];
+ var b = destBySlug(state.compareB) || list[1];
+ var options = list.map(function (d) {
+ return '' + esc(d.name) + " ";
+ }).join("");
+ return (
+ '' +
+ '' + options.replace('value="' + esc(a.slug) + '"', 'value="' + esc(a.slug) + '" selected') + " " +
+ '' + options.replace('value="' + esc(b.slug) + '"', 'value="' + esc(b.slug) + '" selected') + " " +
+ "
" +
+ '' +
+ row("城市", (a.emoji || "") + " " + a.name, (b.emoji || "") + " " + b.name) +
+ row("国家", a.country, b.country) +
+ row("月生活费", "¥" + a.cost, "¥" + b.cost) +
+ row("网速", a.speed + " Mbps", b.speed + " Mbps") +
+ row("均温", a.temperature + "°C", b.temperature + "°C") +
+ row("评分", a.rating, b.rating) +
+ row("游民规模", a.nomads_count, b.nomads_count) +
+ "
"
+ );
+ }
+
+ function row(k, a, b) {
+ return "" + esc(k) + " " + esc(a) + " " + esc(b) + " ";
+ }
+
+ function viewConnectHub() {
+ setChrome("连接", "Meet fellow nomads", false);
+ var rings = (DATA.rings && DATA.rings.connect) || [];
+ return (
+ '连接环:遇见同行。离线可浏览活动与讨论;报名/发帖请到网页版。
' +
+ '' +
+ rings.map(function (r) {
+ return '' +
+ esc(r.emoji) + " " + esc(r.title) + " " + esc(r.desc) + " ";
+ }).join("") +
+ "
"
+ );
+ }
+
+ function viewMeetups() {
+ setChrome("游民活动", "Events snapshot", true);
+ return (
+ '' +
+ (DATA.meetups || []).map(function (m) {
+ return '
' + esc(m.emoji || "🎉") + '
' +
+ '
' + esc(m.title) + "
" +
+ '
' + esc(m.city) + " · " + esc(m.date) + " " + esc(m.time || "") + " · " +
+ esc(m.venue || "") + "
" +
+ '
' + esc(m.description || "") + "
" +
+ '
' + (m.rsvp_count || 0) + "/" + (m.max_attendees || "—") + " · " +
+ esc((m.tags || []).join(" · ")) + "
" +
+ "
";
+ }).join("") +
+ "
"
+ );
+ }
+
+ function viewCommunity() {
+ setChrome("社区讨论", "Offline threads", true);
+ return (
+ '' +
+ (DATA.discussions || []).map(function (d) {
+ return '
' +
+ '' + esc(d.author_emoji || "💬") + '
' +
+ '
' + (d.is_pinned ? "📌 " : "") + esc(d.title) + "
" +
+ '
' + esc(d.excerpt) + "
" +
+ '
' + esc(d.author) + " · " + (d.reply_count || 0) + " 回复 · ♥ " + (d.like_count || 0) + "
" +
+ "
";
+ }).join("") +
+ "
"
+ );
+ }
+
+ function viewDiscussion(params) {
+ var list = DATA.discussions || [];
+ var d = null;
+ var i;
+ for (i = 0; i < list.length; i++) if (list[i].id === params.id) d = list[i];
+ if (!d) return '讨论不存在
';
+ setChrome("讨论", d.category || "community", true);
+ var detail = (DATA.discussionDetails && DATA.discussionDetails[d.id]) || null;
+ var replies = (detail && detail.replies) || [];
+ return (
+ '' +
+ "
" + esc(d.title) + " " +
+ '
' + esc(d.author) + " · " + esc(d.created_at || "") + "
" +
+ "
" + esc(d.excerpt) + "
" +
+ "
" +
+ (replies.length
+ ? '
回复 ' +
+ replies.map(function (r) {
+ return '
' + esc(r.content || r.body || "") + '
' +
+ esc(r.author || "") + "
";
+ }).join("") + "
"
+ : '离线包仅含帖子摘要。完整回复请到网页版社区。
')
+ );
+ }
+
+ function viewVoices() {
+ setChrome("游民心声", "Real stories", true);
+ return (DATA.testimonials || []).map(function (t) {
+ return '' + esc(t.avatar || "") + " " + esc(t.content) + '
' +
+ esc(t.author) + " · " + esc(t.role) + " · " + stars(t.rating) + "
";
+ }).join("");
+ }
+
+ function viewGrowHub() {
+ setChrome("成长", "Learn on the road", false);
+ var rings = (DATA.rings && DATA.rings.grow) || [];
+ return (
+ '成长环:路上做事。学院、博客与工具均可离线浏览。
' +
+ '' +
+ rings.map(function (r) {
+ return '' +
+ esc(r.emoji) + " " + esc(r.title) + " " + esc(r.desc) + " ";
+ }).join("") +
+ "
"
+ );
+ }
+
+ function viewDigital() {
+ setChrome("游民学院", "Digital academy", true);
+ var modules = DATA.course || [];
+ return modules.map(function (mod, mi) {
+ return '
' + esc(mod.title) + " " +
+ (mod.lessons || []).map(function (les, li) {
+ var key = mi + "-" + li;
+ return '
' +
+ '' + (les.free ? "🆓" : "🔒") + '
' +
+ '
' + esc(les.title) + "
" +
+ '
' + esc(les.duration) + (les.free ? " · 免费" : " · 网页版 VIP") + "
" +
+ "
";
+ }).join("") + "
";
+ }).join("");
+ }
+
+ function viewLesson(params) {
+ var lesson = (DATA.lessons || {})[params.key];
+ if (!lesson) return '课程不存在
';
+ setChrome(lesson.title, "Lesson", true);
+ return (
+ '' +
+ "
" + esc(lesson.title) + " " +
+ '
' + esc(lesson.duration) + (lesson.free ? " · 免费" : " · VIP 内容离线摘要") + "
" +
+ "
" +
+ '' + esc(lesson.content || "") + "
"
+ );
+ }
+
+ function viewBlog() {
+ setChrome("博客", "Guides & stories", true);
+ return (
+ '' +
+ (DATA.blog || []).map(function (b) {
+ return '
' +
+ '' + esc(b.emoji || "📝") + '
' +
+ '
' + esc(b.title) + "
" +
+ '
' + esc(b.excerpt) + "
" +
+ '
' + esc(b.author) + " · " + esc(b.published_at) + " · " + (b.read_time || "?") + " 分钟
" +
+ "
";
+ }).join("") +
+ "
"
+ );
+ }
+
+ function viewBlogDetail(params) {
+ var list = DATA.blog || [];
+ var b = null;
+ var i;
+ for (i = 0; i < list.length; i++) if (list[i].slug === params.slug) b = list[i];
+ if (!b) return '文章不存在
';
+ setChrome("博客", b.slug, true);
+ var content = (DATA.blogContent && DATA.blogContent[b.slug]) || b.excerpt || "";
+ return (
+ '' +
+ '
' + esc(b.emoji || "📝") + "
" +
+ "
" + esc(b.title) + " " +
+ '
' + esc(b.author) + " · " + esc(b.published_at) + " · " +
+ (b.read_time || "?") + " 分钟 · " + esc((b.tags || []).join(" · ")) + "
" +
+ "
" +
+ '' + mdLite(content) + " "
+ );
+ }
+
+ function viewTools() {
+ setChrome("工具箱", "Offline utilities", true);
+ return (
+ '
实用工具 ' +
+ '
' +
+ '🧮 月费用估算 住宿+餐饮+交通 ' +
+ '✅ 落地清单 第一月 checklist ' +
+ "
" +
+ '
推荐软件栈 ' +
+ (DATA.tools || []).map(function (t) {
+ return '
' + esc(t.emoji) + '
' +
+ '
' + esc(t.name) + "
" +
+ '
' + esc(t.description) + "
" +
+ '
' + esc((t.tags || []).join(" · ")) + "
";
+ }).join("") +
+ "
"
+ );
+ }
+
+ function viewCalc() {
+ setChrome("月费用估算", "Cost calculator", true);
+ return (
+ '"
+ );
+ }
+
+ function viewFaq() {
+ setChrome("常见问题", "FAQ", true);
+ return (DATA.faqs || []).map(function (f) {
+ return '' + esc(f.question) + "
" +
+ esc(f.answer) + "
";
+ }).join("");
+ }
+
+ function viewPlan() {
+ setChrome("旅居计划", "Offline plan", true);
+ var checks = DATA.planChecklist || [];
+ var planCities = state.plan.map(function (slug) {
+ var d = destBySlug(slug);
+ if (!d) return "";
+ return destCard(d);
+ }).join("");
+ return (
+ '计划与清单仅保存在本机(小红书 Storage / localStorage)。
' +
+ '
计划中的城市 ' +
+ (planCities || '
还没有城市,去目的地页点「加入计划」
') +
+ "
" +
+ '
落地清单 ' +
+ checks.map(function (c) {
+ var on = !!state.checklist[c.id];
+ return '' +
+ '' + (on ? "✓" : "") + " " + esc(c.label) + " ";
+ }).join("") +
+ "
"
+ );
+ }
+
+ function viewMine() {
+ setChrome("我的", "Favorites & plan", false);
+ var favCards = state.favs.map(function (slug) {
+ var d = destBySlug(slug);
+ return d ? destCard(d) : "";
+ }).join("");
+ var recent = state.recent.map(function (slug) {
+ var d = destBySlug(slug);
+ return d ? destCard(d) : "";
+ }).join("");
+ return (
+ '' + esc(DATA.offlineNote || "") + "
" +
+ '' +
+ '🗓️ 旅居计划 清单与城市 ' +
+ '🎯 智能匹配 重算推荐 ' +
+ "
" +
+ '
我的收藏 ' +
+ (favCards || '
暂无收藏
') + "
" +
+ '
最近浏览 ' +
+ (recent || '
还没有浏览记录
') + "
" +
+ '
关于 ' +
+ '
品牌 nomadro — 数字游民旅居平台离线镜像。
' +
+ '
完整功能:' + esc(DATA.site || "https://nomadweb.nomadro.com") + "
"
+ );
+ }
+
+ function render() {
+ var route = currentRoute();
+ var html = "";
+ switch (route.view) {
+ case "home": html = viewHome(); break;
+ case "explore": html = viewExploreHub(); break;
+ case "connect": html = viewConnectHub(); break;
+ case "grow": html = viewGrowHub(); break;
+ case "mine": html = viewMine(); break;
+ case "destinations": html = viewDestinations(); break;
+ case "destination": html = viewDestination(route.params); break;
+ case "visas": html = viewVisas(); break;
+ case "matcher": html = viewMatcher(); break;
+ case "compare": html = viewCompare(); break;
+ case "meetups": html = viewMeetups(); break;
+ case "community": html = viewCommunity(); break;
+ case "discussion": html = viewDiscussion(route.params); break;
+ case "voices": html = viewVoices(); break;
+ case "digital": html = viewDigital(); break;
+ case "lesson": html = viewLesson(route.params); break;
+ case "blog": html = viewBlog(); break;
+ case "blog-detail": html = viewBlogDetail(route.params); break;
+ case "tools": html = viewTools(); break;
+ case "calc": html = viewCalc(); break;
+ case "faq": html = viewFaq(); break;
+ case "plan": html = viewPlan(); break;
+ default: html = viewHome();
+ }
+ main.innerHTML = html;
+ bindDynamic();
+ window.scrollTo(0, 0);
+ }
+
+ function bindDynamic() {
+ Array.prototype.forEach.call(main.querySelectorAll("[data-go]"), function (el) {
+ on(el, "click", function () { goTab(el.getAttribute("data-go")); });
+ });
+ Array.prototype.forEach.call(main.querySelectorAll("[data-open]"), function (el) {
+ on(el, "click", function () {
+ var view = el.getAttribute("data-open");
+ var params = {};
+ if (el.getAttribute("data-slug")) params.slug = el.getAttribute("data-slug");
+ if (el.getAttribute("data-id")) params.id = el.getAttribute("data-id");
+ if (el.getAttribute("data-key")) params.key = el.getAttribute("data-key");
+ openView(view, params);
+ });
+ });
+ Array.prototype.forEach.call(main.querySelectorAll("[data-region]"), function (el) {
+ on(el, "click", function () {
+ state.region = el.getAttribute("data-region");
+ render();
+ });
+ });
+ Array.prototype.forEach.call(main.querySelectorAll("[data-life]"), function (el) {
+ on(el, "click", function () {
+ state.lifeIdx = Number(el.getAttribute("data-life")) || 0;
+ render();
+ });
+ });
+ Array.prototype.forEach.call(main.querySelectorAll("[data-fav]"), function (el) {
+ on(el, "click", function () { toggleFav(el.getAttribute("data-fav")); });
+ });
+ Array.prototype.forEach.call(main.querySelectorAll("[data-plan-add]"), function (el) {
+ on(el, "click", function () {
+ var slug = el.getAttribute("data-plan-add");
+ if (state.plan.indexOf(slug) < 0) state.plan.push(slug);
+ storageSet(PLAN_KEY, state.plan);
+ openSheet("已加入旅居计划。 可在「我的 → 旅居计划」查看。
" +
+ '好的 ');
+ on(document.getElementById("sheet-ok"), "click", closeSheet);
+ });
+ });
+ Array.prototype.forEach.call(main.querySelectorAll("[data-check]"), function (el) {
+ on(el, "click", function () {
+ var id = el.getAttribute("data-check");
+ state.checklist[id] = !state.checklist[id];
+ storageSet(CHECK_KEY, state.checklist);
+ render();
+ });
+ });
+
+ var mRun = document.getElementById("m-run");
+ if (mRun) {
+ on(mRun, "click", function () {
+ var b = document.getElementById("m-budget");
+ var c = document.getElementById("m-climate");
+ var r = document.getElementById("m-region");
+ state.matchBudget = Number(b && b.value) || 6000;
+ state.matchClimate = (c && c.value) || "any";
+ state.matchRegion = (r && r.value) || "all";
+ render();
+ });
+ }
+
+ var cmpA = document.getElementById("cmp-a");
+ var cmpB = document.getElementById("cmp-b");
+ if (cmpA) on(cmpA, "change", function () { state.compareA = cmpA.value; render(); });
+ if (cmpB) on(cmpB, "change", function () { state.compareB = cmpB.value; render(); });
+
+ var cRun = document.getElementById("c-run");
+ if (cRun) {
+ on(cRun, "click", function () {
+ function n(id) { var el = document.getElementById(id); return Number(el && el.value) || 0; }
+ var total = n("c-house") + n("c-food") + n("c-trans") + n("c-cowork") + n("c-other");
+ var out = document.getElementById("c-out");
+ if (out) out.textContent = "预估月开销约 ¥" + total + "(离线估算,仅供参考)";
+ });
+ }
+ }
+
+ function bindChrome() {
+ Array.prototype.forEach.call(tabbar.querySelectorAll(".tab"), function (btn) {
+ on(btn, "click", function () { goTab(btn.getAttribute("data-tab")); });
+ });
+ on(btnBack, "click", goBack);
+ on(document.getElementById("btn-offline"), "click", function () {
+ openSheet(
+ "离线说明 " +
+ "" + esc(DATA.offlineNote || "") + "
" +
+ '知道了 '
+ );
+ on(document.getElementById("sheet-ok"), "click", closeSheet);
+ });
+ on(document.getElementById("sheet-mask"), "click", closeSheet);
+ }
+
+ function boot() {
+ bindChrome();
+ Promise.all([
+ storageGet(FAV_KEY, []),
+ storageGet(PLAN_KEY, []),
+ storageGet(CHECK_KEY, {}),
+ storageGet(RECENT_KEY, []),
+ ]).then(function (vals) {
+ state.favs = Array.isArray(vals[0]) ? vals[0] : [];
+ state.plan = Array.isArray(vals[1]) ? vals[1] : [];
+ state.checklist = vals[2] && typeof vals[2] === "object" ? vals[2] : {};
+ state.recent = Array.isArray(vals[3]) ? vals[3] : [];
+ render();
});
}
- window.NomadroXhs.getLocalData(favKey).then(function (favs) {
- render(Array.isArray(favs) ? favs : []);
- });
+ boot();
})();
diff --git a/redmini/release/data.js b/redmini/release/data.js
index 173b82e..1d0587b 100644
--- a/redmini/release/data.js
+++ b/redmini/release/data.js
@@ -1,19 +1 @@
-window.NOMADRO_DATA = {
- brand: 'nomadro',
- tagline: '用一行代码环游世界',
- note: '小红书小程序无法联网,本包为纯前端离线资料',
- destinations: [
- { slug: 'chiang-mai', name: '清迈', country: '泰国', cost: 4500, speed: 80, rating: 4.7, tagline: '咖啡与 coworking' },
- { slug: 'bali', name: '巴厘岛', country: '印尼', cost: 6000, speed: 60, rating: 4.5, tagline: '海边远程' },
- { slug: 'lisbon', name: '里斯本', country: '葡萄牙', cost: 12000, speed: 120, rating: 4.6, tagline: '欧洲数字游民枢纽' },
- { slug: 'da-nang', name: '岘港', country: '越南', cost: 5000, speed: 90, rating: 4.4, tagline: '性价比海岸' },
- { slug: 'mexico-city', name: '墨西哥城', country: '墨西哥', cost: 9000, speed: 70, rating: 4.3, tagline: '美食与时区友好' },
- { slug: 'bangkok', name: '曼谷', country: '泰国', cost: 5500, speed: 100, rating: 4.5, tagline: '交通便利大城' }
- ],
- tips: [
- '先定预算与签证天数,再选城市。',
- '到站第一周优先搞定住宿、SIM 与 coworking。',
- '同城活动是认识同行最快的方式。',
- '完整功能请打开 nomadweb.nomadro.com(本包离线不可请求网络)。'
- ]
-};
+window.NOMADRO_DATA = {"brand": "nomadro", "tagline": "用一行代码环游世界", "offlineNote": "小红书小程序无法联网。本包为官网内容离线镜像,收藏/计划仅保存在本机。完整互动请打开 nomadweb.nomadro.com", "site": "https://nomadweb.nomadro.com", "stats": {"destinations": 12, "visas": 12, "meetups": 12, "blog": 10, "faqs": 6}, "ticker": ["🌏 全球数字游民正在路上", "📋 签证政策实时整理(离线快照)", "🎉 同城活动与社区讨论可先浏览", "🎓 游民学院免费课时可学"], "rings": {"explore": [{"id": "destinations", "emoji": "🌴", "title": "目的地", "desc": "发现下一座城市"}, {"id": "visas", "emoji": "📋", "title": "签证指南", "desc": "远程友好签证"}, {"id": "matcher", "emoji": "🎯", "title": "智能匹配", "desc": "按预算气候排序"}, {"id": "compare", "emoji": "⚖️", "title": "城市对比", "desc": "并排看成本网速"}], "connect": [{"id": "meetups", "emoji": "🎉", "title": "游民活动", "desc": "同城与线上"}, {"id": "community", "emoji": "💬", "title": "社区讨论", "desc": "签证住宿经验"}, {"id": "voices", "emoji": "🗣️", "title": "游民心声", "desc": "真实旅居评价"}], "grow": [{"id": "digital", "emoji": "🎓", "title": "游民学院", "desc": "远程工作课程"}, {"id": "blog", "emoji": "📝", "title": "博客", "desc": "指南与攻略"}, {"id": "tools", "emoji": "🧰", "title": "工具箱", "desc": "费用估算等"}, {"id": "faq", "emoji": "❓", "title": "常见问题", "desc": "启动资金与保险"}]}, "lifestyle": [{"key": "morning", "emoji": "🌅", "title": "清晨", "body": "咖啡馆开机,时区对齐,列出今日三件交付。"}, {"key": "work", "emoji": "💻", "title": "深度工作", "body": "联合办公或家中安静角落,异步协作优先。"}, {"key": "explore", "emoji": "🚶", "title": "探索城市", "body": "傍晚散步、夜市与同城 meetup。"}, {"key": "collab", "emoji": "🤝", "title": "协作", "body": "和同行交换签证与住宿情报。"}, {"key": "social", "emoji": "🌙", "title": "社交", "body": "社区圆桌、兴趣局,或安静充电。"}], "planChecklist": [{"id": "visa", "label": "确认签证/停留天数"}, {"id": "flight", "label": "预订机票"}, {"id": "housing", "label": "首周短租/酒店"}, {"id": "sim", "label": "当地 SIM / eSIM"}, {"id": "cowork", "label": "踩点联合办公"}, {"id": "insurance", "label": "国际医疗保险"}, {"id": "budget", "label": "写下月预算"}, {"id": "meetup", "label": "报名一场同城活动"}], "destinations": [{"id": "1", "slug": "bali", "name": "巴厘岛", "country": "印尼", "emoji": "🏝️", "tag": "东南亚 · 热带天堂", "description": "乌布的数字游民社区闻名全球,稻田间的 Co-working Space 和瑜伽文化让这里成为游民圣地。", "region": "sea", "cost": 4500, "speed": 85, "temperature": 28, "rating": 9.2, "hue": 170, "nomads_count": "12,000+", "highlights": ["🏄 冲浪与海滩生活", "🧘 瑜伽冥想文化", "💰 东南亚性价比之王", "🌴 热带气候全年温暖"], "map_x": 720, "map_y": 310}, {"id": "2", "slug": "lisbon", "name": "里斯本", "country": "葡萄牙", "emoji": "🌊", "tag": "欧洲 · 海滨明珠", "description": "D7 签证友好,阳光海岸与悠久历史的完美融合,欧洲数字游民的首选基地。", "region": "europe", "cost": 9000, "speed": 120, "temperature": 22, "rating": 9.5, "hue": 220, "nomads_count": "8,500+", "highlights": ["📋 D7 签证门槛低", "☀️ 300天阳光", "🎵 Fado 音乐文化", "🚋 复古有轨电车"], "map_x": 430, "map_y": 195}, {"id": "3", "slug": "chiangmai", "name": "清迈", "country": "泰国", "emoji": "🏔️", "tag": "东南亚 · 文化古城", "description": "数字游民大本营,咖啡文化与夜市生活的天堂,全球性价比最高的游民城市。", "region": "sea", "cost": 3800, "speed": 95, "temperature": 30, "rating": 9.4, "hue": 45, "nomads_count": "15,000+", "highlights": ["☕ 咖啡馆文化浓厚", "🏮 夜市与寺庙", "💰 月生活费最低", "🤝 游民社区最活跃"], "map_x": 700, "map_y": 240}, {"id": "4", "slug": "mexico", "name": "墨西哥城", "country": "墨西哥", "emoji": "🌃", "tag": "拉美 · 活力之都", "description": "艺术、美食与科技交织,时区便利对接北美市场,拉美最具活力的游民城市。", "region": "latam", "cost": 6500, "speed": 75, "temperature": 18, "rating": 8.8, "hue": 300, "nomads_count": "5,200+", "highlights": ["🎨 街头艺术天堂", "🌮 世界美食之都", "🕐 北美时区友好", "💃 丰富夜生活"], "map_x": 220, "map_y": 240}, {"id": "5", "slug": "barcelona", "name": "巴塞罗那", "country": "西班牙", "emoji": "🏖️", "tag": "欧洲 · 地中海", "description": "高迪建筑与创业生态并存,Nomad Visa 政策领先,地中海生活的理想之选。", "region": "europe", "cost": 10500, "speed": 150, "temperature": 20, "rating": 9.1, "hue": 130, "nomads_count": "6,800+", "highlights": ["🏛️ 高迪建筑奇迹", "🏖️ 地中海海滩", "📋 Nomad Visa 便利", "🍷 美食与夜生活"], "map_x": 460, "map_y": 200}, {"id": "6", "slug": "tokyo", "name": "东京", "country": "日本", "emoji": "🗼", "tag": "亚洲 · 现代都市", "description": "极致效率与安全,适合追求高品质生活的远程工作者,亚洲科技之都。", "region": "asia", "cost": 12000, "speed": 200, "temperature": 15, "rating": 8.6, "hue": 10, "nomads_count": "4,100+", "highlights": ["🚄 极致公共交通", "🛡️ 全球最安全城市", "📶 网速亚洲第一", "🍣 美食文化巅峰"], "map_x": 820, "map_y": 210}, {"id": "7", "slug": "berlin", "name": "柏林", "country": "德国", "emoji": "🎨", "tag": "欧洲 · 创意之都", "description": "开放的创业与开源氛围,租金相对西欧友好,夜生活与艺术场景极具吸引力。", "region": "europe", "cost": 9800, "speed": 140, "temperature": 12, "rating": 8.9, "hue": 250, "nomads_count": "7,200+", "highlights": ["🎨 艺术与街头文化", "💻 创业生态活跃", "🚲 骑行友好", "🍺 社交场景丰富"], "map_x": 500, "map_y": 160}, {"id": "8", "slug": "dali", "name": "大理", "country": "中国", "emoji": "🏔️", "tag": "国内 · 慢生活", "description": "苍山洱海之间的内容创作者聚集地,生活节奏慢、社交松弛,适合深度创作与试住。", "region": "asia", "cost": 4200, "speed": 80, "temperature": 18, "rating": 8.7, "hue": 190, "nomads_count": "3,600+", "highlights": ["📷 内容创作友好", "🏞️ 自然风光", "🍵 咖啡馆密集", "🏡 合租成本低"], "map_x": 730, "map_y": 220}, {"id": "9", "slug": "seoul", "name": "首尔", "country": "韩国", "emoji": "🏙️", "tag": "亚洲 · 潮流都市", "description": "高速网络与深夜城市活力并存,适合对接东亚市场与产品迭代。", "region": "asia", "cost": 11000, "speed": 220, "temperature": 14, "rating": 8.5, "hue": 330, "nomads_count": "3,200+", "highlights": ["📶 全球顶尖网速", "🍜 美食与便利店文化", "🛍️ 潮流消费", "🚇 地铁覆盖极强"], "map_x": 800, "map_y": 200}, {"id": "10", "slug": "medellin", "name": "麦德林", "country": "哥伦比亚", "emoji": "🌺", "tag": "拉美 · 春城", "description": "四季如春、生活成本友好,英语游民社区成熟,适合北美时区远程工作。", "region": "latam", "cost": 5200, "speed": 90, "temperature": 23, "rating": 9.0, "hue": 140, "nomads_count": "6,400+", "highlights": ["🌤️ 四季如春", "💰 高性价比", "🕐 北美时区", "🤝 社区活动密集"], "map_x": 260, "map_y": 300}, {"id": "11", "slug": "dubai", "name": "迪拜", "country": "阿联酋", "emoji": "🏙️", "tag": "中东 · 枢纽都市", "description": "免税与签证便利吸引远程高收入人群,国际航班枢纽,适合中东/欧亚跳板。", "region": "asia", "cost": 14000, "speed": 180, "temperature": 32, "rating": 8.4, "hue": 40, "nomads_count": "4,800+", "highlights": ["🧾 税务友好", "✈️ 航线枢纽", "🏗️ 现代基础设施", "🌐 多元国际社区"], "map_x": 600, "map_y": 240}, {"id": "12", "slug": "tbilisi", "name": "第比利斯", "country": "格鲁吉亚", "emoji": "🍷", "tag": "欧亚 · 税务友好", "description": "1% 小企业税与远程友好签证,旧城氛围浓厚,东欧游民新基地。", "region": "europe", "cost": 4800, "speed": 70, "temperature": 16, "rating": 8.8, "hue": 15, "nomads_count": "5,100+", "highlights": ["🧾 1% 小企业税", "🏰 旧城与温泉", "🍷 葡萄酒文化", "💰 低生活成本"], "map_x": 560, "map_y": 190}], "visas": [{"id": "1", "country": "葡萄牙", "flag": "🇵🇹", "name": "葡萄牙 D7 签证", "badge": "⭐ 推荐", "badge_type": "easy", "duration": "2年,可续签", "income_req": "€760/月", "approval_time": "3-6 个月", "extra": "🏥 可享欧盟医疗", "difficulty": 35, "difficulty_label": "简单"}, {"id": "2", "country": "西班牙", "flag": "🇪🇸", "name": "西班牙 Nomad Visa", "badge": "🔥 热门", "badge_type": "hot", "duration": "1年,可续3年", "income_req": "€2,160/月", "approval_time": "1-3 个月", "extra": "🌍 可申根区旅行", "difficulty": 45, "difficulty_label": "中等"}, {"id": "3", "country": "印尼", "flag": "🇮🇩", "name": "印尼 B211A 签证", "badge": "💰 低成本", "badge_type": "budget", "duration": "60天,可延期", "income_req": "约 ¥2,000", "approval_time": "5-10 天", "extra": "🏝️ 适合巴厘岛旅居", "difficulty": 25, "difficulty_label": "简单"}, {"id": "4", "country": "泰国", "flag": "🇹🇭", "name": "泰国 LTR 签证", "badge": "🆕 新政策", "badge_type": "new", "duration": "10年", "income_req": "$80,000/年", "approval_time": "1-2 个月", "extra": "✈️ 多次入境", "difficulty": 60, "difficulty_label": "中等"}, {"id": "5", "country": "墨西哥", "flag": "🇲🇽", "name": "墨西哥 Temporary Resident", "badge": "💰 低成本", "badge_type": "budget", "duration": "1-4年", "income_req": "$2,500/月", "approval_time": "2-4 周", "extra": "🌮 北美时区友好", "difficulty": 30, "difficulty_label": "简单"}, {"id": "6", "country": "爱沙尼亚", "flag": "🇪🇪", "name": "爱沙尼亚 DNV", "badge": "🚀 先锋", "badge_type": "pioneer", "duration": "1年", "income_req": "€3,504/月", "approval_time": "2-4 周", "extra": "💻 全球首个数字游民签证", "difficulty": 40, "difficulty_label": "中等"}, {"id": "7", "country": "格鲁吉亚", "flag": "🇬🇪", "name": "格鲁吉亚 Remotely from Georgia", "badge": "⭐ 推荐", "badge_type": "easy", "duration": "1年", "income_req": "$2,000/月", "approval_time": "约 10 天", "extra": "🧾 可搭配小企业 1% 税", "difficulty": 28, "difficulty_label": "简单"}, {"id": "8", "country": "阿联酋", "flag": "🇦🇪", "name": "迪拜 Virtual Working Programme", "badge": "🔥 热门", "badge_type": "hot", "duration": "1年", "income_req": "$3,500/月", "approval_time": "2-4 周", "extra": "✈️ 中东航线枢纽", "difficulty": 42, "difficulty_label": "中等"}, {"id": "9", "country": "哥伦比亚", "flag": "🇨🇴", "name": "哥伦比亚 V Visa (Digital Nomad)", "badge": "💰 低成本", "badge_type": "budget", "duration": "最长 2 年", "income_req": "约 3× 最低工资", "approval_time": "2-6 周", "extra": "🌺 适合麦德林春城", "difficulty": 32, "difficulty_label": "简单"}, {"id": "10", "country": "韩国", "flag": "🇰🇷", "name": "韩国 Workation Visa (C-3-8)", "badge": "🆕 新政策", "badge_type": "new", "duration": "最长 2 年", "income_req": "约 $65,000/年", "approval_time": "1-2 个月", "extra": "🏙️ 对接首尔与济州", "difficulty": 55, "difficulty_label": "中等"}, {"id": "11", "country": "德国", "flag": "🇩🇪", "name": "德国 Freelance / Self-employment", "badge": "🚀 先锋", "badge_type": "pioneer", "duration": "1-3年", "income_req": "需商业计划与客户证明", "approval_time": "2-4 个月", "extra": "🎨 适合柏林自由职业", "difficulty": 65, "difficulty_label": "较难"}, {"id": "12", "country": "泰国", "flag": "🇹🇭", "name": "泰国 DTV 签证", "badge": "⭐ 推荐", "badge_type": "easy", "duration": "最长 5 年(多次入境)", "income_req": "约 50 万泰铢存款证明", "approval_time": "2-6 周", "extra": "🤝 远程工作友好", "difficulty": 38, "difficulty_label": "中等"}], "faqs": [{"id": "1", "question": "💰 做数字游民需要多少启动资金?", "order": 1, "answer": "建议准备 3-6 个月的生活费作为缓冲。以东南亚为例,¥15,000-30,000 即可开始。包括机票、首月住宿、签证费用和应急资金。欧洲目的地建议准备 ¥50,000 以上。"}, {"id": "2", "question": "📶 如何确保远程工作的网络稳定?", "order": 2, "answer": "选择网络评分高的城市,入住前用 Speedtest 测试。备用方案:本地 SIM 卡热点、随身 WiFi 设备、附近 Co-working Space。推荐携带 USB 网卡和 VPN 作为双保险。"}, {"id": "3", "question": "🏥 旅居期间的保险怎么办?", "order": 3, "answer": "推荐 SafetyWing 或 World Nomads 等国际医疗保险,月费约 $40-80,覆盖全球(部分国家除外)。长期旅居者可考虑目的地国家的本地保险,费用更低、报销更方便。"}, {"id": "4", "question": "🧾 税务问题如何处理?", "order": 4, "answer": "税务居民身份取决于居住天数(通常 183 天规则)。建议咨询专业税务顾问,了解双重征税协定。很多游民选择税务友好的国家(如葡萄牙、格鲁吉亚)作为基地。"}, {"id": "5", "question": "👨👩👧 可以带娃一起做数字游民吗?", "order": 5, "answer": "完全可以!巴厘岛、清迈、里斯本都有成熟的数字游民家庭社区。关键是选择教育资源丰富、医疗条件好的目的地,以及保持稳定的工作节奏,给孩子规律的生活。"}, {"id": "6", "question": "🤝 如何快速融入当地游民社区?", "order": 6, "answer": "加入 Nomad List、Facebook 群组和本地 Meetup 活动。入住游民友好的 Co-living 空间,参加每周的 Coworking 社交日。大部分游民社区非常开放,主动打招呼就能结识朋友。"}], "testimonials": [{"id": "1", "avatar": "👩💻", "content": "在清迈住了 8 个月,月花费不到 4000 元,但生活质量比国内一线城市高太多了。每天早上骑摩托去咖啡馆,这种感觉无法形容。", "author": "小林", "role": "前端开发 · 清迈 🇹🇭", "rating": 5}, {"id": "2", "avatar": "👨🎨", "content": "里斯本的 D7 签证让我在欧洲有了基地。白天在 Alfama 区的共享办公空间工作,周末去 Sintra 徒步,完美平衡。", "author": "Marco", "role": "UI 设计师 · 里斯本 🇵🇹", "rating": 5}, {"id": "3", "avatar": "🧑💼", "content": "带着家人做数字游民听起来疯狂,但在巴厘岛乌布,孩子们上国际学校,我和妻子远程工作,这是我们做过最正确的决定。", "author": "张家", "role": "产品经理 · 巴厘岛 🇮🇩", "rating": 5}, {"id": "4", "avatar": "👩🎨", "content": "麦德林四季如春,英语社区很活跃。我用北美时区接单,下午还能去公园运动,效率反而更高。", "author": "Sofia", "role": "自由职业设计 · 麦德林 🇨🇴", "rating": 5}, {"id": "5", "avatar": "👨💻", "content": "第比利斯生活成本低,还能开本地公司走 1% 税。旧城咖啡馆写代码,周末去卡兹别吉徒步,节奏很舒服。", "author": "Devon", "role": "DevOps · 第比利斯 🇬🇪", "rating": 5}, {"id": "6", "avatar": "📷", "content": "大理适合慢创作。合租便宜、风景治愈,但要主动找同频伙伴,别只待在民宿里。", "author": "Lina", "role": "内容创作者 · 大理 🇨🇳", "rating": 4}, {"id": "7", "avatar": "🧑🚀", "content": "柏林创业氛围强,开源 meetup 特别多。冬天冷、德语门槛真实存在,但找到圈子后很值得。", "author": "Ken", "role": "数据分析 · 柏林 🇩🇪", "rating": 4}, {"id": "8", "avatar": "🏙️", "content": "迪拜当跳板很香:航班方便、签证路径清晰。夏天太热,建议把深度工作时间放在室内联合办公。", "author": "Omar", "role": "增长顾问 · 迪拜 🇦🇪", "rating": 4}], "tools": [{"id": "1", "emoji": "💼", "name": "远程协作", "description": "Slack · Notion · Figma · Zoom", "tags": ["团队", "设计", "沟通"], "category": "work"}, {"id": "2", "emoji": "✈️", "name": "旅行规划", "description": "Skyscanner · Nomad List · SafetyWing", "tags": ["机票", "签证", "保险"], "category": "travel"}, {"id": "3", "emoji": "💳", "name": "财务管理", "description": "Wise · Revolut · Xero · 多币种账户", "tags": ["汇款", "记账", "税务"], "category": "finance"}, {"id": "4", "emoji": "🤝", "name": "社群网络", "description": "Nomad List · Remote Year · 本地 Meetup", "tags": ["社交", "活动", "合租"], "category": "connect"}, {"id": "5", "emoji": "🏥", "name": "健康保障", "description": "SafetyWing · World Nomads · 运动 App", "tags": ["保险", "健身", "心理"], "category": "health"}, {"id": "6", "emoji": "📚", "name": "持续学习", "description": "Coursera · Duolingo · 当地语言班", "tags": ["技能", "语言", "文化"], "category": "learn"}], "blog": [{"id": "1", "slug": "chiangmai-guide-2026", "title": "清迈数字游民完全指南 2026", "excerpt": "从签证到住宿,从咖啡馆到 Co-working,一篇搞定清迈旅居。", "emoji": "🏔️", "author": "nomadro", "published_at": "2026-01-15", "read_time": 8, "tags": ["清迈", "指南", "东南亚"]}, {"id": "2", "slug": "portugal-d7-visa", "title": "葡萄牙 D7 签证申请全攻略", "excerpt": "手把手教你申请欧洲最受欢迎的远程工作签证。", "emoji": "🇵🇹", "author": "nomadro", "published_at": "2026-02-03", "read_time": 12, "tags": ["签证", "葡萄牙", "欧洲"]}, {"id": "3", "slug": "nomad-tax-basics", "title": "数字游民税务入门:你需要知道的 5 件事", "excerpt": "183 天规则、双重征税、税务居民身份——一文理清。", "emoji": "🧾", "author": "nomadro", "published_at": "2026-02-20", "read_time": 10, "tags": ["税务", "法律", "指南"]}, {"id": "4", "slug": "medellin-spring-city", "title": "麦德林春城试住 30 天笔记", "excerpt": "时区、安全区、联合办公与西语社区初体验。", "emoji": "🌺", "author": "Sofia", "published_at": "2026-03-05", "read_time": 9, "tags": ["麦德林", "拉美", "试住"]}, {"id": "5", "slug": "tbilisi-tax-base", "title": "第比利斯当税务基地靠谱吗?", "excerpt": "远程签证、小企业税与旧城生活成本实测。", "emoji": "🍷", "author": "Devon", "published_at": "2026-03-18", "read_time": 11, "tags": ["格鲁吉亚", "税务", "欧洲"]}, {"id": "6", "slug": "dali-slow-create", "title": "大理慢创作:合租、网速与社群", "excerpt": "国内游民城市怎么住得久又不散漫。", "emoji": "🏔️", "author": "Lina", "published_at": "2026-04-02", "read_time": 7, "tags": ["大理", "内容", "国内"]}, {"id": "7", "slug": "berlin-startup-nomad", "title": "柏林自由职业与开源社群指南", "excerpt": "签证路径、共享办公与每周活动清单。", "emoji": "🎨", "author": "Ken", "published_at": "2026-04-20", "read_time": 10, "tags": ["柏林", "创业", "开源"]}, {"id": "8", "slug": "dubai-hub-month", "title": "迪拜一个月跳板旅居怎么排期", "excerpt": "签证、炎热季节、航班中转与办公点选择。", "emoji": "🏙️", "author": "Omar", "published_at": "2026-05-01", "read_time": 8, "tags": ["迪拜", "中东", "枢纽"]}, {"id": "9", "slug": "seoul-workation", "title": "首尔 Workation:网速、便利与成本", "excerpt": "C-3-8 签证与江南区办公踩点。", "emoji": "🏙️", "author": "Yuki", "published_at": "2026-05-16", "read_time": 9, "tags": ["首尔", "韩国", "网速"]}, {"id": "10", "slug": "first-month-checklist", "title": "旅居第一月清单:从落地到稳定输出", "excerpt": "SIM、短租、联合办公、社群与预算五步走。", "emoji": "✅", "author": "nomadro", "published_at": "2026-06-01", "read_time": 6, "tags": ["清单", "新手", "落地"]}], "blogContent": {"chiangmai-guide-2026": "## 为什么选择清迈?\n\n清迈是公认的全球数字游民之都。低廉的生活成本、完善的 Co-working 生态、友善的本地人和丰富的文化活动,让它成为新手游民的最佳起点。\n\n## 签证方案\n\n- **旅游签**:落地签 15 天,或提前办旅游签 60 天\n- **DTV 签证**:2024 年推出的 Destination Thailand Visa,适合远程工作者\n- **学生签/精英签**:长期旅居的进阶选择\n\n## 住宿推荐\n\n| 区域 | 月租 | 特点 |\n|------|------|------|\n| Nimman | ¥2,500-4,000 | 咖啡馆、餐厅、年轻人多 |\n| Old City | ¥1,500-3,000 | 文化氛围浓,步行可达寺庙 |\n| Hang Dong | ¥2,000-3,500 | 安静,适合深度工作 |\n\n## 最佳 Co-working\n\n1. **Punspace** — Nimman 区经典,日票 ¥60\n2. **CAMP** — Maya 商场顶楼,免费(消费即可)\n3. **Hub53** — 安静专业,月票 ¥800\n\n## 月均预算\n\n- 住宿:¥2,500\n- 餐饮:¥1,200\n- 交通:¥300(租摩托)\n- Co-working:¥400\n- 其他:¥400\n- **合计:约 ¥3,800-4,500**\n\n## 实用 Tips\n\n☕ 推荐咖啡馆:Ristr8to(世界冠军)、Graph One Nimman\n🏍️ 租摩托月费约 ¥400,注意戴头盔\n📱 推荐 AIS 或 TrueMove 无限流量套餐\n🤝 每周四有 Nomad Meetup,关注 Facebook 群组", "portugal-d7-visa": "## D7 签证是什么?\n\n葡萄牙 D7 签证(Passive Income Visa)最初为退休者设计,但因收入要求低、审批相对简单,成为数字游民进入欧盟的最佳通道之一。\n\n## 申请条件\n\n- 月收入不低于 **€760**(葡萄牙最低工资)\n- 银行存款建议 **€9,120+**(12个月生活费)\n- 无犯罪记录\n- 葡萄牙本地银行账户\n- 健康保险\n\n## 申请流程\n\n1. **准备材料**(2-4 周)\n - 护照、照片、收入证明、银行流水\n - 葡萄牙税号(NIF)\n - 住宿证明(租房合同或酒店预订)\n\n2. **递交申请**\n - 在中国:葡萄牙驻华使馆\n - 或入境葡萄牙后转居留许可\n\n3. **等待审批**(3-6 个月)\n\n4. **登陆葡萄牙**\n - 领取居留卡\n - 登记住址\n\n## 费用预算\n\n- 签证费:约 €90\n- 律师费(可选):€500-1,500\n- NIF + 银行开户:€200-300\n- 首月生活费:€800-1,200\n\n## 里斯本生活成本\n\n- 一居室公寓:€800-1,200/月\n- Co-working:€150-250/月\n- 餐饮:€300-500/月\n- **月均总花费:€1,200-1,800**\n\n## 常见问题\n\n**Q: D7 可以工作吗?**\nA: 可以远程为海外雇主工作,但不能在葡萄牙本地公司就职。\n\n**Q: 5年后能拿护照吗?**\nA: 满足居住要求后可以申请永居或入籍。", "nomad-tax-basics": "## 1. 什么是税务居民?\n\n大多数国家用 **183 天规则** 判定税务居民:在一个自然年内居住超过 183 天,即成为该国税务居民,需在该国申报全球收入。\n\n## 2. 双重征税怎么办?\n\n如果两个国家都认为你是税务居民,查 **双边税收协定**(DTA)。中国已与 100+ 国家签署 DTA,可避免重复缴税。\n\n## 3. 数字游民常见税务策略\n\n| 策略 | 说明 | 适合人群 |\n|------|------|----------|\n| 零税务居民 | 不在任何国家住满 183 天 | 短期旅居者 |\n| 税务友好国 | 葡萄牙 NHR、格鲁吉亚 1% 税 | 长期旅居者 |\n| 原籍国申报 | 回国期间申报 | 兼职游民 |\n\n## 4. 中国税务居民注意\n\n- 中国公民默认是中国税务居民(全球征税)\n- 海外收入也需申报(可抵免境外已缴税)\n- 建议咨询专业税务师\n\n## 5. 实用建议\n\n1. 📋 记录每个国家的入境/出境日期\n2. 🧾 保留所有收入凭证和银行流水\n3. 🏦 使用 Wise 等工具便于跨境汇款记录\n4. 👨💼 收入超过一定金额建议聘请税务顾问\n5. 📱 推荐工具:Xero(记账)、TaxScouts(报税)", "medellin-spring-city": "## 为什么麦德林?\n\n四季如春、生活成本可控,且贴近北美时区,适合接美加客户。Laureles / El Poblado 是游民常见落脚区。\n\n## 30 天试住建议\n\n1. 前 7 天住短租,实测网速与噪音\n2. 办本地 SIM,备份热点\n3. 参加 2 场线下 meetup,验证社区匹配度\n4. 确定联合办公月票再签长租\n\n## 预算参考(月)\n\n- 住宿:¥2,000–3,500\n- 餐饮:¥1,200–1,800\n- 交通:¥300–500\n- 办公:¥400–800", "tbilisi-tax-base": "## 远程签证 + 小企业税\n\n格鲁吉亚对远程工作者友好,许多人会评估本地公司与 1% 税路径(需自行核实最新政策)。\n\n## 生活感受\n\n旧城与 Vake 区咖啡馆多,适合深度工作;冬季偏冷,夏天舒服。\n\n## 注意\n\n- 银行开户与本地手机号流程可能变动\n- 税务方案务必咨询持证顾问\n- 把「试住 30 天」和「税务落地」分开决策", "dali-slow-create": "## 适合谁\n\n内容创作者、独立开发者、想放慢节奏但仍保持产出的人。\n\n## 实操建议\n\n- 合租优先选稳定 WiFi 与独立工作桌\n- 用固定作息对抗「度假感」\n- 主动加入本地创作者局,避免社交孤岛\n\n## 成本\n\n月生活费常可控制在 ¥3,500–5,000,视合租与出行频率而定。", "berlin-startup-nomad": "## 柏林为什么吸引游民\n\n开源、设计、创业活动密度高,共享办公选择多。\n\n## 落地要点\n\n- 提前规划居留/自由职业路径\n- 冬天日照短,准备补光与室内运动\n- 德语不是必须,但能明显提升生活便利\n\n## 每周节奏\n\n2 天联合办公 + 1 场 meetup + 周末短途,比纯咖啡馆更稳。", "dubai-hub-month": "## 适合当跳板\n\n国际航班密集,签证路径相对清晰,适合中东/欧亚中转月。\n\n## 炎热季节\n\n夏季高温,深度工作尽量安排在空调联合办公;户外活动放早晚。\n\n## 预算\n\n整体偏高,建议按「枢纽月」而非「低成本旅居」来规划。", "seoul-workation": "## 网速与便利\n\n首尔网速与城市便利度极强,适合需要高频协作的产品/工程角色。\n\n## 成本与节奏\n\n住宿与餐饮成本接近一线城市;用便利店与共享办公控制变量。\n\n## 签证\n\n关注 Workation / 相关短期远程路径的最新材料要求。", "first-month-checklist": "## 落地五步\n\n1. **连接**:落地办 SIM,测公寓与办公点网速\n2. **住处**:先短租 7–14 天,再决定长租\n3. **办公**:固定 1 个主点 + 2 个备份咖啡馆\n4. **社群**:报名 1 场 meetup,加 1 个本地群\n5. **预算**:用第一周真实账单校准月预算\n\n## 输出稳定\n\n每天保护 4 小时深度工作,旅行探索放在下午后段。"}, "meetups": [{"id": "online-roundtable", "title": "远程工作节奏圆桌", "city": "线上", "destination_slug": "", "emoji": "🎙️", "date": "2026-09-12", "time": "20:00", "venue": "nomadro 线上活动厅", "description": "聊聊异步协作、时区管理和旅居城市选择。登录后可 RSVP,活动前会收到提醒。", "mode": "online", "access_level": "public", "rsvp_count": 38, "max_attendees": 80, "organizer": "nomadro 社区", "tags": ["远程", "协作", "新手友好"], "is_upcoming": true}, {"id": "chiangmai-cowork", "title": "清迈联合办公下午茶", "city": "清迈", "destination_slug": "chiangmai", "emoji": "☕", "date": "2026-09-18", "time": "15:00", "venue": "Nimman 区 Co-working", "description": "一起办公、分享签证经验和住宿踩坑。线下为主,也开放线上旁听链接。", "mode": "hybrid", "access_level": "public", "rsvp_count": 14, "max_attendees": 24, "organizer": "清迈游民小组", "tags": ["线下", "社交", "东南亚"], "is_upcoming": true}, {"id": "lisbon-sunset", "title": "里斯本日落漫步", "city": "里斯本", "destination_slug": "lisbon", "emoji": "🌅", "date": "2026-09-22", "time": "18:30", "venue": "Miradouro 观景点集合", "description": "工作一周后,和同城游民一起看日落、交换欧洲签证情报。", "mode": "offline", "access_level": "public", "rsvp_count": 9, "max_attendees": 16, "organizer": "Lisbon Nomads", "tags": ["欧洲", "户外", "社交"], "is_upcoming": true}, {"id": "bali-surf-morning", "title": "巴厘岛晨间冲浪 + 办公", "city": "巴厘岛", "destination_slug": "bali", "emoji": "🏄", "date": "2026-10-05", "time": "07:00", "venue": "Canggu Beach", "description": "早起冲浪,10 点回咖啡馆集中办公。适合想体验 Work-Life 平衡的游民。", "mode": "offline", "access_level": "public", "rsvp_count": 11, "max_attendees": 20, "organizer": "Bali Remote Crew", "tags": ["运动", "生活方式"], "is_upcoming": true}, {"id": "tokyo-tax-talk", "title": "日本税务与居留线上分享", "city": "线上", "destination_slug": "tokyo", "emoji": "🧾", "date": "2026-10-12", "time": "19:30", "venue": "线上直播", "description": "特邀长期旅居日本的游民分享税务申报、保险和银行开户经验(非法律建议)。", "mode": "online", "access_level": "members", "rsvp_count": 52, "max_attendees": 120, "organizer": "nomadro 社区", "tags": ["税务", "日本", "干货"], "is_upcoming": true}, {"id": "berlin-opensource", "title": "柏林开源与 DevOps 夜谈", "city": "柏林", "destination_slug": "berlin", "emoji": "🖥️", "date": "2026-09-28", "time": "19:00", "venue": "Kreuzberg Co-working", "description": "分享自托管、CI 与远程团队协作实践,会后可继续社交。", "mode": "hybrid", "access_level": "public", "rsvp_count": 22, "max_attendees": 40, "organizer": "Berlin Remote", "tags": ["开源", "技术", "欧洲"], "is_upcoming": true}, {"id": "dali-create-walk", "title": "大理洱海创作散步", "city": "大理", "destination_slug": "dali", "emoji": "📷", "date": "2026-10-02", "time": "16:00", "venue": "才村码头集合", "description": "边走边拍,分享内容选题与变现路径,适合创作者与慢旅居者。", "mode": "offline", "access_level": "public", "rsvp_count": 8, "max_attendees": 18, "organizer": "大理创作者局", "tags": ["内容", "户外", "国内"], "is_upcoming": true}, {"id": "medellin-spanish", "title": "麦德林西语咖啡局", "city": "麦德林", "destination_slug": "medellin", "emoji": "☕", "date": "2026-10-08", "time": "10:30", "venue": "Laureles 咖啡馆", "description": "初级西语练习 + 安全区与租房情报交换,新手友好。", "mode": "offline", "access_level": "public", "rsvp_count": 16, "max_attendees": 20, "organizer": "Medellín Nomads", "tags": ["语言", "拉美", "社交"], "is_upcoming": true}, {"id": "seoul-night-office", "title": "首尔深夜联合办公", "city": "首尔", "destination_slug": "seoul", "emoji": "🌃", "date": "2026-10-15", "time": "21:00", "venue": "江南共享办公", "description": "适合东亚时区冲刺交付的游民,自带电脑即可。", "mode": "offline", "access_level": "public", "rsvp_count": 12, "max_attendees": 25, "organizer": "Seoul Workation", "tags": ["办公", "韩国", "效率"], "is_upcoming": true}, {"id": "dubai-hub-brunch", "title": "迪拜枢纽早午餐", "city": "迪拜", "destination_slug": "dubai", "emoji": "🥂", "date": "2026-10-18", "time": "11:00", "venue": "Downtown 咖啡厅", "description": "中转游民见面:签证、航班与下一站城市选择。", "mode": "offline", "access_level": "public", "rsvp_count": 10, "max_attendees": 16, "organizer": "Dubai Hub Crew", "tags": ["中东", "社交", "跳板"], "is_upcoming": true}, {"id": "tbilisi-tax-cafe", "title": "第比利斯税务咖啡聊", "city": "第比利斯", "destination_slug": "tbilisi", "emoji": "🍷", "date": "2026-10-22", "time": "15:00", "venue": "Vake 区咖啡馆", "description": "交流远程签证与本地公司实务(非正式法律意见),会后可徒步旧城。", "mode": "offline", "access_level": "members", "rsvp_count": 19, "max_attendees": 22, "organizer": "Tbilisi Base", "tags": ["税务", "欧洲", "干货"], "is_upcoming": true}, {"id": "mexico-food-walk", "title": "墨西哥城街头美食漫步", "city": "墨西哥城", "destination_slug": "mexico", "emoji": "🌮", "date": "2026-10-26", "time": "17:30", "venue": "Roma Norte 集合", "description": "边吃边聊北美时区接单与安全出行经验。", "mode": "offline", "access_level": "public", "rsvp_count": 13, "max_attendees": 18, "organizer": "CDMX Nomads", "tags": ["美食", "拉美", "社交"], "is_upcoming": true}], "discussions": [{"id": "visa-sea-2026", "title": "2026 东南亚长期停留签证怎么选?", "excerpt": "泰国 DTV、马来西亚 DE Rantau、印尼第二家园……大家最近实际办下来体验如何?", "author": "小林", "author_emoji": "🧳", "category": "签证", "reply_count": 23, "like_count": 47, "is_pinned": true, "created_at": "2026-08-20", "tags": ["签证", "东南亚"]}, {"id": "async-remote", "title": "异步团队怎么写日报才不烦人?", "excerpt": "我们团队跨 5 个时区,想收集团队节奏模板和工具推荐。", "author": "Marco", "author_emoji": "💻", "category": "远程工作", "reply_count": 15, "like_count": 31, "is_pinned": false, "created_at": "2026-08-22", "tags": ["远程", "协作"]}, {"id": "coliving-tips", "title": "第一次租 Coliving 必问房东的 10 个问题", "excerpt": "押金、网速、清洁、访客政策……欢迎补充你的踩坑经历。", "author": "阿静", "author_emoji": "🏡", "category": "住宿", "reply_count": 19, "like_count": 56, "is_pinned": false, "created_at": "2026-08-25", "tags": ["住宿", "经验"]}, {"id": "health-insurance", "title": "数字游民国际医疗保险对比", "excerpt": "SafetyWing、Genki、本地险……按年龄段和目的地聊聊性价比。", "author": "Yuki", "author_emoji": "🏥", "category": "安全", "reply_count": 11, "like_count": 28, "is_pinned": false, "created_at": "2026-08-27", "tags": ["保险", "安全"]}, {"id": "meetup-feedback", "title": "你希望 nomadro 线下活动开在哪些城市?", "excerpt": "我们在规划 Q4 活动路线,投票 + 留言告诉我们你的城市!", "author": "nomadro", "author_emoji": "🌍", "category": "社区", "reply_count": 34, "like_count": 72, "is_pinned": true, "created_at": "2026-08-28", "tags": ["活动", "投票"]}, {"id": "medellin-safety", "title": "麦德林哪些街区适合第一次长住?", "excerpt": "Laureles / Poblado / Envigado 的网速、噪音和夜间出行体验求分享。", "author": "Sofia", "author_emoji": "🌺", "category": "住宿", "reply_count": 17, "like_count": 41, "created_at": "2026-08-30", "tags": ["麦德林", "安全"]}, {"id": "tbilisi-bank", "title": "第比利斯开户与收美元的实操经验", "excerpt": "需要本地号码吗?Wise 能不能直接用?求最新流程。", "author": "Devon", "author_emoji": "🍷", "category": "财务", "reply_count": 14, "like_count": 36, "created_at": "2026-09-01", "tags": ["格鲁吉亚", "银行"]}, {"id": "berlin-winter", "title": "柏林冬天怎么保持产出和社交?", "excerpt": "日照短、天气冷,有没有室内运动 + meetup 推荐。", "author": "Ken", "author_emoji": "🎨", "category": "生活方式", "reply_count": 9, "like_count": 22, "created_at": "2026-09-02", "tags": ["柏林", "健康"]}, {"id": "dali-wifi", "title": "大理合租找房:网速坑位合集", "excerpt": "哪些区域光纤稳定?有没有房东靠谱名单。", "author": "Lina", "author_emoji": "📷", "category": "住宿", "reply_count": 21, "like_count": 48, "created_at": "2026-09-03", "tags": ["大理", "网速"]}, {"id": "dubai-summer", "title": "迪拜夏天还能正常远程办公吗?", "excerpt": "空调联合办公、作息调整和预算增加幅度求真实数据。", "author": "Omar", "author_emoji": "🏙️", "category": "生活方式", "reply_count": 8, "like_count": 19, "created_at": "2026-09-04", "tags": ["迪拜", "气候"]}, {"id": "seoul-visa", "title": "韩国 Workation 签证材料清单核对", "excerpt": "收入证明、保险、行程……有人最近过签吗?", "author": "Yuki", "author_emoji": "🇰🇷", "category": "签证", "reply_count": 12, "like_count": 33, "created_at": "2026-09-05", "tags": ["韩国", "签证"]}, {"id": "cowork-month-pass", "title": "各城市联合办公月票性价比对比", "excerpt": "清迈 / 里斯本 / 柏林 / 麦德林,欢迎贴价目与座位体验。", "author": "Alex", "author_emoji": "🚀", "category": "远程工作", "reply_count": 26, "like_count": 61, "created_at": "2026-09-06", "tags": ["办公", "成本"]}], "discussionDetails": {"visa-sea-2026": [{"id": "r1", "author": "阿Ken", "author_emoji": "🇹🇭", "content": "DTV 刚办下来,材料比想象中简单,关键是银行流水和远程工作证明。", "created_at": "2026-08-21", "like_count": 12}, {"id": "r2", "author": "Sara", "author_emoji": "🇲🇾", "content": "DE Rantau 审批大概 3 周,适合想待吉隆坡的。", "created_at": "2026-08-21", "like_count": 8}, {"id": "r3", "author": "Yuki", "author_emoji": "🇮🇩", "content": "巴厘岛短期用 B211A 足够,长期再看第二家园成本是否划算。", "created_at": "2026-08-22", "like_count": 6}], "async-remote": [{"id": "r1", "author": "Devon", "author_emoji": "🕐", "content": "我们用 Loom 录屏 + 简短文字摘要,比长日报友好很多。", "created_at": "2026-08-23", "like_count": 9}, {"id": "r2", "author": "Alex", "author_emoji": "🚀", "content": "模板三要素:昨天完成 / 今天计划 / 卡点,别写散文。", "created_at": "2026-08-23", "like_count": 11}], "coliving-tips": [{"id": "r1", "author": "小林", "author_emoji": "🧳", "content": "一定要问清押金退还条件和网速实测截图,别只看宣传。", "created_at": "2026-08-26", "like_count": 14}], "medellin-safety": [{"id": "r1", "author": "Alex", "author_emoji": "🌮", "content": "第一次建议 Laureles,生活成本与便利平衡;夜间少拿手机逛街。", "created_at": "2026-08-31", "like_count": 10}], "tbilisi-bank": [{"id": "r1", "author": "Omar", "author_emoji": "🏙️", "content": "先办本地 SIM 再去银行,材料准备护照+地址证明更顺利。", "created_at": "2026-09-01", "like_count": 7}], "dali-wifi": [{"id": "r1", "author": "阿静", "author_emoji": "🏡", "content": "才村/喜洲合租差别大,入住前用手机热点对照测速最靠谱。", "created_at": "2026-09-03", "like_count": 9}], "cowork-month-pass": [{"id": "r1", "author": "Marco", "author_emoji": "💻", "content": "里斯本月票大约 €150–250;清迈很多空间 ¥600–1000 就够用。", "created_at": "2026-09-06", "like_count": 15}], "health-insurance": [{"id": "r1", "author": "Sofia", "author_emoji": "🏥", "content": "30 岁以下 SafetyWing 够用;有慢性病建议加本地门诊险。", "created_at": "2026-08-28", "like_count": 10}, {"id": "r2", "author": "Ken", "author_emoji": "🇯🇵", "content": "在日本长期停留我叠了本地国民健康保险,报销更稳。", "created_at": "2026-08-28", "like_count": 7}], "meetup-feedback": [{"id": "r1", "author": "Lina", "author_emoji": "📷", "content": "大理 + 清迈希望能固定每月一场创作局。", "created_at": "2026-08-29", "like_count": 18}, {"id": "r2", "author": "Omar", "author_emoji": "🏙️", "content": "迪拜适合枢纽月见面,建议放周末早午餐。", "created_at": "2026-08-29", "like_count": 9}], "berlin-winter": [{"id": "r1", "author": "Devon", "author_emoji": "🖥️", "content": "冬天把联合办公当社交主场,再加一周一次室内攀岩/跑步局。", "created_at": "2026-09-02", "like_count": 11}], "dubai-summer": [{"id": "r1", "author": "Nina", "author_emoji": "🍷", "content": "夏天基本只在空调空间办公,预算里把交通和水电预留高一点。", "created_at": "2026-09-04", "like_count": 6}], "seoul-visa": [{"id": "r1", "author": "Jin", "author_emoji": "🇰🇷", "content": "最近材料重点是收入证明和保险;审批大约 3–6 周,建议提前办。", "created_at": "2026-09-05", "like_count": 8}]}, "course": [{"title": "启程:远程工作基础", "lessons": [{"title": "什么是数字游民", "duration": "8 分钟", "free": true}, {"title": "异步协作入门", "duration": "12 分钟", "free": true}, {"title": "时区与会议管理", "duration": "15 分钟", "free": false}]}, {"title": "签证与税务", "lessons": [{"title": "东南亚长期签证概览", "duration": "18 分钟", "free": false}, {"title": "183 天规则与税务居民", "duration": "14 分钟", "free": false}]}, {"title": "Homelab 与连接", "lessons": [{"title": "把家装进一个机柜", "duration": "20 分钟", "free": false}, {"title": "无论在哪都能连回家", "duration": "16 分钟", "free": false}]}], "lessons": {"0-0": {"title": "什么是数字游民", "moduleIndex": 0, "lessonIndex": 0, "duration": "8 分钟", "free": true, "content": "数字游民是一种工作方式:产出不绑定在某个地理坐标上。你可以在任何有网络的地方完成交付。"}, "0-1": {"title": "异步协作入门", "moduleIndex": 0, "lessonIndex": 1, "duration": "12 分钟", "free": true, "content": "异步沟通不是「随时在线」,而是「明确预期」:写清楚截止时间、交付物和决策人。"}, "0-2": {"title": "时区与会议管理", "moduleIndex": 0, "lessonIndex": 2, "duration": "15 分钟", "free": false, "content": "用 overlap 窗口安排会议,非重叠时段留给深度工作。工具箱里的「会议黄金时段」可以帮你算。"}, "1-0": {"title": "东南亚长期签证概览", "moduleIndex": 1, "lessonIndex": 0, "duration": "18 分钟", "free": false, "content": "泰国 DTV、马来西亚 DE Rantau、印尼第二家园……各国有不同的门槛与材料清单。"}, "1-1": {"title": "183 天规则与税务居民", "moduleIndex": 1, "lessonIndex": 1, "duration": "14 分钟", "free": false, "content": "多数国家用 183 天判定税务居民。用 nomadro 税居天数工具追踪停留。"}, "2-0": {"title": "把家装进一个机柜", "moduleIndex": 2, "lessonIndex": 0, "duration": "20 分钟", "free": false, "content": "Homelab 让你在旅途中拥有可控的 NAS、VPN 和开发环境。"}, "2-1": {"title": "无论在哪都能连回家", "moduleIndex": 2, "lessonIndex": 1, "duration": "16 分钟", "free": false, "content": "WireGuard + 动态 DNS,把家里的服务安全暴露给在外的你。"}}, "jobs": [{"id": "j1", "title": "Senior Frontend Engineer", "company": "Remote First Co", "location": "全球远程", "type": "全职", "salary": "$80k–120k", "tags": ["React", "TypeScript", "远程"], "url": "/gigs"}, {"id": "j2", "title": "DevOps / SRE", "company": "Nomad Labs", "location": "欧洲时区", "type": "合同", "salary": "€60–90/h", "tags": ["K8s", "AWS", "异步"], "url": "/gigs"}, {"id": "j3", "title": "内容运营(中文)", "company": "nomadro", "location": "东南亚友好", "type": "兼职", "salary": "面议", "tags": ["社区", "写作", "游民"], "url": "/gigs"}, {"id": "j4", "title": "Product Designer", "company": "Atlantic Remote", "location": "欧盟时区 ±3h", "type": "全职", "salary": "€55k–75k", "tags": ["Figma", "B2B", "远程"], "url": "/gigs"}, {"id": "j5", "title": "Growth Marketer", "company": "Hub Ventures", "location": "中东/欧洲友好", "type": "合同", "salary": "$4k–6k/月", "tags": ["SEO", "内容", "增长"], "url": "/gigs"}, {"id": "j6", "title": "Backend Engineer (Python)", "company": "Latam Cloud", "location": "北美时区", "type": "全职", "salary": "$90k–130k", "tags": ["Python", "FastAPI", "Postgres"], "url": "/gigs"}, {"id": "j7", "title": "Community Manager", "company": "nomadro", "location": "全球远程", "type": "兼职", "salary": "¥8k–12k/月", "tags": ["社区", "活动", "中英"], "url": "/gigs"}, {"id": "j8", "title": "Mobile Engineer (iOS/Android)", "company": "Seoul Soft", "location": "东亚时区", "type": "全职", "salary": "$70k–100k", "tags": ["Flutter", "移动端"], "url": "/gigs"}]};
diff --git a/redmini/release/index.html b/redmini/release/index.html
index aca5e88..a1d639d 100644
--- a/redmini/release/index.html
+++ b/redmini/release/index.html
@@ -3,11 +3,41 @@
- nomadro · 离线目的地
+ nomadro · 数字游民
-
+
+
+
+
+
+
+ 🏠 首页
+ 🧭 探索
+ 🤝 连接
+ 🌱 成长
+ 👤 我的
+
+
+
+
+
diff --git a/redmini/release/styles.css b/redmini/release/styles.css
index 466149d..63e3d4c 100644
--- a/redmini/release/styles.css
+++ b/redmini/release/styles.css
@@ -1,12 +1,277 @@
-*{box-sizing:border-box}html,body{margin:0;padding:0;background:#0c0b12;color:#f4f0ea;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
-#app{min-height:100vh;padding:28px 20px 48px}
-.brand{font-size:28px;font-weight:700;letter-spacing:1px}
-.tag{margin:8px 0 6px;color:#a8a3b3;font-size:14px}
-.note{color:#c9a227;font-size:12px;margin-bottom:20px}
-.card{background:rgba(255,255,255,.06);border-radius:14px;padding:14px;margin-bottom:12px}
-.name{font-size:17px;font-weight:600}
-.meta{margin-top:6px;color:#a8a3b3;font-size:13px}
-.tips{margin-top:24px}
-.tips h2{font-size:16px;margin:0 0 10px}
-.tips li{margin:8px 0;color:#cfc9d8;font-size:13px;line-height:1.5}
-.fav{margin-top:10px;font-size:12px;color:#f0c674}
+: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; }
diff --git a/redmini/release/xhs-bridge.js b/redmini/release/xhs-bridge.js
index b2673df..ee7811f 100644
--- a/redmini/release/xhs-bridge.js
+++ b/redmini/release/xhs-bridge.js
@@ -1,40 +1,639 @@
+/**
+ * Official XHS miniTool bridge helpers (Chrome 61 / ES2017).
+ * Prefer window.xhs.miniTool Storage (9.46+); fall back to localStorage.
+ */
(function (global) {
- 'use strict';
+ "use strict";
+
+ var STORAGE_MIN = 9460;
+ var COMMENT_MIN = 9490;
+ var FILE_MIN = 9490;
+
+ function readBuildVersion(launchOptions) {
+ var env = launchOptions && launchOptions.miniToolEnv;
+ return Number(env && env.buildVersion) || 0;
+ }
+
+ function getClientVersion(buildVersion) {
+ return Math.floor((Number(buildVersion) || 0) / 1000);
+ }
+
+ function isClientVersionAtLeast(buildVersion, minimum) {
+ return getClientVersion(buildVersion) >= minimum;
+ }
+
function getMiniTool() {
return global.xhs && global.xhs.miniTool ? global.xhs.miniTool : null;
}
- function setLocalData(key, data) {
- var serialized = JSON.stringify(data);
- var mt = getMiniTool();
- if (mt && typeof mt.setStorage === 'function') {
- return Promise.resolve(mt.setStorage({ key: key, data: serialized }))
- .then(function () { return true; })
- .catch(function () {
- try { localStorage.setItem(key, serialized); return true; } catch (e) { return false; }
- });
+
+ function getBuildVersion() {
+ var xhs = global.xhs;
+ var sync = readBuildVersion(xhs && xhs.launchOptions);
+ if (sync) {
+ return Promise.resolve(sync);
}
- try { localStorage.setItem(key, serialized); return Promise.resolve(true); } catch (e) {
+ var mt = getMiniTool();
+ if (!mt || typeof mt.getLaunchOptions !== "function") {
+ return Promise.resolve(0);
+ }
+ return Promise.resolve(mt.getLaunchOptions())
+ .then(function (opts) {
+ return readBuildVersion(opts);
+ })
+ .catch(function () {
+ return 0;
+ });
+ }
+
+ function setLocalData(key, data, encrypt) {
+ var serialized;
+ try {
+ serialized = JSON.stringify(data);
+ } catch (e) {
return Promise.resolve(false);
}
+ if (typeof serialized !== "string") return Promise.resolve(false);
+ var useEncrypt = !!encrypt;
+
+ return getBuildVersion().then(function (buildVersion) {
+ var mt = getMiniTool();
+ if (
+ isClientVersionAtLeast(buildVersion, STORAGE_MIN) &&
+ mt &&
+ typeof mt.setStorage === "function"
+ ) {
+ var payload = { key: key, data: serialized };
+ if (useEncrypt) payload.encrypt = true;
+ return Promise.resolve(mt.setStorage(payload))
+ .then(function () {
+ return true;
+ })
+ .catch(function () {
+ return writeBrowser(key, serialized);
+ });
+ }
+ return writeBrowser(key, serialized);
+ });
}
- function getLocalData(key) {
- var mt = getMiniTool();
- if (mt && typeof mt.getStorage === 'function') {
- return Promise.resolve(mt.getStorage({ key: key }))
- .then(function (res) {
- var raw = res && (res.data || res);
- if (typeof raw === 'string') {
- try { return JSON.parse(raw); } catch (e) { return null; }
- }
- return raw || null;
- })
- .catch(function () {
- try { return JSON.parse(localStorage.getItem(key) || 'null'); } catch (e) { return null; }
- });
+
+ function writeBrowser(key, serialized) {
+ try {
+ global.localStorage.setItem(key, serialized);
+ return true;
+ } catch (e) {
+ return false;
}
- try { return Promise.resolve(JSON.parse(localStorage.getItem(key) || 'null')); } catch (e) {
+ }
+
+ function getLocalData(key, fallback, encrypt) {
+ var useEncrypt = !!encrypt;
+ return getBuildVersion().then(function (buildVersion) {
+ var mt = getMiniTool();
+ if (
+ isClientVersionAtLeast(buildVersion, STORAGE_MIN) &&
+ mt &&
+ typeof mt.getStorage === "function"
+ ) {
+ var payload = { key: key };
+ if (useEncrypt) payload.encrypt = true;
+ return Promise.resolve(mt.getStorage(payload))
+ .then(function (res) {
+ var raw = res && res.data;
+ if (raw != null && raw !== "") {
+ try {
+ return JSON.parse(raw);
+ } catch (e) {
+ return fallback;
+ }
+ }
+ // Retry without encrypt (migration), then browser
+ if (useEncrypt) {
+ return Promise.resolve(mt.getStorage({ key: key }))
+ .then(function (res2) {
+ var raw2 = res2 && res2.data;
+ if (raw2 != null && raw2 !== "") {
+ try {
+ var parsed = JSON.parse(raw2);
+ return setLocalData(key, parsed, true).then(function () {
+ return parsed;
+ });
+ } catch (e2) {
+ return readBrowser(key, fallback);
+ }
+ }
+ return readBrowser(key, fallback);
+ })
+ .catch(function () {
+ return readBrowser(key, fallback);
+ });
+ }
+ var fromBrowser = readBrowser(key, null);
+ if (fromBrowser == null) return fallback;
+ return writeNative(mt, key, JSON.stringify(fromBrowser), false).then(
+ function () {
+ return fromBrowser;
+ },
+ function () {
+ return fromBrowser;
+ }
+ );
+ })
+ .catch(function () {
+ return readBrowser(key, fallback);
+ });
+ }
+ return readBrowser(key, fallback);
+ });
+ }
+
+ function writeNative(mt, key, serialized, encrypt) {
+ if (!mt || typeof mt.setStorage !== "function") {
+ return Promise.resolve(false);
+ }
+ var payload = { key: key, data: serialized };
+ if (encrypt) payload.encrypt = true;
+ return Promise.resolve(mt.setStorage(payload)).then(
+ function () {
+ return true;
+ },
+ function () {
+ return false;
+ }
+ );
+ }
+
+ function readBrowser(key, fallback) {
+ try {
+ var raw = global.localStorage.getItem(key);
+ if (raw == null || raw === "") return fallback;
+ return JSON.parse(raw);
+ } catch (e) {
+ return fallback;
+ }
+ }
+
+ function removeLocalData(key) {
+ return getBuildVersion().then(function (buildVersion) {
+ var mt = getMiniTool();
+ if (
+ isClientVersionAtLeast(buildVersion, STORAGE_MIN) &&
+ mt &&
+ typeof mt.removeStorage === "function"
+ ) {
+ return Promise.resolve(mt.removeStorage({ key: key }))
+ .then(function () {
+ removeBrowser(key);
+ return true;
+ })
+ .catch(function () {
+ return removeBrowser(key);
+ });
+ }
+ return removeBrowser(key);
+ });
+ }
+
+ function clearLocalData() {
+ return getBuildVersion().then(function (buildVersion) {
+ var mt = getMiniTool();
+ if (
+ isClientVersionAtLeast(buildVersion, STORAGE_MIN) &&
+ mt &&
+ typeof mt.clearStorage === "function"
+ ) {
+ return Promise.resolve(mt.clearStorage())
+ .then(function () {
+ clearBrowser();
+ return true;
+ })
+ .catch(function () {
+ return clearBrowser();
+ });
+ }
+ return clearBrowser();
+ });
+ }
+
+ function removeBrowser(key) {
+ try {
+ global.localStorage.removeItem(key);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ function clearBrowser() {
+ try {
+ global.localStorage.clear();
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ function getLaunchOptions() {
+ var xhs = global.xhs;
+ if (xhs && xhs.launchOptions) {
+ return Promise.resolve(xhs.launchOptions);
+ }
+ var mt = getMiniTool();
+ if (!mt || typeof mt.getLaunchOptions !== "function") {
return Promise.resolve(null);
}
+ return Promise.resolve(mt.getLaunchOptions()).catch(function () {
+ return null;
+ });
}
- global.NomadroXhs = { setLocalData: setLocalData, getLocalData: getLocalData };
+
+ /** Read miniToolSnapshotInfo from launch options (comment re-entry, §3.9). */
+ function readSnapshotInfo(launchOptions) {
+ if (!launchOptions) return null;
+ var raw =
+ launchOptions.miniToolSnapshotInfo ||
+ (launchOptions.query && launchOptions.query.miniToolSnapshotInfo) ||
+ (launchOptions.referrerInfo &&
+ launchOptions.referrerInfo.miniToolSnapshotInfo) ||
+ (launchOptions.extraData && launchOptions.extraData.miniToolSnapshotInfo);
+ if (!raw) return null;
+ if (typeof raw === "object") return raw;
+ try {
+ return JSON.parse(raw);
+ } catch (e) {
+ return null;
+ }
+ }
+
+ function callApi(name, options) {
+ var mt = getMiniTool();
+ if (!mt || typeof mt[name] !== "function") {
+ return Promise.reject({ errMsg: name + ":fail not available" });
+ }
+ return Promise.resolve(mt[name](options || {}));
+ }
+
+ function writeTempFile(dataUrl) {
+ return callApi("writeTempFile", { data: dataUrl }).then(function (res) {
+ return (res && res.filePath) || dataUrl;
+ });
+ }
+
+ function saveImageToPhotosAlbum(filePath) {
+ return callApi("saveImageToPhotosAlbum", { filePath: filePath });
+ }
+
+ function postNote(opts) {
+ return callApi("postNote", opts);
+ }
+
+ function canPostComment(buildVersion) {
+ var mt = getMiniTool();
+ return (
+ isClientVersionAtLeast(buildVersion, COMMENT_MIN) &&
+ mt &&
+ typeof mt.interactionOpenApi === "function"
+ );
+ }
+
+ function postComment(payload, saveToAlbum) {
+ return callApi("interactionOpenApi", {
+ payload: payload,
+ saveToAlbum: saveToAlbum !== false
+ });
+ }
+
+ function getStorageInfo() {
+ return getBuildVersion().then(function (buildVersion) {
+ var mt = getMiniTool();
+ if (
+ !isClientVersionAtLeast(buildVersion, STORAGE_MIN) ||
+ !mt ||
+ typeof mt.getStorageInfo !== "function"
+ ) {
+ return null;
+ }
+ return Promise.resolve(mt.getStorageInfo()).catch(function () {
+ return null;
+ });
+ });
+ }
+
+ function canUseFileSystem(buildVersion) {
+ var mt = getMiniTool();
+ return (
+ isClientVersionAtLeast(buildVersion, FILE_MIN) &&
+ mt &&
+ typeof mt.writeFile === "function" &&
+ typeof mt.readFile === "function"
+ );
+ }
+
+ function getUserDataPath() {
+ return getLaunchOptions().then(function (opts) {
+ var env = opts && opts.miniToolEnv;
+ return (env && env.userDataPath) || "";
+ });
+ }
+
+ function ensureDir(dirPath) {
+ var mt = getMiniTool();
+ if (!mt || typeof mt.mkdir !== "function") {
+ return Promise.resolve(false);
+ }
+ return Promise.resolve(mt.mkdir({ dirPath: dirPath, recursive: true }))
+ .then(function () {
+ return true;
+ })
+ .catch(function () {
+ return false;
+ });
+ }
+
+ function writeUtf8File(relativePath, data) {
+ return getBuildVersion().then(function (bv) {
+ if (!canUseFileSystem(bv)) {
+ return Promise.reject({ errMsg: "writeFile:fail version" });
+ }
+ return getUserDataPath().then(function (root) {
+ if (!root) return Promise.reject({ errMsg: "writeFile:fail no path" });
+ var rel = String(relativePath || "").replace(/^\/+/, "");
+ var parts = rel.split("/");
+ var dir =
+ parts.length > 1 ? root + "/" + parts.slice(0, -1).join("/") : root;
+ var filePath = root + "/" + rel;
+ return ensureDir(dir).then(function () {
+ return callApi("writeFile", {
+ filePath: filePath,
+ data: typeof data === "string" ? data : JSON.stringify(data),
+ encoding: "utf8"
+ }).then(function () {
+ return filePath;
+ });
+ });
+ });
+ });
+ }
+
+ function appendUtf8File(relativePath, data) {
+ return getBuildVersion().then(function (bv) {
+ if (!canUseFileSystem(bv)) {
+ return Promise.reject({ errMsg: "appendFile:fail version" });
+ }
+ var mt = getMiniTool();
+ if (!mt || typeof mt.appendFile !== "function") {
+ // Fallback: read + write
+ return readUtf8File(relativePath).then(function (prev) {
+ return writeUtf8File(
+ relativePath,
+ (prev || "") + (typeof data === "string" ? data : String(data))
+ );
+ });
+ }
+ return getUserDataPath().then(function (root) {
+ if (!root) return Promise.reject({ errMsg: "appendFile:fail no path" });
+ var rel = String(relativePath || "").replace(/^\/+/, "");
+ var parts = rel.split("/");
+ var dir =
+ parts.length > 1 ? root + "/" + parts.slice(0, -1).join("/") : root;
+ var filePath = root + "/" + rel;
+ return ensureDir(dir).then(function () {
+ return callApi("appendFile", {
+ filePath: filePath,
+ data: typeof data === "string" ? data : String(data),
+ encoding: "utf8"
+ }).then(function () {
+ return filePath;
+ });
+ });
+ });
+ });
+ }
+
+ function postVideoNote(opts) {
+ var videoRes = { video_url: opts.videoPath };
+ if (opts.coverPath) videoRes.cover_url = opts.coverPath;
+ return callApi("postNote", {
+ title: opts.title,
+ content: opts.content,
+ pageType: "video_publish",
+ mediaInfo: {
+ video_resources: videoRes
+ }
+ });
+ }
+
+ function readUtf8File(relativePath) {
+ return getBuildVersion().then(function (bv) {
+ if (!canUseFileSystem(bv)) {
+ return Promise.resolve(null);
+ }
+ return getUserDataPath().then(function (root) {
+ if (!root) return null;
+ var filePath = root + "/" + String(relativePath || "").replace(/^\/+/, "");
+ return callApi("readFile", { filePath: filePath, encoding: "utf8" })
+ .then(function (res) {
+ return res && res.data != null ? res.data : null;
+ })
+ .catch(function () {
+ return null;
+ });
+ });
+ });
+ }
+
+ /** Write dataURL to temp, then optionally persist via saveFile (9.49+). */
+ function persistImage(dataUrl, relativePath) {
+ return writeTempFile(dataUrl).then(function (tempPath) {
+ return getBuildVersion().then(function (bv) {
+ var mt = getMiniTool();
+ if (
+ !relativePath ||
+ !isClientVersionAtLeast(bv, FILE_MIN) ||
+ !mt ||
+ typeof mt.saveFile !== "function"
+ ) {
+ return tempPath;
+ }
+ return getUserDataPath().then(function (root) {
+ if (!root) return tempPath;
+ var dest = root + "/" + String(relativePath).replace(/^\/+/, "");
+ var dir = dest.replace(/\/[^/]+$/, "");
+ return ensureDir(dir).then(function () {
+ return callApi("saveFile", {
+ tempFilePath: tempPath,
+ filePath: dest
+ })
+ .then(function (res) {
+ return (res && res.savedFilePath) || dest || tempPath;
+ })
+ .catch(function () {
+ return tempPath;
+ });
+ });
+ });
+ });
+ });
+ }
+
+ function getFileStorageInfo() {
+ return getBuildVersion().then(function (bv) {
+ var mt = getMiniTool();
+ if (
+ !isClientVersionAtLeast(bv, FILE_MIN) ||
+ !mt ||
+ typeof mt.getFileStorageInfo !== "function"
+ ) {
+ return null;
+ }
+ return Promise.resolve(mt.getFileStorageInfo()).catch(function () {
+ return null;
+ });
+ });
+ }
+
+ function unlinkFile(relativeOrAbsPath) {
+ return getBuildVersion().then(function (bv) {
+ var mt = getMiniTool();
+ if (
+ !isClientVersionAtLeast(bv, FILE_MIN) ||
+ !mt ||
+ typeof mt.unlink !== "function"
+ ) {
+ return false;
+ }
+ var path = String(relativeOrAbsPath || "");
+ if (!path) return false;
+ var p = Promise.resolve(path);
+ if (path.indexOf("/") !== 0 && path.indexOf("usr") !== 0) {
+ p = getUserDataPath().then(function (root) {
+ return root ? root + "/" + path.replace(/^\/+/, "") : path;
+ });
+ }
+ return p.then(function (filePath) {
+ return callApi("unlink", { filePath: filePath })
+ .then(function () {
+ return true;
+ })
+ .catch(function () {
+ return false;
+ });
+ });
+ });
+ }
+
+ function readDirList(relativeDir) {
+ return getBuildVersion().then(function (bv) {
+ var mt = getMiniTool();
+ if (
+ !isClientVersionAtLeast(bv, FILE_MIN) ||
+ !mt ||
+ typeof mt.readDir !== "function"
+ ) {
+ return [];
+ }
+ return getUserDataPath().then(function (root) {
+ if (!root) return [];
+ var dirPath =
+ root +
+ (relativeDir
+ ? "/" + String(relativeDir).replace(/^\/+|\/+$/g, "")
+ : "");
+ return callApi("readDir", { dirPath: dirPath })
+ .then(function (res) {
+ var files = (res && res.files) || [];
+ return files.map(function (f) {
+ if (typeof f === "string") {
+ return { name: f, path: dirPath + "/" + f };
+ }
+ var name = f.name || f.fileName || String(f);
+ return {
+ name: name,
+ path: f.path || f.filePath || dirPath + "/" + name,
+ isDir: !!(f.isDir || f.isDirectory)
+ };
+ });
+ })
+ .catch(function () {
+ return [];
+ });
+ });
+ });
+ }
+
+ function cleanupShareDir() {
+ return readDirList("share").then(function (files) {
+ var chain = Promise.resolve(0);
+ for (var i = 0; i < files.length; i++) {
+ (function (file) {
+ if (file.isDir) return;
+ chain = chain.then(function (n) {
+ return callApi("unlink", { filePath: file.path })
+ .then(function () {
+ return n + 1;
+ })
+ .catch(function () {
+ return n;
+ });
+ });
+ })(files[i]);
+ }
+ return chain;
+ });
+ }
+
+ /** Convert File / Blob to data URL (offline, no network). */
+ function blobToDataUrl(blob) {
+ return new Promise(function (resolve, reject) {
+ if (!blob) {
+ reject({ errMsg: "blobToDataUrl:fail empty" });
+ return;
+ }
+ try {
+ var reader = new FileReader();
+ reader.onload = function () {
+ resolve(reader.result);
+ };
+ reader.onerror = function () {
+ reject({ errMsg: "blobToDataUrl:fail read" });
+ };
+ reader.readAsDataURL(blob);
+ } catch (e) {
+ reject({ errMsg: "blobToDataUrl:fail " + (e && e.message) });
+ }
+ });
+ }
+
+ global.XhsBridge = {
+ getBuildVersion: getBuildVersion,
+ getClientVersion: getClientVersion,
+ isClientVersionAtLeast: isClientVersionAtLeast,
+ setLocalData: setLocalData,
+ getLocalData: getLocalData,
+ removeLocalData: removeLocalData,
+ clearLocalData: clearLocalData,
+ getStorageInfo: getStorageInfo,
+ getFileStorageInfo: getFileStorageInfo,
+ getLaunchOptions: getLaunchOptions,
+ readSnapshotInfo: readSnapshotInfo,
+ writeTempFile: writeTempFile,
+ saveImageToPhotosAlbum: saveImageToPhotosAlbum,
+ postNote: postNote,
+ canPostComment: canPostComment,
+ postComment: postComment,
+ canUseFileSystem: canUseFileSystem,
+ getUserDataPath: getUserDataPath,
+ writeUtf8File: writeUtf8File,
+ appendUtf8File: appendUtf8File,
+ readUtf8File: readUtf8File,
+ persistImage: persistImage,
+ unlinkFile: unlinkFile,
+ readDirList: readDirList,
+ cleanupShareDir: cleanupShareDir,
+ blobToDataUrl: blobToDataUrl,
+ postVideoNote: postVideoNote,
+ getMiniTool: getMiniTool,
+ STORAGE_MIN: STORAGE_MIN,
+ COMMENT_MIN: COMMENT_MIN,
+ FILE_MIN: FILE_MIN
+ };
})(window);
diff --git a/redmini/scripts/build_data.py b/redmini/scripts/build_data.py
new file mode 100644
index 0000000..7f40da3
--- /dev/null
+++ b/redmini/scripts/build_data.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+"""Rebuild redmini/src/data.js from backend mock data (offline snapshot)."""
+from __future__ import annotations
+
+import json
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[2]
+BACKEND = ROOT / "backend"
+OUT = ROOT / "redmini" / "src" / "data.js"
+
+sys.path.insert(0, str(BACKEND))
+
+from app.data import community_data as c # noqa: E402
+from app.data import digital_content as d # noqa: E402
+from app.data import mock_data as m # noqa: E402
+
+
+def main() -> int:
+ meetups = [
+ {k: v for k, v in x.items() if k not in ("mirotalkRoom", "loungeChannel", "meetingUrl")}
+ for x in c.MEETUPS
+ ]
+ details = getattr(c, "DISCUSSION_DETAILS", {}) or {}
+ payload = {
+ "brand": "nomadro",
+ "tagline": "用一行代码环游世界",
+ "offlineNote": "小红书小程序无法联网。本包为官网内容离线镜像,收藏/计划仅保存在本机。完整互动请打开 nomadweb.nomadro.com",
+ "site": "https://nomadweb.nomadro.com",
+ "stats": {
+ "destinations": len(m.DESTINATIONS),
+ "visas": len(m.VISAS),
+ "meetups": len(meetups),
+ "blog": len(m.BLOG_POSTS),
+ "faqs": len(m.FAQS),
+ },
+ "ticker": [
+ "🌏 全球数字游民正在路上",
+ "📋 签证政策实时整理(离线快照)",
+ "🎉 同城活动与社区讨论可先浏览",
+ "🎓 游民学院免费课时可学",
+ ],
+ "rings": {
+ "explore": [
+ {"id": "destinations", "emoji": "🌴", "title": "目的地", "desc": "发现下一座城市"},
+ {"id": "visas", "emoji": "📋", "title": "签证指南", "desc": "远程友好签证"},
+ {"id": "matcher", "emoji": "🎯", "title": "智能匹配", "desc": "按预算气候排序"},
+ {"id": "compare", "emoji": "⚖️", "title": "城市对比", "desc": "并排看成本网速"},
+ ],
+ "connect": [
+ {"id": "meetups", "emoji": "🎉", "title": "游民活动", "desc": "同城与线上"},
+ {"id": "community", "emoji": "💬", "title": "社区讨论", "desc": "签证住宿经验"},
+ {"id": "voices", "emoji": "🗣️", "title": "游民心声", "desc": "真实旅居评价"},
+ ],
+ "grow": [
+ {"id": "digital", "emoji": "🎓", "title": "游民学院", "desc": "远程工作课程"},
+ {"id": "blog", "emoji": "📝", "title": "博客", "desc": "指南与攻略"},
+ {"id": "tools", "emoji": "🧰", "title": "工具箱", "desc": "费用估算等"},
+ {"id": "faq", "emoji": "❓", "title": "常见问题", "desc": "启动资金与保险"},
+ ],
+ },
+ "lifestyle": [
+ {"key": "morning", "emoji": "🌅", "title": "清晨", "body": "咖啡馆开机,时区对齐,列出今日三件交付。"},
+ {"key": "work", "emoji": "💻", "title": "深度工作", "body": "联合办公或家中安静角落,异步协作优先。"},
+ {"key": "explore", "emoji": "🚶", "title": "探索城市", "body": "傍晚散步、夜市与同城 meetup。"},
+ {"key": "collab", "emoji": "🤝", "title": "协作", "body": "和同行交换签证与住宿情报。"},
+ {"key": "social", "emoji": "🌙", "title": "社交", "body": "社区圆桌、兴趣局,或安静充电。"},
+ ],
+ "planChecklist": [
+ {"id": "visa", "label": "确认签证/停留天数"},
+ {"id": "flight", "label": "预订机票"},
+ {"id": "housing", "label": "首周短租/酒店"},
+ {"id": "sim", "label": "当地 SIM / eSIM"},
+ {"id": "cowork", "label": "踩点联合办公"},
+ {"id": "insurance", "label": "国际医疗保险"},
+ {"id": "budget", "label": "写下月预算"},
+ {"id": "meetup", "label": "报名一场同城活动"},
+ ],
+ "destinations": m.DESTINATIONS,
+ "visas": m.VISAS,
+ "faqs": m.FAQS,
+ "testimonials": m.TESTIMONIALS,
+ "tools": m.TOOLS,
+ "blog": m.BLOG_POSTS,
+ "blogContent": m.BLOG_CONTENT,
+ "meetups": meetups,
+ "discussions": c.DISCUSSIONS,
+ "discussionDetails": details if isinstance(details, dict) else {},
+ "course": d.COURSE_MODULES,
+ "lessons": d.LESSONS,
+ "jobs": (d.JOBS[:8] if getattr(d, "JOBS", None) else []),
+ }
+ OUT.write_text(
+ "window.NOMADRO_DATA = " + json.dumps(payload, ensure_ascii=False, default=str) + ";\n",
+ encoding="utf-8",
+ )
+ print(f"wrote {OUT} ({OUT.stat().st_size} bytes)")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/redmini/scripts/static_scan.py b/redmini/scripts/static_scan.py
index d46105d..3d8f6c6 100644
--- a/redmini/scripts/static_scan.py
+++ b/redmini/scripts/static_scan.py
@@ -1,21 +1,23 @@
#!/usr/bin/env python3
-"""Fail if release/ contains networking APIs (XHS miniTool rule)."""
+"""Fail if release/ contains networking APIs (XHS miniTool cannot network)."""
from __future__ import annotations
+
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
RELEASE = ROOT / "release"
+# Ban callable network surfaces only — plain URL text in copy is OK.
BANNED = re.compile(
- r"\b(fetch|XMLHttpRequest|WebSocket|navigator\.sendBeacon)\b|"
- r"https?://(?!miniapp\.xiaohongshu\.com)",
+ r"\b(fetch\s*\(|XMLHttpRequest|WebSocket\s*\(|navigator\.sendBeacon|"
+ r"axios\.|\$\.ajax|\$\.get\s*\(|\$\.post\s*\()",
re.I,
)
def main() -> int:
- bad = []
+ bad: list[str] = []
for p in RELEASE.rglob("*"):
if not p.is_file() or p.suffix.lower() not in {".js", ".html", ".css"}:
continue
diff --git a/redmini/src/data.js b/redmini/src/data.js
index 173b82e..1d0587b 100644
--- a/redmini/src/data.js
+++ b/redmini/src/data.js
@@ -1,19 +1 @@
-window.NOMADRO_DATA = {
- brand: 'nomadro',
- tagline: '用一行代码环游世界',
- note: '小红书小程序无法联网,本包为纯前端离线资料',
- destinations: [
- { slug: 'chiang-mai', name: '清迈', country: '泰国', cost: 4500, speed: 80, rating: 4.7, tagline: '咖啡与 coworking' },
- { slug: 'bali', name: '巴厘岛', country: '印尼', cost: 6000, speed: 60, rating: 4.5, tagline: '海边远程' },
- { slug: 'lisbon', name: '里斯本', country: '葡萄牙', cost: 12000, speed: 120, rating: 4.6, tagline: '欧洲数字游民枢纽' },
- { slug: 'da-nang', name: '岘港', country: '越南', cost: 5000, speed: 90, rating: 4.4, tagline: '性价比海岸' },
- { slug: 'mexico-city', name: '墨西哥城', country: '墨西哥', cost: 9000, speed: 70, rating: 4.3, tagline: '美食与时区友好' },
- { slug: 'bangkok', name: '曼谷', country: '泰国', cost: 5500, speed: 100, rating: 4.5, tagline: '交通便利大城' }
- ],
- tips: [
- '先定预算与签证天数,再选城市。',
- '到站第一周优先搞定住宿、SIM 与 coworking。',
- '同城活动是认识同行最快的方式。',
- '完整功能请打开 nomadweb.nomadro.com(本包离线不可请求网络)。'
- ]
-};
+window.NOMADRO_DATA = {"brand": "nomadro", "tagline": "用一行代码环游世界", "offlineNote": "小红书小程序无法联网。本包为官网内容离线镜像,收藏/计划仅保存在本机。完整互动请打开 nomadweb.nomadro.com", "site": "https://nomadweb.nomadro.com", "stats": {"destinations": 12, "visas": 12, "meetups": 12, "blog": 10, "faqs": 6}, "ticker": ["🌏 全球数字游民正在路上", "📋 签证政策实时整理(离线快照)", "🎉 同城活动与社区讨论可先浏览", "🎓 游民学院免费课时可学"], "rings": {"explore": [{"id": "destinations", "emoji": "🌴", "title": "目的地", "desc": "发现下一座城市"}, {"id": "visas", "emoji": "📋", "title": "签证指南", "desc": "远程友好签证"}, {"id": "matcher", "emoji": "🎯", "title": "智能匹配", "desc": "按预算气候排序"}, {"id": "compare", "emoji": "⚖️", "title": "城市对比", "desc": "并排看成本网速"}], "connect": [{"id": "meetups", "emoji": "🎉", "title": "游民活动", "desc": "同城与线上"}, {"id": "community", "emoji": "💬", "title": "社区讨论", "desc": "签证住宿经验"}, {"id": "voices", "emoji": "🗣️", "title": "游民心声", "desc": "真实旅居评价"}], "grow": [{"id": "digital", "emoji": "🎓", "title": "游民学院", "desc": "远程工作课程"}, {"id": "blog", "emoji": "📝", "title": "博客", "desc": "指南与攻略"}, {"id": "tools", "emoji": "🧰", "title": "工具箱", "desc": "费用估算等"}, {"id": "faq", "emoji": "❓", "title": "常见问题", "desc": "启动资金与保险"}]}, "lifestyle": [{"key": "morning", "emoji": "🌅", "title": "清晨", "body": "咖啡馆开机,时区对齐,列出今日三件交付。"}, {"key": "work", "emoji": "💻", "title": "深度工作", "body": "联合办公或家中安静角落,异步协作优先。"}, {"key": "explore", "emoji": "🚶", "title": "探索城市", "body": "傍晚散步、夜市与同城 meetup。"}, {"key": "collab", "emoji": "🤝", "title": "协作", "body": "和同行交换签证与住宿情报。"}, {"key": "social", "emoji": "🌙", "title": "社交", "body": "社区圆桌、兴趣局,或安静充电。"}], "planChecklist": [{"id": "visa", "label": "确认签证/停留天数"}, {"id": "flight", "label": "预订机票"}, {"id": "housing", "label": "首周短租/酒店"}, {"id": "sim", "label": "当地 SIM / eSIM"}, {"id": "cowork", "label": "踩点联合办公"}, {"id": "insurance", "label": "国际医疗保险"}, {"id": "budget", "label": "写下月预算"}, {"id": "meetup", "label": "报名一场同城活动"}], "destinations": [{"id": "1", "slug": "bali", "name": "巴厘岛", "country": "印尼", "emoji": "🏝️", "tag": "东南亚 · 热带天堂", "description": "乌布的数字游民社区闻名全球,稻田间的 Co-working Space 和瑜伽文化让这里成为游民圣地。", "region": "sea", "cost": 4500, "speed": 85, "temperature": 28, "rating": 9.2, "hue": 170, "nomads_count": "12,000+", "highlights": ["🏄 冲浪与海滩生活", "🧘 瑜伽冥想文化", "💰 东南亚性价比之王", "🌴 热带气候全年温暖"], "map_x": 720, "map_y": 310}, {"id": "2", "slug": "lisbon", "name": "里斯本", "country": "葡萄牙", "emoji": "🌊", "tag": "欧洲 · 海滨明珠", "description": "D7 签证友好,阳光海岸与悠久历史的完美融合,欧洲数字游民的首选基地。", "region": "europe", "cost": 9000, "speed": 120, "temperature": 22, "rating": 9.5, "hue": 220, "nomads_count": "8,500+", "highlights": ["📋 D7 签证门槛低", "☀️ 300天阳光", "🎵 Fado 音乐文化", "🚋 复古有轨电车"], "map_x": 430, "map_y": 195}, {"id": "3", "slug": "chiangmai", "name": "清迈", "country": "泰国", "emoji": "🏔️", "tag": "东南亚 · 文化古城", "description": "数字游民大本营,咖啡文化与夜市生活的天堂,全球性价比最高的游民城市。", "region": "sea", "cost": 3800, "speed": 95, "temperature": 30, "rating": 9.4, "hue": 45, "nomads_count": "15,000+", "highlights": ["☕ 咖啡馆文化浓厚", "🏮 夜市与寺庙", "💰 月生活费最低", "🤝 游民社区最活跃"], "map_x": 700, "map_y": 240}, {"id": "4", "slug": "mexico", "name": "墨西哥城", "country": "墨西哥", "emoji": "🌃", "tag": "拉美 · 活力之都", "description": "艺术、美食与科技交织,时区便利对接北美市场,拉美最具活力的游民城市。", "region": "latam", "cost": 6500, "speed": 75, "temperature": 18, "rating": 8.8, "hue": 300, "nomads_count": "5,200+", "highlights": ["🎨 街头艺术天堂", "🌮 世界美食之都", "🕐 北美时区友好", "💃 丰富夜生活"], "map_x": 220, "map_y": 240}, {"id": "5", "slug": "barcelona", "name": "巴塞罗那", "country": "西班牙", "emoji": "🏖️", "tag": "欧洲 · 地中海", "description": "高迪建筑与创业生态并存,Nomad Visa 政策领先,地中海生活的理想之选。", "region": "europe", "cost": 10500, "speed": 150, "temperature": 20, "rating": 9.1, "hue": 130, "nomads_count": "6,800+", "highlights": ["🏛️ 高迪建筑奇迹", "🏖️ 地中海海滩", "📋 Nomad Visa 便利", "🍷 美食与夜生活"], "map_x": 460, "map_y": 200}, {"id": "6", "slug": "tokyo", "name": "东京", "country": "日本", "emoji": "🗼", "tag": "亚洲 · 现代都市", "description": "极致效率与安全,适合追求高品质生活的远程工作者,亚洲科技之都。", "region": "asia", "cost": 12000, "speed": 200, "temperature": 15, "rating": 8.6, "hue": 10, "nomads_count": "4,100+", "highlights": ["🚄 极致公共交通", "🛡️ 全球最安全城市", "📶 网速亚洲第一", "🍣 美食文化巅峰"], "map_x": 820, "map_y": 210}, {"id": "7", "slug": "berlin", "name": "柏林", "country": "德国", "emoji": "🎨", "tag": "欧洲 · 创意之都", "description": "开放的创业与开源氛围,租金相对西欧友好,夜生活与艺术场景极具吸引力。", "region": "europe", "cost": 9800, "speed": 140, "temperature": 12, "rating": 8.9, "hue": 250, "nomads_count": "7,200+", "highlights": ["🎨 艺术与街头文化", "💻 创业生态活跃", "🚲 骑行友好", "🍺 社交场景丰富"], "map_x": 500, "map_y": 160}, {"id": "8", "slug": "dali", "name": "大理", "country": "中国", "emoji": "🏔️", "tag": "国内 · 慢生活", "description": "苍山洱海之间的内容创作者聚集地,生活节奏慢、社交松弛,适合深度创作与试住。", "region": "asia", "cost": 4200, "speed": 80, "temperature": 18, "rating": 8.7, "hue": 190, "nomads_count": "3,600+", "highlights": ["📷 内容创作友好", "🏞️ 自然风光", "🍵 咖啡馆密集", "🏡 合租成本低"], "map_x": 730, "map_y": 220}, {"id": "9", "slug": "seoul", "name": "首尔", "country": "韩国", "emoji": "🏙️", "tag": "亚洲 · 潮流都市", "description": "高速网络与深夜城市活力并存,适合对接东亚市场与产品迭代。", "region": "asia", "cost": 11000, "speed": 220, "temperature": 14, "rating": 8.5, "hue": 330, "nomads_count": "3,200+", "highlights": ["📶 全球顶尖网速", "🍜 美食与便利店文化", "🛍️ 潮流消费", "🚇 地铁覆盖极强"], "map_x": 800, "map_y": 200}, {"id": "10", "slug": "medellin", "name": "麦德林", "country": "哥伦比亚", "emoji": "🌺", "tag": "拉美 · 春城", "description": "四季如春、生活成本友好,英语游民社区成熟,适合北美时区远程工作。", "region": "latam", "cost": 5200, "speed": 90, "temperature": 23, "rating": 9.0, "hue": 140, "nomads_count": "6,400+", "highlights": ["🌤️ 四季如春", "💰 高性价比", "🕐 北美时区", "🤝 社区活动密集"], "map_x": 260, "map_y": 300}, {"id": "11", "slug": "dubai", "name": "迪拜", "country": "阿联酋", "emoji": "🏙️", "tag": "中东 · 枢纽都市", "description": "免税与签证便利吸引远程高收入人群,国际航班枢纽,适合中东/欧亚跳板。", "region": "asia", "cost": 14000, "speed": 180, "temperature": 32, "rating": 8.4, "hue": 40, "nomads_count": "4,800+", "highlights": ["🧾 税务友好", "✈️ 航线枢纽", "🏗️ 现代基础设施", "🌐 多元国际社区"], "map_x": 600, "map_y": 240}, {"id": "12", "slug": "tbilisi", "name": "第比利斯", "country": "格鲁吉亚", "emoji": "🍷", "tag": "欧亚 · 税务友好", "description": "1% 小企业税与远程友好签证,旧城氛围浓厚,东欧游民新基地。", "region": "europe", "cost": 4800, "speed": 70, "temperature": 16, "rating": 8.8, "hue": 15, "nomads_count": "5,100+", "highlights": ["🧾 1% 小企业税", "🏰 旧城与温泉", "🍷 葡萄酒文化", "💰 低生活成本"], "map_x": 560, "map_y": 190}], "visas": [{"id": "1", "country": "葡萄牙", "flag": "🇵🇹", "name": "葡萄牙 D7 签证", "badge": "⭐ 推荐", "badge_type": "easy", "duration": "2年,可续签", "income_req": "€760/月", "approval_time": "3-6 个月", "extra": "🏥 可享欧盟医疗", "difficulty": 35, "difficulty_label": "简单"}, {"id": "2", "country": "西班牙", "flag": "🇪🇸", "name": "西班牙 Nomad Visa", "badge": "🔥 热门", "badge_type": "hot", "duration": "1年,可续3年", "income_req": "€2,160/月", "approval_time": "1-3 个月", "extra": "🌍 可申根区旅行", "difficulty": 45, "difficulty_label": "中等"}, {"id": "3", "country": "印尼", "flag": "🇮🇩", "name": "印尼 B211A 签证", "badge": "💰 低成本", "badge_type": "budget", "duration": "60天,可延期", "income_req": "约 ¥2,000", "approval_time": "5-10 天", "extra": "🏝️ 适合巴厘岛旅居", "difficulty": 25, "difficulty_label": "简单"}, {"id": "4", "country": "泰国", "flag": "🇹🇭", "name": "泰国 LTR 签证", "badge": "🆕 新政策", "badge_type": "new", "duration": "10年", "income_req": "$80,000/年", "approval_time": "1-2 个月", "extra": "✈️ 多次入境", "difficulty": 60, "difficulty_label": "中等"}, {"id": "5", "country": "墨西哥", "flag": "🇲🇽", "name": "墨西哥 Temporary Resident", "badge": "💰 低成本", "badge_type": "budget", "duration": "1-4年", "income_req": "$2,500/月", "approval_time": "2-4 周", "extra": "🌮 北美时区友好", "difficulty": 30, "difficulty_label": "简单"}, {"id": "6", "country": "爱沙尼亚", "flag": "🇪🇪", "name": "爱沙尼亚 DNV", "badge": "🚀 先锋", "badge_type": "pioneer", "duration": "1年", "income_req": "€3,504/月", "approval_time": "2-4 周", "extra": "💻 全球首个数字游民签证", "difficulty": 40, "difficulty_label": "中等"}, {"id": "7", "country": "格鲁吉亚", "flag": "🇬🇪", "name": "格鲁吉亚 Remotely from Georgia", "badge": "⭐ 推荐", "badge_type": "easy", "duration": "1年", "income_req": "$2,000/月", "approval_time": "约 10 天", "extra": "🧾 可搭配小企业 1% 税", "difficulty": 28, "difficulty_label": "简单"}, {"id": "8", "country": "阿联酋", "flag": "🇦🇪", "name": "迪拜 Virtual Working Programme", "badge": "🔥 热门", "badge_type": "hot", "duration": "1年", "income_req": "$3,500/月", "approval_time": "2-4 周", "extra": "✈️ 中东航线枢纽", "difficulty": 42, "difficulty_label": "中等"}, {"id": "9", "country": "哥伦比亚", "flag": "🇨🇴", "name": "哥伦比亚 V Visa (Digital Nomad)", "badge": "💰 低成本", "badge_type": "budget", "duration": "最长 2 年", "income_req": "约 3× 最低工资", "approval_time": "2-6 周", "extra": "🌺 适合麦德林春城", "difficulty": 32, "difficulty_label": "简单"}, {"id": "10", "country": "韩国", "flag": "🇰🇷", "name": "韩国 Workation Visa (C-3-8)", "badge": "🆕 新政策", "badge_type": "new", "duration": "最长 2 年", "income_req": "约 $65,000/年", "approval_time": "1-2 个月", "extra": "🏙️ 对接首尔与济州", "difficulty": 55, "difficulty_label": "中等"}, {"id": "11", "country": "德国", "flag": "🇩🇪", "name": "德国 Freelance / Self-employment", "badge": "🚀 先锋", "badge_type": "pioneer", "duration": "1-3年", "income_req": "需商业计划与客户证明", "approval_time": "2-4 个月", "extra": "🎨 适合柏林自由职业", "difficulty": 65, "difficulty_label": "较难"}, {"id": "12", "country": "泰国", "flag": "🇹🇭", "name": "泰国 DTV 签证", "badge": "⭐ 推荐", "badge_type": "easy", "duration": "最长 5 年(多次入境)", "income_req": "约 50 万泰铢存款证明", "approval_time": "2-6 周", "extra": "🤝 远程工作友好", "difficulty": 38, "difficulty_label": "中等"}], "faqs": [{"id": "1", "question": "💰 做数字游民需要多少启动资金?", "order": 1, "answer": "建议准备 3-6 个月的生活费作为缓冲。以东南亚为例,¥15,000-30,000 即可开始。包括机票、首月住宿、签证费用和应急资金。欧洲目的地建议准备 ¥50,000 以上。"}, {"id": "2", "question": "📶 如何确保远程工作的网络稳定?", "order": 2, "answer": "选择网络评分高的城市,入住前用 Speedtest 测试。备用方案:本地 SIM 卡热点、随身 WiFi 设备、附近 Co-working Space。推荐携带 USB 网卡和 VPN 作为双保险。"}, {"id": "3", "question": "🏥 旅居期间的保险怎么办?", "order": 3, "answer": "推荐 SafetyWing 或 World Nomads 等国际医疗保险,月费约 $40-80,覆盖全球(部分国家除外)。长期旅居者可考虑目的地国家的本地保险,费用更低、报销更方便。"}, {"id": "4", "question": "🧾 税务问题如何处理?", "order": 4, "answer": "税务居民身份取决于居住天数(通常 183 天规则)。建议咨询专业税务顾问,了解双重征税协定。很多游民选择税务友好的国家(如葡萄牙、格鲁吉亚)作为基地。"}, {"id": "5", "question": "👨👩👧 可以带娃一起做数字游民吗?", "order": 5, "answer": "完全可以!巴厘岛、清迈、里斯本都有成熟的数字游民家庭社区。关键是选择教育资源丰富、医疗条件好的目的地,以及保持稳定的工作节奏,给孩子规律的生活。"}, {"id": "6", "question": "🤝 如何快速融入当地游民社区?", "order": 6, "answer": "加入 Nomad List、Facebook 群组和本地 Meetup 活动。入住游民友好的 Co-living 空间,参加每周的 Coworking 社交日。大部分游民社区非常开放,主动打招呼就能结识朋友。"}], "testimonials": [{"id": "1", "avatar": "👩💻", "content": "在清迈住了 8 个月,月花费不到 4000 元,但生活质量比国内一线城市高太多了。每天早上骑摩托去咖啡馆,这种感觉无法形容。", "author": "小林", "role": "前端开发 · 清迈 🇹🇭", "rating": 5}, {"id": "2", "avatar": "👨🎨", "content": "里斯本的 D7 签证让我在欧洲有了基地。白天在 Alfama 区的共享办公空间工作,周末去 Sintra 徒步,完美平衡。", "author": "Marco", "role": "UI 设计师 · 里斯本 🇵🇹", "rating": 5}, {"id": "3", "avatar": "🧑💼", "content": "带着家人做数字游民听起来疯狂,但在巴厘岛乌布,孩子们上国际学校,我和妻子远程工作,这是我们做过最正确的决定。", "author": "张家", "role": "产品经理 · 巴厘岛 🇮🇩", "rating": 5}, {"id": "4", "avatar": "👩🎨", "content": "麦德林四季如春,英语社区很活跃。我用北美时区接单,下午还能去公园运动,效率反而更高。", "author": "Sofia", "role": "自由职业设计 · 麦德林 🇨🇴", "rating": 5}, {"id": "5", "avatar": "👨💻", "content": "第比利斯生活成本低,还能开本地公司走 1% 税。旧城咖啡馆写代码,周末去卡兹别吉徒步,节奏很舒服。", "author": "Devon", "role": "DevOps · 第比利斯 🇬🇪", "rating": 5}, {"id": "6", "avatar": "📷", "content": "大理适合慢创作。合租便宜、风景治愈,但要主动找同频伙伴,别只待在民宿里。", "author": "Lina", "role": "内容创作者 · 大理 🇨🇳", "rating": 4}, {"id": "7", "avatar": "🧑🚀", "content": "柏林创业氛围强,开源 meetup 特别多。冬天冷、德语门槛真实存在,但找到圈子后很值得。", "author": "Ken", "role": "数据分析 · 柏林 🇩🇪", "rating": 4}, {"id": "8", "avatar": "🏙️", "content": "迪拜当跳板很香:航班方便、签证路径清晰。夏天太热,建议把深度工作时间放在室内联合办公。", "author": "Omar", "role": "增长顾问 · 迪拜 🇦🇪", "rating": 4}], "tools": [{"id": "1", "emoji": "💼", "name": "远程协作", "description": "Slack · Notion · Figma · Zoom", "tags": ["团队", "设计", "沟通"], "category": "work"}, {"id": "2", "emoji": "✈️", "name": "旅行规划", "description": "Skyscanner · Nomad List · SafetyWing", "tags": ["机票", "签证", "保险"], "category": "travel"}, {"id": "3", "emoji": "💳", "name": "财务管理", "description": "Wise · Revolut · Xero · 多币种账户", "tags": ["汇款", "记账", "税务"], "category": "finance"}, {"id": "4", "emoji": "🤝", "name": "社群网络", "description": "Nomad List · Remote Year · 本地 Meetup", "tags": ["社交", "活动", "合租"], "category": "connect"}, {"id": "5", "emoji": "🏥", "name": "健康保障", "description": "SafetyWing · World Nomads · 运动 App", "tags": ["保险", "健身", "心理"], "category": "health"}, {"id": "6", "emoji": "📚", "name": "持续学习", "description": "Coursera · Duolingo · 当地语言班", "tags": ["技能", "语言", "文化"], "category": "learn"}], "blog": [{"id": "1", "slug": "chiangmai-guide-2026", "title": "清迈数字游民完全指南 2026", "excerpt": "从签证到住宿,从咖啡馆到 Co-working,一篇搞定清迈旅居。", "emoji": "🏔️", "author": "nomadro", "published_at": "2026-01-15", "read_time": 8, "tags": ["清迈", "指南", "东南亚"]}, {"id": "2", "slug": "portugal-d7-visa", "title": "葡萄牙 D7 签证申请全攻略", "excerpt": "手把手教你申请欧洲最受欢迎的远程工作签证。", "emoji": "🇵🇹", "author": "nomadro", "published_at": "2026-02-03", "read_time": 12, "tags": ["签证", "葡萄牙", "欧洲"]}, {"id": "3", "slug": "nomad-tax-basics", "title": "数字游民税务入门:你需要知道的 5 件事", "excerpt": "183 天规则、双重征税、税务居民身份——一文理清。", "emoji": "🧾", "author": "nomadro", "published_at": "2026-02-20", "read_time": 10, "tags": ["税务", "法律", "指南"]}, {"id": "4", "slug": "medellin-spring-city", "title": "麦德林春城试住 30 天笔记", "excerpt": "时区、安全区、联合办公与西语社区初体验。", "emoji": "🌺", "author": "Sofia", "published_at": "2026-03-05", "read_time": 9, "tags": ["麦德林", "拉美", "试住"]}, {"id": "5", "slug": "tbilisi-tax-base", "title": "第比利斯当税务基地靠谱吗?", "excerpt": "远程签证、小企业税与旧城生活成本实测。", "emoji": "🍷", "author": "Devon", "published_at": "2026-03-18", "read_time": 11, "tags": ["格鲁吉亚", "税务", "欧洲"]}, {"id": "6", "slug": "dali-slow-create", "title": "大理慢创作:合租、网速与社群", "excerpt": "国内游民城市怎么住得久又不散漫。", "emoji": "🏔️", "author": "Lina", "published_at": "2026-04-02", "read_time": 7, "tags": ["大理", "内容", "国内"]}, {"id": "7", "slug": "berlin-startup-nomad", "title": "柏林自由职业与开源社群指南", "excerpt": "签证路径、共享办公与每周活动清单。", "emoji": "🎨", "author": "Ken", "published_at": "2026-04-20", "read_time": 10, "tags": ["柏林", "创业", "开源"]}, {"id": "8", "slug": "dubai-hub-month", "title": "迪拜一个月跳板旅居怎么排期", "excerpt": "签证、炎热季节、航班中转与办公点选择。", "emoji": "🏙️", "author": "Omar", "published_at": "2026-05-01", "read_time": 8, "tags": ["迪拜", "中东", "枢纽"]}, {"id": "9", "slug": "seoul-workation", "title": "首尔 Workation:网速、便利与成本", "excerpt": "C-3-8 签证与江南区办公踩点。", "emoji": "🏙️", "author": "Yuki", "published_at": "2026-05-16", "read_time": 9, "tags": ["首尔", "韩国", "网速"]}, {"id": "10", "slug": "first-month-checklist", "title": "旅居第一月清单:从落地到稳定输出", "excerpt": "SIM、短租、联合办公、社群与预算五步走。", "emoji": "✅", "author": "nomadro", "published_at": "2026-06-01", "read_time": 6, "tags": ["清单", "新手", "落地"]}], "blogContent": {"chiangmai-guide-2026": "## 为什么选择清迈?\n\n清迈是公认的全球数字游民之都。低廉的生活成本、完善的 Co-working 生态、友善的本地人和丰富的文化活动,让它成为新手游民的最佳起点。\n\n## 签证方案\n\n- **旅游签**:落地签 15 天,或提前办旅游签 60 天\n- **DTV 签证**:2024 年推出的 Destination Thailand Visa,适合远程工作者\n- **学生签/精英签**:长期旅居的进阶选择\n\n## 住宿推荐\n\n| 区域 | 月租 | 特点 |\n|------|------|------|\n| Nimman | ¥2,500-4,000 | 咖啡馆、餐厅、年轻人多 |\n| Old City | ¥1,500-3,000 | 文化氛围浓,步行可达寺庙 |\n| Hang Dong | ¥2,000-3,500 | 安静,适合深度工作 |\n\n## 最佳 Co-working\n\n1. **Punspace** — Nimman 区经典,日票 ¥60\n2. **CAMP** — Maya 商场顶楼,免费(消费即可)\n3. **Hub53** — 安静专业,月票 ¥800\n\n## 月均预算\n\n- 住宿:¥2,500\n- 餐饮:¥1,200\n- 交通:¥300(租摩托)\n- Co-working:¥400\n- 其他:¥400\n- **合计:约 ¥3,800-4,500**\n\n## 实用 Tips\n\n☕ 推荐咖啡馆:Ristr8to(世界冠军)、Graph One Nimman\n🏍️ 租摩托月费约 ¥400,注意戴头盔\n📱 推荐 AIS 或 TrueMove 无限流量套餐\n🤝 每周四有 Nomad Meetup,关注 Facebook 群组", "portugal-d7-visa": "## D7 签证是什么?\n\n葡萄牙 D7 签证(Passive Income Visa)最初为退休者设计,但因收入要求低、审批相对简单,成为数字游民进入欧盟的最佳通道之一。\n\n## 申请条件\n\n- 月收入不低于 **€760**(葡萄牙最低工资)\n- 银行存款建议 **€9,120+**(12个月生活费)\n- 无犯罪记录\n- 葡萄牙本地银行账户\n- 健康保险\n\n## 申请流程\n\n1. **准备材料**(2-4 周)\n - 护照、照片、收入证明、银行流水\n - 葡萄牙税号(NIF)\n - 住宿证明(租房合同或酒店预订)\n\n2. **递交申请**\n - 在中国:葡萄牙驻华使馆\n - 或入境葡萄牙后转居留许可\n\n3. **等待审批**(3-6 个月)\n\n4. **登陆葡萄牙**\n - 领取居留卡\n - 登记住址\n\n## 费用预算\n\n- 签证费:约 €90\n- 律师费(可选):€500-1,500\n- NIF + 银行开户:€200-300\n- 首月生活费:€800-1,200\n\n## 里斯本生活成本\n\n- 一居室公寓:€800-1,200/月\n- Co-working:€150-250/月\n- 餐饮:€300-500/月\n- **月均总花费:€1,200-1,800**\n\n## 常见问题\n\n**Q: D7 可以工作吗?**\nA: 可以远程为海外雇主工作,但不能在葡萄牙本地公司就职。\n\n**Q: 5年后能拿护照吗?**\nA: 满足居住要求后可以申请永居或入籍。", "nomad-tax-basics": "## 1. 什么是税务居民?\n\n大多数国家用 **183 天规则** 判定税务居民:在一个自然年内居住超过 183 天,即成为该国税务居民,需在该国申报全球收入。\n\n## 2. 双重征税怎么办?\n\n如果两个国家都认为你是税务居民,查 **双边税收协定**(DTA)。中国已与 100+ 国家签署 DTA,可避免重复缴税。\n\n## 3. 数字游民常见税务策略\n\n| 策略 | 说明 | 适合人群 |\n|------|------|----------|\n| 零税务居民 | 不在任何国家住满 183 天 | 短期旅居者 |\n| 税务友好国 | 葡萄牙 NHR、格鲁吉亚 1% 税 | 长期旅居者 |\n| 原籍国申报 | 回国期间申报 | 兼职游民 |\n\n## 4. 中国税务居民注意\n\n- 中国公民默认是中国税务居民(全球征税)\n- 海外收入也需申报(可抵免境外已缴税)\n- 建议咨询专业税务师\n\n## 5. 实用建议\n\n1. 📋 记录每个国家的入境/出境日期\n2. 🧾 保留所有收入凭证和银行流水\n3. 🏦 使用 Wise 等工具便于跨境汇款记录\n4. 👨💼 收入超过一定金额建议聘请税务顾问\n5. 📱 推荐工具:Xero(记账)、TaxScouts(报税)", "medellin-spring-city": "## 为什么麦德林?\n\n四季如春、生活成本可控,且贴近北美时区,适合接美加客户。Laureles / El Poblado 是游民常见落脚区。\n\n## 30 天试住建议\n\n1. 前 7 天住短租,实测网速与噪音\n2. 办本地 SIM,备份热点\n3. 参加 2 场线下 meetup,验证社区匹配度\n4. 确定联合办公月票再签长租\n\n## 预算参考(月)\n\n- 住宿:¥2,000–3,500\n- 餐饮:¥1,200–1,800\n- 交通:¥300–500\n- 办公:¥400–800", "tbilisi-tax-base": "## 远程签证 + 小企业税\n\n格鲁吉亚对远程工作者友好,许多人会评估本地公司与 1% 税路径(需自行核实最新政策)。\n\n## 生活感受\n\n旧城与 Vake 区咖啡馆多,适合深度工作;冬季偏冷,夏天舒服。\n\n## 注意\n\n- 银行开户与本地手机号流程可能变动\n- 税务方案务必咨询持证顾问\n- 把「试住 30 天」和「税务落地」分开决策", "dali-slow-create": "## 适合谁\n\n内容创作者、独立开发者、想放慢节奏但仍保持产出的人。\n\n## 实操建议\n\n- 合租优先选稳定 WiFi 与独立工作桌\n- 用固定作息对抗「度假感」\n- 主动加入本地创作者局,避免社交孤岛\n\n## 成本\n\n月生活费常可控制在 ¥3,500–5,000,视合租与出行频率而定。", "berlin-startup-nomad": "## 柏林为什么吸引游民\n\n开源、设计、创业活动密度高,共享办公选择多。\n\n## 落地要点\n\n- 提前规划居留/自由职业路径\n- 冬天日照短,准备补光与室内运动\n- 德语不是必须,但能明显提升生活便利\n\n## 每周节奏\n\n2 天联合办公 + 1 场 meetup + 周末短途,比纯咖啡馆更稳。", "dubai-hub-month": "## 适合当跳板\n\n国际航班密集,签证路径相对清晰,适合中东/欧亚中转月。\n\n## 炎热季节\n\n夏季高温,深度工作尽量安排在空调联合办公;户外活动放早晚。\n\n## 预算\n\n整体偏高,建议按「枢纽月」而非「低成本旅居」来规划。", "seoul-workation": "## 网速与便利\n\n首尔网速与城市便利度极强,适合需要高频协作的产品/工程角色。\n\n## 成本与节奏\n\n住宿与餐饮成本接近一线城市;用便利店与共享办公控制变量。\n\n## 签证\n\n关注 Workation / 相关短期远程路径的最新材料要求。", "first-month-checklist": "## 落地五步\n\n1. **连接**:落地办 SIM,测公寓与办公点网速\n2. **住处**:先短租 7–14 天,再决定长租\n3. **办公**:固定 1 个主点 + 2 个备份咖啡馆\n4. **社群**:报名 1 场 meetup,加 1 个本地群\n5. **预算**:用第一周真实账单校准月预算\n\n## 输出稳定\n\n每天保护 4 小时深度工作,旅行探索放在下午后段。"}, "meetups": [{"id": "online-roundtable", "title": "远程工作节奏圆桌", "city": "线上", "destination_slug": "", "emoji": "🎙️", "date": "2026-09-12", "time": "20:00", "venue": "nomadro 线上活动厅", "description": "聊聊异步协作、时区管理和旅居城市选择。登录后可 RSVP,活动前会收到提醒。", "mode": "online", "access_level": "public", "rsvp_count": 38, "max_attendees": 80, "organizer": "nomadro 社区", "tags": ["远程", "协作", "新手友好"], "is_upcoming": true}, {"id": "chiangmai-cowork", "title": "清迈联合办公下午茶", "city": "清迈", "destination_slug": "chiangmai", "emoji": "☕", "date": "2026-09-18", "time": "15:00", "venue": "Nimman 区 Co-working", "description": "一起办公、分享签证经验和住宿踩坑。线下为主,也开放线上旁听链接。", "mode": "hybrid", "access_level": "public", "rsvp_count": 14, "max_attendees": 24, "organizer": "清迈游民小组", "tags": ["线下", "社交", "东南亚"], "is_upcoming": true}, {"id": "lisbon-sunset", "title": "里斯本日落漫步", "city": "里斯本", "destination_slug": "lisbon", "emoji": "🌅", "date": "2026-09-22", "time": "18:30", "venue": "Miradouro 观景点集合", "description": "工作一周后,和同城游民一起看日落、交换欧洲签证情报。", "mode": "offline", "access_level": "public", "rsvp_count": 9, "max_attendees": 16, "organizer": "Lisbon Nomads", "tags": ["欧洲", "户外", "社交"], "is_upcoming": true}, {"id": "bali-surf-morning", "title": "巴厘岛晨间冲浪 + 办公", "city": "巴厘岛", "destination_slug": "bali", "emoji": "🏄", "date": "2026-10-05", "time": "07:00", "venue": "Canggu Beach", "description": "早起冲浪,10 点回咖啡馆集中办公。适合想体验 Work-Life 平衡的游民。", "mode": "offline", "access_level": "public", "rsvp_count": 11, "max_attendees": 20, "organizer": "Bali Remote Crew", "tags": ["运动", "生活方式"], "is_upcoming": true}, {"id": "tokyo-tax-talk", "title": "日本税务与居留线上分享", "city": "线上", "destination_slug": "tokyo", "emoji": "🧾", "date": "2026-10-12", "time": "19:30", "venue": "线上直播", "description": "特邀长期旅居日本的游民分享税务申报、保险和银行开户经验(非法律建议)。", "mode": "online", "access_level": "members", "rsvp_count": 52, "max_attendees": 120, "organizer": "nomadro 社区", "tags": ["税务", "日本", "干货"], "is_upcoming": true}, {"id": "berlin-opensource", "title": "柏林开源与 DevOps 夜谈", "city": "柏林", "destination_slug": "berlin", "emoji": "🖥️", "date": "2026-09-28", "time": "19:00", "venue": "Kreuzberg Co-working", "description": "分享自托管、CI 与远程团队协作实践,会后可继续社交。", "mode": "hybrid", "access_level": "public", "rsvp_count": 22, "max_attendees": 40, "organizer": "Berlin Remote", "tags": ["开源", "技术", "欧洲"], "is_upcoming": true}, {"id": "dali-create-walk", "title": "大理洱海创作散步", "city": "大理", "destination_slug": "dali", "emoji": "📷", "date": "2026-10-02", "time": "16:00", "venue": "才村码头集合", "description": "边走边拍,分享内容选题与变现路径,适合创作者与慢旅居者。", "mode": "offline", "access_level": "public", "rsvp_count": 8, "max_attendees": 18, "organizer": "大理创作者局", "tags": ["内容", "户外", "国内"], "is_upcoming": true}, {"id": "medellin-spanish", "title": "麦德林西语咖啡局", "city": "麦德林", "destination_slug": "medellin", "emoji": "☕", "date": "2026-10-08", "time": "10:30", "venue": "Laureles 咖啡馆", "description": "初级西语练习 + 安全区与租房情报交换,新手友好。", "mode": "offline", "access_level": "public", "rsvp_count": 16, "max_attendees": 20, "organizer": "Medellín Nomads", "tags": ["语言", "拉美", "社交"], "is_upcoming": true}, {"id": "seoul-night-office", "title": "首尔深夜联合办公", "city": "首尔", "destination_slug": "seoul", "emoji": "🌃", "date": "2026-10-15", "time": "21:00", "venue": "江南共享办公", "description": "适合东亚时区冲刺交付的游民,自带电脑即可。", "mode": "offline", "access_level": "public", "rsvp_count": 12, "max_attendees": 25, "organizer": "Seoul Workation", "tags": ["办公", "韩国", "效率"], "is_upcoming": true}, {"id": "dubai-hub-brunch", "title": "迪拜枢纽早午餐", "city": "迪拜", "destination_slug": "dubai", "emoji": "🥂", "date": "2026-10-18", "time": "11:00", "venue": "Downtown 咖啡厅", "description": "中转游民见面:签证、航班与下一站城市选择。", "mode": "offline", "access_level": "public", "rsvp_count": 10, "max_attendees": 16, "organizer": "Dubai Hub Crew", "tags": ["中东", "社交", "跳板"], "is_upcoming": true}, {"id": "tbilisi-tax-cafe", "title": "第比利斯税务咖啡聊", "city": "第比利斯", "destination_slug": "tbilisi", "emoji": "🍷", "date": "2026-10-22", "time": "15:00", "venue": "Vake 区咖啡馆", "description": "交流远程签证与本地公司实务(非正式法律意见),会后可徒步旧城。", "mode": "offline", "access_level": "members", "rsvp_count": 19, "max_attendees": 22, "organizer": "Tbilisi Base", "tags": ["税务", "欧洲", "干货"], "is_upcoming": true}, {"id": "mexico-food-walk", "title": "墨西哥城街头美食漫步", "city": "墨西哥城", "destination_slug": "mexico", "emoji": "🌮", "date": "2026-10-26", "time": "17:30", "venue": "Roma Norte 集合", "description": "边吃边聊北美时区接单与安全出行经验。", "mode": "offline", "access_level": "public", "rsvp_count": 13, "max_attendees": 18, "organizer": "CDMX Nomads", "tags": ["美食", "拉美", "社交"], "is_upcoming": true}], "discussions": [{"id": "visa-sea-2026", "title": "2026 东南亚长期停留签证怎么选?", "excerpt": "泰国 DTV、马来西亚 DE Rantau、印尼第二家园……大家最近实际办下来体验如何?", "author": "小林", "author_emoji": "🧳", "category": "签证", "reply_count": 23, "like_count": 47, "is_pinned": true, "created_at": "2026-08-20", "tags": ["签证", "东南亚"]}, {"id": "async-remote", "title": "异步团队怎么写日报才不烦人?", "excerpt": "我们团队跨 5 个时区,想收集团队节奏模板和工具推荐。", "author": "Marco", "author_emoji": "💻", "category": "远程工作", "reply_count": 15, "like_count": 31, "is_pinned": false, "created_at": "2026-08-22", "tags": ["远程", "协作"]}, {"id": "coliving-tips", "title": "第一次租 Coliving 必问房东的 10 个问题", "excerpt": "押金、网速、清洁、访客政策……欢迎补充你的踩坑经历。", "author": "阿静", "author_emoji": "🏡", "category": "住宿", "reply_count": 19, "like_count": 56, "is_pinned": false, "created_at": "2026-08-25", "tags": ["住宿", "经验"]}, {"id": "health-insurance", "title": "数字游民国际医疗保险对比", "excerpt": "SafetyWing、Genki、本地险……按年龄段和目的地聊聊性价比。", "author": "Yuki", "author_emoji": "🏥", "category": "安全", "reply_count": 11, "like_count": 28, "is_pinned": false, "created_at": "2026-08-27", "tags": ["保险", "安全"]}, {"id": "meetup-feedback", "title": "你希望 nomadro 线下活动开在哪些城市?", "excerpt": "我们在规划 Q4 活动路线,投票 + 留言告诉我们你的城市!", "author": "nomadro", "author_emoji": "🌍", "category": "社区", "reply_count": 34, "like_count": 72, "is_pinned": true, "created_at": "2026-08-28", "tags": ["活动", "投票"]}, {"id": "medellin-safety", "title": "麦德林哪些街区适合第一次长住?", "excerpt": "Laureles / Poblado / Envigado 的网速、噪音和夜间出行体验求分享。", "author": "Sofia", "author_emoji": "🌺", "category": "住宿", "reply_count": 17, "like_count": 41, "created_at": "2026-08-30", "tags": ["麦德林", "安全"]}, {"id": "tbilisi-bank", "title": "第比利斯开户与收美元的实操经验", "excerpt": "需要本地号码吗?Wise 能不能直接用?求最新流程。", "author": "Devon", "author_emoji": "🍷", "category": "财务", "reply_count": 14, "like_count": 36, "created_at": "2026-09-01", "tags": ["格鲁吉亚", "银行"]}, {"id": "berlin-winter", "title": "柏林冬天怎么保持产出和社交?", "excerpt": "日照短、天气冷,有没有室内运动 + meetup 推荐。", "author": "Ken", "author_emoji": "🎨", "category": "生活方式", "reply_count": 9, "like_count": 22, "created_at": "2026-09-02", "tags": ["柏林", "健康"]}, {"id": "dali-wifi", "title": "大理合租找房:网速坑位合集", "excerpt": "哪些区域光纤稳定?有没有房东靠谱名单。", "author": "Lina", "author_emoji": "📷", "category": "住宿", "reply_count": 21, "like_count": 48, "created_at": "2026-09-03", "tags": ["大理", "网速"]}, {"id": "dubai-summer", "title": "迪拜夏天还能正常远程办公吗?", "excerpt": "空调联合办公、作息调整和预算增加幅度求真实数据。", "author": "Omar", "author_emoji": "🏙️", "category": "生活方式", "reply_count": 8, "like_count": 19, "created_at": "2026-09-04", "tags": ["迪拜", "气候"]}, {"id": "seoul-visa", "title": "韩国 Workation 签证材料清单核对", "excerpt": "收入证明、保险、行程……有人最近过签吗?", "author": "Yuki", "author_emoji": "🇰🇷", "category": "签证", "reply_count": 12, "like_count": 33, "created_at": "2026-09-05", "tags": ["韩国", "签证"]}, {"id": "cowork-month-pass", "title": "各城市联合办公月票性价比对比", "excerpt": "清迈 / 里斯本 / 柏林 / 麦德林,欢迎贴价目与座位体验。", "author": "Alex", "author_emoji": "🚀", "category": "远程工作", "reply_count": 26, "like_count": 61, "created_at": "2026-09-06", "tags": ["办公", "成本"]}], "discussionDetails": {"visa-sea-2026": [{"id": "r1", "author": "阿Ken", "author_emoji": "🇹🇭", "content": "DTV 刚办下来,材料比想象中简单,关键是银行流水和远程工作证明。", "created_at": "2026-08-21", "like_count": 12}, {"id": "r2", "author": "Sara", "author_emoji": "🇲🇾", "content": "DE Rantau 审批大概 3 周,适合想待吉隆坡的。", "created_at": "2026-08-21", "like_count": 8}, {"id": "r3", "author": "Yuki", "author_emoji": "🇮🇩", "content": "巴厘岛短期用 B211A 足够,长期再看第二家园成本是否划算。", "created_at": "2026-08-22", "like_count": 6}], "async-remote": [{"id": "r1", "author": "Devon", "author_emoji": "🕐", "content": "我们用 Loom 录屏 + 简短文字摘要,比长日报友好很多。", "created_at": "2026-08-23", "like_count": 9}, {"id": "r2", "author": "Alex", "author_emoji": "🚀", "content": "模板三要素:昨天完成 / 今天计划 / 卡点,别写散文。", "created_at": "2026-08-23", "like_count": 11}], "coliving-tips": [{"id": "r1", "author": "小林", "author_emoji": "🧳", "content": "一定要问清押金退还条件和网速实测截图,别只看宣传。", "created_at": "2026-08-26", "like_count": 14}], "medellin-safety": [{"id": "r1", "author": "Alex", "author_emoji": "🌮", "content": "第一次建议 Laureles,生活成本与便利平衡;夜间少拿手机逛街。", "created_at": "2026-08-31", "like_count": 10}], "tbilisi-bank": [{"id": "r1", "author": "Omar", "author_emoji": "🏙️", "content": "先办本地 SIM 再去银行,材料准备护照+地址证明更顺利。", "created_at": "2026-09-01", "like_count": 7}], "dali-wifi": [{"id": "r1", "author": "阿静", "author_emoji": "🏡", "content": "才村/喜洲合租差别大,入住前用手机热点对照测速最靠谱。", "created_at": "2026-09-03", "like_count": 9}], "cowork-month-pass": [{"id": "r1", "author": "Marco", "author_emoji": "💻", "content": "里斯本月票大约 €150–250;清迈很多空间 ¥600–1000 就够用。", "created_at": "2026-09-06", "like_count": 15}], "health-insurance": [{"id": "r1", "author": "Sofia", "author_emoji": "🏥", "content": "30 岁以下 SafetyWing 够用;有慢性病建议加本地门诊险。", "created_at": "2026-08-28", "like_count": 10}, {"id": "r2", "author": "Ken", "author_emoji": "🇯🇵", "content": "在日本长期停留我叠了本地国民健康保险,报销更稳。", "created_at": "2026-08-28", "like_count": 7}], "meetup-feedback": [{"id": "r1", "author": "Lina", "author_emoji": "📷", "content": "大理 + 清迈希望能固定每月一场创作局。", "created_at": "2026-08-29", "like_count": 18}, {"id": "r2", "author": "Omar", "author_emoji": "🏙️", "content": "迪拜适合枢纽月见面,建议放周末早午餐。", "created_at": "2026-08-29", "like_count": 9}], "berlin-winter": [{"id": "r1", "author": "Devon", "author_emoji": "🖥️", "content": "冬天把联合办公当社交主场,再加一周一次室内攀岩/跑步局。", "created_at": "2026-09-02", "like_count": 11}], "dubai-summer": [{"id": "r1", "author": "Nina", "author_emoji": "🍷", "content": "夏天基本只在空调空间办公,预算里把交通和水电预留高一点。", "created_at": "2026-09-04", "like_count": 6}], "seoul-visa": [{"id": "r1", "author": "Jin", "author_emoji": "🇰🇷", "content": "最近材料重点是收入证明和保险;审批大约 3–6 周,建议提前办。", "created_at": "2026-09-05", "like_count": 8}]}, "course": [{"title": "启程:远程工作基础", "lessons": [{"title": "什么是数字游民", "duration": "8 分钟", "free": true}, {"title": "异步协作入门", "duration": "12 分钟", "free": true}, {"title": "时区与会议管理", "duration": "15 分钟", "free": false}]}, {"title": "签证与税务", "lessons": [{"title": "东南亚长期签证概览", "duration": "18 分钟", "free": false}, {"title": "183 天规则与税务居民", "duration": "14 分钟", "free": false}]}, {"title": "Homelab 与连接", "lessons": [{"title": "把家装进一个机柜", "duration": "20 分钟", "free": false}, {"title": "无论在哪都能连回家", "duration": "16 分钟", "free": false}]}], "lessons": {"0-0": {"title": "什么是数字游民", "moduleIndex": 0, "lessonIndex": 0, "duration": "8 分钟", "free": true, "content": "数字游民是一种工作方式:产出不绑定在某个地理坐标上。你可以在任何有网络的地方完成交付。"}, "0-1": {"title": "异步协作入门", "moduleIndex": 0, "lessonIndex": 1, "duration": "12 分钟", "free": true, "content": "异步沟通不是「随时在线」,而是「明确预期」:写清楚截止时间、交付物和决策人。"}, "0-2": {"title": "时区与会议管理", "moduleIndex": 0, "lessonIndex": 2, "duration": "15 分钟", "free": false, "content": "用 overlap 窗口安排会议,非重叠时段留给深度工作。工具箱里的「会议黄金时段」可以帮你算。"}, "1-0": {"title": "东南亚长期签证概览", "moduleIndex": 1, "lessonIndex": 0, "duration": "18 分钟", "free": false, "content": "泰国 DTV、马来西亚 DE Rantau、印尼第二家园……各国有不同的门槛与材料清单。"}, "1-1": {"title": "183 天规则与税务居民", "moduleIndex": 1, "lessonIndex": 1, "duration": "14 分钟", "free": false, "content": "多数国家用 183 天判定税务居民。用 nomadro 税居天数工具追踪停留。"}, "2-0": {"title": "把家装进一个机柜", "moduleIndex": 2, "lessonIndex": 0, "duration": "20 分钟", "free": false, "content": "Homelab 让你在旅途中拥有可控的 NAS、VPN 和开发环境。"}, "2-1": {"title": "无论在哪都能连回家", "moduleIndex": 2, "lessonIndex": 1, "duration": "16 分钟", "free": false, "content": "WireGuard + 动态 DNS,把家里的服务安全暴露给在外的你。"}}, "jobs": [{"id": "j1", "title": "Senior Frontend Engineer", "company": "Remote First Co", "location": "全球远程", "type": "全职", "salary": "$80k–120k", "tags": ["React", "TypeScript", "远程"], "url": "/gigs"}, {"id": "j2", "title": "DevOps / SRE", "company": "Nomad Labs", "location": "欧洲时区", "type": "合同", "salary": "€60–90/h", "tags": ["K8s", "AWS", "异步"], "url": "/gigs"}, {"id": "j3", "title": "内容运营(中文)", "company": "nomadro", "location": "东南亚友好", "type": "兼职", "salary": "面议", "tags": ["社区", "写作", "游民"], "url": "/gigs"}, {"id": "j4", "title": "Product Designer", "company": "Atlantic Remote", "location": "欧盟时区 ±3h", "type": "全职", "salary": "€55k–75k", "tags": ["Figma", "B2B", "远程"], "url": "/gigs"}, {"id": "j5", "title": "Growth Marketer", "company": "Hub Ventures", "location": "中东/欧洲友好", "type": "合同", "salary": "$4k–6k/月", "tags": ["SEO", "内容", "增长"], "url": "/gigs"}, {"id": "j6", "title": "Backend Engineer (Python)", "company": "Latam Cloud", "location": "北美时区", "type": "全职", "salary": "$90k–130k", "tags": ["Python", "FastAPI", "Postgres"], "url": "/gigs"}, {"id": "j7", "title": "Community Manager", "company": "nomadro", "location": "全球远程", "type": "兼职", "salary": "¥8k–12k/月", "tags": ["社区", "活动", "中英"], "url": "/gigs"}, {"id": "j8", "title": "Mobile Engineer (iOS/Android)", "company": "Seoul Soft", "location": "东亚时区", "type": "全职", "salary": "$70k–100k", "tags": ["Flutter", "移动端"], "url": "/gigs"}]};
diff --git a/redmini/tool/app.js b/redmini/tool/app.js
index e22fbb2..0692e53 100644
--- a/redmini/tool/app.js
+++ b/redmini/tool/app.js
@@ -1,42 +1,805 @@
(function () {
- 'use strict';
- var data = window.NOMADRO_DATA || { destinations: [], tips: [] };
- var root = document.getElementById('app');
- var favKey = 'nomadro_redmini_favs';
+ "use strict";
- function render(favs) {
- favs = favs || [];
- var cards = (data.destinations || []).map(function (d) {
- var marked = favs.indexOf(d.slug) >= 0 ? ' · 已收藏' : '';
- return (
- '' +
- '' + d.name + '
' +
- '' + (d.country || '') + ' · ¥' + (d.cost || '—') + '/月 · ★' + (d.rating || '—') + '
' +
- '' + (d.tagline || '') + '
' +
- '' + (marked || '点按收藏(仅本地)') + '
' +
- ' '
- );
- }).join('');
- var tips = (data.tips || []).map(function (t) { return '' + t + ' '; }).join('');
- root.innerHTML =
- '' + (data.brand || 'nomadro') + '
' +
- '' + (data.tagline || '') + '
' +
- '' + (data.note || '') + '
' +
- cards +
- '';
+ var DATA = window.NOMADRO_DATA || {};
+ var Bridge = window.XhsBridge || window.NomadroXhs || null;
+ var FAV_KEY = "nomadro_xhs_favs";
+ var PLAN_KEY = "nomadro_xhs_plan";
+ var CHECK_KEY = "nomadro_xhs_checklist";
+ var RECENT_KEY = "nomadro_xhs_recent";
- Array.prototype.forEach.call(root.querySelectorAll('.card'), function (el) {
- el.addEventListener('click', function () {
- var slug = el.getAttribute('data-slug');
- var next = favs.slice();
- var i = next.indexOf(slug);
- if (i >= 0) next.splice(i, 1); else next.push(slug);
- window.NomadroXhs.setLocalData(favKey, next).then(function () { render(next); });
+ var state = {
+ tab: "home",
+ stack: [],
+ favs: [],
+ plan: [],
+ checklist: {},
+ recent: [],
+ region: "all",
+ lifeIdx: 0,
+ compareA: "",
+ compareB: "",
+ matchBudget: 6000,
+ matchClimate: "any",
+ matchRegion: "all",
+ };
+
+ var main = document.getElementById("main");
+ var topBrand = document.getElementById("top-brand");
+ var topSub = document.getElementById("top-sub");
+ var btnBack = document.getElementById("btn-back");
+ var tabbar = document.getElementById("tabbar");
+ var sheet = document.getElementById("sheet");
+ var sheetBody = document.getElementById("sheet-body");
+
+ function on(el, ev, fn) {
+ if (el) el.addEventListener(ev, fn);
+ }
+
+ function esc(s) {
+ return String(s == null ? "" : s)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+ }
+
+ function stars(n) {
+ var s = "";
+ var i;
+ for (i = 0; i < 5; i++) s += i < Math.round(n || 0) ? "★" : "☆";
+ return s;
+ }
+
+ function storageGet(key, fallback) {
+ if (Bridge && typeof Bridge.getLocalData === "function") {
+ return Bridge.getLocalData(key).then(function (v) {
+ return v == null ? fallback : v;
});
+ }
+ try {
+ var raw = localStorage.getItem(key);
+ return Promise.resolve(raw ? JSON.parse(raw) : fallback);
+ } catch (e) {
+ return Promise.resolve(fallback);
+ }
+ }
+
+ function storageSet(key, val) {
+ if (Bridge && typeof Bridge.setLocalData === "function") {
+ return Bridge.setLocalData(key, val);
+ }
+ try {
+ localStorage.setItem(key, JSON.stringify(val));
+ return Promise.resolve(true);
+ } catch (e) {
+ return Promise.resolve(false);
+ }
+ }
+
+ function destBySlug(slug) {
+ var list = DATA.destinations || [];
+ var i;
+ for (i = 0; i < list.length; i++) if (list[i].slug === slug) return list[i];
+ return null;
+ }
+
+ function isFav(slug) {
+ return state.favs.indexOf(slug) >= 0;
+ }
+
+ function toggleFav(slug) {
+ var i = state.favs.indexOf(slug);
+ if (i >= 0) state.favs.splice(i, 1);
+ else state.favs.push(slug);
+ storageSet(FAV_KEY, state.favs);
+ render();
+ }
+
+ function pushRecent(slug) {
+ var next = [slug].concat(state.recent.filter(function (s) { return s !== slug; })).slice(0, 8);
+ state.recent = next;
+ storageSet(RECENT_KEY, next);
+ }
+
+ function setChrome(title, sub, showBack) {
+ topBrand.textContent = title || "nomadro";
+ topSub.textContent = sub || "离线镜像";
+ if (showBack) btnBack.classList.remove("is-hidden");
+ else btnBack.classList.add("is-hidden");
+ }
+
+ function goTab(tab) {
+ state.tab = tab;
+ state.stack = [];
+ Array.prototype.forEach.call(tabbar.querySelectorAll(".tab"), function (btn) {
+ btn.classList.toggle("is-active", btn.getAttribute("data-tab") === tab);
+ });
+ render();
+ }
+
+ function openView(view, params) {
+ state.stack.push({ view: view, params: params || {} });
+ render();
+ }
+
+ function goBack() {
+ if (state.stack.length) {
+ state.stack.pop();
+ render();
+ return;
+ }
+ goTab("home");
+ }
+
+ function currentRoute() {
+ if (state.stack.length) return state.stack[state.stack.length - 1];
+ return { view: state.tab, params: {} };
+ }
+
+ function openSheet(html) {
+ sheetBody.innerHTML = html;
+ sheet.classList.remove("is-hidden");
+ }
+
+ function closeSheet() {
+ sheet.classList.add("is-hidden");
+ sheetBody.innerHTML = "";
+ }
+
+ function mdLite(src) {
+ var text = String(src || "");
+ text = esc(text);
+ text = text.replace(/^### (.*)$/gm, "$1 ");
+ text = text.replace(/^## (.*)$/gm, "$1 ");
+ text = text.replace(/\*\*(.+?)\*\*/g, "$1 ");
+ text = text.replace(/^- (.*)$/gm, "$1 ");
+ text = text.replace(/(.*<\/li>\n?)+/g, function (m) {
+ return "";
+ });
+ text = text.replace(/\n\n/g, "");
+ return "
" + text + "
";
+ }
+
+ /* ---------- views ---------- */
+
+ function viewHome() {
+ setChrome("nomadro", "用一行代码环游世界", false);
+ var stats = DATA.stats || {};
+ var dests = (DATA.destinations || []).slice().sort(function (a, b) { return (b.rating || 0) - (a.rating || 0); }).slice(0, 4);
+ var voices = (DATA.testimonials || []).slice(0, 3);
+ var life = DATA.lifestyle || [];
+ var lifeActive = life[state.lifeIdx] || life[0];
+ var ticker = (DATA.ticker || []).join(" · ");
+
+ return (
+ '' +
+ '🌏 全球数字游民正在路上
' +
+ "用一行代码 环游世界 " +
+ "" + esc(DATA.offlineNote || "") + "
" +
+ '' +
+ '从城市开始 ' +
+ '🎯 智能匹配 ' +
+ "
" +
+ " " +
+ (ticker ? '' + esc(ticker) + "
" : "") +
+ '' +
+ '
' + (stats.destinations || "—") + " 目的地
" +
+ '
' + (stats.visas || "—") + " 签证
" +
+ '
' + (stats.meetups || "—") + " 活动
" +
+ '
' + (stats.blog || "—") + " 文章
" +
+ "
" +
+ '
⚡ 三环路径 ' +
+ '
' +
+ '🧭 探索 Explore 目的地 · 签证 · 匹配 ' +
+ '🤝 连接 Connect 活动 · 社区 · 心声 ' +
+ '🌱 成长 Grow 学院 · 博客 · 工具 ' +
+ '🗓️ 旅居计划 离线清单与收藏 ' +
+ "
" +
+ '
🌴 热门目的地 全部 → ' +
+ '
' + dests.map(destCard).join("") + "
" +
+ '
🗓️ 游民一天 ' +
+ '
' +
+ life.map(function (item, idx) {
+ return '' +
+ '' + esc(item.emoji) + " " + esc(item.title) + " ";
+ }).join("") +
+ "
" +
+ (lifeActive ? '
' + esc(lifeActive.body) + "
" : "") +
+ "
" +
+ '
🗣️ 游民心声 更多 → ' +
+ voices.map(function (t) {
+ return '
' + esc(t.avatar || "") + " " + esc(t.content) + '
' +
+ esc(t.author) + " · " + esc(t.role) + " · " + stars(t.rating) + "
";
+ }).join("") +
+ "
"
+ );
+ }
+
+ function destCard(d) {
+ return (
+ '' +
+ '' + esc(d.emoji || "🌍") + "
" +
+ '' + esc(d.name) +
+ (isFav(d.slug) ? '★ ' : "") +
+ '
' + esc(d.country) + " · ¥" + (d.cost || "—") + "/月 · ★" + (d.rating || "—") +
+ '
' + esc(d.tag || d.tagline || "") + "
"
+ );
+ }
+
+ function viewExploreHub() {
+ setChrome("探索", "Discover where to go", false);
+ var rings = (DATA.rings && DATA.rings.explore) || [];
+ return (
+ '探索环:发现去哪。离线可浏览目的地、签证,并做本地智能匹配。
' +
+ '' +
+ rings.map(function (r) {
+ return '' +
+ esc(r.emoji) + " " + esc(r.title) + " " + esc(r.desc) + " ";
+ }).join("") +
+ "
" +
+ '
🗺️ 游民地图快照 ' + mapBoard() + "
"
+ );
+ }
+
+ function mapBoard() {
+ var list = DATA.destinations || [];
+ var pins = list.map(function (d) {
+ var left = Math.max(8, Math.min(92, ((d.map_x || 500) / 900) * 100));
+ var top = Math.max(12, Math.min(88, ((d.map_y || 220) / 400) * 100));
+ return '' + esc(d.emoji || "📍") + "" + esc(d.name) + " ";
+ }).join("");
+ return '' + pins + "
";
+ }
+
+ function viewDestinations() {
+ setChrome("目的地", "Top nomad cities", true);
+ var regions = [
+ { id: "all", label: "全部" },
+ { id: "sea", label: "东南亚" },
+ { id: "europe", label: "欧洲" },
+ { id: "asia", label: "东亚" },
+ { id: "latam", label: "拉美" },
+ ];
+ var list = (DATA.destinations || []).filter(function (d) {
+ return state.region === "all" || d.region === state.region;
+ });
+ return (
+ '' +
+ regions.map(function (r) {
+ return '' + r.label + " ";
+ }).join("") +
+ "
" +
+ '' + list.map(destCard).join("") + "
"
+ );
+ }
+
+ function viewDestination(params) {
+ var d = destBySlug(params.slug);
+ if (!d) return '未找到城市
';
+ pushRecent(d.slug);
+ setChrome(d.name, d.country, true);
+ var fav = isFav(d.slug);
+ return (
+ '' +
+ '
' + esc(d.emoji || "🌍") + "
" +
+ "
" + esc(d.name) + " " +
+ '
' + esc(d.tag) + " · " + esc(d.nomads_count || "") + " 游民
" +
+ "
" + esc(d.description) + "
" +
+ '
' +
+ '' + (fav ? "取消收藏" : "收藏城市") + " " +
+ '加入计划 ' +
+ "
" +
+ "
" +
+ '' +
+ '
月生活费
¥' + (d.cost || "—") + " " +
+ '
网速
' + (d.speed || "—") + " Mbps " +
+ '
均温
' + (d.temperature || "—") + "°C " +
+ '
评分
' + (d.rating || "—") + " " +
+ "
" +
+ '' + (d.highlights || []).map(function (h) { return "" + esc(h) + " "; }).join("") + "
" +
+ '
接下来 ' +
+ '' +
+ '📋 签证指南 查远程友好签证 ' +
+ '🎉 同城活动 先线下遇见 ' +
+ "
"
+ );
+ }
+
+ function viewVisas() {
+ setChrome("签证指南", "Remote-friendly visas", true);
+ return (
+ '离线快照,政策可能变动。出发前请再核对官网。
' +
+ '' +
+ (DATA.visas || []).map(function (v) {
+ return '
' + esc(v.flag || "🛂") + '
' +
+ '
' + esc(v.name) + " · " + esc(v.badge) + "
" +
+ '
' + esc(v.duration) + " · 收入 " + esc(v.income_req) + " · 审批 " + esc(v.approval_time) + "
" +
+ '
' + esc(v.extra) + " · 难度 " + esc(v.difficulty_label) + "
" +
+ "
";
+ }).join("") +
+ "
"
+ );
+ }
+
+ function viewMatcher() {
+ setChrome("智能匹配", "Find your city", true);
+ var scored = scoreDestinations();
+ return (
+ '" +
+ '
推荐结果 ' +
+ scored.slice(0, 6).map(function (item) {
+ var d = item.d;
+ return '
' +
+ '' + esc(d.emoji) + '
' + esc(d.name) +
+ ' · 匹配 ' + item.score + '
¥' + d.cost + "/月 · " + d.speed + "Mbps · ★" + d.rating +
+ "
";
+ }).join("") +
+ "
"
+ );
+ }
+
+ function opt(v, label, cur) {
+ return '" + label + " ";
+ }
+
+ function scoreDestinations() {
+ var budget = Number(state.matchBudget) || 999999;
+ return (DATA.destinations || []).map(function (d) {
+ var score = 50;
+ if (d.cost <= budget) score += 25;
+ else score -= Math.min(30, (d.cost - budget) / 400);
+ if (state.matchRegion !== "all" && d.region !== state.matchRegion) score -= 40;
+ var t = d.temperature || 20;
+ if (state.matchClimate === "warm" && t >= 24) score += 15;
+ if (state.matchClimate === "mild" && t >= 16 && t < 24) score += 15;
+ if (state.matchClimate === "cool" && t <= 16) score += 15;
+ score += (d.rating || 0) * 3;
+ score += Math.min(15, (d.speed || 0) / 15);
+ return { d: d, score: Math.round(score) };
+ }).sort(function (a, b) { return b.score - a.score; });
+ }
+
+ function viewCompare() {
+ setChrome("城市对比", "Side by side", true);
+ var list = DATA.destinations || [];
+ if (!state.compareA && list[0]) state.compareA = list[0].slug;
+ if (!state.compareB && list[1]) state.compareB = list[1].slug;
+ var a = destBySlug(state.compareA) || list[0];
+ var b = destBySlug(state.compareB) || list[1];
+ var options = list.map(function (d) {
+ return '' + esc(d.name) + " ";
+ }).join("");
+ return (
+ '' +
+ '' + options.replace('value="' + esc(a.slug) + '"', 'value="' + esc(a.slug) + '" selected') + " " +
+ '' + options.replace('value="' + esc(b.slug) + '"', 'value="' + esc(b.slug) + '" selected') + " " +
+ "
" +
+ '' +
+ row("城市", (a.emoji || "") + " " + a.name, (b.emoji || "") + " " + b.name) +
+ row("国家", a.country, b.country) +
+ row("月生活费", "¥" + a.cost, "¥" + b.cost) +
+ row("网速", a.speed + " Mbps", b.speed + " Mbps") +
+ row("均温", a.temperature + "°C", b.temperature + "°C") +
+ row("评分", a.rating, b.rating) +
+ row("游民规模", a.nomads_count, b.nomads_count) +
+ "
"
+ );
+ }
+
+ function row(k, a, b) {
+ return "" + esc(k) + " " + esc(a) + " " + esc(b) + " ";
+ }
+
+ function viewConnectHub() {
+ setChrome("连接", "Meet fellow nomads", false);
+ var rings = (DATA.rings && DATA.rings.connect) || [];
+ return (
+ '连接环:遇见同行。离线可浏览活动与讨论;报名/发帖请到网页版。
' +
+ '' +
+ rings.map(function (r) {
+ return '' +
+ esc(r.emoji) + " " + esc(r.title) + " " + esc(r.desc) + " ";
+ }).join("") +
+ "
"
+ );
+ }
+
+ function viewMeetups() {
+ setChrome("游民活动", "Events snapshot", true);
+ return (
+ '' +
+ (DATA.meetups || []).map(function (m) {
+ return '
' + esc(m.emoji || "🎉") + '
' +
+ '
' + esc(m.title) + "
" +
+ '
' + esc(m.city) + " · " + esc(m.date) + " " + esc(m.time || "") + " · " +
+ esc(m.venue || "") + "
" +
+ '
' + esc(m.description || "") + "
" +
+ '
' + (m.rsvp_count || 0) + "/" + (m.max_attendees || "—") + " · " +
+ esc((m.tags || []).join(" · ")) + "
" +
+ "
";
+ }).join("") +
+ "
"
+ );
+ }
+
+ function viewCommunity() {
+ setChrome("社区讨论", "Offline threads", true);
+ return (
+ '' +
+ (DATA.discussions || []).map(function (d) {
+ return '
' +
+ '' + esc(d.author_emoji || "💬") + '
' +
+ '
' + (d.is_pinned ? "📌 " : "") + esc(d.title) + "
" +
+ '
' + esc(d.excerpt) + "
" +
+ '
' + esc(d.author) + " · " + (d.reply_count || 0) + " 回复 · ♥ " + (d.like_count || 0) + "
" +
+ "
";
+ }).join("") +
+ "
"
+ );
+ }
+
+ function viewDiscussion(params) {
+ var list = DATA.discussions || [];
+ var d = null;
+ var i;
+ for (i = 0; i < list.length; i++) if (list[i].id === params.id) d = list[i];
+ if (!d) return '讨论不存在
';
+ setChrome("讨论", d.category || "community", true);
+ var detail = (DATA.discussionDetails && DATA.discussionDetails[d.id]) || null;
+ var replies = (detail && detail.replies) || [];
+ return (
+ '' +
+ "
" + esc(d.title) + " " +
+ '
' + esc(d.author) + " · " + esc(d.created_at || "") + "
" +
+ "
" + esc(d.excerpt) + "
" +
+ "
" +
+ (replies.length
+ ? '
回复 ' +
+ replies.map(function (r) {
+ return '
' + esc(r.content || r.body || "") + '
' +
+ esc(r.author || "") + "
";
+ }).join("") + "
"
+ : '离线包仅含帖子摘要。完整回复请到网页版社区。
')
+ );
+ }
+
+ function viewVoices() {
+ setChrome("游民心声", "Real stories", true);
+ return (DATA.testimonials || []).map(function (t) {
+ return '' + esc(t.avatar || "") + " " + esc(t.content) + '
' +
+ esc(t.author) + " · " + esc(t.role) + " · " + stars(t.rating) + "
";
+ }).join("");
+ }
+
+ function viewGrowHub() {
+ setChrome("成长", "Learn on the road", false);
+ var rings = (DATA.rings && DATA.rings.grow) || [];
+ return (
+ '成长环:路上做事。学院、博客与工具均可离线浏览。
' +
+ '' +
+ rings.map(function (r) {
+ return '' +
+ esc(r.emoji) + " " + esc(r.title) + " " + esc(r.desc) + " ";
+ }).join("") +
+ "
"
+ );
+ }
+
+ function viewDigital() {
+ setChrome("游民学院", "Digital academy", true);
+ var modules = DATA.course || [];
+ return modules.map(function (mod, mi) {
+ return '
' + esc(mod.title) + " " +
+ (mod.lessons || []).map(function (les, li) {
+ var key = mi + "-" + li;
+ return '
' +
+ '' + (les.free ? "🆓" : "🔒") + '
' +
+ '
' + esc(les.title) + "
" +
+ '
' + esc(les.duration) + (les.free ? " · 免费" : " · 网页版 VIP") + "
" +
+ "
";
+ }).join("") + "
";
+ }).join("");
+ }
+
+ function viewLesson(params) {
+ var lesson = (DATA.lessons || {})[params.key];
+ if (!lesson) return '课程不存在
';
+ setChrome(lesson.title, "Lesson", true);
+ return (
+ '' +
+ "
" + esc(lesson.title) + " " +
+ '
' + esc(lesson.duration) + (lesson.free ? " · 免费" : " · VIP 内容离线摘要") + "
" +
+ "
" +
+ '' + esc(lesson.content || "") + "
"
+ );
+ }
+
+ function viewBlog() {
+ setChrome("博客", "Guides & stories", true);
+ return (
+ '' +
+ (DATA.blog || []).map(function (b) {
+ return '
' +
+ '' + esc(b.emoji || "📝") + '
' +
+ '
' + esc(b.title) + "
" +
+ '
' + esc(b.excerpt) + "
" +
+ '
' + esc(b.author) + " · " + esc(b.published_at) + " · " + (b.read_time || "?") + " 分钟
" +
+ "
";
+ }).join("") +
+ "
"
+ );
+ }
+
+ function viewBlogDetail(params) {
+ var list = DATA.blog || [];
+ var b = null;
+ var i;
+ for (i = 0; i < list.length; i++) if (list[i].slug === params.slug) b = list[i];
+ if (!b) return '文章不存在
';
+ setChrome("博客", b.slug, true);
+ var content = (DATA.blogContent && DATA.blogContent[b.slug]) || b.excerpt || "";
+ return (
+ '' +
+ '
' + esc(b.emoji || "📝") + "
" +
+ "
" + esc(b.title) + " " +
+ '
' + esc(b.author) + " · " + esc(b.published_at) + " · " +
+ (b.read_time || "?") + " 分钟 · " + esc((b.tags || []).join(" · ")) + "
" +
+ "
" +
+ '' + mdLite(content) + " "
+ );
+ }
+
+ function viewTools() {
+ setChrome("工具箱", "Offline utilities", true);
+ return (
+ '
实用工具 ' +
+ '
' +
+ '🧮 月费用估算 住宿+餐饮+交通 ' +
+ '✅ 落地清单 第一月 checklist ' +
+ "
" +
+ '
推荐软件栈 ' +
+ (DATA.tools || []).map(function (t) {
+ return '
' + esc(t.emoji) + '
' +
+ '
' + esc(t.name) + "
" +
+ '
' + esc(t.description) + "
" +
+ '
' + esc((t.tags || []).join(" · ")) + "
";
+ }).join("") +
+ "
"
+ );
+ }
+
+ function viewCalc() {
+ setChrome("月费用估算", "Cost calculator", true);
+ return (
+ '"
+ );
+ }
+
+ function viewFaq() {
+ setChrome("常见问题", "FAQ", true);
+ return (DATA.faqs || []).map(function (f) {
+ return '' + esc(f.question) + "
" +
+ esc(f.answer) + "
";
+ }).join("");
+ }
+
+ function viewPlan() {
+ setChrome("旅居计划", "Offline plan", true);
+ var checks = DATA.planChecklist || [];
+ var planCities = state.plan.map(function (slug) {
+ var d = destBySlug(slug);
+ if (!d) return "";
+ return destCard(d);
+ }).join("");
+ return (
+ '计划与清单仅保存在本机(小红书 Storage / localStorage)。
' +
+ '
计划中的城市 ' +
+ (planCities || '
还没有城市,去目的地页点「加入计划」
') +
+ "
" +
+ '
落地清单 ' +
+ checks.map(function (c) {
+ var on = !!state.checklist[c.id];
+ return '' +
+ '' + (on ? "✓" : "") + " " + esc(c.label) + " ";
+ }).join("") +
+ "
"
+ );
+ }
+
+ function viewMine() {
+ setChrome("我的", "Favorites & plan", false);
+ var favCards = state.favs.map(function (slug) {
+ var d = destBySlug(slug);
+ return d ? destCard(d) : "";
+ }).join("");
+ var recent = state.recent.map(function (slug) {
+ var d = destBySlug(slug);
+ return d ? destCard(d) : "";
+ }).join("");
+ return (
+ '' + esc(DATA.offlineNote || "") + "
" +
+ '' +
+ '🗓️ 旅居计划 清单与城市 ' +
+ '🎯 智能匹配 重算推荐 ' +
+ "
" +
+ '
我的收藏 ' +
+ (favCards || '
暂无收藏
') + "
" +
+ '
最近浏览 ' +
+ (recent || '
还没有浏览记录
') + "
" +
+ '
关于 ' +
+ '
品牌 nomadro — 数字游民旅居平台离线镜像。
' +
+ '
完整功能:' + esc(DATA.site || "https://nomadweb.nomadro.com") + "
"
+ );
+ }
+
+ function render() {
+ var route = currentRoute();
+ var html = "";
+ switch (route.view) {
+ case "home": html = viewHome(); break;
+ case "explore": html = viewExploreHub(); break;
+ case "connect": html = viewConnectHub(); break;
+ case "grow": html = viewGrowHub(); break;
+ case "mine": html = viewMine(); break;
+ case "destinations": html = viewDestinations(); break;
+ case "destination": html = viewDestination(route.params); break;
+ case "visas": html = viewVisas(); break;
+ case "matcher": html = viewMatcher(); break;
+ case "compare": html = viewCompare(); break;
+ case "meetups": html = viewMeetups(); break;
+ case "community": html = viewCommunity(); break;
+ case "discussion": html = viewDiscussion(route.params); break;
+ case "voices": html = viewVoices(); break;
+ case "digital": html = viewDigital(); break;
+ case "lesson": html = viewLesson(route.params); break;
+ case "blog": html = viewBlog(); break;
+ case "blog-detail": html = viewBlogDetail(route.params); break;
+ case "tools": html = viewTools(); break;
+ case "calc": html = viewCalc(); break;
+ case "faq": html = viewFaq(); break;
+ case "plan": html = viewPlan(); break;
+ default: html = viewHome();
+ }
+ main.innerHTML = html;
+ bindDynamic();
+ window.scrollTo(0, 0);
+ }
+
+ function bindDynamic() {
+ Array.prototype.forEach.call(main.querySelectorAll("[data-go]"), function (el) {
+ on(el, "click", function () { goTab(el.getAttribute("data-go")); });
+ });
+ Array.prototype.forEach.call(main.querySelectorAll("[data-open]"), function (el) {
+ on(el, "click", function () {
+ var view = el.getAttribute("data-open");
+ var params = {};
+ if (el.getAttribute("data-slug")) params.slug = el.getAttribute("data-slug");
+ if (el.getAttribute("data-id")) params.id = el.getAttribute("data-id");
+ if (el.getAttribute("data-key")) params.key = el.getAttribute("data-key");
+ openView(view, params);
+ });
+ });
+ Array.prototype.forEach.call(main.querySelectorAll("[data-region]"), function (el) {
+ on(el, "click", function () {
+ state.region = el.getAttribute("data-region");
+ render();
+ });
+ });
+ Array.prototype.forEach.call(main.querySelectorAll("[data-life]"), function (el) {
+ on(el, "click", function () {
+ state.lifeIdx = Number(el.getAttribute("data-life")) || 0;
+ render();
+ });
+ });
+ Array.prototype.forEach.call(main.querySelectorAll("[data-fav]"), function (el) {
+ on(el, "click", function () { toggleFav(el.getAttribute("data-fav")); });
+ });
+ Array.prototype.forEach.call(main.querySelectorAll("[data-plan-add]"), function (el) {
+ on(el, "click", function () {
+ var slug = el.getAttribute("data-plan-add");
+ if (state.plan.indexOf(slug) < 0) state.plan.push(slug);
+ storageSet(PLAN_KEY, state.plan);
+ openSheet("已加入旅居计划。 可在「我的 → 旅居计划」查看。
" +
+ '好的 ');
+ on(document.getElementById("sheet-ok"), "click", closeSheet);
+ });
+ });
+ Array.prototype.forEach.call(main.querySelectorAll("[data-check]"), function (el) {
+ on(el, "click", function () {
+ var id = el.getAttribute("data-check");
+ state.checklist[id] = !state.checklist[id];
+ storageSet(CHECK_KEY, state.checklist);
+ render();
+ });
+ });
+
+ var mRun = document.getElementById("m-run");
+ if (mRun) {
+ on(mRun, "click", function () {
+ var b = document.getElementById("m-budget");
+ var c = document.getElementById("m-climate");
+ var r = document.getElementById("m-region");
+ state.matchBudget = Number(b && b.value) || 6000;
+ state.matchClimate = (c && c.value) || "any";
+ state.matchRegion = (r && r.value) || "all";
+ render();
+ });
+ }
+
+ var cmpA = document.getElementById("cmp-a");
+ var cmpB = document.getElementById("cmp-b");
+ if (cmpA) on(cmpA, "change", function () { state.compareA = cmpA.value; render(); });
+ if (cmpB) on(cmpB, "change", function () { state.compareB = cmpB.value; render(); });
+
+ var cRun = document.getElementById("c-run");
+ if (cRun) {
+ on(cRun, "click", function () {
+ function n(id) { var el = document.getElementById(id); return Number(el && el.value) || 0; }
+ var total = n("c-house") + n("c-food") + n("c-trans") + n("c-cowork") + n("c-other");
+ var out = document.getElementById("c-out");
+ if (out) out.textContent = "预估月开销约 ¥" + total + "(离线估算,仅供参考)";
+ });
+ }
+ }
+
+ function bindChrome() {
+ Array.prototype.forEach.call(tabbar.querySelectorAll(".tab"), function (btn) {
+ on(btn, "click", function () { goTab(btn.getAttribute("data-tab")); });
+ });
+ on(btnBack, "click", goBack);
+ on(document.getElementById("btn-offline"), "click", function () {
+ openSheet(
+ "离线说明 " +
+ "" + esc(DATA.offlineNote || "") + "
" +
+ '知道了 '
+ );
+ on(document.getElementById("sheet-ok"), "click", closeSheet);
+ });
+ on(document.getElementById("sheet-mask"), "click", closeSheet);
+ }
+
+ function boot() {
+ bindChrome();
+ Promise.all([
+ storageGet(FAV_KEY, []),
+ storageGet(PLAN_KEY, []),
+ storageGet(CHECK_KEY, {}),
+ storageGet(RECENT_KEY, []),
+ ]).then(function (vals) {
+ state.favs = Array.isArray(vals[0]) ? vals[0] : [];
+ state.plan = Array.isArray(vals[1]) ? vals[1] : [];
+ state.checklist = vals[2] && typeof vals[2] === "object" ? vals[2] : {};
+ state.recent = Array.isArray(vals[3]) ? vals[3] : [];
+ render();
});
}
- window.NomadroXhs.getLocalData(favKey).then(function (favs) {
- render(Array.isArray(favs) ? favs : []);
- });
+ boot();
})();
diff --git a/redmini/tool/data.js b/redmini/tool/data.js
new file mode 100644
index 0000000..1d0587b
--- /dev/null
+++ b/redmini/tool/data.js
@@ -0,0 +1 @@
+window.NOMADRO_DATA = {"brand": "nomadro", "tagline": "用一行代码环游世界", "offlineNote": "小红书小程序无法联网。本包为官网内容离线镜像,收藏/计划仅保存在本机。完整互动请打开 nomadweb.nomadro.com", "site": "https://nomadweb.nomadro.com", "stats": {"destinations": 12, "visas": 12, "meetups": 12, "blog": 10, "faqs": 6}, "ticker": ["🌏 全球数字游民正在路上", "📋 签证政策实时整理(离线快照)", "🎉 同城活动与社区讨论可先浏览", "🎓 游民学院免费课时可学"], "rings": {"explore": [{"id": "destinations", "emoji": "🌴", "title": "目的地", "desc": "发现下一座城市"}, {"id": "visas", "emoji": "📋", "title": "签证指南", "desc": "远程友好签证"}, {"id": "matcher", "emoji": "🎯", "title": "智能匹配", "desc": "按预算气候排序"}, {"id": "compare", "emoji": "⚖️", "title": "城市对比", "desc": "并排看成本网速"}], "connect": [{"id": "meetups", "emoji": "🎉", "title": "游民活动", "desc": "同城与线上"}, {"id": "community", "emoji": "💬", "title": "社区讨论", "desc": "签证住宿经验"}, {"id": "voices", "emoji": "🗣️", "title": "游民心声", "desc": "真实旅居评价"}], "grow": [{"id": "digital", "emoji": "🎓", "title": "游民学院", "desc": "远程工作课程"}, {"id": "blog", "emoji": "📝", "title": "博客", "desc": "指南与攻略"}, {"id": "tools", "emoji": "🧰", "title": "工具箱", "desc": "费用估算等"}, {"id": "faq", "emoji": "❓", "title": "常见问题", "desc": "启动资金与保险"}]}, "lifestyle": [{"key": "morning", "emoji": "🌅", "title": "清晨", "body": "咖啡馆开机,时区对齐,列出今日三件交付。"}, {"key": "work", "emoji": "💻", "title": "深度工作", "body": "联合办公或家中安静角落,异步协作优先。"}, {"key": "explore", "emoji": "🚶", "title": "探索城市", "body": "傍晚散步、夜市与同城 meetup。"}, {"key": "collab", "emoji": "🤝", "title": "协作", "body": "和同行交换签证与住宿情报。"}, {"key": "social", "emoji": "🌙", "title": "社交", "body": "社区圆桌、兴趣局,或安静充电。"}], "planChecklist": [{"id": "visa", "label": "确认签证/停留天数"}, {"id": "flight", "label": "预订机票"}, {"id": "housing", "label": "首周短租/酒店"}, {"id": "sim", "label": "当地 SIM / eSIM"}, {"id": "cowork", "label": "踩点联合办公"}, {"id": "insurance", "label": "国际医疗保险"}, {"id": "budget", "label": "写下月预算"}, {"id": "meetup", "label": "报名一场同城活动"}], "destinations": [{"id": "1", "slug": "bali", "name": "巴厘岛", "country": "印尼", "emoji": "🏝️", "tag": "东南亚 · 热带天堂", "description": "乌布的数字游民社区闻名全球,稻田间的 Co-working Space 和瑜伽文化让这里成为游民圣地。", "region": "sea", "cost": 4500, "speed": 85, "temperature": 28, "rating": 9.2, "hue": 170, "nomads_count": "12,000+", "highlights": ["🏄 冲浪与海滩生活", "🧘 瑜伽冥想文化", "💰 东南亚性价比之王", "🌴 热带气候全年温暖"], "map_x": 720, "map_y": 310}, {"id": "2", "slug": "lisbon", "name": "里斯本", "country": "葡萄牙", "emoji": "🌊", "tag": "欧洲 · 海滨明珠", "description": "D7 签证友好,阳光海岸与悠久历史的完美融合,欧洲数字游民的首选基地。", "region": "europe", "cost": 9000, "speed": 120, "temperature": 22, "rating": 9.5, "hue": 220, "nomads_count": "8,500+", "highlights": ["📋 D7 签证门槛低", "☀️ 300天阳光", "🎵 Fado 音乐文化", "🚋 复古有轨电车"], "map_x": 430, "map_y": 195}, {"id": "3", "slug": "chiangmai", "name": "清迈", "country": "泰国", "emoji": "🏔️", "tag": "东南亚 · 文化古城", "description": "数字游民大本营,咖啡文化与夜市生活的天堂,全球性价比最高的游民城市。", "region": "sea", "cost": 3800, "speed": 95, "temperature": 30, "rating": 9.4, "hue": 45, "nomads_count": "15,000+", "highlights": ["☕ 咖啡馆文化浓厚", "🏮 夜市与寺庙", "💰 月生活费最低", "🤝 游民社区最活跃"], "map_x": 700, "map_y": 240}, {"id": "4", "slug": "mexico", "name": "墨西哥城", "country": "墨西哥", "emoji": "🌃", "tag": "拉美 · 活力之都", "description": "艺术、美食与科技交织,时区便利对接北美市场,拉美最具活力的游民城市。", "region": "latam", "cost": 6500, "speed": 75, "temperature": 18, "rating": 8.8, "hue": 300, "nomads_count": "5,200+", "highlights": ["🎨 街头艺术天堂", "🌮 世界美食之都", "🕐 北美时区友好", "💃 丰富夜生活"], "map_x": 220, "map_y": 240}, {"id": "5", "slug": "barcelona", "name": "巴塞罗那", "country": "西班牙", "emoji": "🏖️", "tag": "欧洲 · 地中海", "description": "高迪建筑与创业生态并存,Nomad Visa 政策领先,地中海生活的理想之选。", "region": "europe", "cost": 10500, "speed": 150, "temperature": 20, "rating": 9.1, "hue": 130, "nomads_count": "6,800+", "highlights": ["🏛️ 高迪建筑奇迹", "🏖️ 地中海海滩", "📋 Nomad Visa 便利", "🍷 美食与夜生活"], "map_x": 460, "map_y": 200}, {"id": "6", "slug": "tokyo", "name": "东京", "country": "日本", "emoji": "🗼", "tag": "亚洲 · 现代都市", "description": "极致效率与安全,适合追求高品质生活的远程工作者,亚洲科技之都。", "region": "asia", "cost": 12000, "speed": 200, "temperature": 15, "rating": 8.6, "hue": 10, "nomads_count": "4,100+", "highlights": ["🚄 极致公共交通", "🛡️ 全球最安全城市", "📶 网速亚洲第一", "🍣 美食文化巅峰"], "map_x": 820, "map_y": 210}, {"id": "7", "slug": "berlin", "name": "柏林", "country": "德国", "emoji": "🎨", "tag": "欧洲 · 创意之都", "description": "开放的创业与开源氛围,租金相对西欧友好,夜生活与艺术场景极具吸引力。", "region": "europe", "cost": 9800, "speed": 140, "temperature": 12, "rating": 8.9, "hue": 250, "nomads_count": "7,200+", "highlights": ["🎨 艺术与街头文化", "💻 创业生态活跃", "🚲 骑行友好", "🍺 社交场景丰富"], "map_x": 500, "map_y": 160}, {"id": "8", "slug": "dali", "name": "大理", "country": "中国", "emoji": "🏔️", "tag": "国内 · 慢生活", "description": "苍山洱海之间的内容创作者聚集地,生活节奏慢、社交松弛,适合深度创作与试住。", "region": "asia", "cost": 4200, "speed": 80, "temperature": 18, "rating": 8.7, "hue": 190, "nomads_count": "3,600+", "highlights": ["📷 内容创作友好", "🏞️ 自然风光", "🍵 咖啡馆密集", "🏡 合租成本低"], "map_x": 730, "map_y": 220}, {"id": "9", "slug": "seoul", "name": "首尔", "country": "韩国", "emoji": "🏙️", "tag": "亚洲 · 潮流都市", "description": "高速网络与深夜城市活力并存,适合对接东亚市场与产品迭代。", "region": "asia", "cost": 11000, "speed": 220, "temperature": 14, "rating": 8.5, "hue": 330, "nomads_count": "3,200+", "highlights": ["📶 全球顶尖网速", "🍜 美食与便利店文化", "🛍️ 潮流消费", "🚇 地铁覆盖极强"], "map_x": 800, "map_y": 200}, {"id": "10", "slug": "medellin", "name": "麦德林", "country": "哥伦比亚", "emoji": "🌺", "tag": "拉美 · 春城", "description": "四季如春、生活成本友好,英语游民社区成熟,适合北美时区远程工作。", "region": "latam", "cost": 5200, "speed": 90, "temperature": 23, "rating": 9.0, "hue": 140, "nomads_count": "6,400+", "highlights": ["🌤️ 四季如春", "💰 高性价比", "🕐 北美时区", "🤝 社区活动密集"], "map_x": 260, "map_y": 300}, {"id": "11", "slug": "dubai", "name": "迪拜", "country": "阿联酋", "emoji": "🏙️", "tag": "中东 · 枢纽都市", "description": "免税与签证便利吸引远程高收入人群,国际航班枢纽,适合中东/欧亚跳板。", "region": "asia", "cost": 14000, "speed": 180, "temperature": 32, "rating": 8.4, "hue": 40, "nomads_count": "4,800+", "highlights": ["🧾 税务友好", "✈️ 航线枢纽", "🏗️ 现代基础设施", "🌐 多元国际社区"], "map_x": 600, "map_y": 240}, {"id": "12", "slug": "tbilisi", "name": "第比利斯", "country": "格鲁吉亚", "emoji": "🍷", "tag": "欧亚 · 税务友好", "description": "1% 小企业税与远程友好签证,旧城氛围浓厚,东欧游民新基地。", "region": "europe", "cost": 4800, "speed": 70, "temperature": 16, "rating": 8.8, "hue": 15, "nomads_count": "5,100+", "highlights": ["🧾 1% 小企业税", "🏰 旧城与温泉", "🍷 葡萄酒文化", "💰 低生活成本"], "map_x": 560, "map_y": 190}], "visas": [{"id": "1", "country": "葡萄牙", "flag": "🇵🇹", "name": "葡萄牙 D7 签证", "badge": "⭐ 推荐", "badge_type": "easy", "duration": "2年,可续签", "income_req": "€760/月", "approval_time": "3-6 个月", "extra": "🏥 可享欧盟医疗", "difficulty": 35, "difficulty_label": "简单"}, {"id": "2", "country": "西班牙", "flag": "🇪🇸", "name": "西班牙 Nomad Visa", "badge": "🔥 热门", "badge_type": "hot", "duration": "1年,可续3年", "income_req": "€2,160/月", "approval_time": "1-3 个月", "extra": "🌍 可申根区旅行", "difficulty": 45, "difficulty_label": "中等"}, {"id": "3", "country": "印尼", "flag": "🇮🇩", "name": "印尼 B211A 签证", "badge": "💰 低成本", "badge_type": "budget", "duration": "60天,可延期", "income_req": "约 ¥2,000", "approval_time": "5-10 天", "extra": "🏝️ 适合巴厘岛旅居", "difficulty": 25, "difficulty_label": "简单"}, {"id": "4", "country": "泰国", "flag": "🇹🇭", "name": "泰国 LTR 签证", "badge": "🆕 新政策", "badge_type": "new", "duration": "10年", "income_req": "$80,000/年", "approval_time": "1-2 个月", "extra": "✈️ 多次入境", "difficulty": 60, "difficulty_label": "中等"}, {"id": "5", "country": "墨西哥", "flag": "🇲🇽", "name": "墨西哥 Temporary Resident", "badge": "💰 低成本", "badge_type": "budget", "duration": "1-4年", "income_req": "$2,500/月", "approval_time": "2-4 周", "extra": "🌮 北美时区友好", "difficulty": 30, "difficulty_label": "简单"}, {"id": "6", "country": "爱沙尼亚", "flag": "🇪🇪", "name": "爱沙尼亚 DNV", "badge": "🚀 先锋", "badge_type": "pioneer", "duration": "1年", "income_req": "€3,504/月", "approval_time": "2-4 周", "extra": "💻 全球首个数字游民签证", "difficulty": 40, "difficulty_label": "中等"}, {"id": "7", "country": "格鲁吉亚", "flag": "🇬🇪", "name": "格鲁吉亚 Remotely from Georgia", "badge": "⭐ 推荐", "badge_type": "easy", "duration": "1年", "income_req": "$2,000/月", "approval_time": "约 10 天", "extra": "🧾 可搭配小企业 1% 税", "difficulty": 28, "difficulty_label": "简单"}, {"id": "8", "country": "阿联酋", "flag": "🇦🇪", "name": "迪拜 Virtual Working Programme", "badge": "🔥 热门", "badge_type": "hot", "duration": "1年", "income_req": "$3,500/月", "approval_time": "2-4 周", "extra": "✈️ 中东航线枢纽", "difficulty": 42, "difficulty_label": "中等"}, {"id": "9", "country": "哥伦比亚", "flag": "🇨🇴", "name": "哥伦比亚 V Visa (Digital Nomad)", "badge": "💰 低成本", "badge_type": "budget", "duration": "最长 2 年", "income_req": "约 3× 最低工资", "approval_time": "2-6 周", "extra": "🌺 适合麦德林春城", "difficulty": 32, "difficulty_label": "简单"}, {"id": "10", "country": "韩国", "flag": "🇰🇷", "name": "韩国 Workation Visa (C-3-8)", "badge": "🆕 新政策", "badge_type": "new", "duration": "最长 2 年", "income_req": "约 $65,000/年", "approval_time": "1-2 个月", "extra": "🏙️ 对接首尔与济州", "difficulty": 55, "difficulty_label": "中等"}, {"id": "11", "country": "德国", "flag": "🇩🇪", "name": "德国 Freelance / Self-employment", "badge": "🚀 先锋", "badge_type": "pioneer", "duration": "1-3年", "income_req": "需商业计划与客户证明", "approval_time": "2-4 个月", "extra": "🎨 适合柏林自由职业", "difficulty": 65, "difficulty_label": "较难"}, {"id": "12", "country": "泰国", "flag": "🇹🇭", "name": "泰国 DTV 签证", "badge": "⭐ 推荐", "badge_type": "easy", "duration": "最长 5 年(多次入境)", "income_req": "约 50 万泰铢存款证明", "approval_time": "2-6 周", "extra": "🤝 远程工作友好", "difficulty": 38, "difficulty_label": "中等"}], "faqs": [{"id": "1", "question": "💰 做数字游民需要多少启动资金?", "order": 1, "answer": "建议准备 3-6 个月的生活费作为缓冲。以东南亚为例,¥15,000-30,000 即可开始。包括机票、首月住宿、签证费用和应急资金。欧洲目的地建议准备 ¥50,000 以上。"}, {"id": "2", "question": "📶 如何确保远程工作的网络稳定?", "order": 2, "answer": "选择网络评分高的城市,入住前用 Speedtest 测试。备用方案:本地 SIM 卡热点、随身 WiFi 设备、附近 Co-working Space。推荐携带 USB 网卡和 VPN 作为双保险。"}, {"id": "3", "question": "🏥 旅居期间的保险怎么办?", "order": 3, "answer": "推荐 SafetyWing 或 World Nomads 等国际医疗保险,月费约 $40-80,覆盖全球(部分国家除外)。长期旅居者可考虑目的地国家的本地保险,费用更低、报销更方便。"}, {"id": "4", "question": "🧾 税务问题如何处理?", "order": 4, "answer": "税务居民身份取决于居住天数(通常 183 天规则)。建议咨询专业税务顾问,了解双重征税协定。很多游民选择税务友好的国家(如葡萄牙、格鲁吉亚)作为基地。"}, {"id": "5", "question": "👨👩👧 可以带娃一起做数字游民吗?", "order": 5, "answer": "完全可以!巴厘岛、清迈、里斯本都有成熟的数字游民家庭社区。关键是选择教育资源丰富、医疗条件好的目的地,以及保持稳定的工作节奏,给孩子规律的生活。"}, {"id": "6", "question": "🤝 如何快速融入当地游民社区?", "order": 6, "answer": "加入 Nomad List、Facebook 群组和本地 Meetup 活动。入住游民友好的 Co-living 空间,参加每周的 Coworking 社交日。大部分游民社区非常开放,主动打招呼就能结识朋友。"}], "testimonials": [{"id": "1", "avatar": "👩💻", "content": "在清迈住了 8 个月,月花费不到 4000 元,但生活质量比国内一线城市高太多了。每天早上骑摩托去咖啡馆,这种感觉无法形容。", "author": "小林", "role": "前端开发 · 清迈 🇹🇭", "rating": 5}, {"id": "2", "avatar": "👨🎨", "content": "里斯本的 D7 签证让我在欧洲有了基地。白天在 Alfama 区的共享办公空间工作,周末去 Sintra 徒步,完美平衡。", "author": "Marco", "role": "UI 设计师 · 里斯本 🇵🇹", "rating": 5}, {"id": "3", "avatar": "🧑💼", "content": "带着家人做数字游民听起来疯狂,但在巴厘岛乌布,孩子们上国际学校,我和妻子远程工作,这是我们做过最正确的决定。", "author": "张家", "role": "产品经理 · 巴厘岛 🇮🇩", "rating": 5}, {"id": "4", "avatar": "👩🎨", "content": "麦德林四季如春,英语社区很活跃。我用北美时区接单,下午还能去公园运动,效率反而更高。", "author": "Sofia", "role": "自由职业设计 · 麦德林 🇨🇴", "rating": 5}, {"id": "5", "avatar": "👨💻", "content": "第比利斯生活成本低,还能开本地公司走 1% 税。旧城咖啡馆写代码,周末去卡兹别吉徒步,节奏很舒服。", "author": "Devon", "role": "DevOps · 第比利斯 🇬🇪", "rating": 5}, {"id": "6", "avatar": "📷", "content": "大理适合慢创作。合租便宜、风景治愈,但要主动找同频伙伴,别只待在民宿里。", "author": "Lina", "role": "内容创作者 · 大理 🇨🇳", "rating": 4}, {"id": "7", "avatar": "🧑🚀", "content": "柏林创业氛围强,开源 meetup 特别多。冬天冷、德语门槛真实存在,但找到圈子后很值得。", "author": "Ken", "role": "数据分析 · 柏林 🇩🇪", "rating": 4}, {"id": "8", "avatar": "🏙️", "content": "迪拜当跳板很香:航班方便、签证路径清晰。夏天太热,建议把深度工作时间放在室内联合办公。", "author": "Omar", "role": "增长顾问 · 迪拜 🇦🇪", "rating": 4}], "tools": [{"id": "1", "emoji": "💼", "name": "远程协作", "description": "Slack · Notion · Figma · Zoom", "tags": ["团队", "设计", "沟通"], "category": "work"}, {"id": "2", "emoji": "✈️", "name": "旅行规划", "description": "Skyscanner · Nomad List · SafetyWing", "tags": ["机票", "签证", "保险"], "category": "travel"}, {"id": "3", "emoji": "💳", "name": "财务管理", "description": "Wise · Revolut · Xero · 多币种账户", "tags": ["汇款", "记账", "税务"], "category": "finance"}, {"id": "4", "emoji": "🤝", "name": "社群网络", "description": "Nomad List · Remote Year · 本地 Meetup", "tags": ["社交", "活动", "合租"], "category": "connect"}, {"id": "5", "emoji": "🏥", "name": "健康保障", "description": "SafetyWing · World Nomads · 运动 App", "tags": ["保险", "健身", "心理"], "category": "health"}, {"id": "6", "emoji": "📚", "name": "持续学习", "description": "Coursera · Duolingo · 当地语言班", "tags": ["技能", "语言", "文化"], "category": "learn"}], "blog": [{"id": "1", "slug": "chiangmai-guide-2026", "title": "清迈数字游民完全指南 2026", "excerpt": "从签证到住宿,从咖啡馆到 Co-working,一篇搞定清迈旅居。", "emoji": "🏔️", "author": "nomadro", "published_at": "2026-01-15", "read_time": 8, "tags": ["清迈", "指南", "东南亚"]}, {"id": "2", "slug": "portugal-d7-visa", "title": "葡萄牙 D7 签证申请全攻略", "excerpt": "手把手教你申请欧洲最受欢迎的远程工作签证。", "emoji": "🇵🇹", "author": "nomadro", "published_at": "2026-02-03", "read_time": 12, "tags": ["签证", "葡萄牙", "欧洲"]}, {"id": "3", "slug": "nomad-tax-basics", "title": "数字游民税务入门:你需要知道的 5 件事", "excerpt": "183 天规则、双重征税、税务居民身份——一文理清。", "emoji": "🧾", "author": "nomadro", "published_at": "2026-02-20", "read_time": 10, "tags": ["税务", "法律", "指南"]}, {"id": "4", "slug": "medellin-spring-city", "title": "麦德林春城试住 30 天笔记", "excerpt": "时区、安全区、联合办公与西语社区初体验。", "emoji": "🌺", "author": "Sofia", "published_at": "2026-03-05", "read_time": 9, "tags": ["麦德林", "拉美", "试住"]}, {"id": "5", "slug": "tbilisi-tax-base", "title": "第比利斯当税务基地靠谱吗?", "excerpt": "远程签证、小企业税与旧城生活成本实测。", "emoji": "🍷", "author": "Devon", "published_at": "2026-03-18", "read_time": 11, "tags": ["格鲁吉亚", "税务", "欧洲"]}, {"id": "6", "slug": "dali-slow-create", "title": "大理慢创作:合租、网速与社群", "excerpt": "国内游民城市怎么住得久又不散漫。", "emoji": "🏔️", "author": "Lina", "published_at": "2026-04-02", "read_time": 7, "tags": ["大理", "内容", "国内"]}, {"id": "7", "slug": "berlin-startup-nomad", "title": "柏林自由职业与开源社群指南", "excerpt": "签证路径、共享办公与每周活动清单。", "emoji": "🎨", "author": "Ken", "published_at": "2026-04-20", "read_time": 10, "tags": ["柏林", "创业", "开源"]}, {"id": "8", "slug": "dubai-hub-month", "title": "迪拜一个月跳板旅居怎么排期", "excerpt": "签证、炎热季节、航班中转与办公点选择。", "emoji": "🏙️", "author": "Omar", "published_at": "2026-05-01", "read_time": 8, "tags": ["迪拜", "中东", "枢纽"]}, {"id": "9", "slug": "seoul-workation", "title": "首尔 Workation:网速、便利与成本", "excerpt": "C-3-8 签证与江南区办公踩点。", "emoji": "🏙️", "author": "Yuki", "published_at": "2026-05-16", "read_time": 9, "tags": ["首尔", "韩国", "网速"]}, {"id": "10", "slug": "first-month-checklist", "title": "旅居第一月清单:从落地到稳定输出", "excerpt": "SIM、短租、联合办公、社群与预算五步走。", "emoji": "✅", "author": "nomadro", "published_at": "2026-06-01", "read_time": 6, "tags": ["清单", "新手", "落地"]}], "blogContent": {"chiangmai-guide-2026": "## 为什么选择清迈?\n\n清迈是公认的全球数字游民之都。低廉的生活成本、完善的 Co-working 生态、友善的本地人和丰富的文化活动,让它成为新手游民的最佳起点。\n\n## 签证方案\n\n- **旅游签**:落地签 15 天,或提前办旅游签 60 天\n- **DTV 签证**:2024 年推出的 Destination Thailand Visa,适合远程工作者\n- **学生签/精英签**:长期旅居的进阶选择\n\n## 住宿推荐\n\n| 区域 | 月租 | 特点 |\n|------|------|------|\n| Nimman | ¥2,500-4,000 | 咖啡馆、餐厅、年轻人多 |\n| Old City | ¥1,500-3,000 | 文化氛围浓,步行可达寺庙 |\n| Hang Dong | ¥2,000-3,500 | 安静,适合深度工作 |\n\n## 最佳 Co-working\n\n1. **Punspace** — Nimman 区经典,日票 ¥60\n2. **CAMP** — Maya 商场顶楼,免费(消费即可)\n3. **Hub53** — 安静专业,月票 ¥800\n\n## 月均预算\n\n- 住宿:¥2,500\n- 餐饮:¥1,200\n- 交通:¥300(租摩托)\n- Co-working:¥400\n- 其他:¥400\n- **合计:约 ¥3,800-4,500**\n\n## 实用 Tips\n\n☕ 推荐咖啡馆:Ristr8to(世界冠军)、Graph One Nimman\n🏍️ 租摩托月费约 ¥400,注意戴头盔\n📱 推荐 AIS 或 TrueMove 无限流量套餐\n🤝 每周四有 Nomad Meetup,关注 Facebook 群组", "portugal-d7-visa": "## D7 签证是什么?\n\n葡萄牙 D7 签证(Passive Income Visa)最初为退休者设计,但因收入要求低、审批相对简单,成为数字游民进入欧盟的最佳通道之一。\n\n## 申请条件\n\n- 月收入不低于 **€760**(葡萄牙最低工资)\n- 银行存款建议 **€9,120+**(12个月生活费)\n- 无犯罪记录\n- 葡萄牙本地银行账户\n- 健康保险\n\n## 申请流程\n\n1. **准备材料**(2-4 周)\n - 护照、照片、收入证明、银行流水\n - 葡萄牙税号(NIF)\n - 住宿证明(租房合同或酒店预订)\n\n2. **递交申请**\n - 在中国:葡萄牙驻华使馆\n - 或入境葡萄牙后转居留许可\n\n3. **等待审批**(3-6 个月)\n\n4. **登陆葡萄牙**\n - 领取居留卡\n - 登记住址\n\n## 费用预算\n\n- 签证费:约 €90\n- 律师费(可选):€500-1,500\n- NIF + 银行开户:€200-300\n- 首月生活费:€800-1,200\n\n## 里斯本生活成本\n\n- 一居室公寓:€800-1,200/月\n- Co-working:€150-250/月\n- 餐饮:€300-500/月\n- **月均总花费:€1,200-1,800**\n\n## 常见问题\n\n**Q: D7 可以工作吗?**\nA: 可以远程为海外雇主工作,但不能在葡萄牙本地公司就职。\n\n**Q: 5年后能拿护照吗?**\nA: 满足居住要求后可以申请永居或入籍。", "nomad-tax-basics": "## 1. 什么是税务居民?\n\n大多数国家用 **183 天规则** 判定税务居民:在一个自然年内居住超过 183 天,即成为该国税务居民,需在该国申报全球收入。\n\n## 2. 双重征税怎么办?\n\n如果两个国家都认为你是税务居民,查 **双边税收协定**(DTA)。中国已与 100+ 国家签署 DTA,可避免重复缴税。\n\n## 3. 数字游民常见税务策略\n\n| 策略 | 说明 | 适合人群 |\n|------|------|----------|\n| 零税务居民 | 不在任何国家住满 183 天 | 短期旅居者 |\n| 税务友好国 | 葡萄牙 NHR、格鲁吉亚 1% 税 | 长期旅居者 |\n| 原籍国申报 | 回国期间申报 | 兼职游民 |\n\n## 4. 中国税务居民注意\n\n- 中国公民默认是中国税务居民(全球征税)\n- 海外收入也需申报(可抵免境外已缴税)\n- 建议咨询专业税务师\n\n## 5. 实用建议\n\n1. 📋 记录每个国家的入境/出境日期\n2. 🧾 保留所有收入凭证和银行流水\n3. 🏦 使用 Wise 等工具便于跨境汇款记录\n4. 👨💼 收入超过一定金额建议聘请税务顾问\n5. 📱 推荐工具:Xero(记账)、TaxScouts(报税)", "medellin-spring-city": "## 为什么麦德林?\n\n四季如春、生活成本可控,且贴近北美时区,适合接美加客户。Laureles / El Poblado 是游民常见落脚区。\n\n## 30 天试住建议\n\n1. 前 7 天住短租,实测网速与噪音\n2. 办本地 SIM,备份热点\n3. 参加 2 场线下 meetup,验证社区匹配度\n4. 确定联合办公月票再签长租\n\n## 预算参考(月)\n\n- 住宿:¥2,000–3,500\n- 餐饮:¥1,200–1,800\n- 交通:¥300–500\n- 办公:¥400–800", "tbilisi-tax-base": "## 远程签证 + 小企业税\n\n格鲁吉亚对远程工作者友好,许多人会评估本地公司与 1% 税路径(需自行核实最新政策)。\n\n## 生活感受\n\n旧城与 Vake 区咖啡馆多,适合深度工作;冬季偏冷,夏天舒服。\n\n## 注意\n\n- 银行开户与本地手机号流程可能变动\n- 税务方案务必咨询持证顾问\n- 把「试住 30 天」和「税务落地」分开决策", "dali-slow-create": "## 适合谁\n\n内容创作者、独立开发者、想放慢节奏但仍保持产出的人。\n\n## 实操建议\n\n- 合租优先选稳定 WiFi 与独立工作桌\n- 用固定作息对抗「度假感」\n- 主动加入本地创作者局,避免社交孤岛\n\n## 成本\n\n月生活费常可控制在 ¥3,500–5,000,视合租与出行频率而定。", "berlin-startup-nomad": "## 柏林为什么吸引游民\n\n开源、设计、创业活动密度高,共享办公选择多。\n\n## 落地要点\n\n- 提前规划居留/自由职业路径\n- 冬天日照短,准备补光与室内运动\n- 德语不是必须,但能明显提升生活便利\n\n## 每周节奏\n\n2 天联合办公 + 1 场 meetup + 周末短途,比纯咖啡馆更稳。", "dubai-hub-month": "## 适合当跳板\n\n国际航班密集,签证路径相对清晰,适合中东/欧亚中转月。\n\n## 炎热季节\n\n夏季高温,深度工作尽量安排在空调联合办公;户外活动放早晚。\n\n## 预算\n\n整体偏高,建议按「枢纽月」而非「低成本旅居」来规划。", "seoul-workation": "## 网速与便利\n\n首尔网速与城市便利度极强,适合需要高频协作的产品/工程角色。\n\n## 成本与节奏\n\n住宿与餐饮成本接近一线城市;用便利店与共享办公控制变量。\n\n## 签证\n\n关注 Workation / 相关短期远程路径的最新材料要求。", "first-month-checklist": "## 落地五步\n\n1. **连接**:落地办 SIM,测公寓与办公点网速\n2. **住处**:先短租 7–14 天,再决定长租\n3. **办公**:固定 1 个主点 + 2 个备份咖啡馆\n4. **社群**:报名 1 场 meetup,加 1 个本地群\n5. **预算**:用第一周真实账单校准月预算\n\n## 输出稳定\n\n每天保护 4 小时深度工作,旅行探索放在下午后段。"}, "meetups": [{"id": "online-roundtable", "title": "远程工作节奏圆桌", "city": "线上", "destination_slug": "", "emoji": "🎙️", "date": "2026-09-12", "time": "20:00", "venue": "nomadro 线上活动厅", "description": "聊聊异步协作、时区管理和旅居城市选择。登录后可 RSVP,活动前会收到提醒。", "mode": "online", "access_level": "public", "rsvp_count": 38, "max_attendees": 80, "organizer": "nomadro 社区", "tags": ["远程", "协作", "新手友好"], "is_upcoming": true}, {"id": "chiangmai-cowork", "title": "清迈联合办公下午茶", "city": "清迈", "destination_slug": "chiangmai", "emoji": "☕", "date": "2026-09-18", "time": "15:00", "venue": "Nimman 区 Co-working", "description": "一起办公、分享签证经验和住宿踩坑。线下为主,也开放线上旁听链接。", "mode": "hybrid", "access_level": "public", "rsvp_count": 14, "max_attendees": 24, "organizer": "清迈游民小组", "tags": ["线下", "社交", "东南亚"], "is_upcoming": true}, {"id": "lisbon-sunset", "title": "里斯本日落漫步", "city": "里斯本", "destination_slug": "lisbon", "emoji": "🌅", "date": "2026-09-22", "time": "18:30", "venue": "Miradouro 观景点集合", "description": "工作一周后,和同城游民一起看日落、交换欧洲签证情报。", "mode": "offline", "access_level": "public", "rsvp_count": 9, "max_attendees": 16, "organizer": "Lisbon Nomads", "tags": ["欧洲", "户外", "社交"], "is_upcoming": true}, {"id": "bali-surf-morning", "title": "巴厘岛晨间冲浪 + 办公", "city": "巴厘岛", "destination_slug": "bali", "emoji": "🏄", "date": "2026-10-05", "time": "07:00", "venue": "Canggu Beach", "description": "早起冲浪,10 点回咖啡馆集中办公。适合想体验 Work-Life 平衡的游民。", "mode": "offline", "access_level": "public", "rsvp_count": 11, "max_attendees": 20, "organizer": "Bali Remote Crew", "tags": ["运动", "生活方式"], "is_upcoming": true}, {"id": "tokyo-tax-talk", "title": "日本税务与居留线上分享", "city": "线上", "destination_slug": "tokyo", "emoji": "🧾", "date": "2026-10-12", "time": "19:30", "venue": "线上直播", "description": "特邀长期旅居日本的游民分享税务申报、保险和银行开户经验(非法律建议)。", "mode": "online", "access_level": "members", "rsvp_count": 52, "max_attendees": 120, "organizer": "nomadro 社区", "tags": ["税务", "日本", "干货"], "is_upcoming": true}, {"id": "berlin-opensource", "title": "柏林开源与 DevOps 夜谈", "city": "柏林", "destination_slug": "berlin", "emoji": "🖥️", "date": "2026-09-28", "time": "19:00", "venue": "Kreuzberg Co-working", "description": "分享自托管、CI 与远程团队协作实践,会后可继续社交。", "mode": "hybrid", "access_level": "public", "rsvp_count": 22, "max_attendees": 40, "organizer": "Berlin Remote", "tags": ["开源", "技术", "欧洲"], "is_upcoming": true}, {"id": "dali-create-walk", "title": "大理洱海创作散步", "city": "大理", "destination_slug": "dali", "emoji": "📷", "date": "2026-10-02", "time": "16:00", "venue": "才村码头集合", "description": "边走边拍,分享内容选题与变现路径,适合创作者与慢旅居者。", "mode": "offline", "access_level": "public", "rsvp_count": 8, "max_attendees": 18, "organizer": "大理创作者局", "tags": ["内容", "户外", "国内"], "is_upcoming": true}, {"id": "medellin-spanish", "title": "麦德林西语咖啡局", "city": "麦德林", "destination_slug": "medellin", "emoji": "☕", "date": "2026-10-08", "time": "10:30", "venue": "Laureles 咖啡馆", "description": "初级西语练习 + 安全区与租房情报交换,新手友好。", "mode": "offline", "access_level": "public", "rsvp_count": 16, "max_attendees": 20, "organizer": "Medellín Nomads", "tags": ["语言", "拉美", "社交"], "is_upcoming": true}, {"id": "seoul-night-office", "title": "首尔深夜联合办公", "city": "首尔", "destination_slug": "seoul", "emoji": "🌃", "date": "2026-10-15", "time": "21:00", "venue": "江南共享办公", "description": "适合东亚时区冲刺交付的游民,自带电脑即可。", "mode": "offline", "access_level": "public", "rsvp_count": 12, "max_attendees": 25, "organizer": "Seoul Workation", "tags": ["办公", "韩国", "效率"], "is_upcoming": true}, {"id": "dubai-hub-brunch", "title": "迪拜枢纽早午餐", "city": "迪拜", "destination_slug": "dubai", "emoji": "🥂", "date": "2026-10-18", "time": "11:00", "venue": "Downtown 咖啡厅", "description": "中转游民见面:签证、航班与下一站城市选择。", "mode": "offline", "access_level": "public", "rsvp_count": 10, "max_attendees": 16, "organizer": "Dubai Hub Crew", "tags": ["中东", "社交", "跳板"], "is_upcoming": true}, {"id": "tbilisi-tax-cafe", "title": "第比利斯税务咖啡聊", "city": "第比利斯", "destination_slug": "tbilisi", "emoji": "🍷", "date": "2026-10-22", "time": "15:00", "venue": "Vake 区咖啡馆", "description": "交流远程签证与本地公司实务(非正式法律意见),会后可徒步旧城。", "mode": "offline", "access_level": "members", "rsvp_count": 19, "max_attendees": 22, "organizer": "Tbilisi Base", "tags": ["税务", "欧洲", "干货"], "is_upcoming": true}, {"id": "mexico-food-walk", "title": "墨西哥城街头美食漫步", "city": "墨西哥城", "destination_slug": "mexico", "emoji": "🌮", "date": "2026-10-26", "time": "17:30", "venue": "Roma Norte 集合", "description": "边吃边聊北美时区接单与安全出行经验。", "mode": "offline", "access_level": "public", "rsvp_count": 13, "max_attendees": 18, "organizer": "CDMX Nomads", "tags": ["美食", "拉美", "社交"], "is_upcoming": true}], "discussions": [{"id": "visa-sea-2026", "title": "2026 东南亚长期停留签证怎么选?", "excerpt": "泰国 DTV、马来西亚 DE Rantau、印尼第二家园……大家最近实际办下来体验如何?", "author": "小林", "author_emoji": "🧳", "category": "签证", "reply_count": 23, "like_count": 47, "is_pinned": true, "created_at": "2026-08-20", "tags": ["签证", "东南亚"]}, {"id": "async-remote", "title": "异步团队怎么写日报才不烦人?", "excerpt": "我们团队跨 5 个时区,想收集团队节奏模板和工具推荐。", "author": "Marco", "author_emoji": "💻", "category": "远程工作", "reply_count": 15, "like_count": 31, "is_pinned": false, "created_at": "2026-08-22", "tags": ["远程", "协作"]}, {"id": "coliving-tips", "title": "第一次租 Coliving 必问房东的 10 个问题", "excerpt": "押金、网速、清洁、访客政策……欢迎补充你的踩坑经历。", "author": "阿静", "author_emoji": "🏡", "category": "住宿", "reply_count": 19, "like_count": 56, "is_pinned": false, "created_at": "2026-08-25", "tags": ["住宿", "经验"]}, {"id": "health-insurance", "title": "数字游民国际医疗保险对比", "excerpt": "SafetyWing、Genki、本地险……按年龄段和目的地聊聊性价比。", "author": "Yuki", "author_emoji": "🏥", "category": "安全", "reply_count": 11, "like_count": 28, "is_pinned": false, "created_at": "2026-08-27", "tags": ["保险", "安全"]}, {"id": "meetup-feedback", "title": "你希望 nomadro 线下活动开在哪些城市?", "excerpt": "我们在规划 Q4 活动路线,投票 + 留言告诉我们你的城市!", "author": "nomadro", "author_emoji": "🌍", "category": "社区", "reply_count": 34, "like_count": 72, "is_pinned": true, "created_at": "2026-08-28", "tags": ["活动", "投票"]}, {"id": "medellin-safety", "title": "麦德林哪些街区适合第一次长住?", "excerpt": "Laureles / Poblado / Envigado 的网速、噪音和夜间出行体验求分享。", "author": "Sofia", "author_emoji": "🌺", "category": "住宿", "reply_count": 17, "like_count": 41, "created_at": "2026-08-30", "tags": ["麦德林", "安全"]}, {"id": "tbilisi-bank", "title": "第比利斯开户与收美元的实操经验", "excerpt": "需要本地号码吗?Wise 能不能直接用?求最新流程。", "author": "Devon", "author_emoji": "🍷", "category": "财务", "reply_count": 14, "like_count": 36, "created_at": "2026-09-01", "tags": ["格鲁吉亚", "银行"]}, {"id": "berlin-winter", "title": "柏林冬天怎么保持产出和社交?", "excerpt": "日照短、天气冷,有没有室内运动 + meetup 推荐。", "author": "Ken", "author_emoji": "🎨", "category": "生活方式", "reply_count": 9, "like_count": 22, "created_at": "2026-09-02", "tags": ["柏林", "健康"]}, {"id": "dali-wifi", "title": "大理合租找房:网速坑位合集", "excerpt": "哪些区域光纤稳定?有没有房东靠谱名单。", "author": "Lina", "author_emoji": "📷", "category": "住宿", "reply_count": 21, "like_count": 48, "created_at": "2026-09-03", "tags": ["大理", "网速"]}, {"id": "dubai-summer", "title": "迪拜夏天还能正常远程办公吗?", "excerpt": "空调联合办公、作息调整和预算增加幅度求真实数据。", "author": "Omar", "author_emoji": "🏙️", "category": "生活方式", "reply_count": 8, "like_count": 19, "created_at": "2026-09-04", "tags": ["迪拜", "气候"]}, {"id": "seoul-visa", "title": "韩国 Workation 签证材料清单核对", "excerpt": "收入证明、保险、行程……有人最近过签吗?", "author": "Yuki", "author_emoji": "🇰🇷", "category": "签证", "reply_count": 12, "like_count": 33, "created_at": "2026-09-05", "tags": ["韩国", "签证"]}, {"id": "cowork-month-pass", "title": "各城市联合办公月票性价比对比", "excerpt": "清迈 / 里斯本 / 柏林 / 麦德林,欢迎贴价目与座位体验。", "author": "Alex", "author_emoji": "🚀", "category": "远程工作", "reply_count": 26, "like_count": 61, "created_at": "2026-09-06", "tags": ["办公", "成本"]}], "discussionDetails": {"visa-sea-2026": [{"id": "r1", "author": "阿Ken", "author_emoji": "🇹🇭", "content": "DTV 刚办下来,材料比想象中简单,关键是银行流水和远程工作证明。", "created_at": "2026-08-21", "like_count": 12}, {"id": "r2", "author": "Sara", "author_emoji": "🇲🇾", "content": "DE Rantau 审批大概 3 周,适合想待吉隆坡的。", "created_at": "2026-08-21", "like_count": 8}, {"id": "r3", "author": "Yuki", "author_emoji": "🇮🇩", "content": "巴厘岛短期用 B211A 足够,长期再看第二家园成本是否划算。", "created_at": "2026-08-22", "like_count": 6}], "async-remote": [{"id": "r1", "author": "Devon", "author_emoji": "🕐", "content": "我们用 Loom 录屏 + 简短文字摘要,比长日报友好很多。", "created_at": "2026-08-23", "like_count": 9}, {"id": "r2", "author": "Alex", "author_emoji": "🚀", "content": "模板三要素:昨天完成 / 今天计划 / 卡点,别写散文。", "created_at": "2026-08-23", "like_count": 11}], "coliving-tips": [{"id": "r1", "author": "小林", "author_emoji": "🧳", "content": "一定要问清押金退还条件和网速实测截图,别只看宣传。", "created_at": "2026-08-26", "like_count": 14}], "medellin-safety": [{"id": "r1", "author": "Alex", "author_emoji": "🌮", "content": "第一次建议 Laureles,生活成本与便利平衡;夜间少拿手机逛街。", "created_at": "2026-08-31", "like_count": 10}], "tbilisi-bank": [{"id": "r1", "author": "Omar", "author_emoji": "🏙️", "content": "先办本地 SIM 再去银行,材料准备护照+地址证明更顺利。", "created_at": "2026-09-01", "like_count": 7}], "dali-wifi": [{"id": "r1", "author": "阿静", "author_emoji": "🏡", "content": "才村/喜洲合租差别大,入住前用手机热点对照测速最靠谱。", "created_at": "2026-09-03", "like_count": 9}], "cowork-month-pass": [{"id": "r1", "author": "Marco", "author_emoji": "💻", "content": "里斯本月票大约 €150–250;清迈很多空间 ¥600–1000 就够用。", "created_at": "2026-09-06", "like_count": 15}], "health-insurance": [{"id": "r1", "author": "Sofia", "author_emoji": "🏥", "content": "30 岁以下 SafetyWing 够用;有慢性病建议加本地门诊险。", "created_at": "2026-08-28", "like_count": 10}, {"id": "r2", "author": "Ken", "author_emoji": "🇯🇵", "content": "在日本长期停留我叠了本地国民健康保险,报销更稳。", "created_at": "2026-08-28", "like_count": 7}], "meetup-feedback": [{"id": "r1", "author": "Lina", "author_emoji": "📷", "content": "大理 + 清迈希望能固定每月一场创作局。", "created_at": "2026-08-29", "like_count": 18}, {"id": "r2", "author": "Omar", "author_emoji": "🏙️", "content": "迪拜适合枢纽月见面,建议放周末早午餐。", "created_at": "2026-08-29", "like_count": 9}], "berlin-winter": [{"id": "r1", "author": "Devon", "author_emoji": "🖥️", "content": "冬天把联合办公当社交主场,再加一周一次室内攀岩/跑步局。", "created_at": "2026-09-02", "like_count": 11}], "dubai-summer": [{"id": "r1", "author": "Nina", "author_emoji": "🍷", "content": "夏天基本只在空调空间办公,预算里把交通和水电预留高一点。", "created_at": "2026-09-04", "like_count": 6}], "seoul-visa": [{"id": "r1", "author": "Jin", "author_emoji": "🇰🇷", "content": "最近材料重点是收入证明和保险;审批大约 3–6 周,建议提前办。", "created_at": "2026-09-05", "like_count": 8}]}, "course": [{"title": "启程:远程工作基础", "lessons": [{"title": "什么是数字游民", "duration": "8 分钟", "free": true}, {"title": "异步协作入门", "duration": "12 分钟", "free": true}, {"title": "时区与会议管理", "duration": "15 分钟", "free": false}]}, {"title": "签证与税务", "lessons": [{"title": "东南亚长期签证概览", "duration": "18 分钟", "free": false}, {"title": "183 天规则与税务居民", "duration": "14 分钟", "free": false}]}, {"title": "Homelab 与连接", "lessons": [{"title": "把家装进一个机柜", "duration": "20 分钟", "free": false}, {"title": "无论在哪都能连回家", "duration": "16 分钟", "free": false}]}], "lessons": {"0-0": {"title": "什么是数字游民", "moduleIndex": 0, "lessonIndex": 0, "duration": "8 分钟", "free": true, "content": "数字游民是一种工作方式:产出不绑定在某个地理坐标上。你可以在任何有网络的地方完成交付。"}, "0-1": {"title": "异步协作入门", "moduleIndex": 0, "lessonIndex": 1, "duration": "12 分钟", "free": true, "content": "异步沟通不是「随时在线」,而是「明确预期」:写清楚截止时间、交付物和决策人。"}, "0-2": {"title": "时区与会议管理", "moduleIndex": 0, "lessonIndex": 2, "duration": "15 分钟", "free": false, "content": "用 overlap 窗口安排会议,非重叠时段留给深度工作。工具箱里的「会议黄金时段」可以帮你算。"}, "1-0": {"title": "东南亚长期签证概览", "moduleIndex": 1, "lessonIndex": 0, "duration": "18 分钟", "free": false, "content": "泰国 DTV、马来西亚 DE Rantau、印尼第二家园……各国有不同的门槛与材料清单。"}, "1-1": {"title": "183 天规则与税务居民", "moduleIndex": 1, "lessonIndex": 1, "duration": "14 分钟", "free": false, "content": "多数国家用 183 天判定税务居民。用 nomadro 税居天数工具追踪停留。"}, "2-0": {"title": "把家装进一个机柜", "moduleIndex": 2, "lessonIndex": 0, "duration": "20 分钟", "free": false, "content": "Homelab 让你在旅途中拥有可控的 NAS、VPN 和开发环境。"}, "2-1": {"title": "无论在哪都能连回家", "moduleIndex": 2, "lessonIndex": 1, "duration": "16 分钟", "free": false, "content": "WireGuard + 动态 DNS,把家里的服务安全暴露给在外的你。"}}, "jobs": [{"id": "j1", "title": "Senior Frontend Engineer", "company": "Remote First Co", "location": "全球远程", "type": "全职", "salary": "$80k–120k", "tags": ["React", "TypeScript", "远程"], "url": "/gigs"}, {"id": "j2", "title": "DevOps / SRE", "company": "Nomad Labs", "location": "欧洲时区", "type": "合同", "salary": "€60–90/h", "tags": ["K8s", "AWS", "异步"], "url": "/gigs"}, {"id": "j3", "title": "内容运营(中文)", "company": "nomadro", "location": "东南亚友好", "type": "兼职", "salary": "面议", "tags": ["社区", "写作", "游民"], "url": "/gigs"}, {"id": "j4", "title": "Product Designer", "company": "Atlantic Remote", "location": "欧盟时区 ±3h", "type": "全职", "salary": "€55k–75k", "tags": ["Figma", "B2B", "远程"], "url": "/gigs"}, {"id": "j5", "title": "Growth Marketer", "company": "Hub Ventures", "location": "中东/欧洲友好", "type": "合同", "salary": "$4k–6k/月", "tags": ["SEO", "内容", "增长"], "url": "/gigs"}, {"id": "j6", "title": "Backend Engineer (Python)", "company": "Latam Cloud", "location": "北美时区", "type": "全职", "salary": "$90k–130k", "tags": ["Python", "FastAPI", "Postgres"], "url": "/gigs"}, {"id": "j7", "title": "Community Manager", "company": "nomadro", "location": "全球远程", "type": "兼职", "salary": "¥8k–12k/月", "tags": ["社区", "活动", "中英"], "url": "/gigs"}, {"id": "j8", "title": "Mobile Engineer (iOS/Android)", "company": "Seoul Soft", "location": "东亚时区", "type": "全职", "salary": "$70k–100k", "tags": ["Flutter", "移动端"], "url": "/gigs"}]};
diff --git a/redmini/tool/index.html b/redmini/tool/index.html
index aca5e88..a1d639d 100644
--- a/redmini/tool/index.html
+++ b/redmini/tool/index.html
@@ -3,11 +3,41 @@
- nomadro · 离线目的地
+ nomadro · 数字游民
-
+
+
+
+
+
+
+ 🏠 首页
+ 🧭 探索
+ 🤝 连接
+ 🌱 成长
+ 👤 我的
+
+
+
+
+
diff --git a/redmini/tool/styles.css b/redmini/tool/styles.css
index 466149d..63e3d4c 100644
--- a/redmini/tool/styles.css
+++ b/redmini/tool/styles.css
@@ -1,12 +1,277 @@
-*{box-sizing:border-box}html,body{margin:0;padding:0;background:#0c0b12;color:#f4f0ea;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
-#app{min-height:100vh;padding:28px 20px 48px}
-.brand{font-size:28px;font-weight:700;letter-spacing:1px}
-.tag{margin:8px 0 6px;color:#a8a3b3;font-size:14px}
-.note{color:#c9a227;font-size:12px;margin-bottom:20px}
-.card{background:rgba(255,255,255,.06);border-radius:14px;padding:14px;margin-bottom:12px}
-.name{font-size:17px;font-weight:600}
-.meta{margin-top:6px;color:#a8a3b3;font-size:13px}
-.tips{margin-top:24px}
-.tips h2{font-size:16px;margin:0 0 10px}
-.tips li{margin:8px 0;color:#cfc9d8;font-size:13px;line-height:1.5}
-.fav{margin-top:10px;font-size:12px;color:#f0c674}
+: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; }
diff --git a/redmini/tool/xhs-bridge.js b/redmini/tool/xhs-bridge.js
index b2673df..ee7811f 100644
--- a/redmini/tool/xhs-bridge.js
+++ b/redmini/tool/xhs-bridge.js
@@ -1,40 +1,639 @@
+/**
+ * Official XHS miniTool bridge helpers (Chrome 61 / ES2017).
+ * Prefer window.xhs.miniTool Storage (9.46+); fall back to localStorage.
+ */
(function (global) {
- 'use strict';
+ "use strict";
+
+ var STORAGE_MIN = 9460;
+ var COMMENT_MIN = 9490;
+ var FILE_MIN = 9490;
+
+ function readBuildVersion(launchOptions) {
+ var env = launchOptions && launchOptions.miniToolEnv;
+ return Number(env && env.buildVersion) || 0;
+ }
+
+ function getClientVersion(buildVersion) {
+ return Math.floor((Number(buildVersion) || 0) / 1000);
+ }
+
+ function isClientVersionAtLeast(buildVersion, minimum) {
+ return getClientVersion(buildVersion) >= minimum;
+ }
+
function getMiniTool() {
return global.xhs && global.xhs.miniTool ? global.xhs.miniTool : null;
}
- function setLocalData(key, data) {
- var serialized = JSON.stringify(data);
- var mt = getMiniTool();
- if (mt && typeof mt.setStorage === 'function') {
- return Promise.resolve(mt.setStorage({ key: key, data: serialized }))
- .then(function () { return true; })
- .catch(function () {
- try { localStorage.setItem(key, serialized); return true; } catch (e) { return false; }
- });
+
+ function getBuildVersion() {
+ var xhs = global.xhs;
+ var sync = readBuildVersion(xhs && xhs.launchOptions);
+ if (sync) {
+ return Promise.resolve(sync);
}
- try { localStorage.setItem(key, serialized); return Promise.resolve(true); } catch (e) {
+ var mt = getMiniTool();
+ if (!mt || typeof mt.getLaunchOptions !== "function") {
+ return Promise.resolve(0);
+ }
+ return Promise.resolve(mt.getLaunchOptions())
+ .then(function (opts) {
+ return readBuildVersion(opts);
+ })
+ .catch(function () {
+ return 0;
+ });
+ }
+
+ function setLocalData(key, data, encrypt) {
+ var serialized;
+ try {
+ serialized = JSON.stringify(data);
+ } catch (e) {
return Promise.resolve(false);
}
+ if (typeof serialized !== "string") return Promise.resolve(false);
+ var useEncrypt = !!encrypt;
+
+ return getBuildVersion().then(function (buildVersion) {
+ var mt = getMiniTool();
+ if (
+ isClientVersionAtLeast(buildVersion, STORAGE_MIN) &&
+ mt &&
+ typeof mt.setStorage === "function"
+ ) {
+ var payload = { key: key, data: serialized };
+ if (useEncrypt) payload.encrypt = true;
+ return Promise.resolve(mt.setStorage(payload))
+ .then(function () {
+ return true;
+ })
+ .catch(function () {
+ return writeBrowser(key, serialized);
+ });
+ }
+ return writeBrowser(key, serialized);
+ });
}
- function getLocalData(key) {
- var mt = getMiniTool();
- if (mt && typeof mt.getStorage === 'function') {
- return Promise.resolve(mt.getStorage({ key: key }))
- .then(function (res) {
- var raw = res && (res.data || res);
- if (typeof raw === 'string') {
- try { return JSON.parse(raw); } catch (e) { return null; }
- }
- return raw || null;
- })
- .catch(function () {
- try { return JSON.parse(localStorage.getItem(key) || 'null'); } catch (e) { return null; }
- });
+
+ function writeBrowser(key, serialized) {
+ try {
+ global.localStorage.setItem(key, serialized);
+ return true;
+ } catch (e) {
+ return false;
}
- try { return Promise.resolve(JSON.parse(localStorage.getItem(key) || 'null')); } catch (e) {
+ }
+
+ function getLocalData(key, fallback, encrypt) {
+ var useEncrypt = !!encrypt;
+ return getBuildVersion().then(function (buildVersion) {
+ var mt = getMiniTool();
+ if (
+ isClientVersionAtLeast(buildVersion, STORAGE_MIN) &&
+ mt &&
+ typeof mt.getStorage === "function"
+ ) {
+ var payload = { key: key };
+ if (useEncrypt) payload.encrypt = true;
+ return Promise.resolve(mt.getStorage(payload))
+ .then(function (res) {
+ var raw = res && res.data;
+ if (raw != null && raw !== "") {
+ try {
+ return JSON.parse(raw);
+ } catch (e) {
+ return fallback;
+ }
+ }
+ // Retry without encrypt (migration), then browser
+ if (useEncrypt) {
+ return Promise.resolve(mt.getStorage({ key: key }))
+ .then(function (res2) {
+ var raw2 = res2 && res2.data;
+ if (raw2 != null && raw2 !== "") {
+ try {
+ var parsed = JSON.parse(raw2);
+ return setLocalData(key, parsed, true).then(function () {
+ return parsed;
+ });
+ } catch (e2) {
+ return readBrowser(key, fallback);
+ }
+ }
+ return readBrowser(key, fallback);
+ })
+ .catch(function () {
+ return readBrowser(key, fallback);
+ });
+ }
+ var fromBrowser = readBrowser(key, null);
+ if (fromBrowser == null) return fallback;
+ return writeNative(mt, key, JSON.stringify(fromBrowser), false).then(
+ function () {
+ return fromBrowser;
+ },
+ function () {
+ return fromBrowser;
+ }
+ );
+ })
+ .catch(function () {
+ return readBrowser(key, fallback);
+ });
+ }
+ return readBrowser(key, fallback);
+ });
+ }
+
+ function writeNative(mt, key, serialized, encrypt) {
+ if (!mt || typeof mt.setStorage !== "function") {
+ return Promise.resolve(false);
+ }
+ var payload = { key: key, data: serialized };
+ if (encrypt) payload.encrypt = true;
+ return Promise.resolve(mt.setStorage(payload)).then(
+ function () {
+ return true;
+ },
+ function () {
+ return false;
+ }
+ );
+ }
+
+ function readBrowser(key, fallback) {
+ try {
+ var raw = global.localStorage.getItem(key);
+ if (raw == null || raw === "") return fallback;
+ return JSON.parse(raw);
+ } catch (e) {
+ return fallback;
+ }
+ }
+
+ function removeLocalData(key) {
+ return getBuildVersion().then(function (buildVersion) {
+ var mt = getMiniTool();
+ if (
+ isClientVersionAtLeast(buildVersion, STORAGE_MIN) &&
+ mt &&
+ typeof mt.removeStorage === "function"
+ ) {
+ return Promise.resolve(mt.removeStorage({ key: key }))
+ .then(function () {
+ removeBrowser(key);
+ return true;
+ })
+ .catch(function () {
+ return removeBrowser(key);
+ });
+ }
+ return removeBrowser(key);
+ });
+ }
+
+ function clearLocalData() {
+ return getBuildVersion().then(function (buildVersion) {
+ var mt = getMiniTool();
+ if (
+ isClientVersionAtLeast(buildVersion, STORAGE_MIN) &&
+ mt &&
+ typeof mt.clearStorage === "function"
+ ) {
+ return Promise.resolve(mt.clearStorage())
+ .then(function () {
+ clearBrowser();
+ return true;
+ })
+ .catch(function () {
+ return clearBrowser();
+ });
+ }
+ return clearBrowser();
+ });
+ }
+
+ function removeBrowser(key) {
+ try {
+ global.localStorage.removeItem(key);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ function clearBrowser() {
+ try {
+ global.localStorage.clear();
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ function getLaunchOptions() {
+ var xhs = global.xhs;
+ if (xhs && xhs.launchOptions) {
+ return Promise.resolve(xhs.launchOptions);
+ }
+ var mt = getMiniTool();
+ if (!mt || typeof mt.getLaunchOptions !== "function") {
return Promise.resolve(null);
}
+ return Promise.resolve(mt.getLaunchOptions()).catch(function () {
+ return null;
+ });
}
- global.NomadroXhs = { setLocalData: setLocalData, getLocalData: getLocalData };
+
+ /** Read miniToolSnapshotInfo from launch options (comment re-entry, §3.9). */
+ function readSnapshotInfo(launchOptions) {
+ if (!launchOptions) return null;
+ var raw =
+ launchOptions.miniToolSnapshotInfo ||
+ (launchOptions.query && launchOptions.query.miniToolSnapshotInfo) ||
+ (launchOptions.referrerInfo &&
+ launchOptions.referrerInfo.miniToolSnapshotInfo) ||
+ (launchOptions.extraData && launchOptions.extraData.miniToolSnapshotInfo);
+ if (!raw) return null;
+ if (typeof raw === "object") return raw;
+ try {
+ return JSON.parse(raw);
+ } catch (e) {
+ return null;
+ }
+ }
+
+ function callApi(name, options) {
+ var mt = getMiniTool();
+ if (!mt || typeof mt[name] !== "function") {
+ return Promise.reject({ errMsg: name + ":fail not available" });
+ }
+ return Promise.resolve(mt[name](options || {}));
+ }
+
+ function writeTempFile(dataUrl) {
+ return callApi("writeTempFile", { data: dataUrl }).then(function (res) {
+ return (res && res.filePath) || dataUrl;
+ });
+ }
+
+ function saveImageToPhotosAlbum(filePath) {
+ return callApi("saveImageToPhotosAlbum", { filePath: filePath });
+ }
+
+ function postNote(opts) {
+ return callApi("postNote", opts);
+ }
+
+ function canPostComment(buildVersion) {
+ var mt = getMiniTool();
+ return (
+ isClientVersionAtLeast(buildVersion, COMMENT_MIN) &&
+ mt &&
+ typeof mt.interactionOpenApi === "function"
+ );
+ }
+
+ function postComment(payload, saveToAlbum) {
+ return callApi("interactionOpenApi", {
+ payload: payload,
+ saveToAlbum: saveToAlbum !== false
+ });
+ }
+
+ function getStorageInfo() {
+ return getBuildVersion().then(function (buildVersion) {
+ var mt = getMiniTool();
+ if (
+ !isClientVersionAtLeast(buildVersion, STORAGE_MIN) ||
+ !mt ||
+ typeof mt.getStorageInfo !== "function"
+ ) {
+ return null;
+ }
+ return Promise.resolve(mt.getStorageInfo()).catch(function () {
+ return null;
+ });
+ });
+ }
+
+ function canUseFileSystem(buildVersion) {
+ var mt = getMiniTool();
+ return (
+ isClientVersionAtLeast(buildVersion, FILE_MIN) &&
+ mt &&
+ typeof mt.writeFile === "function" &&
+ typeof mt.readFile === "function"
+ );
+ }
+
+ function getUserDataPath() {
+ return getLaunchOptions().then(function (opts) {
+ var env = opts && opts.miniToolEnv;
+ return (env && env.userDataPath) || "";
+ });
+ }
+
+ function ensureDir(dirPath) {
+ var mt = getMiniTool();
+ if (!mt || typeof mt.mkdir !== "function") {
+ return Promise.resolve(false);
+ }
+ return Promise.resolve(mt.mkdir({ dirPath: dirPath, recursive: true }))
+ .then(function () {
+ return true;
+ })
+ .catch(function () {
+ return false;
+ });
+ }
+
+ function writeUtf8File(relativePath, data) {
+ return getBuildVersion().then(function (bv) {
+ if (!canUseFileSystem(bv)) {
+ return Promise.reject({ errMsg: "writeFile:fail version" });
+ }
+ return getUserDataPath().then(function (root) {
+ if (!root) return Promise.reject({ errMsg: "writeFile:fail no path" });
+ var rel = String(relativePath || "").replace(/^\/+/, "");
+ var parts = rel.split("/");
+ var dir =
+ parts.length > 1 ? root + "/" + parts.slice(0, -1).join("/") : root;
+ var filePath = root + "/" + rel;
+ return ensureDir(dir).then(function () {
+ return callApi("writeFile", {
+ filePath: filePath,
+ data: typeof data === "string" ? data : JSON.stringify(data),
+ encoding: "utf8"
+ }).then(function () {
+ return filePath;
+ });
+ });
+ });
+ });
+ }
+
+ function appendUtf8File(relativePath, data) {
+ return getBuildVersion().then(function (bv) {
+ if (!canUseFileSystem(bv)) {
+ return Promise.reject({ errMsg: "appendFile:fail version" });
+ }
+ var mt = getMiniTool();
+ if (!mt || typeof mt.appendFile !== "function") {
+ // Fallback: read + write
+ return readUtf8File(relativePath).then(function (prev) {
+ return writeUtf8File(
+ relativePath,
+ (prev || "") + (typeof data === "string" ? data : String(data))
+ );
+ });
+ }
+ return getUserDataPath().then(function (root) {
+ if (!root) return Promise.reject({ errMsg: "appendFile:fail no path" });
+ var rel = String(relativePath || "").replace(/^\/+/, "");
+ var parts = rel.split("/");
+ var dir =
+ parts.length > 1 ? root + "/" + parts.slice(0, -1).join("/") : root;
+ var filePath = root + "/" + rel;
+ return ensureDir(dir).then(function () {
+ return callApi("appendFile", {
+ filePath: filePath,
+ data: typeof data === "string" ? data : String(data),
+ encoding: "utf8"
+ }).then(function () {
+ return filePath;
+ });
+ });
+ });
+ });
+ }
+
+ function postVideoNote(opts) {
+ var videoRes = { video_url: opts.videoPath };
+ if (opts.coverPath) videoRes.cover_url = opts.coverPath;
+ return callApi("postNote", {
+ title: opts.title,
+ content: opts.content,
+ pageType: "video_publish",
+ mediaInfo: {
+ video_resources: videoRes
+ }
+ });
+ }
+
+ function readUtf8File(relativePath) {
+ return getBuildVersion().then(function (bv) {
+ if (!canUseFileSystem(bv)) {
+ return Promise.resolve(null);
+ }
+ return getUserDataPath().then(function (root) {
+ if (!root) return null;
+ var filePath = root + "/" + String(relativePath || "").replace(/^\/+/, "");
+ return callApi("readFile", { filePath: filePath, encoding: "utf8" })
+ .then(function (res) {
+ return res && res.data != null ? res.data : null;
+ })
+ .catch(function () {
+ return null;
+ });
+ });
+ });
+ }
+
+ /** Write dataURL to temp, then optionally persist via saveFile (9.49+). */
+ function persistImage(dataUrl, relativePath) {
+ return writeTempFile(dataUrl).then(function (tempPath) {
+ return getBuildVersion().then(function (bv) {
+ var mt = getMiniTool();
+ if (
+ !relativePath ||
+ !isClientVersionAtLeast(bv, FILE_MIN) ||
+ !mt ||
+ typeof mt.saveFile !== "function"
+ ) {
+ return tempPath;
+ }
+ return getUserDataPath().then(function (root) {
+ if (!root) return tempPath;
+ var dest = root + "/" + String(relativePath).replace(/^\/+/, "");
+ var dir = dest.replace(/\/[^/]+$/, "");
+ return ensureDir(dir).then(function () {
+ return callApi("saveFile", {
+ tempFilePath: tempPath,
+ filePath: dest
+ })
+ .then(function (res) {
+ return (res && res.savedFilePath) || dest || tempPath;
+ })
+ .catch(function () {
+ return tempPath;
+ });
+ });
+ });
+ });
+ });
+ }
+
+ function getFileStorageInfo() {
+ return getBuildVersion().then(function (bv) {
+ var mt = getMiniTool();
+ if (
+ !isClientVersionAtLeast(bv, FILE_MIN) ||
+ !mt ||
+ typeof mt.getFileStorageInfo !== "function"
+ ) {
+ return null;
+ }
+ return Promise.resolve(mt.getFileStorageInfo()).catch(function () {
+ return null;
+ });
+ });
+ }
+
+ function unlinkFile(relativeOrAbsPath) {
+ return getBuildVersion().then(function (bv) {
+ var mt = getMiniTool();
+ if (
+ !isClientVersionAtLeast(bv, FILE_MIN) ||
+ !mt ||
+ typeof mt.unlink !== "function"
+ ) {
+ return false;
+ }
+ var path = String(relativeOrAbsPath || "");
+ if (!path) return false;
+ var p = Promise.resolve(path);
+ if (path.indexOf("/") !== 0 && path.indexOf("usr") !== 0) {
+ p = getUserDataPath().then(function (root) {
+ return root ? root + "/" + path.replace(/^\/+/, "") : path;
+ });
+ }
+ return p.then(function (filePath) {
+ return callApi("unlink", { filePath: filePath })
+ .then(function () {
+ return true;
+ })
+ .catch(function () {
+ return false;
+ });
+ });
+ });
+ }
+
+ function readDirList(relativeDir) {
+ return getBuildVersion().then(function (bv) {
+ var mt = getMiniTool();
+ if (
+ !isClientVersionAtLeast(bv, FILE_MIN) ||
+ !mt ||
+ typeof mt.readDir !== "function"
+ ) {
+ return [];
+ }
+ return getUserDataPath().then(function (root) {
+ if (!root) return [];
+ var dirPath =
+ root +
+ (relativeDir
+ ? "/" + String(relativeDir).replace(/^\/+|\/+$/g, "")
+ : "");
+ return callApi("readDir", { dirPath: dirPath })
+ .then(function (res) {
+ var files = (res && res.files) || [];
+ return files.map(function (f) {
+ if (typeof f === "string") {
+ return { name: f, path: dirPath + "/" + f };
+ }
+ var name = f.name || f.fileName || String(f);
+ return {
+ name: name,
+ path: f.path || f.filePath || dirPath + "/" + name,
+ isDir: !!(f.isDir || f.isDirectory)
+ };
+ });
+ })
+ .catch(function () {
+ return [];
+ });
+ });
+ });
+ }
+
+ function cleanupShareDir() {
+ return readDirList("share").then(function (files) {
+ var chain = Promise.resolve(0);
+ for (var i = 0; i < files.length; i++) {
+ (function (file) {
+ if (file.isDir) return;
+ chain = chain.then(function (n) {
+ return callApi("unlink", { filePath: file.path })
+ .then(function () {
+ return n + 1;
+ })
+ .catch(function () {
+ return n;
+ });
+ });
+ })(files[i]);
+ }
+ return chain;
+ });
+ }
+
+ /** Convert File / Blob to data URL (offline, no network). */
+ function blobToDataUrl(blob) {
+ return new Promise(function (resolve, reject) {
+ if (!blob) {
+ reject({ errMsg: "blobToDataUrl:fail empty" });
+ return;
+ }
+ try {
+ var reader = new FileReader();
+ reader.onload = function () {
+ resolve(reader.result);
+ };
+ reader.onerror = function () {
+ reject({ errMsg: "blobToDataUrl:fail read" });
+ };
+ reader.readAsDataURL(blob);
+ } catch (e) {
+ reject({ errMsg: "blobToDataUrl:fail " + (e && e.message) });
+ }
+ });
+ }
+
+ global.XhsBridge = {
+ getBuildVersion: getBuildVersion,
+ getClientVersion: getClientVersion,
+ isClientVersionAtLeast: isClientVersionAtLeast,
+ setLocalData: setLocalData,
+ getLocalData: getLocalData,
+ removeLocalData: removeLocalData,
+ clearLocalData: clearLocalData,
+ getStorageInfo: getStorageInfo,
+ getFileStorageInfo: getFileStorageInfo,
+ getLaunchOptions: getLaunchOptions,
+ readSnapshotInfo: readSnapshotInfo,
+ writeTempFile: writeTempFile,
+ saveImageToPhotosAlbum: saveImageToPhotosAlbum,
+ postNote: postNote,
+ canPostComment: canPostComment,
+ postComment: postComment,
+ canUseFileSystem: canUseFileSystem,
+ getUserDataPath: getUserDataPath,
+ writeUtf8File: writeUtf8File,
+ appendUtf8File: appendUtf8File,
+ readUtf8File: readUtf8File,
+ persistImage: persistImage,
+ unlinkFile: unlinkFile,
+ readDirList: readDirList,
+ cleanupShareDir: cleanupShareDir,
+ blobToDataUrl: blobToDataUrl,
+ postVideoNote: postVideoNote,
+ getMiniTool: getMiniTool,
+ STORAGE_MIN: STORAGE_MIN,
+ COMMENT_MIN: COMMENT_MIN,
+ FILE_MIN: FILE_MIN
+ };
})(window);