diff --git a/src/api/perfManage/goldTarget.ts b/src/api/perfManage/goldTarget.ts index 88a38b0e..9ee39937 100644 --- a/src/api/perfManage/goldTarget.ts +++ b/src/api/perfManage/goldTarget.ts @@ -33,3 +33,28 @@ export async function getKPITitle(neType: string) { // 解析数据// return result; } +//忙时呼叫 +export async function getbusyhour(query: Record) { + return request({ + url: '/neData/ims/kpi/busy-hour', + method: 'GET', + params: query, + }); +} +//MOS指标 +export async function getMosHour(query: Record) { + return request({ + url: '/neData/ims/cdr/mos-hour', + method: 'GET', + params: query, + }); +} + +//CCT指标 +export async function getCctHour(query: Record) { + return request({ + url: '/neData/ims/cdr/cct-hour', + method: 'GET', + params: query, + }); +} diff --git a/src/views/perfManage/overview/index.vue b/src/views/perfManage/overview/index.vue index babc2a35..644172b7 100644 --- a/src/views/perfManage/overview/index.vue +++ b/src/views/perfManage/overview/index.vue @@ -88,6 +88,81 @@ + + + +
Busy Hour Call Attempts📞
+
+
+
+
+
+
+ {{ calculateCallAttemptsValue() }} + {{ calculateCallAttemptsArrow() }} +
+
{{ calculateCallAttemptsChange() }}
+
+
+
+
+ + +
Busy Hour Call Completions📞
+
+
+
+
+
+
+ {{ calculateCallCompletionsValue() }} + {{ calculateCallCompletionsArrow() }} +
+
{{ calculateCallCompletionsChange() }}
+
+
+
+
+
+ + + +
MOS📞
+
+
+
+
+
+
+ {{ calculateMosValue() }} + {{ calculateMosArrow() }} +
+
{{ calculateMosChange() }}
+
+
+
+
+ + +
+ Call Connection Time + 📞 +
+
+
+
+
+
+
+ {{ calculateCctValue() }} + {{ calculateCctArrow() }} +
+
{{ calculateCctChange() }}
+
+
+
+
+
{{ t('views.perfManage.voiceOverView.registration') }}
@@ -143,22 +218,78 @@ import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue' import * as echarts from 'echarts/core' import { LineChart } from 'echarts/charts' -import { GridComponent } from 'echarts/components' +import { GridComponent, GraphicComponent, TooltipComponent } from 'echarts/components' import { CanvasRenderer } from 'echarts/renderers' -import useNeListStore from '@/store/modules/ne_list'; +import useNeInfoStore from '@/store/modules/ne_list' import { WS } from '@/plugins/ws-websocket' -import { listKPIData } from '@/api/perfManage/goldTarget' +import { listKPIData ,getbusyhour, getMosHour, getCctHour} 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]) +echarts.use([LineChart, GridComponent, CanvasRenderer, GraphicComponent, TooltipComponent]) const callsChartRef = ref(null) const mosChartRef = ref(null) const failedCallsChartRef = ref(null) +const callAttemptsChartRef = ref(null) +const callCompletionsChartRef = ref(null) +const cctChartRef = ref(null) const regChartRef = ref(null) const failedRegChartRef = ref(null) +// 存储ResizeObserver实例 +const chartObservers = new Map() + +// 图表实例映射 +const chartInstances = new Map() + +// 设置图表ResizeObserver +function setupChartResizeObserver(container: HTMLElement, chart: echarts.ECharts) { + // 如果已存在observer,先断开连接 + if (chartObservers.has(container)) { + chartObservers.get(container)?.disconnect() + chartObservers.delete(container) + } + + // 创建新的ResizeObserver + const observer = new ResizeObserver(() => { + if (chart && !chart.isDisposed()) { + // 使用requestAnimationFrame确保在DOM更新完成后执行resize + requestAnimationFrame(() => { + chart.resize() + }) + } + }) + + // 开始观察容器尺寸变化 + observer.observe(container) + + // 存储observer实例 + chartObservers.set(container, observer) +} + +// 清理单个图表的ResizeObserver +function cleanupChartObserver(container: HTMLElement) { + const observer = chartObservers.get(container) + if (observer) { + observer.disconnect() + chartObservers.delete(container) + } + chartInstances.delete(container) +} + +// 全局resize处理函数,用于处理页面缩放变化 +function handleGlobalResize() { + // 延迟执行以确保DOM完全更新 + requestAnimationFrame(() => { + chartInstances.forEach((chart, container) => { + if (chart && !chart.isDisposed()) { + chart.resize() + } + }) + }) +} + // IMS网元列表 const imsNeList = ref<{ neId: string, neName: string }[]>([]) // 当前选中的IMS网元ID @@ -167,14 +298,23 @@ const selectedImsNeId = ref('') const imsWs = ref(null) // IMS实时原始数据(只存储当前选中网元) const imsRealtimeRawData = ref([]) +// Busy Hour数据(用于Call Attempts和Call Completions) +const busyHourData = ref(null) +// MOS数据 +const mosData = ref(null) +// CCT数据 +const cctData = ref(null) // WebSocket连接状态 -const wsStatus = ref('未连接') +const wsStatus = ref('no connection') // 获取IMS网元列表 onMounted(async () => { // console.log('组件挂载,开始获取IMS网元列表') // 调试信息 - const res = await useNeListStore().fnNelist() + // 添加全局窗口resize监听器,处理页面缩放变化 + window.addEventListener('resize', handleGlobalResize) + + const res = await useNeInfoStore().fnNelist() // console.log('获取到的网元列表响应:', res) // 调试信息 if (res && Array.isArray(res.data)) { @@ -185,8 +325,11 @@ onMounted(async () => { selectedImsNeId.value = imsNeList.value[0].neId // console.log('默认选中第一个IMS网元:', selectedImsNeId.value) // 调试信息 - // 先获取历史数据,再订阅实时数据 + // 先获取历史数据和Busy Hour数据,再订阅实时数据 await fetchHistoryData(selectedImsNeId.value) + await fetchBusyHourData(selectedImsNeId.value) + await fetchMosData(selectedImsNeId.value) + await fetchCctData(selectedImsNeId.value) subscribeImsRealtime(selectedImsNeId.value) } else { // console.warn('没有找到IMS类型的网元') // 调试信息 @@ -218,27 +361,19 @@ async function fetchHistoryData(neId: string) { pageNum: 1 } - // console.log('获取历史数据参数:', params) - const res = await listKPIData(params) - //console.log('历史数据响应:', res) - if (res.code === 200001 && Array.isArray(res.data)) { + if (res.code === RESULT_CODE_SUCCESS && 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() @@ -252,12 +387,135 @@ async function fetchHistoryData(neId: string) { } } +// 获取Busy Hour数据 +async function fetchBusyHourData(neId: string) { + if (!neId) return + + try { + // 获取当天日期的时间戳 + const today = new Date() + today.setHours(0, 0, 0, 0) // 设置为当天00:00:00 + const timestamp = today.getTime() + + // 构建请求参数 + const params = { + neId: neId, + timestamp: timestamp + } + + // console.log('获取Busy Hour数据参数:', params) + + const res = await getbusyhour(params) + // console.log('Busy Hour数据响应:', res) + + if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) { + busyHourData.value = res.data + // console.log('Busy Hour数据加载完成:', busyHourData.value) + + // 更新Call Attempts和Call Completions图表 + updateCallAttemptsChart() + updateCallCompletionsChart() + } else { + // console.warn('获取Busy Hour数据失败或数据为空') + busyHourData.value = null + } + } catch (error) { + // console.error('获取Busy Hour数据出错:', error) + busyHourData.value = null + } +} + +// 获取MOS数据 +async function fetchMosData(neId: string) { + if (!neId) return + + try { + // 获取当天日期的时间戳 + const today = new Date() + today.setHours(0, 0, 0, 0) // 设置为当天00:00:00 + const timestamp = today.getTime() + + // 构建请求参数 + const params = { + neId: neId, + timestamp: timestamp + } + + const res = await getMosHour(params) + // console.log('MOS数据响应:', res) + + if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) { + // 使用当天0点到现在的数据补全功能 + mosData.value = generateTodayTimeSeries(res.data, 'mosAvg') + // console.log('MOS数据加载完成:', mosData.value) + + // 更新MOS图表 + updateMosChart() + } else { + // console.warn('获取MOS数据失败或数据为空') + mosData.value = null + // 确保图表也更新为空状态 + updateMosChart() + } + } catch (error) { + // console.error('获取MOS数据出错:', error) + mosData.value = null + // 确保图表也更新为空状态 + updateMosChart() + } +} + +// 获取CCT数据 +async function fetchCctData(neId: string) { + if (!neId) return + + try { + // 获取当天日期的时间戳 + const today = new Date() + today.setHours(0, 0, 0, 0) // 设置为当天00:00:00 + const timestamp = today.getTime() + + // 构建请求参数 + const params = { + neId: neId, + timestamp: timestamp + } + + // console.log('获取CCT数据参数:', params) + + const res = await getCctHour(params) + // console.log('CCT数据响应:', res) + + if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) { + // 使用当天0点到现在的数据补全功能 + cctData.value = generateTodayTimeSeries(res.data, 'cctAvg') + // console.log('CCT数据加载完成:', cctData.value) + + // 更新CCT图表 + updateCctChart() + } else { + // console.warn('获取CCT数据失败或数据为空') + cctData.value = null + // 确保图表也更新为空状态 + updateCctChart() + } + } catch (error) { + // console.error('获取CCT数据出错:', error) + cctData.value = null + // 确保图表也更新为空状态 + updateCctChart() + } +} + // 切换IMS网元时,重新订阅 async function onImsNeChange() { // console.log('切换IMS网元,新的网元ID:', selectedImsNeId.value) // 调试信息 // 切换网元时,先清空旧数据 imsRealtimeRawData.value = [] + busyHourData.value = null + mosData.value = null + cctData.value = null // 强制触发Vue响应式更新,确保所有计算值立即更新为默认状态 await nextTick() @@ -267,16 +525,23 @@ async function onImsNeChange() { updateFailedCallsChart() updateActiveRegistrationsChart() updateFailedRegistrationsChart() + updateCallAttemptsChart() + updateCallCompletionsChart() + updateMosChart() + updateCctChart() - // 先获取历史数据,再订阅实时数据 + // 先获取历史数据和Busy Hour数据,再订阅实时数据 await fetchHistoryData(selectedImsNeId.value) + await fetchBusyHourData(selectedImsNeId.value) + await fetchMosData(selectedImsNeId.value) + await fetchCctData(selectedImsNeId.value) subscribeImsRealtime(selectedImsNeId.value) } // 订阅指定IMS网元实时数据 function subscribeImsRealtime(neId: string) { // console.log('开始订阅IMS网元数据,网元ID:', neId) // 调试信息 - wsStatus.value = '连接中...' // 更新状态 + wsStatus.value = 'Connecting...' // 更新状态 // 关闭旧WebSocket if (imsWs.value) { @@ -299,703 +564,185 @@ function subscribeImsRealtime(neId: string) { onmessage: handleIMSRealtimeData, onerror: (error: any) => { // console.error('WebSocket连接错误:', error) // 调试信息 - wsStatus.value = '连接错误' // 更新状态 + wsStatus.value = 'Connect error' // 更新状态 }, onopen: () => { // console.log('WebSocket连接已建立') // 调试信息 - wsStatus.value = '已连接' // 更新状态 + wsStatus.value = 'Connected' // 更新状态 }, onclose: () => { // console.log('WebSocket连接已关闭') // 调试信息 - wsStatus.value = '已断开' // 更新状态 + wsStatus.value = 'Disconnect' // 更新状态 } }) } -// 组件卸载时关闭WebSocket +// 组件卸载时关闭WebSocket和清理ResizeObserver onBeforeUnmount(() => { + // 移除全局resize监听器 + window.removeEventListener('resize', handleGlobalResize) + if (imsWs.value) { imsWs.value.close() imsWs.value = null } - wsStatus.value = '已断开' // 更新状态 + wsStatus.value = 'Disconnect' // 更新状态 + + // 清理所有ResizeObserver + chartObservers.forEach((observer, container) => { + observer.disconnect() + // 销毁对应的图表实例 + const chart = chartInstances.get(container) + if (chart && !chart.isDisposed()) { + chart.dispose() + } + }) + chartObservers.clear() + chartInstances.clear() }) // 更新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 + updateChart({ + chartRef: callsChartRef, + data: imsRealtimeRawData.value, + dataExtractor: (item) => Number(item.data['SCSCF.07']) || 0, + lineColor: '#1890ff', + areaColor: 'rgba(24,144,255,0.1)', + labelColor: '#1890ff', + dataType: 'realtime-enhanced', // 使用增强的实时数据类型 + formatValue: (value: number) => value.toString() }) - - 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)' } // 淡灰色填充 - }] - }) - - // 清除所有旧标注,即使在无数据时也要清理 - const chartContainer = callsChartRef.value - if (chartContainer) { - const existingLabels = chartContainer.querySelectorAll('.chart-label') - existingLabels.forEach(label => label.remove()) - } - 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 + updateChart({ + chartRef: failedCallsChartRef, + data: imsRealtimeRawData.value, + dataExtractor: (item) => { + const kpiEvent = item.data + const scscf06 = Number(kpiEvent['SCSCF.06']) || 0 + const scscf07 = Number(kpiEvent['SCSCF.07']) || 0 + return scscf06 - scscf07 + }, + lineColor: '#faad14', + areaColor: 'rgba(250,173,20,0.1)', + labelColor: '#faad14', + dataType: 'realtime-enhanced', // 使用增强的实时数据类型 + formatValue: (value: number) => value.toString() }) - - // 如果没有数据,显示默认的平直线 - 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)' } // 淡灰色填充 - }] - }) - - // 清除所有旧标注,即使在无数据时也要清理 - const chartContainer = failedCallsChartRef.value - if (chartContainer) { - const existingLabels = chartContainer.querySelectorAll('.chart-label') - existingLabels.forEach(label => label.remove()) - } - 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 + updateChart({ + chartRef: regChartRef, + data: imsRealtimeRawData.value, + dataExtractor: (item) => Number(item.data['SCSCF.03']) || 0, + lineColor: '#1890ff', + areaColor: 'rgba(24,144,255,0.1)', + labelColor: '#1890ff', + dataType: 'realtime-enhanced', // 使用增强的实时数据类型 + formatValue: (value: number) => value.toString() }) - - // 如果没有数据,显示默认的平直线 - 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)' } // 淡灰色填充 - }] - }) - - // 清除所有旧标注,即使在无数据时也要清理 - const chartContainer = regChartRef.value - if (chartContainer) { - const existingLabels = chartContainer.querySelectorAll('.chart-label') - existingLabels.forEach(label => label.remove()) - } - 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 + updateChart({ + chartRef: failedRegChartRef, + data: imsRealtimeRawData.value, + dataExtractor: (item) => { + const kpiEvent = item.data + const scscf04 = Number(kpiEvent['SCSCF.04']) || 0 + const scscf03 = Number(kpiEvent['SCSCF.03']) || 0 + return scscf04 - scscf03 + }, + lineColor: '#f5222d', + areaColor: 'rgba(245,34,45,0.1)', + labelColor: '#f5222d', + dataType: 'realtime-enhanced', // 使用增强的实时数据类型 + formatValue: (value: number) => value.toString() }) +} - // 如果没有数据,显示默认的平直线 - 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)' } // 淡灰色填充 - }] - }) - - // 清除所有旧标注,即使在无数据时也要清理 - const chartContainer = failedRegChartRef.value - if (chartContainer) { - const existingLabels = chartContainer.querySelectorAll('.chart-label') - existingLabels.forEach(label => label.remove()) +// 更新Call Attempts图表 +function updateCallAttemptsChart() { + updateChart({ + chartRef: callAttemptsChartRef, + data: busyHourData.value || [], + dataExtractor: (item: any) => Number(item.callAttempts) || 0, + lineColor: '#52c41a', + areaColor: 'rgba(82,196,26,0.1)', + labelColor: '#52c41a', + dataType: 'hourly', + formatValue: (value: number) => value.toString(), + timeCalculator: (data) => { + if (data.length === 0) return '--' + const oldestTime = Number(data[0]?.timeGroup) || Date.now() + return calculateRelativeTime(oldestTime) } - 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 = '--' +// 更新Call Completions图表 +function updateCallCompletionsChart() { + updateChart({ + chartRef: callCompletionsChartRef, + data: busyHourData.value || [], + dataExtractor: (item: any) => Number(item.callCompletions) || 0, + lineColor: '#722ed1', + areaColor: 'rgba(114,46,209,0.1)', + labelColor: '#722ed1', + dataType: 'hourly', + formatValue: (value: number) => value.toString(), + timeCalculator: (data) => { + if (data.length === 0) return '--' + const oldestTime = Number(data[0]?.timeGroup) || Date.now() + return calculateRelativeTime(oldestTime) } + }) +} - 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') +// 更新MOS图表 +function updateMosChart() { + updateChart({ + chartRef: mosChartRef, + data: mosData.value || [], + dataExtractor: (item: any) => Number(item.mosAvg) || 0, + lineColor: '#52c41a', + areaColor: 'rgba(82,196,26,0.1)', + labelColor: '#52c41a', + dataType: 'hourly', + formatValue: (value: number) => value.toFixed(2), + timeCalculator: (data) => { + if (data.length === 0) return '--' + // MOS时间戳是秒级,需要转换为毫秒级 + const oldestTime = (Number(data[0]?.timeGroup) || 0) * 1000 + return calculateRelativeTime(oldestTime) + } + }) +} - chartContainer.appendChild(maxLabel) - chartContainer.appendChild(latestLabel) - chartContainer.appendChild(minLabel) - chartContainer.appendChild(oldestTimeLabel) - chartContainer.appendChild(nowTimeLabel) - } +// 更新CCT图表 +function updateCctChart() { + updateChart({ + chartRef: cctChartRef, + data: cctData.value || [], + dataExtractor: (item: any) => Number(item.cctAvg) || 0, + lineColor: '#fa8c16', + areaColor: 'rgba(250,140,22,0.1)', + labelColor: '#fa8c16', + dataType: 'hourly', + formatValue: (value: number) => value.toFixed(2), + timeCalculator: (data) => { + if (data.length === 0) return '--' + // CCT时间戳是秒级,需要转换为毫秒级 + const oldestTime = (Number(data[0]?.timeGroup) || 0) * 1000 + return calculateRelativeTime(oldestTime) + } + }) } // 处理IMS实时数据(只存储当前选中网元) @@ -1006,7 +753,7 @@ function handleIMSRealtimeData(res: any) { const { code, data, msg } = res // 检查是否是错误响应 - 修改为适配实际的code值 - if (code !== 200001 || !data) { + if (code !== RESULT_CODE_SUCCESS || !data) { // console.warn('收到错误响应或数据格式不正确:', res) // 调试信息 return } @@ -1034,7 +781,7 @@ function handleIMSRealtimeData(res: any) { return } - console.log('处理IMS网元KPI数据:', kpiEvent) // 调试信息 + // console.log('处理IMS网元KPI数据:', kpiEvent) // 调试信息 // 确保数据结构正确 const dataToStore = { @@ -1090,60 +837,10 @@ onMounted(() => { chart.setOption(defaultChartOption) } - // MOS - 保持原有的模拟数据 + // 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' - } - } - ] - }) + chart.setOption(defaultChartOption) } // failed calls @@ -1152,6 +849,18 @@ onMounted(() => { chart.setOption(defaultChartOption) } + // call attempts + if (callAttemptsChartRef.value) { + const chart = echarts.init(callAttemptsChartRef.value) + chart.setOption(defaultChartOption) + } + + // call completions + if (callCompletionsChartRef.value) { + const chart = echarts.init(callCompletionsChartRef.value) + chart.setOption(defaultChartOption) + } + // active registrations if (regChartRef.value) { const chart = echarts.init(regChartRef.value) @@ -1163,6 +872,18 @@ onMounted(() => { const chart = echarts.init(failedRegChartRef.value) chart.setOption(defaultChartOption) } + + // MOS + if (mosChartRef.value) { + const chart = echarts.init(mosChartRef.value) + chart.setOption(defaultChartOption) + } + + // CCT + if (cctChartRef.value) { + const chart = echarts.init(cctChartRef.value) + chart.setOption(defaultChartOption) + } }) // 计算MO值 (SCSCF.05/SCSCF.06)*100 @@ -1203,13 +924,13 @@ function calculateMTValue() { // 计算MO变化值(完善版本) function calculateMOChange() { - if (imsRealtimeRawData.value.length < 2) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m' + if (imsRealtimeRawData.value.length < 2) return t('views.perfManage.voiceOverView.last') +' 1 Min'+' +0%' // 获取最新和上一个数据点 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' + if (!latestData?.data || !previousData?.data) return t('views.perfManage.voiceOverView.last') +' 1 Min'+' +0%' const latestKpi = latestData.data const previousKpi = previousData.data @@ -1218,31 +939,31 @@ function calculateMOChange() { const previousMO = calculateMOValueFromData(previousKpi) // 检查MO是否有有效值 - if (latestMO === '-' || previousMO === '-') return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m' + if (latestMO === '-' || previousMO === '-') return t('views.perfManage.voiceOverView.last') +' 1 Min'+' +0%' // 计算变化幅度 const change = latestMO - previousMO // 检查是否有变化 - if (change === 0) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m' + if (change === 0) return t('views.perfManage.voiceOverView.last') +' 1 Min'+' +0%' const changeText = change > 0 ? `+${change.toFixed(2)}%` : `${change.toFixed(2)}%` // 计算时间差 const timeDiff = calculateTimeDifference(latestData, previousData) - return `${changeText} ${t('views.perfManage.voiceOverView.last')} ${timeDiff}` + return `${t('views.perfManage.voiceOverView.last')} ${timeDiff} ${changeText}` } // 计算MT变化值(完善版本) function calculateMTChange() { - if (imsRealtimeRawData.value.length < 2) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m' + if (imsRealtimeRawData.value.length < 2) return t('views.perfManage.voiceOverView.last') +' 1 Min'+' +0%' // 获取最新和上一个数据点 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' + if (!latestData?.data || !previousData?.data) return t('views.perfManage.voiceOverView.last') +' 1 Min'+' +0%' const latestKpi = latestData.data const previousKpi = previousData.data @@ -1251,20 +972,20 @@ function calculateMTChange() { const previousMT = calculateMTValueFromData(previousKpi) // 检查MT是否有有效值 - if (latestMT === '-' || previousMT === '-') return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m' + if (latestMT === '-' || previousMT === '-') return t('views.perfManage.voiceOverView.last') +' 1 Min'+' +0%' // 计算变化幅度 const change = latestMT - previousMT // 检查是否有变化 - if (change === 0) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m' + if (change === 0) return t('views.perfManage.voiceOverView.last') +' 1 Min'+' +0%' const changeText = change > 0 ? `+${change.toFixed(2)}%` : `${change.toFixed(2)}%` // 计算时间差 const timeDiff = calculateTimeDifference(latestData, previousData) - return `${changeText} ${t('views.perfManage.voiceOverView.last')} ${timeDiff}` + return `${t('views.perfManage.voiceOverView.last')} ${timeDiff} ${changeText}` } // 计算时间差函数 @@ -1273,9 +994,6 @@ 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) @@ -1285,15 +1003,11 @@ function calculateTimeDifference(latestData: any, previousData: any) { // 转换为分钟 const diffMinutes = Math.floor(diffSeconds / 60) - console.log('计算时间差 - 时间差(毫秒):', diffMs) - console.log('计算时间差 - 时间差(秒):', diffSeconds) - console.log('计算时间差 - 时间差(分钟):', diffMinutes) - // 根据时间差返回合适的格式 if (diffMinutes > 0) { - return `${diffMinutes}m` + return `${diffMinutes} Min` } else { - return `${diffSeconds}s` + return `${diffSeconds} Second` } } @@ -1305,14 +1019,670 @@ function calculateRelativeTime(timestamp: number) { const diffHours = Math.floor(diffMinutes / 60) if (diffHours >= 1) { - return `${diffHours}h` + return `${diffHours} Hour` } else if (diffMinutes >= 1) { - return `${diffMinutes}m` + return `${diffMinutes} Min` } else { - return '1m' + return '1 Min' } } +// 计算MOS/CCT时间差函数(处理秒级时间戳) +function calculateMosCctTimeDifference(latestData: any, previousData: any) { + // MOS/CCT的时间戳是秒级,需要转换为毫秒级 + const latestTime = (Number(latestData.timeGroup) || 0) * 1000 + const previousTime = (Number(previousData.timeGroup) || 0) * 1000 + + // 计算时间差(毫秒) + const diffMs = Math.abs(latestTime - previousTime) + + // 转换为秒 + const diffSeconds = Math.floor(diffMs / 1000) + + // 转换为分钟 + const diffMinutes = Math.floor(diffSeconds / 60) + + // 转换为小时 + const diffHours = Math.floor(diffMinutes / 60) + + // 根据时间差返回合适的格式 + if (diffHours > 0) { + return `${diffHours} Hour` + } else if (diffMinutes > 0) { + return `${diffMinutes} Min` + } else { + return `${diffSeconds} Second` + } +} + +// ========== 通用工具函数 ========== + +// 通用图表更新函数 +function updateChart(config: { + chartRef: any, + data: any[], + dataExtractor: (item: any, index?: number) => number, + lineColor: string, + areaColor: string, + labelColor: string, + dataType?: 'realtime' | 'hourly' | 'realtime-enhanced', + formatValue?: (value: number) => string, + timeCalculator?: (data: any[]) => string +}) { + if (!config.chartRef.value) return + + // 获取图表实例 + let chart = echarts.getInstanceByDom(config.chartRef.value) + if (!chart) { + chart = echarts.init(config.chartRef.value) + + // 设置ResizeObserver以监听容器尺寸变化 + setupChartResizeObserver(config.chartRef.value, chart) + } + + // 更新图表实例映射 + chartInstances.set(config.chartRef.value, chart) + + // 准备图表数据 + const chartData = config.data.map(config.dataExtractor) + + // 如果没有数据,显示默认的平直线 + if (chartData.length === 0) { + const defaultData = [0, 0, 0, 0, 0] + const xAxisData = [1, 2, 3, 4, 5] + + chart.setOption({ + grid: { + left: 5, // 给图表左侧留出少量空间,避免被容器边界遮挡 + right: 20, // 减少右侧边距,为gap腾出空间 + top: 5, // 减少顶部边距 + bottom: (config.dataType === 'hourly' || config.dataType === 'realtime-enhanced') ? 25 : 5 // 调整底部边距 + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'none' + }, + formatter: (params: any) => { + if (!params || params.length === 0) return '' + const param = params[0] + if (config.dataType === 'hourly') { + // 将小时数转换为AM/PM格式 + const hour = parseInt(param.name) + let displayHour = hour + let period = 'AM' + + if (hour === 0) { + displayHour = 12 + period = 'AM' + } else if (hour < 12) { + displayHour = hour + period = 'AM' + } else if (hour === 12) { + displayHour = 12 + period = 'PM' + } else { + displayHour = hour - 12 + period = 'PM' + } + + return `${displayHour} ${period}:0` + } else if (config.dataType === 'realtime-enhanced') { + const minutesAgo = defaultData.length - 1 - param.dataIndex + return `${minutesAgo}Min ago: 0` + } else { + return ` ${param.dataIndex + 1}: 0` + } + }, + backgroundColor: 'rgba(0,0,0,0.8)', + textStyle: { color: '#fff', fontSize: 12 }, + borderWidth: 0, + padding: [6, 10], + extraCssText: 'border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.2);' + }, + xAxis: { + type: 'category', + show: config.dataType === 'hourly', + data: config.dataType === 'hourly' ? + (() => { + const now = new Date() + const currentHour = now.getHours() + const maxHour = Math.min(currentHour, 4) // 最多显示到4小时或当前小时 + return Array.from({ length: maxHour + 1 }, (_, i) => + i === maxHour ? `${i}Hour` : i.toString() + ) + })() : xAxisData, + axisLabel: { + fontSize: 11, + color: '#999', + interval: 0 + }, + axisLine: { show: false }, + axisTick: { show: false } + }, + yAxis: { type: 'value', show: false }, + series: [{ + data: (config.dataType === 'hourly' || config.dataType === 'realtime-enhanced') ? defaultData.map((value, index) => ({ + value, + itemStyle: { + color: '#d9d9d9' // 空数据时所有数据点都使用灰色 + } + })) : defaultData, + type: 'line', + symbol: 'circle', // 所有图表都显示数据点,以支持hover tooltip + symbolSize: 4, // 设置数据点大小 + lineStyle: { width: 2, color: '#d9d9d9' }, + areaStyle: { color: 'rgba(217,217,217,0.1)' } + }], + graphic: [] + }) + + // 清除所有旧标注 + const chartContainer = config.chartRef.value + if (chartContainer) { + const existingLabels = chartContainer.querySelectorAll('.chart-label') + existingLabels.forEach((label: any) => label.remove()) + } + return + } + + // 实时数据需要补充和限制数据点 + let processedData = [...chartData] + if (config.dataType === 'realtime' || config.dataType === 'realtime-enhanced') { + // 如果数据不足,补充默认数据 + while (processedData.length < 5) { + processedData.unshift(0) + } + // 限制数据点数量为最近30个 + if (processedData.length > 30) { + processedData.splice(0, processedData.length - 30) + } + } + + // 生成时间轴数据 + let xAxisData: (string | number)[] + if (config.dataType === 'hourly') { + // 小时数据:生成绝对时间标签,基于实际数据长度 + const now = new Date() + const currentHour = now.getHours() + + // 生成统一格式的时间标签,避免标签宽度不一致 + xAxisData = Array.from({ length: processedData.length }, (_, i) => i.toString()) + } else if (config.dataType === 'realtime-enhanced') { + // 增强实时数据:生成分钟级时间标签,但只显示关键时间点 + xAxisData = Array.from({ length: processedData.length }, (_, i) => { + const minutesAgo = processedData.length - 1 - i + // 只为特定时间点生成标签,其他返回空字符串 + if (i === 0 || i === processedData.length - 1 || minutesAgo % 5 === 0) { + return minutesAgo.toString() + } + return '' // 空标签,但数据点仍然存在 + }) + } else { + // 普通实时数据:使用数字序列 + xAxisData = Array.from({ length: processedData.length }, (_, i) => i + 1) + } + + // 计算最大值、最小值、最新值 + const maxValue = Math.max(...processedData) + const minValue = Math.min(...processedData) + const latestValue = processedData[processedData.length - 1] + + chart.setOption({ + grid: { + left: 5, // 给图表左侧留出少量空间,避免被容器边界遮挡 + right: 20, // 减少右侧边距,为gap腾出空间 + top: 5, // 减少顶部边距 + bottom: (config.dataType === 'hourly' || config.dataType === 'realtime-enhanced') ? 25 : 5 // 调整底部边距 + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'none' + }, + formatter: (params: any) => { + if (!params || params.length === 0) return '' + const param = params[0] + const formatValue = config.formatValue || ((val: number) => val.toString()) + const value = typeof param.value === 'object' ? param.value.value : param.value + if (config.dataType === 'hourly') { + // 将小时数转换为AM/PM格式 + const hour = parseInt(param.name) + let displayHour = hour + let period = 'AM' + + if (hour === 0) { + displayHour = 12 + period = 'AM' + } else if (hour < 12) { + displayHour = hour + period = 'AM' + } else if (hour === 12) { + displayHour = 12 + period = 'PM' + } else { + displayHour = hour - 12 + period = 'PM' + } + + return `${displayHour} ${period}:${formatValue(value)}` + } else if (config.dataType === 'realtime-enhanced') { + // 对于增强实时数据,计算实际的分钟数 + const minutesAgo = processedData.length - 1 - param.dataIndex + return `${minutesAgo}Min ago: ${formatValue(value)}` + } else { + // 对于普通实时数据,显示数据点索引和值 + return `数据点 ${param.dataIndex + 1}: ${formatValue(value)}` + } + }, + backgroundColor: 'rgba(0,0,0,0.8)', + textStyle: { color: '#fff', fontSize: 12 }, + borderWidth: 0, + padding: [6, 10], + extraCssText: 'border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.2);' + }, + xAxis: { + type: 'category', + show: config.dataType === 'hourly' || config.dataType === 'realtime-enhanced', // 小时数据和增强实时数据显示x轴 + data: xAxisData, + axisLabel: { + fontSize: 11, + color: '#999', + interval: 0, // 显示所有标签(已在数据生成阶段控制标签密度) + rotate: 0, + margin: 8, + overflow: 'truncate', + formatter: undefined // 移除Hour后缀,保持所有标签格式一致 + }, + axisLine: { show: false }, + axisTick: { show: false } + }, + yAxis: { type: 'value', show: false }, + series: [{ + data: processedData.map((value, index) => ({ + value, + itemStyle: { + color: index === processedData.length - 1 + ? config.lineColor // 最新值使用原色 + : `${config.lineColor}80` // 其他数据点使用50%透明度的颜色 + } + })), + type: 'line', + symbol: 'circle', // 所有图表都显示数据点,以支持hover tooltip + symbolSize: 4, // 设置数据点大小 + lineStyle: { width: 2, color: config.lineColor }, + areaStyle: { color: config.areaColor } + }], + graphic: [] + }) + + // 只为普通实时数据添加侧边数值标签,小时数据和增强实时数据通过hover tooltip显示 + if (config.dataType !== 'hourly' && config.dataType !== 'realtime-enhanced') { + addChartLabels({ + container: config.chartRef.value, + maxValue, + minValue, + latestValue, + labelColor: config.labelColor, + formatValue: config.formatValue, + dataType: config.dataType + }) + } + + // 添加时间标签(只为普通实时数据添加,小时数据和增强实时数据通过xAxis显示) + if (config.data.length > 0 && config.dataType !== 'hourly' && config.dataType !== 'realtime-enhanced' && config.timeCalculator) { + addTimeLabels({ + container: config.chartRef.value, + timeText: config.timeCalculator(config.data) + }) + } +} + +// 通用标签添加函数 +function addChartLabels(config: { + container: any, + maxValue: number, + minValue: number, + latestValue: number, + labelColor: string, + formatValue?: (value: number) => string, + dataType?: 'realtime' | 'hourly' +}) { + if (!config.container) return + + // 清除之前的标注 + const existingLabels = config.container.querySelectorAll('.chart-label') + existingLabels.forEach((label: any) => label.remove()) + + const formatValue = config.formatValue || ((val: number) => val.toString()) + + // 小时数据使用改进的定位策略 - 相对于图表容器定位 + if (config.dataType === 'hourly') { + // 直接在图表容器内定位,确保图表容器有相对定位 + const chartContainer = config.container + + // 创建hourly标签的辅助函数 + const createHourlyLabel = (value: string, color: string, position: string) => { + const label = document.createElement('div') + label.className = 'chart-label hourly-label' + label.style.cssText = ` + position: absolute; + right: -45px; /* 相对于图表容器右侧外部定位,稍微靠近一些 */ + ${position} + font-size: 12px; + font-weight: bold; + color: ${color}; + pointer-events: none; + text-align: left; /* 标签文本左对齐,向右延伸 */ + min-width: 40px; + white-space: nowrap; + z-index: 10; + ` + label.textContent = value + return label + } + + // 清除旧的hourly标签 + const existingHourlyLabels = chartContainer.querySelectorAll('.hourly-label') + existingHourlyLabels.forEach((label: any) => label.remove()) + + // 只显示当前值标签 - 居中显示 + chartContainer.appendChild(createHourlyLabel(formatValue(config.latestValue), config.labelColor, 'top: 50%; transform: translateY(-50%);')) + + return // 早期返回,不执行下面的标准标签逻辑 + } + + // 实时数据使用原有逻辑 + const rightPosition = '8px' + + // 添加右侧数值标注 + const maxLabel = document.createElement('div') + maxLabel.className = 'chart-label' + maxLabel.style.cssText = ` + position: absolute; + right: ${rightPosition}; + top: 8px; /* 实时数据固定位置 */ + font-size: 12px; + font-weight: bold; + color: #666; + pointer-events: none; + z-index: 10; + text-align: right; + min-width: 40px; + white-space: nowrap; + ` + maxLabel.textContent = formatValue(config.maxValue) + + const latestLabel = document.createElement('div') + latestLabel.className = 'chart-label' + latestLabel.style.cssText = ` + position: absolute; + right: ${rightPosition}; + top: 50%; /* 实时数据居中 */ + transform: translateY(-50%); + font-size: 12px; + font-weight: bold; + color: ${config.labelColor}; + pointer-events: none; + z-index: 10; + text-align: right; + min-width: 40px; + white-space: nowrap; + ` + latestLabel.textContent = formatValue(config.latestValue) + + const minLabel = document.createElement('div') + minLabel.className = 'chart-label' + minLabel.style.cssText = ` + position: absolute; + right: ${rightPosition}; + bottom: 8px; /* 实时数据固定位置 */ + font-size: 12px; + font-weight: bold; + color: #666; + pointer-events: none; + z-index: 10; + text-align: right; + min-width: 40px; + white-space: nowrap; + ` + minLabel.textContent = formatValue(config.minValue) + + config.container.appendChild(maxLabel) + config.container.appendChild(latestLabel) + config.container.appendChild(minLabel) +} + +// 通用时间标签添加函数 +function addTimeLabels(config: { + container: any, + timeText: string +}) { + if (!config.container) return + + // 添加底部时间标注 + 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; + ` + oldestTimeLabel.textContent = config.timeText + + 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 = 'Now' + + config.container.appendChild(oldestTimeLabel) + config.container.appendChild(nowTimeLabel) +} + +// 小时数据专用时间标签函数 +function addHourlyTimeLabels(config: { + container: any, + data: any[] +}) { + if (!config.container || !config.data || config.data.length === 0) return + + // 计算当前小时 + const now = new Date() + const currentHour = now.getHours() + + // 清除之前的时间标签 + const existingTimeLabels = config.container.querySelectorAll('.chart-time-label') + existingTimeLabels.forEach((label: any) => label.remove()) + + // 添加左侧时间标签(0点) + const beginTimeLabel = document.createElement('div') + beginTimeLabel.className = 'chart-time-label' + beginTimeLabel.style.cssText = ` + position: absolute; + left: 8px; + bottom: -20px; + font-size: 11px; + color: #999; + pointer-events: none; + z-index: 10; + ` + beginTimeLabel.textContent = '0' + + // 添加中间时间标签(如果当前小时大于6,显示中间时间) + if (currentHour > 6) { + const midHour = Math.floor(currentHour / 2) + const midTimeLabel = document.createElement('div') + midTimeLabel.className = 'chart-time-label' + midTimeLabel.style.cssText = ` + position: absolute; + left: 50%; + transform: translateX(-50%); + bottom: -20px; + font-size: 11px; + color: #999; + pointer-events: none; + z-index: 10; + ` + midTimeLabel.textContent = midHour.toString() + config.container.appendChild(midTimeLabel) + } + + // 添加右侧时间标签(当前小时Hour) + const endTimeLabel = document.createElement('div') + endTimeLabel.className = 'chart-time-label' + endTimeLabel.style.cssText = ` + position: absolute; + right: 8px; + bottom: -20px; + font-size: 11px; + color: #999; + pointer-events: none; + z-index: 10; + ` + endTimeLabel.textContent = `${currentHour}Hour` + + config.container.appendChild(beginTimeLabel) + config.container.appendChild(endTimeLabel) +} + +// 通用计算值函数 +function calculateMetricValue(config: { + data: any[] | null, + valueExtractor: (item: any) => number, + formatValue?: (value: number) => string +}) { + if (!config.data || config.data.length === 0) return '-' + + const latestData = config.data[config.data.length - 1] + if (!latestData) return '-' + + const value = config.valueExtractor(latestData) + const formatValue = config.formatValue || ((val: number) => val.toString()) + + return formatValue(value) +} + +// 通用计算变化值函数 +function calculateMetricChange(config: { + data: any[], + valueExtractor: (item: any) => number | string, + timeDiffCalculator?: (latest: any, previous: any) => string, + defaultTimeText?: string, + formatValue?: (value: number) => string, + isPercentage?: boolean +}) { + if (!config.data || config.data.length < 2) { + const defaultTime = config.defaultTimeText || '1 Min' + return `${t('views.perfManage.voiceOverView.last')} ${defaultTime} +0${config.isPercentage ? '%' : ''}` + } + + const latestData = config.data[config.data.length - 1] + const previousData = config.data[config.data.length - 2] + + const latestValue = config.valueExtractor(latestData) + const previousValue = config.valueExtractor(previousData) + + // 检查值是否有效 + if (latestValue === '-' || previousValue === '-' || + (typeof latestValue === 'number' && typeof previousValue === 'number')) { + + if (typeof latestValue !== 'number' || typeof previousValue !== 'number') { + const defaultTime = config.defaultTimeText || '1 Min' + return `${t('views.perfManage.voiceOverView.last')} ${defaultTime} +0${config.isPercentage ? '%' : ''}` + } + + const change = latestValue - previousValue + if (change === 0) { + const defaultTime = config.defaultTimeText || '1 Min' + return `${t('views.perfManage.voiceOverView.last')} ${defaultTime} +0${config.isPercentage ? '%' : ''}` + } + + const formatValue = config.formatValue || ((val: number) => val.toFixed(config.isPercentage ? 2 : 0)) + const changeText = change > 0 ? `+${formatValue(Math.abs(change))}` : `-${formatValue(Math.abs(change))}` + const suffix = config.isPercentage ? '%' : '' + + const timeDiff = config.timeDiffCalculator ? + config.timeDiffCalculator(latestData, previousData) : + (config.defaultTimeText || '1 Min') + + return `${t('views.perfManage.voiceOverView.last')} ${timeDiff} ${changeText}${suffix}` + } + + const defaultTime = config.defaultTimeText || '1 Min' + return `${t('views.perfManage.voiceOverView.last')} ${defaultTime} +0${config.isPercentage ? '%' : ''}` +} + +// 生成当天0点到现在的完整时间序列数据(用于MOS/CCT数据补全) +function generateTodayTimeSeries(rawData: any[], dataField: string) { + if (!rawData || rawData.length === 0) { + // 如果没有数据,返回当天0点到现在的0值数据点 + const now = new Date() + const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0) + const hoursFromStart = Math.floor((now.getTime() - todayStart.getTime()) / (1000 * 60 * 60)) + + return Array.from({ length: hoursFromStart + 1 }, (_, i) => ({ + timeGroup: Math.floor(todayStart.getTime() / 1000) + i * 3600, + [dataField]: 0 + })) + } + + // 获取当前时间和当天0点 + const now = new Date() + const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0) + const todaybeginTimestamp = Math.floor(todayStart.getTime() / 1000) + const nowTimestamp = Math.floor(now.getTime() / 1000) + + // 计算从当天0点到现在的小时数 + const hoursFromStart = Math.floor((nowTimestamp - todaybeginTimestamp) / 3600) + + // 生成当天0点到现在的完整时间点(每小时一个点) + const timePoints = Array.from({ length: hoursFromStart + 1 }, (_, i) => { + const hourTimestamp = todaybeginTimestamp + i * 3600 + return { + timestamp: hourTimestamp, + hourStart: hourTimestamp // 已经是整点时间 + } + }) + + // 将原始数据按时间分组 + const dataMap = new Map() + rawData.forEach(item => { + const timeGroup = Number(item.timeGroup) || 0 + const hourStart = Math.floor(timeGroup / 3600) * 3600 + dataMap.set(hourStart, item) + }) + + // 补全当天数据 + const completeData = timePoints.map(({ hourStart }) => { + const existingData = dataMap.get(hourStart) + if (existingData) { + return { + timeGroup: hourStart, + [dataField]: Number(existingData[dataField]) || 0 + } + } else { + return { + timeGroup: hourStart, + [dataField]: 0 + } + } + }) + + return completeData +} + // 辅助函数:从数据中计算MO值 function calculateMOValueFromData(kpiEvent: any) { const scscf05 = Number(kpiEvent['SCSCF.05']) || 0 @@ -1347,13 +1717,13 @@ function calculateRegSuccessValue() { // 计算registration success变化值 function calculateRegSuccessChange() { - if (imsRealtimeRawData.value.length < 2) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m' + if (imsRealtimeRawData.value.length < 2) return t('views.perfManage.voiceOverView.last') +' 1 Min'+' +0%' // 获取最新和上一个数据点 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' + if (!latestData?.data || !previousData?.data) return t('views.perfManage.voiceOverView.last') +' 1 Min'+' +0%' const latestKpi = latestData.data const previousKpi = previousData.data @@ -1362,20 +1732,20 @@ function calculateRegSuccessChange() { const previousRegSuccess = calculateRegSuccessValueFromData(previousKpi) // 检查registration success是否有有效值 - if (latestRegSuccess === '-' || previousRegSuccess === '-') return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m' + if (latestRegSuccess === '-' || previousRegSuccess === '-') return t('views.perfManage.voiceOverView.last') +' 1 Min'+' +0%' // 计算变化幅度 const change = latestRegSuccess - previousRegSuccess // 检查是否有变化 - if (change === 0) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m' + if (change === 0) return t('views.perfManage.voiceOverView.last') +' 1 Min'+' +0%' const changeText = change > 0 ? `+${change.toFixed(2)}%` : `${change.toFixed(2)}%` // 计算时间差 const timeDiff = calculateTimeDifference(latestData, previousData) - return `${changeText} ${t('views.perfManage.voiceOverView.last')} ${timeDiff}` + return ` ${t('views.perfManage.voiceOverView.last')} ${timeDiff} ${changeText}` } // 辅助函数:从数据中计算registration success值 @@ -1387,18 +1757,14 @@ function calculateRegSuccessValueFromData(kpiEvent: any) { // 计算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) + return calculateMetricValue({ + data: imsRealtimeRawData.value, + valueExtractor: (item: any) => { + if (!item || !item.data) return 0 + return Number(item.data['SCSCF.07']) || 0 + }, + formatValue: (value: number) => value.toFixed(0) + }) } // 计算active calls箭头方向 @@ -1451,49 +1817,31 @@ function calculateActiveCallsArrow() { // 计算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}` + return calculateMetricChange({ + data: imsRealtimeRawData.value || [], + valueExtractor: (item: any) => { + if (!item || !item.data) return 0 + return Number(item.data['SCSCF.07']) || 0 + }, + timeDiffCalculator: calculateTimeDifference, + defaultTimeText: '1 Min', + formatValue: (value: number) => value.toFixed(0), + isPercentage: false + }) } // 计算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) + return calculateMetricValue({ + data: imsRealtimeRawData.value, + valueExtractor: (item: any) => { + if (!item || !item.data) return 0 + const scscf06 = Number(item.data['SCSCF.06']) || 0 + const scscf07 = Number(item.data['SCSCF.07']) || 0 + return scscf06 - scscf07 + }, + formatValue: (value: number) => value.toFixed(0) + }) } // 计算failed calls箭头方向 @@ -1556,13 +1904,13 @@ function calculateFailedCallsArrow() { // 计算failed calls变化值 function calculateFailedCallsChange() { - if (imsRealtimeRawData.value.length < 2) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m' + if (imsRealtimeRawData.value.length < 2) return t('views.perfManage.voiceOverView.last') +' 1 Min'+' +0' // 获取最新和上一个数据点 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' + if (!latestData?.data || !previousData?.data) return t('views.perfManage.voiceOverView.last') +' 1 Min'+' +0' const latestKpi = latestData.data const previousKpi = previousData.data @@ -1579,14 +1927,14 @@ function calculateFailedCallsChange() { const change = latestFailedCalls - previousFailedCalls // 检查是否有变化 - if (change === 0) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m' + if (change === 0) return t('views.perfManage.voiceOverView.last') +' 1 Min'+' +0' const changeText = change > 0 ? `+${change.toFixed(0)}` : `${change.toFixed(0)}` // 计算时间差 const timeDiff = calculateTimeDifference(latestData, previousData) - return `${changeText} ${t('views.perfManage.voiceOverView.last')} ${timeDiff}` + return `${t('views.perfManage.voiceOverView.last')} ${timeDiff} ${changeText}` } // 计算active registrations值 @@ -1655,13 +2003,13 @@ function calculateActiveRegistrationsArrow() { // 计算active registrations变化值 function calculateActiveRegistrationsChange() { - if (imsRealtimeRawData.value.length < 2) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m' + if (imsRealtimeRawData.value.length < 2) return t('views.perfManage.voiceOverView.last') +' 1 Min'+' +0' // 获取最新和上一个数据点 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' + if (!latestData?.data || !previousData?.data) return t('views.perfManage.voiceOverView.last') +' 1 Min'+' +0' const latestKpi = latestData.data const previousKpi = previousData.data @@ -1674,14 +2022,14 @@ function calculateActiveRegistrationsChange() { const change = latestActiveRegistrations - previousActiveRegistrations // 检查是否有变化 - if (change === 0) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m' + if (change === 0) return t('views.perfManage.voiceOverView.last') +' 1 Min'+' +0' const changeText = change > 0 ? `+${change.toFixed(0)}` : `${change.toFixed(0)}` // 计算时间差 const timeDiff = calculateTimeDifference(latestData, previousData) - return `${changeText} ${t('views.perfManage.voiceOverView.last')} ${timeDiff}` + return `${t('views.perfManage.voiceOverView.last')} ${timeDiff} ${changeText}` } // 计算failed registrations值 @@ -1760,13 +2108,13 @@ function calculateFailedRegistrationsArrow() { // 计算failed registrations变化值 function calculateFailedRegistrationsChange() { - if (imsRealtimeRawData.value.length < 2) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m' + if (imsRealtimeRawData.value.length < 2) return t('views.perfManage.voiceOverView.last') +' 1 Min'+' +0' // 获取最新和上一个数据点 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' + if (!latestData?.data || !previousData?.data) return t('views.perfManage.voiceOverView.last') +' 1 Min'+' +0' const latestKpi = latestData.data const previousKpi = previousData.data @@ -1783,40 +2131,190 @@ function calculateFailedRegistrationsChange() { const change = latestFailedRegistrations - previousFailedRegistrations // 检查是否有变化 - if (change === 0) return '±0.00%' +t('views.perfManage.voiceOverView.last') +'1m' + if (change === 0) return t('views.perfManage.voiceOverView.last') +' 1 Min'+' +0' const changeText = change > 0 ? `+${change.toFixed(0)}` : `${change.toFixed(0)}` // 计算时间差 const timeDiff = calculateTimeDifference(latestData, previousData) - return `${changeText} ${+t('views.perfManage.voiceOverView.last')} ${timeDiff}` + return `${t('views.perfManage.voiceOverView.last')} ${timeDiff} ${changeText}` } -// 测试数据更新 -// 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) -// } + +// Call Attempts相关计算函数 +function calculateCallAttemptsValue() { + return calculateMetricValue({ + data: busyHourData.value, + valueExtractor: (item: any) => Number(item.callAttempts) || 0, + formatValue: (value: number) => value.toString() + }) +} + +function calculateCallAttemptsArrowDirection() { + if (!busyHourData.value || busyHourData.value.length < 2) return 'up' + + const latestValue = Number(busyHourData.value[busyHourData.value.length - 1].callAttempts) || 0 + const previousValue = Number(busyHourData.value[busyHourData.value.length - 2].callAttempts) || 0 + + const change = latestValue - previousValue + if (change > 0) return 'up' + if (change < 0) return 'down' + return 'up' +} + +function calculateCallAttemptsArrow() { + if (!busyHourData.value || busyHourData.value.length < 2) return '→' + + const latestValue = Number(busyHourData.value[busyHourData.value.length - 1].callAttempts) || 0 + const previousValue = Number(busyHourData.value[busyHourData.value.length - 2].callAttempts) || 0 + + const change = latestValue - previousValue + if (change > 0) return '↗' + if (change < 0) return '↘' + return '→' +} + +function calculateCallAttemptsChange() { + return calculateMetricChange({ + data: busyHourData.value || [], + valueExtractor: (item: any) => Number(item.callAttempts) || 0, + defaultTimeText: '1 Hour', + formatValue: (value: number) => value.toString(), + isPercentage: false + }) +} + +// Call Completions相关计算函数 +function calculateCallCompletionsValue() { + return calculateMetricValue({ + data: busyHourData.value, + valueExtractor: (item: any) => Number(item.callCompletions) || 0, + formatValue: (value: number) => value.toString() + }) +} + +function calculateCallCompletionsArrowDirection() { + if (!busyHourData.value || busyHourData.value.length < 2) return 'up' + + const latestValue = Number(busyHourData.value[busyHourData.value.length - 1].callCompletions) || 0 + const previousValue = Number(busyHourData.value[busyHourData.value.length - 2].callCompletions) || 0 + + const change = latestValue - previousValue + if (change > 0) return 'up' + if (change < 0) return 'down' + return 'up' +} + +function calculateCallCompletionsArrow() { + if (!busyHourData.value || busyHourData.value.length < 2) return '→' + + const latestValue = Number(busyHourData.value[busyHourData.value.length - 1].callCompletions) || 0 + const previousValue = Number(busyHourData.value[busyHourData.value.length - 2].callCompletions) || 0 + + const change = latestValue - previousValue + if (change > 0) return '↗' + if (change < 0) return '↘' + return '→' +} + +function calculateCallCompletionsChange() { + return calculateMetricChange({ + data: busyHourData.value || [], + valueExtractor: (item: any) => Number(item.callCompletions) || 0, + defaultTimeText: '1 Hour', + formatValue: (value: number) => value.toString(), + isPercentage: false + }) +} + +// MOS相关计算函数 +function calculateMosValue() { + return calculateMetricValue({ + data: mosData.value, + valueExtractor: (item: any) => Number(item.mosAvg) || 0, + formatValue: (value: number) => value.toFixed(2) + }) +} + +function calculateMosArrowDirection() { + if (!mosData.value || mosData.value.length < 2) return 'up' + + const latestValue = Number(mosData.value[mosData.value.length - 1].mosAvg) || 0 + const previousValue = Number(mosData.value[mosData.value.length - 2].mosAvg) || 0 + + const change = latestValue - previousValue + if (change > 0) return 'up' + if (change < 0) return 'down' + return 'up' +} + +function calculateMosArrow() { + if (!mosData.value || mosData.value.length < 2) return '→' + + const latestValue = Number(mosData.value[mosData.value.length - 1].mosAvg) || 0 + const previousValue = Number(mosData.value[mosData.value.length - 2].mosAvg) || 0 + + const change = latestValue - previousValue + if (change > 0) return '↗' + if (change < 0) return '↘' + return '→' +} + +function calculateMosChange() { + return calculateMetricChange({ + data: mosData.value || [], + valueExtractor: (item: any) => Number(item.mosAvg) || 0, + timeDiffCalculator: calculateMosCctTimeDifference, + defaultTimeText: '1 Hour', + formatValue: (value: number) => value.toFixed(2), + isPercentage: false + }) +} + +// CCT相关计算函数 +function calculateCctValue() { + return calculateMetricValue({ + data: cctData.value, + valueExtractor: (item: any) => Number(item.cctAvg) || 0, + formatValue: (value: number) => value.toFixed(2) + }) +} + +function calculateCctArrowDirection() { + if (!cctData.value || cctData.value.length < 2) return 'up' + + const latestValue = Number(cctData.value[cctData.value.length - 1].cctAvg) || 0 + const previousValue = Number(cctData.value[cctData.value.length - 2].cctAvg) || 0 + + const change = latestValue - previousValue + if (change > 0) return 'up' + if (change < 0) return 'down' + return 'up' +} + +function calculateCctArrow() { + if (!cctData.value || cctData.value.length < 2) return '→' + + const latestValue = Number(cctData.value[cctData.value.length - 1].cctAvg) || 0 + const previousValue = Number(cctData.value[cctData.value.length - 2].cctAvg) || 0 + + const change = latestValue - previousValue + if (change > 0) return '↗' + if (change < 0) return '↘' + return '→' +} + +function calculateCctChange() { + return calculateMetricChange({ + data: cctData.value || [], + valueExtractor: (item: any) => Number(item.cctAvg) || 0, + timeDiffCalculator: calculateMosCctTimeDifference, + defaultTimeText: '1 Hour', + formatValue: (value: number) => value.toFixed(2), + isPercentage: false + }) +} +