Files
fe.ems.vue3/src/views/dashboard/smfCDRByIMSI/index.vue

679 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import * as echarts from 'echarts/core';
import {
TitleComponent,
ToolboxComponent,
TooltipComponent,
GridComponent,
LegendComponent,
DataZoomComponent,
} from 'echarts/components';
import { LineChart } from 'echarts/charts';
import { UniversalTransition } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
echarts.use([
TitleComponent,
ToolboxComponent,
TooltipComponent,
GridComponent,
LegendComponent,
DataZoomComponent,
LineChart,
CanvasRenderer,
UniversalTransition,
]);
import { reactive, onMounted, toRaw, onBeforeUnmount, ref } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { OptionsType, WS } from '@/plugins/ws-websocket';
import useI18n from '@/hooks/useI18n';
import { listSMFDataCDR } from '@/api/neData/smf';
import {
RESULT_CODE_ERROR,
RESULT_CODE_SUCCESS,
} from '@/constants/result-constants';
import { parseSizeFromByte } from '@/utils/parse-utils';
import { message } from 'ant-design-vue';
import useNeInfoStore from '@/store/modules/neinfo';
import dayjs, { Dayjs } from 'dayjs';
const { t, currentLocale } = useI18n();
const ws = new WS();
/**图DOM节点实例对象 */
const cdrChartDom = ref<HTMLElement | undefined>(undefined);
/**图实例对象 */
let cdrChart: echarts.ECharts | null = null;
/**图表配置 */
const option = {
title: {
text: 'Data Usage Report',
left: 'left',
},
tooltip: {
trigger: 'axis',
axisPointer: {
animation: true,
},
formatter: (params: any) => {
const title = params[0].name;
let uplinkValue = 0;
let downlinkValue = 0;
if (params[0].seriesName === 'Uplink') {
uplinkValue = params[0].value;
} else {
downlinkValue = params[0].value;
}
if (params[1].seriesName === 'Uplink') {
uplinkValue = params[1].value;
} else {
downlinkValue = params[1].value;
}
const uplinkValueF = parseSizeFromByte(uplinkValue);
const downlinkValueF = parseSizeFromByte(downlinkValue);
return `
<div style="font-weight: bold;">${title}</div>
<div>Downlink: ${downlinkValueF}</div>
<div>Uplink: ${uplinkValueF}</div>
`;
},
},
toolbox: {
feature: {
dataZoom: {
yAxisIndex: 'none',
},
saveAsImage: {},
},
},
axisPointer: {
link: [
{
xAxisIndex: 'all',
},
],
},
dataZoom: [
{
show: false,
realtime: true,
start: 0,
end: 100,
xAxisIndex: [0, 1],
},
{
type: 'inside',
realtime: true,
start: 0,
end: 100,
xAxisIndex: [0, 1],
},
],
grid: [
{
left: '10%',
right: 50,
height: '40%',
},
{
left: '10%',
right: 50,
top: '50%',
height: '40%',
},
],
xAxis: [
{
type: 'category',
boundaryGap: false,
axisLine: { onZero: true },
data: [], // x轴初始数据
axisLabel: {
show: false, // 显示标签
rotate: 15, // 设置倾斜角度如15度
},
},
{
gridIndex: 1,
type: 'category',
boundaryGap: false,
axisLine: { onZero: true },
data: [], // x轴初始数据
axisLabel: {
show: false, // 隐藏第二个 x 轴的标签
},
position: 'top',
},
],
yAxis: [
{
name: 'Downlink (Byte)',
type: 'value',
},
{
gridIndex: 1,
name: 'Uplink (Byte)',
type: 'value',
inverse: true,
},
],
series: [
{
name: 'Downlink',
type: 'line',
data: [], // y轴初始数据
symbol: 'circle', // 数据点形状
symbolSize: 6, // 数据点大小
smooth: true, // 平滑曲线
color: 'rgb(0, 190, 99)',
areaStyle: {
color: {
colorStops: [
{ offset: 0, color: 'rgba(0, 190, 99, .5)' },
{ offset: 1, color: 'rgba(0, 190, 99, 0.5)' },
],
x: 0,
y: 0,
x2: 0,
y2: 1,
type: 'linear',
global: false,
},
shadowColor: 'rgba(0, 0, 0, 0.1)',
shadowBlur: 10,
},
},
{
name: 'Uplink',
type: 'line',
xAxisIndex: 1,
yAxisIndex: 1,
data: [], // y轴初始数据
symbol: 'circle', // 数据点形状
symbolSize: 6, // 数据点大小
smooth: true, // 平滑曲线
color: 'rgb(17, 178, 255)',
areaStyle: {
color: {
colorStops: [
{ offset: 0, color: 'rgba(17, 178, 255, .5)' },
{ offset: 1, color: 'rgba(17, 178, 255, 0.5)' },
],
x: 0,
y: 0,
x2: 0,
y2: 1,
type: 'linear',
global: false,
},
shadowColor: 'rgba(0, 0, 0, 0.1)',
shadowBlur: 10,
},
},
],
};
/**绘制图表 */
function fnRanderChart() {
const container: HTMLElement | undefined = cdrChartDom.value;
if (!container) return;
const locale = currentLocale.value.split('_')[0];
cdrChart = echarts.init(container, 'light', {
// https://github.com/apache/echarts/tree/release/src/i18n 取值langEN.ts ==> EN
locale: locale.toUpperCase(),
});
cdrChart.setOption(option);
// cdrChart.showLoading('default', {
// text: 'Please enter IMSI to query user traffic',
// fontSize: 16, // 字体大小
// });
// 创建 ResizeObserver 实例 监听图表容器大小变化,并在变化时调整图表大小
var observer = new ResizeObserver(entries => {
if (cdrChart) {
cdrChart.resize();
}
});
// 监听元素大小变化
observer.observe(container);
}
/**网元可选 */
let neOtions = ref<Record<string, any>[]>([]);
/**开始结束时间 */
let queryRangePicker = ref<[Dayjs, Dayjs] | undefined>([
dayjs().startOf('hour'),
dayjs().endOf('hour'),
]);
/**时间范围 */
let rangePickerPresets = ref([
{
label: 'Now hour',
value: [dayjs().startOf('hour'), dayjs().endOf('hour')],
},
{ label: 'Today', value: [dayjs().startOf('day'), dayjs().endOf('day')] },
{
label: 'Yesterday',
value: [
dayjs().subtract(1, 'day').startOf('day'),
dayjs().subtract(1, 'day').endOf('day'),
],
},
]);
/**查询参数 */
let queryParams = reactive({
/**网元类型 */
neType: 'SMF',
neId: '001',
subscriberID: '',
dnn: '',
sortField: 'timestamp',
sortOrder: 'desc',
/**开始时间 */
startTime: undefined as undefined | number,
/**结束时间 */
endTime: undefined as undefined | number,
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 1000,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams.subscriberID = '';
queryParams.dnn = '';
queryRangePicker.value = [dayjs().startOf('hour'), dayjs().endOf('hour')];
fnGetList(1);
}
let state = reactive({
/**表格数据 */
data: [] as any[],
/**表格总数 */
total: 0,
/**表格加载状态 */
loading: false,
});
/**查询列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (state.loading) return;
state.loading = true;
if (!queryParams.subscriberID) {
message.warning('Please enter IMSI to query user traffic');
state.loading = false;
return;
}
if (pageNum) {
queryParams.pageNum = pageNum;
}
if (cdrChart) {
cdrChart.showLoading('default', {
text: 'Loading...',
fontSize: 16, // 字体大小
});
}
// 时间范围
if (
Array.isArray(queryRangePicker.value) &&
queryRangePicker.value.length > 0
) {
queryParams.startTime = queryRangePicker.value[0].valueOf();
queryParams.endTime = queryRangePicker.value[1].valueOf();
} else {
queryParams.startTime = undefined;
queryParams.endTime = undefined;
}
listSMFDataCDR(toRaw(queryParams))
.then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.rows)) {
state.total = res.total;
// 遍历处理cdr字符串数据
state.data = res.rows
.map(item => {
let cdrJSON = item.cdrJSON;
if (!cdrJSON) {
Reflect.set(item, 'cdrJSON', {});
}
try {
cdrJSON = JSON.parse(cdrJSON);
Reflect.set(item, 'cdrJSON', cdrJSON);
} catch (error) {
console.error(error);
Reflect.set(item, 'cdrJSON', {});
}
return item;
})
.reverse();
}
})
.finally(() => {
state.loading = false;
fnRanderChartDataLoad();
});
}
/**图表配置数据x轴 */
let dataTimeXAxisData: string[] = [];
/**图表配置数据y轴 */
let dataVolumeUplinkYSeriesData: number[] = [];
let dataVolumeDownlinkYSeriesData: number[] = [];
/**图表数据渲染 */
function fnRanderChartDataLoad() {
if (!cdrChart) return;
dataTimeXAxisData = [];
dataVolumeUplinkYSeriesData = [];
dataVolumeDownlinkYSeriesData = [];
if (state.data.length > 0) {
// 处理数据渲染图表
for (const item of state.data) {
if (!item.cdrJSON.invocationTimestamp) {
break;
}
// 时间
const dataTime = item.cdrJSON.invocationTimestamp;
const listOfMultipleUnitUsage = item.cdrJSON.listOfMultipleUnitUsage;
if (
!Array.isArray(listOfMultipleUnitUsage) ||
listOfMultipleUnitUsage.length < 1
) {
return 0;
}
// 数据
let dataVolumeUplink = 0;
let dataVolumeDownlink = 0;
for (const v of listOfMultipleUnitUsage) {
if (Array.isArray(v.usedUnitContainer)) {
for (const used of v.usedUnitContainer) {
dataVolumeUplink += +used.dataVolumeUplink;
dataVolumeDownlink += +used.dataVolumeDownlink;
}
}
}
dataTimeXAxisData.push(dataTime);
dataVolumeUplinkYSeriesData.push(dataVolumeUplink);
dataVolumeDownlinkYSeriesData.push(dataVolumeDownlink);
}
// 绘制图数据
fnRanderChartDataUpdate();
} else {
cdrChart.showLoading('default', {
text: 'No Data',
fontSize: 16, // 字体大小
});
cdrChart.setOption({
title: {
text: `Data Volume Uplink / Downlink By IMSI ${queryParams.subscriberID}`,
},
xAxis: [
{
data: dataTimeXAxisData,
},
{
data: dataTimeXAxisData,
},
],
series: [
{
data: dataVolumeUplinkYSeriesData,
},
{
data: dataVolumeDownlinkYSeriesData,
},
],
});
}
}
/**图表数据渲染 */
function fnRanderChartDataUpdate() {
if (cdrChart == null) return;
// 绘制图数据
cdrChart.setOption({
title: {
text: `Data Usage Report of IMSI ${queryParams.subscriberID}`,
},
xAxis: [
{
data: dataTimeXAxisData,
},
{
data: dataTimeXAxisData,
},
],
series: [
{
data: dataVolumeUplinkYSeriesData,
},
{
data: dataVolumeDownlinkYSeriesData,
},
],
});
cdrChart.hideLoading();
}
/**
* 实时数据
*/
function fnRealTime() {
if (ws.state() === WebSocket.OPEN) {
ws.close();
}
// 建立链接
const options: OptionsType = {
url: '/ws',
params: {
/**订阅通道组
*
* CDR会话事件-SMF (GroupID:1006)
*/
subGroupID: `1006_${queryParams.neId}`,
},
onmessage: (res: Record<string, any>) => {
const { code, requestId, data } = res;
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
// 订阅组信息
if (!data?.groupId) {
return;
}
// cdrEvent CDR会话事件
if (data.groupId === `1006_${queryParams.neId}`) {
const cdrEvent = data.data;
// 对应结束时间内
if (queryParams.endTime) {
const endTime = Math.round(queryParams.endTime / 1000);
if (cdrEvent.timestamp > endTime) {
return;
}
}
const cdrJSON = cdrEvent.CDR;
if (!cdrJSON.invocationTimestamp) {
return;
}
// 对应IMSI
if (
cdrJSON.subscriberIdentifier.subscriptionIDData !==
queryParams.subscriberID
) {
return;
}
// 时间
const dataTime = cdrJSON.invocationTimestamp;
const listOfMultipleUnitUsage = cdrJSON.listOfMultipleUnitUsage;
if (
!Array.isArray(listOfMultipleUnitUsage) ||
listOfMultipleUnitUsage.length < 1
) {
return 0;
}
// 数据
let dataVolumeUplink = 0;
let dataVolumeDownlink = 0;
for (const v of listOfMultipleUnitUsage) {
if (Array.isArray(v.usedUnitContainer)) {
for (const used of v.usedUnitContainer) {
dataVolumeUplink += +used.dataVolumeUplink;
dataVolumeDownlink += +used.dataVolumeDownlink;
}
}
}
// 添加数据
dataTimeXAxisData.push(dataTime);
dataVolumeUplinkYSeriesData.push(dataVolumeUplink);
dataVolumeDownlinkYSeriesData.push(dataVolumeDownlink);
fnRanderChartDataUpdate();
}
},
onerror: (ev: any) => {
console.error(ev);
},
};
ws.connect(options);
}
onMounted(() => {
// 获取网元网元列表
useNeInfoStore()
.fnNelist()
.then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
if (res.data.length > 0) {
let arr: Record<string, any>[] = [];
res.data.forEach(i => {
if (i.neType === 'SMF') {
arr.push({ value: i.neId, label: i.neName });
}
});
neOtions.value = arr;
if (arr.length > 0) {
queryParams.neId = arr[0].value;
}
}
} else {
message.warning({
content: t('common.noData'),
duration: 2,
});
}
})
.finally(() => {
fnRanderChart();
fnRealTime();
});
});
onBeforeUnmount(() => {
ws.close();
if (cdrChart) {
cdrChart.clear();
cdrChart.dispose();
}
});
</script>
<template>
<PageContainer>
<a-card
:bordered="false"
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
>
<!-- 表格搜索栏 -->
<a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="SMF" name="neId ">
<a-select
v-model:value="queryParams.neId"
:options="neOtions"
:placeholder="t('common.selectPlease')"
@change="fnRealTime()"
:disabled="state.loading"
/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="IMSI" name="subscriberID" :required="true">
<a-input
v-model:value="queryParams.subscriberID"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="40"
:disabled="state.loading"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="DNN" name="dnn">
<a-input
v-model:value="queryParams.dnn"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="40"
:disabled="state.loading"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="4" :md="12" :xs="24">
<a-form-item>
<a-space :size="8">
<a-button
type="primary"
@click.prevent="fnGetList(1)"
:loading="state.loading"
>
<template #icon><SearchOutlined /></template>
{{ t('common.search') }}
</a-button>
<a-button
type="default"
@click.prevent="fnQueryReset"
:disabled="state.loading"
>
<template #icon><ClearOutlined /></template>
{{ t('common.reset') }}
</a-button>
</a-space>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :xs="24">
<a-form-item
:label="t('views.dashboard.cdr.time')"
name="queryRangePicker"
>
<a-range-picker
v-model:value="queryRangePicker"
:presets="rangePickerPresets"
:bordered="true"
:allow-clear="false"
style="width: 100%"
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
:disabled="state.loading"
></a-range-picker>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-card>
<a-card :bordered="false">
<!-- 图数据 -->
<div ref="cdrChartDom" style="height: 600px; width: 100%"></div>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>