602 lines
24 KiB
TypeScript
602 lines
24 KiB
TypeScript
"use client"
|
|
|
|
import Link from "next/link"
|
|
import { BookOpen, Search, Settings, LogOut, User, ChevronDown, Plus, Trash2, Network, LayoutGrid, Calculator, HelpCircle, Calendar, Menu, X, Home, Users as UsersIcon, Clock, TreePine } from "lucide-react"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu"
|
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
|
|
import { useFamily } from "@/context/family-context"
|
|
import { useState, useRef, useEffect, useCallback } from "react"
|
|
import { useRouter, usePathname, useSearchParams } from "next/navigation"
|
|
import { useSession, signOut } from "next-auth/react"
|
|
import { useDialog } from "@/components/ui/alert-dialog-custom"
|
|
import { permissions } from "@/lib/permissions"
|
|
import { solar2lunar } from "@/lib/lunar-calendar"
|
|
import { CalendarDialog } from "@/components/calendar-dialog"
|
|
import { MemberNameWithStatus } from "./member-name-with-status"
|
|
|
|
interface FamilyTree {
|
|
id: string
|
|
name: string
|
|
description: string | null
|
|
ownerId: string
|
|
currentUserRole?: "OWNER" | "EDITOR" | "VIEWER"
|
|
}
|
|
|
|
export function SiteHeader() {
|
|
const { searchMembers, searchResults, currentTree: familyCurrentTree } = useFamily()
|
|
const { data: session, status } = useSession()
|
|
const { showAlert, showConfirm, showPrompt } = useDialog()
|
|
const pathname = usePathname()
|
|
const searchParams = useSearchParams()
|
|
const [searchQuery, setSearchQuery] = useState("")
|
|
const [showResults, setShowResults] = useState(false)
|
|
const [familyTrees, setFamilyTrees] = useState<FamilyTree[]>([])
|
|
const [deletingTreeId, setDeletingTreeId] = useState<string | null>(null)
|
|
const [treeDropdownOpen, setTreeDropdownOpen] = useState(false)
|
|
const [currentDate, setCurrentDate] = useState(new Date())
|
|
const [calendarDialogOpen, setCalendarDialogOpen] = useState(false)
|
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
|
const searchRef = useRef<HTMLDivElement>(null)
|
|
const router = useRouter()
|
|
|
|
// 使用 family context 的 currentTree
|
|
const currentTree = familyCurrentTree
|
|
|
|
// 获取当前视图模式
|
|
const currentViewMode = searchParams.get('view') || 'traditional'
|
|
const isTreePage = pathname === '/tree'
|
|
|
|
// 更新日期
|
|
useEffect(() => {
|
|
const timer = setInterval(() => {
|
|
setCurrentDate(new Date())
|
|
}, 60000) // 每分钟更新一次
|
|
return () => clearInterval(timer)
|
|
}, [])
|
|
|
|
// 获取农历信息
|
|
const lunarInfo = solar2lunar(currentDate)
|
|
const solarDateStr = `${currentDate.getFullYear()}年${currentDate.getMonth() + 1}月${currentDate.getDate()}日`
|
|
const lunarDateStr = lunarInfo ? lunarInfo.toString() : '农历信息不可用'
|
|
|
|
// 生成带有 treeId 的 URL
|
|
const getUrlWithTreeId = (path: string) => {
|
|
if (currentTree?.id) {
|
|
return `${path}?treeId=${currentTree.id}`
|
|
}
|
|
return path
|
|
}
|
|
|
|
// 切换视图模式
|
|
const handleViewModeChange = (mode: string) => {
|
|
const params = new URLSearchParams(searchParams.toString())
|
|
params.set('view', mode)
|
|
if (currentTree?.id) {
|
|
params.set('treeId', currentTree.id)
|
|
}
|
|
router.push(`${pathname}?${params.toString()}`)
|
|
}
|
|
|
|
const getRoleBadge = (role?: string) => {
|
|
switch (role) {
|
|
case "OWNER":
|
|
return <span className="text-[10px] px-1.5 py-0.5 rounded bg-purple-100 text-purple-700 border border-purple-200 ml-2">所有者</span>
|
|
case "EDITOR":
|
|
return <span className="text-[10px] px-1.5 py-0.5 rounded bg-blue-100 text-blue-700 border border-blue-200 ml-2">编辑</span>
|
|
case "VIEWER":
|
|
return <span className="text-[10px] px-1.5 py-0.5 rounded bg-gray-100 text-gray-700 border border-gray-200 ml-2">查看</span>
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
|
|
const handleSignOut = async () => {
|
|
await signOut({ callbackUrl: "/auth/signin" })
|
|
}
|
|
|
|
// 获取用户名首字母作为头像
|
|
const getUserInitial = () => {
|
|
if (session?.user?.name) {
|
|
return session.user.name.charAt(0).toUpperCase()
|
|
}
|
|
if (session?.user?.email) {
|
|
return session.user.email.charAt(0).toUpperCase()
|
|
}
|
|
return "U"
|
|
}
|
|
|
|
// 删除家族树
|
|
const handleDeleteTree = async (tree: FamilyTree, e: React.MouseEvent) => {
|
|
e.stopPropagation() // 阻止触发选择事件
|
|
|
|
// 检查权限
|
|
if (tree.ownerId !== session?.user?.id) {
|
|
await showAlert('只有家族树的创建者才能删除', '权限不足')
|
|
return
|
|
}
|
|
|
|
const confirmText = `确定要删除家族树"${tree.name}"吗?\n\n此操作将永久删除所有成员、活动日志和协作者数据,且不可撤销!`
|
|
const confirmed = await showConfirm(confirmText, '删除家族树', 'destructive')
|
|
if (!confirmed) {
|
|
return
|
|
}
|
|
|
|
// 二次确认
|
|
const finalConfirm = await showPrompt(
|
|
`请输入家族树名称"${tree.name}"以确认删除:`,
|
|
'',
|
|
'确认删除'
|
|
)
|
|
if (finalConfirm !== tree.name) {
|
|
await showAlert('名称不匹配,删除已取消', '操作已取消')
|
|
return
|
|
}
|
|
|
|
setDeletingTreeId(tree.id)
|
|
try {
|
|
const response = await fetch(`/api/trees/${tree.id}`, {
|
|
method: 'DELETE',
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json()
|
|
throw new Error(error.error || '删除失败')
|
|
}
|
|
|
|
// 刷新家族树列表
|
|
await loadFamilyTrees()
|
|
|
|
// 如果删除的是当前树,跳转到首页
|
|
if (currentTree?.id === tree.id) {
|
|
window.location.href = '/'
|
|
}
|
|
} catch (error) {
|
|
console.error('删除家族树失败:', error)
|
|
await showAlert('删除失败:' + (error as Error).message, '错误')
|
|
} finally {
|
|
setDeletingTreeId(null)
|
|
}
|
|
}
|
|
|
|
// 获取家族树列表
|
|
const loadFamilyTrees = useCallback(() => {
|
|
if (!session?.user?.id || status !== 'authenticated') return
|
|
|
|
const controller = new AbortController()
|
|
|
|
fetch('/api/trees', { signal: controller.signal })
|
|
.then(res => {
|
|
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`)
|
|
return res.json()
|
|
})
|
|
.then(data => {
|
|
if (data.trees) {
|
|
setFamilyTrees(data.trees)
|
|
}
|
|
})
|
|
.catch(err => {
|
|
// 忽略 abort 错误和 fetch 错误(可能是页面切换导致的)
|
|
if (err.name !== 'AbortError' && err.message !== 'Failed to fetch') {
|
|
console.error('获取家族树列表失败:', err)
|
|
}
|
|
})
|
|
|
|
return () => controller.abort()
|
|
}, [session?.user?.id, status])
|
|
|
|
useEffect(() => {
|
|
loadFamilyTrees()
|
|
}, [loadFamilyTrees])
|
|
|
|
// 监听路由变化,刷新家族树列表
|
|
useEffect(() => {
|
|
const handleRouteChange = () => {
|
|
loadFamilyTrees()
|
|
}
|
|
|
|
window.addEventListener('focus', handleRouteChange)
|
|
return () => window.removeEventListener('focus', handleRouteChange)
|
|
}, [loadFamilyTrees])
|
|
|
|
// 点击外部关闭搜索结果
|
|
useEffect(() => {
|
|
function handleClickOutside(event: MouseEvent) {
|
|
if (searchRef.current && !searchRef.current.contains(event.target as Node)) {
|
|
setShowResults(false)
|
|
}
|
|
}
|
|
document.addEventListener("mousedown", handleClickOutside)
|
|
return () => document.removeEventListener("mousedown", handleClickOutside)
|
|
}, [])
|
|
|
|
const handleSearch = (query: string) => {
|
|
setSearchQuery(query)
|
|
searchMembers(query)
|
|
setShowResults(query.length > 0)
|
|
}
|
|
|
|
const handleSelectMember = (memberId: string) => {
|
|
const params = new URLSearchParams(window.location.search)
|
|
router.push(`/members/${memberId}?${params.toString()}`)
|
|
setSearchQuery("")
|
|
setShowResults(false)
|
|
}
|
|
|
|
// 导航菜单项配置
|
|
const navItems = [
|
|
{ href: "/", label: "总览", icon: Home },
|
|
{ href: "/tree", label: "族谱", icon: Network },
|
|
{ href: "/members", label: "成员", icon: UsersIcon },
|
|
{ href: "/timeline", label: "大事记", icon: Clock },
|
|
]
|
|
|
|
// 判断当前路由是否激活
|
|
const isActive = (href: string) => {
|
|
if (href === "/") return pathname === "/"
|
|
return pathname.startsWith(href)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<header className="sticky top-0 z-50 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
<div className="container mx-auto flex h-16 items-center px-4 gap-4">
|
|
{/* Logo */}
|
|
<div className="flex items-center gap-2">
|
|
<Link href={getUrlWithTreeId("/")} className="flex items-center gap-2 hover:opacity-80 transition-opacity">
|
|
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-primary to-primary/80 text-primary-foreground shadow-sm">
|
|
<BookOpen className="h-5 w-5" />
|
|
</div>
|
|
<span className="text-xl font-serif font-bold tracking-tight bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text">
|
|
华夏家谱
|
|
</span>
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Desktop Navigation */}
|
|
<nav className="hidden md:flex items-center gap-1 ml-6" onClick={() => setTreeDropdownOpen(false)}>
|
|
{navItems.map((item) => {
|
|
const Icon = item.icon
|
|
const active = isActive(item.href)
|
|
return (
|
|
<Link
|
|
key={item.href}
|
|
href={getUrlWithTreeId(item.href)}
|
|
className={`
|
|
relative px-4 py-2 rounded-lg font-serif text-sm font-medium
|
|
transition-all duration-200 flex items-center gap-2
|
|
${active
|
|
? 'text-primary bg-primary/10'
|
|
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
|
}
|
|
`}
|
|
>
|
|
<Icon className="h-4 w-4" />
|
|
<span>{item.label}</span>
|
|
{active && (
|
|
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-12 h-0.5 bg-primary rounded-full" />
|
|
)}
|
|
</Link>
|
|
)
|
|
})}
|
|
</nav>
|
|
|
|
{/* Mobile Menu Button */}
|
|
<Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>
|
|
<SheetTrigger asChild className="md:hidden">
|
|
<Button variant="ghost" size="icon" className="ml-auto md:ml-0">
|
|
<Menu className="h-5 w-5" />
|
|
<span className="sr-only">打开菜单</span>
|
|
</Button>
|
|
</SheetTrigger>
|
|
<SheetContent side="left" className="w-[280px] sm:w-[320px]">
|
|
<SheetHeader>
|
|
<SheetTitle className="flex items-center gap-2 font-serif">
|
|
<BookOpen className="h-5 w-5 text-primary" />
|
|
华夏家谱
|
|
</SheetTitle>
|
|
</SheetHeader>
|
|
<div className="flex flex-col gap-4 mt-8">
|
|
{/* Mobile Navigation Links */}
|
|
<div className="flex flex-col gap-1">
|
|
{navItems.map((item) => {
|
|
const Icon = item.icon
|
|
const active = isActive(item.href)
|
|
return (
|
|
<Link
|
|
key={item.href}
|
|
href={getUrlWithTreeId(item.href)}
|
|
onClick={() => setMobileMenuOpen(false)}
|
|
className={`
|
|
flex items-center gap-3 px-4 py-3 rounded-lg font-serif
|
|
transition-all duration-200
|
|
${active
|
|
? 'text-primary bg-primary/10 font-medium'
|
|
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
|
}
|
|
`}
|
|
>
|
|
<Icon className="h-5 w-5" />
|
|
<span>{item.label}</span>
|
|
</Link>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Mobile Utility Links */}
|
|
<div className="border-t pt-4 flex flex-col gap-1">
|
|
<Link
|
|
href={getUrlWithTreeId("/relationship")}
|
|
onClick={() => setMobileMenuOpen(false)}
|
|
className="flex items-center gap-3 px-4 py-3 rounded-lg font-serif text-muted-foreground hover:text-foreground hover:bg-muted transition-all"
|
|
>
|
|
<Calculator className="h-5 w-5" />
|
|
<span>关系计算</span>
|
|
</Link>
|
|
<Link
|
|
href="/settings"
|
|
onClick={() => setMobileMenuOpen(false)}
|
|
className="flex items-center gap-3 px-4 py-3 rounded-lg font-serif text-muted-foreground hover:text-foreground hover:bg-muted transition-all"
|
|
>
|
|
<Settings className="h-5 w-5" />
|
|
<span>系统设置</span>
|
|
</Link>
|
|
<Link
|
|
href="/help"
|
|
onClick={() => setMobileMenuOpen(false)}
|
|
className="flex items-center gap-3 px-4 py-3 rounded-lg font-serif text-muted-foreground hover:text-foreground hover:bg-muted transition-all"
|
|
>
|
|
<HelpCircle className="h-5 w-5" />
|
|
<span>帮助中心</span>
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Mobile User Section */}
|
|
{session && (
|
|
<div className="border-t pt-4">
|
|
<div className="px-4 py-2 text-sm text-muted-foreground">
|
|
<div className="font-medium text-foreground">{session.user?.name || "用户"}</div>
|
|
<div className="text-xs">{session.user?.email}</div>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => {
|
|
setMobileMenuOpen(false)
|
|
handleSignOut()
|
|
}}
|
|
className="w-full justify-start gap-3 px-4 py-3 text-red-600 hover:text-red-700 hover:bg-red-50"
|
|
>
|
|
<LogOut className="h-5 w-5" />
|
|
<span>退出登录</span>
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</SheetContent>
|
|
</Sheet>
|
|
|
|
{/* 家族树选择器 */}
|
|
{session && familyTrees.length > 0 && (
|
|
<div className="ml-4 flex-shrink-0">
|
|
<DropdownMenu open={treeDropdownOpen} onOpenChange={setTreeDropdownOpen} modal={false}>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="outline" className="gap-2 w-[180px] font-serif">
|
|
<span className="flex-1 truncate text-left">{currentTree?.name || '选择家族树'}</span>
|
|
<ChevronDown className="h-4 w-4 flex-shrink-0" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent
|
|
align="end"
|
|
className="w-[300px] font-serif"
|
|
sideOffset={5}
|
|
avoidCollisions={false}
|
|
sticky="always"
|
|
>
|
|
<DropdownMenuLabel className="font-light">我的家族树</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
{familyTrees.map((tree) => (
|
|
<DropdownMenuItem
|
|
key={tree.id}
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
setTreeDropdownOpen(false)
|
|
// 切换家族后跳转到总览页面,family-context 会自动加载新的树
|
|
router.push(`/?treeId=${tree.id}`)
|
|
}}
|
|
className="cursor-pointer flex items-center justify-between group py-3"
|
|
>
|
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
<span className="truncate flex-1">{tree.name}</span>
|
|
{getRoleBadge(tree.currentUserRole)}
|
|
{currentTree?.id === tree.id && (
|
|
<span className="text-primary flex-shrink-0">✓</span>
|
|
)}
|
|
</div>
|
|
{permissions.canDeleteTree(tree.currentUserRole) && (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-6 w-6 ml-2 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
onClick={(e) => handleDeleteTree(tree, e)}
|
|
disabled={deletingTreeId === tree.id}
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
)}
|
|
</DropdownMenuItem>
|
|
))}
|
|
<DropdownMenuSeparator />
|
|
{session && (
|
|
<DropdownMenuItem
|
|
onClick={() => router.push('/trees/new')}
|
|
className="gap-2"
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
创建新家族树
|
|
</DropdownMenuItem>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
)}
|
|
|
|
<div className="ml-auto flex items-center gap-2">
|
|
{/* Search Bar */}
|
|
<div className="relative hidden lg:block" ref={searchRef}>
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
type="search"
|
|
placeholder="搜索成员..."
|
|
value={searchQuery}
|
|
onChange={(e) => handleSearch(e.target.value)}
|
|
className="h-9 w-56 xl:w-64 rounded-lg bg-muted/50 pl-9 text-sm border-0 focus-visible:ring-1 focus-visible:ring-primary/50"
|
|
/>
|
|
|
|
{/* 搜索结果下拉框 */}
|
|
{showResults && searchResults.length > 0 && (
|
|
<div className="absolute top-full mt-2 w-full bg-background border border-border rounded-lg shadow-lg max-h-96 overflow-y-auto z-50">
|
|
{searchResults.map((member) => (
|
|
<button
|
|
key={member.id}
|
|
onClick={() => handleSelectMember(member.id)}
|
|
className="w-full px-4 py-3 text-left hover:bg-muted transition-colors border-b border-border last:border-0"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<MemberNameWithStatus
|
|
name={member.fullName}
|
|
isDead={!!member.deathDate}
|
|
className="font-medium"
|
|
/>
|
|
<span className={`text-xs px-1.5 py-0.5 rounded ${member.gender === "MALE" ? "bg-blue-100 text-blue-700" : "bg-pink-100 text-pink-700"}`}>
|
|
{member.gender === "MALE" ? "男" : "女"}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">第{member.generation}世</span>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{member.birthDate && `生于 ${member.birthDate}`}
|
|
{member.birthPlace && ` · ${member.birthPlace}`}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* 无结果提示 */}
|
|
{showResults && searchQuery && searchResults.length === 0 && (
|
|
<div className="absolute top-full mt-2 w-full bg-background border border-border rounded-lg shadow-lg p-4 text-center text-sm text-muted-foreground z-50">
|
|
未找到匹配的成员
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 日期显示 */}
|
|
<button
|
|
onClick={() => setCalendarDialogOpen(true)}
|
|
className="hidden xl:flex items-center gap-2 px-3 py-1.5 rounded-lg bg-gradient-to-br from-red-50/80 to-amber-50/80 dark:from-red-950/20 dark:to-amber-950/20 border border-red-200/50 dark:border-red-800/30 hover:shadow-sm transition-all cursor-pointer"
|
|
title="点击查询公历农历"
|
|
>
|
|
<Calendar className="h-4 w-4 text-red-600 dark:text-red-400" />
|
|
<div className="flex flex-col text-xs leading-tight">
|
|
<span className="font-medium text-gray-900 dark:text-gray-100">{solarDateStr}</span>
|
|
<span className="text-red-700 dark:text-red-400 font-serif">{lunarDateStr}</span>
|
|
</div>
|
|
</button>
|
|
|
|
{/* Utility Buttons - Desktop Only */}
|
|
<div className="hidden md:flex items-center gap-1">
|
|
<Link href={getUrlWithTreeId("/relationship")}>
|
|
<Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground hover:bg-muted" title="关系计算">
|
|
<Calculator className="h-5 w-5" />
|
|
<span className="sr-only">关系计算</span>
|
|
</Button>
|
|
</Link>
|
|
|
|
<Link href="/settings">
|
|
<Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground hover:bg-muted" title="系统设置">
|
|
<Settings className="h-5 w-5" />
|
|
<span className="sr-only">系统设置</span>
|
|
</Button>
|
|
</Link>
|
|
|
|
<Link href="/help">
|
|
<Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground hover:bg-muted" title="帮助中心">
|
|
<HelpCircle className="h-5 w-5" />
|
|
<span className="sr-only">帮助中心</span>
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
|
|
{/* User Menu */}
|
|
{status === "loading" ? (
|
|
<div className="h-9 w-9 rounded-full bg-muted animate-pulse" />
|
|
) : session ? (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" className="relative h-9 w-9 rounded-full hover:bg-muted">
|
|
<Avatar className="h-9 w-9 border-2 border-border">
|
|
<AvatarImage src={undefined} alt={session.user?.name || "User"} />
|
|
<AvatarFallback className="bg-primary/10 text-primary font-serif">
|
|
{getUserInitial()}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent className="w-56 font-serif" align="end" forceMount>
|
|
<DropdownMenuLabel className="font-normal">
|
|
<div className="flex flex-col space-y-1">
|
|
<p className="text-sm font-medium leading-none">{session.user?.name || "用户"}</p>
|
|
<p className="text-xs leading-none text-muted-foreground">
|
|
{session.user?.email}
|
|
</p>
|
|
</div>
|
|
</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem asChild>
|
|
<Link href={getUrlWithTreeId("/settings")} className="cursor-pointer">
|
|
<Settings className="mr-2 h-4 w-4" />
|
|
<span>系统设置</span>
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem asChild>
|
|
<Link href="/help" className="cursor-pointer">
|
|
<HelpCircle className="mr-2 h-4 w-4" />
|
|
<span>帮助中心</span>
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={handleSignOut} className="cursor-pointer text-red-600 focus:text-red-600 focus:bg-red-50">
|
|
<LogOut className="mr-2 h-4 w-4" />
|
|
<span>退出登录</span>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
) : (
|
|
<Link href="/auth/signin">
|
|
<Button variant="default" size="sm" className="font-serif">
|
|
登录
|
|
</Button>
|
|
</Link>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* 日历查询对话框 - 放在 header 外部避免定位冲突 */}
|
|
<CalendarDialog open={calendarDialogOpen} onOpenChange={setCalendarDialogOpen} />
|
|
</>
|
|
)
|
|
}
|