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>
效果:
