react19设计AntVX6 人工智能建模 DAG 图

HomeTop.tsx

import React, { useState, useEffect, useRef } from 'react'
import useStore from '../../../store/state'
import { Graph, Path } from '@antv/x6'
import { History } from '@antv/x6-plugin-history'
import AlgoNode from '../../AntVX6/AlgoNode'
import { register } from '@antv/x6-react-shape'
import { Selection } from '@antv/x6-plugin-selection'
import { Snapline } from '@antv/x6-plugin-snapline'
import styles from './HomeTop.module.scss'
import { Space, Button } from 'antd'
import { IoExpandOutline } from 'react-icons/io5'
import { CiSaveDown1 } from 'react-icons/ci'
import { LuFileTerminal } from 'react-icons/lu'
import { SiStreamrunners } from 'react-icons/si'
import { TbArrowBackUp, TbArrowForwardUp } from 'react-icons/tb'
import { FaSearchPlus, FaSearchMinus } from 'react-icons/fa'
import { AiOutlineFullscreenExit } from 'react-icons/ai'
register({
  shape: 'dag-node',
  width: 180,
  height: 36,
  component: AlgoNode,
  ports: {
    groups: {
      left: {
        position: 'left',
        attrs: {
          circle: {
            r: 4,
            magnet: true,
            stroke: '#C2C8D5',
            strokeWidth: 1,
            fill: '#fff',
          },
        },
      },
      right: {
        position: 'right',
        attrs: {
          circle: {
            r: 4,
            magnet: true,
            stroke: '#C2C8D5',
            strokeWidth: 1,
            fill: '#fff',
          },
        },
      },
    },
  },
})
// 注册自定义边样式
Graph.registerEdge(
  'dag-edge',
  {
    inherit: 'edge',
    attrs: {
      line: {
        stroke: '#3498DB',
        strokeWidth: 3,
        targetMarker: {
          name: 'block', // 箭头类型,可以是 block、classic、circle 等
          width: 25, // 箭头宽度
          height: 15, // 箭头高度
          fill: '#3498DB', // 箭头颜色
        },
      },
    },
  },
  true
)
//注册连接器的样式
Graph.registerConnector(
  'algo-connector',
  (sourcePoint, targetPoint) => {
    const hgap = Math.abs(targetPoint.x - sourcePoint.x)
    const path = new Path()
    path.appendSegment(Path.createSegment('M', sourcePoint.x - 4, sourcePoint.y))
    path.appendSegment(Path.createSegment('L', sourcePoint.x + 12, sourcePoint.y))
    // 水平三阶贝塞尔曲线
    path.appendSegment(
      Path.createSegment(
        'C',
        sourcePoint.x < targetPoint.x ? sourcePoint.x + hgap / 2 : sourcePoint.x - hgap / 2,
        sourcePoint.y,
        sourcePoint.x < targetPoint.x ? targetPoint.x - hgap / 2 : targetPoint.x + hgap / 2,
        targetPoint.y,
        targetPoint.x - 6,
        targetPoint.y
      )
    )
    path.appendSegment(Path.createSegment('L', targetPoint.x + 2, targetPoint.y))
    return path.serialize()
  },
  true
)
const HomeTop: React.FC = () => {
  const graph = useRef(null) // 使用 useRef 保存 graph 引用
  useEffect(() => {
    //为什么要放置在内部因为=> 
{ const container = document.getElementById('antVX6Container') if (!container) return // 确保容器存在 const ports = container.querySelectorAll('.x6-port-body') const texts = container.querySelectorAll('.x6-port-label') for (let i = 0; i < texts.length; i++) { ;(texts[i] as HTMLElement).style.visibility = visible ? 'visible' : 'hidden' } for (let i = 0, len = ports.length; i < len; i++) { ;(ports[i] as HTMLElement).style.visibility = visible ? 'visible' : 'hidden' } } // 监听节点的鼠标进入事件,显示连接桩 graph.current.on('node:mouseenter', ({ node }) => { changePortsVisible(true) node.addTools({ name: 'button-remove', args: { x: '100%', y: 0, offset: { x: -10, y: 10 }, }, }) }) // 监听节点的鼠标离开事件,隐藏连接桩 graph.current.on('node:mouseleave', ({ node }) => { changePortsVisible(false) node.removeTools() }) // 监听节点数据变化事件 graph.current.on('node:change:data', ({ node }) => { const edges = graph.current!.getIncomingEdges(node) // 获取入边 const { status } = node.getData() as { status: string } // 获取节点状态 edges?.forEach(edge => { if (status === 'running') { edge.attr('line/strokeDasharray', 5) // 设置虚线 edge.attr('line/style/animation', 'running-line 30s infinite linear') // 添加动画 } else { edge.attr('line/strokeDasharray', '') // 清除虚线 edge.attr('line/style/animation', '') // 移除动画 } }) }) }, []) const { algorihtm } = useStore() const [dragOver, setDragOver] = useState(false) // 判断是否正在拖拽 // 拖拽区域的样式 const style = { border: dragOver ? '2px dashed #000' : '2px solid transparent', } // 处理拖拽开始 const handleDragOver = (e: React.DragEvent) => { e.preventDefault() // 必须阻止默认行为才能触发 drop 事件 setDragOver(true) } // 处理拖拽结束 const handleDragLeave = () => { setDragOver(false) } // 处理放置操作 const handleDrop = (e: React.DragEvent) => { e.preventDefault() setDragOver(false) console.log('HomeTop.tsx-handleDrop:算子信息' + algorihtm) // 获取鼠标在画布中的坐标 const { x, y } = graph.current!.pageToLocal(e.pageX, e.pageY) // 设置节点的宽高 const nodeWidth = 180 const nodeHeight = 36 // 调整坐标,使节点的中心在鼠标放置的位置 const adjustedX = x - nodeWidth / 2 const adjustedY = y - nodeHeight / 2 graph.current!.addNode({ id: String(algorihtm.key), shape: 'dag-node', data: { label: algorihtm.title, status: 'default' }, x: adjustedX, y: adjustedY, ports: { items: [ { id: 'port_1', group: 'left', }, { id: 'port_2', group: 'right', }, ], }, }) } function runCode() { // 获取所有节点和边 const nodes = graph.current!.getNodes() // 获取画布中所有的节点 const edges = graph.current!.getEdges() // 获取画布中所有的连线 // 构建图的邻接表、反向邻接表和入出度表 const adjList: Record = {} // 邻接表,用于存储每个节点指向的节点 const reverseAdjList: Record = {} // 反向邻接表,用于存储指向该节点的节点 const indegree: Record = {} // 入度表,记录每个节点的被连接次数 const outdegree: Record = {} // 出度表,记录每个节点指向其他节点的次数 // 初始化邻接表和度表 nodes.forEach(node => { const nodeId = node.id // 节点的唯一标识符 adjList[nodeId] = [] // 初始化为空数组,表示暂时没有指向任何节点 reverseAdjList[nodeId] = [] // 初始化为空数组,表示暂时没有被其他节点指向 indegree[nodeId] = 0 // 初始入度为 0 outdegree[nodeId] = 0 // 初始出度为 0 }) // 填充邻接表和度表 edges.forEach(edge => { const source = (edge.getSource() as { cell: string }).cell // 获取边的起点节点 ID const target = (edge.getTarget() as { cell: string }).cell // 获取边的终点节点 ID if (adjList[source] && adjList[target]) { // 确保 source 和 target 都在节点列表中 adjList[source].push(target) // 起点的邻接表增加终点 reverseAdjList[target].push(source) // 终点的反向邻接表增加起点 indegree[target]++ // 终点的入度加 1 outdegree[source]++ // 起点的出度加 1 } }) // 拓扑排序逻辑 const queue: string[] = [] // 队列,用于存储入度为 0 的节点 for (const nodeId in indegree) { if (indegree[nodeId] === 0) { // 找出所有入度为 0 的节点 queue.push(nodeId) // 加入队列 } } const topoOrder: string[] = [] // 用于存储拓扑排序的结果 while (queue.length > 0) { const nodeId = queue.shift()! // 从队列中取出一个节点 topoOrder.push(nodeId) // 将节点加入拓扑排序结果 adjList[nodeId].forEach(neighbor => { // 遍历该节点的所有邻居节点 indegree[neighbor]-- // 邻居节点的入度减 1 if (indegree[neighbor] === 0) { // 如果邻居节点的入度变为 0 queue.push(neighbor) // 加入队列 } }) } // 检查是否有环 if (topoOrder.length !== nodes.length) { // 如果拓扑排序结果的节点数与总节点数不一致,说明有环\ console.log('错误连接,出现环,请查看连接情况并修正!') return // 中断函数 } // 检查未连接节点 const allNodes = new Set(nodes.map(node => node.id)) // 获取所有节点的 ID 集合 const reachableFromStart = new Set() // 用于存储从起点可达的节点 const reachableFromEnd = new Set() // 用于存储从终点反向可达的节点 // 深度优先搜索(DFS)函数 const dfs = (start: string, visited: Set, graph: Record) => { if (visited.has(start)) return // 如果节点已经访问过,直接返回 visited.add(start) // 标记当前节点为已访问 graph[start].forEach(neighbor => dfs(neighbor, visited, graph)) // 遍历当前节点的所有邻居 } // 从所有起点出发,检查哪些节点可达 topoOrder.forEach(node => { dfs(node, reachableFromStart, adjList) // 正向 DFS 检查从起点可达的节点 dfs(node, reachableFromEnd, reverseAdjList) // 反向 DFS 检查从终点反向可达的节点 }) // 找到未连接的节点 const unconnectedNodes = Array.from(allNodes).filter( node => !reachableFromStart.has(node) && !reachableFromEnd.has(node) ) if (unconnectedNodes.length > 0) { // 如果有未连接的节点 console.log('运行错误,存在未连接的算子: ${unconnectedNodes.join(', ')}`') return // 中断函数 } // 导出拓扑排序结果 console.log('Topological Order:', topoOrder) // 导出连接关系并按拓扑顺序输出 const orderedConnections: Record = {} topoOrder.forEach(nodeId => { const outgoingNodes = adjList[nodeId] if (outgoingNodes.length > 0) { orderedConnections[nodeId] = outgoingNodes } }) // 导出节点数据 const nodesData: Record< string, { label: string; status: string; position: { x: number; y: number } } > = {} nodes.forEach(node => { const data = node.getData() // 获取节点的数据 nodesData[node.id] = { label: data.label, // 节点的标签 status: data.status, // 节点的状态 position: node.getPosition(), // 节点的位置 } }) // 输出最终结果 const result = { topoOrder, // 拓扑排序结果 orderedConnections, // 按拓扑顺序排列的连接关系 nodesData, // 节点数据 } console.log('Result:', JSON.stringify(result, null, 2)) // 打印结果 // 动态运行拓扑 simulateExecution(topoOrder) } // 延时函数 const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) async function simulateExecution(topoOrder: string[]) { for (let i = 0; i < topoOrder.length; i++) { const nodeId = topoOrder[i] const node = graph.current!.getCellById(nodeId) // 将当前节点设置为运行中状态 node.setData({ ...node.getData(), status: 'running', }) await delay(2000) // 模拟运行时间 // 根据模拟逻辑设置状态 const isSuccess = Math.random() > 0.1 // 80% 成功概率 node.setData({ ...node.getData(), status: isSuccess ? 'success' : 'failed', }) // 如果失败,终止后续执行 if (!isSuccess) { console.log(`运行失败,节点 ${nodeId} 执行错误!`) break } } console.log(`运行完成!`) } return (
graph.current!.zoomToFit({ maxScale: 2 })} > 自适应放大
{ graph.current!.toJSON() console.log(graph.current!.toJSON()) }} > 保存
{ graph.current!.toJSON() console.log(graph.current!.toJSON({})) }} > 导出分析流
graph.current!.zoom(0.2)} > graph.current!.zoom(-0.2)} > graph.current!.zoomToFit({ maxScale: 2 })} >
) } export default HomeTop

