feat: 实训教学模块

This commit is contained in:
TsMask
2024-11-23 11:18:40 +08:00
parent 2fb3467fb5
commit 285c9b660b
26 changed files with 16854 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
# 实训教学模块
网元固定一套ne_id 默认使用`001`
## 静态资源
将目录下文件放置到对应目录
- i18n 对应覆盖 src\i18n
- views 对应覆盖 src\views
## 涉及文件
- src\i18n\locales\en-US.ts
- src\i18n\locales\zh-CN.ts
- src\views\monitor\topologyArchitecture\index.vue
- src\views\configManage\configParamTreeTable
- src\views\configManage\configParamApply
- src\views\ne\neInfo\index.vue
- src\plugins\auth-user.ts
- src\views\dashboard\amfUE\index.vue
- src\views\dashboard\mmeUE\index.vue
- src\views\dashboard\imsCDR\index.vue
- src\views\dashboard\smfCDR\index.vue
- src\views\dashboard\smscCDR\index.vue
- src\store\modules\user.ts

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,713 @@
<script setup lang="ts">
import { reactive, onMounted, toRaw, computed } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { ProModal } from 'antdv-pro-modal';
import { Form, 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 { parseDateToStr } from '@/utils/date-utils';
import useDictStore from '@/store/modules/dict';
import useNeInfoStore from '@/store/modules/neinfo';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import useI18n from '@/hooks/useI18n';
import {
getPtNeConfigApplyList,
stuPtNeConfigApply,
updatePtNeConfigApply,
} from '@/api/pt/neConfigApply';
import { hasRoles } from '@/plugins/auth-user';
const { t } = useI18n();
const { getDict } = useDictStore();
const neInfoStore = useNeInfoStore();
/**字典数据 */
let dict: {
/**配置申请应用状态 */
ptConfigApplyStatus: DictType[];
} = reactive({
ptConfigApplyStatus: [],
});
/**查询参数 */
let queryParams = reactive({
/**网元类型 */
neType: '',
/**申请人 */
createBy: '',
/**状态 */
status: undefined,
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
neType: '',
createBy: '',
status: undefined,
pageNum: 1,
pageSize: 20,
});
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
seached: true,
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: t('common.rowId'),
dataIndex: 'id',
align: 'center',
width: 100,
},
{
title: t('views.ne.common.neType'),
dataIndex: 'neType',
align: 'left',
width: 100,
},
{
title: '申请人',
dataIndex: 'createBy',
align: 'left',
width: 120,
},
{
title: '申请时间',
dataIndex: 'createTime',
align: 'left',
width: 150,
customRender(opt) {
if (+opt.value <= 0) return '';
return parseDateToStr(+opt.value);
},
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
align: 'left',
width: 100,
},
{
title: '处理人',
dataIndex: 'updateBy',
align: 'left',
width: 120,
},
{
title: '处理时间',
dataIndex: 'updateTime',
align: 'left',
width: 150,
customRender(opt) {
if (+opt.value <= 0) return '';
return parseDateToStr(+opt.value);
},
},
{
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: 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)[], infos: any) {
const arr = [];
for (const item of infos) {
if (item.status === '0') {
arr.push(item.id);
}
}
tableState.selectedRowKeys = arr;
}
/**查询列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (tableState.loading) return;
tableState.loading = true;
if (pageNum) {
queryParams.pageNum = pageNum;
}
getPtNeConfigApplyList(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
tablePagination.total = res.total;
tableState.data = res.rows;
if (
tablePagination.total <=
(queryParams.pageNum - 1) * tablePagination.pageSize &&
queryParams.pageNum !== 1
) {
tableState.loading = false;
fnGetList(queryParams.pageNum - 1);
}
}
tableState.loading = false;
});
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**详情框是否显示 */
openByView: boolean;
/**新增框或修改框是否显示 */
openByEdit: boolean;
/**标题 */
title: string;
/**表单数据 */
from: Record<string, any>;
/**确定按钮 loading */
confirmLoading: boolean;
/**cron生成框是否显示 */
openByCron: boolean;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
openByView: false,
openByEdit: false,
title: '任务',
from: {
id: undefined,
createBy: '',
createTime: 0,
updateBy: '',
updateTime: 0,
neType: 'MME',
status: '0',
backInfo: '',
},
confirmLoading: false,
openByCron: false,
});
/**对话框内表单属性和校验规则 */
const modalStateFrom = Form.useForm(
modalState.from,
reactive({
status: [
{
required: true,
message: t('common.selectPlease'),
},
],
})
);
/**
* 对话框弹出显示为 查看
* @param jobId 任务id
*/
function fnModalVisibleByVive(row: Record<string, any>) {
modalState.from = Object.assign(modalState.from, row);
modalState.title = '查看';
modalState.openByView = true;
}
/**
* 对话框弹出显示为 编辑
* @param jobId 任务id
*/
function fnModalVisibleByEdit(row: Record<string, any>) {
Object.assign(modalState.from, row);
modalState.from.status = '3';
modalState.title = '编辑状态';
modalState.openByEdit = true;
}
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
function fnModalOk() {
modalStateFrom
.validate()
.then(() => {
modalState.confirmLoading = true;
const from = toRaw(modalState.from);
updatePtNeConfigApply({
applyId: from.id,
neType: from.neType,
status: from.status,
backInfo: from.backInfo,
})
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 3,
});
modalState.openByEdit = false;
modalStateFrom.resetFields();
fnGetList();
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
modalState.confirmLoading = false;
});
})
.catch(e => {
message.error(t('common.errorFields', { num: e.errorFields.length }), 3);
});
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalCancel() {
modalState.openByEdit = false;
modalState.openByView = false;
modalState.from = {};
}
/**批量退回 */
function fnRecordBack(row?: Record<string, any>) {
Modal.confirm({
title: t('common.tipTitle'),
content: row
? '确认要撤回配置应用申请吗?'
: '确认要批量退回学生的配置应用申请吗?',
onOk() {
let result: any;
if (row) {
result = stuPtNeConfigApply({ neType: row.neType, status: '1' });
} else {
result = updatePtNeConfigApply({
status: "3",
backId: tableState.selectedRowKeys.join(","),
backInfo: "请重新检查配置",
});
}
result.then((res: any) => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 3,
});
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
});
},
});
}
/**应用状态 */
const applyStatus = computed(() => {
if (hasRoles(['student'])) {
return dict.ptConfigApplyStatus.filter(s => ['0', '1'].includes(s.value));
}
let data = dict.ptConfigApplyStatus;
if (modalState.openByEdit && modalState.from.id) {
data = dict.ptConfigApplyStatus.filter(s => ['2', '3'].includes(s.value));
}
return data;
});
onMounted(() => {
// 初始字典数据
Promise.allSettled([getDict('pt_config_apply_status')]).then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.ptConfigApplyStatus = resArr[0].value;
}
});
// 获取网元列表
neInfoStore.fnNelist().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.configManage.license.neType')"
name="neType "
>
<a-auto-complete
v-model:value="queryParams.neType"
:options="neInfoStore.getNeSelectOtions"
allow-clear
:placeholder="t('views.configManage.license.neTypePlease')"
/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="状态" name="status">
<a-select
v-model:value="queryParams.status"
allow-clear
:placeholder="t('common.selectPlease')"
:options="applyStatus"
>
</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-row>
</a-form>
</a-card>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<div class="button-container">
<a-button
type="default"
danger
:disabled="tableState.selectedRowKeys.length <= 0"
@click.prevent="fnRecordBack()"
v-roles:has="['admin', 'teacher']"
>
<template #icon><DeleteOutlined /></template>
批量退回
</a-button>
</div>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<div class="button-container">
<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>
</div>
</template>
<!-- 表格列表 -->
<a-table
class="table"
row-key="id"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:scroll="{ x: tableColumns.length * 120 }"
:pagination="tablePagination"
:row-selection="{
type: 'checkbox',
selectedRowKeys: tableState.selectedRowKeys,
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<DictTag
:options="dict.ptConfigApplyStatus"
:value="record.status"
/>
</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)"
>
<template #icon><ProfileOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip v-if="record.status === '0' && hasRoles(['student'])">
<template #title>撤回</template>
<a-button type="link" @click.prevent="fnRecordBack(record)">
<template #icon><RollbackOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip
v-if="record.status === '0' && hasRoles(['admin', 'teacher'])"
>
<template #title>{{ t('common.editText') }}</template>
<a-button
type="link"
@click.prevent="fnModalVisibleByEdit(record)"
>
<template #icon><FormOutlined /></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 :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item :label="t('views.ne.common.neType')" name="neType">
{{ modalState.from.neType }}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="状态" name="status">
<DictTag
:options="dict.ptConfigApplyStatus"
:value="modalState.from.status"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="申请人" name="createBy">
{{ modalState.from.createBy }}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="申请时间" name="createTime">
{{ parseDateToStr(+modalState.from.createTime) }}
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16" v-if="modalState.from.status !== '0'">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="处理人" name="updateBy">
{{ modalState.from.updateBy }}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="处理时间" name="updateTime">
{{ parseDateToStr(+modalState.from.updateTime) }}
</a-form-item>
</a-col>
</a-row>
<a-form-item
v-if="modalState.from.status === '3'"
label="退回说明"
name="backInfo"
:label-col="{ span: 3 }"
:label-wrap="true"
>
{{ modalState.from.backInfo }}
</a-form-item>
</a-form>
<template #footer>
<a-button key="cancel" @click="fnModalCancel">
{{ t('common.close') }}
</a-button>
</template>
</ProModal>
<!-- 新增框或修改框 -->
<ProModal
:drag="true"
:width="800"
:open="modalState.openByEdit"
:title="modalState.title"
:confirm-loading="modalState.confirmLoading"
:destroyOnClose="true"
@ok="fnModalOk"
@cancel="fnModalCancel"
>
<a-form
name="modalStateFrom"
layout="horizontal"
:label-col="{ span: 6 }"
:label-wrap="true"
>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item :label="t('views.ne.common.neType')" name="neType">
{{ modalState.from.neType }}
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="申请人" name="createBy">
{{ modalState.from.createBy }}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="申请时间" name="createTime">
{{ parseDateToStr(+modalState.from.createTime) }}
</a-form-item>
</a-col>
</a-row>
<a-form-item
label="状态"
name="status"
:label-col="{ span: 3 }"
:label-wrap="true"
v-bind="modalStateFrom.validateInfos.status"
>
<a-select
v-model:value="modalState.from.status"
:placeholder="t('common.selectPlease')"
:options="applyStatus"
>
</a-select>
</a-form-item>
<a-form-item
v-if="modalState.from.status === '3'"
label="退回说明"
name="backInfo"
:label-col="{ span: 3 }"
:label-wrap="true"
>
<a-textarea
v-model:value="modalState.from.backInfo"
:auto-size="{ minRows: 2, maxRows: 6 }"
:maxlength="400"
:placeholder="t('common.inputPlease')"
/>
</a-form-item>
</a-form>
</ProModal>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,327 @@
<script setup lang="ts">
import { reactive, onMounted, toRaw, watch } from 'vue';
import useI18n from '@/hooks/useI18n';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import CodemirrorEditeDiff from '@/components/CodemirrorEditeDiff/index.vue';
import { parseDateToStr } from '@/utils/date-utils';
import {
getPtNeConfigDataLogList,
restorePtNeConfigDataLog,
} from '@/api/pt/neConfigDataLog';
import useDictStore from '@/store/modules/dict';
import { message } from 'ant-design-vue';
const { t } = useI18n();
const emit = defineEmits(['ok', 'cancel', 'update:open']);
const props = defineProps({
open: {
type: Boolean,
default: false,
},
/**网元类型 */
neType: {
type: String,
default: '',
},
/**参数名 */
paramName: {
type: String,
default: '',
},
/**学生用户账号 */
student: {
type: String,
default: '',
},
});
const { getDict } = useDictStore();
/**字典数据 */
let dict: {
/**业务类型 */
sysBusinessType: DictType[];
} = reactive({
sysBusinessType: [],
});
/**对话框对象信息状态类型 */
type StateType = {
/**新增框或修改框是否显示 */
openByList: boolean;
/**差异比较框是否显示 */
openByDiff: boolean;
/**标题 */
title: string;
/**加载状态 */
loading: boolean;
/**数据 */
data: Record<string, any>[];
/**差异数据 */
dataDiff: Record<string, any>;
/**确定按钮 loading */
confirmLoading: boolean;
};
/**对话框对象信息状态 */
let state: StateType = reactive({
openByList: false,
openByDiff: false,
title: '操作参数名称-学生账号',
loading: false,
data: [],
dataDiff: {},
confirmLoading: false,
});
function onClose() {
state.loading = false;
state.openByList = false;
state.openByDiff = false;
state.data = [];
state.dataDiff = {};
emit('cancel');
emit('update:open', false);
queryParams = {
neType: '',
paramName: '',
student: '',
pageNum: 1,
pageSize: 10,
};
}
/**查询参数 */
let queryParams = reactive({
/**网元类型 */
neType: '',
/**可用属性值 */
paramName: '',
/**学生账号 */
student: '',
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 10,
});
/**查询列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (state.loading) return;
state.loading = true;
if (pageNum) {
queryParams.pageNum = pageNum;
if (pageNum === 1) state.data = [];
}
getPtNeConfigDataLogList(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.rows)) {
// tablePagination.total = res.total;
state.data = state.data.concat(res.rows);
// 去首个做标题
if (queryParams.pageNum === 1 && state.data.length > 0) {
const item = state.data[0];
state.title = `${item.paramDisplay} - ${item.createBy}`;
}
if (state.data.length <= res.total && res.rows.length > 0) {
queryParams.pageNum++;
}
}
state.loading = false;
});
}
/**差异比较框打开 */
function fnMergeCellOpen(row: Record<string, any>) {
state.dataDiff = row;
state.dataDiff.paramJsonOld = JSON.stringify(
JSON.parse(state.dataDiff.paramJsonOld),
null,
2
);
state.dataDiff.paramJsonNew = JSON.stringify(
JSON.parse(state.dataDiff.paramJsonNew),
null,
2
);
state.openByDiff = true;
}
/**差异比较框关闭 */
function fnMergeCellClose() {
state.openByDiff = false;
state.dataDiff = {};
}
/**差异比较还原 */
function fnMergeCellRestore(value: 'old' | 'new') {
if (state.confirmLoading) return;
const id = state.dataDiff.id;
restorePtNeConfigDataLog({ id, value })
.then((res: any) => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 3,
});
fnMergeCellClose();
fnGetList(1);
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
state.confirmLoading = false;
});
}
/**监听是否显示,初始数据 */
watch(
() => props.open,
val => {
if (val) {
if (props.neType && props.paramName) {
state.title = '';
state.openByList = true;
// 根据条件查询数据
queryParams.neType = props.neType;
queryParams.paramName = props.paramName;
if (props.student) {
queryParams.student = props.student;
}
fnGetList();
}
}
}
);
onMounted(() => {
// 初始字典数据
Promise.allSettled([getDict('sys_oper_type')]).then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.sysBusinessType = resArr[0].value;
}
});
});
</script>
<template>
<div>
<a-drawer
:width="500"
:title="state.title"
placement="right"
:open="state.openByList"
@close="onClose"
>
<a-list
class="demo-loadmore-list"
item-layout="horizontal"
:data-source="state.data"
>
<template #loadMore>
<div
:style="{
textAlign: 'center',
marginTop: '12px',
height: '32px',
lineHeight: '32px',
}"
>
<a-button @click="fnGetList()" :loading="state.loading">
{{ t('views.configManage.configParamForm.ptDiffLoad') }}
</a-button>
</div>
</template>
<template #renderItem="{ item }">
<a-list-item>
<template #actions>
<a-tooltip>
<template #title>
{{ t('views.configManage.configParamForm.ptDiffMerge') }}
</template>
<a-button type="primary" @click.prevent="fnMergeCellOpen(item)">
<template #icon><MergeCellsOutlined /></template>
</a-button>
</a-tooltip>
</template>
<a-list-item-meta>
<template #title>
<DictTag
:options="dict.sysBusinessType"
:value="item.operaType"
/>
</template>
<template #description>
{{ parseDateToStr(item.createTime) }}
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</a-drawer>
<a-modal
:width="800"
:destroyOnClose="true"
:mask-closable="false"
v-model:open="state.openByDiff"
:footer="null"
:body-style="{ padding: 0, maxHeight: '650px', 'overflow-y': 'auto' }"
@ok="fnMergeCellClose()"
@cancel="fnMergeCellClose()"
>
<template #title>
<DictTag
:options="dict.sysBusinessType"
:value="state.dataDiff.operaType"
/>
{{ parseDateToStr(state.dataDiff.createTime) }}
</template>
<div class="diffBack">
<div>
<a-button
type="text"
:loading="state.confirmLoading"
@click.prevent="fnMergeCellRestore('old')"
>
<template #icon><MergeCellsOutlined /></template>
{{ t('views.configManage.configParamForm.ptDiffRest') }}
</a-button>
</div>
<div>
<a-button
type="text"
:loading="state.confirmLoading"
@click.prevent="fnMergeCellRestore('new')"
>
<template #icon><MergeCellsOutlined /></template>
{{ t('views.configManage.configParamForm.ptDiffRest') }}
</a-button>
</div>
</div>
<CodemirrorEditeDiff
:old-area="state.dataDiff.paramJsonOld"
:new-area="state.dataDiff.paramJsonNew"
></CodemirrorEditeDiff>
</a-modal>
</div>
</template>
<style lang="less" scoped>
.diffBack {
display: flex;
flex-direction: row;
justify-content: space-between;
& > div:first-child {
flex: 1;
background: #fa9;
}
& > div:last-child {
flex: 1;
background: #8f8;
}
}
</style>

View File

@@ -0,0 +1,342 @@
import {
addPtNeConfigData,
delPtNeConfigData,
editPtNeConfigData,
} from '@/api/pt/neConfig';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { Modal } from 'ant-design-vue/lib';
import { SizeType } from 'ant-design-vue/lib/config-provider';
import message from 'ant-design-vue/lib/message';
import { reactive, watch } from 'vue';
/**
* 参数配置array类型
* @param param 父级传入 { t, treeState, fnActiveConfigNode, ruleVerification, modalState, fnModalCancel}
* @returns
*/
export default function useConfigArray({
t,
treeState,
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);
}
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>) {
// 遍历提取属性和值
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);
editPtNeConfigData({
neType: treeState.neType,
paramName: treeState.selectNode.paramName,
paramData: data,
loc: `${from['index']['value']}`,
})
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('views.configManage.configParamForm.updateItem', {
num: modalState.title,
}),
duration: 3,
});
fnActiveConfigNode('#');
} else {
message.warning({
content: t('views.configManage.configParamForm.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.configManage.configParamForm.delItemTip', {
num: title,
}),
onOk() {
delPtNeConfigData({
neType: treeState.neType,
paramName: treeState.selectNode.paramName,
loc: loc,
}).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('views.configManage.configParamForm.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);
}
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>) {
// 遍历提取属性和值
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);
addPtNeConfigData({
neType: treeState.neType,
paramName: treeState.selectNode.paramName,
paramData: data,
loc: `${from['index']['value']}`,
})
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('views.configManage.configParamForm.addItemOk', {
num: modalState.title,
}),
duration: 3,
});
fnActiveConfigNode('#');
} else {
message.warning({
content: t('views.configManage.configParamForm.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);
}
}
return ruleFrom;
}
// 监听表格字段列排序变化关闭展开
watch(
() => arrayState.columnsDnd,
() => {
arrayEditClose();
}
);
return {
arrayState,
arrayEdit,
arrayEditClose,
arrayEditOk,
arrayDelete,
arrayAdd,
arrayAddOk,
arrayInitEdit,
arrayInitAdd,
};
}

View File

