Files
chinese-family-tree-2/context/family-context.tsx
T
freedakgmail 0326894b09 0.3.1.3
2025-11-30 20:00:18 +08:00

436 lines
13 KiB
TypeScript

"use client"
import type React from "react"
import { createContext, useContext, useState, useCallback, useEffect, useRef, Suspense, useMemo } from "react"
import type { FamilyMember, FamilyTreeData } from "@/types/family"
import { useSession } from "next-auth/react"
import { useSearchParams } from "next/navigation"
interface FamilyTreeInfo {
id: string
name: string
description: string | null
ownerId: string
rootMemberId: string | null
currentUserRole?: "OWNER" | "EDITOR" | "VIEWER"
}
interface FamilyContextType {
treeData: FamilyTreeData
currentTree: FamilyTreeInfo | null
getMember: (id: string) => FamilyMember | undefined
addMember: (member: FamilyMember) => Promise<void>
updateMember: (id: string, updates: Partial<FamilyMember>) => Promise<void>
deleteMember: (id: string) => Promise<void>
searchResults: FamilyMember[]
searchMembers: (query: string) => void
loadData: (data: FamilyTreeData) => Promise<void>
resetData: () => Promise<void>
isLoading: boolean
highlightedMemberId: string | null
setHighlightedMemberId: (id: string | null) => void
}
const FamilyContext = createContext<FamilyContextType | undefined>(undefined)
function FamilyProviderInner({ children }: { children: React.ReactNode }) {
const { data: session, status } = useSession()
const searchParams = useSearchParams()
const [searchResults, setSearchResults] = useState<FamilyMember[]>([])
const [isLoading, setIsLoading] = useState(true)
const [highlightedMemberId, setHighlightedMemberId] = useState<string | null>(null)
const [treeData, setTreeData] = useState<FamilyTreeData>({
rootId: "",
members: {},
})
const [currentTree, setCurrentTree] = useState<FamilyTreeInfo | null>(null)
const loadingTreeIdRef = useRef<string | null>(null)
// 监听 URL 和 Session 变化,加载家族树
useEffect(() => {
const controller = new AbortController()
const signal = controller.signal
const loadTree = async () => {
const treeId = searchParams.get('treeId')
// 如果 session 正在加载,等待
if (status === 'loading') {
return
}
// 如果 session 不存在(未登录),清除所有数据
if (!session?.user?.id) {
if (!signal.aborted) {
setCurrentTree(null)
setTreeData({ rootId: "", members: {} })
setIsLoading(false)
loadingTreeIdRef.current = null
}
return
}
// 如果 URL 中有 treeId,使用它
if (treeId) {
// 如果正在加载相同的树,跳过
if (loadingTreeIdRef.current === treeId) {
return
}
// 如果 treeId 与当前树相同,跳过
if (treeId === currentTree?.id) {
return
}
} else {
// URL 中没有 treeId
// 如果已经有当前树,保持不变(避免在页面跳转时丢失上下文)
if (currentTree?.id) {
return
}
// 如果没有当前树,加载第一个
}
const targetTreeId = treeId || 'first'
// 如果正在加载相同的树,跳过
if (loadingTreeIdRef.current === targetTreeId) {
return
}
// 标记正在加载
loadingTreeIdRef.current = targetTreeId
if (!signal.aborted) setIsLoading(true)
const fetchUrl = targetTreeId === 'first'
? '/api/trees'
: `/api/trees/${targetTreeId}`
try {
const res = await fetch(fetchUrl, { signal })
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`)
}
const data = await res.json()
if (signal.aborted) return
let tree = null
if (targetTreeId === 'first') {
tree = data.trees?.[0]
} else {
tree = data.tree
}
if (tree) {
setCurrentTree(tree)
// 加载该树的成员数据
try {
const memberRes = await fetch(`/api/trees/${tree.id}/members`, { signal })
if (!memberRes.ok) {
throw new Error(`Failed to load members: ${memberRes.status}`)
}
const memberData = await memberRes.json()
if (signal.aborted) return
if (memberData.members) {
const membersMap = memberData.members.reduce(
(acc: Record<string, FamilyMember>, member: FamilyMember) => {
acc[member.id] = member
return acc
},
{}
)
setTreeData({
rootId: memberData.rootId || "",
members: membersMap,
})
}
} catch (err: any) {
if (err.name === 'AbortError') {
return
}
console.error('加载成员数据失败:', err)
// 即使成员加载失败,也保留树的信息
setTreeData({ rootId: "", members: {} })
}
} else {
setCurrentTree(null)
setTreeData({ rootId: "", members: {} })
}
} catch (err: any) {
if (err.name === 'AbortError') {
return
}
// 忽略 Failed to fetch 错误
if (err.message === 'Failed to fetch' || err.toString().includes('Failed to fetch')) {
return
}
console.error('加载家族树失败:', err.message || err)
setCurrentTree(null)
setTreeData({ rootId: "", members: {} })
} finally {
if (!signal.aborted) {
setIsLoading(false)
// 加载完成后清除标记
loadingTreeIdRef.current = null
}
}
}
loadTree()
return () => {
controller.abort()
}
}, [searchParams, session?.user?.id, status])
const getMember = useCallback(
(id: string) => {
return treeData.members[id]
},
[treeData],
)
const addMember = useCallback(async (member: FamilyMember) => {
if (!currentTree?.id) throw new Error('没有选择家族树')
const response = await fetch(`/api/trees/${currentTree.id}/members`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(member),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || '添加成员失败')
}
const data = await response.json()
// 更新本地状态
setTreeData(prev => ({
...prev,
members: {
...prev.members,
[data.member.id]: data.member,
},
}))
}, [currentTree?.id])
const updateMember = useCallback(async (id: string, updates: Partial<FamilyMember>) => {
if (!currentTree?.id) throw new Error('没有选择家族树')
// 将 undefined 转为 null,确保 JSON 序列化时能正确传递给后端
// 否则 JSON.stringify 会忽略 undefined 值,导致后端收不到更新
const payload = Object.fromEntries(
Object.entries(updates).map(([key, value]) => [key, value === undefined ? null : value])
)
const response = await fetch(`/api/trees/${currentTree.id}/members/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || '更新成员失败')
}
const data = await response.json()
// 由于后端会维护双向关系,需要重新加载所有成员以保持数据一致性
// 特别是当修改了关系字段(spouseIds, childrenIds, fatherId, motherId)时
const hasRelationshipChanges =
updates.spouseIds !== undefined ||
updates.childrenIds !== undefined ||
updates.fatherId !== undefined ||
updates.motherId !== undefined
if (hasRelationshipChanges) {
// 重新加载所有成员数据以同步双向关系
const membersResponse = await fetch(`/api/trees/${currentTree.id}/members`)
if (membersResponse.ok) {
const membersData = await membersResponse.json()
const membersMap: Record<string, FamilyMember> = {}
membersData.members.forEach((m: FamilyMember) => {
membersMap[m.id] = m
})
setTreeData(prev => ({
...prev,
members: membersMap,
}))
return
}
}
// 如果没有关系变更,只更新当前成员
setTreeData(prev => ({
...prev,
members: {
...prev.members,
[id]: data.member,
},
}))
}, [currentTree?.id])
const deleteMember = useCallback(async (id: string) => {
if (!currentTree?.id) throw new Error('没有选择家族树')
const response = await fetch(`/api/trees/${currentTree.id}/members/${id}`, {
method: 'DELETE',
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || '删除成员失败')
}
// 更新本地状态
setTreeData(prev => {
const newMembers = { ...prev.members }
delete newMembers[id]
return {
...prev,
members: newMembers,
}
})
}, [currentTree?.id])
const loadData = useCallback(async (data: FamilyTreeData) => {
if (!currentTree?.id) throw new Error('没有选择家族树')
// 导入数据到服务器
const response = await fetch(`/api/trees/${currentTree.id}/import`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || '导入数据失败')
}
// 重新加载数据
const membersResponse = await fetch(`/api/trees/${currentTree.id}/members`)
const membersData = await membersResponse.json()
if (membersData.members) {
const membersMap = membersData.members.reduce(
(acc: Record<string, FamilyMember>, member: FamilyMember) => {
acc[member.id] = member
return acc
},
{}
)
setTreeData({
rootId: membersData.rootId || "",
members: membersMap,
})
}
}, [currentTree?.id])
const resetData = useCallback(async () => {
// 重置为空数据
setTreeData({ rootId: "", members: {} })
}, [])
const searchMembers = useCallback(
(query: string) => {
if (!query.trim()) {
setSearchResults([])
return
}
const lowerQuery = query.toLowerCase()
// 在内存中的 treeData 上执行搜索,简单快速
const results = Object.values(treeData.members).filter(
(member) =>
member.fullName.includes(lowerQuery) ||
member.givenName.includes(lowerQuery) ||
member.courtesyName?.includes(lowerQuery) ||
member.artName?.includes(lowerQuery),
)
setSearchResults(results)
},
[treeData],
)
// Listen for import events
useEffect(() => {
const handleImport = (e: CustomEvent<FamilyTreeData>) => {
loadData(e.detail)
}
window.addEventListener("family-data-import" as any, handleImport as any)
return () => window.removeEventListener("family-data-import" as any, handleImport as any)
}, [loadData])
// 监听 treeData 变化
// useEffect(() => {
// console.log('🔄 FamilyContext treeData 更新:', {
// rootId: treeData.rootId,
// membersCount: Object.keys(treeData.members).length,
// memberIds: Object.keys(treeData.members),
// currentTreeId: currentTree?.id,
// currentTreeName: currentTree?.name
// })
// }, [treeData, currentTree])
// 使用 useMemo 缓存 context value,避免不必要的重渲染
const contextValue = useMemo(() => ({
treeData,
currentTree,
getMember,
addMember,
updateMember,
deleteMember,
searchResults,
searchMembers,
loadData,
resetData,
isLoading,
highlightedMemberId,
setHighlightedMemberId,
}), [
treeData,
currentTree,
getMember,
addMember,
updateMember,
deleteMember,
searchResults,
searchMembers,
loadData,
resetData,
isLoading,
highlightedMemberId,
])
return (
<FamilyContext.Provider value={contextValue}>
{children}
</FamilyContext.Provider>
)
}
export function FamilyProvider({ children }: { children: React.ReactNode }) {
return (
<Suspense fallback={<div>Loading...</div>}>
<FamilyProviderInner>{children}</FamilyProviderInner>
</Suspense>
)
}
export function useFamily() {
const context = useContext(FamilyContext)
if (context === undefined) {
throw new Error("useFamily must be used within a FamilyProvider")
}
return context
}