966 lines
41 KiB
TypeScript
966 lines
41 KiB
TypeScript
"use client"
|
||
|
||
import type React from "react"
|
||
|
||
import { useState, useEffect } from "react"
|
||
import type { FamilyMember, FamilyPhoto } 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 { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||
import { CalendarIcon, User, Scroll, Users, Images, BookOpen } from "lucide-react"
|
||
import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle, ChineseCardDescription } from "@/components/ui/chinese-card"
|
||
import { Switch } from "@/components/ui/switch"
|
||
import { ImageUpload } from "@/components/ui/image-upload"
|
||
import { PhotoGallery } from "./photo-gallery"
|
||
import { StoryManager } from "./story-manager"
|
||
import { DateInputWithLunar } from "@/components/ui/date-input-with-lunar"
|
||
import { MemberNameWithStatus } from "@/components/member-name-with-status"
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||
|
||
interface MemberFormProps {
|
||
initialData?: Partial<FamilyMember>
|
||
existingMembers?: FamilyMember[]
|
||
onSubmit: (data: FamilyMember) => void
|
||
onCancel: () => void
|
||
isFounder?: boolean
|
||
hideTabs?: boolean
|
||
activeTab?: string
|
||
}
|
||
|
||
export function MemberForm({ initialData, existingMembers = [], onSubmit, onCancel, isFounder = false, hideTabs = false, activeTab = "basic" }: 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 || ""
|
||
|
||
// 始祖专用样式
|
||
const founderCardClass = isFounder ? "border-2 border-amber-400 shadow-md" : ""
|
||
|
||
// 生成兼容的UUID
|
||
const generateUUID = () => {
|
||
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||
return crypto.randomUUID()
|
||
}
|
||
// 降级方案:生成简单的UUID
|
||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||
const r = Math.random() * 16 | 0
|
||
const v = c === 'x' ? r : (r & 0x3 | 0x8)
|
||
return v.toString(16)
|
||
})
|
||
}
|
||
|
||
// Initialize state with default values or initialData
|
||
const [formData, setFormData] = useState<Partial<FamilyMember>>(() => {
|
||
const defaults = {
|
||
id: generateUUID(),
|
||
surname: ancestorSurname,
|
||
givenName: "",
|
||
fullName: "",
|
||
gender: "MALE" as "MALE" | "FEMALE",
|
||
generation: maxGeneration,
|
||
spouseIds: [],
|
||
childrenIds: [],
|
||
tags: [],
|
||
photos: [] as FamilyPhoto[],
|
||
photoIds: [] as string[],
|
||
}
|
||
|
||
if (!initialData) return defaults
|
||
|
||
const normalizedPhotos: FamilyPhoto[] = initialData.photos && initialData.photos.length > 0
|
||
? initialData.photos
|
||
: (initialData.photoIds || []).map((url) => ({
|
||
url,
|
||
uploadedAt: initialData.updatedAt || new Date().toISOString(),
|
||
}))
|
||
|
||
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,
|
||
photos: normalizedPhotos,
|
||
photoIds: initialData.photoIds || normalizedPhotos.map((photo) => photo.url),
|
||
}
|
||
})
|
||
|
||
const [errors, setErrors] = useState<string[]>([])
|
||
|
||
// 监听 initialData 变化,更新 formData
|
||
useEffect(() => {
|
||
if (!initialData) return
|
||
|
||
setFormData((prev) => {
|
||
const normalizedPhotos: FamilyPhoto[] = initialData.photos && initialData.photos.length > 0
|
||
? initialData.photos
|
||
: (initialData.photoIds || []).map((url) => ({
|
||
url,
|
||
uploadedAt: initialData.updatedAt || new Date().toISOString(),
|
||
}))
|
||
|
||
// 优先使用 initialData 中的 motherId,如果没有才使用之前的值
|
||
const newMotherId = initialData.motherId !== undefined ? initialData.motherId : prev.motherId
|
||
console.log('Calculating new motherId:', {
|
||
initial: initialData.motherId,
|
||
prev: prev.motherId,
|
||
result: newMotherId,
|
||
hasInitialMother: initialData.motherId !== undefined
|
||
})
|
||
|
||
return {
|
||
...prev,
|
||
...initialData,
|
||
// 确保特定字段被正确更新
|
||
gender: initialData.gender ? (initialData.gender.toUpperCase() as "MALE" | "FEMALE") : prev.gender,
|
||
spouseIds: initialData.spouseIds || prev.spouseIds || [],
|
||
childrenIds: initialData.childrenIds || prev.childrenIds || [],
|
||
tags: initialData.tags || prev.tags || [],
|
||
fatherId: initialData.fatherId || prev.fatherId,
|
||
motherId: newMotherId,
|
||
photos: initialData.photos ? normalizedPhotos : prev.photos,
|
||
photoIds: initialData.photoIds || (initialData.photos ? normalizedPhotos.map((p) => p.url) : prev.photoIds),
|
||
}
|
||
})
|
||
}, [initialData])
|
||
|
||
const handleChange = (field: keyof FamilyMember, value: any) => {
|
||
console.log('handleChange:', field, value)
|
||
|
||
setFormData((prev) => {
|
||
const newData = { ...prev, [field]: value }
|
||
|
||
// 自动更新全名
|
||
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]
|
||
if (!newData.motherId) { // Only auto-set if motherId is not already present
|
||
newData.motherId = motherId
|
||
console.log('Auto-set mother from father:', { fatherId: value, 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]
|
||
if (!newData.fatherId) { // Only auto-set if fatherId is not already present
|
||
newData.fatherId = fatherId
|
||
console.log('Auto-set father from mother:', { motherId: value, fatherId })
|
||
}
|
||
}
|
||
}
|
||
|
||
return newData
|
||
})
|
||
}
|
||
|
||
const handlePhotosChange = (photos: FamilyPhoto[]) => {
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
photos,
|
||
photoIds: photos.map(p => p.url),
|
||
}))
|
||
}
|
||
|
||
const handleSpouseAdd = (spouseId: string) => {
|
||
if (spouseId === "none") return
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
spouseIds: [...(prev.spouseIds || []), spouseId],
|
||
}))
|
||
}
|
||
|
||
const handleSpouseRemove = (spouseId: string) => {
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
spouseIds: (prev.spouseIds || []).filter((id) => id !== spouseId),
|
||
}))
|
||
}
|
||
|
||
const handleChildAdd = (childId: string) => {
|
||
if (childId === "none") return
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
childrenIds: [...(prev.childrenIds || []), childId],
|
||
}))
|
||
}
|
||
|
||
const handleChildRemove = (childId: string) => {
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
childrenIds: (prev.childrenIds || []).filter((id) => id !== childId),
|
||
}))
|
||
}
|
||
|
||
const validateForm = (): boolean => {
|
||
const newErrors: string[] = []
|
||
|
||
if (!formData.surname?.trim()) {
|
||
newErrors.push("请填写姓氏")
|
||
}
|
||
|
||
if (!formData.givenName?.trim()) {
|
||
newErrors.push("请填写名字")
|
||
}
|
||
|
||
if (!formData.gender) {
|
||
newErrors.push("请选择性别")
|
||
}
|
||
|
||
if (!formData.generation || formData.generation < 1) {
|
||
newErrors.push("请填写正确的世系")
|
||
}
|
||
|
||
// 注:允许孤立成员存在,可以后续通过族谱关联功能建立关系
|
||
// 这样可以修正错误的关系设置
|
||
|
||
setErrors(newErrors)
|
||
return newErrors.length === 0
|
||
}
|
||
|
||
const handleSubmit = (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
|
||
if (!validateForm()) {
|
||
return
|
||
}
|
||
|
||
// 确保所有必填字段都有值
|
||
// 注意:fatherId/motherId 需要显式设为 null 而不是 undefined,
|
||
// 否则 JSON.stringify 会忽略 undefined 值,导致后端收不到更新
|
||
const memberData: FamilyMember = {
|
||
id: formData.id || generateUUID(),
|
||
surname: formData.surname || "",
|
||
givenName: formData.givenName || "",
|
||
fullName: formData.fullName || `${formData.surname || ""}${formData.givenName || ""}`,
|
||
gender: formData.gender as "MALE" | "FEMALE",
|
||
generation: formData.generation || 1,
|
||
spouseIds: formData.spouseIds || [],
|
||
childrenIds: formData.childrenIds || [],
|
||
tags: formData.tags || [],
|
||
photos: formData.photos || [],
|
||
photoIds: formData.photoIds || [],
|
||
fatherId: formData.fatherId,
|
||
motherId: formData.motherId,
|
||
spouseFatherName: formData.spouseFatherName,
|
||
spouseMotherName: formData.spouseMotherName,
|
||
phone: formData.phone,
|
||
telephone: formData.telephone,
|
||
email: formData.email,
|
||
occupation: formData.occupation,
|
||
courtesyName: formData.courtesyName,
|
||
artName: formData.artName,
|
||
generationName: formData.generationName,
|
||
posthumousName: formData.posthumousName,
|
||
birthDate: formData.birthDate,
|
||
deathDate: formData.deathDate,
|
||
isLunarDate: formData.isLunarDate || false,
|
||
ancestralHome: formData.ancestralHome,
|
||
birthPlace: formData.birthPlace,
|
||
deathPlace: formData.deathPlace,
|
||
burialPlace: formData.burialPlace,
|
||
address: formData.address,
|
||
bio: formData.bio,
|
||
avatarUrl: formData.avatarUrl,
|
||
stories: formData.stories || [],
|
||
createdAt: formData.createdAt || new Date().toISOString(),
|
||
updatedAt: new Date().toISOString(),
|
||
}
|
||
|
||
onSubmit(memberData)
|
||
}
|
||
|
||
const potentialRelatives = existingMembers.filter((m) => m.id !== formData.id)
|
||
|
||
// 调试日志
|
||
console.log('MemberForm render:', {
|
||
motherId: formData.motherId,
|
||
existingMembersCount: existingMembers.length,
|
||
motherInList: existingMembers.find(m => m.id === formData.motherId)
|
||
})
|
||
|
||
return (
|
||
<form onSubmit={handleSubmit} className="w-full">
|
||
{errors.length > 0 && (
|
||
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md border border-destructive/20 mb-6">
|
||
<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>
|
||
)}
|
||
|
||
<Tabs defaultValue="info" value={hideTabs ? activeTab : undefined} className="w-full flex flex-col h-full">
|
||
{!hideTabs && (
|
||
<div className="flex-shrink-0">
|
||
<TabsList className="grid w-full grid-cols-4">
|
||
<TabsTrigger value="info">基本信息</TabsTrigger>
|
||
<TabsTrigger value="details">详细资料</TabsTrigger>
|
||
<TabsTrigger value="photos">生平影像</TabsTrigger>
|
||
<TabsTrigger value="stories">家族故事</TabsTrigger>
|
||
</TabsList>
|
||
</div>
|
||
)}
|
||
|
||
<div className="flex-1 overflow-y-auto">
|
||
{/* 基本信息Tab - 包含基本信息和家庭关系 */}
|
||
<TabsContent value="info" className="space-y-6 mt-6 w-full">
|
||
{/* 基本信息卡片 */}
|
||
<ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
|
||
<ChineseCardHeader>
|
||
<ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
|
||
<User className="h-5 w-5" />
|
||
基本信息
|
||
</ChineseCardTitle>
|
||
</ChineseCardHeader>
|
||
<ChineseCardContent className="space-y-6">
|
||
{/* 头像和基本信息两栏布局 */}
|
||
<div className="grid grid-cols-1 lg:grid-cols-[200px_1fr] gap-8">
|
||
{/* 左侧:头像 */}
|
||
<div className="flex flex-col items-center">
|
||
<ImageUpload
|
||
value={formData.avatarUrl}
|
||
onChange={(imageUrl) => handleChange("avatarUrl", imageUrl || null)}
|
||
/>
|
||
</div>
|
||
|
||
{/* 右侧:表单 */}
|
||
<div className="space-y-6">
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="surname">
|
||
姓氏 <span className="text-red-500">*</span>
|
||
</Label>
|
||
<Input
|
||
id="surname"
|
||
value={formData.surname || ""}
|
||
onChange={(e) => handleChange("surname", e.target.value)}
|
||
placeholder="例如:李"
|
||
className={errors.includes("surname") ? "border-red-500" : ""}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="givenName">
|
||
名字 <span className="text-red-500">*</span>
|
||
</Label>
|
||
<Input
|
||
id="givenName"
|
||
value={formData.givenName || ""}
|
||
onChange={(e) => handleChange("givenName", e.target.value)}
|
||
placeholder="例如:明"
|
||
className={errors.includes("givenName") ? "border-red-500" : ""}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label>性别</Label>
|
||
<RadioGroup
|
||
value={formData.gender}
|
||
onValueChange={(val) => handleChange("gender", val)}
|
||
className="flex space-x-4"
|
||
>
|
||
<div className="flex items-center space-x-2">
|
||
<RadioGroupItem value="MALE" id="male" />
|
||
<Label htmlFor="male">男</Label>
|
||
</div>
|
||
<div className="flex items-center space-x-2">
|
||
<RadioGroupItem value="FEMALE" id="female" />
|
||
<Label htmlFor="female">女</Label>
|
||
</div>
|
||
</RadioGroup>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="generation">世代 (第几世)</Label>
|
||
<div className="flex items-center gap-4">
|
||
<Input
|
||
id="generation"
|
||
type="number"
|
||
value={formData.generation || ""}
|
||
onChange={(e) => handleChange("generation", parseInt(e.target.value) || 0)}
|
||
className="w-32"
|
||
min={1}
|
||
/>
|
||
<span className="text-sm text-muted-foreground">世</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</ChineseCardContent>
|
||
</ChineseCard>
|
||
|
||
{/* 家庭关系 - 左右两个独立卡片 */}
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
{/* 左栏:父母 */}
|
||
<ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
|
||
<ChineseCardHeader>
|
||
<ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
|
||
<Users className="h-5 w-5" />
|
||
父母
|
||
</ChineseCardTitle>
|
||
{!isFounder && (
|
||
<ChineseCardDescription className="text-xs">
|
||
需关联父母或配偶以连接至家族树
|
||
</ChineseCardDescription>
|
||
)}
|
||
</ChineseCardHeader>
|
||
<ChineseCardContent>
|
||
<div className="space-y-3">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="fatherId">父亲</Label>
|
||
{isFounder ? (
|
||
<Input
|
||
placeholder="填写父亲姓名"
|
||
value={formData.spouseFatherName || ""}
|
||
onChange={(e) => handleChange("spouseFatherName", e.target.value)}
|
||
/>
|
||
) : (
|
||
<div className="flex gap-2">
|
||
<Select
|
||
value={formData.fatherId || "none"}
|
||
onValueChange={(val) => {
|
||
const newValue = val === "none" ? undefined : val
|
||
console.log('Father select change:', { val, newValue })
|
||
handleChange("fatherId", newValue)
|
||
}}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="选择父亲" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="none">无</SelectItem>
|
||
{potentialRelatives
|
||
.filter((m) => {
|
||
if (m.id === formData.fatherId) return true
|
||
return m.gender === "MALE" && m.generation === (formData.generation || 1) - 1
|
||
})
|
||
.map((m) => (
|
||
<SelectItem key={m.id} value={m.id}>
|
||
<MemberNameWithStatus
|
||
name={m.fullName}
|
||
isDead={!!m.deathDate}
|
||
/> ({m.generation}世)
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
{!formData.fatherId && (
|
||
<Input
|
||
placeholder="或填写姓名"
|
||
value={formData.spouseFatherName || ""}
|
||
onChange={(e) => handleChange("spouseFatherName", e.target.value)}
|
||
className="w-32"
|
||
/>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="motherId">母亲</Label>
|
||
{isFounder ? (
|
||
<Input
|
||
placeholder="填写母亲姓名"
|
||
value={formData.spouseMotherName || ""}
|
||
onChange={(e) => handleChange("spouseMotherName", e.target.value)}
|
||
/>
|
||
) : (
|
||
<div className="flex gap-2">
|
||
<Select
|
||
value={formData.motherId || "none"}
|
||
onValueChange={(val) => {
|
||
const newValue = val === "none" ? undefined : val
|
||
console.log('Mother select change:', { val, newValue })
|
||
handleChange("motherId", newValue)
|
||
}}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="选择母亲" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="none">无</SelectItem>
|
||
{potentialRelatives
|
||
.filter((m) => {
|
||
if (m.id === formData.motherId) return true
|
||
return m.gender === "FEMALE" && m.generation === (formData.generation || 1) - 1
|
||
})
|
||
.map((m) => (
|
||
<SelectItem key={m.id} value={m.id}>
|
||
<MemberNameWithStatus
|
||
name={m.fullName}
|
||
isDead={!!m.deathDate}
|
||
/> ({m.generation}世)
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
{!formData.motherId && (
|
||
<Input
|
||
placeholder="或填写姓名"
|
||
value={formData.spouseMotherName || ""}
|
||
onChange={(e) => handleChange("spouseMotherName", e.target.value)}
|
||
className="w-32"
|
||
/>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</ChineseCardContent>
|
||
</ChineseCard>
|
||
|
||
{/* 右栏:配偶和子女 */}
|
||
<ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
|
||
<ChineseCardHeader>
|
||
<ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
|
||
<Users className="h-5 w-5" />
|
||
配偶与子女
|
||
</ChineseCardTitle>
|
||
{!isFounder && (
|
||
<ChineseCardDescription className="text-xs">
|
||
需关联父母或配偶以连接至家族树
|
||
</ChineseCardDescription>
|
||
)}
|
||
</ChineseCardHeader>
|
||
<ChineseCardContent>
|
||
<div className="space-y-3">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="spouseId">配偶</Label>
|
||
<Select value="none" onValueChange={handleSpouseAdd}>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="添加配偶..." />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="none">选择配偶</SelectItem>
|
||
{potentialRelatives
|
||
.filter((m) => {
|
||
if (formData.spouseIds?.includes(m.id)) return false
|
||
if (formData.fatherId === m.id || formData.motherId === m.id) return false
|
||
if (formData.childrenIds?.includes(m.id)) return false
|
||
if (m.generation !== (formData.generation || 1)) return false
|
||
if (m.gender === formData.gender) return false
|
||
return true
|
||
})
|
||
.map((m) => (
|
||
<SelectItem key={m.id} value={m.id}>
|
||
<MemberNameWithStatus
|
||
name={m.fullName}
|
||
isDead={!!m.deathDate}
|
||
/> ({m.generation}世)
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
|
||
{formData.spouseIds && formData.spouseIds.length > 0 && (
|
||
<div className="mt-2 space-y-1">
|
||
{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 text-sm">
|
||
<span>
|
||
{index + 1}. <MemberNameWithStatus
|
||
name={spouse.fullName}
|
||
isDead={!!spouse.deathDate}
|
||
/>
|
||
</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">子女</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}>
|
||
<MemberNameWithStatus
|
||
name={m.fullName}
|
||
isDead={!!m.deathDate}
|
||
/> ({m.generation}世)
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
|
||
{formData.childrenIds && formData.childrenIds.length > 0 && (
|
||
<div className="mt-2 space-y-1">
|
||
{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 text-sm">
|
||
<span>
|
||
{index + 1}. <MemberNameWithStatus
|
||
name={child.fullName}
|
||
isDead={!!child.deathDate}
|
||
/>
|
||
</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>
|
||
</div>
|
||
</ChineseCardContent>
|
||
</ChineseCard>
|
||
</div>
|
||
</TabsContent>
|
||
|
||
{/* 详细资料Tab */}
|
||
<TabsContent value="details" className="space-y-6 mt-6 w-full">
|
||
{/* 使用两列布局放置两个卡片 */}
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
<ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
|
||
<ChineseCardHeader>
|
||
<ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
|
||
<Scroll className="h-5 w-5" />
|
||
传统称谓
|
||
</ChineseCardTitle>
|
||
</ChineseCardHeader>
|
||
<ChineseCardContent className="space-y-4">
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="courtesyName">字</Label>
|
||
<Input
|
||
id="courtesyName"
|
||
value={formData.courtesyName || ""}
|
||
onChange={(e) => handleChange("courtesyName", e.target.value)}
|
||
placeholder="例如:伯虎"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="artName">号</Label>
|
||
<Input
|
||
id="artName"
|
||
value={formData.artName || ""}
|
||
onChange={(e) => handleChange("artName", e.target.value)}
|
||
placeholder="例如:六如居士"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="generationName">字辈</Label>
|
||
<Input
|
||
id="generationName"
|
||
value={formData.generationName || ""}
|
||
onChange={(e) => handleChange("generationName", e.target.value)}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="posthumousName">谥号</Label>
|
||
<Input
|
||
id="posthumousName"
|
||
value={formData.posthumousName || ""}
|
||
onChange={(e) => handleChange("posthumousName", e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</ChineseCardContent>
|
||
</ChineseCard>
|
||
|
||
<ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
|
||
<ChineseCardHeader>
|
||
<ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
|
||
<CalendarIcon className="h-5 w-5" />
|
||
生平日期
|
||
</ChineseCardTitle>
|
||
</ChineseCardHeader>
|
||
<ChineseCardContent className="space-y-4">
|
||
<div className="grid grid-cols-1 gap-4">
|
||
<DateInputWithLunar
|
||
id="birthDate"
|
||
label="出生日期"
|
||
value={formData.birthDate || ""}
|
||
isLunar={formData.isLunarDate || false}
|
||
onChange={(value) => handleChange("birthDate", value)}
|
||
showLunarToggle={false}
|
||
/>
|
||
<DateInputWithLunar
|
||
id="deathDate"
|
||
label="逝世日期"
|
||
value={formData.deathDate || ""}
|
||
isLunar={formData.isLunarDate || false}
|
||
onChange={(value) => handleChange("deathDate", value)}
|
||
showLunarToggle={false}
|
||
/>
|
||
</div>
|
||
</ChineseCardContent>
|
||
</ChineseCard>
|
||
</div>
|
||
|
||
{/* 地点和简介卡片 */}
|
||
<ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
|
||
<ChineseCardHeader>
|
||
<ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
|
||
<Scroll className="h-5 w-5" />
|
||
地点与简介
|
||
</ChineseCardTitle>
|
||
</ChineseCardHeader>
|
||
<ChineseCardContent className="space-y-4">
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="ancestralHome">籍贯</Label>
|
||
<Input
|
||
id="ancestralHome"
|
||
value={formData.ancestralHome || ""}
|
||
onChange={(e) => handleChange("ancestralHome", e.target.value)}
|
||
placeholder="请输入籍贯"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="birthPlace">出生地</Label>
|
||
<Input
|
||
id="birthPlace"
|
||
value={formData.birthPlace || ""}
|
||
onChange={(e) => handleChange("birthPlace", e.target.value)}
|
||
placeholder="请输入出生地"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="address">居住地</Label>
|
||
<Input
|
||
id="address"
|
||
value={formData.address || ""}
|
||
onChange={(e) => handleChange("address", e.target.value)}
|
||
placeholder="请输入居住地"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="deathPlace">逝世地</Label>
|
||
<Input
|
||
id="deathPlace"
|
||
value={formData.deathPlace || ""}
|
||
onChange={(e) => handleChange("deathPlace", e.target.value)}
|
||
placeholder="请输入逝世地"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="burialPlace">安葬地</Label>
|
||
<Input
|
||
id="burialPlace"
|
||
value={formData.burialPlace || ""}
|
||
onChange={(e) => handleChange("burialPlace", e.target.value)}
|
||
placeholder="请输入安葬地"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="occupation">职业</Label>
|
||
<Input
|
||
id="occupation"
|
||
value={formData.occupation || ""}
|
||
onChange={(e) => handleChange("occupation", e.target.value)}
|
||
placeholder="请输入职业"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="phone">手机号</Label>
|
||
<Input
|
||
id="phone"
|
||
value={formData.phone || ""}
|
||
onChange={(e) => handleChange("phone", e.target.value)}
|
||
placeholder="请输入手机号"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="telephone">固定电话</Label>
|
||
<Input
|
||
id="telephone"
|
||
value={formData.telephone || ""}
|
||
onChange={(e) => handleChange("telephone", e.target.value)}
|
||
placeholder="请输入固定电话"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="email">电子邮箱</Label>
|
||
<Input
|
||
id="email"
|
||
type="email"
|
||
value={formData.email || ""}
|
||
onChange={(e) => handleChange("email", e.target.value)}
|
||
placeholder="请输入电子邮箱"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="bio">个人简介</Label>
|
||
<Textarea
|
||
id="bio"
|
||
value={formData.bio || ""}
|
||
onChange={(e) => handleChange("bio", e.target.value)}
|
||
placeholder="请输入个人简介"
|
||
rows={4}
|
||
/>
|
||
</div>
|
||
</ChineseCardContent>
|
||
</ChineseCard>
|
||
</TabsContent>
|
||
|
||
{/* 生平影像Tab */}
|
||
<TabsContent value="photos" className="space-y-6 mt-6 w-full">
|
||
<ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
|
||
<ChineseCardHeader>
|
||
<ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
|
||
<Images className="h-5 w-5" />
|
||
照片墙
|
||
</ChineseCardTitle>
|
||
<ChineseCardDescription>上传多张照片记录珍贵时刻</ChineseCardDescription>
|
||
</ChineseCardHeader>
|
||
<ChineseCardContent>
|
||
<PhotoGallery
|
||
photos={formData.photos}
|
||
photoIds={formData.photoIds}
|
||
onChange={handlePhotosChange}
|
||
/>
|
||
</ChineseCardContent>
|
||
</ChineseCard>
|
||
</TabsContent>
|
||
|
||
{/* 家族故事Tab */}
|
||
<TabsContent value="stories" className="space-y-6 mt-6 w-full">
|
||
<ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
|
||
<ChineseCardHeader>
|
||
<ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
|
||
<BookOpen className="h-5 w-5" />
|
||
家族故事
|
||
</ChineseCardTitle>
|
||
<ChineseCardDescription>记录与此成员相关的家族故事</ChineseCardDescription>
|
||
</ChineseCardHeader>
|
||
<ChineseCardContent>
|
||
<StoryManager
|
||
stories={formData.stories || []}
|
||
onChange={(stories) => handleChange("stories", stories)}
|
||
/>
|
||
</ChineseCardContent>
|
||
</ChineseCard>
|
||
|
||
<ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
|
||
<ChineseCardHeader>
|
||
<ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
|
||
<Scroll className="h-5 w-5" />
|
||
标签
|
||
</ChineseCardTitle>
|
||
<ChineseCardDescription>添加标签以便分类和搜索(按回车添加)</ChineseCardDescription>
|
||
</ChineseCardHeader>
|
||
<ChineseCardContent 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>
|
||
</ChineseCardContent>
|
||
</ChineseCard>
|
||
</TabsContent>
|
||
</div>
|
||
</Tabs>
|
||
</form>
|
||
)
|
||
}
|