diff --git a/README.md b/README.md index b7a9019..65f3e24 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,9 @@ docker compose up -d | 🆘 紧急联系 | 急救、医院、领事馆速查 | | 📝 城市笔记 | 按城本地记录避坑与灵感 | | ⏱️ 专注时钟 | 番茄钟深度工作计时 | +| 🛠️ 工具箱页 | `/tools` 分类搜索全部工具 | +| 📜 更新日志 | `/changelog` 迭代记录 | +| 🏧 取现避坑 | ATM / DCC / 换汇建议 | | 📝 博客 | 标签筛选 + 阅读进度 + 相关推荐 + 分享 | | ❓ FAQ | 搜索 + 展开/收起全部 | diff --git a/frontend/src/app/changelog/page.tsx b/frontend/src/app/changelog/page.tsx new file mode 100644 index 0000000..8ed8fcd --- /dev/null +++ b/frontend/src/app/changelog/page.tsx @@ -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 ( + +
+ +
+ 📜 CHANGELOG +

更新日志

+

记录 nomadro 的每一次出发

+ +
+ {LOGS.map((log) => ( +
+
+ + {log.tag} +
+
    + {log.items.map((item) => ( +
  • {item}
  • + ))} +
+
+ ))} +
+ +
+ 打开工具箱 🛠️ + 关于 nomadro +
+
+
+
+ ); +} diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 5d8ce5e..1da0b0a 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -7327,3 +7327,158 @@ img { max-width: 100%; display: block; } } .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); +} diff --git a/frontend/src/app/sitemap.ts b/frontend/src/app/sitemap.ts index 5a08603..c736919 100644 --- a/frontend/src/app/sitemap.ts +++ b/frontend/src/app/sitemap.ts @@ -12,6 +12,8 @@ export default function sitemap(): MetadataRoute.Sitemap { { url: `${SITE}/login`, lastModified: now, changeFrequency: "monthly", priority: 0.5 }, { url: `${SITE}/privacy`, lastModified: now, changeFrequency: "yearly", priority: 0.3 }, { 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 }, ...DESTINATIONS.map((slug) => ({ url: `${SITE}/destinations/${slug}`, diff --git a/frontend/src/app/tools/page.tsx b/frontend/src/app/tools/page.tsx new file mode 100644 index 0000000..1132e94 --- /dev/null +++ b/frontend/src/app/tools/page.tsx @@ -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 ( + +
+
+ +
+ 🛠️ TOOLKIT +

nomadro 工具箱

+

规划、金钱、工作、生活与安全——一站直达所有旅居工具

+
+ +
+ setQ(e.target.value)} + /> +
+ {TOOL_CATEGORIES.map((c) => ( + + ))} +
+
+ +
+ {filtered.map((t) => ( + + {t.emoji} +

{t.title}

+

{t.desc}

+ 打开 → + + ))} +
+ {filtered.length === 0 && ( +

没有匹配的工具,试试其他关键词

+ )} +
+
+
+ ); +} diff --git a/frontend/src/components/AtmCashGuide.tsx b/frontend/src/components/AtmCashGuide.tsx new file mode 100644 index 0000000..be58afa --- /dev/null +++ b/frontend/src/components/AtmCashGuide.tsx @@ -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 = { + 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 ( +
+
+
+ 🏧 ATM +

取现与换汇避坑

+

拒 DCC、选对机器,少交「智商税」

+
+ +
+ {destinations.map((d) => ( + + ))} +
+ +

{dest?.emoji} {dest?.name}

+ +
+ {tips.map((t, i) => ( +
+ {t.emoji} +

{t.title}

+

{t.detail}

+
+ ))} +
+
+
+ ); +} diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx index e114bd6..72a3e23 100644 --- a/frontend/src/components/Footer.tsx +++ b/frontend/src/components/Footer.tsx @@ -40,28 +40,24 @@ export default function Footer() {

探索

目的地 - 命运转盘 - 网速评级 - 咖啡馆 - 紧急联系 - 电源插座 - 城市笔记 + 签证指南 + 活动日历 博客 + 关于 nomadro

工具

+ 全部工具箱 行程规划 - 专注时钟 - 支出账本 - 税居天数 费用计算器 + 取现避坑 + 更新日志 API 文档

账户

登录 / 注册 用户中心 - 关于 nomadro 隐私政策 hello@nomadro.com
diff --git a/frontend/src/components/Hero.tsx b/frontend/src/components/Hero.tsx index ed639f5..54bcc44 100644 --- a/frontend/src/components/Hero.tsx +++ b/frontend/src/components/Hero.tsx @@ -120,6 +120,7 @@ export default function Hero({ stats }: Props) { 🧮 费用计算 + 🛠️ 工具箱 diff --git a/frontend/src/components/HomeClient.tsx b/frontend/src/components/HomeClient.tsx index 1880cc6..b92cfac 100644 --- a/frontend/src/components/HomeClient.tsx +++ b/frontend/src/components/HomeClient.tsx @@ -55,6 +55,7 @@ import PowerPlugGuide from "./PowerPlugGuide"; import EmergencyContacts from "./EmergencyContacts"; import CityNotes from "./CityNotes"; import FocusTimer from "./FocusTimer"; +import AtmCashGuide from "./AtmCashGuide"; import KeyboardShortcuts from "./KeyboardShortcuts"; import BackToTop from "./BackToTop"; @@ -144,6 +145,7 @@ export default function HomeClient(props: Props) { + diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index b43db4d..8f19e3d 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -9,8 +9,8 @@ const LINKS = [ { href: "/#map", label: "🗺️ 地图", section: "map" }, { href: "/#destinations", label: "🌍 目的地", section: "destinations" }, { href: "/#trip", label: "🗓️ 行程", section: "trip" }, - { href: "/#timezone", label: "🕐 时区", section: "timezone" }, { href: "/#calculator", label: "🧮 计算器", section: "calculator" }, + { href: "/tools", label: "🛠️ 工具箱", section: "tools" }, { href: "/#visa", label: "📋 签证", section: "visa" }, { href: "/#blog", label: "📝 博客", section: "blog" }, ]; @@ -41,7 +41,8 @@ export default function Navbar() { useEffect(() => { 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( (entries) => entries.forEach((e) => { if (e.isIntersecting) setActive(e.target.id); }), { threshold: 0.3, rootMargin: "-80px 0px -50% 0px" } @@ -84,7 +85,11 @@ export default function Navbar() { diff --git a/frontend/src/components/SectionNav.tsx b/frontend/src/components/SectionNav.tsx index c25bdf6..59c70ed 100644 --- a/frontend/src/components/SectionNav.tsx +++ b/frontend/src/components/SectionNav.tsx @@ -1,36 +1,14 @@ "use client"; import { useEffect, useState } from "react"; +import Link from "next/link"; -const SECTIONS = [ +const PRIMARY = [ { id: "map", emoji: "🗺️", label: "地图" }, { id: "destinations", emoji: "🌍", label: "目的地" }, - { id: "spin", emoji: "🎲", label: "转盘" }, - { id: "timezone", emoji: "🕐", label: "时区" }, - { id: "jetlag", emoji: "😴", label: "时差" }, { id: "trip", 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: "calculator", 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: "blog", emoji: "📝", label: "博客" }, ]; @@ -47,7 +25,7 @@ export default function SectionNav() { }, []); 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( (entries) => entries.forEach((e) => { if (e.isIntersecting) setActive(e.target.id); }), { threshold: 0.25, rootMargin: "-80px 0px -55% 0px" } @@ -62,7 +40,7 @@ export default function SectionNav() { return ( ); } diff --git a/frontend/src/lib/tools.ts b/frontend/src/lib/tools.ts new file mode 100644 index 0000000..b18bbcd --- /dev/null +++ b/frontend/src/lib/tools.ts @@ -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;