HomeTop.module.scss 

.topBar{
    display: flex;
    align-items: center;
    position: absolute;
    z-index: 1;
    background-color: white;  // 改为淡灰色
    height: 40px;
    border: 1px solid #ccc;    // 添加边框
    width: calc(100% - 280px);  // 宽度减小
}

.controlItem {
  display: flex;
  align-items: center;
  margin-right: 20px;
  padding: 8px 12px; // 添加内边距
  cursor: pointer;
  background-color: #fff; // 背景颜色
  transition: background-color 0.3s, box-shadow 0.3s; // 添加过渡效果
  border-radius: 8px; // 圆角
}
.controlItem:hover {
  background-color: #f0f0f0;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); // 悬停时增加阴影
}

.controlItem span {
  font-size: 16px;
  color: #333;
  font-weight: bold;
}
.controlItem i {
  display: flex;
  align-items: center;
  font-size: 21px;
  color: #333;
}


.topButton{
  position: absolute;
  z-index: 1;
  top: 110px;
}
.topButtonRun{
  border-radius: 16px;
  left: 20px;
  width: 60px !important;
  height: 60px;
  background-color: #0fdfb5 /* 设置背景为绿色 */;
  color: white /* 设置图标颜色为白色 */;
  font-size: 25px;
}

.topButtonCancel{
  border-radius: 16px;
  left: 20px;
  width: 60px !important;
  height: 60px;
  color: black /* 设置图标颜色为白色 */;
  font-size: 25px;  
}
.sideButton {
  position: absolute;
  z-index: 1;
  background-color: white;
  width: 50px;
  top: 180px;
  left: 305px;
  display: flex;
  justify-content: center;
  align-items: center;
  border-radius: 12px;
  cursor: pointer;  /* 让鼠标变为点击手势 */
  box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);  /* 初始阴影效果 */
  transition: all 0.3s ease;  /* 平滑过渡效果 */
}

