0.6.1.0
This commit is contained in:
+215
-27
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user