Skip to content

JavaScript智能待办事项管理器界面设计与交互2024:前端开发者完整UI/UX实现与用户体验优化指南

📊 SEO元描述:2024年最新JavaScript智能待办事项管理器界面设计与交互教程,详解响应式UI设计、用户交互优化、动画效果实现。包含完整的React+Tailwind CSS实现,适合前端开发者快速掌握现代UI开发技术。

核心关键词:JavaScript界面设计2024、React UI开发、用户交互设计、响应式界面、前端动画效果、用户体验优化

长尾关键词:JavaScript界面怎么设计、React组件开发、前端交互效果、响应式设计实现、用户体验优化方法


📚 JavaScript界面设计与交互学习目标与核心收获

通过本节JavaScript界面设计与交互教程,你将系统性掌握:

  • 响应式UI设计:掌握现代响应式界面设计原则和实现技术
  • 组件化开发:学会构建可复用、可维护的React组件系统
  • 用户交互优化:理解用户体验设计原则和交互效果实现
  • 动画效果实现:掌握CSS动画和JavaScript动画的设计和实现
  • 无障碍设计:学会实现符合WCAG标准的无障碍用户界面
  • 性能优化技巧:掌握前端界面性能优化的方法和最佳实践

🎯 适合人群

  • 前端开发工程师的UI/UX开发技能提升和实战经验积累
  • React开发者的组件设计和用户体验优化能力培养
  • UI/UX设计师的前端实现技术学习和协作能力提升
  • 全栈开发者的前端界面开发技术栈完善

🌟 现代化界面设计系统架构

现代化界面设计系统需要考虑用户体验、可访问性、性能和可维护性等多个维度。我们将构建一个基于设计系统的组件库,实现一致性和可扩展性的完美平衡,也是现代前端UI开发的最佳实践案例。

设计系统核心要素

  • 🎯 设计令牌:颜色、字体、间距、阴影等基础设计元素的标准化
  • 🔧 组件库:可复用的UI组件和交互模式的系统化管理
  • 💡 布局系统:响应式网格系统和灵活的布局解决方案
  • 📚 交互规范:统一的用户交互行为和反馈机制
  • 🚀 主题系统:支持多主题切换和个性化定制的主题管理

💡 设计理念:采用原子设计方法论,从最小的设计元素开始,逐步构建复杂的界面组件,确保设计的一致性和系统性。

设计令牌系统

typescript
// 🎉 设计令牌系统定义

// 颜色系统
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'
    }
  }
};

基础组件库实现

tsx
// 🎉 基础组件库实现

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(' ');
}

基础组件库的核心特点

  • 🎯 一致性设计:基于设计令牌系统确保视觉一致性
  • 🎯 可组合性:组件可以灵活组合构建复杂界面
  • 🎯 可访问性:内置无障碍支持和键盘导航

💼 组件设计价值:通过系统化的组件设计,提高开发效率,确保界面一致性,同时为用户提供优秀的交互体验。

任务界面组件实现

tsx
// 🎉 任务管理界面组件实现

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;

交互动画效果实现

tsx
// 🎉 交互动画效果实现

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界面设计与交互学习总结与下一步规划

✅ 本节核心收获回顾

通过本节JavaScript界面设计与交互的学习,你已经掌握:

  1. 设计系统构建:学会了构建完整的设计令牌系统和组件库架构
  2. 响应式UI设计:掌握了现代响应式界面设计原则和实现技术
  3. 组件化开发:理解了可复用、可维护的React组件设计和开发方法
  4. 交互动画实现:学会了使用Framer Motion等库实现流畅的交互动画
  5. 用户体验优化:掌握了提升用户体验的界面设计技巧和最佳实践

🎯 界面设计技术下一步

  1. 无障碍设计实践:深入学习WCAG标准和无障碍设计的实现方法
  2. 性能优化进阶:掌握界面渲染性能优化和动画性能调优技术
  3. 设计系统管理:学习大型设计系统的维护和版本管理方法
  4. 跨平台适配:探索响应式设计在不同设备和平台上的适配策略

🔗 相关学习资源

  • 设计系统案例:学习Material Design、Ant Design等成熟设计系统
  • 动画库深入:掌握Framer Motion、React Spring等动画库的高级用法
  • CSS进阶技术:学习CSS Grid、Flexbox、CSS变量等现代CSS技术
  • 用户体验设计:了解UX设计原理和用户研究方法

💪 实践建议

  1. 完善组件库:继续完善和扩展组件库,添加更多实用组件
  2. 用户测试:进行用户测试收集反馈,持续优化界面设计
  3. 性能监控:建立界面性能监控机制,持续优化用户体验
  4. 设计文档:编写完整的设计系统文档和使用指南

🔍 常见问题FAQ

Q1: 如何平衡动画效果和性能表现?

A: 优先使用CSS动画和transform属性,避免频繁操作DOM,合理设置动画时长(通常200-500ms),使用will-change属性优化动画性能,在低性能设备上提供简化的动画或禁用动画选项。

Q2: 响应式设计如何处理复杂的布局需求?

A: 采用移动优先的设计策略,使用CSS Grid和Flexbox构建灵活的布局系统,设计合理的断点策略,考虑内容优先级在不同屏幕尺寸下的展示方式,使用容器查询等新技术处理组件级响应式。

Q3: 组件库如何保证一致性和可维护性?

A: 建立完整的设计令牌系统,使用TypeScript确保类型安全,编写详细的组件文档和使用示例,建立组件测试和视觉回归测试,定期进行代码审查和重构。

Q4: 如何实现良好的无障碍体验?

A: 遵循WCAG 2.1 AA标准,确保键盘导航支持,提供合适的ARIA标签,保证足够的颜色对比度,支持屏幕阅读器,提供替代文本和语义化的HTML结构。

Q5: 大型应用的界面性能如何优化?

A: 实现组件懒加载和代码分割,使用虚拟滚动处理大列表,优化图片加载和缓存策略,减少不必要的重新渲染,使用Web Workers处理复杂计算,实施渐进式加载策略。


🛠️ 界面设计与交互最佳实践指南

设计系统管理

1. 设计令牌版本控制

typescript
// 设计令牌版本管理
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 };
};

2. 组件API设计原则

typescript
// 良好的组件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>
    );
  }
);

性能优化策略

1. 动画性能优化

typescript
// 高性能动画实现
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 };
};

2. 组件渲染优化

typescript
// 使用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
  );
});

无障碍设计实践

1. 键盘导航支持

typescript
// 键盘导航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 };
};

2. ARIA标签和语义化

tsx
// 无障碍友好的组件实现
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>
  );
};

"优秀的界面设计不仅要美观,更要实用、易用、包容。通过系统化的设计方法、精心的交互设计和无障碍的实现,我们可以创造出真正服务于用户的产品界面。记住,最好的设计是让用户感觉不到设计的存在。"