This commit is contained in:
freedakgmail
2025-11-30 20:11:34 +08:00
parent 0326894b09
commit a2873bf3c2
2 changed files with 207 additions and 82 deletions
+111 -60
View File
@@ -269,25 +269,38 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
.container(chartRef.current) .container(chartRef.current)
.data(data) .data(data)
.nodeWidth(() => 160) .nodeWidth(() => 160)
.nodeHeight(() => 120) .nodeHeight(() => 165)
.childrenMargin(() => 50) .childrenMargin(() => 70)
.compactMarginBetween(() => 35) .compactMarginBetween(() => 40)
.compactMarginPair(() => 35) .compactMarginPair(() => 40)
.neighbourMargin(() => 35) .neighbourMargin(() => 40)
.siblingsMargin(() => 35) .siblingsMargin(() => 40)
.linkUpdate(function (d: any, i: any, arr: any) { .linkUpdate(function (d: any, i: any, arr: any) {
// 加深连线颜 // 中国风连线颜色 - 使用深褐
d3.select(arr[i]) d3.select(arr[i])
.attr('stroke', '#475569') .attr('stroke', '#78716c')
.attr('stroke-width', 2.5) .attr('stroke-width', 2)
}) })
.buttonContent(({ node }: any) => { .buttonContent(({ node }: any) => {
return `<div style="color:#716E7B;border-radius:5px;padding:4px;font-size:10px;margin:auto auto;background-color:white;border: 1px solid #E4E2E9"> <span style="font-size:9px">${node.children ? `<i class="fas fa-angle-up"></i>` : `<i class="fas fa-angle-down"></i>`}</span> ${node.data._directSubordinates || 0} </div>` return `<div style="color:#716E7B;border-radius:5px;padding:4px;font-size:10px;margin:auto auto;background-color:white;border: 1px solid #E4E2E9"> <span style="font-size:9px">${node.children ? `<i class="fas fa-angle-up"></i>` : `<i class="fas fa-angle-down"></i>`}</span> ${node.data._directSubordinates || 0} </div>`
}) })
.nodeContent((d: any) => { .nodeContent((d: any) => {
const node = d.data const node = d.data
const color = node.gender === '男' ? '#3b82f6' : '#ec4899' const isMale = node.gender === '男'
const bgColor = node.gender === '男' ? '#dbeafe' : '#fce7f3' const isDead = !!node.deathYear
// 庄严正式的中国风配色
const borderColor = isDead ? '#78716c' : isMale ? '#92400e' : '#9f1239'
const bgGradient = isDead
? 'linear-gradient(to bottom, #f5f5f4, #e7e5e4)'
: isMale
? 'linear-gradient(to bottom, #fffbeb, #fef3c7, #fffbeb)'
: 'linear-gradient(to bottom, #fff1f2, #fce7f3, #fff1f2)'
const avatarBorder = isDead ? '#78716c' : isMale ? '#b45309' : '#be185d'
const textColor = isDead ? '#57534e' : '#1c1917'
const subTextColor = isDead ? '#78716c' : '#57534e'
const badgeBg = isDead ? '#57534e' : isMale ? '#92400e' : '#9f1239'
const badgeText = '#fef3c7'
// 获取头像URL // 获取头像URL
const avatarUrl = node.avatarUrl ? avatarCache.get(node.avatarUrl) : null const avatarUrl = node.avatarUrl ? avatarCache.get(node.avatarUrl) : null
@@ -300,87 +313,125 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
data-member-id="${node.id}" data-member-id="${node.id}"
style=" style="
position: relative; position: relative;
width: 160px; width: 150px;
height: 120px; height: 155px;
box-sizing: border-box; box-sizing: border-box;
background: ${node.deathYear ? '#fafafa' : 'white'}; background: ${bgGradient};
border: 2px solid ${color}; border: 3px solid ${borderColor};
border-radius: 10px; padding: 14px 10px 10px;
padding: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.12);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
" "
onmouseover="this.style.boxShadow='0 4px 12px rgba(0,0,0,0.2)'" onmouseover="this.style.boxShadow='0 6px 20px rgba(0,0,0,0.2)'; this.style.transform='scale(1.02)'"
onmouseout="this.style.boxShadow='0 2px 8px rgba(0,0,0,0.1)'" onmouseout="this.style.boxShadow='0 4px 12px rgba(0,0,0,0.12)'; this.style.transform='scale(1)'"
> >
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 5px;"> <!-- 内边框装饰 -->
<div style="position: absolute; inset: 3px; border: 1px solid ${borderColor}40; pointer-events: none;"></div>
<!-- 四角装饰 - 庄严角花 -->
<div style="position: absolute; top: 0; left: 0; width: 12px; height: 12px; border-top: 3px solid ${borderColor}; border-left: 3px solid ${borderColor};"></div>
<div style="position: absolute; top: 0; right: 0; width: 12px; height: 12px; border-top: 3px solid ${borderColor}; border-right: 3px solid ${borderColor};"></div>
<div style="position: absolute; bottom: 0; left: 0; width: 12px; height: 12px; border-bottom: 3px solid ${borderColor}; border-left: 3px solid ${borderColor};"></div>
<div style="position: absolute; bottom: 0; right: 0; width: 12px; height: 12px; border-bottom: 3px solid ${borderColor}; border-right: 3px solid ${borderColor};"></div>
<!-- 世代标签 - 庄严印章风格 -->
<div style="
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
background: ${badgeBg};
color: ${badgeText};
font-size: 11px;
font-weight: 700;
padding: 3px 12px;
border: 2px solid ${borderColor};
font-family: serif;
letter-spacing: 2px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
">
${node.generation}
</div>
<!-- 选中标记 -->
${isSelected ? `
<div style="position: absolute; top: 6px; right: 6px;">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</div>
` : ''}
<!-- 头像和姓名区域 -->
<div style="display: flex; flex-direction: column; align-items: center; gap: 6px; margin-top: 6px;">
<!-- 头像 - 方形庄严风格 -->
<div style=" <div style="
width: 40px; width: 50px;
height: 40px; height: 50px;
border-radius: 50%; background: rgba(255,255,255,0.9);
background: ${bgColor};
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 16px;
color: ${color};
font-weight: bold;
flex-shrink: 0;
overflow: hidden; overflow: hidden;
border: 1px solid ${color}40; border: 2px solid ${avatarBorder};
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
"> ">
${avatarUrl ${avatarUrl
? `<img src="${avatarUrl}" alt="${node.name}" style="width: 100%; height: 100%; object-fit: cover;" />` ? `<img src="${avatarUrl}" alt="${node.name}" style="width: 100%; height: 100%; object-fit: cover;" />`
: node.gender === '女' : `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="${isDead ? '#78716c' : isMale ? '#b45309' : '#be185d'}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="5"/><path d="M20 21a8 8 0 1 0-16 0"/></svg>`
? `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width: 24px; height: 24px;"><circle cx="12" cy="8" r="5"/><path d="M20 21a8 8 0 1 0-16 0"/></svg>`
: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width: 24px; height: 24px;"><circle cx="12" cy="8" r="5"/><path d="M20 21a8 8 0 1 0-16 0"/></svg>`
} }
</div> </div>
<div style="flex: 1; min-width: 0;">
<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> <div style="
<span style="position: absolute; right: 2px; top: 4px; width: 8px; height: 8px; border-radius: 50%; background-color: ${node.deathYear ? '#9ca3af' : '#22c55e'};"></span> font-size: 15px;
${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>` : ''} font-weight: 700;
</div> color: ${textColor};
<div style="font-size: 9px; color: #6b7280; padding: 1px 4px; background: #f3f4f6; border-radius: 3px; display: inline-block;"> font-family: serif;
${node.generation} letter-spacing: 2px;
</div> display: flex;
align-items: center;
gap: 6px;
">
<span>${node.name}</span>
<span style="width: 7px; height: 7px; border-radius: 50%; background-color: ${isDead ? '#78716c' : '#16a34a'}; box-shadow: 0 0 3px ${isDead ? '#78716c' : '#16a34a'};"></span>
</div> </div>
</div> </div>
<div style="font-size: 10px; color: #6b7280; line-height: 1.3;"> <!-- 生卒年信息 -->
${node.birthYear ? `<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1px;"> <div style="text-align: center; margin-top: 6px;">
<span>${node.birthYear}${node.deathYear ? ` - ${node.deathYear}` : ''}</span> ${node.birthYear ? `
${node.ageText ? `<span style="font-size: 9px; color: #9ca3af;">${node.ageText}</span>` : ''} <div style="font-size: 11px; color: ${subTextColor}; font-family: monospace; letter-spacing: 1px;">
</div>` : ''} ${node.birthYear}${node.deathYear ? `${node.deathYear}` : ''}
${node.birthPlace ? `<div style="margin-bottom: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"> </div>
${node.birthPlace} ${node.ageText ? `<div style="font-size: 10px; color: ${subTextColor}; font-family: serif; margin-top: 2px;">${node.ageText}</div>` : ''}
</div>` : ''} ` : ''}
</div> </div>
<!-- 配偶信息 -->
${node.spouseName && node.spouseId ? ` ${node.spouseName && node.spouseId ? `
<div <div
class="spouse-area" class="spouse-area"
data-spouse-id="${node.spouseId}" data-spouse-id="${node.spouseId}"
style=" style="
margin-top: 4px; margin-top: 6px;
padding: 3px 5px; padding: 4px 8px;
background: #f9fafb; background: rgba(120, 113, 108, 0.08);
border: 1px solid #e5e7eb; border: 1px dashed ${borderColor};
border-radius: 4px; font-size: 10px;
font-size: 9px; color: ${subTextColor};
color: #4b5563;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
text-align: center;
font-family: serif;
letter-spacing: 1px;
" "
onmouseover="this.style.borderColor='#3b82f6'; this.style.background='#eff6ff'; this.style.color='#1e40af'" onmouseover="this.style.background='rgba(120, 113, 108, 0.15)'; this.style.borderStyle='solid'"
onmouseout="this.style.borderColor='#e5e7eb'; this.style.background='#f9fafb'; this.style.color='#4b5563'" onmouseout="this.style.background='rgba(120, 113, 108, 0.08)'; this.style.borderStyle='dashed'"
> >
配偶: ${node.spouseName} 配偶${node.spouseName}
</div> </div>
` : ''} ` : ''}
</div> </div>
+96 -22
View File
@@ -179,28 +179,92 @@ function PersonCard({
// 调试日志 // 调试日志
console.log('PersonCard render:', member.fullName, { canEdit, hasAddOptions, relationMode }) console.log('PersonCard render:', member.fullName, { canEdit, hasAddOptions, relationMode })
// 庄严正式的中国风配色
const isDead = !!member.deathDate
const isMale = member.gender === 'MALE'
const cardContent = ( const cardContent = (
<div <div
className={cn( className={cn(
"family-node-card relative flex flex-col items-center gap-2 p-3 rounded-lg transition-all cursor-pointer hover:scale-105 shadow-sm w-32", "family-node-card relative flex flex-col items-center p-4 transition-all cursor-pointer hover:scale-[1.02] w-[140px]",
// 庄严的边框样式 - 双层边框效果
"border-[3px] rounded-none",
"shadow-lg hover:shadow-xl",
// 根据状态设置样式
isSelected isSelected
? "border-4 border-blue-500 bg-blue-50 dark:bg-blue-900/20 ring-4 ring-blue-200 dark:ring-blue-800" ? "border-blue-600 bg-blue-50 dark:bg-blue-950/50 ring-2 ring-blue-400"
: isHighlighted : isHighlighted
? "border-4 border-yellow-400 bg-yellow-50 dark:bg-yellow-900/20 ring-4 ring-yellow-200 dark:ring-yellow-800 animate-pulse" ? "border-yellow-600 bg-yellow-50/90 dark:bg-yellow-950/50 ring-2 ring-yellow-400 animate-pulse"
: isRoot : isRoot
? "border-2 border-primary bg-primary/5" ? "border-red-800 bg-gradient-to-b from-red-50 via-amber-50/30 to-red-50 dark:from-red-950/40 dark:to-amber-950/20 shadow-xl"
: isSpouse : isSpouse
? "border-2 border-dashed border-border/60 bg-muted/30" ? "border-dashed border-stone-500 bg-stone-50/50 dark:bg-stone-900/30"
: "border-2 border-border bg-card", : isDead
relationMode && "hover:border-blue-400 hover:shadow-lg" ? "border-stone-500 bg-gradient-to-b from-stone-100 to-stone-50 dark:from-stone-900/50 dark:to-stone-800/30"
: isMale
? "border-amber-800 bg-gradient-to-b from-amber-50 via-orange-50/30 to-amber-50/80 dark:from-amber-950/40 dark:to-orange-950/20"
: "border-rose-800 bg-gradient-to-b from-rose-50 via-pink-50/30 to-rose-50/80 dark:from-rose-950/40 dark:to-pink-950/20",
relationMode && "hover:border-blue-600 hover:shadow-xl"
)} )}
onClick={() => onSelect?.(member)} onClick={() => onSelect?.(member)}
> >
<div className="absolute -top-3 bg-background border border-border px-2 py-0.5 rounded-full text-[10px] text-muted-foreground font-mono"> {/* 内边框装饰 */}
<div className={cn(
"absolute inset-[3px] border pointer-events-none",
isSelected ? "border-blue-400/50"
: isHighlighted ? "border-yellow-500/50"
: isRoot ? "border-red-600/40"
: isSpouse ? "border-stone-400/30"
: isDead ? "border-stone-400/40"
: isMale ? "border-amber-600/40"
: "border-rose-600/40"
)} />
{/* 四角装饰 - 更精致的角花 */}
<div className={cn(
"absolute top-0 left-0 w-4 h-4 border-t-[3px] border-l-[3px]",
isSelected ? "border-blue-600" : isHighlighted ? "border-yellow-600" : isRoot ? "border-red-800" : isSpouse ? "border-stone-500" : isDead ? "border-stone-500" : isMale ? "border-amber-800" : "border-rose-800"
)} />
<div className={cn(
"absolute top-0 right-0 w-4 h-4 border-t-[3px] border-r-[3px]",
isSelected ? "border-blue-600" : isHighlighted ? "border-yellow-600" : isRoot ? "border-red-800" : isSpouse ? "border-stone-500" : isDead ? "border-stone-500" : isMale ? "border-amber-800" : "border-rose-800"
)} />
<div className={cn(
"absolute bottom-0 left-0 w-4 h-4 border-b-[3px] border-l-[3px]",
isSelected ? "border-blue-600" : isHighlighted ? "border-yellow-600" : isRoot ? "border-red-800" : isSpouse ? "border-stone-500" : isDead ? "border-stone-500" : isMale ? "border-amber-800" : "border-rose-800"
)} />
<div className={cn(
"absolute bottom-0 right-0 w-4 h-4 border-b-[3px] border-r-[3px]",
isSelected ? "border-blue-600" : isHighlighted ? "border-yellow-600" : isRoot ? "border-red-800" : isSpouse ? "border-stone-500" : isDead ? "border-stone-500" : isMale ? "border-amber-800" : "border-rose-800"
)} />
{/* 世代标签 - 庄严印章风格 */}
<div className={cn(
"absolute -top-3 px-3 py-1 text-[11px] font-serif font-bold tracking-wider",
"border-2 shadow-md",
isRoot
? "bg-red-800 text-amber-100 border-red-900"
: isDead
? "bg-stone-600 text-stone-100 border-stone-700"
: isMale
? "bg-amber-800 text-amber-100 border-amber-900"
: "bg-rose-800 text-rose-100 border-rose-900"
)}>
{member.generation} {member.generation}
</div> </div>
<div className="h-12 w-12 rounded-full bg-muted flex items-center justify-center text-muted-foreground overflow-hidden border border-border/50"> {/* 头像 - 方形带庄严边框 */}
<div className={cn(
"h-14 w-14 flex items-center justify-center overflow-hidden mt-2",
"border-2 shadow-inner bg-white/80 dark:bg-black/20",
isRoot
? "border-red-700"
: isDead
? "border-stone-400"
: isMale
? "border-amber-700"
: "border-rose-700"
)}>
{avatarBlobUrl ? ( {avatarBlobUrl ? (
<img <img
src={avatarBlobUrl} src={avatarBlobUrl}
@@ -209,38 +273,48 @@ function PersonCard({
/> />
) : ( ) : (
member.gender === 'FEMALE' ? ( member.gender === 'FEMALE' ? (
<CircleUserRound className="h-8 w-8 text-pink-400" /> <CircleUserRound className={cn("h-9 w-9", isDead ? "text-stone-400" : "text-rose-700")} />
) : ( ) : (
<CircleUser className="h-8 w-8 text-blue-400" /> <CircleUser className={cn("h-9 w-9", isDead ? "text-stone-400" : "text-amber-700")} />
) )
)} )}
</div> </div>
<div className="text-center w-full"> {/* 姓名和信息 */}
<div className="font-serif font-bold text-foreground leading-tight flex items-center justify-center gap-1"> <div className="text-center w-full mt-2 space-y-0.5">
<div className={cn(
"font-serif font-bold leading-tight flex items-center justify-center gap-1 text-base tracking-wide",
isDead ? "text-stone-500" : "text-stone-900 dark:text-stone-100"
)}>
<MemberNameWithStatus <MemberNameWithStatus
name={`${member.surname}${member.givenName}`} name={`${member.surname}${member.givenName}`}
isDead={!!member.deathDate} isDead={isDead}
/> />
</div> </div>
{member.courtesyName && ( {member.courtesyName && (
<div className="text-[10px] text-muted-foreground mt-0.5"> {member.courtesyName}</div> <div className={cn(
"text-[11px] font-serif tracking-wide",
isDead ? "text-stone-400" : "text-stone-600 dark:text-stone-400"
)}>
{member.courtesyName}
</div>
)} )}
{lifeSpan && ( {lifeSpan && (
<div className="text-[9px] text-muted-foreground/70 mt-1 font-mono"> <div className={cn(
"text-[10px] font-mono tracking-wider mt-1",
isDead ? "text-stone-400" : "text-stone-500"
)}>
{lifeSpan} {lifeSpan}
</div> </div>
)} )}
{ageText && ( {ageText && (
<div className="text-[9px] text-muted-foreground/60 font-mono"> <div className={cn(
"text-[10px] font-serif",
isDead ? "text-stone-400" : "text-stone-500"
)}>
{ageText} {ageText}
</div> </div>
)} )}
{member.phone && (
<div className="text-[9px] text-muted-foreground/60 font-mono truncate">
📱 {member.phone}
</div>
)}
</div> </div>
</div> </div>
) )