"use client" import * as React from "react" import { flushSync } from "react-dom" import { Moon, Sun } from "lucide-react" import { useTheme } from "next-themes" import { cn } from "@/lib/utils" /** * 主题切换组件 - 滑动开关样式 + 圆形扩展动画 */ export function ThemeToggle() { const { setTheme, resolvedTheme } = useTheme() const [mounted, setMounted] = React.useState(false) const [isToggled, setIsToggled] = React.useState(false) const buttonRef = React.useRef(null) React.useEffect(() => { setMounted(true) }, []) React.useEffect(() => { if (mounted) { setIsToggled(resolvedTheme === "dark") } }, [resolvedTheme, mounted]) const handleToggle = async () => { const newIsDark = !isToggled const newTheme = newIsDark ? "dark" : "light" // 不支持 View Transitions 或用户偏好减少动画,直接切换 if ( !buttonRef.current || !('startViewTransition' in document) || window.matchMedia('(prefers-reduced-motion: reduce)').matches ) { setIsToggled(newIsDark) setTheme(newTheme) return } // 1. 先让滑块滑动(不触发主题切换) setIsToggled(newIsDark) // 2. 等待滑块动画完成(100ms) await new Promise(r => setTimeout(r, 100)) // 获取按钮位置 const { top, left, width, height } = buttonRef.current.getBoundingClientRect() const x = left + width / 2 const y = top + height / 2 const right = window.innerWidth - left const bottom = window.innerHeight - top const maxRadius = Math.hypot(Math.max(left, right), Math.max(top, bottom)) // 禁用默认的 View Transition 动画 const style = document.createElement('style') style.textContent = ` ::view-transition-old(root), ::view-transition-new(root) { animation: none; mix-blend-mode: normal; } ` document.head.appendChild(style) // 3. 滑块滑完后,启动 View Transition 切换主题 const transition = (document as any).startViewTransition(() => { flushSync(() => { setTheme(newTheme) }) }) await transition.ready document.documentElement.animate( { clipPath: [ `circle(0px at ${x}px ${y}px)`, `circle(${maxRadius}px at ${x}px ${y}px)`, ], }, { duration: 500, easing: 'ease-out', pseudoElement: '::view-transition-new(root)', } ) transition.finished.then(() => { style.remove() }) } if (!mounted) { return (
) } return ( ) }