Files
chinese-family-tree-2/components/tree/add-relation-dialog.tsx
T
freedakgmail 0326894b09 0.3.1.3
2025-11-30 20:00:18 +08:00

217 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
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 { useMemo, useState, useEffect } from "react"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { ScrollArea } from "@/components/ui/scroll-area"
import { MemberNameWithStatus } from "@/components/member-name-with-status"
import { useFamily } from "@/context/family-context"
import { useToast } from "@/components/ui/use-toast"
import type { FamilyMember } from "@/types/family"
interface AddRelationDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
member: FamilyMember
relationType: "child" | "spouse"
onCreateNew: (prefillName: string, relationType: "child" | "spouse") => void
}
interface CandidateMeta {
member: FamilyMember
fatherName?: string
motherName?: string
}
export function AddRelationDialog({
open,
onOpenChange,
member,
relationType,
onCreateNew,
}: AddRelationDialogProps) {
const { treeData, updateMember } = useFamily()
const { toast } = useToast()
const [query, setQuery] = useState("")
const [linkingId, setLinkingId] = useState<string | null>(null)
useEffect(() => {
if (!open) {
setQuery("")
setLinkingId(null)
}
}, [open])
const normalizedQuery = query.trim().toLowerCase()
const candidates: CandidateMeta[] = useMemo(() => {
if (!normalizedQuery) return []
const members = Object.values(treeData.members)
return members
.filter((candidate) => {
if (candidate.id === member.id) return false
if (!candidate.fullName.toLowerCase().includes(normalizedQuery) &&
!(candidate.givenName || "").toLowerCase().includes(normalizedQuery) &&
!(candidate.courtesyName || "").toLowerCase().includes(normalizedQuery)) {
return false
}
if (relationType === "child") {
if (candidate.generation !== (member.generation + 1)) return false
if (member.childrenIds?.includes(candidate.id)) return false
if (member.spouseIds?.includes(candidate.id)) return false
const fatherMatches = candidate.fatherId ? candidate.fatherId === member.id : true
const motherMatches = candidate.motherId ? candidate.motherId === member.id : true
if (member.gender === "MALE" && !fatherMatches) return false
if (member.gender === "FEMALE" && !motherMatches) return false
if (member.gender !== "MALE" && member.gender !== "FEMALE") {
// 非男女成员,允许填补任一缺失的父母
if (candidate.fatherId && candidate.motherId) return false
}
} else {
if (candidate.generation !== member.generation) return false
if (member.spouseIds?.includes(candidate.id)) return false
if (candidate.spouseIds?.includes(member.id)) return false
if (candidate.gender === member.gender) return false
}
return true
})
.slice(0, 8)
.map((candidate) => ({
member: candidate,
fatherName: candidate.fatherId ? treeData.members[candidate.fatherId]?.fullName : undefined,
motherName: candidate.motherId ? treeData.members[candidate.motherId]?.fullName : undefined,
}))
}, [member, relationType, treeData.members, normalizedQuery])
const handleLink = async (target: FamilyMember) => {
try {
setLinkingId(target.id)
if (relationType === "child") {
const payload: Partial<FamilyMember> = {}
if (member.gender === "FEMALE") {
payload.motherId = member.id
} else if (member.gender === "MALE") {
payload.fatherId = member.id
} else {
if (!target.fatherId) {
payload.fatherId = member.id
} else if (!target.motherId) {
payload.motherId = member.id
} else {
throw new Error("该成员已绑定父母")
}
}
await updateMember(target.id, payload)
toast({
title: "关联成功",
description: `已将 ${target.fullName} 标记为 ${member.fullName} 的子女。`
})
} else {
const existing = new Set(member.spouseIds || [])
existing.add(target.id)
await updateMember(member.id, { spouseIds: Array.from(existing) })
toast({
title: "关联成功",
description: `${member.fullName}${target.fullName} 已建立配偶关系。`
})
}
onOpenChange(false)
} catch (error) {
const message = error instanceof Error ? error.message : "关联失败"
toast({
title: "操作失败",
description: message,
variant: "destructive",
})
} finally {
setLinkingId(null)
}
}
const handleCreateNew = () => {
if (!normalizedQuery) return
onCreateNew(query.trim(), relationType)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>{relationType === "child" ? "子女" : "配偶"}</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="relation-name"></Label>
<Input
id="relation-name"
placeholder="请输入姓名或字号"
value={query}
autoFocus
onChange={(e) => setQuery(e.target.value)}
/>
</div>
{normalizedQuery ? (
candidates.length > 0 ? (
<div>
<p className="text-sm text-muted-foreground mb-2"></p>
<ScrollArea className="max-h-64 rounded-md border">
<div className="divide-y">
{candidates.map(({ member: candidate, fatherName, motherName }) => (
<div key={candidate.id} className="p-3 flex flex-col gap-2">
<div className="flex items-center justify-between gap-3">
<div>
<MemberNameWithStatus name={candidate.fullName} isDead={!!candidate.deathDate} />
<p className="text-xs text-muted-foreground">
{candidate.generation} · {candidate.gender === "FEMALE" ? "女" : "男"}
</p>
</div>
<Button
size="sm"
onClick={() => handleLink(candidate)}
disabled={linkingId === candidate.id}
>
{linkingId === candidate.id ? "关联中..." : "关联"}
</Button>
</div>
<div className="text-xs text-muted-foreground">
<div>{fatherName || "未登记"}</div>
<div>{motherName || "未登记"}</div>
</div>
</div>
))}
</div>
</ScrollArea>
</div>
) : (
<p className="text-sm text-muted-foreground"></p>
)
) : (
<p className="text-sm text-muted-foreground"></p>
)}
</div>
<DialogFooter className="flex flex-col gap-2 sm:flex-row sm:justify-between">
<Button variant="outline" onClick={() => onOpenChange(false)} className="sm:flex-1">
</Button>
<Button onClick={handleCreateNew} disabled={!normalizedQuery} className="sm:flex-1">
{query || "成员"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}