Improve search, tools strip, trip MD export, lazy-load home sections

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
eric 2026-08-29 00:29:30 -05:00
parent 4ab2d7e412
commit 96d3d83906
10 changed files with 338 additions and 48 deletions

View File

@ -130,6 +130,9 @@ docker compose up -d
| 🛠️ 工具箱页 | `/tools` 分类搜索全部工具 | | 🛠️ 工具箱页 | `/tools` 分类搜索全部工具 |
| 📜 更新日志 | `/changelog` 迭代记录 | | 📜 更新日志 | `/changelog` 迭代记录 |
| 🏧 取现避坑 | ATM / DCC / 换汇建议 | | 🏧 取现避坑 | ATM / DCC / 换汇建议 |
| 🥗 饮食饮水 | 各城饮水与街头饮食注意 |
| ⚡ 工具快捷条 | 首页常用工具一键直达 |
| 🔍 搜索增强 | Ctrl+K 可搜工具与页面 |
| 📝 博客 | 标签筛选 + 阅读进度 + 相关推荐 + 分享 | | 📝 博客 | 标签筛选 + 阅读进度 + 相关推荐 + 分享 |
| ❓ FAQ | 搜索 + 展开/收起全部 | | ❓ FAQ | 搜索 + 展开/收起全部 |

View File

@ -8,6 +8,16 @@ export const metadata: Metadata = {
}; };
const LOGS = [ const LOGS = [
{
date: "2026-08-29",
tag: "体验优化",
items: [
"全局搜索覆盖全部工具与页面",
"首页常用工具快捷条 + 下方模块懒加载",
"行程支持下载 Markdown 文件",
"新增饮食饮水注意事项",
],
},
{ {
date: "2026-08-29", date: "2026-08-29",
tag: "工具箱", tag: "工具箱",

View File

@ -7482,3 +7482,72 @@ img { max-width: 100%; display: block; }
left: 0; left: 0;
color: var(--accent-3); color: var(--accent-3);
} }
/* ===== Tools Strip ===== */
.tools-strip {
padding: 12px 0 28px;
position: relative;
z-index: 1;
}
.tools-strip-inner {
padding: 18px 20px;
background: var(--bg-card);
border: var(--border-glass);
border-radius: var(--radius-xl);
backdrop-filter: blur(16px);
}
.tools-strip-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
gap: 12px;
}
.tools-strip-head strong { font-size: 0.95rem; }
.tools-strip-head a {
font-size: 0.85rem;
color: var(--accent-3);
font-weight: 600;
}
.tools-strip-list {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 8px;
}
.tools-strip-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 12px 6px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
border-radius: var(--radius-md);
transition: all 0.2s;
text-align: center;
}
.tools-strip-item:hover {
border-color: rgba(78, 205, 196, 0.35);
background: rgba(78, 205, 196, 0.08);
transform: translateY(-2px);
}
.tools-strip-item span { font-size: 1.3rem; }
.tools-strip-item small {
font-size: 0.75rem;
color: var(--text-secondary);
}
@media (max-width: 900px) {
.tools-strip-list { grid-template-columns: repeat(4, 1fr); }
}
@media (max-width: 480px) {
.tools-strip-list { grid-template-columns: repeat(4, 1fr); }
}

View File

