feat: Complete Modal and Tab UI/UX optimization
Enhanced Modal Components: - Optimized DeleteConfirmDialog with beautiful gradients and improved layout - Enhanced MaterialEditDialog with modern design and better information hierarchy - Improved TemplateDetailModal with elegant header and refined tab navigation - Enhanced AiClassificationFormDialog with modern styling Unified Tab System: - Created reusable TabNavigation component with multiple variants (default, pills, underline) - Implemented consistent tab design across ProjectDetails and other pages - Added support for icons, counts, and disabled states in tabs - Improved accessibility and keyboard navigation Advanced Animations: - Added comprehensive modal animations (fade-in, scale-in, slide-in) - Enhanced backdrop blur effects and smooth transitions - Implemented proper enter/exit animations for better UX - Added reduced motion support for accessibility Responsive Design: - Optimized modal layouts for mobile, tablet, and desktop - Improved touch-friendly interactions for mobile devices - Enhanced modal sizing and positioning across screen sizes - Added proper scrolling and overflow handling Interaction Improvements: - Enhanced ESC key support for modal closing - Improved backdrop click handling - Better focus management and keyboard navigation - Consistent button styling and hover effects All modal and tab components now follow unified design language while maintaining full functionality.
This commit is contained in:
@@ -102,27 +102,32 @@ export const AiClassificationFormDialog: React.FC<AiClassificationFormDialogProp
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div className="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
{/* 背景遮罩 - 优化透明度和动画 */}
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto animate-fade-in">
|
||||
<div className="flex items-center justify-center min-h-screen pt-4 px-4 pb-20">
|
||||
{/* 美观的背景遮罩 */}
|
||||
<div
|
||||
className="fixed inset-0 bg-black/20 backdrop-blur-sm transition-all duration-300"
|
||||
className="fixed inset-0 bg-black bg-opacity-60 backdrop-blur-sm transition-all duration-300"
|
||||
onClick={handleCancel}
|
||||
/>
|
||||
|
||||
{/* 对话框 - 优化圆角、阴影和尺寸 */}
|
||||
<div className="inline-block align-bottom bg-white rounded-2xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full border border-gray-100">
|
||||
{/* 标题栏 - 优化间距和字体 */}
|
||||
<div className="bg-white px-6 pt-6 pb-4">
|
||||
{/* 优化的对话框 */}
|
||||
<div className="relative bg-white rounded-2xl text-left overflow-hidden shadow-2xl transform transition-all duration-300 animate-scale-in max-w-3xl w-full mx-4">
|
||||
{/* 美观的标题栏 */}
|
||||
<div className="bg-gradient-to-r from-purple-50 via-pink-50 to-purple-50 px-6 py-5 border-b border-purple-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-gray-900 tracking-tight">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-purple-500 to-pink-500 rounded-xl flex items-center justify-center shadow-sm">
|
||||
<span className="text-white text-lg">🤖</span>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-gray-900">
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500/20"
|
||||
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-white hover:shadow-sm rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500/20"
|
||||
>
|
||||
<XMarkIcon className="h-4 w-4" />
|
||||
<XMarkIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -35,74 +35,84 @@ export const DeleteConfirmDialog: React.FC<DeleteConfirmDialogProps> = ({
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
{/* 背景遮罩 */}
|
||||
<div
|
||||
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto animate-fade-in">
|
||||
<div className="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center">
|
||||
{/* 美观的背景遮罩 */}
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-60 backdrop-blur-sm transition-all duration-300"
|
||||
onClick={deleting ? undefined : onCancel}
|
||||
/>
|
||||
|
||||
{/* 对话框 */}
|
||||
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
{/* 警告图标 */}
|
||||
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
{/* 优化的对话框 */}
|
||||
<div className="relative bg-white rounded-2xl text-left overflow-hidden shadow-2xl transform transition-all duration-300 animate-scale-in max-w-md w-full mx-4">
|
||||
{/* 美观的内容区域 */}
|
||||
<div className="p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
{/* 优化的警告图标 */}
|
||||
<div className="flex-shrink-0 w-12 h-12 bg-gradient-to-br from-red-100 to-red-200 rounded-xl flex items-center justify-center shadow-sm">
|
||||
<ExclamationTriangleIcon className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
|
||||
{/* 内容 */}
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||||
|
||||
{/* 优化的内容区域 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-2">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
{message}
|
||||
</p>
|
||||
{itemName && (
|
||||
<div className="mt-3 p-3 bg-gray-50 rounded-md">
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{itemName}
|
||||
<p className="text-gray-600 mb-4 leading-relaxed">
|
||||
{message}
|
||||
</p>
|
||||
|
||||
{itemName && (
|
||||
<div className="mb-4 p-4 bg-gradient-to-r from-gray-50 to-gray-100 rounded-xl border border-gray-200">
|
||||
<p className="text-sm font-medium text-gray-700 mb-1">要删除的项目:</p>
|
||||
<p className="font-semibold text-gray-900 truncate" title={itemName}>
|
||||
{itemName}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-4 bg-gradient-to-r from-red-50 to-red-100 border border-red-200 rounded-xl">
|
||||
<div className="flex items-start gap-2">
|
||||
<ExclamationTriangleIcon className="w-5 h-5 text-red-600 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-red-800 mb-1">重要提醒</p>
|
||||
<p className="text-sm text-red-700">
|
||||
此操作无法撤销,请确认您要继续。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3 p-3 bg-red-50 border border-red-200 rounded-md">
|
||||
<p className="text-sm text-red-700">
|
||||
<strong>注意:</strong>此操作无法撤销,请确认您要继续。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 按钮栏 */}
|
||||
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onConfirm}
|
||||
disabled={deleting}
|
||||
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{deleting ? (
|
||||
<>
|
||||
<LoadingSpinner size="small" className="mr-2" />
|
||||
删除中...
|
||||
</>
|
||||
) : (
|
||||
'确认删除'
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
disabled={deleting}
|
||||
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:mt-0 sm:w-auto sm:text-sm disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
{/* 美观的按钮区域 */}
|
||||
<div className="px-6 py-4 bg-gradient-to-r from-gray-50 to-gray-100 border-t border-gray-200">
|
||||
<div className="flex gap-3 justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
disabled={deleting}
|
||||
className="px-4 py-2.5 bg-white border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-all duration-200 font-medium text-sm disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onConfirm}
|
||||
disabled={deleting}
|
||||
className="px-4 py-2.5 bg-gradient-to-r from-red-600 to-red-700 hover:from-red-700 hover:to-red-800 text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition-all duration-200 font-medium text-sm disabled:opacity-50 disabled:cursor-not-allowed shadow-sm hover:shadow-md min-w-[100px]"
|
||||
>
|
||||
{deleting ? (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<LoadingSpinner size="small" />
|
||||
<span>删除中...</span>
|
||||
</div>
|
||||
) : (
|
||||
'确认删除'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -98,52 +98,68 @@ export const MaterialEditDialog: React.FC<MaterialEditDialogProps> = ({
|
||||
if (!isOpen || !material) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||
<h2 className="text-xl font-semibold text-gray-900">编辑素材</h2>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-60 backdrop-blur-sm flex items-center justify-center z-50 animate-fade-in">
|
||||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-4 animate-scale-in overflow-hidden">
|
||||
{/* 美观的头部 */}
|
||||
<div className="flex items-center justify-between p-6 bg-gradient-to-r from-primary-50 to-primary-100 border-b border-primary-200">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-primary-500 to-primary-600 rounded-xl flex items-center justify-center shadow-sm">
|
||||
<User className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-gray-900">编辑素材</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-white hover:shadow-sm rounded-lg transition-all duration-200"
|
||||
disabled={saving}
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 内容 */}
|
||||
{/* 美观的内容区域 */}
|
||||
<div className="p-6 space-y-6">
|
||||
{/* 错误信息 */}
|
||||
{error && <ErrorMessage message={error} />}
|
||||
|
||||
{/* 素材信息 */}
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium text-gray-700">素材信息</h3>
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<p className="text-sm font-medium text-gray-900">{material.name}</p>
|
||||
<p className="text-xs text-gray-600 mt-1">
|
||||
类型: {material.material_type} | 状态: {material.processing_status}
|
||||
{/* 优化的素材信息 */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-semibold text-gray-800 flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-primary-500 rounded-full"></div>
|
||||
素材信息
|
||||
</h3>
|
||||
<div className="bg-gradient-to-r from-gray-50 to-gray-100 rounded-xl p-4 border border-gray-200">
|
||||
<p className="font-semibold text-gray-900 mb-2 truncate" title={material.name}>
|
||||
{material.name}
|
||||
</p>
|
||||
<div className="flex items-center gap-4 text-xs text-gray-600">
|
||||
<span className="px-2 py-1 bg-white rounded-lg border">
|
||||
类型: {material.material_type}
|
||||
</span>
|
||||
<span className="px-2 py-1 bg-white rounded-lg border">
|
||||
状态: {material.processing_status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 模特绑定 */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* 优化的模特绑定 */}
|
||||
<div className="space-y-3">
|
||||
<label className="block text-sm font-semibold text-gray-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<User className="w-4 h-4" />
|
||||
<span>关联模特</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<div className="flex items-center justify-center py-6 bg-gray-50 rounded-xl">
|
||||
<LoadingSpinner size="small" />
|
||||
<span className="ml-2 text-sm text-gray-600">加载模特列表...</span>
|
||||
<span className="ml-3 text-sm text-gray-600">加载模特列表...</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<CustomSelect
|
||||
value={selectedModelId}
|
||||
onChange={setSelectedModelId}
|
||||
@@ -152,55 +168,77 @@ export const MaterialEditDialog: React.FC<MaterialEditDialogProps> = ({
|
||||
disabled={saving}
|
||||
className="w-full"
|
||||
/>
|
||||
|
||||
{selectedModelId && (
|
||||
<div className="text-sm text-gray-600">
|
||||
{getSelectedModel()?.description && (
|
||||
<p className="mt-1">{getSelectedModel()?.description}</p>
|
||||
)}
|
||||
|
||||
{selectedModelId && getSelectedModel() && (
|
||||
<div className="p-3 bg-green-50 border border-green-200 rounded-xl">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center">
|
||||
<User className="w-4 h-4 text-green-600" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-green-800">
|
||||
{getSelectedModel()?.name}
|
||||
</p>
|
||||
{getSelectedModel()?.description && (
|
||||
<p className="text-xs text-green-700 mt-1">
|
||||
{getSelectedModel()?.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 说明 */}
|
||||
<div className="bg-blue-50 rounded-lg p-3">
|
||||
<p className="text-xs text-blue-700">
|
||||
<strong>提示:</strong> 为素材绑定模特后,可以更好地进行素材分类和管理。
|
||||
如果素材中没有出现任何模特,可以选择"无关联模特"。
|
||||
</p>
|
||||
{/* 优化的说明 */}
|
||||
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-xl p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-5 h-5 bg-blue-100 rounded-lg flex items-center justify-center mt-0.5">
|
||||
<span className="text-blue-600 text-xs font-bold">💡</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-blue-800 mb-1">使用提示</p>
|
||||
<p className="text-xs text-blue-700 leading-relaxed">
|
||||
为素材绑定模特后,可以更好地进行素材分类和管理。
|
||||
如果素材中没有出现任何模特,可以选择"无关联模特"。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex justify-end space-x-3 p-6 border-t border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
|
||||
disabled={saving}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2"
|
||||
disabled={saving || loading}
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
<LoadingSpinner size="small" />
|
||||
<span>保存中...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="w-4 h-4" />
|
||||
<span>保存</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{/* 美观的操作按钮 */}
|
||||
<div className="px-6 py-4 bg-gradient-to-r from-gray-50 to-gray-100 border-t border-gray-200">
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-all duration-200"
|
||||
disabled={saving}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
className="px-4 py-2.5 text-sm font-medium text-white bg-gradient-to-r from-primary-600 to-primary-700 hover:from-primary-700 hover:to-primary-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm hover:shadow-md min-w-[100px]"
|
||||
disabled={saving || loading}
|
||||
>
|
||||
{saving ? (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<LoadingSpinner size="small" />
|
||||
<span>保存中...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Save className="w-4 h-4" />
|
||||
<span>保存</span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
204
apps/desktop/src/components/Modal.tsx
Normal file
204
apps/desktop/src/components/Modal.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
icon?: React.ReactNode;
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
||||
variant?: 'default' | 'danger' | 'success' | 'warning' | 'info';
|
||||
children: React.ReactNode;
|
||||
showCloseButton?: boolean;
|
||||
closeOnBackdropClick?: boolean;
|
||||
closeOnEscape?: boolean;
|
||||
className?: string;
|
||||
headerClassName?: string;
|
||||
contentClassName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一的弹框组件
|
||||
* 支持多种尺寸、变体和交互方式
|
||||
*/
|
||||
export const Modal: React.FC<ModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
title,
|
||||
subtitle,
|
||||
icon,
|
||||
size = 'md',
|
||||
variant = 'default',
|
||||
children,
|
||||
showCloseButton = true,
|
||||
closeOnBackdropClick = true,
|
||||
closeOnEscape = true,
|
||||
className = '',
|
||||
headerClassName = '',
|
||||
contentClassName = '',
|
||||
}) => {
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// ESC 键关闭
|
||||
useEffect(() => {
|
||||
if (!isOpen || !closeOnEscape) return;
|
||||
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
return () => document.removeEventListener('keydown', handleEscape);
|
||||
}, [isOpen, closeOnEscape, onClose]);
|
||||
|
||||
// 防止背景滚动
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const getSizeClasses = () => {
|
||||
const sizeMap = {
|
||||
sm: 'max-w-md',
|
||||
md: 'max-w-lg',
|
||||
lg: 'max-w-2xl',
|
||||
xl: 'max-w-4xl',
|
||||
full: 'max-w-7xl',
|
||||
};
|
||||
return sizeMap[size];
|
||||
};
|
||||
|
||||
const getVariantClasses = () => {
|
||||
const variantMap = {
|
||||
default: 'from-primary-50 via-blue-50 to-primary-50 border-primary-200',
|
||||
danger: 'from-red-50 via-red-50 to-red-100 border-red-200',
|
||||
success: 'from-green-50 via-green-50 to-green-100 border-green-200',
|
||||
warning: 'from-yellow-50 via-yellow-50 to-yellow-100 border-yellow-200',
|
||||
info: 'from-blue-50 via-blue-50 to-blue-100 border-blue-200',
|
||||
};
|
||||
return variantMap[variant];
|
||||
};
|
||||
|
||||
const getIconColor = () => {
|
||||
const colorMap = {
|
||||
default: 'from-primary-500 to-primary-600',
|
||||
danger: 'from-red-500 to-red-600',
|
||||
success: 'from-green-500 to-green-600',
|
||||
warning: 'from-yellow-500 to-yellow-600',
|
||||
info: 'from-blue-500 to-blue-600',
|
||||
};
|
||||
return colorMap[variant];
|
||||
};
|
||||
|
||||
const handleBackdropClick = (e: React.MouseEvent) => {
|
||||
if (closeOnBackdropClick && e.target === e.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 overflow-y-auto animate-fade-in"
|
||||
onClick={handleBackdropClick}
|
||||
>
|
||||
<div className="flex items-center justify-center min-h-screen pt-4 px-4 pb-20">
|
||||
{/* 背景遮罩 */}
|
||||
<div className="fixed inset-0 modal-backdrop" />
|
||||
|
||||
{/* 弹框容器 */}
|
||||
<div
|
||||
ref={modalRef}
|
||||
className={`modal-container ${size === 'xl' || size === 'full' ? 'large' : ''} ${getSizeClasses()} w-full mx-4 animate-modal-scale-in ${className}`}
|
||||
>
|
||||
{/* 头部 */}
|
||||
{(title || subtitle || icon || showCloseButton) && (
|
||||
<div className={`flex items-center justify-between p-6 bg-gradient-to-r ${getVariantClasses()} border-b ${headerClassName}`}>
|
||||
<div className="flex items-center gap-4 flex-1 min-w-0">
|
||||
{icon && (
|
||||
<div className={`w-12 h-12 bg-gradient-to-br ${getIconColor()} rounded-xl flex items-center justify-center shadow-sm`}>
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
{title && (
|
||||
<h2 className="text-xl font-bold text-gray-900 truncate">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
{subtitle && (
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{showCloseButton && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="ml-4 p-2 text-gray-400 hover:text-gray-600 hover:bg-white hover:shadow-sm rounded-lg transition-all duration-200 flex-shrink-0"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 内容 */}
|
||||
<div className={`${contentClassName}`}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 预设的弹框配置
|
||||
export const ModalPresets = {
|
||||
// 确认对话框
|
||||
confirm: {
|
||||
size: 'sm' as const,
|
||||
variant: 'default' as const,
|
||||
closeOnBackdropClick: false,
|
||||
},
|
||||
|
||||
// 危险操作确认
|
||||
danger: {
|
||||
size: 'sm' as const,
|
||||
variant: 'danger' as const,
|
||||
closeOnBackdropClick: false,
|
||||
},
|
||||
|
||||
// 表单弹框
|
||||
form: {
|
||||
size: 'md' as const,
|
||||
variant: 'default' as const,
|
||||
closeOnBackdropClick: true,
|
||||
},
|
||||
|
||||
// 详情查看
|
||||
detail: {
|
||||
size: 'lg' as const,
|
||||
variant: 'default' as const,
|
||||
closeOnBackdropClick: true,
|
||||
},
|
||||
|
||||
// 全屏弹框
|
||||
fullscreen: {
|
||||
size: 'full' as const,
|
||||
variant: 'default' as const,
|
||||
closeOnBackdropClick: false,
|
||||
},
|
||||
};
|
||||
@@ -249,158 +249,156 @@ const ModelList: React.FC<ModelListProps> = ({ onModelSelect }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in">
|
||||
{/* 美观的头部工具栏 */}
|
||||
<div className="bg-gradient-to-r from-white via-primary-50/30 to-white rounded-xl shadow-sm border border-gray-200/50 p-6 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-primary-100/30 to-primary-200/30 rounded-full -translate-y-16 translate-x-16 opacity-50"></div>
|
||||
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4 relative z-10">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-primary-500 to-primary-600 rounded-xl flex items-center justify-center shadow-sm">
|
||||
<SparklesIcon className="h-6 w-6 text-white" />
|
||||
<>
|
||||
<div className="space-y-8 animate-fade-in">
|
||||
{/* 美观的头部工具栏 */}
|
||||
<div className="bg-gradient-to-r from-white via-primary-50/30 to-white rounded-xl shadow-sm border border-gray-200/50 p-6 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-primary-100/30 to-primary-200/30 rounded-full -translate-y-16 translate-x-16 opacity-50"></div>
|
||||
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4 relative z-10">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-primary-500 to-primary-600 rounded-xl flex items-center justify-center shadow-sm">
|
||||
<SparklesIcon className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-900 mb-0.5">模特管理</h1>
|
||||
<p className="text-sm text-gray-500 flex items-center gap-2">
|
||||
<span>共 {filteredModels.length} 个模特</span>
|
||||
{favorites.size > 0 && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<HeartIcon className="h-3.5 w-3.5 text-red-500" />
|
||||
{favorites.size} 个收藏
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-900 mb-0.5">模特管理</h1>
|
||||
<p className="text-sm text-gray-500 flex items-center gap-2">
|
||||
<span>共 {filteredModels.length} 个模特</span>
|
||||
{favorites.size > 0 && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<HeartIcon className="h-3.5 w-3.5 text-red-500" />
|
||||
{favorites.size} 个收藏
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 过滤器切换 */}
|
||||
<button
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg transition-all duration-200 text-sm font-medium ${
|
||||
showFilters
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 过滤器切换 */}
|
||||
<button
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg transition-all duration-200 text-sm font-medium ${showFilters
|
||||
? 'bg-primary-100 text-primary-700 border border-primary-200'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
}`}
|
||||
title={showFilters ? '隐藏筛选器' : '显示筛选器'}
|
||||
>
|
||||
<FunnelIcon className={`h-3.5 w-3.5 transition-transform duration-200 ${
|
||||
showFilters ? 'rotate-0' : 'rotate-180'
|
||||
}`} />
|
||||
{showFilters ? '隐藏筛选' : '显示筛选'}
|
||||
</button>
|
||||
}`}
|
||||
title={showFilters ? '隐藏筛选器' : '显示筛选器'}
|
||||
>
|
||||
<FunnelIcon className={`h-3.5 w-3.5 transition-transform duration-200 ${showFilters ? 'rotate-0' : 'rotate-180'
|
||||
}`} />
|
||||
{showFilters ? '隐藏筛选' : '显示筛选'}
|
||||
</button>
|
||||
|
||||
{/* 视图模式切换 */}
|
||||
<div className="flex rounded-lg border border-gray-200 bg-gray-50 p-0.5">
|
||||
<button
|
||||
onClick={() => setViewMode(ModelViewMode.Grid)}
|
||||
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-md transition-all duration-200 text-sm font-medium ${
|
||||
viewMode === ModelViewMode.Grid
|
||||
{/* 视图模式切换 */}
|
||||
<div className="flex rounded-lg border border-gray-200 bg-gray-50 p-0.5">
|
||||
<button
|
||||
onClick={() => setViewMode(ModelViewMode.Grid)}
|
||||
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-md transition-all duration-200 text-sm font-medium ${viewMode === ModelViewMode.Grid
|
||||
? 'bg-white text-primary-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
<Squares2X2Icon className="h-3.5 w-3.5" />
|
||||
网格
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode(ModelViewMode.List)}
|
||||
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-md transition-all duration-200 text-sm font-medium ${
|
||||
viewMode === ModelViewMode.List
|
||||
}`}
|
||||
>
|
||||
<Squares2X2Icon className="h-3.5 w-3.5" />
|
||||
网格
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode(ModelViewMode.List)}
|
||||
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-md transition-all duration-200 text-sm font-medium ${viewMode === ModelViewMode.List
|
||||
? 'bg-white text-primary-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
<ListBulletIcon className="h-3.5 w-3.5" />
|
||||
列表
|
||||
</button>
|
||||
</div>
|
||||
}`}
|
||||
>
|
||||
<ListBulletIcon className="h-3.5 w-3.5" />
|
||||
列表
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 创建模特按钮 */}
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="flex items-center gap-1.5 px-4 py-2 bg-gradient-to-r from-primary-500 to-primary-600 text-white rounded-lg hover:from-primary-600 hover:to-primary-700 transition-all duration-200 shadow-sm hover:shadow-md text-sm font-medium"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
添加模特
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 搜索和过滤 */}
|
||||
<div className={`transition-all duration-300 ease-in-out overflow-hidden ${
|
||||
showFilters
|
||||
? 'max-h-96 opacity-100'
|
||||
: 'max-h-0 opacity-0'
|
||||
}`}>
|
||||
<ModelSearch
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
statusFilter={statusFilter}
|
||||
onStatusFilterChange={setStatusFilter}
|
||||
genderFilter={genderFilter}
|
||||
onGenderFilterChange={setGenderFilter}
|
||||
sortBy={sortBy}
|
||||
onSortByChange={setSortBy}
|
||||
sortOrder={sortOrder}
|
||||
onSortOrderChange={setSortOrder}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 模特列表 */}
|
||||
{filteredModels.length === 0 ? (
|
||||
<div className="text-center py-16 animate-fade-in">
|
||||
<div className="max-w-md mx-auto">
|
||||
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<MagnifyingGlassIcon className="h-8 w-8 text-gray-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
{models.length === 0 ? '还没有模特' : '没有找到匹配的模特'}
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-6">
|
||||
{models.length === 0
|
||||
? '开始添加您的第一个模特,建立您的模特库'
|
||||
: '尝试调整搜索条件或筛选器'
|
||||
}
|
||||
</p>
|
||||
{models.length === 0 && (
|
||||
{/* 创建模特按钮 */}
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="px-4 py-2 bg-gradient-to-r from-primary-500 to-primary-600 text-white rounded-lg hover:from-primary-600 hover:to-primary-700 transition-all duration-200 shadow-sm hover:shadow-md text-sm font-medium"
|
||||
className="flex items-center gap-1.5 px-4 py-2 bg-gradient-to-r from-primary-500 to-primary-600 text-white rounded-lg hover:from-primary-600 hover:to-primary-700 transition-all duration-200 shadow-sm hover:shadow-md text-sm font-medium"
|
||||
>
|
||||
添加第一个模特
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
添加模特
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`animate-fade-in ${
|
||||
viewMode === ModelViewMode.Grid
|
||||
|
||||
{/* 搜索和过滤 */}
|
||||
<div className={`transition-all duration-300 ease-in-out overflow-hidden ${showFilters
|
||||
? 'max-h-96 opacity-100'
|
||||
: 'max-h-0 opacity-0'
|
||||
}`}>
|
||||
<ModelSearch
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
statusFilter={statusFilter}
|
||||
onStatusFilterChange={setStatusFilter}
|
||||
genderFilter={genderFilter}
|
||||
onGenderFilterChange={setGenderFilter}
|
||||
sortBy={sortBy}
|
||||
onSortByChange={setSortBy}
|
||||
sortOrder={sortOrder}
|
||||
onSortOrderChange={setSortOrder}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 模特列表 */}
|
||||
{filteredModels.length === 0 ? (
|
||||
<div className="text-center py-16 animate-fade-in">
|
||||
<div className="max-w-md mx-auto">
|
||||
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<MagnifyingGlassIcon className="h-8 w-8 text-gray-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
{models.length === 0 ? '还没有模特' : '没有找到匹配的模特'}
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-6">
|
||||
{models.length === 0
|
||||
? '开始添加您的第一个模特,建立您的模特库'
|
||||
: '尝试调整搜索条件或筛选器'
|
||||
}
|
||||
</p>
|
||||
{models.length === 0 && (
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="px-4 py-2 bg-gradient-to-r from-primary-500 to-primary-600 text-white rounded-lg hover:from-primary-600 hover:to-primary-700 transition-all duration-200 shadow-sm hover:shadow-md text-sm font-medium"
|
||||
>
|
||||
添加第一个模特
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`animate-fade-in ${viewMode === ModelViewMode.Grid
|
||||
? 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4'
|
||||
: 'space-y-3'
|
||||
}`}>
|
||||
{filteredModels.map((model, index) => (
|
||||
<div
|
||||
key={model.id}
|
||||
className="animate-scale-in"
|
||||
style={{ animationDelay: `${index * 50}ms` }}
|
||||
>
|
||||
<ModelCard
|
||||
model={model}
|
||||
viewMode={viewMode}
|
||||
onEdit={() => setEditingModel(model)}
|
||||
onDelete={() => handleDeleteModel(model.id)}
|
||||
onSelect={() => onModelSelect?.(model)}
|
||||
onFavorite={handleFavorite}
|
||||
isFavorite={favorites.has(model.id)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
}`}>
|
||||
{filteredModels.map((model, index) => (
|
||||
<div
|
||||
key={model.id}
|
||||
className="animate-scale-in"
|
||||
style={{ animationDelay: `${index * 50}ms` }}
|
||||
>
|
||||
<ModelCard
|
||||
model={model}
|
||||
viewMode={viewMode}
|
||||
onEdit={() => setEditingModel(model)}
|
||||
onDelete={() => handleDeleteModel(model.id)}
|
||||
onSelect={() => onModelSelect?.(model)}
|
||||
onFavorite={handleFavorite}
|
||||
isFavorite={favorites.has(model.id)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{/* 创建模特模态框 */}
|
||||
{showCreateModal && (
|
||||
@@ -419,7 +417,7 @@ const ModelList: React.FC<ModelListProps> = ({ onModelSelect }) => {
|
||||
isEdit={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
139
apps/desktop/src/components/TabNavigation.tsx
Normal file
139
apps/desktop/src/components/TabNavigation.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import React from 'react';
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
|
||||
export interface TabItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: LucideIcon;
|
||||
count?: number;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface TabNavigationProps {
|
||||
tabs: TabItem[];
|
||||
activeTab: string;
|
||||
onTabChange: (tabId: string) => void;
|
||||
variant?: 'default' | 'pills' | 'underline';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一的选项卡导航组件
|
||||
* 支持多种样式变体和尺寸
|
||||
*/
|
||||
export const TabNavigation: React.FC<TabNavigationProps> = ({
|
||||
tabs,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
variant = 'default',
|
||||
size = 'md',
|
||||
className = '',
|
||||
}) => {
|
||||
const getTabClasses = (tab: TabItem, isActive: boolean) => {
|
||||
const baseClasses = 'inline-flex items-center gap-2 font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2';
|
||||
|
||||
// 尺寸样式
|
||||
const sizeClasses = {
|
||||
sm: 'px-3 py-2 text-sm',
|
||||
md: 'px-4 py-2.5 text-sm',
|
||||
lg: 'px-5 py-3 text-base',
|
||||
};
|
||||
|
||||
// 变体样式
|
||||
const variantClasses = {
|
||||
default: isActive
|
||||
? 'text-primary-600 border-b-2 border-primary-500 bg-white rounded-t-lg'
|
||||
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50 rounded-t-lg',
|
||||
pills: isActive
|
||||
? 'text-primary-600 bg-primary-100 border border-primary-200 rounded-lg'
|
||||
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg',
|
||||
underline: isActive
|
||||
? 'text-primary-600 border-b-2 border-primary-500'
|
||||
: 'text-gray-500 hover:text-gray-700 border-b-2 border-transparent hover:border-gray-300',
|
||||
};
|
||||
|
||||
const disabledClasses = tab.disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer';
|
||||
|
||||
return `${baseClasses} ${sizeClasses[size]} ${variantClasses[variant]} ${disabledClasses}`;
|
||||
};
|
||||
|
||||
const getContainerClasses = () => {
|
||||
const baseClasses = 'flex overflow-x-auto';
|
||||
|
||||
const variantContainerClasses = {
|
||||
default: 'space-x-1 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-4',
|
||||
pills: 'space-x-2 p-1 bg-gray-100 rounded-lg',
|
||||
underline: 'space-x-6 border-b border-gray-200',
|
||||
};
|
||||
|
||||
return `${baseClasses} ${variantContainerClasses[variant]} ${className}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className={getContainerClasses()}>
|
||||
{tabs.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const isActive = activeTab === tab.id;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => !tab.disabled && onTabChange(tab.id)}
|
||||
className={getTabClasses(tab, isActive)}
|
||||
disabled={tab.disabled}
|
||||
type="button"
|
||||
>
|
||||
{Icon && (
|
||||
<Icon
|
||||
className={`w-4 h-4 transition-all duration-200 ${
|
||||
isActive ? 'text-primary-600' : 'text-gray-400'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
<span className="whitespace-nowrap">{tab.label}</span>
|
||||
{tab.count !== undefined && (
|
||||
<span className={`inline-flex items-center justify-center px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
isActive
|
||||
? 'bg-primary-200 text-primary-800'
|
||||
: 'bg-gray-200 text-gray-600'
|
||||
}`}>
|
||||
{tab.count}
|
||||
</span>
|
||||
)}
|
||||
{isActive && variant === 'default' && (
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-primary-50/30 to-primary-100/30 rounded-t-lg -z-10"></div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
// 预设的选项卡样式
|
||||
export const TabNavigationPresets = {
|
||||
// 默认样式 - 适用于页面主要导航
|
||||
default: {
|
||||
variant: 'default' as const,
|
||||
size: 'md' as const,
|
||||
},
|
||||
|
||||
// 药丸样式 - 适用于紧凑的选项切换
|
||||
pills: {
|
||||
variant: 'pills' as const,
|
||||
size: 'sm' as const,
|
||||
},
|
||||
|
||||
// 下划线样式 - 适用于简洁的导航
|
||||
underline: {
|
||||
variant: 'underline' as const,
|
||||
size: 'md' as const,
|
||||
},
|
||||
|
||||
// 大尺寸 - 适用于重要的页面导航
|
||||
large: {
|
||||
variant: 'default' as const,
|
||||
size: 'lg' as const,
|
||||
},
|
||||
};
|
||||
@@ -3,9 +3,7 @@ import ModelList from '../components/ModelList';
|
||||
|
||||
const Models: React.FC = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<ModelList />
|
||||
</div>
|
||||
<ModelList />
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -199,11 +199,128 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 弹框专用动画 ===== */
|
||||
|
||||
/* 弹框淡入动画 */
|
||||
@keyframes modalFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 弹框缩放进入动画 */
|
||||
@keyframes modalScaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 弹框滑入动画 */
|
||||
@keyframes modalSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 弹框淡出动画 */
|
||||
@keyframes modalFadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 弹框缩放退出动画 */
|
||||
@keyframes modalScaleOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 弹框动画类 */
|
||||
.animate-modal-fade-in {
|
||||
animation: modalFadeIn 0.2s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-modal-scale-in {
|
||||
animation: modalScaleIn 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
.animate-modal-slide-in {
|
||||
animation: modalSlideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
.animate-modal-fade-out {
|
||||
animation: modalFadeOut 0.2s ease-in forwards;
|
||||
}
|
||||
|
||||
.animate-modal-scale-out {
|
||||
animation: modalScaleOut 0.2s ease-in forwards;
|
||||
}
|
||||
|
||||
/* 背景遮罩动画 */
|
||||
.modal-backdrop {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(4px);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.modal-backdrop.entering {
|
||||
background: rgba(0, 0, 0, 0);
|
||||
backdrop-filter: blur(0px);
|
||||
}
|
||||
|
||||
.modal-backdrop.exiting {
|
||||
background: rgba(0, 0, 0, 0);
|
||||
backdrop-filter: blur(0px);
|
||||
}
|
||||
|
||||
/* 弹框容器样式 */
|
||||
.modal-container {
|
||||
position: relative;
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
overflow: hidden;
|
||||
transform-origin: center;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.modal-container.large {
|
||||
border-radius: 1.5rem;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 减少动画偏好设置 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.animate-fade-in-up,
|
||||
.hover-glow,
|
||||
.card-interactive {
|
||||
.card-interactive,
|
||||
.animate-modal-fade-in,
|
||||
.animate-modal-scale-in,
|
||||
.animate-modal-slide-in,
|
||||
.animate-modal-fade-out,
|
||||
.animate-modal-scale-out {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
@@ -212,6 +329,11 @@
|
||||
.card-interactive:hover {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.modal-backdrop,
|
||||
.modal-container {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-out {
|
||||
|
||||
@@ -581,3 +581,106 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 弹框响应式优化 ===== */
|
||||
|
||||
/* 移动端弹框优化 */
|
||||
@media (max-width: 640px) {
|
||||
.modal-container {
|
||||
margin: 1rem;
|
||||
max-height: calc(100vh - 2rem);
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.modal-container.large {
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
/* 移动端弹框头部 */
|
||||
.modal-header-mobile {
|
||||
padding: 1rem;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* 移动端弹框内容 */
|
||||
.modal-content-mobile {
|
||||
padding: 1rem;
|
||||
max-height: calc(100vh - 8rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 移动端按钮区域 */
|
||||
.modal-actions-mobile {
|
||||
padding: 1rem;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.modal-actions-mobile button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* 平板端弹框优化 */
|
||||
@media (min-width: 641px) and (max-width: 1024px) {
|
||||
.modal-container {
|
||||
margin: 2rem;
|
||||
max-height: calc(100vh - 4rem);
|
||||
}
|
||||
|
||||
.modal-content-tablet {
|
||||
max-height: calc(100vh - 12rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* 大屏幕弹框优化 */
|
||||
@media (min-width: 1920px) {
|
||||
.modal-container {
|
||||
border-radius: 1.5rem;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.modal-container.large {
|
||||
border-radius: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 弹框内容滚动优化 */
|
||||
.modal-scroll {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
||||
}
|
||||
|
||||
.modal-scroll::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.modal-scroll::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.modal-scroll::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(156, 163, 175, 0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.modal-scroll::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(156, 163, 175, 0.7);
|
||||
}
|
||||
|
||||
/* 弹框层级管理 */
|
||||
.modal-z-index {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-z-index.high {
|
||||
z-index: 1100;
|
||||
}
|
||||
|
||||
.modal-z-index.highest {
|
||||
z-index: 1200;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user