(function () { "use strict"; 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"; 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 || "—") + "文章
    " + "
    " + '

    ⚡ 三环路径

    ' + '
    ' + '' + '' + '' + '' + "
    " + '

    🌴 热门目的地

    ' + '
    ' + dests.map(destCard).join("") + "
    " + '

    🗓️ 游民一天

    ' + '
    ' + life.map(function (item, idx) { return '"; }).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 ( '" ); } function viewExploreHub() { setChrome("探索", "Discover where to go", false); var rings = (DATA.rings && DATA.rings.explore) || []; return ( '

    探索环:发现去哪。离线可浏览目的地、签证,并做本地智能匹配。

    ' + '
    ' + rings.map(function (r) { return '"; }).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 '"; }).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 '"; }).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) + "

    " + '
    ' + '" + '' + "
    " + "
    " + '
    ' + '
    月生活费
    ¥' + (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 '"; }).join("") + "
    " ); } function opt(v, label, cur) { return '"; } 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 '"; }).join(""); return ( '
    ' + '" + '" + "
    " + '' + 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 '"; }).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 '"; }).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 '"; }).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 '"; }).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 '"; }).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 ( '

    实用工具

    ' + '
    ' + '' + '' + "
    " + '

    推荐软件栈

    ' + (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 '"; }).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(); }); } boot(); })();