/* 悬浮时的效果 */
.sideButton:hover {
  transform: scale(1.1);  /* 增大按钮尺寸 */
  box-shadow: 0px 6px 12px rgba(0, 0, 0, 0.2);  /* 增强阴影效果 */
}

/* 按下按钮时的效果 */
.sideButton:active {
  transform: scale(1);  /* 返回原本大小 */
  box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.1);  /* 按下时的阴影效果 */
}

/* 按钮图标样式 */
.sideIcon {
  margin-top: 15px;
  font-size: 20px;
  color: #333;
  font-weight: bold;
  transition: color 0.2s ease;  /* 图标颜色的过渡效果 */
}

/* 悬浮时图标颜色变化 */
.sideIcon:hover {
  color: #007bff;  /* 改变颜色为蓝色 */
}

 

AlgoNode.tsx

import './AlgoNode.css'
import { Graph, Node } from '@antv/x6'
import logo from '../../assets/antVX6NodeIcon/logo.png'
import running from '../../assets/antVX6NodeIcon/running.png'
import success from '../../assets/antVX6NodeIcon/success.png'
import failed from '../../assets/antVX6NodeIcon/failed.png'

interface NodeStatus {
  id: string
  label?: string
  status: 'default' | 'success' | 'failed' | 'running'
}
interface propsType {
  node: Node
  graph?: Graph
}
const image = {
  logo: logo,
  success: success,
  failed: failed,
  running: running,
}

