feat: 终端目录部分调整,添加udm-voip/volte功能页面

This commit is contained in:
TsMask
2025-04-24 09:58:34 +08:00
parent 352f7082f2
commit f76602d66e
21 changed files with 2334 additions and 14 deletions

View File

@@ -0,0 +1,434 @@
<script setup lang="ts">
import { reactive, ref, onMounted, toRaw } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { TableColumnsType, message } from 'ant-design-vue/es';
import { SizeType } from 'ant-design-vue/es/config-provider';
import { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import useI18n from '@/hooks/useI18n';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import useNeInfoStore from '@/store/modules/neinfo';
import { useRoute } from 'vue-router';
import { listAMFNblist } from '@/api/neData/amf';
import { listMMENblist } from '@/api/neData/mme';
const neInfoStore = useNeInfoStore();
const route = useRoute();
const { t } = useI18n();
/**网元参数 */
let neCascaderOptions = ref<Record<string, any>[]>([]);
/**查询参数 */
let queryParams = reactive({
/**网元ID */
neId: '',
/**网元类型 */
neType: ['', ''],
/**GNB_ID */
id: '',
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
neId: '',
id: '',
pageNum: 1,
pageSize: 20,
});
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'small',
seached: true,
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns = ref<TableColumnsType>([
{
title: 'Radio ID',
dataIndex: 'id',
align: 'center',
width: 100,
},
{
title: 'NE Name',
dataIndex: 'neName',
align: 'left',
resizable: true,
width: 200,
minWidth: 150,
maxWidth: 400,
},
{
title: 'UE Number',
dataIndex: 'ueNum',
align: 'center',
width: 100,
},
{
title: 'Radio Name',
dataIndex: 'name',
align: 'left',
resizable: true,
width: 200,
minWidth: 150,
maxWidth: 400,
},
{
title: 'Radio Address',
dataIndex: 'address',
align: 'left',
},
]);
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 20,
/**默认的每页条数 */
defaultPageSize: 20,
/**指定每页可以显示多少条 */
pageSizeOptions: ['10', '20', '50', '100'],
/**只有一页时是否隐藏分页器 */
hideOnSinglePage: false,
/**是否可以快速跳转至某页 */
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;
queryParams.pageNum = page;
queryParams.pageSize = pageSize;
fnGetList();
},
});
/**表格紧凑型变更操作 */
function fnTableSize({ key }: MenuInfo) {
tableState.size = key as SizeType;
}
let promises = ref<any[]>([]);
/**查询列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (tableState.loading) return;
tableState.loading = true;
if (pageNum) {
queryParams.pageNum = pageNum;
}
if (!queryParams.neType) {
tableState.data = [];
promises.value = [];
//同时获取45G基站信息 且在每条信息中添加45G字段(原始数据没有) 已经筛选后的
neCascaderOptions.value.map((item: any) => {
item.children.forEach((child: any) => {
let resq = null;
let s = {
neId: child.neId,
neType: child.neType,
nbId: queryParams.id,
pageNum: queryParams.pageNum,
pageSize: 10000,
};
if (child.neType === '5G' || child.neType === 'AMF') {
resq = listAMFNblist(s);
}
if (child.neType === '4G' || child.neType === 'MME') {
resq = listMMENblist(s);
}
if (resq !== null) {
promises.value.push(
resq.then(res => {
// 添加 neName 字段到每一项数据
if (res.code === RESULT_CODE_SUCCESS) {
res.data.forEach((row: any) => {
row.neName = `${child.neType}_${child.neId}`;
});
}
return res;
})
);
}
});
});
Promise.allSettled(promises.value).then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
const allBaseData = result.value;
if (
allBaseData.code === RESULT_CODE_SUCCESS &&
Array.isArray(allBaseData.rows)
) {
// 处理成功结果
tablePagination.total += allBaseData.total;
tableState.data = [...tableState.data, ...allBaseData.rows];
if (
tablePagination.total <=
(queryParams.pageNum - 1) * tablePagination.pageSize &&
queryParams.pageNum !== 1
) {
tableState.loading = false;
fnGetList(queryParams.pageNum - 1);
}
} else {
//AMF返回404是代表没找到这个数据 GNB_NOT_FOUND
tablePagination.total = 0;
tableState.data = [];
}
tableState.loading = false;
}
});
});
return;
}
let toBack: Record<string, any> = {
neType: queryParams.neType[0],
neId: queryParams.neType[1],
nbId: queryParams.id,
pageNum: queryParams.pageNum,
pageSize: queryParams.pageSize,
};
let resq = null;
if (queryParams.neType[0] === '5G' || queryParams.neType[0] === 'AMF') {
resq = listAMFNblist(toRaw(toBack));
}
if (queryParams.neType[0] === '4G' || queryParams.neType[0] === 'MME') {
resq = listMMENblist(toRaw(toBack));
}
if (resq === null) return;
resq.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
tablePagination.total = res.data.length;
tableState.data = res.data;
res.data.forEach((item: any) => {
item.neName = queryParams.neType.join('_');
});
if (
tablePagination.total <=
(queryParams.pageNum - 1) * tablePagination.pageSize &&
queryParams.pageNum !== 1
) {
tableState.loading = false;
fnGetList(queryParams.pageNum - 1);
}
} else {
//AMF返回404是代表没找到这个数据 GNB_NOT_FOUND
tablePagination.total = 0;
tableState.data = [];
}
tableState.loading = false;
});
}
onMounted(() => {
// 获取网元网元列表
neInfoStore
.fnNelist()
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
if (res.data.length > 0) {
// 过滤不可用的网元
for (const item of neInfoStore.getNeCascaderOptions) {
if (!['AMF', 'MME'].includes(item.value)) continue;
const v = JSON.parse(JSON.stringify(item));
if (v.label === 'AMF') {
v.label = '5G';
}
if (v.label === 'MME') {
v.label = '4G';
}
neCascaderOptions.value.push(v);
}
if (neCascaderOptions.value.length === 0) {
message.warning({
content: t('common.noData'),
duration: 2,
});
return;
}
// 无查询参数neType时 默认选择AMF
const queryNeType = (route.query.neType as string) || '5G';
const item = neCascaderOptions.value.find(
s => s.value === queryNeType
);
if (item && item.children) {
const info = item.children[0];
queryParams.neType = [info.neType, info.neId];
} else {
const info = neCascaderOptions.value[0].children[0];
queryParams.neType = [info.neType, info.neId];
}
}
} else {
message.warning({
content: t('common.noData'),
duration: 2,
});
}
})
.finally(() => {
// 获取列表数据
fnGetList();
});
});
</script>
<template>
<PageContainer>
<a-card
v-show="tableState.seached"
: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 name="neId" :label="t('views.neUser.base5G.neType')">
<a-cascader
v-model:value="queryParams.neType"
:options="neCascaderOptions"
:placeholder="t('common.selectPlease')"
@change="fnGetList(1)"
/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="Radio ID" name="id">
<a-input v-model:value="queryParams.id" allow-clear></a-input>
</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(1)">
<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> </template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-tooltip>
<template #title>{{ t('common.searchBarText') }}</template>
<a-switch
v-model:checked="tableState.seached"
:checked-children="t('common.switch.show')"
:un-checked-children="t('common.switch.hide')"
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>{{ t('common.reloadText') }}</template>
<a-button type="text" @click.prevent="fnGetList()">
<template #icon><ReloadOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip placement="topRight">
<template #title>{{ t('common.sizeText') }}</template>
<a-dropdown placement="bottomRight" trigger="click">
<a-button type="text">
<template #icon><ColumnHeightOutlined /></template>
</a-button>
<template #overlay>
<a-menu
:selected-keys="[tableState.size as string]"
@click="fnTableSize"
>
<a-menu-item key="default">
{{ t('common.size.default') }}
</a-menu-item>
<a-menu-item key="middle">
{{ t('common.size.middle') }}
</a-menu-item>
<a-menu-item key="small">
{{ t('common.size.small') }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-tooltip>
</a-space>
</template>
<!-- 表格列表 -->
<a-table
class="table"
row-key="id"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:pagination="tablePagination"
:scroll="{ y: 'calc(100vh - 480px)' }"
@resizeColumn="(w:number, col:any) => (col.width = w)"
>
</a-table>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,384 @@
<script setup lang="ts">
import { reactive, ref, toRaw, watch } from 'vue';
import { ProModal } from 'antdv-pro-modal';
import { message, Modal } from 'ant-design-vue/es';
import { SizeType } from 'ant-design-vue/es/config-provider';
import { ColumnsType } from 'ant-design-vue/es/table';
import { saveAs } from 'file-saver';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import useI18n from '@/hooks/useI18n';
import { exportNBState, listNBState } from '@/api/neData/nb-state';
import { Dayjs } from 'dayjs';
const { t } = useI18n();
const emit = defineEmits(['cancel', 'update:open']);
const props = defineProps({
title: {
type: String,
default: '标题',
},
open: {
type: Boolean,
default: false,
},
/**网元ID */
neId: {
type: String,
default: false,
},
neType: {
type: String,
default: false,
},
});
/**开始结束时间 */
let queryRangePicker = ref<[Dayjs, Dayjs] | undefined>(undefined);
/**状态字典 */
const nbStateOptions = ref<DictType[]>([
{
value: 'ON',
label: t('views.neData.baseStation.online'),
tagType: 'green',
tagClass: '',
},
{
value: 'OFF',
label: t('views.neData.baseStation.offline'),
tagType: 'red',
tagClass: '',
},
]);
/**查询参数 */
let queryParams = reactive({
/**网元 */
neType: '',
neId: '',
/**排序字段 */
sortField: 'id',
sortOrder: 'desc',
/**状态 */
status: undefined as undefined | string,
/**开始时间 */
startTime: undefined as undefined | number,
/**结束时间 */
endTime: undefined as undefined | number,
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
status: undefined,
startTime: undefined,
endTime: undefined,
pageNum: 1,
pageSize: 20,
});
queryRangePicker.value = undefined;
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
seached: true,
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns = ref<ColumnsType>([
{
title: t('common.rowId'),
dataIndex: 'id',
align: 'left',
width: 80,
},
{
title: t('views.neData.baseStation.name'),
dataIndex: 'name',
align: 'left',
resizable: true,
width: 120,
minWidth: 100,
maxWidth: 250,
ellipsis: true,
},
{
title: t('views.neData.baseStation.position'),
dataIndex: 'position',
align: 'left',
resizable: true,
width: 150,
minWidth: 100,
maxWidth: 400,
ellipsis: true,
},
{
title: t('views.neData.baseStation.address'),
dataIndex: 'address',
align: 'left',
width: 100,
},
{
title: t('views.neData.baseStation.nbName'),
dataIndex: 'nbName',
align: 'left',
resizable: true,
width: 100,
minWidth: 100,
maxWidth: 200,
},
{
title: t('views.neData.baseStation.state'),
dataIndex: 'state',
key: 'state',
align: 'left',
width: 80,
},
{
title: t('views.neData.baseStation.time'),
dataIndex: 'time',
align: 'left',
width: 150,
},
]);
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 20,
/**默认的每页条数 */
defaultPageSize: 20,
/**指定每页可以显示多少条 */
pageSizeOptions: ['10', '20', '50', '100'],
/**只有一页时是否隐藏分页器 */
hideOnSinglePage: false,
/**是否可以快速跳转至某页 */
showQuickJumper: true,
/**是否可以改变 pageSize */
showSizeChanger: true,
/**数据总数 */
total: 0,
showTotal: (total: number) =>
t('common.tablePaginationTotal', { total: total }),
onChange: (page: number, pageSize: number) => {
tablePagination.current = page;
tablePagination.pageSize = pageSize;
queryParams.pageNum = page;
queryParams.pageSize = pageSize;
fnGetList();
},
});
/**列表导出 */
function fnExportList() {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.neData.baseStation.exportTip'),
onOk() {
const hide = message.loading(t('common.loading'), 0);
const querys = toRaw(queryParams);
exportNBState(querys)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 3,
});
saveAs(res.data, `nb_state_history_records_export_${Date.now()}.xlsx`);
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
hide();
});
},
});
}
/**查询字典数据列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (tableState.loading) return;
tableState.loading = true;
if (pageNum) {
queryParams.pageNum = pageNum;
}
// 时间范围
if (
Array.isArray(queryRangePicker.value) &&
queryRangePicker.value.length > 0
) {
queryParams.startTime = queryRangePicker.value[0].valueOf();
queryParams.endTime = queryRangePicker.value[1].valueOf();
} else {
queryParams.startTime = undefined;
queryParams.endTime = undefined;
}
listNBState(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
const { total, rows } = res.data;
tablePagination.total = total;
tableState.data = rows;
if (
tablePagination.total <=
(queryParams.pageNum - 1) * tablePagination.pageSize &&
queryParams.pageNum !== 1
) {
tableState.loading = false;
fnGetList(queryParams.pageNum - 1);
}
}
tableState.loading = false;
});
}
/**弹框取消按钮事件 */
function fnModalCancel() {
emit('update:open', false);
emit('cancel');
}
/**监听是否显示,初始数据 */
watch(
() => props.open,
val => {
if (val) {
queryParams.neType = props.neType;
queryParams.neId = props.neId;
// 获取列表数据
fnGetList();
}
}
);
</script>
<template>
<ProModal
:drag="true"
:destroyOnClose="true"
:width="1200"
:title="props.title"
:open="props.open"
:keyboard="false"
:mask-closable="false"
@cancel="fnModalCancel"
:footer="null"
>
<!-- 表格搜索栏 -->
<a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16" style="margin-left: 0; margin-right: 0">
<a-col :lg="6" :md="12" :xs="24">
<a-form-item
:label="t('views.neData.baseStation.state')"
name="status"
>
<a-select
v-model:value="queryParams.status"
allow-clear
:placeholder="t('common.selectPlease')"
:options="nbStateOptions"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="16" :md="12" :xs="24">
<a-form-item
:label="t('views.neData.baseStation.time')"
name="queryRangePicker"
>
<a-range-picker
v-model:value="queryRangePicker"
:bordered="true"
:allow-clear="true"
style="width: 100%"
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
></a-range-picker>
</a-form-item>
</a-col>
<a-col :lg="24" :md="24" :xs="24">
<a-form-item>
<a-space :size="8">
<a-button type="primary" @click.prevent="fnGetList(1)">
<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-button type="dashed" @click.prevent="fnExportList()">
<template #icon><ExportOutlined /></template>
{{ t('common.export') }}
</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
<!-- 表格列表 -->
<a-table
class="table"
row-key="id"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:pagination="tablePagination"
:scroll="{ x: tableColumns.length * 120, y: 'calc(100vh - 480px)' }"
@resizeColumn="(w:number, col:any) => (col.width = w)"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'state'">
<DictTag
:options="nbStateOptions"
:value="record.state"
value-default="OFF"
/>
</template>
</template>
</a-table>
</ProModal>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import { ref } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import ListComponent from './list.vue';
import TopologyComponent from './topology.vue';
import useI18n from '@/hooks/useI18n';
const { t } = useI18n();
const value = ref<string>('list');
</script>
<template>
<PageContainer>
<template #extra>
<a-radio-group v-model:value="value" button-style="solid">
<a-radio-button value="list">
{{ t('views.neData.baseStation.list') }}
</a-radio-button>
<a-radio-button value="topology">
{{ t('views.neData.baseStation.topology') }}
</a-radio-button>
</a-radio-group>
</template>
<div v-show="value === 'list'">
<ListComponent></ListComponent>
</div>
<div v-if="value === 'topology'">
<TopologyComponent></TopologyComponent>
</div>
</PageContainer>
</template>
<style scoped></style>

