瀏覽代碼

历史数据,历史报警,操作记录,页面和接口开发

cuiHe 1 月之前
父節點
當前提交
861070450f

+ 26 - 16
backend/app/api/v1/module_business/crane/controller.py

@@ -55,11 +55,12 @@ 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"]))
+        page: PaginationQueryParam = Depends(),
+        search: BizCraneQueryParam = Depends(),
+        auth: AuthSchema = Depends(AuthPermission(["module_business:crane:query"]))
 ) -> JSONResponse:
     """查询行车信息列表接口(数据库分页)"""
     result_dict = await BizCraneService.page_crane_service(
@@ -69,21 +70,30 @@ async def get_crane_list_status_controller(
         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:
+    try:
+        async with httpx.AsyncClient() as client:
+            # 捕获请求层面的异常(超时、连接失败等)
+            response = await client.get(
+                url=settings.COLLECT_DATA_FULL,
+                params={},
+                timeout=2
+            )
+            # 捕获HTTP状态码非200的情况
+            response.raise_for_status()
+            # 捕获JSON解析异常
             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)
+            # 捕获数据结构异常(key不存在)
+            if json_data.get('code') == 200 and json_data.get('data'):
+                for item in result_dict.get('items', []):
+                    crane_no = item.get('crane_no')
+                    if not crane_no:
+                        continue
+                    crane_data = json_data.get('data', {}).get(crane_no)
                     if crane_data:
-                        item['work_status'] = crane_data.get('data').get('status').get('status')
+                        item['work_status'] = crane_data.get('data', {}).get('status', {}).get('status')
+    except Exception as e:
+        log.error(f"调用采集接口获取行车状态时发生未知异常:{str(e)}", exc_info=True)
+
     log.info("查询行车信息列表成功")
     return SuccessResponse(data=result_dict, msg="查询行车信息列表成功")
 

+ 14 - 4
backend/app/api/v1/module_business/crane/service.py

@@ -67,7 +67,7 @@ class BizCraneService:
         obj = await BizCraneCRUD(auth).create_crane_crud(data=data)
         if obj:
             # 更新缓存中数据
-            await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:*")
+            await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:{data.crane_no}")
         return BizCraneOutSchema.model_validate(obj).model_dump()
     
     @classmethod
@@ -83,7 +83,7 @@ class BizCraneService:
         obj = await BizCraneCRUD(auth).update_crane_crud(id=id, data=data)
         if obj:
             # 更新缓存中数据
-            await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:*")
+            await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:{obj.crane_no}")
         return BizCraneOutSchema.model_validate(obj).model_dump()
     
     @classmethod
@@ -91,20 +91,30 @@ class BizCraneService:
         """删除"""
         if len(ids) < 1:
             raise CustomException(msg='删除失败,删除对象不能为空')
+        crane_nos = []
         for id in ids:
             obj = await BizCraneCRUD(auth).get_by_id_crane_crud(id=id)
             if not obj:
                 raise CustomException(msg=f'删除失败,ID为{id}的数据不存在')
+            crane_nos.append(obj.crane_no)
         await BizCraneCRUD(auth).delete_crane_crud(ids=ids)
         # 更新缓存中数据
-        await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:*")
+        for crane_no in crane_nos:
+            await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:{crane_no}")
     
     @classmethod
     async def set_available_crane_service(cls, auth: AuthSchema, data: BatchSetAvailable,redis: Redis) -> None:
+        crane_nos = []
+        for id in data.ids:
+            obj = await BizCraneCRUD(auth).get_by_id_crane_crud(id=id)
+            if not obj:
+                raise CustomException(msg=f'批量操作失败,ID为{id}的数据不存在')
+            crane_nos.append(obj.crane_no)
         """批量设置状态"""
         await BizCraneCRUD(auth).set_available_crane_crud(ids=data.ids, status=data.status)
         # 更新缓存中数据
-        await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:*")
+        for crane_no in crane_nos:
+            await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:{crane_no}")
     
     @classmethod
     async def batch_export_crane_service(cls, obj_list: list[dict]) -> bytes:

+ 14 - 4
backend/app/api/v1/module_business/mec/service.py

@@ -73,7 +73,7 @@ class BizMecService:
         obj = await BizMecCRUD(auth).create_mec_crud(data=data)
         if obj:
             # 更新缓存中数据
-            await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:*")
+            await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:{data.crane_no}")
         return BizMecOutSchema.model_validate(obj).model_dump()
     
     @classmethod
@@ -89,7 +89,7 @@ class BizMecService:
         obj = await BizMecCRUD(auth).update_mec_crud(id=id, data=data)
         if obj:
             # 更新缓存中数据
-            await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:*")
+            await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:{obj.crane_no}")
         return BizMecOutSchema.model_validate(obj).model_dump()
     
     @classmethod
@@ -97,20 +97,30 @@ class BizMecService:
         """删除"""
         if len(ids) < 1:
             raise CustomException(msg='删除失败,删除对象不能为空')
+        crane_nos = []
         for id in ids:
             obj = await BizMecCRUD(auth).get_by_id_mec_crud(id=id)
             if not obj:
                 raise CustomException(msg=f'删除失败,ID为{id}的数据不存在')
+            crane_nos.append(obj.crane_no)
         await BizMecCRUD(auth).delete_mec_crud(ids=ids)
         # 更新缓存中数据
-        await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:*")
+        for crane_no in crane_nos:
+            await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:{crane_no}")
     
     @classmethod
     async def set_available_mec_service(cls, auth: AuthSchema, data: BatchSetAvailable,redis: Redis) -> None:
+        crane_nos = []
+        for id in data.ids:
+            obj = await BizMecCRUD(auth).get_by_id_mec_crud(id=id)
+            if not obj:
+                raise CustomException(msg=f'批量设置失败,ID为{id}的数据不存在')
+            crane_nos.append(obj.crane_no)
         """批量设置状态"""
         await BizMecCRUD(auth).set_available_mec_crud(ids=data.ids, status=data.status)
         # 更新缓存中数据
-        await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:*")
+        for crane_no in crane_nos:
+            await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:{crane_no}")
     
     @classmethod
     async def batch_export_mec_service(cls, obj_list: list[dict]) -> bytes:

+ 109 - 42
backend/app/api/v1/module_business/vardict/controller.py

@@ -14,6 +14,7 @@ from app.utils.common_util import bytes2file_response
 from app.core.logger import log
 from app.core.base_schema import BatchSetAvailable
 
+
 from .service import BizVarDictService
 from .schema import BizVarDictCreateSchema, BizVarDictUpdateSchema, BizVarDictQueryParam
 from ..crane.service import BizCraneService
@@ -140,22 +141,27 @@ async def get_vardict_list_alarms_controller(
 ) -> JSONResponse:
     if search.crane_no: #带crane_no说明是单台车报警直接查数据库即可
         result_dict = await BizVarDictService.vardict_alarms_list(auth=auth, crane_no=search.crane_no)
-        async with httpx.AsyncClient() as client:
-            response = await client.get(
-                url=settings.COLLECT_DATA_FULL,
-                params={},
-                timeout=2
-            )
-            if response.status_code == 200:
+        try:
+            async with httpx.AsyncClient() as client:
+                response = await client.get(
+                    url=settings.COLLECT_DATA_FULL,
+                    params={},
+                    timeout=2
+                )
+                # 捕获HTTP状态码非200的情况
+                response.raise_for_status()
+                # 捕获JSON解析异常
                 json_data = response.json()
                 if json_data['code'] == 200 and json_data.get('data'):
                     for item in result_dict:
                         item['value'] = False
                         crane_no = item['crane_no']
-                        alarm = json_data['data'].get(crane_no, {}).get('data', {}).get('alarm', {}).get(
+                        alarm = json_data.get('data').get(crane_no, {}).get('data', {}).get('alarm', {}).get(
                             item['var_code'])
                         if alarm:
                             item['value'] = alarm.get('value')
+        except Exception as e:
+            log.error(f"调用数据初始化接口获取报警列表时发生未知异常:{str(e)}", exc_info=True)
     else: #全部报警点位查缓存
         result_dict = await BizVarDictService.get_vardict_alarms_service(auth=auth, redis=redis)
         cranes = await BizCraneService.list_crane_service(auth=auth)
@@ -166,30 +172,33 @@ async def get_vardict_list_alarms_controller(
             crane_no = crane_dict.get('crane_no')
             if crane_no and isinstance(crane_no, str):
                 valid_crane_nos.add(crane_no)
-
-        json_data = None
-        async with httpx.AsyncClient() as client:
-            response = await client.get(
-                url=settings.COLLECT_DATA_FULL,
-                params={},
-                timeout=2
-            )
-            if response.status_code == 200:
+        try:
+            async with httpx.AsyncClient() as client:
+                response = await client.get(
+                    url=settings.COLLECT_DATA_FULL,
+                    params={},
+                    timeout=2
+                )
+                # 捕获HTTP状态码非200的情况
+                response.raise_for_status()
+                # 捕获JSON解析异常
                 json_data = response.json()
 
-        filtered_result = []
-        for item in result_dict:
-            item_crane_no = item.get('crane_no')
-            if item_crane_no not in valid_crane_nos:
-                continue
-            item['value'] = False  # 默认值
-            if json_data and json_data['code'] == 200 and json_data.get('data'):
-                alarm = json_data['data'].get(item_crane_no, {}).get('data', {}).get('alarm', {}).get(
-                    item.get('var_code'))
-                if alarm:
-                    item['value'] = alarm.get('value')
-            filtered_result.append(item)
-        result_dict = filtered_result
+            filtered_result = []
+            for item in result_dict:
+                item_crane_no = item.get('crane_no')
+                if item_crane_no not in valid_crane_nos:
+                    continue
+                item['value'] = False  # 默认值
+                if json_data and json_data['code'] == 200 and json_data.get('data'):
+                    alarm = json_data.get('data').get(item_crane_no, {}).get('data', {}).get('alarm', {}).get(
+                        item.get('var_code'))
+                    if alarm:
+                        item['value'] = alarm.get('value')
+                filtered_result.append(item)
+            result_dict = filtered_result
+        except Exception as e:
+            log.error(f"调用数据初始化接口获取报警列表时发生未知异常:{str(e)}", exc_info=True)
     log.info("查询变量信息列表成功")
     return SuccessResponse(data=result_dict, msg="查询变量信息列表成功")
 @BizVarDictRouter.get("/list_analog", summary="查询变量信息列表", description="查询变量信息列表")
@@ -216,24 +225,82 @@ async def get_vardict_mec_group_controller(
         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:
+    try:
+        async with httpx.AsyncClient() as client:
+            response = await client.get(
+                url=settings.COLLECT_DATA_FULL,
+                params={},
+                timeout=2
+            )
+            # 捕获HTTP状态码非200的情况
+            response.raise_for_status()
+            # 捕获JSON解析异常
             json_data = response.json()
-            if json_data['code'] == 200 and json_data['data']:
+            if json_data['code'] == 200 and json_data.get('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():
+                    for key, inner_dict in var_dict.items():
                         if key != 'mec_type' and key != 'alarm_varList' and key != 'mecVarList_simple':
                             for item in inner_dict:
                                 if key == 'digital_varList':
-                                    item['value'] = json_digital.get(item.get('var_code')).get('value')
+                                    if json_digital:
+                                        item['value'] = json_digital.get(item.get('var_code')).get('value')
                                 else:
-                                    item['value'] = json_analog.get(item.get('var_code')).get('value')
+                                    if json_analog:
+                                        item['value'] = json_analog.get(item.get('var_code')).get('value')
+    except Exception as e:
+        log.error(f"调用采集接口获取分组变量状态时发生未知异常:{str(e)}", exc_info=True)
     log.info(f"获取变量信息分组数据成功:{result_dict}")
-    return SuccessResponse(data=result_dict, msg="获取变量分组数据成功")
+    return SuccessResponse(data=result_dict, msg="获取变量分组数据成功")
+
+@BizVarDictRouter.get("/historyData", summary="查询历史数据列表", description="查询历史数据列表")
+async def get_vardict_historyData_controller(
+    page: PaginationQueryParam = Depends(),
+    search: BizVarDictQueryParam = Depends(),
+    auth: AuthSchema = Depends(AuthPermission(["module_business:vardict:query"]))
+) -> JSONResponse:
+    search.data_type = ('!=', 1)
+    result_dict = await BizVarDictService.get_tdengine_data(
+        search=search,
+        stable_name='st_analog',
+        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,
+        auth=auth
+    )
+    log.info("查询历史数据列表成功")
+    return SuccessResponse(data=result_dict, msg="查询历史数据列表成功")
+
+@BizVarDictRouter.get("/operationRecord", summary="查询操作记录列表", description="查询操作记录列表")
+async def get_vardict_operationRecord_controller(
+    page: PaginationQueryParam = Depends(),
+    search: BizVarDictQueryParam = Depends(),
+    auth: AuthSchema = Depends(AuthPermission(["module_business:vardict:query"]))
+) -> JSONResponse:
+
+    result_dict = await BizVarDictService.get_tdengine_data(
+        search=search,
+        stable_name='st_digital',
+        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,
+        auth=auth
+    )
+    log.info("查询操作记录列表成功")
+    return SuccessResponse(data=result_dict, msg="查询操作记录列表成功")
+
+@BizVarDictRouter.get("/historyAlarm", summary="查询报警记录列表", description="查询报警记录列表")
+async def get_vardict_historyAlarm_controller(
+    page: PaginationQueryParam = Depends(),
+    search: BizVarDictQueryParam = Depends(),
+    auth: AuthSchema = Depends(AuthPermission(["module_business:vardict:query"]))
+) -> JSONResponse:
+
+    result_dict = await BizVarDictService.get_tdengine_data(
+        search=search,
+        stable_name='st_alarm',
+        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,
+        auth=auth
+    )
+    log.info("查询报警记录列表成功")
+    return SuccessResponse(data=result_dict, msg="查询报警记录列表成功")

+ 84 - 11
backend/app/api/v1/module_business/vardict/service.py

@@ -1,7 +1,8 @@
 # -*- coding: utf-8 -*-
-
+import asyncio
 import io
 import json
+from datetime import datetime
 from typing import Any
 
 from fastapi import UploadFile
@@ -24,6 +25,7 @@ from ..gateway.model import GatewayModel
 from ..mec.crud import BizMecCRUD
 from ..vardict.crud import BizVarDictCRUD
 from ..vardict.schema import VarDictMecGroupSchema
+from app.utils.tdengine_util import tdengine_rest_query, format_rest_result, get_table_total_count
 
 
 class BizVarDictService:
@@ -131,7 +133,7 @@ class BizVarDictService:
         obj = await BizVarDictCRUD(auth).create_vardict_crud(data=data)
         if obj:
             # 更新缓存中数据
-            await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:*")
+            await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:{data.crane_no}")
         return BizVarDictOutSchema.model_validate(obj).model_dump()
     
     @classmethod
@@ -147,7 +149,7 @@ class BizVarDictService:
         obj = await BizVarDictCRUD(auth).update_vardict_crud(id=id, data=data)
         if obj:
             # 更新缓存中数据
-            await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:*")
+            await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:{obj.crane_no}")
         return BizVarDictOutSchema.model_validate(obj).model_dump()
     
     @classmethod
@@ -155,20 +157,30 @@ class BizVarDictService:
         """删除"""
         if len(ids) < 1:
             raise CustomException(msg='删除失败,删除对象不能为空')
+        crane_nos = []
         for id in ids:
             obj = await BizVarDictCRUD(auth).get_by_id_vardict_crud(id=id)
             if not obj:
                 raise CustomException(msg=f'删除失败,ID为{id}的数据不存在')
+            crane_nos.append(obj.crane_no)
         await BizVarDictCRUD(auth).delete_vardict_crud(ids=ids)
         # 更新缓存中数据
-        await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:*")
+        for crane_no in crane_nos:
+            await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:{crane_no}")
     
     @classmethod
-    async def set_available_vardict_service(cls, auth: AuthSchema, data: BatchSetAvailable,redis: Redis) -> None:
+    async def set_availale_vardict_service(cls, auth: AuthSchema, data: BatchSetAvailable,redis: Redis) -> None:
+        crane_nos = []
+        for id in data.ids:
+            obj = await BizVarDictCRUD(auth).get_by_id_vardict_crud(id=id)
+            if not obj:
+                raise CustomException(msg=f'批量设置失败,ID为{id}的数据不存在')
+            crane_nos.append(obj.crane_no)
         """批量设置状态"""
         await BizVarDictCRUD(auth).set_available_vardict_crud(ids=data.ids, status=data.status)
         # 更新缓存中数据
-        await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:*")
+        for crane_no in crane_nos:
+            await RedisCURD(redis).clear(f"{RedisInitKeyConfig.VAR_DICT.key}:{crane_no}")
     
     @classmethod
     async def batch_export_vardict_service(cls, obj_list: list[dict]) -> bytes:
@@ -407,7 +419,7 @@ class BizVarDictService:
                     return obj_list_dict
 
             # 缓存不存在或格式错误时重新初始化
-            await cls.init_vardict_service(redis)
+            await cls.init_vardict_service(redis,crane_no=crane_no)
             obj_list_dict = await RedisCURD(redis).get(redis_key)
             if not obj_list_dict:
                 raise CustomException(msg="变量分组数据不存在")
@@ -470,7 +482,7 @@ class BizVarDictService:
             log.error(f"获取变量报警数据缓存失败: {str(e)}")
             raise CustomException(msg=f"获取变量报警数据失败: {str(e)}")
     @classmethod
-    async def init_vardict_service(cls, redis: Redis):
+    async def init_vardict_service(cls, redis: Redis,crane_no:str = None):
         """
         应用初始化: 获取所有天车变量数据信息并缓存service
 
@@ -486,7 +498,11 @@ class BizVarDictService:
                     # 在初始化过程中,不需要检查数据权限
                     auth = AuthSchema(db=session, check_data_scope=False)
                     #初始化行车机构分组变量数据
-                    crane_list = await BizCraneCRUD(auth).list(search={'status':'1'},order_by=[{'order':'asc'}])
+                    if crane_no:
+                        search = {'status':'1','crane_no':crane_no}
+                    else:
+                        search = {'status': '1'}
+                    crane_list = await BizCraneCRUD(auth).list(search=search,order_by=[{'order':'asc'}])
                     success_count = 0
                     fail_count = 0
                     for crane in crane_list:
@@ -529,7 +545,7 @@ class BizVarDictService:
                     log.info(f"机构分组变量数据初始化完成 - 成功: {success_count}, 失败: {fail_count}")
                     #初始化所有行车报警变量数据
                     try:
-                        varDicts = await cls.vardict_alarms_list(auth)
+                        varDicts = await cls.vardict_alarms_list(auth=auth)
                         redis_key = f"{RedisInitKeyConfig.VAR_DICT.key}:'alarms_all'"
                         value = json.dumps(varDicts, ensure_ascii=False)
                         await RedisCURD(redis).set(
@@ -542,4 +558,61 @@ class BizVarDictService:
         except Exception as e:
             log.error(f"变量数据初始化过程发生错误: {e}")
             # 只在严重错误时抛出异常,允许单个字典加载失败
-            raise CustomException(msg=f"变量数据初始化失败: {str(e)}")
+            raise CustomException(msg=f"变量数据初始化失败: {str(e)}")
+
+    @classmethod
+    async def get_tdengine_data(cls, auth: AuthSchema, page_no: int, page_size: int,stable_name:str,
+                               search: BizVarDictQueryParam | None = None) -> dict:
+        var_dict_search_dict = {'crane_no':search.crane_no,'data_type':search.data_type}
+        offset = (page_no - 1) * page_size
+        base_sql = "SELECT * FROM "+stable_name
+        filter_conditions = []
+        crane_no = search.crane_no
+        if crane_no and isinstance(crane_no, str) and crane_no.strip():
+            safe_crane_no = crane_no.strip().replace("'", "''")
+            filter_conditions.append(f"crane_no = '{safe_crane_no}'")
+
+        # 4. 过滤条件2:created_time时间范围(新增核心逻辑)
+        created_time = search.created_time
+        if created_time and isinstance(created_time, tuple) and len(created_time) == 2:
+            # 解析between条件:格式为('between', (start_time, end_time))
+            condition_type, time_range = created_time
+            if condition_type == "between" and isinstance(time_range, (list, tuple)) and len(time_range) == 2:
+                start_time, end_time = time_range
+                # 校验时间类型并格式化为TDengine支持的字符串
+                if isinstance(start_time, datetime) and isinstance(end_time, datetime):
+                    # 格式化时间为"YYYY-MM-DD HH:MM:SS"(匹配TDengine的时间格式)
+                    start_str = start_time.strftime("%Y-%m-%d %H:%M:%S")
+                    end_str = end_time.strftime("%Y-%m-%d %H:%M:%S")
+                    # 防SQL注入:转义单引号(虽然时间格式不会有,但做兜底)
+                    safe_start = start_str.replace("'", "''")
+                    safe_end = end_str.replace("'", "''")
+                    # 添加时间范围条件(TDengine的ts字段对应创建时间)
+                    filter_conditions.append(f"ts BETWEEN '{safe_start}' AND '{safe_end}'")
+
+        # 5. 拼接WHERE子句
+        where_clause = " WHERE " + " AND ".join(filter_conditions) if filter_conditions else ""
+
+        # 6. 构建完整查询SQL(排序+分页)
+        query_sql = f"{base_sql}{where_clause} ORDER BY ts DESC LIMIT {offset}, {page_size}"
+        rest_result = await tdengine_rest_query(query_sql)
+        formatted_data = await format_rest_result(rest_result)
+        #查找var_name
+        varDicts = await BizVarDictCRUD(auth).list(search=var_dict_search_dict)
+        if formatted_data:
+            for item in formatted_data:
+                normal_time = item.get('ts').replace('T', ' ').replace('+08:00', '')
+                item['ts'] = normal_time
+                for varDict in varDicts:
+                    if item.get('var_code') == varDict.var_code:
+                        item['var_name'] = varDict.var_name
+                        break
+        total = await get_table_total_count(stable_name, where_clause)
+
+        return {
+            "page_no": page_no,
+            "page_size": page_size,
+            "total": total,
+            "has_next": offset + page_size < total,
+            "items": formatted_data
+        }

+ 4 - 4
backend/app/api/v1/module_system/auth/service.py

@@ -64,10 +64,10 @@ class LoginService:
         request_from_docs = referer.endswith(('docs', 'redoc'))
         
         # 验证码校验
-        if settings.CAPTCHA_ENABLE and not request_from_docs:
-            if not login_form.captcha_key or not login_form.captcha:
-                raise CustomException(msg="验证码不能为空")
-            await CaptchaService.check_captcha_service(redis=redis, key=login_form.captcha_key, captcha=login_form.captcha)
+        # if settings.CAPTCHA_ENABLE and not request_from_docs:
+        #     if not login_form.captcha_key or not login_form.captcha:
+        #         raise CustomException(msg="验证码不能为空")
+        #     await CaptchaService.check_captcha_service(redis=redis, key=login_form.captcha_key, captcha=login_form.captcha)
 
         # 用户认证
         auth = AuthSchema(db=db)

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

@@ -35,7 +35,7 @@ class Settings(BaseSettings):
     # ******************* API文档配置 ****************** #
     # ================================================= #
     DEBUG: bool = True            # 调试模式
-    TITLE: str = "🎉 FastapiAdmin 🎉 -dev"  # 文档标题
+    TITLE: str = "🎉 恒达地面站管理系统 🎉 -dev"  # 文档标题
     VERSION: str = '0.1.0'        # 版本号
     DESCRIPTION: str = "该项目是一个基于python的web服务框架,基于fastapi和sqlalchemy实现。"  # 文档描述
     SUMMARY: str = "接口汇总"      # 文档概述
@@ -159,7 +159,10 @@ class Settings(BaseSettings):
     # ******************* 数据采集配置 ****************** #
     # ================================================= #
     COLLECT_DATA_FULL: str = ''
-
+    TDENGINE_HOST: str = ''
+    TDENGINE_PORT: str = ''
+    TDENGINE_USER: str = ''
+    TDENGINE_PWD: str = ''
     # ================================================= #
     # ******************* 请求限制配置 ****************** #
     # ================================================= #

+ 1 - 1
backend/app/core/base_params.py

@@ -9,7 +9,7 @@ class PaginationQueryParam:
     def __init__(
         self,
         page_no: int = Query(default=1, description="当前页码", ge=1),
-        page_size: int = Query(default=10, description="每页数量", ge=1, le=100), 
+        page_size: int = Query(default=10, description="每页数量", ge=1, le=1000),
         order_by: str | None = Query(default=None, description="排序字段,格式:field1,asc;field2,desc"),
     ) -> None:
         """

+ 1 - 1
backend/app/core/logger.py

@@ -72,7 +72,7 @@ def setup_logging():
     global _logger_handlers
     
     # 添加上下文信息
-    _ = logger.configure(extra={"app_name": "FastapiAdmin"})
+    _ = logger.configure(extra={"app_name": "恒达地面站管理系统"})
     # 步骤1:移除默认处理器
     logger.remove()
 

+ 65 - 0
backend/app/utils/tdengine_util.py

@@ -0,0 +1,65 @@
+# common.py
+import httpx
+import base64
+from fastapi import HTTPException
+
+from app.config.setting import settings
+
+# TDengine REST配置(跨平台通用)
+TDENGINE_CONFIG = {
+    "host": settings.TDENGINE_HOST,
+    "port": settings.TDENGINE_PORT,
+    "user": settings.TDENGINE_USER,
+    "password": settings.TDENGINE_PWD,
+    "database": "crane_data"
+}
+
+
+async def tdengine_rest_query(sql: str) -> dict:
+    """改用httpx的REST查询函数(跨平台)"""
+    rest_url = f"http://{TDENGINE_CONFIG['host']}:{TDENGINE_CONFIG['port']}/rest/sql/{TDENGINE_CONFIG['database']}?tz=Asia%2FShanghai"
+    auth_str = f"{TDENGINE_CONFIG['user']}:{TDENGINE_CONFIG['password']}"
+    auth_base64 = base64.b64encode(auth_str.encode("utf-8")).decode("utf-8")
+    headers = {
+        "Authorization": f"Basic {auth_base64}",
+        "Content-Type": "text/plain",
+        "TZ": "Asia/Shanghai"
+    }
+
+    try:
+        # 替换requests.post为httpx.post(同步调用)
+        response = httpx.post(rest_url, content=sql, headers=headers, timeout=30)
+        response.raise_for_status()
+
+        result = response.json()
+        if result.get("code") != 0:
+            raise Exception(f"SQL执行失败: {result.get('desc', '未知错误')}")
+
+        return result
+    except httpx.HTTPError as e:  # 替换requests.exceptions为httpx.HTTPError
+        raise HTTPException(status_code=500, detail=f"数据库连接失败: {str(e)}")
+    except Exception as e:
+        raise HTTPException(status_code=500, detail=f"查询失败: {str(e)}")
+
+
+async def format_rest_result(rest_result: dict) -> list[dict]:
+    """公共数据格式化函数(适配Element UI)"""
+    columns = [col[0] for col in rest_result["column_meta"]]
+    rows = rest_result["data"]
+
+    formatted_data = []
+    for row in rows:
+        row_dict = dict(zip(columns, row))
+        # 二进制数据转字符串
+        for key, value in row_dict.items():
+            if isinstance(value, bytes):
+                row_dict[key] = value.decode("utf-8", errors="ignore")
+        formatted_data.append(row_dict)
+    return formatted_data
+
+
+async def get_table_total_count(table_name: str, filter_sql: str = "") -> int:
+    """获取表的总条数(用于分页)"""
+    count_sql = f"SELECT COUNT(*) FROM {table_name} {filter_sql}"
+    count_result = await tdengine_rest_query(count_sql)
+    return count_result["data"][0][0] if count_result["data"] else 0

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

@@ -44,3 +44,7 @@ LOGGER_LEVEL = 'DEBUG'   # 日志级别
 
 # 数据采集服务地址
 COLLECT_DATA_FULL = http://192.168.0.247:5000/api/data/full
+TDENGINE_HOST = 192.168.0.247
+TDENGINE_PORT = 6041
+TDENGINE_USER = root
+TDENGINE_PWD = taosdata

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

@@ -44,3 +44,7 @@ LOGGER_LEVEL = 'INFO'
 
 # 数据采集服务地址
 COLLECT_DATA_FULL = http://localhost:5000/api/data/full
+TDENGINE_HOST = localhost
+TDENGINE_PORT = 6041
+TDENGINE_USER = root
+TDENGINE_PWD = taosdata

+ 3 - 1
frontend/package.json

@@ -90,7 +90,8 @@
     "vue-json-pretty": "^2.5.0",
     "vue-router": "^4.5.1",
     "vue3-cron-plus": "^0.1.9",
-    "vuedraggable": "^4.1.0"
+    "vuedraggable": "^4.1.0",
+    "xlsx": "^0.18.5"
   },
   "devDependencies": {
     "@eslint/js": "^9.32.0",
@@ -102,6 +103,7 @@
     "@types/nprogress": "^0.2.3",
     "@types/path-browserify": "^1.0.3",
     "@types/qs": "^6.14.0",
+    "@types/xlsx": "^0.0.36",
     "@typescript-eslint/eslint-plugin": "^8.38.0",
     "@typescript-eslint/parser": "^8.38.0",
     "@vitejs/plugin-vue": "^5.2.4",

+ 24 - 0
frontend/src/api/module_business/vardict.ts

@@ -109,6 +109,27 @@ const BizVarDictAPI = {
       headers: { "Content-Type": "multipart/form-data" },
     });
   },
+  historyData(query: BizVarDictPageQuery) {
+    return request<ApiResponse<PageResult<BizVarDictTable[]>>>({
+      url: `${API_PATH}/historyData`,
+      method: "get",
+      params: query,
+    });
+  },
+  operationRecord(query: BizVarDictPageQuery) {
+    return request<ApiResponse<PageResult<BizVarDictTable[]>>>({
+      url: `${API_PATH}/operationRecord`,
+      method: "get",
+      params: query,
+    });
+  },
+  historyAlarm(query: BizVarDictPageQuery) {
+    return request<ApiResponse<PageResult<BizVarDictTable[]>>>({
+      url: `${API_PATH}/historyAlarm`,
+      method: "get",
+      params: query,
+    });
+  },
 };
 
 export default BizVarDictAPI;
@@ -137,6 +158,7 @@ export interface BizVarDictPageQuery extends PageQuery {
   created_time?: string[];
   updated_time?: string[];
   order_by?:string;
+  data_type?:string;
 }
 
 // 列表展示项
@@ -170,6 +192,8 @@ export interface BizVarDictTable extends BaseType{
   created_by?: creatorType;
   updated_by?: updatorType;
   value?:string;
+  ts?:string;
+  val?:string;
 }
 
 // 新增/修改/详情表单参数

二進制
frontend/src/assets/images/button-bg.png


+ 128 - 0
frontend/src/components/base-search/index.vue

@@ -0,0 +1,128 @@
+<template>
+  <div class="search fx">
+    <div class="fx">
+      <div class="time" v-if="props.isShowDate">
+        <el-date-picker 
+          v-model="dateValue.arr"
+          type="datetimerange"
+          format="YYYY-MM-DD HH:mm:ss"
+          @change="onDateChange" 
+          range-separator="至"
+          start-placeholder="开始日期" 
+          end-placeholder="结束日期" />
+      </div>
+      <slot></slot>
+      <el-button :icon="Filter" style="margin-left: 15px;" type="primary" @click="onSearch">筛选</el-button>
+      <el-button type="primary" :icon="RefreshRight" @click="reset">重置</el-button>      
+    </div>
+    <div>
+      <el-button style="float: right;" :icon="Download" type="primary" @click="exportToExcel">导出</el-button>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { watchEffect, reactive, ref } from 'vue'
+import { Filter, RefreshRight, Download } from '@element-plus/icons-vue'
+let dateValue: any = reactive({
+  arr: []
+})
+const props = defineProps({
+  isShowDate: {
+    type: Boolean,
+    default: true
+  },
+  dateValue: {
+    type: Array,
+    default: () => ['', '']
+  }
+})
+watchEffect(() => {
+  dateValue.arr = props.dateValue
+})
+const onDateChange = (v: any) => {
+  if (v) {
+    emit('update-dateValue', [...v])
+  }
+
+  emit('dateChange', {
+    StartTime: dateValue.arr?.[0],
+    EndTime: dateValue.arr?.[1]
+  })
+}
+const emit = defineEmits(['on-search', 'clear', 'reset', 'update-dateValue', 'dateChange', 'exportToExcel'])
+const reset = () => {
+  dateValue.arr = []
+  emit('clear')
+  emit('reset')
+}
+const onSearch = () => {
+  if (props.isShowDate) {
+    emit('on-search', {
+      dateValue: dateValue.arr
+    })
+  }
+}
+
+const exportToExcel = () => {
+  emit('exportToExcel')
+}
+</script>
+
+<style lang="less" scoped>
+.search {
+  height: 50px;
+  width: 100%;
+
+  :deep(.el-button) {
+    height: 40px;
+    width: 100px;
+    font-size: 18px;
+    background-image: url('../../assets/images/button-bg.png');
+    border-radius: 5px;
+    border: 0px;
+  }
+
+}
+.fx {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+:deep(.time) {
+  margin-left: 25px;
+  min-width: 530px;
+  max-width: 530px;
+  --el-font-size-base: 18px;
+  --el-fill-color-blank: #121F34;
+  --el-border-color: #2353AE;
+  --el-text-color-regular: #ffffff;
+  --el-text-color-primary: #ffffff;
+
+  .el-range-editor.el-input__wrapper {
+    height: 40px;
+    border-radius: 10px;
+    width: 100%;
+  }
+
+  .el-icon {
+    --el-text-color-placeholder: #5080da;
+    margin-right: 10px;
+  }
+}
+
+:deep(.input) {
+  .el-input__wrapper {
+    border-radius: 10px;
+  }
+}
+
+:deep(.el-select__wrapper) {
+  height: 40px;
+  font-size: 20px;
+  --el-text-color-regular: #ffffff;
+  --el-fill-color-blank: #121F34;
+  --el-border-color: #2353AE;
+}
+</style>

+ 1 - 1
frontend/src/store/modules/user.store.ts

@@ -68,7 +68,7 @@ export const useUserStore = defineStore("user", {
 
       const permissionSet = new Set<string>();
       const collect = (items: MenuTable[]) => {
-        items.forEach((item) => {
+        items.forEach((item) => { 
           // 收集当前菜单的权限
           if (item.permission) {
             permissionSet.add(item.permission);

+ 9 - 2
frontend/src/utils/request.ts

@@ -9,6 +9,7 @@ import { useUserStoreHook } from "@/store/modules/user.store";
 import { ResultEnum } from "@/enums/api/result.enum";
 import { Auth } from "@/utils/auth";
 import router from "@/router";
+import { ElMessage, ElNotification } from "element-plus"; // 补充缺失的导入(如果没加的话)
 
 /**
  * 创建 HTTP 请求实例
@@ -17,7 +18,13 @@ const httpRequest: AxiosInstance = axios.create({
   baseURL: import.meta.env.VITE_APP_BASE_API,
   timeout: import.meta.env.VITE_TIMEOUT,
   headers: { "Content-Type": "application/json;charset=utf-8" },
-  paramsSerializer: (params) => qs.stringify(params),
+  // 核心修复:给实例的paramsSerializer添加arrayFormat: 'repeat'
+  paramsSerializer: (params) => {
+    return qs.stringify(params, { 
+      arrayFormat: 'repeat', // 数组转为重复键名(created_time=a&created_time=b)
+      encode: true // 自动编码特殊字符(如空格、冒号),无需手动处理
+    });
+  },
 });
 
 /**
@@ -151,4 +158,4 @@ async function redirectToLogin(message: string = "请重新登录"): Promise<voi
   }
 }
 
-export default httpRequest;
+export default httpRequest;

+ 1 - 0
frontend/src/views/module_business/crane/index.vue

@@ -696,6 +696,7 @@ async function handleRefresh() {
 async function loadingData() {
   loading.value = true;
   try {
+    console.log(queryFormData);
     const response = await BizCraneAPI.listBizCrane(queryFormData);
     pageTableData.value = response.data.data.items;
     total.value = response.data.data.total;

+ 10 - 10
frontend/src/views/module_system/auth/components/Login.vue

@@ -37,7 +37,7 @@
       </el-tooltip>
 
       <!-- 验证码 -->
-      <el-form-item v-if="captchaState.enable" prop="captcha">
+      <!-- <el-form-item v-if="captchaState.enable" prop="captcha">
         <div flex>
           <el-input
             v-model.trim="loginForm.captcha"
@@ -56,7 +56,7 @@
             <el-image v-else object-cover :src="captchaState.img_base" @click="getCaptcha" />
           </div>
         </div>
-      </el-form-item>
+      </el-form-item> -->
 
       <div class="flex-x-between w-full">
         <el-checkbox v-model="loginForm.remember">{{ t("login.rememberMe") }}</el-checkbox>
@@ -211,14 +211,14 @@ const loginRules = computed(() => {
 // 获取验证码
 const codeLoading = ref(false);
 async function getCaptcha() {
-  try {
-    codeLoading.value = true;
-    const response = await AuthAPI.getCaptcha();
-    loginForm.captcha_key = response.data.data.key;
-    captchaState.img_base = response.data.data.img_base;
-  } finally {
-    codeLoading.value = false;
-  }
+  // try {
+  //   codeLoading.value = true;
+  //   const response = await AuthAPI.getCaptcha();
+  //   loginForm.captcha_key = response.data.data.key;
+  //   captchaState.img_base = response.data.data.img_base;
+  // } finally {
+  //   codeLoading.value = false;
+  // }
 }
 
 /**

+ 171 - 92
frontend/src/views/web/detail/historyAlarm.vue

@@ -1,104 +1,183 @@
 <template>
-  <!-- <div>
+  <div>
     <div class="el-table-content">
-      <base-search :isShowSect="true" :sectName="'查询数量'" :sectValue="sectValue" :sectOption="sectOption" :isShowDate="true" :dateValue="date.arr"
+      <div>
+        <base-search :isShowDate="true" :dateValue="date.arr"
         @on-search="search" @reset="reset" @exportToExcel="exportToExcel"></base-search>
-      <pro-table :height="tabHeight" :loading="tab_loading" :data="allData" :config="tableConfig"></pro-table>
+      </div>
+      <div>
+        <pro-table :height="tabHeight" :loading="tab_loading" :data="allData" :config="tableConfig">
+          <template #default="{ row }">
+            <el-tag :type="row.val == '1' ? 'primary' : 'info'">
+              {{ row.val == '1' ? '触发' : '恢复' }}
+            </el-tag>
+          </template>
+        </pro-table>
+      </div>
+      <div style="height: 50px;display: flex;align-items: flex-end;justify-content: flex-end;">
+        <el-pagination
+          @current-change="handlePageChange"
+          @size-change="handleSizeChange"
+          :current-page="queryFormVarDictData.page_no"
+          :page-size="queryFormVarDictData.page_size"
+          :page-sizes="[20, 50, 100, 200]" 
+          :total="total"
+          layout="sizes,->,prev, next"
+          :disabled="total === 0"
+          background
+        >
+        </el-pagination>
+      </div>
     </div>
-  </div> -->
+  </div>
 </template>
 
-<script setup>
-// import { getOperationRecords } from '@/api/crane'
-// import * as XLSX from 'xlsx'
-// import dayjs from 'dayjs';
-// import { ref } from 'vue';
-// import { tableDataConvert } from '@/utils/hooks'
-// import { ElMessage } from 'element-plus'
+<script setup lang="ts">
+import { ref, reactive, onMounted } from 'vue';
+import BizVarDictAPI, { BizVarDictTable,BizVarDictPageQuery } from '@/api/module_business/vardict'
+import dayjs from 'dayjs';
+import * as XLSX from 'xlsx';
+import { formatToDateTime } from "@/utils/dateUtil";
 
-// const route = useRoute();
-// const sectValue = ref('500')  
-// const tabHeight = ref('calc(100vh - 70px - 5px - 50px - 10px - 44px - 50px)')
-// const tab_loading = ref(true)
-// const date = reactive({
-//   arr: [dayjs(dayjs().subtract(1, 'day')).format('YYYY/MM/DD HH:mm:ss'), dayjs().format('YYYY/MM/DD HH:mm:ss')]
-// })
-// const sectOption = [
-//   {
-//     value: '500',
-//     label: '500',
-//   },
-//   {
-//     value: '1000',
-//     label: '1000',
-//   },
-//   {
-//     value: '1500',
-//     label: '1500',
-//   },
-//   {
-//     value: '2000',
-//     label: '2000',
-//   },
-//   {
-//     value: '3000',
-//     label: '3000',
-//   },
-// ]
-// const allData = ref([]);
-// const tableConfig = ref([])
-// const getData = async () => {
-//   getOperationRecordsData()
-// }
-// const getOperationRecordsData = async () => {
-//   try {
-//     tab_loading.value = true
-//     let data = await getOperationRecords({
-//       craneNo: route.params.craneNo,
-//       startTime: date.arr[0],
-//       endTime: date.arr[1],
-//       count: sectValue.value,
-//       eventType:'3'
-//     })
-//     let res = tableDataConvert(data.Data);
-//     allData.value = res.data;
-//     tableConfig.value = res.tableConfig;
-//   } finally {
-//     tab_loading.value = false
-//   }
-// }
-// onMounted(async () => {
-//   getData();
-// })
-// const search = (v) => {
-//   date.arr[0] = v.dateValue[0]
-//   date.arr[1] = v.dateValue[1]
-//   sectValue.value = v.sectValue
-//   getData();
-// }
-// const reset = () => {
-//   sectValue.value = '500'
-//   date.arr = [dayjs(dayjs().subtract(1, 'day')).format('YYYY/MM/DD HH:mm:ss'), dayjs().format('YYYY/MM/DD HH:mm:ss')]
-//   getData();
-// }
-// const exportToExcel = () => {
-//   if (allData.value.length > 0) {
-//     const worksheet = XLSX.utils.json_to_sheet(allData.value)
-//     const workbook = XLSX.utils.book_new()
-//     XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
-//     XLSX.writeFile(workbook, '历史报警.xlsx')
-//   } else {
-//     ElMessage.error('暂无数据')
-//   }
+const tabHeight = ref('calc(100vh - 70px - 5px - 50px - 10px - 44px - 50px - 50px)')
+const tab_loading = ref(true)
+const allData = ref<BizVarDictTable[]>([]);
+const total = ref(0);
+const craneInfo = JSON.parse(localStorage.getItem('craneInfo') || '{}');
+const date = reactive({
+  arr: [dayjs(dayjs().subtract(1, 'day')).format('YYYY/MM/DD HH:mm:ss'), dayjs().format('YYYY/MM/DD HH:mm:ss')]
+})
+const queryFormVarDictData = reactive<BizVarDictPageQuery>({
+  page_no: 1,
+  page_size: 20,
+  crane_no: craneInfo.crane_no,
+  var_code: undefined,
+  var_name: undefined,
+  mec_type: undefined,
+  switch_type: undefined,
+  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,
+  data_type:'1'
+});
 
-// }
+const tableConfig = [
+  {
+    label: '点位名称',
+    prop: 'var_name'
+  },
+  {
+    label: '点位状态',
+    prop: 'val',
+    slot: true
+  },
+  {
+    label: '时间',
+    prop: 'ts'
+  }
+]
+
+const getData = async () => {
+  // 时间查询条件
+  queryFormVarDictData.created_time = [formatToDateTime(date.arr[0]), formatToDateTime(date.arr[1])]
+  let response = await BizVarDictAPI.historyAlarm(queryFormVarDictData);
+  allData.value = response.data.data.items
+  total.value = response.data.data.total
+  tab_loading.value = false
+};
+
+const handlePageChange = (newPage: number) => {
+  tab_loading.value = true
+  queryFormVarDictData.page_no = newPage;
+  getData(); // 页码变化后重新请求数据
+};
+
+const handleSizeChange = (newSize: number) => {
+  tab_loading.value = true
+  queryFormVarDictData.page_size = newSize; // 更新每页条数
+  queryFormVarDictData.page_no = 1; // 条数变化时重置为第1页(通用逻辑)
+  getData(); // 重新请求数据
+};
+
+const search = (v:any) => {
+  tab_loading.value = true
+  date.arr = v.dateValue
+  getData();
+}
+const reset = () => {
+  tab_loading.value = true
+  date.arr = [dayjs(dayjs().subtract(1, 'day')).format('YYYY-MM-DD HH:mm:ss'), dayjs().format('YYYY-MM-DD HH:mm:ss')]
+  getData();
+}
+const exportToExcel = () => {
+  // 1. 空数据校验
+  if (!allData.value || allData.value.length === 0) {
+    ElMessage.warning('暂无可导出的数据');
+    return;
+  }
+
+  try {
+    // 2. 从tableConfig中提取【导出字段】和【中文表头】(核心复用逻辑)
+    const exportFields = tableConfig.map(item => item.prop); // 提取字段名:['var_name', 'val', 'ts']
+    const chineseHeaders = tableConfig.map(item => item.label); // 提取中文表头:['点位名称', '点位状态', '时间']
+
+    // 3. 处理原始数据:只保留tableConfig中配置的字段,空值兜底
+    const processedData = allData.value.map(item => {
+      const row = {};
+      exportFields.forEach(field => {
+        row[field] = item[field] ?? ''; // 空值替换为'',避免undefined
+        // 特殊字段格式化(比如时间字段ts,按需调整)
+        if (field === 'val') {
+          row[field] = item[field] == '1' ? '触发' : '恢复';
+        }
+      });
+      return row;
+    });
+
+    // 4. 生成Excel工作表(复用tableConfig的表头)
+    const worksheet = XLSX.utils.json_to_sheet(processedData, {
+      header: exportFields, // 匹配tableConfig的字段名
+      skipHeader: true, // 跳过默认的英文字段表头
+    });
+
+    // 5. 手动设置中文表头(从tableConfig取label)
+    chineseHeaders.forEach((header, index) => {
+      const cellRef = XLSX.utils.encode_cell({ r: 0, c: index }); // 0行=表头行,index=列索引
+      worksheet[cellRef] = { v: header, t: 's' }; // 设置表头内容为tableConfig的label
+    });
+
+    // 6. 生成工作簿并导出
+    const workbook = XLSX.utils.book_new();
+    XLSX.utils.book_append_sheet(workbook, worksheet, '报警记录'); // 自定义sheet名
+    // 文件名加时间戳,避免覆盖
+    const fileName = craneInfo.crane_name+`报警记录.xlsx`;
+    XLSX.writeFile(workbook, fileName);
+
+    ElMessage.success('Excel导出成功!');
+  } catch (error) {
+    ElMessage.error(`导出失败:${error.message}`);
+  }
+};
+
+onMounted(async () => {
+  await getData();
+});
 </script>
 
 <style lang="less" scoped>
-// .el-table-content {
-//   padding: 20px;
-//   background: linear-gradient(to bottom, rgba(20, 66, 140, 0.8) 0%, rgba(18, 31, 52, 1) 15%);
-//   border: 2px solid rgba(40, 73, 136, 1);
-//   height: 100%;
-// }
+.el-table-content {
+  padding: 20px;
+  background: linear-gradient(to bottom, rgba(20, 66, 140, 0.8) 0%, rgba(18, 31, 52, 1) 15%);
+  border: 2px solid rgba(40, 73, 136, 1);
+  height: 100%;
+}
 </style>

+ 160 - 91
frontend/src/views/web/detail/historyData.vue

@@ -1,103 +1,172 @@
 <template>
-  <!-- <div>
+  <div>
     <div class="el-table-content">
-      <base-search :isShowSect="true" :sectName="'查询数量'" :sectValue="sectValue" :sectOption="sectOption" :isShowDate="true" :dateValue="date.arr"
+      <div>
+        <base-search :isShowDate="true" :dateValue="date.arr"
         @on-search="search" @reset="reset" @exportToExcel="exportToExcel"></base-search>
-      <pro-table :height="tabHeight" :loading="tab_loading" :data="allData" :config="tableConfig"></pro-table>
+      </div>
+      <div>
+        <pro-table :height="tabHeight" :loading="tab_loading" :data="allData" :config="tableConfig"></pro-table>
+      </div>
+      <div style="height: 50px;display: flex;align-items: flex-end;justify-content: flex-end;">
+        <el-pagination
+          @current-change="handlePageChange"
+          @size-change="handleSizeChange"
+          :current-page="queryFormVarDictData.page_no"
+          :page-size="queryFormVarDictData.page_size"
+          :page-sizes="[20, 50, 100, 200]" 
+          :total="total"
+          layout="sizes,->,prev, next"
+          :disabled="total === 0"
+          background
+        >
+        </el-pagination>
+      </div>
     </div>
-  </div> -->
+  </div>
 </template>
 
-<script setup>
-// import { getAll } from '@/api/crane'
-// import * as XLSX from 'xlsx'
-// import dayjs from 'dayjs';
-// import { ref } from 'vue';
-// import { tableDataConvert } from '@/utils/hooks'
-// import { ElMessage } from 'element-plus'
+<script setup lang="ts">
+import { ref, reactive, onMounted } from 'vue';
+import BizVarDictAPI, { BizVarDictTable,BizVarDictPageQuery } from '@/api/module_business/vardict'
+import dayjs from 'dayjs';
+import * as XLSX from 'xlsx';
+import { formatToDateTime } from "@/utils/dateUtil";
 
-// const route = useRoute();
-// const sectValue = ref('500')
-// const tabHeight = ref('calc(100vh - 70px - 5px - 50px - 10px - 44px - 50px)')
-// const tab_loading = ref(true)
-// const date = reactive({
-//   arr: [dayjs(dayjs().subtract(1, 'day')).format('YYYY/MM/DD HH:mm:ss'), dayjs().format('YYYY/MM/DD HH:mm:ss')]
-// })
-// const sectOption = [
-//   {
-//     value: '500',
-//     label: '500',
-//   },
-//   {
-//     value: '1000',
-//     label: '1000',
-//   },
-//   {
-//     value: '1500',
-//     label: '1500',
-//   },
-//   {
-//     value: '2000',
-//     label: '2000',
-//   },
-//   {
-//     value: '3000',
-//     label: '3000',
-//   },
-// ]
-// const allData = ref([]);
-// const tableConfig = ref([])
-// const getData = async () => {
-//   getAllData()
-// }
-// const getAllData = async () => {
-//   try {
-//     tab_loading.value = true
-//     let data = await getAll({
-//       craneNo: route.params.craneNo,
-//       startTime: date.arr[0],
-//       endTime: date.arr[1],
-//       count: sectValue.value
-//     })
-//     let res = tableDataConvert(data.Data);
-//     allData.value = res.data;
-//     tableConfig.value = res.tableConfig;
-//   } finally {
-//     tab_loading.value = false
-//   }
-// }
-// onMounted(async () => {
-//   getData();
-// })
-// const search = (v) => {
-//   date.arr[0] = v.dateValue[0]
-//   date.arr[1] = v.dateValue[1]
-//   sectValue.value = v.sectValue
-//   getData();
-// }
-// const reset = () => {
-//   sectValue.value = '500'
-//   date.arr = [dayjs(dayjs().subtract(1, 'day')).format('YYYY/MM/DD HH:mm:ss'), dayjs().format('YYYY/MM/DD HH:mm:ss')]
-//   getData();
-// }
-// const exportToExcel = () => {
-//   if (allData.value.length > 0) {
-//     const worksheet = XLSX.utils.json_to_sheet(allData.value)
-//     const workbook = XLSX.utils.book_new()
-//     XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
-//     XLSX.writeFile(workbook, '历史数据.xlsx')
-//   } else {
-//     ElMessage.error('暂无数据')
-//   }
+const tabHeight = ref('calc(100vh - 70px - 5px - 50px - 10px - 44px - 50px - 50px)')
+const tab_loading = ref(true)
+const allData = ref<BizVarDictTable[]>([]);
+const total = ref(0);
+const craneInfo = JSON.parse(localStorage.getItem('craneInfo') || '{}');
+const date = reactive({
+  arr: [dayjs(dayjs().subtract(1, 'day')).format('YYYY/MM/DD HH:mm:ss'), dayjs().format('YYYY/MM/DD HH:mm:ss')]
+})
+const queryFormVarDictData = reactive<BizVarDictPageQuery>({
+  page_no: 1,
+  page_size: 20,
+  crane_no: craneInfo.crane_no,
+  var_code: undefined,
+  var_name: undefined,
+  mec_type: undefined,
+  switch_type: undefined,
+  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,
+  data_type:'1'
+});
 
-// }
+const tableConfig = [
+  {
+    label: '点位名称',
+    prop: 'var_name'
+  },
+  {
+    label: '点位数据',
+    prop: 'val'
+  },
+  {
+    label: '时间',
+    prop: 'ts'
+  }
+]
+
+const getData = async () => {
+  // 时间查询条件
+  queryFormVarDictData.created_time = [formatToDateTime(date.arr[0]), formatToDateTime(date.arr[1])]
+  let response = await BizVarDictAPI.historyData(queryFormVarDictData);
+  allData.value = response.data.data.items
+  total.value = response.data.data.total
+  tab_loading.value = false
+};
+
+const handlePageChange = (newPage: number) => {
+  tab_loading.value = true
+  queryFormVarDictData.page_no = newPage;
+  getData(); // 页码变化后重新请求数据
+};
+
+const handleSizeChange = (newSize: number) => {
+  tab_loading.value = true
+  queryFormVarDictData.page_size = newSize; // 更新每页条数
+  queryFormVarDictData.page_no = 1; // 条数变化时重置为第1页(通用逻辑)
+  getData(); // 重新请求数据
+};
+
+const search = (v:any) => {
+  tab_loading.value = true
+  date.arr = v.dateValue
+  getData();
+}
+const reset = () => {
+  tab_loading.value = true
+  date.arr = [dayjs(dayjs().subtract(1, 'day')).format('YYYY-MM-DD HH:mm:ss'), dayjs().format('YYYY-MM-DD HH:mm:ss')]
+  getData();
+}
+const exportToExcel = () => {
+  // 1. 空数据校验
+  if (!allData.value || allData.value.length === 0) {
+    ElMessage.warning('暂无可导出的数据');
+    return;
+  }
+
+  try {
+    // 2. 从tableConfig中提取【导出字段】和【中文表头】(核心复用逻辑)
+    const exportFields = tableConfig.map(item => item.prop); // 提取字段名:['var_name', 'val', 'ts']
+    const chineseHeaders = tableConfig.map(item => item.label); // 提取中文表头:['点位名称', '点位状态', '时间']
+
+    // 3. 处理原始数据:只保留tableConfig中配置的字段,空值兜底
+    const processedData = allData.value.map(item => {
+      const row = {};
+      exportFields.forEach(field => {
+        row[field] = item[field] ?? ''; // 空值替换为'',避免undefined
+      });
+      return row;
+    });
+
+    // 4. 生成Excel工作表(复用tableConfig的表头)
+    const worksheet = XLSX.utils.json_to_sheet(processedData, {
+      header: exportFields, // 匹配tableConfig的字段名
+      skipHeader: true, // 跳过默认的英文字段表头
+    });
+
+    // 5. 手动设置中文表头(从tableConfig取label)
+    chineseHeaders.forEach((header, index) => {
+      const cellRef = XLSX.utils.encode_cell({ r: 0, c: index }); // 0行=表头行,index=列索引
+      worksheet[cellRef] = { v: header, t: 's' }; // 设置表头内容为tableConfig的label
+    });
+
+    // 6. 生成工作簿并导出
+    const workbook = XLSX.utils.book_new();
+    XLSX.utils.book_append_sheet(workbook, worksheet, '历史数据'); // 自定义sheet名
+    // 文件名加时间戳,避免覆盖
+    const fileName = craneInfo.crane_name+`历史数据.xlsx`;
+    XLSX.writeFile(workbook, fileName);
+
+    ElMessage.success('Excel导出成功!');
+  } catch (error) {
+    ElMessage.error(`导出失败:${error.message}`);
+  }
+};
+
+onMounted(async () => {
+  await getData();
+});
 </script>
 
 <style lang="less" scoped>
-// .el-table-content {
-//   padding: 20px;
-//   background: linear-gradient(to bottom, rgba(20, 66, 140, 0.8) 0%, rgba(18, 31, 52, 1) 15%);
-//   border: 2px solid rgba(40, 73, 136, 1);
-//   height: 100%;
-// }
+.el-table-content {
+  padding: 20px;
+  background: linear-gradient(to bottom, rgba(20, 66, 140, 0.8) 0%, rgba(18, 31, 52, 1) 15%);
+  border: 2px solid rgba(40, 73, 136, 1);
+  height: 100%;
+}
 </style>

+ 167 - 88
frontend/src/views/web/detail/operationRecord.vue

@@ -1,101 +1,180 @@
 <template>
-  <!-- <div>
-    <div class="historydata-content">
-      <base-search :isShowSect="true" :sectName="'查询数量'" :sectValue="sectValue" :sectOption="sectOption" :isShowDate="true" :dateValue="date.arr"
+  <div>
+    <div class="el-table-content">
+      <div>
+        <base-search :isShowDate="true" :dateValue="date.arr"
         @on-search="search" @reset="reset" @exportToExcel="exportToExcel"></base-search>
-      <pro-table :height="tabHeight" :loading="tab_loading" :data="allData" :config="tableConfig"></pro-table>
+      </div>
+      <div>
+        <pro-table :height="tabHeight" :loading="tab_loading" :data="allData" :config="tableConfig">
+          <template #default="{ row }">
+            <el-tag :type="row.val == '1' ? 'primary' : 'info'">
+              {{ row.val == '1' ? '触发' : '恢复' }}
+            </el-tag>
+          </template>
+        </pro-table>
+      </div>
+      <div style="height: 50px;display: flex;align-items: flex-end;justify-content: flex-end;">
+        <el-pagination
+          @current-change="handlePageChange"
+          @size-change="handleSizeChange"
+          :current-page="queryFormVarDictData.page_no"
+          :page-size="queryFormVarDictData.page_size"
+          :page-sizes="[20, 50, 100, 200]" 
+          :total="total"
+          layout="sizes,->,prev, next"
+          :disabled="total === 0"
+          background
+        >
+        </el-pagination>
+      </div>
     </div>
-  </div> -->
+  </div>
 </template>
 
-<script setup>
-// import { getOperationRecords } from '@/api/crane'
-// import * as XLSX from 'xlsx'
-// import dayjs from 'dayjs';
-// import { ref } from 'vue';
-// import { tableDataConvert } from '@/utils/hooks'
-// import { ElMessage } from 'element-plus'
+<script setup lang="ts">
+import { ref, reactive, onMounted } from 'vue';
+import BizVarDictAPI, { BizVarDictTable,BizVarDictPageQuery } from '@/api/module_business/vardict'
+import dayjs from 'dayjs';
+import * as XLSX from 'xlsx';
+import { formatToDateTime } from "@/utils/dateUtil";
 
-// const route = useRoute();
-// const sectValue = ref('500')
-// const tabHeight = ref('calc(100vh - 70px - 5px - 50px - 10px - 44px - 50px)')
-// const tab_loading = ref(true)
-// const date = reactive({
-//   arr: [dayjs(dayjs().subtract(1, 'day')).format('YYYY/MM/DD HH:mm:ss'), dayjs().format('YYYY/MM/DD HH:mm:ss')]
-// })
-// const sectOption = [
-//   {
-//     value: '500',
-//     label: '500',
-//   },
-//   {
-//     value: '1000',
-//     label: '1000',
-//   },
-//   {
-//     value: '1500',
-//     label: '1500',
-//   },
-//   {
-//     value: '2000',
-//     label: '2000',
-//   },
-//   {
-//     value: '3000',
-//     label: '3000',
-//   },
-// ]
-// const allData = ref([]);
-// const tableConfig = ref([])
-// const getData = async () => {
-//   getOperationRecordsData()
-// }
-// const getOperationRecordsData = async () => {
-//   try {
-//     tab_loading.value = true
-//     let data = await getOperationRecords({
-//       craneNo: route.params.craneNo,
-//       startTime: date.arr[0],
-//       endTime: date.arr[1],
-//       count: sectValue.value,
-//       eventType:'1'
-//     })
-//     let res = tableDataConvert(data.Data);
-//     allData.value = res.data;
-//     tableConfig.value = res.tableConfig;
-//   } finally {
-//     tab_loading.value = false
-//   }
-// }
-// onMounted(async () => {
-//   getData();
-// })
-// const search = (v) => {
-//   date.arr[0] = v.dateValue[0]
-//   date.arr[1] = v.dateValue[1]
-//   sectValue.value = v.sectValue
-//   getData();
-// }
-// const reset = () => {
-//   sectValue.value = '500'
-//   date.arr = [dayjs(dayjs().subtract(1, 'day')).format('YYYY/MM/DD HH:mm:ss'), dayjs().format('YYYY/MM/DD HH:mm:ss')]
-//   getData();
-// }
-// const exportToExcel = () => {
-//   if (allData.value.length > 0) {
-//     const worksheet = XLSX.utils.json_to_sheet(allData.value)
-//     const workbook = XLSX.utils.book_new()
-//     XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
-//     XLSX.writeFile(workbook, '操作记录.xlsx')
-//   } else {
-//     ElMessage.error('暂无数据')
-//   }
+const tabHeight = ref('calc(100vh - 70px - 5px - 50px - 10px - 44px - 50px - 50px)')
+const tab_loading = ref(true)
+const allData = ref<BizVarDictTable[]>([]);
+const total = ref(0);
+const craneInfo = JSON.parse(localStorage.getItem('craneInfo') || '{}');
+const date = reactive({
+  arr: [dayjs(dayjs().subtract(1, 'day')).format('YYYY/MM/DD HH:mm:ss'), dayjs().format('YYYY/MM/DD HH:mm:ss')]
+})
+const queryFormVarDictData = reactive<BizVarDictPageQuery>({
+  page_no: 1,
+  page_size: 20,
+  crane_no: craneInfo.crane_no,
+  var_code: undefined,
+  var_name: undefined,
+  mec_type: undefined,
+  switch_type: undefined,
+  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,
+  data_type:'1'
+});
 
-// }
+const tableConfig = [
+  {
+    label: '点位名称',
+    prop: 'var_name'
+  },
+  {
+    label: '点位状态',
+    prop: 'val',
+    slot: true
+  },
+  {
+    label: '时间',
+    prop: 'ts'
+  }
+]
+
+const getData = async () => {
+  // 时间查询条件
+  queryFormVarDictData.created_time = [formatToDateTime(date.arr[0]), formatToDateTime(date.arr[1])]
+  let response = await BizVarDictAPI.operationRecord(queryFormVarDictData);
+  allData.value = response.data.data.items
+  total.value = response.data.data.total
+  tab_loading.value = false
+};
+
+const handlePageChange = (newPage: number) => {
+  tab_loading.value = true
+  queryFormVarDictData.page_no = newPage;
+  getData(); // 页码变化后重新请求数据
+};
+
+const handleSizeChange = (newSize: number) => {
+  tab_loading.value = true
+  queryFormVarDictData.page_size = newSize; // 更新每页条数
+  queryFormVarDictData.page_no = 1; // 条数变化时重置为第1页(通用逻辑)
+  getData(); // 重新请求数据
+};
+
+const search = (v:any) => {
+  tab_loading.value = true
+  date.arr = v.dateValue
+  getData();
+}
+const reset = () => {
+  tab_loading.value = true
+  date.arr = [dayjs(dayjs().subtract(1, 'day')).format('YYYY-MM-DD HH:mm:ss'), dayjs().format('YYYY-MM-DD HH:mm:ss')]
+  getData();
+}
+const exportToExcel = () => {
+  // 1. 空数据校验
+  if (!allData.value || allData.value.length === 0) {
+    ElMessage.warning('暂无可导出的数据');
+    return;
+  }
+
+  try {
+    // 2. 从tableConfig中提取【导出字段】和【中文表头】(核心复用逻辑)
+    const exportFields = tableConfig.map(item => item.prop); // 提取字段名:['var_name', 'val', 'ts']
+    const chineseHeaders = tableConfig.map(item => item.label); // 提取中文表头:['点位名称', '点位状态', '时间']
+
+    // 3. 处理原始数据:只保留tableConfig中配置的字段,空值兜底
+    const processedData = allData.value.map(item => {
+      const row = {};
+      exportFields.forEach(field => {
+        row[field] = item[field] ?? ''; // 空值替换为'',避免undefined
+        // 特殊字段格式化(比如时间字段ts,按需调整)
+        if (field === 'val') {
+          row[field] = item[field] == '1' ? '触发' : '恢复';
+        }
+      });
+      return row;
+    });
+
+    // 4. 生成Excel工作表(复用tableConfig的表头)
+    const worksheet = XLSX.utils.json_to_sheet(processedData, {
+      header: exportFields, // 匹配tableConfig的字段名
+      skipHeader: true, // 跳过默认的英文字段表头
+    });
+
+    // 5. 手动设置中文表头(从tableConfig取label)
+    chineseHeaders.forEach((header, index) => {
+      const cellRef = XLSX.utils.encode_cell({ r: 0, c: index }); // 0行=表头行,index=列索引
+      worksheet[cellRef] = { v: header, t: 's' }; // 设置表头内容为tableConfig的label
+    });
+
+    // 6. 生成工作簿并导出
+    const workbook = XLSX.utils.book_new();
+    XLSX.utils.book_append_sheet(workbook, worksheet, '操作记录'); // 自定义sheet名
+    // 文件名加时间戳,避免覆盖
+    const fileName = craneInfo.crane_name+`操作记录.xlsx`;
+    XLSX.writeFile(workbook, fileName);
+
+    ElMessage.success('Excel导出成功!');
+  } catch (error) {
+    ElMessage.error(`导出失败:${error.message}`);
+  }
+};
+
+onMounted(async () => {
+  await getData();
+});
 </script>
 
 <style lang="less" scoped>
-.historydata-content {
+.el-table-content {
   padding: 20px;
   background: linear-gradient(to bottom, rgba(20, 66, 140, 0.8) 0%, rgba(18, 31, 52, 1) 15%);
   border: 2px solid rgba(40, 73, 136, 1);

+ 4 - 5
frontend/src/views/web/detail/realtimeAlarm.vue

@@ -82,11 +82,10 @@ const handleMqttMessage: MqttMessageCallback = (topic, payload) => {
   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 targetItem = allData.value.find(
+      item => item.var_code === payload.var_code
+    )
+    if (targetItem) targetItem.value = payload.value
   }
 }
 

+ 8 - 11
frontend/src/views/web/detail/realtimeData.vue

@@ -142,19 +142,16 @@ const handleMqttMessage: MqttMessageCallback = (topic, payload) => {
   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
-          }
-        })
+        const targetItem = payload.data.find(
+          (payloadItem: any) => numberItem.var_code === payloadItem.var_code
+        )
+        if (targetItem) numberItem.value = targetItem.value
       })
     }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;
-        }
-      })
+      const targetItem = item.bool_type_list.find(
+        (boolItem: any) => boolItem.var_code === var_code
+      )
+      if (targetItem) targetItem.value = payload.value
     }
   })
   //挡位计算

+ 8 - 10
frontend/src/views/web/overview/index.vue

@@ -216,17 +216,15 @@ const handleMqttMessage: MqttMessageCallback = (topic, payload) => {
   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
-      }
-    });
+    const targetItem = varDictData.value.find(
+      item => item.crane_no === crane_no && item.var_code === payload.var_code
+    );
+    if (targetItem) targetItem.value = payload.value;
   } else if (suffix === 'status') {
-    craneData.value.forEach((craneItem) => {
-      if (craneItem.crane_no === crane_no) {
-        craneItem.work_status = payload.status;
-      }
-    });
+    const targetCrane = craneData.value.find(
+      craneItem => craneItem.crane_no === crane_no
+    );
+    if (targetCrane) targetCrane.work_status = payload.status;
   }
 }