casualroom/apps/fenghuo/web/app/[locale]/dashboard/permissions/page.tsx

853 lines
28 KiB
TypeScript
Raw Normal View History

2025-07-28 07:50:50 +08:00
'use client';
import * as React from 'react';
import { useState } from 'react';
import { SiteHeader, PageInfo } from '@/components/site-header';
import {
IconPlus,
IconEdit,
IconTrash,
IconSearch,
IconUserShield,
IconShield,
IconEye,
IconSettings,
IconCheck,
IconX,
IconChevronDown,
IconChevronRight,
} from '@tabler/icons-react';
import { Button } from '@nice/ui/components/button';
import { Input } from '@nice/ui/components/input';
import { Badge } from '@nice/ui/components/badge';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@nice/ui/components/dropdown-menu';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@nice/ui/components/dialog';
import { Label } from '@nice/ui/components/label';
import { Textarea } from '@nice/ui/components/textarea';
import { Switch } from '@nice/ui/components/switch';
import { ScrollArea } from '@nice/ui/components/scroll-area';
import { Checkbox } from '@nice/ui/components/checkbox';
import { toast } from '@nice/ui/components/sonner';
import {
SystemRole,
SystemRoleConfig,
SystemPermission,
SystemPermissionMeta,
PermissionGroup,
PermissionUtils,
PermissionGroupNames,
PermissionLevelBadge,
PermissionLevelLabel,
} from '@fenghuo/common';
import { useQuery } from '@tanstack/react-query';
import { useTRPC, useRole } from '@fenghuo/client';
import { Role } from '@fenghuo/db';
import { useSetPageInfo } from '@/components/providers/dashboard-provider';
// 模拟角色数据
type RoleData = Role & {
userCount?: number;
};
// 远程数据将从 API 获取,这里不再使用模拟数据
// 权限分组组件
interface PermissionGroupProps {
group: PermissionGroup;
permissions: SystemPermission[];
selectedPermissions: Set<SystemPermission>;
onPermissionToggle: (permission: SystemPermission) => void;
onGroupToggle: (permissions: SystemPermission[], shouldSelect: boolean) => void;
disabled?: boolean;
}
function PermissionGroupComponent({
group,
permissions,
selectedPermissions,
onPermissionToggle,
onGroupToggle,
disabled = false,
}: PermissionGroupProps) {
const [isExpanded, setIsExpanded] = useState(true);
const groupPermissions = permissions.filter((p) => SystemPermissionMeta[p]?.group === group);
if (groupPermissions.length === 0) return null;
const allSelected = groupPermissions.length > 0 && groupPermissions.every((p) => selectedPermissions.has(p));
const someSelected = groupPermissions.some((p) => selectedPermissions.has(p));
const handleGroupToggle = () => {
if (disabled) return;
onGroupToggle(groupPermissions, !allSelected);
};
return (
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden bg-gray-50 dark:bg-gray-900/30">
<div
className="flex items-center justify-between p-4 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800/70 transition-colors bg-white dark:bg-gray-800/50"
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="flex items-center gap-3">
<Button variant="ghost" size="sm" className="h-6 w-6 p-0 hover:bg-gray-200 dark:hover:bg-gray-700">
{isExpanded ? <IconChevronDown className="h-4 w-4" /> : <IconChevronRight className="h-4 w-4" />}
</Button>
<div className="flex items-center gap-3" onClick={(e) => e.stopPropagation()}>
<Checkbox
checked={allSelected ? true : someSelected ? 'indeterminate' : false}
onCheckedChange={handleGroupToggle}
disabled={disabled}
/>
<div className="flex items-center gap-2">
<span className="font-semibold text-sm text-gray-900 dark:text-gray-100">
{PermissionGroupNames[group]}
</span>
<Badge variant="secondary" className="text-xs font-medium">
{someSelected ? `${groupPermissions.filter((p) => selectedPermissions.has(p)).length}/` : ''}
{groupPermissions.length}
</Badge>
</div>
</div>
</div>
{disabled && (
<Badge variant="outline" className="text-xs text-gray-500">
</Badge>
)}
</div>
{isExpanded && (
<div className="bg-white dark:bg-gray-800/30">
<div className="p-2 space-y-1">
{groupPermissions.map((permission) => {
const meta = SystemPermissionMeta[permission];
const isSelected = selectedPermissions.has(permission);
return (
<div
key={permission}
className="flex items-center justify-between p-3 rounded-lg border border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors bg-white dark:bg-gray-800/20"
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<Checkbox
checked={isSelected}
onCheckedChange={() => onPermissionToggle(permission)}
disabled={disabled}
className={disabled ? 'opacity-40' : ''}
/>
<div className="flex-1 min-w-0">
<div className="font-medium text-sm text-gray-900 dark:text-gray-100 truncate">{meta.name}</div>
<div className="text-xs text-gray-500 dark:text-gray-400 line-clamp-1">{meta.description}</div>
</div>
</div>
<Badge variant={PermissionLevelBadge[meta.level]} className="text-xs font-medium ml-2 shrink-0">
{PermissionLevelLabel[meta.level]}
</Badge>
</div>
);
})}
</div>
</div>
)}
</div>
);
}
export default function PermissionsPage() {
// 状态管理
const [searchTerm, setSearchTerm] = useState('');
const [roles, setRoles] = useState<RoleData[]>([]);
const [showCreateDialog, setShowCreateDialog] = useState(false);
const [showEditDialog, setShowEditDialog] = useState(false);
const [editingRole, setEditingRole] = useState<RoleData | null>(null);
// 表单状态
const [formData, setFormData] = useState({
name: '',
slug: '',
description: '',
isActive: true,
});
const [selectedPermissions, setSelectedPermissions] = useState<Set<SystemPermission>>(new Set());
useSetPageInfo({
title: '权限管理',
subtitle: '管理系统角色和权限分配',
})
// tRPC & TanStack Query
const trpc = useTRPC();
const roleMutations = useRole();
// 获取角色列表(包含用户数量统计)
const { data: rolePagination } = useQuery(
trpc.role.findManyWithPagination.queryOptions({
page: 1,
pageSize: 100,
select: {
id: true,
name: true,
slug: true,
description: true,
permissions: true,
isSystem: true,
isActive: true,
createdAt: true,
updatedAt: true,
_count: {
select: {
users: {
where: {
deletedAt: null
},
},
},
},
},
}),
);
// 同步远程数据到本地状态
React.useEffect(() => {
if (rolePagination?.items) {
console.log('rolePagination', rolePagination);
const rolesWithUserCount = rolePagination.items.map((role: any) => ({
...role,
userCount: role._count?.users || 0,
_count: undefined, // 移除 _count 字段,避免混淆
}));
setRoles(rolesWithUserCount as RoleData[]);
}
}, [rolePagination]);
// 获取所有权限与分组(仅初始化时计算)
const allPermissions = React.useMemo(() => Object.values(SystemPermission) as SystemPermission[], []);
const permissionGroups = React.useMemo(() => Object.values(PermissionGroup) as PermissionGroup[], []);
// 根据搜索关键字过滤角色
const filteredRoles = React.useMemo(
() =>
roles.filter(
(role) =>
role.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
role.description?.toLowerCase().includes(searchTerm.toLowerCase()),
),
[roles, searchTerm],
);
// 权限切换
const handlePermissionToggle = (permission: SystemPermission) => {
const newSelected = new Set(selectedPermissions);
if (newSelected.has(permission)) {
newSelected.delete(permission);
} else {
newSelected.add(permission);
}
setSelectedPermissions(newSelected);
};
// Batch toggle permissions for a group
const handleGroupPermissionToggle = (permissions: SystemPermission[], shouldSelect: boolean) => {
setSelectedPermissions((prev) => {
const newSelected = new Set(prev);
if (shouldSelect) {
permissions.forEach((p) => newSelected.add(p));
} else {
permissions.forEach((p) => newSelected.delete(p));
}
return newSelected;
});
};
// 重置表单
const resetForm = () => {
setFormData({
name: '',
slug: '',
description: '',
isActive: true,
});
setSelectedPermissions(new Set());
setEditingRole(null);
};
// 创建角色
const handleCreateRole = () => {
setShowCreateDialog(true);
resetForm();
};
// 编辑角色
const handleEditRole = (role: RoleData) => {
setEditingRole(role);
setFormData({
name: role.name,
slug: role.slug,
description: role.description || '',
isActive: role.isActive,
});
setSelectedPermissions(new Set(role.permissions as SystemPermission[]));
setShowEditDialog(true);
};
// 删除角色
const handleDeleteRole = (role: RoleData) => {
if (role.isSystem) {
toast.error('系统角色不能删除');
return;
}
if (confirm(`确定要删除角色"${role.name}"吗?`)) {
roleMutations.softDeleteByIds.mutate(
{ ids: [role.id] },
{
onSuccess: () => {
toast.success('角色删除成功');
},
onError: (error) => {
toast.error('删除失败: ' + error.message);
},
},
);
}
};
// 保存角色
const handleSaveRole = () => {
if (!formData.name.trim()) {
toast.error('请输入角色名称');
return;
}
if (!formData.slug.trim()) {
toast.error('请输入角色标识');
return;
}
if (selectedPermissions.size === 0) {
toast.error('请至少选择一个权限');
return;
}
const onSuccess = (result: any) => {
// 根据操作类型更新本地列表,避免等待重新拉取
setRoles((prev) => {
if (editingRole) {
return prev.map((r) => (r.id === (result?.id || editingRole.id) ? { ...r, ...result } : r));
}
return [...prev, result];
});
toast.success(editingRole ? '角色更新成功' : '角色创建成功');
setShowCreateDialog(false);
setShowEditDialog(false);
resetForm();
};
const onError = (error: any) => {
toast.error('操作失败: ' + error.message);
};
if (editingRole) {
roleMutations.update.mutate(
{
where: { id: editingRole.id },
data: {
name: formData.name,
slug: formData.slug,
description: formData.description,
permissions: Array.from(selectedPermissions),
isActive: formData.isActive,
},
},
{ onSuccess, onError },
);
} else {
roleMutations.create.mutate(
{
data: {
name: formData.name,
slug: formData.slug,
description: formData.description,
permissions: Array.from(selectedPermissions),
isActive: formData.isActive,
isSystem: false,
},
},
{ onSuccess, onError },
);
}
// 后续逻辑在 onSuccess 内处理
return;
};
// 切换角色状态
const handleToggleRoleStatus = (role: RoleData) => {
if (role.isSystem && role.slug === 'super_admin') {
toast.error('超级管理员角色不能禁用');
return;
}
roleMutations.update.mutate(
{
where: { id: role.id },
data: { isActive: !role.isActive },
},
{
onSuccess: () => {
toast.success(role.isActive ? '角色已禁用' : '角色已启用');
},
onError: (error) => {
toast.error('操作失败: ' + error.message);
},
},
);
};
return (
<div className="h-full flex flex-col">
{/* 工具栏 */}
<div className="flex items-center justify-between gap-4 p-6 border-b">
<div className="flex items-center gap-4 flex-1">
<div className="relative max-w-md">
<IconSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="搜索角色名称或描述..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<Button onClick={handleCreateRole} className="flex items-center gap-2">
<IconPlus className="h-4 w-4" />
</Button>
</div>
{/* 角色列表 */}
<div className="flex-1 p-6">
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
{filteredRoles.map((role) => (
<div key={role.id} className="border rounded-lg p-6 space-y-4 hover:shadow-md transition-shadow">
{/* 角色头部 */}
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-100 dark:bg-blue-900/20 rounded-lg">
<IconUserShield className="h-5 w-5 text-blue-600 dark:text-blue-400" />
</div>
<div>
<div className="font-semibold text-lg flex items-center gap-2">
{role.name}
{role.isSystem && (
<Badge variant="outline" className="text-xs">
</Badge>
)}
{!role.isActive && (
<Badge variant="destructive" className="text-xs">
</Badge>
)}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">/{role.slug}</div>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<IconSettings className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleEditRole(role)} className="cursor-pointer">
<IconEdit className="mr-2 h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleToggleRoleStatus(role)} className="cursor-pointer">
{role.isActive ? (
<>
<IconX className="mr-2 h-4 w-4" />
</>
) : (
<>
<IconCheck className="mr-2 h-4 w-4" />
</>
)}
</DropdownMenuItem>
{!role.isSystem && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => handleDeleteRole(role)}
className="text-red-600 focus:text-red-600 dark:text-red-400 dark:focus:text-red-400 cursor-pointer"
>
<IconTrash className="mr-2 h-4 w-4" />
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* 角色描述 */}
{role.description && <p className="text-sm text-gray-600 dark:text-gray-300">{role.description}</p>}
{/* 统计信息 */}
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-4">
<span className="text-gray-500 dark:text-gray-400">
: <span className="font-medium text-gray-900 dark:text-gray-100">{role.userCount || 0}</span>
</span>
<span className="text-gray-500 dark:text-gray-400">
:{' '}
<span className="font-medium text-gray-900 dark:text-gray-100">{role.permissions.length}</span>
</span>
</div>
</div>
{/* 权限预览 */}
<div>
<div className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2"></div>
<div className="flex flex-wrap gap-1">
{role.permissions.slice(0, 6).map((permission) => {
const meta = SystemPermissionMeta[permission];
return (
<Badge key={permission} variant="outline" className="text-xs">
{meta?.name || permission}
</Badge>
);
})}
{role.permissions.length > 6 && (
<Badge variant="outline" className="text-xs">
+{role.permissions.length - 6}
</Badge>
)}
</div>
</div>
</div>
))}
</div>
{filteredRoles.length === 0 && (
<div className="text-center py-12">
<div className="text-gray-500 dark:text-gray-400">{searchTerm ? '未找到匹配的角色' : '暂无角色数据'}</div>
</div>
)}
</div>
{/* 创建角色对话框 */}
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
<DialogContent className="w-full max-h-[90vh] flex flex-col sm:max-w-2xl md:max-w-4xl lg:max-w-5xl xl:max-w-screen-xl">
<DialogHeader className="shrink-0 pb-6">
<DialogTitle className="flex items-center gap-2 text-xl">
<IconUserShield className="h-5 w-5 text-blue-600" />
</DialogTitle>
</DialogHeader>
<ScrollArea className="flex-1 -mx-6 px-6">
{/* 卡片采用两列布局:基本信息左,权限配置右;在小屏依旧垂直 */}
<div className="flex flex-col lg:flex-row gap-6 py-2">
<div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-6 flex-1 lg:w-1/2">
<div className="flex items-center gap-2 mb-4">
<IconEdit className="h-4 w-4 text-gray-600 dark:text-gray-400" />
<h3 className="font-semibold text-base"></h3>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name" className="text-sm font-medium">
<span className="text-red-500">*</span>
</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
placeholder="如:内容编辑"
className="bg-white dark:bg-gray-800"
/>
</div>
<div className="space-y-2">
<Label htmlFor="slug" className="text-sm font-medium">
<span className="text-red-500">*</span>
</Label>
<Input
id="slug"
value={formData.slug}
onChange={(e) => setFormData((prev) => ({ ...prev, slug: e.target.value }))}
placeholder="如content_editor"
className="bg-white dark:bg-gray-800"
/>
</div>
<div className="lg:col-span-2 space-y-2">
<Label htmlFor="description" className="text-sm font-medium">
</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => setFormData((prev) => ({ ...prev, description: e.target.value }))}
placeholder="描述这个角色的职责和用途..."
rows={2}
className="bg-white dark:bg-gray-800"
/>
</div>
<div className="flex items-center justify-between p-3 bg-white dark:bg-gray-800 rounded-lg border">
<div>
<Label htmlFor="isActive" className="text-sm font-medium">
</Label>
<p className="text-xs text-gray-500 dark:text-gray-400"></p>
</div>
<Switch
id="isActive"
checked={formData.isActive}
onCheckedChange={(checked) => setFormData((prev) => ({ ...prev, isActive: checked }))}
/>
</div>
</div>
</div>
<div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-6 flex-1 lg:w-1/2">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<IconShield className="h-4 w-4 text-gray-600 dark:text-gray-400" />
<h3 className="font-semibold text-base"></h3>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 text-sm">
<span className="text-gray-500 dark:text-gray-400">:</span>
<Badge variant="secondary" className="font-medium">
{selectedPermissions.size} / {allPermissions.length}
</Badge>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setSelectedPermissions(new Set(allPermissions))}
>
</Button>
<Button variant="outline" size="sm" onClick={() => setSelectedPermissions(new Set())}>
</Button>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border max-h-80 overflow-y-auto">
<div className="p-4 space-y-3">
{permissionGroups.map((group) => (
<PermissionGroupComponent
key={group}
group={group}
permissions={allPermissions}
selectedPermissions={selectedPermissions}
onPermissionToggle={handlePermissionToggle}
onGroupToggle={handleGroupPermissionToggle}
/>
))}
</div>
</div>
</div>
</div>
</ScrollArea>
<div className="flex justify-between items-center gap-3 mt-6 pt-6 border-t shrink-0">
<div className="text-sm text-gray-500 dark:text-gray-400">
{selectedPermissions.size === 0 && '请至少选择一个权限'}
</div>
<div className="flex gap-3">
<Button variant="outline" onClick={() => setShowCreateDialog(false)}>
</Button>
<Button
onClick={handleSaveRole}
disabled={!formData.name.trim() || !formData.slug.trim() || selectedPermissions.size === 0}
>
<IconPlus className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* 编辑角色对话框 */}
<Dialog open={showEditDialog} onOpenChange={setShowEditDialog}>
<DialogContent className="w-full max-h-[90vh] flex flex-col sm:max-w-2xl md:max-w-4xl lg:max-w-5xl xl:max-w-screen-xl">
<DialogHeader className="shrink-0 pb-6">
<DialogTitle className="flex items-center gap-2 text-xl">
<IconEdit className="h-5 w-5 text-blue-600" />
{editingRole?.isSystem && (
<Badge variant="outline" className="ml-2 text-xs">
</Badge>
)}
</DialogTitle>
</DialogHeader>
<ScrollArea className="flex-1 -mx-6 px-6">
{/* 卡片采用两列布局:基本信息左,权限配置右;在小屏依旧垂直 */}
<div className="flex flex-col lg:flex-row gap-6 py-2">
<div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-6 flex-1 lg:w-1/2">
<div className="flex items-center gap-2 mb-4">
<IconEdit className="h-4 w-4 text-gray-600 dark:text-gray-400" />
<h3 className="font-semibold text-base"></h3>
{editingRole?.isSystem && (
<Badge variant="secondary" className="text-xs">
</Badge>
)}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="edit-name" className="text-sm font-medium">
<span className="text-red-500">*</span>
</Label>
<Input
id="edit-name"
value={formData.name}
onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
placeholder="如:内容编辑"
disabled={editingRole?.isSystem}
className="bg-white dark:bg-gray-800 disabled:opacity-60"
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-slug" className="text-sm font-medium">
<span className="text-red-500">*</span>
</Label>
<Input
id="edit-slug"
value={formData.slug}
onChange={(e) => setFormData((prev) => ({ ...prev, slug: e.target.value }))}
placeholder="如content_editor"
disabled={editingRole?.isSystem}
className="bg-white dark:bg-gray-800 disabled:opacity-60"
/>
</div>
<div className="lg:col-span-2 space-y-2">
<Label htmlFor="edit-description" className="text-sm font-medium">
</Label>
<Textarea
id="edit-description"
value={formData.description}
onChange={(e) => setFormData((prev) => ({ ...prev, description: e.target.value }))}
placeholder="描述这个角色的职责和用途..."
rows={2}
className="bg-white dark:bg-gray-800"
/>
</div>
<div className="flex items-center justify-between p-3 bg-white dark:bg-gray-800 rounded-lg border">
<div>
<Label htmlFor="edit-isActive" className="text-sm font-medium">
</Label>
<p className="text-xs text-gray-500 dark:text-gray-400">
{editingRole?.slug === 'super_admin' ? '超级管理员角色无法禁用' : '控制角色是否生效'}
</p>
</div>
<Switch
id="edit-isActive"
checked={formData.isActive}
onCheckedChange={(checked) => setFormData((prev) => ({ ...prev, isActive: checked }))}
disabled={editingRole?.slug === 'super_admin'}
/>
</div>
</div>
</div>
<div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-6 flex-1 lg:w-1/2">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<IconShield className="h-4 w-4 text-gray-600 dark:text-gray-400" />
<h3 className="font-semibold text-base"></h3>
{editingRole?.slug === 'super_admin' && (
<Badge variant="destructive" className="text-xs">
</Badge>
)}
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 text-sm">
<span className="text-gray-500 dark:text-gray-400">:</span>
<Badge variant="secondary" className="font-medium">
{selectedPermissions.size} / {allPermissions.length}
</Badge>
</div>
{editingRole?.slug !== 'super_admin' && (
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setSelectedPermissions(new Set(allPermissions))}
>
</Button>
<Button variant="outline" size="sm" onClick={() => setSelectedPermissions(new Set())}>
</Button>
</div>
)}
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border max-h-80 overflow-y-auto">
<div className="p-4 space-y-3">
{permissionGroups.map((group) => (
<PermissionGroupComponent
key={group}
group={group}
permissions={allPermissions}
selectedPermissions={selectedPermissions}
onPermissionToggle={handlePermissionToggle}
disabled={editingRole?.slug === 'super_admin'}
onGroupToggle={handleGroupPermissionToggle}
/>
))}
</div>
</div>
</div>
</div>
</ScrollArea>
<div className="flex justify-between items-center gap-3 mt-6 pt-6 border-t shrink-0">
<div className="text-sm text-gray-500 dark:text-gray-400">
{selectedPermissions.size === 0 && '请至少选择一个权限'}
</div>
<div className="flex gap-3">
<Button variant="outline" onClick={() => setShowEditDialog(false)}>
</Button>
<Button
onClick={handleSaveRole}
disabled={!formData.name.trim() || !formData.slug.trim() || selectedPermissions.size === 0}
>
<IconCheck className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
);
}