完善权限树的展示与选择

This commit is contained in:
2026-03-28 17:07:34 +08:00
parent 1e6b307586
commit 2c7970796f
5 changed files with 209 additions and 97 deletions

View File

@@ -1,19 +1,136 @@
"use client"
import { Suspense } from "react"
import { getPagePermission } from "@/actions/permission"
import { DataTable, useDataTable } from "@/components/data-table"
import {
flexRender,
getCoreRowModel,
getExpandedRowModel,
type Row,
useReactTable,
} from "@tanstack/react-table"
import { Suspense, useCallback, useEffect, useState } from "react"
import { toast } from "sonner"
import { getAllPermissions } from "@/actions/permission"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { cn } from "@/lib/utils"
type Node = {
id: number
name: string
description: string
children: Node[]
}
export default function PermissionsPage() {
const table = useDataTable((page, size) => getPagePermission({ page, size }))
return (
<Suspense>
<DataTable
{...table}
columns={[
{ header: "编码", accessorKey: "name" },
{ header: "描述", accessorKey: "description" },
]}
/>
<PermissionTable />
</Suspense>
)
}
function PermissionTable() {
const [data, setData] = useState<Node[]>([])
const table = useReactTable({
getCoreRowModel: getCoreRowModel(),
getExpandedRowModel: getExpandedRowModel(),
data,
columns: [
{ header: "编码", accessorKey: "name" },
{ header: "描述", accessorKey: "description" },
],
getSubRows: row => row.children,
})
const refresh = useCallback(async () => {
try {
const resp = await getAllPermissions()
if (!resp.success) {
throw new Error(resp.message)
}
const map = new Map<number, Node>()
resp.data.forEach(permission => {
map.set(permission.id, {
id: permission.id,
name: permission.name,
description: permission.description,
children: [],
})
})
const roots: Node[] = []
resp.data.forEach(permission => {
const node = map.get(permission.id)
if (!node) {
throw new Error(`找不到权限节点: ${permission.name}`)
}
if (!permission.parent_id) {
roots.push(node)
return
}
const parent = map.get(permission.parent_id)
if (!parent) {
throw new Error(`找不到父权限节点: ${permission.name}`)
}
parent.children.push(node)
})
setData(roots)
console.log(roots)
} catch (e) {
toast.error(e instanceof Error ? e.message : "获取权限列表失败")
}
}, [])
useEffect(() => {
refresh()
}, [refresh])
return (
<div className="bg-background rounded-lg">
<Table>
<TableHeader>
<TableRow className="h-10">
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{table.getRowModel().rows.map(row => (
<PermisionRow key={row.id} row={row} />
))}
</TableBody>
</Table>
</div>
)
}
function PermisionRow(props: { row: Row<Node> }) {
const row = props.row
return (
<>
<TableRow key={row.id} className="h-10">
<TableCell className="flex">
<div style={{ width: row.depth * 16 }}></div>
<span className={cn(row.subRows.length ? "font-bold" : "text-sm")}>
{row.original.name}
</span>
</TableCell>
<TableCell>{row.original.description}</TableCell>
</TableRow>
{row.subRows.map(subRow => (
<PermisionRow key={subRow.id} row={subRow} />
))}
</>
)
}

View File

