Search K
Appearance
Appearance
📊 SEO元描述:2024年最新JavaScript智能待办事项管理器界面设计与交互教程,详解响应式UI设计、用户交互优化、动画效果实现。包含完整的React+Tailwind CSS实现,适合前端开发者快速掌握现代UI开发技术。
核心关键词:JavaScript界面设计2024、React UI开发、用户交互设计、响应式界面、前端动画效果、用户体验优化
长尾关键词:JavaScript界面怎么设计、React组件开发、前端交互效果、响应式设计实现、用户体验优化方法
通过本节JavaScript界面设计与交互教程,你将系统性掌握:
现代化界面设计系统需要考虑用户体验、可访问性、性能和可维护性等多个维度。我们将构建一个基于设计系统的组件库,实现一致性和可扩展性的完美平衡,也是现代前端UI开发的最佳实践案例。
💡 设计理念:采用原子设计方法论,从最小的设计元素开始,逐步构建复杂的界面组件,确保设计的一致性和系统性。
// 🎉 设计令牌系统定义
// 颜色系统
export const colors = {
// 主色调
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
950: '#172554'
},
// 辅助色
secondary: {
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
950: '#020617'
},
// 语义化颜色
semantic: {
success: {
light: '#dcfce7',
DEFAULT: '#16a34a',
dark: '#15803d'
},
warning: {
light: '#fef3c7',
DEFAULT: '#d97706',
dark: '#b45309'
},
error: {
light: '#fee2e2',
DEFAULT: '#dc2626',
dark: '#b91c1c'
},
info: {
light: '#dbeafe',
DEFAULT: '#2563eb',
dark: '#1d4ed8'
}
},
// 中性色
neutral: {
white: '#ffffff',
black: '#000000',
transparent: 'transparent',
current: 'currentColor'
}
} as const;
// 字体系统
export const typography = {
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'Consolas', 'monospace'],
display: ['Cal Sans', 'Inter', 'system-ui', 'sans-serif']
},
fontSize: {
xs: ['0.75rem', { lineHeight: '1rem' }],
sm: ['0.875rem', { lineHeight: '1.25rem' }],
base: ['1rem', { lineHeight: '1.5rem' }],
lg: ['1.125rem', { lineHeight: '1.75rem' }],
xl: ['1.25rem', { lineHeight: '1.75rem' }],
'2xl': ['1.5rem', { lineHeight: '2rem' }],
'3xl': ['1.875rem', { lineHeight: '2.25rem' }],
'4xl': ['2.25rem', { lineHeight: '2.5rem' }],
'5xl': ['3rem', { lineHeight: '1' }],
'6xl': ['3.75rem', { lineHeight: '1' }]
},
fontWeight: {
thin: '100',
extralight: '200',
light: '300',
normal: '400',
medium: '500',
semibold: '600',
bold: '700',
extrabold: '800',
black: '900'
}
} as const;
// 间距系统
export const spacing = {
0: '0px',
px: '1px',
0.5: '0.125rem',
1: '0.25rem',
1.5: '0.375rem',
2: '0.5rem',
2.5: '0.625rem',
3: '0.75rem',
3.5: '0.875rem',
4: '1rem',
5: '1.25rem',
6: '1.5rem',
7: '1.75rem',
8: '2rem',
9: '2.25rem',
10: '2.5rem',
11: '2.75rem',
12: '3rem',
14: '3.5rem',
16: '4rem',
20: '5rem',
24: '6rem',
28: '7rem',
32: '8rem',
36: '9rem',
40: '10rem',
44: '11rem',
48: '12rem',
52: '13rem',
56: '14rem',
60: '15rem',
64: '16rem',
72: '18rem',
80: '20rem',
96: '24rem'
} as const;
// 阴影系统
export const shadows = {
sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
DEFAULT: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
md: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
xl: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)',
'2xl': '0 25px 50px -12px rgb(0 0 0 / 0.25)',
inner: 'inset 0 2px 4px 0 rgb(0 0 0 / 0.05)',
none: '0 0 #0000'
} as const;
// 圆角系统
export const borderRadius = {
none: '0px',
sm: '0.125rem',
DEFAULT: '0.25rem',
md: '0.375rem',
lg: '0.5rem',
xl: '0.75rem',
'2xl': '1rem',
'3xl': '1.5rem',
full: '9999px'
} as const;
// 动画系统
export const animation = {
duration: {
75: '75ms',
100: '100ms',
150: '150ms',
200: '200ms',
300: '300ms',
500: '500ms',
700: '700ms',
1000: '1000ms'
},
easing: {
linear: 'linear',
in: 'cubic-bezier(0.4, 0, 1, 1)',
out: 'cubic-bezier(0, 0, 0.2, 1)',
'in-out': 'cubic-bezier(0.4, 0, 0.2, 1)'
},
keyframes: {
spin: {
to: { transform: 'rotate(360deg)' }
},
ping: {
'75%, 100%': { transform: 'scale(2)', opacity: '0' }
},
pulse: {
'50%': { opacity: '0.5' }
},
bounce: {
'0%, 100%': {
transform: 'translateY(-25%)',
animationTimingFunction: 'cubic-bezier(0.8,0,1,1)'
},
'50%': {
transform: 'none',
animationTimingFunction: 'cubic-bezier(0,0,0.2,1)'
}
},
fadeIn: {
from: { opacity: '0' },
to: { opacity: '1' }
},
slideInUp: {
from: { transform: 'translateY(100%)', opacity: '0' },
to: { transform: 'translateY(0)', opacity: '1' }
},
slideInDown: {
from: { transform: 'translateY(-100%)', opacity: '0' },
to: { transform: 'translateY(0)', opacity: '1' }
}
}
} as const;
// 断点系统
export const breakpoints = {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
'2xl': '1536px'
} as const;
// Z-index系统
export const zIndex = {
auto: 'auto',
0: '0',
10: '10',
20: '20',
30: '30',
40: '40',
50: '50',
dropdown: '1000',
sticky: '1020',
fixed: '1030',
modal: '1040',
popover: '1050',
tooltip: '1060',
toast: '1070'
} as const;
// 主题配置
export interface Theme {
colors: typeof colors;
typography: typeof typography;
spacing: typeof spacing;
shadows: typeof shadows;
borderRadius: typeof borderRadius;
animation: typeof animation;
breakpoints: typeof breakpoints;
zIndex: typeof zIndex;
}
export const lightTheme: Theme = {
colors,
typography,
spacing,
shadows,
borderRadius,
animation,
breakpoints,
zIndex
};
export const darkTheme: Theme = {
...lightTheme,
colors: {
...colors,
// 暗色主题的颜色覆盖
primary: {
...colors.primary,
500: '#60a5fa',
600: '#3b82f6'
},
secondary: {
50: '#0f172a',
100: '#1e293b',
200: '#334155',
300: '#475569',
400: '#64748b',
500: '#94a3b8',
600: '#cbd5e1',
700: '#e2e8f0',
800: '#f1f5f9',
900: '#f8fafc',
950: '#ffffff'
}
}
};// 🎉 基础组件库实现
import React, { forwardRef, ButtonHTMLAttributes, InputHTMLAttributes } from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
// Button组件
const buttonVariants = cva(
// 基础样式
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'underline-offset-4 hover:underline text-primary'
},
size: {
default: 'h-10 py-2 px-4',
sm: 'h-9 px-3 rounded-md',
lg: 'h-11 px-8 rounded-md',
icon: 'h-10 w-10'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
}
);
export interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
loading?: boolean;
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, loading, children, disabled, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
disabled={disabled || loading}
{...props}
>
{loading && (
<svg
className="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
)}
{children}
</button>
);
}
);
Button.displayName = 'Button';
// Input组件
const inputVariants = cva(
'flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
{
variants: {
variant: {
default: '',
error: 'border-destructive focus-visible:ring-destructive'
},
size: {
default: 'h-10',
sm: 'h-9',
lg: 'h-11'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
}
);
export interface InputProps
extends InputHTMLAttributes<HTMLInputElement>,
VariantProps<typeof inputVariants> {
error?: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, variant, size, error, ...props }, ref) => {
return (
<div className="space-y-1">
<input
className={cn(inputVariants({ variant: error ? 'error' : variant, size, className }))}
ref={ref}
{...props}
/>
{error && (
<p className="text-sm text-destructive">{error}</p>
)}
</div>
);
}
);
Input.displayName = 'Input';
// Card组件
const Card = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
'rounded-lg border bg-card text-card-foreground shadow-sm',
className
)}
{...props}
/>
)
);
Card.displayName = 'Card';
const CardHeader = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
)
);
CardHeader.displayName = 'CardHeader';
const CardTitle = forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn('text-2xl font-semibold leading-none tracking-tight', className)}
{...props}
/>
)
);
CardTitle.displayName = 'CardTitle';
const CardDescription = forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
)
);
CardDescription.displayName = 'CardDescription';
const CardContent = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
)
);
CardContent.displayName = 'CardContent';
const CardFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
)
);
CardFooter.displayName = 'CardFooter';
// Badge组件
const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive: 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: 'text-foreground'
}
},
defaultVariants: {
variant: 'default'
}
}
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
// Avatar组件
const Avatar = forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(
({ className, ...props }, ref) => (
<span
ref={ref}
className={cn(
'relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full',
className
)}
{...props}
/>
)
);
Avatar.displayName = 'Avatar';
const AvatarImage = forwardRef<HTMLImageElement, React.ImgHTMLAttributes<HTMLImageElement>>(
({ className, ...props }, ref) => (
<img
ref={ref}
className={cn('aspect-square h-full w-full', className)}
{...props}
/>
)
);
AvatarImage.displayName = 'AvatarImage';
const AvatarFallback = forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(
({ className, ...props }, ref) => (
<span
ref={ref}
className={cn(
'flex h-full w-full items-center justify-center rounded-full bg-muted',
className
)}
{...props}
/>
)
);
AvatarFallback.displayName = 'AvatarFallback';
// Tooltip组件
interface TooltipProps {
children: React.ReactNode;
content: React.ReactNode;
side?: 'top' | 'right' | 'bottom' | 'left';
align?: 'start' | 'center' | 'end';
}
const Tooltip: React.FC<TooltipProps> = ({
children,
content,
side = 'top',
align = 'center'
}) => {
const [isVisible, setIsVisible] = React.useState(false);
return (
<div
className="relative inline-block"
onMouseEnter={() => setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
>
{children}
{isVisible && (
<div
className={cn(
'absolute z-tooltip px-2 py-1 text-xs text-white bg-gray-900 rounded shadow-lg whitespace-nowrap',
{
'bottom-full mb-1': side === 'top',
'top-full mt-1': side === 'bottom',
'right-full mr-1': side === 'left',
'left-full ml-1': side === 'right',
'left-1/2 transform -translate-x-1/2': align === 'center' && (side === 'top' || side === 'bottom'),
'top-1/2 transform -translate-y-1/2': align === 'center' && (side === 'left' || side === 'right'),
'left-0': align === 'start' && (side === 'top' || side === 'bottom'),
'right-0': align === 'end' && (side === 'top' || side === 'bottom'),
'top-0': align === 'start' && (side === 'left' || side === 'right'),
'bottom-0': align === 'end' && (side === 'left' || side === 'right')
}
)}
>
{content}
</div>
)}
</div>
);
};
// 导出所有组件
export {
Button,
Input,
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
Badge,
Avatar,
AvatarImage,
AvatarFallback,
Tooltip,
buttonVariants,
inputVariants,
badgeVariants
};
// 工具函数
export function cn(...inputs: (string | undefined)[]): string {
return inputs.filter(Boolean).join(' ');
}基础组件库的核心特点:
💼 组件设计价值:通过系统化的组件设计,提高开发效率,确保界面一致性,同时为用户提供优秀的交互体验。
// 🎉 任务管理界面组件实现
import React, { useState, useCallback, useMemo } from 'react';
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
import { format, isToday, isTomorrow, isPast } from 'date-fns';
import { zhCN } from 'date-fns/locale';
import {
CheckCircle2,
Circle,
Calendar,
Tag,
MoreHorizontal,
Plus,
Filter,
Search,
SortAsc,
Grid3X3,
List,
Clock,
AlertCircle
} from 'lucide-react';
import { Task, TaskStatus, TaskPriority } from '@/types/task';
import { useTasks } from '@/hooks/useTasks';
import {
Button,
Input,
Card,
CardContent,
Badge,
Avatar,
AvatarFallback,
Tooltip
} from '@/components/ui';
// 任务项组件
interface TaskItemProps {
task: Task;
index: number;
onToggle: (id: string) => void;
onEdit: (task: Task) => void;
onDelete: (id: string) => void;
isDragging?: boolean;
}
const TaskItem: React.FC<TaskItemProps> = React.memo(({
task,
index,
onToggle,
onEdit,
onDelete,
isDragging
}) => {
const [isHovered, setIsHovered] = useState(false);
const priorityColors = {
[TaskPriority.LOW]: 'bg-blue-100 text-blue-800',
[TaskPriority.MEDIUM]: 'bg-yellow-100 text-yellow-800',
[TaskPriority.HIGH]: 'bg-orange-100 text-orange-800',
[TaskPriority.URGENT]: 'bg-red-100 text-red-800'
};
const statusIcons = {
[TaskStatus.TODO]: Circle,
[TaskStatus.IN_PROGRESS]: Clock,
[TaskStatus.COMPLETED]: CheckCircle2,
[TaskStatus.CANCELLED]: AlertCircle,
[TaskStatus.ON_HOLD]: Clock
};
const StatusIcon = statusIcons[task.status];
const formatDueDate = (date: Date) => {
if (isToday(date)) return '今天';
if (isTomorrow(date)) return '明天';
return format(date, 'MM月dd日', { locale: zhCN });
};
const isDueSoon = task.dueDate && isPast(new Date(task.dueDate)) && task.status !== TaskStatus.COMPLETED;
return (
<Draggable draggableId={task.id} index={index}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className={cn(
'group transition-all duration-200',
snapshot.isDragging && 'rotate-2 scale-105'
)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<Card className={cn(
'mb-3 transition-all duration-200 hover:shadow-md',
task.status === TaskStatus.COMPLETED && 'opacity-75',
isDueSoon && 'border-red-200 bg-red-50',
snapshot.isDragging && 'shadow-lg'
)}>
<CardContent className="p-4">
<div className="flex items-start space-x-3">
{/* 状态切换按钮 */}
<button
onClick={() => onToggle(task.id)}
className={cn(
'mt-0.5 transition-colors duration-200',
task.status === TaskStatus.COMPLETED
? 'text-green-600 hover:text-green-700'
: 'text-gray-400 hover:text-gray-600'
)}
>
<StatusIcon className="h-5 w-5" />
</button>
{/* 任务内容 */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className={cn(
'text-sm font-medium text-gray-900 mb-1',
task.status === TaskStatus.COMPLETED && 'line-through text-gray-500'
)}>
{task.title}
</h3>
{task.description && (
<p className="text-xs text-gray-600 mb-2 line-clamp-2">
{task.description}
</p>
)}
{/* 元数据 */}
<div className="flex items-center space-x-2 text-xs text-gray-500">
{/* 优先级 */}
<Badge
variant="secondary"
className={cn('text-xs', priorityColors[task.priority])}
>
{task.priority}
</Badge>
{/* 截止日期 */}
{task.dueDate && (
<div className={cn(
'flex items-center space-x-1',
isDueSoon && 'text-red-600'
)}>
<Calendar className="h-3 w-3" />
<span>{formatDueDate(new Date(task.dueDate))}</span>
</div>
)}
{/* 标签 */}
{task.tags.length > 0 && (
<div className="flex items-center space-x-1">
<Tag className="h-3 w-3" />
<span>{task.tags.slice(0, 2).join(', ')}</span>
{task.tags.length > 2 && (
<span>+{task.tags.length - 2}</span>
)}
</div>
)}
{/* 进度 */}
{task.progress > 0 && task.status !== TaskStatus.COMPLETED && (
<div className="flex items-center space-x-1">
<div className="w-12 h-1.5 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-blue-500 transition-all duration-300"
style={{ width: `${task.progress}%` }}
/>
</div>
<span className="text-xs">{task.progress}%</span>
</div>
)}
</div>
{/* 子任务进度 */}
{task.subtasks.length > 0 && (
<div className="mt-2 text-xs text-gray-500">
<div className="flex items-center space-x-2">
<div className="flex-1 bg-gray-200 rounded-full h-1.5">
<div
className="bg-green-500 h-1.5 rounded-full transition-all duration-300"
style={{
width: `${(task.subtasks.filter(s => s.completed).length / task.subtasks.length) * 100}%`
}}
/>
</div>
<span>
{task.subtasks.filter(s => s.completed).length}/{task.subtasks.length}
</span>
</div>
</div>
)}
</div>
{/* 操作按钮 */}
<div className={cn(
'flex items-center space-x-1 opacity-0 transition-opacity duration-200',
(isHovered || snapshot.isDragging) && 'opacity-100'
)}>
<Tooltip content="编辑任务">
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => onEdit(task)}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</Tooltip>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
)}
</Draggable>
);
});
TaskItem.displayName = 'TaskItem';
// 任务列表组件
interface TaskListProps {
tasks: Task[];
onTaskToggle: (id: string) => void;
onTaskEdit: (task: Task) => void;
onTaskDelete: (id: string) => void;
onTaskReorder: (result: DropResult) => void;
loading?: boolean;
}
const TaskList: React.FC<TaskListProps> = ({
tasks,
onTaskToggle,
onTaskEdit,
onTaskDelete,
onTaskReorder,
loading
}) => {
if (loading) {
return (
<div className="space-y-3">
{Array.from({ length: 5 }).map((_, index) => (
<Card key={index} className="animate-pulse">
<CardContent className="p-4">
<div className="flex items-start space-x-3">
<div className="w-5 h-5 bg-gray-200 rounded-full" />
<div className="flex-1 space-y-2">
<div className="h-4 bg-gray-200 rounded w-3/4" />
<div className="h-3 bg-gray-200 rounded w-1/2" />
<div className="flex space-x-2">
<div className="h-5 bg-gray-200 rounded w-16" />
<div className="h-5 bg-gray-200 rounded w-20" />
</div>
</div>
</div>
</CardContent>
</Card>
))}
</div>
);
}
if (tasks.length === 0) {
return (
<div className="text-center py-12">
<div className="w-24 h-24 mx-auto mb-4 text-gray-300">
<CheckCircle2 className="w-full h-full" />
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">暂无任务</h3>
<p className="text-gray-500 mb-4">创建你的第一个任务开始管理工作吧</p>
<Button>
<Plus className="w-4 h-4 mr-2" />
创建任务
</Button>
</div>
);
}
return (
<DragDropContext onDragEnd={onTaskReorder}>
<Droppable droppableId="tasks">
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
className={cn(
'transition-colors duration-200',
snapshot.isDraggingOver && 'bg-blue-50 rounded-lg'
)}
>
{tasks.map((task, index) => (
<TaskItem
key={task.id}
task={task}
index={index}
onToggle={onTaskToggle}
onEdit={onTaskEdit}
onDelete={onTaskDelete}
/>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
};
// 任务筛选栏组件
interface TaskFilterBarProps {
searchQuery: string;
onSearchChange: (query: string) => void;
selectedStatus: TaskStatus | 'all';
onStatusChange: (status: TaskStatus | 'all') => void;
selectedPriority: TaskPriority | 'all';
onPriorityChange: (priority: TaskPriority | 'all') => void;
viewMode: 'list' | 'board';
onViewModeChange: (mode: 'list' | 'board') => void;
sortBy: string;
onSortChange: (sort: string) => void;
}
const TaskFilterBar: React.FC<TaskFilterBarProps> = ({
searchQuery,
onSearchChange,
selectedStatus,
onStatusChange,
selectedPriority,
onPriorityChange,
viewMode,
onViewModeChange,
sortBy,
onSortChange
}) => {
return (
<div className="bg-white border-b border-gray-200 p-4 space-y-4">
{/* 搜索和视图切换 */}
<div className="flex items-center justify-between">
<div className="flex-1 max-w-md">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
placeholder="搜索任务..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex items-center space-x-2">
{/* 视图模式切换 */}
<div className="flex items-center bg-gray-100 rounded-lg p-1">
<Button
variant={viewMode === 'list' ? 'default' : 'ghost'}
size="sm"
onClick={() => onViewModeChange('list')}
className="h-8"
>
<List className="h-4 w-4" />
</Button>
<Button
variant={viewMode === 'board' ? 'default' : 'ghost'}
size="sm"
onClick={() => onViewModeChange('board')}
className="h-8"
>
<Grid3X3 className="h-4 w-4" />
</Button>
</div>
</div>
</div>
{/* 筛选器 */}
<div className="flex items-center space-x-4 overflow-x-auto">
{/* 状态筛选 */}
<div className="flex items-center space-x-2">
<span className="text-sm font-medium text-gray-700 whitespace-nowrap">状态:</span>
<select
value={selectedStatus}
onChange={(e) => onStatusChange(e.target.value as TaskStatus | 'all')}
className="text-sm border border-gray-300 rounded-md px-2 py-1"
>
<option value="all">全部</option>
<option value={TaskStatus.TODO}>待办</option>
<option value={TaskStatus.IN_PROGRESS}>进行中</option>
<option value={TaskStatus.COMPLETED}>已完成</option>
<option value={TaskStatus.ON_HOLD}>暂停</option>
</select>
</div>
{/* 优先级筛选 */}
<div className="flex items-center space-x-2">
<span className="text-sm font-medium text-gray-700 whitespace-nowrap">优先级:</span>
<select
value={selectedPriority}
onChange={(e) => onPriorityChange(e.target.value as TaskPriority | 'all')}
className="text-sm border border-gray-300 rounded-md px-2 py-1"
>
<option value="all">全部</option>
<option value={TaskPriority.URGENT}>紧急</option>
<option value={TaskPriority.HIGH}>重要</option>
<option value={TaskPriority.MEDIUM}>普通</option>
<option value={TaskPriority.LOW}>较低</option>
</select>
</div>
{/* 排序 */}
<div className="flex items-center space-x-2">
<span className="text-sm font-medium text-gray-700 whitespace-nowrap">排序:</span>
<select
value={sortBy}
onChange={(e) => onSortChange(e.target.value)}
className="text-sm border border-gray-300 rounded-md px-2 py-1"
>
<option value="createdAt">创建时间</option>
<option value="dueDate">截止时间</option>
<option value="priority">优先级</option>
<option value="title">标题</option>
<option value="status">状态</option>
</select>
</div>
</div>
</div>
);
};
// 主任务管理页面组件
const TaskManagementPage: React.FC = () => {
const {
tasks,
loading,
fetchTasks,
toggleTaskStatus,
updateTask,
deleteTask
} = useTasks();
const [searchQuery, setSearchQuery] = useState('');
const [selectedStatus, setSelectedStatus] = useState<TaskStatus | 'all'>('all');
const [selectedPriority, setSelectedPriority] = useState<TaskPriority | 'all'>('all');
const [viewMode, setViewMode] = useState<'list' | 'board'>('list');
const [sortBy, setSortBy] = useState('createdAt');
// 筛选和排序任务
const filteredAndSortedTasks = useMemo(() => {
let filtered = tasks.filter(task => {
// 搜索筛选
if (searchQuery && !task.title.toLowerCase().includes(searchQuery.toLowerCase())) {
return false;
}
// 状态筛选
if (selectedStatus !== 'all' && task.status !== selectedStatus) {
return false;
}
// 优先级筛选
if (selectedPriority !== 'all' && task.priority !== selectedPriority) {
return false;
}
return true;
});
// 排序
filtered.sort((a, b) => {
switch (sortBy) {
case 'dueDate':
if (!a.dueDate && !b.dueDate) return 0;
if (!a.dueDate) return 1;
if (!b.dueDate) return -1;
return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime();
case 'priority':
const priorityOrder = { urgent: 0, high: 1, medium: 2, low: 3 };
return priorityOrder[a.priority] - priorityOrder[b.priority];
case 'title':
return a.title.localeCompare(b.title);
case 'status':
return a.status.localeCompare(b.status);
default: // createdAt
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
}
});
return filtered;
}, [tasks, searchQuery, selectedStatus, selectedPriority, sortBy]);
const handleTaskReorder = useCallback((result: DropResult) => {
if (!result.destination) return;
const items = Array.from(filteredAndSortedTasks);
const [reorderedItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderedItem);
// 这里可以调用API更新任务顺序
console.log('Reordered tasks:', items);
}, [filteredAndSortedTasks]);
const handleTaskEdit = useCallback((task: Task) => {
// 打开编辑模态框
console.log('Edit task:', task);
}, []);
return (
<div className="h-full flex flex-col bg-gray-50">
{/* 筛选栏 */}
<TaskFilterBar
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
selectedStatus={selectedStatus}
onStatusChange={setSelectedStatus}
selectedPriority={selectedPriority}
onPriorityChange={setSelectedPriority}
viewMode={viewMode}
onViewModeChange={setViewMode}
sortBy={sortBy}
onSortChange={setSortBy}
/>
{/* 任务列表 */}
<div className="flex-1 overflow-auto p-4">
<TaskList
tasks={filteredAndSortedTasks}
onTaskToggle={toggleTaskStatus}
onTaskEdit={handleTaskEdit}
onTaskDelete={deleteTask}
onTaskReorder={handleTaskReorder}
loading={loading}
/>
</div>
</div>
);
};
export default TaskManagementPage;// 🎉 交互动画效果实现
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence, useSpring, useTransform } from 'framer-motion';
import { useInView } from 'react-intersection-observer';
// 页面过渡动画
export const PageTransition: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
>
{children}
</motion.div>
);
};
// 列表项动画
export const ListItemAnimation: React.FC<{
children: React.ReactNode;
index: number;
delay?: number;
}> = ({ children, index, delay = 0.1 }) => {
return (
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{
duration: 0.3,
delay: index * delay,
ease: 'easeOut'
}}
layout
>
{children}
</motion.div>
);
};
// 模态框动画
export const ModalAnimation: React.FC<{
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}> = ({ isOpen, onClose, children }) => {
return (
<AnimatePresence>
{isOpen && (
<>
{/* 背景遮罩 */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 bg-black bg-opacity-50 z-modal"
onClick={onClose}
/>
{/* 模态框内容 */}
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
transition={{ duration: 0.2, ease: 'easeOut' }}
className="fixed inset-0 flex items-center justify-center z-modal p-4"
onClick={(e) => e.stopPropagation()}
>
<div className="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[90vh] overflow-auto">
{children}
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
};
// 悬浮按钮动画
export const FloatingActionButton: React.FC<{
onClick: () => void;
icon: React.ReactNode;
label?: string;
}> = ({ onClick, icon, label }) => {
const [isHovered, setIsHovered] = useState(false);
return (
<motion.button
className="fixed bottom-6 right-6 bg-primary text-white rounded-full shadow-lg z-50 flex items-center"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onHoverStart={() => setIsHovered(true)}
onHoverEnd={() => setIsHovered(false)}
onClick={onClick}
>
<div className="p-4">
{icon}
</div>
<AnimatePresence>
{isHovered && label && (
<motion.span
initial={{ width: 0, opacity: 0 }}
animate={{ width: 'auto', opacity: 1 }}
exit={{ width: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="pr-4 whitespace-nowrap overflow-hidden"
>
{label}
</motion.span>
)}
</AnimatePresence>
</motion.button>
);
};
// 进度条动画
export const AnimatedProgressBar: React.FC<{
progress: number;
className?: string;
showLabel?: boolean;
}> = ({ progress, className, showLabel = false }) => {
const [displayProgress, setDisplayProgress] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
setDisplayProgress(progress);
}, 100);
return () => clearTimeout(timer);
}, [progress]);
return (
<div className={cn('relative', className)}>
<div className="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
<motion.div
className="h-full bg-gradient-to-r from-blue-500 to-blue-600 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${displayProgress}%` }}
transition={{ duration: 0.8, ease: 'easeOut' }}
/>
</div>
{showLabel && (
<motion.span
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
className="absolute right-0 top-3 text-xs text-gray-600"
>
{Math.round(displayProgress)}%
</motion.span>
)}
</div>
);
};
// 滚动触发动画
export const ScrollTriggerAnimation: React.FC<{
children: React.ReactNode;
threshold?: number;
triggerOnce?: boolean;
}> = ({ children, threshold = 0.1, triggerOnce = true }) => {
const { ref, inView } = useInView({
threshold,
triggerOnce
});
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 50 }}
animate={inView ? { opacity: 1, y: 0 } : { opacity: 0, y: 50 }}
transition={{ duration: 0.6, ease: 'easeOut' }}
>
{children}
</motion.div>
);
};
// 数字计数动画
export const CountUpAnimation: React.FC<{
end: number;
duration?: number;
prefix?: string;
suffix?: string;
}> = ({ end, duration = 2, prefix = '', suffix = '' }) => {
const [count, setCount] = useState(0);
useEffect(() => {
let startTime: number;
let animationFrame: number;
const animate = (timestamp: number) => {
if (!startTime) startTime = timestamp;
const progress = Math.min((timestamp - startTime) / (duration * 1000), 1);
setCount(Math.floor(progress * end));
if (progress < 1) {
animationFrame = requestAnimationFrame(animate);
}
};
animationFrame = requestAnimationFrame(animate);
return () => {
if (animationFrame) {
cancelAnimationFrame(animationFrame);
}
};
}, [end, duration]);
return (
<motion.span
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
{prefix}{count.toLocaleString()}{suffix}
</motion.span>
);
};
// 加载动画组件
export const LoadingSpinner: React.FC<{
size?: 'sm' | 'md' | 'lg';
className?: string;
}> = ({ size = 'md', className }) => {
const sizeClasses = {
sm: 'w-4 h-4',
md: 'w-6 h-6',
lg: 'w-8 h-8'
};
return (
<motion.div
className={cn('inline-block', sizeClasses[size], className)}
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
>
<svg
className="w-full h-full text-current"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
</motion.div>
);
};
// 脉冲动画
export const PulseAnimation: React.FC<{
children: React.ReactNode;
duration?: number;
}> = ({ children, duration = 2 }) => {
return (
<motion.div
animate={{ scale: [1, 1.05, 1] }}
transition={{
duration,
repeat: Infinity,
ease: 'easeInOut'
}}
>
{children}
</motion.div>
);
};
// 弹跳动画
export const BounceAnimation: React.FC<{
children: React.ReactNode;
delay?: number;
}> = ({ children, delay = 0 }) => {
return (
<motion.div
initial={{ y: -10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{
type: 'spring',
stiffness: 300,
damping: 20,
delay
}}
>
{children}
</motion.div>
);
};
// 导出所有动画组件
export {
PageTransition,
ListItemAnimation,
ModalAnimation,
FloatingActionButton,
AnimatedProgressBar,
ScrollTriggerAnimation,
CountUpAnimation,
LoadingSpinner,
PulseAnimation,
BounceAnimation
};任务界面和动画效果的核心特点:
💼 交互设计价值:通过精心设计的界面组件和动画效果,为用户提供直观、高效、愉悦的任务管理体验。
通过本节JavaScript界面设计与交互的学习,你已经掌握:
A: 优先使用CSS动画和transform属性,避免频繁操作DOM,合理设置动画时长(通常200-500ms),使用will-change属性优化动画性能,在低性能设备上提供简化的动画或禁用动画选项。
A: 采用移动优先的设计策略,使用CSS Grid和Flexbox构建灵活的布局系统,设计合理的断点策略,考虑内容优先级在不同屏幕尺寸下的展示方式,使用容器查询等新技术处理组件级响应式。
A: 建立完整的设计令牌系统,使用TypeScript确保类型安全,编写详细的组件文档和使用示例,建立组件测试和视觉回归测试,定期进行代码审查和重构。
A: 遵循WCAG 2.1 AA标准,确保键盘导航支持,提供合适的ARIA标签,保证足够的颜色对比度,支持屏幕阅读器,提供替代文本和语义化的HTML结构。
A: 实现组件懒加载和代码分割,使用虚拟滚动处理大列表,优化图片加载和缓存策略,减少不必要的重新渲染,使用Web Workers处理复杂计算,实施渐进式加载策略。
// 设计令牌版本管理
export const designTokens = {
version: '2.1.0',
// 版本变更记录
changelog: {
'2.1.0': [
'Added new semantic colors for success/warning/error states',
'Updated spacing scale for better consistency',
'Added new typography tokens for display text'
],
'2.0.0': [
'Breaking: Renamed primary color tokens',
'Added dark theme support',
'Restructured spacing system'
]
},
// 令牌定义
colors: {
// 使用语义化命名
semantic: {
background: {
primary: 'var(--color-background-primary)',
secondary: 'var(--color-background-secondary)',
tertiary: 'var(--color-background-tertiary)'
},
text: {
primary: 'var(--color-text-primary)',
secondary: 'var(--color-text-secondary)',
disabled: 'var(--color-text-disabled)'
}
}
}
};
// 主题切换实现
export const useTheme = () => {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
return { theme, setTheme };
};// 良好的组件API设计
interface ButtonProps {
// 必需属性
children: React.ReactNode;
// 可选属性,提供默认值
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
// 扩展原生属性
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
disabled?: boolean;
loading?: boolean;
// 高级定制
className?: string;
style?: React.CSSProperties;
// 无障碍支持
'aria-label'?: string;
'aria-describedby'?: string;
}
// 组件实现示例
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({
children,
variant = 'primary',
size = 'md',
loading = false,
disabled = false,
className,
onClick,
...props
}, ref) => {
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
if (loading || disabled) return;
onClick?.(event);
};
return (
<button
ref={ref}
className={cn(
buttonVariants({ variant, size }),
className
)}
disabled={disabled || loading}
onClick={handleClick}
{...props}
>
{loading && <LoadingSpinner size="sm" className="mr-2" />}
{children}
</button>
);
}
);// 高性能动画实现
const OptimizedAnimation: React.FC<{
children: React.ReactNode;
isVisible: boolean;
}> = ({ children, isVisible }) => {
return (
<motion.div
// 使用transform和opacity,避免触发layout
initial={{ opacity: 0, transform: 'translateY(20px)' }}
animate={{
opacity: isVisible ? 1 : 0,
transform: isVisible ? 'translateY(0px)' : 'translateY(20px)'
}}
transition={{
duration: 0.3,
ease: [0.4, 0, 0.2, 1] // 使用贝塞尔曲线
}}
// 优化渲染性能
style={{ willChange: isVisible ? 'transform, opacity' : 'auto' }}
>
{children}
</motion.div>
);
};
// 使用Intersection Observer优化滚动动画
const useInViewAnimation = (threshold = 0.1) => {
const [ref, inView] = useInView({
threshold,
triggerOnce: true, // 只触发一次,避免重复动画
rootMargin: '50px 0px' // 提前触发动画
});
return { ref, inView };
};// 使用React.memo优化组件渲染
const TaskItem = React.memo<TaskItemProps>(({ task, onUpdate }) => {
// 使用useCallback避免函数重新创建
const handleToggle = useCallback(() => {
onUpdate(task.id, { completed: !task.completed });
}, [task.id, task.completed, onUpdate]);
// 使用useMemo优化计算
const formattedDate = useMemo(() => {
return task.dueDate ? format(new Date(task.dueDate), 'MM/dd') : null;
}, [task.dueDate]);
return (
<div className="task-item">
<button onClick={handleToggle}>
{task.completed ? '✓' : '○'}
</button>
<span>{task.title}</span>
{formattedDate && <span>{formattedDate}</span>}
</div>
);
}, (prevProps, nextProps) => {
// 自定义比较函数
return (
prevProps.task.id === nextProps.task.id &&
prevProps.task.title === nextProps.task.title &&
prevProps.task.completed === nextProps.task.completed &&
prevProps.task.dueDate === nextProps.task.dueDate
);
});// 键盘导航Hook
const useKeyboardNavigation = (items: any[], onSelect: (item: any) => void) => {
const [focusedIndex, setFocusedIndex] = useState(-1);
const handleKeyDown = useCallback((event: KeyboardEvent) => {
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
setFocusedIndex(prev => Math.min(prev + 1, items.length - 1));
break;
case 'ArrowUp':
event.preventDefault();
setFocusedIndex(prev => Math.max(prev - 1, 0));
break;
case 'Enter':
case ' ':
event.preventDefault();
if (focusedIndex >= 0) {
onSelect(items[focusedIndex]);
}
break;
case 'Escape':
setFocusedIndex(-1);
break;
}
}, [items, focusedIndex, onSelect]);
useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [handleKeyDown]);
return { focusedIndex, setFocusedIndex };
};// 无障碍友好的组件实现
const AccessibleTaskList: React.FC<{
tasks: Task[];
onTaskToggle: (id: string) => void;
}> = ({ tasks, onTaskToggle }) => {
const completedCount = tasks.filter(task => task.completed).length;
return (
<div role="region" aria-labelledby="task-list-heading">
<h2 id="task-list-heading">
任务列表 ({completedCount}/{tasks.length} 已完成)
</h2>
<ul role="list" aria-label="任务列表">
{tasks.map((task, index) => (
<li key={task.id} role="listitem">
<button
onClick={() => onTaskToggle(task.id)}
aria-pressed={task.completed}
aria-describedby={`task-${task.id}-description`}
className="task-toggle"
>
<span aria-hidden="true">
{task.completed ? '✓' : '○'}
</span>
<span className="sr-only">
{task.completed ? '标记为未完成' : '标记为已完成'}
</span>
</button>
<span
id={`task-${task.id}-description`}
className={task.completed ? 'completed' : ''}
>
{task.title}
</span>
{task.dueDate && (
<time
dateTime={task.dueDate.toISOString()}
aria-label={`截止日期:${format(task.dueDate, 'yyyy年MM月dd日')}`}
>
{format(task.dueDate, 'MM/dd')}
</time>
)}
</li>
))}
</ul>
</div>
);
};"优秀的界面设计不仅要美观,更要实用、易用、包容。通过系统化的设计方法、精心的交互设计和无障碍的实现,我们可以创造出真正服务于用户的产品界面。记住,最好的设计是让用户感觉不到设计的存在。"