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>
This commit is contained in:
2025-12-23 16:58:55 +08:00
parent 42be39b343
commit 5020bd1532
49 changed files with 2209 additions and 290 deletions

View File

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