View File

@@ -0,0 +1,958 @@
<script setup lang="ts">
import {
reactive,
ref,
onMounted,
toRaw,
computed,
defineAsyncComponent,
} 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 UploadModal from '@/components/UploadModal/index.vue';
import useNeInfoStore from '@/store/modules/neinfo';
import useI18n from '@/hooks/useI18n';
const { t, currentLocale } = useI18n();
import {
addAMFNbState,
delAMFNbState,
editAMFNbState,
listAMFNbStatelist,
} from '@/api/neData/amf';
import {
addMMENbState,
delMMENbState,
editMMENbState,
listMMENbStatelist,
} from '@/api/neData/mme';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import saveAs from 'file-saver';
import { readSheet, writeSheet } from '@/utils/execl-utils';
import { useRoute } from 'vue-router';
const route = useRoute();
// 异步加载组件
const HistoryModal = defineAsyncComponent(
() => import('./components/history.vue')
);
const nbState = ref<DictType[]>([
{
value: 'ON',
label: t('views.neData.baseStation.online'),
tagType: 'green',
tagClass: '',
},
{
value: 'OFF',
label: t('views.neData.baseStation.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: t('common.rowId'),
// dataIndex: 'index',
// align: 'left',
// width: 80,
// },
{
title: t('views.neData.baseStation.name'),
dataIndex: 'name',
align: 'left',
resizable: true,
width: 120,
minWidth: 100,
maxWidth: 250,
ellipsis: true,
},
{
title: t('views.neData.baseStation.position'),
dataIndex: 'position',
align: 'left',
resizable: true,
width: 150,
minWidth: 100,
maxWidth: 400,
ellipsis: true,
},
{
title: t('views.neData.baseStation.address'),
dataIndex: 'address',
align: 'left',
resizable: true,
width: 100,
minWidth: 100,
maxWidth: 200,
},
{
title: t('views.neData.baseStation.nbName'),
dataIndex: 'nbName',
align: 'left',
resizable: true,
width: 100,
minWidth: 100,
maxWidth: 200,
},
{
title: t('views.neData.baseStation.ueNum'),
dataIndex: 'ueNum',
align: 'left',
resizable: true,
width: 80,
minWidth: 80,
maxWidth: 120,
},
{
title: t('views.neData.baseStation.state'),
dataIndex: 'state',
key: 'state',
align: 'left',
resizable: true,
width: 80,
minWidth: 80,
maxWidth: 120,
},
{
title: t('views.neData.baseStation.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(delMMENbState(neId, v));
}
if (neType === 'AMF') {
reqArr.push(delAMFNbState(neId, v));
}
}
} else {
if (neType === 'MME') {
reqArr.push(delMMENbState(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 = listMMENbStatelist(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;
});
}
const stateNum = computed(() => {
let onNum = 0;
let offNum = 0;
tableState.data.forEach((item: any) => {
if (item.state === 'ON') {
onNum += 1;
}
if (item.state === 'OFF') {
offNum += 1;
}
});
return [onNum, offNum];
});
/**对话框对象信息状态类型 */
type ModalStateType = {
/**新增框或修改框是否显示 */
openByEdit: boolean;
/**标题 */
title: string;
/**表单数据 */
from: Record<string, any>;
/**确定按钮 loading */
confirmLoading: boolean;
/**历史框 */
openByHistory: boolean;
/**导入框 */
openByImport: boolean;
importMsgArr: string[];
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
openByEdit: false,
title: 'NB Config List',
from: {
address: '',
index: undefined,
name: '',
nbName: undefined,
offTime: undefined,
onTime: undefined,
position: '',
state: undefined,
ueNum: undefined,
},
confirmLoading: false,
openByHistory: false,
openByImport: false,
importMsgArr: [],
});
/**对话框内表单属性和校验规则 */
const modalStateFrom = Form.useForm(
modalState.from,
reactive({
address: [
{ required: true, message: t('views.neData.baseStation.addressPlease') },
],
name: [
{ required: true, message: t('views.neData.baseStation.namePlease') },
],
position: [
{ required: true, message: t('views.neData.baseStation.positionPlease') },
],
})
);
/**
* 对话框弹出显示为 新增或者修改
* @param noticeId 网元id, 不传为新增
*/
function fnModalVisibleByEdit(edit?: string | number) {
if (!edit) {
modalStateFrom.resetFields(); //重置表单
modalState.title = t('views.neData.baseStation.addRadio');
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 = t('views.neData.baseStation.editRadio');
modalState.openByEdit = true;
}
}
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
function fnModalOk() {
const [neType, neId] = neTypeAndId.value;
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 = null;
if (neType === 'MME') {
result = from.state
? editMMENbState(neId, from)
: addMMENbState(neId, from);
}
if (neType === 'AMF') {
result = from.state
? editAMFNbState(neId, from)
: addAMFNbState(neId, from);
}
if (result === null) {
return;
}
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;
modalState.openByHistory = false;
modalStateFrom.resetFields();
}
/**导出当前列表 */
function fnExportList() {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.neData.baseStation.exportTip'),
onOk: async () => {
if (modalState.confirmLoading) return;
modalState.confirmLoading = true;
let rows: Record<string, any>[] = [];
// 勾选的网元数据
if (tableState.selectedRowKeys.length > 0) {
rows = tableState.data.filter(item =>
tableState.selectedRowKeys.includes(item.index)
);
} else {
rows = tableState.data;
}
const dataArr: Record<string, any>[] = [];
for (const row of rows) {
let data: any = {};
data[t('views.neData.baseStation.name')] = row.name;
data[t('views.neData.baseStation.position')] = row.position;
data[t('views.neData.baseStation.address')] = row.address;
data[t('views.neData.baseStation.nbName')] = row.nbName;
data[t('views.neData.baseStation.ueNum')] = row.ueNum;
nbState.value.find(item => {
if (item.value === row.state) {
data[t('views.neData.baseStation.state')] = item.label;
}
});
data[t('views.neData.baseStation.time')] = row.time || '-';
dataArr.push(data);
}
// 导出
writeSheet(dataArr, 'Sheet1', {
header: [
t('views.neData.baseStation.name'),
t('views.neData.baseStation.position'),
t('views.neData.baseStation.address'),
t('views.neData.baseStation.nbName'),
t('views.neData.baseStation.ueNum'),
t('views.neData.baseStation.state'),
t('views.neData.baseStation.time'),
],
}).then(fileBlob =>
saveAs(fileBlob, `nb_state_records_export_${Date.now()}.xlsx`)
);
modalState.confirmLoading = false;
tableState.selectedRowKeys = [];
},
});
}
/**对话框弹出历史窗口 */
function fnHistoryView() {
modalState.openByHistory = true;
}
/**对话框表格信息导入弹出窗口 */
function fnModalImportOpen() {
modalState.openByImport = true;
}
function fnModalImportClose() {
modalState.openByImport = false;
fnQueryReset();
}
/**对话框表格信息导入上传 */
function fnModalImportUpload(file: File) {
const hide = message.loading(t('common.loading'), 0);
const [neType, neId] = neTypeAndId.value;
modalState.importMsgArr = [];
// 获取最大index
let index = 0;
if (tableState.data.length <= 0) {
index = 1;
} else {
const last = tableState.data[tableState.data.length - 1];
index = last.index + 1;
}
const reader = new FileReader();
reader.onload = function (e: any) {
const arrayBuffer = e.target.result;
readSheet(arrayBuffer).then(async rows => {
console.log(rows);
if (rows.length <= 0) {
hide();
message.error({
content: t('views.neData.baseStation.importDataEmpty'),
duration: 3,
});
return;
}
// 开始导入
modalState.confirmLoading = true;
for (const row of rows) {
const rowId = row[t('common.rowId')];
const name = row[t('views.neData.baseStation.name')];
const position = row[t('views.neData.baseStation.position')];
const address = row[t('views.neData.baseStation.address')];
let result: any = null;
// 检查IP地址是否定义
const hasAddress = tableState.data.find(
item => item.address === address
);
if (hasAddress) {
// 定义则更新名称位置
if (neType === 'MME') {
result = await editMMENbState(
neId,
Object.assign({}, hasAddress, {
name,
position,
})
);
}
if (neType === 'AMF') {
result = await editAMFNbState(
neId,
Object.assign({}, hasAddress, {
name,
position,
})
);
}
let msg = `${t('common.rowId')}: ${rowId} ${t(
'views.neData.baseStation.editRadio'
)}${t('common.operateErr')}`;
if (result.code === RESULT_CODE_SUCCESS) {
msg = `${t('common.rowId')}: ${rowId} ${t(
'views.neData.baseStation.editRadio'
)}${t('common.operateOk')}`;
}
modalState.importMsgArr.push(msg);
} else {
// 未定义则新增
const form = {
index,
name: `${name}`,
position: `${position}`,
address: `${address}`,
};
if (neType === 'MME') {
result = await addMMENbState(neId, form);
}
if (neType === 'AMF') {
result = await addAMFNbState(neId, form);
}
let msg = `${t('common.rowId')}: ${rowId} ${t(
'views.neData.baseStation.addRadio'
)}${t('common.operateErr')}`;
if (result.code === RESULT_CODE_SUCCESS) {
index += 1;
msg = `${t('common.rowId')}: ${rowId} ${t(
'views.neData.baseStation.addRadio'
)}${t('common.operateOk')}`;
}
modalState.importMsgArr.push(msg);
}
}
hide();
modalState.confirmLoading = false;
});
};
reader.onerror = function (e) {
hide();
console.error('reader file error:', e);
};
reader.readAsArrayBuffer(file);
}
/**对话框表格信息导入模板 */
async function fnModalImportTemplate() {
const baseUrl = import.meta.env.VITE_HISTORY_BASE_URL;
const xlsxUrl = `${
baseUrl.length === 1 && baseUrl.indexOf('/') === 0
? ''
: baseUrl.indexOf('/') === -1
? '/' + baseUrl
: baseUrl
}/nbStateImput`;
const lang = currentLocale.value.split('_')[0];
saveAs(
`${xlsxUrl}/${lang}.xlsx`,
`nb_state_records_import_template_${Date.now()}.xlsx`
);
}
onMounted(() => {
// 获取网元网元列表
useNeInfoStore()
.fnNelist()
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
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="4" :md="6" :xs="24">
<a-form-item
:label="t('views.neData.baseStation.state')"
name="state"
>
<a-select
v-model:value="queryParams.state"
:options="nbState"
:placeholder="t('common.selectPlease')"
@change="fnGetList"
/>
</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-button type="dashed" @click.prevent="fnModalImportOpen()">
<template #icon><ImportOutlined /></template>
{{ t('common.import') }}
</a-button>
<a-button type="dashed" @click.prevent="fnExportList()">
<template #icon><ExportOutlined /></template>
{{ t('common.export') }}
</a-button>
<a-button type="default" @click.prevent="fnHistoryView()">
<template #icon><ContainerOutlined /></template>
{{ t('views.neData.baseStation.history') }}
</a-button>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<div>
<template
v-if="
queryParams.state === undefined || queryParams.state === 'ON'
"
>
{{ t('views.neData.baseStation.online') }}:
<strong style="color: green">{{ stateNum[0] }} </strong>
</template>
<template
v-if="
queryParams.state === undefined || queryParams.state === 'OFF'
"
>
&nbsp;
{{ t('views.neData.baseStation.offline') }}:
<strong style="color: red">
{{ stateNum[1] }}
</strong>
</template>
</div>
<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"
:row-selection="{
type: 'checkbox',
selectedRowKeys: tableState.selectedRowKeys,
onChange: fnTableSelectedRowKeys,
}"
:scroll="{ x: tableColumns.length * 120, y: 'calc(100vh - 480px)' }"
@resizeColumn="(w:number, col:any) => (col.width = w)"
>
<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="t('views.neData.baseStation.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="t('views.neData.baseStation.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-item
v-if="!modalState.from.state"
:label="t('views.neData.baseStation.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>
</ProModal>
<!-- 状态历史框 -->
<HistoryModal
v-model:open="modalState.openByHistory"
:title="t('views.neData.baseStation.history')"
:ne-type="neTypeAndId[0]"
:ne-id="neTypeAndId[1]"
@cancel="fnModalCancel"
></HistoryModal>
<!-- 上传导入表格数据文件框 -->
<UploadModal
:title="t('common.import')"
@upload="fnModalImportUpload"
@close="fnModalImportClose"
v-model:open="modalState.openByImport"
:ext="['.xls', '.xlsx']"
:size="10"
>
<template #default>
<a-row justify="space-between" align="middle">
<a-col :span="12"> </a-col>
<a-col :span="6">
<a-button
type="link"
:title="t('views.system.user.downloadObj')"
@click.prevent="fnModalImportTemplate"
>
{{ t('views.system.user.downloadObj') }}
</a-button>
</a-col>
</a-row>
<a-textarea
:disabled="true"
:hidden="modalState.importMsgArr.length <= 0"
:value="modalState.importMsgArr.join('\r\n')"
:auto-size="{ minRows: 2, maxRows: 8 }"
style="background-color: transparent; color: rgba(0, 0, 0, 0.85)"
/>
</template>
</UploadModal>
</div>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View 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>

View File

@@ -0,0 +1,322 @@
<script setup lang="ts">
import { reactive, ref, onMounted, toRaw } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { message } from 'ant-design-vue/es';
import { SizeType } from 'ant-design-vue/es/config-provider';
import { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/es/table';
import { listUEInfoByIMS } from '@/api/neUser/ims';
import useNeInfoStore from '@/store/modules/neinfo';
import useI18n from '@/hooks/useI18n';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { listIMSSessionList } from '@/api/neData/ims';
const { t } = useI18n();
/**网元参数 */
let neOtions = ref<Record<string, any>[]>([]);
/**查询参数 */
let queryParams = reactive({
/**网元ID */
neId: undefined,
/**IMSI */
imsi: '',
/**msisdn */
msisdn: '',
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
imsi: '',
msisdn: '',
pageNum: 1,
pageSize: 20,
});
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
seached: true,
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: 'IMSI',
dataIndex: 'imsi',
sorter: (a: any, b: any) => Number(a.imsi) - Number(b.imsi),
align: 'center',
width: 150,
},
{
title: 'MSISDN',
dataIndex: 'msisdn',
sorter: (a: any, b: any) => Number(a.msisdn) - Number(b.msisdn),
align: 'center',
width: 150,
},
{
title: 'Barring',
dataIndex: 'barring',
align: 'center',
width: 80,
},
{
title: 'Registration State',
dataIndex: 'regState',
align: 'center',
width: 150,
},
{
title: 'Active Time',
dataIndex: 'activeTime',
align: 'center',
width: 150,
},
{
title: 'IMPU',
dataIndex: 'impu',
align: 'left',
},
];
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 20,
/**默认的每页条数 */
defaultPageSize: 20,
/**指定每页可以显示多少条 */
pageSizeOptions: ['10', '20', '50', '100'],
/**只有一页时是否隐藏分页器 */
hideOnSinglePage: false,
/**是否可以快速跳转至某页 */
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;
queryParams.pageNum = page;
queryParams.pageSize = pageSize;
fnGetList();
},
});
/**表格紧凑型变更操作 */
function fnTableSize({ key }: MenuInfo) {
tableState.size = key as SizeType;
}
/**查询列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (tableState.loading) return;
tableState.loading = true;
if (pageNum) {
queryParams.pageNum = pageNum;
}
listIMSSessionList(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
tablePagination.total = res.data.length;
tableState.data = res.data;
if (
tablePagination.total <=
(queryParams.pageNum - 1) * tablePagination.pageSize &&
queryParams.pageNum !== 1
) {
tableState.loading = false;
fnGetList(queryParams.pageNum - 1);
}
} else {
tablePagination.total = 0;
tableState.data = [];
}
tableState.loading = false;
});
}
onMounted(() => {
// 获取网元网元列表
useNeInfoStore()
.fnNelist()
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
if (res.data.length > 0) {
let arr: Record<string, any>[] = [];
res.data.forEach((v: any) => {
if (v.neType === 'IMS') {
arr.push({ value: v.neId, label: v.neName });
}
});
neOtions.value = arr;
if (arr.length > 0) {
queryParams.neId = arr[0].value;
}
}
} else {
message.warning({
content: t('common.noData'),
duration: 2,
});
}
})
.finally(() => {
// 获取列表数据
fnGetList();
});
});
</script>
<template>
<PageContainer>
<a-card
v-show="tableState.seached"
: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.neUser.ims.neType')" name="neId ">
<a-select
v-model:value="queryParams.neId"
:options="neOtions"
:placeholder="t('common.selectPlease')"
/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="IMSI" name="imsi">
<a-input v-model:value="queryParams.imsi" allow-clear :maxlength="15"></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="MSISDN" name="msisdn">
<a-input v-model:value="queryParams.msisdn" allow-clear></a-input>
</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(1)">
<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> </template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-tooltip>
<template #title>{{ t('common.searchBarText') }}</template>
<a-switch
v-model:checked="tableState.seached"
:checked-children="t('common.switch.show')"
:un-checked-children="t('common.switch.hide')"
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>{{ t('common.reloadText') }}</template>
<a-button type="text" @click.prevent="fnGetList()">
<template #icon><ReloadOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip placement="topRight">
<template #title>{{ t('common.sizeText') }}</template>
<a-dropdown placement="bottomRight" trigger="click">
<a-button type="text">
<template #icon><ColumnHeightOutlined /></template>
</a-button>
<template #overlay>
<a-menu
:selected-keys="[tableState.size as string]"
@click="fnTableSize"
>
<a-menu-item key="default">
{{ t('common.size.default') }}
</a-menu-item>
<a-menu-item key="middle">
{{ t('common.size.middle') }}
</a-menu-item>
<a-menu-item key="small">
{{ t('common.size.small') }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-tooltip>
</a-space>
</template>
<!-- 表格列表 -->
<a-table
class="table"
row-key="imsi"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:pagination="tablePagination"
:scroll="{ x: 1200, y: 400 }"
>
</a-table>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,472 @@
<script setup lang="ts">
import { reactive, ref, onMounted, toRaw } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { ProModal } from 'antdv-pro-modal';
import { message } from 'ant-design-vue/es';
import { SizeType } from 'ant-design-vue/es/config-provider';
import { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/es/table';
import { listSMFSubList } from '@/api/neData/smf';
import useNeInfoStore from '@/store/modules/neinfo';
import useI18n from '@/hooks/useI18n';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
const { t } = useI18n();
/**网元参数 */
let neOtions = ref<Record<string, any>[]>([]);
/**查询参数 */
let queryParams = reactive({
/**网元ID */
neId: undefined,
/**IMSI */
imsi: '',
/**msisdn */
msisdn: '',
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 50,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
imsi: '',
msisdn: '',
pageNum: 1,
pageSize: 50,
});
tablePagination.current = 1;
tablePagination.pageSize = 50;
fnGetList();
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
seached: true,
data: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: 'IMSI',
dataIndex: 'imsi',
align: 'left',
sorter: (a: any, b: any) => Number(a.imsi) - Number(b.imsi),
customRender(opt) {
const idx = opt.value.lastIndexOf('-');
if (idx != -1) {
return opt.value.substring(idx + 1);
}
return opt.value;
},
width: 150,
},
{
title: 'MSISDN',
dataIndex: 'msisdn',
align: 'left',
sorter: (a: any, b: any) => Number(a.msisdn) - Number(b.msisdn),
customRender(opt) {
const idx = opt.value.lastIndexOf('-');
if (idx != -1) {
return opt.value.substring(idx + 1);
}
return opt.value;
},
width: 150,
},
{
title: 'RAT Type',
dataIndex: 'ratType',
align: 'left',
width: 100,
},
{
title: 'APN/DNN List',
dataIndex: 'pduSessionInfo',
align: 'left',
customRender(opt) {
if (opt.value) {
let arr = [];
for (const v of opt.value) {
if (v.dnn) {
arr.push(v.dnn);
}
}
return arr.sort().join(',');
}
return '';
},
width: 200,
},
{
title: t('common.operate'),
key: 'imsi',
align: 'left',
width: 100,
},
{
title: 'Remark',
dataIndex: 'remark',
align: 'left',
},
];
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 50,
/**只有一页时是否隐藏分页器 */
hideOnSinglePage: false,
/**是否可以快速跳转至某页 */
showQuickJumper: true,
/**是否可以改变 pageSize */
showSizeChanger: false,
/**数据总数 */
total: 0,
showTotal: (total: number) => t('common.tablePaginationTotal', { total }),
onChange: (page: number, pageSize: number) => {
tablePagination.current = page;
tablePagination.pageSize = pageSize;
queryParams.pageNum = page;
queryParams.pageSize = pageSize;
fnGetList();
},
});
/**表格紧凑型变更操作 */
function fnTableSize({ key }: MenuInfo) {
tableState.size = key as SizeType;
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**详情框是否显示 */
openByView: boolean;
/**新增框或修改框是否显示 */
openByEdit: boolean;
/**标题 */
title: string;
/**表单数据 */
from: Record<string, any>;
/**确定按钮 loading */
confirmLoading: boolean;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
openByView: false,
openByEdit: false,
title: '在线信息',
from: {
imsi: '',
msisdn: '',
pduSessionInfo: undefined,
ratType: '',
},
confirmLoading: false,
});
/**
* 对话框弹出显示为 查看
* @param row 单行记录信息
*/
function fnModalVisibleByVive(row: Record<string, any>) {
if (!row.imsi) {
message.error(t('common.getInfoFail'), 2);
return;
}
const imsiIdx = row.imsi.lastIndexOf('-');
if (imsiIdx != -1) {
row.imsi = row.imsi.substring(imsiIdx + 1);
}
const msisdnIdx = row.msisdn.lastIndexOf('-');
if (msisdnIdx != -1) {
row.msisdn = row.msisdn.substring(msisdnIdx + 1);
}
modalState.from = Object.assign(modalState.from, row);
modalState.title = `${row.imsi}`;
modalState.openByView = true;
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalCancel() {
modalState.openByEdit = false;
modalState.openByView = false;
}
/**查询列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (tableState.loading) return;
tableState.loading = true;
if (pageNum) {
queryParams.pageNum = pageNum;
}
listSMFSubList(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
const { total, rows } = res.data;
tablePagination.total = total;
tableState.data = rows;
if (
tablePagination.total <=
(queryParams.pageNum - 1) * tablePagination.pageSize &&
queryParams.pageNum !== 1
) {
tableState.loading = false;
fnGetList(queryParams.pageNum - 1);
}
} else {
tablePagination.total = 0;
tableState.data = [];
}
tableState.loading = false;
});
}
onMounted(() => {
// 获取网元网元列表
useNeInfoStore()
.fnNelist()
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
if (res.data.length > 0) {
let arr: Record<string, any>[] = [];
res.data.forEach((v: any) => {
if (v.neType === 'SMF') {
arr.push({ value: v.neId, label: v.neName });
}
});
neOtions.value = arr;
if (arr.length > 0) {
queryParams.neId = arr[0].value;
}
}
} else {
message.warning({
content: t('common.noData'),
duration: 2,
});
}
})
.finally(() => {
// 获取列表数据
fnGetList();
});
});
</script>
<template>
<PageContainer>
<a-card
v-show="tableState.seached"
: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.neUser.ue.neType')" name="neId ">
<a-select
v-model:value="queryParams.neId"
:options="neOtions"
:placeholder="t('common.selectPlease')"
/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="IMSI" name="imsi">
<a-input v-model:value="queryParams.imsi" allow-clear></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="MSISDN" name="msisdn">
<a-input v-model:value="queryParams.msisdn" allow-clear></a-input>
</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(1)">
<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> </template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-tooltip>
<template #title>{{ t('common.searchBarText') }}</template>
<a-switch
v-model:checked="tableState.seached"
:checked-children="t('common.switch.show')"
:un-checked-children="t('common.switch.hide')"
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>{{ t('common.reloadText') }}</template>
<a-button type="text" @click.prevent="fnGetList()">
<template #icon><ReloadOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip placement="topRight">
<template #title>{{ t('common.sizeText') }}</template>
<a-dropdown placement="bottomRight" trigger="click">
<a-button type="text">
<template #icon><ColumnHeightOutlined /></template>
</a-button>
<template #overlay>
<a-menu
:selected-keys="[tableState.size as string]"
@click="fnTableSize"
>
<a-menu-item key="default">
{{ t('common.size.default') }}
</a-menu-item>
<a-menu-item key="middle">
{{ t('common.size.middle') }}
</a-menu-item>
<a-menu-item key="small">
{{ t('common.size.small') }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-tooltip>
</a-space>
</template>
<!-- 表格列表 -->
<a-table
class="table"
row-key="id"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:pagination="tablePagination"
:scroll="{ x: true, y: 400 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'imsi'">
<a-space :size="8" align="center">
<a-tooltip>
<template #title>{{ t('common.viewText') }}</template>
<a-button
type="link"
@click.prevent="fnModalVisibleByVive(record)"
>
<template #icon><ProfileOutlined /></template>
</a-button>
</a-tooltip>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 详情框 -->
<ProModal
:drag="true"
:width="800"
:open="modalState.openByView"
:title="modalState.title"
@cancel="fnModalCancel"
:footer="false"
>
<a-form layout="horizontal" labelAlign="left" :labelWrap="false">
<a-row>
<a-col :lg="8" :md="8" :xs="24">
<a-form-item label="IMSI" name="imsi">
{{ modalState.from.imsi }}
</a-form-item>
</a-col>
<a-col :lg="8" :md="8" :xs="24">
<a-form-item label="MSISDN" name="msisdn">
{{ modalState.from.msisdn }}
</a-form-item>
</a-col>
<a-col :lg="8" :md="8" :xs="24">
<a-form-item label="RAT Type" name="ratType">
{{ modalState.from.ratType }}
</a-form-item>
</a-col>
</a-row>
<a-descriptions
:title="v.dnn"
:column="2"
size="small"
bordered
v-for="v in modalState.from.pduSessionInfo"
:key="v.dnn"
>
<a-descriptions-item label="PDU Session ID">
{{ v.pduSessionID }}
</a-descriptions-item>
<a-descriptions-item label="User Plane State">
{{ v.upState }}
</a-descriptions-item>
<a-descriptions-item label="IPV4">{{ v.ipv4 }}</a-descriptions-item>
<a-descriptions-item label="IPV6">{{ v.ipv6 }}</a-descriptions-item>
<a-descriptions-item label="TAI">{{ v.tai }}</a-descriptions-item>
<a-descriptions-item label="SST-SD">
{{ v.sstSD }}</a-descriptions-item
>
<a-descriptions-item label="UPF N3 IP">
{{ v.upfN3IP }}
</a-descriptions-item>
<a-descriptions-item label="RAN N3 IP">
{{ v.ranN3IP }}
</a-descriptions-item>
<a-descriptions-item label="Create Time">
{{ v.activeTime }}
</a-descriptions-item>
</a-descriptions>
</a-form>
</ProModal>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,943 @@
<script setup lang="ts">
import { reactive, ref, onMounted, toRaw } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { ProModal } from 'antdv-pro-modal';
import {
message,
Modal,
Form,
TableColumnsType,
notification,
} from 'ant-design-vue';
import { SizeType } from 'ant-design-vue/es/config-provider';
import { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import UploadModal from '@/components/UploadModal/index.vue';
import {
addUDMVOIP,
batchAddUDMVOIP,
batchDelUDMVOIP,
delUDMVOIP,
exportUDMVOIP,
importUDMVOIP,
listUDMVOIP,
resetUDMVOIP,
} from '@/api/neData/udm_voip';
import useNeInfoStore from '@/store/modules/neinfo';
import useI18n from '@/hooks/useI18n';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { saveAs } from 'file-saver';
import { uploadFileToNE } from '@/api/tool/file';
const { t } = useI18n();
/**网元参数 */
let neOtions = ref<Record<string, any>[]>([]);
/**查询参数 */
let queryParams = reactive({
/**网元ID */
neId: undefined,
/**用户名 */
username: '',
/**排序字段 */
sortField: 'username',
/**排序方式 */
sortOrder: 'asc',
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
username: '',
sortField: 'username',
sortOrder: 'asc',
});
fnGetList(1);
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'small',
seached: true,
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns = ref<TableColumnsType>([
{
title: t('views.neData.udmVOIP.username'),
dataIndex: 'username',
sorter: true,
align: 'left',
resizable: true,
width: 250,
minWidth: 100,
maxWidth: 300,
},
{
title: t('common.operate'),
key: 'id',
align: 'left',
},
]);
/**表格字段列排序 */
let tableColumnsDnd = ref<TableColumnsType>([]);
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 20,
/**默认的每页条数 */
defaultPageSize: 20,
/**指定每页可以显示多少条 */
pageSizeOptions: ['10', '20', '50', '100'],
/**只有一页时是否隐藏分页器 */
hideOnSinglePage: false,
/**是否可以快速跳转至某页 */
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;
queryParams.pageNum = page;
queryParams.pageSize = pageSize;
fnGetList();
},
});
/**表格分页、排序、筛选变化时触发操作, 排序方式,取值为 ascend descend */
function fnTableChange(pagination: any, filters: any, sorter: any, extra: any) {
const { field, order } = sorter;
if (order) {
queryParams.sortField = field;
queryParams.sortOrder = order.replace('end', '');
} else {
queryParams.sortOrder = 'asc';
}
fnGetList(1);
}
/**表格紧凑型变更操作 */
function fnTableSize({ key }: MenuInfo) {
tableState.size = key as SizeType;
}
/**表格多选 */
function fnTableSelectedRowKeys(keys: (string | number)[]) {
tableState.selectedRowKeys = keys;
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**新增框或修改框是否显示 */
openByEdit: boolean;
/**标题 */
title: string;
/**表单数据 */
from: Record<string, any>;
/**是否批量操作 */
isBatch: boolean;
/**操作类型 */
type: 'delete' | 'add';
/**确定按钮 loading */
confirmLoading: boolean;
/**更新加载数据按钮 loading */
loadDataLoading: boolean;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
openByEdit: false,
title: 'UDMVOIP',
from: {
num: 1,
username: undefined,
password: undefined,
},
isBatch: false,
type: 'add',
confirmLoading: false,
loadDataLoading: false,
});
/**对话框内表单属性和校验规则 */
const modalStateFrom = Form.useForm(
modalState.from,
reactive({
num: [
{
required: true,
message: t('views.neData.common.batchNum'),
},
],
username: [
{ required: true, message: t('views.neData.udmVOIP.usernamePlease') },
],
password: [
{ required: true, message: t('views.neData.udmVOIP.passwordPlease') },
],
})
);
/**
* 对话框弹出显示为 新增或者修改
* @param noticeId 网元id, 不传为新增
*/
function fnModalVisibleByEdit(row?: Record<string, any>) {
modalState.isBatch = false;
if (!row) {
modalStateFrom.resetFields(); //重置表单
modalState.title = t('views.neData.udmVOIP.addTitle');
modalState.openByEdit = true;
modalState.type = 'add';
}
}
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
function fnModalOk() {
const from = JSON.parse(JSON.stringify(modalState.from));
from.neId = queryParams.neId || '-';
from.username = `${from.username}`;
// 校验规则
let validateArr = ['username', 'password'];
if (modalState.isBatch) {
validateArr.push('num');
if (modalState.type === 'delete') {
validateArr = ['num', 'username'];
}
}
modalStateFrom
.validate(validateArr)
.then(e => {
modalState.confirmLoading = true;
const hide = message.loading(t('common.loading'), 0);
// 根据类型选择函数
let result: any = null;
if (modalState.isBatch) {
if (modalState.type === 'add') {
result = batchAddUDMVOIP(
from.neId,
{ username: from.username, password: from.password },
from.num
);
}
if (modalState.type === 'delete') {
result = batchDelUDMVOIP(from.neId, from.username, from.num);
}
} else {
if (modalState.type === 'add') {
result = addUDMVOIP(from.neId, {
username: from.username,
password: from.password,
});
}
}
result
.then((res: any) => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 3,
});
fnModalCancel();
fnGetList();
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
hide();
modalState.confirmLoading = false;
});
})
.catch(e => {
message.error(t('common.errorFields', { num: e.errorFields.length }), 3);
});
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalCancel() {
modalState.type = 'add';
modalState.isBatch = false;
modalState.openByEdit = false;
modalStateFrom.resetFields();
}
/**
* 对话框弹出显示为 批量操作
* @param type 类型
*/
function fnModalVisibleByBatch(type: 'delete' | 'add') {
modalStateFrom.resetFields(); //重置表单
modalState.isBatch = true;
modalState.type = type;
if (type === 'add') {
modalState.title = t('views.neData.common.batchAddText');
modalState.openByEdit = true;
}
if (type === 'delete') {
modalState.title = t('views.neData.common.batchDelText');
modalState.openByEdit = true;
}
}
/**
* 记录删除
* @param username 网元编号ID
*/
function fnRecordDelete(username: string) {
const neID = queryParams.neId;
if (!neID) return;
let msg = username;
if (username === '0') {
msg = `${tableState.selectedRowKeys[0]}... ${tableState.selectedRowKeys.length}`;
username = tableState.selectedRowKeys.join(',');
}
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.neData.udmVOIP.delTip', { num: msg }),
onOk() {
const hide = message.loading(t('common.loading'), 0);
delUDMVOIP(neID, username)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('common.operateOk'), 3);
fnGetList();
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
hide();
});
},
});
}
/**列表导出 */
function fnExportList(type: string) {
const neId = queryParams.neId;
if (!neId) return;
const hide = message.loading(t('common.loading'), 0);
exportUDMVOIP(Object.assign({ type: type }, queryParams))
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 2,
});
saveAs(res.data, `UDM_VOIP_${neId}_${Date.now()}.${type}`);
} else {
message.error({
content: `${res.msg}`,
duration: 2,
});
}
})
.finally(() => {
hide();
});
}
/**查询列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (tableState.loading) return;
tableState.loading = true;
if (pageNum) {
queryParams.pageNum = pageNum;
tablePagination.current = pageNum;
}
listUDMVOIP(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
const { total, rows } = res.data;
tablePagination.total = total;
tableState.data = rows;
} else {
tableState.data = [];
}
tableState.loading = false;
});
}
/**重新加载数据 */
function fnLoadData() {
const neId = queryParams.neId;
if (tableState.loading || !neId) return;
modalState.loadDataLoading = true;
tablePagination.total = 0;
tableState.data = [];
tableState.loading = true; // 表格loading
resetUDMVOIP(neId).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
const num = res.data;
const timerS = Math.ceil(+num / 800) + 3;
notification.success({
message: t('views.neData.common.loadData'),
description: t('views.neData.common.loadDataTip', {
num,
timer: timerS,
}),
duration: timerS,
});
// 延迟10s后关闭loading刷新列表
setTimeout(() => {
modalState.loadDataLoading = false;
tableState.loading = false; // 表格loading
fnQueryReset();
}, timerS * 1000);
} else {
modalState.loadDataLoading = false;
tableState.loading = false; // 表格loading
fnQueryReset();
message.error({
content: t('common.getInfoFail'),
duration: 3,
});
}
});
}
/**对话框表格信息导入对象信息状态类型 */
type ModalUploadImportStateType = {
/**是否显示 */
open: boolean;
/**标题 */
title: string;
/**是否上传中 */
loading: boolean;
/**上传结果信息 */
msg: string;
/**含失败信息 */
hasFail: boolean;
};
/**对话框表格信息导入对象信息状态 */
let uploadImportState: ModalUploadImportStateType = reactive({
open: false,
title: t('components.UploadModal.uploadTitle'),
loading: false,
msg: '',
hasFail: false,
});
/**对话框表格信息导入弹出窗口 */
function fnModalUploadImportOpen() {
uploadImportState.msg = '';
uploadImportState.hasFail = false;
uploadImportState.loading = false;
uploadImportState.open = true;
}
/**对话框表格信息导入关闭窗口 */
function fnModalUploadImportClose() {
uploadImportState.open = false;
fnGetList();
}
/**对话框表格信息导入上传 */
function fnModalUploadImportUpload(file: File) {
const neID = queryParams.neId;
if (!neID) {
return Promise.reject('Unknown network element');
}
const hide = message.loading(t('common.loading'), 0);
uploadImportState.loading = true;
uploadFileToNE('UDM', neID, file, 5)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
return importUDMVOIP({
neId: neID,
uploadPath: res.data,
});
}
return res;
})
.then(res => {
if (!res) return;
uploadImportState.msg = res.msg;
const regex = /fail num: (\d+)/;
const match = res.msg.match(regex);
if (match) {
const failNum = Number(match[1]);
uploadImportState.hasFail = failNum > 0;
} else {
uploadImportState.hasFail = false;
}
})
.finally(() => {
hide();
uploadImportState.loading = false;
});
}
/**对话框表格信息导入模板 */
function fnModalDownloadImportTemplate() {
const hide = message.loading(t('common.loading'), 0);
const baseUrl = import.meta.env.VITE_HISTORY_BASE_URL;
const templateUrl = `${
baseUrl.length === 1 && baseUrl.indexOf('/') === 0
? ''
: baseUrl.indexOf('/') === -1
? '/' + baseUrl
: baseUrl
}/neDataImput`;
saveAs(
`${templateUrl}/udm_voip_template.txt`,
`import_udmvoip_template_${Date.now()}.txt`
);
hide();
}
onMounted(() => {
// 获取网元网元列表
useNeInfoStore()
.fnNelist()
.then(res => {
if (res.code === RESULT_CODE_SUCCESS && res.data?.length > 0) {
let arr: Record<string, any>[] = [];
res.data.forEach((v: any) => {
if (v.neType === 'UDM') {
arr.push({ value: v.neId, label: v.neName });
}
});
neOtions.value = arr;
if (arr.length > 0) {
queryParams.neId = arr[0].value;
}
} else {
message.warning({
content: t('common.noData'),
duration: 2,
});
}
})
.finally(() => {
// 获取列表数据
fnGetList();
});
});
</script>
<template>
<PageContainer>
<a-card
v-show="tableState.seached"
: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="UDM" name="neId ">
<a-select
v-model:value="queryParams.neId"
:options="neOtions"
:placeholder="t('common.selectPlease')"
@change="fnGetList(1)"
/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item
:label="t('views.neData.udmVOIP.username')"
name="username"
>
<a-input
v-model:value="queryParams.username"
allow-clear
:placeholder="t('common.inputPlease')"
></a-input>
</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"
danger
:disabled="tableState.selectedRowKeys.length <= 0"
:loading="modalState.confirmLoading"
@click.prevent="fnRecordDelete('0')"
>
<template #icon><DeleteOutlined /></template>
{{ t('views.neData.common.checkDel') }}
</a-button>
<a-dropdown trigger="click">
<a-button>
{{ t('views.neData.common.batchOper') }}
<DownOutlined />
</a-button>
<template #overlay>
<a-menu @click="({ key }:any) => fnModalVisibleByBatch(key)">
<a-menu-item key="add">
<PlusOutlined />
{{ t('views.neData.common.batchAddText') }}
</a-menu-item>
<a-menu-item key="delete">
<DeleteOutlined />
{{ t('views.neData.common.batchDelText') }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<a-popconfirm
placement="topRight"
:title="t('views.neData.common.loadDataConfirm')"
:ok-text="t('common.ok')"
:cancel-text="t('common.cancel')"
:disabled="modalState.loadDataLoading"
@confirm="fnLoadData"
>
<a-button
type="dashed"
danger
:disabled="modalState.loadDataLoading"
:loading="modalState.loadDataLoading"
>
<template #icon><SyncOutlined /></template>
{{ t('views.neData.common.loadData') }}
</a-button>
</a-popconfirm>
<a-button type="dashed" @click.prevent="fnModalUploadImportOpen">
<template #icon><ImportOutlined /></template>
{{ t('common.import') }}
</a-button>
<a-popconfirm
placement="topRight"
:title="t('views.neData.udmVOIP.exportTip')"
ok-text="TXT"
ok-type="default"
@confirm="fnExportList('txt')"
>
<a-button type="dashed">
<template #icon><ExportOutlined /></template>
{{ t('common.export') }}
</a-button>
</a-popconfirm>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-tooltip placement="topRight">
<template #title>
{{
tableState.seached
? t('common.switch.show')
: t('common.switch.hide')
}}
</template>
<a-switch
v-model:checked="tableState.seached"
:checked-children="t('common.searchBarText')"
:un-checked-children="t('common.searchBarText')"
size="small"
/>
</a-tooltip>
<TableColumnsDnd
cache-id="udmVOIPData"
:columns="tableColumns"
v-model:columns-dnd="tableColumnsDnd"
></TableColumnsDnd>
<a-tooltip placement="topRight">
<template #title>{{ t('common.reloadText') }}</template>
<a-button type="text" @click.prevent="fnGetList()">
<template #icon><ReloadOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip placement="topRight">
<template #title>{{ t('common.sizeText') }}</template>
<a-dropdown placement="bottomRight" trigger="click">
<a-button type="text">
<template #icon><ColumnHeightOutlined /></template>
</a-button>
<template #overlay>
<a-menu
:selected-keys="[tableState.size as string]"
@click="fnTableSize"
>
<a-menu-item key="default">
{{ t('common.size.default') }}
</a-menu-item>
<a-menu-item key="middle">
{{ t('common.size.middle') }}
</a-menu-item>
<a-menu-item key="small">
{{ t('common.size.small') }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-tooltip>
</a-space>
</template>
<!-- 表格列表 -->
<a-table
class="table"
row-key="username"
:columns="tableColumnsDnd"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:pagination="tablePagination"
:scroll="{ y: 'calc(100vh - 480px)' }"
@change="fnTableChange"
@resizeColumn="(w:number, col:any) => (col.width = w)"
:row-selection="{
type: 'checkbox',
selectedRowKeys: tableState.selectedRowKeys,
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column?.key === 'id'">
<a-space :size="8" align="center">
<a-tooltip>
<template #title>{{ t('common.deleteText') }}</template>
<a-button
type="link"
@click.prevent="fnRecordDelete(record.username)"
>
<template #icon>
<DeleteOutlined />
</template>
</a-button>
</a-tooltip>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 新增框或修改框 -->
<ProModal
:drag="true"
:width="520"
: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"
>
<!--批量删除-->
<template v-if="modalState.isBatch && modalState.type === 'delete'">
<a-form-item
:label="t('views.neData.common.batchNum')"
name="num"
v-bind="modalStateFrom.validateInfos.num"
>
<a-input-number
v-model:value="modalState.from.num"
style="width: 100%"
:min="1"
:max="500"
placeholder="<=500"
></a-input-number>
</a-form-item>
<a-form-item
:label="
modalState.isBatch
? t('views.neData.udmVOIP.startUsername')
: t('views.neData.udmVOIP.username')
"
name="username"
v-bind="modalStateFrom.validateInfos.username"
>
<a-input-number
v-model:value="modalState.from.username"
style="width: 100%"
:min="4"
:maxlangth="16"
:placeholder="t('views.neData.udmVOIP.username')"
>
</a-input-number>
</a-form-item>
</template>
<template v-else>
<!--批量数-->
<a-form-item
v-if="modalState.isBatch"
:label="t('views.neData.common.batchNum')"
name="num"
v-bind="modalStateFrom.validateInfos.num"
>
<a-input-number
v-model:value="modalState.from.num"
style="width: 100%"
:min="1"
:max="500"
placeholder="<=500"
></a-input-number>
</a-form-item>
<a-form-item
:label="
modalState.isBatch
? t('views.neData.udmVOIP.startUsername')
: t('views.neData.udmVOIP.username')
"
name="username"
v-bind="modalStateFrom.validateInfos.username"
>
<a-input-number
v-model:value="modalState.from.username"
style="width: 100%"
:min="4"
:maxlength="16"
:placeholder="t('views.neData.udmVOIP.username')"
>
</a-input-number>
</a-form-item>
<a-form-item
:label="t('views.neData.udmVOIP.password')"
name="password"
v-bind="modalStateFrom.validateInfos.password"
>
<a-input-password
v-model:value="modalState.from.password"
style="width: 100%"
:min="4"
:max="64"
:placeholder="t('views.neData.udmVOIP.password')"
>
</a-input-password>
</a-form-item>
</template>
</a-form>
</ProModal>
<!-- 上传导入表格数据文件框 -->
<UploadModal
:title="uploadImportState.title"
:loading="uploadImportState.loading"
@upload="fnModalUploadImportUpload"
@close="fnModalUploadImportClose"
v-model:open="uploadImportState.open"
:ext="['.txt']"
>
<template #default>
<a-row justify="space-between" align="middle">
<a-col :span="12"> </a-col>
<a-col>
<a-button
type="link"
:title="t('views.neData.common.importTemplate')"
@click.prevent="fnModalDownloadImportTemplate"
>
{{ t('views.neData.common.importTemplate') }}
</a-button>
</a-col>
</a-row>
<a-textarea
:disabled="true"
:hidden="!uploadImportState.msg"
:value="uploadImportState.msg"
:auto-size="{ minRows: 2, maxRows: 8 }"
style="background-color: transparent; color: rgba(0, 0, 0, 0.85)"
/>
</template>
</UploadModal>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

File diff suppressed because it is too large Load Diff