src/views/costAccounting/stdVsActCostAnalysis/index.vue
@@ -1,44 +1,58 @@
<template>
  <div class="std-cost-page">
    <el-card class="filter-card" shadow="never">
    <div class="page-bg" aria-hidden="true">
      <div class="bg-mesh" />
      <div class="bg-orb bg-orb--a" />
      <div class="bg-orb bg-orb--b" />
      <div class="bg-orb bg-orb--c" />
      <div class="bg-grid" />
    </div>
    <div class="page-inner">
    <el-card class="filter-card glass-card" shadow="never">
      <template #header>
        <div class="card-head">
          <div class="card-head-left">
            <el-icon class="card-icon ui-icon"><DataLine /></el-icon>
            <span class="card-title">标准/实际成本对比分析</span>
            <span class="subtle">差异 = 实际成本 - 标准成本</span>
            <div class="title-badge">
              <el-icon class="card-icon ui-icon"><DataLine /></el-icon>
            </div>
            <div class="title-block">
              <div class="title-row">
                <span class="card-title shimmer-text">标准/实际成本对比分析</span>
                <span class="live-pill">实时分析</span>
              </div>
              <span class="subtle">差异 = 实际成本 − 标准成本</span>
            </div>
          </div>
        </div>
      </template>
      <div class="filter-layout">
        <el-form :model="searchForm" :inline="true" class="filter-form">
          <el-form-item label="月份范围">
          <el-form-item label="月份">
            <el-date-picker
              v-model="searchForm.monthRange"
              type="monthrange"
              range-separator="至"
              start-placeholder="开始月份"
              end-placeholder="结束月份"
              v-model="searchForm.month"
              type="month"
              value-format="YYYY-MM"
              placeholder="选择月份"
              class="w-260"
              @change="handleQuery"
              @change="handleMonthChange"
            />
          </el-form-item>
          <el-form-item label="产品类别">
          <el-form-item label="产品类型">
            <el-select
              v-model="searchForm.category"
              v-model="searchForm.productType"
              clearable
              filterable
              placeholder="全部类别"
              placeholder="全部类型"
              class="w-180"
              @change="handleQuery"
            >
              <el-option
                v-for="item in categoryOptions"
                :key="item"
                :label="item"
                :value="item"
                :key="item.value"
                :label="item.label"
                :value="item.value"
              />
            </el-select>
          </el-form-item>
@@ -46,12 +60,16 @@
            <el-select
              v-model="searchForm.costType"
              clearable
              placeholder="全部类型"
              placeholder="全部成本类型"
              class="w-180"
              @change="handleQuery"
            >
              <el-option label="能耗成本" value="能耗成本" />
              <el-option label="生产成本" value="生产成本" />
              <el-option
                v-for="item in costTypeOptions"
                :key="item.value"
                :label="item.label"
                :value="item.value"
              />
            </el-select>
          </el-form-item>
        </el-form>
