feat: 拓扑架构图状态

This commit is contained in:
TsMask
2024-01-09 18:25:26 +08:00
parent a9930e999d
commit 913d60d785
4 changed files with 608 additions and 7 deletions

View File

@@ -0,0 +1,500 @@
<script setup lang="ts">
import { reactive, onMounted, ref, onBeforeUnmount } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { listNe, stateNe } from '@/api/ne/ne';
import { message } from 'ant-design-vue/lib';
import { getGraphData } from '@/api/monitor/topology';
import { parseDateToStr } from '@/utils/date-utils';
import { Graph, GraphData, Menu, Tooltip } from '@antv/g6';
import {
edgeCubicAnimateCircleMove,
edgeCubicAnimateLineDash,
edgeLineAnimateState,
} from '../topologyBuild/hooks/registerEdge';
import {
nodeCircleAnimateShapeR,
nodeCircleAnimateShapeStroke,
nodeImageAnimateState,
nodeRectAnimateState,
} from '../topologyBuild/hooks/registerNode';
import useNeOptions from './useNeOptions';
import useI18n from '@/hooks/useI18n';
const { t } = useI18n();
const { fnNeRestart, fnNeStop, fnNeLogFile } = useNeOptions();
/**图DOM节点实例对象 */
const graphG6Dom = ref<HTMLElement | undefined>(undefined);
/**图状态 */
const graphState = reactive<Record<string, any>>({
/**当前图组名 */
group: '5GC System Architecture2',
/**图数据 */
data: {
combos: [],
edges: [],
nodes: [],
},
});
/**非网元元素 */
const notNeNodes = ['5GC', 'DN', 'UE', 'Base'];
/**图实例对象 */
const graphG6 = ref<any>(null);
/**图节点右击菜单 */
const graphNodeMenu = new Menu({
offsetX: 6,
offseY: 10,
itemTypes: ['node'],
getContent(evt) {
if (!evt) return t('views.monitor.topologyBuild.graphNotInfo');
const { id, label, neState }: any = evt.item?.getModel();
if (notNeNodes.includes(id)) {
return `<div><span>${label || id}</span></div>`;
}
if (!neState) {
return `<div><span>${label || id}</span></div>`;
}
return `
<div
style="
display: flex;
flex-direction: column;
width: 140px;
"
>
<h3 style="margin-bottom: 8px">
${t('views.monitor.topology.name')}:
${neState.neName ?? '--'}
</h3>
<div id="restart" style="cursor: pointer; margin-bottom: 4px">
> 重启
</div>
<div id="stop" style="cursor: pointer; margin-bottom: 4px;">
> ${t('views.configManage.neManage.stop')}
</div>
<div id="log" style="cursor: pointer; margin-bottom: 4px;">
> 查看日志文件
</div>
</div>
`;
},
handleMenuClick(target, item) {
const { neInfo }: any = item?.getModel();
const { neName, neType, neId } = neInfo;
const targetId = target.id;
switch (targetId) {
case 'restart':
fnNeRestart({ neName, neType, neId });
break;
case 'stop':
fnNeStop({ neName, neType, neId });
break;
case 'log':
fnNeLogFile({ neType, neId });
break;
}
},
});
/**图节点展示 */
const graphNodeTooltip = new Tooltip({
offsetX: 10,
offsetY: 20,
getContent(evt) {
if (!evt) return t('views.monitor.topologyBuild.graphNotInfo');
const { id, label, neState }: any = evt.item?.getModel();
if (notNeNodes.includes(id)) {
return `<div><span>${label || id}</span></div>`;
}
if (!neState) {
return `<div><span>${label || id}</span></div>`;
}
return `
<div
style="
display: flex;
flex-direction: column;
width: 200px;
"
>
<div><strong>${t('views.monitor.topology.state')}</strong><span>
${
neState.online
? t('views.monitor.topology.normalcy')
: t('views.monitor.topology.exceptions')
}
</span></div>
<div><strong>${t('views.monitor.topology.refreshTime')}</strong><span>
${neState.refreshTime ?? '--'}
</span></div>
<div>========================</div>
<div><strong>ID</strong><span>${neState.neId}</span></div>
<div><strong>${t('views.monitor.topology.name')}</strong><span>
${neState.neName ?? '--'}
</span></div>
<div><strong>IP</strong><span>${neState.neIP}</span></div>
<div><strong>${t('views.monitor.topology.version')}</strong><span>
${neState.version ?? '--'}
</span></div>
<div><strong>${t('views.monitor.topology.serialNum')}</strong><span>
${neState.sn ?? '--'}
</span></div>
<div><strong>${t('views.monitor.topology.expiryDate')}</strong><span>
${neState.expire ?? '--'}
</span></div>
</div>
`;
},
itemTypes: ['node'],
});
/**图数据渲染 */
function handleRanderGraph(
container: HTMLElement | undefined,
data: GraphData
) {
if (!container) return;
const { clientHeight, clientWidth } = container;
// 边
edgeCubicAnimateLineDash();
edgeCubicAnimateCircleMove();
edgeLineAnimateState();
// 节点
nodeCircleAnimateShapeR();
nodeCircleAnimateShapeStroke();
nodeRectAnimateState();
nodeImageAnimateState();
const graph = new Graph({
container: container,
width: clientWidth,
height: clientHeight,
fitCenter: true,
modes: {
default: [
'drag-combo',
'drag-canvas',
'zoom-canvas',
'collapse-expand-combo',
],
},
groupByTypes: false,
nodeStateStyles: {
selected: {
fill: 'transparent',
},
},
plugins: [graphNodeMenu, graphNodeTooltip],
animate: true, // 是否使用动画过度,默认为 false
animateCfg: {
duration: 500, // Number一次动画的时长
easing: 'linearEasing', // String动画函数
},
});
graph.data(data);
graph.render();
// 图绑定事件
fnGraphEvent(graph);
graphG6.value = graph;
return graph;
}
/**图绑定事件 */
function fnGraphEvent(graph: Graph) {
// 鼠标进入节点事件
graph.on('edge:mouseenter', (ev: any) => {
// 获得鼠标当前目标边
const edge = ev.item;
// 该边的起始点
const source = edge.getSource();
// 该边的结束点
const target = edge.getTarget();
// 先将边提前,再将端点提前。这样该边两个端点还是在该边上层,较符合常规。
// edge.toFront();
// source.toFront();
// target.toFront();
});
graph.on('edge:mouseleave', (ev: any) => {
// 获得图上所有边实例
const edges = graph.getEdges();
// 遍历边,将所有边的层级放置在后方,以恢复原样
// edges.forEach(edge => {
// edge.toBack();
// });
});
graph.on('node:mouseenter', evt => {
// 获得鼠标当前目标节点
const node = evt.item;
// 获取该节点的所有相关边
const edges = node && graph.getEdges();
// 遍历相关边,将所有相关边提前,再将相关边的两个端点提前,以保证相关边的端点在边的上方常规效果
// edges.forEach((edge: any) => {
// edge.toFront();
// edge.getSource().toFront();
// edge.getTarget().toFront();
// });
// graphEvent.value = {
// type: 'node:mouseenter',
// target: evt.target,
// item: evt.item,
// };
});
graph.on('node:mouseleave', (ev: any) => {
// 获得图上所有边实例
const edges = graph.getEdges();
// 遍历边,将所有边的层级放置在后方,以恢复原样
// edges.forEach(edge => {
// edge.toBack();
// });
});
}
/**
* 获取图组数据渲染到画布
* @param reload 是否重载数据
*/
function fnGraphDataLoad(reload: boolean = false) {
Promise.all([
getGraphData(graphState.group),
listNe({
bandStatus: false,
}),
])
.then(resArr => {
const graphRes = resArr[0];
const neRes = resArr[1];
if (
graphRes.code === RESULT_CODE_SUCCESS &&
Array.isArray(graphRes.data.nodes) &&
graphRes.data.nodes.length > 0 &&
neRes.code === RESULT_CODE_SUCCESS &&
Array.isArray(neRes.data) &&
neRes.data.length > 0
) {
return {
graphData: graphRes.data,
neList: neRes.data,
};
} else {
message.warning({
content: '找不到对应的图组数据',
duration: 5,
});
}
})
.then(res => {
console.log('fnGraphDataLoad', res);
if (!res) return;
const { combos, edges, nodes } = res.graphData;
// 节点过滤
const nf: Record<string, any>[] = nodes.filter(
(node: Record<string, any>) => {
Reflect.set(node, 'neState', { online: false });
const nodeID: string = node.id;
const hasNe = res.neList.some(ne => {
Reflect.set(node, 'neInfo', ne.neType === nodeID ? ne : {});
return ne.neType === nodeID;
});
if (hasNe) {
return true;
}
if (notNeNodes.includes(nodeID)) {
return true;
}
return false;
}
);
console.log(nf);
// 边过滤
const ef: Record<string, any>[] = edges.filter(
(edge: Record<string, any>) => {
const edgeSource: string = edge.source;
const edgeTarget: string = edge.target;
const hasNeS = nf.some(n => n.id === edgeSource);
const hasNeT = nf.some(n => n.id === edgeTarget);
// console.log(hasNeS, edgeSource, hasNeT, edgeTarget);
if (hasNeS && hasNeT) {
return true;
}
if (hasNeS && notNeNodes.includes(edgeTarget)) {
return true;
}
if (hasNeT && notNeNodes.includes(edgeSource)) {
return true;
}
return false;
}
);
// 分组过滤
combos.forEach((combo: Record<string, any>) => {
const comboChildren: Record<string, any>[] = combo.children;
combo.children = comboChildren.filter(c => nf.some(n => n.id === c.id));
return combo;
});
// 图数据
graphState.data = { combos, edges: ef, nodes: nf };
})
.finally(() => {
if (graphState.data.length < 0) return;
// 重载数据
if (reload) {
graphG6.value.read(graphState.data);
} else {
handleRanderGraph(graphG6Dom.value, graphState.data);
}
fnGetState(); // 获取网元状态
});
}
/**网元状态定时器 */
const stateTimeout = ref<any>(null);
/**查询网元状态 */
async function fnGetState() {
clearTimeout(stateTimeout.value);
const { combos, edges, nodes } = graphState.data;
// console.log({ combos, edges, nodes })
// 获取节点状态
for (const node of nodes) {
if (notNeNodes.includes(node.id)) continue;
const { neType, neId, neName } = node.neInfo;
if (!neType || !neId) continue;
const result = await stateNe(neType, neId);
if (result.code === RESULT_CODE_SUCCESS) {
// 更新网元状态
const newNeState = Object.assign(node.neState, result.data, {
refreshTime: parseDateToStr(result.data.refreshTime, 'HH:mm:ss'),
});
// 通过 ID 查询节点实例
const item = graphG6.value.findById(node.id);
if (item) {
const stateColor = newNeState.online ? '#52c41a' : '#f5222d'; // 状态颜色
// 图片类型不能填充
if (node.type.startsWith('image')) {
// 更新节点
graphG6.value.updateItem(item, {
label: neName,
});
// 设置状态
graphG6.value.setItemState(item, 'top-right-dot', stateColor);
} else {
// 更新节点
graphG6.value.updateItem(item, {
label: neName,
// neState: newNeState,
style: {
fill: stateColor, // 填充色
stroke: stateColor, // 填充色
},
// labelCfg: {
// style: {
// fill: '#ffffff', // 标签文本色
// },
// },
});
// 设置状态
graphG6.value.setItemState(item, 'stroke', newNeState.online);
}
}
}
}
// 设置边状态
for (const edge of edges) {
const edgeSource: string = edge.source;
const edgeTarget: string = edge.target;
const neS = nodes.find((n: any) => n.id === edgeSource);
const neT = nodes.find((n: any) => n.id === edgeTarget);
// console.log(neS, edgeSource, neT, edgeTarget);
// 通过 ID 查询节点实例
const item = graphG6.value.findById(edge.id);
if (neS && neT && item) {
// console.log(
// `${edgeSource} - ${edgeTarget}`,
// neS.neState.online && neT.neState.online
// );
const stateColor =
neS.neState.online && neT.neState.online ? '#000000' : '#ff4d4f'; // 状态颜色
// 更新边
// graphG6.value.updateItem(item, {
// label: `${edgeSource} - ${edgeTarget}`,
// style: {
// stroke: stateColor, // 填充色
// },
// labelCfg: {
// style: {
// fill: '#ffffff', // 标签文本色
// },
// },
// });
// 设置状态
// graphG6.value.setItemState(
// item,
// 'line-dash',
// neS.neState.online && neT.neState.online
// );
}
if (neS && notNeNodes.includes(edgeTarget)) {
}
if (neT && notNeNodes.includes(edgeSource)) {
}
}
stateTimeout.value = setTimeout(() => fnGetState(), 30_000);
}
onMounted(() => {
fnGraphDataLoad(false);
});
onBeforeUnmount(() => {
clearTimeout(stateTimeout.value);
});
</script>
<template>
<PageContainer>
<a-card
:bordered="false"
:body-style="{ marginBottom: '24px' }"
size="small"
>
<!-- 插槽-卡片左侧侧 -->
<template #title> </template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-button type="default" @click.prevent="fnGraphDataLoad(true)">
<template #icon><RetweetOutlined /></template>
{{ t('views.monitor.topology.switchLayout') }}
</a-button>
</template>
<div ref="graphG6Dom" class="chart"></div>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped>
.chart {
width: 100%;
height: calc(100vh - 300px);
background-color: rgb(43, 47, 51);
}
</style>