ref: v3变更,,根据核心网过滤显示网元

This commit is contained in:
TsMask
2025-09-16 19:41:57 +08:00
parent 847517fdfe
commit 254c49d894
81 changed files with 27708 additions and 29 deletions

View File

@@ -16,15 +16,15 @@ import {
export const CORE_NE_PATH = '/core-ne';
/**
* 获取当前coreUid
* @returns coreUid
* 获取当前coreInfo
* @returns coreInfo
*/
export function current() {
return sessionGetJSON(CACHE_SESSION_CORE) || {};
}
/**
* 获取当前coreUid
* 获取当前coreInfo
* @returns coreUid
*/
export function changeCurrent(v: Record<string, any>) {

View File

@@ -120,7 +120,16 @@ const menuData = computed(() => {
rootRoute.children = rootRouteChildren;
}
}
const neTypes = neStore.getNeSelectOtions.map(v => v.value);
// 过滤网元限定菜单
let neTypes = [];
// 多核心网切换菜单显示
if (appStore.serverType === APP_SERVER_TYPE_M) {
neTypes = neStore.getCoreDataNeSelectOtions.map(v => v.value);
} else {
neTypes = neStore.getNeSelectOtions.map(v => v.value);
}
let routes = clearMenuItem(router.getRoutes());
routes = routerStore.clearMenuItemByNeList(routes, neTypes);
const { menuData } = getMenuData(routes);

View File

@@ -20,7 +20,7 @@ export const constantRoutes: RouteRecordRaw[] = [
path: '/index',
name: 'Index',
meta: { title: 'router.index', icon: 'icon-pcduan' },
component: () => import('@/views/index.vue'),
component: () => import('@/views/index/index.vue'),
},
{
path: '/account',

View File

@@ -3,7 +3,7 @@ import {
CACHE_LOCAL_I18N,
CACHE_SESSION_CRYPTO_API,
} from '@/constants/cache-keys-constants';
import { RESULT_CODE_EXCEPTION, RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { delAccessToken, delRefreshToken } from '@/plugins/auth-token';
import { parseUrlPath } from '@/plugins/file-static-url';
import { localGet, localSet } from '@/utils/cache-local-utils';
@@ -131,7 +131,7 @@ const useAppStore = defineStore('app', {
if (localI18n == null || (!this.i18nOpen && this.i18nDefault)) {
localSet(CACHE_LOCAL_I18N, this.i18nDefault);
}
};
}
},
},
});

View File

@@ -84,6 +84,7 @@ const useCoreStore = defineStore('core', {
});
}
// 当前未选择时
this.coreSelectOtions.unshift(this.globalDefaultSelect);
if (!this.currentSelect) {
this.setCurrent(this.globalDefaultSelect);
}

View File

