| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629 |
- <!doctype html><html lang="zh" data-theme="light"><head><meta charset="utf-8"><script>(function(){try{var q=new URLSearchParams(location.search).get("theme");if(q){localStorage.setItem("windscada_theme",q)}var t=q||"light";document.documentElement.setAttribute("data-theme",t)}catch(e){}})();</script><meta name="viewport" content="width=device-width,initial-scale=1">
- <title>观澜 · 风电智能分析决策</title><style>%CSS%</style></head><body>
- <header><h1><s>观澜</s> 如东海上</h1><span id="winbox"></span>
- </header>
- <nav class="tabs" id="tabs"></nav>
- <div id="root"><div class="card"><div class="skel" style="width:38%"></div><div class="skel" style="width:72%;margin-top:9px"></div><div class="skel" style="width:56%;margin-top:9px"></div></div></div>
- <footer id="foot"></footer>
- <script>%JS%
- async function reload(win){
- const r=await fetch('/api/fleet?win='+encodeURIComponent(win)); const d=await r.json();
- if(d.all_months) window.ALLMONTHS=d.all_months;
- document.getElementById('winbox').innerHTML=winSel(d.win);
- D=d; renderTabs(); render();
- syncOverviewWin();
- }
- function syncOverviewWin(){
- // 总览静态页窗自适应 (2026-08-28 用户令): 把所选窗月列表推给 iframe, 页内重算 KPI/五态/限电/损失账
- const f=document.getElementById('ovframe');
- if(f&&D&&D.months) try{ f.contentWindow.postMessage({months:D.months, label:D.win}, '*') }catch(e){}
- }
- const TABS=['总览','电量算账','部件问题','振动融合分析','发电性能','故障统计','检修决策','检修助手','汇报定制','系统维护'] // 2026-08-28 用户指定顺序与命名; 分组标签撤 (10 个平铺, 分组反而多一层视觉层级); // 问题总览撤: 系统入口进总览, 点系统看详情 // 本体不设独立tab (用户裁: 检修决策页=ontology展现的窗口)
- let CURVES=null;
- let D=null, TAB='总览';
- const TABGROUPS=null; // 分组已撤 (用户裁 2026-08-28)
- function renderTabs(){
- // 平铺 (2026-08-28 用户裁): 10 个标签直接排, 撤掉"状态/分析/决策/维护"分组标签 —
- // 分组标签本身占一个视觉层级, 而 10 个中文标签本来就读得完, 分组是多余的一跳。
- document.getElementById('tabs').innerHTML=TABS.map(t=>
- `<button class="${t===TAB?'on':''}" onclick="TAB='${t}';renderTabs();render()">${t}</button>`).join('');
- }
- function render(){
- const d=D; const k=d.kpi, m=(d.m8&&!d.m8.err)?d.m8.mtbf:null;
- let h='';
- // 九系统入口 + 系统详情 (2026-08-28 用户令: 取消问题总览, 每个系统点开看详情)
- // 系统详情: 入口卡已随'两页顶端互换'移到可靠性概览, 条件不能再钉死总览 (2026-08-28 用户逮'点卡没反应')
- if(window.SYSV&&(TAB==='部件问题'||TAB==='总览')){
- const sname=window.SYSV, rows=(d.systems||{})[sname]||[], dist=(d.sysdist||{})[sname]||{};
- const cnt=st=>dist[st]||0;
- const al=rows.filter(r=>r.st==='报警'), un=rows.filter(r=>r.st==='不可判'), gd=(d.systems||{})[sname+'·良好']||[];
- const N=Object.values(dist).reduce((x,y)=>x+y,0)||1;
- h+=`<div class="card mb"><p class="small"><a class="tl" href="#" onclick="window.SYSV=null;render();return false">← 返回${TAB}</a></p>
- <h3>${sname} · 全场 ${N} 台</h3>
- <div style="display:flex;height:14px;border-radius:4px;overflow:hidden;margin:8px 0 4px">
- ${[['报警','var(--bad)'],['不可判','var(--unknown)'],['良好','var(--info)'],['优秀','var(--ok)']]
- .map(([k,c])=>{const n=cnt(k); return n?`<i title="${k} ${n}" style="width:${(n/N*100).toFixed(1)}%;background:${c}"></i>`:''}).join('')}
- </div>
- <div class="small" style="color:var(--muted)">报警 ${cnt('报警')} · 不可判 ${cnt('不可判')} · 良好 ${cnt('良好')} · 优秀 ${cnt('优秀')}</div></div>`;
- // 部件解剖图: 该系统内部怎么构成、介质/动力怎么流 (与传动链简图分工: 链=哪一环, 解剖=环内怎么坏的)
- if(ANAT[sname]){
- const _hit = al.map(r=>r.why).join(' ');
- h+=`<div class="card mb"><h3>${sname} 部件构成与失效路径</h3>${ANAT[sname](_hit, {})}</div>`;
- }
- const tbl=(title,list,col)=>list.length?`<div class="card mb"><h3>${title} (${list.length} 台)</h3>
- <table><tr><th style="width:74px">机组</th><th>依据</th><th style="width:96px">详情</th></tr>
- ${list.map(r=>`<tr><td><a class="tl" href="/turbine/${r.t}"><b>${r.t.replace('WTG','')}#</b></a></td>
- <td class="small">${r.why}</td>
- <td><a class="tl small" href="/problem/${r.t}/${encodeURIComponent(sname)}">该问题页 →</a></td></tr>`).join('')}</table></div>`:'';
- h+=tbl('报警机组',al)+tbl('不可判 (监测/数据不足, ≠正常)',un);
- if(gd.length) h+=`<details class="card mb"><summary style="cursor:pointer;font-weight:600;font-size:14px">良好 ${gd.length} 台 (轻微偏离, 周期观察 — 点开看逐台依据)</summary>
- <table style="margin-top:10px"><tr><th style="width:74px">机组</th><th>依据</th><th style="width:96px">详情</th></tr>
- ${gd.map(r=>`<tr><td><a class="tl" href="/turbine/${r.t}"><b>${r.t.replace('WTG','')}#</b></a></td><td class="small">${r.why}</td>
- <td><a class="tl small" href="/problem/${r.t}/${encodeURIComponent(sname)}">该问题页 →</a></td></tr>`).join('')}</table></details>`;
- if(!al.length&&!un.length&&!gd.length) h+=`<div class="card"><p class="small">本系统全场无非常态。</p></div>`;
- document.getElementById('root').innerHTML=h; document.getElementById('foot').innerHTML=d.note; return;
- }
- if(TAB==='总览'){
- // 总览顶部 = 可用率与损失总结 (2026-08-28 用户令: 与可靠性页最上端互换)
- const LS={}; ((d.control&&d.control.states)||[]).forEach(x=>{LS[x.k]=x.loss});
- const M8=(d.m8&&d.m8.mtbf)||null;
- h+=`<div class="kpis">
- <div class="kpi ${d.control&&d.control.avail>=95?'ok':'warn'}"><b>${d.control?d.control.avail:'—'}%</b><span>时间可用率(不含调度令) · ${d.win}</span></div>`;
- if(M8) h+=`<div class="kpi"><b>${M8.停机总时长h}h</b><span>停机时长 | ${M8.停机事件数} 起 (${d.win})</span></div>
- <div class="kpi"><b>${M8.MTBF_h}h</b><span>平均停机间隔(MTBO)| 剔除口径 ${M8.剔除口径.MTBF_h}h(非行业 MTBF)</span></div>`;
- if(LS['停机']!=null) h+=`<div class="kpi"><b>${Math.round(LS['停机']).toLocaleString()}</b><span>停机损失 MWh${LS['停机(调度令)']!=null?` (另调度令 ${Math.round(LS['停机(调度令)']).toLocaleString()})`:''}</span></div>`;
- if(LS['限电·绑定']!=null) h+=`<div class="kpi"><b>${Math.round(LS['限电·绑定']).toLocaleString()}</b><span>限电·绑定损失 MWh</span></div>`;
- h+=`</div>`;
- // 快照模式下 /static/index.html 取不到 (它是服务端另一套页面), 会渲染出静态服务的 404 页;
- // 快照里改为提示条, 并指向快照内已有的各分析页。
- const _snap = (typeof window.__SNAP__!=='undefined');
- // ★快照版的总览不能是一句"内嵌页不可用" (2026-09-01 用户: "over view 也打不开")。
- // 它是访客落地的**第一屏**, 而原来那句等于开门就说"这里没东西"。
- // 数据本来就在快照里 —— 逐系统机组、kpi、可靠性统计 —— 直接在这里汇总,
- // 并给一张"哪一屏回答什么"的导览。服务端版仍走 iframe, 不受影响。
- document.getElementById('root').innerHTML = h + (_snap
- ? (farmScene(d) + snapOverview(d))
- : `<iframe id="ovframe" src="/static/index.html" title="总览" onload="syncOverviewWin()"></iframe>`);
- document.getElementById('foot').innerHTML=d.note; return;
- }
- if(TAB==='发电性能'){ renderCurves(); return; }
- if(TAB==='本体'){ renderOntology(); return; }
- if(TAB==='振动融合分析'&&d.fus&&!d.fus.err&&d.fus.kpi){
- const K=d.fus.kpi; // 振动域分布卡·按钮化 (用户令: 点卡进入看该级具体台; FUSF=当前筛选级)
- const btn=(f,cls,val,label)=>{const on=window.FUSF===f;
- return `<div class="kpi ${cls}" role="button" tabindex="0" onclick="window.FUSF=window.FUSF==='${f}'?null:'${f}';window.FUSSUB='逐台情况';render()" style="cursor:pointer${on?';outline:2px solid var(--accent)':''}"><b>${val}</b><span>${label}${on?' · <b>筛选中,再点清除</b>':' · 点击查看'}</span></div>`};
- h+=`<div class="kpis">`;
- h+=btn('bad', K.定论>0?'bad':'ok', K.定论, `定论/报警级 (机制链定案 ${K.机制链} 台)`);
- h+=btn('warn', K.预警>0?'warn':'ok', K.预警, '预警级机组');
- h+=btn('note', '', K.候选观察, '候选/监视/观察级');
- h+=btn('ok', 'ok', K.销案正常, '销案/无新证');
- h+=btn('unlisted', '', `${K.收录台}<span style="font-size:13px;color:var(--muted)">/${K.全场}</span>`, `振动线收录台 (未列 ${K.未列} 台 ≠ 正常)`);
- // (可检出系数KPI卡撤 — 2026-08-28 用户令: 能力边界取消, 模型计算隐藏)
- h+=`</div>`;
- // 数据时点 (用户令"以最新导入的报告日期为准, 并说明时间"): 判级转录自振动线, 有自己的出件日期
- // 与证据窗末, 与本页所选时间窗无关 — 不标时点会让人以为它跟着窗走
- if(d.fus.数据时点) h+=`<p class="small" style="color:var(--muted);margin:-4px 0 12px">
- 判级数据: ${d.fus.数据时点} — 转录自振动线, <b>不随上方时间窗变</b>; 报警/工单/温度列才跟随所选窗。</p>`;
- } else {
- // (KPI 组已移至总览顶部 — 2026-08-28 用户令: 两页最上端互换)
- }
- if(TAB==='部件问题'){
- // 九系统入口 (点开看该系统全场详情) — 替代原"问题总览"tab
- const SD=d.sysdist||{};
- h+=`<div class="eyebrow">按系统查看</div><div class="card mb"><div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px">`
- +Object.entries(SD).map(([s2,dist])=>{
- const a=dist['报警']||0, u=dist['不可判']||0, N=Object.values(dist).reduce((x,y)=>x+y,0)||1;
- const col=a>=8?'var(--bad)':a>=3?'var(--warn)':a?'var(--info)':'var(--ok)';
- return `<div role="button" tabindex="0" onclick="window.SYSV='${s2}';render()" style="cursor:pointer;border:1px solid var(--line);border-left:3px solid ${col};border-radius:9px;padding:10px 12px"
- onmouseover="this.style.borderColor='var(--accent)';this.style.borderLeftColor='${col}'" onmouseout="this.style.borderColor='var(--line)';this.style.borderLeftColor='${col}'">
- <div style="font-weight:600;font-size:13px">${s2}</div>
- <div style="display:flex;height:7px;border-radius:3px;overflow:hidden;margin:6px 0 5px">
- ${[['报警','var(--bad)'],['不可判','var(--unknown)'],['良好','var(--info)'],['优秀','var(--ok)']]
- .map(([k,c])=>{const n=dist[k]||0; return n?`<i title="${k} ${n}" style="width:${(n/N*100).toFixed(1)}%;background:${c}"></i>`:''}).join('')}
- </div>
- <div class="small" style="color:var(--muted)">报警 <b style="color:${col}">${a}</b>${u?` · 不可判 ${u}`:''} · 全场 ${N} 台</div></div>`}).join('')
- +`</div><p class="small" style="margin-top:8px;color:var(--muted)">点系统卡 → 该系统全场机组与依据; 九系统 = 交付九分册一一对应; 叶片与叶根/塔架与基础无 SCADA 轴 (分册蓝本取 CMS/巡检)</p></div>`;
- if(d.rel&&d.rel.err) h+=blockErr('rel', d.rel.err);
- if(d.rel&&!d.rel.err){
- const R=d.rel, tb=R.表, maxH=Math.max(...tb.map(r=>r.停机时长h));
- h+=`<div class="eyebrow">部件可靠性 · ${R.窗}</div>`;
- h+=`<div class="card mb"><h3>部件停机贡献与可靠性 <span class="small" style="font-weight:400;color:var(--muted)">总停机 ${R.总停机h}h = 设备类 ${R.设备类停机h} + 外部类 ${R.外部类停机h}</span></h3>
- <p class="small" style="color:var(--muted);margin:-2px 0 8px">口径 ≥1h 停机段, 不含调度令 —
- 与"电量算账"里的停机时长${(function(){var v=d.control.energy&&d.control.energy.items.filter(function(x){return x.k==='停机'}).map(function(x){return x.h})[0];return (v===undefined||v===null)?'':' '+v+'h'})()} 不同,
- 差额是 <b>短于 1h 的停机段</b> (本页按段做可靠性指标须成段, 能量账按逐条工况态算电量): 两个数都对, 不可互相相减。</p>
- <table><tr><th>部件</th><th class="barcell">停机时长</th><th class="num">事件</th><th class="num">平均停机间隔(MTBO,h)</th><th class="num">单次停机时长(MDT,h)</th><th class="num">影响机组</th><th>典型失效模式 (按时长)</th></tr>`;
- tb.forEach(r=>{
- const w=Math.round(r.停机时长h/maxH*72);
- const sev=r.部件==='无记录停机'?'var(--unknown)':r.外部?'var(--info)':(r.占停机>=0.06?'var(--bad)':r.占停机>=0.03?'var(--warn)':'var(--ok)');
- h+=`<tr><td><b>${r.部件}</b>${r.外部?' <span class="badge">外部</span>':''}</td>
- <td class="barcell"><i class="bar" style="width:${w}px;--c:${sev}"></i><span class="cnt">${r.停机时长h}h · ${(r.占停机*100).toFixed(1)}%</span></td>
- <td class="num">${r.事件数}</td><td class="num">${r.外部?'—':r.MTBF_h}</td><td class="num">${r.MDT_h}</td>
- <td class="num">${r.影响台数}</td><td class="small">${r.典型失效模式||'—'}</td></tr>`});
- h+=`</table><p class="small">${R.口径}</p></div>`;
- }
- if(d.fus&&d.fus.err) h+=blockErr('fus', d.fus.err);
- if(d.fus&&!d.fus.err){
- h+=`<div class="card mb"><p class="small">振动线结论与四源融合已移至独立板块 → <a class="tl" href="#" onclick="TAB='振动融合分析';render();return false"><b>振动融合</b></a> (${d.fus.表.length} 台次收录)</p></div>`;
- }
- h+=`<div class="eyebrow">多系统交叉机组</div>`;
- h+=`<div class="card mb"><h3>关注机组 (≥2 系统交叉, ${d.watch.length} 台; 点机组号 → 该台独立页)</h3>
- <table><tr><th>机组</th><th>系统数</th><th>命中系统</th><th>依据</th></tr>`;
- d.watch.forEach(w=>{h+=`<tr><td><a class="tl" href="/turbine/${w.t}"><b>${w.t.replace('WTG','')}#</b></a></td><td class="num">${w.n}</td>
- <td class="small">${w.syss.map(x=>`<a class="tl" href="/problem/${w.t}/${encodeURIComponent(x)}">${x}</a>`).join(' · ')}</td>
- <td class="small">${w.why}</td></tr>`});
- h+=`</table></div>`;
- }
- if(TAB==='振动融合分析'){
- if(!d.fus||d.fus.err){ h+=`<div class="card"><p>融合面数据不可用: ${d.fus&&d.fus.err||'无数据'}</p></div>`; }
- else{
- const F=d.fus, CL={bad:'var(--bad)',warn:'var(--warn)',note:'var(--muted)',ok:'var(--ok)',unlisted:'var(--muted)',stale:'var(--muted)'};
- if(!window.FUSSUB||window.FUSSUB==='能力边界') window.FUSSUB='融合矩阵'; // 旧持久值兜底
- const SUBS=['融合矩阵','端到端闭环','逐台情况','报警工单','振动分析'];
- h+=`<div class="eyebrow">振动与 SCADA 证据视图 · 展示振动结论、SCADA 指标、报警与工单线索</div>`;
- h+=`<div style="display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap">`+SUBS.map(s=>{
- const on=s===FUSSUB;
- return `<button onclick="window.FUSSUB='${s}';if('${s}'!=='逐台情况')window.FUSF=null;render()" style="cursor:pointer;padding:8px 16px;border-radius:20px;border:1px solid ${on?'var(--accent)':'var(--line)'};background:${on?'var(--accent)':'var(--panel)'};color:${on?'#fff':'var(--ink)'};font-size:13px;font-weight:${on?'600':'400'}">${s}</button>`}).join('')+`</div>`;
- if(FUSSUB==='融合矩阵'){
- h+=`<div class="card mb" style="border-left:3px solid var(--warn)"><b>两条判读纪律 (随板块常显)</b><p class="small">① 同一失效机制的多个观测面 ≠ 多源印证 — 融合禁按源数加权 (29# 柱塞/温度/振动稳态/振动瞬态 = 四个观测面一条机制链)。<br>② "没报警" ≠ "正常" — 在已知盲区上 (见"能力边界"页), 振动阴性不构成证据; 矩阵"未列"台同理。</p></div>`;
- const spark=(hist,color)=>{ // 记忆轨迹: 旧窗=细线, 现窗=实心点
- if(!hist||hist.filter(x=>x!=null).length<3) return '';
- const vs=hist.filter(x=>x!=null), mn=Math.min(...vs), mx=Math.max(...vs), sp=(mx-mn)||1;
- const W=84,H=22,n=hist.length;
- let dpath='',last=null;
- hist.forEach((v,i)=>{ if(v==null)return; const x=(i/(n-1))*W, y=H-2-((v-mn)/sp)*(H-4);
- dpath+=(dpath?'L':'M')+x.toFixed(1)+' '+y.toFixed(1); last=[x,y]; });
- return `<svg width="${W}" height="${H}" style="vertical-align:middle;margin-left:6px"><path d="${dpath}" fill="none" stroke="${color}" stroke-width="1.2" opacity="0.75"/><circle cx="${last[0]}" cy="${last[1]}" r="2.4" fill="${color}"/></svg>`;
- };
- h+=`<div class="card mb"><h3>全场融合矩阵 (38 台 × 6 个证据面)</h3>
- <p class="small" style="color:var(--muted)">各列取数窗口不同 (见列头) — 不同窗的两格同时发红不等于"同时发生", 不可当共时印证。</p>
- <p class="small">每格右侧曲线 = 逐月记忆轨迹 (${(F.列窗.months||[]).length} 个月, ${(F.列窗.months||['?'])[0]}起; 实心点=现窗) — 现窗快照会丢形态, 轨迹才看得见"塌→回升→翻转"类机制签名。</p>`;
- // 等宽固定布局 (用户令): 六证据列各 15.5%, 机组列 7%; 长口径收进列头副行与悬停
- const shortWin=w=>{const t=zh(w||''); const m=t.match(/20[0-9][0-9]-[0-9][0-9](?:~20[0-9][0-9]-[0-9][0-9])?|至[ ]*20[0-9][0-9]-[0-9][0-9]|20[0-9][0-9]~20[0-9][0-9]/); return m?m[0]:t.slice(0,14)};
- const th=(name,win,extra)=>`<th title="${esc(zh(win))}${extra?' · '+extra:''}"><div>${name}</div>
- <div class="small" style="font-weight:400;color:var(--muted)">${shortWin(win)}${extra?' · '+extra:''}</div></th>`;
- h+=`<div class="small" style="color:var(--muted);margin:6px 0 12px;line-height:2">
- <div><b>格子颜色 = 设备状态 (业主口径, 与 windcms 报告同一套):</b>
- ${['危险','报警','良好','优秀','不可判'].map(k=>
- `<span style="display:inline-flex;align-items:center;gap:4px;margin-right:12px"><i style="width:10px;height:10px;border-radius:2px;background:${C[k]||'#ccc'}"></i>${k}</span>`).join('')}</div>
- <div><b>徽章 = 证据状态 (可信度, windcms 九枚举):</b>
- ${['定论','准定论·预警','候选·新发','候选','候选·记基线','参考','INSUFFICIENT'].map(k=>
- `<span style="display:inline-flex;align-items:center;gap:4px;margin-right:12px"><i style="width:10px;height:10px;border-radius:2px;background:${(F.色标||{})[k]||'#ccc'}"></i>${k}</span>`).join('')}</div>
- <div style="color:var(--muted)">两轴不可互相替代: "候选"说的是可信度不是设备好坏; "良好"说的是设备状态不是证据强度。设备状态只取自振动线的判级栏这一个来源。</div>
- </div>`;
- h+=`<table style="table-layout:fixed;width:100%"><colgroup><col style="width:7%">${'<col style="width:15.5%">'.repeat(6)}</colgroup>
- <tr><th>机组</th>${th('振动',F.列窗.振动,'不随本时间窗')}${th('温度',F.列窗.温度)}${th('润滑柱塞',F.列窗.润滑,'只展示')}${th('油液',F.列窗.油液)}${th('报警',F.列窗.报警||'')}${th('工单',F.列窗.工单||'')}</tr>`;
- const MAXAL = Math.max(1, ...F.矩阵.map(r=>(r.报警||{}).n||0));
- const MAXWO = Math.max(1, ...F.矩阵.map(r=>(r.工单||{}).n||0));
- F.矩阵.forEach(r=>{
- // 各列只列与问题相关的 (2026-08-28 用户令"不要发散"): 该格非问题级 → 收敛成一词, 全文留悬停;
- // 有问题才展开证据。矩阵是找问题的面, 不是数据倾倒面。
- // 非问题格短词: 优先取 note 里的判断短语 (振动列已把"未收录"细化成
- // 已评估无异常 / 数据不足·不可判 / 未评估 三类, 固定字典会把这个区分盖掉)
- // 格内一律显示明确定级词, 不显示截断的原文 (2026-08-28 用户逮"评估报告 2026-08-2…"这类无意义截断)
- const shortOf = (c) => {
- if(c.ostate && c.ostate !== '不可判') return c.ostate; // 明确设备状态优先
- if(/数据不足|不可判/.test(String(c.note))) return '数据不足';
- if(/未评估/.test(String(c.note))) return '未评估';
- if(/无样|未取样/.test(String(c.note))) return '未取样';
- if(/样龄|stale/.test(String(c.note))) return '样龄过期';
- return {ok:'正常', unlisted:'未收录', stale:'样龄过期'}[c.level] || '—';
- };
- // ★两套体系并存 (2026-08-28 用户令"参照 windcms 优秀/良好/报警/危险 级别体系"):
- // ① 业主口径状态等级 (危险>报警>良好>优秀>不可判) = 设备状态轴 → 决定格子颜色 (C 表, 与九系统矩阵同色)
- // ② 证据状态 九枚举 (定论/准定论·预警/候选/参考/INSUFFICIENT) = 可信度轴 → 作徽章挂在旁边 (windcms 色标)
- // 两轴不可互相替代: "候选"是可信度不是设备好坏, "良好"是设备状态不是证据强度。
- const WC = F.色标 || {};
- const cell=(c,lim,icon,tid)=>{
- const col = C[c.ostate] || WC[c.wlevel] || CL[c.level] || 'var(--muted)';
- const full = vibPlain(zh(c.note));
- const isProb = ['bad','warn','note'].includes(c.level);
- if(!isProb) return `<td class="small tipw" data-tip="${esc(full)} — 设备状态 ${c.ostate||'?'}" style="border-left:3px solid ${col};color:var(--muted)">${shortOf(c)}</td>`;
- const cut = full.length>(lim||46) ? full.slice(0,lim||46)+'…' : full;
- const hot = ['报警','危险'].includes(c.ostate);
- const ic = icon ? partGlyph(full.split(/[::]/)[0], hot ? col : 'var(--muted)', hot) : '';
- // 图 + 判级短词; 全文走悬停浮层 (2026-08-28 用户令: 列宽有限, 长句在格里会被挤成竖排)
- // 部件名已由图表达 ⇒ 格内文字只留判级词, 不重复部件名
- const parts = full.split(/[::]/);
- // 明确定级词打头, 后面才是简述; 截断的原文(如"评估报告 2026-08-2…")不作为格内主文案
- const brief = (parts.length > 1 ? parts.slice(1).join(':') : full).trim();
- const cleanBrief = /^[ ]*(评估报告|两口径差异|⚠)/.test(brief) || brief.length > 16 ? '' : brief;
- // duo 徽章已给定级词 → 主文案只留简述, 无简述时才回退定级词 (避免同格重复两次)
- const shortCut = cleanBrief || (c.ostate && c.ostate !== '不可判' ? '' : shortOf(c));
- // 点部件图 → 浮窗 (问题说明 + 该部件小曲线 + 四源微视图); 悬停只给文字
- const clk = (tid && ic) ? ` onclick="popPart('${tid}','${esc(parts[0]).replace(/'/g,'')}')" style="cursor:pointer"` : '';
- // 设备状态 | 证据状态 同框 (用户令: 两个 UI 分开显示太乱)
- const ev = (c.wlevel && c.wlevel !== '—' && c.wlevel !== '正常' && c.ostate)
- ? `<span class="duo" style="--c:${col};margin-top:4px"><span class="st">${c.ostate}</span><span class="ev">${c.wlevel}</span></span>` : '';
- return `<td class="small tipw" data-tip="${esc(full)} — 设备状态 ${c.ostate||'?'} / 证据状态 ${c.wlevel||'?'}"
- style="border-left:3px solid ${col};overflow:hidden">
- <div${clk}>${ic || ''}<div style="color:${col};font-weight:${['bad','warn'].includes(c.level)?600:400}">${shortCut}</div>${ev}</div>
- ${spark(c.hist,c.level==='bad'?'var(--bad)':c.level==='warn'?'var(--warn)':'var(--muted)')}</td>`};
- const al=r.报警||{n:0,fam:[]}, wo=r.工单||{n:0,acts:[]};
- const alc=al.rank&&al.rank<=5?'var(--warn)':'var(--line)';
- const bu=al.burst;
- h+=`<tr><td><a class="tl" href="/turbine/${r.turbine}"><b>${r.turbine.replace('WTG','')}#</b></a></td>${cell(r.振动,46,true,r.turbine)}${cell(r.温度)}${cell(r.润滑)}${cell(r.油液)}
- <td class="${(al.items||[]).length?'tipw':''}" data-tip="${esc(((al.items||[]).map(x=>`${x.code} ${x.text} — ${x.n} 条 (${x.first}~${x.last})`).join('\n')) + (bu?`\n⚠ 单码 ${bu.code} 占 ${(bu.share*100).toFixed(0)}%, 中位 ${bu.med_s} 秒/次 — 条数被抖动灌满`:''))}" style="border-left:3px solid ${bu?'var(--unknown)':alc};white-space:pre-line">
- <b class="small" style="font-variant-numeric:tabular-nums;color:${al.rank&&al.rank<=10?'var(--ink)':'var(--muted)'}">${al.n} 条</b>
- ${al.rank?`<span class="small" style="color:var(--muted)"> · 第${al.rank}/${al.tot}</span>`:''}
- ${bu?`<div class="small" style="color:var(--unknown)">⚠ ${(bu.share*100).toFixed(0)}% 是码${bu.code}抖动</div>`:''}
- ${(al.rank&&al.rank<=10)||bu?famBar(al.fam,al.n,MAXAL):''}</td>
- <td class="${(wo.items||[]).length?'tipw':''}" data-tip="${esc((wo.items||[]).map(x=>`${x.date} ${x.name} → ${x.act}${x.part?' '+x.part:''}`).join('\n') || '时间窗内无传动链工单')}" style="min-width:110px;white-space:pre-line">
- ${wo.n?`<b class="small">${wo.n} 单</b>${wo.last?`<span class="small" style="color:var(--muted)"> · ${wo.last}</span>`:''}
- <div style="height:7px;border-radius:3px;background:var(--muted);opacity:.5;margin-top:4px;width:${Math.max(6,Math.round(wo.n/MAXWO*90))}px"></div>
- <div class="small" style="color:var(--muted);margin-top:3px">${(wo.acts||[]).slice(0,2).map(a=>a.k+'×'+a.n).join(' · ')}</div>`
- :`<span class="small" style="color:var(--muted)">—</span>`}</td></tr>`});
- h+=`</table><p class="small">润滑柱塞各台设定本就不同 (全场单次循环 22~961 秒), 所以此列只展示不判级, 只有已定案的机制链才标色; 油液多数台末次取样距今超 12 个月, 此列看的是"样够不够新", 不代表当前状态。</p></div>`;
- }
- // (能力边界撤; 端到端闭环替入 — 2026-08-28 用户令: 回恢复的和不恢复的都列, 结果性实证非方法学)
- if(FUSSUB==='端到端闭环'){
- const LP=(F.能力&&F.能力.loop&&F.能力.loop['换件闭环 (检出→换件→恢复)'])?F.能力.loop:null;
- if(!LP){ h+=`<div class="card mb"><p class="small">handoff 无端到端闭环数据</p></div>`; }
- else {
- const cl=LP['换件闭环 (检出→换件→恢复)']||[], up=LP['当前在升 (无工单可对)']||[], neg=LP['负面对照 (工单后持平)'];
- h+=`<div class="card mb"><h3>维修后验证案例 (检出 → 换件 → 恢复)</h3>
- <p class="small">有换件记录且同口径指标回落, 可作为检测链已闭合的案例; 指标仍在上升但缺少可匹配工单的机组, 需现场或台账确认后再关闭。</p>
- <h3 style="margin-top:8px">已闭环 · 换件后恢复 (${cl.length} 例)</h3>
- <table><tr><th>机组</th><th>部件</th><th>事项</th><th>换件日</th><th>换前→换后 (同标尺)</th><th>恢复比</th><th>判</th></tr>
- ${cl.map(r=>`<tr><td><b>${r.turbine.replace('WTG','')}#</b></td><td style="min-width:220px">${dtMini(r.component+(r.title||''),'var(--ok)',null,r.turbine)}</td><td class="small">${r.title}</td><td>${r.replace_date}</td><td>${baBar(r.pre,r.post,'var(--ok)')}<span class="small" style="color:var(--muted)">${r.pre} → ${r.post}</span></td><td class="num"><b style="color:var(--ok)">${r.ratio}×</b></td><td><span class="chip" style="--c:var(--ok)">${r.verdict}</span></td></tr>`).join('')}</table>
- <p class="small" style="margin-top:6px">负面对照: 工单后持平 ${neg} 例 (换件≠必然回落, 说明回落非巧合)。</p>
- <h3 style="margin-top:14px">未闭环 · 指标上升待现场/工单确认 (${up.length} 台)</h3>
- <table><tr><th>机组</th><th>部件</th><th>早期→近期 (同标尺)</th><th>倍比</th><th>状态</th></tr>
- ${up.map(r=>`<tr><td><b>${r.turbine.replace('WTG','')}#</b></td><td style="min-width:220px">${dtMini(r.component,'var(--warn)',null,r.turbine)}</td><td>${baBar(r.early,r.late,'var(--warn)')}<span class="small" style="color:var(--muted)">${r.early} → ${r.late}</span></td><td class="num"><b style="color:var(--warn)">${r.ratio}×</b></td><td><span class="chip" style="--c:var(--warn)">上升待确认</span></td></tr>`).join('')}</table>
- <p class="small" style="margin-top:8px;color:var(--muted)">数据边界: ${(LP['★数据边界']||'').split('**').join('')}</p>
- </div>`;
- }
- }
- if(FUSSUB==='逐台情况'){
- const lv=v=>/定论|报警/.test(v)?'bad':/预警/.test(v)?'warn':/销案|无新证|保留/.test(v)?'ok':'note';
- if(window.FUSF==='unlisted'){
- const un=F.矩阵.filter(r=>r.振动.level==='unlisted');
- h+=`<div class="card mb"><h3>未列机组 ${un.length} 台 (振动线未收录 — <b>未列 ≠ 正常</b>, 盲区见"能力边界"页) <a class="tl" href="#" onclick="window.FUSF=null;render();return false">×清除筛选</a></h3>
- <table><tr><th>机组</th><th>温度轴现状</th><th>润滑柱塞现状</th></tr>`;
- un.forEach(r=>{h+=`<tr><td><a class="tl" href="/turbine/${r.turbine}"><b>${r.turbine.replace('WTG','')}#</b></a></td><td class="small">${r.温度.note}</td><td class="small">${r.润滑.note}</td></tr>`});
- h+=`</table></div>`;
- } else {
- let rows=F.表;
- const FLABEL={bad:'定论/报警级',warn:'预警级',note:'候选/监视/观察级',ok:'销案/无新证'};
- if(window.FUSF) rows=F.表.filter(r=>lv(r.振动结论)===FUSF);
- h+=`<div class="card mb"><h3>逐台情况 (全场 ${new Set(F.表.map(x=>x.turbine)).size} 台 / ${rows.length}${window.FUSF?`/${F.表.length} 台次 · 筛选: ${FLABEL[FUSF]} <a class="tl" href="#" onclick="window.FUSF=null;render();return false">×清除</a>`:' 台次'}; 深入谱图/波形 → "振动分析"页)</h3>
- <div class="card" style="border-left:3px solid var(--accent);margin-bottom:12px">
- <b class="small">评价体系 (规范化定义 — 两轴并存, 不可互相替代)</b>
- <table style="margin-top:8px"><tr><th style="width:34%">设备状态 (业主口径)</th><th style="width:34%">证据状态 (可信度)</th><th>含义</th></tr>
- <tr><td>${chip('危险')}</td><td><span class="badge">定论</span></td><td class="small">异常严重, 建议尽快停机检查 <span style="color:var(--muted)">(证据: 已定案)</span></td></tr>
- <tr><td>${chip('报警')}</td><td><span class="badge">准定论·预警 / 候选·新发</span></td><td class="small">存在需现场核实或处置的异常 <span style="color:var(--muted)">(证据: 基本确认 / 新出现待核实)</span></td></tr>
- <tr><td>${chip('良好')}</td><td><span class="badge">候选 / 候选·记基线</span></td><td class="small">轻微偏离, 记录观察 <span style="color:var(--muted)">(证据: 待现场核实 / 已记为基线下窗观察)</span></td></tr>
- <tr><td>${chip('优秀')}</td><td><span class="badge">参考 / 正常 / 撤回</span></td><td class="small">未见异常, 保持常规监视 <span style="color:var(--muted)">(证据: 仅记录 / 已取消)</span></td></tr>
- <tr><td>${chip('不可判')}</td><td><span class="badge">INSUFFICIENT</span></td><td class="small">数据不足或监测中断 — 不等于正常 <span style="color:var(--muted)">(证据: 数据不足)</span></td></tr></table>
- <p class="small" style="color:var(--muted);margin-top:7px">
- 设备状态答"这台设备现在怎么样"(危险>报警>良好>优秀>不可判); 证据状态答"这个判断有多可信"(定论→INSUFFICIENT)。
- 设备状态一律取自振动线的判级栏这一个来源(遇到未登记的定级词直接报错, 不静默当成"优秀")。
- 本页判级为转录, 不产新判级; 标注为引用同族/谱线级/测量链/未映射的行<b>不作部件级结论消费</b>;
- claim_window = 该结论的证据窗, 窗外状态该结论不背书。</p></div>
- <table><tr><th>机组</th><th>部件</th><th>设备状态 / 振动结论</th><th>证据窗<span class='small' style='color:var(--muted);font-weight:400'> 26-01→09</span></th><th>振动信号</th><th>温度轴 <span class='small' style='color:var(--muted);font-weight:400'>±6K</span></th><th>油样</th></tr>`;
- rows.forEach(r=>{
- // 判级取后端单源列 (禁前端正则: '销案(定论)'含'定论'会被误染红 — 2026-08-28 同根第2例)
- const LVC={bad:'var(--bad)',warn:'var(--warn)',ok:'var(--ok)',meta:'var(--muted)',note:'var(--info)'};
- const sev=LVC[r.级]||'var(--info)';
- h+=`<tr><td><a class="tl" href="/turbine/${r.turbine}"><b>${r.turbine.replace('WTG','')}#</b></a></td>
- <td style="min-width:220px">${dtMini(r.部件,sev,null,r.turbine)}</td>
- <td><span class="duo" style="--c:${C[r.设备状态]||'var(--muted)'}" title="${r.设备状态||'?'}: ${ST_NOTE[r.设备状态]||''} | 证据 ${r.证据状态||'?'}: ${EV_NOTE[r.证据状态]||''} | 振动线原文: ${esc(r.振动结论)}">
- <span class="st">${r.设备状态||'—'}</span><span class="ev">${plain(r.振动结论)}</span></span></td>
- <td style="min-width:130px">${winBar(r.时间窗区间,r.claim_window)}</td>
- <td class="small tipw" data-tip="振动线原文: ${esc(r.振动信号)}">${vibPlain(r.振动信号)}</td><td style="min-width:158px">${devBar(r.温度通道,r.温度dev,r.温度判,r.温度轴)}</td>
- <td style="min-width:112px">${oilPill(r.油末样,r.油新鲜,r.油样轴)}</td></tr>`});
- h+=`</table></div>`;
- }
- }
- if(FUSSUB==='报警工单'&&F.事件){
- // 数据边界长条撤 (2026-08-28 用户令): 两源不重叠的完整说明太长且每页重复;
- // 关键一句仍在工单卡("0 条=记录缺失非未修过")与汇报定制页第⑧章口径与数据边界。
- h+=`<div class="card mb"><p class="small" style="color:var(--muted)"><b>时间窗 ${d.win}</b> · 顶部选择器可换</p></div>`;
- for(const [comp,E] of Object.entries(F.事件)){
- h+=`<div class="card mb"><h3>${comp} — 报警 ${E.报警.总条数} 条/${E.报警.台数} 台 · 工单 ${E.工单.总条数} 条/${E.工单.台数} 台 (时间窗内)</h3>`;
- if(E.报警.总条数>0){
- h+=`<table><tr><th>码</th><th>报警内容</th><th class="num">条数</th><th class="num">台数</th><th class="num">累计时长h</th></tr>`;
- E.报警.码统计.forEach(c=>{h+=`<tr><td>${c.code}</td><td class="small">${c.text}</td><td class="num">${c.条数}</td><td class="num">${c.台数}</td><td class="num">${c.时长h}</td></tr>`});
- h+=`</table><p class="small">台分布 top: ${Object.entries(E.报警.top台).map(([t,n])=>`<a class="tl" href="/turbine/${t}">${t.replace('WTG','')}#</a>×${n}`).join(' · ')}</p>`;
- h+=`<details><summary class="small">最近 ${E.报警.近况.length} 条明细</summary><table><tr><th>机组</th><th>时间</th><th>码</th><th>内容</th><th class="num">时长min</th></tr>`;
- E.报警.近况.forEach(r=>{h+=`<tr><td>${r.t.replace('WTG','')}#</td><td class="small">${r.on}</td><td>${r.code}</td><td class="small">${r.text}</td><td class="num">${r.dur_min??'—'}</td></tr>`});
- h+=`</table></details>`;
- } else h+=`<p class="small">时间窗内无该部件报警</p>`;
- if(E.工单.总条数>0){
- h+=`<table style="margin-top:8px"><tr><th>机组</th><th>日期</th><th>故障名称</th><th>处理</th><th>原因</th></tr>`;
- E.工单.明细.forEach(r=>{h+=`<tr><td>${r.t.replace('WTG','')}#</td><td class="small">${r.date}</td><td class="small">${r.name}</td><td class="small">${r.act}</td><td class="small">${r.cause}</td></tr>`});
- h+=`</table>`;
- } else h+=`<p class="small">时间窗内无该部件工单 (工单数据止 2024-11, 2026 类窗必空 = 数据边界)</p>`;
- h+=`</div>`;
- }
- }
- if(FUSSUB==='振动分析'){
- // 2026-08-28 用户令: 生接改融合 — 只显示结果(windcms 评估报告转录), 模型与计算隐藏, 分析功能留独立 cms
- if(!window.VIBCMS) { fetch('/api/vibcms').then(r=>r.json()).then(d=>{window.VIBCMS=d; render();}); h+=`<div class="card mb"><p class="small">载入振动评估结果…</p></div>`; }
- else if(VIBCMS.error){ h+=`<div class="card mb"><p class="small" style="color:var(--bad)">${VIBCMS.error}</p></div>`; }
- else {
- const V=VIBCMS;
- const gc=st=>({'报警':'var(--bad)','危险':'var(--bad)','不可判':'#9aa4b0','良好':'var(--warn2,#8a8f3b)','优秀':'var(--ok)'}[st]||'var(--muted)');
- const cp=st=>`<span class="chip" style="--c:${gc(st)}">${st}</span>`;
- h+=`<div class="card mb"><h3>振动状态评估 (windcms 报告 ${V.date} 结果转录)</h3>
- <p class="small">${V.overall||''} 状态管理判级, 不代表确诊数量; 判据/谱线/计算过程不在本页 — 深潜走底部独立系统。</p>
- <h3 style="margin-top:10px">需行动机组 (报警/不可判)</h3>
- <table><tr><th>机组</th><th>状态</th><th>状态焦点 <span class='small' style='font-weight:400;color:var(--muted)'>(传动链: 主承前·主承后·齿轮箱·发电机两端)</span></th><th>证据强度</th><th>行动</th></tr>
- ${V.action.map(r=>{const cm={};String(r[2]).split(/[;;]/).forEach(seg=>{const m=seg.split(/[::]/);
- if(m.length>1){const k=m[0].trim(); cm[k]=gc(m[1].trim())}});
- return `<tr><td><b>${r[0]}</b></td><td>${cp(r[1])}</td><td style="min-width:220px" title="${esc(r[2])}">${dtChainStates(cm,null,r[0])}</td>
- <td style="min-width:150px">${evLadder(r[3])}</td><td>${/P0/.test(r[4])?`<b style="color:var(--bad)">${r[4]}</b>`:r[4]}</td></tr>`}).join('')}</table>
- <h3 style="margin-top:14px">38 台部件等级一览</h3>
- <table><tr><th>机组</th><th>部件状态链 <span class='small' style='font-weight:400;color:var(--muted)'>(主承前·主承后·齿轮箱三级·发电机DE/NDE; 悬停看逐件等级)</span></th><th>综合</th><th>行动</th></tr>
- ${V.grades.map(g=>`<tr><td><b>${g.t}</b></td>
- <td style="min-width:220px" title="主轴承前 ${g.主轴承前} · 主轴承后 ${g.主轴承后} · 齿轮箱 ${g.齿轮箱} · 发电机 ${g.发电机}">${dtChainStates({'主轴承前':gc(g.主轴承前),'主轴承后':gc(g.主轴承后),'齿轮箱':gc(g.齿轮箱),'发电机':gc(g.发电机)},null,g.t)}</td>
- <td>${cp(g.综合)}</td><td class="small">${g.行动}</td></tr>`).join('')}</table>
- <div class="small" style="margin-top:12px;padding:10px;border:1px solid var(--line);border-radius:8px">
- <b>口径说明</b> 本表=windcms 状态管理判级(含标量趋势与数据可判性); 融合矩阵/逐台结论=振动线定谳链转录(候选以上)。两口径差异台:
- 仅本表列 ${V.diff.only_cms.join(', ')||'无'} (多为标量发作/数据不可判, 未到定谳链); 仅定谳链列 ${V.diff.only_handoff.join(', ')||'无'} (预警级在察, 状态管理未升)。
- </div>
- <p class="small" style="margin-top:10px;color:var(--muted)">谱图 · 波形 · 判据 · 工作台等分析功能在独立 windcms 系统 →
- <a class="tl" href="http://localhost:8030/index.html" target="_blank">打开 windcms ↗</a> (未启动时 <a class="tl" href="/windcms/index.html" target="_blank">静态版</a>)</p>
- </div>`;
- }
- }
- }
- }
- if(TAB==='系统维护'){
- h+=`<div class="eyebrow">系统维护 · 数据输入与更新端口</div>
- <div id="mtOut"><div class="card"><p class="small">盘点中…</p></div></div>`;
- document.getElementById('root').innerHTML=h;
- document.getElementById('foot').innerHTML=d.note;
- fetch('/api/maint_survey').then(r=>r.json()).then(md=>{
- const el=document.getElementById('mtOut'); if(el) el.innerHTML=maintCards(md);
- }).catch(e=>{const el=document.getElementById('mtOut'); if(el) el.innerHTML=`<div class="card"><p class="small" style="color:var(--bad)">盘点失败: ${e.message}</p></div>`});
- return;
- }
- if(TAB==='检修助手'){
- // 三窗并列 (2026-08-28 用户令): 抢修 / 预防性维护 / 故障树 各有自己的输入与结果,
- // 不再共用一个框切换 — 实际用法是三者对照着看。
- const SYSOPT=['变桨','偏航','主轴承','齿轮箱','发电机','变流器','塔架与基础','主控与传感网'];
- // ★示例码原来写死 5 个, 其中 64000 在本体里 name 是字符串 "nan" (pandas NaN 漏成字符串,
- // 555 个码里有 17 个是这样) —— 页面把一条垃圾记录当"常查"推荐, 点了必然空白。
- // 改为**按本场实际计数**取: 只从 (本场出现过 ∧ 名字有效) 的 309 个里选。
- // 写死清单迟早与数据脱节, 数据驱动才不会推荐自己答不上的东西。
- const HOT=(window.__HOTCODES && window.__HOTCODES.length) ? window.__HOTCODES
- : ['3225','3130','13902','10105','3224'];
- const btn=m=>`<button onclick="asstGo('${m}')" style="cursor:pointer;flex:none;padding:7px 18px;border-radius:7px;
- border:1px solid var(--accent);background:var(--accent);color:#fff;font-weight:600;font-size:13px">查询</button>`;
- h+=`<div class="eyebrow">检修助手 · 手册、报警与工单证据查询</div>`;
- h+=`<div class="card mb"><h3>抢修 <span class="small" style="font-weight:400;color:var(--muted)">报警码 → 失效模式 → 手册分步处置 → 历史工单</span></h3>
- <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
- <input id="asstIn_fault" placeholder="报警码, 如 3225 / 3130 / 13902" value="${window.ASSTV||''}"
- onkeydown="if(event.key==='Enter')asstGo('fault')"
- style="flex:1 1 220px;min-width:180px;padding:7px 12px;border:1px solid var(--line);border-radius:7px;background:var(--panel);color:var(--ink);font-size:13px">
- ${btn('fault')}
- <span class="small" style="color:var(--muted);flex:none">常查 ${HOT.map(c=>
- `<a class="tl" href="#" onclick="document.getElementById('asstIn_fault').value='${c}';asstGo('fault');return false">${c}</a>`).join(' · ')}</span></div>
- <div id="asstOut_fault" style="margin-top:10px"></div></div>`;
- h+=`<div class="card mb"><h3>预防性维护 <span class="small" style="font-weight:400;color:var(--muted)">系统 → 失效模式 → 年度任务 → WI 步骤</span></h3>
- <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
- <select id="asstIn_prev" style="flex:1 1 200px;min-width:170px;padding:7px 10px;border:1px solid var(--line);border-radius:7px;background:var(--panel);color:var(--ink);font-size:13px">
- ${SYSOPT.map(x=>`<option>${x}</option>`).join('')}</select>
- ${btn('prev')}</div>
- <div id="asstOut_prev" style="margin-top:10px"></div></div>`;
- h+=`<div class="card mb"><h3>故障树 <span class="small" style="font-weight:400;color:var(--muted)">系统失效模式按实证量排 + 部件级原因</span></h3>
- <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
- <select id="asstIn_tree" style="flex:1 1 200px;min-width:170px;padding:7px 10px;border:1px solid var(--line);border-radius:7px;background:var(--panel);color:var(--ink);font-size:13px">
- ${SYSOPT.map(x=>`<option>${x}</option>`).join('')}</select>
- ${btn('tree')}</div>
- <div id="asstOut_tree" style="margin-top:10px"></div></div>`;
- document.getElementById('root').innerHTML=h;
- document.getElementById('foot').innerHTML=d.note;
- for(const m of ['fault','prev','tree']){
- const c=(window.ASSTCACHE3||{})[m], el=document.getElementById('asstOut_'+m);
- if(c&&el) el.innerHTML=c;
- }
- return;
- }
- if(TAB==='检修决策'){
- if(!window.MAINTSTD){ fetch('/api/maint_std').then(r=>r.json()).then(d=>{window.MAINTSTD=d; render()}); }
- h+=maintStd(window.MAINTSTD);
- if(!window.MAINTFW){ fetch('/api/maint_framework').then(r=>r.json()).then(d=>{window.MAINTFW=d; render()}); }
- h+=maintFw(window.MAINTFW);
- if(!window.DQF){ fetch('/api/dq_findings').then(r=>r.json()).then(d=>{window.DQF=d; render()}); }
- h+=dqFindings(window.DQF);
- h+=`<div class="eyebrow">检修决策 · ontology 展现的窗口 (语义本体 + dsh 编排; 推理无界, 结论受闸)</div>`;
- const CATS={
- '设备类':{hint:'设备状态/机制/判级 — 从机组档案、对象查询与机制检索取证',
- qs:['29号机组现在什么状态、为什么、该怎么办?','全场哪些机组有报警级或定论级判级?逐台列出并注明审级','失效机制库里有哪些机制族?各涉及哪些机组、状态如何?']},
- '检修排程决策':{hint:'排期沙盘/天气窗/验收树 — ont_scenario/ont_get 取证',
- qs:['29号修泵什么时候做最合适?对比各情景的损失与风险','未来几个月哪些月份适合安排CTV检修和吊装作业?依据历史可作业窗','29号修好之后按什么判据验收?三分支分别是什么条件?']},
- '损失分析':{hint:'当前3个月损失/未来损失 — ont_loss/ont_scenario 取证, 金额带电价区间口径',
- qs:['近3个月全场损失结构如何?分状态和分机组说, 电量与金额都给','29号如果拖到主轴承失效, 未来损失量级是多少?与现在修对比','限电常态化对检修机会成本有什么影响?']},
- '安全风险预测':{hint:'风险敞口/劣化趋势/盲区 — ont_risk/ont_mechanism_search 取证, 未列≠正常',
- qs:['当前哪些机组风险在上升需要重点盯?联动监视读数如何?','监测有哪些盲区?哪些"没报警"不等于正常?','29号轴承正在承受金属接触, 风险敞口怎么评估?']}};
- h+=`<div id="dbase"><div class="card mb"><div class="skel" style="width:40%"></div><div class="skel" style="width:65%;margin-top:8px"></div></div></div>`;
- setTimeout(loadDbase, 200);
- if(!window.ASKCAT) window.ASKCAT='设备类';
- h+=`<div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap">`+Object.keys(CATS).map(c=>{
- const on=c===ASKCAT;
- return `<button onclick="ASKCAT='${c}';render()" style="cursor:pointer;padding:8px 16px;border-radius:20px;border:1px solid ${on?'var(--accent)':'var(--line)'};background:${on?'var(--accent)':'var(--panel)'};color:${on?'#fff':'var(--ink)'};font-weight:${on?'600':'400'}">${c}</button>`}).join('')+`</div>`;
- const cat=CATS[ASKCAT];
- h+=`<div class="card mb"><h3>${ASKCAT} · 常用问题 (点即问, 或下方自由输入)</h3>
- <p class="small">${cat.hint}。答案句句带[对象id]溯源+接地闸核验; 判级只转述本体原文带审级, 模型零判级权。</p>`;
- cat.qs.forEach(q=>{h+=`<div class="small" style="cursor:pointer;border:1px solid var(--line);border-radius:8px;padding:8px 12px;margin-bottom:8px" onclick="document.getElementById('askq').value=this.textContent;askGo()">${q}</div>`});
- h+=`<div style="display:flex;gap:8px;flex-wrap:wrap;margin:10px 0">
- <input id="askq" type="text" placeholder="自由提问 (${ASKCAT})" style="flex:1;min-width:280px;padding:8px 12px;border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--ink)">
- <select id="askm" onchange="mdlNote()" style="padding:8px;border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--ink)">
- <option value="">…</option>
- </select>
- <span id="mdlnote" class="small" style="color:var(--muted)"></span>
- <label class="small" style="cursor:pointer;white-space:nowrap" title="由另一本地模型基于同一份证据复核; 同一模型自审无法构成独立校验">
- <input type="checkbox" id="askxrev"> 交叉审核 <span style="color:var(--muted)">(DeepSeek 14B 互审, +约 1 分钟)</span></label>
- <button onclick="askGo()" style="cursor:pointer;padding:8px 16px;border-radius:8px;border:1px solid var(--accent);background:var(--accent);color:#fff;font-weight:600">提问</button>
- </div>
- <div id="askout" class="small">空闲 — 点常用问题或输入后提问 (实测: 8b 单台问约 45 秒 / 复合问题约 50~90 秒, 27b/30b 约 2~4 分钟, deepseek 走 dsh 约 2~6 分钟; 串行一次一问; 本地模型零外发)。</div></div>`;
- setTimeout(askPoll, 300); setTimeout(askModelInit, 320); // 填下拉后 askModelInit 内部会调 mdlNote
- }
- if(TAB==='故障统计'){
- const _pt=(d.faults.partial||[]);
- h+=`<div class="eyebrow">故障统计与可靠性</div>`
- +winBanner('故障统计', d.win, _pt.length?`<span style="color:var(--warn)">时间窗内 ${_pt.join('/')} 数据不满月(覆盖 ${Math.round(d.faults.cov_min*100)}%), 月度图已按天归一</span>`:'');
- }
- if(TAB==='电量算账') h+=`<div class="eyebrow">电量算账</div>`+winBanner('电量算账', d.win, '');
- if(TAB==='汇报定制'){ document.getElementById('root').innerHTML=renderReport(d);
- document.getElementById('foot').innerHTML=d.note; return; }
- h+=`<div class="grid">`;
- if(TAB==='故障统计'){
- // 排布 (2026-08-28 用户令: 界面优化排布)。原来 9 张卡一条龙堆着, 报警面与停机面混排,
- // 且三张卡硬设 grid-column:1/-1 — 窄视口本来就单列, 全宽设置只在宽屏起反作用(挡住并排)。
- // 现按经典可靠性报告顺序分两节: 报警面 总量→关键少数→结构→台级→季节; 停机面 结构→归因→台级。
- // 只有"需要横向空间才读得出结构"的图保留全宽 (四象限散点 / RAM 象限), 其余交给 auto-fit:
- // 窄屏自然单列(帕累托 720 viewBox 几乎 1:1), 宽屏自动两列。
- const F2=d.faults, _pm=(F2.partial||[]);
- const sec=(t,n)=>`<div class="eyebrow" style="grid-column:1/-1;margin-top:4px">${t}<span
- style="text-transform:none;letter-spacing:0;font-weight:400;color:var(--muted)">${n}</span></div>`;
- h+=sec('报警面', `${F2.total.toLocaleString()} 条 · ${F2.n_codes} 个码 · 窗 ${d.win}`);
- h+=`<div class="card" style="grid-column:1/-1"><h3>月度故障率</h3>${rateChart(F2.monthly)}
- <p class="small" style="color:var(--muted);margin-top:6px">
- <b>柱高=日均条数, 不是月总数</b> — ${_pm.length?`本时间窗 ${_pm.join('/')} 数据不满月(灰虚线柱), 直接比月总数会读反`:'各月覆盖完整'}。
- ${_pm.length?`实测: ${_pm[0]} 总数最低但日均 ${F2.monthly.per_day[F2.monthly.months.indexOf(_pm[0])]} 条/日 = 全时间窗前列。`:''}</p></div>`;
- h+=`<div class="card"><h3>帕累托 · 按发生条数</h3>
- ${paretoChart(F2.pareto_n, ' 条', '条数=发生频次, 不反映影响程度 — 高频短时报警码(信号抖动或重复触发)排序靠前')}</div>`;
- h+=`<div class="card"><h3>帕累托 · 按持续时长</h3>
- ${paretoChart(F2.pareto_d, ' h', '时长=闩锁口径(复位−激活), 只作排序参考不作停机时长')}</div>`;
- h+=`<div class="card" style="grid-column:1/-1"><h3>故障模式四象限 · 条数 × 单次时长</h3>
- ${quadChart(F2.quad)}
- <p class="small" style="color:var(--muted);margin-top:6px">时间窗内共 ${F2.n_codes} 个码, 图上为条数前 ${F2.quad.length} 个。
- ${_pm.length?`<span style="color:var(--warn)">窗含不满月 ${_pm.join('/')} (覆盖 ${Math.round(F2.cov_min*100)}%), 条数为原始累计未按天归一。</span> `:''}
- 点面积表示影响机组数, 大点为批次共性、小点为单台; 竖分界取条数中位, 横分界取 10min (一个SCADA采样格)。
- <b>右下角的码不要按条数排优先级</b> — 属重复触发, 非硬件故障。上面两张帕累托对照看即此图的两条投影。</p></div>`;
- const maxT=Math.max(1,...F2.per_t.map(x=>x.v));
- h+=`<div class="card"><h3>台级分布 · 报警条数 top${F2.per_t.length}</h3>
- <table><tr><th style="width:70px">机组</th><th class="barcell">条数</th><th class="num" style="width:80px">占全场</th></tr>
- ${F2.per_t.map(x=>`<tr><td><a class="tl" href="/turbine/${x.k}"><b>${x.k.replace('WTG','')}#</b></a></td>
- <td class="barcell"><i class="bar" style="width:${Math.round(x.v/maxT*90)}px;--c:${x.v/maxT>0.6?'var(--bad)':'var(--accent)'}"></i><span class="cnt">${x.v.toLocaleString()}</span></td>
- <td class="num small">${(x.v/F2.total*100).toFixed(1)}%</td></tr>`).join('')}</table>
- <p class="small" style="color:var(--muted);margin-top:6px">全场合计 ${F2.total.toLocaleString()} 条 · 单台占比高须先核查是否为单一报警码重复触发</p></div>`;
- if(d.m8&&d.m8.err) h+=blockErr('m8', d.m8.err);
- if(m) h+=drawFig({kind:'multiline',title:'故障季节性 · top 报警族 × 月',unit:'条/月',xlab:'月',
- months:d.m8.seasonal[0].months, series:d.m8.seasonal.slice(0,4).map(x=>({name:x.name.slice(0,16),vals:x.vals})),
- note:'全程 19 个月 (跨 2 冬 1 夏); 未做 χ² 显著性检验前为【参考级】, 只看形态不作季节归因'});
- if(m){
- const RT=(d.rel&&!d.rel.err)?d.rel:null;
- h+=sec('停机面', `${m.停机总时长h?m.停机总时长h.toLocaleString():''}h · ${m.停机事件数||''} 起 · 口径 ≥1h 段, 不含调度令`);
- if(RT) h+=`<div class="card" style="grid-column:1/-1"><h3>可靠性 × 可维修性 · 部件象限</h3>
- ${ramChart(RT.表)}
- <p class="small" style="color:var(--muted);margin-top:6px">
- 按停机时长排序的表看不出这层结构: <b>同样 500h 的停机, "频发易修"要查根因、"罕发难修"要备件, 处置相反</b>。
- 设备类停机 ${RT.设备类停机h}h / 外部类 ${RT.外部类停机h}h (外部=电网·风况·运维操作, 不计设备可靠性)。
- 海上机组 MDT 普遍偏高是<b>交通窗口</b>决定的, 不等于维修本身慢 — 跨场比 MDT 须同为海上。</p>
- <div style="margin-top:12px">${attribBar(RT.表, RT.总停机h)}</div>
- <p class="small" style="color:var(--muted);margin-top:2px">
- 归因覆盖: "无记录停机" ${(RT.表.find(x=>x.部件==='无记录停机')||{}).停机时长h||0}h 是全场最大单项,
- 因不给 MTBF 而在上图中<b>不出现</b> — 它不是"没有故障", 是"没归上因"。
- 口径为段起点±30min 首发码。<b>缺口成因待定</b>: 此前写的"98% 的段在前 72h 内有报警 ⇒ 长停远窗"
- 不成立 — 前 72h 有报警不等于该报警与本段停机有因果 (双模审 2026-08-28 逮)。
- 要定成因须改判"段起止是否落在某条报警的持续区间内", 未做之前只报缺口不报原因。</p></div>`;
- h+=`<div class="card"><h3>停机归因帕累托</h3>
- ${paretoChart(d.m8.stop_pareto.map(x=>({k:x.k.split(';')[0].slice(0,26), v:x.h})), ' h',
- '按停机时长; 事件次数见悬停')}
- <p class="small" style="color:var(--muted);margin-top:4px">${m.口径}</p></div>`;
- h+=`<div class="card"><h3>台级停机 top${m.台级top.length}</h3>
- <table><tr><th style="width:64px">机组</th><th class="barcell">停机时长</th><th class="num" style="width:56px">次数</th></tr>
- ${(()=>{const mx=Math.max(...m.台级top.map(x=>x.时长h));return m.台级top.map(x=>`<tr>
- <td><a class="tl" href="/turbine/${x.t}"><b>${x.t.replace('WTG','')}#</b></a></td>
- <td class="barcell"><i class="bar" style="width:${Math.round(x.时长h/mx*92)}px;--c:${x.时长h/mx>0.6?'var(--bad)':'var(--accent)'}"></i><span class="cnt">${x.时长h}h</span></td>
- <td class="num small">${x.次}</td></tr>`).join('')})()}</table>
- <p class="small" style="color:var(--muted);margin-top:6px">停机时长高 ≠ 设备差 — 须先看归因里外部类占比 (调度令已剔除)</p></div>`;
- }
- }
- if(TAB==='电量算账'){
- const E=d.control.energy;
- if(E) h+=`<div class="kpis" style="grid-column:1/-1;margin-bottom:0">
- <div class="kpi"><b>${E.theo.toLocaleString()}<span> MWh</span></b><span>理论可发 (实际+损失)</span></div>
- <div class="kpi ok"><b>${E.act.toLocaleString()}<span> MWh</span></b><span>实际上网 · 等效满发 ${E.eflh.toLocaleString()} h</span></div>
- <div class="kpi ${E.loss_pct>=10?'warn':''}"><b>${E.loss_pct}<span>%</span></b><span>损失率 · ${E.loss.toLocaleString()} MWh</span></div>
- <div class="kpi ${E.cap_ok?'ok':'bad'}"><b>${E.cap_ok?'通过':'不通过'}</b><span>能量守恒核 · 理论 ${E.cap_ok?'<':'≥'} 全额定上限 ${E.cap.toLocaleString()}</span></div></div>`;
- if(E) h+=`<div class="card" style="grid-column:1/-1"><h3>电量平衡 <span class="small" style="font-weight:400;color:var(--muted)">窗 ${d.win}</span></h3>
- ${waterfall(E)}</div>`;
- if(E) h+=`<div class="card"><h3>损失责任分账</h3>${blameBar(E)}
- <p class="small" style="color:var(--muted);margin-top:6px"><b>最大一笔是限电不是故障。</b>
- 进设备考核的只有"故障与检修停机" ${Math.round((E.items.find(x=>x.k==='停机')||{}).loss||0).toLocaleString()} MWh;
- 限电属交易与调度侧, 2026 起交易策略主导, 少发未必是弃风。</p></div>`;
- h+=`<div class="card"><h3>工况态明细 <span class="small" style="font-weight:400;color:var(--muted)">窗 ${d.win}</span></h3>
- <table><tr><th>工况态</th><th class="barcell">时长占比</th><th class="num" style="width:76px">损失MWh</th></tr>`;
- const _mxp=Math.max(...d.control.states.map(x=>x.pct));
- d.control.states.forEach(x=>{h+=`<tr><td class="small">${LOSSPLAIN[x.k]||x.k}</td>
- <td class="barcell"><i class="bar" style="width:${Math.round(x.pct/_mxp*84)}px;--c:${x.k.indexOf('停机')>=0&&x.k.indexOf('调度')<0?'var(--bad)':'var(--accent)'}"></i><span class="cnt">${x.pct}% · ${Math.round(x.h).toLocaleString()}h</span></td>
- <td class="num small">${Math.round(x.loss).toLocaleString()}</td></tr>`});
- h+=`</table><p class="small" style="color:var(--muted)">${d.control.anchors}
- <br>控制参数一致性与曲线欠发筛查在<b>固定干净窗</b>判, 已迁入「发电性能」页 — 本页只出当期电量账。</p></div>`;
- }
- h+=`</div>`;
- document.getElementById('root').innerHTML=h;
- document.getElementById('foot').innerHTML=d.note;
- }
- async function renderCurves(){
- const root=document.getElementById('root');
- if(!CURVES){root.innerHTML='<div class="card"><h3>特性曲线多镜头</h3><p class="small">正在计算七个镜头 (剥限电 · 逐档中位 · 同型机群残差)…</p><div class="skel" style="width:60%;margin-top:10px"></div><div class="skel" style="width:80%;margin-top:8px"></div></div>';
- CURVES=await (await fetch('/api/curves')).json();}
- const d=CURVES; let h='';
- // 物理闸: 表格窄屏崩版 (2026-08-28 用户内部设计审逮) → 响应式三卡
- const P=d.physics, L=P['λ叶尖速比'];
- const pg=(name,crit,meas,verdict)=>`<div style="flex:1;min-width:220px;border:1px solid var(--line);border-left:3px solid var(--ok);border-radius:10px;padding:12px 14px;background:var(--panel)">
- <div style="font-weight:600">${name} <span class="small" style="color:var(--muted);font-weight:400">${crit}</span></div>
- <div class="num" style="margin:6px 0;font-size:15px">${meas}</div>
- <div class="small" style="color:var(--muted)">${verdict}</div></div>`;
- // (第一性原理硬闸卡撤 — 2026-08-28 用户令: 数据分析原则不出现在交付界面; 校验仍在后端跑, 只是不展示)
- h+=`<div class="eyebrow">特性曲线与控制参数</div>`+winBanner('发电性能','','');
- // 控制参数一致性与曲线欠发候选自「控制策略」页迁入 (2026-08-28 用户裁): 三者同为固定干净窗,
- // 放在跟随窗的电量页里会造成一页两窗 — 迁过来后两页各自单窗。
- const _D=(typeof D!=='undefined'&&D)?D:null; // D 是块作用域变量, 不挂 window
- const F=(_D&&_D.control)?_D.control:null, M9=(_D&&_D.m9&&!_D.m9.err)?_D.m9:null;
- if(_D&&_D.m9&&_D.m9.err) h+=blockErr('m9', _D.m9.err);
- if(M9){
- const p9=M9['P封顶'],w9=M9['ω封顶'],b9=M9['β_schedule'];
- h+=`<div class="card mb" style="grid-column:1/-1"><h3>控制参数一致性 <span class="small" style="font-weight:400;color:var(--muted)">窗 ${M9['窗']}</span></h3>
- <div style="display:flex;flex-wrap:wrap;gap:18px">
- <div style="flex:1 1 300px;min-width:280px">
- <div class="small" style="font-weight:600;margin-bottom:4px">封顶参数落位</div>
- ${capAxis(p9, w9)}
- <p class="small" style="color:var(--muted);margin-top:4px"><b>台数落在几个离散位置上, 不是连成一片</b> —
- 这是配置/版本差异的形状; 磨损或工况引起的会是连续散布。两轴各自成组且分组不同台 ⇒ 参数级差异, 成因须配置台账。</p>
- </div>
- <div style="flex:1 1 300px;min-width:280px">
- <div class="small" style="font-weight:600;margin-bottom:4px">桨距调度同档差</div>
- ${betaBar(b9)}
- <p class="small" style="color:var(--muted);margin-top:4px">同一功率档下的桨角与全场中位之差。
- ${b9.离群.map(x=>'<a class="tl" href="/turbine/'+x.t+'">'+x.t.replace('WTG','')+'#</a>').join(' / ')} 偏离达中位的
- ${Math.round(Math.max(...b9.离群.map(x=>x.平均偏))/Math.max(b9.全场同档差中位,0.01))} ${TIMES_SFX} → ${b9.判}</p>
- <div class="small" style="font-weight:600;margin:12px 0 4px">尺子巡检 · 极差 ÷ σ</div>
- ${rulerBar(M9['尺子'])}
- <p class="small" style="color:var(--muted);margin-top:4px">判据自身的分辨力。38 台正态抽样的极差期望约 3.7σ,
- <b>明显超出即分布非单峰</b> — 与左图的分组互为印证; 落在期望附近的轴(额定桨角)则是连续分布。</p>
- </div>
- </div></div>`;
- }
- if(F&&F.curve){
- h+=`<div class="card mb"><h3>曲线欠发筛查 <span class="small" style="font-weight:400;color:var(--muted)">${F.curve.note} · σ=${F.sigma||F.curve.sigma}%</span></h3>
- <p class="small">${F.curve.cands.map(c=>'<a class="tl" href="/turbine/'+c.t+'">'+c.t.replace('WTG','')+'#</a> '+c.dev+'% '+c.判).join(' | ')||'无候选'}</p></div>`;
- }
- h+=`<div class="grid">`; d.figs.forEach(f=>{h+=drawFig(f)}); h+=`</div>`;
- root.innerHTML=h;
- document.getElementById('foot').innerHTML=d.note;
- }
- reload('2026年');
- </script></body></html>
|