0.0.1.0
This commit is contained in:
+201
-123
@@ -3,14 +3,20 @@
|
||||
import type React from "react"
|
||||
import { createContext, useContext, useState, useCallback, useEffect } from "react"
|
||||
import type { FamilyMember, FamilyTreeData } from "@/types/family"
|
||||
import { mockFamilyData } from "@/lib/mock-data"
|
||||
import { db } from "@/lib/db"
|
||||
import { useLiveQuery } from "dexie-react-hooks"
|
||||
import { setupDailyNotificationCheck } from "@/lib/notifications"
|
||||
import { logActivity } from "@/lib/activity-logger"
|
||||
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
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -27,51 +33,102 @@ interface FamilyContextType {
|
||||
const FamilyContext = createContext<FamilyContextType | undefined>(undefined)
|
||||
|
||||
export function FamilyProvider({ children }: { children: React.ReactNode }) {
|
||||
const { data: session } = useSession()
|
||||
const searchParams = useSearchParams()
|
||||
const [searchResults, setSearchResults] = useState<FamilyMember[]>([])
|
||||
const [isInitialized, setIsInitialized] = useState(false)
|
||||
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)
|
||||
|
||||
// Live query to get all members
|
||||
const members = useLiveQuery(() => db.members.toArray())
|
||||
// Live query to get rootId
|
||||
const rootIdSetting = useLiveQuery(() => db.settings.get("rootId"))
|
||||
|
||||
// Initialize DB with mock data if empty
|
||||
// 监听 URL 和 Session 变化,加载家族树
|
||||
useEffect(() => {
|
||||
const initDb = async () => {
|
||||
const count = await db.members.count()
|
||||
if (count === 0) {
|
||||
await db.transaction("rw", db.members, db.settings, async () => {
|
||||
await db.members.bulkAdd(Object.values(mockFamilyData.members))
|
||||
await db.settings.put({ key: "rootId", value: mockFamilyData.rootId })
|
||||
})
|
||||
}
|
||||
setIsInitialized(true)
|
||||
const treeId = searchParams.get('treeId')
|
||||
console.log('FamilyContext - Effect triggered, treeId:', treeId, 'currentTree:', currentTree?.id)
|
||||
|
||||
// 如果 session 不存在,清除所有数据
|
||||
if (!session?.user?.id) {
|
||||
console.log('FamilyContext - No session, clearing data')
|
||||
setCurrentTree(null)
|
||||
setTreeData({ rootId: "", members: {} })
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
initDb()
|
||||
}, [])
|
||||
|
||||
// 设置通知检查
|
||||
useEffect(() => {
|
||||
if (isInitialized && members && members.length > 0) {
|
||||
// 检查通知权限并设置每日检查
|
||||
if ("Notification" in window && Notification.permission === "granted") {
|
||||
setupDailyNotificationCheck(members)
|
||||
}
|
||||
// 如果 treeId 与当前树相同,跳过
|
||||
if (treeId && treeId === currentTree?.id) {
|
||||
console.log('FamilyContext - Same tree, skipping')
|
||||
return
|
||||
}
|
||||
}, [isInitialized, members])
|
||||
|
||||
// Construct treeData from DB results
|
||||
const treeData: FamilyTreeData = {
|
||||
rootId: rootIdSetting?.value || "",
|
||||
members: (members || []).reduce(
|
||||
(acc, member) => {
|
||||
acc[member.id] = member
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, FamilyMember>,
|
||||
),
|
||||
}
|
||||
// 如果没有 treeId 且已有树,跳过
|
||||
if (!treeId && currentTree) {
|
||||
console.log('FamilyContext - No treeId but has currentTree, skipping')
|
||||
return
|
||||
}
|
||||
|
||||
// 需要加载新树
|
||||
const targetTreeId = treeId || 'first'
|
||||
console.log('FamilyContext - Loading tree:', targetTreeId)
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
const fetchUrl = targetTreeId === 'first'
|
||||
? '/api/trees'
|
||||
: `/api/trees/${targetTreeId}`
|
||||
|
||||
console.log('FamilyContext - Fetching URL:', fetchUrl)
|
||||
|
||||
fetch(fetchUrl)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
let tree = null
|
||||
if (targetTreeId === 'first') {
|
||||
tree = data.trees?.[0]
|
||||
} else {
|
||||
tree = data.tree
|
||||
}
|
||||
|
||||
console.log('FamilyContext - Fetched tree:', tree?.name, tree?.id)
|
||||
|
||||
if (tree) {
|
||||
setCurrentTree(tree)
|
||||
// 加载该树的成员数据
|
||||
return fetch(`/api/trees/${tree.id}/members`)
|
||||
.then(res => res.json())
|
||||
.then(memberData => {
|
||||
console.log('FamilyContext - Loaded members:', memberData.members?.length)
|
||||
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,
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 没找到树(可能是被删除了,或者没有权限)
|
||||
console.log('FamilyContext - Tree not found')
|
||||
setCurrentTree(null)
|
||||
setTreeData({ rootId: "", members: {} })
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('FamilyContext - Load failed:', err)
|
||||
setCurrentTree(null)
|
||||
setTreeData({ rootId: "", members: {} })
|
||||
})
|
||||
.finally(() => setIsLoading(false))
|
||||
}, [searchParams, session?.user?.id])
|
||||
|
||||
const getMember = useCallback(
|
||||
(id: string) => {
|
||||
@@ -81,97 +138,118 @@ export function FamilyProvider({ children }: { children: React.ReactNode }) {
|
||||
)
|
||||
|
||||
const addMember = useCallback(async (member: FamilyMember) => {
|
||||
await db.transaction("rw", db.members, db.activityLogs, async () => {
|
||||
// Add the member
|
||||
await db.members.add(member)
|
||||
|
||||
// Update relationships (Father)
|
||||
if (member.fatherId) {
|
||||
const father = await db.members.get(member.fatherId)
|
||||
if (father && !father.childrenIds.includes(member.id)) {
|
||||
await db.members.update(member.fatherId, {
|
||||
childrenIds: [...father.childrenIds, member.id],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Update relationships (Mother)
|
||||
if (member.motherId) {
|
||||
const mother = await db.members.get(member.motherId)
|
||||
if (mother && !mother.childrenIds.includes(member.id)) {
|
||||
await db.members.update(member.motherId, {
|
||||
childrenIds: [...mother.childrenIds, member.id],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Update relationships (Spouses)
|
||||
if (member.spouseIds && member.spouseIds.length > 0) {
|
||||
for (const spouseId of member.spouseIds) {
|
||||
const spouse = await db.members.get(spouseId)
|
||||
if (spouse && !spouse.spouseIds.includes(member.id)) {
|
||||
await db.members.update(spouseId, {
|
||||
spouseIds: [...spouse.spouseIds, member.id],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 记录日志(在事务内)
|
||||
await logActivity("create", "member", member.id, member.fullName)
|
||||
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>) => {
|
||||
await db.transaction("rw", db.members, db.activityLogs, async () => {
|
||||
const member = await db.members.get(id)
|
||||
await db.members.update(id, {
|
||||
...updates,
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
|
||||
// 记录日志(在事务内)
|
||||
if (member) {
|
||||
await logActivity("update", "member", id, member.fullName, updates)
|
||||
}
|
||||
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) => {
|
||||
await db.transaction("rw", db.members, db.settings, db.activityLogs, async () => {
|
||||
const member = await db.members.get(id)
|
||||
|
||||
// Remove from DB
|
||||
await db.members.delete(id)
|
||||
|
||||
// Note: In a real app, we should also remove references to this ID from other members'
|
||||
// childrenIds, spouseIds, etc. For now, we'll keep it simple as per the original implementation.
|
||||
|
||||
// If we deleted the root, we might want to handle that, but for now let's leave it.
|
||||
|
||||
// 记录日志(在事务内)
|
||||
if (member) {
|
||||
await logActivity("delete", "member", id, member.fullName)
|
||||
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) => {
|
||||
await db.transaction("rw", db.members, db.settings, db.activityLogs, async () => {
|
||||
await db.members.clear()
|
||||
await db.members.bulkAdd(Object.values(data.members))
|
||||
await db.settings.put({ key: "rootId", value: data.rootId })
|
||||
|
||||
// 记录日志(在事务内)
|
||||
await logActivity("import", "settings", undefined, "导入家族数据", {
|
||||
memberCount: Object.keys(data.members).length
|
||||
})
|
||||
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 () => {
|
||||
await loadData(mockFamilyData)
|
||||
}, [loadData])
|
||||
// 重置为空数据
|
||||
setTreeData({ rootId: "", members: {} })
|
||||
}, [])
|
||||
|
||||
const searchMembers = useCallback(
|
||||
(query: string) => {
|
||||
@@ -195,8 +273,7 @@ export function FamilyProvider({ children }: { children: React.ReactNode }) {
|
||||
[treeData],
|
||||
)
|
||||
|
||||
// Listen for import events (legacy support for the file input in settings if needed,
|
||||
// but we should prefer using the context method)
|
||||
// Listen for import events
|
||||
useEffect(() => {
|
||||
const handleImport = (e: CustomEvent<FamilyTreeData>) => {
|
||||
loadData(e.detail)
|
||||
@@ -209,6 +286,7 @@ export function FamilyProvider({ children }: { children: React.ReactNode }) {
|
||||
<FamilyContext.Provider
|
||||
value={{
|
||||
treeData,
|
||||
currentTree,
|
||||
getMember,
|
||||
addMember,
|
||||
updateMember,
|
||||
@@ -217,7 +295,7 @@ export function FamilyProvider({ children }: { children: React.ReactNode }) {
|
||||
searchMembers,
|
||||
loadData,
|
||||
resetData,
|
||||
isLoading: !isInitialized || !members,
|
||||
isLoading,
|
||||
highlightedMemberId,
|
||||
setHighlightedMemberId,
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user