From 0d114ed070bc7d3c53612c129fbfcdd283adbc0f Mon Sep 17 00:00:00 2001 From: lsy Date: Mon, 5 May 2025 21:19:23 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=B8=BB=E9=A2=98=E5=88=87?= =?UTF-8?q?=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Header.astro | 3 +- src/components/ThemeToggle.astro | 280 ++++++++++++++++++++----------- 2 files changed, 185 insertions(+), 98 deletions(-) diff --git a/src/components/Header.astro b/src/components/Header.astro index 8826b14..d0a4599 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -153,7 +153,7 @@ const navSelectorClassName = "mr-4";
- +
@@ -320,6 +320,7 @@ const navSelectorClassName = "mr-4"; width={14} height={14} className="group" + transitionDuration={700} /> diff --git a/src/components/ThemeToggle.astro b/src/components/ThemeToggle.astro index 9bdbed7..cf0e5b0 100644 --- a/src/components/ThemeToggle.astro +++ b/src/components/ThemeToggle.astro @@ -6,6 +6,8 @@ interface Props { className?: string; // 更新主题过渡动画模式配置 transitionMode?: "expand" | "shrink" | "auto" | "reverse-auto"; + // 新增自定义主题切换动画时间 + transitionDuration?: number; } const { @@ -14,6 +16,7 @@ const { fill = "currentColor", className = "", transitionMode = "auto", // 默认为自动模式 + transitionDuration = 700, // 默认动画时间(毫秒) } = Astro.props; --- @@ -25,6 +28,7 @@ const { role="button" tabindex="0" data-transition-mode={transitionMode} + data-transition-duration={transitionDuration} > { + if (transitionTimeout) { + clearTimeout(transitionTimeout); + transitionTimeout = null; + } + + transitioning = false; + window._themeTransitioning = false; + + // 移除所有残留的波纹效果 + document.querySelectorAll(".theme-ripple").forEach(ripple => { + if (ripple.parentNode) { + ripple.parentNode.removeChild(ripple); + } + }); + + console.debug('主题切换: 状态已重置'); + }; + + // 监听Astro的页面切换事件 + document.addEventListener('astro:before-swap', resetThemeToggleState, { once: false }); + document.addEventListener('astro:after-swap', resetThemeToggleState, { once: false }); + allListeners.push( + { element: document, eventType: 'astro:before-swap', handler: resetThemeToggleState, options: { once: false } }, + { element: document, eventType: 'astro:after-swap', handler: resetThemeToggleState, options: { once: false } } + ); + + // 监听视图过渡事件 + if (typeof document.startViewTransition === 'function') { + document.addEventListener('view-transition-start', resetThemeToggleState, { once: false }); + document.addEventListener('view-transition-end', resetThemeToggleState, { once: false }); + allListeners.push( + { element: document, eventType: 'view-transition-start', handler: resetThemeToggleState, options: { once: false } }, + { element: document, eventType: 'view-transition-end', handler: resetThemeToggleState, options: { once: false } } + ); + } + + // 全局事件拦截器 - 拦截所有冒泡阶段的点击事件,防止重复触发 + const globalClickHandler = (e) => { + // 不处理未冒泡到document的事件 + if (!e || !e.target) return; + + // 如果点击事件来自于主题切换相关元素,且正在过渡中,拦截所有后续处理 + if (window._themeTransitioning && + (e.target.closest('#theme-toggle-button') || + e.target.closest('#theme-toggle-container'))) { + console.debug('全局拦截: 主题切换正在进行中,忽略额外点击'); + e.stopPropagation(); + e.preventDefault(); + } + }; + + // 在document级别添加事件捕获,确保所有相关点击都能被拦截 + document.addEventListener('click', globalClickHandler, { capture: true }); + // 清理时记得移除 + allListeners.push({ + element: document, + eventType: 'click', + handler: globalClickHandler, + options: { capture: true } + }); // 获取系统首选主题 const getSystemTheme = () => { @@ -826,40 +907,65 @@ const { } }; - // 切换主题 - const toggleTheme = (e) => { - if (transitioning) { + // 防抖函数 - 确保在指定时间内只执行一次 + 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) => { + // 防止事件默认行为和冒泡,无论如何都要先阻止 + if (e && typeof e.preventDefault === 'function') { + e.preventDefault(); + } + if (e && typeof e.stopPropagation === 'function') { + e.stopPropagation(); + } + + // 第1层保护:全局事件防抖 + if (isThrottled()) { + console.debug('主题切换: 防抖拦截,忽略点击'); return; } - transitioning = true; - - // 更可靠的点击坐标获取 - let clickX, clickY; + // 第2层保护:transitioning状态检查 + if (window._themeTransitioning) { + console.debug('主题切换: 已有过渡进行中,忽略点击'); + return; + } + + // 第3层保护:视图过渡检查 + if (window._themeTransition) { + console.debug('主题切换: 视图过渡已存在,忽略点击'); + return; + } + + // 所有检查通过后,开始处理主题切换 + console.debug('主题切换: 开始处理'); + + // 立即设置transitioning状态,阻止后续点击 + window._themeTransitioning = true; try { + // 计算点击坐标或使用按钮中心坐标 + let clickX, clickY; + if (e instanceof Event && typeof e.clientX === 'number' && typeof e.clientY === 'number') { - // 如果是真实事件对象且有有效坐标 clickX = e.clientX; clickY = e.clientY; } else if (e && typeof e.clientX === 'number' && typeof e.clientY === 'number') { - // 如果是合成事件对象且有有效坐标 clickX = e.clientX; clickY = e.clientY; } else { - // 如果无法获取有效坐标,使用屏幕中心点 - clickX = window.innerWidth / 2; - clickY = window.innerHeight / 2; - - // 尝试从目标元素获取位置 - if (e && e.target) { - const button = e.target.closest ? e.target.closest("#theme-toggle-button") : null; - if (button) { - const rect = button.getBoundingClientRect(); - clickX = rect.left + rect.width / 2; - clickY = rect.top + rect.height / 2; - } - } + const rect = targetButton.getBoundingClientRect(); + clickX = rect.left + rect.width / 2; + clickY = rect.top + rect.height / 2; } // 确保坐标有效(防止NaN或无限值) @@ -867,12 +973,7 @@ const { clickY = isFinite(clickY) ? clickY : window.innerHeight / 2; // 在按钮上创建小波纹效果 - if (e && e.target) { - const button = e.target.closest ? e.target.closest("#theme-toggle-button") : null; - if (button) { - createRippleEffect(clickX, clickY, button); - } - } + createRippleEffect(clickX, clickY, targetButton); // 获取当前主题 const currentTheme = document.documentElement.dataset.theme; @@ -881,14 +982,11 @@ const { // 获取过渡模式 let transitionMode = TRANSITION_MODES.AUTO; // 默认自动模式 - if (e && e.target) { - const button = e.target.closest ? e.target.closest("#theme-toggle-button") : null; - if (button && button.dataset && button.dataset.transitionMode) { - transitionMode = button.dataset.transitionMode; - } else { - // 如果无法从按钮获取,从本地存储获取 - transitionMode = getThemeTransitionMode(); - } + if (targetButton && targetButton.dataset && targetButton.dataset.transitionMode) { + transitionMode = targetButton.dataset.transitionMode; + } else { + // 如果无法从按钮获取,从本地存储获取 + transitionMode = getThemeTransitionMode(); } // 在开始新的过渡前,先重置transitioning的延时清除 @@ -912,6 +1010,7 @@ const { } }; + // 调用视图过渡 createViewTransition( safeThemeChangeCallback, clickX, @@ -922,8 +1021,9 @@ const { ).then(() => { // 过渡完成后恢复状态 setTimeout(() => { - transitioning = false; - }, ANIMATION_BUFFER); // 添加缓冲时间,确保动画完全结束 + window._themeTransitioning = false; + console.debug('主题切换: 过渡完成,恢复状态'); + }, ANIMATION_BUFFER); // 添加缓冲时间 }).catch(error => { // 出现错误时强制执行主题切换以确保功能可用 if (error.name !== 'AbortError') { @@ -935,13 +1035,15 @@ const { } setTimeout(() => { - transitioning = false; + window._themeTransitioning = false; + console.debug('主题切换: 过渡被取消,恢复状态'); }, ANIMATION_BUFFER); }); // 设置防抖保底 - 防止transition.finished不触发导致状态卡死 transitionTimeout = setTimeout(() => { - transitioning = false; + window._themeTransitioning = false; + console.debug('主题切换: 过渡超时,强制恢复状态'); }, TOTAL_TRANSITION_TIME + 200); // 总过渡时间加额外缓冲 } catch (err) { // 即使发生错误,也要确保主题能切换 @@ -967,7 +1069,8 @@ const { } // 确保状态被重置 - transitioning = false; + window._themeTransitioning = false; + console.debug('主题切换: 发生错误,恢复状态'); } }; @@ -985,79 +1088,50 @@ const { // 为每个按钮添加事件 themeToggleButtons.forEach(button => { - // 创建点击处理函数 - const clickHandler = (e) => { - e.preventDefault(); - e.stopPropagation(); - toggleTheme(e); - }; - // 点击事件 - 使用捕获模式 - addListener(button, "click", clickHandler, { capture: true }); + addListener(button, "click", (e) => handleThemeToggle(e, button), { capture: true }); // 键盘事件 const keydownHandler = (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); - // 为键盘事件创建模拟点击事件,使其中心点位于按钮中央 - const rect = button.getBoundingClientRect(); - const centerX = rect.left + rect.width / 2; - const centerY = rect.top + rect.height / 2; - - // 创建模拟事件对象 - const simulatedEvent = { - clientX: centerX, - clientY: centerY, - target: button, - preventDefault: () => {}, - stopPropagation: () => {} - }; - - toggleTheme(simulatedEvent); + handleThemeToggle(e, button); } }; addListener(button, "keydown", keydownHandler); }); - // 处理移动端主题切换容器 - 重新添加此功能以提供更一致的用户体验 + // 处理移动端主题切换容器 - 使用同样的逻辑处理 const themeToggleContainer = document.getElementById("theme-toggle-container"); if (themeToggleContainer) { const containerClickHandler = (e) => { - // 只有当点击不是直接在按钮上时,才托管事件处理 - if (!e.target.closest("#theme-toggle-button")) { - // 找到容器内的主题切换按钮 - const button = themeToggleContainer.querySelector("#theme-toggle-button"); - if (button) { - // 从点击事件中提取坐标 - const clickX = e.clientX || 0; - const clickY = e.clientY || 0; - - // 使用更可靠的坐标计算方法 - const rect = button.getBoundingClientRect(); - const centerX = clickX || (rect.left + rect.width / 2); - const centerY = clickY || (rect.top + rect.height / 2); - - // 创建一个更可靠的合成事件对象 - const simulatedEvent = { - clientX: centerX, - clientY: centerY, - target: button, - preventDefault: () => {}, - stopPropagation: () => {} - }; - - // 阻止事件冒泡 - e.preventDefault(); - e.stopPropagation(); - - // 调用主题切换函数 - toggleTheme(simulatedEvent); - } + // 如果点击的是按钮内部,让按钮自己处理 + 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 }); } // 初始化主题 @@ -1071,6 +1145,18 @@ const { // 设置主题切换功能 setupThemeToggle(); + + // 设置防止内存泄漏的自动清理 - 最多30秒后自毁 + setTimeout(() => { + // 如果页面没有被卸载,检查是否已经清理过 + if (document.body && !document._themeToggleCleanedUp) { + // 为避免重复清理,设置一个标记 + document._themeToggleCleanedUp = true; + // 执行清理 + selfDestruct(); + console.debug('主题切换: 自动清理已执行'); + } + }, 30000); } // 判断DOM是否已加载