Files
chinese-family-tree-2/components/members/member-form.tsx
T
freedakgmail 3375a79ed4 0.0.5.1
2025-11-23 15:56:24 +08:00

808 lines
31 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
// 获取始祖的姓氏(第1世成员)
const ancestorSurname = existingMembers.find(m => m.generation === 1)?.surname || ""
// Initialize state with default values or initialData
const [formData, setFormData] = useState<Partial<FamilyMember>>(() => {
const defaults = {
id: crypto.randomUUID(),
surname: ancestorSurname,
givenName: "",
fullName: "",
gender: "MALE" as "MALE" | "FEMALE",
generation: maxGeneration,
spouseIds: [],
childrenIds: [],
tags: [],
}
if (!initialData) return defaults
return {
...defaults,
...initialData,
gender: (initialData.gender || "MALE").toUpperCase() as "MALE" | "FEMALE",
spouseIds: initialData.spouseIds || [],
childrenIds: initialData.childrenIds || [],
tags: initialData.tags || [],
fatherId: initialData.fatherId || undefined,
motherId: initialData.motherId || undefined,
}
})
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 handleSpouseAdd = (spouseId: string) => {
if (!spouseId || spouseId === "none") return
setFormData((prev) => {
const currentSpouses = prev.spouseIds || []
// 避免重复添加
if (currentSpouses.includes(spouseId)) return prev
return {
...prev,
spouseIds: [...currentSpouses, spouseId]
}
})
}
const handleSpouseRemove = (spouseId: string) => {
setFormData((prev) => ({
...prev,
spouseIds: (prev.spouseIds || []).filter(id => id !== spouseId)
}))
}
const handleChildAdd = (childId: string) => {
if (!childId || childId === "none") return
setFormData((prev) => {
const currentChildren = prev.childrenIds || []
// 避免重复添加
if (currentChildren.includes(childId)) return prev
return {
...prev,
childrenIds: [...currentChildren, childId]
}
})
}
const handleChildRemove = (childId: string) => {
setFormData((prev) => ({
...prev,
childrenIds: (prev.childrenIds || []).filter(id => id !== childId)
}))
}
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="none" onValueChange={handleSpouseAdd}>
<SelectTrigger>
<SelectValue placeholder="添加配偶..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
{potentialRelatives
.filter((m) => {
// 必须是同世系
if (m.generation !== formData.generation) return false
// 通常配偶应该是异性
if (m.gender === formData.gender) return false
// 排除已选的配偶
if (formData.spouseIds?.includes(m.id)) return false
// 一夫多妻:女性可以选择已有配偶的男性,男性只能选择未婚女性
if (formData.gender === 'MALE') {
// 男性:排除已有配偶的女性
if (m.spouseIds && m.spouseIds.length > 0) 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>
{/* 已选配偶列表 */}
{formData.spouseIds && formData.spouseIds.length > 0 && (
<div className="mt-2 space-y-2">
{formData.spouseIds.map((spouseId, index) => {
const spouse = existingMembers.find(m => m.id === spouseId)
if (!spouse) return null
return (
<div key={spouseId} className="flex items-center justify-between p-2 bg-muted rounded">
<span className="text-sm">
{index + 1}. {spouse.fullName} ({spouse.generation})
</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => handleSpouseRemove(spouseId)}
className="h-6 w-6 p-0 hover:bg-destructive hover:text-destructive-foreground"
>
×
</Button>
</div>
)
})}
</div>
)}
</div>
<div className="space-y-2">
<Label htmlFor="childId"> (Children)</Label>
<Select value="none" onValueChange={handleChildAdd}>
<SelectTrigger>
<SelectValue placeholder="添加子女..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
{potentialRelatives
.filter((m) => {
// 必须是下一世系
if (m.generation !== (formData.generation || 1) + 1) return false
// 排除已选的子女
if (formData.childrenIds?.includes(m.id)) return false
// 排除配偶
if (formData.spouseIds?.includes(m.id)) return false
// 排除配偶身份的成员(没有父母但有配偶的)
const isSpouse = !m.fatherId && !m.motherId && m.spouseIds && m.spouseIds.length > 0
if (isSpouse) return false
// 根据当前成员性别,排除已有对应父母的子女
if (formData.gender === 'MALE') {
// 男性:排除已有父亲的子女
if (m.fatherId) return false
} else if (formData.gender === 'FEMALE') {
// 女性:排除已有母亲的子女
if (m.motherId) return false
}
return true
})
.map((m) => (
<SelectItem key={m.id} value={m.id}>
{m.fullName} ({m.generation})
</SelectItem>
))}
</SelectContent>
</Select>
{/* 已选子女列表 */}
{formData.childrenIds && formData.childrenIds.length > 0 && (
<div className="mt-2 space-y-2">
{formData.childrenIds.map((childId, index) => {
const child = existingMembers.find(m => m.id === childId)
if (!child) return null
return (
<div key={childId} className="flex items-center justify-between p-2 bg-muted rounded">
<span className="text-sm">
{index + 1}. {child.fullName} ({child.generation})
</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => handleChildRemove(childId)}
className="h-6 w-6 p-0 hover:bg-destructive hover:text-destructive-foreground"
>
×
</Button>
</div>
)
})}
</div>
)}
</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>
{/* Tags */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif">
<Scroll className="h-5 w-5" />
(Tags)
</CardTitle>
<CardDescription>便</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
<div className="flex gap-2">
<Input
placeholder="输入标签,按回车添加..."
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
const input = e.currentTarget
const value = input.value.trim()
if (value && !(formData.tags || []).includes(value)) {
handleChange('tags', [...(formData.tags || []), value])
input.value = ''
}
}
}}
/>
</div>
{formData.tags && formData.tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{formData.tags.map((tag, index) => (
<span
key={index}
className="inline-flex items-center gap-1 px-2 py-1 bg-primary/10 text-primary text-sm rounded-md border border-primary/20"
>
{tag}
<button
type="button"
onClick={() => {
const newTags = formData.tags?.filter((_, i) => i !== index) || []
handleChange('tags', newTags)
}}
className="ml-1 hover:text-destructive"
>
×
</button>
</span>
))}
</div>
)}
</div>
</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>
)
}