0.6.0.2
This commit is contained in:
+167
-4
@@ -16,13 +16,14 @@ import {
|
||||
} 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 } from "lucide-react"
|
||||
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"
|
||||
|
||||
// 动态导入 D3 图表组件(减少初始加载体积)
|
||||
const D3OrgChartFlow = dynamic(
|
||||
@@ -92,9 +93,138 @@ export default function TreePage() {
|
||||
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<HTMLDivElement>(null)
|
||||
const d3ChartRef = useRef<D3OrgChartRef>(null)
|
||||
|
||||
// 传统族谱初始定位到始祖并自适应缩放
|
||||
useEffect(() => {
|
||||
if (viewMode !== 'traditional' || hasInitialPositioned || !treeData.rootId) 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])
|
||||
|
||||
// 切换视图模式时重置初始定位状态和缩放位置
|
||||
useEffect(() => {
|
||||
setHasInitialPositioned(false)
|
||||
setScale(1)
|
||||
setPosition({ x: 0, y: 0 })
|
||||
}, [viewMode])
|
||||
|
||||
// 传统族谱自适应视图函数
|
||||
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
|
||||
|
||||
// 查找树内容容器
|
||||
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()
|
||||
|
||||
// 计算树的实际尺寸(需要除以当前缩放)
|
||||
const treeWidth = treeRect.width / scale
|
||||
const treeHeight = treeRect.height / scale
|
||||
|
||||
// 计算适合容器的缩放比例
|
||||
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)
|
||||
|
||||
// 先重置位置到0,再设置缩放
|
||||
setPosition({ x: 0, y: 0 })
|
||||
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 })
|
||||
}, 150)
|
||||
}, [treeData.rootId, treeData.members, scale])
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
// 如果点击的是节点卡片,不启动拖拽
|
||||
const target = e.target as HTMLElement
|
||||
@@ -406,6 +536,27 @@ export default function TreePage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 导出 PDF(按世代导出成员信息)
|
||||
const handleExportPDF = useCallback(async () => {
|
||||
if (!treeData.members || Object.keys(treeData.members).length === 0) {
|
||||
alert('没有成员数据可导出')
|
||||
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)
|
||||
alert('导出失败,请重试')
|
||||
}
|
||||
}, [treeData.members, currentTree?.name])
|
||||
|
||||
// 未登录时不渲染内容(已在 useEffect 中重定向)
|
||||
if (status === 'loading' || !session) {
|
||||
return null
|
||||
@@ -495,13 +646,19 @@ export default function TreePage() {
|
||||
<Button variant="outline" size="icon" onClick={() => setScale((s) => Math.max(s - 0.1, 0.1))}>
|
||||
<ZoomOut className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" onClick={() => setPosition({ x: 0, y: 0 })}>
|
||||
<Button variant="outline" size="icon" onClick={handleTraditionalFitView} title="自适应视图">
|
||||
<Move className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" onClick={() => setExpandAll(true)} title="完全展开">
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="h-px bg-border my-1" />
|
||||
<Button variant="outline" size="icon" onClick={handleExportImage} title="导出为图片">
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" onClick={handleExportPDF} title="导出族谱PDF">
|
||||
<FileText className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Tree Canvas */}
|
||||
@@ -527,6 +684,9 @@ export default function TreePage() {
|
||||
relationMode={relationMode}
|
||||
selectedMembers={selectedMembers}
|
||||
onMemberClick={handleMemberClick}
|
||||
expandAll={expandAll}
|
||||
onExpandAllChange={setExpandAll}
|
||||
scale={scale}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -535,8 +695,8 @@ export default function TreePage() {
|
||||
<div className="flex-1 relative min-h-0">
|
||||
{/* 统一的控制按钮 */}
|
||||
<div className="absolute top-4 left-4 z-20 flex flex-col gap-2 bg-background/80 backdrop-blur p-2 rounded-lg border shadow-sm">
|
||||
<Button variant="outline" size="icon" onClick={() => d3ChartRef.current?.expandAll()} title="展开全部">
|
||||
<ZoomIn className="h-4 w-4" />
|
||||
<Button variant="outline" size="icon" onClick={() => d3ChartRef.current?.expandAll()} title="完全展开">
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" onClick={() => d3ChartRef.current?.collapseAll()} title="折叠全部">
|
||||
<ZoomOut className="h-4 w-4" />
|
||||
@@ -548,6 +708,9 @@ export default function TreePage() {
|
||||
<Button variant="outline" size="icon" onClick={() => d3ChartRef.current?.exportPNG()} title="导出PNG">
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" onClick={handleExportPDF} title="导出族谱PDF">
|
||||
<FileText className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-full">
|
||||
|
||||
Reference in New Issue
Block a user