| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102 |
- import { DETECTOR_TEMPLATE_CONFIG, adaptiveRadarMax } from "./anomalyChartBuilder.js";
- const SENSOR_TYPE_LABELS = {
- 1: "功率异常",
- 2: "风速异常",
- 3: "变桨角度异常",
- 4: "转速异常",
- 5: "扭矩异常",
- 6: "风速-功率逻辑异常",
- 7: "转速-扭矩逻辑异常",
- 8: "偏航角、扭缆角、偏航误差等偏航",
- 9: "大部件温度异常",
- };
- const MODEL_SENSOR_FIELDS = [
- { field: "sensorAnomalyPower", label: "功率异常" },
- { field: "sensorAnomalyWind", label: "风速异常" },
- { field: "sensorAnomalyPitch", label: "变桨角度异常" },
- { field: "sensorAnomalySpeed", label: "转速异常" },
- { field: "sensorAnomalyTorque", label: "扭矩异常" },
- { field: "sensorAnomalyWindPwr", label: "风速-功率逻辑异常" },
- { field: "sensorAnomalySpdTrq", label: "转速-扭矩逻辑异常" },
- {
- field: "sensorAnomalyFreeze",
- label: "偏航角、扭缆角、偏航误差等偏航",
- },
- { field: "sensorAnomalyMajorTemp", label: "大部件温度异常" },
- ];
- const SENSOR_HEATMAP_FIELDS = [
- { ratioFields: ["sensorAnomalyPowerRatio"], label: "功率异常" },
- { ratioFields: ["sensorAnomalyWindRatio"], label: "风速异常" },
- { ratioFields: ["sensorAnomalyPitchRatio"], label: "变桨角度异常" },
- { ratioFields: ["sensorAnomalySpeedRatio"], label: "转速异常" },
- { ratioFields: ["sensorAnomalyTorqueRatio"], label: "扭矩异常" },
- { ratioFields: ["sensorAnomalyWindPwrRatio"], label: "风速-功率逻辑异常" },
- { ratioFields: ["sensorAnomalySpdTrqRatio"], label: "转速-扭矩逻辑异常" },
- {
- ratioFields: ["sensorAnomalyFreezeRatio"],
- label: "偏航角、扭缆角、偏航误差等偏航",
- },
- { ratioFields: ["sensorAnomalyMajorTempRatio"], label: "大部件温度异常" },
- ];
- /** 表3-2:与热力图共用 ratio 字段,按「风机 × 异常类型」展开行 */
- const SENSOR_TABLE_FIELDS = MODEL_SENSOR_FIELDS.map((cfg, index) => ({
- flagField: cfg.field,
- label: cfg.label,
- ratioFields: SENSOR_HEATMAP_FIELDS[index]?.ratioFields || [],
- }));
- const DETECTOR_HEATMAP_FIELDS = [
- { ratioFields: ["model1WindpwrPowercurveRatio"], label: "风功率曲线情况" },
- { ratioFields: ["model1WindpwrScatterRatio"], label: "风功率散点异常" },
- { ratioFields: ["model2YawStaticyawRatio"], label: "偏航异常" },
- { ratioFields: ["model2YawCabletwistRatio"], label: "扭缆异常" },
- {
- ratioFields: [
- "model2YawYawcountRatio",
- "model2YawCountRatio",
- "model2YawYawCountRatio",
- ],
- label: "偏航次数",
- valueKind: "count",
- },
- {
- ratioFields: [
- "model2YawYawerrorRatio",
- "model2YawErrorRatio",
- "model2YawYawErrorRatio",
- ],
- label: "静态偏航误差",
- valueKind: "degree",
- },
- { ratioFields: ["model3PitchPitchregulationRatio"], label: "变桨一致性异常" },
- { ratioFields: ["model3PitchPitchcoordRatio"], label: "变桨协调异常" },
- {
- ratioFields: ["model3PitchMinpitchRatio", "model3PitchMinPitchRatio"],
- label: "最小桨距角异常",
- valueKind: "degree",
- },
- { ratioFields: ["model4CtrlparamDeloadRatio"], label: "降载情况" },
- {
- ratioFields: ["model4CtrlparamOperationstateRatio"],
- label: "运行状态异常",
- },
- { ratioFields: ["model4CtrlparamPowerqualityRatio"], label: "电能质量异常" },
- { ratioFields: ["model5AerodynamicsTsrRatio"], label: "TSR异常" },
- { ratioFields: ["model5AerodynamicsCpRatio"], label: "CP异常" },
- {
- ratioFields: ["model5AerodynamicsCpTsrRatio"],
- label: "TSR-CP联合分布异常",
- },
- ];
- const HEATMAP_CHUNK_SIZE = 12;
- /** 报告数据完整度热力图:每张图最多的风机数 */
- const COMPLETENESS_TURBINE_CHUNK = 10;
- const SENSOR_RADAR_FIELDS = [
- { key: "sensorAnomalyPowerCount", label: "功率异常" },
- { key: "sensorAnomalyWindCount", label: "风速异常" },
- { key: "sensorAnomalyPitchCount", label: "变桨角度异常" },
- { key: "sensorAnomalyApeedCount", label: "转速异常" },
- { key: "sensorAnomalyTorqueCount", label: "扭矩异常" },
- { key: "sensorAnomalyWindPwrCount", label: "风速-功率\n逻辑异常" },
- { key: "sensorAnomalySpdTrqCount", label: "转速-扭矩\n逻辑异常" },
- {
- key: "sensorAnomalyFreezeCount",
- label: "偏航角、扭缆角、\n偏航误差等偏航",
- },
- { key: "sensorAnomalyMajorTempCount", label: "大部件温度异常" },
- ];
- const MODEL_RADAR_FIELDS = [
- { key: "model1Count", label: "风功率异常" },
- { key: "model2Count", label: "偏航系统\n异常" },
- { key: "model3Count", label: "变桨系统\n异常" },
- { key: "model4Count", label: "运行状态\n异常" },
- { key: "model5Count", label: "气动性能\n异常" },
- ];
- /**
- * 第6章重点机组判定字段。
- * 偏航次数、最小桨距角、静态偏航误差与降载判定、功率曲线一样不参与判定。
- */
- const WATCHLIST_RATIO_FIELDS = [
- "model1WindpwrScatterRatio",
- "model2YawStaticyawRatio",
- "model2YawCabletwistRatio",
- "model3PitchPitchregulationRatio",
- "model3PitchPitchcoordRatio",
- "model4CtrlparamPowerqualityRatio",
- "model4CtrlparamOperationstateRatio",
- "model5AerodynamicsCpRatio",
- "model5AerodynamicsTsrRatio",
- "model5AerodynamicsCpTsrRatio",
- ];
- export function displayTurbineName(item = {}) {
- return item.engineName || item.engineId || "";
- }
- export function formatHeatmapEngineLabel(engineName) {
- if (engineName == null || engineName === "") return "--";
- const text = String(engineName);
- if (/号$/.test(text)) return text;
- if (/^\d+$/.test(text)) return `${text}号`;
- return text;
- }
- function readRatioPercent(item, ratioFields = []) {
- for (const field of ratioFields) {
- if (item?.[field] == null || item[field] === "") continue;
- return toPercentNumber(item[field]);
- }
- return 0;
- }
- function readHeatmapRaw(item, ratioFields = []) {
- for (const field of ratioFields) {
- if (item?.[field] == null || item[field] === "") continue;
- const num = Number(item[field]);
- return Number.isFinite(num) ? num : 0;
- }
- return 0;
- }
- function buildHeatmapFromFields(modelList = [], fields = []) {
- const list = sortTurbines(modelList);
- return {
- xLabels: list.map((item) =>
- formatHeatmapEngineLabel(displayTurbineName(item)),
- ),
- yLabels: fields.map((item) => item.label),
- valueKinds: fields.map((item) => item.valueKind || "percent"),
- matrix: fields.map((field) =>
- list.map((item) =>
- field.valueKind === "count" || field.valueKind === "degree"
- ? readHeatmapRaw(item, field.ratioFields)
- : readRatioPercent(item, field.ratioFields),
- ),
- ),
- };
- }
- export function buildSensorHeatmapData(modelList = []) {
- return buildHeatmapFromFields(modelList, SENSOR_HEATMAP_FIELDS);
- }
- export function buildDetectorHeatmapData(modelList = []) {
- return buildHeatmapFromFields(modelList, DETECTOR_HEATMAP_FIELDS);
- }
- function formatCompletenessDate(sourceDatetime) {
- const ms = Number(sourceDatetime);
- if (!Number.isFinite(ms) || ms <= 0) return "--";
- const d = new Date(ms);
- const pad = (n) => String(n).padStart(2, "0");
- return `${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
- }
- function parseCompletenessValue(raw) {
- if (raw == null || raw === "") return null;
- if (typeof raw === "number") {
- return Number.isFinite(raw) ? raw : null;
- }
- const text = String(raw).trim();
- if (!text) return null;
- const direct = Number(text);
- if (Number.isFinite(direct)) return direct;
- try {
- const parsed = JSON.parse(text);
- if (typeof parsed === "number") return parsed;
- if (parsed && typeof parsed === "object") {
- const nested =
- parsed.value ??
- parsed.ratio ??
- parsed.completeness ??
- parsed.percent;
- const num = Number(nested);
- if (Number.isFinite(num)) return num;
- }
- } catch (_error) {
- // ignore
- }
- return null;
- }
- /**
- * 数据完整度热力图:X=日期,Y=机组(与功能诊断热力图轴向不同,但复用同一 ECharts 渲染器)。
- */
- export function buildDataCompletenessHeatmapData(
- rows = [],
- valueKey = "minuteDataCompleteness",
- ) {
- const list = Array.isArray(rows) ? rows : [];
- if (!list.length) {
- return { xLabels: [], yLabels: [], matrix: [] };
- }
- const dateKeys = [
- ...new Set(
- list
- .map((row) => Number(row?.sourceDatetime))
- .filter((ms) => Number.isFinite(ms) && ms > 0),
- ),
- ].sort((a, b) => a - b);
- const xLabels = dateKeys.map(formatCompletenessDate);
- const engineNames = [
- ...new Set(
- list.map((row) =>
- formatHeatmapEngineLabel(displayTurbineName(row)),
- ),
- ),
- ].sort((a, b) => a.localeCompare(b, "zh-CN", { numeric: true }));
- const yLabels = engineNames;
- const cellMap = new Map();
- list.forEach((row) => {
- const engine = formatHeatmapEngineLabel(displayTurbineName(row));
- const ms = Number(row?.sourceDatetime);
- if (!engine || !Number.isFinite(ms)) return;
- const value = parseCompletenessValue(row?.[valueKey]);
- if (value == null) return;
- cellMap.set(`${engine}|${ms}`, value);
- });
- const matrix = yLabels.map((engine) =>
- dateKeys.map((ms) => {
- const value = cellMap.get(`${engine}|${ms}`);
- return value == null ? 0 : value;
- }),
- );
- return { xLabels, yLabels, matrix };
- }
- function buildRadarData(fields, source = {}) {
- const values = fields.map((item) => Number(source[item.key]) || 0);
- const max = adaptiveRadarMax(values);
- return {
- indicators: fields.map((item) => ({ name: item.label, max })),
- values,
- max,
- };
- }
- export function buildSensorRadarData(sensorCount = {}) {
- return buildRadarData(SENSOR_RADAR_FIELDS, sensorCount);
- }
- export function buildDetectorRadarData(modelCount = {}) {
- return buildRadarData(MODEL_RADAR_FIELDS, modelCount);
- }
- export function chunkHeatmapData(heatmap, size = HEATMAP_CHUNK_SIZE) {
- const xLabels = heatmap?.xLabels || [];
- const yLabels = heatmap?.yLabels || [];
- const matrix = heatmap?.matrix || [];
- if (!xLabels.length || !yLabels.length) return [];
- const chunks = [];
- for (let i = 0; i < xLabels.length; i += size) {
- chunks.push({
- yLabels,
- valueKinds: heatmap?.valueKinds,
- xLabels: xLabels.slice(i, i + size),
- matrix: matrix.map((row) => (row || []).slice(i, i + size)),
- });
- }
- return chunks;
- }
- /**
- * 数据完整度:Y 轴按风机分页,每张最多 10 台;日期仍按列分页,避免 90 天挤在一张图上。
- */
- export function chunkCompletenessHeatmapData(
- heatmap,
- turbineSize = COMPLETENESS_TURBINE_CHUNK,
- dateSize = HEATMAP_CHUNK_SIZE,
- ) {
- const xLabels = heatmap?.xLabels || [];
- const yLabels = heatmap?.yLabels || [];
- const matrix = heatmap?.matrix || [];
- if (!xLabels.length || !yLabels.length) return [];
- const turbineStep = Math.max(1, Number(turbineSize) || COMPLETENESS_TURBINE_CHUNK);
- const dateStep = Math.max(1, Number(dateSize) || HEATMAP_CHUNK_SIZE);
- const chunks = [];
- for (let y = 0; y < yLabels.length; y += turbineStep) {
- const ySlice = yLabels.slice(y, y + turbineStep);
- const matrixSlice = matrix.slice(y, y + turbineStep);
- for (let x = 0; x < xLabels.length; x += dateStep) {
- chunks.push({
- yLabels: ySlice,
- xLabels: xLabels.slice(x, x + dateStep),
- matrix: matrixSlice.map((row) => (row || []).slice(x, x + dateStep)),
- });
- }
- }
- return chunks;
- }
- export function sortTurbines(list = []) {
- return [...list].sort((a, b) =>
- String(displayTurbineName(a)).localeCompare(
- String(displayTurbineName(b)),
- "zh-CN",
- {
- numeric: true,
- },
- ),
- );
- }
- export function toPercentNumber(value) {
- const num = Number(value);
- if (!Number.isFinite(num)) return 0;
- return Number.parseFloat((num * 100).toPrecision(12));
- }
- export function formatPercent(value, digits = 2) {
- return toPercentNumber(value).toFixed(digits);
- }
- /** 表4-2 数值说明:角度、次数按原值,其余检测项为异常率 */
- export function formatSummaryValueCaption(templateKey, title) {
- if (templateKey === "yaw_error") return "静态偏航误差度";
- if (templateKey === "pitch_min") return "最小桨距角";
- if (templateKey === "yaw_count") return "偏航次数";
- const name = String(title || "检测项").replace(/分析$/, "");
- return `${name}异常率`;
- }
- export function formatSummaryMetricValue(templateKey, rate) {
- const num = Number(rate);
- if (!Number.isFinite(num)) return "—";
- if (templateKey === "yaw_count") return `${Math.round(num)}次`;
- if (templateKey === "yaw_error" || templateKey === "pitch_min") {
- return `${Number.parseFloat(num.toPrecision(12))}°`;
- }
- return `${formatPercent(num)}%`;
- }
- function detectorConfigByHit(hit = {}) {
- if (hit.templateKey) {
- const matched = DETECTOR_TEMPLATE_CONFIG.find(
- (cfg) => cfg.templateKey === hit.templateKey,
- );
- if (matched) return matched;
- }
- return (
- DETECTOR_TEMPLATE_CONFIG.find((cfg) => cfg.title === hit.title) || null
- );
- }
- function summaryRowFromHit(turbineName, hit = {}) {
- const cfg = detectorConfigByHit(hit);
- const templateKey = cfg?.templateKey || hit.templateKey || "";
- const title = cfg?.title || hit.title || "—";
- return {
- turbine_name: turbineName,
- anomaly_detector_count: title,
- anomaly_rate: formatSummaryMetricValue(templateKey, hit.rate),
- main_anomaly_type: formatSummaryValueCaption(templateKey, title),
- anomaly_points: String(Number(hit.points) || 0),
- };
- }
- /** 偏航次数是次数,静态偏航误差和最小桨距角是度,都不按占比换算 */
- export function formatDetectorRowMetric(templateKey, rate) {
- const num = Number(rate);
- if (!Number.isFinite(num)) return "—";
- if (templateKey === "yaw_count") {
- return String(Math.round(num));
- }
- if (templateKey === "yaw_error" || templateKey === "pitch_min") {
- return Number.parseFloat(num.toPrecision(12)).toFixed(2);
- }
- return `${formatPercent(num)}%`;
- }
- /** 模板正文为「占检测机组的{anomaly_turbine_ratio}%」,这里只给百分数 */
- export function formatAnomalyTurbineRatio(anomalyTurbines, testedTurbines) {
- const total = Number(testedTurbines) || 0;
- const anomaly = Number(anomalyTurbines) || 0;
- if (total <= 0) return "0.00";
- return formatPercent(anomaly / total);
- }
- function pad2(n) {
- return String(n).padStart(2, "0");
- }
- function parseDateParts(raw) {
- const text = String(raw || "").trim();
- const match = text.match(/^(\d{4})[-/](\d{1,2})(?:[-/](\d{1,2}))?/);
- if (match) {
- return {
- year: match[1],
- month: pad2(match[2]),
- day: pad2(match[3] || "1"),
- };
- }
- const now = new Date();
- return {
- year: String(now.getFullYear()),
- month: pad2(now.getMonth() + 1),
- day: pad2(now.getDate()),
- };
- }
- /** PO 的 sensorAnomalyType 编码 / 各传感器 flag → 「数据感知异常类型」文案 */
- export function formatSensorTypeText(item = {}) {
- const fromFlags = MODEL_SENSOR_FIELDS.filter(
- (cfg) => Number(item[cfg.field]) > 0,
- ).map((cfg) => cfg.label);
- if (fromFlags.length) return fromFlags.join("、");
- const raw = item.sensorAnomalyType;
- if (raw == null || raw === "") return "暂无异常";
- const labels = String(raw)
- .split(/[,|、\s]+/)
- .map((code) => SENSOR_TYPE_LABELS[Number(code)] || "")
- .filter(Boolean);
- return labels.length ? labels.join("、") : "暂无异常";
- }
- function sensorAnomalyPoints(item = {}) {
- const count = Number(item.sensorAnomalyCount);
- if (Number.isFinite(count) && count > 0) return count;
- return MODEL_SENSOR_FIELDS.reduce(
- (sum, cfg) => sum + (Number(item[cfg.field]) > 0 ? 1 : 0),
- 0,
- );
- }
- export function pickPo(moduleBundle, poKey) {
- if (!poKey || !moduleBundle) return null;
- return (
- moduleBundle.wind?.[poKey] ||
- moduleBundle.yaw?.[poKey] ||
- moduleBundle.pitch?.[poKey] ||
- moduleBundle.run?.[poKey] ||
- moduleBundle.aero?.[poKey] ||
- null
- );
- }
- export function isPoAnomaly(po) {
- return Number(po?.detectorIsAnomaly) === 1;
- }
- export function poAnomalyPoints(po) {
- return Number(po?.detectorAnomalyCount) || 0;
- }
- export function poAnomalyRate(po) {
- const raw = Number(po?.detectorAnomalyRate);
- if (Number.isFinite(raw)) return raw;
- const anomaly = poAnomalyPoints(po);
- const normal = Number(po?.detectorNormallyCount) || 0;
- const total = anomaly + normal;
- return total ? anomaly / total : 0;
- }
- export function buildDetectorComment(po, templateKey) {
- if (!po) return "暂无数据";
- if (templateKey === "wind_power_curve") {
- const ratio = Number(po.detectorAnomalyRate);
- if (Number.isFinite(ratio) && ratio > 1.2) return "相对理论功率曲线超发";
- if (Number.isFinite(ratio) && ratio > 0 && ratio < 0.8) {
- return "相对理论功率曲线欠发";
- }
- }
- if (templateKey === "ctrl_deload") {
- return isPoAnomaly(po) ? "存在降载运行" : "未见明显降载";
- }
- return isPoAnomaly(po) ? "功能诊断异常" : "正常";
- }
- function isWatchlistUnit(item) {
- return WATCHLIST_RATIO_FIELDS.some((key) => {
- const num = Number(item?.[key]);
- return Number.isFinite(num) && num > 0.5;
- });
- }
- function detectorHeatmapRatioByTitle(title = "") {
- const text = String(title || "");
- const field = DETECTOR_HEATMAP_FIELDS.find((item) =>
- text.includes(item.label.replace(/异常$/, "")),
- );
- return field?.ratioFields || [];
- }
- export function summarizeTurbineAnomalyRate(detectorHits = [], modelItem = {}) {
- const hits = Array.isArray(detectorHits) ? detectorHits : [];
- if (!hits.length) return 0;
- const weighted = hits.reduce(
- (acc, hit) => {
- const points = Number(hit?.points) || 0;
- const heatmapFields = detectorHeatmapRatioByTitle(hit?.title);
- let rate = Number(hit?.rate) || 0;
- if (heatmapFields.length) {
- const raw = heatmapFields
- .map((field) => modelItem?.[field])
- .find((value) => value != null && value !== "");
- if (raw != null) {
- rate = toPercentNumber(raw) / 100;
- }
- }
- const weight = points > 0 ? points : 1;
- acc.weightedSum += rate * weight;
- acc.weight += weight;
- return acc;
- },
- { weightedSum: 0, weight: 0 },
- );
- return weighted.weight ? weighted.weightedSum / weighted.weight : 0;
- }
- function collectMachineTypes(modelList = []) {
- return [
- ...new Set(
- modelList
- .map(
- (item) =>
- item.machineTypeCode ||
- item.engineTypeName ||
- item.engineTypeCode ||
- item.modelName ||
- "",
- )
- .filter(Boolean),
- ),
- ];
- }
- function withWindFarmLabel(raw = "") {
- const text = String(raw || "").trim();
- if (!text) return "";
- if (/风电[场厂]$/u.test(text) || /风场$/u.test(text)) return text;
- return `${text}风电场`;
- }
- /** 合并省/市或公司归属,去掉完全重复或互相包含的片段 */
- function joinLocationParts(...parts) {
- const cleaned = [];
- parts.forEach((part) => {
- const text = String(part || "").trim();
- if (!text) return;
- const overlapIndex = cleaned.findIndex(
- (item) => item === text || item.includes(text) || text.includes(item),
- );
- if (overlapIndex >= 0) {
- if (text.length > cleaned[overlapIndex].length) {
- cleaned[overlapIndex] = text;
- }
- return;
- }
- cleaned.push(text);
- });
- return cleaned.join("");
- }
- function buildOverviewText(
- fieldMeta = {},
- fieldName,
- machineTypes,
- turbineCount,
- ) {
- const province = fieldMeta.provinceName || fieldMeta.province || "";
- const city = fieldMeta.cityName || fieldMeta.city || "";
- const location = joinLocationParts(province, city);
- const typeText = machineTypes.join("、") || "—";
- const farmLabel = withWindFarmLabel(fieldName);
- // 模板为「{Overview_of_the_Wind_Farm}。基于…」,此处不加句号,避免「。。」
- if (location) {
- return `${farmLabel}位于${location},机型${typeText},共安装${turbineCount}台风机`;
- }
- return `${farmLabel},机型${typeText},接入机组${turbineCount}台`;
- }
- function riskLevel(rate, turbines) {
- if (turbines >= 8 || rate >= 0.2) return "P1";
- if (turbines >= 3 || rate >= 0.05) return "P2";
- return "P3";
- }
- export function buildEmptyDetectorSection(cfg) {
- const farmTag = `zn-techcn-replace-tags-${cfg.templateKey}-farmSummary`;
- const fileTag = `zn-techcn-replace-tags-${cfg.templateKey}-generalFiles`;
- const rowsKey = `${cfg.templateKey}Rows`;
- return {
- [`show-${fileTag}`]: [],
- [farmTag]: [],
- [fileTag]: [],
- [rowsKey]: [],
- };
- }
- export function buildDetectorSectionPayload(
- cfg,
- {
- farmImages = [],
- turbineImages = [],
- rows = [],
- anomalyPoints = 0,
- anomalyRate = 0,
- anomalyTurbines = 0,
- testedTurbines = 0,
- hasChartData = false,
- },
- ) {
- const farmTag = `zn-techcn-replace-tags-${cfg.templateKey}-farmSummary`;
- const fileTag = `zn-techcn-replace-tags-${cfg.templateKey}-generalFiles`;
- const rowsKey = `${cfg.templateKey}Rows`;
- const tableRows =
- Array.isArray(rows) && rows.length ? rows : emptyDetectorRowsPlaceholder();
- const visible =
- hasChartData && (farmImages.length > 0 || turbineImages.length > 0);
- const section = {
- anomaly_points: String(anomalyPoints),
- anomaly_rate: `${formatPercent(anomalyRate)}%`,
- anomaly_turbines: String(anomalyTurbines),
- anomaly_turbine_ratio: formatAnomalyTurbineRatio(
- anomalyTurbines,
- testedTurbines,
- ),
- [farmTag]: farmImages,
- [fileTag]: turbineImages,
- [rowsKey]: tableRows,
- };
- return {
- [`show-${fileTag}`]: visible ? [section] : [],
- [farmTag]: visible ? farmImages : [],
- [fileTag]: visible ? turbineImages : [],
- [rowsKey]: visible ? tableRows : [],
- };
- }
- export function buildModuleVisibilityPayload(detectorState = {}) {
- const payload = {};
- const modules = new Set(
- DETECTOR_TEMPLATE_CONFIG.map((cfg) => cfg.module).filter(Boolean),
- );
- modules.forEach((moduleKey) => {
- const hasData = DETECTOR_TEMPLATE_CONFIG.some(
- (cfg) =>
- cfg.module === moduleKey && detectorState[cfg.templateKey]?.hasChartData,
- );
- payload[`show_module_${moduleKey}`] = hasData ? [{}] : [];
- });
- return payload;
- }
- /** 页眉模板:大唐{Province}能源有限公司{Wind_farm}风机异常检测数据分析报告 */
- const CHINA_PROVINCE_NAMES = [
- "新疆",
- "内蒙古",
- "西藏",
- "宁夏",
- "广西",
- "黑龙江",
- "吉林",
- "辽宁",
- "河北",
- "河南",
- "山东",
- "山西",
- "陕西",
- "甘肃",
- "青海",
- "江苏",
- "浙江",
- "安徽",
- "福建",
- "江西",
- "湖北",
- "湖南",
- "广东",
- "海南",
- "四川",
- "贵州",
- "云南",
- "北京",
- "天津",
- "上海",
- "重庆",
- ];
- function inferProvinceName(...texts) {
- const blob = texts.map((item) => String(item || "")).join(" ");
- return CHINA_PROVINCE_NAMES.find((name) => blob.includes(name)) || "";
- }
- function normalizeHeaderProvince(raw = "") {
- const inferred = inferProvinceName(raw);
- if (inferred) return inferred;
- let text = String(raw || "").trim();
- if (!text) return "";
- text = text.replace(/^大唐+/g, "").trim();
- text = text
- .replace(/能源?有限公司$/g, "")
- .replace(/有限责任公司$/g, "")
- .replace(/公司$/g, "")
- .trim();
- return CHINA_PROVINCE_NAMES.includes(text) ? text : "";
- }
- function isFarmDisplayName(raw = "") {
- const text = String(raw || "").trim();
- if (!text) return false;
- if (!/[\u4e00-\u9fff]/.test(text) && /^[\w-]+$/.test(text)) return false;
- return true;
- }
- function pickFarmDisplayName({
- fieldName,
- fieldMeta = {},
- overview = {},
- fieldCode,
- } = {}) {
- const candidates = [
- fieldName,
- fieldMeta.fieldName,
- fieldMeta.companyName,
- overview.fieldName,
- ]
- .map((item) => String(item || "").trim())
- .filter(isFarmDisplayName);
- if (candidates[0]) return candidates[0];
- return isFarmDisplayName(fieldCode) ? String(fieldCode).trim() : "";
- }
- function ensureWindFarmSuffix(raw = "") {
- let text = String(raw || "").trim();
- if (!text) return "";
- if (/风电场$/u.test(text)) return text;
- if (/风场$/u.test(text)) return text.replace(/风场$/u, "风电场");
- return `${text}风电场`;
- }
- function normalizeHeaderWindFarm(raw = "", provincePart = "") {
- let text = String(raw || "").trim();
- if (!text) return "";
- text = text.replace(/^大唐/g, "").trim();
- if (provincePart && text.startsWith(provincePart)) {
- text = text.slice(provincePart.length).trim();
- }
- text = text.replace(/^[\s·\-—]+/, "").trim();
- return ensureWindFarmSuffix(text || String(raw || "").trim());
- }
- function normalizeMachineTypeCode(raw) {
- if (Array.isArray(raw)) {
- return [
- ...new Set(raw.map((item) => String(item || "").trim()).filter(Boolean)),
- ].join("、");
- }
- return String(raw || "").trim();
- }
- export function mapAnomalyCoverFields({
- fieldCode,
- datatime,
- fieldName,
- fieldMeta = {},
- overview = {},
- modelList = [],
- machineTypeCode,
- }) {
- const parts = parseDateParts(datatime || overview.sourceDatetime);
- const turbines = sortTurbines(modelList);
- const machineTypes = collectMachineTypes(turbines);
- const typeText =
- normalizeMachineTypeCode(machineTypeCode) ||
- machineTypes.join("、") ||
- fieldMeta.machineTypeCode ||
- "—";
- const rawFarm = pickFarmDisplayName({
- fieldName,
- fieldMeta,
- overview,
- fieldCode,
- });
- const province = inferProvinceName(
- fieldMeta.provinceName,
- fieldMeta.province,
- fieldName,
- fieldMeta.fieldName,
- fieldMeta.companyName,
- rawFarm,
- ) || normalizeHeaderProvince(
- fieldMeta.provinceName || fieldMeta.province || "",
- );
- const farm = normalizeHeaderWindFarm(rawFarm, province);
- const turbineCount = turbines.length || Number(fieldMeta.engineCount) || 0;
- return {
- reportNo: `AD-${fieldCode || "FIELD"}-${parts.year}${parts.month}${
- parts.day
- }`,
- Province: province,
- Wind_farm: farm,
- Year_now: parts.year,
- Month_now: parts.month,
- machineTypeCode: typeText,
- Company: province ? `大唐${province}能源有限公司` : "大唐可再生能源试验研究院有限公司",
- turbine_count: String(turbineCount),
- target_date:
- datatime ||
- overview.sourceDatetime ||
- `${parts.year}-${parts.month}-${parts.day}`,
- Overview_of_the_Wind_Farm: String(
- buildOverviewText(
- fieldMeta,
- rawFarm,
- typeText === "—" ? [] : typeText.split("、"),
- turbineCount,
- ) || "",
- ).replace(/[。..]+$/u, ""),
- };
- }
- function isSensorTableRowActive(item = {}, cfg = {}) {
- if (Number(item[cfg.flagField]) > 0) return true;
- return readRatioPercent(item, cfg.ratioFields) > 0;
- }
- export function buildSensorAnomalyRows(modelList = []) {
- const rows = [];
- sortTurbines(modelList).forEach((item) => {
- SENSOR_TABLE_FIELDS.forEach((cfg) => {
- if (!isSensorTableRowActive(item, cfg)) return;
- const ratio = readRatioPercent(item, cfg.ratioFields);
- rows.push({
- turbine_name: displayTurbineName(item),
- sensor_anomaly_type: cfg.label,
- ratio: `${ratio.toFixed(2)}%`,
- });
- });
- });
- return rows.length ? rows : emptySensorAnomalyRowsPlaceholder();
- }
- export function emptySensorAnomalyRowsPlaceholder() {
- return [
- {
- turbine_name: "暂无",
- sensor_anomaly_type: "暂无",
- anomaly_points: "—",
- ratio: "—",
- },
- ];
- }
- export function buildDetectorSummaryRows(aggregates = {}) {
- return DETECTOR_TEMPLATE_CONFIG.filter((cfg) => cfg.poKey).map((cfg) => {
- const item = aggregates[cfg.templateKey] || {};
- return {
- detector_name: cfg.title,
- module_name: cfg.moduleName,
- data_granularity: cfg.granularity,
- anomaly_turbines: String(item.anomalyTurbines || 0),
- anomaly_points: String(item.anomalyPoints || 0),
- avg_anomaly_rate: `${formatPercent(item.avgRate || 0)}%`,
- };
- });
- }
- const DELOAD_MAIN_ANOMALY_TITLE = "降载判定分析";
- function isDeloadMainAnomalyType(title = "") {
- return String(title || "").includes(DELOAD_MAIN_ANOMALY_TITLE);
- }
- /** 表4-2:一行一个检测项。功能诊断项为检测器名称,数值为该项指标,数值说明为指标含义 */
- export function buildAnomalySummaryRows(turbineStats = []) {
- const rows = [];
- turbineStats.forEach((item) => {
- const rawHits = Array.isArray(item.detectorHits) ? item.detectorHits : [];
- const hits = rawHits
- .filter((hit) => !isDeloadMainAnomalyType(hit?.title))
- .sort((a, b) => (Number(b.points) || 0) - (Number(a.points) || 0));
- if (hits.length) {
- hits.forEach((hit) => rows.push(summaryRowFromHit(item.turbineName, hit)));
- return;
- }
- if (isDeloadMainAnomalyType(item.mainAnomalyType)) return;
- if (!(item.anomalyDetectorCount > 0 || item.anomalyPoints > 0)) return;
- rows.push(
- summaryRowFromHit(item.turbineName, {
- title: item.mainAnomalyType,
- rate: item.anomalyRate,
- points: item.anomalyPoints,
- }),
- );
- });
- return rows;
- }
- export function pickKeyTurbines(turbineStats = [], modelList = []) {
- const watchIds = new Set(
- modelList
- .filter(isWatchlistUnit)
- .map((item) => String(item.engineId || "")),
- );
- return turbineStats.filter((item) => watchIds.has(String(item.engineId)));
- }
- export function emptyDetectorRowsPlaceholder() {
- return [
- {
- turbine_name: "暂无",
- anomaly_points: "—",
- anomaly_rate: "—",
- sensor_anomaly_type: "—",
- comment: "暂无",
- },
- ];
- }
- export function emptyKeyTurbinePlaceholder() {
- return [
- {
- turbine_name: "暂无",
- turbineDetailRows: [
- {
- detector_name: "暂无",
- anomaly_points: "—",
- anomaly_rate: "—",
- conclusion: "暂无",
- },
- ],
- "zn-techcn-replace-tags-key_turbine-generalFiles": [],
- },
- ];
- }
- export function extractProblemDesc(raw = "") {
- const text = String(raw || "")
- .replace(/>/g, ">")
- .replace(/</g, "<")
- .replace(/\s+/g, " ")
- .trim();
- if (!text) return "";
- const start = text.indexOf("主要表现为");
- let body =
- start >= 0
- ? text.slice(start)
- : text.replace(/^本次检测中,[^;;]*[;;]/, "").trim();
- body = body
- .replace(
- /[,,;;、]?(?:机组明细|相关机组[^。]{0,24})见(?:表|图)[^。]*。?/g,
- "",
- )
- .replace(/[,,;;]\s*$/g, "")
- .trim();
- if (body && !body.startsWith("主要表现为")) {
- body = `主要表现为${body}`;
- }
- if (body && !/[。!?]$/.test(body)) body += "。";
- return body;
- }
- export function formatConclusionTurbineName(name = "") {
- const text = String(name || "").trim();
- if (!text) return "";
- const stripped = text.replace(/^#+/, "").replace(/号$/u, "").trim();
- if (/^\d+$/.test(stripped)) {
- return `#${stripped.padStart(2, "0")}`;
- }
- return text;
- }
- export function formatConclusionTurbines(names = []) {
- const unique = [
- ...new Set(
- (Array.isArray(names) ? names : [])
- .map((item) => formatConclusionTurbineName(item))
- .filter(Boolean),
- ),
- ];
- unique.sort((a, b) => a.localeCompare(b, "zh-CN", { numeric: true }));
- return unique.join("、") || "—";
- }
- export function buildConclusionRows(aggregates = {}) {
- const rows = DETECTOR_TEMPLATE_CONFIG.filter(
- (cfg) => cfg.poKey && !cfg.skipConclusion,
- )
- .map((cfg) => {
- const item = aggregates[cfg.templateKey] || {};
- if (!item.anomalyTurbines && !item.anomalyPoints) return null;
- return {
- problem_desc: extractProblemDesc(cfg.problemDesc || cfg.title),
- suggestion: String(cfg.suggestion || "").trim() || "—",
- problem_nature: cfg.moduleTitle || cfg.moduleName || "—",
- turbine_names: formatConclusionTurbines(item.turbineNames),
- };
- })
- .filter(Boolean)
- .map((row, index) => ({ ...row, index: String(index + 1) }));
- if (rows.length) return rows;
- return [
- {
- index: "1",
- problem_desc: "本次检测未发现功能诊断异常。",
- suggestion: "—",
- problem_nature: "—",
- turbine_names: "—",
- },
- ];
- }
- export function summarizeFarmStats({
- overview = {},
- modelList = [],
- turbineStats = [],
- aggregates = {},
- }) {
- const turbineCount = modelList.length;
- const anomalyTurbines = turbineStats.filter(
- (item) => item.anomalyDetectorCount > 0,
- ).length;
- const totalPoints = turbineStats.reduce(
- (sum, item) => sum + (item.anomalyPoints || 0),
- 0,
- );
- const overviewPoints =
- Number(overview.detectorAnomalyCount) ||
- Number(overview.totalAnomalyCount) ||
- totalPoints;
- const rate = turbineCount ? anomalyTurbines / turbineCount : 0;
- const moduleKeys = new Set(
- DETECTOR_TEMPLATE_CONFIG.filter(
- (cfg) => (aggregates[cfg.templateKey]?.anomalyTurbines || 0) > 0,
- ).map((cfg) => cfg.moduleName),
- );
- const topDetector = Object.entries(aggregates)
- .map(([key, value]) => ({
- key,
- ...value,
- title: DETECTOR_TEMPLATE_CONFIG.find((cfg) => cfg.templateKey === key)
- ?.title,
- }))
- .sort((a, b) => (b.anomalyPoints || 0) - (a.anomalyPoints || 0))[0];
- return {
- turbine_count: String(turbineCount),
- anomaly_turbine_count: String(
- Number(overview.anomalyCount) || anomalyTurbines,
- ),
- total_anomaly_points: String(overviewPoints || totalPoints),
- anomaly_rate: formatPercent(rate),
- anomaly_module_count: String(moduleKeys.size),
- main_problem_modules: [...moduleKeys].join("、") || "暂无突出模块",
- top_problem_description: topDetector?.title
- ? `${topDetector.title}异常点数最多(${topDetector.anomalyPoints || 0})`
- : "各检测器未见集中异常",
- };
- }
|