530 lines
20 KiB
TypeScript
530 lines
20 KiB
TypeScript
"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) {
|
||
// 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 || 1,
|
||
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 || "")
|
||
}
|
||
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="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
{/* Basic Identity */}
|
||
<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>
|
||
|
||
{/* 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>
|
||
|
||
{/* Relationships */}
|
||
<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="grid grid-cols-1 gap-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="fatherId">父亲 (Father)</Label>
|
||
<Select
|
||
value={formData.fatherId || "none"}
|
||
onValueChange={(val) => handleChange("fatherId", val === "none" ? undefined : val)}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="选择父亲 Select Father" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="none">无 (None)</SelectItem>
|
||
{potentialRelatives
|
||
.filter((m) => m.gender === "male")
|
||
.map((m) => (
|
||
<SelectItem key={m.id} value={m.id}>
|
||
{m.fullName} ({m.generation}世)
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="motherId">母亲 (Mother)</Label>
|
||
<Select
|
||
value={formData.motherId || "none"}
|
||
onValueChange={(val) => handleChange("motherId", val === "none" ? undefined : val)}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="选择母亲 Select Mother" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="none">无 (None)</SelectItem>
|
||
{potentialRelatives
|
||
.filter((m) => m.gender === "female")
|
||
.map((m) => (
|
||
<SelectItem key={m.id} value={m.id}>
|
||
{m.fullName} ({m.generation}世)
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</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) => m.gender !== formData.gender) // Suggest opposite gender
|
||
.map((m) => (
|
||
<SelectItem key={m.id} value={m.id}>
|
||
{m.fullName} ({m.generation}世)
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
</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>
|
||
)
|
||
}
|