@@ -0,0 +1,349 @@
import {
addPtNeConfigData,
delPtNeConfigData,
editPtNeConfigData,
} from '@/api/pt/neConfig';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { Modal } from 'ant-design-vue/lib';
import { SizeType } from 'ant-design-vue/lib/config-provider';
import message from 'ant-design-vue/lib/message';
import { nextTick, reactive } from 'vue';
/**
* 参数配置array类型的嵌套array
* @param param 父级传入 { t, treeState, fnActiveConfigNode, ruleVerification, modalState, arrayState, arrayInitEdit, arrayInitAdd, arrayEditClose}
* @returns
*/
export default function useConfigArrayChild({
t,
treeState,
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);
editPtNeConfigData({
neType: treeState.neType,
paramName: treeState.selectNode.paramName,
paramData: data,
loc,
})
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('views.configManage.configParamForm.updateItem', {
num: modalState.title,
}),
duration: 3,
});
fnActiveConfigNode('#');
} else {
message.warning({
content: t('views.configManage.configParamForm.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.configManage.configParamForm.delItemTip', {
num: title,
}),
onOk() {
delPtNeConfigData({
neType: treeState.neType,
paramName: treeState.selectNode.paramName,
loc,
}).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('views.configManage.configParamForm.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);
addPtNeConfigData({
neType: treeState.neType,
paramName: treeState.selectNode.paramName,
paramData: data,
loc,
})
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('views.configManage.configParamForm.addItemOk', {
num: modalState.title,
}),
duration: 3,
});
fnActiveConfigNode('#');
} else {
message.warning({
content: t('views.configManage.configParamForm.addItemErr'),
duration: 3,
});
}
})
.finally(() => {
hide();
arrayEditClose();
});
}
return {
arrayChildState,
arrayChildExpand,
arrayChildEdit,
arrayChildEditOk,
arrayChildDelete,
arrayChildAdd,
arrayChildAddOk,
};
}

View File

@@ -0,0 +1,139 @@
import { editPtNeConfigData } from '@/api/pt/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/message';
import { reactive, toRaw } from 'vue';
/**
* list类型参数处理
* @param param 父级传入 {t, treeState, ruleVerification}
* @returns
*/
export default function useConfigList({ t, treeState, 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>) {
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);
editPtNeConfigData({
neType: treeState.neType,
paramName: treeState.selectNode.paramName,
paramData: {
[from['name']]: from['value'],
},
})
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('views.configManage.configParamForm.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.configManage.configParamForm.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,192 @@
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 && 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.configManage.configParamForm.requireInt', {
display,
filter,
}),
];
}
}
break;
case 'ipv4':
if (!regExpIPv4.test(value)) {
return [
false,
t('views.configManage.configParamForm.requireIpv4', { display }),
];
}
break;
case 'ipv6':
if (!regExpIPv6.test(value)) {
return [
false,
t('views.configManage.configParamForm.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.configManage.configParamForm.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.configManage.configParamForm.requireBool', { display }),
];
}
}
break;
case 'string':
// filter: "0~128"
// 字符串长度判断
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.configManage.configParamForm.requireString', {
display,
}),
];
}
} catch (error) {
console.error(error);
}
}
// 字符串http判断
if (value.startsWith('http')) {
try {
if (!validURL(value)) {
return [
false,
t('views.configManage.configParamForm.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.configManage.configParamForm.requireString', {
display,
}),
];
}
} catch (error) {
console.error(error);
}
}
break;
default:
return [
false,
t('views.configManage.configParamForm.requireUn', { display }),
];
}
return result;
}
/**upfId可选择 */
const SMFByUPFIdOptions = ref<{ value: string; label: string }[]>([]);
/**加载smf配置的upfId */
function getConfigSMFByUPFIds(neId: string) {
getNeConfigData({
neType: 'SMF',
neId: neId,
paramName: 'upfConfig',
}).then(res => {
SMFByUPFIdOptions.value = [];
for (const s of res.data) {
SMFByUPFIdOptions.value.push({
value: s.id,
label: s.id,
});
}
});
}
return {
ruleVerification,
getConfigSMFByUPFIds,
SMFByUPFIdOptions,
};
}

View File

