| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <!-- 昨日用电快览 --> |
| | | <el-card class="yesterday-card" v-loading="yesterdayLoading"> |
| | | <div class="yesterday-header"> |
| | | <div> |
| | | <h3>昨日用电量</h3> |
| | | <p class="sub">{{ yesterdayLabel }}</p> |
| | | </div> |
| | | <el-button type="primary" link @click="viewYesterdayDetail">查看昨日明细</el-button> |
| | | </div> |
| | | <el-row :gutter="16"> |
| | | <el-col :span="6"> |
| | | <div class="metric-box highlight"> |
| | | <div class="metric-label">总用电量</div> |
| | | <div class="metric-value">{{ formatKwh(yesterdaySummary.totalConsumption) }} <span>kWh</span></div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="metric-box"> |
| | | <div class="metric-label">平均用电量</div> |
| | | <div class="metric-value">{{ formatKwh(yesterdaySummary.avgConsumption) }} <span>kWh</span></div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="metric-box"> |
| | | <div class="metric-label">最大用电量</div> |
| | | <div class="metric-value">{{ formatKwh(yesterdaySummary.maxConsumption) }} <span>kWh</span></div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="metric-box"> |
| | | <div class="metric-label">最小用电量</div> |
| | | <div class="metric-value">{{ formatKwh(yesterdaySummary.minConsumption) }} <span>kWh</span></div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | </el-card> |
| | | |
| | | <el-card> |
| | | <template #header> |
| | | <div class="card-header"> |
| | |
| | | <el-radio-button value="year">年</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="时间范围"> |
| | | <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'" |
| | |
| | | range-separator="至" |
| | | value-format="YYYY" |
| | | /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button @click="setYesterday">昨日</el-button> |
| | | <el-button @click="setLast7Days">近7天</el-button> |
| | | <el-button type="primary" :loading="loading" @click="handleQuery">查询</el-button> |
| | | <el-button @click="handleExport">导出</el-button> |
| | | </el-form-item> |
| | |
| | | <el-row :gutter="16" class="summary-row"> |
| | | <el-col :span="6"> |
| | | <div class="summary-card total"> |
| | | <div class="label">总用电量</div> |
| | | <div class="label">{{ summaryLabels.total }}</div> |
| | | <div class="value">{{ formatKwh(summary.totalConsumption) }} <span>kWh</span></div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="summary-card"> |
| | | <div class="label">平均用电量</div> |
| | | <div class="label">{{ summaryLabels.avg }}</div> |
| | | <div class="value">{{ formatKwh(summary.avgConsumption) }} <span>kWh</span></div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="summary-card"> |
| | | <div class="label">最大用电量</div> |
| | | <div class="label">{{ summaryLabels.max }}</div> |
| | | <div class="value">{{ formatKwh(summary.maxConsumption) }} <span>kWh</span></div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="summary-card"> |
| | | <div class="label">最小用电量</div> |
| | | <div class="label">{{ summaryLabels.min }}</div> |
| | | <div class="value">{{ formatKwh(summary.minConsumption) }} <span>kWh</span></div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <div class="chart-toolbar"> |
| | | <span>趋势图</span> |
| | | <el-radio-group v-model="chartType" size="small" @change="renderChart"> |
| | | <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> |
| | |
| | | {{ parseTimeKey(row.timeKey, queryForm.dimension) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="meterName" label="电表名称" min-width="120" show-overflow-tooltip> |
| | | <template #default="{ row }">{{ row.meterName || row.address || row.meterId || "-" }}</template> |
| | | <el-table-column label="电表" min-width="160" show-overflow-tooltip> |
| | | <template #default="{ row }"> |
| | | <div class="meter-cell"> |
| | | <span class="meter-name">{{ row.meterName || row.address || "-" }}</span> |
| | | <span v-if="row.meterId" class="meter-id">ID: {{ row.meterId }}</span> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="address" label="表地址" min-width="120" show-overflow-tooltip /> |
| | | <el-table-column prop="totalConsumption" label="总电量(kWh)" width="120"> |
| | | <template #default="{ row }">{{ formatKwh(row.totalConsumption) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="sharpConsumption" label="尖(kWh)" width="100"> |
| | | <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 prop="peakConsumption" label="峰(kWh)" width="100"> |
| | | <el-table-column v-if="hasPeriodData" prop="peakConsumption" label="峰(kWh)" width="100"> |
| | | <template #default="{ row }">{{ formatKwh(row.peakConsumption) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="flatConsumption" label="平(kWh)" width="100"> |
| | | <el-table-column v-if="hasPeriodData" prop="flatConsumption" label="平(kWh)" width="100"> |
| | | <template #default="{ row }">{{ formatKwh(row.flatConsumption) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="valleyConsumption" label="谷(kWh)" width="100"> |
| | | <el-table-column v-if="hasPeriodData" prop="valleyConsumption" label="谷(kWh)" width="100"> |
| | | <template #default="{ row }">{{ formatKwh(row.valleyConsumption) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="startTime" label="开始时间" min-width="150" /> |
| | | <el-table-column prop="endTime" label="结束时间" min-width="150" /> |
| | | <el-table-column label="操作" width="80" fixed="right"> |
| | | <template #default="{ row }"> |
| | | <el-button type="primary" link @click="showDetail(row)">详情</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | |
| | | <el-dialog v-model="detailVisible" title="用电明细详情" width="520px" destroy-on-close> |
| | | <el-descriptions v-if="detailRow" :column="1" border size="small"> |
| | | <el-descriptions-item label="时间"> |
| | | {{ parseTimeKey(detailRow.timeKey, queryForm.dimension) }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="电表名称">{{ detailRow.meterName || "-" }}</el-descriptions-item> |
| | | <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> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, getCurrentInstance, onBeforeUnmount, onMounted, reactive, ref } from "vue"; |
| | | import { computed, getCurrentInstance, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import * as echarts from "echarts"; |
| | | import { |
| | | summaryStatisticEle, |
| | | getYesterdaySummary, |
| | | formatDayPicker, |
| | | formatDayTime, |
| | | formatMonthTime, |
| | |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const loading = ref(false); |
| | | const yesterdayLoading = ref(false); |
| | | const chartRef = ref(null); |
| | | let chartInstance = null; |
| | | |
| | |
| | | const summary = ref({}); |
| | | const chartRecords = ref([]); |
| | | const detailRecords = ref([]); |
| | | const yesterdaySummary = ref({}); |
| | | const detailVisible = ref(false); |
| | | const detailRow = ref(null); |
| | | |
| | | const dayRange = ref([]); |
| | | const monthRange = ref([]); |
| | | const quarterRange = ref([]); |
| | | const yearRange = ref([]); |
| | | |
| | | const yesterdayLabel = computed(() => getYesterdayDayPicker()); |
| | | const isSingleDay = computed(() => { |
| | | if (queryForm.dimension !== "day" || !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 hasPeriodData = computed(() => |
| | | detailRecords.value.some((row) => |
| | | hasPeriodValue(row, "sharpConsumption") |
| | | || hasPeriodValue(row, "peakConsumption") |
| | | || hasPeriodValue(row, "flatConsumption") |
| | | || hasPeriodValue(row, "valleyConsumption") |
| | | ) |
| | | ); |
| | | |
| | | const dayShortcuts = [ |
| | | { |
| | | text: "昨日", |
| | | value: () => { |
| | | const yesterday = new Date(); |
| | | yesterday.setDate(yesterday.getDate() - 1); |
| | | return [yesterday, yesterday]; |
| | | }, |
| | | }, |
| | | { |
| | | text: "近7天", |
| | | value: () => { |
| | | const end = new Date(); |
| | | const start = new Date(end.getTime() - 6 * 86400000); |
| | | return [start, end]; |
| | | }, |
| | | }, |
| | | ]; |
| | | |
| | | function hasPeriodValue(row, field) { |
| | | const n = Number(row?.[field]); |
| | | return Number.isFinite(n) && n !== 0; |
| | | } |
| | | |
| | | function formatKwh(value, digits = 2) { |
| | | const n = Number(value); |
| | |
| | | }; |
| | | } |
| | | |
| | | async function loadYesterday() { |
| | | yesterdayLoading.value = true; |
| | | try { |
| | | const res = await getYesterdaySummary(); |
| | | yesterdaySummary.value = res.data || {}; |
| | | } finally { |
| | | yesterdayLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function handleQuery() { |
| | | loading.value = true; |
| | | try { |
| | |
| | | summary.value = res.data || {}; |
| | | chartRecords.value = res.data?.chartRecords || []; |
| | | detailRecords.value = res.data?.records || []; |
| | | syncChartType(); |
| | | renderChart(); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | function syncChartType() { |
| | | chartType.value = isSingleDay.value ? "line" : "bar"; |
| | | } |
| | | |
| | | function renderChart() { |
| | |
| | | if (!chartInstance) { |
| | | chartInstance = echarts.init(chartRef.value); |
| | | } |
| | | const labels = chartRecords.value.map((item) => |
| | | parseTimeKey(item.timeKey, queryForm.dimension) |
| | | ); |
| | | const values = chartRecords.value.map((item) => |
| | | Number(formatKwh(item.totalConsumption)) |
| | | ); |
| | | const dim = chartDimension.value; |
| | | const labels = chartRecords.value.map((item) => parseTimeKey(item.timeKey, dim)); |
| | | const values = chartRecords.value.map((item) => Number(formatKwh(item.totalConsumption))); |
| | | const type = isSingleDay.value ? "line" : chartType.value; |
| | | |
| | | chartInstance.setOption({ |
| | | tooltip: { trigger: "axis" }, |
| | | grid: { left: 50, right: 20, top: 30, bottom: 50 }, |
| | | xAxis: { type: "category", data: labels, axisLabel: { rotate: 30, fontSize: 11 } }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: labels, |
| | | axisLabel: { rotate: isSingleDay.value ? 45 : 30, fontSize: 11 }, |
| | | }, |
| | | yAxis: { type: "value", name: "kWh" }, |
| | | series: [ |
| | | { |
| | | name: "总用电量", |
| | | type: chartType.value, |
| | | type, |
| | | data: values, |
| | | smooth: chartType.value === "line", |
| | | areaStyle: chartType.value === "line" ? { opacity: 0.12 } : undefined, |
| | | smooth: type === "line", |
| | | areaStyle: type === "line" ? { opacity: 0.12 } : undefined, |
| | | itemStyle: { color: "#409EFF" }, |
| | | barMaxWidth: 40, |
| | | }, |
| | | ], |
| | | }); |
| | | } |
| | | |
| | | function setYesterday() { |
| | | queryForm.dimension = "day"; |
| | | const yesterday = getYesterdayDayPicker(); |
| | | dayRange.value = [yesterday, yesterday]; |
| | | handleQuery(); |
| | | } |
| | | |
| | | function setLast7Days() { |
| | | queryForm.dimension = "day"; |
| | | const now = new Date(); |
| | | const weekAgo = new Date(now.getTime() - 6 * 86400000); |
| | | dayRange.value = [formatDayPicker(weekAgo), formatDayPicker(now)]; |
| | | handleQuery(); |
| | | } |
| | | |
| | | function viewYesterdayDetail() { |
| | | setYesterday(); |
| | | }, true); |
| | | } |
| | | |
| | | function handleDimensionChange() { |
| | | handleQuery(); |
| | | } |
| | | |
| | | function showDetail(row) { |
| | | detailRow.value = row; |
| | | detailVisible.value = true; |
| | | } |
| | | |
| | | function handleExport() { |
| | |
| | | chartInstance?.resize(); |
| | | } |
| | | |
| | | watch(isSingleDay, () => { |
| | | syncChartType(); |
| | | renderChart(); |
| | | }); |
| | | |
| | | onMounted(() => { |
| | | initDefaultRange(); |
| | | loadYesterday(); |
| | | handleQuery(); |
| | | window.addEventListener("resize", handleResize); |
| | | }); |
| | |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .yesterday-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | .yesterday-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: flex-start; |
| | | margin-bottom: 16px; |
| | | } |
| | | .yesterday-header h3 { |
| | | margin: 0 0 4px; |
| | | font-size: 18px; |
| | | } |
| | | .yesterday-header .sub { |
| | | margin: 0; |
| | | color: #909399; |
| | | font-size: 13px; |
| | | } |
| | | .metric-box { |
| | | background: #f5f7fa; |
| | | border-radius: 8px; |
| | | padding: 16px; |
| | | text-align: center; |
| | | } |
| | | .metric-box.highlight { |
| | | background: linear-gradient(135deg, #409eff22, #409eff11); |
| | | } |
| | | .metric-label { |
| | | font-size: 13px; |
| | | color: #909399; |
| | | margin-bottom: 8px; |
| | | } |
| | | .metric-value { |
| | | font-size: 24px; |
| | | font-weight: 600; |
| | | } |
| | | .metric-value span { |
| | | font-size: 13px; |
| | | font-weight: 400; |
| | | color: #909399; |
| | | } |
| | | .card-header { |
| | | display: flex; |
| | | align-items: center; |
| | |
| | | } |
| | | .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; |
| | |
| | | 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; |
| | | } |
| | | </style> |