511 lines
24 KiB
TypeScript
511 lines
24 KiB
TypeScript
"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>
|
||
)
|
||
}
|