@@ -0,0 +1,228 @@
import { ptSaveAsDefault, ptResetAsDefault } from '@/api/pt/neConfig';
import {
getPtClassStudents,
stuPtNeConfigApply,
updatePtNeConfigApply,
} from '@/api/pt/neConfigApply';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { hasRoles } from '@/plugins/auth-user';
import { message } from 'ant-design-vue/lib';
import { computed, onMounted, reactive } from 'vue';
/**
* 实训教学函数
* @param param 父级传入 {t,fnActiveConfigNode}
* @returns
*/
export default function usePtOptions({ t, fnActiveConfigNode }: any) {
const ptConfigState = reactive({
saveLoading: false,
restLoading: false,
applyLoading: false,
});
/**(管理员)保存网元下所有配置为示例配置 */
function ptConfigSave(neType: string) {
ptConfigState.saveLoading = true;
ptSaveAsDefault(neType, '001')
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 3,
});
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
ptConfigState.saveLoading = false;
fnActiveConfigNode('#');
});
}
/**重置网元下所有配置 */
function ptConfigReset(neType: string) {
ptConfigState.restLoading = true;
ptResetAsDefault(neType)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 3,
});
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
ptConfigState.restLoading = false;
fnActiveConfigNode('#');
});
}
/**配置下方应用(学生)申请撤回和(管理/教师)应用退回 */
function ptConfigApply(
neType: string,
status: '0' | '1' | '2' | '3',
student?: string
) {
let result: any;
if (status === '2' || status === '3') {
let from: {
neType: string;
status: string;
student?: string;
backInfo?: string;
} = {
neType,
status: '2',
};
if (student) {
if (status === '2') {
from = { neType, status, student };
}
if (status === '3') {
from = { neType, status, student, backInfo: '请重新检查配置' };
}
}
result = updatePtNeConfigApply(from);
}
if (status === '0' || status === '1') {
result = stuPtNeConfigApply({ neType, status });
}
if (!result) return;
ptConfigState.applyLoading = true;
result
.then((res: any) => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 3,
});
// 教师修改学生时改变状态
if (student) {
const item = classState.studentOptionsDef.find(
s => s.value === classState.student
);
if (item) {
item.applyStatus = status;
}
}
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
ptConfigState.applyLoading = false;
});
}
const classState = reactive<{
/**学生账号 */
student: string | undefined;
/**学生可选择列表 */
studentOptions: {
value: string;
label: string;
applyId: string;
applyStatus: string;
}[];
studentOptionsDef: {
value: string;
label: string;
applyId: string;
applyStatus: string;
}[];
}>({
student: undefined,
studentOptions: [],
studentOptionsDef: [],
});
// 仅教师加载
if (hasRoles(['teacher'])) {
onMounted(() => {
classStudents(); // 初始学生列表
});
}
/**学生选择搜索 */
function studentChange(v: any) {
if (!v) {
Object.assign(classState.studentOptions, classState.studentOptionsDef);
}
fnActiveConfigNode('#');
}
let timeout: any;
/**学生选择搜索 */
function studentSearch(val: string) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
if (!val) {
Object.assign(classState.studentOptions, classState.studentOptionsDef);
return;
}
timeout = setTimeout(() => classStudents(val), 500);
}
/**班级学生列表 */
function classStudents(val?: string) {
getPtClassStudents({ userName: val }).then(res => {
classState.studentOptions = [];
if (!Array.isArray(res.data) || res.data.length <= 0) {
return;
}
for (const v of res.data) {
classState.studentOptions.push({
value: v.userName,
label: v.userName,
applyId: v.applyId,
applyStatus: v.applyStatus,
});
// 设为最新状态
const item = classState.studentOptionsDef.find(
s => s.value === v.userName
);
if (item) {
item.applyStatus = v.applyStatus;
}
}
if (!val) {
Object.assign(classState.studentOptionsDef, classState.studentOptions);
}
});
}
// 学生状态
const studentStatus = computed(() => {
const item = classState.studentOptionsDef.find(
s => s.value === classState.student
);
if (item) return item.applyStatus;
return '';
});
return {
ptConfigState,
ptConfigSave,
ptConfigReset,
ptConfigApply,
classState,
studentStatus,
studentSearch,
studentChange,
};
}

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,442 @@
<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 } 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 useDictStore from '@/store/modules/dict';
import { listAllNeInfo } from '@/api/ne/neInfo';
import { parseDateToStr } from '@/utils/date-utils';
const { getDict } = useDictStore();
const appStore = useAppStore();
const route = useRoute();
const { t } = useI18n();
echarts.use([
TooltipComponent,
GaugeChart,
TitleComponent,
LegendComponent,
PieChart,
CanvasRenderer,
LabelLayout,
]);
/**图DOM节点实例对象 */
const statusBar = ref<HTMLElement | undefined>(undefined);
/**图实例对象 */
const statusBarChart = ref<any>(null);
/**网元状态字典数据 */
let indexColor = ref<DictType[]>([
{ label: 'Normal', value: 'normal', tagType: '', tagClass: '#91cc75' },
{
label: 'Abnormal',
value: 'abnormal',
tagType: '',
tagClass: '#ee6666',
},
]);
/**表格字段列 */
//customRender(){} ----单元格处理
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);
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: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
data: [],
selectedRowKeys: [],
});
/**表格状态 */
let nfInfo: any = reactive({
obj: 'OMC',
version: appStore.version,
status: t('views.index.normal'),
outTimeDate: '',
serialNum: appStore.serialNum,
});
/**表格状态类型 */
type nfStateType = {
/**主机名 */
hostName: string;
/**操作系统信息 */
osInfo: string;
/**IP地址 */
ipAddress: string;
/**版本 */
version: string;
/**CPU利用率 */
cpuUse: string;
/**内存使用 */
memoryUse: string;
/**用户容量 */
capability: number;
/**序列号 */
serialNum: string;
/**许可证到期日期 */
/* selectedRowKeys: (string | number)[];*/
expiryDate: string;
};
/**网元详细信息 */
let pronInfo: nfStateType = reactive({
hostName: '5gc',
osInfo: 'Linux 5gc 4.15.0-112-generic 2020 x86_64 GNU/Linux',
ipAddress: '-',
version: '-',
cpuUse: '-',
memoryUse: '-',
capability: 0,
serialNum: '-',
expiryDate: '-',
});
/**查询网元状态列表 */
function fnGetList(one: boolean) {
if (tableState.loading) return;
one && (tableState.loading = true);
listAllNeInfo({ bandStatus: true }).then(res => {
tableState.data = res.data;
tableState.loading = false;
var rightNum = 0;
var errorNum = 0;
res.data.forEach((item: any) => {
if (item.serverState.online) {
rightNum++;
} else {
errorNum++;
}
});
const optionData: any = {
title: {
text: '',
subtext: '',
left: 'center',
},
tooltip: {
trigger: 'item',
},
legend: {
orient: 'vertical',
left: 'left',
},
color: indexColor.value.map(item => item.tagClass),
series: [
{
name: t('views.index.realNeStatus'),
type: 'pie',
radius: '70%',
center: ['50%', '50%'],
data: [
{ value: rightNum, name: t('views.index.normal') },
{ value: errorNum, name: t('views.index.abnormal') },
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
label: {},
},
],
};
fnDesign(statusBar.value, optionData);
});
}
function fnDesign(container: HTMLElement | undefined, option: any) {
if (!container) return;
if (!statusBarChart.value) {
statusBarChart.value = markRaw(echarts.init(container, 'light'));
}
option && statusBarChart.value.setOption(option);
// 创建 ResizeObserver 实例
var observer = new ResizeObserver(entries => {
if (statusBarChart.value) {
statusBarChart.value.resize();
}
});
// 监听元素大小变化
observer.observe(container);
}
/**抽屉 网元详细信息 */
const open = ref(false);
const closeDrawer = () => {
open.value = false;
};
/**抽屉 网元详细信息 */
/**监听表格行事件*/
function rowClick(record: any, index: any) {
return {
onClick: (event: any) => {
let pronData = JSON.parse(JSON.stringify(record.serverState));
if (!pronData.online) {
message.error(t('views.index.neStatus'), 2);
return false;
} else {
const totalMemInKB = pronData.mem?.totalMem;
const nfUsedMemInKB = pronData.mem?.nfUsedMem;
const sysMemUsageInKB = pronData.mem?.sysMemUsage;
// 将KB转换为MB
const totalMemInMB = Math.round((totalMemInKB / 1024) * 100) / 100;
const nfUsedMemInMB = Math.round((nfUsedMemInKB / 1024) * 100) / 100;
const sysMemUsageInMB =
Math.round((sysMemUsageInKB / 1024) * 100) / 100;
//渲染详细信息
pronInfo = {
hostName: pronData.hostname,
osInfo: pronData.os,
ipAddress: pronData.neIP,
version: pronData.version,
cpuUse:
pronData.neName +
':' +
pronData.cpu?.nfCpuUsage / 100 +
'%; ' +
'SYS:' +
pronData.cpu?.sysCpuUsage / 100 +
'%',
memoryUse:
'Total:' +
totalMemInMB +
'MB; ' +
pronData.name +
':' +
nfUsedMemInMB +
'MB; SYS:' +
sysMemUsageInMB +
'MB',
capability: pronData.capability,
serialNum: pronData.sn,
expiryDate: pronData.expire,
};
}
open.value = true;
},
};
}
let timer: any;
/**
* 国际化翻译转换
*/
function fnLocale() {
let title = route.meta.title as string;
if (title.indexOf('router.') !== -1) {
title = t(title);
}
appStore.setTitle(title);
}
onMounted(() => {
getDict('index_status')
.then(res => {
if (res.length > 0) {
indexColor.value = res;
}
})
.finally(() => {
fnLocale();
fnGetList(true);
timer = setInterval(() => fnGetList(false), 10000); // 每隔10秒执行一次
});
});
// 在组件卸载之前清除定时器
onBeforeUnmount(() => {
clearInterval(timer);
});
</script>
<template>
<PageContainer :breadcrumb="{}">
<div>
<a-drawer :open="open" @close="closeDrawer" :width="700">
<a-descriptions bordered :column="1" :label-style="{ width: '160px' }">
<a-descriptions-item :label="t('views.index.hostName')">{{
pronInfo.hostName
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.osInfo')">{{
pronInfo.osInfo
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.ipAddress')">{{
pronInfo.ipAddress
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.version')">{{
pronInfo.version
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.capability')">{{
pronInfo.capability
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.cpuUse')">{{
pronInfo.cpuUse
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.memoryUse')">{{
pronInfo.memoryUse
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.serialNum')">{{
pronInfo.serialNum
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.expiryDate')">{{
pronInfo.expiryDate
}}</a-descriptions-item>
</a-descriptions>
</a-drawer>
</div>
<a-row :gutter="16">
<a-col :lg="14" :md="16" :xs="24">
<!-- 表格列表 -->
<a-table
class="table"
row-key="id"
size="small"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:pagination="false"
:scroll="{ x: true }"
:customRow="rowClick"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<div v-if="record.serverState.online">
<a-tag color="blue">{{ t('views.index.normal') }}</a-tag>
</div>
<div v-else>
<a-tag color="pink">{{ t('views.index.abnormal') }}</a-tag>
</div>
</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
:title="t('views.index.mark')"
style="margin-top: 16px"
size="small"
>
<a-descriptions
bordered
:column="1"
:label-style="{ width: '160px' }"
>
<a-descriptions-item :label="t('views.index.object')">{{
nfInfo.obj
}}</a-descriptions-item>
<template v-if="nfInfo.obj === 'OMC'">
<a-descriptions-item :label="t('views.index.versionNum')">{{
nfInfo.version
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.systemStatus')">{{
nfInfo.status
}}</a-descriptions-item>
</template>
<template v-else>
<a-descriptions-item :label="t('views.index.serialNum')">{{
nfInfo.serialNum
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.expiryDate')">{{
nfInfo.outTimeDate
}}</a-descriptions-item>
</template>
</a-descriptions>
</a-card>
</a-col>
</a-row>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,728 @@
<script setup lang="ts">
import { reactive, onMounted, toRaw, ref, onBeforeUnmount } 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_ERROR,
RESULT_CODE_SUCCESS,
} from '@/constants/result-constants';
import useDictStore from '@/store/modules/dict';
import { listAMFDataUE, delAMFDataUE, exportAMFDataUE } from '@/api/neData/amf';
import { OptionsType, WS } from '@/plugins/ws-websocket';
import saveAs from 'file-saver';
import PQueue from 'p-queue';
import { hasRoles } from '@/plugins/auth-user';
const { t } = useI18n();
const { getDict } = useDictStore();
const ws = new WS();
const queue = new PQueue({ concurrency: 1, autoStart: true });
/**字典数据 */
let dict: {
/**UE 事件认证代码类型 */
ueAauthCode: DictType[];
/**UE 事件类型 */
ueEventType: DictType[];
/**UE 事件CM状态 */
ueEventCmState: DictType[];
} = reactive({
ueAauthCode: [],
ueEventType: [],
ueEventCmState: [],
});
/**开始结束时间 */
let queryRangePicker = ref<[string, string]>(['', '']);
/**查询参数 */
let queryParams = reactive({
/**网元类型 */
neType: 'AMF',
neId: '001',
eventType: '',
imsi: '',
sortField: 'timestamp',
sortOrder: 'desc',
/**开始时间 */
startTime: '',
/**结束时间 */
endTime: '',
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
eventTypes.value = [];
queryParams = Object.assign(queryParams, {
eventType: '',
imsi: '',
startTime: '',
endTime: '',
pageNum: 1,
pageSize: 20,
});
queryRangePicker.value = ['', ''];
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**记录类型 */
const eventTypes = ref<string[]>([]);
/**查询记录类型变更 */
function fnQueryEventTypeChange(value: any) {
if (Array.isArray(value)) {
queryParams.eventType = value.join(',');
}
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
seached: true,
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: t('common.rowId'),
dataIndex: 'id',
align: 'left',
width: 100,
},
{
title: 'IMSI',
dataIndex: 'eventJSON',
align: 'left',
width: 150,
customRender(opt) {
const eventJSON = opt.value;
return eventJSON.imsi;
},
},
{
title: t('views.dashboard.ue.eventType'),
dataIndex: 'eventType',
key: 'eventType',
align: 'left',
width: 150,
},
{
title: t('views.dashboard.ue.result'),
dataIndex: 'eventJSON',
key: 'result',
align: 'left',
width: 150,
},
{
title: t('views.dashboard.ue.time'),
dataIndex: 'eventJSON',
key: 'time',
align: 'left',
width: 150,
},
{
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 = {
/**确定按钮 loading */
confirmLoading: boolean;
/**最大ID值 */
maxId: number;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
confirmLoading: false,
maxId: 0,
});
/**
* 记录删除
* @param id 编号
*/
function fnRecordDelete(id: string) {
if (!id || modalState.confirmLoading) return;
let msg = id;
if (id === '0') {
msg = `${id}... ${tableState.selectedRowKeys.length}`;
id = tableState.selectedRowKeys.join(',');
}
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.dashboard.ue.delTip', { msg }),
onOk() {
modalState.confirmLoading = true;
const hide = message.loading(t('common.loading'), 0);
delAMFDataUE(id)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 3,
});
fnGetList(1);
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
hide();
modalState.confirmLoading = false;
});
},
});
}
/**查询列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (tableState.loading) return;
tableState.loading = true;
if (pageNum) {
queryParams.pageNum = pageNum;
}
if (!queryRangePicker.value) {
queryRangePicker.value = ['', ''];
}
queryParams.startTime = queryRangePicker.value[0];
queryParams.endTime = queryRangePicker.value[1];
listAMFDataUE(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.rows)) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
tablePagination.total = res.total;
// 遍历处理cdr字符串数据
tableState.data = res.rows.map(item => {
let eventJSON = item.eventJSON;
if (!eventJSON) {
Reflect.set(item, 'eventJSON', {});
}
try {
eventJSON = JSON.parse(eventJSON);
Reflect.set(item, 'eventJSON', eventJSON);
} catch (error) {
console.error(error);
Reflect.set(item, 'eventJSON', {});
}
return item;
});
// 取最大值ID用作实时累加
if (res.total > 0) {
modalState.maxId = Number(res.rows[0].id);
}
}
tableState.loading = false;
});
}
/**列表导出 */
function fnExportList() {
if (modalState.confirmLoading) return;
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.dashboard.ue.exportTip'),
onOk() {
const hide = message.loading(t('common.loading'), 0);
const querys = toRaw(queryParams);
querys.pageSize = 10000;
exportAMFDataUE(querys)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 3,
});
saveAs(res.data, `amf_ue_event_export_${Date.now()}.xlsx`);
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
hide();
modalState.confirmLoading = false;
});
},
});
}
/**实时数据开关 */
const realTimeData = ref<boolean>(false);
/**
* 实时数据
*/
function fnRealTime() {
realTimeData.value = !realTimeData.value;
if (realTimeData.value) {
// 建立链接
const options: OptionsType = {
url: '/ws',
params: {
/**订阅通道组
*
* AMF_UE会话事件(GroupID:1010)
*/
subGroupID: '1010',
},
onmessage: wsMessage,
onerror: wsError,
};
ws.connect(options);
} else {
ws.close();
}
}
/**接收数据后回调 */
function wsError(ev: any) {
// 接收数据后回调
console.error(ev);
}
/**接收数据后回调 */
function wsMessage(res: Record<string, any>) {
const { code, requestId, data } = res;
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
// 订阅组信息
if (!data?.groupId) {
return;
}
// ueEvent AMF_UE会话事件
if (data.groupId === '1010') {
const ueEvent = data.data;
queue.add(async () => {
modalState.maxId += 1;
tableState.data.unshift({
id: modalState.maxId,
neType: ueEvent.neType,
neName: ueEvent.neName, // 空
rmUID: ueEvent.rmUID, // 空
timestamp: ueEvent.timestamp,
eventType: ueEvent.eventType,
eventJSON: ueEvent.eventJSON,
});
tablePagination.total += 1;
if (tableState.data.length > 100) {
tableState.data.pop();
}
await new Promise(resolve => setTimeout(resolve, 800));
});
}
}
onMounted(() => {
// 初始字典数据
Promise.allSettled([
getDict('ue_auth_code'),
getDict('ue_event_type'),
getDict('ue_event_cm_state'),
])
.then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.ueAauthCode = resArr[0].value;
}
if (resArr[1].status === 'fulfilled') {
dict.ueEventType = resArr[1].value;
}
if (resArr[2].status === 'fulfilled') {
dict.ueEventCmState = resArr[2].value;
}
})
.finally(() => {
// 获取列表数据
fnGetList();
});
});
onBeforeUnmount(() => {
if (ws.state() !== -1) {
ws.close();
}
});
</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.dashboard.ue.eventType')"
name="eventType "
>
<a-select
v-model:value="eventTypes"
mode="multiple"
:options="dict.ueEventType"
:placeholder="t('common.selectPlease')"
@change="fnQueryEventTypeChange"
></a-select>
</a-form-item>
</a-col>
<a-col :lg="4" :md="12" :xs="24">
<a-form-item label="IMSI" name="imsi ">
<a-input
v-model:value="queryParams.imsi"
allow-clear
: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.dashboard.cdr.time')"
name="queryRangePicker"
>
<a-range-picker
v-model:value="queryRangePicker"
allow-clear
bordered
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
value-format="x"
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>
<a-space :size="8" align="center">
<a-popconfirm
placement="bottomLeft"
:title="
!realTimeData
? t('views.dashboard.ue.realTimeDataStart')
: t('views.dashboard.ue.realTimeDataStop')
"
ok-text="Yes"
cancel-text="No"
@confirm="fnRealTime()"
>
<a-button type="primary" :danger="realTimeData">
<template #icon><FundOutlined /> </template>
{{
!realTimeData
? t('views.dashboard.ue.realTimeDataStart')
: t('views.dashboard.ue.realTimeDataStop')
}}
</a-button>
</a-popconfirm>
<a-button
type="default"
danger
:disabled="tableState.selectedRowKeys.length <= 0"
:loading="modalState.confirmLoading"
@click.prevent="fnRecordDelete('0')"
v-if="!hasRoles(['student'])"
>
<template #icon><DeleteOutlined /></template>
{{ t('common.deleteText') }}
</a-button>
<a-button type="dashed" @click.prevent="fnExportList()">
<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>
<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="hasRoles(['student']) ? tableColumns.filter((s:any)=>s.key !== 'id'): tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:pagination="tablePagination"
:scroll="{ x: tableColumns.length * 120, y: 'calc(100vh - 480px)' }"
:row-selection="{
type: 'checkbox',
columnWidth: '48px',
selectedRowKeys: tableState.selectedRowKeys,
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'eventType'">
<DictTag :options="dict.ueEventType" :value="record.eventType" />
</template>
<template v-if="column.key === 'result'">
<span v-if="record.eventType === 'auth-result'">
<DictTag
:options="dict.ueAauthCode"
:value="record.eventJSON.authCode"
/>
</span>
<span v-if="record.eventType === 'detach'">
<span>{{ t('views.dashboard.ue.resultOk') }}</span>
</span>
<span v-if="record.eventType === 'cm-state'">
<DictTag
:options="dict.ueEventCmState"
:value="record.eventJSON.status"
/>
</span>
</template>
<template v-if="column.key === 'time'">
<span
v-if="record.eventType === 'auth-result'"
:title="record.eventJSON.authTime"
>
{{ record.eventJSON.authTime }}
</span>
<span
v-if="record.eventType === 'detach'"
:title="record.eventJSON.detachTime"
>
{{ record.eventJSON.detachTime }}
</span>
<span
v-if="record.eventType === 'cm-state'"
:title="record.eventJSON.changeTime"
>
{{ record.eventJSON.changeTime }}
</span>
</template>
<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.id)"
>
<template #icon>
<DeleteOutlined />
</template>
</a-button>
</a-tooltip>
</a-space>
</template>
</template>
<template #expandedRowRender="{ record }">
<div style="width: 46%; padding-left: 32px; padding-bottom: 16px">
<a-divider orientation="left">
{{ t('views.dashboard.ue.ueInfo') }}
</a-divider>
<div>
<span>{{ t('views.ne.common.neName') }}: </span>
<span>{{ record.neName }}</span>
</div>
<div>
<span>{{ t('views.ne.common.rmUid') }}: </span>
<span>{{ record.rmUID }}</span>
</div>
<a-divider orientation="left">
{{ t('views.dashboard.ue.rowInfo') }}
</a-divider>
<div>
<span>{{ t('views.dashboard.ue.time') }}: </span>
<span
v-if="record.eventType === 'auth-result'"
:title="record.eventJSON.authTime"
>
{{ record.eventJSON.authTime }}
</span>
<span
v-if="record.eventType === 'detach'"
:title="record.eventJSON.detachTime"
>
{{ record.eventJSON.detachTime }}
</span>
<span
v-if="record.eventType === 'cm-state'"
:title="record.eventJSON.changeTime"
>
{{ record.eventJSON.changeTime }}
</span>
</div>
<div>
<span>{{ t('views.dashboard.ue.eventType') }}: </span>
<DictTag :options="dict.ueEventType" :value="record.eventType" />
</div>
<div>
<span>{{ t('views.dashboard.ue.result') }}: </span>
<span v-if="record.eventType === 'auth-result'">
<DictTag
:options="dict.ueAauthCode"
:value="record.eventJSON.authCode"
/>
</span>
<span v-if="record.eventType === 'detach'">
{{ t('views.dashboard.ue.resultOk') }}
</span>
<span v-if="record.eventType === 'cm-state'">
<DictTag
:options="dict.ueEventCmState"
:value="record.eventJSON.status"
/>
</span>
</div>
</div>
</template>
</a-table>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,839 @@
<script setup lang="ts">
import { reactive, onMounted, toRaw, ref, onBeforeUnmount } 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_ERROR,
RESULT_CODE_SUCCESS,
} from '@/constants/result-constants';
import useDictStore from '@/store/modules/dict';
import useNeInfoStore from '@/store/modules/neinfo';
import {
delIMSDataCDR,
exportIMSDataCDR,
listIMSDataCDR,
} from '@/api/neData/ims';
import { parseDateToStr, parseDuration } from '@/utils/date-utils';
import { OptionsType, WS } from '@/plugins/ws-websocket';
import saveAs from 'file-saver';
import PQueue from 'p-queue';
import { hasRoles } from '@/plugins/auth-user';
const { t } = useI18n();
const { getDict } = useDictStore();
const ws = new WS();
const queue = new PQueue({ concurrency: 1, autoStart: true });
/**字典数据 */
let dict: {
/**CDR SIP响应代码类别类型 */
cdrSipCode: DictType[];
/**CDR 呼叫类型 */
cdrCallType: DictType[];
} = reactive({
cdrSipCode: [],
cdrCallType: [],
});
/**网元可选 */
let neOtions = ref<Record<string, any>[]>([]);
/**开始结束时间 */
let queryRangePicker = ref<[string, string]>(['', '']);
/**查询参数 */
let queryParams = reactive({
/**网元类型 */
neType: 'IMS',
neId: '001',
recordType: '',
callerParty: '',
calledParty: '',
sortField: 'timestamp',
sortOrder: 'desc',
/**开始时间 */
startTime: '',
/**结束时间 */
endTime: '',
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
recordTypes.value = [];
queryParams = Object.assign(queryParams, {
recordType: '',
callerParty: '',
calledParty: '',
startTime: '',
endTime: '',
pageNum: 1,
pageSize: 20,
});
queryRangePicker.value = ['', ''];
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**记录类型 */
const recordTypes = ref<string[]>([]);
/**查询记录类型变更 */
function fnQueryRecordTypeChange(value: any) {
if (Array.isArray(value)) {
queryParams.recordType = value.join(',');
}
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
seached: true,
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: t('common.rowId'),
dataIndex: 'id',
align: 'left',
width: 100,
},
{
title: t('views.dashboard.cdr.recordType'),
dataIndex: 'cdrJSON',
align: 'left',
width: 150,
customRender(opt) {
const cdrJSON = opt.value;
return cdrJSON.recordType;
},
},
{
title: t('views.dashboard.cdr.type'),
dataIndex: 'cdrJSON',
key: 'callType',
align: 'left',
width: 100,
},
{
title: t('views.dashboard.cdr.caller'),
dataIndex: 'cdrJSON',
key: 'callerParty',
align: 'left',
width: 120,
customRender(opt) {
const cdrJSON = opt.value;
return cdrJSON.callerParty;
},
},
{
title: t('views.dashboard.cdr.called'),
dataIndex: 'cdrJSON',
key: 'calledParty',
align: 'left',
width: 120,
customRender(opt) {
const cdrJSON = opt.value;
return cdrJSON.calledParty;
},
},
{
title: t('views.dashboard.cdr.result'),
dataIndex: 'cdrJSON',
key: 'cause',
align: 'left',
width: 150,
},
{
title: t('views.dashboard.cdr.duration'),
dataIndex: 'cdrJSON',
key: 'callDuration',
align: 'left',
width: 100,
customRender(opt) {
const cdrJSON = opt.value;
return cdrJSON.callType === 'sms'
? '-'
: parseDuration(cdrJSON.callDuration);
},
},
{
title: t('views.dashboard.cdr.seizureTime'),
dataIndex: 'cdrJSON',
align: 'left',
width: 200,
customRender(opt) {
const cdrJSON = opt.value;
if (typeof cdrJSON.seizureTime === 'number') {
return parseDateToStr(+cdrJSON.seizureTime * 1000);
}
return cdrJSON.seizureTime;
},
},
{
title: t('views.dashboard.cdr.releaseTime'),
dataIndex: 'cdrJSON',
align: 'left',
width: 200,
customRender(opt) {
const cdrJSON = opt.value;
if (typeof cdrJSON.releaseTime === 'number') {
return parseDateToStr(+cdrJSON.releaseTime * 1000);
}
return cdrJSON.releaseTime;
},
},
{
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 = {
/**确定按钮 loading */
confirmLoading: boolean;
/**最大ID值 */
maxId: number;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
confirmLoading: false,
maxId: 0,
});
/**
* 记录删除
* @param id 编号
*/
function fnRecordDelete(id: string) {
if (!id || modalState.confirmLoading) return;
let msg = id;
if (id === '0') {
msg = `${id}... ${tableState.selectedRowKeys.length}`;
id = tableState.selectedRowKeys.join(',');
}
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.dashboard.cdr.delTip', { msg }),
onOk() {
modalState.confirmLoading = true;
const hide = message.loading(t('common.loading'), 0);
delIMSDataCDR(id)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 3,
});
fnGetList(1);
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
hide();
modalState.confirmLoading = false;
});
},
});
}
/**查询列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (tableState.loading) return;
tableState.loading = true;
if (pageNum) {
queryParams.pageNum = pageNum;
}
if (!queryRangePicker.value) {
queryRangePicker.value = ['', ''];
}
queryParams.startTime = queryRangePicker.value[0];
queryParams.endTime = queryRangePicker.value[1];
listIMSDataCDR(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.rows)) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
tablePagination.total = res.total;
// 遍历处理cdr字符串数据
tableState.data = res.rows.map(item => {
let cdrJSON = item.cdrJSON;
if (!cdrJSON) {
Reflect.set(item, 'cdrJSON', {});
}
try {
cdrJSON = JSON.parse(cdrJSON);
Reflect.set(item, 'cdrJSON', cdrJSON);
} catch (error) {
console.error(error);
Reflect.set(item, 'cdrJSON', {});
}
return item;
});
// 取最大值ID用作实时累加
if (res.total > 0) {
modalState.maxId = Number(res.rows[0].id);
}
}
tableState.loading = false;
});
}
/**列表导出 */
function fnExportList() {
if (modalState.confirmLoading) return;
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.dashboard.cdr.exportTip'),
onOk() {
const hide = message.loading(t('common.loading'), 0);
const querys = toRaw(queryParams);
querys.pageSize = 10000;
exportIMSDataCDR(querys)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 3,
});
saveAs(res.data, `ims_cdr_event_export_${Date.now()}.xlsx`);
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
hide();
modalState.confirmLoading = false;
});
},
});
}
/**实时数据开关 */
const realTimeData = ref<boolean>(false);
/**
* 实时数据
*/
function fnRealTime() {
realTimeData.value = !realTimeData.value;
if (realTimeData.value) {
tableState.seached = false;
// 建立链接
const options: OptionsType = {
url: '/ws',
params: {
/**订阅通道组
*
* IMS_CDR会话事件(GroupID:1005)
*/
subGroupID: `1005_${queryParams.neId}`,
},
onmessage: wsMessage,
onerror: wsError,
};
ws.connect(options);
} else {
ws.close();
tableState.seached = true;
fnGetList(1);
}
}
/**接收数据后回调 */
function wsError(ev: any) {
// 接收数据后回调
console.error(ev);
}
/**接收数据后回调 */
function wsMessage(res: Record<string, any>) {
const { code, requestId, data } = res;
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
// 订阅组信息
if (!data?.groupId) {
return;
}
// cdrEvent CDR会话事件
if (data.groupId === `1005_${queryParams.neId}`) {
const cdrEvent = data.data;
queue.add(async () => {
modalState.maxId += 1;
tableState.data.unshift({
id: modalState.maxId,
neType: cdrEvent.neType,
neName: cdrEvent.neName,
rmUID: cdrEvent.rmUID,
timestamp: cdrEvent.timestamp,
cdrJSON: cdrEvent.CDR,
});
tablePagination.total += 1;
if (tableState.data.length > 100) {
tableState.data.pop();
}
await new Promise(resolve => setTimeout(resolve, 800));
});
}
}
onMounted(() => {
// 初始字典数据
Promise.allSettled([getDict('cdr_sip_code'), getDict('cdr_call_type')]).then(
resArr => {
if (resArr[0].status === 'fulfilled') {
dict.cdrSipCode = resArr[0].value;
}
if (resArr[1].status === 'fulfilled') {
dict.cdrCallType = resArr[1].value;
}
}
);
// 获取网元网元列表
useNeInfoStore()
.fnNelist()
.then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
if (res.data.length > 0) {
let arr: Record<string, any>[] = [];
res.data.forEach(i => {
if (i.neType === 'IMS') {
arr.push({ value: i.neId, label: i.neName });
}
});
neOtions.value = arr;
if (arr.length > 0) {
queryParams.neId = arr[0].value;
}
}
} else {
message.warning({
content: t('common.noData'),
duration: 2,
});
}
})
.finally(() => {
// 获取列表数据
fnGetList();
});
});
onBeforeUnmount(() => {
if (ws.state() !== -1) {
ws.close();
}
});
</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="IMS" name="neId ">
<a-select
v-model:value="queryParams.neId"
:options="neOtions"
:placeholder="t('common.selectPlease')"
/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item
:label="t('views.dashboard.cdr.called')"
name="calledParty"
>
<a-input
v-model:value="queryParams.calledParty"
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.dashboard.cdr.caller')"
name="callerParty "
>
<a-input
v-model:value="queryParams.callerParty"
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="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.dashboard.cdr.recordType')"
name="recordType"
>
<a-select
v-model:value="recordTypes"
mode="multiple"
:options="['MOC', 'MTC'].map(v => ({ value: v }))"
:placeholder="t('common.selectPlease')"
@change="fnQueryRecordTypeChange"
></a-select>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :xs="24">
<a-form-item
:label="t('views.dashboard.cdr.time')"
name="queryRangePicker"
>
<a-range-picker
v-model:value="queryRangePicker"
allow-clear
bordered
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
value-format="x"
style="width: 100%"
></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-popconfirm
placement="bottomLeft"
:title="
!realTimeData
? t('views.dashboard.cdr.realTimeDataStart')
: t('views.dashboard.cdr.realTimeDataStop')
"
ok-text="Yes"
cancel-text="No"
@confirm="fnRealTime()"
>
<a-button type="primary" :danger="realTimeData">
<template #icon><FundOutlined /> </template>
{{
!realTimeData
? t('views.dashboard.cdr.realTimeDataStart')
: t('views.dashboard.cdr.realTimeDataStop')
}}
</a-button>
</a-popconfirm>
<a-button
type="default"
danger
:disabled="tableState.selectedRowKeys.length <= 0"
:loading="modalState.confirmLoading"
@click.prevent="fnRecordDelete('0')"
v-if="!hasRoles(['student'])"
>
<template #icon><DeleteOutlined /></template>
{{ t('common.deleteText') }}
</a-button>
<a-button type="dashed" @click.prevent="fnExportList()">
<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"
:disabled="realTimeData"
/>
</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="hasRoles(['student']) ? tableColumns.filter((s:any)=>s.key !== 'id'): tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:pagination="tablePagination"
:scroll="{ x: tableColumns.length * 150, y: 'calc(100vh - 480px)' }"
:row-selection="{
type: 'checkbox',
columnWidth: '48px',
selectedRowKeys: tableState.selectedRowKeys,
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'callType'">
<DictTag
:options="dict.cdrCallType"
:value="record.cdrJSON.callType"
/>
</template>
<template v-if="column.key === 'cause'">
<span v-if="record.cdrJSON.callType !== 'sms'">
<DictTag
:options="dict.cdrSipCode"
:value="record.cdrJSON.cause"
value-default="0"
/>
</span>
<span v-else>
{{ t('views.dashboard.cdr.resultOk') }}
</span>
</template>
<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.id)"
>
<template #icon>
<DeleteOutlined />
</template>
</a-button>
</a-tooltip>
</a-space>
</template>
</template>
<template #expandedRowRender="{ record }">
<a-row :gutter="16">
<a-col :lg="5" :md="12" :xs="24">
<a-divider orientation="left">
{{ t('views.dashboard.cdr.cdrInfo') }}
</a-divider>
<div>
<span>{{ t('views.ne.common.neName') }}: </span>
<span>{{ record.neName }}</span>
</div>
<div>
<span>{{ t('views.ne.common.rmUid') }}: </span>
<span>{{ record.rmUID }}</span>
</div>
<div>
<span>{{ t('views.dashboard.cdr.time') }}: </span>
<span>
{{
typeof record.cdrJSON.releaseTime === 'number'
? parseDateToStr(+record.cdrJSON.releaseTime * 1000)
: record.cdrJSON.releaseTime
}}
</span>
</div>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-divider orientation="left">
{{ t('views.dashboard.cdr.rowInfo') }}
</a-divider>
<div>
<span>{{ t('views.dashboard.cdr.type') }}: </span>
<DictTag
:options="dict.cdrCallType"
:value="record.cdrJSON.callType"
/>
</div>
<div>
<span>{{ t('views.dashboard.cdr.duration') }}: </span>
<span v-if="record.cdrJSON.callType !== 'sms'">
{{ parseDuration(record.cdrJSON.callDuration) }}
</span>
<span v-else> - </span>
</div>
<div>
<span>{{ t('views.dashboard.cdr.caller') }}: </span>
<span>{{ record.cdrJSON.callerParty }}</span>
</div>
<div>
<span>{{ t('views.dashboard.cdr.called') }}: </span>
<span>{{ record.cdrJSON.calledParty }}</span>
</div>
<div>
<span>{{ t('views.dashboard.cdr.result') }}: </span>
<span v-if="record.cdrJSON.callType !== 'sms'">
<DictTag
:options="dict.cdrSipCode"
:value="record.cdrJSON.cause"
value-default="0"
/>
</span>
<span v-else>
{{ t('views.dashboard.cdr.resultOk') }}
</span>
</div>
<div>
<span>{{ t('views.dashboard.cdr.seizureTime') }}: </span>
<span>
{{
typeof record.cdrJSON.seizureTime === 'number'
? parseDateToStr(+record.cdrJSON.seizureTime * 1000)
: record.cdrJSON.seizureTime
}}
</span>
</div>
<div>
<span>{{ t('views.dashboard.cdr.releaseTime') }}: </span>
<span>
{{
typeof record.cdrJSON.releaseTime === 'number'
? parseDateToStr(+record.cdrJSON.releaseTime * 1000)
: record.cdrJSON.releaseTime
}}
</span>
</div>
</a-col>
</a-row>
</template>
</a-table>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,742 @@
<script setup lang="ts">
import { reactive, onMounted, toRaw, ref, onBeforeUnmount } 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_ERROR,
RESULT_CODE_SUCCESS,
} from '@/constants/result-constants';
import useDictStore from '@/store/modules/dict';
import useNeInfoStore from '@/store/modules/neinfo';
import { listMMEDataUE, delMMEDataUE, exportMMEDataUE } from '@/api/neData/mme';
import { parseDateToStr } from '@/utils/date-utils';
import { OptionsType, WS } from '@/plugins/ws-websocket';
import saveAs from 'file-saver';
import PQueue from 'p-queue';
import { hasRoles } from '@/plugins/auth-user';
const { t } = useI18n();
const { getDict } = useDictStore();
const ws = new WS();
const queue = new PQueue({ concurrency: 1, autoStart: true });
/**网元可选 */
let neOtions = ref<Record<string, any>[]>([]);
/**字典数据 */
let dict: {
/**UE 事件认证代码类型 */
ueAauthCode: DictType[];
/**UE 事件类型 */
ueEventType: DictType[];
/**UE 事件CM状态 */
ueEventCmState: DictType[];
} = reactive({
ueAauthCode: [],
ueEventType: [],
ueEventCmState: [],
});
/**开始结束时间 */
let queryRangePicker = ref<[string, string]>(['', '']);
/**查询参数 */
let queryParams = reactive({
/**网元类型 */
neType: 'MME',
neId: '001',
eventType: '',
imsi: '',
sortField: 'timestamp',
sortOrder: 'desc',
/**开始时间 */
startTime: '',
/**结束时间 */
endTime: '',
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
eventTypes.value = [];
queryParams = Object.assign(queryParams, {
eventType: '',
imsi: '',
startTime: '',
endTime: '',
pageNum: 1,
pageSize: 20,
});
queryRangePicker.value = ['', ''];
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**记录类型 */
const eventTypes = ref<string[]>([]);
/**查询记录类型变更 */
function fnQueryEventTypeChange(value: any) {
if (Array.isArray(value)) {
queryParams.eventType = value.join(',');
}
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
seached: true,
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: t('common.rowId'),
dataIndex: 'id',
align: 'left',
width: 100,
},
{
title: 'IMSI',
dataIndex: 'eventJSON',
align: 'left',
width: 150,
customRender(opt) {
const eventJSON = opt.value;
return eventJSON.imsi;
},
},
{
title: t('views.dashboard.ue.eventType'),
dataIndex: 'eventType',
key: 'eventType',
align: 'left',
width: 150,
},
{
title: t('views.dashboard.ue.result'),
dataIndex: 'eventJSON',
key: 'result',
align: 'left',
width: 150,
},
{
title: t('views.dashboard.ue.time'),
dataIndex: 'eventJSON',
align: 'left',
width: 150,
customRender(opt) {
const cdrJSON = opt.value;
return parseDateToStr(+cdrJSON.timestamp * 1000);
},
},
{
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 = {
/**确定按钮 loading */
confirmLoading: boolean;
/**最大ID值 */
maxId: number;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
confirmLoading: false,
maxId: 0,
});
/**
* 记录删除
* @param id 编号
*/
function fnRecordDelete(id: string) {
if (!id || modalState.confirmLoading) return;
let msg = id;
if (id === '0') {
msg = `${id}... ${tableState.selectedRowKeys.length}`;
id = tableState.selectedRowKeys.join(',');
}
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.dashboard.ue.delTip', { msg }),
onOk() {
modalState.confirmLoading = true;
const hide = message.loading(t('common.loading'), 0);
delMMEDataUE(id)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 3,
});
fnGetList(1);
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
hide();
modalState.confirmLoading = false;
});
},
});
}
/**查询列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (tableState.loading) return;
tableState.loading = true;
if (pageNum) {
queryParams.pageNum = pageNum;
}
if (!queryRangePicker.value) {
queryRangePicker.value = ['', ''];
}
queryParams.startTime = queryRangePicker.value[0];
queryParams.endTime = queryRangePicker.value[1];
listMMEDataUE(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.rows)) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
tablePagination.total = res.total;
// 遍历处理cdr字符串数据
tableState.data = res.rows.map(item => {
let eventJSON = item.eventJSON;
if (!eventJSON) {
Reflect.set(item, 'eventJSON', {});
}
try {
eventJSON = JSON.parse(eventJSON);
Reflect.set(item, 'eventJSON', eventJSON);
} catch (error) {
console.error(error);
Reflect.set(item, 'eventJSON', {});
}
return item;
});
// 取最大值ID用作实时累加
if (res.total > 0) {
modalState.maxId = Number(res.rows[0].id);
}
}
tableState.loading = false;
});
}
/**列表导出 */
function fnExportList() {
if (modalState.confirmLoading) return;
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.dashboard.ue.exportTip'),
onOk() {
const hide = message.loading(t('common.loading'), 0);
const querys = toRaw(queryParams);
querys.pageSize = 10000;
exportMMEDataUE(querys)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 3,
});
saveAs(res.data, `mme_ue_event_export_${Date.now()}.xlsx`);
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
hide();
modalState.confirmLoading = false;
});
},
});
}
/**实时数据开关 */
const realTimeData = ref<boolean>(false);
/**
* 实时数据
*/
function fnRealTime() {
realTimeData.value = !realTimeData.value;
if (realTimeData.value) {
tableState.seached = false;
// 建立链接
const options: OptionsType = {
url: '/ws',
params: {
/**订阅通道组
*
* MME_UE会话事件(GroupID:1011)
*/
subGroupID: `1011_${queryParams.neId}`,
},
onmessage: wsMessage,
onerror: wsError,
};
ws.connect(options);
} else {
ws.close();
tableState.seached = true;
fnGetList(1);
}
}
/**接收数据后回调 */
function wsError(ev: any) {
// 接收数据后回调
console.error(ev);
}
/**接收数据后回调 */
function wsMessage(res: Record<string, any>) {
const { code, requestId, data } = res;
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
// 订阅组信息
if (!data?.groupId) {
return;
}
// ueEvent MME_UE会话事件
if (data.groupId === `1011_${queryParams.neId}`) {
const ueEvent = data.data;
queue.add(async () => {
modalState.maxId += 1;
tableState.data.unshift({
id: modalState.maxId,
neType: ueEvent.neType,
neName: ueEvent.neName, // 空
rmUID: ueEvent.rmUID, // 空
timestamp: ueEvent.timestamp,
eventType: ueEvent.eventType,
eventJSON: ueEvent.eventJSON,
});
tablePagination.total += 1;
if (tableState.data.length > 100) {
tableState.data.pop();
}
await new Promise(resolve => setTimeout(resolve, 800));
});
}
}
onMounted(() => {
// 初始字典数据
Promise.allSettled([
getDict('ue_auth_code'),
getDict('ue_event_type'),
getDict('ue_event_cm_state'),
]).then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.ueAauthCode = resArr[0].value;
}
if (resArr[1].status === 'fulfilled') {
const ueEventType: any[] = JSON.parse(JSON.stringify(resArr[1]));
dict.ueEventType = ueEventType.map(item => {
if (item.value === 'cm-state') {
item.label = item.label.replace('CM', 'ECM');
}
return item;
});
}
if (resArr[2].status === 'fulfilled') {
dict.ueEventCmState = resArr[2].value;
}
});
// 获取网元网元列表
useNeInfoStore()
.fnNelist()
.then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
if (res.data.length > 0) {
let arr: Record<string, any>[] = [];
res.data.forEach(i => {
if (i.neType === 'MME') {
arr.push({ value: i.neId, label: i.neName });
}
});
neOtions.value = arr;
if (arr.length > 0) {
queryParams.neId = arr[0].value;
}
}
} else {
message.warning({
content: t('common.noData'),
duration: 2,
});
}
})
.finally(() => {
// 获取列表数据
fnGetList();
});
});
onBeforeUnmount(() => {
if (ws.state() !== -1) {
ws.close();
}
});
</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="MME" name="neId ">
<a-select
v-model:value="queryParams.neId"
:options="neOtions"
:placeholder="t('common.selectPlease')"
/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item
:label="t('views.dashboard.ue.eventType')"
name="eventType "
>
<a-select
v-model:value="eventTypes"
mode="multiple"
:options="dict.ueEventType"
:placeholder="t('common.selectPlease')"
@change="fnQueryEventTypeChange"
></a-select>
</a-form-item>
</a-col>
<a-col :lg="4" :md="12" :xs="24">
<a-form-item label="IMSI" name="imsi ">
<a-input
v-model:value="queryParams.imsi"
allow-clear
: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.dashboard.cdr.time')"
name="queryRangePicker"
>
<a-range-picker
v-model:value="queryRangePicker"
allow-clear
bordered
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
value-format="x"
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>
<a-space :size="8" align="center">
<a-popconfirm
placement="bottomLeft"
:title="
!realTimeData
? t('views.dashboard.ue.realTimeDataStart')
: t('views.dashboard.ue.realTimeDataStop')
"
ok-text="Yes"
cancel-text="No"
@confirm="fnRealTime()"
>
<a-button type="primary" :danger="realTimeData">
<template #icon><FundOutlined /> </template>
{{
!realTimeData
? t('views.dashboard.ue.realTimeDataStart')
: t('views.dashboard.ue.realTimeDataStop')
}}
</a-button>
</a-popconfirm>
<a-button
type="default"
danger
:disabled="tableState.selectedRowKeys.length <= 0"
:loading="modalState.confirmLoading"
@click.prevent="fnRecordDelete('0')"
v-if="!hasRoles(['student'])"
>
<template #icon><DeleteOutlined /></template>
{{ t('common.deleteText') }}
</a-button>
<a-button type="dashed" @click.prevent="fnExportList()">
<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"
:disabled="realTimeData"
/>
</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="hasRoles(['student']) ? tableColumns.filter((s:any)=>s.key !== 'id'): tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:pagination="tablePagination"
:scroll="{ x: tableColumns.length * 120, y: 'calc(100vh - 480px)' }"
:row-selection="{
type: 'checkbox',
columnWidth: '48px',
selectedRowKeys: tableState.selectedRowKeys,
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'eventType'">
<DictTag :options="dict.ueEventType" :value="record.eventType" />
</template>
<template v-if="column.key === 'result'">
<span v-if="record.eventType === 'auth-result'">
<DictTag
:options="dict.ueAauthCode"
:value="record.eventJSON.result"
/>
</span>
<span v-if="record.eventType === 'detach'">
<span>{{ t('views.dashboard.ue.resultOk') }}</span>
</span>
<span v-if="record.eventType === 'cm-state'">
<DictTag
:options="dict.ueEventCmState"
:value="record.eventJSON.result"
/>
</span>
</template>
<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.id)"
>
<template #icon>
<DeleteOutlined />
</template>
</a-button>
</a-tooltip>
</a-space>
</template>
</template>
<template #expandedRowRender="{ record }">
<div style="width: 46%; padding-left: 32px; padding-bottom: 16px">
<a-divider orientation="left">
{{ t('views.dashboard.ue.ueInfo') }}
</a-divider>
<div>
<span>{{ t('views.ne.common.neName') }}: </span>
<span>{{ record.neName }}</span>
</div>
<div>
<span>{{ t('views.ne.common.rmUid') }}: </span>
<span>{{ record.rmUID }}</span>
</div>
<a-divider orientation="left">
{{ t('views.dashboard.ue.rowInfo') }}
</a-divider>
<div>
<span>{{ t('views.dashboard.ue.time') }}: </span>
{{ parseDateToStr(record.eventJSON.timestamp * 1000) }}
</div>
<div>
<span>{{ t('views.dashboard.ue.eventType') }}: </span>
<DictTag :options="dict.ueEventType" :value="record.eventType" />
</div>
<div>
<span>{{ t('views.dashboard.ue.result') }}: </span>
<span v-if="record.eventType === 'auth-result'">
<DictTag
:options="dict.ueAauthCode"
:value="record.eventJSON.result"
/>
</span>
<span v-if="record.eventType === 'detach'">
{{ t('views.dashboard.ue.resultOk') }}
</span>
<span v-if="record.eventType === 'cm-state'">
<DictTag
:options="dict.ueEventCmState"
:value="record.eventJSON.result"
/>
</span>
</div>
</div>
</template>
</a-table>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,838 @@
<script setup lang="ts">
import { reactive, onMounted, toRaw, onBeforeUnmount, ref } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { Modal, message } from 'ant-design-vue/es';
import { SizeType } from 'ant-design-vue/es/config-provider';
import { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/es/table';
import useNeInfoStore from '@/store/modules/neinfo';
import useI18n from '@/hooks/useI18n';
import {
RESULT_CODE_ERROR,
RESULT_CODE_SUCCESS,
} from '@/constants/result-constants';
import {
delSMFDataCDR,
exportSMFDataCDR,
listSMFDataCDR,
} from '@/api/neData/smf';
import { OptionsType, WS } from '@/plugins/ws-websocket';
import PQueue from 'p-queue';
import saveAs from 'file-saver';
import { hasRoles } from '@/plugins/auth-user';
const { t } = useI18n();
const ws = new WS();
const queue = new PQueue({ concurrency: 1, autoStart: true });
/**网元可选 */
let neOtions = ref<Record<string, any>[]>([]);
/**开始结束时间 */
let queryRangePicker = ref<[string, string]>(['', '']);
/**查询参数 */
let queryParams = reactive({
/**网元类型 */
neType: 'SMF',
neId: '001',
subscriberID: '',
sortField: 'timestamp',
sortOrder: 'desc',
/**开始时间 */
startTime: '',
/**结束时间 */
endTime: '',
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
subscriberID: '',
startTime: '',
endTime: '',
pageNum: 1,
pageSize: 20,
});
queryRangePicker.value = ['', ''];
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
seached: true,
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: t('common.rowId'),
dataIndex: 'id',
align: 'center',
width: 100,
},
{
title: t('views.dashboard.cdr.smfChargingID'), // 计费ID
dataIndex: 'cdrJSON',
align: 'left',
width: 100,
customRender(opt) {
const cdrJSON = opt.value;
return cdrJSON.chargingID;
},
},
{
title: t('views.dashboard.cdr.smfSubscriptionIDType'), // 订阅 ID 类型
dataIndex: 'cdrJSON',
align: 'left',
width: 150,
customRender(opt) {
const cdrJSON = opt.value;
return cdrJSON.subscriberIdentifier?.subscriptionIDType;
},
},
{
title: t('views.dashboard.cdr.smfSubscriptionIDData'), // 订阅 ID 数据
dataIndex: 'cdrJSON',
align: 'left',
width: 150,
customRender(opt) {
const cdrJSON = opt.value;
return cdrJSON.subscriberIdentifier?.subscriptionIDData;
},
},
{
title: t('views.dashboard.cdr.smfDataVolumeUplink'), // 数据量上行链路
dataIndex: 'cdrJSON',
align: 'left',
width: 150,
customRender(opt) {
const cdrJSON = opt.value;
const listOfMultipleUnitUsage = cdrJSON.listOfMultipleUnitUsage;
if (
!Array.isArray(listOfMultipleUnitUsage) ||
listOfMultipleUnitUsage.length < 1
) {
return 0;
}
const usedUnitContainer = listOfMultipleUnitUsage[0].usedUnitContainer;
if (!Array.isArray(usedUnitContainer) || usedUnitContainer.length < 1) {
return 0;
}
return usedUnitContainer[0].dataVolumeUplink;
},
},
{
title: t('views.dashboard.cdr.smfDataVolumeDownlink'), // 数据量下行链路
dataIndex: 'cdrJSON',
align: 'left',
width: 180,
customRender(opt) {
const cdrJSON = opt.value;
const listOfMultipleUnitUsage = cdrJSON.listOfMultipleUnitUsage;
if (
!Array.isArray(listOfMultipleUnitUsage) ||
listOfMultipleUnitUsage.length < 1
) {
return 0;
}
const usedUnitContainer = listOfMultipleUnitUsage[0].usedUnitContainer;
if (!Array.isArray(usedUnitContainer) || usedUnitContainer.length < 1) {
return 0;
}
return usedUnitContainer[0].dataVolumeDownlink;
},
},
{
title: t('views.dashboard.cdr.smfDataTotalVolume'), // 数据总量
dataIndex: 'cdrJSON',
align: 'left',
width: 150,
customRender(opt) {
const cdrJSON = opt.value;
const listOfMultipleUnitUsage = cdrJSON.listOfMultipleUnitUsage;
if (
!Array.isArray(listOfMultipleUnitUsage) ||
listOfMultipleUnitUsage.length < 1
) {
return 0;
}
const usedUnitContainer = listOfMultipleUnitUsage[0].usedUnitContainer;
if (!Array.isArray(usedUnitContainer) || usedUnitContainer.length < 1) {
return 0;
}
return usedUnitContainer[0].dataTotalVolume;
},
},
{
title: t('views.dashboard.cdr.smfDuration'), // 持续时间
dataIndex: 'cdrJSON',
align: 'left',
width: 100,
customRender(opt) {
const cdrJSON = opt.value;
return cdrJSON.duration;
},
},
{
title: t('views.dashboard.cdr.smfInvocationTime'), // 调用时间
dataIndex: 'cdrJSON',
align: 'left',
width: 200,
customRender(opt) {
const cdrJSON = opt.value;
return cdrJSON.invocationTimestamp;
},
},
{
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 = {
/**确定按钮 loading */
confirmLoading: boolean;
/**最大ID值 */
maxId: number;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
confirmLoading: false,
maxId: 0,
});
/**
* 记录删除
* @param id 编号
*/
function fnRecordDelete(id: string) {
if (!id || modalState.confirmLoading) return;
let msg = id;
if (id === '0') {
msg = `${id}... ${tableState.selectedRowKeys.length}`;
id = tableState.selectedRowKeys.join(',');
}
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.dashboard.cdr.delTip', { msg }),
onOk() {
modalState.confirmLoading = true;
const hide = message.loading(t('common.loading'), 0);
delSMFDataCDR(id)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 3,
});
fnGetList(1);
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
hide();
modalState.confirmLoading = false;
});
},
});
}
/**查询列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (tableState.loading) return;
tableState.loading = true;
if (pageNum) {
queryParams.pageNum = pageNum;
}
if (!queryRangePicker.value) {
queryRangePicker.value = ['', ''];
}
queryParams.startTime = queryRangePicker.value[0];
queryParams.endTime = queryRangePicker.value[1];
listSMFDataCDR(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.rows)) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
tablePagination.total = res.total;
// 遍历处理cdr字符串数据
tableState.data = res.rows.map(item => {
let cdrJSON = item.cdrJSON;
if (!cdrJSON) {
Reflect.set(item, 'cdrJSON', {});
}
try {
cdrJSON = JSON.parse(cdrJSON);
Reflect.set(item, 'cdrJSON', cdrJSON);
} catch (error) {
console.error(error);
Reflect.set(item, 'cdrJSON', {});
}
return item;
});
// 取最大值ID用作实时累加
if (res.total > 0) {
modalState.maxId = Number(res.rows[0].id);
}
}
tableState.loading = false;
});
}
/**列表导出 */
function fnExportList() {
if (modalState.confirmLoading) return;
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.dashboard.cdr.exportTip'),
onOk() {
const hide = message.loading(t('common.loading'), 0);
const querys = toRaw(queryParams);
querys.pageSize = 10000;
exportSMFDataCDR(querys)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 3,
});
saveAs(res.data, `smf_cdr_event_export_${Date.now()}.xlsx`);
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
hide();
modalState.confirmLoading = false;
});
},
});
}
/**实时数据开关 */
const realTimeData = ref<boolean>(false);
/**
* 实时数据
*/
function fnRealTime() {
realTimeData.value = !realTimeData.value;
if (realTimeData.value) {
tableState.seached = false;
// 建立链接
const options: OptionsType = {
url: '/ws',
params: {
/**订阅通道组
*
* CDR会话事件-SMF (GroupID:1006)
*/
subGroupID: `1006_${queryParams.neId}`,
},
onmessage: wsMessage,
onerror: wsError,
};
ws.connect(options);
} else {
ws.close();
tableState.seached = true;
fnGetList(1);
}
}
/**接收数据后回调 */
function wsError(ev: any) {
// 接收数据后回调
console.error(ev);
}
/**接收数据后回调 */
function wsMessage(res: Record<string, any>) {
const { code, requestId, data } = res;
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
// 订阅组信息
if (!data?.groupId) {
return;
}
// cdrEvent CDR会话事件
if (data.groupId === `1006_${queryParams.neId}`) {
const cdrEvent = data.data;
queue.add(async () => {
modalState.maxId += 1;
tableState.data.unshift({
id: modalState.maxId,
neType: cdrEvent.neType,
neName: cdrEvent.neName,
rmUID: cdrEvent.rmUID,
timestamp: cdrEvent.timestamp,
cdrJSON: cdrEvent.CDR,
});
tablePagination.total += 1;
if (tableState.data.length > 100) {
tableState.data.pop();
}
await new Promise(resolve => setTimeout(resolve, 800));
});
}
}
onMounted(() => {
// 获取网元网元列表
useNeInfoStore()
.fnNelist()
.then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
if (res.data.length > 0) {
let arr: Record<string, any>[] = [];
res.data.forEach(i => {
if (i.neType === 'SMF') {
arr.push({ value: i.neId, label: i.neName });
}
});
neOtions.value = arr;
if (arr.length > 0) {
queryParams.neId = arr[0].value;
}
}
} else {
message.warning({
content: t('common.noData'),
duration: 2,
});
}
})
.finally(() => {
// 获取列表数据
fnGetList();
});
});
onBeforeUnmount(() => {
if (ws.state() !== -1) {
ws.close();
}
});
</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="SMF" name="neId ">
<a-select
v-model:value="queryParams.neId"
:options="neOtions"
:placeholder="t('common.selectPlease')"
/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item
:label="t('views.dashboard.cdr.smfSubscriptionIDData')"
name="subscriberID"
>
<a-input
v-model:value="queryParams.subscriberID"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="40"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :xs="24">
<a-form-item
:label="t('views.dashboard.cdr.time')"
name="queryRangePicker"
>
<a-range-picker
v-model:value="queryRangePicker"
allow-clear
bordered
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
value-format="x"
style="width: 100%"
></a-range-picker>
</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="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-popconfirm
placement="bottomLeft"
:title="
!realTimeData
? t('views.dashboard.cdr.realTimeDataStart')
: t('views.dashboard.cdr.realTimeDataStop')
"
ok-text="Yes"
cancel-text="No"
@confirm="fnRealTime()"
>
<a-button type="primary" :danger="realTimeData">
<template #icon><FundOutlined /> </template>
{{
!realTimeData
? t('views.dashboard.cdr.realTimeDataStart')
: t('views.dashboard.cdr.realTimeDataStop')
}}
</a-button>
</a-popconfirm>
<a-button
type="default"
danger
:disabled="tableState.selectedRowKeys.length <= 0"
:loading="modalState.confirmLoading"
@click.prevent="fnRecordDelete('0')"
v-if="!hasRoles(['student'])"
>
<template #icon><DeleteOutlined /></template>
{{ t('common.deleteText') }}
</a-button>
<a-button type="dashed" @click.prevent="fnExportList()">
<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"
:disabled="realTimeData"
/>
</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="hasRoles(['student']) ? tableColumns.filter((s:any)=>s.key !== 'id'): tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:pagination="tablePagination"
:scroll="{ x: tableColumns.length * 150, y: 'calc(100vh - 480px)' }"
:row-selection="{
type: 'checkbox',
columnWidth: '48px',
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.id)"
>
<template #icon>
<DeleteOutlined />
</template>
</a-button>
</a-tooltip>
</a-space>
</template>
</template>
<template #expandedRowRender="{ record }">
<a-row :gutter="16">
<a-col :lg="8" :md="12" :xs="24" :offset="2">
<a-divider orientation="left">
{{ t('views.dashboard.cdr.cdrInfo') }}
</a-divider>
<div>
<span>{{ t('views.ne.common.neName') }}: </span>
<span>{{ record.neName }}</span>
</div>
<div>
<span>{{ t('views.ne.common.rmUid') }}: </span>
<span>{{ record.rmUID }}</span>
</div>
<div>
<span>{{ t('views.dashboard.cdr.time') }}: </span>
<span>{{ record.cdrJSON.invocationTimestamp }}</span>
</div>
<a-divider orientation="left">
{{ t('views.dashboard.cdr.rowInfo') }}
</a-divider>
<div>
<span>Record Network Function ID: </span>
<span>{{ record.cdrJSON.recordingNetworkFunctionID }}</span>
</div>
<div>
<span>Record Type: </span>
<span>{{ record.cdrJSON.recordType }}</span>
</div>
<div>
<span>Record Opening Time: </span>
<span>{{ record.cdrJSON.recordOpeningTime }}</span>
</div>
<div>
<span>Charging ID: </span>
<span>{{ record.cdrJSON.chargingID }}</span>
</div>
<div>
<span>Duration: </span>
<span>{{ record.cdrJSON.duration }}</span>
</div>
<a-divider orientation="left"> Subscriber Identifier </a-divider>
<div>
<span>Subscription ID Type: </span>
<span>
{{ record.cdrJSON.subscriberIdentifier?.subscriptionIDType }}
</span>
</div>
<div>
<span>Subscription ID Data: </span>
<span>
{{ record.cdrJSON.subscriberIdentifier?.subscriptionIDData }}
</span>
</div>
</a-col>
<a-col :lg="8" :md="12" :xs="24">
<a-divider orientation="left">
List Of Multiple Unit Usage
</a-divider>
<div v-for="u in record.cdrJSON.listOfMultipleUnitUsage">
<div>RatingGroup: {{ u.ratingGroup }}</div>
<div v-for="udata in u.usedUnitContainer">
<div>
<span>Data Total Volume: </span>
<span>{{ udata.dataTotalVolume }}</span>
</div>
<div>
<span>Data Volume Downlink: </span>
<span>{{ udata.dataVolumeDownlink }}</span>
</div>
<div>
<span>Data Volume Uplink: </span>
<span>{{ udata.dataVolumeUplink }}</span>
</div>
<div>
<span>Time: </span>
<span>{{ udata.time }}</span>
</div>
</div>
</div>
<a-divider orientation="left">
PDU Session Charging Information
</a-divider>
<div>
<span>User Identifier: </span>
<span>{{
record.cdrJSON.pDUSessionChargingInformation?.userIdentifier
}}</span>
</div>
<div>
<span>SSC Mode: </span>
<span>{{
record.cdrJSON.pDUSessionChargingInformation?.sSCMode
}}</span>
&nbsp;&nbsp;
<span>RAT Type: </span>
<span>{{
record.cdrJSON.pDUSessionChargingInformation?.rATType
}}</span>
&nbsp;&nbsp;
<span>DNN ID: </span>
<span>
{{ record.cdrJSON.pDUSessionChargingInformation?.dNNID }}
</span>
</div>
<div>
<span>PDU Type: </span>
<span>
{{ record.cdrJSON.pDUSessionChargingInformation?.pDUType }}
</span>
</div>
<div>
<span>PDU IPv4 Address: </span>
<span>
{{
record.cdrJSON.pDUSessionChargingInformation?.pDUAddress
?.pDUIPv4Address
}}
</span>
</div>
<div>
<span>PDU IPv6 Addres Swith Prefix: </span>
<span>
{{
record.cdrJSON.pDUSessionChargingInformation?.pDUAddress
?.pDUIPv6AddresswithPrefix
}}
</span>
</div>
<div>
<span>Network Function IPv4: </span>
<span>
{{
record.cdrJSON.nFunctionConsumerInformation
?.networkFunctionIPv4Address
}}
</span>
</div>
</a-col>
</a-row>
</template>
</a-table>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,769 @@
<script setup lang="ts">
import { reactive, onMounted, toRaw, ref, onBeforeUnmount } 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 useDictStore from '@/store/modules/dict';
import useI18n from '@/hooks/useI18n';
import {
RESULT_CODE_ERROR,
RESULT_CODE_SUCCESS,
} from '@/constants/result-constants';
import useNeInfoStore from '@/store/modules/neinfo';
import {
delSMSCDataCDR,
exportSMSCDataCDR,
listSMSCDataCDR,
} from '@/api/neData/smsc';
import { parseDateToStr } from '@/utils/date-utils';
import { OptionsType, WS } from '@/plugins/ws-websocket';
import saveAs from 'file-saver';
import PQueue from 'p-queue';
const { getDict } = useDictStore();
const { t } = useI18n();
const ws = new WS();
const queue = new PQueue({ concurrency: 1, autoStart: true });
/**字典数据 */
let dict: {
/**CDR 响应原因代码类别类型 */
cdrCauseCode: DictType[];
} = reactive({
cdrCauseCode: [],
});
/**网元可选 */
let neOtions = ref<Record<string, any>[]>([]);
/**开始结束时间 */
let queryRangePicker = ref<[string, string]>(['', '']);
/**查询参数 */
let queryParams = reactive({
/**网元类型 */
neType: 'SMSC',
neId: '001',
recordType: '',
callerParty: '',
calledParty: '',
sortField: 'timestamp',
sortOrder: 'desc',
/**开始时间 */
startTime: '',
/**结束时间 */
endTime: '',
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
recordTypes.value = [];
queryParams = Object.assign(queryParams, {
recordType: '',
callerParty: '',
calledParty: '',
startTime: '',
endTime: '',
pageNum: 1,
pageSize: 20,
});
queryRangePicker.value = ['', ''];
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**记录类型 */
const recordTypes = ref<string[]>([]);
/**查询记录类型变更 */
function fnQueryRecordTypeChange(value: any) {
if (Array.isArray(value)) {
queryParams.recordType = value.join(',');
}
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
seached: true,
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: t('common.rowId'),
dataIndex: 'id',
align: 'left',
width: 100,
},
{
title: t('views.dashboard.cdr.recordType'),
dataIndex: 'cdrJSON',
align: 'left',
width: 150,
customRender(opt) {
const cdrJSON = opt.value;
return cdrJSON.recordType;
},
},
{
title: t('views.dashboard.cdr.type'),
dataIndex: 'cdrJSON',
align: 'left',
width: 150,
customRender(opt) {
const cdrJSON = opt.value;
return cdrJSON.serviceType;
},
},
{
title: t('views.dashboard.cdr.caller'),
dataIndex: 'cdrJSON',
key: 'callerParty',
align: 'left',
width: 120,
customRender(opt) {
const cdrJSON = opt.value;
return cdrJSON.callerParty;
},
},
{
title: t('views.dashboard.cdr.called'),
dataIndex: 'cdrJSON',
key: 'calledParty',
align: 'left',
width: 120,
customRender(opt) {
const cdrJSON = opt.value;
return cdrJSON.calledParty;
},
},
{
title: t('views.dashboard.cdr.result'),
dataIndex: 'cdrJSON',
key: 'cause',
align: 'left',
width: 200,
},
{
title: t('views.dashboard.cdr.time'),
dataIndex: 'cdrJSON',
align: 'left',
width: 150,
customRender(opt) {
const cdrJSON = opt.value;
if (typeof cdrJSON.updateTime === 'number') {
return parseDateToStr(+cdrJSON.updateTime * 1000);
}
return cdrJSON.updateTime;
},
},
{
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 = {
/**确定按钮 loading */
confirmLoading: boolean;
/**最大ID值 */
maxId: number;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
confirmLoading: false,
maxId: 0,
});
/**
* 记录删除
* @param id 编号
*/
function fnRecordDelete(id: string) {
if (!id || modalState.confirmLoading) return;
let msg = id;
if (id === '0') {
msg = `${id}... ${tableState.selectedRowKeys.length}`;
id = tableState.selectedRowKeys.join(',');
}
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.dashboard.cdr.delTip', { msg }),
onOk() {
modalState.confirmLoading = true;
const hide = message.loading(t('common.loading'), 0);
delSMSCDataCDR(id)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 3,
});
fnGetList(1);
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
hide();
modalState.confirmLoading = false;
});
},
});
}
/**查询列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (tableState.loading) return;
tableState.loading = true;
if (pageNum) {
queryParams.pageNum = pageNum;
}
if (!queryRangePicker.value) {
queryRangePicker.value = ['', ''];
}
queryParams.startTime = queryRangePicker.value[0];
queryParams.endTime = queryRangePicker.value[1];
listSMSCDataCDR(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.rows)) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
tablePagination.total = res.total;
// 遍历处理cdr字符串数据
tableState.data = res.rows.map(item => {
let cdrJSON = item.cdrJSON;
if (!cdrJSON) {
Reflect.set(item, 'cdrJSON', {});
}
try {
cdrJSON = JSON.parse(cdrJSON);
Reflect.set(item, 'cdrJSON', cdrJSON);
} catch (error) {
console.error(error);
Reflect.set(item, 'cdrJSON', {});
}
return item;
});
// 取最大值ID用作实时累加
if (res.total > 0) {
modalState.maxId = Number(res.rows[0].id);
}
}
tableState.loading = false;
});
}
/**列表导出 */
function fnExportList() {
if (modalState.confirmLoading) return;
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.dashboard.cdr.exportTip'),
onOk() {
const hide = message.loading(t('common.loading'), 0);
const querys = toRaw(queryParams);
querys.pageSize = 10000;
exportSMSCDataCDR(querys)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 3,
});
saveAs(res.data, `smsc_cdr_event_export_${Date.now()}.xlsx`);
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
hide();
modalState.confirmLoading = false;
});
},
});
}
/**实时数据开关 */
const realTimeData = ref<boolean>(false);
/**
* 实时数据
*/
function fnRealTime() {
realTimeData.value = !realTimeData.value;
if (realTimeData.value) {
tableState.seached = false;
// 建立链接
const options: OptionsType = {
url: '/ws',
params: {
/**订阅通道组
*
* SMSC_CDR会话事件(GroupID:1007_neId)
*/
subGroupID: `1007_${queryParams.neId}`,
},
onmessage: wsMessage,
onerror: wsError,
};
ws.connect(options);
} else {
ws.close();
tableState.seached = true;
fnGetList(1);
}
}
/**接收数据后回调 */
function wsError(ev: any) {
// 接收数据后回调
console.error(ev);
}
/**接收数据后回调 */
function wsMessage(res: Record<string, any>) {
const { code, requestId, data } = res;
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
// 订阅组信息
if (!data?.groupId) {
return;
}
// cdrEvent CDR会话事件
if (data.groupId === `1007_${queryParams.neId}`) {
const cdrEvent = data.data;
queue.add(async () => {
modalState.maxId += 1;
tableState.data.unshift({
id: modalState.maxId,
neType: cdrEvent.neType,
neName: cdrEvent.neName,
rmUID: cdrEvent.rmUID,
timestamp: cdrEvent.timestamp,
cdrJSON: cdrEvent.CDR,
});
tablePagination.total += 1;
if (tableState.data.length > 100) {
tableState.data.pop();
}
await new Promise(resolve => setTimeout(resolve, 800));
});
}
}
onMounted(() => {
// 初始字典数据
Promise.allSettled([getDict('cdr_cause_code')]).then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.cdrCauseCode = resArr[0].value;
}
});
// 获取网元网元列表
useNeInfoStore()
.fnNelist()
.then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
if (res.data.length > 0) {
let arr: Record<string, any>[] = [];
res.data.forEach(i => {
if (i.neType === 'SMSC') {
arr.push({ value: i.neId, label: i.neName });
}
});
neOtions.value = arr;
if (arr.length > 0) {
queryParams.neId = arr[0].value;
}
}
} else {
message.warning({
content: t('common.noData'),
duration: 2,
});
}
})
.finally(() => {
// 获取列表数据
fnGetList();
});
});
onBeforeUnmount(() => {
if (ws.state() !== -1) {
ws.close();
}
});
</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="SMSC" name="neId ">
<a-select
v-model:value="queryParams.neId"
:options="neOtions"
:placeholder="t('common.selectPlease')"
/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item
:label="t('views.dashboard.cdr.called')"
name="calledParty"
>
<a-input
v-model:value="queryParams.calledParty"
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.dashboard.cdr.caller')"
name="callerParty "
>
<a-input
v-model:value="queryParams.callerParty"
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="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.dashboard.cdr.recordType')"
name="recordType"
>
<a-select
v-model:value="recordTypes"
mode="multiple"
:options="['MOSM', 'MTSM'].map(v => ({ value: v }))"
:placeholder="t('common.selectPlease')"
@change="fnQueryRecordTypeChange"
></a-select>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :xs="24">
<a-form-item
:label="t('views.dashboard.cdr.time')"
name="queryRangePicker"
>
<a-range-picker
v-model:value="queryRangePicker"
allow-clear
bordered
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
value-format="x"
style="width: 100%"
></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-popconfirm
placement="bottomLeft"
:title="
!realTimeData
? t('views.dashboard.cdr.realTimeDataStart')
: t('views.dashboard.cdr.realTimeDataStop')
"
ok-text="Yes"
cancel-text="No"
@confirm="fnRealTime()"
>
<a-button type="primary" :danger="realTimeData">
<template #icon><FundOutlined /> </template>
{{
!realTimeData
? t('views.dashboard.cdr.realTimeDataStart')
: t('views.dashboard.cdr.realTimeDataStop')
}}
</a-button>
</a-popconfirm>
<a-button
type="default"
danger
:disabled="tableState.selectedRowKeys.length <= 0"
:loading="modalState.confirmLoading"
@click.prevent="fnRecordDelete('0')"
>
<template #icon><DeleteOutlined /></template>
{{ t('common.deleteText') }}
</a-button>
<a-button type="dashed" @click.prevent="fnExportList()">
<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"
:disabled="realTimeData"
/>
</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 * 150, y: 'calc(100vh - 480px)' }"
:row-selection="{
type: 'checkbox',
columnWidth: '48px',
selectedRowKeys: tableState.selectedRowKeys,
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'cause'">
<span v-if="record.cdrJSON.result === 0">
{{ t('views.dashboard.cdr.resultFail') }},
<DictTag
:options="dict.cdrCauseCode"
:value="record.cdrJSON.cause"
value-default="0"
/>
</span>
<span v-else>
{{ t('views.dashboard.cdr.resultOk') }}
</span>
</template>
<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.id)"
>
<template #icon>
<DeleteOutlined />
</template>
</a-button>
</a-tooltip>
</a-space>
</template>
</template>
<template #expandedRowRender="{ record }">
<div style="width: 46%; padding-left: 32px; padding-bottom: 16px">
<a-divider orientation="left">
{{ t('views.dashboard.cdr.cdrInfo') }}
</a-divider>
<div>
<span>{{ t('views.ne.common.neName') }}: </span>
<span>{{ record.neName }}</span>
</div>
<div>
<span>{{ t('views.ne.common.rmUid') }}: </span>
<span>{{ record.rmUID }}</span>
</div>
<div>
<span>{{ t('views.dashboard.cdr.time') }}: </span>
<span>
{{
typeof record.cdrJSON.updateTime === 'number'
? parseDateToStr(+record.cdrJSON.updateTime * 1000)
: record.cdrJSON.updateTime
}}
</span>
</div>
<a-divider orientation="left">
{{ t('views.dashboard.cdr.rowInfo') }}
</a-divider>
<div>
<span>{{ t('views.dashboard.cdr.type') }}: </span>
<span>{{ record.cdrJSON.serviceType }}</span>
</div>
<div>
<span>{{ t('views.dashboard.cdr.caller') }}: </span>
<span>{{ record.cdrJSON.callerParty }}</span>
</div>
<div>
<span>{{ t('views.dashboard.cdr.called') }}: </span>
<span>{{ record.cdrJSON.calledParty }}</span>
</div>
<div>
<span>{{ t('views.dashboard.cdr.result') }}: </span>
<span v-if="record.cdrJSON.result === 0">
{{ t('views.dashboard.cdr.resultFail') }},
<DictTag
:options="dict.cdrCauseCode"
:value="record.cdrJSON.cause"
value-default="0"
/>
</span>
<span v-else>
{{ t('views.dashboard.cdr.resultOk') }}
</span>
</div>
</div>
</template>
</a-table>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,551 @@
<script setup lang="ts">
import { reactive, onMounted, ref, onBeforeUnmount } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import {
RESULT_CODE_ERROR,
RESULT_CODE_SUCCESS,
} from '@/constants/result-constants';
import { listAllNeInfo } from '@/api/ne/neInfo';
import { message } from 'ant-design-vue/es';
import { getGraphData } from '@/api/monitor/topology';
import { parseDateToStr } from '@/utils/date-utils';
import { Graph, GraphData, Menu, Tooltip } from '@antv/g6';
import {
edgeCubicAnimateCircleMove,
edgeCubicAnimateLineDash,
edgeLineAnimateState,
} from '../topologyBuild/hooks/registerEdge';
import {
nodeCircleAnimateShapeR,
nodeCircleAnimateShapeStroke,
nodeImageAnimateState,
nodeRectAnimateState,
} from '../topologyBuild/hooks/registerNode';
import useNeOptions from '@/views/ne/neInfo/hooks/useNeOptions';
import useI18n from '@/hooks/useI18n';
import { OptionsType, WS } from '@/plugins/ws-websocket';
import { hasRoles } from '@/plugins/auth-user';
import { parseBasePath } from '@/plugins/file-static-url';
const { t } = useI18n();
const { fnNeRestart, fnNeStop, fnNeLogFile } = useNeOptions();
const ws = new WS();
/**图DOM节点实例对象 */
const graphG6Dom = ref<HTMLElement | undefined>(undefined);
/**图状态 */
const graphState = reactive<Record<string, any>>({
/**当前图组名 */
group: '5GC System Architecture',
/**图数据 */
data: {
combos: [],
edges: [],
nodes: [],
},
});
/**非网元元素 */
const notNeNodes = [
'5GC',
'DN',
'UE',
'Base',
'lan',
'lan1',
'lan2',
'lan3',
'lan4',
'lan5',
'lan6',
'lan7',
'LAN',
'NR',
];
/**图实例对象 */
const graphG6 = ref<any>(null);
/**图节点右击菜单 */
const graphNodeMenu = new Menu({
offsetX: 6,
offseY: 10,
itemTypes: ['node'],
getContent(evt) {
if (!evt) return t('views.monitor.topologyBuild.graphNotInfo');
const { id, label, neState }: any = evt.item?.getModel();
if (notNeNodes.includes(id)) {
return `<div><span>${label || id}</span></div>`;
}
if (!neState) {
return `<div><span>${label || id}</span></div>`;
}
return `
<div
style="
display: flex;
flex-direction: column;
width: 140px;
"
>
<h3 style="margin-bottom: 8px">
${t('views.monitor.topology.name')}:
${neState.neName ?? '--'}
</h3>
<div id="restart" style="cursor: pointer; margin-bottom: 4px">
> ${t('views.configManage.neManage.restart')}
</div>
<div id="stop" style="cursor: pointer; margin-bottom: 4px;">
> ${t('views.configManage.neManage.stop')}
</div>
<div id="log" style="cursor: pointer; margin-bottom: 4px;">
> ${t('views.configManage.neManage.log')}
</div>
</div>
`;
},
handleMenuClick(target, item) {
const { neInfo }: any = item?.getModel();
const { neName, neType, neId } = neInfo;
const targetId = target.id;
switch (targetId) {
case 'restart':
fnNeRestart({ neName, neType, neId });
break;
case 'stop':
fnNeStop({ neName, neType, neId });
break;
case 'log':
fnNeLogFile({ neType, neId });
break;
}
},
});
/**图节点展示 */
const graphNodeTooltip = new Tooltip({
offsetX: 10,
offsetY: 20,
getContent(evt) {
if (!evt) return t('views.monitor.topologyBuild.graphNotInfo');
const { id, label, neState }: any = evt.item?.getModel();
if (notNeNodes.includes(id)) {
return `<div><span>${label || id}</span></div>`;
}
if (!neState) {
return `<div><span>${label || id}</span></div>`;
}
let notStudentInfo = '';
if (hasRoles(['teacher', 'admin'])) {
notStudentInfo = `
<div><strong>${t('views.monitor.topology.serialNum')}</strong><span>
${neState.sn ?? '--'}
</span></div>
<div><strong>${t('views.monitor.topology.expiryDate')}</strong><span>
${neState.expire ?? '--'}
</span></div> `;
}
return `
<div
style="
display: flex;
flex-direction: column;
width: 200px;
"
>
<div><strong>${t('views.monitor.topology.state')}</strong><span>
${
neState.online
? t('views.monitor.topology.normalcy')
: t('views.monitor.topology.exceptions')
}
</span></div>
<div><strong>${t('views.monitor.topology.refreshTime')}</strong><span>
${neState.refreshTime ?? '--'}
</span></div>
<div>========================</div>
<div><strong>ID</strong><span>${neState.neId}</span></div>
<div><strong>${t('views.monitor.topology.name')}</strong><span>
${neState.neName ?? '--'}
</span></div>
<div><strong>IP</strong><span>${neState.neIP}</span></div>
<div><strong>${t('views.monitor.topology.version')}</strong><span>
${neState.version ?? '--'}
</span></div>
${notStudentInfo}
</div>
`;
},
itemTypes: ['node'],
});
/**注册自定义边或节点 */
function registerEdgeNode() {
// 边
edgeCubicAnimateLineDash();
edgeCubicAnimateCircleMove();
edgeLineAnimateState();
// 节点
nodeCircleAnimateShapeR();
nodeCircleAnimateShapeStroke();
nodeRectAnimateState();
nodeImageAnimateState();
}
/**图数据渲染 */
function handleRanderGraph(
container: HTMLElement | undefined,
data: GraphData
) {
if (!container) return;
const { clientHeight, clientWidth } = container;
// 注册自定义边或节点
registerEdgeNode();
const graph = new Graph({
container: container,
width: clientWidth,
height: clientHeight,
fitCenter: true,
fitView: true,
fitViewPadding: [40],
autoPaint: true,
modes: {
default: [
'drag-combo',
'drag-canvas',
'zoom-canvas',
'collapse-expand-combo',
],
},
groupByTypes: false,
nodeStateStyles: {
selected: {
fill: 'transparent',
},
},
plugins: [graphNodeMenu, graphNodeTooltip],
animate: true, // 是否使用动画过度,默认为 false
animateCfg: {
duration: 500, // Number一次动画的时长
easing: 'linearEasing', // String动画函数
},
});
graph.data(data);
graph.render();
graphG6.value = graph;
// 创建 ResizeObserver 实例
var observer = new ResizeObserver(function (entries) {
// 当元素大小发生变化时触发回调函数
entries.forEach(function (entry) {
if (!graphG6.value) {
return;
}
graphG6.value.changeSize(
entry.contentRect.width,
entry.contentRect.height - 30
);
graphG6.value.fitCenter();
});
});
// 监听元素大小变化
observer.observe(container);
return graph;
}
/**
* 获取图组数据渲染到画布
* @param reload 是否重载数据
*/
function fnGraphDataLoad(reload: boolean = false) {
Promise.all([
getGraphData(graphState.group),
listAllNeInfo({
bandStatus: false,
}),
])
.then(resArr => {
const graphRes = resArr[0];
const neRes = resArr[1];
if (
graphRes.code === RESULT_CODE_SUCCESS &&
Array.isArray(graphRes.data.nodes) &&
graphRes.data.nodes.length > 0 &&
neRes.code === RESULT_CODE_SUCCESS &&
Array.isArray(neRes.data) &&
neRes.data.length > 0
) {
return {
graphData: graphRes.data,
neList: neRes.data,
};
} else {
message.warning({
content: t('views.monitor.topology.noData'),
duration: 5,
});
}
})
.then(res => {
if (!res) return;
const { combos, edges, nodes } = res.graphData;
// 节点过滤
const nf: Record<string, any>[] = nodes.filter(
(node: Record<string, any>) => {
Reflect.set(node, 'neState', { online: false });
// 图片路径处理
if (node.img) node.img = parseBasePath(node.img);
if (node.icon.show && node.icon?.img)
node.icon.img = parseBasePath(node.icon.img);
// 遍历是否有网元数据
const nodeID: string = node.id;
const hasNe = res.neList.some(ne => {
Reflect.set(node, 'neInfo', ne.neType === nodeID ? ne : {});
return ne.neType === nodeID;
});
if (hasNe) {
return true;
}
if (notNeNodes.includes(nodeID)) {
return true;
}
return false;
}
);
// 边过滤
const ef: Record<string, any>[] = edges.filter(
(edge: Record<string, any>) => {
const edgeSource: string = edge.source;
const edgeTarget: string = edge.target;
const hasNeS = nf.some(n => n.id === edgeSource);
const hasNeT = nf.some(n => n.id === edgeTarget);
// console.log(hasNeS, edgeSource, hasNeT, edgeTarget);
if (hasNeS && hasNeT) {
return true;
}
if (hasNeS && notNeNodes.includes(edgeTarget)) {
return true;
}
if (hasNeT && notNeNodes.includes(edgeSource)) {
return true;
}
return false;
}
);
// 分组过滤
combos.forEach((combo: Record<string, any>) => {
const comboChildren: Record<string, any>[] = combo.children;
combo.children = comboChildren.filter(c => nf.some(n => n.id === c.id));
return combo;
});
// 图数据
graphState.data = { combos, edges: ef, nodes: nf };
})
.finally(() => {
if (graphState.data.length < 0) return;
// 重载数据
if (reload) {
graphG6.value.read(graphState.data);
} else {
handleRanderGraph(graphG6Dom.value, graphState.data);
}
clearInterval(interval10s.value);
interval10s.value = null;
fnGetState();
interval10s.value = setInterval(async () => {
if (!interval10s.value) return;
fnGetState(); // 获取网元状态
}, 20_000);
});
}
/**网元状态调度器 */
const interval10s = ref<any>(null);
/**查询网元状态 */
function fnGetState() {
// 获取节点状态
for (const node of graphState.data.nodes) {
if (notNeNodes.includes(node.id)) continue;
const { neType, neId } = node.neInfo;
if (!neType || !neId) continue;
ws.send({
requestId: `${neType}_${neId}`,
type: 'ne_state',
data: {
neType: neType,
neId: neId,
},
});
}
}
/**接收数据后回调 */
function wsError(ev: any) {
// 接收数据后回调
console.error(ev);
}
/**接收数据后回调 */
function wsMessage(res: Record<string, any>) {
const { code, requestId, data } = res;
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
if (!requestId) return;
const [neType, neId] = requestId.split('_');
const { combos, edges, nodes } = graphState.data;
const node = nodes.find((item: Record<string, any>) => item.id === neType);
// 更新网元状态
const newNeState = Object.assign(node.neState, data, {
refreshTime: parseDateToStr(data.refreshTime, 'HH:mm:ss'),
online: !!data.cpu,
});
// 通过 ID 查询节点实例
const item = graphG6.value.findById(node.id);
if (item) {
const stateColor = newNeState.online ? '#52c41a' : '#f5222d'; // 状态颜色
// 图片类型不能填充
if (node.type.startsWith('image')) {
// 更新节点
if (node.label !== newNeState.neName) {
graphG6.value.updateItem(item, {
label: newNeState.neName,
});
}
// 设置状态
graphG6.value.setItemState(item, 'top-right-dot', stateColor);
} else {
// 更新节点
graphG6.value.updateItem(item, {
label: newNeState.neName,
// neState: newNeState,
style: {
fill: stateColor, // 填充色
stroke: stateColor, // 填充色
},
// labelCfg: {
// style: {
// fill: '#ffffff', // 标签文本色
// },
// },
});
// 设置状态
graphG6.value.setItemState(item, 'stroke', newNeState.online);
}
}
// 设置边状态
for (const edge of edges) {
const edgeSource: string = edge.source;
const edgeTarget: string = edge.target;
const neS = nodes.find((n: any) => n.id === edgeSource);
const neT = nodes.find((n: any) => n.id === edgeTarget);
// console.log(neS, edgeSource, neT, edgeTarget);
if (neS && neT) {
// 通过 ID 查询节点实例
// const item = graphG6.value.findById(edge.id);
// console.log(
// `${edgeSource} - ${edgeTarget}`,
// neS.neState.online && neT.neState.online
// );
// const stateColor = neS.neState.online && neT.neState.online ? '#000000' : '#ff4d4f'; // 状态颜色
// 更新边
// graphG6.value.updateItem(item, {
// label: `${edgeSource} - ${edgeTarget}`,
// style: {
// stroke: stateColor, // 填充色
// },
// labelCfg: {
// style: {
// fill: '#ffffff', // 标签文本色
// },
// },
// });
// 设置状态
graphG6.value.setItemState(
edge.id,
'circle-move',
neS.neState.online && neT.neState.online
);
}
if (neS && notNeNodes.includes(edgeTarget)) {
graphG6.value.setItemState(edge.id, 'line-dash', neS.neState.online);
}
if (neT && notNeNodes.includes(edgeSource)) {
graphG6.value.setItemState(edge.id, 'line-dash', neT.neState.online);
}
}
}
onMounted(() => {
fnGraphDataLoad(false);
// 建立链接
const options: OptionsType = {
url: '/ws',
onmessage: wsMessage,
onerror: wsError,
};
ws.connect(options);
});
onBeforeUnmount(() => {
ws.close();
clearInterval(interval10s.value);
interval10s.value = null;
});
</script>
<template>
<PageContainer>
<a-card
:bordered="false"
:body-style="{ marginBottom: '24px' }"
size="small"
>
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8" align="center">
<span>
{{ t('views.monitor.topologyBuild.graphGroup') }}
{{ graphState.group }}
</span>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-button
type="default"
size="small"
@click.prevent="fnGraphDataLoad(true)"
>
<template #icon><ReloadOutlined /></template>
{{ t('common.reloadText') }}
</a-button>
</template>
<div ref="graphG6Dom" class="chart"></div>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped>
.chart {
width: 100%;
height: calc(100vh - 300px);
background-color: rgb(43, 47, 51);
}
</style>

