mosaicmap/app/admin/permissions/role-permission-management.tsx
2025-08-18 00:03:16 +08:00

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>
)
}