Эх сурвалжийг харах

feat(views): 添加能效分析页面

wangcong 1 сар өмнө
parent
commit
f00f068f65

+ 2 - 1
src/api/index.ts

@@ -16,6 +16,7 @@ import type {
   BatchUpdate,
   BatchUpdate,
   ChangeState,
   ChangeState,
   CoolingHistoryDataQuery,
   CoolingHistoryDataQuery,
+  CoolingHistoryDataResult,
   CoolingRealTimeDataQuery,
   CoolingRealTimeDataQuery,
   CoolingRealTimeDataResult,
   CoolingRealTimeDataResult,
   CoolingStatisticsQuery,
   CoolingStatisticsQuery,
@@ -738,7 +739,7 @@ export const getCoolingRealTimeData = async (params: CoolingRealTimeDataQuery) =
 };
 };
 
 
 export const getCoolingHistoryData = async (params: CoolingHistoryDataQuery) => {
 export const getCoolingHistoryData = async (params: CoolingHistoryDataQuery) => {
-  const data = await request<CoolingRealTimeDataResult>(apiBiz('/deviceCoolingData/his'), {
+  const data = await request<CoolingHistoryDataResult[]>(apiBiz('/deviceCoolingData/his'), {
     method: 'POST',
     method: 'POST',
     body: JSON.stringify(params),
     body: JSON.stringify(params),
   });
   });

+ 9 - 0
src/constants/index.ts

@@ -146,3 +146,12 @@ export const enum CoolingDataType {
    */
    */
   ChilledWaterFlow = 4,
   ChilledWaterFlow = 4,
 }
 }
+
+/**
+ * 时间尺度
+ */
+export const enum TimeScaleType {
+  Hour,
+  Day,
+  Month,
+}

+ 1 - 0
src/i18n/locales/zh.json

@@ -332,6 +332,7 @@
     "dailyElectricityUnit": "日均用电量(kwh)",
     "dailyElectricityUnit": "日均用电量(kwh)",
     "deviceGroupName": "设备(组)名称",
     "deviceGroupName": "设备(组)名称",
     "deviceType": "设备类型",
     "deviceType": "设备类型",
+    "efficientRoomEnergyStandard": "高效机房能效标准",
     "electricityConsumption": "电量",
     "electricityConsumption": "电量",
     "electricityConsumptionGrowthRate": "电量环比增长",
     "electricityConsumptionGrowthRate": "电量环比增长",
     "electricityConsumptionUnit": "电量(kwh)",
     "electricityConsumptionUnit": "电量(kwh)",

+ 21 - 1
src/types/index.ts

@@ -6,7 +6,7 @@ import type { StepProps, UploadProps } from 'ant-design-vue';
 import type { Rule, RuleObject } from 'ant-design-vue/es/form';
 import type { Rule, RuleObject } from 'ant-design-vue/es/form';
 import type { FormLabelAlign } from 'ant-design-vue/es/form/interface';
 import type { FormLabelAlign } from 'ant-design-vue/es/form/interface';
 import type { Dayjs } from 'dayjs';
 import type { Dayjs } from 'dayjs';
-import type { CoolingDataType, DeviceRunningStatus, ProtocolConfigMethod } from '@/constants';
+import type { CoolingDataType, DeviceRunningStatus, ProtocolConfigMethod, TimeScaleType } from '@/constants';
 
 
 export interface ApiResponse<T> {
 export interface ApiResponse<T> {
   data: T;
   data: T;
@@ -1842,6 +1842,14 @@ export interface CoolingHistoryDataQuery {
   type: CoolingDataType;
   type: CoolingDataType;
 }
 }
 
 
+export interface CoolingHistoryDataResult {
+  deviceTypeName: string;
+  data: {
+    time: string;
+    [key: string]: string | number;
+  }[];
+}
+
 export interface CoolingStatisticsQuery {
 export interface CoolingStatisticsQuery {
   deviceGroupId: number;
   deviceGroupId: number;
   deviceTypes: number[];
   deviceTypes: number[];
@@ -1867,7 +1875,9 @@ export interface CoolingHisQueryVo {
 }
 }
 
 
 export interface CoolingStatisticsResult {
 export interface CoolingStatisticsResult {
+  timeScaleType: TimeScaleType;
   totalCoolingData: number;
   totalCoolingData: number;
+  groupCoolingEfficiency: number;
   tempDataList: {
   tempDataList: {
     temperature: number;
     temperature: number;
     time: string;
     time: string;
@@ -1877,6 +1887,16 @@ export interface CoolingStatisticsResult {
     time: string;
     time: string;
     valueList: Omit<CoolingDeviceValue, 'time'>[];
     valueList: Omit<CoolingDeviceValue, 'time'>[];
   }[];
   }[];
+  coolingStationDataVos: {
+    time: string;
+    coolingEfficiencyTotal: number;
+    valueList: Omit<CoolingDeviceValue, 'time'>[];
+  }[];
+  deviceTypeCoolingEfficiencyVos: {
+    deviceType: number;
+    deviceTypeName: string;
+    coolingEfficiencyTotal: number;
+  }[];
 }
 }
 export interface AutomaticMatching {
 export interface AutomaticMatching {
   autoVos: AutomaticMatchingItem[];
   autoVos: AutomaticMatchingItem[];

+ 17 - 1
src/utils/index.ts

@@ -1,7 +1,8 @@
+import dayjs from 'dayjs';
 import { kebabCase } from 'lodash-es';
 import { kebabCase } from 'lodash-es';
 
 
 import { t } from '@/i18n';
 import { t } from '@/i18n';
-import { fileContentTypeRegExp } from '@/constants';
+import { fileContentTypeRegExp, TimeScaleType } from '@/constants';
 
 
 import { fetchWithTimeout } from './fetch';
 import { fetchWithTimeout } from './fetch';
 
 
@@ -203,3 +204,18 @@ export const getTablePageSorts = (sorter: SorterResult | SorterResult[]): PageSo
     },
     },
   ];
   ];
 };
 };