@@ -5,6 +5,7 @@ import {
} from '@/constants/result-constants';
import { listAllNeInfo } from '@/api/ne/neInfo';
import { parseDataToOptions } from '@/utils/parse-tree-utils';
import { currentCoreUid } from '@/hooks/useCoreUid';
/**网元信息类型 */
type Ne = {
@@ -14,6 +15,10 @@ type Ne = {
neCascaderOptions: Record<string, any>[];
/**选择器单级父类型 */
neSelectOtions: Record<string, any>[];
/**Core数据级联options树结构 coreUid */
coreDataNeCascaderOptions: Map<string, Record<string, any>[]>;
/**Core选择器单级父类型 coreUid */
coreDataNeSelectOtions: Map<string, Record<string, any>[]>;
};
const useNeStore = defineStore('ne', {
@@ -21,6 +26,10 @@ const useNeStore = defineStore('ne', {
list: [],
neCascaderOptions: [],
neSelectOtions: [],
/**Core数据级联options树结构 coreUid */
coreDataNeCascaderOptions: new Map(),
/**Core选择器单级父类型 coreUid */
coreDataNeSelectOtions: new Map(),
}),
getters: {
/**
@@ -40,13 +49,31 @@ const useNeStore = defineStore('ne', {
return state.neCascaderOptions;
},
/**
* 选择器单级父类型
* Core选择器单级父类型
* @param state 内部属性不用传入
* @returns 选择options
*/
getNeSelectOtions(state) {
return state.neSelectOtions;
},
/**
* Core获取级联options树结构
* @param state 内部属性不用传入
* @returns 级联options
*/
getCoreDataNeCascaderOptions(state) {
const coreUid = currentCoreUid();
return state.coreDataNeCascaderOptions.get(coreUid) || [];
},
/**
* 选择器单级父类型
* @param state 内部属性不用传入
* @returns 选择options
*/
getCoreDataNeSelectOtions(state) {
const coreUid = currentCoreUid();
return state.coreDataNeSelectOtions.get(coreUid) || [];
},
},
actions: {
// 刷新网元列表
@@ -70,11 +97,12 @@ const useNeStore = defineStore('ne', {
});
if (res.code === RESULT_CODE_SUCCESS) {
// 原始列表
this.list = JSON.parse(JSON.stringify(res.data));
const originalList = JSON.parse(JSON.stringify(res.data));
this.list = originalList;
// 转级联数据
const options = parseDataToOptions(
res.data,
originalList,
'neType',
'neName',
'neUid'
@@ -88,6 +116,39 @@ const useNeStore = defineStore('ne', {
value: item.value,
};
});
// 根据coreUid分组
let groupCore: Record<string, any[]> = {};
for (const element of originalList) {
if (element.coreId !== 0 && element.coreUid) {
if (groupCore[element.coreUid]) {
groupCore[element.coreUid].push(element);
} else {
groupCore[element.coreUid] = [element];
}
}
}
// 转Core数据级联options树结构
for (const coreUid in groupCore) {
const arr = groupCore[coreUid];
// 转级联数据
const cascaderOptions = parseDataToOptions(
arr,
'neType',
'neName',
'neUid'
);
this.coreDataNeCascaderOptions.set(coreUid, cascaderOptions);
// 转选择器单级父类型
const selectOtions = cascaderOptions.map(item => {
return {
label: item.label,
value: item.value,
};
});
this.coreDataNeSelectOtions.set(coreUid, selectOtions);
}
}
return res;
},

File diff suppressed because it is too large Load Diff

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,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>Overview 核心网 概览</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>Overview 核心网 概览</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>Overview 核心网 概览</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>Overview 核心网 概览</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>Overview 核心网 概览</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>Overview 核心网 概览</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>Overview 核心网 概览</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>指标概览 卡片展示</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>数据大屏 实时展示</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>拓扑图 状态流动</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -8,7 +8,7 @@ onMounted(() => {});
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>核心网列表</h1>
<h1>UE接入事件 实时</h1>
</a-card>
</PageContainer>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>网元VM资源 周期获取</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>Overview 核心网 概览</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>Overview 核心网 概览</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>Overview 核心网 概览</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>Overview 核心网 概览</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>Overview 核心网 概览</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>Overview 核心网 概览</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>Overview 核心网 概览</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>Overview 核心网 概览</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>Overview 核心网 概览</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>Overview 核心网 概览</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>Overview 核心网 概览</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,85 @@
import { delNeConfigData } from '@/api/ne/neConfig';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { message } from 'ant-design-vue';
import { reactive } from 'vue';
/**
* 批量删除array
* @param param 父级传入 { t, neTypeSelect, fnActiveConfigNode }
* @returns
*/
export default function useArrayBatch({
t,
neTypeSelect,
fnActiveConfigNode,
}: any) {
/**状态属性 */
const batchState = reactive({
open: false,
loading: false, //批量删除
paramName: '',
startIndex: 1,
num: 1,
});
/**对话框表格信息导入弹出窗口 */
function modalBatchOpen(paramName: string) {
batchState.paramName = paramName;
batchState.open = true;
}
function modalBatchClose() {
if (batchState.loading) {
message.error({
content: 'Delete is in progress, please wait for it to complete',
duration: 3,
});
return;
}
batchState.open = false;
batchState.loading = false;
batchState.startIndex = 1;
batchState.num = 1;
fnActiveConfigNode('#');
}
async function modalBatchOk() {
let okNum = 0;
let failNum = 0;
const endIndex = batchState.startIndex + batchState.num - 1;
for (let i = endIndex; i >= batchState.startIndex; i--) {
const res = await delNeConfigData({
neType: neTypeSelect.value[0],
neUid: neTypeSelect.value[1],
paramName: batchState.paramName,
loc: `${i}`,
});
if (res.code === RESULT_CODE_SUCCESS) {
okNum++;
} else {
failNum++;
break;
}
}
if (okNum > 0) {
message.success({
content: `Successfully deleted ${okNum} items`,
duration: 3,
});
}
if (failNum > 0) {
message.error({
content: `Delete failed, please check the index range`,
duration: 3,
});
}
modalBatchClose();
}
return {
batchState,
modalBatchOpen,
modalBatchClose,
modalBatchOk,
};
}

View File

@@ -0,0 +1,207 @@
import { addNeConfigData, editNeConfigData } from '@/api/ne/neConfig';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { readSheet } from '@/utils/execl-utils';
import { message } from 'ant-design-vue';
import { reactive } from 'vue';
import saveAs from 'file-saver';
/**
* 导入文件加array
* @param param 父级传入 { t, neTypeSelect, arrayState, fnActiveConfigNode }
* @returns
*/
export default function useArrayImport({
t,
neTypeSelect,
arrayState,
fnActiveConfigNode,
}: any) {
/**网元导入模板解析 */
const m: Record<string, any> = {
AMF: {
imeiWhitelist: {
filename: 'import_amf_imeiWhitelist_template',
fileetx: '.xlsx',
itemKey: 'index',
item: (row: Record<string, any>) => {
const index = row['Index'] || 0;
return {
imeiPrefixValue: `${row['IMEI Prefix']}`,
index: parseInt(index),
};
},
},
whitelist: {
filename: 'import_amf_whitelist_template',
fileetx: '.xlsx',
itemKey: 'index',
item: (row: Record<string, any>) => {
const index = row['Index'] || 0;
return {
imsiValue: `${row['IMSI Value']}`,
imeiValue: `${row['IMEI Value/Prefix']}`,
index: parseInt(index),
};
},
},
},
MME: {
white_list: {
filename: 'import_mme_imeiWhitelist_template',
fileetx: '.xlsx',
itemKey: 'index',
item: (row: Record<string, any>) => {
const index = row['Index'] || 0;
return {
imei: `${row['IMEI']}`,
index: parseInt(index),
};
},
},
},
};
/**状态属性 */
const importState = reactive({
open: false,
msgArr: [] as string[],
loading: false, //开始导入
itemKey: '', // 解析item的key
item: null as any, // 解析item方法
paramName: '',
filename: '',
fileetx: '',
});
/**对话框表格信息导入弹出窗口 */
function modalImportOpen(neType: string, paramName: string) {
const tmpM = m[neType][paramName];
importState.itemKey = tmpM.itemKey;
importState.item = tmpM.item;
importState.paramName = paramName;
importState.filename = tmpM.filename;
importState.fileetx = tmpM.fileetx;
importState.open = true;
}
function modalImportClose() {
if (importState.loading) {
message.error({
content: 'Import is in progress, please wait for it to complete',
duration: 3,
});
return;
}
importState.open = false;
importState.msgArr = [];
importState.loading = false;
fnActiveConfigNode('#');
}
/**对话框表格信息导入上传 */
async function modalImportUpload(file: File) {
const hide = message.loading(t('common.loading'), 0);
importState.msgArr = [];
// 获取最大index
let index = 0;
if (arrayState.columnsData.length <= 0) {
index = 0;
} else {
const last = arrayState.columnsData[arrayState.columnsData.length - 1];
index = last.index.value + 1;
}
const reader = new FileReader();
reader.onload = function (e: any) {
const arrayBuffer = e.target.result;
readSheet(arrayBuffer).then(async rows => {
if (rows.length <= 0) {
hide();
message.error({
content: t('views.neData.baseStation.importDataEmpty'),
duration: 3,
});
return;
}
// 开始导入
importState.loading = true;
for (const row of rows) {
const rowItem = importState.item(row);
const rowKey = rowItem[importState.itemKey] || -1;
let result: any = null;
// 检查index是否定义
const has = arrayState.columnsData.find(
(item: any) => item[importState.itemKey].value === rowKey
);
if (has) {
// 已定义则更新
rowItem.index = has.index.value;
result = await editNeConfigData({
neType: neTypeSelect.value[0],
neUid: neTypeSelect.value[1],
paramName: importState.paramName,
paramData: rowItem,
loc: `${rowItem.index}`,
});
let msg = `index:${rowItem.index} update fail`;
if (result.code === RESULT_CODE_SUCCESS) {
msg = `index:${rowItem.index} update success`;
}
importState.msgArr.push(msg);
} else {
// 未定义则新增
result = await addNeConfigData({
neType: neTypeSelect.value[0],
neUid: neTypeSelect.value[1],
paramName: importState.paramName,
paramData: Object.assign(rowItem, { index }),
loc: `${index}`,
});
let msg = `index:${index} add fail`;
if (result.code === RESULT_CODE_SUCCESS) {
msg = `index:${index} add success`;
index += 1;
}
importState.msgArr.push(msg);
}
}
hide();
importState.loading = false;
});
};
reader.onerror = function (e) {
hide();
console.error('reader file error:', e);
};
reader.readAsArrayBuffer(file);
}
/**对话框表格信息导入模板 */
function modalImportTemplate() {
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}/${importState.filename}${importState.fileetx}`,
`${importState.filename}_${Date.now()}${importState.fileetx}`
);
hide();
}
return {
importState,
modalImportOpen,
modalImportClose,
modalImportUpload,
modalImportTemplate,
};
}

View File

@@ -0,0 +1,411 @@
import {
addNeConfigData,
delNeConfigData,
editNeConfigData,
} from '@/api/ne/neConfig';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { Modal, message } from 'ant-design-vue/es';
import { SizeType } from 'ant-design-vue/es/config-provider';
import { reactive, watch } from 'vue';
/**
* 参数配置array类型
* @param param 父级传入 { t, treeState, neTypeSelect, fnActiveConfigNode, ruleVerification, modalState, fnModalCancel}
* @returns
*/
export default function useConfigArray({
t,
treeState,
neTypeSelect,
fnActiveConfigNode,
ruleVerification,
modalState,
fnModalCancel,
}: any) {
/**多列列表状态类型 */
type ArrayStateType = {
/**紧凑型 */
size: SizeType;
/**多列嵌套记录字段 */
columns: Record<string, any>[];
/**表格字段列排序 */
columnsDnd: Record<string, any>[];
/**多列记录数据 */
columnsData: Record<string, any>[];
/**多列嵌套展开key */
arrayChildExpandKeys: any[];
/**多列记录数据 */
data: Record<string, any>[];
/**多列记录规则 */
dataRule: Record<string, any>;
};
/**多列列表状态 */
let arrayState: ArrayStateType = reactive({
size: 'small',
columns: [],
columnsDnd: [],
columnsData: [],
arrayChildExpandKeys: [],
data: [],
dataRule: {},
});
/**多列表编辑 */
function arrayEdit(rowIndex: Record<string, any>) {
const item = arrayState.data.find((s: any) => s.key === rowIndex.value);
if (!item) return;
const from = arrayInitEdit(item, arrayState.dataRule);
// 处理信息
const row: Record<string, any> = {};
for (const v of from.record) {
if (Array.isArray(v.array)) {
continue;
}
row[v.name] = Object.assign({}, v);
}
// 特殊SMF-upfid选择
if (neTypeSelect.value[0] === 'SMF' && Reflect.has(row, 'upfId')) {
const v = row.upfId.value;
if (typeof v === 'string') {
if (v === '') {
row.upfId.value = [];
} else if (v.includes(';')) {
row.upfId.value = v.split(';');
} else if (v.includes(',')) {
row.upfId.value = v.split(',');
} else {
row.upfId.value = [v];
}
}
}
modalState.from = row;
modalState.type = 'arrayEdit';
modalState.title = `${treeState.selectNode.paramDisplay} ${from.title}`;
modalState.key = from.key;
modalState.data = from.record.filter((v: any) => !Array.isArray(v.array));
modalState.open = true;
// 关闭嵌套
arrayState.arrayChildExpandKeys = [];
}
/**多列表编辑关闭 */
function arrayEditClose() {
arrayState.arrayChildExpandKeys = [];
fnModalCancel();
}
/**多列表编辑确认 */
function arrayEditOk(from: Record<string, any>) {
const loc = `${from['index']['value']}`;
// 特殊SMF-upfid选择
if (neTypeSelect.value[0] === 'SMF' && Reflect.has(from, 'upfId')) {
const v = from.upfId.value;
if (Array.isArray(v)) {
from.upfId.value = v.join(';');
}
}
// 遍历提取属性和值
let data: Record<string, any> = {};
for (const key in from) {
// 子嵌套的不插入
if (from[key]['array']) {
continue;
}
// 检查规则
const [ok, msg] = ruleVerification(from[key]);
if (!ok) {
message.warning({
content: `${msg}`,
duration: 3,
});
return;
}
data[key] = from[key]['value'];
}
// 发送
const hide = message.loading(t('common.loading'), 0);
editNeConfigData({
neType: neTypeSelect.value[0],
neUid: neTypeSelect.value[1],
paramName: treeState.selectNode.paramName,
paramData: data,
loc: loc,
})
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('views.ne.neConfig.updateItem', {
num: modalState.title,
}),
duration: 3,
});
fnActiveConfigNode('#');
} else {
message.warning({
content: t('views.ne.neConfig.updateItemErr'),
duration: 3,
});
}
})
.finally(() => {
hide();
arrayEditClose();
});
}
/**多列表删除单行 */
function arrayDelete(rowIndex: Record<string, any>) {
const loc = `${rowIndex.value}`;
const title = `${treeState.selectNode.paramDisplay} Index-${loc}`;
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.ne.neConfig.delItemTip', {
num: title,
}),
onOk() {
delNeConfigData({
neType: neTypeSelect.value[0],
neUid: neTypeSelect.value[1],
paramName: treeState.selectNode.paramName,
loc: loc,
}).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('views.ne.neConfig.delItemOk', {
num: title,
}),
duration: 2,
});
arrayEditClose();
fnActiveConfigNode('#');
} else {
message.error({
content: `${res.msg}`,
duration: 2,
});
}
});
},
});
}
/**多列表新增单行 */
function arrayAdd() {
const from = arrayInitAdd(arrayState.data, arrayState.dataRule);
// 处理信息
const row: Record<string, any> = {};
for (const v of from.record) {
if (Array.isArray(v.array)) {
continue;
}
row[v.name] = Object.assign({}, v);
}
// 特殊SMF-upfid选择
if (neTypeSelect.value[0] === 'SMF' && Reflect.has(row, 'upfId')) {
const v = row.upfId.value;
if (typeof v === 'string') {
if (v === '') {
row.upfId.value = [];
} else if (v.includes(';')) {
row.upfId.value = v.split(';');
} else if (v.includes(',')) {
row.upfId.value = v.split(',');
} else {
row.upfId.value = [v];
}
}
}
modalState.from = row;
modalState.type = 'arrayAdd';
modalState.title = `${treeState.selectNode.paramDisplay} ${from.title}`;
modalState.key = from.key;
modalState.data = from.record.filter((v: any) => !Array.isArray(v.array));
modalState.open = true;
}
/**多列表新增单行确认 */
function arrayAddOk(from: Record<string, any>) {
// 特殊SMF-upfid选择
if (neTypeSelect.value[0] === 'SMF' && Reflect.has(from, 'upfId')) {
const v = from.upfId.value;
if (Array.isArray(v)) {
from.upfId.value = v.join(';');
}
}
// 遍历提取属性和值
let data: Record<string, any> = {};
for (const key in from) {
// 子嵌套的不插入
if (from[key]['array']) {
continue;
}
// 检查规则
const [ok, msg] = ruleVerification(from[key]);
if (!ok) {
message.warning({
content: `${msg}`,
duration: 3,
});
return;
}
data[key] = from[key]['value'];
}
// 发送
const hide = message.loading(t('common.loading'), 0);
addNeConfigData({
neType: neTypeSelect.value[0],
neUid: neTypeSelect.value[1],
paramName: treeState.selectNode.paramName,
paramData: data,
loc: `${from['index']['value']}`,
})
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('views.ne.neConfig.addItemOk', {
num: modalState.title,
}),
duration: 3,
});
fnActiveConfigNode('#');
} else {
message.warning({
content: t('views.ne.neConfig.addItemErr'),
duration: 3,
});
}
})
.finally(() => {
hide();
arrayEditClose();
});
}
/**多列表编辑行数据初始化 */
function arrayInitEdit(data: Record<string, any>, dataRule: any) {
const dataFrom = data.record;
const ruleFrom = Object.assign({}, JSON.parse(JSON.stringify(dataRule)));
for (const row of ruleFrom.record) {
// 子嵌套的不初始
if (row.array) {
row.value = [];
continue;
}
// 查找项的值
const item = dataFrom.find((s: any) => s.name === row.name);
if (!item) {
continue;
}
// 可选的
row.optional = 'true';
// 根据规则类型转值
if (['enum', 'int'].includes(row.type)) {
row.value = Number(item.value);
} else if ('bool' === row.type) {
row.value = Boolean(item.value);
} else {
row.value = item.value;
}
}
ruleFrom.key = data.key;
ruleFrom.title = data.title;
return ruleFrom;
}
/**多列表新增行数据初始化 */
function arrayInitAdd(data: any[], dataRule: any) {
// 有数据时取得最后的index
let dataLastIndex = 0;
if (data.length !== 0) {
const lastFrom = Object.assign(
{},
JSON.parse(JSON.stringify(data.at(-1)))
);
if (lastFrom.record.length > 0) {
dataLastIndex = parseInt(lastFrom.key);
dataLastIndex += 1;
}
}
const ruleFrom = Object.assign({}, JSON.parse(JSON.stringify(dataRule)));
for (const row of ruleFrom.record) {
// 子嵌套的不初始
if (row.array) {
row.value = [];
continue;
}
// 可选的
row.optional = 'true';
// index值
if (row.name === 'index') {
let newIndex =
dataLastIndex !== 0 ? dataLastIndex : parseInt(row.value);
if (isNaN(newIndex)) {
newIndex = 0;
}
row.value = newIndex;
ruleFrom.key = newIndex;
ruleFrom.title = `Index-${newIndex}`;
continue;
}
// 根据规则类型转值
if (['enum', 'int'].includes(row.type)) {
row.value = Number(row.value);
}
if ('bool' === row.type) {
row.value = Boolean(row.value);
}
// 特殊SMF-upfid选择
if (neTypeSelect.value[0] === 'SMF' && row.name === 'upfId') {
const v = row.value;
if (typeof v === 'string') {
if (v === '') {
row.value = [];
} else if (v.includes(';')) {
row.value = v.split(';');
} else if (v.includes(',')) {
row.value = v.split(',');
} else {
row.value = [v];
}
}
}
}
return ruleFrom;
}
// 监听表格字段列排序变化关闭展开
watch(
() => arrayState.columnsDnd,
() => {
arrayEditClose();
}
);
return {
arrayState,
arrayEdit,
arrayEditClose,
arrayEditOk,
arrayDelete,
arrayAdd,
arrayAddOk,
arrayInitEdit,
arrayInitAdd,
};
}

View File

@@ -0,0 +1,352 @@
import {
addNeConfigData,
editNeConfigData,
delNeConfigData,
} from '@/api/ne/neConfig';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { Modal, message } from 'ant-design-vue/es';
import { SizeType } from 'ant-design-vue/es/config-provider';
import { nextTick, reactive } from 'vue';
/**
* 参数配置array类型的嵌套array
* @param param 父级传入 { t, treeState, neTypeSelect, fnActiveConfigNode, ruleVerification, modalState, arrayState, arrayInitEdit, arrayInitAdd, arrayEditClose}
* @returns
*/
export default function useConfigArrayChild({
t,
treeState,
neTypeSelect,
fnActiveConfigNode,
ruleVerification,
modalState,
arrayState,
arrayInitEdit,
arrayInitAdd,
arrayEditClose,
}: any) {
/**多列嵌套列表状态类型 */
type ArrayChildStateType = {
/**标题 */
title: string;
/**层级index */
loc: string;
/**紧凑型 */
size: SizeType;
/**多列嵌套记录字段 */
columns: Record<string, any>[];
/**表格字段列排序 */
columnsDnd: Record<string, any>[];
/**多列记录数据 */
columnsData: Record<string, any>[];
/**多列嵌套记录数据 */
data: Record<string, any>[];
/**多列嵌套记录规则 */
dataRule: Record<string, any>;
};
/**多列嵌套表格状态 */
let arrayChildState: ArrayChildStateType = reactive({
title: '',
loc: '',
size: 'small',
columns: [],
columnsDnd: [],
columnsData: [],
data: [],
dataRule: {},
});
/**多列表展开嵌套行 */
function arrayChildExpand(
indexRow: Record<string, any>,
row: Record<string, any>
) {
const loc = indexRow.value;
if (arrayChildState.loc === `${loc}/${row.name}`) {
arrayChildState.loc = '';
arrayState.arrayChildExpandKeys = [];
return;
}
arrayChildState.loc = '';
arrayState.arrayChildExpandKeys = [];
const from = Object.assign({}, JSON.parse(JSON.stringify(row)));
// 无数据时
if (!Array.isArray(from.value)) {
from.value = [];
}
const dataArr = Object.freeze(from.value);
const ruleArr = Object.freeze(from.array);
// 列表项数据
const dataArray: Record<string, any>[] = [];
for (const item of dataArr) {
const index = item['index'];
let record: Record<string, any>[] = [];
for (const key of Object.keys(item)) {
// 规则为准
for (const rule of ruleArr) {
if (rule['name'] === key) {
const ruleItem = Object.assign({ optional: 'true' }, rule, {
value: item[key],
});
record.push(ruleItem);
break;
}
}
}
// dataArray.push(record);
dataArray.push({ title: `Index-${index}`, key: index, record });
}
arrayChildState.data = dataArray;
// 无数据时,用于新增
arrayChildState.dataRule = {
title: `Index-0`,
key: 0,
record: ruleArr,
};
// 列表数据
const columnsData: Record<string, any>[] = [];
for (const v of arrayChildState.data) {
const row: Record<string, any> = {};
for (const item of v.record) {
row[item.name] = item;
}
columnsData.push(row);
}
arrayChildState.columnsData = columnsData;
// 列表字段
const columns: Record<string, any>[] = [];
for (const rule of arrayChildState.dataRule.record) {
columns.push({
title: rule.display,
dataIndex: rule.name,
align: 'left',
resizable: true,
width: 50,
minWidth: 50,
maxWidth: 250,
});
}
columns.push({
title: t('common.operate'),
dataIndex: 'index',
key: 'index',
align: 'center',
fixed: 'right',
width: 100,
});
arrayChildState.columns = columns;
nextTick(() => {
// 设置展开key
arrayState.arrayChildExpandKeys = [indexRow];
// 层级标识
arrayChildState.loc = `${loc}/${from['name']}`;
// 设置展开列表标题
arrayChildState.title = `${from['display']}`;
});
}
/**多列表嵌套行编辑 */
function arrayChildEdit(rowIndex: Record<string, any>) {
const item = arrayChildState.data.find(
(s: any) => s.key === rowIndex.value
);
if (!item) return;
const from = arrayInitEdit(item, arrayChildState.dataRule);
// 处理信息
const row: Record<string, any> = {};
for (const v of from.record) {
if (Array.isArray(v.array)) {
continue;
}
row[v.name] = Object.assign({}, v);
}
modalState.from = row;
modalState.type = 'arrayChildEdit';
modalState.title = `${arrayChildState.title} ${from.title}`;
modalState.key = from.key;
modalState.data = from.record.filter((v: any) => !Array.isArray(v.array));
modalState.open = true;
}
/**多列表嵌套行编辑确认 */
function arrayChildEditOk(from: Record<string, any>) {
const loc = `${arrayChildState.loc}/${from['index']['value']}`;
let data: Record<string, any> = {};
for (const key in from) {
// 子嵌套的不插入
if (from[key]['array']) {
continue;
}
// 检查规则
const [ok, msg] = ruleVerification(from[key]);
if (!ok) {
message.warning({
content: `${msg}`,
duration: 3,
});
return;
}
data[key] = from[key]['value'];
}
// 发送
const hide = message.loading(t('common.loading'), 0);
editNeConfigData({
neType: neTypeSelect.value[0],
neUid: neTypeSelect.value[1],
paramName: treeState.selectNode.paramName,
paramData: data,
loc,
})
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('views.ne.neConfig.updateItem', {
num: modalState.title,
}),
duration: 3,
});
fnActiveConfigNode('#');
} else {
message.warning({
content: t('views.ne.neConfig.updateItemErr'),
duration: 3,
});
}
})
.finally(() => {
hide();
arrayEditClose();
});
}
/**多列表嵌套行删除单行 */
function arrayChildDelete(rowIndex: Record<string, any>) {
const index = rowIndex.value;
const loc = `${arrayChildState.loc}/${index}`;
const title = `${arrayChildState.title} Index-${index}`;
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.ne.neConfig.delItemTip', {
num: title,
}),
onOk() {
delNeConfigData({
neType: neTypeSelect.value[0],
neUid: neTypeSelect.value[1],
paramName: treeState.selectNode.paramName,
loc,
}).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('views.ne.neConfig.delItemOk', {
num: title,
}),
duration: 2,
});
arrayEditClose();
fnActiveConfigNode('#');
} else {
message.error({
content: `${res.msg}`,
duration: 2,
});
}
});
},
});
}
/**多列表嵌套行新增单行 */
function arrayChildAdd() {
const from = arrayInitAdd(arrayChildState.data, arrayChildState.dataRule);
// 处理信息
const row: Record<string, any> = {};
for (const v of from.record) {
if (Array.isArray(v.array)) {
continue;
}
row[v.name] = Object.assign({}, v);
}
modalState.from = row;
modalState.type = 'arrayChildAdd';
modalState.title = `${arrayChildState.title} ${from.title}`;
modalState.key = from.key;
modalState.data = from.record.filter((v: any) => !Array.isArray(v.array));
modalState.open = true;
}
/**多列表新增单行确认 */
function arrayChildAddOk(from: Record<string, any>) {
const loc = `${arrayChildState.loc}/${from['index']['value']}`;
let data: Record<string, any> = {};
for (const key in from) {
// 子嵌套的不插入
if (from[key]['array']) {
continue;
}
// 检查规则
const [ok, msg] = ruleVerification(from[key]);
if (!ok) {
message.warning({
content: `${msg}`,
duration: 3,
});
return;
}
data[key] = from[key]['value'];
}
// 发送
const hide = message.loading(t('common.loading'), 0);
addNeConfigData({
neType: neTypeSelect.value[0],
neUid: neTypeSelect.value[1],
paramName: treeState.selectNode.paramName,
paramData: data,
loc,
})
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('views.ne.neConfig.addItemOk', {
num: modalState.title,
}),
duration: 3,
});
fnActiveConfigNode('#');
} else {
message.warning({
content: t('views.ne.neConfig.addItemErr'),
duration: 3,
});
}
})
.finally(() => {
hide();
arrayEditClose();
});
}
return {
arrayChildState,
arrayChildExpand,
arrayChildEdit,
arrayChildEditOk,
arrayChildDelete,
arrayChildAdd,
arrayChildAddOk,
};
}

View File

@@ -0,0 +1,152 @@
import { editNeConfigData } from '@/api/ne/neConfig';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { SizeType } from 'ant-design-vue/es/config-provider';
import { message } from 'ant-design-vue/es';
import { reactive, toRaw } from 'vue';
/**
* list类型参数处理
* @param param 父级传入 {t, treeState, neTypeSelect, ruleVerification}
* @returns
*/
export default function useConfigList({
t,
treeState,
neTypeSelect,
ruleVerification,
}: any) {
/**单列表状态类型 */
type ListStateType = {
/**紧凑型 */
size: SizeType;
/**单列记录字段 */
columns: Record<string, any>[];
/**单列记录数据 */
data: Record<string, any>[];
/**编辑行记录 */
editRecord: Record<string, any>;
/**确认提交等待 */
confirmLoading: boolean;
};
/**单列表状态 */
let listState: ListStateType = reactive({
size: 'small',
columns: [
{
title: 'Key',
dataIndex: 'display',
align: 'left',
width: '30%',
},
{
title: 'Value',
dataIndex: 'value',
align: 'left',
width: '70%',
},
],
data: [],
confirmLoading: false,
editRecord: {},
});
/**单列表编辑 */
function listEdit(row: Record<string, any>) {
if (
listState.confirmLoading ||
['read-only', 'read', 'ro'].includes(row.access)
) {
return;
}
listState.editRecord = Object.assign({}, row);
}
/**单列表编辑关闭 */
function listEditClose() {
listState.confirmLoading = false;
listState.editRecord = {};
}
/**单列表编辑确认 */
function listEditOk() {
if (listState.confirmLoading) return;
const from = toRaw(listState.editRecord);
// 检查规则
const [ok, msg] = ruleVerification(from);
if (!ok) {
message.warning({
content: `${msg}`,
duration: 3,
});
return;
}
// 发送
listState.confirmLoading = true;
const hide = message.loading(t('common.loading'), 0);
editNeConfigData({
neType: neTypeSelect.value[0],
neUid: neTypeSelect.value[1],
paramName: treeState.selectNode.paramName,
paramData: {
[from['name']]: from['value'],
},
})
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('views.ne.neConfig.updateValue', {
num: from['display'],
}),
duration: 3,
});
// 改变表格数据
const item = listState.data.find(
(item: Record<string, any>) => from['name'] === item['name']
);
if (item) {
Object.assign(item, listState.editRecord);
}
} else {
message.warning({
content: t('views.ne.neConfig.updateValueErr'),
duration: 3,
});
}
})
.finally(() => {
hide();
listState.confirmLoading = false;
listState.editRecord = {};
});
}
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 10,
/**默认的每页条数 */
defaultPageSize: 10,
/**指定每页可以显示多少条 */
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;
},
});
return { tablePagination, listState, listEdit, listEditClose, listEditOk };
}

View File

@@ -0,0 +1,190 @@
import { getNeConfigData } from '@/api/ne/neConfig';
import { regExpIPv4, regExpIPv6, validURL } from '@/utils/regular-utils';
import { ref } from 'vue';
/**
* 参数公共函数
* @param param 父级传入 {t}
* @returns
*/
export default function useOptions({ t }: any) {
/**规则校验 */
function ruleVerification(row: Record<string, any>): (string | boolean)[] {
let result = [true, ''];
const type = row.type;
const value = row.value;
const filter = row.filter;
const display = row.display;
// 子嵌套的不检查
if (row.array) {
return result;
}
// 可选的同时没有值不检查
if (row.optional === 'true' && !value) {
return result;
}
switch (type) {
case 'int':
// filter: "0~128"
if (filter) {
let filterArr = ['0', '1'];
if (filter.indexOf('-') !== -1) {
filterArr = filter.split('-');
} else if (filter.indexOf('~') !== -1) {
filterArr = filter.split('~');
}
const minInt = parseInt(filterArr[0]);
const maxInt = parseInt(filterArr[1]);
const valueInt = parseInt(value);
if (valueInt < minInt || valueInt > maxInt) {
return [
false,
t('views.ne.neConfig.requireInt', {
display,
filter,
}),
];
}
}
break;
case 'ipv4':
if (!regExpIPv4.test(value)) {
return [false, t('views.ne.neConfig.requireIpv4', { display })];
}
break;
case 'ipv6':
if (!regExpIPv6.test(value)) {
return [false, t('views.ne.neConfig.requireIpv6', { display })];
}
break;
case 'enum':
if (filter && filter.indexOf('{') === 1) {
let filterJson: Record<string, any> = {};
try {
filterJson = JSON.parse(filter); //string---json
} catch (error) {
console.error(error);
}
if (!Object.keys(filterJson).includes(`${value}`)) {
return [false, t('views.ne.neConfig.requireEnum', { display })];
}
}
break;
case 'bool':
// filter: '{"0":"false", "1":"true"}'
if (filter && filter.indexOf('{') === 1) {
let filterJson: Record<string, any> = {};
try {
filterJson = JSON.parse(filter); //string---json
} catch (error) {
console.error(error);
}
if (!Object.values(filterJson).includes(`${value}`)) {
return [false, t('views.ne.neConfig.requireBool', { display })];
}
}
break;
case 'string':
// filter: "0~128"
// 字符串长度判断
if (filter) {
try {
let rule: RegExp = new RegExp('^.*$');
if (filter.indexOf('-') !== -1) {
const filterArr = filter.split('-');
rule = new RegExp(
'^.{' + filterArr[0] + ',' + filterArr[1] + '}$'
);
} else if (filter.indexOf('~') !== -1) {
const filterArr = filter.split('~');
rule = new RegExp(
'^\\S{' + filterArr[0] + ',' + filterArr[1] + '}$'
);
}
if (!rule.test(value)) {
return [
false,
t('views.ne.neConfig.requireString', {
display,
}),
];
}
} catch (error) {
console.error(error);
}
}
// 字符串http判断
if (value.startsWith('http')) {
try {
if (!validURL(value)) {
return [
false,
t('views.ne.neConfig.requireString', {
display,
}),
];
}
} catch (error) {
console.error(error);
}
}
break;
case 'regex':
// filter: "^[0-9]{3}$"
if (filter) {
try {
let regex = new RegExp(filter);
if (!regex.test(value)) {
return [
false,
t('views.ne.neConfig.requireString', {
display,
}),
];
}
} catch (error) {
console.error(error);
}
}
break;
default:
return [false, t('views.ne.neConfig.requireUn', { display })];
}
return result;
}
/**upfId可选择 */
const smfByUPFIdOptions = ref<{ value: string; label: string }[]>([]);
/**加载smf配置的upfId */
function smfByUPFIdLoadData(neUid: string) {
getNeConfigData({
neType: 'SMF',
neUid: neUid,
paramName: 'upfConfig',
}).then(res => {
smfByUPFIdOptions.value = [];
for (const s of res.data) {
smfByUPFIdOptions.value.push({
value: s.id,
label: s.id,
});
}
});
}
return {
ruleVerification,
smfByUPFIdLoadData,
smfByUPFIdOptions,
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,848 @@
<script setup lang="ts">
import { reactive, onMounted, toRaw, defineAsyncComponent, ref } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { message, Modal } 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 useI18n from '@/hooks/useI18n';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import useNeStore from '@/store/modules/ne';
import { listNeInfo, delNeInfo } from '@/api/ne/neInfo';
import { stateNeInfo } from '@/api/ne/neAction';
import useDictStore from '@/store/modules/dict';
import useNeOptions from '@/views/ne/info/hooks/useNeOptions';
import useCoreStore from '@/store/modules/core';
const { getDict } = useDictStore();
const neStore = useNeStore();
const coreStore = useCoreStore();
const { t } = useI18n();
const {
fnNeStart,
fnNeRestart,
fnNeStop,
fnNeReload,
fnNeLogFile,
parseResouresUsage,
} = useNeOptions();
// 异步加载组件
const EditModal = defineAsyncComponent(
() => import('@/views/ne/info/components/EditModal.vue')
);
const OAMModal = defineAsyncComponent(
() => import('@/views/ne/info/components/OAMModal.vue')
);
// 软件授权上传
const LicenseEditModal = defineAsyncComponent(
() => import('@/views/ne/info/components/LicenseEditModal.vue')
);
// 配置备份文件导入
const BackConfModal = defineAsyncComponent(
() => import('@/views/ne/info/components/BackConfModal.vue')
);
const backConf = ref(); // 引用句柄,取导出函数
/**字典数据 */
let dict: {
/**网元信息状态 */
neInfoStatus: DictType[];
} = reactive({
neInfoStatus: [],
});
/**查询参数 */
let queryParams = reactive({
coreUid: coreStore.currentCoreUid,
/**网元类型 */
neType: '',
/**带状态信息 */
bandStatus: true,
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
neType: '',
pageNum: 1,
pageSize: 20,
});
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: Record<string, any>[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
/**勾选记录 */
selectedRows: Record<string, any>[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
seached: true,
data: [],
selectedRowKeys: [],
selectedRows: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: t('views.ne.common.neUid'),
dataIndex: 'neUid',
align: 'left',
width: 100,
},
{
title: t('views.ne.common.neType'),
dataIndex: 'neType',
align: 'left',
width: 100,
},
{
title: t('views.ne.common.neName'),
dataIndex: 'neName',
align: 'left',
width: 150,
},
{
title: t('views.ne.common.ipAddr'),
dataIndex: 'ipAddr',
align: 'left',
width: 150,
},
{
title: t('views.ne.common.port'),
dataIndex: 'port',
align: 'left',
width: 100,
},
{
title: t('views.ne.common.serialNum'),
dataIndex: 'serialNum',
align: 'left',
width: 120,
},
{
title: t('views.ne.common.expiryDate'),
dataIndex: 'expiryDate',
align: 'left',
width: 150,
},
{
title: t('views.ne.common.ueNumber'),
dataIndex: 'ueNumber',
align: 'left',
customRender(opt) {
if (['UDM', 'AMF', 'MME'].includes(opt.record.neType)) {
return opt.value;
}
return '';
},
width: 120,
},
{
title: t('views.ne.common.nbNumber'),
dataIndex: 'nbNumber',
align: 'left',
customRender(opt) {
if (['AMF', 'MME'].includes(opt.record.neType)) {
return opt.value;
}
return '';
},
width: 120,
},
{
title: t('views.ne.neInfo.state'),
dataIndex: 'status',
key: 'status',
align: 'left',
width: 100,
},
{
title: t('common.operate'),
key: 'id',
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;
}
/**表格多选 */
function fnTableSelectedRowKeys(keys: (string | number)[], rows: any[]) {
tableState.selectedRowKeys = keys;
tableState.selectedRows = rows.map(item => {
return {
id: item.id,
neUid: item.neUid,
neType: item.neType,
};
});
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**软件授权上传框是否显示 */
openByLicense: boolean;
/**配置备份框是否显示 */
openByBackConf: boolean;
/**OAM文件配置框是否显示 */
openByOAM: boolean;
/**新增框或修改框是否显示 */
openByEdit: boolean;
/**新增框或修改框ID */
id: number;
neUid: string;
neType: string;
/**确定按钮 loading */
confirmLoading: boolean;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
openByLicense: false,
openByBackConf: false,
openByOAM: false,
openByEdit: false,
id: 0,
neUid: '',
neType: '',
confirmLoading: false,
});
/**
* 对话框弹出显示为 新增或者修改
* @param noticeId 网元id, 不传为新增
*/
function fnModalVisibleByEdit(row?: Record<string, any>) {
if (!row) {
modalState.id = 0;
modalState.neUid = '';
modalState.neType = '';
} else {
modalState.id = row.id;
modalState.neUid = row.neUid;
modalState.neType = row.neType;
}
modalState.openByEdit = !modalState.openByEdit;
}
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
function fnModalEditOk(from: Record<string, any>) {
// 新增时刷新列表
if (!from.neUid) {
fnGetList();
return;
}
// 编辑时局部更新信息
reloadRowInfo(from);
}
/**局部更新信息 */
function reloadRowInfo(row: Record<string, any>) {
stateNeInfo(row.neUid)
.then(res => {
// 找到编辑更新的网元
const item = tableState.data.find(s => s.id === row.id);
if (item && res.code === RESULT_CODE_SUCCESS) {
item.neType = row.neType;
item.neUid = row.neUid;
item.neName = row.neName;
item.ipAddr = row.ipAddr;
item.port = row.port;
if (res.data.online) {
item.status = '1';
if (res.data.standby) {
item.status = '3';
}
} else {
item.status = '0';
}
Object.assign(item.serverState, res.data);
const resouresUsage = parseResouresUsage(item.serverState);
Reflect.set(item, 'resoures', resouresUsage);
}
})
.finally(() => {
neStore.fnNelistRefresh();
});
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalEditCancel() {
modalState.neUid = '';
modalState.neType = '';
modalState.openByEdit = false;
modalState.openByOAM = false;
modalState.openByBackConf = false;
}
/**
* 记录删除
* @param id 编号
*/
function fnRecordDelete(id: string) {
if (modalState.confirmLoading) return;
let msg = t('views.ne.neInfo.delTip');
if (id === '0') {
msg = `${msg} ...${tableState.selectedRowKeys.length}`;
}
Modal.confirm({
title: t('common.tipTitle'),
content: msg,
onOk() {
modalState.confirmLoading = true;
const hide = message.loading(t('common.loading'), 0);
let reqArr: any = [];
if (id === '0') {
const ids = tableState.selectedRowKeys.join(',');
delNeInfo({ id: ids });
} else {
tableState.data.forEach(item => {
if (item.id === id) {
reqArr.push(
delNeInfo({
id: item.id,
})
);
}
});
}
Promise.all(reqArr)
.then(resArr => {
if (resArr.every((item: any) => item.code === RESULT_CODE_SUCCESS)) {
message.success(t('common.operateOk'), 3);
// 过滤掉删除的id
tableState.data = tableState.data.filter(item => {
if (tableState.selectedRowKeys.length > 0) {
return !tableState.selectedRowKeys.includes(item.id);
} else {
return item.id !== id;
}
});
// 刷新缓存
neStore.fnNelistRefresh();
} else {
message.error({
content: t('common.operateErr'),
duration: 3,
});
}
})
.finally(() => {
hide();
modalState.confirmLoading = false;
});
},
});
}
/**
* 记录多项选择
*/
function fnRecordMore(type: string | number, row: Record<string, any>) {
switch (type) {
case 'delete':
fnRecordDelete(row.id);
break;
case 'start':
fnNeStart(row, () => reloadRowInfo(row));
break;
case 'restart':
fnNeRestart(row, () => reloadRowInfo(row));
break;
case 'stop':
fnNeStop(row, () => reloadRowInfo(row));
break;
case 'reload':
fnNeReload(row);
break;
case 'log':
fnNeLogFile(row);
break;
case 'oam':
modalState.neUid = row.neUid;
modalState.neType = row.neType;
modalState.openByOAM = !modalState.openByOAM;
break;
case 'license':
modalState.id = row.id;
modalState.neUid = row.neUid;
modalState.neType = row.neType;
modalState.openByLicense = !modalState.openByLicense;
break;
case 'backConfExport':
backConf.value.exportConf(row.neUid, row.neType);
break;
case 'backConfImport':
modalState.neUid = row.neUid;
modalState.neType = row.neType;
modalState.openByBackConf = !modalState.openByBackConf;
break;
default:
console.warn(type);
break;
}
}
/**查询列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (tableState.loading) return;
tableState.loading = true;
if (pageNum) {
queryParams.pageNum = pageNum;
}
listNeInfo(toRaw(queryParams))
.then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data.rows)) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
const { total, rows } = res.data;
tablePagination.total = total;
// 遍历处理资源情况数值
tableState.data = rows.map((item: any) => {
let resouresUsage = {
sysDiskUsage: 0,
sysMemUsage: 0,
sysCpuUsage: 0,
nfCpuUsage: 0,
};
const neState = item.serverState;
if (neState) {
resouresUsage = parseResouresUsage(neState);
} else {
item.serverState = { online: false };
}
Reflect.set(item, 'resoures', resouresUsage);
return item;
});
}
tableState.loading = false;
})
.finally(() => {
// 刷新缓存的网元信息
neStore.fnNelistRefresh();
});
}
onMounted(() => {
// 初始字典数据
Promise.allSettled([getDict('ne_info_status')]).then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.neInfoStatus = resArr[0].value;
}
});
// 获取列表数据
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.ne.common.neType')" name="neType ">
<a-auto-complete
v-model:value="queryParams.neType"
:options="neStore.getNeSelectOtions"
allow-clear
:placeholder="t('common.inputPlease')"
/>
</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>
<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('common.deleteText') }}
</a-button>
</a-space>
</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>
<template #title>{{ t('common.sizeText') }}</template>
<a-dropdown trigger="click" placement="bottomRight">
<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: tableColumns.length * 120 }"
:row-selection="{
type: 'checkbox',
columnWidth: '48px',
selectedRowKeys: tableState.selectedRowKeys,
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<DictTag :options="dict.neInfoStatus" :value="record.status" />
</template>
<template v-if="column.key === 'id'">
<a-space :size="8" align="center">
<a-tooltip>
<template #title>{{ t('common.editText') }}</template>
<a-button
type="link"
@click.prevent="fnModalVisibleByEdit(record)"
>
<template #icon><FormOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>
{{ t('views.ne.common.restart') }}
</template>
<a-button
type="link"
@click.prevent="fnRecordMore('restart', record)"
>
<template #icon><UndoOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip placement="left">
<template #title>{{ t('common.moreText') }}</template>
<a-dropdown placement="bottomRight" trigger="click">
<a-button type="link">
<template #icon><EllipsisOutlined /> </template>
</a-button>
<template #overlay>
<a-menu @click="({ key }:any) => fnRecordMore(key, record)">
<a-menu-item key="log">
<FileTextOutlined />
{{ t('views.ne.common.log') }}
</a-menu-item>
<a-menu-item key="start">
<ThunderboltOutlined />
{{ t('views.ne.common.start') }}
</a-menu-item>
<a-menu-item key="stop">
<CloseSquareOutlined />
{{ t('views.ne.common.stop') }}
</a-menu-item>
<a-menu-item key="reload" v-if="false">
<SyncOutlined />
{{ t('views.ne.common.reload') }}
</a-menu-item>
<a-menu-item key="delete">
<DeleteOutlined />
{{ t('common.deleteText') }}
</a-menu-item>
<a-menu-item
key="oam"
v-if="!['OMC'].includes(record.neType)"
>
<FileTextOutlined />
{{ t('views.ne.common.oam') }}
</a-menu-item>
<a-menu-item
key="license"
v-if="!['OMC'].includes(record.neType)"
>
<FileTextOutlined />
{{ t('views.ne.common.license') }}
</a-menu-item>
<!-- 配置备份 -->
<a-menu-item key="backConfExport">
<ExportOutlined />
{{ t('views.ne.neInfo.backConf.export') }}
</a-menu-item>
<a-menu-item key="backConfImport">
<ImportOutlined />
{{ t('views.ne.neInfo.backConf.import') }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-tooltip>
</a-space>
</template>
</template>
<template #expandedRowRender="{ record }">
<a-row>
<a-col :offset="2" :lg="8" :md="8" :xs="8">
<a-divider orientation="left">
{{ t('views.ne.neInfo.info') }}
</a-divider>
<div>
<span>{{ t('views.ne.neInfo.serviceState') }}</span>
<DictTag :options="dict.neInfoStatus" :value="record.status" />
</div>
<div>
<span>{{ t('views.ne.neVersion.version') }}</span>
<span>{{ record.serverState.version }}</span>
</div>
<div>
<span>{{ t('views.ne.common.serialNum') }}</span>
<span>{{ record.serverState.sn }}</span>
</div>
<div>
<span>{{ t('views.ne.common.expiryDate') }}</span>
<span>{{ record.serverState.expire }}</span>
</div>
<div>
<span>{{ t('views.ne.common.ueNumber') }}</span>
<span
v-if="
['UDM', 'AMF', 'MME'].includes(record.serverState.neType)
"
>
{{ record.serverState.ueNumber }}
</span>
<span v-else> - </span>
</div>
<div v-if="['AMF', 'MME'].includes(record.serverState.neType)">
<span>{{ t('views.ne.common.nbNumber') }}</span>
<span> {{ record.serverState.nbNumber }} </span>
</div>
</a-col>
<a-col :offset="2" :lg="8" :md="8" :xs="8">
<a-divider orientation="left">
{{ t('views.ne.neInfo.resourceInfo') }}
</a-divider>
<div>
<span>{{ t('views.ne.neInfo.neCpu') }}</span>
<a-progress
status="normal"
:stroke-color="
record.resoures.nfCpuUsage < 30
? '#52c41a'
: record.resoures.nfCpuUsage > 70
? '#ff4d4f'
: '#1890ff'
"
:percent="record.resoures.nfCpuUsage"
/>
</div>
<div>
<span>{{ t('views.ne.neInfo.sysCpu') }}</span>
<a-progress
status="normal"
:stroke-color="
record.resoures.sysCpuUsage < 30
? '#52c41a'
: record.resoures.sysCpuUsage > 70
? '#ff4d4f'
: '#1890ff'
"
:percent="record.resoures.sysCpuUsage"
/>
</div>
<div>
<span>{{ t('views.ne.neInfo.sysMem') }}</span>
<a-progress
status="normal"
:stroke-color="
record.resoures.sysMemUsage < 30
? '#52c41a'
: record.resoures.sysMemUsage > 70
? '#ff4d4f'
: '#1890ff'
"
:percent="record.resoures.sysMemUsage"
/>
</div>
<div>
<span>{{ t('views.ne.neInfo.sysDisk') }}</span>
<a-progress
status="normal"
:stroke-color="
record.resoures.sysDiskUsage < 30
? '#52c41a'
: record.resoures.sysDiskUsage > 70
? '#ff4d4f'
: '#1890ff'
"
:percent="record.resoures.sysDiskUsage"
/>
</div>
</a-col>
</a-row>
</template>
</a-table>
</a-card>
<!-- 新增框或修改框 -->
<EditModal
v-model:open="modalState.openByEdit"
:id="modalState.id"
@ok="fnModalEditOk"
@cancel="fnModalEditCancel"
></EditModal>
<!-- OAM编辑框 -->
<OAMModal
v-model:open="modalState.openByOAM"
:ne-uid="modalState.neUid"
:ne-type="modalState.neType"
@cancel="fnModalEditCancel"
></OAMModal>
<!-- 配置文件备份框 -->
<BackConfModal
ref="backConf"
v-model:open="modalState.openByBackConf"
:ne-uid="modalState.neUid"
:ne-type="modalState.neType"
@cancel="fnModalEditCancel"
></BackConfModal>
<!-- 文件上传框 -->
<LicenseEditModal
v-model:open="modalState.openByLicense"
:id="modalState.id"
:ne-uid="modalState.neUid"
:ne-type="modalState.neType"
@ok="fnModalEditOk"
@cancel="fnModalEditCancel"
></LicenseEditModal>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,724 @@
<script setup lang="ts">
import { reactive, ref, onMounted, toRaw, defineAsyncComponent } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { ProModal } from 'antdv-pro-modal';
import {
Modal,
TableColumnsType,
message,
notification,
} 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 useNeStore from '@/store/modules/ne';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { listNeVersion, operateNeVersion } from '@/api/ne/neVersion';
import { parseDateToStr } from '@/utils/date-utils';
import useI18n from '@/hooks/useI18n';
import useDictStore from '@/store/modules/dict';
import useMaskStore from '@/store/modules/mask';
const maskStore = useMaskStore();
const neStore = useNeStore();
const { t } = useI18n();
const { getDict } = useDictStore();
// 异步加载组件
const EditModal = defineAsyncComponent(
() => import('@/views/ne/neSoftware/components/EditModal.vue')
);
const UploadMoreFile = defineAsyncComponent(
() => import('@/views/ne/neSoftware/components/UploadMoreFile.vue')
);
/**字典数据-状态 */
let dictStatus = ref<DictType[]>([]);
/**查询参数 */
let queryParams = reactive({
/**网元类型 */
neType: undefined,
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
neType: undefined,
pageNum: 1,
pageSize: 20,
});
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: any[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
/**勾选单行记录 */
selectedRowOne: any;
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
seached: true,
data: [],
selectedRowKeys: [],
/**勾选单行记录 */
selectedRowOne: { neType: '' },
});
/**表格字段列 */
let tableColumns = ref<TableColumnsType>([
{
title: t('common.rowId'),
dataIndex: 'id',
align: 'left',
width: 100,
},
{
title: t('views.ne.common.neType'),
dataIndex: 'neType',
align: 'left',
width: 100,
},
{
title: t('views.ne.neVersion.version'),
dataIndex: 'version',
key: 'version',
align: 'left',
width: 150,
resizable: true,
minWidth: 150,
maxWidth: 200,
},
{
title: t('views.ne.neVersion.preVersion'),
dataIndex: 'preVersion',
key: 'preVersion',
align: 'left',
width: 150,
resizable: true,
minWidth: 150,
maxWidth: 200,
},
{
title: t('views.ne.neVersion.newVersion'),
dataIndex: 'newVersion',
align: 'left',
width: 150,
resizable: true,
minWidth: 150,
maxWidth: 200,
},
{
title: t('views.ne.neVersion.status'),
key: 'status',
dataIndex: 'status',
align: 'left',
width: 120,
},
{
title: t('common.updateTime'),
dataIndex: 'updateTime',
align: 'left',
customRender(opt) {
if (!opt.value) return '';
return parseDateToStr(opt.value);
},
width: 200,
},
{
title: t('common.operate'),
key: 'id',
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;
}
/**表格多选 */
function fnTableSelectedRowKeys(
keys: (string | number)[],
selectedRows: any[]
) {
tableState.selectedRowKeys = keys;
// 勾选单个上传
if (selectedRows.length === 1) {
tableState.selectedRowOne = selectedRows[0];
} else {
tableState.selectedRowOne = { neType: '' };
}
}
/**查询列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (tableState.loading) return;
tableState.loading = true;
if (pageNum) {
queryParams.pageNum = pageNum;
}
listNeVersion(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);
}
} else {
tablePagination.total = 0;
tableState.data = [];
}
tableState.loading = false;
});
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**单文件上传 */
openByEdit: boolean;
/**多文件上传 */
openByMoreFile: boolean;
/**勾选升级情况 */
openByUpgrade: boolean;
/**操作数据进行版本升级 */
operateDataUpgrade: any[];
/**确定按钮 loading */
confirmLoading: boolean;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
openByEdit: false,
openByMoreFile: false,
openByUpgrade: false,
operateDataUpgrade: [],
confirmLoading: false,
});
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
function fnModalEditOk() {
fnGetList(1);
if (modalState.openByUpgrade) {
fnModalEditCancel();
}
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalEditCancel() {
modalState.openByEdit = false;
modalState.openByMoreFile = false;
modalState.openByUpgrade = false;
modalState.operateDataUpgrade = [];
}
/**版本控制升级回退 */
function fnRecordVersion(
action: 'upgrade' | 'rollback',
row: Record<string, any>
) {
let contentTip = `${action} version packages?`;
if (action === 'upgrade') {
contentTip = t('views.ne.neVersion.upgradeTip');
if (row.newVersion === '' || row.newVersion === '-') {
message.warning(t('views.ne.neVersion.upgradeTipEmpty'), 3);
return;
}
if (row.newVersion === row.version) {
contentTip = t('views.ne.neVersion.upgradeTipEqual');
}
}
if (action === 'rollback') {
contentTip = t('views.ne.neVersion.rollbackTip');
if (row.preVersion === '' || row.preVersion === '-') {
message.warning(t('views.ne.neVersion.rollbackTipEmpty'), 3);
return;
}
if (row.prePath === '' || row.prePath === '-') {
message.warning(t('views.ne.neVersion.noPath'), 3);
return;
}
if (row.preVersion === row.version) {
contentTip = t('views.ne.neVersion.rollbackTipEqual');
}
}
Modal.confirm({
title: t('common.tipTitle'),
content: contentTip,
onOk() {
if (modalState.confirmLoading) return;
modalState.confirmLoading = true;
const notificationKey = 'NE_VERSION_' + action;
notification.info({
key: notificationKey,
message: t('common.tipTitle'),
description: `${row.neType} ${t('common.loading')}`,
duration: 0,
});
let preinput = {};
if (row.neType.toUpperCase() === 'IMS') {
preinput = { pisCSCF: 'y', updateMFetc: 'No', updateMFshare: 'No' };
}
operateNeVersion({
neType: row.neType,
neUid: row.neUid,
action: action,
preinput: preinput,
})
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
// OMC自升级
if (row.neType.toUpperCase() === 'OMC') {
if (res.code === RESULT_CODE_SUCCESS) {
maskStore.handleMaskType('reload');
} else {
message.error(t('views.ne.neVersion.upgradeFail'), 3);
}
return;
}
fnGetList(1);
} else {
message.error(t('views.ne.neVersion.upgradeFail'), 3);
}
})
.finally(() => {
notification.close(notificationKey);
modalState.confirmLoading = false;
});
},
});
}
/**版本升级弹出确认是否升级 */
function fnRecordUpgradeConfirm() {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.ne.neVersion.upgradeBatchTip'),
onOk() {
fnRecordUpgrade();
},
});
}
/**版本升级进行 */
async function fnRecordUpgrade() {
if (modalState.confirmLoading) return;
modalState.confirmLoading = true;
modalState.openByUpgrade = true;
// 操作升级的网元数据
const selectRows = tableState.data.filter(item =>
tableState.selectedRowKeys.includes(item.id)
);
for (const row of selectRows) {
if (row.newVersion === '-' || row.newVersion === '') {
modalState.operateDataUpgrade.push({
neType: row.neType,
neId: row.neId,
status: 'fail',
log: t('views.ne.neVersion.upgradeNotNewVer'),
});
continue;
}
// OMC跳过操作
if (row.neType.toUpperCase() === 'OMC') {
modalState.operateDataUpgrade.push({
neType: row.neType,
neId: row.neId,
status: 'fail',
log: t('views.ne.neVersion.upgradeOMCVer'),
});
continue;
}
// 开始升级
let preinput = {};
if (row.neType.toUpperCase() === 'IMS') {
preinput = { pisCSCF: 'y', updateMFetc: 'No', updateMFshare: 'No' };
}
const installData = {
neType: row.neType,
neId: row.neId,
action: 'upgrade',
preinput: preinput,
};
try {
const res = await operateNeVersion(installData);
const operateData = {
neType: row.neType,
neId: row.neId,
status: 'fail',
log: t('common.operateErr'),
};
if (res.code === RESULT_CODE_SUCCESS) {
operateData.status = 'done';
operateData.log = t('views.ne.neVersion.upgradeDone');
} else {
operateData.status = 'fail';
operateData.log = t('views.ne.neVersion.upgradeFail');
}
modalState.operateDataUpgrade.unshift(operateData);
} catch (error) {
console.error(error);
}
}
// 结束
modalState.confirmLoading = false;
}
onMounted(() => {
// 初始字典数据
getDict('ne_version_status')
.then(res => {
dictStatus.value = res;
})
.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.ne.common.neType')" name="neType ">
<a-auto-complete
v-model:value="queryParams.neType"
:options="neStore.getNeSelectOtions"
allow-clear
:placeholder="t('common.inputPlease')"
/>
</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>
<a-space :size="8" align="center">
<a-button
type="primary"
@click.prevent="
() => (modalState.openByEdit = !modalState.openByEdit)
"
>
<template #icon><UploadOutlined /></template>
{{ t('views.ne.neSoftware.upload') }}
</a-button>
<a-button
type="primary"
:disabled="tableState.selectedRowKeys.length > 1"
@click.prevent="
() => (modalState.openByMoreFile = !modalState.openByMoreFile)
"
>
<template #icon><UploadOutlined /></template>
<template v-if="tableState.selectedRowOne.neType">
{{ t('views.ne.neSoftware.upload') }}
{{ tableState.selectedRowOne.neType }}
</template>
<template v-else>
{{ t('views.ne.neSoftware.uploadBatch') }}
</template>
</a-button>
<a-button
type="primary"
:ghost="true"
:disabled="tableState.selectedRowKeys.length <= 0"
:loading="modalState.confirmLoading"
@click.prevent="fnRecordUpgradeConfirm()"
>
<template #icon><ThunderboltOutlined /></template>
{{ t('views.ne.neVersion.upgradeBatch') }}
</a-button>
</a-space>
</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: tableColumns.length * 150 }"
@resizeColumn="(w:number, col:any) => (col.width = w)"
:row-selection="{
type: 'checkbox',
columnWidth: '48px',
selectedRowKeys: tableState.selectedRowKeys,
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<DictTag :options="dictStatus" :value="record.status" />
</template>
<template v-if="column.key === 'version'">
{{ record.version }}
<a-tooltip
placement="topRight"
v-if="
record.version && (record.path === '' || record.path === '-')
"
>
<template #title>
{{ t('views.ne.neVersion.noPath') }}
</template>
<InfoCircleOutlined />
</a-tooltip>
</template>
<template v-if="column.key === 'preVersion'">
{{ record.preVersion }}
<a-tooltip
placement="topRight"
v-if="
record.preVersion &&
(record.prePath === '' || record.prePath === '-')
"
>
<template #title>
{{ t('views.ne.neVersion.noPath') }}
</template>
<InfoCircleOutlined />
</a-tooltip>
</template>
<template v-if="column.key === 'id'">
<a-space :size="8" align="center">
<a-tooltip placement="topRight">
<template #title>
{{ t('views.ne.neVersion.upgrade') }}
</template>
<a-button
type="link"
@click.prevent="fnRecordVersion('upgrade', record)"
>
<template #icon><ThunderboltOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip placement="topRight">
<template #title>
{{ t('views.ne.neVersion.rollback') }}
</template>
<a-button
type="link"
@click.prevent="fnRecordVersion('rollback', record)"
>
<template #icon><RollbackOutlined /></template>
</a-button>
</a-tooltip>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 新增单文件上传 -->
<EditModal
v-model:open="modalState.openByEdit"
@ok="fnModalEditOk"
@cancel="fnModalEditCancel"
></EditModal>
<!-- 新增多文件上传框 -->
<UploadMoreFile
v-model:open="modalState.openByMoreFile"
:ne-type="tableState.selectedRowOne.neType"
@ok="fnModalEditOk"
@cancel="fnModalEditCancel"
></UploadMoreFile>
<!-- 勾选网元版本进行升级框 -->
<ProModal
:drag="true"
:width="800"
:destroyOnClose="true"
:body-style="{ height: '520px', overflowY: 'scroll' }"
:keyboard="false"
:mask-closable="false"
:open="modalState.openByUpgrade"
:title="t('views.ne.neVersion.upgradeModal')"
:closable="false"
@ok="fnModalEditOk"
@cancel="fnModalEditCancel"
>
<template #footer>
<a-button
key="submit"
type="primary"
:disabled="modalState.confirmLoading"
@click="fnModalEditOk"
>
{{ t('common.close') }}
</a-button>
</template>
<p>
<a-alert
v-if="modalState.confirmLoading"
:message="t('common.loading')"
type="info"
show-icon
>
<template #icon>
<LoadingOutlined />
</template>
</a-alert>
</p>
<p v-for="o in modalState.operateDataUpgrade" :key="o.neUid">
<a-alert
:message="`${o.neType}-${o.neUid}`"
:description="o.log"
:type="o.status === 'done' ? 'success' : 'error'"
show-icon
>
<template #icon>
<CheckCircleOutlined v-if="o.status === 'done'" />
<InfoCircleOutlined v-else />
</template>
</a-alert>
</p>
</ProModal>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,306 @@
<script setup lang="ts">
import { reactive, ref, onMounted, toRaw } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
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 { parseDateToStr } from '@/utils/date-utils';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { mmlLogList } from '@/api/tool/mml';
import useI18n from '@/hooks/useI18n';
const { t } = useI18n();
/**记录开始结束时间 */
let queryRangePicker = ref<[string, string]>(['', '']);
/**查询参数 */
let queryParams = reactive({
/**登录账号 */
user: '',
/**记录时间 */
beginTime: '',
endTime: '',
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
accountName: '',
beginTime: '',
endTime: '',
pageNum: 1,
pageSize: 20,
});
queryRangePicker.value = ['', ''];
tablePagination.current = 1;
tablePagination.pageSize = 20;
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 = reactive([
{
title: t('common.rowId'),
dataIndex: 'id',
align: 'left',
width: 80,
},
{
title: t('views.ne.common.neType'),
dataIndex: 'neType',
align: 'left',
width: 100,
},
{
title: t('views.ne.common.neName'),
dataIndex: 'neName',
align: 'left',
width: 100,
},
{
title: t('views.logManage.mml.logTime'),
dataIndex: 'createTime',
align: 'left',
width: 200,
customRender(opt) {
if (!opt.value) return '';
return parseDateToStr(opt.value);
},
},
{
title: t('views.logManage.mml.MML'),
dataIndex: 'command',
key: 'command',
align: 'left',
ellipsis: true,
},
]);
/**表格分页器参数 */
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;
}
if (!queryRangePicker.value) {
queryRangePicker.value = ['', ''];
}
queryParams.beginTime = queryRangePicker.value[0];
queryParams.endTime = queryRangePicker.value[1];
mmlLogList(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);
}
}
tableState.loading = false;
});
}
onMounted(() => {
// 获取列表数据
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.logManage.mml.account')" name="user">
<a-input
v-model:value="queryParams.user"
:allow-clear="true"
:placeholder="t('common.inputPlease')"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :xs="24">
<a-form-item
:label="t('views.logManage.mml.logTime')"
name="queryRangePicker"
>
<a-range-picker
v-model:value="queryRangePicker"
allow-clear
bordered
show-time
value-format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
></a-range-picker>
</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>
<template #title>{{ t('common.sizeText') }}</template>
<a-dropdown trigger="click" placement="bottomRight">
<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"
>
<template #bodyCell="{ column, record }">
<!-- <template v-if="column.key === 'mml'">
<a-tooltip placement="topLeft">
<template #title>{{ record.result }}</template>
<div class="mmlText">{{ record.mml }}</div>
</a-tooltip>
</template> -->
</template>
</a-table>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
.mmlText {
// max-width: 800px; sdf
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,825 @@
<script setup lang="ts">
import { reactive, ref, onMounted, toRaw } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { Modal, message } from 'ant-design-vue/es';
import CodemirrorEdite from '@/components/CodemirrorEdite/index.vue';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import useNeStore from '@/store/modules/ne';
import { regExpIPv4, regExpIPv6 } from '@/utils/regular-utils';
import useI18n from '@/hooks/useI18n';
import { getMMLByNE, sendMML } from '@/api/mmlManage/neOperate';
import { uploadFileToNE } from '@/api/tool/file';
import { UploadRequestOption } from 'ant-design-vue/es/vc-upload/interface';
const neStore = useNeStore();
const { t } = useI18n();
/**网元参数 */
let neCascaderOptions = ref<Record<string, any>[]>([]);
/**对象信息状态类型 */
type StateType = {
/**网元类型 */
neType: string[];
/**命令网元类型 */
mmlNeType: string;
/**命令数据 tree */
mmlTreeData: any[];
/**命令选中 */
mmlSelect: Record<string, any>;
/**表单数据 */
from: Record<string, any>;
/**自动完成 input */
autoCompleteValue: string;
/**自动完成 options */
autoCompleteData: any[];
/**自动完成 options */
autoCompleteSearch: any[];
/**命令发送日志 */
mmlCmdLog: string;
};
/**对象信息状态 */
let state: StateType = reactive({
neType: [],
mmlNeType: '',
mmlTreeData: [],
mmlSelect: {
title: '',
key: '',
operation: '',
object: '',
objectType: 'General',
param: [],
},
from: {
uploadLoading: false,
sendLoading: false,
},
autoCompleteValue: '',
autoCompleteData: [],
autoCompleteSearch: [],
mmlCmdLog: '',
});
/**查询可选命令列表 */
function fnTreeSelect(_: any, info: any) {
state.mmlSelect = info.node.dataRef;
state.from = {};
// 遍历判断是否有初始value
if (Array.isArray(state.mmlSelect.param)) {
for (const param of state.mmlSelect.param) {
if (typeof param.value !== 'undefined' && param.value != '') {
const valueType = param.type;
if (['enum', 'int'].includes(valueType)) {
state.from[param.name] = Number(param.value);
} else if (valueType === 'bool') {
state.from[param.name] = Boolean(param.value);
} else {
state.from[param.name] = param.value;
}
}
}
}
state.autoCompleteValue =
`${state.mmlSelect.operation} ${state.mmlSelect.object}`.trim();
// state.mmlCmdLog = '';
// 回到顶部
window.scrollTo({
top: 0,
behavior: 'smooth', // 平滑滚动到顶部,如果不需要平滑效果可以将此行代码删除
});
}
/**清空控制台日志 */
function fnCleanCmdLog() {
state.mmlCmdLog = '';
}
/**清空表单 */
function fnCleanFrom() {
state.mmlSelect = {
title: '',
key: '',
operation: '',
object: '',
objectType: 'General',
param: [],
};
state.from = {};
}
/**命令发送 */
function fnSendMML() {
if (state.from.sendLoading) {
return;
}
let cmdArr: string[] = [];
const { operation, object, objectType, param } = state.mmlSelect;
// 根据参数取值
let argsArr: string[] = [];
if (operation && Array.isArray(param)) {
const from = toRaw(state.from);
for (const item of toRaw(param)) {
const value = from[item.name];
// 是否必填项且有效值
const notV = value === null || value === undefined || value === '';
if (item.optional === 'false' && notV) {
message.warning(t('views.mmlManage.require', { num: item.display }), 2);
return;
}
// 检查是否存在值
if (!Reflect.has(from, item.name) || notV) {
continue;
}
// 检查规则
const [ok, msg] = ruleVerification(item, from[item.name]);
if (!ok) {
message.warning({
content: `${msg}`,
duration: 3,
});
return;
}
argsArr.push(`${item.name}=${from[item.name]}`);
}
// 拼装命令
const argsStr = argsArr.join(',');
let cmdStr = '';
if (object && argsStr) {
cmdStr = `${operation} ${object} ${argsStr}`;
} else if (object) {
cmdStr = `${operation} ${object}`;
} else {
cmdStr = `${operation} ${argsStr}`;
}
cmdArr = [cmdStr.trim()];
}
if (cmdArr.length > 0) {
state.autoCompleteValue = cmdArr[0];
} else {
let value = state.autoCompleteValue;
if (value.indexOf('\n') !== -1) {
value = value.replace(/(\r\n|\n)/g, ';');
}
cmdArr = value.split(';');
}
if (cmdArr.length === 1 && cmdArr[0] === '') {
return;
}
// 发送
state.from.sendLoading = true;
const [neType, neUid] = state.neType;
sendMML({
neUid: neUid,
neType: neType,
type: objectType,
command: cmdArr,
})
.then(res => {
state.from.sendLoading = false;
if (res.code === RESULT_CODE_SUCCESS) {
let resultArr = res.data;
for (let i = 0; i < resultArr.length; i++) {
const str = resultArr[i] || '';
const logStr = str.replace(/(\r\n|\n)/g, '\n');
const cmdStr = cmdArr[i] || '';
state.mmlCmdLog += `${cmdStr}\n${logStr}\n`;
}
} else {
state.mmlCmdLog += `${res.msg}\n`;
}
})
.finally(() => {
// 控制台滚动底部
const container = document.getElementsByClassName('cm-scroller')[0];
if (container) {
container.scrollTop = container.scrollHeight;
}
});
}
/**上传变更 */
function fnUpload(up: UploadRequestOption, name: string) {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.mmlManage.uploadFileTip'),
onOk() {
const hide = message.loading(t('common.loading'), 0);
state.from.uploadLoading = true;
const [neType, neUid] = state.neType;
uploadFileToNE(neType, neUid, up.file as File, 5)
.then(res => {
// 文件转存
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('views.mmlManage.uploadFileOk'), 3);
state.from[name] = res.data;
} else {
message.error(t('views.mmlManage.uploadFileErr'), 3);
}
})
.finally(() => {
state.from.uploadLoading = false;
hide();
});
},
});
}
/**规则校验 */
function ruleVerification(
row: Record<string, any>,
value: any
): (string | boolean)[] {
let result = [true, ''];
const type = row.type;
const filter = row.filter;
const display = row.display;
switch (type) {
case 'int':
if (filter && filter.indexOf('~') !== -1) {
const filterArr = filter.split('~');
const minInt = parseInt(filterArr[0]);
const maxInt = parseInt(filterArr[1]);
const valueInt = parseInt(value);
if (valueInt < minInt || valueInt > maxInt) {
return [false, t('views.mmlManage.requireInt', { display, filter })];
}
}
break;
case 'ipv4':
if (!regExpIPv4.test(value)) {
return [false, t('views.mmlManage.requireIpv4', { display })];
}
break;
case 'ipv6':
if (!regExpIPv6.test(value)) {
return [false, t('views.mmlManage.requireIpv6', { display })];
}
break;
case 'enum':
if (filter && filter.indexOf('{') === 1) {
let filterJson: Record<string, any> = {};
try {
filterJson = JSON.parse(filter); //string---json
} catch (error) {
console.error(error);
}
if (!Object.keys(filterJson).includes(`${value}`)) {
return [false, t('views.mmlManage.requireEnum', { display })];
}
}
break;
case 'bool':
if (filter && filter.indexOf('{') === 1) {
let filterJson: Record<string, any> = {};
try {
filterJson = JSON.parse(filter); //string---json
} catch (error) {
console.error(error);
}
if (!Object.values(filterJson).includes(`${value}`)) {
return [false, t('views.mmlManage.requireBool', { display })];
}
}
break;
case 'string':
if (filter && filter.indexOf('~') !== -1) {
try {
const filterArr = filter.split('~');
let rule = new RegExp(
'^\\S{' + filterArr[0] + ',' + filterArr[1] + '}$'
);
if (!rule.test(value)) {
return [false, t('views.mmlManage.requireString', { display })];
}
} catch (error) {
console.error(error);
}
}
break;
case 'regex':
if (filter) {
try {
let regex = new RegExp(filter);
if (!regex.test(value)) {
return [false, t('views.mmlManage.requireString', { display })];
}
} catch (error) {
console.error(error);
}
}
break;
case 'file':
if (filter) {
const arr: string[] = filter.split(',');
const itemArr = arr.filter(item => value.endsWith(item));
if (itemArr.length === 0) {
return [false, t('views.mmlManage.requireFile', { display })];
}
}
break;
default:
console.warn(t('views.mmlManage.requireUn', { display }), type);
}
return result;
}
/**网元类型选择对应修改 */
function fnNeChange(keys: any, _: any) {
// 不是同类型时需要重新加载
if (state.mmlNeType !== keys[0]) {
state.autoCompleteSearch = [];
state.autoCompleteData = [];
state.mmlTreeData = [];
state.mmlSelect = {
title: '',
key: '',
operation: '',
object: '',
objectType: 'General',
param: {},
};
fnGetList();
}
}
/**查询可选命令列表 */
function fnGetList() {
const neType = state.neType[0];
state.mmlNeType = neType;
getMMLByNE(neType).then(res => {
if (res.code === RESULT_CODE_SUCCESS && res.data) {
// 构建自动完成筛选结构
const autoCompleteArr: Record<string, any>[] = [];
// 构建树结构
const treeArr: Record<string, any>[] = [];
for (const item of res.data.rows) {
const id = item['id'];
const object = item['object'];
const objectType = item['objectType'];
const operation = item['operation'];
const mmlDisplay = item['mmlDisplay'];
// 可选属性参数
let param = [];
try {
param = JSON.parse(item['paramJson']);
} catch (error) {
console.error(error);
}
// 遍历检查大类
const treeItemIndex = treeArr.findIndex(i => i.key == item['category']);
if (treeItemIndex < 0) {
treeArr.push({
title: item['catDisplay'],
key: item['category'],
selectable: false,
children: [
{
key: id,
title: mmlDisplay,
object,
objectType,
operation,
param,
},
],
});
autoCompleteArr.push({
value: item['catDisplay'],
key: item['category'],
selectable: false,
options: [
{
key: id,
value: mmlDisplay,
object,
objectType,
operation,
param,
},
],
});
} else {
treeArr[treeItemIndex].children.push({
key: id,
title: mmlDisplay,
object,
objectType,
operation,
param,
});
autoCompleteArr[treeItemIndex].options.push({
key: id,
value: mmlDisplay,
object,
objectType,
operation,
param,
});
}
}
state.mmlTreeData = treeArr;
state.autoCompleteData = autoCompleteArr;
} else {
message.warning({
content: t('views.mmlManage.cmdNoTip', { num: neType }),
duration: 2,
});
}
});
}
/**自动完成搜索匹配前缀 */
function fnAutoCompleteSearch(value: string) {
state.autoCompleteSearch = [];
for (const item of state.autoCompleteData) {
const filterOptions = item.options.filter((s: any) => {
return `${s.operation} ${s.object}`.indexOf(value.toLowerCase()) >= 0;
});
if (filterOptions.length > 0) {
state.autoCompleteSearch.push({
value: item.value,
key: item.value,
selectable: false,
options: filterOptions,
});
}
}
}
/**自动完成搜索选择 */
function fnAutoCompleteSelect(_: any, option: any) {
if (Object.keys(option).length === 0) {
return;
}
state.mmlSelect = {
title: option.value,
key: option.key,
operation: option.operation,
object: option.object,
objectType: option.objectType,
param: option.param,
};
state.from = {};
state.autoCompleteValue = `${option.operation} ${option.object}`.trim();
}
/**自动完成搜索选择 */
function fnAutoCompleteChange(value: any, _: any) {
if (value.indexOf(';') !== -1 || value.indexOf('\n') !== -1) {
fnCleanFrom();
return;
}
for (const item of state.autoCompleteData) {
const findItem = item.options.find((s: any) => {
const prefix = `${s.operation} ${s.object}`;
return value.startsWith(prefix);
});
if (findItem) {
state.mmlSelect = {
title: findItem.value,
key: findItem.key,
operation: findItem.operation,
object: findItem.object,
param: findItem.param,
};
state.from = {};
// 截取拆分赋值
const prefix = `${findItem.operation} ${findItem.object} `;
const argsStr = value.replace(prefix, '');
if (argsStr.length > 3) {
const argsArr = argsStr.split(',');
for (const arg of argsArr) {
const kvArr = arg.split('=');
if (kvArr.length >= 2) {
state.from[kvArr[0]] = kvArr[1];
}
}
}
break;
} else {
state.mmlSelect = {
title: '',
key: '',
operation: '',
object: '',
objectType: 'General',
param: [],
};
}
}
}
/**自动完成按键触发 */
function fnAutoCompleteKeydown(evt: KeyboardEvent) {
if (evt.key === 'Enter') {
// 阻止默认的换行行为
evt.preventDefault();
// 按下 Shift + Enter 键时换行
if (evt.shiftKey && evt.target) {
// 插入换行符
const textarea = evt.target as HTMLInputElement;
const start = textarea.selectionStart || 0;
const end = textarea.selectionEnd || 0;
const text = textarea.value;
textarea.value = text.substring(0, start) + '\n' + text.substring(end);
state.autoCompleteValue = textarea.value;
// 更新光标位置
textarea.selectionStart = textarea.selectionEnd = start + 1;
} else {
fnSendMML();
}
}
}
onMounted(() => {
// 获取网元网元列表
neCascaderOptions.value = neStore.getNeCascaderOptions.filter((item: any) => {
return !['OMC', 'CBC', 'SGWC'].includes(item.value); // 过滤不可用的网元
});
if (neCascaderOptions.value.length === 0) {
message.warning({
content: t('common.noData'),
duration: 2,
});
return;
}
// 默认选择AMF
const item = neCascaderOptions.value.find(s => s.value === 'AMF');
if (item && item.children) {
const info = item.children[0];
state.neType = [info.neType, info.neUid];
} else {
const info = neCascaderOptions.value[0].children[0];
state.neType = [info.neType, info.neUid];
}
// 列表
fnGetList();
});
</script>
<template>
<PageContainer>
<a-row :gutter="16">
<a-col :lg="6" :md="6" :xs="24" style="margin-bottom: 24px">
<!-- 命令导航 -->
<a-card
size="small"
:bordered="false"
:title="t('views.mmlManage.cmdTitle')"
>
<a-form layout="vertical" autocomplete="off">
<a-form-item name="neType">
<a-cascader
v-model:value="state.neType"
:options="neCascaderOptions"
@change="fnNeChange"
:allow-clear="false"
:placeholder="t('common.selectPlease')"
/>
</a-form-item>
<a-form-item name="mmlTree" v-if="state.mmlTreeData.length > 0">
<a-directory-tree
:tree-data="state.mmlTreeData"
default-expand-all
@select="fnTreeSelect"
:selectedKeys="[state.mmlSelect.key]"
></a-directory-tree>
</a-form-item>
</a-form>
</a-card>
</a-col>
<a-col :lg="18" :md="18" :xs="24">
<!-- 命令参数输入 -->
<a-card size="small" :bordered="false">
<template #title>
<a-typography-text strong v-if="state.mmlSelect.title">
{{ state.mmlSelect.title }}
</a-typography-text>
<a-typography-text type="danger" v-else>
{{ t('views.mmlManage.cmdOpTip') }}
</a-typography-text>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8">
<a-button
type="default"
size="small"
@click.prevent="fnCleanFrom"
v-if="!!state.mmlSelect.param"
>
<template #icon>
<ClearOutlined />
</template>
{{ t('views.mmlManage.clearForm') }}
</a-button>
<a-button
type="primary"
size="small"
:loading="state.from.sendLoading"
@click.prevent="fnSendMML"
>
<template #icon>
<SendOutlined />
</template>
{{ t('views.mmlManage.exec') }}
</a-button>
</a-space>
</template>
<a-form
layout="horizontal"
autocomplete="off"
:validate-on-rule-change="false"
:validateTrigger="[]"
>
<a-form-item
:label="t('views.mmlManage.cmdQuickEntry')"
:help="t('views.mmlManage.cmdQuickEntryHelp')"
>
<!-- 非UPF通用4100 -->
<a-auto-complete
v-if="state.neType[0] !== 'UPF'"
v-model:value="state.autoCompleteValue"
:dropdown-match-select-width="500"
style="width: 100%"
:options="state.autoCompleteSearch"
@search="fnAutoCompleteSearch"
@select="fnAutoCompleteSelect"
@change="fnAutoCompleteChange"
@keydown="fnAutoCompleteKeydown"
>
<a-textarea :placeholder="t('common.inputPlease')" auto-size />
</a-auto-complete>
<!-- 可选接口类型mml -->
<a-input-group compact v-else>
<a-auto-complete
v-model:value="state.autoCompleteValue"
:dropdown-match-select-width="500"
style="width: 80%"
:options="state.autoCompleteSearch"
@search="fnAutoCompleteSearch"
@select="fnAutoCompleteSelect"
@change="fnAutoCompleteChange"
@keydown="fnAutoCompleteKeydown"
>
<a-textarea
:placeholder="t('common.inputPlease')"
auto-size
/>
</a-auto-complete>
<a-select
v-model:value="state.mmlSelect.objectType"
style="width: 20%"
>
<a-select-option value="General">
{{ t('views.mmlManage.neOperate.mml') }}
</a-select-option>
<a-select-option value="Standard">
{{ t('views.mmlManage.neOperate.mml2') }}
</a-select-option>
</a-select>
</a-input-group>
</a-form-item>
</a-form>
<template v-if="state.mmlSelect.operation && state.mmlSelect.param">
<a-form
layout="vertical"
autocomplete="off"
:validate-on-rule-change="false"
:validateTrigger="[]"
>
<a-divider orientation="left">
{{ t('views.mmlManage.cmdParamPanel') }}
</a-divider>
<a-row :gutter="16">
<a-col
:lg="6"
:md="12"
:xs="24"
v-for="item in state.mmlSelect.param"
>
<a-form-item
:label="item.display"
:name="item.name"
:required="item.optional === 'false'"
>
<a-tooltip>
<template #title v-if="item.comment">
{{ item.comment }}
</template>
<a-input-number
v-if="item.type === 'int'"
v-model:value="state.from[item.name]"
style="width: 100%"
></a-input-number>
<a-switch
v-else-if="item.type === 'bool'"
v-model:checked="state.from[item.name]"
:checked-children="t('common.switch.open')"
:un-checked-children="t('common.switch.shut')"
></a-switch>
<a-select
v-else-if="item.type === 'enum'"
v-model:value="state.from[item.name]"
:allow-clear="item.optional === 'true'"
>
<a-select-option
:value="v"
:key="v"
v-for="(k, v) in JSON.parse(item.filter)"
>
{{ k }}
</a-select-option>
</a-select>
<a-input-group compact v-else-if="item.type === 'file'">
<a-input
v-model:value="state.from[item.name]"
style="width: calc(100% - 32px)"
/>
<a-upload
name="file"
list-type="text"
:accept="item.filter"
:max-count="1"
:show-upload-list="false"
:custom-request="(v:any) => fnUpload(v, item.name)"
>
<a-button
type="primary"
:loading="state.from.uploadLoading"
>
<template #icon>
<UploadOutlined />
</template>
</a-button>
</a-upload>
</a-input-group>
<a-input
v-else
v-model:value="state.from[item.name]"
></a-input>
</a-tooltip>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
</a-card>
<!-- 命令展示 -->
<a-card
:title="t('views.mmlManage.cmdConsole')"
:bordered="false"
size="small"
:body-style="{ padding: 0 }"
style="margin-top: 16px"
v-show="state.mmlCmdLog"
>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-button
type="default"
size="small"
@click.prevent="fnCleanCmdLog"
>
<template #icon>
<ClearOutlined />
</template>
{{ t('views.mmlManage.clearLog') }}
</a-button>
</a-space>
</template>
<CodemirrorEdite
:value="state.mmlCmdLog"
:disabled="true"
height="500px"
></CodemirrorEdite>
</a-card>
</a-col>
</a-row>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,751 @@
<script setup lang="ts">
import { reactive, ref, onMounted, toRaw } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { Modal, message } from 'ant-design-vue/es';
import CodemirrorEdite from '@/components/CodemirrorEdite/index.vue';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import useNeStore from '@/store/modules/ne';
import { regExpIPv4, regExpIPv6 } from '@/utils/regular-utils';
import useI18n from '@/hooks/useI18n';
import { getMMLByUDM, sendMMlByGeneral } from '@/api/mmlManage/neOperate';
import { UploadRequestOption } from 'ant-design-vue/es/vc-upload/interface';
import { uploadFileToNE } from '@/api/tool/file';
import useCoreStore from '@/store/modules/core';
const { t } = useI18n();
const neStore = useNeStore();
const coreStore = useCoreStore();
/**网元参数 */
let neOptions = ref<Record<string, any>[]>([]);
/**对象信息状态类型 */
type StateType = {
/**网元ID */
neUid: string | undefined;
/**命令数据 tree */
mmlTreeData: any[];
/**命令选中 */
mmlSelect: Record<string, any>;
/**表单数据 */
from: Record<string, any>;
/**自动完成 input */
autoCompleteValue: string;
/**自动完成 options */
autoCompleteData: any[];
/**自动完成 options */
autoCompleteSearch: any[];
/**命令发送日志 */
mmlCmdLog: string;
};
/**对象信息状态 */
let state: StateType = reactive({
neUid: undefined,
mmlTreeData: [],
mmlSelect: {
title: '',
key: '',
operation: '',
object: '',
param: [],
},
from: {
uploadLoading: false,
sendLoading: false,
},
autoCompleteValue: '',
autoCompleteData: [],
autoCompleteSearch: [],
mmlCmdLog: '',
});
/**查询可选命令列表 */
function fnTreeSelect(_: any, info: any) {
state.mmlSelect = info.node.dataRef;
state.from = {};
// 遍历判断是否有初始value
if (Array.isArray(state.mmlSelect.param)) {
for (const param of state.mmlSelect.param) {
if (typeof param.value !== 'undefined' && param.value != '') {
const valueType = param.type;
if (['enum', 'int'].includes(valueType)) {
state.from[param.name] = Number(param.value);
} else if (valueType === 'bool') {
state.from[param.name] = Boolean(param.value);
} else {
state.from[param.name] = param.value;
}
}
}
}
state.autoCompleteValue =
`${state.mmlSelect.operation} ${state.mmlSelect.object}`.trim();
// state.mmlCmdLog = '';
// 回到顶部
window.scrollTo({
top: 0,
behavior: 'smooth', // 平滑滚动到顶部,如果不需要平滑效果可以将此行代码删除
});
}
/**清空控制台日志 */
function fnCleanCmdLog() {
state.mmlCmdLog = '';
}
/**清空表单 */
function fnCleanFrom() {
state.mmlSelect = {
title: '',
key: '',
operation: '',
object: '',
param: [],
};
state.from = {};
}
/**命令发送 */
function fnSendMML() {
if (state.from.sendLoading) {
return;
}
if (!state.neUid) {
message.warning({
content: t('views.mmlManage.udmOpesrate.noUDM'),
duration: 5,
});
return;
}
let cmdArr: string[] = [];
const operation = state.mmlSelect.operation;
const object = state.mmlSelect.object;
// 根据参数取值
let argsArr: string[] = [];
const param = toRaw(state.mmlSelect.param) || [];
if (operation && Array.isArray(param)) {
const from = toRaw(state.from);
for (const item of param) {
const value = from[item.name];
// 是否必填项且有效值
const notV = value === null || value === undefined || value === '';
if (item.optional === 'false' && notV) {
message.warning(t('views.mmlManage.require', { num: item.display }), 2);
return;
}
// 检查是否存在值
if (!Reflect.has(from, item.name) || notV) {
continue;
}
// 检查规则
const [ok, msg] = ruleVerification(item, from[item.name]);
if (!ok) {
message.warning({
content: `${msg}`,
duration: 3,
});
return;
}
argsArr.push(`${item.name}=${from[item.name]}`);
}
// 拼装命令
const argsStr = argsArr.join(',');
let cmdStr = '';
if (object && argsStr) {
cmdStr = `${operation} ${object}:${argsStr}`;
} else if (object) {
cmdStr = `${operation} ${object}`;
} else {
cmdStr = `${operation} ${argsStr}`;
}
cmdArr = [cmdStr.trim()];
}
if (cmdArr.length > 0) {
state.autoCompleteValue = cmdArr[0];
} else {
let value = state.autoCompleteValue;
if (value.indexOf('\n') !== -1) {
value = value.replace(/(\r\n|\n)/g, ';');
}
cmdArr = value.split(';');
}
// 发送
state.from.sendLoading = true;
sendMMlByGeneral(state.neUid, cmdArr)
.then(res => {
state.from.sendLoading = false;
if (res.code === RESULT_CODE_SUCCESS) {
let resultArr = res.data;
for (let i = 0; i < resultArr.length; i++) {
const str = resultArr[i] || '';
const logStr = str.replace(/(\r\n|\n)/g, '\n');
const cmdStr = cmdArr[i] || '';
state.mmlCmdLog += `${cmdStr}\n${logStr}\n`;
}
} else {
state.mmlCmdLog += `${res.msg}\n`;
}
})
.finally(() => {
// 控制台滚动底部
const container = document.getElementsByClassName('cm-scroller')[0];
if (container) {
container.scrollTop = container.scrollHeight;
}
});
}
/**上传变更 */
function fnUpload(up: UploadRequestOption, name: string) {
const neUid = state.neUid;
if (!neUid) {
message.warning({
content: t('views.mmlManage.udmOpesrate.noUDM'),
duration: 5,
});
return;
}
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.mmlManage.uploadFileTip'),
onOk() {
const hide = message.loading(t('common.loading'), 0);
state.from.uploadLoading = true;
uploadFileToNE('UDM', neUid, up.file as File, 5)
.then(res => {
// 文件转存
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('views.mmlManage.uploadFileOk'), 3);
state.from[name] = res.data;
} else {
message.error(t('views.mmlManage.uploadFileErr'), 3);
}
})
.finally(() => {
state.from.uploadLoading = false;
hide();
});
},
});
}
/**规则校验 */
function ruleVerification(
row: Record<string, any>,
value: any
): (string | boolean)[] {
let result = [true, ''];
const type = row.type;
const filter = row.filter;
const display = row.display;
switch (type) {
case 'int':
if (filter && filter.indexOf('~') !== -1) {
const filterArr = filter.split('~');
const minInt = parseInt(filterArr[0]);
const maxInt = parseInt(filterArr[1]);
const valueInt = parseInt(value);
if (valueInt < minInt || valueInt > maxInt) {
return [false, t('views.mmlManage.requireInt', { display, filter })];
}
}
break;
case 'ipv4':
if (!regExpIPv4.test(value)) {
return [false, t('views.mmlManage.requireIpv4', { display })];
}
break;
case 'ipv6':
if (!regExpIPv6.test(value)) {
return [false, t('views.mmlManage.requireIpv6', { display })];
}
break;
case 'enum':
if (filter && filter.indexOf('{') === 1) {
let filterJson: Record<string, any> = {};
try {
filterJson = JSON.parse(filter); //string---json
} catch (error) {
console.error(error);
}
if (!Object.keys(filterJson).includes(`${value}`)) {
return [false, t('views.mmlManage.requireEnum', { display })];
}
}
break;
case 'bool':
if (filter && filter.indexOf('{') === 1) {
let filterJson: Record<string, any> = {};
try {
filterJson = JSON.parse(filter); //string---json
} catch (error) {
console.error(error);
}
if (!Object.values(filterJson).includes(`${value}`)) {
return [false, t('views.mmlManage.requireBool', { display })];
}
}
break;
case 'string':
if (filter && filter.indexOf('~') !== -1) {
try {
const filterArr = filter.split('~');
let rule = new RegExp(
'^\\S{' + filterArr[0] + ',' + filterArr[1] + '}$'
);
if (!rule.test(value)) {
return [false, t('views.mmlManage.requireString', { display })];
}
} catch (error) {
console.error(error);
}
}
break;
case 'regex':
if (filter) {
try {
let regex = new RegExp(filter);
if (!regex.test(value)) {
return [false, t('views.mmlManage.requireString', { display })];
}
} catch (error) {
console.error(error);
}
}
break;
case 'file':
if (filter) {
const arr: string[] = filter.split(',');
const itemArr = arr.filter(item => value.endsWith(item));
if (itemArr.length === 0) {
return [false, t('views.mmlManage.requireFile', { display })];
}
}
break;
default:
console.warn(t('views.mmlManage.requireUn', { display }), type);
}
return result;
}
/**查询可选命令列表 */
function fnGetList() {
getMMLByUDM().then(res => {
if (res.code === RESULT_CODE_SUCCESS && res.data) {
// 构建自动完成筛选结构
const autoCompleteArr: Record<string, any>[] = [];
// 构建树结构
const treeArr: Record<string, any>[] = [];
for (const item of res.data.rows) {
const id = item['id'];
const object = item['object'];
const operation = item['operation'];
const mmlDisplay = item['mmlDisplay'];
// 可选属性参数
let param = [];
try {
param = JSON.parse(item['paramJson']);
} catch (error) {
console.error(error);
}
// 遍历检查大类
const treeItemIndex = treeArr.findIndex(i => i.key == item['category']);
if (treeItemIndex < 0) {
treeArr.push({
title: item['catDisplay'],
key: item['category'],
selectable: false,
children: [
{ key: id, title: mmlDisplay, object, operation, param },
],
});
autoCompleteArr.push({
value: item['catDisplay'],
key: item['category'],
selectable: false,
options: [{ key: id, value: mmlDisplay, object, operation, param }],
});
} else {
treeArr[treeItemIndex].children.push({
key: id,
title: mmlDisplay,
object,
operation,
param,
});
autoCompleteArr[treeItemIndex].options.push({
key: id,
value: mmlDisplay,
object,
operation,
param,
});
}
}
state.mmlTreeData = treeArr;
state.autoCompleteData = autoCompleteArr;
} else {
message.warning({
content: t('views.mmlManage.cmdNoTip', { num: 'UDM' }),
duration: 2,
});
}
});
}
/**自动完成搜索匹配前缀 */
function fnAutoCompleteSearch(value: string) {
state.autoCompleteSearch = [];
for (const item of state.autoCompleteData) {
const filterOptions = item.options.filter((s: any) => {
return `${s.operation} ${s.object}`.indexOf(value.toLowerCase()) >= 0;
});
if (filterOptions.length > 0) {
state.autoCompleteSearch.push({
value: item.value,
key: item.value,
selectable: false,
options: filterOptions,
});
}
}
}
/**自动完成搜索选择 */
function fnAutoCompleteSelect(_: any, option: any) {
if (Object.keys(option).length === 0) {
return;
}
state.mmlSelect = {
title: option.value,
key: option.key,
operation: option.operation,
object: option.object,
param: option.param,
};
state.from = {};
state.autoCompleteValue = `${option.operation} ${option.object}`.trim();
}
/**自动完成搜索选择 */
function fnAutoCompleteChange(value: any, _: any) {
if (value.indexOf(';') !== -1 || value.indexOf('\n') !== -1) {
fnCleanFrom();
return;
}
for (const item of state.autoCompleteData) {
const findItem = item.options.find((s: any) => {
const prefix = `${s.operation} ${s.object}`;
return value.startsWith(prefix);
});
if (findItem) {
state.mmlSelect = {
title: findItem.value,
key: findItem.key,
operation: findItem.operation,
object: findItem.object,
param: findItem.param,
};
state.from = {};
// 截取拆分赋值
const prefix = `${findItem.operation} ${findItem.object}:`;
const argsStr = value.replace(prefix, '');
if (argsStr.length > 3) {
const argsArr = argsStr.split(',');
for (const arg of argsArr) {
const kvArr = arg.split('=');
if (kvArr.length >= 2) {
state.from[kvArr[0]] = kvArr[1];
}
}
}
break;
} else {
state.mmlSelect = {
title: '',
key: '',
operation: '',
object: '',
param: [],
};
}
}
}
/**自动完成按键触发 */
function fnAutoCompleteKeydown(evt: any) {
if (evt.key === 'Enter') {
// 阻止默认的换行行为
evt.preventDefault();
// 按下 Shift + Enter 键时换行
if (evt.shiftKey) {
// 插入换行符
const textarea = evt.target;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const text = textarea.value;
textarea.value = text.substring(0, start) + '\n' + text.substring(end);
state.autoCompleteValue = textarea.value;
// 更新光标位置
textarea.selectionStart = textarea.selectionEnd = start + 1;
} else {
fnSendMML();
}
}
}
onMounted(() => {
// 获取网元网元列表
const coreUid = coreStore.currentCoreUid;
const coreNeList = neStore.coreDataNeCascaderOptions.get(coreUid) || [];
coreNeList.forEach(item => {
if (item.value === 'UDM') {
neOptions.value = JSON.parse(JSON.stringify(item.children));
}
});
if (neOptions.value.length === 0) {
message.warning({
content: t('common.noData'),
duration: 2,
});
return;
}
if (neOptions.value.length > 0) {
state.neUid = neOptions.value[0].value;
}
// 获取列表数据
fnGetList();
});
</script>
<template>
<PageContainer>
<a-row :gutter="16">
<a-col :lg="6" :md="6" :xs="24" style="margin-bottom: 24px">
<!-- 命令导航 -->
<a-card
size="small"
:bordered="false"
:title="t('views.mmlManage.cmdTitle')"
>
<a-form layout="vertical" autocomplete="off">
<a-form-item name="neUid ">
<a-select
v-model:value="state.neUid"
:options="neOptions"
:placeholder="t('common.selectPlease')"
/>
</a-form-item>
<a-form-item name="mmlTree" v-if="state.mmlTreeData.length > 0">
<a-directory-tree
:tree-data="state.mmlTreeData"
default-expand-all
@select="fnTreeSelect"
:selectedKeys="[state.mmlSelect.key]"
></a-directory-tree>
</a-form-item>
</a-form>
</a-card>
</a-col>
<a-col :lg="18" :md="18" :xs="24">
<!-- 命令参数输入 -->
<a-card size="small" :bordered="false">
<template #title>
<a-typography-text strong v-if="state.mmlSelect.title">
{{ state.mmlSelect.title }}
</a-typography-text>
<a-typography-text type="danger" v-else>
{{ t('views.mmlManage.cmdOpTip') }}
</a-typography-text>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8">
<a-button
type="default"
size="small"
@click.prevent="fnCleanFrom"
v-if="!!state.mmlSelect.param"
>
<template #icon>
<ClearOutlined />
</template>
{{ t('views.mmlManage.clearForm') }}
</a-button>
<a-button
type="primary"
size="small"
:loading="state.from.sendLoading"
@click.prevent="fnSendMML"
>
<template #icon>
<SendOutlined />
</template>
{{ t('views.mmlManage.exec') }}
</a-button>
</a-space>
</template>
<a-form
layout="horizontal"
autocomplete="off"
:validate-on-rule-change="false"
:validateTrigger="[]"
>
<a-form-item
:label="t('views.mmlManage.cmdQuickEntry')"
:help="t('views.mmlManage.cmdQuickEntryHelp')"
>
<a-auto-complete
v-model:value="state.autoCompleteValue"
:dropdown-match-select-width="500"
style="width: 100%"
:options="state.autoCompleteSearch"
@search="fnAutoCompleteSearch"
@select="fnAutoCompleteSelect"
@change="fnAutoCompleteChange"
@keydown="fnAutoCompleteKeydown"
>
<a-textarea :placeholder="t('common.inputPlease')" auto-size />
</a-auto-complete>
</a-form-item>
</a-form>
<template v-if="state.mmlSelect.operation && state.mmlSelect.param">
<a-form
layout="vertical"
autocomplete="off"
:validate-on-rule-change="false"
:validateTrigger="[]"
>
<a-divider orientation="left">
{{ t('views.mmlManage.cmdParamPanel') }}
</a-divider>
<a-row :gutter="16">
<a-col
:lg="6"
:md="12"
:xs="24"
v-for="item in state.mmlSelect.param"
>
<a-form-item
:label="item.display"
:name="item.name"
:required="item.optional === 'false'"
>
<a-tooltip>
<template #title v-if="item.comment">
{{ item.comment }}
</template>
<a-input-number
v-if="item.type === 'int'"
v-model:value="state.from[item.name]"
style="width: 100%"
></a-input-number>
<a-switch
v-else-if="item.type === 'bool'"
v-model:checked="state.from[item.name]"
:checked-children="t('common.switch.open')"
:un-checked-children="t('common.switch.shut')"
></a-switch>
<a-select
v-else-if="item.type === 'enum'"
v-model:value="state.from[item.name]"
:allow-clear="item.optional === 'true'"
>
<a-select-option
:value="v"
:key="v"
v-for="(k, v) in JSON.parse(item.filter)"
>
{{ k }}
</a-select-option>
</a-select>
<a-input-group compact v-else-if="item.type === 'file'">
<a-input
v-model:value="state.from[item.name]"
style="width: calc(100% - 32px)"
/>
<a-upload
name="file"
list-type="text"
:accept="item.filter"
:max-count="1"
:show-upload-list="false"
:custom-request="(v:any) => fnUpload(v, item.name)"
>
<a-button
type="primary"
:loading="state.from.uploadLoading"
>
<template #icon>
<UploadOutlined />
</template>
</a-button>
</a-upload>
</a-input-group>
<a-input
v-else
v-model:value="state.from[item.name]"
></a-input>
</a-tooltip>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
</a-card>
<!-- 命令展示 -->
<a-card
:title="t('views.mmlManage.cmdConsole')"
:bordered="false"
size="small"
:body-style="{ padding: 0 }"
style="margin-top: 16px"
v-show="state.mmlCmdLog"
>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-button
type="default"
size="small"
@click.prevent="fnCleanCmdLog"
>
<template #icon>
<ClearOutlined />
</template>
{{ t('views.mmlManage.clearLog') }}
</a-button>
</a-space>
</template>
<CodemirrorEdite
:value="state.mmlCmdLog"
:disabled="true"
height="500px"
></CodemirrorEdite>
</a-card>
</a-col>
</a-row>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>Overview 核心网 概览</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,479 @@
<script setup lang="ts">
import { reactive, ref, onMounted, toRaw } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { SizeType } from 'ant-design-vue/es/config-provider';
import { ColumnsType } from 'ant-design-vue/es/table';
import { Modal, message } from 'ant-design-vue/es';
import { parseDateToStr } from '@/utils/date-utils';
import { parseSizeFromFile } from '@/utils/parse-utils';
import { getNeDirZip, getNeFile, listNeFiles } from '@/api/tool/neFile';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import ViewDrawer from '@/views/ne/neFile/components/ViewDrawer.vue';
import useNeStore from '@/store/modules/ne';
import useTabsStore from '@/store/modules/tabs';
import useI18n from '@/hooks/useI18n';
import saveAs from 'file-saver';
import { useRoute, useRouter } from 'vue-router';
const tabsStore = useTabsStore();
const neStore = useNeStore();
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
// 获取地址栏参数
const routeParams = route.query as Record<string, any>;
/**网元参数 */
let neTypeSelect = ref<string[]>([]);
/**查询参数 */
let queryParams = reactive({
/**网元类型 */
neType: '',
neUid: '',
/**读取路径 */
path: '',
/**前缀过滤 */
search: '',
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**记录数据 */
data: object[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'small',
data: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = reactive([
{
title: t('views.logManage.neFile.fileMode'),
dataIndex: 'fileMode',
align: 'center',
width: 150,
},
{
title: t('views.logManage.neFile.size'),
dataIndex: 'size',
align: 'left',
customRender(opt) {
return parseSizeFromFile(opt.value);
},
width: 100,
},
{
title: t('views.logManage.neFile.modifiedTime'),
dataIndex: 'modifiedTime',
align: 'left',
customRender(opt) {
if (!opt.value) return '';
return parseDateToStr(opt.value * 1000);
},
width: 200,
},
{
title: t('views.logManage.neFile.fileName'),
dataIndex: 'fileName',
align: 'left',
resizable: true,
width: 200,
minWidth: 100,
maxWidth: 350,
},
{
title: t('common.operate'),
key: 'fileName',
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();
},
});
/**下载触发等待 */
let downLoading = ref<boolean>(false);
/**信息文件下载 */
function fnDownloadFile(row: Record<string, any>) {
if (downLoading.value) return;
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.logManage.neFile.downTip', { fileName: row.fileName }),
onOk() {
downLoading.value = true;
const hide = message.loading(t('common.loading'), 0);
getNeFile({
neType: queryParams.neType,
neUid: queryParams.neUid,
path: queryParams.path,
fileName: row.fileName,
delTemp: true,
})
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.msgSuccess', {
msg: t('common.downloadText'),
}),
duration: 2,
});
saveAs(res.data, `${row.fileName}`);
} else {
message.error({
content: t('views.logManage.neFile.downTipErr'),
duration: 2,
});
}
})
.finally(() => {
hide();
downLoading.value = false;
});
},
});
}
/**信息文件下载 */
function fnDownloadFileZIP(row: Record<string, any>) {
if (downLoading.value) return;
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.logManage.neFile.downTipZip', { fileName: row.fileName }),
onOk() {
downLoading.value = true;
const hide = message.loading(t('common.loading'), 0);
getNeDirZip({
neType: queryParams.neType,
neUid: queryParams.neUid,
path: `${queryParams.path}/${row.fileName}`,
delTemp: true,
})
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.msgSuccess', {
msg: t('common.downloadText'),
}),
duration: 2,
});
saveAs(res.data, `${row.fileName}.zip`);
} else {
message.error({
content: t('views.logManage.neFile.downTipErr'),
duration: 2,
});
}
})
.finally(() => {
hide();
downLoading.value = false;
});
},
});
}
/**tmp目录下UPF标准版内部输出目录 */
let tmp = ref<boolean>(false);
/**UPF标准版内部抓包的输出目录 */
function fnUPFTmp() {
fnDirCD('/tmp', 0);
}
/**关闭跳转 */
function fnClose() {
const to = tabsStore.tabClose(route.path);
if (to) {
router.push(to);
} else {
router.back();
}
}
/**访问路径 */
let nePathArr = ref<string[]>([]);
/**进入目录 */
function fnDirCD(dir: string, index?: number) {
if (index === undefined) {
nePathArr.value.push(dir);
queryParams.search = '';
fnGetList(1);
return;
}
if (index === 0) {
const neType = queryParams.neType;
if (neType === 'UPF' && tmp.value) {
nePathArr.value = ['/tmp'];
queryParams.search = `${neType}_${queryParams.neUid}`;
} else {
nePathArr.value = [
`/usr/local/omc/tcpdump/${neType.toLowerCase()}/${queryParams.neUid}`,
];
queryParams.search = '';
}
fnGetList(1);
} else {
nePathArr.value = nePathArr.value.slice(0, index + 1);
queryParams.search = '';
fnGetList(1);
}
}
/**网元类型选择对应修改 */
function fnNeChange(keys: any, _: any) {
if (!Array.isArray(keys)) return;
const neType = keys[0];
const neUid = keys[1];
// 不是同类型时需要重新加载
if (queryParams.neType !== neType || queryParams.neUid !== neUid) {
queryParams.neType = neType;
queryParams.neUid = neUid;
if (neType === 'UPF' && tmp.value) {
nePathArr.value = ['/tmp'];
queryParams.search = `${neType}_${neUid}`;
} else {
nePathArr.value = [
`/usr/local/omc/tcpdump/${neType.toLowerCase()}/${neUid}`,
];
queryParams.search = '';
}
fnGetList(1);
}
}
/**查询列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (queryParams.neUid === '') {
message.warning({
content: t('views.logManage.neFile.neTypePlease'),
duration: 2,
});
return;
}
if (tableState.loading) return;
tableState.loading = true;
if (pageNum) {
queryParams.pageNum = pageNum;
}
queryParams.path = nePathArr.value.join('/');
listNeFiles(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 {
message.error(res.msg, 3);
tablePagination.total = 0;
tableState.data = [];
}
tableState.loading = false;
});
}
/**抽屉状态 */
const viewDrawerState = reactive({
open: false,
/**文件路径 /var/log/amf.log */
filePath: '',
/**网元类型 */
neType: '',
/**网元ID */
neUid: '',
});
/**打开抽屉查看 */
function fnDrawerOpen(row: Record<string, any>) {
viewDrawerState.filePath = [...nePathArr.value, row.fileName].join('/');
viewDrawerState.neType = neTypeSelect.value[0];
viewDrawerState.neUid = neTypeSelect.value[1];
viewDrawerState.open = !viewDrawerState.open;
}
onMounted(() => {
if (routeParams.neType && routeParams.neUid) {
neTypeSelect.value = [routeParams.neType, routeParams.neUid];
fnNeChange(neTypeSelect.value, undefined);
}
});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-row :gutter="16" :wrap="true" align="middle">
<a-col>
<a-button type="default" @click.prevent="fnClose()">
<template #icon><CloseOutlined /></template>
{{ t('common.close') }}
</a-button>
</a-col>
<a-col>
<span>{{ t('views.logManage.neFile.neType') }}:</span>&nbsp;
<a-cascader
v-model:value="neTypeSelect"
:options="neStore.getNeCascaderOptions"
@change="fnNeChange"
:allow-clear="false"
:placeholder="t('views.logManage.neFile.neTypePlease')"
:disabled="downLoading || tableState.loading"
/>
</a-col>
<template v-if="nePathArr.length > 0">
<span>{{ t('views.logManage.neFile.nePath') }}:</span>&nbsp;
<a-col>
<a-breadcrumb>
<a-breadcrumb-item
v-for="(path, index) in nePathArr"
:key="path"
@click="fnDirCD(path, index)"
>
{{ path }}
</a-breadcrumb-item>
</a-breadcrumb>
</a-col>
</template>
</a-row>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-tooltip placement="topRight" v-if="neTypeSelect[0] === 'UPF'">
<template #title>
{{ t('views.traceManage.pcap.fileUPFTip') }}
</template>
<a-checkbox v-model:checked="tmp" @change="fnUPFTmp()">
{{ t('views.traceManage.pcap.fileUPF') }}
</a-checkbox>
</a-tooltip>
<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-space>
</template>
<!-- 表格列表 -->
<a-table
class="table"
row-key="fileName"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:pagination="tablePagination"
:scroll="{ x: 800 }"
@resizeColumn="(w:number, col:any) => (col.width = w)"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'fileName'">
<a-space :size="8" align="center">
<a-button
type="link"
:loading="downLoading"
@click.prevent="fnDownloadFile(record)"
v-if="record.fileType === 'file'"
>
<template #icon><DownloadOutlined /></template>
{{ t('common.downloadText') }}
</a-button>
<a-button
type="link"
@click.prevent="fnDrawerOpen(record)"
v-if="
record.fileType === 'file' && record.fileName.endsWith('.log')
"
>
<template #icon><ProfileOutlined /></template>
{{ t('common.viewText') }}
</a-button>
<template v-if="record.fileType === 'dir'">
<a-button
type="link"
:loading="downLoading"
@click.prevent="fnDownloadFileZIP(record)"
>
<template #icon><DownloadOutlined /></template>
{{ t('common.downloadText') }}
</a-button>
<a-button
type="link"
:loading="downLoading"
@click.prevent="fnDirCD(record.fileName)"
>
<template #icon><FolderOutlined /></template>
{{ t('views.logManage.neFile.dirCd') }}
</a-button>
</template>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 文件内容查看抽屉 -->
<ViewDrawer
v-model:open="viewDrawerState.open"
:file-path="viewDrawerState.filePath"
:ne-type="viewDrawerState.neType"
:ne-uid="viewDrawerState.neUid"
></ViewDrawer>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,811 @@
<script lang="ts" setup>
import { onMounted, reactive } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { message, Modal } from 'ant-design-vue/es';
import { ColumnsType } from 'ant-design-vue/es/table';
import { PageContainer } from 'antdv-pro-layout';
import { ProModal } from 'antdv-pro-modal';
import { dumpStart, dumpStop, traceUPF } from '@/api/trace/pcap';
import { listAllNeInfo } from '@/api/ne/neInfo';
import { getNeDirZip, getNeFile, getNeViewFile } from '@/api/tool/neFile';
import saveAs from 'file-saver';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import useI18n from '@/hooks/useI18n';
import { MENU_PATH_INLINE } from '@/constants/menu-constants';
const router = useRouter();
const route = useRoute();
const { t } = useI18n();
/**对话框对象信息状态类型 */
type ModalStateType = {
/**表单数据 */
from: Record<
string,
{
loading: boolean;
/**网元名 */
title: string;
/**命令 */
cmdStart: string;
/**upf标准版需要停止命令一般空字符 */
cmdStop: string;
/**任务编号 */
taskCode: string;
/**任务日志,upf标准版为空字符串 */
taskFiles: string[];
/**提交表单参数 */
data: {
neType: string;
neUid: string;
cmd?: string;
};
}
>;
/**tcpdump命令组 */
cmdOptions: {
/**命令名称 */
label: string;
/**命令选中值 */
value: string;
/**开始命令 */
start: string;
/**停止命令 */
stop: string;
}[];
/**UPF命令组 */
cmdOptionsUPF: {
/**命令名称 */
label: string;
/**命令选中值 */
value: string;
/**开始命令 */
start: string;
/**停止命令 */
stop: string;
}[];
/**详情框是否显示 */
openByView: boolean;
/**详情框内容 */
viewFrom: {
neType: string;
neUid: string;
path: string;
action: string;
files: string[];
content: string;
};
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
from: {},
cmdOptions: [
{
label: t('views.traceManage.pcap.execCmd'),
value: 'any',
start: '-n -v -s 0',
stop: '',
},
{
label: t('views.traceManage.pcap.execCmd2'),
value: 'any2',
start: 'sctp or tcp port 8088 or 33030',
stop: '',
},
{
label: t('views.traceManage.pcap.execCmd3'),
value: 'any3',
start: '-n -s 0 -v -G 10 -W 7',
stop: '',
},
],
cmdOptionsUPF: [
{
label: t('views.traceManage.pcap.execUPFCmdA'),
value: 'pcap trace',
start: 'pcap trace rx tx max 100000 intfc any',
stop: 'pcap trace rx tx off',
},
{
label: t('views.traceManage.pcap.execUPFCmdB'),
value: 'pcap dispatch',
start: 'pcap dispatch trace on max 100000',
stop: 'pcap dispatch trace off',
},
],
openByView: false,
viewFrom: {
neType: '',
neUid: '',
path: '',
action: '',
files: [],
content: '',
},
});
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: t('views.ne.common.neType'),
dataIndex: 'neType',
align: 'left',
width: 100,
},
{
title: t('views.ne.common.neName'),
dataIndex: 'neName',
align: 'left',
width: 100,
},
{
title: t('views.ne.common.ipAddr'),
dataIndex: 'ipAddr',
align: 'left',
width: 150,
},
{
title: t('views.traceManage.pcap.cmd'),
key: 'cmd',
dataIndex: 'serverState',
align: 'left',
width: 350,
},
{
title: t('common.operate'),
key: 'id',
align: 'left',
},
];
/**表格多选 */
function fnTableSelectedRowKeys(keys: (string | number)[]) {
tableState.selectedRowKeys = keys;
}
/**查询全部网元数据列表 */
function fnGetList() {
if (tableState.loading) return;
tableState.loading = true;
listAllNeInfo({
bandStatus: false,
}).then(res => {
if (
res.code === RESULT_CODE_SUCCESS &&
Array.isArray(res.data) &&
res.data.length > 0
) {
tableState.data = res.data;
// 初始网元参数表单
if (tableState.data.length > 0) {
const { start, stop } = modalState.cmdOptions[0];
for (const item of res.data) {
modalState.from[item.id] = {
loading: false,
title: item.neName,
cmdStart: start,
cmdStop: stop,
taskCode: '',
taskFiles: [],
data: {
neType: item.neType,
neUid: item.neUid,
},
};
}
}
} else {
message.warning({
content: t('common.noData'),
duration: 2,
});
}
tableState.loading = false;
});
}
/**抓包命令选择 */
function fnSelectCmd(id: any, option: any) {
modalState.from[id].cmdStart = option.start;
modalState.from[id].cmdStop = option.stop;
// 重置任务
modalState.from[id].taskCode = '';
modalState.from[id].taskFiles = [];
}
/**
* 开始
* @param row 行记录
*/
function fnRecordStart(row?: Record<string, any>) {
let neIDs: string[] = [];
if (row) {
neIDs = [`${row.id}`];
} else {
row = {
neName: t('views.traceManage.pcap.textSelect'),
};
neIDs = tableState.selectedRowKeys.map(s => `${s}`);
}
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.traceManage.pcap.startTip', { title: row.neName }),
onOk() {
const hide = message.loading(t('common.loading'), 0);
const fromArr = neIDs.map(id => modalState.from[id]);
const reqArr = fromArr.map(from => {
from.loading = true;
const data = Object.assign({ cmd: from.cmdStart }, from.data);
if (from.data.neType === 'UPF' && from.cmdStart.startsWith('pcap')) {
return traceUPF(data);
}
return dumpStart(data);
});
Promise.allSettled(reqArr)
.then(resArr => {
resArr.forEach((res, idx) => {
const title = fromArr[idx].title;
if (res.status === 'fulfilled') {
const resV = res.value;
if (resV.code === RESULT_CODE_SUCCESS) {
if (!fromArr[idx].cmdStop) {
fromArr[idx].taskCode = resV.data;
}
fromArr[idx].loading = true;
message.success({
content: t('views.traceManage.pcap.startOk', { title }),
duration: 3,
});
} else {
fromArr[idx].loading = false;
message.warning({
content: `${resV.msg}`,
duration: 3,
});
}
} else {
fromArr[idx].loading = false;
message.error({
content: t('views.traceManage.pcap.startErr', { title }),
duration: 3,
});
}
});
})
.finally(() => {
hide();
});
},
});
}
/**
* 停止
* @param row 行记录
*/
function fnRecordStop(row?: Record<string, any>) {
let neIDs: string[] = [];
if (row) {
neIDs = [`${row.id}`];
} else {
row = {
neName: t('views.traceManage.pcap.textSelect'),
};
neIDs = tableState.selectedRowKeys.map(s => `${s}`);
}
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.traceManage.pcap.stopTip', { title: row.neName }),
onOk() {
const fromArr = neIDs.map(id => modalState.from[id]);
const reqArr: any = [];
for (const from of fromArr) {
if (from.data.neType === 'UPF' && from.cmdStart.startsWith('pcap')) {
reqArr.push(
traceUPF(Object.assign({ cmd: from.cmdStop }, from.data))
);
} else {
const taskCode = from.taskCode;
if (!taskCode) {
message.warning({
content: t('views.traceManage.pcap.stopNotRun', {
title: from.title,
}),
duration: 3,
});
continue;
}
reqArr.push(
dumpStop(Object.assign({ taskCode: from.taskCode }, from.data))
);
}
}
if (reqArr.length === 0) return;
const hide = message.loading(t('common.loading'), 0);
Promise.allSettled(reqArr)
.then(resArr => {
resArr.forEach((res, idx) => {
const title = fromArr[idx].title;
if (res.status === 'fulfilled') {
const resV = res.value;
fromArr[idx].loading = false;
fromArr[idx].taskFiles = [];
if (fromArr[idx].cmdStop) {
fromArr[idx].taskCode = '';
}
if (resV.code === RESULT_CODE_SUCCESS) {
if (fromArr[idx].cmdStop) {
fromArr[idx].taskCode = resV.data;
} else {
fromArr[idx].taskFiles = resV.data;
}
message.success({
content: t('views.traceManage.pcap.stopOk', { title }),
duration: 3,
});
} else if (resV.msg.indexOf('not run') > 0) {
message.warning({
content: t('views.traceManage.pcap.stopNotRun', { title }),
duration: 3,
});
} else {
message.warning({
content: `${resV.msg}`,
duration: 3,
});
}
} else {
message.error({
content: t('views.traceManage.pcap.stopErr', { title }),
duration: 3,
});
}
});
})
.finally(() => {
hide();
});
},
});
}
/**下载PCAP文件 */
function fnDownPCAP(row?: Record<string, any>) {
let neIDs: string[] = [];
if (row) {
neIDs = [`${row.id}`];
} else {
row = {
neName: t('views.traceManage.pcap.textSelect'),
};
neIDs = tableState.selectedRowKeys.map(s => `${s}`);
}
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.traceManage.pcap.downTip', { title: row.neName }),
onOk() {
const fromArr = neIDs.map(id => modalState.from[id]);
const reqArr: any = [];
for (const from of fromArr) {
const taskCode = from.taskCode;
if (!taskCode) {
message.warning({
content: t('views.traceManage.pcap.stopNotRun', {
title: from.title,
}),
duration: 3,
});
continue;
}
if (from.data.neType === 'UPF' && taskCode.startsWith('/tmp')) {
const fileName = taskCode.substring(taskCode.lastIndexOf('/') + 1);
reqArr.push(
getNeFile(
Object.assign(
{ path: '/tmp', fileName, delTemp: true },
from.data
)
)
);
} else {
const { neType, neUid } = from.data;
const path = `/usr/local/omc/tcpdump/${neType.toLowerCase()}/${neUid}/${taskCode}`;
reqArr.push(
getNeDirZip({
neType,
neUid,
path,
delTemp: true,
})
);
}
}
if (reqArr.length === 0) return;
const hide = message.loading(t('common.loading'), 0);
Promise.allSettled(reqArr)
.then(resArr => {
type successType = { data: Blob; filename: string }[];
const successResults: successType = [];
resArr.forEach((res, idx) => {
const title = fromArr[idx].title;
const taskCode = fromArr[idx].taskCode;
if (res.status === 'fulfilled') {
const resV = res.value;
if (resV.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('views.traceManage.pcap.downOk', { title }),
duration: 3,
});
// 文件名
let filename = `${title}_${Date.now()}.zip`;
if (taskCode.startsWith('/tmp')) {
filename = `${title}_${Date.now()}.pcap`;
}
successResults.push({
data: resV.data,
filename: filename,
});
} else {
message.warning({
content: `${resV.msg}`,
duration: 3,
});
}
} else {
message.error({
content: t('views.traceManage.pcap.downErr', { title }),
duration: 3,
});
}
});
// 分批下载函数每批3个间隔300ms
const batchDownload = async (results: successType) => {
const batchSize = 3;
const delay = 1_000; // 毫秒
for (let i = 0; i < results.length; i += batchSize) {
const batch = results.slice(i, i + batchSize);
// 处理当前批次
batch.forEach(item => {
saveAs(item.data, item.filename);
});
await new Promise(resolve =>
setTimeout(() => resolve(true), delay)
);
}
};
// 开始分批下载
batchDownload(successResults);
})
.finally(() => {
hide();
});
},
});
}
/**批量操作 */
function fnBatchOper(key: string) {
switch (key) {
case 'start':
fnRecordStart();
break;
case 'stop':
fnRecordStop();
break;
case 'down':
fnDownPCAP();
break;
default:
console.warn('undefined batch oper', key);
break;
}
}
/**
* 对话框弹出显示为 查看
* @param dictId 编号id
*/
function fnModalVisibleByVive(id: string | number) {
const from = modalState.from[id];
if (!from) return;
const { neType, neUid } = from.data;
const path = `/usr/local/omc/tcpdump/${neType.toLowerCase()}/${neUid}/${
from.taskCode
}`;
const files = from.taskFiles.filter(f => f.endsWith('log'));
modalState.viewFrom.neType = neType;
modalState.viewFrom.neUid = neUid;
modalState.viewFrom.path = path;
modalState.viewFrom.files = [...files];
fnViveTab(files[0]);
modalState.openByView = true;
}
/**对话框tab查看 */
function fnViveTab(action: any) {
// console.log('fnViveTab', action);
if (modalState.viewFrom.action === action) return;
modalState.viewFrom.action = action;
modalState.viewFrom.content = '';
const { neType, neUid, path } = modalState.viewFrom;
getNeViewFile({
neUid,
neType,
path,
fileName: action,
}).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
modalState.viewFrom.content = res.data;
} else {
modalState.viewFrom.content = '';
message.warning({
content: `${res.msg}`,
duration: 3,
});
}
});
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalCancel() {
modalState.openByView = false;
modalState.viewFrom.action = '';
modalState.viewFrom.files = [];
}
/**跳转文件数据页面 */
function fnFileView(row?: Record<string, any>) {
let query = undefined;
if (row) {
const from = modalState.from[row.id];
query = {
neUid: from.data.neUid,
neType: from.data.neType,
};
}
router.push({
path: `${route.path}${MENU_PATH_INLINE}/file`,
query: query,
});
}
onMounted(() => {
// 获取网元列表
fnGetList();
});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8" align="center">
<a-button @click="fnFileView()">
<FileSearchOutlined />
{{ t('views.traceManage.pcap.fileView') }}
</a-button>
<a-dropdown trigger="click">
<a-button :disabled="tableState.selectedRowKeys.length <= 0">
{{ t('views.traceManage.pcap.batchOper') }}
<DownOutlined />
</a-button>
<template #overlay>
<a-menu @click="({ key }:any) => fnBatchOper(key)">
<a-menu-item key="start">
<PlayCircleOutlined />
{{ t('views.traceManage.pcap.batchStartText') }}
</a-menu-item>
<a-menu-item key="stop">
<StopOutlined />
{{ t('views.traceManage.pcap.batchStopText') }}
</a-menu-item>
<a-menu-item key="down">
<DownloadOutlined />
{{ t('views.traceManage.pcap.batchDownText') }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</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="id"
size="small"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:pagination="false"
:scroll="{ x: tableColumns.length * 170 }"
:row-selection="{
type: 'checkbox',
selectedRowKeys: tableState.selectedRowKeys,
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'cmd'">
<a-auto-complete
v-model:value="modalState.from[record.id].cmdStart"
:options="
record.neType === 'UPF'
? modalState.cmdOptions.concat(modalState.cmdOptionsUPF)
: modalState.cmdOptions
"
:placeholder="t('views.traceManage.pcap.capArgPlease')"
:disabled="modalState.from[record.id].loading"
allow-clear
@select="(_: any, option: any) => fnSelectCmd(record.id, option)"
style="width: 100%"
/>
</template>
<template v-if="column.key === 'id'">
<a-space :size="8" align="start" direction="horizontal">
<a-tooltip placement="topRight">
<template #title>
<div>{{ t('views.traceManage.pcap.textStart') }}</div>
</template>
<a-button
type="primary"
size="small"
:disabled="modalState.from[record.id].loading"
@click.prevent="fnRecordStart(record)"
>
<template #icon><PlayCircleOutlined /> </template>
</a-button>
</a-tooltip>
<a-tooltip
placement="topRight"
v-if="
modalState.from[record.id].loading ||
modalState.from[record.id].cmdStop
"
>
<template #title>
<div>{{ t('views.traceManage.pcap.textStop') }}</div>
</template>
<a-button
type="default"
danger
size="small"
@click.prevent="fnRecordStop(record)"
>
<template #icon><StopOutlined /> </template>
</a-button>
</a-tooltip>
<a-tooltip
placement="topRight"
v-if="
!modalState.from[record.id].loading &&
modalState.from[record.id].taskFiles.length > 0
"
>
<template #title>
<div>{{ t('views.traceManage.pcap.textLog') }}</div>
</template>
<a-button
type="primary"
ghost
size="small"
@click.prevent="fnModalVisibleByVive(record.id)"
>
<template #icon><FileTextOutlined /> </template>
</a-button>
</a-tooltip>
<a-tooltip
placement="topRight"
v-if="
!modalState.from[record.id].loading &&
!!modalState.from[record.id].taskCode
"
>
<template #title>
<div>{{ t('views.traceManage.pcap.textDown') }}</div>
</template>
<a-button
type="primary"
ghost
size="small"
@click.prevent="fnDownPCAP(record)"
>
<template #icon><DownloadOutlined /> </template>
</a-button>
</a-tooltip>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 日志信息框 -->
<ProModal
:drag="true"
:fullscreen="true"
:borderDraw="true"
:width="800"
:open="modalState.openByView"
:footer="false"
:maskClosable="false"
:keyboard="false"
:body-style="{ padding: '0 12px 12px' }"
:title="t('views.traceManage.pcap.textLogMsg')"
@cancel="fnModalCancel"
>
<a-tabs
v-model:activeKey="modalState.viewFrom.action"
tab-position="top"
size="small"
@tabClick="fnViveTab"
>
<a-tab-pane
v-for="fileName in modalState.viewFrom.files"
:key="fileName"
:tab="fileName"
:destroyInactiveTabPane="false"
>
<a-spin :spinning="!modalState.viewFrom.content">
<a-textarea
:value="modalState.viewFrom.content"
:auto-size="{ minRows: 2 }"
:disabled="true"
style="color: rgba(0, 0, 0, 0.85)"
/>
</a-spin>
</a-tab-pane>
</a-tabs>
</ProModal>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>Overview 核心网 概览</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,381 @@
<script setup lang="ts">
import { reactive, onMounted, toRaw, defineAsyncComponent } from 'vue';
import { SizeType } from 'ant-design-vue/es/config-provider';
import { ColumnsType } from 'ant-design-vue/es/table';
import { Modal, message } from 'ant-design-vue/es';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { delNeHost, listNeHost } from '@/api/ne/neHost';
import useNeStore from '@/store/modules/ne';
import useDictStore from '@/store/modules/dict';
import useI18n from '@/hooks/useI18n';
const neStore = useNeStore();
const { getDict } = useDictStore();
const { t } = useI18n();
const EditModal = defineAsyncComponent(
() => import('@/views/ne/neHost/components/EditModal.vue')
);
const emit = defineEmits(['modal', 'link']);
/**字典数据 */
let dict: {
/**主机类型 */
neHostType: DictType[];
/**分组 */
neHostGroupId: DictType[];
/**认证模式 */
neHostAuthMode: DictType[];
} = reactive({
neHostType: [],
neHostGroupId: [],
neHostAuthMode: [],
});
/**查询参数 */
let queryParams = reactive({
/**主机类型 */
hostType: undefined,
/**分组 */
groupId: undefined,
/**名称 */
title: 'OMC',
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 10,
sortField: 'createTime',
sortOrder: 'desc',
});
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**记录数据 */
data: object[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'small',
data: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: t('common.rowId'),
dataIndex: 'id',
align: 'center',
width: 50,
},
{
title: t('views.ne.neHost.hostType'),
dataIndex: 'hostType',
key: 'hostType',
align: 'left',
width: 100,
},
{
title: t('views.ne.neHost.groupId'),
dataIndex: 'groupId',
key: 'groupId',
align: 'left',
width: 100,
},
{
title: t('views.ne.neHost.title'),
dataIndex: 'title',
align: 'left',
width: 100,
},
{
title: t('views.ne.neHost.addr'),
dataIndex: 'addr',
align: 'left',
width: 100,
},
{
title: t('views.ne.neHost.port'),
dataIndex: 'port',
align: 'left',
width: 100,
},
{
title: t('common.operate'),
key: 'id',
align: 'left',
},
];
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 10,
/**默认的每页条数 */
defaultPageSize: 10,
/**指定每页可以显示多少条 */
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();
},
});
/**查询信息列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (tableState.loading) return;
tableState.loading = true;
if (pageNum) {
queryParams.pageNum = pageNum;
}
listNeHost(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);
}
}
tableState.loading = false;
});
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**新增框或修改框是否显示 */
openByEdit: boolean;
/**记录ID */
id: number | undefined;
/**确定按钮 loading */
confirmLoading: boolean;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
openByEdit: false,
id: undefined,
confirmLoading: false,
});
/**
* 对话框弹出显示为 新增或者修改
* @param roleId 角色编号ID, 不传为新增
*/
function fnModalVisibleByEdit(roleId?: undefined) {
modalState.id = roleId;
modalState.openByEdit = true;
emit('modal');
}
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
function fnModalOk() {
// 获取列表数据
fnGetList();
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalCancel() {
modalState.openByEdit = false;
modalState.id = undefined;
}
/**
* 网元主机删除
* @param id 网元主机编号ID
*/
function fnRecordDelete(id: string) {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.ne.neHost.delTip', { num: id }),
onOk() {
const hide = message.loading(t('common.loading'), 0);
delNeHost(id).then(res => {
hide();
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 2,
});
fnGetList();
} else {
message.error({
content: `${res.msg}`,
duration: 2,
});
}
});
},
});
}
/**
* 网元主机连接
* @param id 网元主机编号ID
*/
function fnRecordLink(host: Record<string, any>) {
emit('link', JSON.parse(JSON.stringify(host)));
}
onMounted(() => {
// 初始字典数据
Promise.allSettled([
getDict('ne_host_type'),
getDict('ne_host_groupId'),
getDict('ne_host_authMode'),
]).then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.neHostType = resArr[0].value;
}
if (resArr[1].status === 'fulfilled') {
dict.neHostGroupId = resArr[1].value;
}
if (resArr[2].status === 'fulfilled') {
dict.neHostAuthMode = resArr[2].value;
}
});
// 获取列表数据
fnGetList();
});
</script>
<template>
<a-card :bordered="false" :body-style="{ padding: '0px' }" size="small">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8" align="center">
<a-button
type="primary"
size="small"
@click.prevent="fnModalVisibleByEdit()"
>
<template #icon><PlusOutlined /></template>
{{ t('common.addText') }}
</a-button>
<a-button type="text" size="small" @click.prevent="fnGetList()">
<template #icon><ReloadOutlined /></template>
{{ t('common.reloadText') }}
</a-button>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center" direction="horizontal">
<a-form layout="inline">
<a-form-item :label="t('views.ne.neHost.hostType')" name="hostType">
<a-select
v-model:value="queryParams.hostType"
:options="dict.neHostType"
allow-clear
size="small"
:placeholder="t('common.selectPlease')"
@change="fnGetList(1)"
style="width: 100px"
/>
</a-form-item>
<a-form-item :label="t('views.ne.neHost.title')" name="title">
<a-auto-complete
v-model:value="queryParams.title"
:options="neStore.getNeSelectOtions"
allow-clear
size="small"
:placeholder="t('common.inputPlease')"
@change="fnGetList(1)"
style="width: 180px"
/>
</a-form-item>
</a-form>
</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: tableColumns.length * 120 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'groupId'">
<DictTag :options="dict.neHostGroupId" :value="record.groupId" />
</template>
<template v-if="column.key === 'hostType'">
<DictTag :options="dict.neHostType" :value="record.hostType" />
</template>
<template v-if="column.key === 'id'">
<a-space :size="8" align="center">
<a-tooltip>
<template #title> Link to host </template>
<a-button type="link" @click.prevent="fnRecordLink(record)">
<template #icon><CodeOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>{{ t('common.editText') }}</template>
<a-button
type="link"
@click.prevent="fnModalVisibleByEdit(record.id)"
>
<template #icon><FormOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip v-if="record.groupId !== '1'">
<template #title>{{ t('common.deleteText') }}</template>
<a-button type="link" @click.prevent="fnRecordDelete(record.id)">
<template #icon><DeleteOutlined /></template>
</a-button>
</a-tooltip>
</a-space>
</template>
</template>
</a-table>
<!-- 新增框或修改框 -->
<EditModal
v-model:open="modalState.openByEdit"
:edit-id="modalState.id"
:ne-group="true"
@ok="fnModalOk"
@cancel="fnModalCancel"
></EditModal>
</a-card>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,340 @@
<script lang="ts" setup>
import { PageContainer } from 'antdv-pro-layout';
import { Modal } from 'ant-design-vue/es';
import { useFullscreen } from '@vueuse/core';
import { defineAsyncComponent, reactive, ref } from 'vue';
import { parseDuration } from '@/utils/date-utils';
import useI18n from '@/hooks/useI18n';
const { t } = useI18n();
const TerminalSSH = defineAsyncComponent(
() => import('@/components/TerminalSSH/index.vue')
);
const TerminalTelnet = defineAsyncComponent(
() => import('@/components/TerminalTelnet/index.vue')
);
const TerminalRedis = defineAsyncComponent(
() => import('@/components/TerminalRedis/index.vue')
);
const HostList = defineAsyncComponent(
() => import('./components/hostList.vue')
);
// 全屏
const terminalCard = ref<HTMLElement | null>(null);
const { isFullscreen, toggle } = useFullscreen(terminalCard);
/**主机对象信息状态类型 */
type HostStateType = {
/**显示主机列表 */
show: boolean;
/**加载等待 */
loading: boolean;
/**查询参数 */
params: {
pageNum: number;
pageSize: number;
};
/**数据总数 */
total: number;
data: Record<string, any>[];
};
/**主机对象信息状态 */
const hostState: HostStateType = reactive({
show: false,
loading: false,
params: {
pageNum: 1,
pageSize: 20,
},
total: 0,
data: [],
});
/**连接主机 */
function fnConnectHost(data: Record<string, any>) {
const id = `${Date.now()}`;
tabState.panes.push({
id,
status: false,
host: data,
});
tabState.activeKey = id;
}
/**标签对象信息状态类型 */
type TabStateType = {
/**激活选中 */
activeKey: string;
/**页签数据 */
panes: {
id: string;
status: boolean;
host: Record<string, any>;
connectStamp?: string;
}[];
};
/**标签对象信息状态 */
const tabState: TabStateType = reactive({
activeKey: '0',
panes: [
{
id: '0',
host: {
id: 0,
title: t('views.tool.terminal.start'),
type: '0',
},
status: true,
},
],
});
/**
* 终端连接状态
* @param data 主机连接结果
*/
function fnTerminalConnect(data: Record<string, any>) {
const { id, timeStamp } = data;
const seconds = timeStamp / 1000;
// 获取当前项下标
const tab = tabState.panes.find(item => item.id === id);
if (tab) {
tab.status = true;
tab.connectStamp = parseDuration(seconds);
}
}
/**
* 终端关闭状态
* @param data 主机连接结果
*/
function fnTerminalClose(data: Record<string, any>) {
const { id } = data;
// 获取当前项下标
const tab = tabState.panes.find(item => item.id === id);
if (tab) {
tab.status = false;
}
}
/**
* 标签更多菜单项
* @param key 菜单key
*/
function fnTabMenu(key: string | number) {
// 刷新当前
if (key === 'reload') {
const tabIndex = tabState.panes.findIndex(
item => item.id === tabState.activeKey
);
if (tabIndex) {
const tab = tabState.panes[tabIndex];
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.tool.terminal.reloadTip', {
num: `${tab.host.hostType} - ${tab.host.title}`,
}),
onOk() {
tabState.panes.splice(tabIndex, 1);
tab.host && fnConnectHost(tab.host);
},
});
}
}
// 关闭当前
if (key === 'current') {
fnTabClose(tabState.activeKey);
}
// 关闭其他
if (key === 'other') {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.tool.terminal.otherTip'),
onOk() {
hostState.show = false;
tabState.panes = tabState.panes.filter(
tab => tab.id === '0' || tab.id === tabState.activeKey
);
tabState.activeKey = tabState.activeKey;
},
});
}
// 关闭全部
if (key === 'all') {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.tool.terminal.allTip'),
onOk() {
hostState.show = false;
tabState.panes.splice(1);
tabState.activeKey = '0';
},
});
}
}
/**
* 导航标签关闭
* @param id 标签的key
*/
function fnTabClose(id: string) {
if (isFullscreen.value) toggle();
// 获取当前项下标
const tabIndex = tabState.panes.findIndex(tab => tab.id === id);
if (tabIndex === -1) return;
const item = tabState.panes[tabIndex];
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.tool.terminal.closeTip', {
num: `${item.host.hostType.toUpperCase()} - ${item.host.title}`,
}),
onOk() {
tabState.panes.splice(tabIndex, 1);
// 激活前一项标签
tabState.activeKey = tabState.panes[tabIndex - 1].id;
},
});
}
</script>
<template>
<PageContainer>
<a-card
:bordered="false"
size="small"
:body-style="{ padding: '12px' }"
ref="terminalCard"
>
<a-tabs
class="terminal-tabs"
hide-add
size="small"
tab-position="top"
type="editable-card"
:tab-bar-gutter="8"
:tab-bar-style="{ margin: '0' }"
v-model:activeKey="tabState.activeKey"
@edit="(id:any) => fnTabClose(id)"
>
<a-tab-pane
v-for="pane in tabState.panes"
:key="pane.id"
:closable="pane.id !== '0'"
>
<template #tab>
<a-badge
:status="pane.status ? 'success' : 'error'"
:text="pane.host.title"
/>
</template>
<div class="pane-box">
<!-- 非开始页的ssh主机 -->
<TerminalSSH
v-if="pane.id !== '0' && pane.host.hostType === 'ssh'"
:id="pane.id"
:hostId="pane.host.id"
@connect="fnTerminalConnect"
@close="fnTerminalClose"
>
</TerminalSSH>
<!-- 非开始页的telnet主机 -->
<TerminalTelnet
v-if="pane.id !== '0' && pane.host.hostType === 'telnet'"
:id="pane.id"
:hostId="pane.host.id"
init-cmd="help"
:disable="true"
@connect="fnTerminalConnect"
@close="fnTerminalClose"
>
</TerminalTelnet>
<!-- 非开始页的redis主机 -->
<TerminalRedis
v-if="pane.id !== '0' && pane.host.hostType === 'redis'"
:id="pane.id"
:hostId="pane.host.id"
init-cmd="PING"
@connect="fnTerminalConnect"
@close="fnTerminalClose"
>
</TerminalRedis>
<!-- 开始页 -->
<div v-if="pane.id === '0'">
<!-- 主机列表 -->
<HostList
v-show="tabState.activeKey === '0'"
@modal="() => (isFullscreen ? toggle() : null)"
@link="fnConnectHost"
></HostList>
</div>
</div>
</a-tab-pane>
<template #rightExtra>
<a-space :size="8" align="center">
<a-tooltip placement="topRight">
<template #title>
{{ t('loayouts.rightContent.fullscreen') }}
</template>
<a-button
type="default"
shape="circle"
size="small"
style="color: inherit"
@click="toggle"
>
<template #icon>
<FullscreenExitOutlined v-if="isFullscreen" />
<FullscreenOutlined v-else />
</template>
</a-button>
</a-tooltip>
<!-- 非开始页的更多操作 -->
<div v-show="tabState.activeKey !== '0'">
<a-tooltip placement="topRight">
<template #title>
{{ t('views.tool.terminal.more') }}
</template>
<a-dropdown trigger="click" placement="bottomRight">
<a-button type="ghost" shape="circle" size="small">
<template #icon><EllipsisOutlined /></template>
</a-button>
<template #overlay>
<a-menu @click="({ key }:any) => fnTabMenu(key)">
<a-menu-item key="reload">
{{ t('views.tool.terminal.reload') }}
</a-menu-item>
<a-menu-item key="current">
{{ t('views.tool.terminal.current') }}
</a-menu-item>
<a-menu-item key="other">
{{ t('views.tool.terminal.other') }}
</a-menu-item>
<a-menu-item key="all">
{{ t('views.tool.terminal.all') }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-tooltip>
</div>
</a-space>
</template>
</a-tabs>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped>
.pane-box {
height: calc(100vh - 200px);
overflow-x: hidden;
}
</style>

View File

@@ -0,0 +1,518 @@
<script setup lang="ts">
import { reactive, onMounted, onBeforeUnmount, ref } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { message, Modal } from 'ant-design-vue/es';
import useI18n from '@/hooks/useI18n';
import {
RESULT_CODE_ERROR,
RESULT_CODE_SUCCESS,
} from '@/constants/result-constants';
import TerminalSSHView from '@/components/TerminalSSHView/index.vue';
import useNeStore from '@/store/modules/ne';
import { iperfV } from '@/api/tool/iperf';
const neStore = useNeStore();
const { t } = useI18n();
/**网元参数 */
let neCascaderOptions = ref<Record<string, any>[]>([]);
/**状态对象 */
let state = reactive({
/**初始化 */
initialized: false,
/**运行中 */
running: false,
/**版本信息 */
versionInfo: [],
/**网元类型 */
neType: [],
/**数据类型 */
dataType: 'options' as 'options' | 'command',
/**ws参数 */
params: {
neType: '',
neUid: '',
cols: 120,
rows: 40,
},
/**ws数据 */
data: {
command: '', // 命令字符串
version: 'V3', // 服务版本默认V3
mode: 'client', // 服务端或客户端,默认服务端
// Server or Client
port: 5201, // 服务端口
interval: 1, // 每次报告之间的时间间隔,单位为秒
// Server
oneOff: false, // 只进行一次连接
// Client
host: '', // 客户端连接到的服务端IP地址
udp: false, // use UDP rather than TCP
time: 10, // 以秒为单位的传输时间(默认为 10 秒)
reverse: false, // 以反向模式运行(服务器发送,客户端接收)
window: '300k', // 设置窗口大小/套接字缓冲区大小
parallel: 1, // 运行的并行客户端流数量
bitrate: 0, // 以比特/秒为单位0 表示无限制)
},
});
/**连接发送 */
async function fnIPerf() {
const [neType, neUid] = state.neType;
if (!neType || !neUid) {
message.warning({
content: 'No Found NE Type',
duration: 2,
});
return;
}
if (
state.dataType === 'options' &&
state.data.mode === 'client' &&
state.data.host === ''
) {
message.warning({
content: 'Please fill in the Host',
duration: 2,
});
return;
}
if (state.dataType === 'command' && state.data.command === '') {
message.warning({
content: 'Please fill in the Command',
duration: 2,
});
return;
}
// 网元切换时重置
if (neType !== state.params.neType || neUid !== state.params.neUid) {
state.initialized = false;
state.params.neType = neType;
state.params.neUid = neUid;
}
// 软件版本检查
state.params.neType = neType;
state.params.neUid = neUid;
const resVersion = await iperfV({
neType,
neUid,
version: state.data.version,
});
if (resVersion.code !== RESULT_CODE_SUCCESS) {
Modal.confirm({
title: t('common.tipTitle'),
content: 'Not found iperf version, please install (https://iperf.fr)',
onOk: () => {},
});
return;
} else {
state.versionInfo = resVersion.data;
}
// 初始化的直接重发
if (state.initialized) {
fnResend();
return;
}
state.initialized = true;
}
/**终端实例 */
const toolTerminal = ref();
/**重置并停止 */
function fnReset() {
if (!toolTerminal.value) return;
toolTerminal.value.ctrlC();
// toolTerminal.value.clear();
state.running = false;
}
/**重载发送 */
function fnResend() {
if (!toolTerminal.value) return;
state.running = true;
toolTerminal.value.ctrlC();
setTimeout(() => {
toolTerminal.value.clear();
const data = JSON.parse(JSON.stringify(state.data));
if (state.dataType === 'options') data.command = '';
toolTerminal.value.send('iperf', data);
}, 1000);
}
/**终端初始连接*/
function fnConnect() {
fnResend();
}
/**终端消息处理*/
function fnProcessMessage(data: string): string {
// 查找的开始输出标记
const parts: string[] = data.split('\u001b[?2004l\r');
if (parts.length > 0) {
if (parts[0].startsWith('^C') || parts[0].startsWith('\r')) {
return '';
}
let text = parts[parts.length - 1];
// 找到最后输出标记
let lestIndex = text.lastIndexOf('\u001b[?2004h\u001b]0;');
if (lestIndex !== -1) {
text = text.substring(0, lestIndex);
}
if (text === '' || text === '\r\n' || text.startsWith('^C')) {
return '';
}
// 是否还有最后输出标记
lestIndex = text.lastIndexOf('\u001b[?2004h');
if (lestIndex !== -1) {
text = text.substring(0, lestIndex);
}
if (text.endsWith('# ')) {
text = text.substring(0, text.lastIndexOf('\r\n') + 2);
}
// console.log({ parts, text });
if (parts.length > 1 && parts[0].startsWith('iperf')) {
return parts[0] + '\r\n' + text;
}
return text;
}
return data;
}
/**终端消息监听*/
function fnMessage(res: Record<string, any>) {
const { code, requestId, data } = res;
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
if (!requestId) return;
let lestIndex = data.lastIndexOf('unable to');
if (lestIndex !== -1) {
state.running = false;
return;
}
lestIndex = data.lastIndexOf('failed:');
if (lestIndex !== -1) {
state.running = false;
return;
}
lestIndex = data.lastIndexOf('iperf Done.');
if (lestIndex !== -1) {
state.running = false;
return;
}
}
/**钩子函数,界面打开初始化*/
onMounted(() => {
// 获取网元网元列表
for (const item of neStore.getNeCascaderOptions) {
neCascaderOptions.value.push(item); // 过滤不可用的网元
}
if (neCascaderOptions.value.length === 0) {
message.warning({
content: t('common.noData'),
duration: 2,
});
return;
}
});
/**钩子函数,界面关闭*/
onBeforeUnmount(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8">
<span>
{{ t('views.ne.common.neType') }}:
<a-cascader
v-model:value="state.neType"
:options="neCascaderOptions"
:placeholder="t('common.selectPlease')"
:disabled="state.running"
/>
</span>
<a-radio-group
v-model:value="state.dataType"
button-style="solid"
:disabled="state.running"
>
<a-radio-button value="options">Options</a-radio-button>
<a-radio-button value="command">Command</a-radio-button>
</a-radio-group>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8">
<a-button
@click.prevent="fnIPerf()"
type="primary"
:loading="state.running"
>
<template #icon><PlayCircleOutlined /></template>
{{ state.running ? 'Running' : 'Launch' }}
</a-button>
<a-button v-if="state.running" @click.prevent="fnReset()" danger>
<template #icon><CloseCircleOutlined /></template>
Stop
</a-button>
<!-- 版本信息 -->
<a-popover
trigger="click"
placement="bottomRight"
v-if="state.versionInfo.length > 0"
>
<template #content>
<div v-for="v in state.versionInfo">{{ v }}</div>
</template>
<InfoCircleOutlined />
</a-popover>
</a-space>
</template>
<!-- options -->
<a-form
v-if="state.dataType === 'options'"
:model="state.data"
name="queryParams"
layout="horizontal"
:label-col="{ span: 6 }"
:label-wrap="true"
style="padding: 12px"
>
<a-row :gutter="16">
<a-col :span="4" :offset="2">
<a-form-item label="Mode" name="mode">
<a-radio-group
v-model:value="state.data.mode"
:disabled="state.running"
button-style="solid"
size="small"
>
<a-radio-button value="client">Client</a-radio-button>
<a-radio-button value="server">Server</a-radio-button>
</a-radio-group>
</a-form-item>
</a-col>
<a-col :span="4">
<a-form-item
label="Version"
name="version"
:label-col="{ span: 8 }"
:label-wrap="true"
>
<a-radio-group
v-model:value="state.data.version"
:disabled="state.running"
button-style="solid"
size="small"
>
<a-radio-button value="V2">V2</a-radio-button>
<a-radio-button value="V3">V3</a-radio-button>
</a-radio-group>
</a-form-item>
</a-col>
<a-col :lg="6" :md="6" :xs="12">
<a-form-item label="Port" name="port">
<a-input-number
v-model:value="state.data.port"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
:min="1024"
:max="65535"
></a-input-number> </a-form-item
></a-col>
<a-col :lg="6" :md="6" :xs="12">
<a-form-item label="Interval" name="interval">
<a-input-number
v-model:value="state.data.interval"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
:min="1"
:max="30"
></a-input-number>
</a-form-item>
</a-col>
</a-row>
<template v-if="state.data.mode === 'client'">
<a-form-item
label="Host"
name="host"
help="run in client mode, connecting to <host>"
:label-col="{ span: 3 }"
:wrapper-col="{ span: 9 }"
:label-wrap="true"
>
<a-input
v-model:value="state.data.host"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
>
</a-input>
</a-form-item>
<a-row>
<a-col :lg="12" :md="12" :xs="12">
<a-form-item
label="UDP"
name="udp"
help="use UDP rather than TCP"
>
<a-switch
v-model:checked="state.data.udp"
:disabled="state.running"
/>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="12">
<a-form-item
label="Reverse"
name="reverse"
help="run in reverse mode (server sends, client receives)"
>
<a-switch
v-model:checked="state.data.reverse"
:disabled="state.running"
/>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="12">
<a-form-item
label="Time"
name="time"
help="time in seconds to transmit for (default 10 secs)"
>
<a-input-number
v-model:value="state.data.time"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
:min="1"
></a-input-number>
</a-form-item>
<a-form-item
label="Parallel"
name="parallel"
help="number of parallel client streams to run"
>
<a-input-number
v-model:value="state.data.parallel"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
:min="1"
></a-input-number>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="12">
<a-form-item
label="Window"
name="window"
help="set window size / socket buffer size"
>
<a-input
v-model:value="state.data.window"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
></a-input>
</a-form-item>
<a-form-item
label="Bitrate"
name="bitrate"
help="target bitrate in bits/sec (0 for unlimited)"
>
<a-input-number
v-model:value="state.data.bitrate"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
:min="1"
></a-input-number>
</a-form-item>
</a-col>
</a-row>
</template>
<a-row v-else>
<a-col :lg="12" :md="12" :xs="12">
<a-form-item
label="OneOff"
name="oneOff"
help=" handle one client connection then exit"
>
<a-switch
v-model:checked="state.data.oneOff"
:disabled="state.running"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
<!-- command -->
<div v-else style="padding: 12px">
<a-input-group compact>
<a-select
v-model:value="state.data.version"
:disabled="state.running"
>
<a-select-option value="V2">iperf</a-select-option>
<a-select-option value="V3">iperf3</a-select-option>
</a-select>
<a-auto-complete
v-model:value="state.data.command"
:disabled="state.running"
:options="[
{ value: '--help' },
{ value: '-c 172.5.16.100 -p 5201' },
{ value: '-s -p 5201' },
]"
style="width: 400px"
placeholder="client or server command"
/>
</a-input-group>
</div>
<!-- 运行过程 -->
<TerminalSSHView
v-if="state.initialized"
ref="toolTerminal"
:id="`V${Date.now()}`"
prefix="iperf"
url="/tool/iperf/run"
:ne-type="state.params.neType"
:ne-uid="state.params.neUid"
:rows="state.params.rows"
:cols="state.params.cols"
:process-messages="fnProcessMessage"
style="height: 400px"
@connect="fnConnect"
@message="fnMessage"
></TerminalSSHView>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,338 @@
<script setup lang="ts">
import { reactive, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { ColumnsType } from 'ant-design-vue/es/table';
import useI18n from '@/hooks/useI18n';
import { OptionsType, WS } from '@/plugins/ws-websocket';
import { RESULT_CODE_ERROR } from '@/constants/result-constants';
const { t } = useI18n();
const ws = new WS();
/**表单查询参数 */
let queryParams = reactive({
pid: undefined,
name: '',
port: undefined,
});
/**状态对象 */
let state = reactive({
/**调度器 */
interval: null as any,
/**刷新周期 */
intervalTime: 5_000,
/**查询参数 */
query: {
pid: undefined,
name: '',
port: undefined,
},
});
/**接收数据后回调(成功) */
function wsMessage(res: Record<string, any>) {
const { code, requestId, data } = res;
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
// 建联时发送请求
if (!requestId && data.clientId) {
fnGetList();
return;
}
// 收到消息数据
if (requestId.startsWith('net_')) {
// 将数据填入表格
if (Array.isArray(data)) {
if (tableState.loading) {
tableState.loading = false;
}
tableState.data = data;
} else {
tableState.data = [];
}
}
}
/**实时数据*/
function fnRealTime(reLink: boolean) {
if (reLink) {
ws.close();
}
const options: OptionsType = {
url: '/ws',
onmessage: wsMessage,
onerror: (ev: any) => {
// 接收数据后回调
console.error(ev);
},
};
//建立连接
ws.connect(options);
}
/**调度器周期变更*/
function fnIntervalChange(v: any) {
clearInterval(state.interval);
state.interval = null;
const timer = parseInt(v);
if (timer > 1_000) {
state.intervalTime = v;
fnGetList();
}
}
/**查询列表 */
function fnGetList() {
if (tableState.loading || ws.state() === -1) return;
tableState.loading = true;
const msg = {
requestId: `net_${state.interval}`,
type: 'net',
data: state.query,
};
// 首发
ws.send(msg);
// 定时刷新数据
state.interval = setInterval(() => {
msg.data = JSON.parse(JSON.stringify(state.query));
ws.send(msg);
}, state.intervalTime);
}
/**查询参数传入 */
function fnQuery() {
state.query = JSON.parse(JSON.stringify(queryParams));
nextTick(() => {
ws.send({
requestId: `net_${state.interval}`,
type: 'net',
data: state.query,
});
});
}
/**查询参数重置 */
function fnQueryReset() {
Object.assign(queryParams, {
pid: undefined,
name: '',
port: undefined,
});
tablePagination.current = 1;
tablePagination.pageSize = 20;
// 重置查询条件
Object.assign(state.query, {
pid: undefined,
name: '',
port: undefined,
});
}
/**表格状态类型 */
type TableStateType = {
/**加载等待 */
loading: boolean;
/**记录数据 */
data: object[];
};
/**表格状态 */
let tableState: TableStateType = reactive({
loading: false,
data: [],
});
/**表格字段列 */
const tableColumns: ColumnsType<any> = [
{
title: t('views.tool.ps.pid'),
dataIndex: 'pid',
align: 'right',
width: 100,
sorter: {
compare: (a: any, b: any) => a.pid - b.pid,
multiple: 3,
},
},
{
title: t('views.tool.net.proto'),
dataIndex: 'type',
align: 'left',
width: 100,
customRender(opt) {
return `${opt.value}`.toUpperCase();
},
},
{
title: t('views.tool.net.localAddr'),
dataIndex: 'localAddr',
align: 'left',
width: 150,
customRender(opt) {
const { ip, port } = opt.value;
return port !== 0 ? `${ip}:${port}` : `${ip}`;
},
},
{
title: t('views.tool.net.remoteAddr'),
dataIndex: 'remoteAddr',
align: 'left',
width: 150,
customRender(opt) {
const { ip, port } = opt.value;
return port !== 0 ? `${ip}:${port}` : `${ip}`;
},
},
{
title: t('views.tool.net.status'),
dataIndex: 'status',
align: 'left',
width: 120,
},
{
title: t('views.tool.ps.name'),
dataIndex: 'name',
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;
},
});
/**钩子函数,界面打开初始化*/
onMounted(() => {
fnRealTime(false);
});
/**钩子函数,界面关闭*/
onBeforeUnmount(() => {
clearInterval(state.interval);
state.interval = null;
ws.close();
});
</script>
<template>
<PageContainer>
<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="4" :md="6" :xs="12">
<a-form-item :label="t('views.tool.ps.pid')" name="pid">
<a-input-number
v-model:value="queryParams.pid"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
></a-input-number>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item :label="t('views.tool.ps.name')" name="name">
<a-input
v-model:value="queryParams.name"
allow-clear
:placeholder="t('common.inputPlease')"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item :label="t('views.tool.net.port')" name="port">
<a-input-number
v-model:value="queryParams.port"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
></a-input-number>
</a-form-item>
</a-col>
<a-col :lg="4" :md="12" :xs="24">
<a-form-item>
<a-space :size="8">
<a-button type="primary" @click.prevent="fnQuery()">
<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-form layout="inline">
<a-form-item :label="t('views.tool.ps.realTime')" name="realTime">
<a-select
v-model:value="state.intervalTime"
:options="[
{ label: t('views.tool.ps.realTimeHigh'), value: 3_000 },
{ label: t('views.tool.ps.realTimeRegular'), value: 5_000 },
{ label: t('views.tool.ps.realTimeLow'), value: 10_000 },
{ label: t('views.tool.ps.realTimeStop'), value: -1 },
]"
:placeholder="t('common.selectPlease')"
@change="fnIntervalChange"
style="width: 100px"
/>
</a-form-item>
</a-form>
</template>
<!-- 表格列表 -->
<a-table
class="table"
row-key="id"
:columns="tableColumns"
:pagination="tablePagination"
:loading="tableState.loading"
:data-source="tableState.data"
size="small"
:scroll="{ x: tableColumns.length * 120 }"
></a-table>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,408 @@
<script setup lang="ts">
import { reactive, onMounted, onBeforeUnmount, ref } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { message } from 'ant-design-vue/es';
import useI18n from '@/hooks/useI18n';
import {
RESULT_CODE_ERROR,
RESULT_CODE_SUCCESS,
} from '@/constants/result-constants';
import TerminalSSHView from '@/components/TerminalSSHView/index.vue';
import useNeStore from '@/store/modules/ne';
import { pingV } from '@/api/tool/ping';
const neStore = useNeStore();
const { t } = useI18n();
/**网元参数 */
let neCascaderOptions = ref<Record<string, any>[]>([]);
/**状态对象 */
let state = reactive({
/**初始化 */
initialized: false,
/**运行中 */
running: false,
/**版本信息 */
versionInfo: '',
/**网元类型 */
neType: [],
/**数据类型 */
dataType: 'options' as 'options' | 'command',
/**ws参数 */
params: {
neUid: '',
neType: '',
cols: 120,
rows: 40,
},
/**ws数据 */
data: {
command: '', // 命令字符串
desAddr: '8.8.8.8', // dns name or ip address
// Options
interval: 1, // seconds between sending each packet
ttl: 255, // define time to live
count: 4, // <count> 次回复后停止
size: 56, // 使用 <size> 作为要发送的数据字节数
timeout: 10, // seconds time to wait for response
},
});
/**连接发送 */
async function fnPing() {
const [neType, neUid] = state.neType;
if (!neType || !neUid) {
message.warning({
content: 'No Found NE Type',
duration: 2,
});
return;
}
if (state.dataType === 'options' && state.data.desAddr === '') {
message.warning({
content: 'Please fill in the Destination',
duration: 2,
});
return;
}
if (state.dataType === 'command' && state.data.command === '') {
message.warning({
content: 'Please fill in the Command',
duration: 2,
});
return;
}
// 网元切换时重置
if (neType !== state.params.neType || neUid !== state.params.neUid) {
state.initialized = false;
state.params.neType = neType;
state.params.neUid = neUid;
}
// 软件版本检查
state.params.neType = neType;
state.params.neUid = neUid;
const resVersion = await pingV({ neUid, neType });
if (resVersion.code !== RESULT_CODE_SUCCESS) {
message.warning({
content: 'No Found ping iputils',
duration: 2,
});
return;
} else {
state.versionInfo = resVersion.data;
}
// 初始化的直接重发
if (state.initialized) {
fnResend();
return;
}
state.initialized = true;
}
/**终端实例 */
const toolTerminal = ref();
/**重置并停止 */
function fnReset() {
if (!toolTerminal.value) return;
toolTerminal.value.ctrlC();
// toolTerminal.value.clear();
state.running = false;
}
/**重载发送 */
function fnResend() {
if (!toolTerminal.value) return;
state.running = true;
toolTerminal.value.ctrlC();
setTimeout(() => {
toolTerminal.value.clear();
const data = JSON.parse(JSON.stringify(state.data));
if (state.dataType === 'options') data.command = '';
toolTerminal.value.send('ping', data);
}, 1000);
}
/**终端初始连接*/
function fnConnect() {
fnResend();
}
/**终端消息处理*/
function fnProcessMessage(data: string): string {
// 查找的开始输出标记
const parts: string[] = data.split('\u001b[?2004l\r');
if (parts.length > 0) {
if (parts[0].startsWith('^C') || parts[0].startsWith('\r')) {
return '';
}
let text = parts[parts.length - 1];
// 找到最后输出标记
let lestIndex = text.lastIndexOf('\u001b[?2004h\u001b]0;');
if (lestIndex !== -1) {
text = text.substring(0, lestIndex);
}
if (text === '' || text === '\r\n' || text.startsWith('^C')) {
return '';
}
// 是否还有最后输出标记
lestIndex = text.lastIndexOf('\u001b[?2004h');
if (lestIndex !== -1) {
text = text.substring(0, lestIndex);
}
if (text.endsWith('# ')) {
text = text.substring(0, text.lastIndexOf('\r\n') + 2);
}
// console.log({ parts, text });
if (parts.length > 1 && parts[0].startsWith('ping')) {
return parts[0] + '\r\n' + text;
}
return text;
}
return data;
}
/**终端消息监听*/
function fnMessage(res: Record<string, any>) {
const { code, requestId, data } = res;
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
if (!requestId) return;
let lestIndex = data.lastIndexOf('ping statistics ---');
if (lestIndex !== -1) {
state.running = false;
return;
}
lestIndex = data.lastIndexOf('failure');
if (lestIndex !== -1) {
state.running = false;
return;
}
}
/**钩子函数,界面打开初始化*/
onMounted(() => {
// 获取网元网元列表
for (const item of neStore.getNeCascaderOptions) {
neCascaderOptions.value.push(item); // 过滤不可用的网元
}
if (neCascaderOptions.value.length === 0) {
message.warning({
content: t('common.noData'),
duration: 2,
});
return;
}
});
/**钩子函数,界面关闭*/
onBeforeUnmount(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8">
<span>
{{ t('views.ne.common.neType') }}:
<a-cascader
v-model:value="state.neType"
:options="neCascaderOptions"
:placeholder="t('common.selectPlease')"
:disabled="state.running"
/>
</span>
<a-radio-group
v-model:value="state.dataType"
button-style="solid"
:disabled="state.running"
>
<a-radio-button value="options">Options</a-radio-button>
<a-radio-button value="command">Command</a-radio-button>
</a-radio-group>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8">
<a-button
@click.prevent="fnPing()"
type="primary"
:loading="state.running"
>
<template #icon><PlayCircleOutlined /></template>
{{ state.running ? 'Running' : 'Launch' }}
</a-button>
<a-button v-if="state.running" @click.prevent="fnReset()" danger>
<template #icon><CloseCircleOutlined /></template>
Stop
</a-button>
<!-- 版本信息 -->
<a-popover
trigger="click"
placement="bottomRight"
v-if="state.versionInfo"
>
<template #content>
{{ state.versionInfo }}
</template>
<InfoCircleOutlined />
</a-popover>
</a-space>
</template>
<!-- options -->
<a-form
v-if="state.dataType === 'options'"
:model="state.data"
name="queryParams"
layout="horizontal"
:label-col="{ span: 6 }"
:label-wrap="true"
style="padding: 12px"
>
<a-form-item
label="Destination"
name="desAddr"
help="dns name or ip address"
:label-col="{ span: 3 }"
:label-wrap="true"
>
<a-input
v-model:value="state.data.desAddr"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
>
</a-input>
</a-form-item>
<a-row>
<a-col :lg="12" :md="12" :xs="12">
<a-form-item
label="Interval"
name="Interval"
help="seconds between sending each packet"
>
<a-input-number
v-model:value="state.data.interval"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
:min="1"
:max="120"
>
</a-input-number>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="12">
<a-form-item label="TTL" name="ttl" help="define time to live">
<a-input-number
v-model:value="state.data.ttl"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
:min="1"
:max="255"
></a-input-number>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="12">
<a-form-item
label="Count"
name="count"
help="stop after <count> replies"
>
<a-input-number
v-model:value="state.data.count"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
:min="1"
:max="120"
></a-input-number>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="12">
<a-form-item
label="Size"
name="size"
help="use <size> as number of data bytes to be sent"
>
<a-input-number
v-model:value="state.data.size"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
:min="1"
:max="128"
></a-input-number>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="12">
<a-form-item
label="Deadline"
name="timeout"
help="reply wait <deadline> in seconds"
>
<a-input-number
v-model:value="state.data.timeout"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
:min="1"
:max="60"
></a-input-number>
</a-form-item>
</a-col>
</a-row>
</a-form>
<!-- command -->
<div v-else style="padding: 12px">
<a-auto-complete
v-model:value="state.data.command"
:disabled="state.running"
:options="[{ value: '-help' }, { value: '-i 1 -c 4 8.8.8.8' }]"
:dropdown-match-select-width="500"
style="width: 100%"
>
<a-input addon-before="ping" placeholder="command" />
</a-auto-complete>
</div>
<!-- 运行过程 -->
<TerminalSSHView
v-if="state.initialized"
ref="toolTerminal"
:id="`V${Date.now()}`"
prefix="ping"
url="/tool/ping/run"
:ne-type="state.params.neType"
:ne-uid="state.params.neUid"
:rows="state.params.rows"
:cols="state.params.cols"
:process-messages="fnProcessMessage"
style="height: 400px"
@connect="fnConnect"
@message="fnMessage"
></TerminalSSHView>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,370 @@
<script setup lang="ts">
import { reactive, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { ColumnsType } from 'ant-design-vue/es/table';
import useI18n from '@/hooks/useI18n';
import { OptionsType, WS } from '@/plugins/ws-websocket';
import { RESULT_CODE_ERROR } from '@/constants/result-constants';
import { diffValue, parseDuration } from '@/utils/date-utils';
import { parseSizeFromFile } from '@/utils/parse-utils';
const { t } = useI18n();
const ws = new WS();
/**表单查询参数 */
let queryParams = reactive({
pid: undefined,
name: '',
username: '',
});
/**状态对象 */
let state = reactive({
/**调度器 */
interval: null as any,
/**刷新周期 */
intervalTime: 5_000,
/**查询参数 */
query: {
pid: undefined,
name: '',
username: '',
},
});
/**接收数据后回调(成功) */
function wsMessage(res: Record<string, any>) {
const { code, requestId, data } = res;
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
// 建联时发送请求
if (!requestId && data.clientId) {
fnGetList();
return;
}
// 收到消息数据
if (requestId.startsWith('ps_')) {
// 将数据填入表格
if (Array.isArray(data)) {
if (tableState.loading) {
tableState.loading = false;
}
tableState.data = data;
} else {
tableState.data = [];
}
}
}
/**实时数据*/
function fnRealTime(reLink: boolean) {
if (reLink) {
ws.close();
}
const options: OptionsType = {
url: '/ws',
onmessage: wsMessage,
onerror: (ev: any) => {
// 接收数据后回调
console.error(ev);
},
};
//建立连接
ws.connect(options);
}
/**调度器周期变更*/
function fnIntervalChange(v: any) {
clearInterval(state.interval);
state.interval = null;
const timer = parseInt(v);
if (timer > 1_000) {
state.intervalTime = v;
fnGetList();
}
}
/**查询列表 */
function fnGetList() {
if (tableState.loading || ws.state() === -1) return;
tableState.loading = true;
const msg = {
requestId: `ps_${state.interval}`,
type: 'ps',
data: state.query,
};
// 首发
ws.send(msg);
// 定时刷新数据
state.interval = setInterval(() => {
msg.data = JSON.parse(JSON.stringify(state.query));
ws.send(msg);
}, state.intervalTime);
}
/**查询参数传入 */
function fnQuery() {
state.query = JSON.parse(JSON.stringify(queryParams));
nextTick(() => {
ws.send({
requestId: `ps_${state.interval}`,
type: 'ps',
data: state.query,
});
});
}
/**查询参数重置 */
function fnQueryReset() {
Object.assign(queryParams, {
pid: undefined,
name: '',
username: '',
});
tablePagination.current = 1;
tablePagination.pageSize = 20;
// 重置查询条件
Object.assign(state.query, {
pid: undefined,
name: '',
username: '',
});
}
/**表格状态类型 */
type TableStateType = {
/**加载等待 */
loading: boolean;
/**记录数据 */
data: object[];
};
/**表格状态 */
let tableState: TableStateType = reactive({
loading: false,
data: [],
});
/**表格字段列 */
const tableColumns: ColumnsType<any> = [
{
title: t('views.tool.ps.pid'),
dataIndex: 'pid',
align: 'right',
width: 100,
sorter: {
compare: (a: any, b: any) => a.pid - b.pid,
multiple: 3,
},
},
{
title: t('views.tool.ps.cpuPercent'),
dataIndex: 'cpuPercent',
align: 'left',
width: 120,
sorter: {
compare: (a: any, b: any) => a.cpuPercent - b.cpuPercent,
multiple: 3,
},
customRender(opt) {
return `${opt.value} %`;
},
},
{
title: t('views.tool.ps.diskRead'),
dataIndex: 'diskRead',
align: 'right',
width: 100,
sorter: {
compare: (a: any, b: any) => a.diskRead - b.diskRead,
multiple: 3,
},
customRender(opt) {
return parseSizeFromFile(+opt.value);
},
},
{
title: t('views.tool.ps.diskWrite'),
dataIndex: 'diskWrite',
align: 'right',
width: 100,
sorter: {
compare: (a: any, b: any) => a.diskWrite - b.diskWrite,
multiple: 3,
},
customRender(opt) {
return parseSizeFromFile(+opt.value);
},
},
{
title: t('views.tool.ps.numThreads'),
dataIndex: 'numThreads',
align: 'left',
width: 100,
sorter: {
//线程数比较大小
compare: (a: any, b: any) => a.numThreads - b.numThreads,
multiple: 4, //优先级4
},
},
{
title: t('views.tool.ps.runTime'),
dataIndex: 'startTime',
align: 'left',
width: 100,
customRender(opt) {
const second = diffValue(Date.now(), +opt.value, 'second');
return parseDuration(second);
},
},
{
title: t('views.tool.ps.username'),
dataIndex: 'username',
align: 'left',
width: 100,
},
{
title: t('views.tool.ps.name'),
dataIndex: 'name',
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;
},
});
/**钩子函数,界面打开初始化*/
onMounted(() => {
fnRealTime(false);
});
/**钩子函数,界面关闭*/
onBeforeUnmount(() => {
clearInterval(state.interval);
state.interval = null;
ws.close();
});
</script>
<template>
<PageContainer>
<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="4" :md="6" :xs="12">
<a-form-item :label="t('views.tool.ps.pid')" name="pid">
<a-input-number
v-model:value="queryParams.pid"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
></a-input-number>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item :label="t('views.tool.ps.name')" name="name">
<a-input
v-model:value="queryParams.name"
allow-clear
:placeholder="t('common.inputPlease')"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item :label="t('views.tool.ps.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="4" :md="12" :xs="24">
<a-form-item>
<a-space :size="8">
<a-button type="primary" @click.prevent="fnQuery()">
<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-form layout="inline">
<a-form-item :label="t('views.tool.ps.realTime')" name="realTime">
<a-select
v-model:value="state.intervalTime"
:options="[
{ label: t('views.tool.ps.realTimeHigh'), value: 3_000 },
{ label: t('views.tool.ps.realTimeRegular'), value: 5_000 },
{ label: t('views.tool.ps.realTimeLow'), value: 10_000 },
{ label: t('views.tool.ps.realTimeStop'), value: -1 },
]"
:placeholder="t('common.selectPlease')"
@change="fnIntervalChange"
style="width: 100px"
/>
</a-form-item>
</a-form>
</template>
<!-- 表格列表 -->
<a-table
class="table"
row-key="pid"
:columns="tableColumns"
:pagination="tablePagination"
:loading="tableState.loading"
:data-source="tableState.data"
size="small"
:scroll="{ x: tableColumns.length * 120 }"
></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

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,979 @@
<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,
delUDMVoIP,
exportUDMVoIP,
importUDMVoIP,
listUDMVoIP,
resetUDMVoIP,
} from '@/api/neDataNf/udm_voip';
import useNeStore from '@/store/modules/ne';
import useI18n from '@/hooks/useI18n';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { saveAs } from 'file-saver';
import { uploadFile } from '@/api/tool/file';
import { getNeViewFile } from '@/api/tool/neFile';
const { t } = useI18n();
const neStore = useNeStore();
/**网元参数 */
let neOtions = ref<Record<string, any>[]>([]);
/**查询参数 */
let queryParams = reactive({
/**网元ID */
neUid: 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.neUid = queryParams.neUid || '-';
from.neType = 'UDM';
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 = addUDMVoIP(from);
}
if (modalState.type === 'delete') {
result = delUDMVoIP(from);
}
} else {
if (modalState.type === 'add') {
result = addUDMVoIP(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: `${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 neUid = queryParams.neUid;
if (!neUid) 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({ neUid, username, num: 0 })
.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 neUid = queryParams.neUid;
if (!neUid) return;
const hide = message.loading(t('common.loading'), 0);
queryParams.pageNum = 1;
queryParams.pageSize = tablePagination.total;
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_${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 neUid = queryParams.neUid;
if (tableState.loading || !neUid) return;
modalState.loadDataLoading = true;
tablePagination.total = 0;
tableState.data = [];
tableState.loading = true; // 表格loading
resetUDMVoIP(neUid).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 neUid = queryParams.neUid;
if (!neUid) {
return Promise.reject('Unknown network element');
}
const hide = message.loading(t('common.loading'), 0);
uploadImportState.loading = true;
// 上传文件
let formData = new FormData();
formData.append('file', file);
formData.append('subPath', 'import');
uploadFile(formData)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
return res.data.filePath;
} else {
uploadImportState.msg = res.msg;
uploadImportState.loading = false;
return '';
}
})
.then((filePath: string) => {
if (!filePath) return;
// 文件导入
return importUDMVoIP({
neUid: neUid,
uploadPath: filePath,
});
})
.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 fnModalUploadImportFailReason() {
const neUid = queryParams.neUid;
if (!neUid) return;
const hide = message.loading(t('common.loading'), 0);
getNeViewFile({
neType: 'UDM',
neUid: neUid,
path: '/tmp',
fileName: 'import_imsuser_err_records.txt',
})
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('common.operateOk'), 3);
const blob = new Blob([res.data], {
type: 'text/plain',
});
saveAs(blob, `import_udmvoip_err_records_${Date.now()}.txt`);
} else {
message.error(`${res.msg}`, 3);
}
})
.finally(() => {
hide();
});
}
/**对话框表格信息导入模板 */
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(() => {
// 获取网元网元列表
neStore.getCoreDataNeCascaderOptions.forEach(item => {
if (item.value === 'UDM') {
neOtions.value = JSON.parse(JSON.stringify(item.children));
}
});
if (neOtions.value.length === 0) {
message.warning({
content: t('common.noData'),
duration: 2,
});
return;
}
if (neOtions.value.length > 0) {
queryParams.neUid = neOtions.value[0].value;
}
// 获取列表数据
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="neUid ">
<a-select
v-model:value="queryParams.neUid"
:options="neOtions"
:placeholder="t('common.selectPlease')"
:disabled="modalState.loadDataLoading"
@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"
:maxlength="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
v-model:value="modalState.from.username"
style="width: 100%"
:maxlength="16"
:placeholder="t('views.neData.udmVoIP.username')"
>
</a-input>
</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-alert
:message="uploadImportState.msg"
:type="uploadImportState.hasFail ? 'warning' : 'info'"
v-show="uploadImportState.msg.length > 0"
>
<template #action>
<a-button
size="small"
type="link"
danger
@click="fnModalUploadImportFailReason"
v-if="uploadImportState.hasFail"
>
{{ t('views.neUser.auth.importFail') }}
</a-button>
</template>
</a-alert>
</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

86
src/views/index/index.vue Normal file
View File

@@ -0,0 +1,86 @@
<script setup lang="ts">
import {
defineAsyncComponent,
onMounted,
ref,
shallowRef,
type Component,
} from 'vue';
import { getConfigKey, changeValue } from '@/api/system/config';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import useAppStore from '@/store/modules/app';
import { APP_SERVER_TYPE_M } from '@/constants/app-constants';
const appStore = useAppStore();
const currentComponent = shallowRef<Component | null>(null);
const spinning = ref<boolean>(true);
/**匹配views里面所有的.vue或.tsx文件 */
const views = import.meta.glob('../views/**/*.{vue,tsx}') as Record<
string,
() => Promise<Component>
>;
/**
* 查找页面模块
* @param dirName 组件路径
* @returns 路由懒加载函数
*/
function findView(dirName: string): () => Promise<Component> {
for (const dir in views) {
let viewDirName = '';
const component = dir.match(/\/(.+)\.(vue|tsx)/);
if (component && component.length === 3) {
viewDirName = component[1];
}
if (viewDirName === dirName) {
return views[dir];
}
}
return () => import('@/views/ne/neOverview/index.vue');
}
onMounted(() => {
// 应用-服务类型 多核心网
if (appStore.serverType === APP_SERVER_TYPE_M) {
currentComponent.value = defineAsyncComponent(
() => import('./m/index.vue')
);
spinning.value = false;
return;
}
//获取当前系统设置的首页路径
getConfigKey('sys.homePage').then(res => {
spinning.value = false;
if (res.code === RESULT_CODE_SUCCESS && res.data) {
if (res.data) {
const asyncComponent = findView(`${res.data}`);
currentComponent.value = defineAsyncComponent(asyncComponent);
}
} else {
currentComponent.value = defineAsyncComponent(
() => import('@/views/ne/neOverview/index.vue')
);
}
});
});
</script>
<template>
<a-spin
size="large"
:spinning="spinning"
style="
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
min-height: 362px;
"
>
<component :is="currentComponent" />
</a-spin>
</template>
<style lang="less" scoped></style>

399
src/views/index/m/index.vue Normal file
View File

@@ -0,0 +1,399 @@
<script setup lang="ts">
import { PageContainer } from 'antdv-pro-layout';
import { ColumnsType } from 'ant-design-vue/es/table';
import { message } from 'ant-design-vue/es';
import {
reactive,
ref,
onMounted,
onBeforeUnmount,
markRaw,
useTemplateRef,
} from 'vue';
import useI18n from '@/hooks/useI18n';
import { TooltipComponent } from 'echarts/components';
import { GaugeChart } from 'echarts/charts';
import { CanvasRenderer } from 'echarts/renderers';
import * as echarts from 'echarts/core';
import { TitleComponent, LegendComponent } from 'echarts/components';
import { PieChart } from 'echarts/charts';
import { LabelLayout } from 'echarts/features';
import { useRoute } from 'vue-router';
import useAppStore from '@/store/modules/app';
import useCoreStore from '@/store/modules/core';
import useDictStore from '@/store/modules/dict';
import { listAllNeInfo } from '@/api/ne/neInfo';
import { parseDateToStr } from '@/utils/date-utils';
const { getDict } = useDictStore();
const appStore = useAppStore();
const coreStore = useCoreStore();
const route = useRoute();
const { t } = useI18n();
echarts.use([
TooltipComponent,
GaugeChart,
TitleComponent,
LegendComponent,
PieChart,
CanvasRenderer,
LabelLayout,
]);
/**图DOM节点实例对象 */
const statusBar = useTemplateRef<HTMLDivElement>('statusBar');
/**图实例对象 */
const statusBarChart = ref<any>(null);
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: t('views.index.object'),
dataIndex: 'neName',
align: 'left',
},
{
title: t('views.index.realNeStatus'),
dataIndex: 'serverState',
align: 'left',
key: 'status',
},
{
title: t('views.index.reloadTime'),
dataIndex: 'serverState',
align: 'left',
customRender(opt) {
if (opt.value?.refreshTime) {
return parseDateToStr(opt.value?.refreshTime, 'HH:mm:ss');
}
return '-';
},
},
{
title: t('views.index.version'),
dataIndex: 'serverState',
align: 'left',
customRender(opt) {
return opt.value?.version || '-';
},
},
{
title: t('views.index.serialNum'),
dataIndex: 'serverState',
align: 'left',
customRender(opt) {
return opt.value?.sn || '-';
},
},
{
title: t('views.index.expiryDate'),
dataIndex: 'serverState',
align: 'left',
customRender(opt) {
return opt.value?.expire || '-';
},
},
{
title: t('views.index.ipAddress'),
dataIndex: 'serverState',
align: 'left',
customRender(opt) {
return opt.value?.neIP || '-';
},
},
];
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**记录数据 */
data: Record<string, any>[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
data: [],
selectedRowKeys: [],
});
/**状态 */
let serverState: any = ref({});
/**查询网元状态列表 */
async function fnGetList(reload: boolean = false) {
tableState.loading = !reload;
try {
const res = await listAllNeInfo({
coreUid: coreStore.currentCoreUid,
bandStatus: true,
});
tableState.data = res.data;
} catch (error) {
console.error(error);
tableState.data = [];
}
tableState.loading = false;
if (tableState.data.length == 0) {
return;
}
var rightNum = 0;
var errorNum = 0;
for (const v of tableState.data) {
if (v?.serverState?.online) {
rightNum++;
} else {
errorNum++;
}
}
// 初始
if (!reload) {
// 选择第一个
if (tableState.data.length > 0) {
const item = tableState.data.find((item: any) => item.status === 1);
if (item) {
const id = item.id;
fnTableSelectedRowKeys([id]);
}
} else {
fnTableSelectedRowKeys(tableState.selectedRowKeys);
}
if (statusBar.value) {
fnDesign(statusBar.value, rightNum, errorNum);
}
} else {
statusBarChart.value.setOption({
series: [
{
data: [
{ value: rightNum, name: t('views.index.normal') },
{ value: errorNum, name: t('views.index.abnormal') },
],
},
],
});
}
}
function fnDesign(container: HTMLElement, rightNum: number, errorNum: number) {
/// 图表数据
const optionData: any = {
title: {
text: '',
subtext: '',
left: 'center',
},
tooltip: {
trigger: 'item',
},
legend: {
orient: 'vertical',
left: 'left',
},
color: dict.indexStatus.map(item => item.tagClass),
series: [
{
name: t('views.index.runStatus'),
type: 'pie',
radius: '70%',
center: ['50%', '50%'],
data: [
{ value: rightNum, name: t('views.index.normal') },
{ value: errorNum, name: t('views.index.abnormal') },
],
},
],
};
statusBarChart.value = markRaw(echarts.init(container, 'light'));
statusBarChart.value.setOption(optionData);
// 创建 ResizeObserver 实例
var observer = new ResizeObserver(entries => {
if (statusBarChart.value) {
statusBarChart.value.resize();
}
});
// 监听元素大小变化
observer.observe(container);
}
/**表格多选 */
function fnTableSelectedRowKeys(keys: (string | number)[]) {
if (keys.length <= 0) return;
const id = keys[0];
const row: any = tableState.data.find((item: any) => item.id === id);
if (!row) {
message.error(t('views.index.neStatus'), 2);
return;
}
const neState = row.serverState;
if (!neState?.online) {
message.error(t('views.index.neStatus'), 2);
return;
}
tableState.selectedRowKeys = keys;
// Mem 将KB转换为MB
// const totalMemInKB = neState.mem?.totalMem;
// const nfUsedMemInKB = neState.mem?.nfUsedMem;
// const sysMemUsageInKB = neState.mem?.sysMemUsage;
// const totalMemInMB = Math.round((totalMemInKB / 1024) * 100) / 100;
// const nfUsedMemInMB = Math.round((nfUsedMemInKB / 1024) * 100) / 100;
// const sysMemUsageInMB = Math.round((sysMemUsageInKB / 1024) * 100) / 100;
// CPU
// const nfCpu = neState.cpu?.nfCpuUsage;
// const sysCpu = neState.cpu?.sysCpuUsage;
// const nfCpuP = Math.round(nfCpu) / 100;
// const sysCpuP = Math.round(sysCpu) / 100;
serverState.value = Object.assign(
{
// cpuUse: `NE:${nfCpuP}%; SYS:${sysCpuP}%`,
// memoryUse: `Total: ${totalMemInMB}MB; NE: ${nfUsedMemInMB}MB; SYS: ${sysMemUsageInMB}MB`,
},
neState
);
}
/**
* 国际化翻译转换
*/
function fnLocale() {
let title = route.meta.title as string;
if (title.indexOf('router.') !== -1) {
title = t(title);
}
appStore.setTitle(title);
}
/**字典数据 */
let dict: {
/**网元信息状态 */
neInfoStatus: DictType[];
/**主页状态 */
indexStatus: DictType[];
} = reactive({
neInfoStatus: [],
indexStatus: [],
});
let timer: any;
let timerFlag: boolean = false;
onMounted(() => {
// 初始字典数据
Promise.allSettled([getDict('ne_info_status'), getDict('index_status')])
.then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.neInfoStatus = resArr[0].value;
}
if (resArr[1].status === 'fulfilled') {
dict.indexStatus = resArr[1].value;
}
})
.finally(async () => {
fnLocale();
await fnGetList(false);
timer = setInterval(() => {
if (timerFlag) return;
fnGetList(true);
}, 10_000); // 每隔10秒执行一次
});
});
// 在组件卸载之前清除定时器
onBeforeUnmount(() => {
clearInterval(timer);
timer = null;
timerFlag = true;
});
</script>
<template>
<PageContainer :breadcrumb="{}">
<a-row :gutter="16">
<a-col :lg="14" :md="16" :xs="24">
<!-- 表格列表 -->
<a-table
class="table"
row-key="id"
size="small"
:columns="tableColumns"
:data-source="tableState.data"
:loading="tableState.loading"
:pagination="false"
:scroll="{ x: true }"
:row-selection="{
type: 'radio',
columnWidth: '48px',
selectedRowKeys: tableState.selectedRowKeys,
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<DictTag :options="dict.neInfoStatus" :value="record.status" />
</template>
</template>
</a-table>
</a-col>
<a-col :lg="10" :md="8" :xs="24">
<a-card
:title="t('views.index.runStatus')"
style="margin-bottom: 16px"
size="small"
>
<div style="width: 100%; min-height: 200px" ref="statusBar"></div>
</a-card>
<a-card
:loading="tableState.loading"
:title="`${t('views.index.mark')} - ${serverState.neName || 'OMC'}`"
style="margin-top: 16px"
size="small"
>
<a-descriptions
bordered
:column="1"
:label-style="{ width: '160px' }"
>
<a-descriptions-item :label="t('views.index.hostName')">
{{ serverState.hostname }}
</a-descriptions-item>
<a-descriptions-item :label="t('views.index.osInfo')">
{{ serverState.os }}
</a-descriptions-item>
<a-descriptions-item :label="t('views.index.ipAddress')">
{{ serverState.neIP }}
</a-descriptions-item>
<a-descriptions-item :label="t('views.index.version')">
{{ serverState.version }}
</a-descriptions-item>
<!-- <a-descriptions-item :label="t('views.index.capability')">
{{ serverState.capability }}
</a-descriptions-item> -->
<!-- <a-descriptions-item :label="t('views.index.cpuUse')">
{{ serverState.cpuUse }}
</a-descriptions-item>
<a-descriptions-item :label="t('views.index.memoryUse')">
{{ serverState.memoryUse }}
</a-descriptions-item> -->
<a-descriptions-item :label="t('views.index.serialNum')">
{{ serverState.sn }}
</a-descriptions-item>
<a-descriptions-item :label="t('views.index.expiryDate')">
{{ serverState.expire }}
</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
</a-row>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,569 @@
<script setup lang="ts">
import { reactive, ref, onMounted, toRaw } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { message, Modal } 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 {
exportSysLogLogin,
listSysLogLogin,
delSysLogLogin,
cleanSysLogLogin,
unlock,
} from '@/api/system/log/login';
import { saveAs } from 'file-saver';
import { parseDateToStr } from '@/utils/date-utils';
import useDictStore from '@/store/modules/dict';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import useI18n from '@/hooks/useI18n';
import type { Dayjs } from 'dayjs';
const { t } = useI18n();
const { getDict } = useDictStore();
/**字典数据 */
let dict: {
/**登录状态 */
sysCommonStatus: DictType[];
} = reactive({
sysCommonStatus: [],
});
/**开始结束时间 */
let queryRangePicker = ref<[Dayjs, Dayjs] | undefined>(undefined);
/**查询参数 */
let queryParams = reactive({
/**登录地址 */
loginIp: '',
/**登录账号 */
userName: '',
/**登录状态 */
statusFlag: undefined,
/**记录开始时间 */
beginTime: undefined as number | undefined,
/**记录结束时间 */
endTime: undefined as number | undefined,
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
loginIp: '',
userName: '',
statusFlag: undefined,
beginTime: 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)[];
/**勾选单个的登录账号 */
selectedUserName: string;
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
seached: false,
data: [],
selectedRowKeys: [],
selectedUserName: '',
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: t('views.system.log.login.operId'),
dataIndex: 'id',
align: 'left',
width: 100,
},
{
title: t('views.system.log.login.account'),
dataIndex: 'userName',
align: 'left',
width: 150,
},
{
title: t('views.system.log.login.loginIp'),
dataIndex: 'loginIp',
align: 'left',
width: 150,
},
// {
// title: t('views.system.log.login.loginLoc'),
// dataIndex: 'loginLocation',
// align: 'left',
// width: 150,
// },
{
title: t('views.system.log.login.os'),
dataIndex: 'os',
align: 'left',
width: 200,
},
{
title: t('views.system.log.login.browser'),
dataIndex: 'browser',
align: 'left',
width: 200,
},
{
title: t('views.system.log.login.status'),
dataIndex: 'statusFlag',
key: 'statusFlag',
align: 'left',
width: 100,
},
{
title: t('views.system.log.login.loginTime'),
dataIndex: 'loginTime',
align: 'left',
width: 200,
customRender(opt) {
if (+opt.value <= 0) return '';
return parseDateToStr(+opt.value);
},
},
{
title: t('views.system.log.login.info'),
dataIndex: 'msg',
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;
}
/**表格多选 */
function fnTableSelectedRows(
_: (string | number)[],
rows: Record<string, string>[]
) {
tableState.selectedRowKeys = rows.map(item => item.id);
// 针对单个登录账号解锁
if (rows.length === 1) {
tableState.selectedUserName = rows[0].userName;
} else {
tableState.selectedUserName = '';
}
}
/**记录删除 */
function fnRecordDelete() {
const ids = tableState.selectedRowKeys.join(',');
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.system.log.login.delSure', { ids }),
onOk() {
const key = 'delSysLogLogin';
message.loading({ content: t('common.loading'), key });
delSysLogLogin(ids).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.msgSuccess', { msg: t('common.deleteText') }),
key,
duration: 3,
});
} else {
message.error({
content: `${res.msg}`,
key,
duration: 3,
});
}
fnGetList();
});
},
});
}
/**列表清空 */
function fnCleanList() {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.system.log.login.delAllSure'),
onOk() {
const key = 'cleanSysLogLogin';
message.loading({ content: t('common.loading'), key });
cleanSysLogLogin().then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.msgSuccess', {
msg: t('views.system.log.login.delAll'),
}),
key,
duration: 3,
});
} else {
message.error({
content: `${res.msg}`,
key,
duration: 3,
});
}
fnGetList();
});
},
});
}
/**登录账号解锁 */
function fnUnlock() {
const username = tableState.selectedUserName;
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.system.log.login.unlockSure', { username }),
onOk() {
const hide = message.loading(t('common.loading'), 0);
unlock(username).then(res => {
hide();
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('views.system.log.login.unlockSuss', {
userName: username,
}),
duration: 3,
});
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
});
},
});
}
/**列表导出 */
function fnExportList() {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.system.user.exportSure'),
onOk() {
const key = 'exportSysLogLogin';
message.loading({ content: t('common.loading'), key });
exportSysLogLogin(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.msgSuccess', {
msg: t('views.system.user.export'),
}),
key,
duration: 2,
});
saveAs(res.data, `sys_log_login_${Date.now()}.xlsx`);
} else {
message.error({
content: `${res.msg}`,
key,
duration: 2,
});
}
});
},
});
}
/**查询登录日志列表, 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.beginTime = queryRangePicker.value[0].valueOf();
queryParams.endTime = queryRangePicker.value[1].valueOf();
} else {
queryParams.beginTime = undefined;
queryParams.endTime = undefined;
}
listSysLogLogin(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;
}
tableState.loading = false;
});
}
onMounted(() => {
// 初始字典数据
Promise.allSettled([getDict('sys_common_status')]).then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.sysCommonStatus = resArr[0].value;
}
});
// 获取列表数据
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.system.log.login.loginIp')"
name="loginIp"
>
<a-input
v-model:value="queryParams.loginIp"
allow-clear
:maxlength="128"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item
:label="t('views.system.log.login.account')"
name="userName"
>
<a-input
v-model:value="queryParams.userName"
allow-clear
:maxlength="30"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item
:label="t('views.system.log.login.status')"
name="statusFlag"
>
<a-select
v-model:value="queryParams.statusFlag"
allow-clear
:options="dict.sysCommonStatus"
>
</a-select>
</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-col :lg="8" :md="12" :xs="24">
<a-form-item
:label="t('views.system.log.login.loginTime')"
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-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"
:disabled="!tableState.selectedUserName"
@click.prevent="fnUnlock()"
v-perms:has="['system:log:login:unlock']"
>
<template #icon><UnlockOutlined /></template>
{{ t('views.system.log.login.unlock') }}
</a-button>
<a-button
type="default"
danger
:disabled="tableState.selectedRowKeys.length <= 0"
@click.prevent="fnRecordDelete()"
v-perms:has="['system:log:login:remove']"
>
<template #icon><DeleteOutlined /></template>
{{ t('common.deleteText') }}
</a-button>
<a-button
type="dashed"
danger
@click.prevent="fnCleanList()"
v-perms:has="['system:log:login:remove']"
>
<template #icon><DeleteOutlined /></template>
{{ t('views.system.log.operate.delAll') }}
</a-button>
<a-button
type="dashed"
@click.prevent="fnExportList()"
v-perms:has="['system:log:login:export']"
>
<template #icon><ExportOutlined /></template>
{{ t('common.export') }}
</a-button>
</a-space>
</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"
:scroll="{ x: tableColumns.length * 180 }"
:pagination="tablePagination"
:row-selection="{
type: 'checkbox',
selectedRowKeys: tableState.selectedRowKeys,
onChange: fnTableSelectedRows,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'statusFlag'">
<DictTag
:options="dict.sysCommonStatus"
:value="record.statusFlag"
/>
</template>
</template>
</a-table>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,766 @@
<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 } 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 {
exportSysLogOperate,
listSysLogOperate,
delSysLogOperate,
cleanSysLogOperate,
} from '@/api/system/log/operate';
import { saveAs } from 'file-saver';
import { parseDateToStr } from '@/utils/date-utils';
import useDictStore from '@/store/modules/dict';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import useI18n from '@/hooks/useI18n';
import type { Dayjs } from 'dayjs';
const { t } = useI18n();
const { getDict } = useDictStore();
/**字典数据 */
let dict: {
/**业务类型 */
sysBusinessType: DictType[];
/**登录状态 */
sysCommonStatus: DictType[];
} = reactive({
sysBusinessType: [],
sysCommonStatus: [],
});
/**开始结束时间 */
let queryRangePicker = ref<[Dayjs, Dayjs] | undefined>(undefined);
/**查询参数 */
let queryParams = reactive({
/**操作模块 */
title: '',
/**操作人员 */
operaBy: '',
/**业务类型 */
businessType: undefined,
/**操作状态 */
statusFlag: undefined,
/**记录开始时间 */
beginTime: undefined as number | undefined,
/**记录结束时间 */
endTime: undefined as number | undefined,
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
title: '',
operaBy: '',
businessType: undefined,
statusFlag: undefined,
beginTime: 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: false,
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: t('views.system.log.operate.operId'),
dataIndex: 'id',
align: 'left',
width: 100,
},
{
title: t('views.system.log.operate.moduleName'),
dataIndex: 'title',
align: 'left',
width: 250,
},
{
title: t('views.system.log.operate.workType'),
dataIndex: 'businessType',
key: 'businessType',
align: 'left',
width: 120,
},
{
title: t('views.system.log.operate.operUser'),
dataIndex: 'operaBy',
align: 'left',
width: 120,
},
// {
// title: t('views.system.log.operate.requestMe'),
// dataIndex: 'operaUrlMethod',
// align: 'left',
// width: 150,
// },
{
title: t('views.system.log.operate.host'),
dataIndex: 'operaIp',
align: 'left',
width: 150,
},
{
title: t('views.system.log.operate.operStatus'),
dataIndex: 'statusFlag',
key: 'statusFlag',
align: 'left',
width: 100,
},
{
title: t('views.system.log.operate.operDate'),
dataIndex: 'operaTime',
align: 'left',
width: 200,
customRender(opt) {
if (+opt.value <= 0) return '';
return parseDateToStr(+opt.value);
},
},
{
title: t('views.system.log.operate.useTime'),
dataIndex: 'costTime',
key: 'costTime',
align: 'right',
width: 100,
customRender(opt) {
return `${opt.value} ms`;
},
},
{
title: t('common.operate'),
key: 'id',
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;
}
/**表格多选 */
function fnTableSelectedRowKeys(keys: (string | number)[]) {
tableState.selectedRowKeys = keys;
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**详情框是否显示 */
openByView: boolean;
/**标题 */
title: string;
/**表单数据 */
from: Record<string, any>;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
openByView: false,
title: '操作日志',
from: {
id: undefined,
businessType: 0,
deptName: '',
operaMethod: '',
operaIp: '',
operaLocation: '',
operaMsg: '',
operaBy: '',
operaParam: '',
operaTime: 0,
operaUrl: '',
operaUrlMethod: 'PUT',
statusFlag: 1,
title: '',
},
});
/**
* 对话框弹出显示为 查看
* @param row 操作日志信息对象
*/
function fnModalVisibleByVive(row: Record<string, string>) {
modalState.from = Object.assign(modalState.from, row);
try {
modalState.from.operaParam = JSON.stringify(
JSON.parse(modalState.from.operaParam),
null,
4
);
} catch (_) {}
try {
modalState.from.operaMsg = JSON.stringify(
JSON.parse(modalState.from.operaMsg),
null,
4
);
} catch (_) {}
modalState.title = t('views.system.log.operate.logInfo');
modalState.openByView = true;
}
/**
* 对话框弹出关闭执行函数
*/
function fnModalCancel() {
modalState.openByView = false;
}
/**记录删除 */
function fnRecordDelete() {
const ids = tableState.selectedRowKeys.join(',');
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.system.log.operate.delSure', { ids }),
onOk() {
const key = 'delSysLogOperate';
message.loading({ content: t('common.loading'), key });
delSysLogOperate(ids).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.msgSuccess', { msg: t('common.deleteText') }),
key,
duration: 2,
});
fnGetList();
} else {
message.error({
content: `${res.msg}`,
key,
duration: 2,
});
}
});
},
});
}
/**列表清空 */
function fnCleanList() {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.system.log.operate.delAllSure'),
onOk() {
const key = 'cleanSysLogOperate';
message.loading({ content: t('common.loading'), key });
cleanSysLogOperate().then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('views.system.log.operate.delAllSuss'),
key,
duration: 2,
});
fnQueryReset();
} else {
message.error({
content: `${res.msg}`,
key,
duration: 2,
});
}
});
},
});
}
/**列表导出 */
function fnExportList() {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.system.user.exportSure'),
onOk() {
const key = 'exportSysLogOperate';
message.loading({ content: t('common.loading'), key });
exportSysLogOperate(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.msgSuccess', {
msg: t('views.system.user.export'),
}),
key,
duration: 2,
});
saveAs(res.data, `sys_log_operate_${Date.now()}.xlsx`);
} else {
message.error({
content: `${res.msg}`,
key,
duration: 2,
});
}
});
},
});
}
/**查询登录日志列表, 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.beginTime = queryRangePicker.value[0].valueOf();
queryParams.endTime = queryRangePicker.value[1].valueOf();
} else {
queryParams.beginTime = undefined;
queryParams.endTime = undefined;
}
listSysLogOperate(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;
});
}
onMounted(() => {
// 初始字典数据
Promise.allSettled([
getDict('sys_oper_type'),
getDict('sys_common_status'),
]).then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.sysBusinessType = resArr[0].value;
}
if (resArr[1].status === 'fulfilled') {
dict.sysCommonStatus = resArr[1].value;
}
});
// 获取列表数据
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="8" :md="12" :xs="24">
<a-form-item
:label="t('views.system.log.operate.operModule')"
name="title"
>
<a-input v-model:value="queryParams.title" allow-clear></a-input>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :xs="24">
<a-form-item
:label="t('views.system.log.operate.operUser')"
name="operaBy"
>
<a-input
v-model:value="queryParams.operaBy"
allow-clear
></a-input>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :xs="24">
<a-form-item
:label="t('views.system.log.operate.workType')"
name="businessType"
>
<a-select
v-model:value="queryParams.businessType"
allow-clear
:options="dict.sysBusinessType"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :xs="24">
<a-form-item
:label="t('views.system.log.operate.operStatus')"
name="statusFlag"
>
<a-select
v-model:value="queryParams.statusFlag"
allow-clear
:options="dict.sysCommonStatus"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :xs="24">
<a-form-item
:label="t('views.system.log.operate.operTime')"
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="8" :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>
<a-space :size="8" align="center">
<a-button
type="default"
danger
:disabled="tableState.selectedRowKeys.length <= 0"
@click.prevent="fnRecordDelete()"
v-perms:has="['system:log:operate:remove']"
>
<template #icon><DeleteOutlined /></template>
{{ t('common.deleteText') }}
</a-button>
<a-button
type="dashed"
danger
@click.prevent="fnCleanList()"
v-perms:has="['system:log:operate:remove']"
>
<template #icon><DeleteOutlined /></template>
{{ t('views.system.log.operate.delAll') }}
</a-button>
<a-button
type="dashed"
@click.prevent="fnExportList()"
v-perms:has="['system:log:operate:export']"
>
<template #icon><ExportOutlined /></template>
{{ t('common.export') }}
</a-button>
</a-space>
</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"
:scroll="{ x: tableColumns.length * 150 }"
:pagination="tablePagination"
:row-selection="{
type: 'checkbox',
selectedRowKeys: tableState.selectedRowKeys,
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'businessType'">
<DictTag
:options="dict.sysBusinessType"
:value="record.businessType"
/>
</template>
<template v-if="column.key === 'statusFlag'">
<DictTag
:options="dict.sysCommonStatus"
:value="record.statusFlag"
/>
</template>
<template v-if="column.key === 'id'">
<a-space :size="8" align="center">
<a-tooltip>
<template #title>{{ t('common.viewText') }}</template>
<a-button
type="link"
@click.prevent="fnModalVisibleByVive(record)"
v-perms:has="['system:log:operate:query']"
>
<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"
>
<a-form layout="horizontal" :label-col="{ span: 6 }" :label-wrap="true">
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.system.log.operate.operId')"
name="id"
>
{{ modalState.from.id }}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.system.log.operate.operStatus')"
name="statusFlag"
>
<a-tag :color="+modalState.from.statusFlag ? 'success' : 'error'">
{{
[
t('views.system.log.operate.fail'),
t('views.system.log.operate.suss'),
][+modalState.from.statusFlag]
}}
</a-tag>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.system.log.operate.workType')"
name="businessType"
>
{{ modalState.from.title }} /
<DictTag
:options="dict.sysBusinessType"
:value="modalState.from.businessType"
/>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.system.log.operate.operUser')"
name="operaBy"
>
{{ modalState.from.operaBy }} / {{ modalState.from.operaIp }}
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.system.log.operate.RequestIp')"
name="operaUrl"
>
{{ modalState.from.operaUrlMethod }} -
{{ modalState.from.operaUrl }}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.system.log.operate.operTime')"
name="operaTime"
>
<span v-if="+modalState.from.operaTime > 0">
{{ parseDateToStr(+modalState.from.operaTime) }}
</span>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.system.log.operate.useTime')"
name="costTime"
>
{{ modalState.from.costTime }} ms
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<!-- <a-form-item
:label="t('views.system.log.operate.operMe')"
name="operaMethod"
>
{{ modalState.from.operaMethod }}
</a-form-item> -->
</a-col>
</a-row>
<a-form-item
:label="t('views.system.log.operate.reqParam')"
name="operaParam"
:label-col="{ span: 3 }"
:label-wrap="true"
>
<a-textarea
v-model:value="modalState.from.operaParam"
:auto-size="{ minRows: 2, maxRows: 6 }"
:disabled="true"
/>
</a-form-item>
<a-form-item
:label="t('views.system.log.operate.operInfo')"
name="operaMsg"
:label-col="{ span: 3 }"
:label-wrap="true"
>
<a-textarea
v-model:value="modalState.from.operaMsg"
:auto-size="{ minRows: 2, maxRows: 6 }"
:disabled="true"
/>
</a-form-item>
</a-form>
<template #footer>
<a-button key="cancel" @click="fnModalCancel">
{{ t('common.cancel') }}
</a-button>
</template>
</ProModal>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -508,7 +508,7 @@ function fnAutoCompleteKeydown(evt: any) {
onMounted(() => {
// 获取网元网元列表
neStore.neCascaderOptions.forEach(item => {
neStore.getCoreDataNeCascaderOptions.forEach(item => {
if (item.value === 'UDM') {
neOptions.value = JSON.parse(JSON.stringify(item.children));
}

View File

@@ -60,7 +60,7 @@ const modalStateFrom = Form.useForm(
coreUid: [
{
required: true,
message: t('views.ne.neInfo.oam.kpiTimerPlease'),
message: t('common.selectPlease'),
},
],
})
@@ -72,7 +72,12 @@ const modalStateFrom = Form.useForm(
function fnModalVisible() {
const hide = message.loading(t('common.loading'), 0);
modalState.from.neUid = props.neUid;
if (props.coreUid.length === 8) {
modalState.from.coreUid = props.coreUid;
} else {
modalState.from.coreUid = undefined;
}
modalState.select = 'Associated';
modalState.openByEdit = true;
modalState.confirmLoading = false;
hide();
@@ -122,6 +127,7 @@ function fnModalOk() {
function fnModalCancel() {
modalState.openByEdit = false;
modalState.confirmLoading = false;
modalState.select = 'Associated';
modalStateFrom.resetFields();
emit('cancel');
emit('update:open', false);
@@ -131,7 +137,6 @@ function fnModalCancel() {
watch(
() => props.open,
val => {
console.log('CoreModal open', val, props.neUid, props.coreUid);
if (val && props.neUid) {
fnModalVisible();
}

View File

@@ -3,7 +3,7 @@ import { Modal } from 'ant-design-vue/es';
import { defineAsyncComponent, onMounted, onUnmounted, reactive } from 'vue';
import { fnRestStepState, stepState } from '../hooks/useStep';
import useI18n from '@/hooks/useI18n';
import { codeNeLicense, stateNeLicense } from '@/api/ne/neLicense';
import { getNeInfo, codeNeLicense } from '@/api/ne/neInfo';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
const { t } = useI18n();
const EditModal = defineAsyncComponent(
@@ -60,10 +60,10 @@ function fnModalOk(e: any) {
return;
}
if (state.timeCount % 5 === 0) {
stateNeLicense(e.neId).then(res => {
getNeInfo(stepState.neInfo.id).then(res => {
if (res.code === RESULT_CODE_SUCCESS && res.data) {
state.from.sn = res.data.sn;
state.from.expire = res.data.expire;
state.from.sn = res.data.serialNum;
state.from.expire = res.data.expiryDate;
state.from.ueNumber = res.data.ueNumber;
state.from.nbNumber = res.data.ueNumber;
@@ -95,15 +95,15 @@ function fnStepEnd() {
}
onMounted(() => {
const { neUid } = stepState.neInfo;
if (neUid) {
const { id, neUid } = stepState.neInfo;
if (id && neUid) {
state.from.neUid = neUid;
state.confirmLoading = true;
stateNeLicense(neUid)
getNeInfo(id)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS && res.data) {
state.from.sn = res.data.sn;
state.from.expire = res.data.expire;
state.from.sn = res.data.serialNum;
state.from.expire = res.data.expiryDate;
} else {
return codeNeLicense(neUid);
}

View File

@@ -0,0 +1,448 @@
<script setup lang="ts">
import { Form, Modal, message } from 'ant-design-vue/es';
import { onMounted, reactive, toRaw } from 'vue';
import { addNeInfo, getNeInfo } from '@/api/ne/neInfo';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { NE_TYPE_LIST } from '@/constants/ne-constants';
import { regExpIPv4, regExpIPv6 } from '@/utils/regular-utils';
import { fnRestStepState, fnToStepName, stepState } from '../hooks/useStep';
import useI18n from '@/hooks/useI18n';
import useDictStore from '@/store/modules/dict';
import useNeStore from '@/store/modules/ne';
const { getDict } = useDictStore();
const neStore = useNeStore();
const { t } = useI18n();
/**字典数据 */
let dict: {
/**主机类型 */
neHostType: DictType[];
/**分组 */
neHostGroupId: DictType[];
/**认证模式 */
neHostAuthMode: DictType[];
} = reactive({
neHostType: [],
neHostGroupId: [],
neHostAuthMode: [],
});
/**对话框对象信息状态类型 */
type ModalStateType = {
/**是否可以下一步 */
stepNext: boolean;
/**标题 */
title: string;
/**表单数据 */
from: Record<string, any>;
/**确定按钮 loading */
confirmLoading: boolean;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
stepNext: false,
title: '网元',
from: {
id: undefined,
coreId: undefined,
neUid: '',
neType: '',
neName: '',
ipAddr: '',
port: 33030,
pvFlag: 'PNF',
macAddr: '',
dn: '-',
vendorName: '-',
province: 'Area',
// 主机
hosts: [
{
id: undefined,
hostType: 'ssh',
groupId: '1',
title: 'SSH_NE_22',
addr: '',
port: 22,
user: 'omcuser',
authMode: '2',
password: '',
privateKey: '',
passPhrase: '',
remark: '',
},
{
id: undefined,
hostType: 'telnet',
groupId: '1',
title: 'Telnet_NE_4100',
addr: '',
port: 4100,
user: 'admin',
authMode: '0',
password: 'admin',
remark: '',
},
],
},
confirmLoading: false,
});
/**对话框内表单属性和校验规则 */
const modalStateFrom = Form.useForm(
modalState.from,
reactive({
neType: [
{
required: true,
message: t('views.ne.common.neTypePlease'),
},
],
neName: [
{
required: true,
message: t('views.ne.common.neNamePlease'),
},
],
ipAddr: [
{
required: true,
validator: modalStateFromEqualIPV4AndIPV6,
},
],
})
);
/**表单验证IP地址是否有效 */
function modalStateFromEqualIPV4AndIPV6(
rule: Record<string, any>,
value: string,
callback: (error?: string) => void
) {
if (!value) {
return Promise.reject(t('views.ne.common.ipAddrPlease'));
}
if (value.indexOf('.') === -1 && value.indexOf(':') === -1) {
return Promise.reject(t('valid.ipPlease'));
}
if (value.indexOf('.') !== -1 && !regExpIPv4.test(value)) {
return Promise.reject(t('valid.ipv4Reg'));
}
if (value.indexOf(':') !== -1 && !regExpIPv6.test(value)) {
return Promise.reject(t('valid.ipv6Reg'));
}
return Promise.resolve();
}
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
function fnModalOk() {
const from = toRaw(modalState.from);
modalStateFrom
.validate()
.then(() => {
// 清除更新必要信息
from.id = undefined;
from.hostIds = '';
for (let index = 0; index < from.hosts.length; index++) {
from.hosts[index].id = undefined;
}
const hide = message.loading(t('common.loading'), 0);
addNeInfo(from)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: `${t('common.operateOk')}`,
duration: 3,
});
return res.data;
}
message.error({
content: res.msg,
duration: 3,
});
return 0;
})
.then(neId => {
if (neId === 0) {
return;
}
neStore.fnNelistRefresh(); // 刷新缓存的网元信息
getNeInfo(neId).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
stepState.neInfo = res.data; // 保存网元信息
modalState.stepNext = true; // 开启下一步
} 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);
})
.finally(() => {
modalState.confirmLoading = false;
});
}
/**
* 表单修改网元类型
*/
function fnNeTypeChange(v: any) {
// 网元默认只含22和4100
if (modalState.from.hosts.length === 3) {
modalState.from.hosts.pop();
}
const hostsLen = modalState.from.hosts.length;
// UPF标准版本可支持5002
if (hostsLen === 2 && v === 'UPF') {
modalState.from.hosts.push({
id: undefined,
hostType: 'telnet',
groupId: '1',
title: 'Telnet_NE_5002',
addr: modalState.from.ip,
port: 5002,
user: 'admin',
authMode: '0',
password: 'admin',
remark: '',
});
}
// UDM可支持6379
if (hostsLen === 2 && v === 'UDM') {
modalState.from.hosts.push({
id: undefined,
hostType: 'redis',
groupId: '1',
title: 'REDIS_NE_6379',
addr: modalState.from.ip,
port: 6379,
user: 'udmdb',
authMode: '0',
password: 'helloearth',
dbName: '0',
remark: '',
});
}
}
/**
* 表单修改网元IP
*/
function fnNeIPChange(e: any) {
const v = e.target.value;
if (v.length < 7) return;
for (const host of modalState.from.hosts) {
host.addr = v;
}
}
/**返回上一步 */
function fnStepPrev() {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.ne.neQuickSetup.stepPrevTip'),
onOk() {
fnRestStepState(t);
fnToStepName('Start');
},
});
}
/**下一步操作 */
function fnStepNext() {
if (!modalState.stepNext) return;
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.ne.neQuickSetup.configStepNext'),
onOk() {
fnToStepName('NeInfoSoftwareInstall');
},
});
}
onMounted(() => {
// 初始字典数据
Promise.allSettled([
getDict('ne_host_type'),
getDict('ne_host_groupId'),
getDict('ne_host_authMode'),
])
.then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.neHostType = resArr[0].value;
}
if (resArr[1].status === 'fulfilled') {
dict.neHostGroupId = resArr[1].value;
}
if (resArr[2].status === 'fulfilled') {
dict.neHostAuthMode = resArr[2].value;
}
})
.finally(() => {
if (stepState.neInfo.id) {
Object.assign(modalState.from, stepState.neInfo);
} else {
modalState.from.ipAddr = stepState.neHost.addr;
Object.assign(modalState.from.hosts[0], stepState.neHost);
Object.assign(modalState.from.hosts[1], {
addr: modalState.from.ipAddr,
user: 'admin',
password: 'admin',
});
}
});
});
</script>
<template>
<div class="ne">
<a-form
name="modalStateFrom"
layout="horizontal"
:label-col="{ span: 3 }"
:wrapper-col="{ span: 9 }"
:labelWrap="true"
>
<a-form-item
:label="t('views.ne.common.ipAddr')"
name="ip"
v-bind="modalStateFrom.validateInfos.ipAddr"
>
<a-input
v-model:value="modalState.from.ipAddr"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="128"
@change="fnNeIPChange"
:disabled="true"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title>
<div>
{{ t('views.ne.common.ipAddrTip') }}
</div>
</template>
<InfoCircleOutlined style="opacity: 0.45; color: inherit" />
</a-tooltip>
</template>
</a-input>
</a-form-item>
<a-form-item
:label="t('views.ne.common.port')"
name="port"
v-bind="modalStateFrom.validateInfos.port"
:help="t('views.ne.common.portTip')"
>
<a-input-number
v-model:value="modalState.from.port"
style="width: 100%"
:min="1"
:max="65535"
:maxlength="5"
placeholder="<=65535"
>
</a-input-number>
</a-form-item>
<a-form-item
:label="t('views.ne.common.neType')"
name="neType"
v-bind="modalStateFrom.validateInfos.neType"
>
<a-auto-complete
v-model:value="modalState.from.neType"
:options="
NE_TYPE_LIST.filter(s => s !== 'OMC').map(v => ({ value: v }))
"
@change="fnNeTypeChange"
>
<a-input
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="32"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title>
{{ t('views.ne.common.neTypeTip') }}
</template>
<InfoCircleOutlined style="opacity: 0.45; color: inherit" />
</a-tooltip>
</template>
</a-input>
</a-auto-complete>
</a-form-item>
<a-form-item
:label="t('views.ne.common.neName')"
name="neName"
v-bind="modalStateFrom.validateInfos.neName"
>
<a-input
v-model:value="modalState.from.neName"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="24"
>
</a-input>
</a-form-item>
</a-form>
<div class="ne-oper">
<a-space direction="horizontal" :size="18">
<a-button @click="fnStepPrev()">
{{ t('views.ne.neQuickSetup.stepPrev') }}
</a-button>
<a-button
type="primary"
ghost
@click="fnModalOk()"
:loading="modalState.confirmLoading"
>
{{ t('views.ne.neQuickSetup.stepSave') }}
</a-button>
<a-button
type="primary"
@click="fnStepNext()"
:disabled="!modalState.stepNext"
>
{{ t('views.ne.neQuickSetup.stepNext') }}
</a-button>
</a-space>
</div>
</div>
</template>
<style lang="less" scoped>
.ne {
min-height: 400px;
display: flex;
flex-direction: column;
& .ant-form {
flex: 1;
}
&-oper {
text-align: end;
}
}
</style>

