Files

126 lines
3.4 KiB
Vue

<template>
<button
ref="toggleBtn"
@click="toggle"
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors flex items-center justify-center"
:title="isDark ? '切换到亮色模式' : '切换到暗黑模式'"
>
<!-- 太阳图标 - 暗黑模式下显示 -->
<Icon
v-if="isDark"
name="heroicons:sun-solid"
class="w-5 h-5 text-yellow-400 flex-shrink-0"
/>
<!-- 月亮图标 - 亮色模式下显示 -->
<Icon
v-else
name="heroicons:moon-solid"
class="w-5 h-5 text-gray-600 dark:text-gray-400 flex-shrink-0"
/>
</button>
</template>
<script setup>
// 直接从 DOM 读取当前主题状态,确保与实际状态同步
const isDark = ref(false)
const isInitialized = ref(false)
const toggleBtn = ref(null)
// 同步读取当前 DOM 状态
const syncFromDOM = () => {
return document.documentElement.classList.contains('dark')
}
onMounted(() => {
// 从 localStorage 读取主题设置
const savedTheme = localStorage.getItem('theme')
let shouldBeDark = false
if (savedTheme) {
shouldBeDark = savedTheme === 'dark'
} else {
// 检测系统主题
shouldBeDark = window.matchMedia('(prefers-color-scheme: dark)').matches
}
// 直接设置 DOM,然后同步状态
if (shouldBeDark) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
// 同步响应式状态
isDark.value = shouldBeDark
isInitialized.value = true
})
// 执行主题切换
const applyThemeChange = (newDarkState) => {
if (newDarkState) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
isDark.value = newDarkState
localStorage.setItem('theme', newDarkState ? 'dark' : 'light')
}
const toggle = async (event) => {
// 防止未初始化时的点击
if (!isInitialized.value) return
// 先从 DOM 读取当前真实状态,避免状态不同步
const currentDarkState = syncFromDOM()
const newDarkState = !currentDarkState
// 检查是否支持 View Transitions API
if (!document.startViewTransition) {
// 不支持时直接切换
applyThemeChange(newDarkState)
return
}
// 获取点击位置(按钮中心)
const btn = toggleBtn.value
const rect = btn.getBoundingClientRect()
const x = rect.left + rect.width / 2
const y = rect.top + rect.height / 2
// 计算扩散半径(从点击位置到最远角落的距离)
const endRadius = Math.hypot(
Math.max(x, window.innerWidth - x),
Math.max(y, window.innerHeight - y)
)
// 设置 CSS 变量用于动画
document.documentElement.style.setProperty('--theme-toggle-x', `${x}px`)
document.documentElement.style.setProperty('--theme-toggle-y', `${y}px`)
document.documentElement.style.setProperty('--theme-toggle-radius', `${endRadius}px`)
// 使用 View Transitions API 进行过渡
const transition = document.startViewTransition(() => {
applyThemeChange(newDarkState)
})
// 等待过渡完成
await transition.ready
// 圆形扩散动画 - 始终对新视图应用
const clipPathStart = `circle(0px at ${x}px ${y}px)`
const clipPathEnd = `circle(${endRadius}px at ${x}px ${y}px)`
document.documentElement.animate(
{
clipPath: [clipPathStart, clipPathEnd]
},
{
duration: 500,
easing: 'ease-out',
pseudoElement: '::view-transition-new(root)'
}
)
}
</script>