From c50b5a6c5eed149730ae16f2aee3483698772646 Mon Sep 17 00:00:00 2001 From: AI Directory Platform Date: Thu, 10 Sep 2026 08:47:03 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=BF=9B=E5=BA=A6=E6=9D=A1=E8=A7=A6?= =?UTF-8?q?=E5=8F=91=E6=97=B6=E6=9C=BA=E6=8F=90=E5=89=8D=E5=88=B0=E7=82=B9?= =?UTF-8?q?=E5=87=BB=E7=9E=AC=E9=97=B4=EF=BC=88=E8=A6=86=E7=9B=96=E5=85=A8?= =?UTF-8?q?=E9=83=A8=E5=AF=BC=E8=88=AA=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 根因:此前靠 history.pushState 触发,但 App Router 在导航提交期 (数据加载完成后)才调用它——点击到提交之间的加载期进度条不出现; View Transitions 的旧页快照还会遮住提交后的一闪 - 修复:document 级 click 捕获(站内 点击瞬间触发,先于框架逻辑 与 VT 快照)+ history API 兜底(popstate/程序化导航)+ 4s 超时保护 - 回归:顶部导航/footer 链接/语言切换(程序化导航)三类全部触发 --- .../src/components/navigation-progress.tsx | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/packages/core/src/components/navigation-progress.tsx b/packages/core/src/components/navigation-progress.tsx index d7084b6..c36d0c7 100644 --- a/packages/core/src/components/navigation-progress.tsx +++ b/packages/core/src/components/navigation-progress.tsx @@ -5,9 +5,12 @@ import { usePathname, useSearchParams } from "next/navigation"; /** * # 组件 NavigationProgress - * 说明:顶部导航进度条。拦截 history API(pushState/replaceState/popstate) - * 触发进度条出现,pathname 变化后完成并淡出。 - * 纯客户端实现,零依赖;颜色走主题 token(深浅模式自动适配)。 + * 说明:顶部导航进度条。 + * - 触发:document 级 click 捕获(点击站内 的瞬间,先于框架逻辑与 + * View Transitions 快照)+ history API 兜底(popstate / 程序化导航)。 + * - 完成:pathname/search 变化后冲刺 100% 并淡出;带 4s 超时保护 + * (导航被阻止时进度条不悬挂)。 + * - 颜色走主题 token(深浅模式自动适配),零依赖。 */ export function NavigationProgress() { const pathname = usePathname(); @@ -15,22 +18,38 @@ export function NavigationProgress() { const [visible, setVisible] = useState(false); const [progress, setProgress] = useState(0); const [fading, setFading] = useState(false); - const timers = useRef<{ slow?: ReturnType; done?: ReturnType }>({}); + const timers = useRef<{ slow?: ReturnType; done?: ReturnType; guard?: ReturnType }>({}); - // 拦截导航开始 useEffect(() => { const start = () => { setVisible(true); setFading(false); setProgress(12); - // 缓慢爬升到 70%,营造"进行中" clearTimeout(timers.current.slow); timers.current.slow = setTimeout(() => setProgress(70), 180); + // 超时保护:导航未发生时自动收起 + clearTimeout(timers.current.guard); + timers.current.guard = setTimeout(() => { + setFading(true); + setVisible(false); + setProgress(0); + }, 4000); }; + // ① 点击捕获:站内 点击瞬间触发(先于 next/link 与 View Transitions) + const onClickCapture = (e: MouseEvent) => { + if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; + const anchor = (e.target as HTMLElement).closest?.("a"); + if (!anchor) return; + const href = anchor.getAttribute("href"); + if (!href || href.startsWith("http") || href.startsWith("//") || href.startsWith("#") || anchor.target === "_blank") return; + if (!href.startsWith("/")) return; + start(); + }; + + // ② history 兜底:popstate / 程序化 router.push 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); @@ -40,18 +59,21 @@ export function NavigationProgress() { return origReplace(...args); }; window.addEventListener("popstate", start); + document.addEventListener("click", onClickCapture, true); return () => { window.history.pushState = origPush; window.history.replaceState = origReplace; window.removeEventListener("popstate", start); + document.removeEventListener("click", onClickCapture, true); clearTimeout(timers.current.slow); clearTimeout(timers.current.done); + clearTimeout(timers.current.guard); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // 导航完成(pathname/search 变化)→ 冲刺到 100% → 淡出 + // 导航完成(pathname/search 变化)→ 冲刺 100% → 淡出 useEffect(() => { setVisible((visibleNow) => { if (!visibleNow) return visibleNow;