feat: 基站状态页面及拓扑展示页面功能实现
This commit is contained in:
@@ -41,3 +41,90 @@ export function exportAMFDataUE(data: Record<string, any>) {
|
||||
timeout: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* AMF-接入基站信息列表
|
||||
* @param query 查询参数 neId=001&id=1
|
||||
* @returns object
|
||||
*/
|
||||
export function listAMFNblist(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/neData/amf/nb/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
timeout: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* AMF-接入基站状态信息列表
|
||||
* @param query 查询参数 neId=001&state=1
|
||||
* @returns object
|
||||
*/
|
||||
export function listAMFNbStatelist(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/neData/amf/nb/list-cfg',
|
||||
method: 'get',
|
||||
params: query,
|
||||
timeout: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* AMF-接入基站状态信息新增
|
||||
* @param neId 网元ID
|
||||
* @param data 数据 { "index": 1, "name": "Gnb", "address": "192.168.8.1", "position": "Area-B" }
|
||||
* @returns object
|
||||
*/
|
||||
export function addAMFNbState(neId: string, data: Record<string, any>) {
|
||||
return request({
|
||||
url: `/ne/config/data`,
|
||||
method: 'post',
|
||||
data: {
|
||||
neType: 'AMF',
|
||||
neId: neId,
|
||||
paramName: 'gnbList',
|
||||
paramData: data,
|
||||
loc: `${data.index}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* AMF-接入基站状态信息修改
|
||||
* @param neId 网元ID
|
||||
* @param data 数据 { "index": 1, "name": "Gnb", "address": "192.168.8.1", "position": "Area-B" }
|
||||
* @returns object
|
||||
*/
|
||||
export function editAMFNbState(neId: string, data: Record<string, any>) {
|
||||
return request({
|
||||
url: `/ne/config/data`,
|
||||
method: 'put',
|
||||
data: {
|
||||
neType: 'AMF',
|
||||
neId: neId,
|
||||
paramName: 'gnbList',
|
||||
paramData: data,
|
||||
loc: `${data.index}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* AMF-接入基站状态信息删除
|
||||
* @param neId 网元ID
|
||||
* @param index 数据index
|
||||
* @returns object
|
||||
*/
|
||||
export function delAMFNbState(neId: string, index: string | number) {
|
||||
return request({
|
||||
url: `/ne/config/data`,
|
||||
method: 'delete',
|
||||
params: {
|
||||
neType: 'AMF',
|
||||
neId: neId,
|
||||
paramName: 'gnbList',
|
||||
loc: `${index}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -130,26 +130,17 @@ export function edgeLineAnimateState() {
|
||||
|
||||
// circle-move 圆点沿边运动
|
||||
if (name === 'circle-move') {
|
||||
let back1 = group.find(
|
||||
(ele: any) => ele.get('name') === 'circle-stroke1'
|
||||
const backArr = group.findAll((ele: any) =>
|
||||
ele.get('name').startsWith('circle-stroke')
|
||||
);
|
||||
if (back1) {
|
||||
back1.remove();
|
||||
back1.destroy();
|
||||
}
|
||||
let back2 = group.find(
|
||||
(ele: any) => ele.get('name') === 'circle-stroke2'
|
||||
);
|
||||
if (back2) {
|
||||
back2.remove();
|
||||
back2.destroy();
|
||||
}
|
||||
if (value) {
|
||||
if (backArr.length > 0) return;
|
||||
|
||||
// 第一个矩形边
|
||||
const fillColor = typeof value === 'string' ? value : '#1890ff';
|
||||
// 边缘路径的起始位置
|
||||
const startPoint = keyShape.getPoint(0);
|
||||
back1 = group.addShape('circle', {
|
||||
const back1 = group.addShape('circle', {
|
||||
attrs: {
|
||||
x: startPoint.x,
|
||||
y: startPoint.y,
|
||||
@@ -159,6 +150,7 @@ export function edgeLineAnimateState() {
|
||||
// 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性
|
||||
name: 'circle-stroke1',
|
||||
});
|
||||
back1.show();
|
||||
back1.animate(
|
||||
(ratio: any) => {
|
||||
// 每帧中的操作。比率范围从 0 到 1,表示动画的进度。返回修改后的配置
|
||||
@@ -177,7 +169,7 @@ export function edgeLineAnimateState() {
|
||||
);
|
||||
// 第二个矩形边
|
||||
const endPoint = keyShape.getPoint(1);
|
||||
back2 = group.addShape('circle', {
|
||||
const back2 = group.addShape('circle', {
|
||||
zIndex: -2,
|
||||
attrs: {
|
||||
x: endPoint.x,
|
||||
@@ -188,7 +180,7 @@ export function edgeLineAnimateState() {
|
||||
// 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性
|
||||
name: 'circle-stroke2',
|
||||
});
|
||||
|
||||
back2.show();
|
||||
back2.animate(
|
||||
(ratio: any) => {
|
||||
// 每帧中的操作。比率范围从 0 到 1,表示动画的进度。返回修改后的配置
|
||||
@@ -205,6 +197,14 @@ export function edgeLineAnimateState() {
|
||||
duration: 2 * 1000, // 执行一次的持续时间
|
||||
}
|
||||
);
|
||||
} else {
|
||||
if (backArr.length <= 0) return;
|
||||
backArr.forEach((ele: any) => {
|
||||
ele.hide();
|
||||
ele.remove();
|
||||
ele.destroy();
|
||||
});
|
||||
backArr.length = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -212,11 +212,7 @@ export function edgeLineAnimateState() {
|
||||
// line-dash 虚线运动
|
||||
if (name === 'line-dash') {
|
||||
if (value) {
|
||||
keyShape.stopAnimate();
|
||||
keyShape.attr({
|
||||
lineDash: null,
|
||||
lineDashOffset: null,
|
||||
});
|
||||
if (keyShape.cfg.animating) return;
|
||||
let index = 0;
|
||||
keyShape.animate(
|
||||
() => {
|
||||
@@ -234,6 +230,12 @@ export function edgeLineAnimateState() {
|
||||
duration: 3000, // 执行一次的持续时间
|
||||
}
|
||||
);
|
||||
} else {
|
||||
keyShape.stopAnimate();
|
||||
keyShape.attr({
|
||||
lineDash: null,
|
||||
lineDashOffset: null,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -246,6 +248,7 @@ export function edgeLineAnimateState() {
|
||||
back.remove();
|
||||
back.destroy();
|
||||
}
|
||||
|
||||
const { path, stroke, lineWidth } = keyShape.attr();
|
||||
back = group.addShape('path', {
|
||||
attrs: {
|
||||
|
||||
@@ -300,12 +300,23 @@ export function nodeImageAnimateState() {
|
||||
ele.get('name').startsWith('circle-shape')
|
||||
);
|
||||
if (value) {
|
||||
if (Array.isArray(backArr) && backArr.length === 3) return;
|
||||
const fillColor = typeof value === 'string' ? value : '#f5222d';
|
||||
// 移除
|
||||
if (Array.isArray(backArr) && backArr.length >= 3) {
|
||||
for (const back of backArr) {
|
||||
back.stopAnimate();
|
||||
back.hide();
|
||||
back.remove();
|
||||
back.destroy();
|
||||
}
|
||||
backArr.length = 0;
|
||||
}
|
||||
|
||||
// 根据大小确定半径
|
||||
const size = Array.isArray(model?.size) ? model?.size : [40, 40];
|
||||
const x = size[0] / 2;
|
||||
const y = -size[1] / 2;
|
||||
const r = 3;
|
||||
const fillColor = typeof value === 'string' ? value : '#f5222d';
|
||||
|
||||
// 第一个背景圆
|
||||
const back1 = group.addShape('circle', {
|
||||
|
||||
593
src/views/ne-data/base-station/components/list.vue
Normal file
593
src/views/ne-data/base-station/components/list.vue
Normal file
@@ -0,0 +1,593 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, onMounted, toRaw } from 'vue';
|
||||
import { Form, message, Modal } from 'ant-design-vue';
|
||||
import { SizeType } from 'ant-design-vue/es/config-provider';
|
||||
import { ColumnsType } from 'ant-design-vue/es/table';
|
||||
import { ProModal } from 'antdv-pro-modal';
|
||||
import useNeInfoStore from '@/store/modules/neinfo';
|
||||
import useI18n from '@/hooks/useI18n';
|
||||
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
|
||||
import {
|
||||
addAMFNbState,
|
||||
delAMFNbState,
|
||||
editAMFNbState,
|
||||
listAMFNbStatelist,
|
||||
} from '@/api/neData/amf';
|
||||
const { t } = useI18n();
|
||||
import { useRoute } from 'vue-router';
|
||||
const route = useRoute();
|
||||
|
||||
const nbState = ref<DictType[]>([
|
||||
{
|
||||
value: 'ON',
|
||||
label: 'Online',
|
||||
tagType: 'green',
|
||||
tagClass: '',
|
||||
},
|
||||
{
|
||||
value: 'OFF',
|
||||
label: 'Offline',
|
||||
tagType: 'red',
|
||||
tagClass: '',
|
||||
},
|
||||
]);
|
||||
|
||||
/**网元参数 */
|
||||
let neCascaderOptions = ref<Record<string, any>[]>([]);
|
||||
/**网元数据 */
|
||||
let neTypeAndId = ref<string[]>([]);
|
||||
/**查询参数 */
|
||||
let queryParams = reactive({
|
||||
/**网元ID */
|
||||
neId: '',
|
||||
/**IMSI */
|
||||
state: undefined,
|
||||
});
|
||||
|
||||
/**查询参数重置 */
|
||||
function fnQueryReset() {
|
||||
queryParams = Object.assign(queryParams, {
|
||||
state: undefined,
|
||||
});
|
||||
fnGetList();
|
||||
}
|
||||
|
||||
/**表格状态类型 */
|
||||
type TabeStateType = {
|
||||
/**加载等待 */
|
||||
loading: boolean;
|
||||
/**紧凑型 */
|
||||
size: SizeType;
|
||||
/**记录数据 */
|
||||
data: Record<string, any>[];
|
||||
/**勾选记录 */
|
||||
selectedRowKeys: (string | number)[];
|
||||
};
|
||||
|
||||
/**表格状态 */
|
||||
let tableState: TabeStateType = reactive({
|
||||
loading: false,
|
||||
size: 'small',
|
||||
data: [],
|
||||
selectedRowKeys: [],
|
||||
});
|
||||
|
||||
/**表格字段列 */
|
||||
let tableColumns = ref<ColumnsType>([
|
||||
{
|
||||
title: 'Index',
|
||||
dataIndex: 'index',
|
||||
align: 'left',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
align: 'left',
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: 'Position',
|
||||
dataIndex: 'position',
|
||||
align: 'left',
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: 'Address',
|
||||
dataIndex: 'address',
|
||||
align: 'left',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: 'State',
|
||||
dataIndex: 'state',
|
||||
key: 'state',
|
||||
align: 'left',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: 'Time',
|
||||
align: 'left',
|
||||
width: 150,
|
||||
customRender(opt) {
|
||||
const record = opt.value;
|
||||
if (record.state === 'OFF') {
|
||||
return record.offTime;
|
||||
}
|
||||
return record.onTime;
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
/**表格分页器参数 */
|
||||
let tablePagination = {
|
||||
/**当前页数 */
|
||||
current: 1,
|
||||
/**每页条数 */
|
||||
pageSize: 20,
|
||||
/**默认的每页条数 */
|
||||
defaultPageSize: 20,
|
||||
/**指定每页可以显示多少条 */
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
/**只有一页时是否隐藏分页器 */
|
||||
hideOnSinglePage: true,
|
||||
/**是否可以快速跳转至某页 */
|
||||
showQuickJumper: true,
|
||||
/**是否可以改变 pageSize */
|
||||
showSizeChanger: true,
|
||||
/**数据总数 */
|
||||
total: 0,
|
||||
showTotal: (total: number) => t('common.tablePaginationTotal', { total }),
|
||||
onChange: (page: number, pageSize: number) => {
|
||||
tablePagination.current = page;
|
||||
tablePagination.pageSize = pageSize;
|
||||
},
|
||||
};
|
||||
|
||||
/**表格多选 */
|
||||
function fnTableSelectedRowKeys(keys: (string | number)[]) {
|
||||
tableState.selectedRowKeys = keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录删除
|
||||
* @param index ID
|
||||
*/
|
||||
function fnRecordDelete(index: string) {
|
||||
const [neType, neId] = neTypeAndId.value;
|
||||
if (!neId) return;
|
||||
let msg = `Delete index as:${index}`;
|
||||
if (index === '0') {
|
||||
msg = `Remove the index checkbox:${tableState.selectedRowKeys.length}`;
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
title: t('common.tipTitle'),
|
||||
content: msg,
|
||||
onOk() {
|
||||
const reqArr = [];
|
||||
if (index === '0') {
|
||||
if (tableState.selectedRowKeys.length <= 0) {
|
||||
return;
|
||||
}
|
||||
for (const v of tableState.selectedRowKeys) {
|
||||
if (neType === 'MME') {
|
||||
// reqArr.push(delAMFNbState(neId, v));
|
||||
}
|
||||
if (neType === 'AMF') {
|
||||
reqArr.push(delAMFNbState(neId, v));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (neType === 'MME') {
|
||||
// reqArr.push(delAMFNbState(neId, index));
|
||||
}
|
||||
if (neType === 'AMF') {
|
||||
reqArr.push(delAMFNbState(neId, index));
|
||||
}
|
||||
}
|
||||
if (reqArr.length <= 0) return;
|
||||
Promise.all(reqArr).then(res => {
|
||||
const resArr = res.filter(
|
||||
(item: any) => item.code !== RESULT_CODE_SUCCESS
|
||||
);
|
||||
if (resArr.length <= 0) {
|
||||
message.success({
|
||||
content: `${t('common.operateOk')}`,
|
||||
duration: 3,
|
||||
});
|
||||
fnGetList();
|
||||
} else {
|
||||
message.error({
|
||||
content: `${t('common.operateErr')}`,
|
||||
duration: 3,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**查询列表 */
|
||||
function fnGetList() {
|
||||
if (tableState.loading) return;
|
||||
tableState.loading = true;
|
||||
const [neType, neId] = neTypeAndId.value;
|
||||
queryParams.neId = neId;
|
||||
let req = null;
|
||||
// if (neType === 'MME') {
|
||||
// req = listAMFNbStatelist(toRaw(queryParams));
|
||||
// }
|
||||
if (neType === 'AMF') {
|
||||
req = listAMFNbStatelist(toRaw(queryParams));
|
||||
}
|
||||
if (req === null) {
|
||||
tableState.data = [];
|
||||
tableState.loading = false;
|
||||
return;
|
||||
}
|
||||
req.then(res => {
|
||||
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
||||
// 取消勾选
|
||||
if (tableState.selectedRowKeys.length > 0) {
|
||||
tableState.selectedRowKeys = [];
|
||||
}
|
||||
tableState.data = res.data.filter((item: any) => {
|
||||
// 状态过滤
|
||||
if (queryParams.state) {
|
||||
return item.state === queryParams.state;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
tableState.data = [];
|
||||
}
|
||||
tableState.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**对话框对象信息状态类型 */
|
||||
type ModalStateType = {
|
||||
/**新增框或修改框是否显示 */
|
||||
openByEdit: boolean;
|
||||
/**标题 */
|
||||
title: string;
|
||||
/**表单数据 */
|
||||
from: Record<string, any>;
|
||||
/**确定按钮 loading */
|
||||
confirmLoading: boolean;
|
||||
};
|
||||
|
||||
/**对话框对象信息状态 */
|
||||
let modalState: ModalStateType = reactive({
|
||||
openByEdit: false,
|
||||
title: 'NB Config List',
|
||||
from: {
|
||||
index: undefined,
|
||||
address: '',
|
||||
name: '',
|
||||
position: '',
|
||||
},
|
||||
confirmLoading: false,
|
||||
});
|
||||
|
||||
/**对话框内表单属性和校验规则 */
|
||||
const modalStateFrom = Form.useForm(
|
||||
modalState.from,
|
||||
reactive({
|
||||
address: [{ required: true, message: `text content length 0~64` }],
|
||||
name: [{ required: true, message: `text content length 0~64` }],
|
||||
position: [
|
||||
{
|
||||
required: true,
|
||||
message: `location description. Prohibition of spaces, length of text content 0-64`,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* 对话框弹出显示为 新增或者修改
|
||||
* @param noticeId 网元id, 不传为新增
|
||||
*/
|
||||
function fnModalVisibleByEdit(edit?: string | number) {
|
||||
if (!edit) {
|
||||
modalStateFrom.resetFields(); //重置表单
|
||||
modalState.title = 'Add Radio Info';
|
||||
modalState.openByEdit = true;
|
||||
// 获取最大index
|
||||
if (tableState.data.length <= 0) {
|
||||
modalState.from.index = 1;
|
||||
} else {
|
||||
const last = tableState.data[tableState.data.length - 1];
|
||||
modalState.from.index = last.index + 1;
|
||||
}
|
||||
}
|
||||
// 编辑
|
||||
if (edit === '0') {
|
||||
const row = tableState.data.find((row: any) => {
|
||||
return row.index === tableState.selectedRowKeys[0];
|
||||
});
|
||||
modalStateFrom.resetFields(); //重置表单
|
||||
Object.assign(modalState.from, row);
|
||||
modalState.title = 'Edit Radio Info';
|
||||
modalState.openByEdit = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话框弹出确认执行函数
|
||||
* 进行表达规则校验
|
||||
*/
|
||||
function fnModalOk() {
|
||||
const neID = queryParams.neId;
|
||||
if (!neID) return;
|
||||
const from = JSON.parse(JSON.stringify(modalState.from));
|
||||
modalStateFrom
|
||||
.validate()
|
||||
.then(e => {
|
||||
modalState.confirmLoading = true;
|
||||
const hide = message.loading(t('common.loading'), 0);
|
||||
let result: any = modalState.title.startsWith('Edit')
|
||||
? editAMFNbState(neID, from)
|
||||
: addAMFNbState(neID, from);
|
||||
result
|
||||
.then((res: any) => {
|
||||
if (res.code === RESULT_CODE_SUCCESS) {
|
||||
message.success({
|
||||
content: t('common.operateOk'),
|
||||
duration: 3,
|
||||
});
|
||||
fnModalCancel();
|
||||
fnGetList();
|
||||
} else {
|
||||
message.error({
|
||||
content: t('common.operateErr'),
|
||||
duration: 3,
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
hide();
|
||||
modalState.confirmLoading = false;
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
message.error(t('common.errorFields', { num: e.errorFields.length }), 3);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话框弹出关闭执行函数
|
||||
* 进行表达规则校验
|
||||
*/
|
||||
function fnModalCancel() {
|
||||
modalState.openByEdit = false;
|
||||
modalStateFrom.resetFields();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 获取网元网元列表
|
||||
useNeInfoStore()
|
||||
.fnNelist()
|
||||
.then(res => {
|
||||
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
||||
if (res.data.length > 0) {
|
||||
let arr: Record<string, any>[] = [];
|
||||
useNeInfoStore().neCascaderOptions.forEach(item => {
|
||||
if (['AMF', 'MME'].includes(item.value)) {
|
||||
arr.push(JSON.parse(JSON.stringify(item)));
|
||||
}
|
||||
});
|
||||
neCascaderOptions.value = arr;
|
||||
// 无查询参数neType时 默认选择AMF
|
||||
const queryNeType = (route.query.neType as string) || 'AMF';
|
||||
const item = arr.find(s => s.value === queryNeType);
|
||||
if (item && item.children) {
|
||||
const info = item.children[0];
|
||||
neTypeAndId.value = [info.neType, info.neId];
|
||||
} else {
|
||||
const info = neCascaderOptions.value[0].children[0];
|
||||
neTypeAndId.value = [info.neType, info.neId];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
message.warning({
|
||||
content: t('common.noData'),
|
||||
duration: 2,
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
// 获取列表数据
|
||||
fnGetList();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a-card
|
||||
:bordered="false"
|
||||
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
|
||||
>
|
||||
<!-- 表格搜索栏 -->
|
||||
<a-form :model="queryParams" name="queryParams" layout="horizontal">
|
||||
<a-row :gutter="16">
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-form-item :label="t('views.ne.common.neType')" name="neType ">
|
||||
<a-cascader
|
||||
v-model:value="neTypeAndId"
|
||||
:options="neCascaderOptions"
|
||||
:allow-clear="false"
|
||||
:placeholder="t('common.selectPlease')"
|
||||
@change="fnGetList"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-form-item label="State" name="state">
|
||||
<a-select
|
||||
v-model:value="queryParams.state"
|
||||
:options="nbState"
|
||||
:placeholder="t('common.selectPlease')"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-form-item>
|
||||
<a-space :size="8">
|
||||
<a-button type="primary" @click.prevent="fnGetList()">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
{{ t('common.search') }}
|
||||
</a-button>
|
||||
<a-button type="default" @click.prevent="fnQueryReset">
|
||||
<template #icon><ClearOutlined /></template>
|
||||
{{ t('common.reset') }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<a-card :bordered="false" :body-style="{ padding: '0px' }">
|
||||
<!-- 插槽-卡片左侧侧 -->
|
||||
<template #title>
|
||||
<a-space :size="8" align="center">
|
||||
<a-button type="primary" @click.prevent="fnModalVisibleByEdit()">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
{{ t('common.addText') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
type="default"
|
||||
:disabled="tableState.selectedRowKeys.length != 1"
|
||||
:loading="modalState.confirmLoading"
|
||||
@click.prevent="fnModalVisibleByEdit('0')"
|
||||
>
|
||||
<template #icon><FormOutlined /></template>
|
||||
{{ t('common.editText') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
type="default"
|
||||
danger
|
||||
:disabled="tableState.selectedRowKeys.length <= 0"
|
||||
:loading="modalState.confirmLoading"
|
||||
@click.prevent="fnRecordDelete('0')"
|
||||
>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
{{ t('common.deleteText') }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<!-- 插槽-卡片右侧 -->
|
||||
<template #extra>
|
||||
<a-space :size="8" align="center">
|
||||
<a-tooltip>
|
||||
<template #title>{{ t('common.reloadText') }}</template>
|
||||
<a-button type="text" @click.prevent="fnGetList()">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<!-- 表格列表 -->
|
||||
<a-table
|
||||
class="table"
|
||||
row-key="index"
|
||||
:columns="tableColumns"
|
||||
:loading="tableState.loading"
|
||||
:data-source="tableState.data"
|
||||
:size="tableState.size"
|
||||
:pagination="tablePagination"
|
||||
:scroll="{ x: tableColumns.length * 120 }"
|
||||
:row-selection="{
|
||||
type: 'checkbox',
|
||||
selectedRowKeys: tableState.selectedRowKeys,
|
||||
onChange: fnTableSelectedRowKeys,
|
||||
}"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'state'">
|
||||
<DictTag
|
||||
:options="nbState"
|
||||
:value="record.state"
|
||||
value-default="OFF"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 新增框或修改框 -->
|
||||
<ProModal
|
||||
:drag="true"
|
||||
:width="500"
|
||||
:destroyOnClose="true"
|
||||
:keyboard="false"
|
||||
:mask-closable="false"
|
||||
:open="modalState.openByEdit"
|
||||
:title="modalState.title"
|
||||
:confirm-loading="modalState.confirmLoading"
|
||||
@ok="fnModalOk"
|
||||
@cancel="fnModalCancel"
|
||||
>
|
||||
<a-form
|
||||
name="modalStateFrom"
|
||||
layout="horizontal"
|
||||
:label-col="{ span: 6 }"
|
||||
:labelWrap="true"
|
||||
>
|
||||
<a-form-item
|
||||
label="Name"
|
||||
name="name"
|
||||
v-bind="modalStateFrom.validateInfos.name"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="modalState.from.name"
|
||||
allow-clear
|
||||
:maxlength="64"
|
||||
>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="Address"
|
||||
name="address"
|
||||
v-bind="modalStateFrom.validateInfos.address"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="modalState.from.address"
|
||||
allow-clear
|
||||
:maxlength="64"
|
||||
>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="Position"
|
||||
name="position"
|
||||
v-bind="modalStateFrom.validateInfos.position"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="modalState.from.position"
|
||||
allow-clear
|
||||
:maxlength="64"
|
||||
>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ProModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.table :deep(.ant-pagination) {
|
||||
padding: 0 24px;
|
||||
}
|
||||
</style>
|
||||
557
src/views/ne-data/base-station/components/topology.vue
Normal file
557
src/views/ne-data/base-station/components/topology.vue
Normal file
@@ -0,0 +1,557 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted, ref, onBeforeUnmount, useTemplateRef } from 'vue';
|
||||
import { Graph, GraphData, Menu, Tooltip } 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';
|
||||
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();
|
||||
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: 228px;
|
||||
"
|
||||
>
|
||||
<div><strong>${t('views.monitor.topology.state')}:</strong><span>
|
||||
${
|
||||
nInfo.state === 'ON'
|
||||
? t('views.monitor.topology.normalcy')
|
||||
: t('views.monitor.topology.exceptions')
|
||||
}
|
||||
</span></div>
|
||||
<div><strong>OnTime:</strong><span>
|
||||
${nInfo.onTime ?? '--'}
|
||||
</span></div>
|
||||
<div><strong>OffTime:</strong><span>
|
||||
${nInfo.offTime ?? '--'}
|
||||
</span></div>
|
||||
<div>===========================</div>
|
||||
<div><strong>ID:</strong><span>${nInfo.index}</span></div>
|
||||
<div><strong>${t('views.monitor.topology.name')}:</strong><span>
|
||||
${nInfo.name ?? '--'}
|
||||
</span></div>
|
||||
<div><strong>Address:</strong><span>${nInfo.address}</span></div>
|
||||
<div><strong>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();
|
||||
}
|
||||
|
||||
/**图事件 */
|
||||
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: 'BT', // 布局的方向,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.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 (['AMF'].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: `${nb.name}`,
|
||||
img: parseBasePath('/svg/base5G.svg'),
|
||||
nInfo: nb,
|
||||
nType: 'GNB',
|
||||
});
|
||||
data.edges?.push({
|
||||
source: item.id,
|
||||
target: id,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (item.nType === 'MME') {
|
||||
}
|
||||
}
|
||||
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') {
|
||||
}
|
||||
});
|
||||
|
||||
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) {
|
||||
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"> Radio State Graph </a-space>
|
||||
</template>
|
||||
<!-- 插槽-卡片右侧 -->
|
||||
<template #extra>
|
||||
<a-button type="default" @click.prevent="fullscreen()">
|
||||
<template #icon>
|
||||
<FullscreenExitOutlined v-if="isFullscreen" />
|
||||
<FullscreenOutlined v-else />
|
||||
</template>
|
||||
Full Screen
|
||||
</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>
|
||||
40
src/views/ne-data/base-station/index.vue
Normal file
40
src/views/ne-data/base-station/index.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
onMounted,
|
||||
type Component,
|
||||
defineAsyncComponent,
|
||||
shallowRef,
|
||||
} from 'vue';
|
||||
import { PageContainer } from 'antdv-pro-layout';
|
||||
const defineComponent = shallowRef<Component | null>(null);
|
||||
|
||||
function fnSwitch(name: string) {
|
||||
if (name === 'topology') {
|
||||
defineComponent.value = defineAsyncComponent(
|
||||
() => import('@/views/ne-data/base-station/components/topology.vue')
|
||||
);
|
||||
}
|
||||
if (name === 'list') {
|
||||
defineComponent.value = defineAsyncComponent(
|
||||
() => import('@/views/ne-data/base-station/components/list.vue')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fnSwitch('topology');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageContainer>
|
||||
<template #extra>
|
||||
<a-button @click="fnSwitch('list')">List</a-button>
|
||||
<a-button type="primary" @click="fnSwitch('topology')">Topology</a-button>
|
||||
</template>
|
||||
|
||||
<component :is="defineComponent" />
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
Reference in New Issue
Block a user