feat: 增加DEFAULT_USER_PASSWORD,作为创建用户时的默认密码,添加p-limit库,添加DB_PARALLEL_LIMIT = 32环境变量作为“数据库批次操作默认并发数” 限制只有超级管理员才能创建超级管理员用户 删除用户时可以级联删除SelectionLog 添加zustand全局状态管理 一对多的院系管理功能 ,支持增删改查院系管理员信息、用户可以在header中切换管理的院系
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
|
||||
'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">
|
||||
确定要删除角色 "{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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user