Fix redmini lifestyle/voices/FAQ to use real H5 DOM classes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
eric 2026-09-27 13:20:01 -05:00
parent dbeac128bb
commit 2bcbc303d8
8 changed files with 319 additions and 66 deletions

View File

@ -17,6 +17,9 @@
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)
@ -336,18 +339,45 @@
}
function renderVoices() {
var C = D.homeCommunity || {};
var list = D.testimonials || [];
var cur = list[voiceActive] || list[0];
if (!cur) return "";
return (
'<section class="section section-compact" id="voices">' +
'<section class="section community" id="voices">' +
'<div class="container">' +
'<div class="section-header reveal visible"><span class="section-tag">🗣️ VOICES</span><h2>游民心声</h2><p>真实旅居反馈</p></div>' +
'<div class="testimonials">' +
(D.testimonials || []).map(function (t) {
'<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 (
'<article class="testimonial-card reveal visible">' +
'<div class="t-avatar">' + esc(t.avatar) + "</div>" +
"<p>" + esc(t.content) + "</p>" +
'<div class="t-meta"><strong>' + esc(t.author) + "</strong><span>" + esc(t.role) + "</span></div>" +
"</article>"
'<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>" +
@ -357,21 +387,61 @@
}
function renderLifestyle() {
var items = D.lifestyle || [];
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 section-compact" id="lifestyle">' +
'<section class="section lifestyle" id="lifestyle">' +
'<div class="container">' +
'<div class="section-header reveal visible"><span class="section-tag">🗓️ NOMAD LIFE</span><h2>游民的一天</h2><p>点选时段看细节</p></div>' +
'<div class="lifestyle-grid">' +
items.map(function (it, idx) {
return (
'<button type="button" class="lifestyle-card' + (idx === 0 ? " active" : "") + '" data-life="' + idx + '">' +
'<span class="life-emoji">' + esc(it.emoji) + "</span>" +
"<strong>" + esc(it.title) + "</strong>" +
'<p class="life-body">' + esc(it.body) + "</p>" +
"</button>"
);
}).join("") +
'<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>"
@ -380,16 +450,22 @@
function renderFaq() {
return (
'<section class="section section-compact" id="faq">' +
'<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></div>' +
'<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 (
'<details class="faq-item reveal visible">' +
"<summary>" + esc(f.question) + "</summary>" +
"<p>" + esc(f.answer) + "</p>" +
"</details>"
'<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>" +
@ -797,6 +873,25 @@
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();

File diff suppressed because one or more lines are too long

View File

@ -8,6 +8,14 @@
<link rel="stylesheet" href="./nomadro.css" />
<style>
/* XHS offline shell — keep H5 look; hide web-only chrome that needs network */
button {
font: inherit;
color: inherit;
background: transparent;
border: 0;
padding: 0;
cursor: pointer;
}
.xhs-offline-banner {
position: sticky;
top: 0;

View File

@ -60,13 +60,60 @@ def main() -> int:
{"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": "社区圆桌、兴趣局,或安静充电。"},
],
"lifestyle": {
"tag": "💻 NOMAD LIFE",
"title": "数字游民的一天",
"subtitle": "自由不等于散漫,高效工作才能尽情探索 — 点击卡片查看详情",
"ctaDay": "七天落地指南",
"ctaMeetups": "去参加活动",
"items": [
{
"key": "morning",
"time": "🌅 07:00",
"icon": "🧘",
"title": "晨间仪式",
"desc": "瑜伽、冥想或海边跑步,用身心唤醒开启新的一天",
"tip": "推荐:清迈河边晨跑、巴厘岛海滩瑜伽",
},
{
"key": "work",
"time": "☕ 09:00",
"icon": "💻",
"title": "深度工作",
"desc": "在 Co-working Space 或咖啡馆专注 4 小时,完成核心任务",
"tip": "推荐:Punspace 清迈、Outsite 里斯本",
},
{
"key": "explore",
"time": "🍜 13:00",
"icon": "🍽️",
"title": "本地探索",
"desc": "品尝地道美食,与当地人交流,感受文化碰撞",
"tip": "推荐:清迈夜市、巴塞罗那 Tapas 之旅",
},
{
"key": "collab",
"time": "🌇 17:00",
"icon": "📞",
"title": "跨时区协作",
"desc": "与全球团队视频会议,灵活安排跨时区沟通",
"tip": "工具:Calendly 排时区、World Time Buddy",
},
{
"key": "social",
"time": "🌙 20:00",
"icon": "🎉",
"title": "社群社交",
"desc": "参加 Nomad Meetup,结识来自世界各地的同行者",
"tip": "平台:nomadro 活动、同城社群",
},
],
},
"homeCommunity": {
"tag": "🤝 COMMUNITY",
"title": "游民心声",
"subtitle": "来自全球数字游民的真实故事",
},
"planChecklist": [
{"id": "visa", "label": "确认签证/停留天数"},
{"id": "flight", "label": "预订机票"},

File diff suppressed because one or more lines are too long

View File

@ -17,6 +17,9 @@
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)
@ -336,18 +339,45 @@
}
function renderVoices() {
var C = D.homeCommunity || {};
var list = D.testimonials || [];
var cur = list[voiceActive] || list[0];
if (!cur) return "";
return (
'<section class="section section-compact" id="voices">' +
'<section class="section community" id="voices">' +
'<div class="container">' +
'<div class="section-header reveal visible"><span class="section-tag">🗣️ VOICES</span><h2>游民心声</h2><p>真实旅居反馈</p></div>' +
'<div class="testimonials">' +
(D.testimonials || []).map(function (t) {
'<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 (
'<article class="testimonial-card reveal visible">' +
'<div class="t-avatar">' + esc(t.avatar) + "</div>" +
"<p>" + esc(t.content) + "</p>" +
'<div class="t-meta"><strong>' + esc(t.author) + "</strong><span>" + esc(t.role) + "</span></div>" +
"</article>"
'<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>" +
@ -357,21 +387,61 @@
}
function renderLifestyle() {
var items = D.lifestyle || [];
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 section-compact" id="lifestyle">' +
'<section class="section lifestyle" id="lifestyle">' +
'<div class="container">' +
'<div class="section-header reveal visible"><span class="section-tag">🗓️ NOMAD LIFE</span><h2>游民的一天</h2><p>点选时段看细节</p></div>' +
'<div class="lifestyle-grid">' +
items.map(function (it, idx) {
return (
'<button type="button" class="lifestyle-card' + (idx === 0 ? " active" : "") + '" data-life="' + idx + '">' +
'<span class="life-emoji">' + esc(it.emoji) + "</span>" +
"<strong>" + esc(it.title) + "</strong>" +
'<p class="life-body">' + esc(it.body) + "</p>" +
"</button>"
);
}).join("") +
'<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>"
@ -380,16 +450,22 @@
function renderFaq() {
return (
'<section class="section section-compact" id="faq">' +
'<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></div>' +
'<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 (
'<details class="faq-item reveal visible">' +
"<summary>" + esc(f.question) + "</summary>" +
"<p>" + esc(f.answer) + "</p>" +
"</details>"
'<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>" +
@ -797,6 +873,25 @@
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();

File diff suppressed because one or more lines are too long

View File

@ -8,6 +8,14 @@
<link rel="stylesheet" href="./nomadro.css" />
<style>
/* XHS offline shell — keep H5 look; hide web-only chrome that needs network */
button {
font: inherit;
color: inherit;
background: transparent;
border: 0;
padding: 0;
cursor: pointer;
}
.xhs-offline-banner {
position: sticky;
top: 0;