已添加1个文件
已修改10个文件
已删除1个文件
3162 ■■■■■ 文件已修改
multiple/config.json 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/costAccounting/productionCost.js 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/logo/logo.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/logo/宁夏中盛建材科技有限公司.png 补丁 | 查看 | 原始文档 | blame | 历史
src/views/costAccounting/productionCostAccounting/index.vue 1722 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/index.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionReporting/detailDialog.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionReporting/reportingDialog.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionPlan/productionPlan/index.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionPlan/trackProgress/index.vue 789 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/salesStatistics/index.vue 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/unitEnergyConsumption/index.vue 569 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
multiple/config.json
@@ -18,6 +18,6 @@
    "favicon": "favicon/ZSJCLogo.ico"
  },
  "screen": "/src/assets/images/login-background.png",
  "logo": "/src/assets/logo/宁夏中盛建材科技有限公司.png",
  "logo": "/src/assets/logo/logo.png",
  "favicon": "/public/favicon.ico"
}
src/api/costAccounting/productionCost.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
import request from '@/utils/request';
// ç”Ÿäº§æˆæœ¬æ ¸ç®—相关API
export function getProductionCostSummary(params) {
  return request({
    url: '/cost/productionCost/summary',
    method: 'get',
    params
  });
}
// æŒ‰äº§å“ç‰©æ–™æ±‡æ€»
export function getProductionCostAggregateByProduct(params) {
  return request({
    url: '/cost/productionCost/aggregate/product',
    method: 'get',
    params
  });
}
// ç”Ÿäº§è®¢å•Top10
export function getProductionCostTopOrders(params) {
  return request({
    url: '/cost/productionCost/top/order',
    method: 'get',
    params
  });
}
// æŒ‰è®¢å•汇总
export function getProductionCostAggregateByOrder(params) {
  return request({
    url: '/cost/productionCost/aggregate/order',
    method: 'get',
    params
  });
}
src/assets/logo/logo.png

src/assets/logo/ÄþÏÄÖÐÊ¢½¨²Ä¿Æ¼¼ÓÐÏÞ¹«Ë¾.png
Binary files differ
src/views/costAccounting/productionCostAccounting/index.vue
@@ -1,14 +1,17 @@
<template>
  <div class="production-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">
            <el-icon class="card-icon ui-icon"><DataLine /></el-icon>
            <el-icon class="card-icon ui-icon">
              <DataLine />
            </el-icon>
            <span class="card-title">生产成本核算</span>
            <span class="subtle">成本 = Î£ æŠ•入量 Ã— å¯¹åº”单价</span>
          </div>
          <div class="card-head-right">
          <!-- <div class="card-head-right">
            <el-radio-group
              v-model="statisticsType"
              size="small"
@@ -17,84 +20,79 @@
              <el-radio-button label="day">按日</el-radio-button>
              <el-radio-button label="month">按月</el-radio-button>
            </el-radio-group>
          </div>
          </div> -->
        </div>
      </template>
      <div class="filter-layout">
        <el-form :model="searchForm" :inline="true" class="filter-form">
        <el-form :model="searchForm"
                 :inline="true"
                 class="filter-form">
          <el-form-item label="时间范围">
            <el-date-picker
              v-if="statisticsType === 'day'"
              v-model="searchForm.dateRange"
              type="daterange"
              range-separator="至"
              start-placeholder="开始日期"
              end-placeholder="结束日期"
              value-format="YYYY-MM-DD"
              class="w-260"
              @change="handleQuery"
            />
            <el-date-picker
              v-else
              v-model="searchForm.monthRange"
              type="monthrange"
              range-separator="至"
              start-placeholder="开始月份"
              end-placeholder="结束月份"
              value-format="YYYY-MM"
              class="w-260"
              @change="handleQuery"
            />
            <el-date-picker v-if="statisticsType === 'day'"
                            v-model="searchForm.dateRange"
                            type="daterange"
                            range-separator="至"
                            start-placeholder="开始日期"
                            end-placeholder="结束日期"
                            value-format="YYYY-MM-DD"
                            class="w-260"
                            @change="handleQuery" />
            <el-date-picker v-else
                            v-model="searchForm.monthRange"
                            type="monthrange"
                            range-separator="至"
                            start-placeholder="开始月份"
                            end-placeholder="结束月份"
                            value-format="YYYY-MM"
                            class="w-260"
                            @change="handleQuery" />
          </el-form-item>
          <el-form-item label="产品类别">
            <el-select
              v-model="searchForm.category"
              clearable
              filterable
              placeholder="全部类别"
              class="w-180"
              @change="handleQuery"
            >
              <el-option
                v-for="item in categoryOptions"
                :key="item"
                :label="item"
                :value="item"
              />
            <el-select v-model="searchForm.dictCode"
                       clearable
                       filterable
                       placeholder="全部类别"
                       class="w-180"
                       @change="handleQuery">
              <el-option v-for="item in categoryOptions"
                         :key="item.dictCode"
                         :label="item.dictLabel"
                         :value="item.dictCode" />
            </el-select>
          </el-form-item>
          <el-form-item label="生产订单">
            <el-select
              v-model="searchForm.orderNo"
              clearable
              filterable
              placeholder="全部订单"
              class="w-180"
              @change="handleQuery"
            >
              <el-option
                v-for="item in orderOptions"
                :key="item"
                :label="item"
                :value="item"
              />
            <el-select v-model="searchForm.productOrderId"
                       clearable
                       filterable
                       placeholder="全部订单"
                       class="w-180"
                       @change="handleQuery">
              <el-option v-for="order in orderList"
                         :key="order.id"
                         :label="`${order.npsNo}`"
                         :value="order.id" />
            </el-select>
          </el-form-item>
        </el-form>
        <div class="filter-actions">
          <el-button class="lux-btn" type="primary" @click="handleQuery">
          <el-button class="lux-btn"
                     type="primary"
                     @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 class="lux-btn"
                     @click="handleReset">重置</el-button>
          <el-button class="lux-btn"
                     type="success"
                     plain
                     @click="handleExport">
            å¯¼å‡º
          </el-button>
        </div>
      </div>
    </el-card>
    <el-card class="panel-card" shadow="never">
    <el-card class="panel-card"
             shadow="never">
      <div class="kpi-strip">
        <div class="kpi-item kpi-total">
          <div class="kpi-label">总生产成本</div>
@@ -110,26 +108,37 @@
        </div>
      </div>
    </el-card>
    <el-row :gutter="14" class="summary-row">
    <el-row :gutter="14"
            class="summary-row">
      <el-col :span="12">
        <el-card class="table-card" shadow="never">
        <el-card class="table-card"
                 shadow="never">
          <template #header>
            <div class="panel-head">
              <span class="card-title">按产品类别汇总</span>
            </div>
          </template>
          <el-table :data="categorySummary" stripe class="lux-table" height="260">
            <el-table-column prop="category" label="产品类别" min-width="140" />
            <el-table-column prop="totalQuantity" label="用量" align="right" min-width="120">
          <el-table :data="categorySummary"
                    stripe
                    class="lux-table"
                    height="260">
            <el-table-column prop="name"
                             label="产品类别"
                             min-width="140" />
            <el-table-column prop="quantity"
                             label="用量"
                             align="right"
                             min-width="120">
              <template #default="scope">
                <span class="quantity-cell">
                  <span class="quantity-value">{{ formatNumber(scope.row.totalQuantity, 2) }}</span>
                  <span class="quantity-value">{{ formatNumber(scope.row.quantity, 2) }}</span>
                  <span class="quantity-unit">{{ scope.row.unit || "-" }}</span>
                </span>
              </template>
            </el-table-column>
            <el-table-column prop="totalCost" label="成本(元)" align="right">
            <el-table-column prop="totalCost"
                             label="成本(元)"
                             align="right">
              <template #default="scope">
                <span class="cost-value">Â¥{{ formatMoney(scope.row.totalCost) }}</span>
              </template>
@@ -138,24 +147,37 @@
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card class="table-card" shadow="never">
        <el-card class="table-card"
                 shadow="never">
          <template #header>
            <div class="panel-head">
              <span class="card-title">按生产订单汇总</span>
            </div>
          </template>
          <el-table :data="orderSummary" stripe class="lux-table" height="260">
            <el-table-column prop="orderNo" label="生产订单" min-width="150" />
            <el-table-column prop="category" label="产品类别" min-width="120" />
            <el-table-column prop="totalQuantity" label="用量" align="right" min-width="120">
          <el-table :data="orderSummary"
                    stripe
                    class="lux-table"
                    height="260">
            <el-table-column prop="name"
                             label="生产订单"
                             min-width="150" />
            <el-table-column prop="strength"
                             label="产品类别"
                             min-width="120" />
            <el-table-column prop="quantity"
                             label="用量"
                             align="right"
                             min-width="120">
              <template #default="scope">
                <span class="quantity-cell">
                  <span class="quantity-value">{{ formatNumber(scope.row.totalQuantity, 2) }}</span>
                  <span class="quantity-value">{{ formatNumber(scope.row.quantity, 2) }}</span>
                  <span class="quantity-unit">{{ scope.row.unit || "-" }}</span>
                </span>
              </template>
            </el-table-column>
            <el-table-column prop="totalCost" label="总成本(元)" align="right">
            <el-table-column prop="totalCost"
                             label="总成本(元)"
                             align="right">
              <template #default="scope">
                <span class="cost-value">Â¥{{ formatMoney(scope.row.totalCost) }}</span>
              </template>
@@ -164,61 +186,41 @@
        </el-card>
      </el-col>
    </el-row>
    <el-card class="table-card" shadow="never">
      <template #header>
        <div class="panel-head">
          <span class="card-title">多维度汇总明细</span>
          <span class="subtle">{{ timeColumnLabel }} + äº§å“ç±»åˆ« + ç”Ÿäº§è®¢å•</span>
        </div>
      </template>
      <el-table :data="pagedTableData" stripe class="lux-table">
        <el-table-column prop="timeLabel" :label="timeColumnLabel" min-width="110" />
        <el-table-column prop="category" label="产品类别" min-width="120" />
        <el-table-column prop="orderNo" label="生产订单" min-width="150" />
        <el-table-column prop="totalQuantity" label="用量" align="right" min-width="130">
          <template #default="scope">
            <span class="quantity-cell">
              <span class="quantity-value">{{ formatNumber(scope.row.totalQuantity, 2) }}</span>
              <span class="quantity-unit">{{ scope.row.unit || "-" }}</span>
            </span>
    <el-row :gutter="14"
            class="summary-row">
      <el-col :span="12">
        <el-card class="table-card"
                 shadow="never">
          <template #header>
            <div class="panel-head">
              <span class="card-title">产品物料Top10</span>
            </div>
          </template>
        </el-table-column>
        <el-table-column prop="totalCost" label="成本(元)" align="right">
          <template #default="scope">
            <span class="cost-value">Â¥{{ formatMoney(scope.row.totalCost) }}</span>
          <div ref="topOrdersChartRef"
               class="chart-container"
               style="height: 300px;"></div>
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card class="table-card"
                 shadow="never">
          <template #header>
            <div class="panel-head">
              <span class="card-title">生产订单Top10</span>
            </div>
          </template>
        </el-table-column>
        <el-table-column label="拆分明细" width="92" fixed="right">
          <template #default="scope">
            <el-button link type="primary" @click="openDetail(scope.row)">查看</el-button>
          </template>
        </el-table-column>
      </el-table>
      <div class="pagination-container">
        <el-pagination
          v-model:current-page="page.current"
          v-model:page-size="page.size"
          :page-sizes="[10, 20, 50, 100]"
          :total="tableData.length"
          layout="total, sizes, prev, pager, next, jumper"
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
        />
      </div>
    </el-card>
    <el-drawer
      v-model="detailVisible"
      :with-header="false"
      class="detail-drawer"
      size="760px"
      :close-on-click-modal="true"
      :close-on-press-escape="true"
      destroy-on-close
    >
      <div v-if="detailRow" class="drawer-head">
        </el-card>
      </el-col>
    </el-row>
    <el-drawer v-model="detailVisible"
               :with-header="false"
               class="detail-drawer"
               size="760px"
               :close-on-click-modal="true"
               :close-on-press-escape="true"
               destroy-on-close>
      <div v-if="detailRow"
           class="drawer-head">
        <div class="meta-item">
          <span class="meta-label">{{ timeColumnLabel }}</span>
          <span class="meta-value">{{ detailRow.timeLabel }}</span>
@@ -232,9 +234,16 @@
          <span class="meta-value">{{ detailRow.orderNo }}</span>
        </div>
      </div>
      <el-table :data="detailMaterials" class="lux-table" stripe>
        <el-table-column prop="materialName" label="物料名称" min-width="120" />
        <el-table-column prop="quantity" label="投入量" align="right" min-width="140">
      <el-table :data="detailMaterials"
                class="lux-table"
                stripe>
        <el-table-column prop="materialName"
                         label="物料名称"
                         min-width="120" />
        <el-table-column prop="quantity"
                         label="投入量"
                         align="right"
                         min-width="140">
          <template #default="scope">
            <span class="quantity-cell">
              <span class="quantity-value">{{ formatNumber(scope.row.quantity, 2) }}</span>
