FloatingButton.tsx 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. import { useState, useRef } from 'react';
  2. import Draggable, { type DraggableEvent, type DraggableData } from 'react-draggable';
  3. import { Gamepad2 } from 'lucide-react';
  4. interface FloatingButtonProps {
  5. onClick: () => void;
  6. }
  7. export function FloatingButton({ onClick }: FloatingButtonProps) {
  8. const [position, setPosition] = useState({ x: 20, y: 100 });
  9. const [isDragging, setIsDragging] = useState(false);
  10. const [isIdle, setIsIdle] = useState(false);
  11. const nodeRef = useRef<HTMLDivElement>(null);
  12. const idleTimerRef = useRef<any>(null);
  13. const dragRef = useRef(false); // Track real drag movement
  14. // 清除空闲倒计时
  15. const clearIdleTimer = () => {
  16. if (idleTimerRef.current) {
  17. clearTimeout(idleTimerRef.current);
  18. idleTimerRef.current = null;
  19. }
  20. setIsIdle(false);
  21. };
  22. const handleDragStart = () => {
  23. dragRef.current = false;
  24. clearIdleTimer();
  25. setIsDragging(true);
  26. // 如果处于空闲状态(半隐藏),视觉上立刻恢复原状
  27. if (isIdle) {
  28. setPosition(prev => {
  29. const btnWidth = nodeRef.current ? nodeRef.current.offsetWidth : 48;
  30. return {
  31. ...prev,
  32. x: prev.x < 0 ? 0 : window.innerWidth - btnWidth
  33. };
  34. });
  35. }
  36. };
  37. const handleDrag = (_e: DraggableEvent, data: DraggableData) => {
  38. dragRef.current = true;
  39. setPosition({ x: data.x, y: data.y });
  40. };
  41. const handleDragStop = (_e: DraggableEvent, data: DraggableData) => {
  42. setIsDragging(false);
  43. if (!dragRef.current) {
  44. onClick();
  45. }
  46. // 自动吸附屏幕左右边缘
  47. const btnWidth = nodeRef.current ? nodeRef.current.offsetWidth : 48; // 默认 48px
  48. const btnHeight = nodeRef.current ? nodeRef.current.offsetHeight : 48;
  49. // 限制不要拖出上下边缘
  50. let endY = data.y;
  51. const maxY = window.innerHeight - btnHeight;
  52. endY = Math.max(0, Math.min(endY, maxY));
  53. let endX = data.x;
  54. const centerX = data.x + (btnWidth / 2);
  55. const windowCenterX = window.innerWidth / 2;
  56. endX = centerX < windowCenterX ? 0 : window.innerWidth - btnWidth;
  57. setPosition({ x: endX, y: endY });
  58. // 启动 3 秒空闲隐藏计时器
  59. idleTimerRef.current = setTimeout(() => {
  60. setIsIdle(true);
  61. // 将半个按钮宽度移出屏幕之外
  62. setPosition(prev => {
  63. const halfWidth = btnWidth / 2;
  64. const newX = prev.x < windowCenterX ? -halfWidth : window.innerWidth - halfWidth;
  65. return { ...prev, x: newX };
  66. });
  67. }, 3000);
  68. };
  69. return (
  70. <Draggable
  71. nodeRef={nodeRef}
  72. position={position}
  73. onStart={handleDragStart}
  74. onDrag={handleDrag}
  75. onStop={handleDragStop}
  76. >
  77. <div
  78. ref={nodeRef}
  79. className={`fixed top-0 left-0 z-50 flex items-center justify-center w-12 h-12 bg-primary rounded-full shadow-[0_4px_0_0_rgba(0,0,0,0.15)] cursor-move active:scale-95
  80. ${isDragging ? 'opacity-80 scale-105 shadow-xl transition-none' : 'opacity-100 hover:scale-110 transition-all duration-300 ease-out'}
  81. ${isIdle ? 'opacity-50' : ''}`}
  82. style={{ touchAction: 'none' }}
  83. >
  84. <Gamepad2 className="text-white w-6 h-6 pointer-events-none" />
  85. </div>
  86. </Draggable>
  87. );
  88. }