Merge remote-tracking branch 'origin/dev_银川_中盛建材' into dev_银川_中盛建材
已添加1个文件
已修改9个文件
2326 ■■■■■ 文件已修改
src/api/costAccounting/productionSettlementBatches.js 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionPlan/productionPlan.js 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/reportAnalysis/salesStatistics.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/costAccounting/stdVsActCostAnalysis/index.vue 541 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/index.vue 222 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/processRouteItem/index.vue 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionPlan/productionPlan/index.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionPlan/trackProgress/index.vue 667 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/salesStatistics/index.vue 790 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/costAccounting/productionSettlementBatches.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
import request from "@/utils/request";
// èŽ·å–äº§å“ç±»åˆ«ï¼ˆæŒ‰æœˆä»½ï¼‰
export function getProductTypes(params) {
  return request({
    url: "/productionSettlementBatches/getProductTypes",
    method: "get",
    params,
  });
}
// èŽ·å–ç§‘ç›®ç±»åˆ«ï¼ˆæŒ‰æœˆä»½ï¼‰
export function getSubjectNames(params) {
  return request({
    url: "/productionSettlementBatches/getSubjectNames",
    method: "get",
    params,
  });
}
// æ ‡å‡†æˆæœ¬å¯¼å…¥ï¼ˆel-upload éœ€è¦å®Œæ•´ URL)
export function getImportActionUrl() {
  return `${import.meta.env.VITE_APP_BASE_API}/productionSettlementBatches/import`;
}
// ä¸‹è½½å¯¼å…¥æ¨¡æ¿ï¼ˆGET,返回 blob)
export function downloadTemplate(params) {
  return request({
    url: "/productionSettlementBatches/downloadTemplate",
    method: "get",
    params,
    responseType: "blob",
  });
}
// æ ¸ç®—查询(按月份/产品类型/科目/成本类型)
export function getSettlement(params) {
  return request({
    url: "/productionSettlementBatches/getSettlement",
    method: "get",
    params,
  });
}
// æ±‡æ€»æˆæœ¬ï¼ˆç¬¬äºŒæ¨¡å— KPI)
export function getTotalCosts(params) {
  return request({
    url: "/productionSettlementBatches/getTotalCosts",
    method: "get",
    params,
  });
}
src/api/productionPlan/productionPlan.js
@@ -68,4 +68,14 @@
    method: "post",
    data: query,
  });
}
}
// è¿½è¸ªè¿›åº¦
export function trackProgressByNo(query) {
  return request({
    url: "/track/trackProgressByNo",
    method: "get",
    params: query,
  });
}
src/api/reportAnalysis/salesStatistics.js
@@ -14,4 +14,20 @@
    url: '/home/total',
    method: 'get'
  })
}
// é”€é‡åˆ†æžè¶‹åŠ¿å›¾
export function getSalesAnalysisTrend(params) {
  return request({
    url: '/home/salesAnalysis',
    method: 'get',
    params: params
  })
}
// é”€å”®é‡‘额分析
export function getSalesAmountAnalysis(params) {
  return request({
    url: '/home/salesAmount',
    method: 'get',
    params: params
  })
}
src/views/costAccounting/stdVsActCostAnalysis/index.vue
@@ -29,32 +29,30 @@
      <div class="filter-layout">
        <el-form :model="searchForm" :inline="true" class="filter-form">
          <el-form-item label="月份范围">
          <el-form-item label="月份">
            <el-date-picker
              v-model="searchForm.monthRange"
              type="monthrange"
              range-separator="至"
              start-placeholder="开始月份"
              end-placeholder="结束月份"
              v-model="searchForm.month"
              type="month"
              value-format="YYYY-MM"
              placeholder="选择月份"
              class="w-260"
              @change="handleQuery"
              @change="handleMonthChange"
            />
          </el-form-item>
          <el-form-item label="产品类别">
          <el-form-item label="产品类型">
            <el-select
              v-model="searchForm.category"
              v-model="searchForm.productType"
              clearable
              filterable
              placeholder="全部类别"
              placeholder="全部类型"
              class="w-180"
              @change="handleQuery"
            >
              <el-option
                v-for="item in categoryOptions"
                :key="item"
                :label="item"
                :value="item"
                :key="item.value"
                :label="item.label"
                :value="item.value"
              />
            </el-select>
          </el-form-item>
@@ -62,12 +60,16 @@
            <el-select
              v-model="searchForm.costType"
              clearable
              placeholder="全部类型"
              placeholder="全部成本类型"
              class="w-180"
              @change="handleQuery"
            >
              <el-option label="能耗成本" value="能耗成本" />
              <el-option label="生产成本" value="生产成本" />
              <el-option
                v-for="item in costTypeOptions"
                :key="item.value"
                :label="item.label"
                :value="item.value"
              />
            </el-select>
          </el-form-item>
        </el-form>
@@ -78,30 +80,31 @@
            <el-button class="lux-btn" @click="handleReset">重置</el-button>
          </div>
          <div class="action-group">
            <el-dropdown trigger="click" @command="handleImportCommand">
              <el-button class="lux-btn" type="success" plain>
                æ ‡å‡†æˆæœ¬å¯¼å…¥
                <el-icon class="el-icon--right"><ArrowDown /></el-icon>
              </el-button>
              <template #dropdown>
                <el-dropdown-menu>
                  <el-dropdown-item command="template">下载导入模板</el-dropdown-item>
                  <el-dropdown-item command="upload">Excel å¯¼å…¥</el-dropdown-item>
                </el-dropdown-menu>
              </template>
            </el-dropdown>
            <el-upload
              ref="uploadRef"
              class="hidden-upload"
              :auto-upload="false"
              :show-file-list="false"
              accept=".xlsx,.xls"
              :on-change="handleFileChange"
            />
            <el-button class="lux-btn" type="success" plain @click="openImportDialog">
              æ ‡å‡†æˆæœ¬å¯¼å…¥
            </el-button>
          </div>
        </div>
      </div>
    </el-card>
    <ImportDialog
      ref="importDialogRef"
      v-model="importDialogVisible"
      title="标准成本导入"
      width="520px"
      :headers="importHeaders"
      :action="importAction"
      :auto-upload="false"
      :limit="1"
      tip-text="仅允许导入 xls、xlsx æ ¼å¼æ–‡ä»¶ã€‚"
      :show-download-template="true"
      :on-success="handleImportSuccess"
      @confirm="handleImportConfirm"
      @download-template="downloadTemplate"
      @close="handleImportDialogClose"
      @cancel="handleImportDialogClose"
    />
    <el-card class="panel-card glass-card kpi-card" shadow="never">
      <div class="kpi-strip">
@@ -194,29 +197,23 @@
        </div>
      </template>
      <el-table :data="pagedTableData" stripe class="lux-table" @sort-change="handleSortChange">
        <el-table-column prop="month" label="月份" width="110" />
        <el-table-column prop="category" label="产品类别" min-width="140" />
        <el-table-column prop="periodTime" label="月份" width="110" />
        <el-table-column prop="productType" label="产品类型" min-width="140" />
        <el-table-column prop="costType" label="成本类型" min-width="120" />
        <el-table-column prop="standardCost" label="标准成本(元)" sortable="custom" align="right">
          <template #default="scope">Â¥{{ formatMoney(scope.row.standardCost) }}</template>
        <el-table-column prop="subjectName" label="科目" min-width="140" show-overflow-tooltip />
        <el-table-column prop="budgetQty" label="预算耗量" sortable="custom" align="right" min-width="120" />
        <el-table-column prop="budgetPrice" label="预算单价" sortable="custom" align="right" min-width="120" />
        <el-table-column prop="budgetTotal" label="预算总成本" sortable="custom" align="right" min-width="130">
          <template #default="scope">Â¥{{ formatMoney(scope.row.budgetTotal) }}</template>
        </el-table-column>
        <el-table-column prop="actualCost" label="实际成本(元)" sortable="custom" align="right">
          <template #default="scope">Â¥{{ formatMoney(scope.row.actualCost) }}</template>
        <el-table-column prop="actualQty" label="实际耗量" sortable="custom" align="right" min-width="120" />
        <el-table-column prop="actualPrice" label="实际单价" sortable="custom" align="right" min-width="120" />
        <el-table-column prop="actualTotal" label="实际总成本" sortable="custom" align="right" min-width="130">
          <template #default="scope">Â¥{{ formatMoney(scope.row.actualTotal) }}</template>
        </el-table-column>
        <el-table-column prop="diff" label="差异(元)" sortable="custom" align="right">
          <template #default="scope">
            <span :class="scope.row.diff >= 0 ? 'cost-value' : 'ok-value'">
              {{ formatSignedMoney(scope.row.diff) }}
            </span>
          </template>
        </el-table-column>
        <el-table-column prop="diffRate" label="差异率" sortable="custom" align="right">
          <template #default="scope">
            <span :class="scope.row.diffRate >= 0 ? 'cost-value' : 'ok-value'">
              {{ formatPercent(scope.row.diffRate) }}
            </span>
          </template>
        </el-table-column>
        <el-table-column prop="diffQty" label="耗量差异" min-width="110" align="right" />
        <el-table-column prop="diffPrice" label="单价差异" min-width="110" align="right" />
        <el-table-column prop="diffTotal" label="总成本差异" min-width="110" align="right" />
      </el-table>
      <div class="pagination-container">
        <el-pagination
