982 lines
47 KiB
JavaScript
982 lines
47 KiB
JavaScript
/**
|
|
* nomadro XHS offline mirror
|
|
* Reuses the same CSS classes / visual structure as frontend H5 (localhost:3000).
|
|
* No network — data from window.NOMADRO_DATA.
|
|
*/
|
|
(function () {
|
|
"use strict";
|
|
|
|
var D = window.NOMADRO_DATA || {};
|
|
var Bridge = window.XhsBridge;
|
|
var FAV_KEY = "nomadro_xhs_favs";
|
|
var root = document.getElementById("app-root");
|
|
var backBtn = document.getElementById("xhs-back");
|
|
var route = { name: "home", params: {} };
|
|
var favs = [];
|
|
var destFilter = "all";
|
|
var destSort = "rating";
|
|
var destSearch = "";
|
|
var visaFilter = "all";
|
|
var lifeActive = 0;
|
|
var voiceActive = 0;
|
|
var faqActive = null;
|
|
|
|
function esc(s) {
|
|
return String(s == null ? "" : s)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
|
|
function on(el, ev, fn) {
|
|
if (el) el.addEventListener(ev, fn);
|
|
}
|
|
|
|
function storageGet(key, fb) {
|
|
if (Bridge && Bridge.getLocalData) {
|
|
return Bridge.getLocalData(key).then(function (v) { return v == null ? fb : v; });
|
|
}
|
|
try {
|
|
var raw = localStorage.getItem(key);
|
|
return Promise.resolve(raw ? JSON.parse(raw) : fb);
|
|
} catch (e) {
|
|
return Promise.resolve(fb);
|
|
}
|
|
}
|
|
|
|
function storageSet(key, val) {
|
|
if (Bridge && Bridge.setLocalData) return Bridge.setLocalData(key, val);
|
|
try { localStorage.setItem(key, JSON.stringify(val)); } catch (e) {}
|
|
return Promise.resolve(true);
|
|
}
|
|
|
|
function go(name, params) {
|
|
route = { name: name, params: params || {} };
|
|
render();
|
|
window.scrollTo(0, 0);
|
|
}
|
|
|
|
function destBySlug(slug) {
|
|
var list = D.destinations || [];
|
|
for (var i = 0; i < list.length; i++) if (list[i].slug === slug) return list[i];
|
|
return null;
|
|
}
|
|
|
|
function isFav(slug) { return favs.indexOf(slug) >= 0; }
|
|
|
|
function toggleFav(slug) {
|
|
var i = favs.indexOf(slug);
|
|
if (i >= 0) favs.splice(i, 1); else favs.push(slug);
|
|
storageSet(FAV_KEY, favs);
|
|
render();
|
|
}
|
|
|
|
function logoSvg(id) {
|
|
return (
|
|
'<svg class="logo-icon" viewBox="0 0 40 40" fill="none" aria-hidden="true">' +
|
|
'<circle cx="20" cy="20" r="18" stroke="url(#' + id + ')" stroke-width="2.5" />' +
|
|
'<path d="M12 22c4-8 12-8 16 0" stroke="url(#' + id + ')" stroke-width="2" stroke-linecap="round" />' +
|
|
'<circle cx="28" cy="14" r="3" fill="url(#' + id + ')" />' +
|
|
'<defs><linearGradient id="' + id + '" x1="0" y1="0" x2="40" y2="40">' +
|
|
'<stop offset="0%" stop-color="#FF6B6B" /><stop offset="50%" stop-color="#FFE66D" /><stop offset="100%" stop-color="#4ECDC4" />' +
|
|
"</linearGradient></defs></svg>"
|
|
);
|
|
}
|
|
|
|
/* ===== chrome shared with H5 ===== */
|
|
|
|
function renderNavbar() {
|
|
return (
|
|
'<nav class="navbar scrolled" id="navbar">' +
|
|
'<div class="nav-inner">' +
|
|
'<a href="#/" class="logo" data-nav="home">' + logoSvg("navLogoGrad") + "<span>nomadro</span></a>" +
|
|
'<div class="nav-links open-static">' +
|
|
'<a href="#/explore" data-nav="explore">探索</a>' +
|
|
'<a href="#/connect" data-nav="connect">连接</a>' +
|
|
'<a href="#/grow" data-nav="grow">成长</a>' +
|
|
'<a href="#/mine" data-nav="mine">我的</a>' +
|
|
"</div>" +
|
|
'<div class="nav-actions">' +
|
|
'<span class="nav-chip">离线</span>' +
|
|
"</div>" +
|
|
"</div>" +
|
|
"</nav>"
|
|
);
|
|
}
|
|
|
|
function renderTicker() {
|
|
var msgs = D.ticker || [];
|
|
if (!msgs.length) return "";
|
|
return (
|
|
'<div class="ticker-bar"><div class="ticker-track">' +
|
|
msgs.concat(msgs).map(function (m) {
|
|
return '<span class="ticker-item">' + esc(m) + "</span>";
|
|
}).join("") +
|
|
"</div></div>"
|
|
);
|
|
}
|
|
|
|
function renderHero() {
|
|
var stats = D.stats || {};
|
|
return (
|
|
'<section class="hero" id="hero">' +
|
|
'<div class="hero-bg-shapes"><div class="shape shape-1"></div><div class="shape shape-2"></div><div class="shape shape-3"></div></div>' +
|
|
'<div class="hero-inner">' +
|
|
'<div class="hero-top">' +
|
|
'<div class="hero-content">' +
|
|
'<div class="hero-badge reveal visible"><span class="badge-dot"></span>nomadro · ' +
|
|
esc(stats.total_nomads || "35.6M") + " 全球数字游民正在路上</div>" +
|
|
'<h1 class="hero-title reveal visible">用一行代码<br /><span class="gradient-text">环游世界</span> 🌏</h1>' +
|
|
'<p class="hero-subtitle reveal visible"><span id="typewriter"></span><span class="typewriter-cursor">|</span></p>' +
|
|
"</div>" +
|
|
'<div class="hero-visual reveal visible">' +
|
|
'<div class="globe-container">' +
|
|
'<svg class="globe-svg" viewBox="0 0 400 400">' +
|
|
"<defs>" +
|
|
'<radialGradient id="globeGlow" cx="50%" cy="50%" r="50%"><stop offset="0%" stop-color="#4ECDC4" stop-opacity="0.3" /><stop offset="100%" stop-color="#4ECDC4" stop-opacity="0" /></radialGradient>' +
|
|
'<linearGradient id="orbitGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#FF6B6B" /><stop offset="100%" stop-color="#4ECDC4" /></linearGradient>' +
|
|
"</defs>" +
|
|
'<circle cx="200" cy="200" r="160" fill="url(#globeGlow)" />' +
|
|
'<circle cx="200" cy="200" r="120" fill="none" stroke="rgba(78,205,196,0.2)" stroke-width="1" />' +
|
|
'<ellipse cx="200" cy="200" rx="120" ry="40" fill="none" stroke="rgba(78,205,196,0.3)" stroke-width="1.5" class="orbit-ring" />' +
|
|
'<ellipse cx="200" cy="200" rx="40" ry="120" fill="none" stroke="rgba(255,107,107,0.3)" stroke-width="1.5" class="orbit-ring orbit-ring-2" />' +
|
|
'<path d="M160 140 Q180 130 200 135 Q220 128 240 140 Q250 155 245 170 Q235 185 220 190 Q200 195 185 188 Q165 180 160 165 Z" fill="rgba(78,205,196,0.4)" class="continent" />' +
|
|
'<g class="nomad-dots">' +
|
|
'<circle cx="180" cy="155" r="5" fill="#FF6B6B" class="pulse-dot" />' +
|
|
'<circle cx="260" cy="215" r="5" fill="#FFE66D" class="pulse-dot" />' +
|
|
'<circle cx="145" cy="225" r="5" fill="#4ECDC4" class="pulse-dot" />' +
|
|
'<circle cx="220" cy="175" r="5" fill="#A78BFA" class="pulse-dot" />' +
|
|
"</g>" +
|
|
'<path d="M180 155 Q220 140 260 215" fill="none" stroke="url(#orbitGrad)" stroke-width="2" stroke-dasharray="6 4" class="flight-path" />' +
|
|
"</svg>" +
|
|
'<div class="floating-cards">' +
|
|
'<div class="float-card fc-1">🏝️ Bali<br /><small>WiFi 98Mbps</small></div>' +
|
|
'<div class="float-card fc-2">☕ Lisbon<br /><small>€800/mo</small></div>' +
|
|
'<div class="float-card fc-3">🏔️ Chiang Mai<br /><small>⭐ 4.9</small></div>' +
|
|
"</div>" +
|
|
"</div>" +
|
|
"</div>" +
|
|
"</div>" +
|
|
'<div class="hero-bottom reveal visible">' +
|
|
'<div class="hero-actions">' +
|
|
'<button type="button" class="btn btn-primary" data-nav="matcher"><span>🎯 智能匹配去哪</span></button>' +
|
|
'<a href="#destinations" class="btn btn-ghost" data-scroll="destinations"><span>从城市开始</span></a>' +
|
|
"</div>" +
|
|
"</div>" +
|
|
"</div>" +
|
|
'<div class="scroll-indicator"><span>向下滚动</span></div>' +
|
|
"</section>"
|
|
);
|
|
}
|
|
|
|
function renderJourney() {
|
|
return (
|
|
'<section class="journey-strip section-compact" id="path">' +
|
|
'<div class="container">' +
|
|
'<div class="section-header journey-head reveal visible">' +
|
|
'<span class="section-tag">PATH</span><h2>三条环,走通旅居</h2>' +
|
|
"<p>探索去哪 → 遇见同行 → 路上成长</p>" +
|
|
"</div>" +
|
|
'<ol class="journey-rail journey-rail-3 reveal visible">' +
|
|
'<li class="journey-step"><div class="journey-card">' +
|
|
'<div class="journey-card-top"><span class="journey-n">01</span><span class="journey-emoji">🌍</span></div>' +
|
|
"<h3>探索</h3><p>目的地、签证与智能匹配</p>" +
|
|
'<div class="journey-actions"><button type="button" class="btn btn-sm btn-primary" data-nav="explore">开始探索</button></div>' +
|
|
"</div></li>" +
|
|
'<li class="journey-step"><div class="journey-card">' +
|
|
'<div class="journey-card-top"><span class="journey-n">02</span><span class="journey-emoji">🤝</span></div>' +
|
|
"<h3>连接</h3><p>活动、社区与游民心声</p>" +
|
|
'<div class="journey-actions"><button type="button" class="btn btn-sm btn-primary" data-nav="connect">去连接</button></div>' +
|
|
"</div></li>" +
|
|
'<li class="journey-step"><div class="journey-card">' +
|
|
'<div class="journey-card-top"><span class="journey-n">03</span><span class="journey-emoji">🚀</span></div>' +
|
|
"<h3>成长</h3><p>学院、博客与工具箱</p>" +
|
|
'<div class="journey-actions"><button type="button" class="btn btn-sm btn-primary" data-nav="grow">去成长</button></div>' +
|
|
"</div></li>" +
|
|
"</ol>" +
|
|
"</div>" +
|
|
"</section>"
|
|
);
|
|
}
|
|
|
|
function filteredDests() {
|
|
var list = (D.destinations || []).slice();
|
|
if (destFilter !== "all") list = list.filter(function (d) { return d.region === destFilter; });
|
|
if (destSearch) {
|
|
var q = destSearch.toLowerCase();
|
|
list = list.filter(function (d) {
|
|
return String(d.name).toLowerCase().indexOf(q) >= 0 || String(d.country).toLowerCase().indexOf(q) >= 0;
|
|
});
|
|
}
|
|
if (destSort === "cost-asc") list.sort(function (a, b) { return a.cost - b.cost; });
|
|
else if (destSort === "cost-desc") list.sort(function (a, b) { return b.cost - a.cost; });
|
|
else if (destSort === "speed") list.sort(function (a, b) { return b.speed - a.speed; });
|
|
else list.sort(function (a, b) { return b.rating - a.rating; });
|
|
return list;
|
|
}
|
|
|
|
function destCard(d) {
|
|
var hue = d.hue || 170;
|
|
return (
|
|
'<article class="dest-card reveal visible">' +
|
|
'<a href="#/destination/' + esc(d.slug) + '" class="dest-card-link" data-dest="' + esc(d.slug) + '">' +
|
|
'<div class="dest-img" style="--hue:' + hue + '">' +
|
|
'<div class="dest-emoji">' + esc(d.emoji) + "</div>" +
|
|
'<div class="dest-overlay"><span class="dest-tag">' + esc(d.tag) + "</span></div>" +
|
|
"</div>" +
|
|
'<div class="dest-body">' +
|
|
"<h3>" + esc(d.name) + ", " + esc(d.country) +
|
|
(isFav(d.slug) ? " ★" : "") + "</h3>" +
|
|
"<p>" + esc(d.description) + "</p>" +
|
|
'<div class="dest-meta">' +
|
|
"<span>💰 ¥" + Number(d.cost).toLocaleString() + "</span>" +
|
|
"<span>📶 " + d.speed + "Mbps</span>" +
|
|
"<span>🌡️ " + d.temperature + "°C</span>" +
|
|
"</div>" +
|
|
'<div class="dest-rating"><span class="stars">' +
|
|
"⭐".repeat(Math.max(1, Math.round((d.rating || 8) / 2))) +
|
|
"</span><span>" + d.rating + "</span></div>" +
|
|
"</div>" +
|
|
"</a>" +
|
|
"</article>"
|
|
);
|
|
}
|
|
|
|
function renderDestinations(limit) {
|
|
var list = filteredDests();
|
|
var visible = typeof limit === "number" ? list.slice(0, limit) : list;
|
|
var filters = [
|
|
{ key: "all", label: "全部" },
|
|
{ key: "sea", label: "东南亚" },
|
|
{ key: "europe", label: "欧洲" },
|
|
{ key: "latam", label: "拉美" },
|
|
{ key: "asia", label: "东亚" },
|
|
];
|
|
return (
|
|
'<section class="section destinations section-compact" id="destinations">' +
|
|
'<div class="container">' +
|
|
'<div class="section-header reveal visible">' +
|
|
'<span class="section-tag">🌴 TOP DESTINATIONS</span>' +
|
|
"<h2>热门旅居目的地</h2>" +
|
|
"<p>按预算、网速与气候筛选,点进城市看详情</p>" +
|
|
'<button type="button" class="btn btn-primary dest-matcher-btn" data-nav="matcher">🎯 智能匹配</button>' +
|
|
"</div>" +
|
|
'<div class="dest-filters reveal visible">' +
|
|
'<div class="filter-search">' +
|
|
'<input id="dest-search" placeholder="搜索城市 / 国家" value="' + esc(destSearch) + '" />' +
|
|
"</div>" +
|
|
'<div class="filter-tags">' +
|
|
filters.map(function (f) {
|
|
return '<button type="button" class="filter-btn' + (destFilter === f.key ? " active" : "") +
|
|
'" data-dest-filter="' + f.key + '">' + f.label + "</button>";
|
|
}).join("") +
|
|
"</div>" +
|
|
'<div class="filter-sort"><select id="dest-sort">' +
|
|
'<option value="rating"' + (destSort === "rating" ? " selected" : "") + ">评分</option>" +
|
|
'<option value="cost-asc"' + (destSort === "cost-asc" ? " selected" : "") + ">费用低→高</option>" +
|
|
'<option value="cost-desc"' + (destSort === "cost-desc" ? " selected" : "") + ">费用高→低</option>" +
|
|
'<option value="speed"' + (destSort === "speed" ? " selected" : "") + ">网速</option>" +
|
|
"</select></div>" +
|
|
"</div>" +
|
|
'<div class="dest-grid" id="dest-grid">' + visible.map(destCard).join("") + "</div>" +
|
|
(typeof limit === "number" && list.length > limit
|
|
? '<div style="text-align:center;margin-top:20px"><button type="button" class="btn btn-ghost" data-nav="explore">查看全部 ' + list.length + " 座城市</button></div>"
|
|
: "") +
|
|
"</div>" +
|
|
"</section>"
|
|
);
|
|
}
|
|
|
|
function renderVisas() {
|
|
var list = (D.visas || []).slice();
|
|
if (visaFilter === "easy") list = list.filter(function (v) { return v.difficulty <= 35; });
|
|
else if (visaFilter === "medium") list = list.filter(function (v) { return v.difficulty > 35 && v.difficulty <= 55; });
|
|
else if (visaFilter === "budget") list = list.filter(function (v) { return v.badge_type === "budget"; });
|
|
var filters = [
|
|
{ key: "all", label: "🌏 全部" },
|
|
{ key: "easy", label: "✅ 简单" },
|
|
{ key: "medium", label: "📋 中等" },
|
|
{ key: "budget", label: "💰 低成本" },
|
|
];
|
|
return (
|
|
'<section class="section visa-section section-compact" id="visa">' +
|
|
'<div class="container">' +
|
|
'<div class="section-header reveal visible">' +
|
|
'<span class="section-tag">📋 VISA</span><h2>远程友好签证</h2>' +
|
|
"<p>离线快照,出发前请再核对官方政策</p>" +
|
|
"</div>" +
|
|
'<div class="visa-filters reveal visible">' +
|
|
filters.map(function (f) {
|
|
return '<button type="button" class="filter-btn' + (visaFilter === f.key ? " active" : "") +
|
|
'" data-visa-filter="' + f.key + '">' + f.label + "</button>";
|
|
}).join("") +
|
|
"</div>" +
|
|
'<div class="visa-grid">' +
|
|
list.map(function (v, i) {
|
|
return (
|
|
'<div class="visa-card reveal visible" style="animation-delay:' + (i * 0.05) + 's">' +
|
|
'<div class="visa-flag">' + esc(v.flag) + "</div>" +
|
|
"<h3>" + esc(v.name) + "</h3>" +
|
|
'<div class="visa-badge ' + esc(v.badge_type) + '">' + esc(v.badge) + "</div>" +
|
|
'<ul class="visa-details">' +
|
|
"<li>📅 " + esc(v.duration) + "</li>" +
|
|
"<li>💰 " + esc(v.income_req) + "</li>" +
|
|
"<li>⏱️ " + esc(v.approval_time) + "</li>" +
|
|
"<li>" + esc(v.extra) + "</li>" +
|
|
"</ul>" +
|
|
'<div class="visa-progress"><span>难度</span>' +
|
|
'<div class="progress-bar"><div class="progress-fill" style="width:' + v.difficulty + '%"></div></div>' +
|
|
"<span>" + esc(v.difficulty_label) + "</span>" +
|
|
"</div>" +
|
|
"</div>"
|
|
);
|
|
}).join("") +
|
|
"</div>" +
|
|
"</div>" +
|
|
"</section>"
|
|
);
|
|
}
|
|
|
|
function renderVoices() {
|
|
var C = D.homeCommunity || {};
|
|
var list = D.testimonials || [];
|
|
var cur = list[voiceActive] || list[0];
|
|
if (!cur) return "";
|
|
return (
|
|
'<section class="section community" id="voices">' +
|
|
'<div class="container">' +
|
|
'<div class="section-header reveal visible">' +
|
|
'<span class="section-tag">' + esc(C.tag || "🤝 COMMUNITY") + "</span>" +
|
|
"<h2>" + esc(C.title || "游民心声") + "</h2>" +
|
|
"<p>" + esc(C.subtitle || "来自全球数字游民的真实故事") + "</p>" +
|
|
"</div>" +
|
|
'<div class="testimonial-carousel reveal visible">' +
|
|
'<div class="testimonial-featured">' +
|
|
'<div class="testimonial-avatar">' + esc(cur.avatar) + "</div>" +
|
|
'<div class="testimonial-content">' +
|
|
'<div class="testimonial-stars">' + "⭐".repeat(cur.rating || 5) + "</div>" +
|
|
"<p>“" + esc(cur.content) + "”</p>" +
|
|
'<div class="testimonial-author"><strong>' + esc(cur.author) + "</strong><span>" + esc(cur.role) + "</span></div>" +
|
|
"</div>" +
|
|
"</div>" +
|
|
'<div class="testimonial-dots">' +
|
|
list.map(function (_t, i) {
|
|
return '<button type="button" class="testimonial-dot' + (i === voiceActive ? " active" : "") +
|
|
'" data-voice="' + i + '" aria-label="' + (i + 1) + '"></button>';
|
|
}).join("") +
|
|
"</div>" +
|
|
"</div>" +
|
|
'<div class="testimonials testimonials-grid">' +
|
|
list.map(function (item) {
|
|
return (
|
|
'<div class="testimonial-card reveal visible">' +
|
|
'<div class="testimonial-avatar">' + esc(item.avatar) + "</div>" +
|
|
'<div class="testimonial-content">' +
|
|
'<div class="testimonial-stars">' + "⭐".repeat(item.rating || 5) + "</div>" +
|
|
"<p>“" + esc(item.content) + "”</p>" +
|
|
'<div class="testimonial-author"><strong>' + esc(item.author) + "</strong><span>" + esc(item.role) + "</span></div>" +
|
|
"</div>" +
|
|
"</div>"
|
|
);
|
|
}).join("") +
|
|
"</div>" +
|
|
"</div>" +
|
|
"</section>"
|
|
);
|
|
}
|
|
|
|
function renderLifestyle() {
|
|
var L = D.lifestyle || {};
|
|
var items = L.items || [];
|
|
if (!items.length) return "";
|
|
if (lifeActive < 0 || lifeActive >= items.length) lifeActive = 0;
|
|
var cur = items[lifeActive];
|
|
return (
|
|
'<section class="section lifestyle" id="lifestyle">' +
|
|
'<div class="container">' +
|
|
'<div class="section-header reveal visible">' +
|
|
'<span class="section-tag">' + esc(L.tag || "💻 NOMAD LIFE") + "</span>" +
|
|
"<h2>" + esc(L.title || "数字游民的一天") + "</h2>" +
|
|
"<p>" + esc(L.subtitle || "") + "</p>" +
|
|
"</div>" +
|
|
'<div class="lifestyle-layout reveal visible">' +
|
|
'<div class="timeline">' +
|
|
'<div class="timeline-line">' +
|
|
'<svg viewBox="0 0 4 600" preserveAspectRatio="none">' +
|
|
'<line x1="2" y1="0" x2="2" y2="600" stroke="url(#timelineGrad)" stroke-width="3" stroke-dasharray="8 6" class="timeline-svg-line" />' +
|
|
"<defs><linearGradient id=\"timelineGrad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">" +
|
|
'<stop offset="0%" stop-color="#FF6B6B" /><stop offset="50%" stop-color="#FFE66D" /><stop offset="100%" stop-color="#4ECDC4" />' +
|
|
"</linearGradient></defs>" +
|
|
"</svg>" +
|
|
"</div>" +
|
|
items.map(function (item, i) {
|
|
return (
|
|
'<div class="timeline-item' + (lifeActive === i ? " active" : "") + '" data-life="' + i + '" role="button" tabindex="0">' +
|
|
'<div class="timeline-time">' + esc(item.time) + "</div>" +
|
|
'<div class="timeline-card">' +
|
|
'<div class="timeline-icon">' + esc(item.icon) + "</div>" +
|
|
"<h3>" + esc(item.title) + "</h3>" +
|
|
"<p>" + esc(item.desc) + "</p>" +
|
|
"</div>" +
|
|
"</div>"
|
|
);
|
|
}).join("") +
|
|
"</div>" +
|
|
'<div class="lifestyle-detail">' +
|
|
'<div class="lifestyle-detail-card">' +
|
|
'<span class="lifestyle-detail-icon">' + esc(cur.icon) + "</span>" +
|
|
'<span class="lifestyle-detail-time">' + esc(cur.time) + "</span>" +
|
|
"<h3>" + esc(cur.title) + "</h3>" +
|
|
"<p>" + esc(cur.desc) + "</p>" +
|
|
'<div class="lifestyle-tip"><span>💡</span><p>' + esc(cur.tip) + "</p></div>" +
|
|
'<div class="dating-empty-actions" style="margin-top:16px">' +
|
|
'<button type="button" class="btn btn-primary btn-sm" data-nav="digital">' + esc(L.ctaDay || "七天落地指南") + "</button>" +
|
|
'<button type="button" class="btn btn-sm" data-nav="meetups">' + esc(L.ctaMeetups || "去参加活动") + "</button>" +
|
|
"</div>" +
|
|
'<div class="lifestyle-dots">' +
|
|
items.map(function (_item, i) {
|
|
return '<button type="button" class="lifestyle-dot' + (lifeActive === i ? " active" : "") +
|
|
'" data-life="' + i + '" aria-label="' + (i + 1) + '"></button>';
|
|
}).join("") +
|
|
"</div>" +
|
|
"</div>" +
|
|
"</div>" +
|
|
"</div>" +
|
|
"</div>" +
|
|
"</section>"
|
|
);
|
|
}
|
|
|
|
function renderFaq() {
|
|
return (
|
|
'<section class="section faq-section section-compact" id="faq">' +
|
|
'<div class="container">' +
|
|
'<div class="section-header reveal visible">' +
|
|
'<span class="section-tag">❓ FAQ</span><h2>常见问题</h2><p>启动资金、网络、保险与税务</p>' +
|
|
"</div>" +
|
|
'<div class="faq-list">' +
|
|
(D.faqs || []).map(function (f) {
|
|
var open = faqActive === f.id;
|
|
return (
|
|
'<div class="faq-item reveal visible' + (open ? " active" : "") + '">' +
|
|
'<button type="button" class="faq-question" data-faq="' + esc(f.id) + '">' +
|
|
"<span>" + esc(f.question) + "</span>" +
|
|
'<svg class="faq-chevron" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6" /></svg>' +
|
|
"</button>" +
|
|
'<div class="faq-answer"><p>' + esc(f.answer) + "</p></div>" +
|
|
"</div>"
|
|
);
|
|
}).join("") +
|
|
"</div>" +
|
|
"</div>" +
|
|
"</section>"
|
|
);
|
|
}
|
|
|
|
function renderFooter() {
|
|
return (
|
|
'<footer class="footer">' +
|
|
'<div class="container">' +
|
|
'<div class="footer-grid footer-grid-rings">' +
|
|
'<div class="footer-brand">' +
|
|
'<a href="#/" class="logo" data-nav="home">' + logoSvg("footerLogoGrad") + "<span>nomadro</span></a>" +
|
|
"<p>让每个人都能自由地工作和生活在这个星球上 🌏</p>" +
|
|
"<p style=\"margin-top:8px;color:var(--text-muted);font-size:12px\">小红书离线镜像 · 完整站 nomadweb.nomadro.com</p>" +
|
|
"</div>" +
|
|
'<div class="footer-links"><h4>探索</h4>' +
|
|
'<a href="#/explore" data-nav="explore">目的地</a>' +
|
|
'<a href="#/visas" data-nav="visas">签证</a>' +
|
|
'<a href="#/matcher" data-nav="matcher">智能匹配</a></div>' +
|
|
'<div class="footer-links"><h4>连接</h4>' +
|
|
'<a href="#/meetups" data-nav="meetups">活动</a>' +
|
|
'<a href="#/community" data-nav="community">社区</a></div>' +
|
|
'<div class="footer-links"><h4>成长</h4>' +
|
|
'<a href="#/digital" data-nav="digital">学院</a>' +
|
|
'<a href="#/blog" data-nav="blog">博客</a>' +
|
|
'<a href="#/tools" data-nav="tools">工具箱</a></div>' +
|
|
"</div>" +
|
|
"</div>" +
|
|
"</footer>"
|
|
);
|
|
}
|
|
|
|
function pageHome() {
|
|
return (
|
|
'<div class="home-flow">' +
|
|
renderNavbar() +
|
|
renderTicker() +
|
|
renderHero() +
|
|
renderJourney() +
|
|
renderDestinations(6) +
|
|
renderLifestyle() +
|
|
renderVisas() +
|
|
renderVoices() +
|
|
renderFaq() +
|
|
renderFooter() +
|
|
"</div>"
|
|
);
|
|
}
|
|
|
|
function pageExplore() {
|
|
return (
|
|
'<div class="home-flow">' +
|
|
renderNavbar() +
|
|
'<div class="container" style="padding-top:calc(var(--nav-height) + 24px)">' +
|
|
'<div class="section-header reveal visible"><span class="section-tag">🧭 EXPLORE</span><h2>探索</h2><p>发现去哪</p></div>' +
|
|
'<div class="tools-grid" style="margin-bottom:24px">' +
|
|
'<button type="button" class="tool-card" data-nav="matcher"><span class="tool-emoji">🎯</span><h3>智能匹配</h3><p>按预算气候排序</p></button>' +
|
|
'<button type="button" class="tool-card" data-nav="visas"><span class="tool-emoji">📋</span><h3>签证指南</h3><p>远程友好签证</p></button>' +
|
|
'<button type="button" class="tool-card" data-nav="compare"><span class="tool-emoji">⚖️</span><h3>城市对比</h3><p>并排看成本网速</p></button>' +
|
|
"</div>" +
|
|
"</div>" +
|
|
renderDestinations() +
|
|
renderFooter() +
|
|
"</div>"
|
|
);
|
|
}
|
|
|
|
function pageDestination(slug) {
|
|
var d = destBySlug(slug);
|
|
if (!d) return '<div class="container" style="padding:120px 24px"><p>未找到城市</p></div>';
|
|
return (
|
|
'<div class="home-flow">' +
|
|
renderNavbar() +
|
|
'<div class="container" style="padding-top:calc(var(--nav-height) + 32px);padding-bottom:48px">' +
|
|
'<nav class="detail-nav"><a href="#/explore" data-nav="explore">← 目的地</a></nav>' +
|
|
'<div class="dest-detail-hero" style="--hue:' + (d.hue || 170) + '">' +
|
|
'<div class="dest-emoji" style="font-size:64px">' + esc(d.emoji) + "</div>" +
|
|
"<h1>" + esc(d.name) + " · " + esc(d.country) + "</h1>" +
|
|
'<p class="section-desc">' + esc(d.tag) + " · " + esc(d.nomads_count) + " 游民</p>" +
|
|
"<p style=\"margin-top:12px;max-width:640px\">" + esc(d.description) + "</p>" +
|
|
'<div class="hero-actions" style="margin-top:16px">' +
|
|
'<button type="button" class="btn btn-primary" data-fav="' + esc(d.slug) + '">' +
|
|
(isFav(d.slug) ? "取消收藏" : "收藏") + "</button>" +
|
|
'<button type="button" class="btn btn-ghost" data-nav="meetups">同城活动</button>' +
|
|
"</div>" +
|
|
"</div>" +
|
|
'<div class="dest-meta" style="margin:20px 0;display:flex;flex-wrap:wrap">' +
|
|
"<span>💰 ¥" + Number(d.cost).toLocaleString() + "/月</span>" +
|
|
"<span style=\"margin-left:16px\">📶 " + d.speed + "Mbps</span>" +
|
|
"<span style=\"margin-left:16px\">🌡️ " + d.temperature + "°C</span>" +
|
|
"<span style=\"margin-left:16px\">⭐ " + d.rating + "</span>" +
|
|
"</div>" +
|
|
'<div class="hl" style="display:flex;flex-wrap:wrap">' +
|
|
(d.highlights || []).map(function (h) {
|
|
return '<span class="dest-tag" style="margin:4px 8px 4px 0">' + esc(h) + "</span>";
|
|
}).join("") +
|
|
"</div>" +
|
|
"</div>" +
|
|
renderFooter() +
|
|
"</div>"
|
|
);
|
|
}
|
|
|
|
function pageConnect() {
|
|
return (
|
|
'<div class="home-flow">' + renderNavbar() +
|
|
'<div class="container" style="padding-top:calc(var(--nav-height) + 32px)">' +
|
|
'<div class="section-header reveal visible"><span class="section-tag">🤝 CONNECT</span><h2>连接</h2><p>遇见同行(离线可浏览)</p></div>' +
|
|
'<div class="tools-grid">' +
|
|
'<button type="button" class="tool-card" data-nav="meetups"><span class="tool-emoji">🎉</span><h3>游民活动</h3><p>同城与线上</p></button>' +
|
|
'<button type="button" class="tool-card" data-nav="community"><span class="tool-emoji">💬</span><h3>社区讨论</h3><p>签证住宿经验</p></button>' +
|
|
'<button type="button" class="tool-card" data-nav="voices"><span class="tool-emoji">🗣️</span><h3>游民心声</h3><p>真实评价</p></button>' +
|
|
"</div></div>" + renderFooter() + "</div>"
|
|
);
|
|
}
|
|
|
|
function pageMeetups() {
|
|
return (
|
|
'<div class="home-flow">' + renderNavbar() +
|
|
'<div class="container" style="padding-top:calc(var(--nav-height) + 32px);padding-bottom:40px">' +
|
|
'<div class="section-header reveal visible"><span class="section-tag">🎉 MEETUPS</span><h2>游民活动</h2><p>离线快照 · RSVP 请到网页版</p></div>' +
|
|
'<div class="dest-grid">' +
|
|
(D.meetups || []).map(function (m) {
|
|
return (
|
|
'<article class="dest-card reveal visible"><div class="dest-body">' +
|
|
'<div class="dest-emoji" style="font-size:32px;margin-bottom:8px">' + esc(m.emoji || "🎉") + "</div>" +
|
|
"<h3>" + esc(m.title) + "</h3>" +
|
|
"<p>" + esc(m.description) + "</p>" +
|
|
'<div class="dest-meta"><span>' + esc(m.city) + "</span><span>" + esc(m.date) + " " + esc(m.time || "") +
|
|
"</span><span>" + (m.rsvp_count || 0) + " 人</span></div>" +
|
|
"</div></article>"
|
|
);
|
|
}).join("") +
|
|
"</div></div>" + renderFooter() + "</div>"
|
|
);
|
|
}
|
|
|
|
function pageCommunity() {
|
|
return (
|
|
'<div class="home-flow">' + renderNavbar() +
|
|
'<div class="container" style="padding-top:calc(var(--nav-height) + 32px);padding-bottom:40px">' +
|
|
'<div class="section-header reveal visible"><span class="section-tag">💬 COMMUNITY</span><h2>社区讨论</h2></div>' +
|
|
'<div class="faq-list">' +
|
|
(D.discussions || []).map(function (d) {
|
|
return (
|
|
'<div class="faq-item reveal visible" style="padding:16px;margin-bottom:12px;background:var(--bg-card);border:var(--border-glass);border-radius:var(--radius-md)">' +
|
|
"<h3 style=\"font-size:16px\">" + (d.is_pinned ? "📌 " : "") + esc(d.title) + "</h3>" +
|
|
"<p style=\"margin-top:8px;color:var(--text-secondary)\">" + esc(d.excerpt) + "</p>" +
|
|
'<div class="dest-meta" style="margin-top:10px"><span>' + esc(d.author) + "</span><span>" +
|
|
(d.reply_count || 0) + " 回复</span><span>♥ " + (d.like_count || 0) + "</span></div>" +
|
|
"</div>"
|
|
);
|
|
}).join("") +
|
|
"</div></div>" + renderFooter() + "</div>"
|
|
);
|
|
}
|
|
|
|
function pageGrow() {
|
|
return (
|
|
'<div class="home-flow">' + renderNavbar() +
|
|
'<div class="container" style="padding-top:calc(var(--nav-height) + 32px)">' +
|
|
'<div class="section-header reveal visible"><span class="section-tag">🌱 GROW</span><h2>成长</h2><p>路上做事</p></div>' +
|
|
'<div class="tools-grid">' +
|
|
'<button type="button" class="tool-card" data-nav="digital"><span class="tool-emoji">🎓</span><h3>游民学院</h3><p>远程工作课程</p></button>' +
|
|
'<button type="button" class="tool-card" data-nav="blog"><span class="tool-emoji">📝</span><h3>博客</h3><p>指南与攻略</p></button>' +
|
|
'<button type="button" class="tool-card" data-nav="tools"><span class="tool-emoji">🧰</span><h3>工具箱</h3><p>费用估算</p></button>' +
|
|
'<button type="button" class="tool-card" data-nav="faq"><span class="tool-emoji">❓</span><h3>FAQ</h3><p>常见问题</p></button>' +
|
|
"</div></div>" + renderFooter() + "</div>"
|
|
);
|
|
}
|
|
|
|
function pageDigital() {
|
|
var modules = D.course || [];
|
|
return (
|
|
'<div class="home-flow">' + renderNavbar() +
|
|
'<div class="container" style="padding-top:calc(var(--nav-height) + 32px);padding-bottom:40px">' +
|
|
'<div class="section-header reveal visible"><span class="section-tag">🎓 ACADEMY</span><h2>游民学院</h2></div>' +
|
|
modules.map(function (mod, mi) {
|
|
return (
|
|
'<div class="section-header" style="margin-top:24px"><h3>' + esc(mod.title) + "</h3></div>" +
|
|
'<div class="tools-grid">' +
|
|
(mod.lessons || []).map(function (les, li) {
|
|
var key = mi + "-" + li;
|
|
var lesson = (D.lessons || {})[key] || {};
|
|
return (
|
|
'<article class="tool-card">' +
|
|
"<h3>" + (les.free ? "🆓 " : "🔒 ") + esc(les.title) + "</h3>" +
|
|
"<p>" + esc(les.duration) + "</p>" +
|
|
"<p style=\"margin-top:8px;color:var(--text-secondary);font-size:13px\">" + esc(lesson.content || "") + "</p>" +
|
|
"</article>"
|
|
);
|
|
}).join("") +
|
|
"</div>"
|
|
);
|
|
}).join("") +
|
|
"</div>" + renderFooter() + "</div>"
|
|
);
|
|
}
|
|
|
|
function pageBlog() {
|
|
return (
|
|
'<div class="home-flow">' + renderNavbar() +
|
|
'<div class="container" style="padding-top:calc(var(--nav-height) + 32px);padding-bottom:40px">' +
|
|
'<div class="section-header reveal visible"><span class="section-tag">📝 BLOG</span><h2>博客</h2></div>' +
|
|
'<div class="dest-grid">' +
|
|
(D.blog || []).map(function (b) {
|
|
return (
|
|
'<article class="dest-card reveal visible">' +
|
|
'<a href="#/blog/' + esc(b.slug) + '" class="dest-card-link" data-blog="' + esc(b.slug) + '">' +
|
|
'<div class="dest-body">' +
|
|
'<div class="dest-emoji" style="font-size:36px">' + esc(b.emoji) + "</div>" +
|
|
"<h3>" + esc(b.title) + "</h3>" +
|
|
"<p>" + esc(b.excerpt) + "</p>" +
|
|
'<div class="dest-meta"><span>' + esc(b.author) + "</span><span>" + esc(b.published_at) +
|
|
"</span><span>" + (b.read_time || "?") + " 分钟</span></div>" +
|
|
"</div></a></article>"
|
|
);
|
|
}).join("") +
|
|
"</div></div>" + renderFooter() + "</div>"
|
|
);
|
|
}
|
|
|
|
function pageBlogDetail(slug) {
|
|
var b = null;
|
|
var list = D.blog || [];
|
|
for (var i = 0; i < list.length; i++) if (list[i].slug === slug) b = list[i];
|
|
if (!b) return '<div class="container" style="padding:120px 24px"><p>文章不存在</p></div>';
|
|
var content = (D.blogContent && D.blogContent[slug]) || b.excerpt || "";
|
|
return (
|
|
'<div class="home-flow">' + renderNavbar() +
|
|
'<div class="container" style="padding-top:calc(var(--nav-height) + 32px);padding-bottom:48px;max-width:720px">' +
|
|
'<nav class="detail-nav"><a href="#/blog" data-nav="blog">← 博客</a></nav>' +
|
|
'<div class="section-header"><span class="section-tag">' + esc(b.emoji) + "</span>" +
|
|
"<h1 style=\"font-size:28px\">" + esc(b.title) + "</h1>" +
|
|
'<p>' + esc(b.author) + " · " + esc(b.published_at) + " · " + (b.read_time || "?") + " 分钟</p></div>" +
|
|
'<article class="blog-prose" style="white-space:pre-wrap;line-height:1.75;color:var(--text-secondary)">' +
|
|
esc(content) +
|
|
"</article>" +
|
|
"</div>" + renderFooter() + "</div>"
|
|
);
|
|
}
|
|
|
|
function pageTools() {
|
|
return (
|
|
'<div class="home-flow">' + renderNavbar() +
|
|
'<div class="container" style="padding-top:calc(var(--nav-height) + 32px);padding-bottom:40px">' +
|
|
'<div class="section-header reveal visible"><span class="section-tag">🧰 TOOLS</span><h2>工具箱</h2></div>' +
|
|
'<div class="tools-grid">' +
|
|
(D.tools || []).map(function (t) {
|
|
return (
|
|
'<article class="tool-card"><span class="tool-emoji">' + esc(t.emoji) + "</span>" +
|
|
"<h3>" + esc(t.name) + "</h3><p>" + esc(t.description) + "</p></article>"
|
|
);
|
|
}).join("") +
|
|
'<article class="tool-card"><span class="tool-emoji">🧮</span><h3>月费用估算</h3>' +
|
|
'<div style="margin-top:12px">' +
|
|
'<label style="font-size:12px;color:var(--text-muted)">住宿</label>' +
|
|
'<input id="c-house" type="number" value="2500" style="width:100%;margin:4px 0 8px;padding:8px;border-radius:8px;border:var(--border-glass);background:var(--bg-glass);color:inherit" />' +
|
|
'<label style="font-size:12px;color:var(--text-muted)">餐饮</label>' +
|
|
'<input id="c-food" type="number" value="1500" style="width:100%;margin:4px 0 8px;padding:8px;border-radius:8px;border:var(--border-glass);background:var(--bg-glass);color:inherit" />' +
|
|
'<label style="font-size:12px;color:var(--text-muted)">其他</label>' +
|
|
'<input id="c-other" type="number" value="1500" style="width:100%;margin:4px 0 8px;padding:8px;border-radius:8px;border:var(--border-glass);background:var(--bg-glass);color:inherit" />' +
|
|
'<button type="button" class="btn btn-primary btn-sm" id="c-run">计算</button>' +
|
|
'<p id="c-out" style="margin-top:10px;color:var(--accent-3)"></p>' +
|
|
"</div></article>" +
|
|
"</div></div>" + renderFooter() + "</div>"
|
|
);
|
|
}
|
|
|
|
function pageMatcher() {
|
|
var budget = 6000;
|
|
var scored = (D.destinations || []).map(function (d) {
|
|
var score = 40 + (d.rating || 0) * 4 + Math.min(20, (d.speed || 0) / 10);
|
|
if (d.cost <= budget) score += 20; else score -= 10;
|
|
return { d: d, score: Math.round(score) };
|
|
}).sort(function (a, b) { return b.score - a.score; });
|
|
return (
|
|
'<div class="home-flow">' + renderNavbar() +
|
|
'<div class="container" style="padding-top:calc(var(--nav-height) + 32px);padding-bottom:40px">' +
|
|
'<div class="section-header reveal visible"><span class="section-tag">🎯 MATCHER</span><h2>智能匹配</h2><p>离线本地打分 · 默认预算 ¥6000</p></div>' +
|
|
'<div class="dest-grid">' + scored.map(function (item) {
|
|
return destCard(item.d).replace("</h3>", " · 匹配 " + item.score + "</h3>");
|
|
}).join("") + "</div>" +
|
|
"</div>" + renderFooter() + "</div>"
|
|
);
|
|
}
|
|
|
|
function pageCompare() {
|
|
var list = D.destinations || [];
|
|
var a = list[0] || {};
|
|
var b = list[1] || {};
|
|
return (
|
|
'<div class="home-flow">' + renderNavbar() +
|
|
'<div class="container" style="padding-top:calc(var(--nav-height) + 32px);padding-bottom:40px">' +
|
|
'<div class="section-header reveal visible"><span class="section-tag">⚖️ COMPARE</span><h2>城市对比</h2></div>' +
|
|
'<div class="dest-grid">' +
|
|
(a.slug ? destCard(a) : "") +
|
|
(b.slug ? destCard(b) : "") +
|
|
"</div>" +
|
|
'<div class="container" style="margin-top:20px;overflow:auto">' +
|
|
'<table style="width:100%;border-collapse:collapse;font-size:14px">' +
|
|
"<tr><th align=\"left\">指标</th><th align=\"left\">" + esc(a.name) + "</th><th align=\"left\">" + esc(b.name) + "</th></tr>" +
|
|
"<tr><td>月费</td><td>¥" + a.cost + "</td><td>¥" + b.cost + "</td></tr>" +
|
|
"<tr><td>网速</td><td>" + a.speed + "</td><td>" + b.speed + "</td></tr>" +
|
|
"<tr><td>均温</td><td>" + a.temperature + "</td><td>" + b.temperature + "</td></tr>" +
|
|
"<tr><td>评分</td><td>" + a.rating + "</td><td>" + b.rating + "</td></tr>" +
|
|
"</table></div>" +
|
|
"</div>" + renderFooter() + "</div>"
|
|
);
|
|
}
|
|
|
|
function pageMine() {
|
|
var cards = favs.map(function (slug) {
|
|
var d = destBySlug(slug);
|
|
return d ? destCard(d) : "";
|
|
}).join("");
|
|
return (
|
|
'<div class="home-flow">' + renderNavbar() +
|
|
'<div class="container" style="padding-top:calc(var(--nav-height) + 32px);padding-bottom:40px">' +
|
|
'<div class="section-header reveal visible"><span class="section-tag">👤 MINE</span><h2>我的</h2>' +
|
|
"<p>" + esc(D.offlineNote || "") + "</p></div>" +
|
|
'<h3 style="margin:16px 0">收藏的城市</h3>' +
|
|
'<div class="dest-grid">' + (cards || '<p class="dest-empty">还没有收藏</p>') + "</div>" +
|
|
"</div>" + renderFooter() + "</div>"
|
|
);
|
|
}
|
|
|
|
function pageVisasOnly() {
|
|
return '<div class="home-flow">' + renderNavbar() +
|
|
'<div style="padding-top:var(--nav-height)">' + renderVisas() + "</div>" + renderFooter() + "</div>";
|
|
}
|
|
|
|
function pageVoicesOnly() {
|
|
return '<div class="home-flow">' + renderNavbar() +
|
|
'<div style="padding-top:var(--nav-height)">' + renderVoices() + "</div>" + renderFooter() + "</div>";
|
|
}
|
|
|
|
function pageFaqOnly() {
|
|
return '<div class="home-flow">' + renderNavbar() +
|
|
'<div style="padding-top:var(--nav-height)">' + renderFaq() + "</div>" + renderFooter() + "</div>";
|
|
}
|
|
|
|
function render() {
|
|
var html = "";
|
|
switch (route.name) {
|
|
case "home": html = pageHome(); break;
|
|
case "explore": html = pageExplore(); break;
|
|
case "destination": html = pageDestination(route.params.slug); break;
|
|
case "connect": html = pageConnect(); break;
|
|
case "meetups": html = pageMeetups(); break;
|
|
case "community": html = pageCommunity(); break;
|
|
case "voices": html = pageVoicesOnly(); break;
|
|
case "grow": html = pageGrow(); break;
|
|
case "digital": html = pageDigital(); break;
|
|
case "blog": html = pageBlog(); break;
|
|
case "blog-detail": html = pageBlogDetail(route.params.slug); break;
|
|
case "tools": html = pageTools(); break;
|
|
case "matcher": html = pageMatcher(); break;
|
|
case "compare": html = pageCompare(); break;
|
|
case "visas": html = pageVisasOnly(); break;
|
|
case "faq": html = pageFaqOnly(); break;
|
|
case "mine": html = pageMine(); break;
|
|
default: html = pageHome();
|
|
}
|
|
root.innerHTML = html;
|
|
backBtn.className = "xhs-back" + (route.name === "home" ? "" : " is-on");
|
|
bind();
|
|
startTypewriter();
|
|
}
|
|
|
|
function bind() {
|
|
Array.prototype.forEach.call(document.querySelectorAll("[data-nav]"), function (el) {
|
|
on(el, "click", function (e) {
|
|
e.preventDefault();
|
|
go(el.getAttribute("data-nav"));
|
|
});
|
|
});
|
|
Array.prototype.forEach.call(document.querySelectorAll("[data-dest]"), function (el) {
|
|
on(el, "click", function (e) {
|
|
e.preventDefault();
|
|
go("destination", { slug: el.getAttribute("data-dest") });
|
|
});
|
|
});
|
|
Array.prototype.forEach.call(document.querySelectorAll("[data-blog]"), function (el) {
|
|
on(el, "click", function (e) {
|
|
e.preventDefault();
|
|
go("blog-detail", { slug: el.getAttribute("data-blog") });
|
|
});
|
|
});
|
|
Array.prototype.forEach.call(document.querySelectorAll("[data-dest-filter]"), function (el) {
|
|
on(el, "click", function () {
|
|
destFilter = el.getAttribute("data-dest-filter");
|
|
render();
|
|
});
|
|
});
|
|
Array.prototype.forEach.call(document.querySelectorAll("[data-visa-filter]"), function (el) {
|
|
on(el, "click", function () {
|
|
visaFilter = el.getAttribute("data-visa-filter");
|
|
render();
|
|
});
|
|
});
|
|
Array.prototype.forEach.call(document.querySelectorAll("[data-fav]"), function (el) {
|
|
on(el, "click", function () { toggleFav(el.getAttribute("data-fav")); });
|
|
});
|
|
Array.prototype.forEach.call(document.querySelectorAll("[data-life]"), function (el) {
|
|
on(el, "click", function () {
|
|
lifeActive = Number(el.getAttribute("data-life")) || 0;
|
|
render();
|
|
});
|
|
});
|
|
Array.prototype.forEach.call(document.querySelectorAll("[data-voice]"), function (el) {
|
|
on(el, "click", function () {
|
|
voiceActive = Number(el.getAttribute("data-voice")) || 0;
|
|
render();
|
|
});
|
|
});
|
|
Array.prototype.forEach.call(document.querySelectorAll("[data-faq]"), function (el) {
|
|
on(el, "click", function () {
|
|
var id = el.getAttribute("data-faq");
|
|
faqActive = faqActive === id ? null : id;
|
|
render();
|
|
});
|
|
});
|
|
Array.prototype.forEach.call(document.querySelectorAll("[data-scroll]"), function (el) {
|
|
on(el, "click", function (e) {
|
|
e.preventDefault();
|
|
var id = el.getAttribute("data-scroll");
|
|
var node = document.getElementById(id);
|
|
if (node) node.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
});
|
|
});
|
|
var search = document.getElementById("dest-search");
|
|
if (search) {
|
|
on(search, "change", function () { destSearch = search.value || ""; render(); });
|
|
on(search, "keydown", function (e) {
|
|
if (e.key === "Enter") { destSearch = search.value || ""; render(); }
|
|
});
|
|
}
|
|
var sort = document.getElementById("dest-sort");
|
|
if (sort) on(sort, "change", function () { destSort = sort.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 out = document.getElementById("c-out");
|
|
if (out) out.textContent = "预估月开销约 ¥" + (n("c-house") + n("c-food") + n("c-other"));
|
|
});
|
|
}
|
|
}
|
|
|
|
var typeTimer = null;
|
|
function startTypewriter() {
|
|
var el = document.getElementById("typewriter");
|
|
if (!el) return;
|
|
var PHRASES = [
|
|
"数字游民不是逃离生活,而是用科技重新定义生活。",
|
|
"在巴厘岛写代码,在里斯本开会议,在清迈看日落 ☀️",
|
|
"世界那么大,你的办公室可以在任何地方 🌍",
|
|
"远程工作 + 全球旅居 = 无限可能 ✨",
|
|
];
|
|
var phraseIdx = 0, charIdx = 0, deleting = false;
|
|
if (typeTimer) clearTimeout(typeTimer);
|
|
function type() {
|
|
var current = PHRASES[phraseIdx];
|
|
if (!deleting) {
|
|
el.textContent = current.substring(0, charIdx + 1);
|
|
charIdx++;
|
|
if (charIdx === current.length) { deleting = true; typeTimer = setTimeout(type, 2500); return; }
|
|
typeTimer = setTimeout(type, 60);
|
|
} else {
|
|
el.textContent = current.substring(0, charIdx - 1);
|
|
charIdx--;
|
|
if (charIdx === 0) { deleting = false; phraseIdx = (phraseIdx + 1) % PHRASES.length; }
|
|
typeTimer = setTimeout(type, 30);
|
|
}
|
|
}
|
|
typeTimer = setTimeout(type, 400);
|
|
}
|
|
|
|
function parseHash() {
|
|
var h = (location.hash || "#/").replace(/^#\/?/, "");
|
|
var parts = h.split("/");
|
|
if (!parts[0] || parts[0] === "") return go("home");
|
|
if (parts[0] === "destination" && parts[1]) return go("destination", { slug: parts[1] });
|
|
if (parts[0] === "blog" && parts[1]) return go("blog-detail", { slug: parts[1] });
|
|
return go(parts[0]);
|
|
}
|
|
|
|
on(backBtn, "click", function () {
|
|
if (history.length > 1) history.back();
|
|
else go("home");
|
|
});
|
|
on(window, "hashchange", parseHash);
|
|
|
|
// enrich stats like H5 API
|
|
if (!D.stats) D.stats = {};
|
|
D.stats.total_nomads = D.stats.total_nomads || "35.6M";
|
|
D.stats.countries = D.stats.countries || 195;
|
|
D.stats.satisfaction = D.stats.satisfaction || 87;
|
|
|
|
storageGet(FAV_KEY, []).then(function (v) {
|
|
favs = Array.isArray(v) ? v : [];
|
|
if (location.hash && location.hash.length > 2) parseHash();
|
|
else render();
|
|
});
|
|
})();
|