This commit is contained in:
freedakgmail
2025-11-23 09:13:25 +08:00
parent 81dbf709aa
commit bade25ff26
378 changed files with 769815 additions and 2334 deletions
+87 -41
View File
@@ -17,17 +17,20 @@ import { useFamily } from "@/context/family-context"
import { useState, useRef, useEffect, useCallback } from "react"
import { useRouter } from "next/navigation"
import { useSession, signOut } from "next-auth/react"
import { useDialog } from "@/components/ui/alert-dialog-custom"
interface FamilyTree {
id: string
name: string
description: string | null
ownerId: string
currentUserRole?: "OWNER" | "EDITOR" | "VIEWER"
}
export function SiteHeader() {
const { searchMembers, searchResults } = useFamily()
const { data: session, status } = useSession()
const { showAlert, showConfirm, showPrompt } = useDialog()
const [searchQuery, setSearchQuery] = useState("")
const [showResults, setShowResults] = useState(false)
const [familyTrees, setFamilyTrees] = useState<FamilyTree[]>([])
@@ -36,6 +39,27 @@ export function SiteHeader() {
const searchRef = useRef<HTMLDivElement>(null)
const router = useRouter()
// 生成带有 treeId 的 URL
const getUrlWithTreeId = (path: string) => {
if (currentTree?.id) {
return `${path}?treeId=${currentTree.id}`
}
return path
}
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" })
}
@@ -57,19 +81,24 @@ export function SiteHeader() {
// 检查权限
if (tree.ownerId !== session?.user?.id) {
alert('只有家族树的创建者才能删除')
await showAlert('只有家族树的创建者才能删除', '权限不足')
return
}
const confirmText = `确定要删除家族树"${tree.name}"吗?\n\n此操作将永久删除所有成员、活动日志和协作者数据,且不可撤销!`
if (!confirm(confirmText)) {
const confirmed = await showConfirm(confirmText, '删除家族树', 'destructive')
if (!confirmed) {
return
}
// 二次确认
const finalConfirm = prompt(`请输入家族树名称"${tree.name}"以确认删除:`)
const finalConfirm = await showPrompt(
`请输入家族树名称"${tree.name}"以确认删除:`,
'',
'确认删除'
)
if (finalConfirm !== tree.name) {
alert('名称不匹配,删除已取消')
await showAlert('名称不匹配,删除已取消', '操作已取消')
return
}
@@ -93,7 +122,7 @@ export function SiteHeader() {
}
} catch (error) {
console.error('删除家族树失败:', error)
alert('删除失败:' + (error as Error).message)
await showAlert('删除失败:' + (error as Error).message, '错误')
} finally {
setDeletingTreeId(null)
}
@@ -101,37 +130,49 @@ export function SiteHeader() {
// 获取家族树列表
const loadFamilyTrees = useCallback(() => {
if (session?.user?.id) {
fetch('/api/trees')
.then(res => {
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
return res.json()
})
.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])
}
// 只在 session 加载完成且用户已登录时才获取
if (status === 'loading') {
return
}
if (!session?.user?.id) {
setFamilyTrees([])
setCurrentTree(null)
return
}
fetch('/api/trees')
.then(res => {
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
return res.json()
})
.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 => {
}
})
.catch(err => {
// 只在非 abort 错误时才记录
if (err.name !== 'AbortError') {
console.error('获取家族树列表失败:', err)
})
}
}, [session?.user?.id])
}
})
}, [session?.user?.id, status])
useEffect(() => {
loadFamilyTrees()
@@ -198,11 +239,16 @@ export function SiteHeader() {
key={tree.id}
onClick={() => {
setCurrentTree(tree)
router.push(`/?treeId=${tree.id}`)
// 保持当前路径,只更新 treeId 参数
const currentPath = window.location.pathname
router.push(`${currentPath}?treeId=${tree.id}`)
}}
className="cursor-pointer flex items-center justify-between group"
>
<span className="flex-1 truncate">{tree.name}</span>
<div className="flex items-center overflow-hidden">
<span className="truncate">{tree.name}</span>
{getRoleBadge(tree.currentUserRole)}
</div>
<div className="flex items-center gap-2">
{currentTree?.id === tree.id && (
<span className="text-primary"></span>
@@ -236,18 +282,18 @@ export function SiteHeader() {
<nav className="hidden md:flex items-center gap-6 text-sm font-medium text-muted-foreground">
<Link
href="/"
href={getUrlWithTreeId("/")}
className="transition-colors hover:text-foreground data-[active=true]:text-foreground data-[active=true]:font-semibold"
>
(Overview)
</Link>
<Link href="/tree" className="transition-colors hover:text-foreground">
<Link href={getUrlWithTreeId("/tree")} className="transition-colors hover:text-foreground">
(Tree)
</Link>
<Link href="/members" className="transition-colors hover:text-foreground">
<Link href={getUrlWithTreeId("/members")} className="transition-colors hover:text-foreground">
(Members)
</Link>
<Link href="/timeline" className="transition-colors hover:text-foreground">
<Link href={getUrlWithTreeId("/timeline")} className="transition-colors hover:text-foreground">
(Timeline)
</Link>
</nav>
@@ -276,8 +322,8 @@ export function SiteHeader() {
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium">{member.fullName}</span>
<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 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>
@@ -338,8 +384,8 @@ export function SiteHeader() {
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="/settings" className="cursor-pointer">
<User className="mr-2 h-4 w-4" />
<span></span>
<Settings className="mr-2 h-4 w-4" />
<span></span>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />