|
@@ -1,27 +1,28 @@
|
|
|
+// puppeteer-radar-chart.js
|
|
|
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";
|
|
|
-// 获取 plotly.js 的绝对路径
|
|
|
-const plotlyPath = path.join(
|
|
|
+
|
|
|
+// 获取 echarts.min.js 的路径
|
|
|
+const echartsPath = path.join(
|
|
|
process.cwd(),
|
|
|
"src",
|
|
|
"public",
|
|
|
"js",
|
|
|
"echarts.min.js"
|
|
|
);
|
|
|
-const plotlyContent = await fs.readFile(plotlyPath, "utf-8");
|
|
|
+const echartsScript = await fs.readFile(echartsPath, "utf-8");
|
|
|
|
|
|
-// HTML 模板
|
|
|
+// 构造 HTML
|
|
|
const getHtmlContent = () => `
|
|
|
<!DOCTYPE html>
|
|
|
<html lang="en">
|
|
|
<head>
|
|
|
- <meta charset="UTF-8" />
|
|
|
+ <meta charset="UTF-8">
|
|
|
<title>Radar Chart</title>
|
|
|
- <script>${plotlyContent}</script>
|
|
|
+ <script>${echartsScript}</script>
|
|
|
<style>
|
|
|
html, body { margin: 0; padding: 0; width: 600px; height: 600px; }
|
|
|
#chart { width: 100%; height: 100%; }
|
|
@@ -30,49 +31,27 @@ const getHtmlContent = () => `
|
|
|
<body>
|
|
|
<div id="chart"></div>
|
|
|
<script>
|
|
|
- window.renderChart = function (chartData, itemCsvData,engineTypeCode) {
|
|
|
- function calcValues(data) {
|
|
|
- const matrix = data.map((item) => [
|
|
|
- Number(item.TurbinePowerRate),
|
|
|
- Number(item.TurbineRunRate),
|
|
|
- Number(item.WindSpeedAvr),
|
|
|
- Number(item.Thi),
|
|
|
- Number(item.Ws),
|
|
|
- ]);
|
|
|
-
|
|
|
- if (matrix.length === 0) {
|
|
|
- return {
|
|
|
- max: [0, 0, 0, 0, 0],
|
|
|
- min: [0, 0, 0, 0, 0],
|
|
|
- median: [0, 0, 0, 0, 0],
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- const max = matrix[0].map((_, i) =>
|
|
|
- Math.max(...matrix.map((row) => row[i] ?? 0))
|
|
|
- );
|
|
|
-
|
|
|
- const min = matrix[0].map((_, i) =>
|
|
|
- Math.min(...matrix.map((row) => row[i] ?? 0))
|
|
|
- );
|
|
|
-
|
|
|
- const median = matrix[0].map((_, i) => {
|
|
|
- const sorted = matrix
|
|
|
- .map((row) => Number(row[i]))
|
|
|
- .filter((v) => typeof v === "number" && !isNaN(v))
|
|
|
- .sort((a, b) => a - b);
|
|
|
- if (sorted.length === 0) return 0;
|
|
|
- const mid = Math.floor(sorted.length / 2);
|
|
|
- return sorted.length % 2 === 0
|
|
|
- ? (sorted[mid - 1] + sorted[mid]) / 2
|
|
|
- : sorted[mid];
|
|
|
- });
|
|
|
-
|
|
|
- return { max, min, median };
|
|
|
-}
|
|
|
-
|
|
|
+ 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 } = calcValues(itemCsvData);
|
|
|
+ const { max, min, median } = calcStats(itemCsvData);
|
|
|
const values = [
|
|
|
chartData.TurbinePowerRate,
|
|
|
chartData.TurbineRunRate,
|
|
@@ -92,7 +71,7 @@ const getHtmlContent = () => `
|
|
|
const chart = echarts.init(document.getElementById("chart"));
|
|
|
chart.setOption({
|
|
|
title: {
|
|
|
- text:engineTypeCode+'机型'+ chartData.wind_turbine_name + "机组指标",
|
|
|
+ text: engineTypeCode + '机型' + chartData.wind_turbine_name + "机组指标",
|
|
|
left: "center",
|
|
|
},
|
|
|
radar: {
|
|
@@ -102,6 +81,7 @@ const getHtmlContent = () => `
|
|
|
},
|
|
|
series: [
|
|
|
{
|
|
|
+ name: chartData.wind_turbine_name,
|
|
|
type: "radar",
|
|
|
data: [
|
|
|
{
|
|
@@ -109,6 +89,12 @@ const getHtmlContent = () => `
|
|
|
name: chartData.wind_turbine_name,
|
|
|
areaStyle: { color: "rgba(99,110,252,0.3)" },
|
|
|
},
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "中位值",
|
|
|
+ type: "radar",
|
|
|
+ data: [
|
|
|
{
|
|
|
value: median,
|
|
|
name: "中位值",
|
|
@@ -121,6 +107,11 @@ const getHtmlContent = () => `
|
|
|
},
|
|
|
],
|
|
|
});
|
|
|
+
|
|
|
+ // 等待渲染完成
|
|
|
+ chart.on("finished", () => {
|
|
|
+ window.chartFinished = true;
|
|
|
+ });
|
|
|
};
|
|
|
</script>
|
|
|
</body>
|
|
@@ -137,26 +128,17 @@ export const getRadarCharts = async (
|
|
|
) => {
|
|
|
const browser = await puppeteer.launch({
|
|
|
headless: "new",
|
|
|
- // 根据系统改路径
|
|
|
- executablePath: `${process.env.CHROME_PATH}`, // 根据系统改路径
|
|
|
-
|
|
|
+ executablePath: process.env.CHROME_PATH,
|
|
|
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
|
|
});
|
|
|
const page = await browser.newPage();
|
|
|
- // 创建临时目录
|
|
|
const tempDir = path.join(process.cwd(), "images");
|
|
|
await fs.ensureDir(tempDir);
|
|
|
- const tempFilePath = path.join(
|
|
|
- tempDir,
|
|
|
- `temp_scatter_chart_${Date.now()}.jpeg`
|
|
|
- );
|
|
|
+ const tempFilePath = path.join(tempDir, `radar_chart_${Date.now()}.jpeg`);
|
|
|
|
|
|
await page.setContent(getHtmlContent(), { waitUntil: "load" });
|
|
|
-
|
|
|
- // 等待 renderChart 被定义
|
|
|
await page.waitForFunction(() => typeof window.renderChart === "function");
|
|
|
|
|
|
- // 调用渲染函数
|
|
|
await page.evaluate(
|
|
|
(chartData, itemCsvData, engineTypeCode) => {
|
|
|
window.renderChart(chartData, itemCsvData, engineTypeCode);
|
|
@@ -166,37 +148,32 @@ export const getRadarCharts = async (
|
|
|
engineTypeCode
|
|
|
);
|
|
|
|
|
|
- // 再等待图表渲染完成(给 ECharts 时间)
|
|
|
- // (await page.waitForTimeout)
|
|
|
- // ? page.waitForTimeout(1000)
|
|
|
- // : new Promise((res) => setTimeout(res, 1000));
|
|
|
- (await page.waitForTimeout?.(1000)) ??
|
|
|
- new Promise((res) => setTimeout(res, 1000));
|
|
|
+ // ✅ 等待 echarts 渲染完成
|
|
|
+ await page.waitForFunction("window.chartFinished === true", {
|
|
|
+ timeout: 3000,
|
|
|
+ });
|
|
|
|
|
|
- // 上传逻辑
|
|
|
- // 截图并保存到临时文件
|
|
|
const chartElement = await page.$("#chart");
|
|
|
- await chartElement.screenshot({
|
|
|
- path: tempFilePath,
|
|
|
- type: "jpeg",
|
|
|
- });
|
|
|
+ await chartElement.screenshot({ path: tempFilePath, type: "jpeg" });
|
|
|
|
|
|
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`,
|
|
|
{
|
|
|
filePath: tempFilePath,
|
|
|
bucketName,
|
|
|
- objectName: newUrl + "/" + chartData.wind_turbine_name + ".jpg",
|
|
|
+ objectName: `${newUrl}/${chartData.wind_turbine_name}.jpg`,
|
|
|
}
|
|
|
);
|
|
|
+
|
|
|
return response?.data?.url;
|
|
|
} catch (error) {
|
|
|
console.error("❌ 上传失败:", error.message);
|
|
|
+ } finally {
|
|
|
+ await browser.close();
|
|
|
}
|
|
|
};
|