mixvideo-v2/apps/desktop/src/components/InteractiveButton.tsx

267 lines
7.8 KiB
TypeScript

import React, { useState, useRef } from 'react';
import { Loader2 } from 'lucide-react';
interface InteractiveButtonProps {
children: React.ReactNode;
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void | Promise<void>;
variant?: 'primary' | 'secondary' | 'danger' | 'success' | 'ghost' | 'outline';
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
disabled?: boolean;
loading?: boolean;
icon?: React.ReactNode;
iconPosition?: 'left' | 'right';
fullWidth?: boolean;
ripple?: boolean;
haptic?: boolean;
className?: string;
type?: 'button' | 'submit' | 'reset';
}
/**
* 增强的交互按钮组件
* 提供丰富的视觉反馈和微交互效果
*/
export const InteractiveButton: React.FC<InteractiveButtonProps> = ({
children,
onClick,
variant = 'primary',
size = 'md',
disabled = false,
loading = false,
icon,
iconPosition = 'left',
fullWidth = false,
ripple = true,
haptic = true,
className = '',
type = 'button',
}) => {
const [isPressed, setIsPressed] = useState(false);
const [ripples, setRipples] = useState<Array<{ id: number; x: number; y: number }>>([]);
const buttonRef = useRef<HTMLButtonElement>(null);
const rippleIdRef = useRef(0);
const getVariantClasses = () => {
const variants = {
primary: 'bg-gradient-to-r from-primary-600 to-primary-700 hover:from-primary-700 hover:to-primary-800 text-white shadow-sm hover:shadow-md focus:ring-primary-500',
secondary: 'bg-gray-100 hover:bg-gray-200 text-gray-900 shadow-sm hover:shadow focus:ring-gray-500',
danger: 'bg-gradient-to-r from-red-600 to-red-700 hover:from-red-700 hover:to-red-800 text-white shadow-sm hover:shadow-md focus:ring-red-500',
success: 'bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 text-white shadow-sm hover:shadow-md focus:ring-green-500',
ghost: 'hover:bg-gray-100 text-gray-700 hover:text-gray-900 focus:ring-gray-500',
outline: 'border border-gray-300 hover:border-gray-400 bg-white hover:bg-gray-50 text-gray-700 hover:text-gray-900 shadow-sm focus:ring-gray-500',
};
return variants[variant];
};
const getSizeClasses = () => {
const sizes = {
xs: 'px-2 py-1 text-xs',
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-sm',
lg: 'px-5 py-2.5 text-base',
xl: 'px-6 py-3 text-lg',
};
return sizes[size];
};
const getIconSize = () => {
const iconSizes = {
xs: 'w-3 h-3',
sm: 'w-4 h-4',
md: 'w-4 h-4',
lg: 'w-5 h-5',
xl: 'w-6 h-6',
};
return iconSizes[size];
};
const handleClick = async (e: React.MouseEvent<HTMLButtonElement>) => {
if (disabled || loading) return;
// 添加按压效果
setIsPressed(true);
setTimeout(() => setIsPressed(false), 150);
// 添加涟漪效果
if (ripple && buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const newRipple = { id: rippleIdRef.current++, x, y };
setRipples(prev => [...prev, newRipple]);
// 移除涟漪效果
setTimeout(() => {
setRipples(prev => prev.filter(r => r.id !== newRipple.id));
}, 600);
}
// 触觉反馈(如果支持)
if (haptic && 'vibrate' in navigator) {
navigator.vibrate(10);
}
// 执行点击处理
if (onClick) {
await onClick(e);
}
};
const baseClasses = `
relative overflow-hidden
inline-flex items-center justify-center
font-medium rounded-lg
transition-all duration-200 ease-out
focus:outline-none focus:ring-2 focus:ring-offset-2
disabled:opacity-50 disabled:cursor-not-allowed
transform hover:scale-105 active:scale-95
${isPressed ? 'animate-button-press' : ''}
${fullWidth ? 'w-full' : ''}
`;
return (
<button
ref={buttonRef}
type={type}
className={`${baseClasses} ${getVariantClasses()} ${getSizeClasses()} ${className}`}
onClick={handleClick}
disabled={disabled || loading}
>
{/* 涟漪效果 */}
{ripples.map((ripple) => (
<span
key={ripple.id}
className="absolute bg-white bg-opacity-30 rounded-full animate-ping"
style={{
left: ripple.x - 10,
top: ripple.y - 10,
width: 20,
height: 20,
}}
/>
))}
{/* 按钮内容 */}
<span className="relative flex items-center gap-2">
{loading ? (
<Loader2 className={`${getIconSize()} animate-spin`} />
) : (
icon && iconPosition === 'left' && (
<span className={getIconSize()}>{icon}</span>
)
)}
<span>{children}</span>
{!loading && icon && iconPosition === 'right' && (
<span className={getIconSize()}>{icon}</span>
)}
</span>
</button>
);
};
/**
* 浮动操作按钮
*/
interface FloatingActionButtonProps {
onClick?: () => void;
icon: React.ReactNode;
tooltip?: string;
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
className?: string;
}
export const FloatingActionButton: React.FC<FloatingActionButtonProps> = ({
onClick,
icon,
tooltip,
variant = 'primary',
size = 'md',
position = 'bottom-right',
className = '',
}) => {
const [showTooltip, setShowTooltip] = useState(false);
const getVariantClasses = () => {
const variants = {
primary: 'bg-gradient-to-r from-primary-600 to-primary-700 hover:from-primary-700 hover:to-primary-800 text-white shadow-lg hover:shadow-xl',
secondary: 'bg-white hover:bg-gray-50 text-gray-700 shadow-lg hover:shadow-xl border border-gray-200',
danger: 'bg-gradient-to-r from-red-600 to-red-700 hover:from-red-700 hover:to-red-800 text-white shadow-lg hover:shadow-xl',
};
return variants[variant];
};
const getSizeClasses = () => {
const sizes = {
sm: 'w-12 h-12',
md: 'w-14 h-14',
lg: 'w-16 h-16',
};
return sizes[size];
};
const getPositionClasses = () => {
const positions = {
'bottom-right': 'fixed bottom-6 right-6',
'bottom-left': 'fixed bottom-6 left-6',
'top-right': 'fixed top-6 right-6',
'top-left': 'fixed top-6 left-6',
};
return positions[position];
};
const getIconSize = () => {
const iconSizes = {
sm: 'w-5 h-5',
md: 'w-6 h-6',
lg: 'w-8 h-8',
};
return iconSizes[size];
};
return (
<div className="relative">
<button
onClick={onClick}
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
className={`
${getPositionClasses()}
${getSizeClasses()}
${getVariantClasses()}
rounded-full
flex items-center justify-center
transition-all duration-300 ease-out
transform hover:scale-110 active:scale-95
focus:outline-none focus:ring-4 focus:ring-primary-500 focus:ring-opacity-50
z-50
${className}
`}
>
<span className={getIconSize()}>{icon}</span>
</button>
{/* 工具提示 */}
{tooltip && showTooltip && (
<div className={`
absolute z-50 px-3 py-2 text-sm text-white bg-gray-900 rounded-lg shadow-lg
whitespace-nowrap animate-fade-in
${position.includes('right') ? 'right-full mr-3' : 'left-full ml-3'}
${position.includes('bottom') ? 'bottom-0' : 'top-0'}
`}>
{tooltip}
<div className={`
absolute w-2 h-2 bg-gray-900 transform rotate-45
${position.includes('right') ? 'right-0 translate-x-1' : 'left-0 -translate-x-1'}
${position.includes('bottom') ? 'bottom-3' : 'top-3'}
`} />
</div>
)}
</div>
);
};