@@ -242,12 +251,17 @@
            </span>
          </template>
        </el-table-column>
        <el-table-column prop="unitPrice" label="单价(元)" align="right">
        <el-table-column prop="unitPrice"
                         label="单价(元)"
                         align="right">
          <template #default="scope">
            {{ formatNumber(scope.row.unitPrice, 2) }}
          </template>
        </el-table-column>
        <el-table-column prop="cost" label="成本(元)" align="right" min-width="132">
        <el-table-column prop="cost"
                         label="成本(元)"
                         align="right"
                         min-width="132">
          <template #default="scope">
            <span class="cost-value no-wrap-money">Â¥{{ formatMoney(scope.row.cost) }}</span>
          </template>
@@ -264,608 +278,920 @@
</template>
<script setup>
import { computed, reactive, ref, watch } from "vue";
import { DataLine } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
  import { computed, reactive, ref, watch, onMounted, nextTick } from "vue";
  import { DataLine } from "@element-plus/icons-vue";
  import { ElMessage } from "element-plus";
  import * as echarts from "echarts";
  import { getDicts } from "@/api/system/dict/data.js";
  import { productOrderListPage } from "@/api/productionManagement/productionOrder.js";
  import {
    getProductionCostSummary,
    getProductionCostAggregateByProduct,
    getProductionCostAggregateByOrder,
    getProductionCostTopOrders,
  } from "@/api/costAccounting/productionCost.js";
const statisticsType = ref("day");
  const statisticsType = ref("day");
