import React, { useState, useMemo } from 'react'; import { Grid, List, SortAsc, SortDesc } from 'lucide-react'; import { SearchInput } from './InteractiveInput'; import { InteractiveButton } from './InteractiveButton'; export interface CardGridItem { id: string; [key: string]: any; } export interface GridAction { key: string; label: string; icon?: React.ReactNode; onClick: (item: T) => void; variant?: 'primary' | 'secondary' | 'danger' | 'success' | 'ghost' | 'outline'; disabled?: (item: T) => boolean; } export interface SortOption { key: string; label: string; direction?: 'asc' | 'desc'; } export interface FilterOption { key: string; label: string; value: any; } interface CardGridProps { items: T[]; renderCard: (item: T, index: number) => React.ReactNode; loading?: boolean; searchable?: boolean; searchKeys?: (keyof T)[]; searchPlaceholder?: string; sortable?: boolean; sortOptions?: SortOption[]; filterable?: boolean; filterOptions?: FilterOption[]; viewModes?: ('grid' | 'list')[]; defaultViewMode?: 'grid' | 'list'; gridCols?: { sm?: number; md?: number; lg?: number; xl?: number; '2xl'?: number; }; gap?: number; emptyText?: string; emptyComponent?: React.ReactNode; className?: string; actions?: GridAction[]; selectedItems?: T[]; onSelectionChange?: (selectedItems: T[]) => void; bulkActions?: GridAction[]; } /** * 增强的卡片网格组件 * 支持搜索、排序、筛选、视图切换等功能 */ export function CardGrid({ items, renderCard, loading = false, searchable = true, searchKeys = [], searchPlaceholder = '搜索...', sortable = true, sortOptions = [], filterable = false, filterOptions = [], viewModes = ['grid', 'list'], defaultViewMode = 'grid', gridCols = { sm: 1, md: 2, lg: 3, xl: 4, '2xl': 5, }, gap = 6, emptyText = '暂无数据', emptyComponent, className = '', selectedItems = [], onSelectionChange, bulkActions = [], }: CardGridProps) { const [searchQuery, setSearchQuery] = useState(''); const [sortConfig, setSortConfig] = useState(null); const [activeFilters, setActiveFilters] = useState>({}); const [viewMode, setViewMode] = useState<'grid' | 'list'>(defaultViewMode); // 搜索过滤 const searchedItems = useMemo(() => { if (!searchQuery.trim()) return items; const searchLower = searchQuery.toLowerCase(); return items.filter(item => { if (searchKeys.length > 0) { return searchKeys.some(key => { const value = item[key]; return String(value).toLowerCase().includes(searchLower); }); } else { return Object.values(item).some(value => String(value).toLowerCase().includes(searchLower) ); } }); }, [items, searchQuery, searchKeys]); // 筛选 const filteredItems = useMemo(() => { return searchedItems.filter(item => { return Object.entries(activeFilters).every(([key, value]) => { if (value === null || value === undefined || value === '') return true; return item[key] === value; }); }); }, [searchedItems, activeFilters]); // 排序 const sortedItems = useMemo(() => { if (!sortConfig) return filteredItems; return [...filteredItems].sort((a, b) => { const aValue = a[sortConfig.key]; const bValue = b[sortConfig.key]; const direction = sortConfig.direction || 'asc'; if (aValue < bValue) { return direction === 'asc' ? -1 : 1; } if (aValue > bValue) { return direction === 'asc' ? 1 : -1; } return 0; }); }, [filteredItems, sortConfig]); // 网格列数类名 const getGridColsClass = () => { const classes = []; if (gridCols.sm) classes.push(`grid-cols-${gridCols.sm}`); if (gridCols.md) classes.push(`md:grid-cols-${gridCols.md}`); if (gridCols.lg) classes.push(`lg:grid-cols-${gridCols.lg}`); if (gridCols.xl) classes.push(`xl:grid-cols-${gridCols.xl}`); if (gridCols['2xl']) classes.push(`2xl:grid-cols-${gridCols['2xl']}`); return classes.join(' ') || 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'; }; // 处理排序 const handleSort = (option: SortOption) => { setSortConfig(current => { if (current?.key === option.key) { const newDirection = current.direction === 'asc' ? 'desc' : 'asc'; return { ...option, direction: newDirection }; } return { ...option, direction: option.direction || 'asc' }; }); }; // 处理筛选 const handleFilter = (key: string, value: any) => { setActiveFilters(current => ({ ...current, [key]: value, })); }; // 选择处理 const isItemSelected = (item: T): boolean => { return selectedItems.some(selected => selected.id === item.id); }; const handleItemSelection = (item: T, selected: boolean) => { if (!onSelectionChange) return; if (selected) { onSelectionChange([...selectedItems, item]); } else { onSelectionChange(selectedItems.filter(selected => selected.id !== item.id)); } }; const handleSelectAll = (selected: boolean) => { if (!onSelectionChange) return; if (selected) { onSelectionChange(sortedItems); } else { onSelectionChange([]); } }; const allSelected = sortedItems.length > 0 && sortedItems.every(item => isItemSelected(item)); const someSelected = selectedItems.length > 0 && !allSelected; return (
{/* 工具栏 */} {(searchable || sortable || filterable || viewModes.length > 1 || bulkActions.length > 0) && (
{/* 搜索 */} {searchable && (
)} {/* 排序 */} {sortable && sortOptions.length > 0 && (
排序:
{sortOptions.map(option => ( handleSort(option)} icon={ sortConfig?.key === option.key ? ( sortConfig.direction === 'asc' ? : ) : undefined } > {option.label} ))}
)} {/* 筛选 */} {filterable && filterOptions.length > 0 && (
筛选:
{filterOptions.map(option => ( handleFilter(option.key, activeFilters[option.key] === option.value ? null : option.value )} > {option.label} ))}
)}
{/* 批量操作 */} {bulkActions.length > 0 && selectedItems.length > 0 && (
已选择 {selectedItems.length} 项 {bulkActions.map(action => ( action.onClick(selectedItems)} icon={action.icon} > {action.label} ))}
)} {/* 全选 */} {onSelectionChange && sortedItems.length > 0 && ( )} {/* 视图切换 */} {viewModes.length > 1 && (
{viewModes.map(mode => ( ))}
)}
)} {/* 内容区域 */} {loading ? (
加载中...
) : sortedItems.length === 0 ? (
{emptyComponent || (

{emptyText}

)}
) : (
{sortedItems.map((item, index) => (
{/* 选择框 */} {onSelectionChange && (
handleItemSelection(item, e.target.checked)} className="rounded border-gray-300 text-primary-600 focus:ring-primary-500 bg-white shadow-sm" />
)} {/* 卡片内容 */} {renderCard(item, index)}
))}
)}
); }