397 lines
14 KiB
TypeScript
397 lines
14 KiB
TypeScript
|
||
'use client'
|
||
|
||
import React, { useState } from 'react'
|
||
import { trpc } from '@/lib/trpc'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Input } from '@/components/ui/input'
|
||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
||
import { Badge } from '@/components/ui/badge'
|
||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||
import {
|
||
AdvancedSelect,
|
||
SelectPopover,
|
||
SelectTrigger,
|
||
SelectContent,
|
||
SelectInput,
|
||
SelectItemList,
|
||
SelectedBadges
|
||
} from '@/components/common/advanced-select'
|
||
import { Settings, Edit, Trash2, Save, X, Plus, Check } from 'lucide-react'
|
||
import { toast } from 'sonner'
|
||
|
||
interface RoleData {
|
||
id: number
|
||
name: string
|
||
userCount: number
|
||
permissions: Array<{ id: number; name: string }>
|
||
}
|
||
|
||
interface EditingRole {
|
||
id: number | null
|
||
name: string
|
||
permissionIds: number[]
|
||
}
|
||
|
||
export function RoleManagementDialog() {
|
||
const [isOpen, setIsOpen] = useState(false)
|
||
const [editingRole, setEditingRole] = useState<EditingRole | null>(null)
|
||
const [isAddingNew, setIsAddingNew] = useState(false)
|
||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState<number | null>(null)
|
||
const utils = trpc.useUtils()
|
||
|
||
// 获取角色列表和权限列表
|
||
const { data: roles = [], refetch: refetchRoles } = trpc.users.getRolesWithStats.useQuery(undefined, {
|
||
enabled: isOpen
|
||
})
|
||
const { data: permissions = [] } = trpc.users.getPermissions.useQuery(undefined, {
|
||
enabled: isOpen
|
||
})
|
||
|
||
// 创建角色
|
||
const createRoleMutation = trpc.users.createRole.useMutation({
|
||
onSuccess: () => {
|
||
toast.success('角色创建成功')
|
||
refetchRoles()
|
||
utils.users.getRoles.invalidate()
|
||
setIsAddingNew(false)
|
||
setEditingRole(null)
|
||
},
|
||
onError: (error) => {
|
||
toast.error(error.message || '创建角色失败')
|
||
}
|
||
})
|
||
|
||
// 更新角色
|
||
const updateRoleMutation = trpc.users.updateRole.useMutation({
|
||
onSuccess: () => {
|
||
toast.success('角色更新成功')
|
||
refetchRoles()
|
||
utils.users.getRoles.invalidate()
|
||
setEditingRole(null)
|
||
},
|
||
onError: (error) => {
|
||
toast.error(error.message || '更新角色失败')
|
||
}
|
||
})
|
||
|
||
// 删除角色
|
||
const deleteRoleMutation = trpc.users.deleteRole.useMutation({
|
||
onSuccess: () => {
|
||
toast.success('角色删除成功')
|
||
refetchRoles()
|
||
utils.users.getRoles.invalidate()
|
||
setDeleteConfirmOpen(null)
|
||
},
|
||
onError: (error) => {
|
||
toast.error(error.message || '删除角色失败')
|
||
}
|
||
})
|
||
|
||
// 开始编辑角色
|
||
const handleEditRole = (role: RoleData) => {
|
||
setEditingRole({
|
||
id: role.id,
|
||
name: role.name,
|
||
permissionIds: role.permissions.map(p => p.id)
|
||
})
|
||
setIsAddingNew(false)
|
||
}
|
||
|
||
// 开始新增角色
|
||
const handleAddNewRole = () => {
|
||
setEditingRole({
|
||
id: null,
|
||
name: '',
|
||
permissionIds: []
|
||
})
|
||
setIsAddingNew(true)
|
||
}
|
||
|
||
// 取消编辑
|
||
const handleCancelEdit = () => {
|
||
setEditingRole(null)
|
||
setIsAddingNew(false)
|
||
}
|
||
|
||
// 保存角色
|
||
const handleSaveRole = () => {
|
||
if (!editingRole) return
|
||
|
||
if (!editingRole.name.trim()) {
|
||
toast.error('角色名称不能为空')
|
||
return
|
||
}
|
||
|
||
if (editingRole.id === null) {
|
||
// 新增角色
|
||
createRoleMutation.mutate({
|
||
name: editingRole.name.trim(),
|
||
permissionIds: editingRole.permissionIds
|
||
})
|
||
} else {
|
||
// 更新角色
|
||
updateRoleMutation.mutate({
|
||
id: editingRole.id,
|
||
name: editingRole.name.trim(),
|
||
permissionIds: editingRole.permissionIds
|
||
})
|
||
}
|
||
}
|
||
|
||
// 删除角色
|
||
const handleDeleteRole = (roleId: number) => {
|
||
deleteRoleMutation.mutate({ id: roleId })
|
||
}
|
||
|
||
// 处理权限选择变化
|
||
const handlePermissionChange = (permissionIds: string | undefined | string[]) => {
|
||
if (!editingRole) return
|
||
|
||
const ids = Array.isArray(permissionIds) ? permissionIds : []
|
||
setEditingRole(prev => {
|
||
if (!prev) return prev
|
||
return { ...prev, permissionIds: ids.map(Number) }
|
||
})
|
||
}
|
||
|
||
return (
|
||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||
<DialogTrigger asChild>
|
||
<Button variant="outline" className="flex items-center gap-2">
|
||
<Settings className="h-4 w-4" />
|
||
角色管理
|
||
</Button>
|
||
</DialogTrigger>
|
||
<DialogContent className="max-w-5xl sm:max-w-5xl max-h-[90vh] overflow-y-auto">
|
||
<DialogHeader>
|
||
<DialogTitle>角色管理</DialogTitle>
|
||
<DialogDescription>
|
||
管理系统中的角色和权限分配
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead className="w-16">ID</TableHead>
|
||
<TableHead className="w-48">角色名称</TableHead>
|
||
<TableHead className="w-24">用户数量</TableHead>
|
||
<TableHead className="w-96">权限</TableHead>
|
||
<TableHead className="w-32">操作</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{roles.map((role) => (
|
||
<TableRow key={role.id}>
|
||
<TableCell>{role.id}</TableCell>
|
||
<TableCell>
|
||
{editingRole?.id === role.id ? (
|
||
<Input
|
||
value={editingRole.name}
|
||
onChange={(e) =>
|
||
setEditingRole(prev => prev ? { ...prev, name: e.target.value } : null)
|
||
}
|
||
placeholder="输入角色名称"
|
||
className="w-full"
|
||
/>
|
||
) : (
|
||
role.name
|
||
)}
|
||
</TableCell>
|
||
<TableCell>{role.userCount}</TableCell>
|
||
<TableCell>
|
||
{editingRole?.id === role.id ? (
|
||
<AdvancedSelect
|
||
options={permissions.map(p => ({ ...p, id: p.id.toString() }))}
|
||
value={editingRole.permissionIds.map(String)}
|
||
onChange={handlePermissionChange}
|
||
multiple={{ enable: true }}
|
||
>
|
||
<SelectPopover>
|
||
<SelectTrigger placeholder="选择权限">
|
||
<SelectedBadges maxDisplay={2} />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectInput placeholder="搜索权限..." />
|
||
<SelectItemList />
|
||
</SelectContent>
|
||
</SelectPopover>
|
||
</AdvancedSelect>
|
||
) : (
|
||
<div className="flex flex-wrap gap-1 max-w-xs">
|
||
{role.permissions.map((perm) => (
|
||
<Badge key={perm.id} variant="outline" className="text-xs">
|
||
{perm.name}
|
||
</Badge>
|
||
))}
|
||
</div>
|
||
)}
|
||
</TableCell>
|
||
<TableCell>
|
||
{editingRole?.id === role.id ? (
|
||
<div className="flex items-center gap-2">
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={handleSaveRole}
|
||
disabled={updateRoleMutation.isPending}
|
||
className="text-green-600 hover:text-green-700 hover:bg-green-50 p-2"
|
||
>
|
||
<Save className="h-3 w-3" />
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={handleCancelEdit}
|
||
className="text-red-600 hover:text-red-700 hover:bg-red-50 p-2"
|
||
>
|
||
<X className="h-3 w-3" />
|
||
</Button>
|
||
</div>
|
||
) : (
|
||
<div className="flex items-center gap-2">
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => handleEditRole(role)}
|
||
disabled={editingRole !== null || isAddingNew}
|
||
className="p-2"
|
||
>
|
||
<Edit className="h-3 w-3" />
|
||
</Button>
|
||
{role.userCount === 0 && (
|
||
<Popover
|
||
open={deleteConfirmOpen === role.id}
|
||
onOpenChange={(open) => setDeleteConfirmOpen(open ? role.id : null)}
|
||
>
|
||
<PopoverTrigger asChild>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
disabled={deleteRoleMutation.isPending}
|
||
className="text-red-600 hover:text-red-700 hover:bg-red-50 p-2"
|
||
>
|
||
<Trash2 className="h-3 w-3" />
|
||
</Button>
|
||
</PopoverTrigger>
|
||
<PopoverContent className="w-80">
|
||
<div className="space-y-4">
|
||
<div className="space-y-2">
|
||
<h4 className="font-medium leading-none">确认删除</h4>
|
||
<p className="text-sm text-muted-foreground">
|
||
确定要删除角色 "{role.name}" 吗?
|
||
</p>
|
||
</div>
|
||
<div className="flex justify-end gap-2">
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => setDeleteConfirmOpen(null)}
|
||
>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
variant="destructive"
|
||
size="sm"
|
||
onClick={() => handleDeleteRole(role.id)}
|
||
disabled={deleteRoleMutation.isPending}
|
||
>
|
||
确认删除
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</PopoverContent>
|
||
</Popover>
|
||
)}
|
||
</div>
|
||
)}
|
||
</TableCell>
|
||
</TableRow>
|
||
))}
|
||
|
||
{/* 新增角色行 */}
|
||
<TableRow>
|
||
{isAddingNew && editingRole ? (
|
||
<>
|
||
<TableCell>
|
||
<Plus className="h-4 w-4 text-gray-400" />
|
||
</TableCell>
|
||
<TableCell>
|
||
<Input
|
||
value={editingRole.name}
|
||
onChange={(e) =>
|
||
setEditingRole(prev => prev ? { ...prev, name: e.target.value } : null)
|
||
}
|
||
placeholder="输入角色名称"
|
||
className="w-full"
|
||
/>
|
||
</TableCell>
|
||
<TableCell>0</TableCell>
|
||
<TableCell>
|
||
<AdvancedSelect
|
||
options={permissions.map(p => ({ ...p, id: p.id.toString() }))}
|
||
value={editingRole.permissionIds.map(String)}
|
||
onChange={handlePermissionChange}
|
||
multiple={{ enable: true }}
|
||
>
|
||
<SelectPopover>
|
||
<SelectTrigger placeholder="选择权限">
|
||
<SelectedBadges maxDisplay={2} />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectInput placeholder="搜索权限..." />
|
||
<SelectItemList />
|
||
</SelectContent>
|
||
</SelectPopover>
|
||
</AdvancedSelect>
|
||
</TableCell>
|
||
<TableCell>
|
||
<div className="flex items-center gap-2">
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={handleSaveRole}
|
||
disabled={createRoleMutation.isPending}
|
||
className="bg-green-600 hover:bg-green-700 text-white p-2"
|
||
>
|
||
<Check className="h-3 w-3" />
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={handleCancelEdit}
|
||
className="text-red-600 hover:text-red-700 hover:bg-red-50 p-2"
|
||
>
|
||
<X className="h-3 w-3" />
|
||
</Button>
|
||
</div>
|
||
</TableCell>
|
||
</>
|
||
) : (
|
||
<>
|
||
<TableCell>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={handleAddNewRole}
|
||
disabled={editingRole !== null}
|
||
className="p-2 hover:bg-gray-100"
|
||
>
|
||
<Plus className="h-4 w-4 text-gray-600" />
|
||
</Button>
|
||
</TableCell>
|
||
<TableCell className="text-gray-400">点击+新增角色</TableCell>
|
||
<TableCell>-</TableCell>
|
||
<TableCell>-</TableCell>
|
||
<TableCell>-</TableCell>
|
||
</>
|
||
)}
|
||
</TableRow>
|
||
</TableBody>
|
||
</Table>
|
||
</DialogContent>
|
||
</Dialog>
|
||
)
|
||
} |