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"
>
@@ -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