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})` : "各检测器未见集中异常", }; }