View File

@@ -0,0 +1,433 @@
<script setup lang="ts">
import { Modal, message } from 'ant-design-vue/es';
import { defineAsyncComponent, onMounted, reactive, toRaw } from 'vue';
import { fnToStepName, stepState } from '../hooks/useStep';
import useI18n from '@/hooks/useI18n';
import { operateNeVersion } from '@/api/ne/neVersion';
import {
RESULT_CODE_ERROR,
RESULT_CODE_SUCCESS,
} from '@/constants/result-constants';
import { listNeSoftware, newNeVersion } from '@/api/ne/neSoftware';
import { parseDateToStr } from '@/utils/date-utils';
import { ColumnsType } from 'ant-design-vue/es/table';
const { t } = useI18n();
const EditModal = defineAsyncComponent(
() => import('@/views/ne/neSoftware/components/EditModal.vue')
);
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: t('common.rowId'),
dataIndex: 'id',
align: 'left',
width: 50,
},
{
title: t('views.ne.common.neType'),
dataIndex: 'neType',
align: 'left',
width: 100,
},
{
title: t('views.ne.neSoftware.version'),
dataIndex: 'version',
align: 'left',
width: 150,
},
{
title: t('views.ne.neSoftware.name'),
dataIndex: 'name',
key: 'name',
align: 'left',
width: 300,
},
{
title: t('common.description'),
dataIndex: 'description',
key: 'description',
align: 'left',
ellipsis: true,
},
{
title: t('common.createTime'),
dataIndex: 'createTime',
align: 'left',
width: 150,
customRender(opt) {
if (!opt.value) return '';
return parseDateToStr(+opt.value);
},
},
];
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 10,
/**默认的每页条数 */
defaultPageSize: 10,
/**指定每页可以显示多少条 */
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;
tableState.queryParams.pageNum = page;
tableState.queryParams.pageSize = pageSize;
fnGetList();
},
});
/**表格状态类型 */
type TabeStateType = {
/**查询参数 */
queryParams: Record<string, any>;
/**加载等待 */
loading: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
queryParams: {
neType: '',
pageNum: 1,
pageSize: 10,
},
loading: false,
data: [],
selectedRowKeys: [],
});
/**表格多选 */
function fnTableSelectedRowKeys(
keys: (string | number)[],
selectedRows: any[]
) {
tableState.selectedRowKeys = keys;
// 选择的表单数据填充
Object.assign(state.from, selectedRows[0], { id: undefined });
}
/**查询列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (tableState.loading) return;
tableState.loading = true;
if (pageNum) {
tableState.queryParams.pageNum = pageNum;
}
listNeSoftware(toRaw(tableState.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;
}
tableState.loading = false;
});
}
/**对象信息信息状态类型 */
type StateType = {
/**是否可以下一步 */
stepNext: boolean;
/**文件操作类型 上传 or 选择 */
optionType: 'upload' | 'option';
/**文件上传 */
openByFile: boolean;
/**网元拓展包列表类型 */
depType: string[];
/**软件包信息数据 */
from: {
neType: string; // 版本需要
neUid: string; // 版本需要
name: string; // 软件需要
path: string;
version: string; // 软件需要
description: string;
};
/**确定按钮 loading */
confirmLoading: boolean;
};
/**对象信息状态 */
let state: StateType = reactive({
stepNext: false,
optionType: 'option',
openByFile: false,
depType: [],
from: {
id: undefined,
neType: '',
neUid: '',
name: '',
path: '',
version: '',
description: '',
},
confirmLoading: false,
});
/**
* 表单修改文件操作类型
*/
function fnOptionTypeChange() {
state.from.name = '';
state.from.version = '';
tableState.selectedRowKeys = [];
if (state.optionType === 'option') {
fnGetList(1);
}
}
/**对话框弹出 */
function fnModalOpen() {
state.openByFile = !state.openByFile;
}
/**对话框弹出确认执行函数*/
function fnModalOk(e: any) {
Object.assign(state.from, e, { id: undefined });
}
/**对话框弹出关闭执行函数*/
function fnModalCancel() {
state.openByFile = false;
}
/**版本安装 */
function fnRecordInstall() {
const from = toRaw(state.from);
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.ne.neQuickSetup.installConfirmTip', {
name: from.name,
}),
onOk: async () => {
if (state.confirmLoading) return;
state.confirmLoading = true;
const hide = message.loading(t('common.loading'), 0);
// 选择的软件需要更新版本
if (state.optionType === 'option') {
const res = await newNeVersion(from);
if (res.code === RESULT_CODE_ERROR) {
message.error(res.msg, 3);
hide();
state.confirmLoading = false;
return;
}
}
// 进行安装
let preinput = {};
if (from.neType.toUpperCase() === 'IMS') {
preinput = { pisCSCF: 'y', updateMFetc: 'No', updateMFshare: 'No' };
}
const res = await operateNeVersion({
neUid: from.neUid,
neType: from.neType,
action: 'install',
preinput: preinput,
});
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('common.operateOk'), 3);
state.stepNext = true;
} else {
message.error(res.msg, 3);
}
// 非选择的重置
state.optionType = 'option';
fnOptionTypeChange();
hide();
state.confirmLoading = false;
},
});
}
/**返回上一步 */
function fnStepPrev() {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.ne.neQuickSetup.stepPrevTip'),
onOk() {
fnToStepName('NeInfoConfig');
},
});
}
/**下一步操作 */
function fnStepNext() {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.ne.neQuickSetup.installStepNext'),
onOk() {
fnToStepName('NeInfoSoftwareLicense');
},
});
}
onMounted(() => {
const { neType, neUid } = stepState.neInfo;
if (neType && neUid) {
tableState.queryParams.neType = neType;
state.from.neUid = neUid;
state.from.neType = neType;
fnGetList(1);
}
});
</script>
<template>
<div class="ne">
<a-form
name="installStateFrom"
layout="horizontal"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 16 }"
:label-wrap="true"
>
<a-form-item
:label="t('views.ne.neQuickSetup.installSource')"
name="optionType"
>
<a-radio-group
v-model:value="state.optionType"
button-style="solid"
:disabled="state.confirmLoading"
@change="fnOptionTypeChange"
>
<a-radio-button value="option">
{{ t('views.ne.neQuickSetup.installSourceOption') }}
</a-radio-button>
<a-radio-button value="upload">
{{ t('views.ne.neQuickSetup.installSourceUpload') }}
</a-radio-button>
</a-radio-group>
</a-form-item>
<!-- 选择已上传 -->
<template v-if="state.optionType === 'option'">
<a-form-item
:label="t('views.ne.neQuickSetup.installSelect')"
name="option"
>
<a-table
class="table"
row-key="id"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:pagination="tablePagination"
size="small"
:scroll="{ x: tableColumns.length * 100, y: '400px' }"
:row-selection="{
type: 'radio',
columnWidth: '48px',
selectedRowKeys: tableState.selectedRowKeys,
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
<a-tooltip placement="topLeft">
<template #title>{{ record.path }}</template>
<div style="cursor: pointer">{{ record.name }}</div>
</a-tooltip>
</template>
<template v-if="column.key === 'description'">
<a-tooltip placement="topLeft">
<template #title>{{ record.description }}</template>
<div style="cursor: pointer">{{ record.description }}</div>
</a-tooltip>
</template>
</template>
</a-table>
</a-form-item>
</template>
<!-- 重新上传 -->
<template v-if="state.optionType === 'upload'">
<a-form-item
:label="t('views.ne.neQuickSetup.installUpload')"
name="upload"
:help="state.from.name"
>
<a-button type="primary" @click.prevent="fnModalOpen()">
<template #icon><UploadOutlined /></template>
{{ t('views.ne.neSoftware.upload') }}
</a-button>
</a-form-item>
</template>
</a-form>
<!-- 文件上传框 -->
<EditModal
v-model:open="state.openByFile"
@ok="fnModalOk"
@cancel="fnModalCancel"
></EditModal>
<div class="ne-oper">
<a-space direction="horizontal" :size="18">
<a-button @click="fnStepPrev()">
{{ t('views.ne.neQuickSetup.stepPrev') }}
</a-button>
<a-button
type="primary"
ghost
:disabled="!state.from.version"
:loading="state.confirmLoading"
@click.prevent="fnRecordInstall()"
>
<template #icon><ThunderboltOutlined /></template>
{{ t('views.ne.neQuickSetup.installText') }}
</a-button>
<a-button
type="primary"
@click="fnStepNext()"
:disabled="!state.stepNext"
>
{{ t('views.ne.neQuickSetup.stepNext') }}
</a-button>
</a-space>
</div>
</div>
</template>
<style lang="less" scoped>
.ne {
min-height: 400px;
display: flex;
flex-direction: column;
& .ant-form {
flex: 1;
}
&-oper {
text-align: end;
}
}
</style>

