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

853 lines
28 KiB
TypeScript
Executable File
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 { 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>
);
}