0.0.8.5
This commit is contained in:
+118
-54
@@ -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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user