1
yyb
10 小时以前 0878762ff3d0796e5fcb22fc38103a3079d6ca24
1
已修改1个文件
1049 ■■■■ 文件已修改
src/views/costAccounting/energyCosts/index.vue 1049 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/costAccounting/energyCosts/index.vue
@@ -2,8 +2,7 @@
<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">
@@ -13,9 +12,11 @@
            <span class="card-title">查询条件</span>
          </div>
          <div class="card-head-right">
            <el-radio-group v-model="statisticsType"
            <el-radio-group
              v-model="statisticsType"
                            size="small"
                            @change="handleTypeChange">
              @change="handleTypeChange"
            >
              <el-radio-button label="day">按日</el-radio-button>
              <el-radio-button label="month">按月</el-radio-button>
            </el-radio-group>
@@ -24,9 +25,7 @@
      </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="全部"
@@ -44,19 +43,20 @@
            </el-select>
          </el-form-item> -->
          <el-form-item label="能耗用途">
            <el-select v-model="searchForm.type"
            <el-select
              v-model="searchForm.type"
                       placeholder=""
                       clearable
                       class="w-140"
                       @change="handleQuery">
              <el-option label="生产"
                         value="生产" />
              <el-option label="办公"
                         value="办公" />
              @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'"
            <el-date-picker
              v-if="statisticsType === 'day'"
                            v-model="searchForm.dateRange"
                            type="daterange"
                            range-separator="至"
@@ -64,8 +64,10 @@
                            end-placeholder="结束日期"
                            value-format="YYYY-MM-DD"
                            class="w-260"
                            @change="handleQuery" />
            <el-date-picker v-else
              @change="handleQuery"
            />
            <el-date-picker
              v-else
                            v-model="searchForm.monthRange"
                            type="monthrange"
                            range-separator="至"
@@ -73,51 +75,62 @@
                            end-placeholder="结束月份"
                            value-format="YYYY-MM"
                            class="w-260"
                            @change="handleQuery" />
              @change="handleQuery"
            />
          </el-form-item>
        </el-form>
        <div class="filter-actions">
          <el-button class="lux-btn"
          <el-button
            class="lux-btn"
                     type="primary"
                     :loading="tableLoading"
                     @click="handleQuery">刷新</el-button>
          <el-button class="lux-btn"
                     @click="handleReset">重置</el-button>
          <el-button class="lux-btn"
                     type="success"
                     plain
                     @click="handleExport">导出</el-button>
            @click="handleQuery"
            >刷新</el-button
          >
          <el-button class="lux-btn" @click="handleReset">重置</el-button>
          <el-button class="lux-btn" type="success" plain @click="handleExport"
            >导出</el-button
          >
        </div>
      </div>
    </el-card>
    <!-- 图表区域 -->
    <div class="charts">
      <el-card class="panel-card"
               shadow="never">
        <div class="kpi-strip"
      <el-card class="panel-card" shadow="never">
        <div
          class="kpi-strip"
             :class="{ pulse: queryPulse }"
             title="快捷键:Enter 刷新 / Esc 重置 / Alt+E 导出">
          <button class="kpi-item kpi-total"
          title="快捷键:Enter 刷新 / Esc 重置 / Alt+E 导出"
        >
          <button
            class="kpi-item kpi-total"
                  type="button"
                  :class="{ selected: selectedKpi === 'all' }"
                  @click="handleKpiClick('all')">
            @click="handleKpiClick('all')"
          >
            <div class="kpi-left">
              <div class="kpi-label">总能耗成本</div>
              <div class="kpi-value">¥{{ formatMoney(animatedOverview.totalCost) }}</div>
              <div class="kpi-value">
                ¥{{ formatMoney(animatedOverview.totalCost) }}
              </div>
              <div class="kpi-meta">
                <span class="kpi-chip"
                <span
                  class="kpi-chip"
                      :class="kpiDelta.total.pct >= 0 ? 'up' : 'down'"
                      v-if="kpiDelta.total.valid">{{ kpiDelta.total.pct >= 0 ? '+' : '' }}{{ kpiDelta.total.pct.toFixed(1) }}%</span>
                <svg class="kpi-spark"
                     viewBox="0 0 72 22"
                     aria-hidden="true">
                  <polyline :points="sparklinePoints(kpiSeries.total)"
                  v-if="kpiDelta.total.valid"
                  >{{ kpiDelta.total.pct >= 0 ? "+" : ""
                  }}{{ kpiDelta.total.pct.toFixed(1) }}%</span
                >
                <svg class="kpi-spark" viewBox="0 0 72 22" aria-hidden="true">
                  <polyline
                    :points="sparklinePoints(kpiSeries.total)"
                            fill="none"
                            stroke="rgba(47, 111, 237, 0.85)"
                            stroke-width="2"
                            stroke-linecap="round"
                            stroke-linejoin="round" />
                    stroke-linejoin="round"
                  />
                </svg>
              </div>
            </div>
