mirror of
https://github.com/stardrophere/InsightRadar.git
synced 2026-06-05 23:56:36 +08:00
150 lines
3.8 KiB
Vue
150 lines
3.8 KiB
Vue
<script setup lang="ts">
|
|
import { useThemeStore } from '@/stores/theme'
|
|
|
|
const themeStore = useThemeStore()
|
|
|
|
/**
|
|
* 切换主题,使用 View Transitions API 实现高级扩散动画(如果浏览器支持)
|
|
* 这种动画比之前像玩具一样的开关要高级得多,提供原生级的丝滑过渡
|
|
*/
|
|
function handleToggle(event: MouseEvent) {
|
|
// 检查浏览器是否支持 document.startViewTransition 并且用户没有开启减弱动画
|
|
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)
|
|
)
|
|
|
|
// @ts-ignore: TypeScript 类型可能较旧,忽略 startViewTransition 报错
|
|
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>
|