| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679 |
- import fs from "fs";
- import path from "path";
- import { fileURLToPath } from "url";
- import {
- registerImageBuffer,
- renderDocxReport,
- } from "../src/server/reportService/docxReportBuilder.js";
- import {
- buildDetectorSectionPayload,
- pickKeyTurbines,
- buildSensorHeatmapData,
- buildDetectorHeatmapData,
- buildSensorRadarData,
- buildDetectorRadarData,
- chunkHeatmapData,
- buildSensorAnomalyRows,
- } from "../src/server/reportService/anomalyReportMapper.js";
- import {
- DETECTOR_TEMPLATE_CONFIG,
- buildAnomalyRadarOption,
- buildDetectorPlotOption,
- buildEmptyPlaceholderOption,
- buildFarmPlotJsons,
- buildReportHeatmapOption,
- listDetectorPlotOptions,
- listFarmPlotOptionsFromTurbines,
- listFarmChartOptions,
- optionHasData,
- turbinePlotsToFarmJson,
- } from "../src/server/reportService/anomalyChartBuilder.js";
- import { renderEchartsOption } from "../src/server/reportService/echartsRenderer.js";
- import {
- initChartService,
- shutdownChartService,
- } from "../src/server/utils/chartService/index.js";
- import {
- diagnosticFarmSeriesColor,
- diagnosticSeriesSymbol,
- FARM_ANOMALY_WARM_COLORS,
- FARM_NORMAL_COOL_COLORS,
- } from "../shared/anomalyDiagnosticVisual.mjs";
- import { farmAggregateToPanels } from "../shared/anomalyDetectorPlotAdapter.mjs";
- import { fillFarmPanelsToRule, getFarmPanelRule } from "../shared/anomalyFarmChartRules.mjs";
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
- function samplePlot(labelPrefix = "") {
- return {
- title: `${labelPrefix}偏航`,
- xaxis: "时间",
- yaxis: "偏移 (°)",
- data: [
- {
- label: "正常偏航角",
- mode: "markers",
- timeData: [
- "2026-07-14 04:00:00",
- "2026-07-14 08:00:00",
- "2026-07-14 12:00:00",
- ],
- yData: [180, 200, 190],
- },
- {
- label: "12h均值",
- mode: "lines",
- timeData: [
- "2026-07-14 04:00:00",
- "2026-07-14 08:00:00",
- "2026-07-14 12:00:00",
- ],
- yData: [185, 198, 120],
- },
- ],
- };
- }
- function assert(condition, message) {
- if (!condition) throw new Error(message);
- }
- const scatterFarm = buildFarmPlotJsons(
- [
- {
- engineName: "A",
- plotJson: {
- xaxis: "风速",
- yaxis: "功率",
- data: [
- { label: "散点", mode: "markers", xData: [1, 2], yData: [3, 4] },
- { label: "上限", mode: "lines", xData: [1, 2], yData: [8, 9] },
- { label: "下限", mode: "lines", xData: [1, 2], yData: [1, 2] },
- {
- label: "参考功率曲线",
- mode: "lines",
- xData: [1, 2],
- yData: [5, 6],
- },
- ],
- },
- },
- {
- engineName: "B",
- plotJson: {
- xaxis: "风速",
- yaxis: "功率",
- data: [
- { label: "散点", mode: "markers", xData: [2, 3], yData: [4, 5] },
- {
- label: "参考功率曲线",
- mode: "lines",
- xData: [1, 2],
- yData: [5, 6],
- },
- ],
- },
- },
- ],
- "wind_power_scatter",
- "全场散点",
- );
- assert(
- scatterFarm[0].data.filter((row) => row.__shared).length === 1,
- "参考功率曲线应只保留一条",
- );
- assert(
- scatterFarm[0].data[scatterFarm[0].data.length - 1].__shared,
- "参考/合同功率曲线应放在图例最后",
- );
- assert(
- !scatterFarm[0].data.some((row) => /上限|下限/.test(row.label)),
- "散点总图不应含上下限",
- );
- assert(
- scatterFarm[0].data.some((row) => row.label === "A") &&
- scatterFarm[0].data.some((row) => row.label === "B"),
- "散点总图应按风机分系列",
- );
- const farmScatterOption = buildDetectorPlotOption(
- scatterFarm[0],
- scatterFarm[0].title,
- "anomalyScatterPO",
- );
- const farmScatterSeries = (farmScatterOption.series || []).filter(
- (item) => item.type === "scatter3D",
- );
- assert(farmScatterSeries.length > 0, "全场散点应输出 scatter3D 系列");
- assert(
- farmScatterSeries.every((item) => Number(item.symbolSize) >= 3),
- "全场散点 3D symbolSize 过小",
- );
- assert(farmScatterOption.grid3D, "风功率散点应输出 3D 图表");
- const qualityPanels = listDetectorPlotOptions(
- {
- panels: [
- {
- panelTitle: "功率因数 | 异常点: 0",
- xaxis: "时间",
- yaxis: "功率因数",
- data: [
- {
- label: "功率因数",
- mode: "markers",
- timeData: ["2026-08-01 00:00:00", "2026-08-01 01:00:00"],
- yData: [0.98, 0.97],
- },
- ],
- },
- {
- panelTitle: "电流不平衡度",
- xaxis: "时间",
- yaxis: "电流不平衡度",
- data: [
- {
- label: "电流不平衡度",
- mode: "markers",
- timeData: ["2026-08-01 00:00:00", "2026-08-01 01:00:00"],
- yData: [0.12, 0.15],
- },
- ],
- },
- ],
- },
- "DT01 电能质量分析",
- "anomalyPowerqualityPO",
- );
- assert(qualityPanels.length === 2, "电能质量单机图应按 panel 全部出图");
- assert(
- qualityPanels[1].yAxis.name === "电流不平衡度",
- "第二张单机图应使用对应 panel 的 Y 轴",
- );
- assert(
- (qualityPanels[1].series || []).some(
- (item) => Array.isArray(item.data) && item.data.length > 0,
- ),
- "单机图系列不能为空",
- );
- const pitchFarm = buildFarmPlotJsons(
- [
- {
- engineName: "101",
- plotJson: {
- panels: [
- {
- panelTitle: "桨距角时序",
- xaxis: "时间",
- yaxis: "桨距角",
- data: [
- {
- label: "桨叶 1",
- mode: "markers",
- timeData: ["2026-01-01 00:00"],
- yData: [1],
- },
- {
- label: "桨叶 2",
- mode: "lines",
- timeData: ["2026-01-01 00:00"],
- yData: [2],
- },
- {
- label: "桨叶 3",
- mode: "lines",
- timeData: ["2026-01-01 00:00"],
- yData: [3],
- },
- {
- label: "阈值参考",
- mode: "lines",
- timeData: ["2026-01-01 00:00"],
- yData: [4],
- },
- ],
- },
- {
- panelTitle: "桨距角差",
- xaxis: "时间",
- yaxis: "差值",
- data: [
- {
- label: "桨叶 1",
- mode: "markers",
- timeData: ["2026-01-01 00:00"],
- yData: [1],
- },
- ],
- },
- ],
- },
- },
- ],
- "pitch_regulation",
- "全场变桨一致性",
- );
- assert(pitchFarm.length === 2, "变桨一致性总图应为 2 张");
- assert(
- /各桨叶分风速段/.test(pitchFarm[0].title || ""),
- "变桨一致性第一张应为各桨叶分风速段中位数趋势",
- );
- assert(
- /极差/.test(pitchFarm[1].title || ""),
- "变桨一致性第二张应为桨叶极差时序及异常识别",
- );
- assert(
- !pitchFarm[0].data.some((row) => /阈值/.test(row.originalLabel || "")),
- "变桨一致性不应展示阈值参考",
- );
- const pitchFarmReportOptions = listFarmChartOptions(
- [
- {
- engineName: "101",
- plotJson: {
- panels: [
- {
- panelTitle: "桨距角时序",
- xaxis: "时间",
- yaxis: "桨距角",
- data: [
- { label: "桨叶 1", mode: "markers", timeData: ["2026-01-01 00:00"], yData: [1] },
- { label: "桨叶 2", mode: "lines", timeData: ["2026-01-01 00:00"], yData: [2] },
- { label: "桨叶 3", mode: "lines", timeData: ["2026-01-01 00:00"], yData: [3] },
- { label: "阈值参考", mode: "lines", timeData: ["2026-01-01 00:00"], yData: [4] },
- ],
- },
- {
- panelTitle: "桨距角差",
- xaxis: "时间",
- yaxis: "差值",
- data: [
- { label: "桨叶 1", mode: "markers", timeData: ["2026-01-01 00:00"], yData: [1] },
- ],
- },
- ],
- },
- },
- ],
- "全场变桨一致性",
- "anomalyPitchregulationPO",
- { templateKey: "pitch_regulation", farmMode: true },
- );
- assert(pitchFarmReportOptions.length === 2, "报告变桨一致性应出 2 张图");
- const deloadFarm = buildFarmPlotJsons(
- [
- {
- engineName: "101",
- plotJson: {
- panels: [
- {
- panelTitle: "风速",
- xaxis: "时间",
- yaxis: "风速",
- data: [{ label: "风速", timeData: ["t"], yData: [1] }],
- },
- {
- panelTitle: "有功功率时序",
- xaxis: "时间",
- yaxis: "有功功率",
- data: [
- { label: "正常", mode: "markers", timeData: ["t"], yData: [1] },
- { label: "异常", mode: "markers", timeData: ["t"], yData: [2] },
- ],
- },
- ],
- },
- },
- ],
- "ctrl_deload",
- "全场降载",
- );
- assert(deloadFarm.length === 1, "降载总图只出有功功率时序");
- assert(
- /有功功率/.test(deloadFarm[0].title + deloadFarm[0].yaxis),
- "降载总图应为有功功率",
- );
- assert(
- diagnosticFarmSeriesColor(
- { originalLabel: "正常", turbineIndex: 1 },
- 0,
- "ctrl_deload",
- ) ===
- diagnosticFarmSeriesColor(
- { originalLabel: "异常", turbineIndex: 1 },
- 1,
- "ctrl_deload",
- ),
- "降载图同一风机的正常和异常点应使用相同颜色",
- );
- assert(
- FARM_NORMAL_COOL_COLORS.includes(
- diagnosticFarmSeriesColor(
- { originalLabel: "正常", turbineIndex: 0 },
- 0,
- "pitch_coord",
- ),
- ),
- "总图正常风机应使用冷色",
- );
- assert(
- FARM_ANOMALY_WARM_COLORS.includes(
- diagnosticFarmSeriesColor(
- { originalLabel: "异常", turbineIndex: 0 },
- 0,
- "pitch_coord",
- ),
- ),
- "总图异常风机应使用红橙暖色",
- );
- assert(
- diagnosticFarmSeriesColor(
- { extra: { anomaly: true }, originalLabel: "正常", turbineIndex: 3 },
- 0,
- "ctrl_deload",
- ) ===
- diagnosticFarmSeriesColor(
- { extra: { anomaly: true }, originalLabel: "异常", turbineIndex: 3 },
- 1,
- "ctrl_deload",
- ) &&
- FARM_ANOMALY_WARM_COLORS.includes(
- diagnosticFarmSeriesColor(
- { extra: { anomaly: true }, turbineIndex: 3 },
- 0,
- "ctrl_deload",
- ),
- ),
- "异常降载风机应整机暖色且正常/异常点同色",
- );
- assert(
- diagnosticSeriesSymbol({ originalLabel: "正常" }) !==
- diagnosticSeriesSymbol({ originalLabel: "异常" }),
- "降载图应使用不同点形区分正常和异常",
- );
- const deloadRateOption = listDetectorPlotOptions(
- {
- panels: [
- {
- panelTitle: "有功功率时序",
- xaxis: "时间",
- yaxis: "有功功率",
- data: [
- {
- label: "正常运行",
- mode: "markers",
- timeData: ["2026-08-22 00:00:00", "2026-08-22 04:00:00"],
- yData: [1000, 1100],
- },
- ],
- },
- {
- panelTitle: "累计降载率随时间变化",
- xaxis: "时间",
- yaxis: "累计降载率 (%)",
- data: [
- {
- label: "累计降载率",
- mode: "markers",
- timeData: [
- "2026-08-22 00:00:00",
- "2026-08-22 04:00:00",
- "2026-08-22 08:00:00",
- ],
- yData: [14, 2, 3],
- },
- {
- label: "正常上限 5%",
- mode: "markers",
- timeData: ["2026-08-22 00:00:00", "2026-08-22 08:00:00"],
- yData: [5, 5],
- },
- {
- label: "频繁阈值 20%",
- mode: "markers",
- timeData: ["2026-08-22 00:00:00"],
- yData: [20],
- },
- ],
- },
- ],
- },
- "002 降载判定分析",
- "anomalyDeloadPO",
- { templateKey: "ctrl_deload" },
- );
- assert(deloadRateOption.length === 2, "单机降载应按 panel 出图");
- const rateChart = deloadRateOption[1];
- assert(
- rateChart.series.every((item) => item.type === "line"),
- "累计降载率应为折线图",
- );
- assert(
- rateChart.series.find((item) => item.name === "累计降载率")?.lineStyle
- .type === "solid",
- "累计降载率应为实线",
- );
- assert(
- rateChart.series.find((item) => /正常上限/.test(item.name))?.lineStyle
- .type === "dashed",
- "正常上限应为虚线",
- );
- assert(
- rateChart.series.find((item) => /频繁阈值/.test(item.name))?.lineStyle
- .type === "dashed",
- "频繁阈值应为虚线",
- );
- assert(
- (rateChart.series.find((item) => /频繁阈值/.test(item.name))?.data || [])
- .length >= 2,
- "阈值线应拉满时间轴",
- );
- const opFarm = buildFarmPlotJsons(
- [
- {
- engineName: "101",
- plotJson: {
- panels: [
- {
- panelTitle: "投影",
- xaxis: "x",
- yaxis: "y",
- data: [{ label: "簇", xData: [1], yData: [1] }],
- },
- {
- panelTitle: "转速-功率散点",
- xaxis: "转速",
- yaxis: "功率",
- data: [{ label: "散点", xData: [1], yData: [2] }],
- },
- ],
- },
- },
- ],
- "ctrl_op_state",
- "全场运行状态",
- );
- assert(opFarm.length === 1, "运行状态总图只出一张");
- assert(
- /转速/.test(opFarm[0].title + opFarm[0].xaxis),
- "运行状态总图应为转速-功率",
- );
- const pqFarm = buildFarmPlotJsons(
- [
- {
- engineName: "101",
- plotJson: {
- panels: [
- {
- panelTitle: "三相电流不平衡度 | 异常点: 0",
- xaxis: "时间",
- yaxis: "电流不平衡度",
- data: [{ label: "点", xData: [1], yData: [1] }],
- },
- {
- panelTitle: "三相电压不平衡度 | 异常点: 0",
- xaxis: "时间",
- yaxis: "电压不平衡度",
- data: [{ label: "点", xData: [1], yData: [1] }],
- },
- {
- panelTitle: "功率因数 | 异常点: 0",
- xaxis: "时间",
- yaxis: "功率因数",
- data: [{ label: "点", xData: [1], yData: [1] }],
- },
- ],
- },
- },
- ],
- "ctrl_power_quality",
- "全场电能质量",
- );
- assert(pqFarm.length === 3, "电能质量总图应出三张");
- assert(
- pqFarm.length === getFarmPanelRule("ctrl_power_quality").count,
- "电能质量图数必须与共享报告模板规则一致",
- );
- assert(
- /电流不平衡/.test(pqFarm[0].title + pqFarm[0].yaxis),
- "第一张应为三相电流不平衡度",
- );
- assert(
- /电压不平衡/.test(pqFarm[1].title + pqFarm[1].yaxis),
- "第二张应为三相电压不平衡度",
- );
- assert(
- /功率因数/.test(pqFarm[2].title + pqFarm[2].yaxis),
- "第三张应为功率因数",
- );
- function loadLocalFarmJson(name) {
- const filePath = path.resolve(__dirname, `../../SVLgVZfi/${name}`);
- if (!fs.existsSync(filePath)) return null;
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
- }
- function dummyFarmPanel(title, yaxis) {
- return {
- title,
- xaxis: "x",
- yaxis,
- data: [{ label: "占位", xData: [1], yData: [1] }],
- };
- }
- const localPitchFarm = loadLocalFarmJson("farm_pitch_regulation.json");
- if (localPitchFarm) {
- const overlay = farmAggregateToPanels(localPitchFarm, {
- templateKey: "pitch_regulation",
- });
- const filled = fillFarmPanelsToRule(
- overlay,
- [
- dummyFarmPanel("各桨叶分风速段中位数趋势", "桨距角 (°)"),
- dummyFarmPanel("桨叶极差时序及异常识别", "桨叶极差 (°)"),
- ],
- "pitch_regulation",
- );
- assert(filled.length === 2, "变桨一致性页面总图应为 2 面");
- assert(/各桨叶分风速段/.test(filled[0].title), "第一面应为各桨叶分风速段中位数趋势");
- assert(/极差/.test(filled[1].title), "第二面应为桨叶极差时序及异常识别");
- assert(filled[1].data.length > 1, "极差面应保留 farm JSON 真实叠加");
- }
- const localCoordFarm = loadLocalFarmJson("farm_pitch_coord.json");
- if (localCoordFarm) {
- const overlay = farmAggregateToPanels(localCoordFarm, {
- templateKey: "pitch_coord",
- });
- const filled = fillFarmPanelsToRule(
- overlay,
- [
- dummyFarmPanel("功率-转速散点(桨距角着色)", "有功功率 (kW)"),
- dummyFarmPanel("桨距角-转速散点", "桨距角 (°)"),
- ],
- "pitch_coord",
- );
- assert(filled.length === 2, "变桨协调页面总图应为 2 面");
- assert(/着色/.test(filled[0].title), "变桨协调第一面应为桨距角着色");
- assert(/桨距角-转速/.test(filled[1].title), "变桨协调第二面应为桨距角-转速散点");
- }
- const localPqFarm = loadLocalFarmJson("farm_ctrl_power_quality.json");
- if (localPqFarm) {
- const overlay = farmAggregateToPanels(localPqFarm, {
- templateKey: "ctrl_power_quality",
- });
- const filled = fillFarmPanelsToRule(
- overlay,
- [
- dummyFarmPanel("三相电流不平衡度", "电流不平衡度"),
- dummyFarmPanel("三相电压不平衡度", "电压不平衡度"),
- dummyFarmPanel("功率因数", "功率因数"),
- ],
- "ctrl_power_quality",
- );
- assert(filled.length === 3, "电能质量页面总图应为 3 面");
- assert(/电流不平衡/.test(filled[0].title), "电能质量第一面应为电流不平衡");
- assert(/电压不平衡/.test(filled[1].title), "电能质量第二面应为电压不平衡");
- assert(/功率因数/.test(filled[2].title), "电能质量第三面应为功率因数");
- assert(filled[2].data.length > 1, "功率因数面应保留 farm JSON 真实叠加");
- }
- const rawPointCount = 9001;
- const rawX = Array.from({ length: rawPointCount }, (_, index) => index);
- const rawY = rawX.map((value) => value * 2);
- const rawPointFarm = buildFarmPlotJsons(
- [
- {
- engineName: "101",
- plotJson: {
- panels: [
- {
- panelTitle: "转速-功率散点",
- xaxis: "转速",
- yaxis: "功率",
- data: [
- { label: "散点", mode: "markers", xData: rawX, yData: rawY },
- ],
- },
- ],
- },
- },
- ],
- "ctrl_op_state",
- "全场运行状态",
- );
- assert(
- rawPointFarm[0].data[0].xData.length === rawPointCount,
- "全场图不得抽样原始数据点",
- );
- const rawPointOption = buildDetectorPlotOption(
- rawPointFarm[0],
- rawPointFarm[0].title,
- "anomalyOperationPO",
- );
- assert(
- rawPointOption.series[0].data.length === rawPointCount,
- "报告图渲染不得抽样原始数据点",
- );
- console.log("farm chart rules ok");
- const curveFarm = buildFarmPlotJsons(
- [
- {
- engineName: "DT01",
- plotJson: {
- xaxis: "风速 (m/s)",
- yaxis: "有功功率 (kW)",
- data: [
- {
- label: "30天功率曲线",
- mode: "lines",
- timeData: ["2026-01-01 00:00:00", "2026-01-01 01:00:00"],
- xData: [4, 12],
- yData: [200, 2000],
- },
- {
- label: "合同功率曲线",
- mode: "lines",
- timeData: ["2026-01-01 00:00:00", "2026-01-01 01:00:00"],
- xData: [4, 12],
- yData: [180, 2100],
- },
- ],
- },
- },
- {
- engineName: "DT02",
- plotJson: {
- xaxis: "风速 (m/s)",
- yaxis: "有功功率 (kW)",
- data: [
- {
- label: "30天功率曲线",
- mode: "lines",
- timeData: ["2026-01-01 00:00:00", "2026-01-01 01:00:00"],
- xData: [5, 11],
- yData: [300, 1900],
- },
- {
- label: "合同功率曲线",
- mode: "lines",
- xData: [4, 12],
- yData: [180, 2100],
- },
- ],
- },
- },
- ],
- "wind_power_curve",
- "全场功率曲线",
- );
- assert(curveFarm.length === 1, "功率曲线总图应 1 张");
- assert(
- curveFarm[0].data.filter((row) => row.__shared).length === 1,
- "合同功率曲线全场只保留一条",
- );
- assert(
- curveFarm[0].data[curveFarm[0].data.length - 1].label === "合同功率曲线",
- "合同功率曲线应在图例末尾",
- );
- const curveOption = buildDetectorPlotOption(
- curveFarm[0],
- curveFarm[0].title,
- "anomalyPowercurvePO",
- );
- assert(curveOption.grid3D, "功率曲线应输出 3D 图表");
- assert(curveOption.xAxis3D?.name === "风机机位", "3D X 轴应为风机机位");
- assert(/风速/.test(String(curveOption.yAxis3D?.name || "")), "3D Y 轴应为风速");
- assert(/功率/.test(String(curveOption.zAxis3D?.name || "")), "3D Z 轴应为功率");
- assert(
- curveOption.series.some(
- (item) =>
- (item.type === "scatter3D" || item.type === "line3D") &&
- (item.data?.[0]?.[1] === 4 || item.data?.[0]?.[1] === 5),
- ),
- "功率曲线 3D Y 轴应使用风速",
- );
- assert(
- curveOption.series.some(
- (item) => item.type === "line3D" && item.data?.[0]?.[0] > 0,
- ),
- "功率曲线各风机系列 X 轴应为机组序号",
- );
- assert(
- !curveOption.series.some(
- (item) =>
- item.type === "scatter3D" && item.name && !/参考|合同/.test(item.name),
- ),
- "功率曲线风机数据不应为 scatter3D",
- );
- const misplacedX = buildDetectorPlotOption(
- {
- title: "DT01 功率曲线分析",
- xaxis: "风速 (m/s)",
- yaxis: "有功功率 (kW)",
- data: [
- {
- label: "30天功率曲线",
- mode: "lines",
- timeData: ["2026-01-01 00:00:00", "2026-01-01 01:00:00"],
- xData: [6, 10],
- yData: [400, 1800],
- },
- ],
- },
- "DT01 功率曲线分析",
- "anomalyPowercurvePO",
- );
- assert(misplacedX.series[0].data[0][0] === 6, "单机功率曲线应取 xData 风速");
- assert(!misplacedX.grid3D, "单机功率曲线应保持 2D");
- assert(misplacedX.xAxis?.type === "value", "单机功率曲线风速轴应为数值轴");
- const pitchCoordFarm = buildFarmPlotJsons(
- [
- {
- engineName: "DT40",
- plotJson: {
- xaxis: "转速 (r/min)",
- yaxis: "有功功率 (kW)",
- data: [
- {
- label: "散点",
- mode: "lines",
- timeData: ["2026-01-01 00:00:00", "2026-01-01 01:00:00"],
- xData: [8, 12],
- yData: [400, 1800],
- },
- ],
- },
- },
- ],
- "pitch_coord",
- "全场变桨协调",
- );
- const pitchCoordOption = buildDetectorPlotOption(
- pitchCoordFarm[0],
- pitchCoordFarm[0].title,
- "anomalyPitchcoordPO",
- );
- assert(pitchCoordOption.series[0].type === "scatter", "变桨协调应输出散点");
- assert(pitchCoordOption.series[0].data[0][0] === 8, "变桨协调 X 轴应为转速");
- assert(
- Number(pitchCoordOption.series[0].symbolSize) >= 12,
- "全场变桨协调散点应加大",
- );
- const pitchCoordFarmTwo = buildFarmPlotJsons(
- [
- {
- engineName: "001",
- plotJson: {
- panels: [
- {
- panelTitle: "功率-转速散点(桨距角着色)",
- xaxis: "转速 (r/min)",
- yaxis: "有功功率 (kW)",
- data: [
- {
- label: "正常数据",
- mode: "markers",
- xData: [8, 12],
- yData: [400, 1800],
- colorbar: [1, 8],
- colorbarTitle: "桨距角 (°)",
- },
- {
- label: "异常点",
- mode: "markers",
- xData: [10],
- yData: [900],
- colorbar: [12],
- },
- ],
- },
- {
- panelTitle: "桨距角-转速散点",
- xaxis: "转速 (r/min)",
- yaxis: "桨距角 (°)",
- data: [
- {
- label: "正常数据",
- mode: "markers",
- xData: [8, 12],
- yData: [1, 8],
- },
- ],
- },
- ],
- },
- },
- {
- engineName: "005",
- plotJson: {
- panels: [
- {
- panelTitle: "功率-转速散点(桨距角着色)",
- xaxis: "转速 (r/min)",
- yaxis: "有功功率 (kW)",
- data: [
- {
- label: "正常数据",
- mode: "markers",
- xData: [9, 11],
- yData: [500, 1600],
- colorbar: [2, 7],
- colorbarTitle: "桨距角 (°)",
- },
- ],
- },
- {
- panelTitle: "桨距角-转速散点",
- xaxis: "转速 (r/min)",
- yaxis: "桨距角 (°)",
- data: [
- {
- label: "正常数据",
- mode: "markers",
- xData: [9, 11],
- yData: [2, 7],
- },
- ],
- },
- ],
- },
- },
- ],
- "pitch_coord",
- "全场变桨协调分析检测汇总",
- );
- assert(pitchCoordFarmTwo.length === 2, "变桨协调总图应为 2 张");
- assert(
- /功率-转速散点(桨距角着色)/.test(pitchCoordFarmTwo[0].title),
- "第一张应为功率-转速散点(桨距角着色)",
- );
- assert(
- /桨距角-转速散点/.test(pitchCoordFarmTwo[1].title),
- "第二张应为桨距角-转速散点",
- );
- const pitchColorOption = buildDetectorPlotOption(
- pitchCoordFarmTwo[0],
- pitchCoordFarmTwo[0].title,
- "anomalyPitchcoordPO",
- );
- assert(Array.isArray(pitchColorOption.visualMap), "功率-转速图应按桨距角着色");
- assert(
- pitchColorOption.series.some((item) => item.name === "正常数据"),
- "桨距角着色图应按正常/异常分系列而非每台风机",
- );
- assert(
- pitchColorOption.series.find((item) => item.name === "正常数据")?.data
- .length === 4 &&
- pitchColorOption.series.find((item) => item.name === "异常点")?.data
- .length === 1,
- "桨距角着色图合并系列时不得丢失原始点",
- );
- const pitchSpeedOption = buildDetectorPlotOption(
- pitchCoordFarmTwo[1],
- pitchCoordFarmTwo[1].title,
- "anomalyPitchcoordPO",
- );
- assert(
- pitchSpeedOption.series.some((item) => item.name === "001") &&
- pitchSpeedOption.series.some((item) => item.name === "005"),
- "桨距角-转速图应按风机分色",
- );
- const pitchStyleOption = listDetectorPlotOptions(
- {
- xaxis: "风速分箱中心值 (m/s)",
- yaxis: "桨距角中位数 (°)",
- data: [
- {
- label: "桨叶 1",
- mode: "lines",
- xData: [2, 6],
- yData: [1, 2],
- timeData: ["t1", "t2"],
- },
- {
- label: "桨叶 2",
- mode: "lines",
- xData: [2, 6],
- yData: [1.1, 2.1],
- },
- {
- label: "桨叶 3",
- mode: "lines",
- xData: [2, 6],
- yData: [1.2, 2.2],
- },
- ],
- },
- "DT01 变桨一致性分析",
- "anomalyPitchregulationPO",
- { templateKey: "pitch_regulation" },
- )[0];
- assert(pitchStyleOption.series.length === 3, "单机变桨一致性应绘出三支桨叶");
- assert(pitchStyleOption.series[0].symbol === "rect", "桨叶1应为方形");
- assert(pitchStyleOption.series[0].lineStyle.type === "dashed", "桨叶1应为虚线");
- assert(pitchStyleOption.series[1].symbol === "circle", "桨叶2应为圆形");
- assert(pitchStyleOption.series[1].lineStyle.type === "solid", "桨叶2应为实线");
- assert(pitchStyleOption.series[2].symbol === "triangle", "桨叶3应为三角");
- assert(pitchStyleOption.series[2].lineStyle.type === "dotted", "桨叶3应为点线");
- assert(
- pitchStyleOption.series[0].data[0][0] === 2,
- "变桨一致性 X 轴应使用分箱风速",
- );
- const yawErrorOptions = listDetectorPlotOptions(
- {
- title: "静态偏航误差分析",
- xaxis: "对风角度/偏航误差 (°)",
- yaxis: "平均有功功率 (kW)",
- data: [
- {
- label: "4.5-5.0 m/s",
- mode: "lines+markers",
- xData: [-2, 0, 4],
- yData: [300, 420, 380],
- },
- {
- label: "5.0-5.5 m/s",
- mode: "lines+markers",
- xData: [-1, 2, 5],
- yData: [410, 500, 430],
- },
- ],
- },
- "01 静态偏航误差分析",
- "anomalyYawErrorPO",
- { templateKey: "yaw_error" },
- );
- assert(yawErrorOptions.length === 1, "偏航误差单机图应按 data[] 出一张");
- assert(yawErrorOptions[0].series.length === 2, "偏航误差应按风速箱分系列");
- assert(
- yawErrorOptions[0].series[0].markPoint?.data?.[0]?.coord?.[0] === 0,
- "偏航误差应标注平均功率最大的对风角度",
- );
- const yawCountOptions = listDetectorPlotOptions(
- {
- title: "偏航次数检测(CW/CCW 跳变)",
- xaxis: "时间",
- yaxis: "状态 (0/1)",
- data: [
- {
- label: "CW 偏航状态",
- mode: "lines",
- xData: ["2026-09-11 00:00:00", "2026-09-11 00:01:00"],
- yData: [0, 1],
- timeData: ["2026-09-11 00:00:00", "2026-09-11 00:01:00"],
- },
- {
- label: "CCW 偏航状态",
- mode: "lines",
- xData: ["2026-09-11 00:00:00", "2026-09-11 00:01:00"],
- yData: [0, 0],
- timeData: ["2026-09-11 00:00:00", "2026-09-11 00:01:00"],
- },
- ],
- },
- "01 偏航次数分析",
- "anomalyYawCountPO",
- { templateKey: "yaw_count" },
- );
- assert(yawCountOptions[0].yAxis.min === -0.05, "偏航次数 Y 轴应为 0/1");
- assert(yawCountOptions[0].series.length === 2, "偏航次数应绘制 CW/CCW");
- assert(
- yawCountOptions[0].xAxis.type === "time",
- "偏航次数横轴应为时间",
- );
- const pitchMinOptions = listDetectorPlotOptions(
- {
- title: "最小桨距角分布",
- xaxis: "时间",
- yaxis: "桨距角 (°)",
- xrange: ["2026-09-05", "2026-09-11"],
- yrange: [-1.0, 4.5],
- data: [
- {
- label: "最小桨距角分布",
- mode: "markers",
- xData: ["2026-09-05", "2026-09-06"],
- yData: [-0.5, 0.2],
- colorbar: [120, 40],
- colorbarTitle: "点数",
- },
- ],
- },
- "01 最小桨距角分析",
- "anomalyMinpitchPO",
- { templateKey: "pitch_min" },
- );
- assert(pitchMinOptions[0].series[0].type === "scatter", "最小桨距角应为散点");
- assert(
- typeof pitchMinOptions[0].series[0].symbolSize === "function",
- "最小桨距角气泡大小应跟随点数",
- );
- const opStateOptions = listDetectorPlotOptions(
- {
- detector_name: "ctrl_op_state",
- title: "运行状态综合异常检测",
- panels: [
- {
- panelTitle: "转速-功率散点图",
- xaxis: "转速(r/min)",
- yaxis: "有功功率(kW)",
- xrange: [0, 30],
- yrange: [0, 800],
- data: [
- {
- label: "正常数据",
- mode: "markers",
- xData: [12, 18],
- yData: [320, 500],
- },
- ],
- },
- {
- panelTitle: "PCA 降维投影",
- xaxis: "PC1",
- yaxis: "PC2",
- componentDefinition: [
- {
- name: "PC1",
- explainedVarianceRatio: 0.63,
- loadings: [
- {
- feature: "p_active",
- displayName: "z(功率)",
- coefficient: 0.81,
- standardized: true,
- },
- ],
- },
- ],
- data: [
- {
- label: "投影点",
- mode: "markers",
- xData: [0.1],
- yData: [0.2],
- },
- ],
- },
- ],
- },
- "01 运行状态分析",
- "anomalyOperationPO",
- { templateKey: "ctrl_op_state" },
- );
- assert(opStateOptions.length === 2, "运行状态单机图应按 panels[] 全部出图");
- assert(
- String(opStateOptions[1].title?.subtext || "").includes("PC1"),
- "运行状态 PCA 面板应展示主成分定义",
- );
- const farmCurveOptions = listDetectorPlotOptions(
- {
- detector: "wind_power_curve",
- farm: "XX风电场",
- chartType: "series",
- xaxis: "风速 (m/s)",
- yaxis: "有功功率 (kW)",
- xrange: [3.25, 25.25],
- turbines: [
- {
- turbine: "a1",
- anomaly: false,
- xType: "number",
- xData: [3.25, 4],
- yData: [120, 200],
- ratio: 1.02,
- status: "正常",
- },
- {
- turbine: "a2",
- anomaly: true,
- xType: "number",
- xData: [3.25, 4],
- yData: [80, 150],
- ratio: 0.7,
- status: "欠发",
- },
- ],
- },
- "全场功率曲线分析检测汇总",
- "anomalyPowercurvePO",
- {
- templateKey: "wind_power_curve",
- farmMode: true,
- nameMap: { a1: "01", a2: "02" },
- },
- );
- assert(farmCurveOptions.length === 1, "风场功率曲线总图应为一张叠加图");
- assert(farmCurveOptions[0].grid3D, "风场功率曲线总图应按全场 3D 渲染");
- assert(
- farmCurveOptions[0].series.some((item) => /01号/.test(item.name)),
- "风场总图应把 engineId 映射成风机名",
- );
- const farmYawErrorOptions = listDetectorPlotOptions(
- {
- detector: "yaw_error",
- chartType: "value",
- turbines: [
- { turbine: "a1", anomaly: true, value: 15, powerloss: 2.1, coverage: 0.9 },
- { turbine: "a2", anomaly: false, value: 3, powerloss: 0.1, coverage: 0.95 },
- ],
- },
- "全场静态偏航误差分析检测汇总",
- "anomalyYawErrorPO",
- {
- templateKey: "yaw_error",
- farmMode: true,
- nameMap: { a1: "01", a2: "02" },
- },
- );
- assert(farmYawErrorOptions[0].series[0].type === "bar", "风场偏航误差总图应为柱状对比");
- assert(
- farmYawErrorOptions[0].series.some((item) => item.name === "风场均值"),
- "风场 value 型总图应绘制均值线",
- );
- const farmYawStaticOptions = listDetectorPlotOptions(
- {
- detector: "yaw_static",
- chartType: "series",
- xaxis: "时间",
- yaxis: "偏航角 (°)",
- turbines: [
- {
- turbine: "t1",
- anomaly: true,
- xType: "time",
- xData: [
- "2026-09-11 00:00:00",
- "2026-09-11 00:01:00",
- "2026-09-11 00:02:00",
- ],
- yData: [10, 80, 12],
- pointAnomaly: [false, true, false],
- },
- ],
- },
- "全场静态偏航分析检测汇总",
- "anomalyStaticyawPO",
- { templateKey: "yaw_static", farmMode: true },
- );
- assert(
- farmYawStaticOptions[0].series.length === 2,
- "风场静态偏航总图应按 pointAnomaly 拆成正常/异常点",
- );
- const stitchedFarmJson = turbinePlotsToFarmJson(
- [
- {
- engineId: "a1",
- engineName: "01",
- plotJson: {
- title: "功率曲线",
- xaxis: "风速 (m/s)",
- yaxis: "有功功率 (kW)",
- data: [
- {
- label: "实测功率",
- mode: "lines",
- xData: [3.25, 4],
- yData: [120, 200],
- ratio: 1.02,
- status: "正常",
- },
- ],
- },
- },
- {
- engineId: "a2",
- engineName: "02",
- po: { detectorIsAnomaly: 1 },
- plotJson: {
- title: "功率曲线",
- xaxis: "风速 (m/s)",
- yaxis: "有功功率 (kW)",
- data: [
- {
- label: "实测功率",
- mode: "lines",
- xData: [3.25, 4],
- yData: [80, 150],
- ratio: 0.7,
- status: "欠发",
- },
- ],
- },
- },
- ],
- "wind_power_curve",
- );
- assert(stitchedFarmJson?.chartType === "series", "单机 JSON 拼总图应为 series");
- assert(stitchedFarmJson.turbines.length === 2, "单机 JSON 拼总图应包含全部风机");
- const stitchedFarmOptions = listFarmPlotOptionsFromTurbines(
- [
- {
- engineId: "a1",
- engineName: "01",
- plotJson: {
- title: "功率曲线",
- xaxis: "风速 (m/s)",
- yaxis: "有功功率 (kW)",
- data: [{ label: "实测功率", xData: [3.25, 4], yData: [120, 200] }],
- },
- },
- ],
- "全场功率曲线分析检测汇总",
- "anomalyPowercurvePO",
- {
- templateKey: "wind_power_curve",
- nameMap: { a1: "01" },
- },
- );
- assert(stitchedFarmOptions[0].grid3D, "报告总图应与页面一样走全场 3D");
- const yawErrorFarmFromTurbines = listFarmPlotOptionsFromTurbines(
- [
- {
- engineId: "a1",
- engineName: "01",
- po: { detectorIsAnomaly: 1, detectorAnomalyRate: 15 },
- plotJson: {
- xaxis: "对风角度/偏航误差 (°)",
- yaxis: "平均有功功率 (kW)",
- data: [
- {
- label: "4.5-5.0 m/s",
- xData: [-2, 0, 4],
- yData: [300, 420, 380],
- },
- {
- label: "5.0-5.5 m/s",
- xData: [-1, 2, 5],
- yData: [410, 500, 430],
- },
- ],
- },
- },
- {
- engineId: "a2",
- engineName: "02",
- po: { detectorIsAnomaly: 0, detectorAnomalyRate: 3 },
- plotJson: {
- data: [
- {
- label: "4.5-5.0 m/s",
- xData: [-1, 3],
- yData: [200, 260],
- },
- ],
- },
- },
- ],
- "全场静态偏航误差分析检测汇总",
- "anomalyYawErrorPO",
- {
- templateKey: "yaw_error",
- nameMap: { a1: "01", a2: "02" },
- },
- );
- assert(
- yawErrorFarmFromTurbines[0].series[0].type === "bar",
- "报告偏航误差总图应与页面一样为单值柱状",
- );
- assert(
- yawErrorFarmFromTurbines[0].series.some((item) => item.name === "风场均值"),
- "报告 value 型总图应绘制均值线",
- );
- const emptyOption = buildEmptyPlaceholderOption("全场检测");
- assert(!optionHasData(emptyOption), "占位图不应视为有数据");
- assert(
- JSON.stringify(emptyOption.title).includes("暂无"),
- "无 JSON 时应输出暂无占位",
- );
- const watchlist = pickKeyTurbines(
- [
- { engineId: "1", turbineName: "DT01", anomalyPoints: 99 },
- { engineId: "2", turbineName: "DT02", anomalyPoints: 1 },
- ],
- [
- { engineId: "2", model3PitchPitchcoordRatio: 0.6 },
- { engineId: "1", model3PitchPitchcoordRatio: 0.1 },
- ],
- );
- assert(
- watchlist.length === 1 && watchlist[0].turbineName === "DT02",
- "第6章应只取关注机组",
- );
- assert(
- pickKeyTurbines([{ engineId: "1" }], []).length === 0,
- "无关注机组时应为空",
- );
- const emptyRowsPayload = buildDetectorSectionPayload(
- DETECTOR_TEMPLATE_CONFIG.find((cfg) => cfg.templateKey === "pitch_coord"),
- { farmImages: [], turbineImages: [], rows: [] },
- );
- assert(
- emptyRowsPayload.pitch_coordRows[0].turbine_name === "暂无",
- "异常机组清单无数据时应写暂无",
- );
- const emptySensorRows = buildSensorAnomalyRows([]);
- assert(
- emptySensorRows[0].turbine_name === "暂无",
- "数据感知统计无数据时应写暂无",
- );
- const sensorHeat = buildSensorHeatmapData([
- {
- engineName: "09",
- sensorAnomalyPowerRatio: 0.12,
- sensorAnomalyWindRatio: 0,
- },
- { engineName: "16", sensorAnomalyPowerRatio: 0, sensorAnomalyWindRatio: 0.2 },
- ]);
- assert(
- sensorHeat.xLabels.join(",") === "09号,16号",
- "数据感知热力图风机名应带号",
- );
- assert(sensorHeat.yLabels[0] === "功率异常", "数据感知热力图首行应为功率异常");
- assert(
- sensorHeat.yLabels.includes("偏航角、扭缆角、偏航误差等偏航"),
- "数据感知热力图应含偏航角、扭缆角、偏航误差等偏航",
- );
- assert(sensorHeat.yLabels.length === 8, "数据感知热力图应为8行");
- assert(sensorHeat.matrix[0][0] === 12, "数据感知热力图应按占比×100");
- assert(chunkHeatmapData(sensorHeat, 1).length === 2, "热力图应按风机分片");
- const detectorHeat = buildDetectorHeatmapData([
- {
- engineName: "01",
- model2YawYawerrorRatio: 0.9247,
- model4CtrlparamDeloadRatio: 0,
- },
- ]);
- assert(
- detectorHeat.yLabels.includes("静态偏航误差"),
- "功能诊断热力图应含静态偏航误差",
- );
- assert(
- detectorHeat.matrix[detectorHeat.yLabels.indexOf("静态偏航误差")][0] ===
- 92.47,
- "功能诊断热力图占比换算错误",
- );
- const heatOption = buildReportHeatmapOption(
- sensorHeat,
- "各机组数据感知异常分布",
- );
- assert(heatOption.series[0].type === "heatmap", "图3-1应为热力图");
- assert(
- heatOption.series[0].data[0]?.name?.includes("%"),
- "报告热力图单元格应预置占比文本",
- );
- assert(optionHasData(heatOption), "数据感知热力图应视为有数据");
- const sensorRadar = buildSensorRadarData(
- { sensorAnomalyPowerCount: 2, sensorAnomalyWindCount: 1 },
- 20,
- );
- assert(sensorRadar.values[0] === 2, "数据感知雷达图应映射异常台数");
- const detectorRadar = buildDetectorRadarData(
- { model1Count: 3, model2Count: 1 },
- 20,
- );
- const sensorRadarOption = buildAnomalyRadarOption(
- sensorRadar,
- "数据感知异常台数",
- );
- assert(sensorRadarOption.series[0].type === "radar", "概览区应输出雷达图");
- assert(
- Number(sensorRadarOption.series[0].lineStyle?.width) >= 2,
- "雷达图 series 应配置连线宽度",
- );
- assert(
- sensorRadarOption.radar.indicator[0].name.includes("2台"),
- "雷达图维度应标注台数",
- );
- console.log("axis/watchlist/placeholder rules ok");
- await initChartService();
- const imageBufferMap = {};
- const sensorRadarBuf = await renderEchartsOption(sensorRadarOption, {
- width: 520,
- height: 360,
- });
- registerImageBuffer(imageBufferMap, "anomaly_sensor_radar", sensorRadarBuf);
- const detectorRadarBuf = await renderEchartsOption(
- buildAnomalyRadarOption(detectorRadar, "功能诊断异常台数"),
- { width: 520, height: 360 },
- );
- registerImageBuffer(imageBufferMap, "anomaly_detector_radar", detectorRadarBuf);
- const sensorHeatBuf = await renderEchartsOption(heatOption, {
- width: 920,
- height: 420,
- });
- registerImageBuffer(imageBufferMap, "anomaly_sensor_dist", sensorHeatBuf);
- const detectorHeatBuf = await renderEchartsOption(
- buildReportHeatmapOption(detectorHeat, "各机组功能诊断异常分布"),
- { width: 920, height: 560 },
- );
- registerImageBuffer(imageBufferMap, "anomaly_detector_dist", detectorHeatBuf);
- const farmPlots = buildFarmPlotJsons(
- [
- { engineName: "111", plotJson: samplePlot("111") },
- { engineName: "112", plotJson: samplePlot("112") },
- ],
- "yaw_static",
- "全场静态偏航分析检测汇总",
- );
- const farmLabels = (farmPlots[0]?.data || []).map((row) => row.label);
- if (farmLabels.join(",") !== "111,112") {
- throw new Error(`全场偏航分色失败: ${farmLabels.join(",")}`);
- }
- if (
- (farmPlots[0]?.data || []).some((row) => /均值/.test(row.originalLabel || ""))
- ) {
- throw new Error("全场偏航仍包含 2h/12h 均值");
- }
- const farmBuf = await renderEchartsOption(
- buildDetectorPlotOption(
- farmPlots[0],
- farmPlots[0].title,
- "anomalyStaticyawPO",
- ),
- { width: 820, height: 420 },
- );
- registerImageBuffer(imageBufferMap, "anomaly_farm_yaw_static", farmBuf);
- const turbineBuf = await renderEchartsOption(
- buildDetectorPlotOption(samplePlot("111"), "111 静态偏航分析"),
- { width: 760, height: 400 },
- );
- registerImageBuffer(imageBufferMap, "anomaly_111_yaw_static", turbineBuf);
- const denseCount = 4500;
- const denseTimes = Array.from({ length: denseCount }, (_, i) => {
- const t = new Date("2026-08-01T00:00:00").getTime() + i * 60000;
- const pad = (n) => String(n).padStart(2, "0");
- const d = new Date(t);
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(
- d.getHours(),
- )}:${pad(d.getMinutes())}:00`;
- });
- const denseOption = buildDetectorPlotOption(
- {
- title: "DT01 电能质量分析",
- xaxis: "时间",
- yaxis: "电流不平衡度",
- data: [
- {
- label: "电流不平衡度",
- mode: "markers",
- timeData: denseTimes,
- yData: Array.from(
- { length: denseCount },
- (_, i) => 0.08 + (i % 20) / 200,
- ),
- },
- ],
- },
- "DT01 电能质量分析",
- "anomalyPowerqualityPO",
- );
- assert(denseOption.series[0].large === false, "密点单机图不应使用 large");
- assert(
- Number(denseOption.series[0].symbolSize) >= 8,
- "密点单机图 symbolSize 过小",
- );
- const denseBuf = await renderEchartsOption(denseOption, {
- width: 760,
- height: 420,
- });
- assert(denseBuf.length > 10000, "密点单机图截图过小,可能未画出数据");
- const renderData = {
- reportNo: "AD-SMOKE-20260824",
- Province: "测试省",
- Wind_farm: "烟雾风场",
- Year_now: "2026",
- Month_now: "08",
- machineTypeCode: "WD",
- turbine_count: "2",
- anomaly_turbine_count: "1",
- total_anomaly_points: "3",
- anomaly_rate: "50.00",
- Overview_of_the_Wind_Farm: "烟雾风场用于模板渲染校验。",
- target_date: "2026-08-24",
- anomaly_module_count: "1",
- main_problem_modules: "偏航与扭缆",
- top_problem_description: "静态偏航分析异常点数最多(3)",
- sensorAnomalyRows: [
- {
- turbine_name: "111",
- sensor_anomaly_type: "风速异常",
- anomaly_points: "1",
- ratio: "—",
- },
- ],
- detectorSummaryRows: [
- {
- detector_name: "静态偏航分析",
- module_name: "偏航与扭缆",
- data_granularity: "秒级",
- anomaly_turbines: "1",
- anomaly_points: "3",
- avg_anomaly_rate: "12.00%",
- },
- ],
- anomalySummaryRows: [
- {
- turbine_name: "111",
- anomaly_detector_count: "1",
- anomaly_points: "3",
- anomaly_rate: "12.00%",
- main_anomaly_type: "静态偏航分析",
- },
- ],
- priorityList: "111",
- keyTurbineLoop: [
- {
- turbine_name: "111",
- turbineDetailRows: [
- {
- detector_name: "静态偏航分析",
- anomaly_points: "3",
- anomaly_rate: "12.00%",
- conclusion: "建议结合现场复核",
- },
- ],
- "zn-techcn-replace-tags-key_turbine-generalFiles": [
- { image: "anomaly_111_yaw_static" },
- ],
- },
- ],
- conclusionRows: [
- {
- index: "1",
- problem_type: "静态偏航分析",
- turbine_names: "111",
- anomaly_points: "3",
- risk_level: "P2",
- suggestion: "核对偏航角越限情况。",
- },
- ],
- "zn-techcn-replace-tags-anomaly_overview-generalFiles": [
- { image: "anomaly_sensor_radar" },
- { image: "anomaly_detector_radar" },
- ],
- "zn-techcn-replace-tags-data_sensor_anomaly-generalFiles": [
- { image: "anomaly_sensor_dist" },
- { image: "anomaly_detector_dist" },
- ],
- "zn-techcn-replace-tags-key_turbine-generalFiles": [],
- };
- DETECTOR_TEMPLATE_CONFIG.forEach((cfg) => {
- if (cfg.templateKey === "yaw_static") {
- Object.assign(
- renderData,
- buildDetectorSectionPayload(cfg, {
- farmImages: [{ image: "anomaly_farm_yaw_static" }],
- turbineImages: [{ image: "anomaly_111_yaw_static" }],
- rows: [
- {
- turbine_name: "111",
- anomaly_points: "3",
- anomaly_rate: "12.00%",
- comment: "功能诊断异常",
- },
- ],
- anomalyPoints: 3,
- anomalyRate: 0.12,
- anomalyTurbines: 1,
- }),
- );
- return;
- }
- const farmTag = `zn-techcn-replace-tags-${cfg.templateKey}-farmSummary`;
- const fileTag = `zn-techcn-replace-tags-${cfg.templateKey}-generalFiles`;
- renderData[`show-${fileTag}`] = [];
- renderData[farmTag] = [];
- renderData[fileTag] = [];
- renderData[`${cfg.templateKey}Rows`] = [];
- });
- const buffer = await renderDocxReport({
- templateName: "异常检测数据分析报告模板(大唐版).docx",
- renderData,
- imageBufferMap,
- });
- const outDir = path.join(__dirname, "../templates");
- fs.mkdirSync(outDir, { recursive: true });
- const outPath = path.join(outDir, "_smoke_anomaly_report.docx");
- fs.writeFileSync(outPath, buffer);
- console.log("smoke anomaly report written:", outPath, "bytes=", buffer.length);
- await shutdownChartService();
|