antv x6 + vue3 (二)

<template>
    <div class="w-full h-full relative" v-loading="loading">
        <!-- 画布外层容器(自适应) -->
        <div class="absolute top-0 left-0 right-[460px] bottom-0" ref="leftRef">
            <div ref="container" class="w-full h-full" style="background:#f5f5f5;"></div>            
            <!-- 画布 overlay:放区域名称 -->
            <div ref="areaLabels" class="absolute top-0 left-0 pointer-events-none w-full h-full"></div>

            <!-- 右上角 MiniMap -->
            <div id="minimap" class="absolute top-2 right-2" style="
                width:100px;
                height:150px;
                z-index:10;
                border-radius:12px;
                background: rgba(255,255,255,0.2);
                backdrop-filter: blur(6px);
                box-shadow: 0 4px 12px rgba(0,0,0,0.15);
                overflow:hidden;
            "></div>

            <div class="absolute top-[178px] right-[0px]"
                style="height:150px; z-index: 11; display:flex; align-items:center;">
                <el-slider v-model="zoomLevel" :min="1" :max="5" :step="0.01" @input="onZoomChange" size="small"
                    vertical />
            </div>
            <!-- 重新渲染按钮,固定在画布左下角 -->
            <el-button class="absolute left-4 bottom-4 z-20" type="primary" circle @click="refreshGraph">
                <el-icon>
                    <Refresh />
                </el-icon>
            </el-button>
        </div>

        <!-- 右侧面板 -->
        <div class="absolute top-0 right-0 bottom-0 w-[460px] bg-pink border-l border-gray-300">
            <ServiceMapRight :mapJson="mapJson" :nowNode="nowNodeData" ref="rightRef" />
        </div>
    </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { Graph, Path } from '@antv/x6'
import { MiniMap } from '@antv/x6-plugin-minimap'
import ServicesMap from '@/generated/com/appdcn/ui/common/api/events/ServicesMap'
import { useRoute, useRouter } from 'vue-router'
import ServiceMapRight from '@/views/serviceMap/components/service_map_right.vue'
import { DagreLayout } from '@antv/layout'
import {
    Refresh,
} from '@element-plus/icons-vue'
import { Console } from 'console'


const container = ref<HTMLDivElement | null>(null)
const leftRef = ref<HTMLDivElement | null>(null)
let graph: any
let lastSize = { w: 0, h: 0 }
interface Area {
    id: string
    x: number
    y: number
    width: number
    height: number
}

interface TopologyGroup {
    label: string
    value: {
        id: any
        rowId: any
        name: string | null
        baselineReasonText: any
        nodes: any[]
        edges: any[]
        properties: any
    }
}

interface FlattenedGroup {
    label: string
    value: {
        id: any
        rowId: any
        name: string | null
        baselineReasonText: any
        nodes: any[]
        edges: any[]
        properties: any
    }
}

const loading = ref(false)
const mapJson = ref({
    bottom_nodes: [],
    right_outgoing: [],
    top_nodes: [],
    left_incoming: [],
    targetNode: null,
})
const rightRef = ref<HTMLDivElement | any>(null)
const route = useRoute()
const router = useRouter()

const areaLabels = ref<HTMLDivElement | null>(null)

const zoomLevel = ref(1)
const nowNodeData: any = ref(null)
let roLeft: ResizeObserver | null = null
let areas: Record<string, Area> = {
    'User Experience': { id: 'User Experience', x: 0, y: 50, width: 600, height: 150 },
    'Services': { id: 'Services', x: 0, y: 250, width: 600, height: 150 },
    'Nodes': { id: 'Nodes', x: 0, y: 450, width: 600, height: 150 },
    'Infrastructure': { id: 'Infrastructure', x: 0, y: 650, width: 600, height: 150 },
}
let areasOrigin: Record<string, Area> = JSON.parse(JSON.stringify(areas))
const nodeMap: Record<string, any> = {}

