|
@@ -0,0 +1,167 @@
|
|
|
+import { useState, useEffect, useRef } from 'react';
|
|
|
+import { Button } from 'antd';
|
|
|
+import { useAppDispatch, useAppSelector } from '../states/store';
|
|
|
+import { setTheme } from '../states/themeSlice';
|
|
|
+
|
|
|
+interface Position {
|
|
|
+ top: number;
|
|
|
+ right: number;
|
|
|
+}
|
|
|
+
|
|
|
+interface ThemeSwitcherProps {
|
|
|
+ initialPosition?: Position;
|
|
|
+ buttonSize?: number;
|
|
|
+}
|
|
|
+
|
|
|
+const STORAGE_KEY = 'theme-switcher-position';
|
|
|
+
|
|
|
+export default function ThemeSwitcher({
|
|
|
+ initialPosition = { top: 50, right: 16 },
|
|
|
+ buttonSize = 24,
|
|
|
+}: ThemeSwitcherProps): JSX.Element {
|
|
|
+ const dispatch = useAppDispatch();
|
|
|
+ const { themeType, currentTheme } = useAppSelector((state) => state.theme);
|
|
|
+
|
|
|
+ // 从 localStorage 恢复位置或使用初始位置
|
|
|
+ const [position, setPosition] = useState<Position>(() => {
|
|
|
+ try {
|
|
|
+ const saved = localStorage.getItem(STORAGE_KEY);
|
|
|
+ return saved ? JSON.parse(saved) : initialPosition;
|
|
|
+ } catch {
|
|
|
+ return initialPosition;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ const [isDragging, setIsDragging] = useState(false);
|
|
|
+ const isDraggingRef = useRef(false);
|
|
|
+ const dragStartPos = useRef({ x: 0, y: 0 });
|
|
|
+ const elementStartPos = useRef<Position>({ top: 0, right: 0 });
|
|
|
+
|
|
|
+ // 保存位置到 localStorage
|
|
|
+ useEffect(() => {
|
|
|
+ try {
|
|
|
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(position));
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('Failed to save theme switcher position:', error);
|
|
|
+ }
|
|
|
+ }, [position]);
|
|
|
+
|
|
|
+ const handleDragStart = (e: React.MouseEvent): void => {
|
|
|
+ e.preventDefault();
|
|
|
+ isDraggingRef.current = true;
|
|
|
+ setIsDragging(true);
|
|
|
+ dragStartPos.current = { x: e.clientX, y: e.clientY };
|
|
|
+ elementStartPos.current = { ...position };
|
|
|
+
|
|
|
+ // 添加全局事件监听
|
|
|
+ document.addEventListener('mousemove', handleDrag as any);
|
|
|
+ document.addEventListener('mouseup', handleDragEnd);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleDrag = (e: MouseEvent): void => {
|
|
|
+ if (!isDraggingRef.current) return;
|
|
|
+
|
|
|
+ const deltaX = e.clientX - dragStartPos.current.x;
|
|
|
+ const deltaY = e.clientY - dragStartPos.current.y;
|
|
|
+
|
|
|
+ // 计算新位置(限制在窗口范围内)
|
|
|
+ const windowHeight = window.innerHeight;
|
|
|
+ const newTopPx = (elementStartPos.current.top / 100) * windowHeight + deltaY;
|
|
|
+ const newTopPercent = (newTopPx / windowHeight) * 100;
|
|
|
+
|
|
|
+ const newRight = Math.max(0, elementStartPos.current.right - deltaX);
|
|
|
+
|
|
|
+ // 限制 top 在 0-100% 之间
|
|
|
+ const clampedTop = Math.max(0, Math.min(100, newTopPercent));
|
|
|
+
|
|
|
+ setPosition({
|
|
|
+ top: clampedTop,
|
|
|
+ right: newRight,
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleDragEnd = (): void => {
|
|
|
+ isDraggingRef.current = false;
|
|
|
+ setIsDragging(false);
|
|
|
+ document.removeEventListener('mousemove', handleDrag as any);
|
|
|
+ document.removeEventListener('mouseup', handleDragEnd);
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ position: 'fixed',
|
|
|
+ top: `${position.top}%`,
|
|
|
+ right: position.right,
|
|
|
+ transform: 'translateY(-50%)',
|
|
|
+ display: 'flex',
|
|
|
+ flexDirection: 'column',
|
|
|
+ alignItems: 'center',
|
|
|
+ gap: 4,
|
|
|
+ zIndex: 1000,
|
|
|
+ userSelect: 'none',
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {/* 拖拽手柄 */}
|
|
|
+ <div
|
|
|
+ onMouseDown={handleDragStart}
|
|
|
+ style={{
|
|
|
+ cursor: isDragging ? 'grabbing' : 'grab',
|
|
|
+ padding: '4px 8px',
|
|
|
+ fontSize: '14px',
|
|
|
+ color: currentTheme.token.colorText,
|
|
|
+ opacity: 0.6,
|
|
|
+ textAlign: 'center',
|
|
|
+ lineHeight: 1,
|
|
|
+ }}
|
|
|
+ title="拖动以移动主题切换器"
|
|
|
+ >
|
|
|
+ ⋮⋮
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 深色主题按钮 */}
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ shape="circle"
|
|
|
+ size="small"
|
|
|
+ onClick={() => dispatch(setTheme('dark'))}
|
|
|
+ title="切换到深色主题"
|
|
|
+ disabled={themeType === 'dark'}
|
|
|
+ style={{
|
|
|
+ width: buttonSize,
|
|
|
+ height: buttonSize,
|
|
|
+ minWidth: buttonSize,
|
|
|
+ padding: 0,
|
|
|
+ display: 'flex',
|
|
|
+ alignItems: 'center',
|
|
|
+ justifyContent: 'center',
|
|
|
+ fontSize: Math.floor(buttonSize * 0.5),
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ 🌙
|
|
|
+ </Button>
|
|
|
+
|
|
|
+ {/* 浅色主题按钮 */}
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ shape="circle"
|
|
|
+ size="small"
|
|
|
+ onClick={() => dispatch(setTheme('light'))}
|
|
|
+ title="切换到浅色主题"
|
|
|
+ disabled={themeType === 'light'}
|
|
|
+ style={{
|
|
|
+ width: buttonSize,
|
|
|
+ height: buttonSize,
|
|
|
+ minWidth: buttonSize,
|
|
|
+ padding: 0,
|
|
|
+ display: 'flex',
|
|
|
+ alignItems: 'center',
|
|
|
+ justifyContent: 'center',
|
|
|
+ fontSize: Math.floor(buttonSize * 0.5),
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ ☀️
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|