0.3.0.6
This commit is contained in:
@@ -4,9 +4,18 @@ import { useEffect, useRef, useState, forwardRef, useImperativeHandle, memo } fr
|
||||
import * as d3 from "d3"
|
||||
// @ts-ignore - d3-org-chart没有TypeScript类型定义
|
||||
import { OrgChart } from "d3-org-chart"
|
||||
import { Download, FileText } from "lucide-react"
|
||||
import { Download, FileText, UserPlus, Users, Baby } from "lucide-react"
|
||||
import type { FamilyMember } from "@/types/family"
|
||||
import jsPDF from "jspdf"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useFamily } from "@/context/family-context"
|
||||
|
||||
// 全局类型声明
|
||||
declare global {
|
||||
interface Window {
|
||||
d3ChartContextMenu?: (event: MouseEvent, memberId: string) => void
|
||||
}
|
||||
}
|
||||
|
||||
interface D3OrgChartFlowProps {
|
||||
members: Record<string, FamilyMember>
|
||||
@@ -66,6 +75,88 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
|
||||
const chartInstanceRef = useRef<any>(null)
|
||||
const [isReady, setIsReady] = useState(false)
|
||||
const [avatarCache, setAvatarCache] = useState<Map<string, string>>(new Map())
|
||||
const [contextMenuNode, setContextMenuNode] = useState<FamilyMember | null>(null)
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 })
|
||||
const router = useRouter()
|
||||
const { currentTree } = useFamily()
|
||||
|
||||
// 构建新增成员的 URL
|
||||
const buildAddUrl = (type: 'father' | 'mother' | 'spouse' | 'child', member: FamilyMember) => {
|
||||
const params = new URLSearchParams()
|
||||
if (currentTree?.id) {
|
||||
params.set('treeId', currentTree.id)
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'father':
|
||||
params.set('childId', member.id)
|
||||
params.set('gender', 'MALE')
|
||||
params.set('generation', (member.generation - 1).toString())
|
||||
// 如果已有母亲,设为配偶
|
||||
if (member.motherId) {
|
||||
params.set('spouseId', member.motherId)
|
||||
}
|
||||
break
|
||||
case 'mother':
|
||||
params.set('childId', member.id)
|
||||
params.set('gender', 'FEMALE')
|
||||
params.set('generation', (member.generation - 1).toString())
|
||||
// 如果已有父亲,设为配偶
|
||||
if (member.fatherId) {
|
||||
params.set('spouseId', member.fatherId)
|
||||
}
|
||||
break
|
||||
case 'spouse':
|
||||
params.set('spouseId', member.id)
|
||||
params.set('generation', member.generation.toString())
|
||||
// 配偶性别与当前成员相反
|
||||
params.set('gender', member.gender === 'MALE' ? 'FEMALE' : 'MALE')
|
||||
|
||||
// 如果当前成员有子女,将他们也作为新配偶的子女
|
||||
if (member.childrenIds && member.childrenIds.length > 0) {
|
||||
// 传递所有子女ID
|
||||
member.childrenIds.forEach(id => {
|
||||
params.append('childrenIds', id)
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'child':
|
||||
// 根据当前成员性别设置父/母
|
||||
if (member.gender === 'MALE') {
|
||||
params.set('fatherId', member.id)
|
||||
// 如果父亲有配偶,设置配偶为母亲
|
||||
if (member.spouseIds && member.spouseIds.length > 0) {
|
||||
params.set('motherId', member.spouseIds[0])
|
||||
}
|
||||
} else {
|
||||
params.set('motherId', member.id)
|
||||
// 如果母亲有配偶,设置配偶为父亲
|
||||
if (member.spouseIds && member.spouseIds.length > 0) {
|
||||
params.set('fatherId', member.spouseIds[0])
|
||||
}
|
||||
}
|
||||
params.set('generation', (member.generation + 1).toString())
|
||||
break
|
||||
}
|
||||
|
||||
return `/members/new?${params.toString()}`
|
||||
}
|
||||
|
||||
const handleAddMember = (type: 'father' | 'mother' | 'spouse' | 'child', member: FamilyMember) => {
|
||||
const url = buildAddUrl(type, member)
|
||||
console.log('D3 handleAddMember called:', type, url)
|
||||
// 使用 setTimeout 延迟跳转,确保菜单关闭后再执行
|
||||
setTimeout(() => {
|
||||
console.log('Navigating to:', url)
|
||||
window.location.href = url
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 判断可以添加的关系
|
||||
const canAddFather = (member: FamilyMember) => !member.fatherId
|
||||
const canAddMother = (member: FamilyMember) => !member.motherId
|
||||
const canAddSpouse = (member: FamilyMember) => true // 配偶总是可以添加
|
||||
const canAddChild = (member: FamilyMember) => true // 子女总是可以添加
|
||||
|
||||
// 转换数据格式
|
||||
const convertToChartData = (): ChartNode[] => {
|
||||
@@ -205,19 +296,24 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
|
||||
const isSelected = selectedMembers.includes(node.id)
|
||||
|
||||
return `
|
||||
<div style="
|
||||
position: relative;
|
||||
width: 160px;
|
||||
height: 120px;
|
||||
box-sizing: border-box;
|
||||
background: ${node.deathYear ? '#fafafa' : 'white'};
|
||||
border: 2px solid ${color};
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
" onmouseover="this.style.boxShadow='0 4px 12px rgba(0,0,0,0.2)'" onmouseout="this.style.boxShadow='0 2px 8px rgba(0,0,0,0.1)'">
|
||||
<div
|
||||
data-member-id="${node.id}"
|
||||
style="
|
||||
position: relative;
|
||||
width: 160px;
|
||||
height: 120px;
|
||||
box-sizing: border-box;
|
||||
background: ${node.deathYear ? '#fafafa' : 'white'};
|
||||
border: 2px solid ${color};
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
"
|
||||
onmouseover="this.style.boxShadow='0 4px 12px rgba(0,0,0,0.2)'"
|
||||
onmouseout="this.style.boxShadow='0 2px 8px rgba(0,0,0,0.1)'"
|
||||
>
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 5px;">
|
||||
<div style="
|
||||
width: 40px;
|
||||
@@ -314,6 +410,28 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 添加右键菜单支持
|
||||
// 直接为节点添加右键事件监听
|
||||
setTimeout(() => {
|
||||
const nodes = chartRef.current?.querySelectorAll('[data-member-id]')
|
||||
console.log('Found nodes for context menu:', nodes?.length)
|
||||
nodes?.forEach((node) => {
|
||||
const memberId = (node as HTMLElement).getAttribute('data-member-id')
|
||||
if (memberId) {
|
||||
node.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault()
|
||||
const mouseEvent = e as MouseEvent
|
||||
const member = members[memberId]
|
||||
if (member && !relationMode) {
|
||||
console.log('Right click on member:', member.fullName, 'at', mouseEvent.clientX, mouseEvent.clientY)
|
||||
setContextMenuNode(member)
|
||||
setContextMenuPosition({ x: mouseEvent.clientX, y: mouseEvent.clientY })
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}, 200)
|
||||
}, 100)
|
||||
|
||||
chartInstanceRef.current = chart
|
||||
@@ -324,6 +442,10 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
|
||||
if (chartRef.current) {
|
||||
chartRef.current.innerHTML = ''
|
||||
}
|
||||
// 清理全局函数
|
||||
if (window.d3ChartContextMenu) {
|
||||
delete window.d3ChartContextMenu
|
||||
}
|
||||
}
|
||||
}, [members, rootId, onMemberClick, avatarCache])
|
||||
|
||||
@@ -417,6 +539,88 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
|
||||
className="w-full h-full"
|
||||
style={{ overflow: 'hidden' }}
|
||||
/>
|
||||
|
||||
{/* 自定义右键菜单 */}
|
||||
{contextMenuNode && (
|
||||
<div
|
||||
className="fixed z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md"
|
||||
style={{
|
||||
left: contextMenuPosition.x,
|
||||
top: contextMenuPosition.y,
|
||||
pointerEvents: 'auto'
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="px-2 py-1.5 text-sm text-muted-foreground">
|
||||
为 {contextMenuNode.fullName} 添加
|
||||
</div>
|
||||
<div className="h-px bg-border my-1" />
|
||||
|
||||
{canAddFather(contextMenuNode) && (
|
||||
<div
|
||||
className="relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => {
|
||||
handleAddMember('father', contextMenuNode)
|
||||
setContextMenuNode(null)
|
||||
}}
|
||||
>
|
||||
<UserPlus className="h-4 w-4 text-blue-500" />
|
||||
添加父亲
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canAddMother(contextMenuNode) && (
|
||||
<div
|
||||
className="relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => {
|
||||
handleAddMember('mother', contextMenuNode)
|
||||
setContextMenuNode(null)
|
||||
}}
|
||||
>
|
||||
<UserPlus className="h-4 w-4 text-pink-500" />
|
||||
添加母亲
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(canAddFather(contextMenuNode) || canAddMother(contextMenuNode)) && (canAddSpouse(contextMenuNode) || canAddChild(contextMenuNode)) && (
|
||||
<div className="h-px bg-border my-1" />
|
||||
)}
|
||||
|
||||
{canAddSpouse(contextMenuNode) && (
|
||||
<div
|
||||
className="relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => {
|
||||
handleAddMember('spouse', contextMenuNode)
|
||||
setContextMenuNode(null)
|
||||
}}
|
||||
>
|
||||
<Users className="h-4 w-4 text-red-500" />
|
||||
添加配偶
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canAddChild(contextMenuNode) && (
|
||||
<div
|
||||
className="relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => {
|
||||
handleAddMember('child', contextMenuNode)
|
||||
setContextMenuNode(null)
|
||||
}}
|
||||
>
|
||||
<Baby className="h-4 w-4 text-green-500" />
|
||||
添加子女
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 点击其他地方关闭菜单 */}
|
||||
{contextMenuNode && (
|
||||
<div
|
||||
className="fixed inset-0 z-40"
|
||||
onClick={() => setContextMenuNode(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user