feat: 终端目录部分调整,添加udm-voip/volte功能页面
This commit is contained in:
642
src/views/neData/base-station/topology.vue
Normal file
642
src/views/neData/base-station/topology.vue
Normal file
@@ -0,0 +1,642 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted, ref, onBeforeUnmount, useTemplateRef } from 'vue';
|
||||
import { Graph, GraphData, Menu, Tooltip, Util } from '@antv/g6';
|
||||
import { listAMFNbStatelist } from '@/api/neData/amf';
|
||||
import { parseBasePath } from '@/plugins/file-static-url';
|
||||
import { edgeLineAnimateState } from '@/views/monitor/topologyBuild/hooks/registerEdge';
|
||||
import { nodeImageAnimateState } from '@/views/monitor/topologyBuild/hooks/registerNode';
|
||||
import useNeInfoStore from '@/store/modules/neinfo';
|
||||
import useI18n from '@/hooks/useI18n';
|
||||
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
|
||||
import { stateNeInfo } from '@/api/ne/neInfo';
|
||||
import { parseDateToStr } from '@/utils/date-utils';
|
||||
import { useFullscreen } from '@vueuse/core';
|
||||
import { listMMENbStatelist } from '@/api/neData/mme';
|
||||
const { t } = useI18n();
|
||||
|
||||
/**图DOM节点实例对象 */
|
||||
const graphG6Dom = useTemplateRef('graphG6Dom');
|
||||
|
||||
/**图数据 */
|
||||
const graphData = reactive<Record<string, any>>({
|
||||
nodes: [
|
||||
{
|
||||
id: 'omc',
|
||||
label: 'OMC',
|
||||
img: parseBasePath('/svg/service_db.svg'),
|
||||
},
|
||||
{
|
||||
id: 'amf1',
|
||||
label: 'amf1',
|
||||
img: parseBasePath('/svg/service.svg'),
|
||||
},
|
||||
{
|
||||
id: 'amf2',
|
||||
label: 'amf2',
|
||||
img: parseBasePath('/svg/service.svg'),
|
||||
},
|
||||
{
|
||||
id: 'base1',
|
||||
label: 'base1',
|
||||
img: parseBasePath('/svg/base.svg'),
|
||||
},
|
||||
{
|
||||
id: 'base2',
|
||||
label: 'base2',
|
||||
img: parseBasePath('/svg/base.svg'),
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'omc',
|
||||
target: 'amf1',
|
||||
},
|
||||
{
|
||||
source: 'omc',
|
||||
target: 'amf2',
|
||||
},
|
||||
{
|
||||
source: 'amf1',
|
||||
target: 'base1',
|
||||
},
|
||||
{
|
||||
source: 'amf2',
|
||||
target: 'base1',
|
||||
},
|
||||
{
|
||||
source: 'amf2',
|
||||
target: 'base2',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
/**图实例对象 */
|
||||
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, nType, nInfo }: any = evt.item?.getModel();
|
||||
if (['GNB', 'ENB'].includes(nType)) {
|
||||
return `
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 140px;
|
||||
"
|
||||
>
|
||||
<span>
|
||||
${t('views.neData.baseStation.name')}:
|
||||
${label ?? '--'}
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return `
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 140px;
|
||||
"
|
||||
>
|
||||
<span>
|
||||
${t('views.monitor.topology.name')}:
|
||||
${label ?? '--'}
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
});
|
||||
|
||||
/**图节点展示 */
|
||||
const graphNodeTooltip = new Tooltip({
|
||||
offsetX: 10,
|
||||
offsetY: 20,
|
||||
getContent(evt) {
|
||||
if (!evt) return t('views.monitor.topologyBuild.graphNotInfo');
|
||||
const { id, label, nType, nInfo }: any = evt.item?.getModel();
|
||||
if (['GNB', 'ENB'].includes(nType)) {
|
||||
return `
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 256px;
|
||||
"
|
||||
>
|
||||
<div><strong>${t('views.neData.baseStation.state')}:</strong><span>
|
||||
${
|
||||
nInfo.state === 'ON'
|
||||
? t('views.neData.baseStation.online')
|
||||
: t('views.neData.baseStation.offline')
|
||||
}
|
||||
</span></div>
|
||||
<div><strong>${t('views.neData.baseStation.time')}:</strong><span>
|
||||
${nInfo.state === 'ON' ? nInfo.onTime ?? '--' : nInfo.offTime ?? '--'}
|
||||
</span></div>
|
||||
<div>==============================</div>
|
||||
<div><strong>ID:</strong><span>${nInfo.index}</span></div>
|
||||
<div><strong>${t('views.neData.baseStation.address')}:</strong><span>
|
||||
${nInfo.address ?? '--'}</span></div>
|
||||
<div><strong>${t('views.neData.baseStation.nbName')}:</strong><span>
|
||||
${nInfo.nbName ?? '--'}</span></div>
|
||||
<div><strong>${t('views.neData.baseStation.ueNum')}:</strong><span>
|
||||
${nInfo.ueNum ?? '--'}</span></div>
|
||||
<div><strong>${t('views.neData.baseStation.name')}:</strong><span>
|
||||
${nInfo.name ?? '--'}
|
||||
</span></div>
|
||||
<div><strong>${t(
|
||||
'views.neData.baseStation.position'
|
||||
)}:</strong><span style="word-wrap: break-word;">
|
||||
${nInfo.position}
|
||||
</span></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return `
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 200px;
|
||||
"
|
||||
>
|
||||
<div><strong>${t('views.monitor.topology.state')}:</strong><span>
|
||||
${
|
||||
nInfo.online
|
||||
? t('views.monitor.topology.normalcy')
|
||||
: t('views.monitor.topology.exceptions')
|
||||
}
|
||||
</span></div>
|
||||
<div><strong>${t('views.monitor.topology.refreshTime')}:</strong><span>
|
||||
${nInfo.refreshTime ?? '--'}
|
||||
</span></div>
|
||||
<div>========================</div>
|
||||
<div><strong>ID:</strong><span>${nInfo.neId}</span></div>
|
||||
<div><strong>${t('views.monitor.topology.name')}:</strong><span>
|
||||
${nInfo.neName ?? '--'}
|
||||
</span></div>
|
||||
<div><strong>IP:</strong><span>${nInfo.neIP}</span></div>
|
||||
<div><strong>${t('views.monitor.topology.version')}:</strong><span>
|
||||
${nInfo.version ?? '--'}
|
||||
</span></div>
|
||||
<div><strong>${t('views.monitor.topology.serialNum')}:</strong><span>
|
||||
${nInfo.sn ?? '--'}
|
||||
</span></div>
|
||||
<div><strong>${t('views.monitor.topology.expiryDate')}:</strong><span>
|
||||
${nInfo.expire ?? '--'}
|
||||
</span></div>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
itemTypes: ['node'],
|
||||
});
|
||||
|
||||
/**注册自定义边或节点 */
|
||||
function registerEdgeNode() {
|
||||
// 边
|
||||
edgeLineAnimateState();
|
||||
// 节点
|
||||
nodeImageAnimateState();
|
||||
}
|
||||
|
||||
/**
|
||||
* format the string
|
||||
* @param {string} str The origin string
|
||||
* @param {number} maxWidth max width
|
||||
* @param {number} fontSize font size
|
||||
* @return {string} the processed result
|
||||
*/
|
||||
function fittingString(str: string, maxWidth: number, fontSize: number) {
|
||||
let currentWidth = 0;
|
||||
let res = str;
|
||||
const pattern = new RegExp('[\u4E00-\u9FA5]+'); // distinguish the Chinese charactors and letters
|
||||
str.split('').forEach((letter, i) => {
|
||||
if (currentWidth > maxWidth) return;
|
||||
if (pattern.test(letter)) {
|
||||
// Chinese charactors
|
||||
currentWidth += fontSize;
|
||||
} else {
|
||||
// get the width of single letter according to the fontSize
|
||||
currentWidth += Util.getLetterWidth(letter, fontSize);
|
||||
}
|
||||
if (currentWidth > maxWidth) {
|
||||
res = `${str.substring(0, i)}\n${str.substring(i)}`;
|
||||
}
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
/**图事件 */
|
||||
function graphEvent(graph: Graph) {
|
||||
graph.on('edge:mouseenter', evt => {
|
||||
const { item } = evt;
|
||||
if (!item) return;
|
||||
graph.setItemState(item, 'circle-move', '#b5d6fb');
|
||||
});
|
||||
graph.on('edge:mouseleave', evt => {
|
||||
const { item } = evt;
|
||||
if (!item) return;
|
||||
graph.setItemState(item, 'circle-move', false);
|
||||
graph.setItemState(item, 'circle-move:#b5d6fb', false);
|
||||
});
|
||||
}
|
||||
|
||||
/**图数据渲染 */
|
||||
function handleRanderGraph(container: HTMLElement | null, data: GraphData) {
|
||||
if (!container) return;
|
||||
const { clientHeight, clientWidth } = container;
|
||||
|
||||
// 注册自定义边或节点
|
||||
registerEdgeNode();
|
||||
|
||||
const graph = new Graph({
|
||||
container: container,
|
||||
width: clientWidth,
|
||||
height: clientHeight,
|
||||
fitCenter: false,
|
||||
fitView: true,
|
||||
fitViewPadding: [40, 40, 40, 40],
|
||||
modes: {
|
||||
// default: ['drag-canvas', 'drag-node', 'zoom-canvas'],
|
||||
default: [
|
||||
'zoom-canvas',
|
||||
'drag-canvas',
|
||||
'drag-node',
|
||||
{
|
||||
type: 'drag-combo',
|
||||
onlyChangeComboSize: true, // 不改变层级关系
|
||||
},
|
||||
{
|
||||
type: 'collapse-expand-combo',
|
||||
trigger: 'dblclick',
|
||||
relayout: true, // 收缩展开后,不重新布局
|
||||
},
|
||||
],
|
||||
},
|
||||
groupByTypes: false,
|
||||
plugins: [graphNodeMenu, graphNodeTooltip],
|
||||
layout: {
|
||||
type: 'dagre',
|
||||
rankdir: 'TB', // 布局的方向,TB:从上到下,BT:从下到上,LR:从左到右,RL:从右到左
|
||||
//align: 'UL', // 节点对齐方式 UL、UR、DL、DR
|
||||
controlPoints: true,
|
||||
nodesep: 20,
|
||||
ranksep: 40,
|
||||
},
|
||||
animate: true,
|
||||
defaultNode: {
|
||||
type: 'image-animate-state',
|
||||
labelCfg: {
|
||||
offset: 8,
|
||||
position: 'bottom',
|
||||
style: { fill: '#ffffff', fontSize: 14, fontWeight: 500 },
|
||||
},
|
||||
size: 48,
|
||||
img: parseBasePath('/svg/cloud.svg'),
|
||||
width: 48,
|
||||
height: 48,
|
||||
},
|
||||
defaultEdge: {
|
||||
type: 'line-animate-state',
|
||||
labelCfg: {
|
||||
autoRotate: true,
|
||||
refY: 10,
|
||||
refX: 40,
|
||||
},
|
||||
style: {
|
||||
stroke: '#fafafa',
|
||||
lineWidth: 1.5,
|
||||
},
|
||||
},
|
||||
defaultCombo: {
|
||||
labelCfg: {
|
||||
offset: 16,
|
||||
position: 'bottom',
|
||||
style: { fill: '#ffffff', fontSize: 14, fontWeight: 500 },
|
||||
},
|
||||
style: {
|
||||
stroke: '#BDEFDB',
|
||||
fill: '#BDEFDB',
|
||||
opacity: 0.25,
|
||||
},
|
||||
collapsedSubstituteIcon: {
|
||||
show: true,
|
||||
img: parseBasePath('/svg/service.svg'),
|
||||
width: 48,
|
||||
height: 48,
|
||||
},
|
||||
},
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
graphEvent(graph);
|
||||
|
||||
// 创建 ResizeObserver 实例
|
||||
const observer = new ResizeObserver(function (entries) {
|
||||
// 当元素大小发生变化时触发回调函数
|
||||
entries.forEach(function (entry) {
|
||||
if (!graph) {
|
||||
return;
|
||||
}
|
||||
graph.changeSize(entry.contentRect.width, entry.contentRect.height);
|
||||
graph.fitCenter();
|
||||
});
|
||||
});
|
||||
// 监听元素大小变化
|
||||
observer.observe(container);
|
||||
|
||||
return graph;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图组数据渲染到画布
|
||||
*/
|
||||
async function fnGraphDataLoad() {
|
||||
// 加载基础网元
|
||||
await useNeInfoStore().fnNelist();
|
||||
const dataNe = await fnGraphDataBase();
|
||||
Object.assign(graphData, dataNe);
|
||||
graphG6.value = handleRanderGraph(graphG6Dom.value, dataNe);
|
||||
// 添加基站
|
||||
const dataNb = await fnGraphDataNb(dataNe);
|
||||
Object.assign(graphData, dataNb);
|
||||
// graphG6.value.clear();
|
||||
graphG6.value.read(dataNb);
|
||||
// 添加状态
|
||||
interval.value = true;
|
||||
repeatFn();
|
||||
}
|
||||
|
||||
/**图数据网元构建 */
|
||||
async function fnGraphDataBase() {
|
||||
const data: GraphData = {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
};
|
||||
// 添加基础网元
|
||||
for (const item of useNeInfoStore().getNeSelectOtions) {
|
||||
if ('OMC' === item.value) {
|
||||
if (item.children?.length === 0) continue;
|
||||
// 是否存在OMC保证唯一
|
||||
const hasOMC = data.nodes?.findIndex(v => v.neType === 'OMC');
|
||||
if (hasOMC !== -1) continue;
|
||||
// 根网元
|
||||
const omcInfo = item.children[0];
|
||||
const node = {
|
||||
id: 'OMC',
|
||||
label: omcInfo.neName,
|
||||
img: parseBasePath('/svg/service_db.svg'),
|
||||
nInfo: { online: false, neId: omcInfo.neId, neType: omcInfo.neType },
|
||||
nType: 'OMC',
|
||||
};
|
||||
// 添加OMC节点
|
||||
data.nodes?.push(node);
|
||||
continue;
|
||||
}
|
||||
if (['AMF', 'MME'].includes(item.value)) {
|
||||
if (item.children?.length === 0) continue;
|
||||
for (const child of item.children) {
|
||||
const id = `${child.neType}_${child.neId}`;
|
||||
const node = {
|
||||
id: id,
|
||||
label: child.neName,
|
||||
img: parseBasePath('/svg/service.svg'),
|
||||
nInfo: { online: false, neId: child.neId, neType: child.neType },
|
||||
nType: item.value,
|
||||
};
|
||||
// 添加节点
|
||||
data.nodes?.push(node);
|
||||
data.edges?.push({
|
||||
source: 'OMC',
|
||||
target: id,
|
||||
});
|
||||
}
|
||||
item.children.forEach((v: any) => {});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**图数据基站构建 */
|
||||
async function fnGraphDataNb(data: GraphData) {
|
||||
const arr = data.nodes?.filter((v: any) => ['AMF', 'MME'].includes(v.nType));
|
||||
if (arr === undefined || arr.length === 0) return data;
|
||||
for (const item of arr) {
|
||||
if (item.nType === 'AMF') {
|
||||
const neId = (item.nInfo as any).neId;
|
||||
const res = await listAMFNbStatelist({ neId });
|
||||
if (res.code !== RESULT_CODE_SUCCESS || !Array.isArray(res.data)) {
|
||||
continue;
|
||||
}
|
||||
for (const nb of res.data) {
|
||||
const id = `${item.id}_${nb.index}`;
|
||||
data.nodes?.push({
|
||||
id: id,
|
||||
label: fittingString(`${nb.name}`, 80, 14),
|
||||
img: parseBasePath('/svg/base5G.svg'),
|
||||
nInfo: nb,
|
||||
nType: 'GNB',
|
||||
});
|
||||
data.edges?.push({
|
||||
source: item.id,
|
||||
target: id,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (item.nType === 'MME') {
|
||||
const neId = (item.nInfo as any).neId;
|
||||
const res = await listMMENbStatelist({ neId });
|
||||
if (res.code !== RESULT_CODE_SUCCESS || !Array.isArray(res.data)) {
|
||||
continue;
|
||||
}
|
||||
for (const nb of res.data) {
|
||||
const id = `${item.id}_${nb.index}`;
|
||||
data.nodes?.push({
|
||||
id: id,
|
||||
label: fittingString(`${nb.name}`, 80, 14),
|
||||
img: parseBasePath('/svg/base4G.svg'),
|
||||
nInfo: nb,
|
||||
nType: 'ENB',
|
||||
});
|
||||
data.edges?.push({
|
||||
source: item.id,
|
||||
target: id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 图状态构建
|
||||
* @param reload 是否重载状态
|
||||
*/
|
||||
async function fnGraphState(reload: boolean = false) {
|
||||
// 节点状态
|
||||
if (!Array.isArray(graphData.nodes)) return;
|
||||
|
||||
const onc = graphData.nodes.find((v: any) => v.nType === 'OMC');
|
||||
if (onc) {
|
||||
const { id, nInfo } = onc as any;
|
||||
if (!nInfo) return;
|
||||
const res = await stateNeInfo(nInfo.neType, nInfo.neId);
|
||||
if (res.code === RESULT_CODE_SUCCESS) {
|
||||
Object.assign(nInfo, res.data, {
|
||||
refreshTime: parseDateToStr(res.data.refreshTime, 'HH:mm:ss'),
|
||||
});
|
||||
}
|
||||
const stateColor = nInfo.online ? '#52c41a' : '#f5222d'; // 状态颜色
|
||||
graphG6.value.setItemState(id, 'top-right-dot', stateColor);
|
||||
}
|
||||
|
||||
graphData.nodes
|
||||
.filter((v: any) => ['AMF', 'MME'].includes(v.nType))
|
||||
.forEach(async (v: any) => {
|
||||
const { id, nInfo } = v;
|
||||
if (!nInfo) return;
|
||||
const res = await stateNeInfo(nInfo.neType, nInfo.neId);
|
||||
if (res.code === RESULT_CODE_SUCCESS) {
|
||||
Object.assign(nInfo, res.data, {
|
||||
refreshTime: parseDateToStr(res.data.refreshTime, 'HH:mm:ss'),
|
||||
});
|
||||
}
|
||||
const stateColor = nInfo.online ? '#52c41a' : '#f5222d'; // 状态颜色
|
||||
graphG6.value.setItemState(id, 'top-right-dot', stateColor);
|
||||
|
||||
// 重载时更新下级基站状态
|
||||
if (reload && nInfo.neType === 'AMF') {
|
||||
const res = await listAMFNbStatelist({ neId: nInfo.neId });
|
||||
if (res.code == RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
||||
for (const nb of res.data) {
|
||||
const nbItem = graphData.nodes.find(
|
||||
(v: any) => v.id === `${id}_${nb.index}`
|
||||
);
|
||||
if (nbItem) {
|
||||
Object.assign(nbItem.nInfo, nb);
|
||||
const stateColor = nb.state === 'ON' ? '#52c41a' : '#f5222d'; // 状态颜色
|
||||
graphG6.value.setItemState(
|
||||
nbItem.id,
|
||||
'top-right-dot',
|
||||
stateColor
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (reload && nInfo.neType === 'MME') {
|
||||
const res = await listMMENbStatelist({ neId: nInfo.neId });
|
||||
if (res.code == RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
||||
for (const nb of res.data) {
|
||||
const nbItem = graphData.nodes.find(
|
||||
(v: any) => v.id === `${id}_${nb.index}`
|
||||
);
|
||||
if (nbItem) {
|
||||
Object.assign(nbItem.nInfo, nb);
|
||||
const stateColor = nb.state === 'ON' ? '#52c41a' : '#f5222d'; // 状态颜色
|
||||
graphG6.value.setItemState(
|
||||
nbItem.id,
|
||||
'top-right-dot',
|
||||
stateColor
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (reload) {
|
||||
await new Promise(resolve => setTimeout(resolve, 15_000));
|
||||
return;
|
||||
}
|
||||
// 非重载时使用初始获取的状态
|
||||
graphData.nodes
|
||||
.filter((v: any) => ['GNB', 'ENB'].includes(v.nType))
|
||||
.forEach(async (v: any) => {
|
||||
const { id, nInfo } = v;
|
||||
if (!nInfo) return;
|
||||
const stateColor = nInfo.state === 'ON' ? '#52c41a' : '#f5222d'; // 状态颜色
|
||||
graphG6.value.setItemState(id, 'top-right-dot', stateColor);
|
||||
});
|
||||
}
|
||||
|
||||
/**递归调度器 */
|
||||
const interval = ref<boolean>(false);
|
||||
|
||||
/**递归刷新图状态 */
|
||||
function repeatFn(reload: boolean = false) {
|
||||
if (!interval.value || !graphG6Dom.value) {
|
||||
return;
|
||||
}
|
||||
fnGraphState(reload)
|
||||
.finally(() => {
|
||||
repeatFn(true); // 递归调用自己
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
const viewportDom = ref<HTMLElement | null>(null);
|
||||
const { isFullscreen, toggle } = useFullscreen(viewportDom);
|
||||
function fullscreen() {
|
||||
toggle();
|
||||
|
||||
if (!graphG6Dom.value) return;
|
||||
if (isFullscreen.value) {
|
||||
graphG6Dom.value.style.height = 'calc(100vh - 300px)';
|
||||
} else {
|
||||
graphG6Dom.value.style.height = '100vh';
|
||||
}
|
||||
const { clientHeight, clientWidth } = graphG6Dom.value;
|
||||
graphG6.value.changeSize(clientHeight, clientWidth);
|
||||
graphG6.value.fitView(40);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fnGraphDataLoad();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
interval.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-card :bordered="false" :body-style="{ padding: '0' }" ref="viewportDom">
|
||||
<!-- 插槽-卡片左侧侧 -->
|
||||
<template #title>
|
||||
<a-space :size="8" align="center">
|
||||
{{ t('views.neData.baseStation.topologyTitle') }}
|
||||
</a-space>
|
||||
</template>
|
||||
<!-- 插槽-卡片右侧 -->
|
||||
<template #extra>
|
||||
<a-button type="default" @click.prevent="fullscreen()">
|
||||
<template #icon>
|
||||
<FullscreenExitOutlined v-if="isFullscreen" />
|
||||
<FullscreenOutlined v-else />
|
||||
</template>
|
||||
{{ t('loayouts.rightContent.fullscreen') }}
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<div ref="graphG6Dom" class="chart"></div>
|
||||
</a-card>
|
||||
</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