Explorar o código

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

1. 支持分组筛选和时间筛选数据
2. 添加 loading
3. 历史数据按年、月、日等时间尺度显示
wangcong hai 1 mes
pai
achega
e1bf6e50a9

+ 41 - 12
src/views/energy-analysis/EnergyAnalysis.vue

@@ -1,29 +1,54 @@
 <script setup lang="ts">
-import { ref } from 'vue';
+import { computed, onMounted, ref } from 'vue';
 
 import DeviceGroupSelect from '@/components/DeviceGroupSelect.vue';
+import { useRefreshView } from '@/hooks/refresh-view';
+import { t } from '@/i18n';
 
 import EnergyConsumption from './EnergyConsumption.vue';
 import EnergyEfficiency from './EnergyEfficiency.vue';
 
-const activeKey = ref('energyConsumption');
+import type { TabComponent } from '@/types';
+
+const { renderView, refreshView } = useRefreshView();
+const activeKey = ref('');
+const deviceGroupId = ref<number>();
+
+const energyTabs = computed<TabComponent[]>(() => {
+  return [
+    {
+      key: 'energyConsumption',
+      name: t('energyAnalysis.energyConsumptionAnalysis'),
+      component: EnergyConsumption,
+    },
+    {
+      key: 'energyEfficiency',
+      name: t('energyAnalysis.energyEfficiencyAnalysis'),
+      component: EnergyEfficiency,
+    },
+  ];
+});
+
+onMounted(() => {
+  activeKey.value = energyTabs.value[0].key;
+  refreshView();
+});
 
 const handleDevGroupChange = (id: number) => {
-  console.log(id);
+  deviceGroupId.value = id;
+  refreshView();
 };
 </script>
 
 <template>
-  <ATabs class="button-tabs-compact" v-model:active-key="activeKey" type="card">
-    <ATabPane key="energyConsumption" :tab="$t('energyAnalysis.energyConsumptionAnalysis')">
-      <EnergyConsumption />
+  <ATabs class="button-tabs-compact" v-model:active-key="activeKey" type="card" @tab-click="refreshView">
+    <ATabPane v-for="item in energyTabs" :key="item.key" :tab="item.name">
+      <component
+        v-if="deviceGroupId !== undefined && renderView"
+        :is="item.component"
+        :device-group-id="deviceGroupId"
+      />
     </ATabPane>
-    <ATabPane key="energyEfficiency" :tab="$t('energyAnalysis.energyEfficiencyAnalysis')">
-      <EnergyEfficiency />
-    </ATabPane>
-    <!-- <ATabPane key="energySaving" :tab="$t('energyAnalysis.energySavingAssessment')">
-      <EnergySaving />
-    </ATabPane> -->
     <template #rightExtra>
       <DeviceGroupSelect @change="handleDevGroupChange" />
     </template>
@@ -58,6 +83,10 @@ const handleDevGroupChange = (id: number) => {
     border-left: 2px solid var(--antd-color-primary);
   }
 