+
+export const formatTimeByScale = (time: string, scale?: TimeScaleType) => {
+  switch (scale) {
+    case TimeScaleType.Hour:
+      return dayjs(time).format('HH:mm');
+    case TimeScaleType.Day:
+      return dayjs(time).format('MM-DD');
+    case TimeScaleType.Month:
+      return dayjs(time).format('YYYY-MM');
+    default:
+      return dayjs(time).format('YYYY-MM-DD HH:mm:ss');
+  }
+};
+
+export const timeSorter = (a: string, b: string) => dayjs(a).unix() - dayjs(b).unix();

+ 1075 - 0
src/views/energy-analysis/EnergyEfficiency.vue

@@ -0,0 +1,1075 @@
+<script setup lang="ts">
+import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
+import VChart from 'vue-echarts';
+import dayjs from 'dayjs';
+import { BarChart, GaugeChart, LineChart, PieChart } from 'echarts/charts';
+import { GridComponent, LegendComponent, TitleComponent, TooltipComponent } from 'echarts/components';
+import { use } from 'echarts/core';
+import { CanvasRenderer } from 'echarts/renderers';
+
+import TimeRangeSelect from '@/components/TimeRangeSelect.vue';
+import { useRequest } from '@/hooks/request';
+import { useViewVisible } from '@/hooks/view-visible';
+import { t } from '@/i18n';
+import { downloadCoolingHisData, getCoolingDataStatistics, getCoolingHistoryData, getCoolingRealTimeData } from '@/api';
+import { downloadBlob, formatTimeByScale, timeSorter } from '@/utils';
+import { CoolingDataType, TimeScaleType } from '@/constants';
+
+import chilledWaterFlowRate from '@/assets/img/chilled-water-flow-rate.png';
+import coolingCapacity from '@/assets/img/cooling-capacity.png';
+import coolingPower from '@/assets/img/cooling-power.png';
+import coolingStationEfficiency from '@/assets/img/cooling-station-efficiency.png';
+
+import { DeviceType } from '../device-work-status/device-card';
+
+import type { Dayjs } from 'dayjs';
+import type { BarSeriesOption, GaugeSeriesOption, LineSeriesOption, PieSeriesOption } from 'echarts/charts';
+import type {
+  DatasetComponentOption,
+  GridComponentOption,
+  LegendComponentOption,
+  TitleComponentOption,
+  TooltipComponentOption,
+} from 'echarts/components';
+import type { ComposeOption } from 'echarts/core';
+import type {
+  CoolingEnergyCardItem,
+  CoolingHistoryDataResult,
+  CoolingRealTimeDataResult,
+  CoolingStatisticsResult,
+} from '@/types';
+
+use([
+  CanvasRenderer,
+  BarChart,
+  GaugeChart,
+  LineChart,
+  PieChart,
+  GridComponent,
+  TooltipComponent,
+  LegendComponent,
+  TitleComponent,
+]);
+
+type EChartsOption = ComposeOption<
+  | TitleComponentOption
+  | LegendComponentOption
+  | TooltipComponentOption
+  | DatasetComponentOption
+  | GridComponentOption
+  | BarSeriesOption
+  | GaugeSeriesOption
+  | LineSeriesOption
+  | PieSeriesOption
+>;
+
+const deviceTypes = [DeviceType.冷水主机, DeviceType.冷却塔, DeviceType.冷却泵, DeviceType.冷冻泵];
+
+const { handleRequest } = useRequest();
+const coolingRTDResult = ref<CoolingRealTimeDataResult>();
+let coolingRTDTimer: number | undefined;
+
+const getCoolingRTDResult = () => {
+  clearTimeout(coolingRTDTimer);
+
+  handleRequest(async () => {
+    const now = dayjs();
+
+    coolingRTDResult.value = await getCoolingRealTimeData({
+      deviceGroupId: 7,
+      startTime: now.startOf('day').format('YYYY-MM-DD HH:mm:ss'),
+      endTime: now.endOf('day').format('YYYY-MM-DD HH:mm:ss'),
+    });
+  });
+
+  coolingRTDTimer = window.setTimeout(getCoolingRTDResult, 3600 * 1000);
+};
+
+const { handleRequest: handleCoolingHisDataRequest } = useRequest();
+const { visible, showView } = useViewVisible();
+const coolingHisResult = ref<CoolingHistoryDataResult[]>([]);
+
+watch(visible, () => {
+  if (visible.value) {
+    getCoolingHisData();
+  } else {
+    coolingHisResult.value = [];
+  }
+});
+
+const viewCoolingHisData = (energyCard: CoolingEnergyCardItem) => {
+  selectedEnergyCard.value = energyCard;
+  showView();
+};
+
+const getCoolingHisData = () => {
+  handleCoolingHisDataRequest(async () => {
+    coolingHisResult.value = await getCoolingHistoryData({
+      deviceGroupId: 7,
+      deviceTypes,
+      startTime: '2025-04-01 00:00:00',
+      endTime: '2025-05-01 23:00:00',
+      type: selectedEnergyCard.value?.type as CoolingDataType,
+    });
+  });
+};
+
+const coolingHisDataOption = computed<EChartsOption>(() => {
+  const unitText = selectedEnergyCard.value?.unit ?? '-';
+  const legendData = coolingHisResult.value.map((device) => device.deviceTypeName);
+
+  const allTimesSet = new Set<string>();
+
+  coolingHisResult.value.forEach((device) => {
+    device.data.forEach((point) => allTimesSet.add(point.time));
+  });
+
+  const times: string[] = Array.from(allTimesSet).sort(timeSorter);
+
+  const seriesData = coolingHisResult.value.map<LineSeriesOption>((device) => {
+    const valueKey = Object.keys(device.data[0] ?? {}).find((key) => key !== 'time');
+    const timeMap: Record<string, string | number> = {};
+
+    if (valueKey) {
+      device.data.reduce((acc, point) => {
+        acc[point.time] = point[valueKey];
+        return acc;
+      }, timeMap);
+    }
+
+    return {
+      name: device.deviceTypeName,
+      type: 'line',
+      data: times.map((time) => timeMap[time] ?? '-'),
+    };
+  });
+
+  return {
+    title: [
+      {
+        text: `${t('common.unit')}: ${unitText}`,
+        textStyle: {
+          color: '#999',
+          fontSize: 12,
+          fontWeight: 400,
+          lineHeight: 17,
+        },
+        left: 20,
+        top: 0,
+      },
+    ],
+    tooltip: {
+      trigger: 'axis',
+      formatter(params) {
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        const tempParms = params as any[];
+        const time = dayjs(tempParms[0].axisValue).format('MM-DD HH:mm');
+
+        let tableHtml = '<table class="echarts-tooltip-electricity">';
+        tableHtml += `<tr><th>${time}</th></tr>`;
+
+        tempParms.forEach((param) => {
+          tableHtml += `<tr><td>${param.marker}${param.seriesName}</td><td>${param.value}${unitText}</td></tr>`;
+        });
+
+        tableHtml += '</table>';
+        return tableHtml;
+      },
+    },
+    legend: {
+      orient: 'horizontal',
+      bottom: 0,
+      textStyle: {
+        color: '#000',
+        fontSize: 12,
+      },
+      itemWidth: 22,
+      itemHeight: 8,
+      itemGap: 24,
+      data: legendData,
+    },
+    xAxis: {
+      type: 'category',
+      boundaryGap: true,
+      axisLine: {
+        lineStyle: {
+          type: 'dashed',
+          color: '#EBEBEB',
+        },
+      },
+      axisLabel: {
+        color: '#999',
+        fontSize: 10,
+        formatter(value) {
+          return dayjs(value).format('MM-DD HH:mm');
+        },
+      },
+      axisTick: {
+        show: false,
+      },
+      data: times,
+    },
+    yAxis: {
+      type: 'value',
+      axisLabel: {
+        color: '#999',
+        fontSize: 10,
+      },
+      splitLine: {
+        lineStyle: {
+          type: 'dashed',
+          color: '#EBEBEB',
+        },
+      },
+    },
+    series: seriesData,
+    grid: {
+      left: 0,
+      right: 0,
+      bottom: 38,
+      top: 25,
+      containLabel: true,
+    },
+    color: ['#4ECDC4', '#5470C6', '#36A2EB', '#FFC107'],
+  };
+});
+
+onMounted(() => {
+  getCoolingRTDResult();
+  getEnergyConsumeResult();
+});
+
+onUnmounted(() => {
+  clearTimeout(coolingRTDTimer);
+});
+
+const energyEfficiencyResult = ref<CoolingStatisticsResult>();
+
+const getEnergyConsumeResult = () => {
+  handleRequest(async () => {
+    energyEfficiencyResult.value = await getCoolingDataStatistics({
+      deviceGroupId: 7,
+      deviceTypes,
+      startTime: '2025-04-16 00:00:00',
+      endTime: '2025-04-16 23:00:00',
+    });
+
+    energyEfficiencyResult.value.hisQueryVos.forEach((item) => {
+      item.groupName = item.deviceTypeName + t('common.group');
+    });
+
+    if (energyEfficiencyResult.value.timeScaleType !== TimeScaleType.Hour) {
+      energyEfficiencyResult.value.hisCoolingDataVos.forEach((item) => {
+        item.time = formatTimeByScale(item.time);
+      });
+    }
+  });
+};
+
+const selectedEnergyCard = ref<CoolingEnergyCardItem>();
+
+const energyCardList = computed<CoolingEnergyCardItem[]>(() => {
+  return [
+    {
+      type: CoolingDataType.ColdStationEfficiency,
+      value: coolingRTDResult.value?.coolingStationEnergyEfficiency ?? '-',
+      unit: 'kw/kw',
+      description: t('energyAnalysis.coolingStationEfficiency'),
+      icon: coolingStationEfficiency,
+      bgColor: '#FFF6E6',
+    },
+    {
+      type: CoolingDataType.CoolingCapacity,
+      value: coolingRTDResult.value?.coolingStationCoolingData ?? '-',
+      unit: 'kw',
+      description: t('energyAnalysis.coolingStationCoolingCapacity'),
+      icon: coolingCapacity,
+      bgColor: '#EFF1FE',
+    },
+    {
+      type: CoolingDataType.Power,
+      value: coolingRTDResult.value?.coolingStationOutputActivePower ?? '-',
+      unit: 'kw',
+      description: t('energyAnalysis.coolingStationCoolingPower'),
+      icon: coolingPower,
+      bgColor: 'var(--antd-color-primary-opacity-15)',
+    },
+    {
+      type: CoolingDataType.ChilledWaterFlow,
+      value: coolingRTDResult.value?.coolingStationWaterFlow ?? '-',
+      unit: 'm³/h',
+      description: t('energyAnalysis.coolingStationChilledWaterFlowRate'),
+      icon: chilledWaterFlowRate,
+      bgColor: 'rgba(104, 194, 58, 0.12)',
+    },
+  ];
+});
+
+interface SubEfficiencyItem {
+  label: string;
+  value: string | number;
+  color: string;
+}
+
+const subItemEfficiency = computed<SubEfficiencyItem[]>(() => {
+  const devEfficiencyMap: Partial<Record<DeviceType, string | number>> = {};
+
+  energyEfficiencyResult.value?.deviceTypeCoolingEfficiencyVos.forEach((item) => {
+    devEfficiencyMap[item.deviceType as DeviceType] = item.coolingEfficiencyTotal;
+  });
+
+  return [
+    {
+      label: t('energyAnalysis.chilledWaterChillerGroup'),
+      value: devEfficiencyMap[DeviceType.冷水主机] ?? '-',
+      color: '#32BAC0',
+    },
+    {
+      label: t('energyAnalysis.refrigerationPumpGroup'),
+      value: devEfficiencyMap[DeviceType.冷冻泵] ?? '-',
+      color: '#5B8FF9',
+    },
+    {
+      label: t('energyAnalysis.coolingPumpGroup'),
+      value: devEfficiencyMap[DeviceType.冷却泵] ?? '-',
+      color: '#5D7092',
+    },
+    {
+      label: t('energyAnalysis.coolingTowerGroup'),
+      value: devEfficiencyMap[DeviceType.冷却塔] ?? '-',
+      color: '#F6BD16',
+    },
+  ];
+});
+
+const gaugeOption = computed<EChartsOption>(() => {
+  const value = energyEfficiencyResult.value?.groupCoolingEfficiency ?? 0;
+  const progressColor = '#32BAC0';
+
+  return {
+    series: [
+      {
+        type: 'gauge',
+        startAngle: 210,
+        endAngle: -30,
+        radius: '80%',
+        min: 0,
+        max: 10,
+        itemStyle: {
+          color: progressColor,
+        },
+        progress: {
+          show: true,
+          width: 15,
+        },
+        axisLine: {
+          lineStyle: {
+            width: 15,
+            color: [[1, '#E4E7ED']],
+          },
+        },
+        pointer: {
+          icon: 'path://M12.8,0.7l12,40.1H0.7L12.8,0.7z',
+          length: '8%',
+          width: 12,
+          offsetCenter: [0, '-63%'],
+          itemStyle: {
+            color: progressColor,
+          },
+        },
+        axisTick: {
+          length: 8,
+          lineStyle: {
+            color: 'auto',
+            width: 2,
+          },
+        },
+        splitLine: {
+          length: 8,
+          lineStyle: {
+            color: 'auto',
+            width: 2,
+          },
+        },
+        axisLabel: {
+          show: false,
+        },
+        title: {
+          offsetCenter: [0, '5%'],
+          fontSize: 14,
+          lineHeight: 22,
+        },
+        detail: {
+          color: '#000',
+          fontSize: 30,
+          lineHeight: 36,
+          offsetCenter: [0, '-20%'],
+          valueAnimation: true,
+          formatter(value: number) {
+            return value.toFixed(2);
+          },
+        },
+        data: [
+          {
+            value,
+            name: 'kw/kw',
+          },
+        ],
+      },
+      {
+        type: 'gauge',
+        startAngle: 210,
+        endAngle: -30,
+        radius: '47%',
+        min: 0,
+        max: 10,
+        itemStyle: {
+          color: progressColor,
+        },
+        progress: {
+          show: true,
+          width: 3,
+        },
+        axisLine: {
+          lineStyle: {
+            width: 3,
+            color: [[1, '#E4E7ED']],
+          },
+        },
+        pointer: {
+          show: false,
+        },
+        axisTick: {
+          show: false,
+        },
+        splitLine: {
+          show: false,
+        },
+        axisLabel: {
+          show: false,
+        },
+        title: {
+          show: false,
+        },
+        detail: {
+          show: false,
+        },
+        data: [
+          {
+            value,
+          },
+        ],
+      },
+    ],
+  };
+});
+
+const energyEfficiencyLineOption = computed<EChartsOption>(() => {
+  const timeScaleType = energyEfficiencyResult.value?.timeScaleType;
+  const hisQueryVos = energyEfficiencyResult.value?.hisQueryVos || [];
+  const coolingStationDataVos = energyEfficiencyResult.value?.coolingStationDataVos || [];
+  const legendData = [t('energyAnalysis.coolingStation'), ...hisQueryVos.map((item) => item.groupName as string)];
+
+  const allTimesSet = new Set<string>();
+
+  hisQueryVos.forEach((item) => {
+    item.valueList.forEach((value) => {
+      allTimesSet.add(value.time);
+    });
+  });
+
+  coolingStationDataVos.forEach((item) => {
+    allTimesSet.add(item.time);
+  });
+
+  const times: string[] = Array.from(allTimesSet).sort(timeSorter);
+
+  return {
+    title: {
+      text: `${t('energyAnalysis.energyEfficiency')}(kw/kw)`,
+      textStyle: {
+        color: '#999',
+        fontSize: 10,
+        fontWeight: 400,
+        lineHeight: 14,
+      },
+      left: '4%',
+      top: '9%',
+    },
+    tooltip: {
+      trigger: 'axis',
+      formatter(params) {
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        const tempParms = params as any[];
+        const time = formatTimeByScale(tempParms[0].axisValue, timeScaleType);
+
+        let tableHtml = '<table class="echarts-tooltip-electricity">';
+        tableHtml += `<tr><th>${time}</th></tr>`;
+
+        const isEfficientRoomExisted = tempParms.find(
+          (param) => param.seriesName === t('energyAnalysis.efficientRoomEnergyStandard'),
+        );
+
+        if (isEfficientRoomExisted) {
+          tableHtml += `<tr><td>${t('energyAnalysis.efficientRoomEnergyStandard')}</td><td>5.00kw/kw</td></tr>`;
+        }
+
+        tempParms.forEach((param) => {
+          if (param.seriesName === t('energyAnalysis.efficientRoomEnergyStandard')) {
+            return;
+          }
+
+          tableHtml += `<tr><td>${param.marker}${param.seriesName}</td><td>${param.value}kw/kw</td></tr>`;
+        });
+
+        tableHtml += '</table>';
+        return tableHtml;
+      },
+    },
+    legend: {
+      orient: 'horizontal',
+      bottom: 0,
+      textStyle: {
+        color: '#000',
+        fontSize: 12,
+      },
+      itemWidth: 22,
+      itemHeight: 8,
+      itemGap: 24,
+      data: [
+        ...legendData,
+        {
+          name: t('energyAnalysis.efficientRoomEnergyStandard'),
+          icon: 'path://M128 501.333v64H0v-64zm213.333 0v64h-128v-64zm213.334 0v64h-128v-64zm234.666 0v64h-128v-64z',
+        },
+      ],
+    },
+    xAxis: {
+      type: 'category',
+      boundaryGap: true,
+      axisLine: {
+        lineStyle: {
+          type: 'dashed',
+          color: '#EBEBEB',
+        },
+      },
+      axisLabel: {
+        color: '#999',
+        fontSize: 10,
+        formatter(value) {
+          return formatTimeByScale(value, timeScaleType);
+        },
+      },
+      axisTick: {
+        show: false,
+      },
+      data: times,
+    },
+    yAxis: {
+      type: 'value',
+      axisLabel: {
+        color: '#999',
+        fontSize: 10,
+      },
+      splitLine: {
+        lineStyle: {
+          type: 'dashed',
+          color: '#EBEBEB',
+        },
+      },
+    },
+    series: [
+      {
+        name: t('energyAnalysis.coolingStation'),
+        type: 'line',
+        data: times.map((time) => {
+          const value = coolingStationDataVos.find((item) => item.time === time);
+          return value?.coolingEfficiencyTotal ?? '-';
+        }),
+        symbolSize: 6,
+      },
+      ...hisQueryVos.map<LineSeriesOption>((item) => ({
+        name: item.groupName as string,
+        type: 'line',
+        data: times.map((time) => {
+          const value = item.valueList.find((value) => value.time === time);
+          return value?.coolingEfficiency ?? '-';
+        }),
+        symbolSize: 6,
+      })),
+      {
+        name: t('energyAnalysis.efficientRoomEnergyStandard'),
+        type: 'line',
+        data: new Array(times.length).fill(5),
+        symbolSize: 6,
+        lineStyle: {
+          type: 'dashed',
+          width: 1,
+        },
+      },
+    ],
+    grid: {
+      left: '4%',
+      right: 0,
+      bottom: 46,
+      top: '17.5%',
+      containLabel: true,
+    },
+    color: ['#4ECDC4', '#5470C6', '#36A2EB', '#FFC107', '#FF6384'],
+  };
+});
+
+const pieOption = computed<EChartsOption>(() => {
+  const legendData = energyEfficiencyResult.value?.hisQueryVos.map((item) => item.groupName as string);
+  const seriesData = energyEfficiencyResult.value?.hisQueryVos.map((item) => ({
+    name: item.groupName as string,
+    value: item.billTotal,
+  }));
+  let centerValue = 0;
+
+  seriesData?.forEach((item) => {
+    centerValue += item.value;
+  });
+
+  return {
+    tooltip: {
+      trigger: 'item',
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      formatter(params: any) {
+        return `${params.marker}${params.name}: ${params.value}${t('energyAnalysis.coolingCapacityUnit')}`;
+      },
+    },
+    legend: {
+      orient: 'horizontal',
+      bottom: 0,
+      textStyle: {
+        color: '#000',
+        fontSize: 12,
+      },
+      itemWidth: 8,
+      itemHeight: 8,
+      itemGap: 24,
+      data: legendData,
+    },
+    series: [
+      {
+        type: 'pie',
+        radius: ['40%', '65%'],
+        avoidLabelOverlap: true,
+        label: {
+          show: true,
+          position: 'outside',
+          formatter: '{d}%',
+        },
+        labelLine: {
+          show: true,
+        },
+        data: seriesData,
+        center: ['50%', '50%'],
+      },
+      {
+        type: 'pie',
+        radius: ['0', '30%'],
+        itemStyle: {
+          color: 'transparent',
+        },
+        tooltip: {
+          show: false,
+        },
+        label: {
+          position: 'center',
+          formatter: `{value|${centerValue}}\n{title|${t('energyAnalysis.coolingCapacityUnit')}}`,
+          rich: {
+            value: {
+              fontSize: 30,
+              fontWeight: 'bold',
+              color: '#000',
+              lineHeight: 36,
+            },
+            title: {
+              fontSize: 14,
+              color: '#666',
+              lineHeight: 22,
+            },
+          },
+        },
+        data: [{ value: 1, name: '' }],
+      },
+    ],
+    color: ['#4ECDC4', '#5470C6', '#36A2EB', '#FFC107'],
+  };
+});
+
+const lineOption = computed<EChartsOption>(() => {
+  const timeScaleType = energyEfficiencyResult.value?.timeScaleType;
+  const hisQueryVos = energyEfficiencyResult.value?.hisCoolingDataVos || [];
+  const tempDataList = energyEfficiencyResult.value?.tempDataList || [];
+  const coolingData: Array<number | string> = [];
+  const coolingBill: Array<number | string> = [];
+  const outdoorTempData: Array<number | string> = [];
+
+  const allTimesSet = new Set<string>();
+  hisQueryVos.forEach((item) => allTimesSet.add(item.time));
+  tempDataList.forEach((item) => allTimesSet.add(item.time));
+
+  const times: string[] = Array.from(allTimesSet).sort(timeSorter);
+
+  times.forEach((time) => {
+    const coolingItem = hisQueryVos.find((item) => item.time === time);
+    const tempItem = tempDataList.find((item) => item.time === time);
+    coolingData.push(coolingItem?.valueList[0]?.coolingData ?? '-');
+    coolingBill.push(coolingItem?.valueList[0]?.bill ?? '-');
+    outdoorTempData.push(tempItem?.temperature ?? '-');
+  });
+
+  // 计算 Y 轴的分割段数
+  const yAxisSegments = 6;
+  const validCoolingData = coolingData.filter((value) => typeof value === 'number');
+  const validCoolingBill = coolingBill.filter((value) => typeof value === 'number');
+  const coolingDataMax =
+    validCoolingData.length > 0 ? Math.ceil(Math.max(...validCoolingData) / yAxisSegments) * yAxisSegments : 0;
+  const coolingBillMax =
+    validCoolingBill.length > 0 ? Math.ceil(Math.max(...validCoolingBill) / yAxisSegments) * yAxisSegments : 0;
+
+  return {
+    title: [
+      {
+        text: `${t('energyAnalysis.coolingCapacity')}(GJ)`,
+        textStyle: {
+          color: '#999',
+          fontSize: 10,
+          fontWeight: 400,
+          lineHeight: 14,
+        },
+        left: '5%',
+        top: '9%',
+      },
+      {
+        text: `${t('energyAnalysis.coolingPricePerUnit')}(${t('energyAnalysis.coolingCapacityUnit')})`,
+        textStyle: {
+          color: '#999',
+          fontSize: 10,
+          fontWeight: 400,
+          lineHeight: 14,
+        },
+        right: '1%',
+        top: '9%',
+      },
+    ],
+    tooltip: {
+      trigger: 'axis',
+      formatter(params) {
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        const tempParms = params as any[];
+        let tooltipStr = '<table class="echarts-tooltip-electricity">';
+        let currentTime = formatTimeByScale(tempParms[0].name, timeScaleType);
+
+        if (timeScaleType === TimeScaleType.Hour) {
+          currentTime += ` - ${currentTime.split(':')[0]}:59`; // 若时间尺度为小时,则显示为 01:00 - 01:59
+        }
+
+        tooltipStr += `<tr><th>${currentTime}</th></tr>`;
+
+        tempParms.forEach((param) => {
+          const { seriesName } = param;
+          let unit = '';
+
+          if (seriesName === t('energyAnalysis.coolingCapacity')) {
+            unit = 'GJ';
+          } else if (seriesName === t('energyAnalysis.coolingPricePerUnit')) {
+            unit = t('energyAnalysis.coolingCapacityUnit');
+          } else if (seriesName === t('envMonitor.outdoorTemperature')) {
+            unit = '℃';
+          }
+
+          tooltipStr += `<tr><td>${param.marker}${param.seriesName}</td><td>${param.value}${unit}</td></tr>`;
+        });
+
+        tooltipStr += '</table>';
+        return tooltipStr;
+      },
+    },
+    legend: {
+      orient: 'horizontal',
+      bottom: 0,
+      textStyle: {
+        color: '#000',
+        fontSize: 12,
+      },
+      itemWidth: 22,
+      itemHeight: 8,
+      itemGap: 24,
+      data: [
+        {
+          name: t('energyAnalysis.coolingCapacity'),
+          icon: 'path://M0 0h8v8h-8z',
+        },
+        t('energyAnalysis.coolingPricePerUnit'),
+        t('envMonitor.outdoorTemperature'),
+      ],
+    },
+    xAxis: {
+      type: 'category',
+      boundaryGap: true,
+      axisLine: {
+        lineStyle: {
+          type: 'dashed',
+          color: '#EBEBEB',
+        },
+      },
+      axisLabel: {
+        color: '#999',
+        fontSize: 10,
+        formatter(value) {
+          return formatTimeByScale(value, timeScaleType);
+        },
+      },
+      axisTick: {
+        show: false,
+      },
+      data: times,
+    },
+    yAxis: [
+      {
+        type: 'value',
+        position: 'left',
+        min: 0,
+        max: coolingDataMax,
+        interval: coolingDataMax / yAxisSegments,
+        axisLabel: {
+          color: '#999',
+          fontSize: 10,
+        },
+        splitLine: {
+          lineStyle: {
+            type: 'dashed',
+            color: '#EBEBEB',
+          },
+        },
+      },
+      {
+        type: 'value',
+        position: 'right',
+        min: 0,
+        max: coolingBillMax,
+        interval: coolingBillMax / yAxisSegments,
+        axisLabel: {
+          color: '#999',
+          fontSize: 10,
+        },
+        splitLine: {
+          lineStyle: {
+            type: 'dashed',
+            color: '#EBEBEB',
+          },
+        },
+      },
+    ],
+    series: [
+      {
+        name: t('energyAnalysis.coolingCapacity'),
+        type: 'bar',
+        data: coolingData,
+        barWidth: 24,
+      },
+      {
+        name: t('energyAnalysis.coolingPricePerUnit'),
+        type: 'line',
+        yAxisIndex: 1,
+        data: coolingBill,
+        symbolSize: 6,
+      },
+      {
+        name: t('envMonitor.outdoorTemperature'),
+        type: 'line',
+        data: outdoorTempData,
+        symbolSize: 6,
+      },
+    ],
+    grid: {
+      left: '4%',
+      right: 0,
+      bottom: 46,
+      top: '17.5%',
+      containLabel: true,
+    },
+    color: ['#4ECDC4', '#5470C6', '#36A2EB', '#FFC107'],
+  };
+});
+
+const exportData = async () => {
+  const fileName = t('energyAnalysis.energyEfficiencyAnalysis') + ' - ' + dayjs().format('YYYYMMDDHHmmss');
+  const file = await downloadCoolingHisData({
+    deviceGroupId: 7,
+    deviceTypes,
+    startTime: '2025-04-01 00:00:00',
+    endTime: '2025-05-01 23:00:00',
+  });
+
+  downloadBlob(file, fileName);
+};
+
+const handleTimeChange = (range: [Dayjs, Dayjs]) => {
+  console.log(range[0].format('YYYY-MM-DD HH:mm:ss'), range[1].format('YYYY-MM-DD HH:mm:ss'));
+};
+</script>
+
+<template>
+  <ACard :title="$t('energyAnalysis.realTimeValue')" :bordered="false">
+    <ARow class="energy-card-list" :gutter="[24, 24]">
+      <ACol v-for="(item, index) in energyCardList" :key="index" :xs="24" :sm="24" :md="12" :lg="12" :xl="6" :xxl="6">
+        <div
+          class="energy-card-container"
+          :style="{
+            backgroundColor: item.bgColor,
+          }"
+          @click="viewCoolingHisData(item)"
+        >
+          <img class="energy-card-icon" :src="item.icon" />
+          <div>
+            <div class="energy-card-title">
+              <span class="energy-card-value">{{ item.value }}</span>
+              <span class="energy-card-unit">{{ item.unit }}</span>
+            </div>
+            <div class="energy-card-description">{{ item.description }}</div>
+          </div>
+        </div>
+      </ACol>
+    </ARow>
+  </ACard>
+  <ACard :title="$t('energyAnalysis.coolingStationEfficiency')" :bordered="false">
+    <template #extra>
+      <TimeRangeSelect @change="handleTimeChange" />
+      <AButton class="export-button" @click="exportData">{{ $t('common.export') }}</AButton>
+    </template>
+    <div class="chart-container">
+      <div class="chart-wrapper chart-pie">
+        <div class="chart-pie-title">{{ $t('energyAnalysis.subItemEnergyEfficiency') }}</div>
+        <VChart class="chart" :option="gaugeOption" autoresize />
+        <!-- <div class="chart-pie-level">
+          <span>中等</span>
+        </div> -->
+        <ARow class="sub-efficiency-container" :gutter="[24, 8]">
+          <ACol v-for="(item, index) in subItemEfficiency" :key="index" :span="12">
+            <div class="sub-efficiency-item">
+              <div :style="{ backgroundColor: item.color }"></div>
+              <span>{{ item.label }}: {{ item.value }}</span>
+            </div>
+          </ACol>
+        </ARow>
+      </div>
+      <div class="chart-wrapper chart-line">
+        <VChart class="chart" :option="energyEfficiencyLineOption" autoresize />
+      </div>
+    </div>
+  </ACard>
+  <ACard :title="$t('energyAnalysis.coolingPricePerUnit')" :bordered="false">
+    <div class="chart-container">
+      <div class="chart-wrapper chart-pie">
+        <div class="chart-pie-title">
+          {{ $t('energyAnalysis.cumulativeCoolingCapacity') }}(GJ):
+          {{ energyEfficiencyResult?.totalCoolingData ?? '-' }}
+        </div>
+        <VChart class="chart" :option="pieOption" autoresize />
+      </div>
+      <div class="chart-wrapper chart-line">
+        <VChart class="chart" :option="lineOption" autoresize />
+      </div>
+    </div>
+  </ACard>
+  <AModal v-model:open="visible" :title="$t('deviceWorkStatus.viewData')" :width="920" :footer="null">
+    <TimeRangeSelect class="cooling-history-select" is-last />
+    <VChart class="cooling-history-chart" :option="coolingHisDataOption" autoresize />
+  </AModal>
+</template>
+
+<style lang="scss" scoped>
+.energy-card-container {
+  cursor: pointer;
+}
+
+.sub-efficiency-container {
+  position: absolute;
+  bottom: 0;
+  padding-inline: 59px;
+}
+
+.sub-efficiency-item {
+  display: flex;
+  align-items: center;
+
+  div {
+    width: 8px;
+    height: 8px;
+    margin-right: 4px;
+  }
+
+  span {
+    font-size: 12px;
+    line-height: 20px;
+    color: #000;
+  }
+}
+
+.chart-container {
+  display: flex;
+}
+
+.chart-wrapper {
+  height: 318px;
+}
+
+.chart-pie {
+  position: relative;
+  width: 380px;
+}
+
+.chart-pie-title {
+  position: absolute;
+  padding: 4px 8px;
+  font-size: 14px;
+  font-weight: 500;
+  color: #333;
+  text-align: center;
+  background: #f5f7fa;
+  border-radius: 4px;
+}
+
+.chart-pie-level {
+  position: absolute;
+  top: 197px;
+  right: 0;
+  left: 0;
+  text-align: center;
+
+  span {
+    display: inline-block;
+    height: 24px;
+    padding-inline: 6px;
+    font-size: 14px;
+    font-weight: 500;
+    line-height: 24px;
+    color: #e6a23c;
+    text-align: center;
+    background: rgb(230 162 60 / 12%);
+    border: 1px solid #e6a23c;
+    border-radius: 4px;
+  }
+}
+
+.chart-line {
+  width: calc(100% - 380px);
+}
+
+.chart {
+  height: 100%;
+}
+
+.cooling-history-select {
+  margin-block: 16px;
+}
+
+.cooling-history-chart {
+  height: 401px;
+}
+</style>