This commit is contained in:
freedakgmail
2025-11-24 14:02:34 +08:00
parent b7a8c9ee6e
commit 3d075c6076
941 changed files with 25613 additions and 27641 deletions
+211 -128
View File
@@ -9,20 +9,25 @@ 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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/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"
interface MemberFormProps {
initialData?: Partial<FamilyMember>
existingMembers?: FamilyMember[]
onSubmit: (data: FamilyMember) => void
onCancel: () => void
isFounder?: boolean
}
export function MemberForm({ initialData, existingMembers = [], onSubmit, onCancel }: MemberFormProps) {
export function MemberForm({ initialData, existingMembers = [], onSubmit, onCancel, isFounder = false }: MemberFormProps) {
// 计算最大世系
const maxGeneration = existingMembers.length > 0
? Math.max(...existingMembers.map(m => m.generation || 1))
@@ -31,10 +36,26 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
// 获取始祖的姓氏(第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: crypto.randomUUID(),
id: generateUUID(),
surname: ancestorSurname,
givenName: "",
fullName: "",
@@ -255,18 +276,18 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
{/* 基本信息和家庭关系 - 两列布局 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 左列:基本信息 */}
<Card>
<Card className={founderCardClass}>
<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)}
value={formData.avatarUrl}
onChange={(imageUrl) => handleChange("avatarUrl", imageUrl)}
/>
</div>
@@ -291,19 +312,19 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="gender"> (Gender)</Label>
<Label htmlFor="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>
<SelectItem value="MALE"></SelectItem>
<SelectItem value="FEMALE"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="generation"> (Generation)</Label>
<Label htmlFor="generation"></Label>
<Input
id="generation"
type="number"
@@ -316,80 +337,102 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
</Card>
{/* 右列:家庭关系 */}
<Card>
<Card className={founderCardClass}>
<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>
<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) => handleChange("fatherId", val === "none" ? undefined : val)}
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="选择父亲" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
{potentialRelatives
.filter((m) => 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="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>
<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) => handleChange("motherId", val === "none" ? undefined : val)}
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="选择母亲" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
{potentialRelatives
.filter((m) => 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="flex-1"
/>
)}
</div>
)}
</div>
<div className="space-y-2">
<Label htmlFor="spouseId"> (Spouse)</Label>
<Label htmlFor="spouseId"></Label>
<Select value="none" onValueChange={handleSpouseAdd}>
<SelectTrigger>
<SelectValue placeholder="添加配偶..." />
@@ -429,7 +472,10 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
})
.map((m) => (
<SelectItem key={m.id} value={m.id}>
{m.fullName} ({m.generation})
<MemberNameWithStatus
name={m.fullName}
isDead={!!m.deathDate}
/> ({m.generation})
</SelectItem>
))}
</SelectContent>
@@ -445,7 +491,10 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
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})
{index + 1}. <MemberNameWithStatus
name={spouse.fullName}
isDead={!!spouse.deathDate}
/> ({spouse.generation})
</span>
<Button
type="button"
@@ -464,7 +513,7 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
</div>
<div className="space-y-2">
<Label htmlFor="childId"> (Children)</Label>
<Label htmlFor="childId"></Label>
<Select value="none" onValueChange={handleChildAdd}>
<SelectTrigger>
<SelectValue placeholder="添加子女..." />
@@ -499,7 +548,10 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
})
.map((m) => (
<SelectItem key={m.id} value={m.id}>
{m.fullName} ({m.generation})
<MemberNameWithStatus
name={m.fullName}
isDead={!!m.deathDate}
/> ({m.generation})
</SelectItem>
))}
</SelectContent>
@@ -515,7 +567,10 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
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})
{index + 1}. <MemberNameWithStatus
name={child.fullName}
isDead={!!child.deathDate}
/> ({child.generation})
</span>
<Button
type="button"
@@ -537,37 +592,37 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
</div>
{/* Traditional Names */}
<Card>
<Card className={founderCardClass}>
<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>
<Label htmlFor="courtesyName"></Label>
<Input
id="courtesyName"
value={formData.courtesyName || ""}
onChange={(e) => handleChange("courtesyName", e.target.value)}
placeholder="e.g. 伯虎"
placeholder="例如:伯虎"
/>
</div>
<div className="space-y-2">
<Label htmlFor="artName"> (Art Name)</Label>
<Label htmlFor="artName"></Label>
<Input
id="artName"
value={formData.artName || ""}
onChange={(e) => handleChange("artName", e.target.value)}
placeholder="e.g. 六如居士"
placeholder="例如:六如居士"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="generationName"> (Gen. Name)</Label>
<Label htmlFor="generationName"></Label>
<Input
id="generationName"
value={formData.generationName || ""}
@@ -575,7 +630,7 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
/>
</div>
<div className="space-y-2">
<Label htmlFor="posthumousName"> (Posthumous)</Label>
<Label htmlFor="posthumousName"></Label>
<Input
id="posthumousName"
value={formData.posthumousName || ""}
@@ -587,45 +642,73 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
</Card>
{/* Dates & Places */}
<Card>
<Card className={founderCardClass}>
<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 className="grid grid-cols-1 md:grid-cols-2 gap-6">
<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>
{/* 统一的公历/农历选择 */}
{(formData.birthDate || formData.deathDate) && (
<div className="space-y-2 pt-2">
<Label className="text-sm font-medium"></Label>
<RadioGroup
value={formData.isLunarDate ? "lunar" : "solar"}
onValueChange={(val) => handleChange("isLunarDate", val === "lunar")}
className="flex flex-row gap-6"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="solar" id="date-type-solar" />
<Label htmlFor="date-type-solar" className="text-sm font-normal cursor-pointer">
()
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="lunar" id="date-type-lunar" />
<Label htmlFor="date-type-lunar" className="text-sm font-normal cursor-pointer">
()
</Label>
</div>
</RadioGroup>
<p className="text-xs text-muted-foreground">
{formData.isLunarDate
? "💡 将按农历日期计算每年的纪念日,如生日、祭日等"
: "💡 将按公历日期计算每年的纪念日"}
</p>
</div>
)}
<div className="space-y-2">
<Label htmlFor="ancestralHome"> (Ancestral Home)</Label>
<Label htmlFor="ancestralHome"></Label>
<Input
id="ancestralHome"
value={formData.ancestralHome || ""}
onChange={(e) => handleChange("ancestralHome", e.target.value)}
placeholder="e.g. 福建省泉州市"
placeholder="例如:福建省泉州市"
/>
</div>
<div className="space-y-2">
<Label htmlFor="burialPlace"> (Burial Place)</Label>
<Label htmlFor="burialPlace"></Label>
<Input
id="burialPlace"
value={formData.burialPlace || ""}
@@ -636,63 +719,63 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
</Card>
{/* Contact Information */}
<Card>
<Card className={founderCardClass}>
<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>
<Label htmlFor="phone"></Label>
<Input
id="phone"
type="tel"
value={formData.phone || ""}
onChange={(e) => handleChange("phone", e.target.value)}
placeholder="e.g. 13800138000"
placeholder="例如:13800138000"
/>
</div>
<div className="space-y-2">
<Label htmlFor="telephone"> (Telephone)</Label>
<Label htmlFor="telephone"></Label>
<Input
id="telephone"
type="tel"
value={formData.telephone || ""}
onChange={(e) => handleChange("telephone", e.target.value)}
placeholder="e.g. 0592-1234567"
placeholder="例如:0592-1234567"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email"> (Email)</Label>
<Label htmlFor="email"></Label>
<Input
id="email"
type="email"
value={formData.email || ""}
onChange={(e) => handleChange("email", e.target.value)}
placeholder="e.g. example@email.com"
placeholder="例如:example@email.com"
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="address"> (Address)</Label>
<Label htmlFor="address"></Label>
<Input
id="address"
value={formData.address || ""}
onChange={(e) => handleChange("address", e.target.value)}
placeholder="e.g. 福建省厦门市思明区XX路XX号"
placeholder="例如:福建省厦门市思明区XX路XX号"
/>
</div>
</CardContent>
</Card>
{/* Biography */}
<Card>
<Card className={founderCardClass}>
<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">
@@ -706,11 +789,11 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
</Card>
{/* Tags */}
<Card>
<Card className={founderCardClass}>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif">
<Scroll className="h-5 w-5" />
(Tags)
</CardTitle>
<CardDescription>便</CardDescription>
</CardHeader>
@@ -759,11 +842,11 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
</Card>
{/* Photo Gallery */}
<Card>
<Card className={founderCardClass}>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif">
<Images className="h-5 w-5" />
(Photo Gallery)
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
@@ -776,11 +859,11 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
</Card>
{/* Family Stories */}
<Card>
<Card className={founderCardClass}>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif">
<BookOpen className="h-5 w-5" />
(Family Stories)
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
@@ -796,10 +879,10 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
<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>