const getDefaultDateRange = () => {
  const end = new Date();
  const start = new Date();
  start.setDate(start.getDate() - 6);
  return [start.toISOString().slice(0, 10), end.toISOString().slice(0, 10)];
};
const getDefaultMonthRange = () => {
  const end = new Date();
  const start = new Date();
  start.setMonth(start.getMonth() - 2);
  return [start.toISOString().slice(0, 7), end.toISOString().slice(0, 7)];
};
const searchForm = reactive({
  dateRange: getDefaultDateRange(),
  monthRange: getDefaultMonthRange(),
  category: "",
  orderNo: "",
});
const sourceRecords = ref([
  { date: "2026-03-17", category: "瓷砖", orderNo: "PO-260317-01", materialName: "陶瓷粉", materialType: "原料", quantity: 1200, unit: "kg", unitPrice: 2.8 },
  { date: "2026-03-17", category: "瓷砖", orderNo: "PO-260317-01", materialName: "釉料", materialType: "辅料", quantity: 180, unit: "kg", unitPrice: 8.6 },
  { date: "2026-03-17", category: "æ°´æ³¥", orderNo: "PO-260317-02", materialName: "熟料", materialType: "原料", quantity: 2200, unit: "kg", unitPrice: 1.36 },
  { date: "2026-03-17", category: "æ°´æ³¥", orderNo: "PO-260317-02", materialName: "石膏", materialType: "辅料", quantity: 260, unit: "kg", unitPrice: 0.92 },
  { date: "2026-03-18", category: "砂浆", orderNo: "PO-260318-01", materialName: "机制砂", materialType: "原料", quantity: 1600, unit: "kg", unitPrice: 0.58 },
  { date: "2026-03-18", category: "砂浆", orderNo: "PO-260318-01", materialName: "保水剂", materialType: "辅料", quantity: 65, unit: "kg", unitPrice: 11.4 },
  { date: "2026-03-19", category: "瓷砖", orderNo: "PO-260319-01", materialName: "陶瓷粉", materialType: "原料", quantity: 980, unit: "kg", unitPrice: 2.9 },
  { date: "2026-03-19", category: "瓷砖", orderNo: "PO-260319-01", materialName: "色料", materialType: "辅料", quantity: 42, unit: "kg", unitPrice: 15.8 },
  { date: "2026-03-19", category: "砂浆", orderNo: "PO-260319-03", materialName: "机制砂", materialType: "原料", quantity: 1400, unit: "kg", unitPrice: 0.56 },
  { date: "2026-03-19", category: "砂浆", orderNo: "PO-260319-03", materialName: "减水剂", materialType: "辅料", quantity: 74, unit: "kg", unitPrice: 7.2 },
  { date: "2026-03-20", category: "æ°´æ³¥", orderNo: "PO-260320-02", materialName: "熟料", materialType: "原料", quantity: 2400, unit: "kg", unitPrice: 1.33 },
  { date: "2026-03-20", category: "æ°´æ³¥", orderNo: "PO-260320-02", materialName: "矿粉", materialType: "辅料", quantity: 380, unit: "kg", unitPrice: 1.08 },
]);
const normalizedRecords = computed(() =>
  sourceRecords.value.map((item) => {
    const month = item.date.slice(0, 7);
    const cost = Number(item.quantity) * Number(item.unitPrice);
    return { ...item, month, cost };
  })
);
const categoryOptions = computed(() =>
  Array.from(new Set(normalizedRecords.value.map((item) => item.category)))
);
const orderOptions = computed(() =>
  Array.from(new Set(normalizedRecords.value.map((item) => item.orderNo)))
);
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 getMonthRangeDays = (monthRange) => {
  if (!Array.isArray(monthRange) || monthRange.length !== 2 || !monthRange[0] || !monthRange[1]) {
    return 0;
  }
  const [startMonth, endMonth] = monthRange;
  const startDate = new Date(`${startMonth}-01T00:00:00`);
  const endDate = new Date(`${endMonth}-01T00:00:00`);
  if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime()) || startDate > endDate) {
    return 0;
  }
  const endMonthLastDay = new Date(endDate.getFullYear(), endDate.getMonth() + 1, 0);
  const diffMs = endMonthLastDay.getTime() - startDate.getTime();
  return Math.floor(diffMs / (24 * 60 * 60 * 1000)) + 1;
};
const buildQueryParams = () => {
  const isDay = statisticsType.value === "day";
  const params = {
    statisticsType: statisticsType.value,
    category: searchForm.category || undefined,
    orderNo: searchForm.orderNo || undefined,
  const getDefaultDateRange = () => {
    const end = new Date();
    const start = new Date();
    start.setDate(start.getDate() - 6);
    return [start.toISOString().slice(0, 10), end.toISOString().slice(0, 10)];
  };
  if (isDay) {
    const [startDate, endDate] = searchForm.dateRange || [];
    params.startDate = startDate;
    params.endDate = endDate;
  } else {
    const [startMonth, endMonth] = searchForm.monthRange || [];
    params.startMonth = startMonth;
    params.endMonth = endMonth;
    params.days = getMonthRangeDays(searchForm.monthRange);
  }
  const getDefaultMonthRange = () => {
    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 params;
};
  const searchForm = reactive({
    dateRange: getDefaultDateRange(),
    monthRange: getDefaultMonthRange(),
    dictCode: "",
    productOrderId: "",
  });
const filteredRecords = computed(() =>
  normalizedRecords.value.filter((item) => {
    const hitTime =
      statisticsType.value === "day"
        ? inRange(item.date, searchForm.dateRange)
        : inRange(item.month, searchForm.monthRange);
    const hitCategory = !searchForm.category || item.category === searchForm.category;
    const hitOrder = !searchForm.orderNo || item.orderNo === searchForm.orderNo;
    return hitTime && hitCategory && hitOrder;
  })
);
  const sourceRecords = ref([
    {
      date: "2026-03-17",
      category: "瓷砖",
      orderNo: "PO-260317-01",
      materialName: "陶瓷粉",
      materialType: "原料",
      quantity: 1200,
      unit: "kg",
      unitPrice: 2.8,
    },
    {
      date: "2026-03-17",
      category: "瓷砖",
      orderNo: "PO-260317-01",
      materialName: "釉料",
      materialType: "辅料",
      quantity: 180,
      unit: "kg",
      unitPrice: 8.6,
    },
    {
      date: "2026-03-17",
      category: "æ°´æ³¥",
      orderNo: "PO-260317-02",
      materialName: "熟料",
      materialType: "原料",
      quantity: 2200,
      unit: "kg",
      unitPrice: 1.36,
    },
    {
      date: "2026-03-17",
      category: "æ°´æ³¥",
      orderNo: "PO-260317-02",
      materialName: "石膏",
      materialType: "辅料",
      quantity: 260,
      unit: "kg",
      unitPrice: 0.92,
    },
    {
      date: "2026-03-18",
      category: "砂浆",
      orderNo: "PO-260318-01",
      materialName: "机制砂",
      materialType: "原料",
      quantity: 1600,
      unit: "kg",
      unitPrice: 0.58,
    },
    {
      date: "2026-03-18",
      category: "砂浆",
      orderNo: "PO-260318-01",
      materialName: "保水剂",
      materialType: "辅料",
      quantity: 65,
      unit: "kg",
      unitPrice: 11.4,
    },
    {
      date: "2026-03-19",
      category: "瓷砖",
      orderNo: "PO-260319-01",
      materialName: "陶瓷粉",
      materialType: "原料",
      quantity: 980,
      unit: "kg",
      unitPrice: 2.9,
    },
    {
      date: "2026-03-19",
      category: "瓷砖",
      orderNo: "PO-260319-01",
      materialName: "色料",
      materialType: "辅料",
      quantity: 42,
      unit: "kg",
      unitPrice: 15.8,
    },
    {
      date: "2026-03-19",
      category: "砂浆",
      orderNo: "PO-260319-03",
      materialName: "机制砂",
      materialType: "原料",
      quantity: 1400,
      unit: "kg",
      unitPrice: 0.56,
    },
    {
      date: "2026-03-19",
      category: "砂浆",
      orderNo: "PO-260319-03",
      materialName: "减水剂",
      materialType: "辅料",
      quantity: 74,
      unit: "kg",
      unitPrice: 7.2,
    },
    {
      date: "2026-03-20",
      category: "æ°´æ³¥",
      orderNo: "PO-260320-02",
      materialName: "熟料",
      materialType: "原料",
      quantity: 2400,
      unit: "kg",
      unitPrice: 1.33,
    },
    {
      date: "2026-03-20",
      category: "æ°´æ³¥",
      orderNo: "PO-260320-02",
      materialName: "矿粉",
      materialType: "辅料",
      quantity: 380,
      unit: "kg",
      unitPrice: 1.08,
    },
  ]);
  const orderList = ref([]);
const timeColumnLabel = computed(() => (statisticsType.value === "day" ? "日期" : "月份"));
  // åŠ è½½ç”Ÿäº§è®¢å•åˆ—è¡¨
  const loadOrders = () => {
    productOrderListPage({ pageNum: -1, pageSize: -1 })
      .then(res => {
        orderList.value = res.data.records || [];
      })
      .finally(() => {});
  };
  const normalizedRecords = computed(() =>
    sourceRecords.value.map(item => {
      const month = item.date.slice(0, 7);
      const cost = Number(item.quantity) * Number(item.unitPrice);
      return { ...item, month, cost };
    })
  );
const aggregateBy = (list, keyFn) => {
  const map = new Map();
  for (const item of list) {
    const key = keyFn(item);
    if (!map.has(key)) {
      map.set(key, {
        totalCost: 0,
        totalQuantity: 0,
        materials: [],
  const categoryOptions = ref([]);
  // èŽ·å–äº§å“ç±»åž‹å­—å…¸
  const getProductTypeOptions = () => {
    getDicts("product_type")
      .then(res => {
        if (res.code === 200) {
          categoryOptions.value = res.data;
        }
      })
      .catch(err => {
        console.error("获取产品类型字典失败:", err);
      });
  };
  const orderOptions = computed(() =>
    Array.from(new Set(normalizedRecords.value.map(item => item.orderNo)))
  );
  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 getMonthRangeDays = monthRange => {
    if (
      !Array.isArray(monthRange) ||
      monthRange.length !== 2 ||
      !monthRange[0] ||
      !monthRange[1]
    ) {
      return 0;
    }
    const [startMonth, endMonth] = monthRange;
    const startDate = new Date(`${startMonth}-01T00:00:00`);
    const endDate = new Date(`${endMonth}-01T00:00:00`);
    if (
      Number.isNaN(startDate.getTime()) ||
      Number.isNaN(endDate.getTime()) ||
      startDate > endDate
    ) {
      return 0;
    }
    const endMonthLastDay = new Date(
      endDate.getFullYear(),
      endDate.getMonth() + 1,
      0
    );
    const diffMs = endMonthLastDay.getTime() - startDate.getTime();
    return Math.floor(diffMs / (24 * 60 * 60 * 1000)) + 1;
  };
  const buildQueryParams = () => {
    const isDay = statisticsType.value === "day";
    const params = {
      statisticsType: statisticsType.value,
      dictCode: searchForm.dictCode || undefined,
      productOrderId: searchForm.productOrderId || undefined,
    };
    if (isDay) {
      const [startDate, endDate] = searchForm.dateRange || [];
      params.startDate = startDate;
      params.endDate = endDate;
    } else {
      const [startMonth, endMonth] = searchForm.monthRange || [];
      params.startMonth = startMonth;
      params.endMonth = endMonth;
      params.days = getMonthRangeDays(searchForm.monthRange);
    }
    return params;
  };
  const filteredRecords = computed(() =>
    normalizedRecords.value.filter(item => {
      const hitTime =
        statisticsType.value === "day"
          ? inRange(item.date, searchForm.dateRange)
          : inRange(item.month, searchForm.monthRange);
      const hitCategory =
        !searchForm.dictCode || item.dictCode === searchForm.dictCode;
      const hitOrder =
        !searchForm.productOrderId ||
        item.productOrderId === searchForm.productOrderId;
      return hitTime && hitCategory && hitOrder;
    })
  );
  const timeColumnLabel = computed(() =>
    statisticsType.value === "day" ? "日期" : "月份"
  );
  const aggregateBy = (list, keyFn) => {
    const map = new Map();
    for (const item of list) {
      const key = keyFn(item);
      if (!map.has(key)) {
        map.set(key, {
          totalCost: 0,
          totalQuantity: 0,
          materials: [],
        });
      }
      const bucket = map.get(key);
      bucket.totalCost += item.cost;
      bucket.totalQuantity += Number(item.quantity) || 0;
      bucket.materials.push(item);
    }
    return map;
  };
  const groupedMap = computed(() =>
    aggregateBy(filteredRecords.value, item => {
      const timeKey = statisticsType.value === "day" ? item.date : item.month;
      return `${timeKey}__${item.category}__${item.orderNo}`;
    })
  );
  const tableData = computed(() => {
    const rows = [];
    for (const [key, val] of groupedMap.value) {
      const [timeLabel, category, orderNo] = key.split("__");
      rows.push({
        key,
        timeLabel,
        category,
        orderNo,
        totalQuantity: val.totalQuantity,
        unit: val.materials[0]?.unit || "",
        totalCost: val.totalCost,
        materials: val.materials,
      });
    }
    const bucket = map.get(key);
    bucket.totalCost += item.cost;
    bucket.totalQuantity += Number(item.quantity) || 0;
    bucket.materials.push(item);
  }
  return map;
};
    return rows.sort((a, b) => (a.timeLabel > b.timeLabel ? -1 : 1));
  });
const groupedMap = computed(() =>
  aggregateBy(filteredRecords.value, (item) => {
    const timeKey = statisticsType.value === "day" ? item.date : item.month;
    return `${timeKey}__${item.category}__${item.orderNo}`;
  })
);
  const page = reactive({
    current: 1,
    size: 10,
  });
const tableData = computed(() => {
  const rows = [];
  for (const [key, val] of groupedMap.value) {
    const [timeLabel, category, orderNo] = key.split("__");
    rows.push({
      key,
      timeLabel,
      category,
      orderNo,
      totalQuantity: val.totalQuantity,
      unit: val.materials[0]?.unit || "",
      totalCost: val.totalCost,
      materials: val.materials,
    });
  }
  return rows.sort((a, b) => (a.timeLabel > b.timeLabel ? -1 : 1));
});
  const pagedTableData = computed(() => {
    const start = (page.current - 1) * page.size;
    return tableData.value.slice(start, start + page.size);
  });
const page = reactive({
  current: 1,
  size: 10,
});
  const categorySummary = ref([]);
  const orderSummary = ref([]);
  const topOrders = ref([]);
const pagedTableData = computed(() => {
  const start = (page.current - 1) * page.size;
  return tableData.value.slice(start, start + page.size);
});
  const overview = ref({
    totalCost: 0,
    orderCount: 0,
    avgCostPerOrder: 0,
  });
const categorySummary = computed(() => {
  const map = aggregateBy(filteredRecords.value, (item) => item.category);
  const rows = [];
  for (const [category, val] of map) {
    rows.push({
      category,
      totalQuantity: val.totalQuantity,
      unit: val.materials[0]?.unit || "",
      totalCost: val.totalCost,
    });
  }
  return rows.sort((a, b) => b.totalCost - a.totalCost);
});
  // å›¾è¡¨ç›¸å…³
  const topOrdersChartRef = ref(null);
  let topOrdersChartInstance = null;
const orderSummary = computed(() => {
  const map = aggregateBy(filteredRecords.value, (item) => item.orderNo);
  const rows = [];
  for (const [orderNo, val] of map) {
    rows.push({
      orderNo,
      category: val.materials[0]?.category || "-",
      totalQuantity: val.totalQuantity,
      unit: val.materials[0]?.unit || "",
      totalCost: val.totalCost,
    });
  }
  return rows.sort((a, b) => b.totalCost - a.totalCost);
});
  const detailVisible = ref(false);
  const detailRow = ref(null);
const overview = computed(() => {
  const orderCount = new Set(filteredRecords.value.map((item) => item.orderNo)).size;
  const totalCost = filteredRecords.value.reduce((sum, item) => sum + item.cost, 0);
  return {
    totalCost,
    orderCount,
    avgCostPerOrder: orderCount === 0 ? 0 : totalCost / orderCount,
  };
});
  const detailMaterials = computed(() => detailRow.value?.materials || []);
const detailVisible = ref(false);
const detailRow = ref(null);
const detailMaterials = computed(() => detailRow.value?.materials || []);
const detailTotalCost = computed(() =>
  detailMaterials.value.reduce((sum, item) => sum + item.cost, 0)
);
const openDetail = (row) => {
  detailRow.value = row;
  detailVisible.value = true;
};
const handleTypeChange = () => {
  handleQuery();
};
const handleQuery = () => {
  page.current = 1;
  const queryParams = buildQueryParams();
  console.log("[productionCostAccounting] query params:", queryParams);
  ElMessage.success("已按条件完成汇总");
};
const handleReset = () => {
  searchForm.dateRange = getDefaultDateRange();
  searchForm.monthRange = getDefaultMonthRange();
  searchForm.category = "";
  searchForm.orderNo = "";
  handleQuery();
};
const handleSizeChange = (val) => {
  page.size = val;
  page.current = 1;
};
const handleCurrentChange = (val) => {
  page.current = val;
};
const handleExport = () => {
  const headers = [timeColumnLabel.value, "产品类别", "生产订单", "用量", "单位", "成本(元)"];
  const lines = tableData.value.map((row) =>
    [
      row.timeLabel,
      row.category,
      row.orderNo,
      row.totalQuantity.toFixed(2),
      row.unit || "",
      row.totalCost.toFixed(2),
    ].join(",")
  const detailTotalCost = computed(() =>
    detailMaterials.value.reduce((sum, item) => sum + item.cost, 0)
  );
  const csv = [headers.join(","), ...lines].join("\n");
  const blob = new Blob(["\uFEFF" + csv], { type: "text/csv;charset=utf-8;" });
  const url = URL.createObjectURL(blob);
  const link = document.createElement("a");
  link.href = url;
  link.download = `生产成本汇总_${statisticsType.value}_${Date.now()}.csv`;
  link.click();
  URL.revokeObjectURL(url);
  ElMessage.success("导出成功");
};
const formatMoney = (v) => {
  const n = Number.parseFloat(v);
  const value = Number.isFinite(n) ? n : 0;
  return value.toLocaleString("zh-CN", {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
  const openDetail = row => {
    detailRow.value = row;
    detailVisible.value = true;
  };
  // åˆå§‹åŒ–生产订单Top10图表
  const initTopOrdersChart = () => {
    nextTick(() => {
      if (topOrdersChartRef.value) {
        topOrdersChartInstance = echarts.init(topOrdersChartRef.value);
        updateTopOrdersChart();
      }
    });
  };
  // æ›´æ–°ç”Ÿäº§è®¢å•Top10图表
  const updateTopOrdersChart = () => {
    if (!topOrdersChartInstance) return;
    const data = topOrders.value;
    const xAxisData = data.map(item => item.name);
    const seriesData = data.map(item => item.totalCost);
    const option = {
      tooltip: {
        trigger: "axis",
        axisPointer: {
          type: "shadow",
        },
        backgroundColor: "rgba(255, 255, 255, 0.95)",
        borderColor: "#409EFF",
        borderWidth: 1,
        textStyle: { color: "#303133" },
      },
      grid: {
        left: "3%",
        right: "4%",
        bottom: "15%",
        top: "3%",
        containLabel: true,
      },
      xAxis: {
        type: "category",
        data: xAxisData,
        axisLabel: {
          color: "#606266",
          rotate: 45,
        },
        axisLine: { lineStyle: { color: "#ebeef5" } },
        splitLine: { show: false },
      },
      yAxis: {
        type: "value",
        name: "成本(元)",
        nameTextStyle: { color: "#606266" },
        axisLabel: { color: "#606266" },
        axisLine: { show: false },
        splitLine: { lineStyle: { color: "#f0f2f5" } },
      },
      series: [
        {
          name: "成本",
          type: "bar",
          data: seriesData,
          itemStyle: {
            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
              { offset: 0, color: "#409EFF" },
              { offset: 1, color: "#66B1FF" },
            ]),
          },
          barWidth: "60%",
        },
      ],
    };
    topOrdersChartInstance.setOption(option);
  };
  // çª—口大小变化时重新渲染图表
  const handleResize = () => {
    topOrdersChartInstance && topOrdersChartInstance.resize();
  };
  const handleTypeChange = () => {
    handleQuery();
  };
  const handleQuery = () => {
    page.current = 1;
    // æž„建API请求参数
    const apiParams = {
      startDate: searchForm.dateRange?.[0],
      endDate: searchForm.dateRange?.[1],
      dictCode: searchForm.dictCode,
      productOrderId: searchForm.productOrderId,
      current: -1,
      size: -1,
    };
    // è°ƒç”¨API获取概览数据
    getProductionCostSummary(apiParams)
      .then(res => {
        if (res.code === 200) {
          const data = res.data;
          overview.value = {
            totalCost: parseFloat(data.totalCost) || 0,
            orderCount: data.orderCount || 0,
            avgCostPerOrder: parseFloat(data.averageOrderCost) || 0,
          };
        } else {
          ElMessage.error(res.message || "获取概览数据失败");
        }
      })
      .catch(err => {
        console.error("获取生产成本汇总数据失败:", err);
        ElMessage.error("系统异常,获取概览数据失败");
      });
    getProductionCostAggregateByOrder;
    // è°ƒç”¨API获取按产品物料汇总数据
    getProductionCostAggregateByProduct(apiParams)
      .then(res => {
        if (res.code === 200) {
          // æŒ‰ç‰©æ–™åç§°åˆ†ç»„计算
          // è¿™é‡Œç®€åŒ–处理,orderSummary暂时使用相同的数据
          // å®žé™…项目中可能需要调用专门的API获取按订单汇总的数据
          categorySummary.value = res.data.records || [];
        } else {
          ElMessage.error(res.message || "获取物料汇总数据失败");
        }
      })
      .catch(err => {
        console.error("获取按产品物料汇总数据失败:", err);
        ElMessage.error("系统异常,获取物料汇总数据失败");
      });
    getProductionCostAggregateByOrder(apiParams)
      .then(res => {
        if (res.code === 200) {
          // æŒ‰ç‰©æ–™åç§°åˆ†ç»„计算
          // è¿™é‡Œç®€åŒ–处理,orderSummary暂时使用相同的数据
          // å®žé™…项目中可能需要调用专门的API获取按订单汇总的数据
          orderSummary.value = res.data.records || [];
        } else {
          ElMessage.error(res.message || "获取订单汇总数据失败");
        }
      })
      .catch(err => {
        console.error("获取按订单汇总数据失败:", err);
        ElMessage.error("系统异常,获取订单汇总数据失败");
      });
    // è°ƒç”¨API获取生产订单Top10数据
    getProductionCostTopOrders(apiParams)
      .then(res => {
        if (res.code === 200) {
          topOrders.value = res.data || [];
          updateTopOrdersChart();
        } else {
          ElMessage.error(res.message || "获取生产订单Top10数据失败");
        }
      })
      .catch(err => {
        console.error("获取生产订单Top10数据失败:", err);
        ElMessage.error("系统异常,获取生产订单Top10数据失败");
      });
  };
  const handleReset = () => {
    searchForm.dateRange = getDefaultDateRange();
    searchForm.monthRange = getDefaultMonthRange();
    searchForm.dictCode = "";
    searchForm.productOrderId = "";
    handleQuery();
  };
  const handleSizeChange = val => {
    page.size = val;
    page.current = 1;
  };
  const handleCurrentChange = val => {
    page.current = val;
  };
  onMounted(() => {
    getProductTypeOptions();
    loadOrders();
    handleQuery();
    initTopOrdersChart();
    window.addEventListener("resize", handleResize);
  });
};
const formatNumber = (v, digits = 2) => {
  const n = Number.parseFloat(v);
  if (!Number.isFinite(n)) return "--";
  return n.toLocaleString("zh-CN", {
    minimumFractionDigits: digits,
    maximumFractionDigits: digits,
  const handleExport = () => {
    const headers = [
      timeColumnLabel.value,
      "产品类别",
      "生产订单",
      "用量",
      "单位",
      "成本(元)",
    ];
    const lines = tableData.value.map(row =>
      [
        row.timeLabel,
        row.category,
        row.orderNo,
        row.totalQuantity.toFixed(2),
        row.unit || "",
        row.totalCost.toFixed(2),
      ].join(",")
    );
    const csv = [headers.join(","), ...lines].join("\n");
    const blob = new Blob(["\uFEFF" + csv], { type: "text/csv;charset=utf-8;" });
    const url = URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = url;
    link.download = `生产成本汇总_${statisticsType.value}_${Date.now()}.csv`;
    link.click();
    URL.revokeObjectURL(url);
    ElMessage.success("导出成功");
  };
  const formatMoney = v => {
    const n = Number.parseFloat(v);
    const value = Number.isFinite(n) ? n : 0;
    return value.toLocaleString("zh-CN", {
      minimumFractionDigits: 2,
      maximumFractionDigits: 2,
    });
  };
  const formatNumber = (v, digits = 2) => {
    const n = Number.parseFloat(v);
    if (!Number.isFinite(n)) return "--";
    return n.toLocaleString("zh-CN", {
      minimumFractionDigits: digits,
      maximumFractionDigits: digits,
    });
  };
  watch(tableData, () => {
    const maxPage = Math.max(1, Math.ceil(tableData.value.length / page.size));
    if (page.current > maxPage) page.current = maxPage;
  });
};
watch(tableData, () => {
  const maxPage = Math.max(1, Math.ceil(tableData.value.length / page.size));
  if (page.current > maxPage) page.current = maxPage;
});
</script>
<style scoped lang="scss">
.production-cost-page {
  --lux-bg: #f6f7fb;
  --lux-card: rgba(255, 255, 255, 0.86);
  --lux-border: rgba(15, 23, 42, 0.08);
  --lux-text: rgba(15, 23, 42, 0.92);
  --lux-subtle: rgba(15, 23, 42, 0.58);
  --lux-muted: rgba(15, 23, 42, 0.38);
  --lux-primary: #2f6fed;
  --lux-success: #16a34a;
  --lux-warning: #f59e0b;
  --lux-danger: #ef4444;
  --lux-shadow: 0 18px 50px rgba(15, 23, 42, 0.08);
  --lux-shadow-soft: 0 10px 28px rgba(15, 23, 42, 0.06);
  --lux-radius: 14px;
  .production-cost-page {
    --lux-bg: #f6f7fb;
    --lux-card: rgba(255, 255, 255, 0.86);
    --lux-border: rgba(15, 23, 42, 0.08);
    --lux-text: rgba(15, 23, 42, 0.92);
    --lux-subtle: rgba(15, 23, 42, 0.58);
    --lux-muted: rgba(15, 23, 42, 0.38);
    --lux-primary: #2f6fed;
    --lux-success: #16a34a;
    --lux-warning: #f59e0b;
    --lux-danger: #ef4444;
    --lux-shadow: 0 18px 50px rgba(15, 23, 42, 0.08);
    --lux-shadow-soft: 0 10px 28px rgba(15, 23, 42, 0.06);
    --lux-radius: 14px;
  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%);
}
    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%);
  }
.filter-card,
.panel-card,
.table-card {
  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 {
    border-radius: var(--lux-radius);
    border-color: var(--lux-border);
    background: var(--lux-card);
    box-shadow: var(--lux-shadow-soft);
  }
.filter-card {
  margin-bottom: 16px;
}
  .filter-card {
    margin-bottom: 16px;
  }
.panel-card,
.summary-row {
  margin-bottom: 14px;
}
.filter-layout {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
}
.filter-form {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 14px;
}
.filter-form :deep(.el-form-item) {
  margin: 0;
}
.filter-actions {
  display: flex;
  gap: 10px;
}
.card-head,
.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.card-head-left {
  display: flex;
  align-items: center;
  gap: 8px;
}
.card-head-right {
  display: flex;
  align-items: center;
}
.card-icon {
  color: var(--lux-primary);
}
.card-title {
  font-weight: 760;
  color: var(--lux-text);
}
.subtle {
  color: var(--lux-subtle);
  font-size: 12px;
}
.kpi-strip {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
}
.kpi-item {
  padding: 12px 14px;
  border-radius: 12px;
  border: 1px solid rgba(15, 23, 42, 0.08);
}
.kpi-total {
  background: linear-gradient(135deg, rgba(47, 111, 237, 0.1), rgba(255, 255, 255, 0.86));
}
.kpi-raw {
  background: linear-gradient(135deg, rgba(22, 163, 74, 0.1), rgba(255, 255, 255, 0.86));
}
.kpi-avg {
  background: linear-gradient(135deg, rgba(99, 102, 241, 0.14), rgba(255, 255, 255, 0.86));
}
.kpi-order {
  background: linear-gradient(135deg, rgba(100, 116, 139, 0.1), rgba(255, 255, 255, 0.86));
}
.kpi-label {
  font-size: 12px;
  color: var(--lux-subtle);
}
.kpi-value {
  font-size: 22px;
  margin-top: 6px;
  font-weight: 780;
  color: var(--lux-text);
}
.price-value {
  font-weight: 700;
  color: var(--lux-success);
}
.cost-value {
  font-weight: 700;
  color: var(--lux-danger);
}
.no-wrap-money {
  display: inline-block;
  white-space: nowrap;
}
.drawer-head {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
  margin-bottom: 12px;
}
.meta-item {
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid var(--lux-border);
  background: rgba(15, 23, 42, 0.03);
  display: grid;
  gap: 4px;
}
.meta-label {
  font-size: 12px;
  color: var(--lux-subtle);
}
.meta-value {
  color: var(--lux-text);
  font-size: 16px;
  font-weight: 700;
}
.material-type-tag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 46px;
  height: 24px;
  padding: 0 8px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 700;
}
.material-type-tag.is-raw {
  color: #15803d;
  background: rgba(22, 163, 74, 0.12);
}
.material-type-tag.is-aux {
  color: #b45309;
  background: rgba(245, 158, 11, 0.16);
}
.quantity-value {
  font-weight: 700;
  color: var(--lux-text);
  margin-right: 6px;
}
.quantity-cell {
  display: inline-flex;
  align-items: baseline;
  white-space: nowrap;
}
.quantity-unit {
  color: var(--lux-subtle);
}
.drawer-foot {
  display: flex;
  justify-content: flex-end;
  gap: 12px;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px dashed var(--lux-border);
}
.foot-item {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 16px;
  border-radius: 999px;
  border: 1px solid var(--lux-border);
  background: #fff;
}
.foot-label {
  color: var(--lux-subtle);
  font-size: 14px;
}
.foot-value {
  color: var(--lux-text);
  font-weight: 700;
  font-size: 16px;
}
.foot-item.total {
  border-color: rgba(47, 111, 237, 0.26);
  background: rgba(47, 111, 237, 0.08);
}
.foot-item.total .foot-value {
  color: #1e3a8a;
  font-size: 18px;
  font-weight: 800;
}
.pagination-container {
  display: flex;
  justify-content: flex-end;
  padding-top: 12px;
}
.strong {
  font-weight: 800;
}
.w-260 {
  width: 260px;
}
.w-180 {
  width: 180px;
}
::deep(.lux-table) {
  border-radius: 12px;
  overflow: hidden;
}
::deep(.lux-table th.el-table__cell) {
  background: rgba(15, 23, 42, 0.03);
}
::deep(.lux-table .el-table__row:hover > td.el-table__cell) {
  background-color: rgba(47, 111, 237, 0.06) !important;
}
@media (max-width: 1100px) {
  .kpi-strip {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  .panel-card,
  .summary-row {
    margin-bottom: 14px;
  }
  .filter-layout {
    flex-direction: column;
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 14px;
  }
  .filter-form {
    display: flex;
    flex-wrap: wrap;
    gap: 10px 14px;
  }
  .filter-form :deep(.el-form-item) {
    margin: 0;
  }
  .filter-actions {
    display: flex;
    gap: 10px;
  }
  .card-head,
  .panel-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
  }
  .card-head-left {
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .card-head-right {
    display: flex;
    align-items: center;
  }
  .card-icon {
    color: var(--lux-primary);
  }
  .card-title {
    font-weight: 760;
    color: var(--lux-text);
  }
  .subtle {
    color: var(--lux-subtle);
    font-size: 12px;
  }
  .kpi-strip {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 12px;
  }
  .kpi-item {
    padding: 12px 14px;
    border-radius: 12px;
    border: 1px solid rgba(15, 23, 42, 0.08);
  }
  .kpi-total {
    background: linear-gradient(
      135deg,
      rgba(47, 111, 237, 0.1),
      rgba(255, 255, 255, 0.86)
    );
  }
  .kpi-raw {
    background: linear-gradient(
      135deg,
      rgba(22, 163, 74, 0.1),
      rgba(255, 255, 255, 0.86)
    );
  }
  .kpi-avg {
    background: linear-gradient(
      135deg,
      rgba(99, 102, 241, 0.14),
      rgba(255, 255, 255, 0.86)
    );
  }
  .kpi-order {
    background: linear-gradient(
      135deg,
      rgba(100, 116, 139, 0.1),
      rgba(255, 255, 255, 0.86)
    );
  }
  .kpi-label {
    font-size: 12px;
    color: var(--lux-subtle);
  }
  .kpi-value {
    font-size: 22px;
    margin-top: 6px;
    font-weight: 780;
    color: var(--lux-text);
  }
  .price-value {
    font-weight: 700;
    color: var(--lux-success);
  }
  .cost-value {
    font-weight: 700;
    color: var(--lux-danger);
  }
  .no-wrap-money {
    display: inline-block;
    white-space: nowrap;
  }
  .drawer-head {
    grid-template-columns: 1fr;
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 10px;
    margin-bottom: 12px;
  }
  .meta-item {
    padding: 10px 12px;
    border-radius: 10px;
    border: 1px solid var(--lux-border);
    background: rgba(15, 23, 42, 0.03);
    display: grid;
    gap: 4px;
  }
  .meta-label {
    font-size: 12px;
    color: var(--lux-subtle);
  }
  .meta-value {
    color: var(--lux-text);
    font-size: 16px;
    font-weight: 700;
  }
  .material-type-tag {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 46px;
    height: 24px;
    padding: 0 8px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: 700;
  }
  .material-type-tag.is-raw {
    color: #15803d;
    background: rgba(22, 163, 74, 0.12);
  }
  .material-type-tag.is-aux {
    color: #b45309;
    background: rgba(245, 158, 11, 0.16);
  }
  .quantity-value {
    font-weight: 700;
    color: var(--lux-text);
    margin-right: 6px;
  }
  .quantity-cell {
    display: inline-flex;
    align-items: baseline;
    white-space: nowrap;
  }
  .quantity-unit {
    color: var(--lux-subtle);
  }
  .drawer-foot {
    justify-content: flex-start;
    flex-wrap: wrap;
    display: flex;
    justify-content: flex-end;
    gap: 12px;
    margin-top: 12px;
    padding-top: 12px;
    border-top: 1px dashed var(--lux-border);
  }
}
  .foot-item {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 7px 16px;
    border-radius: 999px;
    border: 1px solid var(--lux-border);
    background: #fff;
  }
  .foot-label {
    color: var(--lux-subtle);
    font-size: 14px;
  }
  .foot-value {
    color: var(--lux-text);
    font-weight: 700;
    font-size: 16px;
  }
  .foot-item.total {
    border-color: rgba(47, 111, 237, 0.26);
    background: rgba(47, 111, 237, 0.08);
  }
  .foot-item.total .foot-value {
    color: #1e3a8a;
    font-size: 18px;
    font-weight: 800;
  }
  .pagination-container {
    display: flex;
    justify-content: flex-end;
    padding-top: 12px;
  }
  .strong {
    font-weight: 800;
  }
  .w-260 {
    width: 260px;
  }
  .w-180 {
    width: 180px;
  }
  ::deep(.lux-table) {
    border-radius: 12px;
    overflow: hidden;
  }
  ::deep(.lux-table th.el-table__cell) {
    background: rgba(15, 23, 42, 0.03);
  }
  ::deep(.lux-table .el-table__row:hover > td.el-table__cell) {
    background-color: rgba(47, 111, 237, 0.06) !important;
  }
  @media (max-width: 1100px) {
    .kpi-strip {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
    .filter-layout {
      flex-direction: column;
    }
    .drawer-head {
      grid-template-columns: 1fr;
    }
    .drawer-foot {
      justify-content: flex-start;
      flex-wrap: wrap;
    }
  }
</style>
src/views/productionManagement/productionOrder/index.vue
@@ -96,6 +96,10 @@
        <template #completeQuantity="{ row }">
          {{ row.completeQuantity || '-' }}<span style="color:rgb(42, 169, 146)"> æ–¹</span>
        </template>
        <template #strength="{ row }">
          <el-tag v-if="row.strength"
                  :type="row.strength === 'A3.5' ? 'primary' : 'warning'">{{ row.strength }}</el-tag>
        </template>
      </PIMTable>
    </div>
    <el-dialog v-model="bindRouteDialogVisible"
@@ -329,7 +333,9 @@
      label: "强度",
      prop: "strength",
      width: "120px",
      dataType: "tag",
      dataType: "slot",
      slot: "strength",
      // formatData: val => (val ? val : ""),
    },
    {
      label: "物料编码",
src/views/productionManagement/productionReporting/detailDialog.vue
@@ -28,7 +28,7 @@
      <div class="detail-section"
           v-if="detailData.productionProductRouteItemDtoList && detailData.productionProductRouteItemDtoList.length > 0">
        <h3 class="section-title">工序信息</h3>
        <div v-for="(process, index) in detailData.productionProductRouteItemDtoList"
        <div v-for="(process) in detailData.productionProductRouteItemDtoList"
             :key="process.id"
             class="process-item">
          <div class="process-header">
src/views/productionManagement/productionReporting/reportingDialog.vue
@@ -704,7 +704,7 @@
  // åŠ è½½ç”Ÿäº§è®¢å•åˆ—è¡¨
  const loadOrders = () => {
    orderLoading.value = true;
    productOrderListPage({ pageNum: 1, pageSize: 100 })
    productOrderListPage({ pageNum: -1, pageSize: -1 })
      .then(res => {
        orderList.value = res.data.records || [];
      })
src/views/productionPlan/productionPlan/index.vue
@@ -556,6 +556,7 @@
            mergeForm.totalAssignedQuantity =
              (Number(row.volume) - Number(row.assignedQuantity)).toFixed(4) || 0;
            mergeForm.planCompleteTime = row.planCompleteTime || "";
            mergeForm.productMaterialId = row.productMaterialId || "";
            mergeForm.strength = row.strength || "";
            sumAssignedQuantity.value = mergeForm.totalAssignedQuantity;
            // æ‰“开弹窗
@@ -599,6 +600,7 @@
    totalAssignedQuantity: 0,
    planCompleteTime: "",
    strength: "",
    productMaterialId: "",
  });
  // å¯¼å…¥ç›¸å…³
@@ -672,7 +674,7 @@
    router.push({
      path: "/productionPlan/trackProgress",
      query: {
        row: encodeURIComponent(JSON.stringify(row)),
        applyNo: encodeURIComponent(row.applyNo),
      },
    });
  };
@@ -935,6 +937,7 @@
    mergeForm.height = firstRow.height || 0;
    mergeForm.totalAssignedQuantity = totalAssignedQuantity;
    mergeForm.planCompleteTime = firstRow.planCompleteTime || "";
    mergeForm.productMaterialId = firstRow.productMaterialId || "";
    mergeForm.strength = firstStrength;
    mergeForm.ids = selectedRows.value.map(row => row.id);
@@ -953,6 +956,9 @@
      ElMessage.error("砌块产品的强度为必填项");
      return;
    }
    if (mergeForm.productName != "砌块") {
      mergeForm.strength = "";
    }
    console.log(sumAssignedQuantity.value, "sumAssignedQuantity");
    // è®¡ç®—当前选中行的总方数
    const totalVolume = selectedRows.value.reduce((sum, row) => {
src/views/productionPlan/trackProgress/index.vue
@@ -1,11 +1,24 @@
<template>
  <div class="app-container">
    <PageHeader content="生产计划追踪进度">
    <PageHeader v-if="applyNo"
                content="生产计划追踪进度">
    </PageHeader>
    <el-card style="height:82vh;overflow:auto;">
      <template #header>
        <div class="card-header">
          <span>申请单编号 - {{ rowData.applyNo || '' }}</span>
          <el-form :inline="true"
                   :model="searchForm"
                   class="search-form">
            <el-form-item label="申请单编号">
              <el-input v-model="searchForm.applyNo"
                        placeholder="请输入申请单编号"
                        style="width: 400px;"></el-input>
            </el-form-item>
            <el-form-item>
              <el-button type="primary"
                         @click="handleSearch">搜索</el-button>
            </el-form-item>
          </el-form>
        </div>
      </template>
      <!-- åŸºç¡€ä¿¡æ¯ -->
@@ -34,18 +47,19 @@
            <el-descriptions :column="3"
                             border>
              <el-descriptions-item label="订单编号">{{ item.orderNo || '-' }}</el-descriptions-item>
              <el-descriptions-item label="订单状态">
              <!-- <el-descriptions-item label="订单状态">
                <el-tag :type="getStatusType(item.status)">{{ getStatusText(item.status) }}</el-tag>
              </el-descriptions-item>
              </el-descriptions-item> -->
              <el-descriptions-item label="开始日期">{{ item.startTime || '-' }}</el-descriptions-item>
              <el-descriptions-item label="需求数量">{{ item.quantity || 0 }} <span class="unit">方</span></el-descriptions-item>
              <el-descriptions-item label="完成数量">{{ item.completeQuantity || 0 }} <span class="unit">方</span></el-descriptions-item>
              <el-descriptions-item label="完成进度">
                <el-progress :percentage="item.completionRate"
                             :color="customColors(item.completionRate)"
                             :status="item.completionRate === 100 ? 'success' : ''"
                             style="width: 120px;" />
              </el-descriptions-item>
              <el-descriptions-item label="需求数量">{{ item.quantity || 0 }} <span class="unit">方</span></el-descriptions-item>
              <el-descriptions-item label="完成数量">{{ item.completeQuantity || 0 }} <span class="unit">方</span></el-descriptions-item>
              <el-descriptions-item label="剩余数量">{{ item.remainingQuantity || 0 }} <span class="unit">方</span></el-descriptions-item>
            </el-descriptions>
            <el-table :data="trackProgressForm.progressDetails"
                      border
@@ -57,11 +71,10 @@
                  <el-link v-if="$index!=0"
                           @click="handleClickStep(row)"
                           type="primary">{{ row.step }}</el-link>
                  <span v-else
                        @click="handleClickStep(row)">{{ row.step }}</span>
                  <span v-else>{{ row.step }}</span>
                </template>
              </el-table-column>
              <el-table-column prop="status"
              <!-- <el-table-column prop="status"
                               label="状态"
                               align="center">
                <template #default="scope">
@@ -69,64 +82,171 @@
                    {{ scope.row.status === 'completed' ? '已完成' : scope.row.status === 'processing' ? '进行中' : '待开始' }}
                  </el-tag>
                </template>
              </el-table-column>
              </el-table-column> -->
              <el-table-column prop="quantity"
                               label="数量"
                               align="center" />
              <el-table-columnstep prop="startTime"
                                   label="时间"
                                   align="center" />
              <el-table-column prop="startTime"
                               label="时间"
                               align="center" />
              <el-table-column prop="startTime1"
                               label="岗位人员"
                               align="center" />
            </el-table>
          </div>
        </div>
        <!-- <div class="progress-section">
          <h3 class="section-title">进度信息</h3>
          <div class="progress-item">
            <div class="progress-label">完成进度:</div>
            <div class="progress-content">
              <el-progress :percentage="trackProgressForm.completionRate"
                           :color="customColors"
                           :status="trackProgressForm.completionRate === 100 ? 'success' : ''" />
            </div>
          </div>
          <div class="progress-item">
            <div class="progress-label">进度详情:</div>
            <div class="progress-content">
              <el-table :data="trackProgressForm.progressDetails"
                        border
                        style="width: auto; height: 300px">
                <el-table-column prop="step"
                                 label="步骤(点击查看详情)"
                                 align="center">
                  <template #default="{ row, $index }">
                    <el-link v-if="$index!=0"
                             @click="handleClickStep(row)"
                             type="primary">{{ row.step }}</el-link>
                    <span v-else
                          @click="handleClickStep(row)">{{ row.step }}</span>
                  </template>
                </el-table-column>
                <el-table-column prop="status"
                                 label="状态"
                                 align="center">
                  <template #default="scope">
                    <el-tag :type="scope.row.status === 'completed' ? 'success' : scope.row.status === 'processing' ? 'warning' : 'info'">
                      {{ scope.row.status === 'completed' ? '已完成' : scope.row.status === 'processing' ? '进行中' : '待开始' }}
                    </el-tag>
                  </template>
                </el-table-column>
                <el-table-column prop="quantity"
                                 label="数量"
                                 align="center" />
                <el-table-column prop="startTime"
                                 label="时间"
                                 align="center" />
              </el-table>
            </div>
          </div>
        </div> -->
      </div>
    </el-card>
    <!-- ç”Ÿäº§æŠ¥å·¥è¯¦æƒ…弹窗 -->
    <el-dialog v-model="detailDialogVisible"
               :title="'生产报工详情'"
               width="1000px"
               :close-on-click-modal="false"
               custom-class="custom-dialog">
      <div class="detail-container">
        <!-- åŸºç¡€ä¿¡æ¯ -->
        <div class="detail-section">
          <h3 class="section-title">基础信息</h3>
          <el-descriptions :column="3"
                           border>
            <el-descriptions-item label="生产订单号">{{ detailData.npsNo || '-' }}</el-descriptions-item>
            <el-descriptions-item label="班组"><el-tag :type="detailData.schedule == '白班' ? 'primary' : 'warning'">{{ detailData.schedule || '-' }}</el-tag></el-descriptions-item>
            <el-descriptions-item label="岗位人员">{{ detailData.postName || '-' }}</el-descriptions-item>
            <el-descriptions-item label="产品编码">{{ detailData.materialCode || '-' }}</el-descriptions-item>
            <el-descriptions-item label="产品名称">{{ detailData.productName || '-' }}</el-descriptions-item>
            <el-descriptions-item label="规格">{{ detailData.model || '-' }}</el-descriptions-item>
            <el-descriptions-item label="合格数量"><span class="num2">{{ detailData.qualifiedQuantity || 0 }}</span> <span class="unit">方</span></el-descriptions-item>
            <el-descriptions-item label="不合格数量"><span class="num3">{{ detailData.unqualifiedQuantity || 0 }}</span> <span class="unit">方</span></el-descriptions-item>
            <el-descriptions-item label="总数量"><span class="num1">{{ detailData.quantity || 0 }}</span> <span class="unit">方</span></el-descriptions-item>
            <el-descriptions-item label="报工时间">{{ formatTime(detailData.reportingTime) }}</el-descriptions-item>
            <el-descriptions-item label="创建时间">{{ formatTime(detailData.createTime) }}</el-descriptions-item>
            <el-descriptions-item label="更新时间">{{ formatTime(detailData.updateTime) }}</el-descriptions-item>
          </el-descriptions>
        </div>
        <!-- å·¥åºä¿¡æ¯ -->
        <div class="detail-section"
             v-if="detailData.productionProductRouteItemDtoList && detailData.productionProductRouteItemDtoList.length > 0">
          <h3 class="section-title">工序信息</h3>
          <div v-for="(process) in detailData.productionProductRouteItemDtoList"
               :key="process.id"
               class="process-item">
            <div class="process-header">
              <h4 class="process-title">{{ process.processName || '-' }}</h4>
              <div class="process-info">
                <span class="process-label">岗位人员:{{ process.postName || '-' }}</span>
                <span class="process-label">工序ID:{{ process.processNo || '-' }}</span>
              </div>
            </div>
            <!-- å·¥åºåŸºæœ¬ä¿¡æ¯ -->
            <div class="process-details">
              <el-descriptions :column="2"
                               border>
                <el-descriptions-item label="设备异常情况">{{ process.equipmentMalfunction || '-' }}</el-descriptions-item>
                <el-descriptions-item label="当班设备处置">{{ process.equipmentDisposal || '-' }}</el-descriptions-item>
                <el-descriptions-item label="工艺人员交待"
                                      :span="2">{{ process.processExplained || '-' }}</el-descriptions-item>
              </el-descriptions>
            </div>
            <!-- å·¥åºå‚æ•° -->
            <div v-if="process.productionProductRouteItemParamDtoList && process.productionProductRouteItemParamDtoList.length > 0">
              <!-- BOM信息 -->
              <div class="param-section"
                   v-if="getBomList(process.productionProductRouteItemParamDtoList).length > 0">
                <h5 class="param-title">投入品信息</h5>
                <el-table :data="getBomList(process.productionProductRouteItemParamDtoList)"
                          style="width: 100%"
                          size="small">
                  <el-table-column prop="paramName"
                                   label="产品名称"
                                   min-width="120"></el-table-column>
                  <el-table-column prop="model"
                                   label="规格型号"
                                   min-width="120"></el-table-column>
                  <el-table-column prop="productValue"
                                   label="投入量"
                                   min-width="100"></el-table-column>
                  <el-table-column prop="unit"
                                   label="单位"
                                   width="80"></el-table-column>
                </el-table>
              </div>
              <!-- å‚数信息 -->
              <div class="param-section"
                   v-if="getParamList(process.productionProductRouteItemParamDtoList).length > 0">
                <h5 class="param-title">生产记录</h5>
                <el-card v-for="group in getParamGroups(process.productionProductRouteItemParamDtoList)"
                         :key="group.sourceSort"
                         class="detail-card"
                         style="margin-top: 10px;">
                  <template #header>
                    <div class="card-header">
                      <span v-if="Object.keys(getParamGroups(process.productionProductRouteItemParamDtoList)).length > 1">生产记录组 - {{ group.sourceSort }}</span>
                      <span v-else>生产记录</span>
                    </div>
                  </template>
                  <el-table :data="group.items"
                            style="width: 100%"
                            :row-class-name="rowClassName">
                    <el-table-column prop="paramName"
                                     label="指标" />
                    <el-table-column prop="unit"
                                     label="单位"
                                     width="100">
                      <template #default="scope">
                        {{ scope.row.unit || "/" }}
                      </template>
                    </el-table-column>
                    <el-table-column prop="standardText"
                                     label="标准值" />
                    <el-table-column prop="paramValue"
                                     label="实际值" />
                    <el-table-column prop="result"
                                     label="结果"
                                     width="100">
                      <template #default="scope">
                        <el-tag :type="scope.row.result === '合格' ? 'success' : 'danger'">
                          {{ scope.row.result }}
                        </el-tag>
                      </template>
                    </el-table-column>
                  </el-table>
                </el-card>
              </div>
            </div>
            <!-- ä¸Šä¼ æ–‡ä»¶ -->
            <div class="file-section"
                 v-if="process.fileList && process.fileList.length > 0">
              <h5 class="file-title">上传文件</h5>
              <div class="file-grid">
                <div v-for="file in process.fileList"
                     :key="file.id"
                     class="file-item">
                  <el-image style="width: 100px; height: 100px"
                            v-if="file.fileUrl"
                            :src="baseUrl + file.fileUrl"
                            :zoom-rate="1.2"
                            :max-scale="7"
                            :alt="file.fileName"
                            :min-scale="0.2"
                            :preview-src-list="formatFileList(process.fileList)"
                            show-progress
                            :initial-index="4"
                            fit="cover" />
                  <div class="file-info">
                    <span class="file-name">{{ file.fileName || '-' }}</span>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="detailDialogVisible = false">关闭</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
@@ -134,6 +254,7 @@
  import { ref, reactive, onMounted } from "vue";
  import { ElMessage } from "element-plus";
  import { useRouter, useRoute } from "vue-router";
  import dayjs from "dayjs";
  const router = useRouter();
  const route = useRoute();
@@ -149,6 +270,16 @@
    progressDetails: [],
    remark: "",
  });
  // æœç´¢è¡¨å•
  const searchForm = reactive({
    applyNo: "",
  });
  // ç”Ÿäº§æŠ¥å·¥è¯¦æƒ…弹窗
  const detailDialogVisible = ref(false);
  const detailData = ref({});
  const baseUrl = import.meta.env.VITE_APP_BASE_API;
  // èŽ·å–çŠ¶æ€ç±»åž‹
  const getStatusType = status => {
@@ -261,6 +392,30 @@
    router.push("/productionPlan/productionPlan");
  };
  // å¤„理搜索
  const handleSearch = () => {
    const applyNo = searchForm.applyNo.trim();
    if (!applyNo) {
      ElMessage.warning("请输入申请单编号");
      return;
    }
    // è¿™é‡Œå¯ä»¥æ·»åŠ æœç´¢é€»è¾‘ï¼Œä¾‹å¦‚è°ƒç”¨API获取数据
    // ç›®å‰ä½¿ç”¨æ¨¡æ‹Ÿæ•°æ®
    ElMessage.success(`搜索申请单编号: ${applyNo}`);
    // æ¨¡æ‹Ÿæœç´¢ç»“æžœ
    rowData.applyNo = applyNo;
    rowData.productName = "搜索结果产品";
    rowData.model = "搜索结果规格";
    rowData.materialCode = "MAT-" + applyNo;
    rowData.assignedQuantity = 100;
    rowData.status = 1;
    trackProgressForm.progressDetails = generateProgressDetails(1);
    trackProgressForm.completionRate = calculateCompletionRate(
      trackProgressForm.progressDetails
    );
    rowData.orderList = generateOrderList();
  };
  // ç”Ÿæˆæ¨¡æ‹Ÿè®¢å•数据
  const generateOrderList = () => {
    return [
@@ -269,6 +424,7 @@
        status: 1,
        quantity: 233.28,
        completeQuantity: 14,
        remainingQuantity: 149.28,
        completionRate: 6,
        startTime: "2026-03-25",
      },
@@ -277,6 +433,7 @@
        status: 2,
        quantity: 150.5,
        completeQuantity: 100,
        remainingQuantity: 50.5,
        completionRate: 67,
        startTime: "2026-03-20",
      },
@@ -285,30 +442,222 @@
        status: 0,
        quantity: 80.0,
        completeQuantity: 0,
        remainingQuantity: 80.0,
        completionRate: 0,
        startTime: "2026-03-30",
      },
    ];
  };
  // å¤„理点击步骤查看详情
  const handleClickStep = row => {
    // è¿™é‡Œå¯ä»¥æ·»åŠ èŽ·å–æŠ¥å·¥è¯¦æƒ…æ•°æ®çš„é€»è¾‘
    // ç›®å‰ä½¿ç”¨æ¨¡æ‹Ÿæ•°æ®
    detailData.value = {
      npsNo: "NPS-2026-001",
      schedule: "白班",
      postName: "张三",
      materialCode: rowData.materialCode || "MAT-001",
      productName: rowData.productName || "产品A",
      model: rowData.model || "规格A",
      qualifiedQuantity: 100,
      unqualifiedQuantity: 5,
      quantity: 105,
      reportingTime: new Date(),
      createTime: new Date(),
      updateTime: new Date(),
      productionProductRouteItemDtoList: [
        {
          id: 1,
          processName: "工序1",
          postName: "张三",
          processNo: "PROC-001",
          equipmentMalfunction: "无异常",
          equipmentDisposal: "正常运行",
          processExplained: "按照标准工艺操作",
          productionProductRouteItemParamDtoList: [
            {
              id: 11,
              paramName: "原材料A",
              model: "型号A",
              productValue: "100",
              unit: "kg",
              bomId: 101,
            },
            {
              id: 12,
              paramName: "温度",
              paramValue: "25",
              unit: "°C",
              sourceSort: 1,
              valueMode: 2,
              minValue: 20,
              maxValue: 30,
            },
            {
              id: 13,
              paramName: "压力",
              paramValue: "1.5",
              unit: "MPa",
              sourceSort: 1,
              valueMode: 2,
              minValue: 1.0,
              maxValue: 2.0,
            },
            {
              id: 14,
              paramName: "转速",
              paramValue: "1500",
              unit: "rpm",
              sourceSort: 2,
              valueMode: 1,
              standardValue: "1500",
            },
            {
              id: 15,
              paramName: "电流",
              paramValue: "12",
              unit: "A",
              sourceSort: 2,
              valueMode: 2,
              minValue: 10,
              maxValue: 15,
            },
          ],
          fileList: [
            {
              id: 21,
              fileName: "生产记录1.jpg",
              fileUrl: "/upload/files/20260301/12345.jpg",
              fileSize: 1024000,
            },
            {
              id: 22,
              fileName: "生产记录2.jpg",
              fileUrl: "/upload/files/20260301/67890.jpg",
              fileSize: 2048000,
            },
          ],
        },
      ],
    };
    detailDialogVisible.value = true;
  };
  // æ ¼å¼åŒ–æ—¶é—´
  const formatTime = time => {
    return time ? dayjs(time).format("YYYY-MM-DD HH:mm:ss") : "-";
  };
  // æ ¼å¼åŒ–文件列表
  const formatFileList = fileList => {
    return fileList.map(file => ({
      name: file.fileName,
      url: baseUrl + file.fileUrl,
      size: file.fileSize,
    }));
  };
  // èŽ·å–BOM列表
  const getBomList = paramList => {
    return paramList.filter(item => item.bomId);
  };
  // èŽ·å–å‚æ•°åˆ—è¡¨
  const getParamList = paramList => {
    return paramList.filter(item => !item.bomId);
  };
  // æŒ‰sourceSort分组参数
  const getParamGroups = paramList => {
    const params = getParamList(paramList);
    const groups = {};
    params.forEach(param => {
      const sort = param.sourceSort || 1;
      if (!groups[sort]) {
        groups[sort] = [];
      }
      // è®¡ç®—结果
      let result = "合格";
      let standardText = "";
      if (param.valueMode === 1) {
        // å•值比较
        if (param.standardValue !== null && param.standardValue !== undefined) {
          standardText = param.standardValue;
          if (param.paramValue !== param.standardValue) {
            result = "不合格";
          }
        } else {
          standardText = "-";
          result = "合格";
        }
      } else if (param.valueMode === 2) {
        // åŒºé—´æ¯”较
        if (param.minValue !== null || param.maxValue !== null) {
          standardText =
            (param.minValue ? param.minValue : "-∞") +
            "~" +
            (param.maxValue ? param.maxValue : "+∞");
          if (
            param.paramValue < param.minValue ||
            param.paramValue > param.maxValue
          ) {
            result = "不合格";
          }
        } else {
          standardText = "-";
          result = "合格";
        }
      } else {
        // é»˜è®¤æƒ…况
        standardText = "-";
        result = "合格";
      }
      groups[sort].push({
        ...param,
        standardText,
        result,
      });
    });
    // è½¬æ¢ä¸ºæ•°ç»„格式
    return Object.entries(groups).map(([key, items]) => ({
      sourceSort: key,
      items,
    }));
  };
  // ä¸ºä¸åˆæ ¼çš„行添加样式
  const rowClassName = ({ row }) => {
    return row.result === "不合格" ? "warning-row" : "";
  };
  const applyNo = ref(null);
  // é¡µé¢åŠ è½½æ—¶èŽ·å–æ•°æ®
  onMounted(() => {
    // ä»Žè·¯ç”±å‚数中获取数据
    const data = route.query.row
      ? JSON.parse(decodeURIComponent(route.query.row))
    applyNo.value = route.query.applyNo
      ? decodeURIComponent(route.query.applyNo)
      : null;
    if (data) {
      // èµ‹å€¼ç»™rowData
      Object.assign(rowData, data);
      // èµ‹å€¼ç»™è¡¨å•数据
      trackProgressForm.materialCode = data.materialCode;
      trackProgressForm.currentStatus = data.status;
      trackProgressForm.progressDetails = generateProgressDetails(data.status);
      trackProgressForm.completionRate = calculateCompletionRate(
        trackProgressForm.progressDetails
      );
      trackProgressForm.remark = "";
    }
    searchForm.applyNo = applyNo.value;
    // ç”Ÿæˆå‡æ•°æ®
    rowData.applyNo = applyNo.value || "APPLY-2026-001";
    rowData.productName = "测试产品";
    rowData.model = "测试规格";
    rowData.materialCode = "MAT-001";
    rowData.assignedQuantity = 233;
    rowData.status = 1;
    // èµ‹å€¼ç»™è¡¨å•数据
    trackProgressForm.materialCode = rowData.materialCode;
    trackProgressForm.currentStatus = rowData.status;
    trackProgressForm.progressDetails = generateProgressDetails(rowData.status);
    trackProgressForm.completionRate = calculateCompletionRate(
      trackProgressForm.progressDetails
    );
    trackProgressForm.remark = "";
    // ç”Ÿæˆæ¨¡æ‹Ÿè®¢å•数据
    rowData.orderList = generateOrderList();
  });
@@ -326,6 +675,14 @@
    justify-content: space-between;
    align-items: center;
    padding: 0 10px;
  }
  .search-form {
    width: 100%;
  }
  .search-form .el-form-item {
    margin-right: 10px;
  }
  .action-buttons {
@@ -409,6 +766,7 @@
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
    flex: 1;
    transition: all 0.3s ease;
    width: 100%;
  }
  .progress-section:hover {
@@ -468,4 +826,289 @@
    border-radius: 12px;
    padding: 2px 10px;
  }
  /* å¼¹çª—样式 */
  .detail-container {
    max-height: 600px;
    overflow-y: auto;
    padding: 0 16px;
  }
  .process-item {
    margin-bottom: 24px;
    padding: 20px;
    background-color: #ffffff;
    border-radius: 8px;
    border: 1px solid #ebeef5;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  }
  .process-header {
    margin-bottom: 20px;
    padding-bottom: 12px;
    border-bottom: 1px solid #f0f2f5;
  }
  .process-title {
    font-size: 15px;
    font-weight: 600;
    margin-bottom: 12px;
    color: #1a1a1a;
    display: flex;
    align-items: center;
  }
  .process-title::before {
    content: "";
    display: inline-block;
    width: 4px;
    height: 16px;
    background-color: #409eff;
    margin-right: 8px;
    border-radius: 2px;
  }
  .process-info {
    display: flex;
    gap: 20px;
    font-size: 13px;
    color: #606266;
  }
  .process-label {
    padding: 4px 12px;
    background-color: #ecf5ff;
    border-radius: 4px;
    color: #409eff;
    font-weight: 500;
  }
  .process-details {
    margin-bottom: 20px;
  }
  .param-section {
    margin-bottom: 20px;
    background-color: #f9f9f9;
    border-radius: 6px;
    padding: 16px;
    border: 1px solid #f0f2f5;
  }
  .param-title {
    font-size: 14px;
    font-weight: 600;
    margin-bottom: 14px;
    color: #1a1a1a;
    padding-bottom: 8px;
    border-bottom: 1px solid #e8e8e8;
  }
  .file-section {
    margin-top: 20px;
    background-color: #f9f9f9;
    border-radius: 6px;
    padding: 16px;
    border: 1px solid #f0f2f5;
  }
  .file-title {
    font-size: 14px;
    font-weight: 600;
    margin-bottom: 14px;
    color: #1a1a1a;
    padding-bottom: 8px;
    border-bottom: 1px solid #e8e8e8;
  }
  .file-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
    gap: 16px;
  }
  .file-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    background-color: #ffffff;
    border: 1px solid #e8e8e8;
    border-radius: 6px;
    padding: 10px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
    transition: all 0.3s ease;
  }
  .file-item:hover {
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
    border-color: #409eff;
    transform: translateY(-2px);
  }
  .file-info {
    width: 100%;
    text-align: center;
  }
  .file-name {
    font-size: 12px;
    color: #606266;
    word-break: break-all;
    line-height: 1.4;
  }
  .param-group {
    margin-bottom: 16px;
    padding: 14px;
    background-color: #ffffff;
    border-radius: 6px;
    border: 1px solid #e8e8e8;
  }
  .group-header {
    margin-bottom: 12px;
    padding-bottom: 8px;
    border-bottom: 1px solid #f0f2f5;
  }
  .num1 {
    color: #1107cc;
    font-weight: 600;
  }
  .num2 {
    color: #0fcf25;
    font-weight: 600;
  }
  .num3 {
    color: #d31818;
    font-weight: 600;
  }
  .group-title {
    font-size: 14px;
    font-weight: 600;
    color: #303133;
  }
  .param-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 16px;
  }
  .param-item {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 8px 0;
    border-bottom: 1px solid #f5f7fa;
  }
  .param-item:last-child {
    border-bottom: none;
  }
  .param-label {
    font-size: 13px;
    color: #606266;
    min-width: 100px;
    font-weight: 500;
  }
  .param-value {
    font-size: 13px;
    color: #1a1a1a;
    font-weight: 600;
    flex: 1;
  }
  .param-unit {
    font-size: 12px;
    color: #909399;
    background-color: #f0f2f5;
    padding: 2px 6px;
    border-radius: 3px;
  }
  .dialog-footer {
    text-align: center;
    padding: 20px;
    border-top: 1px solid #ebeef5;
  }
  .dialog-footer .el-button {
    min-width: 100px;
    padding: 8px 20px;
  }
  /* è‡ªå®šä¹‰å¯¹è¯æ¡†æ ·å¼ */
  :deep(.custom-dialog) {
    border-radius: 12px;
    overflow: hidden;
  }
  :deep(.custom-dialog .el-dialog__header) {
    background-color: #f5f7fa;
    padding: 20px;
    border-bottom: 1px solid #ebeef5;
  }
  :deep(.custom-dialog .el-dialog__title) {
    font-size: 18px;
    font-weight: 600;
    color: #1a1a1a;
  }
  :deep(.custom-dialog .el-dialog__body) {
    padding: 20px;
  }
  /* è¡¨æ ¼æ ·å¼ä¼˜åŒ– */
  :deep(.el-table) {
    border-radius: 6px;
    overflow: hidden;
  }
  :deep(.el-table th) {
    background-color: #f5f7fa;
    font-weight: 600;
    color: #303133;
  }
  :deep(.el-table tr:hover > td) {
    background-color: #ecf5ff !important;
  }
  /* æè¿°åˆ—表样式优化 */
  :deep(.el-descriptions) {
    border-radius: 6px;
    overflow: hidden;
  }
  :deep(.el-descriptions__label) {
    font-weight: 500;
    color: #606266;
  }
  :deep(.el-descriptions__content) {
    color: #1a1a1a;
    font-weight: 500;
  }
  /* ä¸åˆæ ¼è¡Œæ ·å¼ */
  :deep(.el-table .warning-row) {
    background-color: #fef0f0 !important;
  }
  .detail-card {
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  }
  .card-header {
    font-weight: 600;
    color: #303133;
  }
</style>
src/views/reportAnalysis/salesStatistics/index.vue
@@ -119,16 +119,16 @@
            <div class="center-metric-unit">å®¶</div>
          </div>
          <div class="center-metric m4">
            <div class="center-metric-label">总销售区</div>
            <div class="center-metric-value">{{ totalSalesAreaCount }}</div>
            <div class="center-metric-unit">区</div>
            <div class="center-metric-label">销售方数</div>
            <div class="center-metric-value">{{ totalSalesAreaCount.toFixed(0) }}</div>
            <div class="center-metric-unit">方</div>
          </div>
        </div>
      </div>
      <!-- å·¦ä¸‹ï¼šäº§å“ç±»åž‹é”€é‡ -->
      <div class="bi-panel bi-panel-bottom-left">
        <PanelHeader :isFullscreen="true"
                     title="销量数据-排名分析" />
                     title="销量数据统计" />
        <div class="panel-tabs">
          <span class="tab-item"
                :class="{ active: blockTimeDimension === 'year' }"
@@ -157,7 +157,7 @@
            <table class="scroll-table">
              <thead>
                <tr>
                  <th>排名</th>
                  <th>序号</th>
                  <th>产品类型</th>
                  <th>年月</th>
                  <th>销售区</th>
@@ -208,7 +208,7 @@
      <!-- å³ä¸‹ï¼šé”€å”®åŒºåŸŸé”€é‡ -->
      <div class="bi-panel bi-panel-bottom-right">
        <PanelHeader :isFullscreen="true"
                     title="销售额数据-排名分析" />
                     title="销售额数据统计" />
        <div class="panel-tabs">
          <span class="tab-item"
                :class="{ active: boardTimeDimension === 'year' }"
