From 2828deaece78b8d8c08c7cfe819a2485d918fb2d Mon Sep 17 00:00:00 2001 From: zhongzm Date: Fri, 19 Sep 2025 09:14:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E8=AF=AD=E9=9F=B3=E4=BB=AA=E8=A1=A8?= =?UTF-8?q?=E7=9B=98=E5=A2=9E=E5=8A=A0=E6=8C=87=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/perfManage/goldTarget.ts | 17 + src/views/perfManage/overview/index.vue | 529 +++++++++++++++++++++++- 2 files changed, 541 insertions(+), 5 deletions(-) diff --git a/src/api/perfManage/goldTarget.ts b/src/api/perfManage/goldTarget.ts index 91860abd..6cca9850 100644 --- a/src/api/perfManage/goldTarget.ts +++ b/src/api/perfManage/goldTarget.ts @@ -142,3 +142,20 @@ export async function getbusyhour(query: Record) { 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 ccec08e3..5559a6e6 100644 --- a/src/views/perfManage/overview/index.vue +++ b/src/views/perfManage/overview/index.vue @@ -124,6 +124,42 @@ + + + +
MOS📞
+
+
+
+
+
+
+ {{ calculateMosValue() }} + {{ calculateMosArrow() }} +
+
{{ calculateMosChange() }}
+
+
+
+
+ + +
CCT📞
+
+
+
+
+
+
+ {{ calculateCctValue() }} + {{ calculateCctArrow() }} +
+
{{ calculateCctChange() }}
+
+
+
+
+
{{ t('views.perfManage.voiceOverView.registration') }}
@@ -183,7 +219,7 @@ import { GridComponent } from 'echarts/components' import { CanvasRenderer } from 'echarts/renderers' import useNeInfoStore from '@/store/modules/neinfo' import { WS } from '@/plugins/ws-websocket' -import { listKPIData ,getbusyhour} 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(); @@ -194,6 +230,7 @@ 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) @@ -207,6 +244,10 @@ const imsWs = ref(null) 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('no connection') @@ -228,6 +269,8 @@ onMounted(async () => { // 先获取历史数据和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类型的网元') // 调试信息 @@ -331,6 +374,80 @@ async function fetchBusyHourData(neId: string) { } } +// 获取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 + } + + console.log('获取MOS数据参数:', params) + + const res = await getMosHour(params) + console.log('MOS数据响应:', res) + + if (res.code === 1 && Array.isArray(res.data)) { + mosData.value = res.data + console.log('MOS数据加载完成:', mosData.value) + + // 更新MOS图表 + updateMosChart() + } else { + console.warn('获取MOS数据失败或数据为空') + mosData.value = null + } + } catch (error) { + console.error('获取MOS数据出错:', error) + mosData.value = null + } +} + +// 获取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 === 1 && Array.isArray(res.data)) { + cctData.value = res.data + console.log('CCT数据加载完成:', cctData.value) + + // 更新CCT图表 + updateCctChart() + } else { + console.warn('获取CCT数据失败或数据为空') + cctData.value = null + } + } catch (error) { + console.error('获取CCT数据出错:', error) + cctData.value = null + } +} + // 切换IMS网元时,重新订阅 async function onImsNeChange() { // console.log('切换IMS网元,新的网元ID:', selectedImsNeId.value) // 调试信息 @@ -350,6 +467,8 @@ async function onImsNeChange() { // 先获取历史数据和Busy Hour数据,再订阅实时数据 await fetchHistoryData(selectedImsNeId.value) await fetchBusyHourData(selectedImsNeId.value) + await fetchMosData(selectedImsNeId.value) + await fetchCctData(selectedImsNeId.value) subscribeImsRealtime(selectedImsNeId.value) } @@ -1088,8 +1207,8 @@ function updateCallAttemptsChart() { chart = echarts.init(callAttemptsChartRef.value) } - // 如果没有Busy Hour数据,显示默认的平直线 - if (!busyHourData.value) { + // 如果没有Busy Hour数据或数据为空数组,显示默认的平直线 + if (!busyHourData.value || busyHourData.value.length === 0) { const defaultData = [0, 0, 0, 0, 0] // 5个默认数据点,值为0 const xAxisData = [1, 2, 3, 4, 5] @@ -1235,8 +1354,8 @@ function updateCallCompletionsChart() { chart = echarts.init(callCompletionsChartRef.value) } - // 如果没有Busy Hour数据,显示默认的平直线 - if (!busyHourData.value) { + // 如果没有Busy Hour数据或数据为空数组,显示默认的平直线 + if (!busyHourData.value || busyHourData.value.length === 0) { const defaultData = [0, 0, 0, 0, 0] // 5个默认数据点,值为0 const xAxisData = [1, 2, 3, 4, 5] @@ -1372,6 +1491,300 @@ function updateCallCompletionsChart() { } } +// 更新MOS图表 +function updateMosChart() { + if (!mosChartRef.value) return + + // 获取图表实例 + let chart = echarts.getInstanceByDom(mosChartRef.value) + if (!chart) { + chart = echarts.init(mosChartRef.value) + } + + // 如果没有MOS数据或数据为空数组,显示默认的平直线 + if (!mosData.value || mosData.value.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 = mosChartRef.value + if (chartContainer) { + const existingLabels = chartContainer.querySelectorAll('.chart-label') + existingLabels.forEach(label => label.remove()) + } + return + } + + // 从MOS数据中提取MOS值 + const chartData = mosData.value.map((item: any) => Number(item.mosAverage) || 0) + + // 生成时间轴数据 + 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: '#52c41a' }, // 绿色线条 + areaStyle: { color: 'rgba(82,196,26,0.1)' } // 淡绿色填充 + }] + }) + + // 在图表容器中添加数值标注 + const chartContainer = mosChartRef.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.toFixed(2) + + 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: #52c41a; + pointer-events: none; + z-index: 10; + ` + latestLabel.textContent = latestValue.toFixed(2) + + 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.toFixed(2) + + // 添加底部时间标注 + 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 oldestTime = Number(mosData.value[0]?.timeGroup) || Date.now() + oldestTimeLabel.textContent = 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') + + chartContainer.appendChild(maxLabel) + chartContainer.appendChild(latestLabel) + chartContainer.appendChild(minLabel) + chartContainer.appendChild(oldestTimeLabel) + chartContainer.appendChild(nowTimeLabel) + } +} + +// 更新CCT图表 +function updateCctChart() { + if (!cctChartRef.value) return + + // 获取图表实例 + let chart = echarts.getInstanceByDom(cctChartRef.value) + if (!chart) { + chart = echarts.init(cctChartRef.value) + } + + // 如果没有CCT数据或数据为空数组,显示默认的平直线 + if (!cctData.value || cctData.value.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 = cctChartRef.value + if (chartContainer) { + const existingLabels = chartContainer.querySelectorAll('.chart-label') + existingLabels.forEach(label => label.remove()) + } + return + } + + // 从CCT数据中提取CCT值 + const chartData = cctData.value.map((item: any) => Number(item.callConnectionTime) || 0) + + // 生成时间轴数据 + 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: '#fa8c16' }, // 橙色线条 + areaStyle: { color: 'rgba(250,140,22,0.1)' } // 淡橙色填充 + }] + }) + + // 在图表容器中添加数值标注 + const chartContainer = cctChartRef.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.toFixed(2) + + 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: #fa8c16; + pointer-events: none; + z-index: 10; + ` + latestLabel.textContent = latestValue.toFixed(2) + + 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.toFixed(2) + + // 添加底部时间标注 + 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 oldestTime = Number(cctData.value[0]?.timeGroup) || Date.now() + oldestTimeLabel.textContent = 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') + + chartContainer.appendChild(maxLabel) + chartContainer.appendChild(latestLabel) + chartContainer.appendChild(minLabel) + chartContainer.appendChild(oldestTimeLabel) + chartContainer.appendChild(nowTimeLabel) + } +} + // 处理IMS实时数据(只存储当前选中网元) function handleIMSRealtimeData(res: any) { //console.log('收到实时数据:', res) // 调试信息 @@ -1549,6 +1962,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 @@ -2273,6 +2698,100 @@ function calculateCallCompletionsChange() { return `${changeText} ` + t('views.perfManage.voiceOverView.last') + ' 1h' } +// MOS相关计算函数 +function calculateMosValue() { + if (!mosData.value || mosData.value.length === 0) return '-' + + // 获取最新的MOS值(数组最后一个元素) + const latestData = mosData.value[mosData.value.length - 1] + const mosValue = Number(latestData.mosAverage) || 0 + return mosValue.toFixed(2) +} + +function calculateMosArrowDirection() { + if (!mosData.value || mosData.value.length < 2) return 'up' + + const latestValue = Number(mosData.value[mosData.value.length - 1].mosAverage) || 0 + const previousValue = Number(mosData.value[mosData.value.length - 2].mosAverage) || 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].mosAverage) || 0 + const previousValue = Number(mosData.value[mosData.value.length - 2].mosAverage) || 0 + + const change = latestValue - previousValue + if (change > 0) return '↗' + if (change < 0) return '↘' + return '→' +} + +function calculateMosChange() { + if (!mosData.value || mosData.value.length < 2) return '±0.00 ' + t('views.perfManage.voiceOverView.last') + ' 1h' + + const latestValue = Number(mosData.value[mosData.value.length - 1].mosAverage) || 0 + const previousValue = Number(mosData.value[mosData.value.length - 2].mosAverage) || 0 + + const change = latestValue - previousValue + if (change === 0) return '±0.00 ' + t('views.perfManage.voiceOverView.last') + ' 1h' + + const changeText = change > 0 ? `+${change.toFixed(2)}` : `${change.toFixed(2)}` + return `${changeText} ` + t('views.perfManage.voiceOverView.last') + ' 1h' +} + +// CCT相关计算函数 +function calculateCctValue() { + if (!cctData.value || cctData.value.length === 0) return '-' + + // 获取最新的CCT值(数组最后一个元素) + const latestData = cctData.value[cctData.value.length - 1] + const cctValue = Number(latestData.callConnectionTime) || 0 + return cctValue.toFixed(2) +} + +function calculateCctArrowDirection() { + if (!cctData.value || cctData.value.length < 2) return 'up' + + const latestValue = Number(cctData.value[cctData.value.length - 1].callConnectionTime) || 0 + const previousValue = Number(cctData.value[cctData.value.length - 2].callConnectionTime) || 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].callConnectionTime) || 0 + const previousValue = Number(cctData.value[cctData.value.length - 2].callConnectionTime) || 0 + + const change = latestValue - previousValue + if (change > 0) return '↗' + if (change < 0) return '↘' + return '→' +} + +function calculateCctChange() { + if (!cctData.value || cctData.value.length < 2) return '±0.00 ' + t('views.perfManage.voiceOverView.last') + ' 1h' + + const latestValue = Number(cctData.value[cctData.value.length - 1].callConnectionTime) || 0 + const previousValue = Number(cctData.value[cctData.value.length - 2].callConnectionTime) || 0 + + const change = latestValue - previousValue + if (change === 0) return '±0.00 ' + t('views.perfManage.voiceOverView.last') + ' 1h' + + const changeText = change > 0 ? `+${change.toFixed(2)}` : `${change.toFixed(2)}` + return `${changeText} ` + t('views.perfManage.voiceOverView.last') + ' 1h' +} + // 测试数据更新 // function testDataUpdate() { // console.log('测试数据更新')