Add daylight work window and bag weight estimator with polished UI.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b4f1846d65
commit
402e2e950b
@ -112,6 +112,8 @@ docker compose up -d
|
||||
| 🎯 启动金目标 | 存款进度追踪,预估出发时间 |
|
||||
| 🚀 旅居跑道 | 存款 ÷ 月开销,算出还能飞多久 |
|
||||
| 🤝 会议黄金时段 | 双时区工作重叠可视化 |
|
||||
| ☀️ 日照办公窗 | 日出日落与深度工作建议 |
|
||||
| ⚖️ 行李重量 | 对照航司限额估算装备重量 |
|
||||
| ✨ 今日金句 | 每日游民灵感语录 |
|
||||
| 😴 时差估算 | 时差跨度与恢复期建议 |
|
||||
| 🏡 住宿指南 | Villa / Coliving / 公寓方案对比 |
|
||||
|
||||
@ -12,9 +12,11 @@ const LOGS = [
|
||||
date: "2026-08-29",
|
||||
tag: "新工具",
|
||||
items: [
|
||||
"日照办公窗:按城市月份估算日出日落与深度工作时段",
|
||||
"行李重量估算:勾选装备对照航司限额",
|
||||
"旅居跑道计算器:存款 + 月开销一眼算出还能飞多久",
|
||||
"会议黄金时段:双时区工作重叠可视化时间轴",
|
||||
"快捷工具条更新,直达跑道 / 会议 / 汇率",
|
||||
"快捷工具条更新,直达日照 / 行李 / 跑道 / 会议",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -7872,3 +7872,243 @@ img { max-width: 100%; display: block; }
|
||||
.meeting-bars { gap: 2px; }
|
||||
.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; }
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import Link from "next/link";
|
||||
import { TOOL_CATEGORIES, TOOL_LINKS } from "@/lib/tools";
|
||||
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() {
|
||||
const [filter, setFilter] = useState("all");
|
||||
@ -41,7 +41,7 @@ export default function ToolsHubPage() {
|
||||
<div className="tools-featured">
|
||||
<div className="tools-featured-head">
|
||||
<strong>✨ 本周推荐</strong>
|
||||
<span>新上线 · 跑道 & 会议窗口</span>
|
||||
<span>新上线 · 日照窗 & 行李重量</span>
|
||||
</div>
|
||||
<div className="tools-featured-grid">
|
||||
{featured.map((t) => (
|
||||
|
||||
136
frontend/src/components/BagWeightEstimator.tsx
Normal file
136
frontend/src/components/BagWeightEstimator.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
151
frontend/src/components/DaylightWorkHours.tsx
Normal file
151
frontend/src/components/DaylightWorkHours.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -42,6 +42,7 @@ const TaxDayCounter = lazy(() => import("./TaxDayCounter"));
|
||||
const FlightCost = lazy(() => import("./FlightCost"));
|
||||
const CurrencyConverter = lazy(() => import("./CurrencyConverter"));
|
||||
const MeetingWindow = lazy(() => import("./MeetingWindow"));
|
||||
const DaylightWorkHours = lazy(() => import("./DaylightWorkHours"));
|
||||
const WifiWorkScore = lazy(() => import("./WifiWorkScore"));
|
||||
const FocusTimer = lazy(() => import("./FocusTimer"));
|
||||
const NomadScore = lazy(() => import("./NomadScore"));
|
||||
@ -59,6 +60,7 @@ const PowerPlugGuide = lazy(() => import("./PowerPlugGuide"));
|
||||
const CafeGuide = lazy(() => import("./CafeGuide"));
|
||||
const SeasonGuide = lazy(() => import("./SeasonGuide"));
|
||||
const PackingChecklist = lazy(() => import("./PackingChecklist"));
|
||||
const BagWeightEstimator = lazy(() => import("./BagWeightEstimator"));
|
||||
const ArrivalChecklist = lazy(() => import("./ArrivalChecklist"));
|
||||
const Phrasebook = lazy(() => import("./Phrasebook"));
|
||||
const CityNotes = lazy(() => import("./CityNotes"));
|
||||
@ -173,6 +175,7 @@ export default function HomeClient(props: Props) {
|
||||
<FlightCost destinations={props.destinations} />
|
||||
<CurrencyConverter />
|
||||
<MeetingWindow destinations={props.destinations} />
|
||||
<DaylightWorkHours destinations={props.destinations} />
|
||||
<WifiWorkScore destinations={props.destinations} />
|
||||
<FocusTimer />
|
||||
<NomadScore />
|
||||
@ -190,6 +193,7 @@ export default function HomeClient(props: Props) {
|
||||
<CafeGuide destinations={props.destinations} />
|
||||
<SeasonGuide destinations={props.destinations} />
|
||||
<PackingChecklist />
|
||||
<BagWeightEstimator />
|
||||
<ArrivalChecklist destinations={props.destinations} />
|
||||
<Phrasebook destinations={props.destinations} />
|
||||
<CityNotes destinations={props.destinations} />
|
||||
|
||||
@ -4,12 +4,12 @@ import Link from "next/link";
|
||||
|
||||
const QUICK = [
|
||||
{ href: "/#runway", emoji: "🚀", label: "跑道" },
|
||||
{ href: "/#daylight", emoji: "☀️", label: "日照" },
|
||||
{ href: "/#meetings", emoji: "🤝", label: "会议" },
|
||||
{ href: "/#bag-weight", emoji: "⚖️", label: "行李" },
|
||||
{ href: "/#trip", emoji: "🗓️", label: "行程" },
|
||||
{ href: "/#calculator", emoji: "🧮", label: "费用" },
|
||||
{ href: "/#visa", emoji: "📋", label: "签证" },
|
||||
{ href: "/#focus", emoji: "⏱️", label: "专注" },
|
||||
{ href: "/#currency", emoji: "💱", label: "汇率" },
|
||||
{ href: "/tools", emoji: "🛠️", label: "全部" },
|
||||
];
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ export const TOOL_LINKS: ToolLink[] = [
|
||||
{ id: "spin", emoji: "🎲", title: "命运转盘", desc: "随机抽下一站", href: "/#spin", category: "plan" },
|
||||
{ id: "matcher", emoji: "🎯", title: "智能匹配", desc: "问卷推荐目的地", href: "/#destinations", 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: "cost-compare", emoji: "💰", title: "省钱对比", desc: "家乡 vs 旅居成本", href: "/#cost-compare", 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: "plugs", emoji: "🔌", title: "电源插座", desc: "电压与插头类型", href: "/#plugs", 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: "phrases", emoji: "🗣️", title: "生存短语", desc: "常用语一键复制", href: "/#phrases", category: "life" },
|
||||
{ id: "notes", emoji: "📝", title: "城市笔记", desc: "本地私密记录", href: "/#notes", category: "life" },
|
||||
|
||||
Loading…
Reference in New Issue
Block a user