Add first-month landing cost and mobile data budget planners.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
45b4aaef53
commit
90abeac6f6
@ -116,6 +116,8 @@ docker compose up -d
|
|||||||
| ⚖️ 行李重量 | 对照航司限额估算装备重量 |
|
| ⚖️ 行李重量 | 对照航司限额估算装备重量 |
|
||||||
| 🛂 签证停留倒计时 | 入境日与剩余天数提醒 |
|
| 🛂 签证停留倒计时 | 入境日与剩余天数提醒 |
|
||||||
| 💼 办公点成本 | 月卡 vs 咖啡馆划算度对比 |
|
| 💼 办公点成本 | 月卡 vs 咖啡馆划算度对比 |
|
||||||
|
| 🛬 落地首月成本 | 房租押金 SIM 接机估算 |
|
||||||
|
| 📶 流量预算 | 月流量与套餐档建议 |
|
||||||
| ✨ 今日金句 | 每日游民灵感语录 |
|
| ✨ 今日金句 | 每日游民灵感语录 |
|
||||||
| 😴 时差估算 | 时差跨度与恢复期建议 |
|
| 😴 时差估算 | 时差跨度与恢复期建议 |
|
||||||
| 🏡 住宿指南 | Villa / Coliving / 公寓方案对比 |
|
| 🏡 住宿指南 | Villa / Coliving / 公寓方案对比 |
|
||||||
|
|||||||
@ -12,10 +12,12 @@ const LOGS = [
|
|||||||
date: "2026-08-29",
|
date: "2026-08-29",
|
||||||
tag: "新工具",
|
tag: "新工具",
|
||||||
items: [
|
items: [
|
||||||
|
"落地首月成本:房租押金 SIM 接机一次算清",
|
||||||
|
"流量预算规划:按办公习惯估算月 GB 与套餐档",
|
||||||
"签证停留倒计时:入境日 + 额度,剩余天数环形提醒",
|
"签证停留倒计时:入境日 + 额度,剩余天数环形提醒",
|
||||||
"办公点成本对比:联合办公月卡 vs 咖啡馆日刷",
|
"办公点成本对比:联合办公月卡 vs 咖啡馆日刷",
|
||||||
"日照办公窗、行李重量、旅居跑道、会议黄金时段",
|
"日照办公窗、行李重量、旅居跑道、会议黄金时段",
|
||||||
"快捷工具条直达签证 / 办公 / 跑道等核心工具",
|
"快捷工具条直达首月 / 流量 / 签证等核心工具",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -8297,3 +8297,212 @@ img { max-width: 100%; display: block; }
|
|||||||
.workspot-compare { grid-template-columns: 1fr; }
|
.workspot-compare { grid-template-columns: 1fr; }
|
||||||
.visa-stay-stats { 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; }
|
||||||
|
}
|
||||||
|
|||||||
@ -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 = ["visa-stay", "workspot", "runway", "daylight", "meetings", "bag-weight"];
|
const FEATURED = ["first-month", "data-budget", "visa-stay", "workspot", "runway", "daylight"];
|
||||||
|
|
||||||
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) => (
|
||||||
|
|||||||
103
frontend/src/components/DataBudgetPlanner.tsx
Normal file
103
frontend/src/components/DataBudgetPlanner.tsx
Normal file
@ -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 (
|
||||||
|
<section className="section data-budget-section" id="data-budget">
|
||||||
|
<div className="container">
|
||||||
|
<div className="section-header reveal">
|
||||||
|
<span className="section-tag">📶 DATA BUDGET</span>
|
||||||
|
<h2>流量预算规划</h2>
|
||||||
|
<p>按办公习惯估算月流量,选 SIM / eSIM 套餐更有数</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="data-budget-card reveal">
|
||||||
|
<div className="data-budget-meter">
|
||||||
|
<div className="data-budget-gauge">
|
||||||
|
<div className="data-budget-fill" style={{ width: `${pct}%` }} />
|
||||||
|
</div>
|
||||||
|
<div className="data-budget-hero">
|
||||||
|
<strong>{total} GB</strong>
|
||||||
|
<span>/ 月估算</span>
|
||||||
|
</div>
|
||||||
|
<p className="data-budget-plan">
|
||||||
|
建议套餐档:<b>{plan.gb}GB · {plan.label}</b> — {plan.tip}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="data-budget-grid">
|
||||||
|
{WORK_GB.map((w) => {
|
||||||
|
const on = selected.includes(w.id);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={w.id}
|
||||||
|
type="button"
|
||||||
|
className={`data-budget-item${on ? " on" : ""}`}
|
||||||
|
onClick={() => toggle(w.id)}
|
||||||
|
aria-pressed={on}
|
||||||
|
>
|
||||||
|
<span>{w.emoji}</span>
|
||||||
|
<div>
|
||||||
|
<strong>{w.label}</strong>
|
||||||
|
<small>~{w.gb} GB</small>
|
||||||
|
</div>
|
||||||
|
<i>{on ? "✓" : "+"}</i>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="data-budget-sliders">
|
||||||
|
<label>
|
||||||
|
<span>视频会议时长:{meetHours} 小时/月</span>
|
||||||
|
<input type="range" min={0} max={40} value={meetHours} onChange={(e) => setMeetHours(+e.target.value)} />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>纯热点办公天数:{hotspotDays} 天/月</span>
|
||||||
|
<input type="range" min={0} max={20} value={hotspotDays} onChange={(e) => setHotspotDays(+e.target.value)} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
116
frontend/src/components/FirstMonthCost.tsx
Normal file
116
frontend/src/components/FirstMonthCost.tsx
Normal file
@ -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<string, { rent: number; depositMult: number; sim: number; transfer: number; misc: number }> = {
|
||||||
|
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 (
|
||||||
|
<section className="section firstmonth-section" id="first-month">
|
||||||
|
<div className="container">
|
||||||
|
<div className="section-header reveal">
|
||||||
|
<span className="section-tag">🛬 FIRST MONTH</span>
|
||||||
|
<h2>落地首月成本</h2>
|
||||||
|
<p>房租、押金、SIM、接机一次算清,避免到了才慌</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="firstmonth-card reveal">
|
||||||
|
<div className="firstmonth-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>
|
||||||
|
<div className="firstmonth-housing">
|
||||||
|
{([
|
||||||
|
["budget", "💰 经济"],
|
||||||
|
["mid", "🏠 舒适"],
|
||||||
|
["nice", "✨ 品质"],
|
||||||
|
] as const).map(([k, label]) => (
|
||||||
|
<button
|
||||||
|
key={k}
|
||||||
|
type="button"
|
||||||
|
className={`firstmonth-chip${housing === k ? " active" : ""}`}
|
||||||
|
onClick={() => setHousing(k)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<label className="firstmonth-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={includeDeposit}
|
||||||
|
onChange={(e) => setIncludeDeposit(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span>计入押金(当地常见 {base.depositMult} 个月)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="firstmonth-result">
|
||||||
|
<div className="firstmonth-total">
|
||||||
|
<small>{dest?.emoji} 建议准备</small>
|
||||||
|
<strong>¥{total.toLocaleString()}</strong>
|
||||||
|
<span>含首月硬成本,不含机票与签证费</span>
|
||||||
|
</div>
|
||||||
|
<div className="firstmonth-bars">
|
||||||
|
{breakdown.filter((b) => b.key !== "total" && b.amount > 0).map((b) => (
|
||||||
|
<div key={b.key} className="firstmonth-bar-row">
|
||||||
|
<span>{b.emoji} {b.label}</span>
|
||||||
|
<div className="firstmonth-bar-track">
|
||||||
|
<i style={{ width: `${(b.amount / maxBar) * 100}%` }} />
|
||||||
|
</div>
|
||||||
|
<em>¥{b.amount.toLocaleString()}</em>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -37,6 +37,7 @@ const JetLagEstimator = lazy(() => import("./JetLagEstimator"));
|
|||||||
const CostCompare = lazy(() => import("./CostCompare"));
|
const CostCompare = lazy(() => import("./CostCompare"));
|
||||||
const SavingsGoal = lazy(() => import("./SavingsGoal"));
|
const SavingsGoal = lazy(() => import("./SavingsGoal"));
|
||||||
const RunwayCalculator = lazy(() => import("./RunwayCalculator"));
|
const RunwayCalculator = lazy(() => import("./RunwayCalculator"));
|
||||||
|
const FirstMonthCost = lazy(() => import("./FirstMonthCost"));
|
||||||
const ExpenseTracker = lazy(() => import("./ExpenseTracker"));
|
const ExpenseTracker = lazy(() => import("./ExpenseTracker"));
|
||||||
const TaxDayCounter = lazy(() => import("./TaxDayCounter"));
|
const TaxDayCounter = lazy(() => import("./TaxDayCounter"));
|
||||||
const FlightCost = lazy(() => import("./FlightCost"));
|
const FlightCost = lazy(() => import("./FlightCost"));
|
||||||
@ -45,6 +46,7 @@ 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 WorkSpotCost = lazy(() => import("./WorkSpotCost"));
|
||||||
|
const DataBudgetPlanner = lazy(() => import("./DataBudgetPlanner"));
|
||||||
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"));
|
||||||
@ -172,6 +174,7 @@ export default function HomeClient(props: Props) {
|
|||||||
<CostCompare destinations={props.destinations} />
|
<CostCompare destinations={props.destinations} />
|
||||||
<SavingsGoal destinations={props.destinations} />
|
<SavingsGoal destinations={props.destinations} />
|
||||||
<RunwayCalculator destinations={props.destinations} />
|
<RunwayCalculator destinations={props.destinations} />
|
||||||
|
<FirstMonthCost destinations={props.destinations} />
|
||||||
<ExpenseTracker />
|
<ExpenseTracker />
|
||||||
<TaxDayCounter />
|
<TaxDayCounter />
|
||||||
<FlightCost destinations={props.destinations} />
|
<FlightCost destinations={props.destinations} />
|
||||||
@ -180,6 +183,7 @@ export default function HomeClient(props: Props) {
|
|||||||
<DaylightWorkHours destinations={props.destinations} />
|
<DaylightWorkHours destinations={props.destinations} />
|
||||||
<WifiWorkScore destinations={props.destinations} />
|
<WifiWorkScore destinations={props.destinations} />
|
||||||
<WorkSpotCost destinations={props.destinations} />
|
<WorkSpotCost destinations={props.destinations} />
|
||||||
|
<DataBudgetPlanner />
|
||||||
<FocusTimer />
|
<FocusTimer />
|
||||||
<NomadScore />
|
<NomadScore />
|
||||||
<Lifestyle />
|
<Lifestyle />
|
||||||
|
|||||||
@ -3,11 +3,11 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
const QUICK = [
|
const QUICK = [
|
||||||
|
{ href: "/#first-month", emoji: "🛬", label: "首月" },
|
||||||
|
{ href: "/#data-budget", emoji: "📶", label: "流量" },
|
||||||
{ href: "/#visa-stay", emoji: "🛂", label: "签证" },
|
{ href: "/#visa-stay", emoji: "🛂", label: "签证" },
|
||||||
{ href: "/#workspot", emoji: "💼", label: "办公" },
|
{ href: "/#workspot", emoji: "💼", label: "办公" },
|
||||||
{ href: "/#runway", emoji: "🚀", label: "跑道" },
|
{ href: "/#runway", emoji: "🚀", label: "跑道" },
|
||||||
{ href: "/#daylight", emoji: "☀️", label: "日照" },
|
|
||||||
{ href: "/#meetings", emoji: "🤝", label: "会议" },
|
|
||||||
{ href: "/#trip", emoji: "🗓️", label: "行程" },
|
{ href: "/#trip", emoji: "🗓️", label: "行程" },
|
||||||
{ href: "/#focus", emoji: "⏱️", label: "专注" },
|
{ href: "/#focus", emoji: "⏱️", label: "专注" },
|
||||||
{ href: "/tools", emoji: "🛠️", label: "全部" },
|
{ href: "/tools", emoji: "🛠️", label: "全部" },
|
||||||
|
|||||||
@ -17,6 +17,7 @@ export const TOOL_LINKS: ToolLink[] = [
|
|||||||
{ id: "cost-compare", emoji: "💰", title: "省钱对比", desc: "家乡 vs 旅居成本", href: "/#cost-compare", category: "money" },
|
{ id: "cost-compare", emoji: "💰", title: "省钱对比", desc: "家乡 vs 旅居成本", href: "/#cost-compare", category: "money" },
|
||||||
{ id: "savings", emoji: "🎯", title: "启动金目标", desc: "存款进度追踪", href: "/#savings", category: "money" },
|
{ id: "savings", emoji: "🎯", title: "启动金目标", desc: "存款进度追踪", href: "/#savings", category: "money" },
|
||||||
{ id: "runway", emoji: "🚀", title: "旅居跑道", desc: "存款能撑几个月", href: "/#runway", 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: "expenses", emoji: "📒", title: "支出账本", desc: "分类记账汇总", href: "/#expenses", category: "money" },
|
||||||
{ id: "flight", emoji: "✈️", title: "机票估算", desc: "往返票价粗算", href: "/#flight", category: "money" },
|
{ id: "flight", emoji: "✈️", title: "机票估算", desc: "往返票价粗算", href: "/#flight", category: "money" },
|
||||||
{ id: "currency", emoji: "💱", title: "汇率换算", desc: "多币种互转", href: "/#currency", 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: "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: "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: "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" },
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user