View File

@@ -0,0 +1,374 @@
<script setup lang="ts">
import { reactive, toRaw, watch } from 'vue';
import { ProModal } from 'antdv-pro-modal';
import { Form, Modal, Upload, message, notification } from 'ant-design-vue/es';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { UploadRequestOption } from 'ant-design-vue/es/vc-upload/interface';
import { FileType, UploadFile } from 'ant-design-vue/es/upload/interface';
import {
exportNeConfigBackup,
importNeConfigBackup,
listNeConfigBackup,
} from '@/api/ne/neConfigBackup';
import saveAs from 'file-saver';
import { uploadFile } 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,
},
/**网元ID */
neId: {
type: String,
default: '',
},
neType: {
type: String,
default: '',
},
});
/**导入状态数据 */
const importState = reactive({
typeOption: [
{ label: t('views.ne.neInfo.backConf.server'), value: 'backup' },
{ label: t('views.ne.neInfo.backConf.local'), value: 'upload' },
],
backupData: <any[]>[],
});
/**查询网元远程服务器备份文件 */
function backupSearch(name?: string) {
const { neType, neId } = modalState.from;
listNeConfigBackup({
neType,
neId,
name,
pageNum: 1,
pageSize: 20,
}).then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.rows)) {
importState.backupData = [];
res.rows.forEach((item: any) => {
importState.backupData.push({
label: item.name,
value: item.path,
});
});
}
});
}
/**服务器备份文件选择切换 */
function backupChange(value: any) {
if (!value) {
backupSearch();
}
}
/**类型切换 */
function typeChange(value: any) {
modalState.from.path = undefined;
if (value === 'backup') {
backupSearch();
}
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**新增框或修改框是否显示 */
openByEdit: boolean;
/**标题 */
title: string;
/**表单数据 */
from: {
neType: string;
neId: string;
type: 'upload' | 'backup';
path: string | undefined;
};
/**确定按钮 loading */
confirmLoading: boolean;
/**上传文件 */
uploadFiles: any[];
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
openByEdit: false,
title: '配置文件导入',
from: {
neType: '',
neId: '',
type: 'upload',
path: undefined,
},
confirmLoading: false,
uploadFiles: [],
});
/**对话框内表单属性和校验规则 */
const modalStateFrom = Form.useForm(
modalState.from,
reactive({
path: [
{
required: true,
message: t('views.ne.neInfo.backConf.pathPlease'),
},
],
})
);
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
function fnModalOk() {
if (modalState.confirmLoading) return;
const from = toRaw(modalState.from);
modalStateFrom
.validate()
.then(e => {
modalState.confirmLoading = true;
const hide = message.loading(t('common.loading'), 0);
importNeConfigBackup(from)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('common.operateOk'), 3);
// 返回无引用信息
emit('ok', JSON.parse(JSON.stringify(from)));
fnModalCancel();
} 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.openByEdit = false;
modalState.confirmLoading = false;
modalStateFrom.resetFields();
modalState.uploadFiles = [];
emit('cancel');
emit('update:open', false);
}
/**表单上传前删除 */
function fnBeforeRemoveFile(file: UploadFile) {
modalState.from.path = undefined;
return true;
}
/**表单上传前检查或转换压缩 */
function fnBeforeUploadFile(file: FileType) {
if (modalState.confirmLoading) return false;
if (!file.name.endsWith('.zip')) {
const msg = `${t('components.UploadModal.onlyAllow')} .zip`;
message.error(msg, 3);
return Upload.LIST_IGNORE;
}
const isLt3M = file.size / 1024 / 1024 < 100;
if (!isLt3M) {
const msg = `${t('components.UploadModal.allowFilter')} 100MB`;
message.error(msg, 3);
return Upload.LIST_IGNORE;
}
return true;
}
/**表单上传文件 */
function fnUploadFile(up: UploadRequestOption) {
// 发送请求
const hide = message.loading(t('common.loading'), 0);
modalState.confirmLoading = true;
let formData = new FormData();
formData.append('file', up.file);
formData.append('subPath', 'import');
uploadFile(formData)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
// 改为完成状态
const file = modalState.uploadFiles[0];
file.percent = 100;
file.status = 'done';
// 预置到表单
const { fileName } = res.data;
modalState.from.path = fileName;
} else {
message.error(res.msg, 3);
}
})
.finally(() => {
hide();
modalState.confirmLoading = false;
});
}
/**监听是否显示,初始数据 */
watch(
() => props.open,
val => {
if (val) {
if (props.neType && props.neId) {
modalState.from.neType = props.neType;
modalState.from.neId = props.neId;
modalState.title = t('views.ne.neInfo.backConf.title');
modalState.openByEdit = true;
}
}
}
);
/**
* 网元导出配置
* @param row 网元编号ID
*/
function fnExportConf(neType: string, neId: string) {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.ne.neInfo.backConf.exportTip'),
onOk() {
const hide = message.loading(t('common.loading'), 0);
exportNeConfigBackup({ neType, neId })
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
notification.success({
message: t('common.tipTitle'),
description: t('views.ne.neInfo.backConf.exportMsg'),
});
saveAs(
res.data,
`${neType}_${neId}_config_backup_${Date.now()}.zip`
);
} else {
message.error(`${res.msg}`, 3);
}
})
.finally(() => {
hide();
});
},
});
}
// 给组件设置属性 ref="xxxBackConf"
// setup内使用 const xxxBackConf = ref();
defineExpose({
/**导出文件 */
exportConf: fnExportConf,
});
</script>
<template>
<ProModal
:drag="true"
:width="800"
: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 }">
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item :label="t('views.ne.common.neType')" name="neType">
{{ modalState.from.neType }}
</a-form-item>
<a-form-item
:label="t('views.ne.neInfo.backConf.importType')"
name="type"
>
<a-select
v-model:value="modalState.from.type"
default-value="server"
:options="importState.typeOption"
@change="typeChange"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item :label="t('views.ne.common.neId')" name="neId">
{{ modalState.from.neId }}
</a-form-item>
<a-form-item
:label="t('views.ne.neInfo.backConf.server')"
name="fileName"
v-bind="modalStateFrom.validateInfos.path"
v-if="modalState.from.type === 'backup'"
>
<a-select
v-model:value="modalState.from.path"
:options="importState.backupData"
:placeholder="t('common.selectPlease')"
:show-search="true"
:default-active-first-option="false"
:show-arrow="false"
:allow-clear="true"
:filter-option="false"
:not-found-content="null"
@search="backupSearch"
@change="backupChange"
>
</a-select>
</a-form-item>
<a-form-item
:label="t('views.ne.neInfo.backConf.local')"
name="file"
v-bind="modalStateFrom.validateInfos.path"
v-if="modalState.from.type === 'upload'"
>
<a-upload
name="file"
v-model:file-list="modalState.uploadFiles"
accept=".zip"
list-type="text"
:max-count="1"
: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.neInfo.backConf.localUpload') }}
</a-button>
</a-upload>
</a-form-item>
</a-col>
</a-row>
</a-form>
</ProModal>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,834 @@
<script setup lang="ts">
import { reactive, onMounted, toRaw, watch } from 'vue';
import { ProModal } from 'antdv-pro-modal';
import { message, Form, Modal } from 'ant-design-vue/es';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { NE_TYPE_LIST } from '@/constants/ne-constants';
import { regExpIPv4, regExpIPv6 } from '@/utils/regular-utils';
import { getNeInfo, addNeInfo, updateNeInfo } from '@/api/ne/neInfo';
import { neHostAuthorizedRSA, testNeHost } from '@/api/ne/neHost';
import useDictStore from '@/store/modules/dict';
import useI18n from '@/hooks/useI18n';
const { getDict } = useDictStore();
const { t } = useI18n();
const emit = defineEmits(['ok', 'cancel', 'update:open']);
const props = defineProps({
open: {
type: Boolean,
default: false,
},
editId: {
type: String,
default: '',
},
});
/**字典数据 */
let dict: {
/**主机类型 */
neHostType: DictType[];
/**分组 */
neHostGroupId: DictType[];
/**认证模式 */
neHostAuthMode: DictType[];
} = reactive({
neHostType: [],
neHostGroupId: [],
neHostAuthMode: [],
});
/**
* 测试主机连接
*/
function fnHostTest(row: Record<string, any>) {
if (modalState.confirmLoading || !row.addr || !row.port) return;
modalState.confirmLoading = true;
const hide = message.loading(t('common.loading'), 0);
testNeHost(row)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: `${row.addr}:${row.port} ${t('views.ne.neHost.testOk')}`,
duration: 2,
});
} else {
message.error({
content: `${row.addr}:${row.port} ${res.msg}`,
duration: 2,
});
}
})
.finally(() => {
hide();
modalState.confirmLoading = false;
});
}
/**测试主机连接-免密直连 */
function fnHostAuthorized(row: Record<string, any>) {
if (modalState.confirmLoading) return;
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.ne.neHost.authRSATip'),
onOk: () => {
modalState.confirmLoading = true;
neHostAuthorizedRSA(row).then(res => {
modalState.confirmLoading = false;
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('common.operateOk'), 3);
} else {
message.error(t('common.operateErr'), 3);
}
});
},
});
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**新增框或修改框是否显示 */
openByEdit: boolean;
/**标题 */
title: string;
/**表单数据 */
from: Record<string, any>;
/**确定按钮 loading */
confirmLoading: boolean;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
openByEdit: false,
title: '网元',
from: {
id: undefined,
neId: '001',
neType: 'AMF',
neName: '',
ip: '',
port: 33030,
pvFlag: 'PNF',
rmUid: '4400HXAMF001',
neAddress: '',
dn: '',
vendorName: '',
province: '',
remark: '',
// 主机
hosts: [
{
hostId: undefined,
hostType: 'ssh',
groupId: '1',
title: 'SSH_NE_22',
addr: '',
port: 22,
user: 'omcuser',
authMode: '2',
password: '',
privateKey: '',
passPhrase: '',
remark: '',
},
{
hostId: 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'),
},
],
neId: [
{
required: true,
message: t('views.ne.common.neIdPlease'),
},
],
rmUid: [
{
required: true,
message: t('views.ne.common.rmUidPlease'),
},
],
ip: [
{
required: true,
validator: modalStateFromEqualIPV4AndIPV6,
},
],
neName: [
{
required: true,
message: t('views.ne.common.neNamePlease'),
},
],
})
);
/**表单验证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();
}
/**
* 对话框弹出显示为 新增或者修改
* @param editId 网元id, 不传为新增
*/
function fnModalVisibleByEdit(editId: string) {
if (!editId) {
modalStateFrom.resetFields();
modalState.title = t('views.ne.neInfo.addTitle');
modalState.openByEdit = true;
} else {
if (modalState.confirmLoading) return;
const hide = message.loading(t('common.loading'), 0);
modalState.confirmLoading = true;
getNeInfo(editId).then(res => {
modalState.confirmLoading = false;
hide();
if (res.code === RESULT_CODE_SUCCESS) {
Object.assign(modalState.from, res.data);
modalState.title = t('views.ne.neInfo.editTitle');
modalState.openByEdit = true;
} else {
message.error(t('common.getInfoFail'), 2);
}
});
}
}
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
function fnModalOk() {
modalStateFrom
.validate()
.then(e => {
modalState.confirmLoading = true;
const from = toRaw(modalState.from);
const result = from.id ? updateNeInfo(from) : addNeInfo(from);
const hide = message.loading(t('common.loading'), 0);
result
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('common.operateOk'), 3);
// 返回无引用信息
emit('ok', JSON.parse(JSON.stringify(from)));
fnModalCancel();
} 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.openByEdit = false;
modalState.confirmLoading = false;
modalStateFrom.resetFields();
emit('cancel');
emit('update:open', 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({
hostId: 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({
hostId: undefined,
hostType: 'redis',
groupId: '1',
title: 'REDIS_NE_6379',
addr: modalState.from.ip,
port: 6379,
user: 'udmdb',
authMode: '0',
password: 'helloearth',
dbName: '0',
remark: '',
});
}
modalState.from.rmUid = `4400HX${v}${modalState.from.neId}`; // 4400HX1AMF001
}
/**表单修改网元neId */
function fnNeIdChange(e: any) {
const v = e.target.value;
if (v.length < 1) return;
modalState.from.rmUid = `4400HX${modalState.from.neType}${v}`; // 4400HX1AMF001
}
/**表单修改网元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;
}
}
/**监听是否显示,初始数据 */
watch(
() => props.open,
val => {
if (val) fnModalVisibleByEdit(props.editId);
}
);
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;
}
});
});
</script>
<template>
<ProModal
:drag="true"
:width="800"
:destroyOnClose="true"
:body-style="{ maxHeight: '600px', 'overflow-y': 'auto' }"
:keyboard="false"
:mask-closable="false"
:open="modalState.openByEdit"
:title="modalState.title"
:confirm-loading="modalState.confirmLoading"
@ok="fnModalOk"
@cancel="fnModalCancel"
>
<a-form
name="modalStateFrom"
layout="horizontal"
:label-col="{ span: 6 }"
:labelWrap="true"
>
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<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 }))"
@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-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.ne.neInfo.pvflag')"
name="pvFlag"
v-bind="modalStateFrom.validateInfos.pvFlag"
>
<a-select
v-model:value="modalState.from.pvFlag"
default-value="PNF"
>
<a-select-opt-group :label="t('views.ne.neInfo.pnf')">
<a-select-option value="PNF">PNF</a-select-option>
</a-select-opt-group>
<a-select-opt-group :label="t('views.ne.neInfo.vnf')">
<a-select-option value="VNF">VNF</a-select-option>
</a-select-opt-group>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.ne.common.neId')"
name="neId"
v-bind="modalStateFrom.validateInfos.neId"
>
<a-input
v-model:value="modalState.from.neId"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="32"
@change="fnNeIdChange"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title>
{{ t('views.ne.common.neIdTip') }}
</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="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="64"
>
</a-input>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.ne.common.ipAddr')"
name="ip"
v-bind="modalStateFrom.validateInfos.ip"
>
<a-input
v-model:value="modalState.from.ip"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="128"
@change="fnNeIPChange"
>
<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-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.ne.common.port')"
name="port"
v-bind="modalStateFrom.validateInfos.port"
>
<a-input-number
v-model:value="modalState.from.port"
style="width: 100%"
:min="1"
:max="65535"
:maxlength="5"
placeholder="<=65535"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title>
<div>{{ t('views.ne.common.portTip') }}</div>
</template>
<InfoCircleOutlined style="opacity: 0.45; color: inherit" />
</a-tooltip>
</template>
</a-input-number>
</a-form-item>
</a-col>
</a-row>
<a-form-item
:label="t('views.ne.common.rmUid')"
name="rmUid"
v-bind="modalStateFrom.validateInfos.rmUid"
:label-col="{ span: 3 }"
:labelWrap="true"
>
<a-input
v-model:value="modalState.from.rmUid"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="40"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title>
<div>
{{ t('views.ne.common.rmUidTip') }}
</div>
</template>
<InfoCircleOutlined style="opacity: 0.45; color: inherit" />
</a-tooltip>
</template>
</a-input>
</a-form-item>
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item :label="t('views.ne.neInfo.neAddress')" name="neAddress">
<a-input
v-model:value="modalState.from.neAddress"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="64"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title>
<div>{{ t('views.ne.neInfo.neAddressTip') }}</div>
</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="t('views.ne.neInfo.dn')" name="dn">
<a-input
v-model:value="modalState.from.dn"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="255"
></a-input>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.ne.neInfo.vendorName')"
name="vendorName"
>
<a-input
v-model:value="modalState.from.vendorName"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="64"
>
</a-input>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item :label="t('views.ne.neInfo.province')" name="province">
<a-input
v-model:value="modalState.from.province"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="32"
></a-input>
</a-form-item>
</a-col>
</a-row>
<a-form-item
:label="t('common.remark')"
:label-col="{ span: 3 }"
:label-wrap="true"
>
<a-textarea
v-model:value="modalState.from.remark"
:auto-size="{ minRows: 1, maxRows: 6 }"
:maxlength="450"
:show-count="true"
:placeholder="t('common.inputPlease')"
/>
</a-form-item>
<!-- 主机连接配置 -->
<a-divider orientation="left">
{{ t('views.ne.neInfo.hostConfig') }}
</a-divider>
<a-collapse class="collapse" ghost>
<a-collapse-panel
v-for="host in modalState.from.hosts.filter(
(s:any) => !(s.hostType === 'telnet' && modalState.from.neType === 'OMC')
)"
:key="host.title"
>
<template #header>
<span v-if="host.hostType === 'redis'"> DB {{ host.port }} </span>
<span v-else>
{{ `${host.hostType.toUpperCase()} ${host.port}` }}
</span>
</template>
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item :label="t('views.ne.neHost.addr')">
<a-input
v-model:value="host.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="neHost.port"
>
<a-input-number
v-model:value="host.port"
:min="10"
:max="65535"
:step="1"
:maxlength="5"
style="width: 100%"
></a-input-number>
</a-form-item>
</a-col>
</a-row>
<a-form-item
v-if="host.hostType === 'telnet'"
:label="t('views.ne.neHost.user')"
:label-col="{ span: 3 }"
:label-wrap="true"
>
<a-input
v-model:value="host.user"
allow-clear
:maxlength="32"
:placeholder="t('common.inputPlease')"
>
</a-input>
</a-form-item>
<a-row v-if="host.hostType === 'ssh'">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item :label="t('views.ne.neHost.user')">
<a-input
v-model:value="host.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="host.authMode"
default-value="0"
:options="dict.neHostAuthMode"
>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item
v-if="host.authMode === '0'"
:label="t('views.ne.neHost.password')"
:label-col="{ span: 3 }"
:label-wrap="true"
>
<a-input-password
v-model:value="host.password"
:maxlength="128"
:placeholder="t('common.inputPlease')"
>
</a-input-password>
</a-form-item>
<template v-if="host.authMode === '1'">
<a-form-item
:label="t('views.ne.neHost.privateKey')"
:label-col="{ span: 3 }"
:label-wrap="true"
>
<a-textarea
v-model:value="host.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="host.passPhrase"
:maxlength="128"
:placeholder="t('common.inputPlease')"
>
</a-input-password>
</a-form-item>
</template>
<a-form-item
v-if="host.hostType === 'mysql'"
:label="t('views.ne.neHost.database')"
:label-col="{ span: 3 }"
:label-wrap="true"
>
<a-input
v-model:value="host.dbName"
allow-clear
:maxlength="32"
:placeholder="t('common.inputPlease')"
>
</a-input>
</a-form-item>
<a-form-item
:label="t('common.remark')"
:label-col="{ span: 3 }"
:label-wrap="true"
>
<a-textarea
v-model:value="host.remark"
:auto-size="{ minRows: 1, maxRows: 6 }"
:maxlength="450"
:show-count="true"
:placeholder="t('common.inputPlease')"
/>
</a-form-item>
<!-- 测试 -->
<a-form-item
:label="t('views.ne.neHost.test')"
name="test"
:label-col="{ span: 3 }"
:label-wrap="true"
>
<a-button
type="primary"
shape="round"
@click="fnHostTest(host)"
:loading="modalState.confirmLoading"
>
<template #icon><LinkOutlined /></template>
</a-button>
<a-button
type="link"
@click="fnHostAuthorized(host)"
:loading="modalState.confirmLoading"
v-if="host.hostType === 'ssh' && host.authMode !== '2'"
>
{{ t('views.ne.neHost.authRSA') }}
</a-button>
</a-form-item>
</a-collapse-panel>
</a-collapse>
</a-form>
</ProModal>
</template>
<style lang="less" scoped>
.collapse :deep(.ant-collapse-item) > .ant-collapse-header {
padding-left: 0;
padding-right: 0;
}
.collapse-header {
flex: 1;
display: flex;
flex-direction: row;
justify-content: space-between;
}
</style>

