antd x6 + vue3

javascript 复制代码
<template>
  <div class="w-full h-full relative" v-loading="loading">
    <!-- 画布外层容器(自适应) -->
    <div class="absolute top-0 left-0 right-[480px] bottom-0" ref="leftRef">
      <div class="w-full h-full" ref="container" style="background:#f5f5f5;"></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="0.2"
            :max="2"
            :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" />
    </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 data from '@/utils/data.json'
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'



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
  }
}


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
)

const container = ref<HTMLDivElement | null>(null)
const leftRef = ref<HTMLDivElement | null>(null)
const groupMap: Record<string, any> = {}
let graph: Graph | null = null
let roLeft: ResizeObserver | null = null
let lastSize = { w: 0, h: 0 }
const route = useRoute()
const router = useRouter()
const graphData:any = ref([])
const loading = ref(false)

const mapJson = ref({
  bottom_nodes: [],
  right_outgoing: [],
  top_nodes: [],
  left_incoming: [],
  targetNode: null,
})

const nowNodeData:any = ref(null)



const zoomLevel = ref(1)

function onZoomChange(val: number) {
  if (graph) graph.zoomTo(val)
}


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



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(flattenToFourCategories(res), res,"------------------------当前的service_map数据------------------------")
      graphData.value = flattenToFourCategories(res)
      // graphData.value = res
      await renderGraph()
      setupObserver()
      setupWindowResize()
      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' // 默认灰色
}


function toggleGroupCollapse(groupNode: any) {
  const collapsed = !groupNode.data.collapsed
  groupNode.data.collapsed = collapsed

  const children = groupNode.getChildren() || []
  const initialHeight = groupNode.data.currentHeight || groupNode.getBBox().height
  const collapsedHeight = 40 // 折叠后高度

  if (collapsed) {
    // 隐藏所有子节点
    children.forEach(child => child.hide())
    // 设置折叠高度
    groupNode.resize(groupNode.getBBox().width, collapsedHeight)
  } else {
    // 显示子节点
    children.forEach(child => child.show())
    // 恢复原始高度,如果原始高度记录了
    const rows = Math.ceil(children.length / 6) // 6列一行
    const newHeight = Math.max(collapsedHeight + rows * 80, initialHeight)
    groupNode.resize(groupNode.getBBox().width, newHeight)
  }

  // 更新按钮符号
  const icon = groupNode?.findOne('text[selector=icon]')
  if (icon) icon.attr('text', collapsed ? '+' : '-')
}


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
  }
}


