From c366261008590556c38361ac6d1628d3712556d5 Mon Sep 17 00:00:00 2001 From: imeepos Date: Mon, 14 Jul 2025 00:23:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20UI=E7=BE=8E=E5=8C=96=E5=92=8CUX?= =?UTF-8?q?=E6=94=B9=E8=BF=9B=20v0.1.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化设计系统和主题配置 - 增强Tailwind配置,添加更丰富的色彩系统 - 新增多种动画效果和过渡动画 - 改进字体系统和间距设置 - 添加阴影变体和渐变效果 - 改进项目列表页面UI/UX - 重新设计页面头部,添加渐变背景和统计信息 - 优化项目卡片布局和响应式设计 - 增强加载和错误状态的视觉效果 - 添加交错动画效果 - 优化项目卡片组件 - 重新设计卡片布局,添加背景装饰 - 改进统计信息展示,使用渐变背景和徽章 - 增强悬停效果和交互反馈 - 优化菜单和按钮设计 - 改进表单和模态框体验 - 重新设计模态框头部和布局 - 增强表单字段的视觉设计和验证反馈 - 添加加载状态和成功状态指示 - 改进按钮样式和交互效果 - 增强加载状态和反馈机制 - 重新设计EmptyState组件,添加装饰效果 - 增强LoadingSpinner,支持多种动画样式 - 改进ErrorMessage组件,支持多种消息类型 - 新增SkeletonLoader组件用于骨架屏加载 - 添加微交互和动画效果 - 新增AnimatedButton组件,支持涟漪效果 - 创建PageTransition组件用于页面过渡 - 添加多种动画工具组件 - 增强按钮和卡片的微交互效果 - 优化响应式设计和移动端体验 - 改进容器布局和间距设置 - 优化移动端的触摸体验 - 确保所有组件在不同屏幕尺寸下的完美适配 遵循前端开发规范,提升界面美观性和用户体验 --- apps/desktop/src/App.css | 334 ++++++++++++++++-- apps/desktop/src/App.tsx | 5 +- .../desktop/src/components/AnimatedButton.tsx | 195 ++++++++++ apps/desktop/src/components/EmptyState.tsx | 53 ++- apps/desktop/src/components/ErrorMessage.tsx | 123 ++++++- .../desktop/src/components/LoadingSpinner.tsx | 109 +++++- .../desktop/src/components/PageTransition.tsx | 209 +++++++++++ apps/desktop/src/components/ProjectCard.tsx | 277 ++++++++------- apps/desktop/src/components/ProjectForm.tsx | 162 ++++++--- apps/desktop/src/components/ProjectList.tsx | 243 ++++++++----- .../desktop/src/components/SkeletonLoader.tsx | 136 +++++++ apps/desktop/tailwind.config.js | 251 ++++++++++++- 12 files changed, 1751 insertions(+), 346 deletions(-) create mode 100644 apps/desktop/src/components/AnimatedButton.tsx create mode 100644 apps/desktop/src/components/PageTransition.tsx create mode 100644 apps/desktop/src/components/SkeletonLoader.tsx diff --git a/apps/desktop/src/App.css b/apps/desktop/src/App.css index a3a906b..52414b8 100644 --- a/apps/desktop/src/App.css +++ b/apps/desktop/src/App.css @@ -2,60 +2,135 @@ @tailwind components; @tailwind utilities; +/* 自定义基础样式 */ +@layer base { + /* 改进字体渲染 */ + html { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + } + + /* 改进滚动行为 */ + * { + scroll-behavior: smooth; + } + + /* 改进焦点样式 */ + *:focus-visible { + @apply outline-none ring-2 ring-primary-500 ring-offset-2; + } +} + /* 自定义组件样式 */ @layer components { - /* 按钮样式 */ + /* 按钮样式系统 */ .btn { - @apply inline-flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed; + @apply inline-flex items-center justify-center gap-2 px-4 py-2.5 text-sm font-medium rounded-xl transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed relative overflow-hidden; + } + + /* 按钮悬停效果 */ + .btn::before { + @apply absolute inset-0 bg-white opacity-0 transition-opacity duration-200; + content: ''; + } + + .btn:hover::before { + @apply opacity-10; + } + + .btn:active { + @apply transform scale-95; } .btn-primary { - @apply bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500 active:bg-primary-800; + @apply bg-gradient-to-r from-primary-600 to-primary-700 text-white hover:from-primary-700 hover:to-primary-800 focus:ring-primary-500 active:from-primary-800 active:to-primary-900 shadow-lg hover:shadow-xl; } .btn-secondary { - @apply bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-500 border border-gray-300; + @apply bg-white text-gray-700 hover:bg-gray-50 focus:ring-gray-500 border border-gray-200 hover:border-gray-300 shadow-sm hover:shadow-md; } .btn-ghost { - @apply text-gray-600 hover:bg-gray-100 focus:ring-gray-500; + @apply text-gray-600 hover:bg-gray-100 hover:text-gray-900 focus:ring-gray-500 rounded-lg; } .btn-danger { - @apply bg-red-600 text-white hover:bg-red-700 focus:ring-red-500 active:bg-red-800; + @apply bg-gradient-to-r from-red-600 to-red-700 text-white hover:from-red-700 hover:to-red-800 focus:ring-red-500 active:from-red-800 active:to-red-900 shadow-lg hover:shadow-xl; + } + + .btn-success { + @apply bg-gradient-to-r from-green-600 to-green-700 text-white hover:from-green-700 hover:to-green-800 focus:ring-green-500 active:from-green-800 active:to-green-900 shadow-lg hover:shadow-xl; + } + + .btn-warning { + @apply bg-gradient-to-r from-yellow-500 to-yellow-600 text-white hover:from-yellow-600 hover:to-yellow-700 focus:ring-yellow-500 active:from-yellow-700 active:to-yellow-800 shadow-lg hover:shadow-xl; } .btn-sm { - @apply px-3 py-1.5 text-xs; + @apply px-3 py-2 text-xs rounded-lg; } .btn-lg { - @apply px-6 py-3 text-base; + @apply px-6 py-3 text-base rounded-2xl; + } + + .btn-xl { + @apply px-8 py-4 text-lg rounded-2xl; + } + + /* 表单样式系统 */ + .form-group { + @apply space-y-2; } - /* 表单样式 */ .form-input { - @apply w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-colors duration-200; + @apply w-full px-4 py-3 border border-gray-200 rounded-xl bg-white focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200 placeholder-gray-400 text-gray-900; + } + + .form-input:hover { + @apply border-gray-300; } .form-input.error { - @apply border-red-500 focus:ring-red-500; + @apply border-red-300 bg-red-50 focus:ring-red-500 focus:border-red-500; + } + + .form-input.success { + @apply border-green-300 bg-green-50 focus:ring-green-500 focus:border-green-500; } .form-textarea { - @apply w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-colors duration-200 resize-none; + @apply w-full px-4 py-3 border border-gray-200 rounded-xl bg-white focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200 resize-none placeholder-gray-400 text-gray-900; + } + + .form-textarea:hover { + @apply border-gray-300; } .form-textarea.error { - @apply border-red-500 focus:ring-red-500; + @apply border-red-300 bg-red-50 focus:ring-red-500 focus:border-red-500; + } + + .form-textarea.success { + @apply border-green-300 bg-green-50 focus:ring-green-500 focus:border-green-500; } .form-label { - @apply block text-sm font-medium text-gray-700 mb-1; + @apply block text-sm font-semibold text-gray-800 mb-2; + } + + .form-label.required::after { + @apply text-red-500 ml-1; + content: '*'; } .form-error { - @apply flex items-center gap-1 text-sm text-red-600 mt-1; + @apply flex items-center gap-2 text-sm text-red-600 mt-2 font-medium; + } + + .form-success { + @apply flex items-center gap-2 text-sm text-green-600 mt-2 font-medium; } .form-hint { @@ -63,50 +138,106 @@ } .form-info { - @apply text-sm text-blue-600 mt-1; + @apply text-sm text-blue-600 mt-2 font-medium; } - /* 加载动画 */ + .form-warning { + @apply text-sm text-yellow-600 mt-2 font-medium; + } + + /* 加载动画增强 */ .spinner { @apply animate-spin; } - /* 模态框样式 */ + .loading-pulse { + @apply animate-pulse; + } + + .loading-bounce { + @apply animate-bounce; + } + + /* 模态框样式增强 */ .modal-overlay { - @apply fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4; + @apply fixed inset-0 bg-black bg-opacity-60 backdrop-blur-sm flex items-center justify-center z-50 p-4 animate-fade-in; } .modal { - @apply bg-white rounded-xl shadow-xl max-w-md w-full max-h-[90vh] overflow-hidden; + @apply bg-white rounded-2xl shadow-2xl max-w-md w-full max-h-[90vh] overflow-hidden animate-scale-in; + } + + .modal-lg { + @apply max-w-2xl; + } + + .modal-xl { + @apply max-w-4xl; } .modal-header { - @apply flex items-center justify-between p-6 border-b border-gray-200; + @apply flex items-center justify-between p-6 border-b border-gray-100 bg-gray-25; } .modal-close { - @apply p-1 hover:bg-gray-100 rounded-lg transition-colors duration-200; + @apply p-2 hover:bg-gray-100 rounded-xl transition-all duration-200 text-gray-400 hover:text-gray-600; } - /* 卡片样式 */ + .modal-body { + @apply p-6; + } + + .modal-footer { + @apply flex gap-3 p-6 border-t border-gray-100 bg-gray-25; + } + + /* 卡片样式增强 */ .card { - @apply bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow duration-200; + @apply bg-white rounded-2xl border border-gray-100 shadow-sm hover:shadow-lg hover:shadow-primary-500/10 transition-all duration-300 overflow-hidden; } - /* 菜单样式 */ + .card-elevated { + @apply shadow-lg hover:shadow-xl; + } + + .card-interactive { + @apply cursor-pointer hover:-translate-y-1 hover:scale-[1.02] active:scale-[0.98]; + } + + .card-header { + @apply p-6 border-b border-gray-100; + } + + .card-body { + @apply p-6; + } + + .card-footer { + @apply p-6 border-t border-gray-100 bg-gray-25; + } + + /* 菜单样式增强 */ .dropdown-menu { - @apply absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-50; + @apply absolute right-0 mt-2 w-48 bg-white rounded-xl shadow-xl border border-gray-100 py-2 z-50 animate-fade-in-down; } .dropdown-item { - @apply flex items-center gap-2 w-full px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors duration-150; + @apply flex items-center gap-3 w-full px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 hover:text-gray-900 transition-all duration-150 mx-1 rounded-lg; } .dropdown-item.danger { - @apply text-red-600 hover:bg-red-50; + @apply text-red-600 hover:bg-red-50 hover:text-red-700; } - /* 响应式工具类 */ + .dropdown-item.success { + @apply text-green-600 hover:bg-green-50 hover:text-green-700; + } + + .dropdown-divider { + @apply my-1 border-t border-gray-100; + } + + /* 响应式工具类增强 */ .line-clamp-1 { display: -webkit-box; -webkit-line-clamp: 1; @@ -121,30 +252,157 @@ overflow: hidden; } - /* 动画增强 */ - .animate-fade-in { - animation: fadeIn 0.3s ease-out; + .line-clamp-3 { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; } - .animate-slide-up { - animation: slideUp 0.3s ease-out; + /* 状态指示器 */ + .status-indicator { + @apply inline-flex items-center gap-2 px-3 py-1 rounded-full text-xs font-medium; } - /* 自定义滚动条 */ + .status-success { + @apply bg-green-100 text-green-800; + } + + .status-warning { + @apply bg-yellow-100 text-yellow-800; + } + + .status-error { + @apply bg-red-100 text-red-800; + } + + .status-info { + @apply bg-blue-100 text-blue-800; + } + + .status-neutral { + @apply bg-gray-100 text-gray-800; + } + + /* 徽章样式 */ + .badge { + @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium; + } + + .badge-primary { + @apply bg-primary-100 text-primary-800; + } + + .badge-secondary { + @apply bg-gray-100 text-gray-800; + } + + .badge-success { + @apply bg-green-100 text-green-800; + } + + .badge-warning { + @apply bg-yellow-100 text-yellow-800; + } + + .badge-error { + @apply bg-red-100 text-red-800; + } + + /* 分隔线样式 */ + .divider { + @apply border-t border-gray-200; + } + + .divider-vertical { + @apply border-l border-gray-200; + } + + /* 自定义滚动条增强 */ .custom-scrollbar::-webkit-scrollbar { - width: 6px; + width: 8px; + height: 8px; } .custom-scrollbar::-webkit-scrollbar-track { - @apply bg-gray-100 rounded-full; + @apply bg-gray-50 rounded-full; } .custom-scrollbar::-webkit-scrollbar-thumb { - @apply bg-gray-300 rounded-full hover:bg-gray-400; + @apply bg-gray-300 rounded-full hover:bg-gray-400 transition-colors duration-200; + } + + .custom-scrollbar::-webkit-scrollbar-corner { + @apply bg-gray-50; } /* 焦点样式增强 */ .focus-visible { @apply focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2; } + + /* 交互状态 */ + .interactive { + @apply transition-all duration-200 cursor-pointer; + } + + .interactive:hover { + @apply transform scale-105; + } + + .interactive:active { + @apply transform scale-95; + } + + /* 玻璃态效果 */ + .glass { + @apply bg-white bg-opacity-80 backdrop-blur-md border border-white border-opacity-20; + } + + .glass-dark { + @apply bg-gray-900 bg-opacity-80 backdrop-blur-md border border-gray-700 border-opacity-20; + } + + /* 渐变背景 */ + .gradient-primary { + @apply bg-gradient-to-r from-primary-500 to-primary-600; + } + + .gradient-secondary { + @apply bg-gradient-to-r from-gray-500 to-gray-600; + } + + .gradient-success { + @apply bg-gradient-to-r from-green-500 to-green-600; + } + + .gradient-warning { + @apply bg-gradient-to-r from-yellow-500 to-yellow-600; + } + + .gradient-error { + @apply bg-gradient-to-r from-red-500 to-red-600; + } + + /* 文本渐变 */ + .text-gradient-primary { + @apply bg-gradient-to-r from-primary-600 to-primary-800 bg-clip-text text-transparent; + } + + .text-gradient-secondary { + @apply bg-gradient-to-r from-gray-600 to-gray-800 bg-clip-text text-transparent; + } + + /* 阴影变体 */ + .shadow-glow { + box-shadow: 0 0 20px rgba(59, 130, 246, 0.15); + } + + .shadow-glow-lg { + box-shadow: 0 0 40px rgba(59, 130, 246, 0.2); + } + + .shadow-colored { + box-shadow: 0 10px 25px -5px rgba(59, 130, 246, 0.1), 0 4px 6px -2px rgba(59, 130, 246, 0.05); + } } \ No newline at end of file diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index b71fc70..d66631c 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -48,8 +48,9 @@ function App() { return ( -
-
+
+ {/* 主要内容区域 */} +
} /> } /> diff --git a/apps/desktop/src/components/AnimatedButton.tsx b/apps/desktop/src/components/AnimatedButton.tsx new file mode 100644 index 0000000..c1d2c85 --- /dev/null +++ b/apps/desktop/src/components/AnimatedButton.tsx @@ -0,0 +1,195 @@ +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 = ({ + 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>([]); + + const handleClick = (e: React.MouseEvent) => { + 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 ( + + ); +}; + +/** + * 浮动操作按钮组件 + */ +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 ( + + ); +}; diff --git a/apps/desktop/src/components/EmptyState.tsx b/apps/desktop/src/components/EmptyState.tsx index 2e613fb..3a9dfc0 100644 --- a/apps/desktop/src/components/EmptyState.tsx +++ b/apps/desktop/src/components/EmptyState.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { FolderPlus } from 'lucide-react'; +import { FolderPlus, Sparkles } from 'lucide-react'; interface EmptyStateProps { title: string; @@ -10,7 +10,7 @@ interface EmptyStateProps { /** * 空状态组件 - * 遵循简洁大方的设计风格 + * 遵循现代化设计风格,提供更好的用户体验 */ export const EmptyState: React.FC = ({ title, @@ -19,15 +19,48 @@ export const EmptyState: React.FC = ({ onAction }) => { return ( -
-
- +
+ {/* 图标区域 */} +
+ {/* 背景装饰 */} +
+ + {/* 主图标 */} +
+ + + {/* 装饰性小图标 */} +
+ +
+
+
+ + {/* 文本内容 */} +
+

+ {title} +

+

+ {description} +

+
+ + {/* 操作按钮 */} +
+ +
+ + {/* 提示信息 */} +
+

💡 提示:您可以通过拖拽文件夹到此处快速创建项目

-

{title}

-

{description}

-
); }; diff --git a/apps/desktop/src/components/ErrorMessage.tsx b/apps/desktop/src/components/ErrorMessage.tsx index 780a54e..2894992 100644 --- a/apps/desktop/src/components/ErrorMessage.tsx +++ b/apps/desktop/src/components/ErrorMessage.tsx @@ -1,36 +1,125 @@ import React from 'react'; -import { AlertCircle, RefreshCw, X } from 'lucide-react'; +import { AlertCircle, RefreshCw, X, AlertTriangle, Info, CheckCircle } from 'lucide-react'; interface ErrorMessageProps { message: string; + type?: 'error' | 'warning' | 'info' | 'success'; onRetry?: () => void; onDismiss?: () => void; + title?: string; } /** - * 错误消息组件 + * 增强的消息组件 + * 支持多种消息类型和更好的视觉设计 */ export const ErrorMessage: React.FC = ({ message, + type = 'error', onRetry, - onDismiss + onDismiss, + title }) => { + const getConfig = () => { + switch (type) { + case 'warning': + return { + bgColor: 'bg-yellow-50', + borderColor: 'border-yellow-200', + textColor: 'text-yellow-800', + iconColor: 'text-yellow-600', + icon: AlertTriangle, + defaultTitle: '警告' + }; + case 'info': + return { + bgColor: 'bg-blue-50', + borderColor: 'border-blue-200', + textColor: 'text-blue-800', + iconColor: 'text-blue-600', + icon: Info, + defaultTitle: '信息' + }; + case 'success': + return { + bgColor: 'bg-green-50', + borderColor: 'border-green-200', + textColor: 'text-green-800', + iconColor: 'text-green-600', + icon: CheckCircle, + defaultTitle: '成功' + }; + default: + return { + bgColor: 'bg-red-50', + borderColor: 'border-red-200', + textColor: 'text-red-800', + iconColor: 'text-red-600', + icon: AlertCircle, + defaultTitle: '错误' + }; + } + }; + + const config = getConfig(); + const Icon = config.icon; + return ( -
-
- - {message} -
-
- {onRetry && ( - - )} +
+
+ {/* 图标 */} +
+ +
+ + {/* 内容 */} +
+ {/* 标题 */} + {(title || config.defaultTitle) && ( +

+ {title || config.defaultTitle} +

+ )} + + {/* 消息内容 */} +

+ {message} +

+ + {/* 操作按钮 */} + {(onRetry || onDismiss) && ( +
+ {onRetry && ( + + )} + {onDismiss && ( + + )} +
+ )} +
+ + {/* 右上角关闭按钮 */} {onDismiss && ( - )}
diff --git a/apps/desktop/src/components/LoadingSpinner.tsx b/apps/desktop/src/components/LoadingSpinner.tsx index c07dbb5..1fb8bc4 100644 --- a/apps/desktop/src/components/LoadingSpinner.tsx +++ b/apps/desktop/src/components/LoadingSpinner.tsx @@ -1,28 +1,119 @@ import React from 'react'; -import { Loader2 } from 'lucide-react'; +import { Loader2, Circle } from 'lucide-react'; interface LoadingSpinnerProps { size?: 'small' | 'medium' | 'large'; text?: string; + variant?: 'spinner' | 'dots' | 'pulse' | 'bars'; + color?: 'primary' | 'secondary' | 'white'; } /** - * 加载动画组件 + * 增强的加载动画组件 + * 支持多种动画样式和尺寸 */ export const LoadingSpinner: React.FC = ({ size = 'medium', - text + text, + variant = 'spinner', + color = 'primary' }) => { const sizeMap = { - small: 16, - medium: 24, - large: 32 + small: { icon: 16, text: 'text-sm', padding: 'p-3' }, + medium: { icon: 24, text: 'text-base', padding: 'p-6' }, + large: { icon: 32, text: 'text-lg', padding: 'p-8' } + }; + + const colorMap = { + primary: 'text-primary-600', + secondary: 'text-gray-600', + white: 'text-white' + }; + + const currentSize = sizeMap[size]; + const currentColor = colorMap[color]; + + const renderSpinner = () => { + switch (variant) { + case 'dots': + return ( +
+ {[0, 1, 2].map((i) => ( + + ))} +
+ ); + + case 'pulse': + return ( +
+ ); + + case 'bars': + return ( +
+ {[0, 1, 2, 3].map((i) => ( +
+ ))} +
+ ); + + default: + return ( + + ); + } }; return ( -
- - {text && {text}} +
+ {/* 加载动画 */} +
+ {/* 背景光晕效果 */} + {color === 'primary' && ( +
+ )} + + {/* 主要动画 */} +
+ {renderSpinner()} +
+
+ + {/* 加载文本 */} + {text && ( +
+ + {text} + + + {/* 动态点点点效果 */} + + {[0, 1, 2].map((i) => ( + + . + + ))} + +
+ )}
); }; diff --git a/apps/desktop/src/components/PageTransition.tsx b/apps/desktop/src/components/PageTransition.tsx new file mode 100644 index 0000000..c133ba8 --- /dev/null +++ b/apps/desktop/src/components/PageTransition.tsx @@ -0,0 +1,209 @@ +import React, { useEffect, useState } from 'react'; + +interface PageTransitionProps { + children: React.ReactNode; + className?: string; + delay?: number; +} + +/** + * 页面过渡动画组件 + * 提供平滑的页面切换效果 + */ +export const PageTransition: React.FC = ({ + children, + className = '', + delay = 0 +}) => { + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + const timer = setTimeout(() => { + setIsVisible(true); + }, delay); + + return () => clearTimeout(timer); + }, [delay]); + + return ( +
+ {children} +
+ ); +}; + +/** + * 交错动画容器 + * 为子元素提供依次出现的动画效果 + */ +export const StaggeredContainer: React.FC<{ + children: React.ReactNode; + staggerDelay?: number; + className?: string; +}> = ({ children, staggerDelay = 100, className = '' }) => { + return ( +
+ {React.Children.map(children, (child, index) => ( + + {child} + + ))} +
+ ); +}; + +/** + * 滑入动画组件 + */ +export const SlideIn: React.FC<{ + children: React.ReactNode; + direction?: 'left' | 'right' | 'up' | 'down'; + delay?: number; + className?: string; +}> = ({ children, direction = 'up', delay = 0, className = '' }) => { + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + const timer = setTimeout(() => { + setIsVisible(true); + }, delay); + + return () => clearTimeout(timer); + }, [delay]); + + const getTransformClasses = () => { + const transforms = { + left: isVisible ? 'translate-x-0' : '-translate-x-8', + right: isVisible ? 'translate-x-0' : 'translate-x-8', + up: isVisible ? 'translate-y-0' : 'translate-y-8', + down: isVisible ? 'translate-y-0' : '-translate-y-8' + }; + return transforms[direction]; + }; + + return ( +
+ {children} +
+ ); +}; + +/** + * 缩放动画组件 + */ +export const ScaleIn: React.FC<{ + children: React.ReactNode; + delay?: number; + className?: string; +}> = ({ children, delay = 0, className = '' }) => { + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + const timer = setTimeout(() => { + setIsVisible(true); + }, delay); + + return () => clearTimeout(timer); + }, [delay]); + + return ( +
+ {children} +
+ ); +}; + +/** + * 悬停放大组件 + */ +export const HoverScale: React.FC<{ + children: React.ReactNode; + scale?: number; + className?: string; +}> = ({ children, scale = 1.05, className = '' }) => { + return ( +
+ {children} +
+ ); +}; + +/** + * 悬停倾斜组件 + */ +export const HoverTilt: React.FC<{ + children: React.ReactNode; + degree?: number; + className?: string; +}> = ({ children, degree = 2, className = '' }) => { + return ( +
+ {children} +
+ ); +}; + +/** + * 脉冲动画组件 + */ +export const Pulse: React.FC<{ + children: React.ReactNode; + className?: string; +}> = ({ children, className = '' }) => { + return ( +
+ {children} +
+ ); +}; + +/** + * 呼吸动画组件 + */ +export const Breathe: React.FC<{ + children: React.ReactNode; + className?: string; +}> = ({ children, className = '' }) => { + return ( +
+ {children} +
+ ); +}; diff --git a/apps/desktop/src/components/ProjectCard.tsx b/apps/desktop/src/components/ProjectCard.tsx index c6f113a..b407bed 100644 --- a/apps/desktop/src/components/ProjectCard.tsx +++ b/apps/desktop/src/components/ProjectCard.tsx @@ -17,9 +17,7 @@ import { FileAudio, FileImage, File, - HardDrive, - BarChart3, - Hash + BarChart3 } from 'lucide-react'; interface ProjectCardProps { @@ -141,131 +139,159 @@ export const ProjectCard: React.FC = ({ }; return ( -
-
-
- -
-
- - {showMenu && ( -
- - - - +
+ {/* 背景装饰 */} +
+ + {/* 卡片头部 */} +
+
+
+
+
- )} +
+

+ {project.name} +

+
+ + {formatTime(project.updated_at)} +
+
+
+ + {/* 菜单按钮 */} +
+ + {showMenu && ( +
+ + +
+ + +
+ )} +
-
- -
onOpen(project)}> -

- {project.name} -

+ {/* 项目描述 */} {project.description && ( -

+

{project.description}

)} +
- {/* 项目统计信息 */} + {/* 项目统计信息 */} +
onOpen(project)}> {isLoadingStats ? ( -
-
+
+
加载统计中...
) : stats && ( -
-
- - 项目统计 +
+
+
+ + 项目统计 +
+ {stats.total_materials > 0 && ( +
+ {stats.total_materials} 个素材 +
+ )}
- {/* 素材统计 */} -
-
- - {stats.total_materials} 个素材 + {/* 素材统计网格 */} +
+
+
+ {stats.total_materials} +
+
总素材
-
- - {formatFileSize(stats.total_size)} +
+
+ {formatFileSize(stats.total_size)} +
+
总大小
{/* 文件类型分布 */} - {(stats.video_count > 0 || stats.audio_count > 0 || stats.image_count > 0) && ( -
+ {(stats.video_count > 0 || stats.audio_count > 0 || stats.image_count > 0 || stats.other_count > 0) && ( +
{stats.video_count > 0 && ( -
+
- {stats.video_count} + {stats.video_count}
)} {stats.audio_count > 0 && ( -
+
- {stats.audio_count} + {stats.audio_count}
)} {stats.image_count > 0 && ( -
+
- {stats.image_count} + {stats.image_count}
)} {stats.other_count > 0 && ( -
+
- {stats.other_count} + {stats.other_count}
)}
@@ -273,43 +299,40 @@ export const ProjectCard: React.FC = ({
)} - {/* 项目基本信息 */} -
-
- - - {getDirectoryName(project.path)} - -
-
- - - {formatTime(project.updated_at)} - -
+ {/* 项目路径信息 */} +
+ + + {getDirectoryName(project.path)} +
-
- - - + {/* 卡片底部操作区 */} +
+
+ + + +
); diff --git a/apps/desktop/src/components/ProjectForm.tsx b/apps/desktop/src/components/ProjectForm.tsx index 581990a..bf5d08d 100644 --- a/apps/desktop/src/components/ProjectForm.tsx +++ b/apps/desktop/src/components/ProjectForm.tsx @@ -133,20 +133,33 @@ export const ProjectForm: React.FC = ({ return (
-
+
+ {/* 模态框头部 */}
-

- {isEdit ? '编辑项目' : '新建项目'} -

+
+
+ +
+
+

+ {isEdit ? '编辑项目' : '新建项目'} +

+

+ {isEdit ? '修改项目信息' : '创建一个新的视频项目'} +

+
+
-
-
-