214 lines
5.4 KiB
TypeScript
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>
|
|
)
|
|
}
|