"use client" import { useState, useRef, useEffect, useCallback } from "react" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { GitBranch, Database, ArrowRight, Zap, RefreshCw, Plus, Eye, MousePointer2, Move, Link2, Unlink, CheckCircle2, Settings, ZoomIn, ZoomOut, Maximize2, } from "lucide-react" // 节点类型 interface LineageNode { id: string type: 'source' | 'transform' | 'target' name: string database?: string collection?: string fields: string[] x: number y: number color: string } // 连接类型 interface LineageConnection { id: string sourceNode: string sourceField: string targetNode: string targetField: string } // 节点和连接从API动态加载 export default function LineagePage() { const [nodes, setNodes] = useState([]) const [connections, setConnections] = useState([]) const [selectedNode, setSelectedNode] = useState(null) const [draggingNode, setDraggingNode] = useState(null) const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }) const [connectingFrom, setConnectingFrom] = useState<{ nodeId: string; field: string } | null>(null) const [showNodeDetail, setShowNodeDetail] = useState(false) const [zoom, setZoom] = useState(0.8) const [loading, setLoading] = useState(true) const canvasRef = useRef(null) // 加载真实数据库结构 useEffect(() => { setLoading(true) fetch('/api/database-structure?action=lineage') .then(res => res.json()) .then(data => { if (data.success) { setNodes(data.nodes || []) setConnections(data.connections || []) } }) .catch(console.error) .finally(() => setLoading(false)) }, []) // 节点拖动开始 const handleNodeMouseDown = (e: React.MouseEvent, nodeId: string) => { e.stopPropagation() const node = nodes.find(n => n.id === nodeId) if (!node) return setDraggingNode(nodeId) setDragOffset({ x: e.clientX - node.x * zoom, y: e.clientY - node.y * zoom, }) } // 节点拖动 const handleMouseMove = useCallback((e: MouseEvent) => { if (!draggingNode || !canvasRef.current) return const rect = canvasRef.current.getBoundingClientRect() const newX = (e.clientX - rect.left - dragOffset.x + rect.left) / zoom const newY = (e.clientY - rect.top - dragOffset.y + rect.top) / zoom setNodes(prev => prev.map(node => node.id === draggingNode ? { ...node, x: Math.max(0, newX), y: Math.max(0, newY) } : node )) }, [draggingNode, dragOffset, zoom]) // 节点拖动结束 const handleMouseUp = useCallback(() => { setDraggingNode(null) }, []) useEffect(() => { if (draggingNode) { window.addEventListener('mousemove', handleMouseMove) window.addEventListener('mouseup', handleMouseUp) return () => { window.removeEventListener('mousemove', handleMouseMove) window.removeEventListener('mouseup', handleMouseUp) } } }, [draggingNode, handleMouseMove, handleMouseUp]) // 开始连接字段 const startConnecting = (nodeId: string, field: string) => { setConnectingFrom({ nodeId, field }) } // 完成连接 const finishConnecting = (nodeId: string, field: string) => { if (!connectingFrom || connectingFrom.nodeId === nodeId) { setConnectingFrom(null) return } // 创建新连接 const newConnection: LineageConnection = { id: `conn_${Date.now()}`, sourceNode: connectingFrom.nodeId, sourceField: connectingFrom.field, targetNode: nodeId, targetField: field, } setConnections(prev => [...prev, newConnection]) setConnectingFrom(null) } // 删除连接 const deleteConnection = (connId: string) => { setConnections(prev => prev.filter(c => c.id !== connId)) } // 获取节点位置 const getNodeFieldPosition = (nodeId: string, field: string, isSource: boolean) => { const node = nodes.find(n => n.id === nodeId) if (!node) return { x: 0, y: 0 } const fieldIndex = node.fields.indexOf(field) const nodeWidth = 200 const headerHeight = 40 const fieldHeight = 28 return { x: isSource ? node.x + nodeWidth : node.x, y: node.y + headerHeight + fieldIndex * fieldHeight + fieldHeight / 2, } } // 渲染连接线 const renderConnections = () => { return connections.map(conn => { const source = getNodeFieldPosition(conn.sourceNode, conn.sourceField, true) const target = getNodeFieldPosition(conn.targetNode, conn.targetField, false) // 贝塞尔曲线 const midX = (source.x + target.x) / 2 const path = `M ${source.x} ${source.y} C ${midX} ${source.y}, ${midX} ${target.y}, ${target.x} ${target.y}` return ( deleteConnection(conn.id)}> ) }) } // 渲染节点 const renderNode = (node: LineageNode) => { const isConnecting = connectingFrom !== null const isSource = connectingFrom?.nodeId === node.id return (
handleNodeMouseDown(e, node.id)} onClick={() => { setSelectedNode(node) setShowNodeDetail(true) }} > {/* 节点头部 */}
{node.type === 'source' && } {node.type === 'transform' && } {node.type === 'target' && } {node.name}
{/* 字段列表 */}
{node.fields.map((field, i) => (
{ e.stopPropagation() if (isConnecting && !isSource) { finishConnecting(node.id, field) } else if (!isConnecting) { startConnecting(node.id, field) } }} > {field}
))}
{/* 数据库信息 */} {node.database && (
{node.database}.{node.collection}
)}
) } return (
{/* 顶部标题 */}

数据血缘

可视化数据流向,拖拽节点,点击字段创建关联

{loading && ( 加载真实数据库... )}
{Math.round(zoom * 100)}%
{/* 操作提示 */}
拖拽节点移动位置
点击字段圆点创建连接
点击连线删除关联
{connectingFrom && ( 正在连接: {connectingFrom.field} → 点击目标字段完成 )}
{/* 图例 */}
数据源
转换层
目标表
节点: {nodes.length} | 连接: {connections.length}
{/* 画布 */}
{/* SVG连接线层 */} {renderConnections()} {/* 节点层 */}
{nodes.map(renderNode)}
{/* 节点详情弹窗 */} {selectedNode?.name} {selectedNode?.type === 'source' && '数据源节点'} {selectedNode?.type === 'transform' && '转换处理节点'} {selectedNode?.type === 'target' && '目标输出节点'} {selectedNode && (
{selectedNode.database && (
{selectedNode.database}.{selectedNode.collection}
)}
{selectedNode.fields.map(field => ( {field} ))}
{connections.filter(c => c.sourceNode === selectedNode.id || c.targetNode === selectedNode.id).map(conn => { const isSource = conn.sourceNode === selectedNode.id const otherNode = nodes.find(n => n.id === (isSource ? conn.targetNode : conn.sourceNode)) return (
{isSource ? conn.sourceField : conn.targetField} {otherNode?.name} {isSource ? conn.targetField : conn.sourceField}
) })}
)}
) }