View File

@@ -0,0 +1,188 @@
<script setup lang="ts">
import { Modal } from 'ant-design-vue/es';
import { defineAsyncComponent, onMounted, onUnmounted, reactive } from 'vue';
import { fnRestStepState, stepState } from '../hooks/useStep';
import useI18n from '@/hooks/useI18n';
import { getNeInfo, codeNeLicense } from '@/api/ne/neInfo';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
const { t } = useI18n();
const EditModal = defineAsyncComponent(
() => import('@/views/ne/neLicense/components/EditModal.vue')
);
/**对象信息信息状态类型 */
type StateType = {
/**文件上传 */
openByFile: boolean;
/**授权信息数据 */
from: {
neUid: string;
neType: string;
// 下面是状态检查结果
expire: string;
sn: string;
ueNumber: string;
nbNumber: string;
};
/**确定按钮 loading */
confirmLoading: boolean;
/**定时调度 */
timeInterval: any;
timeCount: number;
};
/**对象信息状态 */
let state: StateType = reactive({
openByFile: false,
from: {
neUid: '',
neType: '',
// 下面是状态检查结果
expire: '',
sn: '',
ueNumber: '',
nbNumber: '',
},
confirmLoading: false,
timeInterval: null,
timeCount: 30,
});
/**对话框弹出确认执行函数*/
function fnModalOk(e: any) {
state.timeInterval = setInterval(() => {
if (state.timeCount <= 0) {
state.from.sn = '';
state.from.expire = '';
clearInterval(state.timeInterval);
state.timeInterval = null;
state.timeCount = 30;
return;
}
if (state.timeCount % 5 === 0) {
getNeInfo(stepState.neInfo.id).then(res => {
if (res.code === RESULT_CODE_SUCCESS && res.data) {
state.from.sn = res.data.serialNum;
state.from.expire = res.data.expiryDate;
state.from.ueNumber = res.data.ueNumber;
state.from.nbNumber = res.data.ueNumber;
clearInterval(state.timeInterval);
state.timeInterval = null;
state.timeCount = 30;
}
});
}
state.timeCount--;
}, 1_000);
}
/**对话框弹出关闭执行函数*/
function fnModalCancel() {
state.openByFile = false;
state.confirmLoading = false;
}
/**结束操作 */
function fnStepEnd() {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.ne.neQuickSetup.licenseEndTip'),
onOk() {
fnRestStepState(t);
},
});
}
onMounted(() => {
const { id, neUid } = stepState.neInfo;
if (id && neUid) {
state.from.neUid = neUid;
state.confirmLoading = true;
getNeInfo(id)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS && res.data) {
state.from.sn = res.data.serialNum;
state.from.expire = res.data.expiryDate;
} else {
return codeNeLicense(neUid);
}
})
.finally(() => {
state.confirmLoading = false;
});
}
});
onUnmounted(() => {
clearInterval(state.timeInterval);
state.timeInterval = null;
state.timeCount = 30;
});
</script>
<template>
<a-result
:status="!state.from.sn ? 'info' : 'success'"
:title="
t(
!state.from.sn
? 'views.ne.neQuickSetup.licenseResultTitle'
: 'views.ne.neQuickSetup.licenseResultTitleOk'
)
"
>
<template #extra>
<a-button
type="primary"
:disabled="state.from.sn !== ''"
:loading="state.timeCount < 30 || state.confirmLoading"
@click="() => (state.openByFile = !state.openByFile)"
>
{{ t('views.ne.neQuickSetup.licenseUpload') }}
</a-button>
<a-button
type="default"
:disabled="state.timeCount < 30 || state.confirmLoading"
@click="fnStepEnd()"
>
{{ t('views.ne.neQuickSetup.licenseEnd') }}
</a-button>
</template>
<div
v-if="
state.timeInterval === null && state.timeCount === 30 && !state.from.sn
"
>
<p>{{ t('views.ne.neQuickSetup.licenseTip1') }}</p>
<p>{{ t('views.ne.neQuickSetup.licenseTip2') }}</p>
</div>
<div v-if="state.timeInterval !== null">
<a-space direction="horizontal" :size="16">
<a-spin />
{{ t('views.ne.neQuickSetup.licenseCheack') }} {{ state.timeCount }}s
</a-space>
</div>
<div v-if="state.from.sn !== ''" style="font-size: 16px">
<p>{{ t('views.ne.common.neType') }}{{ state.from.neType }}</p>
<p>{{ t('views.ne.common.neUid') }}{{ state.from.neUid }}</p>
<p>{{ t('views.ne.common.serialNum') }}{{ state.from.sn }}</p>
<p>{{ t('views.ne.common.expiryDate') }}{{ state.from.expire }}</p>
<p>{{ t('views.ne.common.ueNumber') }}{{ state.from.ueNumber }}</p>
<p>{{ t('views.ne.common.nbNumber') }}{{ state.from.nbNumber }}</p>
</div>
</a-result>
<!-- 许可证上传框 -->
<EditModal
v-model:open="state.openByFile"
:ne-type="state.from.neType"
:ne-uid="state.from.neUid"
:reload="true"
@ok="fnModalOk"
@cancel="fnModalCancel"
></EditModal>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,564 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import useI18n from '@/hooks/useI18n';
const { t } = useI18n();
const emit = defineEmits(['update:data']);
const props = defineProps({
/**表单数据 */
data: {
type: Object,
required: true,
},
/**根据网元显示配置项 */
ne: {
type: Object,
default: () => ({
amf: false,
upf: false,
ims: false,
mme: false,
}),
},
});
/**表单信息状态 */
let fromState = ref({
basic: {
plmnId: {
mcc: '001',
mnc: '01',
},
tac: '4388',
snssai: {
sst: '1',
sd: '000001',
},
dnn_data: 'internet',
dnn_ims: 'ims',
},
external: {
amfn2_ip: '192.168.8.120',
upfn3_ip: '192.168.8.190/24',
upfn3_gw: '192.168.1.1',
upfn6_ip: '192.168.8.191/24',
upfn6_gw: '192.168.1.1',
ue_pool: '10.2.1.0/24',
// 非指定属性
mmes1_ip: '192.168.8.220/20',
mmes10_ip: '172.16.5.221/24',
mmes11_ip: '172.16.5.220/24',
ims_sip_ip: '192.168.8.110',
upf_type: 'LightUPF',
upf_driver_type: 'vmxnet3',
upfn3_card_name: 'eth0',
upfn3_pci: '0000:00:00.0',
upfn3_mac: '00:00:00:00:00:00',
upfn6_card_name: 'eth0',
upfn6_pci: '0000:00:00.0',
upfn6_mac: '00:00:00:00:00:00',
},
sbi: {
omc_ip: '172.16.5.100',
ims_ip: '172.16.5.110',
amf_ip: '172.16.5.120',
ausf_ip: '172.16.5.130',
udm_ip: '172.16.5.140',
db_ip: '0.0.0.0',
smf_ip: '172.16.5.150',
pcf_ip: '172.16.5.160',
nssf_ip: '172.16.5.170',
nrf_ip: '172.16.5.180',
upf_ip: '172.16.5.190',
lmf_ip: '172.16.5.200',
nef_ip: '172.16.5.210',
mme_ip: '172.16.5.220',
n3iwf_ip: '172.16.5.230',
smsc_ip: '172.16.5.240',
},
});
/**监听数据 */
watch(
() => fromState,
val => {
if (val) emit('update:data', val);
},
{ deep: true, immediate: true }
);
</script>
<template>
<a-form
name="para5GFileeFrom"
layout="horizontal"
:label-col="{ span: 8 }"
:label-wrap="true"
>
<a-row>
<a-col :lg="16" :md="16" :xs="24">
<a-divider orientation="left">Basic</a-divider>
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="DNN_DATA" name="basic.dnn_data">
<a-input
v-model:value="fromState.basic.dnn_data"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="50"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title> DNN </template>
<InfoCircleOutlined style="opacity: 0.45; color: inherit" />
</a-tooltip>
</template>
</a-input>
</a-form-item>
<a-form-item label="MCC" name="basic.plmnId.mcc">
<a-input
v-model:value="fromState.basic.plmnId.mcc"
placeholder="1-65535"
></a-input>
</a-form-item>
<a-form-item label="SST" name="basic.snssai.sst">
<a-input-number
v-model:value="fromState.basic.snssai.sst"
:min="1"
:max="3"
placeholder="1-3"
style="width: 100%"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title> 1-3 </template>
<InfoCircleOutlined style="opacity: 0.45; color: inherit" />
</a-tooltip>
</template>
</a-input-number>
</a-form-item>
<a-form-item label="TAC" name="basic.tac">
<a-input
v-model:value="fromState.basic.tac"
placeholder="1-65535"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="DNN_IMS" name="basic.dnn_ims">
<a-input
v-model:value="fromState.basic.dnn_ims"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="50"
>
</a-input>
</a-form-item>
<a-form-item label="MNC" name="basic.plmnId.mnc">
<a-input
v-model:value="fromState.basic.plmnId.mnc"
placeholder="1-65535"
></a-input>
</a-form-item>
<a-form-item label="SD" name="basic.snssai.sd">
<a-input
v-model:value="fromState.basic.snssai.sd"
placeholder="1-65535"
></a-input>
</a-form-item>
<a-form-item label="UE_POOL" name="external.ue_pool">
<a-input
v-model:value="fromState.external.ue_pool"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="50"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title> UE IP and mask </template>
<InfoCircleOutlined style="opacity: 0.45; color: inherit" />
</a-tooltip>
</template>
</a-input>
</a-form-item>
</a-col>
</a-row>
</a-col>
<a-col :lg="8" :md="8" :xs="24">
<a-divider orientation="left">OMC</a-divider>
<a-row>
<a-col :lg="24" :md="24" :xs="24">
<a-form-item
label="OMC_IP"
name="sbi.omc_ip"
:required="true"
:validate-on-rule-change="false"
:validateTrigger="[]"
>
<a-input
v-model:value="fromState.sbi.omc_ip"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="50"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title>
Network Elemment send data tu EMS IP
</template>
<InfoCircleOutlined style="opacity: 0.45; color: inherit" />
</a-tooltip>
</template>
</a-input>
</a-form-item>
</a-col>
</a-row>
<template v-if="props.ne.amf">
<a-divider orientation="left">AMF</a-divider>
<a-row>
<a-col :lg="24" :md="24" :xs="24">
<a-form-item label="N2_IP" name="external.amfn2_ip">
<a-input
v-model:value="fromState.external.amfn2_ip"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="50"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title> AMF N2 </template>
<InfoCircleOutlined
style="opacity: 0.45; color: inherit"
/>
</a-tooltip>
</template>
</a-input>
</a-form-item>
</a-col>
</a-row>
</template>
</a-col>
<a-col :lg="16" :md="16" :xs="24" v-if="props.ne.upf">
<a-divider orientation="left">UPF</a-divider>
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
label="UPF_TYPE"
name="external.upf_type"
help="Install of Standard or Light"
>
<a-select
v-model:value="fromState.external.upf_type"
:placeholder="t('common.selectPlease')"
>
<a-select-option value="StandardUPF">
StandardUPF
</a-select-option>
<a-select-option value="LightUPF">LightUPF</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col
:lg="12"
:md="12"
:xs="24"
v-if="fromState.external.upf_type === 'StandardUPF'"
>
<a-form-item label="DRIVER_TYPE" name="external.upf_driver_type">
<a-select
v-model:value="fromState.external.upf_driver_type"
:placeholder="t('common.selectPlease')"
>
<a-select-option value="vmxnet3">vmxnet3</a-select-option>
<a-select-option value="dpdk">dpdk</a-select-option>
<a-select-option value="host">host</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row v-if="fromState.external.upf_type === 'LightUPF'">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="N3_IP" name="external.upfn3_ip">
<a-input
v-model:value="fromState.external.upfn3_ip"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="50"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title> netwrok ip </template>
<InfoCircleOutlined style="opacity: 0.45; color: inherit" />
</a-tooltip>
</template>
</a-input>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="N3_GW" name="external.upfn3_gw">
<a-input
v-model:value="fromState.external.upfn3_gw"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="50"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title> geteway </template>
<InfoCircleOutlined style="opacity: 0.45; color: inherit" />
</a-tooltip>
</template>
</a-input>
</a-form-item>
</a-col>
</a-row>
<a-row v-if="fromState.external.upf_type === 'StandardUPF'">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="N3_IP" name="external.upfn3_ip">
<a-input
v-model:value="fromState.external.upfn3_ip"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="50"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title> netwrok ip </template>
<InfoCircleOutlined style="opacity: 0.45; color: inherit" />
</a-tooltip>
</template>
</a-input>
</a-form-item>
<a-form-item label="N3_GW" name="external.upfn3_gw">
<a-input
v-model:value="fromState.external.upfn3_gw"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="50"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title> geteway </template>
<InfoCircleOutlined style="opacity: 0.45; color: inherit" />
</a-tooltip>
</template>
</a-input>
</a-form-item>
<a-form-item label="N3_PCI" name="external.upfn3_pci">
<a-input
v-model:value="fromState.external.upfn3_pci"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="50"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title>
use `ip a` show info, to inet name
<br />
or <br />
use `lshw -class network -businfo` show device name
</template>
<InfoCircleOutlined style="opacity: 0.45; color: inherit" />
</a-tooltip>
</template>
</a-input>
</a-form-item>
<a-form-item label="N3_MAC" name="external.upfn3_mac">
<a-input
v-model:value="fromState.external.upfn3_mac"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="50"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title>
use `ip a` show info, to inet ip
</template>
<InfoCircleOutlined style="opacity: 0.45; color: inherit" />
</a-tooltip>
</template>
</a-input>
</a-form-item>
<a-form-item label="N3_NIC_NAME" name="external.upfn3_card_name">
<a-input
v-model:value="fromState.external.upfn3_card_name"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="50"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title>
use `ip a` show info, to inet name
<br />
or <br />
use `lshw -class network -businfo` show device name
</template>
<InfoCircleOutlined style="opacity: 0.45; color: inherit" />
</a-tooltip>
</template>
</a-input>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="N6_IP" name="external.upfn6_ip">
<a-input
v-model:value="fromState.external.upfn6_ip"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="50"
>
</a-input>
</a-form-item>
<a-form-item label="N6_GW" name="external.upfn6_gw">
<a-input
v-model:value="fromState.external.upfn6_gw"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="50"
>
</a-input>
</a-form-item>
<a-form-item label="N6_PCI" name="external.upfn6_pci">
<a-input
v-model:value="fromState.external.upfn6_pci"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="50"
>
</a-input>
</a-form-item>
<a-form-item label="N6_MAC" name="external.upfn6_mac">
<a-input
v-model:value="fromState.external.upfn6_mac"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="50"
>
</a-input>
</a-form-item>
<a-form-item label="N6_NIC_NAME" name="external.upfn6_card_name">
<a-input
v-model:value="fromState.external.upfn6_card_name"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="50"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title>
use `ip a` show info, to inet name
<br />
or <br />
use `lshw -class network -businfo` show device name
</template>
<InfoCircleOutlined style="opacity: 0.45; color: inherit" />
</a-tooltip>
</template>
</a-input>
</a-form-item>
</a-col>
</a-row>
</a-col>
<a-col :lg="8" :md="8" :xs="24">
<template v-if="props.ne.ims">
<a-divider orientation="left">IMS</a-divider>
<a-row>
<a-col :lg="24" :md="24" :xs="24">
<a-form-item label="SIP_IP" name="external.ims_sip_ip">
<a-input
v-model:value="fromState.external.ims_sip_ip"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="50"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title> IMS SIP </template>
<InfoCircleOutlined
style="opacity: 0.45; color: inherit"
/>
</a-tooltip>
</template>
</a-input>
</a-form-item>
</a-col>
</a-row>
</template>
<template v-if="props.ne.mme">
<a-divider orientation="left">MME</a-divider>
<a-row>
<a-col :lg="24" :md="24" :xs="24">
<a-form-item label="S1_IP" name="external.mmes1_ip">
<a-input
v-model:value="fromState.external.mmes1_ip"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="50"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title>
MME binded interface for S1-C or S1-MME communication
(S1AP), can be ethernet interface, virtual ethernet
interface, we don't advise wireless interfaces
</template>
<InfoCircleOutlined
style="opacity: 0.45; color: inherit"
/>
</a-tooltip>
</template>
</a-input>
</a-form-item>
<a-form-item label="S10_IP" name="external.mmes10_ip">
<a-input
v-model:value="fromState.external.mmes10_ip"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="50"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title> Interface </template>
<InfoCircleOutlined
style="opacity: 0.45; color: inherit"
/>
</a-tooltip>
</template>
</a-input>
</a-form-item>
<a-form-item label="S11_IP" name="external.mmes11_ip">
<a-input
v-model:value="fromState.external.mmes11_ip"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="50"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title>
MME binded interface for S11-U communication (GTPV1-U)
</template>
<InfoCircleOutlined
style="opacity: 0.45; color: inherit"
/>
</a-tooltip>
</template>
</a-input>
</a-form-item>
</a-col>
</a-row>
</template>
</a-col>
</a-row>
</a-form>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,496 @@
<script setup lang="ts">
import { reactive, onMounted, toRaw } from 'vue';
import { message, Form, Modal } from 'ant-design-vue/es';
import useI18n from '@/hooks/useI18n';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { regExpIPv4, regExpIPv6 } from '@/utils/regular-utils';
import { neHostAuthorizedRSA, neHostCheckInfo } from '@/api/ne/neHost';
import useDictStore from '@/store/modules/dict';
import { fnToStepName, stepState } from '../hooks/useStep';
const { getDict } = useDictStore();
const { t } = useI18n();
/**字典数据 */
let dict: {
/**认证模式 */
neHostAuthMode: DictType[];
} = reactive({
neHostAuthMode: [],
});
/**对象信息状态类型 */
type StateType = {
/**服务器信息 */
info: Record<string, any>;
/**表单数据 */
from: Record<string, any>;
/**是否可以下一步 */
stepNext: boolean;
/**确定按钮 loading */
confirmLoading: boolean;
};
/**对象信息状态 */
let state: StateType = reactive({
info: {
addr: '0.0.0.0',
kernelName: '-',
kernelRelease: '-',
machine: '-',
nodename: '-',
prettyName: '-',
sshLink: false,
sudo: false,
},
from: {
id: undefined,
hostType: 'ssh',
groupId: '1',
title: 'SSH_NE_22',
addr: '',
port: 22,
user: '',
authMode: '2',
password: '',
privateKey: '',
passPhrase: '',
remark: '',
},
stepNext: false,
confirmLoading: false,
});
/**表单属性和校验规则 */
const checkStateFrom = Form.useForm(
state.from,
reactive({
addr: [
{
required: true,
min: 1,
max: 128,
validator: modalStateFromEqualIPV4AndIPV6,
},
],
port: [
{
required: true,
message: t('views.ne.neHost.portPlease'),
},
],
user: [
{
required: true,
min: 1,
max: 50,
message: t('views.ne.neHost.userPlease'),
},
],
password: [
{
required: true,
min: 1,
max: 128,
message: t('views.ne.neHost.passwordPlease'),
},
],
privateKey: [
{
required: true,
min: 1,
max: 3000,
message: t('views.ne.neHost.privateKeyPlease'),
},
],
})
);
/**表单验证IP地址是否有效 */
function modalStateFromEqualIPV4AndIPV6(
rule: Record<string, any>,
value: string,
callback: (error?: string) => void
) {
if (!value) {
return Promise.reject(t('views.ne.common.ipAddrPlease'));
}
if (value.indexOf('.') === -1 && value.indexOf(':') === -1) {
return Promise.reject(t('valid.ipPlease'));
}
if (value.indexOf('.') !== -1 && !regExpIPv4.test(value)) {
return Promise.reject(t('valid.ipv4Reg'));
}
if (value.indexOf(':') !== -1 && !regExpIPv6.test(value)) {
return Promise.reject(t('valid.ipv6Reg'));
}
return Promise.resolve();
}
/**测试连接检查信息 */
function fnCheckInfo() {
if (state.confirmLoading) return;
const form = toRaw(state.from);
const validateArr = ['addr', 'port', 'user'];
if (form.authMode === '0') {
validateArr.push('password');
}
if (form.authMode === '1') {
validateArr.push('privateKey');
}
state.confirmLoading = true;
const hide = message.loading(t('common.loading'), 0);
checkStateFrom
.validate(validateArr)
.then(() => {
Object.assign(state.info, {
addr: '0.0.0.0',
kernelName: '-',
kernelRelease: '-',
machine: '-',
nodename: '-',
prettyName: '-',
sshLink: false,
sudo: false,
});
return neHostCheckInfo(form);
})
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
state.info = res.data;
if (!res.data.sudo) {
message.warning(t('views.ne.neQuickSetup.sudoErr'), 3);
return;
}
// if (!res.data.sshLink) {
// message.warning({
// content: `请配置服务器间免密信任关系,确保服务器间文件传输功能`,
// duration: 2,
// });
// return;
// }
stepState.neHost = form; // 保存主机连接信息
state.stepNext = true; // 开启下一步
message.success({
content: `${form.addr}:${form.port} ${t('views.ne.neHost.testOk')}`,
duration: 2,
});
} else {
message.error({
content: `${form.addr}:${form.port} ${res.msg}`,
duration: 2,
});
}
})
.catch(e => {
message.error(t('common.errorFields', { num: e.errorFields.length }), 3);
})
.finally(() => {
hide();
state.confirmLoading = false;
});
}
/**测试连接检查信息表单重置 */
function fnCheckInfoReset() {
Object.assign(state.info, {
addr: '0.0.0.0',
kernelName: '-',
kernelRelease: '-',
machine: '-',
nodename: '-',
prettyName: '-',
sshLink: false,
sudo: false,
});
state.stepNext = false;
checkStateFrom.resetFields();
}
/**测试主机连接-免密直连 */
function fnHostAuthorized() {
if (state.confirmLoading) return;
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.ne.neHost.authRSATip'),
onOk: () => {
const form = toRaw(state.from);
state.confirmLoading = true;
neHostAuthorizedRSA(form).then(res => {
state.confirmLoading = false;
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('common.operateOk'), 3);
} else {
message.error(t('common.operateErr'), 3);
}
});
},
});
}
/**返回上一步 */
function fnStepPrev() {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.ne.neQuickSetup.stepPrevTip'),
onOk() {
fnToStepName('Para5G');
},
});
}
/**下一步操作 */
function fnStepNext() {
if (!state.stepNext) return;
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.ne.neQuickSetup.startStepNext'),
onOk() {
fnToStepName('NeInfoConfig');
},
});
}
onMounted(() => {
// 初始字典数据
Promise.allSettled([getDict('ne_host_authMode')]).then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.neHostAuthMode = resArr[0].value;
}
});
});
</script>
<template>
<div class="ne">
<a-descriptions :column="{ lg: 3, md: 2, sm: 2, xs: 1 }" bordered>
<a-descriptions-item :label="t('views.ne.neQuickSetup.addr')" :span="3">
{{ state.info.addr }}
</a-descriptions-item>
<a-descriptions-item :label="t('views.ne.neQuickSetup.kernelName')">
{{ state.info.kernelName }}
</a-descriptions-item>
<a-descriptions-item :label="t('views.ne.neQuickSetup.machine')">
{{ state.info.machine }}
</a-descriptions-item>
<a-descriptions-item :label="t('views.ne.neQuickSetup.kernelRelease')">
{{ state.info.kernelRelease }}
</a-descriptions-item>
<a-descriptions-item>
<template #label>
{{ t('views.ne.neQuickSetup.prettyName') }}
<a-tooltip placement="topLeft">
<template #title>
{{ t('views.ne.neQuickSetup.prettyNameTip') }}
</template>
<InfoCircleOutlined style="opacity: 0.45; color: inherit" />
</a-tooltip>
</template>
{{ state.info.prettyName }}
</a-descriptions-item>
<a-descriptions-item :label="t('views.ne.neQuickSetup.nodename')">
{{ state.info.nodename }}
</a-descriptions-item>
<a-descriptions-item :label="t('views.ne.neQuickSetup.auth')">
<a-tag :color="state.info.sudo ? 'success' : 'error'">
<template #icon>
<CheckCircleOutlined v-if="state.info.sudo" />
<CloseCircleOutlined v-else />
</template>
{{ t('views.ne.neQuickSetup.sudo') }}
</a-tag>
<a-tag :color="state.info.sshLink ? 'success' : 'error'">
<template #icon>
<CheckCircleOutlined v-if="state.info.sshLink" />
<CloseCircleOutlined v-else />
</template>
{{ t('views.ne.neQuickSetup.sshLink') }}
</a-tag>
</a-descriptions-item>
</a-descriptions>
<a-form
name="checkStateFrom"
layout="horizontal"
:label-col="{ span: 6 }"
:label-wrap="true"
style="margin-top: 20px; width: 68%"
>
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.ne.neHost.addr')"
name="addr"
v-bind="checkStateFrom.validateInfos.addr"
>
<a-input
v-model:value="state.from.addr"
allow-clear
:maxlength="128"
:placeholder="t('common.inputPlease')"
>
</a-input>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.ne.neHost.port')"
name="port"
v-bind="checkStateFrom.validateInfos.port"
>
<a-input-number
v-model:value="state.from.port"
:min="10"
:max="65535"
:step="1"
:maxlength="5"
style="width: 100%"
></a-input-number>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.ne.neHost.user')"
name="user"
v-bind="checkStateFrom.validateInfos.user"
>
<a-input
v-model:value="state.from.user"
allow-clear
:maxlength="32"
:placeholder="t('common.inputPlease')"
>
</a-input>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item :label="t('views.ne.neHost.authMode')">
<a-select
v-model:value="state.from.authMode"
default-value="0"
:options="dict.neHostAuthMode"
>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item
v-if="state.from.authMode === '0'"
:label="t('views.ne.neHost.password')"
:label-col="{ span: 3 }"
:label-wrap="true"
name="password"
v-bind="checkStateFrom.validateInfos.password"
>
<a-input-password
v-model:value="state.from.password"
:maxlength="128"
:placeholder="t('common.inputPlease')"
>
</a-input-password>
</a-form-item>
<template v-if="state.from.authMode === '1'">
<a-form-item
:label="t('views.ne.neHost.privateKey')"
:label-col="{ span: 3 }"
:label-wrap="true"
name="privateKey"
v-bind="checkStateFrom.validateInfos.privateKey"
>
<a-textarea
v-model:value="state.from.privateKey"
:auto-size="{ minRows: 4, maxRows: 6 }"
:maxlength="3000"
:show-count="true"
:placeholder="t('views.ne.neHost.privateKeyPlease')"
/>
</a-form-item>
<a-form-item
:label="t('views.ne.neHost.passPhrase')"
:label-col="{ span: 3 }"
:label-wrap="true"
>
<a-input-password
v-model:value="state.from.passPhrase"
:maxlength="128"
:placeholder="t('common.inputPlease')"
>
</a-input-password>
</a-form-item>
</template>
<a-form-item :wrapper-col="{ span: 8, offset: 3 }">
<a-space direction="horizontal" :size="18">
<a-button
type="primary"
ghost
html-type="submit"
@click="fnCheckInfo()"
:loading="state.confirmLoading"
>
{{ t('views.ne.neHost.test') }}
</a-button>
<a-button
type="dashed"
@click="fnHostAuthorized()"
:disabled="state.confirmLoading"
v-if="state.from.authMode !== '2'"
>
{{ t('views.ne.neHost.authRSA') }}
</a-button>
<a-button
type="link"
@click="fnCheckInfoReset()"
:disabled="state.confirmLoading"
>
{{ t('common.reset') }}
</a-button>
</a-space>
</a-form-item>
</a-form>
<div class="ne-oper">
<a-space direction="horizontal" :size="18">
<a-button @click="fnStepPrev()">
{{ t('views.ne.neQuickSetup.stepPrev') }}
</a-button>
<a-button
type="primary"
@click="fnStepNext()"
:disabled="!state.stepNext"
>
{{ t('views.ne.neQuickSetup.stepNext') }}
</a-button>
</a-space>
</div>
</div>
</template>
<style lang="less" scoped>
.ne {
min-height: 400px;
display: flex;
flex-direction: column;
& .ant-form {
flex: 1;
}
&-oper {
text-align: end;
}
}
</style>

