feat(web-antd): 实现电表数据实时推送功能

- 新增 WebSocket 服务,用于接收实时推送的电表数据
This commit is contained in:
2025-09-01 18:09:56 +08:00
parent ee6aa163d9
commit b91d073b8d
6 changed files with 251 additions and 49 deletions

View File

@@ -20,6 +20,8 @@ VITE_GLOB_RSA_PUBLIC_KEY=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj6
VITE_GLOB_RSA_PRIVATE_KEY=MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=
# 客户端id
VITE_GLOB_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
# 开启WEBSOCKET
VITE_APP_WEBSOCKET=true
# 开启SSE
VITE_GLOB_SSE_ENABLE=true

View File

@@ -26,6 +26,8 @@ VITE_GLOB_RSA_PUBLIC_KEY=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj6
VITE_GLOB_RSA_PRIVATE_KEY=MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=
# 客户端id
VITE_GLOB_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
# 开启WEBSOCKET
VITE_APP_WEBSOCKET=true
# 开启SSE
VITE_GLOB_SSE_ENABLE=true

View File

@@ -72,5 +72,5 @@ export function queryTree(params?: any) {
* 获取水/电/气表当前读数/状态
*/
export function currentReading(params?: any) {
return requestClient.get<MeterInfoVO[]>(`/property/meterInfo/currentReading`, { params })
return requestClient.get<void>(`/property/meterInfo/currentReading`, { params })
}

View File

@@ -0,0 +1,162 @@
import { useAccessStore } from '@vben/stores';
interface WebSocketCallbacks {
onOpen?: (event: Event) => void;
onMessage?: (event: MessageEvent) => void;
onError?: (error: Event) => void;
onClose?: (event: CloseEvent) => void;
}
export default class WebSocketService {
private webSocket: WebSocket | null = null;
private webSocketURL: string = '';
// 默认回调函数
private onOpenCallback: (event: Event) => void;
private onMessageCallback: (event: MessageEvent) => void;
private onErrorCallback: (error: Event) => void;
private onCloseCallback: (event: CloseEvent) => void;
constructor(callbacks?: WebSocketCallbacks) {
// 设置回调函数,使用自定义回调或默认回调
this.onOpenCallback = callbacks?.onOpen || this.defaultOnOpen;
this.onMessageCallback = callbacks?.onMessage || this.defaultOnMessage;
this.onErrorCallback = callbacks?.onError || this.defaultOnError;
this.onCloseCallback = callbacks?.onClose || this.defaultOnClose;
}
// 初始化WebSocket连接
initWebSocket(webSocketURL: string): void {
this.webSocketURL = webSocketURL;
try {
this.webSocket = new WebSocket(webSocketURL);
this.webSocket.onopen = this.onOpenCallback;
this.webSocket.onmessage = this.onMessageCallback;
this.webSocket.onerror = this.onErrorCallback;
this.webSocket.onclose = this.onCloseCallback;
} catch (error) {
console.error('Failed to initialize WebSocket:', error);
}
}
// 设置onOpen回调并更新WebSocket事件处理器
setOnOpenCallback(callback: (event: Event) => void): void {
this.onOpenCallback = callback;
if (this.webSocket) {
this.webSocket.onopen = this.onOpenCallback;
}
}
// 设置onMessage回调并更新WebSocket事件处理器
setOnMessageCallback(callback: (event: MessageEvent) => void): void {
this.onMessageCallback = callback;
if (this.webSocket) {
this.webSocket.onmessage = this.onMessageCallback;
}
}
// 设置onError回调并更新WebSocket事件处理器
setOnErrorCallback(callback: (error: Event) => void): void {
this.onErrorCallback = callback;
if (this.webSocket) {
this.webSocket.onerror = this.onErrorCallback;
}
}
// 设置onClose回调并更新WebSocket事件处理器
setOnCloseCallback(callback: (event: CloseEvent) => void): void {
this.onCloseCallback = callback;
if (this.webSocket) {
this.webSocket.onclose = this.onCloseCallback;
}
}
// 默认连接建立成功的回调
private defaultOnOpen(event: Event): void {
console.log('WebSocket连接建立成功', event);
}
// 默认收到服务器消息的回调
private defaultOnMessage(event: MessageEvent): void {
console.log('收到服务器消息', event.data);
// 通常这里会解析数据并更新页面
// const data = JSON.parse(event.data);
// 根据消息类型处理不同业务...
}
// 默认发生错误的回调
private defaultOnError(error: Event): void {
console.error('WebSocket连接错误', error);
}
// 默认连接关闭的回调
private defaultOnClose(event: CloseEvent): void {
console.log('WebSocket连接关闭', event);
}
// 关闭连接
close(): void {
if (this.webSocket) {
this.webSocket.close();
this.webSocket = null;
}
}
// 发送消息
sendMessage(message: string | object): void {
if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
const dataToSend = typeof message === 'string' ? message : JSON.stringify(message);
this.webSocket.send(dataToSend);
} else {
console.error('WebSocket连接未就绪');
}
}
// 获取当前WebSocket状态
getReadyState(): number | null {
return this.webSocket ? this.webSocket.readyState : null;
}
// 获取WebSocket URL
getWebSocketURL(): string {
return this.webSocketURL;
}
// 重新连接
reconnect(): void {
if (this.webSocketURL) {
this.close();
this.initWebSocket(this.webSocketURL);
}
}
}
// 创建一个可导出的WebSocket实例
let globalWebSocketService: WebSocketService | null = null;
/**
* 初始化WebSocket连接的可导出方法
* @param url WebSocket服务器地址
* @param callbacks 回调函数对象(可选)
* @returns WebSocketService实例
*/
export function initWebSocket(callbacks?: WebSocketCallbacks): void {
if (!globalWebSocketService) {
globalWebSocketService = new WebSocketService(callbacks);
}
const accessStore = useAccessStore();
const clinetId = import.meta.env.VITE_GLOB_APP_CLIENT_ID;
const api = import.meta.env.VITE_GLOB_API_URL;
const host = window.location.host;
const url = `ws://${host}${api}/resource/websocket?clientid=${clinetId}&Authorization=Bearer ${accessStore.accessToken}`;
globalWebSocketService.initWebSocket(url);
}
/**
* 获取全局WebSocket服务实例
* @returns WebSocketService实例或null
*/
export function getWebSocketService(): WebSocketService | null {
return globalWebSocketService;
}