+  .export-button {
+    margin-left: 16px;
+  }
+
   .energy-card-container {
     display: flex;
     flex: 1;

+ 107 - 86
src/views/energy-analysis/EnergyEfficiency.vue

@@ -37,6 +37,8 @@ import type {
   CoolingHistoryDataResult,
   CoolingRealTimeDataResult,
   CoolingStatisticsResult,
+  DevGroupTabCompProps,
+  RangeValue,
 } from '@/types';
 
 use([
@@ -63,9 +65,11 @@ type EChartsOption = ComposeOption<
   | PieSeriesOption
 >;
 
+const props = defineProps<DevGroupTabCompProps>();
+
 const deviceTypes = [DeviceType.冷水主机, DeviceType.冷却塔, DeviceType.冷却泵, DeviceType.冷冻泵];
 
-const { handleRequest } = useRequest();
+const { isLoading, handleRequest } = useRequest();
 const coolingRTDResult = ref<CoolingRealTimeDataResult>();
 let coolingRTDTimer: number | undefined;
 
@@ -76,7 +80,7 @@ const getCoolingRTDResult = () => {
     const now = dayjs();
 
     coolingRTDResult.value = await getCoolingRealTimeData({
-      deviceGroupId: 7,
+      deviceGroupId: props.deviceGroupId,
       startTime: now.startOf('day').format('YYYY-MM-DD HH:mm:ss'),
       endTime: now.endOf('day').format('YYYY-MM-DD HH:mm:ss'),
     });
@@ -85,15 +89,19 @@ const getCoolingRTDResult = () => {
   coolingRTDTimer = window.setTimeout(getCoolingRTDResult, 3600 * 1000);
 };
 
-const { handleRequest: handleCoolingHisDataRequest } = useRequest();
+const { isLoading: isCoolingHisDataLoading, handleRequest: handleCoolingHisDataRequest } = useRequest();
 const { visible, showView } = useViewVisible();
-const coolingHisResult = ref<CoolingHistoryDataResult[]>([]);
+const coolingHisResult = ref<CoolingHistoryDataResult>();
+let coolingHisTimeRange: Dayjs[] = [];
+
+const handleCoolingHisTimeChange = (range: RangeValue) => {
+  coolingHisTimeRange = range;
+  getCoolingHisData();
+};
 
 watch(visible, () => {
-  if (visible.value) {
-    getCoolingHisData();
-  } else {
-    coolingHisResult.value = [];
+  if (!visible.value) {
+    coolingHisResult.value = undefined;
   }
 });
 
@@ -105,10 +113,10 @@ const viewCoolingHisData = (energyCard: CoolingEnergyCardItem) => {
 const getCoolingHisData = () => {
   handleCoolingHisDataRequest(async () => {
     coolingHisResult.value = await getCoolingHistoryData({
-      deviceGroupId: 7,
+      deviceGroupId: props.deviceGroupId,
       deviceTypes,
-      startTime: '2025-04-01 00:00:00',
-      endTime: '2025-05-01 23:00:00',
+      startTime: coolingHisTimeRange[0].format('YYYY-MM-DD HH:mm:ss'),
+      endTime: coolingHisTimeRange[1].format('YYYY-MM-DD HH:mm:ss'),
       type: selectedEnergyCard.value?.type as CoolingDataType,
     });
   });
@@ -116,17 +124,19 @@ const getCoolingHisData = () => {
 
 const coolingHisDataOption = computed<EChartsOption>(() => {
   const unitText = selectedEnergyCard.value?.unit ?? '-';
-  const legendData = coolingHisResult.value.map((device) => device.deviceTypeName);
+  const timeScaleType = coolingHisResult.value?.timeScale;
+  const historyData = coolingHisResult.value?.data || [];
+  const legendData = historyData.map((device) => device.deviceTypeName);
 
   const allTimesSet = new Set<string>();
 
-  coolingHisResult.value.forEach((device) => {
+  historyData.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 seriesData = historyData.map<LineSeriesOption>((device) => {
     const valueKey = Object.keys(device.data[0] ?? {}).find((key) => key !== 'time');
     const timeMap: Record<string, string | number> = {};
 
@@ -163,7 +173,7 @@ const coolingHisDataOption = computed<EChartsOption>(() => {
       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');
+        const time = formatTimeByScale(tempParms[0].axisValue, timeScaleType);
 
         let tableHtml = '<table class="echarts-tooltip-electricity">';
         tableHtml += `<tr><th>${time}</th></tr>`;
@@ -201,7 +211,7 @@ const coolingHisDataOption = computed<EChartsOption>(() => {
         color: '#999',
         fontSize: 10,
         formatter(value) {
-          return dayjs(value).format('MM-DD HH:mm');
+          return formatTimeByScale(value, timeScaleType);
         },
       },
       axisTick: {
@@ -236,22 +246,28 @@ const coolingHisDataOption = computed<EChartsOption>(() => {
 
 onMounted(() => {
   getCoolingRTDResult();
-  getEnergyConsumeResult();
 });
 
 onUnmounted(() => {
   clearTimeout(coolingRTDTimer);
 });
 
+const { isLoading: isEfficiencyLoading, handleRequest: handleEfficiencyRequest } = useRequest();
 const energyEfficiencyResult = ref<CoolingStatisticsResult>();
+let efficiencyTimeRange: Dayjs[] = [];
 
-const getEnergyConsumeResult = () => {
-  handleRequest(async () => {
+const handleEfficiencyTimeChange = (range: RangeValue) => {
+  efficiencyTimeRange = range;
+  getEnergyEfficiencyResult();
+};
+
+const getEnergyEfficiencyResult = () => {
+  handleEfficiencyRequest(async () => {
     energyEfficiencyResult.value = await getCoolingDataStatistics({
-      deviceGroupId: 7,
+      deviceGroupId: props.deviceGroupId,
       deviceTypes,
-      startTime: '2025-04-16 00:00:00',
-      endTime: '2025-04-16 23:00:00',
+      startTime: efficiencyTimeRange[0].format('YYYY-MM-DD HH:mm:ss'),
+      endTime: efficiencyTimeRange[1].format('YYYY-MM-DD HH:mm:ss'),
     });
 
     energyEfficiencyResult.value.hisQueryVos.forEach((item) => {
@@ -900,90 +916,95 @@ const lineOption = computed<EChartsOption>(() => {
 const exportData = async () => {
   const fileName = t('energyAnalysis.energyEfficiencyAnalysis') + ' - ' + dayjs().format('YYYYMMDDHHmmss');
   const file = await downloadCoolingHisData({
-    deviceGroupId: 7,
+    deviceGroupId: props.deviceGroupId,
     deviceTypes,
-    startTime: '2025-04-01 00:00:00',
-    endTime: '2025-05-01 23:00:00',
+    startTime: efficiencyTimeRange[0].format('YYYY-MM-DD HH:mm:ss'),
+    endTime: efficiencyTimeRange[1].format('YYYY-MM-DD HH:mm:ss'),
   });
 
   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>
+  <ASpin :spinning="isLoading">
+    <ACard class="real-time-card" :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 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">
+        </ACol>
+      </ARow>
+    </ACard>
+  </ASpin>
+  <ASpin :spinning="isEfficiencyLoading">
+    <ACard :title="$t('energyAnalysis.coolingStationEfficiency')" :bordered="false">
+      <template #extra>
+        <TimeRangeSelect @change="handleEfficiencyTimeChange" />
+        <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 ?? '-' }}
+          <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>
-        <VChart class="chart" :option="pieOption" autoresize />
       </div>
-      <div class="chart-wrapper chart-line">
-        <VChart class="chart" :option="lineOption" autoresize />
+    </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>
-    </div>
-  </ACard>
-  <AModal v-model:open="visible" :title="$t('deviceWorkStatus.viewData')" :width="920" :footer="null">
-    <TimeRangeSelect class="cooling-history-select" is-last />
+    </ACard>
+  </ASpin>
+  <AModal v-model:open="visible" :title="$t('deviceWorkStatus.viewData')" :width="920" :footer="null" destroy-on-close>
+    <ASpin class="center-loading" :spinning="isCoolingHisDataLoading" />
+    <TimeRangeSelect class="cooling-history-select" @change="handleCoolingHisTimeChange" />
     <VChart class="cooling-history-chart" :option="coolingHisDataOption" autoresize />
   </AModal>
 </template>
 
 <style lang="scss" scoped>
+.real-time-card {
+  margin-bottom: 16px;
+}
+
 .energy-card-container {
   cursor: pointer;
 }