From 4ec37425fba3bc5aa8ceab98b9b4de333375f4f2 Mon Sep 17 00:00:00 2001
From: yuan <123@>
Date: 星期二, 16 六月 2026 13:55:04 +0800
Subject: [PATCH] feat: 添加能耗综合分析功能,优化统计维度和趋势粒度选择

---
 src/api/energyManagement/statisticEle.js              |   13 +
 src/views/energyManagement/energyStatistics/index.vue |  588 +++++++++++++++++++++++++++++++++--------------------
 2 files changed, 378 insertions(+), 223 deletions(-)

diff --git a/src/api/energyManagement/statisticEle.js b/src/api/energyManagement/statisticEle.js
index 289d98a..2d25046 100644
--- a/src/api/energyManagement/statisticEle.js
+++ b/src/api/energyManagement/statisticEle.js
@@ -18,6 +18,15 @@
   });
 }
 
+/** 鑳借�楃患鍚堝垎鏋� */
+export function analyticsStatisticEle(query) {
+  return request({
+    url: "/statisticEle/analytics",
+    method: "get",
+    params: query,
+  });
+}
+
 /** 鏄ㄦ棩鐢ㄧ數閲忔眹鎬� */
 export function getYesterdaySummary() {
   return request({
@@ -94,6 +103,10 @@
   if ((dimension === "manual" || dimension === "minute") && timeKey.length >= 12) {
     return `${timeKey.slice(0, 4)}-${timeKey.slice(4, 6)}-${timeKey.slice(6, 8)} ${timeKey.slice(8, 10)}:${timeKey.slice(10, 12)}`;
   }
+  if (dimension === "week" && timeKey.includes("W")) {
+    const [y, w] = timeKey.split("W");
+    return `${y}骞� 绗�${Number(w)}鍛╜;
+  }
   if (dimension === "day" && timeKey.length >= 8) {
     return `${timeKey.slice(0, 4)}-${timeKey.slice(4, 6)}-${timeKey.slice(6, 8)}`;
   }
diff --git a/src/views/energyManagement/energyStatistics/index.vue b/src/views/energyManagement/energyStatistics/index.vue
index ee4bbd6..591fd04 100644
--- a/src/views/energyManagement/energyStatistics/index.vue
+++ b/src/views/energyManagement/energyStatistics/index.vue
@@ -4,51 +4,58 @@
       <template #header>
         <div class="card-header">
           <span>鑳借�楃粺璁″垎鏋�</span>
-          <span class="desc">鎸夊ぉ銆佹湀銆佸搴︺�佸勾姹囨�荤粺璁★紙鐢卞皬鏃舵暟鎹疮绉绠楋級</span>
+          <span class="desc">鍛ㄦ湡绱銆佹椂娈垫媶鍒嗐�佽秼鍔垮姣斾笌璐熻嵎鍒嗘瀽</span>
         </div>
       </template>
 
       <el-form :inline="true" class="search-form">
         <el-form-item label="缁熻缁村害">
           <el-radio-group v-model="queryForm.dimension" @change="handleDimensionChange">
-            <el-radio-button value="day">澶�</el-radio-button>
+            <el-radio-button value="day">鏃�</el-radio-button>
+            <el-radio-button value="week">鍛�</el-radio-button>
             <el-radio-button value="month">鏈�</el-radio-button>
-            <el-radio-button value="quarter">瀛e害</el-radio-button>
+            <el-radio-button value="quarter">瀛�</el-radio-button>
             <el-radio-button value="year">骞�</el-radio-button>
           </el-radio-group>
         </el-form-item>
         <el-form-item label="鏃堕棿鑼冨洿" class="time-range-item">
-          <div class="time-range-row">
-            <el-date-picker
-              v-if="queryForm.dimension === 'day'"
-              v-model="dayRange"
-              type="daterange"
-              range-separator="鑷�"
-              value-format="YYYY-MM-DD"
-              :shortcuts="dayShortcuts"
-            />
-            <el-date-picker
-              v-else-if="queryForm.dimension === 'month'"
-              v-model="monthRange"
-              type="monthrange"
-              range-separator="鑷�"
-              value-format="YYYY-MM"
-            />
-            <el-date-picker
-              v-else-if="queryForm.dimension === 'quarter'"
-              v-model="quarterRange"
-              type="daterange"
-              range-separator="鑷�"
-              value-format="YYYY-MM-DD"
-            />
-            <el-date-picker
-              v-else
-              v-model="yearRange"
-              type="yearrange"
-              range-separator="鑷�"
-              value-format="YYYY"
-            />
-          </div>
+          <el-date-picker
+            v-if="queryForm.dimension === 'day' || queryForm.dimension === 'week'"
+            v-model="dayRange"
+            type="daterange"
+            range-separator="鑷�"
+            value-format="YYYY-MM-DD"
+            :shortcuts="dayShortcuts"
+          />
+          <el-date-picker
+            v-else-if="queryForm.dimension === 'month'"
+            v-model="monthRange"
+            type="monthrange"
+            range-separator="鑷�"
+            value-format="YYYY-MM"
+          />
+          <el-date-picker
+            v-else-if="queryForm.dimension === 'quarter'"
+            v-model="quarterRange"
+            type="daterange"
+            range-separator="鑷�"
+            value-format="YYYY-MM-DD"
+          />
+          <el-date-picker
+            v-else
+            v-model="yearRange"
+            type="yearrange"
+            range-separator="鑷�"
+            value-format="YYYY"
+          />
+        </el-form-item>
+        <el-form-item label="瓒嬪娍绮掑害">
+          <el-radio-group v-model="queryForm.trendGranularity" size="small" @change="handleQuery">
+            <el-radio-button value="hour">灏忔椂</el-radio-button>
+            <el-radio-button value="day">鏃�</el-radio-button>
+            <el-radio-button value="week">鍛�</el-radio-button>
+            <el-radio-button value="month">鏈�</el-radio-button>
+          </el-radio-group>
         </el-form-item>
         <el-form-item>
           <el-button type="primary" :loading="loading" @click="handleQuery">鏌ヨ</el-button>
@@ -56,46 +63,140 @@
         </el-form-item>
       </el-form>
 
-      <el-row :gutter="16" class="summary-row">
-        <el-col :span="6">
+      <!-- 涓�銆佸熀纭�鐢ㄩ噺缁熻 -->
+      <div class="section-title">鍩虹鐢ㄩ噺缁熻</div>
+      <el-row :gutter="12" class="summary-row">
+        <el-col :span="4">
           <div class="summary-card total">
-            <div class="label">{{ summaryLabels.total }}</div>
-            <div class="value">{{ formatKwh(summary.totalConsumption) }} <span>kWh</span></div>
+            <div class="label">鍛ㄦ湡绱鐢甸噺</div>
+            <div class="value">{{ formatKwh(analytics.totalConsumption) }} <span>kWh</span></div>
           </div>
         </el-col>
-        <el-col :span="6">
+        <el-col :span="5">
           <div class="summary-card">
-            <div class="label">{{ summaryLabels.avg }}</div>
-            <div class="value">{{ formatKwh(summary.avgConsumption) }} <span>kWh</span></div>
+            <div class="label">灏忔椂骞冲潎鐢ㄧ數閲�</div>
+            <div class="value">{{ formatKwh(analytics.avgConsumption) }} <span>kWh</span></div>
           </div>
         </el-col>
-        <el-col :span="6">
+        <el-col :span="5">
           <div class="summary-card">
-            <div class="label">{{ summaryLabels.max }}</div>
-            <div class="value">{{ formatKwh(summary.maxConsumption) }} <span>kWh</span></div>
+            <div class="label">灏忔椂鏈�澶х敤鐢甸噺</div>
+            <div class="value">{{ formatKwh(analytics.maxConsumption) }} <span>kWh</span></div>
           </div>
         </el-col>
-        <el-col :span="6">
+        <el-col :span="5">
           <div class="summary-card">
-            <div class="label">{{ summaryLabels.min }}</div>
-            <div class="value">{{ formatKwh(summary.minConsumption) }} <span>kWh</span></div>
+            <div class="label">灏忔椂鏈�灏忕敤鐢甸噺</div>
+            <div class="value">{{ formatKwh(analytics.minConsumption) }} <span>kWh</span></div>
+          </div>
+        </el-col>
+        <el-col :span="5">
+          <div class="summary-card load">
+            <div class="label">璐熻嵎鐜�</div>
+            <div class="value">{{ formatKwh(analytics.loadRate, 1) }} <span>%</span></div>
+            <div class="hint">骞冲潎梅鏈�澶�100</div>
           </div>
         </el-col>
       </el-row>
 
-      <div class="chart-toolbar">
-        <span>{{ chartTitle }}</span>
-        <el-radio-group
-          v-if="!isSingleDay"
-          v-model="chartType"
-          size="small"
-          @change="renderChart"
-        >
-          <el-radio-button value="line">鎶樼嚎鍥�</el-radio-button>
-          <el-radio-button value="bar">鏌辩姸鍥�</el-radio-button>
-        </el-radio-group>
-      </div>
-      <div ref="chartRef" class="chart-container"></div>
+      <!-- 浜屻�佸悓姣旂幆姣� -->
+      <div class="section-title">鍚屾瘮 / 鐜瘮鍒嗘瀽</div>
+      <el-row :gutter="16" class="compare-row">
+        <el-col :span="12">
+          <div class="compare-card">
+            <div class="compare-label">{{ analytics.chainComparison?.label || "鐜瘮涓婃湡" }}</div>
+            <div class="compare-body">
+              <div>
+                <span class="sub">涓婃湡</span>
+                <strong>{{ formatKwh(analytics.chainComparison?.compareTotal) }} kWh</strong>
+              </div>
+              <div>
+                <span class="sub">鏈湡</span>
+                <strong>{{ formatKwh(analytics.chainComparison?.currentTotal) }} kWh</strong>
+              </div>
+              <div :class="deltaClass(analytics.chainComparison?.delta)">
+                {{ formatDelta(analytics.chainComparison) }}
+              </div>
+            </div>
+          </div>
+        </el-col>
+        <el-col :span="12">
+          <div class="compare-card">
+            <div class="compare-label">{{ analytics.yoyComparison?.label || "鍚屾瘮鍘诲勾鍚屾湡" }}</div>
+            <div class="compare-body">
+              <div>
+                <span class="sub">鍘诲勾鍚屾湡</span>
+                <strong>{{ formatKwh(analytics.yoyComparison?.compareTotal) }} kWh</strong>
+              </div>
+              <div>
+                <span class="sub">鏈湡</span>
+                <strong>{{ formatKwh(analytics.yoyComparison?.currentTotal) }} kWh</strong>
+              </div>
+              <div :class="deltaClass(analytics.yoyComparison?.delta)">
+                {{ formatDelta(analytics.yoyComparison) }}
+              </div>
+            </div>
+          </div>
+        </el-col>
+      </el-row>
+
+      <!-- 涓夈�佽秼鍔夸笌鎷嗗垎 -->
+      <div class="section-title">瓒嬪娍涓庢椂娈靛垎鏋�</div>
+      <el-row :gutter="16">
+        <el-col :span="14">
+          <div class="chart-panel">
+            <div class="chart-toolbar">
+              <span>鐢ㄧ數瓒嬪娍锛坽{ trendGranularityLabel }}锛�</span>
+              <el-radio-group
+                v-if="!isSingleDay"
+                v-model="chartType"
+                size="small"
+                @change="renderAllCharts"
+              >
+                <el-radio-button value="line">鎶樼嚎</el-radio-button>
+                <el-radio-button value="bar">鏌辩姸</el-radio-button>
+              </el-radio-group>
+            </div>
+            <div ref="trendChartRef" class="chart-container" />
+          </div>
+        </el-col>
+        <el-col :span="10">
+          <div class="chart-panel">
+            <div class="chart-toolbar"><span>鏃舵鎷嗗垎锛堝嘲骞宠胺锛�</span></div>
+            <div ref="periodChartRef" class="chart-container short" />
+          </div>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="16" class="sub-chart-row">
+        <el-col :span="8">
+          <div class="chart-panel">
+            <div class="chart-toolbar"><span>鐝鐢ㄧ數瀵规瘮</span></div>
+            <div ref="shiftChartRef" class="chart-container short" />
+          </div>
+        </el-col>
+        <el-col :span="8">
+          <div class="chart-panel">
+            <div class="chart-toolbar"><span>宸ヤ綔鏃� / 浼戞伅鏃�</span></div>
+            <div ref="dayTypeChartRef" class="chart-container short" />
+          </div>
+        </el-col>
+        <el-col :span="8">
+          <div class="split-table-panel">
+            <div class="chart-toolbar"><span>鎷嗗垎鍗犳瘮鏄庣粏</span></div>
+            <el-table :data="splitTableRows" size="small" border max-height="280">
+              <el-table-column prop="category" label="绫诲埆" width="80" />
+              <el-table-column prop="name" label="椤�" min-width="80" />
+              <el-table-column label="鐢甸噺(kWh)" width="100">
+                <template #default="{ row }">{{ formatKwh(row.consumption) }}</template>
+              </el-table-column>
+              <el-table-column label="鍗犳瘮" width="70">
+                <template #default="{ row }">{{ formatKwh(row.ratio, 1) }}%</template>
+              </el-table-column>
+            </el-table>
+          </div>
+        </el-col>
+      </el-row>
 
       <div class="detail-title">鐢ㄧ數鏄庣粏</div>
       <el-table v-loading="loading" :data="detailRecords" border stripe max-height="360">
@@ -112,19 +213,16 @@
             </div>
           </template>
         </el-table-column>
-        <el-table-column prop="totalConsumption" label="鎬荤數閲�(kWh)" width="120">
+        <el-table-column label="鎬荤數閲�(kWh)" width="120">
           <template #default="{ row }">{{ formatKwh(row.totalConsumption) }}</template>
         </el-table-column>
-        <el-table-column v-if="hasPeriodData" prop="sharpConsumption" label="灏�(kWh)" width="100">
-          <template #default="{ row }">{{ formatKwh(row.sharpConsumption) }}</template>
-        </el-table-column>
-        <el-table-column v-if="hasPeriodData" prop="peakConsumption" label="宄�(kWh)" width="100">
+        <el-table-column v-if="hasPeriodData" label="宄�(kWh)" width="90">
           <template #default="{ row }">{{ formatKwh(row.peakConsumption) }}</template>
         </el-table-column>
-        <el-table-column v-if="hasPeriodData" prop="flatConsumption" label="骞�(kWh)" width="100">
+        <el-table-column v-if="hasPeriodData" label="骞�(kWh)" width="90">
           <template #default="{ row }">{{ formatKwh(row.flatConsumption) }}</template>
         </el-table-column>
-        <el-table-column v-if="hasPeriodData" prop="valleyConsumption" label="璋�(kWh)" width="100">
+        <el-table-column v-if="hasPeriodData" label="璋�(kWh)" width="90">
           <template #default="{ row }">{{ formatKwh(row.valleyConsumption) }}</template>
         </el-table-column>
         <el-table-column label="鎿嶄綔" width="80" fixed="right">
@@ -144,18 +242,6 @@
         <el-descriptions-item label="鐢佃〃ID">{{ detailRow.meterId ?? "-" }}</el-descriptions-item>
         <el-descriptions-item label="琛ㄥ湴鍧�">{{ detailRow.address || "-" }}</el-descriptions-item>
         <el-descriptions-item label="鎬荤數閲�(kWh)">{{ formatKwh(detailRow.totalConsumption) }}</el-descriptions-item>
-        <el-descriptions-item v-if="hasPeriodValue(detailRow, 'sharpConsumption')" label="灏�(kWh)">
-          {{ formatKwh(detailRow.sharpConsumption) }}
-        </el-descriptions-item>
-        <el-descriptions-item v-if="hasPeriodValue(detailRow, 'peakConsumption')" label="宄�(kWh)">
-          {{ formatKwh(detailRow.peakConsumption) }}
-        </el-descriptions-item>
-        <el-descriptions-item v-if="hasPeriodValue(detailRow, 'flatConsumption')" label="骞�(kWh)">
-          {{ formatKwh(detailRow.flatConsumption) }}
-        </el-descriptions-item>
-        <el-descriptions-item v-if="hasPeriodValue(detailRow, 'valleyConsumption')" label="璋�(kWh)">
-          {{ formatKwh(detailRow.valleyConsumption) }}
-        </el-descriptions-item>
         <el-descriptions-item label="寮�濮嬫椂闂�">{{ detailRow.startTime || "-" }}</el-descriptions-item>
         <el-descriptions-item label="缁撴潫鏃堕棿">{{ detailRow.endTime || "-" }}</el-descriptions-item>
       </el-descriptions>
@@ -164,13 +250,12 @@
 </template>
 
 <script setup>
-import { computed, getCurrentInstance, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
+import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
 import { ElMessageBox } from "element-plus";
 import * as echarts from "echarts";
 import {
-  summaryStatisticEle,
+  analyticsStatisticEle,
   formatDayPicker,
-  formatDayTime,
   formatMonthTime,
   getYesterdayDayPicker,
   parseTimeKey,
@@ -179,13 +264,15 @@
 const { proxy } = getCurrentInstance();
 
 const loading = ref(false);
-const chartRef = ref(null);
-let chartInstance = null;
+const trendChartRef = ref(null);
+const periodChartRef = ref(null);
+const shiftChartRef = ref(null);
+const dayTypeChartRef = ref(null);
+const chartInstances = {};
 
-const queryForm = reactive({ dimension: "day" });
-const chartType = ref("bar");
-const summary = ref({});
-const chartRecords = ref([]);
+const queryForm = reactive({ dimension: "day", trendGranularity: "hour" });
+const chartType = ref("line");
+const analytics = ref({});
 const detailRecords = ref([]);
 const detailVisible = ref(false);
 const detailRow = ref(null);
@@ -196,51 +283,42 @@
 const yearRange = ref([]);
 
 const isSingleDay = computed(() => {
-  if (queryForm.dimension !== "day" || !dayRange.value?.length) return false;
+  if (!["day", "week"].includes(queryForm.dimension) || !dayRange.value?.length) return false;
   return dayRange.value[0] === dayRange.value[1];
 });
 
-const chartDimension = computed(() => (isSingleDay.value ? "hour" : queryForm.dimension));
-
-const chartTitle = computed(() =>
-  isSingleDay.value ? "24灏忔椂鐢ㄧ數瓒嬪娍" : "鐢ㄧ數閲忓姣�"
-);
-
-const summaryLabels = computed(() => {
-  if (isSingleDay.value) {
-    return {
-      total: "鏃ユ�荤敤鐢甸噺",
-      avg: "灏忔椂骞冲潎鐢ㄧ數閲�",
-      max: "灏忔椂鏈�澶х敤鐢甸噺",
-      min: "灏忔椂鏈�灏忕敤鐢甸噺",
-    };
-  }
-  const unitMap = { day: "鏃�", month: "鏈�", quarter: "瀛e害", year: "骞�" };
-  const unit = unitMap[queryForm.dimension] || "鏈�";
-  return {
-    total: "鎬荤敤鐢甸噺",
-    avg: `骞冲潎${unit}鐢ㄧ數閲廯,
-    max: `鏈�澶�${unit}鐢ㄧ數閲廯,
-    min: `鏈�灏�${unit}鐢ㄧ數閲廯,
-  };
+const trendGranularityLabel = computed(() => {
+  const map = { hour: "灏忔椂", day: "鏃�", week: "鍛�", month: "鏈�", year: "骞�" };
+  return map[analytics.value.trendGranularity || queryForm.trendGranularity] || "鏃�";
 });
 
 const hasPeriodData = computed(() =>
-  detailRecords.value.some((row) =>
-    hasPeriodValue(row, "sharpConsumption")
-    || hasPeriodValue(row, "peakConsumption")
+  (analytics.value.periodSplits || []).length > 0
+  || detailRecords.value.some((row) =>
+    hasPeriodValue(row, "peakConsumption")
     || hasPeriodValue(row, "flatConsumption")
     || hasPeriodValue(row, "valleyConsumption")
   )
 );
 
+const splitTableRows = computed(() => {
+  const rows = [];
+  const push = (category, list) => {
+    (list || []).forEach((item) => rows.push({ category, ...item }));
+  };
+  push("宄板钩璋�", analytics.value.periodSplits);
+  push("鐝", analytics.value.shiftSplits);
+  push("鏃ョ被鍨�", analytics.value.dayTypeSplits);
+  return rows;
+});
+
 const dayShortcuts = [
   {
     text: "鏄ㄦ棩",
     value: () => {
-      const yesterday = new Date();
-      yesterday.setDate(yesterday.getDate() - 1);
-      return [yesterday, yesterday];
+      const d = new Date();
+      d.setDate(d.getDate() - 1);
+      return [d, d];
     },
   },
   {
@@ -251,7 +329,17 @@
       return [start, end];
     },
   },
+  {
+    text: "杩�30澶�",
+    value: () => {
+      const end = new Date();
+      const start = new Date(end.getTime() - 29 * 86400000);
+      return [start, end];
+    },
+  },
 ];
+
+const PIE_COLORS = ["#409EFF", "#67C23A", "#E6A23C", "#F56C6C", "#909399"];
 
 function hasPeriodValue(row, field) {
   const n = Number(row?.[field]);
@@ -260,10 +348,20 @@
 
 function formatKwh(value, digits = 2) {
   const n = Number(value);
-  if (!Number.isFinite(n)) {
-    return (0).toFixed(digits);
-  }
+  if (!Number.isFinite(n)) return (0).toFixed(digits);
   return n.toFixed(digits);
+}
+
+function formatDelta(comp) {
+  if (!comp) return "-";
+  const sign = comp.delta >= 0 ? "+" : "";
+  return `${sign}${formatKwh(comp.delta)} kWh (${sign}${formatKwh(comp.changeRate, 1)}%)`;
+}
+
+function deltaClass(delta) {
+  if (delta > 0) return "delta up";
+  if (delta < 0) return "delta down";
+  return "delta";
 }
 
 function initDefaultRange() {
@@ -281,7 +379,7 @@
 
 function buildTimeParams() {
   const dim = queryForm.dimension;
-  if (dim === "day") {
+  if (dim === "day" || dim === "week") {
     return {
       startTime: dayRange.value[0].replace(/-/g, ""),
       endTime: dayRange.value[1].replace(/-/g, ""),
@@ -299,65 +397,125 @@
       endTime: quarterRange.value[1].replace(/-/g, ""),
     };
   }
-  return {
-    startTime: yearRange.value[0],
-    endTime: yearRange.value[1],
-  };
+  return { startTime: yearRange.value[0], endTime: yearRange.value[1] };
+}
+
+function syncTrendGranularity() {
+  if (isSingleDay.value) {
+    queryForm.trendGranularity = "hour";
+    chartType.value = "line";
+    return;
+  }
+  if (queryForm.trendGranularity === "hour") {
+    queryForm.trendGranularity = "day";
+  }
+  chartType.value = "bar";
 }
 
 async function handleQuery() {
+  syncTrendGranularity();
   loading.value = true;
   try {
-    const params = { dimension: queryForm.dimension, ...buildTimeParams() };
-    const res = await summaryStatisticEle(params);
-    summary.value = res.data || {};
-    chartRecords.value = res.data?.chartRecords || [];
+    const params = {
+      dimension: queryForm.dimension,
+      trendGranularity: queryForm.trendGranularity,
+      ...buildTimeParams(),
+    };
+    const res = await analyticsStatisticEle(params);
+    analytics.value = res.data || {};
     detailRecords.value = res.data?.records || [];
-    syncChartType();
-    renderChart();
+    await nextTick();
+    renderAllCharts();
   } finally {
     loading.value = false;
   }
 }
 
-function syncChartType() {
-  chartType.value = isSingleDay.value ? "line" : "bar";
+function getChart(key, refEl) {
+  if (!refEl) return null;
+  if (!chartInstances[key]) {
+    chartInstances[key] = echarts.init(refEl);
+  }
+  return chartInstances[key];
 }
 
-function renderChart() {
-  if (!chartRef.value) return;
-  if (!chartInstance) {
-    chartInstance = echarts.init(chartRef.value);
-  }
-  const dim = chartDimension.value;
-  const labels = chartRecords.value.map((item) => parseTimeKey(item.timeKey, dim));
-  const values = chartRecords.value.map((item) => Number(formatKwh(item.totalConsumption)));
+function renderTrendChart() {
+  const chart = getChart("trend", trendChartRef.value);
+  if (!chart) return;
+  const gran = analytics.value.trendGranularity || queryForm.trendGranularity;
+  const records = analytics.value.trendRecords || analytics.value.chartRecords || [];
+  const labels = records.map((item) => parseTimeKey(item.timeKey, gran));
+  const values = records.map((item) => Number(formatKwh(item.totalConsumption)));
   const type = isSingleDay.value ? "line" : chartType.value;
-
-  chartInstance.setOption({
+  chart.setOption({
     tooltip: { trigger: "axis" },
     grid: { left: 50, right: 20, top: 30, bottom: 50 },
-    xAxis: {
-      type: "category",
-      data: labels,
-      axisLabel: { rotate: isSingleDay.value ? 45 : 30, fontSize: 11 },
-    },
+    xAxis: { type: "category", data: labels, axisLabel: { rotate: gran === "hour" ? 45 : 30, fontSize: 11 } },
     yAxis: { type: "value", name: "kWh" },
-    series: [
-      {
-        name: "鎬荤敤鐢甸噺",
-        type,
-        data: values,
-        smooth: type === "line",
-        areaStyle: type === "line" ? { opacity: 0.12 } : undefined,
-        itemStyle: { color: "#409EFF" },
-        barMaxWidth: 40,
-      },
-    ],
+    series: [{
+      name: "鐢ㄧ數閲�",
+      type,
+      data: values,
+      smooth: type === "line",
+      areaStyle: type === "line" ? { opacity: 0.1 } : undefined,
+      itemStyle: { color: "#409EFF" },
+      barMaxWidth: 36,
+    }],
   }, true);
 }
 
+function renderPieChart(key, refEl, items, title) {
+  const chart = getChart(key, refEl);
+  if (!chart) return;
+  const data = (items || []).map((item, i) => ({
+    name: item.name,
+    value: Number(formatKwh(item.consumption)),
+    itemStyle: { color: PIE_COLORS[i % PIE_COLORS.length] },
+  }));
+  chart.setOption({
+    title: data.length ? undefined : { text: "鏆傛棤鏁版嵁", left: "center", top: "center", textStyle: { color: "#909399", fontSize: 13 } },
+    tooltip: { trigger: "item", formatter: "{b}: {c} kWh ({d}%)" },
+    legend: { bottom: 0, type: "scroll" },
+    series: [{
+      name: title,
+      type: "pie",
+      radius: ["40%", "65%"],
+      center: ["50%", "45%"],
+      data,
+      label: { formatter: "{b}\n{d}%" },
+    }],
+  }, true);
+}
+
+function renderBarChart(key, refEl, items, title) {
+  const chart = getChart(key, refEl);
+  if (!chart) return;
+  const list = items || [];
+  chart.setOption({
+    title: list.length ? undefined : { text: "鏆傛棤鏁版嵁", left: "center", top: "center", textStyle: { color: "#909399", fontSize: 13 } },
+    tooltip: { trigger: "axis" },
+    grid: { left: 50, right: 16, top: 20, bottom: 30 },
+    xAxis: { type: "category", data: list.map((i) => i.name) },
+    yAxis: { type: "value", name: "kWh" },
+    series: [{
+      name: title,
+      type: "bar",
+      data: list.map((i) => Number(formatKwh(i.consumption))),
+      itemStyle: { color: "#409EFF" },
+      barMaxWidth: 40,
+    }],
+  }, true);
+}
+
+function renderAllCharts() {
+  renderTrendChart();
+  renderPieChart("period", periodChartRef.value, analytics.value.periodSplits, "宄板钩璋�");
+  renderBarChart("shift", shiftChartRef.value, analytics.value.shiftSplits, "鐝");
+  renderPieChart("dayType", dayTypeChartRef.value, analytics.value.dayTypeSplits, "鏃ョ被鍨�");
+}
+
 function handleDimensionChange() {
+  syncTrendGranularity();
   handleQuery();
 }
 
@@ -378,12 +536,12 @@
 }
 
 function handleResize() {
-  chartInstance?.resize();
+  Object.values(chartInstances).forEach((c) => c?.resize());
 }
 
 watch(isSingleDay, () => {
-  syncChartType();
-  renderChart();
+  syncTrendGranularity();
+  renderAllCharts();
 });
 
 onMounted(() => {
@@ -394,84 +552,68 @@
 
 onBeforeUnmount(() => {
   window.removeEventListener("resize", handleResize);
-  chartInstance?.dispose();
-  chartInstance = null;
+  Object.values(chartInstances).forEach((c) => c?.dispose());
 });
 </script>
 
 <style scoped>
-.card-header {
-  display: flex;
-  align-items: center;
-  gap: 12px;
+.card-header { display: flex; align-items: center; gap: 12px; }
+.card-header .desc { font-size: 13px; color: #909399; }
+.search-form { margin-bottom: 12px; }
+.time-range-item { margin-right: 0; }
+.section-title {
+  font-weight: 600;
+  font-size: 14px;
+  margin: 16px 0 10px;
+  padding-left: 8px;
+  border-left: 3px solid #409eff;
 }
-.card-header .desc {
-  font-size: 13px;
-  color: #909399;
-}
-.search-form {
-  margin-bottom: 16px;
-}
-.time-range-item {
-  margin-right: 0;
-}
-.time-range-row {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-}
-.summary-row {
-  margin-bottom: 16px;
-}
+.summary-row { margin-bottom: 8px; }
 .summary-card {
   background: #f5f7fa;
   border-radius: 8px;
-  padding: 20px;
+  padding: 16px 12px;
   text-align: center;
+  min-height: 96px;
 }
-.summary-card.total {
-  background: linear-gradient(135deg, #409eff22, #409eff11);
+.summary-card.total { background: linear-gradient(135deg, #409eff22, #409eff11); }
+.summary-card.load { background: linear-gradient(135deg, #67c23a22, #67c23a11); }
+.summary-card .label { font-size: 12px; color: #909399; margin-bottom: 6px; }
+.summary-card .value { font-size: 22px; font-weight: 600; }
+.summary-card .value span { font-size: 12px; font-weight: 400; color: #909399; }
+.summary-card .hint { font-size: 11px; color: #c0c4cc; margin-top: 4px; }
+.compare-row { margin-bottom: 8px; }
+.compare-card {
+  background: #fafafa;
+  border: 1px solid #ebeef5;
+  border-radius: 8px;
+  padding: 14px 16px;
 }
-.summary-card .label {
-  font-size: 13px;
-  color: #909399;
-  margin-bottom: 8px;
+.compare-label { font-weight: 500; margin-bottom: 10px; }
+.compare-body { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; }
+.compare-body .sub { display: block; font-size: 12px; color: #909399; margin-bottom: 2px; }
+.delta { font-weight: 600; font-size: 13px; }
+.delta.up { color: #f56c6c; }
+.delta.down { color: #67c23a; }
+.chart-panel, .split-table-panel {
+  border: 1px solid #ebeef5;
+  border-radius: 8px;
+  padding: 12px;
+  margin-bottom: 12px;
 }
-.summary-card .value {
-  font-size: 26px;
-  font-weight: 600;
-}
-.summary-card .value span {
-  font-size: 13px;
-  font-weight: 400;
-  color: #909399;
-}
+.sub-chart-row { margin-top: 0; }
 .chart-toolbar {
   display: flex;
   justify-content: space-between;
   align-items: center;
   margin-bottom: 8px;
   font-weight: 500;
-}
-.chart-container {
-  width: 100%;
-  height: 380px;
-  margin-bottom: 20px;
-}
-.detail-title {
-  font-weight: 500;
-  margin-bottom: 10px;
-}
-.meter-cell {
-  display: flex;
-  flex-direction: column;
-  gap: 2px;
-}
-.meter-name {
   font-size: 13px;
 }
-.meter-id {
-  font-size: 12px;
-  color: #909399;
-}
+.chart-container { width: 100%; height: 320px; }
+.chart-container.short { height: 280px; }
+.detail-title { font-weight: 500; margin: 8px 0 10px; }
+.meter-cell { display: flex; flex-direction: column; gap: 2px; }
+.meter-name { font-size: 13px; }
+.meter-id { font-size: 12px; color: #909399; }
 </style>

--
Gitblit v1.9.3