let flattened = ref<FlattenedGroup[]>([])


Graph.registerEdge(
    'dag-edge',
    {
        inherit: 'edge',
        attrs: {
            line: {
                stroke: '#C2C8D5',
                strokeWidth: 1,
                targetMarker: null,
            },
        },
    },
    true,
)


Graph.registerConnector(
    'algo-offset-connector',
    (source, target) => {
        const deltaY = target.y - source.y
        const deltaX = target.x - source.x
        const control = Math.abs(deltaY) / 2

        // 多条边偏移(这里假设传了 edgeIndex 和 totalEdges)
        // 如果没有,可在 addEdge 时通过 edge.data 存储 offset
        const offset = target?.data?.offset ?? 0

        const v1 = { x: source.x, y: source.y + control + offset }
        const v2 = { x: target.x, y: target.y - control + offset }

        return Path.normalize(
            `M ${source.x} ${source.y}
       C ${v1.x} ${v1.y} ${v2.x} ${v2.y} ${target.x} ${target.y}`
        )
    },
    true
)


function renderAreaLabels() {
  if (!areaLabels.value || !graph) return
  areaLabels.value.innerHTML = ''

  let prevBottom = 0 // 用于防止重叠

  Object.values(areas).forEach(area => {
    // 获取区域左上角在画布中的实际显示位置
    const point = graph.localToClient({ x: area.x, y: area.y })
    let labelY = Math.max(0, point.y) - 130  // 向上偏移
    if (labelY < prevBottom) labelY = prevBottom

    const div = document.createElement('div')
    div.textContent = area.id
    div.style.position = 'absolute'
    div.style.left = `0px` // X固定
    div.style.top = `${labelY}px`
    div.style.width = `${area.width}px`
    div.style.fontSize = '14px'
    div.style.fontWeight = 'bold'
    div.style.color = '#333'
    div.style.pointerEvents = 'none'
    div.style.background = 'rgba(255,255,255,0.08)'
    div.style.padding = '2px 4px'
    div.style.borderRadius = '4px'

    areaLabels.value!.appendChild(div)

    prevBottom = labelY + div.offsetHeight
  })
}



onMounted(() => {
    getServiceMapData()
    renderAreaLabels() 
})


function updateAreasHeightByFlattened(
    areasOrigin: Record<string, Area>,
    flattened: FlattenedGroup[]
) {
    const newAreas: Record<string, Area> = {}

    // 1. 先计算每个区域新的 height
    Object.keys(areasOrigin).forEach(key => {
        const area = areasOrigin[key]

        // 找对应分类
        const group = flattened.find(f => f.label === area.id)

        const dynamicHeight =
            group ? Math.max(150, group.value.nodes.length * 60) : area.height

        newAreas[key] = {
            ...area,
            height: dynamicHeight // 仅更新高度
        }
    })

    // 2. 再统一计算 y(根据顺序累加)
    const order = [
        'User Experience',
        'Services',
        'Nodes',
        'Infrastructure'
    ]

    let currentY = 50  // 第一个区域的起始 y
    const gap = 0     // 区域之间的间距

    order.forEach(key => {
        const area = newAreas[key]
        newAreas[key] = {
            ...area,
            y: currentY
        }
        currentY += area.height + gap
    })

    return newAreas
}


