From 0d4d7c9e3187dd16fed49a238d533fbd5286b5d7 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Sun, 4 Jan 2026 15:25:53 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=A1=B5=E9=9D=A2=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E8=BF=9B=E5=BA=A6=E6=9D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 + pnpm-lock.yaml | 28 ++++--- src/app/globals.css | 37 ++++++++++ src/app/layout.tsx | 2 + src/components/TopProgressBar.tsx | 117 ++++++++++++++++++++++++++++++ 5 files changed, 174 insertions(+), 12 deletions(-) create mode 100644 src/components/TopProgressBar.tsx diff --git a/package.json b/package.json index fce8496..d16d2e3 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@headlessui/react": "^2.2.4", "@heroicons/react": "^2.2.0", "@types/crypto-js": "^4.2.2", + "@types/nprogress": "^0.2.3", "@upstash/redis": "^1.25.0", "@vidstack/react": "^1.12.13", "anime4k-webgpu": "^1.0.0", @@ -47,6 +48,7 @@ "next-pwa": "^5.6.0", "next-themes": "^0.4.6", "node-fetch": "^2.7.0", + "nprogress": "^0.2.0", "parse-torrent-name": "^0.5.4", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e71f7f..e8e64c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@types/crypto-js': specifier: ^4.2.2 version: 4.2.2 + '@types/nprogress': + specifier: ^0.2.3 + version: 0.2.3 '@upstash/redis': specifier: ^1.25.0 version: 1.35.1 @@ -89,6 +92,9 @@ importers: node-fetch: specifier: ^2.7.0 version: 2.7.0 + nprogress: + specifier: ^0.2.0 + version: 0.2.0 parse-torrent-name: specifier: ^0.5.4 version: 0.5.4 @@ -1203,28 +1209,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@14.2.33': resolution: {integrity: sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@14.2.33': resolution: {integrity: sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@14.2.33': resolution: {integrity: sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@14.2.33': resolution: {integrity: sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==} @@ -1624,6 +1626,9 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/nprogress@0.2.3': + resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==} + '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -1786,49 +1791,41 @@ packages: resolution: {integrity: sha512-vdqBh911wc5awE2bX2zx3eflbyv8U9xbE/jVKAm425eRoOVv/VseGZsqi3A3SykckSpF4wSROkbQPvbQFn8EsA==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.9.0': resolution: {integrity: sha512-/8JFZ/SnuDr1lLEVsxsuVwrsGquTvT51RZGvyDB/dOK3oYK2UqeXzgeyq6Otp8FZXQcEYqJwxb9v+gtdXn03eQ==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.9.0': resolution: {integrity: sha512-FkJjybtrl+rajTw4loI3L6YqSOpeZfDls4SstL/5lsP2bka9TiHUjgMBjygeZEis1oC8LfJTS8FSgpKPaQx2tQ==} cpu: [ppc64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.9.0': resolution: {integrity: sha512-w/NZfHNeDusbqSZ8r/hp8iL4S39h4+vQMc9/vvzuIKMWKppyUGKm3IST0Qv0aOZ1rzIbl9SrDeIqK86ZpUK37w==} cpu: [riscv64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.9.0': resolution: {integrity: sha512-bEPBosut8/8KQbUixPry8zg/fOzVOWyvwzOfz0C0Rw6dp+wIBseyiHKjkcSyZKv/98edrbMknBaMNJfA/UEdqw==} cpu: [riscv64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.9.0': resolution: {integrity: sha512-LDtMT7moE3gK753gG4pc31AAqGUC86j3AplaFusc717EUGF9ZFJ356sdQzzZzkBk1XzMdxFyZ4f/i35NKM/lFA==} cpu: [s390x] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.9.0': resolution: {integrity: sha512-WmFd5KINHIXj8o1mPaT8QRjA9HgSXhN1gl9Da4IZihARihEnOylu4co7i/yeaIpcfsI6sYs33cNZKyHYDh0lrA==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.9.0': resolution: {integrity: sha512-CYuXbANW+WgzVRIl8/QvZmDaZxrqvOldOwlbUjIM4pQ46FJ0W5cinJ/Ghwa/Ng1ZPMJMk1VFdsD/XwmCGIXBWg==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.9.0': resolution: {integrity: sha512-6Rp2WH0OoitMYR57Z6VE8Y6corX8C6QEMWLgOV6qXiJIeZ1F9WGXY/yQ8yDC4iTraotyLOeJ2Asea0urWj2fKQ==} @@ -4480,6 +4477,9 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + nprogress@0.2.0: + resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -7762,6 +7762,8 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/nprogress@0.2.3': {} + '@types/parse-json@4.0.2': {} '@types/prettier@2.7.3': {} @@ -11414,6 +11416,8 @@ snapshots: dependencies: path-key: 3.1.1 + nprogress@0.2.0: {} + nth-check@2.1.1: dependencies: boolbase: 1.0.0 diff --git a/src/app/globals.css b/src/app/globals.css index ba4907b..ae2660d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,6 +2,43 @@ @tailwind components; @tailwind utilities; +/* NProgress 进度条样式 */ +#nprogress { + pointer-events: none; +} + +#nprogress .bar { + background: #3b82f6; + position: fixed; + z-index: 9999; + top: 0; + left: 0; + width: 100%; + height: 3px; + box-shadow: 0 0 10px rgba(59, 130, 246, 0.8), 0 0 5px rgba(59, 130, 246, 0.6); +} + +#nprogress .peg { + display: block; + position: absolute; + right: 0px; + width: 100px; + height: 100%; + box-shadow: 0 0 10px rgba(59, 130, 246, 0.8), 0 0 5px rgba(59, 130, 246, 0.6); + opacity: 1.0; + transform: rotate(3deg) translate(0px, -4px); +} + +/* 暗色模式下的进度条样式 */ +.dark #nprogress .bar { + background: #60a5fa; + box-shadow: 0 0 10px rgba(96, 165, 250, 0.8), 0 0 5px rgba(96, 165, 250, 0.6); +} + +.dark #nprogress .peg { + box-shadow: 0 0 10px rgba(96, 165, 250, 0.8), 0 0 5px rgba(96, 165, 250, 0.6); +} + @layer utilities { .scrollbar-hide { -ms-overflow-style: none; /* IE and Edge */ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b5f4ce6..c3ffe9e 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -16,6 +16,7 @@ import { DownloadProvider } from '../contexts/DownloadContext'; import { DownloadBubble } from '../components/DownloadBubble'; import { DownloadPanel } from '../components/DownloadPanel'; import { DanmakuCacheCleanup } from '../components/DanmakuCacheCleanup'; +import TopProgressBar from '../components/TopProgressBar'; const inter = Inter({ subsets: ['latin'] }); export const dynamic = 'force-dynamic'; @@ -194,6 +195,7 @@ export default async function RootLayout({ enableSystem disableTransitionOnChange > + diff --git a/src/components/TopProgressBar.tsx b/src/components/TopProgressBar.tsx new file mode 100644 index 0000000..0e781e5 --- /dev/null +++ b/src/components/TopProgressBar.tsx @@ -0,0 +1,117 @@ +'use client'; + +import { useEffect, useRef } from 'react'; +import { usePathname, useSearchParams, useRouter } from 'next/navigation'; +import NProgress from 'nprogress'; + +// 创建全局钩子来拦截 router +let globalRouterRef: any = null; + +export default function TopProgressBar() { + const pathname = usePathname(); + const searchParams = useSearchParams(); + const router = useRouter(); + const isNavigatingRef = useRef(false); + + useEffect(() => { + // 配置 NProgress + NProgress.configure({ + showSpinner: false, + trickleSpeed: 200, + minimum: 0.08, + easing: 'ease', + speed: 200, + }); + + // 保存原始的 router 方法 + globalRouterRef = router; + const originalPush = router.push; + const originalReplace = router.replace; + const originalBack = router.back; + const originalForward = router.forward; + + // 拦截 router.push + router.push = function (...args: any[]) { + isNavigatingRef.current = true; + NProgress.start(); + return originalPush.apply(this, args); + }; + + // 拦截 router.replace + router.replace = function (...args: any[]) { + isNavigatingRef.current = true; + NProgress.start(); + return originalReplace.apply(this, args); + }; + + // 拦截 router.back + router.back = function () { + isNavigatingRef.current = true; + NProgress.start(); + return originalBack.apply(this); + }; + + // 拦截 router.forward + router.forward = function () { + isNavigatingRef.current = true; + NProgress.start(); + return originalForward.apply(this); + }; + + // 监听所有链接点击事件 + const handleAnchorClick = (event: MouseEvent) => { + const target = event.target as HTMLElement; + const anchor = target.closest('a'); + + if (anchor && anchor.href) { + const currentUrl = window.location.href; + const targetUrl = anchor.href; + + if (targetUrl !== currentUrl && !anchor.target && !anchor.download) { + const currentOrigin = window.location.origin; + try { + const targetOrigin = new URL(targetUrl, currentOrigin).origin; + if (currentOrigin === targetOrigin) { + isNavigatingRef.current = true; + NProgress.start(); + } + } catch (e) { + // URL 解析失败,忽略 + } + } + } + }; + + // 监听浏览器前进后退按钮 + const handlePopState = () => { + isNavigatingRef.current = true; + NProgress.start(); + }; + + document.addEventListener('click', handleAnchorClick, true); + window.addEventListener('popstate', handlePopState); + + return () => { + // 恢复原始方法 + if (globalRouterRef) { + globalRouterRef.push = originalPush; + globalRouterRef.replace = originalReplace; + globalRouterRef.back = originalBack; + globalRouterRef.forward = originalForward; + } + + document.removeEventListener('click', handleAnchorClick, true); + window.removeEventListener('popstate', handlePopState); + }; + }, [router]); + + useEffect(() => { + // 页面路径变化时,表示页面已加载完成,结束进度条 + if (isNavigatingRef.current) { + NProgress.done(); + isNavigatingRef.current = false; + } + }, [pathname, searchParams]); + + return null; +}