@@ -126,36 +139,51 @@
                <Money />
              </el-icon>
            </div>
            <div class="kpi-actions"
                 @click.stop>
              <button class="kpi-action"
            <div class="kpi-actions" @click.stop>
              <button
                class="kpi-action"
                      type="button"
                      @click="copyKpi('totalCost')">复制</button>
              <button class="kpi-action"
                @click="copyKpi('totalCost')"
              >
                复制
              </button>
              <button
                class="kpi-action"
                      type="button"
                      @click="viewKpiDetails('all')">明细</button>
                @click="viewKpiDetails('all')"
              >
                明细
              </button>
            </div>
          </button>
          <button class="kpi-item kpi-production"
          <button
            class="kpi-item kpi-production"
                  type="button"
                  :class="{ selected: selectedKpi === 'production' }"
                  @click="handleKpiClick('production')">
            @click="handleKpiClick('production')"
          >
            <div class="kpi-left">
              <div class="kpi-label">生产能耗成本</div>
              <div class="kpi-value">¥{{ formatMoney(animatedOverview.productionCost) }}</div>
              <div class="kpi-value">
                ¥{{ formatMoney(animatedOverview.productionCost) }}
              </div>
              <div class="kpi-meta">
                <span class="kpi-chip"
                <span
                  class="kpi-chip"
                      :class="kpiDelta.production.pct >= 0 ? 'up' : 'down'"
                      v-if="kpiDelta.production.valid">{{ kpiDelta.production.pct >= 0 ? '+' : '' }}{{ kpiDelta.production.pct.toFixed(1) }}%</span>
                <svg class="kpi-spark"
                     viewBox="0 0 72 22"
                     aria-hidden="true">
                  <polyline :points="sparklinePoints(kpiSeries.production)"
                  v-if="kpiDelta.production.valid"
                  >{{ kpiDelta.production.pct >= 0 ? "+" : ""
                  }}{{ kpiDelta.production.pct.toFixed(1) }}%</span
                >
                <svg class="kpi-spark" viewBox="0 0 72 22" aria-hidden="true">
                  <polyline
                    :points="sparklinePoints(kpiSeries.production)"
                            fill="none"
                            stroke="rgba(22, 163, 74, 0.85)"
                            stroke-width="2"
                            stroke-linecap="round"
                            stroke-linejoin="round" />
                    stroke-linejoin="round"
                  />
                </svg>
              </div>
            </div>
@@ -164,36 +192,51 @@
                <DataLine />
              </el-icon>
            </div>
            <div class="kpi-actions"
                 @click.stop>
              <button class="kpi-action"
            <div class="kpi-actions" @click.stop>
              <button
                class="kpi-action"
                      type="button"
                      @click="copyKpi('productionCost')">复制</button>
              <button class="kpi-action"
                @click="copyKpi('productionCost')"
              >
                复制
              </button>
              <button
                class="kpi-action"
                      type="button"
                      @click="viewKpiDetails('production')">明细</button>
                @click="viewKpiDetails('production')"
              >
                明细
              </button>
            </div>
          </button>
          <button class="kpi-item kpi-office"
          <button
            class="kpi-item kpi-office"
                  type="button"
                  :class="{ selected: selectedKpi === 'office' }"
                  @click="handleKpiClick('office')">
            @click="handleKpiClick('office')"
          >
            <div class="kpi-left">
              <div class="kpi-label">办公能耗成本</div>
              <div class="kpi-value">¥{{ formatMoney(animatedOverview.officeCost) }}</div>
              <div class="kpi-value">
                ¥{{ formatMoney(animatedOverview.officeCost) }}
              </div>
              <div class="kpi-meta">
                <span class="kpi-chip"
                <span
                  class="kpi-chip"
                      :class="kpiDelta.office.pct >= 0 ? 'up' : 'down'"
                      v-if="kpiDelta.office.valid">{{ kpiDelta.office.pct >= 0 ? '+' : '' }}{{ kpiDelta.office.pct.toFixed(1) }}%</span>
                <svg class="kpi-spark"
                     viewBox="0 0 72 22"
                     aria-hidden="true">
                  <polyline :points="sparklinePoints(kpiSeries.office)"
                  v-if="kpiDelta.office.valid"
                  >{{ kpiDelta.office.pct >= 0 ? "+" : ""
                  }}{{ kpiDelta.office.pct.toFixed(1) }}%</span
                >
                <svg class="kpi-spark" viewBox="0 0 72 22" aria-hidden="true">
                  <polyline
                    :points="sparklinePoints(kpiSeries.office)"
                            fill="none"
                            stroke="rgba(100, 116, 139, 0.90)"
                            stroke-width="2"
                            stroke-linecap="round"
                            stroke-linejoin="round" />
                    stroke-linejoin="round"
                  />
                </svg>
              </div>
            </div>
