Files
InsightRadar/frontend/src/components/ThemeToggle.vue
T

149 lines
3.6 KiB
Vue

<script setup lang="ts">
import { useThemeStore } from '@/stores/theme'
const themeStore = useThemeStore()
/**
* 切换主题:支持 View Transitions API 时使用点击位置扩散动画,
* 否则直接切换
*/
function handleToggle(event: MouseEvent) {
// 检测浏览器是否支持 View Transitions 且用户未开启减弱动画
const isAppearanceTransition = typeof document !== 'undefined' &&
'startViewTransition' in document &&
!window.matchMedia('(prefers-reduced-motion: reduce)').matches
if (!isAppearanceTransition) {
// 不支持时直接切换,无动画
themeStore.toggleTheme()
return
}
const x = event.clientX
const y = event.clientY
const endRadius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y)
)
const transition = document.startViewTransition(() => {
themeStore.toggleTheme()
})
transition.ready.then(() => {
const clipPath = [
`circle(0px at ${x}px ${y}px)`,
`circle(${endRadius}px at ${x}px ${y}px)`
]
document.documentElement.animate(
{
clipPath: clipPath,
},
{
duration: 500,
easing: 'ease-in-out',
pseudoElement: '::view-transition-new(root)',
}
)
})
}
</script>
<template>
<button
class="theme-toggle-btn"
:aria-label="themeStore.isDark ? '切换到浅色模式' : '切换到暗黑模式'"
title="切换显示模式"
@click="handleToggle"
>
<div class="icon-container">
<i class="fa-solid fa-sun sun-icon" :class="{ 'is-hidden': themeStore.isDark }"></i>
<i class="fa-solid fa-moon moon-icon" :class="{ 'is-hidden': !themeStore.isDark }"></i>
</div>
</button>
</template>
<style scoped>
/* ==========================================
极简且高级的毛玻璃材质主题切换按钮
========================================== */
.theme-toggle-btn {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
border-radius: 50%;
background: var(--bg-surface);
/* 使用轻微透明度与模糊实现毛玻璃质感 */
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid var(--border-subtle);
color: var(--text-secondary);
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: var(--shadow-sm);
overflow: hidden;
outline: none;
}
.theme-toggle-btn:hover {
color: var(--text-primary);
border-color: var(--border-strong);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.theme-toggle-btn:active {
transform: translateY(0);
box-shadow: var(--shadow-sm);
}
.icon-container {
position: relative;
width: 20px;
height: 20px;
}
/* 图标动画:旋转加缩放的平滑过渡 */
.sun-icon, .moon-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 18px;
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 隐藏状态:优雅地旋出 */
.sun-icon.is-hidden {
opacity: 0;
transform: translate(-50%, -50%) rotate(90deg) scale(0.5);
}
.moon-icon.is-hidden {
opacity: 0;
transform: translate(-50%, -50%) rotate(-90deg) scale(0.5);
}
</style>
<style>
/* ==========================================
全局 View Transitions API 动画样式
控制页面级别的黑白模式无缝扩散切换
========================================== */
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
::view-transition-old(root) {
z-index: 1;
}
::view-transition-new(root) {
z-index: 9999;
}
</style>