View File

@@ -0,0 +1,322 @@
<script setup lang="ts">
import { reactive, toRaw, watch } from 'vue';
import { ProModal } from 'antdv-pro-modal';
import { message, Form } from 'ant-design-vue/es';
import useI18n from '@/hooks/useI18n';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { getOAMFile, saveOAMFile } from '@/api/ne/neInfo';
const { t } = useI18n();
const emit = defineEmits(['ok', 'cancel', 'update:open']);
const props = defineProps({
open: {
type: Boolean,
default: false,
},
/**网元ID */
neId: {
type: String,
default: '',
},
neType: {
type: String,
default: '',
},
});
/**对话框对象信息状态类型 */
type ModalStateType = {
/**新增框或修改框是否显示 */
openByEdit: boolean;
/**标题 */
title: string;
/**是否同步 */
sync: boolean;
/**表单数据 */
from: Record<string, any>;
/**确定按钮 loading */
confirmLoading: boolean;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
openByEdit: false,
title: 'OAM Configuration',
sync: true,
from: {
omcIP: '',
oamEnable: true,
oamPort: 33030,
snmpEnable: true,
snmpPort: 4957,
kpiEnable: true,
kpiTimer: 60,
},
confirmLoading: false,
});
/**对话框内表单属性和校验规则 */
const modalStateFrom = Form.useForm(
modalState.from,
reactive({
kpiTimer: [
{
required: true,
message: t('views.ne.neInfo.oam.kpiTimerPlease'),
},
],
})
);
/**
* 对话框弹出显示为 新增或者修改
* @param neType 网元类型
* @param neId 网元ID
*/
function fnModalVisibleByTypeAndId(neType: string, neId: string) {
const hide = message.loading(t('common.loading'), 0);
getOAMFile(neType, neId)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
const data = res.data;
Object.assign(modalState.from, {
omcIP: data.oamConfig[data.oamConfig.ipType],
oamEnable: data.oamConfig.enable,
oamPort: data.oamConfig.port,
snmpEnable: data.snmpConfig.enable,
snmpPort: data.snmpConfig.port,
kpiEnable: data.kpiConfig.enable,
kpiTimer: data.kpiConfig.timer,
});
modalState.title = t('views.ne.neInfo.oam.title');
modalState.openByEdit = true;
} else {
message.error(res.msg, 3);
}
})
.finally(() => {
modalState.confirmLoading = false;
hide();
});
}
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
function fnModalOk() {
modalStateFrom
.validate()
.then(e => {
modalState.confirmLoading = true;
const hide = message.loading(t('common.loading'), 0);
const from = toRaw(modalState.from);
saveOAMFile({
neType: props.neType,
neId: props.neId,
content: from,
sync: modalState.sync,
})
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('common.operateOk'), 3);
emit('ok');
fnModalCancel();
} 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.openByEdit = false;
modalState.confirmLoading = false;
modalStateFrom.resetFields();
emit('cancel');
emit('update:open', false);
}
/**监听是否显示,初始数据 */
watch(
() => props.open,
val => {
if (val) {
if (props.neType && props.neId) {
fnModalVisibleByTypeAndId(props.neType, props.neId);
}
}
}
);
</script>
<template>
<ProModal
:drag="true"
:destroyOnClose="true"
:body-style="{ maxHeight: '600px', 'overflow-y': 'auto' }"
: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: 12 }"
:labelWrap="true"
>
<a-form-item
:label="t('views.ne.neInfo.oam.sync')"
name="sync"
:label-col="{ span: 6 }"
:labelWrap="true"
>
<a-switch
:checked-children="t('common.switch.open')"
:un-checked-children="t('common.switch.shut')"
v-model:checked="modalState.sync"
></a-switch>
</a-form-item>
<a-collapse class="collapse" ghost>
<a-collapse-panel header="OAM">
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.ne.neInfo.oam.oamEnable')"
name="oamEnable"
>
<a-switch
:checked-children="t('common.switch.open')"
:un-checked-children="t('common.switch.shut')"
v-model:checked="modalState.from.oamEnable"
></a-switch>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.ne.neInfo.oam.oamPort')"
name="oamPort"
v-bind="modalStateFrom.validateInfos.oamPort"
>
<a-input-number
:min="3000"
:max="65535"
:step="1"
:maxlength="5"
v-model:value="modalState.from.oamPort"
style="width: 100%"
></a-input-number>
</a-form-item>
</a-col>
</a-row>
<a-form-item
:label="t('views.ne.neInfo.oam.omcIP')"
name="omcIP"
:label-col="{ span: 6 }"
:labelWrap="true"
>
<a-input
v-model:value="modalState.from.omcIP"
:maxlength="128"
></a-input>
</a-form-item>
</a-collapse-panel>
<a-collapse-panel header="SNMP">
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.ne.neInfo.oam.snmpEnable')"
name="snmpEnable"
>
<a-switch
:checked-children="t('common.switch.open')"
:un-checked-children="t('common.switch.shut')"
v-model:checked="modalState.from.snmpEnable"
></a-switch>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.ne.neInfo.oam.snmpPort')"
name="snmpPort"
v-bind="modalStateFrom.validateInfos.snmpPort"
>
<a-input-number
:min="3000"
:max="65535"
:step="1"
:maxlength="5"
v-model:value="modalState.from.snmpPort"
style="width: 100%"
></a-input-number>
</a-form-item>
</a-col>
</a-row>
</a-collapse-panel>
<a-collapse-panel header="KPI">
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.ne.neInfo.oam.kpiEnable')"
name="kpiEnable"
>
<a-switch
:checked-children="t('common.switch.open')"
:un-checked-children="t('common.switch.shut')"
v-model:checked="modalState.from.kpiEnable"
></a-switch>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.ne.neInfo.oam.kpiTimer')"
name="kpiTimer"
v-bind="modalStateFrom.validateInfos.kpiTimer"
>
<a-input-number
:min="5"
:max="3600"
:step="1"
:maxlength="4"
v-model:value="modalState.from.kpiTimer"
style="width: 100%"
></a-input-number>
</a-form-item>
</a-col>
</a-row>
</a-collapse-panel>
</a-collapse>
</a-form>
</ProModal>
</template>
<style lang="less" scoped>
.collapse :deep(.ant-collapse-item) > .ant-collapse-header {
padding-left: 0;
padding-right: 0;
}
.collapse-header {
flex: 1;
display: flex;
flex-direction: row;
justify-content: space-between;
}
</style>

