Explorar o código

修改下载报告代码

liujiejie hai 3 meses
pai
achega
b613f42b74

+ 3 - 2
.env.dev

@@ -1,7 +1,7 @@
 ###
  # @Author: your name
  # @Date: 2025-07-17 14:14:27
- # @LastEditTime: 2026-04-17 15:09:38
+ # @LastEditTime: 2026-05-11 18:51:59
  # @LastEditors: milo-MacBook-Pro.local
  # @Description: In User Settings Edit
  # @FilePath: /performance-test/.env.dev
@@ -38,7 +38,8 @@ VUE_APP_UPLOAD="http://192.168.50.235/energy-manage-service/api/check/upload"
 VUE_APP_MAPVIEW=/tiles/{z}/{x}/{y}.png
 VUE_APP_MAP=http://192.168.50.235
 # http://192.168.50.235:16200 开发内网地址
-VUE_APP_APIPROXY='http://192.168.50.235:16300' # 生产数据库
+# VUE_APP_APIPROXY='http://192.168.50.235:16300' # 生产数据库
+VUE_APP_APIPROXY='http://192.168.50.235:16300'
 VUE_APP_WZLAPIPROXY='http://192.168.50.241:9002'
 # VUE_APP_ETLAPIPROXY='http://192.168.50.241:9002'
 VUE_APP_ETLAPIPROXY='http://192.168.50.241:9000/transDataWebProd/'

+ 3 - 0
downLoadServer/.env

@@ -15,6 +15,9 @@ MINIO_SECRET_KEY=jtBuqZ80biRWQf6sbwzDQJwHtEBicPjkZBtvjTrA
 # MINIO_SECRET_KEY=idxO5SAjboUYERpDICgHgBoHX7bcYv355lMQANt6
 CHROME_PATH=/Applications/Google Chrome.app/Contents/MacOS/Google Chrome
 CHART_RENDER_TIMEOUT_MS=180000
+CHART_RENDER_CONCURRENCY=2
+CHART_MAX_PAGES=2
+CHART_MAX_COLORBAR_CATEGORIES=50
 #   nginx 配置 minio
 #   env MINIO_ENDPOINT=192.168.50.233;
 #   env MINIO_PORT=6900;

BIN=BIN
downLoadServer/images/temp_heatmap_chart_1778497529006.jpeg


+ 48 - 25
downLoadServer/src/server/controllers/chartController.js

@@ -29,6 +29,11 @@ import Papa from "papaparse";
 import fs from "fs-extra";
 import path from "path";
 import FormData from "form-data";
