196 lines
5.2 KiB
TypeScript
196 lines
5.2 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { LucideIcon } from 'lucide-react';
|
|
|
|
interface AnimatedButtonProps {
|
|
children: React.ReactNode;
|
|
onClick?: () => void;
|
|
variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'success';
|
|
size?: 'sm' | 'md' | 'lg';
|
|
icon?: LucideIcon;
|
|
iconPosition?: 'left' | 'right';
|
|
disabled?: boolean;
|
|
loading?: boolean;
|
|
className?: string;
|
|
ripple?: boolean;
|
|
glow?: boolean;
|
|
type?: 'button' | 'submit' | 'reset';
|
|
}
|
|
|
|
/**
|
|
* 增强的动画按钮组件
|
|
* 支持涟漪效果、发光效果和各种微交互
|
|
*/
|
|
export const AnimatedButton: React.FC<AnimatedButtonProps> = ({
|
|
children,
|
|
onClick,
|
|
variant = 'primary',
|
|
size = 'md',
|
|
icon: Icon,
|
|
iconPosition = 'left',
|
|
disabled = false,
|
|
loading = false,
|
|
className = '',
|
|
ripple = true,
|
|
glow = false,
|
|
type = 'button'
|
|
}) => {
|
|
const [ripples, setRipples] = useState<Array<{ id: number; x: number; y: number }>>([]);
|
|
|
|
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
if (disabled || loading) return;
|
|
|
|
// 创建涟漪效果
|
|
if (ripple) {
|
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const y = e.clientY - rect.top;
|
|
const newRipple = { id: Date.now(), x, y };
|
|
|
|
setRipples(prev => [...prev, newRipple]);
|
|
|
|
// 移除涟漪效果
|
|
setTimeout(() => {
|
|
setRipples(prev => prev.filter(r => r.id !== newRipple.id));
|
|
}, 600);
|
|
}
|
|
|
|
onClick?.();
|
|
};
|
|
|
|
const getVariantClasses = () => {
|
|
const base = 'btn relative overflow-hidden';
|
|
const variants = {
|
|
primary: 'btn-primary',
|
|
secondary: 'btn-secondary',
|
|
ghost: 'btn-ghost',
|
|
danger: 'btn-danger',
|
|
success: 'btn-success'
|
|
};
|
|
return `${base} ${variants[variant]}`;
|
|
};
|
|
|
|
const getSizeClasses = () => {
|
|
const sizes = {
|
|
sm: 'btn-sm',
|
|
md: '',
|
|
lg: 'btn-lg'
|
|
};
|
|
return sizes[size];
|
|
};
|
|
|
|
const glowClasses = glow ? 'shadow-glow hover:shadow-glow-lg' : '';
|
|
const disabledClasses = disabled ? 'opacity-50 cursor-not-allowed' : '';
|
|
const loadingClasses = loading ? 'cursor-wait' : '';
|
|
|
|
return (
|
|
<button
|
|
type={type}
|
|
className={`
|
|
${getVariantClasses()}
|
|
${getSizeClasses()}
|
|
${glowClasses}
|
|
${disabledClasses}
|
|
${loadingClasses}
|
|
${className}
|
|
group
|
|
transform transition-all duration-200
|
|
hover:scale-105 active:scale-95
|
|
focus:outline-none focus:ring-2 focus:ring-offset-2
|
|
`}
|
|
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,
|
|
pointerEvents: 'none'
|
|
}}
|
|
/>
|
|
))}
|
|
|
|
{/* 按钮内容 */}
|
|
<span className="relative flex items-center justify-center gap-2">
|
|
{/* 左侧图标 */}
|
|
{Icon && iconPosition === 'left' && (
|
|
<Icon
|
|
size={size === 'sm' ? 14 : size === 'lg' ? 20 : 16}
|
|
className={`transition-transform duration-200 ${loading ? 'animate-spin' : 'group-hover:scale-110'}`}
|
|
/>
|
|
)}
|
|
|
|
{/* 加载状态 */}
|
|
{loading && (
|
|
<div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
|
)}
|
|
|
|
{/* 文本内容 */}
|
|
<span className={`transition-all duration-200 ${loading ? 'opacity-70' : ''}`}>
|
|
{children}
|
|
</span>
|
|
|
|
{/* 右侧图标 */}
|
|
{Icon && iconPosition === 'right' && (
|
|
<Icon
|
|
size={size === 'sm' ? 14 : size === 'lg' ? 20 : 16}
|
|
className={`transition-transform duration-200 ${loading ? 'animate-spin' : 'group-hover:scale-110'}`}
|
|
/>
|
|
)}
|
|
</span>
|
|
|
|
{/* 悬停光晕效果 */}
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent opacity-0 group-hover:opacity-10 transition-opacity duration-300 -skew-x-12 transform translate-x-full group-hover:translate-x-[-100%] transition-transform duration-700" />
|
|
</button>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* 浮动操作按钮组件
|
|
*/
|
|
export const FloatingActionButton: React.FC<{
|
|
icon: LucideIcon;
|
|
onClick: () => void;
|
|
className?: string;
|
|
size?: 'sm' | 'md' | 'lg';
|
|
}> = ({ icon: Icon, onClick, className = '', size = 'md' }) => {
|
|
const sizeClasses = {
|
|
sm: 'w-12 h-12',
|
|
md: 'w-14 h-14',
|
|
lg: 'w-16 h-16'
|
|
};
|
|
|
|
const iconSizes = {
|
|
sm: 20,
|
|
md: 24,
|
|
lg: 28
|
|
};
|
|
|
|
return (
|
|
<button
|
|
onClick={onClick}
|
|
className={`
|
|
${sizeClasses[size]}
|
|
bg-primary-600 hover:bg-primary-700 text-white
|
|
rounded-full shadow-lg hover:shadow-xl
|
|
flex items-center justify-center
|
|
transition-all duration-300
|
|
hover:scale-110 active:scale-95
|
|
focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2
|
|
group
|
|
${className}
|
|
`}
|
|
>
|
|
<Icon
|
|
size={iconSizes[size]}
|
|
className="transition-transform duration-200 group-hover:rotate-12"
|
|
/>
|
|
</button>
|
|
);
|
|
};
|