Files
hair-keeper/src/app/(main)/users/user-info/components/RoleManagementDialog.tsx

397 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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, limit: 1 }}
>
<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">
&quot;{role.name}&quot;
</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>
)
}