async function renderGraph() {
  await nextTick()
  const c = container.value
  const l = leftRef.value
  if (!c || !l) return

  const rect = l.getBoundingClientRect()
  const w = rect.width - 10
  const h = rect.height
  lastSize = { w, h }

  if (!graph) {
    // 初始化 Graph
    graph = new Graph({
      container: container.value!,
      width: w,
      height: h,
      panning: true,
      mousewheel: true,
      selecting: true,
      interacting: {
        nodeMovable: (node) => !node.data?.isGroup,
      },
      // 背景色
      background: {
        color: '#fff',
      },
      // 网格
      grid: {
        size: 10,            // 网格间距
        visible: true,       // 显示网格
        type: 'dot',         // 网格类型,可选 'dot' | 'line' | 'doubleMesh'
        args: {
          color: '#e0e0e0', // 网格颜色
        },
      },
    })

    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 {
    // resize
    graph.resize(w, h)
    graph.zoomToFit({ padding: 20, maxScale: 1 })
    return
  }
  

  // ---- 渲染节点与群组 ----
  const nodeMap: Record<string, any> = {}
  const categories: Record<string, any[]> = {}
  graphData.value.forEach((item:any) => {
    if (!categories[item.label]) categories[item.label] = []
    categories[item.label].push(...item.value.nodes)
  })

  const groupX = 20
  const groupGap = 20
  const rowLimit = 6
  const nodeGapX = 140
  let groupIndex = 0
  const groupCount = Object.keys(categories).length
  const initialGroupHeight = (h - groupGap * (groupCount - 1)) / groupCount

  for (const [label, nodes] of Object.entries(categories)) {
    const groupY = groupIndex * (initialGroupHeight + groupGap)
    const rows = Math.ceil(nodes.length / rowLimit)
    const minGroupHeight = rows * 80 + 40
    const groupHeight = Math.max(initialGroupHeight, minGroupHeight)

    const group = graph.addNode({
      shape: 'rect',
      x: groupX,
      y: groupY,
      width: w - groupX * 2,
      height: groupHeight,
      attrs: {
        body: { fill: '#fafafa', stroke: '#d1d4d4', strokeWidth: 1, rx: 10, ry: 10 },
        label: { 
          text: label, 
          fontSize: 16, 
          fill: '#00796b', 
          fontWeight: 'bold',
          refX: 10,                   // 左上角 X 偏移 10px
          refY: 10,                   // 左上角 Y 偏移 10px
          textAnchor: 'start',        // 水平对齐左边
          textVerticalAnchor: 'top',  // 垂直对齐顶部
        },
      },
      resizable: { directions: ['bottom'] },
      data: { isGroup: true, collapsed: false, initialPos: { x: groupX, y: groupY } ,currentHeight: groupHeight },
      movable: false,
    })


    group.addTools([
      {
        name: 'button',
        args: {
          x: '100%', // 右上角
          y: 0,
          offset: { x: -20, y: 10 },
          markup: [
            {
              tagName: 'rect',
              selector: 'button',
              attrs: {
                width: 16,
                height: 16,
                fill: '#00796b',
                stroke: '#004d40',
                rx: 4,
                ry: 4,
              },
            },
            {
              tagName: 'text',
              selector: 'icon',
              attrs: {
                text: '+', // 初始显示展开
                fill: '#fff',
                fontSize: 12,
                textAnchor: 'middle',
                refX: 8,
                refY: 12,
              },
            },
          ],
          onClick({ cell }) {
            toggleGroupCollapse(cell)
          },
        },
      },
    ])

    console.log(group,nodes,"----------当前生成的group")

    nodes.forEach((n, idx) => {
      const row = Math.floor(idx / rowLimit)
      const col = idx % rowLimit

      const nodesInRow = Math.min(rowLimit, nodes.length - row * rowLimit)
      const rowWidth = nodesInRow * nodeGapX

      const offsetX = groupX + (w - 2 * groupX - rowWidth) / 2 + col * nodeGapX

      const nodeY = groupY + 40 + row * 80

      //  随机偏移,避免整齐排列导致连线重叠
      const jitterX = (Math.random() - 0.5) * 60   // -30 ~ +30 像素
      const jitterY = (Math.random() - 0.5) * 30   // -15 ~ +15 像素

      const finalX = offsetX + jitterX
      const finalY = nodeY + jitterY

      // const isHealthy = n.componentHealthStats?.healthy
      const strokeColor = getColor(n.componentHealthStats)

      const width = 40
      const height = 40
      const points = `
        ${width / 2},0
        ${width},${height * 0.25}
        ${width},${height * 0.75}
        ${width / 2},${height}
        0,${height * 0.75}
        0,${height * 0.25}
      `

      const node = graph!.addNode({
        shape: 'polygon',
        x: finalX,
        y: finalY,
        width,
        height,
        points,
        attrs: {
          body: {
            stroke: strokeColor,
            strokeWidth: 2,
          },
          label: {
            text: n.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,   // 超出显示省略号
            },
          },
        },
        parent: group,
        movable: true,
        data: { groupId: group.id, groupLabel: label, idNum: n.idNum ,nodeLabel: n.name},
      })

      nodeMap[n.id] = node

      group.addChild(node)
    })

      groupIndex++
  }

  // ---- 添加连线(避免重叠) ----
  graphData.value.forEach((item:any) => {
    const edgeGroups: Record<string, any[]> = {}

    item.value.edges.forEach(e => {
      const key = `${e.sourceNode}_${e.targetNode}`
      if (!edgeGroups[key]) edgeGroups[key] = []
      edgeGroups[key].push(e)
    })

    Object.values(edgeGroups).forEach(edges => {
      edges.forEach((e, idx) => {
        const s = nodeMap[e.sourceNode]
        const t = nodeMap[e.targetNode]
        if (!s || !t) return

        // ---- 根据节点位置自动选择端口 ----
        let sourcePort = 'right'
        let targetPort = 'left'

        if (t.x < s.x) {
          sourcePort = 'left'
          targetPort = 'right'
        } else if (Math.abs(t.x - s.x) < 20) {
          sourcePort = s.y < t.y ? 'bottom' : 'top'
          targetPort = s.y < t.y ? 'top' : 'bottom'
        }

        // ---- 多条边偏移 ----
        const offsetStep = 10
        const totalEdges = edges.length
        const offset = (idx - (totalEdges - 1) / 2) * offsetStep
        // ---- 判断健康状态,设置边颜色和虚实 ----
        const strokeColor = getColor(s.data?.componentHealthStats || t.data?.componentHealthStats)
        
        // ---- 使用动态蚂蚁线 ----
        graph!.addEdge({
          source: { cell: s.id, port: sourcePort },
          target: { cell: t.id, port: targetPort },
          shape: 'dag-edge',
          connector: 'algo-offset-connector',
          data: { offset },
          attrs: {
            line: {
              stroke: strokeColor,
              strokeDasharray: '5 5',
              strokeWidth: 1.5,
              targetMarker: {
                name: 'classic',
                width: 8,       // ← 缩小箭头宽度
                height: 8,      // ← 缩小箭头高度
                offset: 0       // ← 让箭头贴紧线,不会歪
              },
              class: 'ant-line', // 动态虚线关键
            },
          },
        })
      })
    })
  })

  // 同步缩放
  graph?.on('scale', ({ sx }) => {
    zoomLevel.value = Number(sx.toFixed(2))
  })

  // ---- 节点拖动约束 ----
  graph.on('node:moving', ({ node, x, y }) => {
    console.log('节点拖动:', node.id, x, y)
    if (!node.data) return
  })

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

    nowNodeData.value = node.data
    console.log(node.data,"----------------当前点击的node节点")
    const groupNode: any = node.getData()
    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,
      groupNode?.groupLabel
    ).then((res: any) => {
      loading.value = 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) => {
      loading.value = false
      console.error('获取邻居节点失败:', error)
    })
    // 普通节点
    console.log('点击了节点:', node)
    console.log('节点数据:', node.getData())
  })

  // ---- 群组禁止拖动 ----
  graph.on('node:dragend', ({ node, e }) => {
    if (node.data?.isGroup) {
      e.preventDefault();
      e.stopPropagation();
      const pos = node.data.initialPos;
      if (pos) {
        (node as any).setPosition(pos.x, pos.y)
      }
    }
    node.setAttrs({
      body: { cursor: 'grab' },
    })
  })

  let ctrlPressed = false
  const embedPadding = 20

  graph.on('node:change:size', ({ node, options }) => {
    if (options.skipParentHandler) return
    const children = node.getChildren()
    if (children && children.length) {
      node.prop('originSize', node.getSize())
    }
  })


  graph.on('node:change:position', ({ node, options }) => {
    if (options.skipParentHandler || ctrlPressed) {
      return
    }

    const children = node.getChildren()
    if (children && children.length) {
      node.prop('originPosition', node.getPosition())
    }

    const parent = node.getParent()
    if (parent && parent.isNode()) {
      let originSize = parent.prop('originSize')
      if (originSize == null) {
        originSize = parent.getSize()
        parent.prop('originSize', originSize)
      }

      let originPosition = parent.prop('originPosition')
      if (originPosition == null) {
        originPosition = parent.getPosition()
        parent.prop('originPosition', originPosition)
      }

      let x = originPosition.x
      let y = originPosition.y
      let cornerX = originPosition.x + originSize.width
      let cornerY = originPosition.y + originSize.height
      let hasChange = false

      const children = parent.getChildren()
      if (children) {
        children.forEach((child) => {
          const bbox = child.getBBox().inflate(embedPadding)
          const corner = bbox.getCorner()

          if (bbox.x < x) {
            x = bbox.x
            hasChange = true
          }

          if (bbox.y < y) {
            y = bbox.y
            hasChange = true
          }

          if (corner.x > cornerX) {
            cornerX = corner.x
            hasChange = true
          }

          if (corner.y > cornerY) {
            cornerY = corner.y
            hasChange = true
          }
        })
      }

      if (hasChange) {
        const newHeight = cornerY - y

        parent.prop(
          {
            position: { x, y },
            size: { width: cornerX - x, height: cornerY - y },
             data: {
              ...parent.data,            // 保留原来的 data
              currentHeight: newHeight,  // 更新当前高度
            },
          },
          { skipParentHandler: true },
        )
      }
    }
  })

  graph.on('node:dragstart', ({ node, e }) => {
    if (node.data?.isGroup) {
      e.preventDefault();
      e.stopPropagation();
    }
    node.setAttrs({
      body: { cursor: 'grabbing' },
    })
  })

}