@@ -62,97 +80,140 @@
            <el-button class="lux-btn" @click="handleReset">重置</el-button>
          </div>
          <div class="action-group">
            <el-dropdown trigger="click" @command="handleImportCommand">
              <el-button class="lux-btn" type="success" plain>
                标准成本导入
                <el-icon class="el-icon--right"><ArrowDown /></el-icon>
              </el-button>
              <template #dropdown>
                <el-dropdown-menu>
                  <el-dropdown-item command="template">下载导入模板</el-dropdown-item>
                  <el-dropdown-item command="upload">Excel 导入</el-dropdown-item>
                </el-dropdown-menu>
              </template>
            </el-dropdown>
            <el-upload
              ref="uploadRef"
              class="hidden-upload"
              :auto-upload="false"
              :show-file-list="false"
              accept=".xlsx,.xls"
              :on-change="handleFileChange"
            />
            <el-button class="lux-btn" type="success" plain @click="openImportDialog">
              标准成本导入
            </el-button>
          </div>
        </div>
      </div>
    </el-card>
    <el-card class="panel-card" shadow="never">
    <ImportDialog
      ref="importDialogRef"
      v-model="importDialogVisible"
      title="标准成本导入"
      width="520px"
      :headers="importHeaders"
      :action="importAction"
      :auto-upload="false"
      :limit="1"
      tip-text="仅允许导入 xls、xlsx 格式文件。"
      :show-download-template="true"
      :on-success="handleImportSuccess"
      @confirm="handleImportConfirm"
      @download-template="downloadTemplate"
      @close="handleImportDialogClose"
      @cancel="handleImportDialogClose"
    />
    <el-card class="panel-card glass-card kpi-card" shadow="never">
      <div class="kpi-strip">
        <div class="kpi-item kpi-std">
          <div class="kpi-label">标准成本合计</div>
          <div class="kpi-top">
            <el-icon class="kpi-ico"><Histogram /></el-icon>
            <div class="kpi-label">标准成本合计</div>
          </div>
          <div class="kpi-value">¥{{ formatMoney(overview.standardCost) }}</div>
          <div class="kpi-glow" />
        </div>
        <div class="kpi-item kpi-act">
          <div class="kpi-label">实际成本合计</div>
          <div class="kpi-top">
            <el-icon class="kpi-ico"><TrendCharts /></el-icon>
            <div class="kpi-label">实际成本合计</div>
          </div>
          <div class="kpi-value">¥{{ formatMoney(overview.actualCost) }}</div>
          <div class="kpi-glow" />
        </div>
        <div class="kpi-item kpi-diff">
          <div class="kpi-label">差异合计</div>
          <div class="kpi-top">
            <el-icon class="kpi-ico"><Switch /></el-icon>
            <div class="kpi-label">差异合计</div>
          </div>
          <div class="kpi-value" :class="overview.diff >= 0 ? 'cost-value' : 'ok-value'">
            ¥{{ formatMoney(overview.diff) }}
          </div>
          <div class="kpi-glow" />
        </div>
        <div class="kpi-item kpi-rate">
          <div class="kpi-label">差异率</div>
          <div class="kpi-top">
            <el-icon class="kpi-ico"><PieChart /></el-icon>
            <div class="kpi-label">差异率</div>
          </div>
          <div class="kpi-value">{{ formatPercent(overview.diffRate) }}</div>
          <div class="kpi-glow" />
        </div>
      </div>
    </el-card>
    <el-card class="table-card" shadow="never">
    <el-card class="table-card glass-card chart-section" shadow="never">
      <template #header>
        <div class="panel-head">
          <span class="card-title">标准/实际成本可视化(柱状 + 折线)</span>
          <div class="panel-head-main">
            <span class="panel-accent" />
            <div>
              <span class="card-title">标准/实际成本可视化</span>
              <span class="chart-tag">柱状 · 折线</span>
            </div>
          </div>
          <span class="subtle">支持按月份、产品类别、成本类型筛选</span>
        </div>
      </template>
      <div class="chart-wrap">
        <div class="chart-tools chart-tools-inline" @click.stop>
          <button class="chart-tool chart-tool--primary" type="button" @click="openLargeChart">
            <el-icon><ZoomIn /></el-icon>
            查看大图
          </button>
          <button class="chart-tool" type="button" @click="downloadChartImage">
            <el-icon><Download /></el-icon>
            下载图表
          </button>
        </div>
        <div ref="chartRef" class="chart-content"></div>
      </div>
    </el-card>
    <el-card class="table-card" shadow="never">
    <el-dialog
      v-model="largeChartVisible"
      title="标准/实际成本对比大图"
      width="88%"
      top="6vh"
      append-to-body
      destroy-on-close
      @opened="initLargeChart"
      @closed="disposeLargeChart"
    >
      <div ref="largeChartRef" class="large-chart-content"></div>
    </el-dialog>
    <el-card class="table-card glass-card" shadow="never">
      <template #header>
        <div class="panel-head">
          <span class="card-title">对比明细</span>
          <span class="subtle">共 {{ tableData.length }} 条</span>
          <div class="panel-head-main">
            <span class="panel-accent panel-accent--emerald" />
            <span class="card-title">对比明细</span>
          </div>
          <span class="count-chip">共 {{ tableData.length }} 条</span>
        </div>
      </template>
      <el-table :data="pagedTableData" stripe class="lux-table">
        <el-table-column prop="month" label="月份" width="110" />
        <el-table-column prop="category" label="产品类别" min-width="140" />
      <el-table :data="pagedTableData" stripe class="lux-table" @sort-change="handleSortChange">
        <el-table-column prop="periodTime" label="月份" width="110" />
        <el-table-column prop="productType" label="产品类型" min-width="140" />
        <el-table-column prop="costType" label="成本类型" min-width="120" />
        <el-table-column prop="standardCost" label="标准成本(元)" align="right">
          <template #default="scope">¥{{ formatMoney(scope.row.standardCost) }}</template>
        <el-table-column prop="subjectName" label="科目" min-width="140" show-overflow-tooltip />
        <el-table-column prop="budgetQty" label="预算耗量" sortable="custom" align="right" min-width="120" />
        <el-table-column prop="budgetPrice" label="预算单价" sortable="custom" align="right" min-width="120" />
        <el-table-column prop="budgetTotal" label="预算总成本" sortable="custom" align="right" min-width="130">
          <template #default="scope">¥{{ formatMoney(scope.row.budgetTotal) }}</template>
        </el-table-column>
        <el-table-column prop="actualCost" label="实际成本(元)" align="right">
          <template #default="scope">¥{{ formatMoney(scope.row.actualCost) }}</template>
        <el-table-column prop="actualQty" label="实际耗量" sortable="custom" align="right" min-width="120" />
        <el-table-column prop="actualPrice" label="实际单价" sortable="custom" align="right" min-width="120" />
        <el-table-column prop="actualTotal" label="实际总成本" sortable="custom" align="right" min-width="130">
          <template #default="scope">¥{{ formatMoney(scope.row.actualTotal) }}</template>
        </el-table-column>
        <el-table-column prop="diff" label="差异(元)" align="right">
          <template #default="scope">
            <span :class="scope.row.diff >= 0 ? 'cost-value' : 'ok-value'">
              {{ formatSignedMoney(scope.row.diff) }}
            </span>
          </template>
        </el-table-column>
        <el-table-column prop="diffRate" label="差异率" align="right">
          <template #default="scope">
            <span :class="scope.row.diffRate >= 0 ? 'cost-value' : 'ok-value'">
              {{ formatPercent(scope.row.diffRate) }}
            </span>
          </template>
        </el-table-column>
        <el-table-column prop="diffQty" label="耗量差异" min-width="110" align="right" />
        <el-table-column prop="diffPrice" label="单价差异" min-width="110" align="right" />
        <el-table-column prop="diffTotal" label="总成本差异" min-width="110" align="right" />
      </el-table>
      <div class="pagination-container">
        <el-pagination
@@ -166,175 +227,220 @@
        />
      </div>
    </el-card>
    </div>
  </div>
