| | |
| | | <template> |
| | | <div class="energy-cost-page"> |
| | | <!-- 筛选区域 --> |
| | | <el-card class="filter-card" shadow="never"> |
| | | <el-card class="filter-card" |
| | | shadow="never"> |
| | | <template #header> |
| | | <div class="card-head"> |
| | | <div class="card-head-left"> |
| | |
| | | <span class="card-title">查询条件</span> |
| | | </div> |
| | | <div class="card-head-right"> |
| | | <el-radio-group |
| | | v-model="statisticsType" |
| | | <el-radio-group v-model="statisticsType" |
| | | size="small" |
| | | @change="handleTypeChange" |
| | | > |
| | | @change="handleTypeChange"> |
| | | <el-radio-button label="day">按日</el-radio-button> |
| | | <el-radio-button label="month">按月</el-radio-button> |
| | | <el-radio-button label="year">按年</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="filter-layout"> |
| | | <el-form :model="searchForm" :inline="true" class="filter-form"> |
| | | <el-form :model="searchForm" |
| | | :inline="true" |
| | | class="filter-form"> |
| | | <!-- <el-form-item label="能耗类型"> |
| | | <el-select v-model="searchForm.energyType" |
| | | placeholder="全部" |
| | |
| | | </el-select> |
| | | </el-form-item> --> |
| | | <el-form-item label="时间范围"> |
| | | <el-date-picker |
| | | v-if="statisticsType === 'day'" |
| | | <el-date-picker v-if="statisticsType === 'day'" |
| | | v-model="searchForm.dateRange" |
| | | type="daterange" |
| | | range-separator="至" |
| | |
| | | end-placeholder="结束日期" |
| | | value-format="YYYY-MM-DD" |
| | | class="w-260" |
| | | @change="handleQuery" |
| | | /> |
| | | <el-date-picker |
| | | v-else |
| | | @change="handleQuery" /> |
| | | <el-date-picker v-else-if="statisticsType === 'month'" |
| | | v-model="searchForm.monthRange" |
| | | type="monthrange" |
| | | range-separator="至" |
| | |
| | | end-placeholder="结束月份" |
| | | value-format="YYYY-MM" |
| | | class="w-260" |
| | | @change="handleQuery" |
| | | /> |
| | | @change="handleQuery" /> |
| | | <el-select v-else-if="statisticsType === 'year'" |
| | | v-model="searchForm.selectedYear" |
| | | placeholder="请选择年份" |
| | | class="w-260" |
| | | @change="handleYearChange"> |
| | | <el-option v-for="year in recentYears" |
| | | :key="year" |
| | | :label="year + '年'" |
| | | :value="year" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div class="filter-actions"> |
| | | <el-button |
| | | class="lux-btn" |
| | | <el-button class="lux-btn" |
| | | type="primary" |
| | | :loading="tableLoading" |
| | | @click="handleQuery" |
| | | >刷新</el-button |
| | | > |
| | | <el-button class="lux-btn" @click="handleReset">重置</el-button> |
| | | <el-button class="lux-btn" type="success" plain @click="handleExport" |
| | | >导出</el-button |
| | | > |
| | | @click="handleQuery">刷新</el-button> |
| | | <el-button class="lux-btn" |
| | | @click="handleReset">重置</el-button> |
| | | <el-button class="lux-btn" |
| | | type="success" |
| | | plain |
| | | @click="handleExport">导出</el-button> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- 图表区域 --> |
| | | <div class="charts"> |
| | | <el-card class="panel-card" shadow="never"> |
| | | <div |
| | | class="kpi-strip" |
| | | <el-card class="panel-card" |
| | | shadow="never"> |
| | | <div class="kpi-strip" |
| | | :class="{ pulse: queryPulse }" |
| | | title="快捷键:Enter 刷新 / Esc 重置 / Alt+E 导出" |
| | | > |
| | | <button |
| | | class="kpi-item kpi-total" |
| | | title="快捷键:Enter 刷新 / Esc 重置 / Alt+E 导出"> |
| | | <button class="kpi-item kpi-total" |
| | | type="button" |
| | | :class="{ selected: selectedKpi === 'all' }" |
| | | @click="handleKpiClick('all')" |
| | | > |
| | | @click="handleKpiClick('all')"> |
| | | <div class="kpi-left"> |
| | | <div class="kpi-label">总能耗成本</div> |
| | | <div class="kpi-value"> |
| | | ¥{{ formatMoney(animatedOverview.totalEnergyCost) }} |
| | | </div> |
| | | <div class="kpi-meta"> |
| | | <span |
| | | class="kpi-chip" |
| | | <span class="kpi-chip" |
| | | :class="kpiDelta.total.pct >= 0 ? 'up' : 'down'" |
| | | v-if="kpiDelta.total.valid" |
| | | >{{ kpiDelta.total.pct >= 0 ? "+" : "" |
| | | }}{{ kpiDelta.total.pct.toFixed(1) }}%</span |
| | | > |
| | | <svg class="kpi-spark" viewBox="0 0 72 22" aria-hidden="true"> |
| | | <polyline |
| | | :points="sparklinePoints(kpiSeries.total)" |
| | | v-if="kpiDelta.total.valid">{{ kpiDelta.total.pct >= 0 ? "+" : "" |
| | | }}{{ kpiDelta.total.pct.toFixed(1) }}%</span> |
| | | <svg class="kpi-spark" |
| | | viewBox="0 0 72 22" |
| | | aria-hidden="true"> |
| | | <polyline :points="sparklinePoints(kpiSeries.total)" |
| | | fill="none" |
| | | stroke="rgba(47, 111, 237, 0.85)" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" |
| | | /> |
| | | stroke-linejoin="round" /> |
| | | </svg> |
| | | </div> |
| | | </div> |
| | |
| | | <Money /> |
| | | </el-icon> |
| | | </div> |
| | | <div class="kpi-actions" @click.stop> |
| | | <button |
| | | class="kpi-action" |
| | | <div class="kpi-actions" |
| | | @click.stop> |
| | | <button class="kpi-action" |
| | | type="button" |
| | | @click="copyKpi('totalEnergyCost')" |
| | | > |
| | | @click="copyKpi('totalEnergyCost')"> |
| | | 复制 |
| | | </button> |
| | | <button |
| | | class="kpi-action" |
| | | <button class="kpi-action" |
| | | type="button" |
| | | @click="viewKpiDetails('all')" |
| | | > |
| | | @click="viewKpiDetails('all')"> |
| | | 明细 |
| | | </button> |
| | | </div> |
| | | </button> |
| | | <button |
| | | class="kpi-item kpi-production" |
| | | <button class="kpi-item kpi-production" |
| | | type="button" |
| | | :class="{ selected: selectedKpi === 'production' }" |
| | | @click="handleKpiClick('production')" |
| | | > |
| | | @click="handleKpiClick('production')"> |
| | | <div class="kpi-left"> |
| | | <div class="kpi-label">生产能耗成本</div> |
| | | <div class="kpi-value"> |
| | | ¥{{ formatMoney(animatedOverview.productEnergyCost) }} |
| | | </div> |
| | | <div class="kpi-meta"> |
| | | <span |
| | | class="kpi-chip" |
| | | <span class="kpi-chip" |
| | | :class="kpiDelta.production.pct >= 0 ? 'up' : 'down'" |
| | | v-if="kpiDelta.production.valid" |
| | | >{{ kpiDelta.production.pct >= 0 ? "+" : "" |
| | | }}{{ kpiDelta.production.pct.toFixed(1) }}%</span |
| | | > |
| | | <svg class="kpi-spark" viewBox="0 0 72 22" aria-hidden="true"> |
| | | <polyline |
| | | :points="sparklinePoints(kpiSeries.production)" |
| | | v-if="kpiDelta.production.valid">{{ kpiDelta.production.pct >= 0 ? "+" : "" |
| | | }}{{ kpiDelta.production.pct.toFixed(1) }}%</span> |
| | | <svg class="kpi-spark" |
| | | viewBox="0 0 72 22" |
| | | aria-hidden="true"> |
| | | <polyline :points="sparklinePoints(kpiSeries.production)" |
| | | fill="none" |
| | | stroke="rgba(22, 163, 74, 0.85)" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" |
| | | /> |
| | | stroke-linejoin="round" /> |
| | | </svg> |
| | | </div> |
| | | </div> |
| | |
| | | <DataLine /> |
| | | </el-icon> |
| | | </div> |
| | | <div class="kpi-actions" @click.stop> |
| | | <button |
| | | class="kpi-action" |
| | | <div class="kpi-actions" |
| | | @click.stop> |
| | | <button class="kpi-action" |
| | | type="button" |
| | | @click="copyKpi('productEnergyCost')" |
| | | > |
| | | @click="copyKpi('productEnergyCost')"> |
| | | 复制 |
| | | </button> |
| | | <button |
| | | class="kpi-action" |
| | | <button class="kpi-action" |
| | | type="button" |
| | | @click="viewKpiDetails('production')" |
| | | > |
| | | @click="viewKpiDetails('production')"> |
| | | 明细 |
| | | </button> |
| | | </div> |
| | | </button> |
| | | <button |
| | | class="kpi-item kpi-office" |
| | | <button class="kpi-item kpi-office" |
| | | type="button" |
| | | :class="{ selected: selectedKpi === 'office' }" |
| | | @click="handleKpiClick('office')" |
| | | > |
| | | @click="handleKpiClick('office')"> |
| | | <div class="kpi-left"> |
| | | <div class="kpi-label">办公能耗成本</div> |
| | | <div class="kpi-value"> |
| | | ¥{{ formatMoney(animatedOverview.officeEnergyCost) }} |
| | | </div> |
| | | <div class="kpi-meta"> |
| | | <span |
| | | class="kpi-chip" |
| | | <span class="kpi-chip" |
| | | :class="kpiDelta.office.pct >= 0 ? 'up' : 'down'" |
| | | v-if="kpiDelta.office.valid" |
| | | >{{ kpiDelta.office.pct >= 0 ? "+" : "" |
| | | }}{{ kpiDelta.office.pct.toFixed(1) }}%</span |
| | | > |
| | | <svg class="kpi-spark" viewBox="0 0 72 22" aria-hidden="true"> |
| | | <polyline |
| | | :points="sparklinePoints(kpiSeries.office)" |
| | | v-if="kpiDelta.office.valid">{{ kpiDelta.office.pct >= 0 ? "+" : "" |
| | | }}{{ kpiDelta.office.pct.toFixed(1) }}%</span> |
| | | <svg class="kpi-spark" |
| | | viewBox="0 0 72 22" |
| | | aria-hidden="true"> |
| | | <polyline :points="sparklinePoints(kpiSeries.office)" |
| | | fill="none" |
| | | stroke="rgba(100, 116, 139, 0.90)" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" |
| | | /> |
| | | stroke-linejoin="round" /> |
| | | </svg> |
| | | </div> |
| | | </div> |
| | |
| | | <TrendCharts /> |
| | | </el-icon> |
| | | </div> |
| | | <div class="kpi-actions" @click.stop> |
| | | <button |
| | | class="kpi-action" |
| | | <div class="kpi-actions" |
| | | @click.stop> |
| | | <button class="kpi-action" |
| | | type="button" |
| | | @click="copyKpi('officeEnergyCost')" |
| | | > |
| | | @click="copyKpi('officeEnergyCost')"> |
| | | 复制 |
| | | </button> |
| | | <button |
| | | class="kpi-action" |
| | | <button class="kpi-action" |
| | | type="button" |
| | | @click="viewKpiDetails('office')" |
| | | > |
| | | @click="viewKpiDetails('office')"> |
| | | 明细 |
| | | </button> |
| | | </div> |
| | | </button> |
| | | <button |
| | | class="kpi-item kpi-avg" |
| | | <button class="kpi-item kpi-avg" |
| | | type="button" |
| | | @click="handleKpiClick('all')" |
| | | > |
| | | @click="handleKpiClick('all')"> |
| | | <div class="kpi-left"> |
| | | <div class="kpi-label">平均成本</div> |
| | | <div class="kpi-value"> |
| | | ¥{{ formatMoney(animatedOverview.averageEnergyCost) }} |
| | | <span class="kpi-unit" |
| | | >/{{ statisticsType === "day" ? "日" : "月" }}</span |
| | | > |
| | | <span class="kpi-unit">/{{ statisticsType === "day" ? "日" : statisticsType === "month" ? "月" : "年" }}</span> |
| | | </div> |
| | | <div class="kpi-meta muted">基于当前筛选与明细统计</div> |
| | | </div> |
| | |
| | | <Histogram /> |
| | | </el-icon> |
| | | </div> |
| | | <div class="kpi-actions" @click.stop> |
| | | <button |
| | | class="kpi-action" |
| | | <div class="kpi-actions" |
| | | @click.stop> |
| | | <button class="kpi-action" |
| | | type="button" |
| | | @click="copyKpi('averageEnergyCost')" |
| | | > |
| | | @click="copyKpi('averageEnergyCost')"> |
| | | 复制 |
| | | </button> |
| | | <button |
| | | class="kpi-action" |
| | | <button class="kpi-action" |
| | | type="button" |
| | | @click="viewKpiDetails('all')" |
| | | > |
| | | @click="viewKpiDetails('all')"> |
| | | 明细 |
| | | </button> |
| | | </div> |
| | | </button> |
| | | </div> |
| | | |
| | | <div class="panel-head"> |
| | | <div |
| | | class="segmented" |
| | | <div class="segmented" |
| | | role="tablist" |
| | | aria-label="分析面板切换" |
| | | :class="{ 'no-active': chartPanel === 'none' }" |
| | | > |
| | | <div |
| | | class="segmented-indicator" |
| | | :class="{ 'no-active': chartPanel === 'none' }"> |
| | | <div class="segmented-indicator" |
| | | :class="{ hidden: chartPanel === 'none' }" |
| | | :style="panelIndicatorStyle" |
| | | ></div> |
| | | <button |
| | | class="segmented-item" |
| | | :style="panelIndicatorStyle"></div> |
| | | <button class="segmented-item" |
| | | type="button" |
| | | role="tab" |
| | | :aria-selected="chartPanel === 'core'" |
| | | :class="{ active: chartPanel === 'core' }" |
| | | @click="handleChartPanelClick('core')" |
| | | > |
| | | @click="handleChartPanelClick('core')"> |
| | | <span class="seg-title">核心分析</span> |
| | | <span class="seg-sub">趋势 / 类型占比</span> |
| | | </button> |
| | | <button |
| | | class="segmented-item" |
| | | <button class="segmented-item" |
| | | type="button" |
| | | role="tab" |
| | | :aria-selected="chartPanel === 'advanced'" |
| | | :class="{ active: chartPanel === 'advanced' }" |
| | | @click="handleChartPanelClick('advanced')" |
| | | > |
| | | @click="handleChartPanelClick('advanced')"> |
| | | <span class="seg-title">高级分析</span> |
| | | <span class="seg-sub">用途占比 / 单价对比</span> |
| | | </button> |
| | | </div> |
| | | </div> |
| | | |
| | | <transition name="lux-collapse"> |
| | | <div v-show="chartPanel === 'core'" class="panel-body"> |
| | | <div v-show="chartPanel === 'core'" |
| | | class="panel-body"> |
| | | <el-row :gutter="16"> |
| | | <el-col :xs="24" :lg="12"> |
| | | <el-card class="chart-card" shadow="never"> |
| | | <el-col :xs="24" |
| | | :lg="12"> |
| | | <el-card class="chart-card" |
| | | shadow="never"> |
| | | <template #header> |
| | | <div class="chart-head"> |
| | | <span class="chart-title">能耗成本趋势</span> |
| | | <div class="chart-tools" @click.stop> |
| | | <button |
| | | class="chart-tool" |
| | | <div class="chart-tools" |
| | | @click.stop> |
| | | <button class="chart-tool" |
| | | type="button" |
| | | @click="downloadChart('cost', '能耗成本趋势')" |
| | | > |
| | | @click="downloadChart('cost', '能耗成本趋势')"> |
| | | 下载 |
| | | </button> |
| | | <button |
| | | class="chart-tool" |
| | | <button class="chart-tool" |
| | | type="button" |
| | | @click="openBigChart('cost', '能耗成本趋势')" |
| | | > |
| | | @click="openBigChart('cost', '能耗成本趋势')"> |
| | | 大图 |
| | | </button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <div |
| | | ref="costChartWrap" |
| | | <div ref="costChartWrap" |
| | | class="chart-wrap" |
| | | v-loading="tableLoading" |
| | | > |
| | | <div |
| | | ref="costChart" |
| | | v-loading="tableLoading"> |
| | | <div ref="costChart" |
| | | class="chart-content" |
| | | v-show="hasTableData" |
| | | ></div> |
| | | <div class="chart-empty" v-show="!hasTableData"> |
| | | v-show="hasTableData"></div> |
| | | <div class="chart-empty" |
| | | v-show="!hasTableData"> |
| | | <el-empty description="暂无数据" /> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="24" :lg="12"> |
| | | <el-card class="chart-card" shadow="never"> |
| | | <el-col :xs="24" |
| | | :lg="12"> |
| | | <el-card class="chart-card" |
| | | shadow="never"> |
| | | <template #header> |
| | | <div class="chart-head"> |
| | | <span class="chart-title">能耗类型成本占比</span> |
| | | <div class="chart-tools" @click.stop> |
| | | <button |
| | | class="chart-tool" |
| | | <div class="chart-tools" |
| | | @click.stop> |
| | | <button class="chart-tool" |
| | | type="button" |
| | | @click="downloadChart('type', '能耗类型成本占比')" |
| | | > |
| | | @click="downloadChart('type', '能耗类型成本占比')"> |
| | | 下载 |
| | | </button> |
| | | <button |
| | | class="chart-tool" |
| | | <button class="chart-tool" |
| | | type="button" |
| | | @click="openBigChart('type', '能耗类型成本占比')" |
| | | > |
| | | @click="openBigChart('type', '能耗类型成本占比')"> |
| | | 大图 |
| | | </button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <div |
| | | ref="typeChartWrap" |
| | | <div ref="typeChartWrap" |
| | | class="chart-wrap" |
| | | v-loading="tableLoading" |
| | | > |
| | | <div |
| | | ref="typeChart" |
| | | v-loading="tableLoading"> |
| | | <div ref="typeChart" |
| | | class="chart-content" |
| | | v-show="hasTableData" |
| | | ></div> |
| | | <div class="chart-empty" v-show="!hasTableData"> |
| | | v-show="hasTableData"></div> |
| | | <div class="chart-empty" |
| | | v-show="!hasTableData"> |
| | | <el-empty description="暂无数据" /> |
| | | </div> |
| | | </div> |
| | |
| | | </el-row> |
| | | </div> |
| | | </transition> |
| | | |
| | | <transition name="lux-collapse"> |
| | | <div v-show="chartPanel === 'advanced'" class="panel-body"> |
| | | <el-row :gutter="16" class="charts-row"> |
| | | <el-col :xs="24" :lg="12"> |
| | | <el-card class="chart-card" shadow="never"> |
| | | <div v-show="chartPanel === 'advanced'" |
| | | class="panel-body"> |
| | | <el-row :gutter="16" |
| | | class="charts-row"> |
| | | <el-col :xs="24" |
| | | :lg="12"> |
| | | <el-card class="chart-card" |
| | | shadow="never"> |
| | | <template #header> |
| | | <div class="chart-head"> |
| | | <span class="chart-title">能耗用途成本占比</span> |
| | | <div class="chart-tools" @click.stop> |
| | | <button |
| | | class="chart-tool" |
| | | <div class="chart-tools" |
| | | @click.stop> |
| | | <button class="chart-tool" |
| | | type="button" |
| | | @click="downloadChart('purpose', '能耗用途成本占比')" |
| | | > |
| | | @click="downloadChart('purpose', '能耗用途成本占比')"> |
| | | 下载 |
| | | </button> |
| | | <button |
| | | class="chart-tool" |
| | | <button class="chart-tool" |
| | | type="button" |
| | | @click="openBigChart('purpose', '能耗用途成本占比')" |
| | | > |
| | | @click="openBigChart('purpose', '能耗用途成本占比')"> |
| | | 大图 |
| | | </button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <div |
| | | ref="purposeChartWrap" |
| | | <div ref="purposeChartWrap" |
| | | class="chart-wrap" |
| | | v-loading="tableLoading" |
| | | > |
| | | <div |
| | | ref="purposeChart" |
| | | v-loading="tableLoading"> |
| | | <div ref="purposeChart" |
| | | class="chart-content" |
| | | v-show="hasTableData" |
| | | ></div> |
| | | <div class="chart-empty" v-show="!hasTableData"> |
| | | v-show="hasTableData"></div> |
| | | <div class="chart-empty" |
| | | v-show="!hasTableData"> |
| | | <el-empty description="暂无数据" /> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="24" :lg="12"> |
| | | <el-card class="chart-card" shadow="never"> |
| | | <el-col :xs="24" |
| | | :lg="12"> |
| | | <el-card class="chart-card" |
| | | shadow="never"> |
| | | <template #header> |
| | | <div class="chart-head"> |
| | | <span class="chart-title">能耗用量对比</span> |
| | | <div class="chart-tools" @click.stop> |
| | | <button |
| | | class="chart-tool" |
| | | <div class="chart-tools" |
| | | @click.stop> |
| | | <button class="chart-tool" |
| | | type="button" |
| | | @click="downloadChart('unit', '能耗用量对比')" |
| | | > |
| | | @click="downloadChart('unit', '能耗用量对比')"> |
| | | 下载 |
| | | </button> |
| | | <button |
| | | class="chart-tool" |
| | | <button class="chart-tool" |
| | | type="button" |
| | | @click="openBigChart('unit', '能耗用量对比')" |
| | | > |
| | | @click="openBigChart('unit', '能耗用量对比')"> |
| | | 大图 |
| | | </button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <div |
| | | ref="unitChartWrap" |
| | | <div ref="unitChartWrap" |
| | | class="chart-wrap" |
| | | v-loading="tableLoading" |
| | | > |
| | | <div |
| | | ref="unitChart" |
| | | v-loading="tableLoading"> |
| | | <div ref="unitChart" |
| | | class="chart-content" |
| | | v-show="hasTableData" |
| | | ></div> |
| | | <div class="chart-empty" v-show="!hasTableData"> |
| | | v-show="hasTableData"></div> |
| | | <div class="chart-empty" |
| | | v-show="!hasTableData"> |
| | | <el-empty description="暂无数据" /> |
| | | </div> |
| | | </div> |
| | |
| | | </transition> |
| | | </el-card> |
| | | </div> |
| | | |
| | | <el-dialog |
| | | v-model="bigChartVisible" |
| | | <el-dialog v-model="bigChartVisible" |
| | | :title="bigChartTitle" |
| | | width="92%" |
| | | top="6vh" |
| | | class="big-chart-dialog" |
| | | destroy-on-close |
| | | @opened="handleBigChartOpened" |
| | | @closed="handleBigChartClosed" |
| | | > |
| | | <div ref="bigChartEl" class="big-chart-canvas"></div> |
| | | @closed="handleBigChartClosed"> |
| | | <div ref="bigChartEl" |
| | | class="big-chart-canvas"></div> |
| | | <template #footer> |
| | | <div class="big-chart-footer"> |
| | | <el-button |
| | | class="lux-btn" |
| | | @click="downloadChart(bigChartKey, bigChartTitle)" |
| | | >下载图片</el-button |
| | | > |
| | | <el-button |
| | | class="lux-btn" |
| | | <el-button class="lux-btn" |
| | | @click="downloadChart(bigChartKey, bigChartTitle)">下载图片</el-button> |
| | | <el-button class="lux-btn" |
| | | type="primary" |
| | | @click="bigChartVisible = false" |
| | | >关闭</el-button |
| | | > |
| | | @click="bigChartVisible = false">关闭</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- 数据表格 --> |
| | | <el-card class="table-card" shadow="never"> |
| | | <el-card class="table-card" |
| | | shadow="never"> |
| | | <div ref="tableAnchor"></div> |
| | | <template #header> |
| | | <div class="card-head"> |
| | |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-table |
| | | :data="displayTableData" |
| | | <el-table :data="displayTableData" |
| | | v-loading="tableLoading" |
| | | stripe |
| | | :header-cell-style="{ height: '44px' }" |
| | | class="data-table lux-table" |
| | | @sort-change="handleSortChange" |
| | | > |
| | | @sort-change="handleSortChange"> |
| | | <template #empty> |
| | | <el-empty description="暂无明细数据" /> |
| | | </template> |
| | | <el-table-column type="index" label="序号" width="60" align="center" /> |
| | | <el-table-column |
| | | prop="meterReadingDate" |
| | | <el-table-column type="index" |
| | | label="序号" |
| | | width="60" |
| | | align="center" /> |
| | | <el-table-column prop="meterReadingDate" |
| | | :label="timeColumnLabel" |
| | | align="center" |
| | | sortable="custom" |
| | | /> |
| | | <el-table-column |
| | | prop="energyTyep" |
| | | sortable="custom" /> |
| | | <el-table-column prop="energyTyep" |
| | | label="能耗类型" |
| | | width="100" |
| | | align="center" |
| | | :filters="energyTypeFilters" |
| | | :filter-method="filterEnergyType" |
| | | filter-placement="bottom-end" |
| | | > |
| | | filter-placement="bottom-end"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getEnergyTypeType(scope.row.energyTyep)"> |
| | | {{ scope.row.energyTyep }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | prop="type" |
| | | <el-table-column prop="type" |
| | | label="能耗用途" |
| | | width="100" |
| | | align="center" |
| | | :filters="energyPurposeFilters" |
| | | :filter-method="filterEnergyPurpose" |
| | | filter-placement="bottom-end" |
| | | > |
| | | filter-placement="bottom-end"> |
| | | <template #default="scope"> |
| | | <el-tag :type="scope.row.type === '生产' ? 'primary' : 'warning'"> |
| | | {{ scope.row.type }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="dosage" label="用量" align="right"> |
| | | <el-table-column prop="dosage" |
| | | label="用量" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="unit-value">{{ |
| | | formatNumber(scope.row.dosage, 2) |
| | |
| | | <span class="unit-unit">{{ scope.row.unit }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | prop="unitPrice" |
| | | <el-table-column prop="unitPrice" |
| | | label="单价(元)" |
| | | align="right" |
| | | sortable="custom" |
| | | > |
| | | sortable="custom"> |
| | | <template #default="scope"> |
| | | <span class="price-value">{{ |
| | | formatNumber(scope.row.unitPrice, 2) |
| | | }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | prop="cost" |
| | | <el-table-column prop="cost" |
| | | label="成本(元)" |
| | | align="right" |
| | | sortable="custom" |
| | | fixed="right" |
| | | > |
| | | fixed="right"> |
| | | <template #default="scope"> |
| | | <span class="cost-value" |
| | | >¥{{ formatNumber(scope.row.cost, 2) }}</span |
| | | > |
| | | <span class="cost-value">¥{{ formatNumber(scope.row.cost, 2) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <div class="pagination-container"> |
| | | <el-pagination |
| | | v-model:current-page="page.current" |
| | | <el-pagination v-model:current-page="page.current" |
| | | v-model:page-size="page.size" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | :total="page.total" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | @size-change="handleSizeChange" |
| | | @current-change="handleCurrentChange" |
| | | /> |
| | | @current-change="handleCurrentChange" /> |
| | | </div> |
| | | </el-card> |
| | | </div> |
| | |
| | | start.setMonth(start.getMonth() - 2); |
| | | return [start.toISOString().slice(0, 7), end.toISOString().slice(0, 7)]; |
| | | })(), |
| | | selectedYear: new Date().getFullYear(), // 默认今年 |
| | | }); |
| | | |
| | | // 最近七年 |
| | | const recentYears = computed(() => { |
| | | const currentYear = new Date().getFullYear(); |
| | | const years = []; |
| | | for (let i = 6; i >= 0; i--) { |
| | | years.push(currentYear - i); |
| | | } |
| | | return years; |
| | | }); |
| | | |
| | | // 时间列标签 |
| | | const timeColumnLabel = computed(() => { |
| | | return statisticsType.value === "day" ? "日期" : "月份"; |
| | | if (statisticsType.value === "day") return "日期"; |
| | | if (statisticsType.value === "month") return "月份"; |
| | | if (statisticsType.value === "year") return "年份"; |
| | | return "时间"; |
| | | }); |
| | | |
| | | // 统计概览 |
| | |
| | | averageEnergyCost: 0, |
| | | }); |
| | | |
| | | const formatMoney = (v) => { |
| | | const formatMoney = v => { |
| | | const n = Number.parseFloat(v); |
| | | const value = Number.isFinite(n) ? n : 0; |
| | | return value.toLocaleString("zh-CN", { |
| | |
| | | const from = animatedOverview[key] || 0; |
| | | const to = Number.isFinite(toValue) ? toValue : 0; |
| | | const start = performance.now(); |
| | | const easeOut = (t) => 1 - Math.pow(1 - t, 3); |
| | | const easeOut = t => 1 - Math.pow(1 - t, 3); |
| | | |
| | | const tick = (now) => { |
| | | const tick = now => { |
| | | const p = Math.min(1, (now - start) / duration); |
| | | animatedOverview[key] = from + (to - from) * easeOut(p); |
| | | if (p < 1) requestAnimationFrame(tick); |
| | |
| | | |
| | | watch( |
| | | () => ({ ...overview }), |
| | | (val) => { |
| | | val => { |
| | | animateNumber("totalEnergyCost", Number.parseFloat(val.totalEnergyCost)); |
| | | animateNumber("productEnergyCost", Number.parseFloat(val.productEnergyCost)); |
| | | animateNumber( |
| | | "productEnergyCost", |
| | | Number.parseFloat(val.productEnergyCost) |
| | | ); |
| | | animateNumber("officeEnergyCost", Number.parseFloat(val.officeEnergyCost)); |
| | | animateNumber("averageEnergyCost", Number.parseFloat(val.averageEnergyCost)); |
| | | animateNumber( |
| | | "averageEnergyCost", |
| | | Number.parseFloat(val.averageEnergyCost) |
| | | ); |
| | | }, |
| | | { deep: true, immediate: true } |
| | | ); |
| | |
| | | const times = Array.from(byTime.keys()).sort((a, b) => |
| | | String(a).localeCompare(String(b)) |
| | | ); |
| | | const total = times.map((t) => byTime.get(t).total); |
| | | const production = times.map((t) => byTime.get(t).production); |
| | | const office = times.map((t) => byTime.get(t).office); |
| | | const total = times.map(t => byTime.get(t).total); |
| | | const production = times.map(t => byTime.get(t).production); |
| | | const office = times.map(t => byTime.get(t).office); |
| | | return { times, total, production, office }; |
| | | }); |
| | | |
| | | const kpiDelta = computed(() => { |
| | | const pick = (arr) => { |
| | | const pick = arr => { |
| | | const a = Array.isArray(arr) ? arr : []; |
| | | if (a.length < 2) return { pct: 0, valid: false }; |
| | | const prev = a[a.length - 2]; |
| | |
| | | }; |
| | | }); |
| | | |
| | | const sparklinePoints = (values) => { |
| | | const sparklinePoints = values => { |
| | | const v = (Array.isArray(values) ? values : []).slice(-12); |
| | | if (v.length < 2) return ""; |
| | | const min = Math.min(...v); |
| | |
| | | .join(" "); |
| | | }; |
| | | |
| | | const handleKpiClick = (key) => { |
| | | const handleKpiClick = key => { |
| | | selectedKpi.value = key; |
| | | if (key === "all") searchForm.type = ""; |
| | | if (key === "production") searchForm.type = "生产"; |
| | |
| | | handleQuery(); |
| | | }; |
| | | |
| | | const viewKpiDetails = (key) => { |
| | | const viewKpiDetails = key => { |
| | | handleKpiClick(key); |
| | | nextTick(() => { |
| | | const el = tableAnchor.value; |
| | |
| | | }); |
| | | }; |
| | | |
| | | const copyKpi = async (field) => { |
| | | const copyKpi = async field => { |
| | | const map = { |
| | | totalEnergyCost: animatedOverview.totalEnergyCost, |
| | | productEnergyCost: animatedOverview.productEnergyCost, |
| | |
| | | } |
| | | }; |
| | | |
| | | const getChartByKey = (key) => { |
| | | const getChartByKey = key => { |
| | | if (key === "cost") return costChartInstance; |
| | | if (key === "type") return typeChartInstance; |
| | | if (key === "purpose") return purposeChartInstance; |
| | |
| | | return null; |
| | | }; |
| | | |
| | | const ensurePanelForChart = (key) => { |
| | | const ensurePanelForChart = key => { |
| | | if (key === "cost" || key === "type") chartPanel.value = "core"; |
| | | if (key === "purpose" || key === "unit") chartPanel.value = "advanced"; |
| | | }; |
| | |
| | | if (statisticsType.value === "day") { |
| | | if (searchForm.dateRange?.length === 2) |
| | | rangePart = `_${searchForm.dateRange[0]}~${searchForm.dateRange[1]}`; |
| | | } else { |
| | | } else if (statisticsType.value === "month") { |
| | | if (searchForm.monthRange?.length === 2) |
| | | rangePart = `_${searchForm.monthRange[0]}~${searchForm.monthRange[1]}`; |
| | | } else if (statisticsType.value === "year") { |
| | | if (searchForm.yearRange?.length === 2) |
| | | rangePart = `_${searchForm.yearRange[0]}~${searchForm.yearRange[1]}`; |
| | | } |
| | | a.download = `${title || "chart"}${typePart}${purposePart}${rangePart}.png`; |
| | | a.click(); |
| | |
| | | const bigChartEl = ref(null); |
| | | let bigChartInstance = null; |
| | | |
| | | watch(bigChartVisible, (v) => { |
| | | watch(bigChartVisible, v => { |
| | | if (v) window.addEventListener("resize", handleBigChartResize); |
| | | else window.removeEventListener("resize", handleBigChartResize); |
| | | }); |
| | |
| | | // 图表区切换:core | advanced | none(点击当前选中可收起) |
| | | const chartPanel = ref("core"); |
| | | |
| | | const ensureChartsReady = (panel) => { |
| | | const ensureChartsReady = panel => { |
| | | if (panel === "core") { |
| | | if (costChart.value && !costChartInstance) { |
| | | costChartInstance = echarts.init(costChart.value); |
| | |
| | | }); |
| | | }; |
| | | |
| | | const handleChartPanelClick = (key) => { |
| | | const handleChartPanelClick = key => { |
| | | chartPanel.value = chartPanel.value === key ? "none" : key; |
| | | }; |
| | | |
| | |
| | | return { transform: `translateX(${x})` }; |
| | | }); |
| | | |
| | | watch(chartPanel, (val) => { |
| | | watch(chartPanel, val => { |
| | | if (val !== "none") resizeChartsAfterExpand(); |
| | | }); |
| | | |
| | | // 监听表格数据变化,确保数据加载后图表正确渲染 |
| | | watch(tableData, () => { |
| | | watch( |
| | | tableData, |
| | | () => { |
| | | nextTick(() => { |
| | | updateCharts(); |
| | | nextTick(() => { |
| | | handleResize(); |
| | | }); |
| | | }); |
| | | }, { deep: true }); |
| | | }, |
| | | { deep: true } |
| | | ); |
| | | |
| | | // 获取能耗类型标签类型 |
| | | const getEnergyTypeType = (type) => { |
| | | const getEnergyTypeType = type => { |
| | | const typeMap = { |
| | | 水: "primary", |
| | | 电: "warning", |
| | |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: data.map((item) => item.meterReadingDate), |
| | | data: data.map(item => item.meterReadingDate), |
| | | axisLabel: { |
| | | rotate: statisticsType.value === "day" ? 45 : 0, |
| | | color: "rgba(15, 23, 42, 0.62)", |
| | |
| | | { |
| | | name: "生产能耗成本", |
| | | type: "bar", |
| | | data: data.map((item) => (item.type === "生产" ? item.cost : 0)), |
| | | data: data.map(item => (item.type === "生产" ? item.cost : 0)), |
| | | itemStyle: { |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { offset: 0, color: "#409EFF" }, |
| | |
| | | { |
| | | name: "办公能耗成本", |
| | | type: "bar", |
| | | data: data.map((item) => (item.type === "办公" ? item.cost : 0)), |
| | | data: data.map(item => (item.type === "办公" ? item.cost : 0)), |
| | | itemStyle: { |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { offset: 0, color: "#67C23A" }, |
| | |
| | | const data = tableData.value; |
| | | const typeCosts = {}; |
| | | |
| | | data.forEach((item) => { |
| | | data.forEach(item => { |
| | | if (!typeCosts[item.energyTyep]) { |
| | | typeCosts[item.energyTyep] = 0; |
| | | } |
| | |
| | | 办公: 0, |
| | | }; |
| | | |
| | | data.forEach((item) => { |
| | | data.forEach(item => { |
| | | if (purposeCosts.hasOwnProperty(item.type)) { |
| | | purposeCosts[item.type] += parseFloat(item.cost); |
| | | } |
| | |
| | | const data = tableData.value; |
| | | const unitData = {}; |
| | | |
| | | data.forEach((item) => { |
| | | data.forEach(item => { |
| | | if (!unitData[item.energyTyep]) { |
| | | unitData[item.energyTyep] = { |
| | | 生产: 0, |
| | |
| | | }; |
| | | } |
| | | if (unitData[item.energyTyep].hasOwnProperty(item.type)) { |
| | | unitData[item.energyTyep][item.type] += parseFloat( |
| | | item.dosage || 0 |
| | | ); |
| | | unitData[item.energyTyep][item.type] += parseFloat(item.dosage || 0); |
| | | } |
| | | }); |
| | | |
| | | const energyTypes = Object.keys(unitData); |
| | | const productionConsumptions = energyTypes.map( |
| | | (type) => unitData[type].生产 |
| | | ); |
| | | const officeConsumptions = energyTypes.map( |
| | | (type) => unitData[type].办公 |
| | | ); |
| | | const productionConsumptions = energyTypes.map(type => unitData[type].生产); |
| | | const officeConsumptions = energyTypes.map(type => unitData[type].办公); |
| | | |
| | | const option = { |
| | | tooltip: { |
| | |
| | | start.toISOString().split("T")[0], |
| | | end.toISOString().split("T")[0], |
| | | ]; |
| | | } else { |
| | | } else if (statisticsType.value === "month") { |
| | | const end = new Date(); |
| | | const start = new Date(); |
| | | start.setMonth(start.getMonth() - 2); |
| | |
| | | start.toISOString().slice(0, 7), |
| | | end.toISOString().slice(0, 7), |
| | | ]; |
| | | } else if (statisticsType.value === "year") { |
| | | searchForm.selectedYear = new Date().getFullYear(); // 默认今年 |
| | | } |
| | | page.current = 1; |
| | | handleQuery(); |
| | | }; |
| | | |
| | | // 年份选择变化 |
| | | const handleYearChange = () => { |
| | | page.current = 1; |
| | | handleQuery(); |
| | | }; |
| | | |
| | | // 查询 |
| | | const handleQuery = () => { |
| | | |
| | | queryPulse.value = true; |
| | | window.setTimeout(() => { |
| | | queryPulse.value = false; |
| | |
| | | // type: searchForm.type || undefined, |
| | | pageNum: page.current, |
| | | pageSize: page.size, |
| | | state: |
| | | statisticsType.value === "day" |
| | | ? "日" |
| | | : statisticsType.value === "month" |
| | | ? "月" |
| | | : "年", |
| | | }; |
| | | |
| | | if (statisticsType.value === "day") { |
| | |
| | | params.startDate = searchForm.dateRange[0]; |
| | | params.endDate = searchForm.dateRange[1]; |
| | | } |
| | | } else { |
| | | } else if (statisticsType.value === "month") { |
| | | if (searchForm.monthRange && searchForm.monthRange.length === 2) { |
| | | params.startDate = searchForm.monthRange[0] + "-01"; |
| | | |
| | |
| | | params.endDate = searchForm.monthRange[1] + "-01"; |
| | | } |
| | | } |
| | | } else if (statisticsType.value === "year") { |
| | | if (searchForm.selectedYear) { |
| | | const year = searchForm.selectedYear; |
| | | params.startDate = year + "-01-01"; |
| | | params.endDate = year + "-12-31"; |
| | | } |
| | | } |
| | | |
| | | // 计算开始到结束的天数(包含起止两天) |
| | |
| | | |
| | | // 调用接口获取数据 |
| | | energyConsumptionDetailAccount(params) |
| | | .then((res) => { |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | const data = res.data; |
| | | overview.totalEnergyCost = data.totalEnergyCost || "0"; |
| | |
| | | overview.averageEnergyCost = "0.00"; |
| | | } |
| | | }) |
| | | .catch((err) => { |
| | | .catch(err => { |
| | | ElMessage.error("获取数据异常"); |
| | | tableData.value = []; |
| | | page.total = 0; |
| | |
| | | start.toISOString().split("T")[0], |
| | | end.toISOString().split("T")[0], |
| | | ]; |
| | | } else { |
| | | } else if (statisticsType.value === "month") { |
| | | const end = new Date(); |
| | | const start = new Date(); |
| | | start.setMonth(start.getMonth() - 2); |
| | |
| | | start.toISOString().slice(0, 7), |
| | | end.toISOString().slice(0, 7), |
| | | ]; |
| | | } else if (statisticsType.value === "year") { |
| | | searchForm.selectedYear = new Date().getFullYear(); // 默认今年 |
| | | } |
| | | page.current = 1; |
| | | handleQuery(); |
| | |
| | | }; |
| | | |
| | | // 分页大小变化 |
| | | const handleSizeChange = (val) => { |
| | | const handleSizeChange = val => { |
| | | page.size = val; |
| | | page.current = 1; |
| | | handleQuery(); |
| | | }; |
| | | |
| | | // 页码变化 |
| | | const handleCurrentChange = (val) => { |
| | | const handleCurrentChange = val => { |
| | | page.current = val; |
| | | handleQuery(); |
| | | }; |
| | |
| | | window.addEventListener("resize", handleResize); |
| | | }); |
| | | |
| | | const handleGlobalHotkeys = (e) => { |
| | | const handleGlobalHotkeys = e => { |
| | | // 避免在输入框内误触 |
| | | const target = e?.target; |
| | | const tag = target?.tagName?.toLowerCase?.(); |