Add daylight work window and bag weight estimator with polished UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
eric 2026-08-29 01:26:07 -05:00
parent b4f1846d65
commit 402e2e950b
9 changed files with 542 additions and 5 deletions

View File

@ -112,6 +112,8 @@ docker compose up -d
| 🎯 启动金目标 | 存款进度追踪,预估出发时间 | | 🎯 启动金目标 | 存款进度追踪,预估出发时间 |
| 🚀 旅居跑道 | 存款 ÷ 月开销,算出还能飞多久 | | 🚀 旅居跑道 | 存款 ÷ 月开销,算出还能飞多久 |
| 🤝 会议黄金时段 | 双时区工作重叠可视化 | | 🤝 会议黄金时段 | 双时区工作重叠可视化 |
| ☀️ 日照办公窗 | 日出日落与深度工作建议 |
| ⚖️ 行李重量 | 对照航司限额估算装备重量 |
| ✨ 今日金句 | 每日游民灵感语录 | | ✨ 今日金句 | 每日游民灵感语录 |
| 😴 时差估算 | 时差跨度与恢复期建议 | | 😴 时差估算 | 时差跨度与恢复期建议 |
| 🏡 住宿指南 | Villa / Coliving / 公寓方案对比 | | 🏡 住宿指南 | Villa / Coliving / 公寓方案对比 |

View File