View File

@@ -0,0 +1,153 @@
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { Modal, message } from 'ant-design-vue/es';
import useI18n from '@/hooks/useI18n';
import { useRouter } from 'vue-router';
import { updateNeConfigReload } from '@/api/configManage/configParam';
import { serviceNeAction } from '@/api/ne/neInfo';
import useMaskStore from '@/store/modules/mask';
export default function useNeOptions() {
const router = useRouter();
const { t } = useI18n();
const maskStore = useMaskStore();
/**
* 网元启动
* @param row {neName,neType,neId}
*/
function fnNeStart(row: Record<string, any>) {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.ne.common.startTip'),
onOk() {
const hide = message.loading(t('common.loading'), 0);
serviceNeAction({
neType: row.neType,
neId: row.neId,
action: 'start',
})
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('common.operateOk'), 3);
} else {
message.error(`${res.msg}`, 3);
}
})
.finally(() => {
hide();
});
},
});
}
/**
* 网元重启
* @param row {neName,neType,neId}
*/
function fnNeRestart(row: Record<string, any>) {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.ne.common.restartTip'),
onOk() {
const hide = message.loading(t('common.loading'), 0);
serviceNeAction({
neType: row.neType,
neId: row.neId,
action: 'restart',
})
.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({
content: `${res.msg}`,
duration: 3,
});
}
return;
}
message.success(t('common.operateOk'), 3);
} else {
message.error(`${res.msg}`, 3);
}
})
.finally(() => {
hide();
});
},
});
}
/**
* 网元停止
* @param row {neName,neType,neId}
*/
function fnNeStop(row: Record<string, any>) {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.ne.common.stopTip'),
onOk() {
const hide = message.loading(t('common.loading'), 0);
serviceNeAction({
neType: row.neType,
neId: row.neId,
action: 'stop',
})
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('common.operateOk'), 3);
} else {
message.error(`${res.msg}`, 3);
}
})
.finally(() => {
hide();
});
},
});
}
/**
* 网元重新加载
* @param row {neName,neType,neId}
*/
function fnNeReload(row: Record<string, any>) {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.ne.common.reloadTip'),
onOk() {
const hide = message.loading(t('common.loading'), 0);
updateNeConfigReload(row.neType, row.neId)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('common.operateOk'), 3);
} else {
message.error(`${res.msg}`, 3);
}
})
.finally(() => {
hide();
});
},
});
}
/**
* 跳转网元日志文件页面
* @param row {neType,neId}
*/
function fnNeLogFile(row: Record<string, any>) {
router.push({
name: 'NeFile_2123',
query: {
neType: row.neType,
neId: row.neId,
},
});
}
return { fnNeStart, fnNeRestart, fnNeStop, fnNeReload, fnNeLogFile };
}