// ---- 监听 leftRef 尺寸变化 ----
function setupObserver() {
  const l = leftRef.value
  if (!l) return
  roLeft = new ResizeObserver(() => {
    renderGraph()
  })
  roLeft.observe(l)
}

// ---- 监听窗口 resize ----
function setupWindowResize() {
  window.addEventListener('resize', renderGraph)
}

// ---- 销毁 ----
function destroy() {
  if (roLeft && leftRef.value) { try { roLeft.unobserve(leftRef.value) } catch { } roLeft.disconnect() }
  if (graph) graph.dispose()
  window.removeEventListener('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>
javascript 复制代码
<template>
  <!-- title 文字展示区域 -->
  <div class=" w-full h-[40px] leading-[40px] flex items-center justify-center" v-if="!!nodeData?.nodeLabel">
    <div class="w-[40px] h-full leading-[40px]">
      节点名称:
    </div>
    <div class="w-full h-full">
      {{ nodeData?.nodeLabel }}
    </div>
  </div>
  <!-- 整个 图形区域 -->
  <div
    class="hex-map flex flex-col items-center justify-center w-full gap-6 mt-[10px] pb-[10px] "
    style="height: max-content; border-bottom: 1px solid #ccc;"
  >
    <!-- Top layer -->
    <div class="flex justify-center items-center gap-4 text-[14px]">
      User Experience
    </div>
    <div class="hex-layer flex justify-center items-center gap-4">
      <div
        v-for="(node, index) in top_nodes.length ? top_nodes : [null]"
        :key="node?.id ?? index"
        class="hex-wrapper"
      >
        <el-tooltip
          :content="getDisplayTitle(node, 'User Experience')"
          placement="top"
          effect="dark"
        >
          <div
            :class="['hex', 'hex-small', !node ? 'hex-placeholder' : '']"
            :style="node ? { background: getHexColor(node) } : {}"
          ></div>
        </el-tooltip>
      </div>
    </div>

    <!-- Middle layer -->
    <div class="flex justify-center items-center gap-4 text-[14px]">
      Services
    </div>
    <div class="hex-layer flex justify-center items-center gap-4 w-full">
      <!-- Left nodes -->
      <div class="flex flex-col justify-center items-center gap-2">
        <div
          v-for="(node, index) in left_incoming.length ? left_incoming : [null]"
          :key="node?.id ?? index"
          class="hex-wrapper"
        >
          <el-tooltip
            :content="getDisplayTitle(node, 'Left Node')"
            placement="top"
            effect="dark"
          >
            <div
              :class="['hex', 'hex-small', !node ? 'hex-placeholder' : '']"
              :style="node ? { background: getHexColor(node) } : {}"
            ></div>
          </el-tooltip>
        </div>
      </div>

      <!-- Arrow to center -->
      <div class="arrow">→</div>

      <!-- Center node -->
      <div class="hex-wrapper">
        <el-tooltip
          :content="getDisplayTitle(targetNode, 'Services')"
          placement="top"
          effect="dark"
        >
          <div
            class="hex hex-big"
            :style="{ background: targetNode ? getHexColor(targetNode) : '#e0e0e0' }"
          >
            <i class="center-icon">🌐</i>
          </div>
        </el-tooltip>
      </div>

      <!-- Arrow to right -->
      <div class="arrow">→</div>

      <!-- Right nodes -->
      <div class="flex flex-col justify-center items-center gap-2">
        <div
          v-for="(node, index) in right_outgoing.length ? right_outgoing : [null]"
          :key="node?.id ?? index"
          class="hex-wrapper"
        >
          <el-tooltip
            :content="getDisplayTitle(node, 'Right Node')"
            placement="top"
            effect="dark"
          >
            <div
              :class="['hex', 'hex-small', !node ? 'hex-placeholder' : '']"
              :style="node ? { background: getHexColor(node) } : {}"
            ></div>
          </el-tooltip>
        </div>
      </div>
    </div>

    <!-- Bottom layer -->
    <div class="flex justify-center items-center gap-4 text-[14px]">
      Infrastructure
    </div>
    <div class="hex-layer flex justify-center items-center gap-4">
      <div
        v-for="(node, index) in bottom_nodes.length ? bottom_nodes : [null]"
        :key="node?.id ?? index"
        class="hex-wrapper"
      >
        <el-tooltip
          :content="getDisplayTitle(node, 'Infrastructure')"
          placement="top"
          effect="dark"
        >
          <div
            :class="['hex', 'hex-small', !node ? 'hex-placeholder' : '']"
            :style="node ? { background: getHexColor(node) } : {}"
          ></div>
        </el-tooltip>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, defineProps, defineEmits } from 'vue'

const props = defineProps<{
  mapJson?: {
    top_nodes?: any[]
    bottom_nodes?: any[]
    left_incoming?: any[]
    right_outgoing?: any[]
    targetNode?: any
  },
  nowNode?:any
}>()

const emit = defineEmits<{ (e: 'click-node', node: any): void }>()


const top_nodes = computed(() => props.mapJson?.top_nodes ?? [])
const bottom_nodes = computed(() => props.mapJson?.bottom_nodes ?? [])
const left_incoming = computed(() => props.mapJson?.left_incoming ?? [])
const right_outgoing = computed(() => props.mapJson?.right_outgoing ?? [])
const targetNode = computed(() => props.mapJson?.targetNode ?? null)

const nodeData = computed(() => props.nowNode ?? null)

/**
 * 根据节点健康值返回颜色
 * 颜色规则与图表保持一致
 */
function getHexColor(node: any) {
  if (!node?.componentHealthStats) return '#999999' // 默认灰色

  const nodesCount: Record<string, number> = {
    criticalNodes: node?.componentHealthStats?.criticalNodes || 0,
    warningNodes: node?.componentHealthStats?.warningNodes || 0,
    unknownNodes: node?.componentHealthStats?.unknownNodes || 0,
    normalNodes: node?.componentHealthStats?.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] || '#999999'
  }

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

  return '#999999' // 默认灰色
}

