Add tools hub, changelog, ATM guide, and slim section nav
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
04f8c14ed5
commit
4ab2d7e412
@ -127,6 +127,9 @@ docker compose up -d
|
|||||||
| 🆘 紧急联系 | 急救、医院、领事馆速查 |
|
| 🆘 紧急联系 | 急救、医院、领事馆速查 |
|
||||||
| 📝 城市笔记 | 按城本地记录避坑与灵感 |
|
| 📝 城市笔记 | 按城本地记录避坑与灵感 |
|
||||||
| ⏱️ 专注时钟 | 番茄钟深度工作计时 |
|
| ⏱️ 专注时钟 | 番茄钟深度工作计时 |
|
||||||
|
| 🛠️ 工具箱页 | `/tools` 分类搜索全部工具 |
|
||||||
|
| 📜 更新日志 | `/changelog` 迭代记录 |
|
||||||
|
| 🏧 取现避坑 | ATM / DCC / 换汇建议 |
|
||||||
| 📝 博客 | 标签筛选 + 阅读进度 + 相关推荐 + 分享 |
|
| 📝 博客 | 标签筛选 + 阅读进度 + 相关推荐 + 分享 |
|
||||||
| ❓ FAQ | 搜索 + 展开/收起全部 |
|
| ❓ FAQ | 搜索 + 展开/收起全部 |
|
||||||
|
|
||||||
|
|||||||
76
frontend/src/app/changelog/page.tsx
Normal file
76
frontend/src/app/changelog/page.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import SiteShell from "@/components/SiteShell";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "更新日志 · nomadro",
|
||||||
|
description: "nomadro 产品更新与功能迭代记录",
|
||||||
|
};
|
||||||
|
|
||||||
|
const LOGS = [
|
||||||
|
{
|
||||||
|
date: "2026-08-29",
|
||||||
|
tag: "工具箱",
|
||||||
|
items: [
|
||||||
|
"新增 /tools 工具聚合页,按分类快速跳转",
|
||||||
|
"精简首页章节导航,核心入口更清晰",
|
||||||
|
"新增 ATM 取现避坑指南",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "2026-08-29",
|
||||||
|
tag: "实用工具",
|
||||||
|
items: [
|
||||||
|
"电源插座、紧急联系、城市笔记、专注时钟",
|
||||||
|
"税居天数、到站清单、咖啡馆推荐、新手引导",
|
||||||
|
"保险指南、支出账本、SIM/eSIM 方案",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "2026-08-28",
|
||||||
|
tag: "品牌",
|
||||||
|
items: [
|
||||||
|
"品牌统一为 nomadro",
|
||||||
|
"命运转盘、机票估算、活动日历、隐私政策",
|
||||||
|
"签证智能匹配、联合办公、行李清单",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ChangelogPage() {
|
||||||
|
return (
|
||||||
|
<SiteShell showFooter>
|
||||||
|
<div className="legal-page">
|
||||||
|
<nav className="detail-nav">
|
||||||
|
<Link href="/">← 返回首页</Link>
|
||||||
|
</nav>
|
||||||
|
<article className="legal-content">
|
||||||
|
<span className="section-tag">📜 CHANGELOG</span>
|
||||||
|
<h1>更新日志</h1>
|
||||||
|
<p className="legal-updated">记录 nomadro 的每一次出发</p>
|
||||||
|
|
||||||
|
<div className="changelog-list">
|
||||||
|
{LOGS.map((log) => (
|
||||||
|
<section key={log.date + log.tag} className="changelog-item">
|
||||||
|
<div className="changelog-meta">
|
||||||
|
<time>{log.date}</time>
|
||||||
|
<span className="changelog-tag">{log.tag}</span>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
{log.items.map((item) => (
|
||||||
|
<li key={item}>{item}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="about-actions">
|
||||||
|
<Link href="/tools" className="btn btn-primary">打开工具箱 🛠️</Link>
|
||||||
|
<Link href="/about" className="btn btn-ghost">关于 nomadro</Link>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</SiteShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -7327,3 +7327,158 @@ img { max-width: 100%; display: block; }
|
|||||||
}
|
}
|
||||||
.notes-count { width: 100%; margin-top: 4px; }
|
.notes-count { width: 100%; margin-top: 4px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Tools Hub ===== */
|
||||||
|
.tools-hub-page {
|
||||||
|
padding: calc(var(--nav-height) + 40px) 0 80px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-hub-page .section-header h1 {
|
||||||
|
font-size: clamp(1.8rem, 4vw, 2.4rem);
|
||||||
|
margin: 12px 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-hub-toolbar {
|
||||||
|
margin: 28px auto 32px;
|
||||||
|
max-width: 900px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-hub-search {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 18px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: var(--border-glass);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-hub-search:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: rgba(78, 205, 196, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-hub-filters {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-hub-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-hub-card {
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: var(--border-glass);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
transition: transform 0.25s, border-color 0.25s;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-hub-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
border-color: rgba(78, 205, 196, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-hub-emoji { font-size: 1.8rem; margin-bottom: 8px; }
|
||||||
|
.tools-hub-card h3 { font-size: 1.05rem; margin-bottom: 6px; }
|
||||||
|
.tools-hub-card p {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.45;
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-hub-go {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--accent-3);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-nav-tools {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
border: 1px dashed rgba(78, 205, 196, 0.35) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== ATM Guide ===== */
|
||||||
|
.atm-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.atm-card {
|
||||||
|
padding: 22px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: var(--border-glass);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.atm-card:hover { transform: translateY(-3px); }
|
||||||
|
.atm-emoji { font-size: 1.8rem; display: block; margin-bottom: 8px; }
|
||||||
|
.atm-card h4 { font-size: 1.05rem; margin-bottom: 8px; }
|
||||||
|
.atm-card p { font-size: 0.85rem; color: var(--text-secondary); line-height: 1.5; }
|
||||||
|
|
||||||
|
/* ===== Changelog ===== */
|
||||||
|
.changelog-list { margin: 28px 0; }
|
||||||
|
|
||||||
|
.changelog-item {
|
||||||
|
padding: 20px 0;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-meta time {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-tag {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 3px 10px;
|
||||||
|
background: rgba(78, 205, 196, 0.12);
|
||||||
|
color: var(--accent-3);
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-item ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-item li {
|
||||||
|
position: relative;
|
||||||
|
padding: 6px 0 6px 16px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-item li::before {
|
||||||
|
content: "•";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: var(--accent-3);
|
||||||
|
}
|
||||||
|
|||||||
@ -12,6 +12,8 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
|||||||
{ url: `${SITE}/login`, lastModified: now, changeFrequency: "monthly", priority: 0.5 },
|
{ url: `${SITE}/login`, lastModified: now, changeFrequency: "monthly", priority: 0.5 },
|
||||||
{ url: `${SITE}/privacy`, lastModified: now, changeFrequency: "yearly", priority: 0.3 },
|
{ url: `${SITE}/privacy`, lastModified: now, changeFrequency: "yearly", priority: 0.3 },
|
||||||
{ url: `${SITE}/about`, lastModified: now, changeFrequency: "monthly", priority: 0.5 },
|
{ url: `${SITE}/about`, lastModified: now, changeFrequency: "monthly", priority: 0.5 },
|
||||||
|
{ url: `${SITE}/tools`, lastModified: now, changeFrequency: "weekly", priority: 0.8 },
|
||||||
|
{ url: `${SITE}/changelog`, lastModified: now, changeFrequency: "weekly", priority: 0.4 },
|
||||||
{ url: `${SITE}/profile`, lastModified: now, changeFrequency: "monthly", priority: 0.4 },
|
{ url: `${SITE}/profile`, lastModified: now, changeFrequency: "monthly", priority: 0.4 },
|
||||||
...DESTINATIONS.map((slug) => ({
|
...DESTINATIONS.map((slug) => ({
|
||||||
url: `${SITE}/destinations/${slug}`,
|
url: `${SITE}/destinations/${slug}`,
|
||||||
|
|||||||
71
frontend/src/app/tools/page.tsx
Normal file
71
frontend/src/app/tools/page.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { TOOL_CATEGORIES, TOOL_LINKS } from "@/lib/tools";
|
||||||
|
import SiteShell from "@/components/SiteShell";
|
||||||
|
|
||||||
|
export default function ToolsHubPage() {
|
||||||
|
const [filter, setFilter] = useState("all");
|
||||||
|
const [q, setQ] = useState("");
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
let list = filter === "all" ? TOOL_LINKS : TOOL_LINKS.filter((t) => t.category === filter);
|
||||||
|
if (q.trim()) {
|
||||||
|
const s = q.toLowerCase();
|
||||||
|
list = list.filter((t) => t.title.toLowerCase().includes(s) || t.desc.toLowerCase().includes(s));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}, [filter, q]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SiteShell showFooter>
|
||||||
|
<div className="tools-hub-page">
|
||||||
|
<div className="container">
|
||||||
|
<nav className="detail-nav">
|
||||||
|
<Link href="/">← 返回首页</Link>
|
||||||
|
</nav>
|
||||||
|
<div className="section-header">
|
||||||
|
<span className="section-tag">🛠️ TOOLKIT</span>
|
||||||
|
<h1>nomadro 工具箱</h1>
|
||||||
|
<p>规划、金钱、工作、生活与安全——一站直达所有旅居工具</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="tools-hub-toolbar">
|
||||||
|
<input
|
||||||
|
className="tools-hub-search"
|
||||||
|
placeholder="搜索工具,如「签证」「机票」「笔记」…"
|
||||||
|
value={q}
|
||||||
|
onChange={(e) => setQ(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="tools-hub-filters">
|
||||||
|
{TOOL_CATEGORIES.map((c) => (
|
||||||
|
<button
|
||||||
|
key={c.key}
|
||||||
|
className={`filter-btn${filter === c.key ? " active" : ""}`}
|
||||||
|
onClick={() => setFilter(c.key)}
|
||||||
|
>
|
||||||
|
{c.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="tools-hub-grid">
|
||||||
|
{filtered.map((t) => (
|
||||||
|
<Link key={t.id} href={t.href} className="tools-hub-card">
|
||||||
|
<span className="tools-hub-emoji">{t.emoji}</span>
|
||||||
|
<h3>{t.title}</h3>
|
||||||
|
<p>{t.desc}</p>
|
||||||
|
<span className="tools-hub-go">打开 →</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{filtered.length === 0 && (
|
||||||
|
<p className="dest-empty">没有匹配的工具,试试其他关键词</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SiteShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
89
frontend/src/components/AtmCashGuide.tsx
Normal file
89
frontend/src/components/AtmCashGuide.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import type { Destination } from "@/lib/types";
|
||||||
|
|
||||||
|
interface AtmTip {
|
||||||
|
emoji: string;
|
||||||
|
title: string;
|
||||||
|
detail: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TIPS: Record<string, AtmTip[]> = {
|
||||||
|
bali: [
|
||||||
|
{ emoji: "🏦", title: "优先银行 ATM", detail: "BCA / Mandiri 手续费相对透明,避开独立小黄机。" },
|
||||||
|
{ emoji: "💳", title: "Wise / 实体卡", detail: "尽量用无外汇手续费卡,按需取现。" },
|
||||||
|
{ emoji: "💵", title: "别一次取太多", detail: "现金够用即可,酒店与餐厅多数可刷卡。" },
|
||||||
|
],
|
||||||
|
chiangmai: [
|
||||||
|
{ emoji: "🏧", title: "Aeon / 银行柜员机", detail: "注意屏幕提示的「动态汇率」选项,尽量选拒绝。" },
|
||||||
|
{ emoji: "💱", title: "拒绝 DCC", detail: "出现「用人民币结算?」选 No,用泰铢记账更划算。" },
|
||||||
|
{ emoji: "🧾", title: "保留小票", detail: "便于核对手续费与异常扣款。" },
|
||||||
|
],
|
||||||
|
lisbon: [
|
||||||
|
{ emoji: "🇪🇺", title: "欧元区友好", detail: "多数 ATM 支持国际卡,留意 Multibanco 标识。" },
|
||||||
|
{ emoji: "🚫", title: "拒绝动态货币转换", detail: "选以欧元扣款,避免银行额外加点。" },
|
||||||
|
{ emoji: "🪙", title: "小面额现金", detail: "市场与有轨电车偶尔需要硬币。" },
|
||||||
|
],
|
||||||
|
barcelona: [
|
||||||
|
{ emoji: "🏧", title: "银行网点 ATM", detail: "CaixaBank / Santander 更稳,避开街头独立机。" },
|
||||||
|
{ emoji: "🚫", title: "拒 DCC", detail: "选欧元结算,别让机器帮你换汇。" },
|
||||||
|
{ emoji: "👛", title: "现金备用量", detail: "堂食可刷卡,小摊与夜市准备现金。" },
|
||||||
|
],
|
||||||
|
mexico: [
|
||||||
|
{ emoji: "🏦", title: "BBVA / Santander", detail: "大行 ATM 更安全,白天取现。" },
|
||||||
|
{ emoji: "⚠️", title: "警惕侧录", detail: "检查插卡口是否松动,优先商场内机器。" },
|
||||||
|
{ emoji: "💵", title: "小费文化", detail: "预留小额比索现金给餐厅与快递。" },
|
||||||
|
],
|
||||||
|
tokyo: [
|
||||||
|
{ emoji: "🏪", title: "7-Eleven ATM", detail: "对国际卡最友好,便利店随处可取。" },
|
||||||
|
{ emoji: "💴", title: "日元现金仍重要", detail: "部分居酒屋与神社只收现金。" },
|
||||||
|
{ emoji: "💱", title: "少在机场换汇", detail: "汇率通常较差,落地后便利店 ATM 更优。" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
destinations: Destination[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AtmCashGuide({ destinations }: Props) {
|
||||||
|
const [slug, setSlug] = useState(destinations[0]?.slug || "chiangmai");
|
||||||
|
const tips = useMemo(() => TIPS[slug] || TIPS.chiangmai, [slug]);
|
||||||
|
const dest = destinations.find((d) => d.slug === slug);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="section atm-section" id="atm">
|
||||||
|
<div className="container">
|
||||||
|
<div className="section-header reveal">
|
||||||
|
<span className="section-tag">🏧 ATM</span>
|
||||||
|
<h2>取现与换汇避坑</h2>
|
||||||
|
<p>拒 DCC、选对机器,少交「智商税」</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="housing-cities reveal">
|
||||||
|
{destinations.map((d) => (
|
||||||
|
<button
|
||||||
|
key={d.slug}
|
||||||
|
className={`season-city-btn${slug === d.slug ? " active" : ""}`}
|
||||||
|
onClick={() => setSlug(d.slug)}
|
||||||
|
>
|
||||||
|
{d.emoji} {d.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="sim-city-title reveal">{dest?.emoji} {dest?.name}</h3>
|
||||||
|
|
||||||
|
<div className="atm-grid">
|
||||||
|
{tips.map((t, i) => (
|
||||||
|
<div key={t.title} className="atm-card reveal" style={{ transitionDelay: `${i * 0.08}s` }}>
|
||||||
|
<span className="atm-emoji">{t.emoji}</span>
|
||||||
|
<h4>{t.title}</h4>
|
||||||
|
<p>{t.detail}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -40,28 +40,24 @@ export default function Footer() {
|
|||||||
<div className="footer-links">
|
<div className="footer-links">
|
||||||
<h4>探索</h4>
|
<h4>探索</h4>
|
||||||
<Link href="/#destinations">目的地</Link>
|
<Link href="/#destinations">目的地</Link>
|
||||||
<Link href="/#spin">命运转盘</Link>
|
<Link href="/#visa">签证指南</Link>
|
||||||
<Link href="/#wifi">网速评级</Link>
|
<Link href="/#events">活动日历</Link>
|
||||||
<Link href="/#cafes">咖啡馆</Link>
|
|
||||||
<Link href="/#emergency">紧急联系</Link>
|
|
||||||
<Link href="/#plugs">电源插座</Link>
|
|
||||||
<Link href="/#notes">城市笔记</Link>
|
|
||||||
<Link href="/#blog">博客</Link>
|
<Link href="/#blog">博客</Link>
|
||||||
|
<Link href="/about">关于 nomadro</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="footer-links">
|
<div className="footer-links">
|
||||||
<h4>工具</h4>
|
<h4>工具</h4>
|
||||||
|
<Link href="/tools">全部工具箱</Link>
|
||||||
<Link href="/#trip">行程规划</Link>
|
<Link href="/#trip">行程规划</Link>
|
||||||
<Link href="/#focus">专注时钟</Link>
|
|
||||||
<Link href="/#expenses">支出账本</Link>
|
|
||||||
<Link href="/#tax-days">税居天数</Link>
|
|
||||||
<Link href="/#calculator">费用计算器</Link>
|
<Link href="/#calculator">费用计算器</Link>
|
||||||
|
<Link href="/#atm">取现避坑</Link>
|
||||||
|
<Link href="/changelog">更新日志</Link>
|
||||||
<a href={`${SITE}/docs`} target="_blank" rel="noopener noreferrer">API 文档</a>
|
<a href={`${SITE}/docs`} target="_blank" rel="noopener noreferrer">API 文档</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="footer-links">
|
<div className="footer-links">
|
||||||
<h4>账户</h4>
|
<h4>账户</h4>
|
||||||
<Link href="/login">登录 / 注册</Link>
|
<Link href="/login">登录 / 注册</Link>
|
||||||
<Link href="/profile">用户中心</Link>
|
<Link href="/profile">用户中心</Link>
|
||||||
<Link href="/about">关于 nomadro</Link>
|
|
||||||
<Link href="/privacy">隐私政策</Link>
|
<Link href="/privacy">隐私政策</Link>
|
||||||
<a href="mailto:hello@nomadro.com">hello@nomadro.com</a>
|
<a href="mailto:hello@nomadro.com">hello@nomadro.com</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -120,6 +120,7 @@ export default function Hero({ stats }: Props) {
|
|||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M5 12h14M12 5l7 7-7 7" /></svg>
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M5 12h14M12 5l7 7-7 7" /></svg>
|
||||||
</a>
|
</a>
|
||||||
<a href="#calculator" className="btn btn-ghost"><span>🧮 费用计算</span></a>
|
<a href="#calculator" className="btn btn-ghost"><span>🧮 费用计算</span></a>
|
||||||
|
<a href="/tools" className="btn btn-ghost"><span>🛠️ 工具箱</span></a>
|
||||||
<button className="btn btn-ghost hero-matcher-btn" onClick={() => {
|
<button className="btn btn-ghost hero-matcher-btn" onClick={() => {
|
||||||
window.dispatchEvent(new CustomEvent("open-matcher"));
|
window.dispatchEvent(new CustomEvent("open-matcher"));
|
||||||
}}><span>🎯 智能匹配</span></button>
|
}}><span>🎯 智能匹配</span></button>
|
||||||
|
|||||||
@ -55,6 +55,7 @@ import PowerPlugGuide from "./PowerPlugGuide";
|
|||||||
import EmergencyContacts from "./EmergencyContacts";
|
import EmergencyContacts from "./EmergencyContacts";
|
||||||
import CityNotes from "./CityNotes";
|
import CityNotes from "./CityNotes";
|
||||||
import FocusTimer from "./FocusTimer";
|
import FocusTimer from "./FocusTimer";
|
||||||
|
import AtmCashGuide from "./AtmCashGuide";
|
||||||
import KeyboardShortcuts from "./KeyboardShortcuts";
|
import KeyboardShortcuts from "./KeyboardShortcuts";
|
||||||
import BackToTop from "./BackToTop";
|
import BackToTop from "./BackToTop";
|
||||||
|
|
||||||
@ -144,6 +145,7 @@ export default function HomeClient(props: Props) {
|
|||||||
<VisaSection visas={props.visas} />
|
<VisaSection visas={props.visas} />
|
||||||
<InsuranceGuide />
|
<InsuranceGuide />
|
||||||
<EmergencyContacts destinations={props.destinations} />
|
<EmergencyContacts destinations={props.destinations} />
|
||||||
|
<AtmCashGuide destinations={props.destinations} />
|
||||||
<CoworkingGuide />
|
<CoworkingGuide />
|
||||||
<HousingGuide destinations={props.destinations} />
|
<HousingGuide destinations={props.destinations} />
|
||||||
<SimGuide destinations={props.destinations} />
|
<SimGuide destinations={props.destinations} />
|
||||||
|
|||||||
@ -9,8 +9,8 @@ const LINKS = [
|
|||||||
{ href: "/#map", label: "🗺️ 地图", section: "map" },
|
{ href: "/#map", label: "🗺️ 地图", section: "map" },
|
||||||
{ href: "/#destinations", label: "🌍 目的地", section: "destinations" },
|
{ href: "/#destinations", label: "🌍 目的地", section: "destinations" },
|
||||||
{ href: "/#trip", label: "🗓️ 行程", section: "trip" },
|
{ href: "/#trip", label: "🗓️ 行程", section: "trip" },
|
||||||
{ href: "/#timezone", label: "🕐 时区", section: "timezone" },
|
|
||||||
{ href: "/#calculator", label: "🧮 计算器", section: "calculator" },
|
{ href: "/#calculator", label: "🧮 计算器", section: "calculator" },
|
||||||
|
{ href: "/tools", label: "🛠️ 工具箱", section: "tools" },
|
||||||
{ href: "/#visa", label: "📋 签证", section: "visa" },
|
{ href: "/#visa", label: "📋 签证", section: "visa" },
|
||||||
{ href: "/#blog", label: "📝 博客", section: "blog" },
|
{ href: "/#blog", label: "📝 博客", section: "blog" },
|
||||||
];
|
];
|
||||||
@ -41,7 +41,8 @@ export default function Navbar() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isHome) return;
|
if (!isHome) return;
|
||||||
const sections = LINKS.map((l) => document.getElementById(l.section)).filter(Boolean);
|
const homeLinks = LINKS.filter((l) => l.href.startsWith("/#"));
|
||||||
|
const sections = homeLinks.map((l) => document.getElementById(l.section)).filter(Boolean);
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
(entries) => entries.forEach((e) => { if (e.isIntersecting) setActive(e.target.id); }),
|
(entries) => entries.forEach((e) => { if (e.isIntersecting) setActive(e.target.id); }),
|
||||||
{ threshold: 0.3, rootMargin: "-80px 0px -50% 0px" }
|
{ threshold: 0.3, rootMargin: "-80px 0px -50% 0px" }
|
||||||
@ -84,7 +85,11 @@ export default function Navbar() {
|
|||||||
<nav className={`nav-links${menuOpen ? " open" : ""}`}>
|
<nav className={`nav-links${menuOpen ? " open" : ""}`}>
|
||||||
{LINKS.map((l) => (
|
{LINKS.map((l) => (
|
||||||
<Link key={l.section} href={l.href} data-section={l.section}
|
<Link key={l.section} href={l.href} data-section={l.section}
|
||||||
className={isHome && active === l.section ? "active" : ""}
|
className={
|
||||||
|
l.href === "/tools"
|
||||||
|
? (pathname === "/tools" ? "active" : "")
|
||||||
|
: (isHome && active === l.section ? "active" : "")
|
||||||
|
}
|
||||||
onClick={() => setMenuOpen(false)}>{l.label}</Link>
|
onClick={() => setMenuOpen(false)}>{l.label}</Link>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@ -1,36 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
const SECTIONS = [
|
const PRIMARY = [
|
||||||
{ id: "map", emoji: "🗺️", label: "地图" },
|
{ id: "map", emoji: "🗺️", label: "地图" },
|
||||||
{ id: "destinations", emoji: "🌍", label: "目的地" },
|
{ id: "destinations", emoji: "🌍", label: "目的地" },
|
||||||
{ id: "spin", emoji: "🎲", label: "转盘" },
|
|
||||||
{ id: "timezone", emoji: "🕐", label: "时区" },
|
|
||||||
{ id: "jetlag", emoji: "😴", label: "时差" },
|
|
||||||
{ id: "trip", emoji: "🗓️", label: "行程" },
|
{ id: "trip", emoji: "🗓️", label: "行程" },
|
||||||
{ id: "calculator", emoji: "🧮", label: "计算器" },
|
{ id: "calculator", emoji: "🧮", label: "费用" },
|
||||||
{ id: "cost-compare", emoji: "💰", label: "省钱" },
|
|
||||||
{ id: "savings", emoji: "🎯", label: "存款" },
|
|
||||||
{ id: "expenses", emoji: "📒", label: "账本" },
|
|
||||||
{ id: "tax-days", emoji: "🧾", label: "税居" },
|
|
||||||
{ id: "flight", emoji: "✈️", label: "机票" },
|
|
||||||
{ id: "wifi", emoji: "📶", label: "网速" },
|
|
||||||
{ id: "focus", emoji: "⏱️", label: "专注" },
|
|
||||||
{ id: "lifestyle", emoji: "💻", label: "生活" },
|
|
||||||
{ id: "visa", emoji: "📋", label: "签证" },
|
{ id: "visa", emoji: "📋", label: "签证" },
|
||||||
{ id: "insurance", emoji: "🏥", label: "保险" },
|
|
||||||
{ id: "emergency", emoji: "🆘", label: "紧急" },
|
|
||||||
{ id: "coworking", emoji: "💻", label: "办公" },
|
|
||||||
{ id: "housing", emoji: "🏡", label: "住宿" },
|
|
||||||
{ id: "sim", emoji: "📱", label: "上网" },
|
|
||||||
{ id: "plugs", emoji: "🔌", label: "插座" },
|
|
||||||
{ id: "cafes", emoji: "☕", label: "咖啡" },
|
|
||||||
{ id: "season", emoji: "🌤️", label: "季节" },
|
|
||||||
{ id: "packing", emoji: "🧳", label: "行李" },
|
|
||||||
{ id: "arrival", emoji: "🛬", label: "到站" },
|
|
||||||
{ id: "phrases", emoji: "🗣️", label: "短语" },
|
|
||||||
{ id: "notes", emoji: "📝", label: "笔记" },
|
|
||||||
{ id: "events", emoji: "📅", label: "活动" },
|
{ id: "events", emoji: "📅", label: "活动" },
|
||||||
{ id: "blog", emoji: "📝", label: "博客" },
|
{ id: "blog", emoji: "📝", label: "博客" },
|
||||||
];
|
];
|
||||||
@ -47,7 +25,7 @@ export default function SectionNav() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sections = SECTIONS.map((s) => document.getElementById(s.id)).filter(Boolean);
|
const sections = PRIMARY.map((s) => document.getElementById(s.id)).filter(Boolean);
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
(entries) => entries.forEach((e) => { if (e.isIntersecting) setActive(e.target.id); }),
|
(entries) => entries.forEach((e) => { if (e.isIntersecting) setActive(e.target.id); }),
|
||||||
{ threshold: 0.25, rootMargin: "-80px 0px -55% 0px" }
|
{ threshold: 0.25, rootMargin: "-80px 0px -55% 0px" }
|
||||||
@ -62,7 +40,7 @@ export default function SectionNav() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={`section-nav${visible ? " visible" : ""}`} aria-label="章节导航">
|
<nav className={`section-nav${visible ? " visible" : ""}`} aria-label="章节导航">
|
||||||
{SECTIONS.map((s) => (
|
{PRIMARY.map((s) => (
|
||||||
<button
|
<button
|
||||||
key={s.id}
|
key={s.id}
|
||||||
className={`section-nav-dot${active === s.id ? " active" : ""}`}
|
className={`section-nav-dot${active === s.id ? " active" : ""}`}
|
||||||
@ -74,6 +52,10 @@ export default function SectionNav() {
|
|||||||
<span className="section-nav-label">{s.label}</span>
|
<span className="section-nav-label">{s.label}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
<Link href="/tools" className="section-nav-dot section-nav-tools" title="全部工具" aria-label="全部工具">
|
||||||
|
<span className="section-nav-emoji">🛠️</span>
|
||||||
|
<span className="section-nav-label">工具</span>
|
||||||
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
48
frontend/src/lib/tools.ts
Normal file
48
frontend/src/lib/tools.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
export interface ToolLink {
|
||||||
|
id: string;
|
||||||
|
emoji: string;
|
||||||
|
title: string;
|
||||||
|
desc: string;
|
||||||
|
href: string;
|
||||||
|
category: "plan" | "money" | "life" | "work" | "safety";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TOOL_LINKS: ToolLink[] = [
|
||||||
|
{ id: "trip", emoji: "🗓️", title: "行程规划", desc: "多城串联与预算汇总", href: "/#trip", category: "plan" },
|
||||||
|
{ 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: "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" },
|
||||||
|
{ id: "expenses", emoji: "📒", title: "支出账本", desc: "分类记账汇总", href: "/#expenses", category: "money" },
|
||||||
|
{ id: "flight", emoji: "✈️", title: "机票估算", desc: "往返票价粗算", href: "/#flight", category: "money" },
|
||||||
|
{ id: "currency", emoji: "💱", title: "汇率换算", desc: "多币种互转", href: "/#currency", category: "money" },
|
||||||
|
{ id: "tax-days", emoji: "🧾", title: "税居天数", desc: "183 天规则追踪", href: "/#tax-days", category: "money" },
|
||||||
|
{ id: "timezone", emoji: "🕐", title: "时区看板", desc: "工作重叠分析", href: "/#timezone", category: "work" },
|
||||||
|
{ id: "jetlag", emoji: "😴", title: "时差恢复", desc: "恢复期建议", href: "/#jetlag", category: "work" },
|
||||||
|
{ id: "wifi", emoji: "📶", title: "网速评级", desc: "远程办公是否够用", href: "/#wifi", 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: "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" },
|
||||||
|
{ id: "packing", emoji: "🧳", title: "行李清单", desc: "出发前逐项勾选", href: "/#packing", 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" },
|
||||||
|
{ id: "insurance", emoji: "🏥", title: "保险指南", desc: "国际医疗保险对比", href: "/#insurance", category: "safety" },
|
||||||
|
{ id: "emergency", emoji: "🆘", title: "紧急联系", desc: "急救与领事馆", href: "/#emergency", category: "safety" },
|
||||||
|
{ id: "atm", emoji: "🏧", title: "取现避坑", desc: "ATM 与换汇建议", href: "/#atm", category: "safety" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TOOL_CATEGORIES = [
|
||||||
|
{ key: "all", label: "🌏 全部" },
|
||||||
|
{ key: "plan", label: "🗺️ 规划" },
|
||||||
|
{ key: "money", label: "💰 金钱" },
|
||||||
|
{ key: "work", label: "💻 工作" },
|
||||||
|
{ key: "life", label: "🌴 生活" },
|
||||||
|
{ key: "safety", label: "🛡️ 安全" },
|
||||||
|
] as const;
|
||||||
Loading…
Reference in New Issue
Block a user