// 能耗成本核算
|
<template>
|
<div class="energy-cost-page">
|
<!-- 筛选区域 -->
|
<el-card class="filter-card" shadow="never">
|
<template #header>
|
<div class="card-head">
|
<div class="card-head-left">
|
<el-icon class="card-icon ui-icon">
|
<DataLine />
|
</el-icon>
|
<span class="card-title">查询条件</span>
|
</div>
|
<div class="card-head-right">
|
<el-radio-group
|
v-model="statisticsType"
|
size="small"
|
@change="handleTypeChange"
|
>
|
<el-radio-button label="day">按日</el-radio-button>
|
<el-radio-button label="month">按月</el-radio-button>
|
</el-radio-group>
|
</div>
|
</div>
|
</template>
|
|
<div class="filter-layout">
|
<el-form :model="searchForm" :inline="true" class="filter-form">
|
<!-- <el-form-item label="能耗类型">
|
<el-select v-model="searchForm.energyType"
|
placeholder="全部"
|
clearable
|
class="w-140"
|
@change="handleQuery">
|
<el-option label="全部"
|
value="全部" />
|
<el-option label="水"
|
value="水" />
|
<el-option label="电"
|
value="电" />
|
<el-option label="气"
|
value="气" />
|
</el-select>
|
</el-form-item> -->
|
<!-- <el-form-item label="能耗用途">
|
<el-select
|
v-model="searchForm.type"
|
placeholder=""
|
clearable
|
class="w-140"
|
@change="handleQuery"
|
>
|
<el-option label="生产" value="生产" />
|
<el-option label="办公" value="办公" />
|
</el-select>
|
</el-form-item> -->
|
<el-form-item label="时间范围">
|
<el-date-picker
|
v-if="statisticsType === 'day'"
|
v-model="searchForm.dateRange"
|
type="daterange"
|
range-separator="至"
|
start-placeholder="开始日期"
|
end-placeholder="结束日期"
|
value-format="YYYY-MM-DD"
|
class="w-260"
|
@change="handleQuery"
|
/>
|
<el-date-picker
|
v-else
|
v-model="searchForm.monthRange"
|
type="monthrange"
|
range-separator="至"
|
start-placeholder="开始月份"
|
end-placeholder="结束月份"
|
value-format="YYYY-MM"
|
class="w-260"
|
@change="handleQuery"
|
/>
|
</el-form-item>
|
</el-form>
|
<div class="filter-actions">
|
<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
|
>
|
</div>
|
</div>
|
</el-card>
|
|
<!-- 图表区域 -->
|
<div class="charts">
|
<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"
|
type="button"
|
:class="{ selected: selectedKpi === 'all' }"
|
@click="handleKpiClick('all')"
|
>
|
<div class="kpi-left">
|
<div class="kpi-label">总能耗成本</div>
|
<div class="kpi-value">
|
¥{{ formatMoney(animatedOverview.totalCost) }}
|
</div>
|
<div class="kpi-meta">
|
<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)"
|
fill="none"
|
stroke="rgba(47, 111, 237, 0.85)"
|
stroke-width="2"
|
stroke-linecap="round"
|
stroke-linejoin="round"
|
/>
|
</svg>
|
</div>
|
</div>
|
<div class="kpi-icon">
|
<el-icon class="ui-icon">
|
<Money />
|
</el-icon>
|
</div>
|
<div class="kpi-actions" @click.stop>
|
<button
|
class="kpi-action"
|
type="button"
|
@click="copyKpi('totalCost')"
|
>
|
复制
|
</button>
|
<button
|
class="kpi-action"
|
type="button"
|
@click="viewKpiDetails('all')"
|
>
|
明细
|
</button>
|
</div>
|
</button>
|
<button
|
class="kpi-item kpi-production"
|
type="button"
|
:class="{ selected: selectedKpi === 'production' }"
|
@click="handleKpiClick('production')"
|
>
|
<div class="kpi-left">
|
<div class="kpi-label">生产能耗成本</div>
|
<div class="kpi-value">
|
¥{{ formatMoney(animatedOverview.productionCost) }}
|
</div>
|
<div class="kpi-meta">
|
<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)"
|
fill="none"
|
stroke="rgba(22, 163, 74, 0.85)"
|
stroke-width="2"
|
stroke-linecap="round"
|
stroke-linejoin="round"
|
/>
|
</svg>
|
</div>
|
</div>
|
<div class="kpi-icon">
|
<el-icon class="ui-icon">
|
<DataLine />
|
</el-icon>
|
</div>
|
<div class="kpi-actions" @click.stop>
|
<button
|
class="kpi-action"
|
type="button"
|
@click="copyKpi('productionCost')"
|
>
|
复制
|
</button>
|
<button
|
class="kpi-action"
|
type="button"
|
@click="viewKpiDetails('production')"
|
>
|
明细
|
</button>
|
</div>
|
</button>
|
<button
|
class="kpi-item kpi-office"
|
type="button"
|
:class="{ selected: selectedKpi === 'office' }"
|
@click="handleKpiClick('office')"
|
>
|
<div class="kpi-left">
|
<div class="kpi-label">办公能耗成本</div>
|
<div class="kpi-value">
|
¥{{ formatMoney(animatedOverview.officeCost) }}
|
</div>
|
<div class="kpi-meta">
|
<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)"
|
fill="none"
|
stroke="rgba(100, 116, 139, 0.90)"
|
stroke-width="2"
|
stroke-linecap="round"
|
stroke-linejoin="round"
|
/>
|
</svg>
|
</div>
|
</div>
|
<div class="kpi-icon">
|
<el-icon class="ui-icon">
|
<TrendCharts />
|
</el-icon>
|
</div>
|
<div class="kpi-actions" @click.stop>
|
<button
|
class="kpi-action"
|
type="button"
|
@click="copyKpi('officeCost')"
|
>
|
复制
|
</button>
|
<button
|
class="kpi-action"
|
type="button"
|
@click="viewKpiDetails('office')"
|
>
|
明细
|
</button>
|
</div>
|
</button>
|
<button
|
class="kpi-item kpi-avg"
|
type="button"
|
@click="handleKpiClick('all')"
|
>
|
<div class="kpi-left">
|
<div class="kpi-label">平均成本</div>
|
<div class="kpi-value">
|
¥{{ formatMoney(animatedOverview.avgCost) }}
|
<span class="kpi-unit"
|
>/{{ statisticsType === "day" ? "日" : "月" }}</span
|
>
|
</div>
|
<div class="kpi-meta muted">基于当前筛选与明细统计</div>
|
</div>
|
<div class="kpi-icon">
|
<el-icon class="ui-icon">
|
<Histogram />
|
</el-icon>
|
</div>
|
<div class="kpi-actions" @click.stop>
|
<button
|
class="kpi-action"
|
type="button"
|
@click="copyKpi('avgCost')"
|
>
|
复制
|
</button>
|
<button
|
class="kpi-action"
|
type="button"
|
@click="viewKpiDetails('all')"
|
>
|
明细
|
</button>
|
</div>
|
</button>
|
</div>
|
|
<div class="panel-head">
|
<div
|
class="segmented"
|
role="tablist"
|
aria-label="分析面板切换"
|
:class="{ 'no-active': chartPanel === 'none' }"
|
>
|
<div
|
class="segmented-indicator"
|
:class="{ hidden: chartPanel === 'none' }"
|
:style="panelIndicatorStyle"
|
></div>
|
<button
|
class="segmented-item"
|
type="button"
|
role="tab"
|
:aria-selected="chartPanel === 'core'"
|
:class="{ active: chartPanel === 'core' }"
|
@click="handleChartPanelClick('core')"
|
>
|
<span class="seg-title">核心分析</span>
|
<span class="seg-sub">趋势 / 类型占比</span>
|
</button>
|
<button
|
class="segmented-item"
|
type="button"
|
role="tab"
|
:aria-selected="chartPanel === 'advanced'"
|
:class="{ active: chartPanel === '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">
|
<el-row :gutter="16">
|
<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"
|
type="button"
|
@click="downloadChart('cost', '能耗成本趋势')"
|
>
|
下载
|
</button>
|
<button
|
class="chart-tool"
|
type="button"
|
@click="openBigChart('cost', '能耗成本趋势')"
|
>
|
大图
|
</button>
|
</div>
|
</div>
|
</template>
|
<div
|
ref="costChartWrap"
|
class="chart-wrap"
|
v-loading="tableLoading"
|
>
|
<div
|
ref="costChart"
|
class="chart-content"
|
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">
|
<template #header>
|
<div class="chart-head">
|
<span class="chart-title">能耗类型成本占比</span>
|
<div class="chart-tools" @click.stop>
|
<button
|
class="chart-tool"
|
type="button"
|
@click="downloadChart('type', '能耗类型成本占比')"
|
>
|
下载
|
</button>
|
<button
|
class="chart-tool"
|
type="button"
|
@click="openBigChart('type', '能耗类型成本占比')"
|
>
|
大图
|
</button>
|
</div>
|
</div>
|
</template>
|
<div
|
ref="typeChartWrap"
|
class="chart-wrap"
|
v-loading="tableLoading"
|
>
|
<div
|
ref="typeChart"
|
class="chart-content"
|
v-show="hasTableData"
|
></div>
|
<div class="chart-empty" v-show="!hasTableData">
|
<el-empty description="暂无数据" />
|
</div>
|
</div>
|
</el-card>
|
</el-col>
|
</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">
|
<template #header>
|
<div class="chart-head">
|
<span class="chart-title">能耗用途成本占比</span>
|
<div class="chart-tools" @click.stop>
|
<button
|
class="chart-tool"
|
type="button"
|
@click="downloadChart('purpose', '能耗用途成本占比')"
|
>
|
下载
|
</button>
|
<button
|
class="chart-tool"
|
type="button"
|
@click="openBigChart('purpose', '能耗用途成本占比')"
|
>
|
大图
|
</button>
|
</div>
|
</div>
|
</template>
|
<div
|
ref="purposeChartWrap"
|
class="chart-wrap"
|
v-loading="tableLoading"
|
>
|
<div
|
ref="purposeChart"
|
class="chart-content"
|
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">
|
<template #header>
|
<div class="chart-head">
|
<span class="chart-title">能耗用量对比</span>
|
<div class="chart-tools" @click.stop>
|
<button
|
class="chart-tool"
|
type="button"
|
@click="downloadChart('consumption', '能耗用量对比')"
|
>
|
下载
|
</button>
|
<button
|
class="chart-tool"
|
type="button"
|
@click="openBigChart('consumption', '能耗用量对比')"
|
>
|
大图
|
</button>
|
</div>
|
</div>
|
</template>
|
<div
|
ref="consumptionChartWrap"
|
class="chart-wrap"
|
v-loading="tableLoading"
|
>
|
<div
|
ref="consumptionChart"
|
class="chart-content"
|
v-show="hasTableData"
|
></div>
|
<div class="chart-empty" v-show="!hasTableData">
|
<el-empty description="暂无数据" />
|
</div>
|
</div>
|
</el-card>
|
</el-col>
|
</el-row>
|
</div>
|
</transition>
|
</el-card>
|
</div>
|
|
<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>
|
<template #footer>
|
<div class="big-chart-footer">
|
<el-button
|
class="lux-btn"
|
@click="downloadChart(bigChartKey, bigChartTitle)"
|
>下载图片</el-button
|
>
|
<el-button
|
class="lux-btn"
|
type="primary"
|
@click="bigChartVisible = false"
|
>关闭</el-button
|
>
|
</div>
|
</template>
|
</el-dialog>
|
|
<!-- 数据表格 -->
|
<el-card class="table-card" shadow="never">
|
<div ref="tableAnchor"></div>
|
<template #header>
|
<div class="card-head">
|
<div class="card-head-left">
|
<el-icon class="card-icon ui-icon">
|
<List />
|
</el-icon>
|
<span class="card-title">明细数据</span>
|
</div>
|
<div class="card-head-right subtle">
|
<span>共 {{ page.total }} 条</span>
|
</div>
|
</div>
|
</template>
|
|
<el-table
|
:data="displayTableData"
|
v-loading="tableLoading"
|
stripe
|
:header-cell-style="{ height: '44px' }"
|
class="data-table lux-table"
|
@sort-change="handleSortChange"
|
>
|
<template #empty>
|
<el-empty description="暂无明细数据" />
|
</template>
|
<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="energyType"
|
label="能耗类型"
|
width="100"
|
align="center"
|
:filters="energyTypeFilters"
|
:filter-method="filterEnergyType"
|
filter-placement="bottom-end"
|
>
|
<template #default="scope">
|
<el-tag :type="getEnergyTypeType(scope.row.energyType)">
|
{{ scope.row.energyType }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column
|
prop="type"
|
label="能耗用途"
|
width="100"
|
align="center"
|
:filters="energyPurposeFilters"
|
:filter-method="filterEnergyPurpose"
|
filter-placement="bottom-end"
|
>
|
<template #default="scope">
|
<el-tag :type="scope.row.type === '生产' ? 'primary' : 'info'">
|
{{ scope.row.type }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column prop="consumption" label="用量" align="right">
|
<template #default="scope">
|
<span class="consumption-value">{{
|
formatNumber(scope.row.consumption, 2)
|
}}</span>
|
<span class="consumption-unit">{{ scope.row.unit }}</span>
|
</template>
|
</el-table-column>
|
<el-table-column
|
prop="price"
|
label="单价(元)"
|
align="right"
|
sortable="custom"
|
>
|
<template #default="scope">
|
<span class="price-value">{{
|
formatNumber(scope.row.price, 2)
|
}}</span>
|
</template>
|
</el-table-column>
|
<el-table-column
|
prop="cost"
|
label="成本(元)"
|
align="right"
|
sortable="custom"
|
fixed="right"
|
>
|
<template #default="scope">
|
<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"
|
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"
|
/>
|
</div>
|
</el-card>
|
</div>
|
</template>
|
|
<script setup>
|
import {
|
ref,
|
reactive,
|
onMounted,
|
onUnmounted,
|
computed,
|
nextTick,
|
watch,
|
} from "vue";
|
import { ElMessage } from "element-plus";
|
import {
|
Money,
|
DataLine,
|
TrendCharts,
|
Histogram,
|
List,
|
ArrowDown,
|
} from "@element-plus/icons-vue";
|
import * as echarts from "echarts";
|
// import { energyCostStatistics } from "@/api/costAccounting/energyCosts";
|
import { energyConsumptionDetailStatistics } from "@/api/energyManagement/energyType";
|
// 统计维度:day-按日,month-按月
|
const statisticsType = ref("day");
|
|
// 搜索表单
|
const searchForm = reactive({
|
// energyType: "",
|
// type: "",
|
dateRange: (() => {
|
// 默认最近7天
|
const end = new Date();
|
const start = new Date();
|
start.setDate(start.getDate() - 6);
|
return [start.toISOString().split("T")[0], end.toISOString().split("T")[0]];
|
})(),
|
monthRange: (() => {
|
// 默认最近3个月
|
const end = new Date();
|
const start = new Date();
|
start.setMonth(start.getMonth() - 2);
|
return [start.toISOString().slice(0, 7), end.toISOString().slice(0, 7)];
|
})(),
|
});
|
|
// 时间列标签
|
const timeColumnLabel = computed(() => {
|
return statisticsType.value === "day" ? "日期" : "月份";
|
});
|
|
// 统计概览
|
const overview = reactive({
|
totalCost: "0.00",
|
productionCost: "0.00",
|
officeCost: "0.00",
|
avgCost: "0.00",
|
});
|
|
const selectedKpi = ref("all"); // all | production | office
|
const animatedOverview = reactive({
|
totalCost: 0,
|
productionCost: 0,
|
officeCost: 0,
|
avgCost: 0,
|
});
|
|
const formatMoney = (v) => {
|
const n = Number.parseFloat(v);
|
const value = Number.isFinite(n) ? n : 0;
|
return value.toLocaleString("zh-CN", {
|
minimumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
});
|
};
|
|
const formatNumber = (v, digits = 2) => {
|
const n = Number.parseFloat(v);
|
if (!Number.isFinite(n)) return "--";
|
return n.toLocaleString("zh-CN", {
|
minimumFractionDigits: digits,
|
maximumFractionDigits: digits,
|
});
|
};
|
|
const animateNumber = (key, toValue, duration = 420) => {
|
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 tick = (now) => {
|
const p = Math.min(1, (now - start) / duration);
|
animatedOverview[key] = from + (to - from) * easeOut(p);
|
if (p < 1) requestAnimationFrame(tick);
|
};
|
requestAnimationFrame(tick);
|
};
|
|
watch(
|
() => ({ ...overview }),
|
(val) => {
|
animateNumber("totalCost", Number.parseFloat(val.totalCost));
|
animateNumber("productionCost", Number.parseFloat(val.productionCost));
|
animateNumber("officeCost", Number.parseFloat(val.officeCost));
|
animateNumber("avgCost", Number.parseFloat(val.avgCost));
|
},
|
{ deep: true, immediate: true }
|
);
|
|
// 表格数据
|
const tableData = ref([]);
|
const tableLoading = ref(false);
|
const hasTableData = computed(
|
() => Array.isArray(tableData.value) && tableData.value.length > 0
|
);
|
const queryPulse = ref(false);
|
|
const kpiSeries = computed(() => {
|
const rows = Array.isArray(tableData.value) ? tableData.value : [];
|
const byTime = new Map();
|
for (const r of rows) {
|
const t = r?.meterReadingDate ?? "";
|
if (!t) continue;
|
if (!byTime.has(t)) byTime.set(t, { total: 0, production: 0, office: 0 });
|
const bucket = byTime.get(t);
|
const c = Number.parseFloat(r?.cost);
|
const cost = Number.isFinite(c) ? c : 0;
|
bucket.total += cost;
|
if (r?.type === "生产") bucket.production += cost;
|
if (r?.type === "办公") bucket.office += cost;
|
}
|
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);
|
return { times, total, production, office };
|
});
|
|
const kpiDelta = computed(() => {
|
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 cur = a[a.length - 1];
|
if (!Number.isFinite(prev) || prev === 0) return { pct: 0, valid: false };
|
return { pct: ((cur - prev) / prev) * 100, valid: true };
|
};
|
return {
|
total: pick(kpiSeries.value.total),
|
production: pick(kpiSeries.value.production),
|
office: pick(kpiSeries.value.office),
|
};
|
});
|
|
const sparklinePoints = (values) => {
|
const v = (Array.isArray(values) ? values : []).slice(-12);
|
if (v.length < 2) return "";
|
const min = Math.min(...v);
|
const max = Math.max(...v);
|
const range = max - min || 1;
|
const w = 72;
|
const h = 22;
|
return v
|
.map((n, i) => {
|
const x = (i / (v.length - 1)) * w;
|
const y = h - ((n - min) / range) * h;
|
return `${x.toFixed(2)},${y.toFixed(2)}`;
|
})
|
.join(" ");
|
};
|
|
const handleKpiClick = (key) => {
|
selectedKpi.value = key;
|
if (key === "all") searchForm.type = "";
|
if (key === "production") searchForm.type = "生产";
|
if (key === "office") searchForm.type = "办公";
|
page.current = 1;
|
handleQuery();
|
};
|
|
const viewKpiDetails = (key) => {
|
handleKpiClick(key);
|
nextTick(() => {
|
const el = tableAnchor.value;
|
if (el?.scrollIntoView)
|
el.scrollIntoView({ behavior: "smooth", block: "start" });
|
});
|
};
|
|
const copyKpi = async (field) => {
|
const map = {
|
totalCost: animatedOverview.totalCost,
|
productionCost: animatedOverview.productionCost,
|
officeCost: animatedOverview.officeCost,
|
avgCost: animatedOverview.avgCost,
|
};
|
const raw = map[field];
|
const text = `¥${formatMoney(raw)}`;
|
try {
|
if (navigator?.clipboard?.writeText) {
|
await navigator.clipboard.writeText(text);
|
} else {
|
const input = document.createElement("input");
|
input.value = text;
|
document.body.appendChild(input);
|
input.select();
|
document.execCommand("copy");
|
document.body.removeChild(input);
|
}
|
ElMessage.success("已复制到剪贴板");
|
} catch (e) {
|
console.error(e);
|
ElMessage.error("复制失败");
|
}
|
};
|
|
const getChartByKey = (key) => {
|
if (key === "cost") return costChartInstance;
|
if (key === "type") return typeChartInstance;
|
if (key === "purpose") return purposeChartInstance;
|
if (key === "consumption") return consumptionChartInstance;
|
return null;
|
};
|
|
const ensurePanelForChart = (key) => {
|
if (key === "cost" || key === "type") chartPanel.value = "core";
|
if (key === "purpose" || key === "consumption") chartPanel.value = "advanced";
|
};
|
|
const downloadChart = (key, title) => {
|
ensurePanelForChart(key);
|
nextTick(() => {
|
ensureChartsReady(chartPanel.value === "none" ? "core" : chartPanel.value);
|
const ins = getChartByKey(key);
|
if (!ins) return;
|
const url = ins.getDataURL({ pixelRatio: 2, backgroundColor: "#ffffff" });
|
const a = document.createElement("a");
|
a.href = url;
|
const typePart = searchForm.energyType ? `_${searchForm.energyType}` : "";
|
const purposePart = searchForm.type ? `_${searchForm.type}` : "";
|
let rangePart = "";
|
if (statisticsType.value === "day") {
|
if (searchForm.dateRange?.length === 2)
|
rangePart = `_${searchForm.dateRange[0]}~${searchForm.dateRange[1]}`;
|
} else {
|
if (searchForm.monthRange?.length === 2)
|
rangePart = `_${searchForm.monthRange[0]}~${searchForm.monthRange[1]}`;
|
}
|
a.download = `${title || "chart"}${typePart}${purposePart}${rangePart}.png`;
|
a.click();
|
});
|
};
|
|
const openBigChart = (key, title) => {
|
bigChartKey.value = key;
|
bigChartTitle.value = title || "图表";
|
bigChartVisible.value = true;
|
};
|
|
const handleBigChartOpened = () => {
|
nextTick(() => {
|
ensurePanelForChart(bigChartKey.value);
|
ensureChartsReady(chartPanel.value === "none" ? "core" : chartPanel.value);
|
const src = getChartByKey(bigChartKey.value);
|
const el = bigChartEl.value;
|
if (!src || !el) return;
|
|
try {
|
bigChartInstance?.dispose?.();
|
} catch (e) {
|
// ignore
|
}
|
bigChartInstance = echarts.init(el);
|
const opt = src.getOption();
|
bigChartInstance.setOption(opt, true);
|
bigChartInstance.resize();
|
});
|
};
|
|
const handleBigChartClosed = () => {
|
try {
|
bigChartInstance?.dispose?.();
|
} catch (e) {
|
// ignore
|
}
|
bigChartInstance = null;
|
};
|
|
const handleBigChartResize = () => {
|
try {
|
bigChartInstance?.resize?.();
|
} catch (e) {
|
// ignore
|
}
|
};
|
// 表格排序(前端排序:仅影响当前页数据,避免破坏后端分页协议)
|
const sortState = reactive({
|
prop: "",
|
order: "",
|
});
|
|
const handleSortChange = ({ prop, order }) => {
|
sortState.prop = prop || "";
|
sortState.order = order || "";
|
};
|
|
const displayTableData = computed(() => {
|
const data = Array.isArray(tableData.value) ? [...tableData.value] : [];
|
if (!sortState.prop || !sortState.order) return data;
|
|
const prop = sortState.prop;
|
const direction = sortState.order === "ascending" ? 1 : -1;
|
const numFields = new Set(["price", "cost", "consumption"]);
|
|
return data.sort((a, b) => {
|
const av = a?.[prop];
|
const bv = b?.[prop];
|
|
if (numFields.has(prop)) {
|
const an = Number.parseFloat(av);
|
const bn = Number.parseFloat(bv);
|
const aNum = Number.isFinite(an) ? an : -Infinity;
|
const bNum = Number.isFinite(bn) ? bn : -Infinity;
|
return (aNum - bNum) * direction;
|
}
|
|
return (
|
String(av ?? "").localeCompare(String(bv ?? ""), "zh-Hans-CN") * direction
|
);
|
});
|
});
|
|
const energyTypeFilters = [
|
{ text: "水", value: "水" },
|
{ text: "电", value: "电" },
|
{ text: "气", value: "气" },
|
];
|
const energyPurposeFilters = [
|
{ text: "生产", value: "生产" },
|
{ text: "办公", value: "办公" },
|
];
|
|
const filterEnergyType = (value, row) => row.energyType === value;
|
const filterEnergyPurpose = (value, row) => row.type === value;
|
|
// 分页
|
const page = reactive({
|
current: 1,
|
size: 10,
|
total: 0,
|
});
|
|
// 图表引用
|
const costChart = ref(null);
|
const typeChart = ref(null);
|
const purposeChart = ref(null);
|
const consumptionChart = ref(null);
|
|
const costChartWrap = ref(null);
|
const typeChartWrap = ref(null);
|
const purposeChartWrap = ref(null);
|
const consumptionChartWrap = ref(null);
|
|
const tableAnchor = ref(null);
|
|
const bigChartVisible = ref(false);
|
const bigChartKey = ref("cost");
|
const bigChartTitle = ref("");
|
const bigChartEl = ref(null);
|
let bigChartInstance = null;
|
|
watch(bigChartVisible, (v) => {
|
if (v) window.addEventListener("resize", handleBigChartResize);
|
else window.removeEventListener("resize", handleBigChartResize);
|
});
|
|
onUnmounted(() => {
|
window.removeEventListener("resize", handleBigChartResize);
|
try {
|
bigChartInstance?.dispose?.();
|
} catch (e) {
|
// ignore
|
}
|
});
|
|
// 图表实例
|
let costChartInstance = null;
|
let typeChartInstance = null;
|
let purposeChartInstance = null;
|
let consumptionChartInstance = null;
|
|
// 图表区切换:core | advanced | none(点击当前选中可收起)
|
const chartPanel = ref("core");
|
|
const ensureChartsReady = (panel) => {
|
if (panel === "core") {
|
if (costChart.value && !costChartInstance)
|
costChartInstance = echarts.init(costChart.value);
|
if (typeChart.value && !typeChartInstance)
|
typeChartInstance = echarts.init(typeChart.value);
|
if (costChartInstance) updateCostChart();
|
if (typeChartInstance) updateTypeChart();
|
return;
|
}
|
if (panel === "advanced") {
|
if (purposeChart.value && !purposeChartInstance)
|
purposeChartInstance = echarts.init(purposeChart.value);
|
if (consumptionChart.value && !consumptionChartInstance)
|
consumptionChartInstance = echarts.init(consumptionChart.value);
|
if (purposeChartInstance) updatePurposeChart();
|
if (consumptionChartInstance) updateConsumptionChart();
|
}
|
};
|
|
const resizeChartsAfterExpand = () => {
|
nextTick(() => {
|
ensureChartsReady(chartPanel.value === "none" ? "core" : chartPanel.value);
|
handleResize();
|
updateCharts();
|
});
|
};
|
|
const handleChartPanelClick = (key) => {
|
chartPanel.value = chartPanel.value === key ? "none" : key;
|
};
|
|
const panelIndicatorStyle = computed(() => {
|
const x = chartPanel.value === "advanced" ? "calc(100% + 4px)" : "0";
|
return { transform: `translateX(${x})` };
|
});
|
|
watch(chartPanel, (val) => {
|
if (val !== "none") resizeChartsAfterExpand();
|
});
|
|
// 获取能耗类型标签类型
|
const getEnergyTypeType = (type) => {
|
const typeMap = {
|
水: "primary",
|
电: "warning",
|
气: "success",
|
};
|
return typeMap[type] || "info";
|
};
|
|
// 初始化图表
|
const initCharts = () => {
|
nextTick(() => {
|
// 只初始化可见面板,避免隐藏容器初始化为 0 尺寸导致空白
|
ensureChartsReady(chartPanel.value === "none" ? "core" : chartPanel.value);
|
});
|
};
|
|
// 更新能耗成本趋势图
|
const updateCostChart = () => {
|
const data = tableData.value;
|
const option = {
|
tooltip: {
|
trigger: "axis",
|
axisPointer: { type: "shadow" },
|
backgroundColor: "rgba(255, 255, 255, 0.96)",
|
borderColor: "#2f6fed",
|
borderWidth: 1,
|
textStyle: { color: "rgba(15, 23, 42, 0.92)" },
|
extraCssText:
|
"box-shadow: 0 14px 40px rgba(15,23,42,.14); border-radius: 12px;",
|
},
|
legend: {
|
data: ["生产能耗成本", "办公能耗成本"],
|
top: 0,
|
right: 10,
|
textStyle: { color: "#606266" },
|
},
|
grid: {
|
left: "3%",
|
right: "4%",
|
bottom: "10%",
|
top: "15%",
|
containLabel: true,
|
},
|
xAxis: {
|
type: "category",
|
data: data.map((item) => item.meterReadingDate),
|
axisLabel: {
|
rotate: statisticsType.value === "day" ? 45 : 0,
|
color: "rgba(15, 23, 42, 0.62)",
|
},
|
axisLine: { lineStyle: { color: "rgba(15, 23, 42, 0.08)" } },
|
splitLine: { show: false },
|
},
|
yAxis: {
|
type: "value",
|
name: "成本(元)",
|
nameTextStyle: { color: "rgba(15, 23, 42, 0.58)" },
|
axisLabel: { color: "rgba(15, 23, 42, 0.58)" },
|
axisLine: { show: false },
|
splitLine: { lineStyle: { color: "rgba(15, 23, 42, 0.06)" } },
|
},
|
series: [
|
{
|
name: "生产能耗成本",
|
type: "bar",
|
data: data.map((item) => (item.type === "生产" ? item.cost : 0)),
|
itemStyle: {
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: "#409EFF" },
|
{ offset: 1, color: "#66b1ff" },
|
]),
|
borderRadius: [4, 4, 0, 0],
|
},
|
animationDelay: function (idx) {
|
return idx * 100;
|
},
|
},
|
{
|
name: "办公能耗成本",
|
type: "bar",
|
data: data.map((item) => (item.type === "办公" ? item.cost : 0)),
|
itemStyle: {
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: "#67C23A" },
|
{ offset: 1, color: "#85ce61" },
|
]),
|
borderRadius: [4, 4, 0, 0],
|
},
|
animationDelay: function (idx) {
|
return idx * 100 + 100;
|
},
|
},
|
],
|
animationEasing: "elasticOut",
|
animationDelayUpdate: function (idx) {
|
return idx * 5;
|
},
|
};
|
costChartInstance.setOption(option);
|
};
|
|
// 更新能耗类型成本占比图
|
const updateTypeChart = () => {
|
const data = tableData.value;
|
const typeCosts = {};
|
|
data.forEach((item) => {
|
if (!typeCosts[item.energyType]) {
|
typeCosts[item.energyType] = 0;
|
}
|
typeCosts[item.energyType] += parseFloat(item.cost);
|
});
|
|
const chartData = Object.entries(typeCosts).map(([name, value]) => ({
|
name,
|
value: value.toFixed(2),
|
}));
|
|
const option = {
|
tooltip: {
|
trigger: "item",
|
formatter: "{a} <br/>{b}: ¥{c} ({d}%)",
|
backgroundColor: "rgba(255, 255, 255, 0.96)",
|
borderColor: "#2f6fed",
|
borderWidth: 1,
|
textStyle: { color: "rgba(15, 23, 42, 0.92)" },
|
extraCssText:
|
"box-shadow: 0 14px 40px rgba(15,23,42,.14); border-radius: 12px;",
|
},
|
legend: {
|
orient: "horizontal",
|
bottom: 0,
|
textStyle: { color: "rgba(15, 23, 42, 0.62)" },
|
},
|
series: [
|
{
|
name: "能耗类型成本",
|
type: "pie",
|
radius: ["40%", "70%"],
|
center: ["50%", "40%"],
|
avoidLabelOverlap: false,
|
itemStyle: {
|
borderRadius: 4,
|
borderColor: "#fff",
|
borderWidth: 2,
|
},
|
label: {
|
show: false,
|
position: "center",
|
},
|
emphasis: {
|
label: {
|
show: true,
|
fontSize: "18",
|
fontWeight: "bold",
|
color: "#303133",
|
},
|
itemStyle: {
|
shadowBlur: 10,
|
shadowOffsetX: 0,
|
shadowColor: "rgba(0, 0, 0, 0.3)",
|
},
|
},
|
labelLine: {
|
show: false,
|
},
|
data: chartData,
|
},
|
],
|
color: ["#2f6fed", "#16a34a", "#f59e0b"],
|
};
|
typeChartInstance.setOption(option);
|
};
|
|
// 更新能耗用途成本占比图
|
const updatePurposeChart = () => {
|
const data = tableData.value;
|
const purposeCosts = {
|
生产: 0,
|
办公: 0,
|
};
|
|
data.forEach((item) => {
|
if (purposeCosts.hasOwnProperty(item.type)) {
|
purposeCosts[item.type] += parseFloat(item.cost);
|
}
|
});
|
|
const chartData = Object.entries(purposeCosts).map(([name, value]) => ({
|
name,
|
value: value.toFixed(2),
|
}));
|
|
const option = {
|
tooltip: {
|
trigger: "item",
|
formatter: "{a} <br/>{b}: ¥{c} ({d}%)",
|
backgroundColor: "rgba(255, 255, 255, 0.96)",
|
borderColor: "#2f6fed",
|
borderWidth: 1,
|
textStyle: { color: "rgba(15, 23, 42, 0.92)" },
|
extraCssText:
|
"box-shadow: 0 14px 40px rgba(15,23,42,.14); border-radius: 12px;",
|
},
|
legend: {
|
orient: "horizontal",
|
bottom: 0,
|
textStyle: { color: "rgba(15, 23, 42, 0.62)" },
|
},
|
series: [
|
{
|
name: "能耗用途成本",
|
type: "pie",
|
radius: "60%",
|
center: ["50%", "40%"],
|
data: chartData,
|
emphasis: {
|
itemStyle: {
|
shadowBlur: 10,
|
shadowOffsetX: 0,
|
shadowColor: "rgba(0, 0, 0, 0.3)",
|
},
|
},
|
label: {
|
show: true,
|
formatter: "{b}: {d}%",
|
color: "rgba(15, 23, 42, 0.62)",
|
},
|
labelLine: {
|
show: true,
|
lineStyle: { color: "rgba(15, 23, 42, 0.10)" },
|
},
|
},
|
],
|
color: ["#2f6fed", "#16a34a"],
|
};
|
purposeChartInstance.setOption(option);
|
};
|
|
// 更新能耗用量对比图
|
const updateConsumptionChart = () => {
|
const data = tableData.value;
|
const consumptionData = {};
|
|
data.forEach((item) => {
|
if (!consumptionData[item.energyType]) {
|
consumptionData[item.energyType] = {
|
生产: 0,
|
办公: 0,
|
};
|
}
|
if (consumptionData[item.energyType].hasOwnProperty(item.type)) {
|
consumptionData[item.energyType][item.type] = parseFloat(
|
item.consumption
|
);
|
}
|
});
|
|
const energyTypes = Object.keys(consumptionData);
|
const productionConsumptions = energyTypes.map(
|
(type) => consumptionData[type].生产
|
);
|
const officeConsumptions = energyTypes.map(
|
(type) => consumptionData[type].办公
|
);
|
|
const option = {
|
tooltip: {
|
trigger: "axis",
|
axisPointer: { type: "shadow" },
|
backgroundColor: "rgba(255, 255, 255, 0.96)",
|
borderColor: "#2f6fed",
|
borderWidth: 1,
|
textStyle: { color: "rgba(15, 23, 42, 0.92)" },
|
extraCssText:
|
"box-shadow: 0 14px 40px rgba(15,23,42,.14); border-radius: 12px;",
|
},
|
legend: {
|
data: ["生产能耗用量", "办公能耗用量"],
|
top: 0,
|
right: 10,
|
textStyle: { color: "rgba(15, 23, 42, 0.62)" },
|
},
|
grid: {
|
left: "3%",
|
right: "4%",
|
bottom: "10%",
|
top: "15%",
|
containLabel: true,
|
},
|
xAxis: {
|
type: "category",
|
data: energyTypes,
|
axisLabel: { color: "rgba(15, 23, 42, 0.62)" },
|
axisLine: { lineStyle: { color: "rgba(15, 23, 42, 0.08)" } },
|
splitLine: { show: false },
|
},
|
yAxis: {
|
type: "value",
|
name: "用量",
|
nameTextStyle: { color: "rgba(15, 23, 42, 0.58)" },
|
axisLabel: { color: "rgba(15, 23, 42, 0.58)" },
|
axisLine: { show: false },
|
splitLine: { lineStyle: { color: "rgba(15, 23, 42, 0.06)" } },
|
},
|
series: [
|
{
|
name: "生产能耗用量",
|
type: "bar",
|
data: productionConsumptions,
|
itemStyle: {
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: "#2f6fed" },
|
{ offset: 1, color: "#5b8cff" },
|
]),
|
borderRadius: [4, 4, 0, 0],
|
},
|
},
|
{
|
name: "办公能耗用量",
|
type: "bar",
|
data: officeConsumptions,
|
itemStyle: {
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: "#16a34a" },
|
{ offset: 1, color: "rgba(22, 163, 74, 0.65)" },
|
]),
|
borderRadius: [4, 4, 0, 0],
|
},
|
},
|
],
|
};
|
consumptionChartInstance.setOption(option);
|
};
|
|
// 统计维度切换
|
const handleTypeChange = () => {
|
// 重置时间范围
|
if (statisticsType.value === "day") {
|
const end = new Date();
|
const start = new Date();
|
start.setDate(start.getDate() - 6);
|
searchForm.dateRange = [
|
start.toISOString().split("T")[0],
|
end.toISOString().split("T")[0],
|
];
|
} else {
|
const end = new Date();
|
const start = new Date();
|
start.setMonth(start.getMonth() - 2);
|
searchForm.monthRange = [
|
start.toISOString().slice(0, 7),
|
end.toISOString().slice(0, 7),
|
];
|
}
|
page.current = 1;
|
handleQuery();
|
};
|
|
// 查询
|
const handleQuery = () => {
|
queryPulse.value = true;
|
window.setTimeout(() => {
|
queryPulse.value = false;
|
}, 520);
|
tableLoading.value = true;
|
|
// 构造请求参数
|
const params = {
|
days: 0,
|
// energyType: searchForm.energyType || undefined,
|
// type: searchForm.type || undefined,
|
pageNum: page.current,
|
pageSize: page.size,
|
};
|
|
if (statisticsType.value === "day") {
|
if (searchForm.dateRange && searchForm.dateRange.length === 2) {
|
params.startDate = searchForm.dateRange[0];
|
params.endDate = searchForm.dateRange[1];
|
}
|
} else {
|
if (searchForm.monthRange && searchForm.monthRange.length === 2) {
|
params.startDate = searchForm.monthRange[0] + "-01";
|
|
// 结束时间需要取结束月份的最后一天(例如 2026-03 -> 2026-03-31)
|
const [endYearStr, endMonthStr] = String(searchForm.monthRange[1]).split(
|
"-"
|
);
|
const endYear = Number(endYearStr);
|
const endMonth = Number(endMonthStr); // 1-12
|
if (
|
!Number.isNaN(endYear) &&
|
!Number.isNaN(endMonth) &&
|
endMonth >= 1 &&
|
endMonth <= 12
|
) {
|
const lastDay = new Date(endYear, endMonth, 0).getDate(); // 下个月第0天 = 本月最后一天
|
params.endDate = `${endYearStr}-${endMonthStr}-${String(
|
lastDay
|
).padStart(2, "0")}`;
|
} else {
|
params.endDate = searchForm.monthRange[1] + "-01";
|
}
|
}
|
}
|
|
// 计算开始到结束的天数(包含起止两天)
|
if (params.startDate && params.endDate) {
|
const start = new Date(params.startDate);
|
const end = new Date(params.endDate);
|
if (!Number.isNaN(start.getTime()) && !Number.isNaN(end.getTime())) {
|
const diffTime = end.getTime() - start.getTime();
|
const diffDays = Math.floor(diffTime / (24 * 60 * 60 * 1000)) + 1;
|
params.days = diffDays > 0 ? diffDays : 0;
|
}
|
}
|
|
// 调用接口获取数据
|
energyConsumptionDetailStatistics(params)
|
.then((res) => {
|
if (res.code === 200) {
|
const data = res.data;
|
overview.totalCost = data.totalEnergyConsumption || "0";
|
overview.productionCost = data.totalEnergyCost || "0";
|
overview.avgCost = data.averageConsumption || "0";
|
overview.officeCost = data.changeVite || 0;
|
|
// 处理表格数据
|
tableData.value = data.energyCostDtos || [];
|
page.total = tableData.value.length || 0;
|
} else {
|
ElMessage.error(res.message || "获取数据失败");
|
tableData.value = [];
|
page.total = 0;
|
overview.totalCost = "0.00";
|
overview.productionCost = "0.00";
|
overview.officeCost = "0.00";
|
overview.avgCost = "0.00";
|
}
|
})
|
.catch((err) => {
|
ElMessage.error("获取数据异常");
|
tableData.value = [];
|
page.total = 0;
|
overview.totalCost = "0.00";
|
overview.productionCost = "0.00";
|
overview.officeCost = "0.00";
|
overview.avgCost = "0.00";
|
})
|
.finally(() => {
|
tableLoading.value = false;
|
updateCharts();
|
});
|
};
|
|
// 更新所有图表
|
const updateCharts = () => {
|
nextTick(() => {
|
if (costChartInstance) updateCostChart();
|
if (typeChartInstance) updateTypeChart();
|
if (purposeChartInstance) updatePurposeChart();
|
if (consumptionChartInstance) updateConsumptionChart();
|
});
|
};
|
|
// 重置
|
const handleReset = () => {
|
// searchForm.energyType = "";
|
searchForm.type = "";
|
if (statisticsType.value === "day") {
|
const end = new Date();
|
const start = new Date();
|
start.setDate(start.getDate() - 6);
|
searchForm.dateRange = [
|
start.toISOString().split("T")[0],
|
end.toISOString().split("T")[0],
|
];
|
} else {
|
const end = new Date();
|
const start = new Date();
|
start.setMonth(start.getMonth() - 2);
|
searchForm.monthRange = [
|
start.toISOString().slice(0, 7),
|
end.toISOString().slice(0, 7),
|
];
|
}
|
page.current = 1;
|
handleQuery();
|
};
|
|
// 导出
|
const handleExport = () => {
|
ElMessage.success("报表导出成功");
|
};
|
|
// 分页大小变化
|
const handleSizeChange = (val) => {
|
page.size = val;
|
page.current = 1;
|
handleQuery();
|
};
|
|
// 页码变化
|
const handleCurrentChange = (val) => {
|
page.current = val;
|
handleQuery();
|
};
|
|
// 窗口大小变化时重新渲染图表
|
const handleResize = () => {
|
costChartInstance && costChartInstance.resize();
|
typeChartInstance && typeChartInstance.resize();
|
purposeChartInstance && purposeChartInstance.resize();
|
consumptionChartInstance && consumptionChartInstance.resize();
|
};
|
|
onMounted(() => {
|
handleQuery();
|
initCharts();
|
window.addEventListener("resize", handleResize);
|
});
|
|
const handleGlobalHotkeys = (e) => {
|
// 避免在输入框内误触
|
const target = e?.target;
|
const tag = target?.tagName?.toLowerCase?.();
|
const isTyping =
|
tag === "input" ||
|
tag === "textarea" ||
|
target?.isContentEditable ||
|
target?.classList?.contains?.("el-input__inner");
|
if (isTyping) return;
|
|
// Enter: 刷新查询
|
if (e.key === "Enter") {
|
e.preventDefault();
|
handleQuery();
|
return;
|
}
|
|
// Esc: 重置
|
if (e.key === "Escape") {
|
e.preventDefault();
|
handleReset();
|
return;
|
}
|
|
// Alt+E: 导出
|
if (e.altKey && (e.key === "e" || e.key === "E")) {
|
e.preventDefault();
|
handleExport();
|
}
|
};
|
|
onMounted(() => {
|
window.addEventListener("keydown", handleGlobalHotkeys);
|
});
|
|
onUnmounted(() => {
|
window.removeEventListener("keydown", handleGlobalHotkeys);
|
});
|
</script>
|
|
<style scoped lang="scss">
|
.energy-cost-page {
|
--lux-bg: #f6f7fb;
|
--lux-card: rgba(255, 255, 255, 0.86);
|
--lux-card-solid: #ffffff;
|
--lux-border: rgba(15, 23, 42, 0.08);
|
--lux-text: rgba(15, 23, 42, 0.92);
|
--lux-subtle: rgba(15, 23, 42, 0.58);
|
--lux-muted: rgba(15, 23, 42, 0.38);
|
--lux-primary: #2f6fed;
|
--lux-primary-2: #5b8cff;
|
--lux-success: #16a34a;
|
--lux-warning: #f59e0b;
|
--lux-danger: #ef4444;
|
--lux-shadow: 0 18px 50px rgba(15, 23, 42, 0.08);
|
--lux-shadow-soft: 0 10px 28px rgba(15, 23, 42, 0.06);
|
--lux-radius: 14px;
|
--lux-radius-sm: 12px;
|
|
padding: 18px 22px 24px;
|
background: radial-gradient(
|
1200px 420px at 20% 0%,
|
rgba(47, 111, 237, 0.1),
|
transparent 55%
|
),
|
radial-gradient(
|
900px 380px at 90% 10%,
|
rgba(22, 163, 74, 0.06),
|
transparent 55%
|
),
|
linear-gradient(180deg, var(--lux-bg) 0%, #ffffff 58%);
|
}
|
|
.filter-actions {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
flex-wrap: nowrap;
|
margin-top: 0;
|
padding-top: 0;
|
border-top: none;
|
justify-content: flex-end;
|
flex: 0 0 auto;
|
white-space: nowrap;
|
align-self: flex-start;
|
padding-bottom: 0;
|
width: 290px;
|
}
|
|
.filter-actions :deep(.el-button) {
|
min-width: 78px;
|
}
|
|
.filter-actions :deep(.el-button.is-loading) {
|
min-width: 90px;
|
}
|
|
.filter-layout {
|
display: flex;
|
align-items: flex-start;
|
justify-content: space-between;
|
gap: 14px;
|
}
|
|
.filter-form {
|
flex: 0.1 1 auto;
|
min-width: 0;
|
}
|
|
.w-260 {
|
width: 260px;
|
max-width: 100%;
|
}
|
|
.lux-btn {
|
transition: transform 0.18s ease, box-shadow 0.18s ease, filter 0.18s ease;
|
|
&:hover {
|
transform: translateY(-1px);
|
box-shadow: 0 10px 22px rgba(15, 23, 42, 0.1);
|
filter: saturate(1.02);
|
}
|
|
&:active {
|
transform: translateY(0);
|
box-shadow: none;
|
}
|
}
|
|
.filter-card {
|
margin-bottom: 16px;
|
border-radius: var(--lux-radius);
|
border-color: var(--lux-border);
|
background: var(--lux-card);
|
backdrop-filter: blur(10px);
|
box-shadow: var(--lux-shadow-soft);
|
}
|
|
/* 查询区控件统一皮肤 */
|
:deep(.filter-card .el-form-item__label) {
|
color: rgba(15, 23, 42, 0.7);
|
font-weight: 650;
|
}
|
|
:deep(.filter-card .el-input__wrapper),
|
:deep(.filter-card .el-select__wrapper) {
|
border-radius: 12px;
|
box-shadow: none;
|
border: 1px solid rgba(15, 23, 42, 0.1);
|
background: rgba(255, 255, 255, 0.82);
|
transition: border-color 0.18s ease, box-shadow 0.18s ease,
|
transform 0.18s ease;
|
}
|
|
:deep(.filter-card .el-input__wrapper:hover),
|
:deep(.filter-card .el-select__wrapper:hover) {
|
border-color: rgba(47, 111, 237, 0.2);
|
transform: translateY(-1px);
|
}
|
|
:deep(.filter-card .is-focus .el-input__wrapper),
|
:deep(.filter-card .is-focus .el-select__wrapper) {
|
border-color: rgba(47, 111, 237, 0.3);
|
box-shadow: 0 0 0 3px rgba(47, 111, 237, 0.14);
|
}
|
|
:deep(.filter-card .el-range-editor.el-input__wrapper) {
|
border-radius: 12px;
|
}
|
|
.card-head {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 12px;
|
}
|
|
.card-head-left {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
min-width: 200px;
|
}
|
|
.card-head-right {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
}
|
|
.card-icon {
|
color: var(--lux-primary);
|
}
|
|
.card-title {
|
font-weight: 760;
|
color: var(--lux-text);
|
}
|
|
.subtle {
|
color: var(--lux-subtle);
|
font-size: 12px;
|
}
|
|
.filter-form {
|
display: flex;
|
flex-wrap: nowrap;
|
gap: 10px 14px;
|
align-items: flex-end;
|
min-width: 0;
|
}
|
|
.filter-form :deep(.el-form-item) {
|
margin-right: 0;
|
margin-bottom: 0;
|
flex: 0 0 auto;
|
}
|
|
.filter-form :deep(.el-form-item__content) {
|
min-width: 0;
|
}
|
|
.filter-form :deep(.el-form-item:last-child) {
|
flex: 1 1 auto;
|
}
|
|
.filter-form :deep(.el-form-item:last-child .el-form-item__content) {
|
width: 100%;
|
}
|
|
.w-140 {
|
width: 140px;
|
}
|
|
.w-260 {
|
width: 260px;
|
max-width: 100%;
|
}
|
|
@media (max-width: 1280px) {
|
.filter-form {
|
flex-wrap: wrap;
|
align-items: flex-start;
|
}
|
}
|
|
.section-title {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
margin: 10px 0 12px;
|
font-size: 14px;
|
font-weight: 700;
|
color: var(--lux-text);
|
}
|
|
.section-icon {
|
color: var(--lux-primary);
|
}
|
|
.metrics {
|
margin-bottom: 10px;
|
}
|
|
.ui-icon {
|
font-size: 16px;
|
transition: transform 0.18s ease, opacity 0.18s ease;
|
}
|
|
.card-head-left:hover .ui-icon,
|
.section-title:hover .ui-icon {
|
transform: translateY(-1px);
|
}
|
|
.metric-card {
|
border-radius: var(--lux-radius-sm);
|
padding: 14px 14px 14px 16px;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
border: 1px solid var(--lux-border);
|
background: rgba(255, 255, 255, 0.9);
|
backdrop-filter: blur(10px);
|
min-height: 78px;
|
transition: box-shadow 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
|
|
&:hover {
|
transform: translateY(-1px);
|
box-shadow: var(--lux-shadow);
|
border-color: rgba(47, 111, 237, 0.18);
|
}
|
}
|
|
.metric-left {
|
display: flex;
|
flex-direction: column;
|
gap: 6px;
|
}
|
|
.metric-label {
|
color: var(--lux-subtle);
|
font-size: 12px;
|
}
|
|
.metric-value {
|
color: var(--lux-text);
|
font-size: 20px;
|
font-weight: 800;
|
letter-spacing: 0.2px;
|
}
|
|
.metric-unit {
|
font-size: 12px;
|
font-weight: 500;
|
color: var(--lux-muted);
|
margin-left: 4px;
|
}
|
|
.metric-right {
|
width: 42px;
|
height: 42px;
|
border-radius: 10px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.metric-icon {
|
font-size: 20px;
|
color: #fff;
|
}
|
|
.metric-total {
|
background: linear-gradient(
|
135deg,
|
rgba(47, 111, 237, 0.12),
|
rgba(47, 111, 237, 0.02)
|
);
|
|
.metric-right {
|
background: linear-gradient(
|
135deg,
|
var(--lux-primary),
|
var(--lux-primary-2)
|
);
|
}
|
}
|
|
.metric-production {
|
background: linear-gradient(
|
135deg,
|
rgba(22, 163, 74, 0.12),
|
rgba(22, 163, 74, 0.02)
|
);
|
|
.metric-right {
|
background: linear-gradient(
|
135deg,
|
var(--lux-success),
|
rgba(22, 163, 74, 0.65)
|
);
|
}
|
}
|
|
.metric-office {
|
background: linear-gradient(
|
135deg,
|
rgba(144, 147, 153, 0.14),
|
rgba(144, 147, 153, 0.03)
|
);
|
|
.metric-right {
|
background: linear-gradient(135deg, #909399, #b1b3b8);
|
}
|
}
|
|
.metric-avg {
|
background: linear-gradient(
|
135deg,
|
rgba(245, 158, 11, 0.12),
|
rgba(245, 158, 11, 0.02)
|
);
|
|
.metric-right {
|
background: linear-gradient(
|
135deg,
|
var(--lux-warning),
|
rgba(245, 158, 11, 0.62)
|
);
|
}
|
}
|
|
.charts {
|
margin-top: 6px;
|
margin-bottom: 12px;
|
}
|
|
.charts-row {
|
margin-top: 16px;
|
}
|
|
.kpi-strip {
|
display: grid;
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
gap: 12px;
|
padding: 4px 4px 10px;
|
}
|
|
.kpi-strip.pulse {
|
animation: kpiPulse 520ms cubic-bezier(0.16, 1, 0.3, 1);
|
}
|
|
@keyframes kpiPulse {
|
0% {
|
filter: saturate(1.02);
|
}
|
35% {
|
filter: saturate(1.1);
|
}
|
100% {
|
filter: saturate(1.02);
|
}
|
}
|
|
.kpi-item {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 12px;
|
padding: 12px 12px 12px 14px;
|
border-radius: 14px;
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
background: rgba(255, 255, 255, 0.86);
|
transition: transform 0.18s ease, box-shadow 0.18s ease,
|
border-color 0.18s ease;
|
min-height: 68px;
|
text-align: left;
|
cursor: pointer;
|
position: relative;
|
overflow: hidden;
|
outline: none;
|
transform: translateZ(0);
|
}
|
|
.kpi-item:hover {
|
transform: translateY(-1px);
|
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.1);
|
border-color: rgba(47, 111, 237, 0.18);
|
}
|
|
.kpi-item::before {
|
content: "";
|
position: absolute;
|
inset: 0;
|
background: radial-gradient(
|
520px 140px at 20% 0%,
|
rgba(255, 255, 255, 0.65),
|
transparent 60%
|
),
|
radial-gradient(
|
620px 180px at 90% 40%,
|
rgba(47, 111, 237, 0.1),
|
transparent 55%
|
);
|
opacity: 0;
|
transform: translateX(-8%) translateY(-2%);
|
transition: opacity 0.22s ease, transform 0.42s cubic-bezier(0.16, 1, 0.3, 1);
|
pointer-events: none;
|
}
|
|
.kpi-item:hover::before {
|
opacity: 1;
|
transform: translateX(0) translateY(0);
|
}
|
|
.kpi-item::after {
|
content: "";
|
position: absolute;
|
inset: -1px;
|
border-radius: 15px;
|
background: linear-gradient(
|
135deg,
|
rgba(47, 111, 237, 0.18),
|
rgba(255, 255, 255, 0),
|
rgba(22, 163, 74, 0.14)
|
);
|
opacity: 0;
|
transition: opacity 0.22s ease;
|
pointer-events: none;
|
}
|
|
.kpi-item:hover::after {
|
opacity: 1;
|
}
|
|
.kpi-item:active {
|
transform: translateY(0);
|
box-shadow: 0 10px 22px rgba(15, 23, 42, 0.08);
|
}
|
|
.kpi-item:focus-visible {
|
box-shadow: 0 16px 44px rgba(15, 23, 42, 0.1),
|
0 0 0 3px rgba(47, 111, 237, 0.18);
|
border-color: rgba(47, 111, 237, 0.22);
|
}
|
|
.kpi-item.selected {
|
border-color: rgba(47, 111, 237, 0.22);
|
box-shadow: 0 16px 44px rgba(15, 23, 42, 0.1),
|
inset 0 0 0 1px rgba(47, 111, 237, 0.1);
|
}
|
|
.kpi-left {
|
display: flex;
|
flex-direction: column;
|
gap: 6px;
|
min-width: 0;
|
position: relative;
|
z-index: 1;
|
}
|
|
.kpi-label {
|
font-size: 12px;
|
color: var(--lux-subtle);
|
white-space: nowrap;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
}
|
|
.kpi-value {
|
font-size: 18px;
|
font-weight: 850;
|
letter-spacing: 0.2px;
|
color: var(--lux-text);
|
line-height: 1.1;
|
}
|
|
.kpi-meta {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
margin-top: 2px;
|
min-height: 22px;
|
}
|
|
.kpi-meta.muted {
|
font-size: 11px;
|
color: var(--lux-muted);
|
}
|
|
.kpi-chip {
|
font-size: 11px;
|
font-weight: 700;
|
padding: 2px 8px;
|
border-radius: 999px;
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
background: rgba(255, 255, 255, 0.72);
|
color: rgba(15, 23, 42, 0.72);
|
}
|
|
.kpi-chip.up {
|
border-color: rgba(22, 163, 74, 0.2);
|
color: rgba(22, 163, 74, 0.96);
|
background: rgba(22, 163, 74, 0.06);
|
}
|
|
.kpi-chip.down {
|
border-color: rgba(239, 68, 68, 0.2);
|
color: rgba(239, 68, 68, 0.96);
|
background: rgba(239, 68, 68, 0.06);
|
}
|
|
.kpi-spark {
|
width: 72px;
|
height: 22px;
|
opacity: 0.9;
|
filter: drop-shadow(0 8px 16px rgba(15, 23, 42, 0.1));
|
}
|
|
.kpi-actions {
|
position: absolute;
|
top: 10px;
|
right: 10px;
|
display: flex;
|
gap: 6px;
|
opacity: 0;
|
transform: translateY(-2px);
|
pointer-events: none;
|
transition: opacity 0.16s ease, transform 0.16s ease;
|
z-index: 2;
|
}
|
|
.kpi-item:hover .kpi-actions {
|
opacity: 1;
|
transform: translateY(0);
|
pointer-events: auto;
|
}
|
|
.kpi-action {
|
font-size: 11px;
|
font-weight: 650;
|
padding: 4px 8px;
|
border-radius: 999px;
|
border: 1px solid rgba(15, 23, 42, 0.1);
|
background: rgba(255, 255, 255, 0.78);
|
color: rgba(15, 23, 42, 0.78);
|
cursor: pointer;
|
transition: background-color 0.16s ease, border-color 0.16s ease,
|
transform 0.16s ease;
|
}
|
|
.kpi-action:hover {
|
background: rgba(47, 111, 237, 0.08);
|
border-color: rgba(47, 111, 237, 0.22);
|
transform: translateY(-1px);
|
}
|
|
.kpi-action:active {
|
transform: translateY(0);
|
}
|
|
.chart-wrap {
|
border-radius: 12px;
|
overflow: hidden;
|
position: relative;
|
}
|
|
.chart-empty {
|
height: 240px;
|
display: grid;
|
place-items: center;
|
background: rgba(255, 255, 255, 0.7);
|
border-radius: 12px;
|
position: absolute;
|
inset: 0;
|
}
|
|
:deep(.big-chart-dialog .el-dialog) {
|
border-radius: 16px;
|
overflow: hidden;
|
}
|
|
:deep(.big-chart-dialog .el-dialog__header) {
|
padding: 14px 16px;
|
background: radial-gradient(
|
900px 240px at 10% 0%,
|
rgba(47, 111, 237, 0.1),
|
transparent 55%
|
),
|
rgba(255, 255, 255, 0.92);
|
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
|
}
|
|
:deep(.big-chart-dialog .el-dialog__body) {
|
padding: 14px 16px 8px;
|
background: rgba(255, 255, 255, 0.92);
|
}
|
|
:deep(.big-chart-dialog .el-dialog__footer) {
|
padding: 10px 16px 14px;
|
background: rgba(255, 255, 255, 0.92);
|
border-top: 1px solid rgba(15, 23, 42, 0.06);
|
}
|
|
.big-chart-canvas {
|
width: 100%;
|
height: min(74vh, 760px);
|
border-radius: 14px;
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
background: #ffffff;
|
}
|
|
.big-chart-footer {
|
display: flex;
|
justify-content: flex-end;
|
gap: 10px;
|
}
|
|
.chart-tools {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
opacity: 0;
|
transform: translateY(-2px);
|
transition: opacity 0.16s ease, transform 0.16s ease;
|
}
|
|
.chart-card:hover .chart-tools {
|
opacity: 1;
|
transform: translateY(0);
|
}
|
|
.chart-card:focus-within .chart-tools {
|
opacity: 1;
|
transform: translateY(0);
|
}
|
|
:deep(.chart-wrap .el-loading-mask) {
|
border-radius: 12px;
|
backdrop-filter: blur(2px);
|
background-color: rgba(255, 255, 255, 0.55);
|
}
|
|
.chart-tool {
|
font-size: 11px;
|
font-weight: 650;
|
padding: 4px 8px;
|
border-radius: 10px;
|
border: 1px solid rgba(15, 23, 42, 0.1);
|
background: rgba(255, 255, 255, 0.78);
|
color: rgba(15, 23, 42, 0.78);
|
cursor: pointer;
|
transition: background-color 0.16s ease, border-color 0.16s ease,
|
transform 0.16s ease;
|
}
|
|
.chart-tool:hover {
|
background: rgba(47, 111, 237, 0.08);
|
border-color: rgba(47, 111, 237, 0.22);
|
transform: translateY(-1px);
|
}
|
|
.kpi-unit {
|
font-size: 12px;
|
font-weight: 600;
|
color: var(--lux-muted);
|
margin-left: 4px;
|
}
|
|
.kpi-icon {
|
width: 38px;
|
height: 38px;
|
border-radius: 12px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
color: #fff;
|
flex: 0 0 auto;
|
position: relative;
|
z-index: 1;
|
transition: transform 0.28s cubic-bezier(0.16, 1, 0.3, 1), filter 0.28s ease;
|
}
|
|
.kpi-item:hover .kpi-icon {
|
transform: translateY(-1px) rotate(-2deg);
|
filter: saturate(1.06);
|
}
|
|
.kpi-total {
|
background: linear-gradient(
|
135deg,
|
rgba(47, 111, 237, 0.1),
|
rgba(255, 255, 255, 0.86)
|
);
|
}
|
.kpi-total .kpi-icon {
|
background: linear-gradient(135deg, var(--lux-primary), var(--lux-primary-2));
|
}
|
|
.kpi-production {
|
background: linear-gradient(
|
135deg,
|
rgba(22, 163, 74, 0.1),
|
rgba(255, 255, 255, 0.86)
|
);
|
}
|
.kpi-production .kpi-icon {
|
background: linear-gradient(
|
135deg,
|
var(--lux-success),
|
rgba(22, 163, 74, 0.65)
|
);
|
}
|
|
.kpi-office {
|
background: linear-gradient(
|
135deg,
|
rgba(100, 116, 139, 0.1),
|
rgba(255, 255, 255, 0.86)
|
);
|
}
|
.kpi-office .kpi-icon {
|
background: linear-gradient(135deg, #64748b, #94a3b8);
|
}
|
|
.kpi-avg {
|
background: linear-gradient(
|
135deg,
|
rgba(245, 158, 11, 0.1),
|
rgba(255, 255, 255, 0.86)
|
);
|
}
|
.kpi-avg .kpi-icon {
|
background: linear-gradient(
|
135deg,
|
var(--lux-warning),
|
rgba(245, 158, 11, 0.62)
|
);
|
}
|
|
.panel-card {
|
margin-top: 0;
|
border-radius: var(--lux-radius);
|
border-color: var(--lux-border);
|
background: var(--lux-card);
|
backdrop-filter: blur(10px);
|
box-shadow: var(--lux-shadow-soft);
|
padding-bottom: 8px;
|
overflow: hidden;
|
}
|
|
.panel-head {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 12px;
|
padding: 8px 4px 6px;
|
}
|
|
.segmented {
|
position: relative;
|
display: grid;
|
grid-template-columns: 1fr 1fr;
|
padding: 4px;
|
border-radius: 14px;
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
background: rgba(15, 23, 42, 0.03);
|
overflow: hidden;
|
flex: 1 1 auto;
|
min-width: 0;
|
}
|
|
.segmented::after {
|
content: "";
|
position: absolute;
|
top: 10px;
|
bottom: 10px;
|
left: 50%;
|
width: 2px;
|
border-radius: 999px;
|
background: linear-gradient(
|
180deg,
|
rgba(15, 23, 42, 0.06),
|
rgba(15, 23, 42, 0.12),
|
rgba(15, 23, 42, 0.06)
|
);
|
transform: translateX(-0.5px);
|
pointer-events: none;
|
z-index: 0;
|
}
|
|
.segmented.no-active {
|
background: radial-gradient(
|
900px 220px at 20% 0%,
|
rgba(47, 111, 237, 0.06),
|
transparent 55%
|
),
|
rgba(15, 23, 42, 0.03);
|
border-color: rgba(15, 23, 42, 0.1);
|
}
|
|
.segmented-indicator {
|
position: absolute;
|
top: 4px;
|
left: 4px;
|
width: calc(50% - 4px);
|
height: calc(100% - 8px);
|
border-radius: 13px;
|
background: linear-gradient(
|
180deg,
|
rgba(47, 111, 237, 0.1),
|
rgba(255, 255, 255, 0.82)
|
);
|
border: 1px solid rgba(47, 111, 237, 0.18);
|
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.1),
|
0 1px 0 rgba(255, 255, 255, 0.65) inset;
|
transition: transform 0.36s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.2s ease;
|
pointer-events: none;
|
will-change: transform;
|
z-index: 1;
|
}
|
|
.segmented-indicator.hidden {
|
opacity: 0;
|
}
|
|
.segmented-item {
|
position: relative;
|
z-index: 2;
|
width: 100%;
|
display: flex;
|
flex-direction: column;
|
gap: 2px;
|
text-align: left;
|
padding: 10px 12px;
|
border-radius: 12px;
|
border: 1px solid transparent;
|
background: transparent;
|
cursor: pointer;
|
transition: transform 0.16s ease, color 0.16s ease,
|
background-color 0.16s ease;
|
}
|
|
.segmented-item:hover {
|
transform: translateY(-1px);
|
}
|
|
.segmented-item:active {
|
transform: translateY(0);
|
}
|
|
.seg-title {
|
font-size: 13px;
|
font-weight: 780;
|
color: rgba(15, 23, 42, 0.86);
|
letter-spacing: 0.2px;
|
}
|
|
.seg-sub {
|
font-size: 11px;
|
color: rgba(15, 23, 42, 0.46);
|
}
|
|
.segmented-item.active .seg-title {
|
color: var(--lux-text);
|
}
|
|
.segmented-item.active .seg-sub {
|
color: rgba(15, 23, 42, 0.56);
|
}
|
|
.panel-body {
|
padding-top: 4px;
|
}
|
|
.core-kpi {
|
font-size: 12px;
|
font-weight: 650;
|
color: rgba(15, 23, 42, 0.78);
|
padding: 2px 10px;
|
border-radius: 999px;
|
background: rgba(15, 23, 42, 0.04);
|
border: 1px solid rgba(15, 23, 42, 0.06);
|
}
|
|
.chart-card {
|
border-radius: var(--lux-radius);
|
border-color: var(--lux-border);
|
background: var(--lux-card);
|
backdrop-filter: blur(10px);
|
box-shadow: var(--lux-shadow-soft);
|
transition: box-shadow 0.22s ease, transform 0.22s ease,
|
border-color 0.22s ease;
|
|
&:hover {
|
transform: translateY(-2px);
|
box-shadow: var(--lux-shadow);
|
border-color: rgba(47, 111, 237, 0.16);
|
}
|
}
|
|
.chart-head {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 12px;
|
}
|
|
.chart-title {
|
font-weight: 700;
|
color: var(--lux-text);
|
}
|
|
.chart-content {
|
height: 240px;
|
}
|
|
.table-card {
|
border-radius: var(--lux-radius);
|
border-color: var(--lux-border);
|
background: var(--lux-card);
|
backdrop-filter: blur(10px);
|
box-shadow: var(--lux-shadow-soft);
|
transition: box-shadow 0.22s ease, transform 0.22s ease,
|
border-color 0.22s ease;
|
|
&:hover {
|
transform: translateY(-1px);
|
box-shadow: var(--lux-shadow);
|
border-color: rgba(15, 23, 42, 0.1);
|
}
|
}
|
|
.data-table {
|
width: 100%;
|
}
|
|
.consumption-value {
|
font-weight: bold;
|
color: var(--lux-primary);
|
}
|
|
.consumption-unit {
|
font-size: 12px;
|
color: var(--lux-muted);
|
margin-left: 2px;
|
}
|
|
.price-value {
|
font-weight: bold;
|
color: var(--lux-success);
|
}
|
|
.cost-value {
|
font-weight: bold;
|
color: var(--lux-danger);
|
}
|
|
.pagination-container {
|
display: flex;
|
justify-content: flex-end;
|
padding-top: 12px;
|
}
|
|
/* Element Plus 深度样式:卡片式表格质感 */
|
:deep(.lux-table) {
|
border-radius: 12px;
|
overflow: hidden;
|
font-variant-numeric: tabular-nums;
|
}
|
|
:deep(.lux-table .el-table__inner-wrapper::before) {
|
height: 0;
|
}
|
|
:deep(.lux-table .el-table__header-wrapper) {
|
background: linear-gradient(
|
180deg,
|
rgba(15, 23, 42, 0.04) 0%,
|
rgba(15, 23, 42, 0.02) 100%
|
);
|
}
|
|
:deep(.lux-table th.el-table__cell) {
|
background: transparent;
|
color: rgba(15, 23, 42, 0.78);
|
font-weight: 700;
|
letter-spacing: 0.2px;
|
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
|
}
|
|
:deep(.lux-table td.el-table__cell) {
|
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
|
}
|
|
:deep(.lux-table .el-table__row) {
|
transition: background-color 0.18s ease;
|
}
|
|
:deep(.lux-table .el-table__row:hover > td.el-table__cell) {
|
background-color: rgba(47, 111, 237, 0.06) !important;
|
}
|
|
:deep(.lux-table .el-table__row:hover) {
|
box-shadow: inset 3px 0 0 rgba(47, 111, 237, 0.3);
|
}
|
|
:deep(
|
.lux-table .el-table__body tr.el-table__row--striped > td.el-table__cell
|
) {
|
background: rgba(15, 23, 42, 0.018);
|
}
|
|
:deep(.el-pagination) {
|
--el-pagination-button-color: rgba(15, 23, 42, 0.72);
|
--el-pagination-button-bg-color: transparent;
|
--el-pagination-hover-color: var(--lux-primary);
|
}
|
|
:deep(.el-pagination .btn-next),
|
:deep(.el-pagination .btn-prev) {
|
border-radius: 10px;
|
transition: background-color 0.18s ease, transform 0.18s ease;
|
}
|
|
:deep(.el-pagination .btn-next:hover),
|
:deep(.el-pagination .btn-prev:hover) {
|
background-color: rgba(47, 111, 237, 0.06);
|
transform: translateY(-1px);
|
}
|
|
/* 响应式 */
|
@media (max-width: 960px) {
|
.filter-form {
|
flex-direction: column;
|
align-items: flex-start;
|
}
|
|
.filter-actions {
|
justify-content: flex-start;
|
}
|
|
.kpi-strip {
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
}
|
|
.panel-head {
|
flex-wrap: wrap;
|
}
|
}
|
|
/* 折叠动画 */
|
.lux-collapse-enter-active,
|
.lux-collapse-leave-active {
|
transition: max-height 0.22s ease, opacity 0.18s ease;
|
overflow: hidden;
|
}
|
|
.lux-collapse-enter-from,
|
.lux-collapse-leave-to {
|
max-height: 0;
|
opacity: 0;
|
}
|
|
.lux-collapse-enter-to,
|
.lux-collapse-leave-from {
|
max-height: 600px;
|
opacity: 1;
|
}
|
</style>
|