178 lines
6.3 KiB
TypeScript
178 lines
6.3 KiB
TypeScript
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>
|
|
)
|
|
}
|