重构:更新生产统计视图以有条件地呈现材料卡并增强双模式的图表配置
已修改1个文件
437 ■■■■ 文件已修改
src/views/reportAnalysis/productionStatistics/index.vue 437 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionStatistics/index.vue
@@ -61,7 +61,8 @@
                  :class="{ active: blockSelectedMaterial === name }"
                  @click="selectBlockMaterial(name)">{{ name }}</span>
          </div>
          <div class="material-info-card">
          <div v-if="blockSelectedMaterial !== '全部'"
               class="material-info-card">
            <div class="material-icon">
              <svg width="24"
                   height="24"
@@ -168,7 +169,8 @@
                  :class="{ active: boardSelectedMaterial === name }"
                  @click="selectBoardMaterial(name)">{{ name }}</span>
          </div>
          <div class="material-info-card">
          <div v-if="boardSelectedMaterial !== '全部'"
               class="material-info-card">
            <div class="material-icon">
              <svg width="24"
                   height="24"
@@ -341,19 +343,26 @@
    yearlyConsumption: "--",
  });
  /** xMode: time=横轴为时间;material=选「全部」时横轴为各物料名称 */
  const blockCostChartSeries = ref({
    xMode: "time",
    categories: [],
    input: [],
    output: [],
  });
  const boardCostChartSeries = ref({
    xMode: "time",
    categories: [],
    input: [],
    output: [],
  });
  const productionChartSeries = ref({
    categories: [],
    /** single:单线;dual:全部时砌块+板材双线 */
    mode: "single",
    values: [],
    blockValues: [],
    plateValues: [],
  });
  // 固废处理量折线图(/home/productionStatistics/solidWaste)