View File

@@ -1,30 +1,31 @@
import { createApp, watchEffect } from 'vue';
import { createApp, watchEffect } from 'vue'
import { registerAccessDirective } from '@vben/access';
import { registerLoadingDirective } from '@vben/common-ui/es/loading';
import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
import '@vben/styles/antd';
import { registerAccessDirective } from '@vben/access'
import { registerLoadingDirective } from '@vben/common-ui/es/loading'
import { preferences } from '@vben/preferences'
import { initStores } from '@vben/stores'
import '@vben/styles'
import '@vben/styles/antd'
import { useTitle } from '@vueuse/core';
import { useTitle } from '@vueuse/core'
import { setupGlobalComponent } from '#/components/global';
import { $t, setupI18n } from '#/locales';
import { setupGlobalComponent } from '#/components/global'
import { $t, setupI18n } from '#/locales'
import { initComponentAdapter } from './adapter/component';
import { initSetupVbenForm } from './adapter/form';
import App from './app.vue';
import { router } from './router';
import { initComponentAdapter } from './adapter/component'
import { initSetupVbenForm } from './adapter/form'
import App from './app.vue'
import { router } from './router'
import { initWebSocket } from '#/api/websocket'
async function bootstrap(namespace: string) {
// 初始化组件适配器
await initComponentAdapter();
await initComponentAdapter()
// 初始化表单组件
await initSetupVbenForm();
await initSetupVbenForm()
// // 设置弹窗的默认配置
// setDefaultModalProps({
@@ -35,49 +36,53 @@ async function bootstrap(namespace: string) {
// zIndex: 1020,
// });
const app = createApp(App);
const app = createApp(App)
// 全局组件
setupGlobalComponent(app);
setupGlobalComponent(app)
// 注册v-loading指令
registerLoadingDirective(app, {
loading: 'loading', // 在这里可以自定义指令名称也可以明确提供false表示不注册这个指令
spinning: 'spinning',
});
})
// 国际化 i18n 配置
await setupI18n(app);
await setupI18n(app)
// 配置 pinia-tore
await initStores(app, { namespace });
await initStores(app, { namespace })
// 初始化WebSocket
initWebSocket()
// 安装权限指令
registerAccessDirective(app);
registerAccessDirective(app)
// 初始化 tippy
const { initTippy } = await import('@vben/common-ui/es/tippy');
initTippy(app);
const { initTippy } = await import('@vben/common-ui/es/tippy')
initTippy(app)
// 配置路由及路由守卫
app.use(router);
app.use(router)
// 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin);
const { MotionPlugin } = await import('@vben/plugins/motion')
app.use(MotionPlugin)
// 动态更新标题
watchEffect(() => {
if (preferences.app.dynamicTitle) {
const routeTitle = router.currentRoute.value.meta?.title;
const routeTitle = router.currentRoute.value.meta?.title
const pageTitle =
(routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
useTitle(pageTitle);
(routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name
useTitle(pageTitle)
}
});
})
app.mount('#app');
app.mount('#app')
}
export { bootstrap };
export { bootstrap }

