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:
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { User, LogOut, KeyRound } from 'lucide-react'
|
||||
import { DevPanel } from '@/app/(main)/dev/panel'
|
||||
import { ChangePasswordDialog } from '@/components/layout/change-password-dialog'
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { SidebarTrigger } from '@/components/ui/sidebar'
|
||||
import { signOut } from 'next-auth/react'
|
||||
@@ -23,6 +22,11 @@ import { useTheme } from 'next-themes'
|
||||
import { ThemeToggleButton, useThemeTransition } from '@/components/common/theme-toggle-button'
|
||||
import { getMenuTitle } from '@/constants/menu'
|
||||
import type { User as AppUser } from '@/types/user'
|
||||
import { useUserStore } from '@/lib/stores/userStore'
|
||||
import { toast } from 'sonner'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
import { AdvancedSelect, SelectContent, SelectedName, SelectInput, SelectItemList, SelectPopover, SelectTrigger } from '@/components/common/advanced-select'
|
||||
import type { Dept } from '@prisma/client'
|
||||
|
||||
interface HeaderProps {
|
||||
user?: AppUser
|
||||
@@ -38,6 +42,47 @@ export function Header({ user }: HeaderProps) {
|
||||
|
||||
const pageTitle = getMenuTitle(pathname, 2) // 只匹配到第二级菜单
|
||||
|
||||
// 从zustand store获取当前管理的院系信息
|
||||
const { setCurrentManagedDept } = useUserStore()
|
||||
|
||||
// 获取可管理的院系列表(包含当前管理的院系信息)
|
||||
const { data: managedDeptsData } = trpc.users.getManagedDepts.useQuery()
|
||||
|
||||
// 本地状态管理
|
||||
const [currentManagedDeptCode, setCurrentManagedDeptCode] = useState<string | null>(null)
|
||||
const [managedDepts, setManagedDepts] = useState<Array<Dept>>([])
|
||||
|
||||
// 初始化数据
|
||||
useEffect(() => {
|
||||
if (managedDeptsData && managedDeptsData.currentDept !== undefined) {
|
||||
setCurrentManagedDeptCode(managedDeptsData.currentDept)
|
||||
setManagedDepts(managedDeptsData.depts)
|
||||
|
||||
// 更新store中的当前管理院系信息
|
||||
const deptInfo = managedDeptsData.depts.find(dept => dept.code === managedDeptsData.currentDept) || null
|
||||
setCurrentManagedDept(deptInfo)
|
||||
}
|
||||
}, [managedDeptsData, setCurrentManagedDept])
|
||||
|
||||
// 切换管理院系
|
||||
const switchManagedDeptMutation = trpc.users.switchManagedDept.useMutation({
|
||||
onSuccess: (data) => {
|
||||
toast.success('切换管理院系成功')
|
||||
// 更新本地状态
|
||||
setCurrentManagedDeptCode(data.deptCode)
|
||||
// 更新store中的当前管理院系信息
|
||||
setCurrentManagedDept(managedDepts.find(dept => dept.code === data.deptCode) || null)
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message || '切换管理院系失败')
|
||||
},
|
||||
})
|
||||
|
||||
// 处理院系切换
|
||||
const handleDeptChange = (deptCode: string | null) => {
|
||||
switchManagedDeptMutation.mutate({ deptCode })
|
||||
}
|
||||
|
||||
const handleThemeToggle = () => {
|
||||
startTransition(() => {
|
||||
setTheme(theme === 'dark' ? 'light' : 'dark')
|
||||
@@ -105,6 +150,52 @@ export function Header({ user }: HeaderProps) {
|
||||
<DropdownMenuItem disabled>
|
||||
{user.isSuperAdmin ? '超级管理员' : (Array.isArray(user.roles) ? user.roles.join('、') : user.roles)}
|
||||
</DropdownMenuItem>
|
||||
{/* 管理院系 - 根据可管理院系数量决定显示方式 */}
|
||||
{managedDepts.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>管理院系</DropdownMenuLabel>
|
||||
{managedDepts.length === 1 ? (
|
||||
// 只有一个可管理院系时,直接显示院系名称
|
||||
<DropdownMenuItem disabled>
|
||||
{managedDepts[0].fullName}
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
// 多个可管理院系时,显示下拉选择器
|
||||
<div className="px-2 py-1.5">
|
||||
<AdvancedSelect
|
||||
value={currentManagedDeptCode}
|
||||
onChange={handleDeptChange}
|
||||
options={managedDepts.map(dept => ({
|
||||
id: dept.code,
|
||||
name: dept.fullName,
|
||||
shortName: dept.name
|
||||
}))}
|
||||
disabled={switchManagedDeptMutation.isPending}
|
||||
filterFunction={(option, searchValue) => {
|
||||
const search = searchValue.toLowerCase()
|
||||
return option.id.includes(search) ||
|
||||
option.name.toLowerCase().includes(search) ||
|
||||
(option.shortName && option.shortName.toLowerCase().includes(search))
|
||||
}}
|
||||
>
|
||||
<SelectPopover>
|
||||
<SelectTrigger
|
||||
placeholder="请选择管理院系"
|
||||
className="h-9"
|
||||
>
|
||||
<SelectedName />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{managedDepts.length > 5 && <SelectInput placeholder="搜索院系名称/代码" />}
|
||||
<SelectItemList />
|
||||
</SelectContent>
|
||||
</SelectPopover>
|
||||
</AdvancedSelect>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-red-600 cursor-pointer"
|
||||
@@ -131,4 +222,4 @@ export function Header({ user }: HeaderProps) {
|
||||
/>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user