Merge remote-tracking branch 'origin/main'

This commit is contained in:
zhongzm
2024-11-15 16:58:38 +08:00
18 changed files with 518 additions and 210 deletions

View File

@@ -38,7 +38,7 @@ onBeforeMount(() => {
// 输出应用版本号 // 输出应用版本号
const appStore = useAppStore(); const appStore = useAppStore();
console.info( console.info(
`%c ${t('common.title')} %c ${appStore.appCode} - ${appStore.appVersion} `, `%c ${t('common.desc')} %c ${appStore.appCode} - ${appStore.appVersion} `,
'color: #fadfa3; background: #030307; padding: 4px 0;', 'color: #fadfa3; background: #030307; padding: 4px 0;',
'color: #030307; background: #fadfa3; padding: 4px 0;' 'color: #030307; background: #fadfa3; padding: 4px 0;'
); );

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 B

After

Width:  |  Height:  |  Size: 226 KiB

View File

@@ -1,9 +1,9 @@
<!-- https://github.com/jackocnr/intl-tel-input/blob/master/react/src/intl-tel-input/react.tsx --> <!-- https://github.com/jackocnr/intl-tel-input/blob/master/react/src/intl-tel-input/react.tsx -->
<script lang="ts" setup> <script lang="ts" setup>
import intlTelInput, { Iti, SomeOptions } from 'intl-tel-input'; import { Iti } from 'intl-tel-input';
import intlTelInput from 'intl-tel-input/intlTelInputWithUtils';
import 'intl-tel-input/build/css/intlTelInput.min.css'; import 'intl-tel-input/build/css/intlTelInput.min.css';
import 'intl-tel-input/build/js/utils.js'; import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import useI18n from '@/hooks/useI18n'; import useI18n from '@/hooks/useI18n';
const { currentLocale } = useI18n(); const { currentLocale } = useI18n();
const emit = defineEmits(['update:value', 'update:change']); const emit = defineEmits(['update:value', 'update:change']);
@@ -45,13 +45,13 @@ const itiRef = ref<Iti | null>(null);
function fnChange() { function fnChange() {
if (!itiRef.value) return; if (!itiRef.value) return;
const num = itiRef.value?.getNumber() || ''; const number = itiRef.value?.getNumber() || '';
const countryIso = itiRef.value?.getSelectedCountryData().iso2 || ''; const countryIso = itiRef.value?.getSelectedCountryData().iso2 || '';
// note: this number will be in standard E164 format, but any container component can use // note: this number will be in standard E164 format, but any container component can use
// intlTelInputUtils.formatNumber() to convert this to another format // intlTelInputUtils.formatNumber() to convert this to another format
// as well as intlTelInputUtils.getNumberType() etc. if need be // as well as intlTelInputUtils.getNumberType() etc. if need be
let data = { let data = {
num, number,
countryIso, countryIso,
validity: false, validity: false,
errorCode: -1, errorCode: -1,
@@ -69,21 +69,11 @@ function fnChange() {
data.errorCode = errorCode; data.errorCode = errorCode;
} }
// console.log(data); // console.log(data);
emit('update:value', num);
emit('update:value', number);
emit('update:change', data); emit('update:change', data);
} }
watch(
() => props.value,
v => {
if (v) {
itiRef.value?.setNumber(v);
} else {
itiRef.value?.setNumber('');
}
}
);
onMounted(() => { onMounted(() => {
nextTick(async () => { nextTick(async () => {
if (inputRef.value) { if (inputRef.value) {
@@ -106,7 +96,13 @@ onMounted(() => {
formatOnDisplay: true, formatOnDisplay: true,
autoPlaceholder: 'polite', autoPlaceholder: 'polite',
i18n: i18n, i18n: i18n,
} as SomeOptions); });
if (props.value) {
itiRef.value.setNumber(props.value);
}
if (props.disabled) {
itiRef.value.setDisabled(props.disabled);
}
inputRef.value.addEventListener('countrychange', fnChange); inputRef.value.addEventListener('countrychange', fnChange);
} }
}); });
@@ -124,8 +120,7 @@ onBeforeUnmount(() => {
<input <input
type="tel" type="tel"
class="ant-input" class="ant-input"
ref="inputRef" ref="inputRef"
:value="value"
:disabled="disabled" :disabled="disabled"
:placeholder="placeholder" :placeholder="placeholder"
:maxlength="maxlength" :maxlength="maxlength"

View File

@@ -6,7 +6,7 @@ export default {
// 通用 // 通用
common: { common: {
title: 'Core Network Management Platform', title: 'Login Platform',
desc: 'Core Network Management Platform', desc: 'Core Network Management Platform',
loading: 'Please wait...', loading: 'Please wait...',
inputPlease: 'Please input', inputPlease: 'Please input',

View File

@@ -6,7 +6,7 @@ export default {
// 通用 // 通用
common: { common: {
title: '核心网管理平台', title: '登录平台',
desc: '核心网管理平台', desc: '核心网管理平台',
loading: '请稍等...', loading: '请稍等...',
inputPlease: '请输入', inputPlease: '请输入',

View File

@@ -8,3 +8,8 @@ declare module '*.vue' {
} }
declare module 'vue3-smooth-dnd'; declare module 'vue3-smooth-dnd';
declare module 'intl-tel-input/intlTelInputWithUtils' {
import intlTelInput from 'intl-tel-input';
export default intlTelInput;
}

View File

@@ -169,7 +169,7 @@ onMounted(() => {
<IntlTelInput <IntlTelInput
v-model:value="stateForm.form.phonenumber" v-model:value="stateForm.form.phonenumber"
allow-clear allow-clear
:maxlength="16" :maxlength="20"
:placeholder="t('views.account.settings.phonenumberPleace')" :placeholder="t('views.account.settings.phonenumberPleace')"
></IntlTelInput> ></IntlTelInput>
</a-form-item> </a-form-item>

View File

@@ -176,7 +176,10 @@ let tableColumns: ColumnsType = [
width: 150, width: 150,
customRender(opt) { customRender(opt) {
const cdrJSON = opt.value; const cdrJSON = opt.value;
return parseDateToStr(+cdrJSON.updateTime * 1000); if (typeof cdrJSON.seizureTime === 'number') {
return parseDateToStr(+cdrJSON.updateTime * 1000);
}
return cdrJSON.updateTime;
}, },
}, },
{ {

View File

@@ -12,7 +12,7 @@ import useLayoutStore from '@/store/modules/layout';
import { viewTransitionTheme } from 'antdv-pro-layout'; import { viewTransitionTheme } from 'antdv-pro-layout';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants'; import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { parseUrlPath } from '@/plugins/file-static-url'; import { parseUrlPath } from '@/plugins/file-static-url';
const { t, changeLocale, optionsLocale, currentLocale } = useI18n(); const { t, changeLocale, optionsLocale } = useI18n();
const layoutStore = useLayoutStore(); const layoutStore = useLayoutStore();
const appStore = useAppStore(); const appStore = useAppStore();
const router = useRouter(); const router = useRouter();
@@ -89,28 +89,12 @@ function fnGetCaptcha() {
}); });
} }
// LOGO地址
const logoUrl = computed(() => {
let url =
appStore.logoType === 'brand'
? parseUrlPath(appStore.filePathBrand)
: parseUrlPath(appStore.filePathIcon);
if (url.indexOf('{language}') === -1) {
return url;
}
// 语言参数替换
const local = currentLocale.value;
const lang = local.split('_')[0];
return url.replace('{language}', lang);
});
// 判断是否有背景地址 // 判断是否有背景地址
const calcBG = computed(() => { const calcBG = computed(() => {
const bgURL = parseUrlPath(appStore.loginBackground); const bgURL = parseUrlPath(appStore.loginBackground);
if (bgURL && bgURL !== '#') { if (bgURL && bgURL !== '#') {
return { return {
backgroundImage: `url(${bgURL})`, backgroundImage: `url('${bgURL}')`,
backgroundPosition: 'center', backgroundPosition: 'center',
backgroundSize: 'cover', backgroundSize: 'cover',
}; };
@@ -157,21 +141,9 @@ function fnChangeLocale(e: any) {
<div class="animation animation5"></div> <div class="animation animation5"></div>
</section> </section>
<header class="header">
<div class="header-left">
<template v-if="appStore.logoType === 'icon'">
<img :src="logoUrl" class="logo-icon" :alt="appStore.appName" />
<span class="title">{{ appStore.appName }}</span>
</template>
<template v-if="appStore.logoType === 'brand'">
<img :src="logoUrl" class="logo-brand" :alt="appStore.appName" />
</template>
</div>
</header>
<a-card :bordered="false" class="login-card"> <a-card :bordered="false" class="login-card">
<div class="desc"> <div class="title">
{{ t('common.desc') }} {{ t('common.title') }}
</div> </div>
<a-form :model="state.from" name="stateFrom" @finish="fnFinish"> <a-form :model="state.from" name="stateFrom" @finish="fnFinish">
<a-form-item <a-form-item
@@ -317,10 +289,6 @@ function fnChangeLocale(e: any) {
</a-row> </a-row>
</a-form> </a-form>
</a-card> </a-card>
<footer class="footer">
<div class="footer-copyright">{{ appStore.copyright }}</div>
</footer>
</div> </div>
</template> </template>
@@ -386,52 +354,6 @@ function fnChangeLocale(e: any) {
background-color: #141414; background-color: #141414;
} }
.header {
position: fixed;
left: 0;
top: 0;
width: 100%;
z-index: 1000;
background-color: rgb(255 255 255 / 85%);
padding: 0 16px;
display: flex;
justify-content: space-between;
align-items: center;
.logo-icon {
height: 40px;
width: 40px;
margin-right: 14px;
vertical-align: top;
border-style: none;
border-radius: 6.66px;
margin-top: 4px;
margin-bottom: 4px;
}
.logo-brand {
height: 48px;
width: 174px;
vertical-align: top;
border-style: none;
border-radius: 2px;
}
.title {
position: relative;
top: 12px;
color: rgba(0, 0, 0, 0.85);
font-weight: 600;
font-size: 24px;
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
}
}
[data-theme='dark'] .header {
background-color: rgb(0 0 0 / 85%);
.title {
color: rgba(255, 255, 255, 0.85);
}
}
.login-card { .login-card {
width: 368px; width: 368px;
min-width: 260px; min-width: 260px;
@@ -439,10 +361,10 @@ function fnChangeLocale(e: any) {
margin-left: 60%; margin-left: 60%;
border-radius: 6px; border-radius: 6px;
& .desc { & .title {
text-align: center; text-align: left;
margin-bottom: 18px; margin-bottom: 18px;
color: #666; color: #141414;
font-weight: 600; font-weight: 600;
font-size: 18px; font-size: 18px;
} }
@@ -450,8 +372,10 @@ function fnChangeLocale(e: any) {
color: #8c8c8c; color: #8c8c8c;
font-size: 16px; font-size: 16px;
} }
}
[data-theme='dark'] & .desc { [data-theme='dark'] .login-card {
& .title {
color: #999; color: #999;
} }
} }
@@ -461,28 +385,4 @@ function fnChangeLocale(e: any) {
margin: 0 auto; margin: 0 auto;
} }
} }
.footer {
position: absolute;
bottom: 0;
height: 32px;
padding: 0px 16px;
text-align: left;
background-color: rgba(255, 255, 255, 0.85);
width: 100%;
line-height: 32px;
&-copyright {
font-size: 14px;
color: rgba(0, 0, 0, 0.75);
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
}
}
[data-theme='dark'] .footer {
background-color: rgb(0 0 0 / 85%);
&-copyright {
color: rgba(255, 255, 255, 0.75);
}
}
</style> </style>

View File

@@ -496,7 +496,7 @@ onMounted(() => {
</a-card> </a-card>
</a-col> </a-col>
</a-row> </a-row>
<a-row> <a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24"> <a-col :lg="12" :md="12" :xs="24">
<a-card :bordered="false" :body-style="{ marginBottom: '24px' }"> <a-card :bordered="false" :body-style="{ marginBottom: '24px' }">
<!-- 插槽-卡片左侧侧 --> <!-- 插槽-卡片左侧侧 -->
@@ -554,7 +554,7 @@ onMounted(() => {
</a-card> </a-card>
</a-col> </a-col>
</a-row> </a-row>
<a-row> <a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24"> <a-col :lg="12" :md="12" :xs="24">
<a-card :bordered="false" :body-style="{ marginBottom: '24px' }"> <a-card :bordered="false" :body-style="{ marginBottom: '24px' }">
<!-- 插槽-卡片左侧侧 --> <!-- 插槽-卡片左侧侧 -->

View File

@@ -24,8 +24,9 @@ const graphG6Data = reactive<Record<string, any>>({
/**查询全部网元数据列表 */ /**查询全部网元数据列表 */
function fnRanderData() { function fnRanderData() {
if (!graphG6Dom.value) return; if (!graphG6Dom.value) return false;
graphG6.value = randerGroph(t, graphG6Dom.value, graphG6Data); graphG6.value = randerGroph(t, graphG6Dom.value, graphG6Data);
return true;
} }
/**网元状态调度器 */ /**网元状态调度器 */
@@ -140,17 +141,21 @@ function fnGetList(refresh: boolean = false) {
} }
}) })
.then(hasNeList => { .then(hasNeList => {
if (!hasNeList) return; if (!hasNeList) return false;
if (refresh) { if (refresh) {
// graphG6.value.get('canvas').set('localRefresh', true); // graphG6.value.get('canvas').set('localRefresh', true);
graphG6.value.destroy(); graphG6.value.destroy();
// graphG6.value.clear(); // graphG6.value.clear();
} }
fnRanderData(); return fnRanderData();
fnGetState(); })
interval10s.value = setInterval(() => { .then(randerGroph => {
fnGetState(); // 获取网元状态 if (!randerGroph) return;
}, 10_000); fnGetState().finally(() => {
interval10s.value = setInterval(() => {
fnGetState(); // 获取网元状态
}, 10_000);
});
}); });
} }

View File

@@ -413,7 +413,7 @@ onMounted(() => {
:loading="modalState.confirmLoading" :loading="modalState.confirmLoading"
@click.prevent="fnRecordStateBatch()" @click.prevent="fnRecordStateBatch()"
> >
<template #icon><SecurityScanOutlined /></template> <template #icon><CloudSyncOutlined /></template>
{{ t('views.ne.neLicense.reloadBatch') }} {{ t('views.ne.neLicense.reloadBatch') }}
</a-button> </a-button>
</a-space> </a-space>
@@ -505,19 +505,19 @@ onMounted(() => {
</template> </template>
<template v-if="column.key === 'id'"> <template v-if="column.key === 'id'">
<a-space :size="8" align="center"> <a-space :size="8" align="center">
<a-tooltip placement="topRight">
<template #title>{{ t('views.ne.neLicense.reload') }}</template>
<a-button type="link" @click.prevent="fnRecordState(record)">
<template #icon><CloudSyncOutlined /> </template>
</a-button>
</a-tooltip>
<a-tooltip placement="topRight"> <a-tooltip placement="topRight">
<template #title>{{ t('views.ne.neLicense.change') }}</template> <template #title>{{ t('views.ne.neLicense.change') }}</template>
<a-button <a-button
type="link" type="link"
@click.prevent="fnModalVisibleByEdit(record.id)" @click.prevent="fnModalVisibleByEdit(record.id)"
> >
<template #icon><InteractionOutlined /> </template> <template #icon><CloudUploadOutlined /> </template>
</a-button>
</a-tooltip>
<a-tooltip placement="topRight">
<template #title>{{ t('views.ne.neLicense.reload') }}</template>
<a-button type="link" @click.prevent="fnRecordState(record)">
<template #icon><SecurityScanOutlined /> </template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
</a-space> </a-space>

View File

@@ -22,9 +22,10 @@ import {
markRaw, markRaw,
nextTick, nextTick,
onBeforeUnmount, onBeforeUnmount,
h,
} from 'vue'; } from 'vue';
import { PageContainer } from 'antdv-pro-layout'; import { PageContainer } from 'antdv-pro-layout';
import { message, Modal } from 'ant-design-vue/es'; import { message, Modal, TableColumnType } from 'ant-design-vue/es';
import { SizeType } from 'ant-design-vue/es/config-provider'; import { SizeType } from 'ant-design-vue/es/config-provider';
import { MenuInfo } from 'ant-design-vue/es/menu/src/interface'; import { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import TableColumnsDnd from '@/components/TableColumnsDnd/index.vue'; import TableColumnsDnd from '@/components/TableColumnsDnd/index.vue';
@@ -41,7 +42,8 @@ import saveAs from 'file-saver';
import { generateColorRGBA } from '@/utils/generate-utils'; import { generateColorRGBA } from '@/utils/generate-utils';
import { OptionsType, WS } from '@/plugins/ws-websocket'; import { OptionsType, WS } from '@/plugins/ws-websocket';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
const neInfoStore = useNeInfoStore(); import { LineOutlined } from '@ant-design/icons-vue';
const neInfoStore = useNeInfoStore();
const route = useRoute(); const route = useRoute();
const { t, currentLocale } = useI18n(); const { t, currentLocale } = useI18n();
const ws = new WS(); const ws = new WS();
@@ -187,6 +189,51 @@ let state: StateType = reactive({
chartLegendSelectedFlag: false, chartLegendSelectedFlag: false,
}); });
// 存储每个指标的临时固定颜色
const kpiColors = new Map<string, string>();
//legend表格数据
const kpiStats: any = ref([]);
// 添加表格列定义
const statsColumns: TableColumnType<any>[] = [
{
title: '',
key: 'icon',
width: 50,
customRender: ({ record }: { record: any }) => {
return h(LineOutlined, {
style: {
color: kpiColors.get(record.kpiId) || '#000', // 使用与折线图相同的颜色
fontSize: '30px', // 增大图标尺寸到30px
fontWeight: 'bold', // 加粗
},
});
},
},
{
title: t('views.perfManage.kpiOverView.kpiName'),
dataIndex: 'title',
key: 'title',
width: '65%',
},
{
title: t('views.perfManage.kpiOverView.maxValue'),
dataIndex: 'max',
key: 'max',
width: '17%',
sorter: (a: any, b: any) => a.max - b.max, // 添加排序函数
sortDirections: ['ascend', 'descend'],
},
{
title: t('views.perfManage.kpiOverView.minValue'),
dataIndex: 'min',
key: 'min',
width: '17%',
sorter: (a: any, b: any) => a.min - b.min, // 添加排序函数
sortDirections: ['ascend', 'descend'],
},
];
/** /**
* 数据列表导出 * 数据列表导出
*/ */
@@ -320,6 +367,7 @@ function fnGetList() {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) { if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
tablePagination.total = res.data.length; tablePagination.total = res.data.length;
tableState.data = res.data; tableState.data = res.data;
return true; return true;
} }
return false; return false;
@@ -327,6 +375,27 @@ function fnGetList() {
.then(result => { .then(result => {
if (result) { if (result) {
fnRanderChartData(); fnRanderChartData();
//封装legend表格数据
kpiStats.value = [];
for (const columns of tableColumns.value) {
if (
columns.key === 'neName' ||
columns.key === 'startIndex' ||
columns.key === 'timeGroup'
) {
continue;
}
const values = tableState.data.map((item: any) => {
return item[columns.key] ? Number(item[columns.key]) : 0;
});
kpiStats.value.push({
kpiId: columns.key,
title: columns.title,
max: values.length > 0 ? Math.max(...values) : 0,
min: values.length > 0 ? Math.min(...values) : 0,
});
}
} }
}); });
} }
@@ -362,6 +431,7 @@ function fnRanderChart() {
boundaryGap: [0, '100%'], boundaryGap: [0, '100%'],
}, },
legend: { legend: {
show: false,
//图例垂直滚动 //图例垂直滚动
type: 'scroll', type: 'scroll',
orient: 'vertical', orient: 'vertical',
@@ -377,22 +447,12 @@ function fnRanderChart() {
}, },
grid: { grid: {
//网格区域边距 //网格区域边距
left: '10%', left: '7%',
right: '30%', right: '7%',
bottom: '20%', bottom: '7%',
containLabel: true,
}, },
dataZoom: [
{
//启用图表的数据缩放范围90%-100%
type: 'inside',
start: 90,
end: 100,
},
{
start: 90,
end: 100,
},
],
series: [], // 数据y轴 series: [], // 数据y轴
}; };
kpiChart.value.setOption(option); //设置图表配置项应用到kpiChart实例上 kpiChart.value.setOption(option); //设置图表配置项应用到kpiChart实例上
@@ -433,7 +493,8 @@ function fnRanderChartData() {
) { ) {
continue; continue;
} }
const color = generateColorRGBA(); const color = kpiColors.get(columns.key) || generateColorRGBA();
kpiColors.set(columns.key, color);
chartDataYSeriesData.push({ chartDataYSeriesData.push({
name: `${columns.title}`, name: `${columns.title}`,
key: `${columns.key}`, key: `${columns.key}`,
@@ -462,7 +523,6 @@ function fnRanderChartData() {
// 用降序就反转 // 用降序就反转
let orgData = tableState.data; let orgData = tableState.data;
console.log(orgData);
if (queryParams.sortOrder === 'desc') { if (queryParams.sortOrder === 'desc') {
orgData = orgData.toReversed(); orgData = orgData.toReversed();
} }
@@ -587,6 +647,37 @@ function wsMessage(res: Record<string, any>) {
}); });
} }
// 添加一个变量来跟踪当前选中的行
const selectedRow = ref<string | null>(null);
// 添加处理行点击的方法
function handleRowClick(record: any) {
if (selectedRow.value === record.kpiId) {
// 如果点击的是当前选中的行,则取消选中
selectedRow.value = null;
// 更新图表,显示所有指标
for (let key in chartLegendSelected) {
chartLegendSelected[key] = true;
}
} else {
// 选中新行
selectedRow.value = record.kpiId;
// 更新图表,只显示选中的指标
for (let key in chartLegendSelected) {
if (key === record.title) {
chartLegendSelected[key] = true;
} else {
chartLegendSelected[key] = false;
}
}
}
kpiChart.value.setOption({
legend: {
selected: chartLegendSelected,
},
});
}
onMounted(() => { onMounted(() => {
// 目前支持的 AMF AUSF MME MOCNGW NSSF SMF UDM UPF PCF // 目前支持的 AMF AUSF MME MOCNGW NSSF SMF UDM UPF PCF
// 获取网元网元列表 // 获取网元网元列表
@@ -791,7 +882,7 @@ onBeforeUnmount(() => {
</a-tooltip> </a-tooltip>
</a-space> </a-space>
<a-form layout="inline" v-show="!tableState.showTable"> <a-form layout="inline" v-show="!tableState.showTable">
<a-form-item <!-- <a-form-item
:label="t('views.perfManage.goldTarget.showChartSelected')" :label="t('views.perfManage.goldTarget.showChartSelected')"
name="chartLegendSelectedFlag" name="chartLegendSelectedFlag"
> >
@@ -803,7 +894,7 @@ onBeforeUnmount(() => {
@change="fnLegendSelected" @change="fnLegendSelected"
size="small" size="small"
/> />
</a-form-item> </a-form-item> -->
<a-form-item <a-form-item
:label="t('views.perfManage.goldTarget.realTimeData')" :label="t('views.perfManage.goldTarget.realTimeData')"
name="chartRealTime" name="chartRealTime"
@@ -844,10 +935,105 @@ onBeforeUnmount(() => {
<!-- 图表 --> <!-- 图表 -->
<div style="padding: 24px" v-show="!tableState.showTable"> <div style="padding: 24px" v-show="!tableState.showTable">
<div ref="kpiChartDom" style="height: 450px; width: 100%"></div> <div
ref="kpiChartDom"
class="chart-container"
style="height: 450px; width: 100%"
></div>
<div class="table-container">
<a-table
:columns="statsColumns"
:data-source="kpiStats"
:pagination="false"
:scroll="{ y: 250 }"
size="small"
:custom-row="
record => ({
onClick: () => handleRowClick(record),
class: record.kpiId === selectedRow ? 'selected-row' : '',
})
"
/>
</div>
</div> </div>
</a-card> </a-card>
</PageContainer> </PageContainer>
</template> </template>
<style lang="less" scoped></style> <style scoped>
.chart-container {
height: 800px;
width: 100%;
}
.table-container {
height: 282px;
width: 100%;
margin-top: 20px;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* 表格布局相关样式 */
:deep(.ant-table-wrapper),
:deep(.ant-table),
:deep(.ant-table-container),
:deep(.ant-table-content) {
flex: 1;
display: flex;
flex-direction: column;
}
:deep(.ant-table-body) {
flex: 1;
overflow-y: auto !important;
min-height: 0;
}
/* 表格行和表头样式 */
:deep(.ant-table-thead tr th),
:deep(.ant-table-tbody tr td) {
padding: 8px;
height: 40px;
}
/* 美化滚动条样式 */
:deep(.ant-table-body::-webkit-scrollbar) {
width: 6px;
height: 6px;
}
:deep(.ant-table-body::-webkit-scrollbar-thumb) {
background: #ccc;
border-radius: 3px;
}
:deep(.ant-table-body::-webkit-scrollbar-track) {
background: #f1f1f1;
border-radius: 3px;
}
[data-theme='dark'] :deep(.ant-table-body::-webkit-scrollbar-thumb) {
background: #4c4c4c;
}
[data-theme='dark'] :deep(.ant-table-body::-webkit-scrollbar-track) {
background: #2a2a2a;
}
/* 选中行样式 */
:deep(.selected-row) {
background-color: rgba(24, 144, 255, 0.1);
}
[data-theme='dark'] :deep(.selected-row) {
background-color: rgba(24, 144, 255, 0.2);
}
/* 鼠标悬停样式 */
:deep(.ant-table-tbody tr:hover) {
cursor: pointer;
}
</style>

View File

@@ -22,9 +22,10 @@ import {
markRaw, markRaw,
nextTick, nextTick,
onBeforeUnmount, onBeforeUnmount,
h,
} from 'vue'; } from 'vue';
import { PageContainer } from 'antdv-pro-layout'; import { PageContainer } from 'antdv-pro-layout';
import { message, Modal } from 'ant-design-vue/es'; import { message, Modal, TableColumnType } from 'ant-design-vue/es';
import { SizeType } from 'ant-design-vue/es/config-provider'; import { SizeType } from 'ant-design-vue/es/config-provider';
import { MenuInfo } from 'ant-design-vue/es/menu/src/interface'; import { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import TableColumnsDnd from '@/components/TableColumnsDnd/index.vue'; import TableColumnsDnd from '@/components/TableColumnsDnd/index.vue';
@@ -41,6 +42,7 @@ import { writeSheet } from '@/utils/execl-utils';
import saveAs from 'file-saver'; import saveAs from 'file-saver';
import { generateColorRGBA } from '@/utils/generate-utils'; import { generateColorRGBA } from '@/utils/generate-utils';
import { OptionsType, WS } from '@/plugins/ws-websocket'; import { OptionsType, WS } from '@/plugins/ws-websocket';
import { LineOutlined } from '@ant-design/icons-vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import dayjs, { Dayjs } from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
const neInfoStore = useNeInfoStore(); const neInfoStore = useNeInfoStore();
@@ -200,6 +202,51 @@ let state: StateType = reactive({
chartLegendSelectedFlag: false, chartLegendSelectedFlag: false,
}); });
// 存储每个指标的临时固定颜色
const kpiColors = new Map<string, string>();
//legend表格数据
const kpiStats: any = ref([]);
// 添加表格列定义
const statsColumns: TableColumnType<any>[] = [
{
title: '',
key: 'icon',
width: 50,
customRender: ({ record }: { record: any }) => {
return h(LineOutlined, {
style: {
color: kpiColors.get(record.kpiId) || '#000', // 使用与折线图相同的颜色
fontSize: '30px', // 增大图标尺寸到30px
fontWeight: 'bold', // 加粗
},
});
},
},
{
title: t('views.perfManage.kpiOverView.kpiName'),
dataIndex: 'title',
key: 'title',
width: '65%',
},
{
title: t('views.perfManage.kpiOverView.maxValue'),
dataIndex: 'max',
key: 'max',
width: '17%',
sorter: (a: any, b: any) => a.max - b.max, // 添加排序函数
sortDirections: ['ascend', 'descend'],
},
{
title: t('views.perfManage.kpiOverView.minValue'),
dataIndex: 'min',
key: 'min',
width: '17%',
sorter: (a: any, b: any) => a.min - b.min, // 添加排序函数
sortDirections: ['ascend', 'descend'],
},
];
/** /**
* 数据列表导出 * 数据列表导出
*/ */
@@ -342,6 +389,13 @@ function fnGetList() {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) { if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
tablePagination.total = res.data.length; tablePagination.total = res.data.length;
tableState.data = res.data; tableState.data = res.data;
if (!res.data.length) {
message.warning({
content: t('common.noData'),
duration: 2,
});
return false;
}
return true; return true;
} }
return false; return false;
@@ -349,6 +403,27 @@ function fnGetList() {
.then(result => { .then(result => {
if (result) { if (result) {
fnRanderChartData(); fnRanderChartData();
//封装legend表格数据
kpiStats.value = [];
for (const columns of tableColumns.value) {
if (
columns.key === 'neName' ||
columns.key === 'startIndex' ||
columns.key === 'timeGroup'
) {
continue;
}
const values = tableState.data.map((item: any) => {
return item[columns.key] ? Number(item[columns.key]) : 0;
});
kpiStats.value.push({
kpiId: columns.key,
title: columns.title,
max: values.length > 0 ? Math.max(...values) : 0,
min: values.length > 0 ? Math.min(...values) : 0,
});
}
} }
}); });
} }
@@ -381,6 +456,7 @@ function fnRanderChart() {
boundaryGap: [0, '100%'], boundaryGap: [0, '100%'],
}, },
legend: { legend: {
show: false,
type: 'scroll', type: 'scroll',
orient: 'vertical', orient: 'vertical',
top: 40, top: 40,
@@ -394,21 +470,12 @@ function fnRanderChart() {
selected: {}, selected: {},
}, },
grid: { grid: {
left: '10%', //网格区域边距
right: '30%', left: '7%',
bottom: '20%', right: '7%',
bottom: '7%',
containLabel: true,
}, },
dataZoom: [
{
type: 'inside',
start: 90,
end: 100,
},
{
start: 90,
end: 100,
},
],
series: [], // 数据y轴 series: [], // 数据y轴
}; };
kpiChart.value.setOption(option); kpiChart.value.setOption(option);
@@ -449,7 +516,8 @@ function fnRanderChartData() {
) { ) {
continue; continue;
} }
const color = generateColorRGBA(); const color = kpiColors.get(columns.key) || generateColorRGBA();
kpiColors.set(columns.key, color);
chartDataYSeriesData.push({ chartDataYSeriesData.push({
name: `${columns.title}`, name: `${columns.title}`,
key: `${columns.key}`, key: `${columns.key}`,
@@ -602,6 +670,37 @@ function wsMessage(res: Record<string, any>) {
}); });
} }
// 添加一个变量来跟踪当前选中的行
const selectedRow = ref<string | null>(null);
// 添加处理行点击的方法
function handleRowClick(record: any) {
if (selectedRow.value === record.kpiId) {
// 如果点击的是当前选中的行,则取消选中
selectedRow.value = null;
// 更新图表,显示所有指标
for (let key in chartLegendSelected) {
chartLegendSelected[key] = true;
}
} else {
// 选中新行
selectedRow.value = record.kpiId;
// 更新图表,只显示选中的指标
for (let key in chartLegendSelected) {
if (key === record.title) {
chartLegendSelected[key] = true;
} else {
chartLegendSelected[key] = false;
}
}
}
kpiChart.value.setOption({
legend: {
selected: chartLegendSelected,
},
});
}
onMounted(() => { onMounted(() => {
// 目前支持的 AMF AUSF MME MOCNGW NSSF SMF UDM UPF PCF // 目前支持的 AMF AUSF MME MOCNGW NSSF SMF UDM UPF PCF
// 获取网元网元列表 // 获取网元网元列表
@@ -790,7 +889,7 @@ onBeforeUnmount(() => {
</a-tooltip> </a-tooltip>
</a-space> </a-space>
<a-form layout="inline" v-show="!tableState.showTable"> <a-form layout="inline" v-show="!tableState.showTable">
<a-form-item <!-- <a-form-item
:label="t('views.perfManage.goldTarget.showChartSelected')" :label="t('views.perfManage.goldTarget.showChartSelected')"
name="chartLegendSelectedFlag" name="chartLegendSelectedFlag"
> >
@@ -802,7 +901,7 @@ onBeforeUnmount(() => {
@change="fnLegendSelected" @change="fnLegendSelected"
size="small" size="small"
/> />
</a-form-item> </a-form-item> -->
<a-form-item <a-form-item
:label="t('views.perfManage.goldTarget.realTimeData')" :label="t('views.perfManage.goldTarget.realTimeData')"
name="chartRealTime" name="chartRealTime"
@@ -838,10 +937,105 @@ onBeforeUnmount(() => {
<!-- 图表 --> <!-- 图表 -->
<div style="padding: 24px" v-show="!tableState.showTable"> <div style="padding: 24px" v-show="!tableState.showTable">
<div ref="kpiChartDom" style="height: 450px; width: 100%"></div> <div
ref="kpiChartDom"
class="chart-container"
style="height: 450px; width: 100%"
></div>
<div class="table-container">
<a-table
:columns="statsColumns"
:data-source="kpiStats"
:pagination="false"
:scroll="{ y: 250 }"
size="small"
:custom-row="
record => ({
onClick: () => handleRowClick(record),
class: record.kpiId === selectedRow ? 'selected-row' : '',
})
"
/>
</div>
</div> </div>
</a-card> </a-card>
</PageContainer> </PageContainer>
</template> </template>
<style lang="less" scoped></style> <style scoped>
.chart-container {
height: 800px;
width: 100%;
}
.table-container {
height: 282px;
width: 100%;
margin-top: 20px;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* 表格布局相关样式 */
:deep(.ant-table-wrapper),
:deep(.ant-table),
:deep(.ant-table-container),
:deep(.ant-table-content) {
flex: 1;
display: flex;
flex-direction: column;
}
:deep(.ant-table-body) {
flex: 1;
overflow-y: auto !important;
min-height: 0;
}
/* 表格行和表头样式 */
:deep(.ant-table-thead tr th),
:deep(.ant-table-tbody tr td) {
padding: 8px;
height: 40px;
}
/* 美化滚动条样式 */
:deep(.ant-table-body::-webkit-scrollbar) {
width: 6px;
height: 6px;
}
:deep(.ant-table-body::-webkit-scrollbar-thumb) {
background: #ccc;
border-radius: 3px;
}
:deep(.ant-table-body::-webkit-scrollbar-track) {
background: #f1f1f1;
border-radius: 3px;
}
[data-theme='dark'] :deep(.ant-table-body::-webkit-scrollbar-thumb) {
background: #4c4c4c;
}
[data-theme='dark'] :deep(.ant-table-body::-webkit-scrollbar-track) {
background: #2a2a2a;
}
/* 选中行样式 */
:deep(.selected-row) {
background-color: rgba(24, 144, 255, 0.1);
}
[data-theme='dark'] :deep(.selected-row) {
background-color: rgba(24, 144, 255, 0.2);
}
/* 鼠标悬停样式 */
:deep(.ant-table-tbody tr:hover) {
cursor: pointer;
}
</style>

View File

@@ -98,6 +98,7 @@ let tableColumns: ColumnsType = [
title: t('common.rowId'), title: t('common.rowId'),
dataIndex: 'configId', dataIndex: 'configId',
align: 'left', align: 'left',
width: 100,
}, },
{ {
title: t('views.system.config.configName'), title: t('views.system.config.configName'),

View File

@@ -1367,7 +1367,7 @@ onMounted(() => {
<IntlTelInput <IntlTelInput
v-model:value="modalState.from.phonenumber" v-model:value="modalState.from.phonenumber"
allow-clear allow-clear
:maxlength="16" :maxlength="20"
:placeholder="t('common.inputPlease')" :placeholder="t('common.inputPlease')"
></IntlTelInput> ></IntlTelInput>
</a-form-item> </a-form-item>

View File

@@ -80,6 +80,14 @@ async function fnIPerf() {
}); });
return; return;
} }
// 网元切换时重置
if (neType !== state.params.neType || neId !== state.params.neId) {
state.initialized = false;
state.params.neType = neType;
state.params.neId = neId;
}
// 软件版本检查 // 软件版本检查
state.params.neType = neType; state.params.neType = neType;
state.params.neId = neId; state.params.neId = neId;
@@ -88,7 +96,6 @@ async function fnIPerf() {
neId, neId,
version: state.data.version, version: state.data.version,
}); });
if (resVersion.code !== RESULT_CODE_SUCCESS) { if (resVersion.code !== RESULT_CODE_SUCCESS) {
Modal.confirm({ Modal.confirm({
title: t('common.tipTitle'), title: t('common.tipTitle'),
@@ -99,6 +106,7 @@ async function fnIPerf() {
} else { } else {
state.versionInfo = resVersion.data; state.versionInfo = resVersion.data;
} }
// 初始化的直接重发 // 初始化的直接重发
if (state.initialized) { if (state.initialized) {
fnResend(); fnResend();
@@ -145,8 +153,8 @@ function fnResend() {
if (!toolTerminal.value) return; if (!toolTerminal.value) return;
state.running = true; state.running = true;
toolTerminal.value.ctrlC(); toolTerminal.value.ctrlC();
toolTerminal.value.clear();
setTimeout(() => { setTimeout(() => {
toolTerminal.value.clear();
const data = JSON.parse(JSON.stringify(state.data)); const data = JSON.parse(JSON.stringify(state.data));
if (state.dataType === 'options') data.command = ''; if (state.dataType === 'options') data.command = '';
toolTerminal.value.send('iperf', data); toolTerminal.value.send('iperf', data);
@@ -180,9 +188,12 @@ function fnProcessMessage(data: string): string {
if (lestIndex !== -1) { if (lestIndex !== -1) {
text = text.substring(0, lestIndex); text = text.substring(0, lestIndex);
} }
if (text.endsWith('# ')) {
text = text.substring(0, text.lastIndexOf('\r\n') + 2);
}
// console.log({ parts, text }); // console.log({ parts, text });
if (parts[0].startsWith('iperf')) { if (parts.length > 1 && parts[0].startsWith('iperf')) {
return parts[0] + '\r\n' + text; return parts[0] + '\r\n' + text;
} }
return text; return text;

View File

@@ -72,13 +72,15 @@ async function fnPing() {
}); });
return; return;
} }
if (state.initialized) {
fnResend(); // 网元切换时重置
return; if (neType !== state.params.neType || neId !== state.params.neId) {
state.initialized = false;
state.params.neType = neType;
state.params.neId = neId;
} }
state.params.neType = neType; // 软件版本检查
state.params.neId = neId;
const resVersion = await pingV({ neType, neId }); const resVersion = await pingV({ neType, neId });
if (resVersion.code !== RESULT_CODE_SUCCESS) { if (resVersion.code !== RESULT_CODE_SUCCESS) {
message.warning({ message.warning({
@@ -89,6 +91,12 @@ async function fnPing() {
} else { } else {
state.versionInfo = resVersion.data; state.versionInfo = resVersion.data;
} }
// 初始化的直接重发
if (state.initialized) {
fnResend();
return;
}
state.initialized = true; state.initialized = true;
} }
@@ -108,8 +116,8 @@ function fnResend() {
if (!toolTerminal.value) return; if (!toolTerminal.value) return;
state.running = true; state.running = true;
toolTerminal.value.ctrlC(); toolTerminal.value.ctrlC();
toolTerminal.value.clear();
setTimeout(() => { setTimeout(() => {
toolTerminal.value.clear();
const data = JSON.parse(JSON.stringify(state.data)); const data = JSON.parse(JSON.stringify(state.data));
if (state.dataType === 'options') data.command = ''; if (state.dataType === 'options') data.command = '';
toolTerminal.value.send('ping', data); toolTerminal.value.send('ping', data);
@@ -143,9 +151,12 @@ function fnProcessMessage(data: string): string {
if (lestIndex !== -1) { if (lestIndex !== -1) {
text = text.substring(0, lestIndex); text = text.substring(0, lestIndex);
} }
if (text.endsWith('# ')) {
text = text.substring(0, text.lastIndexOf('\r\n') + 2);
}
// console.log({ parts, text }); // console.log({ parts, text });
if (parts[0].startsWith('ping')) { if (parts.length > 1 && parts[0].startsWith('ping')) {
return parts[0] + '\r\n' + text; return parts[0] + '\r\n' + text;
} }
return text; return text;
@@ -377,10 +388,7 @@ onBeforeUnmount(() => {});
<a-auto-complete <a-auto-complete
v-model:value="state.data.command" v-model:value="state.data.command"
:disabled="state.running" :disabled="state.running"
:options="[ :options="[{ value: '-help' }, { value: '-i 1 -c 4 8.8.8.8' }]"
{ value: '-help' },
{ value: '-i 1 -c 4 8.8.8.8' },
]"
:dropdown-match-select-width="500" :dropdown-match-select-width="500"
style="width: 100%" style="width: 100%"
> >