function flattenToFourCategories(data: TopologyGroup[]): FlattenedGroup[] {
    // 初始化四类
    const categories: Record<string, FlattenedGroup> = {
        'User Experience': { label: 'User Experience', value: { id: null, rowId: null, name: null, baselineReasonText: null, nodes: [], edges: [], properties: null } },
        'Services': { label: 'Services', value: { id: null, rowId: null, name: null, baselineReasonText: null, nodes: [], edges: [], properties: null } },
        'Nodes': { label: 'Nodes', value: { id: null, rowId: null, name: null, baselineReasonText: null, nodes: [], edges: [], properties: null } },
        'Infrastructure': { label: "Infrastructure", value: { id: null, rowId: null, name: null, baselineReasonText: null, nodes: [], edges: [], properties: null } },
    }

    data.forEach(group => {
        const key = group.label in categories ? group.label : null
        if (!key) return // 不在四类中,忽略
        const cat = categories[key]

        // 如果 value.name 为空,用 group.value.name 否则保留原名
        if (group.value.name) {
            cat.value.name = group.value.name
        }

        // 合并节点
        group.value.nodes.forEach(node => {
            // 避免重复节点
            if (!cat.value.nodes.find(n => n.id === node.id)) {
                cat.value.nodes.push(node)
            }
        })

        // 合并边
        group.value.edges.forEach(edge => {
            // 避免重复边(可根据 source+target判定)
            if (!cat.value.edges.find(e => e.sourceNode === edge.sourceNode && e.targetNode === edge.targetNode)) {
                cat.value.edges.push(edge)
            }
        })
    })

    // 返回四类数组
    return Object.values(categories)
}

function getServiceMapData() {
    loading.value = true
    let timeRange = route.query.timeRange as string
    let appId = Number(route.query.application) as number
    let mapId = Number(route.query.mapId ?? -1) as number
    let baselineId = Number(route.query.baselineId ?? -1) as number

    const serviceMap = ServicesMap.instance()
    serviceMap.getFlowMapData(appId, timeRange, mapId, baselineId)
        .then(async (res: any) => {
            console.log(res, "------------------------当前的service_map数据------------------------")
            flattened.value = flattenToFourCategories(res)
            areas = updateAreasHeightByFlattened(areasOrigin,  flattened.value)
            console.log(areas, "------------------------更新后的areas------------------------")
            await nextTick()
            renderGraph(flattened.value)
        
            setupObserver()
            setupWindowResize()
            // applyForceLayout()
            console.log(flattened, "------------------------flattened------------------------")
            loading.value = false
        })
        .catch((error: any) => {
            console.log(error)
            loading.value = false
        })
}


function getColor(stats: any) {
    if (!stats) return '#1890ff' // 默认灰色

    const nodesCount: Record<string, number> = {
        criticalNodes: stats.criticalNodes || 0,
        warningNodes: stats.warningNodes || 0,
        unknownNodes: stats.unknownNodes || 0,
        normalNodes: stats.normalNodes || 0,
    }

    const maxValue = Math.max(...Object.values(nodesCount))
    if (maxValue > 0) {
        // 找出最大值对应的类别
        const maxKeys = Object.keys(nodesCount).filter(k => nodesCount[k] === maxValue)
        const key = maxKeys[0]
        const colorMap: Record<string, string> = {
            criticalNodes: '#f44336',  // 红
            warningNodes: '#ff9800',   // 橙
            unknownNodes: '#ffeb3b',   // 黄
            normalNodes: '#4caf50',    // 绿
        }
        return colorMap[key] || '#1890ff'
    }

    // 如果没有节点数量,则根据 healthy 判断
    if (stats.healthy === false) return '#f44336' // 红色
    if (stats.healthy === true) return '#4caf50'  // 绿色

    return '#1890ff' // 默认灰色
}

