Files
xingrin/frontend/components/organization/targets/targets-data-table.tsx
2025-12-22 12:06:38 +08:00

408 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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,
IconPlus,
IconTrash,
IconSearch,
IconLoader2,
} from "@tabler/icons-react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
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 { Target } from "@/types/target.types"
import type { PaginationInfo } from "@/types/common.types"
interface TargetsDataTableProps {
data: Target[]
columns: ColumnDef<Target>[]
onAddNew?: () => void
onAddHover?: () => void
onBulkDelete?: () => void
onSelectionChange?: (selectedRows: Target[]) => void
searchPlaceholder?: string
searchColumn?: string
searchValue?: string
onSearch?: (value: string) => void
isSearching?: boolean
addButtonText?: string
pagination?: { pageIndex: number; pageSize: number }
setPagination?: React.Dispatch<React.SetStateAction<{ pageIndex: number; pageSize: number }>>
paginationInfo?: PaginationInfo
onPaginationChange?: (pagination: { pageIndex: number; pageSize: number }) => void
}
/**
* 目标数据表格组件
* 专门用于显示和管理目标数据的表格
* 包含搜索、分页、列显示控制等功能
*/
export function TargetsDataTable({
data = [],
columns,
onAddNew,
onAddHover,
onBulkDelete,
onSelectionChange,
searchPlaceholder = "搜索目标名称...",
searchColumn = "name",
searchValue,
onSearch,
isSearching = false,
addButtonText = "添加目标",
pagination: externalPagination,
setPagination: setExternalPagination,
paginationInfo,
onPaginationChange,
}: TargetsDataTableProps) {
// 搜索本地状态
const [localSearchValue, setLocalSearchValue] = React.useState(searchValue || "")
React.useEffect(() => {
setLocalSearchValue(searchValue || "")
}, [searchValue])
const handleSearchSubmit = () => {
if (onSearch) {
onSearch(localSearchValue)
}
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
handleSearchSubmit()
}
}
const [rowSelection, setRowSelection] = React.useState<Record<string, boolean>>({})
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
const [sorting, setSorting] = React.useState<SortingState>([])
const [columnSizing, setColumnSizing] = React.useState<ColumnSizingState>({})
const [internalPagination, setInternalPagination] = React.useState<{ pageIndex: number, pageSize: number }>({
pageIndex: 0,
pageSize: 10,
})
const pagination = externalPagination || internalPagination
const setPagination = setExternalPagination || setInternalPagination
const validData = React.useMemo(() => {
const filtered = (data || []).filter(item => item && typeof item.id !== 'undefined' && item.id !== null)
return filtered
}, [data])
const table = useReactTable({
data: validData,
columns,
state: {
sorting,
columnVisibility,
rowSelection,
columnFilters,
pagination,
columnSizing,
},
enableColumnResizing: true,
columnResizeMode: 'onChange',
onColumnSizingChange: setColumnSizing,
pageCount: paginationInfo?.totalPages ?? -1,
manualPagination: !!paginationInfo,
getRowId: (row) => row.id.toString(),
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onPaginationChange: (updater) => {
const newPagination = typeof updater === 'function' ? updater(pagination) : updater
setPagination(newPagination)
if (onPaginationChange) {
onPaginationChange(newPagination)
}
},
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
})
React.useEffect(() => {
if (onSelectionChange) {
const selectedRows = table.getFilteredSelectedRowModel().rows.map(row => row.original)
onSelectionChange(selectedRows)
}
}, [rowSelection, onSelectionChange, table])
return (
<div className="w-full space-y-4">
{/* 工具栏 */}
<div className="flex items-center justify-between">
{/* 搜索框 */}
<div className="flex items-center space-x-2">
<Input
placeholder={searchPlaceholder}
value={localSearchValue}
onChange={(e) => setLocalSearchValue(e.target.value)}
onKeyDown={handleKeyDown}
className="h-8 max-w-sm"
/>
<Button variant="outline" size="sm" onClick={handleSearchSubmit} disabled={isSearching}>
{isSearching ? (
<IconLoader2 className="h-4 w-4 animate-spin" />
) : (
<IconSearch className="h-4 w-4" />
)}
</Button>
</div>
{/* 右侧操作按钮 */}
<div className="flex items-center space-x-2">
{/* 列显示控制 */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<IconLayoutColumns />
<IconChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter(
(column) =>
typeof column.accessorFn !== "undefined" && column.getCanHide()
)
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{column.id === "id" && "ID"}
{column.id === "name" && "目标名称"}
{column.id === "type" && "类型"}
{column.id === "createdAt" && "创建时间"}
{column.id === "updatedAt" && "更新时间"}
{!["id", "name", "type", "createdAt", "updatedAt"].includes(column.id) && column.id}
</DropdownMenuCheckboxItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
{/* 添加新目标按钮 */}
{onAddNew && (
<Button onClick={onAddNew} onMouseEnter={onAddHover} size="sm">
<IconPlus />
{addButtonText}
</Button>
)}
</div>
</div>
{/* 表格容器 */}
<div className="rounded-md border overflow-x-auto">
<Table style={{ minWidth: table.getCenterTotalSize() }}>
{/* 表头 */}
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead
key={header.id}
colSpan={header.colSpan}
style={{ width: header.getSize() }}
className="relative group"
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
{header.column.getCanResize() && (
<div
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
onDoubleClick={() => header.column.resetSize()}
className="absolute -right-2.5 top-0 h-full w-5 cursor-col-resize select-none touch-none bg-transparent flex justify-center"
>
<div className="w-1.5 h-full bg-transparent group-hover:bg-border" />
</div>
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
{/* 表体 */}
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="group"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} style={{ width: cell.column.getSize() }}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* 分页控制 */}
<div className="flex items-center justify-between px-2">
{/* 选中行信息 */}
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} {" "}
{paginationInfo ? paginationInfo.total : table.getFilteredRowModel().rows.length}
</div>
{/* 分页控制器 */}
<div className="flex items-center space-x-6 lg:space-x-8">
{/* 每页显示数量选择 */}
<div className="flex items-center space-x-2">
<Label htmlFor="rows-per-page" className="text-sm font-medium">
</Label>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value))
}}
>
<SelectTrigger className="h-8 w-[90px]" id="rows-per-page">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 50, 100, 200, 500, 1000].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 页码信息 */}
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
{table.getState().pagination.pageIndex + 1} {" "}
{table.getPageCount()}
</div>
{/* 分页按钮 */}
<div className="flex items-center space-x-2">
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only"></span>
<IconChevronsLeft />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only"></span>
<IconChevronLeft />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only"></span>
<IconChevronRight />
</Button>
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="sr-only"></span>
<IconChevronsRight />
</Button>
</div>
</div>
</div>
</div>
)
}