优化主题切换
This commit is contained in:
parent
afbdc61605
commit
0d114ed070
@ -153,7 +153,7 @@ const navSelectorClassName = "mr-4";
|
|||||||
|
|
||||||
<!-- 使用自定义主题切换组件 -->
|
<!-- 使用自定义主题切换组件 -->
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<ThemeToggle className="group" />
|
<ThemeToggle className="group" transitionDuration={700} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -320,6 +320,7 @@ const navSelectorClassName = "mr-4";
|
|||||||
width={14}
|
width={14}
|
||||||
height={14}
|
height={14}
|
||||||
className="group"
|
className="group"
|
||||||
|
transitionDuration={700}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,6 +6,8 @@ interface Props {
|
|||||||
className?: string;
|
className?: string;
|
||||||
// 更新主题过渡动画模式配置
|
// 更新主题过渡动画模式配置
|
||||||
transitionMode?: "expand" | "shrink" | "auto" | "reverse-auto";
|
transitionMode?: "expand" | "shrink" | "auto" | "reverse-auto";
|
||||||
|
// 新增自定义主题切换动画时间
|
||||||
|
transitionDuration?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -14,6 +16,7 @@ const {
|
|||||||
fill = "currentColor",
|
fill = "currentColor",
|
||||||
className = "",
|
className = "",
|
||||||
transitionMode = "auto", // 默认为自动模式
|
transitionMode = "auto", // 默认为自动模式
|
||||||
|
transitionDuration = 700, // 默认动画时间(毫秒)
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -25,6 +28,7 @@ const {
|
|||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
data-transition-mode={transitionMode}
|
data-transition-mode={transitionMode}
|
||||||
|
data-transition-duration={transitionDuration}
|
||||||
>
|
>
|
||||||
<!-- 月亮图标 (暗色模式) -->
|
<!-- 月亮图标 (暗色模式) -->
|
||||||
<svg
|
<svg
|
||||||
@ -137,10 +141,18 @@ const {
|
|||||||
REVERSE_AUTO: 'reverse-auto' // 反向自动模式
|
REVERSE_AUTO: 'reverse-auto' // 反向自动模式
|
||||||
};
|
};
|
||||||
|
|
||||||
// 动画持续时间配置(毫秒)
|
// 获取用户自定义的动画持续时间
|
||||||
const ANIMATION_DURATION = 700; // 动画持续时间
|
let ANIMATION_DURATION = 700; // 默认动画持续时间
|
||||||
|
const customDuration = document.querySelector('#theme-toggle-button')?.dataset?.transitionDuration;
|
||||||
|
if (customDuration && !isNaN(parseInt(customDuration))) {
|
||||||
|
ANIMATION_DURATION = parseInt(customDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动画配置(毫秒)
|
||||||
const ANIMATION_BUFFER = 100; // 动画缓冲时间
|
const ANIMATION_BUFFER = 100; // 动画缓冲时间
|
||||||
const TOTAL_TRANSITION_TIME = ANIMATION_DURATION + ANIMATION_BUFFER; // 总过渡时间
|
const TOTAL_TRANSITION_TIME = ANIMATION_DURATION + ANIMATION_BUFFER; // 总过渡时间
|
||||||
|
// 防抖时间动态计算,始终比动画时间略长一些
|
||||||
|
const DEBOUNCE_TIME = TOTAL_TRANSITION_TIME + 200; // 防抖时间比总过渡时间多200ms
|
||||||
|
|
||||||
// 从本地存储获取主题过渡模式
|
// 从本地存储获取主题过渡模式
|
||||||
function getThemeTransitionMode() {
|
function getThemeTransitionMode() {
|
||||||
@ -802,7 +814,76 @@ const {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 防抖配置 - 使用前面定义的动态计算值
|
||||||
|
let lastClickTime = 0;
|
||||||
let transitioning = false;
|
let transitioning = false;
|
||||||
|
|
||||||
|
// 将transitioning状态暴露给全局,便于其他脚本检查
|
||||||
|
window._themeTransitioning = false;
|
||||||
|
window._lastThemeToggleClickTime = 0;
|
||||||
|
|
||||||
|
// 状态清理函数 - 在页面切换、错误和超时情况下调用
|
||||||
|
const resetThemeToggleState = () => {
|
||||||
|
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 = () => {
|
const getSystemTheme = () => {
|
||||||
@ -826,40 +907,65 @@ const {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 切换主题
|
// 防抖函数 - 确保在指定时间内只执行一次
|
||||||
const toggleTheme = (e) => {
|
const isThrottled = () => {
|
||||||
if (transitioning) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
transitioning = true;
|
// 第2层保护:transitioning状态检查
|
||||||
|
if (window._themeTransitioning) {
|
||||||
// 更可靠的点击坐标获取
|
console.debug('主题切换: 已有过渡进行中,忽略点击');
|
||||||
let clickX, clickY;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第3层保护:视图过渡检查
|
||||||
|
if (window._themeTransition) {
|
||||||
|
console.debug('主题切换: 视图过渡已存在,忽略点击');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有检查通过后,开始处理主题切换
|
||||||
|
console.debug('主题切换: 开始处理');
|
||||||
|
|
||||||
|
// 立即设置transitioning状态,阻止后续点击
|
||||||
|
window._themeTransitioning = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 计算点击坐标或使用按钮中心坐标
|
||||||
|
let clickX, clickY;
|
||||||
|
|
||||||
if (e instanceof Event && typeof e.clientX === 'number' && typeof e.clientY === 'number') {
|
if (e instanceof Event && typeof e.clientX === 'number' && typeof e.clientY === 'number') {
|
||||||
// 如果是真实事件对象且有有效坐标
|
|
||||||
clickX = e.clientX;
|
clickX = e.clientX;
|
||||||
clickY = e.clientY;
|
clickY = e.clientY;
|
||||||
} else if (e && typeof e.clientX === 'number' && typeof e.clientY === 'number') {
|
} else if (e && typeof e.clientX === 'number' && typeof e.clientY === 'number') {
|
||||||
// 如果是合成事件对象且有有效坐标
|
|
||||||
clickX = e.clientX;
|
clickX = e.clientX;
|
||||||
clickY = e.clientY;
|
clickY = e.clientY;
|
||||||
} else {
|
} else {
|
||||||
// 如果无法获取有效坐标,使用屏幕中心点
|
const rect = targetButton.getBoundingClientRect();
|
||||||
clickX = window.innerWidth / 2;
|
clickX = rect.left + rect.width / 2;
|
||||||
clickY = window.innerHeight / 2;
|
clickY = rect.top + rect.height / 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保坐标有效(防止NaN或无限值)
|
// 确保坐标有效(防止NaN或无限值)
|
||||||
@ -867,12 +973,7 @@ const {
|
|||||||
clickY = isFinite(clickY) ? clickY : window.innerHeight / 2;
|
clickY = isFinite(clickY) ? clickY : window.innerHeight / 2;
|
||||||
|
|
||||||
// 在按钮上创建小波纹效果
|
// 在按钮上创建小波纹效果
|
||||||
if (e && e.target) {
|
createRippleEffect(clickX, clickY, targetButton);
|
||||||
const button = e.target.closest ? e.target.closest("#theme-toggle-button") : null;
|
|
||||||
if (button) {
|
|
||||||
createRippleEffect(clickX, clickY, button);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前主题
|
// 获取当前主题
|
||||||
const currentTheme = document.documentElement.dataset.theme;
|
const currentTheme = document.documentElement.dataset.theme;
|
||||||
@ -881,14 +982,11 @@ const {
|
|||||||
// 获取过渡模式
|
// 获取过渡模式
|
||||||
let transitionMode = TRANSITION_MODES.AUTO; // 默认自动模式
|
let transitionMode = TRANSITION_MODES.AUTO; // 默认自动模式
|
||||||
|
|
||||||
if (e && e.target) {
|
if (targetButton && targetButton.dataset && targetButton.dataset.transitionMode) {
|
||||||
const button = e.target.closest ? e.target.closest("#theme-toggle-button") : null;
|
transitionMode = targetButton.dataset.transitionMode;
|
||||||
if (button && button.dataset && button.dataset.transitionMode) {
|
} else {
|
||||||
transitionMode = button.dataset.transitionMode;
|
// 如果无法从按钮获取,从本地存储获取
|
||||||
} else {
|
transitionMode = getThemeTransitionMode();
|
||||||
// 如果无法从按钮获取,从本地存储获取
|
|
||||||
transitionMode = getThemeTransitionMode();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在开始新的过渡前,先重置transitioning的延时清除
|
// 在开始新的过渡前,先重置transitioning的延时清除
|
||||||
@ -912,6 +1010,7 @@ const {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 调用视图过渡
|
||||||
createViewTransition(
|
createViewTransition(
|
||||||
safeThemeChangeCallback,
|
safeThemeChangeCallback,
|
||||||
clickX,
|
clickX,
|
||||||
@ -922,8 +1021,9 @@ const {
|
|||||||
).then(() => {
|
).then(() => {
|
||||||
// 过渡完成后恢复状态
|
// 过渡完成后恢复状态
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
transitioning = false;
|
window._themeTransitioning = false;
|
||||||
}, ANIMATION_BUFFER); // 添加缓冲时间,确保动画完全结束
|
console.debug('主题切换: 过渡完成,恢复状态');
|
||||||
|
}, ANIMATION_BUFFER); // 添加缓冲时间
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
// 出现错误时强制执行主题切换以确保功能可用
|
// 出现错误时强制执行主题切换以确保功能可用
|
||||||
if (error.name !== 'AbortError') {
|
if (error.name !== 'AbortError') {
|
||||||
@ -935,13 +1035,15 @@ const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
transitioning = false;
|
window._themeTransitioning = false;
|
||||||
|
console.debug('主题切换: 过渡被取消,恢复状态');
|
||||||
}, ANIMATION_BUFFER);
|
}, ANIMATION_BUFFER);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 设置防抖保底 - 防止transition.finished不触发导致状态卡死
|
// 设置防抖保底 - 防止transition.finished不触发导致状态卡死
|
||||||
transitionTimeout = setTimeout(() => {
|
transitionTimeout = setTimeout(() => {
|
||||||
transitioning = false;
|
window._themeTransitioning = false;
|
||||||
|
console.debug('主题切换: 过渡超时,强制恢复状态');
|
||||||
}, TOTAL_TRANSITION_TIME + 200); // 总过渡时间加额外缓冲
|
}, TOTAL_TRANSITION_TIME + 200); // 总过渡时间加额外缓冲
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 即使发生错误,也要确保主题能切换
|
// 即使发生错误,也要确保主题能切换
|
||||||
@ -967,7 +1069,8 @@ const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 确保状态被重置
|
// 确保状态被重置
|
||||||
transitioning = false;
|
window._themeTransitioning = false;
|
||||||
|
console.debug('主题切换: 发生错误,恢复状态');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -985,79 +1088,50 @@ const {
|
|||||||
|
|
||||||
// 为每个按钮添加事件
|
// 为每个按钮添加事件
|
||||||
themeToggleButtons.forEach(button => {
|
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) => {
|
const keydownHandler = (e) => {
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// 为键盘事件创建模拟点击事件,使其中心点位于按钮中央
|
handleThemeToggle(e, button);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
addListener(button, "keydown", keydownHandler);
|
addListener(button, "keydown", keydownHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理移动端主题切换容器 - 重新添加此功能以提供更一致的用户体验
|
// 处理移动端主题切换容器 - 使用同样的逻辑处理
|
||||||
const themeToggleContainer = document.getElementById("theme-toggle-container");
|
const themeToggleContainer = document.getElementById("theme-toggle-container");
|
||||||
if (themeToggleContainer) {
|
if (themeToggleContainer) {
|
||||||
const containerClickHandler = (e) => {
|
const containerClickHandler = (e) => {
|
||||||
// 只有当点击不是直接在按钮上时,才托管事件处理
|
// 如果点击的是按钮内部,让按钮自己处理
|
||||||
if (!e.target.closest("#theme-toggle-button")) {
|
if (e.target.closest("#theme-toggle-button")) {
|
||||||
// 找到容器内的主题切换按钮
|
return;
|
||||||
const button = themeToggleContainer.querySelector("#theme-toggle-button");
|
}
|
||||||
if (button) {
|
|
||||||
// 从点击事件中提取坐标
|
// 找到容器内的主题切换按钮
|
||||||
const clickX = e.clientX || 0;
|
const button = themeToggleContainer.querySelector("#theme-toggle-button");
|
||||||
const clickY = e.clientY || 0;
|
if (button) {
|
||||||
|
// 调用统一的处理函数
|
||||||
// 使用更可靠的坐标计算方法
|
handleThemeToggle(e, button);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 使用capture模式确保事件在捕获阶段就被处理
|
// 使用capture模式确保事件在捕获阶段就被处理
|
||||||
addListener(themeToggleContainer, "click", containerClickHandler, { capture: true });
|
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();
|
setupThemeToggle();
|
||||||
|
|
||||||
|
// 设置防止内存泄漏的自动清理 - 最多30秒后自毁
|
||||||
|
setTimeout(() => {
|
||||||
|
// 如果页面没有被卸载,检查是否已经清理过
|
||||||
|
if (document.body && !document._themeToggleCleanedUp) {
|
||||||
|
// 为避免重复清理,设置一个标记
|
||||||
|
document._themeToggleCleanedUp = true;
|
||||||
|
// 执行清理
|
||||||
|
selfDestruct();
|
||||||
|
console.debug('主题切换: 自动清理已执行');
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断DOM是否已加载
|
// 判断DOM是否已加载
|
||||||
|
Loading…
Reference in New Issue
Block a user