"use client" import * as React from "react" import { ColumnDef, ColumnFiltersState, ColumnSizingState, flexRender, getCoreRowModel, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, SortingState, useReactTable, VisibilityState, } from "@tanstack/react-table" import { IconChevronDown, IconChevronLeft, IconChevronRight, IconChevronsLeft, IconChevronsRight, IconLayoutColumns, IconTrash, IconDownload, IconSearch, IconLoader2, } from "@tabler/icons-react" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import type { Vulnerability } from "@/types/vulnerability.types" import type { PaginationInfo } from "@/types/common.types" interface VulnerabilitiesDataTableProps { data: Vulnerability[] columns: ColumnDef[] searchPlaceholder?: string searchColumn?: string searchValue?: string onSearch?: (value: string) => void isSearching?: boolean pagination?: { pageIndex: number; pageSize: number } setPagination?: React.Dispatch> paginationInfo?: PaginationInfo onPaginationChange?: (pagination: { pageIndex: number; pageSize: number }) => void onBulkDelete?: () => void // 批量删除回调函数 onSelectionChange?: (selectedRows: Vulnerability[]) => void // 选中行变化回调 // 下载回调函数 onDownloadAll?: () => void // 下载所有漏洞 onDownloadSelected?: () => void // 下载选中的漏洞 } export function VulnerabilitiesDataTable({ data = [], columns, searchPlaceholder = "搜索漏洞类型...", searchColumn = "title", searchValue, onSearch, isSearching = false, pagination, setPagination, paginationInfo, onPaginationChange, onBulkDelete, onSelectionChange, onDownloadAll, onDownloadSelected, }: VulnerabilitiesDataTableProps) { const [sorting, setSorting] = React.useState([]) const [columnFilters, setColumnFilters] = React.useState([]) const [columnVisibility, setColumnVisibility] = React.useState({}) const [columnSizing, setColumnSizing] = React.useState({}) const [rowSelection, setRowSelection] = React.useState({}) const [internalPagination, setInternalPagination] = React.useState({ pageIndex: 0, pageSize: 10, }) // 本地搜索输入状态(只在回车或点击按钮时触发搜索) const [localSearchValue, setLocalSearchValue] = React.useState(searchValue ?? "") React.useEffect(() => { setLocalSearchValue(searchValue ?? "") }, [searchValue]) const handleSearchSubmit = () => { if (onSearch) { onSearch(localSearchValue) } else { table.getColumn(searchColumn)?.setFilterValue(localSearchValue) } } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { handleSearchSubmit() } } const useServerPagination = !!paginationInfo && !!pagination && !!setPagination const tablePagination = useServerPagination ? pagination : internalPagination const setTablePagination = useServerPagination ? setPagination : setInternalPagination const table = useReactTable({ data, columns, state: { sorting, columnVisibility, rowSelection, columnFilters, columnSizing, pagination: tablePagination, }, getRowId: (row) => row.id.toString(), enableRowSelection: true, enableColumnResizing: true, columnResizeMode: 'onChange', onRowSelectionChange: setRowSelection, onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, onColumnVisibilityChange: setColumnVisibility, onColumnSizingChange: setColumnSizing, onPaginationChange: (updater) => { const nextPagination = typeof updater === "function" ? updater(tablePagination) : updater setTablePagination?.(nextPagination as { pageIndex: number; pageSize: number }) onPaginationChange?.(nextPagination) }, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFacetedRowModel: getFacetedRowModel(), getFacetedUniqueValues: getFacetedUniqueValues(), manualPagination: useServerPagination, pageCount: useServerPagination ? paginationInfo?.totalPages ?? -1 : Math.ceil(data.length / tablePagination.pageSize) || 1, }) const totalItems = useServerPagination ? paginationInfo?.total ?? data.length : table.getFilteredRowModel().rows.length // 处理选中行变化 React.useEffect(() => { if (onSelectionChange) { const selectedRows = table.getFilteredSelectedRowModel().rows.map(row => row.original) onSelectionChange(selectedRows) } }, [rowSelection, onSelectionChange, table]) return (
{/* 工具栏 */}
{/* 搜索框 */}
setLocalSearchValue(e.target.value)} onKeyDown={handleKeyDown} className="h-8 max-w-sm" />
{/* 右侧操作按钮 */}
{/* 列显示控制 */} {table .getAllColumns() .filter( (column) => typeof column.accessorFn !== "undefined" && column.getCanHide() ) .map((column) => ( column.toggleVisibility(!!value)} > {column.id === "select" && "Select"} {column.id === "title" && "Title"} {column.id === "severity" && "Severity"} {column.id === "status" && "Status"} {column.id === "url" && "URL"} {column.id === "createdAt" && "Created At"} {column.id === "actions" && "Actions"} {!["select", "title", "severity", "status", "url", "createdAt", "actions"].includes(column.id) && column.id} ))} {/* 下载按钮 */} {(onDownloadAll || onDownloadSelected) && ( Download Options {onDownloadAll && ( Download All Vulnerabilities )} {onDownloadSelected && ( Download Selected Vulnerabilities )} )} {/* 批量删除按钮 */} {onBulkDelete && ( )}
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { return ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext() )} {header.column.getCanResize() && (
header.column.resetSize()} className="absolute -right-2.5 top-0 h-full w-8 cursor-col-resize select-none touch-none bg-transparent flex justify-center" >
)} ) })} ))} {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( {flexRender( cell.column.columnDef.cell, cell.getContext() )} ))} )) ) : ( 暂无数据 )}
{/* 分页控制 */}
{/* 选中行信息 */}
{table.getFilteredSelectedRowModel().rows.length} of{" "} {paginationInfo ? paginationInfo.total : table.getFilteredRowModel().rows.length} row(s) selected
{/* 分页控制器 */}
{/* 每页显示数量选择 */}
{/* 页码信息 */}
Page {table.getState().pagination.pageIndex + 1} of{" "} {table.getPageCount()}
{/* 分页按钮 */}
) }