This commit is contained in:
freedakgmail
2025-11-24 14:02:34 +08:00
parent b7a8c9ee6e
commit 3d075c6076
941 changed files with 25613 additions and 27641 deletions
+118 -54
View File
@@ -1,7 +1,7 @@
"use client"
import Link from "next/link"
import { BookOpen, Search, Settings, LogOut, User, ChevronDown, Plus, Trash2, Network, LayoutGrid, Calculator, HelpCircle } from "lucide-react"
import { BookOpen, Search, Settings, LogOut, User, ChevronDown, Plus, Trash2, Network, LayoutGrid, Calculator, HelpCircle, Calendar } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
@@ -19,6 +19,9 @@ 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
@@ -29,7 +32,7 @@ interface FamilyTree {
}
export function SiteHeader() {
const { searchMembers, searchResults } = useFamily()
const { searchMembers, searchResults, currentTree: familyCurrentTree } = useFamily()
const { data: session, status } = useSession()
const { showAlert, showConfirm, showPrompt } = useDialog()
const pathname = usePathname()
@@ -37,15 +40,33 @@ export function SiteHeader() {
const [searchQuery, setSearchQuery] = useState("")
const [showResults, setShowResults] = useState(false)
const [familyTrees, setFamilyTrees] = useState<FamilyTree[]>([])
const [currentTree, setCurrentTree] = useState<FamilyTree | null>(null)
const [deletingTreeId, setDeletingTreeId] = useState<string | null>(null)
const [treeDropdownOpen, setTreeDropdownOpen] = useState(false)
const [currentDate, setCurrentDate] = useState(new Date())
const [calendarDialogOpen, setCalendarDialogOpen] = 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) {
@@ -159,19 +180,6 @@ export function SiteHeader() {
.then(data => {
if (data.trees) {
setFamilyTrees(data.trees)
// 如果 URL 中有 treeId,使用该树,否则使用第一个
const urlParams = new URLSearchParams(window.location.search)
const treeId = urlParams.get('treeId')
if (treeId) {
const tree = data.trees.find((t: FamilyTree) => t.id === treeId)
if (tree) {
setCurrentTree(tree)
} else if (data.trees.length > 0) {
setCurrentTree(data.trees[0])
}
} else if (data.trees.length > 0) {
setCurrentTree(data.trees[0])
}
}
})
.catch(err => {
@@ -216,42 +224,95 @@ export function SiteHeader() {
}
const handleSelectMember = (memberId: string) => {
router.push(`/members/${memberId}`)
const params = new URLSearchParams(window.location.search)
router.push(`/members/${memberId}?${params.toString()}`)
setSearchQuery("")
setShowResults(false)
}
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">
<div className="mr-8 flex items-center gap-4">
<Link href="/" className="flex items-center gap-2">
<div className="mr-8 flex items-center gap-2">
<Link href={getUrlWithTreeId("/")} className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded bg-primary text-primary-foreground">
<BookOpen className="h-5 w-5" />
</div>
<span className="text-xl font-serif font-bold tracking-tight"></span>
</Link>
{/* 家族树选择器 */}
{session && familyTrees.length > 0 && (
<DropdownMenu>
</div>
<nav className="hidden md:flex items-center gap-3 font-serif" onClick={() => setTreeDropdownOpen(false)}>
<Link
href={getUrlWithTreeId("/")}
className="relative w-20 py-2.5 flex items-center justify-center group"
style={{ filter: 'url(#rough-edge)' }}
>
<span className="relative z-10 text-amber-50 font-black text-base tracking-wider drop-shadow-sm"></span>
<div className="absolute inset-0 bg-gradient-to-br from-red-700 via-red-600 to-red-700 rounded-sm transition-all group-hover:from-red-800 group-hover:via-red-700 group-hover:to-red-800 shadow-md" style={{ clipPath: 'polygon(1% 0%, 99% 1%, 100% 98%, 2% 99%, 0% 2%)' }}></div>
<div className="absolute inset-0 border-2 border-red-900/40 rounded-sm" style={{ clipPath: 'polygon(0.5% 1%, 98.5% 0.5%, 99.5% 99%, 1.5% 98.5%, 1% 1.5%)' }}></div>
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-red-500/20 via-transparent to-transparent rounded-sm"></div>
</Link>
<Link
href={getUrlWithTreeId("/tree")}
className="relative w-20 py-2.5 flex items-center justify-center group"
style={{ filter: 'url(#rough-edge)' }}
>
<span className="relative z-10 text-amber-50 font-black text-base tracking-wider drop-shadow-sm"></span>
<div className="absolute inset-0 bg-gradient-to-br from-red-700 via-red-600 to-red-700 rounded-sm transition-all group-hover:from-red-800 group-hover:via-red-700 group-hover:to-red-800 shadow-md" style={{ clipPath: 'polygon(0% 1%, 99% 0%, 100% 99%, 1% 100%, 1% 1%)' }}></div>
<div className="absolute inset-0 border-2 border-red-900/40 rounded-sm" style={{ clipPath: 'polygon(1% 0.5%, 98% 1%, 99% 98.5%, 2% 99.5%, 0.5% 2%)' }}></div>
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-red-500/20 via-transparent to-transparent rounded-sm"></div>
</Link>
<Link
href={getUrlWithTreeId("/members")}
className="relative w-20 py-2.5 flex items-center justify-center group"
style={{ filter: 'url(#rough-edge)' }}
>
<span className="relative z-10 text-amber-50 font-black text-base tracking-wider drop-shadow-sm"></span>
<div className="absolute inset-0 bg-gradient-to-br from-red-700 via-red-600 to-red-700 rounded-sm transition-all group-hover:from-red-800 group-hover:via-red-700 group-hover:to-red-800 shadow-md" style={{ clipPath: 'polygon(1% 1%, 98% 0%, 99% 99%, 0% 98%, 2% 2%)' }}></div>
<div className="absolute inset-0 border-2 border-red-900/40 rounded-sm" style={{ clipPath: 'polygon(0.5% 0%, 99% 1.5%, 98.5% 99%, 1% 98%, 1.5% 1%)' }}></div>
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-red-500/20 via-transparent to-transparent rounded-sm"></div>
</Link>
<Link
href={getUrlWithTreeId("/timeline")}
className="relative w-20 py-2.5 flex items-center justify-center group"
style={{ filter: 'url(#rough-edge)' }}
>
<span className="relative z-10 text-amber-50 font-black text-base tracking-wider drop-shadow-sm"></span>
<div className="absolute inset-0 bg-gradient-to-br from-red-700 via-red-600 to-red-700 rounded-sm transition-all group-hover:from-red-800 group-hover:via-red-700 group-hover:to-red-800 shadow-md" style={{ clipPath: 'polygon(0% 0%, 100% 1%, 99% 100%, 1% 99%, 0.5% 1.5%)' }}></div>
<div className="absolute inset-0 border-2 border-red-900/40 rounded-sm" style={{ clipPath: 'polygon(1.5% 1%, 98.5% 0%, 99.5% 98%, 0.5% 99.5%, 1% 2%)' }}></div>
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-red-500/20 via-transparent to-transparent rounded-sm"></div>
</Link>
</nav>
{/* 家族树选择器 */}
{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 min-w-[200px]">
<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="start" className="w-[300px]">
<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={() => {
setCurrentTree(tree)
// 保持当前路径,只更新 treeId 参数
const currentPath = window.location.pathname
router.push(`${currentPath}?treeId=${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"
>
@@ -287,26 +348,8 @@ export function SiteHeader() {
)}
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
<nav className="hidden md:flex items-center gap-6 text-sm font-medium text-muted-foreground">
<Link
href={getUrlWithTreeId("/")}
className="transition-colors hover:text-foreground data-[active=true]:text-foreground data-[active=true]:font-semibold"
>
(Overview)
</Link>
<Link href={getUrlWithTreeId("/tree")} className="transition-colors hover:text-foreground">
(Tree)
</Link>
<Link href={getUrlWithTreeId("/members")} className="transition-colors hover:text-foreground">
(Members)
</Link>
<Link href={getUrlWithTreeId("/timeline")} className="transition-colors hover:text-foreground">
(Timeline)
</Link>
</nav>
</div>
)}
<div className="ml-auto flex items-center gap-4">
<div className="relative hidden sm:block" ref={searchRef}>
@@ -331,7 +374,11 @@ export function SiteHeader() {
<div className="flex items-center gap-3">
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium">{member.fullName}</span>
<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>
@@ -356,7 +403,20 @@ export function SiteHeader() {
)}
</div>
<Link href="/relationship">
{/* 日期显示 */}
<button
onClick={() => setCalendarDialogOpen(true)}
className="hidden lg:flex items-center gap-2 px-3 py-1.5 rounded-lg bg-gradient-to-br from-red-50 to-amber-50 border border-red-200/50 shadow-sm hover:shadow-md hover:from-red-100 hover:to-amber-100 transition-all cursor-pointer"
title="点击查询公历农历"
>
<Calendar className="h-4 w-4 text-red-600" />
<div className="flex flex-col text-xs leading-tight">
<span className="font-medium text-gray-900">{solarDateStr}</span>
<span className="text-red-700 font-serif">{lunarDateStr}</span>
</div>
</button>
<Link href={getUrlWithTreeId("/relationship")}>
<Button variant="ghost" size="icon" className="text-muted-foreground">
<Calculator className="h-5 w-5" />
<span className="sr-only">Relationship Calculator</span>
@@ -391,7 +451,7 @@ export function SiteHeader() {
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount>
<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>
@@ -402,7 +462,7 @@ export function SiteHeader() {
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="/settings" className="cursor-pointer">
<Link href={getUrlWithTreeId("/settings")} className="cursor-pointer">
<Settings className="mr-2 h-4 w-4" />
<span></span>
</Link>
@@ -424,5 +484,9 @@ export function SiteHeader() {
</div>
</div>
</header>
{/* 日历查询对话框 - 放在 header 外部避免定位冲突 */}
<CalendarDialog open={calendarDialogOpen} onOpenChange={setCalendarDialogOpen} />
</>
)
}