@@ -237,7 +237,7 @@
            <table class="scroll-table">
              <thead>
                <tr>
                  <th>排名</th>
                  <th>序号</th>
                  <th>年月</th>
                  <th>销售区</th>
                  <th>销售额(万元)</th>
@@ -1880,12 +1880,12 @@
  }
  /* .scroll-table tbody tr:nth-child(odd) {
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      background-color: rgba(64, 158, 255, 0.05);
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                background-color: rgba(64, 158, 255, 0.05);
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    .scroll-table tbody tr:nth-child(even) {
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        background-color: rgba(64, 158, 255, 0.1);
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          } */
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              .scroll-table tbody tr:nth-child(even) {
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  background-color: rgba(64, 158, 255, 0.1);
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    } */
  .oddTableTr {
    background-color: rgba(64, 158, 255, 0.05);
  }
src/views/reportAnalysis/unitEnergyConsumption/index.vue
@@ -4,45 +4,19 @@
    <div class="search_form">
      <el-form :model="searchForm"
               :inline="true">
        <el-form-item label="时间维度:">
          <el-select v-model="searchForm.timeDimension"
                     placeholder="请选择时间维度"
                     style="width: 120px;"
                     @change="handleQuery">
            <el-option label="年度"
                       value="year" />
            <el-option label="月度"
                       value="month" />
          </el-select>
        <el-form-item label="统计维度:">
          <el-radio-group v-model="statisticsType"
                          @change="handleTypeChange">
            <el-radio-button label="day">按日统计</el-radio-button>
            <el-radio-button label="month">按月统计</el-radio-button>
            <el-radio-button label="year">按年统计</el-radio-button>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="年份:">
          <el-select v-model="searchForm.year"
                     placeholder="请选择年份"
                     style="width: 120px;"
                     @change="handleQuery">
            <el-option v-for="year in years"
                       :key="year"
                       :label="year + 'å¹´'"
                       :value="year" />
          </el-select>
        </el-form-item>
        <el-form-item label="月份:"
                      v-if="searchForm.timeDimension === 'month'">
          <el-select v-model="searchForm.month"
                     placeholder="请选择月份"
                     style="width: 120px;"
                     @change="handleQuery">
            <el-option v-for="month in 12"
                       :key="month"
                       :label="month + '月'"
                       :value="month" />
          </el-select>
        </el-form-item>
        <el-form-item label="能耗类型:">
        <!-- <el-form-item label="能耗类型:">
          <el-select v-model="searchForm.energyType"
                     placeholder="全部"
                     clearable
                     style="width: 140px;"
                     style="width: 120px;"
                     @change="handleQuery">
            <el-option label="全部"
                       value="全部" />
