Files
chinese-family-tree-2/components/members/member-form.tsx
T
freedakgmail 626d7dddde 0.0.0.5
2025-11-23 00:49:16 +08:00

595 lines
23 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 type React from "react"
import { useState } from "react"
import type { FamilyMember } from "@/types/family"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { CalendarIcon, User, Scroll, Users, Images, BookOpen } from "lucide-react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { ImageUpload } from "@/components/ui/image-upload"
import { PhotoGallery } from "./photo-gallery"
import { StoryManager } from "./story-manager"
interface MemberFormProps {
initialData?: Partial<FamilyMember>
existingMembers?: FamilyMember[]
onSubmit: (data: FamilyMember) => void
onCancel: () => void
}
export function MemberForm({ initialData, existingMembers = [], onSubmit, onCancel }: MemberFormProps) {
// 计算最大世系
const maxGeneration = existingMembers.length > 0
? Math.max(...existingMembers.map(m => m.generation || 1))
: 1
// Initialize state with default values or initialData
const [formData, setFormData] = useState<Partial<FamilyMember>>({
id: initialData?.id || crypto.randomUUID(),
surname: initialData?.surname || "李",
givenName: initialData?.givenName || "",
fullName: initialData?.fullName || "",
gender: initialData?.gender || "male",
generation: initialData?.generation || maxGeneration,
spouseIds: initialData?.spouseIds || [],
childrenIds: initialData?.childrenIds || [],
fatherId: initialData?.fatherId,
motherId: initialData?.motherId,
...initialData,
})
const [errors, setErrors] = useState<string[]>([])
const handleChange = (field: keyof FamilyMember, value: any) => {
setFormData((prev) => {
const newData = { ...prev, [field]: value }
// Auto-update full name if surname or given name changes
if (field === "surname" || field === "givenName") {
newData.fullName = (newData.surname || "") + (newData.givenName || "")
}
// 选择父亲时自动带出母亲
if (field === "fatherId" && value) {
const father = existingMembers.find(m => m.id === value)
if (father && father.spouseIds && father.spouseIds.length > 0) {
// 找到父亲的配偶(母亲)
const motherId = father.spouseIds[0]
newData.motherId = motherId
}
}
// 选择母亲时自动带出父亲
if (field === "motherId" && value) {
const mother = existingMembers.find(m => m.id === value)
if (mother && mother.spouseIds && mother.spouseIds.length > 0) {
// 找到母亲的配偶(父亲)
const fatherId = mother.spouseIds[0]
newData.fatherId = fatherId
}
}
return newData
})
// Clear errors when user makes changes
if (errors.length > 0) setErrors([])
}
const handleSpouseChange = (spouseId: string) => {
// If selecting "none" (empty string), clear spouses
if (!spouseId) {
setFormData((prev) => ({ ...prev, spouseIds: [] }))
return
}
// Otherwise set as single spouse (logic can be expanded for multiple later)
setFormData((prev) => ({ ...prev, spouseIds: [spouseId] }))
}
const validate = (): boolean => {
const newErrors: string[] = []
// 1. Date Validation
if (formData.birthDate && formData.deathDate) {
if (new Date(formData.birthDate) > new Date(formData.deathDate)) {
newErrors.push("出生日期不能晚于逝世日期")
}
}
// 2. Parent Age Validation
const birthYear = formData.birthDate ? new Date(formData.birthDate).getFullYear() : null
if (birthYear) {
if (formData.fatherId) {
const father = existingMembers.find((m) => m.id === formData.fatherId)
if (father?.birthDate) {
const fatherBirthYear = new Date(father.birthDate).getFullYear()
if (birthYear - fatherBirthYear < 15) {
newErrors.push(`与父亲的年龄差过小 (${birthYear - fatherBirthYear}岁),请确认`)
}
if (birthYear < fatherBirthYear) {
newErrors.push("出生日期不能早于父亲")
}
}
}
if (formData.motherId) {
const mother = existingMembers.find((m) => m.id === formData.motherId)
if (mother?.birthDate) {
const motherBirthYear = new Date(mother.birthDate).getFullYear()
if (birthYear - motherBirthYear < 15) {
newErrors.push(`与母亲的年龄差过小 (${birthYear - motherBirthYear}岁),请确认`)
}
if (birthYear < motherBirthYear) {
newErrors.push("出生日期不能早于母亲")
}
}
}
}
// 3. Generation Validation
if (formData.fatherId) {
const father = existingMembers.find((m) => m.id === formData.fatherId)
if (father && formData.generation !== father.generation + 1) {
newErrors.push(`世系错误:父亲是第 ${father.generation} 世,子女应为第 ${father.generation + 1}`)
}
}
// 4. Circular Reference Detection (循环引用检测)
if (formData.fatherId || formData.motherId) {
const visited = new Set<string>()
const checkCircular = (memberId: string): boolean => {
if (memberId === formData.id) return true
if (visited.has(memberId)) return false
visited.add(memberId)
const member = existingMembers.find((m) => m.id === memberId)
if (!member) return false
if (member.fatherId && checkCircular(member.fatherId)) return true
if (member.motherId && checkCircular(member.motherId)) return true
return false
}
if (formData.fatherId && checkCircular(formData.fatherId)) {
newErrors.push("检测到循环引用:不能将自己的后代设置为父亲")
}
if (formData.motherId && checkCircular(formData.motherId)) {
newErrors.push("检测到循环引用:不能将自己的后代设置为母亲")
}
}
// 5. Self-Reference Detection (自我引用检测)
if (formData.fatherId === formData.id) {
newErrors.push("不能将自己设置为自己的父亲")
}
if (formData.motherId === formData.id) {
newErrors.push("不能将自己设置为自己的母亲")
}
if (formData.spouseIds?.includes(formData.id || "")) {
newErrors.push("不能将自己设置为自己的配偶")
}
setErrors(newErrors)
return newErrors.length === 0
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (validate()) {
onSubmit(formData as FamilyMember)
}
}
const potentialRelatives = existingMembers.filter((m) => m.id !== formData.id)
return (
<form onSubmit={handleSubmit} className="space-y-8 max-w-4xl mx-auto p-6">
{errors.length > 0 && (
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md border border-destructive/20">
<p className="font-bold mb-1"></p>
<ul className="list-disc list-inside text-sm">
{errors.map((err, index) => (
<li key={index}>{err}</li>
))}
</ul>
</div>
)}
<div className="space-y-6">
{/* 基本信息和家庭关系 - 两列布局 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 左列:基本信息 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif">
<User className="h-5 w-5" />
(Identity)
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-center mb-4">
<ImageUpload
value={formData.avatarImageId}
onChange={(imageId) => handleChange("avatarImageId", imageId)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="surname"> (Surname)</Label>
<Input
id="surname"
value={formData.surname}
onChange={(e) => handleChange("surname", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="givenName"> (Given Name)</Label>
<Input
id="givenName"
value={formData.givenName}
onChange={(e) => handleChange("givenName", e.target.value)}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="gender"> (Gender)</Label>
<Select value={formData.gender} onValueChange={(val) => handleChange("gender", val)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="male"> (Male)</SelectItem>
<SelectItem value="female"> (Female)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="generation"> (Generation)</Label>
<Input
id="generation"
type="number"
value={formData.generation}
onChange={(e) => handleChange("generation", Number.parseInt(e.target.value))}
/>
</div>
</div>
</CardContent>
</Card>
{/* 右列:家庭关系 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif">
<Users className="h-5 w-5" />
(Relationships)
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="fatherId"> (Father)</Label>
<div className="flex gap-2">
<Select
value={formData.fatherId || "none"}
onValueChange={(val) => handleChange("fatherId", val === "none" ? undefined : val)}
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="选择父亲" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"> (None)</SelectItem>
{potentialRelatives
.filter((m) => m.gender === "male" && m.generation === (formData.generation || 1) - 1)
.map((m) => (
<SelectItem key={m.id} value={m.id}>
{m.fullName} ({m.generation})
</SelectItem>
))}
</SelectContent>
</Select>
{!formData.fatherId && (
<Input
placeholder="非家族成员填写姓名"
value={formData.spouseFatherName || ""}
onChange={(e) => handleChange("spouseFatherName", e.target.value)}
className="flex-1"
/>
)}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="motherId"> (Mother)</Label>
<div className="flex gap-2">
<Select
value={formData.motherId || "none"}
onValueChange={(val) => handleChange("motherId", val === "none" ? undefined : val)}
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="选择母亲" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"> (None)</SelectItem>
{potentialRelatives
.filter((m) => m.gender === "female" && m.generation === (formData.generation || 1) - 1)
.map((m) => (
<SelectItem key={m.id} value={m.id}>
{m.fullName} ({m.generation})
</SelectItem>
))}
</SelectContent>
</Select>
{!formData.motherId && (
<Input
placeholder="非家族成员填写姓名"
value={formData.spouseMotherName || ""}
onChange={(e) => handleChange("spouseMotherName", e.target.value)}
className="flex-1"
/>
)}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="spouseId"> (Spouse)</Label>
<Select value={formData.spouseIds?.[0] || "none"} onValueChange={handleSpouseChange}>
<SelectTrigger>
<SelectValue placeholder="选择配偶 Select Spouse" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"> (None)</SelectItem>
{potentialRelatives
.filter((m) => {
// 必须是同世系
if (m.generation !== formData.generation) return false
// 排除直系亲属
// 1. 排除父母
if (m.id === formData.fatherId || m.id === formData.motherId) return false
// 2. 排除子女
if (formData.childrenIds?.includes(m.id)) return false
// 3. 排除兄弟姐妹(同父或同母)
if (formData.fatherId && m.fatherId === formData.fatherId) return false
if (formData.motherId && m.motherId === formData.motherId) return false
return true
})
.map((m) => (
<SelectItem key={m.id} value={m.id}>
{m.fullName} ({m.generation})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
</div>
{/* Traditional Names */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif">
<Scroll className="h-5 w-5" />
(Traditional Names)
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="courtesyName"> (Courtesy Name)</Label>
<Input
id="courtesyName"
value={formData.courtesyName || ""}
onChange={(e) => handleChange("courtesyName", e.target.value)}
placeholder="e.g. 伯虎"
/>
</div>
<div className="space-y-2">
<Label htmlFor="artName"> (Art Name)</Label>
<Input
id="artName"
value={formData.artName || ""}
onChange={(e) => handleChange("artName", e.target.value)}
placeholder="e.g. 六如居士"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="generationName"> (Gen. Name)</Label>
<Input
id="generationName"
value={formData.generationName || ""}
onChange={(e) => handleChange("generationName", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="posthumousName"> (Posthumous)</Label>
<Input
id="posthumousName"
value={formData.posthumousName || ""}
onChange={(e) => handleChange("posthumousName", e.target.value)}
/>
</div>
</div>
</CardContent>
</Card>
{/* Dates & Places */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif">
<CalendarIcon className="h-5 w-5" />
(Life & Places)
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="birthDate"> (Birth)</Label>
<Input
id="birthDate"
type="date"
value={formData.birthDate || ""}
onChange={(e) => handleChange("birthDate", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="deathDate"> (Death)</Label>
<Input
id="deathDate"
type="date"
value={formData.deathDate || ""}
onChange={(e) => handleChange("deathDate", e.target.value)}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="ancestralHome"> (Ancestral Home)</Label>
<Input
id="ancestralHome"
value={formData.ancestralHome || ""}
onChange={(e) => handleChange("ancestralHome", e.target.value)}
placeholder="e.g. 福建省泉州市"
/>
</div>
<div className="space-y-2">
<Label htmlFor="burialPlace"> (Burial Place)</Label>
<Input
id="burialPlace"
value={formData.burialPlace || ""}
onChange={(e) => handleChange("burialPlace", e.target.value)}
/>
</div>
</CardContent>
</Card>
{/* Contact Information */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif">
<User className="h-5 w-5" />
(Contact Information)
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="phone"> (Mobile Phone)</Label>
<Input
id="phone"
type="tel"
value={formData.phone || ""}
onChange={(e) => handleChange("phone", e.target.value)}
placeholder="e.g. 13800138000"
/>
</div>
<div className="space-y-2">
<Label htmlFor="telephone"> (Telephone)</Label>
<Input
id="telephone"
type="tel"
value={formData.telephone || ""}
onChange={(e) => handleChange("telephone", e.target.value)}
placeholder="e.g. 0592-1234567"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email"> (Email)</Label>
<Input
id="email"
type="email"
value={formData.email || ""}
onChange={(e) => handleChange("email", e.target.value)}
placeholder="e.g. example@email.com"
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="address"> (Address)</Label>
<Input
id="address"
value={formData.address || ""}
onChange={(e) => handleChange("address", e.target.value)}
placeholder="e.g. 福建省厦门市思明区XX路XX号"
/>
</div>
</CardContent>
</Card>
{/* Biography */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif">
<Scroll className="h-5 w-5" />
(Biography)
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Textarea
className="min-h-[200px]"
placeholder="记录生平事迹..."
value={formData.bio || ""}
onChange={(e) => handleChange("bio", e.target.value)}
/>
</CardContent>
</Card>
{/* Photo Gallery */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif">
<Images className="h-5 w-5" />
(Photo Gallery)
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<PhotoGallery
photoIds={formData.photoIds || []}
onChange={(photoIds) => handleChange("photoIds", photoIds)}
/>
</CardContent>
</Card>
{/* Family Stories */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif">
<BookOpen className="h-5 w-5" />
(Family Stories)
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<StoryManager
stories={formData.stories || []}
onChange={(stories) => handleChange("stories", stories)}
/>
</CardContent>
</Card>
</div>
<div className="flex justify-end gap-4 sticky bottom-4 bg-background/90 p-4 border-t border-border backdrop-blur rounded-lg">
<Button type="button" variant="outline" onClick={onCancel}>
(Cancel)
</Button>
<Button type="submit" className="bg-primary text-primary-foreground hover:bg-primary/90">
(Save Member)
</Button>
</div>
</form>
)
}