diff --git a/src/plugins/ws-websocket.ts b/src/plugins/ws-websocket.ts index c956f20d..5019c27f 100644 --- a/src/plugins/ws-websocket.ts +++ b/src/plugins/ws-websocket.ts @@ -6,18 +6,18 @@ import { TOKEN_RESPONSE_FIELD } from '@/constants/token-constants'; /**连接参数类型 */ export type OptionsType = { - /**WebSocket服务器将响应的URL */ - url?: string; + /**WebSocket服务地址 */ + url: string; /**地址栏参数 */ params?: Record; /**onopen事件的回调函数 */ - onopen?: Function; + onopen?: () => void; /**message事件的回调函数 */ onmessage: (data: Record) => void; /**error事件的回调函数 */ onerror: Function; /**close事件的回调函数 */ - onclose?: Function; + onclose?: (code: number) => void; /**心跳周期 若为0则不启用 */ heartTimer?: number; /**重连等待 若为0则不启用 */ @@ -28,17 +28,12 @@ export type OptionsType = { * WebSocket 使用方法 * * import WS from '@/plugins/ws-websocket.ts'; - * const options = { - * url: 'ws://127.0.0.1:8080', - * onmessage: (res) => { - * // 接收数据后回调 - * }, - * // 保活周期 10s - * timer: 10000, - * // 断线重连 - * reconnect: true, - * }; - * const ws = new WS(options); + * + * const ws = new WS(); + * + * 创建连接 + * const options: OptionsType = { }; + * ws.connect(options); * * 手动关闭 * ws.close(); @@ -57,60 +52,59 @@ export class WS { * 构造函数 * @param {object} params 构造函数参数 */ - constructor(options: OptionsType) { + constructor(options?: OptionsType) { if (!window.WebSocket) { // 检测浏览器支持 - console.error('抱歉! 浏览器不支持websocket'); + console.error('Sorry! Browser does not support websocket'); return; } - this.options = options; - this.create(options); + options && this.connect(options); } /** * 创建链接 * @param {object} options 连接参数 */ - public create(options: OptionsType) { + public connect(options: OptionsType) { + this.options = options; try { - if (!options.url) { + if (!options.url.startsWith('ws')) { + const uri = options.url.startsWith('/') + ? options.url + : `/${options.url}`; // 兼容旧前端可改配置文件 const wsUrl = import.meta.env.PROD ? sessionGet('wsUrl') || import.meta.env.VITE_API_BASE_URL : '/socket'; if (wsUrl.startsWith('ws')) { - options.url = `${wsUrl}/ws`; + options.url = `${wsUrl}${uri}`; } else if (wsUrl.startsWith('https')) { - options.url = `${wsUrl.replace('https', 'wss')}/ws`; + options.url = `${wsUrl.replace('https', 'wss')}${uri}`; } else if (wsUrl.startsWith('http')) { - options.url = `${wsUrl.replace('http', 'ws')}/ws`; + options.url = `${wsUrl.replace('http', 'ws')}${uri}`; } else { const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; - options.url = `${protocol}${location.host}${wsUrl}/ws`; + options.url = `${protocol}${location.host}${wsUrl}${uri}`; } + } - // 地址栏参数 - let params = options.params || {}; + // 地址栏参数 + let params = Object.assign({}, options.params, { // 设置 token - const token = getToken(); - if (token) { - params[TOKEN_RESPONSE_FIELD] = token; - } - // 多语言处理 - params['language'] = localGet(CACHE_LOCAL_I18N) || 'en_US'; - let paramStr = ''; - for (const key in params) { - const value = params[key]; - // 空字符或未定义的值不作为参数发送 - if (value === '' || value === undefined) continue; - paramStr += `&${encodeURIComponent(key)}=${encodeURIComponent( - value - )}`; - } - if (paramStr && paramStr.startsWith('&')) { - options.url = `${options.url}?${paramStr.substring(1)}`; - } + [TOKEN_RESPONSE_FIELD]: getToken(), + // 多语言 + ['language']: localGet(CACHE_LOCAL_I18N) || 'en_US', + }); + let paramStr = ''; + for (const key in params) { + const value = params[key]; + // 空字符或未定义的值不作为参数发送 + if (value === '' || value === undefined) continue; + paramStr += `&${encodeURIComponent(key)}=${encodeURIComponent(value)}`; + } + if (paramStr && paramStr.startsWith('&')) { + options.url = `${options.url}?${paramStr.substring(1)}`; } const ws = new WebSocket(options.url, 'omc-ws'); @@ -120,33 +114,33 @@ export class WS { this.heartCheck(options.heartTimer); } if (typeof options.onopen === 'function') { - options.onopen(ev); + options.onopen(); } }; // 用于指定当从服务器接受到信息时的回调函数。 ws.onmessage = ev => { - const data = ev.data; // 解析文本消息 if (ev.type === 'message') { + const data = ev.data; try { const jsonData = JSON.parse(data); if (typeof options.onmessage === 'function') { options.onmessage(jsonData); } } catch (error) { - console.error('websocket 消息格式错误', error); + console.error('websocket message formatting error', error); } } }; // 用于指定连接关闭后的回调函数。 ws.onclose = ev => { if (typeof options.onclose === 'function') { - options.onclose(ev); + options.onclose(ev.code); } }; // 用于指定连接失败后的回调函数。 ws.onerror = ev => { - console.error('websocket 连接异常', ev); + console.error('websocket connection anomaly', ev); if (typeof options.onerror === 'function') { options.onerror(ev); @@ -169,7 +163,7 @@ export class WS { // 发送消息 public send(data: Record) { if (!this.ws) { - console.warn('websocket 不可用'); + console.warn('websocket unavailable'); return; } console.log(' readyState', this.ws.readyState); @@ -189,16 +183,17 @@ export class WS { this.heartInterval && clearInterval(this.heartInterval); this.reconnectTimeout && clearTimeout(this.reconnectTimeout); if (!this.ws) { - console.warn('websocket 不可用'); + console.warn('websocket unavailable'); return; } - this.ws.close(WebSocket.CLOSED); + this.ws.close(1000, 'user initiated closure'); } // 周期性发送ping 保活 private heartCheck(heartTimer: number) { this.heartInterval = window.setInterval(() => { this.send({ + requestId: `${Date.now()}`, type: 'ping', }); }, heartTimer); @@ -210,7 +205,7 @@ export class WS { clearTimeout(this.reconnectTimeout); this.reconnectTimeout = window.setTimeout(() => { if (this.options) { - this.create(this.options); + this.connect(this.options); this.reconnectTimeout = 0; } }, reconnectTimer); diff --git a/src/views/dashboard/overview/components/UEEvent/index.vue b/src/views/dashboard/overview/components/UEEvent/index.vue new file mode 100644 index 00000000..b8e18e23 --- /dev/null +++ b/src/views/dashboard/overview/components/UEEvent/index.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/src/views/dashboard/overview/hooks/useUEEvent.ts b/src/views/dashboard/overview/hooks/useUEEvent.ts new file mode 100644 index 00000000..22655aab --- /dev/null +++ b/src/views/dashboard/overview/hooks/useUEEvent.ts @@ -0,0 +1,10 @@ +import { ref } from 'vue'; + +/**事件数据 */ +export const ueEventData = ref[]>([]); + +/**事件总量 */ +export const ueEventTotal = ref(0); + +/**事件推送id */ +export const ueEventId = ref(''); diff --git a/src/views/dashboard/overview/hooks/useWS.ts b/src/views/dashboard/overview/hooks/useWS.ts new file mode 100644 index 00000000..232d2287 --- /dev/null +++ b/src/views/dashboard/overview/hooks/useWS.ts @@ -0,0 +1,148 @@ +import { + RESULT_CODE_ERROR, + RESULT_CODE_SUCCESS, +} from '@/constants/result-constants'; +import { OptionsType, WS } from '@/plugins/ws-websocket'; +import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'; +import { ueEventData, ueEventId, ueEventTotal } from './useUEEvent'; + +/**websocket连接 */ +export default function useWS() { + const ws = new WS(); + + /**发消息 */ + function wsSend(data: Record) { + ws.send(data); + } + + /**接收数据后回调 */ + function wsMessage(res: Record) { + console.log(res); + const { code, requestId, groupId, data } = res; + if (code === RESULT_CODE_ERROR) { + console.warn(res.msg); + return; + } + // 普通信息 + switch (requestId) { + // ueEvent UE会话事件 + case '1010': + if (Array.isArray(data.rows)) { + ueEventTotal.value = data.total; + const evDataArr: Record[] = []; + for (const item of data.rows) { + const v = ueEventParse(item); + if (v) { + evDataArr.push(v); + } + } + ueEventData.value = evDataArr; + if (evDataArr.length > 0) { + ueEventId.value = evDataArr[0].id; + } + } + break; + } + + // 订阅组信息 + switch (groupId) { + // ueEvent UE会话事件 + case '1010': + if (data.data) { + const v = ueEventParse(data.data); + if (v) { + ueEventData.value.unshift(v); + ueEventId.value = v.id; + } + } + break; + } + } + + function wsInitData() { + ws.send({ + requestId: '1010', + type: 'ue', + data: { + neType: 'AMF', + neId: '001', + sortField: 'timestamp', + sortOrder: 'desc', + pageNum: 1, + pageSize: 50, + }, + }); + } + + /**ueEvent UE会话事件 数据解析 */ + function ueEventParse(item: Record) { + let evData: Record = {}; + try { + evData = JSON.parse(item.eventJSON); + } catch (error) { + console.error(error); + return false; + } + + if (Reflect.has(evData, 'authTime')) { + return { + id: item.id, + type: item.eventType, + time: evData.authTime, + imsi: evData.imsi, + msg: `${evData.authMessage}`, + }; + } + + if (Reflect.has(evData, 'detachTime')) { + return { + id: item.id, + type: item.eventType, + time: evData.detachTime, + imsi: evData.imsi, + msg: `${evData.detachResult}`, + }; + } + + if (Reflect.has(evData, 'changeTime')) { + return { + id: item.id, + type: item.eventType, + time: evData.changeTime, + imsi: evData.imsi, + msg: `${evData.onlineNumber}`, + }; + } + } + + /**接收数据后回调 */ + function wsError(ev: any) { + // 接收数据后回调 + console.log(ev); + } + + onMounted(() => { + const options: OptionsType = { + url: '/ws', + params: { + /**订阅通道组 + * + * UE会话事件-AMF (GroupID:1010) + */ + subGroupID: '1010', + }, + onmessage: wsMessage, + onerror: wsError, + }; + ws.connect(options); + }); + + onBeforeUnmount(() => { + ws.close(); + }); + + return { + wsInitData, + wsSend, + }; +} diff --git a/src/views/dashboard/overview/index.vue b/src/views/dashboard/overview/index.vue index 3b18767f..968b7561 100644 --- a/src/views/dashboard/overview/index.vue +++ b/src/views/dashboard/overview/index.vue @@ -3,7 +3,7 @@ import { onMounted, reactive, ref } from 'vue'; import useI18n from '@/hooks/useI18n'; import Topology from './components/Topology/index.vue'; import NeResources from './components/NeResources/index.vue'; -import CDREvent from './components/CDREvent/index.vue'; +import UEEvent from './components/UEEvent/index.vue'; import AlarnDayLine from './components/AlarnDayLine/index.vue'; import AlarnTypeBar from './components/AlarnTypeBar/index.vue'; import UPFFlow from './components/UPFFlow/index.vue'; @@ -14,9 +14,11 @@ import { listBase5G } from '@/api/neUser/base5G'; import { formatBytes } from '@/utils/parse-utils'; import { graphNodeClickID } from './hooks/useTopology'; import { useFullscreen } from '@vueuse/core'; +import useWS from './hooks/useWS'; import useAppStore from '@/store/modules/app'; const appStore = useAppStore(); const { t } = useI18n(); +const { wsSend, wsInitData } = useWS(); /**用户在线信息 */ let onlineInfo: { @@ -61,27 +63,30 @@ onMounted(() => { }), listUPFIndex(), listUENum('001'), - ]).then(resArr => { - if (resArr[0].status === 'fulfilled') { - onlineInfo.subNum = resArr[0].value.total; - } - if (resArr[1].status === 'fulfilled') { - resArr[1].value['data'].map((item: any) => { - switch (item.kpiId) { - case 'UPF.03': - upfFlowInfo.up = item.Total; - break; - case 'UPF.06': - upfFlowInfo.down = item.Total; - } - }); - } + ]) + .then(resArr => { + if (resArr[0].status === 'fulfilled') { + onlineInfo.subNum = resArr[0].value.total; + } + if (resArr[1].status === 'fulfilled') { + resArr[1].value['data'].map((item: any) => { + switch (item.kpiId) { + case 'UPF.03': + upfFlowInfo.up = item.Total; + break; + case 'UPF.06': + upfFlowInfo.down = item.Total; + } + }); + } - if (resArr[2].status === 'fulfilled') { - console.log(resArr) - - } - }); + if (resArr[2].status === 'fulfilled') { + console.log(resArr); + } + }) + .finally(() => { + wsInitData(); + }); }); @@ -186,14 +191,14 @@ onMounted(() => {
-

{{formatBytes(upfFlowInfo.up)}}

+

{{ formatBytes(upfFlowInfo.up) }}

上行
-

{{formatBytes(upfFlowInfo.down)}}

+

{{ formatBytes(upfFlowInfo.down) }}

下行 @@ -221,7 +226,7 @@ onMounted(() => {    会话监控
- +