This commit is contained in:
freedakgmail
2025-11-30 20:00:18 +08:00
parent 86e889cc2a
commit 0326894b09
6 changed files with 316 additions and 52 deletions
+216
View File
@@ -0,0 +1,216 @@
"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>
)
}