View File

@@ -1,13 +1,44 @@
<script setup lang="ts">
import { Page } from '@vben/common-ui';
import { useAccessStore } from '@vben/stores';
import { BackTop, message, Spin } from 'ant-design-vue';
import { Page, VbenCountToAnimator } from '@vben/common-ui';
import { ref, onMounted, onBeforeUnmount, reactive } from 'vue';
import FloorTree from '../components/floor-tree.vue';
import { getWebSocketService } from '#/api/websocket';
import { currentReading } from '#/api/property/energyManagement/meterInfo';
onMounted(() => {});
const ws = getWebSocketService();
onBeforeUnmount(() => {});
if (ws) {
// 使用setOnMessageCallback方法设置消息回调
ws.setOnMessageCallback((event: MessageEvent) => {
// 解析数据并更新UI
try {
const data = JSON.parse(event.data);
if (data.type === 'meter') {
if (typeof data.data === 'undefined') {
message.warn('当前楼层暂无电表!');
}
readingData.value = data.data;
readingTime.value = data.readingTime;
}
} catch (e) {
console.error('Error parsing data:');
}
readingLoading.value = false;
});
// 如果需要,也可以设置错误回调
ws.setOnErrorCallback((error: any) => {
console.log('Error in WebSocket:');
currentReading({ meterType: 0, floorId: 0 });
readingLoading.value = false;
});
}
onBeforeUnmount(() => {
currentReading({ meterType: 0, floorId: 0 });
});
const readingData = ref<any>({});
const readingTime = ref('');
@@ -16,21 +47,15 @@ async function handleSelectFloor(selectedKeys, info) {
if (typeof selectedKeys[0] === 'undefined') {
return;
}
if (ws.webSocket.readyState !== 1) {
message.warn('websocket未连接请刷新页面重试');
return;
}
readingLoading.value = true;
const reading = await currentReading({
await currentReading({
meterType: 1,
floorId: selectedKeys[0],
});
readingLoading.value = false;
if (reading === null) {
message.warn('当前楼层暂无电表数据!');
}
const nowTime =
new Date().toLocaleDateString().replace(/\//g, '-') +
' ' +
new Date().toLocaleTimeString();
readingTime.value = nowTime;
readingData.value = reading;
}
function targetFn() {
@@ -54,7 +79,13 @@ function targetFn() {
{{ item.meterName }}
</h2>
<div class="meterInfo-reading">
<p>{{ item.initReading }}</p>
<p>
<VbenCountToAnimator
:end-val="item.initReading"
:decimals="2"
prefix=""
/>
</p>
<div></div>
</div>
<div class="meterInfo-list">