mosaicmap/app/admin/permissions/permission-table.tsx
2025-08-17 20:28:13 +08:00

773 lines
31 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 { useQuery, useMutation, gql } from '@apollo/client';
import {
ColumnDef,
ColumnFiltersState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
SortingState,
useReactTable,
VisibilityState,
} from "@tanstack/react-table"
import { z } from "zod"
import {
IconKey,
IconPlus,
IconDotsVertical,
IconPencil,
IconTrash,
IconShield,
IconChevronLeft,
IconChevronRight,
IconChevronsLeft,
IconChevronsRight,
} from "@tabler/icons-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { toast } from "sonner"
const permissionSchema = z.object({
id: z.string(),
name: z.string(),
code: z.string(),
description: z.string().optional(),
module: z.string(),
action: z.string(),
resource: z.string(),
level: z.number(),
isActive: z.boolean(),
roleCount: z.number(),
createdAt: z.string(),
updatedAt: z.string(),
})
type Permission = z.infer<typeof permissionSchema>
const GET_PERMISSIONS = gql`
query GetPermissions($pagination: PaginationInput) {
permissions(pagination: $pagination) {
items {
id
name
code
description
module
action
resource
level
isActive
roleCount
createdAt
updatedAt
}
total
page
perPage
totalPages
}
}
`
const CREATE_PERMISSION = gql`
mutation CreatePermission($input: CreatePermissionInput!) {
createPermission(input: $input) {
id
name
code
module
action
resource
isActive
}
}
`
const UPDATE_PERMISSION = gql`
mutation UpdatePermission($id: UUID!, $input: UpdatePermissionInput!) {
updatePermission(id: $id, input: $input) {
id
name
code
isActive
updatedAt
}
}
`
const DELETE_PERMISSION = gql`
mutation DeletePermission($id: UUID!) {
deletePermission(id: $id)
}
`
const getActionColor = (action: string) => {
switch (action.toLowerCase()) {
case 'create':
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300'
case 'read':
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300'
case 'update':
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300'
case 'delete':
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300'
default:
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300'
}
}
const columns: ColumnDef<Permission>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "name",
header: "权限名称",
cell: ({ row }) => {
const permission = row.original
return (
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10">
<IconKey className="h-4 w-4 text-primary" />
</div>
<div>
<div className="font-medium">{permission.name}</div>
<div className="text-sm text-muted-foreground">{permission.code}</div>
</div>
</div>
)
},
},
{
accessorKey: "description",
header: "描述",
cell: ({ row }) => {
const description = row.getValue("description") as string
return (
<div className="max-w-xs">
<span className="text-sm text-muted-foreground line-clamp-2">
{description || "无描述"}
</span>
</div>
)
},
},
{
accessorKey: "module",
header: "模块",
cell: ({ row }) => {
const module = row.getValue("module") as string
return (
<Badge variant="outline" className="font-mono">
{module}
</Badge>
)
},
},
{
accessorKey: "action",
header: "操作",
cell: ({ row }) => {
const action = row.getValue("action") as string
return (
<Badge className={`${getActionColor(action)} border-0`}>
{action}
</Badge>
)
},
},
{
accessorKey: "resource",
header: "资源",
cell: ({ row }) => {
const resource = row.getValue("resource") as string
return (
<Badge variant="secondary" className="font-mono">
{resource}
</Badge>
)
},
},
{
accessorKey: "level",
header: "级别",
cell: ({ row }) => {
const level = row.getValue("level") as number
const getLevelColor = (level: number) => {
if (level >= 90) return "text-red-600"
if (level >= 70) return "text-orange-600"
if (level >= 50) return "text-yellow-600"
return "text-green-600"
}
return (
<span className={`font-medium ${getLevelColor(level)}`}>
{level}
</span>
)
},
},
{
accessorKey: "roleCount",
header: "角色数",
cell: ({ row }) => {
const count = row.getValue("roleCount") as number
return (
<div className="text-center">
<Badge variant="outline">{count}</Badge>
</div>
)
},
},
{
accessorKey: "isActive",
header: "状态",
cell: ({ row }) => {
const isActive = row.getValue("isActive") as boolean
return (
<Badge variant={isActive ? "default" : "secondary"}>
{isActive ? "启用" : "禁用"}
</Badge>
)
},
},
{
id: "actions",
cell: ({ row, table }) => {
const permission = row.original
const updatePermission = (table.options.meta as any)?.updatePermission
const deletePermission = (table.options.meta as any)?.deletePermission
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<IconDotsVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem>
<IconPencil className="mr-2 h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuItem>
<IconShield className="mr-2 h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
updatePermission({
variables: {
id: permission.id,
input: { isActive: !permission.isActive }
}
})
}}
>
{permission.isActive ? "禁用" : "启用"}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-red-600"
onClick={() => {
if (confirm('确定要删除这个权限吗?此操作不可撤销。')) {
deletePermission({
variables: { id: permission.id }
})
}
}}
>
<IconTrash className="mr-2 h-4 w-4" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
},
},
]
function CreatePermissionDialog({ onSuccess }: { onSuccess: () => void }) {
const [open, setOpen] = React.useState(false)
const [loading, setLoading] = React.useState(false)
const [createPermission] = useMutation(CREATE_PERMISSION)
const [formData, setFormData] = React.useState({
name: "",
code: "",
description: "",
module: "",
action: "",
resource: "",
level: 50,
isActive: true,
})
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!formData.name || !formData.code || !formData.module || !formData.action || !formData.resource) {
toast.error("请填写所有必需字段")
return
}
setLoading(true)
try {
await createPermission({
variables: { input: formData }
})
toast.success("权限创建成功")
setOpen(false)
setFormData({
name: "",
code: "",
description: "",
module: "",
action: "",
resource: "",
level: 50,
isActive: true,
})
onSuccess()
} catch (error) {
toast.error("创建失败")
} finally {
setLoading(false)
}
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>
<IconPlus className="mr-2 h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
使
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name"> *</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
placeholder="查看用户"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="code"> *</Label>
<Input
id="code"
value={formData.code}
onChange={(e) => setFormData(prev => ({ ...prev, code: e.target.value.toUpperCase() }))}
placeholder="USER_READ"
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="description"></Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
placeholder="描述这个权限的作用和范围"
rows={2}
/>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="module"> *</Label>
<Select
value={formData.module}
onValueChange={(value) => setFormData(prev => ({ ...prev, module: value }))}
>
<SelectTrigger>
<SelectValue placeholder="选择模块" />
</SelectTrigger>
<SelectContent>
<SelectItem value="user"></SelectItem>
<SelectItem value="role"></SelectItem>
<SelectItem value="permission"></SelectItem>
<SelectItem value="content"></SelectItem>
<SelectItem value="analytics"></SelectItem>
<SelectItem value="system"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="action"> *</Label>
<Select
value={formData.action}
onValueChange={(value) => setFormData(prev => ({ ...prev, action: value }))}
>
<SelectTrigger>
<SelectValue placeholder="选择操作" />
</SelectTrigger>
<SelectContent>
<SelectItem value="create"></SelectItem>
<SelectItem value="read"></SelectItem>
<SelectItem value="update"></SelectItem>
<SelectItem value="delete"></SelectItem>
<SelectItem value="manage"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="resource"> *</Label>
<Input
id="resource"
value={formData.resource}
onChange={(e) => setFormData(prev => ({ ...prev, resource: e.target.value }))}
placeholder="users"
required
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="level"></Label>
<Select
value={formData.level.toString()}
onValueChange={(value) => setFormData(prev => ({ ...prev, level: parseInt(value) }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">10 - </SelectItem>
<SelectItem value="30">30 - </SelectItem>
<SelectItem value="50">50 - </SelectItem>
<SelectItem value="70">70 - </SelectItem>
<SelectItem value="90">90 - </SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<div className="flex items-center space-x-2 pt-2">
<Checkbox
id="isActive"
checked={formData.isActive}
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, isActive: !!checked }))}
/>
<Label htmlFor="isActive" className="text-sm font-normal">
</Label>
</div>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
</Button>
<Button type="submit" disabled={loading}>
{loading ? "创建中..." : "创建权限"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
export function PermissionTable() {
const [sorting, setSorting] = React.useState<SortingState>([])
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})
const [rowSelection, setRowSelection] = React.useState({})
const [pagination, setPagination] = React.useState({
pageIndex: 0,
pageSize: 10,
})
const { data, loading, error, refetch } = useQuery(GET_PERMISSIONS, {
variables: {
pagination: {
page: pagination.pageIndex + 1,
perPage: pagination.pageSize,
}
},
fetchPolicy: 'cache-and-network'
})
const [updatePermission] = useMutation(UPDATE_PERMISSION, {
onCompleted: () => {
refetch()
toast.success("权限状态已更新")
}
})
const [deletePermission] = useMutation(DELETE_PERMISSION, {
onCompleted: () => {
refetch()
toast.success("权限已删除")
}
})
const permissions = data?.permissions?.items || []
const totalCount = data?.permissions?.total || 0
const table = useReactTable({
data: permissions,
columns,
pageCount: Math.ceil(totalCount / pagination.pageSize),
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
manualPagination: true,
manualSorting: true,
meta: {
updatePermission,
deletePermission,
},
})
if (error) {
return <div className="text-red-500"></div>
}
return (
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between py-4">
<div className="flex items-center space-x-2">
<Input
placeholder="搜索权限..."
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn("name")?.setFilterValue(event.target.value)
}
className="max-w-sm"
/>
<Select
value={(table.getColumn("module")?.getFilterValue() as string) ?? "all"}
onValueChange={(value) => table.getColumn("module")?.setFilterValue(value === "all" ? "" : value)}
>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="筛选模块" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="user"></SelectItem>
<SelectItem value="role"></SelectItem>
<SelectItem value="permission"></SelectItem>
<SelectItem value="content"></SelectItem>
<SelectItem value="analytics"></SelectItem>
<SelectItem value="system"></SelectItem>
</SelectContent>
</Select>
</div>
<CreatePermissionDialog onSuccess={refetch} />
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
...
</TableCell>
</TableRow>
) : 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}>
{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 space-x-2 py-4">
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} / {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">
<p className="text-sm font-medium"></p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value))
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 30, 40, 50].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()}
>
<IconChevronsLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<IconChevronLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<IconChevronRight className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<IconChevronsRight className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
)
}