Files
chinese-family-tree-2/app/relationship/page.tsx
T
freedakgmail 86e889cc2a 0.3.1.2
2025-11-30 19:10:30 +08:00

511 lines
24 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { useState, useEffect } 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, Users, User } from 'lucide-react'
import { cn } from '@/lib/utils'
import { MemberNameWithStatus } from '@/components/member-name-with-status'
import { RelationshipPathDisplay } from '@/components/relationship-path-display'
import { useSession } from 'next-auth/react'
import { EmptyStateBadge } from '@/components/empty-state-badge'
import { useRouter } from 'next/navigation'
export default function RelationshipTestPage() {
const { treeData } = useFamily()
const { calculateRelationship, getDirectRelatives, getSiblings } = useRelationship()
const { data: session, status } = useSession()
const router = useRouter()
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)
// 未登录时重定向到登录页
useEffect(() => {
if (status === 'unauthenticated') {
router.push('/auth/signin')
}
}, [status, router])
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
})
}
// 未登录时不渲染内容(已在 useEffect 中重定向)
if (status === 'loading' || !session) {
return null
}
// 无数据时显示空状态
if (members.length === 0) {
return (
<div className="min-h-screen flex flex-col bg-background">
<SiteHeader />
<main className="flex-1 flex items-center justify-center">
<div className="text-center space-y-4">
<div className="inline-flex items-center justify-center w-16 h-16 text-primary/80">
<Users className="h-16 w-16" strokeWidth={1} />
</div>
<p className="text-base text-foreground font-light tracking-wide"></p>
</div>
</main>
</div>
)
}
return (
<div className="h-screen flex flex-col bg-background">
<SiteHeader />
<main className="flex-1 overflow-y-auto">
<div className="container mx-auto px-4 py-6">
<div className="max-w-4xl mx-auto space-y-6">
{/* 标题 */}
<div>
<h1 className="text-3xl font-serif font-bold"></h1>
<p className="text-muted-foreground">
</p>
</div>
{/* 选择成员 */}
<Card className="border-none shadow-sm bg-card/50 backdrop-blur">
<CardHeader className="text-center pb-4">
<CardTitle className="text-xl font-serif font-light"></CardTitle>
<CardDescription className="text-sm font-light"></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
? (() => {
const selectedMember = members.find((member) => member.id === fromId)
return selectedMember ? (
<MemberNameWithStatus
name={selectedMember.fullName}
isDead={!!selectedMember.deathDate}
/>
) : "搜索或选择成员A..."
})()
: "搜索或选择成员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"
)}
/>
<MemberNameWithStatus
name={member.fullName}
isDead={!!member.deathDate}
/> ({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
? (() => {
const selectedMember = members.find((member) => member.id === toId)
return selectedMember ? (
<MemberNameWithStatus
name={selectedMember.fullName}
isDead={!!selectedMember.deathDate}
/>
) : "搜索或选择成员B..."
})()
: "搜索或选择成员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"
)}
/>
<MemberNameWithStatus
name={member.fullName}
isDead={!!member.deathDate}
/> ({member.generation})
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
</div>
<Button
onClick={handleCalculate}
disabled={!fromId || !toId}
className="w-full h-10 bg-primary text-primary-foreground hover:bg-primary/90"
>
</Button>
</CardContent>
</Card>
{/* 计算结果 */}
{result && (
<Card className="border-none shadow-sm bg-card/50 backdrop-blur">
<CardHeader className="text-center pb-4">
<CardTitle className="text-xl font-serif font-light"></CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center justify-center gap-4 p-5 bg-muted/30 rounded-lg">
<div className="text-center">
<div className="text-xl font-serif font-light mb-1">
<MemberNameWithStatus
name={result.from.fullName}
isDead={!!result.from.deathDate}
/>
</div>
<div className="text-sm text-muted-foreground font-light">
{result.from.generation}
</div>
</div>
<div className="flex flex-col items-center gap-1.5">
<Badge className="text-base px-5 py-1.5 bg-primary text-primary-foreground">
{result.relationship.term}
</Badge>
{result.relationship.path && (
<div className="text-xs text-muted-foreground">
<div className="mb-1">:</div>
<RelationshipPathDisplay path={result.relationship.path} />
</div>
)}
</div>
<div className="text-center">
<div className="text-xl font-serif font-light mb-1">
<MemberNameWithStatus
name={result.to.fullName}
isDead={!!result.to.deathDate}
/>
</div>
<div className="text-sm text-muted-foreground font-light">
{result.to.generation}
</div>
</div>
</div>
{result.relationship.description && (
<div className="p-4 bg-primary/5 rounded-lg">
<div className="text-sm font-light mb-2 text-center"></div>
<div className="text-muted-foreground font-light text-center">
<strong><MemberNameWithStatus name={result.from.fullName} isDead={!!result.from.deathDate} /></strong> <strong><MemberNameWithStatus name={result.to.fullName} isDead={!!result.to.deathDate} /></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-4">
{fromId && (
<Card className="border-none shadow-sm bg-card/50 backdrop-blur">
<CardHeader className="pb-3">
<CardTitle className="text-lg font-serif font-light">
<MemberNameWithStatus
name={treeData.members[fromId]?.fullName || ''}
isDead={!!treeData.members[fromId]?.deathDate}
/>
</CardTitle>
</CardHeader>
<CardContent>
{(() => {
const relatives = getDirectRelatives(fromId)
if (!relatives) return null
return (
<>
<div className="grid grid-cols-1 gap-2">
{relatives.father && (
<div className="p-2.5 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground mb-1"></div>
<div className="font-medium">
<MemberNameWithStatus
name={relatives.father.fullName}
isDead={!!relatives.father.deathDate}
/>
</div>
</div>
)}
{relatives.mother && (
<div className="p-2.5 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground mb-1"></div>
<div className="font-medium">
<MemberNameWithStatus
name={relatives.mother.fullName}
isDead={!!relatives.mother.deathDate}
/>
</div>
</div>
)}
{relatives.spouses.length > 0 && (
<div className="p-2.5 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">
<MemberNameWithStatus
name={spouse.fullName}
isDead={!!spouse.deathDate}
/>
</div>
))}
</div>
)}
{relatives.children.length > 0 && (
<div className="p-2.5 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">
<MemberNameWithStatus
name={child.fullName}
isDead={!!child.deathDate}
/>
</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-2 p-2.5 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">
<MemberNameWithStatus
name={sibling.fullName}
isDead={!!sibling.deathDate}
/>
</Badge>
))}
</div>
</div>
)
})()}
</>
)
})()}
</CardContent>
</Card>
)}
{toId && (
<Card className="border-none shadow-sm bg-card/50 backdrop-blur">
<CardHeader className="pb-3">
<CardTitle className="text-lg font-serif font-light">
{treeData.members[toId]?.fullName}
</CardTitle>
</CardHeader>
<CardContent>
{(() => {
const relatives = getDirectRelatives(toId)
if (!relatives) return null
return (
<>
<div className="grid grid-cols-1 gap-2">
{relatives.father && (
<div className="p-2.5 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground mb-1"></div>
<div className="font-medium">
<MemberNameWithStatus
name={relatives.father.fullName}
isDead={!!relatives.father.deathDate}
/>
</div>
</div>
)}
{relatives.mother && (
<div className="p-2.5 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground mb-1"></div>
<div className="font-medium">
<MemberNameWithStatus
name={relatives.mother.fullName}
isDead={!!relatives.mother.deathDate}
/>
</div>
</div>
)}
{relatives.spouses.length > 0 && (
<div className="p-2.5 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">
<MemberNameWithStatus
name={spouse.fullName}
isDead={!!spouse.deathDate}
/>
</div>
))}
</div>
)}
{relatives.children.length > 0 && (
<div className="p-2.5 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">
<MemberNameWithStatus
name={child.fullName}
isDead={!!child.deathDate}
/>
</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-2 p-2.5 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">
<MemberNameWithStatus
name={sibling.fullName}
isDead={!!sibling.deathDate}
/>
</Badge>
))}
</div>
</div>
)
})()}
</>
)
})()}
</CardContent>
</Card>
)}
</div>
)}
</div>
</div>
</main>
</div>
)
}