Files
admin/src/pages/Roles.tsx
lafay dacad1587a
All checks were successful
Admin CI / build-and-push-web (push) Successful in 3m48s
refactor(ui): clean up unused imports and variables in UI components
2026-04-04 00:25:18 +08:00

350 lines
12 KiB
TypeScript
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.
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>
)
}