</template>
<script setup>
import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watch } from "vue";
import { ArrowDown, DataLine } from "@element-plus/icons-vue";
import {
  ArrowDown,
  DataLine,
  Download,
  Histogram,
  PieChart,
  Switch,
  TrendCharts,
  ZoomIn,
} from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import ImportDialog from "@/components/Dialog/ImportDialog.vue";
import { getToken } from "@/utils/auth.js";
import * as echarts from "echarts";
import * as XLSX from "xlsx";
import {
  downloadTemplate as downloadProductionSettlementTemplate,
  getImportActionUrl,
  getSettlement,
  getTotalCosts,
  getProductTypes,
} from "@/api/costAccounting/productionSettlementBatches";
const getDefaultMonthRange = () => {
const getDefaultMonth = () => {
  const end = new Date();
  const start = new Date();
  start.setMonth(start.getMonth() - 2);
  return [start.toISOString().slice(0, 7), end.toISOString().slice(0, 7)];
  return end.toISOString().slice(0, 7);
};
const searchForm = reactive({
  monthRange: getDefaultMonthRange(),
  category: "",
  month: getDefaultMonth(),
  productType: "",
  costType: "",
});
const uploadRef = ref();
const categoryOptions = ref([]);
const costTypeOptions = ref([]);
const importDialogVisible = ref(false);
const importDialogRef = ref(null);
const importHeaders = computed(() => ({
  Authorization: `Bearer ${getToken()}`,
}));
const importAction = computed(() =>
  getImportActionUrl({ periodTime: searchForm.month || undefined })
);
const chartRef = ref(null);
const largeChartRef = ref(null);
let chartInstance = null;
let largeChartInstance = null;
const largeChartVisible = ref(false);
const currentChartOption = ref(null);
const actualCostSource = ref([
  { month: "2026-01", category: "瓷砖", costType: "能耗成本", actualCost: 182000 },
  { month: "2026-01", category: "瓷砖", costType: "生产成本", actualCost: 465000 },
  { month: "2026-01", category: "水泥", costType: "能耗成本", actualCost: 138500 },
  { month: "2026-01", category: "水泥", costType: "生产成本", actualCost: 398000 },
  { month: "2026-02", category: "瓷砖", costType: "能耗成本", actualCost: 191500 },
  { month: "2026-02", category: "瓷砖", costType: "生产成本", actualCost: 472500 },
  { month: "2026-02", category: "水泥", costType: "能耗成本", actualCost: 142300 },
  { month: "2026-02", category: "水泥", costType: "生产成本", actualCost: 407000 },
  { month: "2026-03", category: "砂浆", costType: "能耗成本", actualCost: 95800 },
  { month: "2026-03", category: "砂浆", costType: "生产成本", actualCost: 265400 },
  { month: "2026-03", category: "瓷砖", costType: "能耗成本", actualCost: 189800 },
  { month: "2026-03", category: "瓷砖", costType: "生产成本", actualCost: 469900 },
]);
const standardCostSource = ref([
  { month: "2026-01", category: "瓷砖", costType: "能耗成本", standardCost: 176000 },
  { month: "2026-01", category: "瓷砖", costType: "生产成本", standardCost: 452000 },
  { month: "2026-01", category: "水泥", costType: "能耗成本", standardCost: 136000 },
  { month: "2026-01", category: "水泥", costType: "生产成本", standardCost: 392000 },
  { month: "2026-02", category: "瓷砖", costType: "能耗成本", standardCost: 186000 },
  { month: "2026-02", category: "瓷砖", costType: "生产成本", standardCost: 458000 },
  { month: "2026-02", category: "水泥", costType: "能耗成本", standardCost: 139000 },
  { month: "2026-02", category: "水泥", costType: "生产成本", standardCost: 401000 },
  { month: "2026-03", category: "砂浆", costType: "能耗成本", standardCost: 93000 },
  { month: "2026-03", category: "砂浆", costType: "生产成本", standardCost: 259000 },
  { month: "2026-03", category: "瓷砖", costType: "能耗成本", standardCost: 185000 },
  { month: "2026-03", category: "瓷砖", costType: "生产成本", standardCost: 461000 },
]);
const categoryOptions = computed(() => {
  const all = [...actualCostSource.value, ...standardCostSource.value];
  return Array.from(new Set(all.map((item) => item.category)));
const settlementRows = ref([]);
const totalCosts = reactive({
  budgetTotal: 0,
  actualTotal: 0,
  diffTotal: 0,
  diffRate: "0%",
});
const inRange = (value, range) => {
  if (!Array.isArray(range) || range.length !== 2 || !range[0] || !range[1]) return true;
  return value >= range[0] && value <= range[1];
};
const mergedRows = computed(() => {
  const key = (item) => `${item.month}__${item.category}__${item.costType}`;
  const stdMap = new Map(standardCostSource.value.map((item) => [key(item), item]));
  const actMap = new Map(actualCostSource.value.map((item) => [key(item), item]));
  const keySet = new Set([...stdMap.keys(), ...actMap.keys()]);
  const rows = [];
  for (const k of keySet) {
    const std = stdMap.get(k);
    const act = actMap.get(k);
    const month = std?.month || act?.month || "";
    const category = std?.category || act?.category || "";
    const costType = std?.costType || act?.costType || "";
    const standardCost = Number(std?.standardCost || 0);
    const actualCost = Number(act?.actualCost || 0);
    const diff = actualCost - standardCost;
    const diffRate = standardCost === 0 ? 0 : (diff / standardCost) * 100;
    rows.push({ month, category, costType, standardCost, actualCost, diff, diffRate });
  }
  return rows.sort((a, b) => {
    if (a.month !== b.month) return a.month > b.month ? 1 : -1;
    if (a.category !== b.category) return a.category.localeCompare(b.category, "zh-Hans-CN");
    return a.costType.localeCompare(b.costType, "zh-Hans-CN");
const tableData = computed(() => {
  return (Array.isArray(settlementRows.value) ? settlementRows.value : []).filter((item) => {
    const hitMonth = !searchForm.month || item.periodTime === searchForm.month;
    const hitProductType = !searchForm.productType || item.productType === searchForm.productType;
    const hitCostType = !searchForm.costType || item.costType === searchForm.costType;
    return hitMonth && hitProductType && hitCostType;
  });
});
const tableData = computed(() =>
  mergedRows.value.filter((item) => {
    const hitMonth = inRange(item.month, searchForm.monthRange);
    const hitCategory = !searchForm.category || item.category === searchForm.category;
    const hitCostType = !searchForm.costType || item.costType === searchForm.costType;
    return hitMonth && hitCategory && hitCostType;
  })
);
const page = reactive({
  current: 1,
  size: 10,
});
const pagedTableData = computed(() => {
  const start = (page.current - 1) * page.size;
  return tableData.value.slice(start, start + page.size);
/** sortable="custom" 需在 sort-change 里自行排序,再分页 */
const tableSort = reactive({
  prop: "",
  order: "",
});
const overview = computed(() => {
  const standardCost = tableData.value.reduce((sum, item) => sum + item.standardCost, 0);
  const actualCost = tableData.value.reduce((sum, item) => sum + item.actualCost, 0);
  const diff = actualCost - standardCost;
  const diffRate = standardCost === 0 ? 0 : (diff / standardCost) * 100;
  return { standardCost, actualCost, diff, diffRate };
});
const getChartData = () => {
  const xAxis = tableData.value.map(
    (item) => `${item.month}\n${item.category}-${item.costType.replace("成本", "")}`
  );
  const standard = tableData.value.map((item) => item.standardCost);
  const actual = tableData.value.map((item) => item.actualCost);
  const diffRate = tableData.value.map((item) => Number(item.diffRate.toFixed(2)));
  return { xAxis, standard, actual, diffRate };
const handleSortChange = ({ prop, order }) => {
  tableSort.prop = prop || "";
  tableSort.order = order || "";
  page.current = 1;
};
const updateChart = () => {
  if (!chartInstance) return;
  const { xAxis, standard, actual, diffRate } = getChartData();
  chartInstance.setOption({
const sortedTableData = computed(() => {
  const rows = [...tableData.value];
  if (!tableSort.prop || !tableSort.order) return rows;
  const dir = tableSort.order === "ascending" ? 1 : -1;
  const key = tableSort.prop;
  rows.sort((a, b) => {
    const na = Number(a[key]);
    const nb = Number(b[key]);
    const va = Number.isFinite(na) ? na : 0;
    const vb = Number.isFinite(nb) ? nb : 0;
    if (va === vb) return 0;
    return va < vb ? -dir : dir;
  });
  return rows;
});
const pagedTableData = computed(() => {
  const start = (page.current - 1) * page.size;
  return sortedTableData.value.slice(start, start + page.size);
});
const overview = computed(() => ({
  standardCost: Number(totalCosts.budgetTotal || 0),
  actualCost: Number(totalCosts.actualTotal || 0),
  diff: Number(totalCosts.diffTotal || 0),
  diffRate: totalCosts.diffRate ?? "0%",
}));
const getChartData = () => {
  // 图表口径:按“科目”汇总展示全部科目
  const agg = new Map();
  for (const row of tableData.value) {
    const subjectName = String(row?.subjectName || "").trim() || "-";
    const budgetTotal = Number(row?.budgetTotal || 0);
    const actualTotal = Number(row?.actualTotal || 0);
    const bucket = agg.get(subjectName) || { subjectName, budgetTotal: 0, actualTotal: 0 };
    bucket.budgetTotal += Number.isFinite(budgetTotal) ? budgetTotal : 0;
    bucket.actualTotal += Number.isFinite(actualTotal) ? actualTotal : 0;
    agg.set(subjectName, bucket);
  }
  const rows = Array.from(agg.values()).sort((a, b) =>
    String(a.subjectName).localeCompare(String(b.subjectName), "zh-Hans-CN")
  );
  const xAxis = rows.map((item) => item.subjectName);
  const standard = rows.map((item) => item.budgetTotal);
  const actual = rows.map((item) => item.actualTotal);
  const diffRate = rows.map((item) => {
    const base = Number(item.budgetTotal || 0);
    const diff = Number(item.actualTotal || 0) - base;
    if (!base) return 0;
    return (diff / base) * 100;
  });
  return { xAxis, standard, actual, diffRate, rows };
};
const buildChartOption = () => {
  const { xAxis, standard, actual, diffRate, rows } = getChartData();
  return {
    animation: true,
    animationDuration: 920,
    animationEasing: "cubicOut",
    textStyle: { fontFamily: "inherit" },
    tooltip: {
      trigger: "axis",
      axisPointer: { type: "shadow" },
      axisPointer: {
        type: "cross",
        crossStyle: { color: "rgba(47, 111, 237, 0.35)" },
        lineStyle: { type: "dashed" },
      },
      backgroundColor: "rgba(255, 255, 255, 0.94)",
      borderColor: "rgba(47, 111, 237, 0.22)",
      borderWidth: 1,
      padding: [12, 14],
      textStyle: { color: "rgba(15, 23, 42, 0.88)" },
      extraCssText: "box-shadow: 0 12px 40px rgba(15, 23, 42, 0.12); border-radius: 12px;",
      formatter: (params) => {
        const row = tableData.value[params[0]?.dataIndex] || {};
        const row = rows?.[params[0]?.dataIndex] || {};
        const budgetTotal = Number(row?.budgetTotal || 0);
        const actualTotal = Number(row?.actualTotal || 0);
        const diff = actualTotal - budgetTotal;
        const rate = budgetTotal ? (diff / budgetTotal) * 100 : 0;
        return [
          `${row.month || ""} ${row.category || ""} ${row.costType || ""}`,
          `标准成本:¥${formatMoney(row.standardCost || 0)}`,
          `实际成本:¥${formatMoney(row.actualCost || 0)}`,
          `差异:${formatSignedMoney(row.diff || 0)}`,
          `差异率:${formatPercent(row.diffRate || 0)}`,
          `科目:${row.subjectName || "-"}`,
          `预算总成本:¥${formatMoney(budgetTotal)}`,
          `实际总成本:¥${formatMoney(actualTotal)}`,
          `差异:${formatSignedMoney(diff)}`,
          `差异率:${formatPercent(rate)}`,
        ].join("<br/>");
      },
    },
    legend: { data: ["标准成本", "实际成本", "差异率"] },
    grid: { left: "4%", right: "4%", top: "16%", bottom: "16%", containLabel: true },
    legend: {
      data: ["标准成本", "实际成本", "差异率"],
      top: 6,
      itemGap: 18,
      textStyle: { color: "rgba(15, 23, 42, 0.72)" },
    },
    grid: { left: "3%", right: "3%", top: "18%", bottom: "14%", containLabel: true },
    xAxis: {
      type: "category",
      data: xAxis,
      axisLabel: { color: "rgba(15, 23, 42, 0.62)" },
      axisLine: { lineStyle: { color: "rgba(15, 23, 42, 0.08)" } },
      axisLabel: { color: "rgba(15, 23, 42, 0.62)", fontSize: 11 },
      axisLine: { lineStyle: { color: "rgba(15, 23, 42, 0.1)" } },
      axisTick: { show: false },
    },
    yAxis: [
      {
        type: "value",
        name: "成本(元)",
        axisLabel: { color: "rgba(15, 23, 42, 0.58)" },
        splitLine: { lineStyle: { color: "rgba(15, 23, 42, 0.06)" } },
        nameTextStyle: { color: "rgba(15, 23, 42, 0.5)", fontSize: 11 },
        axisLabel: { color: "rgba(15, 23, 42, 0.55)" },
        splitLine: { lineStyle: { color: "rgba(15, 23, 42, 0.06)", type: "dashed" } },
      },
      {
        type: "value",
        name: "差异率(%)",
        axisLabel: { color: "rgba(15, 23, 42, 0.58)" },
        nameTextStyle: { color: "rgba(15, 23, 42, 0.5)", fontSize: 11 },
        axisLabel: { color: "rgba(15, 23, 42, 0.55)" },
        splitLine: { show: false },
      },
    ],
@@ -342,28 +448,72 @@
      {
        name: "标准成本",
        type: "bar",
        barMaxWidth: 24,
        barMaxWidth: 26,
        data: standard,
        itemStyle: { color: "#5b8cff", borderRadius: [4, 4, 0, 0] },
        itemStyle: {
          borderRadius: [6, 6, 0, 0],
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: "#8eb4ff" },
            { offset: 1, color: "#3d74f5" },
          ]),
        },
        emphasis: {
          itemStyle: {
            shadowBlur: 12,
            shadowColor: "rgba(61, 116, 245, 0.45)",
          },
        },
      },
      {
        name: "实际成本",
        type: "bar",
        barMaxWidth: 24,
        barMaxWidth: 26,
        data: actual,
        itemStyle: { color: "#f59e0b", borderRadius: [4, 4, 0, 0] },
        itemStyle: {
          borderRadius: [6, 6, 0, 0],
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: "#fcd34d" },
            { offset: 1, color: "#ea580c" },
          ]),
        },
        emphasis: {
          itemStyle: {
            shadowBlur: 12,
            shadowColor: "rgba(234, 88, 12, 0.4)",
          },
        },
      },
      {
        name: "差异率",
        type: "line",
        yAxisIndex: 1,
        smooth: true,
        symbol: "circle",
        symbolSize: 7,
        showSymbol: true,
        data: diffRate,
        itemStyle: { color: "#ef4444" },
        lineStyle: { width: 2 },
        lineStyle: { width: 3, shadowBlur: 8, shadowColor: "rgba(239, 68, 68, 0.35)" },
        itemStyle: {
          color: "#ef4444",
          borderColor: "#fff",
          borderWidth: 2,
        },
        areaStyle: {
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: "rgba(239, 68, 68, 0.28)" },
            { offset: 1, color: "rgba(239, 68, 68, 0.02)" },
          ]),
        },
      },
    ],
  });
  };
};
const updateChart = () => {
  const option = buildChartOption();
  currentChartOption.value = option;
  chartInstance?.setOption(option);
  largeChartInstance?.setOption(option);
};
const normalizeCostType = (value) => {
@@ -397,72 +547,147 @@
  standardCostSource.value = Array.from(map.values());
};
const handleFileChange = async (uploadFile) => {
  try {
    const file = uploadFile.raw;
    if (!file) return;
    const data = await file.arrayBuffer();
    const workbook = XLSX.read(data, { type: "array" });
    const sheetName = workbook.SheetNames[0];
    const sheet = workbook.Sheets[sheetName];
    const rows = XLSX.utils.sheet_to_json(sheet, { defval: "" });
    const parsed = parseImportedRows(rows);
    if (!parsed.length) {
      ElMessage.warning("导入失败:模板内容为空或字段不匹配");
      return;
    }
    replaceStandardSourceByImport(parsed);
    ElMessage.success(`导入成功:${parsed.length} 条标准成本记录`);
    handleQuery();
  } catch (error) {
    console.error(error);
    ElMessage.error("导入失败,请检查 Excel 格式");
  } finally {
    uploadRef.value?.clearFiles?.();
  }
const openImportDialog = () => {
  importDialogVisible.value = true;
};
const openUploadSelector = () => {
  const input = uploadRef.value?.$el?.querySelector?.("input[type='file']");
  if (!input) {
    ElMessage.warning("上传组件尚未就绪,请稍后重试");
    return;
  }
  input.click();
const handleImportConfirm = () => {
  importDialogRef.value?.submit?.();
};
const handleImportCommand = (command) => {
  if (command === "template") {
    downloadTemplate();
    return;
  }
  if (command === "upload") {
    openUploadSelector();
  }
const handleImportDialogClose = () => {
  importDialogRef.value?.clearFiles?.();
};
const downloadTemplate = () => {
  const sample = [
    { 月份: "2026-03", 产品类别: "瓷砖", 成本类型: "标准能耗成本", 标准成本: 185000 },
    { 月份: "2026-03", 产品类别: "瓷砖", 成本类型: "标准生产成本", 标准成本: 461000 },
    { 月份: "2026-03", 产品类别: "水泥", 成本类型: "标准能耗成本", 标准成本: 140000 },
    { 月份: "2026-03", 产品类别: "水泥", 成本类型: "标准生产成本", 标准成本: 405000 },
  ];
  const ws = XLSX.utils.json_to_sheet(sample);
  const wb = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(wb, ws, "标准成本模板");
  XLSX.writeFile(wb, "标准成本按月导入模板.xlsx");
  ElMessage.success("模板已下载");
  downloadProductionSettlementTemplate({ periodTime: searchForm.month || undefined })
    .then((data) => {
      const blob =
        data instanceof Blob
          ? data
          : new Blob([data], {
              type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            });
      const url = window.URL.createObjectURL(blob);
      const link = document.createElement("a");
      link.href = url;
      link.download = "标准成本导入模板.xlsx";
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
      window.URL.revokeObjectURL(url);
      ElMessage.success("模板下载成功");
    })
    .catch(() => {
      ElMessage.error("模板下载失败");
    });
};
const handleQuery = () => {
  updateChart();
const handleImportSuccess = (response) => {
  const code = response?.code;
  const msg = response?.msg || response?.message;
  if (code === 200) {
    ElMessage.success(msg || "导入成功");
    importDialogVisible.value = false;
    importDialogRef.value?.clearFiles?.();
    handleQuery();
    return;
  }
  ElMessage.error(msg || "导入失败");
};
const normalizeSettlementResponse = (data) => {
  const map = data && typeof data === "object" ? data : {};
  const rows = [];
  for (const [costType, list] of Object.entries(map)) {
    if (!Array.isArray(list)) continue;
    for (const item of list) {
      rows.push({
        ...item,
        periodTime: searchForm.month,
        costType: costType,
      });
    }
  }
  return rows;
};
const fetchCategoryOptions = async () => {
  if (!searchForm.month) {
    categoryOptions.value = [];
    return;
  }
  try {
    const { data } = await getProductTypes({ periodTime: searchForm.month });
    const list = Array.isArray(data) ? data : data?.records || [];
    categoryOptions.value = list.map((item) => ({
      label: item.label || item.name || item.typeName || item,
      value: item.value || item.code || item.typeCode || item,
    }));
  } catch (e) {
    categoryOptions.value = [];
  }
};
const fetchCostTypeOptions = async () => {
  if (!searchForm.month) {
    costTypeOptions.value = [];
    return;
  }
  try {
    // 不带 costType,拿到完整分组 key 用于下拉选项
    const res = await getSettlement({ periodTime: searchForm.month });
    const map = res?.data || {};
    const keys = Object.keys(map || {});
    costTypeOptions.value = keys.map((k) => ({ label: k, value: k }));
  } catch (e) {
    costTypeOptions.value = [];
  }
};
const handleMonthChange = () => {
  searchForm.productType = "";
  searchForm.costType = "";
  Promise.all([fetchCategoryOptions(), fetchCostTypeOptions()]).then(() => {
    handleQuery();
  });
};
const handleQuery = async () => {
  try {
    const params = {
      periodTime: searchForm.month || undefined,
      productType: searchForm.productType || undefined,
      costType: searchForm.costType || undefined,
    };
    const [settlementRes, totalRes] = await Promise.all([getSettlement(params), getTotalCosts(params)]);
    const map = settlementRes?.data || {};
    const rows = normalizeSettlementResponse(map);
    settlementRows.value = rows;
    const totals = totalRes?.data || {};
    totalCosts.budgetTotal = Number(totals.budgetTotal || 0);
    totalCosts.actualTotal = Number(totals.actualTotal || 0);
    totalCosts.diffTotal = Number(totals.diffTotal || 0);
    totalCosts.diffRate = totals.diffRate ?? "0%";
    updateChart();
  } catch (e) {
    settlementRows.value = [];
    totalCosts.budgetTotal = 0;
    totalCosts.actualTotal = 0;
    totalCosts.diffTotal = 0;
    totalCosts.diffRate = "0%";
    updateChart();
  }
};
const handleReset = () => {
  searchForm.monthRange = getDefaultMonthRange();
  searchForm.category = "";
  searchForm.month = getDefaultMonth();
  searchForm.productType = "";
  searchForm.costType = "";
  tableSort.prop = "";
  tableSort.order = "";
  page.current = 1;
  handleQuery();
};
@@ -493,6 +718,7 @@
};
const formatPercent = (v) => {
  if (typeof v === "string" && v.trim().endsWith("%")) return v.trim();
  const n = Number.parseFloat(v);
  const value = Number.isFinite(n) ? n : 0;
  const sign = value >= 0 ? "+" : "";
@@ -501,6 +727,56 @@
const handleResize = () => {
  chartInstance?.resize?.();
  largeChartInstance?.resize?.();
};
const openLargeChart = () => {
  if (!tableData.value.length) {
    ElMessage.warning("暂无图表数据可查看");
    return;
  }
  largeChartVisible.value = true;
};
const initLargeChart = () => {
  nextTick(() => {
    if (!largeChartRef.value) return;
    if (!largeChartInstance) {
      largeChartInstance = echarts.init(largeChartRef.value);
    }
    if (currentChartOption.value) {
      largeChartInstance.setOption(currentChartOption.value);
    } else {
      updateChart();
    }
    // 弹窗出现后容器尺寸会变化,强制 resize 防止 canvas 溢出遮挡表头/关闭按钮
    largeChartInstance?.resize?.();
  });
};
const disposeLargeChart = () => {
  largeChartInstance?.dispose?.();
  largeChartInstance = null;
};
const downloadChartImage = () => {
  const sourceChart = chartInstance || largeChartInstance;
  if (!sourceChart) {
    ElMessage.warning("图表尚未加载完成");
    return;
  }
  const url = sourceChart.getDataURL({
    type: "png",
    pixelRatio: 2,
    backgroundColor: "#ffffff",
  });
  const link = document.createElement("a");
  link.href = url;
  link.download = `标准实际成本对比图_${new Date().toISOString().slice(0, 10)}.png`;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
  ElMessage.success("图表下载成功");
};
onMounted(() => {
@@ -510,6 +786,9 @@
    }
    updateChart();
  });
  Promise.all([fetchCategoryOptions(), fetchCostTypeOptions()]).then(() => {
    handleQuery();
  });
  window.addEventListener("resize", handleResize);
});
@@ -517,6 +796,7 @@
  window.removeEventListener("resize", handleResize);
  chartInstance?.dispose?.();
  chartInstance = null;
  disposeLargeChart();
});
watch(tableData, () => {
@@ -527,26 +807,138 @@
</script>
<style scoped lang="scss">
@keyframes mesh-shift {
  0%,
  100% {
    opacity: 1;
    transform: scale(1) translate(0, 0);
  }
  50% {
    opacity: 0.85;
    transform: scale(1.02) translate(-1%, 1%);
  }
}
@keyframes orb-float {
  0%,
  100% {
    transform: translate(0, 0) scale(1);
  }
  33% {
    transform: translate(12px, -18px) scale(1.05);
  }
  66% {
    transform: translate(-8px, 10px) scale(0.98);
  }
}
@keyframes shimmer {
  0% {
    background-position: 200% center;
  }
  100% {
    background-position: -200% center;
  }
}
.std-cost-page {
  --lux-bg: #f6f7fb;
  --lux-card: rgba(255, 255, 255, 0.86);
  --lux-border: rgba(15, 23, 42, 0.08);
  --lux-bg: #eef1f8;
  --lux-card: rgba(255, 255, 255, 0.72);
  --lux-border: rgba(15, 23, 42, 0.1);
  --lux-text: rgba(15, 23, 42, 0.92);
  --lux-subtle: rgba(15, 23, 42, 0.58);
  --lux-primary: #2f6fed;
  --lux-success: #16a34a;
  --lux-warning: #f59e0b;
  --lux-danger: #ef4444;
  --lux-shadow-soft: 0 10px 28px rgba(15, 23, 42, 0.06);
  --lux-radius: 14px;
  --lux-shadow-soft: 0 12px 40px rgba(15, 23, 42, 0.08);
  --lux-shadow-lift: 0 20px 50px rgba(47, 111, 237, 0.12);
  --lux-radius: 16px;
  padding: 18px 22px 24px;
  background: radial-gradient(
      1200px 420px at 20% 0%,
      rgba(47, 111, 237, 0.1),
      transparent 55%
    ),
    linear-gradient(180deg, var(--lux-bg) 0%, #ffffff 58%);
  position: relative;
  min-height: 100%;
  padding: 20px 22px 28px;
  overflow: hidden;
  background: linear-gradient(165deg, #e8ecf7 0%, #f4f6fb 42%, #fafbfd 100%);
}
.page-bg {
  pointer-events: none;
  position: absolute;
  inset: 0;
  z-index: 0;
}
.bg-mesh {
  position: absolute;
  inset: -20%;
  background:
    radial-gradient(ellipse 80% 50% at 15% 0%, rgba(47, 111, 237, 0.18), transparent 50%),
    radial-gradient(ellipse 60% 45% at 85% 15%, rgba(245, 158, 11, 0.12), transparent 45%),
    radial-gradient(ellipse 50% 40% at 50% 100%, rgba(22, 163, 74, 0.08), transparent 50%);
  animation: mesh-shift 14s ease-in-out infinite;
}
.bg-orb {
  position: absolute;
  border-radius: 50%;
  filter: blur(60px);
  opacity: 0.55;
  animation: orb-float 18s ease-in-out infinite;
}
.bg-orb--a {
  width: 320px;
  height: 320px;
  top: -80px;
  right: 5%;
  background: rgba(47, 111, 237, 0.35);
}
.bg-orb--b {
  width: 260px;
  height: 260px;
  bottom: 10%;
  left: -40px;
  background: rgba(99, 102, 241, 0.28);
  animation-delay: -6s;
}
.bg-orb--c {
  width: 200px;
  height: 200px;
  top: 40%;
  right: 25%;
  background: rgba(245, 158, 11, 0.22);
  animation-delay: -12s;
}
.bg-grid {
  position: absolute;
  inset: 0;
  opacity: 0.35;
  background-image:
    linear-gradient(rgba(15, 23, 42, 0.04) 1px, transparent 1px),
    linear-gradient(90deg, rgba(15, 23, 42, 0.04) 1px, transparent 1px);
  background-size: 48px 48px;
  mask-image: radial-gradient(ellipse 90% 70% at 50% 30%, black 20%, transparent 75%);
}
.page-inner {
  position: relative;
  z-index: 1;
}
.glass-card {
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid rgba(255, 255, 255, 0.65);
  box-shadow: var(--lux-shadow-soft), inset 0 1px 0 rgba(255, 255, 255, 0.85);
  transition: box-shadow 0.35s ease, transform 0.35s ease;
}
.glass-card:hover {
  box-shadow: var(--lux-shadow-lift), inset 0 1px 0 rgba(255, 255, 255, 0.9);
}
.filter-card,
@@ -555,13 +947,69 @@
  border-radius: var(--lux-radius);
  border-color: var(--lux-border);
  background: var(--lux-card);
  box-shadow: var(--lux-shadow-soft);
}
.filter-card,
.panel-card,
.table-card {
  margin-bottom: 14px;
  margin-bottom: 16px;
}
.title-badge {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 14px;
  background: linear-gradient(145deg, rgba(47, 111, 237, 0.2), rgba(47, 111, 237, 0.06));
  box-shadow: 0 8px 24px rgba(47, 111, 237, 0.15);
}
.card-icon {
  font-size: 22px;
  color: var(--lux-primary);
}
.title-block {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.title-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
}
.shimmer-text {
  background: linear-gradient(
    90deg,
    var(--lux-text) 0%,
    var(--lux-text) 40%,
    rgba(47, 111, 237, 0.95) 50%,
    var(--lux-text) 60%,
    var(--lux-text) 100%
  );
  background-size: 200% auto;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: shimmer 5s linear infinite;
}
.live-pill {
  font-size: 11px;
  font-weight: 650;
  letter-spacing: 0.04em;
  padding: 4px 10px;
  border-radius: 999px;
  color: #0d47a1;
  background: linear-gradient(135deg, rgba(47, 111, 237, 0.18), rgba(47, 111, 237, 0.06));
  border: 1px solid rgba(47, 111, 237, 0.22);
  box-shadow: 0 2px 10px rgba(47, 111, 237, 0.12);
}
.filter-layout {
@@ -581,14 +1029,40 @@
  margin: 0;
}
.filter-form :deep(.el-input__wrapper),
.filter-form :deep(.el-select .el-input__wrapper) {
  border-radius: 10px;
  box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
  transition: box-shadow 0.2s ease;
}
.filter-form :deep(.el-input__wrapper:hover) {
  box-shadow: 0 4px 14px rgba(47, 111, 237, 0.1);
}
.filter-actions {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px 14px;
  padding-top: 10px;
  border-top: 1px dashed rgba(15, 23, 42, 0.1);
  padding-top: 12px;
  border-top: 1px dashed rgba(15, 23, 42, 0.12);
}
.filter-actions :deep(.lux-btn) {
  border-radius: 10px;
  font-weight: 600;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.filter-actions :deep(.lux-btn:hover) {
  transform: translateY(-1px);
  box-shadow: 0 8px 20px rgba(47, 111, 237, 0.18);
}
.filter-actions :deep(.el-button--success.is-plain) {
  border-color: rgba(22, 163, 74, 0.35);
}
.action-group {
@@ -613,22 +1087,59 @@
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  gap: 12px;
  flex-wrap: wrap;
}
.card-head-left {
  display: flex;
  align-items: center;
  gap: 8px;
  gap: 14px;
}
.card-icon {
.panel-head-main {
  display: flex;
  align-items: center;
  gap: 12px;
}
.panel-accent {
  width: 4px;
  height: 22px;
  border-radius: 4px;
  background: linear-gradient(180deg, #5b8cff, #2f6fed);
  box-shadow: 0 2px 8px rgba(47, 111, 237, 0.35);
}
.panel-accent--emerald {
  background: linear-gradient(180deg, #34d399, #059669);
  box-shadow: 0 2px 8px rgba(5, 150, 105, 0.35);
}
.chart-tag {
  margin-left: 10px;
  font-size: 11px;
  font-weight: 650;
  color: rgba(15, 23, 42, 0.55);
  padding: 2px 8px;
  border-radius: 6px;
  background: rgba(15, 23, 42, 0.05);
}
.count-chip {
  font-size: 12px;
  font-weight: 650;
  color: var(--lux-primary);
  padding: 6px 12px;
  border-radius: 999px;
  background: rgba(47, 111, 237, 0.1);
  border: 1px solid rgba(47, 111, 237, 0.15);
}
.card-title {
  font-weight: 760;
  color: var(--lux-text);
  letter-spacing: -0.02em;
}
.subtle {
@@ -636,44 +1147,115 @@
  font-size: 12px;
}
.kpi-card {
  padding: 2px;
}
.kpi-strip {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 12px;
  gap: 14px;
}
.kpi-item {
  padding: 12px 14px;
  border-radius: 12px;
  border: 1px solid rgba(15, 23, 42, 0.08);
  position: relative;
  padding: 16px 16px 14px;
  border-radius: 14px;
  border: 1px solid rgba(255, 255, 255, 0.7);
  overflow: hidden;
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s ease;
}
.kpi-item:hover {
  transform: translateY(-4px);
  box-shadow: 0 16px 36px rgba(15, 23, 42, 0.1);
}
.kpi-glow {
  position: absolute;
  right: -20%;
  bottom: -40%;
  width: 120px;
  height: 120px;
  border-radius: 50%;
  filter: blur(40px);
  opacity: 0.45;
  pointer-events: none;
}
.kpi-std .kpi-glow {
  background: rgba(47, 111, 237, 0.55);
}
.kpi-act .kpi-glow {
  background: rgba(245, 158, 11, 0.5);
}
.kpi-diff .kpi-glow {
  background: rgba(239, 68, 68, 0.45);
}
.kpi-rate .kpi-glow {
  background: rgba(22, 163, 74, 0.5);
}
.kpi-top {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}
.kpi-ico {
  font-size: 18px;
  opacity: 0.88;
}
.kpi-std {
  background: linear-gradient(135deg, rgba(47, 111, 237, 0.1), rgba(255, 255, 255, 0.86));
  background: linear-gradient(145deg, rgba(47, 111, 237, 0.14), rgba(255, 255, 255, 0.92));
}
.kpi-std .kpi-ico {
  color: #2563eb;
}
.kpi-act {
  background: linear-gradient(135deg, rgba(245, 158, 11, 0.1), rgba(255, 255, 255, 0.86));
  background: linear-gradient(145deg, rgba(245, 158, 11, 0.16), rgba(255, 255, 255, 0.92));
}
.kpi-act .kpi-ico {
  color: #d97706;
}
.kpi-diff {
  background: linear-gradient(135deg, rgba(239, 68, 68, 0.08), rgba(255, 255, 255, 0.86));
  background: linear-gradient(145deg, rgba(239, 68, 68, 0.12), rgba(255, 255, 255, 0.92));
}
.kpi-diff .kpi-ico {
  color: #dc2626;
}
.kpi-rate {
  background: linear-gradient(135deg, rgba(22, 163, 74, 0.1), rgba(255, 255, 255, 0.86));
  background: linear-gradient(145deg, rgba(22, 163, 74, 0.12), rgba(255, 255, 255, 0.92));
}
.kpi-rate .kpi-ico {
  color: #059669;
}
.kpi-label {
  font-size: 12px;
  color: var(--lux-subtle);
  font-weight: 600;
}
.kpi-value {
  margin-top: 6px;
  position: relative;
  z-index: 1;
  font-size: 22px;
  font-weight: 780;
  color: var(--lux-text);
  font-variant-numeric: tabular-nums;
}
.cost-value {
@@ -686,19 +1268,95 @@
  font-weight: 700;
}
.chart-section :deep(.el-card__header) {
  border-bottom: 1px solid rgba(15, 23, 42, 0.06);
}
.chart-wrap {
  border-radius: 12px;
  position: relative;
  padding-top: 40px;
  border-radius: 14px;
  overflow: hidden;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.5) 0%, rgba(248, 250, 252, 0.95) 100%);
  border: 1px solid rgba(15, 23, 42, 0.06);
}
.chart-content {
  height: 360px;
  height: 380px;
}
.chart-tools {
  display: flex;
  align-items: center;
  gap: 8px;
}
.chart-tools-inline {
  position: absolute;
  top: 6px;
  right: 8px;
  z-index: 2;
}
.chart-tool {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  font-weight: 650;
  line-height: 1;
  padding: 8px 12px;
  border-radius: 10px;
  border: 1px solid rgba(15, 23, 42, 0.1);
  background: rgba(255, 255, 255, 0.88);
  color: rgba(15, 23, 42, 0.75);
  cursor: pointer;
  transition:
    background-color 0.2s ease,
    border-color 0.2s ease,
    transform 0.2s ease,
    box-shadow 0.2s ease;
}
.chart-tool--primary {
  border-color: rgba(47, 111, 237, 0.28);
  background: linear-gradient(135deg, rgba(47, 111, 237, 0.12), rgba(255, 255, 255, 0.95));
  color: #1e40af;
}
.chart-tool:hover {
  background: rgba(47, 111, 237, 0.1);
  border-color: rgba(47, 111, 237, 0.28);
  transform: translateY(-2px);
  box-shadow: 0 6px 16px rgba(47, 111, 237, 0.12);
}
.large-chart-content {
  height: 70vh;
  min-height: 520px;
  width: 100%;
  overflow: hidden; // 防止 ECharts 画布溢出遮挡弹窗标题栏
}
.std-cost-page :deep(.el-dialog__header) {
  position: relative;
  z-index: 3;
}
.std-cost-page :deep(.el-dialog__headerbtn) {
  position: relative;
  z-index: 4;
}
.std-cost-page :deep(.el-dialog__body) {
  position: relative;
  z-index: 1;
}
.pagination-container {
  display: flex;
  justify-content: flex-end;
  padding-top: 12px;
  padding-top: 14px;
}
.w-260 {
@@ -712,14 +1370,21 @@
::deep(.lux-table) {
  border-radius: 12px;
  overflow: hidden;
  --el-table-border-color: rgba(15, 23, 42, 0.06);
}
::deep(.lux-table th.el-table__cell) {
  background: rgba(15, 23, 42, 0.03);
  background: linear-gradient(180deg, rgba(15, 23, 42, 0.04), rgba(15, 23, 42, 0.02));
  font-weight: 700;
  color: rgba(15, 23, 42, 0.75);
}
::deep(.lux-table .el-table__row) {
  transition: background-color 0.2s ease;
}
::deep(.lux-table .el-table__row:hover > td.el-table__cell) {
  background-color: rgba(47, 111, 237, 0.06) !important;
  background-color: rgba(47, 111, 237, 0.07) !important;
}
@media (max-width: 1100px) {
@@ -731,5 +1396,32 @@
    justify-content: flex-start;
    padding-top: 8px;
  }
  .shimmer-text {
    animation: none;
    color: var(--lux-text);
    background: none;
    -webkit-background-clip: unset;
    background-clip: unset;
  }
}
@media (prefers-reduced-motion: reduce) {
  .bg-mesh,
  .bg-orb,
  .shimmer-text {
    animation: none;
  }
  .shimmer-text {
    color: var(--lux-text);
    background: none;
    -webkit-background-clip: unset;
    background-clip: unset;
  }
  .kpi-item:hover {
    transform: none;
  }
}
</style>