From 20276f1a86f4603288038b8626529b0a08014938 Mon Sep 17 00:00:00 2001 From: lsy Date: Tue, 13 May 2025 12:26:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96swup=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=B8=85=E7=90=86=E4=BA=8B=E4=BB=B6=E5=A4=84=E7=90=86?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E6=94=B9=E8=BF=9B=E4=B8=BB=E9=A2=98?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E5=92=8C=E6=90=9C=E7=B4=A2=E9=9D=A2=E6=9D=BF?= =?UTF-8?q?=E5=85=B3=E9=97=AD=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 141 ++++++++-------- package.json | 1 + src/components/Breadcrumb.astro | 162 +++++++++++------- src/components/Header.astro | 274 ++++++++++++++++++++----------- src/components/Layout.astro | 56 ++++--- src/components/Search.tsx | 112 +++++++++++-- src/components/ThemeToggle.astro | 215 +++++++++++++----------- src/pages/articles/[...id].astro | 52 +++--- src/scripts/swup-init.js | 162 +++++++----------- src/styles/global.css | 1 + src/styles/swup-transitions.css | 36 ++++ 11 files changed, 713 insertions(+), 499 deletions(-) create mode 100644 src/styles/swup-transitions.css diff --git a/package-lock.json b/package-lock.json index 172461c..8606ac7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@swup/head-plugin": "^2.3.1", "@swup/preload-plugin": "^3.2.11", "@swup/progress-plugin": "^3.2.0", + "@swup/scripts-plugin": "^2.1.0", "@tailwindcss/vite": "^4.1.4", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", @@ -310,23 +311,23 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.26.8", - "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.26.8.tgz", - "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "version": "7.27.2", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.27.2.tgz", + "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -372,13 +373,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.27.0", - "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.27.0.tgz", - "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.27.1.tgz", + "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.0", - "@babel/types": "^7.27.0", + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -388,13 +389,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.0", - "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz", - "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==", + "version": "7.27.2", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.26.8", - "@babel/helper-validator-option": "^7.25.9", + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -413,27 +414,27 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", + "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -443,36 +444,36 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.26.5", - "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", - "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -492,12 +493,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.0", - "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.27.0.tgz", - "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "version": "7.27.2", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.0" + "@babel/types": "^7.27.1" }, "bin": { "parser": "bin/babel-parser.js" @@ -537,30 +538,30 @@ } }, "node_modules/@babel/template": { - "version": "7.27.0", - "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.27.0.tgz", - "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "version": "7.27.2", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.27.0", - "@babel/types": "^7.27.0" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.27.0", - "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.27.0.tgz", - "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.27.1.tgz", + "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.27.0", - "@babel/parser": "^7.27.0", - "@babel/template": "^7.27.0", - "@babel/types": "^7.27.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -569,13 +570,13 @@ } }, "node_modules/@babel/types": { - "version": "7.27.0", - "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.27.0.tgz", - "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -2708,6 +2709,18 @@ "swup": "^4.0.0" } }, + "node_modules/@swup/scripts-plugin": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/@swup/scripts-plugin/-/scripts-plugin-2.1.0.tgz", + "integrity": "sha512-JSMFsFCN9gn4q3m1Ccv0gq3gwRoZl6UGALOQO3OeQ8wOIq9vPC5dcUD3CMBuaPanksjR4GC8ZoukIjHrlT52fg==", + "license": "MIT", + "dependencies": { + "@swup/plugin": "^4.0.0" + }, + "peerDependencies": { + "swup": "^4.2.0" + } + }, "node_modules/@tailwindcss/node": { "version": "4.1.4", "resolved": "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.1.4.tgz", diff --git a/package.json b/package.json index 4e94f09..474b0c2 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@swup/head-plugin": "^2.3.1", "@swup/preload-plugin": "^3.2.11", "@swup/progress-plugin": "^3.2.0", + "@swup/scripts-plugin": "^2.1.0", "@tailwindcss/vite": "^4.1.4", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", diff --git a/src/components/Breadcrumb.astro b/src/components/Breadcrumb.astro index 8feb94d..2d8f698 100644 --- a/src/components/Breadcrumb.astro +++ b/src/components/Breadcrumb.astro @@ -208,33 +208,6 @@ const breadcrumbs: Breadcrumb[] = pathSegments return handler; } - // 统一的清理函数,执行完整清理并自销毁 - function selfDestruct() { - // 1. 移除所有普通事件监听器 - allListeners.forEach(({ element, eventType, handler, options }) => { - try { - element.removeEventListener(eventType, handler, options); - } catch (err) { - console.error(`[面包屑]移除事件监听器出错:`, err); - } - }); - - // 清空监听器数组 - allListeners.length = 0; - - // 2. 最后移除清理事件监听器自身 - cleanupListeners.forEach(({ element, eventType, handler, options }) => { - try { - element.removeEventListener(eventType, handler, options); - } catch (err) { - console.error(`[面包屑]移除清理监听器出错:`, err); - } - }); - - // 清空清理监听器数组 - cleanupListeners.length = 0; - } - // 获取当前URL路径(与导航栏保持一致) function getCurrentPath() { const path = window.location.pathname; @@ -556,9 +529,15 @@ const breadcrumbs: Breadcrumb[] = pathSegments // 添加路径变化检测和自动更新 function setupPathChangeDetection() { let lastPathChecked = getCurrentPath(); + let isCleaningUp = false; // 标记是否在清理中 // 统一的路径变化处理函数 function handlePathChange() { + // 如果正在清理中,不要继续更新 + if (isCleaningUp) { + return; + } + const currentPath = getCurrentPath(); if (currentPath !== lastPathChecked) { // 更新面包屑 @@ -571,81 +550,138 @@ const breadcrumbs: Breadcrumb[] = pathSegments // 监听hashchange事件 - 当URL的hash部分改变时触发 addListener(window, 'hashchange', () => { + if (isCleaningUp) return; handlePathChange(); }); - // 为所有导航链接添加点击拦截 - addListener(document, 'click', (e) => { - // 检查点击的是否为站内导航链接 - const link = e.target.closest('a'); - if (link && link.host === window.location.host && !e.ctrlKey && !e.metaKey) { - // 延迟检查以确保导航已完成 - setTimeout(handlePathChange, 50); - } + // 监听popstate事件 + addListener(window, 'popstate', () => { + if (isCleaningUp) return; + handlePathChange(); }); - // 监听history API的方法 - const originalPushState = window.history.pushState; - const originalReplaceState = window.history.replaceState; + // 监听页面转换事件,设置清理标志 + addListener(document, 'page-transition', () => { + // 设置清理标志,阻止后续的handlePathChange调用 + isCleaningUp = true; + }); + + // 监听页面内容替换完成后更新面包屑 + addListener(document, 'swup:contentReplaced', () => { + if (isCleaningUp) return; + setTimeout(() => { + // 在DOM更新后,重新计算面包屑 + handlePathChange(); + }, 10); + }); + + // 保存原始history方法 + originalPushState = window.history.pushState; + originalReplaceState = window.history.replaceState; // 重写pushState window.history.pushState = function() { + // 如果正在清理中,不要执行被重写的方法,直接调用原始方法 + if (isCleaningUp) { + return originalPushState.apply(this, arguments); + } + originalPushState.apply(this, arguments); handlePathChange(); }; // 重写replaceState window.history.replaceState = function() { + // 如果正在清理中,不要执行被重写的方法,直接调用原始方法 + if (isCleaningUp) { + return originalReplaceState.apply(this, arguments); + } + originalReplaceState.apply(this, arguments); handlePathChange(); }; + } + + // 统一的清理函数,执行完整清理并自销毁 + function selfDestruct() { - // 添加到清理列表 - addListener(window, 'beforeunload', () => { - // 恢复原始history方法 - window.history.pushState = originalPushState; - window.history.replaceState = originalReplaceState; - }, { once: true }); - - // 监听popstate事件 - addListener(window, 'popstate', () => { - setTimeout(() => { - handlePathChange(); - }, 50); + // 1. 移除所有普通事件监听器 + allListeners.forEach(({ element, eventType, handler, options }) => { + try { + element.removeEventListener(eventType, handler, options); + } catch (err) { + console.error(`[面包屑] 移除事件监听器出错:`, err); + } }); + + // 清空监听器数组 + allListeners.length = 0; + + // 恢复原始的history方法 + if (originalPushState && originalReplaceState) { + try { + // 立即恢复原始方法,确保后续pushState调用直接使用原始方法 + const tempOriginalPushState = originalPushState; + const tempOriginalReplaceState = originalReplaceState; + + window.history.pushState = tempOriginalPushState; + window.history.replaceState = tempOriginalReplaceState; + + // 清空引用 + originalPushState = null; + originalReplaceState = null; + + } catch (err) { + console.error('[面包屑] 恢复History方法失败:', err); + } + } else { + console.warn('[面包屑] 找不到原始History方法引用'); + } + + // 2. 最后移除清理事件监听器自身 + cleanupListeners.forEach(({ element, eventType, handler, options }) => { + try { + element.removeEventListener(eventType, handler, options); + } catch (err) { + console.error(`[面包屑] 移除清理监听器出错:`, err); + } + }); + + // 清空清理监听器数组 + cleanupListeners.length = 0; + } // 注册清理事件,并保存引用 function registerCleanupEvents() { - // 创建一次性事件处理函数 + // 创建事件处理函数 + const pageTransitionHandler = (event) => { + selfDestruct(); + }; + + // Astro视图转换事件 - 保留这个作为后备 const beforeSwapHandler = () => { selfDestruct(); }; + // 页面卸载事件 - 保留这个作为后备 const beforeUnloadHandler = () => { selfDestruct(); }; - // 添加清理事件监听器并保存引用 + // 只监听统一的页面转换事件 + document.addEventListener("page-transition", pageTransitionHandler); + + // 保留Astro和页面卸载事件作为后备 document.addEventListener("astro:before-swap", beforeSwapHandler, { once: true }); window.addEventListener("beforeunload", beforeUnloadHandler, { once: true }); - // 如果页面使用swup,也注册swup相关的清理事件 - if (typeof window.swup !== 'undefined') { - document.addEventListener("swup:willReplaceContent", beforeSwapHandler, { once: true }); - } - // 保存清理事件引用,用于完全销毁 cleanupListeners.push( + { element: document, eventType: "page-transition", handler: pageTransitionHandler, options: null }, { element: document, eventType: "astro:before-swap", handler: beforeSwapHandler, options: { once: true } }, { element: window, eventType: "beforeunload", handler: beforeUnloadHandler, options: { once: true } } ); - - if (typeof window.swup !== 'undefined') { - cleanupListeners.push( - { element: document, eventType: "swup:willReplaceContent", handler: beforeSwapHandler, options: { once: true } } - ); - } } // 设置返回按钮功能 diff --git a/src/components/Header.astro b/src/components/Header.astro index 7fe5b03..489856d 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -153,7 +153,7 @@ const navSelectorClassName = "mr-4";
- +
@@ -343,6 +343,15 @@ const navSelectorClassName = "mr-4"; // 单独保存清理事件的监听器引用 const cleanupListeners = []; + + // 内部状态管理 + const state = { + isCleaningUp: false, + lastPathLogged: '', + originalPushState: window.history.pushState, + originalReplaceState: window.history.replaceState + }; + // 获取当前URL路径 function getCurrentPath() { const path = window.location.pathname; @@ -361,8 +370,30 @@ const navSelectorClassName = "mr-4"; return handler; } + // 更新高亮背景的函数声明(提前声明) + let updateHighlights; + + // 初始化当前页面的激活状态函数声明(提前声明) + let initActiveState; + + // 统一的路径变化处理函数 + function handlePathChange() { + if (state.isCleaningUp) return; + + const currentPath = getCurrentPath(); + if (currentPath !== state.lastPathLogged) { + // 主动调用初始化和更新高亮 + initActiveState(); + updateHighlights(true); + + state.lastPathLogged = currentPath; + } + } + // 统一的清理函数,执行完整清理并自销毁 function selfDestruct() { + // 标记清理状态 + state.isCleaningUp = true; // 1. 移除所有普通事件监听器 allListeners.forEach(({ element, eventType, handler, options }) => { @@ -376,7 +407,17 @@ const navSelectorClassName = "mr-4"; // 清空监听器数组 allListeners.length = 0; - // 2. 最后移除清理事件监听器自身 + // 2. 恢复原始history方法 + if (state.originalPushState && state.originalReplaceState) { + try { + window.history.pushState = state.originalPushState; + window.history.replaceState = state.originalReplaceState; + } catch (err) { + console.error('恢复History方法失败:', err); + } + } + + // 3. 最后移除清理事件监听器自身 cleanupListeners.forEach(({ element, eventType, handler, options }) => { try { element.removeEventListener(eventType, handler, options); @@ -387,29 +428,30 @@ const navSelectorClassName = "mr-4"; // 清空清理监听器数组 cleanupListeners.length = 0; - } // 注册清理事件,并保存引用 function registerCleanupEvents() { - // 创建一次性事件处理函数 - const beforeSwapHandler = () => { - selfDestruct(); - }; + // 所有可能触发清理的事件 + const cleanupEvents = [ + { element: document, eventType: "astro:before-swap", options: { once: true } }, + { element: window, eventType: "beforeunload", options: { once: true } }, + { element: document, eventType: "page-transition", options: null } + ]; - const beforeUnloadHandler = () => { - selfDestruct(); - }; - - // 添加清理事件监听器并保存引用 - document.addEventListener("astro:before-swap", beforeSwapHandler, { once: true }); - window.addEventListener("beforeunload", beforeUnloadHandler, { once: true }); - - // 保存清理事件引用,用于完全销毁 - cleanupListeners.push( - { element: document, eventType: "astro:before-swap", handler: beforeSwapHandler, options: { once: true } }, - { element: window, eventType: "beforeunload", handler: beforeUnloadHandler, options: { once: true } } - ); + // 为每个事件添加监听器 + cleanupEvents.forEach(({ element, eventType, options }) => { + // 添加监听器 + element.addEventListener(eventType, selfDestruct, options); + + // 保存引用以便后续清理 + cleanupListeners.push({ + element, + eventType, + handler: selfDestruct, + options + }); + }); } // 创建一个共享的计算高亮位置的函数 @@ -633,8 +675,8 @@ const navSelectorClassName = "mr-4"; // 注册清理事件 registerCleanupEvents(); - // 更新高亮背景 - 提升到全局作用域 - function updateHighlights(immediate = false) { + // 更新高亮背景 - 提升到顶部作用域 + updateHighlights = function(immediate = false) { requestAnimationFrame(() => { const highlightPositions = calculateHighlightPositions({ navSelector, @@ -645,10 +687,10 @@ const navSelectorClassName = "mr-4"; highlightPositions.applyPositions(immediate); } }); - } + }; - // 初始化当前页面的激活状态 - 提升到全局作用域 - function initActiveState() { + // 初始化当前页面的激活状态 - 提升到顶部作用域 + initActiveState = function() { // 获取当前路径 const currentPath = getCurrentPath(); @@ -738,7 +780,7 @@ const navSelectorClassName = "mr-4"; // 计算正确高亮位置 updateHighlights(true); - } + }; // DOM加载完成后执行初始化 function initNavigation() { @@ -752,27 +794,15 @@ const navSelectorClassName = "mr-4"; // 主要设置函数 function setupNavigation() { + // 初始化路径记录 + state.lastPathLogged = getCurrentPath(); + // 设置桌面导航 setupNavSelector(); // 设置移动端导航 setupMobileNav(); - // 记录最后一次路径值 - let lastPathLogged = getCurrentPath(); - - // 统一的路径变化处理函数 - function handlePathChange() { - const currentPath = getCurrentPath(); - if (currentPath !== lastPathLogged) { - // 主动调用初始化和更新高亮 - initActiveState(); - updateHighlights(true); - - lastPathLogged = currentPath; - } - } - // 监听hashchange事件 - 当URL的hash部分改变时触发 addListener(window, 'hashchange', () => { handlePathChange(); @@ -788,28 +818,38 @@ const navSelectorClassName = "mr-4"; } }); - // 监听history API的方法 - const originalPushState = window.history.pushState; - const originalReplaceState = window.history.replaceState; + // 设置History API监控 + setupHistoryMonitoring(); + } + + // 监听history API的方法 + function setupHistoryMonitoring() { + // 保存初始状态 - 确保引用正确 + state.originalPushState = window.history.pushState; + state.originalReplaceState = window.history.replaceState; + + // 监听页面转换事件来设置清理标记 + addListener(document, 'page-transition', () => { + state.isCleaningUp = true; + }); // 重写pushState window.history.pushState = function() { - originalPushState.apply(this, arguments); + if (state.isCleaningUp) { + return state.originalPushState.apply(this, arguments); + } + state.originalPushState.apply(this, arguments); handlePathChange(); }; // 重写replaceState window.history.replaceState = function() { - originalReplaceState.apply(this, arguments); + if (state.isCleaningUp) { + return state.originalReplaceState.apply(this, arguments); + } + state.originalReplaceState.apply(this, arguments); handlePathChange(); }; - - // 添加到清理列表 - addListener(window, 'beforeunload', () => { - // 恢复原始history方法 - window.history.pushState = originalPushState; - window.history.replaceState = originalReplaceState; - }, { once: true }); } // 初始化导航选择器 @@ -1459,54 +1499,32 @@ const navSelectorClassName = "mr-4"; }); } - // 为移动端菜单的所有链接添加点击事件,点击后关闭菜单 - const mobileMenuLinks = document.querySelectorAll('#mobile-menu a'); - mobileMenuLinks.forEach(link => { - addListener(link, 'click', (e) => { - // 如果使用客户端路由(如swup或Astro View Transitions),阻止默认行为 - const hasSwup = typeof window.swup !== 'undefined'; - const hasViewTransitions = typeof document.startViewTransition !== 'undefined'; + // 为主题切换容器添加点击事件 + const themeToggleContainer = document.getElementById('theme-toggle-container'); + if (themeToggleContainer) { + addListener(themeToggleContainer, 'click', (e) => { + // 阻止事件冒泡,防止多次触发 + e.stopPropagation(); - if (hasSwup || hasViewTransitions) { - e.preventDefault(); + // 获取容器内的主题切换按钮 + const themeToggleButton = themeToggleContainer.querySelector('#theme-toggle-button'); + + // 如果找到按钮,通过创建自定义事件来传递坐标 + if (themeToggleButton) { + // 创建自定义点击事件并携带原始事件的坐标信息 + const clickEvent = new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window, + clientX: e.clientX, + clientY: e.clientY + }); - // 获取链接地址 - const href = link.getAttribute('href'); - if (!href) return; - - // 先关闭菜单 - closeMobileMenu(); - closeMobileSearch(); - - // 延迟导航,确保菜单关闭动画完成 - setTimeout(() => { - // 使用适当的导航方法 - if (hasSwup) { - try { - window.swup.navigate(href); - } catch (err) { - try { - window.swup.loadPage({ url: href }); - } catch (err2) { - window.location.href = href; - } - } - } else if (hasViewTransitions) { - document.startViewTransition(() => { - window.location.href = href; - }); - } else { - window.location.href = href; - } - }, 50); - } else { - // 普通链接导航,浏览器会自动处理跳转 - // 但仍然需要关闭菜单 - closeMobileMenu(); - closeMobileSearch(); + // 使用自定义事件触发按钮点击 + themeToggleButton.dispatchEvent(clickEvent); } }); - }); + } // 为Astro View Transitions添加事件处理 addListener(document, 'astro:page-load', () => { @@ -1525,11 +1543,71 @@ const navSelectorClassName = "mr-4"; }, 50); }); + // 为移动端菜单链接添加点击事件,点击后关闭菜单 + function setupMobileMenuLinks() { + const mobileMenuLinks = document.querySelectorAll('#mobile-menu a'); + mobileMenuLinks.forEach(link => { + // 避免重复添加事件监听器 + addListener(link, 'click', (e) => { + // 如果是站内链接且不是控制键或Meta键点击(新窗口打开) + if (link.host === window.location.host && !e.ctrlKey && !e.metaKey) { + // 延迟关闭导航栏,确保页面跳转感知和点击体验更流畅 + setTimeout(() => { + closeMobileMenu(); + }, 50); + } + }); + }); + } + // 初始调用一次设置子菜单切换按钮和更新高亮状态 setupMobileSubmenuToggles(); updateMobileMenuHighlight(); + setupMobileMenuLinks(); // 添加对移动端链接的点击监听 - return updateMobileMenuHighlight; + // 当菜单显示状态变化时重新绑定链接点击事件 + addListener(mobileMenuButton, 'click', () => { + // 确保每次打开菜单时重新设置链接点击事件 + setTimeout(() => { + setupMobileMenuLinks(); + }, 50); + }); + + // 添加点击空白区域关闭菜单和搜索面板的事件监听 + addListener(document, 'click', (e) => { + // 获取事件目标 + const target = e.target; + + // 处理移动端菜单内的链接点击 + const menuLink = target.closest('#mobile-menu a'); + if (menuLink && !e.ctrlKey && !e.metaKey) { + // 延迟关闭导航栏 + setTimeout(() => { + closeMobileMenu(); + }, 50); + return; // 如果是菜单链接点击,处理完成后返回 + } + + // 检查移动端菜单是否打开,点击空白区域关闭 + if (mobileMenu && !mobileMenu.classList.contains('hidden')) { + // 检查点击是否在菜单区域外 + // 菜单按钮和菜单内容都应该排除 + if (!mobileMenu.contains(target) && + mobileMenuButton && !mobileMenuButton.contains(target)) { + closeMobileMenu(); + } + } + + // 检查移动端搜索面板是否打开,点击空白区域关闭 + if (mobileSearch && !mobileSearch.classList.contains('hidden')) { + // 检查点击是否在搜索面板区域外 + // 搜索按钮和搜索面板内容都应该排除 + if (!mobileSearch.contains(target) && + mobileSearchButton && !mobileSearchButton.contains(target)) { + closeMobileSearch(); + } + } + }); } // 开始初始化 diff --git a/src/components/Layout.astro b/src/components/Layout.astro index 98e62ac..97d2a76 100644 --- a/src/components/Layout.astro +++ b/src/components/Layout.astro @@ -152,38 +152,42 @@ const { // 注册清理事件,并保存引用 function registerCleanupEvents() { - // 创建一次性事件处理函数 - const beforeSwapHandler = () => { + // 创建统一的清理处理函数 + const cleanup = () => { selfDestruct(); }; - const beforeUnloadHandler = () => { - selfDestruct(); - }; + // 定义需要监听的所有清理事件 + const cleanupEventTypes = [ + { element: document, eventType: "astro:before-swap", options: { once: true } }, + { element: window, eventType: "beforeunload", options: { once: true } }, + { element: document, eventType: "astro:before-preparation", options: { once: true } }, + { element: document, eventType: "page-transition", options: { once: true } } + ]; - // 添加清理事件监听器并保存引用 - document.addEventListener("astro:before-swap", beforeSwapHandler, { once: true }); - window.addEventListener("beforeunload", beforeUnloadHandler, { once: true }); + // 注册所有清理事件 + cleanupEventTypes.forEach(({ element, eventType, options }) => { + // 添加事件监听 + element.addEventListener(eventType, cleanup, options); + + // 保存事件引用到清理列表 + cleanupListeners.push({ + element, + eventType, + handler: cleanup, + options + }); + }); - // Astro特有的页面准备事件 - document.addEventListener("astro:before-preparation", beforeSwapHandler, { once: true }); - - // SPA框架可能使用的事件 + // SPA框架可能使用的事件 - 特殊处理 if (typeof document.addEventListener === 'function') { - document.addEventListener("swup:willReplaceContent", beforeSwapHandler, { once: true }); - } - - // 保存清理事件引用,用于完全销毁 - cleanupListeners.push( - { element: document, eventType: "astro:before-swap", handler: beforeSwapHandler, options: { once: true } }, - { element: window, eventType: "beforeunload", handler: beforeUnloadHandler, options: { once: true } }, - { element: document, eventType: "astro:before-preparation", handler: beforeSwapHandler, options: { once: true } } - ); - - if (typeof document.addEventListener === 'function') { - cleanupListeners.push( - { element: document, eventType: "swup:willReplaceContent", handler: beforeSwapHandler, options: { once: true } } - ); + document.addEventListener("swup:willReplaceContent", cleanup, { once: true }); + cleanupListeners.push({ + element: document, + eventType: "swup:willReplaceContent", + handler: cleanup, + options: { once: true } + }); } } diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 4210753..2a47adb 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -449,7 +449,50 @@ const Search: React.FC = ({ } }; - // 修改处理键盘导航的函数,增加上下箭头键切换建议 + // 添加关闭移动端搜索面板的函数 + const closeMobileSearchPanel = useCallback(() => { + // 查找移动端搜索面板 + const mobileSearch = document.getElementById('mobile-search'); + if (mobileSearch && !mobileSearch.classList.contains('hidden')) { + // 关闭移动端搜索面板 + mobileSearch.classList.add('hidden'); + + // 更新按钮状态 + const searchButton = document.getElementById('mobile-search-button'); + if (searchButton) { + searchButton.setAttribute('aria-expanded', 'false'); + } + } + }, []); + + // 修改navigateToUrl函数,确保在跳转前关闭界面元素 + const navigateToUrl = useCallback((url: string) => { + // 先关闭所有相关UI元素 + setShowResults(false); + setInlineSuggestion(prev => ({ ...prev, visible: false })); + closeMobileSearchPanel(); + + // 使用短暂延迟确保UI状态先更新 + setTimeout(() => { + // 创建一个临时链接元素 + const linkEl = document.createElement('a'); + linkEl.href = url; + + // 设置导航同源属性,确保使用内部导航机制 + linkEl.setAttribute('data-astro-prefetch', 'true'); + + // 添加到DOM中并触发点击 + document.body.appendChild(linkEl); + linkEl.click(); + + // 清理临时元素 + setTimeout(() => { + document.body.removeChild(linkEl); + }, 100); + }, 10); // 很短的延迟,只是让UI状态更新 + }, [closeMobileSearchPanel]); + + // 修改handleKeyDown函数中的回车键处理逻辑 const handleKeyDown = (e: React.KeyboardEvent) => { // Tab键处理内联建议补全 @@ -515,6 +558,12 @@ const Search: React.FC = ({ if (inlineSuggestion.visible && inlineSuggestion.text) { const suggestionText = inlineSuggestion.text; + // 立即更新搜索框内容和状态 + setQuery(suggestionText); + if (searchInputRef.current) { + searchInputRef.current.value = suggestionText; + } + // 先检查当前搜索结果中是否有完全匹配的结果 const exactMatchForSuggestion = allItems.find(item => item.title.replace(/<\/?mark>/g, '').toLowerCase() === suggestionText.toLowerCase() @@ -524,12 +573,14 @@ const Search: React.FC = ({ // 如果有完全匹配的结果,关闭搜索结果面板并导航 setShowResults(false); setInlineSuggestion(prev => ({ ...prev, visible: false })); - window.location.href = exactMatchForSuggestion.url; + // 关闭移动端搜索面板 + closeMobileSearchPanel(); + // 使用新的导航函数替代直接修改location + navigateToUrl(exactMatchForSuggestion.url); return; } // 没有完全匹配,先补全建议并导航到第一个结果 - // completeInlineSuggestion会自动处理关闭搜索结果的逻辑 completeInlineSuggestion(true); // 传入true表示需要导航到第一个结果 return; } @@ -544,14 +595,20 @@ const Search: React.FC = ({ // 找到完全匹配,关闭搜索结果面板并导航 setShowResults(false); setInlineSuggestion(prev => ({ ...prev, visible: false })); - window.location.href = exactMatch.url; + // 关闭移动端搜索面板 + closeMobileSearchPanel(); + // 使用新的导航函数替代直接修改location + navigateToUrl(exactMatch.url); return; } // 如果没有完全匹配,但有搜索结果,关闭搜索结果面板并进入第一个结果 setShowResults(false); setInlineSuggestion(prev => ({ ...prev, visible: false })); - window.location.href = allItems[0].url; + // 关闭移动端搜索面板 + closeMobileSearchPanel(); + // 使用新的导航函数替代直接修改location + navigateToUrl(allItems[0].url); return; } @@ -688,7 +745,10 @@ const Search: React.FC = ({ // 如果需要导航到第一个结果,并且有结果 if (shouldNavigateToFirstResult && result.items.length > 0) { - window.location.href = result.items[0].url; + // 关闭移动端搜索面板 + closeMobileSearchPanel(); + // 使用新的导航函数替代直接修改location + navigateToUrl(result.items[0].url); } } catch (err) { // 检查组件是否仍然挂载 @@ -708,9 +768,9 @@ const Search: React.FC = ({ // 保存建议文本 const textToComplete = inlineSuggestion.text; - // 直接更新DOM和状态 + // 立即更新搜索框内容和状态 + setQuery(textToComplete); if (searchInputRef.current) { - // 立即更新输入框值 searchInputRef.current.value = textToComplete; } @@ -725,11 +785,11 @@ const Search: React.FC = ({ suggestionText: "" }); - // 更新React状态 - setQuery(textToComplete); - // 如果需要导航到第一个结果,保持结果面板显示状态,立即执行搜索 if (shouldNavigateToFirstResult) { + // 立即关闭搜索面板,然后执行搜索 + setShowResults(false); + closeMobileSearchPanel(); performSearch(textToComplete, false, true); } else { // 如果不需要导航,关闭搜索结果面板,但仍然执行搜索以更新结果 @@ -1151,10 +1211,13 @@ const Search: React.FC = ({ { - // 点击搜索结果项时关闭搜索结果面板 - setShowResults(false); - setInlineSuggestion(prev => ({ ...prev, visible: false })); + data-astro-prefetch="hover" + onClick={(e) => { + // 防止默认行为,由我们自己处理导航 + e.preventDefault(); + + // 使用导航函数处理跳转,它会关闭所有面板 + navigateToUrl(item.url); }} >
@@ -1235,6 +1298,25 @@ const Search: React.FC = ({ }; }, []); + // 为搜索组件添加视图切换事件监听 + useEffect(() => { + const handlePageChange = () => { + // 确保在页面切换时关闭所有搜索相关界面 + setShowResults(false); + setInlineSuggestion(prev => ({ ...prev, visible: false })); + closeMobileSearchPanel(); + }; + + // 监听Astro视图转换事件 + document.addEventListener('astro:after-swap', handlePageChange); + document.addEventListener('astro:page-load', handlePageChange); + + return () => { + document.removeEventListener('astro:after-swap', handlePageChange); + document.removeEventListener('astro:page-load', handlePageChange); + }; + }, [closeMobileSearchPanel]); + // 渲染结束 const returnBlock = (
diff --git a/src/components/ThemeToggle.astro b/src/components/ThemeToggle.astro index 3154f17..7b16334 100644 --- a/src/components/ThemeToggle.astro +++ b/src/components/ThemeToggle.astro @@ -31,6 +31,7 @@ import "../styles/theme-toggle.css"; tabindex="0" data-transition-mode={transitionMode} data-transition-duration={transitionDuration} + data-theme-transitioning="false" > { + setTransitioning(button, false); + }); - // 5. 移除普通事件监听器 + // 7. 移除普通事件监听器 allListeners.forEach(({ element, eventType, handler, options }) => { try { element.removeEventListener(eventType, handler, options); @@ -171,7 +210,7 @@ import "../styles/theme-toggle.css"; // 清空监听器数组 allListeners.length = 0; - // 6. 最后移除清理事件监听器自身 + // 8. 最后移除清理事件监听器自身 cleanupListeners.forEach(({ element, eventType, handler, options }) => { try { element.removeEventListener(eventType, handler, options); @@ -186,24 +225,31 @@ import "../styles/theme-toggle.css"; // 注册清理事件,并保存引用 function registerCleanupEvents() { - // 创建一次性事件处理函数 - const beforeSwapHandler = () => { + // 创建统一的清理处理函数 + const cleanup = () => { selfDestruct(); }; - const beforeUnloadHandler = () => { - selfDestruct(); - }; + // 定义需要监听的所有清理事件 + const cleanupEventTypes = [ + { element: document, eventType: "astro:before-swap", options: { once: true } }, + { element: window, eventType: "beforeunload", options: { once: true } }, + { element: document, eventType: "page-transition", options: { once: true } } + ]; - // 添加清理事件监听器并保存引用 - document.addEventListener("astro:before-swap", beforeSwapHandler, { once: true }); - window.addEventListener("beforeunload", beforeUnloadHandler, { once: true }); - - // 保存清理事件引用,用于完全销毁 - cleanupListeners.push( - { element: document, eventType: "astro:before-swap", handler: beforeSwapHandler, options: { once: true } }, - { element: window, eventType: "beforeunload", handler: beforeUnloadHandler, options: { once: true } } - ); + // 注册所有清理事件 + cleanupEventTypes.forEach(({ element, eventType, options }) => { + // 添加事件监听 + element.addEventListener(eventType, cleanup, options); + + // 保存事件引用到清理列表 + cleanupListeners.push({ + element, + eventType, + handler: cleanup, + options + }); + }); } // 创建波纹动画元素 @@ -269,16 +315,16 @@ import "../styles/theme-toggle.css"; // 使用View Transitions API创建全屏过渡效果 function createViewTransition(callback, x, y, fromTheme, toTheme, transitionMode) { // 如果已有正在进行的过渡,先取消它 - if (window._themeTransition) { + if (currentTransition) { try { - if (typeof window._themeTransition.skipTransition === 'function') { - window._themeTransition.skipTransition(); + if (typeof currentTransition.skipTransition === 'function') { + currentTransition.skipTransition(); } } catch (e) { // 忽略取消先前过渡的错误 } finally { // 无论成功与否,都清除引用 - window._themeTransition = null; + currentTransition = null; } } @@ -416,7 +462,7 @@ import "../styles/theme-toggle.css"; const transition = document.startViewTransition(safeCallback); // 存储transition引用以便清理 - window._themeTransition = transition; + currentTransition = transition; // 生成动画需要的SVG资源 const gradientOffset = 0.75; @@ -703,7 +749,7 @@ import "../styles/theme-toggle.css"; .then(() => { // 移除主题过渡标记类 document.documentElement.classList.remove('theme-transition-active'); - window._themeTransition = null; + currentTransition = null; }) .catch(error => { if (error.name !== 'AbortError') { @@ -713,7 +759,7 @@ import "../styles/theme-toggle.css"; document.documentElement.classList.remove('theme-transition-active'); // 清除引用 - window._themeTransition = null; + currentTransition = null; }); } catch (error) { console.error(`主题切换错误:`, error); @@ -735,14 +781,6 @@ import "../styles/theme-toggle.css"; return; } - // 防抖配置 - 使用前面定义的动态计算值 - let lastClickTime = 0; - let transitioning = false; - - // 将transitioning状态暴露给全局,便于其他脚本检查 - window._themeTransitioning = false; - window._lastThemeToggleClickTime = 0; - // 状态清理函数 - 在页面切换、错误和超时情况下调用 const resetThemeToggleState = () => { if (transitionTimeout) { @@ -750,8 +788,13 @@ import "../styles/theme-toggle.css"; transitionTimeout = null; } - transitioning = false; - window._themeTransitioning = false; + // 重置全局切换时间状态 + lastThemeToggleTime = 0; + + // 重置所有按钮的状态 + themeToggleButtons.forEach(button => { + setTransitioning(button, false); + }); // 移除所有残留的波纹效果 document.querySelectorAll(".theme-ripple").forEach(ripple => { @@ -784,10 +827,11 @@ import "../styles/theme-toggle.css"; // 不处理未冒泡到document的事件 if (!e || !e.target) return; - // 如果点击事件来自于主题切换相关元素,且正在过渡中,拦截所有后续处理 - if (window._themeTransitioning && - (e.target.closest('#theme-toggle-button') || - e.target.closest('#theme-toggle-container'))) { + // 获取点击的按钮元素 + const button = e.target.closest('#theme-toggle-button'); + + // 如果点击按钮,且按钮正在过渡中或在冷却期内,拦截后续处理 + if (button && (isTransitioning(button) || !canToggleTheme())) { e.stopPropagation(); e.preventDefault(); } @@ -825,17 +869,6 @@ import "../styles/theme-toggle.css"; } }; - // 防抖函数 - 确保在指定时间内只执行一次 - const isThrottled = () => { - const now = Date.now(); - if (transitioning || now - lastClickTime < DEBOUNCE_TIME) { - return true; - } - lastClickTime = now; - window._lastThemeToggleClickTime = now; - return false; - }; - // 为所有触发点提供统一的处理函数 const handleThemeToggle = (e, targetButton) => { // 防止事件默认行为和冒泡,无论如何都要先阻止 @@ -845,24 +878,37 @@ import "../styles/theme-toggle.css"; if (e && typeof e.stopPropagation === 'function') { e.stopPropagation(); } - - // 第1层保护:全局事件防抖 - if (isThrottled()) { + + // 确保有有效的按钮参数,必要时从事件中获取 + if (!targetButton && e) { + targetButton = e.target.closest('#theme-toggle-button'); + } + + // 确保有按钮可以操作 + if (!targetButton) { return; } - // 第2层保护:transitioning状态检查 - if (window._themeTransitioning) { + // 保护:全局冷却期检查 + if (!canToggleTheme()) { return; } - // 第3层保护:视图过渡检查 - if (window._themeTransition) { + // 保护:按钮过渡中检查 + if (isTransitioning(targetButton)) { return; } - // 立即设置transitioning状态,阻止后续点击 - window._themeTransitioning = true; + // 保护:视图过渡检查 + if (currentTransition) { + return; + } + + // 记录此次切换时间 + recordToggleTime(); + + // 立即设置过渡状态,阻止后续点击 + setTransitioning(targetButton, true); try { // 计算点击坐标或使用按钮中心坐标 @@ -933,7 +979,7 @@ import "../styles/theme-toggle.css"; ).then(() => { // 过渡完成后恢复状态 setTimeout(() => { - window._themeTransitioning = false; + setTransitioning(targetButton, false); }, ANIMATION_BUFFER); // 添加缓冲时间 }).catch(error => { // 出现错误时强制执行主题切换以确保功能可用 @@ -944,13 +990,13 @@ import "../styles/theme-toggle.css"; } setTimeout(() => { - window._themeTransitioning = false; + setTransitioning(targetButton, false); }, ANIMATION_BUFFER); }); // 设置防抖保底 - 防止transition.finished不触发导致状态卡死 transitionTimeout = setTimeout(() => { - window._themeTransitioning = false; + setTransitioning(targetButton, false); }, TOTAL_TRANSITION_TIME + 200); // 总过渡时间加额外缓冲 } catch (err) { // 即使发生错误,也要确保主题能切换 @@ -976,7 +1022,7 @@ import "../styles/theme-toggle.css"; } // 确保状态被重置 - window._themeTransitioning = false; + setTransitioning(targetButton, false); } }; @@ -994,6 +1040,9 @@ import "../styles/theme-toggle.css"; // 为每个按钮添加事件 themeToggleButtons.forEach(button => { + // 确保初始化状态 + setTransitioning(button, false); + // 点击事件 - 使用捕获模式 addListener(button, "click", (e) => handleThemeToggle(e, button), { capture: true }); @@ -1007,39 +1056,6 @@ import "../styles/theme-toggle.css"; addListener(button, "keydown", keydownHandler); }); - // 处理移动端主题切换容器 - 使用同样的逻辑处理 - const themeToggleContainer = document.getElementById("theme-toggle-container"); - if (themeToggleContainer) { - const containerClickHandler = (e) => { - // 如果点击的是按钮内部,让按钮自己处理 - if (e.target.closest("#theme-toggle-button")) { - return; - } - - // 找到容器内的主题切换按钮 - const button = themeToggleContainer.querySelector("#theme-toggle-button"); - if (button) { - // 调用统一的处理函数 - handleThemeToggle(e, button); - } - }; - - // 使用capture模式确保事件在捕获阶段就被处理 - addListener(themeToggleContainer, "click", containerClickHandler, { capture: true }); - - // 为移动端容器添加关闭移动端菜单的功能 - const closeMenuHandler = (e) => { - // 检查是否有closeMobileMenu全局函数(来自Header.astro) - if (typeof window.closeMobileMenu === 'function') { - // 这个函数会在点击主题切换容器后被调用,无论是否真正触发主题切换 - window.closeMobileMenu(); - } - }; - - // 使用冒泡阶段,确保在主题切换处理后执行 - addListener(themeToggleContainer, "click", closeMenuHandler, { capture: false }); - } - // 初始化主题 initializeTheme(); } @@ -1051,7 +1067,6 @@ import "../styles/theme-toggle.css"; // 设置主题切换功能 setupThemeToggle(); - } // 判断DOM是否已加载 diff --git a/src/pages/articles/[...id].astro b/src/pages/articles/[...id].astro index eae7dcc..b1a36bc 100644 --- a/src/pages/articles/[...id].astro +++ b/src/pages/articles/[...id].astro @@ -561,7 +561,6 @@ const tableOfContents = generateTableOfContents(headings); class="scrollbar-thin scrollbar-thumb-primary-200 dark:scrollbar-thumb-primary-800 scrollbar-track-transparent" set:html={tableOfContents} > -
@@ -628,38 +627,31 @@ const tableOfContents = generateTableOfContents(headings); // 注册清理事件,并保存引用 function registerCleanupEvents() { - // 创建一次性事件处理函数 - const beforeSwapHandler = () => { + // 创建统一的清理处理函数 + const cleanup = () => { selfDestruct(); }; - - const beforeUnloadHandler = () => { - selfDestruct(); - }; - - // 添加清理事件监听器并保存引用 - document.addEventListener("astro:before-swap", beforeSwapHandler, { - once: true, + + // 定义需要监听的所有清理事件 + const cleanupEventTypes = [ + { element: document, eventType: "astro:before-swap", options: { once: true } }, + { element: window, eventType: "beforeunload", options: { once: true } }, + { element: document, eventType: "page-transition", options: { once: true } } + ]; + + // 注册所有清理事件 + cleanupEventTypes.forEach(({ element, eventType, options }) => { + // 添加事件监听 + element.addEventListener(eventType, cleanup, options); + + // 保存事件引用到清理列表 + cleanupListeners.push({ + element, + eventType, + handler: cleanup, + options + }); }); - window.addEventListener("beforeunload", beforeUnloadHandler, { - once: true, - }); - - // 保存清理事件引用,用于完全销毁 - cleanupListeners.push( - { - element: document, - eventType: "astro:before-swap", - handler: beforeSwapHandler, - options: { once: true }, - }, - { - element: window, - eventType: "beforeunload", - handler: beforeUnloadHandler, - options: { once: true }, - }, - ); } // 初始化所有功能 diff --git a/src/scripts/swup-init.js b/src/scripts/swup-init.js index 85b7d1c..16df6d3 100644 --- a/src/scripts/swup-init.js +++ b/src/scripts/swup-init.js @@ -5,6 +5,8 @@ import SwupFragmentPlugin from '@swup/fragment-plugin'; import SwupHeadPlugin from '@swup/head-plugin'; // 添加预加载插件 - 优化导航体验 import SwupPreloadPlugin from '@swup/preload-plugin'; +// 添加Scripts插件 - 确保页面转场后脚本能重新执行 +import SwupScriptsPlugin from '@swup/scripts-plugin'; // 检查是否是文章相关页面 function isArticlePage() { @@ -12,49 +14,38 @@ function isArticlePage() { return path.includes('/articles') || path.includes('/filtered'); } -// 为元素应用动画样式 -function applyAnimationStyles(element, className, duration = 300) { +// 为元素设置过渡状态 +function setElementTransition(element) { if (!element) return; - // 添加动画类 - element.classList.add(className); - - // 设置过渡属性 - element.style.transition = 'opacity 0.3s ease'; - element.style.animationDuration = '0.3s'; - element.style.opacity = '1'; - // 添加data-swup属性标记 element.setAttribute('data-swup-transition', 'true'); - element.setAttribute('data-swup-animation-duration', duration.toString()); } // 设置元素淡入/淡出效果 function setElementOpacity(element, opacity) { if (!element) return; element.style.opacity = opacity.toString(); - if (opacity === 0) { - element.style.transition = 'opacity 0.3s ease'; - } } -// 直接应用样式到元素上 -function applyStylesDirectly() { +// 应用过渡效果到相关元素 +function applyTransitions() { // 应用到主容器 - 只在非文章页面 const mainElement = document.querySelector('main'); if (mainElement) { mainElement.classList.add('transition-fade'); - // 只有在非文章页面时,才为main添加必要的动画样式 + // 只有在非文章页面时,才为main添加必要的过渡标记 if (!isArticlePage()) { - applyAnimationStyles(mainElement, 'transition-fade'); + setElementTransition(mainElement); } } // 应用到文章内容 - 只在文章页面 const articleContent = document.querySelector('#article-content'); if (articleContent) { - applyAnimationStyles(articleContent, 'swup-transition-article'); + articleContent.classList.add('swup-transition-article'); + setElementTransition(articleContent); } } @@ -69,13 +60,13 @@ function getActiveElement() { // 在DOM加载完成后初始化 document.addEventListener('DOMContentLoaded', () => { - // 直接应用样式 - applyStylesDirectly(); + // 应用过渡效果 + applyTransitions(); // 创建Swup实例 const swup = new Swup({ // Swup的基本配置 - animationSelector: '[class*="transition-"], #article-content, .swup-transition-article, main', + animationSelector: '[class*="transition-"], .swup-transition-article, #article-content', cache: true, containers: ['main'], animationScope: 'html', // 确保动画状态类添加到html元素 @@ -86,6 +77,17 @@ document.addEventListener('DOMContentLoaded', () => { plugins: [] // 手动添加插件以控制顺序 }); + // 发送页面转换事件 - 自定义全局事件 + function sendPageTransitionEvent() { + // 创建自定义事件并触发 + const event = new CustomEvent('page-transition', { + bubbles: true, + cancelable: false, + detail: { timestamp: Date.now() } + }); + document.dispatchEvent(event); + } + // 添加预加载插件 - 代替原有的预加载功能 const preloadPlugin = new SwupPreloadPlugin({ // 最多同时预加载5个链接 @@ -112,37 +114,25 @@ document.addEventListener('DOMContentLoaded', () => { const headPlugin = new SwupHeadPlugin(); swup.use(headPlugin); + // 添加Scripts插件 - 确保页面转场后脚本能重新执行 + const scriptsPlugin = new SwupScriptsPlugin({ + // 以下选项确定哪些脚本会被重新执行 + head: true, // 重新执行head中的脚本 + body: true, // 重新执行body中的脚本 + optin: false, // 是否只执行带有[data-swup-reload-script]属性的脚本 + oprout: false, // 是否排除带有[data-no-swup]属性的脚本 + once: true // 是否每个脚本只执行一次 + }); + swup.use(scriptsPlugin); + // 创建Fragment插件 - 简化规则避免匹配问题 const fragmentPlugin = new SwupFragmentPlugin({ debug: false, // 关闭调试模式 - // 简化规则,确保基本匹配 rules: [ { - // 文章页面之间的导航 name: 'article-pages', - from: '/articles', // 简化匹配规则 - to: '/articles', - containers: ['#article-content'] - }, - { - // 从文章到筛选页面 - name: 'article-to-filter', - from: '/articles', - to: '/filtered', - containers: ['#article-content'] - }, - { - // 从筛选到文章页面 - name: 'filter-to-article', - from: '/filtered', - to: '/articles', - containers: ['#article-content'] - }, - { - // 筛选页面内部导航 - name: 'filter-pages', - from: '/filtered', - to: '/filtered', + from: ['/articles', '/filtered'], + to: ['/articles', '/filtered'], containers: ['#article-content'] } ] @@ -156,10 +146,10 @@ document.addEventListener('DOMContentLoaded', () => { swup.preloadLinks(); }, 1000); - // 强制应用动画样式到特定元素 + // 重新设置过渡元素 function setupTransition() { - // 直接应用样式 - 会根据页面类型自动选择正确的元素 - applyStylesDirectly(); + // 应用过渡效果 + applyTransitions(); // 确保初始状态正确 setTimeout(() => { @@ -184,6 +174,9 @@ document.addEventListener('DOMContentLoaded', () => { // 监听动画开始和结束 swup.hooks.on('animation:out:start', () => { + // 发送页面切换事件 + sendPageTransitionEvent(); + // 获取并淡出当前活跃元素 const activeElement = getActiveElement(); setElementOpacity(activeElement, 0); @@ -200,6 +193,9 @@ document.addEventListener('DOMContentLoaded', () => { // 添加手动强制动画事件 document.addEventListener('swup:willReplaceContent', () => { + // 发送页面切换事件 + sendPageTransitionEvent(); + // 获取并淡出当前活跃元素 const activeElement = getActiveElement(); setElementOpacity(activeElement, 0); @@ -214,11 +210,13 @@ document.addEventListener('DOMContentLoaded', () => { // 先设置透明 setElementOpacity(activeElement, 0); - // 重新应用适当的类和属性 + // 重新应用适当的类 if (isArticlePage() && activeElement.id === 'article-content') { - applyAnimationStyles(activeElement, 'swup-transition-article'); + activeElement.classList.add('swup-transition-article'); + setElementTransition(activeElement); } else if (!isArticlePage() && activeElement.tagName.toLowerCase() === 'main') { - applyAnimationStyles(activeElement, 'transition-fade'); + activeElement.classList.add('transition-fade'); + setElementTransition(activeElement); } // 延迟后淡入 @@ -229,6 +227,9 @@ document.addEventListener('DOMContentLoaded', () => { // 监听URL变化以更新动画行为 swup.hooks.on('visit:start', (visit) => { + // 发送页面切换事件 + sendPageTransitionEvent(); + // 检查目标URL是否为文章相关页面 const isTargetArticlePage = visit.to.url.includes('/articles') || visit.to.url.includes('/filtered'); const isCurrentArticlePage = isArticlePage(); @@ -244,62 +245,13 @@ document.addEventListener('DOMContentLoaded', () => { else if (!isCurrentArticlePage && isTargetArticlePage) { const mainElement = document.querySelector('main'); if (mainElement) { - // 移除main的动画效果 + // 移除main的过渡效果 mainElement.style.transition = ''; mainElement.style.opacity = '1'; } } }); - // Fragment导航后手动更新面包屑 - function updateBreadcrumb(url) { - // 1. 获取新页面的HTML以提取面包屑 - fetch(url) - .then(response => response.text()) - .then(html => { - // 创建一个临时的DOM解析新页面 - const parser = new DOMParser(); - const newDoc = parser.parseFromString(html, 'text/html'); - - // 获取新页面的面包屑容器 - 使用更精确的选择器 - const newBreadcrumbContainer = newDoc.querySelector('.bg-white.dark\\:bg-gray-800.rounded-xl.mb-4, .bg-white.dark\\:bg-gray-800.rounded-xl.p-4'); - - // 获取当前页面的面包屑容器 - const currentBreadcrumbContainer = document.querySelector('.bg-white.dark\\:bg-gray-800.rounded-xl.mb-4, .bg-white.dark\\:bg-gray-800.rounded-xl.p-4'); - - if (newBreadcrumbContainer && currentBreadcrumbContainer) { - // 更新面包屑内容 - currentBreadcrumbContainer.innerHTML = newBreadcrumbContainer.innerHTML; - - // 重新初始化面包屑相关脚本 - const breadcrumbScript = currentBreadcrumbContainer.querySelector('script'); - if (breadcrumbScript) { - const newScript = document.createElement('script'); - newScript.textContent = breadcrumbScript.textContent; - breadcrumbScript.parentNode.replaceChild(newScript, breadcrumbScript); - } - } - }) - .catch(error => { - // 出错时静默处理 - }); - } - - // 在每次页面转换结束后更新面包屑 - swup.hooks.on('visit:end', (visit) => { - // 所有导航都更新面包屑 - updateBreadcrumb(visit.to.url); - - // 确保在页面加载完成后元素有正确样式 - setTimeout(() => { - setupTransition(); - - // 加载完成后重新扫描预加载链接 - setTimeout(() => { - swup.preloadLinks(); - }, 500); - }, 50); - }); // 监听Fragment插件是否成功应用 document.addEventListener('swup:fragmentReplaced', () => { @@ -311,10 +263,14 @@ document.addEventListener('DOMContentLoaded', () => { // 在页面卸载和Astro视图转换时清理资源 const cleanup = () => { + // 发送页面切换事件 + sendPageTransitionEvent(); + if (swup) { swup.unuse(fragmentPlugin); swup.unuse(headPlugin); swup.unuse(preloadPlugin); + swup.unuse(scriptsPlugin); // 也需要卸载Scripts插件 swup.destroy(); } }; diff --git a/src/styles/global.css b/src/styles/global.css index aa53783..6bc1dc1 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -1,4 +1,5 @@ @import "tailwindcss"; +@import "./swup-transitions.css"; /* 定义深色模式选择器 */ @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *)); diff --git a/src/styles/swup-transitions.css b/src/styles/swup-transitions.css new file mode 100644 index 0000000..cdac852 --- /dev/null +++ b/src/styles/swup-transitions.css @@ -0,0 +1,36 @@ +/* Swup页面过渡动画样式 */ + +/* 基础过渡效果 */ +.transition-fade { + transition: opacity 0.3s ease; + animation-duration: 0.3s; + opacity: 1; +} + +/* 文章内容过渡效果 */ +.swup-transition-article { + transition: opacity 0.3s ease; + animation-duration: 0.3s; + opacity: 1; +} + +/* 直接为article-content元素设置动画 */ +#article-content { + transition: opacity 0.3s ease; + animation-duration: 0.3s; + opacity: 1; +} + +/* 淡出状态 */ +html.is-animating .transition-fade, +html.is-animating .swup-transition-article, +html.is-animating #article-content { + opacity: 0; +} + +/* 淡入状态 */ +html.is-changing .transition-fade, +html.is-changing .swup-transition-article, +html.is-changing #article-content { + opacity: 1; +} \ No newline at end of file