@@ -391,9 +400,12 @@
  let productionChartInstance = null;
  let customerTrendChartInstance = null;
  let salesRankingChartInstance = null;
  let blockCostChartResizeObserver = null;
  let boardCostChartResizeObserver = null;
  // 生产单耗图表配置(砌块,接口数据)
  const blockCostChartOption = computed(() => {
    const xMode = blockCostChartSeries.value.xMode || "time";
    const periods = blockCostChartSeries.value.categories || [];
    const inputData = blockCostChartSeries.value.input || [];
    const outputData = blockCostChartSeries.value.output || [];
@@ -437,8 +449,8 @@
      grid: {
        left: "1%",
        right: "1%",
        bottom: "1%",
        top: "18%",
        bottom: xMode === "material" ? "0%" : "1%",
        containLabel: true,
      },
      xAxis: {
@@ -449,7 +461,12 @@
        axisLabel: {
          color: "#B8C8E0",
          fontSize: getResponsiveValue(11),
          margin: getResponsiveValue(10),
          margin:
            xMode === "material"
              ? getResponsiveValue(6)
              : getResponsiveValue(10),
          rotate: xMode === "material" && periods.length > 5 ? 28 : 0,
          interval: 0,
        },
        splitLine: { show: false },
      },
@@ -469,6 +486,7 @@
  // 板材单耗图表配置(接口数据,与砌块结构一致)
  const boardCostChartOption = computed(() => {
    const xMode = boardCostChartSeries.value.xMode || "time";
    const periods = boardCostChartSeries.value.categories || [];
    const inputData = boardCostChartSeries.value.input || [];
    const outputData = boardCostChartSeries.value.output || [];
@@ -512,8 +530,8 @@
      grid: {
        left: "1%",
        right: "1%",
        bottom: "1%",
        top: "18%",
        bottom: xMode === "material" ? "0%" : "1%",
        containLabel: true,
      },
      xAxis: {
@@ -524,7 +542,12 @@
        axisLabel: {
          color: "#B8C8E0",
          fontSize: getResponsiveValue(11),
          margin: getResponsiveValue(10),
          margin:
            xMode === "material"
              ? getResponsiveValue(6)
              : getResponsiveValue(10),
          rotate: xMode === "material" && periods.length > 5 ? 28 : 0,
          interval: 0,
        },
        splitLine: { show: false },
      },
@@ -545,26 +568,51 @@
  // 物料生产量分析(接口数据)
  const productionChartOption = computed(() => {
    const periods = productionChartSeries.value.categories || [];
    const values = productionChartSeries.value.values || [];
    const lineColor = "#4A8BFF";
    const seriesName = "产量";
    const series = [
      {
        name: seriesName,
        data: values,
        type: "line",
        smooth: true,
        lineStyle: { width: getResponsiveValue(2), color: lineColor },
        itemStyle: { color: lineColor },
        areaStyle: {
          opacity: 0.3,
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: lineColor + "80" },
            { offset: 1, color: lineColor + "00" },
          ]),
        },
    const mode = productionChartSeries.value.mode || "single";
    const blockLineColor = "#4A8BFF";
    const plateLineColor = "#52C9A0";
    const buildAreaLine = (name, data, lineColor) => ({
      name,
      data,
      type: "line",
      smooth: true,
      lineStyle: { width: getResponsiveValue(2), color: lineColor },
      itemStyle: { color: lineColor },
      areaStyle: {
        opacity: 0.3,
        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
          { offset: 0, color: lineColor + "80" },
          { offset: 1, color: lineColor + "00" },
        ]),
      },
    ];
    });
    let series;
    if (mode === "dual") {
      series = [
        buildAreaLine(
          "砌块",
          productionChartSeries.value.blockValues || [],
          blockLineColor
        ),
        buildAreaLine(
          "板材",
          productionChartSeries.value.plateValues || [],
          plateLineColor
        ),
      ];
    } else {
      const cat = productionCategory.value;
      const seriesName = cat === "砌块" ? "砌块" : "板材";
      series = [
        buildAreaLine(
          seriesName,
          productionChartSeries.value.values || [],
          blockLineColor
        ),
      ];
    }
    return {
      backgroundColor: "transparent",
@@ -575,22 +623,12 @@
        borderWidth: getResponsiveValue(1),
        textStyle: { color: "#B8C8E0", fontSize: getResponsiveValue(11) },
      },
      legend: {
        data: [seriesName],
        top: "10%",
        right: "1%",
        textStyle: {
          color: "#B8C8E0",
          fontSize: getResponsiveValue(9),
        },
        itemWidth: getResponsiveValue(10),
        itemHeight: getResponsiveValue(10),
      },
      legend: { show: false },
      grid: {
        left: "1%",
        right: "1%",
        bottom: "1%",
        top: "28%",
        top: "10%",
        containLabel: true,
      },
      xAxis: {
@@ -857,6 +895,36 @@
    板材: "plateOutput",
  };
  /** 全部:按 dateStr 合并砌块/板材两条序列 */
  /** 将某物料时间序列 chartData 汇总为投入/产出合计(用于「全部」横轴=物料对比) */
  const sumChartDataInputOutput = chartData => {
    const arr = Array.isArray(chartData) ? chartData : [];
    const input = arr.reduce((s, c) => s + (Number(c.inputSum) || 0), 0);
    const output = arr.reduce((s, c) => s + (Number(c.outputSum) || 0), 0);
    return { input, output };
  };
  const mergeBlockPlateProductionSeries = (blockList, plateList) => {
    const blocks = Array.isArray(blockList) ? blockList : [];
    const plates = Array.isArray(plateList) ? plateList : [];
    const blockByDate = new Map(blocks.map(i => [i.dateStr, i]));
    const plateByDate = new Map(plates.map(i => [i.dateStr, i]));
    const dates = [
      ...new Set([...blockByDate.keys(), ...plateByDate.keys()]),
    ].sort();
    return {
      categories: dates,
      blockValues: dates.map(d => {
        const row = blockByDate.get(d);
        return row ? Number(row.blockOutput) || 0 : 0;
      }),
      plateValues: dates.map(d => {
        const row = plateByDate.get(d);
        return row ? Number(row.plateOutput) || 0 : 0;
      }),
    };
  };
  const loadBlockCost = async () => {
    const materialName = blockSelectedMaterial.value;
    if (!materialName) {
@@ -865,13 +933,95 @@
        monthlyConsumption: "--",
        yearlyConsumption: "--",
      };
      blockCostChartSeries.value = { categories: [], input: [], output: [] };
      blockCostChartSeries.value = {
        xMode: "time",
        categories: [],
        input: [],
        output: [],
      };
      updateCharts();
      return;
    }
    const dateType = mapTimeDimensionToDateType(blockTimeDimension.value);
    try {
      const res = await getProductionStatisticsBlocks({ materialName, dateType });
      if (materialName === "全部") {
        const names = blockMaterialList.value.filter(n => n !== "全部");
        if (!names.length) {
          blockMaterialSummary.value = {
            materialName: "全部",
            monthlyConsumption: "--",
            yearlyConsumption: "--",
          };
          blockCostChartSeries.value = {
            xMode: "material",
            categories: [],
            input: [],
            output: [],
          };
          updateCharts();
          return;
        }
        const res = await getProductionStatisticsBlocks({ dateType });
        const rows = Array.isArray(res.data) ? res.data : [];
        const rowByName = new Map(
          rows.filter(r => r && r.materialName).map(r => [r.materialName, r])
        );
        const useBulk = names.every(n => rowByName.has(n));
        let categories;
        let input;
        let output;
        if (useBulk) {
          categories = [];
          input = [];
          output = [];
          for (const name of names) {
            const row = rowByName.get(name);
            const chartData = Array.isArray(row.chartData) ? row.chartData : [];
            const sums = sumChartDataInputOutput(chartData);
            categories.push(name);
            input.push(sums.input);
            output.push(sums.output);
          }
        } else {
          const parts = await Promise.all(
            names.map(async name => {
              const r = await getProductionStatisticsBlocks({
                dateType,
                materialName: name,
              });
              const rows2 = Array.isArray(r.data) ? r.data : [];
              const row =
                rows2.find(x => x.materialName === name) || rows2[0] || {};
              const chartData = Array.isArray(row.chartData) ? row.chartData : [];
              const sums = sumChartDataInputOutput(chartData);
              return { name, ...sums };
            })
          );
          categories = parts.map(p => p.name);
          input = parts.map(p => p.input);
          output = parts.map(p => p.output);
        }
        blockMaterialSummary.value = {
          materialName: "全部",
          monthlyConsumption: "--",
          yearlyConsumption: "--",
        };
        blockCostChartSeries.value = {
          xMode: "material",
          categories,
          input,
          output,
        };
        updateCharts();
        return;
      }
      const res = await getProductionStatisticsBlocks({
        dateType,
        materialName,
      });
      const rows = Array.isArray(res.data) ? res.data : [];
      const row =
        rows.find(r => r.materialName === materialName) || rows[0] || {};
@@ -882,6 +1032,7 @@
        yearlyConsumption: row.yearlyConsumption ?? "--",
      };
      blockCostChartSeries.value = {
        xMode: "time",
        categories: chartData.map(c => c.date),
        input: chartData.map(c => c.inputSum ?? 0),
        output: chartData.map(c => c.outputSum ?? 0),
@@ -900,13 +1051,95 @@
        monthlyConsumption: "--",
        yearlyConsumption: "--",
      };
      boardCostChartSeries.value = { categories: [], input: [], output: [] };
      boardCostChartSeries.value = {
        xMode: "time",
        categories: [],
        input: [],
        output: [],
      };
      updateCharts();
      return;
    }
    const dateType = mapTimeDimensionToDateType(boardTimeDimension.value);
    try {
      const res = await getProductionStatisticsPlates({ materialName, dateType });
      if (materialName === "全部") {
        const names = boardMaterialList.value.filter(n => n !== "全部");
        if (!names.length) {
          boardMaterialSummary.value = {
            materialName: "全部",
            monthlyConsumption: "--",
            yearlyConsumption: "--",
          };
          boardCostChartSeries.value = {
            xMode: "material",
            categories: [],
            input: [],
            output: [],
          };
          updateCharts();
          return;
        }
        const res = await getProductionStatisticsPlates({ dateType });
        const rows = Array.isArray(res.data) ? res.data : [];
        const rowByName = new Map(
          rows.filter(r => r && r.materialName).map(r => [r.materialName, r])
        );
        const useBulk = names.every(n => rowByName.has(n));
        let categories;
        let input;
        let output;
        if (useBulk) {
          categories = [];
          input = [];
          output = [];
          for (const name of names) {
            const row = rowByName.get(name);
            const chartData = Array.isArray(row.chartData) ? row.chartData : [];
            const sums = sumChartDataInputOutput(chartData);
            categories.push(name);
            input.push(sums.input);
            output.push(sums.output);
          }
        } else {
          const parts = await Promise.all(
            names.map(async name => {
              const r = await getProductionStatisticsPlates({
                dateType,
                materialName: name,
              });
              const rows2 = Array.isArray(r.data) ? r.data : [];
              const row =
                rows2.find(x => x.materialName === name) || rows2[0] || {};
              const chartData = Array.isArray(row.chartData) ? row.chartData : [];
              const sums = sumChartDataInputOutput(chartData);
              return { name, ...sums };
            })
          );
          categories = parts.map(p => p.name);
          input = parts.map(p => p.input);
          output = parts.map(p => p.output);
        }
        boardMaterialSummary.value = {
          materialName: "全部",
          monthlyConsumption: "--",
          yearlyConsumption: "--",
        };
        boardCostChartSeries.value = {
          xMode: "material",
          categories,
          input,
          output,
        };
        updateCharts();
        return;
      }
      const res = await getProductionStatisticsPlates({
        dateType,
        materialName,
      });
      const rows = Array.isArray(res.data) ? res.data : [];
      const row =
        rows.find(r => r.materialName === materialName) || rows[0] || {};
@@ -917,6 +1150,7 @@
        yearlyConsumption: row.yearlyConsumption ?? "--",
      };
      boardCostChartSeries.value = {
        xMode: "time",
        categories: chartData.map(c => c.date),
        input: chartData.map(c => c.inputSum ?? 0),
        output: chartData.map(c => c.outputSum ?? 0),
@@ -930,7 +1164,8 @@
  const loadBlockMaterials = async () => {
    try {
      const res = await getProductionMaterials({ materialType: "1" });
      const list = Array.isArray(res.data) ? [...res.data] : [];
      const raw = Array.isArray(res.data) ? [...res.data] : [];
      const list = ["全部", ...raw];
      blockMaterialList.value = list;
      if (list.length) {
        if (!list.includes(blockSelectedMaterial.value)) {
@@ -944,7 +1179,12 @@
          monthlyConsumption: "--",
          yearlyConsumption: "--",
        };
        blockCostChartSeries.value = { categories: [], input: [], output: [] };
        blockCostChartSeries.value = {
          xMode: "time",
          categories: [],
          input: [],
          output: [],
        };
        updateCharts();
      }
    } catch (e) {
@@ -955,7 +1195,8 @@
  const loadBoardMaterials = async () => {
    try {
      const res = await getProductionMaterials({ materialType: "2" });
      const list = Array.isArray(res.data) ? [...res.data] : [];
      const raw = Array.isArray(res.data) ? [...res.data] : [];
      const list = ["全部", ...raw];
      boardMaterialList.value = list;
      if (list.length) {
        if (!list.includes(boardSelectedMaterial.value)) {
@@ -969,7 +1210,12 @@
          monthlyConsumption: "--",
          yearlyConsumption: "--",
        };
        boardCostChartSeries.value = { categories: [], input: [], output: [] };
        boardCostChartSeries.value = {
          xMode: "time",
          categories: [],
          input: [],
          output: [],
        };
        updateCharts();
      }
    } catch (e) {
@@ -983,12 +1229,30 @@
      const res = await getMaterialProductionAnalysis({ dateType });
      const payload = res.data && typeof res.data === "object" ? res.data : {};
      const cat = productionCategory.value;
      const list = Array.isArray(payload[cat]) ? payload[cat] : [];
      const field = productionOutputKey[cat] || "totalOutput";
      productionChartSeries.value = {
        categories: list.map(i => i.dateStr),
        values: list.map(i => Number(i[field]) || 0),
      };
      if (cat === "全部") {
        const merged = mergeBlockPlateProductionSeries(
          payload["砌块"],
          payload["板材"]
        );
        productionChartSeries.value = {
          categories: merged.categories,
          mode: "dual",
          values: [],
          blockValues: merged.blockValues,
          plateValues: merged.plateValues,
        };
      } else {
        const list = Array.isArray(payload[cat]) ? payload[cat] : [];
        const field = productionOutputKey[cat] || "totalOutput";
        productionChartSeries.value = {
          categories: list.map(i => i.dateStr),
          mode: "single",
          values: list.map(i => Number(i[field]) || 0),
          blockValues: [],
          plateValues: [],
        };
      }
      updateCharts();
    } catch (e) {
      console.error(e);
@@ -1094,21 +1358,37 @@
    updateCharts();
  };
  /** 砌块/板材容器高度变化后(如隐藏汇总卡)需 resize,否则底部留白 */
  const resizeCostPanelCharts = () => {
    nextTick(() => {
      requestAnimationFrame(() => {
        blockCostChartInstance?.resize();
        boardCostChartInstance?.resize();
      });
    });
  };
  // 更新图表
  const updateCharts = () => {
    // 更新砌块成本图表
    // 更新砌块成本图表(replaceMerge:全部↔单物料时横轴时间/物料切换)
    if (blockCostChartInstance) {
      blockCostChartInstance.setOption(blockCostChartOption.value);
      blockCostChartInstance.setOption(blockCostChartOption.value, {
        replaceMerge: ["series", "xAxis"],
      });
    }
    // 更新板材成本图表
    if (boardCostChartInstance) {
      boardCostChartInstance.setOption(boardCostChartOption.value);
      boardCostChartInstance.setOption(boardCostChartOption.value, {
        replaceMerge: ["series", "xAxis"],
      });
    }
    // 更新产量分析图表
    // 更新产量分析图表(replaceMerge:全部↔砌块/板材切换时去掉多余折线,避免合并残留)
    if (productionChartInstance) {
      productionChartInstance.setOption(productionChartOption.value);
      productionChartInstance.setOption(productionChartOption.value, {
        replaceMerge: ["series"],
      });
    }
    // 更新新增客户趋势图表
@@ -1120,6 +1400,8 @@
    if (salesRankingChartInstance) {
      salesRankingChartInstance.setOption(salesRankingChartOption.value);
    }
    resizeCostPanelCharts();
  };
  // 处理时间维度选择
@@ -1182,6 +1464,20 @@
    // 等待DOM更新后初始化图表并拉取接口数据
    nextTick(async () => {
      initCharts();
      if (typeof ResizeObserver !== "undefined") {
        if (blockCostChart.value) {
          blockCostChartResizeObserver = new ResizeObserver(() => {
            blockCostChartInstance?.resize();
          });
          blockCostChartResizeObserver.observe(blockCostChart.value);
        }
        if (boardCostChart.value) {
          boardCostChartResizeObserver = new ResizeObserver(() => {
            boardCostChartInstance?.resize();
          });
          boardCostChartResizeObserver.observe(boardCostChart.value);
        }
      }
      await Promise.all([
        loadBlockMaterials(),
        loadBoardMaterials(),
@@ -1190,6 +1486,7 @@
        loadSolidWasteData(),
        loadEnergyData(),
      ]);
      resizeCostPanelCharts();
    });
    // 添加窗口大小变化监听
@@ -1203,6 +1500,11 @@
      clearInterval(timeTicker);
      timeTicker = null;
    }
    blockCostChartResizeObserver?.disconnect();
    blockCostChartResizeObserver = null;
    boardCostChartResizeObserver?.disconnect();
    boardCostChartResizeObserver = null;
    if (blockCostChartInstance) {
      blockCostChartInstance.dispose();
@@ -1610,9 +1912,30 @@
    margin-bottom: 0.2vh;
  }
  /* 砌块/板材:图表占满 Tab 与汇总卡下方的剩余高度;隐藏汇总卡时自动拉高 */
  .bi-panel-top-left,
  .bi-panel-bottom-left {
    min-height: 0;
  }
  .bi-panel-top-left .bi-panel-body,
  .bi-panel-bottom-left .bi-panel-body {
    display: flex;
    flex-direction: column;
    min-height: 0;
  }
  .bi-panel-top-left .chart-filter-tabs,
  .bi-panel-bottom-left .chart-filter-tabs {
    flex-shrink: 0;
  }
  .bi-panel-top-left .echart-fill,
  .bi-panel-bottom-left .echart-fill {
    height: 24vh;
    flex: 1;
    min-height: 0;
    height: auto;
    width: 100%;
  }
  .bi-panel-bottom-right .echart-fill {