diff --git a/frontend/src/components/NomadTips.tsx b/frontend/src/components/NomadTips.tsx
new file mode 100644
index 0000000..1579cf2
--- /dev/null
+++ b/frontend/src/components/NomadTips.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+const TIPS = [
+ { emoji: "📶", text: "入住前用 Speedtest 测网速,低于 20Mbps 慎重签约" },
+ { emoji: "💳", text: "准备一张无外汇手续费的信用卡,省下不少换汇成本" },
+ { emoji: "🏥", text: "出发前购买 SafetyWing 等国际医疗保险,月费约 $40+" },
+ { emoji: "🕐", text: "选时区重叠 ≥4 小时的城市,远程协作更轻松" },
+ { emoji: "🛂", text: "护照有效期建议预留 6 个月以上,避免登机被拒" },
+ { emoji: "☕", text: "先订 1 周 Airbnb 探路,再决定长期租房" },
+];
+
+export default function NomadTips() {
+ const [idx, setIdx] = useState(0);
+ const [visible, setVisible] = useState(false);
+
+ useEffect(() => {
+ const show = setTimeout(() => setVisible(true), 8000);
+ return () => clearTimeout(show);
+ }, []);
+
+ useEffect(() => {
+ if (!visible) return;
+ const t = setInterval(() => setIdx((i) => (i + 1) % TIPS.length), 6000);
+ return () => clearInterval(t);
+ }, [visible]);
+
+ if (!visible) return null;
+
+ const tip = TIPS[idx];
+
+ return (
+
+
+
{tip.emoji}
+
+
+ {TIPS.map((_, i) => (
+
+ ))}
+
+
+ );
+}
diff --git a/frontend/src/components/PackingChecklist.tsx b/frontend/src/components/PackingChecklist.tsx
new file mode 100644
index 0000000..4c2c82e
--- /dev/null
+++ b/frontend/src/components/PackingChecklist.tsx
@@ -0,0 +1,125 @@
+"use client";
+
+import { useEffect, useMemo, useState } from "react";
+
+interface PackItem {
+ id: string;
+ emoji: string;
+ label: string;
+ category: string;
+}
+
+const ITEMS: PackItem[] = [
+ { id: "laptop", emoji: "💻", label: "笔记本电脑 + 充电器", category: "tech" },
+ { id: "adapter", emoji: "🔌", label: "全球旅行转接插头", category: "tech" },
+ { id: "headset", emoji: "🎧", label: "降噪耳机", category: "tech" },
+ { id: "powerbank", emoji: "🔋", label: "移动电源", category: "tech" },
+ { id: "sim", emoji: "📱", label: "eSIM / 本地 SIM 卡", category: "tech" },
+ { id: "vpn", emoji: "🔐", label: "VPN 订阅已开通", category: "tech" },
+ { id: "passport", emoji: "🛂", label: "护照(有效期 > 6 个月)", category: "docs" },
+ { id: "visa", emoji: "📋", label: "签证 / 电子签确认", category: "docs" },
+ { id: "insurance", emoji: "🏥", label: "国际旅行保险", category: "docs" },
+ { id: "cards", emoji: "💳", label: "银行卡 + 备用卡", category: "docs" },
+ { id: "vaccine", emoji: "💉", label: "疫苗证明(如需要)", category: "docs" },
+ { id: "clothes", emoji: "👕", label: "轻便衣物 7 天量", category: "daily" },
+ { id: "shoes", emoji: "👟", label: "舒适步行鞋", category: "daily" },
+ { id: "medicine", emoji: "💊", label: "常用药品", category: "daily" },
+ { id: "toiletry", emoji: "🧴", label: "洗漱用品(旅行装)", category: "daily" },
+ { id: "umbrella", emoji: "☂️", label: "折叠伞 / 防晒", category: "daily" },
+];
+
+const CATEGORIES = [
+ { key: "all", label: "📦 全部" },
+ { key: "tech", label: "💻 数码" },
+ { key: "docs", label: "📄 证件" },
+ { key: "daily", label: "🧳 日常" },
+];
+
+const STORAGE_KEY = "nomadflow-packing";
+
+export default function PackingChecklist() {
+ const [checked, setChecked] = useState
>({});
+ const [filter, setFilter] = useState("all");
+ const [ready, setReady] = useState(false);
+
+ useEffect(() => {
+ try {
+ const saved = localStorage.getItem(STORAGE_KEY);
+ if (saved) setChecked(JSON.parse(saved));
+ } catch { /* ignore */ }
+ setReady(true);
+ }, []);
+
+ useEffect(() => {
+ if (!ready) return;
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(checked));
+ }, [checked, ready]);
+
+ const filtered = useMemo(
+ () => (filter === "all" ? ITEMS : ITEMS.filter((i) => i.category === filter)),
+ [filter]
+ );
+
+ const done = Object.values(checked).filter(Boolean).length;
+ const total = ITEMS.length;
+ const pct = Math.round((done / total) * 100);
+
+ const toggle = (id: string) => setChecked((prev) => ({ ...prev, [id]: !prev[id] }));
+ const reset = () => setChecked({});
+ const checkAll = () => {
+ const all: Record = {};
+ ITEMS.forEach((i) => { all[i.id] = true; });
+ setChecked(all);
+ };
+
+ return (
+
+
+
+
🧳 PACKING
+
数字游民行李清单
+
出发前逐项勾选,确保远程工作与旅居无忧
+
+
+
+
+
+ 完成度
+ {pct}%
+ {done}/{total}
+
+
+ {pct === 100 &&
🎉 全部就绪,可以出发了!
}
+
+
+
+ {CATEGORIES.map((c) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/SeasonGuide.tsx b/frontend/src/components/SeasonGuide.tsx
new file mode 100644
index 0000000..7f97e40
--- /dev/null
+++ b/frontend/src/components/SeasonGuide.tsx
@@ -0,0 +1,100 @@
+"use client";
+
+import { useMemo, useState, type CSSProperties } from "react";
+import Link from "next/link";
+import type { Destination } from "@/lib/types";
+
+const MONTHS = ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"];
+
+/** 0=避开 1=一般 2=推荐 3=最佳 */
+const SEASON_MAP: Record = {
+ bali: [2, 2, 3, 3, 3, 2, 1, 1, 2, 3, 3, 2],
+ lisbon: [1, 1, 2, 3, 3, 3, 2, 2, 3, 3, 2, 1],
+ chiangmai: [3, 3, 2, 1, 0, 0, 0, 0, 1, 2, 3, 3],
+ mexico: [2, 2, 3, 3, 2, 1, 0, 0, 1, 2, 3, 3],
+ barcelona: [1, 1, 2, 3, 3, 3, 2, 2, 3, 3, 2, 1],
+ tokyo: [2, 2, 3, 3, 2, 1, 0, 0, 2, 3, 3, 2],
+};
+
+const LEVEL = [
+ { label: "避开", color: "#FF6B6B", emoji: "🌧️" },
+ { label: "一般", color: "#FFE66D", emoji: "☁️" },
+ { label: "推荐", color: "#4ECDC4", emoji: "☀️" },
+ { label: "最佳", color: "#6ee7b7", emoji: "🌟" },
+];
+
+export default function SeasonGuide({ destinations }: { destinations: Destination[] }) {
+ const [slug, setSlug] = useState(destinations[0]?.slug || "bali");
+ const nowMonth = new Date().getMonth();
+
+ const scores = SEASON_MAP[slug] || SEASON_MAP.bali;
+ const dest = destinations.find((d) => d.slug === slug);
+
+ const bestMonths = useMemo(
+ () => scores.map((s, i) => ({ i, s })).filter((x) => x.s === 3).map((x) => MONTHS[x.i]),
+ [scores]
+ );
+
+ return (
+
+
+
+
🌤️ SEASON
+
最佳旅居月份
+
避开雨季与旺季,选对时间开启旅居生活
+
+
+
+
+ {destinations.map((d) => (
+
+ ))}
+
+
+
+
+
{dest?.emoji} {dest?.name}
+
最佳月份:{bestMonths.join(" · ") || "全年适宜"}
+
+
查看详情 →
+
+
+
+ {MONTHS.map((m, i) => {
+ const level = scores[i] ?? 1;
+ const info = LEVEL[level];
+ const isNow = i === nowMonth;
+ return (
+
+ {info.emoji}
+ {m}
+ {info.label}
+ {isNow && 本月}
+
+ );
+ })}
+
+
+
+ {LEVEL.map((l) => (
+
+ {l.emoji} {l.label}
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/SectionNav.tsx b/frontend/src/components/SectionNav.tsx
index c5c35ae..ce6d46c 100644
--- a/frontend/src/components/SectionNav.tsx
+++ b/frontend/src/components/SectionNav.tsx
@@ -11,6 +11,9 @@ const SECTIONS = [
{ id: "cost-compare", emoji: "💰", label: "省钱" },
{ id: "lifestyle", emoji: "💻", label: "生活" },
{ id: "visa", emoji: "📋", label: "签证" },
+ { id: "coworking", emoji: "💻", label: "办公" },
+ { id: "season", emoji: "🌤️", label: "季节" },
+ { id: "packing", emoji: "🧳", label: "行李" },
{ id: "blog", emoji: "📝", label: "博客" },
];
diff --git a/frontend/src/components/ShareButton.tsx b/frontend/src/components/ShareButton.tsx
new file mode 100644
index 0000000..2a16491
--- /dev/null
+++ b/frontend/src/components/ShareButton.tsx
@@ -0,0 +1,67 @@
+"use client";
+
+import { useState } from "react";
+import { useToast } from "@/lib/toast";
+
+interface Props {
+ title: string;
+ url?: string;
+ className?: string;
+ compact?: boolean;
+}
+
+export default function ShareButton({ title, url, className = "", compact = false }: Props) {
+ const { toast } = useToast();
+ const [open, setOpen] = useState(false);
+
+ const getUrl = () => url ?? (typeof window !== "undefined" ? window.location.href : "");
+
+ const copyLink = async () => {
+ await navigator.clipboard.writeText(getUrl());
+ toast("链接已复制到剪贴板 📋");
+ setOpen(false);
+ };
+
+ const nativeShare = async () => {
+ const shareUrl = getUrl();
+ if (navigator.share) {
+ await navigator.share({ title, url: shareUrl });
+ setOpen(false);
+ } else {
+ await copyLink();
+ }
+ };
+
+ const shareTwitter = () => {
+ const shareUrl = encodeURIComponent(getUrl());
+ const text = encodeURIComponent(title);
+ window.open(`https://twitter.com/intent/tweet?text=${text}&url=${shareUrl}`, "_blank");
+ setOpen(false);
+ };
+
+ if (compact) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+ {open && (
+ <>
+
setOpen(false)} />
+
+
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/frontend/src/components/VisaSection.tsx b/frontend/src/components/VisaSection.tsx
index e1db6ec..23a8e73 100644
--- a/frontend/src/components/VisaSection.tsx
+++ b/frontend/src/components/VisaSection.tsx
@@ -2,6 +2,7 @@
import { useMemo, useState } from "react";
import type { Visa } from "@/lib/types";
+import VisaWizard from "./VisaWizard";
const FILTERS = [
{ key: "all", label: "🌏 全部" },
@@ -75,6 +76,7 @@ export default function VisaSection({ visas }: { visas: Visa[] }) {
{filtered.length === 0 && (
暂无匹配的签证类型,试试其他筛选条件
)}
+
);
diff --git a/frontend/src/components/VisaWizard.tsx b/frontend/src/components/VisaWizard.tsx
new file mode 100644
index 0000000..b413ab3
--- /dev/null
+++ b/frontend/src/components/VisaWizard.tsx
@@ -0,0 +1,179 @@
+"use client";
+
+import { useMemo, useState } from "react";
+import type { Visa } from "@/lib/types";
+
+interface Props {
+ visas: Visa[];
+}
+
+const INCOME_OPTIONS = [
+ { key: "low", label: "💰 低于 ¥8,000/月", max: 8000 },
+ { key: "mid", label: "💼 ¥8,000 – ¥20,000/月", max: 20000 },
+ { key: "high", label: "🚀 ¥20,000 – ¥50,000/月", max: 50000 },
+ { key: "premium", label: "👑 高于 ¥50,000/月", max: Infinity },
+];
+
+const DURATION_OPTIONS = [
+ { key: "short", label: "⏱️ 1-3 个月", months: 3 },
+ { key: "medium", label: "📅 3-12 个月", months: 12 },
+ { key: "long", label: "🏠 1 年以上", months: 24 },
+];
+
+const REGION_OPTIONS = [
+ { key: "any", label: "🌏 不限" },
+ { key: "europe", label: "🏰 欧洲" },
+ { key: "sea", label: "🌴 东南亚" },
+ { key: "latam", label: "🌮 拉美" },
+];
+
+const VISA_INCOME: Record = {
+ "1": 6000, "2": 17000, "3": 2000, "4": 47000, "5": 18000, "6": 28000,
+};
+
+const VISA_REGION: Record = {
+ "1": "europe", "2": "europe", "3": "sea", "4": "sea", "5": "latam", "6": "europe",
+};
+
+export default function VisaWizard({ visas }: Props) {
+ const [step, setStep] = useState(0);
+ const [income, setIncome] = useState("");
+ const [duration, setDuration] = useState("");
+ const [region, setRegion] = useState("any");
+ const [showResults, setShowResults] = useState(false);
+
+ const results = useMemo(() => {
+ if (!income || !duration) return [];
+ const incomeOpt = INCOME_OPTIONS.find((o) => o.key === income)!;
+ const durOpt = DURATION_OPTIONS.find((o) => o.key === duration)!;
+
+ return visas
+ .map((v) => {
+ let score = 0;
+ const reqIncome = VISA_INCOME[v.id] ?? 15000;
+ const vRegion = VISA_REGION[v.id] ?? "any";
+
+ if (reqIncome <= incomeOpt.max) score += 40;
+ else if (reqIncome <= incomeOpt.max * 1.5) score += 20;
+
+ if (v.difficulty <= 35) score += 25;
+ else if (v.difficulty <= 55) score += 15;
+ else score += 5;
+
+ if (durOpt.months >= 12 && v.duration.includes("年")) score += 20;
+ else if (durOpt.months <= 3) score += v.duration.includes("天") ? 20 : 10;
+ else score += 15;
+
+ if (region === "any" || vRegion === region) score += 15;
+
+ return { visa: v, score: Math.min(score, 100) };
+ })
+ .filter((r) => r.score >= 30)
+ .sort((a, b) => b.score - a.score)
+ .slice(0, 3);
+ }, [visas, income, duration, region]);
+
+ const canNext = step === 0 ? !!income : step === 1 ? !!duration : true;
+
+ const handleFinish = () => {
+ setShowResults(true);
+ setStep(3);
+ };
+
+ const reset = () => {
+ setStep(0);
+ setIncome("");
+ setDuration("");
+ setRegion("any");
+ setShowResults(false);
+ };
+
+ return (
+
+
+
🧙 VISA WIZARD
+
签证智能匹配
+
回答 3 个问题,找到最适合你的远程工作签证
+
+
+ {!showResults ? (
+
+
+ {["月收入", "停留时长", "目的地"].map((label, i) => (
+
+ {i < step ? "✓" : i + 1}
+ {label}
+
+ ))}
+
+
+ {step === 0 && (
+
+ {INCOME_OPTIONS.map((o) => (
+
+ ))}
+
+ )}
+
+ {step === 1 && (
+
+ {DURATION_OPTIONS.map((o) => (
+
+ ))}
+
+ )}
+
+ {step === 2 && (
+
+ {REGION_OPTIONS.map((o) => (
+
+ ))}
+
+ )}
+
+
+ {step > 0 && }
+ {step < 2 ? (
+
+ ) : (
+
+ )}
+
+
+ ) : (
+
+ {results.length === 0 ? (
+
暂无完全匹配的签证,建议咨询专业移民顾问或放宽条件重试
+ ) : (
+
+ {results.map(({ visa, score }, i) => (
+
+ {i === 0 &&
🏆 最佳匹配}
+
+
{visa.flag}
+
+
{visa.name}
+ {score}% 匹配
+
+
+
+ - 💰 {visa.income_req}
+ - 📅 {visa.duration}
+ - ⏱️ {visa.approval_time}
+
+
+ ))}
+
+ )}
+
+
+ )}
+
+ );
+}
diff --git a/frontend/src/lib/coworking.ts b/frontend/src/lib/coworking.ts
new file mode 100644
index 0000000..7c7cb9d
--- /dev/null
+++ b/frontend/src/lib/coworking.ts
@@ -0,0 +1,60 @@
+export interface CoworkingSpace {
+ id: string;
+ city: string;
+ slug: string;
+ emoji: string;
+ name: string;
+ price: string;
+ wifi: number;
+ rating: number;
+ perks: string[];
+ vibe: string;
+}
+
+export const COWORKING_SPACES: CoworkingSpace[] = [
+ {
+ id: "1", city: "巴厘岛", slug: "bali", emoji: "🏝️", name: "Hubud",
+ price: "¥180/天", wifi: 95, rating: 4.8,
+ perks: ["稻田景观", "瑜伽课", "社区活动"], vibe: "自然疗愈",
+ },
+ {
+ id: "2", city: "巴厘岛", slug: "bali", emoji: "🏝️", name: "Outpost",
+ price: "¥2,800/月", wifi: 98, rating: 4.9,
+ perks: ["泳池", "住宿套餐", "高速光纤"], vibe: "游民大本营",
+ },
+ {
+ id: "3", city: "清迈", slug: "chiangmai", emoji: "🏔️", name: "Punspace",
+ price: "¥60/天", wifi: 92, rating: 4.7,
+ perks: ["夜市步行", "空调充足", "打印扫描"], vibe: "性价比之王",
+ },
+ {
+ id: "4", city: "清迈", slug: "chiangmai", emoji: "🏔️", name: "CAMP",
+ price: "¥80/天", wifi: 100, rating: 4.8,
+ perks: ["商场内", "24h 开放", "美食广场"], vibe: "都市便利",
+ },
+ {
+ id: "5", city: "里斯本", slug: "lisbon", emoji: "🌊", name: "Second Home",
+ price: "€25/天", wifi: 150, rating: 4.9,
+ perks: ["植物花园", "活动丰富", "设计感"], vibe: "创意空间",
+ },
+ {
+ id: "6", city: "里斯本", slug: "lisbon", emoji: "🌊", name: "LACS",
+ price: "€200/月", wifi: 120, rating: 4.6,
+ perks: ["海滨位置", "会议室", "咖啡吧"], vibe: "海滨办公",
+ },
+ {
+ id: "7", city: "巴塞罗那", slug: "barcelona", emoji: "🏖️", name: "Aticco",
+ price: "€22/天", wifi: 140, rating: 4.7,
+ perks: ["屋顶露台", "活动日历", "会员网络"], vibe: "地中海风情",
+ },
+ {
+ id: "8", city: "墨西哥城", slug: "mexico", emoji: "🌃", name: "WeWork Reforma",
+ price: "$15/天", wifi: 110, rating: 4.5,
+ perks: ["市中心", "北美时区", "全球会员"], vibe: "国际连锁",
+ },
+ {
+ id: "9", city: "东京", slug: "tokyo", emoji: "🗼", name: "WeWork 涩谷",
+ price: "¥3,500/天", wifi: 200, rating: 4.6,
+ perks: ["极致网速", "地铁直达", "静音舱"], vibe: "高效都市",
+ },
+];