@@ -247,23 +244,40 @@
  ZoomIn,
} from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import ImportDialog from "@/components/Dialog/ImportDialog.vue";
import { getToken } from "@/utils/auth.js";
import * as echarts from "echarts";
// import * as XLSX from "xlsx";
import {
  downloadTemplate as downloadProductionSettlementTemplate,
  getImportActionUrl,
  getSettlement,
  getTotalCosts,
  getProductTypes,
} from "@/api/costAccounting/productionSettlementBatches";
const getDefaultMonthRange = () => {
const getDefaultMonth = () => {
  const end = new Date();
  const start = new Date();
  start.setMonth(start.getMonth() - 2);
  return [start.toISOString().slice(0, 7), end.toISOString().slice(0, 7)];
  return end.toISOString().slice(0, 7);
};
const searchForm = reactive({
  monthRange: getDefaultMonthRange(),
  category: "",
  month: getDefaultMonth(),
  productType: "",
  costType: "",
});
const uploadRef = ref();
const categoryOptions = ref([]);
const costTypeOptions = ref([]);
const importDialogVisible = ref(false);
const importDialogRef = ref(null);
const importHeaders = computed(() => ({
  Authorization: `Bearer ${getToken()}`,
}));
const importAction = computed(() => getImportActionUrl());
const chartRef = ref(null);
const largeChartRef = ref(null);
let chartInstance = null;
@@ -271,150 +285,22 @@
const largeChartVisible = ref(false);
const currentChartOption = ref(null);
// ------------------------------
// å‡æ•°æ®ï¼šç”¨äºŽå…ˆè”调页面渲染
// ------------------------------
const actualCostSource = ref([]);
const standardCostSource = ref([]);
const fakeMonths = ["2026-01", "2026-02", "2026-03"];
const fakeCategories = [
  "粉煤灰",
  "石灰",
  "æ°´æ³¥",
  "铝粉膏",
  "脱模剂",
  "石膏",
  "打包带",
  "防腐剂(板材用)",
  "氧化镁(板材用)",
  "冷挤丝(板材用)",
  "卡扣(板材用)",
  "材料小计",
  "æ°´",
  "电",
  "蒸汽",
];
const fakeCostType = (category) => (["æ°´", "电", "蒸汽"].includes(category) ? "能耗成本" : "生产成本");
// æ¯ä¸ªç±»åˆ«çš„æ ‡å‡†æˆæœ¬åŸºå‡†å€¼ï¼ˆä»…用于假数据)
const baseStandardCostByCategory = {
  ç²‰ç…¤ç°: 98000,
  çŸ³ç°: 52000,
  æ°´æ³¥: 175000,
  é“ç²‰è†: 32000,
  è„±æ¨¡å‰‚: 21000,
  çŸ³è†: 41000,
  æ‰“包带: 14500,
  "防腐剂(板材用)": 12500,
  "氧化镁(板材用)": 22000,
  "冷挤丝(板材用)": 9800,
  "卡扣(板材用)": 8600,
  ææ–™å°è®¡: 420000,
  æ°´: 6800,
  ç”µ: 26000,
  è’¸æ±½: 52000,
};
// æœˆä»½æ³¢åŠ¨ç³»æ•°ï¼ˆè®©å›¾è¡¨çœ‹èµ·æ¥æ›´â€œçœŸå®žâ€ä¸€äº›ï¼‰
const monthFactorByMonth = {
  "2026-01": 1.0,
  "2026-02": 1.06,
  "2026-03": 0.97,
};
// å®žé™…成本相对标准成本的偏移比例(用于测试正负差异展示)
const diffRatioByCategory = {
  ç²‰ç…¤ç°: 0.05,
  çŸ³ç°: -0.01,
  æ°´æ³¥: 0.03,
  é“ç²‰è†: 0.0,
  è„±æ¨¡å‰‚: -0.04,
  çŸ³è†: 0.02,
  æ‰“包带: -0.03,
  "防腐剂(板材用)": 0.06,
  "氧化镁(板材用)": -0.02,
  "冷挤丝(板材用)": 0.01,
  "卡扣(板材用)": -0.05,
  ææ–™å°è®¡: 0.02,
  æ°´: -0.01,
  ç”µ: 0.04,
  è’¸æ±½: -0.03,
};
const buildFakeSources = () => {
  const stdRows = [];
  const actRows = [];
  for (const month of fakeMonths) {
    const monthFactor = monthFactorByMonth[month] ?? 1;
    const monthAdj = month === "2026-02" ? 0.005 : month === "2026-03" ? -0.006 : 0;
    for (const category of fakeCategories) {
      const costType = fakeCostType(category);
      const base = baseStandardCostByCategory[category] ?? 0;
      const standardCost = Math.round(base * monthFactor);
      const diffRatio = (diffRatioByCategory[category] ?? 0) + monthAdj;
      const actualCost = Math.round(standardCost * (1 + diffRatio));
      stdRows.push({ month, category, costType, standardCost });
      actRows.push({ month, category, costType, actualCost });
    }
  }
  standardCostSource.value = stdRows;
  actualCostSource.value = actRows;
};
buildFakeSources();
const categoryOptions = computed(() => {
  const all = [...actualCostSource.value, ...standardCostSource.value];
  return Array.from(new Set(all.map((item) => item.category)));
const settlementRows = ref([]);
const totalCosts = reactive({
  budgetTotal: 0,
  actualTotal: 0,
  diffTotal: 0,
  diffRate: "0%",
});
const inRange = (value, range) => {
  if (!Array.isArray(range) || range.length !== 2 || !range[0] || !range[1]) return true;
  return value >= range[0] && value <= range[1];
};
const mergedRows = computed(() => {
  const key = (item) => `${item.month}__${item.category}__${item.costType}`;
  const stdMap = new Map(standardCostSource.value.map((item) => [key(item), item]));
  const actMap = new Map(actualCostSource.value.map((item) => [key(item), item]));
  const keySet = new Set([...stdMap.keys(), ...actMap.keys()]);
  const rows = [];
  for (const k of keySet) {
    const std = stdMap.get(k);
    const act = actMap.get(k);
    const month = std?.month || act?.month || "";
    const category = std?.category || act?.category || "";
    const costType = std?.costType || act?.costType || "";
    const standardCost = Number(std?.standardCost || 0);
    const actualCost = Number(act?.actualCost || 0);
    const diff = actualCost - standardCost;
    const diffRate = standardCost === 0 ? 0 : (diff / standardCost) * 100;
    rows.push({ month, category, costType, standardCost, actualCost, diff, diffRate });
  }
  return rows.sort((a, b) => {
    if (a.month !== b.month) return a.month > b.month ? 1 : -1;
    if (a.category !== b.category) return a.category.localeCompare(b.category, "zh-Hans-CN");
    return a.costType.localeCompare(b.costType, "zh-Hans-CN");
const tableData = computed(() => {
  return (Array.isArray(settlementRows.value) ? settlementRows.value : []).filter((item) => {
    const hitMonth = !searchForm.month || item.periodTime === searchForm.month;
    const hitProductType = !searchForm.productType || item.productType === searchForm.productType;
    const hitCostType = !searchForm.costType || item.costType === searchForm.costType;
    return hitMonth && hitProductType && hitCostType;
  });
});
const tableData = computed(() =>
  mergedRows.value.filter((item) => {
    const hitMonth = inRange(item.month, searchForm.monthRange);
    const hitCategory = !searchForm.category || item.category === searchForm.category;
    const hitCostType = !searchForm.costType || item.costType === searchForm.costType;
    return hitMonth && hitCategory && hitCostType;
  })
);
const page = reactive({
  current: 1,
@@ -454,26 +340,45 @@
  return sortedTableData.value.slice(start, start + page.size);
});
const overview = computed(() => {
  const standardCost = tableData.value.reduce((sum, item) => sum + item.standardCost, 0);
  const actualCost = tableData.value.reduce((sum, item) => sum + item.actualCost, 0);
  const diff = actualCost - standardCost;
  const diffRate = standardCost === 0 ? 0 : (diff / standardCost) * 100;
  return { standardCost, actualCost, diff, diffRate };
});
const overview = computed(() => ({
  standardCost: Number(totalCosts.budgetTotal || 0),
  actualCost: Number(totalCosts.actualTotal || 0),
  diff: Number(totalCosts.diffTotal || 0),
  diffRate: totalCosts.diffRate ?? "0%",
}));
const getChartData = () => {
  const xAxis = tableData.value.map(
    (item) => `${item.month}\n${item.category}-${item.costType.replace("成本", "")}`
  // å›¾è¡¨å£å¾„:按“科目”汇总展示全部科目
  const agg = new Map();
  for (const row of tableData.value) {
    const subjectName = String(row?.subjectName || "").trim() || "-";
    const budgetTotal = Number(row?.budgetTotal || 0);
    const actualTotal = Number(row?.actualTotal || 0);
    const bucket = agg.get(subjectName) || { subjectName, budgetTotal: 0, actualTotal: 0 };
    bucket.budgetTotal += Number.isFinite(budgetTotal) ? budgetTotal : 0;
    bucket.actualTotal += Number.isFinite(actualTotal) ? actualTotal : 0;
    agg.set(subjectName, bucket);
  }
  const rows = Array.from(agg.values()).sort((a, b) =>
    String(a.subjectName).localeCompare(String(b.subjectName), "zh-Hans-CN")
  );
  const standard = tableData.value.map((item) => item.standardCost);
  const actual = tableData.value.map((item) => item.actualCost);
  const diffRate = tableData.value.map((item) => Number(item.diffRate.toFixed(2)));
  return { xAxis, standard, actual, diffRate };
  const xAxis = rows.map((item) => item.subjectName);
  const standard = rows.map((item) => item.budgetTotal);
  const actual = rows.map((item) => item.actualTotal);
  const diffRate = rows.map((item) => {
    const base = Number(item.budgetTotal || 0);
    const diff = Number(item.actualTotal || 0) - base;
    if (!base) return 0;
    return (diff / base) * 100;
  });
  return { xAxis, standard, actual, diffRate, rows };
};
const buildChartOption = () => {
  const { xAxis, standard, actual, diffRate } = getChartData();
  const { xAxis, standard, actual, diffRate, rows } = getChartData();
  return {
    animation: true,
    animationDuration: 920,
@@ -493,13 +398,17 @@
      textStyle: { color: "rgba(15, 23, 42, 0.88)" },
      extraCssText: "box-shadow: 0 12px 40px rgba(15, 23, 42, 0.12); border-radius: 12px;",
      formatter: (params) => {
        const row = tableData.value[params[0]?.dataIndex] || {};
        const row = rows?.[params[0]?.dataIndex] || {};
        const budgetTotal = Number(row?.budgetTotal || 0);
        const actualTotal = Number(row?.actualTotal || 0);
        const diff = actualTotal - budgetTotal;
        const rate = budgetTotal ? (diff / budgetTotal) * 100 : 0;
        return [
          `${row.month || ""} ${row.category || ""} ${row.costType || ""}`,
          `标准成本:¥${formatMoney(row.standardCost || 0)}`,
          `实际成本:¥${formatMoney(row.actualCost || 0)}`,
          `差异:${formatSignedMoney(row.diff || 0)}`,
          `差异率:${formatPercent(row.diffRate || 0)}`,
          `科目:${row.subjectName || "-"}`,
          `预算总成本:¥${formatMoney(budgetTotal)}`,
          `实际总成本:¥${formatMoney(actualTotal)}`,
          `差异:${formatSignedMoney(diff)}`,
          `差异率:${formatPercent(rate)}`,
        ].join("<br/>");
      },
    },
@@ -636,72 +545,144 @@
  standardCostSource.value = Array.from(map.values());
};
const handleFileChange = async (uploadFile) => {
  try {
    const file = uploadFile.raw;
    if (!file) return;
    const data = await file.arrayBuffer();
    const workbook = XLSX.read(data, { type: "array" });
    const sheetName = workbook.SheetNames[0];
    const sheet = workbook.Sheets[sheetName];
    const rows = XLSX.utils.sheet_to_json(sheet, { defval: "" });
    const parsed = parseImportedRows(rows);
    if (!parsed.length) {
      ElMessage.warning("导入失败:模板内容为空或字段不匹配");
      return;
    }
    replaceStandardSourceByImport(parsed);
    ElMessage.success(`导入成功:${parsed.length} æ¡æ ‡å‡†æˆæœ¬è®°å½•`);
    handleQuery();
  } catch (error) {
    console.error(error);
    ElMessage.error("导入失败,请检查 Excel æ ¼å¼");
  } finally {
    uploadRef.value?.clearFiles?.();
  }
const openImportDialog = () => {
  importDialogVisible.value = true;
};
const openUploadSelector = () => {
  const input = uploadRef.value?.$el?.querySelector?.("input[type='file']");
  if (!input) {
    ElMessage.warning("上传组件尚未就绪,请稍后重试");
    return;
  }
  input.click();
const handleImportConfirm = () => {
  importDialogRef.value?.submit?.();
};
const handleImportCommand = (command) => {
  if (command === "template") {
    downloadTemplate();
    return;
  }
  if (command === "upload") {
    openUploadSelector();
  }
const handleImportDialogClose = () => {
  importDialogRef.value?.clearFiles?.();
};
const downloadTemplate = () => {
  const sample = [
    { æœˆä»½: "2026-03", äº§å“ç±»åˆ«: "粉煤灰", æˆæœ¬ç±»åž‹: "标准生产成本", æ ‡å‡†æˆæœ¬: 98000 },
    { æœˆä»½: "2026-03", äº§å“ç±»åˆ«: "æ°´æ³¥", æˆæœ¬ç±»åž‹: "标准生产成本", æ ‡å‡†æˆæœ¬: 175000 },
    { æœˆä»½: "2026-03", äº§å“ç±»åˆ«: "电", æˆæœ¬ç±»åž‹: "标准能耗成本", æ ‡å‡†æˆæœ¬: 26000 },
    { æœˆä»½: "2026-03", äº§å“ç±»åˆ«: "蒸汽", æˆæœ¬ç±»åž‹: "标准能耗成本", æ ‡å‡†æˆæœ¬: 52000 },
    { æœˆä»½: "2026-03", äº§å“ç±»åˆ«: "æ°´", æˆæœ¬ç±»åž‹: "标准能耗成本", æ ‡å‡†æˆæœ¬: 6800 },
  ];
  const ws = XLSX.utils.json_to_sheet(sample);
  const wb = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(wb, ws, "标准成本模板");
  XLSX.writeFile(wb, "标准成本按月导入模板.xlsx");
  ElMessage.success("模板已下载");
  downloadProductionSettlementTemplate({ periodTime: searchForm.month || undefined })
    .then((data) => {
      const blob =
        data instanceof Blob
          ? data
          : new Blob([data], {
              type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            });
      const url = window.URL.createObjectURL(blob);
      const link = document.createElement("a");
      link.href = url;
      link.download = "标准成本导入模板.xlsx";
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
      window.URL.revokeObjectURL(url);
      ElMessage.success("模板下载成功");
    })
    .catch(() => {
      ElMessage.error("模板下载失败");
    });
};
const handleQuery = () => {
  updateChart();
const handleImportSuccess = (response) => {
  const code = response?.code;
  const msg = response?.msg || response?.message;
  if (code === 200) {
    ElMessage.success(msg || "导入成功");
    importDialogVisible.value = false;
    importDialogRef.value?.clearFiles?.();
    handleQuery();
    return;
  }
  ElMessage.error(msg || "导入失败");
};
const normalizeSettlementResponse = (data) => {
  const map = data && typeof data === "object" ? data : {};
  const rows = [];
  for (const [costType, list] of Object.entries(map)) {
    if (!Array.isArray(list)) continue;
    for (const item of list) {
      rows.push({
        ...item,
        periodTime: searchForm.month,
        costType: costType,
      });
    }
  }
  return rows;
};
const fetchCategoryOptions = async () => {
  if (!searchForm.month) {
    categoryOptions.value = [];
    return;
  }
  try {
    const { data } = await getProductTypes({ periodTime: searchForm.month });
    const list = Array.isArray(data) ? data : data?.records || [];
    categoryOptions.value = list.map((item) => ({
      label: item.label || item.name || item.typeName || item,
      value: item.value || item.code || item.typeCode || item,
    }));
  } catch (e) {
    categoryOptions.value = [];
  }
};
const fetchCostTypeOptions = async () => {
  if (!searchForm.month) {
    costTypeOptions.value = [];
    return;
  }
  try {
    // ä¸å¸¦ costType,拿到完整分组 key ç”¨äºŽä¸‹æ‹‰é€‰é¡¹
    const res = await getSettlement({ periodTime: searchForm.month });
    const map = res?.data || {};
    const keys = Object.keys(map || {});
    costTypeOptions.value = keys.map((k) => ({ label: k, value: k }));
  } catch (e) {
    costTypeOptions.value = [];
  }
};
const handleMonthChange = () => {
  searchForm.productType = "";
  searchForm.costType = "";
  Promise.all([fetchCategoryOptions(), fetchCostTypeOptions()]).then(() => {
    handleQuery();
  });
};
const handleQuery = async () => {
  try {
    const params = {
      periodTime: searchForm.month || undefined,
      productType: searchForm.productType || undefined,
      costType: searchForm.costType || undefined,
    };
    const [settlementRes, totalRes] = await Promise.all([getSettlement(params), getTotalCosts(params)]);
    const map = settlementRes?.data || {};
    const rows = normalizeSettlementResponse(map);
    settlementRows.value = rows;
    const totals = totalRes?.data || {};
    totalCosts.budgetTotal = Number(totals.budgetTotal || 0);
    totalCosts.actualTotal = Number(totals.actualTotal || 0);
    totalCosts.diffTotal = Number(totals.diffTotal || 0);
    totalCosts.diffRate = totals.diffRate ?? "0%";
    updateChart();
  } catch (e) {
    settlementRows.value = [];
    totalCosts.budgetTotal = 0;
    totalCosts.actualTotal = 0;
    totalCosts.diffTotal = 0;
    totalCosts.diffRate = "0%";
    updateChart();
  }
};
const handleReset = () => {
  searchForm.monthRange = getDefaultMonthRange();
  searchForm.category = "";
  searchForm.month = getDefaultMonth();
  searchForm.productType = "";
  searchForm.costType = "";
  tableSort.prop = "";
  tableSort.order = "";
@@ -735,6 +716,7 @@
};
const formatPercent = (v) => {
  if (typeof v === "string" && v.trim().endsWith("%")) return v.trim();
  const n = Number.parseFloat(v);
  const value = Number.isFinite(n) ? n : 0;
  const sign = value >= 0 ? "+" : "";
@@ -802,6 +784,9 @@
    }
    updateChart();
  });
  Promise.all([fetchCategoryOptions(), fetchCostTypeOptions()]).then(() => {
    handleQuery();
  });
  window.addEventListener("resize", handleResize);
});
src/views/index.vue
@@ -2,9 +2,9 @@
  <div class="home-page">
    <div class="top-bar">
      <div class="user-box">
        <!-- <img :src="userStore.avatar"
        <img :src="userStore.avatar"
             class="avatar"
             alt="" /> -->
             alt="" />
        <div>
          <div class="hello">{{ userStore.roleName || "系统管理员" }},你好</div>
          <div class="sub">登录时间:{{ userStore.currentLoginTime }}</div>
@@ -16,9 +16,9 @@
                   type="primary"
                   plain
                   @click="refreshDashboardData">刷新数据</el-button>
        <el-button size="small"
        <!-- <el-button size="small"
                   plain
                   @click="configDialogVisible = true">首页配置</el-button>
                   @click="configDialogVisible = true">首页配置</el-button> -->
      </div>
    </div>
    <div class="content-grid">
@@ -40,7 +40,7 @@
            </el-button>
          </div>
        </section>
        <section class="section-card">
        <!-- <section class="section-card">
          <div class="section-title">重点待办</div>
          <div class="todo-row"
               v-for="todo in todos"
@@ -49,7 +49,7 @@
                    :type="todo.type">{{ todo.level }}</el-tag>
            <span>{{ todo.title }}</span>
          </div>
        </section>
        </section> -->
        <section class="section-card">
          <div class="section-title">经营关注</div>
          <div class="focus-row"
@@ -61,33 +61,17 @@
        </section>
        <section class="section-card flex-fill-card">
          <div class="section-title-row">
            <div class="section-title">今日待处理</div>
            <div class="section-title">今年销售金额分析</div>
            <el-radio-group v-model="pendingFilter"
                            size="small">
              <el-radio-button label="all">全部</el-radio-button>
              <el-radio-button label="mine">我的</el-radio-button>
              <el-radio-button label="high">高优先</el-radio-button>
              <el-radio-button label="mine">板材</el-radio-button>
              <el-radio-button label="high">砌块</el-radio-button>
            </el-radio-group>
          </div>
          <div class="task-row"
               v-for="task in filteredPendingTasks"
               :key="task.id">
            <div class="task-left">
              <el-tag size="small"
                      :type="task.type">{{ task.level }}</el-tag>
              <span class="task-title">{{ task.title }}</span>
            </div>
            <el-button link
                       type="primary"
                       @click="goTo(task.path)">去处理</el-button>
          </div>
          <el-empty v-if="filteredPendingTasks.length === 0"
                    description="暂无待处理事项"
                    :image-size="80" />
        </section>
      </div>
      <div class="right-col">
        <section class="section-card"
        <!-- <section class="section-card"
                 v-if="isSectionVisible('trendCards')">
          <div class="section-title">最近7天关键指标趋势</div>
          <div class="trend-cards">
@@ -111,7 +95,7 @@
              </div>
            </div>
          </div>
        </section>
        </section> -->
        <section class="section-card"
                 v-if="isSectionVisible('planTrend')">
          <div class="section-title-row">
@@ -137,35 +121,25 @@
             v-if="isSectionVisible('qualityChart') || isSectionVisible('costChart')">
          <section class="section-card"
                   v-if="isSectionVisible('qualityChart')">
            <div class="section-title-row">
              <div class="section-title">质检异常分布</div>
              <el-radio-group v-model="chartRangeQuality"
                              size="small"
                              @change="loadQualityData">
                <el-radio-button :label="1">周</el-radio-button>
                <el-radio-button :label="2">月</el-radio-button>
                <el-radio-button :label="3">季度</el-radio-button>
              </el-radio-group>
            </div>
            <div class="section-title">今年能耗用量趋势</div>
            <Echarts :chartStyle="chartStyle"
                     :grid="grid"
                     :tooltip="barTooltip"
                     :xAxis="qualityXAxis"
                     :xAxis="energyConsumptionXAxis"
                     :yAxis="valueYAxis"
                     :series="qualitySeries"
                     :series="energyConsumptionSeries"
                     style="height: 260px" />
          </section>
          <section class="section-card"
                   v-if="isSectionVisible('costChart')">
            <div class="section-title">能耗与成本结构</div>
            <div class="section-title">今年能耗类型占比</div>
            <Echarts :chartStyle="chartStyle"
                     :legend="costLegend"
                     :tooltip="pieTooltip"
                     :series="costSeries"
                     :series="energyTypeSeries"
                     style="height: 260px" />
          </section>
        </div>
        <section class="section-card"
        <!-- <section class="section-card"
                 v-if="isSectionVisible('warningCenter')">
          <div class="section-title">异常预警中心</div>
          <div class="warning-row"
@@ -183,7 +157,7 @@
          <el-empty v-if="warningList.length === 0"
                    description="暂无异常预警"
                    :image-size="80" />
        </section>
        </section> -->
        <section class="section-card mini-table-wrap"
                 v-if="isSectionVisible('planTable')">
          <div class="section-title">生产计划执行明细</div>
@@ -299,6 +273,7 @@
    qualityInspectionStatistics,
    nonComplianceWarning,
  } from "@/api/viewIndex.js";
  import { energyConsumptionDetailStatistics } from "@/api/energyManagement/energyType";
  const router = useRouter();
  const userStore = useUserStore();
@@ -542,6 +517,150 @@
      data: [],
    },
  ]);
  // èƒ½è€—类型占比数据
  const energyTypeSeries = reactive([
    {
      type: "pie",
      radius: ["40%", "70%"],
      center: ["50%", "50%"],
      avoidLabelOverlap: false,
      itemStyle: {
        borderRadius: 10,
        borderColor: "#fff",
        borderWidth: 2,
      },
      label: {
        show: true,
        formatter: "{b}: {d}%",
      },
      data: [
        { value: 0, name: "æ°´", itemStyle: { color: "#409EFF" } },
        { value: 0, name: "电", itemStyle: { color: "#E6A23C" } },
        { value: 0, name: "气", itemStyle: { color: "#F56C6C" } },
      ],
    },
  ]);
  // èƒ½è€—用量趋势图表
  const energyConsumptionXAxis = reactive({
    type: "category",
    data: [],
    axisLabel: {
      rotate: 45,
    },
  });
  const energyConsumptionSeries = reactive([
    {
      name: "用水量",
      type: "bar",
      data: [],
      itemStyle: { color: "#409EFF" },
    },
    {
      name: "用电量",
      type: "bar",
      data: [],
      itemStyle: { color: "#E6A23C" },
    },
    {
      name: "用气量",
      type: "bar",
      data: [],
      itemStyle: { color: "#67C23A" },
    },
  ]);
  // æ¨¡æ‹Ÿèƒ½è€—数据
  const energyData = reactive({
    water: 120,
    electricity: 350,
    gas: 80,
  });
  // æ›´æ–°èƒ½è€—类型占比图表和能耗用量趋势图表
  const updateEnergyTypeChart = () => {
    // æž„建参数:今年的年初到年末以及天数
    const currentYear = new Date().getFullYear();
    const params = {
      startDate: `${currentYear}-01-01`,
      endDate: `${currentYear}-12-31`,
      days: 365,
      state: "å¹´",
    };
    // è°ƒç”¨æŽ¥å£èŽ·å–æ•°æ®
    energyConsumptionDetailStatistics(params)
      .then(res => {
        if (res.code === 200) {
          const data = res.data;
          // å¤„理能耗类型占比数据
          const energyTypeData = data.energyCostDtos || [];
          // è®¡ç®—各能耗类型的总消耗量
          let total = 0;
          const typeMap = {
            æ°´: 0,
            ç”µ: 0,
            æ°”: 0,
          };
          // å‡†å¤‡èƒ½è€—用量趋势图表数据
          const dates = [];
          const waterConsumptionData = [];
          const electricityConsumptionData = [];
          const gasConsumptionData = [];
          energyTypeData.forEach(item => {
            // æ”¶é›†æ—¥æœŸå’Œå„能耗类型数据
            if (item.meterReadingDate) {
              dates.push(item.meterReadingDate);
              waterConsumptionData.push(item.waterConsumption || 0);
              electricityConsumptionData.push(item.electricityConsumption || 0);
              gasConsumptionData.push(item.gasConsumption || 0);
            }
            // è®¡ç®—总消耗量
            if (item.waterConsumption)
              typeMap.æ°´ += Number(item.waterConsumption);
            if (item.electricityConsumption)
              typeMap.电 += Number(item.electricityConsumption);
            if (item.gasConsumption) typeMap.气 += Number(item.gasConsumption);
          });
          total = typeMap.æ°´ + typeMap.电 + typeMap.气;
          // æ›´æ–°èƒ½è€—类型占比图表数据
          energyTypeSeries[0].data = [
            {
              value: total > 0 ? ((typeMap.æ°´ / total) * 100).toFixed(2) : 0,
              name: "æ°´",
              itemStyle: { color: "#409EFF" },
            },
            {
              value: total > 0 ? ((typeMap.电 / total) * 100).toFixed(2) : 0,
              name: "电",
              itemStyle: { color: "#E6A23C" },
            },
            {
              value: total > 0 ? ((typeMap.气 / total) * 100).toFixed(2) : 0,
              name: "气",
              itemStyle: { color: "#F56C6C" },
            },
          ];
          // æ›´æ–°èƒ½è€—用量趋势图表数据
          energyConsumptionXAxis.data = dates;
          energyConsumptionSeries[0].data = waterConsumptionData;
          energyConsumptionSeries[1].data = electricityConsumptionData;
          energyConsumptionSeries[2].data = gasConsumptionData;
        }
      })
      .catch(err => {
        console.error("获取能耗数据异常:", err);
      });
  };
  const planTable = reactive([]);
  const recentTrendCards = reactive([
@@ -976,18 +1095,19 @@
  };
  const refreshDashboardData = () => {
    loadHomeTodos();
    loadOrderAndProgress();
    loadPlanTrend();
    loadQualityData();
    loadCostComposition();
    loadWarningCenter();
    // loadHomeTodos();
    // loadOrderAndProgress();
    // loadPlanTrend();
    // loadQualityData();
    // loadCostComposition();
    // loadWarningCenter();
    updateEnergyTypeChart();
    lastUpdatedAt.value = new Date().toLocaleString();
  };
  onMounted(() => {
    // initSectionConfig();
    // refreshDashboardData();
    refreshDashboardData();
  });
</script>
src/views/productionManagement/processRoute/index.vue
@@ -247,6 +247,7 @@
        bomId: row.bomId || null,
        description: row.description || "",
        type: "route",
        status: row.status || false,
      },
    });
  };
src/views/productionManagement/processRoute/processRouteItem/index.vue
@@ -47,13 +47,15 @@
         class="section-header">
      <div class="section-title">工艺路线项目列表</div>
      <div class="section-actions">
        <div class="sort-tip">拖拽表格排序</div>
        <div v-if="!routeInfo.status"
             class="sort-tip">拖拽表格排序</div>
        <el-button icon="Grid"
                   @click="toggleView"
                   style="margin-right: 10px;">
          å¡ç‰‡è§†å›¾
        </el-button>
        <el-button type="primary"
        <el-button v-if="!routeInfo.status"
                   type="primary"
                   @click="handleAdd">新增</el-button>
      </div>
    </div>
@@ -97,6 +99,7 @@
        </template>
      </el-table-column>
      <el-table-column label="操作"
                       v-if="!routeInfo.status"
                       align="center"
                       fixed="right"
                       width="150">
@@ -119,13 +122,15 @@
      <div class="section-header">
        <div class="section-title">工艺路线项目列表</div>
        <div class="section-actions">
          <div class="sort-tip">长按拖拽卡片排序</div>
          <div v-if="!routeInfo.status"
               class="sort-tip">长按拖拽卡片排序</div>
          <el-button icon="Menu"
                     @click="toggleView"
                     style="margin-right: 10px;">
            è¡¨æ ¼è§†å›¾
          </el-button>
          <el-button type="primary"
                     v-if="!routeInfo.status"
                     @click="handleAdd">新增</el-button>
        </div>
      </div>
@@ -150,6 +155,7 @@
              <el-button type="primary"
                         link
                         size="small"
                         v-if="!routeInfo.status"
                         @click="handleEdit(item)"
                         :disabled="item.isComplete">编辑</el-button>
              <el-button type="info"
@@ -159,6 +165,7 @@
              <el-button type="danger"
                         link
                         size="small"
                         v-if="!routeInfo.status"
                         @click="handleDelete(item)"
                         :disabled="item.isComplete">删除</el-button>
            </div>
@@ -262,7 +269,7 @@
                      <span v-else>{{ row.unit }}</span>
                    </template>
                  </el-table-column>
                   <el-table-column prop="unitPrice"
                  <el-table-column prop="unitPrice"
                                   label="单价">
                    <template #default="{ row }">
                      <el-form-item v-if="bomDataValue.isEdit"
@@ -541,6 +548,7 @@
      dictLabel: route.query.dictLabel || "",
      bomId: route.query.bomId || null,
      description: route.query.description || "",
      status: route.query.status === "true" ? true : false,
    };
    if (pageType.value === "order") {
      queryList2(route.query.orderId)
src/views/productionPlan/productionPlan/index.vue
@@ -674,7 +674,10 @@
    router.push({
      path: "/productionPlan/trackProgress",
      query: {
        applyNo: encodeURIComponent(row.applyNo),
        id: row.id,
        applyNo: row.applyNo,
        productName: row.productName,
        model: row.model,
      },
    });
  };