@@ -52,6 +26,36 @@
                       value="电" />
            <el-option label="蒸汽"
                       value="蒸汽" />
          </el-select>
        </el-form-item> -->
        <el-form-item label="时间范围:">
          <el-date-picker v-if="statisticsType === 'day'"
                          v-model="searchForm.dateRange"
                          type="daterange"
                          range-separator="至"
                          start-placeholder="开始日期"
                          end-placeholder="结束日期"
                          value-format="YYYY-MM-DD"
                          style="width: 240px;"
                          @change="handleQuery" />
          <el-date-picker v-else-if="statisticsType === 'month'"
                          v-model="searchForm.monthRange"
                          type="monthrange"
                          range-separator="至"
                          start-placeholder="开始月份"
                          end-placeholder="结束月份"
                          value-format="YYYY-MM"
                          style="width: 240px;"
                          @change="handleQuery" />
          <el-select v-else
                     v-model="searchForm.year"
                     placeholder="选择年份"
                     style="width: 140px;"
                     @change="handleQuery">
            <el-option v-for="year in yearOptions"
                       :key="year"
                       :label="year + 'å¹´'"
                       :value="year" />
          </el-select>
        </el-form-item>
        <el-form-item>
@@ -86,56 +90,47 @@
        </el-icon>
        èƒ½è€—单耗数据
      </h2>
      <el-table :data="tableData"
      <el-table :data="tableValue"
                v-loading="tableLoading"
                border>
        <el-table-column prop="energyType"
                         label="能耗"
                         width="100"
        <el-table-column prop="meterReadingDate"
                         label="日期"
                         align="center" />
        <!-- <el-table-column prop="type"
                         label="类型"
                         align="center"
                         width="100">
          <template #default="scope">
            <el-tag :type="scope.row.type === '生产' ? 'primary' : 'success'">
              {{ scope.row.type }}
            </el-tag>
          </template>
        </el-table-column> -->
        <el-table-column prop="energyTyep"
                         label="能耗类型"
                         align="center">
          <template #default="scope">
            <el-tag :type="getEnergyTypeType(scope.row.energyType)">
              {{ scope.row.energyType }}
            <el-tag :type="getEnergyTypeType(scope.row.type)">
              {{ scope.row.type }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="unit"
                         label="单位"
                         width="120"
                         align="center" />
        <el-table-column label="月度数据"
                         v-if="searchForm.timeDimension === 'month'">
          <el-table-column prop="monthlyUnitConsumption"
                           label="月度累计单耗"
                           align="right">
            <template #default="scope">
              <span class="data-value">{{ scope.row.monthlyUnitConsumption }}</span>
            </template>
          </el-table-column>
          <el-table-column prop="monthlyConsumption"
                           label="月度累计用量/月度累计产量"
                           align="right">
            <template #default="scope">
              <span class="data-value">{{ scope.row.monthlyConsumption }}/{{ scope.row.monthlyProduction }}</span>
            </template>
          </el-table-column>
        <el-table-column prop="consumption"
                         label="用量"
                         align="right" />
        <el-table-column prop="cost"
                         label="成本"
                         align="right">
          <template #default="scope">
            <span class="data-value">Â¥{{ scope.row.cost }}</span>
          </template>
        </el-table-column>
        <el-table-column label="年度数据"
                         v-if="searchForm.timeDimension === 'year'">
          <el-table-column prop="annualUnitConsumption"
                           label="年度累计单耗"
                           align="right">
            <template #default="scope">
              <span class="data-value">{{ scope.row.annualUnitConsumption }}</span>
            </template>
          </el-table-column>
          <el-table-column prop="annualConsumption"
                           label="年度累计用量/年度累计产量"
                           align="right">
            <template #default="scope">
              <span class="data-value">{{ scope.row.annualConsumption }}/{{ scope.row.annualProduction }}</span>
            </template>
          </el-table-column>
        <el-table-column prop="unitConsumption"
                         label="单耗"
                         align="right">
          <template #default="scope">
            <span class="data-value">{{ scope.row.unitConsumption }}</span>
          </template>
        </el-table-column>
      </el-table>
    </div>
@@ -147,21 +142,58 @@
  import { ElMessage } from "element-plus";
  import { TrendCharts, List } from "@element-plus/icons-vue";
  import * as echarts from "echarts";
  import { energyConsumptionDetailStatistics } from "@/api/energyManagement/energyType";
  // ç»Ÿè®¡ç»´åº¦
  const statisticsType = ref("day");
  // æœç´¢è¡¨å•
  const searchForm = reactive({
    timeDimension: "year",
    year: new Date().getFullYear(),
    month: new Date().getMonth() + 1,
    energyType: "",
    dateRange: null,
    monthRange: null,
    year: new Date().getFullYear(),
  });
  // ç”Ÿæˆå¹´ä»½é€‰é¡¹
  const years = [];
  // ç”Ÿæˆå¹´ä»½é€‰é¡¹ï¼ˆæœ€è¿‘7年)
  const yearOptions = [];
  const currentYear = new Date().getFullYear();
  for (let i = currentYear - 5; i <= currentYear; i++) {
    years.push(i);
  for (let i = currentYear - 6; i <= currentYear; i++) {
    yearOptions.push(i);
  }
  // å¤„理统计维度变化
  const handleTypeChange = () => {
    // é‡ç½®æ—¶é—´é€‰æ‹©
    if (statisticsType.value === "day") {
      // è®¾ç½®é»˜è®¤æ—¥æœŸèŒƒå›´ä¸ºæœ€è¿‘30天
      const end = new Date();
      const start = new Date();
      start.setDate(start.getDate() - 29);
      searchForm.dateRange = [
        `${start.getFullYear()}-${String(start.getMonth() + 1).padStart(
          2,
          "0"
        )}-${String(start.getDate()).padStart(2, "0")}`,
        `${end.getFullYear()}-${String(end.getMonth() + 1).padStart(
          2,
          "0"
        )}-${String(end.getDate()).padStart(2, "0")}`,
      ];
    } else if (statisticsType.value === "month") {
      // è®¾ç½®é»˜è®¤æœˆä»½èŒƒå›´ä¸ºæœ€è¿‘6个月
      const end = new Date();
      const start = new Date();
      start.setMonth(start.getMonth() - 5);
      searchForm.monthRange = [
        `${start.getFullYear()}-${String(start.getMonth() + 1).padStart(2, "0")}`,
        `${end.getFullYear()}-${String(end.getMonth() + 1).padStart(2, "0")}`,
      ];
    } else {
      searchForm.year = currentYear;
    }
    handleQuery();
  };
  // è¡¨æ ¼æ•°æ®
  const tableData = ref([]);
@@ -195,11 +227,9 @@
  const updateChart = () => {
    const data = tableData.value;
    let xAxisData = [];
    let seriesDataKey = "monthlyData";
    let seriesDataMap = item => item.unitConsumption;
    // æ ¹æ®æ—¶é—´ç»´åº¦å‡†å¤‡æ•°æ®
    if (searchForm.timeDimension === "year") {
    // æ ¹æ®ç»Ÿè®¡ç»´åº¦å‡†å¤‡æ•°æ®
    if (statisticsType.value === "year") {
      // å¹´åº¦æ¨¡å¼ï¼š12个月
      xAxisData = [
        "1月",
@@ -215,43 +245,91 @@
        "11月",
        "12月",
      ];
    } else if (statisticsType.value === "month") {
      // æœˆåº¦æ¨¡å¼ï¼šæ ¹æ®é€‰æ‹©çš„æœˆä»½èŒƒå›´
      if (searchForm.monthRange && searchForm.monthRange.length === 2) {
        const startMonth = searchForm.monthRange[0];
        const endMonth = searchForm.monthRange[1];
        const [startYear, startMonthNum] = startMonth.split("-");
        const [endYear, endMonthNum] = endMonth.split("-");
        xAxisData = [];
        let currentYear = parseInt(startYear);
        let currentMonth = parseInt(startMonthNum);
        const endYearInt = parseInt(endYear);
        const endMonthInt = parseInt(endMonthNum);
        while (
          currentYear < endYearInt ||
          (currentYear === endYearInt && currentMonth <= endMonthInt)
        ) {
          xAxisData.push(
            `${currentYear}-${String(currentMonth).padStart(2, "0")}`
          );
          currentMonth++;
          if (currentMonth > 12) {
            currentMonth = 1;
            currentYear++;
          }
        }
      } else {
        // é»˜è®¤æ˜¾ç¤ºæœ€è¿‘6个月
        xAxisData = [];
        const end = new Date();
        for (let i = 5; i >= 0; i--) {
          const date = new Date();
          date.setMonth(date.getMonth() - i);
          xAxisData.push(
            `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(
              2,
              "0"
            )}`
          );
        }
      }
    } else {
      // æœˆåº¦æ¨¡å¼ï¼šè¯¥æœˆçš„æ¯ä¸€å¤©
      const year = searchForm.year;
      const month = searchForm.month;
      const daysInMonth = new Date(year, month, 0).getDate();
      xAxisData = Array.from({ length: daysInMonth }, (_, i) => `${i + 1}日`);
      seriesDataKey = "dailyData";
      // æŒ‰æ—¥ç»Ÿè®¡ï¼šæ ¹æ®é€‰æ‹©çš„æ—¥æœŸèŒƒå›´
      if (searchForm.dateRange && searchForm.dateRange.length === 2) {
        const startDate = searchForm.dateRange[0];
        const endDate = searchForm.dateRange[1];
        xAxisData = [];
        let currentDate = new Date(startDate);
        const end = new Date(endDate);
        while (currentDate <= end) {
          xAxisData.push(
            `${currentDate.getFullYear()}-${String(
              currentDate.getMonth() + 1
            ).padStart(2, "0")}-${String(currentDate.getDate()).padStart(2, "0")}`
          );
          currentDate.setDate(currentDate.getDate() + 1);
        }
      } else {
        // é»˜è®¤æ˜¾ç¤ºæœ€è¿‘30天
        xAxisData = [];
        const end = new Date();
        for (let i = 29; i >= 0; i--) {
          const date = new Date();
          date.setDate(date.getDate() - i);
          xAxisData.push(
            `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(
              2,
              "0"
            )}-${String(date.getDate()).padStart(2, "0")}`
          );
        }
      }
    }
    // å‡†å¤‡å›¾è¡¨æ•°æ®
    const series = [];
    const energyTypes = ["æ°´", "电", "蒸汽"];
    energyTypes.forEach(type => {
      const typeData = data.find(item => item.energyType === type);
      if (typeData && typeData[seriesDataKey]) {
        series.push({
          name: type,
          type: "line",
          data: typeData[seriesDataKey].map(seriesDataMap),
          smooth: true,
          symbol: "circle",
          symbolSize: 8,
          lineStyle: {
            width: 3,
          },
          itemStyle: {
            color:
              getEnergyTypeType(type) === "primary"
                ? "#409EFF"
                : getEnergyTypeType(type) === "warning"
                ? "#E6A23C"
                : "#67C23A",
          },
        });
    // æå–所有唯一的meterReadingDate并排序
    const allDates = [...new Set(data.map(item => item.meterReadingDate))].sort(
      (a, b) => {
        return new Date(a) - new Date(b);
      }
    });
    );
    // ä½¿ç”¨å®žé™…çš„meterReadingDate作为横轴数据
    xAxisData = allDates;
    const option = {
      tooltip: {
@@ -262,7 +340,7 @@
        textStyle: { color: "#303133" },
      },
      legend: {
        data: energyTypes,
        data: ["单耗"],
        top: 0,
        right: 10,
        textStyle: { color: "#606266" },
@@ -279,7 +357,7 @@
        data: xAxisData,
        axisLabel: {
          color: "#606266",
          rotate: searchForm.timeDimension === "month" ? 45 : 0,
          rotate: statisticsType.value === "month" ? 45 : 0,
        },
        axisLine: { lineStyle: { color: "#ebeef5" } },
        splitLine: { show: false },
@@ -292,30 +370,152 @@
        axisLine: { show: false },
        splitLine: { lineStyle: { color: "#f0f2f5" } },
      },
      series: series,
      series: {
        name: "单耗",
        type: "line",
        data: data.map(item => item.totalCost),
        smooth: true,
        symbol: "circle",
        symbolSize: 8,
        lineStyle: {
          width: 3,
        },
        itemStyle: {
          color: "#409EFF",
        },
      },
    };
    consumptionChartInstance.setOption(option);
  };
  const tableValue = ref([]);
  // æŸ¥è¯¢
  const handleQuery = () => {
    tableLoading.value = true;
    // æ¨¡æ‹ŸæŽ¥å£è°ƒç”¨
    setTimeout(() => {
      generateMockData();
      tableLoading.value = false;
      updateChart();
    }, 500);
    const params = {
      type: "",
      state:
        statisticsType.value === "year"
          ? "å¹´"
          : statisticsType.value === "month"
          ? "月"
          : "日",
    };
    if (searchForm.energyType && searchForm.energyType !== "全部") {
      params.type = searchForm.energyType;
    }
    if (statisticsType.value === "year") {
      params.startDate = searchForm.year + "-01-01";
      params.endDate = searchForm.year + "-12-31";
      // è®¡ç®—天数
      const start = new Date(params.startDate);
      const end = new Date(params.endDate);
      params.days = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
    } else if (statisticsType.value === "month" && searchForm.monthRange) {
      const [startMonth, endMonth] = searchForm.monthRange;
      const [startYearStr, startMonthStr] = startMonth.split("-");
      const [endYearStr, endMonthStr] = endMonth.split("-");
      params.startDate = `${startYearStr}-${startMonthStr}-01`;
      const endYear = Number(endYearStr);
      const endMonthNum = Number(endMonthStr);
      const lastDay = new Date(endYear, endMonthNum, 0).getDate();
      params.endDate = `${endYearStr}-${endMonthStr}-${String(lastDay).padStart(
        2,
        "0"
      )}`;
      // è®¡ç®—天数
      const start = new Date(params.startDate);
      const end = new Date(params.endDate);
      params.days = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
    } else if (statisticsType.value === "day" && searchForm.dateRange) {
      params.startDate = searchForm.dateRange[0];
      params.endDate = searchForm.dateRange[1];
      // è®¡ç®—天数
      const start = new Date(params.startDate);
      const end = new Date(params.endDate);
      params.days = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
    }
    energyConsumptionDetailStatistics(params)
      .then(res => {
        if (res.code === 200) {
          const data = res.data;
          tableData.value = data.energyCostDtos || [];
          tableValue.value = [];
          tableData.value.forEach(item => {
            tableValue.value.push({
              meterReadingDate: item.meterReadingDate,
              consumption: item.waterConsumption,
              cost: item.waterCost,
              type: "æ°´",
            });
            tableValue.value.push({
              consumption: item.electricityConsumption,
              cost: item.electricityCost,
              meterReadingDate: item.meterReadingDate,
              type: "电",
            });
            tableValue.value.push({
              consumption: item.gasConsumption,
              cost: item.gasCost,
              meterReadingDate: item.meterReadingDate,
              type: "蒸汽",
            });
          });
          updateChart();
        } else {
          ElMessage.error(res.message || "获取数据失败");
          tableData.value = [];
        }
      })
      .catch(err => {
        console.error("获取数据异常:", err);
        ElMessage.error("系统异常,获取数据失败");
        tableData.value = [];
      })
      .finally(() => {
        tableLoading.value = false;
      });
  };
  // é‡ç½®
  const handleReset = () => {
    searchForm.timeDimension = "year";
    searchForm.year = new Date().getFullYear();
    searchForm.month = new Date().getMonth() + 1;
    // é‡ç½®æœç´¢è¡¨å•
    searchForm.energyType = "";
    if (statisticsType.value === "day") {
      // è®¾ç½®é»˜è®¤æ—¥æœŸèŒƒå›´ä¸ºæœ€è¿‘30天
      const end = new Date();
      const start = new Date();
      start.setDate(start.getDate() - 29);
      searchForm.dateRange = [
        `${start.getFullYear()}-${String(start.getMonth() + 1).padStart(
          2,
          "0"
        )}-${String(start.getDate()).padStart(2, "0")}`,
        `${end.getFullYear()}-${String(end.getMonth() + 1).padStart(
          2,
          "0"
        )}-${String(end.getDate()).padStart(2, "0")}`,
      ];
    } else if (statisticsType.value === "month") {
      // è®¾ç½®é»˜è®¤æœˆä»½èŒƒå›´ä¸ºæœ€è¿‘6个月
      const end = new Date();
      const start = new Date();
      start.setMonth(start.getMonth() - 5);
      searchForm.monthRange = [
        `${start.getFullYear()}-${String(start.getMonth() + 1).padStart(2, "0")}`,
        `${end.getFullYear()}-${String(end.getMonth() + 1).padStart(2, "0")}`,
      ];
    } else {
      searchForm.year = new Date().getFullYear();
    }
    handleQuery();
  };
@@ -324,89 +524,38 @@
    ElMessage.success("报表导出成功");
  };
  // ç”Ÿæˆå‡æ•°æ®
  const generateMockData = () => {
    const energyTypes = [
      {
        energyType: "æ°´",
        unit: "吨/立方米",
        monthlyUnitConsumption: (Math.random() * 0.5 + 0.8).toFixed(4),
        monthlyConsumption: Math.floor(Math.random() * 5000 + 10000),
        monthlyProduction: Math.floor(Math.random() * 10000 + 20000),
        annualUnitConsumption: (Math.random() * 0.3 + 0.9).toFixed(4),
        annualConsumption: Math.floor(Math.random() * 60000 + 120000),
        annualProduction: Math.floor(Math.random() * 120000 + 240000),
        monthlyData: generateMonthlyData(0.8, 1.3),
        dailyData: generateDailyData(0.7, 1.4),
      },
      {
        energyType: "电",
        unit: "度/立方米",
        monthlyUnitConsumption: (Math.random() * 2 + 5).toFixed(4),
        monthlyConsumption: Math.floor(Math.random() * 50000 + 100000),
        monthlyProduction: Math.floor(Math.random() * 10000 + 20000),
        annualUnitConsumption: (Math.random() * 1.5 + 5.5).toFixed(4),
        annualConsumption: Math.floor(Math.random() * 600000 + 1200000),
        annualProduction: Math.floor(Math.random() * 120000 + 240000),
        monthlyData: generateMonthlyData(5, 7),
        dailyData: generateDailyData(4.5, 7.5),
      },
      {
        energyType: "蒸汽",
        unit: "吨/立方米",
        monthlyUnitConsumption: (Math.random() * 0.3 + 0.5).toFixed(4),
        monthlyConsumption: Math.floor(Math.random() * 3000 + 6000),
        monthlyProduction: Math.floor(Math.random() * 10000 + 20000),
        annualUnitConsumption: (Math.random() * 0.2 + 0.55).toFixed(4),
        annualConsumption: Math.floor(Math.random() * 36000 + 72000),
        annualProduction: Math.floor(Math.random() * 120000 + 240000),
        monthlyData: generateMonthlyData(0.5, 0.8),
        dailyData: generateDailyData(0.4, 0.9),
      },
    ];
    if (searchForm.energyType && searchForm.energyType !== "全部") {
      tableData.value = energyTypes.filter(
        item => item.energyType === searchForm.energyType
      );
    } else {
      tableData.value = energyTypes;
    }
  };
  // ç”Ÿæˆæœˆåº¦æ•°æ®
  const generateMonthlyData = (min, max) => {
    const data = [];
    for (let i = 1; i <= 12; i++) {
      data.push({
        month: i,
        unitConsumption: (Math.random() * (max - min) + min).toFixed(4),
      });
    }
    return data;
  };
  // ç”Ÿæˆæ¯æ—¥æ•°æ®
  const generateDailyData = (min, max) => {
    const year = searchForm.year;
    const month = searchForm.month;
    const daysInMonth = new Date(year, month, 0).getDate();
    const data = [];
    for (let i = 1; i <= daysInMonth; i++) {
      data.push({
        day: i,
        unitConsumption: (Math.random() * (max - min) + min).toFixed(4),
      });
    }
    return data;
  };
  // çª—口大小变化时重新渲染图表
  const handleResize = () => {
    consumptionChartInstance && consumptionChartInstance.resize();
  };
  onMounted(() => {
    // åˆå§‹åŒ–时间范围
    if (statisticsType.value === "day") {
      // è®¾ç½®é»˜è®¤æ—¥æœŸèŒƒå›´ä¸ºæœ€è¿‘30天
      const end = new Date();
      const start = new Date();
      start.setDate(start.getDate() - 29);
      searchForm.dateRange = [
        `${start.getFullYear()}-${String(start.getMonth() + 1).padStart(
          2,
          "0"
        )}-${String(start.getDate()).padStart(2, "0")}`,
        `${end.getFullYear()}-${String(end.getMonth() + 1).padStart(
          2,
          "0"
        )}-${String(end.getDate()).padStart(2, "0")}`,
      ];
    } else if (statisticsType.value === "month") {
      // è®¾ç½®é»˜è®¤æœˆä»½èŒƒå›´ä¸ºæœ€è¿‘6个月
      const end = new Date();
      const start = new Date();
      start.setMonth(start.getMonth() - 5);
      searchForm.monthRange = [
        `${start.getFullYear()}-${String(start.getMonth() + 1).padStart(2, "0")}`,
        `${end.getFullYear()}-${String(end.getMonth() + 1).padStart(2, "0")}`,
      ];
    }
    handleQuery();
    initChart();
    window.addEventListener("resize", handleResize);