View File

@@ -0,0 +1,141 @@
import { reactive, toRaw } from 'vue';
import { updateNeInfo } from '@/api/ne/neInfo';
import useNeStore from '@/store/modules/ne';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { getPara5GFilee, savePara5GFile } from '@/api/ne/neAction';
const neStore = useNeStore();
/**对象信息信息状态类型 */
type StateType = {
/**表单数据 */
from: Record<string, any>;
/**OMC信息需修改当前的IP */
omcInfo: Record<string, any>;
/**根据网元显示配置项 */
hasNE: {
amf: boolean;
upf: boolean;
ims: boolean;
mme: boolean;
};
/**确定按钮 loading */
confirmLoading: boolean;
};
export function usePara5G() {
/**对象信息状态 */
let state: StateType = reactive({
from: {},
omcInfo: {},
hasNE: {
amf: false,
upf: false,
ims: false,
mme: false,
},
confirmLoading: false,
});
/**载入数据*/
function fnReloadData() {
state.confirmLoading = true;
Promise.all([getPara5GFilee(), neStore.fnNelistRefresh()]).then(resArr => {
// 已保存的配置
if (resArr[0].code === RESULT_CODE_SUCCESS) {
Object.assign(state.from, resArr[0].data);
}
// 填充固定网元类型的ip
if (
resArr[1].code === RESULT_CODE_SUCCESS &&
Array.isArray(resArr[1].data)
) {
for (const item of resArr[1].data) {
const ipAddr = item.ipAddr;
// 公共配置文件sbi的各网元IP
switch (item.neType) {
case 'OMC':
state.from.sbi.omc_ip = ipAddr;
Object.assign(state.omcInfo, item); // 主动改OMC_IP
break;
case 'IMS':
state.from.sbi.ims_ip = ipAddr;
state.hasNE.ims = true;
break;
case 'AMF':
state.from.sbi.amf_ip = ipAddr;
state.hasNE.amf = true;
break;
case 'AUSF':
state.from.sbi.ausf_ip = ipAddr;
break;
case 'UDM':
state.from.sbi.udm_ip = ipAddr;
state.from.sbi.db_ip = '0.0.0.0';
break;
case 'SMF':
state.from.sbi.smf_ip = ipAddr;
break;
case 'PCF':
state.from.sbi.pcf_ip = ipAddr;
break;
case 'NSSF':
state.from.sbi.nssf_ip = ipAddr;
break;
case 'NRF':
state.from.sbi.nrf_ip = ipAddr;
break;
case 'UPF':
state.from.sbi.upf_ip = ipAddr;
state.hasNE.upf = true;
break;
case 'LMF':
state.from.sbi.lmf_ip = ipAddr;
break;
case 'NEF':
state.from.sbi.nef_ip = ipAddr;
break;
case 'MME':
state.from.sbi.mme_ip = ipAddr;
if (ipAddr.includes('.')) {
state.from.external.mmes11_ip = ipAddr + '/24';
}
state.hasNE.mme = true;
break;
case 'N3IWF':
state.from.sbi.n3iwf_ip = ipAddr;
break;
case 'SMSC':
state.from.sbi.smsc_ip = ipAddr;
break;
}
}
}
state.confirmLoading = false;
});
}
/**保存数据 */
async function fnSaveData() {
if (state.confirmLoading) return;
state.confirmLoading = true;
const res = await savePara5GFile({
content: toRaw(state.from),
syncNe: [],
});
if (res.code === RESULT_CODE_SUCCESS) {
// 更新omc_ip
if (state.omcInfo.id) {
state.omcInfo.ip = state.from.sbi.omc_ip;
await updateNeInfo(toRaw(state.omcInfo));
}
}
state.confirmLoading = false;
return res;
}
return {
state,
fnReloadData,
fnSaveData,
};
}