@@ -1559,4 +1562,7 @@
  //     margin-bottom: 0px !important;
  //   }
  // }
  :deep(.el-table .el-table__body-wrapper tr td) {
    background-color: #fff;
  }
</style>
src/views/productionPlan/trackProgress/index.vue
@@ -10,92 +10,118 @@
                   :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-select v-model="selectedApplyNo"
                         filterable
                         remote
                         reserve-keyword
                         placeholder="请输入申请单编号"
                         :loading="applyNoLoading"
                         :remote-method="handleApplyNoSearch"
                         @change="handleSearch"
                         style="width: 400px;">
                <el-option v-for="option in applyNoOptions"
                           :key="option.id"
                           :label="option.applyNo+'-'+option.productName+'-'+option.model"
                           :value="option.id" />
              </el-select>
            </el-form-item>
          </el-form>
        </div>
      </template>
      <!-- åŸºç¡€ä¿¡æ¯ -->
      <div class="detail-section">
      <div v-if="rowData.productionPlanDto"
           class="detail-section">
        <h3 class="section-title">基础信息</h3>
        <el-descriptions :column="3"
                         border>
          <el-descriptions-item label="申请单编号">{{ rowData.applyNo || '-' }}</el-descriptions-item>
          <el-descriptions-item label="产品名称">{{ rowData.productName || '-' }}</el-descriptions-item>
          <el-descriptions-item label="产品规格">{{ rowData.model || '-' }}</el-descriptions-item>
          <el-descriptions-item label="物料编码">{{ rowData.materialCode || '-' }}</el-descriptions-item>
          <el-descriptions-item label="下发数量">{{ rowData.assignedQuantity || 0 }} <span class="unit">方</span></el-descriptions-item>
          <el-descriptions-item label="当前状态">
            <el-tag :type="getStatusType(rowData.status)">
              {{ getStatusText(rowData.status) }}
            </el-tag>
          </el-descriptions-item>
        </el-descriptions>
        <el-skeleton :loading="loading"
                     animated>
          <template #template>
            <el-skeleton-item variant="p"
                              style="width: 80%" />
            <el-skeleton-item variant="p"
                              style="width: 60%" />
            <el-skeleton-item variant="p"
                              style="width: 40%" />
          </template>
          <el-descriptions :column="3"
                           border>
            <el-descriptions-item label="申请单编号">{{ rowData.productionPlanDto?.applyNo || '-' }}</el-descriptions-item>
            <el-descriptions-item label="产品名称">{{ rowData.productionPlanDto?.productName || '-' }}</el-descriptions-item>
            <el-descriptions-item label="产品规格">{{ rowData.productionPlanDto?.model || '-' }}</el-descriptions-item>
            <el-descriptions-item label="物料编码">{{ rowData.productionPlanDto?.materialCode || '-' }}</el-descriptions-item>
            <el-descriptions-item label="下发数量">{{ rowData.productionPlanDto?.assignedQuantity || 0 }} <span class="unit">方</span></el-descriptions-item>
            <el-descriptions-item label="当前状态">
              <el-tag :type="getStatusType(rowData.productionPlanDto?.status)">
                {{ getStatusText(rowData.productionPlanDto?.status) }}
              </el-tag>
            </el-descriptions-item>
          </el-descriptions>
        </el-skeleton>
      </div>
      <div class="progress-container">
      <el-empty v-else
                description="请搜索申请单编号" />
      <div v-if="rowData.orderDtoList"
           class="progress-container">
        <div class="progress-section">
          <h3 class="section-title">订单信息</h3>
          <div v-for="item in rowData.orderList"
               :key="item.orderNo"
               class="order-item">
            <el-descriptions :column="3"
                             border>
              <el-descriptions-item label="订单编号">{{ item.orderNo || '-' }}</el-descriptions-item>
              <!-- <el-descriptions-item label="订单状态">
                <el-tag :type="getStatusType(item.status)">{{ getStatusText(item.status) }}</el-tag>
              </el-descriptions-item> -->
              <el-descriptions-item label="开始日期">{{ item.startTime || '-' }}</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
                      style="width: auto; height: 200px">
              <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>{{ 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-column prop="startTime1"
                               label="岗位人员"
                               align="center" />
            </el-table>
          </div>
          <el-skeleton :loading="loading"
                       animated>
            <template #template>
              <el-skeleton-item variant="p"
                                style="width: 80%" />
              <el-skeleton-item variant="p"
                                style="width: 60%" />
              <el-skeleton-item variant="p"
                                style="width: 40%" />
              <el-skeleton-item variant="p"
                                style="width: 100%" />
            </template>
            <div v-for="(item, index) in rowData.orderDtoList"
                 :key="item.productOrderDto?.npsNo || index"
                 class="order-item">
              <el-descriptions :column="3"
                               border>
                <el-descriptions-item label="订单编号">{{ item.productOrderDto?.npsNo || '-' }}</el-descriptions-item>
                <el-descriptions-item label="开始日期">{{ item.productOrderDto?.startTime || '-' }}</el-descriptions-item>
                <el-descriptions-item label="完成进度">
                  <el-progress :percentage="item.productOrderDto?.completionStatus"
                               :color="customColors(item.productOrderDto?.completionStatus)"
                               :status="item.productOrderDto?.completionStatus === 100 ? 'success' : ''"
                               style="width: 120px;" />
                </el-descriptions-item>
                <el-descriptions-item label="需求数量">{{ item.productOrderDto?.quantity || 0 }} <span class="unit">方</span></el-descriptions-item>
                <el-descriptions-item label="完成数量">{{ item.productOrderDto?.completeQuantity || 0 }} <span class="unit">方</span></el-descriptions-item>
                <el-descriptions-item label="剩余数量">{{ (item.productOrderDto?.quantity - item.productOrderDto?.completeQuantity) || 0 }} <span class="unit">方</span></el-descriptions-item>
              </el-descriptions>
              <el-table :data="item.productionProductMainDtos"
                        border
                        style="width: auto; max-height: 200px">
                <el-table-column prop="step"
                                 label="报工(点击查看详情)"
                                 align="center">
                  <template #default="{ row }">
                    <el-link @click="handleClickStep(row)"
                             type="primary">{{ row.productNo }}</el-link>
                  </template>
                </el-table-column>
                <el-table-column prop="quantity"
                                 label="数量(方)"
                                 align="center" />
                <el-table-column prop="reportingTime"
                                 label="时间"
                                 align="center" />
                <el-table-column prop="schedule"
                                 label="班次"
                                 align="center" />
                <el-table-column prop="postName"
                                 label="岗位人员"
                                 align="center" />
              </el-table>
            </div>
          </el-skeleton>
        </div>
      </div>
      <el-empty v-else-if="rowData.productionPlanDto"
                description="暂无进度" />
    </el-card>
    <!-- ç”Ÿäº§æŠ¥å·¥è¯¦æƒ…弹窗 -->
    <el-dialog v-model="detailDialogVisible"
@@ -104,142 +130,159 @@
               :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>
        <el-skeleton :loading="dialogLoading"
                     animated>
          <template #template>
            <el-skeleton-item variant="p"
                              style="width: 80%" />
            <el-skeleton-item variant="p"
                              style="width: 60%" />
            <el-skeleton-item variant="p"
                              style="width: 40%" />
            <el-skeleton-item variant="h3"
                              style="width: 50%" />
            <el-skeleton-item variant="p"
                              style="width: 100%" />
            <el-skeleton-item variant="p"
                              style="width: 100%" />
          </template>
          <!-- åŸºç¡€ä¿¡æ¯ -->
          <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>
            <!-- å·¥åºåŸºæœ¬ä¿¡æ¯ -->
            <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 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 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"
              <!-- å·¥åºå‚æ•° -->
              <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%"
                            :row-class-name="rowClassName">
                            size="small">
                    <el-table-column prop="paramName"
                                     label="指标" />
                                     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="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>
                                     width="80"></el-table-column>
                  </el-table>
                </el-card>
                </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>
            <!-- ä¸Šä¼ æ–‡ä»¶ -->
            <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 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>
        </el-skeleton>
      </div>
      <template #footer>
        <div class="dialog-footer">
@@ -255,6 +298,11 @@
  import { ElMessage } from "element-plus";
  import { useRouter, useRoute } from "vue-router";
  import dayjs from "dayjs";
  import {
    trackProgressByNo,
    productionPlanListPage,
  } from "@/api/productionPlan/productionPlan";
  import { productionReportDetail } from "@/api/productionManagement/productionReporting.js";
  const router = useRouter();
  const route = useRoute();
@@ -271,15 +319,21 @@
    remark: "",
  });
  // æœç´¢è¡¨å•
  const searchForm = reactive({
    applyNo: "",
  });
  // ç”Ÿäº§æŠ¥å·¥è¯¦æƒ…弹窗
  const detailDialogVisible = ref(false);
  const detailData = ref({});
  const baseUrl = import.meta.env.VITE_APP_BASE_API;
  // åŠ è½½çŠ¶æ€
  const loading = ref(false);
  // å¼¹çª—加载状态
  const dialogLoading = ref(false);
  // ç”³è¯·å•下拉框数据
  const applyNoOptions = ref([]);
  const applyNoLoading = ref(false);
  const applyNoQuery = ref("");
  const selectedApplyNo = ref(null);
  // èŽ·å–çŠ¶æ€ç±»åž‹
  const getStatusType = status => {
@@ -392,28 +446,52 @@
    router.push("/productionPlan/productionPlan");
  };
  // å¤„理申请单编号搜索
  const handleApplyNoSearch = query => {
    if (query) {
      applyNoLoading.value = true;
      productionPlanListPage({
        current: -1,
        size: -1,
        applyNo: query,
      })
        .then(res => {
          // è½¬æ¢æ•°æ®æ ¼å¼ä¸ºä¸‹æ‹‰æ¡†æ‰€éœ€çš„æ ¼å¼
          applyNoOptions.value = res.data.records;
        })
        .catch(error => {
          console.error(error);
        })
        .finally(() => {
          applyNoLoading.value = false;
        });
    } else {
      applyNoOptions.value = [];
    }
  };
  // å¤„理搜索
  const handleSearch = () => {
    const applyNo = searchForm.applyNo.trim();
    if (!applyNo) {
      ElMessage.warning("请输入申请单编号");
    if (!selectedApplyNo.value) {
      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();
    // è°ƒç”¨API获取数据
    loading.value = true;
    trackProgressByNo({ productionPlanId: selectedApplyNo.value })
      .then(res => {
        console.log(res, "搜索结果");
        // åˆå¹¶æ•°æ®åˆ°rowData
        Object.assign(rowData, res.data);
        ElMessage.success("搜索成功");
      })
      .catch(error => {
        ElMessage.error("搜索失败,请稍后重试");
        console.error(error);
      })
      .finally(() => {
        loading.value = false;
      });
  };
  // ç”Ÿæˆæ¨¡æ‹Ÿè®¢å•数据
@@ -451,97 +529,23 @@
  // å¤„理点击步骤查看详情
  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;
    // èŽ·å–æŠ¥å·¥è¯¦æƒ…æ•°æ®
    dialogLoading.value = true;
    productionReportDetail(row.id)
      .then(res => {
        console.log(res, "报工详情");
        // å°†API返回的数据赋值给detailData
        detailData.value = res.data;
        // æ‰“开弹窗
        detailDialogVisible.value = true;
      })
      .catch(error => {
        ElMessage.error("获取报工详情失败,请稍后重试");
        console.error(error);
      })
      .finally(() => {
        dialogLoading.value = false;
      });
  };
  // æ ¼å¼åŒ–æ—¶é—´
@@ -637,29 +641,26 @@
  // é¡µé¢åŠ è½½æ—¶èŽ·å–æ•°æ®
  onMounted(() => {
    // ä»Žè·¯ç”±å‚数中获取数据
    applyNo.value = route.query.applyNo
      ? decodeURIComponent(route.query.applyNo)
    selectedApplyNo.value = route.query.applyNo
      ? route.query.applyNo +
        "-" +
        route.query.productName +
        "-" +
        route.query.model
      : null;
    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();
    if (route.query.id) {
      loading.value = true;
      trackProgressByNo({ productionPlanId: route.query.id })
        .then(res => {
          console.log(res, "追踪进度");
          // åˆå¹¶æ•°æ®åˆ°rowData
          Object.assign(rowData, res.data);
        })
        .finally(() => {
          loading.value = false;
        });
    }
  });
</script>
src/views/reportAnalysis/salesStatistics/index.vue
@@ -6,7 +6,7 @@
    <div class="bi-topbar">
      <img class="bi-topbar-title-bg"
           src="@/assets/BI/biaoti.png"
           alt="销售看板统计" />
           alt="销售统计看板" />
      <div class="bi-topbar-content">
        <div class="bi-topbar-left">
          <button class="fullscreen-btn"
@@ -35,7 +35,7 @@
          <span>26℃</span>
          <span class="bi-topbar-sep">湿度:1</span> -->
        </div>
        <div class="bi-topbar-title">销售看板统计</div>
        <div class="bi-topbar-title">销售统计看板</div>
        <div class="bi-topbar-meta">
          <span class="bi-topbar-time">{{ currentTime }}</span>
          <span class="bi-topbar-sep">|</span>
@@ -50,19 +50,19 @@
                     title="销量分析趋势图" />
        <div class="panel-tabs">
          <span class="tab-item"
                :class="{ active: blockTimeDimension === 'year' }"
                @click="handleBlockTimeDimensionChange('year')">å¹´</span>
                :class="{ active: chartTimeDimension === 'å¹´' }"
                @click="handleChartTimeDimensionChange('å¹´')">å¹´</span>
          <span class="tab-item"
                :class="{ active: blockTimeDimension === 'month' }"
                @click="handleBlockTimeDimensionChange('month')">月</span>
                :class="{ active: chartTimeDimension === '月' }"
                @click="handleChartTimeDimensionChange('月')">月</span>
        </div>
        <div class="panel-tabs2">
          <span class="tab-item"
                :class="{ active: blockProductType === '砌块' }"
                @click="handleBlockProductTypeChange('砌块')">砌块</span>
                :class="{ active: chartProductType === '砌块' }"
                @click="handleChartProductTypeChange('砌块')">砌块</span>
          <span class="tab-item"
                :class="{ active: blockProductType === '板材' }"
                @click="handleBlockProductTypeChange('板材')">板材</span>
                :class="{ active: chartProductType === '板材' }"
                @click="handleChartProductTypeChange('板材')">板材</span>
        </div>
        <div class="bi-panel-body">
          <div class="chart-unit-row">
@@ -78,19 +78,19 @@
                     title="销售金额分析" />
        <div class="panel-tabs">
          <span class="tab-item"
                :class="{ active: boardTimeDimension === 'year' }"
                @click="handleBoardTimeDimensionChange('year')">å¹´</span>
                :class="{ active: chartTimeDimension2 === 'å¹´' }"
                @click="handleChartTimeDimensionChange2('å¹´')">å¹´</span>
          <span class="tab-item"
                :class="{ active: boardTimeDimension === 'month' }"
                @click="handleBoardTimeDimensionChange('month')">月</span>
                :class="{ active: chartTimeDimension2 === '月' }"
                @click="handleChartTimeDimensionChange2('月')">月</span>
        </div>
        <div class="panel-tabs2">
          <span class="tab-item"
                :class="{ active: boardProductType === '砌块' }"
                @click="handleBoardProductTypeChange('砌块')">砌块</span>
                :class="{ active: chartProductType2 === '砌块' }"
                @click="handleChartProductTypeChange2('砌块')">砌块</span>
          <span class="tab-item"
                :class="{ active: boardProductType === '板材' }"
                @click="handleBoardProductTypeChange('板材')">板材</span>
                :class="{ active: chartProductType2 === '板材' }"
                @click="handleChartProductTypeChange2('板材')">板材</span>
        </div>
        <div class="bi-panel-body">
          <div class="chart-unit-row">
@@ -131,27 +131,27 @@
                     title="销量数据统计" />
        <div class="panel-tabs">
          <span class="tab-item"
                :class="{ active: blockTimeDimension === 'year' }"
                @click="handleBlockTimeDimensionChange('year')">å¹´</span>
                :class="{ active: tableTimeDimension === 'å¹´' }"
                @click="handleTableTimeDimensionChange('å¹´')">å¹´</span>
          <span class="tab-item"
                :class="{ active: blockTimeDimension === 'month' }"
                @click="handleBlockTimeDimensionChange('month')">月</span>
                :class="{ active: tableTimeDimension === '月' }"
                @click="handleTableTimeDimensionChange('月')">月</span>
        </div>
        <div class="panel-tabs2">
          <span class="tab-item"
                :class="{ active: blockProductType === '砌块' }"
                @click="handleBlockProductTypeChange('砌块')">砌块</span>
                :class="{ active: tableProductType === '砌块' }"
                @click="handleTableProductTypeChange('砌块')">砌块</span>
          <span class="tab-item"
                :class="{ active: blockProductType === '板材' }"
                @click="handleBlockProductTypeChange('板材')">板材</span>
                :class="{ active: tableProductType === '板材' }"
                @click="handleTableProductTypeChange('板材')">板材</span>
        </div>
        <div class="bi-panel-body">
          <div class="chart-filter-tabs">
            <span v-for="area in salesAreas"
            <span v-for="area in tableSalesAreas"
                  :key="area"
                  class="cf-tab"
                  :class="{ active: blockSelectedArea === area }"
                  @click="handleBlockAreaChange(area)">{{ area }}</span>
                  :class="{ active: tableSelectedArea === area }"
                  @click="handleTableAreaChange(area)">{{ area }}</span>
          </div>
          <div class="scroll-table-container">
            <table class="scroll-table">
@@ -166,10 +166,10 @@
              </thead>
              <div class="scroll-table-content">
                <tbody ref="blockTableBody">
                  <tr :class="item.sort % 2 === 0 ? 'evenTableTr' : 'oddTableTr'"
                      v-for="(item, index) in blockSalesData"
                  <tr :class="(index + 1) % 2 === 0 ? 'evenTableTr' : 'oddTableTr'"
                      v-for="(item, index) in filteredTableSalesData"
                      :key="item.period + item.area + index">
                    <td>{{ item.sort }}</td>
                    <td>{{ index + 1 }}</td>
                    <td>{{ item.productType }}</td>
                    <td>{{ item.period }}</td>
                    <td>{{ item.area }}</td>
@@ -181,7 +181,7 @@
          </div>
          <div class="panel-summary-row">
            <div class="summary-label">合计</div>
            <div class="summary-value">127384 m³</div>
            <div class="summary-value">{{ filteredTableSalesTotal }} m³</div>
          </div>
        </div>
      </div>
@@ -211,44 +211,46 @@
                     title="销售额数据统计" />
        <div class="panel-tabs">
          <span class="tab-item"
                :class="{ active: boardTimeDimension === 'year' }"
                @click="handleBoardTimeDimensionChange('year')">å¹´</span>
                :class="{ active: tableTimeDimension2 === 'å¹´' }"
                @click="handleTableTimeDimensionChange2('å¹´')">å¹´</span>
          <span class="tab-item"
                :class="{ active: boardTimeDimension === 'month' }"
                @click="handleBoardTimeDimensionChange('month')">月</span>
                :class="{ active: tableTimeDimension2 === '月' }"
                @click="handleTableTimeDimensionChange2('月')">月</span>
        </div>
        <div class="panel-tabs2">
          <span class="tab-item"
                :class="{ active: boardProductType === '砌块' }"
                @click="handleBoardProductTypeChange('砌块')">砌块</span>
                :class="{ active: tableProductType2 === '砌块' }"
                @click="handleTableProductTypeChange2('砌块')">砌块</span>
          <span class="tab-item"
                :class="{ active: boardProductType === '板材' }"
                @click="handleBoardProductTypeChange('板材')">板材</span>
                :class="{ active: tableProductType2 === '板材' }"
                @click="handleTableProductTypeChange2('板材')">板材</span>
        </div>
        <div class="bi-panel-body">
          <div class="chart-filter-tabs">
            <span v-for="area in salesAreas"
            <span v-for="area in tableSalesAreas"
                  :key="area"
                  class="cf-tab"
                  :class="{ active: boardSelectedArea === area }"
                  @click="handleBoardAreaChange(area)">{{ area }}</span>
                  :class="{ active: tableSelectedArea === area }"
                  @click="handleTableAreaChange2(area)">{{ area }}</span>
          </div>
          <div class="scroll-table-container">
            <table class="scroll-table">
              <thead>
                <tr>
                  <th>序号</th>
                  <th>产品类型</th>
                  <th>年月</th>
                  <th>销售区</th>
                  <th>销售额(万元)</th>
                  <th>销售额(元)</th>
                </tr>
              </thead>
              <div class="scroll-table-content">
                <tbody ref="boardTableBody">
                  <tr :class="item.sort % 2 === 0 ? 'evenTableTr' : 'oddTableTr'"
                      v-for="(item, index) in boardSalesData"
                  <tr :class="(index + 1) % 2 === 0 ? 'evenTableTr' : 'oddTableTr'"
                      v-for="(item, index) in filteredAmountSalesData"
                      :key="item.period + item.area + index">
                    <td>{{ item.sort }}</td>
                    <td>{{ index + 1 }}</td>
                    <td>{{ item.productType }}</td>
                    <td>{{ item.period }}</td>
                    <td>{{ item.area }}</td>
                    <td>{{ item.sales }}</td>
@@ -259,7 +261,7 @@
          </div>
          <div class="panel-summary-row">
            <div class="summary-label">合计</div>
            <div class="summary-value2">127384 ä¸‡å…ƒ</div>
            <div class="summary-value2">{{ filteredAmountSalesTotal }} å…ƒ</div>
          </div>
        </div>
      </div>
@@ -283,6 +285,8 @@
  import {
    getDashboardStatistics,
    getCustomerTrends,
    getSalesAnalysisTrend,
    getSalesAmountAnalysis,
  } from "@/api/reportAnalysis/salesStatistics";
  const router = useRouter();
@@ -343,10 +347,21 @@
  const boardTableBody = ref(null);
  // é€‰æ‹©å™¨æ•°æ®
  const blockTimeDimension = ref("year");
  const blockSelectedArea = ref("全部");
  const blockProductType = ref("砌块");
  const boardTimeDimension = ref("year");
  // é”€é‡åˆ†æžè¶‹åŠ¿å›¾
  const chartTimeDimension = ref("å¹´");
  const chartTimeDimension2 = ref("å¹´");
  const chartSelectedArea = ref("全部");
  const chartProductType = ref("砌块");
  const chartProductType2 = ref("砌块");
  // é”€é‡æ•°æ®ç»Ÿè®¡
  const tableTimeDimension = ref("å¹´");
  const tableSelectedArea = ref("全部");
  const tableSelectedArea2 = ref("全部");
  const tableProductType = ref("砌块");
  const boardTimeDimension = ref("å¹´");
  const boardSelectedArea = ref("全部");
  const boardProductType = ref("板材");
  const customerTimeDimension = ref("å¹´");
@@ -675,6 +690,14 @@
  // å®¢æˆ·è¶‹åŠ¿æ•°æ®
  const customerTrendsData = ref([]);
  // é”€é‡åˆ†æžè¶‹åŠ¿æ•°æ®
  const salesAnalysisTrendData = ref([]);
  // é”€é‡æ•°æ®ç»Ÿè®¡è¡¨æ ¼æ•°æ®
  const tableSalesData = ref([]);
  // é”€é‡æ•°æ®ç»Ÿè®¡è¡¨æ ¼æ€»è®¡
  const tableSalesTotal = ref(0);
  // åŠ¨æ€é”€å”®åŒºåŸŸåˆ—è¡¨
  const tableSalesAreas = ref([]);
  // å˜åŒ–率计算(模拟)
  const salesVolumeChange = ref("+5.2");
@@ -717,6 +740,126 @@
    }
  };
  // èŽ·å–é”€é‡åˆ†æžè¶‹åŠ¿æ•°æ®
  const fetchSalesAnalysisTrendData = async () => {
    try {
      const response = await getSalesAnalysisTrend({
        type: chartProductType.value, // ç Œå—或板材
        days: chartTimeDimension.value, // å¹´æˆ–月
      });
      if (response && response.data) {
        // API返回的数据结构如下:
        // {
        //   "dates": ["2026-01-01", "2025-01-01", ...],
        //   "customerTrends": [{"内蒙古": 470, "银川": 3600, ...}, ...]
        // }
        salesAnalysisTrendData.value = response.data;
        updateCharts();
      }
    } catch (error) {
      console.error("获取销量分析趋势数据失败:", error);
    }
  };
  // èŽ·å–é”€é‡æ•°æ®ç»Ÿè®¡è¡¨æ ¼æ•°æ®
  const fetchTableSalesData = async () => {
    try {
      const response = await getSalesAnalysisTrend({
        type: tableProductType.value, // ç Œå—或板材
        days: tableTimeDimension.value, // å¹´æˆ–月
      });
      if (response && response.data) {
        // API返回的数据结构如下:
        // {
        //   "dates": ["2026-01-01", "2025-01-01", ...],
        //   "customerTrends": [{"内蒙古": 470, "银川": 3600, ...}, ...]
        // }
        updateTableSalesData(response.data);
      }
    } catch (error) {
      console.error("获取销量数据统计表格数据失败:", error);
    }
  };
  // æ›´æ–°é”€é‡æ•°æ®ç»Ÿè®¡è¡¨æ ¼
  const updateTableSalesData = data => {
    if (!data || !data.dates || !data.customerTrends) {
      return;
    }
    console.log(data, "datas");
    const dates = data.dates;
    const customerTrends = data.customerTrends;
    const tableData = [];
    let total = 0;
    const areaSet = new Set();
    dates.forEach((date, index) => {
      const trend = customerTrends[index];
      if (trend) {
        // æå–所有销售区域
        Object.keys(trend).forEach(area => {
          if (area !== "全部") {
            areaSet.add(area);
            const sales = trend[area] || 0;
            tableData.push({
              period: date,
              area: area,
              productType: tableProductType.value,
              sales: sales,
              sort: tableData.length + 1,
            });
            total += sales;
          }
        });
      }
    });
    // æ›´æ–°é”€å”®åŒºåŸŸåˆ—表,添加"全部"选项
    tableSalesAreas.value = ["全部", ...Array.from(areaSet)];
    // ç¡®ä¿ tableSelectedArea åœ¨é”€å”®åŒºåŸŸåˆ—表中
    if (
      tableSalesAreas.value.length > 0 &&
      !tableSalesAreas.value.includes(tableSelectedArea.value)
    ) {
      tableSelectedArea.value = "全部";
    }
    console.log(tableData);
    tableSalesData.value = tableData;
    tableSalesTotal.value = total;
  };
  // å¤„理图表时间维度变化
  const handleChartTimeDimensionChange = dimension => {
    chartTimeDimension.value = dimension;
    fetchSalesAnalysisTrendData();
  };
  // å¤„理图表产品类型变化
  const handleChartProductTypeChange = type => {
    chartProductType.value = type;
    fetchSalesAnalysisTrendData();
  };
  // å¤„理表格时间维度变化
  const handleTableTimeDimensionChange = dimension => {
    tableTimeDimension.value = dimension;
    fetchTableSalesData();
    // é‡æ–°å¯åŠ¨æ»šåŠ¨ï¼Œæ ¹æ®æ—¶é—´ç»´åº¦å†³å®šæ˜¯å¦æ»šåŠ¨
    startBlockTableScroll();
  };
  // å¤„理表格产品类型变化
  const handleTableProductTypeChange = type => {
    tableProductType.value = type;
    fetchTableSalesData();
  };
  // å¤„理表格销售区域变化
  const handleTableAreaChange = area => {
    tableSelectedArea.value = area;
  };
  // è¡¨æ ¼æ•°æ®
  const tableData = computed(() => {
    return filteredData.value.map(item => {
@@ -734,17 +877,45 @@
    });
  });
  // ç­›é€‰åŽçš„表格数据
  const filteredTableSalesData = computed(() => {
    if (tableSelectedArea.value === "全部") {
      // æŒ‰å¹´æœˆåˆ†ç»„汇总数据
      const groupedData = {};
      tableSalesData.value.forEach(item => {
        const key = item.period;
        if (!groupedData[key]) {
          groupedData[key] = {
            period: item.period,
            area: "全部",
            productType: item.productType,
            sales: 0,
            sort: 0,
          };
        }
        groupedData[key].sales += item.sales;
      });
      // è½¬æ¢ä¸ºæ•°ç»„并按年月排序
      return Object.values(groupedData).sort((a, b) => {
        return new Date(b.period) - new Date(a.period);
      });
    } else {
      return tableSalesData.value.filter(
        item => item.area === tableSelectedArea.value
      );
    }
  });
  // ç­›é€‰åŽçš„表格数据总计
  const filteredTableSalesTotal = computed(() => {
    return filteredTableSalesData.value.reduce(
      (total, item) => total + item.sales,
      0
    );
  });
  // é”€é‡è¶‹åŠ¿å›¾è¡¨é…ç½®
  const salesVolumeChartOption = computed(() => {
    // ä¸ºæ¯ä¸ªé”€å”®åŒºç”Ÿæˆæ•°æ®
    const salesAreas = [
      "全部",
      "A销售区",
      "B销售区",
      "C销售区",
      "D销售区",
      "E销售区",
    ];
    const colors = [
      "#00A4ED",
      "#34D8F7",
@@ -752,54 +923,111 @@
      "#8A6BFF",
      "#C8C447",
      "#FF6B6B",
      "#FF9500",
      "#4CD964",
      "#5AC8FA",
    ];
    const year = 2024;
    const periodType = blockTimeDimension.value;
    // ç”Ÿæˆæ—¶é—´æ®µ
    let periods = [];
    if (periodType === "year") {
      // å¹´åº¦æ•°æ®ï¼š12个月
      for (let month = 1; month <= 12; month++) {
        periods.push(`${year}-${month.toString().padStart(2, "0")}`);
    let salesAreas = [];
    let series = [];
    if (
      salesAnalysisTrendData.value &&
      salesAnalysisTrendData.value.dates &&
      salesAnalysisTrendData.value.customerTrends
    ) {
      // ä½¿ç”¨API返回的日期
      periods = salesAnalysisTrendData.value.dates;
      // æå–销售区域
      const customerTrends = salesAnalysisTrendData.value.customerTrends;
      if (customerTrends.length > 0) {
        // æå–销售区域并确保"全部"在第一个位置
        const allAreas = Object.keys(customerTrends[0]);
        salesAreas = allAreas.sort((a, b) => {
          if (a === "全部") return -1;
          if (b === "全部") return 1;
          return 0;
        });
        // ä¸ºæ¯ä¸ªé”€å”®åŒºç”Ÿæˆæ•°æ®
        series = salesAreas.map((area, index) => {
          const data = customerTrends.map(trend => trend[area] || 0);
          return {
            name: area,
            data: data,
            type: "line",
            smooth: false,
            // symbolSize: getResponsiveValue(8),
            lineStyle: {
              width: getResponsiveValue(1),
              color: colors[index % colors.length],
            },
            itemStyle: { color: colors[index % colors.length] },
            areaStyle: {
              opacity: 0.4,
              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                { offset: 0, color: colors[index % colors.length] + "80" },
                { offset: 1, color: colors[index % colors.length] + "00" },
              ]),
            },
          };
        });
      }
    } else {
      // æœˆåº¦æ•°æ®ï¼š30天
      const month = 1;
      for (let day = 1; day <= 30; day++) {
        periods.push(
          `${year}-${month.toString().padStart(2, "0")}-${day
            .toString()
            .padStart(2, "0")}`
        );
      // æ¨¡æ‹Ÿæ•°æ®
      salesAreas = [
        "全部",
        "A销售区",
        "B销售区",
        "C销售区",
        "D销售区",
        "E销售区",
      ];
      const year = 2024;
      if (periodType === "year") {
        // å¹´åº¦æ•°æ®ï¼š12个月
        for (let month = 1; month <= 12; month++) {
          periods.push(`${year}-${month.toString().padStart(2, "0")}`);
        }
      } else {
        // æœˆåº¦æ•°æ®ï¼š30天
        const month = 1;
        for (let day = 1; day <= 30; day++) {
          periods.push(
            `${year}-${month.toString().padStart(2, "0")}-${day
              .toString()
              .padStart(2, "0")}`
          );
        }
      }
    }
      // ä¸ºæ¯ä¸ªé”€å”®åŒºç”Ÿæˆæ•°æ®
      series = salesAreas.map((area, index) => {
        const data = periods.map(() => {
          return periodType === "year"
            ? Math.floor(Math.random() * 500) + 800
            : Math.floor(Math.random() * 50) + 20;
        });
    // ä¸ºæ¯ä¸ªé”€å”®åŒºç”Ÿæˆæ•°æ®
    const series = salesAreas.map((area, index) => {
      const data = periods.map(() => {
        return periodType === "year"
          ? Math.floor(Math.random() * 500) + 800
          : Math.floor(Math.random() * 50) + 20;
        return {
          name: area,
          data: data,
          type: "line",
          smooth: false,
          // symbolSize: getResponsiveValue(8),
          lineStyle: { width: getResponsiveValue(1), color: colors[index] },
          itemStyle: { color: colors[index] },
          areaStyle: {
            opacity: 0.4,
            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
              { offset: 0, color: colors[index] + "80" },
              { offset: 1, color: colors[index] + "00" },
            ]),
          },
        };
      });
      return {
        name: area,
        data: data,
        type: "line",
        smooth: false,
        // symbolSize: getResponsiveValue(8),
        lineStyle: { width: getResponsiveValue(1), color: colors[index] },
        itemStyle: { color: colors[index] },
        areaStyle: {
          opacity: 0.4,
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: colors[index] + "80" },
            { offset: 1, color: colors[index] + "00" },
          ]),
        },
      };
    });
    }
    return {
      backgroundColor: "transparent",
@@ -864,15 +1092,20 @@
  // é”€å”®é‡‘额趋势图表配置
  const salesAmountChartOption = computed(() => {
    // ä¸ºæ¯ä¸ªé”€å”®åŒºç”Ÿæˆæ•°æ®
    const salesAreas = [
      "全部",
      "A销售区",
      "B销售区",
      "C销售区",
      "D销售区",
      "E销售区",
    ];
    const { dates = [], customerTrends = [] } = salesAmountChartData.value;
    // æå–所有销售区域
    const areaSet = new Set();
    customerTrends.forEach(item => {
      Object.keys(item).forEach(key => areaSet.add(key));
    });
    // ç¡®ä¿"全部"在第一个位置
    const salesAreas = Array.from(areaSet).sort((a, b) => {
      if (a === "全部") return -1;
      if (b === "全部") return 1;
      return 0;
    });
    const colors = [
      "#00A4ED",
      "#34D8F7",
@@ -881,48 +1114,26 @@
      "#C8C447",
      "#FF6B6B",
    ];
    const year = 2024;
    const periodType = boardTimeDimension.value;
    // ç”Ÿæˆæ—¶é—´æ®µ
    let periods = [];
    if (periodType === "year") {
      // å¹´åº¦æ•°æ®ï¼š12个月
      for (let month = 1; month <= 12; month++) {
        periods.push(`${year}-${month.toString().padStart(2, "0")}`);
      }
    } else {
      // æœˆåº¦æ•°æ®ï¼š30天
      const month = 1;
      for (let day = 1; day <= 30; day++) {
        periods.push(
          `${year}-${month.toString().padStart(2, "0")}-${day
            .toString()
            .padStart(2, "0")}`
        );
      }
    }
    // ä¸ºæ¯ä¸ªé”€å”®åŒºç”Ÿæˆæ•°æ®
    const series = salesAreas.map((area, index) => {
      const data = periods.map(() => {
        return periodType === "year"
          ? Math.floor(Math.random() * 50000) + 80000
          : Math.floor(Math.random() * 5000) + 2000;
      });
      const data = customerTrends.map(item => item[area] || 0);
      return {
        name: area,
        data: data,
        type: "bar",
        smooth: true,
        lineStyle: { width: getResponsiveValue(3), color: colors[index] },
        itemStyle: { color: colors[index] },
        lineStyle: {
          width: getResponsiveValue(3),
          color: colors[index % colors.length],
        },
        itemStyle: { color: colors[index % colors.length] },
        areaStyle: {
          opacity: 0.2,
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: colors[index] + "80" },
            { offset: 1, color: colors[index] + "00" },
            { offset: 0, color: colors[index % colors.length] + "80" },
            { offset: 1, color: colors[index % colors.length] + "00" },
          ]),
        },
      };
@@ -937,7 +1148,7 @@
        borderWidth: getResponsiveValue(1),
        textStyle: { color: "#B8C8E0", fontSize: getResponsiveValue(11) },
        formatter: function (params) {
          let result = params[0].name + "<br/>";
          let result = params[0]?.name + "<br/>" || "";
          params.forEach(param => {
            result += `${param.marker}${param.seriesName}: ${param.value} å…ƒ<br/>`;
          });
@@ -964,7 +1175,7 @@
      },
      xAxis: {
        type: "category",
        data: periods,
        data: dates,
        axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } },
        axisTick: { show: false },
        axisLabel: {
@@ -1164,7 +1375,12 @@
          areaSet.add(key);
        });
      });
      salesAreas = Array.from(areaSet);
      // ç¡®ä¿"全部"在第一个位置
      salesAreas = Array.from(areaSet).sort((a, b) => {
        if (a === "全部") return -1;
        if (b === "全部") return 1;
        return 0;
      });
      // ä¸ºæ¯ä¸ªé”€å”®åŒºåŸŸç”Ÿæˆæ•°æ®
      series = salesAreas.map((area, index) => {
@@ -1382,15 +1598,18 @@
  let boardScrollTimer = null;
  let blockCurrentIndex = 0;
  let boardCurrentIndex = 0;
  const startBlockTableScroll = () => {
    if (blockScrollTimer) {
      clearInterval(blockScrollTimer);
    }
    const scrollTable = () => {
      if (!blockTableBody.value || blockSalesData.value.length === 0) return;
    // åªæœ‰å½“时间维度不是"å¹´"时才启动滚动
    if (tableTimeDimension.value === "å¹´") {
      return;
    }
    const scrollTable = () => {
      if (!blockTableBody.value) return;
      const rows = blockTableBody.value.querySelectorAll("tr");
      if (rows.length === 0) return;
@@ -1403,9 +1622,10 @@
        blockTableBody.value.style.transition = "none";
        blockTableBody.value.style.transform = "translateY(0)";
        const firstItem = blockSalesData.value[0];
        blockSalesData.value.shift();
        blockSalesData.value.push(firstItem);
        // ç›´æŽ¥æ“ä½œDOM,将第一行移到最后
        const firstRow = rows[0];
        blockTableBody.value.removeChild(firstRow);
        blockTableBody.value.appendChild(firstRow);
      }, 500);
    };
@@ -1450,6 +1670,10 @@
      clearInterval(boardScrollTimer);
      boardScrollTimer = null;
    }
    if (amountScrollTimer.value) {
      clearInterval(amountScrollTimer.value);
      amountScrollTimer.value = null;
    }
  };
  // å¤„理时间维度选择
