feat: add system governance pages
This commit is contained in:
177
frontend/src/features/system/SystemRolesPage.tsx
Normal file
177
frontend/src/features/system/SystemRolesPage.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable'
|
||||
import { AppEmptyState } from '@/components/app/AppEmptyState'
|
||||
import { AppPageHeader } from '@/components/app/AppPageHeader'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
import { createRole, listPermissions, listRoles, setRolePermissions } from './api'
|
||||
import type { Permission, RoleDetail } from './types'
|
||||
|
||||
export function SystemRolesPage() {
|
||||
const [roles, setRoles] = React.useState<RoleDetail[]>([])
|
||||
const [permissions, setPermissions] = React.useState<Permission[]>([])
|
||||
const [roleName, setRoleName] = React.useState('')
|
||||
const [editingRole, setEditingRole] = React.useState<RoleDetail | null>(null)
|
||||
const [error, setError] = React.useState('')
|
||||
|
||||
const load = React.useCallback(async () => {
|
||||
try {
|
||||
const [nextRoles, nextPermissions] = await Promise.all([listRoles(), listPermissions()])
|
||||
setRoles(nextRoles)
|
||||
setPermissions(nextPermissions)
|
||||
setError('')
|
||||
} catch (reason) {
|
||||
setError(reason instanceof Error ? reason.message : '加载失败')
|
||||
}
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
void load()
|
||||
}, [load])
|
||||
|
||||
async function handleCreate(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault()
|
||||
await createRole({ name: roleName, permission_codes: [] })
|
||||
setRoleName('')
|
||||
await load()
|
||||
}
|
||||
|
||||
const columns: AppDataTableColumn<RoleDetail>[] = [
|
||||
{
|
||||
key: 'name',
|
||||
header: '角色',
|
||||
cell: (role) => <span className="font-medium text-foreground">{role.name}</span>
|
||||
},
|
||||
{
|
||||
key: 'permissions',
|
||||
header: '权限数量',
|
||||
cell: (role) => <Badge variant="info">{role.permission_codes.length}</Badge>
|
||||
},
|
||||
{
|
||||
key: 'summary',
|
||||
header: '权限摘要',
|
||||
cell: (role) => (
|
||||
<div className="flex max-w-[520px] flex-wrap gap-1">
|
||||
{role.permission_codes.slice(0, 6).map((code) => <Badge key={code} variant="secondary">{code}</Badge>)}
|
||||
{role.permission_codes.length > 6 ? <Badge variant="outline">+{role.permission_codes.length - 6}</Badge> : null}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
header: '操作',
|
||||
cell: (role) => <Button variant="ghost" onClick={() => setEditingRole(role)}>配置权限</Button>
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-background p-6 text-foreground">
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-4">
|
||||
<AppPageHeader title="角色权限" description="定义后台角色,并将系统权限授予对应角色。" />
|
||||
|
||||
<form className="grid gap-3 rounded-lg border border-border bg-surface p-3 md:grid-cols-[minmax(0,360px)_auto]" onSubmit={handleCreate}>
|
||||
<Input placeholder="角色名称" value={roleName} onChange={(event) => setRoleName(event.target.value)} required />
|
||||
<Button type="submit">创建角色</Button>
|
||||
</form>
|
||||
|
||||
{error ? <AppEmptyState title="无法加载角色" description={error} /> : null}
|
||||
{!error ? (
|
||||
<AppDataTable
|
||||
columns={columns}
|
||||
rows={roles}
|
||||
getRowKey={(role) => role.id}
|
||||
emptyTitle="暂无角色"
|
||||
emptyDescription="创建角色后可继续配置权限。"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<RolePermissionDialog
|
||||
role={editingRole}
|
||||
permissions={permissions}
|
||||
onClose={() => setEditingRole(null)}
|
||||
onSaved={load}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
function RolePermissionDialog({
|
||||
role,
|
||||
permissions,
|
||||
onClose,
|
||||
onSaved
|
||||
}: {
|
||||
role: RoleDetail | null
|
||||
permissions: Permission[]
|
||||
onClose: () => void
|
||||
onSaved: () => Promise<void>
|
||||
}) {
|
||||
const [selected, setSelected] = React.useState<string[]>([])
|
||||
|
||||
React.useEffect(() => {
|
||||
setSelected(role?.permission_codes ?? [])
|
||||
}, [role])
|
||||
|
||||
const groupedPermissions = React.useMemo(() => {
|
||||
return permissions.reduce<Record<string, Permission[]>>((groups, permission) => {
|
||||
groups[permission.module] = groups[permission.module] ?? []
|
||||
groups[permission.module].push(permission)
|
||||
return groups
|
||||
}, {})
|
||||
}, [permissions])
|
||||
|
||||
async function handleSave() {
|
||||
if (!role) return
|
||||
await setRolePermissions(role.id, selected)
|
||||
await onSaved()
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={Boolean(role)} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="sm:max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>配置角色权限</DialogTitle>
|
||||
<DialogDescription>{role?.name} 可访问的菜单和接口能力。</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid max-h-[56vh] gap-4 overflow-auto pr-1">
|
||||
{Object.entries(groupedPermissions).map(([module, modulePermissions]) => (
|
||||
<section key={module} className="rounded-md border border-border p-3">
|
||||
<p className="mb-3 text-sm font-medium text-foreground">{module}</p>
|
||||
<div className="grid gap-2 md:grid-cols-2">
|
||||
{modulePermissions.map((permission) => (
|
||||
<label key={permission.codename} className="flex items-start gap-2 text-sm">
|
||||
<Checkbox
|
||||
checked={selected.includes(permission.codename)}
|
||||
onCheckedChange={(checked) =>
|
||||
setSelected((current) =>
|
||||
checked
|
||||
? [...current, permission.codename]
|
||||
: current.filter((code) => code !== permission.codename)
|
||||
)
|
||||
}
|
||||
/>
|
||||
<span>
|
||||
<span className="block text-foreground">{permission.name}</span>
|
||||
<span className="text-xs text-muted-foreground">{permission.codename}</span>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>取消</Button>
|
||||
<Button onClick={() => void handleSave()}>保存权限</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user