@@ -202,22 +245,36 @@
                <TrendCharts />
              </el-icon>
            </div>
            <div class="kpi-actions"
                 @click.stop>
              <button class="kpi-action"
            <div class="kpi-actions" @click.stop>
              <button
                class="kpi-action"
                      type="button"
                      @click="copyKpi('officeCost')">复制</button>
              <button class="kpi-action"
                @click="copyKpi('officeCost')"
              >
                复制
              </button>
              <button
                class="kpi-action"
                      type="button"
                      @click="viewKpiDetails('office')">明细</button>
                @click="viewKpiDetails('office')"
              >
                明细
              </button>
            </div>
          </button>
          <button class="kpi-item kpi-avg"
          <button
            class="kpi-item kpi-avg"
                  type="button"
                  @click="handleKpiClick('all')">
            @click="handleKpiClick('all')"
          >
            <div class="kpi-left">
              <div class="kpi-label">平均成本</div>
              <div class="kpi-value">¥{{ formatMoney(animatedOverview.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">
@@ -225,41 +282,56 @@
                <Histogram />
              </el-icon>
            </div>
            <div class="kpi-actions"
                 @click.stop>
              <button class="kpi-action"
            <div class="kpi-actions" @click.stop>
              <button
                class="kpi-action"
                      type="button"
                      @click="copyKpi('avgCost')">复制</button>
              <button class="kpi-action"
                @click="copyKpi('avgCost')"
              >
                复制
              </button>
              <button
                class="kpi-action"
                      type="button"
                      @click="viewKpiDetails('all')">明细</button>
                @click="viewKpiDetails('all')"
              >
                明细
              </button>
            </div>
          </button>
        </div>
        <div class="panel-head">
          <div class="segmented"
          <div
            class="segmented"
               role="tablist"
               aria-label="分析面板切换"
               :class="{ 'no-active': chartPanel === 'none' }">
            <div class="segmented-indicator"
            :class="{ 'no-active': chartPanel === 'none' }"
          >
            <div
              class="segmented-indicator"
                 :class="{ hidden: chartPanel === 'none' }"
                 :style="panelIndicatorStyle"></div>
            <button class="segmented-item"
              :style="panelIndicatorStyle"
            ></div>
            <button
              class="segmented-item"
                    type="button"
                    role="tab"
                    :aria-selected="chartPanel === 'core'"
                    :class="{ active: chartPanel === 'core' }"
                    @click="handleChartPanelClick('core')">
              @click="handleChartPanelClick('core')"
            >
              <span class="seg-title">核心分析</span>
              <span class="seg-sub">趋势 / 类型占比</span>
            </button>
            <button class="segmented-item"
            <button
              class="segmented-item"
                    type="button"
                    role="tab"
                    :aria-selected="chartPanel === 'advanced'"
                    :class="{ active: chartPanel === 'advanced' }"
                    @click="handleChartPanelClick('advanced')">
              @click="handleChartPanelClick('advanced')"
            >
              <span class="seg-title">高级分析</span>
              <span class="seg-sub">用途占比 / 单价对比</span>
            </button>
@@ -267,66 +339,81 @@
        </div>
        <transition name="lux-collapse">
          <div v-show="chartPanel === 'core'"
               class="panel-body">
          <div v-show="chartPanel === 'core'" class="panel-body">
            <el-row :gutter="16">
              <el-col :xs="24"
                      :lg="12">
                <el-card class="chart-card"
                         shadow="never">
              <el-col :xs="24" :lg="12">
                <el-card class="chart-card" shadow="never">
                  <template #header>
                    <div class="chart-head">
                      <span class="chart-title">能耗成本趋势</span>
                      <div class="chart-tools"
                           @click.stop>
                        <button class="chart-tool"
                      <div class="chart-tools" @click.stop>
                        <button
                          class="chart-tool"
                                type="button"
                                @click="downloadChart('cost', '能耗成本趋势')">下载</button>
                        <button class="chart-tool"
                          @click="downloadChart('cost', '能耗成本趋势')"
                        >
                          下载
                        </button>
                        <button
                          class="chart-tool"
                                type="button"
                                @click="openBigChart('cost', '能耗成本趋势')">大图</button>
                          @click="openBigChart('cost', '能耗成本趋势')"
                        >
                          大图
                        </button>
                      </div>
                    </div>
                  </template>
                  <div ref="costChartWrap"
                  <div
                    ref="costChartWrap"
                       class="chart-wrap"
                       v-loading="tableLoading">
                    <div ref="costChart"
                    v-loading="tableLoading"
                  >
                    <div
                      ref="costChart"
                         class="chart-content"
                         v-show="hasTableData"></div>
                    <div class="chart-empty"
                         v-show="!hasTableData">
                      v-show="hasTableData"
                    ></div>
                    <div class="chart-empty" v-show="!hasTableData">
                      <el-empty description="暂无数据" />
                    </div>
                  </div>
                </el-card>
              </el-col>
              <el-col :xs="24"
                      :lg="12">
                <el-card class="chart-card"
                         shadow="never">
              <el-col :xs="24" :lg="12">
                <el-card class="chart-card" shadow="never">
                  <template #header>
                    <div class="chart-head">
                      <span class="chart-title">能耗类型成本占比</span>
                      <div class="chart-tools"
                           @click.stop>
                        <button class="chart-tool"
                      <div class="chart-tools" @click.stop>
                        <button
                          class="chart-tool"
                                type="button"
                                @click="downloadChart('type', '能耗类型成本占比')">下载</button>
                        <button class="chart-tool"
                          @click="downloadChart('type', '能耗类型成本占比')"
                        >
                          下载
                        </button>
                        <button
                          class="chart-tool"
                                type="button"
                                @click="openBigChart('type', '能耗类型成本占比')">大图</button>
                          @click="openBigChart('type', '能耗类型成本占比')"
                        >
                          大图
                        </button>
                      </div>
                    </div>
                  </template>
                  <div ref="typeChartWrap"
                  <div
                    ref="typeChartWrap"
                       class="chart-wrap"
                       v-loading="tableLoading">
                    <div ref="typeChart"
                    v-loading="tableLoading"
                  >
                    <div
                      ref="typeChart"
                         class="chart-content"
                         v-show="hasTableData"></div>
                    <div class="chart-empty"
                         v-show="!hasTableData">
                      v-show="hasTableData"
                    ></div>
                    <div class="chart-empty" v-show="!hasTableData">
                      <el-empty description="暂无数据" />
                    </div>
                  </div>
@@ -337,67 +424,81 @@
        </transition>
        <transition name="lux-collapse">
          <div v-show="chartPanel === 'advanced'"
               class="panel-body">
            <el-row :gutter="16"
                    class="charts-row">
              <el-col :xs="24"
                      :lg="12">
                <el-card class="chart-card"
                         shadow="never">
          <div v-show="chartPanel === 'advanced'" class="panel-body">
            <el-row :gutter="16" class="charts-row">
              <el-col :xs="24" :lg="12">
                <el-card class="chart-card" shadow="never">
                  <template #header>
                    <div class="chart-head">
                      <span class="chart-title">能耗用途成本占比</span>
                      <div class="chart-tools"
                           @click.stop>
                        <button class="chart-tool"
                      <div class="chart-tools" @click.stop>
                        <button
                          class="chart-tool"
                                type="button"
                                @click="downloadChart('purpose', '能耗用途成本占比')">下载</button>
                        <button class="chart-tool"
                          @click="downloadChart('purpose', '能耗用途成本占比')"
                        >
                          下载
                        </button>
                        <button
                          class="chart-tool"
                                type="button"
                                @click="openBigChart('purpose', '能耗用途成本占比')">大图</button>
                          @click="openBigChart('purpose', '能耗用途成本占比')"
                        >
                          大图
                        </button>
                      </div>
                    </div>
                  </template>
                  <div ref="purposeChartWrap"
                  <div
                    ref="purposeChartWrap"
                       class="chart-wrap"
                       v-loading="tableLoading">
                    <div ref="purposeChart"
                    v-loading="tableLoading"
                  >
                    <div
                      ref="purposeChart"
                         class="chart-content"
                         v-show="hasTableData"></div>
                    <div class="chart-empty"
                         v-show="!hasTableData">
                      v-show="hasTableData"
                    ></div>
                    <div class="chart-empty" v-show="!hasTableData">
                      <el-empty description="暂无数据" />
                    </div>
                  </div>
                </el-card>
              </el-col>
              <el-col :xs="24"
                      :lg="12">
                <el-card class="chart-card"
                         shadow="never">
              <el-col :xs="24" :lg="12">
                <el-card class="chart-card" shadow="never">
                  <template #header>
                    <div class="chart-head">
                      <span class="chart-title">能耗单价对比</span>
                      <div class="chart-tools"
                           @click.stop>
                        <button class="chart-tool"
                      <div class="chart-tools" @click.stop>
                        <button
                          class="chart-tool"
                                type="button"
                                @click="downloadChart('price', '能耗单价对比')">下载</button>
                        <button class="chart-tool"
                          @click="downloadChart('price', '能耗单价对比')"
                        >
                          下载
                        </button>
                        <button
                          class="chart-tool"
                                type="button"
                                @click="openBigChart('price', '能耗单价对比')">大图</button>
                          @click="openBigChart('price', '能耗单价对比')"
                        >
                          大图
                        </button>
                      </div>
                    </div>
                  </template>
                  <div ref="priceChartWrap"
                  <div
                    ref="priceChartWrap"
                       class="chart-wrap"
                       v-loading="tableLoading">
                    <div ref="priceChart"
                    v-loading="tableLoading"
                  >
                    <div
                      ref="priceChart"
                         class="chart-content"
                         v-show="hasTableData"></div>
                    <div class="chart-empty"
                         v-show="!hasTableData">
                      v-show="hasTableData"
                    ></div>
                    <div class="chart-empty" v-show="!hasTableData">
                      <el-empty description="暂无数据" />
                    </div>
                  </div>
@@ -409,30 +510,36 @@
      </el-card>
    </div>
    <el-dialog v-model="bigChartVisible"
    <el-dialog
      v-model="bigChartVisible"
               :title="bigChartTitle"
               width="92%"
               top="6vh"
               class="big-chart-dialog"
               destroy-on-close
               @opened="handleBigChartOpened"
               @closed="handleBigChartClosed">
      <div ref="bigChartEl"
           class="big-chart-canvas"></div>
      @closed="handleBigChartClosed"
    >
      <div ref="bigChartEl" class="big-chart-canvas"></div>
      <template #footer>
        <div class="big-chart-footer">
          <el-button class="lux-btn"
                     @click="downloadChart(bigChartKey, bigChartTitle)">下载图片</el-button>
          <el-button class="lux-btn"
          <el-button
            class="lux-btn"
            @click="downloadChart(bigChartKey, bigChartTitle)"
            >下载图片</el-button
          >
          <el-button
            class="lux-btn"
                     type="primary"
                     @click="bigChartVisible = false">关闭</el-button>
            @click="bigChartVisible = false"
            >关闭</el-button
          >
        </div>
      </template>
    </el-dialog>
    <!-- 数据表格 -->
    <el-card class="table-card"
             shadow="never">
    <el-card class="table-card" shadow="never">
      <div ref="tableAnchor"></div>
      <template #header>
        <div class="card-head">
@@ -448,91 +555,114 @@
        </div>
      </template>
      <el-table :data="displayTableData"
      <el-table
        :data="displayTableData"
                v-loading="tableLoading"
                stripe
                :header-cell-style="{ height: '44px' }"
                class="data-table lux-table"
                @sort-change="handleSortChange">
        @sort-change="handleSortChange"
      >
        <template #empty>
          <el-empty description="暂无明细数据" />
        </template>
        <el-table-column type="index"
                         label="序号"
                         width="60"
                         align="center" />
        <el-table-column prop="timePeriod"
        <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"
          sortable="custom"
        />
        <el-table-column
          prop="energyType"
                         label="能耗类型"
                         width="100"
                         align="center"
                         :filters="energyTypeFilters"
                         :filter-method="filterEnergyType"
                         filter-placement="bottom-end">
          filter-placement="bottom-end"
        >
          <template #default="scope">
            <el-tag :type="getEnergyTypeType(scope.row.energyType)">
              {{ scope.row.energyType }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="type"
        <el-table-column
          prop="type"
                         label="能耗用途"
                         width="100"
                         align="center"
                         :filters="energyPurposeFilters"
                         :filter-method="filterEnergyPurpose"
                         filter-placement="bottom-end">
          filter-placement="bottom-end"
        >
          <template #default="scope">
            <el-tag :type="scope.row.type === '生产' ? 'primary' : '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"
        <el-table-column
          prop="price"
                         label="单价(元)"
                         align="right"
                         sortable="custom">
          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"
        <el-table-column
          prop="cost"
                         label="成本(元)"
                         align="right"
                         sortable="custom"
                         fixed="right">
          fixed="right"
        >
          <template #default="scope">
            <span class="cost-value">¥{{ formatNumber(scope.row.cost, 2) }}</span>
            <span class="cost-value"
              >¥{{ formatNumber(scope.row.cost, 2) }}</span
            >
          </template>
        </el-table-column>
      </el-table>
      <div class="pagination-container">
        <el-pagination v-model:current-page="page.current"
        <el-pagination
          v-model:current-page="page.current"
                       v-model:page-size="page.size"
                       :page-sizes="[10, 20, 50, 100]"
                       :total="page.total"
                       layout="total, sizes, prev, pager, next, jumper"
                       @size-change="handleSizeChange"
                       @current-change="handleCurrentChange" />
          @current-change="handleCurrentChange"
        />
      </div>
    </el-card>
  </div>
</template>
<script setup>
  import { ref, reactive, onMounted, onUnmounted, computed, nextTick, watch } from "vue";
import {
  ref,
  reactive,
  onMounted,
  onUnmounted,
  computed,
  nextTick,
  watch,
} from "vue";
  import { ElMessage } from "element-plus";
  import {
    Money,
@@ -589,25 +719,31 @@
    avgCost: 0,
  });
  const formatMoney = v => {
const formatMoney = (v) => {
    const n = Number.parseFloat(v);
    const value = Number.isFinite(n) ? n : 0;
    return value.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
  return value.toLocaleString("zh-CN", {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
  });
  };
  const formatNumber = (v, digits = 2) => {
    const n = Number.parseFloat(v);
    if (!Number.isFinite(n)) return "--";
    return n.toLocaleString("zh-CN", { minimumFractionDigits: digits, maximumFractionDigits: digits });
  return n.toLocaleString("zh-CN", {
    minimumFractionDigits: digits,
    maximumFractionDigits: digits,
  });
  };
  const animateNumber = (key, toValue, duration = 420) => {
    const from = animatedOverview[key] || 0;
    const to = Number.isFinite(toValue) ? toValue : 0;
    const start = performance.now();
    const easeOut = t => 1 - Math.pow(1 - t, 3);
  const easeOut = (t) => 1 - Math.pow(1 - t, 3);
    const tick = now => {
  const tick = (now) => {
      const p = Math.min(1, (now - start) / duration);
      animatedOverview[key] = from + (to - from) * easeOut(p);
      if (p < 1) requestAnimationFrame(tick);
@@ -617,7 +753,7 @@
  watch(
    () => ({ ...overview }),
    val => {
  (val) => {
      animateNumber("totalCost", Number.parseFloat(val.totalCost));
      animateNumber("productionCost", Number.parseFloat(val.productionCost));
      animateNumber("officeCost", Number.parseFloat(val.officeCost));
@@ -629,7 +765,9 @@
  // 表格数据
  const tableData = ref([]);
  const tableLoading = ref(false);
  const hasTableData = computed(() => Array.isArray(tableData.value) && tableData.value.length > 0);
const hasTableData = computed(
  () => Array.isArray(tableData.value) && tableData.value.length > 0
);
  const queryPulse = ref(false);
  const kpiSeries = computed(() => {
@@ -646,15 +784,17 @@
      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);
  const times = Array.from(byTime.keys()).sort((a, b) =>
    String(a).localeCompare(String(b))
  );
  const total = times.map((t) => byTime.get(t).total);
  const production = times.map((t) => byTime.get(t).production);
  const office = times.map((t) => byTime.get(t).office);
    return { times, total, production, office };
  });
  const kpiDelta = computed(() => {
    const pick = arr => {
  const pick = (arr) => {
      const a = Array.isArray(arr) ? arr : [];
      if (a.length < 2) return { pct: 0, valid: false };
      const prev = a[a.length - 2];
@@ -669,7 +809,7 @@
    };
  });
  const sparklinePoints = values => {
const sparklinePoints = (values) => {
    const v = (Array.isArray(values) ? values : []).slice(-12);
    if (v.length < 2) return "";
    const min = Math.min(...v);
@@ -686,7 +826,7 @@
      .join(" ");
  };
  const handleKpiClick = key => {
const handleKpiClick = (key) => {
    selectedKpi.value = key;
    if (key === "all") searchForm.type = "";
    if (key === "production") searchForm.type = "生产";
@@ -695,15 +835,16 @@
    handleQuery();
  };
  const viewKpiDetails = key => {
const viewKpiDetails = (key) => {
    handleKpiClick(key);
    nextTick(() => {
      const el = tableAnchor.value;
      if (el?.scrollIntoView) el.scrollIntoView({ behavior: "smooth", block: "start" });
    if (el?.scrollIntoView)
      el.scrollIntoView({ behavior: "smooth", block: "start" });
    });
  };
  const copyKpi = async field => {
const copyKpi = async (field) => {
    const map = {
      totalCost: animatedOverview.totalCost,
      productionCost: animatedOverview.productionCost,
@@ -730,7 +871,7 @@
    }
  };
  const getChartByKey = key => {
const getChartByKey = (key) => {
    if (key === "cost") return costChartInstance;
    if (key === "type") return typeChartInstance;
    if (key === "purpose") return purposeChartInstance;
@@ -738,7 +879,7 @@
    return null;
  };
  const ensurePanelForChart = key => {
const ensurePanelForChart = (key) => {
    if (key === "cost" || key === "type") chartPanel.value = "core";
    if (key === "purpose" || key === "price") chartPanel.value = "advanced";
  };
@@ -756,9 +897,11 @@
      const purposePart = searchForm.type ? `_${searchForm.type}` : "";
      let rangePart = "";
      if (statisticsType.value === "day") {
        if (searchForm.dateRange?.length === 2) rangePart = `_${searchForm.dateRange[0]}~${searchForm.dateRange[1]}`;
      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]}`;
      if (searchForm.monthRange?.length === 2)
        rangePart = `_${searchForm.monthRange[0]}~${searchForm.monthRange[1]}`;
      }
      a.download = `${title || "chart"}${typePart}${purposePart}${rangePart}.png`;
      a.click();
@@ -838,7 +981,9 @@
        return (aNum - bNum) * direction;
      }
      return String(av ?? "").localeCompare(String(bv ?? ""), "zh-Hans-CN") * direction;
    return (
      String(av ?? "").localeCompare(String(bv ?? ""), "zh-Hans-CN") * direction
    );
    });
  });
@@ -881,7 +1026,7 @@
  const bigChartEl = ref(null);
  let bigChartInstance = null;
  watch(bigChartVisible, v => {
watch(bigChartVisible, (v) => {
    if (v) window.addEventListener("resize", handleBigChartResize);
    else window.removeEventListener("resize", handleBigChartResize);
  });
@@ -904,17 +1049,21 @@
  // 图表区切换:core | advanced | none(点击当前选中可收起)
  const chartPanel = ref("core");
  const ensureChartsReady = panel => {
const ensureChartsReady = (panel) => {
    if (panel === "core") {
      if (costChart.value && !costChartInstance) costChartInstance = echarts.init(costChart.value);
      if (typeChart.value && !typeChartInstance) typeChartInstance = echarts.init(typeChart.value);
    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 (purposeChart.value && !purposeChartInstance)
      purposeChartInstance = echarts.init(purposeChart.value);
    if (priceChart.value && !priceChartInstance)
      priceChartInstance = echarts.init(priceChart.value);
      if (purposeChartInstance) updatePurposeChart();
      if (priceChartInstance) updatePriceChart();
    }
@@ -928,7 +1077,7 @@
    });
  };
  const handleChartPanelClick = key => {
const handleChartPanelClick = (key) => {
    chartPanel.value = chartPanel.value === key ? "none" : key;
  };
@@ -937,12 +1086,12 @@
    return { transform: `translateX(${x})` };
  });
  watch(chartPanel, val => {
watch(chartPanel, (val) => {
    if (val !== "none") resizeChartsAfterExpand();
  });
  // 获取能耗类型标签类型
  const getEnergyTypeType = type => {
const getEnergyTypeType = (type) => {
    const typeMap = {
      水: "primary",
      电: "warning",
@@ -970,7 +1119,8 @@
        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;",
      extraCssText:
        "box-shadow: 0 14px 40px rgba(15,23,42,.14); border-radius: 12px;",
      },
      legend: {
        data: ["生产能耗成本", "办公能耗成本"],
@@ -987,7 +1137,7 @@
      },
      xAxis: {
        type: "category",
        data: data.map(item => item.timePeriod),
      data: data.map((item) => item.timePeriod),
        axisLabel: {
          rotate: statisticsType.value === "day" ? 45 : 0,
          color: "rgba(15, 23, 42, 0.62)",
@@ -1007,7 +1157,7 @@
        {
          name: "生产能耗成本",
          type: "bar",
          data: data.map(item => (item.type === "生产" ? item.cost : 0)),
        data: data.map((item) => (item.type === "生产" ? item.cost : 0)),
          itemStyle: {
            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
              { offset: 0, color: "#409EFF" },
@@ -1022,7 +1172,7 @@
        {
          name: "办公能耗成本",
          type: "bar",
          data: data.map(item => (item.type === "办公" ? item.cost : 0)),
        data: data.map((item) => (item.type === "办公" ? item.cost : 0)),
          itemStyle: {
            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
              { offset: 0, color: "#67C23A" },
@@ -1048,7 +1198,7 @@
    const data = tableData.value;
    const typeCosts = {};
    data.forEach(item => {
  data.forEach((item) => {
      if (!typeCosts[item.energyType]) {
        typeCosts[item.energyType] = 0;
      }
@@ -1068,7 +1218,8 @@
        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;",
      extraCssText:
        "box-shadow: 0 14px 40px rgba(15,23,42,.14); border-radius: 12px;",
      },
      legend: {
        orient: "horizontal",
@@ -1123,7 +1274,7 @@
      办公: 0,
    };
    data.forEach(item => {
  data.forEach((item) => {
      if (purposeCosts.hasOwnProperty(item.type)) {
        purposeCosts[item.type] += parseFloat(item.cost);
      }
@@ -1142,7 +1293,8 @@
        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;",
      extraCssText:
        "box-shadow: 0 14px 40px rgba(15,23,42,.14); border-radius: 12px;",
      },
      legend: {
        orient: "horizontal",
@@ -1184,7 +1336,7 @@
    const data = tableData.value;
    const priceData = {};
    data.forEach(item => {
  data.forEach((item) => {
      if (!priceData[item.energyType]) {
        priceData[item.energyType] = {
          生产: 0,
@@ -1197,8 +1349,8 @@
    });
    const energyTypes = Object.keys(priceData);
    const productionPrices = energyTypes.map(type => priceData[type].生产);
    const officePrices = energyTypes.map(type => priceData[type].办公);
  const productionPrices = energyTypes.map((type) => priceData[type].生产);
  const officePrices = energyTypes.map((type) => priceData[type].办公);
    const option = {
      tooltip: {
@@ -1208,7 +1360,8 @@
        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;",
      extraCssText:
        "box-shadow: 0 14px 40px rgba(15,23,42,.14); border-radius: 12px;",
      },
      legend: {
        data: ["生产能耗单价", "办公能耗单价"],
@@ -1305,7 +1458,6 @@
      days: 0,
      // energyType: searchForm.energyType || undefined,
      type: searchForm.type || undefined,
      // 项目内常用分页参数命名
      pageNum: page.current,
      pageSize: page.size,
    };
@@ -1320,12 +1472,21 @@
        params.startDate = searchForm.monthRange[0] + "-01";
        // 结束时间需要取结束月份的最后一天(例如 2026-03 -> 2026-03-31)
        const [endYearStr, endMonthStr] = String(searchForm.monthRange[1]).split("-");
      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) {
      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")}`;
        params.endDate = `${endYearStr}-${endMonthStr}-${String(
          lastDay
        ).padStart(2, "0")}`;
        } else {
          params.endDate = searchForm.monthRange[1] + "-01";
        }
@@ -1345,18 +1506,17 @@
    // 调用接口获取数据
    energyConsumptionDetailStatistics(params)
      .then(res => {
    .then((res) => {
        if (res.code === 200) {
          tableData.value = res.data.records || [];
          page.total = res.data.total || 0;
        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";
          }
        // 处理表格数据
        tableData.value = data.energyCostDtos || [];
        page.total = tableData.value.length || 0;
        } else {
          ElMessage.error(res.message || "获取数据失败");
          tableData.value = [];
@@ -1367,7 +1527,7 @@
          overview.avgCost = "0.00";
        }
      })
      .catch(err => {
    .catch((err) => {
        console.error("获取数据异常:", err);
        // 【假数据(Mock)已禁用】接口异常时不再生成随机假数据,避免误用到生产数据链路
        ElMessage.error("获取数据异常");
@@ -1383,166 +1543,6 @@
        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;
      }
    });
    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 = () => {
@@ -1585,14 +1585,14 @@
  };
  // 分页大小变化
  const handleSizeChange = val => {
const handleSizeChange = (val) => {
    page.size = val;
    page.current = 1;
    handleQuery();
  };
  // 页码变化
  const handleCurrentChange = val => {
const handleCurrentChange = (val) => {
    page.current = val;
    handleQuery();
  };
@@ -1611,7 +1611,7 @@
    window.addEventListener("resize", handleResize);
  });
  const handleGlobalHotkeys = e => {
const handleGlobalHotkeys = (e) => {
    // 避免在输入框内误触
    const target = e?.target;
    const tag = target?.tagName?.toLowerCase?.();
@@ -1672,9 +1672,16 @@
    --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%),
  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%);
  }
@@ -1724,7 +1731,7 @@
    &:hover {
      transform: translateY(-1px);
      box-shadow: 0 10px 22px rgba(15, 23, 42, 0.10);
    box-shadow: 0 10px 22px rgba(15, 23, 42, 0.1);
      filter: saturate(1.02);
    }
@@ -1745,7 +1752,7 @@
  /* 查询区控件统一皮肤 */
  :deep(.filter-card .el-form-item__label) {
    color: rgba(15, 23, 42, 0.70);
  color: rgba(15, 23, 42, 0.7);
    font-weight: 650;
  }
@@ -1753,20 +1760,21 @@
  :deep(.filter-card .el-select__wrapper) {
    border-radius: 12px;
    box-shadow: none;
    border: 1px solid rgba(15, 23, 42, 0.10);
  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;
  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);
  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.30);
  border-color: rgba(47, 111, 237, 0.3);
    box-shadow: 0 0 0 3px rgba(47, 111, 237, 0.14);
  }
@@ -1888,7 +1896,7 @@
    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;
  transition: box-shadow 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
    &:hover {
      transform: translateY(-1px);
@@ -1937,23 +1945,43 @@
  }
  .metric-total {
    background: linear-gradient(135deg, rgba(47, 111, 237, 0.12), rgba(47, 111, 237, 0.02));
  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));
    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));
  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));
    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));
  background: linear-gradient(
    135deg,
    rgba(144, 147, 153, 0.14),
    rgba(144, 147, 153, 0.03)
  );
    .metric-right {
      background: linear-gradient(135deg, #909399, #b1b3b8);
@@ -1961,10 +1989,18 @@
  }
  .metric-avg {
    background: linear-gradient(135deg, rgba(245, 158, 11, 0.12), rgba(245, 158, 11, 0.02));
  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));
    background: linear-gradient(
      135deg,
      var(--lux-warning),
      rgba(245, 158, 11, 0.62)
    );
    }
  }
@@ -1993,7 +2029,7 @@
      filter: saturate(1.02);
    }
    35% {
      filter: saturate(1.10);
    filter: saturate(1.1);
    }
    100% {
      filter: saturate(1.02);
@@ -2009,7 +2045,8 @@
    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;
  transition: transform 0.18s ease, box-shadow 0.18s ease,
    border-color 0.18s ease;
    min-height: 68px;
    text-align: left;
    cursor: pointer;
@@ -2021,7 +2058,7 @@
  .kpi-item:hover {
    transform: translateY(-1px);
    box-shadow: 0 16px 40px rgba(15, 23, 42, 0.10);
  box-shadow: 0 16px 40px rgba(15, 23, 42, 0.1);
    border-color: rgba(47, 111, 237, 0.18);
  }
@@ -2029,9 +2066,16 @@
    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%);
  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);
@@ -2051,7 +2095,7 @@
    background: linear-gradient(
      135deg,
      rgba(47, 111, 237, 0.18),
      rgba(255, 255, 255, 0.0),
    rgba(255, 255, 255, 0),
      rgba(22, 163, 74, 0.14)
    );
    opacity: 0;
@@ -2069,17 +2113,15 @@
  }
  .kpi-item:focus-visible {
    box-shadow:
      0 16px 44px rgba(15, 23, 42, 0.10),
  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.10),
      inset 0 0 0 1px rgba(47, 111, 237, 0.10);
  box-shadow: 0 16px 44px rgba(15, 23, 42, 0.1),
    inset 0 0 0 1px rgba(47, 111, 237, 0.1);
  }
  .kpi-left {
@@ -2131,13 +2173,13 @@
  }
  .kpi-chip.up {
    border-color: rgba(22, 163, 74, 0.20);
  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.20);
  border-color: rgba(239, 68, 68, 0.2);
    color: rgba(239, 68, 68, 0.96);
    background: rgba(239, 68, 68, 0.06);
  }
