diff --git a/frontend/src/components/HomeClient.tsx b/frontend/src/components/HomeClient.tsx
index 275e78e..518d972 100644
--- a/frontend/src/components/HomeClient.tsx
+++ b/frontend/src/components/HomeClient.tsx
@@ -44,6 +44,9 @@ import JetLagEstimator from "./JetLagEstimator";
import HousingGuide from "./HousingGuide";
import Phrasebook from "./Phrasebook";
import FeedbackWidget from "./FeedbackWidget";
+import InsuranceGuide from "./InsuranceGuide";
+import ExpenseTracker from "./ExpenseTracker";
+import SimGuide from "./SimGuide";
import KeyboardShortcuts from "./KeyboardShortcuts";
import BackToTop from "./BackToTop";
@@ -121,6 +124,7 @@ export default function HomeClient(props: Props) {
+
@@ -128,8 +132,10 @@ export default function HomeClient(props: Props) {
+
+
diff --git a/frontend/src/components/InsuranceGuide.tsx b/frontend/src/components/InsuranceGuide.tsx
new file mode 100644
index 0000000..84d9498
--- /dev/null
+++ b/frontend/src/components/InsuranceGuide.tsx
@@ -0,0 +1,106 @@
+"use client";
+
+import { useMemo, useState } from "react";
+
+interface Plan {
+ id: string;
+ emoji: string;
+ name: string;
+ price: string;
+ cover: string[];
+ best: string;
+ note: string;
+}
+
+const PLANS: Plan[] = [
+ {
+ id: "safetywing", emoji: "🛡️", name: "SafetyWing Nomad",
+ price: "$45–80/月", best: "多数数字游民首选",
+ cover: ["门诊/住院", "紧急医疗运送", "旅行延误"],
+ note: "覆盖全球多数国家,按月订阅灵活",
+ },
+ {
+ id: "worldnomads", emoji: "🌍", name: "World Nomads",
+ price: "按行程报价", best: "多国跳跃旅行",
+ cover: ["冒险运动", "行李丢失", "紧急救援"],
+ note: "适合含户外活动的行程,按段购买",
+ },
+ {
+ id: "genki", emoji: "🇯🇵", name: "Genki",
+ price: "€60–100/月", best: "欧洲长期旅居",
+ cover: ["欧盟医疗", "牙科可选", "签证材料支持"],
+ note: "对申根签证申请材料较友好",
+ },
+ {
+ id: "local", emoji: "🏥", name: "目的地本地保险",
+ price: "因地而异", best: "停留 6 个月+",
+ cover: ["本地医院", "报销便捷", "费用通常更低"],
+ note: "需了解当地条款与语言,适合长期定居",
+ },
+];
+
+const FILTERS = [
+ { key: "all", label: "🌏 全部" },
+ { key: "flex", label: "📅 按月灵活" },
+ { key: "trip", label: "✈️ 按行程" },
+ { key: "long", label: "🏠 长期" },
+];
+
+export default function InsuranceGuide() {
+ const [filter, setFilter] = useState("all");
+ const [picked, setPicked] = useState
(null);
+
+ const filtered = useMemo(() => {
+ if (filter === "all") return PLANS;
+ if (filter === "flex") return PLANS.filter((p) => p.id === "safetywing" || p.id === "genki");
+ if (filter === "trip") return PLANS.filter((p) => p.id === "worldnomads");
+ return PLANS.filter((p) => p.id === "local" || p.id === "genki");
+ }, [filter]);
+
+ return (
+
+
+
+
🏥 INSURANCE
+
旅居保险指南
+
远程工作不等于裸奔——选对国际医疗保险再出发
+
+
+
+ {FILTERS.map((f) => (
+
+ ))}
+
+
+
+ {filtered.map((p, i) => (
+
setPicked(p.id === picked ? null : p.id)}
+ >
+
+ {p.emoji}
+ {p.price}
+
+
{p.name}
+
👍 {p.best}
+
+ {p.cover.map((c) => - ✓ {c}
)}
+
+
{p.note}
+ {picked === p.id &&
✓ 已标记关注}
+
+ ))}
+
+
+
+ );
+}
diff --git a/frontend/src/components/PackingChecklist.tsx b/frontend/src/components/PackingChecklist.tsx
index 4c2c82e..6f96567 100644
--- a/frontend/src/components/PackingChecklist.tsx
+++ b/frontend/src/components/PackingChecklist.tsx
@@ -35,7 +35,8 @@ const CATEGORIES = [
{ key: "daily", label: "🧳 日常" },
];
-const STORAGE_KEY = "nomadflow-packing";
+const STORAGE_KEY = "nomadro-packing";
+const LEGACY_KEY = "nomadflow-packing";
export default function PackingChecklist() {
const [checked, setChecked] = useState>({});
@@ -44,8 +45,14 @@ export default function PackingChecklist() {
useEffect(() => {
try {
- const saved = localStorage.getItem(STORAGE_KEY);
- if (saved) setChecked(JSON.parse(saved));
+ const saved = localStorage.getItem(STORAGE_KEY) || localStorage.getItem(LEGACY_KEY);
+ if (saved) {
+ setChecked(JSON.parse(saved));
+ if (!localStorage.getItem(STORAGE_KEY) && localStorage.getItem(LEGACY_KEY)) {
+ localStorage.setItem(STORAGE_KEY, saved);
+ localStorage.removeItem(LEGACY_KEY);
+ }
+ }
} catch { /* ignore */ }
setReady(true);
}, []);
diff --git a/frontend/src/components/SectionNav.tsx b/frontend/src/components/SectionNav.tsx
index 765a1b5..2f9cd74 100644
--- a/frontend/src/components/SectionNav.tsx
+++ b/frontend/src/components/SectionNav.tsx
@@ -12,12 +12,15 @@ const SECTIONS = [
{ id: "calculator", emoji: "🧮", label: "计算器" },
{ id: "cost-compare", emoji: "💰", label: "省钱" },
{ id: "savings", emoji: "🎯", label: "存款" },
+ { id: "expenses", emoji: "📒", label: "账本" },
{ id: "flight", emoji: "✈️", label: "机票" },
{ id: "wifi", emoji: "📶", label: "网速" },
{ id: "lifestyle", emoji: "💻", label: "生活" },
{ id: "visa", emoji: "📋", label: "签证" },
+ { id: "insurance", emoji: "🏥", label: "保险" },
{ id: "coworking", emoji: "💻", label: "办公" },
{ id: "housing", emoji: "🏡", label: "住宿" },
+ { id: "sim", emoji: "📱", label: "上网" },
{ id: "season", emoji: "🌤️", label: "季节" },
{ id: "packing", emoji: "🧳", label: "行李" },
{ id: "phrases", emoji: "🗣️", label: "短语" },
diff --git a/frontend/src/components/SimGuide.tsx b/frontend/src/components/SimGuide.tsx
new file mode 100644
index 0000000..c7a4b8d
--- /dev/null
+++ b/frontend/src/components/SimGuide.tsx
@@ -0,0 +1,95 @@
+"use client";
+
+import { useMemo, useState } from "react";
+import type { Destination } from "@/lib/types";
+
+interface SimPlan {
+ name: string;
+ emoji: string;
+ data: string;
+ price: string;
+ tip: string;
+}
+
+const SIMS: Record = {
+ bali: [
+ { name: "Telkomsel", emoji: "📶", data: "50GB / 30天", price: "约 ¥60", tip: "覆盖最好,机场可办" },
+ { name: "XL / Axis", emoji: "📱", data: "40GB / 30天", price: "约 ¥40", tip: "性价比高,市区够用" },
+ { name: "eSIM Airalo", emoji: "✈️", data: "按需充值", price: "约 $8 起", tip: "落地即用,免换卡" },
+ ],
+ chiangmai: [
+ { name: "AIS", emoji: "📶", data: "无限量套餐", price: "约 ¥80/月", tip: "网速稳,游民常用" },
+ { name: "TrueMove", emoji: "📱", data: "大流量包", price: "约 ¥60/月", tip: "7-Eleven 随处可买" },
+ { name: "eSIM Holafly", emoji: "✈️", data: "无限流量", price: "约 $19/周", tip: "适合短期跳岛" },
+ ],
+ lisbon: [
+ { name: "Vodafone PT", emoji: "📶", data: "20–50GB", price: "€15–25", tip: "覆盖全国,店内可办" },
+ { name: "NOS / MEO", emoji: "📱", data: "Prepaid", price: "€10–20", tip: "预付费灵活" },
+ { name: "eSIM Airalo", emoji: "✈️", data: "按需", price: "$5 起", tip: "抵达前先装好" },
+ ],
+ barcelona: [
+ { name: "Orange / Movistar", emoji: "📶", data: "20–50GB", price: "€10–20", tip: "申根区漫游友好" },
+ { name: "Digi", emoji: "📱", data: "大流量便宜", price: "€5–15", tip: "预算优先之选" },
+ { name: "eSIM", emoji: "✈️", data: "按需", price: "$6 起", tip: "免排队办卡" },
+ ],
+ mexico: [
+ { name: "Telcel", emoji: "📶", data: "Amigo 套餐", price: "$10–20", tip: "覆盖最广" },
+ { name: "AT&T MX", emoji: "📱", data: "预付费", price: "$10–18", tip: "市中心信号好" },
+ { name: "eSIM", emoji: "✈️", data: "按需", price: "$8 起", tip: "对接北美出差方便" },
+ ],
+ tokyo: [
+ { name: "IIJmio / SoftBank", emoji: "📶", data: "10–20GB", price: "¥2,000–4,000", tip: "便利店可买" },
+ { name: "Sakura Mobile", emoji: "📱", data: "游民友好", price: "¥3,000+", tip: "支持英文客服" },
+ { name: "eSIM Airalo", emoji: "✈️", data: "按需", price: "$6 起", tip: "机场落地即连" },
+ ],
+};
+
+interface Props {
+ destinations: Destination[];
+}
+
+export default function SimGuide({ destinations }: Props) {
+ const [slug, setSlug] = useState(destinations[0]?.slug || "bali");
+ const plans = useMemo(() => SIMS[slug] || SIMS.bali, [slug]);
+ const dest = destinations.find((d) => d.slug === slug);
+
+ return (
+
+
+
+
📱 SIM / eSIM
+
当地上网方案
+
落地第一件事:连上网。实体卡与 eSIM 怎么选
+
+
+
+ {destinations.map((d) => (
+
+ ))}
+
+
+
{dest?.emoji} {dest?.name} 推荐套餐
+
+
+ {plans.map((p, i) => (
+
+
{p.emoji}
+
{p.name}
+
+ 📦 {p.data}
+ {p.price}
+
+
{p.tip}
+
+ ))}
+
+
+
+ );
+}
diff --git a/frontend/src/components/TripPlanner.tsx b/frontend/src/components/TripPlanner.tsx
index d6d8f7a..1585202 100644
--- a/frontend/src/components/TripPlanner.tsx
+++ b/frontend/src/components/TripPlanner.tsx
@@ -2,10 +2,9 @@
import { useEffect, useState } from "react";
import { useToast } from "@/lib/toast";
+import { loadTrip, saveTrip } from "@/lib/tripStorage";
import type { Destination, TripItem } from "@/lib/types";
-const STORAGE_KEY = "nomadflow-trip";
-
interface Props { destinations: Destination[] }
export default function TripPlanner({ destinations }: Props) {
@@ -22,20 +21,19 @@ export default function TripPlanner({ destinations }: Props) {
const decoded = JSON.parse(atob(shared)) as TripItem[];
if (Array.isArray(decoded) && decoded.length > 0) {
setTrip(decoded);
- localStorage.setItem(STORAGE_KEY, JSON.stringify(decoded));
+ saveTrip(decoded);
toast("已导入分享的行程 🗺️");
window.history.replaceState({}, "", window.location.pathname + "#trip");
}
} catch { /* ignore */ }
return;
}
- const saved = localStorage.getItem(STORAGE_KEY);
- if (saved) setTrip(JSON.parse(saved));
- }, []);
+ setTrip(loadTrip());
+ }, [toast]);
const save = (items: TripItem[]) => {
setTrip(items);
- localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
+ saveTrip(items);
};
const addCity = () => {
diff --git a/frontend/src/lib/tripStorage.ts b/frontend/src/lib/tripStorage.ts
new file mode 100644
index 0000000..bec6bc5
--- /dev/null
+++ b/frontend/src/lib/tripStorage.ts
@@ -0,0 +1,29 @@
+const TRIP_KEYS = ["nomadro-trip", "nomadflow-trip"] as const;
+const PRIMARY = TRIP_KEYS[0];
+
+export function loadTrip(): T[] {
+ if (typeof window === "undefined") return [];
+ for (const key of TRIP_KEYS) {
+ const raw = localStorage.getItem(key);
+ if (!raw) continue;
+ try {
+ const data = JSON.parse(raw);
+ if (Array.isArray(data)) {
+ if (key !== PRIMARY) {
+ localStorage.setItem(PRIMARY, raw);
+ localStorage.removeItem(key);
+ }
+ return data;
+ }
+ } catch { /* ignore */ }
+ }
+ return [];
+}
+
+export function saveTrip(items: unknown[]) {
+ if (typeof window === "undefined") return;
+ localStorage.setItem(PRIMARY, JSON.stringify(items));
+ localStorage.removeItem("nomadflow-trip");
+}
+
+export const TRIP_STORAGE_KEY = PRIMARY;