Merge remote-tracking branch 'origin/main' into multi-tenant
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { CACHE_SESSION_CRYPTO_API } from '@/constants/cache-keys-constants';
|
||||
import { sessionGet } from '@/utils/cache-session-utils';
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
// 登录方法
|
||||
@@ -7,7 +9,7 @@ export function login(data: Record<string, string>) {
|
||||
method: 'post',
|
||||
data: data,
|
||||
whithToken: false,
|
||||
crypto: true,
|
||||
crypto: sessionGet(CACHE_SESSION_CRYPTO_API) !== 'false',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,7 +24,7 @@ export function register(data: Record<string, any>) {
|
||||
method: 'post',
|
||||
data: data,
|
||||
whithToken: false,
|
||||
crypto: true,
|
||||
crypto: sessionGet(CACHE_SESSION_CRYPTO_API) !== 'false',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { CACHE_SESSION_CRYPTO_API } from '@/constants/cache-keys-constants';
|
||||
import { sessionGet } from '@/utils/cache-session-utils';
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
@@ -36,7 +38,7 @@ export function addNeInfo(data: Record<string, any>) {
|
||||
url: `/ne/info`,
|
||||
method: 'post',
|
||||
data: data,
|
||||
crypto: true,
|
||||
crypto: sessionGet(CACHE_SESSION_CRYPTO_API) !== 'false',
|
||||
timeout: 30_000,
|
||||
});
|
||||
}
|
||||
@@ -51,7 +53,7 @@ export function updateNeInfo(data: Record<string, any>) {
|
||||
url: `/ne/info`,
|
||||
method: 'put',
|
||||
data: data,
|
||||
crypto: true,
|
||||
crypto: sessionGet(CACHE_SESSION_CRYPTO_API) !== 'false',
|
||||
timeout: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**会话缓存-接口加密 */
|
||||
export const CACHE_SESSION_CRYPTO_API = 'cache:session:cryptoApi';
|
||||
|
||||
/**会话缓存-网络请求 */
|
||||
export const CACHE_SESSION_FATCH = 'cache:session:fatch';
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { getSysConf } from '@/api';
|
||||
import { CACHE_LOCAL_I18N } from '@/constants/cache-keys-constants';
|
||||
import { CACHE_LOCAL_I18N, CACHE_SESSION_CRYPTO_API } from '@/constants/cache-keys-constants';
|
||||
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
|
||||
import { removeToken } from '@/plugins/auth-token';
|
||||
import { parseUrlPath } from '@/plugins/file-static-url';
|
||||
import { localGet, localSet } from '@/utils/cache-local-utils';
|
||||
import { sessionSet } from '@/utils/cache-session-utils';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
/**应用参数类型 */
|
||||
@@ -22,6 +23,8 @@ type AppStore = {
|
||||
bootloader: boolean;
|
||||
// 用户登录认证
|
||||
loginAuth: boolean;
|
||||
// 用户接口加密
|
||||
cryptoApi: boolean;
|
||||
// 序列号
|
||||
serialNum: string;
|
||||
/**应用版权声明 */
|
||||
@@ -55,6 +58,7 @@ const useAppStore = defineStore('app', {
|
||||
buildTime: `-`,
|
||||
bootloader: false,
|
||||
loginAuth: true,
|
||||
cryptoApi: true,
|
||||
serialNum: `-`,
|
||||
copyright: `Copyright ©2023 For ${import.meta.env.VITE_APP_NAME}`,
|
||||
logoType: 'icon',
|
||||
@@ -88,7 +92,9 @@ const useAppStore = defineStore('app', {
|
||||
if (this.bootloader) {
|
||||
removeToken();
|
||||
}
|
||||
this.loginAuth = res.data.loginAuth !== 'false' ;
|
||||
this.loginAuth = res.data.loginAuth !== 'false';
|
||||
this.cryptoApi = res.data.cryptoApi !== 'false';
|
||||
sessionSet(CACHE_SESSION_CRYPTO_API, res.data.cryptoApi);
|
||||
this.serialNum = res.data.serialNum;
|
||||
this.appName = res.data.title;
|
||||
this.copyright = res.data.copyright;
|
||||
|
||||
@@ -73,8 +73,8 @@ const option = {
|
||||
const downlinkValueF = parseSizeFromByte(downlinkValue);
|
||||
return `
|
||||
<div style="font-weight: bold;">${title}</div>
|
||||
<div>Uplink: ${uplinkValueF}</div>
|
||||
<div>Downlink: ${downlinkValueF}</div>
|
||||
<div>Uplink: ${uplinkValueF}</div>
|
||||
`;
|
||||
},
|
||||
},
|
||||
@@ -95,7 +95,7 @@ const option = {
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
show: true,
|
||||
show: false,
|
||||
realtime: true,
|
||||
start: 0,
|
||||
end: 100,
|
||||
@@ -113,13 +113,13 @@ const option = {
|
||||
{
|
||||
left: '10%',
|
||||
right: 50,
|
||||
height: '30%',
|
||||
height: '40%',
|
||||
},
|
||||
{
|
||||
left: '10%',
|
||||
right: 50,
|
||||
top: '50%',
|
||||
height: '30%',
|
||||
height: '40%',
|
||||
},
|
||||
],
|
||||
xAxis: [
|
||||
@@ -129,7 +129,7 @@ const option = {
|
||||
axisLine: { onZero: true },
|
||||
data: [], // x轴初始数据
|
||||
axisLabel: {
|
||||
show: true, // 显示标签
|
||||
show: false, // 显示标签
|
||||
rotate: 15, // 设置倾斜角度(如15度)
|
||||
},
|
||||
},
|
||||
@@ -147,30 +147,30 @@ const option = {
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
name: 'Uplink (Byte)',
|
||||
name: 'Downlink (Byte)',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
gridIndex: 1,
|
||||
name: 'Downlink (Byte)',
|
||||
name: 'Uplink (Byte)',
|
||||
type: 'value',
|
||||
inverse: true,
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: 'Uplink',
|
||||
name: 'Downlink',
|
||||
type: 'line',
|
||||
data: [], // y轴初始数据
|
||||
symbol: 'circle', // 数据点形状
|
||||
symbolSize: 6, // 数据点大小
|
||||
smooth: true, // 平滑曲线
|
||||
color: 'rgb(17, 178, 255)',
|
||||
color: 'rgb(0, 190, 99)',
|
||||
areaStyle: {
|
||||
color: {
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(17, 178, 255, .5)' },
|
||||
{ offset: 1, color: 'rgba(17, 178, 255, 0.5)' },
|
||||
{ offset: 0, color: 'rgba(0, 190, 99, .5)' },
|
||||
{ offset: 1, color: 'rgba(0, 190, 99, 0.5)' },
|
||||
],
|
||||
x: 0,
|
||||
y: 0,
|
||||
@@ -184,7 +184,7 @@ const option = {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Downlink',
|
||||
name: 'Uplink',
|
||||
type: 'line',
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
@@ -192,12 +192,12 @@ const option = {
|
||||
symbol: 'circle', // 数据点形状
|
||||
symbolSize: 6, // 数据点大小
|
||||
smooth: true, // 平滑曲线
|
||||
color: 'rgb(0, 190, 99)',
|
||||
color: 'rgb(17, 178, 255)',
|
||||
areaStyle: {
|
||||
color: {
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(0, 190, 99, .5)' },
|
||||
{ offset: 1, color: 'rgba(0, 190, 99, 0.5)' },
|
||||
{ offset: 0, color: 'rgba(17, 178, 255, .5)' },
|
||||
{ offset: 1, color: 'rgba(17, 178, 255, 0.5)' },
|
||||
],
|
||||
x: 0,
|
||||
y: 0,
|
||||
@@ -267,6 +267,7 @@ let queryParams = reactive({
|
||||
neType: 'SMF',
|
||||
neId: '001',
|
||||
subscriberID: '',
|
||||
dnn: '',
|
||||
sortField: 'timestamp',
|
||||
sortOrder: 'desc',
|
||||
/**开始时间 */
|
||||
@@ -281,6 +282,8 @@ let queryParams = reactive({
|
||||
|
||||
/**查询参数重置 */
|
||||
function fnQueryReset() {
|
||||
queryParams.subscriberID = '';
|
||||
queryParams.dnn = '';
|
||||
queryRangePicker.value = [dayjs().startOf('hour'), dayjs().endOf('hour')];
|
||||
fnGetList(1);
|
||||
}
|
||||
@@ -611,21 +614,15 @@ onBeforeUnmount(() => {
|
||||
></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"
|
||||
:presets="rangePickerPresets"
|
||||
:bordered="true"
|
||||
:allow-clear="false"
|
||||
style="width: 100%"
|
||||
:show-time="{ format: 'HH:mm:ss' }"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-form-item label="DNN" name="dnn">
|
||||
<a-input
|
||||
v-model:value="queryParams.dnn"
|
||||
allow-clear
|
||||
:placeholder="t('common.inputPlease')"
|
||||
:maxlength="40"
|
||||
:disabled="state.loading"
|
||||
></a-range-picker>
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="4" :md="12" :xs="24">
|
||||
@@ -650,6 +647,23 @@ onBeforeUnmount(() => {
|
||||
</a-space>
|
||||
</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"
|
||||
:presets="rangePickerPresets"
|
||||
:bordered="true"
|
||||
:allow-clear="false"
|
||||
style="width: 100%"
|
||||
:show-time="{ format: 'HH:mm:ss' }"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
:disabled="state.loading"
|
||||
></a-range-picker>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
@@ -313,6 +313,7 @@ let modalState: ModalStateType = reactive({
|
||||
address: '',
|
||||
name: '',
|
||||
position: '',
|
||||
state: undefined,
|
||||
},
|
||||
confirmLoading: false,
|
||||
});
|
||||
@@ -481,6 +482,7 @@ onMounted(() => {
|
||||
v-model:value="queryParams.state"
|
||||
:options="nbState"
|
||||
:placeholder="t('common.selectPlease')"
|
||||
@change="fnGetList"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
@@ -538,12 +540,25 @@ onMounted(() => {
|
||||
<template #extra>
|
||||
<a-space :size="8" align="center">
|
||||
<div>
|
||||
{{ t('views.neData.baseStation.online') }}:
|
||||
<strong style="color: green">{{ stateNum[0] }} </strong>
|
||||
{{ t('views.neData.baseStation.offline') }}:
|
||||
<strong style="color: red">
|
||||
{{ stateNum[1] }}
|
||||
</strong>
|
||||
<template
|
||||
v-if="
|
||||
queryParams.state === undefined || queryParams.state === 'ON'
|
||||
"
|
||||
>
|
||||
{{ t('views.neData.baseStation.online') }}:
|
||||
<strong style="color: green">{{ stateNum[0] }} </strong>
|
||||
</template>
|
||||
<template
|
||||
v-if="
|
||||
queryParams.state === undefined || queryParams.state === 'OFF'
|
||||
"
|
||||
>
|
||||
|
||||
{{ t('views.neData.baseStation.offline') }}:
|
||||
<strong style="color: red">
|
||||
{{ stateNum[1] }}
|
||||
</strong>
|
||||
</template>
|
||||
</div>
|
||||
<a-tooltip>
|
||||
<template #title>{{ t('common.reloadText') }}</template>
|
||||
@@ -627,6 +642,7 @@ onMounted(() => {
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="!modalState.from.state"
|
||||
:label="t('views.neData.baseStation.address')"
|
||||
name="address"
|
||||
v-bind="modalStateFrom.validateInfos.address"
|
||||
|
||||
@@ -53,8 +53,6 @@ const route = useRoute();
|
||||
const { t, currentLocale } = useI18n();
|
||||
const ws = new WS();
|
||||
|
||||
|
||||
|
||||
echarts.use([
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
@@ -449,20 +447,19 @@ function fnGetList() {
|
||||
return item[columns.key] ? Number(item[columns.key]) : 0;
|
||||
});
|
||||
|
||||
|
||||
// 计算总值
|
||||
const total = Number(values.reduce((sum, val) => sum + val, 0).toFixed(2));
|
||||
const total = Number(
|
||||
values.reduce((sum, val) => sum + val, 0).toFixed(2)
|
||||
);
|
||||
|
||||
// 计算平均值
|
||||
const avg = values.length > 0
|
||||
? Number((total / values.length).toFixed(2))
|
||||
: 0;
|
||||
const avg = values.length > 0 ? Number((total / values.length).toFixed(2)) : 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,
|
||||
avg
|
||||
avg,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -487,16 +484,19 @@ function fnRanderChart() {
|
||||
return [pt[0], '10%'];
|
||||
},
|
||||
confine: true, // 限制 tooltip 显示范围
|
||||
backgroundColor: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? 'rgba(48, 48, 48, 0.8)'
|
||||
: 'rgba(255, 255, 255, 0.9)',
|
||||
borderColor: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#555'
|
||||
: '#ddd',
|
||||
backgroundColor:
|
||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? 'rgba(48, 48, 48, 0.8)'
|
||||
: 'rgba(255, 255, 255, 0.9)',
|
||||
borderColor:
|
||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#555'
|
||||
: '#ddd',
|
||||
textStyle: {
|
||||
color: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333'
|
||||
color:
|
||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333',
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
@@ -633,29 +633,31 @@ function fnRanderChartData() {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
axisLabel: {
|
||||
color: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333'
|
||||
color:
|
||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333',
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: getSplitLineColor()
|
||||
}
|
||||
color: getSplitLineColor(),
|
||||
},
|
||||
},
|
||||
data: chartDataXAxisData,
|
||||
},
|
||||
yAxis: {
|
||||
axisLabel: {
|
||||
color: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333'
|
||||
color:
|
||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333',
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: getSplitLineColor()
|
||||
}
|
||||
color: getSplitLineColor(),
|
||||
},
|
||||
},
|
||||
},
|
||||
series: chartDataYSeriesData,
|
||||
@@ -784,59 +786,49 @@ function handleRowClick(record: any) {
|
||||
// 监听主题变化
|
||||
watch(
|
||||
() => layoutStore.proConfig.theme, // 监听的值
|
||||
(newValue) => {
|
||||
newValue => {
|
||||
if (kpiChart.value) {
|
||||
const splitLineColor = getSplitLineColor();
|
||||
// 绘制图数据
|
||||
kpiChart.value.setOption(
|
||||
{
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
position: function (pt: any) {
|
||||
return [pt[0], '10%'];
|
||||
},
|
||||
confine: true, // 限制 tooltip 显示范围
|
||||
backgroundColor: newValue === 'dark'
|
||||
kpiChart.value.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
position: function (pt: any) {
|
||||
return [pt[0], '10%'];
|
||||
},
|
||||
confine: true, // 限制 tooltip 显示范围
|
||||
backgroundColor:
|
||||
newValue === 'dark'
|
||||
? 'rgba(48, 48, 48, 0.8)'
|
||||
: 'rgba(255, 255, 255, 0.9)',
|
||||
borderColor: newValue === 'dark'
|
||||
? '#555'
|
||||
: '#ddd',
|
||||
textStyle: {
|
||||
color: newValue === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333'
|
||||
borderColor: newValue === 'dark' ? '#555' : '#ddd',
|
||||
textStyle: {
|
||||
color: newValue === 'dark' ? '#CACADA' : '#333',
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
axisLabel: {
|
||||
color: newValue === 'dark' ? '#CACADA' : '#333',
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: splitLineColor,
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
axisLabel: {
|
||||
color: newValue === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333'
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: splitLineColor
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
axisLabel: {
|
||||
color: newValue === 'dark' ? '#CACADA' : '#333',
|
||||
},
|
||||
yAxis: {
|
||||
axisLabel: {
|
||||
color: newValue === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333'
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: splitLineColor,
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: splitLineColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -924,28 +916,50 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<PageContainer>
|
||||
<a-card v-show="tableState.seached" :bordered="false" :body-style="{ marginBottom: '24px', paddingBottom: 0 }">
|
||||
<a-card
|
||||
v-show="tableState.seached"
|
||||
:bordered="false"
|
||||
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
|
||||
>
|
||||
<!-- 表格搜索栏 -->
|
||||
<a-form :model="queryParams" name="queryParamsFrom" layout="horizontal">
|
||||
<a-row :gutter="16">
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-form-item name="neType" :label="t('views.ne.common.neType')">
|
||||
<a-cascader v-model:value="state.neType" :options="neCascaderOptions" :allow-clear="false"
|
||||
:placeholder="t('common.selectPlease')" />
|
||||
<a-cascader
|
||||
v-model:value="state.neType"
|
||||
:options="neCascaderOptions"
|
||||
:allow-clear="false"
|
||||
:placeholder="t('common.selectPlease')"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="10" :md="12" :xs="24">
|
||||
<a-form-item :label="t('views.perfManage.goldTarget.timeFrame')" name="timeFrame">
|
||||
<a-range-picker v-model:value="queryRangePicker" bordered :allow-clear="false"
|
||||
:show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" value-format="x" :presets="ranges"
|
||||
style="width: 100%"></a-range-picker>
|
||||
<a-form-item
|
||||
:label="t('views.perfManage.goldTarget.timeFrame')"
|
||||
name="timeFrame"
|
||||
>
|
||||
<a-range-picker
|
||||
v-model:value="queryRangePicker"
|
||||
bordered
|
||||
:allow-clear="false"
|
||||
:show-time="{ format: 'HH:mm:ss' }"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="x"
|
||||
:presets="ranges"
|
||||
style="width: 100%"
|
||||
></a-range-picker>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :lg="2" :md="12" :xs="24">
|
||||
<a-form-item>
|
||||
<a-space :size="8">
|
||||
<a-button type="primary" :loading="tableState.loading" @click.prevent="fnGetListTitle()">
|
||||
<a-button
|
||||
type="primary"
|
||||
:loading="tableState.loading"
|
||||
@click.prevent="fnGetListTitle()"
|
||||
>
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
@@ -962,7 +976,11 @@ onBeforeUnmount(() => {
|
||||
<!-- 插槽-卡片左侧侧 -->
|
||||
<template #title>
|
||||
<a-space :size="8" align="center">
|
||||
<a-button type="primary" :loading="tableState.loading" @click.prevent="fnChangShowType()">
|
||||
<a-button
|
||||
type="primary"
|
||||
:loading="tableState.loading"
|
||||
@click.prevent="fnChangShowType()"
|
||||
>
|
||||
<template #icon>
|
||||
<AreaChartOutlined />
|
||||
</template>
|
||||
@@ -972,8 +990,12 @@ onBeforeUnmount(() => {
|
||||
: t('views.perfManage.goldTarget.kpiTableTitle')
|
||||
}}
|
||||
</a-button>
|
||||
<a-button type="dashed" :loading="tableState.loading" @click.prevent="fnRecordExport()"
|
||||
v-show="tableState.showTable">
|
||||
<a-button
|
||||
type="dashed"
|
||||
:loading="tableState.loading"
|
||||
@click.prevent="fnRecordExport()"
|
||||
v-show="tableState.showTable"
|
||||
>
|
||||
<template #icon>
|
||||
<ExportOutlined />
|
||||
</template>
|
||||
@@ -993,8 +1015,12 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<TableColumnsDnd v-if="tableColumns.length > 0" :cache-id="`kpiTarget_${state.neType[0]}`"
|
||||
:columns="tableColumns" v-model:columns-dnd="tableColumnsDnd"></TableColumnsDnd>
|
||||
<TableColumnsDnd
|
||||
v-if="tableColumns.length > 0"
|
||||
:cache-id="`kpiTarget_${state.neType[0]}`"
|
||||
:columns="tableColumns"
|
||||
v-model:columns-dnd="tableColumnsDnd"
|
||||
></TableColumnsDnd>
|
||||
<a-tooltip>
|
||||
<template #title>{{ t('common.sizeText') }}</template>
|
||||
<a-dropdown trigger="click" placement="bottomRight">
|
||||
@@ -1004,7 +1030,10 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu :selected-keys="[tableState.size as string]" @click="fnTableSize">
|
||||
<a-menu
|
||||
:selected-keys="[tableState.size as string]"
|
||||
@click="fnTableSize"
|
||||
>
|
||||
<a-menu-item key="default">
|
||||
{{ t('common.size.default') }}
|
||||
</a-menu-item>
|
||||
@@ -1033,32 +1062,108 @@ onBeforeUnmount(() => {
|
||||
size="small"
|
||||
/>
|
||||
</a-form-item> -->
|
||||
<a-form-item :label="t('views.perfManage.goldTarget.realTimeData')" name="chartRealTime">
|
||||
<a-switch :disabled="tableState.loading" v-model:checked="state.chartRealTime"
|
||||
:checked-children="t('common.switch.open')" :un-checked-children="t('common.switch.shut')"
|
||||
@change="fnRealTimeSwitch" size="small" />
|
||||
<a-form-item
|
||||
:label="t('views.perfManage.goldTarget.realTimeData')"
|
||||
name="chartRealTime"
|
||||
>
|
||||
<a-switch
|
||||
:disabled="tableState.loading"
|
||||
v-model:checked="state.chartRealTime"
|
||||
:checked-children="t('common.switch.open')"
|
||||
:un-checked-children="t('common.switch.shut')"
|
||||
@change="fnRealTimeSwitch"
|
||||
size="small"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<!-- 表格列表 -->
|
||||
<a-table v-show="tableState.showTable" class="table" row-key="id" :columns="tableColumnsDnd"
|
||||
:loading="tableState.loading" :data-source="tableState.data" :size="tableState.size"
|
||||
:pagination="tablePagination" :scroll="{ x: tableColumnsDnd.length * 200, y: 'calc(100vh - 480px)' }"
|
||||
@resizeColumn="(w: number, col: any) => (col.width = w)" :show-expand-column="false" @change="fnTableChange">
|
||||
<a-table
|
||||
v-show="tableState.showTable"
|
||||
class="table"
|
||||
row-key="id"
|
||||
:columns="tableColumnsDnd"
|
||||
:loading="tableState.loading"
|
||||
:data-source="tableState.data"
|
||||
:size="tableState.size"
|
||||
:pagination="tablePagination"
|
||||
:scroll="{ x: tableColumnsDnd.length * 200, y: 'calc(100vh - 480px)' }"
|
||||
@resizeColumn="(w: number, col: any) => (col.width = w)"
|
||||
:show-expand-column="false"
|
||||
@change="fnTableChange"
|
||||
>
|
||||
</a-table>
|
||||
|
||||
<!-- 图表 -->
|
||||
<div style="padding: 24px" v-show="!tableState.showTable">
|
||||
<div ref="kpiChartDom" class="chart-container" 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: selectedRow.includes(record.kpiId) ? 'selected-row' : ''
|
||||
})
|
||||
" />
|
||||
<a-table
|
||||
:columns="statsColumns"
|
||||
:data-source="kpiStats"
|
||||
:pagination="false"
|
||||
:scroll="{ y: 250 }"
|
||||
size="small"
|
||||
:custom-row="
|
||||
record => ({
|
||||
onClick: () => handleRowClick(record),
|
||||
class: selectedRow.includes(record.kpiId) ? 'selected-row' : '',
|
||||
})
|
||||
"
|
||||
>
|
||||
<template #headerCell="{ column }">
|
||||
<template v-if="column.key === 'total'">
|
||||
<span>
|
||||
{{ t('views.perfManage.kpiOverView.totalValue') }}
|
||||
<a-tooltip placement="bottom">
|
||||
<template #title>
|
||||
<span>Sum within Time Range</span>
|
||||
</template>
|
||||
<InfoCircleOutlined />
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'avg'">
|
||||
<span>
|
||||
{{ t('views.perfManage.kpiOverView.avgValue') }}
|
||||
<a-tooltip placement="bottom">
|
||||
<template #title>
|
||||
<span>Average value over the time range</span>
|
||||
</template>
|
||||
<InfoCircleOutlined />
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'max'">
|
||||
<span>
|
||||
{{ t('views.perfManage.kpiOverView.maxValue') }}
|
||||
<a-tooltip placement="bottom">
|
||||
<template #title>
|
||||
<span>Maximum value in time range</span>
|
||||
</template>
|
||||
<InfoCircleOutlined />
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'min'">
|
||||
<span>
|
||||
{{ t('views.perfManage.kpiOverView.minValue') }}
|
||||
<a-tooltip placement="bottom">
|
||||
<template #title>
|
||||
<span>Minimum value in the time range</span>
|
||||
</template>
|
||||
<InfoCircleOutlined />
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick, computed, h } from 'vue';
|
||||
import * as echarts from 'echarts/core';
|
||||
import { GridComponent, TooltipComponent, TitleComponent,LegendComponent } from 'echarts/components';
|
||||
import {
|
||||
GridComponent,
|
||||
TooltipComponent,
|
||||
TitleComponent,
|
||||
LegendComponent,
|
||||
} from 'echarts/components';
|
||||
import { LineChart } from 'echarts/charts';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import { getKPITitle, listKPIData } from '@/api/perfManage/goldTarget';
|
||||
import useI18n from '@/hooks/useI18n';
|
||||
import { message,} from 'ant-design-vue';
|
||||
import { RESULT_CODE_ERROR, RESULT_CODE_SUCCESS } from '@/constants/result-constants';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
RESULT_CODE_ERROR,
|
||||
RESULT_CODE_SUCCESS,
|
||||
} from '@/constants/result-constants';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import dayjs from 'dayjs';
|
||||
import { OptionsType, WS } from '@/plugins/ws-websocket';
|
||||
@@ -16,12 +24,12 @@ import { LineOutlined } from '@ant-design/icons-vue';
|
||||
import { TableColumnType } from 'ant-design-vue';
|
||||
const { t, currentLocale } = useI18n();
|
||||
//定义KPI接口
|
||||
interface KPIBase{
|
||||
interface KPIBase {
|
||||
kpiId: string;
|
||||
title: string;
|
||||
}
|
||||
//继承接口
|
||||
interface KPIColumn extends KPIBase{
|
||||
interface KPIColumn extends KPIBase {
|
||||
dataIndex: string;
|
||||
key: string;
|
||||
neType: string;
|
||||
@@ -34,8 +42,8 @@ interface ChartDataItem {
|
||||
const tableLoading = ref(false);
|
||||
const rangeLoading = ref(false);
|
||||
//网元类型定义
|
||||
const ALL_NE_TYPES = ['AMF','SMF','UPF','MME','IMS','SMSC'] as const;
|
||||
type NeType= typeof ALL_NE_TYPES[number];
|
||||
const ALL_NE_TYPES = ['AMF', 'SMF', 'UPF', 'MME', 'IMS', 'SMSC'] as const;
|
||||
type NeType = (typeof ALL_NE_TYPES)[number];
|
||||
|
||||
echarts.use([
|
||||
LineChart,
|
||||
@@ -61,12 +69,19 @@ const ws = ref<WS | null>(null);
|
||||
// [t('views.monitor.monitor.today')]: [dayjs().startOf('day'), dayjs()],
|
||||
// });
|
||||
const ranges = ref([
|
||||
{label:t('views.perfManage.customTarget.sixHoursAgo'),value:[dayjs().subtract(6, 'hours'),
|
||||
dayjs(),]},
|
||||
{label:t('views.perfManage.customTarget.threeHoursAgo'),value:[dayjs().subtract(3, 'hours'),
|
||||
dayjs(),]},
|
||||
{label:t('views.monitor.monitor.today'),value:[dayjs().startOf('day'), dayjs()]},
|
||||
])
|
||||
{
|
||||
label: t('views.perfManage.customTarget.sixHoursAgo'),
|
||||
value: [dayjs().subtract(6, 'hours'), dayjs()],
|
||||
},
|
||||
{
|
||||
label: t('views.perfManage.customTarget.threeHoursAgo'),
|
||||
value: [dayjs().subtract(3, 'hours'), dayjs()],
|
||||
},
|
||||
{
|
||||
label: t('views.monitor.monitor.today'),
|
||||
value: [dayjs().startOf('day'), dayjs()],
|
||||
},
|
||||
]);
|
||||
//日期范围响应式变量
|
||||
const dateRange = ref<[string, string]>([
|
||||
dayjs().subtract(1, 'hour').startOf('hour').valueOf().toString(), // 上一小时开始
|
||||
@@ -118,8 +133,8 @@ const TARGET_KPI_IDS: Record<NeType, string[]> = {
|
||||
// 实时数据开关函数
|
||||
const fnRealTimeSwitch = (bool: boolean) => {
|
||||
if (bool) {
|
||||
if(!chart){
|
||||
isRealtime.value=false;
|
||||
if (!chart) {
|
||||
isRealtime.value = false;
|
||||
return;
|
||||
}
|
||||
if (!ws.value) {
|
||||
@@ -127,7 +142,7 @@ const fnRealTimeSwitch = (bool: boolean) => {
|
||||
}
|
||||
chartData.value = [];
|
||||
|
||||
tableLoading.value =true;
|
||||
tableLoading.value = true;
|
||||
const options: OptionsType = {
|
||||
url: '/ws',
|
||||
params: {
|
||||
@@ -149,8 +164,8 @@ const wsError = () => {
|
||||
message.error(t('common.websocketError'));
|
||||
};
|
||||
|
||||
const handleWebSocketMessage = (kpiEvent:any)=>{
|
||||
if(!kpiEvent)return;
|
||||
const handleWebSocketMessage = (kpiEvent: any) => {
|
||||
if (!kpiEvent) return;
|
||||
|
||||
// 构造新的数据点
|
||||
const newData: ChartDataItem = {
|
||||
@@ -159,40 +174,42 @@ const handleWebSocketMessage = (kpiEvent:any)=>{
|
||||
|
||||
// 添加已选中的指标的数据
|
||||
selectedKPIs.value.forEach(kpiId => {
|
||||
newData[kpiId] = Number(kpiEvent[kpiId])||0;
|
||||
newData[kpiId] = Number(kpiEvent[kpiId]) || 0;
|
||||
});
|
||||
|
||||
// 更新数据
|
||||
updateChartData(newData);
|
||||
if(tableLoading.value){
|
||||
tableLoading.value=false;
|
||||
if (tableLoading.value) {
|
||||
tableLoading.value = false;
|
||||
}
|
||||
};
|
||||
//成功回调
|
||||
const wsMessage = (res:Record<string,any>)=>{
|
||||
if(!chart){
|
||||
const wsMessage = (res: Record<string, any>) => {
|
||||
if (!chart) {
|
||||
return;
|
||||
}
|
||||
const{code,data}=res;
|
||||
if(code===RESULT_CODE_ERROR||!data?.groupId)return;
|
||||
const { code, data } = res;
|
||||
if (code === RESULT_CODE_ERROR || !data?.groupId) return;
|
||||
handleWebSocketMessage(data.data);
|
||||
};
|
||||
// 添加数据处理函数
|
||||
const processChartData = (rawData: any[]) => {
|
||||
const groupedData = new Map<string, any>();//数据按时间分组
|
||||
const groupedData = new Map<string, any>(); //数据按时间分组
|
||||
|
||||
rawData.forEach(item => {//合并相同时间点的数据
|
||||
rawData.forEach(item => {
|
||||
//合并相同时间点的数据
|
||||
const timeKey = item.timeGroup;
|
||||
if (!groupedData.has(timeKey)) {//按时间排序
|
||||
if (!groupedData.has(timeKey)) {
|
||||
//按时间排序
|
||||
groupedData.set(timeKey, { timeGroup: timeKey });
|
||||
}
|
||||
Object.assign(groupedData.get(timeKey), item);
|
||||
});
|
||||
|
||||
|
||||
return Array.from(groupedData.values())
|
||||
.sort((a, b) => Number(a.timeGroup) - Number(b.timeGroup))
|
||||
.map(item => {//转换成图表需要的格式
|
||||
.map(item => {
|
||||
//转换成图表需要的格式
|
||||
const dataItem: ChartDataItem = { date: item.timeGroup.toString() };
|
||||
selectedKPIs.value.forEach(kpiId => {
|
||||
dataItem[kpiId] = Number(item[kpiId]) || 0;
|
||||
@@ -202,21 +219,21 @@ const processChartData = (rawData: any[]) => {
|
||||
};
|
||||
// 获取图表数据方法
|
||||
const fetchChartData = async () => {
|
||||
if(kpiColumns.value.length===0){
|
||||
if (kpiColumns.value.length === 0) {
|
||||
updateChart();
|
||||
return;
|
||||
}
|
||||
tableLoading.value=true;
|
||||
rangeLoading.value=true;
|
||||
tableLoading.value = true;
|
||||
rangeLoading.value = true;
|
||||
try {
|
||||
const[startTime,endTime]=dateRange.value;
|
||||
const [startTime, endTime] = dateRange.value;
|
||||
if (!startTime || !endTime) {
|
||||
console.warn('Invalid date range:', dateRange.value);
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建并行请求数组
|
||||
const requests = ALL_NE_TYPES.map(async (neType) => {
|
||||
const requests = ALL_NE_TYPES.map(async neType => {
|
||||
const params = {
|
||||
neType,
|
||||
neId: '001',
|
||||
@@ -224,7 +241,7 @@ const fetchChartData = async () => {
|
||||
endTime: String(endTime),
|
||||
sortField: 'timeGroup',
|
||||
sortOrder: 'asc',
|
||||
interval: 60*15,
|
||||
interval: 60 * 15,
|
||||
kpiIds: TARGET_KPI_IDS[neType].join(','),
|
||||
};
|
||||
|
||||
@@ -250,9 +267,9 @@ const fetchChartData = async () => {
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch chart data:', error);
|
||||
message.error(t('common.getInfoFail'));
|
||||
}finally {
|
||||
tableLoading.value=false;
|
||||
rangeLoading.value=false;
|
||||
} finally {
|
||||
tableLoading.value = false;
|
||||
rangeLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -263,14 +280,14 @@ const kpiColors = new Map<string, string>();
|
||||
const getSeriesConfig = () => ({
|
||||
symbol: 'none',
|
||||
symbolSize: 6,
|
||||
smooth:0.6,
|
||||
smooth: 0.6,
|
||||
showSymbol: true,
|
||||
});
|
||||
// 添加一个函数来获取当前主题下的网格线颜色
|
||||
const getSplitLineColor = () => {
|
||||
return document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#333333'
|
||||
: '#E8E8E8'; // 亮色模式返回 undefined,使用默认颜色
|
||||
: '#E8E8E8'; // 亮色模式返回 undefined,使用默认颜色
|
||||
};
|
||||
|
||||
// 添加主题变化的观察器
|
||||
@@ -289,27 +306,30 @@ const themeObserver = new MutationObserver(() => {
|
||||
kpiColors.set(kpiId, color);
|
||||
});
|
||||
// 使用存储的颜色更新图表系列
|
||||
const series = selectedKPIs.value.map(kpiId => {
|
||||
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
|
||||
if (!kpi) return null;
|
||||
const series = selectedKPIs.value
|
||||
.map(kpiId => {
|
||||
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
|
||||
if (!kpi) return null;
|
||||
|
||||
return {
|
||||
name: kpi.title,
|
||||
type: 'line',
|
||||
data: chartData.value.map(item => item[kpiId] || 0),
|
||||
itemStyle: { color: kpiColors.get(kpiId) },
|
||||
...getSeriesConfig(),
|
||||
};
|
||||
}).filter(Boolean);
|
||||
return {
|
||||
name: kpi.title,
|
||||
type: 'line',
|
||||
data: chartData.value.map(item => item[kpiId] || 0),
|
||||
itemStyle: { color: kpiColors.get(kpiId) },
|
||||
...getSeriesConfig(),
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
const option = {
|
||||
title: {
|
||||
text: t('views.perfManage.kpiOverView.kpiChartTitle'),
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
color: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333'
|
||||
}
|
||||
color:
|
||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333',
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
// 保持现有的 xAxis 配置
|
||||
@@ -319,61 +339,68 @@ const themeObserver = new MutationObserver(() => {
|
||||
dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss')
|
||||
),
|
||||
axisLabel: {
|
||||
color: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333'
|
||||
color:
|
||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333',
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: splitLineColor
|
||||
}
|
||||
}
|
||||
color: splitLineColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
formatter: '{value}',
|
||||
color: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333'
|
||||
color:
|
||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333',
|
||||
},
|
||||
splitNumber: 5,
|
||||
scale: true,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: splitLineColor
|
||||
}
|
||||
}
|
||||
color: splitLineColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
selected: Object.fromEntries(
|
||||
selectedKPIs.value.map(kpiId => [
|
||||
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || kpiId,
|
||||
selectedRows.value.length === 0 ? true : selectedRows.value.includes(kpiId)
|
||||
selectedRows.value.length === 0
|
||||
? true
|
||||
: selectedRows.value.includes(kpiId),
|
||||
])
|
||||
)
|
||||
),
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
confine: true, // 限制 tooltip 显示范围
|
||||
backgroundColor: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? 'rgba(48, 48, 48, 0.8)'
|
||||
: 'rgba(255, 255, 255, 0.9)',
|
||||
borderColor: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#555'
|
||||
: '#ddd',
|
||||
backgroundColor:
|
||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? 'rgba(48, 48, 48, 0.8)'
|
||||
: 'rgba(255, 255, 255, 0.9)',
|
||||
borderColor:
|
||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#555'
|
||||
: '#ddd',
|
||||
textStyle: {
|
||||
color: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333'
|
||||
color:
|
||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333',
|
||||
},
|
||||
extraCssText: 'box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);'
|
||||
extraCssText: 'box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);',
|
||||
},
|
||||
// 重新设置系列数据
|
||||
series
|
||||
series,
|
||||
};
|
||||
|
||||
// 使用新的配置更新图表
|
||||
@@ -390,24 +417,26 @@ const themeObserver = new MutationObserver(() => {
|
||||
|
||||
const updateChart = () => {
|
||||
if (!chart || !kpiColumns.value.length) return;
|
||||
//获取图表配置
|
||||
//获取图表配置
|
||||
const commonConfig = getSeriesConfig();
|
||||
//构建数据系列
|
||||
const series = selectedKPIs.value.map(kpiId => {
|
||||
const kpi = kpiColumns.value.find(col=>col.kpiId ===kpiId);
|
||||
if (!kpi) return null;
|
||||
//为每个KPI分配临时的固定颜色
|
||||
const color = kpiColors.get(kpiId)||generateColorRGBA();
|
||||
kpiColors.set(kpiId, color);
|
||||
//构建数据系列
|
||||
const series = selectedKPIs.value
|
||||
.map(kpiId => {
|
||||
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
|
||||
if (!kpi) return null;
|
||||
//为每个KPI分配临时的固定颜色
|
||||
const color = kpiColors.get(kpiId) || generateColorRGBA();
|
||||
kpiColors.set(kpiId, color);
|
||||
|
||||
return {
|
||||
name: kpi.title,
|
||||
type: 'line',
|
||||
data: chartData.value.map(item=>item[kpiId]||0),
|
||||
itemStyle: { color },
|
||||
...commonConfig,
|
||||
};
|
||||
}).filter(Boolean);
|
||||
return {
|
||||
name: kpi.title,
|
||||
type: 'line',
|
||||
data: chartData.value.map(item => item[kpiId] || 0),
|
||||
itemStyle: { color },
|
||||
...commonConfig,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
@@ -415,30 +444,35 @@ const updateChart = () => {
|
||||
left: 'center',
|
||||
// 添加文字颜色配置,根据主题切换
|
||||
textStyle: {
|
||||
color: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333'
|
||||
}
|
||||
color:
|
||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333',
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
confine: true, // 限制 tooltip 显示范围
|
||||
backgroundColor: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? 'rgba(48, 48, 48, 0.8)'
|
||||
: 'rgba(255, 255, 255, 0.9)',
|
||||
borderColor: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#555'
|
||||
: '#ddd',
|
||||
backgroundColor:
|
||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? 'rgba(48, 48, 48, 0.8)'
|
||||
: 'rgba(255, 255, 255, 0.9)',
|
||||
borderColor:
|
||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#555'
|
||||
: '#ddd',
|
||||
textStyle: {
|
||||
color: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333'
|
||||
color:
|
||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333',
|
||||
},
|
||||
extraCssText: 'box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);'
|
||||
extraCssText: 'box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);',
|
||||
},
|
||||
legend: {
|
||||
data: selectedKPIs.value.map(kpiId =>
|
||||
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || kpiId
|
||||
data: selectedKPIs.value.map(
|
||||
kpiId =>
|
||||
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || kpiId
|
||||
),
|
||||
type: 'scroll',
|
||||
orient: 'horizontal',
|
||||
@@ -449,7 +483,9 @@ const updateChart = () => {
|
||||
selected: Object.fromEntries(
|
||||
selectedKPIs.value.map(kpiId => [
|
||||
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || kpiId,
|
||||
selectedRows.value.length === 0 ? true : selectedRows.value.includes(kpiId)
|
||||
selectedRows.value.length === 0
|
||||
? true
|
||||
: selectedRows.value.includes(kpiId),
|
||||
])
|
||||
),
|
||||
show: false,
|
||||
@@ -470,15 +506,16 @@ const updateChart = () => {
|
||||
type: 'category',
|
||||
axisLabel: {
|
||||
formatter: '{value}',
|
||||
color: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333'
|
||||
color:
|
||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333',
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: getSplitLineColor()
|
||||
}
|
||||
color: getSplitLineColor(),
|
||||
},
|
||||
},
|
||||
//控制坐标轴两边留白
|
||||
// 当为折线图时(isLine为true)时不留白,柱状图时留白
|
||||
@@ -486,7 +523,7 @@ const updateChart = () => {
|
||||
boundaryGap: false,
|
||||
// 设置x轴的数据
|
||||
// 将时间戳转换为格式化的时间字符串
|
||||
data:chartData.value.map(item=>
|
||||
data: chartData.value.map(item =>
|
||||
dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss')
|
||||
),
|
||||
//设置坐标轴刻度标签的样式
|
||||
@@ -519,9 +556,10 @@ const updateChart = () => {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
formatter: '{value}',
|
||||
color: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333'
|
||||
color:
|
||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
? '#CACADA'
|
||||
: '#333',
|
||||
},
|
||||
// 添加自计算的分割段数
|
||||
splitNumber: 5,
|
||||
@@ -530,13 +568,13 @@ const updateChart = () => {
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: getSplitLineColor()
|
||||
}
|
||||
}
|
||||
color: getSplitLineColor(),
|
||||
},
|
||||
},
|
||||
},
|
||||
series: series, //配置数据
|
||||
};
|
||||
if(chart) {
|
||||
if (chart) {
|
||||
requestAnimationFrame(() => {
|
||||
chart!.setOption(option, true); //使用新的配置更新图表
|
||||
chart!.resize(); //调整图表大小适应容器
|
||||
@@ -561,8 +599,6 @@ const updateChart = () => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
//钩子函数
|
||||
onMounted(async () => {
|
||||
try {
|
||||
@@ -588,7 +624,7 @@ onMounted(async () => {
|
||||
// 添加主题观察器
|
||||
themeObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-theme']
|
||||
attributeFilter: ['data-theme'],
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize:', error);
|
||||
@@ -603,9 +639,10 @@ const selectedKPIs = ref<string[]>(Object.values(TARGET_KPI_IDS).flat());
|
||||
|
||||
// 获取网元指标
|
||||
const fetchSpecificKPI = async () => {
|
||||
const language = currentLocale.value.split('_')[0] === 'zh'
|
||||
? 'cn'
|
||||
: currentLocale.value.split('_')[0];
|
||||
const language =
|
||||
currentLocale.value.split('_')[0] === 'zh'
|
||||
? 'cn'
|
||||
: currentLocale.value.split('_')[0];
|
||||
|
||||
try {
|
||||
let allKPIs: KPIColumn[] = [];
|
||||
@@ -664,12 +701,13 @@ onUnmounted(() => {
|
||||
|
||||
// 实时数据更新图表数据方法
|
||||
const updateChartData = (newData: ChartDataItem) => {
|
||||
if(!chart){
|
||||
if (!chart) {
|
||||
return;
|
||||
}
|
||||
chartData.value.push(newData);
|
||||
if (chartData.value.length > 100) {//100改为50
|
||||
chartData.value.shift();//大于100条时删除最早的数据
|
||||
if (chartData.value.length > 100) {
|
||||
//100改为50
|
||||
chartData.value.shift(); //大于100条时删除最早的数据
|
||||
}
|
||||
//使用try-catch包裹图表更新逻辑
|
||||
try {
|
||||
@@ -692,12 +730,11 @@ const updateChartData = (newData: ChartDataItem) => {
|
||||
};
|
||||
chart.setOption(option);
|
||||
});
|
||||
}catch (error){
|
||||
} catch (error) {
|
||||
console.error('Failed to update chart:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 添加一个接口定义指标统计数据的类型
|
||||
interface KPIStats {
|
||||
kpiId: string;
|
||||
@@ -718,34 +755,36 @@ const updateKpiStats = () => {
|
||||
kpiStats.value = [];
|
||||
return;
|
||||
}
|
||||
kpiStats.value = selectedKPIs.value.map(kpiId => {
|
||||
// 找到对应的KPI标题
|
||||
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
|
||||
if (!kpi) return null;
|
||||
kpiStats.value = selectedKPIs.value
|
||||
.map(kpiId => {
|
||||
// 找到对应的KPI标题
|
||||
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
|
||||
if (!kpi) return null;
|
||||
|
||||
// 获取该指标的所有数值
|
||||
const values = chartData.value.map(item => Number(item[kpiId]) || 0);
|
||||
// 获取该指标的所有数值
|
||||
const values = chartData.value.map(item => Number(item[kpiId]) || 0);
|
||||
|
||||
// 计算总值
|
||||
const total = Number(values.reduce((sum, val) => sum + val, 0).toFixed(2));
|
||||
// 计算总值
|
||||
const total = Number(
|
||||
values.reduce((sum, val) => sum + val, 0).toFixed(2)
|
||||
);
|
||||
|
||||
// 计算平均值
|
||||
const avg = values.length > 0
|
||||
? Number((total / values.length).toFixed(2))
|
||||
: 0;
|
||||
// 计算平均值
|
||||
const avg =
|
||||
values.length > 0 ? Number((total / values.length).toFixed(2)) : 0;
|
||||
|
||||
return {
|
||||
kpiId: kpiId,
|
||||
title: kpi.title,
|
||||
max: Math.max(...values),
|
||||
min: Math.min(...values),
|
||||
avg: avg,
|
||||
total: total
|
||||
};
|
||||
}).filter((item): item is KPIStats => item !== null);
|
||||
return {
|
||||
kpiId: kpiId,
|
||||
title: kpi.title,
|
||||
max: Math.max(...values),
|
||||
min: Math.min(...values),
|
||||
avg: avg,
|
||||
total: total,
|
||||
};
|
||||
})
|
||||
.filter((item): item is KPIStats => item !== null);
|
||||
};
|
||||
|
||||
|
||||
// 添加表列定义
|
||||
const statsColumns: TableColumnType<KPIStats>[] = [
|
||||
{
|
||||
@@ -761,9 +800,9 @@ const statsColumns: TableColumnType<KPIStats>[] = [
|
||||
color: color || '#000', // 使用与折线图相同的颜色
|
||||
fontSize: '30px', // 增大图标尺寸到30px
|
||||
fontWeight: 'bold', // 加粗
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('views.perfManage.kpiOverView.kpiName'),
|
||||
@@ -829,14 +868,16 @@ const updateChartLegendSelect = () => {
|
||||
const legendSelected = Object.fromEntries(
|
||||
selectedKPIs.value.map(kpiId => [
|
||||
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || kpiId,
|
||||
selectedRows.value.length === 0 ? true : selectedRows.value.includes(kpiId)
|
||||
selectedRows.value.length === 0
|
||||
? true
|
||||
: selectedRows.value.includes(kpiId),
|
||||
])
|
||||
);
|
||||
|
||||
chart.setOption({
|
||||
legend: {
|
||||
selected: legendSelected
|
||||
}
|
||||
selected: legendSelected,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -844,7 +885,7 @@ const updateChartLegendSelect = () => {
|
||||
const tableRowConfig = computed(() => {
|
||||
return (record: KPIStats) => ({
|
||||
onClick: () => handleRowClick(record),
|
||||
class: selectedRows.value.includes(record.kpiId) ? 'selected-row' : ''
|
||||
class: selectedRows.value.includes(record.kpiId) ? 'selected-row' : '',
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -882,7 +923,54 @@ const tableRowConfig = computed(() => {
|
||||
size="small"
|
||||
:loading="tableLoading"
|
||||
:custom-row="tableRowConfig"
|
||||
/>
|
||||
>
|
||||
<template #headerCell="{ column }">
|
||||
<template v-if="column.key === 'total'">
|
||||
<span>
|
||||
{{ t('views.perfManage.kpiOverView.totalValue') }}
|
||||
<a-tooltip placement="bottom">
|
||||
<template #title>
|
||||
<span>Sum within Time Range</span>
|
||||
</template>
|
||||
<InfoCircleOutlined />
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'avg'">
|
||||
<span>
|
||||
{{ t('views.perfManage.kpiOverView.avgValue') }}
|
||||
<a-tooltip placement="bottom">
|
||||
<template #title>
|
||||
<span>Average value over the time range</span>
|
||||
</template>
|
||||
<InfoCircleOutlined />
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'max'">
|
||||
<span>
|
||||
{{ t('views.perfManage.kpiOverView.maxValue') }}
|
||||
<a-tooltip placement="bottom">
|
||||
<template #title>
|
||||
<span>Maximum value in time range</span>
|
||||
</template>
|
||||
<InfoCircleOutlined />
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'min'">
|
||||
<span>
|
||||
{{ t('views.perfManage.kpiOverView.minValue') }}
|
||||
<a-tooltip placement="bottom">
|
||||
<template #title>
|
||||
<span>Minimum value in the time range</span>
|
||||
</template>
|
||||
<InfoCircleOutlined />
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -945,8 +1033,8 @@ const tableRowConfig = computed(() => {
|
||||
}
|
||||
|
||||
/* 表格行和表头样式 */
|
||||
:deep(.ant-table-thead tr th),
|
||||
:deep(.ant-table-tbody tr td) {
|
||||
:deep(.ant-table-thead tr th),
|
||||
:deep(.ant-table-tbody tr td) {
|
||||
padding: 8px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user