diff --git a/README.md b/README.md
index 21f0531..48d3db7 100644
--- a/README.md
+++ b/README.md
@@ -116,6 +116,8 @@ docker compose up -d
| ⚖️ 行李重量 | 对照航司限额估算装备重量 |
| 🛂 签证停留倒计时 | 入境日与剩余天数提醒 |
| 💼 办公点成本 | 月卡 vs 咖啡馆划算度对比 |
+| 🛬 落地首月成本 | 房租押金 SIM 接机估算 |
+| 📶 流量预算 | 月流量与套餐档建议 |
| ✨ 今日金句 | 每日游民灵感语录 |
| 😴 时差估算 | 时差跨度与恢复期建议 |
| 🏡 住宿指南 | Villa / Coliving / 公寓方案对比 |
diff --git a/frontend/src/app/changelog/page.tsx b/frontend/src/app/changelog/page.tsx
index fc64a9a..b50c301 100644
--- a/frontend/src/app/changelog/page.tsx
+++ b/frontend/src/app/changelog/page.tsx
@@ -12,10 +12,12 @@ const LOGS = [
date: "2026-08-29",
tag: "新工具",
items: [
+ "落地首月成本:房租押金 SIM 接机一次算清",
+ "流量预算规划:按办公习惯估算月 GB 与套餐档",
"签证停留倒计时:入境日 + 额度,剩余天数环形提醒",
"办公点成本对比:联合办公月卡 vs 咖啡馆日刷",
"日照办公窗、行李重量、旅居跑道、会议黄金时段",
- "快捷工具条直达签证 / 办公 / 跑道等核心工具",
+ "快捷工具条直达首月 / 流量 / 签证等核心工具",
],
},
{
diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css
index 4160efa..a54b8b1 100644
--- a/frontend/src/app/globals.css
+++ b/frontend/src/app/globals.css
@@ -8297,3 +8297,212 @@ img { max-width: 100%; display: block; }
.workspot-compare { grid-template-columns: 1fr; }
.visa-stay-stats { grid-template-columns: 1fr; }
}
+
+
+/* ===== First Month Cost ===== */
+.firstmonth-card {
+ display: grid;
+ grid-template-columns: 0.9fr 1.2fr;
+ gap: 22px;
+ padding: 24px;
+ background: var(--bg-card);
+ border: var(--border-glass);
+ border-radius: var(--radius-xl);
+ backdrop-filter: blur(16px);
+}
+.firstmonth-controls {
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+}
+.firstmonth-controls > label {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+.firstmonth-controls > label > span {
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+}
+.firstmonth-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;
+}
+.firstmonth-housing {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 8px;
+}
+.firstmonth-chip {
+ 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;
+}
+.firstmonth-chip:hover,
+.firstmonth-chip.active {
+ border-color: rgba(78, 205, 196, 0.4);
+ background: rgba(78, 205, 196, 0.1);
+}
+.firstmonth-check {
+ flex-direction: row !important;
+ align-items: center;
+ gap: 10px !important;
+ font-size: 0.9rem;
+ color: var(--text-secondary);
+}
+.firstmonth-total {
+ padding: 18px;
+ border-radius: var(--radius-md);
+ background: linear-gradient(135deg, rgba(255,107,107,0.12), rgba(78,205,196,0.1));
+ border: 1px solid rgba(255,255,255,0.08);
+ margin-bottom: 14px;
+}
+.firstmonth-total small { color: var(--text-muted); display: block; }
+.firstmonth-total strong {
+ font-family: var(--font-display);
+ font-size: 2.2rem;
+ display: block;
+ margin: 4px 0 6px;
+}
+.firstmonth-total span { font-size: 0.85rem; color: var(--text-secondary); }
+.firstmonth-bars { display: flex; flex-direction: column; gap: 10px; }
+.firstmonth-bar-row {
+ display: grid;
+ grid-template-columns: 110px 1fr 72px;
+ gap: 10px;
+ align-items: center;
+ font-size: 0.88rem;
+}
+.firstmonth-bar-track {
+ height: 8px;
+ border-radius: 999px;
+ background: rgba(255,255,255,0.08);
+ overflow: hidden;
+}
+.firstmonth-bar-track i {
+ display: block;
+ height: 100%;
+ border-radius: 999px;
+ background: linear-gradient(90deg, #FF6B6B, #4ECDC4);
+}
+.firstmonth-bar-row em {
+ font-style: normal;
+ text-align: right;
+ font-weight: 600;
+ color: var(--text-secondary);
+}
+
+/* ===== Data Budget ===== */
+.data-budget-card {
+ padding: 24px;
+ background: var(--bg-card);
+ border: var(--border-glass);
+ border-radius: var(--radius-xl);
+ backdrop-filter: blur(16px);
+ display: flex;
+ flex-direction: column;
+ gap: 18px;
+}
+.data-budget-gauge {
+ height: 10px;
+ border-radius: 999px;
+ background: rgba(255,255,255,0.08);
+ overflow: hidden;
+}
+.data-budget-fill {
+ height: 100%;
+ border-radius: 999px;
+ background: linear-gradient(90deg, #4ECDC4, #A78BFA);
+ transition: width 0.3s ease;
+}
+.data-budget-hero {
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
+ margin-top: 10px;
+}
+.data-budget-hero strong {
+ font-family: var(--font-display);
+ font-size: 2.2rem;
+}
+.data-budget-hero span { color: var(--text-muted); }
+.data-budget-plan {
+ margin: 6px 0 0;
+ color: var(--text-secondary);
+ font-size: 0.92rem;
+}
+.data-budget-plan b { color: var(--accent-3); }
+.data-budget-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 8px;
+}
+.data-budget-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 12px;
+ border-radius: var(--radius-md);
+ border: 1px solid rgba(255,255,255,0.08);
+ background: rgba(255,255,255,0.03);
+ cursor: pointer;
+ color: inherit;
+ text-align: left;
+ transition: all 0.2s;
+}
+.data-budget-item:hover { transform: translateY(-1px); }
+.data-budget-item.on {
+ border-color: rgba(167, 139, 250, 0.45);
+ background: rgba(167, 139, 250, 0.12);
+}
+.data-budget-item > span { font-size: 1.25rem; }
+.data-budget-item strong { display: block; font-size: 0.88rem; }
+.data-budget-item small { color: var(--text-muted); }
+.data-budget-item i {
+ margin-left: auto;
+ font-style: normal;
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ display: grid;
+ place-items: center;
+ background: rgba(255,255,255,0.08);
+ font-size: 0.75rem;
+}
+.data-budget-item.on i { background: #A78BFA; color: #0a0e17; font-weight: 700; }
+.data-budget-sliders {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 14px;
+}
+.data-budget-sliders label {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+}
+.data-budget-sliders input[type=range] {
+ width: 100%;
+ accent-color: #A78BFA;
+}
+
+@media (max-width: 900px) {
+ .firstmonth-card { grid-template-columns: 1fr; }
+ .data-budget-grid { grid-template-columns: repeat(2, 1fr); }
+ .data-budget-sliders { grid-template-columns: 1fr; }
+ .firstmonth-bar-row { grid-template-columns: 1fr; gap: 4px; }
+ .firstmonth-bar-row em { text-align: left; }
+}
+@media (max-width: 560px) {
+ .data-budget-grid { grid-template-columns: 1fr; }
+}
diff --git a/frontend/src/app/tools/page.tsx b/frontend/src/app/tools/page.tsx
index 7fc29cc..0b62805 100644
--- a/frontend/src/app/tools/page.tsx
+++ b/frontend/src/app/tools/page.tsx
@@ -5,7 +5,7 @@ import Link from "next/link";
import { TOOL_CATEGORIES, TOOL_LINKS } from "@/lib/tools";
import SiteShell from "@/components/SiteShell";
-const FEATURED = ["visa-stay", "workspot", "runway", "daylight", "meetings", "bag-weight"];
+const FEATURED = ["first-month", "data-budget", "visa-stay", "workspot", "runway", "daylight"];
export default function ToolsHubPage() {
const [filter, setFilter] = useState("all");
@@ -41,7 +41,7 @@ export default function ToolsHubPage() {
✨ 本周推荐
- 新上线 · 停留倒计时 & 办公成本
+ 新上线 · 首月成本 & 流量预算
{featured.map((t) => (
diff --git a/frontend/src/components/DataBudgetPlanner.tsx b/frontend/src/components/DataBudgetPlanner.tsx
new file mode 100644
index 0000000..914b838
--- /dev/null
+++ b/frontend/src/components/DataBudgetPlanner.tsx
@@ -0,0 +1,103 @@
+"use client";
+
+import { useMemo, useState } from "react";
+
+const WORK_GB = [
+ { id: "docs", emoji: "📄", label: "文档/邮件", gb: 3 },
+ { id: "meet", emoji: "🎥", label: "视频会议", gb: 12 },
+ { id: "code", emoji: "💻", label: "云开发/同步", gb: 8 },
+ { id: "stream", emoji: "🎬", label: "娱乐流媒体", gb: 15 },
+ { id: "maps", emoji: "🗺️", label: "地图导航", gb: 2 },
+ { id: "backup", emoji: "☁️", label: "相册备份", gb: 10 },
+];
+
+const PLANS = [
+ { gb: 10, label: "轻量", tip: "以咖啡馆/办公网为主" },
+ { gb: 30, label: "日常", tip: "远程办公够用" },
+ { gb: 50, label: "充足", tip: "会议多也不慌" },
+ { gb: 100, label: "豪横", tip: "流媒体+备份无压力" },
+];
+
+export default function DataBudgetPlanner() {
+ const [selected, setSelected] = useState(["docs", "meet", "code", "maps"]);
+ const [meetHours, setMeetHours] = useState(8);
+ const [hotspotDays, setHotspotDays] = useState(6);
+
+ const total = useMemo(() => {
+ let gb = WORK_GB.filter((w) => selected.includes(w.id)).reduce((s, w) => s + w.gb, 0);
+ // scale video by meeting hours (base assumes ~8h)
+ if (selected.includes("meet")) {
+ gb += Math.max(0, (meetHours - 8) * 1.2);
+ }
+ // hotspot premium days add overhead
+ gb += hotspotDays * 0.8;
+ return Math.round(gb * 10) / 10;
+ }, [selected, meetHours, hotspotDays]);
+
+ const plan = PLANS.find((p) => total <= p.gb) || PLANS[PLANS.length - 1];
+ const pct = Math.min((total / 100) * 100, 100);
+
+ const toggle = (id: string) => {
+ setSelected((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
+ };
+
+ return (
+
+
+
+
📶 DATA BUDGET
+
流量预算规划
+
按办公习惯估算月流量,选 SIM / eSIM 套餐更有数
+
+
+
+
+
+
+ {total} GB
+ / 月估算
+
+
+ 建议套餐档:{plan.gb}GB · {plan.label} — {plan.tip}
+
+
+
+
+ {WORK_GB.map((w) => {
+ const on = selected.includes(w.id);
+ return (
+
+ );
+ })}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/FirstMonthCost.tsx b/frontend/src/components/FirstMonthCost.tsx
new file mode 100644
index 0000000..957fad4
--- /dev/null
+++ b/frontend/src/components/FirstMonthCost.tsx
@@ -0,0 +1,116 @@
+"use client";
+
+import { useMemo, useState } from "react";
+import type { Destination } from "@/lib/types";
+
+/** First-month one-time + rent estimates in CNY. */
+const LANDING: Record
= {
+ chiangmai: { rent: 2800, depositMult: 1, sim: 80, transfer: 120, misc: 400 },
+ bali: { rent: 3500, depositMult: 1, sim: 100, transfer: 200, misc: 500 },
+ lisbon: { rent: 6500, depositMult: 2, sim: 150, transfer: 180, misc: 800 },
+ barcelona: { rent: 7000, depositMult: 2, sim: 150, transfer: 200, misc: 900 },
+ mexico: { rent: 4500, depositMult: 1, sim: 120, transfer: 250, misc: 600 },
+ tokyo: { rent: 9000, depositMult: 2, sim: 200, transfer: 300, misc: 1200 },
+};
+
+interface Props {
+ destinations: Destination[];
+}
+
+export default function FirstMonthCost({ destinations }: Props) {
+ const [slug, setSlug] = useState(destinations[0]?.slug || "chiangmai");
+ const [housing, setHousing] = useState<"budget" | "mid" | "nice">("mid");
+ const [includeDeposit, setIncludeDeposit] = useState(true);
+
+ const base = LANDING[slug] || { rent: 4000, depositMult: 1, sim: 100, transfer: 150, misc: 500 };
+ const dest = destinations.find((d) => d.slug === slug);
+ const rentFactor = housing === "budget" ? 0.75 : housing === "nice" ? 1.35 : 1;
+
+ const breakdown = useMemo(() => {
+ const rent = Math.round(base.rent * rentFactor);
+ const deposit = includeDeposit ? Math.round(rent * base.depositMult) : 0;
+ const sim = base.sim;
+ const transfer = base.transfer;
+ const misc = Math.round(base.misc * rentFactor);
+ const total = rent + deposit + sim + transfer + misc;
+ return [
+ { key: "rent", emoji: "🏡", label: "首月房租", amount: rent },
+ { key: "deposit", emoji: "🔐", label: `押金 ×${base.depositMult}`, amount: deposit },
+ { key: "sim", emoji: "📱", label: "上网卡", amount: sim },
+ { key: "transfer", emoji: "🚕", label: "机场接驳", amount: transfer },
+ { key: "misc", emoji: "🛒", label: "日用启动", amount: misc },
+ { key: "total", emoji: "💳", label: "合计启动金", amount: total },
+ ];
+ }, [base, rentFactor, includeDeposit]);
+
+ const total = breakdown.find((b) => b.key === "total")!.amount;
+ const maxBar = Math.max(...breakdown.filter((b) => b.key !== "total").map((b) => b.amount), 1);
+
+ return (
+
+
+
+
🛬 FIRST MONTH
+
落地首月成本
+
房租、押金、SIM、接机一次算清,避免到了才慌
+
+
+
+
+
+
+ {([
+ ["budget", "💰 经济"],
+ ["mid", "🏠 舒适"],
+ ["nice", "✨ 品质"],
+ ] as const).map(([k, label]) => (
+
+ ))}
+
+
+
+
+
+
+ {dest?.emoji} 建议准备
+ ¥{total.toLocaleString()}
+ 含首月硬成本,不含机票与签证费
+
+
+ {breakdown.filter((b) => b.key !== "total" && b.amount > 0).map((b) => (
+
+
{b.emoji} {b.label}
+
+
+
+
¥{b.amount.toLocaleString()}
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/HomeClient.tsx b/frontend/src/components/HomeClient.tsx
index af293a2..330ac2d 100644
--- a/frontend/src/components/HomeClient.tsx
+++ b/frontend/src/components/HomeClient.tsx
@@ -37,6 +37,7 @@ const JetLagEstimator = lazy(() => import("./JetLagEstimator"));
const CostCompare = lazy(() => import("./CostCompare"));
const SavingsGoal = lazy(() => import("./SavingsGoal"));
const RunwayCalculator = lazy(() => import("./RunwayCalculator"));
+const FirstMonthCost = lazy(() => import("./FirstMonthCost"));
const ExpenseTracker = lazy(() => import("./ExpenseTracker"));
const TaxDayCounter = lazy(() => import("./TaxDayCounter"));
const FlightCost = lazy(() => import("./FlightCost"));
@@ -45,6 +46,7 @@ const MeetingWindow = lazy(() => import("./MeetingWindow"));
const DaylightWorkHours = lazy(() => import("./DaylightWorkHours"));
const WifiWorkScore = lazy(() => import("./WifiWorkScore"));
const WorkSpotCost = lazy(() => import("./WorkSpotCost"));
+const DataBudgetPlanner = lazy(() => import("./DataBudgetPlanner"));
const FocusTimer = lazy(() => import("./FocusTimer"));
const NomadScore = lazy(() => import("./NomadScore"));
const Lifestyle = lazy(() => import("./Lifestyle"));
@@ -172,6 +174,7 @@ export default function HomeClient(props: Props) {
+
@@ -180,6 +183,7 @@ export default function HomeClient(props: Props) {
+
diff --git a/frontend/src/components/ToolsStrip.tsx b/frontend/src/components/ToolsStrip.tsx
index ffab6ce..2954920 100644
--- a/frontend/src/components/ToolsStrip.tsx
+++ b/frontend/src/components/ToolsStrip.tsx
@@ -3,11 +3,11 @@
import Link from "next/link";
const QUICK = [
+ { href: "/#first-month", emoji: "🛬", label: "首月" },
+ { href: "/#data-budget", emoji: "📶", label: "流量" },
{ href: "/#visa-stay", emoji: "🛂", label: "签证" },
{ href: "/#workspot", emoji: "💼", label: "办公" },
{ href: "/#runway", emoji: "🚀", label: "跑道" },
- { href: "/#daylight", emoji: "☀️", label: "日照" },
- { href: "/#meetings", emoji: "🤝", label: "会议" },
{ href: "/#trip", emoji: "🗓️", label: "行程" },
{ href: "/#focus", emoji: "⏱️", label: "专注" },
{ href: "/tools", emoji: "🛠️", label: "全部" },
diff --git a/frontend/src/lib/tools.ts b/frontend/src/lib/tools.ts
index 9bc81cc..622c109 100644
--- a/frontend/src/lib/tools.ts
+++ b/frontend/src/lib/tools.ts
@@ -17,6 +17,7 @@ export const TOOL_LINKS: ToolLink[] = [
{ id: "cost-compare", emoji: "💰", title: "省钱对比", desc: "家乡 vs 旅居成本", href: "/#cost-compare", category: "money" },
{ id: "savings", emoji: "🎯", title: "启动金目标", desc: "存款进度追踪", href: "/#savings", category: "money" },
{ id: "runway", emoji: "🚀", title: "旅居跑道", desc: "存款能撑几个月", href: "/#runway", category: "money" },
+ { id: "first-month", emoji: "🛬", title: "落地首月成本", desc: "房租押金SIM一次算清", href: "/#first-month", category: "money" },
{ id: "expenses", emoji: "📒", title: "支出账本", desc: "分类记账汇总", href: "/#expenses", category: "money" },
{ id: "flight", emoji: "✈️", title: "机票估算", desc: "往返票价粗算", href: "/#flight", category: "money" },
{ id: "currency", emoji: "💱", title: "汇率换算", desc: "多币种互转", href: "/#currency", category: "money" },
@@ -26,6 +27,7 @@ export const TOOL_LINKS: ToolLink[] = [
{ id: "jetlag", emoji: "😴", title: "时差恢复", desc: "恢复期建议", href: "/#jetlag", category: "work" },
{ id: "wifi", emoji: "📶", title: "网速评级", desc: "远程办公是否够用", href: "/#wifi", category: "work" },
{ id: "workspot", emoji: "💼", title: "办公点成本", desc: "月卡 vs 咖啡馆划算度", href: "/#workspot", category: "work" },
+ { id: "data-budget", emoji: "📶", title: "流量预算", desc: "月流量与套餐建议", href: "/#data-budget", category: "work" },
{ id: "focus", emoji: "⏱️", title: "专注时钟", desc: "番茄钟深度工作", href: "/#focus", category: "work" },
{ id: "coworking", emoji: "💻", title: "联合办公", desc: "热门 Co-working", href: "/#coworking", category: "work" },
{ id: "cafes", emoji: "☕", title: "咖啡馆", desc: "办公友好咖啡店", href: "/#cafes", category: "work" },