优化swup,优化清理事件处理逻辑,改进主题切换和搜索面板关闭功能

This commit is contained in:
lsy 2025-05-13 12:26:58 +08:00
parent 2c71dcdbd9
commit 20276f1a86
11 changed files with 713 additions and 499 deletions

141
package-lock.json generated
View File

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

View File

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

View File

@ -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 } }
);
}
}
// 设置返回按钮功能

View File

@ -153,7 +153,7 @@ const navSelectorClassName = "mr-4";
<!-- 使用自定义主题切换组件 -->
<div class="flex items-center">
<ThemeToggle className="group" transitionDuration={700} />
<ThemeToggle transitionDuration={700} />
</div>
</div>
@ -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();
}
}
});
}
// 开始初始化

View File

@ -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 }
});
}
}

View File

@ -449,7 +449,50 @@ const Search: React.FC<SearchProps> = ({
}
};
// 修改处理键盘导航的函数,增加上下箭头键切换建议
// 添加关闭移动端搜索面板的函数
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<HTMLInputElement>) => {
// Tab键处理内联建议补全
@ -515,6 +558,12 @@ const Search: React.FC<SearchProps> = ({
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<SearchProps> = ({
// 如果有完全匹配的结果,关闭搜索结果面板并导航
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<SearchProps> = ({
// 找到完全匹配,关闭搜索结果面板并导航
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<SearchProps> = ({
// 如果需要导航到第一个结果,并且有结果
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<SearchProps> = ({
// 保存建议文本
const textToComplete = inlineSuggestion.text;
// 直接更新DOM和状态
// 立即更新搜索框内容和状态
setQuery(textToComplete);
if (searchInputRef.current) {
// 立即更新输入框值
searchInputRef.current.value = textToComplete;
}
@ -725,11 +785,11 @@ const Search: React.FC<SearchProps> = ({
suggestionText: ""
});
// 更新React状态
setQuery(textToComplete);
// 如果需要导航到第一个结果,保持结果面板显示状态,立即执行搜索
if (shouldNavigateToFirstResult) {
// 立即关闭搜索面板,然后执行搜索
setShowResults(false);
closeMobileSearchPanel();
performSearch(textToComplete, false, true);
} else {
// 如果不需要导航,关闭搜索结果面板,但仍然执行搜索以更新结果
@ -1151,10 +1211,13 @@ const Search: React.FC<SearchProps> = ({
<a
href={item.url}
className="group block hover:bg-primary-200/80 dark:hover:bg-primary-800/20 hover:shadow-md rounded-lg transition-all duration-200 ease-in-out p-2 -m-2 border border-transparent hover:border-primary-300/60 dark:hover:border-primary-700/30"
onClick={() => {
// 点击搜索结果项时关闭搜索结果面板
setShowResults(false);
setInlineSuggestion(prev => ({ ...prev, visible: false }));
data-astro-prefetch="hover"
onClick={(e) => {
// 防止默认行为,由我们自己处理导航
e.preventDefault();
// 使用导航函数处理跳转,它会关闭所有面板
navigateToUrl(item.url);
}}
>
<div className="flex items-start">
@ -1235,6 +1298,25 @@ const Search: React.FC<SearchProps> = ({
};
}, []);
// 为搜索组件添加视图切换事件监听
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 = (
<div className="relative [&_mark]:bg-yellow-200 dark:[&_mark]:bg-yellow-800">

View File

@ -31,6 +31,7 @@ import "../styles/theme-toggle.css";
tabindex="0"
data-transition-mode={transitionMode}
data-transition-duration={transitionDuration}
data-theme-transitioning="false"
>
<!-- 月亮图标 (暗色模式) -->
<svg
@ -78,6 +79,9 @@ import "../styles/theme-toggle.css";
let transitionTimeout = null;
let rippleTimeout = null;
// 局部存储当前过渡的引用
let currentTransition = null;
// 主题过渡模式
const TRANSITION_MODES = {
EXPAND: 'expand', // 扩散模式
@ -96,8 +100,35 @@ import "../styles/theme-toggle.css";
// 动画配置(毫秒)
const ANIMATION_BUFFER = 100; // 动画缓冲时间
const TOTAL_TRANSITION_TIME = ANIMATION_DURATION + ANIMATION_BUFFER; // 总过渡时间
// 防抖时间动态计算,始终比动画时间略长一些
const DEBOUNCE_TIME = TOTAL_TRANSITION_TIME + 200; // 防抖时间比总过渡时间多200ms
// 简化的冷却时间追踪 - 使用单一的上次切换时间
let lastThemeToggleTime = 0;
const COOLDOWN_TIME = TOTAL_TRANSITION_TIME + 200; // 防抖冷却时间
// 辅助函数:设置/获取按钮的过渡状态
function setTransitioning(button, isTransitioning) {
if (!button) return;
button.dataset.themeTransitioning = isTransitioning.toString();
}
function isTransitioning(button) {
if (!button) return false;
return button.dataset.themeTransitioning === 'true';
}
// 检查是否可以执行切换(冷却中检查)
function canToggleTheme() {
const now = Date.now();
// 如果距离上次切换时间小于冷却时间,不允许切换
if (now - lastThemeToggleTime < COOLDOWN_TIME) {
return false;
}
return true;
}
// 记录切换时间
function recordToggleTime() {
lastThemeToggleTime = Date.now();
}
// 从本地存储获取主题过渡模式
function getThemeTransitionMode() {
@ -122,13 +153,13 @@ import "../styles/theme-toggle.css";
// 统一的清理函数,执行完整清理并自销毁
function selfDestruct() {
// 0. 取消正在进行的transition
if (window._themeTransition && typeof window._themeTransition.skipTransition === 'function') {
if (currentTransition && typeof currentTransition.skipTransition === 'function') {
try {
window._themeTransition.skipTransition();
currentTransition.skipTransition();
} catch (err) {
// 忽略AbortError这是正常现象
} finally {
window._themeTransition = null;
currentTransition = null;
}
}
@ -158,8 +189,16 @@ import "../styles/theme-toggle.css";
ripple.parentNode.removeChild(ripple);
}
});
// 5. 重置全局状态
lastThemeToggleTime = 0;
// 6. 重置所有主题切换按钮的状态
document.querySelectorAll("#theme-toggle-button").forEach(button => {
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是否已加载

View File

@ -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}
>
<!-- 目录内容在服务端生成 -->
</div>
</div>
</section>
@ -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 },
},
);
}
// 初始化所有功能

View File

@ -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();
}
};

View File

@ -1,4 +1,5 @@
@import "tailwindcss";
@import "./swup-transitions.css";
/* 定义深色模式选择器 */
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));

View File

@ -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;
}