Files
stu-ai-demo/src/app/(main)/users/user-info/components/RoleManagementDialog.tsx
liuyh 5020bd1532 feat: Hair Keeper v1.1.0 版本更新
本次更新包含以下主要改进:

## 新功能
- 添加quickstart.sh脚本帮助用户快速使用模板项目
- 添加simple_deploy.sh便于部署
- 新增院系管理功能(DeptAdmin),支持增删改查院系管理员信息
- 用户可以在header中切换管理的院系
- 添加zustand全局状态管理
- 添加DEFAULT_USER_PASSWORD环境变量,作为创建用户时的默认密码
- 添加p-limit库和DB_PARALLEL_LIMIT环境变量控制数据库批次操作并发数

## 安全修复
- 修复Next.js CVE-2025-66478漏洞
- 限制只有超级管理员才能创建超级管理员用户

## 开发环境优化
- 开发终端兼容云端环境
- MinIO客户端直传兼容云端环境
- 开发容器增加vim和Claude Code插件
- 编程代理改用Claude
- docker-compose.yml添加全局name属性

## Bug修复与代码优化
- 删除用户时级联删除SelectionLog
- 手机端关闭侧边栏后刷新页面延迟调整(300ms=>350ms)
- instrumentation.ts移至src内部以适配生产环境
- 删除部分引发类型错误的无用代码
- 优化quickstart.sh远程仓库推送相关配置

## 文件变更
- 新增49个文件,修改多个配置和源代码文件
- 重构用户管理模块目录结构

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-23 16:58:55 +08:00

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 }}
>
<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>
)
}