| | |
| | | <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" |
| | | size="small" |
| | | @change="handleTypeChange"> |
| | | <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> |
| | |
| | | </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-select v-model="searchForm.type" |
| | | placeholder="" |
| | | clearable |
| | | class="w-140" |
| | | @change="handleQuery"> |
| | | <el-option label="生产" |
| | | value="生产" /> |
| | | <el-option label="办公" |
| | | value="办公" /> |
| | | <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-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> |
| | | <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')"> |
| | | <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-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" /> |
| | | <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> |
| | |
| | | <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 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')"> |
| | | <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-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" /> |
| | | <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> |
| | |
| | | <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 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')"> |
| | | <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-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" /> |
| | | <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> |
| | |
| | | <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 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')"> |
| | | <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-value"> |
| | | ¥{{ formatMoney(animatedOverview.avgCost) }} |
| | | <span class="kpi-unit" |
| | | >/{{ statisticsType === "day" ? "日" : "月" }}</span |
| | | > |
| | | </div> |
| | | <div class="kpi-meta muted">基于当前筛选与明细统计</div> |
| | | </div> |
| | | <div class="kpi-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 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')"> |
| | | <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')"> |
| | | <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> |
| | | |
| | | <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" |
| | | type="button" |
| | | @click="downloadChart('cost', '能耗成本趋势')">下载</button> |
| | | <button class="chart-tool" |
| | | type="button" |
| | | @click="openBigChart('cost', '能耗成本趋势')">大图</button> |
| | | <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"> |
| | | <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"> |
| | | <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 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"> |
| | | <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> |
| | |
| | | </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" |
| | | type="button" |
| | | @click="downloadChart('purpose', '能耗用途成本占比')">下载</button> |
| | | <button class="chart-tool" |
| | | type="button" |
| | | @click="openBigChart('purpose', '能耗用途成本占比')">大图</button> |
| | | <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"> |
| | | <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"> |
| | | <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('price', '能耗单价对比')">下载</button> |
| | | <button class="chart-tool" |
| | | type="button" |
| | | @click="openBigChart('price', '能耗单价对比')">大图</button> |
| | | <div class="chart-tools" @click.stop> |
| | | <button |
| | | class="chart-tool" |
| | | type="button" |
| | | @click="downloadChart('price', '能耗单价对比')" |
| | | > |
| | | 下载 |
| | | </button> |
| | | <button |
| | | class="chart-tool" |
| | | type="button" |
| | | @click="openBigChart('price', '能耗单价对比')" |
| | | > |
| | | 大图 |
| | | </button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <div ref="priceChartWrap" |
| | | class="chart-wrap" |
| | | v-loading="tableLoading"> |
| | | <div ref="priceChart" |
| | | class="chart-content" |
| | | v-show="hasTableData"></div> |
| | | <div class="chart-empty" |
| | | v-show="!hasTableData"> |
| | | <div |
| | | ref="priceChartWrap" |
| | | class="chart-wrap" |
| | | v-loading="tableLoading" |
| | | > |
| | | <div |
| | | ref="priceChart" |
| | | class="chart-content" |
| | | v-show="hasTableData" |
| | | ></div> |
| | | <div class="chart-empty" v-show="!hasTableData"> |
| | | <el-empty description="暂无数据" /> |
| | | </div> |
| | | </div> |
| | |
| | | </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> |
| | | <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> |
| | | <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"> |
| | | <el-card class="table-card" shadow="never"> |
| | | <div ref="tableAnchor"></div> |
| | | <template #header> |
| | | <div class="card-head"> |
| | |
| | | </div> |
| | | </template> |
| | | |
| | | <el-table :data="displayTableData" |
| | | v-loading="tableLoading" |
| | | stripe |
| | | :header-cell-style="{ height: '44px' }" |
| | | class="data-table lux-table" |
| | | @sort-change="handleSortChange"> |
| | | <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="timePeriod" |
| | | :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"> |
| | | <el-table-column type="index" label="序号" width="60" align="center" /> |
| | | <el-table-column |
| | | prop="timePeriod" |
| | | :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"> |
| | | <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"> |
| | | <el-table-column prop="consumption" label="用量" align="right"> |
| | | <template #default="scope"> |
| | | <span class="consumption-value">{{ formatNumber(scope.row.consumption, 2) }}</span> |
| | | <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"> |
| | | <el-table-column |
| | | prop="price" |
| | | label="单价(元)" |
| | | align="right" |
| | | sortable="custom" |
| | | > |
| | | <template #default="scope"> |
| | | <span class="price-value">{{ formatNumber(scope.row.price, 2) }}</span> |
| | | <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"> |
| | | <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> |
| | | <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" /> |
| | | <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"); |
| | | 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 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 timeColumnLabel = computed(() => { |
| | | return statisticsType.value === "day" ? "日期" : "月份"; |
| | | 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 overview = reactive({ |
| | | totalCost: "0.00", |
| | | productionCost: "0.00", |
| | | officeCost: "0.00", |
| | | avgCost: "0.00", |
| | | }); |
| | | 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 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 tick = (now) => { |
| | | const p = Math.min(1, (now - start) / duration); |
| | | animatedOverview[key] = from + (to - from) * easeOut(p); |
| | | if (p < 1) requestAnimationFrame(tick); |
| | | }; |
| | | requestAnimationFrame(tick); |
| | | }; |
| | | |
| | | const formatNumber = (v, digits = 2) => { |
| | | const n = Number.parseFloat(v); |
| | | if (!Number.isFinite(n)) return "--"; |
| | | return n.toLocaleString("zh-CN", { minimumFractionDigits: digits, maximumFractionDigits: digits }); |
| | | }; |
| | | 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 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 tableData = ref([]); |
| | | const tableLoading = ref(false); |
| | | const hasTableData = computed( |
| | | () => Array.isArray(tableData.value) && tableData.value.length > 0 |
| | | ); |
| | | const queryPulse = ref(false); |
| | | |
| | | 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 kpiSeries = computed(() => { |
| | | const rows = Array.isArray(tableData.value) ? tableData.value : []; |
| | | const byTime = new Map(); |
| | | for (const r of rows) { |
| | | const t = r?.timePeriod ?? ""; |
| | | 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 tableData = ref([]); |
| | | const tableLoading = ref(false); |
| | | const hasTableData = computed(() => Array.isArray(tableData.value) && tableData.value.length > 0); |
| | | const queryPulse = ref(false); |
| | | 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 kpiSeries = computed(() => { |
| | | const rows = Array.isArray(tableData.value) ? tableData.value : []; |
| | | const byTime = new Map(); |
| | | for (const r of rows) { |
| | | const t = r?.timePeriod ?? ""; |
| | | 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 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 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 copyKpi = async (field) => { |
| | | const map = { |
| | | totalCost: animatedOverview.totalCost, |
| | | productionCost: animatedOverview.productionCost, |
| | | officeCost: animatedOverview.officeCost, |
| | | avgCost: animatedOverview.avgCost, |
| | | }; |
| | | |
| | | 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 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 === "price") return priceChartInstance; |
| | | return null; |
| | | }; |
| | | const getChartByKey = (key) => { |
| | | if (key === "cost") return costChartInstance; |
| | | if (key === "type") return typeChartInstance; |
| | | if (key === "purpose") return purposeChartInstance; |
| | | if (key === "price") return priceChartInstance; |
| | | return null; |
| | | }; |
| | | |
| | | const ensurePanelForChart = key => { |
| | | if (key === "cost" || key === "type") chartPanel.value = "core"; |
| | | if (key === "purpose" || key === "price") chartPanel.value = "advanced"; |
| | | }; |
| | | const ensurePanelForChart = (key) => { |
| | | if (key === "cost" || key === "type") chartPanel.value = "core"; |
| | | if (key === "purpose" || key === "price") 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 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 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; |
| | | 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; |
| | | }; |
| | | bigChartInstance = echarts.init(el); |
| | | const opt = src.getOption(); |
| | | bigChartInstance.setOption(opt, true); |
| | | bigChartInstance.resize(); |
| | | }); |
| | | }; |
| | | |
| | | const handleBigChartResize = () => { |
| | | try { |
| | | bigChartInstance?.resize?.(); |
| | | } catch (e) { |
| | | // ignore |
| | | 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; |
| | | } |
| | | }; |
| | | // 表格排序(前端排序:仅影响当前页数据,避免破坏后端分页协议) |
| | | const sortState = reactive({ |
| | | prop: "", |
| | | order: "", |
| | | |
| | | return ( |
| | | String(av ?? "").localeCompare(String(bv ?? ""), "zh-Hans-CN") * direction |
| | | ); |
| | | }); |
| | | }); |
| | | |
| | | const handleSortChange = ({ prop, order }) => { |
| | | sortState.prop = prop || ""; |
| | | sortState.order = order || ""; |
| | | }; |
| | | const energyTypeFilters = [ |
| | | { text: "水", value: "水" }, |
| | | { text: "电", value: "电" }, |
| | | { text: "气", value: "气" }, |
| | | ]; |
| | | const energyPurposeFilters = [ |
| | | { text: "生产", value: "生产" }, |
| | | { text: "办公", value: "办公" }, |
| | | ]; |
| | | |
| | | const displayTableData = computed(() => { |
| | | const data = Array.isArray(tableData.value) ? [...tableData.value] : []; |
| | | if (!sortState.prop || !sortState.order) return data; |
| | | const filterEnergyType = (value, row) => row.energyType === value; |
| | | const filterEnergyPurpose = (value, row) => row.type === value; |
| | | |
| | | const prop = sortState.prop; |
| | | const direction = sortState.order === "ascending" ? 1 : -1; |
| | | const numFields = new Set(["price", "cost", "consumption"]); |
| | | // 分页 |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | total: 0, |
| | | }); |
| | | |
| | | return data.sort((a, b) => { |
| | | const av = a?.[prop]; |
| | | const bv = b?.[prop]; |
| | | // 图表引用 |
| | | const costChart = ref(null); |
| | | const typeChart = ref(null); |
| | | const purposeChart = ref(null); |
| | | const priceChart = ref(null); |
| | | |
| | | 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; |
| | | } |
| | | const costChartWrap = ref(null); |
| | | const typeChartWrap = ref(null); |
| | | const purposeChartWrap = ref(null); |
| | | const priceChartWrap = ref(null); |
| | | |
| | | return String(av ?? "").localeCompare(String(bv ?? ""), "zh-Hans-CN") * direction; |
| | | }); |
| | | 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 priceChartInstance = 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 (priceChart.value && !priceChartInstance) |
| | | priceChartInstance = echarts.init(priceChart.value); |
| | | if (purposeChartInstance) updatePurposeChart(); |
| | | if (priceChartInstance) updatePriceChart(); |
| | | } |
| | | }; |
| | | |
| | | const resizeChartsAfterExpand = () => { |
| | | nextTick(() => { |
| | | ensureChartsReady(chartPanel.value === "none" ? "core" : chartPanel.value); |
| | | handleResize(); |
| | | updateCharts(); |
| | | }); |
| | | }; |
| | | |
| | | const energyTypeFilters = [ |
| | | { text: "水", value: "水" }, |
| | | { text: "电", value: "电" }, |
| | | { text: "气", value: "气" }, |
| | | ]; |
| | | const energyPurposeFilters = [ |
| | | { text: "生产", value: "生产" }, |
| | | { text: "办公", value: "办公" }, |
| | | ]; |
| | | const handleChartPanelClick = (key) => { |
| | | chartPanel.value = chartPanel.value === key ? "none" : key; |
| | | }; |
| | | |
| | | const filterEnergyType = (value, row) => row.energyType === value; |
| | | const filterEnergyPurpose = (value, row) => row.type === value; |
| | | const panelIndicatorStyle = computed(() => { |
| | | const x = chartPanel.value === "advanced" ? "calc(100% + 4px)" : "0"; |
| | | return { transform: `translateX(${x})` }; |
| | | }); |
| | | |
| | | // 分页 |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | total: 0, |
| | | }); |
| | | watch(chartPanel, (val) => { |
| | | if (val !== "none") resizeChartsAfterExpand(); |
| | | }); |
| | | |
| | | // 图表引用 |
| | | const costChart = ref(null); |
| | | const typeChart = ref(null); |
| | | const purposeChart = ref(null); |
| | | const priceChart = ref(null); |
| | | |
| | | const costChartWrap = ref(null); |
| | | const typeChartWrap = ref(null); |
| | | const purposeChartWrap = ref(null); |
| | | const priceChartWrap = 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 priceChartInstance = 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 (priceChart.value && !priceChartInstance) priceChartInstance = echarts.init(priceChart.value); |
| | | if (purposeChartInstance) updatePurposeChart(); |
| | | if (priceChartInstance) updatePriceChart(); |
| | | } |
| | | // 获取能耗类型标签类型 |
| | | const getEnergyTypeType = (type) => { |
| | | const typeMap = { |
| | | 水: "primary", |
| | | 电: "warning", |
| | | 气: "success", |
| | | }; |
| | | return typeMap[type] || "info"; |
| | | }; |
| | | |
| | | 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})` }; |
| | | // 初始化图表 |
| | | const initCharts = () => { |
| | | nextTick(() => { |
| | | // 只初始化可见面板,避免隐藏容器初始化为 0 尺寸导致空白 |
| | | ensureChartsReady(chartPanel.value === "none" ? "core" : chartPanel.value); |
| | | }); |
| | | }; |
| | | |
| | | 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;", |
| | | // 更新能耗成本趋势图 |
| | | 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.timePeriod), |
| | | axisLabel: { |
| | | rotate: statisticsType.value === "day" ? 45 : 0, |
| | | color: "rgba(15, 23, 42, 0.62)", |
| | | }, |
| | | legend: { |
| | | data: ["生产能耗成本", "办公能耗成本"], |
| | | top: 0, |
| | | right: 10, |
| | | textStyle: { color: "#606266" }, |
| | | 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; |
| | | }, |
| | | }, |
| | | grid: { |
| | | left: "3%", |
| | | right: "4%", |
| | | bottom: "10%", |
| | | top: "15%", |
| | | containLabel: true, |
| | | { |
| | | 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; |
| | | }, |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: data.map(item => item.timePeriod), |
| | | axisLabel: { |
| | | rotate: statisticsType.value === "day" ? 45 : 0, |
| | | ], |
| | | 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)", |
| | | }, |
| | | 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; |
| | | }, |
| | | labelLine: { |
| | | show: true, |
| | | lineStyle: { color: "rgba(15, 23, 42, 0.10)" }, |
| | | }, |
| | | { |
| | | 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); |
| | | ], |
| | | color: ["#2f6fed", "#16a34a"], |
| | | }; |
| | | purposeChartInstance.setOption(option); |
| | | }; |
| | | |
| | | // 更新能耗类型成本占比图 |
| | | const updateTypeChart = () => { |
| | | const data = tableData.value; |
| | | const typeCosts = {}; |
| | | // 更新能耗单价对比图 |
| | | const updatePriceChart = () => { |
| | | const data = tableData.value; |
| | | const priceData = {}; |
| | | |
| | | 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 updatePriceChart = () => { |
| | | const data = tableData.value; |
| | | const priceData = {}; |
| | | |
| | | data.forEach(item => { |
| | | if (!priceData[item.energyType]) { |
| | | priceData[item.energyType] = { |
| | | 生产: 0, |
| | | 办公: 0, |
| | | }; |
| | | } |
| | | if (priceData[item.energyType].hasOwnProperty(item.type)) { |
| | | priceData[item.energyType][item.type] = parseFloat(item.price); |
| | | } |
| | | }); |
| | | |
| | | const energyTypes = Object.keys(priceData); |
| | | const productionPrices = energyTypes.map(type => priceData[type].生产); |
| | | const officePrices = energyTypes.map(type => priceData[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: productionPrices, |
| | | 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: officePrices, |
| | | 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], |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | priceChartInstance.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), |
| | | ]; |
| | | data.forEach((item) => { |
| | | if (!priceData[item.energyType]) { |
| | | priceData[item.energyType] = { |
| | | 生产: 0, |
| | | 办公: 0, |
| | | }; |
| | | } |
| | | page.current = 1; |
| | | handleQuery(); |
| | | if (priceData[item.energyType].hasOwnProperty(item.type)) { |
| | | priceData[item.energyType][item.type] = parseFloat(item.price); |
| | | } |
| | | }); |
| | | |
| | | const energyTypes = Object.keys(priceData); |
| | | const productionPrices = energyTypes.map((type) => priceData[type].生产); |
| | | const officePrices = energyTypes.map((type) => priceData[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: productionPrices, |
| | | 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: officePrices, |
| | | 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], |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | priceChartInstance.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, |
| | | }; |
| | | |
| | | // 查询 |
| | | const handleQuery = () => { |
| | | queryPulse.value = true; |
| | | window.setTimeout(() => { |
| | | queryPulse.value = false; |
| | | }, 520); |
| | | tableLoading.value = true; |
| | | 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"; |
| | | |
| | | // 构造请求参数 |
| | | 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"; |
| | | } |
| | | // 结束时间需要取结束月份的最后一天(例如 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; |
| | | } |
| | | // 计算开始到结束的天数(包含起止两天) |
| | | 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) { |
| | | tableData.value = res.data.records || []; |
| | | page.total = res.data.total || 0; |
| | | // 调用接口获取数据 |
| | | energyConsumptionDetailStatistics(params) |
| | | .then((res) => { |
| | | if (res.code === 200) { |
| | | const data = res.data; |
| | | overview.totalConsumption = data.totalEnergyConsumption || "0"; |
| | | overview.totalAmount = data.totalEnergyCost || "0"; |
| | | overview.avgConsumption = data.averageConsumption || "0"; |
| | | overview.compareRate = data.changeVite || 0; |
| | | |
| | | // 更新统计概览数据 |
| | | if (res.data.overview) { |
| | | overview.totalCost = res.data.overview.totalCost || "0.00"; |
| | | overview.productionCost = res.data.overview.productionCost || "0.00"; |
| | | overview.officeCost = res.data.overview.officeCost || "0.00"; |
| | | overview.avgCost = res.data.overview.avgCost || "0.00"; |
| | | } |
| | | } 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 => { |
| | | console.error("获取数据异常:", err); |
| | | // 【假数据(Mock)已禁用】接口异常时不再生成随机假数据,避免误用到生产数据链路 |
| | | ElMessage.error("获取数据异常"); |
| | | // 处理表格数据 |
| | | 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"; |
| | | }) |
| | | .finally(() => { |
| | | tableLoading.value = false; |
| | | updateCharts(); |
| | | }); |
| | | }; |
| | | |
| | | // 【假数据(Mock)已禁用】历史上用于接口异常兜底的随机数据生成逻辑,现已整体注释,避免误用于生产。 |
| | | /* |
| | | // 生成假数据 |
| | | const generateMockData = () => { |
| | | if (statisticsType.value === "day") { |
| | | // 生成最近7天的假数据 |
| | | const mockData = []; |
| | | const today = new Date(); |
| | | |
| | | for (let i = 6; i >= 0; i--) { |
| | | const date = new Date(today); |
| | | date.setDate(date.getDate() - i); |
| | | const dateStr = date.toISOString().split("T")[0]; |
| | | |
| | | // 生产能耗数据 |
| | | mockData.push({ |
| | | timePeriod: dateStr, |
| | | energyType: "电", |
| | | type: "生产", |
| | | consumption: (Math.random() * 1000 + 500).toFixed(2), |
| | | unit: "kWh", |
| | | price: "0.85", |
| | | cost: (Math.random() * 850 + 425).toFixed(2), |
| | | }); |
| | | mockData.push({ |
| | | timePeriod: dateStr, |
| | | energyType: "水", |
| | | type: "生产", |
| | | consumption: (Math.random() * 500 + 200).toFixed(2), |
| | | unit: "m³", |
| | | price: "3.50", |
| | | cost: (Math.random() * 1750 + 700).toFixed(2), |
| | | }); |
| | | mockData.push({ |
| | | timePeriod: dateStr, |
| | | energyType: "气", |
| | | type: "生产", |
| | | consumption: (Math.random() * 300 + 100).toFixed(2), |
| | | unit: "m³", |
| | | price: "2.80", |
| | | cost: (Math.random() * 840 + 280).toFixed(2), |
| | | }); |
| | | |
| | | // 办公能耗数据 |
| | | mockData.push({ |
| | | timePeriod: dateStr, |
| | | energyType: "电", |
| | | type: "办公", |
| | | consumption: (Math.random() * 200 + 100).toFixed(2), |
| | | unit: "kWh", |
| | | price: "0.85", |
| | | cost: (Math.random() * 170 + 85).toFixed(2), |
| | | }); |
| | | mockData.push({ |
| | | timePeriod: dateStr, |
| | | energyType: "水", |
| | | type: "办公", |
| | | consumption: (Math.random() * 50 + 20).toFixed(2), |
| | | unit: "m³", |
| | | price: "3.50", |
| | | cost: (Math.random() * 175 + 70).toFixed(2), |
| | | }); |
| | | } |
| | | |
| | | tableData.value = mockData; |
| | | page.total = mockData.length; |
| | | } else { |
| | | // 生成最近3个月的假数据 |
| | | const mockData = []; |
| | | const today = new Date(); |
| | | |
| | | for (let i = 2; i >= 0; i--) { |
| | | const date = new Date(today); |
| | | date.setMonth(date.getMonth() - i); |
| | | const monthStr = date.toISOString().slice(0, 7); |
| | | |
| | | // 生产能耗数据 |
| | | mockData.push({ |
| | | timePeriod: monthStr, |
| | | energyType: "电", |
| | | type: "生产", |
| | | consumption: (Math.random() * 30000 + 15000).toFixed(2), |
| | | unit: "kWh", |
| | | price: "0.85", |
| | | cost: (Math.random() * 25500 + 12750).toFixed(2), |
| | | }); |
| | | mockData.push({ |
| | | timePeriod: monthStr, |
| | | energyType: "水", |
| | | type: "生产", |
| | | consumption: (Math.random() * 15000 + 6000).toFixed(2), |
| | | unit: "m³", |
| | | price: "3.50", |
| | | cost: (Math.random() * 52500 + 21000).toFixed(2), |
| | | }); |
| | | mockData.push({ |
| | | timePeriod: monthStr, |
| | | energyType: "气", |
| | | type: "生产", |
| | | consumption: (Math.random() * 9000 + 3000).toFixed(2), |
| | | unit: "m³", |
| | | price: "2.80", |
| | | cost: (Math.random() * 25200 + 8400).toFixed(2), |
| | | }); |
| | | |
| | | // 办公能耗数据 |
| | | mockData.push({ |
| | | timePeriod: monthStr, |
| | | energyType: "电", |
| | | type: "办公", |
| | | consumption: (Math.random() * 6000 + 3000).toFixed(2), |
| | | unit: "kWh", |
| | | price: "0.85", |
| | | cost: (Math.random() * 5100 + 2550).toFixed(2), |
| | | }); |
| | | mockData.push({ |
| | | timePeriod: monthStr, |
| | | energyType: "水", |
| | | type: "办公", |
| | | consumption: (Math.random() * 1500 + 600).toFixed(2), |
| | | unit: "m³", |
| | | price: "3.50", |
| | | cost: (Math.random() * 5250 + 2100).toFixed(2), |
| | | }); |
| | | } |
| | | |
| | | tableData.value = mockData; |
| | | page.total = mockData.length; |
| | | } |
| | | |
| | | // 更新统计概览数据 |
| | | calculateOverview(); |
| | | }; |
| | | */ |
| | | |
| | | // 【假数据(Mock)已禁用】与 generateMockData 配套的前端汇总计算(仅供假数据展示),现已注释 |
| | | /* |
| | | // 计算统计概览数据 |
| | | const calculateOverview = () => { |
| | | let totalCost = 0; |
| | | let productionCost = 0; |
| | | let officeCost = 0; |
| | | |
| | | tableData.value.forEach(item => { |
| | | const cost = parseFloat(item.cost); |
| | | totalCost += cost; |
| | | if (item.type === "生产") { |
| | | productionCost += cost; |
| | | } else if (item.type === "办公") { |
| | | officeCost += cost; |
| | | } |
| | | }) |
| | | .catch((err) => { |
| | | console.error("获取数据异常:", err); |
| | | // 【假数据(Mock)已禁用】接口异常时不再生成随机假数据,避免误用到生产数据链路 |
| | | 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(); |
| | | }); |
| | | }; |
| | | |
| | | overview.totalCost = totalCost.toFixed(2); |
| | | overview.productionCost = productionCost.toFixed(2); |
| | | overview.officeCost = officeCost.toFixed(2); |
| | | overview.avgCost = (totalCost / tableData.value.length).toFixed(2); |
| | | }; |
| | | */ |
| | | |
| | | // 更新所有图表 |
| | | const updateCharts = () => { |
| | | nextTick(() => { |
| | | if (costChartInstance) updateCostChart(); |
| | | if (typeChartInstance) updateTypeChart(); |
| | | if (purposeChartInstance) updatePurposeChart(); |
| | | if (priceChartInstance) updatePriceChart(); |
| | | }); |
| | | }; |
| | | |
| | | // 重置 |
| | | 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(); |
| | | priceChartInstance && priceChartInstance.resize(); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | handleQuery(); |
| | | initCharts(); |
| | | window.addEventListener("resize", handleResize); |
| | | // 更新所有图表 |
| | | const updateCharts = () => { |
| | | nextTick(() => { |
| | | if (costChartInstance) updateCostChart(); |
| | | if (typeChartInstance) updateTypeChart(); |
| | | if (purposeChartInstance) updatePurposeChart(); |
| | | if (priceChartInstance) updatePriceChart(); |
| | | }); |
| | | }; |
| | | |
| | | 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; |
| | | // 重置 |
| | | 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(); |
| | | }; |
| | | |
| | | // Enter: 刷新查询 |
| | | if (e.key === "Enter") { |
| | | e.preventDefault(); |
| | | handleQuery(); |
| | | return; |
| | | } |
| | | // 导出 |
| | | const handleExport = () => { |
| | | ElMessage.success("报表导出成功"); |
| | | }; |
| | | |
| | | // Esc: 重置 |
| | | if (e.key === "Escape") { |
| | | e.preventDefault(); |
| | | handleReset(); |
| | | return; |
| | | } |
| | | // 分页大小变化 |
| | | const handleSizeChange = (val) => { |
| | | page.size = val; |
| | | page.current = 1; |
| | | handleQuery(); |
| | | }; |
| | | |
| | | // Alt+E: 导出 |
| | | if (e.altKey && (e.key === "e" || e.key === "E")) { |
| | | e.preventDefault(); |
| | | handleExport(); |
| | | } |
| | | }; |
| | | // 页码变化 |
| | | const handleCurrentChange = (val) => { |
| | | page.current = val; |
| | | handleQuery(); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | window.addEventListener("keydown", handleGlobalHotkeys); |
| | | }); |
| | | // 窗口大小变化时重新渲染图表 |
| | | const handleResize = () => { |
| | | costChartInstance && costChartInstance.resize(); |
| | | typeChartInstance && typeChartInstance.resize(); |
| | | purposeChartInstance && purposeChartInstance.resize(); |
| | | priceChartInstance && priceChartInstance.resize(); |
| | | }; |
| | | |
| | | onUnmounted(() => { |
| | | window.removeEventListener("keydown", handleGlobalHotkeys); |
| | | }); |
| | | 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; |
| | | .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.10), 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%); |
| | | 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: 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 { |
| | | 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: 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.10); |
| | | 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.70); |
| | | 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.10); |
| | | 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.20); |
| | | 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.30); |
| | | 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.20s ease, transform 0.20s ease, border-color 0.20s 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; |
| | | justify-content: flex-start; |
| | | } |
| | | |
| | | .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.10); |
| | | } |
| | | 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.10); |
| | | 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.10), 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.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.10), |
| | | 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.10), |
| | | inset 0 0 0 1px rgba(47, 111, 237, 0.10); |
| | | } |
| | | |
| | | .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.20); |
| | | color: rgba(22, 163, 74, 0.96); |
| | | background: rgba(22, 163, 74, 0.06); |
| | | } |
| | | |
| | | .kpi-chip.down { |
| | | border-color: rgba(239, 68, 68, 0.20); |
| | | 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.10)); |
| | | } |
| | | |
| | | .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.10); |
| | | 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.70); |
| | | 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.10), 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.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.10); |
| | | 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.10), 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.10), 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.10), 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.10), 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; |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | } |
| | | |
| | | .panel-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | padding: 8px 4px 6px; |
| | | flex-wrap: wrap; |
| | | } |
| | | } |
| | | |
| | | .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; |
| | | } |
| | | /* 折叠动画 */ |
| | | .lux-collapse-enter-active, |
| | | .lux-collapse-leave-active { |
| | | transition: max-height 0.22s ease, opacity 0.18s ease; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .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; |
| | | } |
| | | .lux-collapse-enter-from, |
| | | .lux-collapse-leave-to { |
| | | max-height: 0; |
| | | opacity: 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.10); |
| | | } |
| | | |
| | | .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.10), 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.10), |
| | | 0 1px 0 rgba(255, 255, 255, 0.65) inset; |
| | | transition: |
| | | transform 0.36s cubic-bezier(0.16, 1, 0.3, 1), |
| | | opacity 0.20s 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.10); |
| | | } |
| | | } |
| | | |
| | | .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.30); |
| | | } |
| | | |
| | | :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; |
| | | } |
| | | .lux-collapse-enter-to, |
| | | .lux-collapse-leave-from { |
| | | max-height: 600px; |
| | | opacity: 1; |
| | | } |
| | | </style> |