| | |
| | | <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-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> |
| | |
| | | </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"> |
| | |
| | | </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"> |
| | |
| | | <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> |
| | |
| | | </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, |
| | |
| | | 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); |
| | |
| | | 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]; |
| | | }, |
| | | }, |
| | | { |
| | |
| | | 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]); |
| | |
| | | |
| | | 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() { |
| | |
| | | |
| | | 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, ""), |
| | |
| | | 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(); |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | function handleResize() { |
| | | chartInstance?.resize(); |
| | | Object.values(chartInstances).forEach((c) => c?.resize()); |
| | | } |
| | | |
| | | watch(isSingleDay, () => { |
| | | syncChartType(); |
| | | renderChart(); |
| | | syncTrendGranularity(); |
| | | renderAllCharts(); |
| | | }); |
| | | |
| | | onMounted(() => { |
| | |
| | | |
| | | 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> |