Ver Fonte

refactor(components):组件重构
feat(websocket):ws增加断线重连机制

HMY há 7 meses atrás
pai
commit
f6e6965a49

+ 27 - 19
ui/src/components/DCS/ValveComponent.vue

@@ -21,8 +21,9 @@
 </template>
 
 <script setup>
-import { defineProps, computed } from 'vue'
+import { defineProps, computed ,defineEmits} from 'vue'
 import { setCoils } from '@/api/dcs/flowControl';
+import { openElWarnMessage } from '@/utils/message';
 const props = defineProps({
     title: {
         type: String,
@@ -45,12 +46,13 @@ const props = defineProps({
     },
 })
 
-
+// 定义 emits 声明(规范事件类型)
+const emit = defineEmits(['need-reconnect']);
 
 // 计算 pointer 的 class
 const pointerClass = computed(() => {
     // 未传入阀门信号状态时,该阀门为N阀门
-    if(null === props.valveOpenSignalStatus || null === props.valveCloseSignalStatus){
+    if (null === props.valveOpenSignalStatus || null === props.valveCloseSignalStatus) {
         // console.log('未传入阀门信号状态')
         return props.valveOpenObj?.value ? 'pointer_open' : 'pointer_close'
     }
@@ -68,27 +70,33 @@ const pointerClass = computed(() => {
 
 //阀门开启事件
 function onClickOpen() {
-    console.log('点击开启')
-    const param = {
-        dataArr: [true],
-        startAddress: props.valveOpenObj.address,
-        quantity: 1
+    const address = getValveOpenAddress();
+    if (address) {
+        setCoils({ dataArr: [true], startAddress: address, quantity: 1 });
     }
-    setCoils(param).then(res => {
-        console.log(res)
-    })
 }
 //阀门关闭事件
 function onClickClose() {
-    console.log('点击关闭')
-    const param = {
-        dataArr: [false],
-        startAddress: props.valveOpenObj.address,
-        quantity: 1
+    const address = getValveOpenAddress();
+    if (address) {
+        setCoils({ dataArr: [false], startAddress: address, quantity: 1 });
+    }
+}
+// 获取阀门地址 + 触发重连事件
+function getValveOpenAddress() {
+    // 未获取到阀门数据时,通知父组件重连
+    if (!props.valveOpenObj || typeof props.valveOpenObj !== 'object') {
+        openElWarnMessage('未获取到阀门数据,尝试重连WebSocket...');
+        // 触发自定义事件,通知父组件执行重连
+        emit('need-reconnect'); 
+        return null;
+    }
+    // 地址未定义(非连接问题)
+    if (!props.valveOpenObj.address) {
+        openElWarnMessage('阀门地址未定义,请检查配置');
+        return null;
     }
-    setCoils(param).then(res => {
-        console.log(res)
-    })
+    return props.valveOpenObj.address;
 }
 </script>
 

+ 66 - 7
ui/src/components/GeneralComponents/PumpComponent.vue

@@ -1,18 +1,19 @@
 <template>
     <!-- 泵组件 -->
-    <div class="pump" :style="{ width: `${props.iconSize}px`, height: `${props.iconSize}px` }" v-if="props.title">
+    <div class="pump" :style="{ width: `${props.iconSize}px`, height: `${props.iconSize}px` }" v-if="props.title" ref="pumpRef">
         <i class="icon iconfont-colour icon_pump" :class="getPumpClass"
             :style="{ fontSize: `${props.iconSize}px` }"></i>
-        <div class="pump_title zIndex10" :style="getPumpTitleStyle">
+        <div class="pump_title zIndex10" :style="[getPumpTitleStyle, getOutTextRotateStyle]">
             {{ props.title }}
         </div>
-        <div class="pump_status zIndex10" :style="getPumpStatusStyle"> {{ getStartStatus }} </div>
-        <div class="pump_hz zIndex10" :style="getPumpHzStyle"> {{ hzValue }} Hz</div>
+        <div class="pump_status zIndex10" :style="[getPumpStatusStyle, getInTextRotateStyle]"> {{ getStartStatus }} </div>
+        <div class="pump_hz zIndex10" :style="[getPumpHzStyle, getInTextRotateStyle]"> {{ hzValue }} Hz</div>
     </div>
 </template>
 
 <script setup>
-import { defineProps, computed } from 'vue'
+import { defineProps, computed, ref, onMounted, onUnmounted } from 'vue'
+
 const props = defineProps({
     title: {
         type: String,
@@ -31,6 +32,10 @@ const props = defineProps({
         default: [1, 10]
     },
 })
+
+const pumpRef = ref(null)
+const currentRotation = ref(0)  // 记录当前组件的旋转角度
+
 // 频率值 保留两位小数
 const hzValue = computed(() => {
     const raw = Number(props.pumpDataArr[1]) * 0.01;
@@ -82,6 +87,60 @@ const getPumpHzStyle = computed(() => {
     } : { fontSize: `${props.iconSize / 5}px` }
 })
 
+// 文本旋转样式 - 反向旋转以抵消组件的旋转
+const getOutTextRotateStyle = computed(() => {
+    return {
+        transform: `rotate(${-currentRotation.value}deg) translateX(-50%) translateY(-50%)`,
+        transition: 'transform 0.3s ease'
+    }
+})
+
+const getInTextRotateStyle = computed(() => {
+    return {
+        transform: `rotate(${-currentRotation.value}deg)`,
+        transition: 'transform 0.3s ease'
+    }
+})
+
+// 解析元素的旋转角度
+const getRotationAngle = (element) => {
+    if (!element) return 0;
+    
+    const style = window.getComputedStyle(element);
+    const matrix = new DOMMatrixReadOnly(style.transform);
+    
+    // 从变换矩阵计算旋转角度(单位:度)
+    let angle = Math.round(Math.atan2(matrix.b, matrix.a) * (180 / Math.PI));
+    return (angle < 0) ? angle + 360 : angle;  // 转换为0-360度
+}
+
+// 检查旋转变化的函数
+const checkRotation = () => {
+    if (pumpRef.value) {
+        const newRotation = getRotationAngle(pumpRef.value);
+        if (newRotation !== currentRotation.value) {
+            currentRotation.value = newRotation;
+        }
+    }
+}
+
+// 设置定期检查
+let rotationCheckInterval;
+
+onMounted(() => {
+    // 初始检查
+    checkRotation();
+    
+    // 每100ms检查一次旋转变化(可根据需要调整频率)
+    rotationCheckInterval = setInterval(checkRotation, 100);
+});
+
+onUnmounted(() => {
+    // 清除定时器
+    if (rotationCheckInterval) {
+        clearInterval(rotationCheckInterval);
+    }
+});
 </script>
 
 
@@ -98,7 +157,7 @@ const getPumpHzStyle = computed(() => {
     .pump_title {
         position: absolute;
         width: max-content;
-        transform: translate(-50%, -50%);
+        // transform: translate(-50%, -50%);
         top: 116%;
         left: 82%;
         color: aliceblue;
@@ -123,4 +182,4 @@ const getPumpHzStyle = computed(() => {
     }
 
 }
-</style>
+</style>

+ 98 - 14
ui/src/utils/ws/stompClient.js

@@ -10,11 +10,16 @@ class StompClient {
     this.subscriptions = new Map(); // pageCode -> subscription
     this.connectedCallbackQueue = [];
     this.connected = false;
+    this.reconnectTimer = null; // 重连计时器
+    this.reconnectInterval = 5000; // 重连间隔时间(ms)
+    this.maxReconnectAttempts = 0; // 最大重连次数,0表示无限重试
+    this.reconnectAttempts = 0; // 当前重连尝试次数
   }
 
   // 初始化连接
   connect() {
-    if (this.client) return;
+    // 如果已有连接或正在重连中,则不再重复连接
+    if (this.client || this.reconnectTimer) return;
 
     const socket = new SockJS(this.url);
     this.client = new Client({
@@ -23,7 +28,14 @@ class StompClient {
       // debug: (str) => console.log('[STOMP]', str),
       onConnect: () => {
         this.connected = true;
+        this.reconnectAttempts = 0; // 重置重连计数器
+        clearTimeout(this.reconnectTimer);
+        this.reconnectTimer = null;
+
         console.log('STOMP 连接成功');
+        // 重新订阅之前的主题
+        this.restoreSubscriptions();
+        // 执行连接成功后的回调队列
         this.connectedCallbackQueue.forEach((cb) => cb());
         this.connectedCallbackQueue = [];
       },
@@ -31,22 +43,69 @@ class StompClient {
         console.error('STOMP 错误', frame.headers['message'], frame.body);
       },
       onDisconnect: () => {
-        this.connected = false;
-        console.warn('STOMP 连接断开');
+        this.handleDisconnect();
+      },
+      onWebSocketClose: () => {
+        this.handleDisconnect();
       },
+      onWebSocketError: (error) => {
+        console.error('WebSocket 错误:', error);
+        this.handleDisconnect();
+      }
     });
 
     this.client.activate();
   }
 
+  // 处理断开连接
+  handleDisconnect() {
+    if (!this.connected) return; // 已经处于断开状态,无需处理
+
+    this.connected = false;
+    this.client = null;
+    console.warn('STOMP 连接断开,准备重连...');
+
+    // 如果达到最大重连次数限制,则停止重连
+    if (this.maxReconnectAttempts > 0 && this.reconnectAttempts >= this.maxReconnectAttempts) {
+      console.error(`已达到最大重连次数(${this.maxReconnectAttempts}),停止重连`);
+      return;
+    }
+
+    // 启动重连计时器
+    this.reconnectTimer = setTimeout(() => {
+      this.reconnectAttempts++;
+      console.log(`正在进行第 ${this.reconnectAttempts} 次重连...`);
+      this.connect();
+    }, this.reconnectInterval);
+  }
+
+  // 恢复之前的订阅
+  restoreSubscriptions() {
+    if (this.subscriptions.size === 0 || !this.client) return;
+
+    console.log(`重新订阅 ${this.subscriptions.size} 个主题`);
+    // 保存当前的订阅信息
+    const subscriptions = Array.from(this.subscriptions.entries());
+    // 清空现有订阅映射
+    this.subscriptions.clear();
+
+    // 重新订阅所有主题
+    subscriptions.forEach(([pageCode, { onMessage }]) => {
+      this.subscribeToPage(pageCode, onMessage, true);
+    });
+  }
+
   // 订阅页面数据
-  subscribeToPage(pageCode, onMessage) {
+  // restoreFlag用于标识是否是重连后的恢复订阅
+  subscribeToPage(pageCode, onMessage, restoreFlag = false) {
     if (!pageCode || typeof onMessage !== 'function') return;
 
     const doSubscribe = () => {
       const topic = `/topic/page/${pageCode}`;
       if (this.subscriptions.has(pageCode)) {
-        console.warn(`已订阅过页面 ${pageCode}`);
+        if (!restoreFlag) {
+          console.warn(`已订阅过页面 ${pageCode}`);
+        }
         return;
       }
 
@@ -56,8 +115,15 @@ class StompClient {
         onMessage(data);
       });
 
-      this.subscriptions.set(pageCode, subscription);
-      console.log(`订阅页面 ${pageCode} 成功`);
+      // 存储订阅对象和回调函数,用于重连后恢复
+      this.subscriptions.set(pageCode, {
+        subscription,
+        onMessage
+      });
+
+      if (!restoreFlag) {
+        console.log(`订阅页面 ${pageCode} 成功`);
+      }
     };
 
     if (this.connected) {
@@ -70,22 +136,40 @@ class StompClient {
 
   // 取消订阅
   unsubscribeFromPage(pageCode) {
-    const subscription = this.subscriptions.get(pageCode);
-    if (subscription) {
-      subscription.unsubscribe();
+    const subscriptionObj = this.subscriptions.get(pageCode);
+    if (subscriptionObj && subscriptionObj.subscription) {
+      subscriptionObj.subscription.unsubscribe();
       this.subscriptions.delete(pageCode);
       console.log(`已取消订阅页面 ${pageCode}`);
     }
   }
 
-  // 主动断开连接
+  // 主动断开连接,不触发自动重连
   disconnect() {
+    // 清除重连计时器
+    if (this.reconnectTimer) {
+      clearTimeout(this.reconnectTimer);
+      this.reconnectTimer = null;
+    }
+
     if (this.client) {
       this.client.deactivate();
       this.client = null;
-      this.connected = false;
-      this.subscriptions.clear();
-      console.log('STOMP 客户端已断开');
+    }
+
+    this.connected = false;
+    this.subscriptions.clear();
+    this.reconnectAttempts = 0;
+    console.log('STOMP 客户端已手动断开');
+  }
+
+  // 设置重连参数
+  setReconnectOptions(interval, maxAttempts) {
+    if (interval && typeof interval === 'number' && interval > 0) {
+      this.reconnectInterval = interval;
+    }
+    if (maxAttempts !== undefined && typeof maxAttempts === 'number' && maxAttempts >= 0) {
+      this.maxReconnectAttempts = maxAttempts;
     }
   }
 }

+ 36 - 9
ui/src/utils/ws/websocket.js

@@ -2,10 +2,13 @@
 import { wsConfig } from "@/config";
 class WebSocketClient {
     constructor(url) {
-      this.url = url;
-      this.ws = null;
-      this.reconnectTimeout = null;
+        this.url = url;
+        this.ws = null;
+        this.reconnectTimeout = null;
+        this.reconnectCount = 0; // 新增:重连次数计数器
+        this.maxReconnectCount = 10; // 新增:最大重连次数(可根据需求调整)
     }
+
   
     // 初始化 WebSocket 连接
     connect(callback) {
@@ -58,12 +61,36 @@ class WebSocketClient {
     }
   
     // 断线重连
-    reconnect() {
-      if (this.reconnectTimeout) clearTimeout(this.reconnectTimeout);
-      this.reconnectTimeout = setTimeout(() => {
-        console.log("尝试重新连接 WebSocket...");
-        this.connect();
-      }, 3000);
+     reconnect() {
+        // 1. 清除旧的重连定时器(避免重复触发)
+        if (this.reconnectTimeout) clearTimeout(this.reconnectTimeout);
+
+        // 2. 判断是否超过最大重连次数
+        if (this.reconnectCount >= this.maxReconnectCount) {
+            console.error(`WebSocket重连超过${this.maxReconnectCount}次,停止重试,请检查服务端`);
+            openElErrorElNotification('WebSocket连接失败,请联系管理员检查服务'); // 需导入对应提示方法
+            this.reconnectCount = 0; // 重置计数器(方便后续手动触发重连)
+            return;
+        }
+
+        // 3. 触发重连
+        this.reconnectTimeout = setTimeout(() => {
+            this.reconnectCount++;
+            console.log(`尝试重新连接WebSocket(第${this.reconnectCount}/${this.maxReconnectCount}次)...`);
+            
+            // 重连成功后重置计数器
+            this.connect((success) => {
+                if (success) {
+                    this.reconnectCount = 0;
+                }
+            });
+        }, 3000); // 3秒重试一次(可根据需求调整)
+    }
+
+    // 新增:手动触发重连(重置计数器,用于组件主动调用)
+    manualReconnect() {
+        this.reconnectCount = 0; // 重置计数器,确保重新开始重试
+        this.reconnect();
     }
   }