+import pLimit from "p-limit";
+
+const chartTaskLimit = pLimit(
+  Number.parseInt(process.env.CHART_RENDER_CONCURRENCY || "1", 10),
+);
 
 // 提取公共逻辑到辅助函数
 const handleChartGeneration = async (
@@ -39,8 +44,15 @@ const handleChartGeneration = async (
   additionalParams = {},
 ) => {
   try {
-    const { fileAddr, bucketName, objectName, fieldInfo, fieldEngineCode } =
-      req.body;
+    const {
+      fileAddr,
+      bucketName,
+      objectName,
+      fieldInfo,
+      fieldEngineCode,
+      startDate,
+      endDate,
+    } = req.body;
     if (!fileAddr) {
       return errorResponse(res, "缺少数据URL", 400);
     }
@@ -87,15 +99,25 @@ const handleChartGeneration = async (
       return errorResponse(res, "获取的数据格式不正确", 400);
     }
 
-    // 生成图表并上传
-    const imageUrl = await generateChartFn(
-      data,
-      bucketName,
-      objectName,
-      fieldEngineCode,
-      fieldInfo,
-      ...Object.values(additionalParams),
-    );
+    // 生成图表并上传(折线图等:第 5、6 参应为 startDate/endDate,不能误传 fieldInfo)
+    const { skipFieldInfo, ...restAdditional } = additionalParams || {};
+    const imageUrl = skipFieldInfo
+      ? await generateChartFn(
+          data,
+          bucketName,
+          objectName,
+          fieldEngineCode,
+          startDate,
+          endDate,
+        )
+      : await generateChartFn(
+          data,
+          bucketName,
+          objectName,
+          fieldEngineCode,
+          fieldInfo,
+          ...Object.values(restAdditional),
+        );
     // console.log(imageUrl, "imageUrl");
     successResponse(
       res,
@@ -240,18 +262,20 @@ const handleParsedResult = async (
       if (chartType === "radar") {
         const results = await Promise.all(
           data.map((val) =>
-            onParsed(
-              val,
-              data,
-              bucketName,
-              objectName,
-              fieldInfo,
-              engineTypeCode,
-              additionalParams,
-            ).catch((err) => {
-              console.error("getRadarCharts failed:", err);
-              return null;
-            }),
+            chartTaskLimit(() =>
+              onParsed(
+                val,
+                data,
+                bucketName,
+                objectName,
+                fieldInfo,
+                engineTypeCode,
+                additionalParams,
+              ).catch((err) => {
+                console.error("getRadarCharts failed:", err);
+                return null;
+              }),
+            ),
           ),
         );
         imageUrls.push(...results);
@@ -279,9 +303,8 @@ export const createBarChart = async (req, res) => {
 };
 
 export const createLineChart = async (req, res) => {
-  const { fieldEngineCode } = req.body;
   await handleChartGeneration(req, res, generateLineChart, "折线图生成成功", {
-    fieldEngineCode,
+    skipFieldInfo: true,
   });
 };
 

+ 68 - 17
downLoadServer/src/server/utils/chartService/browserPool.js

@@ -2,7 +2,7 @@ import puppeteer from "puppeteer";
 
 let browser;
 const pagePool = [];
-const MAX_PAGES = 5; // 并发上限(可以调)
+const MAX_PAGES = Number.parseInt(process.env.CHART_MAX_PAGES || "1", 10);
 const DEFAULT_TIMEOUT_MS = Number.parseInt(
   process.env.CHART_RENDER_TIMEOUT_MS || "",
   10,
@@ -11,27 +11,64 @@ const EFFECTIVE_TIMEOUT_MS = Number.isFinite(DEFAULT_TIMEOUT_MS)
   ? DEFAULT_TIMEOUT_MS
   : 500000;
 
+const isBrowserAlive = () => browser && browser.isConnected();
+
+const clearPagePool = async () => {
+  const pages = pagePool.splice(0, pagePool.length);
+  await Promise.allSettled(
+    pages.map(async (page) => {
+      if (page && !page.isClosed()) await page.close();
+    }),
+  );
+};
+
+const createPage = async () => {
+  if (!isBrowserAlive()) {
+    await initBrowserPool();
+  }
+
+  const page = await browser.newPage();
+  page.setDefaultTimeout(EFFECTIVE_TIMEOUT_MS);
+  page.setDefaultNavigationTimeout(EFFECTIVE_TIMEOUT_MS);
+  return page;
+};
+
+const isUsablePage = (page) =>
+  page && !page.isClosed() && page.browser()?.isConnected();
+
 /**
  * 初始化浏览器 + 预创建 page
  */
 export const initBrowserPool = async () => {
-  if (!browser) {
+  if (!isBrowserAlive()) {
+    await clearPagePool();
+    browser = null;
+
     browser = await puppeteer.launch({
       headless: "new",
       executablePath: process.env.CHROME_PATH || undefined,
-      args: ["--no-sandbox", "--disable-setuid-sandbox"],
+      args: [
+        "--no-sandbox",
+        "--disable-setuid-sandbox",
+        "--disable-dev-shm-usage",
+        "--disable-background-timer-throttling",
+        "--disable-backgrounding-occluded-windows",
+        "--disable-renderer-backgrounding",
+      ],
       // 避免渲染压力下 CDP 调用超时(对应报错:Runtime.callFunctionOn timed out)
       protocolTimeout: EFFECTIVE_TIMEOUT_MS,
     });
 
+    browser.on("disconnected", () => {
+      browser = null;
+      pagePool.splice(0, pagePool.length);
+    });
+
     console.log("✅ Browser initialized");
 
     // 预创建 page
     for (let i = 0; i < MAX_PAGES; i++) {
-      const page = await browser.newPage();
-      page.setDefaultTimeout(EFFECTIVE_TIMEOUT_MS);
-      page.setDefaultNavigationTimeout(EFFECTIVE_TIMEOUT_MS);
-      pagePool.push(page);
+      pagePool.push(await createPage());
     }
 
     console.log(`✅ Page pool initialized: ${MAX_PAGES}`);
@@ -42,19 +79,20 @@ export const initBrowserPool = async () => {
  * 获取 page(核心)
  */
 export const getPage = async () => {
-  if (!browser) {
-    throw new Error("Browser not initialized");
+  if (!isBrowserAlive()) {
+    await initBrowserPool();
   }
 
   if (pagePool.length > 0) {
-    return pagePool.pop();
+    while (pagePool.length > 0) {
+      const page = pagePool.pop();
+      if (isUsablePage(page)) return page;
+      await discardPage(page);
+    }
   }
 
   // pool 空了就新建(兜底)
-  const page = await browser.newPage();
-  page.setDefaultTimeout(EFFECTIVE_TIMEOUT_MS);
-  page.setDefaultNavigationTimeout(EFFECTIVE_TIMEOUT_MS);
-  return page;
+  return createPage();
 };
 
 /**
@@ -64,6 +102,11 @@ export const releasePage = async (page) => {
   if (!page) return;
 
   try {
+    if (!isUsablePage(page)) {
+      await discardPage(page);
+      return;
+    }
+
     if (pagePool.length < MAX_PAGES) {
       pagePool.push(page);
     } else {
@@ -74,14 +117,22 @@ export const releasePage = async (page) => {
   }
 };
 
+export const discardPage = async (page) => {
+  if (!page) return;
+
+  try {
+    if (!page.isClosed()) await page.close();
+  } catch (err) {
+    console.warn("⚠️ discardPage failed:", err.message);
+  }
+};
+
 /**
  * 关闭
  */
 export const closeBrowserPool = async () => {
   try {
-    for (const page of pagePool) {
-      await page.close();
-    }
+    await clearPagePool();
 
     if (browser) {
       await browser.close();

+ 9 - 140
downLoadServer/src/server/utils/chartService/index.js

@@ -1,142 +1,5 @@
-// /*
-//  * Chart Service - 生产级版本
-//  */
-
-// import { getPage, releasePage } from "./browserPool.js";
-// import { buildHTML } from "./chartRenderer.js";
-// import { uploadImage } from "./uploader.js";
-// import { chartLimit } from "./limiter.js";
-// import { initBrowserPool, closeBrowserPool } from "./browserPool.js";
-// import { initPlotly } from "./plotlyLoader.js";
-
-// /**
-//  * 初始化
-//  */
-// let initialized = false;
-
-// export const initChartService = async () => {
-//   if (initialized) return;
-
-//   await initBrowserPool();
-//   await initPlotly();
-
-//   initialized = true;
-
-//   console.log("🚀 Chart service ready");
-// };
-
-// /**
-//  * 关闭
-//  */
-// export const shutdownChartService = async () => {
-//   try {
-//     await closeBrowserPool();
-//     initialized = false;
-//     console.log("🛑 Chart service shutdown complete");
-//   } catch (err) {
-//     console.error("❌ Shutdown error:", err);
-//   }
-// };
-
-// /**
-//  * 核心渲染函数(带重试)
-//  */
-// export async function renderChart({
-//   traces,
-//   layout,
-//   bucketName,
-//   objectName,
-//   retry = 2, // 默认重试2次
-// }) {
-//   return chartLimit(async () => {
-//     let attempt = 0;
-
-//     while (attempt <= retry) {
-//       const page = await getPage();
-
-//       try {
-//         const html = buildHTML(traces, layout);
-
-//         /**
-//          * ✅ 重置页面(关键)
-//          */
-//         await page.goto("about:blank");
-
-//         await page.setContent(html, {
-//           waitUntil: "domcontentloaded",
-//         });
-
-//         /**
-//          * ✅ 等待渲染完成(带超时)
-//          */
-//         await Promise.race([
-//           page.waitForFunction(() => window.chartReady === true),
-//           page.waitForTimeout(30000),
-//         ]);
-
-//         /**
-//          * ✅ 检查错误
-//          */
-//         const hasError = await page.evaluate(() => {
-//           return window.chartError || false;
-//         });
-
-//         if (hasError) {
-//           throw new Error("Chart render failed in browser");
-//         }
-
-//         /**
-//          * ✅ 获取元素
-//          */
-//         const element = await page.$("#chart");
-
-//         if (!element) {
-//           throw new Error("Chart element not found");
-//         }
-
-//         /**
-//          * ✅ 截图
-//          */
-//         const buffer = await element.screenshot({
-//           type: "jpeg",
-//           quality: 90,
-//         });
-
-//         /**
-//          * ✅ 上传
-//          */
-//         const url = await uploadImage(buffer, bucketName, objectName);
-
-//         return url;
-//       } catch (err) {
-//         attempt++;
-
-//         console.error(
-//           `❌ renderChart error (attempt ${attempt}):`,
-//           err.message,
-//         );
-
-//         if (attempt > retry) {
-//           throw err;
-//         }
-
-//         // 👉 小延迟再重试(防抖)
-//         await new Promise((r) => setTimeout(r, 1000));
-//       } finally {
-//         try {
-//           if (page && !page.isClosed()) {
-//             await releasePage(page); // 只放回健康 page
-//           }
-//         } catch (err) {
-//           console.warn("page release failed, closing:", err);
-//           if (page && !page.isClosed()) await page.close();
-//         }
-//       }
-//     }
-//   });
-// }
 // src/server/utils/chartService/index.js
-import { getPage, releasePage } from "./browserPool.js";
+import { discardPage, getPage, releasePage } from "./browserPool.js";
 import { buildHTML } from "./chartRenderer.js";
 import { chartLimit } from "./limiter.js";
 import { initBrowserPool, closeBrowserPool } from "./browserPool.js";
@@ -194,6 +57,7 @@ export async function renderChart({
 
     while (attempt <= retry) {
       const page = await getPage();
+      let shouldDiscardPage = false;
 
       try {
         const html = buildHTML(traces, layout);
@@ -227,6 +91,7 @@ export async function renderChart({
         const url = await uploadBufferToMinIO(bucketName, objectName, buffer);
         return url;
       } catch (err) {
+        shouldDiscardPage = true;
         attempt++;
         console.error(
           `❌ renderChart error (attempt ${attempt}):`,
@@ -236,10 +101,14 @@ export async function renderChart({
         await new Promise((r) => setTimeout(r, 1000));
       } finally {
         try {
-          if (page && !page.isClosed()) await releasePage(page);
+          if (shouldDiscardPage) {
+            await discardPage(page);
+          } else if (page && !page.isClosed()) {
+            await releasePage(page);
+          }
         } catch (err) {
           console.warn("page release failed, closing:", err);
-          if (page && !page.isClosed()) await page.close();
+          await discardPage(page);
         }
       }
     }

+ 6 - 1
downLoadServer/src/server/utils/chartService/limiter.js

@@ -8,4 +8,9 @@
  */
 import pLimit from "p-limit";
 
-export const chartLimit = pLimit(4);
+const renderConcurrency = Number.parseInt(
+  process.env.CHART_RENDER_CONCURRENCY || "1",
+  10,
+);
+
+export const chartLimit = pLimit(Number.isFinite(renderConcurrency) ? renderConcurrency : 1);

+ 73 - 5
downLoadServer/src/server/utils/chartsCom/BoxLineCharts.js

@@ -12,6 +12,68 @@
  */
 import { renderChart } from "../chartService/index.js";
 
+const normalizeBoxXValue = (x) => {
+  const date = new Date(x);
+  if (!Number.isNaN(date.getTime())) {
+    return date.toISOString().slice(0, 10);
+  }
+  return String(x);
+};
+
+const quantile = (sortedValues, p) => {
+  if (!sortedValues.length) return null;
+  const index = (sortedValues.length - 1) * p;
+  const lower = Math.floor(index);
+  const upper = Math.ceil(index);
+  if (lower === upper) return sortedValues[lower];
+  return (
+    sortedValues[lower] +
+    (sortedValues[upper] - sortedValues[lower]) * (index - lower)
+  );
+};
+
+const buildBoxStats = (xData = [], yData = []) => {
+  const grouped = new Map();
+  const len = Math.min(xData.length, yData.length);
+
+  for (let i = 0; i < len; i++) {
+    const x = xData[i];
+    const y = Number(yData[i]);
+    if (x === null || x === undefined || x === "") continue;
+    if (!Number.isFinite(y)) continue;
+
+    const key = normalizeBoxXValue(x);
+    if (!grouped.has(key)) grouped.set(key, []);
+    grouped.get(key).push(y);
+  }
+
+  const x = [];
+  const q1 = [];
+  const median = [];
+  const q3 = [];
+  const lowerfence = [];
+  const upperfence = [];
+  const mean = [];
+
+  [...grouped.entries()]
+    .sort(([a], [b]) => a.localeCompare(b))
+    .forEach(([key, values]) => {
+      const sortedValues = values.sort((a, b) => a - b);
+      x.push(key);
+      q1.push(quantile(sortedValues, 0.25));
+      median.push(quantile(sortedValues, 0.5));
+      q3.push(quantile(sortedValues, 0.75));
+      lowerfence.push(sortedValues[0]);
+      upperfence.push(sortedValues[sortedValues.length - 1]);
+      mean.push(
+        sortedValues.reduce((sum, value) => sum + value, 0) /
+          sortedValues.length,
+      );
+    });
+
+  return { x, q1, median, q3, lowerfence, upperfence, mean };
+};
+
 /**
  * 生成箱线图并上传
  */
@@ -20,11 +82,17 @@ export const generateBoxLineChart = async (data, bucketName, objectName) => {
 
   // ✅ 数据过滤(保留你的逻辑)
   const filteredData = filterData(chartDataset);
+  const stats = buildBoxStats(filteredData.xData, filteredData.yData);
 
   // ✅ 主箱线图
   const trace = {
-    x: filteredData.xData,
-    y: filteredData.yData,
+    x: stats.x,
+    q1: stats.q1,
+    median: stats.median,
+    q3: stats.q3,
+    lowerfence: stats.lowerfence,
+    upperfence: stats.upperfence,
+    mean: stats.mean,
     type: "box",
     marker: {
       color: "#263649",
@@ -42,10 +110,10 @@ export const generateBoxLineChart = async (data, bucketName, objectName) => {
   // ✅ 中位点(可选)
   const traces = [trace];
 
-  if (filteredData.medians?.x?.length) {
+  if (stats.x.length) {
     traces.push({
-      x: filteredData.medians.x,
-      y: filteredData.medians.y,
+      x: stats.x,
+      y: stats.median,
       mode: "markers",
       marker: {
         color: "#406DAB",

+ 89 - 18
downLoadServer/src/server/utils/chartsCom/BoxMarkersCharts.js

@@ -19,7 +19,72 @@ const isInDateRange = (timestamp, startDate, endDate) => {
   if (!startDate || !endDate) return true;
 
   const t = new Date(timestamp).getTime();
-  return t >= new Date(startDate).getTime() && t <= new Date(endDate).getTime();
+  const start = new Date(startDate).getTime();
+  const end = new Date(endDate).getTime();
+  if (Number.isNaN(t) || Number.isNaN(start) || Number.isNaN(end)) return true;
+  return t >= start && t <= end;
+};
+
+const normalizeBoxXValue = (x) => {
+  const date = new Date(x);
+  if (!Number.isNaN(date.getTime())) {
+    return date.toISOString().slice(0, 10);
+  }
+  return String(x);
+};
+
+const quantile = (sortedValues, p) => {
+  if (!sortedValues.length) return null;
+  const index = (sortedValues.length - 1) * p;
+  const lower = Math.floor(index);
+  const upper = Math.ceil(index);
+  if (lower === upper) return sortedValues[lower];
+  return (
+    sortedValues[lower] +
+    (sortedValues[upper] - sortedValues[lower]) * (index - lower)
+  );
+};
+
+const buildBoxStats = (xData = [], yData = []) => {
+  const grouped = new Map();
+  const len = Math.min(xData.length, yData.length);
+
+  for (let i = 0; i < len; i++) {
+    const x = xData[i];
+    const y = Number(yData[i]);
+    if (x === null || x === undefined || x === "") continue;
+    if (!Number.isFinite(y)) continue;
+
+    const key = normalizeBoxXValue(x);
+    if (!grouped.has(key)) grouped.set(key, []);
+    grouped.get(key).push(y);
+  }
+
+  const x = [];
+  const q1 = [];
+  const median = [];
+  const q3 = [];
+  const lowerfence = [];
+  const upperfence = [];
+  const mean = [];
+
+  [...grouped.entries()]
+    .sort(([a], [b]) => a.localeCompare(b))
+    .forEach(([key, values]) => {
+      const sortedValues = values.sort((a, b) => a - b);
+      x.push(key);
+      q1.push(quantile(sortedValues, 0.25));
+      median.push(quantile(sortedValues, 0.5));
+      q3.push(quantile(sortedValues, 0.75));
+      lowerfence.push(sortedValues[0]);
+      upperfence.push(sortedValues[sortedValues.length - 1]);
+      mean.push(
+        sortedValues.reduce((sum, value) => sum + value, 0) /
+          sortedValues.length,
+      );
+    });
+
+  return { x, q1, median, q3, lowerfence, upperfence, mean };
 };
 
 /**
@@ -79,10 +144,18 @@ export const generateBoxMarkersCharts = async (
   const traces = [];
 
   filteredData.forEach((group) => {
+    const stats = buildBoxStats(group.xData, group.yData);
+    if (!stats.x.length) return;
+
     // ✅ 箱线图
     traces.push({
-      x: group.xData,
-      y: group.yData,
+      x: stats.x,
+      q1: stats.q1,
+      median: stats.median,
+      q3: stats.q3,
+      lowerfence: stats.lowerfence,
+      upperfence: stats.upperfence,
+      mean: stats.mean,
       type: "box",
       name: group.title,
       marker: { color: "#263649" },
@@ -102,21 +175,19 @@ export const generateBoxMarkersCharts = async (
     });
 
     // ✅ 中位点 scatter
-    if (group.medians?.x?.length) {
-      traces.push({
-        x: group.medians.x,
-        y: group.medians.y,
-        mode: "markers",
-        marker: { color: "#f00", size: 3 },
-        name: `${group.title} - 中位点`,
-        type: "scatter",
-        hovertemplate:
-          `<b>${xaixs}</b>: %{x}<br>` +
-          `<b>${yaixs}</b>: %{y}<br>` +
-          `<b>中位点</b>: %{y}<br>` +
-          `<extra></extra>`,
-      });
-    }
+    traces.push({
+      x: stats.x,
+      y: stats.median,
+      mode: "markers",
+      marker: { color: "#f00", size: 3 },
+      name: `${group.title} - 中位点`,
+      type: "scatter",
+      hovertemplate:
+        `<b>${xaixs}</b>: %{x}<br>` +
+        `<b>${yaixs}</b>: %{y}<br>` +
+        `<b>中位点</b>: %{y}<br>` +
+        `<extra></extra>`,
+    });
   });
 
   // ✅ layout(更干净)

+ 12 - 1
downLoadServer/src/server/utils/chartsCom/ColorbarInitTwoDmarkersChart.js

@@ -4,6 +4,17 @@
 import { renderChart } from "../chartService/index.js";
 import { colorSchemes } from "../colors.js";
 
+const maxFiniteValue = (values = []) => {
+  let max = -Infinity;
+  for (const value of values) {
+    const n = Number(value);
+    if (Number.isFinite(n) && n > max) {
+      max = n;
+    }
+  }
+  return max === -Infinity ? 0 : max;
+};
+
 export const generateColorbarInitTwoDmarkersChart = async (
   data,
   bucketName,
@@ -62,7 +73,7 @@ export const generateColorbarInitTwoDmarkersChart = async (
   // ✅ 特殊范围控制(修复版)
   if (scatterData.colorbartitle === "密度") {
     trace.marker.cmin = 0;
-    trace.marker.cmax = Math.max(...colorValues);
+    trace.marker.cmax = maxFiniteValue(colorValues);
   } else if (scatterData.colorbartitle === "百分比(%)") {
     trace.marker.cmin = 0;
     trace.marker.cmax = 100;

+ 108 - 184
downLoadServer/src/server/utils/chartsCom/PlotlyChartsFen.js

@@ -1,9 +1,4 @@
-import puppeteer from "puppeteer";
-import fs from "fs-extra";
-import path from "path";
-import FormData from "form-data";
-import { colorSchemes } from "../colors.js";
-import axios from "axios";
+import { renderChart } from "../chartService/index.js";
 
 /**
  * 生成折线图并上传
@@ -17,199 +12,128 @@ export const generatePlotlyChartsFen = async (
   fieldEngineCode,
 ) => {
   try {
-    // 创建临时目录
-    const tempDir = path.join(process.cwd(), "images");
-    await fs.ensureDir(tempDir);
-    const tempFilePath = path.join(
-      tempDir,
-      `temp_heatmap_chart_${Date.now()}.jpeg`,
-    );
+    if (!data || !Array.isArray(data.data)) {
+      throw new Error("PlotlyChartsFen data invalid");
+    }
 
-    // 获取 plotly.js 的绝对路径
-    const plotlyPath = path.join(
-      process.cwd(),
-      "src",
-      "public",
-      "js",
-      "plotly-latest.min.js",
-    );
-    const plotlyContent = await fs.readFile(plotlyPath, "utf-8");
+    const getEngineCode = (item) => item.engineCode || item.enginCode;
+    const getEngineName = (item) => item.engineName || item.enginName || "";
+    const isContractLine = (item) => getEngineName(item) === "合同功率曲线";
 
-    // 创建浏览器实例
-    const browser = await puppeteer.launch({
-      headless: "new",
-      // 根据系统改路径
-      executablePath: `${process.env.CHROME_PATH}`, // 根据系统改路径
+    const buildValidPairs = (xData = [], yData = []) => {
+      const maxLen = Math.min(xData.length, yData.length);
+      const pairs = [];
+      for (let i = 0; i < maxLen; i++) {
+        const x = xData[i];
+        const y = Number(yData[i]);
+        if (x === null || x === undefined || x === "") continue;
+        if (!Number.isFinite(y)) continue;
+        pairs.push({ x, y });
+      }
+      return pairs;
+    };
 
-      args: ["--no-sandbox", "--disable-setuid-sandbox"],
-    });
+    const sortedData = [...data.data]
+      .filter((item) => !isContractLine(item))
+      .sort((a, b) => {
+        const aIsTarget = getEngineCode(a) === fieldEngineCode;
+        const bIsTarget = getEngineCode(b) === fieldEngineCode;
 
-    try {
-      const page = await browser.newPage();
+        if (aIsTarget && !bIsTarget) return 1;
+        if (!aIsTarget && bIsTarget) return -1;
 
-      // 准备图表数据
-      const sortedData = data.data.sort((a, b) => {
-        if (
-          a.enginCode === fieldEngineCode &&
-          b.enginCode !== fieldEngineCode
-        ) {
-          return 1;
-        }
-        if (
-          a.enginCode !== fieldEngineCode &&
-          b.enginCode === fieldEngineCode
-        ) {
-          return -1;
-        }
-        return 0;
+        return getEngineName(a).localeCompare(getEngineName(b));
       });
 
-      const finalData = [];
-      let enginName = "";
-      sortedData
-        .filter((item) => item.enginName !== "合同功率曲线")
-        .forEach((turbine) => {
-          const color =
-            turbine.enginCode === fieldEngineCode ? "#406DAB" : "#D3D3D3";
-          enginName =
-            turbine.enginCode === fieldEngineCode ? turbine.enginName : "";
-          const chartConfig = {
-            x: turbine.xData,
-            y: turbine.yData,
-            connectgaps: false,
-            name: turbine.enginName,
-            mode: "lines",
-            fill: "none",
-            line: { color },
-            marker: { color },
-          };
-          finalData.push(chartConfig);
-        });
-      const filterData = data.data.filter(
-        (item) => item.enginName === "合同功率曲线",
-      );
-      // 添加合同功率曲线
-      if (filterData && filterData[0].enginName === "合同功率曲线") {
-        finalData.push({
-          x: filterData[0].xData,
-          y: filterData[0].yData,
+    let targetEngineName = "";
+    const traces = sortedData
+      .map((turbine) => {
+        const isTarget = getEngineCode(turbine) === fieldEngineCode;
+        if (isTarget) targetEngineName = getEngineName(turbine);
+
+        const pairs = buildValidPairs(turbine.xData, turbine.yData);
+        if (!pairs.length) return null;
+
+        const color = isTarget ? "#406DAB" : "#D3D3D3";
+        return {
+          x: pairs.map((pair) => pair.x),
+          y: pairs.map((pair) => pair.y),
+          connectgaps: false,
+          name: getEngineName(turbine),
+          type: "scatter",
+          mode: pairs.length === 1 ? "markers" : "lines",
+          fill: "none",
+          line: { color, width: isTarget ? 2 : 1 },
+          marker: { color, size: pairs.length === 1 ? 7 : 4 },
+        };
+      })
+      .filter(Boolean);
+
+    const contractLine = data.data.find(isContractLine);
+    if (contractLine) {
+      const pairs = buildValidPairs(contractLine.xData, contractLine.yData);
+      if (pairs.length) {
+        traces.push({
+          x: pairs.map((pair) => pair.x),
+          y: pairs.map((pair) => pair.y),
+          type: "scatter",
           mode: "lines+markers",
           name: "合同功率曲线",
-          line: {
-            color: "red",
-            width: 1,
-          },
+          line: { color: "red", width: 1 },
           marker: { color: "red", size: 4 },
         });
       }
+    }
 
-      // 准备布局配置
-      const layout = {
-        title: {
-          text: `机组 ${enginName}风速功率曲线分析`,
-          font: {
-            size: 16,
-            weight: "bold",
-          },
-        },
-        xaxis: {
-          title: {
-            text: "风速(m/s)" || "x轴",
-          },
-          gridcolor: "rgb(255,255,255)",
-          tickcolor: "rgb(255,255,255)",
-          backgroundcolor: "#e5ecf6",
-          showbackground: true,
-          showline: true, // ✅ 显示 X 轴轴线
-          linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
-          zeroline: false,
-        },
-        yaxis: {
-          title: { text: "功率(kW)" || "Y轴" },
-          gridcolor: "rgb(255,255,255)",
-          tickcolor: "rgb(255,255,255)",
-          backgroundcolor: "#e5ecf6",
-          showbackground: true,
-          showline: true, // ✅ 显示 X 轴轴线
-          zeroline: false,
-          linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
-        },
-        margin: {
-          l: 50,
-          r: 50,
-          t: 50,
-          b: 50,
-        },
-        autosize: true,
-        plot_bgcolor: "#e5ecf6",
-        gridcolor: "#fff",
-        bgcolor: "#e5ecf6",
-        legend: {
-          orientation: "h",
-          xanchor: "center",
-          x: 0.5,
-          y: -0.2,
-        },
-      };
-      // 创建HTML内容
-      const htmlContent = `
-        <!DOCTYPE html>
-        <html>
-          <head>
-            <script>${plotlyContent}</script>
-            <style>
-              body { margin: 0; }
-              #chart { width: 800px; height: 600px; }
-            </style>
-          </head>
-          <body>
-            <div id="chart"></div>
-            <script>
-              window.onload = function() {
-                const data = ${JSON.stringify(finalData)};
-                const layout = ${JSON.stringify(layout)};
-                Plotly.newPlot('chart', data, layout, {
-                  responsive: true
-                }).then(() => {
-                  window.chartRendered = true;
-                });
-              };
-            </script>
-          </body>
-        </html>
-      `;
-
-      // 设置页面内容
-      await page.setContent(htmlContent, {
-        waitUntil: "networkidle0",
-      });
-
-      // 等待图表渲染完成
-      await page.waitForFunction(() => window.chartRendered === true, {
-        timeout: 60000,
-      });
+    if (!traces.length) {
+      throw new Error("PlotlyChartsFen has no valid points");
+    }
 
-      // 截图并保存到临时文件
-      // 截图并保存到临时文件
-      const chartElement = await page.$("#chart");
-      await chartElement.screenshot({
-        path: tempFilePath,
-        type: "jpeg",
-      });
+    const layout = {
+      title: {
+        text:
+          data.title ||
+          `机组 ${targetEngineName || fieldEngineCode || ""}风速功率曲线分析`,
+        font: { size: 16 },
+      },
+      xaxis: {
+        title: data.xaixs || "风速(m/s)",
+        gridcolor: "rgb(255,255,255)",
+        tickcolor: "rgb(255,255,255)",
+        backgroundcolor: "#e5ecf6",
+        showbackground: true,
+        showline: true,
+        linecolor: "#ffffff",
+        zeroline: false,
+      },
+      yaxis: {
+        title: data.yaixs || "功率(kW)",
+        gridcolor: "rgb(255,255,255)",
+        tickcolor: "rgb(255,255,255)",
+        backgroundcolor: "#e5ecf6",
+        showbackground: true,
+        showline: true,
+        zeroline: false,
+        linecolor: "#ffffff",
+      },
+      margin: { l: 50, r: 50, t: 50, b: 50 },
+      autosize: true,
+      plot_bgcolor: "#e5ecf6",
+      paper_bgcolor: "#e5ecf6",
+      legend: {
+        orientation: "h",
+        xanchor: "center",
+        x: 0.5,
+        y: -0.2,
+      },
+    };
 
-      // 上传图片到服务器
-      const formData = new FormData();
-      formData.append("file", fs.createReadStream(tempFilePath));
-      // return formData;
-      // 发送上传请求
-      const response = await axios.post(
-        `${process.env.API_BASE_URL}/examples/upload`,
-        { filePath: tempFilePath, bucketName, objectName },
-      );
-      return response?.data?.url;
-    } finally {
-      await browser.close();
-    }
+    return await renderChart({
+      traces,
+      layout,
+      bucketName,
+      objectName,
+    });
   } catch (error) {
     console.error("生成折线图失败:", error);
     throw error;

+ 79 - 59
downLoadServer/src/server/utils/chartsCom/Radar.js

@@ -2,7 +2,6 @@
 import puppeteer from "puppeteer";
 import fs from "fs-extra";
 import path from "path";
-import FormData from "form-data";
 import axios from "axios";
 
 // 获取 echarts.min.js 的路径
@@ -31,27 +30,8 @@ const getHtmlContent = () => `
 <body>
   <div id="chart"></div>
   <script>
-    window.renderChart = function(chartData, itemCsvData, engineTypeCode) {
-      function calcStats(data) {
-        const matrix = data.map(item => [
-          Number(item.TurbinePowerRate),
-          Number(item.TurbineRunRate),
-          Number(item.WindSpeedAvr),
-          Number(item.Thi),
-          Number(item.Ws)
-        ]);
-
-        const max = matrix[0].map((_, i) => Math.max(...matrix.map(row => row[i])));
-        const min = matrix[0].map((_, i) => Math.min(...matrix.map(row => row[i])));
-        const median = matrix[0].map((_, i) => {
-          const sorted = matrix.map(row => row[i]).sort((a, b) => a - b);
-          const mid = Math.floor(sorted.length / 2);
-          return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
-        });
-        return { max, min, median };
-      }
-
-      const { max, min, median } = calcStats(itemCsvData);
+    window.renderChart = function(chartData, stats, engineTypeCode) {
+      const { max, min, median } = stats;
       const values = [
         chartData.TurbinePowerRate,
         chartData.TurbineRunRate,
@@ -119,6 +99,37 @@ const getHtmlContent = () => `
 </html>
 `;
 
+const medianOf = (values) => {
+  if (!values.length) return 0;
+  const sorted = values.sort((a, b) => a - b);
+  const mid = Math.floor(sorted.length / 2);
+  return sorted.length % 2 === 0
+    ? (sorted[mid - 1] + sorted[mid]) / 2
+    : sorted[mid];
+};
+
+const calcStats = (data) => {
+  const columns = [[], [], [], [], []];
+  data.forEach((item) => {
+    [
+      item.TurbinePowerRate,
+      item.TurbineRunRate,
+      item.WindSpeedAvr,
+      item.Thi,
+      item.Ws,
+    ].forEach((value, index) => {
+      const n = Number(value);
+      if (Number.isFinite(n)) columns[index].push(n);
+    });
+  });
+
+  return {
+    max: columns.map((values) => (values.length ? Math.max(...values) : 0)),
+    min: columns.map((values) => (values.length ? Math.min(...values) : 0)),
+    median: columns.map(medianOf),
+  };
+};
+
 export const getRadarCharts = async (
   chartData,
   itemCsvData,
@@ -127,53 +138,60 @@ export const getRadarCharts = async (
   fieldInfo,
   engineTypeCode,
 ) => {
+  const tempDir = path.join(process.cwd(), "images");
+  await fs.ensureDir(tempDir);
+  const tempFilePath = path.join(
+    tempDir,
+    `temp_radar_chart_${Date.now()}_${Math.random().toString(36).slice(2)}.jpeg`,
+  );
+
   const browser = await puppeteer.launch({
     headless: "new",
     executablePath: process.env.CHROME_PATH,
     args: ["--no-sandbox", "--disable-setuid-sandbox"],
   });
 
-  const page = await browser.newPage();
-
-  await page.setViewport({
-    width: 600,
-    height: 600,
-    deviceScaleFactor: 2,
-  });
-
-  await page.setContent(getHtmlContent(), {
-    waitUntil: "domcontentloaded",
-    timeout: 0,
-  });
-
-  await page.waitForFunction(() => typeof window.renderChart === "function", {
-    timeout: 10000,
-  });
+  try {
+    const page = await browser.newPage();
+    const stats = calcStats(itemCsvData);
+
+    await page.setViewport({
+      width: 600,
+      height: 600,
+      deviceScaleFactor: 2,
+    });
+
+    await page.setContent(getHtmlContent(), {
+      waitUntil: "domcontentloaded",
+      timeout: 0,
+    });
+
+    await page.waitForFunction(() => typeof window.renderChart === "function", {
+      timeout: 10000,
+    });
+
+    await page.evaluate(
+      (chartData, stats, engineTypeCode) => {
+        window.renderChart(chartData, stats, engineTypeCode);
+      },
+      chartData,
+      stats,
+      engineTypeCode,
+    );
 
-  await page.evaluate(
-    (chartData, itemCsvData, engineTypeCode) => {
-      window.renderChart(chartData, itemCsvData, engineTypeCode);
-    },
-    chartData,
-    itemCsvData,
-    engineTypeCode,
-  );
+    await page.waitForFunction(() => window.chartFinished === true, {
+      timeout: 15000,
+    });
 
-  await page.waitForFunction(() => window.chartFinished === true, {
-    timeout: 15000,
-  });
+    const chartElement = await page.waitForSelector("#chart");
 
-  const chartElement = await page.waitForSelector("#chart");
+    await chartElement.screenshot({
+      path: tempFilePath,
+      type: "jpeg",
+      quality: 100,
+    });
 
-  await chartElement.screenshot({
-    path: tempFilePath,
-    type: "jpeg",
-    quality: 100,
-  });
-  try {
     const newUrl = objectName.substring(0, objectName.lastIndexOf("/"));
-    const formData = new FormData();
-    formData.append("file", fs.createReadStream(tempFilePath));
 
     const response = await axios.post(
       `${process.env.API_BASE_URL}/examples/upload`,
@@ -186,8 +204,10 @@ export const getRadarCharts = async (
 
     return response?.data?.url;
   } catch (error) {
-    console.error("❌ 上传失败:", error.message);
+    console.error("getRadarCharts failed:", error);
+    throw error;
   } finally {
     await browser.close();
+    await fs.remove(tempFilePath).catch(() => {});
   }
 };

+ 47 - 19
downLoadServer/src/server/utils/chartsCom/TwoDMarkersChart.js

@@ -1,6 +1,44 @@
 import { renderChart } from "../chartService/index.js";
 import { colorSchemes } from "../colors.js";
 
+const MAX_COLORBAR_CATEGORIES = Number.parseInt(
+  process.env.CHART_MAX_COLORBAR_CATEGORIES || "50",
+  10,
+);
+
+const buildColorConfig = (rawColorData) => {
+  const seen = new Map();
+  let allNumeric = true;
+
+  for (const value of rawColorData) {
+    const numericValue = Number(value);
+    if (!Number.isFinite(numericValue)) allNumeric = false;
+
+    if (seen.size <= MAX_COLORBAR_CATEGORIES && !seen.has(value)) {
+      seen.set(value, seen.size + 1);
+    }
+  }
+
+  // 连续数值或分类过多时,直接使用连续色条;避免为海量唯一值生成 ticktext/tickvals/mapping。
+  if (allNumeric || seen.size > MAX_COLORBAR_CATEGORIES) {
+    return {
+      colorValues: rawColorData.map((value) => Number(value)),
+      colorbar: undefined,
+      customdata: undefined,
+    };
+  }
+
+  const ticktext = [...seen.keys()];
+  return {
+    colorValues: rawColorData.map((value) => seen.get(value)),
+    colorbar: {
+      tickvals: ticktext.map((_, i) => i + 1),
+      ticktext,
+    },
+    customdata: rawColorData,
+  };
+};
+
 export const generateTwoDMarkersChart = async (
   data,
   bucketName,
@@ -20,18 +58,7 @@ export const generateTwoDMarkersChart = async (
         ? chartDataset.colorbar
         : yData;
 
-    // ✅ 2. 唯一标签
-    const uniqueLabels = [...new Set(rawColorData)];
-
-    const tickvals = uniqueLabels.map((_, i) => i + 1);
-    const ticktext = uniqueLabels;
-
-    const mapping = uniqueLabels.reduce((acc, cur, i) => {
-      acc[cur] = i + 1;
-      return acc;
-    }, {});
-
-    // ✅ 3. 颜色渐变(安全版)
+    // ✅ 2. 颜色渐变(安全版)
     const colors = colorSchemes?.[0]?.colors || [];
 
     const safePick = (i) => colors[i] || colors[0] || "#1B2973";
@@ -47,8 +74,8 @@ export const generateTwoDMarkersChart = async (
       colorscale.push([1, safePick(0)]);
     }
 
-    // ✅ 4. 映射颜色值
-    const colorValues = rawColorData.map((v) => mapping[v]);
+    // ✅ 3. 映射颜色值:小分类使用离散刻度,大数据/连续值使用连续色条
+    const colorConfig = buildColorConfig(rawColorData);
 
     // ⚠️ 性能优化(点很多时很关键🔥)
     const pointCount = xData.length;
@@ -62,20 +89,21 @@ export const generateTwoDMarkersChart = async (
       mode: "markers",
       name: engineName || "散点图",
       marker: {
-        color: colorValues,
+        color: colorConfig.colorValues,
         colorscale,
         size: markerSize,
         colorbar: {
           title: { text: chartDataset.colorbartitle || "分类" },
-          tickvals,
-          ticktext,
+          ...colorConfig.colorbar,
         },
       },
-      customdata: rawColorData,
+      customdata: colorConfig.customdata,
       hovertemplate:
         `${data.xaixs}: %{x}<br>` +
         `${data.yaixs}: %{y}<br>` +
-        `分类: %{customdata}<extra></extra>`,
+        (colorConfig.customdata
+          ? `分类: %{customdata}<extra></extra>`
+          : `<extra></extra>`),
     };
 
     // ✅ 6. layout(简化+统一风格)

+ 110 - 51
downLoadServer/src/server/utils/chartsCom/lineChartsFen.js

@@ -1,8 +1,7 @@
 import { renderChart } from "../chartService/index.js";
-import { colorSchemes } from "../colors.js";
 
 /**
- * 过滤数据(你原来的逻辑,已增强健壮性
+ * 过滤数据(仅当 X 为时间序列且提供了合法起止时间时才按区间筛选
  */
 const filterData = (group, startDate, endDate) => {
   const filteredXData = [];
@@ -33,19 +32,37 @@ const filterData = (group, startDate, endDate) => {
   };
 };
 
+/**
+ * 不能用 Date.parse(数字):在 Node/V8 里 Date.parse(0)、Date.parse(10) 常为非 NaN,
+ * 功率(kW) 轴会被误判为“日期”,再与 startDate/endDate 比较会把点全部滤掉 → 空白图。
+ * 与前端 lineChartsFen.vue 行为对齐:数值功率轴不做日期过滤。
+ */
 const isDateType = (xData) => {
   if (!xData || !xData.length) return false;
-  return !isNaN(Date.parse(xData[0]));
+  const first = xData[0];
+  if (typeof first === "string") {
+    const t = Date.parse(first);
+    return !Number.isNaN(t);
+  }
+  if (typeof first === "number" && Number.isFinite(first)) {
+    if (first > 1e11) return true;
+    if (first > 1e9 && first < 1e11) return true;
+    return false;
+  }
+  return false;
 };
 
 const isInDateRange = (timestamp, startDate, endDate) => {
-  if (!startDate || !endDate) return true;
+  if (startDate == null || endDate == null) return true;
+  const start = new Date(startDate);
+  const end = new Date(endDate);
+  if (Number.isNaN(+start) || Number.isNaN(+end)) return true;
   const date = new Date(timestamp);
-  return date >= new Date(startDate) && date <= new Date(endDate);
+  return date >= start && date <= end;
 };
 
 /**
- * ✅ 终极版折线图生成(接入 chartService)
+ * ✅ 折线图生成(接入 chartService),绘制逻辑与 Vue 侧一致:先排序,再直接成图(仅时间轴才按日期筛)
  */
 export const generateLineChart = async (
   data,
@@ -56,58 +73,82 @@ export const generateLineChart = async (
   endDate,
 ) => {
   try {
-    const colorSchemesItem = colorSchemes[0].colors;
+    const filteredData = data.data.map((group) =>
+      filterData(group, startDate, endDate),
+    );
 
-    // ✅ 1. 排序(目标机组最后)
-    const sortedData = [...data.data].sort((a, b) => {
+    const sortedData = [...filteredData].sort((a, b) => {
       const aIsTarget = a.engineCode === fieldEngineCode;
       const bIsTarget = b.engineCode === fieldEngineCode;
 
       if (aIsTarget && !bIsTarget) return 1;
       if (!aIsTarget && bIsTarget) return -1;
 
-      return a.engineName.localeCompare(b.engineName);
+      return (a.engineName || "").localeCompare(b.engineName || "");
     });
 
-    // ✅ 2. 过滤数据(你之前漏掉的逻辑)
-    const filteredData = sortedData.map((group) =>
-      filterData(group, startDate, endDate),
-    );
+    const buildValidPairs = (xData = [], yData = []) => {
+      const maxLen = Math.min(xData.length, yData.length);
+      const pairs = [];
+      for (let i = 0; i < maxLen; i++) {
+        const x = xData[i];
+        const y = Number(yData[i]);
+        if (!Number.isFinite(y)) continue;
+        if (x === null || x === undefined || x === "") continue;
+        pairs.push({ x, y });
+      }
+      return pairs;
+    };
 
-    // ✅ 3. 构建 traces
-    const traces = filteredData.map((turbine, index) => {
-      const isTarget = turbine.engineCode === fieldEngineCode;
-
-      const color = isTarget
-        ? "#406DAB"
-        : colorSchemesItem[index % colorSchemesItem.length];
-
-      return {
-        x: turbine.xData,
-        y: turbine.yData,
-        name: turbine.engineName,
-        type: "scatter",
-        mode: "lines",
-        line: { color },
-        marker: { color },
-        hovertemplate: `${data.xaixs}: %{x}<br>${data.yaixs}: %{y}<extra></extra>`,
-      };
-    });
+    const traces = sortedData
+      .map((turbine) => {
+        const isTarget = turbine.engineCode === fieldEngineCode;
+
+        const color = isTarget ? "#406DAB" : "#D3D3D3";
+
+        const validPairs = buildValidPairs(turbine.xData, turbine.yData);
+        if (!validPairs.length) return null;
+
+        const xs = validPairs.map((p) => p.x);
+        const ys = validPairs.map((p) => p.y);
+        const onePoint = validPairs.length === 1;
+
+        return {
+          x: xs,
+          y: ys,
+          name: turbine.engineName,
+          type: "scatter",
+          mode: onePoint ? "markers" : "lines",
+          line: { color, width: isTarget ? 2 : 1 },
+          marker: { color, size: onePoint ? 7 : 4 },
+          connectgaps: false,
+          hovertemplate: `${data.xaixs}: %{x}<br>${data.yaixs}: %{y}<extra></extra>`,
+        };
+      })
+      .filter(Boolean);
 
-    // ✅ 4. 合同曲线
     if (data.contract_Cp_curve_yData?.length) {
-      traces.push({
-        x: data.contract_Cp_curve_xData,
-        y: data.contract_Cp_curve_yData,
-        type: "scatter",
-        mode: "lines+markers",
-        name: "合同功率曲线",
-        line: { color: "red", width: 1 },
-        marker: { color: "red", size: 4 },
-      });
+      const cp = buildValidPairs(
+        data.contract_Cp_curve_xData,
+        data.contract_Cp_curve_yData,
+      );
+      if (cp.length) {
+        traces.push({
+          x: cp.map((p) => p.x),
+          y: cp.map((p) => p.y),
+          type: "scatter",
+          mode: "lines+markers",
+          name: "合同功率曲线",
+          line: { color: "red", width: 1 },
+          marker: { color: "red", size: 4 },
+        });
+      }
+    }
+
+    if (!traces.length) {
+      throw new Error("折线图过滤后无有效数据点");
     }
 
-    // ✅ 5. layout
     const layout = {
       title: {
         text: data.title || "图表",
@@ -119,9 +160,23 @@ export const generateLineChart = async (
         tickcolor: "rgb(255,255,255)",
         backgroundcolor: "#e5ecf6",
         showbackground: true,
-        showline: true, // ✅ 显示 X 轴轴线
+        showline: true,
         zeroline: false,
-        linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+        linecolor: "#ffffff",
+        dtick: data.xaixs === "风速" ? 1 : undefined,
+        range:
+          data.analysisTypeCode === "风电机组风能利用系数分析" &&
+          Array.isArray(data.contract_Cp_curve_xData) &&
+          data.contract_Cp_curve_xData.length
+            ? [
+                0,
+                Math.max(
+                  ...data.contract_Cp_curve_xData
+                    .map(Number)
+                    .filter((val) => !Number.isNaN(val)),
+                ) * 0.9,
+              ]
+            : undefined,
       },
       yaxis: {
         title: data.yaixs || "Y轴",
@@ -130,15 +185,19 @@ export const generateLineChart = async (
         backgroundcolor: "#e5ecf6",
         showbackground: true,
         zeroline: false,
-        showline: true, // ✅ 显示 X 轴轴线
-        linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+        showline: true,
+        linecolor: "#ffffff",
+        range:
+          data.analysisTypeCode === "风电机组风能利用系数分析"
+            ? [0, 1]
+            : undefined,
       },
       margin: { l: 50, r: 50, t: 50, b: 50 },
-      plot_bgcolor: "#fff",
-      paper_bgcolor: "#fff",
+      autosize: true,
+      plot_bgcolor: "#e5ecf6",
+      paper_bgcolor: "#e5ecf6",
     };
 
-    // ✅ 6. 直接调用统一服务
     const url = await renderChart({
       traces,
       layout,

+ 134 - 138
downLoadServer/src/server/utils/copyFileCsv.js

@@ -58,6 +58,11 @@ const preloadAllImages = async (chartsImages) => {
   return imageMap;
 };
 
+const isValidImageUrl = (url) => typeof url === "string" && url.trim();
+
+const hasImages = (imageTagData, tag) =>
+  Array.isArray(imageTagData[tag]) && imageTagData[tag].length > 0;
+
 // 生成 Word 文件
 export const copyFileDocx = async (fieldInfo, chartsImages) => {
   try {
@@ -124,7 +129,9 @@ export const copyFileDocx = async (fieldInfo, chartsImages) => {
     const imageTagData = {};
     for (const [tag, urlList] of Object.entries(chartsImages)) {
       if (Array.isArray(urlList)) {
-        imageTagData[tag] = urlList.map((url) => ({ image: url }));
+        imageTagData[tag] = urlList
+          .filter(isValidImageUrl)
+          .map((url) => ({ image: url }));
       } else {
         // console.warn(`⚠️ 无效的 urlList: ${tag}`, urlList);
         imageTagData[tag] = []; // 或 throw error
@@ -153,160 +160,149 @@ export const copyFileDocx = async (fieldInfo, chartsImages) => {
       engineTypeList: chartsImages.engineTypeList,
       dataTime: chartsImages.dataTime,
       "show-zn-techcn-replace-tags-data_integrity_minute-generalFiles":
-        imageTagData[
-          "zn-techcn-replace-tags-data_integrity_minute-generalFiles"
-        ]
-          ? true
-          : false,
+        hasImages(
+          imageTagData,
+          "zn-techcn-replace-tags-data_integrity_minute-generalFiles",
+        ),
       "show-zn-techcn-replace-tags-data_integrity_second-generalFiles":
-        imageTagData[
-          "zn-techcn-replace-tags-data_integrity_second-generalFiles"
-        ]
-          ? true
-          : false,
+        hasImages(
+          imageTagData,
+          "zn-techcn-replace-tags-data_integrity_second-generalFiles",
+        ),
       "show-zn-techcn-replace-tags-production_indicator_unit-generalFiles":
-        imageTagData[
-          "zn-techcn-replace-tags-production_indicator_unit-generalFiles"
-        ]
-          ? true
-          : false,
-      "show-zn-techcn-replace-tags-yaw_error-generalFiles1": imageTagData[
-        "zn-techcn-replace-tags-yaw_error-generalFiles1"
-      ]
-        ? true
-        : false,
-      "show-zn-techcn-replace-tags-yaw_error-diagramRelations": imageTagData[
-        "zn-techcn-replace-tags-yaw_error-diagramRelations"
-      ]
-        ? true
-        : false,
-      "show-zn-techcn-replace-tags-fault_all-generalFiles": imageTagData[
-        "zn-techcn-replace-tags-fault_all-generalFiles"
-      ]
-        ? true
-        : false,
+        hasImages(
+          imageTagData,
+          "zn-techcn-replace-tags-production_indicator_unit-generalFiles",
+        ),
+      "show-zn-techcn-replace-tags-yaw_error-generalFiles1": hasImages(
+        imageTagData,
+        "zn-techcn-replace-tags-yaw_error-generalFiles1",
+      ),
+      "show-zn-techcn-replace-tags-yaw_error-diagramRelations": hasImages(
+        imageTagData,
+        "zn-techcn-replace-tags-yaw_error-diagramRelations",
+      ),
+      "show-zn-techcn-replace-tags-fault_all-generalFiles": hasImages(
+        imageTagData,
+        "zn-techcn-replace-tags-fault_all-generalFiles",
+      ),
       "show-zn-techcn-replace-tags-temperature_large_components_hig-generalFiles":
-        imageTagData[
-          "zn-techcn-replace-tags-temperature_large_components_hig-generalFiles"
-        ]
-          ? true
-          : false,
+        hasImages(
+          imageTagData,
+          "zn-techcn-replace-tags-temperature_large_components_hig-generalFiles",
+        ),
       "show-zn-techcn-replace-tags-temperature_large_components_low-generalFiles":
-        imageTagData[
-          "zn-techcn-replace-tags-temperature_large_components_low-generalFiles"
-        ]
-          ? true
-          : false,
+        hasImages(
+          imageTagData,
+          "zn-techcn-replace-tags-temperature_large_components_low-generalFiles",
+        ),
       "show-zn-techcn-replace-tags-temperature_large_components_adriven-generalFiles":
-        imageTagData[
-          "zn-techcn-replace-tags-temperature_large_components_adriven-generalFiles"
-        ]
-          ? true
-          : false,
+        hasImages(
+          imageTagData,
+          "zn-techcn-replace-tags-temperature_large_components_adriven-generalFiles",
+        ),
       "show-zn-techcn-replace-tags-temperature_large_components_undriven-generalFiles":
-        imageTagData[
-          "zn-techcn-replace-tags-temperature_large_components_undriven-generalFiles"
-        ]
-          ? true
-          : false,
+        hasImages(
+          imageTagData,
+          "zn-techcn-replace-tags-temperature_large_components_undriven-generalFiles",
+        ),
       "show-zn-techcn-replace-tags-temperature_large_components_tem_deviation-diagramRelations":
-        imageTagData[
-          "zn-techcn-replace-tags-temperature_large_components_tem_deviation-diagramRelations"
-        ]
-          ? true
-          : false,
+        hasImages(
+          imageTagData,
+          "zn-techcn-replace-tags-temperature_large_components_tem_deviation-diagramRelations",
+        ),
       "show-zn-techcn-replace-tags-pitch_generator_speed-diagramRelations":
-        imageTagData[
-          "zn-techcn-replace-tags-pitch_generator_speed-diagramRelations"
-        ]
-          ? true
-          : false,
-      "show-zn-techcn-replace-tags-power_curve-generalFiles": imageTagData[
-        "zn-techcn-replace-tags-power_curve-generalFiles"
-      ]
-        ? true
-        : false,
+        hasImages(
+          imageTagData,
+          "zn-techcn-replace-tags-pitch_generator_speed-diagramRelations",
+        ),
+      "show-zn-techcn-replace-tags-power_curve-generalFiles": hasImages(
+        imageTagData,
+        "zn-techcn-replace-tags-power_curve-generalFiles",
+      ),
       "show-zn-techcn-replace-tags-power_scatter_2D-diagramRelations":
-        imageTagData["zn-techcn-replace-tags-power_scatter_2D-diagramRelations"]
-          ? true
-          : false,
-      "show-zn-techcn-replace-tags-cp-generalFiles": imageTagData[
-        "zn-techcn-replace-tags-cp-generalFiles"
-      ]
-        ? true
-        : false,
-      "show-zn-techcn-replace-tags-cp_trend-diagramRelations": imageTagData[
-        "zn-techcn-replace-tags-cp_trend-diagramRelations"
-      ]
-        ? true
-        : false,
-      "show-zn-techcn-replace-tags-tsr-generalFiles": imageTagData[
-        "zn-techcn-replace-tags-tsr-generalFiles"
-      ]
-        ? true
-        : false,
-      "show-zn-techcn-replace-tags-tsr_trend-diagramRelations": imageTagData[
-        "zn-techcn-replace-tags-tsr_trend-diagramRelations"
-      ]
-        ? true
-        : false,
-      "show-zn-techcn-replace-tags-min_pitch-diagramRelations": imageTagData[
-        "zn-techcn-replace-tags-min_pitch-diagramRelations"
-      ]
-        ? true
-        : false,
-      "show-zn-techcn-replace-tags-power_curve-generalFiles": imageTagData[
-        "zn-techcn-replace-tags-power_curve-generalFiles"
-      ]
-        ? true
-        : false,
+        hasImages(
+          imageTagData,
+          "zn-techcn-replace-tags-power_scatter_2D-diagramRelations",
+        ),
+      "show-zn-techcn-replace-tags-cp-generalFiles": hasImages(
+        imageTagData,
+        "zn-techcn-replace-tags-cp-generalFiles",
+      ),
+      "show-zn-techcn-replace-tags-cp-diagramRelations": hasImages(
+        imageTagData,
+        "zn-techcn-replace-tags-cp-diagramRelations",
+      ),
+      "show-zn-techcn-replace-tags-cp_trend-diagramRelations": hasImages(
+        imageTagData,
+        "zn-techcn-replace-tags-cp_trend-diagramRelations",
+      ),
+      "show-zn-techcn-replace-tags-tsr-generalFiles": hasImages(
+        imageTagData,
+        "zn-techcn-replace-tags-tsr-generalFiles",
+      ),
+      "show-zn-techcn-replace-tags-tsr-diagramRelations": hasImages(
+        imageTagData,
+        "zn-techcn-replace-tags-tsr-diagramRelations",
+      ),
+      "show-zn-techcn-replace-tags-tsr_trend-diagramRelations": hasImages(
+        imageTagData,
+        "zn-techcn-replace-tags-tsr_trend-diagramRelations",
+      ),
+      "show-zn-techcn-replace-tags-min_pitch-diagramRelations": hasImages(
+        imageTagData,
+        "zn-techcn-replace-tags-min_pitch-diagramRelations",
+      ),
       "show-zn-techcn-replace-tags-power_scatter_2D-diagramRelations":
-        imageTagData["zn-techcn-replace-tags-power_scatter_2D-diagramRelations"]
-          ? true
-          : false,
+        hasImages(
+          imageTagData,
+          "zn-techcn-replace-tags-power_scatter_2D-diagramRelations",
+        ),
       // "": imageTagData[""] ? true : false,
       //功率散点图
       "show-zn-techcn-replace-tags-power_scatter-diagramRelations":
-        imageTagData["zn-techcn-replace-tags-power_scatter-diagramRelations"]
-          ? true
-          : false,
+        hasImages(
+          imageTagData,
+          "zn-techcn-replace-tags-power_scatter-diagramRelations",
+        ),
       "show-zn-techcn-replace-tags-power_scatter":
-        imageTagData["zn-techcn-replace-tags-power_scatter-diagramRelations"] ||
-        imageTagData[
-          "zn-techcn-replace-tags-power_scatter_2D-diagramRelations"
-        ] ||
-        imageTagData["zn-techcn-replace-tags-power_curve-generalFiles"]
-          ? true
-          : false,
+        hasImages(
+          imageTagData,
+          "zn-techcn-replace-tags-power_scatter-diagramRelations",
+        ) ||
+        hasImages(
+          imageTagData,
+          "zn-techcn-replace-tags-power_scatter_2D-diagramRelations",
+        ) ||
+        hasImages(imageTagData, "zn-techcn-replace-tags-power_curve-generalFiles"),
       "show-zn-techcn-replace-tags-fault_all-generalFiles&show-zn-techcn-replace-tags-temperature_large_components_hig-generalFiles":
-        imageTagData["zn-techcn-replace-tags-fault_all-generalFiles"] &&
-        imageTagData[
-          "zn-techcn-replace-tags-temperature_large_components_hig-generalFiles"
-        ]
-          ? true
-          : false,
+        hasImages(imageTagData, "zn-techcn-replace-tags-fault_all-generalFiles") &&
+        hasImages(
+          imageTagData,
+          "zn-techcn-replace-tags-temperature_large_components_hig-generalFiles",
+        ),
       "show-zn-techcn-replace-tags-yaw_error_density-diagramRelations":
-        imageTagData[
-          "zn-techcn-replace-tags-yaw_error_density-diagramRelations"
-        ]
-          ? true
-          : false,
+        hasImages(
+          imageTagData,
+          "zn-techcn-replace-tags-yaw_error_density-diagramRelations",
+        ),
       "show-zn-techcn-replace-tags-temperature_environment-generalFiles":
-        imageTagData[
-          "zn-techcn-replace-tags-temperature_environment-generalFiles"
-        ]
-          ? true
-          : false,
-      "show-zn-techcn-replace-tags-pitch_power-diagramRelations": imageTagData[
-        "zn-techcn-replace-tags-pitch_power-diagramRelations"
-      ]
-        ? true
-        : false,
-      "show-zn-techcn-replace-tags-speed_power-generalFiles": imageTagData[
-        "zn-techcn-replace-tags-speed_power-generalFiles"
-      ]
-        ? true
-        : false,
+        hasImages(
+          imageTagData,
+          "zn-techcn-replace-tags-temperature_environment-generalFiles",
+        ),
+      "show-zn-techcn-replace-tags-pitch_power-diagramRelations": hasImages(
+        imageTagData,
+        "zn-techcn-replace-tags-pitch_power-diagramRelations",
+      ),
+      "show-zn-techcn-replace-tags-speed_power-generalFiles": hasImages(
+        imageTagData,
+        "zn-techcn-replace-tags-speed_power-generalFiles",
+      ),
+      "show-zn-techcn-replace-tags-speed_power-diagramRelations": hasImages(
+        imageTagData,
+        "zn-techcn-replace-tags-speed_power-diagramRelations",
+      ),
     };
 
     // 渲染 Word 模板

BIN=BIN
downLoadServer/src/src.zip


+ 18 - 7
src/views/performance/assetssMag.vue

@@ -323,7 +323,7 @@
             >
               下载图表
             </el-button> -->
-            <!-- <el-button
+            <el-button
               @click="handleDownLoadChart(scope.row)"
               type="text"
               size="small"
@@ -334,7 +334,7 @@
               "
             >
               下载报告
-            </el-button> -->
+            </el-button>
             <el-button
               v-hasPermi="['home:performance:uploadReport']"
               @click="abnormalDialog(scope.row, '上传报告')"
@@ -496,7 +496,7 @@ import {
 import { downloadDocx } from "@/utils/common";
 import { getFieldInfo } from "@/api/overview";
 import axios from "axios";
-// import pLimit from "p-limit";
+import pLimit from "p-limit";
 import { allAnalysisType } from "./js/allAnalysisType";
 import { mapMutations, mapState } from "vuex";
 import {
@@ -627,6 +627,7 @@ export default {
 
         const res = await axios.post(
           `/downLoadChart/chartServer/charts/${urlType}`,
+          // "/downLoadChart/chartServer/charts/line",
           {
             fieldEngineCode: itemField.fieldEngineCode,
             bucketName: "bucket-zhzn",
@@ -636,10 +637,13 @@ export default {
               objectname,
             fieldInfo: this.fieldInfo,
             fileAddr: itemField.fileAddr,
+            // fileAddr:
+            //   "http://192.168.50.234:6900/wof053500048/WOF053500048-WOB000005/temperature_large_components/manual/generatordrive_end_bearing_temperature/A9.json",
             chartType:
               filterAnalysis.typeDocxName === "production_indicator_unit"
                 ? "radar"
                 : urlType,
+            // chartType: "line",
           },
         );
         // 每完成 10% 提示一次(可调)
@@ -687,14 +691,21 @@ export default {
           if (!this.fileDataList[key]) {
             this.$set(this.fileDataList, key, []); // Vue 2 中响应式设置对象属性
           }
-          res?.data?.data?.imageUrls.map((imgval) => {
-            this.fileDataList[key].push(imgval);
-          });
+          (res?.data?.data?.imageUrls || [])
+            .filter((imgval) => typeof imgval === "string" && imgval.trim())
+            .forEach((imgval) => {
+              this.fileDataList[key].push(imgval);
+            });
         } else {
           if (!this.fileDataList[key]) {
             this.$set(this.fileDataList, key, []); // Vue 2 中响应式设置对象属性
           }
-          this.fileDataList[key].push(res?.data?.data?.imageUrl);
+          if (
+            typeof res?.data?.data?.imageUrl === "string" &&
+            res.data.data.imageUrl.trim()
+          ) {
+            this.fileDataList[key].push(res.data.data.imageUrl);
+          }
         }
       } catch (err) {
         console.error("生成失败:", err);

+ 1 - 1
src/views/performance/js/allAnalysisType.js

@@ -519,7 +519,7 @@ export const allAnalysisType = [
     typeFlag: "minute",
     generalFiles: {
       FileTypeFromUrl: "production_indicator_total",
-      urlType: "productionIndicatorTotal",
+      urlType: "radarChart",
     },
     diagramRelations: {
       urlType: "",