yuan
3 天以前 4ec37425fba3bc5aa8ceab98b9b4de333375f4f2
feat: 添加能耗综合分析功能,优化统计维度和趋势粒度选择
已修改2个文件
521 ■■■■■ 文件已修改
src/api/energyManagement/statisticEle.js 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energyManagement/energyStatistics/index.vue 508 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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)}`;
  }
src/views/energyManagement/energyStatistics/index.vue
@@ -4,23 +4,23 @@
      <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">季度</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-if="queryForm.dimension === 'day' || queryForm.dimension === 'week'"
              v-model="dayRange"
              type="daterange"
              range-separator="至"
@@ -48,7 +48,14 @@
              range-separator="至"
              value-format="YYYY"
            />
          </div>
        </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="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>{{ chartTitle }}</span>
              <span>用电趋势({{ trendGranularityLabel }})</span>
        <el-radio-group
          v-if="!isSingleDay"
          v-model="chartType"
          size="small"
          @change="renderChart"
                @change="renderAllCharts"
        >
          <el-radio-button value="line">折线图</el-radio-button>
          <el-radio-button value="bar">柱状图</el-radio-button>
                <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 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: "季度", 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: "总用电量",
    series: [{
      name: "用电量",
        type,
        data: values,
        smooth: type === "line",
        areaStyle: type === "line" ? { opacity: 0.12 } : undefined,
      areaStyle: type === "line" ? { opacity: 0.1 } : undefined,
        itemStyle: { color: "#409EFF" },
        barMaxWidth: 40,
      },
    ],
      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>