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;