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

334 lines
11 KiB
TypeScript

"use client"
import * as React from "react"
import {
ColumnDef,
ColumnFiltersState,
ColumnSizingState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
SortingState,
useReactTable,
VisibilityState,
} from "@tanstack/react-table"
import {
IconChevronDown,
IconChevronLeft,
IconChevronRight,
IconChevronsLeft,
IconChevronsRight,
IconLayoutColumns,
IconPlus,
IconTrash,
} 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"
interface CommandsDataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
data: TData[]
onBulkDelete?: (selectedIds: number[]) => void
onAdd?: () => void
}
export function CommandsDataTable<TData, TValue>({
columns,
data,
onBulkDelete,
onAdd,
}: CommandsDataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([])
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})
const [rowSelection, setRowSelection] = React.useState({})
const [columnSizing, setColumnSizing] = React.useState<ColumnSizingState>({})
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onColumnSizingChange: setColumnSizing,
enableColumnResizing: true,
columnResizeMode: 'onChange',
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
columnSizing,
},
})
// 获取选中的行
const selectedRows = table.getFilteredSelectedRowModel().rows
// 处理批量删除
const handleBulkDelete = () => {
if (onBulkDelete && selectedRows.length > 0) {
const selectedIds = selectedRows.map((row) => (row.original as { id: number }).id)
onBulkDelete(selectedIds)
}
}
return (
<div className="space-y-4">
{/* 工具栏 */}
<div className="flex items-center justify-between">
{/* 搜索框 */}
<div className="flex items-center space-x-2">
<Input
placeholder="搜索命令名称..."
value={(table.getColumn("displayName")?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn("displayName")?.setFilterValue(event.target.value)
}
className="max-w-sm"
/>
</div>
{/* 右侧操作按钮 */}
<div className="flex items-center space-x-2">
{/* 列显示控制 */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<IconLayoutColumns />
Columns
<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 === "displayName" && "名称"}
{column.id === "tool" && "所属工具"}
{column.id === "commandTemplate" && "命令模板"}
{column.id === "description" && "描述"}
{column.id === "updatedAt" && "更新时间"}
{!["id", "displayName", "tool", "commandTemplate", "description", "updatedAt"].includes(column.id) && column.id}
</DropdownMenuCheckboxItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
{/* 批量删除按钮 */}
{onBulkDelete && (
<Button
onClick={handleBulkDelete}
size="sm"
variant="outline"
disabled={table.getFilteredSelectedRowModel().rows.length === 0}
className={
table.getFilteredSelectedRowModel().rows.length === 0
? "text-muted-foreground"
: "text-destructive hover:text-destructive hover:bg-destructive/10"
}
>
<IconTrash />
Delete
</Button>
)}
{/* 添加命令按钮 */}
{onAdd && (
<Button onClick={onAdd} size="sm">
<IconPlus />
Add
</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}
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"}
>
{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} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected
</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">
Rows per page
</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">
Page {table.getState().pagination.pageIndex + 1} of{" "}
{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">Go to first page</span>
<IconChevronsLeft />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<IconChevronLeft />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</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">Go to last page</span>
<IconChevronsRight />
</Button>
</div>
</div>
</div>
</div>
)
}