Initial commit

This commit is contained in:
2026-03-14 18:24:33 +08:00
commit 890c33f510
67 changed files with 13080 additions and 0 deletions

349
src/pages/Roles.tsx Normal file
View File

@@ -0,0 +1,349 @@
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, Shield, 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>
)
}