Bläddra i källkod

历史曲线页面和接口开发

cuiHe 1 vecka sedan
förälder
incheckning
caab380a8b

+ 87 - 3
backend/app/api/v1/module_business/vardict/service.py

@@ -563,15 +563,23 @@ 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}
+        var_dict_search_dict = {'crane_no':search.crane_no,'data_type':search.data_type,'mec_type':search.mec_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():
+        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}')")
+
         # 4. 过滤条件2:created_time时间范围(新增核心逻辑)
         created_time = search.created_time
         if created_time and isinstance(created_time, tuple) and len(created_time) == 2:
@@ -594,7 +602,10 @@ class BizVarDictService:
         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}"
+        if page_size == 1000: #历史曲线用
+            query_sql = f"{base_sql}{where_clause} ORDER BY ts DESC"
+        else:
+            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
@@ -609,6 +620,79 @@ class BizVarDictService:
                         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
+        }
+
+    @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;
+        """
+
+        rest_result = await tdengine_rest_query(base_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,

+ 47 - 6
frontend/src/components/base-search/index.vue

@@ -1,6 +1,12 @@
 <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>
+      <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>
       <div class="time" v-if="props.isShowDate">
         <el-date-picker 
           v-model="dateValue.arr"
@@ -16,7 +22,7 @@
       <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>
+      <el-button v-if="props.isShowExport" style="float: right;" :icon="Download" type="primary" @click="exportToExcel">导出</el-button>
     </div>
   </div>
 </template>
@@ -24,6 +30,15 @@
 <script setup lang="ts">
 import { watchEffect, reactive, ref } from 'vue'
 import { Filter, RefreshRight, Download } from '@element-plus/icons-vue'
+
+// 定义选项项的类型
+interface OptionItem {
+  value: string | number
+  label: string
+}
+
+const sectValue = ref('')
+
 let dateValue: any = reactive({
   arr: []
 })
@@ -32,13 +47,39 @@ const props = defineProps({
     type: Boolean,
     default: true
   },
+  isShowExport:{
+    type: Boolean,
+    default: true
+  },
   dateValue: {
     type: Array,
     default: () => ['', '']
+  },  
+  isShowSect: {
+    type: Boolean,
+    default: true
+  },  
+  sectName: {
+    type: String,
+    default: ''
+  },
+  sectValue: {
+    type: String,
+    default: ''
+  },
+  sectOption: {
+    type: Array as () => OptionItem[],
+    default: () => [
+      {
+        value: '',
+        label: '',
+      },
+    ]
   }
 })
 watchEffect(() => {
   dateValue.arr = props.dateValue
+  sectValue.value = props.sectValue
 })
 const onDateChange = (v: any) => {
   if (v) {
@@ -52,16 +93,16 @@ const onDateChange = (v: any) => {
 }
 const emit = defineEmits(['on-search', 'clear', 'reset', 'update-dateValue', 'dateChange', 'exportToExcel'])
 const reset = () => {
+  sectValue.value = ''
   dateValue.arr = []
   emit('clear')
   emit('reset')
 }
 const onSearch = () => {
-  if (props.isShowDate) {
-    emit('on-search', {
-      dateValue: dateValue.arr
-    })
-  }
+  emit('on-search', {
+    dateValue: dateValue.arr,
+    sectValue: sectValue.value
+  })
 }
 
 const exportToExcel = () => {

+ 502 - 8
frontend/src/views/web/detail/historyCurve.vue

@@ -1,12 +1,506 @@
 <template>
-    这是历史曲线
+  <div>
+    <div class="realtimeCurve-content">
+      <div>
+          <base-search :isShowDate="true" :dateValue="date.arr" :isShowExport="false"
+          @on-search="search" @reset="reset"></base-search>
+      </div>
+      <div v-loading="loading" :style="{ height: height }" class="echarts-content">
+        <div v-if="allData.length != 0 && historyItems.length != 0" ref="chartRef"></div>
+        <div v-if="allData.length === 0 || historyItems.length === 0" class="el-table-empty">
+          <el-image :src="emptybg"></el-image>
+          <span>当前无数据</span>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
 
-  </template>
-  
-  <script setup lang="ts">
+<script setup lang="ts">
+import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue';
+import * as echarts from 'echarts';
+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";
+// 移除MQTT相关导入
+const emptybg = ref(emptybgUrl)
 
-  </script>
-  
-  <style lang="less" scoped>
+// 起重机信息
+const craneInfo = JSON.parse(localStorage.getItem('craneInfo') || '{}');
+const loading = ref(true);
+const height = ref('calc(100vh - 70px - 5px - 50px - 10px - 44px - 50px)')
+// ========== 新增:时间选择相关 ==========
+const timeDialogVisible = ref(false);
 
-  </style>
+const date = reactive({
+  arr: [dayjs(dayjs().subtract(1, 'hour')).format('YYYY-MM-DD HH:mm:ss'), dayjs().format('YYYY-MM-DD HH:mm:ss')]
+})
+
+// 查询参数:修改created_time为数组类型(适配时间范围查询)
+const queryFormVarDictData = reactive<BizVarDictPageQuery>({
+  page_no: 1,
+  page_size: 1000,
+  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,
+});
+
+// 变量数据存储
+const allData = ref<BizVarDictTable[]>([]);
+const historyItems = ref<any[]>([]);
+
+// ========== ECharts核心配置(样式不变) ==========
+const chartRef = ref<HTMLDivElement | null>(null);
+let chartInstance: ECharts | null = null;
+const gridHeight = 150; // 每组Grid高度(px)
+const gridGap = 50; // 组间间距
+const gridTopBase = 80; // 顶部基础间距
+
+// 单条曲线数据结构
+interface SeriesDataItem {
+  name: string; // 变量名(var_name)
+  code: string; // 变量编码
+  data: [number, number][]; // 时间戳+值
+}
+const seriesData = ref<SeriesDataItem[]>([]);
+
+const search = (v:any) => {
+  date.arr = v.dateValue
+  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')]
+  getData();
+}
+
+// ========== 数据加载:先加载变量列表,再加载历史数据 ==========
+const getData = async () => {
+  loading.value = true;
+  try {
+    // 1. 加载变量列表(原有逻辑不变)
+    const varResponse = await BizVarDictAPI.listBizVarDictAnalog(queryFormVarDictData);
+    allData.value = varResponse.data.data || [];
+    
+    if (allData.value.length === 0) {
+      loading.value = false;
+      return;
+    }
+
+    // 2. 初始化曲线结构
+    initMultiGridSeries();
+    
+    // 3. 加载历史数据
+    await loadHistoryData();
+  } catch (error) {
+    console.error('数据加载失败:', error);
+    loading.value = false;
+  }
+};
+
+// ========== 加载历史数据(核心改造) ==========
+const loadHistoryData = async () => {
+  if (allData.value.length === 0) return;
+
+  try {
+    // 请求历史数据接口
+    queryFormVarDictData.created_time = [formatToDateTime(date.arr[0]), formatToDateTime(date.arr[1])]
+    const historyResponse = await BizVarDictAPI.historyData(queryFormVarDictData);
+    historyItems.value = historyResponse.data.data.items || [];
+
+    // 初始化所有变量的历史数据为空
+    seriesData.value.forEach(item => {
+      item.data = [];
+    });
+
+    // 遍历历史数据,按var_code匹配填充
+    historyItems.value.forEach((item: any) => {
+      const { var_name, var_code, val, ts } = item;
+      if (!var_code || ts === undefined || val === undefined) return;
+
+      const targetSeries = seriesData.value.find(series => series.code === var_code);
+      if (targetSeries) {
+        const timeStamp = dayjs(ts).valueOf(); 
+        targetSeries.data.push([timeStamp, Number(val)]);
+      }
+    });
+
+    // 初始化ECharts并渲染
+    nextTick(() => {
+      initEchartsWithMultiGrid();
+    });
+  } catch (error) {
+    console.error('历史数据加载失败:', error);
+  } finally {
+    loading.value = false;
+  }
+};
+
+// ========== 多Grid初始化(样式逻辑不变) ==========
+const initMultiGridSeries = () => {
+  if (allData.value.length === 0) {
+    console.warn('无变量数据,无法生成曲线');
+    return;
+  }
+
+  // 为每个变量初始化曲线数据结构
+  seriesData.value = allData.value.map(item => ({
+    name: item.var_name || '未知变量',
+    code: item.var_code || '',
+    data: [],
+  }));
+};
+
+// ========== ECharts初始化(样式逻辑不变,移除MQTT相关) ==========
+const initEchartsWithMultiGrid = () => {
+  if (!chartRef.value || !seriesData.value.length) return;
+
+  // 销毁旧实例
+  if (chartInstance) {
+    chartInstance.dispose();
+    chartInstance = null;
+  }
+
+  // 创建ECharts实例
+  chartInstance = echarts.init(chartRef.value);
+
+  // 计算Grid数量和总高度
+  const gridCount = seriesData.value.length;
+  const totalChartHeight = gridTopBase + gridCount * (gridHeight + gridGap);
+
+  // 1. 构建多Grid配置
+  const grids = seriesData.value.map((_, index) => ({
+    id: `grid_${index}`,
+    left: '0%',
+    right: '2%',
+    top: gridTopBase + index * (gridHeight + gridGap),
+    height: gridHeight,
+    width: '98%',
+    containLabel: true,
+    padding: [10, 20, 20, 20],
+    show: true
+  }));
+
+  // 2. 构建多X轴配置
+  const xAxes: any[] = seriesData.value.map((_, index) => ({
+    gridIndex: index,
+    type: 'time',
+    show: true,
+    axisLabel: {
+      interval: 'auto',
+      formatter: (value: number) => dayjs(value).format('YYYY/MM/DD HH:mm:ss'),
+      fontSize: 12,
+      color: '#506388',
+      margin: 10
+    },
+    splitLine: { show: false },
+    alignTicks: true
+  }));
+
+  // 3. 构建多Y轴配置
+  const yAxes: any[] = seriesData.value.map((item, index) => ({
+    gridIndex: index,
+    type: 'value',
+    show: true,
+    name: item.name,
+    nameTextStyle: {
+      fontSize: 14,
+      color: '#fff'
+    },
+    nameGap: 20,
+    axisLabel: {
+      formatter: (value: any) => `{fixedWidth|${value}}`,
+      rich: {
+        fixedWidth: {
+          width: 80,
+          textAlign: 'right',
+          backgroundColor: 'transparent',
+          fontSize: 14,
+        }
+      },
+      color: '#506388',
+      margin: 20,
+      overflow: 'hidden',
+      textOverflow: 'ellipsis'
+    },
+    splitLine: {
+      show: true,
+      lineStyle: {
+        color: '#3E5487',
+        width: 1
+      }
+    },
+    position: 'left'
+  }));
+
+  // 4. 构建多Series配置
+  const series: any[] = seriesData.value.map((item, index) => {
+    const curveColor = getColor(index);
+    return {
+      name: item.name,
+      type: 'line',
+      smooth: true,
+      symbol: 'none',
+      gridIndex: index,
+      xAxisIndex: index,
+      yAxisIndex: index,
+      data: item.data,
+      color: curveColor,
+      lineStyle: { color: curveColor, width: 2 },
+      itemStyle: { color: curveColor },
+      areaStyle: {
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+          { offset: 0, color: curveColor + 'cc' },
+          { offset: 1, color: curveColor + '11' }
+        ])
+      }
+    };
+  });
+
+  // ECharts最终配置
+  const option: EChartsOption = {
+    alignTicks: true,
+    legend: {
+      show: true,
+      top: 20,
+      left: 'center',
+      data: seriesData.value.map(item => item.name),
+      textStyle: {
+        fontSize: 14,
+        color: '#fff'
+      },
+      itemWidth: 12,
+      itemHeight: 12,
+      padding: [0, 20],
+      formatter: (name) => {
+        return name.length > 10 ? name.substring(0, 10) + '...' : name;
+      }
+    },
+    tooltip: {
+      trigger: 'axis',
+      triggerOn: 'mousemove',
+      formatter: function (params: any) {
+        if (!params || params.length === 0) return '';
+
+        const targetTime = params[0].data[0];
+        const timeStr = formatToDateTime(targetTime);
+        let tooltipHtml = `<div style="font-weight: bold; margin-bottom: 8px;font-size: 12px">时间:${timeStr}</div>`;
+
+        seriesData.value.forEach((seriesItem, idx) => {
+          const closestData = seriesItem.data.find(d => d[0] === targetTime);
+          const value = closestData ? closestData[1] : '-';
+          tooltipHtml += `
+            <div style="display: flex; align-items: center; margin: 4px 0;">
+              <span style="display: inline-block; width: 8px; height: 8px; background: ${getColor(idx)}; margin-right: 8px; border-radius: 50%;"></span>
+              <span style="font-size: 12px" >${seriesItem.name}:</span>
+              <span style="margin-left: 8px;font-size: 12px">${value}</span>
+            </div>
+          `;
+        });
+        return tooltipHtml;
+      },
+      backgroundColor: 'rgba(255,255,255,0.95)',
+      borderColor: '#e6e6e6',
+      borderWidth: 1,
+      padding: 12,
+      textStyle: { fontSize: 12 },
+    },
+    height: totalChartHeight,
+    grid: grids,
+    xAxis: xAxes,
+    yAxis: yAxes,
+    series: series,
+  };
+
+  chartInstance.setOption(option);
+
+  // 图例勾选事件(原有逻辑保留)
+  chartInstance.off('legendselectchanged');
+  chartInstance.on('legendselectchanged', (params: any) => {
+    if (!params || !params.selected) return;
+
+    const selectedLegend = params.selected;
+    const showSeriesNames = Object.entries(selectedLegend)
+      .filter(([_, isSelected]) => isSelected)
+      .map(([name]) => name);
+    const showIndices = seriesData.value
+      .map((item, index) => ({ name: item.name, index }))
+      .filter((item) => showSeriesNames.includes(item.name))
+      .map((item) => item.index);
+
+    const updatedGrids = grids.map((grid, index) => {
+      const seriesName = seriesData.value[index].name;
+      const isShow = selectedLegend[seriesName] || false;
+
+      if (!isShow) {
+        return { ...grid, show: false };
+      } else {
+        const showSortIndex = showIndices.findIndex(showIndex => showIndex === index);
+        const newTop = gridTopBase + showSortIndex * (gridHeight + gridGap);
+        return { ...grid, show: true, top: newTop };
+      }
+    });
+
+    const updatedXAxes = xAxes.map((xAxis, index) => {
+      const seriesName = seriesData.value[index].name;
+      return {
+        ...xAxis,
+        show: selectedLegend[seriesName] || false,
+        axisLine: { onZero: false },
+        scale: false
+      };
+    });
+
+    const updatedYAxes = yAxes.map((yAxis, index) => {
+      const seriesName = seriesData.value[index].name;
+      return { ...yAxis, show: selectedLegend[seriesName] || false };
+    });
+
+    const showGridCount = showIndices.length;
+    const newTotalChartHeight = gridTopBase + showGridCount * (gridHeight + gridGap);
+
+    chartInstance?.setOption({
+      grid: updatedGrids,
+      xAxis: updatedXAxes,
+      yAxis: updatedYAxes,
+      height: newTotalChartHeight
+    });
+
+    chartInstance?.resize({
+      width: 'auto',
+      height: newTotalChartHeight
+    });
+  });
+
+  // 调整图表尺寸
+  chartInstance.resize({
+    width: 'auto',
+    height: totalChartHeight,
+  });
+
+  // 窗口自适应
+  const resizeHandler = () => {
+    if (chartInstance) {
+      chartInstance.resize({
+        width: 'auto',
+        height: 'auto',
+      });
+    }
+  };
+  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 = [
+    '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FECA57',
+    '#FF9FF3', '#54A0FF', '#8E44AD', '#E74C3C', '#3498DB',
+    '#2ECC71', '#F1C40F', '#9B59B6', '#1ABC9C', '#E67E22',
+    '#34495E', '#FF7675', '#74B9FF', '#00B894', '#FDCB6E'
+  ];
+  return extendedColors[index % extendedColors.length];
+};
+
+// ========== 生命周期(移除MQTT相关) ==========
+onMounted(async () => {
+  await getData();
+});
+
+onUnmounted(() => {
+  // 销毁ECharts实例
+  if (chartInstance) {
+    chartInstance.dispose();
+    chartInstance = null;
+  }
+  // 移除resize监听
+  window.removeEventListener('resize', () => chartInstance?.resize());
+});
+
+
+</script>
+
+<style lang="less" scoped>
+.realtimeCurve-content {
+  :deep(.el-loading-parent--relative) {
+    --el-mask-color: rgba(255, 255, 255, 0.1);
+  }
+  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%;
+}
+
+.echarts-content {
+  overflow-x: hidden;
+}
+
+.el-table-empty {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  color: #8ECAFF;
+
+  img {
+    width: 200px;
+    height: auto;
+    margin-bottom: 20px;
+  }
+}
+
+::-webkit-scrollbar {
+  width: 5px;
+  height: 5px;
+}
+
+::-webkit-scrollbar-track {
+  border-radius: 10px;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 7px;
+  background-color: #798DAE;
+}
+</style>

+ 61 - 10
frontend/src/views/web/detail/historyData.vue

@@ -2,7 +2,8 @@
   <div>
     <div class="el-table-content">
       <div>
-        <base-search :isShowDate="true" :dateValue="date.arr"
+        <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>
       </div>
       <div>
@@ -29,7 +30,9 @@
 <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 dayjs from 'dayjs';
+import { useDictStore } from "@/store";
 import * as XLSX from 'xlsx';
 import { formatToDateTime } from "@/utils/dateUtil";
 
@@ -39,8 +42,10 @@ 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')]
+  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 queryFormVarDictData = reactive<BizVarDictPageQuery>({
   page_no: 1,
   page_size: 20,
@@ -64,6 +69,23 @@ const queryFormVarDictData = reactive<BizVarDictPageQuery>({
   data_type:'1'
 });
 
+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,
+})
+
+interface OptionItem {
+  value: number | string;
+  label: string;
+}
+
 const tableConfig = [
   {
     label: '点位名称',
@@ -79,9 +101,35 @@ const tableConfig = [
   }
 ]
 
+const dictStore = useDictStore()
+const dictTypes: any = [
+  'mec_type',
+]
+
+const getCraneMecData = async () => {
+  const res = await BizMecAPI.listBizMec(queryFormMecData);
+  sectOption.value.push({
+    value: '0',
+    label: '全部机构'
+  })
+  res.data.data.items.forEach(item => {
+    if(item.mec_type){
+      sectOption.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 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;
   let response = await BizVarDictAPI.historyData(queryFormVarDictData);
   allData.value = response.data.data.items
   total.value = response.data.data.total
@@ -89,26 +137,24 @@ const getData = async () => {
 };
 
 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
+  sectValue.value = v.sectValue
   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')]
+  sectValue.value = '0'
   getData();
 }
 const exportToExcel = () => {
@@ -125,9 +171,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
+        const typedItem = item as Record<string, any>;
+        row[field] = typedItem[field] ?? ''; // 空值替换为'',避免undefined
       });
       return row;
     });
@@ -152,12 +199,16 @@ const exportToExcel = () => {
     XLSX.writeFile(workbook, fileName);
 
     ElMessage.success('Excel导出成功!');
-  } catch (error) {
+  } catch (error:any) {
     ElMessage.error(`导出失败:${error.message}`);
   }
 };
 
 onMounted(async () => {
+  if (dictTypes.length > 0) {
+    await dictStore.getDict(dictTypes)
+  }
+  await getCraneMecData();
   await getData();
 });
 </script>

+ 19 - 22
frontend/src/views/web/detail/realtimeCurve.vue

@@ -1,13 +1,16 @@
 <template>
-  <div class="realtimeCurve-content">
-    <div v-loading="loading" class="echarts-content">
-      <div ref="chartRef" class="echarts-container" :style="{ height: height }"></div>
-      <div v-if="allData.length === 0" class="el-table-empty">
-        <el-image :src="emptybg"></el-image>
-        <span>当前无数据</span>
+  <div>
+    <div class="realtimeCurve-content">
+      <div v-loading="loading":style="{ height: height }" class="echarts-content">
+        <div ref="chartRef"></div>
+        <div v-if="allData.length === 0" class="el-table-empty">
+          <el-image :src="emptybg"></el-image>
+          <span>当前无数据</span>
+        </div>
       </div>
     </div>
   </div>
+
 </template>
 
 <script setup lang="ts">
@@ -22,7 +25,7 @@ const emptybg = ref(emptybgUrl)
 // 起重机信息
 const craneInfo = JSON.parse(localStorage.getItem('craneInfo') || '{}');
 const loading = ref(true);
-const height = ref('0px');
+const height = ref('calc(100vh - 70px - 5px - 50px - 10px - 44px)')
 
 // 查询参数
 const queryFormVarDictData = reactive<BizVarDictPageQuery>({
@@ -116,16 +119,15 @@ const initEchartsWithMultiGrid = () => {
 
   // 动态计算高度:根据allData数量计算总高度,赋值给height ref
   const totalChartHeight = gridTopBase + gridCount * (gridHeight + gridGap);
-  height.value = `${totalChartHeight}px`; // 绑定到容器style的height属性
 
   // 1. 构建多Grid配置
   const grids = seriesData.value.map((_, index) => ({
     id: `grid_${index}`,
-    left: '2%', // 【修改2】Grid左间距设为0,消除视觉上的左侧空白
-    right: '4%', // 【修改2】Grid右间距设为0,消除视觉上的右侧空白
+    left: '0%', // 【修改2】Grid左间距设为0,消除视觉上的左侧空白
+    right: '2%', // 【修改2】Grid右间距设为0,消除视觉上的右侧空白
     top: gridTopBase + index * (gridHeight + gridGap),
     height: gridHeight,
-    width: '94%', // 【修改2】Grid宽度设为100%,填满容器
+    width: '98%', // 【修改2】Grid宽度设为100%,填满容器
     containLabel: true,
     padding: [10, 20, 20, 20], // 微调内边距,避免标签溢出
     show: true
@@ -137,8 +139,9 @@ const initEchartsWithMultiGrid = () => {
     type: 'time',
     show: true,
     axisLabel: {
+      interval: 'auto',
       formatter: (value: number) => new Date(value).toLocaleTimeString(),
-      fontSize: 14,
+      fontSize: 12,
       color: '#506388',
       margin: 10
     },
@@ -325,7 +328,6 @@ const initEchartsWithMultiGrid = () => {
     // 【原有逻辑:重新计算图表总高度】
     const showGridCount = showIndices.length;
     const newTotalChartHeight = gridTopBase + showGridCount * (gridHeight + gridGap);
-    height.value = `${newTotalChartHeight}px`;
 
     // 【核心步骤3:更新ECharts配置,应用新的grid位置和尺寸】
     chartInstance?.setOption({
@@ -458,19 +460,14 @@ onUnmounted(() => {
   :deep(.el-loading-parent--relative) {
     --el-mask-color: rgba(255, 255, 255, 0.1);
   }
+  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%;
 }
 
 .echarts-content {
-  width: 100%;
-  height: 100%;
   overflow-x: hidden;
-  background: linear-gradient(to bottom, rgba(20, 66, 140, 0.8) 0%, rgba(18, 31, 52, 1) 15%);
-}
-
-// ECharts容器:适配动态高度
-.echarts-container {
-  width: 100%;
-  // 高度由动态计算的height ref决定,无需额外设置
 }
 
 .el-table-empty {