newechoes/src/components/ThemeToggle.astro
2025-05-05 21:19:23 +08:00

1169 lines
46 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
interface Props {
height?: number;
width?: number;
fill?: string;
className?: string;
// 更新主题过渡动画模式配置
transitionMode?: "expand" | "shrink" | "auto" | "reverse-auto";
// 新增自定义主题切换动画时间
transitionDuration?: number;
}
const {
height = 16,
width = 16,
fill = "currentColor",
className = "",
transitionMode = "auto", // 默认为自动模式
transitionDuration = 700, // 默认动画时间(毫秒)
} = Astro.props;
---
<button
id="theme-toggle-button"
class={`inline-flex items-center justify-center h-8 w-8 cursor-pointer rounded-md hover:bg-gray-100 dark:hover:bg-gray-700/50 text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 ${className} overflow-hidden relative`}
aria-label="切换主题"
role="button"
tabindex="0"
data-transition-mode={transitionMode}
data-transition-duration={transitionDuration}
>
<!-- 月亮图标 (暗色模式) -->
<svg
id="dark-icon"
style={`height: ${height}px; width: ${width}px;`}
fill={fill}
viewBox="0 0 16 16"
class="hover:scale-110 hidden dark:block relative z-10"
aria-hidden="true"
>
<path
d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"
></path>
</svg>
<!-- 太阳图标 (亮色模式) -->
<svg
id="light-icon"
style={`height: ${height}px; width: ${width}px;`}
fill={fill}
viewBox="0 0 16 16"
class="hover:scale-110 block dark:hidden relative z-10"
aria-hidden="true"
>
<path
d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"
></path>
</svg>
<!-- 波纹效果容器 -->
<span id="ripple-container" class="absolute inset-0 pointer-events-none z-0"></span>
</button>
<style is:global>
/* 波纹效果相关样式 */
@keyframes ripple-effect {
from {
transform: scale(0);
opacity: 0.8;
}
to {
transform: scale(10);
opacity: 0;
}
}
.theme-ripple {
position: absolute;
border-radius: 50%;
background-color: rgba(var(--theme-ripple-color, 100, 100, 100), 0.15);
width: 10px;
height: 10px;
pointer-events: none;
transform-origin: center;
animation: ripple-effect 800ms ease-out forwards;
}
/* 暗色模式下使用不同颜色变量 */
.dark .theme-ripple {
background-color: rgba(var(--theme-ripple-color, 200, 200, 200), 0.15);
}
/* View Transitions 样式控制 */
::view-transition-old(root),
::view-transition-new(root) {
animation: none !important;
mix-blend-mode: normal !important;
isolation: auto !important;
}
/* 新增特殊模式样式 */
html.theme-transition-active {
transition: none !important;
}
::view-transition-old(root) {
z-index: 999 !important;
}
::view-transition-new(root) {
z-index: 1000 !important;
}
/* 设置主题容器在移动设备上的样式 */
#theme-toggle-container {
position: relative;
overflow: hidden;
}
</style>
<script is:inline>
// 主题切换逻辑 - 自销毁模式
(function() {
// 集中管理所有事件监听器
const allListeners = [];
// 单独保存清理事件的监听器引用
const cleanupListeners = [];
// 定时器引用
let transitionTimeout = null;
let rippleTimeout = null;
// 主题过渡模式
const TRANSITION_MODES = {
EXPAND: 'expand', // 扩散模式
SHRINK: 'shrink', // 收缩模式
AUTO: 'auto', // 自动模式(根据切换方向选择)
REVERSE_AUTO: 'reverse-auto' // 反向自动模式
};
// 获取用户自定义的动画持续时间
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 TOTAL_TRANSITION_TIME = ANIMATION_DURATION + ANIMATION_BUFFER; // 总过渡时间
// 防抖时间动态计算,始终比动画时间略长一些
const DEBOUNCE_TIME = TOTAL_TRANSITION_TIME + 200; // 防抖时间比总过渡时间多200ms
// 从本地存储获取主题过渡模式
function getThemeTransitionMode() {
const savedMode = localStorage.getItem('theme-transition-mode');
return Object.values(TRANSITION_MODES).includes(savedMode)
? savedMode
: TRANSITION_MODES.AUTO;
}
// 添加事件监听器并记录,方便后续统一清理
function addListener(element, eventType, handler, options) {
if (!element) {
console.warn(`主题切换尝试为不存在的元素添加事件:`, eventType);
return null;
}
element.addEventListener(eventType, handler, options);
allListeners.push({ element, eventType, handler, options });
return handler;
}
// 统一的清理函数,执行完整清理并自销毁
function selfDestruct() {
// 0. 取消正在进行的transition
if (window._themeTransition && typeof window._themeTransition.skipTransition === 'function') {
try {
window._themeTransition.skipTransition();
console.debug(`主题切换清理阶段取消transition成功`);
} catch (err) {
// 降级为debug日志
if (err.name === 'AbortError') {
console.debug(`主题切换清理阶段: 过渡已被跳过,这是正常现象`);
} else {
console.debug(`主题切换清理阶段取消transition出错:`, err);
}
} finally {
window._themeTransition = null;
}
}
// 1. 清理所有计时器
if (transitionTimeout) {
clearTimeout(transitionTimeout);
transitionTimeout = null;
}
if (rippleTimeout) {
clearTimeout(rippleTimeout);
rippleTimeout = null;
}
// 2. 清理临时样式元素
const tempStyle = document.getElementById('theme-transition-temp-style');
if (tempStyle) {
tempStyle.remove();
}
// 3. 移除过渡动画标记
document.documentElement.classList.remove('theme-transition-active');
// 4. 移除所有波纹效果元素
document.querySelectorAll(".theme-ripple").forEach(ripple => {
if (ripple.parentNode) {
ripple.parentNode.removeChild(ripple);
}
});
// 5. 移除普通事件监听器
allListeners.forEach(({ element, eventType, handler, options }) => {
try {
element.removeEventListener(eventType, handler, options);
} catch (err) {
console.error(`主题切换移除事件监听器出错:`, err);
}
});
// 清空监听器数组
allListeners.length = 0;
// 6. 最后移除清理事件监听器自身
cleanupListeners.forEach(({ element, eventType, handler, options }) => {
try {
element.removeEventListener(eventType, handler, options);
} catch (err) {
console.error(`主题切换移除清理监听器出错:`, err);
}
});
// 清空清理监听器数组
cleanupListeners.length = 0;
}
// 注册清理事件,并保存引用
function registerCleanupEvents() {
// 创建一次性事件处理函数
const beforeSwapHandler = () => {
selfDestruct();
};
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 } }
);
}
// 创建波纹动画元素
function createRippleEffect(x, y, element) {
// 清理旧的波纹元素
const container = element.querySelector("#ripple-container") || element;
const oldRipples = container.querySelectorAll(".theme-ripple");
oldRipples.forEach(ripple => ripple.remove());
// 创建新的波纹元素
const ripple = document.createElement("span");
ripple.classList.add("theme-ripple");
// 设置波纹位置
const rect = element.getBoundingClientRect();
const relativeX = x - rect.left;
const relativeY = y - rect.top;
ripple.style.left = `${relativeX}px`;
ripple.style.top = `${relativeY}px`;
// 添加波纹到容器
container.appendChild(ripple);
// 自动清理波纹元素
rippleTimeout = setTimeout(() => {
if (ripple.parentNode) {
ripple.parentNode.removeChild(ripple);
}
}, 1000);
return ripple;
}
// 确定应该使用的动画类型
function determineAnimationType(transitionMode, fromTheme, toTheme) {
// 如果是固定模式,直接返回
if (transitionMode === TRANSITION_MODES.EXPAND ||
transitionMode === TRANSITION_MODES.SHRINK) {
return transitionMode;
}
// 如果是自动模式,根据切换方向决定
if (transitionMode === TRANSITION_MODES.AUTO) {
// 默认自动模式:亮色->暗色用扩散,暗色->亮色用收缩
return (fromTheme === 'light' && toTheme === 'dark')
? TRANSITION_MODES.EXPAND
: TRANSITION_MODES.SHRINK;
}
// 如果是反向自动模式,反向选择
if (transitionMode === TRANSITION_MODES.REVERSE_AUTO) {
// 反向自动模式:亮色->暗色用收缩,暗色->亮色用扩散
return (fromTheme === 'light' && toTheme === 'dark')
? TRANSITION_MODES.SHRINK
: TRANSITION_MODES.EXPAND;
}
// 默认返回扩散模式
return TRANSITION_MODES.EXPAND;
}
// 使用View Transitions API创建全屏过渡效果
function createViewTransition(callback, x, y, fromTheme, toTheme, transitionMode) {
// 如果已有正在进行的过渡,先取消它
if (window._themeTransition) {
try {
if (typeof window._themeTransition.skipTransition === 'function') {
window._themeTransition.skipTransition();
}
} catch (e) {
// 降级为debug日志不需要显示警告
console.debug(`主题切换取消先前过渡失败:`, e);
} finally {
// 无论成功与否,都清除引用
window._themeTransition = null;
}
}
// 检查浏览器是否支持View Transitions API
if (!document.startViewTransition) {
// 尝试使用简单的回退动画
try {
// 创建一个圆形蒙版元素
const mask = document.createElement('div');
mask.style.position = 'fixed';
mask.style.zIndex = '9999';
mask.style.top = '0';
mask.style.left = '0';
mask.style.width = '100vw';
mask.style.height = '100vh';
mask.style.pointerEvents = 'none';
// 设置当前主题的背景颜色
if (fromTheme === 'dark') {
mask.style.backgroundColor = '#1a1a1a'; // 暗色主题背景色
} else {
mask.style.backgroundColor = '#ffffff'; // 亮色主题背景色
}
// 创建圆形过渡裁剪区域
const clipType = determineAnimationType(transitionMode, fromTheme, toTheme);
if (clipType === TRANSITION_MODES.EXPAND) {
// 扩散效果 - 从点击位置向外扩散
mask.style.clipPath = `circle(0px at ${x}px ${y}px)`;
document.body.appendChild(mask);
// 先执行回调改变主题
callback();
// 然后执行动画
setTimeout(() => {
mask.style.transition = `clip-path ${ANIMATION_DURATION/1000}s ease-out`;
mask.style.clipPath = `circle(150vmax at ${x}px ${y}px)`;
// 动画结束后删除遮罩
setTimeout(() => {
if (mask.parentNode) {
mask.parentNode.removeChild(mask);
}
}, ANIMATION_DURATION);
}, 20);
} else {
// 收缩效果 - 从全屏向点击位置收缩
mask.style.clipPath = `circle(150vmax at ${x}px ${y}px)`;
document.body.appendChild(mask);
// 添加过渡样式
mask.style.transition = `clip-path ${ANIMATION_DURATION/1000}s ease-in`;
// 强制回流
void mask.offsetWidth;
// 设置目标状态
mask.style.clipPath = `circle(0px at ${x}px ${y}px)`;
// 等待动画结束后切换主题并移除遮罩
setTimeout(() => {
callback();
if (mask.parentNode) {
mask.parentNode.removeChild(mask);
}
}, ANIMATION_DURATION);
}
return new Promise(resolve => setTimeout(resolve, TOTAL_TRANSITION_TIME));
} catch (e) {
// 如果回退方案也失败,直接执行回调
callback();
return Promise.resolve();
}
}
try {
// 清除状态标记,为下一次过渡做准备
document.documentElement.classList.remove('theme-transition-active');
// 清除可能存在的临时样式
const oldStyle = document.getElementById('theme-transition-temp-style');
if (oldStyle) {
oldStyle.remove();
}
// 计算从点击位置到页面四个角的最大距离
const w = window.innerWidth;
const h = window.innerHeight;
// 计算最大半径,确保覆盖整个屏幕
const maxDistance = Math.max(
Math.hypot(x, y), // 左上角
Math.hypot(w - x, y), // 右上角
Math.hypot(x, h - y), // 左下角
Math.hypot(w - x, h - y) // 右下角
);
// 设置CSS变量用于波纹颜色
document.documentElement.style.setProperty(
'--theme-ripple-color',
toTheme === 'dark' ? '230, 230, 230' : '20, 20, 20'
);
// 添加主题过渡标记类
document.documentElement.classList.add('theme-transition-active');
// 确定动画类型
const animationType = determineAnimationType(transitionMode, fromTheme, toTheme);
// 创建一个简单回调函数避免复杂操作导致transition跳过
const safeCallback = () => {
try {
// 更新HTML属性
document.documentElement.dataset.theme = toTheme;
// 更新本地存储
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
if (toTheme === systemTheme) {
localStorage.removeItem("theme");
} else {
localStorage.setItem("theme", toTheme);
}
// 最后执行用户回调
callback();
} catch (err) {
console.error(`主题切换 回调执行出错:`, err);
}
};
// 启动视图过渡
const transition = document.startViewTransition(safeCallback);
// 存储transition引用以便清理
window._themeTransition = transition;
// 生成动画需要的SVG资源
const gradientOffset = 0.75;
const maskSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8"><defs><radialGradient id="toggle-theme-gradient"><stop offset="${gradientOffset}"/><stop offset="1" stop-opacity="0"/></radialGradient></defs><circle cx="4" cy="4" r="4" fill="url(#toggle-theme-gradient)"/></svg>`;
const maskUrl = `data:image/svg+xml;base64,${btoa(maskSvg)}`;
// 计算动画需要多大才能覆盖整个屏幕
const maxRadius = Math.ceil(maxDistance / gradientOffset);
// 准备应用自定义动画的操作
const applyCustomAnimation = async () => {
try {
// 使用race和超时Promise改进超时策略
const readyPromise = Promise.race([
transition.ready,
new Promise((_, reject) => {
// 增加超时时间到50ms提高收缩模式的成功率
setTimeout(() => reject(new Error('Ready timeout')), 50);
})
]);
try {
// 等待过渡准备完成
await readyPromise;
} catch (err) {
// 超时处理 - 只记录警告日志,继续执行动画
if (err.message === 'Ready timeout') {
console.debug(`主题切换过渡准备超时,使用备用策略`);
// 收缩模式时此处超时更常见,立即使用备用方案
if (animationType === TRANSITION_MODES.SHRINK) {
applyFallbackAnimation();
return; // 跳过后续代码,使用备用方案
}
} else if (err.name === 'AbortError') {
// 如果是AbortError我们也立即使用备用方案
applyFallbackAnimation();
return; // 跳过后续代码,使用备用方案
} else {
console.debug(`主题切换过渡准备错误:`, err);
}
}
// 应用基础样式到document
const style = document.createElement('style');
style.id = 'theme-transition-temp-style';
if (animationType === TRANSITION_MODES.EXPAND) {
// 扩散效果 - 新主题从点击位置向外扩散
style.textContent = `
::view-transition-new(root) {
animation: none !important;
-webkit-mask-image: url('${maskUrl}') !important;
mask-image: url('${maskUrl}') !important;
-webkit-mask-repeat: no-repeat !important;
mask-repeat: no-repeat !important;
-webkit-mask-position: ${x}px ${y}px !important;
mask-position: ${x}px ${y}px !important;
-webkit-mask-size: 0 !important;
mask-size: 0 !important;
z-index: 1000 !important;
}
`;
document.head.appendChild(style);
// 强制重新计算样式
window.getComputedStyle(document.documentElement).getPropertyValue('--force-reflow');
// 立即设置最终状态
requestAnimationFrame(() => {
if (style.parentNode) {
style.textContent = `
::view-transition-new(root) {
animation: none !important;
-webkit-mask-image: url('${maskUrl}') !important;
mask-image: url('${maskUrl}') !important;
-webkit-mask-repeat: no-repeat !important;
mask-repeat: no-repeat !important;
-webkit-mask-position: ${x - maxRadius}px ${y - maxRadius}px !important;
mask-position: ${x - maxRadius}px ${y - maxRadius}px !important;
-webkit-mask-size: ${maxRadius * 2}px !important;
mask-size: ${maxRadius * 2}px !important;
z-index: 1000 !important;
transition: -webkit-mask-position ${ANIMATION_DURATION/1000}s ease-out, -webkit-mask-size ${ANIMATION_DURATION/1000}s ease-out,
mask-position ${ANIMATION_DURATION/1000}s ease-out, mask-size ${ANIMATION_DURATION/1000}s ease-out !important;
}
`;
}
});
// 清理临时样式
setTimeout(() => {
if (document.getElementById('theme-transition-temp-style')) {
document.getElementById('theme-transition-temp-style').remove();
}
}, TOTAL_TRANSITION_TIME);
} else {
// 改进收缩效果实现
style.textContent = `
::view-transition-old(root) {
animation: none !important;
-webkit-mask-image: url('${maskUrl}') !important;
mask-image: url('${maskUrl}') !important;
-webkit-mask-repeat: no-repeat !important;
mask-repeat: no-repeat !important;
-webkit-mask-position: ${x - maxRadius}px ${y - maxRadius}px !important;
mask-position: ${x - maxRadius}px ${y - maxRadius}px !important;
-webkit-mask-size: ${maxRadius * 2}px !important;
mask-size: ${maxRadius * 2}px !important;
z-index: 999 !important;
}
::view-transition-new(root) {
z-index: 998 !important;
}
`;
document.head.appendChild(style);
// 强制重新计算样式
window.getComputedStyle(document.documentElement).getPropertyValue('--force-reflow');
// 使用双重requestAnimationFrame确保样式先被应用
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (style.parentNode) {
style.textContent = `
::view-transition-old(root) {
animation: none !important;
-webkit-mask-image: url('${maskUrl}') !important;
mask-image: url('${maskUrl}') !important;
-webkit-mask-repeat: no-repeat !important;
mask-repeat: no-repeat !important;
-webkit-mask-position: ${x}px ${y}px !important;
mask-position: ${x}px ${y}px !important;
-webkit-mask-size: 0 !important;
mask-size: 0 !important;
z-index: 999 !important;
transition: -webkit-mask-position ${ANIMATION_DURATION/1000}s ease-in, -webkit-mask-size ${ANIMATION_DURATION/1000}s ease-in,
mask-position ${ANIMATION_DURATION/1000}s ease-in, mask-size ${ANIMATION_DURATION/1000}s ease-in !important;
}
::view-transition-new(root) {
z-index: 998 !important;
}
`;
}
});
});
}
// 清理临时样式
setTimeout(() => {
if (document.getElementById('theme-transition-temp-style')) {
document.getElementById('theme-transition-temp-style').remove();
}
}, TOTAL_TRANSITION_TIME);
} catch (error) {
// AbortError是正常的我们完全可以忽略它但仍保留调试日志
if (error.name === 'AbortError') {
// 降级为debug级别日志不要吓到用户
console.debug(`主题切换过渡被跳过,这是正常现象`);
// 使用备用动画方案
applyFallbackAnimation();
} else if (error.message === 'Ready timeout') {
// 超时是我们主动抛出的,只需要降级处理
console.debug(`主题切换过渡准备超时,继续执行动画`);
// 即使ready超时我们仍然可以应用样式
// 这里可以直接调用备用方案应用动画
applyFallbackAnimation();
} else {
// 只有真正意外的错误才输出error级别日志
console.error(`主题切换应用自定义动画出错:`, error);
// 尝试使用备用方案
applyFallbackAnimation();
}
}
};
// 添加备用动画方案
const applyFallbackAnimation = () => {
try {
// 创建并应用样式但不等待transition.ready
const style = document.createElement('style');
style.id = 'theme-transition-temp-style';
if (animationType === TRANSITION_MODES.EXPAND) {
// 扩散效果样式 - 保持不变
style.textContent = `
::view-transition-new(root) {
animation: none !important;
-webkit-mask-image: url('${maskUrl}') !important;
mask-image: url('${maskUrl}') !important;
-webkit-mask-repeat: no-repeat !important;
mask-repeat: no-repeat !important;
-webkit-mask-position: ${x}px ${y}px !important;
mask-position: ${x}px ${y}px !important;
-webkit-mask-size: 0 !important;
mask-size: 0 !important;
z-index: 1000 !important;
}
`;
document.head.appendChild(style);
// 强制重新计算样式
window.getComputedStyle(document.documentElement).getPropertyValue('--force-reflow');
// 使用双重requestAnimationFrame确保样式先被应用
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (style.parentNode) {
style.textContent = `
::view-transition-new(root) {
animation: none !important;
-webkit-mask-image: url('${maskUrl}') !important;
mask-image: url('${maskUrl}') !important;
-webkit-mask-repeat: no-repeat !important;
mask-repeat: no-repeat !important;
-webkit-mask-position: ${x - maxRadius}px ${y - maxRadius}px !important;
mask-position: ${x - maxRadius}px ${y - maxRadius}px !important;
-webkit-mask-size: ${maxRadius * 2}px !important;
mask-size: ${maxRadius * 2}px !important;
z-index: 1000 !important;
transition: -webkit-mask-position ${ANIMATION_DURATION/1000}s ease-out, -webkit-mask-size ${ANIMATION_DURATION/1000}s ease-out,
mask-position ${ANIMATION_DURATION/1000}s ease-out, mask-size ${ANIMATION_DURATION/1000}s ease-out !important;
}
`;
}
});
});
} else {
// 改进收缩效果实现
style.textContent = `
::view-transition-old(root) {
animation: none !important;
-webkit-mask-image: url('${maskUrl}') !important;
mask-image: url('${maskUrl}') !important;
-webkit-mask-repeat: no-repeat !important;
mask-repeat: no-repeat !important;
-webkit-mask-position: ${x - maxRadius}px ${y - maxRadius}px !important;
mask-position: ${x - maxRadius}px ${y - maxRadius}px !important;
-webkit-mask-size: ${maxRadius * 2}px !important;
mask-size: ${maxRadius * 2}px !important;
z-index: 999 !important;
}
::view-transition-new(root) {
z-index: 998 !important;
}
`;
document.head.appendChild(style);
// 强制重新计算样式
window.getComputedStyle(document.documentElement).getPropertyValue('--force-reflow');
// 使用双重requestAnimationFrame确保样式先被应用
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (style.parentNode) {
style.textContent = `
::view-transition-old(root) {
animation: none !important;
-webkit-mask-image: url('${maskUrl}') !important;
mask-image: url('${maskUrl}') !important;
-webkit-mask-repeat: no-repeat !important;
mask-repeat: no-repeat !important;
-webkit-mask-position: ${x}px ${y}px !important;
mask-position: ${x}px ${y}px !important;
-webkit-mask-size: 0 !important;
mask-size: 0 !important;
z-index: 999 !important;
transition: -webkit-mask-position ${ANIMATION_DURATION/1000}s ease-in, -webkit-mask-size ${ANIMATION_DURATION/1000}s ease-in,
mask-position ${ANIMATION_DURATION/1000}s ease-in, mask-size ${ANIMATION_DURATION/1000}s ease-in !important;
}
::view-transition-new(root) {
z-index: 998 !important;
}
`;
}
});
});
}
// 清理临时样式
setTimeout(() => {
if (document.getElementById('theme-transition-temp-style')) {
document.getElementById('theme-transition-temp-style').remove();
}
}, TOTAL_TRANSITION_TIME);
} catch (err) {
console.debug(`主题切换应用备用动画方案出错:`, err);
}
};
// 非阻塞执行自定义动画应用
applyCustomAnimation().catch(error => {
console.error(`主题切换应用动画过程出错:`, error);
});
// 返回过渡完成的Promise
return transition.finished
.then(() => {
// 移除主题过渡标记类
document.documentElement.classList.remove('theme-transition-active');
window._themeTransition = null;
})
.catch(error => {
if (error.name === 'AbortError') {
console.debug(`主题切换过渡被中断: 这是正常现象,可能是新的过渡开始或页面导航变化`);
} else {
console.error(`主题切换过渡动画错误:`, error);
}
// 确保标记类被移除
document.documentElement.classList.remove('theme-transition-active');
// 清除引用
window._themeTransition = null;
});
} catch (error) {
console.error(`主题切换错误:`, error);
// 在出错时也要执行回调
callback();
// 确保标记类被移除
document.documentElement.classList.remove('theme-transition-active');
return Promise.resolve();
}
}
// 设置主题切换功能
function setupThemeToggle() {
// 获取所有主题切换按钮
const themeToggleButtons = document.querySelectorAll("#theme-toggle-button");
if (!themeToggleButtons.length) {
console.warn(`主题切换未找到主题切换按钮`);
return;
}
// 防抖配置 - 使用前面定义的动态计算值
let lastClickTime = 0;
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 = () => {
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
};
// 初始化主题
const initializeTheme = () => {
const storedTheme = localStorage.getItem("theme");
const systemTheme = getSystemTheme();
// 按照逻辑优先级应用主题
if (storedTheme) {
document.documentElement.dataset.theme = storedTheme;
} else if (systemTheme) {
document.documentElement.dataset.theme = systemTheme;
} else {
document.documentElement.dataset.theme = "light";
}
};
// 防抖函数 - 确保在指定时间内只执行一次
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;
}
// 第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 {
const rect = targetButton.getBoundingClientRect();
clickX = rect.left + rect.width / 2;
clickY = rect.top + rect.height / 2;
}
// 确保坐标有效防止NaN或无限值
clickX = isFinite(clickX) ? clickX : window.innerWidth / 2;
clickY = isFinite(clickY) ? clickY : window.innerHeight / 2;
// 在按钮上创建小波纹效果
createRippleEffect(clickX, clickY, targetButton);
// 获取当前主题
const currentTheme = document.documentElement.dataset.theme;
const newTheme = currentTheme === "light" ? "dark" : "light";
// 获取过渡模式
let transitionMode = TRANSITION_MODES.AUTO; // 默认自动模式
if (targetButton && targetButton.dataset && targetButton.dataset.transitionMode) {
transitionMode = targetButton.dataset.transitionMode;
} else {
// 如果无法从按钮获取,从本地存储获取
transitionMode = getThemeTransitionMode();
}
// 在开始新的过渡前先重置transitioning的延时清除
if (transitionTimeout) {
clearTimeout(transitionTimeout);
transitionTimeout = null;
}
// 创建一个安全的主题切换回调
const safeThemeChangeCallback = () => {
try {
// 直接在这里执行主题切换的核心逻辑
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
if (newTheme === systemTheme) {
localStorage.removeItem("theme");
} else {
localStorage.setItem("theme", newTheme);
}
} catch (err) {
console.error(`主题切换回调执行出错:`, err);
}
};
// 调用视图过渡
createViewTransition(
safeThemeChangeCallback,
clickX,
clickY,
currentTheme,
newTheme,
transitionMode
).then(() => {
// 过渡完成后恢复状态
setTimeout(() => {
window._themeTransitioning = false;
console.debug('主题切换: 过渡完成,恢复状态');
}, ANIMATION_BUFFER); // 添加缓冲时间
}).catch(error => {
// 出现错误时强制执行主题切换以确保功能可用
if (error.name !== 'AbortError') {
console.error(`主题切换过渡动画错误:`, error);
// 如果不是因为过渡被跳过而是真正的错误,直接执行主题切换
safeThemeChangeCallback();
} else {
console.debug(`主题切换过渡被跳过,这是正常现象`);
}
setTimeout(() => {
window._themeTransitioning = false;
console.debug('主题切换: 过渡被取消,恢复状态');
}, ANIMATION_BUFFER);
});
// 设置防抖保底 - 防止transition.finished不触发导致状态卡死
transitionTimeout = setTimeout(() => {
window._themeTransitioning = false;
console.debug('主题切换: 过渡超时,强制恢复状态');
}, TOTAL_TRANSITION_TIME + 200); // 总过渡时间加额外缓冲
} catch (err) {
// 即使发生错误,也要确保主题能切换
console.error(`主题切换处理过程出错:`, err);
// 尝试直接切换主题
try {
const currentTheme = document.documentElement.dataset.theme || 'light';
const newTheme = currentTheme === "light" ? "dark" : "light";
// 更新HTML属性
document.documentElement.dataset.theme = newTheme;
// 更新本地存储
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
if (newTheme === systemTheme) {
localStorage.removeItem("theme");
} else {
localStorage.setItem("theme", newTheme);
}
} catch (fallbackErr) {
console.error(`主题切换降级处理失败:`, fallbackErr);
}
// 确保状态被重置
window._themeTransitioning = false;
console.debug('主题切换: 发生错误,恢复状态');
}
};
// 监听系统主题变化
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleMediaChange = (e) => {
if (!localStorage.getItem("theme")) {
const newTheme = e.matches ? "dark" : "light";
document.documentElement.dataset.theme = newTheme;
}
};
// 添加系统主题变化监听
addListener(mediaQuery, "change", handleMediaChange);
// 为每个按钮添加事件
themeToggleButtons.forEach(button => {
// 点击事件 - 使用捕获模式
addListener(button, "click", (e) => handleThemeToggle(e, button), { capture: true });
// 键盘事件
const keydownHandler = (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
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")) {
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();
}
// 初始化函数
function init() {
// 注册清理事件
registerCleanupEvents();
// 设置主题切换功能
setupThemeToggle();
// 设置防止内存泄漏的自动清理 - 最多30秒后自毁
setTimeout(() => {
// 如果页面没有被卸载,检查是否已经清理过
if (document.body && !document._themeToggleCleanedUp) {
// 为避免重复清理,设置一个标记
document._themeToggleCleanedUp = true;
// 执行清理
selfDestruct();
console.debug('主题切换: 自动清理已执行');
}
}, 30000);
}
// 判断DOM是否已加载
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init, { once: true });
} else {
init();
}
})();
</script>