Explorar o código

perf(views): 优化能耗分析页面

1. 支持分组筛选和时间筛选数据
2. 添加 loading
3. 图表数据按年、月、日等时间尺度显示
wangcong hai 1 mes
pai
achega
1ba06bc779
Modificáronse 2 ficheiros con 162 adicións e 117 borrados
  1. 1 0
      src/types/index.ts
  2. 161 117
      src/views/energy-analysis/EnergyConsumption.vue

+ 1 - 0
src/types/index.ts

@@ -1825,6 +1825,7 @@ export interface ElectricityStatisticsResult {
   cumulativeDailyUse: number;
   hisQueryVos: ElectricityHisQueryVo[];
   groupQueryVos: ElectricityGroupQueryVo[];
+  timeScaleType: TimeScaleType;
 }
 
 export interface CoolingRealTimeDataQuery {

+ 161 - 117
src/views/energy-analysis/EnergyConsumption.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { computed, onMounted, ref, watch } from 'vue';
+import { computed, ref, watch } from 'vue';
 import VChart from 'vue-echarts';
 import dayjs from 'dayjs';
 import { BarChart, LineChart, PieChart } from 'echarts/charts';
@@ -8,11 +8,13 @@ import { use } from 'echarts/core';
 import { CanvasRenderer } from 'echarts/renderers';
 
 import SvgIcon from '@/components/SvgIcon.vue';
+import TimeRangeSelect from '@/components/TimeRangeSelect.vue';
 import { useRequest } from '@/hooks/request';
 import { useViewVisible } from '@/hooks/view-visible';
 import { t } from '@/i18n';
 import { downloadElectricityHisData, getElectricityDataStatistics } from '@/api';
-import { downloadBlob } from '@/utils';
+import { downloadBlob, formatTimeByScale, timeSorter } from '@/utils';
+import { TimeScaleType } from '@/constants';
 import type { IconfontIcon } from '@/icons/fonts/iconfont';
 
 import cumulativeElectricityBill from '@/assets/img/cumulative-electricity-bill.png';
@@ -33,7 +35,7 @@ import type {
   TooltipComponentOption,
 } from 'echarts/components';
 import type { ComposeOption } from 'echarts/core';
-import type { ElectricityStatisticsResult, EnergyCardItem } from '@/types';
+import type { DevGroupTabCompProps, ElectricityStatisticsResult, EnergyCardItem, RangeValue } from '@/types';
 
 type StatisticsValue = 'electricityConsumption' | 'electricityCost';
 
@@ -79,16 +81,26 @@ type EChartsOption = ComposeOption<
   | PieSeriesOption
 >;
 
-const { handleRequest } = useRequest();
+const props = defineProps<DevGroupTabCompProps>();
+
+const deviceTypes = [DeviceType.冷水主机, DeviceType.冷却塔, DeviceType.冷却泵, DeviceType.冷冻泵];
+
+const { isLoading, handleRequest } = useRequest();
 const energyConsumeResult = ref<ElectricityStatisticsResult>();
+let consumeTimeRange: Dayjs[] = [];
+
+const handleConsumeTimeChange = (range: RangeValue) => {
+  consumeTimeRange = range;
+  getEnergyConsumeResult();
+};
 
 const getEnergyConsumeResult = () => {
   handleRequest(async () => {
     energyConsumeResult.value = await getElectricityDataStatistics({
-      deviceGroupId: 7,
-      deviceTypes: [DeviceType.冷水主机, DeviceType.冷却塔, DeviceType.冷却泵, DeviceType.冷冻泵],
-      startTime: '2025-04-01 00:00:00',
-      endTime: '2025-05-01 23:00:00',
+      deviceGroupId: props.deviceGroupId,
+      deviceTypes,
+      startTime: consumeTimeRange[0].format('YYYY-MM-DD HH:mm:ss'),
+      endTime: consumeTimeRange[1].format('YYYY-MM-DD HH:mm:ss'),
     });
 
     energyConsumeResult.value.groupQueryVos.forEach((item) => {
@@ -101,12 +113,6 @@ const getEnergyConsumeResult = () => {
   });
 };
 
-onMounted(() => {
-  getEnergyConsumeResult();
-});
-
-const time = ref<Dayjs>();
-
 const energyCardList = computed<EnergyCardItem[]>(() => {
   return [
     {
@@ -250,13 +256,14 @@ const lineOption = computed<EChartsOption>(() => {
   const tooltipTitle = isElectricityConsume.value
     ? t('energyAnalysis.totalElectricityConsumption')
     : t('energyAnalysis.totalElectricityCost');
+  const timeScaleType = energyConsumeResult.value?.timeScaleType;
   const hisQueryVos = energyConsumeResult.value?.hisQueryVos || [];
   const deviceTypes = hisQueryVos.map((item) => item.groupName as string) || [];
   const dataset = {
     source: [['time', ...deviceTypes]],
   };
 
-  const timeSet = new Set();
+  const timeSet = new Set<string>();
 
   hisQueryVos.forEach((item) => {
     item.valueList.forEach((valueItem) => {
@@ -264,15 +271,16 @@ const lineOption = computed<EChartsOption>(() => {
     });
   });
 
-  const times = Array.from(timeSet);
+  const times = Array.from(timeSet).sort(timeSorter);
 
   times.forEach((time) => {
-    const row = [time];
+    const row: Array<string | number> = [time];
 
     deviceTypes.forEach((deviceType) => {
       const deviceData = hisQueryVos.find((item) => item.groupName === deviceType);
       const valueItem = deviceData?.valueList.find((item) => item.time === time);
-      row.push(isElectricityConsume.value ? valueItem?.energy : valueItem?.bill);
+      const currentValue = isElectricityConsume.value ? valueItem?.energy : valueItem?.bill;
+      row.push(currentValue ?? '-');
     });
 
     dataset.source.push(row as string[]);
@@ -295,7 +303,12 @@ const lineOption = computed<EChartsOption>(() => {
       formatter(params) {
         // eslint-disable-next-line @typescript-eslint/no-explicit-any
         const tempParms = params as any[];
-        const time = tempParms[0].axisValue;
+        let time = formatTimeByScale(tempParms[0].axisValue, timeScaleType);
+
+        if (timeScaleType === TimeScaleType.Hour) {
+          time += ` - ${time.split(':')[0]}:59`; // 若时间尺度为小时,则显示为 01:00 - 01:59
+        }
+
         let totalValue = 0;
 
         tempParms.forEach((param) => {
@@ -340,6 +353,10 @@ const lineOption = computed<EChartsOption>(() => {
       },
       axisLabel: {
         color: '#999',
+        fontSize: 10,
+        formatter(value) {
+          return formatTimeByScale(value, timeScaleType);
+        },
       },
       axisTick: {
         show: false,
@@ -349,6 +366,7 @@ const lineOption = computed<EChartsOption>(() => {
       type: 'value',
       axisLabel: {
         color: '#999',
+        fontSize: 10,
       },
       splitLine: {
         lineStyle: {
@@ -407,23 +425,29 @@ const deviceAndGroups = computed<EquipmentData[]>(() => {
 const exportData = async () => {
   const fileName = t('energyAnalysis.energyConsumptionAnalysis') + ' - ' + dayjs().format('YYYYMMDDHHmmss');
   const file = await downloadElectricityHisData({
-    deviceGroupId: 7,
-    deviceTypes: [DeviceType.冷水主机, DeviceType.冷却塔, DeviceType.冷却泵, DeviceType.冷冻泵],
-    startTime: '2025-04-01 00:00:00',
-    endTime: '2025-05-01 23:00:00',
+    deviceGroupId: props.deviceGroupId,
+    deviceTypes,
+    startTime: consumeTimeRange[0].format('YYYY-MM-DD HH:mm:ss'),
+    endTime: consumeTimeRange[1].format('YYYY-MM-DD HH:mm:ss'),
   });
 
   downloadBlob(file, fileName);
 };
 
-const { handleRequest: handleDevElectricRequest } = useRequest();
+const { isLoading: isDevElectricLoading, handleRequest: handleDevElectricRequest } = useRequest();
 const { visible, showView } = useViewVisible();
 const selectedDevice = ref<EquipmentData>();
 const devEnergyConsumeResult = ref<ElectricityStatisticsResult>();
+let devElectricTimeRange: Dayjs[] = [];
+
+const handleDevElectricTimeChange = (range: RangeValue) => {
+  devElectricTimeRange = range;
+  getDevElectricityData();
+};
 
 watch(visible, () => {
-  if (visible.value) {
-    getDevElectricityData();
+  if (!visible.value) {
+    devEnergyConsumeResult.value = undefined;
   }
 });
 
@@ -435,30 +459,34 @@ const viewDevElectricityData = (record: EquipmentData) => {
 const getDevElectricityData = () => {
   handleDevElectricRequest(async () => {
     devEnergyConsumeResult.value = await getElectricityDataStatistics({
-      deviceGroupId: 7,
+      deviceGroupId: props.deviceGroupId,
       deviceId: selectedDevice.value?.key,
-      startTime: '2025-04-01 00:00:00',
-      endTime: '2025-05-01 23:00:00',
+      startTime: devElectricTimeRange[0].format('YYYY-MM-DD HH:mm:ss'),
+      endTime: devElectricTimeRange[1].format('YYYY-MM-DD HH:mm:ss'),
     });
   });
 };
 
 const devElectricityOption = computed<EChartsOption>(() => {
+  const timeScaleType = devEnergyConsumeResult.value?.timeScaleType;
   const hisQueryVos = devEnergyConsumeResult.value?.hisQueryVos[0]?.valueList || [];
-  const times: string[] = [];
   const energy: number[] = [];
   const bill: number[] = [];
 
+  const allTimesSet = new Set<string>();
+
   hisQueryVos.forEach((item) => {
-    times.push(item.time);
+    allTimesSet.add(item.time);
     energy.push(item.energy);
     bill.push(item.bill);
   });
 
+  const times = Array.from(allTimesSet).sort(timeSorter);
+
   // 计算 Y 轴的分割段数
   const yAxisSegments = 5;
-  const energyMax = Math.ceil(Math.max(...energy) / yAxisSegments) * yAxisSegments;
-  const billMax = Math.ceil(Math.max(...bill) / yAxisSegments) * yAxisSegments;
+  const energyMax = Math.ceil(Math.max(...energy) / yAxisSegments) * (yAxisSegments + 1);
+  const billMax = Math.ceil(Math.max(...bill) / yAxisSegments) * (yAxisSegments + 1);
 
   return {
     title: [
@@ -490,15 +518,26 @@ const devElectricityOption = computed<EChartsOption>(() => {
       formatter(params) {
         // eslint-disable-next-line @typescript-eslint/no-explicit-any
         const tempParms = params as any[];
-        let tooltipStr = `<div>${tempParms[0].name}</div>`;
+        const time = formatTimeByScale(tempParms[0].axisValue, timeScaleType);
+
+        let tableHtml = '<table class="echarts-tooltip-electricity">';
+        tableHtml += `<tr><th>${time}</th></tr>`;
 
         tempParms.forEach((param) => {
-          const unit =
-            param.seriesName === t('energyAnalysis.electricityConsumption') ? 'kwh' : t('energyAnalysis.yuan');
-          tooltipStr += `<div>${param.marker} ${param.seriesName}: ${param.value}${unit}</div>`;
+          const { seriesName } = param;
+          let unit = '';
+
+          if (seriesName === t('energyAnalysis.electricityConsumption')) {
+            unit = 'kwh';
+          } else if (seriesName === t('energyAnalysis.electricityCost')) {
+            unit = t('energyAnalysis.yuan');
+          }
+
+          tableHtml += `<tr><td>${param.marker}${param.seriesName}</td><td>${param.value ?? '-'}${unit}</td></tr>`;
         });
 
-        return tooltipStr;
+        tableHtml += '</table>';
+        return tableHtml;
       },
     },
     legend: [
@@ -541,6 +580,9 @@ const devElectricityOption = computed<EChartsOption>(() => {
       axisLabel: {
         color: '#999',
         fontSize: 10,
+        formatter(value) {
+          return formatTimeByScale(value, timeScaleType);
+        },
       },
       axisTick: {
         show: false,
@@ -611,93 +653,95 @@ const devElectricityOption = computed<EChartsOption>(() => {
 </script>
 
 <template>
-  <ACard :title="$t('energyAnalysis.energyConsumptionAnalysis')" :bordered="false">
-    <template #extra>
-      <span class="">{{ $t('common.selectTime') }}</span>
-      <ADatePicker v-model:value="time" picker="year" />
-      <AButton @click="exportData">{{ $t('common.export') }}</AButton>
-    </template>
-    <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,
-          }"
-        >
-          <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>
+  <ASpin :spinning="isLoading">
+    <ACard :title="$t('energyAnalysis.energyConsumptionAnalysis')" :bordered="false">
+      <template #extra>
+        <TimeRangeSelect @change="handleConsumeTimeChange" />
+        <AButton class="export-button" @click="exportData">{{ $t('common.export') }}</AButton>
+      </template>
+      <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,
+            }"
+          >
+            <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 class="energy-card-description">{{ item.description }}</div>
           </div>
+        </ACol>
+      </ARow>
+      <ASegmented v-model:value="currentStatisticsType" :options="statisticsTypes">
+        <template #label="{ payload }">
+          <SvgIcon :name="payload.icon" />
+          <span>{{ payload.title }}</span>
+        </template>
+      </ASegmented>
+      <div class="chart-container">
+        <div class="chart-wrapper chart-pie">
+          <VChart class="chart" :option="pieOption" autoresize />
+        </div>
+        <div class="chart-wrapper chart-line">
+          <VChart class="chart" :option="lineOption" autoresize />
         </div>
-      </ACol>
-    </ARow>
-    <ASegmented v-model:value="currentStatisticsType" :options="statisticsTypes">
-      <template #label="{ payload }">
-        <SvgIcon :name="payload.icon" />
-        <span>{{ payload.title }}</span>
-      </template>
-    </ASegmented>
-    <div class="chart-container">
-      <div class="chart-wrapper chart-pie">
-        <VChart class="chart" :option="pieOption" autoresize />
-      </div>
-      <div class="chart-wrapper chart-line">
-        <VChart class="chart" :option="lineOption" autoresize />
       </div>
-    </div>
-    <ATable :data-source="deviceAndGroups" :pagination="false">
-      <ATableColumn :title="$t('energyAnalysis.deviceGroupName')" data-index="name" key="name">
-        <template #default="{ record }">
-          <span v-if="record.children">{{ record.name }}</span>
-          <a v-else class="device-name" @click="viewDevElectricityData(record)">
-            {{ record.name }}
-          </a>
-        </template>
-      </ATableColumn>
-      <ATableColumn :title="$t('energyAnalysis.deviceType')" data-index="type" key="type" />
-      <ATableColumn
-        :title="$t('energyAnalysis.electricityConsumptionUnit')"
-        data-index="electricity"
-        key="electricity"
-      />
-      <ATableColumn :title="$t('energyAnalysis.electricityCostUnit')" data-index="cost" key="cost" />
-      <ATableColumn
-        :title="$t('energyAnalysis.dailyElectricityUnit')"
-        data-index="dailyElectricity"
-        key="dailyElectricity"
-      />
-      <ATableColumn :title="$t('energyAnalysis.electricityRatioUnit')" data-index="ratio" key="ratio" />
-    </ATable>
-    <AModal v-model:visible="visible" :title="selectedDevice?.name" :width="920" :footer="null">
-      <!-- <TimeRangeSelect class="device-electricity-select" is-last /> -->
-      <div class="device-electricity-detail">
-        <div class="device-electricity-summary">
-          <div class="device-electricity-item">
-            <span class="device-electricity-value">{{ devEnergyConsumeResult?.cumulativeEnergy ?? '-' }}</span>
-            <span class="device-electricity-label">{{ $t('energyAnalysis.tenThousandKwh') }}</span>
-            <div class="device-electricity-title">
-              <SvgIcon name="electricity-quantity" />
-              <span class="device-electricity-label">{{ $t('energyAnalysis.totalElectricityConsumption') }}</span>
+      <ATable class="hvac-table" :data-source="deviceAndGroups" :pagination="false">
+        <ATableColumn :title="$t('energyAnalysis.deviceGroupName')" data-index="name" key="name">
+          <template #default="{ record }">
+            <span v-if="record.children">{{ record.name }}</span>
+            <a v-else class="device-name" @click="viewDevElectricityData(record)">
+              {{ record.name }}
+            </a>
+          </template>
+        </ATableColumn>
+        <ATableColumn :title="$t('energyAnalysis.deviceType')" data-index="type" key="type" />
+        <ATableColumn
+          :title="$t('energyAnalysis.electricityConsumptionUnit')"
+          data-index="electricity"
+          key="electricity"
+        />
+        <ATableColumn :title="$t('energyAnalysis.electricityCostUnit')" data-index="cost" key="cost" />
+        <ATableColumn
+          :title="$t('energyAnalysis.dailyElectricityUnit')"
+          data-index="dailyElectricity"
+          key="dailyElectricity"
+        />
+        <ATableColumn :title="$t('energyAnalysis.electricityRatioUnit')" data-index="ratio" key="ratio" />
+      </ATable>
+      <AModal v-model:open="visible" :title="selectedDevice?.name" :width="920" :footer="null" destroy-on-close>
+        <ASpin class="center-loading" :spinning="isDevElectricLoading" />
+        <TimeRangeSelect class="device-electricity-select" @change="handleDevElectricTimeChange" />
+        <div class="device-electricity-detail">
+          <div class="device-electricity-summary">
+            <div class="device-electricity-item">
+              <span class="device-electricity-value">{{ devEnergyConsumeResult?.cumulativeEnergy ?? '-' }}</span>
+              <span class="device-electricity-label">{{ $t('energyAnalysis.tenThousandKwh') }}</span>
+              <div class="device-electricity-title">
+                <SvgIcon name="electricity-quantity" />
+                <span class="device-electricity-label">{{ $t('energyAnalysis.totalElectricityConsumption') }}</span>
+              </div>
             </div>
-          </div>
-          <div class="device-electricity-item">
-            <span class="device-electricity-value">{{ devEnergyConsumeResult?.cumulativeBill ?? '-' }}</span>
-            <span class="device-electricity-label">{{ $t('energyAnalysis.tenThousandYuan') }}</span>
-            <div class="device-electricity-title">
-              <SvgIcon name="electricity-bill" />
-              <span class="device-electricity-label">{{ $t('energyAnalysis.totalElectricityCost') }}</span>
+            <div class="device-electricity-item">
+              <span class="device-electricity-value">{{ devEnergyConsumeResult?.cumulativeBill ?? '-' }}</span>
+              <span class="device-electricity-label">{{ $t('energyAnalysis.tenThousandYuan') }}</span>
+              <div class="device-electricity-title">
+                <SvgIcon name="electricity-bill" />
+                <span class="device-electricity-label">{{ $t('energyAnalysis.totalElectricityCost') }}</span>
+              </div>
             </div>
           </div>
+          <VChart class="device-electricity-chart" :option="devElectricityOption" autoresize />
         </div>
-        <VChart class="device-electricity-chart" :option="devElectricityOption" autoresize />
-      </div>
-    </AModal>
-  </ACard>
+      </AModal>
+    </ACard>
+  </ASpin>
 </template>
 
 <style lang="scss" scoped>