Files
chinese-family-tree-2/context/family-context.tsx
T
freedakgmail bade25ff26 0.0.1.2
2025-11-23 09:13:25 +08:00

400 lines
12 KiB
TypeScript

"use client"
import type React from "react"
import { createContext, useContext, useState, useCallback, useEffect, useRef } 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)
export function FamilyProvider({ 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
}
// 需要加载的树 ID
const targetTreeId = treeId || 'first'
// 如果正在加载相同的树,跳过
if (loadingTreeIdRef.current === targetTreeId) {
console.log('⏭️ 跳过重复加载:', targetTreeId)
return
}
// 如果 treeId 与当前树相同,跳过
if (treeId && treeId === currentTree?.id) {
console.log('⏭️ 已加载该树,跳过:', treeId)
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()
console.log('🌳 获取家族树响应:', {
fetchUrl,
targetTreeId,
responseData: data,
treesCount: data.trees?.length,
firstTree: data.trees?.[0],
singleTree: data.tree
})
if (signal.aborted) return
let tree = null
if (targetTreeId === 'first') {
tree = data.trees?.[0]
} else {
tree = data.tree
}
console.log('🎯 选择的家族树:', tree)
if (tree) {
setCurrentTree(tree)
// 加载该树的成员数据
console.log('🔄 开始加载成员数据:', { treeId: tree.id, treeName: tree.name })
try {
const memberRes = await fetch(`/api/trees/${tree.id}/members`, { signal })
console.log('📡 成员数据响应状态:', memberRes.status, memberRes.ok)
if (!memberRes.ok) {
throw new Error(`Failed to load members: ${memberRes.status}`)
}
const memberData = await memberRes.json()
console.log('📊 获取到的成员数据:', {
treeId: tree.id,
treeName: tree.name,
membersCount: memberData.members?.length || 0,
members: memberData.members,
rootId: memberData.rootId
})
if (signal.aborted) return
if (memberData.members) {
const membersMap = memberData.members.reduce(
(acc: Record<string, FamilyMember>, member: FamilyMember) => {
acc[member.id] = member
return acc
},
{}
)
console.log('✅ 设置 treeData:', {
rootId: memberData.rootId || "",
membersMapKeys: Object.keys(membersMap),
membersMapCount: Object.keys(membersMap).length
})
setTreeData({
rootId: memberData.rootId || "",
members: membersMap,
})
}
} catch (err: any) {
if (err.name === 'AbortError') {
console.log('❌ 成员数据加载被中断')
return
}
console.error('加载成员数据失败:', err)
// 即使成员加载失败,也保留树的信息
setTreeData({ rootId: "", members: {} })
}
} else {
setCurrentTree(null)
setTreeData({ rootId: "", members: {} })
}
} catch (err: any) {
if (err.name === 'AbortError') {
console.log('❌ 家族树加载被中断')
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('没有选择家族树')
const response = await fetch(`/api/trees/${currentTree.id}/members/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || '更新成员失败')
}
const data = await response.json()
// 更新本地状态
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()
// Perform search on the in-memory treeData for simplicity and speed
// (Dexie also supports querying, but we already have the data loaded)
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])
return (
<FamilyContext.Provider
value={{
treeData,
currentTree,
getMember,
addMember,
updateMember,
deleteMember,
searchResults,
searchMembers,
loadData,
resetData,
isLoading,
highlightedMemberId,
setHighlightedMemberId,
}}
>
{children}
</FamilyContext.Provider>
)
}
export function useFamily() {
const context = useContext(FamilyContext)
if (context === undefined) {
throw new Error("useFamily must be used within a FamilyProvider")
}
return context
}