2026-03-14 18:24:33 +08:00
|
|
|
|
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'
|
2026-04-04 00:25:18 +08:00
|
|
|
|
import { RefreshCw, Loader2, Users, Check } from 'lucide-react'
|
2026-03-14 18:24:33 +08:00
|
|
|
|
|
|
|
|
|
|
// 角色颜色映射
|
|
|
|
|
|
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>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|