"use client" import type React from "react" import dynamic from "next/dynamic" import { SiteHeader } from "@/components/site-header" import { TreeLayout } from "@/components/tree/tree-layout" import { useFamily } from "@/context/family-context" import { useRelationship } from "@/hooks/use-relationship" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Badge } from "@/components/ui/badge" import { ZoomIn, ZoomOut, Move, Download, LayoutGrid, ChevronDown, Users, Network, Loader2, Maximize2, FileText } from "lucide-react" import { useState, useRef, useEffect, useCallback } from "react" import { useRouter, useSearchParams } from "next/navigation" import { MemberNameWithStatus } from "@/components/member-name-with-status" import { RelationshipPathDisplay } from "@/components/relationship-path-display" import { useSession } from "next-auth/react" import { EmptyStateBadge } from "@/components/empty-state-badge" import { exportFamilyHTML } from "@/lib/export-pdf" import { useDialog } from "@/components/ui/alert-dialog-custom" import { Minimap } from "@/components/tree/minimap" // 动态导入 D3 图表组件(减少初始加载体积) const D3OrgChartFlow = dynamic( () => import("@/components/tree/d3-org-chart-flow").then(mod => ({ default: mod.D3OrgChartFlow })), { ssr: false, loading: () => (
加载图表组件...
) } ) // 导入类型 import type { D3OrgChartRef } from "@/components/tree/d3-org-chart-flow" type ViewMode = "traditional" | "d3-org-chart" export default function TreePage() { const { treeData, currentTree, refreshData, highlightedMemberId, setHighlightedMemberId } = useFamily() const { calculateRelationship } = useRelationship() const { data: session, status } = useSession() const { showAlert } = useDialog() const router = useRouter() const searchParams = useSearchParams() const [viewMode, setViewMode] = useState("traditional") const [isIOSDevice, setIsIOSDevice] = useState(false) // 关系查询模式 const [relationMode, setRelationMode] = useState(false) const [selectedMembers, setSelectedMembers] = useState([]) const [relationResult, setRelationResult] = useState(null) const [showRelationDialog, setShowRelationDialog] = useState(false) // 使用ref存储最新的relationMode值,避免回调重新创建 const relationModeRef = useRef(relationMode) const selectedMembersRef = useRef(selectedMembers) useEffect(() => { relationModeRef.current = relationMode selectedMembersRef.current = selectedMembers }, [relationMode, selectedMembers]) // 未登录时重定向到登录页 useEffect(() => { if (status === 'unauthenticated') { router.push('/auth/signin') } }, [status, router]) // 页面加载时刷新数据 useEffect(() => { if (currentTree?.id) { refreshData() } }, [currentTree?.id, refreshData]) // 从 URL 参数读取视图模式,iOS 触摸设备默认使用传统族谱 useEffect(() => { if (!searchParams) return const view = searchParams.get('view') // 检测 iOS 触摸设备(iPad/iPhone 上的所有浏览器) const isTouchDevice = typeof navigator !== 'undefined' && navigator.maxTouchPoints > 1 const isIOS = typeof navigator !== 'undefined' && ( /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && isTouchDevice) || /CriOS/.test(navigator.userAgent) // iOS Chrome ) // 设置 iOS 设备状态 setIsIOSDevice(isIOS) if (view === 'd3-org-chart' && !isIOS) { // 非 iOS 设备可以使用 D3 setViewMode(view) } else if (view === 'traditional' || isIOS) { // iOS 设备强制使用传统族谱 setViewMode('traditional') } }, [searchParams]) const [scale, setScale] = useState(1) const [position, setPosition] = useState({ x: 0, y: 0 }) const [isDragging, setIsDragging] = useState(false) const [startPos, setStartPos] = useState({ x: 0, y: 0 }) const [dragStartPos, setDragStartPos] = useState({ x: 0, y: 0 }) const [expandAll, setExpandAll] = useState(false) const [hasInitialPositioned, setHasInitialPositioned] = useState(false) const containerRef = useRef(null) const d3ChartRef = useRef(null) // 传统族谱初始定位到始祖并自适应缩放(如果有高亮成员则跳过) useEffect(() => { if (viewMode !== 'traditional' || hasInitialPositioned || !treeData.rootId) return // 如果有高亮成员,跳过初始定位,让高亮定位逻辑处理 if (highlightedMemberId) { setHasInitialPositioned(true) return } // 找到始祖成员:优先使用标记为 isFounder 的成员,否则使用 rootId 对应的成员 const founderMember = Object.values(treeData.members).find(m => m.isFounder === true) || treeData.members[treeData.rootId] if (!founderMember) return // 延迟执行,等待 DOM 渲染完成 const timer = setTimeout(() => { if (!containerRef.current) return // 查找树内容容器(包含 TreeLayout 的 div) const treeContent = containerRef.current.querySelector('div[style*="transform"]') as HTMLElement if (!treeContent) return // 查找始祖节点的 DOM 元素 const founderNode = containerRef.current.querySelector(`[data-member-id="${founderMember.id}"]`) as HTMLElement if (!founderNode) return const containerRect = containerRef.current.getBoundingClientRect() const treeRect = treeContent.getBoundingClientRect() // 计算树的实际尺寸(当前缩放为1) const treeWidth = treeRect.width const treeHeight = treeRect.height // 计算适合容器的缩放比例,留出一些边距 const padding = 40 const scaleX = (containerRect.width - padding * 2) / treeWidth const scaleY = (containerRect.height - padding * 2) / treeHeight const fitScale = Math.min(scaleX, scaleY, 1) // 最大不超过1 const newScale = Math.max(fitScale, 0.2) // 最小0.2 // 先设置缩放 setScale(newScale) // 延迟计算位置(等待缩放生效) setTimeout(() => { if (!containerRef.current) return const updatedFounderNode = containerRef.current.querySelector(`[data-member-id="${founderMember.id}"]`) as HTMLElement if (!updatedFounderNode) return const updatedContainerRect = containerRef.current.getBoundingClientRect() const updatedNodeRect = updatedFounderNode.getBoundingClientRect() // 计算始祖节点相对于容器中心的偏移 const containerCenterX = updatedContainerRect.width / 2 const nodeCenterX = updatedNodeRect.left - updatedContainerRect.left + updatedNodeRect.width / 2 // 计算需要的 x 偏移量使始祖居中 const offsetX = containerCenterX - nodeCenterX setPosition({ x: offsetX, y: 0 }) setHasInitialPositioned(true) }, 100) }, 500) return () => clearTimeout(timer) }, [viewMode, treeData.rootId, treeData.members, hasInitialPositioned, highlightedMemberId]) // 切换视图模式时重置初始定位状态和缩放位置 useEffect(() => { setHasInitialPositioned(false) setScale(1) setPosition({ x: 0, y: 0 }) }, [viewMode]) // 当有高亮成员时,定位到该成员并居中显示,设置 scale 为 1.0 useEffect(() => { if (viewMode !== 'traditional' || !highlightedMemberId) return // 第一步:重置位置和缩放为初始状态 setScale(1) setPosition({ x: 0, y: 0 }) // 延迟执行,等待 DOM 完全渲染(需要足够长的时间让树完全展开) const timer = setTimeout(() => { if (!containerRef.current) { return } // 查找高亮成员的 DOM 元素 const highlightedNode = containerRef.current.querySelector(`[data-member-id="${highlightedMemberId}"]`) as HTMLElement if (!highlightedNode) { return } const containerRect = containerRef.current.getBoundingClientRect() const nodeRect = highlightedNode.getBoundingClientRect() // 计算容器中心 const containerCenterX = containerRect.width / 2 const containerCenterY = containerRect.height / 2 // 此时 position 已经是 {x:0, y:0},所以节点位置就是相对于原点的位置 // 我们需要计算让节点居中的偏移量 const nodeCenterX = nodeRect.left - containerRect.left + nodeRect.width / 2 const nodeCenterY = nodeRect.top - containerRect.top + nodeRect.height / 2 // 计算偏移量 const offsetX = containerCenterX - nodeCenterX const offsetY = containerCenterY - nodeCenterY setPosition({ x: offsetX, y: offsetY }) // 30秒后清除高亮状态 setTimeout(() => { setHighlightedMemberId(null) }, 30000) }, 1000) // 增加到1秒,确保DOM完全渲染 return () => clearTimeout(timer) }, [viewMode, highlightedMemberId, setHighlightedMemberId]) // 传统族谱自适应视图函数 const handleTraditionalFitView = useCallback(() => { if (!containerRef.current || !treeData.rootId) return // 找到始祖成员 const founderMember = Object.values(treeData.members).find(m => m.isFounder === true) || treeData.members[treeData.rootId] if (!founderMember) return // 第一步:重置到标准状态 (scale=1, position=0) setScale(1) setPosition({ x: 0, y: 0 }) // 第二步:等待 DOM 更新后计算 setTimeout(() => { if (!containerRef.current) return // 查找树内容容器 const treeContent = containerRef.current.querySelector('div[style*="transform"]') as HTMLElement if (!treeContent) return // 查找始祖节点 const founderNode = containerRef.current.querySelector(`[data-member-id="${founderMember.id}"]`) as HTMLElement if (!founderNode) return const containerRect = containerRef.current.getBoundingClientRect() const treeRect = treeContent.getBoundingClientRect() // 此时 scale=1,所以 treeRect 就是实际尺寸 const treeWidth = treeRect.width const treeHeight = treeRect.height // 计算适合容器的缩放比例 const padding = 40 const scaleX = (containerRect.width - padding * 2) / treeWidth const scaleY = (containerRect.height - padding * 2) / treeHeight const fitScale = Math.min(scaleX, scaleY, 1) const newScale = Math.max(fitScale, 0.2) // 设置新缩放 setScale(newScale) // 第三步:等待缩放生效后计算始祖居中位置 setTimeout(() => { if (!containerRef.current) return const updatedFounderNode = containerRef.current.querySelector(`[data-member-id="${founderMember.id}"]`) as HTMLElement if (!updatedFounderNode) return const updatedContainerRect = containerRef.current.getBoundingClientRect() const updatedNodeRect = updatedFounderNode.getBoundingClientRect() // 计算始祖节点居中的偏移 const containerCenterX = updatedContainerRect.width / 2 const nodeCenterX = updatedNodeRect.left - updatedContainerRect.left + updatedNodeRect.width / 2 const offsetX = containerCenterX - nodeCenterX setPosition({ x: offsetX, y: 0 }) }, 100) }, 100) }, [treeData.rootId, treeData.members]) const handleMouseDown = (e: React.MouseEvent) => { // 如果点击的是节点卡片,不启动拖拽 const target = e.target as HTMLElement if (target.closest('.family-node-card')) { return } setIsDragging(true) setStartPos({ x: e.clientX - position.x, y: e.clientY - position.y }) setDragStartPos({ x: e.clientX, y: e.clientY }) } const handleMouseMove = (e: React.MouseEvent) => { if (!isDragging) return // 只有移动超过5px才算拖拽,避免误触 const deltaX = Math.abs(e.clientX - dragStartPos.x) const deltaY = Math.abs(e.clientY - dragStartPos.y) if (deltaX < 5 && deltaY < 5) return setPosition({ x: e.clientX - startPos.x, y: e.clientY - startPos.y, }) } const handleMouseUp = () => { setIsDragging(false) } // 触摸事件状态 const touchStartRef = useRef<{ x: number; y: number; distance?: number } | null>(null) const positionRef = useRef(position) const scaleRef = useRef(scale) // 保持 ref 同步 useEffect(() => { positionRef.current = position }, [position]) useEffect(() => { scaleRef.current = scale }, [scale]) // 滚轮和触摸事件处理函数 - 使用 useCallback 确保稳定引用 const bindWheelEvents = useCallback((container: HTMLDivElement | null) => { if (!container) return // 清理之前的事件监听器 const cleanup = (container as any).__wheelCleanup if (cleanup) { cleanup() delete (container as any).__wheelCleanup } // 鼠标滚轮缩放 const handleWheel = (e: WheelEvent) => { e.preventDefault() const delta = e.deltaY > 0 ? -0.1 : 0.1 setScale((s) => Math.min(Math.max(s + delta, 0.1), 2)) } // 计算两指间距离 const getTouchDistance = (touches: TouchList) => { if (touches.length < 2) return 0 const dx = touches[0].clientX - touches[1].clientX const dy = touches[0].clientY - touches[1].clientY return Math.sqrt(dx * dx + dy * dy) } // 触摸开始 const handleTouchStart = (e: globalThis.TouchEvent) => { if (e.touches.length === 1) { touchStartRef.current = { x: e.touches[0].clientX - positionRef.current.x, y: e.touches[0].clientY - positionRef.current.y } } else if (e.touches.length === 2) { touchStartRef.current = { x: (e.touches[0].clientX + e.touches[1].clientX) / 2, y: (e.touches[0].clientY + e.touches[1].clientY) / 2, distance: getTouchDistance(e.touches) } } } // 触摸移动 const handleTouchMove = (e: globalThis.TouchEvent) => { if (!touchStartRef.current) return e.preventDefault() if (e.touches.length === 1 && !touchStartRef.current.distance) { const newX = e.touches[0].clientX - touchStartRef.current.x const newY = e.touches[0].clientY - touchStartRef.current.y setPosition({ x: newX, y: newY }) } else if (e.touches.length === 2 && touchStartRef.current.distance) { const newDistance = getTouchDistance(e.touches) const scaleFactor = newDistance / touchStartRef.current.distance setScale(s => Math.min(Math.max(s * scaleFactor, 0.1), 2)) touchStartRef.current.distance = newDistance } } // 触摸结束 const handleTouchEnd = () => { touchStartRef.current = null } // 添加事件监听器,设置 passive: false container.addEventListener('wheel', handleWheel, { passive: false }) container.addEventListener('touchstart', handleTouchStart, { passive: true }) container.addEventListener('touchmove', handleTouchMove, { passive: false }) container.addEventListener('touchend', handleTouchEnd, { passive: true }) // 保存清理函数 ;(container as any).__wheelCleanup = () => { container.removeEventListener('wheel', handleWheel) container.removeEventListener('touchstart', handleTouchStart) container.removeEventListener('touchmove', handleTouchMove) container.removeEventListener('touchend', handleTouchEnd) } }, []) // 使用 ref callback 在 DOM 挂载时绑定事件 const containerRefCallback = useCallback((node: HTMLDivElement | null) => { // 更新 ref (containerRef as React.MutableRefObject).current = node // 绑定事件 if (node && viewMode === 'traditional') { bindWheelEvents(node) } }, [viewMode, bindWheelEvents]) // 清理事件监听器 useEffect(() => { return () => { const container = containerRef.current if (container && (container as any).__wheelCleanup) { (container as any).__wheelCleanup() delete (container as any).__wheelCleanup } } }, []) // 处理成员选择(关系查询模式) const handleMemberClick = (memberId: string) => { if (selectedMembers.length === 0) { // 选择第一个成员 setSelectedMembers([memberId]) } else if (selectedMembers.length === 1) { // 选择第二个成员,计算关系 const [firstId] = selectedMembers if (firstId === memberId) { // 点击同一个人,取消选择 setSelectedMembers([]) return } const result = calculateRelationship(firstId, memberId) const firstMember = treeData.members[firstId] const secondMember = treeData.members[memberId] setRelationResult({ from: firstMember, to: secondMember, relationship: result }) setShowRelationDialog(true) setSelectedMembers([]) setRelationMode(false) } } // 切换关系查询模式 const toggleRelationMode = () => { setRelationMode(!relationMode) setSelectedMembers([]) setRelationResult(null) } // 处理对话框关闭 const handleDialogClose = (open: boolean) => { setShowRelationDialog(open) if (!open && relationResult) { // 清除选中样式 const clearNodeStyle = (nodeId: string) => { const chartContainer = document.querySelector('.w-full.h-full.bg-background.rounded-lg.border') const svg = chartContainer?.querySelector('svg') if (!svg) return const foreignObjects = svg.querySelectorAll('foreignObject') foreignObjects.forEach((fo: any) => { const nodeData = (fo.parentNode as any)?.__data__?.data if (nodeData?.id === nodeId) { const nameDiv = fo.querySelector('div[style*="font-size: 13px"]') as HTMLElement if (nameDiv) { // 移除勾选图标 const checkIcon = nameDiv.querySelector('svg') if (checkIcon) { checkIcon.remove() } } } }) } clearNodeStyle(relationResult.from.id) clearNodeStyle(relationResult.to.id) } } // 使用useCallback创建稳定的回调,通过ref访问最新的relationMode const handleD3MemberClick = useCallback((id: string) => { console.log('D3 onMemberClick triggered:', id, 'relationMode:', relationModeRef.current) if (relationModeRef.current) { console.log('Calling handleMemberClick, current selected:', selectedMembersRef.current) // 直接操作 DOM 来显示选中标记 // D3 图表的节点在 SVG 的 foreignObject 中,需要特殊查询 // 注意:配偶没有独立的节点,需要在配偶区域显示选中标记 const updateSelectionMark = (memberId: string, selected: boolean) => { // 首先尝试查找主成员节点 let nodeElement = document.querySelector(`[data-member-id="${memberId}"]`) as HTMLElement let isSpouse = false // 如果找不到,可能是配偶,尝试查找配偶区域 if (!nodeElement) { const spouseArea = document.querySelector(`[data-spouse-id="${memberId}"]`) as HTMLElement if (spouseArea) { nodeElement = spouseArea isSpouse = true } } if (!nodeElement) { console.log('Node element not found for:', memberId) return } console.log('Updating selection mark for:', memberId, 'isSpouse:', isSpouse, 'selected:', selected) if (selected) { // 只添加打钩标记,不改变边框和背景 if (!nodeElement.querySelector('.selection-mark')) { const checkMark = document.createElement('div') checkMark.className = 'selection-mark' if (isSpouse) { // 配偶区域打钩位置(右边) checkMark.style.cssText = 'position: absolute; top: -6px; right: -6px; z-index: 20;' nodeElement.style.position = 'relative' } else { // 主成员打钩位置(左边) checkMark.style.cssText = 'position: absolute; top: 6px; left: 6px; z-index: 20;' } checkMark.innerHTML = `` nodeElement.appendChild(checkMark) } } else { // 移除选中标记 const mark = nodeElement.querySelector('.selection-mark') if (mark) mark.remove() } } // 直接处理选择逻辑 if (selectedMembersRef.current.length === 0) { // 选择第一个成员 updateSelectionMark(id, true) setSelectedMembers([id]) } else if (selectedMembersRef.current.length === 1) { const [firstId] = selectedMembersRef.current if (firstId === id) { // 点击同一个成员,取消选择 updateSelectionMark(id, false) setSelectedMembers([]) return } // 选择第二个成员,显示选中标记 updateSelectionMark(id, true) setSelectedMembers([firstId, id]) const result = calculateRelationship(firstId, id) const firstMember = treeData.members[firstId] const secondMember = treeData.members[id] setRelationResult({ from: firstMember, to: secondMember, relationship: result }) setShowRelationDialog(true) // 延迟清除选中状态 setTimeout(() => { // 清除所有选中标记 document.querySelectorAll('.selection-mark').forEach(mark => mark.remove()) setSelectedMembers([]) setRelationMode(false) }, 300) } } else { console.log('Navigating to member detail') const params = new URLSearchParams(window.location.search) router.push(`/members/${id}?${params.toString()}`) } }, [router, calculateRelationship, treeData.members]) const handleExportImage = async () => { if (!containerRef.current) return try { // 临时重置位置和缩放以获取完整视图 const originalPosition = { ...position } const originalScale = scale setPosition({ x: 0, y: 0 }) setScale(1) // 等待 DOM 更新 await new Promise(resolve => setTimeout(resolve, 800)) // 找到实际的族谱容器(包含 TreeLayout 的 div) const treeContainer = containerRef.current.querySelector('div[style*="transform"]') as HTMLElement if (!treeContainer) { throw new Error('找不到族谱容器') } // 查找所有连接线(使用更宽泛的选择器) const allDivs = treeContainer.querySelectorAll('div') const lineElements: HTMLElement[] = [] allDivs.forEach(div => { const el = div as HTMLElement const classes = String(el.className || '') // 查找包含 bg-foreground 的元素(连接线) if (classes.includes('bg-foreground')) { lineElements.push(el) } }) // 克隆容器用于导出 const clonedContainer = treeContainer.cloneNode(true) as HTMLElement // 在克隆的容器中找到对应的连接线并设置样式 const clonedDivs = clonedContainer.querySelectorAll('div') let lineIndex = 0 clonedDivs.forEach(div => { const el = div as HTMLElement const classes = String(el.className || '') if (classes.includes('bg-foreground') && lineElements[lineIndex]) { const original = lineElements[lineIndex] const computed = window.getComputedStyle(original) // 直接设置内联样式 el.style.backgroundColor = computed.backgroundColor el.style.width = computed.width el.style.height = computed.height el.style.position = 'absolute' el.style.left = computed.left el.style.top = computed.top lineIndex++ } }) // 移除卡片边框 clonedContainer.querySelectorAll('*').forEach((child) => { const el = child as HTMLElement const classes = String(el.className || '') // 跳过连接线和连接线容器 if (classes.includes('bg-foreground') || classes.includes('pointer-events-none')) { return } // 移除其他元素的边框 el.style.border = 'none' el.style.outline = 'none' }) // 临时添加到 DOM clonedContainer.style.position = 'fixed' clonedContainer.style.left = '-9999px' clonedContainer.style.top = '0' document.body.appendChild(clonedContainer) // 等待渲染 await new Promise(resolve => setTimeout(resolve, 200)) // 动态导入 dom-to-image-more(仅在客户端) const domtoimage = await import('dom-to-image-more') // 使用 toBlob 方法 const blob = await domtoimage.default.toBlob(clonedContainer, { quality: 0.95, bgcolor: '#ffffff' }) // 清理克隆的容器 document.body.removeChild(clonedContainer) // 恢复原始位置和缩放 setPosition(originalPosition) setScale(originalScale) // 创建下载链接 const url = URL.createObjectURL(blob) const link = document.createElement('a') link.download = `family-tree-${new Date().toISOString().split('T')[0]}.png` link.href = url link.click() // 清理 setTimeout(() => URL.revokeObjectURL(url), 100) } catch (error) { console.error('导出图片失败:', error) } } // 导出 PDF(按世代导出成员信息) const handleExportPDF = useCallback(async () => { if (!treeData.members || Object.keys(treeData.members).length === 0) { await showAlert('没有成员数据可导出', "提示") return } try { const members = Object.values(treeData.members) const getMemberName = (id: string) => treeData.members[id]?.fullName || '' const treeName = currentTree?.name || '家族' // 获取当前页面的基础 URL const baseUrl = window.location.origin await exportFamilyHTML(members, getMemberName, treeName, baseUrl) } catch (error) { console.error('导出PDF失败:', error) await showAlert('导出失败,请重试', "错误") } }, [treeData.members, currentTree?.name, showAlert]) // 未登录时不渲染内容(已在 useEffect 中重定向) if (status === 'loading' || !session) { return null } // Handle root not found if (!treeData.rootId) { return (