@@ -2146,7 +2188,7 @@
    width: 72px;
    height: 22px;
    opacity: 0.9;
    filter: drop-shadow(0 8px 16px rgba(15, 23, 42, 0.10));
  filter: drop-shadow(0 8px 16px rgba(15, 23, 42, 0.1));
  }
  .kpi-actions {
@@ -2173,11 +2215,12 @@
    font-weight: 650;
    padding: 4px 8px;
    border-radius: 999px;
    border: 1px solid rgba(15, 23, 42, 0.10);
  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;
  transition: background-color 0.16s ease, border-color 0.16s ease,
    transform 0.16s ease;
  }
  .kpi-action:hover {
@@ -2200,7 +2243,7 @@
    height: 240px;
    display: grid;
    place-items: center;
    background: rgba(255, 255, 255, 0.70);
  background: rgba(255, 255, 255, 0.7);
    border-radius: 12px;
    position: absolute;
    inset: 0;
@@ -2213,8 +2256,11 @@
  :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%),
  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);
  }
@@ -2248,7 +2294,7 @@
    display: flex;
    align-items: center;
    gap: 8px;
    opacity: 0.0;
  opacity: 0;
    transform: translateY(-2px);
    transition: opacity 0.16s ease, transform 0.16s ease;
  }
@@ -2274,11 +2320,12 @@
    font-weight: 650;
    padding: 4px 8px;
    border-radius: 10px;
    border: 1px solid rgba(15, 23, 42, 0.10);
  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;
  transition: background-color 0.16s ease, border-color 0.16s ease,
    transform 0.16s ease;
  }
  .chart-tool:hover {
@@ -2314,31 +2361,55 @@
  }
  .kpi-total {
    background: linear-gradient(135deg, rgba(47, 111, 237, 0.10), rgba(255, 255, 255, 0.86));
  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.10), rgba(255, 255, 255, 0.86));
  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));
  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));
  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.10), rgba(255, 255, 255, 0.86));
  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));
  background: linear-gradient(
    135deg,
    var(--lux-warning),
    rgba(245, 158, 11, 0.62)
  );
  }
  .panel-card {
@@ -2393,10 +2464,13 @@
  }
  .segmented.no-active {
    background:
      radial-gradient(900px 220px at 20% 0%, rgba(47, 111, 237, 0.06), transparent 55%),
  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);
  border-color: rgba(15, 23, 42, 0.1);
  }
  .segmented-indicator {
@@ -2406,14 +2480,15 @@
    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));
  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.10),
  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.20s ease;
  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;
@@ -2436,7 +2511,8 @@
    border: 1px solid transparent;
    background: transparent;
    cursor: pointer;
    transition: transform 0.16s ease, color 0.16s ease, background-color 0.16s ease;
  transition: transform 0.16s ease, color 0.16s ease,
    background-color 0.16s ease;
  }
  .segmented-item:hover {
@@ -2487,7 +2563,8 @@
    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;
  transition: box-shadow 0.22s ease, transform 0.22s ease,
    border-color 0.22s ease;
    &:hover {
      transform: translateY(-2px);
@@ -2518,12 +2595,13 @@
    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;
  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);
    border-color: rgba(15, 23, 42, 0.1);
    }
  }
@@ -2570,8 +2648,11 @@
  }
  :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%);
  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) {
@@ -2595,10 +2676,12 @@
  }
  :deep(.lux-table .el-table__row:hover) {
    box-shadow: inset 3px 0 0 rgba(47, 111, 237, 0.30);
  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) {
:deep(
    .lux-table .el-table__body tr.el-table__row--striped > td.el-table__cell
  ) {
    background: rgba(15, 23, 42, 0.018);
  }