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