@ -0,0 +1,89 @@
"use client";
import { useMemo, useState } from "react";
import type { Destination } from "@/lib/types";
interface Tip {
emoji: string;
title: string;
detail: string;
}
const TIPS: Record<string, Tip[]> = {
bali: [
{ emoji: "💧", title: "别喝自来水", detail: "买瓶装水或用滤水壶,刷牙也建议用瓶装水。" },
{ emoji: "🌶️", title: "街头小吃循序渐进", detail: "选人多翻台快的摊位,第一周少碰生冷。" },
{ emoji: "🧊", title: "冰块看场合", detail: "正规餐厅一般安全,路边摊可要求无冰。" },
],
chiangmai: [
{ emoji: "💧", title: "便利店饮水够用", detail: "7-Eleven 瓶装水便宜,也可买滤芯壶。" },
{ emoji: "🍜", title: "夜市黄金法则", detail: "跟当地人排队,现做现卖更安心。" },
{ emoji: "🥭", title: "水果现切注意卫生", detail: "优先自己削皮的水果,少买路边预切片。" },
],
lisbon: [
{ emoji: "🚰", title: "自来水可直饮", detail: "市区自来水达标,仍可按习惯买瓶装。" },
{ emoji: "🐟", title: "海鲜选新鲜时段", detail: "午餐海鲜更稳,注意过敏与季节。" },
{ emoji: "🍷", title: "餐饮卫生标准高", detail: "正规餐厅普遍放心,注意小费习惯即可。" },
],
barcelona: [
{ emoji: "🚰", title: "自来水可饮用", detail: "口感偏硬,很多人选瓶装改善口感。" },
{ emoji: "🥘", title: "海鲜饭别贪便宜", detail: "选评价好的店,避开港口旁拉客摊。" },
{ emoji: "🍦", title: "夏日注意冷链", detail: "冰激凌与生食优先人气店铺。" },
],
mexico: [
{ emoji: "💧", title: "只喝瓶装水", detail: "刷牙、洗水果也建议用瓶装或过滤水。" },
{ emoji: "🌮", title: "街头 taco 看人气", detail: "现做、翻台快、有洗手点更佳。" },
{ emoji: "🍋", title: "青柠与辣椒循序", detail: "肠胃适应期少叠加过辣与酒精。" },
],
tokyo: [
{ emoji: "🚰", title: "自来水可直饮", detail: "水质优秀,便利店水也很方便。" },
{ emoji: "🍣", title: "生食选可信店铺", detail: "回转寿司连锁通常安全,街边需谨慎。" },
{ emoji: "🍱", title: "便利店食品放心", detail: "标注清晰,适合赶火车时的稳妥选择。" },
],
};
interface Props {
destinations: Destination[];
}
export default function FoodWaterTips({ destinations }: Props) {
const [slug, setSlug] = useState(destinations[0]?.slug || "bali");
const tips = useMemo(() => TIPS[slug] || TIPS.bali, [slug]);
const dest = destinations.find((d) => d.slug === slug);
return (
<section className="section food-section" id="food-water">
<div className="container">
<div className="section-header reveal">
<span className="section-tag">🥗 FOOD & WATER</span>
<h2>饮食饮水注意</h2>
<p>落地少踩坑,肠胃稳定才能稳定写代码</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>
);
}

View File