// 添加每个区域内部的节点(flattened 数据)
function renderFlattened(flattened: FlattenedGroup[]) {
    flattened.forEach(group => {
        const area = areas[group.label]
        if (!area) return
        const nodes = group.value.nodes
        nodes.forEach((node, i) => {
            const nodeId = `node-${node.id}`
            if (!graph.getCellById(nodeId)) {
                const strokeColor = getColor(node.componentHealthStats) // 根据健康状态获取颜色
                const nodea = graph.addNode({
                    id: nodeId,
                    x: area.x + 50 + (i % 5) * 150, // 简单网格布局
                    y: area.y + 50 + Math.floor(i / 5) * 60,
                    width: 40,
                    height: 40,
                    shape: 'polygon',
                    attrs: {
                        body: {
                            refPoints: '0,10 10,0 30,0 40,10 30,20 10,20', // 六边形路径
                            fill: '#fff',
                            stroke: strokeColor,
                            strokeWidth: 3,
                        },
                        label: {
                                text: node.name || ' ',
                                fontSize: 12,
                                fill: '#000',
                                refX: 0.5,
                                refY: 1,
                                textAnchor: 'middle',
                                textVerticalAnchor: 'bottom',
                                y: 2,
                                pointerEvents: 'none',
                                textWrap: {
                                width: 120,        // 最大宽度 px
                                height: 20,       // 最大高度 px
                                ellipsis: true,   // 超出显示省略号
                                },
                        },
                    },
                    data: { area: group.label, isArea: false, raw: node },
                    zIndex: 1,
                })
                nodeMap[node.id] = nodea
            }
        })
        // 添加边
        group.value.edges.forEach(edge => {
            const s = nodeMap[edge.sourceNode]
            const t = nodeMap[edge.targetNode]
            if (!s || !t) return

            const sourceId = `node-${edge.sourceNode}`
            const targetId = `node-${edge.targetNode}`

            if (!graph.getCellById(sourceId) || !graph.getCellById(targetId)) return

            if (!graph.getEdges().find(e => e.getSourceCellId() === sourceId && e.getTargetCellId() === targetId)) {
                const strokeColor = getColor(s?.data?.componentHealthStats || t?.data?.componentHealthStats)

                graph.addEdge({
                    shape: 'dag-edge', // 默认 edge 可以加 ant-line 样式 
                    connector: { name: 'algo-offset-connector' },
                    source: { cell: sourceId },
                    target: { cell: targetId },
                    attrs: {
                        line: {
                            stroke: strokeColor, strokeWidth: 1,
                            targetMarker: { name: 'classic', width: 8, height: 8, offset: 0 },
                            strokeDasharray: '5 5',
                            class: 'ant-line', // 动态虚线关键
                        },
                    },
                    zIndex: 0,
                })
            }
        })
    })
}