function getDisplayTitle(node: any, layer: string): string {
  console.log(node,layer,"-------------当前要展示的相关文字")
  if (!node) return `${layer} (0)`
  const name = node?.name ?? layer
  const count = node?.count ?? 1
  return `${name}`
}

function onClick(node: any) {
  console.log('点击节点', node)
  emit('click-node', node)
}
</script>


<style scoped>
.hex {
  width: 40px;
  height: 38px;
  clip-path: polygon(
    25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%
  );
  display: flex;
  align-items: center;
  justify-content: center;
}

.hex-big {
  width: 60px;
  height: 56px;
  clip-path: polygon(
    25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%
  );
  display: flex;
  align-items: center;
  justify-content: center;
}

.hex-placeholder {
  background: #fff;
  border: 2px dashed #999;
}

.hex-wrapper {
  display: flex;
  align-items: center;
  justify-content: center;
}

.center-icon {
  font-size: 24px;
}

.arrow {
  font-size: 20px;
}
</style>

效果:

相关推荐
栀秋6661 小时前
当我把 proto 打印出来那一刻,我懂了JS的原型链
前端·javascript
小离a_a1 小时前
flex垂直布局,容器间距相等
开发语言·javascript·ecmascript
Cassie燁1 小时前
element-plus源码解读1——useNamespace
前端·vue.js
ErMao1 小时前
TypeScript的泛型工具集合
前端·javascript
傻啦嘿哟1 小时前
物流爬虫实战:某丰快递信息实时追踪技术全解析
java·开发语言·数据库
码力码力我爱你1 小时前
Harmony OS C++实战
开发语言·c++
茄子凉心1 小时前
android 开机启动App
android·java·开发语言
低客的黑调2 小时前
了解JVM 结构和运行机制,从小白编程Java 大佬
java·linux·开发语言
想唱rap2 小时前
C++ map和set
linux·运维·服务器·开发语言·c++·算法