0.0.8.5
This commit is contained in:
+211
-128
@@ -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>
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { User } from "lucide-react"
|
||||
import { format } from "date-fns"
|
||||
import { zhCN } from "date-fns/locale"
|
||||
|
||||
interface MemberHistory {
|
||||
id: string
|
||||
memberId: string
|
||||
version: number
|
||||
snapshot: any
|
||||
changedBy: string
|
||||
changedAt: string
|
||||
changeType: "CREATE" | "UPDATE" | "DELETE"
|
||||
changes: Record<string, { old: any, new: any }> | null
|
||||
user?: {
|
||||
name?: string | null
|
||||
email?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
interface MemberVersionHistoryProps {
|
||||
memberId: string
|
||||
treeId: string
|
||||
onVersionSelect?: (version: MemberHistory) => void
|
||||
}
|
||||
|
||||
export function MemberVersionHistory({ memberId, treeId, onVersionSelect }: MemberVersionHistoryProps) {
|
||||
const [history, setHistory] = useState<MemberHistory[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
loadHistory()
|
||||
}, [memberId, treeId])
|
||||
|
||||
const loadHistory = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const res = await fetch(`/api/trees/${treeId}/members/${memberId}/history`)
|
||||
if (!res.ok) throw new Error("获取历史失败")
|
||||
|
||||
const data = await res.json()
|
||||
setHistory(data.history || [])
|
||||
} catch (error) {
|
||||
console.error("加载历史失败:", error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const getChangeTypeLabel = (type: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
CREATE: "创建",
|
||||
UPDATE: "更新",
|
||||
DELETE: "删除",
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
|
||||
const getChangeTypeColor = (type: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
CREATE: "bg-green-100 text-green-700 border-green-200",
|
||||
UPDATE: "bg-blue-100 text-blue-700 border-blue-200",
|
||||
DELETE: "bg-red-100 text-red-700 border-red-200",
|
||||
}
|
||||
return colors[type] || "bg-gray-100 text-gray-700 border-gray-200"
|
||||
}
|
||||
|
||||
const fieldLabels: Record<string, string> = {
|
||||
fullName: '姓名', surname: '姓氏', givenName: '名字', gender: '性别',
|
||||
birthDate: '出生日期', deathDate: '去世日期', birthPlace: '出生地',
|
||||
ancestralHome: '祖籍', generation: '世代', generationName: '字辈',
|
||||
courtesyName: '字', artName: '号', posthumousName: '谥号', rank: '排行',
|
||||
bio: '简介', phone: '手机', telephone: '电话', email: '邮箱',
|
||||
address: '地址', photoIds: '照片', spouseIds: '配偶', childrenIds: '子女',
|
||||
motherId: '母亲', fatherId: '父亲', isFounder: '始祖',
|
||||
isLunarDate: '农历日期', burialPlace: '安葬地', tags: '标签',
|
||||
spouseFatherName: '岳父/公公', spouseMotherName: '岳母/婆婆',
|
||||
}
|
||||
|
||||
const formatValue = (value: any) => {
|
||||
if (value === null || value === undefined) return '-'
|
||||
if (typeof value === 'boolean') return value ? '是' : '否'
|
||||
if (Array.isArray(value)) return value.length > 0 ? value.join(', ') : '-'
|
||||
return String(value)
|
||||
}
|
||||
|
||||
const renderVersionChanges = (changes: Record<string, { old: any, new: any }> | null, changeType: string) => {
|
||||
// 如果是创建操作,不显示变更详情
|
||||
if (changeType === 'CREATE') {
|
||||
return (
|
||||
<div className="mt-3 text-xs text-muted-foreground">
|
||||
创建了新成员
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 如果没有变更信息
|
||||
if (!changes || Object.keys(changes).length === 0) {
|
||||
return (
|
||||
<div className="mt-3 text-xs text-muted-foreground">
|
||||
无变更记录
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 分组显示变更
|
||||
const relationFields = ['fatherId', 'motherId', 'spouseIds', 'childrenIds', 'spouseFatherName', 'spouseMotherName']
|
||||
const changedRelations = Object.entries(changes).filter(([key]) => relationFields.includes(key))
|
||||
const changedOthers = Object.entries(changes).filter(([key]) => !relationFields.includes(key))
|
||||
|
||||
return (
|
||||
<div className="mt-3 space-y-4">
|
||||
{/* 家族关系变更 */}
|
||||
{changedRelations.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-foreground mb-2">家族关系变更</h4>
|
||||
<div className="space-y-2 text-xs">
|
||||
{changedRelations.map(([field, change]) => (
|
||||
<div key={field} className="flex items-start gap-2">
|
||||
<span className="text-muted-foreground min-w-[60px]">{fieldLabels[field]}:</span>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-red-600 line-through">{formatValue(change.old)}</span>
|
||||
<span className="text-muted-foreground">→</span>
|
||||
<span className="text-green-600 font-medium">{formatValue(change.new)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 其他信息变更 */}
|
||||
{changedOthers.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-foreground mb-2">详细信息变更</h4>
|
||||
<div className="space-y-2 text-xs">
|
||||
{changedOthers.map(([field, change]) => (
|
||||
<div key={field} className="flex items-start gap-2">
|
||||
<span className="text-muted-foreground min-w-[60px]">{fieldLabels[field]}:</span>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-red-600 line-through">{formatValue(change.old)}</span>
|
||||
<span className="text-muted-foreground">→</span>
|
||||
<span className="text-green-600 font-medium">{formatValue(change.new)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
加载中...
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (history.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
暂无历史记录
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
共 {history.length} 个版本
|
||||
</p>
|
||||
<ScrollArea className="h-[calc(100vh-300px)] pr-4">
|
||||
<div className="space-y-6">
|
||||
{history.map((record) => (
|
||||
<div
|
||||
key={record.id}
|
||||
className="p-4 rounded-lg border border-border bg-card shadow-sm"
|
||||
>
|
||||
{/* 版本头部 */}
|
||||
<div className="flex items-center justify-between mb-3 pb-3 border-b border-border">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className={getChangeTypeColor(record.changeType)}>
|
||||
{getChangeTypeLabel(record.changeType)}
|
||||
</Badge>
|
||||
<span className="text-sm font-semibold">
|
||||
版本 {record.version}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{format(new Date(record.changedAt), 'yyyy-MM-dd HH:mm', { locale: zhCN })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作人 */}
|
||||
{record.user && (
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground mb-3">
|
||||
<User className="h-3 w-3" />
|
||||
<span>{record.user.name || record.user.email || '未知用户'}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 变更详细内容 */}
|
||||
{renderVersionChanges(record.changes, record.changeType)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -24,32 +24,9 @@ export function PhotoGallery({ photoIds = [], onChange, readonly = false }: Phot
|
||||
|
||||
// 加载照片
|
||||
useEffect(() => {
|
||||
const loadPhotos = async () => {
|
||||
const loadedPhotos = await Promise.all(
|
||||
photoIds.map(async (id) => {
|
||||
try {
|
||||
const image = await db.images.get(id)
|
||||
if (image) {
|
||||
const url = URL.createObjectURL(image.blob)
|
||||
return { id, url }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载照片失败:', error)
|
||||
}
|
||||
return null
|
||||
})
|
||||
)
|
||||
setPhotos(loadedPhotos.filter(Boolean) as { id: string; url: string }[])
|
||||
}
|
||||
|
||||
if (photoIds.length > 0) {
|
||||
loadPhotos()
|
||||
}
|
||||
|
||||
// 清理 URL
|
||||
return () => {
|
||||
photos.forEach(photo => URL.revokeObjectURL(photo.url))
|
||||
}
|
||||
// photoIds 现在直接是 URL 数组
|
||||
const loadedPhotos = photoIds.map((url) => ({ id: url, url }))
|
||||
setPhotos(loadedPhotos)
|
||||
}, [photoIds])
|
||||
|
||||
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -72,16 +49,21 @@ export function PhotoGallery({ photoIds = [], onChange, readonly = false }: Phot
|
||||
|
||||
const compressedFile = await imageCompression(file, options)
|
||||
|
||||
// 保存到数据库
|
||||
const imageId = uuidv4()
|
||||
await db.images.add({
|
||||
id: imageId,
|
||||
blob: compressedFile,
|
||||
mimeType: compressedFile.type,
|
||||
createdAt: new Date().toISOString(),
|
||||
// 上传到服务器
|
||||
const formData = new FormData()
|
||||
formData.append('file', compressedFile)
|
||||
|
||||
const response = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
newPhotoIds.push(imageId)
|
||||
if (!response.ok) {
|
||||
throw new Error('上传失败')
|
||||
}
|
||||
|
||||
const { url } = await response.json()
|
||||
newPhotoIds.push(url)
|
||||
}
|
||||
|
||||
// 更新照片列表
|
||||
|
||||
Reference in New Issue
Block a user