@@ -1462,7 +1686,7 @@
    boardTimeDimension.value = dimension;
    generateBoardSalesData();
  };
  const blockProductType = ref("砌块");
  // å¤„理产品类型选择
  const handleBlockProductTypeChange = type => {
    blockProductType.value = type;
@@ -1473,7 +1697,7 @@
    boardProductType.value = type;
    generateBoardSalesData();
  };
  const blockSelectedArea = ref("全部");
  // å¤„理销售区选择
  const handleBlockAreaChange = area => {
    blockSelectedArea.value = area;
@@ -1490,6 +1714,7 @@
    customerTimeDimension.value = dimension;
    fetchCustomerTrendsData();
  };
  const blockTimeDimension = ref("å¹´");
  // ç”Ÿæˆç Œå—销售数据
  const generateBlockSalesData = () => {
@@ -1622,18 +1847,269 @@
    // èŽ·å–æ•°æ®
    await fetchDashboardData();
    await fetchCustomerTrendsData();
    await fetchSalesAnalysisTrendData();
    await fetchTableSalesData();
    await fetchSalesAmountChartData();
    await fetchSalesAmountTableData();
    // ç­‰å¾…DOM更新后初始化图表
    nextTick(() => {
      initCharts();
      // å¯åŠ¨è¡¨æ ¼æ»šåŠ¨åŠ¨ç”»
      startBlockTableScroll();
      startBoardTableScroll();
      startAmountTableScroll();
    });
    // æ·»åŠ çª—å£å¤§å°å˜åŒ–ç›‘å¬
    window.addEventListener("resize", handleResize);
    document.addEventListener("fullscreenchange", handleFullscreenChange);
  });
  // ç›‘听图表时间维度和产品类型变化
  watch([chartTimeDimension, chartProductType], async () => {
    await fetchSalesAnalysisTrendData();
  });
  // ç›‘听表格时间维度和产品类型变化
  watch([tableTimeDimension, tableProductType], async () => {
    await fetchTableSalesData();
  });
  // é”€å”®é‡‘额分析图表数据(右上)
  const salesAmountChartData = ref({
    dates: [],
    customerTrends: [],
  });
  // é”€å”®é¢æ•°æ®ç»Ÿè®¡è¡¨æ ¼æ•°æ®ï¼ˆå³ä¸‹ï¼‰
  const salesAmountTableData = ref({
    dates: [],
    customerTrends: [],
  });
  // é”€å”®é¢æ•°æ®ç»Ÿè®¡è¡¨æ ¼ç­›é€‰çŠ¶æ€ï¼ˆå³ä¸‹ï¼‰
  const tableTimeDimension2 = ref("å¹´");
  const tableProductType2 = ref("砌块");
  // é”€å”®é¢æ•°æ®ç»Ÿè®¡è¡¨æ ¼æ•°æ®
  const amountSalesData = ref([]);
  const amountScrollTimer = ref(null);
  // èŽ·å–é”€å”®é‡‘é¢åˆ†æžå›¾è¡¨æ•°æ®ï¼ˆå³ä¸Šï¼‰
  const fetchSalesAmountChartData = async () => {
    try {
      const response = await getSalesAmountAnalysis({
        type: chartProductType2.value,
        days: chartTimeDimension2.value,
      });
      if (response?.data) {
        salesAmountChartData.value = response.data;
        updateCharts();
      }
    } catch (error) {
      console.error("获取销售金额分析图表数据失败:", error);
      // ä½¿ç”¨æ¨¡æ‹Ÿæ•°æ®
      salesAmountChartData.value = {
        dates: [
          "2026-01-01",
          "2025-01-01",
          "2024-01-01",
          "2023-01-01",
          "2022-01-01",
        ],
        customerTrends: [
          { å†…蒙古: 100, é“¶å·: 200, è‡ªæ: 300, å…¶ä»–: 150, å…¨éƒ¨: 750 },
          { å†…蒙古: 80, é“¶å·: 180, è‡ªæ: 280, å…¶ä»–: 130, å…¨éƒ¨: 670 },
          { å†…蒙古: 90, é“¶å·: 190, è‡ªæ: 290, å…¶ä»–: 140, å…¨éƒ¨: 710 },
          { å†…蒙古: 70, é“¶å·: 170, è‡ªæ: 270, å…¶ä»–: 120, å…¨éƒ¨: 630 },
          { å†…蒙古: 110, é“¶å·: 210, è‡ªæ: 310, å…¶ä»–: 160, å…¨éƒ¨: 790 },
        ],
      };
    }
  };
  // èŽ·å–é”€å”®é¢æ•°æ®ç»Ÿè®¡è¡¨æ ¼æ•°æ®ï¼ˆå³ä¸‹ï¼‰
  const fetchSalesAmountTableData = async () => {
    try {
      const response = await getSalesAmountAnalysis({
        type: tableProductType2.value,
        days: tableTimeDimension2.value,
      });
      if (response?.data) {
        salesAmountTableData.value = response.data;
        updateAmountSalesData();
      }
    } catch (error) {
      console.error("获取销售额数据统计表格数据失败:", error);
      // ä½¿ç”¨æ¨¡æ‹Ÿæ•°æ®
      salesAmountTableData.value = {
        dates: [
          "2026-01-01",
          "2025-01-01",
          "2024-01-01",
          "2023-01-01",
          "2022-01-01",
        ],
        customerTrends: [
          { å†…蒙古: 100, é“¶å·: 200, è‡ªæ: 300, å…¶ä»–: 150, å…¨éƒ¨: 750 },
          { å†…蒙古: 80, é“¶å·: 180, è‡ªæ: 280, å…¶ä»–: 130, å…¨éƒ¨: 670 },
          { å†…蒙古: 90, é“¶å·: 190, è‡ªæ: 290, å…¶ä»–: 140, å…¨éƒ¨: 710 },
          { å†…蒙古: 70, é“¶å·: 170, è‡ªæ: 270, å…¶ä»–: 120, å…¨éƒ¨: 630 },
          { å†…蒙古: 110, é“¶å·: 210, è‡ªæ: 310, å…¶ä»–: 160, å…¨éƒ¨: 790 },
        ],
      };
      updateAmountSalesData();
    }
  };
  // æ›´æ–°é”€å”®é‡‘额分析表格数据
  const updateAmountSalesData = () => {
    const data = [];
    const { dates, customerTrends } = salesAmountTableData.value;
    // æå–所有销售区域
    const areaSet = new Set();
    customerTrends.forEach(item => {
      Object.keys(item).forEach(key => areaSet.add(key));
    });
    // æ›´æ–°é”€å”®åŒºåŸŸåˆ—表,确保"全部"在第一位
    tableSalesAreas.value = [
      "全部",
      ...Array.from(areaSet).filter(area => area !== "全部"),
    ];
    // ç¡®ä¿é€‰ä¸­çš„区域在列表中
    if (!tableSalesAreas.value.includes(tableSelectedArea.value)) {
      tableSelectedArea.value = "全部";
    }
    // ç”Ÿæˆè¡¨æ ¼æ•°æ®
    dates.forEach((date, index) => {
      const trends = customerTrends[index] || {};
      Object.keys(trends).forEach(area => {
        data.push({
          period: date,
          area: area,
          productType: tableProductType2.value,
          sales: trends[area],
          sort: data.length + 1,
        });
      });
    });
    amountSalesData.value = data;
  };
  // ç­›é€‰åŽçš„销售金额分析表格数据
  const filteredAmountSalesData = computed(() => {
    if (tableSelectedArea.value === "全部") {
      // æŒ‰å¹´æœˆåˆ†ç»„汇总数据
      const groupedData = {};
      amountSalesData.value.forEach(item => {
        const key = item.period;
        if (!groupedData[key]) {
          groupedData[key] = {
            period: item.period,
            area: "全部",
            productType: tableProductType2.value,
            sales: 0,
          };
        }
        groupedData[key].sales += item.sales;
      });
      // è½¬æ¢ä¸ºæ•°ç»„并按年月排序
      return Object.values(groupedData).sort((a, b) => {
        return new Date(b.period) - new Date(a.period);
      });
    } else {
      return amountSalesData.value.filter(
        item => item.area === tableSelectedArea.value
      );
    }
  });
  // é”€å”®é‡‘额分析表格总计
  const filteredAmountSalesTotal = computed(() => {
    return filteredAmountSalesData.value.reduce(
      (total, item) => total + item.sales,
      0
    );
  });
  // å¤„理销售金额分析图表时间维度变化(右上)
  const handleChartTimeDimensionChange2 = dimension => {
    chartTimeDimension2.value = dimension;
    fetchSalesAmountChartData();
  };
  // å¤„理销售金额分析图表产品类型变化(右上)
  const handleChartProductTypeChange2 = type => {
    chartProductType2.value = type;
    fetchSalesAmountChartData();
  };
  // å¤„理销售额数据统计表格时间维度变化(右下)
  const handleTableTimeDimensionChange2 = dimension => {
    tableTimeDimension2.value = dimension;
    fetchSalesAmountTableData();
    // é‡æ–°å¯åŠ¨æ»šåŠ¨ï¼Œæ ¹æ®æ—¶é—´ç»´åº¦å†³å®šæ˜¯å¦æ»šåŠ¨
    startAmountTableScroll();
  };
  // å¤„理销售额数据统计表格产品类型变化(右下)
  const handleTableProductTypeChange2 = type => {
    tableProductType2.value = type;
    fetchSalesAmountTableData();
  };
  // å¤„理销售额数据统计表格销售区变化(右下)
  const handleTableAreaChange2 = area => {
    tableSelectedArea.value = area;
  };
  // å¯åŠ¨é”€å”®é‡‘é¢åˆ†æžè¡¨æ ¼æ»šåŠ¨
  const startAmountTableScroll = () => {
    if (amountScrollTimer.value) {
      clearInterval(amountScrollTimer.value);
    }
    // åªæœ‰å½“时间维度不是"å¹´"时才启动滚动
    if (tableTimeDimension2.value === "å¹´") {
      return;
    }
    const scrollTable = () => {
      if (!boardTableBody.value) return;
      const rows = boardTableBody.value.querySelectorAll("tr");
      if (rows.length === 0) return;
      const rowHeight = rows[0].offsetHeight;
      boardTableBody.value.style.transition = "transform 0.5s ease-in-out";
      boardTableBody.value.style.transform = `translateY(-${rowHeight}px)`;
      setTimeout(() => {
        boardTableBody.value.style.transition = "none";
        boardTableBody.value.style.transform = "translateY(0)";
        // ç›´æŽ¥æ“ä½œDOM,将第一行移到最后
        const firstRow = rows[0];
        boardTableBody.value.removeChild(firstRow);
        boardTableBody.value.appendChild(firstRow);
      }, 500);
    };
    amountScrollTimer.value = setInterval(scrollTable, 2000);
  };
  // ç›‘听销售金额分析图表时间维度和产品类型变化(右上)
  watch([chartTimeDimension2, chartProductType2], async () => {
    // await fetchSalesAmountChartData();
  });
  // ç›‘听销售额数据统计表格时间维度和产品类型变化(右下)
  watch([tableTimeDimension2, tableProductType2], async () => {
    // await fetchSalesAmountTableData();
  });
  // èŽ·å–äº§å“ç±»åž‹æ ‡ç­¾ç±»åž‹
@@ -1958,12 +2434,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);
  }
@@ -2062,7 +2538,7 @@
    font-size: 1.4vh;
    font-weight: 800;
    color: #00a4ed;
    margin-right: 5.8vh;
    margin-right: 1.8vh;
    text-shadow: 0 0 1vh rgba(0, 164, 237, 0.5);
  }
  .diamond {