Files
mixvideo-v2/apps/desktop/src/components/InteractiveInput.tsx
imeepos 1b7f7b44a8 feat: Complete comprehensive UI/UX enhancement phase
Enhanced Interactive Components:
- Created InteractiveButton with ripple effects, haptic feedback, and multiple variants
- Developed InteractiveInput and InteractiveTextarea with real-time validation and status indicators
- Added FloatingActionButton for quick actions with elegant tooltips
- Implemented comprehensive micro-interactions and animations

 Advanced Loading & Skeleton States:
- Enhanced SkeletonLoader with multiple variants (model, material, template, table-row)
- Added specialized skeleton components (ModelCardSkeleton, MaterialCardSkeleton, etc.)
- Created EnhancedLoadingState with progress indicators and operation tracking
- Implemented BatchOperationLoading for complex workflows

 Optimized Form Experience:
- Upgraded ProjectForm with new interactive components
- Added real-time validation feedback and error animations
- Implemented smart input states (success, error, loading)
- Enhanced user feedback with visual and haptic responses

 Perfected Empty States:
- Redesigned EmptyState with multiple variants and illustrations
- Created specialized empty state components (EmptyProjectList, EmptyModelList, etc.)
- Added contextual tips and guidance for better user onboarding
- Implemented error states and recovery actions

 Advanced Data Display:
- Built comprehensive DataTable with search, sort, filter, and pagination
- Created flexible CardGrid with view switching and bulk operations
- Added row selection, bulk actions, and advanced filtering
- Implemented responsive layouts and mobile optimization

 Rich Animation System:
- Added 20+ new micro-interaction animations
- Implemented button press, success pulse, error shake effects
- Created smooth slide-in animations for all directions
- Added loading dots, heartbeat, and bounce-in animations

 Key Features:
- Ripple effects on button clicks with haptic feedback
- Real-time form validation with animated error states
- Contextual empty states with actionable guidance
- Advanced data tables with full CRUD operations
- Responsive card grids with multiple view modes
- Comprehensive loading states for better perceived performance

All components now provide rich visual feedback, smooth animations, and professional user experience that matches modern design standards.
2025-07-15 20:38:34 +08:00

432 lines
12 KiB
TypeScript

