|
|
@@ -0,0 +1,71 @@
|
|
|
+/**
|
|
|
+ * ★P99 汇报定制的**章节正文渲染器** —— 从 v2.9.2 `app.js:531-571` 原文移植(`rptFacts`/`rptK`/`rptBlock`)。
|
|
|
+ * 仅机械适配:`CH.x(` → `chart('x', `;`D.` → `d.`;其余(FACTS/RPT/L/SYS_KEY/sysName/stateChip/unitNo/STEP/
|
|
|
+ * winName/displayText/fmt/dv/esc/chart)由调用方注入,保证与旧页逐字同源。
|
|
|
+ */
|
|
|
+import type { chart as ChartFn } from './legacyCharts'
|
|
|
+
|
|
|
+export interface RptEnv {
|
|
|
+ t: (k: string, o?: Record<string, unknown>) => string
|
|
|
+ fmt: (v: unknown, n?: number) => string
|
|
|
+ esc: (v: unknown) => string
|
|
|
+ dv: (v: unknown) => string
|
|
|
+ displayText: (v: unknown) => string
|
|
|
+ chart: typeof ChartFn
|
|
|
+ SYS_KEY: Record<string, string>
|
|
|
+ sysName: (k: string) => string
|
|
|
+ stateChip: (s: unknown) => string
|
|
|
+ unitNo: (t: string) => string
|
|
|
+ STEP: Record<string, string>
|
|
|
+ winName: (w: string) => string
|
|
|
+ L: Record<string, string>
|
|
|
+ RPT: { narr?: { text?: string; ungrounded?: string[] } | null }
|
|
|
+ FACTS: { cards?: Array<Record<string, unknown>>; contract_sha256?: string } | null | false
|
|
|
+}
|
|
|
+
|
|
|
+export function makeReportBlocks(d: any, env: RptEnv): { rptBlock: (k: string, d: any) => string; rptFacts: () => string } {
|
|
|
+ const { t, fmt, esc, dv, displayText, chart, SYS_KEY, sysName, stateChip, unitNo, STEP, winName, L, RPT, FACTS } = env
|
|
|
+ const BD = (d && d.fus && d.fus.链盘) || {} // 旧页 app.js:541 的 BD = d.fus.链盘
|
|
|
+ function rptFacts() { if (!FACTS || !FACTS.cards) return ''; const cards = FACTS.cards.filter(c => c.verdict === '定论' || c.verdict === '准定论·预警'); const rows = cards.map(c => `<tr><td><code>${esc(c.claim_id)}</code></td><td>${esc(c.verdict)}</td><td>${esc(c.title)}</td><td>${esc((c.systems || []).join(', ') || '—')}</td><td>${esc(c.aggregation || '—')}</td></tr>`).join('');
|
|
|
+ return `<div class="rpt-blk" data-contract="${esc(FACTS.contract_sha256 || '')}"><div class="blk-ti">${t('rpt.facts.title')} <span class="small muted">${t('rpt.facts.sub', { n: cards.length, sha: (FACTS.contract_sha256 || '').slice(0, 16) })}</span></div><div style="overflow:auto"><table class="tbl"><tr><th>claim</th><th>${t('rpt.facts.verdict')}</th><th>${t('rpt.facts.text')}</th><th>${t('rpt.facts.sys')}</th><th>${t('rpt.facts.agg')}</th></tr>${rows}</table></div></div>`; }
|
|
|
+ // 判别值 (API 按 _KEEP_ZH 原样保留中文, 前端拿来做判别) —— 显示时查语言包。
|
|
|
+ // 查不到才回落原文标注: 振动线的技术速记 (「2R倒谱自基线红警」) 不可枚举也不该猜译。
|
|
|
+ const enumWord = (p) => { const k = String(p || ''); for (const pre of ['attr.', 'vp.']) if (L[pre + k] != null) return t(pre + k); return null; };
|
|
|
+ const partName = (p) => SYS_KEY[p] ? sysName(p) : (enumWord(p) ? esc(enumWord(p)) : dv(p));
|
|
|
+ const partPlain = (p) => SYS_KEY[p] ? sysName(p) : (enumWord(p) || displayText(p));
|
|
|
+
|
|
|
+ function rptK(v, u, lab, note, cls) { return `<div class="kpi ${cls || ''}"><b>${v}<small>${u}</small></b><span>${lab}<br><span style="opacity:.8">${note || ''}</span></span></div>`; }
|
|
|
+ function rptBlock(k, d) {
|
|
|
+ const E = (d.control && d.control.energy) || {}, em = E.monthly || [], R = (d.rel && !d.rel.err) ? d.rel : null, M = (d.m8 && !d.m8.err) ? d.m8.mtbf : null, F = d.faults || {}, FU = d.fus || {}, BD = FU.链盘 || {};
|
|
|
+ const wrap = (ti, b, src) => `<div class="panel mb"><h3>${ti}</h3>${b}${src ? `<p class="small muted" style="margin-top:8px;padding-top:7px;border-top:1px solid var(--line)">${t('rpt.caliber_lbl')}${src}</p>` : ''}</div>`;
|
|
|
+ if (k === 'kpi') { const last = em[em.length - 1] || {}, prev = em[em.length - 2] || {}, ok = (last.cov || 0) >= .5 && (prev.cov || 0) >= .5, dl = (a, b) => b ? ((a - b) / b * 100) : 0, mom = dl(last.act, prev.act);
|
|
|
+ return `<div class="kpis" style="margin-bottom:14px">${rptK(fmt(E.act), 'MWh', t('rpt.kpi.act'), t('rpt.kpi.theo', { v: fmt(E.theo) }) + ' | ' + (ok ? t('rpt.kpi.mom', { v: (mom >= 0 ? '+' : '') + mom.toFixed(0) }) : t('rpt.kpi.mom_na')))}
|
|
|
+ ${rptK(fmt(E.eflh), 'h', t('rpt.kpi.eflh'), t('rpt.kpi.annual', { v: fmt(Math.round((E.eflh || 0) / Math.max(em.filter(x => x.cov >= .5).length, 1) * 12)) }))}
|
|
|
+ ${rptK(fmt(d.control && d.control.avail, 1), '%', t('rpt.kpi.avail'), t('rpt.kpi.avail_n'), (d.control && d.control.avail >= 95) ? 'ok' : 'warn')}
|
|
|
+ ${rptK(fmt(E.loss_pct, 1), '%', t('rpt.kpi.loss'), fmt(E.loss) + ' MWh', E.loss_pct >= 15 ? 'warn' : '')}
|
|
|
+ ${rptK(M ? fmt(M.MTBF_h) : '—', 'h', t('rpt.kpi.mtbo'), t('rpt.kpi.mtbo_n', { n: M ? fmt(M.停机事件数) : '—', h: M ? M.MDT_h : '—' }))}
|
|
|
+ ${rptK((BD.rows || []).length, t('unit.units'), t('rpt.kpi.action'), t('rpt.kpi.action_n', { a: (FU.kpi || {}).定论 || 0, b: (FU.kpi || {}).预警 || 0, c: (FU.kpi || {}).候选观察 || 0 }), (BD.rows || []).length > 20 ? 'warn' : '')}</div>`; }
|
|
|
+ if (k === 'water') return wrap(t('rpt.water.title'), chart('waterfall', E) + `<p class="small muted" style="margin-top:6px">${t('rpt.water.note', { theo: fmt(E.theo), act: fmt(E.act), loss: fmt(E.loss), pct: E.loss_pct })} | ${E.cap_ok ? t('rpt.water.cap_ok', { cap: fmt(E.cap) }) : t('rpt.water.cap_bad', { cap: fmt(E.cap) })}</p>`, t('rpt.water.cal'));
|
|
|
+ if (k === 'emon') return wrap(t('rpt.emon.title'), em.length ? `<div class="tblwrap"><table><tr><th>${t('rpt.emon.m')}</th><th class="r">${t('rpt.emon.act')}</th><th class="r">${t('rpt.emon.loss')}</th><th class="r">${t('rpt.emon.pct')}</th><th class="r">${t('rpt.emon.eflh')}</th><th>${t('rpt.emon.cov')}</th></tr>${em.map(x => `<tr><td>${x.m}</td><td class="r">${fmt(x.act)}</td><td class="r">${fmt(x.loss)}</td><td class="r">${x.loss_pct}%</td><td class="r">${x.eflh}</td><td class="small" style="color:${x.cov < .5 ? 'var(--warning)' : 'var(--muted)'}">${(x.cov * 100).toFixed(0)}%${x.cov < .5 ? t('rpt.emon.partial') : ''}</td></tr>`).join('')}</table></div>` : '—', t('rpt.emon.cal'));
|
|
|
+ if (k === 'blame') return wrap(t('rpt.blame.title'), chart('blameBar', E) + `<p class="small muted" style="margin-top:8px">${t('rpt.blame.note')}<br><b>${t('rpt.blame.note2')}</b></p>`, t('rpt.blame.cal'));
|
|
|
+ if (k === 'ram') return R ? wrap(t('rpt.ram.title'), chart('ramChart', R.表, partPlain) + `<p class="small muted" style="margin-top:6px">${t('rpt.ram.note', { a: R.设备类停机h, b: R.外部类停机h })}</p>`, t('rpt.ram.cal')) : '';
|
|
|
+ if (k === 'parts') return R ? wrap(t('rpt.parts.title'), `<div class="tblwrap"><table><tr><th>${t('rpt.parts.part')}</th><th class="r">${t('rpt.parts.h')}</th><th class="r">${t('rpt.parts.ev')}</th><th class="r">${t('rpt.parts.mtbf')}</th><th class="r">${t('rpt.parts.mdt')}</th><th class="r">${t('rpt.parts.units')}</th></tr>${R.表.slice(0, 9).map(x => `<tr><td class="small">${partName(x.部件)}${x.外部 ? t('rpt.parts.ext') : ''}</td><td class="r">${x.停机时长h}</td><td class="r">${x.事件数}</td><td class="r">${x.MTBF_h || '—'}</td><td class="r">${x.MDT_h}</td><td class="r">${x.影响台数}</td></tr>`).join('')}</table></div>`, t('rpt.parts.cal')) : '';
|
|
|
+ if (k === 'attrib') return R ? wrap(t('rpt.attrib.title'), chart('attribBar', R.表, R.总停机h) + `<p class="small muted" style="margin-top:6px">${t('rpt.attrib.note')}</p>`, '') : '';
|
|
|
+ if (k === 'pareto') return wrap(t('rpt.pareto.title'), chart('paretoChart', F.pareto_n || [], t('unit.n'), ''), t('rpt.pareto.cal'));
|
|
|
+ if (k === 'quad') return wrap(t('rpt.quad.title'), chart('quadChart', F.quad || []) + `<p class="small muted" style="margin-top:6px">${t('rpt.quad.note')}</p>`, t('rpt.quad.cal'));
|
|
|
+ if (k === 'frate') return wrap(t('rpt.frate.title'), chart('rateChart', F.monthly || { months: [], vals: [] }) + `<p class="small muted" style="margin-top:6px">${t('rpt.frate.note')}</p>`, t('rpt.frate.cal'));
|
|
|
+ if (k === 'units') { const rows = (BD.rows || []).slice(0, 14), stuck = Object.entries(BD.stuck || {}).map(([a, b]) => t('rpt.units.stuck', { n: b, s: t('step.' + (STEP[a] || 'evidence')) })).join(' · ');
|
|
|
+ return wrap(t('rpt.units.title'), `<p class="small muted" style="margin-bottom:8px">${t('rpt.units.head', { n: (BD.rows || []).length, tot: (d.kpi || {}).全场 || 38 })}${stuck}</p><div class="tblwrap"><table><tr><th style="width:52px">${t('ui.unit')}</th><th style="width:80px">${t('rpt.units.state')}</th><th>${t('rpt.units.ev')}</th><th style="width:56px">${t('rpt.units.prog')}</th><th style="width:90px">${t('rpt.units.at')}</th></tr>
|
|
|
+ ${rows.map(r => `<tr><td><b>${unitNo(r.t)}</b></td><td>${stateChip(r.ostate)}</td><td class="small muted">${r.part && r.part.indexOf('未收录') < 0 ? `<b style="color:var(--ink)">${dv(r.part)}</b> · ` : ''}${(r.srcs || []).map(dv).join('/') || '—'}${r.rising ? ` · <span style="color:var(--critical)">${t('rpt.units.rising', { r: r.rising.ratio })}</span>` : ''}</td><td class="r small">${r.done}/6</td><td class="small" style="color:var(--warning)">${r.stuck ? t('step.' + (STEP[r.stuck] || 'evidence')) : t('rpt.units.clear')}</td></tr>`).join('')}</table></div>`, t('rpt.units.cal')); }
|
|
|
+ if (k === 'chain') return wrap(t('rpt.chain.title'), `<p class="small muted">${t('rpt.chain.note', { s: Object.entries(BD.stuck || {}).map(([a, b]) => t('rpt.units.stuck', { n: b, s: t('step.' + (STEP[a] || 'evidence')) })).join(', ') })}</p>`, t('rpt.chain.cal', { b: dv(BD.wo_bound || '') }));
|
|
|
+ if (k === 'loop') { const cl = BD.closed || [];
|
|
|
+ return wrap(t('rpt.loop.title'), `<div class="tblwrap"><table><tr><th>${t('ui.unit')}</th><th>${t('rpt.loop.comp')}</th><th>${t('rpt.loop.date')}</th><th class="r">${t('rpt.loop.pre')}</th><th class="r">${t('rpt.loop.post')}</th><th class="r">${t('rpt.loop.ratio')}</th></tr>${cl.map(x => `<tr><td><b>${unitNo(x.turbine)}</b></td><td class="small">${dv(x.component)}</td><td class="small">${esc(x.replace_date)}</td><td class="r">${x.pre}</td><td class="r">${x.post}</td><td class="r" style="color:var(--good)"><b>${x.ratio}×</b></td></tr>`).join('')}</table></div><p class="small muted" style="margin-top:6px">${t('rpt.loop.note')}</p>`, t('rpt.loop.cal')); }
|
|
|
+ if (k === 'plan') return wrap(t('rpt.plan.title'), `<ol style="margin:0 0 0 20px;line-height:1.85"><li>${t('rpt.plan.1', { n: (BD.stuck || {}).机制 || 0 })}</li><li>${t('rpt.plan.2')}</li><li>${t('rpt.plan.3')}</li><li>${t('rpt.plan.4')}</li></ol>`, t('rpt.plan.cal'));
|
|
|
+ if (k === 'narr') { const N = RPT.narr; if (!N || !N.text) return wrap(t('rpt.narr.title'), `<p class="small muted">${t('rpt.narr.empty')}</p>`, '');
|
|
|
+ return wrap(t('rpt.narr.title'), `<p style="line-height:1.9;margin:0">${esc(N.text).split('\n').join('<br>')}</p>${(N.ungrounded || []).length ? `<p class="small" style="color:var(--warning);margin-top:8px">${t('rpt.narr.ungr', { v: N.ungrounded.map(esc).join(' / ') })}</p>` : ''}`, t('rpt.narr.cal', { m: esc(N.model), g: (N.ungrounded || []).length ? t('rpt.narr.g_n', { n: N.ungrounded.length }) : t('rpt.narr.g_ok') })); }
|
|
|
+ if (k === 'caliber') { const part = F.partial || [];
|
|
|
+ return wrap(t('rpt.cal.title'), `<ul style="margin:0 0 0 20px;line-height:1.85"><li><b>${t('rpt.cal.period')}</b> ${winName(d.win)}${part.length ? t('rpt.cal.partial', { m: part.join('/'), c: Math.round((F.cov_min || 0) * 100) }) : ''}</li><li><b>${t('rpt.cal.denom')}</b> ${t('rpt.cal.denom_d')}</li><li><b>${t('rpt.cal.stop')}</b> ${t('rpt.cal.stop_d')}</li><li><b>${t('rpt.cal.wo')}</b> ${t('rpt.cal.wo_d')}</li><li><b>${t('rpt.cal.rating')}</b> ${t('rpt.cal.rating_d')}</li>${RPT.aud === 'up' ? `<li><b>${t('rpt.cal.deid')}</b> ${t('rpt.cal.deid_d')}</li>` : ''}</ul>`, ''); }
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ return { rptBlock, rptFacts }
|
|
|
+}
|