function renderGraph(flattened:any) {
    const c = container.value
    const l = leftRef.value
    if (!c || !l) return
    const rect = l.getBoundingClientRect()
    const w = rect.width 
    const h = rect.height
    lastSize = { w, h }

    if (!graph) {
        graph = new Graph({
            container: container.value!,
            width: w,
            height: h,
            grid: false,
            panning: true,
            background: { color: '#ffffff' },
            interacting: (cellView) => {
                const cell = cellView.cell
                return {
                    nodeMovable: !cell.data?.isArea, // 区域不可拖,其他可拖
                }
            },
            mousewheel: {
                enabled: true,
                zoomAtMousePosition: true, 
                minScale: 1,
                maxScale: 5,
            },
        })

        graph.use(
            new MiniMap({
                container: document.getElementById('minimap')!,
                width: 100,
                height: 150,
                padding: 10,
                graphOptions: {
                    async: true,
                    background: { color: 'transparent' },
                    grid: false,
                },
                viewportStyle: {
                    stroke: '#1890ff',
                    strokeWidth: 1,
                    fill: 'rgba(24, 144, 255, 0.08)',
                },
            }),
        )
    } else {
        graph.resize(w, h)
        return
    }

    // 添加区域节点(不可拖动)
    Object.values(areas).forEach((area, index) => {
            const hasTopLine = index === 0; // 第一个区域有上边框
            const node = graph?.addNode({
                id: `area-${area.id}`,
                shape: 'rect',
                x: area.x,
                y: area.y,
                width: area.width,
                height: area.height,
                attrs: {
                    body: { fill: 'none', stroke: 'none' }, // 去掉背景和边框
                    label: { text: `${area.id}`, fill: '#333', fontSize: 14 },
                    topLine: hasTopLine
                        ? {
                            stroke: '#999',
                            strokeWidth: 1,
                            fill: 'none',
                            d: `M-100000,0 L100000,0`, // 上边框路径
                        }
                        : undefined,
                    bottomLine: {
                        stroke: '#999',
                        strokeWidth: 1,
                        fill: 'none',
                        d: `M-100000,${area.height} L100000,${area.height}`, // 下边框路径
                    },
                },
                markup: [
                    { tagName: 'rect', selector: 'body' },
                    ...(hasTopLine ? [{ tagName: 'path', selector: 'topLine' }] : []),
                    { tagName: 'path', selector: 'bottomLine' },
                    { tagName: 'text', selector: 'label' },
                ],
                data: { isArea: true, areaId: area.id },
                zIndex: 0,
            })
        })




    graph?.on('node:change:size', ({ node, options }) => {
        console.log(node, options, "--------------------node:change:size")
        if (options.skipParentHandler) {
            return
        }

        if (node.data?.isArea) {
            const bbox = node.getBBox()
            node.attr('bottomLine/d', `M-100000,${bbox.height} L100000,${bbox.height}`)
        }
    })
    // 阻止区域节点拖动
    graph?.on('node:dragstart', ({ node, e }) => {
        console.log(node, "===============node:dragstart")
        if (node.data?.isArea) {
            e.stopPropagation() // 阻止默认事件
            node.stopAnimate()  // 停止拖动
        }
    })

    // 内部节点拖动逻辑
    graph?.on('node:mousemove', ({ node }) => {
        console.log(node, node.data.isArea, "===============node:mousemove")
        if (!node.data?.area || node.data.isArea) {
            return // 跳过区域节点
        }
        const areaKey = node.data.area
        const area = areas[areaKey]
        const box = node.getBBox()
        let { x, y } = node.getPosition()

        // =============== 左右撑 ===============
        const rightOver = x + box.width + 10 - (area.x + area.width)
        if (rightOver > 0) {
            area.width += rightOver
            graph?.getCellById(`area-${areaKey}`)?.resize(area.width, area.height)
        }

        const leftOver = area.x + 10 - x
        if (leftOver > 0) {
            area.x -= leftOver
            area.width += leftOver
            const areaCell = graph?.getCellById(`area-${areaKey}`)
            areaCell?.position(area.x, area.y)
            areaCell?.resize(area.width, area.height)
            x += leftOver
        }

        // 最终锁定
        x = Math.max(area.x + 10, Math.min(x, area.x + area.width - box.width - 10))

        // ---------------- 往下顶 ----------------
        const bottomOver = y + box.height + 10 - (area.y + area.height)
        if (bottomOver > 0) {
            area.height += bottomOver
            graph?.getCellById(`area-${areaKey}`)?.resize(area.width, area.height)
            pushDownAreas(areaKey, bottomOver)
            renderAreaLabels() // 更新 label
        }

        // ---------------- 往上顶 ----------------
        const topOver = area.y + 10 - y
        if (topOver > 0) {
            area.y -= topOver
            area.height += topOver
            graph?.getCellById(`area-${areaKey}`)?.position(area.x, area.y)
            graph?.getCellById(`area-${areaKey}`)?.resize(area.width, area.height)
            pushUpAreas(areaKey, topOver)
            renderAreaLabels() // 更新 label
        }

        // 锁定在区域内
        y = Math.max(area.y + 10, Math.min(y, area.y + area.height - box.height - 10))
        node.setPosition(x, y)
    })

    // 点击事件
    graph.on('node:click', ({ node, e }) => {
        e.stopPropagation()
        // 如果是群组
        if (node.data?.isGroup) {
            console.log('点击了群组:', node)
            return
        }
        rightRef.value?.refreshMap(true)

        nowNodeData.value = node.data.raw
        console.log(node.data, "----------------当前点击的node节点")
        const groupNode: any = node.data.raw
        let timeRange = route.query.timeRange as string
        let appId = Number(route.query.application) as number
        let mapId = Number(route.query.mapId ?? -1) as number
        let baselineId = Number(route.query.baselineId ?? -1) as number

        ServicesMap.instance().getNeighbors(
            appId,
            timeRange,
            mapId,
            baselineId,
            groupNode?.idNum,
            node.data?.area
        ).then((res: any) => {
            rightRef.value?.refreshMap(false)
            console.log('获取到的邻居节点数据:', res)
            mapJson.value = {
                bottom_nodes: res.bottom_nodes || [],
                right_outgoing: res.right_outgoing || [],
                top_nodes: res.top_nodes || [],
                left_incoming: res.left_incoming || [],
                targetNode: res.targetNode || null,
            }
            // 这里可以处理邻居节点数据,比如高亮显示或弹出详情
        }).catch((error: any) => {
            rightRef.value?.refreshMap(false)
            console.error('获取邻居节点失败:', error)
        })
        // 普通节点
        console.log('点击了节点:', node)
        console.log('节点数据:', node.getData())
    })

    graph?.on('scale', ({ sx }) => {
        console.log("===============scale ")
        zoomLevel.value = Number(sx.toFixed(2))
        renderAreaLabels()
    })

    graph.on('translate', () => {
        console.log("===============translate ")
        renderAreaLabels()
    })

    renderFlattened(flattened)
    graph.centerContent()
}

