Jelajahi Sumber

实时数据和实时报警页面和接口开发

cuiHe 2 bulan lalu
induk
melakukan
adcfa9a53d

+ 34 - 1
backend/app/api/v1/module_business/crane/controller.py

@@ -1,10 +1,11 @@
 # -*- coding: utf-8 -*-
-
+import httpx
 from fastapi import APIRouter, Depends, UploadFile, Body, Path, Query
 from fastapi.responses import StreamingResponse, JSONResponse
 from redis.asyncio import Redis
 
 from app.common.response import SuccessResponse, StreamResponse
+from app.config.setting import settings
 from app.core.dependencies import AuthPermission, redis_getter
 from app.api.v1.module_system.auth.schema import AuthSchema
 from app.core.base_params import PaginationQueryParam
@@ -54,6 +55,38 @@ async def get_crane_list_controller(
     log.info("查询行车信息列表成功")
     return SuccessResponse(data=result_dict, msg="查询行车信息列表成功")
 
+@BizCraneRouter.get("/list_status", summary="查询行车信息列表", description="查询行车信息列表")
+async def get_crane_list_status_controller(
+    page: PaginationQueryParam = Depends(),
+    search: BizCraneQueryParam = Depends(),
+    auth: AuthSchema = Depends(AuthPermission(["module_business:crane:query"]))
+) -> JSONResponse:
+    """查询行车信息列表接口(数据库分页)"""
+    result_dict = await BizCraneService.page_crane_service(
+        auth=auth,
+        page_no=page.page_no if page.page_no is not None else 1,
+        page_size=page.page_size if page.page_size is not None else 10,
+        search=search,
+        order_by=page.order_by
+    )
+    #请求采集接口获取状态信息
+    async with httpx.AsyncClient() as client:
+        response = await client.get(
+            url=settings.COLLECT_DATA_FULL,
+            params={},
+            timeout=2
+        )
+        if response.status_code == 200:
+            json_data = response.json()
+            if json_data['code'] == 200 and json_data['data']:
+                for item in result_dict['items']:
+                    crane_no = item['crane_no']
+                    crane_data = json_data.get('data').get(crane_no)
+                    if crane_data:
+                        item['work_status'] = crane_data.get('data').get('status').get('status')
+    log.info("查询行车信息列表成功")
+    return SuccessResponse(data=result_dict, msg="查询行车信息列表成功")
+
 @BizCraneRouter.post("/create", summary="创建行车信息", description="创建行车信息")
 async def create_crane_controller(
     data: BizCraneCreateSchema,

+ 60 - 4
backend/app/api/v1/module_business/vardict/controller.py

@@ -1,9 +1,10 @@
 # -*- coding: utf-8 -*-
-
+import httpx
 from fastapi import APIRouter, Depends, UploadFile, Body, Path, Query
 from fastapi.responses import StreamingResponse, JSONResponse
 from redis.asyncio.client import Redis
 from app.common.response import SuccessResponse, StreamResponse
+from app.config.setting import settings
 from app.core.dependencies import AuthPermission, redis_getter
 from app.api.v1.module_system.auth.schema import AuthSchema
 from app.core.base_params import PaginationQueryParam
@@ -42,6 +43,38 @@ async def get_vardict_list_controller(
     )
     log.info("查询变量信息列表成功")
     return SuccessResponse(data=result_dict, msg="查询变量信息列表成功")
+@BizVarDictRouter.get("/list_alarms", summary="查询变量信息列表", description="查询变量信息列表")
+async def get_vardict_list_alarms_controller(
+    page: PaginationQueryParam = Depends(),
+    search: BizVarDictQueryParam = Depends(),
+    auth: AuthSchema = Depends(AuthPermission(["module_business:vardict:query"]))
+) -> JSONResponse:
+    """查询变量信息列表接口(数据库分页)"""
+    result_dict = await BizVarDictService.page_vardict_service(
+        auth=auth,
+        page_no=page.page_no if page.page_no is not None else 1,
+        page_size=page.page_size if page.page_size is not None else 10,
+        search=search,
+        order_by=[{'mec_type':'asc'}]
+    )
+    #请求采集接口获取状态信息
+    async with httpx.AsyncClient() as client:
+        response = await client.get(
+            url=settings.COLLECT_DATA_FULL,
+            params={},
+            timeout=2
+        )
+        if response.status_code == 200:
+            json_data = response.json()
+            if json_data['code'] == 200 and json_data['data']:
+                for item in result_dict['items']:
+                    item['value'] = False
+                    crane_no = item['crane_no']
+                    alarm = json_data.get('data').get(crane_no).get('data').get('alarm').get(item['var_code'])
+                    if alarm:
+                        item['value'] = alarm.get('value')
+    log.info("查询变量信息列表成功")
+    return SuccessResponse(data=result_dict, msg="查询变量信息列表成功")
 
 @BizVarDictRouter.post("/create", summary="创建变量信息", description="创建变量信息")
 async def create_vardict_controller(
@@ -128,15 +161,38 @@ async def export_vardict_template_controller() -> StreamingResponse:
         headers={'Content-Disposition': 'attachment; filename=biz_var_dict_template.xlsx'}
     )
 
-@BizVarDictRouter.get("/varDictMecGroup/{id}", summary="获取变量信息分组数据", description="获取变量信息分组数据")
+@BizVarDictRouter.get("/varDictMecGroup/{crane_no}", summary="获取变量信息分组数据", description="获取变量信息分组数据")
 async def get_vardict_mec_group_controller(
-    id: int = Path(..., description="ID"),
+    crane_no: str = Path(..., description="crane_no"),
     redis: Redis = Depends(redis_getter),
     auth: AuthSchema = Depends(AuthPermission(["module_business:crane:query"]))
 ) -> JSONResponse:
 
     result_dict = await BizVarDictService.get_vardict_group_service(
-        redis=redis, id=id,auth=auth
+        redis=redis, crane_no=crane_no,auth=auth
     )
+    if not result_dict:
+        log.info(f"获取变量信息分组数据成功:{result_dict}")
+        return SuccessResponse(data=result_dict, msg="获取变量分组数据成功")
+    #请求采集接口获取状态信息
+    async with httpx.AsyncClient() as client:
+        response = await client.get(
+            url=settings.COLLECT_DATA_FULL,
+            params={},
+            timeout=2
+        )
+        if response.status_code == 200:
+            json_data = response.json()
+            if json_data['code'] == 200 and json_data['data']:
+                json_analog = json_data.get('data').get(crane_no).get('data').get('analog')
+                json_digital = json_data.get('data').get(crane_no).get('data').get('digital')
+                for var_dict in result_dict:
+                    for key,inner_dict in var_dict.items():
+                        if key != 'mec_type' and key != 'alarm_varList' and key != 'varList_simple':
+                            for item in inner_dict:
+                                if key == 'digital_varList':
+                                    item['value'] = json_digital.get(item.get('var_code')).get('value')
+                                else:
+                                    item['value'] = json_analog.get(item.get('var_code')).get('value')
     log.info(f"获取变量信息分组数据成功:{result_dict}")
     return SuccessResponse(data=result_dict, msg="获取变量分组数据成功")

+ 10 - 3
backend/app/api/v1/module_business/vardict/schema.py

@@ -82,7 +82,9 @@ class BizVarDictQueryParam:
         updated_id: int | None = Query(None, description="更新人ID"),
         created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
         updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
-        
+        # 新增:场景区分参数,默认 False(代表后台内部请求),True 代表前端接口请求
+        is_api_request: bool | None = Query(False, description="是否为前端接口请求(用于切换 switch_type 查询方式)")
+
     ) -> None:
         
         # 精确查询字段
@@ -95,8 +97,10 @@ class BizVarDictQueryParam:
         self.mec_type = mec_type
         # 精确查询字段
         self.data_type = data_type
-        # 精确查询字段
-        self.switch_type = switch_type
+        if is_api_request:
+            self.switch_type = (">=", switch_type)
+        else:
+            self.switch_type = switch_type
         # 模糊查询字段
         self.addr = ("like", addr)
         # 精确查询字段
@@ -146,4 +150,7 @@ class VarDictMecGroupSchema(BaseModel):
     行车信息页面数据模型
     """
     mec_type: str = Field(default=..., description='所属机构')
+    alarm_varList: list[BizVarDictOutSchema] | None = Field(default=None, description='报警变量数据')
+    digital_varList: list[BizVarDictOutSchema] | None = Field(default=None, description='开关量变量数据')
+    analog_varList: list[BizVarDictOutSchema] | None = Field(default=None, description='模拟量变量数据')
     varList_simple: list[BizVarDictOutSchema] | None = Field(default=None, description='变量数据')

+ 14 - 6
backend/app/api/v1/module_business/vardict/service.py

@@ -325,7 +325,7 @@ class BizVarDictService:
             option_list=option_list
         )
     @classmethod
-    async def get_vardict_group_service(cls,auth: AuthSchema, redis: Redis,id: int):
+    async def get_vardict_group_service(cls,auth: AuthSchema, redis: Redis,crane_no: str):
         """
         从缓存获取变量分组数据列表信息service
 
@@ -337,8 +337,7 @@ class BizVarDictService:
         - list[dict]: 变量分组数据列表
         """
         try:
-            crane = await BizCraneCRUD(auth).get_by_id_crane_crud(id)
-            redis_key = f"{RedisInitKeyConfig.VAR_DICT.key}:{crane.crane_no}"
+            redis_key = f"{RedisInitKeyConfig.VAR_DICT.key}:{crane_no}"
             obj_list_dict = await RedisCURD(redis).get(redis_key)
 
             # 确保返回数据正确序列化
@@ -347,7 +346,7 @@ class BizVarDictService:
                     try:
                         return json.loads(obj_list_dict)
                     except json.JSONDecodeError:
-                        log.warning(f"变量分组数据反序列化失败,尝试重新初始化缓存: {crane.crane_name}")
+                        log.warning(f"变量分组数据反序列化失败,尝试重新初始化缓存: {'行车:'+crane_no}")
                 elif isinstance(obj_list_dict, list):
                     return obj_list_dict
 
@@ -400,11 +399,20 @@ class BizVarDictService:
                             mec_list = await BizMecCRUD(auth).list(search={'crane_no':crane_no,'status':'1'},order_by=[{'sort':'asc'}])
                             for mec in mec_list:
                                 # 获取分组数据
-                                varDicts = await BizVarDictCRUD(auth).list(search={'crane_no':crane_no,'mec_type':mec.mec_type,'status':'1'},order_by=[{'var_sort':'asc'}])
+                                varDicts = await BizVarDictCRUD(auth).list(
+                                    search={'crane_no': crane_no, 'mec_type': mec.mec_type, 'status': '1'},
+                                    order_by=[{'var_sort': 'asc'}])
                                 if not varDicts:
                                     continue
+                                alarmVarList = await BizVarDictCRUD(auth).list(search={'crane_no': crane_no,'mec_type':mec.mec_type, 'switch_type': ('>=','2'), 'status': '1'},order_by=[{'var_sort': 'asc'}])
+                                digitalVarList = await BizVarDictCRUD(auth).list(search={'crane_no':crane_no,'mec_type':mec.mec_type,'data_type':'1','status':'1'},order_by=[{'var_sort':'asc'}])
+                                analogVarList = await BizVarDictCRUD(auth).list(search={'crane_no': crane_no,'mec_type':mec.mec_type, 'data_type': ('!=', '1'), 'status': '1'},order_by=[{'var_sort': 'asc'}])
                                 varDictMecGroupSchema.append(
-                                    VarDictMecGroupSchema(mec_type=mec.mec_type, varList_simple=varDicts))
+                                    VarDictMecGroupSchema(mec_type=mec.mec_type,
+                                                          varList_simple=varDicts,
+                                                          digital_varList=digitalVarList,
+                                                          analog_varList=analogVarList,
+                                                          alarm_varList=alarmVarList))
                             # 保存到Redis并设置过期时间
                             redis_key = f"{RedisInitKeyConfig.VAR_DICT.key}:{crane_no}"
                             var_dict_list = [item.model_dump() for item in varDictMecGroupSchema]

+ 5 - 0
backend/app/config/setting.py

@@ -162,6 +162,11 @@ class Settings(BaseSettings):
     OPENAI_API_KEY: str = ''
     OPENAI_MODEL: str = ''
 
+    # ================================================= #
+    # ******************* 数据采集配置 ****************** #
+    # ================================================= #
+    COLLECT_DATA_FULL: str = ''
+
     # ================================================= #
     # ******************* 请求限制配置 ****************** #
     # ================================================= #

+ 1 - 1
backend/app/plugin/init_app.py

@@ -131,7 +131,7 @@ def register_routers(app: FastAPI) -> None:
     返回:
     - None
     """
-    app.include_router(router=router, dependencies=[Depends(RateLimiter(times=5, seconds=10))])
+    app.include_router(router=router, dependencies=[Depends(RateLimiter(times=10, seconds=10))])
 
 def register_files(app: FastAPI) -> None:
     """

+ 1 - 1
backend/app/utils/console.py

@@ -27,7 +27,7 @@ def create_service_panel(
 
     # 核心服务信息
     service_info = Text()
-    service_info.append(f"服务名称 {settings.TITLE} • 优雅 • 简洁 • 高效", style="bold magenta")
+    service_info.append(f"服务名称 {settings.TITLE}", style="bold magenta")
     service_info.append(f"\n当前版本 v{settings.VERSION}" , style="bold green")
     service_info.append(f"\n服务地址 {url}", style="bold blue")
     service_info.append(f"\n运行环境 {settings.ENVIRONMENT.value if hasattr(settings.ENVIRONMENT, 'value') else settings.ENVIRONMENT}", style="bold red")

+ 9 - 7
backend/banner.txt

@@ -1,8 +1,10 @@
 
-    ___               _                   _                __                _            
-  .' ..]             / |_                (_)              |  ]              (_)           
- _| |_  ,--.   .--. `| |-',--.  _ .--.   __   ,--.    .--.| |  _ .--..--.   __   _ .--.   
-'-| |-'`'_\ : ( (`\] | | `'_\ :[ '/'`\ \[  | `'_\ : / /'`\' | [ `.-. .-. | [  | [ `.-. |  
-  | |  // | |, `'.'. | |,// | |,| \__/ | | | // | |,| \__/  |  | | | | | |  | |  | | | |  
- [___] \'-;__/[\__) )\__/\'-;__/| ;.__/ [___]\'-;__/ '.__.;__][___||__||__][___][___||__] 
-                               [__|                                                       
+ ,--.-,,-,--,       ,----.   .-._                          ,---.         .=-.-.
+/==/  /|=|  |    ,-.--` , \ /==/ \  .-._    _,..---._    .--.'  \       /==/_ /
+|==|_ ||=|, |   |==|-  _.-` |==|, \/ /, / /==/,   -  \   \==\-/\ \     |==|, |
+|==| ,|/=| _|   |==|   `.-. |==|-  \|  |  |==|   _   _\  /==/-|_\ |    |==|  |
+|==|- `-' _ |  /==/_ ,    / |==| ,  | -|  |==|  .=.   |  \==\,   - \   |==|- |
+|==|  _     |  |==|    .-'  |==| -   _ |  |==|,|   | -|  /==/ -   ,|   |==| ,|
+|==|   .-. ,\  |==|_  ,`-._ |==|  /\ , |  |==|  '='   / /==/-  /\ - \  |==|- |
+/==/, //=/  |  /==/ ,     / /==/, | |- |  |==|-,   _`/  \==\ _.\=\.-'  /==/. /
+`--`-' `-`--`  `--`-----``  `--`./  `--`  `-.`.____.'    `--`          `--`-`

+ 4 - 1
backend/env/.env.dev

@@ -25,7 +25,7 @@ DEMO_ENABLE = False
 DATABASE_TYPE = "mysql"  # mysql、postgres、[qlite、dm这俩种不支持代码生成]
 
 # 数据库配置
-DATABASE_HOST = "localhost"
+DATABASE_HOST = "192.168.0.247"
 DATABASE_PORT = 3306  # MySQL:3306 PostgreSQL:5432
 DATABASE_USER = "root" # mysql:root, postgresql:tao
 DATABASE_PASSWORD = "!23Qwe"
@@ -46,3 +46,6 @@ LOGGER_LEVEL = 'DEBUG'   # 日志级别
 OPENAI_BASE_URL = https://dashscope.aliyuncs.com/compatible-mode/v1
 OPENAI_API_KEY = sk-e688534f2d984e7fa2eb46add409422f
 OPENAI_MODEL = qwen-plus
+
+# 数据采集服务地址
+COLLECT_DATA_FULL = http://192.168.0.247:5000/api/data/full

+ 3 - 0
backend/env/.env.prod

@@ -46,3 +46,6 @@ LOGGER_LEVEL = 'INFO'
 OPENAI_BASE_URL = https://dashscope.aliyuncs.com/compatible-mode/v1
 OPENAI_API_KEY = sk-e688534f2d984e7fa2eb46add409422f
 OPENAI_MODEL = qwen-plus
+
+# 数据采集服务地址
+COLLECT_DATA_FULL = http://192.168.0.247:5000/api/data/full

TEMPAT SAMPAH
backend/static/image/favicon.png


+ 1 - 5
frontend/index.html

@@ -4,11 +4,7 @@
     <meta charset="UTF-8" />
     <link rel="icon" href="/logo.png" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <meta name="description" content="Vue3 + Vite + TypeScript + Element-Plus 的后台管理模板 " />
-    <meta
-      name="keywords"
-      content="vue,element-plus,typescript,vue-element-admin,vue3-element-admin"
-    />
+    <meta name="description" content="恒达行车地面站管理系统" />
     <title>%VITE_APP_TITLE%</title>
   </head>
 

+ 10 - 1
frontend/src/api/module_business/crane.ts

@@ -12,6 +12,15 @@ const BizCraneAPI = {
     });
   },
 
+    // 列表查询
+  listBizCraneStatus(query: BizCranePageQuery) {
+    return request<ApiResponse<PageResult<BizCraneTable[]>>>({
+      url: `${API_PATH}/list_status`,
+      method: "get",
+      params: query,
+    });
+  },
+
   // 详情查询
   detailBizCrane(id: number) {
     return request<ApiResponse<BizCraneTable>>({
@@ -120,7 +129,7 @@ export interface BizCraneTable extends BaseType{
   work_span?: string;
   work_height?: string;
   ip_address?: string;
-  online_status?: string;
+  work_status?: string;
   order?: string;
   created_id?: string;
   updated_id?: string;

+ 28 - 3
frontend/src/api/module_business/vardict.ts

@@ -1,4 +1,5 @@
 import request from "@/utils/request";
+import { A } from "vue-router/dist/router-CWoNjPRp.mjs";
 
 const API_PATH = "/business/vardict";
 
@@ -12,6 +13,14 @@ const BizVarDictAPI = {
     });
   },
 
+  listBizVarDictAlarms(query: BizVarDictPageQuery) {
+    return request<ApiResponse<PageResult<BizVarDictTable[]>>>({
+      url: `${API_PATH}/list_alarms`,
+      method: "get",
+      params: query,
+    });
+  },
+
   // 详情查询
   detailBizVarDict(id: number) {
     return request<ApiResponse<BizVarDictTable>>({
@@ -21,9 +30,9 @@ const BizVarDictAPI = {
   },
 
   // 获取变量信息
-  varDictMecGroup(id: number) {
-    return request<ApiResponse<BizVarDictTable>>({
-      url: `${API_PATH}/varDictMecGroup/${id}`,
+  varDictMecGroup(crane_no: string) {
+    return request<ApiResponse<MecDataItem[]>>({
+      url: `${API_PATH}/varDictMecGroup/${crane_no}`,
       method: "get",
     });
   },
@@ -119,6 +128,7 @@ export interface BizVarDictPageQuery extends PageQuery {
   updated_id?: number;
   created_time?: string[];
   updated_time?: string[];
+  is_api_request?: string;
 }
 
 // 列表展示项
@@ -151,6 +161,7 @@ export interface BizVarDictTable extends BaseType{
   updated_id?: string;
   created_by?: creatorType;
   updated_by?: updatorType;
+  value?:string;
 }
 
 // 新增/修改/详情表单参数
@@ -180,3 +191,17 @@ export interface BizVarDictForm extends BaseFormType{
   is_upload?: string;
   diagnosis_id?: string;
 }
+
+export interface VarDictMecGroupData{
+  mec_type?: string;
+  bool_type_list?:BizVarDictTable[]
+  gear_list?:BizVarDictTable[]
+  number_type_list?:BizVarDictTable[]
+}
+
+export interface MecDataItem {
+  mec_type: string;
+  varList_simple: BizVarDictTable[];
+  digital_varList: BizVarDictTable[];
+  analog_varList: BizVarDictTable[];
+}

+ 68 - 0
frontend/src/utils/common.ts

@@ -147,4 +147,72 @@ export function blobValidate(data: Blob): boolean {
   return data.type !== "application/json";
 }
 
+export const gearCalculation = (varData: any[]) => {
+  varData.forEach((item) => {
+    //剔除掉重点显示和配电总览
+    if (item.mec_type != '0' && item.mec_type != '6') {
+      let virtualizationCode = '';
+      const gears: number[] = [];
+      let number = 0;
+      item.bool_type_list.forEach((boolItem: any) => { 
+        if(boolItem.var_category == '1'){
+          gears.push(boolItem.value ? 1 : 0)
+        }
+      })
+      if (gears.length == 0) {
+        return;
+      }
+      //有零位剔除
+      if (gears.length == 6) {
+        gears.shift();
+      }
+
+      virtualizationCode = 'G00'+item.mec_type
+      let str ='零挡';
+
+      item.number_type_list.forEach((numberItem: any) => { 
+        if(numberItem.var_code === virtualizationCode){
+          let direction = gears[0] + '' + gears[1];
+          if(direction == '00'){
+            numberItem.value = str;
+          }else{
+            number += 1;
+            gears.forEach((gearItem, index) => {
+              if (index > 1 && gearItem == 1) {
+                number += 1;
+              }
+            })
+            if (direction == '01') {
+              number = -number
+            }
+            let positiveDirectionStr = '';
+            let oppositeDirectionStr = '';
+            switch (item.mec_type) {
+              case '1':
+              case '2':
+              case '7':
+              case '8':
+                positiveDirectionStr = '上升'
+                oppositeDirectionStr = '下降'
+                break;
+              case '3':
+                positiveDirectionStr = '左行'
+                oppositeDirectionStr = '右行'
+                break;
+              case '4':
+              case '5':
+                positiveDirectionStr = '前行'
+                oppositeDirectionStr = '后行'
+                break;
+            }
+            str = number > 0 ? positiveDirectionStr + number : oppositeDirectionStr + Math.abs(number);
+            str += '挡';
+            numberItem.value = str;
+          }
+        }
+      })
+    }
+  })
+  return reactive(varData);
+}
 

+ 111 - 134
frontend/src/utils/mqttUtil.ts

@@ -1,202 +1,179 @@
 // src/utils/mqttUtil.ts
-import mqtt, { MqttClient, IClientOptions  } from 'mqtt';
+import mqtt, { MqttClient, IClientOptions } from 'mqtt';
 
 /**
  * MQTT消息处理回调类型
- * @param topic 消息主题
- * @param payload 消息内容(已解析为JSON对象,若解析失败则为原始字符串)
  */
 export type MqttMessageCallback = (topic: string, payload: any) => void;
 
 /**
- * MQTT工具类配置项
+ * 单个MQTT实例配置项
  */
-export interface MqttConfig {
-  wsUrl: string; // MQTT WS连接地址
-  clientOptions?: IClientOptions ; // MQTT客户端配置(可选,如用户名、密码等)
-  defaultTopics?: string[]; // 默认订阅的主题列表(可选)
+export interface MqttInstanceConfig {
+  wsUrl: string; // 当前组件的MQTT WS地址
+  clientOptions?: IClientOptions; // 客户端配置(用户名、密码等)
+  topics: string[]; // 当前组件要订阅的主题列表
 }
 
 /**
- * MQTT工具类(单例模式,避免重复创建连接
+ * MQTT工具类(非单例,每个组件可独立实例化
  */
 class MqttUtil {
-  // 私有属性
-  private static instance: MqttUtil; // 单例实例
-  private mqttClient: MqttClient | null = null; // MQTT客户端实例
-  private mqttConfig: MqttConfig; // MQTT配置
-  private messageCallback: MqttMessageCallback | null = null; // 全局消息回调
+  // 实例私有属性(每个实例独立拥有)
+  private mqttClient: MqttClient | null = null;
+  private config: MqttInstanceConfig;
+  private messageCallback: MqttMessageCallback | null = null;
+  private isConnected: boolean = false; // 当前实例连接状态
 
   /**
-   * 私有构造函数(单例模式,禁止外部new
-   * @param config MQTT配置项
+   * 构造函数(每个组件new独立实例
+   * @param config 当前实例的配置项
    */
-  private constructor(config: MqttConfig) {
-    this.mqttConfig = config;
+  constructor(config: MqttInstanceConfig) {
+    this.config = config;
   }
 
   /**
-   * 获取MQTT工具类单例实例
-   * @param config MQTT配置项(仅首次调用需传入,后续调用无需重复传入)
-   * @returns MqttUtil单例
+   * 初始化当前组件的MQTT连接(创建连接+订阅主题)
+   * @param callback 当前组件的消息回调
    */
-  public static getInstance(config?: MqttConfig): MqttUtil {
-    if (!MqttUtil.instance) {
-      if (!config) {
-        throw new Error('首次创建MQTT实例时,必须传入配置项!');
-      }
-      MqttUtil.instance = new MqttUtil(config);
-    }
-    return MqttUtil.instance;
-  }
-
-  /**
-   * 初始化MQTT客户端(创建连接+订阅默认主题)
-   * @param messageCallback 消息处理回调(可选,也可后续通过setMessageCallback设置)
-   */
-  public initMqttClient(messageCallback?: MqttMessageCallback): void {
-    // 避免重复连接
-    if (this.mqttClient && this.mqttClient.connected) {
-      console.log('[MQTT] 客户端已连接,无需重复初始化');
-      // 若传入新的回调,更新全局回调
-      if (messageCallback) {
-        this.messageCallback = messageCallback;
-      }
-      // 若已连接,直接订阅默认主题(补充:避免重复订阅)
-      const { defaultTopics } = this.mqttConfig;
-      if (defaultTopics && defaultTopics.length > 0) {
-        this.subscribeTopics(defaultTopics);
-      }
+  public initConnect(callback: MqttMessageCallback): void {
+    // 避免重复初始化
+    if (this.isConnected && this.mqttClient) {
+      console.log(`[MQTT] 当前组件连接已建立,无需重复初始化`);
+      this.messageCallback = callback;
       return;
     }
-  
-    const { wsUrl, clientOptions, defaultTopics } = this.mqttConfig;
-    console.log(`[MQTT] 开始连接 Broker: ${wsUrl}`);
-  
-    // 创建MQTT客户端
+
+    const { wsUrl, clientOptions, topics } = this.config;
+    console.log(`[MQTT] 组件独立连接 Broker: ${wsUrl}`);
+
+    // 创建独立的MQTT客户端
     this.mqttClient = mqtt.connect(wsUrl, clientOptions);
-    // 保存消息回调
-    if (messageCallback) {
-      this.messageCallback = messageCallback;
-    }
-  
-    // 绑定各类事件(默认主题订阅移到该方法内的connect回调)
-    this.bindClientEvents(defaultTopics); // 传入defaultTopics
+    this.messageCallback = callback;
+
+    // 绑定事件
+    this.bindEvents(topics);
   }
 
   /**
-   * 绑定MQTT客户端事件(连接、消息、断开、错误)
+   * 绑定当前实例的MQTT事件
+   * @param topics 要订阅的主题
    */
-  private bindClientEvents(defaultTopics?: string[]): void {
+  private bindEvents(topics: string[]): void {
     if (!this.mqttClient) return;
-  
-    // 连接成功回调
+
+    // 连接成功
     this.mqttClient.on('connect', () => {
-      console.log('[MQTT] 连接成功');
-      // 连接成功后,订阅默认主题
-      if (defaultTopics && defaultTopics.length > 0) {
-        this.subscribeTopics(defaultTopics);
-      }
+      this.isConnected = true;
+      console.log(`[MQTT] 组件独立连接成功`);
+      // 连接成功后订阅当前组件的主题
+      this.subscribeTopics(topics);
     });
-  
-    // 接收消息回调(统一处理,转发给全局回调)
+
+    // 接收消息(组件专属回调)
     this.mqttClient.on('message', (topic, payload) => {
-        let payloadStr = payload.toString();
-        let data: any = payloadStr;
-    
-        // 步骤1:还原Unicode转义字符为中文(核心处理)
-        try {
-          // 方法1:利用JSON.parse间接还原(推荐,兼容性更好)
-          payloadStr = JSON.parse(`"${payloadStr.replace(/"/g, '\\"')}"`);
-          // 步骤2:解析还原后的JSON字符串
-          data = JSON.parse(payloadStr);
-        } catch (err) {
-          // 方法2:备用方案 - 正则表达式直接替换Unicode转义
-          const unicodeReg = /\\u([0-9a-fA-F]{4})/g;
-          payloadStr = payloadStr.replace(unicodeReg, (_, hex) => {
-            return String.fromCharCode(parseInt(hex, 16));
-          });
-          try {
-            data = JSON.parse(payloadStr);
-          } catch (err2) {
-            console.warn('[MQTT] 消息非JSON格式,使用原始字符串:', err2);
-            data = payloadStr;
-          }
-        }
-    
-        // 调用全局消息回调
-        if (this.messageCallback) {
-          this.messageCallback(topic, data);
-        }
-      });
-    }
+      let data: any = payload.toString();
+      // 组件内单独处理中文转义(按需开启)
+      try {
+        // 还原Unicode中文 + 解析JSON
+        const unicodeReg = /\\u([0-9a-fA-F]{4})/g;
+        const payloadStr = payload.toString().replace(unicodeReg, (_, hex) => {
+          return String.fromCharCode(parseInt(hex, 16));
+        });
+        data = JSON.parse(payloadStr);
+      } catch (err) {
+        console.warn('[MQTT] 消息非JSON格式,使用原始字符串:', err);
+      }
+      // 执行当前组件的回调
+      if (this.messageCallback) {
+        this.messageCallback(topic, data);
+      }
+    });
+
+    // 连接错误
+    this.mqttClient.on('error', (err) => {
+      this.isConnected = false;
+      console.error(`[MQTT] 组件独立连接错误:`, err);
+    });
+
+    // 连接断开
+    this.mqttClient.on('close', () => {
+      this.isConnected = false;
+      console.log(`[MQTT] 组件独立连接已断开`);
+    });
+  }
+
   /**
-   * 订阅主题(支持单个/多个主题)
-   * @param topics 主题字符串或主题数组
+   * 订阅主题(当前实例专属
+   * @param topics 主题列表/单个主题
    */
   public subscribeTopics(topics: string | string[]): void {
-    if (!this.mqttClient || !this.mqttClient.connected) {
-      console.error('[MQTT] 客户端未连接,无法订阅主题');
+    if (!this.mqttClient || !this.isConnected) {
+      console.error(`[MQTT] 组件连接未建立,无法订阅主题`);
       return;
     }
 
-    this.mqttClient.subscribe(topics, (err) => {
+    const topicList = Array.isArray(topics) ? topics : [topics];
+    this.mqttClient.subscribe(topicList, (err) => {
       if (err) {
-        console.error(`[MQTT] 订阅主题失败:`, err);
+        console.error(`[MQTT] 组件订阅主题失败:`, err);
         return;
       }
-      const topicStr = Array.isArray(topics) ? topics.join('、') : topics;
-      console.log(`[MQTT] 订阅主题成功:${topicStr}`);
+      console.log(`[MQTT] 组件订阅主题成功:${topicList.join('、')}`);
     });
   }
 
   /**
-   * 取消订阅主题
-   * @param topics 主题字符串或主题数组
+   * 取消订阅主题(当前实例专属)
+   * @param topics 主题列表/单个主题
    */
   public unsubscribeTopics(topics: string | string[]): void {
-    if (!this.mqttClient || !this.mqttClient.connected) {
-      console.error('[MQTT] 客户端未连接,无法取消订阅');
+    if (!this.mqttClient || !this.isConnected) {
+      console.error(`[MQTT] 组件连接未建立,无法取消订阅`);
       return;
     }
 
-    this.mqttClient.unsubscribe(topics, (err) => {
+    const topicList = Array.isArray(topics) ? topics : [topics];
+    this.mqttClient.unsubscribe(topicList, (err) => {
       if (err) {
-        console.error(`[MQTT] 取消订阅主题失败:`, err);
+        console.error(`[MQTT] 组件取消订阅主题失败:`, err);
         return;
       }
-      const topicStr = Array.isArray(topics) ? topics.join('、') : topics;
-      console.log(`[MQTT] 取消订阅主题成功:${topicStr}`);
+      console.log(`[MQTT] 组件取消订阅主题成功:${topicList.join('、')}`);
     });
   }
 
   /**
-   * 设置消息处理回调(后续可动态更新)
-   * @param callback 消息处理回调
+   * 释放当前实例所有资源(断开连接+清空回调+置空实例)
    */
-  public setMessageCallback(callback: MqttMessageCallback): void {
-    this.messageCallback = callback;
-  }
-
-  /**
-   * 断开MQTT连接
-   */
-  public disconnect(): void {
-    if (this.mqttClient && this.mqttClient.connected) {
-      this.mqttClient.end();
+  public releaseResources(): void {
+    // 1. 仅当连接有效时,执行断开操作(无需手动取消订阅)
+    if (this.mqttClient) {
+      try {
+        // end(false):优雅断开(先发送完未发送的消息,再断开),默认false
+        // 无需手动取消订阅,Broker会自动清理该客户端的所有订阅
+        this.mqttClient.end(false, () => {
+          console.log(`[MQTT] 组件MQTT连接优雅断开`);
+        });
+      } catch (err) {
+        console.warn(`[MQTT] 组件MQTT连接断开时出现异常:`, err);
+      }
       this.mqttClient = null;
-      console.log('[MQTT] 主动断开连接');
     }
+  
+    // 2. 清空回调和连接状态(无论连接是否有效,都要重置)
+    this.messageCallback = null;
+    this.isConnected = false;
+    console.log(`[MQTT] 组件MQTT资源已全部释放`);
   }
 
   /**
-   * 判断MQTT客户端是否已连接
-   * @returns 连接状态(true=已连接,false=未连接)
+   * 获取当前实例连接状态
    */
-  public isConnected(): boolean {
-    return !!this.mqttClient && this.mqttClient.connected;
+  public getConnectStatus(): boolean {
+    return this.isConnected;
   }
 }
 
-// 导出单例实例(默认先不初始化,需在项目入口/首次使用时初始化)
 export default MqttUtil;

+ 3 - 3
frontend/src/views/module_business/gateway/index.vue

@@ -456,10 +456,10 @@
               />
             </el-select>
           </el-form-item>
-          <el-form-item label="PLC型号 " prop="plc_model" :required="true" v-if="formData.gateway_type === 1">
+          <el-form-item label="PLC型号 " prop="plc_model" :required="true" v-if="formData.gateway_type == '1'">
             <el-input v-model="formData.plc_model" placeholder="请输入PLC型号 " />
           </el-form-item>
-          <el-form-item label="端口号" prop="serial_port_name" :required="true" v-if="formData.gateway_type === 1">
+          <el-form-item label="端口号" prop="serial_port_name" :required="true" v-if="formData.gateway_type == '1'">
             <el-input v-model="formData.serial_port_name" placeholder="请输入端口号" />
           </el-form-item>
           <el-form-item label="波特率 " prop="serial_baud_rate" :required="false">
@@ -867,7 +867,7 @@ async function handleOpenDialog(type: "create" | "update" | "detail", id?: numbe
     formData.id = undefined;
     formData.crane_no = undefined;
     formData.gateway_name = undefined;
-    formData.gateway_type = 1;
+    formData.gateway_type = undefined;
     formData.gateway_ipaddress = undefined;
     formData.gateway_port = undefined;
     formData.plc_brand = undefined;

+ 25 - 25
frontend/src/views/module_business/vardict/index.vue

@@ -326,7 +326,7 @@
           min-width="100">
           <template #default="scope">
             {{
-                (scope.row.switch_type
+                (scope.row.switch_type || scope.row.switch_type === 0
                   ? (dictStore.getDictLabel("switch_type", scope.row.switch_type) as any)
                   : undefined
                 )?.dict_label || scope.row.switch_type
@@ -1006,14 +1006,14 @@ const formData = reactive<BizVarDictForm>({
   var_category: undefined,
   translate: undefined,
   device_no: undefined,
-  is_reverse: 0,
-  is_top_show: 0,
-  is_save: 1,
-  is_calibration: 0,
-  is_overview_top_show: 0,
-  is_home_page_show: 0,
-  is_diagnose: 0,
-  is_upload: 0,
+  is_reverse: '0',
+  is_top_show: '0',
+  is_save: "1",
+  is_calibration: '0',
+  is_overview_top_show: '0',
+  is_home_page_show: '0',
+  is_diagnose: '0',
+  is_upload: '0',
   diagnosis_id: undefined,
   status: '1',
   description: undefined,
@@ -1191,14 +1191,14 @@ const initialFormData: BizVarDictForm = {
   var_category: undefined,
   translate: undefined,
   device_no: undefined,
-  is_reverse: 0,
-  is_top_show: 0,
-  is_save: 1,
-  is_calibration: 0,
-  is_overview_top_show: 0,
-  is_home_page_show: 0,
-  is_diagnose: 0,
-  is_upload: 0,
+  is_reverse: '0',
+  is_top_show: '0',
+  is_save: '1',
+  is_calibration: '0',
+  is_overview_top_show: '0',
+  is_home_page_show: '0',
+  is_diagnose: '0',
+  is_upload: '0',
   diagnosis_id: undefined,
   status: '1',
   description: undefined,
@@ -1254,14 +1254,14 @@ async function handleOpenDialog(type: "create" | "update" | "detail", id?: numbe
     formData.var_category = undefined;
     formData.translate = undefined;
     formData.device_no = undefined;
-    formData.is_reverse = 0;
-    formData.is_top_show = 0;
-    formData.is_save = 1;
-    formData.is_calibration = 0;
-    formData.is_overview_top_show = 0;
-    formData.is_home_page_show = 0;
-    formData.is_diagnose = 0;
-    formData.is_upload = 0;
+    formData.is_reverse = '0';
+    formData.is_top_show = '0';
+    formData.is_save = '1';
+    formData.is_calibration = '0';
+    formData.is_overview_top_show = '0';
+    formData.is_home_page_show = '0';
+    formData.is_diagnose = '0';
+    formData.is_upload = '0';
     formData.diagnosis_id = undefined;
     formData.status = '1';
     formData.description = undefined;

+ 108 - 54
frontend/src/views/web/detail/realtimeAlarm.vue

@@ -1,66 +1,120 @@
 <template>
-  <!-- <div>
+  <div>
     <div class="el-table-content">
-      <pro-table :height="tabHeight" :loading="tab_loading" :data="allData" :config="tableConfig">
+      <pro-table :height="tabHeight" :loading="tab_loading" :data="filteredData" :config="tableConfig">
         <template #default="{ row }">
-          <div :class="getColor(row.switch_type)">
-            {{ row.switch_type }}
+          <div :class="getColor(row.switch_type.toString())">
+            {{ (row.switch_type 
+                  ? (dictStore.getDictLabel("switch_type", row.switch_type) as any)
+                  : undefined
+                )?.dict_label || row.switch_type }}
           </div>
         </template>
       </pro-table>
     </div>
-  </div> -->
+  </div>
 </template>
 
-<script setup>
-// import { ref } from 'vue';
-// import MqttService from '@/utils/mqttService';
-
-// const route = useRoute();
-// const tabHeight = ref('calc(100vh - 70px - 5px - 50px - 10px - 44px)')
-// const mqttService = new MqttService();
-// const topics = ['gc/alert'];
-// const tab_loading = ref(true)
-// const tableConfig = ref([{
-//   prop: 'msg',
-//   label: '报警内容'
-// },
-// {
-//   prop: 'switch_type',
-//   label: '报警级别',
-//   slot: 'true'
-// }])
-// const allData = ref([]);
-// const getData = async () => {
-//   mqttService.connect();
-//   topics.forEach(topic => {
-//     mqttService.subscribe(topic, (message) => {
-//       allData.value = [];
-//       message = JSON.parse(message.toString());
-//       message.data.forEach(item => {
-//         if (item.crane_no === route.params.craneNo) {
-//           item.switch_type = item.switch_type === 2 ? '预警' : item.switch_type === 3 ? '报警' : '故障';
-//           allData.value.push(item);
-//         }
-//       })
-//       tab_loading.value = false
-//     });
-//   });
-// }
-// onMounted(async () => {
-//   getData();
-// })
-
-// const getColor = (type) => {
-//   switch (type) {
-//     case '报警':
-//       return 'pilot-lamp-bg-yellow';
-//     case '预警':
-//       return 'pilot-lamp-bg-orange';
-//     case '故障':
-//       return 'pilot-lamp-bg-red';
-//   }
-// }
+<script setup lang="ts">
+import { ref } from 'vue';
+import { useDictStore } from "@/store";
+import BizVarDictAPI, { BizVarDictTable,BizVarDictPageQuery,MecDataItem } from '@/api/module_business/vardict'
+import MqttUtil, { MqttMessageCallback } from '@/utils/mqttUtil';
+
+const tabHeight = ref('calc(100vh - 70px - 5px - 50px - 10px - 44px)')
+const craneInfo = JSON.parse(localStorage.getItem('craneInfo') || '{}')
+const tab_loading = ref(true)
+const tableConfig = ref([{
+  prop: 'var_name',
+  label: '报警内容'
+},
+{
+  prop: 'switch_type',
+  label: '报警级别',
+  slot: 'true'
+}])
+
+const dictStore = useDictStore()
+const dictTypes: any = [
+  'switch_type'
+]
+
+const mqttConfig = {
+  wsUrl: import.meta.env.VITE_APP_WS_ENDPOINT || 'ws://127.0.0.1:9001',
+  topics: ['cdc/'+craneInfo.crane_no+'/alarm/#']
+};
+const mqttUtil = new MqttUtil(mqttConfig);
+
+const queryFormVarDictData = reactive<BizVarDictPageQuery>({
+  page_no: 1,
+  page_size: 100,
+  crane_no: craneInfo.crane_no,
+  var_code: undefined,
+  var_name: undefined,
+  mec_type: undefined,
+  switch_type: '2',
+  gateway_id: undefined,
+  var_group: undefined,
+  var_category: undefined,
+  is_top_show: undefined,
+  is_save: undefined,
+  is_overview_top_show: undefined,
+  is_home_page_show: undefined,
+  status: undefined,
+  created_time: undefined,
+  updated_time: undefined,
+  created_id: undefined,
+  updated_id: undefined,
+  is_api_request: 'True',
+});
+const allData = ref<BizVarDictTable[]>([]);
+
+const filteredData = computed(() => {
+  return allData.value.filter(item => item.value);
+});
+const getData = async () => {
+  const response = await BizVarDictAPI.listBizVarDictAlarms(queryFormVarDictData);
+  allData.value = response.data.data.items
+  tab_loading.value = false
+}
+
+const handleMqttMessage: MqttMessageCallback = (topic, payload) => {
+  let topic_levels = topic.split('/')
+  let crane_no = topic_levels[1]
+  let suffix = topic_levels[2]
+  if (suffix === 'alarm') {
+    allData.value.forEach((item) => {
+      if (item.var_code === payload.var_code) {
+        item.value = payload.value
+      }
+    });
+  }
+}
+
+
+
+const getColor = (type:string) => {
+  switch (type) {
+    case '2':
+      return 'pilot-lamp-bg-yellow';
+    case '3':
+      return 'pilot-lamp-bg-orange';
+    case '4':
+      return 'pilot-lamp-bg-red';
+  }
+}
+
+onMounted(async () => {
+  if (dictTypes.length > 0) {
+    await dictStore.getDict(dictTypes)
+  }
+  getData();
+  mqttUtil.initConnect(handleMqttMessage);
+})
+
+onUnmounted(() => {
+  mqttUtil.releaseResources();
+});
 </script>
 
 <style lang="less" scoped>

+ 133 - 135
frontend/src/views/web/detail/realtimeData.vue

@@ -4,91 +4,94 @@
     <div v-loading="loading" class="mec-content">
       <div class="mec-item" v-for="(item, index) in varDictMecGroupData" :key="index">
         <div class="mec-title">
-          {{ (item.MecType 
-                  ? (dictStore.getDictLabel("mec_type", item.MecType) as any)
+          {{ (item.mec_type 
+                  ? (dictStore.getDictLabel("mec_type", item.mec_type) as any)
                   : undefined
-                )?.dict_label || detailFormData.MecType }}
+                )?.dict_label || item.mec_type }}
         </div>
         <div class="mec-content-item">
-          <div class="number-type-content" v-for="(temp, index1) in item.numberTypeList" :key="index1">
-            <span>{{ temp.VarName }}</span>
+          <div class="number-type-content" v-for="(temp, index1) in item.number_type_list" :key="index1">
+            <span>{{ temp.var_name }}</span>
             <div>
-              <span style="font-size: 22px;">{{ temp.Value }}</span>
+              <span style="font-size: 22px;">{{ temp.value }}</span>
             </div>
           </div>
           <div style="margin-top: 30px;">
-            <div class="bool-type-content" v-for="(temp, index1) in item.boolTypeList" :key="index1">
-              <span>{{ temp.VarName }}</span>
-              <span :class="getBgColor(temp.Value, temp.SwitchType)"></span>
+            <div class="bool-type-content" v-for="(temp, index1) in item.bool_type_list" :key="index1">
+              <span>{{ temp.var_name }}</span>
+              <span :class="getBgColor(temp.value, temp.switch_type)"></span>
             </div>
           </div>
         </div>
       </div>
+      <div v-if="varDictMecGroupData.length === 0" class="el-table-empty">
+        <el-image :src="emptybg"></el-image>
+        <span>当前无数据</span>
+      </div>
     </div>
   </div>
 
 </template>
 <script setup lang="ts">
-import BizVarDictAPI, { } from '@/api/module_business/vardict'
+import BizVarDictAPI, { BizVarDictTable,VarDictMecGroupData,MecDataItem } from '@/api/module_business/vardict'
 import { useRoute } from 'vue-router'
 import { useDictStore } from "@/store";
-import mqtt, { MqttClient } from 'mqtt';
-//import MqttService from '@/utils/mqttService';
-//import { getCraneMecTree } from '@/api/crane';
-//import { mecDataFormat, gearCalculation } from '@/utils/hooks'
+import emptybgUrl from '@/assets/images/empty-bg.png';
+import MqttUtil, { MqttMessageCallback } from '@/utils/mqttUtil';
+import { reactive, onMounted, onUnmounted, inject } from 'vue';
+import { gearCalculation } from '@/utils/common';
+import { S } from 'vue-router/dist/router-CWoNjPRp.mjs';
 
 const route = useRoute()
 const receiveData = inject<(data: { craneName: string; isShowHomeButton: boolean }) => void>('receiveData');
 const craneInfo = JSON.parse(localStorage.getItem('craneInfo') || '{}')
-const varDictMecGroupData = ref([])
+const varDictMecGroupData = ref<VarDictMecGroupData[]>([])
 const loading = ref(true);
-const MQTT_WS_URL = import.meta.env.VITE_APP_WS_ENDPOINT || 'ws://127.0.0.1:9001';
-const VAR_TOPIC = 'gc/var/' + craneInfo.crane_no;
-let mqttClient: MqttClient | null = null;
+const emptybg = ref(emptybgUrl)
+
+const mqttConfig = {
+  wsUrl: import.meta.env.VITE_APP_WS_ENDPOINT || 'ws://127.0.0.1:9001',
+  topics: ['cdc/'+craneInfo.crane_no+'/analog/batch/#','cdc/'+craneInfo.crane_no+'/digital/#']
+};
+
+const mqttUtil = new MqttUtil(mqttConfig);
+
+
 const getCraneMecTreeData = async () => {
-  const response = await BizVarDictAPI.varDictMecGroup(craneInfo.id);
+  const response = await BizVarDictAPI.varDictMecGroup(craneInfo.crane_no);
   varDictMecGroupData.value = mecDataFormat(response.data.data)
-  console.log(varDictMecGroupData.value)
+  //挡位计算
+  gearCalculation(varDictMecGroupData.value)
+  loading.value = false;
   //localStorage.setItem('varDict', JSON.stringify(deviceStateData.value))
 }
 
-const mecDataFormat = (mecData) => {
-  const resultData = []
+const mecDataFormat = (mecData: MecDataItem[]) => {
+  const resultData: VarDictMecGroupData[] = []
   if (mecData) {
     mecData.forEach((item) => {
-      const numberTypelist = []
-      const boolTypeList = []
-      const gearList = []
-      item.varList_simple.forEach((simpleItem) => {
-        if (simpleItem.data_type === 1) {
-            boolTypeList.push({
-              VarName: simpleItem.var_name,
-              Value: true,
-              VarCode: simpleItem.var_code,
-              SwitchType: simpleItem.switch_type
-            })
-          } else {
-            numberTypelist.push({
-              VarName: simpleItem.var_name,
-              Value: 0,
-              VarCategory: simpleItem.var_category,
-              VarCode: simpleItem.var_code
-            })
-          }
-        if (simpleItem.var_category == 1) {
-          gearList.push({
-            VarName: simpleItem.var_name,
-            Value: false,
-            VarCode: simpleItem.var_code,
-            VarCategory: simpleItem.var_category
-          })
+      let numberTypelist: BizVarDictTable[] = []
+      let boolTypeList: BizVarDictTable[] = []
+      let gearList: BizVarDictTable[] = []
+      numberTypelist = [...item.analog_varList]
+      boolTypeList = [...item.digital_varList]
+      if(item.mec_type != '0' && item.mec_type != '6'){
+        numberTypelist.push({
+          var_code: 'G00'+item.mec_type,
+          var_name: '挡位',
+          value: '0'
+        })
+      }
+      boolTypeList.forEach((boolItem) => {
+        if (boolItem.var_category == "1") {
+          gearList.push(boolItem) 
         }
       })
       resultData.push({
-        MecType: item.mec_type,
-        numberTypeList: numberTypelist,
-        boolTypeList: boolTypeList,
-        gearList: gearList
+        mec_type: item.mec_type,
+        bool_type_list: boolTypeList,
+        gear_list: gearList,
+        number_type_list: numberTypelist
       })
     })
   }
@@ -104,84 +107,60 @@ const dictTypes: any = [
 const getData = () => {
   getCraneMecTreeData()
 }
-const getBgColor = (bool, type) => {
-  switch (type) {
-    case 1:
-      return bool ? 'pilot-lamp-bg-green' : 'pilot-lamp-bg-grey';
-    case 2:
-      return bool ? 'pilot-lamp-bg-green' : 'pilot-lamp-bg-yellow';
-    case 3:
-      return bool ? 'pilot-lamp-bg-green' : 'pilot-lamp-bg-orange';
-    case 4:
-      return bool ? 'pilot-lamp-bg-green' : 'pilot-lamp-bg-red';
-  }
-}
+const getBgColor = (bool: string | undefined, type: string | undefined) => {
+  
+  const validType = type || '0';
 
-//初始化 MQTT 连接并订阅主题
-const initMqttClient = () => {
-  // 避免重复连接
-  if (mqttClient && mqttClient.connected) {
-    console.log('[MQTT] 客户端已连接,无需重复初始化');
-    return;
-  }
-  console.log(`[MQTT] 开始连接 Broker: ${MQTT_WS_URL}`);
+  const falseColorMap: Record<string, string> = {
+    '0': 'pilot-lamp-bg-grey',
+    '1': 'pilot-lamp-bg-grey',
+    '2': 'pilot-lamp-bg-green',
+    '3': 'pilot-lamp-bg-green',
+    '4': 'pilot-lamp-bg-green'
+  };
 
-  // 创建 MQTT 客户端
-  mqttClient = mqtt.connect(MQTT_WS_URL);
+  const trueColorMap: Record<string, string> = {
+    '0': 'pilot-lamp-bg-green',
+    '1': 'pilot-lamp-bg-green',
+    '2': 'pilot-lamp-bg-yellow',
+    '3': 'pilot-lamp-bg-orange',
+    '4': 'pilot-lamp-bg-red'
+  };
 
-  // 连接成功回调
-  mqttClient.on('connect', () => {
-    console.log('[MQTT] 连接成功');
+  const class_name = bool 
+    ? trueColorMap[validType]
+    : falseColorMap[validType] || 'pilot-lamp-bg-grey';
 
-    // 同时订阅两个主题
-    mqttClient?.subscribe([VAR_TOPIC], (err) => {
-      if (err) {
-        console.error('[MQTT] 订阅主题失败:', err);
-        return;
-      }
-      console.log(`[MQTT] 订阅主题成功:${VAR_TOPIC}`);
-    });
-  });
-
-  // 接收消息回调(统一处理所有主题消息)
-  mqttClient.on('message', (topic, payload) => {
-    console.log(`[MQTT] 收到主题 ${topic} 的消息:`, payload.toString());
-    try {
-      const message = JSON.parse(payload.toString());
-      //挡位计算
-      message = gearCalculation(message, varDictMecGroupData.value)
-      message.data.forEach(msgItem => {
-        deviceStateData.value.forEach(item => {
-          item.numberTypeList.forEach(numberItem => {
-            if (msgItem.var_code === numberItem.VarCode) {
-              numberItem.Value = msgItem.value
-              return;
-            }
-          })
-          item.boolTypeList.forEach(boolItem => {
-            if (msgItem.var_code === boolItem.VarCode) {
-              boolItem.Value = msgItem.value
-              return;
-            }
-          })
+  return class_name;
+};
+
+const handleMqttMessage: MqttMessageCallback = (topic, payload) => {
+  let topic_levels = topic.split('/')
+  let crane_no = topic_levels[1]
+  let type = topic_levels[2]
+  let var_code = topic_levels[3]
+  varDictMecGroupData.value.forEach(item => {
+    if(type == 'analog' && item.number_type_list && Array(item.number_type_list)){
+      item.number_type_list.forEach(numberItem => {
+        payload.data.forEach((payloadItem: any) => {
+          if (payloadItem.var_code === numberItem.var_code) {
+            numberItem.value = payloadItem.value
+          }
         })
       })
-      loading.value = false
-    } catch (err) {
-      console.error('[MQTT] 解析消息失败:', err);
+    }else if(type == 'digital' && item.bool_type_list && Array(item.bool_type_list)){
+      item.bool_type_list.forEach(boolItem => {
+        if (var_code === boolItem.var_code) {
+          boolItem.value = payload.value
+          return;
+        }
+      })
     }
-  });
-
-  // 连接断开回调
-  mqttClient.on('close', () => {
-    console.log('[MQTT] 连接已断开');
-  });
+  })
+  //挡位计算
+  gearCalculation(varDictMecGroupData.value)
+}
 
-  // 连接错误回调
-  mqttClient.on('error', (err) => {
-    console.error('[MQTT] 连接错误:', err);
-  });
-};
 
 onMounted(async () => {
   if (dictTypes.length > 0) {
@@ -191,22 +170,11 @@ onMounted(async () => {
     receiveData({ craneName: craneInfo.crane_name ?? '', isShowHomeButton: true });
   }
   getData()
-  mqttService.connect();
-  topics.forEach(topic => {
-    mqttService.subscribe(topic, (message) => {
-
-    });
-  });
+  mqttUtil.initConnect(handleMqttMessage);
 });
 
 onUnmounted(() => {
-  // 页面销毁时主动断开 MQTT 连接
-  if (mqttClient) {
-    mqttClient.end(true, () => {
-      console.log('[MQTT] 主动断开连接');
-      mqttClient = null;
-    });
-  }
+  mqttUtil.releaseResources();
 });
 
 </script>
@@ -315,4 +283,34 @@ onUnmounted(() => {
   background: #f39902;
   border-radius: 50%;
 }
+
+.el-table-empty {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  color: #8ECAFF;
+
+  img {
+    width: 200px;
+    height: auto;
+    margin-bottom: 20px;
+  }
+}
+
+::-webkit-scrollbar {
+  width: 5px;
+  height: 5px;
+}
+
+::-webkit-scrollbar-track {
+  border-radius: 10px;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 7px;
+  background-color: #798DAE;
+}
 </style>

+ 93 - 44
frontend/src/views/web/overview/index.vue

@@ -7,16 +7,16 @@
         </div>
         <div class="overview-left-content">
           <ul v-loading="alarm_loading">
-            <li v-for="(item, index) in alertData" :key="index">
-              <div class="content-item" :class="getColor(item.switch_type??'')">
+            <li v-for="(item, index) in varDictData" :key="index">
+              <div v-if="item.value" class="content-item" :class="getColor(item.switch_type?.toString() ?? '')">
                 <IconAlarm />
                 <span style="width:120px; margin-left: 15px;">{{ item.crane_name }}</span>
-                <span style="width: 2px;height: 60%;margin: 0px 30px;" :class="getColorSpan(item.switch_type??'')" />
-                <span>{{ item.msg }}</span>
+                <span style="width: 2px;height: 60%;margin: 0px 30px;" :class="getColorSpan(item.switch_type?.toString() ?? '')" />
+                <span>{{ item.var_name }}</span>
               </div>
             </li>
           </ul>
-          <div v-if="!alarm_loading && alertData.length === 0" class="el-table-empty">
+          <div v-if="!alarm_loading && !hasActiveAlarms" class="el-table-empty">
             <el-image :src="emptybg"></el-image>
             <span>当前无数据</span>
           </div>
@@ -37,9 +37,12 @@
                 </div>
               </div>
               <div v-if="label === '在线状态'">
-                <el-tag effect="dark" v-if="row.online_status != '在线' && row.online_status != '离线'" type="warning">连接中</el-tag>
-                <el-tag effect="dark" v-if="row.online_status === '在线'" type="success">在线</el-tag>
-                <el-tag effect="dark" v-if="row.online_status === '离线'" type="danger">离线</el-tag>
+                <el-tag effect="dark" v-if="!row.work_status && row.work_status != '0'" type="warning">连接中</el-tag>
+                <el-tag effect="dark" v-if="row.work_status == '0'" type="danger">离线</el-tag>
+                <el-tag effect="dark" v-if="row.work_status == '1'" type="success">运行</el-tag>
+                <el-tag effect="dark" v-if="row.work_status == '2'" type="info">停止</el-tag>
+                <el-tag effect="dark" v-if="row.work_status == '3'" type="success">工作</el-tag>
+                <el-tag effect="dark" v-if="row.work_status == '4'" type="primary">空闲</el-tag>
               </div>
             </template>
           </pro-table>
@@ -51,17 +54,12 @@
 
 <script lang="ts" setup>
 import BizCraneAPI, { BizCranePageQuery, BizCraneTable } from '@/api/module_business/crane'
+import BizVarDictAPI, { BizVarDictPageQuery, BizVarDictTable } from '@/api/module_business/vardict'
 import emptybgUrl from '@/assets/images/empty-bg.png';
 import { onMounted, onUnmounted, ref, reactive, inject } from 'vue';
 import { useRouter } from 'vue-router';
 import MqttUtil, { MqttMessageCallback } from '@/utils/mqttUtil';
 
-interface alertData {
-  switch_type?: string;
-  crane_name?: string;
-  msg?: string;
-}
-
 // 路由 & 全局注入
 const router = useRouter()
 const receiveData = inject<(data: { craneName: string; isShowHomeButton: boolean }) => void>('receiveData');
@@ -75,15 +73,18 @@ const alarm_loading = ref(true);
 const tab_loading = ref(true)
 
 // 业务数据
-const alertData = ref<alertData[]>([])
 const craneData = ref<BizCraneTable[]>([]);
+const varDictData = ref<BizVarDictTable[]>([])
+
+const hasActiveAlarms = computed(() => {
+  return varDictData.value.some(item => item.value);
+});
 
 const mqttConfig = {
   wsUrl: import.meta.env.VITE_APP_WS_ENDPOINT || 'ws://127.0.0.1:9001',
-  clientOptions: {},
-  defaultTopics: ['gc/alert', 'gc/crane_status']
-}
-const mqttUtil = MqttUtil.getInstance(mqttConfig);
+  topics: ['cdc/+/alarm/#', 'cdc/+/status']
+};
+const mqttUtil = new MqttUtil(mqttConfig);
 
 // 表格配置
 const tableConfig = ref([
@@ -114,8 +115,7 @@ const tableConfig = ref([
   }
 ])
 
-// 分页查询参数
-const queryFormData = reactive<BizCranePageQuery>({
+const queryFormCraneData = reactive<BizCranePageQuery>({
   page_no: 1,
   page_size: 100,
   crane_name: undefined,
@@ -129,6 +129,29 @@ const queryFormData = reactive<BizCranePageQuery>({
   updated_id: undefined,
 });
 
+const queryFormVarDictData = reactive<BizVarDictPageQuery>({
+  page_no: 1,
+  page_size: 100,
+  crane_no: undefined,
+  var_code: undefined,
+  var_name: undefined,
+  mec_type: undefined,
+  switch_type: '2',
+  gateway_id: undefined,
+  var_group: undefined,
+  var_category: undefined,
+  is_top_show: undefined,
+  is_save: undefined,
+  is_overview_top_show: undefined,
+  is_home_page_show: undefined,
+  status: undefined,
+  created_time: undefined,
+  updated_time: undefined,
+  created_id: undefined,
+  updated_id: undefined,
+  is_api_request: 'True',
+});
+
 // 操作按钮点击事件
 const handleClick = (item: BizCraneTable) => {
   if (receiveData) {
@@ -142,31 +165,41 @@ const handleClick = (item: BizCraneTable) => {
 const getCraneListData = async () => {
   try {
     tab_loading.value = true
-    const response = await BizCraneAPI.listBizCrane(queryFormData);
+    const response = await BizCraneAPI.listBizCraneStatus(queryFormCraneData);
     craneData.value = response.data.data.items;
   } finally {
     tab_loading.value = false
   }
 }
 
+const getVarDictData = async () => {
+  try {
+    alarm_loading.value = true;
+    const response = await BizVarDictAPI.listBizVarDictAlarms(queryFormVarDictData);
+    varDictData.value = response.data.data.items;
+  } finally {
+    alarm_loading.value = false
+  }
+}
+
 // 颜色样式处理
 const getColor = (type: string) => {
   switch (type) {
-    case 2:
+    case '2':
       return 'content-item-yellow';
-    case 3:
+    case '3':
       return 'content-item-orange';
-    case 4:
+    case '4':
       return 'content-item-red';
   }
 }
 const getColorSpan = (type: string) => {
   switch (type) {
-    case 2:
+    case '2':
       return 'content-item-span-yellow';
-    case 3:
+    case '3':
       return 'content-item-span-orange';
-    case 4:
+    case '4':
       return 'content-item-span-red';
   }
 }
@@ -174,37 +207,40 @@ const getColorSpan = (type: string) => {
 // 初始化业务数据
 const getData = () => {
   getCraneListData()
+  getVarDictData()
 }
 
 const handleMqttMessage: MqttMessageCallback = (topic, payload) => {
-  if (topic === 'gc/alert') {
-    alarm_loading.value = false;
-    alertData.value = payload.data || [];
-  } else if (topic === 'gc/crane_status') {
-    if (Array.isArray(payload)) {
-      payload.forEach((item: any) => {
-        craneData.value.forEach((craneItem) => {
-          if (craneItem.crane_no === item.crane_no) {
-            craneItem.online_status = item.is_online ? '在线' : '离线';
-          }
-        });
-      });
-    }
+  let topic_levels = topic.split('/')
+  let crane_no = topic_levels[1]
+  let suffix = topic_levels[2]
+  if (suffix === 'alarm') {
+    varDictData.value.forEach((item) => {
+      if (item.crane_no === crane_no && item.var_code === payload.var_code) {
+        item.value = payload.value
+      }
+    });
+  } else if (suffix === 'status') {
+    craneData.value.forEach((craneItem) => {
+      if (craneItem.crane_no === crane_no) {
+        craneItem.work_status = payload.status;
+      }
+    });
   }
 }
 
 
 onMounted(async () => {
   getData();
-  mqttUtil.initMqttClient(handleMqttMessage);
+  mqttUtil.initConnect(handleMqttMessage);
+  
   if (receiveData) {
     receiveData({ craneName: '', isShowHomeButton: false });
   }
 });
 
 onUnmounted(() => {
-  mqttUtil.unsubscribeTopics(['gc/alert','gc/crane_status']);
-  mqttUtil.disconnect();
+  mqttUtil.releaseResources();
 });
 </script>
 
@@ -351,4 +387,17 @@ onUnmounted(() => {
     margin-bottom: 20px;
   }
 }
+::-webkit-scrollbar {
+  width: 5px;
+  height: 5px;
+}
+
+::-webkit-scrollbar-track {
+  border-radius: 10px;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 7px;
+  background-color: #798DAE;
+}
 </style>