0.3.0.6
This commit is contained in:
+366
-451
@@ -18,6 +18,7 @@ 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>
|
||||
@@ -25,9 +26,11 @@ interface MemberFormProps {
|
||||
onSubmit: (data: FamilyMember) => void
|
||||
onCancel: () => void
|
||||
isFounder?: boolean
|
||||
hideTabs?: boolean
|
||||
activeTab?: string
|
||||
}
|
||||
|
||||
export function MemberForm({ initialData, existingMembers = [], onSubmit, onCancel, isFounder = false }: MemberFormProps) {
|
||||
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))
|
||||
@@ -105,6 +108,7 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
||||
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,
|
||||
@@ -148,37 +152,33 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 || "")
|
||||
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]
|
||||
// 只有当前母亲ID为空时才自动设置
|
||||
if (!newData.motherId) {
|
||||
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]
|
||||
// 只有当前父亲ID为空时才自动设置
|
||||
if (!newData.fatherId) {
|
||||
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 })
|
||||
}
|
||||
@@ -187,153 +187,113 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
||||
|
||||
return newData
|
||||
})
|
||||
// Clear errors when user makes changes
|
||||
if (errors.length > 0) setErrors([])
|
||||
}
|
||||
|
||||
const handlePhotosChange = (photos: FamilyPhoto[]) => {
|
||||
handleChange("photos", photos)
|
||||
handleChange("photoIds", photos.map((photo) => photo.url))
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
photos,
|
||||
photoIds: photos.map(p => p.url),
|
||||
}))
|
||||
}
|
||||
|
||||
const handleSpouseAdd = (spouseId: string) => {
|
||||
if (!spouseId || spouseId === "none") return
|
||||
|
||||
setFormData((prev) => {
|
||||
const currentSpouses = prev.spouseIds || []
|
||||
// 避免重复添加
|
||||
if (currentSpouses.includes(spouseId)) return prev
|
||||
|
||||
return {
|
||||
...prev,
|
||||
spouseIds: [...currentSpouses, spouseId]
|
||||
}
|
||||
})
|
||||
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)
|
||||
spouseIds: (prev.spouseIds || []).filter((id) => id !== spouseId),
|
||||
}))
|
||||
}
|
||||
|
||||
const handleChildAdd = (childId: string) => {
|
||||
if (!childId || childId === "none") return
|
||||
|
||||
setFormData((prev) => {
|
||||
const currentChildren = prev.childrenIds || []
|
||||
// 避免重复添加
|
||||
if (currentChildren.includes(childId)) return prev
|
||||
|
||||
return {
|
||||
...prev,
|
||||
childrenIds: [...currentChildren, childId]
|
||||
}
|
||||
})
|
||||
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)
|
||||
childrenIds: (prev.childrenIds || []).filter((id) => id !== childId),
|
||||
}))
|
||||
}
|
||||
|
||||
const validate = (): boolean => {
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: string[] = []
|
||||
|
||||
// 1. Date Validation
|
||||
if (formData.birthDate && formData.deathDate) {
|
||||
if (new Date(formData.birthDate) > new Date(formData.deathDate)) {
|
||||
newErrors.push("出生日期不能晚于逝世日期")
|
||||
}
|
||||
|
||||
if (!formData.surname?.trim()) {
|
||||
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("出生日期不能早于母亲")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!formData.givenName?.trim()) {
|
||||
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} 世`)
|
||||
}
|
||||
|
||||
if (!formData.gender) {
|
||||
newErrors.push("请选择性别")
|
||||
}
|
||||
|
||||
// 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("检测到循环引用:不能将自己的后代设置为母亲")
|
||||
}
|
||||
|
||||
if (!formData.generation || formData.generation < 1) {
|
||||
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)
|
||||
|
||||
if (!validateForm()) {
|
||||
return
|
||||
}
|
||||
|
||||
// 确保所有必填字段都有值
|
||||
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,
|
||||
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,
|
||||
birthPlace: formData.birthPlace,
|
||||
deathPlace: formData.deathPlace,
|
||||
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)
|
||||
@@ -346,9 +306,9 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
||||
})
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-8 max-w-4xl mx-auto p-6">
|
||||
<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">
|
||||
<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) => (
|
||||
@@ -358,18 +318,28 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 基本信息和家庭关系 - 两列布局 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* 左列:基本信息 */}
|
||||
<Card className={founderCardClass}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<User className="h-5 w-5" />
|
||||
基本信息
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Tabs defaultValue="basic" 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="basic">基本信息</TabsTrigger>
|
||||
<TabsTrigger value="family">家庭关系</TabsTrigger>
|
||||
<TabsTrigger value="details">详细资料</TabsTrigger>
|
||||
<TabsTrigger value="media">照片故事</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<TabsContent value="basic" className="space-y-6 mt-6 w-full">
|
||||
<Card className={`w-full ${founderCardClass}`}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<User className="h-5 w-5" />
|
||||
基本信息
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex justify-center mb-4">
|
||||
<ImageUpload
|
||||
value={formData.avatarUrl}
|
||||
@@ -379,7 +349,7 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="surname">姓 (Surname)</Label>
|
||||
<Label htmlFor="surname">姓</Label>
|
||||
<Input
|
||||
id="surname"
|
||||
value={formData.surname}
|
||||
@@ -387,7 +357,7 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="givenName">名 (Given Name)</Label>
|
||||
<Label htmlFor="givenName">名</Label>
|
||||
<Input
|
||||
id="givenName"
|
||||
value={formData.givenName}
|
||||
@@ -419,11 +389,32 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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="occupation">职业</Label>
|
||||
<Input
|
||||
id="occupation"
|
||||
value={formData.occupation || ""}
|
||||
onChange={(e) => handleChange("occupation", e.target.value)}
|
||||
placeholder="请输入职业"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* 右列:家庭关系 */}
|
||||
<Card className={founderCardClass}>
|
||||
<TabsContent value="family" className="space-y-6 mt-6 w-full">
|
||||
<Card className={`w-full ${founderCardClass}`}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<Users className="h-5 w-5" />
|
||||
@@ -550,32 +541,18 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
||||
<SelectItem value="none">选择配偶</SelectItem>
|
||||
{potentialRelatives
|
||||
.filter((m) => {
|
||||
// 必须是同世系
|
||||
if (m.generation !== formData.generation) return false
|
||||
|
||||
// 通常配偶应该是异性
|
||||
if (m.gender === formData.gender) return false
|
||||
|
||||
// 排除已选的配偶
|
||||
if (formData.spouseIds?.includes(m.id)) return false
|
||||
|
||||
// 一夫多妻:女性可以选择已有配偶的男性,男性只能选择未婚女性
|
||||
if (formData.gender === 'MALE') {
|
||||
// 男性:排除已有配偶的女性
|
||||
if (m.spouseIds && m.spouseIds.length > 0) return false
|
||||
}
|
||||
// 女性:可以选择已有配偶的男性(一夫多妻)
|
||||
|
||||
// 排除直系亲属
|
||||
// 1. 排除父母
|
||||
if (m.id === formData.fatherId || m.id === formData.motherId) return false
|
||||
|
||||
// 2. 排除子女
|
||||
// 排除父母和子女
|
||||
if (formData.fatherId === m.id || formData.motherId === m.id) return false
|
||||
if (formData.childrenIds?.includes(m.id)) return false
|
||||
|
||||
// 3. 排除兄弟姐妹(同父或同母)
|
||||
if (formData.fatherId && m.fatherId === formData.fatherId) return false
|
||||
if (formData.motherId && m.motherId === formData.motherId) return false
|
||||
// 必须是同世系
|
||||
if (m.generation !== (formData.generation || 1)) return false
|
||||
|
||||
// 必须是异性
|
||||
if (m.gender === formData.gender) return false
|
||||
|
||||
return true
|
||||
})
|
||||
@@ -698,303 +675,241 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Traditional Names */}
|
||||
<Card className={founderCardClass}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<Scroll className="h-5 w-5" />
|
||||
传统称谓
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent 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="例如:伯虎"
|
||||
<TabsContent value="details" className="space-y-6 mt-6 w-full">
|
||||
<Card className={`w-full ${founderCardClass}`}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<Scroll className="h-5 w-5" />
|
||||
传统称谓
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent 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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className={`w-full ${founderCardClass}`}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<CalendarIcon className="h-5 w-5" />
|
||||
生平与地点
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<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>
|
||||
<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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Dates & Places */}
|
||||
<Card className={founderCardClass}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<CalendarIcon className="h-5 w-5" />
|
||||
生平与地点
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<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">籍贯</Label>
|
||||
<Input
|
||||
id="ancestralHome"
|
||||
value={formData.ancestralHome || ""}
|
||||
onChange={(e) => handleChange("ancestralHome", 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)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Contact Information */}
|
||||
<Card className={founderCardClass}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<User className="h-5 w-5" />
|
||||
联系方式
|
||||
</CardTitle>
|
||||
<CardDescription>在世成员的联系方式</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone">手机号</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
type="tel"
|
||||
value={formData.phone || ""}
|
||||
onChange={(e) => handleChange("phone", e.target.value)}
|
||||
placeholder="例如:13800138000"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="telephone">固定电话</Label>
|
||||
<Input
|
||||
id="telephone"
|
||||
type="tel"
|
||||
value={formData.telephone || ""}
|
||||
onChange={(e) => handleChange("telephone", e.target.value)}
|
||||
placeholder="例如:0592-1234567"
|
||||
/>
|
||||
</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="例如:example@email.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label htmlFor="address">现居地址</Label>
|
||||
<Input
|
||||
id="address"
|
||||
value={formData.address || ""}
|
||||
onChange={(e) => handleChange("address", e.target.value)}
|
||||
placeholder="例如:福建省厦门市思明区XX路XX号"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Biography */}
|
||||
<Card className={founderCardClass}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<Scroll className="h-5 w-5" />
|
||||
生平事迹
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Textarea
|
||||
className="min-h-[200px]"
|
||||
placeholder="记录生平事迹..."
|
||||
value={formData.bio || ""}
|
||||
onChange={(e) => handleChange("bio", e.target.value)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Tags */}
|
||||
<Card className={founderCardClass}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<Scroll className="h-5 w-5" />
|
||||
标签
|
||||
</CardTitle>
|
||||
<CardDescription>添加标签以便分类和搜索(按回车添加)</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="输入标签,按回车添加..."
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
const input = e.currentTarget
|
||||
const value = input.value.trim()
|
||||
if (value && !(formData.tags || []).includes(value)) {
|
||||
handleChange('tags', [...(formData.tags || []), value])
|
||||
input.value = ''
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{formData.tags && formData.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{formData.tags.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 bg-primary/10 text-primary text-sm rounded-md border border-primary/20"
|
||||
>
|
||||
{tag}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newTags = formData.tags?.filter((_, i) => i !== index) || []
|
||||
handleChange('tags', newTags)
|
||||
}}
|
||||
className="ml-1 hover:text-destructive"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
|
||||
{(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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Photo Gallery */}
|
||||
<Card className={founderCardClass}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<Images className="h-5 w-5" />
|
||||
照片墙
|
||||
</CardTitle>
|
||||
<CardDescription>上传多张照片记录珍贵时刻</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PhotoGallery
|
||||
photos={formData.photos}
|
||||
photoIds={formData.photoIds}
|
||||
onChange={handlePhotosChange}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="birthPlace">出生地</Label>
|
||||
<Input
|
||||
id="birthPlace"
|
||||
value={formData.birthPlace || ""}
|
||||
onChange={(e) => handleChange("birthPlace", e.target.value)}
|
||||
placeholder="请输入出生地"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Family Stories */}
|
||||
<Card className={founderCardClass}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<BookOpen className="h-5 w-5" />
|
||||
家族故事
|
||||
</CardTitle>
|
||||
<CardDescription>记录与此成员相关的家族故事</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<StoryManager
|
||||
stories={formData.stories || []}
|
||||
onChange={(stories) => handleChange("stories", stories)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<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>
|
||||
<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 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}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit" className="bg-primary text-primary-foreground hover:bg-primary/90">
|
||||
保存成员
|
||||
</Button>
|
||||
</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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="media" className="space-y-6 mt-6 w-full">
|
||||
<Card className={`w-full ${founderCardClass}`}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<Images className="h-5 w-5" />
|
||||
照片墙
|
||||
</CardTitle>
|
||||
<CardDescription>上传多张照片记录珍贵时刻</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PhotoGallery
|
||||
photos={formData.photos}
|
||||
photoIds={formData.photoIds}
|
||||
onChange={handlePhotosChange}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className={`w-full ${founderCardClass}`}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<BookOpen className="h-5 w-5" />
|
||||
家族故事
|
||||
</CardTitle>
|
||||
<CardDescription>记录与此成员相关的家族故事</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<StoryManager
|
||||
stories={formData.stories || []}
|
||||
onChange={(stories) => handleChange("stories", stories)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className={`w-full ${founderCardClass}`}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<Scroll className="h-5 w-5" />
|
||||
标签
|
||||
</CardTitle>
|
||||
<CardDescription>添加标签以便分类和搜索(按回车添加)</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="输入标签,按回车添加..."
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
const input = e.currentTarget
|
||||
const value = input.value.trim()
|
||||
if (value && !(formData.tags || []).includes(value)) {
|
||||
handleChange('tags', [...(formData.tags || []), value])
|
||||
input.value = ''
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{formData.tags && formData.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{formData.tags.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 bg-primary/10 text-primary text-sm rounded-md border border-primary/20"
|
||||
>
|
||||
{tag}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newTags = formData.tags?.filter((_, i) => i !== index) || []
|
||||
handleChange('tags', newTags)
|
||||
}}
|
||||
className="ml-1 hover:text-destructive"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,9 +4,18 @@ import { useEffect, useRef, useState, forwardRef, useImperativeHandle, memo } fr
|
||||
import * as d3 from "d3"
|
||||
// @ts-ignore - d3-org-chart没有TypeScript类型定义
|
||||
import { OrgChart } from "d3-org-chart"
|
||||
import { Download, FileText } from "lucide-react"
|
||||
import { Download, FileText, UserPlus, Users, Baby } from "lucide-react"
|
||||
import type { FamilyMember } from "@/types/family"
|
||||
import jsPDF from "jspdf"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useFamily } from "@/context/family-context"
|
||||
|
||||
// 全局类型声明
|
||||
declare global {
|
||||
interface Window {
|
||||
d3ChartContextMenu?: (event: MouseEvent, memberId: string) => void
|
||||
}
|
||||
}
|
||||
|
||||
interface D3OrgChartFlowProps {
|
||||
members: Record<string, FamilyMember>
|
||||
@@ -66,6 +75,88 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
|
||||
const chartInstanceRef = useRef<any>(null)
|
||||
const [isReady, setIsReady] = useState(false)
|
||||
const [avatarCache, setAvatarCache] = useState<Map<string, string>>(new Map())
|
||||
const [contextMenuNode, setContextMenuNode] = useState<FamilyMember | null>(null)
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 })
|
||||
const router = useRouter()
|
||||
const { currentTree } = useFamily()
|
||||
|
||||
// 构建新增成员的 URL
|
||||
const buildAddUrl = (type: 'father' | 'mother' | 'spouse' | 'child', member: FamilyMember) => {
|
||||
const params = new URLSearchParams()
|
||||
if (currentTree?.id) {
|
||||
params.set('treeId', currentTree.id)
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'father':
|
||||
params.set('childId', member.id)
|
||||
params.set('gender', 'MALE')
|
||||
params.set('generation', (member.generation - 1).toString())
|
||||
// 如果已有母亲,设为配偶
|
||||
if (member.motherId) {
|
||||
params.set('spouseId', member.motherId)
|
||||
}
|
||||
break
|
||||
case 'mother':
|
||||
params.set('childId', member.id)
|
||||
params.set('gender', 'FEMALE')
|
||||
params.set('generation', (member.generation - 1).toString())
|
||||
// 如果已有父亲,设为配偶
|
||||
if (member.fatherId) {
|
||||
params.set('spouseId', member.fatherId)
|
||||
}
|
||||
break
|
||||
case 'spouse':
|
||||
params.set('spouseId', member.id)
|
||||
params.set('generation', member.generation.toString())
|
||||
// 配偶性别与当前成员相反
|
||||
params.set('gender', member.gender === 'MALE' ? 'FEMALE' : 'MALE')
|
||||
|
||||
// 如果当前成员有子女,将他们也作为新配偶的子女
|
||||
if (member.childrenIds && member.childrenIds.length > 0) {
|
||||
// 传递所有子女ID
|
||||
member.childrenIds.forEach(id => {
|
||||
params.append('childrenIds', id)
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'child':
|
||||
// 根据当前成员性别设置父/母
|
||||
if (member.gender === 'MALE') {
|
||||
params.set('fatherId', member.id)
|
||||
// 如果父亲有配偶,设置配偶为母亲
|
||||
if (member.spouseIds && member.spouseIds.length > 0) {
|
||||
params.set('motherId', member.spouseIds[0])
|
||||
}
|
||||
} else {
|
||||
params.set('motherId', member.id)
|
||||
// 如果母亲有配偶,设置配偶为父亲
|
||||
if (member.spouseIds && member.spouseIds.length > 0) {
|
||||
params.set('fatherId', member.spouseIds[0])
|
||||
}
|
||||
}
|
||||
params.set('generation', (member.generation + 1).toString())
|
||||
break
|
||||
}
|
||||
|
||||
return `/members/new?${params.toString()}`
|
||||
}
|
||||
|
||||
const handleAddMember = (type: 'father' | 'mother' | 'spouse' | 'child', member: FamilyMember) => {
|
||||
const url = buildAddUrl(type, member)
|
||||
console.log('D3 handleAddMember called:', type, url)
|
||||
// 使用 setTimeout 延迟跳转,确保菜单关闭后再执行
|
||||
setTimeout(() => {
|
||||
console.log('Navigating to:', url)
|
||||
window.location.href = url
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 判断可以添加的关系
|
||||
const canAddFather = (member: FamilyMember) => !member.fatherId
|
||||
const canAddMother = (member: FamilyMember) => !member.motherId
|
||||
const canAddSpouse = (member: FamilyMember) => true // 配偶总是可以添加
|
||||
const canAddChild = (member: FamilyMember) => true // 子女总是可以添加
|
||||
|
||||
// 转换数据格式
|
||||
const convertToChartData = (): ChartNode[] => {
|
||||
@@ -205,19 +296,24 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
|
||||
const isSelected = selectedMembers.includes(node.id)
|
||||
|
||||
return `
|
||||
<div style="
|
||||
position: relative;
|
||||
width: 160px;
|
||||
height: 120px;
|
||||
box-sizing: border-box;
|
||||
background: ${node.deathYear ? '#fafafa' : 'white'};
|
||||
border: 2px solid ${color};
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
" onmouseover="this.style.boxShadow='0 4px 12px rgba(0,0,0,0.2)'" onmouseout="this.style.boxShadow='0 2px 8px rgba(0,0,0,0.1)'">
|
||||
<div
|
||||
data-member-id="${node.id}"
|
||||
style="
|
||||
position: relative;
|
||||
width: 160px;
|
||||
height: 120px;
|
||||
box-sizing: border-box;
|
||||
background: ${node.deathYear ? '#fafafa' : 'white'};
|
||||
border: 2px solid ${color};
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
"
|
||||
onmouseover="this.style.boxShadow='0 4px 12px rgba(0,0,0,0.2)'"
|
||||
onmouseout="this.style.boxShadow='0 2px 8px rgba(0,0,0,0.1)'"
|
||||
>
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 5px;">
|
||||
<div style="
|
||||
width: 40px;
|
||||
@@ -314,6 +410,28 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 添加右键菜单支持
|
||||
// 直接为节点添加右键事件监听
|
||||
setTimeout(() => {
|
||||
const nodes = chartRef.current?.querySelectorAll('[data-member-id]')
|
||||
console.log('Found nodes for context menu:', nodes?.length)
|
||||
nodes?.forEach((node) => {
|
||||
const memberId = (node as HTMLElement).getAttribute('data-member-id')
|
||||
if (memberId) {
|
||||
node.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault()
|
||||
const mouseEvent = e as MouseEvent
|
||||
const member = members[memberId]
|
||||
if (member && !relationMode) {
|
||||
console.log('Right click on member:', member.fullName, 'at', mouseEvent.clientX, mouseEvent.clientY)
|
||||
setContextMenuNode(member)
|
||||
setContextMenuPosition({ x: mouseEvent.clientX, y: mouseEvent.clientY })
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}, 200)
|
||||
}, 100)
|
||||
|
||||
chartInstanceRef.current = chart
|
||||
@@ -324,6 +442,10 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
|
||||
if (chartRef.current) {
|
||||
chartRef.current.innerHTML = ''
|
||||
}
|
||||
// 清理全局函数
|
||||
if (window.d3ChartContextMenu) {
|
||||
delete window.d3ChartContextMenu
|
||||
}
|
||||
}
|
||||
}, [members, rootId, onMemberClick, avatarCache])
|
||||
|
||||
@@ -417,6 +539,88 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
|
||||
className="w-full h-full"
|
||||
style={{ overflow: 'hidden' }}
|
||||
/>
|
||||
|
||||
{/* 自定义右键菜单 */}
|
||||
{contextMenuNode && (
|
||||
<div
|
||||
className="fixed z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md"
|
||||
style={{
|
||||
left: contextMenuPosition.x,
|
||||
top: contextMenuPosition.y,
|
||||
pointerEvents: 'auto'
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="px-2 py-1.5 text-sm text-muted-foreground">
|
||||
为 {contextMenuNode.fullName} 添加
|
||||
</div>
|
||||
<div className="h-px bg-border my-1" />
|
||||
|
||||
{canAddFather(contextMenuNode) && (
|
||||
<div
|
||||
className="relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => {
|
||||
handleAddMember('father', contextMenuNode)
|
||||
setContextMenuNode(null)
|
||||
}}
|
||||
>
|
||||
<UserPlus className="h-4 w-4 text-blue-500" />
|
||||
添加父亲
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canAddMother(contextMenuNode) && (
|
||||
<div
|
||||
className="relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => {
|
||||
handleAddMember('mother', contextMenuNode)
|
||||
setContextMenuNode(null)
|
||||
}}
|
||||
>
|
||||
<UserPlus className="h-4 w-4 text-pink-500" />
|
||||
添加母亲
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(canAddFather(contextMenuNode) || canAddMother(contextMenuNode)) && (canAddSpouse(contextMenuNode) || canAddChild(contextMenuNode)) && (
|
||||
<div className="h-px bg-border my-1" />
|
||||
)}
|
||||
|
||||
{canAddSpouse(contextMenuNode) && (
|
||||
<div
|
||||
className="relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => {
|
||||
handleAddMember('spouse', contextMenuNode)
|
||||
setContextMenuNode(null)
|
||||
}}
|
||||
>
|
||||
<Users className="h-4 w-4 text-red-500" />
|
||||
添加配偶
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canAddChild(contextMenuNode) && (
|
||||
<div
|
||||
className="relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => {
|
||||
handleAddMember('child', contextMenuNode)
|
||||
setContextMenuNode(null)
|
||||
}}
|
||||
>
|
||||
<Baby className="h-4 w-4 text-green-500" />
|
||||
添加子女
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 点击其他地方关闭菜单 */}
|
||||
{contextMenuNode && (
|
||||
<div
|
||||
className="fixed inset-0 z-40"
|
||||
onClick={() => setContextMenuNode(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user