Add visa stay countdown and cowork-vs-cafe cost comparator.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
eric 2026-08-29 02:09:55 -05:00
parent 2943f48035
commit 45b4aaef53
9 changed files with 483 additions and 9 deletions

View File

@ -114,6 +114,8 @@ docker compose up -d
| 🤝 会议黄金时段 | 双时区工作重叠可视化 | | 🤝 会议黄金时段 | 双时区工作重叠可视化 |
| ☀️ 日照办公窗 | 日出日落与深度工作建议 | | ☀️ 日照办公窗 | 日出日落与深度工作建议 |
| ⚖️ 行李重量 | 对照航司限额估算装备重量 | | ⚖️ 行李重量 | 对照航司限额估算装备重量 |
| 🛂 签证停留倒计时 | 入境日与剩余天数提醒 |
| 💼 办公点成本 | 月卡 vs 咖啡馆划算度对比 |
| ✨ 今日金句 | 每日游民灵感语录 | | ✨ 今日金句 | 每日游民灵感语录 |
| 😴 时差估算 | 时差跨度与恢复期建议 | | 😴 时差估算 | 时差跨度与恢复期建议 |
| 🏡 住宿指南 | Villa / Coliving / 公寓方案对比 | | 🏡 住宿指南 | Villa / Coliving / 公寓方案对比 |

View File