View File

@@ -0,0 +1,790 @@
<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 useNeInfoStore from '@/store/modules/neinfo';
import { listNeInfo, delNeInfo, stateNeInfo } from '@/api/ne/neInfo';
import { NE_TYPE_LIST } from '@/constants/ne-constants';
import { hasRoles } from '@/plugins/auth-user';
import useDictStore from '@/store/modules/dict';
import useNeOptions from './hooks/useNeOptions';
const { getDict } = useDictStore();
const { t } = useI18n();
const { fnNeStart, fnNeRestart, fnNeStop, fnNeReload, fnNeLogFile } =
useNeOptions();
// 异步加载组件
const EditModal = defineAsyncComponent(
() => import('./components/EditModal.vue')
);
const OAMModal = defineAsyncComponent(
() => import('./components/OAMModal.vue')
);
// 配置备份文件导入
const BackConfModal = defineAsyncComponent(
() => import('./components/BackConfModal.vue')
);
const backConf = ref(); // 引用句柄,取导出函数
/**字典数据 */
let dict: {
/**网元信息状态 */
neInfoStatus: DictType[];
} = reactive({
neInfoStatus: [],
});
/**查询参数 */
let queryParams = reactive({
/**网元类型 */
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)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
seached: false,
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: t('views.ne.common.neType'),
dataIndex: 'neType',
align: 'left',
width: 100,
},
{
title: t('views.ne.common.neId'),
dataIndex: 'neId',
align: 'left',
width: 100,
},
{
title: t('views.ne.common.rmUid'),
dataIndex: 'rmUid',
align: 'left',
width: 150,
},
{
title: t('views.ne.common.neName'),
dataIndex: 'neName',
align: 'left',
width: 150,
},
{
title: t('views.ne.common.ipAddr'),
dataIndex: 'ip',
align: 'left',
width: 150,
},
{
title: t('views.ne.common.port'),
dataIndex: 'port',
align: 'left',
width: 100,
},
{
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)[]) {
tableState.selectedRowKeys = keys;
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**配置备份框是否显示 */
openByBackConf: boolean;
/**OAM文件配置框是否显示 */
openByOAM: boolean;
/**新增框或修改框是否显示 */
openByEdit: boolean;
/**新增框或修改框ID */
editId: string;
/**OAM框网元类型ID */
neId: string;
neType: string;
/**确定按钮 loading */
confirmLoading: boolean;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
openByBackConf: false,
openByOAM: false,
openByEdit: false,
editId: '',
neId: '',
neType: '',
confirmLoading: false,
});
/**
* 对话框弹出显示为 新增或者修改
* @param noticeId 网元id, 不传为新增
*/
function fnModalVisibleByEdit(row?: Record<string, any>) {
if (!row) {
modalState.editId = '';
} else {
modalState.editId = row.id;
}
modalState.openByEdit = !modalState.openByEdit;
}
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
function fnModalEditOk(from: Record<string, any>) {
// 新增时刷新列表
if (!from.id) {
fnGetList();
return;
}
// 编辑时局部更新信息
stateNeInfo(from.neType, from.neId)
.then(res => {
// 找到编辑更新的网元
const item = tableState.data.find(s => s.id === from.id);
if (item && res.code === RESULT_CODE_SUCCESS) {
item.neType = from.neType;
item.neId = from.neId;
item.rmUid = from.rmUid;
item.neName = from.neName;
item.ip = from.ip;
item.port = from.port;
if (item.status !== '2') {
item.status = res.data.online ? '1' : '0';
}
Object.assign(item.serverState, res.data);
const resouresUsage = parseResouresUsage(item.serverState);
Reflect.set(item, 'resoures', resouresUsage);
}
})
.finally(() => {
useNeInfoStore().fnRefreshNelist();
});
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalEditCancel() {
modalState.editId = '';
modalState.openByEdit = false;
modalState.openByOAM = false;
modalState.openByBackConf = false;
}
/**
* 记录删除
* @param id 编号
*/
function fnRecordDelete(id: string) {
if (!id || modalState.confirmLoading) return;
let msg = t('views.ne.neInfo.delTip');
if (id === '0') {
msg = `${msg} ...${tableState.selectedRowKeys.length}`;
id = tableState.selectedRowKeys.join(',');
}
Modal.confirm({
title: t('common.tipTitle'),
content: msg,
onOk() {
modalState.confirmLoading = true;
const hide = message.loading(t('common.loading'), 0);
delNeInfo(id)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('common.operateOk'), 3);
// 过滤掉删除的id
tableState.data = tableState.data.filter(item => {
if (id.indexOf(',') > -1) {
return !tableState.selectedRowKeys.includes(item.id);
} else {
return item.id !== id;
}
});
// 刷新缓存
useNeInfoStore().fnRefreshNelist();
} else {
message.error({
content: `${res.msg}`,
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);
break;
case 'restart':
fnNeRestart(row);
break;
case 'stop':
fnNeStop(row);
break;
case 'reload':
fnNeReload(row);
break;
case 'log':
fnNeLogFile(row);
break;
case 'oam':
modalState.neId = row.neId;
modalState.neType = row.neType;
modalState.openByOAM = !modalState.openByOAM;
break;
case 'backConfExport':
backConf.value.exportConf(row.neType, row.neId);
break;
case 'backConfImport':
modalState.neId = row.neId;
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.rows)) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
tablePagination.total = res.total;
// 遍历处理资源情况数值
tableState.data = res.rows.map(item => {
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;
});
}
/**解析网元状态携带的资源利用率 */
function parseResouresUsage(neState: Record<string, any>) {
let sysCpuUsage = 0;
let nfCpuUsage = 0;
if (neState.cpu) {
nfCpuUsage = neState.cpu.nfCpuUsage;
const nfCpu = +(nfCpuUsage / 100);
nfCpuUsage = +nfCpu.toFixed(2);
if (nfCpuUsage > 100) {
nfCpuUsage = 100;
}
sysCpuUsage = neState.cpu.sysCpuUsage;
const sysCpu = +(sysCpuUsage / 100);
sysCpuUsage = +sysCpu.toFixed(2);
if (sysCpuUsage > 100) {
sysCpuUsage = 100;
}
}
let sysMemUsage = 0;
if (neState.mem) {
const men = neState.mem.sysMemUsage;
sysMemUsage = +(men / 100).toFixed(2);
if (sysMemUsage > 100) {
sysMemUsage = 100;
}
}
let sysDiskUsage = 0;
if (neState.disk && Array.isArray(neState.disk.partitionInfo)) {
let disks: any[] = neState.disk.partitionInfo;
disks = disks.sort((a, b) => +b.used - +a.used);
if (disks.length > 0) {
const { total, used } = disks[0];
sysDiskUsage = +((used / total) * 100).toFixed(2);
}
}
return {
sysDiskUsage,
sysMemUsage,
sysCpuUsage,
nfCpuUsage,
};
}
onMounted(() => {
// 初始字典数据
Promise.allSettled([getDict('ne_info_status')]).then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.neInfoStatus = resArr[0].value;
}
});
// 刷新缓存的网元信息
useNeInfoStore()
.fnRefreshNelist()
.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="NE_TYPE_LIST.map(v => ({ value: v }))"
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" v-roles:has="['admin']">
<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">
<span v-roles:has="['admin']">
<a-tooltip>
<template #title>{{ t('common.editText') }}</template>
<a-button
type="link"
@click.prevent="fnModalVisibleByEdit(record)"
>
<template #icon><FormOutlined /></template>
</a-button>
</a-tooltip>
</span>
<span v-roles:has="['admin', 'teacher']">
<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>
</span>
<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" v-if="hasRoles(['admin'])">
<ThunderboltOutlined />
{{ t('views.ne.common.start') }}
</a-menu-item>
<a-menu-item key="stop" v-if="hasRoles(['admin'])">
<CloseSquareOutlined />
{{ t('views.ne.common.stop') }}
</a-menu-item>
<a-menu-item
key="reload"
v-if="
!['OMC', 'PCF', 'IMS', 'MME'].includes(
record.neType
) && hasRoles(['admin'])
"
>
<SyncOutlined />
{{ t('views.ne.common.reload') }}
</a-menu-item>
<a-menu-item key="delete" v-if="hasRoles(['admin'])">
<DeleteOutlined />
{{ t('common.deleteText') }}
</a-menu-item>
<a-menu-item key="oam" v-if="hasRoles(['admin'])">
<FileTextOutlined />
{{ t('views.ne.common.oam') }}
</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 :gutter="16">
<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>
<a-tag
:color="record.serverState.online ? 'processing' : 'error'"
>
{{
record.serverState.online
? t('views.ne.common.normalcy')
: t('views.ne.common.exceptions')
}}
</a-tag>
</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>
</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"
:edit-id="modalState.editId"
@ok="fnModalEditOk"
@cancel="fnModalEditCancel"
></EditModal>
<!-- OAM编辑框 -->
<OAMModal
v-model:open="modalState.openByOAM"
:ne-id="modalState.neId"
:ne-type="modalState.neType"
@cancel="fnModalEditCancel"
></OAMModal>
<!-- 配置文件备份框 -->
<BackConfModal
ref="backConf"
v-model:open="modalState.openByBackConf"
:ne-id="modalState.neId"
:ne-type="modalState.neType"
@cancel="fnModalEditCancel"
></BackConfModal>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,66 @@
import { ADMIN_PERMISSION, ADMIN_ROLE_KEY } from '@/constants/admin-constants';
import useUserStore from '@/store/modules/user';
/**
* 是否系统管理员
* @returns true | false
*/
export function isSystemAdmin(): boolean {
const userPermissions = useUserStore().permissions;
if (userPermissions.includes(ADMIN_PERMISSION)) return true;
const userRoles = useUserStore().roles;
if (userRoles.includes(ADMIN_ROLE_KEY)) return true;
return false;
}
/**
* 只需含有其中权限
* @param role 权限字符数组
* @returns true | false
*/
export function hasPermissions(permissions: string[]): boolean {
if (!permissions || permissions.length === 0) return false;
const userPermissions = useUserStore().permissions;
if (!userPermissions || userPermissions.length === 0) return false;
if (userPermissions.includes(ADMIN_PERMISSION)) return true;
return permissions.some(p => userPermissions.some(up => up === p));
}
/**
* 同时匹配其中权限
* @param role 权限字符数组
* @returns true | false
*/
export function matchPermissions(permissions: string[]): boolean {
if (!permissions || permissions.length === 0) return false;
const userPermissions = useUserStore().permissions;
if (!userPermissions || userPermissions.length === 0) return false;
if (userPermissions.includes(ADMIN_PERMISSION)) return true;
return permissions.every(p => userPermissions.some(up => up === p));
}
/**
* 只需含有其中角色
* @param role 角色字符数组
* @returns true | false
*/
export function hasRoles(roles: string[]): boolean {
if (!roles || roles.length === 0) return false;
const userRoles = useUserStore().roles;
if (!userRoles || userRoles.length === 0) return false;
if (userRoles.includes(ADMIN_ROLE_KEY)) return true;
return roles.some(r => userRoles.some(ur => ur === r));
}
/**
* 同时匹配其中角色
* @param role 角色字符数组
* @returns true | false
*/
export function matchRoles(roles: string[]): boolean {
if (!roles || roles.length === 0) return false;
const userRoles = useUserStore().roles;
if (!userRoles || userRoles.length === 0) return false;
if (userRoles.includes(ADMIN_ROLE_KEY)) return true;
return roles.every(r => userRoles.some(ur => ur === r));
}

View File

@@ -0,0 +1,171 @@
import defaultAvatar from '@/assets/images/default_avatar.png';
import useLayoutStore from './layout';
import { login, logout, getInfo } from '@/api/login';
import { setToken, removeToken } from '@/plugins/auth-token';
import { defineStore } from 'pinia';
import { TOKEN_RESPONSE_FIELD } from '@/constants/token-constants';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { parseUrlPath } from '@/plugins/file-static-url';
/**用户信息类型 */
type UserInfo = {
/**用户ID */
userId: string;
/**登录账号 */
userName: string;
/**用户角色 字符串数组 */
roles: string[];
/**用户权限 字符串数组 */
permissions: string[];
/**用户头像 */
avatar: string;
/**用户昵称 */
nickName: string;
/**用户手机号 */
phonenumber: string;
/**用户邮箱 */
email: string;
/**用户性别 */
sex: string | undefined;
/**其他信息 */
profile: Record<string, any>;
};
const useUserStore = defineStore('user', {
state: (): UserInfo => ({
userId: '',
userName: '',
roles: [],
permissions: [],
avatar: '',
nickName: '',
phonenumber: '',
email: '',
sex: undefined,
profile: {},
}),
getters: {
/**
* 获取正确头像地址
* @param state 内部属性不用传入
* @returns 头像地址url
*/
getAvatar(state) {
if (!state.avatar) {
return defaultAvatar;
}
return parseUrlPath(state.avatar);
},
/**
* 获取基础信息属性
* @param state 内部属性不用传入
* @returns 基础信息
*/
getBaseInfo(state) {
return {
nickName: state.nickName,
phonenumber: state.phonenumber,
email: state.email,
sex: state.sex,
};
},
},
actions: {
/**
* 更新基础信息属性
* @param data 变更信息
*/
setBaseInfo(data: Record<string, any>) {
this.nickName = data.nickName;
this.phonenumber = data.phonenumber;
this.email = data.email;
this.sex = data.sex;
},
/**
* 更新头像
* @param avatar 上传后的地址
*/
setAvatar(avatar: string) {
this.avatar = avatar;
},
/**
* 获取正确头像地址
* @param avatar
*/
fnAvatar(avatar: string) {
if (!avatar) {
return defaultAvatar;
}
return parseUrlPath(avatar);
},
// 登录
async fnLogin(loginBody: Record<string, string>) {
const res = await login(loginBody);
if (res.code === RESULT_CODE_SUCCESS && res.data) {
const token = res.data[TOKEN_RESPONSE_FIELD];
setToken(token);
}
return res;
},
// 获取用户信息
async fnGetInfo() {
const res = await getInfo();
if (res.code === RESULT_CODE_SUCCESS && res.data) {
const { user, roles, permissions } = res.data;
this.userId = user.userId;
// 登录账号
this.userName = user.userName;
// 用户头像
this.avatar = user.avatar;
// 基础信息
this.nickName = user.nickName;
this.phonenumber = user.phonenumber;
this.email = user.email;
this.sex = user.sex;
// 验证返回的roles是否是一个非空数组
if (Array.isArray(roles) && roles.length > 0) {
this.roles = roles;
this.permissions = permissions;
} else {
this.roles = ['ROLE_DEFAULT'];
this.permissions = [];
}
// 水印文字信息=用户昵称 手机号
let waterMarkContent = this.userName;
if (this.phonenumber) {
waterMarkContent = `${this.userName} ${this.phonenumber}`;
}
// useLayoutStore().changeWaterMark(waterMarkContent);
useLayoutStore().changeWaterMark('');
// 学生布局用不一样的
if (this.roles.includes('student')) {
useLayoutStore().changeConf('layout', 'side');
useLayoutStore().changeConf('menuTheme', 'dark');
useLayoutStore().changeConf('tabRender', false);
}
}
// 网络错误时退出登录状态
if (res.code === 0) {
removeToken();
window.location.reload();
}
return res;
},
// 退出系统
async fnLogOut() {
try {
await logout();
} catch (error) {
throw error;
} finally {
this.roles = [];
this.permissions = [];
removeToken();
}
},
},
});
export default useUserStore;