This commit is contained in:
freedakgmail
2025-11-23 14:58:51 +08:00
parent d797f1abb9
commit 8b32cda811
132 changed files with 276374 additions and 1009 deletions
+4 -4
View File
@@ -7,7 +7,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { SiteHeader } from "@/components/site-header"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { UserPlus, Network, Map, Calendar, Users, ArrowRight, Clock, Search, BookOpen, BarChart3 } from "lucide-react"
import { UserPlus, Network, Map, Calendar, Users, ArrowRight, Clock, Search, BookOpen, BarChart3, Calculator } from "lucide-react"
import { useFamily } from "@/context/family-context"
import { useMemo, useState, useEffect, useRef } from "react"
import { StatisticsCharts } from "@/components/dashboard/statistics-charts"
@@ -167,10 +167,10 @@ export default function DashboardPage() {
{session && currentTree?.ownerId === session.user?.id && (
<CollaboratorDialog />
)}
<Link href="/tree">
<Link href="/relationship">
<Button variant="outline" className="gap-2 bg-transparent">
<Network className="h-4 w-4" />
<Calculator className="h-4 w-4" />
</Button>
</Link>
{(currentTree?.currentUserRole === "OWNER" || currentTree?.currentUserRole === "EDITOR") && (
-245
View File
@@ -1,245 +0,0 @@
"use client"
import { useState } from 'react'
import { SiteHeader } from '@/components/site-header'
import { useFamily } from '@/context/family-context'
import { useRelationship } from '@/hooks/use-relationship'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
export default function RelationshipTestPage() {
const { treeData } = useFamily()
const { calculateRelationship, getDirectRelatives, getSiblings } = useRelationship()
const [fromId, setFromId] = useState<string>('')
const [toId, setToId] = useState<string>('')
const [result, setResult] = useState<any>(null)
const members = Object.values(treeData.members)
const handleCalculate = () => {
if (!fromId || !toId) return
const relationship = calculateRelationship(fromId, toId)
const fromMember = treeData.members[fromId]
const toMember = treeData.members[toId]
setResult({
from: fromMember,
to: toMember,
relationship
})
}
return (
<div className="min-h-screen flex flex-col bg-background">
<SiteHeader />
<main className="flex-1 container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto space-y-6">
{/* 标题 */}
<div>
<h1 className="text-3xl font-bold font-serif mb-2"></h1>
<p className="text-muted-foreground">
</p>
</div>
{/* 选择成员 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">A</label>
<Select value={fromId} onValueChange={setFromId}>
<SelectTrigger>
<SelectValue placeholder="选择成员A" />
</SelectTrigger>
<SelectContent>
{members.map(member => (
<SelectItem key={member.id} value={member.id}>
{member.fullName} ({member.generation})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">B</label>
<Select value={toId} onValueChange={setToId}>
<SelectTrigger>
<SelectValue placeholder="选择成员B" />
</SelectTrigger>
<SelectContent>
{members.map(member => (
<SelectItem key={member.id} value={member.id}>
{member.fullName} ({member.generation})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<Button
onClick={handleCalculate}
disabled={!fromId || !toId}
className="w-full"
>
</Button>
</CardContent>
</Card>
{/* 计算结果 */}
{result && (
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-center gap-4 p-6 bg-muted rounded-lg">
<div className="text-center">
<div className="text-2xl font-bold font-serif mb-1">
{result.from.fullName}
</div>
<div className="text-sm text-muted-foreground">
{result.from.generation}
</div>
</div>
<div className="flex flex-col items-center gap-2">
<Badge variant="default" className="text-lg px-4 py-2">
{result.relationship.term}
</Badge>
{result.relationship.path && (
<span className="text-xs text-muted-foreground">
: {result.relationship.path}
</span>
)}
</div>
<div className="text-center">
<div className="text-2xl font-bold font-serif mb-1">
{result.to.fullName}
</div>
<div className="text-sm text-muted-foreground">
{result.to.generation}
</div>
</div>
</div>
{result.relationship.description && (
<div className="p-4 bg-blue-50 dark:bg-blue-950 rounded-lg">
<div className="text-sm font-medium mb-1"></div>
<div className="text-muted-foreground">
<strong>{result.from.fullName}</strong> <strong>{result.to.fullName}</strong> <strong className="text-primary">{result.relationship.term}</strong>
</div>
</div>
)}
{result.relationship.success === false && (
<div className="p-4 bg-yellow-50 dark:bg-yellow-950 rounded-lg">
<div className="text-sm text-yellow-800 dark:text-yellow-200">
{result.relationship.error || '无法计算关系'}
</div>
</div>
)}
</CardContent>
</Card>
)}
{/* 快速查询示例 */}
{fromId && (
<Card>
<CardHeader>
<CardTitle>
{treeData.members[fromId]?.fullName}
</CardTitle>
</CardHeader>
<CardContent>
{(() => {
const relatives = getDirectRelatives(fromId)
if (!relatives) return null
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{relatives.father && (
<div className="p-3 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground mb-1"></div>
<div className="font-medium">{relatives.father.fullName}</div>
</div>
)}
{relatives.mother && (
<div className="p-3 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground mb-1"></div>
<div className="font-medium">{relatives.mother.fullName}</div>
</div>
)}
{relatives.spouses.length > 0 && (
<div className="p-3 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground mb-1"></div>
{relatives.spouses.map((spouse: any) => (
<div key={spouse.id} className="font-medium">
{spouse.fullName}
</div>
))}
</div>
)}
{relatives.children.length > 0 && (
<div className="p-3 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground mb-1">
({relatives.children.length})
</div>
<div className="space-y-1">
{relatives.children.slice(0, 3).map((child: any) => (
<div key={child.id} className="font-medium text-sm">
{child.fullName}
</div>
))}
{relatives.children.length > 3 && (
<div className="text-xs text-muted-foreground">
{relatives.children.length - 3} ...
</div>
)}
</div>
</div>
)}
</div>
)
})()}
{(() => {
const siblings = getSiblings(fromId)
if (siblings.length === 0) return null
return (
<div className="mt-4 p-3 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground mb-2">
({siblings.length})
</div>
<div className="flex flex-wrap gap-2">
{siblings.map((sibling: any) => (
<Badge key={sibling.id} variant="outline">
{sibling.fullName}
</Badge>
))}
</div>
</div>
)
})()}
</CardContent>
</Card>
)}
</div>
</main>
</div>
)
}
+402
View File
@@ -0,0 +1,402 @@
"use client"
import { useState } from 'react'
import { SiteHeader } from '@/components/site-header'
import { useFamily } from '@/context/family-context'
import { useRelationship } from '@/hooks/use-relationship'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
import { Check, ChevronsUpDown } from 'lucide-react'
import { cn } from '@/lib/utils'
export default function RelationshipTestPage() {
const { treeData } = useFamily()
const { calculateRelationship, getDirectRelatives, getSiblings } = useRelationship()
const [fromId, setFromId] = useState<string>('')
const [toId, setToId] = useState<string>('')
const [result, setResult] = useState<any>(null)
const [openFromCombobox, setOpenFromCombobox] = useState(false)
const [openToCombobox, setOpenToCombobox] = useState(false)
const members = Object.values(treeData.members)
const handleCalculate = () => {
if (!fromId || !toId) return
const relationship = calculateRelationship(fromId, toId)
const fromMember = treeData.members[fromId]
const toMember = treeData.members[toId]
setResult({
from: fromMember,
to: toMember,
relationship
})
}
return (
<div className="min-h-screen flex flex-col bg-background">
<SiteHeader />
<main className="flex-1 container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto space-y-6">
{/* 标题 */}
<div>
<h1 className="text-3xl font-bold font-serif mb-2"></h1>
<p className="text-muted-foreground">
</p>
</div>
{/* 选择成员 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">A</label>
<Popover open={openFromCombobox} onOpenChange={setOpenFromCombobox}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={openFromCombobox}
className="w-full justify-between"
>
{fromId
? members.find((member) => member.id === fromId)?.fullName
: "搜索或选择成员A..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<Command>
<CommandInput placeholder="输入姓名搜索..." />
<CommandList>
<CommandEmpty></CommandEmpty>
<CommandGroup>
{members.map((member) => (
<CommandItem
key={member.id}
value={`${member.fullName} ${member.generation}`}
onSelect={() => {
setFromId(member.id)
setOpenFromCombobox(false)
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
fromId === member.id ? "opacity-100" : "opacity-0"
)}
/>
{member.fullName} ({member.generation})
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">B</label>
<Popover open={openToCombobox} onOpenChange={setOpenToCombobox}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={openToCombobox}
className="w-full justify-between"
>
{toId
? members.find((member) => member.id === toId)?.fullName
: "搜索或选择成员B..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<Command>
<CommandInput placeholder="输入姓名搜索..." />
<CommandList>
<CommandEmpty></CommandEmpty>
<CommandGroup>
{members.map((member) => (
<CommandItem
key={member.id}
value={`${member.fullName} ${member.generation}`}
onSelect={() => {
setToId(member.id)
setOpenToCombobox(false)
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
toId === member.id ? "opacity-100" : "opacity-0"
)}
/>
{member.fullName} ({member.generation})
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
</div>
<Button
onClick={handleCalculate}
disabled={!fromId || !toId}
className="w-full"
>
</Button>
</CardContent>
</Card>
{/* 计算结果 */}
{result && (
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-center gap-4 p-6 bg-muted rounded-lg">
<div className="text-center">
<div className="text-2xl font-bold font-serif mb-1">
{result.from.fullName}
</div>
<div className="text-sm text-muted-foreground">
{result.from.generation}
</div>
</div>
<div className="flex flex-col items-center gap-2">
<Badge variant="default" className="text-lg px-4 py-2">
{result.relationship.term}
</Badge>
{result.relationship.path && (
<span className="text-xs text-muted-foreground">
: {result.relationship.path}
</span>
)}
</div>
<div className="text-center">
<div className="text-2xl font-bold font-serif mb-1">
{result.to.fullName}
</div>
<div className="text-sm text-muted-foreground">
{result.to.generation}
</div>
</div>
</div>
{result.relationship.description && (
<div className="p-4 bg-blue-50 dark:bg-blue-950 rounded-lg">
<div className="text-sm font-medium mb-1"></div>
<div className="text-muted-foreground">
<strong>{result.from.fullName}</strong> <strong>{result.to.fullName}</strong> <strong className="text-primary">{result.relationship.term}</strong>
</div>
</div>
)}
{result.relationship.success === false && (
<div className="p-4 bg-yellow-50 dark:bg-yellow-950 rounded-lg">
<div className="text-sm text-yellow-800 dark:text-yellow-200">
{result.relationship.error || '无法计算关系'}
</div>
</div>
)}
</CardContent>
</Card>
)}
{/* 直系亲属信息 */}
{(fromId || toId) && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{fromId && (
<Card>
<CardHeader>
<CardTitle>
{treeData.members[fromId]?.fullName}
</CardTitle>
</CardHeader>
<CardContent>
{(() => {
const relatives = getDirectRelatives(fromId)
if (!relatives) return null
return (
<>
<div className="grid grid-cols-1 gap-3">
{relatives.father && (
<div className="p-3 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground mb-1"></div>
<div className="font-medium">{relatives.father.fullName}</div>
</div>
)}
{relatives.mother && (
<div className="p-3 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground mb-1"></div>
<div className="font-medium">{relatives.mother.fullName}</div>
</div>
)}
{relatives.spouses.length > 0 && (
<div className="p-3 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground mb-1"></div>
{relatives.spouses.map((spouse: any) => (
<div key={spouse.id} className="font-medium">
{spouse.fullName}
</div>
))}
</div>
)}
{relatives.children.length > 0 && (
<div className="p-3 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground mb-1">
({relatives.children.length})
</div>
<div className="space-y-1">
{relatives.children.slice(0, 3).map((child: any) => (
<div key={child.id} className="font-medium text-sm">
{child.fullName}
</div>
))}
{relatives.children.length > 3 && (
<div className="text-xs text-muted-foreground">
{relatives.children.length - 3} ...
</div>
)}
</div>
</div>
)}
</div>
{(() => {
const siblings = getSiblings(fromId)
if (siblings.length === 0) return null
return (
<div className="mt-3 p-3 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground mb-2">
({siblings.length})
</div>
<div className="flex flex-wrap gap-2">
{siblings.map((sibling: any) => (
<Badge key={sibling.id} variant="outline">
{sibling.fullName}
</Badge>
))}
</div>
</div>
)
})()}
</>
)
})()}
</CardContent>
</Card>
)}
{toId && (
<Card>
<CardHeader>
<CardTitle>
{treeData.members[toId]?.fullName}
</CardTitle>
</CardHeader>
<CardContent>
{(() => {
const relatives = getDirectRelatives(toId)
if (!relatives) return null
return (
<>
<div className="grid grid-cols-1 gap-3">
{relatives.father && (
<div className="p-3 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground mb-1"></div>
<div className="font-medium">{relatives.father.fullName}</div>
</div>
)}
{relatives.mother && (
<div className="p-3 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground mb-1"></div>
<div className="font-medium">{relatives.mother.fullName}</div>
</div>
)}
{relatives.spouses.length > 0 && (
<div className="p-3 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground mb-1"></div>
{relatives.spouses.map((spouse: any) => (
<div key={spouse.id} className="font-medium">
{spouse.fullName}
</div>
))}
</div>
)}
{relatives.children.length > 0 && (
<div className="p-3 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground mb-1">
({relatives.children.length})
</div>
<div className="space-y-1">
{relatives.children.slice(0, 3).map((child: any) => (
<div key={child.id} className="font-medium text-sm">
{child.fullName}
</div>
))}
{relatives.children.length > 3 && (
<div className="text-xs text-muted-foreground">
{relatives.children.length - 3} ...
</div>
)}
</div>
</div>
)}
</div>
{(() => {
const siblings = getSiblings(toId)
if (siblings.length === 0) return null
return (
<div className="mt-3 p-3 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground mb-2">
({siblings.length})
</div>
<div className="flex flex-wrap gap-2">
{siblings.map((sibling: any) => (
<Badge key={sibling.id} variant="outline">
{sibling.fullName}
</Badge>
))}
</div>
</div>
)
})()}
</>
)
})()}
</CardContent>
</Card>
)}
</div>
)}
</div>
</main>
</div>
)
}