"use client" import React from "react" import type { ReactNode } from "react" import { useState, useEffect, useMemo, useCallback } from "react" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" import { Pagination } from "@/components/ui/pagination" import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" import { Search, RefreshCw, ChevronDown, ChevronUp, MoreHorizontal } from "lucide-react" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { cn } from "@/lib/utils" import { Skeleton } from "@/components/ui/skeleton" // 列定义接口 export interface Column { id: string header: string | ReactNode accessorKey?: keyof T cell?: (item: T) => ReactNode sortable?: boolean className?: string } // DataTable 属性接口 export interface DataTableProps { data: T[] columns: Column[] pageSize?: number loading?: boolean title?: string description?: string emptyMessage?: string withCard?: boolean showSearch?: boolean showRefresh?: boolean showSelection?: boolean onRowClick?: (item: T) => void onSelectionChange?: (selectedItems: T[]) => void onRefresh?: () => void onSearch?: (query: string) => void onSort?: (columnId: string, direction: "asc" | "desc") => void rowActions?: { label: string icon?: ReactNode onClick: (item: T) => void className?: string }[] batchActions?: { label: string icon?: ReactNode onClick: (selectedItems: T[]) => void className?: string }[] className?: string } /** * 经过性能优化的数据表格组件 * - 使用 React.memo 避免不必要的重渲染 * - 使用 useMemo 缓存计算结果 * - 使用 useCallback 稳定化事件处理器 */ function DataTableComponent({ data, columns, pageSize = 10, loading = false, title, description, emptyMessage = "暂无数据", withCard = true, showSearch = true, showRefresh = true, showSelection = false, onRowClick, onSelectionChange, onRefresh, onSearch, onSort, rowActions, batchActions, className, }: DataTableProps) { const [searchQuery, setSearchQuery] = useState("") const [currentPage, setCurrentPage] = useState(1) const [selectedItems, setSelectedItems] = useState([]) const [sortConfig, setSortConfig] = useState<{ columnId: string; direction: "asc" | "desc" } | null>(null) // 当外部数据变化时,重置分页和选择 useEffect(() => { setCurrentPage(1) setSelectedItems([]) }, [data]) // 使用 useMemo 缓存过滤和排序后的数据 const filteredData = useMemo(() => { let filtered = [...data] if (searchQuery && !onSearch) { filtered = data.filter((item) => columns.some((col) => { if (!col.accessorKey) return false const value = item[col.accessorKey] return String(value).toLowerCase().includes(searchQuery.toLowerCase()) }), ) } if (sortConfig && !onSort) { const { columnId, direction } = sortConfig const column = columns.find((c) => c.id === columnId) if (column?.accessorKey) { filtered.sort((a, b) => { const valA = a[column.accessorKey!] const valB = b[column.accessorKey!] if (valA < valB) return direction === "asc" ? -1 : 1 if (valA > valB) return direction === "asc" ? 1 : -1 return 0 }) } } return filtered }, [data, searchQuery, sortConfig, columns, onSearch, onSort]) // 使用 useMemo 缓存当前页的数据 const currentPageItems = useMemo(() => { const startIndex = (currentPage - 1) * pageSize return filteredData.slice(startIndex, startIndex + pageSize) }, [filteredData, currentPage, pageSize]) const totalPages = Math.ceil(filteredData.length / pageSize) const isAllCurrentPageSelected = currentPageItems.length > 0 && currentPageItems.every((item) => selectedItems.some((s) => s.id === item.id)) // 搜索处理 const handleSearch = useCallback( (query: string) => { setSearchQuery(query) setCurrentPage(1) if (onSearch) { onSearch(query) } }, [onSearch], ) // 排序处理 const handleSort = useCallback( (columnId: string) => { const newDirection = sortConfig?.columnId === columnId && sortConfig.direction === "asc" ? "desc" : "asc" setSortConfig({ columnId, direction: newDirection }) if (onSort) { onSort(columnId, newDirection) } }, [sortConfig, onSort], ) // 刷新处理 const handleRefresh = useCallback(() => { setSearchQuery("") setCurrentPage(1) setSortConfig(null) setSelectedItems([]) onRefresh?.() }, [onRefresh]) // 全选/取消全选 const handleSelectAll = useCallback( (checked: boolean) => { const newSelectedItems = checked ? currentPageItems : [] setSelectedItems(newSelectedItems) onSelectionChange?.(newSelectedItems) }, [currentPageItems, onSelectionChange], ) // 单行选择 const handleSelectItem = useCallback( (item: T, checked: boolean) => { const newSelectedItems = checked ? [...selectedItems, item] : selectedItems.filter((selected) => selected.id !== item.id) setSelectedItems(newSelectedItems) onSelectionChange?.(newSelectedItems) }, [selectedItems, onSelectionChange], ) const TableContent = (
{/* 工具栏 */}
{showSearch && (
handleSearch(e.target.value)} />
)} {showRefresh && ( )}
{batchActions && selectedItems.length > 0 && (
已选 {selectedItems.length} 项 {batchActions.map((action, i) => ( action.onClick(selectedItems)} className={action.className}> {action.icon} {action.label} ))}
)}
{/* 表格 */}
{showSelection && ( )} {columns.map((column) => ( column.sortable && handleSort(column.id)} >
{column.header} {column.sortable && sortConfig?.columnId === column.id && (sortConfig.direction === "asc" ? ( ) : ( ))}
))} {rowActions && 操作}
{loading ? ( Array.from({ length: pageSize }).map((_, i) => ( )) ) : currentPageItems.length === 0 ? ( {emptyMessage} ) : ( currentPageItems.map((item) => ( onRowClick?.(item)} > {showSelection && ( e.stopPropagation()}> s.id === item.id)} onCheckedChange={(checked) => handleSelectItem(item, !!checked)} /> )} {columns.map((column) => ( {column.cell ? column.cell(item) : column.accessorKey ? String(item[column.accessorKey] ?? "") : null} ))} {rowActions && ( e.stopPropagation()}> {rowActions.map((action, i) => ( action.onClick(item)} className={action.className}> {action.icon} {action.label} ))} )} )) )}
{/* 分页 */} {totalPages > 1 && (
共 {filteredData.length} 条记录
)}
) if (withCard) { return ( {(title || description) && ( {title && {title}} {description && {description}} )} ) } return
{TableContent}
} export const DataTable = React.memo(DataTableComponent) as typeof DataTableComponent