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({}))
}}
>
导出分析流
}
className={styles['topButtonRun']}
onClick={runCode}
>
}
className={styles['topButtonCancel']}
onClick={() => {
graph.current!.undo()
graph.current!.undo()
}}
>
}
className={styles['topButtonCancel']}
onClick={() => graph.current!.redo()}
>
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 (
{label}
{status === 'success' &&
}
{status === 'failed' &&
}
{status === '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);
}
}