@@ -13,81 +13,58 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import type { Permission } from "@/models/permission"
import { cn } from "@/lib/utils"
import type { Role } from "@/models/role"
function PermissionItem({
permission,
type TreeNode = {
id: number
name: string
description: string
children: TreeNode[]
}
function PermissionRow({
node,
depth,
selected,
onToggle,
}: {
permission: Permission
node: TreeNode
depth: number
selected: Set<number>
onToggle: (id: number, checked: boolean) => void
}) {
const hasChildren = permission.children && permission.children.length > 0
const allChildIds = (p: Permission): number[] => {
if (!p.children || p.children.length === 0) return [p.id]
return [p.id, ...p.children.flatMap(allChildIds)]
}
const childIds = hasChildren ? allChildIds(permission).slice(1) : []
const allChecked =
hasChildren && childIds.length > 0
? childIds.every(id => selected.has(id))
: selected.has(permission.id)
const someChecked =
hasChildren && childIds.length > 0
? childIds.some(id => selected.has(id)) && !allChecked
: false
const handleChange = (checked: boolean) => {
if (hasChildren) {
const ids = allChildIds(permission)
for (const id of ids) {
onToggle(id, checked)
}
} else {
onToggle(permission.id, checked)
}
}
const checkboxId = `perm-${permission.id}`
const hasChildren = node.children.length > 0
return (
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2 py-0.5">
<>
<div className={cn("flex items-center gap-2 h-8", hasChildren && "mt-2")}>
<div style={{ width: depth * 16 }} />
<Checkbox
id={checkboxId}
checked={someChecked ? "indeterminate" : allChecked}
onCheckedChange={checked => handleChange(checked === true)}
id={`perm-${node.id}`}
checked={selected.has(node.id)}
onCheckedChange={checked => onToggle(node.id, checked === true)}
/>
<label
htmlFor={checkboxId}
className="flex items-center gap-1.5 cursor-pointer select-none text-sm"
>
{permission.name}
{permission.description && (
<span className="text-xs text-muted-foreground">
{permission.description}
</span>
htmlFor={`perm-${node.id}`}
className={cn(
"cursor-pointer select-none text-sm size-full flex items-center",
hasChildren && "font-bold",
)}
>
{node.description}
</label>
</div>
{hasChildren && (
<div className="pl-6 flex flex-col gap-1 border-l ml-2">
{permission.children.map(child => (
<PermissionItem
key={child.id}
permission={child}
selected={selected}
onToggle={onToggle}
/>
))}
</div>
)}
</div>
{node.children.map(child => (
<PermissionRow
key={child.id}
node={child}
depth={depth + 1}
selected={selected}
onToggle={onToggle}
/>
))}
</>
)
}
@@ -98,33 +75,51 @@ export function AssignPermissions(props: {
const [open, setOpen] = useState(false)
const [loading, setLoading] = useState(false)
const [submitting, setSubmitting] = useState(false)
const [permissions, setPermissions] = useState<Permission[]>([])
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) {
setPermissions(resp.data ?? [])
} else {
toast.error(resp.message ?? "获取权限列表失败")
}
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) {
const message = error instanceof Error ? error.message : error
toast.error(`接口请求错误: ${message}`)
toast.error(error instanceof Error ? error.message : "获取权限列表失败")
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
if (open) {
fetchPermissions()
}
if (open) fetchPermissions()
}, [open, fetchPermissions])
const handleToggle = (id: number, checked: boolean) => {
const handleToggle = useCallback((id: number, checked: boolean) => {
setSelected(prev => {
const next = new Set(prev)
if (checked) {
@@ -134,7 +129,7 @@ export function AssignPermissions(props: {
}
return next
})
}
}, [])
const handleSubmit = async () => {
setSubmitting(true)
@@ -151,8 +146,7 @@ export function AssignPermissions(props: {
toast.error(resp.message ?? "权限分配失败")
}
} catch (error) {
const message = error instanceof Error ? error.message : error
toast.error(`接口请求错误: ${message}`)
toast.error(error instanceof Error ? error.message : "接口请求错误")
} finally {
setSubmitting(false)
}
@@ -160,11 +154,10 @@ export function AssignPermissions(props: {
const handleOpenChange = (value: boolean) => {
if (value) {
const existingIds = new Set((props.role.permissions ?? []).map(p => p.id))
setSelected(existingIds)
setSelected(new Set((props.role.permissions ?? []).map(p => p.id)))
} else {
setSelected(new Set())
setPermissions([])
setNodes([])
}
setOpen(value)
}
@@ -187,16 +180,17 @@ export function AssignPermissions(props: {
<div className="py-8 text-center text-sm text-muted-foreground">
...
</div>
) : permissions.length === 0 ? (
) : nodes.length === 0 ? (
<div className="py-8 text-center text-sm text-muted-foreground">
</div>
) : (
<div className="flex flex-col gap-2">
{permissions.map(permission => (
<PermissionItem
key={permission.id}
permission={permission}
<div className="flex flex-col">
{nodes.map(node => (
<PermissionRow
key={node.id}
node={node}
depth={0}
selected={selected}
onToggle={handleToggle}
/>

View File

@@ -94,7 +94,7 @@ function PermissionsCell({ permissions }: { permissions: Permission[] }) {
<div className="flex flex-wrap gap-1 cursor-default max-w-52">
{preview.map(p => (
<Badge key={p.id} variant="secondary">
{p.name}
{p.description}
</Badge>
))}
{rest > 0 && <Badge variant="outline">+{rest}</Badge>}
@@ -107,7 +107,7 @@ function PermissionsCell({ permissions }: { permissions: Permission[] }) {
<div className="flex flex-wrap gap-1.5">
{permissions.map(p => (
<Badge key={p.id} variant="secondary">
{p.name}
{p.description}
</Badge>
))}
</div>

View File

@@ -3,6 +3,7 @@ import {
type ColumnDef,
flexRender,
getCoreRowModel,
getExpandedRowModel,
useReactTable,
} from "@tanstack/react-table"
import { Loader } from "lucide-react"

View File

@@ -4,7 +4,7 @@ export type Permission = {
updated_at: Date
expired_at: Date
parent_id: number
parent_id?: number
name: string
description: string
parent: number