This commit is contained in:
freedakgmail
2025-12-10 11:41:28 +08:00
parent 77df6a0e74
commit 47be495fcf
130 changed files with 429 additions and 210 deletions
+215 -27
View File
@@ -493,35 +493,223 @@ export function FamilyNode({
)
}
// 有配偶时,并排显示所有配偶
// 有配偶时,合并到一个卡片中显示(类似D3布局)
const isMale = member.gender === 'MALE'
const isDead = !!member.deathDate
const isFounder = member.isFounder === true
const avatarUrl = member.avatarUrl
// 计算生卒年份
const birthYear = formatYear(member.birthDate)
const deathYear = formatYear(member.deathDate)
const lifeSpan = birthYear ? `${birthYear}${deathYear ? `${deathYear}` : ' —'}` : ''
const age = calculateAge(member.birthDate, member.deathDate)
const ageText = age !== null ? (isDead ? `享年${age}` : `${age}`) : ''
return (
<div className="flex items-center gap-2">
<PersonCard
member={member}
isRoot={isRoot}
onSelect={onSelect}
isHighlighted={isHighlighted}
relationMode={relationMode}
isSelected={isSelected}
canEdit={canEdit}
onAddRelation={onAddRelation}
hasChildren={hasChildren}
isCollapsed={isCollapsed}
onToggleCollapse={onToggleCollapse}
/>
{spouseList.map((spouseMember, index) => (
<div key={spouseMember.id} className="flex items-center gap-2">
<Heart className="h-4 w-4 text-red-400 flex-shrink-0" />
<PersonCard
member={spouseMember}
isSpouse={true}
onSelect={onSelect}
relationMode={relationMode}
isSelected={selectedMembers.includes(spouseMember.id)}
canEdit={canEdit}
/>
<div
className={cn(
"relative rounded-xl overflow-hidden transition-all duration-300",
"shadow-[0_4px_20px_rgba(0,0,0,0.08)] hover:shadow-[0_12px_40px_rgba(0,0,0,0.15)]",
"hover:scale-[1.02] hover:-translate-y-1",
isHighlighted
? "bg-gradient-to-br from-yellow-50 via-amber-50 to-yellow-100 ring-2 ring-yellow-400"
: isFounder || isRoot
? "bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50"
: "bg-gradient-to-br from-slate-50 via-gray-50 to-slate-100"
)}
>
{/* 顶部装饰条 */}
<div className={cn(
"h-1.5",
isHighlighted ? "bg-gradient-to-r from-yellow-400 via-amber-500 to-yellow-400"
: isFounder || isRoot ? "bg-gradient-to-r from-amber-400 via-orange-500 to-amber-400"
: "bg-gradient-to-r from-slate-400 via-gray-500 to-slate-400"
)} />
{/* 世代标签 - 居中显示 */}
{member.generation && (
<div className="flex justify-center -mt-0.5 mb-1">
<span className={cn(
"text-[10px] font-medium px-2 py-0.5 rounded-full text-white",
isFounder || isRoot
? "bg-gradient-to-r from-amber-500 to-orange-500"
: "bg-gradient-to-r from-slate-500 to-gray-600"
)}>
{member.generation}
</span>
</div>
))}
)}
{/* 内容区域 */}
<div className="p-3 pt-1 flex gap-3">
{/* 主成员 */}
<div
className={cn(
"flex flex-col items-center cursor-pointer flex-1 min-w-[90px] relative",
relationMode && "hover:ring-2 hover:ring-primary/50 rounded-lg p-1"
)}
data-member-id={member.id}
onClick={() => onSelect?.(member)}
>
{/* 选中对钩标记 */}
{isSelected && (
<div className="absolute -top-1 -right-1 z-20 w-5 h-5 bg-primary rounded-full flex items-center justify-center shadow-md">
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
</div>
)}
{/* 头像 */}
<div className="relative mb-2">
<div className={cn(
"absolute inset-0 rounded-full blur-md opacity-60",
isFounder ? "bg-amber-300" : isDead ? "bg-stone-300" : isMale ? "bg-sky-300" : "bg-rose-300"
)} style={{ transform: 'scale(1.1)' }} />
<div className={cn(
"relative w-12 h-12 rounded-full border-2 overflow-hidden bg-white flex items-center justify-center",
isFounder ? "border-amber-500" : isDead ? "border-stone-400" : isMale ? "border-sky-400" : "border-rose-400",
isSelected && "ring-2 ring-primary ring-offset-2"
)}>
{avatarUrl ? (
<img src={avatarUrl} alt={member.fullName} className="w-full h-full object-cover" />
) : isMale ? (
<CircleUser className={cn("w-7 h-7", isDead ? "text-stone-400" : "text-sky-500")} />
) : (
<CircleUserRound className={cn("w-7 h-7", isDead ? "text-stone-400" : "text-rose-500")} />
)}
</div>
{/* 状态指示器 */}
<div className={cn(
"absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 rounded-full border-2 border-white",
isDead ? "bg-stone-400" : "bg-emerald-500"
)} />
</div>
{/* 姓名 */}
<div className="text-center">
<div className="font-semibold text-[13px] text-black leading-tight">
<MemberNameWithStatus name={member.fullName} isDead={isDead} />
</div>
{lifeSpan && (
<div className="text-[9px] text-black mt-0.5 bg-gray-200/80 px-1.5 py-0.5 rounded-full">
{lifeSpan}
</div>
)}
{ageText && (
<div className="text-[9px] text-black mt-0.5">
{ageText}
</div>
)}
</div>
</div>
{/* 配偶们 */}
{spouseList.map((sp, idx) => {
const spIsMale = sp.gender === 'MALE'
const spIsDead = !!sp.deathDate
const spAvatarUrl = sp.avatarUrl
const spBirthYear = formatYear(sp.birthDate)
const spDeathYear = formatYear(sp.deathDate)
const spLifeSpan = spBirthYear ? `${spBirthYear}${spDeathYear ? `-${spDeathYear}` : '-'}` : ''
const spAge = calculateAge(sp.birthDate, sp.deathDate)
const spAgeText = spAge !== null ? (spIsDead ? `享年${spAge}` : `${spAge}`) : ''
const spIsSelected = selectedMembers.includes(sp.id)
return (
<div key={sp.id} className="flex items-center gap-2">
{/* 连接线 */}
{idx === 0 && (
<div className="flex flex-col items-center">
<Heart className="h-3 w-3 text-rose-400" />
</div>
)}
{/* 配偶卡片 */}
<div
className={cn(
"flex flex-col items-center cursor-pointer min-w-[80px] relative",
relationMode && "hover:ring-2 hover:ring-primary/50 rounded-lg p-1"
)}
data-member-id={sp.id}
onClick={() => onSelect?.(sp)}
>
{/* 选中对钩标记 */}
{spIsSelected && (
<div className="absolute -top-1 -right-1 z-20 w-5 h-5 bg-primary rounded-full flex items-center justify-center shadow-md">
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
</div>
)}
{/* 头像 */}
<div className="relative mb-2">
<div className={cn(
"absolute inset-0 rounded-full blur-md opacity-50",
spIsDead ? "bg-stone-300" : spIsMale ? "bg-sky-300" : "bg-rose-300"
)} style={{ transform: 'scale(1.1)' }} />
<div className={cn(
"relative w-10 h-10 rounded-full border-2 overflow-hidden bg-white flex items-center justify-center",
spIsDead ? "border-stone-400" : spIsMale ? "border-sky-400" : "border-rose-400",
spIsSelected && "ring-2 ring-primary ring-offset-2"
)}>
{spAvatarUrl ? (
<img src={spAvatarUrl} alt={sp.fullName} className="w-full h-full object-cover" />
) : spIsMale ? (
<CircleUser className={cn("w-6 h-6", spIsDead ? "text-stone-400" : "text-sky-500")} />
) : (
<CircleUserRound className={cn("w-6 h-6", spIsDead ? "text-stone-400" : "text-rose-500")} />
)}
</div>
{/* 状态指示器 */}
<div className={cn(
"absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-white",
spIsDead ? "bg-stone-400" : "bg-emerald-500"
)} />
</div>
{/* 姓名 */}
<div className="text-center">
<div className="font-semibold text-[11px] text-black leading-tight">
<MemberNameWithStatus name={sp.fullName} isDead={spIsDead} />
</div>
{spLifeSpan && (
<div className="text-[8px] text-black mt-0.5 bg-gray-200/80 px-1 py-0.5 rounded-full">
{spLifeSpan}
</div>
)}
{spAgeText && (
<div className="text-[8px] text-black mt-0.5">
{spAgeText}
</div>
)}
</div>
</div>
</div>
)
})}
</div>
{/* 折叠/展开按钮 */}
{hasChildren && onToggleCollapse && (
<button
onClick={(e) => {
e.stopPropagation()
onToggleCollapse()
}}
className={cn(
"absolute -bottom-3 left-1/2 -translate-x-1/2 z-20",
"w-6 h-6 rounded-full flex items-center justify-center",
"bg-white border border-gray-200 shadow-md",
"hover:bg-gray-50 hover:scale-110 transition-all",
"text-gray-600 text-xs font-medium"
)}
>
{isCollapsed ? '▼' : '▲'}
</button>
)}
</div>
)
}