// 下方区域移动
function pushDownAreas(areaKey: string, delta: number) {
    console.log(areaKey, delta, "===============pushDownAreas")
    const keys = Object.keys(areas)
    const idx = keys.indexOf(areaKey)
    for (let i = idx + 1; i < keys.length; i++) {
        const key = keys[i]
        areas[key].y += delta
        graph?.getCellById(`area-${key}`)?.position(areas[key].x, areas[key].y)
        // 区域内节点同步移动
        graph?.getNodes().forEach(node => {
            if (node.data.area === key) {
                const pos = node.getPosition()
                node.setPosition(pos.x, pos.y + delta)
            }
        })
    }
}

// 上方区域移动
function pushUpAreas(areaKey: string, delta: number) {
    console.log(areaKey, delta, "===============pushUpAreas")
    const keys = Object.keys(areas)
    const idx = keys.indexOf(areaKey)
    if (idx > 0) {
        for (let i = idx - 1; i >= 0; i--) {
            const key = keys[i]
            areas[key].y -= delta
            graph?.getCellById(`area-${key}`)?.position(areas[key].x, areas[key].y)
            // 上方区域的节点也跟随
            graph?.getNodes().forEach(node => {
                if (node.data.area === key) {
                    const pos = node.getPosition()
                    node.setPosition(pos.x, pos.y - delta)
                }
            })
        }
    }
}
function onZoomChange(val: number) {
    if (graph) {
        graph?.zoomTo(val)
        graph?.centerContent()
    }
}

async function refreshGraph() {
    loading.value = true
    mapJson.value = {
        bottom_nodes: [],
        right_outgoing: [],
        top_nodes: [],
        left_incoming: [],
        targetNode: null,
    }
    nowNodeData.value = null

    if (graph) {
        destroy()
        graph.dispose()
        graph = null
    }
    try {
        await getServiceMapData()  // 确保接口返回后再渲染
    } catch (e) {
        console.error(e)
        loading.value = false
    }
}

function setupObserver() {
    const l = leftRef.value
    if (!l) return
    roLeft = new ResizeObserver(() => {
        renderGraph(flattened.value)
    })
    roLeft.observe(l)
}

function destroy() {
    if (roLeft && leftRef.value) { try { roLeft.unobserve(leftRef.value) } catch { } roLeft.disconnect() }
    if (graph) graph.dispose()
    window.removeEventListener('resize', renderGraph)
}

