350 lines
12 KiB
TypeScript
350 lines
12 KiB
TypeScript
import { useState, useEffect } from 'react'
|
||
import { rolesApi, type RoleInfo, type Permission } from '@/api'
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Badge } from '@/components/ui/badge'
|
||
import { Checkbox } from '@/components/ui/checkbox'
|
||
import {
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableHead,
|
||
TableHeader,
|
||
TableRow,
|
||
} from '@/components/ui/table'
|
||
import { RefreshCw, Loader2, Users, Check } from 'lucide-react'
|
||
|
||
// 角色颜色映射
|
||
const roleColors: Record<string, { bg: string; text: string; border: string }> = {
|
||
super_admin: { bg: 'bg-purple-100', text: 'text-purple-700', border: 'border-purple-300' },
|
||
admin: { bg: 'bg-red-100', text: 'text-red-700', border: 'border-red-300' },
|
||
moderator: { bg: 'bg-orange-100', text: 'text-orange-700', border: 'border-orange-300' },
|
||
user: { bg: 'bg-blue-100', text: 'text-blue-700', border: 'border-blue-300' },
|
||
}
|
||
|
||
// 角色名称映射
|
||
const roleNameMap: Record<string, string> = {
|
||
super_admin: '超级管理员',
|
||
admin: '管理员',
|
||
moderator: '版主',
|
||
user: '普通用户',
|
||
}
|
||
|
||
// 资源名称映射
|
||
const resourceNames: Record<string, string> = {
|
||
users: '用户',
|
||
posts: '帖子',
|
||
comments: '评论',
|
||
groups: '群组',
|
||
messages: '消息',
|
||
reports: '举报',
|
||
settings: '设置',
|
||
logs: '日志',
|
||
}
|
||
|
||
// 操作名称映射
|
||
const actionNames: Record<string, string> = {
|
||
read: '读取',
|
||
write: '写入',
|
||
delete: '删除',
|
||
manage: '管理',
|
||
}
|
||
|
||
// 预定义的权限矩阵结构
|
||
const permissionMatrix = {
|
||
resources: ['users', 'posts', 'comments', 'groups', 'messages', 'reports', 'settings', 'logs'],
|
||
actions: ['read', 'write', 'delete', 'manage'],
|
||
}
|
||
|
||
export default function Roles() {
|
||
// 状态
|
||
const [roles, setRoles] = useState<RoleInfo[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
const [selectedRole, setSelectedRole] = useState<string | null>(null)
|
||
const [rolePermissions, setRolePermissions] = useState<Permission[]>([])
|
||
const [permissionsLoading, setPermissionsLoading] = useState(false)
|
||
const [saving, setSaving] = useState(false)
|
||
|
||
// 加载角色列表
|
||
const loadRoles = async () => {
|
||
setLoading(true)
|
||
try {
|
||
const response = await rolesApi.getRoles()
|
||
const data = response.data.data || []
|
||
setRoles(data)
|
||
if (data.length > 0 && !selectedRole) {
|
||
setSelectedRole(data[0].name)
|
||
loadRolePermissions(data[0].name)
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to load roles:', error)
|
||
setRoles([])
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
// 加载角色权限
|
||
const loadRolePermissions = async (roleName: string) => {
|
||
setPermissionsLoading(true)
|
||
try {
|
||
const response = await rolesApi.getRolePermissions(roleName)
|
||
setRolePermissions(response.data.data || [])
|
||
} catch (error) {
|
||
console.error('Failed to load role permissions:', error)
|
||
setRolePermissions([])
|
||
} finally {
|
||
setPermissionsLoading(false)
|
||
}
|
||
}
|
||
|
||
// 初始加载
|
||
useEffect(() => {
|
||
loadRoles()
|
||
}, [])
|
||
|
||
// 选择角色时加载权限
|
||
useEffect(() => {
|
||
if (selectedRole) {
|
||
loadRolePermissions(selectedRole)
|
||
}
|
||
}, [selectedRole])
|
||
|
||
// 检查权限是否存在
|
||
const hasPermission = (resource: string, action: string) => {
|
||
return rolePermissions.some(
|
||
(p) => p.resource === resource && p.action === action
|
||
)
|
||
}
|
||
|
||
// 切换权限
|
||
const togglePermission = async (resource: string, action: string) => {
|
||
setSaving(true)
|
||
try {
|
||
const currentPermissions = [...rolePermissions]
|
||
const hasIt = hasPermission(resource, action)
|
||
|
||
let newPermissions: Permission[]
|
||
if (hasIt) {
|
||
newPermissions = currentPermissions.filter(
|
||
(p) => !(p.resource === resource && p.action === action)
|
||
)
|
||
} else {
|
||
newPermissions = [
|
||
...currentPermissions,
|
||
{ resource, action },
|
||
]
|
||
}
|
||
|
||
// 更新权限 - 发送 {resource, action} 格式的数组
|
||
await rolesApi.updateRolePermissions(
|
||
selectedRole!,
|
||
newPermissions.map((p) => ({ resource: p.resource, action: p.action }))
|
||
)
|
||
|
||
setRolePermissions(newPermissions)
|
||
} catch (error) {
|
||
console.error('Failed to update permission:', error)
|
||
} finally {
|
||
setSaving(false)
|
||
}
|
||
}
|
||
|
||
// 刷新
|
||
const handleRefresh = () => {
|
||
loadRoles()
|
||
if (selectedRole) {
|
||
loadRolePermissions(selectedRole)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-3xl font-bold">角色管理</h1>
|
||
<p className="text-muted-foreground">管理系统角色和权限</p>
|
||
</div>
|
||
<Button onClick={handleRefresh} disabled={loading}>
|
||
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
|
||
刷新
|
||
</Button>
|
||
</div>
|
||
|
||
{/* 角色卡片 */}
|
||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||
{loading ? (
|
||
<Card className="col-span-full">
|
||
<CardContent className="flex items-center justify-center py-8">
|
||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||
</CardContent>
|
||
</Card>
|
||
) : roles.length === 0 ? (
|
||
<Card className="col-span-full">
|
||
<CardContent className="text-center py-8 text-muted-foreground">
|
||
暂无角色数据
|
||
</CardContent>
|
||
</Card>
|
||
) : (
|
||
roles.map((role) => {
|
||
const colors = roleColors[role.name] || { bg: 'bg-gray-100', text: 'text-gray-700', border: 'border-gray-300' }
|
||
const isSelected = selectedRole === role.name
|
||
|
||
return (
|
||
<Card
|
||
key={role.name}
|
||
className={`cursor-pointer transition-all hover:shadow-md ${
|
||
isSelected ? `ring-2 ring-primary ${colors.border}` : ''
|
||
}`}
|
||
onClick={() => setSelectedRole(role.name)}
|
||
>
|
||
<CardHeader className="pb-2">
|
||
<div className="flex items-center justify-between">
|
||
<CardTitle className={`text-lg ${colors.text}`}>
|
||
{roleNameMap[role.name] || role.display_name || role.name}
|
||
</CardTitle>
|
||
{isSelected && (
|
||
<Check className="h-5 w-5 text-primary" />
|
||
)}
|
||
</div>
|
||
<CardDescription className="line-clamp-2">
|
||
{role.description || '暂无描述'}
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="flex items-center justify-between text-sm">
|
||
<div className="flex items-center gap-1 text-muted-foreground">
|
||
<Users className="h-4 w-4" />
|
||
<span>{role.priority} 优先级</span>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)
|
||
})
|
||
)}
|
||
</div>
|
||
|
||
{/* 权限矩阵 */}
|
||
{selectedRole && (
|
||
<Card>
|
||
<CardHeader>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<CardTitle>
|
||
权限矩阵 - {roleNameMap[selectedRole] || selectedRole}
|
||
</CardTitle>
|
||
<CardDescription>
|
||
管理角色的各项权限,点击复选框切换权限状态
|
||
</CardDescription>
|
||
</div>
|
||
{saving && (
|
||
<Badge variant="outline" className="text-orange-500">
|
||
<Loader2 className="h-3 w-3 animate-spin mr-1" />
|
||
保存中...
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{permissionsLoading ? (
|
||
<div className="flex items-center justify-center py-8">
|
||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||
</div>
|
||
) : (
|
||
<div className="overflow-x-auto">
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead className="w-[120px] font-semibold">资源</TableHead>
|
||
{permissionMatrix.actions.map((action) => (
|
||
<TableHead key={action} className="text-center font-semibold">
|
||
{actionNames[action] || action}
|
||
</TableHead>
|
||
))}
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{permissionMatrix.resources.map((resource) => (
|
||
<TableRow key={resource}>
|
||
<TableCell className="font-medium">
|
||
{resourceNames[resource] || resource}
|
||
</TableCell>
|
||
{permissionMatrix.actions.map((action) => {
|
||
const has = hasPermission(resource, action)
|
||
return (
|
||
<TableCell key={`${resource}-${action}`} className="text-center">
|
||
<div className="flex justify-center">
|
||
<Checkbox
|
||
checked={has}
|
||
onCheckedChange={() => togglePermission(resource, action)}
|
||
disabled={saving || selectedRole === 'super_admin'}
|
||
className="data-[state=checked]:bg-primary"
|
||
/>
|
||
</div>
|
||
</TableCell>
|
||
)
|
||
})}
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
)}
|
||
|
||
{selectedRole === 'super_admin' && (
|
||
<p className="text-sm text-muted-foreground mt-4 text-center">
|
||
超级管理员拥有所有权限,无法修改
|
||
</p>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* 角色说明 */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>角色说明</CardTitle>
|
||
<CardDescription>各角色的权限范围说明</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="space-y-4">
|
||
<div className="flex items-start gap-3">
|
||
<Badge className={`${roleColors.super_admin?.bg} ${roleColors.super_admin?.text}`}>
|
||
超级管理员
|
||
</Badge>
|
||
<p className="text-sm text-muted-foreground">
|
||
拥有系统所有权限,可以管理其他管理员、系统设置等核心功能
|
||
</p>
|
||
</div>
|
||
<div className="flex items-start gap-3">
|
||
<Badge className={`${roleColors.admin?.bg} ${roleColors.admin?.text}`}>
|
||
管理员
|
||
</Badge>
|
||
<p className="text-sm text-muted-foreground">
|
||
可以管理用户、帖子、评论、群组等内容,处理用户举报
|
||
</p>
|
||
</div>
|
||
<div className="flex items-start gap-3">
|
||
<Badge className={`${roleColors.moderator?.bg} ${roleColors.moderator?.text}`}>
|
||
版主
|
||
</Badge>
|
||
<p className="text-sm text-muted-foreground">
|
||
负责审核和管理帖子、评论等内容,处理违规内容
|
||
</p>
|
||
</div>
|
||
<div className="flex items-start gap-3">
|
||
<Badge className={`${roleColors.user?.bg} ${roleColors.user?.text}`}>
|
||
普通用户
|
||
</Badge>
|
||
<p className="text-sm text-muted-foreground">
|
||
基本用户权限,可以发帖、评论、加入群组等
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|