const AlgoNode = (props: propsType) => {
  const { node } = props
  const data = node?.getData() as NodeStatus
  const { label, status = 'default' } = data

  return (
    
logo {label} {status === 'success' && success} {status === 'failed' && failed} {status === 'running' && running}
) } export default AlgoNode

AlgoNode.css

.node {
  display: flex;
  align-items: center;
  width: 100%;
  height: 100%;
  background-color: #fff;
  border: 1px solid #c2c8d5;
  border-left: 4px solid #5F95FF;
  border-radius: 4px;
  box-shadow: 0 2px 5px 1px rgba(0, 0, 0, 0.06);
}
.node img {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  margin-left: 8px;
}
.node .label {
  display: inline-block;
  flex-shrink: 0;
  width: 104px;
  margin-left: 8px;
  color: #666;
  font-size: 12px;
}
.node .status {
  flex-shrink: 0;
}
.node.success {
  border-left: 4px solid #52c41a;
}
.node.failed {
  border-left: 4px solid #ff4d4f;
}
.node.running .status img {
  animation: spin 1s linear infinite;
}
.x6-node-selected .node {
  border-color: #1890ff;
  border-radius: 2px;
  box-shadow: 0 0 0 4px #d4e8fe;
}
.x6-node-selected .node.success {
  border-color: #52c41a;
  border-radius: 2px;
  box-shadow: 0 0 0 4px #ccecc0;
}
.x6-node-selected .node.failed {
  border-color: #ff4d4f;
  border-radius: 2px;
  box-shadow: 0 0 0 4px #fedcdc;
}
.x6-edge:hover path:nth-child(2){
  stroke: #1890ff;
  stroke-width: 1px;
}

.x6-edge-selected path:nth-child(2){
  stroke: #1890ff;
  stroke-width: 1.5px !important;
}

@keyframes running-line {
  to {
    stroke-dashoffset: -1000;
  }
}
@keyframes spin {
  from {
      transform: rotate(0deg);
  }
  to {
      transform: rotate(360deg);
  }
}

 

你可能感兴趣的:(AntV,X6,javascript,前端,开发语言)