import React, { useState, useRef, useEffect } from 'react';
import { Eye, EyeOff, AlertCircle, CheckCircle, Search, X } from 'lucide-react';
interface InteractiveInputProps {
type?: 'text' | 'email' | 'password' | 'search' | 'number' | 'tel' | 'url';
value?: string;
onChange?: (value: string) => void;
onFocus?: () => void;
onBlur?: () => void;
onEnter?: () => void;
placeholder?: string;
label?: string;
error?: string;
success?: string;
hint?: string;
required?: boolean;
disabled?: boolean;
loading?: boolean;
icon?: React.ReactNode;
iconPosition?: 'left' | 'right';
clearable?: boolean;
autoFocus?: boolean;
maxLength?: number;
className?: string;
inputClassName?: string;
}
/**
* 增强的交互输入组件
* 提供丰富的视觉反馈和状态指示
*/
export const InteractiveInput: React.FC<InteractiveInputProps> = ({
type = 'text',
value = '',
onChange,
onFocus,
onBlur,
onEnter,
placeholder,
label,
error,
success,
hint,
required = false,
disabled = false,
loading = false,
icon,
iconPosition = 'left',
clearable = false,
autoFocus = false,
maxLength,
className = '',
inputClassName = '',
}) => {
const [isFocused, setIsFocused] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [internalValue, setInternalValue] = useState(value);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
setInternalValue(value);
}, [value]);
useEffect(() => {
if (autoFocus && inputRef.current) {
inputRef.current.focus();
}
}, [autoFocus]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
setInternalValue(newValue);
onChange?.(newValue);
};
const handleFocus = () => {
setIsFocused(true);
onFocus?.();
};
const handleBlur = () => {
setIsFocused(false);
onBlur?.();
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
onEnter?.();
}
};
const handleClear = () => {
setInternalValue('');
onChange?.('');
inputRef.current?.focus();
};
const getInputType = () => {
if (type === 'password') {
return showPassword ? 'text' : 'password';
}
return type;
};
const getStatusColor = () => {
if (error) return 'border-red-300 focus:border-red-500 focus:ring-red-500';
if (success) return 'border-green-300 focus:border-green-500 focus:ring-green-500';
return 'border-gray-300 focus:border-primary-500 focus:ring-primary-500';
};
const getStatusIcon = () => {
if (loading) {
return <div className="w-4 h-4 border-2 border-gray-300 border-t-primary-600 rounded-full animate-spin" />;
}
if (error) {
return <AlertCircle className="w-4 h-4 text-red-500" />;
}
if (success) {
return <CheckCircle className="w-4 h-4 text-green-500" />;
}
return null;
};
const showClearButton = clearable && internalValue && !disabled && !loading;
const showPasswordToggle = type === 'password' && !disabled;
return (
<div className={`space-y-1 ${className}`}>
{/* 标签 */}
{label && (
<label className="block text-sm font-medium text-gray-700">
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
)}
{/* 输入容器 */}
<div className="relative">
{/* 左侧图标 */}
{icon && iconPosition === 'left' && (
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span className="text-gray-400 w-4 h-4">{icon}</span>
</div>
)}
{/* 搜索图标(特殊处理) */}
{type === 'search' && !icon && (
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Search className="w-4 h-4 text-gray-400" />
</div>
)}
{/* 输入框 */}
<input
ref={inputRef}
type={getInputType()}
value={internalValue}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
placeholder={placeholder}
disabled={disabled}
maxLength={maxLength}
className={`
block w-full rounded-lg border
${getStatusColor()}
${icon && iconPosition === 'left' || type === 'search' ? 'pl-10' : 'pl-3'}
${showClearButton || showPasswordToggle || getStatusIcon() || (icon && iconPosition === 'right') ? 'pr-10' : 'pr-3'}
py-2.5 text-sm
placeholder-gray-400
focus:outline-none
disabled:bg-gray-50 disabled:text-gray-500 disabled:cursor-not-allowed
${isFocused ? 'shadow-sm' : ''}
${error ? 'animate-error-shake' : ''}
${inputClassName}
`}
/>
{/* 右侧图标区域 */}
<div className="absolute inset-y-0 right-0 flex items-center pr-3 space-x-1">
{/* 清除按钮 */}
{showClearButton && (
<button
type="button"
onClick={handleClear}
className="text-gray-400 hover:text-gray-600 transition-colors p-0.5 rounded hover:bg-gray-100"
>
<X className="w-3 h-3" />
</button>
)}
{/* 密码显示切换 */}
{showPasswordToggle && (
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="text-gray-400 hover:text-gray-600 transition-colors p-0.5 rounded hover:bg-gray-100"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
)}
{/* 状态图标 */}
{getStatusIcon()}
{/* 右侧图标 */}
{icon && iconPosition === 'right' && (
<span className="text-gray-400 w-4 h-4">{icon}</span>
)}
</div>
{/* 焦点指示器 */}
{isFocused && (
<div className="absolute inset-0 rounded-lg border-2 border-primary-500 pointer-events-none animate-pulse" />
)}
</div>
{/* 底部信息 */}
<div className="flex justify-between items-start">
<div className="space-y-1">
{/* 错误信息 */}
{error && (
<p className="text-sm text-red-600 flex items-center gap-1 animate-slide-in-up">
<AlertCircle className="w-3 h-3" />
{error}
</p>
)}
{/* 成功信息 */}
{success && !error && (
<p className="text-sm text-green-600 flex items-center gap-1 animate-slide-in-up">
<CheckCircle className="w-3 h-3" />
{success}
</p>
)}
{/* 提示信息 */}
{hint && !error && !success && (
<p className="text-sm text-gray-500">{hint}</p>
)}
</div>
{/* 字符计数 */}
{maxLength && (
<p className={`text-xs ${internalValue.length > maxLength * 0.8 ? 'text-orange-500' : 'text-gray-400'}`}>
{internalValue.length}/{maxLength}
</p>
)}
</div>
</div>
);
};
/**
* 搜索输入组件
*/
interface SearchInputProps {
value?: string;
onChange?: (value: string) => void;
onSearch?: (value: string) => void;
placeholder?: string;
loading?: boolean;
className?: string;
}
export const SearchInput: React.FC<SearchInputProps> = ({
value = '',
onChange,
onSearch,
placeholder = '搜索...',
loading = false,
className = '',
}) => {
return (
<InteractiveInput
type="search"
value={value}
onChange={onChange}
onEnter={() => onSearch?.(value)}
placeholder={placeholder}
loading={loading}
clearable
className={className}
/>
);
};
/**
* 交互式文本区域组件
*/
interface InteractiveTextareaProps {
value?: string;
onChange?: (value: string) => void;
onFocus?: () => void;
onBlur?: () => void;
placeholder?: string;
label?: string;
error?: string;
success?: string;
hint?: string;
required?: boolean;
disabled?: boolean;
rows?: number;
maxLength?: number;
className?: string;
textareaClassName?: string;
}
export const InteractiveTextarea: React.FC<InteractiveTextareaProps> = ({
value = '',
onChange,
onFocus,
onBlur,
placeholder,
label,
error,
success,
hint,
required = false,
disabled = false,
rows = 4,
maxLength,
className = '',
textareaClassName = '',
}) => {
const [isFocused, setIsFocused] = useState(false);
const [internalValue, setInternalValue] = useState(value);
useEffect(() => {
setInternalValue(value);
}, [value]);
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = e.target.value;
setInternalValue(newValue);
onChange?.(newValue);
};
const handleFocus = () => {
setIsFocused(true);
onFocus?.();
};
const handleBlur = () => {
setIsFocused(false);
onBlur?.();
};
const getStatusColor = () => {
if (error) return 'border-red-300 focus:border-red-500 focus:ring-red-500';
if (success) return 'border-green-300 focus:border-green-500 focus:ring-green-500';
return 'border-gray-300 focus:border-primary-500 focus:ring-primary-500';
};
return (
<div className={`space-y-1 ${className}`}>
{/* 标签 */}
{label && (
<label className="block text-sm font-medium text-gray-700">
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
)}
{/* 文本区域容器 */}
<div className="relative">
<textarea
value={internalValue}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
placeholder={placeholder}
disabled={disabled}
rows={rows}
maxLength={maxLength}
className={`
block w-full rounded-lg border transition-all duration-200
${getStatusColor()}
px-3 py-2.5 text-sm
placeholder-gray-400
focus:outline-none focus:ring-2 focus:ring-opacity-50
disabled:bg-gray-50 disabled:text-gray-500 disabled:cursor-not-allowed
resize-none
${isFocused ? 'shadow-sm' : ''}
${error ? 'animate-error-shake' : ''}
${textareaClassName}
`}
/>
{/* 焦点指示器 */}
{isFocused && (
<div className="absolute inset-0 rounded-lg border-2 border-primary-500 pointer-events-none animate-pulse" />
)}
</div>
{/* 底部信息 */}
<div className="flex justify-between items-start">
<div className="space-y-1">
{/* 错误信息 */}
{error && (
<p className="text-sm text-red-600 flex items-center gap-1 animate-slide-in-up">
<AlertCircle className="w-3 h-3" />
{error}
</p>
)}
{/* 成功信息 */}
{success && !error && (
<p className="text-sm text-green-600 flex items-center gap-1 animate-slide-in-up">
<CheckCircle className="w-3 h-3" />
{success}
</p>
)}
{/* 提示信息 */}
{hint && !error && !success && (
<p className="text-sm text-gray-500">{hint}</p>
)}
</div>
{/* 字符计数 */}
{maxLength && (
<p className={`text-xs ${internalValue.length > maxLength * 0.8 ? 'text-orange-500' : 'text-gray-400'}`}>
{internalValue.length}/{maxLength}
</p>
)}
</div>
</div>
);
};