@ -3,6 +3,7 @@
import { useEffect, useState, useCallback } from "react"; import { useEffect, useState, useCallback } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { api } from "@/lib/api"; import { api } from "@/lib/api";
import { TOOL_LINKS } from "@/lib/tools";
import type { SearchResult } from "@/lib/types"; import type { SearchResult } from "@/lib/types";
const TYPE_LABELS: Record<string, string> = { const TYPE_LABELS: Record<string, string> = {
@ -10,8 +11,34 @@ const TYPE_LABELS: Record<string, string> = {
blog: "博客", blog: "博客",
visa: "签证", visa: "签证",
faq: "FAQ", faq: "FAQ",
tool: "工具",
page: "页面",
}; };
const PAGE_RESULTS: SearchResult[] = [
{ type: "page", title: "工具箱", subtitle: "全部旅居工具入口", emoji: "🛠️", url: "/tools" },
{ type: "page", title: "关于 nomadro", subtitle: "品牌与联系方式", emoji: "🌏", url: "/about" },
{ type: "page", title: "更新日志", subtitle: "功能迭代记录", emoji: "📜", url: "/changelog" },
{ type: "page", title: "隐私政策", subtitle: "Cookie 与数据说明", emoji: "🔒", url: "/privacy" },
];
function searchLocal(q: string): SearchResult[] {
const s = q.toLowerCase();
const tools = TOOL_LINKS
.filter((t) => t.title.toLowerCase().includes(s) || t.desc.toLowerCase().includes(s) || t.id.includes(s))
.map((t) => ({
type: "tool",
title: t.title,
subtitle: t.desc,
emoji: t.emoji,
url: t.href,
}));
const pages = PAGE_RESULTS.filter(
(p) => p.title.toLowerCase().includes(s) || p.subtitle.toLowerCase().includes(s)
);
return [...tools, ...pages];
}
export default function GlobalSearch() { export default function GlobalSearch() {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
@ -35,12 +62,15 @@ export default function GlobalSearch() {
const doSearch = useCallback(async (q: string) => { const doSearch = useCallback(async (q: string) => {
if (!q.trim()) { setResults([]); return; } if (!q.trim()) { setResults([]); return; }
setLoading(true); setLoading(true);
const local = searchLocal(q);
try { try {
const data = await api.search(q); const data = await api.search(q);
setResults(data); const merged = [...local, ...data].slice(0, 20);
setResults(merged);
setActiveIdx(0); setActiveIdx(0);
} catch { } catch {
setResults([]); setResults(local);
setActiveIdx(0);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -89,7 +119,7 @@ export default function GlobalSearch() {
</svg> </svg>
<input <input
autoFocus autoFocus
placeholder="搜索目的地、博客、签证..." placeholder="搜索目的地、工具、签证、博客…"
value={query} value={query}
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
@ -102,7 +132,7 @@ export default function GlobalSearch() {
<p className="search-empty">😢 没有找到「{query}」相关结果</p> <p className="search-empty">😢 没有找到「{query}」相关结果</p>
)} )}
{!loading && results.map((r, i) => ( {!loading && results.map((r, i) => (
<button key={`${r.type}-${r.url}`} <button key={`${r.type}-${r.url}-${r.title}`}
className={`search-result${i === activeIdx ? " active" : ""}`} className={`search-result${i === activeIdx ? " active" : ""}`}
onClick={() => navigate(r.url)} onClick={() => navigate(r.url)}
onMouseEnter={() => setActiveIdx(i)}> onMouseEnter={() => setActiveIdx(i)}>
@ -116,7 +146,7 @@ export default function GlobalSearch() {
))} ))}
{!query && ( {!query && (
<div className="search-hints"> <div className="search-hints">
<p>💡 试试搜索:清迈、签证、税务</p> <p>💡 试试:清迈、机票、税居、工具箱</p>
<p>⌨️ ↑↓ 选择 · Enter 跳转 · Esc 关闭</p> <p>⌨️ ↑↓ 选择 · Enter 跳转 · Esc 关闭</p>
</div> </div>
)} )}

View File

@ -1,6 +1,7 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState, type ComponentType } from "react";
import dynamic from "next/dynamic";
import { api } from "@/lib/api"; import { api } from "@/lib/api";
import type { Destination, BlogPost, FAQ, Testimonial, Tool, Visa, Stats, ChartData } from "@/lib/types"; import type { Destination, BlogPost, FAQ, Testimonial, Tool, Visa, Stats, ChartData } from "@/lib/types";
import Loader from "./Loader"; import Loader from "./Loader";
@ -10,54 +11,60 @@ import TickerBar from "./TickerBar";
import Hero from "./Hero"; import Hero from "./Hero";
import WorldMap from "./WorldMap"; import WorldMap from "./WorldMap";
import Destinations from "./Destinations"; import Destinations from "./Destinations";
import Calculator from "./Calculator";
import TripPlanner from "./TripPlanner";
import GlobalSearch from "./GlobalSearch"; import GlobalSearch from "./GlobalSearch";
import Lifestyle from "./Lifestyle";
import Charts from "./Charts";
import VisaSection from "./VisaSection";
import Tools from "./Tools";
import BlogSection from "./BlogSection";
import FAQSection from "./FAQSection";
import Community from "./Community";
import Footer from "./Footer"; import Footer from "./Footer";
import CompareModal from "./CompareModal"; import CompareModal from "./CompareModal";
import DestinationMatcher, { MatcherTrigger } from "./DestinationMatcher"; import DestinationMatcher, { MatcherTrigger } from "./DestinationMatcher";
import TimezoneBoard from "./TimezoneBoard";
import ScrollProgress from "./ScrollProgress"; import ScrollProgress from "./ScrollProgress";
import SectionNav from "./SectionNav"; import SectionNav from "./SectionNav";
import CurrencyConverter from "./CurrencyConverter";
import NomadScore from "./NomadScore";
import CostCompare from "./CostCompare";
import CookieConsent from "./CookieConsent"; import CookieConsent from "./CookieConsent";
import CoworkingGuide from "./CoworkingGuide";
import PackingChecklist from "./PackingChecklist";
import SeasonGuide from "./SeasonGuide";
import NomadTips from "./NomadTips"; import NomadTips from "./NomadTips";
import SpinGlobe from "./SpinGlobe";
import FlightCost from "./FlightCost";
import NomadEvents from "./NomadEvents";
import WifiWorkScore from "./WifiWorkScore";
import SavingsGoal from "./SavingsGoal";
import DailyQuote from "./DailyQuote"; import DailyQuote from "./DailyQuote";
import JetLagEstimator from "./JetLagEstimator"; import ToolsStrip from "./ToolsStrip";
import HousingGuide from "./HousingGuide";
import Phrasebook from "./Phrasebook";
import FeedbackWidget from "./FeedbackWidget"; import FeedbackWidget from "./FeedbackWidget";
import InsuranceGuide from "./InsuranceGuide";
import ExpenseTracker from "./ExpenseTracker";
import SimGuide from "./SimGuide";
import TaxDayCounter from "./TaxDayCounter";
import ArrivalChecklist from "./ArrivalChecklist";
import CafeGuide from "./CafeGuide";
import OnboardingTour from "./OnboardingTour"; import OnboardingTour from "./OnboardingTour";
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 KeyboardShortcuts from "./KeyboardShortcuts";
import BackToTop from "./BackToTop"; import BackToTop from "./BackToTop";
import TripPlanner from "./TripPlanner";
import Calculator from "./Calculator";
import SpinGlobe from "./SpinGlobe";
const lazy = <P extends object>(loader: () => Promise<{ default: ComponentType<P> }>) =>
dynamic(loader, { ssr: false, loading: () => null });
const TimezoneBoard = lazy(() => import("./TimezoneBoard"));
const JetLagEstimator = lazy(() => import("./JetLagEstimator"));
const CostCompare = lazy(() => import("./CostCompare"));
const SavingsGoal = lazy(() => import("./SavingsGoal"));
const ExpenseTracker = lazy(() => import("./ExpenseTracker"));
const TaxDayCounter = lazy(() => import("./TaxDayCounter"));
const FlightCost = lazy(() => import("./FlightCost"));
const CurrencyConverter = lazy(() => import("./CurrencyConverter"));
const WifiWorkScore = lazy(() => import("./WifiWorkScore"));
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 InsuranceGuide = lazy(() => import("./InsuranceGuide"));
const EmergencyContacts = lazy(() => import("./EmergencyContacts"));
const AtmCashGuide = lazy(() => import("./AtmCashGuide"));
const FoodWaterTips = lazy(() => import("./FoodWaterTips"));
const CoworkingGuide = lazy(() => import("./CoworkingGuide"));
const HousingGuide = lazy(() => import("./HousingGuide"));
const SimGuide = lazy(() => import("./SimGuide"));
const PowerPlugGuide = lazy(() => import("./PowerPlugGuide"));
const CafeGuide = lazy(() => import("./CafeGuide"));
const SeasonGuide = lazy(() => import("./SeasonGuide"));
const PackingChecklist = lazy(() => import("./PackingChecklist"));
const ArrivalChecklist = lazy(() => import("./ArrivalChecklist"));
const Phrasebook = lazy(() => import("./Phrasebook"));
const CityNotes = lazy(() => import("./CityNotes"));
const NomadEvents = lazy(() => import("./NomadEvents"));
const Tools = lazy(() => import("./Tools"));
const BlogSection = lazy(() => import("./BlogSection"));
const FAQSection = lazy(() => import("./FAQSection"));
const Community = lazy(() => import("./Community"));
interface Props { interface Props {
stats: Stats; stats: Stats;
@ -84,16 +91,30 @@ export default function HomeClient(props: Props) {
}, []); }, []);
useEffect(() => { useEffect(() => {
const reveals = document.querySelectorAll(".reveal");
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
(entries) => entries.forEach((e) => { if (e.isIntersecting) { e.target.classList.add("visible"); observer.unobserve(e.target); } }), (entries) => entries.forEach((e) => {
if (e.isIntersecting) {
e.target.classList.add("visible");
observer.unobserve(e.target);
}
}),
{ threshold: 0.1, rootMargin: "0px 0px -50px 0px" } { threshold: 0.1, rootMargin: "0px 0px -50px 0px" }
); );
reveals.forEach((el, i) => {
const observeAll = () => {
document.querySelectorAll(".reveal:not(.visible)").forEach((el, i) => {
(el as HTMLElement).style.transitionDelay = `${(i % 6) * 0.1}s`; (el as HTMLElement).style.transitionDelay = `${(i % 6) * 0.1}s`;
observer.observe(el); observer.observe(el);
}); });
return () => observer.disconnect(); };
observeAll();
const mo = new MutationObserver(observeAll);
mo.observe(document.body, { childList: true, subtree: true });
return () => {
observer.disconnect();
mo.disconnect();
};
}, []); }, []);
const toggleCompare = (slug: string) => { const toggleCompare = (slug: string) => {
@ -127,6 +148,7 @@ export default function HomeClient(props: Props) {
<Destinations destinations={props.destinations} onCompare={toggleCompare} compareList={compareList} onOpenMatcher={() => setMatcherOpen(true)} /> <Destinations destinations={props.destinations} onCompare={toggleCompare} compareList={compareList} onOpenMatcher={() => setMatcherOpen(true)} />
<SpinGlobe destinations={props.destinations} /> <SpinGlobe destinations={props.destinations} />
<DailyQuote /> <DailyQuote />
<ToolsStrip />
<TimezoneBoard destinations={props.destinations} /> <TimezoneBoard destinations={props.destinations} />
<JetLagEstimator destinations={props.destinations} /> <JetLagEstimator destinations={props.destinations} />
<TripPlanner destinations={props.destinations} /> <TripPlanner destinations={props.destinations} />
@ -146,6 +168,7 @@ export default function HomeClient(props: Props) {
<InsuranceGuide /> <InsuranceGuide />
<EmergencyContacts destinations={props.destinations} /> <EmergencyContacts destinations={props.destinations} />
<AtmCashGuide destinations={props.destinations} /> <AtmCashGuide destinations={props.destinations} />
<FoodWaterTips destinations={props.destinations} />
<CoworkingGuide /> <CoworkingGuide />
<HousingGuide destinations={props.destinations} /> <HousingGuide destinations={props.destinations} />
<SimGuide destinations={props.destinations} /> <SimGuide destinations={props.destinations} />

View File

@ -10,6 +10,7 @@ const SHORTCUTS = [
{ keys: ["Ctrl", "K"], label: "全局搜索", icon: "🔍" }, { keys: ["Ctrl", "K"], label: "全局搜索", icon: "🔍" },
{ keys: ["M"], label: "智能匹配", icon: "🎯" }, { keys: ["M"], label: "智能匹配", icon: "🎯" },
{ keys: ["G"], label: "命运转盘", icon: "🎲" }, { keys: ["G"], label: "命运转盘", icon: "🎲" },
{ keys: ["T"], label: "打开工具箱", icon: "🛠️" },
{ keys: ["?"], label: "快捷键帮助", icon: "⌨️" }, { keys: ["?"], label: "快捷键帮助", icon: "⌨️" },
{ keys: ["Esc"], label: "关闭弹窗", icon: "✕" }, { keys: ["Esc"], label: "关闭弹窗", icon: "✕" },
{ keys: ["↑", "↓"], label: "搜索导航", icon: "↕️" }, { keys: ["↑", "↓"], label: "搜索导航", icon: "↕️" },
@ -36,6 +37,10 @@ export default function KeyboardShortcuts({ onOpenMatcher }: Props) {
e.preventDefault(); e.preventDefault();
document.getElementById("spin")?.scrollIntoView({ behavior: "smooth" }); document.getElementById("spin")?.scrollIntoView({ behavior: "smooth" });
} }
if (e.key === "t" && !typing && !e.metaKey && !e.ctrlKey) {
e.preventDefault();
window.location.href = "/tools";
}
if (e.key === "Escape" && open) setOpen(false); if (e.key === "Escape" && open) setOpen(false);
}; };
window.addEventListener("keydown", onKey); window.addEventListener("keydown", onKey);

View File

@ -0,0 +1,37 @@
"use client";
import Link from "next/link";
const QUICK = [
{ href: "/#trip", emoji: "🗓️", label: "行程" },
{ href: "/#calculator", emoji: "🧮", label: "费用" },
{ href: "/#visa", emoji: "📋", label: "签证" },
{ href: "/#wifi", emoji: "📶", label: "网速" },
{ href: "/#focus", emoji: "⏱️", label: "专注" },
{ href: "/#expenses", emoji: "📒", label: "账本" },
{ href: "/#emergency", emoji: "🆘", label: "紧急" },
{ href: "/tools", emoji: "🛠️", label: "全部" },
];
export default function ToolsStrip() {
return (
<section className="tools-strip" id="tools-strip" aria-label="快捷工具">
<div className="container">
<div className="tools-strip-inner reveal">
<div className="tools-strip-head">
<strong>⚡ 常用工具</strong>
<Link href="/tools">打开工具箱 →</Link>
</div>
<div className="tools-strip-list">
{QUICK.map((t) => (
<Link key={t.href + t.label} href={t.href} className="tools-strip-item">
<span>{t.emoji}</span>
<small>{t.label}</small>
</Link>
))}
</div>
</div>
</div>
</section>
);
}

View File

@ -69,6 +69,28 @@ export default function TripPlanner({ destinations }: Props) {
toast("行程已复制到剪贴板 📋"); toast("行程已复制到剪贴板 📋");
}; };
const downloadTrip = () => {
const lines = [
"# nomadro 旅居行程",
"",
...trip.map((t, i) =>
`${i + 1}. ${t.emoji} **${t.name}, ${t.country}** — ${t.months} 个月 · ¥${(t.cost * t.months).toLocaleString()}`
),
"",
`> 总计 **${totalMonths} 个月** · **¥${totalCost.toLocaleString()}**`,
"",
`_由 nomadro 生成 · ${new Date().toLocaleDateString("zh-CN")}_`,
];
const blob = new Blob([lines.join("\n")], { type: "text/markdown;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `nomadro-trip-${Date.now()}.md`;
a.click();
URL.revokeObjectURL(url);
toast("已下载 Markdown 行程文件 📄");
};
return ( return (
<section className="section trip-section" id="trip"> <section className="section trip-section" id="trip">
<div className="container"> <div className="container">
@ -141,6 +163,7 @@ export default function TripPlanner({ destinations }: Props) {
<strong>{trip.length} 个</strong> <strong>{trip.length} 个</strong>
</div> </div>
<button className="btn btn-ghost" onClick={exportTrip}>📋 复制行程</button> <button className="btn btn-ghost" onClick={exportTrip}>📋 复制行程</button>
<button className="btn btn-ghost" onClick={downloadTrip}>📄 下载 MD</button>
<button className="btn btn-ghost" onClick={shareTrip}>🔗 分享行程</button> <button className="btn btn-ghost" onClick={shareTrip}>🔗 分享行程</button>
<button className="btn btn-ghost" onClick={() => save([])}>🗑️ 清空</button> <button className="btn btn-ghost" onClick={() => save([])}>🗑️ 清空</button>
</div> </div>

View File

@ -36,6 +36,7 @@ export const TOOL_LINKS: ToolLink[] = [
{ id: "insurance", emoji: "🏥", title: "保险指南", desc: "国际医疗保险对比", href: "/#insurance", category: "safety" }, { id: "insurance", emoji: "🏥", title: "保险指南", desc: "国际医疗保险对比", href: "/#insurance", category: "safety" },
{ id: "emergency", emoji: "🆘", title: "紧急联系", desc: "急救与领事馆", href: "/#emergency", category: "safety" }, { id: "emergency", emoji: "🆘", title: "紧急联系", desc: "急救与领事馆", href: "/#emergency", category: "safety" },
{ id: "atm", emoji: "🏧", title: "取现避坑", desc: "ATM 与换汇建议", href: "/#atm", category: "safety" }, { id: "atm", emoji: "🏧", title: "取现避坑", desc: "ATM 与换汇建议", href: "/#atm", category: "safety" },
{ id: "food-water", emoji: "🥗", title: "饮食饮水", desc: "落地肠胃与饮水注意", href: "/#food-water", category: "safety" },
]; ];
export const TOOL_CATEGORIES = [ export const TOOL_CATEGORIES = [