From ea030275ce2f1593cdfc729ce0e5efc79e31745c Mon Sep 17 00:00:00 2001 From: eric Date: Sun, 30 Aug 2026 21:37:57 -0500 Subject: [PATCH] Polish three rings: connect auth gates, explore shortcuts, grow nav and forms. Co-authored-by: Cursor --- frontend/src/app/ai/page.tsx | 8 +++-- frontend/src/app/changelog/page.tsx | 9 +++++ frontend/src/app/globals.css | 8 +++++ frontend/src/app/profile/page.tsx | 11 ++++-- frontend/src/components/AiAssistantClient.tsx | 36 ++++++++++++++++--- frontend/src/components/ChatClient.tsx | 1 + frontend/src/components/ChatThreadClient.tsx | 1 + .../src/components/CommunityHubClient.tsx | 20 +++++++++-- frontend/src/components/CompareClient.tsx | 6 +++- .../src/components/ContentSubmitClient.tsx | 9 +++++ frontend/src/components/DatingClient.tsx | 4 ++- .../src/components/DigitalCourseClient.tsx | 35 ++++++++++++------ frontend/src/components/FeedbackClient.tsx | 4 +++ frontend/src/components/Footer.tsx | 3 ++ frontend/src/components/GigPostClient.tsx | 15 ++++++++ frontend/src/components/HomeClient.tsx | 11 ++++++ frontend/src/components/MeetupLiveClient.tsx | 12 ++++++- frontend/src/components/MeetupsHostClient.tsx | 15 ++++++++ frontend/src/components/Navbar.tsx | 4 +++ frontend/src/components/ServicesClient.tsx | 4 +++ frontend/src/components/SiteShell.tsx | 16 +++++++++ frontend/src/lib/authNext.ts | 32 +++++++++++++++++ frontend/src/lib/i18n/dictionaries.ts | 36 +++++++++++++++++++ 23 files changed, 274 insertions(+), 26 deletions(-) diff --git a/frontend/src/app/ai/page.tsx b/frontend/src/app/ai/page.tsx index 400f053..688cafb 100644 --- a/frontend/src/app/ai/page.tsx +++ b/frontend/src/app/ai/page.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next"; +import { api } from "@/lib/api"; import SiteShell from "@/components/SiteShell"; import AiAssistantClient from "@/components/AiAssistantClient"; @@ -7,10 +8,13 @@ export const metadata: Metadata = { description: "智能推荐下一站游民城市", }; -export default function AiPage() { +export const revalidate = 60; + +export default async function AiPage() { + const destinations = await api.getDestinations().catch(() => []); return ( - + ); } diff --git a/frontend/src/app/changelog/page.tsx b/frontend/src/app/changelog/page.tsx index c8bf6b7..5f3c1c4 100644 --- a/frontend/src/app/changelog/page.tsx +++ b/frontend/src/app/changelog/page.tsx @@ -8,6 +8,15 @@ export const metadata: Metadata = { }; const LOGS = [ + { + date: "2026-08-30", + tag: "三环登录与导航", + items: [ + "Connect:个人中心三环快捷入口、匹配加载态、发起活动/发赏金登录门、私信与登录提示", + "Explore:全站快捷键、首页 hash 锚点、对比空态、AI 推荐真正写入计划", + "Grow:导航补全服务/投稿、表单校验、学院空态与 VIP 课时直达开通", + ], + }, { date: "2026-08-30", tag: "三环可用深化", diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index ca38737..e9f851e 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -10903,6 +10903,14 @@ img { max-width: 100%; display: block; } font-size: 0.95rem; } +.digital-lesson-list li a.digital-lesson-locked { + opacity: 0.85; +} + +.digital-lesson-list li a.digital-lesson-locked:hover .digital-lesson-meta { + color: var(--accent-3); +} + .digital-lesson-meta { font-size: 0.8rem; color: var(--text-muted); diff --git a/frontend/src/app/profile/page.tsx b/frontend/src/app/profile/page.tsx index a1af31e..59af3ba 100644 --- a/frontend/src/app/profile/page.tsx +++ b/frontend/src/app/profile/page.tsx @@ -73,7 +73,7 @@ export default function ProfilePage() { useEffect(() => { if (!token) { - router.push("/login"); + router.push("/login?next=/profile"); return; } setLoading(true); @@ -291,8 +291,13 @@ export default function ProfilePage() { 🌍 浏览目的地 🗓️ 旅居计划 ⚖️ 城市对比 - 📋 签证指南 - 📝 阅读博客 + 🧭 智能下一站 + 🎉 游民活动 + 💕 游民匹配 + ✉️ 私信 + 🔔 通知 + {vip ? "✨ VIP 会员" : "✨ 开通 VIP"} + 🎓 游民学院 🛠️ 工具箱 diff --git a/frontend/src/components/AiAssistantClient.tsx b/frontend/src/components/AiAssistantClient.tsx index 6a74e77..3b1fe7a 100644 --- a/frontend/src/components/AiAssistantClient.tsx +++ b/frontend/src/components/AiAssistantClient.tsx @@ -2,9 +2,12 @@ import { useState } from "react"; import Link from "next/link"; +import { useRouter } from "next/navigation"; import { api } from "@/lib/api"; import { useI18n } from "@/lib/i18n"; import { useToast } from "@/lib/toast"; +import { mergeDestinationsIntoTrip } from "@/lib/tripActions"; +import type { Destination } from "@/lib/types"; import RingNext from "@/components/RingNext"; import { useRingSteps } from "@/lib/rings"; @@ -14,9 +17,14 @@ const SUGGESTIONS = [ "清迈和巴厘岛怎么选?", ]; -export default function AiAssistantClient() { +interface Props { + destinations: Destination[]; +} + +export default function AiAssistantClient({ destinations }: Props) { const { t } = useI18n(); const { toast } = useToast(); + const router = useRouter(); const rings = useRingSteps(); const [message, setMessage] = useState(""); const [reply, setReply] = useState(""); @@ -43,6 +51,24 @@ export default function AiAssistantClient() { } }; + const addToPlan = () => { + const dests = cities + .map((c) => destinations.find((d) => d.slug === c.slug)) + .filter((d): d is Destination => Boolean(d)); + if (dests.length === 0) { + toast("推荐城市暂无完整数据,请从详情页加入计划", "info"); + router.push("/plan"); + return; + } + const { added, skipped } = mergeDestinationsIntoTrip(dests, 1); + if (added === 0) { + toast(skipped ? t.compare.alreadyInPlan : "未能写入计划", "info"); + } else { + toast(`${t.compare.addedN} ${added} ${t.compare.citiesUnit}`); + } + router.push("/plan"); + }; + return (
@@ -93,9 +119,11 @@ export default function AiAssistantClient() { 城市对比 - - 写入计划 - + {cities.length > 0 && ( + + )}
)} diff --git a/frontend/src/components/ChatClient.tsx b/frontend/src/components/ChatClient.tsx index fecc697..0f2dcfb 100644 --- a/frontend/src/components/ChatClient.tsx +++ b/frontend/src/components/ChatClient.tsx @@ -45,6 +45,7 @@ export default function ChatClient() {

{t.chat.loginTitle}

+

{t.chat.loginDesc}

{t.nav.login}
diff --git a/frontend/src/components/ChatThreadClient.tsx b/frontend/src/components/ChatThreadClient.tsx index 6262ae7..7c4cab3 100644 --- a/frontend/src/components/ChatThreadClient.tsx +++ b/frontend/src/components/ChatThreadClient.tsx @@ -62,6 +62,7 @@ export default function ChatThreadClient({ convId }: { convId: string }) {

{t.chat.loginTitle}

+

{t.chat.loginDesc}

{t.nav.login} diff --git a/frontend/src/components/CommunityHubClient.tsx b/frontend/src/components/CommunityHubClient.tsx index 3025977..52e0210 100644 --- a/frontend/src/components/CommunityHubClient.tsx +++ b/frontend/src/components/CommunityHubClient.tsx @@ -125,9 +125,23 @@ export default function CommunityHubClient({ discussions, featured }: Props) { {filtered.length === 0 && (

{t.community.empty}

- - {t.community.newTopic} - +
+ {(category !== "全部" || q.trim()) && ( + + )} + + {t.community.newTopic} + +
)} diff --git a/frontend/src/components/CompareClient.tsx b/frontend/src/components/CompareClient.tsx index 65dcce7..7bad7f6 100644 --- a/frontend/src/components/CompareClient.tsx +++ b/frontend/src/components/CompareClient.tsx @@ -145,7 +145,11 @@ export default function CompareClient({ destinations }: Props) {
⚖️

{t.compare.needMore} {Math.max(0, 2 - selected.length)} {t.compare.needMoreSuffix}

- {t.compare.backDest} +
+ {t.compare.backDest} + {t.nav.nextStop} + {t.nav.ai} +
) : ( <> diff --git a/frontend/src/components/ContentSubmitClient.tsx b/frontend/src/components/ContentSubmitClient.tsx index 094e0bc..522c366 100644 --- a/frontend/src/components/ContentSubmitClient.tsx +++ b/frontend/src/components/ContentSubmitClient.tsx @@ -8,11 +8,15 @@ import { useAuth } from "@/lib/auth"; import { useToast } from "@/lib/toast"; import { useI18n } from "@/lib/i18n"; +import RingNext from "@/components/RingNext"; +import { useRingSteps } from "@/lib/rings"; + export default function ContentSubmitClient() { const { token } = useAuth(); const router = useRouter(); const { toast } = useToast(); const { t } = useI18n(); + const rings = useRingSteps(); const [title, setTitle] = useState(""); const [url, setUrl] = useState(""); const [notes, setNotes] = useState(""); @@ -29,6 +33,10 @@ export default function ContentSubmitClient() { toast("请填写更完整的标题", "info"); return; } + if ((contentType === "article" || contentType === "video") && !/^https?:\/\/.+/.test(url.trim())) { + toast("文章/视频请填写有效链接(http/https)", "info"); + return; + } setBusy(true); try { await api.submitContent( @@ -113,6 +121,7 @@ export default function ContentSubmitClient() { )}
)} +
); diff --git a/frontend/src/components/DatingClient.tsx b/frontend/src/components/DatingClient.tsx index 4b8bf57..4567162 100644 --- a/frontend/src/components/DatingClient.tsx +++ b/frontend/src/components/DatingClient.tsx @@ -160,7 +160,9 @@ export default function DatingClient() { )} - {loading && joined &&

加载候选人…

} + {loading && ( +

{joined ? "加载候选人…" : "加载匹配资料…"}

+ )} {top && (
diff --git a/frontend/src/components/DigitalCourseClient.tsx b/frontend/src/components/DigitalCourseClient.tsx index ac601ed..953eb38 100644 --- a/frontend/src/components/DigitalCourseClient.tsx +++ b/frontend/src/components/DigitalCourseClient.tsx @@ -39,7 +39,14 @@ export default function DigitalCourseClient({ course }: { course: DigitalCourse )}
{course.modules.length === 0 ? ( -

课程内容准备中

+
+

课程内容准备中

+
+ {t.digital.back} + {t.nav.gigs} + {t.nav.join} +
+
) : (
{course.modules.map((mod, mi) => ( @@ -50,16 +57,22 @@ export default function DigitalCourseClient({ course }: { course: DigitalCourse const locked = !lesson.free && !vip; return (
  • - - - {locked ? "🔒 " : ""} - {lesson.title} - - - {lesson.duration} - {lesson.free ? ` · ${t.digital.free}` : " · VIP"} - - + {locked ? ( + + 🔒 {lesson.title} + + {lesson.duration} · VIP · 开通解锁 + + + ) : ( + + {lesson.title} + + {lesson.duration} + {lesson.free ? ` · ${t.digital.free}` : " · VIP"} + + + )}
  • ); })} diff --git a/frontend/src/components/FeedbackClient.tsx b/frontend/src/components/FeedbackClient.tsx index 9deff62..dc18d85 100644 --- a/frontend/src/components/FeedbackClient.tsx +++ b/frontend/src/components/FeedbackClient.tsx @@ -6,11 +6,14 @@ import { api } from "@/lib/api"; import { useAuth } from "@/lib/auth"; import { useToast } from "@/lib/toast"; import { useI18n } from "@/lib/i18n"; +import RingNext from "@/components/RingNext"; +import { useRingSteps } from "@/lib/rings"; export default function FeedbackClient() { const { token } = useAuth(); const { toast } = useToast(); const { t } = useI18n(); + const rings = useRingSteps(); const [content, setContent] = useState(""); const [category, setCategory] = useState("general"); const [loading, setLoading] = useState(false); @@ -79,6 +82,7 @@ export default function FeedbackClient() { {!token &&

    未登录也可提交;登录后便于我们回复你

    }
    )} + ); diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx index a2201fa..30b3711 100644 --- a/frontend/src/components/Footer.tsx +++ b/frontend/src/components/Footer.tsx @@ -61,6 +61,9 @@ export default function Footer() {

    {t.footer.grow}

    {t.footer.digital} {t.footer.gigs} + {t.nav.services} + {t.submit.tag} + {t.nav.feedback} {t.footer.toolkit} {t.footer.join} diff --git a/frontend/src/components/GigPostClient.tsx b/frontend/src/components/GigPostClient.tsx index 559777d..3aa91e0 100644 --- a/frontend/src/components/GigPostClient.tsx +++ b/frontend/src/components/GigPostClient.tsx @@ -19,6 +19,21 @@ export default function GigPostClient() { const [deadline, setDeadline] = useState(""); const [busy, setBusy] = useState(false); + if (!token) { + return ( +
    +
    + +
    +

    {t.gigs.postTitle}

    +

    {t.gigs.loginFirst}

    + {t.nav.login} +
    +
    +
    + ); + } + const submit = async () => { if (!token) { router.push("/login?next=/gigs/post"); diff --git a/frontend/src/components/HomeClient.tsx b/frontend/src/components/HomeClient.tsx index 298d254..23f5837 100644 --- a/frontend/src/components/HomeClient.tsx +++ b/frontend/src/components/HomeClient.tsx @@ -53,6 +53,17 @@ export default function HomeClient(props: Props) { const [chromeReady, setChromeReady] = useState(false); const rings = useRingSteps(); + useEffect(() => { + const hash = window.location.hash.slice(1); + if (!hash) return; + const scrollToHash = () => { + const el = document.getElementById(hash); + if (el) el.scrollIntoView({ behavior: "smooth", block: "start" }); + }; + const t = window.setTimeout(scrollToHash, 400); + return () => window.clearTimeout(t); + }, []); + useEffect(() => { const onMatcher = () => setMatcherOpen(true); window.addEventListener("open-matcher", onMatcher); diff --git a/frontend/src/components/MeetupLiveClient.tsx b/frontend/src/components/MeetupLiveClient.tsx index 9ecc17b..c700756 100644 --- a/frontend/src/components/MeetupLiveClient.tsx +++ b/frontend/src/components/MeetupLiveClient.tsx @@ -122,7 +122,17 @@ export default function MeetupLiveClient({ meetupId, title }: { meetupId: string