226 lines
8.9 KiB
TypeScript
226 lines
8.9 KiB
TypeScript
'use client'
|
||
|
||
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'
|
||
import { UserProfileDialog } from '@/components/layout/user-profile-dialog'
|
||
import { Button } from '@/components/ui/button'
|
||
import {
|
||
DropdownMenu,
|
||
DropdownMenuContent,
|
||
DropdownMenuItem,
|
||
DropdownMenuLabel,
|
||
DropdownMenuSeparator,
|
||
DropdownMenuTrigger,
|
||
} from '@/components/ui/dropdown-menu'
|
||
import { Separator } from '@/components/ui/separator'
|
||
import { SidebarTrigger } from '@/components/ui/sidebar'
|
||
import { signOut } from 'next-auth/react'
|
||
import { useRouter, usePathname } from 'next/navigation'
|
||
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
|
||
}
|
||
|
||
export function Header({ user }: HeaderProps) {
|
||
const router = useRouter()
|
||
const pathname = usePathname()
|
||
const { theme, setTheme } = useTheme()
|
||
const { startTransition } = useThemeTransition()
|
||
const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false)
|
||
const [isUserProfileOpen, setIsUserProfileOpen] = useState(false)
|
||
|
||
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')
|
||
})
|
||
}
|
||
|
||
const handleLogout = async () => {
|
||
await signOut({ redirect: false })
|
||
router.push('/login')
|
||
}
|
||
|
||
// 如果没有用户信息,不显示Header(应该被中间件重定向)
|
||
if (!user) {
|
||
return null
|
||
}
|
||
|
||
return (
|
||
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
|
||
<SidebarTrigger />
|
||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||
<div className="flex items-center justify-between flex-1">
|
||
<h2 className="text-lg font-semibold">
|
||
{pageTitle}
|
||
</h2>
|
||
<div className="flex items-center space-x-4">
|
||
{/* 主题切换按钮 */}
|
||
<ThemeToggleButton
|
||
theme={theme === 'dark' ? 'dark' : 'light'}
|
||
variant="polygon"
|
||
onClick={handleThemeToggle}
|
||
className="border-0 shadow-none"
|
||
/>
|
||
|
||
{/* 开发者工具按钮 - 仅开发环境 */}
|
||
{process.env.NODE_ENV === 'development' && user.isSuperAdmin && <DevPanel />}
|
||
|
||
{/* 用户菜单 */}
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="ghost" className="flex items-center space-x-2 px-3">
|
||
<div className="w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center">
|
||
<User className="h-4 w-4 text-primary" />
|
||
</div>
|
||
<div className="text-left hidden sm:block">
|
||
<p className="text-sm font-medium">{user.name}</p>
|
||
<p className="text-xs text-muted-foreground">
|
||
{user.isSuperAdmin ? '超级管理员' : (Array.isArray(user.roles) ? user.roles.join('、') : user.roles)}
|
||
</p>
|
||
</div>
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="end" className="w-56">
|
||
<DropdownMenuLabel>我的账户</DropdownMenuLabel>
|
||
<DropdownMenuSeparator />
|
||
<DropdownMenuItem onClick={() => setIsUserProfileOpen(true)}>
|
||
<User className="mr-2 h-4 w-4" />
|
||
个人资料
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem onClick={() => setIsChangePasswordOpen(true)}>
|
||
<KeyRound className="mr-2 h-4 w-4" />
|
||
修改密码
|
||
</DropdownMenuItem>
|
||
<DropdownMenuSeparator />
|
||
<DropdownMenuLabel>角色</DropdownMenuLabel>
|
||
<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"
|
||
onClick={handleLogout}
|
||
>
|
||
<LogOut className="mr-2 h-4 w-4" />
|
||
退出登录
|
||
</DropdownMenuItem>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 修改密码对话框 */}
|
||
<ChangePasswordDialog
|
||
isOpen={isChangePasswordOpen}
|
||
onClose={() => setIsChangePasswordOpen(false)}
|
||
/>
|
||
|
||
{/* 用户资料对话框 */}
|
||
<UserProfileDialog
|
||
isOpen={isUserProfileOpen}
|
||
onClose={() => setIsUserProfileOpen(false)}
|
||
/>
|
||
</header>
|
||
)
|
||
}
|