Add visa stay countdown and cowork-vs-cafe cost comparator.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2943f48035
commit
45b4aaef53
@ -114,6 +114,8 @@ docker compose up -d
|
||||
| 🤝 会议黄金时段 | 双时区工作重叠可视化 |
|
||||
| ☀️ 日照办公窗 | 日出日落与深度工作建议 |
|
||||
| ⚖️ 行李重量 | 对照航司限额估算装备重量 |
|
||||
| 🛂 签证停留倒计时 | 入境日与剩余天数提醒 |
|
||||
| 💼 办公点成本 | 月卡 vs 咖啡馆划算度对比 |
|
||||
| ✨ 今日金句 | 每日游民灵感语录 |
|
||||
| 😴 时差估算 | 时差跨度与恢复期建议 |
|
||||
| 🏡 住宿指南 | Villa / Coliving / 公寓方案对比 |
|
||||
|
||||
@ -12,11 +12,10 @@ const LOGS = [
|
||||
date: "2026-08-29",
|
||||
tag: "新工具",
|
||||
items: [
|
||||
"日照办公窗:按城市月份估算日出日落与深度工作时段",
|
||||
"行李重量估算:勾选装备对照航司限额",
|
||||
"旅居跑道计算器:存款 + 月开销一眼算出还能飞多久",
|
||||
"会议黄金时段:双时区工作重叠可视化时间轴",
|
||||
"快捷工具条更新,直达日照 / 行李 / 跑道 / 会议",
|
||||
"签证停留倒计时:入境日 + 额度,剩余天数环形提醒",
|
||||
"办公点成本对比:联合办公月卡 vs 咖啡馆日刷",
|
||||
"日照办公窗、行李重量、旅居跑道、会议黄金时段",
|
||||
"快捷工具条直达签证 / 办公 / 跑道等核心工具",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -8112,3 +8112,188 @@ img { max-width: 100%; display: block; }
|
||||
.bag-grid { grid-template-columns: 1fr; }
|
||||
.bag-extra { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
|
||||
/* ===== Visa Stay Countdown ===== */
|
||||
.visa-stay-card {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 280px) 1fr;
|
||||
gap: 28px;
|
||||
padding: 28px;
|
||||
background: var(--bg-card);
|
||||
border: var(--border-glass);
|
||||
border-radius: var(--radius-xl);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
.visa-stay-meter { text-align: center; }
|
||||
.visa-stay-ring {
|
||||
width: 168px;
|
||||
height: 168px;
|
||||
border-radius: 50%;
|
||||
margin: 0 auto 16px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.visa-stay-ring-inner {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-secondary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.visa-stay-ring-inner strong {
|
||||
font-family: var(--font-display);
|
||||
font-size: 2.6rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.visa-stay-ring-inner small { color: var(--text-muted); margin-top: 4px; }
|
||||
.visa-stay-level span { font-weight: 700; font-size: 1.05rem; }
|
||||
.visa-stay-level p {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.88rem;
|
||||
margin-top: 6px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.visa-stay-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
.visa-stay-form > label,
|
||||
.workspot-controls > label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.visa-stay-form > label > span,
|
||||
.workspot-controls > label > span {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.visa-stay-form input,
|
||||
.visa-stay-form select,
|
||||
.workspot-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;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.visa-stay-hint {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--accent-3);
|
||||
}
|
||||
.visa-stay-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
.visa-stay-stats small { display: block; color: var(--text-muted); }
|
||||
.visa-stay-stats strong { font-size: 1.05rem; }
|
||||
|
||||
/* ===== Work Spot Cost ===== */
|
||||
.workspot-card {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.1fr;
|
||||
gap: 22px;
|
||||
padding: 24px;
|
||||
background: var(--bg-card);
|
||||
border: var(--border-glass);
|
||||
border-radius: var(--radius-xl);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
.workspot-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
.workspot-controls input[type=range],
|
||||
.visa-stay-form input[type=range] {
|
||||
width: 100%;
|
||||
accent-color: var(--accent-3);
|
||||
}
|
||||
.workspot-modes {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
.workspot-mode {
|
||||
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;
|
||||
}
|
||||
.workspot-mode:hover,
|
||||
.workspot-mode.active {
|
||||
border-color: rgba(78, 205, 196, 0.4);
|
||||
background: rgba(78, 205, 196, 0.1);
|
||||
}
|
||||
.workspot-result {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
.workspot-hero {
|
||||
padding: 18px;
|
||||
border-radius: var(--radius-md);
|
||||
background: linear-gradient(135deg, rgba(78,205,196,0.12), rgba(255,107,107,0.08));
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
}
|
||||
.workspot-hero small { color: var(--text-muted); display: block; }
|
||||
.workspot-hero strong {
|
||||
font-family: var(--font-display);
|
||||
font-size: 2.2rem;
|
||||
display: block;
|
||||
margin: 4px 0 6px;
|
||||
}
|
||||
.workspot-hero > span { font-weight: 600; font-size: 0.92rem; }
|
||||
.workspot-compare {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
.workspot-compare > div {
|
||||
padding: 12px;
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(255,255,255,0.03);
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.workspot-compare > div.on {
|
||||
border-color: rgba(78, 205, 196, 0.4);
|
||||
background: rgba(78, 205, 196, 0.1);
|
||||
}
|
||||
.workspot-compare small { display: block; color: var(--text-muted); font-size: 0.75rem; }
|
||||
.workspot-compare strong { display: block; margin: 4px 0; font-size: 1.05rem; }
|
||||
.workspot-compare em {
|
||||
font-style: normal;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.35;
|
||||
}
|
||||
.workspot-tip {
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.visa-stay-card,
|
||||
.workspot-card { grid-template-columns: 1fr; }
|
||||
.workspot-compare { grid-template-columns: 1fr; }
|
||||
.visa-stay-stats { 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 = ["daylight", "bag-weight", "runway", "meetings", "trip", "focus"];
|
||||
const FEATURED = ["visa-stay", "workspot", "runway", "daylight", "meetings", "bag-weight"];
|
||||
|
||||
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) => (
|
||||
|
||||
@ -44,11 +44,13 @@ const CurrencyConverter = lazy(() => import("./CurrencyConverter"));
|
||||
const MeetingWindow = lazy(() => import("./MeetingWindow"));
|
||||
const DaylightWorkHours = lazy(() => import("./DaylightWorkHours"));
|
||||
const WifiWorkScore = lazy(() => import("./WifiWorkScore"));
|
||||
const WorkSpotCost = lazy(() => import("./WorkSpotCost"));
|
||||
const FocusTimer = lazy(() => import("./FocusTimer"));
|
||||
const NomadScore = lazy(() => import("./NomadScore"));
|
||||
const Lifestyle = lazy(() => import("./Lifestyle"));
|
||||
const Charts = lazy(() => import("./Charts"));
|
||||
const VisaSection = lazy(() => import("./VisaSection"));
|
||||
const VisaStayCountdown = lazy(() => import("./VisaStayCountdown"));
|
||||
const InsuranceGuide = lazy(() => import("./InsuranceGuide"));
|
||||
const EmergencyContacts = lazy(() => import("./EmergencyContacts"));
|
||||
const AtmCashGuide = lazy(() => import("./AtmCashGuide"));
|
||||
@ -177,11 +179,13 @@ export default function HomeClient(props: Props) {
|
||||
<MeetingWindow destinations={props.destinations} />
|
||||
<DaylightWorkHours destinations={props.destinations} />
|
||||
<WifiWorkScore destinations={props.destinations} />
|
||||
<WorkSpotCost destinations={props.destinations} />
|
||||
<FocusTimer />
|
||||
<NomadScore />
|
||||
<Lifestyle />
|
||||
<Charts {...props.charts} />
|
||||
<VisaSection visas={props.visas} />
|
||||
<VisaStayCountdown destinations={props.destinations} />
|
||||
<InsuranceGuide />
|
||||
<EmergencyContacts destinations={props.destinations} />
|
||||
<AtmCashGuide destinations={props.destinations} />
|
||||
|
||||
@ -3,12 +3,12 @@
|
||||
import Link from "next/link";
|
||||
|
||||
const QUICK = [
|
||||
{ href: "/#visa-stay", emoji: "🛂", label: "签证" },
|
||||
{ href: "/#workspot", emoji: "💼", label: "办公" },
|
||||
{ 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: "/#focus", emoji: "⏱️", label: "专注" },
|
||||
{ href: "/tools", emoji: "🛠️", label: "全部" },
|
||||
];
|
||||
|
||||
143
frontend/src/components/VisaStayCountdown.tsx
Normal file
143
frontend/src/components/VisaStayCountdown.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import type { Destination } from "@/lib/types";
|
||||
|
||||
const STORAGE_KEY = "nomadro-visa-stay";
|
||||
|
||||
const PRESETS: Record<string, { days: number; label: string }> = {
|
||||
chiangmai: { days: 60, label: "免签/落地签常见 60 天" },
|
||||
bali: { days: 30, label: "落地签常见 30 天" },
|
||||
lisbon: { days: 90, label: "申根短签常见 90 天" },
|
||||
barcelona: { days: 90, label: "申根短签常见 90 天" },
|
||||
mexico: { days: 180, label: "常见最长约 180 天" },
|
||||
tokyo: { days: 90, label: "短期停留常见 90 天" },
|
||||
};
|
||||
|
||||
interface Saved {
|
||||
slug: string;
|
||||
entry: string;
|
||||
days: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
destinations: Destination[];
|
||||
}
|
||||
|
||||
function daysBetween(a: string, b: string): number {
|
||||
const t0 = new Date(a + "T00:00:00").getTime();
|
||||
const t1 = new Date(b + "T00:00:00").getTime();
|
||||
return Math.floor((t1 - t0) / 86400000);
|
||||
}
|
||||
|
||||
export default function VisaStayCountdown({ destinations }: Props) {
|
||||
const [slug, setSlug] = useState(destinations[0]?.slug || "chiangmai");
|
||||
const [entry, setEntry] = useState(() => new Date().toISOString().slice(0, 10));
|
||||
const [days, setDays] = useState(60);
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) {
|
||||
const s = JSON.parse(raw) as Saved;
|
||||
if (s.slug) setSlug(s.slug);
|
||||
if (s.entry) setEntry(s.entry);
|
||||
if (s.days) setDays(s.days);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
setReady(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ready) return;
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({ slug, entry, days }));
|
||||
}, [slug, entry, days, ready]);
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const used = Math.max(0, daysBetween(entry, today));
|
||||
const left = days - used;
|
||||
const exitDate = useMemo(() => {
|
||||
const d = new Date(entry + "T00:00:00");
|
||||
d.setDate(d.getDate() + days - 1);
|
||||
return d.toISOString().slice(0, 10);
|
||||
}, [entry, days]);
|
||||
|
||||
const pct = Math.min(Math.max((used / Math.max(days, 1)) * 100, 0), 100);
|
||||
const risk =
|
||||
left <= 0 ? { label: "已到期/超期", color: "#FF6B6B", tip: "请立即确认离境或延期,勿依赖本工具作法律依据" } :
|
||||
left <= 7 ? { label: "紧急窗口", color: "#FF6B6B", tip: "一周内到期,优先订机票与查政策" } :
|
||||
left <= 21 ? { label: "临近到期", color: "#FFE66D", tip: "开始准备下一站或续签材料" } :
|
||||
{ label: "时间充裕", color: "#4ECDC4", tip: "安心旅居,记得关注政策变动" };
|
||||
|
||||
const dest = destinations.find((d) => d.slug === slug);
|
||||
const applyPreset = (s: string) => {
|
||||
setSlug(s);
|
||||
const p = PRESETS[s];
|
||||
if (p) setDays(p.days);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="section visa-stay-section" id="visa-stay">
|
||||
<div className="container">
|
||||
<div className="section-header reveal">
|
||||
<span className="section-tag">🛂 VISA STAY</span>
|
||||
<h2>签证停留倒计时</h2>
|
||||
<p>记录入境日与允许天数,剩余停留一目了然(仅供参考)</p>
|
||||
</div>
|
||||
|
||||
<div className="visa-stay-card reveal">
|
||||
<div className="visa-stay-meter">
|
||||
<div
|
||||
className="visa-stay-ring"
|
||||
style={{ background: `conic-gradient(${risk.color} ${pct}%, rgba(255,255,255,0.08) 0)` }}
|
||||
>
|
||||
<div className="visa-stay-ring-inner">
|
||||
<strong style={{ color: risk.color }}>{left}</strong>
|
||||
<small>天剩余</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="visa-stay-level" style={{ color: risk.color }}>
|
||||
<span>{risk.label}</span>
|
||||
<p>{risk.tip}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="visa-stay-form">
|
||||
<label>
|
||||
<span>目的地</span>
|
||||
<select value={slug} onChange={(e) => applyPreset(e.target.value)}>
|
||||
{destinations.map((d) => (
|
||||
<option key={d.slug} value={d.slug}>{d.emoji} {d.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>入境日期</span>
|
||||
<input type="date" value={entry} onChange={(e) => setEntry(e.target.value)} />
|
||||
</label>
|
||||
<label>
|
||||
<span>允许停留(天)</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={365}
|
||||
value={days}
|
||||
onChange={(e) => setDays(Math.max(1, Math.min(365, +e.target.value || 1)))}
|
||||
/>
|
||||
</label>
|
||||
{PRESETS[slug] && (
|
||||
<p className="visa-stay-hint">{dest?.emoji} {PRESETS[slug].label}</p>
|
||||
)}
|
||||
|
||||
<div className="visa-stay-stats">
|
||||
<div><small>已停留</small><strong>{used} 天</strong></div>
|
||||
<div><small>额度</small><strong>{days} 天</strong></div>
|
||||
<div><small>建议离境前</small><strong>{exitDate}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
139
frontend/src/components/WorkSpotCost.tsx
Normal file
139
frontend/src/components/WorkSpotCost.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import type { Destination } from "@/lib/types";
|
||||
|
||||
/** Rough monthly coworking vs cafe day-pass economics (CNY). */
|
||||
const CITY_COST: Record<string, { cowork: number; cafeDay: number; transitDay: number }> = {
|
||||
chiangmai: { cowork: 900, cafeDay: 35, transitDay: 15 },
|
||||
bali: { cowork: 1200, cafeDay: 45, transitDay: 25 },
|
||||
lisbon: { cowork: 1800, cafeDay: 55, transitDay: 30 },
|
||||
barcelona: { cowork: 2000, cafeDay: 60, transitDay: 35 },
|
||||
mexico: { cowork: 1400, cafeDay: 40, transitDay: 20 },
|
||||
tokyo: { cowork: 2800, cafeDay: 70, transitDay: 40 },
|
||||
};
|
||||
|
||||
interface Props {
|
||||
destinations: Destination[];
|
||||
}
|
||||
|
||||
export default function WorkSpotCost({ destinations }: Props) {
|
||||
const [slug, setSlug] = useState(destinations[0]?.slug || "chiangmai");
|
||||
const [days, setDays] = useState(20);
|
||||
const [mode, setMode] = useState<"cowork" | "cafe" | "mix">("mix");
|
||||
const [mixCoworkDays, setMixCoworkDays] = useState(12);
|
||||
|
||||
const base = CITY_COST[slug] || { cowork: 1500, cafeDay: 45, transitDay: 20 };
|
||||
const dest = destinations.find((d) => d.slug === slug);
|
||||
|
||||
const result = useMemo(() => {
|
||||
const cafeOnly = days * (base.cafeDay + base.transitDay);
|
||||
const coworkOnly = base.cowork;
|
||||
const mixDays = Math.min(mixCoworkDays, days);
|
||||
const cafeDays = Math.max(days - mixDays, 0);
|
||||
const mix = base.cowork + cafeDays * (base.cafeDay + base.transitDay);
|
||||
|
||||
const pick = mode === "cowork" ? coworkOnly : mode === "cafe" ? cafeOnly : mix;
|
||||
const winner =
|
||||
Math.min(coworkOnly, cafeOnly, mix) === coworkOnly ? "联合办公月卡" :
|
||||
Math.min(coworkOnly, cafeOnly, mix) === cafeOnly ? "咖啡馆日刷" :
|
||||
"混合策略";
|
||||
|
||||
return { cafeOnly, coworkOnly, mix, pick, winner };
|
||||
}, [base, days, mode, mixCoworkDays]);
|
||||
|
||||
const saveVsCafe = result.cafeOnly - result.pick;
|
||||
const saveColor = saveVsCafe >= 0 ? "#4ECDC4" : "#FF6B6B";
|
||||
|
||||
return (
|
||||
<section className="section workspot-section" id="workspot">
|
||||
<div className="container">
|
||||
<div className="section-header reveal">
|
||||
<span className="section-tag">💼 WORK SPOT</span>
|
||||
<h2>办公点成本对比</h2>
|
||||
<p>联合办公月卡 vs 咖啡馆日刷,算出哪种更划算</p>
|
||||
</div>
|
||||
|
||||
<div className="workspot-card reveal">
|
||||
<div className="workspot-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>
|
||||
<label>
|
||||
<span>每月外出办公天数:{days}</span>
|
||||
<input type="range" min={4} max={28} value={days} onChange={(e) => setDays(+e.target.value)} />
|
||||
</label>
|
||||
<div className="workspot-modes">
|
||||
{([
|
||||
["cowork", "🏢 月卡"],
|
||||
["cafe", "☕ 咖啡馆"],
|
||||
["mix", "🔀 混合"],
|
||||
] as const).map(([k, label]) => (
|
||||
<button
|
||||
key={k}
|
||||
type="button"
|
||||
className={`workspot-mode${mode === k ? " active" : ""}`}
|
||||
onClick={() => setMode(k)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{mode === "mix" && (
|
||||
<label>
|
||||
<span>混合:月卡覆盖天数 {mixCoworkDays}</span>
|
||||
<input
|
||||
type="range"
|
||||
min={4}
|
||||
max={days}
|
||||
value={Math.min(mixCoworkDays, days)}
|
||||
onChange={(e) => setMixCoworkDays(+e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="workspot-result">
|
||||
<div className="workspot-hero">
|
||||
<small>{dest?.emoji} 本月估算</small>
|
||||
<strong>¥{Math.round(result.pick).toLocaleString()}</strong>
|
||||
<span style={{ color: saveColor }}>
|
||||
{saveVsCafe >= 0
|
||||
? `比纯咖啡馆省 ¥${Math.round(saveVsCafe).toLocaleString()}`
|
||||
: `比纯咖啡馆多花 ¥${Math.round(-saveVsCafe).toLocaleString()}`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="workspot-compare">
|
||||
<div className={mode === "cowork" ? "on" : ""}>
|
||||
<small>联合办公月卡</small>
|
||||
<strong>¥{base.cowork.toLocaleString()}</strong>
|
||||
<em>含工位/网速/打印</em>
|
||||
</div>
|
||||
<div className={mode === "cafe" ? "on" : ""}>
|
||||
<small>咖啡馆×{days}天</small>
|
||||
<strong>¥{Math.round(result.cafeOnly).toLocaleString()}</strong>
|
||||
<em>含饮品+往返约 ¥{base.cafeDay + base.transitDay}/天</em>
|
||||
</div>
|
||||
<div className={mode === "mix" ? "on" : ""}>
|
||||
<small>混合方案</small>
|
||||
<strong>¥{Math.round(result.mix).toLocaleString()}</strong>
|
||||
<em>月卡 + 剩余咖啡馆</em>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="workspot-tip">
|
||||
建议优先:<strong>{result.winner}</strong>
|
||||
· 参考价会随街区与旺季浮动,落地后再微调
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@ -25,10 +25,12 @@ export const TOOL_LINKS: ToolLink[] = [
|
||||
{ id: "meetings", emoji: "🤝", title: "会议黄金时段", desc: "对齐可开会窗口", href: "/#meetings", category: "work" },
|
||||
{ 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: "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" },
|
||||
{ id: "visa", emoji: "📋", title: "签证指南", desc: "政策与智能匹配", href: "/#visa", category: "life" },
|
||||
{ id: "visa-stay", emoji: "🛂", title: "停留倒计时", desc: "签证剩余天数提醒", href: "/#visa-stay", category: "life" },
|
||||
{ id: "housing", emoji: "🏡", title: "住宿指南", desc: "Coliving / 公寓方案", href: "/#housing", category: "life" },
|
||||
{ id: "sim", emoji: "📱", title: "上网方案", desc: "SIM / eSIM 推荐", href: "/#sim", category: "life" },
|
||||
{ id: "plugs", emoji: "🔌", title: "电源插座", desc: "电压与插头类型", href: "/#plugs", category: "life" },
|
||||
|
||||
Loading…
Reference in New Issue
Block a user