function setupWindowResize() {
    window.addEventListener('resize', renderGraph)
}

onBeforeUnmount(() => {
    destroy()
})
</script>

<style lang="scss" scoped>
:deep(.ant-line) {
    animation: ant-line 1.5s infinite linear;
}

@keyframes ant-line {
    to {
        stroke-dashoffset: -20;
    }
}
</style>

### 如何在 Vue3 项目中集成和使用 AntV X6 图形库 #### 1. 安装依赖 在 Vue3 项目中集成 AntV X6,首先需要安装相关依赖。AntV X6 是一个功能强大的图形编辑库,而 `@antv/x6-vue-shape` 是用于将 Vue 组件渲染到画布中的辅助库。可以通过以下命令安装这些依赖: ```bash npm install @antv/x6 @antv/x6-vue-shape vue-draggable-next ``` 上述命令会安装 AntV X6Vue Shape 渲染支持以及可选的拖拽组件工具[^1]。 #### 2. 初始化画布 在 Vue3 中初始化 AntV X6 的画布,通常需要在组件的生命周期方法中进行。以下是一个简单的示例代码,展示如何创建一个基础画布并添加节点: ```vue <template> <div ref="container" style="width: 100%; height: 500px;"></div> </template> <script> import { Graph } from &#39;@antv/x6&#39; import { onMounted, ref } from &#39;vue&#39; export default { setup() { const container = ref(null) onMounted(() => { const graph = new Graph({ container: container.value, width: 800, height: 600, grid: true // 显示网格线 }) // 添加一个矩形节点 graph.addNode({ x: 100, y: 100, width: 100, height: 40, label: &#39;Hello AntV X6&#39;, attrs: { body: { fill: &#39;#f5f5f5&#39;, stroke: &#39;#333&#39;, strokeWidth: 1 } } }) }) return { container } } } </script> ``` 上述代码展示了如何在 Vue3 组件中通过 `onMounted` 生命周期方法初始化画布,并添加一个简单的矩形节点[^3]。 #### 3. 使用 Vue 组件作为节点 AntV X6 支持将 Vue 组件作为节点渲染到画布上。这需要借助 `@antv/x6-vue-shape` 库实现。以下是一个示例,展示如何将 Vue 组件用作节点内容: ```vue <template> <div ref="container" style="width: 100%; height: 500px;"></div> </template> <script> import { Graph } from &#39;@antv/x6&#39; import { registerVueComponent } from &#39;@antv/x6-vue-shape&#39; import { onMounted, ref } from &#39;vue&#39; // 自定义 Vue 组件 const CustomNode = { template: `<div style="background-color: #f5f5f5; padding: 10px; border: 1px solid #ccc;"> <p>{{ title }}</p> </div>`, props: [&#39;title&#39;] } registerVueComponent(CustomNode) export default { setup() { const container = ref(null) onMounted(() => { const graph = new Graph({ container: container.value, width: 800, height: 600, grid: true }) // 添加自定义 Vue 节点 graph.addNode({ x: 100, y: 100, width: 150, height: 80, component: CustomNode, props: { title: &#39;Custom Node&#39; } }) }) return { container } } } </script> ``` 此代码片段展示了如何注册一个 Vue 组件并将其作为节点添加到画布中[^1]。 #### 4. 性能优化与事件绑定 为了提升性能,可以使用 `useTeleport` 方法避免多个 App 导致的性能问题。此外,可以通过 `useCellEvent` 工具函数绑定事件,增强交互性。例如: ```javascript import { useTeleport } from &#39;@antv/x6-vue-shape&#39; graph.on(&#39;node:click&#39;, ({ node }) => { console.log(&#39;Node clicked:&#39;, node) }) useTeleport(graph) // 提升性能 ``` 上述代码片段展示了如何绑定节点点击事件以及使用 `useTeleport` 进行性能优化[^1]。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值