From 935062393c2f4bf915e892178fc0a499642bbbb2 Mon Sep 17 00:00:00 2001 From: AI Directory Platform Date: Thu, 10 Sep 2026 08:27:05 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AF=BC=E8=88=AA=E4=BD=93=E9=AA=8C?= =?UTF-8?q?=E2=80=94=E2=80=94=E9=A1=B6=E9=83=A8=E8=BF=9B=E5=BA=A6=E6=9D=A1?= =?UTF-8?q?=20+=20=E9=A1=B5=E9=9D=A2=E5=88=87=E6=8D=A2=E8=BF=87=E6=B8=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NavigationProgress:顶部 3px 主题色进度条(拦截 history API 触发, pathname 变化后冲刺 100% 并淡出;跟随深浅主题) - PageTransition:页面内容容器以 pathname 为 key 重挂载,播放 page-enter 进入动画(淡入 + 轻微上移 0.3s),消除切换生硬感 - 站内链接(LocaleLink/Header/Footer/Section)切换到 next-view-transitions 的 Link,支持 VT 的浏览器叠加原生整页淡入淡出(不支持时自动降级) - prefers-reduced-motion 用户禁用全部动效 - 回归:进度条出现/隐藏、page-enter 动画、VT CSS 注入、zh 站生效、 多页导航 0 pageerror --- packages/core/src/components/locale-link.tsx | 2 +- .../src/components/navigation-progress.tsx | 88 +++++++++++++++++++ packages/core/src/kit/chrome.tsx | 3 +- packages/core/src/kit/page-transition.tsx | 20 +++++ packages/core/src/kit/section.tsx | 2 +- packages/core/src/kit/theme-footer.tsx | 2 +- packages/core/src/kit/theme-header.tsx | 2 +- packages/core/src/pages/[locale]/layout.tsx | 5 ++ packages/core/src/styles/globals.css | 54 ++++++++++++ 9 files changed, 173 insertions(+), 5 deletions(-) create mode 100644 packages/core/src/components/navigation-progress.tsx create mode 100644 packages/core/src/kit/page-transition.tsx diff --git a/packages/core/src/components/locale-link.tsx b/packages/core/src/components/locale-link.tsx index 9bd93ad..2f8356e 100644 --- a/packages/core/src/components/locale-link.tsx +++ b/packages/core/src/components/locale-link.tsx @@ -1,6 +1,6 @@ "use client"; -import Link from "next/link"; +import { Link } from "next-view-transitions"; import { useLocale } from 'next-intl'; import { defaultLocale } from '@/i18n.config'; diff --git a/packages/core/src/components/navigation-progress.tsx b/packages/core/src/components/navigation-progress.tsx new file mode 100644 index 0000000..d7084b6 --- /dev/null +++ b/packages/core/src/components/navigation-progress.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { usePathname, useSearchParams } from "next/navigation"; + +/** + * # 组件 NavigationProgress + * 说明:顶部导航进度条。拦截 history API(pushState/replaceState/popstate) + * 触发进度条出现,pathname 变化后完成并淡出。 + * 纯客户端实现,零依赖;颜色走主题 token(深浅模式自动适配)。 + */ +export function NavigationProgress() { + const pathname = usePathname(); + const searchParams = useSearchParams(); + const [visible, setVisible] = useState(false); + const [progress, setProgress] = useState(0); + const [fading, setFading] = useState(false); + const timers = useRef<{ slow?: ReturnType; done?: ReturnType }>({}); + + // 拦截导航开始 + useEffect(() => { + const start = () => { + setVisible(true); + setFading(false); + setProgress(12); + // 缓慢爬升到 70%,营造"进行中" + clearTimeout(timers.current.slow); + timers.current.slow = setTimeout(() => setProgress(70), 180); + }; + + const origPush = window.history.pushState.bind(window.history); + const origReplace = window.history.replaceState.bind(window.history); + + window.history.pushState = function (...args: Parameters) { + start(); + return origPush(...args); + }; + window.history.replaceState = function (...args: Parameters) { + start(); + return origReplace(...args); + }; + window.addEventListener("popstate", start); + + return () => { + window.history.pushState = origPush; + window.history.replaceState = origReplace; + window.removeEventListener("popstate", start); + clearTimeout(timers.current.slow); + clearTimeout(timers.current.done); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // 导航完成(pathname/search 变化)→ 冲刺到 100% → 淡出 + useEffect(() => { + setVisible((visibleNow) => { + if (!visibleNow) return visibleNow; + setFading(true); + setProgress(100); + clearTimeout(timers.current.done); + timers.current.done = setTimeout(() => { + setVisible(false); + setFading(false); + setProgress(0); + }, 320); + return visibleNow; + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pathname, searchParams]); + + if (!visible) return null; + + return ( +
+
+
+ ); +} diff --git a/packages/core/src/kit/chrome.tsx b/packages/core/src/kit/chrome.tsx index eaae517..37d1bb0 100644 --- a/packages/core/src/kit/chrome.tsx +++ b/packages/core/src/kit/chrome.tsx @@ -1,6 +1,7 @@ import { getTranslations, getLocale } from "next-intl/server"; import { ThemeHeader } from "./theme-header"; import { ThemeFooter } from "./theme-footer"; +import { PageTransition } from "./page-transition"; import type { SiteConfig } from "./types"; /** @@ -54,7 +55,7 @@ export async function SiteChrome({ navItems={navItems} searchPlaceholder={th.raw("searchPlaceholder") as string} /> - {children} + {children}
); diff --git a/packages/core/src/kit/page-transition.tsx b/packages/core/src/kit/page-transition.tsx new file mode 100644 index 0000000..5c34751 --- /dev/null +++ b/packages/core/src/kit/page-transition.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { usePathname } from "next/navigation"; + +/** + * # 组件 PageTransition + * 说明:页面切换过渡容器。以 pathname 为 key —— 路由变化时内容容器 + * 重新挂载并播放进入动画(淡入 + 轻微上移),消除"页面跳变"的生硬感。 + * 纯 CSS 动画(page-enter),全浏览器支持;支持 View Transitions 的 + * 浏览器会额外叠加原生过渡(见 globals.css 的 ::view-transition 规则)。 + */ +export function PageTransition({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + + return ( +
+ {children} +
+ ); +} diff --git a/packages/core/src/kit/section.tsx b/packages/core/src/kit/section.tsx index 30ffedc..0b8430f 100644 --- a/packages/core/src/kit/section.tsx +++ b/packages/core/src/kit/section.tsx @@ -1,4 +1,4 @@ -import Link from "next/link"; +import { Link } from "next-view-transitions"; import { useLocale } from "next-intl"; import { defaultLocale } from "@/i18n.config"; diff --git a/packages/core/src/kit/theme-footer.tsx b/packages/core/src/kit/theme-footer.tsx index 8c37f90..6d0592c 100644 --- a/packages/core/src/kit/theme-footer.tsx +++ b/packages/core/src/kit/theme-footer.tsx @@ -1,6 +1,6 @@ "use client"; -import Link from "next/link"; +import { Link } from "next-view-transitions"; import { useLocale } from "next-intl"; import { defaultLocale } from "@/i18n.config"; diff --git a/packages/core/src/kit/theme-header.tsx b/packages/core/src/kit/theme-header.tsx index 09b3633..afa086c 100644 --- a/packages/core/src/kit/theme-header.tsx +++ b/packages/core/src/kit/theme-header.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { Menu, Search, X } from "lucide-react"; -import Link from "next/link"; +import { Link } from "next-view-transitions"; import { ModeToggle } from "@/components/mode-toggle"; import { LanguageSwitcher } from "@/components/language-switcher"; import { UserMenu } from "@/features/navigation/components/user-menu"; diff --git a/packages/core/src/pages/[locale]/layout.tsx b/packages/core/src/pages/[locale]/layout.tsx index 7785890..902eb89 100644 --- a/packages/core/src/pages/[locale]/layout.tsx +++ b/packages/core/src/pages/[locale]/layout.tsx @@ -1,3 +1,4 @@ +import { Suspense } from "react"; import { NextIntlClientProvider } from "next-intl"; import { getMessages, setRequestLocale } from "next-intl/server"; import { notFound } from "next/navigation"; @@ -8,6 +9,7 @@ import { ViewTransitions } from "next-view-transitions"; import { ThemeProvider } from "@/context/theme-provider"; import { Toaster } from "sonner"; import Analytics from "@/analytics/analytics"; +import { NavigationProgress } from "@/components/navigation-progress"; export function generateStaticParams() { return locales.map((locale) => ({ locale })); @@ -53,6 +55,9 @@ export default async function LocaleLayout({ defaultTheme={process.env.NEXT_PUBLIC_DEFAULT_THEME || "light"} > + + + {children} diff --git a/packages/core/src/styles/globals.css b/packages/core/src/styles/globals.css index 305d25d..d81d4c7 100644 --- a/packages/core/src/styles/globals.css +++ b/packages/core/src/styles/globals.css @@ -390,3 +390,57 @@ html { padding-top: 0.75rem; padding-bottom: 0.75rem; } + +/* ============================================================ + 导航体验:顶部进度条 + 页面切换过渡 + View Transitions + ============================================================ */ + +/* 顶部进度条:主题色渐变 + 微光晕 */ +.nav-progress-fill { + background: linear-gradient( + 90deg, + hsl(var(--primary) / 0.4), + hsl(var(--primary)) 60%, + hsl(var(--primary) / 0.9) + ); + box-shadow: 0 0 10px hsl(var(--primary) / 0.55); + transition: width 0.22s ease-out, opacity 0.28s ease-in; +} + +/* 页面进入动画:淡入 + 轻微上移(每次路由变化播放) */ +@keyframes page-enter { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} +.page-enter { + animation: page-enter 0.3s ease-out both; +} +/* 用户偏好减少动效时禁用 */ +@media (prefers-reduced-motion: reduce) { + .page-enter, + .animate-drawer-in { + animation: none; + } +} + +/* View Transitions:整页淡出/淡入(支持 VT 的浏览器叠加使用) */ +::view-transition-old(root) { + animation: vt-fade-out 0.18s ease-out both; +} +::view-transition-new(root) { + animation: vt-fade-in 0.24s ease-out both; +} +@keyframes vt-fade-out { + from { opacity: 1; } + to { opacity: 0; } +} +@keyframes vt-fade-in { + from { opacity: 0; } + to { opacity: 1; } +}