feat: 实训教学模块
This commit is contained in:
551
practical_training/views/monitor/topologyArchitecture/index.vue
Normal file
551
practical_training/views/monitor/topologyArchitecture/index.vue
Normal file
@@ -0,0 +1,551 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted, ref, onBeforeUnmount } from 'vue';
|
||||
import { PageContainer } from 'antdv-pro-layout';
|
||||
import {
|
||||
RESULT_CODE_ERROR,
|
||||
RESULT_CODE_SUCCESS,
|
||||
} from '@/constants/result-constants';
|
||||
import { listAllNeInfo } from '@/api/ne/neInfo';
|
||||
import { message } from 'ant-design-vue/es';
|
||||
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 '@/views/ne/neInfo/hooks/useNeOptions';
|
||||
import useI18n from '@/hooks/useI18n';
|
||||
import { OptionsType, WS } from '@/plugins/ws-websocket';
|
||||
import { hasRoles } from '@/plugins/auth-user';
|
||||
import { parseBasePath } from '@/plugins/file-static-url';
|
||||
const { t } = useI18n();
|
||||
const { fnNeRestart, fnNeStop, fnNeLogFile } = useNeOptions();
|
||||
const ws = new WS();
|
||||
|
||||
/**图DOM节点实例对象 */
|
||||
const graphG6Dom = ref<HTMLElement | undefined>(undefined);
|
||||
|
||||
/**图状态 */
|
||||
const graphState = reactive<Record<string, any>>({
|
||||
/**当前图组名 */
|
||||
group: '5GC System Architecture',
|
||||
/**图数据 */
|
||||
data: {
|
||||
combos: [],
|
||||
edges: [],
|
||||
nodes: [],
|
||||
},
|
||||
});
|
||||
|
||||
/**非网元元素 */
|
||||
const notNeNodes = [
|
||||
'5GC',
|
||||
'DN',
|
||||
'UE',
|
||||
'Base',
|
||||
'lan',
|
||||
'lan1',
|
||||
'lan2',
|
||||
'lan3',
|
||||
'lan4',
|
||||
'lan5',
|
||||
'lan6',
|
||||
'lan7',
|
||||
'LAN',
|
||||
'NR',
|
||||
];
|
||||
|
||||
/**图实例对象 */
|
||||
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">
|
||||
> ${t('views.configManage.neManage.restart')}
|
||||
</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;">
|
||||
> ${t('views.configManage.neManage.log')}
|
||||
</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>`;
|
||||
}
|
||||
let notStudentInfo = '';
|
||||
if (hasRoles(['teacher', 'admin'])) {
|
||||
notStudentInfo = `
|
||||
<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> `;
|
||||
}
|
||||
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>
|
||||
${notStudentInfo}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
itemTypes: ['node'],
|
||||
});
|
||||
|
||||
/**注册自定义边或节点 */
|
||||
function registerEdgeNode() {
|
||||
// 边
|
||||
edgeCubicAnimateLineDash();
|
||||
edgeCubicAnimateCircleMove();
|
||||
edgeLineAnimateState();
|
||||
// 节点
|
||||
nodeCircleAnimateShapeR();
|
||||
nodeCircleAnimateShapeStroke();
|
||||
nodeRectAnimateState();
|
||||
nodeImageAnimateState();
|
||||
}
|
||||
|
||||
/**图数据渲染 */
|
||||
function handleRanderGraph(
|
||||
container: HTMLElement | undefined,
|
||||
data: GraphData
|
||||
) {
|
||||
if (!container) return;
|
||||
const { clientHeight, clientWidth } = container;
|
||||
|
||||
// 注册自定义边或节点
|
||||
registerEdgeNode();
|
||||
|
||||
const graph = new Graph({
|
||||
container: container,
|
||||
width: clientWidth,
|
||||
height: clientHeight,
|
||||
fitCenter: true,
|
||||
fitView: true,
|
||||
fitViewPadding: [40],
|
||||
autoPaint: 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();
|
||||
|
||||
graphG6.value = graph;
|
||||
|
||||
// 创建 ResizeObserver 实例
|
||||
var observer = new ResizeObserver(function (entries) {
|
||||
// 当元素大小发生变化时触发回调函数
|
||||
entries.forEach(function (entry) {
|
||||
if (!graphG6.value) {
|
||||
return;
|
||||
}
|
||||
graphG6.value.changeSize(
|
||||
entry.contentRect.width,
|
||||
entry.contentRect.height - 30
|
||||
);
|
||||
graphG6.value.fitCenter();
|
||||
});
|
||||
});
|
||||
// 监听元素大小变化
|
||||
observer.observe(container);
|
||||
|
||||
return graph;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图组数据渲染到画布
|
||||
* @param reload 是否重载数据
|
||||
*/
|
||||
function fnGraphDataLoad(reload: boolean = false) {
|
||||
Promise.all([
|
||||
getGraphData(graphState.group),
|
||||
listAllNeInfo({
|
||||
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: t('views.monitor.topology.noData'),
|
||||
duration: 5,
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(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 });
|
||||
// 图片路径处理
|
||||
if (node.img) node.img = parseBasePath(node.img);
|
||||
if (node.icon.show && node.icon?.img)
|
||||
node.icon.img = parseBasePath(node.icon.img);
|
||||
// 遍历是否有网元数据
|
||||
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;
|
||||
}
|
||||
);
|
||||
|
||||
// 边过滤
|
||||
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);
|
||||
}
|
||||
clearInterval(interval10s.value);
|
||||
interval10s.value = null;
|
||||
fnGetState();
|
||||
interval10s.value = setInterval(async () => {
|
||||
if (!interval10s.value) return;
|
||||
fnGetState(); // 获取网元状态
|
||||
}, 20_000);
|
||||
});
|
||||
}
|
||||
|
||||
/**网元状态调度器 */
|
||||
const interval10s = ref<any>(null);
|
||||
|
||||
/**查询网元状态 */
|
||||
function fnGetState() {
|
||||
// 获取节点状态
|
||||
for (const node of graphState.data.nodes) {
|
||||
if (notNeNodes.includes(node.id)) continue;
|
||||
const { neType, neId } = node.neInfo;
|
||||
if (!neType || !neId) continue;
|
||||
ws.send({
|
||||
requestId: `${neType}_${neId}`,
|
||||
type: 'ne_state',
|
||||
data: {
|
||||
neType: neType,
|
||||
neId: neId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**接收数据后回调 */
|
||||
function wsError(ev: any) {
|
||||
// 接收数据后回调
|
||||
console.error(ev);
|
||||
}
|
||||
|
||||
/**接收数据后回调 */
|
||||
function wsMessage(res: Record<string, any>) {
|
||||
const { code, requestId, data } = res;
|
||||
if (code === RESULT_CODE_ERROR) {
|
||||
console.warn(res.msg);
|
||||
return;
|
||||
}
|
||||
if (!requestId) return;
|
||||
const [neType, neId] = requestId.split('_');
|
||||
const { combos, edges, nodes } = graphState.data;
|
||||
const node = nodes.find((item: Record<string, any>) => item.id === neType);
|
||||
|
||||
// 更新网元状态
|
||||
const newNeState = Object.assign(node.neState, data, {
|
||||
refreshTime: parseDateToStr(data.refreshTime, 'HH:mm:ss'),
|
||||
online: !!data.cpu,
|
||||
});
|
||||
|
||||
// 通过 ID 查询节点实例
|
||||
const item = graphG6.value.findById(node.id);
|
||||
if (item) {
|
||||
const stateColor = newNeState.online ? '#52c41a' : '#f5222d'; // 状态颜色
|
||||
// 图片类型不能填充
|
||||
if (node.type.startsWith('image')) {
|
||||
// 更新节点
|
||||
if (node.label !== newNeState.neName) {
|
||||
graphG6.value.updateItem(item, {
|
||||
label: newNeState.neName,
|
||||
});
|
||||
}
|
||||
// 设置状态
|
||||
graphG6.value.setItemState(item, 'top-right-dot', stateColor);
|
||||
} else {
|
||||
// 更新节点
|
||||
graphG6.value.updateItem(item, {
|
||||
label: newNeState.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);
|
||||
|
||||
if (neS && neT) {
|
||||
// 通过 ID 查询节点实例
|
||||
// const item = graphG6.value.findById(edge.id);
|
||||
// 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(
|
||||
edge.id,
|
||||
'circle-move',
|
||||
neS.neState.online && neT.neState.online
|
||||
);
|
||||
}
|
||||
if (neS && notNeNodes.includes(edgeTarget)) {
|
||||
graphG6.value.setItemState(edge.id, 'line-dash', neS.neState.online);
|
||||
}
|
||||
if (neT && notNeNodes.includes(edgeSource)) {
|
||||
graphG6.value.setItemState(edge.id, 'line-dash', neT.neState.online);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fnGraphDataLoad(false);
|
||||
|
||||
// 建立链接
|
||||
const options: OptionsType = {
|
||||
url: '/ws',
|
||||
onmessage: wsMessage,
|
||||
onerror: wsError,
|
||||
};
|
||||
ws.connect(options);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
ws.close();
|
||||
clearInterval(interval10s.value);
|
||||
interval10s.value = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageContainer>
|
||||
<a-card
|
||||
:bordered="false"
|
||||
:body-style="{ marginBottom: '24px' }"
|
||||
size="small"
|
||||
>
|
||||
<!-- 插槽-卡片左侧侧 -->
|
||||
<template #title>
|
||||
<a-space :size="8" align="center">
|
||||
<span>
|
||||
{{ t('views.monitor.topologyBuild.graphGroup') }}:
|
||||
{{ graphState.group }}
|
||||
</span>
|
||||
</a-space>
|
||||
</template>
|
||||
<!-- 插槽-卡片右侧 -->
|
||||
<template #extra>
|
||||
<a-button
|
||||
type="default"
|
||||
size="small"
|
||||
@click.prevent="fnGraphDataLoad(true)"
|
||||
>
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
{{ t('common.reloadText') }}
|
||||
</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>
|
||||
Reference in New Issue
Block a user