| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184 |
- import fs from "fs";
- import path from "path";
- import { fileURLToPath } from "url";
- import PizZip from "pizzip";
- import {
- registerImageBuffer,
- renderDocxReport,
- patchAnomalyTemplateXml,
- chunkDetailImageRows,
- } from "../src/server/reportService/docxReportBuilder.js";
- import {
- buildDetectorSectionPayload,
- pickKeyTurbines,
- buildSensorHeatmapData,
- buildDataCompletenessHeatmapData,
- buildDetectorHeatmapData,
- buildSensorRadarData,
- buildDetectorRadarData,
- chunkHeatmapData,
- buildSensorAnomalyRows,
- mapAnomalyCoverFields,
- summarizeTurbineAnomalyRate,
- buildAnomalySummaryRows,
- buildConclusionRows,
- formatSensorTypeText,
- } from "../src/server/reportService/anomalyReportMapper.js";
- import {
- DETECTOR_TEMPLATE_CONFIG,
- buildAnomalyRadarOption,
- buildDetectorPlotOption,
- buildEmptyPlaceholderOption,
- buildFarmPlotJsons,
- buildCompletenessHeatmapOption,
- buildReportHeatmapOption,
- listDetectorPlotOptions,
- listFarmPlotOptionsFromTurbines,
- listFarmChartOptions,
- optionHasData,
- plotJsonHasData,
- 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,
- diagnosticFarmStyle,
- diagnosticSeriesColor,
- diagnosticSeriesSymbol,
- diagnosticSingleSeriesZ,
- DIAGNOSTIC_COLORS,
- PITCH_MIN_REPORT_COUNT_COLORS,
- PITCH_MIN_REPORT_COLOR_STOPS,
- PAGE_SHUTDOWN_COLORS,
- } from "../shared/anomalyDiagnosticVisual.mjs";
- import {
- farmAggregateToPanels,
- formatReportValueAxisLabel,
- resolvePanelSubtitle,
- } 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);
- }
- /** 图例项可能是字符串或 { name, itemStyle } 对象,统一取名称 */
- function legendNames(option) {
- const data = option?.legend?.data || [];
- return data.map((item) =>
- item && typeof item === "object" ? item.name : item,
- );
- }
- const headerCover = mapAnomalyCoverFields({
- fieldCode: "SVLgVZfi",
- fieldName: "新疆鄂能风电场",
- fieldMeta: { provinceName: "大唐新疆公司" },
- modelList: [],
- });
- assert(
- headerCover.Province === "新疆",
- `页眉 Province 应去重为「新疆」,实际为「${headerCover.Province}」`,
- );
- assert(
- headerCover.Wind_farm === "鄂能风电场",
- `页眉 Wind_farm 应去掉省份前缀并保留风电场,实际为「${headerCover.Wind_farm}」`,
- );
- assert(
- headerCover.Overview_of_the_Wind_Farm ===
- "新疆鄂能风电场位于大唐新疆公司,机型—,共安装0台风机",
- `项目概要不应重复公司名或多余句号,实际为「${headerCover.Overview_of_the_Wind_Farm}」`,
- );
- const duplicateLocationCover = mapAnomalyCoverFields({
- fieldCode: "SVLgVZfi",
- fieldName: "新疆鄂能风电场",
- fieldMeta: {
- provinceName: "大唐新疆公司",
- cityName: "大唐新疆公司",
- },
- modelList: [{ machineTypeCode: "GW82-1500" }, { machineTypeCode: "GW82-1500" }],
- });
- assert(
- duplicateLocationCover.Overview_of_the_Wind_Farm ===
- "新疆鄂能风电场位于大唐新疆公司,机型GW82-1500,共安装2台风机",
- `省/市同名时应去重,实际为「${duplicateLocationCover.Overview_of_the_Wind_Farm}」`,
- );
- assert(
- !duplicateLocationCover.Overview_of_the_Wind_Farm.includes("。"),
- "项目概要正文不应自带句号(模板已有)",
- );
- const thirteenCover = mapAnomalyCoverFields({
- fieldCode: "FIELD13",
- fieldName: "新疆十三师风电场",
- fieldMeta: { provinceName: "大唐新疆能源有限公司" },
- overview: { fieldName: "鄂能风电场" },
- modelList: [],
- });
- assert(
- thirteenCover.Province === "新疆" &&
- thirteenCover.Wind_farm === "十三师风电场" &&
- thirteenCover.Company === "大唐新疆能源有限公司",
- `选中风场应优先用于页眉,实际 Province=${thirteenCover.Province} Wind_farm=${thirteenCover.Wind_farm}`,
- );
- assert(
- !`${headerCover.Province}${headerCover.Wind_farm}`.includes("大唐大唐"),
- "页眉不应出现「大唐大唐」重复前缀",
- );
- const conclusionSample = buildConclusionRows({
- yaw_error: {
- anomalyTurbines: 3,
- anomalyPoints: 12,
- turbineNames: ["25", "4", "10", "4号"],
- },
- yaw_static: {
- anomalyTurbines: 0,
- anomalyPoints: 0,
- turbineNames: [],
- },
- });
- assert(conclusionSample.length === 1, "无异常检测器不应进入检测结论表");
- assert(
- conclusionSample[0].problem_desc.startsWith("主要表现为") &&
- conclusionSample[0].problem_desc.includes("静态偏差"),
- `问题描述应从「主要表现为」截取,实际为「${conclusionSample[0].problem_desc}」`,
- );
- assert(
- conclusionSample[0].suggestion.startsWith("建议"),
- `建议意见应使用排查建议,实际为「${conclusionSample[0].suggestion}」`,
- );
- assert(
- conclusionSample[0].problem_nature === "偏航与扭缆系统异常",
- `问题性质应为模块大标题,实际为「${conclusionSample[0].problem_nature}」`,
- );
- assert(
- conclusionSample[0].turbine_names === "#04、#10、#25",
- `问题机组应统一罗列,实际为「${conclusionSample[0].turbine_names}」`,
- );
- const conclusionWithoutDeload = buildConclusionRows({
- ctrl_deload: {
- anomalyTurbines: 20,
- anomalyPoints: 80,
- turbineNames: ["01", "02", "03"],
- },
- yaw_error: {
- anomalyTurbines: 1,
- anomalyPoints: 2,
- turbineNames: ["05"],
- },
- });
- assert(
- conclusionWithoutDeload.length === 1 &&
- !conclusionWithoutDeload.some((row) =>
- `${row.problem_desc}${row.suggestion}`.includes("降载"),
- ),
- "表10-1 检测结论不应统计降载判定",
- );
- assert(
- !plotJsonHasData({ data: [] }) && !plotJsonHasData(null),
- "空 JSON 应判定为无图表数据",
- );
- assert(
- plotJsonHasData({
- data: [{ label: "散点", xData: [1], yData: [2] }],
- }),
- "含坐标点的 JSON 应判定为有数据",
- );
- const templateSource = path.join(
- __dirname,
- "../src/public/file/异常检测数据分析报告模板(大唐版)_修订版_人工对齐版_业务化.docx",
- );
- if (fs.existsSync(templateSource)) {
- const sourceXml = new PizZip(fs.readFileSync(templateSource))
- .file("word/document.xml")
- .asText();
- const patchedXml = patchAnomalyTemplateXml(sourceXml);
- const tocParas = patchedXml.match(/<w:p[\s\S]*?<\/w:p>/g) || [];
- let tocHas42 = false;
- let tocHasFigure = false;
- let tocHasModuleTag = false;
- tocParas.slice(0, 70).forEach((paragraph) => {
- const plain = paragraph.replace(/<[^>]+>/g, "").trim();
- const isToc = /w:pStyle w:val="(?:26|29|30|31)"/.test(paragraph);
- if (!isToc) return;
- if (/^4\.2 按功能诊断项统计/.test(plain)) tocHas42 = true;
- if (/^图[34]-[12]/.test(plain)) tocHasFigure = true;
- if (/show_module_/.test(plain)) tocHasModuleTag = true;
- });
- assert(tocHas42, "目录应保留 4.2 按功能诊断项统计 条目");
- assert(!tocHasFigure, "目录中不应出现图3/图4 图题");
- assert(!tocHasModuleTag, "目录中不应插入模块条件标签");
- const detectorLoopOpen =
- patchedXml.indexOf(
- "{#zn-techcn-replace-tags-data_detector_anomaly-generalFiles}",
- );
- const detectorCaption = patchedXml.indexOf(
- "{figure_caption}",
- detectorLoopOpen,
- );
- const detectorImage = patchedXml.indexOf("{%image}", detectorCaption);
- const detectorLoopClose = patchedXml.indexOf(
- "{/zn-techcn-replace-tags-data_detector_anomaly-generalFiles}",
- detectorImage,
- );
- assert(
- detectorLoopOpen >= 0 &&
- detectorLoopOpen < detectorCaption &&
- detectorCaption < detectorImage &&
- detectorImage < detectorLoopClose,
- "第4章概览应按图题、图片逐项循环",
- );
- assert(
- patchAnomalyTemplateXml(patchedXml) === patchedXml,
- "异常模板补丁应可重复执行",
- );
- const detailGrid = patchedXml.indexOf(
- "{#zn-techcn-replace-tags-wind_power_curve-generalFiles}",
- );
- const detailImage = patchedXml.indexOf("{%detail_image}", detailGrid);
- const detailClose = patchedXml.indexOf(
- "{/zn-techcn-replace-tags-wind_power_curve-generalFiles}",
- detailImage,
- );
- assert(
- detailGrid >= 0 && detailGrid < detailImage && detailImage < detailClose,
- "分图应排成一行三列",
- );
- assert(
- patchedXml.includes("{#detail_slot1}") &&
- patchedXml.includes("{#detail_slot2}") &&
- patchedXml.includes("{#detail_slot3}"),
- "分图应预留三列",
- );
- assert(
- patchedXml.includes(
- "{#zn-techcn-replace-tags-wind_power_curve-generalFiles}{#detail_slot1}",
- ) &&
- patchedXml.includes(
- "{/detail_slot3}{/zn-techcn-replace-tags-wind_power_curve-generalFiles}",
- ),
- "分图循环标签应与槽位标签同段,避免第一列下移",
- );
- const farmImage = patchedXml.indexOf(
- "{#zn-techcn-replace-tags-wind_power_curve-farmSummary}",
- );
- assert(
- farmImage >= 0 &&
- patchedXml.indexOf("{%image}", farmImage) > farmImage &&
- patchedXml.indexOf("{%image}", farmImage) <
- patchedXml.indexOf(
- "{/zn-techcn-replace-tags-wind_power_curve-farmSummary}",
- farmImage,
- ),
- "总图应保持一行一张",
- );
- const chunked = chunkDetailImageRows([
- { image: "a" },
- { image: "b" },
- { image: "c" },
- { image: "d" },
- ]);
- assert(
- chunked.length === 2 &&
- chunked[0].detail_slot3[0].detail_image === "c" &&
- chunked[1].detail_slot1[0].detail_image === "d" &&
- chunked[1].detail_slot2.length === 0,
- "分图应按 3 张一组,末行空位留白",
- );
- assert(
- patchedXml.includes("{#conclusionRows}") &&
- patchedXml.includes("{problem_desc}") &&
- patchedXml.includes("{problem_nature}") &&
- patchedXml.includes("{/detectorSummaryRows}"),
- "表10-1 检测结论应改为 conclusionRows 循环,表4-1 应闭合 detectorSummaryRows",
- );
- assert(
- (patchedXml.match(/\{sensor_anomaly_type\}/g) || []).length >= 16,
- "各检测器明细表应保留数据感知异常类型列",
- );
- assert(
- patchedXml.includes("{#show_module_wind}") &&
- patchedXml.includes("{/show_module_aero}"),
- "各检测模块章节应包一层 show_module 条件标签",
- );
- const patchedPlain = patchedXml.replace(/<[^>]+>/g, "");
- const patchedLoops = [
- ...new Set([...patchedPlain.matchAll(/\{#([^{}]+)\}/g)].map((m) => m[1])),
- ];
- const danglingLoops = patchedLoops.filter(
- (name) => !patchedPlain.includes(`{/${name}}`),
- );
- assert(
- danglingLoops.length === 0,
- `模板循环应全部闭合,未闭合:${danglingLoops.join("、")}`,
- );
- assert(
- patchedXml.includes("{/wind_power_curveRows}") &&
- patchedXml.includes("{/aero_tsr_windRows}"),
- "检测明细表循环应补齐结尾标签",
- );
- const patchedParas = patchedXml.match(/<w:p[\s\S]*?<\/w:p>/g) || [];
- patchedParas.slice(0, 70).forEach((paragraph) => {
- const plain = paragraph.replace(/<[^>]+>/g, "").trim();
- const isToc = /w:pStyle w:val="(?:26|29|30|31)"/.test(paragraph);
- if (isToc && /show_module_/.test(plain)) {
- throw new Error("目录 TOC 段落中仍含有 show_module 标签");
- }
- });
- }
- 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) => {
- const size = Number(item.symbolSize);
- return size >= 4 && size <= 6;
- }),
- "全场散点报告正常点应收小,避免糊成色块",
- );
- const emphasisScatter = buildDetectorPlotOption(
- {
- detector: "wind_power_scatter",
- chartType: "series",
- xaxis: "风速 (m/s)",
- yaxis: "有功功率 (kW)",
- turbines: Array.from({ length: 12 }, (_, index) => ({
- turbine: String(index + 1).padStart(2, "0"),
- xType: "number",
- xData: [4, 8, 12],
- yData: [200, 800, 1400],
- pointAnomaly: [false, true, false],
- })),
- },
- "全场风功率散点分析",
- "anomalyScatterPO",
- );
- const emphasisNormal = (emphasisScatter.series || []).find(
- (item) => item.type === "scatter3D" && /正常/.test(item.name),
- );
- const emphasisAnomaly = (emphasisScatter.series || []).find(
- (item) =>
- item.type === "scatter3D" &&
- /异常/.test(item.name) &&
- !/正常/.test(item.name),
- );
- assert(emphasisNormal && emphasisAnomaly, "全场散点应按点拆成正常/异常");
- assert(
- Number(emphasisAnomaly.symbolSize) - Number(emphasisNormal.symbolSize) === 2,
- "报告全场散点异常点只比正常点大一圈",
- );
- assert(
- Number(emphasisAnomaly.itemStyle?.opacity) >
- Number(emphasisNormal.itemStyle?.opacity),
- "报告全场散点异常点应比正常点更不透明",
- );
- assert(
- emphasisAnomaly.itemStyle?.color === "#FF1E1E",
- "报告全场散点异常点应为亮红",
- );
- assert(
- !emphasisAnomaly.itemStyle?.borderWidth,
- "报告全场散点异常点不描白边",
- );
- assert(farmScatterOption.grid3D, "风功率散点应输出 3D 图表");
- const singleScatterOption = buildDetectorPlotOption(
- {
- title: "07 风功率散点分析",
- xaxis: "风速 (m/s)",
- yaxis: "有功功率 (kW)",
- data: [
- {
- label: "正常运行 (271)",
- mode: "markers",
- xData: [6, 8, 10],
- yData: [400, 900, 1500],
- },
- {
- label: "近90日功率曲线",
- mode: "markers",
- xData: [3, 6, 9, 12, 15],
- yData: [0, 400, 1200, 1800, 1850],
- },
- ],
- },
- "07 风功率散点分析",
- "anomalyScatterPO",
- );
- const curveSeries = (singleScatterOption.series || []).find(
- (item) => item.name === "近90日功率曲线",
- );
- assert(curveSeries?.type === "line", "近90日功率曲线应渲染为折线而非散点");
- assert(curveSeries?.showSymbol === false, "近90日功率曲线折线不应显示散点符号");
- assert(
- diagnosticSeriesColor({ label: "正常运行 (271)" }) === DIAGNOSTIC_COLORS.normal &&
- diagnosticSeriesColor({ label: "异常点" }) === DIAGNOSTIC_COLORS.anomaly &&
- diagnosticSeriesColor({ label: "降载" }) === DIAGNOSTIC_COLORS.deload &&
- diagnosticSeriesColor({ label: "停机" }) === DIAGNOSTIC_COLORS.shutdown,
- "单机风功率散点应按正常蓝/异常红/降载黄/停机黑上色",
- );
- assert(
- diagnosticSeriesColor({ label: "累计降载率" }) !== DIAGNOSTIC_COLORS.deload,
- "累计降载率曲线不应被当成散点降载状态色",
- );
- // 页面深色底可换成浅灰;报告与浅色页必须保持 shutdown 深色
- assert(
- diagnosticSeriesColor({ label: "停机" }, 0, {
- pageTheme: true,
- darkTheme: true,
- }) === PAGE_SHUTDOWN_COLORS.dark &&
- diagnosticSeriesColor({ label: "停机" }, 0, {
- pageTheme: true,
- darkTheme: false,
- }) === DIAGNOSTIC_COLORS.shutdown &&
- diagnosticSeriesColor({ label: "停机" }) === DIAGNOSTIC_COLORS.shutdown,
- "停机点色仅在页面深色主题切换,报告保持 shutdown 深色",
- );
- assert(
- diagnosticSeriesColor({ label: "正常运行 (271)" }, 0, {
- pageTheme: true,
- darkTheme: true,
- }) === DIAGNOSTIC_COLORS.normal,
- "页面主题选项不应影响停机以外的状态色",
- );
- const singleCurveWithContract = buildDetectorPlotOption(
- {
- title: "07 功率曲线分析",
- xaxis: "风速 (m/s)",
- yaxis: "有功功率 (kW)",
- data: [
- {
- label: "近90天功率曲线",
- mode: "lines",
- xData: [3, 6, 9, 12],
- yData: [0, 400, 1200, 1500],
- },
- {
- label: "合同功率曲线",
- mode: "lines",
- xData: [3, 6, 9, 12],
- yData: [0, 500, 1300, 1500],
- },
- ],
- },
- "07 功率曲线分析",
- "anomalyPowercurvePO",
- );
- const singleContractSeries = (singleCurveWithContract.series || []).find(
- (item) => item.name === "合同功率曲线",
- );
- const singlePowerCurveSeries = (singleCurveWithContract.series || []).find(
- (item) => /近90/.test(item.name || ""),
- );
- assert(singleContractSeries, "单机功率曲线应含合同功率曲线");
- assert(
- Number(singleContractSeries.z) < Number(singlePowerCurveSeries?.z || 2),
- "单机合同功率曲线层级应低于近90天功率曲线",
- );
- assert(
- diagnosticSingleSeriesZ({ label: "合同功率曲线" }) <
- diagnosticSingleSeriesZ({ label: "正常运行" }),
- "单机合同功率曲线 z 应低于散点",
- );
- const singleStatusScatter = buildDetectorPlotOption(
- {
- title: "07 风功率散点分析",
- xaxis: "风速 (m/s)",
- yaxis: "有功功率 (kW)",
- data: [
- { label: "正常", mode: "markers", xData: [6], yData: [400] },
- { label: "异常", mode: "markers", xData: [8], yData: [500] },
- { label: "降载", mode: "markers", xData: [7], yData: [300] },
- { label: "停机", mode: "markers", xData: [5], yData: [0] },
- {
- label: "合同功率曲线",
- mode: "lines",
- xData: [3, 12],
- yData: [0, 1500],
- },
- ],
- },
- "07 风功率散点分析",
- "anomalyScatterPO",
- );
- const colorByName = Object.fromEntries(
- (singleStatusScatter.series || []).map((item) => [
- item.name,
- item.itemStyle?.color || item.lineStyle?.color,
- ]),
- );
- assert(
- String(colorByName["正常"] || "").startsWith(DIAGNOSTIC_COLORS.normal),
- "正常应为蓝色",
- );
- assert(
- String(colorByName["异常"] || "").startsWith("#FF1E1E"),
- "报告散点异常点应为亮红",
- );
- assert(
- String(colorByName["降载"] || "").startsWith(DIAGNOSTIC_COLORS.deload),
- "降载应为黄色",
- );
- assert(
- String(colorByName["停机"] || "").startsWith(DIAGNOSTIC_COLORS.shutdown),
- "停机应为黑色",
- );
- assert(
- Number(
- (singleStatusScatter.series || []).find((item) => item.name === "合同功率曲线")
- ?.z,
- ) <
- Number(
- (singleStatusScatter.series || []).find((item) => item.name === "正常")?.z,
- ),
- "单机散点图合同功率曲线应在散点下方",
- );
- const scatterFarmWith90d = buildFarmPlotJsons(
- [
- {
- engineName: "A",
- plotJson: {
- xaxis: "风速",
- yaxis: "功率",
- data: [
- { label: "散点", mode: "markers", xData: [1, 2], yData: [3, 4] },
- {
- label: "近90日功率曲线",
- mode: "markers",
- xData: [1, 2, 3],
- yData: [5, 6, 7],
- },
- ],
- },
- },
- {
- engineName: "B",
- plotJson: {
- xaxis: "风速",
- yaxis: "功率",
- data: [
- { label: "散点", mode: "markers", xData: [2, 3], yData: [4, 5] },
- {
- label: "近90日功率曲线",
- mode: "markers",
- xData: [1, 2, 3],
- yData: [5, 6, 7],
- },
- ],
- },
- },
- ],
- "wind_power_scatter",
- "全场散点",
- );
- assert(
- scatterFarmWith90d[0].data.filter((row) => row.__shared).length === 1,
- "近90日功率曲线全场应只保留一条",
- );
- assert(
- scatterFarmWith90d[0].data.some((row) => row.label === "近90日功率曲线"),
- "近90日功率曲线应出现在全场散点图例",
- );
- 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(
- { extra: { anomaly: false }, originalLabel: "正常", turbineIndex: 1 },
- 0,
- "ctrl_deload",
- ) === DIAGNOSTIC_COLORS.normal &&
- diagnosticFarmSeriesColor(
- { extra: { anomaly: false }, originalLabel: "异常", turbineIndex: 1 },
- 1,
- "ctrl_deload",
- ) === DIAGNOSTIC_COLORS.normal,
- "正常降载机组整机应统一蓝色",
- );
- assert(
- diagnosticFarmSeriesColor(
- { originalLabel: "正常", turbineIndex: 0 },
- 0,
- "pitch_coord",
- ) === DIAGNOSTIC_COLORS.normal,
- "变桨协调总图正常点应统一蓝色",
- );
- assert(
- diagnosticFarmSeriesColor(
- { originalLabel: "异常", turbineIndex: 0 },
- 0,
- "pitch_coord",
- ) === DIAGNOSTIC_COLORS.anomaly,
- "变桨协调总图异常点应统一红色",
- );
- assert(
- diagnosticFarmSeriesColor(
- { label: "07号 正常", originalLabel: "正常", turbineIndex: 0 },
- 0,
- "wind_power_scatter",
- ) === DIAGNOSTIC_COLORS.normal &&
- diagnosticFarmSeriesColor(
- { label: "07号 异常", originalLabel: "异常", turbineIndex: 0 },
- 0,
- "wind_power_scatter",
- ) === DIAGNOSTIC_COLORS.anomaly &&
- diagnosticFarmSeriesColor(
- { label: "08号 正常", originalLabel: "正常", turbineIndex: 1 },
- 1,
- "wind_power_curve",
- ) === DIAGNOSTIC_COLORS.normal &&
- diagnosticFarmSeriesColor(
- { label: "08号 异常", originalLabel: "异常", turbineIndex: 1 },
- 1,
- "wind_power_curve",
- ) === DIAGNOSTIC_COLORS.anomaly,
- "风功率曲线/散点 3D 总图应按正常蓝、异常红上色",
- );
- assert(
- diagnosticFarmSeriesColor(
- { extra: { anomaly: true }, originalLabel: "正常", turbineIndex: 3 },
- 0,
- "ctrl_deload",
- ) === DIAGNOSTIC_COLORS.anomaly &&
- diagnosticFarmSeriesColor(
- { extra: { anomaly: true }, originalLabel: "异常", turbineIndex: 3 },
- 1,
- "ctrl_deload",
- ) === DIAGNOSTIC_COLORS.anomaly,
- "异常降载机组整机应统一红色",
- );
- assert(
- diagnosticFarmSeriesColor(
- { originalLabel: "降载", engineName: "08号", turbineIndex: 0 },
- 0,
- "ctrl_deload",
- ) === DIAGNOSTIC_COLORS.anomaly &&
- diagnosticFarmSeriesColor(
- { originalLabel: "停机", engineName: "08号", turbineIndex: 0 },
- 0,
- "ctrl_deload",
- ) === DIAGNOSTIC_COLORS.anomaly,
- "全场降载总图残留降载/停机系列名应标红而非黄/黑",
- );
- assert(
- diagnosticFarmSeriesColor(
- { extra: { anomaly: true }, originalLabel: "桨叶 1", turbineIndex: 0 },
- 0,
- "pitch_regulation",
- ) === DIAGNOSTIC_COLORS.anomaly &&
- diagnosticFarmSeriesColor(
- { extra: { anomaly: false }, originalLabel: "桨叶 2", turbineIndex: 1 },
- 1,
- "pitch_regulation",
- ) === DIAGNOSTIC_COLORS.normal,
- "变桨一致性总图应按机组状态统一红/蓝",
- );
- assert(
- diagnosticFarmStyle(
- { extra: { anomaly: true }, originalLabel: "桨叶 1" },
- "pitch_regulation",
- ).z >
- diagnosticFarmStyle(
- { extra: { anomaly: false }, originalLabel: "桨叶 2" },
- "pitch_regulation",
- ).z,
- "变桨一致性异常机组应叠在正常机组之上",
- );
- assert(
- diagnosticFarmSeriesColor(
- { anomaly: true, originalLabel: "异常", turbineIndex: 0 },
- 0,
- "aero_cp",
- ) === DIAGNOSTIC_COLORS.anomaly &&
- diagnosticFarmSeriesColor(
- { anomaly: false, originalLabel: "正常", turbineIndex: 1 },
- 1,
- "aero_tsr",
- ) === DIAGNOSTIC_COLORS.normal &&
- diagnosticFarmSeriesColor(
- {
- anomaly: false,
- extra: { anomaly: true },
- originalLabel: "正常",
- turbineIndex: 0,
- },
- 0,
- "aero_cp",
- ) === DIAGNOSTIC_COLORS.normal,
- "气动总图应按 pointAnomaly 拆分后的点状态上色(行级优先于整机)",
- );
- assert(
- diagnosticSeriesSymbol({ originalLabel: "正常" }) === "circle" &&
- diagnosticSeriesSymbol({ originalLabel: "异常" }) === "circle" &&
- diagnosticSeriesSymbol({ originalLabel: "降载" }) === "circle",
- "降载总图正常/异常/降载点形应统一为圆形,靠颜色区分",
- );
- 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(
- curveOption.xAxis3D?._turbineLabelMeta?.labels &&
- typeof curveOption.xAxis3D._turbineLabelMeta.labels === "object",
- "功率曲线风机轴应带可序列化抽样标签,供报告渲染重建 formatter",
- );
- 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 farmWithContractJson = {
- detector: "wind_power_scatter",
- chartType: "series",
- xaxis: "风速 (m/s)",
- yaxis: "有功功率 (kW)",
- turbines: [
- {
- turbine: "03",
- anomaly: true,
- xType: "number",
- xData: [3.25, 8.25],
- yData: [56.45, 243.89],
- },
- {
- turbine: "06",
- anomaly: false,
- xType: "number",
- xData: [3.25, 8.25],
- yData: [61.4, 238.51],
- },
- ],
- referenceCurve: {
- label: "合同功率曲线",
- xData: [0, 12, 25],
- yData: [0, 595, 850],
- mode: "lines",
- },
- };
- const farmWithContract = farmAggregateToPanels(farmWithContractJson);
- assert(
- farmWithContract[0].data
- .filter((row) => !row.__shared)
- .every((row) => row.mode === "markers"),
- "风功率散点总图风机数据应为散点",
- );
- assert(
- farmWithContract[0].data.some(
- (row) => row.__shared && row.label === "合同功率曲线" && row.mode === "lines",
- ),
- "总图应绘制 referenceCurve 合同功率曲线",
- );
- const contractFarmOption = buildDetectorPlotOption(
- farmWithContractJson,
- "全场风功率散点分析",
- "anomalyScatterPO",
- );
- const contractSeries = (contractFarmOption.series || []).find(
- (item) => item.name === "合同功率曲线",
- );
- const turbineScatter = (contractFarmOption.series || []).find(
- (item) =>
- item.name !== "合同功率曲线" &&
- (item.type === "scatter3D" || item.type === "scatter"),
- );
- assert(contractSeries, "合同功率曲线应出现在总图中");
- assert(
- contractSeries.type === "line3D" || contractSeries.type === "line",
- "合同功率曲线应为折线",
- );
- assert(turbineScatter, "风功率散点总图风机数据应为散点");
- assert(
- /#7C3AED|rgb\(124,\s*58,\s*237\)/i.test(
- String(contractSeries.lineStyle?.color || ""),
- ),
- `合同功率曲线应与异常红区分,实际为 ${contractSeries.lineStyle?.color}`,
- );
- assert(
- Number(contractSeries.lineStyle?.width) >= 3,
- "合同功率曲线应比风机散点更醒目",
- );
- assert(contractSeries.lineStyle?.type === "solid", "合同功率曲线应为实线");
- 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) >= 3 &&
- Number(pitchCoordOption.series[0].symbolSize) <= 5,
- "全场变桨协调报告散点应更小",
- );
- const rangedOpFarm = listFarmChartOptions(
- [
- {
- engineName: "01",
- plotJson: {
- panels: [
- {
- panelTitle: "转速-功率散点图",
- xaxis: "转速(r/min)",
- yaxis: "有功功率(kW)",
- xrange: [0, 20],
- yrange: [0, 500],
- data: [
- {
- label: "异常点",
- mode: "markers",
- xData: [12, 40],
- yData: [200, 900],
- },
- ],
- },
- ],
- },
- },
- ],
- "全场运行状态分析",
- "anomalyOperationPO",
- { templateKey: "ctrl_op_state" },
- );
- assert(rangedOpFarm.length === 1, "运行状态总图应保留转速-功率散点");
- assert(
- rangedOpFarm[0].xAxis.min == null && rangedOpFarm[0].xAxis.max == null,
- "运行状态总图横轴应自适应",
- );
- assert(
- rangedOpFarm[0].yAxis.min == null && rangedOpFarm[0].yAxis.max == null,
- "运行状态总图纵轴应自适应",
- );
- assert(
- rangedOpFarm[0].xAxis.scale === true && rangedOpFarm[0].yAxis.scale === true,
- "运行状态总图应按数据自适应缩放",
- );
- const rangedPitch = listDetectorPlotOptions(
- {
- detector_name: "pitch_coord",
- panels: [
- {
- panelTitle: "功率-转速散点(桨距角着色)",
- xaxis: "转速 (r/min)",
- yaxis: "有功功率 (kW)",
- xrange: [5, 18],
- yrange: [0, 1500],
- data: [
- {
- label: "异常点",
- mode: "markers",
- xData: [8, 30],
- yData: [400, 2200],
- },
- ],
- },
- ],
- },
- "01 变桨-转速-功率协调分析",
- "anomalyPitchcoordPO",
- { templateKey: "pitch_coord" },
- );
- assert(
- rangedPitch[0].xAxis.min === 1000 && rangedPitch[0].xAxis.max == null,
- "变桨协调分图横轴应从 1000 起,上限自适应",
- );
- assert(
- rangedPitch[0].yAxis.min === 0 && rangedPitch[0].yAxis.max === 1500,
- "变桨协调分图纵轴仍按 yrange",
- );
- 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(!pitchColorOption.visualMap, "功率-转速全场图应按状态色而非桨距角着色");
- assert(
- pitchColorOption.series.some((item) => /001/.test(item.name)) &&
- pitchColorOption.series.some((item) => /005/.test(item.name)),
- "功率-转速全场图应按机组保留系列",
- );
- assert(
- pitchColorOption.series.reduce(
- (sum, item) => sum + (Array.isArray(item.data) ? item.data.length : 0),
- 0,
- ) === 5,
- "功率-转速全场图合并后不得丢失原始点",
- );
- const pitchSpeedOption = buildDetectorPlotOption(
- pitchCoordFarmTwo[1],
- pitchCoordFarmTwo[1].title,
- "anomalyPitchcoordPO",
- );
- assert(
- pitchSpeedOption.series.some((item) => /001/.test(item.name)) &&
- pitchSpeedOption.series.some((item) => /005/.test(item.name)),
- "桨距角-转速图应按机组保留系列",
- );
- 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].xAxis.min === -20, "偏航误差单机图 X 轴最小应为 -20");
- assert(yawErrorOptions[0].xAxis.max === 20, "偏航误差单机图 X 轴最大应为 20");
- 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 === "number",
- "最小桨距角点径应统一,不再按点数缩放气泡",
- );
- {
- const vm = pitchMinOptions[0].visualMap;
- const colors = Array.isArray(vm) ? vm[0]?.inRange?.color : vm?.inRange?.color;
- const step = (colors?.length || 1) - 1;
- const stopsOk = PITCH_MIN_REPORT_COLOR_STOPS.every(([ratio, color]) => {
- const index = Math.round(ratio * step);
- return colors?.[index]?.toLowerCase() === color.toLowerCase();
- });
- assert(
- stopsOk &&
- colors?.[0]?.toLowerCase() ===
- PITCH_MIN_REPORT_COLOR_STOPS[0][1].toLowerCase() &&
- colors?.slice(-1)[0]?.toLowerCase() ===
- PITCH_MIN_REPORT_COLOR_STOPS.at(-1)[1].toLowerCase(),
- "最小桨距角报告色带应与 PITCH_MIN_REPORT_COLOR_STOPS 停靠一致",
- );
- }
- 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(
- opStateOptions[0].xAxis.min == null && opStateOptions[0].xAxis.max == null,
- "运行状态转速-功率分图横轴应自适应",
- );
- assert(
- opStateOptions[0].yAxis.min == null && opStateOptions[0].yAxis.max == null,
- "运行状态转速-功率分图纵轴应自适应",
- );
- assert(
- opStateOptions[0].xAxis.scale === true && opStateOptions[0].yAxis.scale === true,
- "运行状态分图应按数据自适应缩放",
- );
- assert(
- opStateOptions[1].xAxis.min == null && opStateOptions[1].yAxis.min == null,
- "运行状态没有 xrange/yrange 的分图应自适应",
- );
- assert(
- opStateOptions[1].xAxis.scale === true && opStateOptions[1].yAxis.scale === true,
- "运行状态缺省轴范围时应自适应缩放",
- );
- assert(
- !String(opStateOptions[1].title?.subtext || "").includes("PC1"),
- "componentDefinition 不应再被前端拼接为小标题",
- );
- assert(
- resolvePanelSubtitle({ subtitle: "算法小标题" }, {}) === "算法小标题",
- "小标题应仅透传 JSON subtitle 字段",
- );
- 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)),
- "风场功率曲线总图应画出正常机组",
- );
- assert(
- !(Array.isArray(farmCurveOptions[0].legend)
- ? farmCurveOptions[0].legend
- : [farmCurveOptions[0].legend]
- ).some((item) => (item?.data || []).some((name) => /01号/.test(name))),
- "风场功率曲线总图图例不展示正常机组",
- );
- assert(
- farmCurveOptions[0].series.some((item) => /02号/.test(item.name)),
- "风场功率曲线总图应展示异常机组",
- );
- assert(
- farmCurveOptions[0].legend?.orient === "vertical" &&
- farmCurveOptions[0].legend?.right != null &&
- farmCurveOptions[0].grid3D?.right >= 140,
- "功率曲线图例应在三维图右侧",
- );
- const manyAnomalyTurbines = Array.from({ length: 9 }, (_, index) => ({
- turbine: `b${index + 1}`,
- anomaly: true,
- xType: "number",
- xData: [3.25, 4],
- yData: [80, 150],
- status: "异常",
- }));
- const packedCurveOptions = listDetectorPlotOptions(
- {
- detector: "wind_power_curve",
- chartType: "series",
- xaxis: "风速 (m/s)",
- yaxis: "有功功率 (kW)",
- turbines: [
- {
- turbine: "ok",
- anomaly: false,
- xData: [3.25, 4],
- yData: [120, 200],
- status: "正常",
- },
- ...manyAnomalyTurbines,
- ],
- },
- "全场功率曲线分析检测汇总",
- "anomalyPowercurvePO",
- { templateKey: "wind_power_curve", farmMode: true },
- );
- assert(
- packedCurveOptions[0].series.length === 10,
- "功率曲线总图应画出正常机组和全部异常机组",
- );
- assert(
- !Array.isArray(packedCurveOptions[0].legend) &&
- packedCurveOptions[0].legend?.orient === "vertical" &&
- packedCurveOptions[0].legend?.data?.length === 9,
- "异常机组较多时图例仍在右侧且不遗漏",
- );
- 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.length === 1,
- "风场静态偏航误差总图应为一张三档着色柱图",
- );
- assert(
- farmYawErrorOptions[0].series[0].type === "bar",
- "风场偏航误差总图应为柱状对比",
- );
- assert(
- farmYawErrorOptions[0].legend?.data
- ?.map((item) => item?.name ?? item)
- .join(",") === "[0,3],(3,5],>5",
- "风场偏航误差总图应按绝对值三档着色",
- );
- assert(
- !farmYawErrorOptions[0].series.some((item) => item.name === "风场均值"),
- "风场偏航误差总图不应再画均值线",
- );
- assert(
- farmYawErrorOptions[0].yAxis.name === "静态偏航误差(度)",
- "风场偏航误差纵轴应直接表示 JSON value",
- );
- const signedYaw = listDetectorPlotOptions(
- {
- detector: "yaw_error",
- chartType: "value",
- turbines: [{ turbine: "a1", anomaly: true, value: -8 }],
- },
- "全场静态偏航误差分析检测汇总",
- "anomalyYawErrorPO",
- { templateKey: "yaw_error", farmMode: true },
- );
- const signedBar = signedYaw[0].series
- .flatMap((item) => item.data || [])
- .find((item) => item && item.value != null);
- assert(signedBar?.value === -8, "静态偏航误差应使用 JSON value,不取绝对值");
- assert(
- farmYawErrorOptions[0].series.find((item) => item.name === ">5")?.data?.[0]
- ?.itemStyle?.color === "#F04438",
- "大于5°的机组应标红",
- );
- assert(
- farmYawErrorOptions[0].series.find((item) => item.name === "[0,3]")?.data?.[1]
- ?.itemStyle?.color === "#12B76A",
- "小于等于3°的机组应标绿",
- );
- const farmYawCountOptions = listDetectorPlotOptions(
- {
- detector: "yaw_count",
- chartType: "value",
- turbines: [
- { turbine: "a1", anomaly: true, value: 18, cw: 10, ccw: 8 },
- { turbine: "a2", anomaly: false, value: 6, cw: 3, ccw: 3 },
- { turbine: "a3", anomaly: true, value: 22, cw: 12, ccw: 10 },
- ],
- },
- "全场偏航次数分析检测汇总",
- "anomalyYawCountPO",
- {
- templateKey: "yaw_count",
- farmMode: true,
- nameMap: { a1: "01", a2: "02", a3: "03" },
- },
- );
- assert(
- legendNames(farmYawCountOptions[0]).join(",") === "正常机组,01号,03号",
- "偏航次数总图图例应列出正常色和全部异常机组",
- );
- assert(
- farmYawCountOptions[0].series.find((item) => item.name === "正常机组")?.data?.[1]
- ?.itemStyle?.color === DIAGNOSTIC_COLORS.normal,
- "偏航次数正常机组应统一蓝色",
- );
- assert(
- farmYawCountOptions[0].series.find((item) => item.name === "01号")?.data?.[0]
- ?.itemStyle?.color === DIAGNOSTIC_COLORS.anomaly,
- "偏航次数异常机组应标红",
- );
- assert(
- farmYawCountOptions[0].series.some((item) => item.name === "03号"),
- "偏航次数图例应包含全部异常机组",
- );
- const farmPitchMinOptions = listDetectorPlotOptions(
- {
- detector: "pitch_min",
- chartType: "value",
- turbines: [
- { turbine: "a1", anomaly: true, value: 2.4, p10: 1.2, baseline: -0.2 },
- { turbine: "a2", anomaly: false, value: 0.3, p10: 0.2, baseline: 0 },
- ],
- },
- "全场最小桨距角分析检测汇总",
- "anomalyMinpitchPO",
- {
- templateKey: "pitch_min",
- farmMode: true,
- nameMap: { a1: "01", a2: "02" },
- },
- );
- assert(
- legendNames(farmPitchMinOptions[0]).join(",") === "正常机组,01号",
- "最小桨距角总图图例应列出正常色和异常机组",
- );
- assert(
- farmPitchMinOptions[0].series.find((item) => item.name === "01号")?.data?.[0]
- ?.itemStyle?.color === DIAGNOSTIC_COLORS.anomaly,
- "最小桨距角异常机组应标红",
- );
- const farmAeroOptions = listDetectorPlotOptions(
- {
- detector: "aero_cp",
- chartType: "series",
- xaxis: "风速 (m/s)",
- yaxis: "Cp",
- turbines: [
- {
- turbine: "t1",
- anomaly: true,
- xData: [6, 8],
- yData: [0.4, 0.45],
- },
- {
- turbine: "t2",
- anomaly: false,
- xData: [6, 8],
- yData: [0.42, 0.48],
- },
- ],
- },
- "全场Cp功率系数分析检测汇总",
- "anomalyCpPO",
- { templateKey: "aero_cp", farmMode: true },
- );
- assert(
- legendNames(farmAeroOptions[0]).includes("正常机组"),
- "Cp总图应标明正常机组为蓝色",
- );
- assert(
- legendNames(farmAeroOptions[0]).includes("t1"),
- "Cp总图图例应展示异常机组",
- );
- assert(
- !legendNames(farmAeroOptions[0]).includes("t2"),
- "Cp总图有异常时图例不列正常机组",
- );
- assert(
- farmAeroOptions[0].series.find((item) => item.name === "t1")?.itemStyle
- ?.color === DIAGNOSTIC_COLORS.anomaly ||
- String(
- farmAeroOptions[0].series.find((item) => item.name === "t1")?.itemStyle
- ?.color || "",
- ).startsWith(DIAGNOSTIC_COLORS.anomaly),
- "Cp异常机组应标红",
- );
- const farmAeroPointOptions = listDetectorPlotOptions(
- {
- detector: "aero_cp",
- chartType: "series",
- xaxis: "功率 (kW)",
- yaxis: "功率系数 Cp",
- turbines: [
- {
- turbine: "26",
- anomaly: true,
- xData: [100, 200, 300],
- yData: [0.2, 0.3, 0.25],
- pointAnomaly: [false, true, false],
- },
- {
- turbine: "27",
- anomaly: false,
- xData: [120, 220],
- yData: [0.22, 0.28],
- pointAnomaly: [false, false],
- },
- ],
- },
- "全场Cp功率系数分析检测汇总",
- "anomalyCpPO",
- { templateKey: "aero_cp", farmMode: true, nameMap: { 26: "26", 27: "27" } },
- );
- const aeroDataSeries = (farmAeroPointOptions[0].series || []).filter(
- (item) => Array.isArray(item.data) && item.data.length > 0,
- );
- assert(
- aeroDataSeries.length === 3,
- "Cp总图应按 pointAnomaly 拆分:26/27号正常点各一条,26号异常点一条",
- );
- const aero26NormalSeries = aeroDataSeries.find(
- (item) =>
- String(item.name || "").includes("26") &&
- /正常/.test(String(item.name || "")),
- );
- const aero26AnomalySeries = aeroDataSeries.find(
- (item) =>
- String(item.name || "").includes("26") &&
- !/正常/.test(String(item.name || "")),
- );
- assert(
- aero26NormalSeries &&
- String(aero26NormalSeries.itemStyle?.color || "").startsWith(
- DIAGNOSTIC_COLORS.normal,
- ),
- "Cp总图 pointAnomaly=false 的点应为蓝色",
- );
- assert(
- aero26AnomalySeries &&
- String(aero26AnomalySeries.itemStyle?.color || "").startsWith(
- DIAGNOSTIC_COLORS.anomaly,
- ),
- "Cp总图 pointAnomaly=true 的点应为红色",
- );
- assert(
- aero26NormalSeries?.data?.length === 2 &&
- aero26AnomalySeries?.data?.length === 1,
- "26号正常点保留 2 个(下标 0/2),异常点仅 1 个(下标 1)",
- );
- assert(
- !(farmAeroPointOptions[0].series || []).some(
- (item) => String(item.name || "") === "27号" && item.data?.length,
- ),
- "27号无异常点时不单独成异常系列",
- );
- 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 },
- );
- const yawStaticDataSeries = farmYawStaticOptions[0].series.filter(
- (item) => item.data?.length,
- );
- assert(
- yawStaticDataSeries.length === 2,
- "风场静态偏航总图应按 pointAnomaly 拆成正常/异常点",
- );
- assert(
- farmYawStaticOptions[0].series.every((item) => {
- const size = Number(item.symbolSize) || 0;
- if (!item.data?.length) return true;
- return size > 0 && size <= 4;
- }) &&
- new Set(
- farmYawStaticOptions[0].series
- .filter((item) => item.data?.length)
- .map((item) => Number(item.symbolSize)),
- ).size === 1,
- "全场静态偏航报告正常点与异常点大小应一致",
- );
- assert(
- Number(yawStaticDataSeries[0].itemStyle?.opacity) <= 0.7,
- "全场静态偏航报告散点透明度应更低",
- );
- assert(
- legendNames(farmYawStaticOptions[0]).includes("正常机组"),
- "偏航总图应标明正常点为蓝色",
- );
- assert(
- legendNames(farmYawStaticOptions[0]).includes("t1"),
- "偏航总图有异常时应在图例展示异常机组",
- );
- assert(
- farmYawStaticOptions[0].series.some(
- (item) =>
- item.name === "t1" && item.itemStyle?.color?.startsWith("#F04438"),
- ),
- "偏航总图异常点应为统一红色",
- );
- assert(
- farmYawStaticOptions[0].series.some(
- (item) =>
- /正常/.test(item.name) && item.itemStyle?.color?.startsWith("#2E90FA"),
- ),
- "偏航总图正常点应为统一蓝色",
- );
- assert(
- typeof farmYawStaticOptions[0].yAxis?.axisLabel?.formatter === "function",
- "报告 Y 轴应保留 2 位小数",
- );
- assert(
- formatReportValueAxisLabel(-1416.9293302406602) === "-1,416.93",
- "Y 轴刻度应格式化为 2 位小数",
- );
- 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.length === 1,
- "报告偏航误差总图应为一张三档着色柱图",
- );
- assert(
- yawErrorFarmFromTurbines[0].series[0].type === "bar",
- "报告偏航误差总图应与页面一样为单值柱状",
- );
- assert(
- legendNames(yawErrorFarmFromTurbines[0]).join(",") === "[0,3],(3,5],>5",
- "报告偏航误差总图应按绝对值三档着色",
- );
- 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,
- "无关注机组时应为空",
- );
- assert(
- pickKeyTurbines(
- [
- { engineId: "1", turbineName: "DT01" },
- { engineId: "2", turbineName: "DT02" },
- { engineId: "3", turbineName: "DT03" },
- { engineId: "4", turbineName: "DT04" },
- ],
- [
- { engineId: "1", model2YawYawcountRatio: 0.9 },
- { engineId: "2", model2YawYawerrorRatio: 0.9 },
- { engineId: "3", model3PitchMinpitchRatio: 0.9 },
- { engineId: "4", model4CtrlparamDeloadRatio: 0.9 },
- { engineId: "5", model1WindpwrPowercurveRatio: 0.9 },
- ],
- ).length === 0,
- "偏航次数/静态偏航误差/最小桨距角/降载/功率曲线不应参与关注机组判定",
- );
- const emptyRowsPayload = buildDetectorSectionPayload(
- DETECTOR_TEMPLATE_CONFIG.find((cfg) => cfg.templateKey === "pitch_coord"),
- {
- farmImages: [{ image: "demo" }],
- turbineImages: [{ image: "demo" }],
- rows: [],
- hasChartData: true,
- },
- );
- assert(
- emptyRowsPayload.pitch_coordRows[0].turbine_name === "暂无",
- "异常机组清单无数据时应写暂无",
- );
- const emptySensorRows = buildSensorAnomalyRows([]);
- assert(
- emptySensorRows[0].turbine_name === "暂无",
- "数据感知统计无数据时应写暂无",
- );
- const sensorTableRows = buildSensorAnomalyRows([
- {
- engineName: "10",
- sensorAnomalyWindPwr: 1,
- sensorAnomalyWindPwrRatio: 0.086,
- },
- ]);
- assert(sensorTableRows.length === 1, "数据感知表应按异常类型分行");
- assert(
- sensorTableRows[0].ratio === "8.60%",
- `数据感知占比应来自 ratio 字段,实际 ${sensorTableRows[0].ratio}`,
- );
- assert(
- sensorTableRows[0].sensor_anomaly_type === "风速-功率逻辑异常",
- "数据感知表异常类型应与 flag 字段对应",
- );
- const majorTempRows = buildSensorAnomalyRows([
- {
- engineName: "10",
- sensorAnomalyMajorTemp: 1,
- sensorAnomalyMajorTempRatio: 0.3,
- },
- ]);
- assert(
- majorTempRows.length === 1 &&
- majorTempRows[0].sensor_anomaly_type === "大部件温度异常" &&
- majorTempRows[0].ratio === "30.00%",
- "数据感知表应支持大部件温度异常行",
- );
- assert(
- formatSensorTypeText({ sensorAnomalyType: "9" }) === "大部件温度异常",
- "数据感知类型 9 应翻译为大部件温度异常",
- );
- assert(
- formatSensorTypeText({ sensorAnomalyType: "1,5" }) === "功率异常、扭矩异常",
- "多个数据感知类型编码应按顿号拼接",
- );
- assert(
- formatSensorTypeText({}) === "暂无异常",
- "无数据感知异常时应写暂无异常",
- );
- const sensorHeat = buildSensorHeatmapData([
- {
- engineName: "09",
- sensorAnomalyPowerRatio: 0.12,
- sensorAnomalyWindRatio: 0,
- },
- {
- engineName: "16",
- sensorAnomalyPowerRatio: 0,
- sensorAnomalyWindRatio: 0.2,
- sensorAnomalyMajorTempRatio: 0.35,
- },
- ]);
- assert(
- sensorHeat.xLabels.join(",") === "09号,16号",
- "数据感知热力图风机名应带号",
- );
- assert(sensorHeat.yLabels[0] === "功率异常", "数据感知热力图首行应为功率异常");
- assert(
- sensorHeat.yLabels.includes("偏航角、扭缆角、偏航误差等偏航"),
- "数据感知热力图应含偏航角、扭缆角、偏航误差等偏航",
- );
- assert(
- sensorHeat.yLabels.includes("大部件温度异常"),
- "数据感知热力图应含大部件温度异常",
- );
- assert(sensorHeat.yLabels.length === 9, "数据感知热力图应为9行");
- assert(
- sensorHeat.matrix[8][1] === 35 && sensorHeat.matrix[8][0] === 0,
- "大部件温度异常行应按 sensorAnomalyMajorTempRatio 取占比",
- );
- assert(sensorHeat.matrix[0][0] === 12, "数据感知热力图应按占比×100");
- assert(chunkHeatmapData(sensorHeat, 1).length === 2, "热力图应按风机分片");
- const detectorHeat = buildDetectorHeatmapData([
- {
- engineName: "01",
- model2YawYawerrorRatio: 18,
- model2YawYawcountRatio: 110,
- model3PitchMinpitchRatio: 1,
- model4CtrlparamDeloadRatio: 0.01,
- },
- ]);
- assert(
- detectorHeat.yLabels.includes("静态偏航误差"),
- "功能诊断热力图应含静态偏航误差",
- );
- assert(
- detectorHeat.matrix[detectorHeat.yLabels.indexOf("静态偏航误差")][0] === 18,
- "静态偏航误差热力图应按角度原值",
- );
- assert(
- detectorHeat.matrix[detectorHeat.yLabels.indexOf("偏航次数")][0] === 110,
- "偏航次数热力图应按次数原值",
- );
- assert(
- detectorHeat.matrix[detectorHeat.yLabels.indexOf("最小桨距角异常")][0] === 1,
- "最小桨距角热力图应按角度原值",
- );
- assert(
- detectorHeat.matrix[detectorHeat.yLabels.indexOf("降载情况")][0] === 1,
- "功能诊断热力图占比换算错误",
- );
- const detectorHeatOption = buildReportHeatmapOption(
- detectorHeat,
- "各机组功能诊断异常分布",
- );
- const heatRamp = (label) => {
- const y = detectorHeat.yLabels.indexOf(label);
- const seriesIndex = detectorHeatOption.series.findIndex((item) =>
- (item.data || []).some((cell) => cell.value[1] === y),
- );
- const maps = [].concat(detectorHeatOption.visualMap || []);
- return (maps.find((item) => item.seriesIndex === seriesIndex)?.inRange?.color || []).join(",");
- };
- assert(
- Array.isArray(detectorHeatOption.visualMap) &&
- detectorHeatOption.visualMap.length > 1 &&
- heatRamp("静态偏航误差") !== heatRamp("降载情况") &&
- heatRamp("偏航次数") !== heatRamp("静态偏航误差") &&
- heatRamp("风功率曲线情况") !== heatRamp("降载情况"),
- "功能诊断热力图应按符合度、角度、次数、异常率分色",
- );
- assert(
- summarizeTurbineAnomalyRate(
- [
- { title: "降载判定分析", points: 477, rate: 0.3286 },
- { title: "变桨协调分析", points: 8, rate: 0.0052 },
- ],
- { model4CtrlparamDeloadRatio: 0.3286, model3PitchPitchcoordRatio: 0.0052 },
- ).toFixed(4) === "0.3233",
- "表4-2 机组异常率应按异常点数加权,并优先使用热力图单项占比",
- );
- const summaryWithoutDeload = buildAnomalySummaryRows([
- {
- turbineName: "07",
- anomalyDetectorCount: 1,
- anomalyPoints: 250,
- anomalyRate: 0.0893,
- mainAnomalyType: "降载判定分析",
- detectorHits: [{ title: "降载判定分析", points: 250, rate: 0.0893 }],
- },
- {
- turbineName: "08",
- anomalyDetectorCount: 2,
- anomalyPoints: 220,
- anomalyRate: 0.1,
- mainAnomalyType: "降载判定分析",
- detectorHits: [
- { title: "降载判定分析", points: 200, rate: 0.12 },
- { title: "变桨协调分析", points: 20, rate: 0.01 },
- ],
- },
- ]);
- assert(
- summaryWithoutDeload.length === 1 &&
- summaryWithoutDeload[0].turbine_name === "08" &&
- summaryWithoutDeload[0].anomaly_detector_count === "变桨协调分析" &&
- summaryWithoutDeload[0].main_anomaly_type === "变桨协调异常率" &&
- summaryWithoutDeload[0].anomaly_rate === "1.00%",
- "表4-2 不应收录仅降载异常机组,其余检测项应分行给出名称、异常率和说明",
- );
- const summaryMetricRows = buildAnomalySummaryRows([
- {
- turbineName: "009",
- detectorHits: [
- { title: "静态偏航误差分析", templateKey: "yaw_error", points: 4, rate: 8 },
- { title: "偏航次数分析", templateKey: "yaw_count", points: 2, rate: 110 },
- { title: "最小桨距角分析", templateKey: "pitch_min", points: 1, rate: 1.5 },
- { title: "变桨一致性分析", templateKey: "pitch_regulation", points: 3, rate: 0.021 },
- ],
- },
- ]);
- assert(
- summaryMetricRows.map((row) => row.anomaly_detector_count).join(",") ===
- "静态偏航误差分析,变桨一致性分析,偏航次数分析,最小桨距角分析",
- "表4-2 功能诊断项应为检测器名称",
- );
- assert(
- summaryMetricRows.map((row) => row.anomaly_rate).join(",") ===
- "8°,2.10%,110次,1.5°",
- "表4-2 数值应按度、次数或异常率分别格式化",
- );
- assert(
- summaryMetricRows.map((row) => row.main_anomaly_type).join(",") ===
- "静态偏航误差度,变桨一致性异常率,偏航次数,最小桨距角",
- "表4-2 数值说明应写明指标含义",
- );
- 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, "数据感知雷达图应映射异常台数");
- assert(sensorRadar.max === 3, "数据感知雷达图应按异常台数自适应上限");
- assert(
- sensorRadar.indicators.length === 9 &&
- sensorRadar.indicators[8].name === "大部件温度异常" &&
- sensorRadar.values[8] === 0,
- "数据感知雷达图应含大部件温度异常轴",
- );
- const majorTempRadar = buildSensorRadarData(
- { sensorAnomalyMajorTempCount: 5 },
- 20,
- );
- assert(
- majorTempRadar.values[8] === 5,
- "大部件温度异常台数应取自 sensorAnomalyMajorTempCount",
- );
- const detectorRadar = buildDetectorRadarData(
- { model1Count: 3, model2Count: 1 },
- 20,
- );
- assert(detectorRadar.max === 4, "功能诊断雷达图应按异常台数自适应上限");
- 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台"),
- "雷达图维度应标注台数",
- );
- assert(
- sensorRadarOption.radar.indicator.every((item) => item.max === 3),
- "雷达图外圈应按本图最大台数自适应",
- );
- assert(sensorRadarOption.radar.splitNumber === 3, "雷达图刻度应落到整数台数");
- 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 completenessHeatmap = buildDataCompletenessHeatmapData(
- [
- {
- engineName: "111",
- sourceDatetime: new Date("2026-08-01").getTime(),
- minuteDataCompleteness: 96.5,
- },
- {
- engineName: "112",
- sourceDatetime: new Date("2026-08-01").getTime(),
- minuteDataCompleteness: 88.2,
- },
- {
- engineName: "111",
- sourceDatetime: new Date("2026-08-02").getTime(),
- minuteDataCompleteness: 91,
- },
- ],
- "minuteDataCompleteness",
- );
- assert(
- completenessHeatmap.yLabels.length === 2 &&
- completenessHeatmap.xLabels.length === 2,
- "数据完整度热力图应正确展开机组×日期",
- );
- assert(
- completenessHeatmap.xLabels.join(",") === "08-01,08-02",
- "数据完整度 X 轴应为日期",
- );
- assert(
- completenessHeatmap.yLabels.length === 2 &&
- completenessHeatmap.yLabels.every((name) => /111|112/.test(name)),
- "数据完整度 Y 轴应为风机",
- );
- const completenessMinuteBuf = await renderEchartsOption(
- buildCompletenessHeatmapOption(
- completenessHeatmap,
- "分钟级数据逐日完整度(近90天,%)",
- ),
- { width: 920, height: 420 },
- );
- const completenessOption = buildCompletenessHeatmapOption(
- completenessHeatmap,
- "分钟级数据逐日完整度(近90天,%)",
- );
- assert(
- completenessOption.xAxis?.name === "日期" &&
- completenessOption.yAxis?.name === "风机",
- "数据完整度热力图轴名称应为日期/风机",
- );
- registerImageBuffer(
- imageBufferMap,
- "anomaly_data_completeness_minute",
- completenessMinuteBuf,
- );
- registerImageBuffer(
- imageBufferMap,
- "anomaly_data_completeness_second",
- completenessMinuteBuf,
- );
- 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) <= 6,
- "报告散点 symbolSize 应缩小以提升清晰度",
- );
- assert(denseOption.backgroundColor === "#eef4fb", "报告图表应使用浅蓝灰底色");
- 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: buildSensorAnomalyRows([
- {
- engineName: "111",
- sensorAnomalyWindPwr: 1,
- sensorAnomalyWindPwrRatio: 0.125,
- },
- {
- engineName: "16",
- sensorAnomalyWind: 1,
- sensorAnomalyWindRatio: 0.2,
- sensorAnomalyMajorTemp: 1,
- sensorAnomalyMajorTempRatio: 0.3,
- },
- ]),
- 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: "偏航分析",
- 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_desc: "主要表现为偏航角越限、突变或长时间不动作。",
- suggestion:
- "建议检查偏航编码器、风向标、偏航制动器及对风控制参数,对异常机组进行现场复核。",
- problem_nature: "偏航与扭缆系统异常",
- turbine_names: "#111",
- },
- ],
- "zn-techcn-replace-tags-data_sensor_anomaly-generalFiles": [
- {
- image: "anomaly_sensor_radar",
- figure_caption: "图3-1 数据感知异常台数",
- },
- {
- image: "anomaly_sensor_dist",
- figure_caption: "图3-2 各机组数据感知异常分布",
- },
- ],
- "zn-techcn-replace-tags-data_detector_anomaly-generalFiles": [
- {
- image: "anomaly_detector_radar",
- figure_caption: "图4-1 功能诊断异常台数",
- },
- {
- image: "anomaly_detector_dist",
- figure_caption: "图4-2 各机组功能诊断异常分布",
- },
- ],
- "zn-techcn-replace-tags-data_completeness_minute-farmSummary": [
- { image: "anomaly_data_completeness_minute" },
- ],
- "zn-techcn-replace-tags-data_completeness_second-farmSummary": [
- { image: "anomaly_data_completeness_second" },
- ],
- "zn-techcn-replace-tags-key_turbine-generalFiles": [],
- show_module_wind: [],
- show_module_yaw: [{}],
- show_module_pitch: [],
- show_module_run: [],
- show_module_aero: [],
- };
- 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%",
- sensor_anomaly_type: "大部件温度异常",
- comment: "功能诊断异常",
- },
- ],
- anomalyPoints: 3,
- anomalyRate: 0.12,
- anomalyTurbines: 1,
- hasChartData: true,
- }),
- );
- 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);
- const smokeZip = new PizZip(buffer);
- Object.keys(smokeZip.files)
- .filter((name) => /^word\/footer\d+\.xml$/.test(name))
- .sort()
- .forEach((name) => {
- const xml = smokeZip.file(name).asText();
- const plain = xml.replace(/<[^>]+>/g, "").trim();
- assert(!/PAGE/.test(xml), `${name} 不应含页码域`);
- assert(!plain, `${name} 页脚应为空,实际为「${plain}」`);
- });
- const smokeDocText = smokeZip
- .file("word/document.xml")
- .asText()
- .replace(/<[^>]+>/g, "");
- assert(
- smokeDocText.includes("数据感知异常类型"),
- "报告中检测器明细表应包含数据感知异常类型列",
- );
- assert(
- smokeDocText.includes("大部件温度异常"),
- "报告应输出大部件温度异常",
- );
- assert(
- smokeDocText.includes("大部件温度(齿轮箱油温、发电机轴承"),
- "表3-1 应包含大部件温度关联测点说明",
- );
- assert(
- !/\{[A-Za-z_]/.test(smokeDocText),
- "报告中不应残留模板占位符",
- );
- await shutdownChartService();
|