"use client" import { useEffect, useRef, useState, forwardRef, useImperativeHandle, memo } from "react" import * as d3 from "d3" // @ts-ignore - d3-org-chart没有TypeScript类型定义 import { OrgChart } from "d3-org-chart" import { Download, FileText } from "lucide-react" import type { FamilyMember } from "@/types/family" import jsPDF from "jspdf" interface D3OrgChartFlowProps { members: Record rootId: string onMemberClick?: (memberId: string) => void onExport?: () => void relationMode?: boolean selectedMembers?: string[] } export interface D3OrgChartRef { exportPNG: () => void exportPDF: () => void expandAll: () => void collapseAll: () => void fitView: () => void } interface ChartNode { id: string parentId: string | null name: string generation: string birthYear: string deathYear: string birthPlace: string gender: string avatarUrl: string | null | undefined spouseName: string spouseId: string | null hasChildren: boolean ageText: string } // 计算年龄 function calculateAge(birthDate?: string, deathDate?: string): number | null { if (!birthDate) return null const birth = new Date(birthDate) const end = deathDate ? new Date(deathDate) : new Date() let age = end.getFullYear() - birth.getFullYear() const monthDiff = end.getMonth() - birth.getMonth() if (monthDiff < 0 || (monthDiff === 0 && end.getDate() < birth.getDate())) { age-- } return age } const D3OrgChartFlowComponent = forwardRef( ({ members, rootId, onMemberClick, onExport, relationMode = false, selectedMembers = [] }, ref) => { console.log('D3OrgChartFlow render, selectedMembers:', selectedMembers, 'relationMode:', relationMode) const chartRef = useRef(null) const chartInstanceRef = useRef(null) const [isReady, setIsReady] = useState(false) const [avatarCache, setAvatarCache] = useState>(new Map()) // 转换数据格式 const convertToChartData = (): ChartNode[] => { const chartNodes: ChartNode[] = [] // 首先找到根节点(没有父节点的节点) const rootMember = members[rootId] if (!rootMember) return [] // 使用BFS遍历,确保只包含从rootId开始的树 const visited = new Set() const queue: string[] = [rootId] while (queue.length > 0) { const currentId = queue.shift()! if (visited.has(currentId)) continue visited.add(currentId) const member = members[currentId] if (!member) continue // 获取所有配偶信息,确保配偶存在于members中 const spouses = member.spouseIds ?.map(id => members[id]) .filter(Boolean) || [] // 确定父节点ID - 只有根节点的parentId为null let parentId: string | null = null if (currentId !== rootId) { // 根据姓氏判断主线,优先选择同姓的父母 const father = member.fatherId ? members[member.fatherId] : null const mother = member.motherId ? members[member.motherId] : null // 判断父母中谁是家族主线(同姓) const isFatherMainLine = father && member.surname === father.surname const isMotherMainLine = mother && member.surname === mother.surname if (isFatherMainLine) { parentId = member.fatherId || null } else if (isMotherMainLine) { parentId = member.motherId || null } else if (father) { // 如果都不同姓,优先选择父亲 parentId = member.fatherId || null } else if (mother) { parentId = member.motherId || null } } // 计算年龄文本 const age = calculateAge(member.birthDate, member.deathDate) const ageText = age !== null ? member.deathDate ? `享年${age}岁` : `${age}岁` : '' chartNodes.push({ id: member.id, parentId: parentId, name: member.fullName, generation: `第${member.generation}世`, birthYear: member.birthDate ? new Date(member.birthDate).getFullYear().toString() : '', deathYear: member.deathDate ? new Date(member.deathDate).getFullYear().toString() : '', birthPlace: member.birthPlace || '', gender: member.gender === 'MALE' ? '男' : '女', 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), ageText: ageText }) // 添加子节点到队列 Object.values(members).forEach(m => { if ((m.fatherId === currentId || m.motherId === currentId) && !visited.has(m.id)) { queue.push(m.id) } }) } return chartNodes } // 构建头像缓存(URL 直接使用) useEffect(() => { const cache = new Map() for (const member of Object.values(members)) { if (member.avatarUrl) { cache.set(member.avatarUrl, member.avatarUrl) } } setAvatarCache(cache) }, [members]) useEffect(() => { if (!chartRef.current || !rootId) return // 清理旧的图表 if (chartRef.current) { chartRef.current.innerHTML = '' } const data = convertToChartData() // 初始化图表 const chart = new OrgChart() .container(chartRef.current) .data(data) .nodeWidth(() => 160) .nodeHeight(() => 120) .childrenMargin(() => 50) .compactMarginBetween(() => 35) .compactMarginPair(() => 35) .neighbourMargin(() => 35) .siblingsMargin(() => 35) .linkUpdate(function (d: any, i: any, arr: any) { // 加深连线颜色 d3.select(arr[i]) .attr('stroke', '#475569') .attr('stroke-width', 2.5) }) .buttonContent(({ node }: any) => { return `
${node.children ? `` : ``} ${node.data._directSubordinates || 0}
` }) .nodeContent((d: any) => { const node = d.data const color = node.gender === '男' ? '#3b82f6' : '#ec4899' const bgColor = node.gender === '男' ? '#dbeafe' : '#fce7f3' // 获取头像URL const avatarUrl = node.avatarUrl ? avatarCache.get(node.avatarUrl) : null // 检查是否被选中 const isSelected = selectedMembers.includes(node.id) return `
${avatarUrl ? `${node.name}` : node.gender === '女' ? `` : `` }
${node.name} ${isSelected ? `` : ''}
${node.generation}
${node.birthYear ? `
${node.birthYear}${node.deathYear ? ` - ${node.deathYear}` : ''} ${node.ageText ? `${node.ageText}` : ''}
` : ''} ${node.birthPlace ? `
${node.birthPlace}
` : ''}
${node.spouseName && node.spouseId ? `
配偶: ${node.spouseName}
` : ''}
` }) .onNodeClick((d: any) => { if (onMemberClick && d) { // d3-org-chart传递的是节点数据对象,需要提取id const nodeId = typeof d === 'string' ? d : (d.data?.id || d.id) if (nodeId) { onMemberClick(nodeId) } } }) .render() .expandAll() // 初始化时展开所有节点 // 添加配偶区域点击事件监听 setTimeout(() => { const spouseAreas = chartRef.current?.querySelectorAll('.spouse-area') spouseAreas?.forEach((area) => { area.addEventListener('click', (e) => { e.stopPropagation() // 阻止事件冒泡到节点 const spouseId = (area as HTMLElement).getAttribute('data-spouse-id') if (spouseId && onMemberClick) { onMemberClick(spouseId) } }) }) }, 100) chartInstanceRef.current = chart setIsReady(true) return () => { // 清理 if (chartRef.current) { chartRef.current.innerHTML = '' } } }, [members, rootId, onMemberClick, avatarCache]) // 监听selectedMembers变化,动态显示/隐藏勾选图标 // 逻辑已移至tree/page.tsx以避免组件重新渲染 // 导出PNG const handleExportPNG = () => { if (!chartInstanceRef.current) return chartInstanceRef.current.exportImg({ save: true, full: true, onLoad: () => { onExport?.() } }) } // 导出PDF const handleExportPDF = () => { if (!chartInstanceRef.current) return chartInstanceRef.current.exportImg({ save: false, full: true, onLoad: (base64: string) => { const pdf = new jsPDF({ orientation: 'landscape', unit: 'px', format: 'a4' }) const img = new Image() img.src = base64 img.onload = function () { const imgWidth = img.width const imgHeight = img.height const pdfWidth = pdf.internal.pageSize.getWidth() const pdfHeight = pdf.internal.pageSize.getHeight() // 计算缩放比例以适应页面 const ratio = Math.min(pdfWidth / imgWidth, pdfHeight / imgHeight) const scaledWidth = imgWidth * ratio const scaledHeight = imgHeight * ratio // 居中放置 const x = (pdfWidth - scaledWidth) / 2 const y = (pdfHeight - scaledHeight) / 2 pdf.addImage(img, 'PNG', x, y, scaledWidth, scaledHeight) pdf.save(`家谱图-${new Date().toISOString().split('T')[0]}.pdf`) onExport?.() } } }) } // 展开所有节点 const handleExpandAll = () => { if (!chartInstanceRef.current) return chartInstanceRef.current.expandAll() } // 折叠所有节点 const handleCollapseAll = () => { if (!chartInstanceRef.current) return chartInstanceRef.current.collapseAll() } // 适应视图 const handleFitView = () => { if (!chartInstanceRef.current) return chartInstanceRef.current.fit() } // 暴露方法给父组件 useImperativeHandle(ref, () => ({ exportPNG: handleExportPNG, exportPDF: handleExportPDF, expandAll: handleExpandAll, collapseAll: handleCollapseAll, fitView: handleFitView, })) return (
{/* 图表容器 */}
) }) D3OrgChartFlowComponent.displayName = 'D3OrgChartFlow' // 使用memo优化,避免不必要的重新渲染 export const D3OrgChartFlow = memo(D3OrgChartFlowComponent, (prevProps, nextProps) => { return prevProps.rootId === nextProps.rootId && prevProps.members === nextProps.members })