View File

@@ -0,0 +1,103 @@
import { defineAsyncComponent, reactive, shallowRef, watch } from 'vue';
/**步骤信息状态类型 */
type StepStateType = {
/**步骤名称 */
stepName: string;
/**安装网元步骤信息 */
steps: any[];
/**安装网元步骤当前选中 */
current: number;
/**连接主机 */
neHost: Record<string, any>;
/**网元信息 */
neInfo: Record<string, any>;
};
/**步骤信息状态 */
export const stepState: StepStateType = reactive({
stepName: 'Para5G',
steps: [
{
title: '服务器环境',
description: '服务端与网元服务',
},
{
title: '配置网元信息',
description: '网元信息设置',
},
{
title: '网元软件安装',
description: '软件安装到服务端',
},
{
title: '网元授权激活',
description: '网元服务授权激活',
},
],
current: -1,
neHost: {},
neInfo: {},
});
/**步骤信息状态复位 */
export function fnRestStepState(t?: any) {
stepState.stepName = 'Para5G';
stepState.current = -1;
stepState.neHost = {};
stepState.neInfo = {};
// 多语言翻译
if (t) {
stepState.steps = [
{
title: t('views.ne.neQuickSetup.startTitle'),
description: t('views.ne.neQuickSetup.startDesc'),
},
{
title: t('views.ne.neQuickSetup.configTitle'),
description: t('views.ne.neQuickSetup.configDesc'),
},
{
title: t('views.ne.neQuickSetup.installTitle'),
description: t('views.ne.neQuickSetup.installDesc'),
},
{
title: t('views.ne.neQuickSetup.licenseTitle'),
description: t('views.ne.neQuickSetup.licenseDesc'),
},
];
}
}
/**跳转步骤组件 */
export function fnToStepName(stepName: string) {
stepState.current = [
'Start',
'NeInfoConfig',
'NeInfoSoftwareInstall',
'NeInfoSoftwareLicense',
].indexOf(stepName);
stepState.stepName = stepName;
}
export function useStep({ t }: any) {
// 异步加载组件
const Start = defineAsyncComponent(() => import('../components/Start.vue'));
// 当前组件
const currentComponent = shallowRef(Start);
watch(
() => stepState.stepName,
v => {
const loadComponent = defineAsyncComponent(
() => import(`../components/${v}.vue`)
);
currentComponent.value = loadComponent;
}
);
return { currentComponent };
}

