Files
ai-ticket-platform/frontend/src/features/system/SystemRolesPage.tsx
2026-07-01 14:19:59 +08:00

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>
)
}