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
+12 -41
View File
@@ -35,7 +35,7 @@ interface ChartNode {
deathYear: string
birthPlace: string
gender: string
avatarImageId: string | null | undefined
avatarUrl: string | null | undefined
spouseName: string
spouseId: string | null
hasChildren: boolean
@@ -133,7 +133,7 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
deathYear: member.deathDate ? new Date(member.deathDate).getFullYear().toString() : '',
birthPlace: member.birthPlace || '',
gender: member.gender === 'MALE' ? '男' : '女',
avatarImageId: member.avatarImageId,
avatarUrl: member.avatarUrl,
spouseName: spouses.length > 0 ? spouses.map(s => s.fullName).join(', ') : '',
spouseId: spouses.length > 0 ? spouses[0].id : null,
hasChildren: Object.values(members).some(m => m.fatherId === member.id || m.motherId === member.id),
@@ -151,34 +151,17 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
return chartNodes
}
// 加载所有成员的头像
// 构建头像缓存(URL 直接使用)
useEffect(() => {
const loadAvatars = async () => {
const cache = new Map<string, string>()
for (const member of Object.values(members)) {
if (member.avatarImageId) {
try {
const image = await db.images.get(member.avatarImageId)
if (image) {
const url = URL.createObjectURL(image.blob)
cache.set(member.avatarImageId, url)
}
} catch (error) {
console.error(`Failed to load avatar for ${member.fullName}:`, error)
}
}
const cache = new Map<string, string>()
for (const member of Object.values(members)) {
if (member.avatarUrl) {
cache.set(member.avatarUrl, member.avatarUrl)
}
setAvatarCache(cache)
}
loadAvatars()
// 清理函数
return () => {
avatarCache.forEach(url => URL.revokeObjectURL(url))
}
setAvatarCache(cache)
}, [members])
useEffect(() => {
@@ -217,7 +200,7 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
const bgColor = node.gender === '男' ? '#dbeafe' : '#fce7f3'
// 获取头像URL
const avatarUrl = node.avatarImageId ? avatarCache.get(node.avatarImageId) : null
const avatarUrl = node.avatarUrl ? avatarCache.get(node.avatarUrl) : null
// 检查是否被选中
const isSelected = selectedMembers.includes(node.id)
@@ -260,8 +243,9 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
}
</div>
<div style="flex: 1; min-width: 0;">
<div style="font-size: 13px; font-weight: 600; color: #1f2937; margin-bottom: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: flex; align-items: center; gap: 4px;">
<div style="font-size: 13px; font-weight: 600; color: ${node.deathYear ? '#9ca3af' : '#1f2937'}; margin-bottom: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: flex; align-items: center; gap: 4px; position: relative; padding-right: 12px;">
<span>${node.name}</span>
<span style="position: absolute; right: 2px; top: 4px; width: 8px; height: 8px; border-radius: 50%; background-color: ${node.deathYear ? '#9ca3af' : '#22c55e'};"></span>
${isSelected ? `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>` : ''}
</div>
<div style="font-size: 9px; color: #6b7280; padding: 1px 4px; background: #f3f4f6; border-radius: 3px; display: inline-block;">
@@ -304,19 +288,6 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
配偶: ${node.spouseName}
</div>
` : ''}
${node.deathYear ? `
<div style="
position: absolute;
bottom: 4px;
left: 4px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #737373;
border: 2px solid white;
"></div>
` : ''}
</div>
`
})
+8 -6
View File
@@ -4,6 +4,7 @@ import type { FamilyMember } from "@/types/family"
import { cn } from "@/lib/utils"
import { useAvatarCache } from "@/hooks/use-avatar-cache"
import { CircleUser, CircleUserRound, Heart } from "lucide-react"
import { MemberNameWithStatus } from "@/components/member-name-with-status"
interface FamilyNodeProps {
member: FamilyMember
@@ -42,7 +43,7 @@ function calculateAge(birthDate?: string, deathDate?: string): number | null {
// 单个人员卡片组件
function PersonCard({ member, isRoot, isSpouse, onSelect, isHighlighted, relationMode, isSelected }: { member: FamilyMember; isRoot?: boolean; isSpouse?: boolean; onSelect?: (member: FamilyMember) => void; isHighlighted?: boolean; relationMode?: boolean; isSelected?: boolean }) {
const { avatarUrl: avatarBlobUrl } = useAvatarCache(member?.avatarImageId)
const { avatarUrl: avatarBlobUrl } = useAvatarCache(member?.avatarUrl)
const birthYear = formatYear(member.birthDate)
const deathYear = formatYear(member.deathDate)
@@ -98,7 +99,12 @@ function PersonCard({ member, isRoot, isSpouse, onSelect, isHighlighted, relatio
</div>
<div className="text-center w-full">
<div className="font-serif font-bold text-foreground leading-tight">{member.surname} {member.givenName}</div>
<div className="font-serif font-bold text-foreground leading-tight flex items-center justify-center gap-1">
<MemberNameWithStatus
name={`${member.surname}${member.givenName}`}
isDead={!!member.deathDate}
/>
</div>
{member.courtesyName && (
<div className="text-[10px] text-muted-foreground mt-0.5"> {member.courtesyName}</div>
)}
@@ -118,10 +124,6 @@ function PersonCard({ member, isRoot, isSpouse, onSelect, isHighlighted, relatio
</div>
)}
</div>
{member.deathDate && (
<div className="absolute -top-1 -right-1 h-2 w-2 rounded-full bg-neutral-400 ring-2 ring-background" />
)}
</div>
)
}
+2 -1
View File
@@ -43,7 +43,8 @@ export function TreeLayout({
if (relationMode && onMemberClick) {
onMemberClick(member.id)
} else {
router.push(`/members/${member.id}`)
const params = new URLSearchParams(window.location.search)
router.push(`/members/${member.id}?${params.toString()}`)
}
}, [relationMode, onMemberClick, router])