Files
fe.ems.vue3/src/views/perfManage/overview/index.vue
2025-08-06 17:35:03 +08:00

1948 lines
64 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="dashboard-cards">
<div style="font-size:32px; font-weight: bold; margin-bottom: 20px; text-align: center; position: relative;">
{{ t('views.perfManage.voiceOverView.voiceTitle') }}
<!-- <span class="control-tower-badge">IMS Control Tower</span>-->
</div>
<div style="margin-bottom: 24px; display: flex; align-items: center; justify-content: flex-end;">
<span style="font-size: 12px; color: #999; font-style: italic;">*{{ t('views.perfManage.voiceOverView.tips') }}</span>
<div style="display: flex; align-items: center; margin-left: 32px;">
<label style="margin-right: 8px; font-weight: 600;">{{ t('views.perfManage.voiceOverView.ne') }}</label>
<a-select
v-model:value="selectedImsNeId"
:options="imsNeList.map(ne => ({ label: ne.neName || ne.neId, value: ne.neId }))"
@change="onImsNeChange"
style="width: 100px; font-size: 15px;"
:dropdown-style="{ borderRadius: '8px' }"
placeholder='No NE'
:bordered="true"
class="ims-select"
:allow-clear="false"
/>
</div>
</div>
<div class="row-title">{{ t('views.perfManage.voiceOverView.calls') }}</div>
<a-row :gutter="[16, 16]">
<a-col :xs="24" :sm="24" :lg="8">
<a-card bordered class="metric-card">
<div class="card-title">{{ t('views.perfManage.voiceOverView.activeCall') }} <span class="main-icon phone">📞</span></div>
<div class="card-content">
<div class="trend-chart">
<div class="mini-chart" ref="callsChartRef"></div>
<!-- <div class="card-subtext">60m <span class="card-sub-sep">now</span></div>-->
</div>
<div class="metric-info">
<div class="metric-value">
{{ calculateActiveCallsValue() }}
<span class="main-arrow" :class="calculateActiveCallsArrowDirection()">{{ calculateActiveCallsArrow() }}</span>
</div>
<div class="metric-change">{{ calculateActiveCallsChange() }}</div>
</div>
</div>
</a-card>
</a-col>
<!-- <a-col :xs="24" :sm="24" :lg="6">-->
<!-- <a-card bordered class="metric-card">-->
<!-- <div class="card-title">MOS <span class="main-icon phone">📞</span></div>-->
<!-- <div class="card-content">-->
<!-- <div class="trend-chart">-->
<!-- <div class="mini-chart" ref="mosChartRef"></div>-->
<!--&lt;!&ndash; <div class="card-subtext">60m <span class="card-sub-sep">now</span></div>&ndash;&gt;-->
<!-- </div>-->
<!-- <div class="metric-info">-->
<!-- <div class="metric-value">-->
<!-- 4.30-->
<!-- <span class="main-arrow up"></span>-->
<!-- </div>-->
<!-- <div class="metric-change">+0.08 last 5m</div>-->
<!-- </div>-->
<!-- </div>-->
<!-- </a-card>-->
<!-- </a-col>-->
<a-col :xs="24" :sm="24" :lg="8">
<a-card bordered class="metric-card">
<div class="card-title">{{ t('views.perfManage.voiceOverView.callMOMT') }}</div>
<div class="card-content full-width">
<div class="metric-info" style="width:100%;text-align:center;">
<div class="metric-value" style="font-size:38px;justify-content:center;">
{{ calculateMOValue() }}% / {{ calculateMTValue() }}%
</div>
<div class="metric-change">{{ calculateMOChange() }} / {{ calculateMTChange() }}</div>
</div>
</div>
</a-card>
</a-col>
<a-col :xs="24" :sm="24" :lg="8">
<a-card bordered class="metric-card">
<div class="card-title">{{ t('views.perfManage.voiceOverView.failedcall') }} <span class="main-icon phone">📞</span></div>
<div class="card-content">
<div class="trend-chart">
<div class="mini-chart" ref="failedCallsChartRef"></div>
<!-- <div class="card-subtext">60m <span class="card-sub-sep">now</span></div>-->
</div>
<div class="metric-info">
<div class="metric-value">
{{ calculateFailedCallsValue() }}
<span class="main-arrow" :class="calculateFailedCallsArrowDirection()">{{ calculateFailedCallsArrow() }}</span>
</div>
<div class="metric-change">{{ calculateFailedCallsChange() }}</div>
</div>
</div>
</a-card>
</a-col>
</a-row>
<div class="row-title" style="margin-top: 48px;">{{ t('views.perfManage.voiceOverView.registration') }}</div>
<a-row :gutter="[16, 16]">
<a-col :xs="24" :sm="24" :lg="8">
<a-card bordered class="metric-card">
<div class="card-title">{{ t('views.perfManage.voiceOverView.activeregistration') }} <span class="main-icon calendar">📅</span></div>
<div class="card-content">
<div class="trend-chart">
<div class="mini-chart" ref="regChartRef"></div>
<!-- <div class="card-subtext">24h <span class="card-sub-sep">now</span></div>-->
</div>
<div class="metric-info">
<div class="metric-value">
{{ calculateActiveRegistrationsValue() }}
<span class="main-arrow" :class="calculateActiveRegistrationsArrowDirection()">{{ calculateActiveRegistrationsArrow() }}</span>
</div>
<div class="metric-change">{{ calculateActiveRegistrationsChange() }}</div>
</div>
</div>
</a-card>
</a-col>
<a-col :xs="24" :sm="24" :lg="8">
<a-card bordered class="metric-card">
<div class="card-title">{{ t('views.perfManage.voiceOverView.registrationsuccess') }}</div>
<div class="card-content full-width">
<div class="metric-info" style="width:100%;text-align:center;">
<div class="metric-value" style="font-size:38px;justify-content:center;">{{ calculateRegSuccessValue() }}%</div>
<div class="metric-change">{{ calculateRegSuccessChange() }}</div>
</div>
</div>
</a-card>
</a-col>
<a-col :xs="24" :sm="24" :lg="8">
<a-card bordered class="metric-card">
<div class="card-title">{{ t('views.perfManage.voiceOverView.failedregistration') }} <span class="main-icon calendar">📅</span></div>
<div class="card-content">
<div class="trend-chart">
<div class="mini-chart" ref="failedRegChartRef"></div>
<!-- <div class="card-subtext">24h <span class="card-sub-sep">now</span></div>-->
</div>
<div class="metric-info">
<div class="metric-value">
{{ calculateFailedRegistrationsValue() }}
<span class="main-arrow" :class="calculateFailedRegistrationsArrowDirection()">{{ calculateFailedRegistrationsArrow() }}</span>
</div>
<div class="metric-change">{{ calculateFailedRegistrationsChange() }}</div>
</div>
</div>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import * as echarts from 'echarts/core'
import { LineChart } from 'echarts/charts'
import { GridComponent } from 'echarts/components'
import { CanvasRenderer } from 'echarts/renderers'
import useNeListStore from '@/store/modules/ne_list';
import { WS } from '@/plugins/ws-websocket'
import { listKPIData } from '@/api/perfManage/goldTarget'
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants'
import useI18n from '@/hooks/useI18n';
const { t } = useI18n();
echarts.use([LineChart, GridComponent, CanvasRenderer])
const callsChartRef = ref<HTMLDivElement | null>(null)
const mosChartRef = ref<HTMLDivElement | null>(null)
const failedCallsChartRef = ref<HTMLDivElement | null>(null)
const regChartRef = ref<HTMLDivElement | null>(null)
const failedRegChartRef = ref<HTMLDivElement | null>(null)
// IMS网元列表
const imsNeList = ref<{ neId: string, neName: string }[]>([])
// 当前选中的IMS网元ID
const selectedImsNeId = ref('')
// WebSocket实例
const imsWs = ref<any>(null)
// IMS实时原始数据只存储当前选中网元
const imsRealtimeRawData = ref<any[]>([])
// WebSocket连接状态
const wsStatus = ref('未连接')
// 获取IMS网元列表
onMounted(async () => {
// console.log('组件挂载开始获取IMS网元列表') // 调试信息
const res = await useNeListStore().fnNelist()
// console.log('获取到的网元列表响应:', res) // 调试信息
if (res && Array.isArray(res.data)) {
imsNeList.value = res.data.filter((ne: any) => ne.neType === 'IMS')
// console.log('过滤后的IMS网元列表:', imsNeList.value) // 调试信息
if (imsNeList.value.length > 0) {
selectedImsNeId.value = imsNeList.value[0].neId
// console.log('默认选中第一个IMS网元:', selectedImsNeId.value) // 调试信息
// 先获取历史数据,再订阅实时数据
await fetchHistoryData(selectedImsNeId.value)
subscribeImsRealtime(selectedImsNeId.value)
} else {
// console.warn('没有找到IMS类型的网元') // 调试信息
}
} else {
// console.error('获取网元列表失败或数据格式不正确') // 调试信息
}
})
// 获取历史数据
async function fetchHistoryData(neId: string) {
if (!neId) return
try {
// 计算30分钟前的时间
const endTime = Date.now()
const beginTime = endTime - (30 * 60 * 1000) // 30分钟前
// 构建查询参数,与黄金指标界面保持一致
const params = {
neType: 'IMS',
neId: neId,
interval: 60, // 1分钟颗粒度
beginTime: beginTime.toString(),
endTime: endTime.toString(),
sortField: 'timeGroup',
sortOrder: 'desc',
pageSize: 30, // 最多获取30条数据
pageNum: 1
}
console.log('获取历史数据参数:', params)
const res = await listKPIData(params)
console.log('历史数据响应:', res)
if (res.code === 200001 && Array.isArray(res.data)) {
// 将历史数据转换为与实时数据相同的格式
const historyData = res.data.map((item: any) => ({
timestamp: item.timeGroup || Date.now(),
data: item
}))
console.log('转换后的历史数据:', historyData)
// 将历史数据添加到实时数据数组中(追加而不是覆盖)
// 注意:这里直接赋值,因为这是初始加载历史数据
imsRealtimeRawData.value = historyData
console.log('历史数据加载完成,数据点数量:', imsRealtimeRawData.value.length)
console.log('最新历史数据时间戳:', historyData.length > 0 ? historyData[historyData.length - 1].timestamp : '无数据')
// 更新所有图表
updateActiveCallsChart()
updateFailedCallsChart()
updateActiveRegistrationsChart()
updateFailedRegistrationsChart()
} else {
console.warn('获取历史数据失败或数据为空')
}
} catch (error) {
console.error('获取历史数据出错:', error)
}
}
// 切换IMS网元时重新订阅
async function onImsNeChange() {
// console.log('切换IMS网元新的网元ID:', selectedImsNeId.value) // 调试信息
// 切换网元时,先清空旧数据
imsRealtimeRawData.value = []
// 先获取历史数据,再订阅实时数据
await fetchHistoryData(selectedImsNeId.value)
subscribeImsRealtime(selectedImsNeId.value)
}
// 订阅指定IMS网元实时数据
function subscribeImsRealtime(neId: string) {
// console.log('开始订阅IMS网元数据网元ID:', neId) // 调试信息
wsStatus.value = '连接中...' // 更新状态
// 关闭旧WebSocket
if (imsWs.value) {
// console.log('关闭旧WebSocket连接') // 调试信息
imsWs.value.close()
imsWs.value = null
}
// 注意:不要清空历史数据,让历史数据保留
// imsRealtimeRawData.value = [] // 移除这行,避免清空历史数据
if (!neId) return
imsWs.value = new WS()
// console.log('创建新的WebSocket连接') // 调试信息
imsWs.value.connect({
url: '/ws',
params: {
subGroupID: `10_IMS_${neId}`,
},
onmessage: handleIMSRealtimeData,
onerror: (error: any) => {
// console.error('WebSocket连接错误:', error) // 调试信息
wsStatus.value = '连接错误' // 更新状态
},
onopen: () => {
// console.log('WebSocket连接已建立') // 调试信息
wsStatus.value = '已连接' // 更新状态
},
onclose: () => {
// console.log('WebSocket连接已关闭') // 调试信息
wsStatus.value = '已断开' // 更新状态
}
})
}
// 组件卸载时关闭WebSocket
onBeforeUnmount(() => {
if (imsWs.value) {
imsWs.value.close()
imsWs.value = null
}
wsStatus.value = '已断开' // 更新状态
})
// 更新active calls图表
function updateActiveCallsChart() {
if (!callsChartRef.value) return
// 获取图表实例
let chart = echarts.getInstanceByDom(callsChartRef.value)
if (!chart) {
chart = echarts.init(callsChartRef.value)
}
// 准备图表数据
const chartData = imsRealtimeRawData.value.map((item, index) => {
const kpiEvent = item.data
const scscf07 = Number(kpiEvent['SCSCF.07']) || 0
return scscf07
})
console.log('updateActiveCallsChart - 原始数据点数量:', imsRealtimeRawData.value.length)
console.log('updateActiveCallsChart - 图表数据:', chartData)
// 如果没有数据,显示默认的平直线
if (chartData.length === 0) {
const defaultData = [0, 0, 0, 0, 0] // 5个默认数据点值为0
const xAxisData = [1, 2, 3, 4, 5]
chart.setOption({
grid: { left: 0, right: 30, top: 10, bottom: 10 }, // 增加右侧边距
xAxis: { type: 'category', show: false, data: xAxisData },
yAxis: { type: 'value', show: false },
series: [{
data: defaultData,
type: 'line', symbol: 'none',
lineStyle: { width: 2, color: '#d9d9d9' }, // 灰色线条表示无数据
areaStyle: { color: 'rgba(217,217,217,0.1)' } // 淡灰色填充
}]
})
return
}
// 如果数据不足,补充默认数据
while (chartData.length < 5) {
chartData.unshift(0) // 在开头补充默认值0
}
// 限制数据点数量为最近30个
if (chartData.length > 30) {
chartData.splice(0, chartData.length - 30)
}
console.log('updateActiveCallsChart - 最终图表数据点数量:', chartData.length)
// 生成时间轴数据
const xAxisData = Array.from({ length: chartData.length }, (_, i) => i + 1)
// 计算最大值、最小值、最新值
const maxValue = Math.max(...chartData)
const minValue = Math.min(...chartData)
const latestValue = chartData[chartData.length - 1]
chart.setOption({
grid: { left: 0, right: 30, top: 10, bottom: 10 }, // 增加右侧边距
xAxis: { type: 'category', show: false, data: xAxisData },
yAxis: { type: 'value', show: false },
series: [{
data: chartData,
type: 'line', symbol: 'none',
lineStyle: { width: 2, color: '#1890ff' }, // 蓝色线条表示有数据
areaStyle: { color: 'rgba(24,144,255,0.1)' } // 淡蓝色填充
}]
})
// 在图表容器中添加数值标注
const chartContainer = callsChartRef.value
if (chartContainer) {
// 清除之前的标注
const existingLabels = chartContainer.querySelectorAll('.chart-label')
existingLabels.forEach(label => label.remove())
// 添加右侧数值标注
const maxLabel = document.createElement('div')
maxLabel.className = 'chart-label'
maxLabel.style.cssText = `
position: absolute;
right: 8px;
top: 8px;
font-size: 12px;
font-weight: bold;
color: #666;
pointer-events: none;
z-index: 10;
`
maxLabel.textContent = maxValue.toString()
const latestLabel = document.createElement('div')
latestLabel.className = 'chart-label'
latestLabel.style.cssText = `
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
font-size: 12px;
font-weight: bold;
color: #1890ff;
pointer-events: none;
z-index: 10;
`
latestLabel.textContent = latestValue.toString()
const minLabel = document.createElement('div')
minLabel.className = 'chart-label'
minLabel.style.cssText = `
position: absolute;
right: 8px;
bottom: 8px;
font-size: 12px;
font-weight: bold;
color: #666;
pointer-events: none;
z-index: 10;
`
minLabel.textContent = minValue.toString()
// 添加底部时间标注
const oldestTimeLabel = document.createElement('div')
oldestTimeLabel.className = 'chart-label'
oldestTimeLabel.style.cssText = `
position: absolute;
left: 8px;
bottom: -20px;
font-size: 11px;
color: #999;
pointer-events: none;
z-index: 10;
`
// 计算图表实际显示的最旧数据时间(基于图表数据点数量)
const displayDataLength = Math.min(imsRealtimeRawData.value.length, 30)
const oldestDisplayIndex = Math.max(0, imsRealtimeRawData.value.length - displayDataLength)
const oldestDisplayData = imsRealtimeRawData.value[oldestDisplayIndex]
if (oldestDisplayData && oldestDisplayData.timestamp) {
oldestTimeLabel.textContent = calculateRelativeTime(oldestDisplayData.timestamp)
} else {
oldestTimeLabel.textContent = '--'
}
const nowTimeLabel = document.createElement('div')
nowTimeLabel.className = 'chart-label'
nowTimeLabel.style.cssText = `
position: absolute;
right: 8px;
bottom: -20px;
font-size: 11px;
color: #999;
pointer-events: none;
z-index: 10;
`
nowTimeLabel.textContent = t('views.perfManage.voiceOverView.now')
chartContainer.appendChild(maxLabel)
chartContainer.appendChild(latestLabel)
chartContainer.appendChild(minLabel)
chartContainer.appendChild(oldestTimeLabel)
chartContainer.appendChild(nowTimeLabel)
}
}
// 更新failed calls图表
function updateFailedCallsChart() {
if (!failedCallsChartRef.value) return
// 获取图表实例
let chart = echarts.getInstanceByDom(failedCallsChartRef.value)
if (!chart) {
chart = echarts.init(failedCallsChartRef.value)
}
// 准备图表数据
const chartData = imsRealtimeRawData.value.map((item, index) => {
const kpiEvent = item.data
const scscf06 = Number(kpiEvent['SCSCF.06']) || 0
const scscf07 = Number(kpiEvent['SCSCF.07']) || 0
const failedCalls = scscf06 - scscf07
return failedCalls
})
// 如果没有数据,显示默认的平直线
if (chartData.length === 0) {
const defaultData = [0, 0, 0, 0, 0] // 5个默认数据点值为0
const xAxisData = [1, 2, 3, 4, 5]
chart.setOption({
grid: { left: 0, right: 30, top: 10, bottom: 10 }, // 增加右侧边距
xAxis: { type: 'category', show: false, data: xAxisData },
yAxis: { type: 'value', show: false },
series: [{
data: defaultData,
type: 'line', symbol: 'none',
lineStyle: { width: 2, color: '#d9d9d9' }, // 灰色线条表示无数据
areaStyle: { color: 'rgba(217,217,217,0.1)' } // 淡灰色填充
}]
})
return
}
// 如果数据不足,补充默认数据
while (chartData.length < 5) {
chartData.unshift(0) // 在开头补充默认值0
}
// 限制数据点数量为最近30个
if (chartData.length > 30) {
chartData.splice(0, chartData.length - 30)
}
// 生成时间轴数据
const xAxisData = Array.from({ length: chartData.length }, (_, i) => i + 1)
// 计算最大值、最小值、最新值
const maxValue = Math.max(...chartData)
const minValue = Math.min(...chartData)
const latestValue = chartData[chartData.length - 1]
chart.setOption({
grid: { left: 0, right: 30, top: 10, bottom: 10 }, // 增加右侧边距
xAxis: { type: 'category', show: false, data: xAxisData },
yAxis: { type: 'value', show: false },
series: [{
data: chartData,
type: 'line', symbol: 'none',
lineStyle: { width: 2, color: '#faad14' }, // 橙色线条表示failed calls
areaStyle: { color: 'rgba(250,173,20,0.1)' } // 淡橙色填充
}]
})
// 在图表容器中添加数值标注
const chartContainer = failedCallsChartRef.value
if (chartContainer) {
// 清除之前的标注
const existingLabels = chartContainer.querySelectorAll('.chart-label')
existingLabels.forEach(label => label.remove())
// 添加右侧数值标注
const maxLabel = document.createElement('div')
maxLabel.className = 'chart-label'
maxLabel.style.cssText = `
position: absolute;
right: 8px;
top: 8px;
font-size: 12px;
font-weight: bold;
color: #666;
pointer-events: none;
z-index: 10;
`
maxLabel.textContent = maxValue.toString()
const latestLabel = document.createElement('div')
latestLabel.className = 'chart-label'
latestLabel.style.cssText = `
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
font-size: 12px;
font-weight: bold;
color: #faad14;
pointer-events: none;
z-index: 10;
`
latestLabel.textContent = latestValue.toString()
const minLabel = document.createElement('div')
minLabel.className = 'chart-label'
minLabel.style.cssText = `
position: absolute;
right: 8px;
bottom: 8px;
font-size: 12px;
font-weight: bold;
color: #666;
pointer-events: none;
z-index: 10;
`
minLabel.textContent = minValue.toString()
// 添加底部时间标注
const oldestTimeLabel = document.createElement('div')
oldestTimeLabel.className = 'chart-label'
oldestTimeLabel.style.cssText = `
position: absolute;
left: 8px;
bottom: -20px;
font-size: 11px;
color: #999;
pointer-events: none;
z-index: 10;
`
// 计算图表实际显示的最旧数据时间(基于图表数据点数量)
const displayDataLength = Math.min(imsRealtimeRawData.value.length, 30)
const oldestDisplayIndex = Math.max(0, imsRealtimeRawData.value.length - displayDataLength)
const oldestDisplayData = imsRealtimeRawData.value[oldestDisplayIndex]
if (oldestDisplayData && oldestDisplayData.timestamp) {
oldestTimeLabel.textContent = calculateRelativeTime(oldestDisplayData.timestamp)
} else {
oldestTimeLabel.textContent = '--'
}
const nowTimeLabel = document.createElement('div')
nowTimeLabel.className = 'chart-label'
nowTimeLabel.style.cssText = `
position: absolute;
right: 8px;
bottom: -20px;
font-size: 11px;
color: #999;
pointer-events: none;
z-index: 10;
`
nowTimeLabel.textContent = t('views.perfManage.voiceOverView.now')
chartContainer.appendChild(maxLabel)
chartContainer.appendChild(latestLabel)
chartContainer.appendChild(minLabel)
chartContainer.appendChild(oldestTimeLabel)
chartContainer.appendChild(nowTimeLabel)
}
}
// 更新active registrations图表
function updateActiveRegistrationsChart() {
if (!regChartRef.value) return
// 获取图表实例
let chart = echarts.getInstanceByDom(regChartRef.value)
if (!chart) {
chart = echarts.init(regChartRef.value)
}
// 准备图表数据
const chartData = imsRealtimeRawData.value.map((item, index) => {
const kpiEvent = item.data
const scscf03 = Number(kpiEvent['SCSCF.03']) || 0
return scscf03
})
// 如果没有数据,显示默认的平直线
if (chartData.length === 0) {
const defaultData = [0, 0, 0, 0, 0] // 5个默认数据点值为0
const xAxisData = [1, 2, 3, 4, 5]
chart.setOption({
grid: { left: 0, right: 30, top: 10, bottom: 10 }, // 增加右侧边距
xAxis: { type: 'category', show: false, data: xAxisData },
yAxis: { type: 'value', show: false },
series: [{
data: defaultData,
type: 'line', symbol: 'none',
lineStyle: { width: 2, color: '#d9d9d9' }, // 灰色线条表示无数据
areaStyle: { color: 'rgba(217,217,217,0.1)' } // 淡灰色填充
}]
})
return
}
// 如果数据不足,补充默认数据
while (chartData.length < 5) {
chartData.unshift(0) // 在开头补充默认值0
}
// 限制数据点数量为最近30个
if (chartData.length > 30) {
chartData.splice(0, chartData.length - 30)
}
// 生成时间轴数据
const xAxisData = Array.from({ length: chartData.length }, (_, i) => i + 1)
// 计算最大值、最小值、最新值
const maxValue = Math.max(...chartData)
const minValue = Math.min(...chartData)
const latestValue = chartData[chartData.length - 1]
chart.setOption({
grid: { left: 0, right: 30, top: 10, bottom: 10 }, // 增加右侧边距
xAxis: { type: 'category', show: false, data: xAxisData },
yAxis: { type: 'value', show: false },
series: [{
data: chartData,
type: 'line', symbol: 'none',
lineStyle: { width: 2, color: '#1890ff' }, // 蓝色线条表示active registrations
areaStyle: { color: 'rgba(24,144,255,0.1)' } // 淡蓝色填充
}]
})
// 在图表容器中添加数值标注
const chartContainer = regChartRef.value
if (chartContainer) {
// 清除之前的标注
const existingLabels = chartContainer.querySelectorAll('.chart-label')
existingLabels.forEach(label => label.remove())
// 添加右侧数值标注
const maxLabel = document.createElement('div')
maxLabel.className = 'chart-label'
maxLabel.style.cssText = `
position: absolute;
right: 8px;
top: 8px;
font-size: 12px;
font-weight: bold;
color: #666;
pointer-events: none;
z-index: 10;
`
maxLabel.textContent = maxValue.toString()
const latestLabel = document.createElement('div')
latestLabel.className = 'chart-label'
latestLabel.style.cssText = `
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
font-size: 12px;
font-weight: bold;
color: #1890ff;
pointer-events: none;
z-index: 10;
`
latestLabel.textContent = latestValue.toString()
const minLabel = document.createElement('div')
minLabel.className = 'chart-label'
minLabel.style.cssText = `
position: absolute;
right: 8px;
bottom: 8px;
font-size: 12px;
font-weight: bold;
color: #666;
pointer-events: none;
z-index: 10;
`
minLabel.textContent = minValue.toString()
// 添加底部时间标注
const oldestTimeLabel = document.createElement('div')
oldestTimeLabel.className = 'chart-label'
oldestTimeLabel.style.cssText = `
position: absolute;
left: 8px;
bottom: -20px;
font-size: 11px;
color: #999;
pointer-events: none;
z-index: 10;
`
// 计算图表实际显示的最旧数据时间(基于图表数据点数量)
const displayDataLength = Math.min(imsRealtimeRawData.value.length, 30)
const oldestDisplayIndex = Math.max(0, imsRealtimeRawData.value.length - displayDataLength)
const oldestDisplayData = imsRealtimeRawData.value[oldestDisplayIndex]
if (oldestDisplayData && oldestDisplayData.timestamp) {
oldestTimeLabel.textContent = calculateRelativeTime(oldestDisplayData.timestamp)
} else {
oldestTimeLabel.textContent = '--'
}
const nowTimeLabel = document.createElement('div')
nowTimeLabel.className = 'chart-label'
nowTimeLabel.style.cssText = `
position: absolute;
right: 8px;
bottom: -20px;
font-size: 11px;
color: #999;
pointer-events: none;
z-index: 10;
`
nowTimeLabel.textContent = t('views.perfManage.voiceOverView.now')
chartContainer.appendChild(maxLabel)
chartContainer.appendChild(latestLabel)
chartContainer.appendChild(minLabel)
chartContainer.appendChild(oldestTimeLabel)
chartContainer.appendChild(nowTimeLabel)
}
}
// 更新failed registrations图表
function updateFailedRegistrationsChart() {
if (!failedRegChartRef.value) return
// 获取图表实例
let chart = echarts.getInstanceByDom(failedRegChartRef.value)
if (!chart) {
chart = echarts.init(failedRegChartRef.value)
}
// 准备图表数据
const chartData = imsRealtimeRawData.value.map((item, index) => {
const kpiEvent = item.data
const scscf04 = Number(kpiEvent['SCSCF.04']) || 0
const scscf03 = Number(kpiEvent['SCSCF.03']) || 0
const failedRegistrations = scscf04 - scscf03
return failedRegistrations
})
// 如果没有数据,显示默认的平直线
if (chartData.length === 0) {
const defaultData = [0, 0, 0, 0, 0] // 5个默认数据点值为0
const xAxisData = [1, 2, 3, 4, 5]
chart.setOption({
grid: { left: 0, right: 30, top: 10, bottom: 10 }, // 增加右侧边距
xAxis: { type: 'category', show: false, data: xAxisData },
yAxis: { type: 'value', show: false },
series: [{
data: defaultData,
type: 'line', symbol: 'none',
lineStyle: { width: 2, color: '#d9d9d9' }, // 灰色线条表示无数据
areaStyle: { color: 'rgba(217,217,217,0.1)' } // 淡灰色填充
}]
})
return
}
// 如果数据不足,补充默认数据
while (chartData.length < 5) {
chartData.unshift(0) // 在开头补充默认值0
}
// 限制数据点数量为最近30个
if (chartData.length > 30) {
chartData.splice(0, chartData.length - 30)
}
// 生成时间轴数据
const xAxisData = Array.from({ length: chartData.length }, (_, i) => i + 1)
// 计算最大值、最小值、最新值
const maxValue = Math.max(...chartData)
const minValue = Math.min(...chartData)
const latestValue = chartData[chartData.length - 1]
chart.setOption({
grid: { left: 0, right: 30, top: 10, bottom: 10 }, // 增加右侧边距
xAxis: { type: 'category', show: false, data: xAxisData },
yAxis: { type: 'value', show: false },
series: [{
data: chartData,
type: 'line', symbol: 'none',
lineStyle: { width: 2, color: '#f5222d' }, // 红色线条表示failed registrations
areaStyle: { color: 'rgba(245,34,45,0.1)' } // 淡红色填充
}]
})
// 在图表容器中添加数值标注
const chartContainer = failedRegChartRef.value
if (chartContainer) {
// 清除之前的标注
const existingLabels = chartContainer.querySelectorAll('.chart-label')
existingLabels.forEach(label => label.remove())
// 添加右侧数值标注
const maxLabel = document.createElement('div')
maxLabel.className = 'chart-label'
maxLabel.style.cssText = `
position: absolute;
right: 8px;
top: 8px;
font-size: 12px;
font-weight: bold;
color: #666;
pointer-events: none;
z-index: 10;
`
maxLabel.textContent = maxValue.toString()
const latestLabel = document.createElement('div')
latestLabel.className = 'chart-label'
latestLabel.style.cssText = `
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
font-size: 12px;
font-weight: bold;
color: #f5222d;
pointer-events: none;
z-index: 10;
`
latestLabel.textContent = latestValue.toString()
const minLabel = document.createElement('div')
minLabel.className = 'chart-label'
minLabel.style.cssText = `
position: absolute;
right: 8px;
bottom: 8px;
font-size: 12px;
font-weight: bold;
color: #666;
pointer-events: none;
z-index: 10;
`
minLabel.textContent = minValue.toString()
// 添加底部时间标注
const oldestTimeLabel = document.createElement('div')
oldestTimeLabel.className = 'chart-label'
oldestTimeLabel.style.cssText = `
position: absolute;
left: 8px;
bottom: -20px;
font-size: 11px;
color: #999;
pointer-events: none;
z-index: 10;
`
// 计算图表实际显示的最旧数据时间(基于图表数据点数量)
const displayDataLength = Math.min(imsRealtimeRawData.value.length, 30)
const oldestDisplayIndex = Math.max(0, imsRealtimeRawData.value.length - displayDataLength)
const oldestDisplayData = imsRealtimeRawData.value[oldestDisplayIndex]
if (oldestDisplayData && oldestDisplayData.timestamp) {
oldestTimeLabel.textContent = calculateRelativeTime(oldestDisplayData.timestamp)
} else {
oldestTimeLabel.textContent = '--'
}
const nowTimeLabel = document.createElement('div')
nowTimeLabel.className = 'chart-label'
nowTimeLabel.style.cssText = `
position: absolute;
right: 8px;
bottom: -20px;
font-size: 11px;
color: #999;
pointer-events: none;
z-index: 10;
`
nowTimeLabel.textContent = t('views.perfManage.voiceOverView.now')
chartContainer.appendChild(maxLabel)
chartContainer.appendChild(latestLabel)
chartContainer.appendChild(minLabel)
chartContainer.appendChild(oldestTimeLabel)
chartContainer.appendChild(nowTimeLabel)
}
}
// 处理IMS实时数据只存储当前选中网元
function handleIMSRealtimeData(res: any) {
console.log('收到实时数据:', res) // 调试信息
// 检查数据结构:后端实际格式是 {code: 200001, data: {...}, msg: 'success'}
const { code, data, msg } = res
// 检查是否是错误响应 - 修改为适配实际的code值
if (code !== 200001 || !data) {
console.warn('收到错误响应或数据格式不正确:', res) // 调试信息
return
}
// 检查是否是连接确认消息只包含clientId
if (data.clientId) {
console.log('收到WebSocket连接确认消息:', data.clientId) // 调试信息
return
}
// 检查是否是KPI数据消息包含data和groupId
if (data.data && data.groupId) {
console.log('收到KPI数据消息groupId:', data.groupId) // 调试信息
// 解析订阅组ID确认是我们订阅的IMS网元
const [_, neType, neId] = data.groupId.split('_')
if (neType !== 'IMS' || neId !== selectedImsNeId.value) {
console.log('收到其他网元数据,忽略:', data.groupId) // 调试信息
return
}
const kpiEvent = data.data
if (!kpiEvent) {
console.warn('KPI事件数据为空') // 调试信息
return
}
console.log('处理IMS网元KPI数据:', kpiEvent) // 调试信息
// 确保数据结构正确
const dataToStore = {
timestamp: kpiEvent.timeGroup || Date.now(),
data: kpiEvent
}
// 将新数据添加到数组末尾(最新的数据在最后)
imsRealtimeRawData.value.push(dataToStore)
// 保持数据点数量在合理范围内最多200个点
if (imsRealtimeRawData.value.length > 200) {
imsRealtimeRawData.value.shift() // 移除最早的数据点
}
console.log('实时数据已添加,当前数据点数量:', imsRealtimeRawData.value.length)
console.log('最新数据:', dataToStore)
console.log('最新数据时间戳:', dataToStore.timestamp)
// 强制触发Vue响应式更新
imsRealtimeRawData.value = [...imsRealtimeRawData.value]
// 更新active calls图表
updateActiveCallsChart()
// 更新failed calls图表
updateFailedCallsChart()
// 更新active registrations图表
updateActiveRegistrationsChart()
// 更新failed registrations图表
updateFailedRegistrationsChart()
} else {
console.log('收到未知格式的数据:', data) // 调试信息
}
}
onMounted(() => {
// 初始化所有图表为0值线
const defaultChartOption = {
grid: { left: 0, right: 30, top: 10, bottom: 10 }, // 增加右侧边距
xAxis: { type: 'category', show: false, data: [1, 2, 3, 4, 5] },
yAxis: { type: 'value', show: false },
series: [{
data: [0, 0, 0, 0, 0], // 默认显示0值
type: 'line', symbol: 'none',
lineStyle: { width: 2, color: '#d9d9d9' }, // 灰色线条表示无数据
areaStyle: { color: 'rgba(217,217,217,0.1)' } // 淡灰色填充
}]
}
// active calls
if (callsChartRef.value) {
const chart = echarts.init(callsChartRef.value)
chart.setOption(defaultChartOption)
}
// MOS - 保持原有的模拟数据
if (mosChartRef.value) {
const chart = echarts.init(mosChartRef.value)
const mosData = [4.62, 4.50, 4.40, 4.35, 4.30]
const maxValue = Math.max(...mosData)
const minValue = Math.min(...mosData)
const latestValue = mosData[mosData.length - 1]
chart.setOption({
grid: { left: 0, right: 30, top: 10, bottom: 10 }, // 增加右侧边距
xAxis: { type: 'category', show: false, data: [1,2,3,4,5] },
yAxis: { type: 'value', show: false },
series: [{
data: mosData,
type: 'line', symbol: 'none',
lineStyle: { width: 2, color: '#52c41a' },
areaStyle: { color: 'rgba(82,196,26,0.1)' }
}],
graphic: [
{
type: 'text',
right: 8,
top: 8,
style: {
text: maxValue.toFixed(2),
fontSize: 12,
fill: '#666',
fontWeight: 'bold'
}
},
{
type: 'text',
right: 8,
top: '50%',
style: {
text: latestValue.toFixed(2),
fontSize: 12,
fill: '#52c41a',
fontWeight: 'bold'
}
},
{
type: 'text',
right: 8,
bottom: 8,
style: {
text: minValue.toFixed(2),
fontSize: 12,
fill: '#666',
fontWeight: 'bold'
}
}
]
})
}
// failed calls
if (failedCallsChartRef.value) {
const chart = echarts.init(failedCallsChartRef.value)
chart.setOption(defaultChartOption)
}
// active registrations
if (regChartRef.value) {
const chart = echarts.init(regChartRef.value)
chart.setOption(defaultChartOption)
}
// failed registrations
if (failedRegChartRef.value) {
const chart = echarts.init(failedRegChartRef.value)
chart.setOption(defaultChartOption)
}
})
// 计算MO值 (SCSCF.05/SCSCF.06)*100
function calculateMOValue() {
if (imsRealtimeRawData.value.length === 0) return '-'
// 获取最新的数据
const latestData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 1]
if (!latestData || !latestData.data) return '-'
const kpiEvent = latestData.data
const scscf05 = Number(kpiEvent['SCSCF.05']) || 0
const scscf06 = Number(kpiEvent['SCSCF.06']) || 0
if (scscf06 === 0) return '-'
const moValue = (scscf05 / scscf06) * 100
return moValue.toFixed(1)
}
// 计算MT值 (SCSCF.07/SCSCF.08)*100
function calculateMTValue() {
if (imsRealtimeRawData.value.length === 0) return '-'
// 获取最新的数据
const latestData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 1]
if (!latestData || !latestData.data) return '-'
const kpiEvent = latestData.data
const scscf07 = Number(kpiEvent['SCSCF.07']) || 0
const scscf08 = Number(kpiEvent['SCSCF.08']) || 0
if (scscf08 === 0) return '-'
const mtValue = (scscf07 / scscf08) * 100
return mtValue.toFixed(1)
}
// 计算MO变化值完善版本
function calculateMOChange() {
if (imsRealtimeRawData.value.length < 2) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
// 获取最新和上一个数据点
const latestData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 1]
const previousData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 2]
if (!latestData?.data || !previousData?.data) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
const latestKpi = latestData.data
const previousKpi = previousData.data
const latestMO = calculateMOValueFromData(latestKpi)
const previousMO = calculateMOValueFromData(previousKpi)
// 检查MO是否有有效值
if (latestMO === '-' || previousMO === '-') return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
// 计算变化幅度
const change = latestMO - previousMO
// 检查是否有变化
if (change === 0) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
const changeText = change > 0 ? `+${change.toFixed(2)}%` : `${change.toFixed(2)}%`
// 计算时间差
const timeDiff = calculateTimeDifference(latestData, previousData)
return `${changeText} ${t('views.perfManage.voiceOverView.last')} ${timeDiff}`
}
// 计算MT变化值完善版本
function calculateMTChange() {
if (imsRealtimeRawData.value.length < 2) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
// 获取最新和上一个数据点
const latestData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 1]
const previousData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 2]
if (!latestData?.data || !previousData?.data) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
const latestKpi = latestData.data
const previousKpi = previousData.data
const latestMT = calculateMTValueFromData(latestKpi)
const previousMT = calculateMTValueFromData(previousKpi)
// 检查MT是否有有效值
if (latestMT === '-' || previousMT === '-') return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
// 计算变化幅度
const change = latestMT - previousMT
// 检查是否有变化
if (change === 0) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
const changeText = change > 0 ? `+${change.toFixed(2)}%` : `${change.toFixed(2)}%`
// 计算时间差
const timeDiff = calculateTimeDifference(latestData, previousData)
return `${changeText} ${t('views.perfManage.voiceOverView.last')} ${timeDiff}`
}
// 计算时间差函数
function calculateTimeDifference(latestData: any, previousData: any) {
// 获取时间戳
const latestTime = latestData.timestamp || latestData.time || Date.now()
const previousTime = previousData.timestamp || previousData.time || Date.now()
console.log('计算时间差 - 最新数据时间戳:', latestTime)
console.log('计算时间差 - 上一个数据时间戳:', previousTime)
// 计算时间差(毫秒)
const diffMs = Math.abs(latestTime - previousTime)
// 转换为秒
const diffSeconds = Math.floor(diffMs / 1000)
// 转换为分钟
const diffMinutes = Math.floor(diffSeconds / 60)
console.log('计算时间差 - 时间差(毫秒):', diffMs)
console.log('计算时间差 - 时间差(秒):', diffSeconds)
console.log('计算时间差 - 时间差(分钟):', diffMinutes)
// 根据时间差返回合适的格式
if (diffMinutes > 0) {
return `${diffMinutes}m`
} else {
return `${diffSeconds}s`
}
}
// 计算相对时间(距离现在多久)
function calculateRelativeTime(timestamp: number) {
const now = Date.now()
const diffMs = now - timestamp
const diffMinutes = Math.floor(diffMs / (1000 * 60))
const diffHours = Math.floor(diffMinutes / 60)
if (diffHours >= 1) {
return `${diffHours}h`
} else if (diffMinutes >= 1) {
return `${diffMinutes}m`
} else {
return '1m'
}
}
// 辅助函数从数据中计算MO值
function calculateMOValueFromData(kpiEvent: any) {
const scscf05 = Number(kpiEvent['SCSCF.05']) || 0
const scscf06 = Number(kpiEvent['SCSCF.06']) || 0
return scscf06 === 0 ? '-' : (scscf05 / scscf06) * 100
}
// 辅助函数从数据中计算MT值
function calculateMTValueFromData(kpiEvent: any) {
const scscf07 = Number(kpiEvent['SCSCF.07']) || 0
const scscf08 = Number(kpiEvent['SCSCF.08']) || 0
return scscf08 === 0 ? '-' : (scscf07 / scscf08) * 100
}
// 计算registration success值
function calculateRegSuccessValue() {
if (imsRealtimeRawData.value.length === 0) return '-'
// 获取最新的数据
const latestData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 1]
if (!latestData || !latestData.data) return '-'
const kpiEvent = latestData.data
const scscf03 = Number(kpiEvent['SCSCF.03']) || 0
const scscf04 = Number(kpiEvent['SCSCF.04']) || 0
if (scscf04 === 0) return '-'
const regSuccessValue = (scscf03 / scscf04) * 100
return regSuccessValue.toFixed(1)
}
// 计算registration success变化值
function calculateRegSuccessChange() {
if (imsRealtimeRawData.value.length < 2) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
// 获取最新和上一个数据点
const latestData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 1]
const previousData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 2]
if (!latestData?.data || !previousData?.data) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
const latestKpi = latestData.data
const previousKpi = previousData.data
const latestRegSuccess = calculateRegSuccessValueFromData(latestKpi)
const previousRegSuccess = calculateRegSuccessValueFromData(previousKpi)
// 检查registration success是否有有效值
if (latestRegSuccess === '-' || previousRegSuccess === '-') return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
// 计算变化幅度
const change = latestRegSuccess - previousRegSuccess
// 检查是否有变化
if (change === 0) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
const changeText = change > 0 ? `+${change.toFixed(2)}%` : `${change.toFixed(2)}%`
// 计算时间差
const timeDiff = calculateTimeDifference(latestData, previousData)
return `${changeText} ${t('views.perfManage.voiceOverView.last')} ${timeDiff}`
}
// 辅助函数从数据中计算registration success值
function calculateRegSuccessValueFromData(kpiEvent: any) {
const scscf03 = Number(kpiEvent['SCSCF.03']) || 0
const scscf04 = Number(kpiEvent['SCSCF.04']) || 0
return scscf04 === 0 ? '-' : (scscf03 / scscf04) * 100
}
// 计算active calls值
function calculateActiveCallsValue() {
if (imsRealtimeRawData.value.length === 0) return '-'
// 获取最新的数据
const latestData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 1]
if (!latestData || !latestData.data) return '-'
const kpiEvent = latestData.data
const scscf07 = Number(kpiEvent['SCSCF.07']) || 0
// 修改即使值为0也显示"0",因为这是有效的后端数据
const activeCallsValue = scscf07
return activeCallsValue.toFixed(0)
}
// 计算active calls箭头方向
function calculateActiveCallsArrowDirection() {
if (imsRealtimeRawData.value.length < 2) return 'up'
// 获取最新和上一个数据点
const latestData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 1]
const previousData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 2]
if (!latestData?.data || !previousData?.data) return 'up'
const latestKpi = latestData.data
const previousKpi = previousData.data
const latestActiveCalls = Number(latestKpi['SCSCF.07']) || 0
const previousActiveCalls = Number(previousKpi['SCSCF.07']) || 0
// 计算变化幅度
const change = latestActiveCalls - previousActiveCalls
if (change > 0) return 'up'
if (change < 0) return 'down'
return 'up' // 无变化时默认向上
}
// 计算active calls箭头
function calculateActiveCallsArrow() {
if (imsRealtimeRawData.value.length < 2) return '→'
// 获取最新和上一个数据点
const latestData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 1]
const previousData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 2]
if (!latestData?.data || !previousData?.data) return '→'
const latestKpi = latestData.data
const previousKpi = previousData.data
const latestActiveCalls = Number(latestKpi['SCSCF.07']) || 0
const previousActiveCalls = Number(previousKpi['SCSCF.07']) || 0
// 计算变化幅度
const change = latestActiveCalls - previousActiveCalls
if (change > 0) return '↗'
if (change < 0) return '↘'
return '→' // 无变化时显示水平箭头
}
// 计算active calls变化值
function calculateActiveCallsChange() {
if (imsRealtimeRawData.value.length < 2) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
// 获取最新和上一个数据点
const latestData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 1]
const previousData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 2]
if (!latestData?.data || !previousData?.data) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
const latestKpi = latestData.data
const previousKpi = previousData.data
const latestActiveCalls = Number(latestKpi['SCSCF.07']) || 0
const previousActiveCalls = Number(previousKpi['SCSCF.07']) || 0
// 修改即使值为0也参与计算因为这是有效的后端数据
// 计算变化幅度
const change = latestActiveCalls - previousActiveCalls
// 检查是否有变化
if (change === 0) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
const changeText = change > 0 ? `+${change.toFixed(0)}` : `${change.toFixed(0)}`
// 计算时间差
const timeDiff = calculateTimeDifference(latestData, previousData)
return `${changeText} ${t('views.perfManage.voiceOverView.last')} ${timeDiff}`
}
// 计算failed calls值
function calculateFailedCallsValue() {
if (imsRealtimeRawData.value.length === 0) return '-'
// 获取最新的数据
const latestData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 1]
if (!latestData || !latestData.data) return '-'
const kpiEvent = latestData.data
const scscf06 = Number(kpiEvent['SCSCF.06']) || 0
const scscf07 = Number(kpiEvent['SCSCF.07']) || 0
const failedCallsValue = scscf06 - scscf07
return failedCallsValue.toFixed(0)
}
// 计算failed calls箭头方向
function calculateFailedCallsArrowDirection() {
if (imsRealtimeRawData.value.length < 2) return 'up'
// 获取最新和上一个数据点
const latestData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 1]
const previousData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 2]
if (!latestData?.data || !previousData?.data) return 'up'
const latestKpi = latestData.data
const previousKpi = previousData.data
const latestScscf06 = Number(latestKpi['SCSCF.06']) || 0
const latestScscf07 = Number(latestKpi['SCSCF.07']) || 0
const previousScscf06 = Number(previousKpi['SCSCF.06']) || 0
const previousScscf07 = Number(previousKpi['SCSCF.07']) || 0
const latestFailedCalls = latestScscf06 - latestScscf07
const previousFailedCalls = previousScscf06 - previousScscf07
// 计算变化幅度
const change = latestFailedCalls - previousFailedCalls
if (change > 0) return 'up'
if (change < 0) return 'down'
return 'up' // 无变化时默认向上
}
// 计算failed calls箭头
function calculateFailedCallsArrow() {
if (imsRealtimeRawData.value.length < 2) return '→'
// 获取最新和上一个数据点
const latestData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 1]
const previousData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 2]
if (!latestData?.data || !previousData?.data) return '→'
const latestKpi = latestData.data
const previousKpi = previousData.data
const latestScscf06 = Number(latestKpi['SCSCF.06']) || 0
const latestScscf07 = Number(latestKpi['SCSCF.07']) || 0
const previousScscf06 = Number(previousKpi['SCSCF.06']) || 0
const previousScscf07 = Number(previousKpi['SCSCF.07']) || 0
const latestFailedCalls = latestScscf06 - latestScscf07
const previousFailedCalls = previousScscf06 - previousScscf07
// 计算变化幅度
const change = latestFailedCalls - previousFailedCalls
if (change > 0) return '↗'
if (change < 0) return '↘'
return '→' // 无变化时显示水平箭头
}
// 计算failed calls变化值
function calculateFailedCallsChange() {
if (imsRealtimeRawData.value.length < 2) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
// 获取最新和上一个数据点
const latestData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 1]
const previousData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 2]
if (!latestData?.data || !previousData?.data) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
const latestKpi = latestData.data
const previousKpi = previousData.data
const latestScscf06 = Number(latestKpi['SCSCF.06']) || 0
const latestScscf07 = Number(latestKpi['SCSCF.07']) || 0
const previousScscf06 = Number(previousKpi['SCSCF.06']) || 0
const previousScscf07 = Number(previousKpi['SCSCF.07']) || 0
const latestFailedCalls = latestScscf06 - latestScscf07
const previousFailedCalls = previousScscf06 - previousScscf07
// 计算变化幅度
const change = latestFailedCalls - previousFailedCalls
// 检查是否有变化
if (change === 0) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
const changeText = change > 0 ? `+${change.toFixed(0)}` : `${change.toFixed(0)}`
// 计算时间差
const timeDiff = calculateTimeDifference(latestData, previousData)
return `${changeText} ${t('views.perfManage.voiceOverView.last')} ${timeDiff}`
}
// 计算active registrations值
function calculateActiveRegistrationsValue() {
if (imsRealtimeRawData.value.length === 0) return '-'
// 获取最新的数据
const latestData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 1]
if (!latestData || !latestData.data) return '-'
const kpiEvent = latestData.data
const scscf03 = Number(kpiEvent['SCSCF.03']) || 0
// 修改即使值为0也显示"0",因为这是有效的后端数据
const activeRegistrationsValue = scscf03
return activeRegistrationsValue.toFixed(0)
}
// 计算active registrations箭头方向
function calculateActiveRegistrationsArrowDirection() {
if (imsRealtimeRawData.value.length < 2) return 'up'
// 获取最新和上一个数据点
const latestData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 1]
const previousData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 2]
if (!latestData?.data || !previousData?.data) return 'up'
const latestKpi = latestData.data
const previousKpi = previousData.data
const latestActiveRegistrations = Number(latestKpi['SCSCF.03']) || 0
const previousActiveRegistrations = Number(previousKpi['SCSCF.03']) || 0
// 计算变化幅度
const change = latestActiveRegistrations - previousActiveRegistrations
if (change > 0) return 'up'
if (change < 0) return 'down'
return 'up' // 无变化时默认向上
}
// 计算active registrations箭头
function calculateActiveRegistrationsArrow() {
if (imsRealtimeRawData.value.length < 2) return '→'
// 获取最新和上一个数据点
const latestData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 1]
const previousData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 2]
if (!latestData?.data || !previousData?.data) return '→'
const latestKpi = latestData.data
const previousKpi = previousData.data
const latestActiveRegistrations = Number(latestKpi['SCSCF.03']) || 0
const previousActiveRegistrations = Number(previousKpi['SCSCF.03']) || 0
// 计算变化幅度
const change = latestActiveRegistrations - previousActiveRegistrations
if (change > 0) return '↗'
if (change < 0) return '↘'
return '→' // 无变化时显示水平箭头
}
// 计算active registrations变化值
function calculateActiveRegistrationsChange() {
if (imsRealtimeRawData.value.length < 2) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
// 获取最新和上一个数据点
const latestData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 1]
const previousData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 2]
if (!latestData?.data || !previousData?.data) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
const latestKpi = latestData.data
const previousKpi = previousData.data
const latestActiveRegistrations = Number(latestKpi['SCSCF.03']) || 0
const previousActiveRegistrations = Number(previousKpi['SCSCF.03']) || 0
// 修改即使值为0也参与计算因为这是有效的后端数据
// 计算变化幅度
const change = latestActiveRegistrations - previousActiveRegistrations
// 检查是否有变化
if (change === 0) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
const changeText = change > 0 ? `+${change.toFixed(0)}` : `${change.toFixed(0)}`
// 计算时间差
const timeDiff = calculateTimeDifference(latestData, previousData)
return `${changeText} ${t('views.perfManage.voiceOverView.last')} ${timeDiff}`
}
// 计算failed registrations值
function calculateFailedRegistrationsValue() {
if (imsRealtimeRawData.value.length === 0) return '-'
// 获取最新的数据
const latestData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 1]
if (!latestData || !latestData.data) return '-'
const kpiEvent = latestData.data
const scscf04 = Number(kpiEvent['SCSCF.04']) || 0
const scscf03 = Number(kpiEvent['SCSCF.03']) || 0
const failedRegistrationsValue = scscf04 - scscf03
return failedRegistrationsValue.toFixed(0)
}
// 计算failed registrations箭头方向
function calculateFailedRegistrationsArrowDirection() {
if (imsRealtimeRawData.value.length < 2) return 'up'
// 获取最新和上一个数据点
const latestData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 1]
const previousData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 2]
if (!latestData?.data || !previousData?.data) return 'up'
const latestKpi = latestData.data
const previousKpi = previousData.data
const latestScscf04 = Number(latestKpi['SCSCF.04']) || 0
const latestScscf03 = Number(latestKpi['SCSCF.03']) || 0
const previousScscf04 = Number(previousKpi['SCSCF.04']) || 0
const previousScscf03 = Number(previousKpi['SCSCF.03']) || 0
const latestFailedRegistrations = latestScscf04 - latestScscf03
const previousFailedRegistrations = previousScscf04 - previousScscf03
// 计算变化幅度
const change = latestFailedRegistrations - previousFailedRegistrations
if (change > 0) return 'up'
if (change < 0) return 'down'
return 'up' // 无变化时默认向上
}
// 计算failed registrations箭头
function calculateFailedRegistrationsArrow() {
if (imsRealtimeRawData.value.length < 2) return '→'
// 获取最新和上一个数据点
const latestData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 1]
const previousData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 2]
if (!latestData?.data || !previousData?.data) return '→'
const latestKpi = latestData.data
const previousKpi = previousData.data
const latestScscf04 = Number(latestKpi['SCSCF.04']) || 0
const latestScscf03 = Number(latestKpi['SCSCF.03']) || 0
const previousScscf04 = Number(previousKpi['SCSCF.04']) || 0
const previousScscf03 = Number(previousKpi['SCSCF.03']) || 0
const latestFailedRegistrations = latestScscf04 - latestScscf03
const previousFailedRegistrations = previousScscf04 - previousScscf03
// 计算变化幅度
const change = latestFailedRegistrations - previousFailedRegistrations
if (change > 0) return '↗'
if (change < 0) return '↘'
return '→' // 无变化时显示水平箭头
}
// 计算failed registrations变化值
function calculateFailedRegistrationsChange() {
if (imsRealtimeRawData.value.length < 2) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
// 获取最新和上一个数据点
const latestData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 1]
const previousData = imsRealtimeRawData.value[imsRealtimeRawData.value.length - 2]
if (!latestData?.data || !previousData?.data) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
const latestKpi = latestData.data
const previousKpi = previousData.data
const latestScscf04 = Number(latestKpi['SCSCF.04']) || 0
const latestScscf03 = Number(latestKpi['SCSCF.03']) || 0
const previousScscf04 = Number(previousKpi['SCSCF.04']) || 0
const previousScscf03 = Number(previousKpi['SCSCF.03']) || 0
const latestFailedRegistrations = latestScscf04 - latestScscf03
const previousFailedRegistrations = previousScscf04 - previousScscf03
// 计算变化幅度
const change = latestFailedRegistrations - previousFailedRegistrations
// 检查是否有变化
if (change === 0) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m'
const changeText = change > 0 ? `+${change.toFixed(0)}` : `${change.toFixed(0)}`
// 计算时间差
const timeDiff = calculateTimeDifference(latestData, previousData)
return `${changeText} ${+t('views.perfManage.voiceOverView.last')} ${timeDiff}`
}
// 测试数据更新
// function testDataUpdate() {
// // console.log('测试数据更新')
//
// // 创建模拟的后端KPI数据消息格式
// const mockWebSocketMessage = {
// code: 1,
// data: {
// data: {
// 'SCSCF.03': Math.floor(Math.random() * 300000) + 200000, // active registrations
// 'SCSCF.04': Math.floor(Math.random() * 310000) + 200000, // total registrations
// 'SCSCF.05': Math.floor(Math.random() * 15000) + 10000, // MO calls
// 'SCSCF.06': Math.floor(Math.random() * 16000) + 10000, // total calls
// 'SCSCF.07': Math.floor(Math.random() * 15000) + 10000, // successful calls
// },
// groupId: `10_IMS_${selectedImsNeId.value}`
// },
// msg: 'success'
// }
//
// // console.log('模拟WebSocket消息:', mockWebSocketMessage)
//
// // 直接调用handleIMSRealtimeData函数
// handleIMSRealtimeData(mockWebSocketMessage)
// }
</script>
<style scoped>
.row-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 16px;
padding-left: 8px;
border-left: 3px solid #1890ff;
}
.dashboard-cards {
padding: 16px;
}
.metric-card {
border-radius: 16px;
height: 100%;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.09);
min-height: 180px;
display: flex;
flex-direction: column;
justify-content: center;
}
.card-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: space-between;
}
.card-content {
display: flex;
align-items: center;
}
.trend-chart {
flex: 2;
height: 60px;
min-width: 140px;
max-width: 220px;
display: flex;
flex-direction: column;
justify-content: flex-end;
margin-bottom: 25px; /* 为底部时间标注留出空间 */
}
.mini-chart {
width: 100%;
height: 100%;
margin-bottom: 0;
display: block;
position: relative;
}
.card-subtext {
font-size: 12px;
color: #b0b0b0;
margin-top: 2px;
letter-spacing: 0.1em;
}
.card-sub-sep {
margin-left: 18px;
}
.metric-info {
flex: 1;
margin-left: 16px;
text-align: right;
min-width: 90px;
}
.metric-value {
font-size: 24px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: flex-end;
}
.metric-change {
font-size: 12px;
color: #999;
margin-top: 4px;
}
.main-arrow.up { color: #4CAF50; margin-left: 8px; }
.main-arrow.down { color: #F44336; margin-left: 8px; }
.main-icon.phone { color: #1890ff; font-size: 22px; margin-left: 8px; }
.main-icon.calendar { color: #52c41a; font-size: 22px; margin-left: 8px; }
.full-width {
width: 100%;
text-align: center;
}
/* IMS网元选择框样式 */
.ims-select :deep(.ant-select-selector) {
border-radius: 8px !important;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.08);
min-height: 36px;
font-size: 15px;
border: 1px solid #d9d9d9;
transition: all 0.3s;
}
.ims-select :deep(.ant-select-selector:hover) {
border-color: #1890ff;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15);
}
.ims-select :deep(.ant-select-focused .ant-select-selector) {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.ims-select :deep(.ant-select-selection-item) {
font-weight: 600;
color: #333;
}
.ims-select :deep(.ant-select-arrow) {
color: #666;
}
/* 暗色主题适配 */
[data-theme='dark'] .ims-select :deep(.ant-select-selector) {
background-color: #1f1f1f;
border-color: #434343;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
[data-theme='dark'] .ims-select :deep(.ant-select-selection-item) {
color: #cacada;
}
[data-theme='dark'] .ims-select :deep(.ant-select-arrow) {
color: #cacada;
}
/* IMS Control Tower 角标样式 */
.control-tower-badge {
position: absolute;
left: 50%;
bottom: 0;
margin-left: 100px;
font-size: 12px;
font-weight: 500;
color: #1890ff;
background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
padding: 4px 12px;
border-radius: 12px;
border: 1px solid #91d5ff;
box-shadow: 0 2px 4px rgba(24, 144, 255, 0.1);
letter-spacing: 0.5px;
transform: translateY(4px);
}
/* 暗色主题适配 */
[data-theme='dark'] .control-tower-badge {
color: #69c0ff;
background: linear-gradient(135deg, #001529 0%, #002766 100%);
border-color: #1890ff;
box-shadow: 0 2px 4px rgba(105, 192, 255, 0.1);
}
</style>