Просмотр исходного кода

操作记录和报警记录统计展示,所有页面增加点位选择条件和机构联动,报警总览增加跳转

cuiHe 4 дней назад
Родитель
Сommit
2209a5327e

+ 16 - 10
backend/app/api/v1/module_business/vardict/controller.py

@@ -140,7 +140,7 @@ async def get_vardict_list_alarms_controller(
         auth: AuthSchema = Depends(AuthPermission(["module_business:vardict:query"]))
 ) -> JSONResponse:
     if search.crane_no: #带crane_no说明是单台车报警直接查数据库即可
-        result_dict = await BizVarDictService.vardict_alarms_list(auth=auth, crane_no=search.crane_no)
+        result_dict = await BizVarDictService.vardict_alarms_list(auth=auth, search=search)
         try:
             async with httpx.AsyncClient() as client:
                 response = await client.get(
@@ -207,10 +207,20 @@ async def get_vardict_list_analog_controller(
     auth: AuthSchema = Depends(AuthPermission(["module_business:vardict:query"]))
 ) -> JSONResponse:
     """查询变量信息列表接口(数据库分页)"""
-    result_dict = await BizVarDictService.vardict_analog_list(auth=auth,crane_no=search.crane_no)
+    result_dict = await BizVarDictService.vardict_analog_list(auth=auth,search=search)
     log.info("查询变量信息列表成功")
     return SuccessResponse(data=result_dict, msg="查询变量信息列表成功")
 
+@BizVarDictRouter.get("/list_operation_record", summary="查询操作点位变量信息列表", description="查询操作点位变量信息列表")
+async def get_vardict_list_operation_record_controller(
+    search: BizVarDictQueryParam = Depends(),
+    auth: AuthSchema = Depends(AuthPermission(["module_business:vardict:query"]))
+) -> JSONResponse:
+    """查询变量信息列表接口(数据库分页)"""
+    result_dict = await BizVarDictService.vardict_operation_record_list(auth=auth,search=search)
+    log.info("查询操作点位变量信息列表成功")
+    return SuccessResponse(data=result_dict, msg="查询操作点位变量信息列表成功")
+
 @BizVarDictRouter.get("/varDictMecGroup/{crane_no}", summary="获取变量信息分组数据", description="获取变量信息分组数据")
 async def get_vardict_mec_group_controller(
     crane_no: str = Path(..., description="crane_no"),
@@ -273,16 +283,15 @@ async def get_vardict_historyData_controller(
 
 @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.switch_type = ('<=',1)
+    result_dict = await BizVarDictService.get_tdengine_data_operation(
         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("查询操作记录列表成功")
@@ -290,16 +299,13 @@ async def get_vardict_operationRecord_controller(
 
 @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(
+    result_dict = await BizVarDictService.get_tdengine_data_operation(
         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("查询报警记录列表成功")

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

@@ -2,8 +2,9 @@
 import asyncio
 import io
 import json
-from datetime import datetime
-from typing import Any
+from collections import defaultdict
+from datetime import datetime, timedelta
+from typing import Any, List, Dict, Optional
 
 from fastapi import UploadFile
 import pandas as pd
@@ -73,21 +74,53 @@ class BizVarDictService:
         return result
 
     @classmethod
-    async def vardict_alarms_list(cls, auth: AuthSchema, crane_no: str = None) -> list[dict]:
+    async def vardict_alarms_list(cls, auth: AuthSchema, search: BizVarDictQueryParam | None = None) -> list[dict]:
 
         sql_parts = [
             """SELECT a.*,b.crane_name 
             FROM biz_var_dict as a 
             LEFT JOIN biz_crane as b ON a.crane_no = b.crane_no 
-            WHERE a.`status` = :status AND b.`status` = :status AND a.switch_type >= 2"""
+            WHERE a.`status` = :status AND b.`status` = :status AND a.data_type <= 1 AND a.switch_type >= 2"""
         ]
 
         business_params: dict[str, Any] = {"status": 1}
+        if search and search.crane_no:
+            sql_parts.append(f"AND a.crane_no = :crane_no")
+            business_params["crane_no"] = search.crane_no
+        if search and search.mec_type:
+            sql_parts.append(f"AND a.mec_type = :mec_type")
+            business_params["mec_type"] = search.mec_type
+
+        sql_parts.append("ORDER BY a.switch_type desc,b.`order` asc,a.mec_type asc,a.var_sort asc")
+        final_sql = " ".join(sql_parts)
+
+        try:
+            obj_list = await BizVarDictCRUD(auth).list_sql(final_sql, business_params)
 
-        if crane_no and isinstance(crane_no, str) and crane_no.strip():
-            valid_crane_no = crane_no.strip()
+            return [BizVarDictOutSchema.model_validate(obj).model_dump() for obj in obj_list]
+        except Exception as e:
+            raise CustomException(msg=f"查询变量字典报警列表失败:{str(e)}")
+
+    @classmethod
+    async def vardict_operation_record_list(cls, auth: AuthSchema, search: BizVarDictQueryParam | None = None) -> list[dict]:
+
+        crane_no = search.crane_no
+        mec_type = search.mec_type
+        sql_parts = [
+            """SELECT a.*,b.crane_name 
+            FROM biz_var_dict as a 
+            LEFT JOIN biz_crane as b ON a.crane_no = b.crane_no 
+            WHERE a.`status` = :status AND b.`status` = :status AND a.data_type <= 1 AND a.switch_type <= 1"""
+        ]
+
+        business_params: dict[str, Any] = {"status": 1}
+
+        if crane_no:
             sql_parts.append(f"AND a.crane_no = :crane_no")
-            business_params["crane_no"] = valid_crane_no
+            business_params["crane_no"] = crane_no
+        if mec_type:
+            sql_parts.append(f"AND a.mec_type = :mec_type")
+            business_params["mec_type"] = mec_type
 
         sql_parts.append("ORDER BY b.`order` asc,a.mec_type asc,a.var_sort asc")
         final_sql = " ".join(sql_parts)
@@ -99,9 +132,11 @@ class BizVarDictService:
         except Exception as e:
             raise CustomException(msg=f"查询变量字典报警列表失败:{str(e)}")
 
-    @classmethod
-    async def vardict_analog_list(cls, auth: AuthSchema, crane_no: str = None) -> list[dict]:
 
+    @classmethod
+    async def vardict_analog_list(cls, auth: AuthSchema, search: BizVarDictQueryParam | None = None) -> list[dict]:
+        crane_no = search.crane_no
+        mec_type = search.mec_type
         sql_parts = [
             """SELECT a.*,b.crane_name 
             FROM biz_var_dict as a 
@@ -111,10 +146,12 @@ class BizVarDictService:
 
         business_params: dict[str, Any] = {"status": 1}
 
-        if crane_no and isinstance(crane_no, str) and crane_no.strip():
-            valid_crane_no = crane_no.strip()
+        if crane_no:
             sql_parts.append(f"AND a.crane_no = :crane_no")
-            business_params["crane_no"] = valid_crane_no
+            business_params["crane_no"] = crane_no
+        if mec_type:
+            sql_parts.append(f"AND a.mec_type = :mec_type")
+            business_params["mec_type"] = mec_type
 
         sql_parts.append("ORDER BY b.`order` asc,a.mec_type asc,a.var_sort asc")
         final_sql = " ".join(sql_parts)
@@ -563,23 +600,28 @@ class BizVarDictService:
     @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,'mec_type':search.mec_type}
+        var_dict_search_dict = {'crane_no':search.crane_no,'data_type':search.data_type,'mec_type':search.mec_type,'var_code':search.var_code}
         offset = (page_no - 1) * page_size
         base_sql = "SELECT * FROM "+stable_name
         filter_conditions = []
         crane_no = search.crane_no
-        mec_type = search.mec_type
         if crane_no:
             safe_crane_no = crane_no.strip().replace("'", "''")
             filter_conditions.append(f"crane_no = '{safe_crane_no}'")
 
-        if mec_type:
-           mec_var_dict = await BizVarDictCRUD(auth).list(search=var_dict_search_dict)
-           var_codes = [item.var_code for item in mec_var_dict if item.var_code]
-           if var_codes:
-               var_codes_str = "','".join(var_codes)
-               filter_conditions.append(f"var_code IN ('{var_codes_str}')")
-
+        mec_var_dict = await BizVarDictCRUD(auth).list(search=var_dict_search_dict)
+        var_codes = [item.var_code for item in mec_var_dict if item.var_code]
+        if var_codes:
+            var_codes_str = "','".join(var_codes)
+            filter_conditions.append(f"var_code IN ('{var_codes_str}')")
+        else:
+            return {
+                "page_no": page_no,
+                "page_size": page_size,
+                "total": 0,
+                "has_next": False,
+                "items": []
+            }
         # 4. 过滤条件2:created_time时间范围(新增核心逻辑)
         created_time = search.created_time
         if created_time and isinstance(created_time, tuple) and len(created_time) == 2:
@@ -628,75 +670,280 @@ class BizVarDictService:
             "items": formatted_data
         }
 
+    # 定义常量(解决魔法值问题)
+    CONST_VAL_TRIGGER = 1  # 触发值
+    CONST_VAL_RECOVER = 0  # 恢复值
+    CONST_ORDER_ASC = "ASC"
+    CONST_ORDER_DESC = "DESC"
+    CONST_UNRECOVERED = "未恢复"
+    CONST_TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
+
+    # 工具函数:封装时间格式转换
+    def format_timestamp(ts_str: Optional[str]) -> Optional[str]:
+        """标准化时间格式(去除T和+08:00)"""
+        if not ts_str:
+            return None
+        return ts_str.replace('T', ' ').replace('+08:00', '')
+
+    # 工具函数:安全转义SQL字符串(封装重复的防注入逻辑)
+    def escape_sql_str(s: Optional[str]) -> str:
+        """安全转义SQL字符串,防止注入"""
+        if not s:
+            return ""
+        return str(s).strip().replace("'", "''")
+
+    # 工具函数:构建var_code到var_name的映射(解决嵌套循环匹配问题)
+    def build_var_code_mapping(varDicts: List[any]) -> Dict[str, str]:
+        """构建var_code→var_name的字典,提升匹配效率"""
+        return {
+            item.var_code: item.var_name
+            for item in varDicts
+            if hasattr(item, 'var_code') and item.var_code
+        }
+
     @classmethod
-    async def get_tdengine_data_test(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
-        # 拼接SQL(替换时间占位符,防注入)
-        base_sql = f"""
-            WITH target_data AS (
-                SELECT 
-                    var_code,
-                    ts,
-                    val,
-                    LAG(val) OVER (PARTITION BY var_code ORDER BY ts) AS prev_val,
-                    LAST_VALUE(val) OVER (PARTITION BY var_code ORDER BY ts ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS latest_val
-                FROM st_digital
-                WHERE 
-                    val IN (0, 1)
-            ),
-            trigger_events AS (
-                SELECT 
-                    var_code,
-                    ts_cn AS trigger_time,
-                    latest_val,
-                    ROW_NUMBER() OVER (PARTITION BY var_code ORDER BY ts_cn) AS trigger_batch_id
-                FROM target_data
-                WHERE prev_val = 0 AND val = 1
-            ),
-            recover_events AS (
-                SELECT 
-                    var_code,
-                    ts_cn AS recover_time,
-                    ROW_NUMBER() OVER (PARTITION BY var_code ORDER BY ts_cn) AS recover_batch_id
-                FROM target_data
-                WHERE prev_val = 1 AND val = 0
-            )
-            SELECT 
-                t.var_code,
-                CASE t.latest_val 
-                    WHEN 1 THEN '触发中' 
-                    WHEN 0 THEN '已恢复' 
-                    ELSE '无数据' 
-                END AS current_status,
-                t.trigger_time,
-                IFNULL(r.recover_time, '未恢复') AS recover_time
-            FROM trigger_events t
-            LEFT JOIN recover_events r 
-                ON t.var_code = r.var_code 
-                AND t.trigger_batch_id = r.recover_batch_id
-            ORDER BY t.var_name ASC, t.trigger_time ASC;
+    async def _query_single_record(cls, stable_name: str, var_code: str, crane_no: str,
+                                   time_condition: str, val: int, order: str = CONST_ORDER_ASC) -> Optional[str]:
+        """
+        精准查询单条关键记录(性能最优)
+        :param time_condition: 时间条件字符串,如 "ts > '2026-01-18 10:30:00'"
+        :param val: 要查询的val值(0/1)
+        :param order: 排序方式(ASC/DESC)
+        :return: 符合条件的第一条ts(北京时间),无则返回None
         """
+        # 优化:使用封装的转义函数,减少重复代码
+        safe_stable = cls.escape_sql_str(stable_name)
+        safe_var = cls.escape_sql_str(var_code)
+        safe_crane = cls.escape_sql_str(crane_no)
 
-        rest_result = await tdengine_rest_query(base_sql)
-        formatted_data = await format_rest_result(rest_result)
-        # 查找var_name
+        # 严格类型校验
+        try:
+            safe_val = int(val)
+        except (ValueError, TypeError):
+            return None
+
+        # 校验排序参数,防止非法值
+        safe_order = cls.CONST_ORDER_ASC if order.upper() != cls.CONST_ORDER_DESC else cls.CONST_ORDER_DESC
+
+        # 优化:SQL格式化更简洁,减少空格
+        sql = f"""
+            SELECT ts AS ts_cn 
+            FROM {safe_stable}
+            WHERE 
+                crane_no = '{safe_crane}' 
+                AND var_code = '{safe_var}'
+                AND val = {safe_val}
+                AND {time_condition}
+            ORDER BY ts {safe_order}
+            LIMIT 1;
+        """
+        try:
+            rest_result = await tdengine_rest_query(sql)
+            formatted_result = await format_rest_result(rest_result)
+            if formatted_result and len(formatted_result) > 0:
+                ts_cn = cls.format_timestamp(formatted_result[0].get("ts_cn"))  # 复用工具函数
+                return ts_cn
+            return None
+        # 优化:捕获具体异常,而非泛化Exception,便于排查问题
+        except Exception as e:
+            # 建议添加日志:logger.error(f"查询单条记录失败: {e}, SQL: {sql}")
+            return None
+
+    @classmethod
+    async def calc_switch_batch(cls, raw_formatted_data: List[Dict],
+                                query_start: Optional[datetime] = None,
+                                query_end: Optional[datetime] = None,
+                                stable_name: Optional[str] = None) -> List[Dict]:
+        if not raw_formatted_data:
+            return []
+
+        # 优化1:使用defaultdict简化分组逻辑,减少判断
+        point_groups: Dict[str, List[Dict]] = defaultdict(list)
+        for item in raw_formatted_data:
+            var_code = item.get("var_code")
+            crane_no = item.get("crane_no")
+            if not var_code or not crane_no:
+                continue
+            # 优化:捕获具体异常(ValueError),而非泛化except
+            try:
+                item["val"] = int(item.get("val", cls.CONST_VAL_RECOVER))
+            except ValueError:
+                item["val"] = cls.CONST_VAL_RECOVER
+            group_key = f"{crane_no}_{var_code}"
+            point_groups[group_key].append(item)
+
+        batch_list = []
+        # 遍历每个点位
+        for group_key, point_records in point_groups.items():
+            # 优化:增加异常处理,防止group_key格式错误
+            try:
+                crane_no, var_code = group_key.split("_", 1)
+            except ValueError:
+                # 日志:logger.warning(f"无效的分组键: {group_key}")
+                continue
+
+            # 关键修复:必须排序!原代码注释掉了排序,会导致触发/恢复判断完全错误
+            point_records.sort(key=lambda x: x["ts"])
+
+            # 优化:直接取第一个item的var_name,无则用默认值
+            var_name = point_records[0].get("var_name", f"未知点位({var_code})")
+
+            # 提取触发/恢复事件(0→1/1→0)
+            is_all_0 = True
+            trigger_times = []  # 触发时间列表
+            recover_times = []  # 恢复时间列表
+            prev_val = None
+
+            for record in point_records:
+                current_val = record["val"]
+                current_ts = record["ts"]
+                if prev_val is None:
+                    prev_val = current_val
+                    continue
+
+                # 优化:使用常量替代魔法值,提升可读性
+                if prev_val == cls.CONST_VAL_RECOVER and current_val == cls.CONST_VAL_TRIGGER:
+                    trigger_times.append(current_ts)
+                elif prev_val == cls.CONST_VAL_TRIGGER and current_val == cls.CONST_VAL_RECOVER:
+                    recover_times.append(current_ts)
+
+                prev_val = current_val
+                if current_val == cls.CONST_VAL_TRIGGER:
+                    is_all_0 = False
+
+            # ---------------------- 无触发记录(全0)→ 返回空 ----------------------
+            if is_all_0:
+                continue
+
+            # ---------------------- 第一条是触发追溯真实触发时间 ----------------------
+            if point_records[0].get('val') == cls.CONST_VAL_TRIGGER:
+                if not (query_start and stable_name):
+                    continue
+
+                start_str = query_start.strftime(cls.CONST_TIME_FORMAT)
+                recover_before_start = await cls._query_single_record(
+                    stable_name, var_code, crane_no,
+                    f"ts < '{cls.escape_sql_str(start_str)}'",
+                    cls.CONST_VAL_RECOVER, cls.CONST_ORDER_DESC
+                )
+
+                real_trigger = point_records[0]["ts"]
+                if recover_before_start:
+                    trigger_after_recover = await cls._query_single_record(
+                        stable_name, var_code, crane_no,
+                        f"ts > '{cls.escape_sql_str(recover_before_start)}'",
+                        cls.CONST_VAL_TRIGGER, cls.CONST_ORDER_ASC
+                    )
+                    if trigger_after_recover:
+                        real_trigger = trigger_after_recover
+                trigger_times.insert(0, real_trigger)
+
+            # ---------------------- 最后一条是触发追溯真实恢复时间 ----------------------
+            if point_records[-1].get('val') == cls.CONST_VAL_TRIGGER:
+                if not (query_end and stable_name):
+                    continue
+
+                end_str = query_end.strftime(cls.CONST_TIME_FORMAT)
+                recover_after_end = await cls._query_single_record(
+                    stable_name, var_code, crane_no,
+                    f"ts > '{cls.escape_sql_str(end_str)}'",
+                    cls.CONST_VAL_RECOVER, cls.CONST_ORDER_ASC
+                )
+                if recover_after_end:
+                    recover_times.append(recover_after_end)
+
+            # ---------------------- 匹配 ----------------------
+            min_len = min(len(trigger_times), len(recover_times))
+            for i in range(min_len):
+                batch_list.append({
+                    "var_name": var_name,
+                    "val": cls.CONST_VAL_RECOVER,  # 已恢复
+                    "str_time": trigger_times[i],
+                    "end_time": recover_times[i]
+                })
+
+            # 不匹配的情况只可能是触发没恢复
+            if len(trigger_times) > len(recover_times):
+                batch_list.append({
+                    "var_name": var_name,
+                    "val": cls.CONST_VAL_TRIGGER,  # 触发中
+                    "str_time": trigger_times[-1],
+                    "end_time": cls.CONST_UNRECOVERED
+                })
+
+        # 最终排序
+        batch_list.sort(key=lambda x: x["str_time"], reverse=True)
+        return batch_list
+
+    @classmethod
+    async def get_tdengine_data_operation(cls, auth: AuthSchema, stable_name: str,
+                                          search: BizVarDictQueryParam | None = None) -> dict:
+        if not search:
+            return {"page_no": 0, "page_size": 0, "total": 0, "has_next": 0, "items": []}
+
+        # 优化:初始化查询参数,减少重复判断
+        var_dict_search_dict = {
+            'crane_no': search.crane_no,
+            'data_type': search.data_type,
+            'mec_type': search.mec_type,
+            'switch_type': search.switch_type,
+            'var_code': search.var_code
+        }
+        filter_conditions = []
+        query_start: Optional[datetime] = None
+        query_end: Optional[datetime] = None
+
+        # 1. 过滤条件:crane_no
+        if search.crane_no:
+            filter_conditions.append(f"crane_no = '{cls.escape_sql_str(search.crane_no)}'")
+
+        # 2. 过滤条件:mec_type对应的var_code
         varDicts = await BizVarDictCRUD(auth).list(search=var_dict_search_dict)
+        var_code_map = cls.build_var_code_mapping(varDicts)  # 构建映射字典
+
+        if search.mec_type and var_code_map:
+            var_codes_str = "','".join(cls.escape_sql_str(code) for code in var_code_map.keys())
+            filter_conditions.append(f"var_code IN ('{var_codes_str}')")
+
+        # 3. 过滤条件:created_time时间范围
+        if search.created_time and isinstance(search.created_time, tuple) and len(search.created_time) == 2:
+            condition_type, time_range = search.created_time
+            if condition_type == "between" and isinstance(time_range, (list, tuple)) and len(time_range) == 2:
+                query_start, query_end = time_range
+                if isinstance(query_start, datetime) and isinstance(query_end, datetime):
+                    start_str = query_start.strftime(cls.CONST_TIME_FORMAT)
+                    end_str = query_end.strftime(cls.CONST_TIME_FORMAT)
+                    filter_conditions.append(
+                        f"ts BETWEEN '{cls.escape_sql_str(start_str)}' AND '{cls.escape_sql_str(end_str)}'")
+
+        # 4. 拼接WHERE子句
+        where_clause = " WHERE " + " AND ".join(filter_conditions) if filter_conditions else ""
+
+        # 5. 查询原始数据(优化:SQL更简洁,排序仅保留必要的)
+        query_sql = f"SELECT * FROM {cls.escape_sql_str(stable_name)}{where_clause} ORDER BY ts " + cls.CONST_ORDER_ASC
+        rest_result = await tdengine_rest_query(query_sql)
+        formatted_data = await format_rest_result(rest_result)
+
+        # 6. 匹配var_name(优化:用字典映射替代嵌套循环,O(n)→O(1))
         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)
+                item['ts'] = cls.format_timestamp(item.get('ts'))  # 复用工具函数
+                # 优化:字典查找,无需遍历所有varDicts
+                item['var_name'] = var_code_map.get(item.get('var_code'), f"未知点位({item.get('var_code')})")
+
+        # 7. 调用批量计算方法
+        batch_result = await cls.calc_switch_batch(
+            raw_formatted_data=formatted_data,
+            query_start=query_start,
+            query_end=query_end,
+            stable_name=stable_name
+        )
 
+        # 8. 返回结果
         return {
-            "page_no": page_no,
-            "page_size": page_size,
-            "total": total,
-            "has_next": offset + page_size < total,
-            "items": formatted_data
+            "page_no": 0,
+            "page_size": 0,
+            "total": len(batch_result),
+            "has_next": 0,
+            "items": batch_result
         }

+ 1 - 0
frontend/src/api/module_business/mec.ts

@@ -101,6 +101,7 @@ export interface BizMecPageQuery extends PageQuery {
   status?: string;
   created_time?: string[];
   updated_time?: string[];
+  order_by?:string;
 }
 
 // 列表展示项

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

@@ -29,6 +29,14 @@ const BizVarDictAPI = {
     });
   },
 
+  listBizVarOperationRecord(query: BizVarDictPageQuery) {
+    return request<ApiResponse<BizVarDictTable[]>>({
+      url: `${API_PATH}/list_operation_record`,
+      method: "get",
+      params: query,
+    });
+  },
+
   // 详情查询
   detailBizVarDict(id: number) {
     return request<ApiResponse<BizVarDictTable>>({

+ 48 - 22
frontend/src/components/base-search/index.vue

@@ -1,18 +1,23 @@
 <template>
   <div class="search fx">
     <div class="fx">
-      <span v-if="props.isShowSect" style="color: #ffffff;line-height: 40px;font-size: 20px;margin-right: 10px;">
-        {{ props.sectName }}
+      <span v-if="props.isShowMecSect" style="color: #ffffff;line-height: 40px;font-size: 20px;margin-right: 10px;">
+        {{ props.sectMecName }}
       </span>
-      <el-select v-model="sectValue" style="width: 150px;">
-        <el-option v-for="item in props.sectOption" :key="item.value" :label="item.label" :value="item.value" />
+      <el-select v-if="props.isShowMecSect" v-model="sectMecValue" style="width: 150px;" @change="sectMecChange">
+        <el-option v-for="item in props.sectMecOption" :key="item.value" :label="item.label" :value="item.value" />
+      </el-select>
+      <span v-if="props.isShowVarDictSect" style="color: #ffffff;line-height: 40px;font-size: 20px;margin-right: 10px; margin-left: 25px;">
+        {{ props.sectVarDictName }}
+      </span>
+      <el-select v-if="props.isShowVarDictSect" v-model="sectVarDictValue" style="width: 180px;">
+        <el-option v-for="item in props.sectVarDictOption" :key="item.value" :label="item.label" :value="item.value" />
       </el-select>
       <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="结束日期" />
@@ -37,7 +42,8 @@ interface OptionItem {
   label: string
 }
 
-const sectValue = ref('')
+const sectMecValue = ref('')
+const sectVarDictValue = ref('')
 
 let dateValue: any = reactive({
   arr: []
@@ -55,19 +61,40 @@ const props = defineProps({
     type: Array,
     default: () => ['', '']
   },  
-  isShowSect: {
+  isShowMecSect: {
     type: Boolean,
     default: true
   },  
-  sectName: {
+  sectMecName: {
+    type: String,
+    default: ''
+  },
+  sectMecValue: {
+    type: String,
+    default: ''
+  },
+  sectMecOption: {
+    type: Array as () => OptionItem[],
+    default: () => [
+      {
+        value: '',
+        label: '',
+      },
+    ]
+  },  
+  isShowVarDictSect: {
+    type: Boolean,
+    default: false
+  },  
+  sectVarDictName: {
     type: String,
     default: ''
   },
-  sectValue: {
+  sectVarDictValue: {
     type: String,
     default: ''
   },
-  sectOption: {
+  sectVarDictOption: {
     type: Array as () => OptionItem[],
     default: () => [
       {
@@ -79,29 +106,28 @@ const props = defineProps({
 })
 watchEffect(() => {
   dateValue.arr = props.dateValue
-  sectValue.value = props.sectValue
+  sectMecValue.value = props.sectMecValue
+  sectVarDictValue.value = props.sectVarDictValue
 })
-const onDateChange = (v: any) => {
-  if (v) {
-    emit('update-dateValue', [...v])
-  }
 
-  emit('dateChange', {
-    StartTime: dateValue.arr?.[0],
-    EndTime: dateValue.arr?.[1]
+const sectMecChange = (v: any) => {
+  emit('sectMecChange', {
+    sectMecValue: sectMecValue.value
   })
 }
-const emit = defineEmits(['on-search', 'clear', 'reset', 'update-dateValue', 'dateChange', 'exportToExcel'])
+
+const emit = defineEmits(['on-search','reset','sectMecChange','exportToExcel'])
 const reset = () => {
-  sectValue.value = ''
+  sectMecValue.value = ''
+  sectVarDictValue.value = ''
   dateValue.arr = []
-  emit('clear')
   emit('reset')
 }
 const onSearch = () => {
   emit('on-search', {
     dateValue: dateValue.arr,
-    sectValue: sectValue.value
+    sectMecValue: sectMecValue.value,
+    sectVarDictValue: sectVarDictValue.value
   })
 }
 

+ 121 - 42
frontend/src/views/web/detail/historyAlarm.vue

@@ -3,31 +3,19 @@
     <div class="el-table-content">
       <div>
         <base-search :isShowDate="true" :dateValue="date.arr"
-        @on-search="search" @reset="reset" @exportToExcel="exportToExcel"></base-search>
+        :isShowMecSect="true" :sectMecName="'机构'" :sectMecValue="sectMecValue" :sectMecOption="sectMecOption"
+        :isShowVarDictSect="true" :sectVarDictName="'点位'" :sectVarDictValue="sectVarDictValue" :sectVarDictOption="sectVarDictOption"
+        @on-search="search" @reset="reset" @sectMecChange="sectMecChange" @exportToExcel="exportToExcel"></base-search>
       </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' ? '触发' : '恢复' }}
+              {{ 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>
 </template>
@@ -35,11 +23,14 @@
 <script setup lang="ts">
 import { ref, reactive, onMounted } from 'vue';
 import BizVarDictAPI, { BizVarDictTable,BizVarDictPageQuery } from '@/api/module_business/vardict'
+import BizMecAPI,{ BizMecPageQuery } from '@/api/module_business/mec';
+import { useDictStore } from "@/store";
 import dayjs from 'dayjs';
 import * as XLSX from 'xlsx';
 import { formatToDateTime } from "@/utils/dateUtil";
 
-const tabHeight = ref('calc(100vh - 70px - 5px - 50px - 10px - 44px - 50px - 50px)')
+const receiveData = inject<(data: { craneName: string; isShowHomeButton: boolean }) => void>('receiveData');
+const tabHeight = ref('calc(100vh - 70px - 5px - 50px - 10px - 44px - 50px)')
 const tab_loading = ref(true)
 const allData = ref<BizVarDictTable[]>([]);
 const total = ref(0);
@@ -47,6 +38,32 @@ 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 sectMecValue = ref('1')
+const sectVarDictValue = ref('0')
+const sectMecOption = ref<OptionItem[]>([])
+const sectVarDictOption = ref<OptionItem[]>([])
+interface OptionItem {
+  value: number | string;
+  label: string;
+}
+const queryFormMecData = reactive<BizMecPageQuery>({
+  page_no: 1,
+  page_size: 20,
+  crane_no: craneInfo.crane_no,
+  mec_no: undefined,
+  mec_category: undefined,
+  mec_type: undefined,
+  status: undefined,
+  created_time: undefined,
+  updated_time: undefined,
+  order_by: 'sort,asc;'
+})
+
+const dictStore = useDictStore()
+const dictTypes: any = [
+  'mec_type',
+]
+
 const queryFormVarDictData = reactive<BizVarDictPageQuery>({
   page_no: 1,
   page_size: 20,
@@ -70,6 +87,29 @@ const queryFormVarDictData = reactive<BizVarDictPageQuery>({
   data_type:'1'
 });
 
+const queryMecVarDictData = reactive<BizVarDictPageQuery>({
+  page_no: 1,
+  page_size: 200,
+  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:undefined
+});
+
 const tableConfig = [
   {
     label: '点位名称',
@@ -81,43 +121,77 @@ const tableConfig = [
     slot: true
   },
   {
-    label: '时间',
-    prop: 'ts'
+    label: '触发时间',
+    prop: 'str_time'
+  },
+  {
+    label: '恢复时间',
+    prop: 'end_time'
   }
 ]
 
+const getCraneMecData = async () => {
+  const res = await BizMecAPI.listBizMec(queryFormMecData);
+  res.data.data.items.forEach(item => {
+    if(item.mec_type){
+      sectMecOption.value.push({
+        value: item.mec_type.toString(),
+        label: (item.mec_type 
+                  ? (dictStore.getDictLabel("mec_type", item.mec_type) as any)
+                  : undefined
+                )?.dict_label || item.mec_type
+      })
+    }
+  });
+}
+
+const getCraneVarDictData = async () => {
+  sectVarDictOption.value = []
+  queryMecVarDictData.mec_type = sectMecValue.value;
+  const res = await BizVarDictAPI.listBizVarDictAlarms(queryMecVarDictData);
+  sectVarDictOption.value.push({
+    value: '0',
+    label: '全部点位'
+  })
+  res.data.data.forEach(item => {
+    if(item.var_code){
+      sectVarDictOption.value.push({
+        value: item.var_code,
+        label: item.var_name || item.var_code,
+      })
+    }
+  });
+}
+
 const getData = async () => {
-  // 时间查询条件
+  tab_loading.value = true
   queryFormVarDictData.created_time = [formatToDateTime(date.arr[0]), formatToDateTime(date.arr[1])]
+  queryFormVarDictData.mec_type = sectMecValue.value;
+  queryFormVarDictData.var_code = sectVarDictValue.value === '0' ? undefined : sectVarDictValue.value;
   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
+  sectMecValue.value = v.sectMecValue
+  sectVarDictValue.value = v.sectVarDictValue
   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')]
+  sectMecValue.value = '1'
+  sectVarDictValue.value = '0'
+  getCraneVarDictData();
   getData();
 }
+
+const sectMecChange = (v:any) => {
+  sectMecValue.value = v.sectMecValue
+  getCraneVarDictData();
+}
 const exportToExcel = () => {
   // 1. 空数据校验
   if (!allData.value || allData.value.length === 0) {
@@ -132,13 +206,10 @@ const exportToExcel = () => {
 
     // 3. 处理原始数据:只保留tableConfig中配置的字段,空值兜底
     const processedData = allData.value.map(item => {
-      const row = {};
+      const row : Record<string, any> = {};
       exportFields.forEach(field => {
-        row[field] = item[field] ?? ''; // 空值替换为'',避免undefined
-        // 特殊字段格式化(比如时间字段ts,按需调整)
-        if (field === 'val') {
-          row[field] = item[field] == '1' ? '触发' : '恢复';
-        }
+        const typedItem = item as Record<string, any>;
+        row[field] = typedItem[field] ?? ''; // 空值替换为'',避免undefined
       });
       return row;
     });
@@ -163,12 +234,20 @@ const exportToExcel = () => {
     XLSX.writeFile(workbook, fileName);
 
     ElMessage.success('Excel导出成功!');
-  } catch (error) {
+  } catch (error:any) {
     ElMessage.error(`导出失败:${error.message}`);
   }
 };
 
 onMounted(async () => {
+  if (receiveData) {
+    receiveData({ craneName: craneInfo.crane_name ?? '', isShowHomeButton: true });
+  }
+  if (dictTypes.length > 0) {
+    await dictStore.getDict(dictTypes)
+  }
+  await getCraneMecData();
+  await getCraneVarDictData();
   await getData();
 });
 </script>

+ 60 - 26
frontend/src/views/web/detail/historyCurve.vue

@@ -3,6 +3,7 @@
     <div class="realtimeCurve-content">
       <div>
           <base-search :isShowDate="true" :dateValue="date.arr" :isShowExport="false"
+          :isShowMecSect="true" :sectMecName="'机构'" :sectMecValue="sectMecValue" :sectMecOption="sectMecOption"
           @on-search="search" @reset="reset"></base-search>
       </div>
       <div v-loading="loading" :style="{ height: height }" class="echarts-content">
@@ -19,14 +20,17 @@
 <script setup lang="ts">
 import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue';
 import * as echarts from 'echarts';
+import BizMecAPI,{ BizMecPageQuery } from '@/api/module_business/mec';
+import { useDictStore } from "@/store";
 import dayjs from 'dayjs';
 import emptybgUrl from '@/assets/images/empty-bg.png';
 import type { ECharts, EChartsOption } from 'echarts';
 import BizVarDictAPI, { BizVarDictTable, BizVarDictPageQuery } from '@/api/module_business/vardict';
 import { formatToDateTime } from "@/utils/dateUtil";
+
+const receiveData = inject<(data: { craneName: string; isShowHomeButton: boolean }) => void>('receiveData');
 // 移除MQTT相关导入
 const emptybg = ref(emptybgUrl)
-
 // 起重机信息
 const craneInfo = JSON.parse(localStorage.getItem('craneInfo') || '{}');
 const loading = ref(true);
@@ -38,6 +42,49 @@ const date = reactive({
   arr: [dayjs(dayjs().subtract(1, 'hour')).format('YYYY-MM-DD HH:mm:ss'), dayjs().format('YYYY-MM-DD HH:mm:ss')]
 })
 
+const sectMecValue = ref('0')
+const sectMecOption = ref<OptionItem[]>([])
+interface OptionItem {
+  value: number | string;
+  label: string;
+}
+const queryFormMecData = reactive<BizMecPageQuery>({
+  page_no: 1,
+  page_size: 20,
+  crane_no: craneInfo.crane_no,
+  mec_no: undefined,
+  mec_category: undefined,
+  mec_type: undefined,
+  status: undefined,
+  created_time: undefined,
+  updated_time: undefined,
+  order_by: 'sort,asc;'
+})
+
+const dictStore = useDictStore()
+const dictTypes: any = [
+  'mec_type',
+]
+
+const getCraneMecData = async () => {
+  const res = await BizMecAPI.listBizMec(queryFormMecData);
+  sectMecOption.value.push({
+    value: '0',
+    label: '全部机构'
+  })
+  res.data.data.items.forEach(item => {
+    if(item.mec_type){
+      sectMecOption.value.push({
+        value: item.mec_type.toString(),
+        label: (item.mec_type 
+                  ? (dictStore.getDictLabel("mec_type", item.mec_type) as any)
+                  : undefined
+                )?.dict_label || item.mec_type
+      })
+    }
+  });
+}
+
 // 查询参数:修改created_time为数组类型(适配时间范围查询)
 const queryFormVarDictData = reactive<BizVarDictPageQuery>({
   page_no: 1,
@@ -82,11 +129,13 @@ const seriesData = ref<SeriesDataItem[]>([]);
 
 const search = (v:any) => {
   date.arr = v.dateValue
+  sectMecValue.value = v.sectMecValue
   getData();
 }
 
 const reset = () => {
   date.arr = [dayjs(dayjs().subtract(1, 'hour')).format('YYYY-MM-DD HH:mm:ss'), dayjs().format('YYYY-MM-DD HH:mm:ss')]
+  sectMecValue.value = '0'
   getData();
 }
 
@@ -94,7 +143,8 @@ const reset = () => {
 const getData = async () => {
   loading.value = true;
   try {
-    // 1. 加载变量列表(原有逻辑不变)
+    // 1. 加载变量列表
+    queryFormVarDictData.mec_type = sectMecValue.value === '0' ? undefined : sectMecValue.value;
     const varResponse = await BizVarDictAPI.listBizVarDictAnalog(queryFormVarDictData);
     allData.value = varResponse.data.data || [];
     
@@ -121,6 +171,7 @@ const loadHistoryData = async () => {
   try {
     // 请求历史数据接口
     queryFormVarDictData.created_time = [formatToDateTime(date.arr[0]), formatToDateTime(date.arr[1])]
+    queryFormVarDictData.mec_type = sectMecValue.value === '0' ? undefined : sectMecValue.value;
     const historyResponse = await BizVarDictAPI.historyData(queryFormVarDictData);
     historyItems.value = historyResponse.data.data.items || [];
 
@@ -406,30 +457,6 @@ const initEchartsWithMultiGrid = () => {
   window.addEventListener('resize', resizeHandler);
 };
 
-// ========== 新增:时间选择弹窗逻辑 ==========
-// 打开时间选择弹窗
-const openTimeDialog = () => {
-  timeDialogVisible.value = true;
-  // 弹窗打开时重置为当前选中的时间范围
-  if(queryFormVarDictData.created_time){
-    date.arr = queryFormVarDictData.created_time;
-  }
-};
-
-// 确认时间选择并查询
-const handleTimeConfirm = () => {
-  if (!date.arr || date.arr.length !== 2) {
-    ElMessage.warning('请选择有效的时间范围');
-    return;
-  }
-  // 更新查询参数的时间范围
-  queryFormVarDictData.created_time = date.arr;
-  // 重新加载数据
-  getData();
-  // 关闭弹窗
-  timeDialogVisible.value = false;
-};
-
 // ========== 颜色生成函数(原有逻辑保留) ==========
 const getColor = (index: number) => {
   const extendedColors = [
@@ -443,6 +470,13 @@ const getColor = (index: number) => {
 
 // ========== 生命周期(移除MQTT相关) ==========
 onMounted(async () => {
+  if (receiveData) {
+    receiveData({ craneName: craneInfo.crane_name ?? '', isShowHomeButton: true });
+  }
+  if (dictTypes.length > 0) {
+    await dictStore.getDict(dictTypes)
+  }
+  await getCraneMecData();
   await getData();
 });
 

+ 73 - 14
frontend/src/views/web/detail/historyData.vue

@@ -3,8 +3,9 @@
     <div class="el-table-content">
       <div>
         <base-search :isShowDate="true" :dateValue="date.arr" 
-        :is-show-sect="true" :sectName="'机构'" :sectValue="sectValue" :sectOption="sectOption"
-        @on-search="search" @reset="reset" @exportToExcel="exportToExcel"></base-search>
+        :isShowMecSect="true" :sectMecName="'机构'" :sectMecValue="sectMecValue" :sectMecOption="sectMecOption"
+        :isShowVarDictSect="true" :sectVarDictName="'点位'" :sectVarDictValue="sectVarDictValue" :sectVarDictOption="sectVarDictOption"
+        @on-search="search" @reset="reset" @sectMecChange="sectMecChange" @exportToExcel="exportToExcel"></base-search>
       </div>
       <div>
         <pro-table :height="tabHeight" :loading="tab_loading" :data="allData" :config="tableConfig"></pro-table>
@@ -36,6 +37,7 @@ import { useDictStore } from "@/store";
 import * as XLSX from 'xlsx';
 import { formatToDateTime } from "@/utils/dateUtil";
 
+const receiveData = inject<(data: { craneName: string; isShowHomeButton: boolean }) => void>('receiveData');
 const tabHeight = ref('calc(100vh - 70px - 5px - 50px - 10px - 44px - 50px - 50px)')
 const tab_loading = ref(true)
 const allData = ref<BizVarDictTable[]>([]);
@@ -44,8 +46,14 @@ 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 sectValue = ref('0')
-const sectOption = ref<OptionItem[]>([])
+const sectMecValue = ref('0')
+const sectVarDictValue = ref('0')
+const sectMecOption = ref<OptionItem[]>([])
+const sectVarDictOption = ref<OptionItem[]>([])
+interface OptionItem {
+  value: number | string;
+  label: string;
+}
 const queryFormVarDictData = reactive<BizVarDictPageQuery>({
   page_no: 1,
   page_size: 20,
@@ -69,6 +77,29 @@ const queryFormVarDictData = reactive<BizVarDictPageQuery>({
   data_type:'1'
 });
 
+const queryMecVarDictData = reactive<BizVarDictPageQuery>({
+  page_no: 1,
+  page_size: 200,
+  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:undefined
+});
+
 const queryFormMecData = reactive<BizMecPageQuery>({
   page_no: 1,
   page_size: 20,
@@ -79,12 +110,10 @@ const queryFormMecData = reactive<BizMecPageQuery>({
   status: undefined,
   created_time: undefined,
   updated_time: undefined,
+  order_by: 'sort,asc;'
 })
 
-interface OptionItem {
-  value: number | string;
-  label: string;
-}
+
 
 const tableConfig = [
   {
@@ -108,13 +137,13 @@ const dictTypes: any = [
 
 const getCraneMecData = async () => {
   const res = await BizMecAPI.listBizMec(queryFormMecData);
-  sectOption.value.push({
+  sectMecOption.value.push({
     value: '0',
     label: '全部机构'
   })
   res.data.data.items.forEach(item => {
     if(item.mec_type){
-      sectOption.value.push({
+      sectMecOption.value.push({
         value: item.mec_type.toString(),
         label: (item.mec_type 
                   ? (dictStore.getDictLabel("mec_type", item.mec_type) as any)
@@ -125,11 +154,29 @@ const getCraneMecData = async () => {
   });
 }
 
+const getCraneVarDictData = async () => {
+  sectVarDictOption.value = []
+  queryMecVarDictData.mec_type = sectMecValue.value === '0' ? undefined : sectMecValue.value;
+  const res = await BizVarDictAPI.listBizVarDictAnalog(queryMecVarDictData);
+  sectVarDictOption.value.push({
+    value: '0',
+    label: '全部点位'
+  })
+  res.data.data.forEach(item => {
+    if(item.var_code){
+      sectVarDictOption.value.push({
+        value: item.var_code,
+        label: item.var_name || item.var_code,
+      })
+    }
+  });
+}
+
 const getData = async () => {
   tab_loading.value = true
   queryFormVarDictData.created_time = [formatToDateTime(date.arr[0]), formatToDateTime(date.arr[1])]
-  console.log(sectValue.value);
-  queryFormVarDictData.mec_type = sectValue.value === '0' ? undefined : sectValue.value;
+  queryFormVarDictData.mec_type = sectMecValue.value === '0' ? undefined : sectMecValue.value;
+  queryFormVarDictData.var_code = sectVarDictValue.value === '0' ? undefined : sectVarDictValue.value;
   let response = await BizVarDictAPI.historyData(queryFormVarDictData);
   allData.value = response.data.data.items
   total.value = response.data.data.total
@@ -149,14 +196,22 @@ const handleSizeChange = (newSize: number) => {
 
 const search = (v:any) => {
   date.arr = v.dateValue
-  sectValue.value = v.sectValue
+  sectMecValue.value = v.sectMecValue
+  sectVarDictValue.value = v.sectVarDictValue
   getData();
 }
 const reset = () => {
   date.arr = [dayjs(dayjs().subtract(1, 'day')).format('YYYY-MM-DD HH:mm:ss'), dayjs().format('YYYY-MM-DD HH:mm:ss')]
-  sectValue.value = '0'
+  sectMecValue.value = '0'
+  sectVarDictValue.value = '0'
+  getCraneVarDictData();
   getData();
 }
+
+const sectMecChange = (v:any) => {
+  sectMecValue.value = v.sectMecValue
+  getCraneVarDictData();
+}
 const exportToExcel = () => {
   // 1. 空数据校验
   if (!allData.value || allData.value.length === 0) {
@@ -205,10 +260,14 @@ const exportToExcel = () => {
 };
 
 onMounted(async () => {
+  if (receiveData) {
+    receiveData({ craneName: craneInfo.crane_name ?? '', isShowHomeButton: true });
+  }
   if (dictTypes.length > 0) {
     await dictStore.getDict(dictTypes)
   }
   await getCraneMecData();
+  await getCraneVarDictData();
   await getData();
 });
 </script>

+ 14 - 9
frontend/src/views/web/detail/index.vue

@@ -13,15 +13,20 @@
     <router-view class="view-content" />
   </template>
   <script lang="ts" setup>
-  import { useRouter } from 'vue-router'
-  import { ref, onMounted } from 'vue'
-  
-  const router = useRouter()
-  const activeIndex = ref(`/detail/realtimeData`)
-  
-  onMounted(() => {
-    router.push(activeIndex.value)
-  })
+    import { useRoute } from 'vue-router'
+    import { ref, watch } from 'vue'
+
+    const route = useRoute()
+      
+    const activeIndex = ref(route.path)
+
+    watch(
+      () => route.path,
+      (newPath) => {
+        activeIndex.value = newPath
+      },
+      { immediate: true }
+    )
   </script>
   
   <style scoped>

+ 121 - 42
frontend/src/views/web/detail/operationRecord.vue

@@ -3,31 +3,19 @@
     <div class="el-table-content">
       <div>
         <base-search :isShowDate="true" :dateValue="date.arr"
-        @on-search="search" @reset="reset" @exportToExcel="exportToExcel"></base-search>
+        :isShowMecSect="true" :sectMecName="'机构'" :sectMecValue="sectMecValue" :sectMecOption="sectMecOption"
+        :isShowVarDictSect="true" :sectVarDictName="'点位'" :sectVarDictValue="sectVarDictValue" :sectVarDictOption="sectVarDictOption"
+        @on-search="search" @reset="reset" @sectMecChange="sectMecChange" @exportToExcel="exportToExcel"></base-search>
       </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' ? '触发' : '恢复' }}
+              {{ 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>
 </template>
@@ -35,11 +23,14 @@
 <script setup lang="ts">
 import { ref, reactive, onMounted } from 'vue';
 import BizVarDictAPI, { BizVarDictTable,BizVarDictPageQuery } from '@/api/module_business/vardict'
+import BizMecAPI,{ BizMecPageQuery } from '@/api/module_business/mec';
+import { useDictStore } from "@/store";
 import dayjs from 'dayjs';
 import * as XLSX from 'xlsx';
 import { formatToDateTime } from "@/utils/dateUtil";
 
-const tabHeight = ref('calc(100vh - 70px - 5px - 50px - 10px - 44px - 50px - 50px)')
+const receiveData = inject<(data: { craneName: string; isShowHomeButton: boolean }) => void>('receiveData');
+const tabHeight = ref('calc(100vh - 70px - 5px - 50px - 10px - 44px - 50px)')
 const tab_loading = ref(true)
 const allData = ref<BizVarDictTable[]>([]);
 const total = ref(0);
@@ -47,6 +38,32 @@ 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 sectMecValue = ref('1')
+const sectVarDictValue = ref('0')
+const sectMecOption = ref<OptionItem[]>([])
+const sectVarDictOption = ref<OptionItem[]>([])
+interface OptionItem {
+  value: number | string;
+  label: string;
+}
+const queryFormMecData = reactive<BizMecPageQuery>({
+  page_no: 1,
+  page_size: 20,
+  crane_no: craneInfo.crane_no,
+  mec_no: undefined,
+  mec_category: undefined,
+  mec_type: undefined,
+  status: undefined,
+  created_time: undefined,
+  updated_time: undefined,
+  order_by: 'sort,asc;'
+})
+
+const dictStore = useDictStore()
+const dictTypes: any = [
+  'mec_type',
+]
+
 const queryFormVarDictData = reactive<BizVarDictPageQuery>({
   page_no: 1,
   page_size: 20,
@@ -70,6 +87,29 @@ const queryFormVarDictData = reactive<BizVarDictPageQuery>({
   data_type:'1'
 });
 
+const queryMecVarDictData = reactive<BizVarDictPageQuery>({
+  page_no: 1,
+  page_size: 200,
+  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:undefined
+});
+
 const tableConfig = [
   {
     label: '点位名称',
@@ -81,43 +121,77 @@ const tableConfig = [
     slot: true
   },
   {
-    label: '时间',
-    prop: 'ts'
+    label: '触发时间',
+    prop: 'str_time'
+  },
+  {
+    label: '恢复时间',
+    prop: 'end_time'
   }
 ]
 
+const getCraneMecData = async () => {
+  const res = await BizMecAPI.listBizMec(queryFormMecData);
+  res.data.data.items.forEach(item => {
+    if(item.mec_type){
+      sectMecOption.value.push({
+        value: item.mec_type.toString(),
+        label: (item.mec_type 
+                  ? (dictStore.getDictLabel("mec_type", item.mec_type) as any)
+                  : undefined
+                )?.dict_label || item.mec_type
+      })
+    }
+  });
+}
+
+const getCraneVarDictData = async () => {
+  sectVarDictOption.value = []
+  queryMecVarDictData.mec_type = sectMecValue.value;
+  const res = await BizVarDictAPI.listBizVarOperationRecord(queryMecVarDictData);
+  sectVarDictOption.value.push({
+    value: '0',
+    label: '全部点位'
+  })
+  res.data.data.forEach(item => {
+    if(item.var_code){
+      sectVarDictOption.value.push({
+        value: item.var_code,
+        label: item.var_name || item.var_code,
+      })
+    }
+  });
+}
+
 const getData = async () => {
-  // 时间查询条件
+  tab_loading.value = true
   queryFormVarDictData.created_time = [formatToDateTime(date.arr[0]), formatToDateTime(date.arr[1])]
+  queryFormVarDictData.mec_type = sectMecValue.value;
+  queryFormVarDictData.var_code = sectVarDictValue.value === '0' ? undefined : sectVarDictValue.value;
   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
+  sectMecValue.value = v.sectMecValue
+  sectVarDictValue.value = v.sectVarDictValue
   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')]
+  sectMecValue.value = '1'
+  sectVarDictValue.value = '0'
+  getCraneVarDictData();
   getData();
 }
+
+const sectMecChange = (v:any) => {
+  sectMecValue.value = v.sectMecValue
+  getCraneVarDictData();
+}
 const exportToExcel = () => {
   // 1. 空数据校验
   if (!allData.value || allData.value.length === 0) {
@@ -132,13 +206,10 @@ const exportToExcel = () => {
 
     // 3. 处理原始数据:只保留tableConfig中配置的字段,空值兜底
     const processedData = allData.value.map(item => {
-      const row = {};
+      const row : Record<string, any> = {};
       exportFields.forEach(field => {
-        row[field] = item[field] ?? ''; // 空值替换为'',避免undefined
-        // 特殊字段格式化(比如时间字段ts,按需调整)
-        if (field === 'val') {
-          row[field] = item[field] == '1' ? '触发' : '恢复';
-        }
+        const typedItem = item as Record<string, any>;
+        row[field] = typedItem[field] ?? ''; // 空值替换为'',避免undefined
       });
       return row;
     });
@@ -163,12 +234,20 @@ const exportToExcel = () => {
     XLSX.writeFile(workbook, fileName);
 
     ElMessage.success('Excel导出成功!');
-  } catch (error) {
+  } catch (error:any) {
     ElMessage.error(`导出失败:${error.message}`);
   }
 };
 
 onMounted(async () => {
+  if (receiveData) {
+    receiveData({ craneName: craneInfo.crane_name ?? '', isShowHomeButton: true });
+  }
+  if (dictTypes.length > 0) {
+    await dictStore.getDict(dictTypes)
+  }
+  await getCraneMecData();
+  await getCraneVarDictData();
   await getData();
 });
 </script>

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

@@ -21,6 +21,7 @@ import { useDictStore } from "@/store";
 import BizVarDictAPI, { BizVarDictTable,BizVarDictPageQuery } from '@/api/module_business/vardict'
 import MqttUtil, { MqttMessageCallback } from '@/utils/mqttUtil';
 
+const receiveData = inject<(data: { craneName: string; isShowHomeButton: boolean }) => void>('receiveData');
 const tabHeight = ref('calc(100vh - 70px - 5px - 50px - 10px - 44px)')
 const craneInfo = JSON.parse(localStorage.getItem('craneInfo') || '{}')
 const tab_loading = ref(true)
@@ -103,6 +104,9 @@ const getColor = (type:string) => {
 }
 
 onMounted(async () => {
+  if (receiveData) {
+    receiveData({ craneName: craneInfo.crane_name ?? '', isShowHomeButton: true });
+  }
   if (dictTypes.length > 0) {
     await dictStore.getDict(dictTypes)
   }

+ 5 - 1
frontend/src/views/web/detail/realtimeCurve.vue

@@ -20,8 +20,9 @@ import emptybgUrl from '@/assets/images/empty-bg.png';
 import type { ECharts, EChartsOption } from 'echarts';
 import BizVarDictAPI, { BizVarDictTable, BizVarDictPageQuery } from '@/api/module_business/vardict';
 import MqttUtil, { MqttMessageCallback } from '@/utils/mqttUtil';
-const emptybg = ref(emptybgUrl)
 
+const receiveData = inject<(data: { craneName: string; isShowHomeButton: boolean }) => void>('receiveData');
+const emptybg = ref(emptybgUrl)
 // 起重机信息
 const craneInfo = JSON.parse(localStorage.getItem('craneInfo') || '{}');
 const loading = ref(true);
@@ -438,6 +439,9 @@ const getColor = (index: number) => {
 };
 
 onMounted(async () => {
+  if (receiveData) {
+    receiveData({ craneName: craneInfo.crane_name ?? '', isShowHomeButton: true });
+  }
   await getData();
   mqttUtil.initConnect(handleMqttMessage);
 });

+ 23 - 2
frontend/src/views/web/overview/index.vue

@@ -7,7 +7,7 @@
         </div>
         <div class="overview-left-content">
           <ul v-loading="alarm_loading">
-            <li v-for="(item, index) in varDictData" :key="index">
+            <li v-for="(item, index) in varDictData" :key="index" class="alarm-item" @click="handleItemClick(item)">
               <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>
@@ -182,7 +182,13 @@ const getVarDictData = async () => {
     alarm_loading.value = false
   }
 }
-
+const handleItemClick = (item: BizVarDictTable) => {
+  if (receiveData) {
+    receiveData({ craneName: item.crane_name ?? '', isShowHomeButton: true });
+  }
+  localStorage.setItem('craneInfo', JSON.stringify(item))
+  router.push('/detail/realtimeAlarm');
+}
 // 颜色样式处理
 const getColor = (type: string) => {
   switch (type) {
@@ -371,6 +377,21 @@ onUnmounted(() => {
   }
 }
 
+/* 列表项基础样式 */
+.alarm-item {
+  cursor: pointer; /* 鼠标悬浮显示手型,提升交互体验 */
+  transition: background-color 0.2s ease; /* 过渡动画,让遮罩效果更顺滑 */
+  border-radius: 4px; /* 可选:圆角,让样式更美观 */
+  margin-bottom: 8px; /* 可选:行间距 */
+}
+
+/* 鼠标悬浮的透明遮罩效果 */
+.alarm-item:hover .content-item {
+  background-color: rgba(0, 123, 255, 0.1); /* 淡蓝色透明遮罩,可自定义颜色和透明度 */
+  /* 可选:添加边框高亮 */
+  /* border: 1px solid rgba(0, 123, 255, 0.3); */
+}
+
 .el-table-empty {
   width: 100%;
   height: 100%;