571 lines
25 KiB
TypeScript
571 lines
25 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import { useQuery, useMutation, gql } from '@apollo/client';
|
|
import {
|
|
IconShield,
|
|
IconKey,
|
|
IconCheck,
|
|
IconX,
|
|
IconSettings,
|
|
} from "@tabler/icons-react"
|
|
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Checkbox } from "@/components/ui/checkbox"
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog"
|
|
import { Input } from "@/components/ui/input"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select"
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table"
|
|
import { toast } from "sonner"
|
|
|
|
const GET_ROLES_WITH_PERMISSIONS = gql`
|
|
query GetRolesWithPermissions {
|
|
roles(pagination: { page: 1, perPage: 100 }) {
|
|
items {
|
|
id
|
|
name
|
|
code
|
|
level
|
|
isActive
|
|
permissions {
|
|
id
|
|
name
|
|
code
|
|
module
|
|
action
|
|
resource
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
|
|
const GET_ALL_PERMISSIONS = gql`
|
|
query GetAllPermissions {
|
|
permissions(pagination: { page: 1, perPage: 500 }) {
|
|
items {
|
|
id
|
|
name
|
|
code
|
|
description
|
|
module
|
|
action
|
|
resource
|
|
level
|
|
isActive
|
|
}
|
|
}
|
|
}
|
|
`
|
|
|
|
const ADD_POLICY = gql`
|
|
mutation AddPolicy($roleName: String!, $resource: String!, $action: String!) {
|
|
addPolicy(roleName: $roleName, resource: $resource, action: $action)
|
|
}
|
|
`
|
|
|
|
const REMOVE_POLICY = gql`
|
|
mutation RemovePolicy($roleName: String!, $resource: String!, $action: String!) {
|
|
removePolicy(roleName: $roleName, resource: $resource, action: $action)
|
|
}
|
|
`
|
|
|
|
|
|
interface Permission {
|
|
id: string
|
|
name: string
|
|
code: string
|
|
description?: string
|
|
module: string
|
|
action: string
|
|
resource: string
|
|
level: number
|
|
isActive: boolean
|
|
}
|
|
|
|
interface Role {
|
|
id: string
|
|
name: string
|
|
code: string
|
|
level: number
|
|
isActive: boolean
|
|
permissions: Permission[]
|
|
}
|
|
|
|
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'
|
|
}
|
|
}
|
|
|
|
function PermissionMatrix({ roles, permissions, onPermissionChange }: {
|
|
roles: Role[]
|
|
permissions: Permission[]
|
|
onPermissionChange: () => void
|
|
}) {
|
|
const [addPolicy] = useMutation(ADD_POLICY)
|
|
const [removePolicy] = useMutation(REMOVE_POLICY)
|
|
const [selectedModule, setSelectedModule] = React.useState("")
|
|
|
|
const modules = Array.from(new Set(permissions.map(p => p.module)))
|
|
const filteredPermissions = selectedModule
|
|
? permissions.filter(p => p.module === selectedModule)
|
|
: permissions
|
|
|
|
const handlePermissionToggle = async (roleId: string, permissionId: string, hasPermission: boolean) => {
|
|
const role = roles.find(r => r.id === roleId)
|
|
const permission = permissions.find(p => p.id === permissionId)
|
|
|
|
if (!role || !permission) return
|
|
|
|
try {
|
|
if (hasPermission) {
|
|
await removePolicy({
|
|
variables: {
|
|
roleName: role.code,
|
|
resource: permission.resource,
|
|
action: permission.action
|
|
}
|
|
})
|
|
toast.success("权限移除成功")
|
|
} else {
|
|
await addPolicy({
|
|
variables: {
|
|
roleName: role.code,
|
|
resource: permission.resource,
|
|
action: permission.action
|
|
}
|
|
})
|
|
toast.success("权限分配成功")
|
|
}
|
|
onPermissionChange()
|
|
} catch (error) {
|
|
toast.error("操作失败")
|
|
}
|
|
}
|
|
|
|
const hasPermission = (role: Role, permissionId: string) => {
|
|
return role.permissions.some(p => p.id === permissionId)
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>权限矩阵</CardTitle>
|
|
<CardDescription>
|
|
查看和管理角色与权限的对应关系
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
<div className="flex items-center gap-4">
|
|
<Select value={selectedModule || "all"} onValueChange={(value) => setSelectedModule(value === "all" ? "" : value)}>
|
|
<SelectTrigger className="w-[200px]">
|
|
<SelectValue placeholder="筛选模块" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">全部模块</SelectItem>
|
|
{modules.map((module) => (
|
|
<SelectItem key={module} value={module}>
|
|
{module}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<Badge variant="outline">
|
|
共 {filteredPermissions.length} 个权限
|
|
</Badge>
|
|
</div>
|
|
|
|
<div className="rounded-md border overflow-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-[300px] sticky left-0 bg-background">权限</TableHead>
|
|
{roles.filter(r => r.isActive).map((role) => (
|
|
<TableHead key={role.id} className="text-center min-w-[120px]">
|
|
<div className="space-y-1">
|
|
<div className="font-medium">{role.name}</div>
|
|
<Badge variant="outline" className="text-xs">
|
|
Level {role.level}
|
|
</Badge>
|
|
</div>
|
|
</TableHead>
|
|
))}
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredPermissions.map((permission) => (
|
|
<TableRow key={permission.id}>
|
|
<TableCell className="sticky left-0 bg-background">
|
|
<div className="space-y-1">
|
|
<div className="font-medium">{permission.name}</div>
|
|
<div className="text-sm text-muted-foreground">{permission.code}</div>
|
|
<div className="flex items-center gap-1">
|
|
<Badge variant="outline" className="text-xs">
|
|
{permission.module}
|
|
</Badge>
|
|
<Badge className={`${getActionColor(permission.action)} border-0 text-xs`}>
|
|
{permission.action}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
</TableCell>
|
|
{roles.filter(r => r.isActive).map((role) => {
|
|
const roleHasPermission = hasPermission(role, permission.id)
|
|
const canModify = permission.level <= role.level
|
|
|
|
return (
|
|
<TableCell key={role.id} className="text-center">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className={`h-8 w-8 p-0 ${
|
|
roleHasPermission
|
|
? "text-green-600 hover:text-green-700"
|
|
: "text-muted-foreground hover:text-foreground"
|
|
}`}
|
|
disabled={!canModify}
|
|
onClick={() => handlePermissionToggle(
|
|
role.id,
|
|
permission.id,
|
|
roleHasPermission
|
|
)}
|
|
>
|
|
{roleHasPermission ? (
|
|
<IconCheck className="h-4 w-4" />
|
|
) : (
|
|
<IconX className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
</TableCell>
|
|
)
|
|
})}
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
function RolePermissionEditor({ role, allPermissions, onSave }: {
|
|
role: Role
|
|
allPermissions: Permission[]
|
|
onSave: () => void
|
|
}) {
|
|
const [open, setOpen] = React.useState(false)
|
|
const [selectedPermissions, setSelectedPermissions] = React.useState<string[]>(
|
|
role.permissions.map(p => p.id)
|
|
)
|
|
const [searchTerm, setSearchTerm] = React.useState("")
|
|
const [moduleFilter, setModuleFilter] = React.useState("")
|
|
const [loading, setLoading] = React.useState(false)
|
|
|
|
const filteredPermissions = allPermissions.filter(permission => {
|
|
const matchesSearch = permission.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
permission.code.toLowerCase().includes(searchTerm.toLowerCase())
|
|
const matchesModule = !moduleFilter || permission.module === moduleFilter
|
|
return matchesSearch && matchesModule && permission.isActive
|
|
})
|
|
|
|
const modules = Array.from(new Set(allPermissions.map(p => p.module)))
|
|
|
|
const handlePermissionToggle = (permissionId: string) => {
|
|
setSelectedPermissions(prev =>
|
|
prev.includes(permissionId)
|
|
? prev.filter(id => id !== permissionId)
|
|
: [...prev, permissionId]
|
|
)
|
|
}
|
|
|
|
const handleSelectAll = () => {
|
|
const allIds = filteredPermissions.map(p => p.id)
|
|
setSelectedPermissions(prev => {
|
|
const hasAll = allIds.every(id => prev.includes(id))
|
|
if (hasAll) {
|
|
return prev.filter(id => !allIds.includes(id))
|
|
} else {
|
|
return Array.from(new Set([...prev, ...allIds]))
|
|
}
|
|
})
|
|
}
|
|
|
|
const handleSave = async () => {
|
|
toast.info("批量保存功能正在开发中,请使用权限矩阵进行单个权限操作")
|
|
setOpen(false)
|
|
}
|
|
|
|
React.useEffect(() => {
|
|
if (open) {
|
|
setSelectedPermissions(role.permissions.map(p => p.id))
|
|
}
|
|
}, [open, role.permissions])
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button variant="outline" size="sm">
|
|
<IconSettings className="mr-2 h-4 w-4" />
|
|
编辑权限
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="max-w-4xl max-h-[80vh]">
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
编辑角色权限 - {role.name}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
为角色 "{role.name}" 配置权限。选中的权限将被分配给该角色。
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
<div className="flex items-center gap-4">
|
|
<Input
|
|
placeholder="搜索权限..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="flex-1"
|
|
/>
|
|
<Select value={moduleFilter || "all"} onValueChange={(value) => setModuleFilter(value === "all" ? "" : value)}>
|
|
<SelectTrigger className="w-[160px]">
|
|
<SelectValue placeholder="筛选模块" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">全部模块</SelectItem>
|
|
{modules.map((module) => (
|
|
<SelectItem key={module} value={module}>
|
|
{module}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<Button variant="outline" onClick={handleSelectAll}>
|
|
{filteredPermissions.every(p => selectedPermissions.includes(p.id)) ? "取消全选" : "全选"}
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
<span>已选择 {selectedPermissions.length} 个权限</span>
|
|
<span>共 {filteredPermissions.length} 个权限</span>
|
|
</div>
|
|
|
|
<div className="border rounded-md max-h-96 overflow-auto">
|
|
<div className="space-y-0">
|
|
{filteredPermissions.map((permission) => (
|
|
<div
|
|
key={permission.id}
|
|
className="flex items-center space-x-3 p-3 border-b last:border-b-0 hover:bg-muted/50"
|
|
>
|
|
<Checkbox
|
|
checked={selectedPermissions.includes(permission.id)}
|
|
onCheckedChange={() => handlePermissionToggle(permission.id)}
|
|
/>
|
|
<div className="flex-1 space-y-1">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-medium">{permission.name}</span>
|
|
<Badge variant="outline" className="text-xs">
|
|
{permission.module}
|
|
</Badge>
|
|
<Badge className={`${getActionColor(permission.action)} border-0 text-xs`}>
|
|
{permission.action}
|
|
</Badge>
|
|
</div>
|
|
<div className="text-sm text-muted-foreground">
|
|
{permission.code} - {permission.description || "无描述"}
|
|
</div>
|
|
</div>
|
|
<div className="text-sm text-muted-foreground">
|
|
Level {permission.level}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setOpen(false)}>
|
|
取消
|
|
</Button>
|
|
<Button onClick={handleSave} disabled={loading}>
|
|
{loading ? "保存中..." : "保存权限"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
export function RolePermissionManagement() {
|
|
const [selectedRole, setSelectedRole] = React.useState("")
|
|
|
|
const { data: rolesData, loading: rolesLoading, refetch: refetchRoles } = useQuery(GET_ROLES_WITH_PERMISSIONS, {
|
|
fetchPolicy: 'cache-and-network'
|
|
})
|
|
|
|
const { data: permissionsData, loading: permissionsLoading } = useQuery(GET_ALL_PERMISSIONS, {
|
|
fetchPolicy: 'cache-and-network'
|
|
})
|
|
|
|
const roles = rolesData?.roles?.items || []
|
|
const permissions = permissionsData?.permissions?.items || []
|
|
|
|
const activeRoles = roles.filter((r: any) => r.isActive)
|
|
const currentRole = selectedRole ? roles.find((r: any) => r.id === selectedRole) : null
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* 角色概览卡片 */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{activeRoles.map((role: any) => (
|
|
<Card
|
|
key={role.id}
|
|
className={`cursor-pointer transition-colors ${
|
|
selectedRole === role.id ? 'ring-2 ring-primary' : 'hover:bg-muted/50'
|
|
}`}
|
|
onClick={() => setSelectedRole(selectedRole === role.id ? "" : role.id)}
|
|
>
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<IconShield className="h-5 w-5 text-primary" />
|
|
<CardTitle className="text-base">{role.name}</CardTitle>
|
|
</div>
|
|
<Badge variant="outline">
|
|
Level {role.level}
|
|
</Badge>
|
|
</div>
|
|
<CardDescription>{role.code}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex items-center justify-between">
|
|
<div className="text-sm text-muted-foreground">
|
|
权限数: {role.permissions.length}
|
|
</div>
|
|
<RolePermissionEditor
|
|
role={role}
|
|
allPermissions={permissions}
|
|
onSave={refetchRoles}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
{/* 选中角色的权限详情 */}
|
|
{currentRole && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>
|
|
{currentRole.name} 的权限详情
|
|
</CardTitle>
|
|
<CardDescription>
|
|
查看角色 "{currentRole.name}" 当前拥有的所有权限
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{currentRole.permissions.length === 0 ? (
|
|
<div className="text-center py-8">
|
|
<IconKey className="mx-auto h-12 w-12 text-muted-foreground" />
|
|
<h3 className="mt-2 text-sm font-semibold">暂无权限</h3>
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
该角色尚未分配任何权限。
|
|
</p>
|
|
<div className="mt-4">
|
|
<RolePermissionEditor
|
|
role={currentRole}
|
|
allPermissions={permissions}
|
|
onSave={refetchRoles}
|
|
/>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="text-sm text-muted-foreground">
|
|
共 {currentRole.permissions.length} 个权限
|
|
</div>
|
|
<RolePermissionEditor
|
|
role={currentRole}
|
|
allPermissions={permissions}
|
|
onSave={refetchRoles}
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
{currentRole.permissions.map((permission: any) => (
|
|
<Card key={permission.id} className="p-3">
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-2">
|
|
<IconKey className="h-4 w-4 text-primary" />
|
|
<span className="font-medium text-sm">{permission.name}</span>
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{permission.code}
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Badge variant="outline" className="text-xs">
|
|
{permission.module}
|
|
</Badge>
|
|
<Badge className={`${getActionColor(permission.action)} border-0 text-xs`}>
|
|
{permission.action}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* 权限矩阵 */}
|
|
{!rolesLoading && !permissionsLoading && (
|
|
<PermissionMatrix
|
|
roles={roles}
|
|
permissions={permissions}
|
|
onPermissionChange={refetchRoles}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
} |