谱系待续

) } return (
{/* 视图切换和关系查询按钮 - 移动端自适应 */}
{/* 关系查询按钮 */} {/* iOS 设备只显示传统族谱,不显示切换按钮 */} {!isIOSDevice && ( { const params = new URLSearchParams(searchParams.toString()) params.set('view', 'traditional') router.push(`/tree?${params.toString()}`) }} className={viewMode === 'traditional' ? 'bg-accent' : ''} >
传统族谱 {viewMode === 'traditional' && }
{/* iOS 设备不显示 D3 选项(foreignObject 渲染问题) */} {!isIOSDevice && ( { const params = new URLSearchParams(searchParams.toString()) params.set('view', 'd3-org-chart') router.push(`/tree?${params.toString()}`) }} className={viewMode === 'd3-org-chart' ? 'bg-accent' : ''} >
D3族谱 {viewMode === 'd3-org-chart' && }
)}
)}
{viewMode === 'traditional' ? (
{/* Toolbar - 移动端底部横向,桌面端左侧纵向 */}
{/* Tree Canvas */}
{/* 缩略图导航 */}
) : (
{/* 统一的控制按钮 - 移动端底部横向,桌面端左侧纵向 */}
)} {/* 关系查询结果对话框 */} 关系查询结果

显示两个家族成员之间的关系

{relationResult && (
{/* 成员信息 */}
第{relationResult.from.generation}世
{relationResult.relationship.term}
第{relationResult.to.generation}世
{/* 关系说明 */}
关系说明:
{relationResult.relationship.term}
{/* 关系路径 */} {relationResult.relationship.path && (
关系路径:
)}
)}
) }