@ -12,11 +12,10 @@ const LOGS = [
date: "2026-08-29", date: "2026-08-29",
tag: "新工具", tag: "新工具",
items: [ items: [
"日照办公窗:按城市月份估算日出日落与深度工作时段", "签证停留倒计时:入境日 + 额度,剩余天数环形提醒",
"行李重量估算:勾选装备对照航司限额", "办公点成本对比:联合办公月卡 vs 咖啡馆日刷",
"旅居跑道计算器:存款 + 月开销一眼算出还能飞多久", "日照办公窗、行李重量、旅居跑道、会议黄金时段",
"会议黄金时段:双时区工作重叠可视化时间轴", "快捷工具条直达签证 / 办公 / 跑道等核心工具",
"快捷工具条更新,直达日照 / 行李 / 跑道 / 会议",
], ],
}, },
{ {

View File

@ -8112,3 +8112,188 @@ img { max-width: 100%; display: block; }
.bag-grid { grid-template-columns: 1fr; } .bag-grid { grid-template-columns: 1fr; }
.bag-extra { grid-template-columns: 1fr; } .bag-extra { grid-template-columns: 1fr; }
} }
/* ===== Visa Stay Countdown ===== */
.visa-stay-card {
display: grid;
grid-template-columns: minmax(220px, 280px) 1fr;
gap: 28px;
padding: 28px;
background: var(--bg-card);
border: var(--border-glass);
border-radius: var(--radius-xl);
backdrop-filter: blur(16px);
}
.visa-stay-meter { text-align: center; }
.visa-stay-ring {
width: 168px;
height: 168px;
border-radius: 50%;
margin: 0 auto 16px;
display: grid;
place-items: center;
}
.visa-stay-ring-inner {
width: 128px;
height: 128px;
border-radius: 50%;
background: var(--bg-secondary);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.visa-stay-ring-inner strong {
font-family: var(--font-display);
font-size: 2.6rem;
line-height: 1;
}
.visa-stay-ring-inner small { color: var(--text-muted); margin-top: 4px; }
.visa-stay-level span { font-weight: 700; font-size: 1.05rem; }
.visa-stay-level p {
color: var(--text-secondary);
font-size: 0.88rem;
margin-top: 6px;
line-height: 1.45;
}
.visa-stay-form {
display: flex;
flex-direction: column;
gap: 14px;
}
.visa-stay-form > label,
.workspot-controls > label {
display: flex;
flex-direction: column;
gap: 6px;
}
.visa-stay-form > label > span,
.workspot-controls > label > span {
font-size: 0.85rem;
color: var(--text-secondary);
}
.visa-stay-form input,
.visa-stay-form select,
.workspot-controls select {
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.1);
border-radius: var(--radius-sm);
color: var(--text-primary);
padding: 10px 12px;
font-size: 1rem;
}
.visa-stay-hint {
margin: 0;
font-size: 0.85rem;
color: var(--accent-3);
}
.visa-stay-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
padding: 12px;
background: rgba(255,255,255,0.03);
border-radius: var(--radius-md);
}
.visa-stay-stats small { display: block; color: var(--text-muted); }
.visa-stay-stats strong { font-size: 1.05rem; }
/* ===== Work Spot Cost ===== */
.workspot-card {
display: grid;
grid-template-columns: 1fr 1.1fr;
gap: 22px;
padding: 24px;
background: var(--bg-card);
border: var(--border-glass);
border-radius: var(--radius-xl);
backdrop-filter: blur(16px);
}
.workspot-controls {
display: flex;
flex-direction: column;
gap: 14px;
}
.workspot-controls input[type=range],
.visa-stay-form input[type=range] {
width: 100%;
accent-color: var(--accent-3);
}
.workspot-modes {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.workspot-mode {
padding: 10px 8px;
border-radius: var(--radius-md);
border: 1px solid rgba(255,255,255,0.08);
background: rgba(255,255,255,0.03);
color: inherit;
cursor: pointer;
font-weight: 600;
font-size: 0.85rem;
transition: all 0.2s;
}
.workspot-mode:hover,
.workspot-mode.active {
border-color: rgba(78, 205, 196, 0.4);
background: rgba(78, 205, 196, 0.1);
}
.workspot-result {
display: flex;
flex-direction: column;
gap: 14px;
}
.workspot-hero {
padding: 18px;
border-radius: var(--radius-md);
background: linear-gradient(135deg, rgba(78,205,196,0.12), rgba(255,107,107,0.08));
border: 1px solid rgba(255,255,255,0.08);
}
.workspot-hero small { color: var(--text-muted); display: block; }
.workspot-hero strong {
font-family: var(--font-display);
font-size: 2.2rem;
display: block;
margin: 4px 0 6px;
}
.workspot-hero > span { font-weight: 600; font-size: 0.92rem; }
.workspot-compare {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.workspot-compare > div {
padding: 12px;
border-radius: var(--radius-md);
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
transition: all 0.2s;
}
.workspot-compare > div.on {
border-color: rgba(78, 205, 196, 0.4);
background: rgba(78, 205, 196, 0.1);
}
.workspot-compare small { display: block; color: var(--text-muted); font-size: 0.75rem; }
.workspot-compare strong { display: block; margin: 4px 0; font-size: 1.05rem; }
.workspot-compare em {
font-style: normal;
font-size: 0.75rem;
color: var(--text-secondary);
line-height: 1.35;
}
.workspot-tip {
margin: 0;
color: var(--text-secondary);
font-size: 0.9rem;
line-height: 1.45;
}
@media (max-width: 900px) {
.visa-stay-card,
.workspot-card { grid-template-columns: 1fr; }
.workspot-compare { grid-template-columns: 1fr; }
.visa-stay-stats { grid-template-columns: 1fr; }
}

View File

@ -5,7 +5,7 @@ import Link from "next/link";
import { TOOL_CATEGORIES, TOOL_LINKS } from "@/lib/tools"; import { TOOL_CATEGORIES, TOOL_LINKS } from "@/lib/tools";
import SiteShell from "@/components/SiteShell"; import SiteShell from "@/components/SiteShell";
const FEATURED = ["daylight", "bag-weight", "runway", "meetings", "trip", "focus"]; const FEATURED = ["visa-stay", "workspot", "runway", "daylight", "meetings", "bag-weight"];
export default function ToolsHubPage() { export default function ToolsHubPage() {
const [filter, setFilter] = useState("all"); const [filter, setFilter] = useState("all");
@ -41,7 +41,7 @@ export default function ToolsHubPage() {
<div className="tools-featured"> <div className="tools-featured">
<div className="tools-featured-head"> <div className="tools-featured-head">
<strong>✨ 本周推荐</strong> <strong>✨ 本周推荐</strong>
<span>新上线 · 日照窗 & 行李重量</span> <span>新上线 · 停留倒计时 & 办公成本</span>
</div> </div>
<div className="tools-featured-grid"> <div className="tools-featured-grid">
{featured.map((t) => ( {featured.map((t) => (

View File

@ -44,11 +44,13 @@ const CurrencyConverter = lazy(() => import("./CurrencyConverter"));
const MeetingWindow = lazy(() => import("./MeetingWindow")); const MeetingWindow = lazy(() => import("./MeetingWindow"));
const DaylightWorkHours = lazy(() => import("./DaylightWorkHours")); const DaylightWorkHours = lazy(() => import("./DaylightWorkHours"));
const WifiWorkScore = lazy(() => import("./WifiWorkScore")); const WifiWorkScore = lazy(() => import("./WifiWorkScore"));
const WorkSpotCost = lazy(() => import("./WorkSpotCost"));
const FocusTimer = lazy(() => import("./FocusTimer")); const FocusTimer = lazy(() => import("./FocusTimer"));
const NomadScore = lazy(() => import("./NomadScore")); const NomadScore = lazy(() => import("./NomadScore"));
const Lifestyle = lazy(() => import("./Lifestyle")); const Lifestyle = lazy(() => import("./Lifestyle"));
const Charts = lazy(() => import("./Charts")); const Charts = lazy(() => import("./Charts"));
const VisaSection = lazy(() => import("./VisaSection")); const VisaSection = lazy(() => import("./VisaSection"));
const VisaStayCountdown = lazy(() => import("./VisaStayCountdown"));
const InsuranceGuide = lazy(() => import("./InsuranceGuide")); const InsuranceGuide = lazy(() => import("./InsuranceGuide"));
const EmergencyContacts = lazy(() => import("./EmergencyContacts")); const EmergencyContacts = lazy(() => import("./EmergencyContacts"));
const AtmCashGuide = lazy(() => import("./AtmCashGuide")); const AtmCashGuide = lazy(() => import("./AtmCashGuide"));
@ -177,11 +179,13 @@ export default function HomeClient(props: Props) {
<MeetingWindow destinations={props.destinations} /> <MeetingWindow destinations={props.destinations} />
<DaylightWorkHours destinations={props.destinations} /> <DaylightWorkHours destinations={props.destinations} />
<WifiWorkScore destinations={props.destinations} /> <WifiWorkScore destinations={props.destinations} />
<WorkSpotCost destinations={props.destinations} />
<FocusTimer /> <FocusTimer />
<NomadScore /> <NomadScore />
<Lifestyle /> <Lifestyle />
<Charts {...props.charts} /> <Charts {...props.charts} />
<VisaSection visas={props.visas} /> <VisaSection visas={props.visas} />
<VisaStayCountdown destinations={props.destinations} />
<InsuranceGuide /> <InsuranceGuide />
<EmergencyContacts destinations={props.destinations} /> <EmergencyContacts destinations={props.destinations} />
<AtmCashGuide destinations={props.destinations} /> <AtmCashGuide destinations={props.destinations} />

View File

@ -3,12 +3,12 @@
import Link from "next/link"; import Link from "next/link";
const QUICK = [ const QUICK = [
{ href: "/#visa-stay", emoji: "🛂", label: "签证" },
{ href: "/#workspot", emoji: "💼", label: "办公" },
{ href: "/#runway", emoji: "🚀", label: "跑道" }, { href: "/#runway", emoji: "🚀", label: "跑道" },
{ href: "/#daylight", emoji: "☀️", label: "日照" }, { href: "/#daylight", emoji: "☀️", label: "日照" },
{ href: "/#meetings", emoji: "🤝", label: "会议" }, { href: "/#meetings", emoji: "🤝", label: "会议" },
{ href: "/#bag-weight", emoji: "⚖️", label: "行李" },
{ href: "/#trip", emoji: "🗓️", label: "行程" }, { href: "/#trip", emoji: "🗓️", label: "行程" },
{ href: "/#calculator", emoji: "🧮", label: "费用" },
{ href: "/#focus", emoji: "⏱️", label: "专注" }, { href: "/#focus", emoji: "⏱️", label: "专注" },
{ href: "/tools", emoji: "🛠️", label: "全部" }, { href: "/tools", emoji: "🛠️", label: "全部" },
]; ];

View File

@ -0,0 +1,143 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import type { Destination } from "@/lib/types";
const STORAGE_KEY = "nomadro-visa-stay";
const PRESETS: Record<string, { days: number; label: string }> = {
chiangmai: { days: 60, label: "免签/落地签常见 60 天" },
bali: { days: 30, label: "落地签常见 30 天" },
lisbon: { days: 90, label: "申根短签常见 90 天" },
barcelona: { days: 90, label: "申根短签常见 90 天" },
mexico: { days: 180, label: "常见最长约 180 天" },
tokyo: { days: 90, label: "短期停留常见 90 天" },
};
interface Saved {
slug: string;
entry: string;
days: number;
}
interface Props {
destinations: Destination[];
}
function daysBetween(a: string, b: string): number {
const t0 = new Date(a + "T00:00:00").getTime();
const t1 = new Date(b + "T00:00:00").getTime();
return Math.floor((t1 - t0) / 86400000);
}
export default function VisaStayCountdown({ destinations }: Props) {
const [slug, setSlug] = useState(destinations[0]?.slug || "chiangmai");
const [entry, setEntry] = useState(() => new Date().toISOString().slice(0, 10));
const [days, setDays] = useState(60);
const [ready, setReady] = useState(false);
useEffect(() => {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
const s = JSON.parse(raw) as Saved;
if (s.slug) setSlug(s.slug);
if (s.entry) setEntry(s.entry);
if (s.days) setDays(s.days);
}
} catch { /* ignore */ }
setReady(true);
}, []);
useEffect(() => {
if (!ready) return;
localStorage.setItem(STORAGE_KEY, JSON.stringify({ slug, entry, days }));
}, [slug, entry, days, ready]);
const today = new Date().toISOString().slice(0, 10);
const used = Math.max(0, daysBetween(entry, today));
const left = days - used;
const exitDate = useMemo(() => {
const d = new Date(entry + "T00:00:00");
d.setDate(d.getDate() + days - 1);
return d.toISOString().slice(0, 10);
}, [entry, days]);
const pct = Math.min(Math.max((used / Math.max(days, 1)) * 100, 0), 100);
const risk =
left <= 0 ? { label: "已到期/超期", color: "#FF6B6B", tip: "请立即确认离境或延期,勿依赖本工具作法律依据" } :
left <= 7 ? { label: "紧急窗口", color: "#FF6B6B", tip: "一周内到期,优先订机票与查政策" } :
left <= 21 ? { label: "临近到期", color: "#FFE66D", tip: "开始准备下一站或续签材料" } :
{ label: "时间充裕", color: "#4ECDC4", tip: "安心旅居,记得关注政策变动" };
const dest = destinations.find((d) => d.slug === slug);
const applyPreset = (s: string) => {
setSlug(s);
const p = PRESETS[s];
if (p) setDays(p.days);
};
return (
<section className="section visa-stay-section" id="visa-stay">
<div className="container">
<div className="section-header reveal">
<span className="section-tag">🛂 VISA STAY</span>
<h2>签证停留倒计时</h2>
<p>记录入境日与允许天数,剩余停留一目了然(仅供参考)</p>
</div>
<div className="visa-stay-card reveal">
<div className="visa-stay-meter">
<div
className="visa-stay-ring"
style={{ background: `conic-gradient(${risk.color} ${pct}%, rgba(255,255,255,0.08) 0)` }}
>
<div className="visa-stay-ring-inner">
<strong style={{ color: risk.color }}>{left}</strong>
<small>天剩余</small>
</div>
</div>
<div className="visa-stay-level" style={{ color: risk.color }}>
<span>{risk.label}</span>
<p>{risk.tip}</p>
</div>
</div>
<div className="visa-stay-form">
<label>
<span>目的地</span>
<select value={slug} onChange={(e) => applyPreset(e.target.value)}>
{destinations.map((d) => (
<option key={d.slug} value={d.slug}>{d.emoji} {d.name}</option>
))}
</select>
</label>
<label>
<span>入境日期</span>
<input type="date" value={entry} onChange={(e) => setEntry(e.target.value)} />
</label>
<label>
<span>允许停留(天)</span>
<input
type="number"
min={1}
max={365}
value={days}
onChange={(e) => setDays(Math.max(1, Math.min(365, +e.target.value || 1)))}
/>
</label>
{PRESETS[slug] && (
<p className="visa-stay-hint">{dest?.emoji} {PRESETS[slug].label}</p>
)}
<div className="visa-stay-stats">
<div><small>已停留</small><strong>{used} 天</strong></div>
<div><small>额度</small><strong>{days} 天</strong></div>
<div><small>建议离境前</small><strong>{exitDate}</strong></div>
</div>
</div>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,139 @@
"use client";
import { useMemo, useState } from "react";
import type { Destination } from "@/lib/types";
/** Rough monthly coworking vs cafe day-pass economics (CNY). */
const CITY_COST: Record<string, { cowork: number; cafeDay: number; transitDay: number }> = {
chiangmai: { cowork: 900, cafeDay: 35, transitDay: 15 },
bali: { cowork: 1200, cafeDay: 45, transitDay: 25 },
lisbon: { cowork: 1800, cafeDay: 55, transitDay: 30 },
barcelona: { cowork: 2000, cafeDay: 60, transitDay: 35 },
mexico: { cowork: 1400, cafeDay: 40, transitDay: 20 },
tokyo: { cowork: 2800, cafeDay: 70, transitDay: 40 },
};
interface Props {
destinations: Destination[];
}
export default function WorkSpotCost({ destinations }: Props) {
const [slug, setSlug] = useState(destinations[0]?.slug || "chiangmai");
const [days, setDays] = useState(20);
const [mode, setMode] = useState<"cowork" | "cafe" | "mix">("mix");
const [mixCoworkDays, setMixCoworkDays] = useState(12);
const base = CITY_COST[slug] || { cowork: 1500, cafeDay: 45, transitDay: 20 };
const dest = destinations.find((d) => d.slug === slug);
const result = useMemo(() => {
const cafeOnly = days * (base.cafeDay + base.transitDay);
const coworkOnly = base.cowork;
const mixDays = Math.min(mixCoworkDays, days);
const cafeDays = Math.max(days - mixDays, 0);
const mix = base.cowork + cafeDays * (base.cafeDay + base.transitDay);
const pick = mode === "cowork" ? coworkOnly : mode === "cafe" ? cafeOnly : mix;
const winner =
Math.min(coworkOnly, cafeOnly, mix) === coworkOnly ? "联合办公月卡" :
Math.min(coworkOnly, cafeOnly, mix) === cafeOnly ? "咖啡馆日刷" :
"混合策略";
return { cafeOnly, coworkOnly, mix, pick, winner };
}, [base, days, mode, mixCoworkDays]);
const saveVsCafe = result.cafeOnly - result.pick;
const saveColor = saveVsCafe >= 0 ? "#4ECDC4" : "#FF6B6B";
return (
<section className="section workspot-section" id="workspot">
<div className="container">
<div className="section-header reveal">
<span className="section-tag">💼 WORK SPOT</span>
<h2>办公点成本对比</h2>
<p>联合办公月卡 vs 咖啡馆日刷,算出哪种更划算</p>
</div>
<div className="workspot-card reveal">
<div className="workspot-controls">
<label>
<span>城市</span>
<select value={slug} onChange={(e) => setSlug(e.target.value)}>
{destinations.map((d) => (
<option key={d.slug} value={d.slug}>{d.emoji} {d.name}</option>
))}
</select>
</label>
<label>
<span>每月外出办公天数:{days}</span>
<input type="range" min={4} max={28} value={days} onChange={(e) => setDays(+e.target.value)} />
</label>
<div className="workspot-modes">
{([
["cowork", "🏢 月卡"],
["cafe", "☕ 咖啡馆"],
["mix", "🔀 混合"],
] as const).map(([k, label]) => (
<button
key={k}
type="button"
className={`workspot-mode${mode === k ? " active" : ""}`}
onClick={() => setMode(k)}
>
{label}
</button>
))}
</div>
{mode === "mix" && (
<label>
<span>混合:月卡覆盖天数 {mixCoworkDays}</span>
<input
type="range"
min={4}
max={days}
value={Math.min(mixCoworkDays, days)}
onChange={(e) => setMixCoworkDays(+e.target.value)}
/>
</label>
)}
</div>
<div className="workspot-result">
<div className="workspot-hero">
<small>{dest?.emoji} 本月估算</small>
<strong>¥{Math.round(result.pick).toLocaleString()}</strong>
<span style={{ color: saveColor }}>
{saveVsCafe >= 0
? `比纯咖啡馆省 ¥${Math.round(saveVsCafe).toLocaleString()}`
: `比纯咖啡馆多花 ¥${Math.round(-saveVsCafe).toLocaleString()}`}
</span>
</div>
<div className="workspot-compare">
<div className={mode === "cowork" ? "on" : ""}>
<small>联合办公月卡</small>
<strong>¥{base.cowork.toLocaleString()}</strong>
<em>含工位/网速/打印</em>
</div>
<div className={mode === "cafe" ? "on" : ""}>
<small>咖啡馆×{days}天</small>
<strong>¥{Math.round(result.cafeOnly).toLocaleString()}</strong>
<em>含饮品+往返约 ¥{base.cafeDay + base.transitDay}/天</em>
</div>
<div className={mode === "mix" ? "on" : ""}>
<small>混合方案</small>
<strong>¥{Math.round(result.mix).toLocaleString()}</strong>
<em>月卡 + 剩余咖啡馆</em>
</div>
</div>
<p className="workspot-tip">
建议优先:<strong>{result.winner}</strong>
· 参考价会随街区与旺季浮动,落地后再微调
</p>
</div>
</div>
</div>
</section>
);
}

View File

@ -25,10 +25,12 @@ export const TOOL_LINKS: ToolLink[] = [
{ id: "meetings", emoji: "🤝", title: "会议黄金时段", desc: "对齐可开会窗口", href: "/#meetings", category: "work" }, { id: "meetings", emoji: "🤝", title: "会议黄金时段", desc: "对齐可开会窗口", href: "/#meetings", category: "work" },
{ id: "jetlag", emoji: "😴", title: "时差恢复", desc: "恢复期建议", href: "/#jetlag", category: "work" }, { id: "jetlag", emoji: "😴", title: "时差恢复", desc: "恢复期建议", href: "/#jetlag", category: "work" },
{ id: "wifi", emoji: "📶", title: "网速评级", desc: "远程办公是否够用", href: "/#wifi", category: "work" }, { id: "wifi", emoji: "📶", title: "网速评级", desc: "远程办公是否够用", href: "/#wifi", category: "work" },
{ id: "workspot", emoji: "💼", title: "办公点成本", desc: "月卡 vs 咖啡馆划算度", href: "/#workspot", category: "work" },
{ id: "focus", emoji: "⏱️", title: "专注时钟", desc: "番茄钟深度工作", href: "/#focus", category: "work" }, { id: "focus", emoji: "⏱️", title: "专注时钟", desc: "番茄钟深度工作", href: "/#focus", category: "work" },
{ id: "coworking", emoji: "💻", title: "联合办公", desc: "热门 Co-working", href: "/#coworking", category: "work" }, { id: "coworking", emoji: "💻", title: "联合办公", desc: "热门 Co-working", href: "/#coworking", category: "work" },
{ id: "cafes", emoji: "☕", title: "咖啡馆", desc: "办公友好咖啡店", href: "/#cafes", category: "work" }, { id: "cafes", emoji: "☕", title: "咖啡馆", desc: "办公友好咖啡店", href: "/#cafes", category: "work" },
{ id: "visa", emoji: "📋", title: "签证指南", desc: "政策与智能匹配", href: "/#visa", category: "life" }, { id: "visa", emoji: "📋", title: "签证指南", desc: "政策与智能匹配", href: "/#visa", category: "life" },
{ id: "visa-stay", emoji: "🛂", title: "停留倒计时", desc: "签证剩余天数提醒", href: "/#visa-stay", category: "life" },
{ id: "housing", emoji: "🏡", title: "住宿指南", desc: "Coliving / 公寓方案", href: "/#housing", category: "life" }, { id: "housing", emoji: "🏡", title: "住宿指南", desc: "Coliving / 公寓方案", href: "/#housing", category: "life" },
{ id: "sim", emoji: "📱", title: "上网方案", desc: "SIM / eSIM 推荐", href: "/#sim", category: "life" }, { id: "sim", emoji: "📱", title: "上网方案", desc: "SIM / eSIM 推荐", href: "/#sim", category: "life" },
{ id: "plugs", emoji: "🔌", title: "电源插座", desc: "电压与插头类型", href: "/#plugs", category: "life" }, { id: "plugs", emoji: "🔌", title: "电源插座", desc: "电压与插头类型", href: "/#plugs", category: "life" },