View File

@@ -0,0 +1,90 @@
<script lang="ts" setup>
import { PageContainer } from 'antdv-pro-layout';
import Para5GForm from './components/Para5GForm.vue';
import {
stepState,
fnToStepName,
fnRestStepState,
useStep,
} from './hooks/useStep';
import { usePara5G } from './hooks/usePara5G';
import useI18n from '@/hooks/useI18n';
import { onMounted, onUnmounted, watch } from 'vue';
const { t } = useI18n();
const { currentComponent } = useStep(t);
const { state, fnReloadData, fnSaveData } = usePara5G();
watch(
() => stepState.stepName,
v => {
if (v === 'Para5G') {
fnReloadData();
}
}
);
onMounted(() => {
fnRestStepState(t);
fnReloadData();
});
onUnmounted(() => {
fnRestStepState(t);
});
/**公共参数保存前下一步进行网元安装 */
function fnNext() {
fnSaveData().then(() => {
fnToStepName('Start');
});
}
</script>
<template>
<PageContainer>
<a-card :bordered="false" v-if="stepState.stepName === 'Para5G'">
<!-- 公共参数表单 -->
<Para5GForm v-model:data="state.from" :ne="state.hasNE"></Para5GForm>
<div style="padding: 24px 12px 0; text-align: end">
<a-space :size="8" align="center">
<a-button
type="default"
:disabled="state.confirmLoading"
@click.prevent="fnReloadData()"
>
<template #icon><ReloadOutlined /></template>
{{ t('views.ne.neQuickSetup.reloadPara5G') }}
</a-button>
<a-button
type="primary"
:loading="state.confirmLoading"
@click="fnNext()"
>
{{ t('views.ne.neQuickSetup.stepNext') }}
</a-button>
</a-space>
</div>
</a-card>
<a-card :bordered="false" v-else>
<!-- 插槽-卡片左侧 -->
<template #title>
<!-- 步骤进度 -->
<a-steps :current="stepState.current" direction="horizontal" style="margin: 24px 0;">
<a-step
v-for="s in stepState.steps"
:key="s.title"
:title="s.title"
:description="s.description"
:disabled="true"
/>
</a-steps>
</template>
<!-- 步骤页面 -->
<component :is="currentComponent" />
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,473 @@
<script setup lang="ts">
import { reactive, onMounted, toRaw, watch } from 'vue';
import { ProModal } from 'antdv-pro-modal';
import { message, Form, Upload, notification } from 'ant-design-vue/es';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { NE_EXPAND_LIST, NE_TYPE_LIST } from '@/constants/ne-constants';
import { UploadRequestOption } from 'ant-design-vue/es/vc-upload/interface';
import {
addNeSoftware,
getNeSoftware,
updateNeSoftware,
} from '@/api/ne/neSoftware';
import { FileType } from 'ant-design-vue/es/upload/interface';
import { uploadFileChunk } from '@/api/tool/file';
import useI18n from '@/hooks/useI18n';
const { t } = useI18n();
const emit = defineEmits(['ok', 'cancel', 'update:open']);
const props = defineProps({
open: {
type: Boolean,
default: false,
},
editId: {
type: Number,
default: 0,
},
});
/**对话框对象信息状态类型 */
type ModalStateType = {
/**新增框或修改框是否显示 */
openByEdit: boolean;
/**标题 */
title: string;
/**表单数据 */
from: {
id: number | undefined;
neType: string;
name: string;
path: string;
version: string;
description: string;
};
/**确定按钮 loading */
confirmLoading: boolean;
/**上传文件 */
uploadFiles: any[];
/**上传文件-依赖包 */
uploadFilesDep: any[];
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
openByEdit: false,
title: '软件包文件',
from: {
id: undefined,
neType: '',
name: '',
path: '',
version: '',
description: '',
},
confirmLoading: false,
uploadFiles: [],
uploadFilesDep: [],
});
/**对话框内表单属性和校验规则 */
const modalStateFrom = Form.useForm(
modalState.from,
reactive({
neType: [
{
required: true,
min: 1,
max: 32,
message: t('views.ne.common.neTypePlease'),
},
],
version: [
{
required: true,
min: 1,
max: 64,
message: t('views.ne.neSoftware.versionPlease'),
},
],
path: [
{
required: true,
message: t('views.ne.neSoftware.pathPlease'),
},
],
})
);
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
function fnModalOk() {
if (modalState.confirmLoading) return;
modalStateFrom
.validate()
.then(e => {
modalState.confirmLoading = true;
const hide = message.loading(t('common.loading'), 0);
const from = toRaw(modalState.from);
// 安装带依赖包
if (modalState.uploadFilesDep.length > 0) {
const depFiles = [];
for (const depFile of modalState.uploadFilesDep) {
if (depFile.status === 'done' && depFile.path) {
depFiles.push(depFile.path);
}
}
depFiles.push(from.path);
from.path = depFiles.join(',');
}
const software = from.id ? updateNeSoftware(from) : addNeSoftware(from);
software.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 3,
});
// 返回无引用信息
emit('ok', JSON.parse(JSON.stringify(from)));
fnModalCancel();
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
hide();
modalState.confirmLoading = false;
});
})
.catch(e => {
message.error(t('common.errorFields', { num: e.errorFields.length }), 3);
});
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalCancel() {
modalState.openByEdit = false;
modalState.confirmLoading = false;
modalStateFrom.resetFields();
modalState.uploadFiles = [];
modalState.uploadFilesDep = [];
emit('cancel');
emit('update:open', false);
}
/**表单上传前检查或转换压缩 */
function fnBeforeUploadFile(file: FileType) {
if (modalState.confirmLoading) return false;
const fileName = file.name;
const suff = fileName.substring(fileName.lastIndexOf('.'));
if (!['.deb', '.rpm'].includes(suff)) {
message.error(
t('views.ne.neSoftware.fileTypeNotEq', {
txt: '(.deb、.rpm)',
}),
3
);
return Upload.LIST_IGNORE;
}
// 取网元类型判断是否支持
let neType = '';
const neTypeIndex = fileName.indexOf('-');
if (neTypeIndex !== -1) {
neType = fileName.substring(0, neTypeIndex).toUpperCase();
}
// 主包类型
if (!NE_TYPE_LIST.includes(neType)) {
notification.warning({
message: fileName,
description: t('views.ne.neSoftware.fileCheckType'),
});
return Upload.LIST_IGNORE;
}
modalState.from.neType = neType;
// 根据给定的软件名取版本号 ims-r2.2312.x-ub22.deb
const matches = fileName.match(/([0-9.]+[0-9a-zA-Z]+)/);
if (matches) {
modalState.from.version = matches[0];
}
return true;
}
/**表单上传文件 */
function fnUploadFile(up: UploadRequestOption) {
const uploadFile = modalState.uploadFiles.find(
item => item.uid === (up.file as any).uid
);
if (!uploadFile) return;
// 发送请求
uploadFileChunk(up.file as File, 5, 'software')
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
// 改为完成状态
uploadFile.percent = 100;
uploadFile.status = 'done';
uploadFile.path = res.data.filePath;
// 预置到表单
const { filePath, originalFileName } = res.data;
modalState.from.name = originalFileName;
modalState.from.path = filePath;
} else {
message.error(res.msg, 3);
}
})
.catch(error => {
uploadFile.percent = 0;
uploadFile.status = 'error';
uploadFile.response = error.message;
})
.finally(() => {
modalState.confirmLoading = false;
});
}
/**表单上传前检查或转换压缩-依赖包 */
function fnBeforeUploadFileDep(file: FileType) {
if (modalState.confirmLoading) return false;
const fileName = file.name;
const suff = fileName.substring(fileName.lastIndexOf('.'));
if (!['.deb', '.rpm'].includes(suff)) {
message.error(
t('views.ne.neSoftware.fileTypeNotEq', {
txt: '(.deb、.rpm)',
}),
3
);
return Upload.LIST_IGNORE;
}
// 已存在同名文件
const hasItem = modalState.uploadFilesDep.find(
item => item.name === fileName
);
if (hasItem) {
notification.warning({
message: fileName,
description: t('views.ne.neSoftware.fileNameExists'),
});
return Upload.LIST_IGNORE;
}
// 取网元类型判断是否支持
let neType = '';
const neTypeIndex = fileName.indexOf('-');
if (neTypeIndex !== -1) {
neType = fileName.substring(0, neTypeIndex).toUpperCase();
}
// 依赖包类型
if (!NE_EXPAND_LIST.includes(neType)) {
notification.warning({
message: fileName,
description: t('views.ne.neSoftware.fileCheckTypeDep'),
});
return Upload.LIST_IGNORE;
}
return true;
}
/**表单上传文件-依赖包 */
function fnUploadFileDep(up: UploadRequestOption) {
const uploadFile = modalState.uploadFilesDep.find(
item => item.uid === (up.file as any).uid
);
if (!uploadFile) return;
// 发送请求
uploadFileChunk(up.file as File, 5, 'software')
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
// 改为完成状态
uploadFile.percent = 100;
uploadFile.status = 'done';
uploadFile.path = res.data.filePath;
} else {
message.error(res.msg, 3);
}
})
.catch(error => {
uploadFile.percent = 0;
uploadFile.status = 'error';
uploadFile.response = error.message;
})
.finally(() => {
modalState.confirmLoading = false;
});
}
/**
* 对话框弹出显示为 新增或者修改
* @param id id
*/
function fnModalVisibleByEdit(id: number) {
if (id > 0) {
const hide = message.loading(t('common.loading'), 0);
getNeSoftware(id)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
Object.assign(modalState.from, res.data);
modalState.title = t('views.ne.neSoftware.uploadTitle');
modalState.openByEdit = true;
} else {
message.error(res.msg, 3);
}
})
.finally(() => {
modalState.confirmLoading = false;
hide();
});
return;
}
modalState.title = t('views.ne.neSoftware.uploadTitle');
modalState.openByEdit = true;
}
/**监听是否显示,初始数据 */
watch(
() => props.open,
val => {
if (val) fnModalVisibleByEdit(props.editId);
}
);
onMounted(() => {});
</script>
<template>
<ProModal
:drag="true"
:width="650"
: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"
:wrapper-col="{ span: 16 }"
:label-col="{ span: 8 }"
:labelWrap="true"
>
<template v-if="modalState.from.id === undefined">
<a-form-item
:label="t('views.ne.neSoftware.path')"
:help="t('views.ne.neSoftware.uploadFileName')"
name="file"
v-bind="modalStateFrom.validateInfos.path"
>
<a-upload
name="file"
v-model:file-list="modalState.uploadFiles"
accept=".rpm,.deb"
list-type="text"
:max-count="1"
:show-upload-list="{
showPreviewIcon: false,
showRemoveIcon: false,
showDownloadIcon: false,
}"
:before-upload="fnBeforeUploadFile"
:custom-request="fnUploadFile"
:disabled="modalState.confirmLoading"
>
<a-button type="primary">
<template #icon>
<UploadOutlined />
</template>
{{ t('views.ne.neSoftware.upload') }}
</a-button>
</a-upload>
</a-form-item>
<a-form-item
name="dep"
:label="t('views.ne.neSoftware.dependFile')"
:help="t('views.ne.neSoftware.dependFileTip')"
>
<a-upload
name="file"
v-model:file-list="modalState.uploadFilesDep"
accept=".rpm,.deb"
list-type="text"
:multiple="true"
:max-count="5"
:show-upload-list="{
showPreviewIcon: false,
showRemoveIcon: true,
showDownloadIcon: false,
}"
:before-upload="fnBeforeUploadFileDep"
:custom-request="fnUploadFileDep"
:disabled="modalState.confirmLoading"
>
<a-button type="dashed">
<template #icon>
<UploadOutlined />
</template>
{{ t('views.ne.neSoftware.upload') }}
</a-button>
</a-upload>
</a-form-item>
</template>
<a-form-item
:label="t('views.ne.common.neType')"
name="neType"
v-bind="modalStateFrom.validateInfos.neType"
>
<a-auto-complete
v-model:value="modalState.from.neType"
:options="NE_TYPE_LIST.map(v => ({ value: v }))"
:disabled="modalState.from.id !== undefined"
>
<a-input
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="32"
:disabled="modalState.from.id !== undefined"
>
</a-input>
</a-auto-complete>
</a-form-item>
<a-form-item
:label="t('views.ne.neSoftware.version')"
name="version"
v-bind="modalStateFrom.validateInfos.version"
>
<a-input
v-model:value="modalState.from.version"
allow-clear
:placeholder="t('common.inputPlease')"
></a-input>
</a-form-item>
<a-form-item :label="t('common.description')" name="description">
<a-textarea
v-model:value="modalState.from.description"
:maxlength="500"
:show-count="true"
:placeholder="t('common.inputPlease')"
/>
</a-form-item>
</a-form>
</ProModal>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,506 @@
<script setup lang="ts">
import { reactive, onMounted, toRaw, watch } from 'vue';
import { ProModal } from 'antdv-pro-modal';
import { message, Upload, notification } from 'ant-design-vue/es';
import type { UploadRequestOption } from 'ant-design-vue/es/vc-upload/interface';
import type { FileType, UploadFile } from 'ant-design-vue/es/upload/interface';
import useI18n from '@/hooks/useI18n';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { NE_TYPE_LIST, NE_EXPAND_LIST } from '@/constants/ne-constants';
import { addNeSoftware } from '@/api/ne/neSoftware';
import { uploadFileChunk } from '@/api/tool/file';
const { t } = useI18n();
const emit = defineEmits(['ok', 'cancel', 'update:open']);
const props = defineProps({
open: {
type: Boolean,
default: false,
},
/**网元类型,指定上传 */
neType: {
type: String,
},
});
/**网元支持 */
const NE_TYPE_EXP = NE_EXPAND_LIST.concat(NE_TYPE_LIST);
/**对话框对象信息状态类型 */
type ModalStateType = {
/**新增框或修改框是否显示 */
openByMoreFile: boolean;
/**标题 */
title: string;
/**表单数据 */
from: {
neType: string;
name: string;
path: string;
version: string;
description: string;
}[];
/**确定按钮 loading */
confirmLoading: boolean;
/**上传文件 */
uploadFiles: any[];
/**上传文件-依赖包 */
uploadFilesDep: any[];
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
openByMoreFile: false,
title: '软件包文件',
from: [],
confirmLoading: false,
uploadFiles: [],
uploadFilesDep: [],
});
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
async function fnModalOk() {
if (modalState.confirmLoading) return;
if (modalState.uploadFiles.length < 1) {
message.warning({
content: t('views.ne.neSoftware.uploadNotFile'),
duration: 3,
});
return;
}
// 处理上传文件过滤
const uploadFiles = toRaw(modalState.uploadFiles);
const from = toRaw(modalState.from);
const expandFile: Record<string, string> = {};
for (const item of uploadFiles) {
if (item.status !== 'done' || !item.path) continue;
const neSoftware = from.find(s => s.name === item.name);
if (neSoftware && NE_TYPE_LIST.includes(neSoftware.neType)) {
neSoftware.path = item.path;
}
if (neSoftware && NE_EXPAND_LIST.includes(neSoftware.neType)) {
expandFile[neSoftware.neType] = item.path;
}
}
// IMS拼接拓展包
const ims = from.find(s => s.neType === 'IMS');
if (ims) {
const pkgArr = [];
if (expandFile['ADB']) {
pkgArr.push(expandFile['ADB']);
}
if (expandFile['KVDB']) {
pkgArr.push(expandFile['KVDB']);
}
if (expandFile['RTPROXY']) {
pkgArr.push(expandFile['RTPROXY']);
}
if (expandFile['MF']) {
pkgArr.push(expandFile['MF']);
}
pkgArr.push(ims.path);
ims.path = pkgArr.join(',');
}
// UDM拼接拓展包
const udm = from.find(s => s.neType === 'UDM');
if (udm && expandFile['ADB']) {
udm.path = [expandFile['ADB'], udm.path].join(',');
} else if (udm && expandFile['KVDB']) {
udm.path = [expandFile['KVDB'], udm.path].join(',');
}
// 安装带依赖包-指定网元时
if (props.neType && modalState.uploadFilesDep.length > 0) {
const neInfo = from.find(s => s.neType === props.neType);
if (neInfo) {
const depFiles = [];
for (const depFile of modalState.uploadFilesDep) {
if (depFile.status === 'done' && depFile.path) {
depFiles.push(depFile.path);
}
}
depFiles.push(neInfo.path);
neInfo.path = depFiles.join(',');
}
}
// 开始添加到软件包数据
const rows: any[] = [];
modalState.confirmLoading = true;
for (const item of from.filter(s => NE_TYPE_LIST.includes(s.neType))) {
try {
const res = await addNeSoftware(item);
if (res.code === RESULT_CODE_SUCCESS) {
rows.push(item);
} else {
message.error({
content: `${item.neType} ${res.msg}`,
duration: 3,
});
}
} catch (error) {
console.error(error);
}
}
emit('ok', rows);
fnModalCancel();
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalCancel() {
modalState.openByMoreFile = false;
modalState.confirmLoading = false;
modalState.from = [];
modalState.uploadFiles = [];
modalState.uploadFilesDep = [];
emit('cancel');
emit('update:open', false);
}
/**表单上传前检查或转换压缩 */
function fnBeforeUploadFile(file: FileType) {
if (modalState.confirmLoading) return false;
const fileName = file.name;
const suff = fileName.substring(fileName.lastIndexOf('.'));
if (!['.deb', '.rpm'].includes(suff)) {
message.error(
t('views.ne.neSoftware.fileTypeNotEq', {
txt: '(.deb、.rpm)',
}),
3
);
return Upload.LIST_IGNORE;
}
// 取网元类型判断是否支持
let neType = '';
const neTypeIndex = fileName.indexOf('-');
if (neTypeIndex !== -1) {
neType = fileName.substring(0, neTypeIndex).toUpperCase();
}
if (!NE_TYPE_EXP.includes(neType)) {
notification.warning({
message: fileName,
description: t('views.ne.neSoftware.fileCheckType'),
});
return Upload.LIST_IGNORE;
}
// 根据给定的软件名取版本号 amf-r2.2404.xx-ub22.deb
let version = '';
const matches = fileName.match(/([0-9.]+[0-9a-zA-Z]+)/);
if (matches) {
version = matches[0];
} else {
notification.warning({
message: fileName,
description: t('views.ne.neSoftware.fileCheckVer'),
});
return Upload.LIST_IGNORE;
}
// 单网元上传
if (props.neType && props.neType !== neType) {
notification.warning({
message: fileName,
description: t('views.ne.neSoftware.fileTypeNotEq', {
txt: props.neType,
}),
});
return Upload.LIST_IGNORE;
} else {
// 多文件上传时检查是否有同类型网元包
const hasItem = modalState.from.find(item => item.neType === neType);
if (hasItem) {
notification.warning({
message: fileName,
description: t('views.ne.neSoftware.fileTypeExists'),
});
return Upload.LIST_IGNORE;
}
}
modalState.from.push({
name: fileName,
neType: neType,
version: version,
path: '', // 上传完成后提交注入
description: '',
});
return true;
}
/**表单上传前删除 */
function fnBeforeRemoveFile(file: UploadFile) {
const fileName = file.name;
// 取网元类型判断是否支持
let neType = '';
const neTypeIndex = fileName.indexOf('-');
if (neTypeIndex !== -1) {
neType = fileName.substring(0, neTypeIndex).toUpperCase();
}
const idx = modalState.from.findIndex(item => item.neType === neType);
modalState.from.splice(idx, 1);
return true;
}
/**表单上传文件 */
function fnUploadFile(up: UploadRequestOption) {
const uploadFile = modalState.uploadFiles.find(
item => item.uid === (up.file as any).uid
);
if (!uploadFile) return;
// 发送请求
uploadFileChunk(up.file as File, 5, 'software')
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
// 改为完成状态
uploadFile.percent = 100;
uploadFile.status = 'done';
uploadFile.path = res.data.filePath;
} else {
message.error(res.msg, 3);
}
})
.catch(error => {
uploadFile.percent = 0;
uploadFile.status = 'error';
uploadFile.response = error.message;
})
.finally(() => {
modalState.confirmLoading = false;
});
}
/**表单上传前检查或转换压缩-依赖包 */
function fnBeforeUploadFileDep(file: FileType) {
if (modalState.confirmLoading) return false;
const fileName = file.name;
const suff = fileName.substring(fileName.lastIndexOf('.'));
if (!['.deb', '.rpm'].includes(suff)) {
message.error(
t('views.ne.neSoftware.fileTypeNotEq', {
txt: '(.deb、.rpm)',
}),
3
);
return Upload.LIST_IGNORE;
}
// 已存在同名文件
const hasItem = modalState.uploadFilesDep.find(
item => item.name === fileName
);
if (hasItem) {
notification.warning({
message: fileName,
description: t('views.ne.neSoftware.fileNameExists'),
});
return Upload.LIST_IGNORE;
}
// 取网元类型判断是否支持
let neType = '';
const neTypeIndex = fileName.indexOf('-');
if (neTypeIndex !== -1) {
neType = fileName.substring(0, neTypeIndex).toUpperCase();
}
// 依赖包类型
if (!NE_EXPAND_LIST.includes(neType)) {
notification.warning({
message: fileName,
description: t('views.ne.neSoftware.fileCheckTypeDep'),
});
return Upload.LIST_IGNORE;
}
return true;
}
/**表单上传文件-依赖包 */
function fnUploadFileDep(up: UploadRequestOption) {
const uploadFile = modalState.uploadFilesDep.find(
item => item.uid === (up.file as any).uid
);
if (!uploadFile) return;
// 发送请求
uploadFileChunk(up.file as File, 5, 'software')
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
// 改为完成状态
uploadFile.percent = 100;
uploadFile.status = 'done';
uploadFile.path = res.data.filePath;
} else {
message.error(res.msg, 3);
}
})
.catch(error => {
uploadFile.percent = 0;
uploadFile.status = 'error';
uploadFile.response = error.message;
})
.finally(() => {
modalState.confirmLoading = false;
});
}
/**监听是否显示,初始数据 */
watch(
() => props.open,
val => {
if (val) {
modalState.title = t('views.ne.neSoftware.uploadTitle');
modalState.openByMoreFile = true;
}
}
);
onMounted(() => {});
</script>
<template>
<ProModal
:drag="true"
:width="800"
:keyboard="false"
:mask-closable="false"
:open="modalState.openByMoreFile"
:title="modalState.title"
:confirm-loading="modalState.confirmLoading"
@ok="fnModalOk"
@cancel="fnModalCancel"
>
<a-form
name="modalStateFrom"
layout="horizontal"
:wrapper-col="{ span: 18 }"
:label-col="{ span: 6 }"
:labelWrap="true"
>
<template v-if="props.neType">
<a-form-item :label="t('views.ne.common.neType')" name="type">
<a-tag color="processing">
{{ props.neType }}
</a-tag>
</a-form-item>
<a-form-item
:label="t('views.ne.neSoftware.path')"
:help="t('views.ne.neSoftware.uploadFileName')"
:required="true"
:validate-on-rule-change="false"
:validateTrigger="[]"
name="file"
>
<a-upload
name="file"
v-model:file-list="modalState.uploadFiles"
accept=".rpm,.deb"
list-type="text"
:max-count="1"
:show-upload-list="{
showPreviewIcon: false,
showRemoveIcon: false,
showDownloadIcon: false,
}"
:before-upload="fnBeforeUploadFile"
:custom-request="fnUploadFile"
:disabled="modalState.confirmLoading"
>
<a-button type="primary">
<template #icon>
<UploadOutlined />
</template>
{{ t('views.ne.neSoftware.upload') }}
</a-button>
</a-upload>
</a-form-item>
<a-form-item
name="dep"
:label="t('views.ne.neSoftware.dependFile')"
:help="t('views.ne.neSoftware.dependFileTip')"
>
<a-upload
name="file"
v-model:file-list="modalState.uploadFilesDep"
accept=".rpm,.deb"
list-type="text"
:multiple="true"
:max-count="5"
:show-upload-list="{
showPreviewIcon: false,
showRemoveIcon: true,
showDownloadIcon: false,
}"
:before-upload="fnBeforeUploadFileDep"
:custom-request="fnUploadFileDep"
:disabled="modalState.confirmLoading"
>
<a-button type="dashed">
<template #icon>
<UploadOutlined />
</template>
{{ t('views.ne.neSoftware.upload') }}
</a-button>
</a-upload>
</a-form-item>
</template>
<template v-else>
<a-form-item :label="t('common.description')">
{{
t('views.ne.neSoftware.uploadBatchMax', {
txt: NE_TYPE_EXP.length,
})
}}
<br />
{{ t('views.ne.neSoftware.uploadFileName') }}
</a-form-item>
<a-form-item
:label="t('views.ne.neSoftware.path')"
:required="true"
:validate-on-rule-change="false"
:validateTrigger="[]"
name="file"
>
<a-upload
name="file"
v-model:file-list="modalState.uploadFiles"
accept=".rpm,.deb"
list-type="text"
:multiple="true"
:max-count="NE_TYPE_EXP.length"
:show-upload-list="{
showPreviewIcon: false,
showRemoveIcon: true,
showDownloadIcon: false,
}"
@remove="fnBeforeRemoveFile"
:before-upload="fnBeforeUploadFile"
:custom-request="fnUploadFile"
:disabled="modalState.confirmLoading"
>
<a-button type="primary">
<template #icon>
<UploadOutlined />
</template>
{{ t('views.ne.neSoftware.upload') }}
</a-button>
</a-upload>
</a-form-item>
</template>
</a-form>
</ProModal>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,577 @@
<script setup lang="ts">
import { reactive, ref, onMounted, toRaw, defineAsyncComponent } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { Modal, 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 useNeStore from '@/store/modules/ne';
import useI18n from '@/hooks/useI18n';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { listNeSoftware, delNeSoftware } from '@/api/ne/neSoftware';
import { parseDateToStr } from '@/utils/date-utils';
import { downloadFile } from '@/api/tool/file';
import { saveAs } from 'file-saver';
const neStore = useNeStore();
const { t } = useI18n();
// 异步加载组件
const EditModal = defineAsyncComponent(
() => import('./components/EditModal.vue')
);
const UploadMoreFile = defineAsyncComponent(
() => import('./components/UploadMoreFile.vue')
);
/**查询参数 */
let queryParams = reactive({
/**网元类型 */
neType: undefined,
/**包名称 */
name: '',
/**包版本 */
version: '',
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
neType: undefined,
name: '',
version: '',
pageNum: 1,
pageSize: 20,
});
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
/**勾选单行记录 */
selectedRowOne: any;
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
seached: false,
data: [],
selectedRowKeys: [],
/**勾选单行记录 */
selectedRowOne: { neType: '' },
});
/**表格字段列 */
let tableColumns = ref<TableColumnsType>([
{
title: t('common.rowId'),
dataIndex: 'id',
align: 'left',
width: 100,
},
{
title: t('views.ne.common.neType'),
dataIndex: 'neType',
align: 'left',
width: 100,
},
{
title: t('views.ne.neSoftware.version'),
dataIndex: 'version',
align: 'left',
width: 150,
},
{
title: t('views.ne.neSoftware.name'),
dataIndex: 'name',
align: 'left',
width: 250,
resizable: true,
minWidth: 150,
maxWidth: 400,
},
{
title: t('common.description'),
dataIndex: 'description',
key: 'description',
align: 'left',
width: 200,
resizable: true,
minWidth: 100,
maxWidth: 400,
},
{
title: t('common.createTime'),
dataIndex: 'createTime',
align: 'center',
customRender(opt) {
if (!opt.value) return '';
return parseDateToStr(opt.value);
},
width: 200,
},
{
title: t('common.operate'),
key: 'id',
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;
}
/**表格多选 */
function fnTableSelectedRowKeys(
keys: (string | number)[],
selectedRows: any[]
) {
tableState.selectedRowKeys = keys;
// 勾选单个上传
if (selectedRows.length === 1) {
tableState.selectedRowOne = selectedRows[0];
} else {
tableState.selectedRowOne = { neType: '' };
}
}
/**查询列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (tableState.loading) return;
tableState.loading = true;
if (pageNum) {
queryParams.pageNum = pageNum;
}
listNeSoftware(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;
});
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**新增框或修改框是否显示 */
openByEdit: boolean;
/**新增框或修改框ID */
editId: number;
/**多文件上传 */
openByMoreFile: boolean;
/**确定按钮 loading */
confirmLoading: boolean;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
openByEdit: false,
editId: 0,
openByMoreFile: false,
confirmLoading: false,
});
/**
* 对话框弹出显示为 新增或者修改
* @param noticeId 网元id, 不传为新增
*/
function fnModalVisibleByEdit(id: number) {
modalState.editId = id;
modalState.openByEdit = !modalState.openByEdit;
}
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
function fnModalEditOk() {
fnGetList(1);
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalEditCancel() {
modalState.editId = 0;
modalState.openByEdit = false;
modalState.openByMoreFile = false;
}
/**删除软件包 */
function fnRecordDelete(id: string) {
if (!id || modalState.confirmLoading) return;
let msg = t('views.ne.neSoftware.delTip');
if (id === '0') {
msg = `${msg} ...${tableState.selectedRowKeys.length}`;
id = tableState.selectedRowKeys.join(',');
}
Modal.confirm({
title: t('common.tipTitle'),
content: msg,
onOk() {
if (modalState.confirmLoading) return;
modalState.confirmLoading = true;
const hide = message.loading(t('common.loading'), 0);
delNeSoftware(id)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
// 获取列表数据
fnGetList();
} else {
message.error(res.msg, 3);
}
})
.finally(() => {
modalState.confirmLoading = false;
hide();
});
},
});
}
/**下载软件包 */
function fnDownloadFile(row: Record<string, any>) {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.ne.neSoftware.downTip', { txt: row.name }),
onOk() {
const hide = message.loading(t('common.loading'), 0);
downloadFile(row.path)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 2,
});
saveAs(res.data, `${row.name}`);
} else {
message.error({
content: `${res.msg}`,
duration: 2,
});
}
})
.finally(() => {
hide();
});
},
});
}
/**
* 记录更多操作
*/
function fnRecordMore(type: string | number, row: Record<string, any>) {
if (type === 'download') {
fnDownloadFile(row);
return;
}
if (type === 'delete') {
fnRecordDelete(row.id);
return;
}
}
onMounted(() => {
// 获取列表数据
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.ne.common.neType')" name="neType ">
<a-auto-complete
v-model:value="queryParams.neType"
:options="neStore.getNeSelectOtions"
allow-clear
:placeholder="t('common.inputPlease')"
/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item :label="t('views.ne.neSoftware.name')" name="name">
<a-input
v-model:value="queryParams.name"
allow-clear
:placeholder="t('common.inputPlease')"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item
:label="t('views.ne.neSoftware.version')"
name="version"
>
<a-input
v-model:value="queryParams.version"
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(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>
<a-space :size="8" align="center">
<a-button type="primary" @click.prevent="fnModalVisibleByEdit(0)">
<template #icon><UploadOutlined /></template>
{{ t('views.ne.neSoftware.upload') }}
</a-button>
<a-button
type="primary"
:disabled="tableState.selectedRowKeys.length > 1"
@click.prevent="
() => (modalState.openByMoreFile = !modalState.openByMoreFile)
"
>
<template #icon><UploadOutlined /></template>
<template v-if="tableState.selectedRowOne.neType">
{{ t('views.ne.neSoftware.upload') }}
{{ tableState.selectedRowOne.neType }}
</template>
<template v-else>
{{ t('views.ne.neSoftware.uploadBatch') }}
</template>
</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.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: tableColumns.length * 180 }"
@resizeColumn="(w:number, col:any) => (col.width = w)"
:row-selection="{
type: 'checkbox',
columnWidth: '48px',
selectedRowKeys: tableState.selectedRowKeys,
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'description'">
<a-tooltip placement="topLeft">
<template #title>{{ record.description }}</template>
<div
style="
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
width: 50px;
"
:style="{ width: column.width + 'px' }"
>
{{ record.description }}
</div>
</a-tooltip>
</template>
<template v-if="column.key === 'id'">
<a-space :size="8" align="center">
<a-tooltip placement="topRight">
<template #title> {{ t('common.editText') }}</template>
<a-button
type="link"
@click.prevent="fnModalVisibleByEdit(record.id)"
>
<template #icon> <ProfileOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip placement="left">
<template #title>{{ t('common.moreText') }}</template>
<a-dropdown placement="bottomRight" trigger="click">
<a-button type="link">
<template #icon><EllipsisOutlined /> </template>
</a-button>
<template #overlay>
<a-menu @click="({ key }:any) => fnRecordMore(key, record)">
<a-menu-item key="download">
<DownloadOutlined />
{{ t('common.downloadText') }}
</a-menu-item>
<a-menu-item key="delete">
<DeleteOutlined />
{{ t('common.deleteText') }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-tooltip>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 新增框或修改框 -->
<EditModal
v-model:open="modalState.openByEdit"
:edit-id="modalState.editId"
@ok="fnModalEditOk"
@cancel="fnModalEditCancel"
></EditModal>
<!-- 新增多文件上传框 -->
<UploadMoreFile
v-model:open="modalState.openByMoreFile"
:ne-type="tableState.selectedRowOne.neType"
@ok="fnModalEditOk"
@cancel="fnModalEditCancel"
></UploadMoreFile>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,724 @@
<script setup lang="ts">
import { reactive, ref, onMounted, toRaw, defineAsyncComponent } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { ProModal } from 'antdv-pro-modal';
import {
Modal,
TableColumnsType,
message,
notification,
} 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 useNeStore from '@/store/modules/ne';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { listNeVersion, operateNeVersion } from '@/api/ne/neVersion';
import { parseDateToStr } from '@/utils/date-utils';
import useI18n from '@/hooks/useI18n';
import useDictStore from '@/store/modules/dict';
import useMaskStore from '@/store/modules/mask';
const maskStore = useMaskStore();
const neStore = useNeStore();
const { t } = useI18n();
const { getDict } = useDictStore();
// 异步加载组件
const EditModal = defineAsyncComponent(
() => import('@/views/ne/neSoftware/components/EditModal.vue')
);
const UploadMoreFile = defineAsyncComponent(
() => import('@/views/ne/neSoftware/components/UploadMoreFile.vue')
);
/**字典数据-状态 */
let dictStatus = ref<DictType[]>([]);
/**查询参数 */
let queryParams = reactive({
/**网元类型 */
neType: undefined,
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
neType: undefined,
pageNum: 1,
pageSize: 20,
});
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: any[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
/**勾选单行记录 */
selectedRowOne: any;
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
seached: true,
data: [],
selectedRowKeys: [],
/**勾选单行记录 */
selectedRowOne: { neType: '' },
});
/**表格字段列 */
let tableColumns = ref<TableColumnsType>([
{
title: t('common.rowId'),
dataIndex: 'id',
align: 'left',
width: 100,
},
{
title: t('views.ne.common.neType'),
dataIndex: 'neType',
align: 'left',
width: 100,
},
{
title: t('views.ne.neVersion.version'),
dataIndex: 'version',
key: 'version',
align: 'left',
width: 150,
resizable: true,
minWidth: 150,
maxWidth: 200,
},
{
title: t('views.ne.neVersion.preVersion'),
dataIndex: 'preVersion',
key: 'preVersion',
align: 'left',
width: 150,
resizable: true,
minWidth: 150,
maxWidth: 200,
},
{
title: t('views.ne.neVersion.newVersion'),
dataIndex: 'newVersion',
align: 'left',
width: 150,
resizable: true,
minWidth: 150,
maxWidth: 200,
},
{
title: t('views.ne.neVersion.status'),
key: 'status',
dataIndex: 'status',
align: 'left',
width: 120,
},
{
title: t('common.updateTime'),
dataIndex: 'updateTime',
align: 'left',
customRender(opt) {
if (!opt.value) return '';
return parseDateToStr(opt.value);
},
width: 200,
},
{
title: t('common.operate'),
key: 'id',
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;
}
/**表格多选 */
function fnTableSelectedRowKeys(
keys: (string | number)[],
selectedRows: any[]
) {
tableState.selectedRowKeys = keys;
// 勾选单个上传
if (selectedRows.length === 1) {
tableState.selectedRowOne = selectedRows[0];
} else {
tableState.selectedRowOne = { neType: '' };
}
}
/**查询列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (tableState.loading) return;
tableState.loading = true;
if (pageNum) {
queryParams.pageNum = pageNum;
}
listNeVersion(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);
}
} else {
tablePagination.total = 0;
tableState.data = [];
}
tableState.loading = false;
});
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**单文件上传 */
openByEdit: boolean;
/**多文件上传 */
openByMoreFile: boolean;
/**勾选升级情况 */
openByUpgrade: boolean;
/**操作数据进行版本升级 */
operateDataUpgrade: any[];
/**确定按钮 loading */
confirmLoading: boolean;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
openByEdit: false,
openByMoreFile: false,
openByUpgrade: false,
operateDataUpgrade: [],
confirmLoading: false,
});
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
function fnModalEditOk() {
fnGetList(1);
if (modalState.openByUpgrade) {
fnModalEditCancel();
}
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalEditCancel() {
modalState.openByEdit = false;
modalState.openByMoreFile = false;
modalState.openByUpgrade = false;
modalState.operateDataUpgrade = [];
}
/**版本控制升级回退 */
function fnRecordVersion(
action: 'upgrade' | 'rollback',
row: Record<string, any>
) {
let contentTip = `${action} version packages?`;
if (action === 'upgrade') {
contentTip = t('views.ne.neVersion.upgradeTip');
if (row.newVersion === '' || row.newVersion === '-') {
message.warning(t('views.ne.neVersion.upgradeTipEmpty'), 3);
return;
}
if (row.newVersion === row.version) {
contentTip = t('views.ne.neVersion.upgradeTipEqual');
}
}
if (action === 'rollback') {
contentTip = t('views.ne.neVersion.rollbackTip');
if (row.preVersion === '' || row.preVersion === '-') {
message.warning(t('views.ne.neVersion.rollbackTipEmpty'), 3);
return;
}
if (row.prePath === '' || row.prePath === '-') {
message.warning(t('views.ne.neVersion.noPath'), 3);
return;
}
if (row.preVersion === row.version) {
contentTip = t('views.ne.neVersion.rollbackTipEqual');
}
}
Modal.confirm({
title: t('common.tipTitle'),
content: contentTip,
onOk() {
if (modalState.confirmLoading) return;
modalState.confirmLoading = true;
const notificationKey = 'NE_VERSION_' + action;
notification.info({
key: notificationKey,
message: t('common.tipTitle'),
description: `${row.neType} ${t('common.loading')}`,
duration: 0,
});
let preinput = {};
if (row.neType.toUpperCase() === 'IMS') {
preinput = { pisCSCF: 'y', updateMFetc: 'No', updateMFshare: 'No' };
}
operateNeVersion({
neType: row.neType,
neUid: row.neUid,
action: action,
preinput: preinput,
})
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
// OMC自升级
if (row.neType.toUpperCase() === 'OMC') {
if (res.code === RESULT_CODE_SUCCESS) {
maskStore.handleMaskType('reload');
} else {
message.error(t('views.ne.neVersion.upgradeFail'), 3);
}
return;
}
fnGetList(1);
} else {
message.error(t('views.ne.neVersion.upgradeFail'), 3);
}
})
.finally(() => {
notification.close(notificationKey);
modalState.confirmLoading = false;
});
},
});
}
/**版本升级弹出确认是否升级 */
function fnRecordUpgradeConfirm() {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.ne.neVersion.upgradeBatchTip'),
onOk() {
fnRecordUpgrade();
},
});
}
/**版本升级进行 */
async function fnRecordUpgrade() {
if (modalState.confirmLoading) return;
modalState.confirmLoading = true;
modalState.openByUpgrade = true;
// 操作升级的网元数据
const selectRows = tableState.data.filter(item =>
tableState.selectedRowKeys.includes(item.id)
);
for (const row of selectRows) {
if (row.newVersion === '-' || row.newVersion === '') {
modalState.operateDataUpgrade.push({
neType: row.neType,
neId: row.neId,
status: 'fail',
log: t('views.ne.neVersion.upgradeNotNewVer'),
});
continue;
}
// OMC跳过操作
if (row.neType.toUpperCase() === 'OMC') {
modalState.operateDataUpgrade.push({
neType: row.neType,
neId: row.neId,
status: 'fail',
log: t('views.ne.neVersion.upgradeOMCVer'),
});
continue;
}
// 开始升级
let preinput = {};
if (row.neType.toUpperCase() === 'IMS') {
preinput = { pisCSCF: 'y', updateMFetc: 'No', updateMFshare: 'No' };
}
const installData = {
neType: row.neType,
neId: row.neId,
action: 'upgrade',
preinput: preinput,
};
try {
const res = await operateNeVersion(installData);
const operateData = {
neType: row.neType,
neId: row.neId,
status: 'fail',
log: t('common.operateErr'),
};
if (res.code === RESULT_CODE_SUCCESS) {
operateData.status = 'done';
operateData.log = t('views.ne.neVersion.upgradeDone');
} else {
operateData.status = 'fail';
operateData.log = t('views.ne.neVersion.upgradeFail');
}
modalState.operateDataUpgrade.unshift(operateData);
} catch (error) {
console.error(error);
}
}
// 结束
modalState.confirmLoading = false;
}
onMounted(() => {
// 初始字典数据
getDict('ne_version_status')
.then(res => {
dictStatus.value = res;
})
.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.ne.common.neType')" name="neType ">
<a-auto-complete
v-model:value="queryParams.neType"
:options="neStore.getNeSelectOtions"
allow-clear
:placeholder="t('common.inputPlease')"
/>
</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>
<a-space :size="8" align="center">
<a-button
type="primary"
@click.prevent="
() => (modalState.openByEdit = !modalState.openByEdit)
"
>
<template #icon><UploadOutlined /></template>
{{ t('views.ne.neSoftware.upload') }}
</a-button>
<a-button
type="primary"
:disabled="tableState.selectedRowKeys.length > 1"
@click.prevent="
() => (modalState.openByMoreFile = !modalState.openByMoreFile)
"
>
<template #icon><UploadOutlined /></template>
<template v-if="tableState.selectedRowOne.neType">
{{ t('views.ne.neSoftware.upload') }}
{{ tableState.selectedRowOne.neType }}
</template>
<template v-else>
{{ t('views.ne.neSoftware.uploadBatch') }}
</template>
</a-button>
<a-button
type="primary"
:ghost="true"
:disabled="tableState.selectedRowKeys.length <= 0"
:loading="modalState.confirmLoading"
@click.prevent="fnRecordUpgradeConfirm()"
>
<template #icon><ThunderboltOutlined /></template>
{{ t('views.ne.neVersion.upgradeBatch') }}
</a-button>
</a-space>
</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: tableColumns.length * 150 }"
@resizeColumn="(w:number, col:any) => (col.width = w)"
:row-selection="{
type: 'checkbox',
columnWidth: '48px',
selectedRowKeys: tableState.selectedRowKeys,
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<DictTag :options="dictStatus" :value="record.status" />
</template>
<template v-if="column.key === 'version'">
{{ record.version }}
<a-tooltip
placement="topRight"
v-if="
record.version && (record.path === '' || record.path === '-')
"
>
<template #title>
{{ t('views.ne.neVersion.noPath') }}
</template>
<InfoCircleOutlined />
</a-tooltip>
</template>
<template v-if="column.key === 'preVersion'">
{{ record.preVersion }}
<a-tooltip
placement="topRight"
v-if="
record.preVersion &&
(record.prePath === '' || record.prePath === '-')
"
>
<template #title>
{{ t('views.ne.neVersion.noPath') }}
</template>
<InfoCircleOutlined />
</a-tooltip>
</template>
<template v-if="column.key === 'id'">
<a-space :size="8" align="center">
<a-tooltip placement="topRight">
<template #title>
{{ t('views.ne.neVersion.upgrade') }}
</template>
<a-button
type="link"
@click.prevent="fnRecordVersion('upgrade', record)"
>
<template #icon><ThunderboltOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip placement="topRight">
<template #title>
{{ t('views.ne.neVersion.rollback') }}
</template>
<a-button
type="link"
@click.prevent="fnRecordVersion('rollback', record)"
>
<template #icon><RollbackOutlined /></template>
</a-button>
</a-tooltip>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 新增单文件上传 -->
<EditModal
v-model:open="modalState.openByEdit"
@ok="fnModalEditOk"
@cancel="fnModalEditCancel"
></EditModal>
<!-- 新增多文件上传框 -->
<UploadMoreFile
v-model:open="modalState.openByMoreFile"
:ne-type="tableState.selectedRowOne.neType"
@ok="fnModalEditOk"
@cancel="fnModalEditCancel"
></UploadMoreFile>
<!-- 勾选网元版本进行升级框 -->
<ProModal
:drag="true"
:width="800"
:destroyOnClose="true"
:body-style="{ height: '520px', overflowY: 'scroll' }"
:keyboard="false"
:mask-closable="false"
:open="modalState.openByUpgrade"
:title="t('views.ne.neVersion.upgradeModal')"
:closable="false"
@ok="fnModalEditOk"
@cancel="fnModalEditCancel"
>
<template #footer>
<a-button
key="submit"
type="primary"
:disabled="modalState.confirmLoading"
@click="fnModalEditOk"
>
{{ t('common.close') }}
</a-button>
</template>
<p>
<a-alert
v-if="modalState.confirmLoading"
:message="t('common.loading')"
type="info"
show-icon
>
<template #icon>
<LoadingOutlined />
</template>
</a-alert>
</p>
<p v-for="o in modalState.operateDataUpgrade" :key="o.neUid">
<a-alert
:message="`${o.neType}-${o.neUid}`"
:description="o.log"
:type="o.status === 'done' ? 'success' : 'error'"
show-icon
>
<template #icon>
<CheckCircleOutlined v-if="o.status === 'done'" />
<InfoCircleOutlined v-else />
</template>
</a-alert>
</p>
</ProModal>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -394,7 +394,6 @@ onBeforeUnmount(() => {});
url="/tool/ping/run"
:ne-type="state.params.neType"
:ne-uid="state.params.neUid"
:core-uid="state.params.coreUid"
:rows="state.params.rows"
:cols="state.params.cols"
:process-messages="fnProcessMessage"