Files
admin/src/app/(root)/roles/assign-permissions.tsx

214 lines
5.4 KiB
TypeScript

import { useCallback, useEffect, useState } from "react"
import { toast } from "sonner"
import { getAllPermissions } from "@/actions/permission"
import { updateRole } from "@/actions/role"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { cn } from "@/lib/utils"
import type { Role } from "@/models/role"
type TreeNode = {
id: number
name: string
description: string
children: TreeNode[]
}
function PermissionRow({
node,
depth,
selected,
onToggle,
}: {
node: TreeNode
depth: number
selected: Set<number>
onToggle: (id: number, checked: boolean) => void
}) {
const hasChildren = node.children.length > 0
return (
<>
<div className={cn("flex items-center gap-2 h-8", hasChildren && "mt-2")}>
<div style={{ width: depth * 16 }} />
<Checkbox
id={`perm-${node.id}`}
checked={selected.has(node.id)}
onCheckedChange={checked => onToggle(node.id, checked === true)}
/>
<label
htmlFor={`perm-${node.id}`}
className={cn(
"cursor-pointer select-none text-sm size-full flex items-center",
hasChildren && "font-bold",
)}
>
{node.description}
</label>
</div>
{node.children.map(child => (
<PermissionRow
key={child.id}
node={child}
depth={depth + 1}
selected={selected}
onToggle={onToggle}
/>
))}
</>
)
}
export function AssignPermissions(props: {
role: Role
onSuccess?: () => void
}) {
const [open, setOpen] = useState(false)
const [loading, setLoading] = useState(false)
const [submitting, setSubmitting] = useState(false)
const [nodes, setNodes] = useState<TreeNode[]>([])
const [selected, setSelected] = useState<Set<number>>(new Set())
const fetchPermissions = useCallback(async () => {
setLoading(true)
try {
const resp = await getAllPermissions()
if (!resp.success) throw new Error(resp.message)
const data = resp.data ?? []
const map = new Map<number, TreeNode>()
data.forEach(p => {
map.set(p.id, {
id: p.id,
name: p.name,
description: p.description,
children: [],
})
})
const roots: TreeNode[] = []
data.forEach(p => {
const node = map.get(p.id)
if (!node) return
if (!p.parent_id) {
roots.push(node)
} else {
map.get(p.parent_id)?.children.push(node)
}
})
setNodes(roots)
} catch (error) {
toast.error(error instanceof Error ? error.message : "获取权限列表失败")
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
if (open) fetchPermissions()
}, [open, fetchPermissions])
const handleToggle = useCallback((id: number, checked: boolean) => {
setSelected(prev => {
const next = new Set(prev)
if (checked) {
next.add(id)
} else {
next.delete(id)
}
return next
})
}, [])
const handleSubmit = async () => {
setSubmitting(true)
try {
const resp = await updateRole({
id: props.role.id,
permissions: Array.from(selected),
})
if (resp.success) {
toast.success("权限分配成功")
props.onSuccess?.()
setOpen(false)
} else {
toast.error(resp.message ?? "权限分配失败")
}
} catch (error) {
toast.error(error instanceof Error ? error.message : "接口请求错误")
} finally {
setSubmitting(false)
}
}
const handleOpenChange = (value: boolean) => {
if (value) {
setSelected(new Set((props.role.permissions ?? []).map(p => p.id)))
} else {
setSelected(new Set())
setNodes([])
}
setOpen(value)
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
<Button size="sm" variant="secondary">
</Button>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle> · {props.role.name}</DialogTitle>
</DialogHeader>
<div className="max-h-[60vh] overflow-y-auto pr-1">
{loading ? (
<div className="py-8 text-center text-sm text-muted-foreground">
...
</div>
) : nodes.length === 0 ? (
<div className="py-8 text-center text-sm text-muted-foreground">
</div>
) : (
<div className="flex flex-col">
{nodes.map(node => (
<PermissionRow
key={node.id}
node={node}
depth={0}
selected={selected}
onToggle={handleToggle}
/>
))}
</div>
)}
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="ghost"></Button>
</DialogClose>
<Button onClick={handleSubmit} disabled={submitting || loading}>
{submitting ? "保存中..." : "保存"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}