@ -12,9 +12,11 @@ const LOGS = [
date: "2026-08-29", date: "2026-08-29",
tag: "新工具", tag: "新工具",
items: [ items: [
"日照办公窗:按城市月份估算日出日落与深度工作时段",
"行李重量估算:勾选装备对照航司限额",
"旅居跑道计算器:存款 + 月开销一眼算出还能飞多久", "旅居跑道计算器:存款 + 月开销一眼算出还能飞多久",
"会议黄金时段:双时区工作重叠可视化时间轴", "会议黄金时段:双时区工作重叠可视化时间轴",
"快捷工具条更新,直达跑道 / 会议 / 汇率", "快捷工具条更新,直达日照 / 行李 / 跑道 / 会议",
], ],
}, },
{ {

View File

@ -7872,3 +7872,243 @@ img { max-width: 100%; display: block; }
.meeting-bars { gap: 2px; } .meeting-bars { gap: 2px; }
.meeting-cell { min-height: 36px; } .meeting-cell { min-height: 36px; }
} }
/* ===== Daylight Work Hours ===== */
.daylight-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: 20px;
}
.daylight-controls {
display: flex;
flex-direction: column;
gap: 12px;
}
.daylight-controls label {
display: flex;
flex-direction: column;
gap: 6px;
max-width: 320px;
}
.daylight-controls label > span,
.bag-extra > span {
font-size: 0.85rem;
color: var(--text-secondary);
}
.daylight-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;
}
.daylight-months {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.daylight-month {
padding: 6px 10px;
border-radius: 999px;
border: 1px solid rgba(255,255,255,0.08);
background: rgba(255,255,255,0.03);
color: var(--text-secondary);
cursor: pointer;
font-size: 0.8rem;
transition: all 0.2s;
}
.daylight-month:hover,
.daylight-month.active {
color: var(--text-primary);
border-color: rgba(255, 230, 109, 0.45);
background: rgba(255, 230, 109, 0.12);
}
.daylight-sky {
position: relative;
height: 72px;
border-radius: var(--radius-md);
background: linear-gradient(90deg, #0b1224, #1a2744 40%, #2a1f3d 70%, #0b1224);
overflow: hidden;
border: 1px solid rgba(255,255,255,0.06);
}
.daylight-band {
position: absolute;
top: 0;
bottom: 0;
background: linear-gradient(180deg, rgba(255,230,109,0.45), rgba(255,107,107,0.15));
}
.daylight-deep {
position: absolute;
top: 18px;
bottom: 18px;
background: rgba(78, 205, 196, 0.55);
border-radius: 6px;
box-shadow: 0 0 16px rgba(78, 205, 196, 0.35);
}
.daylight-markers {
position: absolute;
inset: 0;
pointer-events: none;
}
.daylight-markers span {
position: absolute;
bottom: 6px;
transform: translateX(-50%);
font-size: 0.65rem;
color: rgba(241,245,249,0.7);
}
.daylight-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin-top: 12px;
}
.daylight-stats small {
display: block;
color: var(--text-muted);
font-size: 0.75rem;
}
.daylight-stats strong {
font-family: var(--font-display);
font-size: 1.25rem;
}
.daylight-tips {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.daylight-tip {
display: flex;
gap: 12px;
padding: 14px;
border-radius: var(--radius-md);
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
}
.daylight-tip > span { font-size: 1.4rem; }
.daylight-tip strong { display: block; margin-bottom: 4px; font-size: 0.95rem; }
.daylight-tip p { margin: 0; color: var(--text-secondary); font-size: 0.85rem; line-height: 1.45; }
/* ===== Bag Weight ===== */
.bag-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;
}
.bag-gauge {
height: 10px;
border-radius: 999px;
background: rgba(255,255,255,0.08);
overflow: hidden;
}
.bag-gauge-fill {
height: 100%;
border-radius: 999px;
transition: width 0.35s ease, background 0.25s;
}
.bag-meter-row {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 12px;
margin-top: 10px;
}
.bag-meter-row strong {
font-family: var(--font-display);
font-size: 1.8rem;
}
.bag-meter-row small { color: var(--text-muted); }
.bag-status { font-weight: 700; }
.bag-tip { color: var(--text-secondary); font-size: 0.9rem; margin: 6px 0 0; }
.bag-limits {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.bag-limit {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 10px 6px;
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;
transition: all 0.2s;
}
.bag-limit:hover,
.bag-limit.active {
border-color: rgba(78, 205, 196, 0.4);
background: rgba(78, 205, 196, 0.1);
}
.bag-limit em { font-style: normal; font-size: 0.8rem; font-weight: 600; }
.bag-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.bag-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;
}
.bag-item:hover { transform: translateY(-1px); }
.bag-item.on {
border-color: rgba(78, 205, 196, 0.4);
background: rgba(78, 205, 196, 0.1);
}
.bag-item > span { font-size: 1.25rem; }
.bag-item strong { display: block; font-size: 0.88rem; }
.bag-item small { color: var(--text-muted); }
.bag-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;
}
.bag-item.on i { background: var(--accent-3); color: #0a0e17; font-weight: 700; }
.bag-extra {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 12px;
align-items: center;
}
.bag-extra input[type=range] { width: 100%; accent-color: var(--accent-3); }
.bag-extra em { font-style: normal; font-weight: 600; color: var(--accent-3); min-width: 3.5rem; text-align: right; }
@media (max-width: 900px) {
.daylight-tips { grid-template-columns: 1fr; }
.daylight-stats { grid-template-columns: repeat(2, 1fr); }
.bag-grid { grid-template-columns: repeat(2, 1fr); }
.bag-limits { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 560px) {
.bag-grid { grid-template-columns: 1fr; }
.bag-extra { 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 = ["runway", "meetings", "trip", "calculator", "visa", "focus"]; const FEATURED = ["daylight", "bag-weight", "runway", "meetings", "trip", "focus"];
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

@ -0,0 +1,136 @@
"use client";
import { useMemo, useState } from "react";
interface Gear {
id: string;
emoji: string;
label: string;
kg: number;
group: "tech" | "wear" | "daily";
}
const GEAR: Gear[] = [
{ id: "laptop", emoji: "💻", label: "笔记本电脑", kg: 1.4, group: "tech" },
{ id: "charger", emoji: "🔌", label: "充电套装", kg: 0.4, group: "tech" },
{ id: "tablet", emoji: "📱", label: "平板 / 手机", kg: 0.5, group: "tech" },
{ id: "headset", emoji: "🎧", label: "耳机", kg: 0.3, group: "tech" },
{ id: "camera", emoji: "📷", label: "相机", kg: 0.8, group: "tech" },
{ id: "clothes", emoji: "👕", label: "衣物一周", kg: 2.5, group: "wear" },
{ id: "shoes", emoji: "👟", label: "鞋 x2", kg: 1.2, group: "wear" },
{ id: "jacket", emoji: "🧥", label: "外套", kg: 0.9, group: "wear" },
{ id: "toiletry", emoji: "🧴", label: "洗漱包", kg: 0.6, group: "daily" },
{ id: "meds", emoji: "💊", label: "药品杂物", kg: 0.4, group: "daily" },
{ id: "book", emoji: "📖", label: "书籍 / 周边", kg: 0.7, group: "daily" },
{ id: "bottle", emoji: "💧", label: "水瓶(空)", kg: 0.3, group: "daily" },
];
const LIMITS = [
{ key: "cabin7", label: "随身 7kg", max: 7, emoji: "🎒" },
{ key: "cabin10", label: "随身 10kg", max: 10, emoji: "🧳" },
{ key: "check20", label: "托运 20kg", max: 20, emoji: "✈️" },
{ key: "check23", label: "托运 23kg", max: 23, emoji: "🛫" },
];
export default function BagWeightEstimator() {
const [selected, setSelected] = useState<string[]>(["laptop", "charger", "headset", "clothes", "shoes", "toiletry"]);
const [limit, setLimit] = useState("cabin10");
const [extra, setExtra] = useState(0.5);
const total = useMemo(() => {
const base = GEAR.filter((g) => selected.includes(g.id)).reduce((s, g) => s + g.kg, 0);
return Math.round((base + extra) * 10) / 10;
}, [selected, extra]);
const lim = LIMITS.find((l) => l.key === limit)!;
const pct = Math.min((total / lim.max) * 100, 100);
const over = total > lim.max;
const room = Math.round((lim.max - total) * 10) / 10;
const toggle = (id: string) => {
setSelected((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
};
const status = over
? { label: "超重预警", color: "#FF6B6B", tip: `超出约 ${Math.abs(room)} kg,建议精简或升舱额` }
: room <= 1
? { label: "接近上限", color: "#FFE66D", tip: "余量很少,登机前再称一次" }
: { label: "轻装通过", color: "#4ECDC4", tip: `还可再装约 ${room} kg` };
return (
<section className="section bag-section" id="bag-weight">
<div className="container">
<div className="section-header reveal">
<span className="section-tag">⚖️ BAG WEIGHT</span>
<h2>行李重量估算</h2>
<p>勾选装备,对照航司限额,出发前心里有数</p>
</div>
<div className="bag-card reveal">
<div className="bag-meter">
<div className="bag-gauge">
<div className="bag-gauge-fill" style={{ width: `${pct}%`, background: status.color }} />
</div>
<div className="bag-meter-row">
<div>
<strong style={{ color: status.color }}>{total} kg</strong>
<small> / {lim.max} kg · {lim.emoji} {lim.label}</small>
</div>
<span className="bag-status" style={{ color: status.color }}>{status.label}</span>
</div>
<p className="bag-tip">{status.tip}</p>
</div>
<div className="bag-limits">
{LIMITS.map((l) => (
<button
key={l.key}
type="button"
className={`bag-limit${limit === l.key ? " active" : ""}`}
onClick={() => setLimit(l.key)}
>
<span>{l.emoji}</span>
<em>{l.label}</em>
</button>
))}
</div>
<div className="bag-grid">
{GEAR.map((g) => {
const on = selected.includes(g.id);
return (
<button
key={g.id}
type="button"
className={`bag-item${on ? " on" : ""}`}
onClick={() => toggle(g.id)}
aria-pressed={on}
>
<span>{g.emoji}</span>
<div>
<strong>{g.label}</strong>
<small>{g.kg} kg</small>
</div>
<i>{on ? "✓" : "+"}</i>
</button>
);
})}
</div>
<label className="bag-extra">
<span>其他杂项(kg)</span>
<input
type="range"
min={0}
max={5}
step={0.1}
value={extra}
onChange={(e) => setExtra(+e.target.value)}
/>
<em>{extra.toFixed(1)} kg</em>
</label>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,151 @@
"use client";
import { useMemo, useState } from "react";
import type { Destination } from "@/lib/types";
/** Approximate daylight by latitude + month (no API). */
const CITY_META: Record<string, { lat: number; tip: string }> = {
chiangmai: { lat: 18.8, tip: "热带全年日照长,午后雷阵雨多见" },
bali: { lat: -8.4, tip: "赤道附近昼夜接近 12h,早晚温和好出门" },
lisbon: { lat: 38.7, tip: "冬短夏长,夏季晚上 9 点仍明亮" },
barcelona: { lat: 41.4, tip: "地中海日照充沛,冬日仍可户外办公" },
mexico: { lat: 19.4, tip: "高原日照强,注意防晒与补水" },
tokyo: { lat: 35.7, tip: "四季分明,冬季日落早适合室内专注" },
};
function approxDayLength(lat: number, month: number): number {
const m = month - 1;
const decl = 23.44 * Math.sin(((284 + month * 30.4) * Math.PI) / 180);
const latRad = (lat * Math.PI) / 180;
const declRad = (decl * Math.PI) / 180;
const cosH = -Math.tan(latRad) * Math.tan(declRad);
const clamped = Math.max(-1, Math.min(1, cosH));
const h = Math.acos(clamped);
void m;
return (2 * h * 12) / Math.PI;
}
function fmtHour(h: number): string {
const hh = Math.floor(h) % 24;
const mm = Math.round((h - Math.floor(h)) * 60);
return `${String(hh).padStart(2, "0")}:${String(mm).padStart(2, "0")}`;
}
interface Props {
destinations: Destination[];
}
export default function DaylightWorkHours({ destinations }: Props) {
const [slug, setSlug] = useState(destinations[0]?.slug || "chiangmai");
const [month, setMonth] = useState(new Date().getMonth() + 1);
const meta = CITY_META[slug] || { lat: 20, tip: "选择城市查看日照与办公建议" };
const dayLen = useMemo(() => approxDayLength(meta.lat, month), [meta.lat, month]);
const sunrise = 12 - dayLen / 2;
const sunset = 12 + dayLen / 2;
const deepStart = Math.max(sunrise + 1.5, 8);
const deepEnd = Math.min(sunset - 1, 17);
const outdoorOk = dayLen >= 11;
const months = Array.from({ length: 12 }, (_, i) => i + 1);
const dest = destinations.find((d) => d.slug === slug);
const score =
dayLen >= 13 ? { label: "长日照", color: "#FFE66D" } :
dayLen >= 11 ? { label: "均衡", color: "#4ECDC4" } :
{ label: "短日照", color: "#A78BFA" };
return (
<section className="section daylight-section" id="daylight">
<div className="container">
<div className="section-header reveal">
<span className="section-tag">☀️ DAYLIGHT</span>
<h2>日照办公窗</h2>
<p>按城市与月份估算日出日落,安排深度工作与户外时间</p>
</div>
<div className="daylight-card reveal">
<div className="daylight-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="daylight-months" role="listbox" aria-label="月份">
{months.map((m) => (
<button
key={m}
type="button"
className={`daylight-month${month === m ? " active" : ""}`}
onClick={() => setMonth(m)}
>
{m}月
</button>
))}
</div>
</div>
<div className="daylight-visual">
<div className="daylight-sky">
<div
className="daylight-band"
style={{
left: `${(sunrise / 24) * 100}%`,
width: `${(dayLen / 24) * 100}%`,
}}
/>
<div className="daylight-deep" style={{
left: `${(deepStart / 24) * 100}%`,
width: `${(Math.max(deepEnd - deepStart, 0) / 24) * 100}%`,
}} />
<div className="daylight-markers">
{[0, 6, 12, 18, 24].map((h) => (
<span key={h} style={{ left: `${(h / 24) * 100}%` }}>{fmtHour(h)}</span>
))}
</div>
</div>
<div className="daylight-stats">
<div>
<small>日出</small>
<strong>{fmtHour(sunrise)}</strong>
</div>
<div>
<small>日落</small>
<strong>{fmtHour(sunset)}</strong>
</div>
<div>
<small>昼长</small>
<strong style={{ color: score.color }}>{dayLen.toFixed(1)}h</strong>
</div>
<div>
<small>状态</small>
<strong style={{ color: score.color }}>{score.label}</strong>
</div>
</div>
</div>
<div className="daylight-tips">
<div className="daylight-tip">
<span>💻</span>
<div>
<strong>深度工作建议</strong>
<p>{fmtHour(deepStart)} – {fmtHour(deepEnd)} · 自然光充足且会议较少的时段</p>
</div>
</div>
<div className="daylight-tip">
<span>{outdoorOk ? "🚶" : "🏠"}</span>
<div>
<strong>{outdoorOk ? "适合户外散步会议" : "偏室内节奏"}</strong>
<p>{meta.tip}{dest ? ` · ${dest.emoji} ${dest.name}` : ""}</p>
</div>
</div>
</div>
</div>
</div>
</section>
);
}

View File

@ -42,6 +42,7 @@ const TaxDayCounter = lazy(() => import("./TaxDayCounter"));
const FlightCost = lazy(() => import("./FlightCost")); const FlightCost = lazy(() => import("./FlightCost"));
const CurrencyConverter = lazy(() => import("./CurrencyConverter")); const CurrencyConverter = lazy(() => import("./CurrencyConverter"));
const MeetingWindow = lazy(() => import("./MeetingWindow")); const MeetingWindow = lazy(() => import("./MeetingWindow"));
const DaylightWorkHours = lazy(() => import("./DaylightWorkHours"));
const WifiWorkScore = lazy(() => import("./WifiWorkScore")); const WifiWorkScore = lazy(() => import("./WifiWorkScore"));
const FocusTimer = lazy(() => import("./FocusTimer")); const FocusTimer = lazy(() => import("./FocusTimer"));
const NomadScore = lazy(() => import("./NomadScore")); const NomadScore = lazy(() => import("./NomadScore"));
@ -59,6 +60,7 @@ const PowerPlugGuide = lazy(() => import("./PowerPlugGuide"));
const CafeGuide = lazy(() => import("./CafeGuide")); const CafeGuide = lazy(() => import("./CafeGuide"));
const SeasonGuide = lazy(() => import("./SeasonGuide")); const SeasonGuide = lazy(() => import("./SeasonGuide"));
const PackingChecklist = lazy(() => import("./PackingChecklist")); const PackingChecklist = lazy(() => import("./PackingChecklist"));
const BagWeightEstimator = lazy(() => import("./BagWeightEstimator"));
const ArrivalChecklist = lazy(() => import("./ArrivalChecklist")); const ArrivalChecklist = lazy(() => import("./ArrivalChecklist"));
const Phrasebook = lazy(() => import("./Phrasebook")); const Phrasebook = lazy(() => import("./Phrasebook"));
const CityNotes = lazy(() => import("./CityNotes")); const CityNotes = lazy(() => import("./CityNotes"));
@ -173,6 +175,7 @@ export default function HomeClient(props: Props) {
<FlightCost destinations={props.destinations} /> <FlightCost destinations={props.destinations} />
<CurrencyConverter /> <CurrencyConverter />
<MeetingWindow destinations={props.destinations} /> <MeetingWindow destinations={props.destinations} />
<DaylightWorkHours destinations={props.destinations} />
<WifiWorkScore destinations={props.destinations} /> <WifiWorkScore destinations={props.destinations} />
<FocusTimer /> <FocusTimer />
<NomadScore /> <NomadScore />
@ -190,6 +193,7 @@ export default function HomeClient(props: Props) {
<CafeGuide destinations={props.destinations} /> <CafeGuide destinations={props.destinations} />
<SeasonGuide destinations={props.destinations} /> <SeasonGuide destinations={props.destinations} />
<PackingChecklist /> <PackingChecklist />
<BagWeightEstimator />
<ArrivalChecklist destinations={props.destinations} /> <ArrivalChecklist destinations={props.destinations} />
<Phrasebook destinations={props.destinations} /> <Phrasebook destinations={props.destinations} />
<CityNotes destinations={props.destinations} /> <CityNotes destinations={props.destinations} />

View File

@ -4,12 +4,12 @@ import Link from "next/link";
const QUICK = [ const QUICK = [
{ href: "/#runway", emoji: "🚀", label: "跑道" }, { href: "/#runway", 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: "/#calculator", emoji: "🧮", label: "费用" },
{ href: "/#visa", emoji: "📋", label: "签证" },
{ href: "/#focus", emoji: "⏱️", label: "专注" }, { href: "/#focus", emoji: "⏱️", label: "专注" },
{ href: "/#currency", emoji: "💱", label: "汇率" },
{ href: "/tools", emoji: "🛠️", label: "全部" }, { href: "/tools", emoji: "🛠️", label: "全部" },
]; ];

View File

@ -12,6 +12,7 @@ export const TOOL_LINKS: ToolLink[] = [
{ id: "spin", emoji: "🎲", title: "命运转盘", desc: "随机抽下一站", href: "/#spin", category: "plan" }, { id: "spin", emoji: "🎲", title: "命运转盘", desc: "随机抽下一站", href: "/#spin", category: "plan" },
{ id: "matcher", emoji: "🎯", title: "智能匹配", desc: "问卷推荐目的地", href: "/#destinations", category: "plan" }, { id: "matcher", emoji: "🎯", title: "智能匹配", desc: "问卷推荐目的地", href: "/#destinations", category: "plan" },
{ id: "season", emoji: "🌤️", title: "最佳月份", desc: "避开雨季与旺季", href: "/#season", category: "plan" }, { id: "season", emoji: "🌤️", title: "最佳月份", desc: "避开雨季与旺季", href: "/#season", category: "plan" },
{ id: "daylight", emoji: "☀️", title: "日照办公窗", desc: "日出日落与深度工作", href: "/#daylight", category: "work" },
{ id: "calculator", emoji: "🧮", title: "费用计算器", desc: "按住宿档次估算", href: "/#calculator", category: "money" }, { id: "calculator", emoji: "🧮", title: "费用计算器", desc: "按住宿档次估算", href: "/#calculator", category: "money" },
{ 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" },
@ -32,6 +33,7 @@ export const TOOL_LINKS: ToolLink[] = [
{ 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" },
{ id: "packing", emoji: "🧳", title: "行李清单", desc: "出发前逐项勾选", href: "/#packing", category: "life" }, { id: "packing", emoji: "🧳", title: "行李清单", desc: "出发前逐项勾选", href: "/#packing", category: "life" },
{ id: "bag-weight", emoji: "⚖️", title: "行李重量", desc: "对照航司限额估算", href: "/#bag-weight", category: "life" },
{ id: "arrival", emoji: "🛬", title: "到站清单", desc: "落地第一周", href: "/#arrival", category: "life" }, { id: "arrival", emoji: "🛬", title: "到站清单", desc: "落地第一周", href: "/#arrival", category: "life" },
{ id: "phrases", emoji: "🗣️", title: "生存短语", desc: "常用语一键复制", href: "/#phrases", category: "life" }, { id: "phrases", emoji: "🗣️", title: "生存短语", desc: "常用语一键复制", href: "/#phrases", category: "life" },
{ id: "notes", emoji: "📝", title: "城市笔记", desc: "本地私密记录", href: "/#notes", category: "life" }, { id: "notes", emoji: "📝", title: "城市笔记", desc: "本地私密记录", href: "/#notes", category: "life" },