diff --git a/README.md b/README.md index 65f3e24..9529cd3 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,9 @@ docker compose up -d | 🛠️ 工具箱页 | `/tools` 分类搜索全部工具 | | 📜 更新日志 | `/changelog` 迭代记录 | | 🏧 取现避坑 | ATM / DCC / 换汇建议 | +| 🥗 饮食饮水 | 各城饮水与街头饮食注意 | +| ⚡ 工具快捷条 | 首页常用工具一键直达 | +| 🔍 搜索增强 | Ctrl+K 可搜工具与页面 | | 📝 博客 | 标签筛选 + 阅读进度 + 相关推荐 + 分享 | | ❓ FAQ | 搜索 + 展开/收起全部 | diff --git a/frontend/src/app/changelog/page.tsx b/frontend/src/app/changelog/page.tsx index 8ed8fcd..abec5a6 100644 --- a/frontend/src/app/changelog/page.tsx +++ b/frontend/src/app/changelog/page.tsx @@ -8,6 +8,16 @@ export const metadata: Metadata = { }; const LOGS = [ + { + date: "2026-08-29", + tag: "体验优化", + items: [ + "全局搜索覆盖全部工具与页面", + "首页常用工具快捷条 + 下方模块懒加载", + "行程支持下载 Markdown 文件", + "新增饮食饮水注意事项", + ], + }, { date: "2026-08-29", tag: "工具箱", diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 1da0b0a..1f2db6e 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -7482,3 +7482,72 @@ img { max-width: 100%; display: block; } left: 0; 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); } +} diff --git a/frontend/src/components/FoodWaterTips.tsx b/frontend/src/components/FoodWaterTips.tsx new file mode 100644 index 0000000..5f27d2c --- /dev/null +++ b/frontend/src/components/FoodWaterTips.tsx @@ -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 = { + 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 ( +
+
+
+ 🥗 FOOD & WATER +

饮食饮水注意

+

落地少踩坑,肠胃稳定才能稳定写代码

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

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

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

{t.title}

+

{t.detail}

+
+ ))} +
+
+
+ ); +} diff --git a/frontend/src/components/GlobalSearch.tsx b/frontend/src/components/GlobalSearch.tsx index 0ec7bab..81c37e9 100644 --- a/frontend/src/components/GlobalSearch.tsx +++ b/frontend/src/components/GlobalSearch.tsx @@ -3,6 +3,7 @@ import { useEffect, useState, useCallback } from "react"; import { useRouter } from "next/navigation"; import { api } from "@/lib/api"; +import { TOOL_LINKS } from "@/lib/tools"; import type { SearchResult } from "@/lib/types"; const TYPE_LABELS: Record = { @@ -10,8 +11,34 @@ const TYPE_LABELS: Record = { blog: "博客", visa: "签证", 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() { const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); @@ -35,12 +62,15 @@ export default function GlobalSearch() { const doSearch = useCallback(async (q: string) => { if (!q.trim()) { setResults([]); return; } setLoading(true); + const local = searchLocal(q); try { const data = await api.search(q); - setResults(data); + const merged = [...local, ...data].slice(0, 20); + setResults(merged); setActiveIdx(0); } catch { - setResults([]); + setResults(local); + setActiveIdx(0); } finally { setLoading(false); } @@ -89,7 +119,7 @@ export default function GlobalSearch() { setQuery(e.target.value)} onKeyDown={onKeyDown} @@ -102,7 +132,7 @@ export default function GlobalSearch() {

😢 没有找到「{query}」相关结果

)} {!loading && results.map((r, i) => ( - + diff --git a/frontend/src/lib/tools.ts b/frontend/src/lib/tools.ts index b18bbcd..1c46b2d 100644 --- a/frontend/src/lib/tools.ts +++ b/frontend/src/lib/tools.ts @@ -36,6 +36,7 @@ export const TOOL_LINKS: ToolLink[] = [ { 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" }, + { id: "food-water", emoji: "🥗", title: "饮食饮水", desc: "落地肠胃与饮水注意", href: "/#food-water", category: "safety" }, ]; export const TOOL_CATEGORIES = [