400 lines
12 KiB
TypeScript
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
|
|
}
|