|
|
@@ -1,2162 +0,0 @@
|
|
|
-
|
|
|
-// 设备状态色 = 观澜语义色单源 (2026-08-28 用户裁: 观澜色块体系更好, 保留; windcms 那边改过来)
|
|
|
-// 对照 windcms 现值见 design.py 的 --st-* (迁移期保留, 供振动线对齐用)
|
|
|
-const C={'优秀':'var(--ok)','良好':'var(--info)','报警':'var(--warn)','危险':'var(--bad)','不可判':'var(--unknown)','监测不可用':'var(--unknown)','无监测':'var(--unknown)'};
|
|
|
-// 传动链运动简图 (2026-08-28 用户令"纯图形": 弃方块标签, 用工程简图语言 —
|
|
|
-// 轴=横线 · 齿轮=竖矩形(啮合处相切) · 轴承=剖面符号 · 行星级=齿圈+行星架 · 发电机=定子/转子;
|
|
|
-// 只有命中部件着色并标名, 其余全灰 = 一眼看出"链条哪一环出问题")
|
|
|
-const baBar=(pre,post,col)=>{
|
|
|
- const M=Math.max(pre,post,7), W=120, sc2=v=>Math.max(2,v/M*(W-4));
|
|
|
- return `<svg class="dt" viewBox="0 0 ${W} 26" style="width:${W}px;height:26px">
|
|
|
- <rect x="2" y="2" width="${sc2(pre).toFixed(1)}" height="9" rx="2" fill="var(--muted)" fill-opacity="0.55"/>
|
|
|
- <rect x="2" y="15" width="${sc2(post).toFixed(1)}" height="9" rx="2" fill="${col}" fill-opacity="0.9"/>
|
|
|
- </svg>`};
|
|
|
-const esc=t=>String(t==null?'':t).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<');
|
|
|
-// 温度偏差条: 0 为中线的双向条 (方向即符号, 长度即幅度, 色即判级) + 数字并存
|
|
|
-const devBar=(ch,dev,vd,raw)=>{
|
|
|
- if(dev==null||dev===undefined) return `<span class="small">${esc(raw).slice(0,30)}</span>`;
|
|
|
- const M=6, W=104, mid=W/2, k=Math.max(-1,Math.min(1,dev/M)), bw=Math.abs(k)*(mid-8);
|
|
|
- // 只有真越档(|dev|>=3K)才上语义色, 其余淡灰 — 避免整列彩条抢注意力
|
|
|
- const strong = Math.abs(dev) >= 3;
|
|
|
- const col = !strong ? 'var(--muted)' : (/候选/.test(vd||'')?'var(--warn)':/记基线/.test(vd||'')?'var(--info)':/不可判|返服/.test(vd||'')?'var(--unknown)':'var(--muted)');
|
|
|
- const over=Math.abs(dev)>M;
|
|
|
- return `<div title="${esc(raw)}" style="display:flex;align-items:center;gap:7px">
|
|
|
- <svg class="dt" viewBox="0 0 ${W} 18" style="width:${W}px;height:18px;flex:none">
|
|
|
- <line x1="4" y1="9" x2="${W-4}" y2="9" stroke="var(--line)" stroke-width="1" stroke-opacity="0.7"/>
|
|
|
- <line x1="${mid}" y1="1" x2="${mid}" y2="17" stroke="var(--muted)" stroke-width="1" stroke-opacity="0.6"/>
|
|
|
- <rect x="${dev>=0?mid:mid-bw}" y="4" width="${Math.max(bw,1.5)}" height="10" rx="2" fill="${col}" fill-opacity="0.9"/>
|
|
|
- ${over?`<path d="M${dev>0?W-5:5},9 l${dev>0?-5:5},-4 v8 z" fill="${col}"/>`:''}
|
|
|
- </svg>
|
|
|
- <b class="small" style="font-variant-numeric:tabular-nums;color:${col}">${dev>0?'+':''}${dev}K</b></div>
|
|
|
- <div class="small" style="color:var(--muted);margin-top:1px">${esc(ch||'')}</div>`};
|
|
|
-// 证据窗时间条: 固定 2026-01→09 轴, 块=claim 窗覆盖段 (跨行可比, 一眼看窗对不对得上)
|
|
|
-const winBar=(spans,raw)=>{
|
|
|
- if(!spans||!spans.length) return `<span class="small" title="${esc(raw)}">未标时间窗</span>`;
|
|
|
- const T0=Date.parse('2026-01-01'), T1=Date.parse('2026-09-01'), W=118;
|
|
|
- const px=d=>{const v=Date.parse(d); return 3+Math.max(0,Math.min(1,(v-T0)/(T1-T0)))*(W-6)};
|
|
|
- const blocks=spans.map(([a,b])=>{const x1=px(a), x2=px(b);
|
|
|
- return `<rect x="${x1.toFixed(1)}" y="4" width="${Math.max(x2-x1,2.5).toFixed(1)}" height="10" rx="2" fill="var(--accent)" fill-opacity="0.8"/>`}).join('');
|
|
|
- return `<div title="${esc(raw)}"><svg class="dt" viewBox="0 0 ${W} 18" style="width:${W}px;height:18px">
|
|
|
- <rect x="3" y="4" width="${W-6}" height="10" rx="2" fill="var(--line)" fill-opacity="0.5"/>${blocks}</svg>
|
|
|
- <div class="small" style="color:var(--muted)">${spans.length} 个窗段</div></div>`};
|
|
|
-// 油液时效胶囊: 绿=近共时可作现时判据 / 灰=stale 只作回测锚
|
|
|
-const oilPill=(last,fresh,raw)=>`<div title="${esc(raw)}" style="display:flex;align-items:center;gap:6px">
|
|
|
- <i style="width:9px;height:9px;border-radius:50%;flex:none;background:${fresh?'var(--ok)':'var(--unknown)'}"></i>
|
|
|
- <span class="small" style="font-variant-numeric:tabular-nums">${last||'无样'}</span></div>
|
|
|
- <div class="small" style="color:var(--muted)">${last?(fresh?'近共时':'stale >12 个月'):'—'}</div>`;
|
|
|
-// 术语去黑话 (2026-08-28 用户令"翻译·审核黑话"): 内部判级词/口径缩写 → 现场话, 括注保留原词以便对账
|
|
|
-const JARGON=[[/正样本/g,'健康对照台'],[/可降(?![级])/g,'可降级观察'],[/记基线观察/g,'记基线·下轮复核'],
|
|
|
- [/CLAIM窗|claim窗/g,'证据窗'],[/位次([0-9]+)[/]([0-9]+)/g,'全场第$1名(共$2台)'],[/未列 [((]≠正常[^))]*[))]/g,'未收录 (≠已排除异常)'],
|
|
|
- [/温度候选[(]定向查[)]/g,'温度偏高·待定向排查'],[/最大偏/g,'偏差最大通道'],[/单次([0-9]+)s/g,'单次循环 $1 秒'],
|
|
|
- [/时间窗内无样/g,'本时间窗未取样'],[/仲裁输入/g,'待仲裁输入'],[/无新证/g,'本轮无新证据'],[/降观察/g,'降为观察'],
|
|
|
- [/弱同向新证/g,'弱同向新证据'],[/传感器项/g,'测量链(仪器)问题'],[/份样/g,' 份油样'],[/NBM/g,'基线模型(NBM)']];
|
|
|
-// 表述规范单源 = windcms agent.py 的业主口径注解 (2026-08-28 用户令: 等级评价和表述一定要参照 windcms)。
|
|
|
-// 规范原文: 状态等级打头(优秀=未见异常保持常规监视 / 良好=轻微偏离记录观察 / 报警=存在需现场核实或处置的异常 /
|
|
|
-// 危险=异常严重建议尽快停机检查 / 不可判=数据不足或监测中断); 证据状态放括号里说可信度
|
|
|
-// (候选=待现场核实 / 准定论·预警=基本确认 / 候选·新发=新出现待核实 / 候选·记基线=已记为基线下窗观察 /
|
|
|
-// 参考=仅记录 / INSUFFICIENT=数据不足 / 撤回=已取消)。
|
|
|
-// ★不自造措辞: 之前我译的"待现场核实/观察即可"虽通顺但偏离了 windcms 词表, 已撤。
|
|
|
-const ST_NOTE = {'优秀':'未见异常, 保持常规监视', '良好':'轻微偏离, 记录观察',
|
|
|
- '报警':'存在需现场核实或处置的异常', '危险':'异常严重, 建议尽快停机检查', '不可判':'数据不足或监测中断'};
|
|
|
-const EV_NOTE = {'候选':'待现场核实', '准定论·预警':'基本确认', '候选·新发':'新出现待核实',
|
|
|
- '候选·记基线':'已记为基线, 下窗观察', '参考':'仅记录', 'INSUFFICIENT':'数据不足',
|
|
|
- '定论':'已定案', '正常':'未见异常', '撤回':'已取消'};
|
|
|
-const plain = t => String(t || ''); // 判级词一律用 windcms 原词, 注解走 ST_NOTE/EV_NOTE
|
|
|
-// 振动技术速记 → 人话 (2026-08-28 用户令: 黑话改掉)。只翻术语不动数字, 原文进悬停。
|
|
|
-// 顺序敏感: 长词先于短词 (DE/NDE 要先于 DE), 否则会被短词切碎。
|
|
|
-const VIBJARGON = [
|
|
|
- [/DE[/]NDE/g, '驱动端与非驱动端'], [/GenDE/g, '发电机驱动端'], [/GenNDE/g, '发电机非驱动端'],
|
|
|
- [/HS_gen2?/g, '高速轴发电机侧'], [/HS_rot/g, '高速轴转子侧'], [/IMS/g, '中间轴'],
|
|
|
- [/(?<![A-Za-z])NDE(?![A-Za-z])/g, '非驱动端'], [/(?<![A-Za-z])DE(?![A-Za-z])/g, '驱动端'],
|
|
|
- // 复合词先于单词: 原文"内圈BPFI"若先翻 BPFI 会变成"内圈内圈特征频率"
|
|
|
- [/内圈BPFI/g, '内圈特征频率'], [/外圈BPFO/g, '外圈特征频率'],
|
|
|
- [/BPFI/g, '内圈特征频率'], [/BPFO/g, '外圈特征频率'], [/FTF/g, '保持架特征频率'], [/BSF/g, '滚动体特征频率'],
|
|
|
- [/TD_1R/g, '时域单转指标'], [/Peak/g, '冲击峰值'], [/Rms/gi, '有效值'],
|
|
|
- [/([0-9]+(?:[.][0-9]+)?)[ ]*阶/g, '$1 倍轴频'], [/[(]o([0-9]+(?:[.][0-9]+)?)[)]/g, '(轴频 $1 倍)'],
|
|
|
- [/×p90/g, '×全场90分位'], [/×fleet/g, '×全场中位'],
|
|
|
- [/六窗/g, '六个观测窗'], [/([0-9])窗/g, '$1 个观测窗'],
|
|
|
- [/宽带/g, '宽频带总量'], [/台阶/g, '阶跃上升'], [/缓升/g, '缓慢上升'], [/回落/g, '回落至'],
|
|
|
- [/全净|六窗全净/g, '各窗均干净'], [/清洁/g, '干净'], [/回归/g, '回到常态'],
|
|
|
- // 替换文本本身不得含后续规则会再匹配的词 (原写'见族主台'会被下一条的 /族/ 二次替换)
|
|
|
- [/同族/g, '同批次共性 (见该批主台)'], [/族第([^ ]+)台/g, '同批次第$1台'], [/族/g, '批次共性组'],
|
|
|
- [/gain异常/g, '测点增益异常'], [/gain/g, '测点增益'],
|
|
|
- [/[(]销[)]/g, '(已销案)'], [/梯度/g, '沿链梯度最高点'],
|
|
|
- [/周期主导/g, '周期成分主导'], [/急性事件/g, '突发事件'], [/慢性/g, '长期'],
|
|
|
- [/主异常在/g, '主要异常位于'],
|
|
|
-];
|
|
|
-const vibPlain = t => { let x = String(t || ''); VIBJARGON.forEach(([a, b]) => x = x.replace(a, b)); return x; };
|
|
|
-const zh=t=>{let x=String(t==null?'':t); JARGON.forEach(([a,b])=>x=x.replace(a,b)); return x};
|
|
|
-// 族分布堆叠条 (报警列)
|
|
|
-// 族分布条 (2026-08-28 用户逮"故障多少长度都一样"):
|
|
|
-// 条总长 = 该台条数/全场最大 → 跨行可比 (原来段宽按台内占比算, 总和恒 100% 所以每行一样长, 量的信息全丢);
|
|
|
-// 段内比例 = 族构成。总长封顶 118px, 最短 6px 保证可见。
|
|
|
-const famBar=(fam,n,maxN)=>{
|
|
|
- if(!n) return `<span class="small" style="color:var(--muted)">时间窗内无报警</span>`;
|
|
|
- const CO={'齿轮箱':'#8A6D3B','发电机':'#4A6FA5','主轴承':'#2E7D6B','变桨叶片':'#4A6FA5','偏航':'#2E7D6B','测风':'#7C5295','其他':'#5A6470'};
|
|
|
- const W=118, w=Math.max(6, Math.round(n/Math.max(maxN||n,1)*W));
|
|
|
- return `<div title="${n} 条 · 全场最多 ${maxN||n} 条" style="display:flex;height:9px;border-radius:3px;overflow:hidden;width:${w}px">`
|
|
|
- +fam.map(f=>`<i title="${f.k} ${f.n}" style="width:${(f.n/n*100).toFixed(1)}%;background:${CO[f.k]||'#5A6470'}"></i>`).join('')
|
|
|
- +`</div><div class="small" style="color:var(--muted);margin-top:3px">${fam.slice(0,2).map(f=>f.k+' '+f.n).join(' · ')}</div>`};
|
|
|
-const PART2SYS=p=>/主轴承|主轴/.test(p)?'主轴承':/齿轮|行星|中间|中速|高速/.test(p)?'齿轮箱':/发电机/.test(p)?'发电机':p;
|
|
|
-// ────────────────────────────────────────────────────────────────────────
|
|
|
-// 报表汇报端口 (2026-08-28 用户令: 按客户汇报管理要求做定制化报表端口, 一级显示, ontology 支持)
|
|
|
-//
|
|
|
-// 范式不是自创的 — 国内风电场上报集团的月度运行分析报告结构是固定的:
|
|
|
-// 发电完成 → 设备运行 → 故障分析 → 限电情况 → 检修执行 → 重点机组 → 下期计划。
|
|
|
-// 端口做三件事: ①选范式与周期 ②勾章节(定制化) ③选受众(决定脱敏级别)。
|
|
|
-// "一级显示" = 顶部六个核心指标用大字直出, 明细下沉到章节; 领导只看第一屏也拿得到结论。
|
|
|
-// 每个数字带口径与来源, 结论可回溯到本体对象 — 这是它区别于"手搓 PPT"的地方。
|
|
|
-// ────────────────────────────────────────────────────────────────────────
|
|
|
-// 报表画布 (2026-08-28 用户令: "像画布一样让客户选, 不是放太多信息")
|
|
|
-//
|
|
|
-// 改法: 从"满配 + 关掉不要的"翻成"空画布 + 挑要的"。前者默认给一大堆再让人做减法,
|
|
|
-// 客户看到的是负担; 后者每加一块都是一次主动选择, 报告长度天然由客户控制。
|
|
|
-// 组件粒度也从"章"降到"块" — 一张图/一张表就是一块, 挑的时候看得见自己在放什么。
|
|
|
-// 范式(月报/周报/专题/年度) 退化为**预设组合**: 点一下把一组块填进画布, 之后照样能加减调序。
|
|
|
-// 组件按主题分组 (2026-08-28 用户"交互界面设计一下"): 15 块平铺成两行时,
|
|
|
-// 客户要在一堆同质按钮里找, 且原生 title 弹出会遮住旁边两个按钮 (截图逮)。
|
|
|
-// 改为按报告的叙事顺序分五组, 说明走固定说明区不浮动。
|
|
|
-const RPT_GROUPS = [['发电', 'gen'], ['可靠性', 'rel'], ['故障', 'flt'], ['机组', 'unit'], ['收尾', 'end']];
|
|
|
-const RPT_BLOCKS = {
|
|
|
- kpi: {g:'gen', nm:'一级指标', ic:'▦', d:'六个核心数: 上网电量 / 等效满发 / 可用率 / 损失率 / 停机间隔 / 需行动台'},
|
|
|
- water: {g:'gen', nm:'能量瀑布', ic:'▤', d:'理论可发 → 逐段扣损 → 实际上网, 用于定位电量流向'},
|
|
|
- emon: {g:'gen', nm:'逐月发电表', ic:'▥', d:'逐月上网 / 损失 / 损失率 / 等效满发, 含数据覆盖率'},
|
|
|
- blame: {g:'gen', nm:'损失责任分账', ic:'◧', d:'交易调度侧 / 设备侧 / 运行侧 三段占比 — 回答"谁的责任"'},
|
|
|
- ram: {g:'rel', nm:'可靠性象限', ic:'◈', d:'平均无故障间隔(MTBF)× 单次停机时长(MDT)部件象限, 分出"频发易修"与"罕发难修", 处置方向相反'},
|
|
|
- parts: {g:'rel', nm:'部件停机表', ic:'▤', d:'逐部件停机时长 / 事件数 / 平均无故障间隔(MTBF)/ 单次停机时长(MDT)/ 影响机组数'},
|
|
|
- attrib: {g:'rel', nm:'归因覆盖', ic:'▬', d:'已归因 vs 无记录停机的占比 — 无记录不等于没有故障'},
|
|
|
- pareto: {g:'flt', nm:'故障帕累托', ic:'▮', d:'按条数降序 + 累计 80% 线, 找关键少数'},
|
|
|
- quad: {g:'flt', nm:'故障四象限', ic:'◇', d:'条数 × 单次时长, 区分重复触发与实际停机 — 按条数排序会误定优先级'},
|
|
|
- frate: {g:'flt', nm:'月度故障率', ic:'▪', d:'按天归一的日均条数, 数据不满月单独标注'},
|
|
|
- units: {g:'unit', nm:'重点机组', ic:'☰', d:'需跟踪台的部件 / 证据源 / 六步进度 / 卡在哪一步'},
|
|
|
- chain: {g:'unit', nm:'决策链进度', ic:'⛓', d:'证据→机制→判级→排期→动作→验收 六步的瓶颈分布'},
|
|
|
- loop: {g:'unit', nm:'闭环实证', ic:'✓', d:'检出 → 换件 → 复测回落的历史例证, 证明这条链是通的'},
|
|
|
- plan: {g:'end', nm:'下期建议', ic:'✎', d:'由数据推出的行动项, 不是套话'},
|
|
|
- caliber:{g:'end', nm:'口径与边界', ic:'⚑', d:'统计期 / 残月 / 工单窗 / 判级性质 — 建议常驻, 没有边界的数字堆不能作汇报依据'},
|
|
|
- narr: {g:'end', nm:'运行总结', ic:'✍', d:'本地模型基于实测数据生成的运行总结 — 为图表补充文字结论, 供汇报使用'},
|
|
|
-};
|
|
|
-const RPT_PRESETS = {
|
|
|
- month: {nm:'月度运行分析', blocks:['kpi','water','emon','blame','ram','pareto','units','plan','caliber']},
|
|
|
- week: {nm:'周报', blocks:['kpi','frate','units','plan','caliber']},
|
|
|
- topic: {nm:'设备专题', blocks:['units','chain','ram','loop','caliber']},
|
|
|
- year: {nm:'年度总结', blocks:['kpi','water','emon','ram','loop','caliber']},
|
|
|
- blank: {nm:'空白画布', blocks:[]},
|
|
|
-};
|
|
|
-function rptInit(){
|
|
|
- if(!window.RPTAUD) window.RPTAUD='in';
|
|
|
- // 默认空画布 (2026-08-28 用户令"都去掉, 只剩选项栏"): 打开只有选项栏, 一块都不预填。
|
|
|
- // 起手给几块仍是替客户做决定 — 画布的意思就是空着等人放东西。
|
|
|
- if(!window.RPTCANVAS) window.RPTCANVAS=[];
|
|
|
-}
|
|
|
-// 报告定制交互 (2026-08-28 用户令: 留一个互动交互窗, 报告按定制, 离线模型支持)。
|
|
|
-// 模型做两件事: 按需求挑组件 + 据实测摘要写总结。数字全部来自后端摘要, 生成后过接地闸;
|
|
|
-// 模型自己算的派生值(如占比求和)会被标出来让人核 — 算得对也要标, 因为核不了的数字不能进报告。
|
|
|
-// ★下拉原来写死两个**本地**模型 (qwen3:8b / qwen3.8:27b), 而云端部署上一个都没配;
|
|
|
-// /api/rpt_compose 在云端的问答微服务里也不存在 (只有 ask / ask_status / ask_models)。
|
|
|
-// 于是这个按钮在生产环境是死的: 点了拿回 404 的 {"err":"not found"}, 显示成 "not found"。
|
|
|
-// 改为按**实际可用**填充: 有编排接口才启用, 模型清单与问答助手同源 (/api/ask_models)。
|
|
|
-// 交付件不该摆一个点了就报错的按钮。
|
|
|
-// ★不能叫 rptInit —— 上面 L1528 已经有一个同名函数 (初始化 RPTAUD / RPTCANVAS)。
|
|
|
-// 函数声明后者覆盖前者, 于是 RPTAUD 永远初始化不了, 渲染直接 ReferenceError,
|
|
|
-// 整个汇报定制面板消失。命名撞车只在运行时才炸, 语法闸看不见。
|
|
|
-// ★模型下拉必须按**实际配了密钥的**填, 不能把 MODEL_SPEC 全列出来 ——
|
|
|
-// MODEL_SPEC 是"档位说明表"(含本地档), 不是"可用清单"。全列出来并默认选第一个
|
|
|
-// (qwen3:8b, 本地档) 的后果: 用户一提问就报「模型 qwen3:8b 未配置密钥」。
|
|
|
-// 云主机 2 核 1GB, 本地模型本来就跑不起来。可用清单唯一来源 = /api/ask_models。
|
|
|
-async function fillModels(selId){
|
|
|
- const sel=document.getElementById(selId); if(!sel) return {};
|
|
|
- let models={};
|
|
|
- try{ models=(await (await fetch('/api/ask_models')).json()).models||{} }catch(e){}
|
|
|
- const ks=Object.keys(models);
|
|
|
- sel.innerHTML = ks.length
|
|
|
- ? ks.map(k=>{const v=MODEL_SPEC[k]||{}; return `<option value="${k}">${v.nm||k}${v.loc?' · '+v.loc:''}${v.t?' · '+v.t:''}</option>`}).join('')
|
|
|
- : '<option value="">无可用模型</option>';
|
|
|
- return models;
|
|
|
-}
|
|
|
-async function askModelInit(){
|
|
|
- await fillModels('askm');
|
|
|
- if(typeof mdlNote==='function') mdlNote();
|
|
|
-}
|
|
|
-async function rptModelInit(){
|
|
|
- const sel=document.getElementById('rptmdl'), btn=document.getElementById('rptgo'),
|
|
|
- out=document.getElementById('rptout');
|
|
|
- if(!sel) return;
|
|
|
- let hasCompose=false;
|
|
|
- const models=await fillModels('rptmdl');
|
|
|
- // 编排已移植上云 (2026-09-01), 故改回**真探测** —— 拦截器已把 rpt_compose 放行到
|
|
|
- // 真网络, 探得到 200 就启用, 离线打开探不到就禁用并说明。
|
|
|
- try{ const r=await fetch('/api/rpt_compose?probe=1'); hasCompose = !!(r&&r.ok) }catch(e){ hasCompose=false }
|
|
|
- const ks=Object.keys(models);
|
|
|
- // ★这句原文是「本地模型…数据不出本地」—— 选 DeepSeek 时数据当然出本地。
|
|
|
- // 与徽章同一判据: 按选中档位的 loc 给话, 不写死。
|
|
|
- const hint=document.getElementById('rpthint');
|
|
|
- const syncHint=()=>{ if(!hint) return;
|
|
|
- const v=MODEL_SPEC[sel.value]||{};
|
|
|
- hint.innerHTML='模型依据需求选取组件, 并基于实测数据生成运行总结'
|
|
|
- + (v.loc==='本地' ? '; 数据不出本地'
|
|
|
- : '; <span style="color:var(--warn)">内容会外发到模型厂商</span>'); };
|
|
|
- sel.addEventListener('change', syncHint); syncHint();
|
|
|
- if(!hasCompose||!ks.length){
|
|
|
- if(btn){ btn.disabled=true; btn.style.opacity='.5'; btn.style.cursor='not-allowed' }
|
|
|
- if(out) out.innerHTML = !ks.length ? '本部署未配置任何模型密钥, 编排不可用'
|
|
|
- : '本部署未启用编排接口, 画布仍可手动拼装';
|
|
|
- }
|
|
|
-}
|
|
|
-async function rptCompose(){
|
|
|
- const need=(document.getElementById('rptneed')||{}).value||'';
|
|
|
- const mdl=(document.getElementById('rptmdl')||{}).value||'qwen3:8b';
|
|
|
- const out=document.getElementById('rptout');
|
|
|
- if(!need.trim()){ out.innerHTML='先输入报告要求'; return }
|
|
|
- const _sp=MODEL_SPEC[mdl]||{}, _loc=(_sp.loc==='本地');
|
|
|
- out.innerHTML='<span style="color:var(--accent)">模型编排中…</span> 挑组件 + 写总结'
|
|
|
- + (_loc ? ', 全程零外发' : ', <span style="color:var(--warn)">内容会外发到模型厂商</span>');
|
|
|
- let d;
|
|
|
- try{ d=await (await fetch(`/api/rpt_compose?need=${encodeURIComponent(need)}&canvas=${encodeURIComponent(RPTCANVAS.join(','))}&model=${encodeURIComponent(mdl)}`)).json(); }
|
|
|
- catch(e){ out.innerHTML=`<span style="color:var(--bad)">生成失败: ${e.message}</span>`; return }
|
|
|
- if(d.err){ out.innerHTML=`<span style="color:var(--bad)">${d.err}</span>`; return }
|
|
|
- window.RPTNARR={text:d.narrative, model:d.model, ungrounded:d.ungrounded||[]};
|
|
|
- const bl=(d.blocks||[]).filter(b=>RPT_BLOCKS[b]);
|
|
|
- if(bl.length) window.RPTCANVAS = (d.narrative? ['narr'] : []).concat(bl);
|
|
|
- else if(d.narrative && !RPTCANVAS.includes('narr')) RPTCANVAS.unshift('narr');
|
|
|
- out.innerHTML=`已按需求编排 <b>${bl.length}</b> 块${d.narrative?' + 一段运行总结':''} ·
|
|
|
- ${d.model} 用时 ${d.secs}s${(d.ungrounded||[]).length?` · <span style="color:var(--warn)">${d.ungrounded.length} 个数字待核</span>`:' · 数字全部接地'}`;
|
|
|
- render();
|
|
|
- setTimeout(()=>{const el=document.getElementById('rptout'); if(el&&!el.innerHTML) el.innerHTML='';},50);
|
|
|
-}
|
|
|
-function rptExport(fmt){
|
|
|
- // 导出走后端生成真 .docx/.xlsx: 图表块转成对应数据表, 信息不丢且可核对。
|
|
|
- // 上报版沿用界面同一套脱敏规则, 不需要导出后再手工处理。
|
|
|
- if(!RPTCANVAS.length){ alert('画布是空的 — 先从组件库选内容'); return }
|
|
|
- const n=(window.RPTNARR&&RPTNARR.text)?RPTNARR.text:'';
|
|
|
- const win=(window.D&&D.win)||'2026年';
|
|
|
- const url=`/api/rpt_export?fmt=${fmt}&blocks=${encodeURIComponent(RPTCANVAS.join(','))}`
|
|
|
- +`&aud=${RPTAUD}&win=${encodeURIComponent(win)}&narr=${encodeURIComponent(n.slice(0,4000))}`;
|
|
|
- const a=document.createElement('a'); a.href=url; a.download=''; document.body.appendChild(a); a.click(); a.remove();
|
|
|
-}
|
|
|
-function rptHint(k){
|
|
|
- // 固定说明区而非浮动 tooltip: 原生 title 弹出会盖住相邻按钮 (用户截图逮),
|
|
|
- // 且鼠标一移就消失, 读不完。放在组件库下方定位不动。
|
|
|
- const el=document.getElementById('rpthint'); if(!el) return;
|
|
|
- const v=k?RPT_BLOCKS[k]:null;
|
|
|
- el.innerHTML = v ? `<b style="color:var(--ink)">${v.ic} ${v.nm}</b> — ${v.d}`
|
|
|
- : '悬停查看组件说明 · 已选组件再次点击移出 · 右侧 ↑↓ 调整顺序';
|
|
|
-}
|
|
|
-function rptPreset(k){ window.RPTCANVAS=RPT_PRESETS[k].blocks.slice(); render(); }
|
|
|
-function rptAdd(k){ if(!RPTCANVAS.includes(k)){ RPTCANVAS.push(k); render(); } }
|
|
|
-function rptDel(k){ const i=RPTCANVAS.indexOf(k); if(i>=0){ RPTCANVAS.splice(i,1); render(); } }
|
|
|
-function rptMove(k,dir){
|
|
|
- const i=RPTCANVAS.indexOf(k), j=i+dir;
|
|
|
- if(i<0||j<0||j>=RPTCANVAS.length) return;
|
|
|
- [RPTCANVAS[i],RPTCANVAS[j]]=[RPTCANVAS[j],RPTCANVAS[i]]; render();
|
|
|
-}
|
|
|
-function rptK(v,u,lab,note,cls){
|
|
|
- return `<div class="kpi ${cls||''}"><b>${v}<span> ${u}</span></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=(t,b,src)=>`<div class="card mb"><h3>${t}</h3>${b}
|
|
|
- ${src?`<p class="small" style="color:var(--muted);margin-top:8px;padding-top:7px;border-top:1px solid var(--line-2)">口径: ${src}</p>`:''}</div>`;
|
|
|
- if(k==='kpi'){
|
|
|
- const last=em[em.length-1]||{}, prev=em[em.length-2]||{};
|
|
|
- const cmpOK=(last.cov||0)>=0.5 && (prev.cov||0)>=0.5;
|
|
|
- const dl=(a,b)=>b?((a-b)/b*100):0;
|
|
|
- return `<div class="kpis" style="margin-bottom:14px">
|
|
|
- ${rptK((E.act||0).toLocaleString(),'MWh','上网电量',`理论可发 ${(E.theo||0).toLocaleString()} | ${cmpOK?`环比 ${dl(last.act,prev.act)>=0?'+':''}${dl(last.act,prev.act).toFixed(0)}%`:'环比不可比(末月不满)'}`)}
|
|
|
- ${rptK((E.eflh||0).toLocaleString(),'h','等效满发小时',`年化约 ${Math.round((E.eflh||0)/Math.max(em.filter(x=>x.cov>=0.5).length,1)*12).toLocaleString()} h`)}
|
|
|
- ${rptK(d.control&&d.control.avail,'%','时间可用率','不含调度令与低风待机',(d.control&&d.control.avail>=95)?'ok':'warn')}
|
|
|
- ${rptK(E.loss_pct,'%','损失率',`${(E.loss||0).toLocaleString()} MWh`,(E.loss_pct>=15)?'warn':'')}
|
|
|
- ${rptK(M?M.MTBF_h:'—','h','平均停机间隔',`停机 ${M?M.停机事件数:'—'} 起 · 单次均 ${M?M.MDT_h:'—'}h`)}
|
|
|
- ${rptK((BD.rows||[]).length,'台','需行动机组',`定论 ${(FU.kpi||{}).定论||0} · 预警 ${(FU.kpi||{}).预警||0} · 候选 ${(FU.kpi||{}).候选观察||0}`,(BD.rows||[]).length>20?'warn':'')}</div>`;
|
|
|
- }
|
|
|
- if(k==='water') return wrap('发电完成情况', waterfall(E)+
|
|
|
- `<p class="small" style="color:var(--muted);margin-top:6px">理论可发 ${(E.theo||0).toLocaleString()} = 上网 ${(E.act||0).toLocaleString()} + 损失 ${(E.loss||0).toLocaleString()} MWh (${E.loss_pct}%)
|
|
|
- | <span title="不是物理判据: 正常数据下必然通过。它的作用是当损失被高估到不可能时(台账数字虚高曾把损失写成超过全停上限)立刻拦下">量级自检</span>: 理论 ${E.cap_ok?'低于':'≥'} 全额定上限 ${(E.cap||0).toLocaleString()} MWh ${E.cap_ok?'✔':'✗ 数字不可信'}</p>`,
|
|
|
- 'SCADA 10min 工况态分解; 损失=同工况理论出力与实测之差。量级自检为防虚高的下限闸, 不是风资源判据');
|
|
|
- if(k==='emon') return wrap('逐月发电', em.length?`<table><tr><th>月</th><th class="num">上网 MWh</th><th class="num">损失 MWh</th><th class="num">损失率</th><th class="num">等效满发 h</th><th>覆盖</th></tr>
|
|
|
- ${em.map(x=>`<tr><td>${x.m}</td><td class="num">${x.act.toLocaleString()}</td><td class="num">${x.loss.toLocaleString()}</td><td class="num">${x.loss_pct}%</td><td class="num">${x.eflh}</td>
|
|
|
- <td class="small" style="color:${x.cov<0.5?'var(--warn)':'var(--muted)'}">${(x.cov*100).toFixed(0)}%${x.cov<0.5?' 不满月':''}</td></tr>`).join('')}</table>`:'—',
|
|
|
- '覆盖率<50% 的月不参与环比');
|
|
|
- if(k==='blame') return wrap('损失责任分账', blameBar(E)+
|
|
|
- `<p class="small" style="color:var(--muted);margin-top:8px">进设备考核的只有"故障与检修停机"; 限电属交易与调度侧。
|
|
|
- <br><b>限电不是"低风时段收到的空令"</b>: 限电·绑定时段平均出力 1.435 MW, 正常发电时段 1.412 MW, 比值 1.02 —
|
|
|
- 风况相同而出力被压, 说明限电是真压制; 但"限电命令期·功率未被压制"那一档平均出力仅 0.724 MW(18% 额定), 确属低风时段收令。</p>`,
|
|
|
- '工况态分解 + 限电命令面绑定判别; 平均出力=实发÷台时, 用作风资源代理量');
|
|
|
- if(k==='ram') return R?wrap('可靠性 × 可维修性', ramChart(R.表)+
|
|
|
- `<p class="small" style="color:var(--muted);margin-top:6px">设备类 ${R.设备类停机h}h / 外部类 ${R.外部类停机h}h (电网·风况·运维操作不计设备可靠性)</p>`,
|
|
|
- 'SCADA ≥1h 停机段, 不含调度令; 海上单次停机时长(MDT)含交通窗口'):'';
|
|
|
- if(k==='parts') return R?wrap('部件停机', `<table><tr><th>部件</th><th class="num">停机h</th><th class="num">事件</th><th class="num">平均无故障间隔(MTBF,h)</th><th class="num">单次停机时长(MDT,h)</th><th class="num">影响机组</th></tr>
|
|
|
- ${R.表.slice(0,9).map(x=>`<tr><td class="small">${x.部件}${x.外部?' (外部)':''}</td><td class="num">${x.停机时长h}</td><td class="num">${x.事件数}</td>
|
|
|
- <td class="num">${x.MTBF_h||'—'}</td><td class="num">${x.MDT_h}</td><td class="num">${x.影响台数}</td></tr>`).join('')}</table>`,
|
|
|
- '归因=段起点±30min 首发码'):'';
|
|
|
- if(k==='attrib') return R?wrap('归因覆盖', attribBar(R.表,R.总停机h)+
|
|
|
- `<p class="small" style="color:var(--muted);margin-top:6px">"无记录停机"不是没有故障, 是没归上因。</p>`,''):'';
|
|
|
- if(k==='pareto') return wrap('故障帕累托', paretoChart(F.pareto_n||[],' 条',''), '报警事件表');
|
|
|
- if(k==='quad') return wrap('故障模式四象限', quadChart(F.quad||[])+
|
|
|
- `<p class="small" style="color:var(--muted);margin-top:6px">右下角报警码属重复触发, 不应按条数排定优先级。</p>`,'纵界=10min(一个SCADA采样格)');
|
|
|
- if(k==='frate') return wrap('月度故障率', rateChart(F.monthly||{months:[],vals:[]})+
|
|
|
- `<p class="small" style="color:var(--muted);margin-top:6px">柱高=日均条数, 不是月总数。</p>`,'不满月按天归一');
|
|
|
- if(k==='units'){
|
|
|
- const rows=(BD.rows||[]).slice(0,14);
|
|
|
- return wrap('重点机组与处置', `<p class="small" style="color:var(--muted);margin-bottom:8px">需跟踪 ${(BD.rows||[]).length} 台 / 38 | 瓶颈 ${Object.entries(BD.stuck||{}).map(([a,b])=>`<b>${b} 台</b>卡在「${a}」`).join(' · ')}</p>
|
|
|
- <table><tr><th style="width:52px">机组</th><th style="width:64px">状态</th><th>部件与证据</th><th style="width:56px">进度</th><th style="width:62px">卡在</th></tr>
|
|
|
- ${rows.map(r=>`<tr><td><b>${r.t.replace('WTG','')}#</b></td><td>${chip(r.ostate||'—')}</td>
|
|
|
- <td class="small" style="color:var(--muted)">${r.part&&r.part.indexOf('未收录')<0?`<b style="color:var(--ink)">${r.part}</b> · `:''}${(r.srcs||[]).join('/')||'—'}${r.rising?` · <span style="color:var(--bad)">在升 ${r.rising.ratio}×</span>`:''}</td>
|
|
|
- <td class="num small">${r.done}/6</td><td class="small" style="color:var(--warn)">${r.stuck||'全通'}</td></tr>`).join('')}</table>`,
|
|
|
- '四源融合判级');
|
|
|
- }
|
|
|
- if(k==='chain') return wrap('决策链进度', `<p class="small" style="color:var(--muted)">六步: 证据 → 机制 → 判级 → 排期 → 动作 → 验收。
|
|
|
- 当前 ${Object.entries(BD.stuck||{}).map(([a,b])=>`${b} 台卡在「${a}」`).join(', ')}。</p>`,
|
|
|
- '「动作」一列不可判 — ' + (BD.wo_bound||''));
|
|
|
- if(k==='loop'){
|
|
|
- const cl=BD.closed||[];
|
|
|
- return wrap('端到端闭环实证', `<table><tr><th>机组</th><th>部件</th><th>换件日期</th><th class="num">换前</th><th class="num">换后</th><th class="num">比值</th></tr>
|
|
|
- ${cl.map(x=>`<tr><td><b>${x.turbine.replace('WTG','')}#</b></td><td class="small">${x.component}</td><td class="small">${x.replace_date}</td>
|
|
|
- <td class="num">${x.pre}</td><td class="num">${x.post}</td><td class="num" style="color:var(--ok)"><b>${x.ratio}×</b></td></tr>`).join('')}</table>
|
|
|
- <p class="small" style="color:var(--muted);margin-top:6px">这几台正是算法当初的检出台 ⇒ 故障前能看见、换件后能看见恢复。</p>`,
|
|
|
- '振动标量前后对比');
|
|
|
- }
|
|
|
- if(k==='plan') return wrap('下期计划与建议', `<ol style="margin:0 0 0 20px;line-height:1.85">
|
|
|
- <li><b>补机制定性</b> — ${(BD.stuck||{}).机制||0} 台停在"判了级但根因未定", 是当前最大单一瓶颈</li>
|
|
|
- <li><b>回收 25~26 工单</b> — 台账止 2024-11, 本期检修执行与闭环无法自动判定</li>
|
|
|
- <li><b>油液采样节奏</b> — 四源里油液长期 stale, 只能作回测锚</li>
|
|
|
- <li><b>限电侧</b> — 损失六成在交易与调度侧, 设备侧改善空间有限</li></ol>`,
|
|
|
- '建议级 — 排期与动作须经审批');
|
|
|
- if(k==='narr'){
|
|
|
- const N=window.RPTNARR;
|
|
|
- if(!N||!N.text) return wrap('运行总结','<p class="small" style="color:var(--muted)">还没有生成 — 在上面的交互窗输入报告要求。</p>','');
|
|
|
- return wrap('运行总结', `<p style="line-height:1.9;margin:0">${N.text.replace(/</g,'<').split(String.fromCharCode(10)).join('<br>')}</p>
|
|
|
- ${(N.ungrounded||[]).length?`<p class="small" style="color:var(--warn);margin-top:8px">
|
|
|
- ⚠ 以下数值由模型自行推算, 不在原始数据摘要中, 引用前须核对: ${N.ungrounded.join(' / ')}</p>`:''}`,
|
|
|
- `本地模型 ${N.model} 生成 · 数字取自系统实测摘要 · 接地闸${(N.ungrounded||[]).length?'报 '+N.ungrounded.length+' 处待核':'全通过'}`);
|
|
|
- }
|
|
|
- if(k==='caliber'){
|
|
|
- const part=(F.partial||[]);
|
|
|
- return wrap('口径与数据边界', `<ul style="margin:0 0 0 20px;line-height:1.85">
|
|
|
- <li><b>统计期</b> ${d.win}${part.length?` — ${part.join('/')} 数据不满月(覆盖 ${Math.round((F.cov_min||0)*100)}%), 月度已按天归一`:''}</li>
|
|
|
- <li><b>可用率与损失率分母不同</b> 可用率分母剔除低风待机与调度令(问"该发时能不能发"),
|
|
|
- 损失率分母含全部时段(问"理论能发多少"); 同一低风时段在可用率里不计时间、在损失率里计入基数 —
|
|
|
- 这是两个指标的设计差异不是错, 但两者不可直接相乘或互推。低风待机对损失的贡献仅 118 MWh(0.5%)</li>
|
|
|
- <li><b>停机</b> 可靠性口径 ≥1h 段, 电量账为逐条工况态 — 两个数都对, 不可相减</li>
|
|
|
- <li><b>工单</b> 台账 2020~2024-11, 本报告不对本期检修执行下结论</li>
|
|
|
- <li><b>判级</b> 由振动/温度/润滑/油液四源融合得出; 行动等级由综合报告出具</li>
|
|
|
- ${RPTAUD==='up'?'<li><b>脱敏</b> 上报版隐去判据门槛与特征频率</li>':''}</ul>`,'');
|
|
|
- }
|
|
|
- return '';
|
|
|
-}
|
|
|
-
|
|
|
-function renderReport(d){
|
|
|
- rptInit();
|
|
|
- const FU=d.fus||{};
|
|
|
- let h=`<div class="eyebrow">汇报定制 · 画布</div>`;
|
|
|
- // ── 画布控制条: 预设 / 组件库 / 受众 / 打印 ──
|
|
|
- h+=`<div class="card mb noprint">
|
|
|
- <div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:10px">
|
|
|
- <span class="small" style="font-weight:600">预设</span>
|
|
|
- ${Object.entries(RPT_PRESETS).map(([k,v])=>`<button onclick="rptPreset('${k}')" style="cursor:pointer;padding:5px 13px;border-radius:15px;
|
|
|
- border:1px solid var(--line);background:var(--panel);color:var(--ink);font-size:12.5px">${v.nm}</button>`).join('')}
|
|
|
- <span style="flex:1"></span>
|
|
|
- <select onchange="window.RPTAUD=this.value;render()" style="padding:5px 9px;border:1px solid var(--line);border-radius:7px;background:var(--panel);color:var(--ink);font-size:12.5px">
|
|
|
- <option value="in"${RPTAUD==='in'?' selected':''}>场站内部 (全量)</option>
|
|
|
- <option value="up"${RPTAUD==='up'?' selected':''}>上报集团 (脱敏)</option></select>
|
|
|
- <button onclick="window.print()" style="cursor:pointer;padding:5px 13px;border-radius:7px;border:1px solid var(--accent);background:var(--panel);color:var(--accent);font-weight:600;font-size:12.5px">打印 / 存 PDF</button>
|
|
|
- <button onclick="rptExport('docx')" style="cursor:pointer;padding:5px 13px;border-radius:7px;border:1px solid var(--line);background:var(--panel);color:var(--ink);font-size:12.5px">导出 Word</button>
|
|
|
- <button onclick="rptExport('xlsx')" style="cursor:pointer;padding:5px 13px;border-radius:7px;border:1px solid var(--line);background:var(--panel);color:var(--ink);font-size:12.5px">导出 Excel</button>
|
|
|
- ${RPTAUD==='up'?'<span class="small" style="color:var(--warn);flex-basis:100%">上报版 — 判据门槛与特征频率已脱敏</span>':''}
|
|
|
- </div>
|
|
|
- <div class="small" style="font-weight:600;margin-bottom:7px">组件库
|
|
|
- <span style="font-weight:400;color:var(--muted)">点击加入报告, 再次点击移出 · 画布里已有 <b>${RPTCANVAS.length}</b> 块</span></div>
|
|
|
- <div style="display:grid;grid-template-columns:auto 1fr;gap:6px 12px;align-items:center">
|
|
|
- ${RPT_GROUPS.map(([gn,gk])=>`
|
|
|
- <div class="small" style="color:var(--muted);text-align:right;white-space:nowrap">${gn}</div>
|
|
|
- <div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
|
- ${Object.entries(RPT_BLOCKS).filter(([,v])=>v.g===gk).map(([k,v])=>{const on=RPTCANVAS.includes(k);
|
|
|
- return `<button onmouseenter="rptHint('${k}')" onmouseleave="rptHint()" onfocus="rptHint('${k}')"
|
|
|
- onclick="rpt${on?'Del':'Add'}('${k}')" style="cursor:pointer;padding:5px 11px;border-radius:7px;
|
|
|
- border:1px solid ${on?'var(--accent)':'var(--line)'};background:${on?'var(--accent)':'var(--panel)'};
|
|
|
- color:${on?'#fff':'var(--ink)'};font-size:12.5px;opacity:${on?1:0.78}">${v.ic} ${v.nm}${on?' ✓':''}</button>`}).join('')}
|
|
|
- </div>`).join('')}
|
|
|
- </div>
|
|
|
- <div id="rpthint" class="small" style="color:var(--muted);margin-top:9px;padding-top:8px;border-top:1px solid var(--line-2);min-height:19px">
|
|
|
- 悬停查看组件说明 · 已选组件再次点击移出 · 右侧 ↑↓ 调整顺序</div>
|
|
|
- <div style="margin-top:11px;padding-top:10px;border-top:1px solid var(--line-2)">
|
|
|
- <div class="small" style="font-weight:600;margin-bottom:6px">输入报告要求
|
|
|
- <span id="rpthint" style="font-weight:400;color:var(--muted)">模型依据需求选取组件, 并基于实测数据生成运行总结</span></div>
|
|
|
- <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
|
|
- <input id="rptneed" placeholder="例如: 上报集团月报, 侧重设备可靠性与重点机组"
|
|
|
- onkeydown="if(event.key==='Enter')rptCompose()"
|
|
|
- style="flex:1 1 300px;min-width:220px;padding:7px 12px;border:1px solid var(--line);border-radius:7px;background:var(--panel);color:var(--ink);font-size:13px">
|
|
|
- <select id="rptmdl" style="padding:7px 9px;border:1px solid var(--line);border-radius:7px;background:var(--panel);color:var(--ink);font-size:12.5px"></select>
|
|
|
- <button id="rptgo" onclick="rptCompose()" style="cursor:pointer;padding:7px 18px;border-radius:7px;border:1px solid var(--accent);background:var(--accent);color:#fff;font-weight:600;font-size:13px">生成</button>
|
|
|
- </div>
|
|
|
- <div id="rptout" class="small" style="color:var(--muted);margin-top:7px"></div></div></div>`;
|
|
|
- setTimeout(rptModelInit, 60);
|
|
|
-
|
|
|
- if(!RPTCANVAS.length)
|
|
|
- return h+`<div class="paper" style="text-align:center;padding:56px 40px">
|
|
|
- <div class="small" style="color:var(--muted)">文档预览窗 — 从上面的组件库点选内容, 此处显示报告预览</div></div>`;
|
|
|
-
|
|
|
- // 报表头卡撤 (2026-08-28 用户令): 场名与统计期页眉已有, 数据源列表与"结论可回溯"声明
|
|
|
- // 属口径块的内容 — 摆在报告最上面是重复且抢戏。打印时的抬头由下面这行轻量署名承担。
|
|
|
-
|
|
|
- // ── 文档预览窗 (2026-08-28 用户令: 生成的文档能预览才打印) ──
|
|
|
- // 画布内容包进"一页纸": 有抬头、有边界、有页脚, 所见即将打印出来的样子。
|
|
|
- const _asof=(FU.数据时点||'').slice(0,10);
|
|
|
- h+=`<div class="paper"><div class="paper-hd">
|
|
|
- <div><div class="paper-ti">如东海上风电场 · 运行分析报告</div>
|
|
|
- <div class="small" style="color:var(--muted);margin-top:3px">统计期 ${d.win} | 38 × SWT-4.0-130${RPTAUD==='up'?' | 上报版':''}</div></div>
|
|
|
- <div class="small" style="color:var(--muted);text-align:right">${RPTCANVAS.length} 个章节</div></div>`;
|
|
|
- RPTCANVAS.forEach((k,i)=>{
|
|
|
- const v=RPT_BLOCKS[k]; if(!v) return;
|
|
|
- // 控制条移到块**上方**独立一行 — 原来绝对定位压在卡右上角, 挡住 KPI 数字与图例 (用户截图逮)
|
|
|
- const bb=(t,fn,dis)=>`<button title="${t}" onclick="${fn}" ${dis?'disabled':''} style="cursor:pointer;padding:1px 8px;border-radius:5px;
|
|
|
- border:1px solid var(--line);background:transparent;color:var(--muted);font-size:11px;opacity:${dis?0.3:0.7}">${t}</button>`;
|
|
|
- h+=`<div class="noprint" style="display:flex;gap:3px;justify-content:flex-end;margin-bottom:-6px;position:relative;z-index:2">
|
|
|
- ${bb('↑',`rptMove('${k}',-1)`,i===0)}${bb('↓',`rptMove('${k}',1)`,i===RPTCANVAS.length-1)}${bb('×',`rptDel('${k}')`,false)}
|
|
|
- </div>${rptBlock(k,d)}`;
|
|
|
- });
|
|
|
- h+=`<div class="paper-ft">
|
|
|
- <span>数据源 SCADA 10min · 报警事件 · 工单台账 · CMS 振动 · 油液化验${_asof?` | 出件 ${_asof}`:''}</span>
|
|
|
- <span>结论可回溯至本体对象${RPTAUD==='up'?' | 判据门槛与特征频率已脱敏':''}</span></div></div>`;
|
|
|
- return h;
|
|
|
-}
|
|
|
-
|
|
|
-// 决策链浮窗 (2026-08-28 用户令: 29号点开应该是决策链的具体信息的浮窗)。
|
|
|
-// 盘上一行只给"卡在哪", 点开给这一台的六步全貌: 每步达没达、依据是什么、为什么卡住。
|
|
|
-// 29# 另有本体链(证据清单/机制/四情景/验收树), 其余台给四源证据明细。
|
|
|
-function popChain(t){
|
|
|
- const BD=(D&&D.fus&&D.fus.链盘)||{}; const r=(BD.rows||[]).find(x=>x.t===t); if(!r) return;
|
|
|
- const STC={done:'var(--ok)',open:'var(--warn)',unknown:'var(--muted)'};
|
|
|
- const STT={done:'已达',open:'未达',unknown:'不可判'};
|
|
|
- const num='①②③④⑤⑥';
|
|
|
- // ★列宽按语言取。原来写死 44/52/96px, 是照**两个汉字**量的; 英文 "Verification" /
|
|
|
- // "Not evaluable" 塞不下 ⇒ 标签被截成 "Mechanis"/"Scheduli"/"Verificati", chip 压到
|
|
|
- // 下一列上 (用户截图实逮)。中文页取原值, 一个像素不动。
|
|
|
- const EN=(typeof window!=='undefined'&&window.__EN), W=EN?[92,104,132]:[44,52,96];
|
|
|
- const steps=r.steps.map((x,i)=>{
|
|
|
- const cur=(x.k===r.stuck);
|
|
|
- return `<div style="display:flex;gap:10px;padding:9px 11px;border-radius:8px;margin-bottom:5px;align-items:flex-start;
|
|
|
- border:1px solid ${cur?STC[x.st]:'var(--line)'};background:${cur?'var(--bg)':'transparent'}">
|
|
|
- <div style="flex:none;width:20px;color:var(--muted);font-size:13px">${num[i]}</div>
|
|
|
- <div style="flex:none;width:${W[0]}px;font-weight:600;font-size:12.5px">${x.k}</div>
|
|
|
- <div style="flex:none;width:${W[1]}px"><span class="badge" style="border-color:${STC[x.st]};color:${STC[x.st]};white-space:nowrap">${STT[x.st]}</span></div>
|
|
|
- <div style="flex:none;width:${W[2]}px;font-size:12px">${x.v}</div>
|
|
|
- <div class="small" style="color:var(--muted);flex:1;line-height:1.5">${x.d}${cur?` <b style="color:${STC[x.st]}">← 卡在这里</b>`:''}</div></div>`;}).join('');
|
|
|
- // 与后端 _tbi 同口径: 多行台取最严重那行, 否则浮窗四源卡会指向另一个部件
|
|
|
- const SEV={bad:0,warn:1,note:2,stale:3,ok:4,unlisted:5};
|
|
|
- const F=(D&&D.fus)||{};
|
|
|
- const frow=(F.表||[]).filter(x=>x.turbine===t).sort((a,b)=>(SEV[a.级]??9)-(SEV[b.级]??9))[0];
|
|
|
- let mini='';
|
|
|
- if(frow){
|
|
|
- mini=`<div class="pop-mini">
|
|
|
- <div><b>振动结论</b><span class="badge" style="border-color:${LVC2(frow.级)};color:${LVC2(frow.级)}">${(frow.振动结论||'').slice(0,26)}</span>
|
|
|
- <div class="small" style="color:var(--muted);margin-top:4px">${frow.部件||''}</div></div>
|
|
|
- <div><b>证据窗</b>${winBar(frow.时间窗区间,frow.claim_window)}</div>
|
|
|
- <div><b>温度轴 (fleet相对)</b>${devBar(frow.温度通道,frow.温度dev,frow.温度判,frow.温度轴)}</div>
|
|
|
- <div><b>油液时效</b>${oilPill(frow.油末样,frow.油新鲜,frow.油样轴)}</div></div>`;
|
|
|
- }
|
|
|
- let ext='';
|
|
|
- const C=(typeof ONT!=='undefined'&&ONT)?ONT.chain29:null;
|
|
|
- if(t==='WTG29'&&C){
|
|
|
- const ev=(C.evidence||[]).map(e=>`<div class="small" style="color:var(--muted);padding:4px 0;border-bottom:1px solid var(--line-2)">
|
|
|
- <b style="color:var(--ink)">${e.facet||e.id||''}</b>
|
|
|
- <span style="color:var(--muted)"> 窗 ${e.claim_window||'—'}</span>
|
|
|
- <div style="line-height:1.55;margin-top:2px">${(e.summary||'').toString().slice(0,150)}</div></div>`).join('');
|
|
|
- const sc=(C.scenario&&C.scenario.scenarios)||[];
|
|
|
- ext=`<div style="margin-top:12px">
|
|
|
- <div class="small" style="font-weight:600;margin-bottom:5px">① 证据清单 · ${(C.evidence||[]).length} 条 / ${C.mechanism.independent_source_count} 个观测面</div>
|
|
|
- ${ev}
|
|
|
- <div class="small" style="color:var(--muted);margin-top:5px">四个观测面是<b>同一条机制链的四个面</b>, 不算四个独立源 — 加权时不可当多源印证。</div>
|
|
|
- <div class="small" style="font-weight:600;margin:12px 0 5px">② 机制 · ${C.mechanism.name||''}</div>
|
|
|
- <div class="small" style="color:var(--muted)">${C.mechanism.note||''}</div>
|
|
|
- <div class="small" style="font-weight:600;margin:12px 0 5px">④ 排期沙盘 · ${sc.length} 个情景</div>
|
|
|
- <table style="margin-top:2px"><tr><th>情景</th><th>窗</th><th>作业级</th><th>停机损失</th></tr>
|
|
|
- ${sc.map(x=>`<tr><td class="small"><b>${x.id}</b></td><td class="small">${x['修理窗']||'—'}</td>
|
|
|
- <td class="small" style="color:var(--muted)">${(x['作业级']||'').slice(0,20)}</td>
|
|
|
- <td class="small" style="color:var(--muted)">${((x['停机损失']||{})['电费口径']||'—').slice(0,22)}</td></tr>`).join('')}</table>
|
|
|
- <div class="small" style="font-weight:600;margin:12px 0 5px">⑥ 验收 · ${C.acceptance.state||'待执行'}</div>
|
|
|
- <div class="small" style="color:var(--muted);line-height:1.55">${(C.acceptance.note||'').slice(0,200)}</div></div>`;
|
|
|
- }
|
|
|
- document.body.insertAdjacentHTML('beforeend',
|
|
|
- `<div class="pop-mask" id="popm" onclick="if(event.target.id==='popm')closePop()"><div class="pop-box">
|
|
|
- <div class="pop-hd"><b>${t.replace('WTG','')}# 决策链</b>
|
|
|
- ${chip(r.ostate||'—')}<span class="small" style="color:var(--muted)">走到 ${r.done}/6 · 卡在「${r.stuck||'全通'}」</span>
|
|
|
- <span class="pop-x" onclick="closePop()">×</span></div>
|
|
|
- <div class="pop-bd">
|
|
|
- <p class="small" style="color:var(--muted);margin-bottom:10px">从发现异常到修完验收的六步。
|
|
|
- <b>下一步:</b> ${r.next||'—'}</p>
|
|
|
- ${steps}${mini}${ext}
|
|
|
- <div style="margin-top:12px;display:flex;gap:8px;flex-wrap:wrap">
|
|
|
- <a class="badge" href="/turbine/${t}">该台完整问题页 →</a></div>
|
|
|
- </div></div></div>`);
|
|
|
-}
|
|
|
-async function popPart(t,part){
|
|
|
- const sys=PART2SYS(part), win=(D&&D.win)||'2026年';
|
|
|
- document.body.insertAdjacentHTML('beforeend',
|
|
|
- `<div class="pop-mask" id="popm" onclick="if(event.target.id==='popm')closePop()"><div class="pop-box">
|
|
|
- <div class="pop-hd"><b>${t} · ${part}</b><span class="small" style="color:var(--muted)">载入中…</span>
|
|
|
- <span class="pop-x" onclick="closePop()">×</span></div><div class="pop-bd" id="popbd"></div></div></div>`);
|
|
|
- let figs=[], p0=null;
|
|
|
- try{ const r=await fetch(`/api/problem?t=${t}&sys=${encodeURIComponent(sys)}&win=${encodeURIComponent(win)}`);
|
|
|
- const d=await r.json(); p0=(d.problems||[])[0]; figs=(p0&&p0.figs)||[]; }catch(e){}
|
|
|
- const F=(D&&D.fus)||{}, frow=(F.表||[]).filter(x=>x.turbine===t&&PART2SYS(x.部件)===sys)[0];
|
|
|
- const curves=figs.filter(f=>f.kind!=='text'&&f.kind!=='vib');
|
|
|
- const mini=[];
|
|
|
- if(frow){
|
|
|
- mini.push(`<div><b>振动结论</b><span class="badge" style="border-color:${LVC2(frow.级)};color:${LVC2(frow.级)}">${frow.振动结论}</span>
|
|
|
- <div class="small" style="color:var(--muted);margin-top:4px">${frow.部件}</div></div>`);
|
|
|
- mini.push(`<div><b>证据窗</b>${winBar(frow.时间窗区间,frow.claim_window)}</div>`);
|
|
|
- mini.push(`<div><b>温度轴 (fleet相对)</b>${devBar(frow.温度通道,frow.温度dev,frow.温度判,frow.温度轴)}</div>`);
|
|
|
- mini.push(`<div><b>油液时效</b>${oilPill(frow.油末样,frow.油新鲜,frow.油样轴)}</div>`);
|
|
|
- }
|
|
|
- if(p0) mini.unshift(`<div><b>系统判级</b>${chip(p0['状态']||'—')}<div class="small" style="color:var(--muted);margin-top:4px">${(p0['依据']||'').slice(0,70)}</div></div>`);
|
|
|
- const hd=document.querySelector('#popm .pop-hd span.small');
|
|
|
- if(hd) hd.outerHTML=`<span class="small" style="color:var(--muted)">${sys} · 窗 ${win}</span>`;
|
|
|
- document.getElementById('popbd').innerHTML=
|
|
|
- `<div style="margin-bottom:10px">${dtChainStates({[part.replace(/[((].*/,'')]:LVC2(p0&&p0['状态']==='报警'?'bad':'warn')},260,null)}</div>
|
|
|
- <div class="pop-mini">${mini.join('')}</div>`
|
|
|
- +(curves.length?curves.slice(0,2).map(f=>`<div style="margin-bottom:10px">${drawFig(f)}</div>`).join('')
|
|
|
- :`<p class="small" style="color:var(--muted)">该部件在本时间窗无 SCADA 曲线 (振动谱级证据走 windcms)</p>`)
|
|
|
- +`<p class="small" style="margin-top:8px"><a class="tl" href="/problem/${t}/${encodeURIComponent(sys)}">完整问题页 (全部曲线与工单史) →</a>
|
|
|
- · <a class="tl" href="/turbine/${t}">本台总览 →</a></p>`;
|
|
|
-}
|
|
|
-// 三个场景各自独立提交与展示 (2026-08-28 用户令"都单独放在一个输入窗")。
|
|
|
-// 原来共用一个输入框 + 一个结果区, 切场景就把上一个结果冲掉 —
|
|
|
-// 而实际用法是"先查报警码怎么修, 顺手看这个系统的预防任务和故障树", 三者要并存对照。
|
|
|
-if(!window.ASSTCACHE3) window.ASSTCACHE3={};
|
|
|
-async function asstGo(mode){
|
|
|
- const m=mode||ASSTMODE;
|
|
|
- const out=document.getElementById('asstOut_'+m); if(!out) return;
|
|
|
- const el=document.getElementById('asstIn_'+m); const v=el?el.value.trim():'';
|
|
|
- out.innerHTML=`<div class="card"><p class="small">查询中…</p></div>`;
|
|
|
- const qs={fault:`kind=fault&code=${encodeURIComponent(v)}`,
|
|
|
- prev:`kind=prev&system=${encodeURIComponent(v||'齿轮箱')}`,
|
|
|
- tree:`kind=tree&system=${encodeURIComponent(v||'齿轮箱')}`}[m];
|
|
|
- let d;
|
|
|
- try{ d=await (await fetch('/api/ont_chain?'+qs)).json(); }
|
|
|
- catch(e){ out.innerHTML=`<div class="card"><p class="small" style="color:var(--bad)">查询失败: ${e.message}</p></div>`; return }
|
|
|
- out.innerHTML = (m==='fault' && d.data) ? faultCards(d.data)
|
|
|
- : (m==='tree' && d.data) ? treeCards(d.data)
|
|
|
- : `<div class="card"><pre class="small" style="white-space:pre-wrap;margin:0;line-height:1.75">${(d.text||'').replace(/</g,'<')}</pre></div>`;
|
|
|
- window.ASSTCACHE3[m]=out.innerHTML;
|
|
|
-}
|
|
|
-
|
|
|
-function treeCards(x){
|
|
|
- // 故障树两层: 失效模式(果) ← causedBy ← 部件级原因(因); 条长=实证量, 步骤=手册原文
|
|
|
- const nodes = x.nodes || [];
|
|
|
- if(!nodes.length) return `<div class="card"><p class="small">该系统无失效模式记录</p></div>`;
|
|
|
- const maxE = Math.max(1, ...nodes.map(n => n.n_alarm + n.n_wo * 10));
|
|
|
- let h = `<div class="card mb"><h3>${x.system} 故障树 · ${nodes.length} 个失效模式</h3>
|
|
|
- <p class="small" style="color:var(--muted)">一层=系统×失效类(归纳自本场码表与工单) · 二层=手册排查方向归一+工单原因计数;
|
|
|
- 条长按实证量(报警条数 + 工单×10), 因果来自手册与工单原文非推断, 树深两层不外推。</p></div>`;
|
|
|
- nodes.forEach(n => {
|
|
|
- const w = Math.max(3, (n.n_alarm + n.n_wo * 10) / maxE * 100);
|
|
|
- const col = n.n_wo >= 5 ? 'var(--bad)' : n.n_alarm >= 100 ? 'var(--warn)' : 'var(--info)';
|
|
|
- h += `<div class="card mb"><div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">
|
|
|
- <div style="flex:1;min-width:200px"><b>${n.name}</b>
|
|
|
- <div style="height:7px;border-radius:3px;background:${col};width:${w.toFixed(0)}%;margin-top:5px;max-width:100%"></div>
|
|
|
- <div class="small" style="color:var(--muted);margin-top:4px">本场报警 ${n.n_alarm} 条 · 工单 ${n.n_wo} 条${n.codes.length?` · 征兆码 ${n.codes.join(', ')}`:''}</div>
|
|
|
- </div></div>`;
|
|
|
- if(n.causes && n.causes.length){
|
|
|
- h += `<div style="margin-top:10px;padding-left:14px;border-left:2px dashed var(--line)">
|
|
|
- <div class="small" style="color:var(--muted);margin-bottom:6px">← 可能原因 (手册排查方向 + 工单实证)</div>
|
|
|
- <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:10px">`
|
|
|
- + n.causes.map(c => `<div style="border:1px solid var(--line);border-radius:8px;padding:10px 12px">
|
|
|
- <div><b class="small">${c.name}</b>
|
|
|
- <span class="small" style="color:var(--muted)"> 手册 ${c.n_ref} 次 · 工单 ${c.n_wo} 次 · 涉码 ${c.n_codes}</span></div>
|
|
|
- ${(c.steps||[]).map(st => `<div class="small" style="color:var(--muted);margin-top:5px;line-height:1.6">· ${st}</div>`).join('')}
|
|
|
- </div>`).join('') + `</div></div>`;
|
|
|
- } else {
|
|
|
- h += `<p class="small" style="color:var(--muted);margin-top:8px">(手册无该类排查条目 — 原因层待补)</p>`;
|
|
|
- }
|
|
|
- h += `</div>`;
|
|
|
- });
|
|
|
- return h;
|
|
|
-}
|
|
|
-
|
|
|
-function maintCards(md){
|
|
|
- // 数据维护面: 两层分开 (混在一起会让"机理被数据覆盖" — 更新频率与责任方完全不同)
|
|
|
- const rows = md.rows || [];
|
|
|
- if(md.error) return `<div class="card"><p class="small" style="color:var(--bad)">${md.error}</p></div>`;
|
|
|
- const LAYER = [['数据','随运营滚动 — 新数据到位后跑摄入, 本体与判级自动刷新','var(--accent)'],
|
|
|
- ['机理','相对稳定 — 换厂商资料或现场反馈修正时才动; 它是判据的地基, 变更须过审','var(--warn)']];
|
|
|
- let h = `<div class="card mb"><h3>数据维护面 · ${rows.length} 个源</h3>
|
|
|
- <p class="small" style="color:var(--muted)">每个源给出: 当前覆盖 / 末次更新 / 约定位置 / 摄入命令 / 更新频率 / 责任方。
|
|
|
- 要补数据就把新文件放到"约定位置"再跑"摄入命令"。</p></div>`;
|
|
|
- LAYER.forEach(([lv, note, col]) => {
|
|
|
- const rs = rows.filter(r => r.层 === lv);
|
|
|
- if(!rs.length) return;
|
|
|
- h += `<div class="card mb" style="border-left:3px solid ${col}"><h3>${lv}${LAYER_SFX} (${rs.length})</h3>
|
|
|
- <p class="small" style="color:var(--muted)">${note}</p>
|
|
|
- <table style="margin-top:10px"><tr><th style="width:20%">源</th><th style="width:22%">当前覆盖</th>
|
|
|
- <th style="width:14%">末次更新</th><th style="width:10%">频率</th><th style="width:10%">责任</th><th>补数据怎么做</th></tr>
|
|
|
- ${rs.map(r => `<tr>
|
|
|
- <td class="small"><b>${r.存在 ? '' : '✗ '}${r.名}</b></td>
|
|
|
- <td class="small" style="font-variant-numeric:tabular-nums">${r.覆盖}${r.条数 ? ` <span style="color:var(--muted)">(${r.条数.toLocaleString()} 条)</span>` : ''}</td>
|
|
|
- <td class="small" style="color:var(--muted)">${r.末次更新 || '—'}</td>
|
|
|
- <td class="small">${r.频率}</td><td class="small">${r.责任}</td>
|
|
|
- <td class="small"><div style="color:var(--muted)">放到: <code>${r.位置}</code></div>
|
|
|
- <div style="margin-top:3px">跑: <code>${r.摄入}</code></div>
|
|
|
- <div style="margin-top:3px;color:var(--muted)">${r.说明}</div></td></tr>`).join('')}</table></div>`;
|
|
|
- });
|
|
|
- return h;
|
|
|
-}
|
|
|
-
|
|
|
-function faultCards(x){
|
|
|
- // 抢修链结构化渲染: 码身份 → 失效模式 → 分步处置(卡片) → 历史工单 → 技术依据
|
|
|
- const sev = x.n_site>=1000?'var(--bad)':x.n_site>=100?'var(--warn)':'var(--muted)';
|
|
|
- let h=`<div class="card mb"><div style="display:flex;align-items:flex-start;gap:14px;flex-wrap:wrap">
|
|
|
- ${x.system?partGlyph(x.system,'var(--accent)'):''}
|
|
|
- <div style="flex:1;min-width:220px"><h3 style="margin:0">${x.code} · ${x.name}</h3>
|
|
|
- <p class="small" style="color:var(--muted);margin-top:5px">
|
|
|
- 系统 ${x.system||'未归属'} · 本场 <b style="color:${sev}">${x.n_site}</b> 条${x.alarm_type?` · 告警类型 ${x.alarm_type}`:''}</p></div></div>`;
|
|
|
- if(x.failmodes&&x.failmodes.length)
|
|
|
- h+=`<div style="margin-top:10px"><b class="small" style="color:var(--muted)">指向的失效模式</b>
|
|
|
- ${x.failmodes.map(f=>`<div class="small" style="margin-top:5px">· ${f.name} <span style="color:var(--muted)">(该类本场报警 ${f.n_alarm} 条 / 工单 ${f.n_wo} 条)</span></div>`).join('')}</div>`;
|
|
|
- h+=`</div>`;
|
|
|
- if(x.steps&&x.steps.length){
|
|
|
- h+=`<div class="card mb"><h3>现场处置 ${x.steps.length} 步 <span class="small" style="font-weight:400;color:var(--muted)">(${x.wi_source||''})</span></h3>
|
|
|
- <p class="small" style="color:var(--muted)">节点部件=手册原文元件位号(确定); 判定规范=实机参数表(2024-10-21 导出)按物理量匹配的<b>候选判据</b>,
|
|
|
- 非厂商绑定 — 现场以铭牌与图纸复核; 参数 ID 与告警码是不同命名空间, 未按同号关联。</p>
|
|
|
- <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:10px;margin-top:10px">`
|
|
|
- +x.steps.map(st=>`<div style="border:1px solid var(--line);border-left:3px solid var(--accent);border-radius:8px;padding:11px 13px">
|
|
|
- <div class="small" style="color:var(--accent);font-weight:600">第 ${st.序} 步${st.类别?` · ${st.类别}`:''}${st.qty?` · 量: ${st.qty}`:''}</div>
|
|
|
- <div style="margin-top:5px;line-height:1.65">${st.步骤}</div>
|
|
|
- ${(st.tags&&st.tags.length)?`<div class="small" style="margin-top:7px"><b style="color:var(--muted)">节点部件</b>
|
|
|
- ${st.tags.map(t=>`<code style="background:var(--panel-2);border:1px solid var(--line);border-radius:4px;padding:1px 6px;margin-right:5px">${t}</code>`).join('')}</div>`:''}
|
|
|
- ${(st.nodes&&st.nodes.length)?`<div class="small" style="margin-top:7px">
|
|
|
- <b style="color:var(--muted)">应检节点 · ${st.circuit||''} (手册此步未给位号 → 按回路构成展开, 逐点查)</b>
|
|
|
- <table style="margin-top:3px"><tr><th style="width:38%">节点</th><th>查什么</th></tr>
|
|
|
- ${st.nodes.map(n=>`<tr><td class="small">${n.name}</td><td class="small">${n.check}</td></tr>`).join('')}</table>
|
|
|
- ${(st.known_tags&&st.known_tags.length)?`<div style="color:var(--muted);margin-top:4px">该系统手册出现过的位号 (对图纸时参考):
|
|
|
- ${st.known_tags.map(k=>`<code style="margin-right:5px">${k.tag}</code>`).join('')}</div>`:''}
|
|
|
- </div>`:''}
|
|
|
- ${(st.specs&&st.specs.length)?`<div class="small" style="margin-top:7px"><b style="color:var(--muted)">判定规范 (实机参数表)</b>
|
|
|
- <table style="margin-top:3px"><tr><th style="width:64px">参数号</th><th>名称</th><th style="width:74px" class="num">整定值</th><th style="width:96px" class="num">允许范围</th></tr>
|
|
|
- ${st.specs.map(sp=>`<tr><td class="small">${sp.no}</td><td class="small">${sp.text}</td>
|
|
|
- <td class="num small">${sp.val!=null?sp.val+(sp.unit||''):'—'}</td>
|
|
|
- <td class="num small">${sp.lo!=null?`${sp.lo} ~ ${sp.hi}`:'—'}</td></tr>`).join('')}</table></div>`:''}
|
|
|
- ${(st.drawings&&st.drawings.length)?`<div class="small" style="margin-top:7px"><b style="color:var(--muted)">对应图纸</b>
|
|
|
- ${st.drawings.map(dw=>`<div style="color:var(--muted);margin-top:2px">· ${dw.name}</div>`).join('')}</div>`:''}
|
|
|
- </div>`).join('')
|
|
|
- +`</div></div>`;
|
|
|
- } else {
|
|
|
- h+=`<div class="card mb"><p class="small">本码无手册处置条目 (手册覆盖本场 161/322 码) — 需查图纸或联系厂商支持。</p></div>`;
|
|
|
- }
|
|
|
- if(x.workorders&&x.workorders.length)
|
|
|
- h+=`<div class="card mb"><h3>现场实际怎么修的 (${x.system} 历史工单 ${x.wo_total} 条, 台账 2020~2024)</h3>
|
|
|
- <table style="margin-top:8px"><tr><th style="width:92px">日期</th><th style="width:64px">机组</th><th>故障</th><th style="width:110px">处置</th></tr>
|
|
|
- ${x.workorders.map(w=>`<tr><td class="small">${w.date}</td><td class="small">${w.turbine.replace('WTG','')}#</td>
|
|
|
- <td class="small">${w.name}</td><td class="small">${w.action} ${w.part}</td></tr>`).join('')}</table>
|
|
|
- <p class="small" style="color:var(--muted);margin-top:6px">工单窗止 2024-11-21; 2025-26 现场未提供 ⇒ "0 条"是记录缺失非未修过。</p></div>`;
|
|
|
- // 技术依据卡撤 (2026-08-28 用户令): 按文件名关键词匹配出来的文档与本次故障常不相关
|
|
|
- // (液压压力偏高 → 推出"单叶片吊具培训资料"/"轮毂起吊图纸"), 列出来是噪音不是依据。
|
|
|
- // ont_doc_search API 保留, 需要时按码精确查, 不再在抢修链里自动铺开。
|
|
|
- return h;
|
|
|
-}
|
|
|
-
|
|
|
-async function ontChain(kind){
|
|
|
- const out=document.getElementById('ocOut'); if(!out) return;
|
|
|
- out.style.display='block'; out.textContent='查询中…';
|
|
|
- const qs={fault:()=>`kind=fault&code=${encodeURIComponent((document.getElementById('ocCode')||{}).value||'')}`,
|
|
|
- prev:()=>`kind=prev&system=${encodeURIComponent((document.getElementById('ocSys')||{}).value||'')}`,
|
|
|
- tree:()=>`kind=tree&system=${encodeURIComponent((document.getElementById('ocSys')||{}).value||'')}`,
|
|
|
- plan:()=>'kind=plan&top=12',
|
|
|
- doc:()=>`kind=doc&keyword=${encodeURIComponent((document.getElementById('ocDoc')||{}).value||'')}`}[kind];
|
|
|
- try{ const r=await fetch('/api/ont_chain?'+qs()); const d=await r.json();
|
|
|
- out.textContent=d.text||JSON.stringify(d); }
|
|
|
- catch(e){ out.textContent='查询失败: '+e.message }
|
|
|
-}
|
|
|
-function closePop(){const m=document.getElementById('popm'); if(m)m.remove()}
|
|
|
-const LVC2=lv=>({bad:'var(--bad)',warn:'var(--warn)',ok:'var(--ok)',meta:'var(--muted)',note:'var(--info)'}[lv]||'var(--warn)');
|
|
|
-document.addEventListener('keydown',e=>{if(e.key==='Escape')closePop()});
|
|
|
-// 状态链条 (per 节点各自着色版; windcms 结果层用: 四部件等级一次性画在传动链上, 替代四列 chips)
|
|
|
-const dtChainStates=(cmap,W,T2)=>{
|
|
|
- const w=W||208, Y=20, MU='var(--muted)', PN='var(--panel)', LN='var(--line)';
|
|
|
- const zone=(x,w2,dash)=>`<rect x="${x}" y="5" width="${w2}" height="30" rx="6" fill="var(--muted)" fill-opacity="0.13"
|
|
|
- stroke="${LN}" stroke-opacity="0.5" ${dash?'stroke-dasharray="3 2"':''}/>`;
|
|
|
- const node=(x,key,kind)=>{
|
|
|
- const col=cmap[key]||null, k=!!col, r=k?6.6:5;
|
|
|
- const teeth=kind==='g'?[0,90,180,270].map(a=>{const t=a*Math.PI/180;
|
|
|
- return `<line x1="${(x+Math.cos(t)*r).toFixed(1)}" y1="${(Y+Math.sin(t)*r).toFixed(1)}" x2="${(x+Math.cos(t)*(r+2.6)).toFixed(1)}" y2="${(Y+Math.sin(t)*(r+2.6)).toFixed(1)}" stroke="${col||MU}" stroke-width="${k?1.7:1.2}"/>`}).join(''):'';
|
|
|
- const core=`<circle cx="${x}" cy="${Y}" r="${r}" fill="${col||PN}" fill-opacity="${k?0.9:1}" stroke="${col||MU}" stroke-opacity="${k?1:0.8}" stroke-width="${k?2:1.3}"/>`
|
|
|
- +(kind==='b'?`<circle cx="${x}" cy="${Y}" r="${(r*0.4).toFixed(1)}" fill="${k?'#fff':MU}" fill-opacity="${k?0.85:0.7}"/>`:teeth);
|
|
|
- return (k&&T2)?`<g class="dt-hot" onclick="event.stopPropagation();popPart('${T2}','${key}')"><title>${key} — 点看曲线与四源微视图</title>
|
|
|
- <circle cx="${x}" cy="${Y}" r="${r+7}" fill="transparent"/>${core}</g>`:core};
|
|
|
- return `<svg class="dt" viewBox="0 0 320 40" style="display:block;width:${w}px;max-width:100%;height:auto">
|
|
|
- ${zone(28,74)}${zone(112,90,1)}${zone(212,96)}
|
|
|
- <line x1="20" y1="${Y}" x2="304" y2="${Y}" stroke="${LN}" stroke-width="3" stroke-linecap="round"/>
|
|
|
- <path d="M20,13 L11,4 M20,27 L11,36 M20,${Y} L9,${Y}" stroke="${MU}" stroke-width="1.6" fill="none"/>
|
|
|
- ${node(46,'主轴承前','b')}${node(88,'主轴承后','b')}
|
|
|
- ${node(133,'齿轮箱','g')}${node(157,'齿轮箱','g')}${node(181,'齿轮箱','g')}
|
|
|
- ${node(236,'发电机','b')}${node(282,'发电机','b')}
|
|
|
- </svg>`};
|
|
|
-// 证据强度阶梯 (确诊4格/准定论3/候选2/参考1/INSUFFICIENT 0虚框) — 文字分级换成可比的阶梯
|
|
|
-const evLadder=t=>{
|
|
|
- const T=String(t||''), n=/确诊/.test(T)?4:/准定论|定论/.test(T)?3:/候选/.test(T)?2:/参考/.test(T)?1:0;
|
|
|
- const col=n>=4?'var(--bad)':n===3?'var(--warn)':n===2?'var(--info)':n===1?'var(--muted)':'var(--unknown)';
|
|
|
- const cells=[0,1,2,3].map(i=>`<rect x="${2+i*15}" y="${12-i*3}" width="11" height="${6+i*3}" rx="1.5"
|
|
|
- fill="${i<n?col:'none'}" fill-opacity="0.9" stroke="${col}" stroke-opacity="${i<n?1:0.4}" stroke-width="1"
|
|
|
- ${i>=n?'stroke-dasharray="2 1.5"':''}/>`).join('');
|
|
|
- return `<div title="${esc(T)}" style="display:flex;align-items:center;gap:7px">
|
|
|
- <svg class="dt" viewBox="0 0 62 20" style="width:62px;height:20px;flex:none">${cells}</svg>
|
|
|
- <span class="small" style="color:${col}">${esc(T)}</span></div>`};
|
|
|
-// 部件微缩符号 (矩阵/列表行内用): 与解剖图同一套图形语言的 24px 版 —
|
|
|
-// 齿轮箱=行星轮系 · 发电机=定子转子 · 主轴承=滚子剖面 · 变桨=桨叶轴承 · 偏航=齿圈驱动
|
|
|
-function partIcon(name, col){
|
|
|
- const c = col || 'currentColor', n = String(name || '');
|
|
|
- const W = 30, H = 26;
|
|
|
- // 尺寸走 inline style (全局 svg{width:100%} 会吃掉 width 属性 — 同一个坑第三次: dtSVG/台页/此处)
|
|
|
- const wrap = body => `<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${n}"
|
|
|
- style="width:${W}px;height:${H}px;min-width:${W}px;vertical-align:-6px;margin-right:6px;flex:none;display:inline-block"
|
|
|
- ><g fill="none" stroke="${c}" stroke-width="1.3">${body}</g></svg>`;
|
|
|
- if(/齿轮箱|行星|中间轴|中速|高速/.test(n))
|
|
|
- return wrap(`<circle cx="15" cy="13" r="10.5"/><circle cx="15" cy="13" r="3.4"/>
|
|
|
- <circle cx="15" cy="5.5" r="3"/><circle cx="8.5" cy="17" r="3"/><circle cx="21.5" cy="17" r="3"/>
|
|
|
- <path d="M15,13 L15,5.5 M15,13 L8.5,17 M15,13 L21.5,17" stroke-width="0.9" stroke-opacity="0.75"/>`);
|
|
|
- if(/发电机|定子|转子|DE|NDE/.test(n))
|
|
|
- return wrap(`<rect x="4" y="6" width="22" height="14" rx="2.5"/>
|
|
|
- <path d="M4,10 h22 M4,16 h22" stroke-opacity="0.45"/>
|
|
|
- <rect x="9" y="10" width="12" height="6" rx="1" fill="${c}" fill-opacity="0.18" stroke="none"/>
|
|
|
- <path d="M1,13 h3 M26,13 h3" stroke-width="1.6"/>`);
|
|
|
- if(/主轴承|主轴/.test(n)){
|
|
|
- // 前后两个轴承都画出来 (2026-08-28 用户令), 命中的那个着色加粗, 另一个留灰 —
|
|
|
- // 左=前(轮毂侧) 右=后(齿轮箱侧), 与传动链图同一朝向; 未指明前后则两个都着色
|
|
|
- const hasF = /前/.test(n), hasR = /后/.test(n), both = !hasF && !hasR;
|
|
|
- const brg = (x, on) => {
|
|
|
- const st = on ? c : 'var(--muted)', w = on ? 1.7 : 1.05, op = on ? 1 : 0.55;
|
|
|
- return `<g stroke="${st}" stroke-width="${w}" stroke-opacity="${op}">
|
|
|
- <rect x="${x}" y="5.5" width="16" height="4" rx="1"/><rect x="${x}" y="16.5" width="16" height="4" rx="1"/>
|
|
|
- <circle cx="${x + 4.5}" cy="13" r="2.3"/><circle cx="${x + 11.5}" cy="13" r="2.3"/></g>`;
|
|
|
- };
|
|
|
- return `<svg viewBox="0 0 46 26" role="img" aria-label="${n}"
|
|
|
- style="width:46px;height:26px;min-width:46px;vertical-align:-6px;margin-right:6px;flex:none;display:inline-block">
|
|
|
- <g fill="none"><path d="M2,13 h42" stroke="var(--muted)" stroke-width="2.2" stroke-opacity="0.35"/>
|
|
|
- ${brg(4, hasF || both)}${brg(26, hasR || both)}</g></svg>`;
|
|
|
- }
|
|
|
- if(/变桨|桨|叶片|叶根|轮毂/.test(n))
|
|
|
- return wrap(`<circle cx="15" cy="13" r="7.5"/><circle cx="15" cy="13" r="4"/>
|
|
|
- <path d="M15,5.5 q7,-3 11,1 M15,20.5 q7,3 11,-1 M7.5,13 q-6,0 -6.5,-4" stroke-width="1.1" stroke-opacity="0.8"/>`);
|
|
|
- if(/偏航|解缆|扭缆|对风/.test(n))
|
|
|
- return wrap(`<circle cx="15" cy="15" r="9"/><circle cx="15" cy="15" r="5.5" stroke-dasharray="2 2"/>
|
|
|
- ${[0,72,144,216,288].map(a=>{const r=(a-90)*Math.PI/180;
|
|
|
- return `<circle cx="${(15+Math.cos(r)*9).toFixed(1)}" cy="${(15+Math.sin(r)*9).toFixed(1)}" r="1.7"/>`}).join('')}`);
|
|
|
- if(/变流|变频|逆变/.test(n))
|
|
|
- return wrap(`<rect x="5" y="6" width="20" height="14" rx="2"/><path d="M11,10 l-3,4 h5 l-3,4" stroke-width="1.2"/>
|
|
|
- <path d="M18,10 v8 M21.5,10 v8" stroke-opacity="0.6"/>`);
|
|
|
- if(/塔架|塔筒|基础|法兰/.test(n))
|
|
|
- return wrap(`<path d="M11,4 h8 l3,17 h-14 z"/><path d="M9,14 h12" stroke-opacity="0.5"/>
|
|
|
- <path d="M5,21 h20" stroke-width="1.8"/>`);
|
|
|
- return wrap(`<circle cx="15" cy="13" r="8" stroke-dasharray="3 2.5"/>`);
|
|
|
-}
|
|
|
-
|
|
|
-// 部件小图 (按部件截取: 只画命中的那一段, 不画整条链; 比八节点链条形象 — 带座体/齿形/散热筋等外形特征)
|
|
|
-function partGlyph(name, col, solid){
|
|
|
- // solid=true 仅用于报警/危险级 — 良好/优秀级用描边不填充, 否则密集表格里满屏深色块
|
|
|
- // (2026-08-28 用户逮'满屏黑化'; 对应 skill 原则: boldness 只花在一处, 其余安静)
|
|
|
- const c = col || 'currentColor', n = String(name || ''), MU = 'var(--muted)';
|
|
|
- const svg = (w, body) => `<svg viewBox="0 0 ${w} 38" role="img" aria-label="${n}"
|
|
|
- style="width:${w}px;height:38px;min-width:${w}px;vertical-align:-11px;flex:none;display:block">
|
|
|
- <g fill="none" stroke-linejoin="round">${body}</g></svg>`;
|
|
|
-
|
|
|
- if(/主轴承|主轴/.test(n)){
|
|
|
- const F = /前/.test(n), R = /后/.test(n), both = !F && !R;
|
|
|
- // 带座体的轴承: 座体梯形 + 外圈 + 滚子 + 内圈, 轴穿过
|
|
|
- const unit = (x, on) => {
|
|
|
- const st = on ? c : MU, w = on ? 1.7 : 1, op = on ? 1 : 0.5;
|
|
|
- return `<g stroke="${st}" stroke-width="${w}" stroke-opacity="${op}">
|
|
|
- <path d="M${x-2},32 h26 l-3,-8 h-20 z" fill="${on&&solid?c:'none'}" fill-opacity="${on&&solid?0.14:0}"/>
|
|
|
- <rect x="${x+1}" y="7" width="18" height="4" rx="1"/>
|
|
|
- <rect x="${x+1}" y="25" width="18" height="4" rx="1"/>
|
|
|
- <rect x="${x+1}" y="12.5" width="18" height="2.6" rx="0.8" stroke-opacity="${op*0.7}"/>
|
|
|
- <rect x="${x+1}" y="21" width="18" height="2.6" rx="0.8" stroke-opacity="${op*0.7}"/>
|
|
|
- <circle cx="${x+5}" cy="18" r="2.6"/><circle cx="${x+10}" cy="18" r="2.6"/><circle cx="${x+15}" cy="18" r="2.6"/></g>`;
|
|
|
- };
|
|
|
- return svg(96, `<path d="M2,18 h92" stroke="${MU}" stroke-width="3" stroke-opacity="0.3" stroke-linecap="round"/>
|
|
|
- ${unit(8, F||both)}${unit(56, R||both)}
|
|
|
- <text x="19" y="6" text-anchor="middle" font-size="8" fill="${F||both?c:MU}" stroke="none" opacity="${F||both?1:0.6}">前</text>
|
|
|
- <text x="67" y="6" text-anchor="middle" font-size="8" fill="${R||both?c:MU}" stroke="none" opacity="${R||both?1:0.6}">后</text>`);
|
|
|
- }
|
|
|
-
|
|
|
- if(/齿轮箱|行星|中间轴|中速|高速/.test(n)){
|
|
|
- const P = /行星/.test(n), M = /中间|中速/.test(n), H = /高速/.test(n), all = !P && !M && !H;
|
|
|
- const gear = (cx, cy, r, teeth, on) => {
|
|
|
- const st = on ? c : MU, sw = on ? 1.6 : 1, op = on ? 1 : 0.45;
|
|
|
- let t = '';
|
|
|
- for(let i=0;i<teeth;i++){const a=i*2*Math.PI/teeth;
|
|
|
- t += `<line x1="${(cx+Math.cos(a)*r).toFixed(1)}" y1="${(cy+Math.sin(a)*r).toFixed(1)}"
|
|
|
- x2="${(cx+Math.cos(a)*(r+2.4)).toFixed(1)}" y2="${(cy+Math.sin(a)*(r+2.4)).toFixed(1)}"/>`}
|
|
|
- return `<g stroke="${st}" stroke-width="${sw}" stroke-opacity="${op}"><circle cx="${cx}" cy="${cy}" r="${r}"/>${t}
|
|
|
- <circle cx="${cx}" cy="${cy}" r="${(r*0.3).toFixed(1)}"/></g>`;
|
|
|
- };
|
|
|
- return svg(104, `<path d="M3,10 h98 v22 q0,4 -4,4 h-90 q-4,0 -4,-4 z" stroke="${MU}" stroke-opacity="0.45" stroke-dasharray="3 2"/>
|
|
|
- ${gear(22, 21, 9, 12, P||all)}${gear(22, 21, 3.4, 0, P||all)}
|
|
|
- ${gear(55, 18, 7.5, 10, M||all)}${gear(80, 25, 5.5, 8, H||all)}
|
|
|
- <path d="M31,21 h16 M62,20 h11" stroke="${MU}" stroke-width="1.6" stroke-opacity="0.45"/>`);
|
|
|
- }
|
|
|
-
|
|
|
- if(/发电机|定子|转子|DE|NDE/.test(n)){
|
|
|
- const D = /(?<!N)DE/.test(n) && !/NDE/.test(n), ND = /NDE/.test(n), all = !D && !ND;
|
|
|
- const end = (x, on) => `<g stroke="${on?c:MU}" stroke-width="${on?1.7:1}" stroke-opacity="${on?1:0.5}">
|
|
|
- <rect x="${x}" y="12" width="9" height="14" rx="2"/><circle cx="${x+4.5}" cy="19" r="2.6"/></g>`;
|
|
|
- return svg(96, `<g stroke="${c}" stroke-width="1.3">
|
|
|
- <rect x="18" y="8" width="60" height="22" rx="3"/></g>
|
|
|
- <g stroke="${MU}" stroke-opacity="0.5" stroke-width="0.9">
|
|
|
- ${[26,34,42,50,58,66].map(x=>`<line x1="${x}" y1="8" x2="${x}" y2="30"/>`).join('')}</g>
|
|
|
- <rect x="30" y="14" width="36" height="10" rx="2" stroke="${c}" stroke-width="1.2" fill="${c}" fill-opacity="${solid?0.14:0.04}"/>
|
|
|
- <path d="M4,19 h14 M78,19 h14" stroke="${MU}" stroke-width="2.4" stroke-opacity="0.35" stroke-linecap="round"/>
|
|
|
- ${end(8, D||all)}${end(79, ND||all)}
|
|
|
- <text x="12" y="6" text-anchor="middle" font-size="8" fill="${D||all?c:MU}" stroke="none" opacity="${D||all?1:0.6}">DE</text>
|
|
|
- <text x="84" y="6" text-anchor="middle" font-size="8" fill="${ND||all?c:MU}" stroke="none" opacity="${ND||all?1:0.6}">NDE</text>`);
|
|
|
- }
|
|
|
-
|
|
|
- if(/变桨|桨|叶片|叶根|轮毂/.test(n))
|
|
|
- return svg(80, `<g stroke="${c}" stroke-width="1.5">
|
|
|
- <circle cx="26" cy="19" r="12"/><circle cx="26" cy="19" r="7"/>
|
|
|
- ${[0,120,240].map(a=>{const r=(a-90)*Math.PI/180;
|
|
|
- return `<circle cx="${(26+Math.cos(r)*9.6).toFixed(1)}" cy="${(19+Math.sin(r)*9.6).toFixed(1)}" r="2.2"/>`}).join('')}</g>
|
|
|
- <g stroke="${MU}" stroke-width="4" stroke-opacity="0.32" stroke-linecap="round">
|
|
|
- <path d="M38,19 q22,-3 36,-11"/><path d="M38,19 q22,3 36,11"/></g>`);
|
|
|
-
|
|
|
- if(/偏航|解缆|扭缆|对风/.test(n))
|
|
|
- return svg(80, `<g stroke="${c}" stroke-width="1.5"><circle cx="34" cy="21" r="13"/>
|
|
|
- ${Array.from({length:14},(_,i)=>{const a=i*2*Math.PI/14;
|
|
|
- return `<line x1="${(34+Math.cos(a)*13).toFixed(1)}" y1="${(21+Math.sin(a)*13).toFixed(1)}"
|
|
|
- x2="${(34+Math.cos(a)*15.6).toFixed(1)}" y2="${(21+Math.sin(a)*15.6).toFixed(1)}"/>`}).join('')}</g>
|
|
|
- <g stroke="${MU}" stroke-width="1.2" stroke-opacity="0.6">
|
|
|
- <circle cx="60" cy="14" r="5"/><circle cx="60" cy="28" r="5"/></g>
|
|
|
- <path d="M18,6 h32" stroke="${MU}" stroke-width="2" stroke-opacity="0.4"/>`);
|
|
|
-
|
|
|
- return svg(60, `<circle cx="30" cy="19" r="11" stroke="${MU}" stroke-dasharray="3 2.5" stroke-opacity="0.6"/>`);
|
|
|
-}
|
|
|
-
|
|
|
-// ══ 部件解剖图 (2026-08-28 用户令"部件UI图像化认真对待") ══
|
|
|
-// 原则: 画机制不画名字 — 每张图展示该系统的介质/动力如何流经部件, 箭头带口径标注,
|
|
|
-// 命中部件按状态着色; 一图一论点。与传动链简图分工: 传动链=链上哪一环, 解剖图=环内怎么坏的。
|
|
|
-const ANAT_C = st => ({'报警':'var(--bad)','危险':'var(--bad)','预警':'var(--warn)','良好':'var(--info)',
|
|
|
- '优秀':'var(--ok)','不可判':'var(--unknown)'}[st] || 'var(--muted)');
|
|
|
-
|
|
|
-function anatMainBearing(hit, states){
|
|
|
- // 主轴承集中润滑链: 泵 →(管路)→ 分配器 →(柱塞往复=出脂计量)→ 前/后轴承
|
|
|
- // 画的是"脂怎么到轴承", 29# 的失效点在泵本体 (柱塞单次时长拖长=打不满)
|
|
|
- const c = k => ANAT_C((states||{})[k]);
|
|
|
- const on = k => (hit||'').includes(k);
|
|
|
- const hl = k => on(k) ? 'var(--bad)' : 'currentColor';
|
|
|
- return `<figure style="margin:0"><svg viewBox="0 0 660 210" role="img" width="660"
|
|
|
- aria-label="主轴承集中润滑链: 润滑泵经管路到分配器, 分配器柱塞往复把脂定量送到前后主轴承"
|
|
|
- style="max-width:100%;height:auto;display:block;margin:0 auto">
|
|
|
- <g fill="none" stroke="currentColor" stroke-width="1.4" font-family="inherit">
|
|
|
- <!-- 脂桶+泵 -->
|
|
|
- <rect x="18" y="60" width="54" height="66" rx="4" stroke="${hl('泵')}" stroke-width="${on('泵')?2.4:1.4}"/>
|
|
|
- <path d="M28,70 h34 M28,80 h34 M28,90 h34" stroke-opacity="0.35"/>
|
|
|
- <text x="45" y="142" text-anchor="middle" font-size="11" fill="currentColor" stroke="none">脂桶</text>
|
|
|
- <circle cx="104" cy="93" r="17" stroke="${hl('泵')}" stroke-width="${on('泵')?2.4:1.4}"/>
|
|
|
- <path d="M97,86 l14,7 -14,7 z" fill="${hl('泵')}" stroke="none"/>
|
|
|
- <text x="104" y="128" text-anchor="middle" font-size="11" fill="${hl('泵')}" stroke="none">润滑泵</text>
|
|
|
- ${on('泵') ? '<text x="104" y="62" text-anchor="middle" font-size="10" fill="var(--bad)" stroke="none">本体失效</text>' : ''}
|
|
|
- <!-- 管路 -->
|
|
|
- <path d="M121,93 h58" marker-end="url(#aH)"/>
|
|
|
- <text x="150" y="86" text-anchor="middle" font-size="10" fill="currentColor" stroke="none" stroke-opacity="0">主管路</text>
|
|
|
- <!-- 分配器 -->
|
|
|
- <rect x="182" y="66" width="66" height="54" rx="4" stroke="${hl('分配器')}"/>
|
|
|
- <path d="M196,80 v26 M215,80 v26 M234,80 v26" stroke-opacity="0.55"/>
|
|
|
- <text x="215" y="134" text-anchor="middle" font-size="11" fill="currentColor" stroke="none">分配器</text>
|
|
|
- <text x="215" y="60" text-anchor="middle" font-size="9.5" fill="currentColor" stroke="none" opacity="0.7">柱塞往复=出脂计量</text>
|
|
|
- <!-- 支路到前后轴承 -->
|
|
|
- <path d="M248,80 h48 q12,0 12,-12 v-14" marker-end="url(#aH)"/>
|
|
|
- <path d="M248,106 h48 q12,0 12,12 v14" marker-end="url(#aH)"/>
|
|
|
- <text x="286" y="70" text-anchor="middle" font-size="9.5" fill="currentColor" stroke="none" opacity="0.7">前支路</text>
|
|
|
- <text x="286" y="140" text-anchor="middle" font-size="9.5" fill="currentColor" stroke="none" opacity="0.7">后支路</text>
|
|
|
- <!-- 前后轴承 (剖面) -->
|
|
|
- ${[['主轴承前', 34], ['主轴承后', 132]].map(([k, y]) => `
|
|
|
- <g stroke="${c(k)}" stroke-width="${(states||{})[k]==='报警'?2.4:1.4}">
|
|
|
- <rect x="330" y="${y}" width="86" height="16" rx="2"/>
|
|
|
- <rect x="330" y="${y+28}" width="86" height="16" rx="2"/>
|
|
|
- <circle cx="352" cy="${y+22}" r="5"/><circle cx="373" cy="${y+22}" r="5"/><circle cx="394" cy="${y+22}" r="5"/>
|
|
|
- </g>
|
|
|
- <text x="373" y="${y+58}" text-anchor="middle" font-size="11" fill="${c(k)}" stroke="none">${k}</text>`).join('')}
|
|
|
- <!-- 主轴穿过 -->
|
|
|
- <path d="M300,105 h180" stroke-width="7" stroke-opacity="0.25" stroke-linecap="round"/>
|
|
|
- <!-- 观测面标注 -->
|
|
|
- <path d="M215,120 v42" stroke-dasharray="3 3" stroke-opacity="0.6"/>
|
|
|
- <text x="215" y="176" text-anchor="middle" font-size="10" fill="currentColor" stroke="none">因侧: 柱塞计数/单次时长</text>
|
|
|
- <path d="M373,${(states||{})['主轴承后']?'190':'190'} v-14" stroke-dasharray="3 3" stroke-opacity="0.6"/>
|
|
|
- <text x="470" y="176" text-anchor="middle" font-size="10" fill="currentColor" stroke="none">果侧: 轴承温度 · 振动冲击</text>
|
|
|
- <defs><marker id="aH" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto">
|
|
|
- <polygon points="0,0 7,3.5 0,7" fill="currentColor"/></marker></defs>
|
|
|
- </g></svg>
|
|
|
- <figcaption class="small" style="color:var(--muted);margin-top:6px">
|
|
|
- 脂路: 泵 → 主管路 → 分配器(柱塞定量) → 前/后支路 → 轴承。因侧看柱塞行为, 果侧看轴承温度与冲击 —
|
|
|
- 两侧不是两个独立源, 是同一条机制链的两端。</figcaption></figure>`;
|
|
|
-}
|
|
|
-
|
|
|
-function anatGearbox(hit, states){
|
|
|
- // 齿轮箱: 一级行星 + 两级平行轴 + 润滑/冷却回路 (油从箱底经泵/滤/冷却器回喷淋)
|
|
|
- const c = k => ANAT_C((states||{})[k]);
|
|
|
- const on = k => (hit||'').includes(k);
|
|
|
- const hl = k => on(k) ? 'var(--bad)' : 'currentColor';
|
|
|
- return `<figure style="margin:0"><svg viewBox="0 0 660 250" role="img" width="660"
|
|
|
- aria-label="齿轮箱: 一级行星加两级平行轴传动, 下方为润滑油路(泵-滤芯-冷却器-喷淋)"
|
|
|
- style="max-width:100%;height:auto;display:block;margin:0 auto">
|
|
|
- <g fill="none" stroke="currentColor" stroke-width="1.4">
|
|
|
- <rect x="24" y="18" width="500" height="128" rx="8" stroke-dasharray="5 4" stroke-opacity="0.5"/>
|
|
|
- <text x="274" y="14" text-anchor="middle" font-size="10.5" fill="currentColor" stroke="none" opacity="0.7">齿轮箱壳体 (i=119.752)</text>
|
|
|
- <!-- 行星级 -->
|
|
|
- <g stroke="${hl('行星')}" stroke-width="${on('行星')?2.2:1.4}">
|
|
|
- <circle cx="98" cy="82" r="46"/><circle cx="98" cy="82" r="15"/>
|
|
|
- <circle cx="98" cy="46" r="14"/><circle cx="67" cy="100" r="14"/><circle cx="129" cy="100" r="14"/>
|
|
|
- <path d="M98,82 L98,46 M98,82 L67,100 M98,82 L129,100" stroke-width="1"/>
|
|
|
- </g>
|
|
|
- <text x="98" y="146" text-anchor="middle" font-size="11" fill="${hl('行星')}" stroke="none">一级行星</text>
|
|
|
- <path d="M144,82 h40" marker-end="url(#gH)"/>
|
|
|
- <text x="164" y="74" text-anchor="middle" font-size="9" fill="currentColor" stroke="none" opacity="0.7">中速</text>
|
|
|
- <!-- 平行轴两级 -->
|
|
|
- <g stroke="${hl('中间')}" stroke-width="${on('中间')?2.2:1.4}">
|
|
|
- <rect x="188" y="52" width="16" height="60" rx="3"/><rect x="188" y="26" width="16" height="26" rx="3"/></g>
|
|
|
- <text x="196" y="146" text-anchor="middle" font-size="11" fill="${hl('中间')}" stroke="none">中间轴</text>
|
|
|
- <g stroke="${hl('高速')}" stroke-width="${on('高速')?2.2:1.4}">
|
|
|
- <rect x="244" y="26" width="16" height="52" rx="3"/><rect x="244" y="78" width="16" height="30" rx="3"/></g>
|
|
|
- <text x="252" y="146" text-anchor="middle" font-size="11" fill="${hl('高速')}" stroke="none">高速轴</text>
|
|
|
- <path d="M268,93 h56" marker-end="url(#gH)"/>
|
|
|
- <text x="296" y="86" text-anchor="middle" font-size="9" fill="currentColor" stroke="none" opacity="0.7">→ 发电机</text>
|
|
|
- <!-- 油路 -->
|
|
|
- <path d="M60,146 v34 h420 v-34" stroke-opacity="0.8"/>
|
|
|
- <circle cx="180" cy="180" r="13" stroke="${hl('油泵')}"/><path d="M174,174 l12,6 -12,6 z" fill="${hl('油泵')}" stroke="none"/>
|
|
|
- <text x="180" y="208" text-anchor="middle" font-size="10" fill="currentColor" stroke="none">油泵</text>
|
|
|
- <rect x="242" y="168" width="26" height="24" rx="2" stroke="${hl('滤芯')}"/>
|
|
|
- <path d="M246,172 l18,16 M264,172 l-18,16" stroke-width="0.9" stroke-opacity="0.6"/>
|
|
|
- <text x="255" y="208" text-anchor="middle" font-size="10" fill="currentColor" stroke="none">滤芯</text>
|
|
|
- <rect x="322" y="166" width="40" height="28" rx="3" stroke="${hl('冷却')}"/>
|
|
|
- <path d="M330,170 v20 M340,170 v20 M350,170 v20" stroke-width="0.9" stroke-opacity="0.6"/>
|
|
|
- <text x="342" y="208" text-anchor="middle" font-size="10" fill="${hl('冷却')}" stroke="none">冷却器</text>
|
|
|
- <path d="M410,180 v-20" marker-end="url(#gH)"/>
|
|
|
- <text x="446" y="176" text-anchor="middle" font-size="10" fill="currentColor" stroke="none">喷淋回箱</text>
|
|
|
- <!-- 观测面 -->
|
|
|
- <text x="560" y="60" font-size="10" fill="currentColor" stroke="none">观测面:</text>
|
|
|
- <text x="560" y="78" font-size="10" fill="currentColor" stroke="none" opacity="0.8">· 振动 (各级测点)</text>
|
|
|
- <text x="560" y="94" font-size="10" fill="currentColor" stroke="none" opacity="0.8">· 轴承/油温 ΔT</text>
|
|
|
- <text x="560" y="110" font-size="10" fill="currentColor" stroke="none" opacity="0.8">· 滤芯压差码</text>
|
|
|
- <text x="560" y="126" font-size="10" fill="currentColor" stroke="none" opacity="0.8">· 油样铁谱</text>
|
|
|
- <defs><marker id="gH" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto">
|
|
|
- <polygon points="0,0 7,3.5 0,7" fill="currentColor"/></marker></defs>
|
|
|
- </g></svg>
|
|
|
- <figcaption class="small" style="color:var(--muted);margin-top:6px">
|
|
|
- 传动: 行星级(输入) → 中间轴 → 高速轴(输出)。油路: 箱底 → 泵 → 滤芯 → 冷却器 → 喷淋回箱 —
|
|
|
- 过热类失效沿油路查(冷却/滤芯), 磨损类沿齿轮线查(振动/铁谱)。</figcaption></figure>`;
|
|
|
-}
|
|
|
-
|
|
|
-function anatPitch(hit, states){
|
|
|
- // 变桨: 蓄能器储压 → 比例阀 → 油缸推动变桨轴承 → 桨叶; 滑环供电与信号
|
|
|
- const c = k => ANAT_C((states||{})[k]);
|
|
|
- const on = k => (hit||'').includes(k);
|
|
|
- const hl = k => on(k) ? 'var(--bad)' : 'currentColor';
|
|
|
- return `<figure style="margin:0"><svg viewBox="0 0 660 230" role="img" width="660"
|
|
|
- aria-label="变桨系统: 蓄能器储压经比例阀推动油缸, 油缸驱动变桨轴承转动桨叶; 滑环传电与信号"
|
|
|
- style="max-width:100%;height:auto;display:block;margin:0 auto">
|
|
|
- <g fill="none" stroke="currentColor" stroke-width="1.4">
|
|
|
- <!-- 滑环 -->
|
|
|
- <g stroke="${hl('滑环')}"><rect x="20" y="86" width="30" height="50" rx="4"/>
|
|
|
- <path d="M20,96 h30 M20,110 h30 M20,124 h30" stroke-opacity="0.5"/></g>
|
|
|
- <text x="35" y="152" text-anchor="middle" font-size="10.5" fill="currentColor" stroke="none">滑环</text>
|
|
|
- <path d="M50,111 h34" marker-end="url(#pH)"/>
|
|
|
- <text x="67" y="104" text-anchor="middle" font-size="9" fill="currentColor" stroke="none" opacity="0.7">电/信号</text>
|
|
|
- <!-- 蓄能器 -->
|
|
|
- <g stroke="${hl('蓄能')}" stroke-width="${on('蓄能')?2.2:1.4}">
|
|
|
- <rect x="88" y="70" width="40" height="82" rx="20"/>
|
|
|
- <path d="M88,104 h40" stroke-dasharray="3 2"/></g>
|
|
|
- <text x="108" y="166" text-anchor="middle" font-size="10.5" fill="${hl('蓄能')}" stroke="none">蓄能器</text>
|
|
|
- <text x="108" y="62" text-anchor="middle" font-size="9" fill="currentColor" stroke="none" opacity="0.7">氮气预充</text>
|
|
|
- <path d="M128,111 h36" marker-end="url(#pH)"/>
|
|
|
- <!-- 比例阀 -->
|
|
|
- <g stroke="${hl('阀')}"><rect x="166" y="92" width="34" height="38" rx="3"/>
|
|
|
- <path d="M166,111 h34 M183,92 v38" stroke-opacity="0.5"/></g>
|
|
|
- <text x="183" y="146" text-anchor="middle" font-size="10.5" fill="currentColor" stroke="none">比例阀</text>
|
|
|
- <path d="M200,111 h34" marker-end="url(#pH)"/>
|
|
|
- <text x="217" y="104" text-anchor="middle" font-size="9" fill="currentColor" stroke="none" opacity="0.7">压力</text>
|
|
|
- <!-- 油缸 -->
|
|
|
- <g stroke="${hl('油缸')}" stroke-width="${on('油缸')?2.2:1.4}">
|
|
|
- <rect x="236" y="94" width="86" height="34" rx="3"/>
|
|
|
- <rect x="246" y="100" width="30" height="22" rx="2" fill="currentColor" fill-opacity="0.14"/>
|
|
|
- <path d="M276,111 h66"/></g>
|
|
|
- <text x="279" y="146" text-anchor="middle" font-size="10.5" fill="${hl('油缸')}" stroke="none">变桨油缸</text>
|
|
|
- <!-- 变桨轴承 + 桨叶 -->
|
|
|
- <g stroke="${hl('轴承')}" stroke-width="${on('轴承')?2.2:1.4}">
|
|
|
- <circle cx="392" cy="111" r="34"/><circle cx="392" cy="111" r="24"/>
|
|
|
- ${[0,120,240].map(a=>{const r=(a-90)*Math.PI/180;
|
|
|
- return `<circle cx="${(392+Math.cos(r)*29).toFixed(1)}" cy="${(111+Math.sin(r)*29).toFixed(1)}" r="3.4"/>`}).join('')}</g>
|
|
|
- <text x="392" y="164" text-anchor="middle" font-size="10.5" fill="${hl('轴承')}" stroke="none">变桨轴承</text>
|
|
|
- <path d="M426,111 q46,-4 74,-40" stroke-width="5" stroke-opacity="0.3" stroke-linecap="round"/>
|
|
|
- <path d="M426,111 q46,4 74,40" stroke-width="5" stroke-opacity="0.3" stroke-linecap="round"/>
|
|
|
- <text x="520" y="76" font-size="10.5" fill="currentColor" stroke="none">桨叶 A/B/C</text>
|
|
|
- <!-- 观测面 -->
|
|
|
- <path d="M108,152 v30 h284 v-18" stroke-dasharray="3 3" stroke-opacity="0.55"/>
|
|
|
- <text x="250" y="196" text-anchor="middle" font-size="10" fill="currentColor" stroke="none">
|
|
|
- 观测: 蓄能带宽/停机泄压 · 桨距跟踪偏差 · 三叶零位一致性 · 轴承润滑脂</text>
|
|
|
- <defs><marker id="pH" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto">
|
|
|
- <polygon points="0,0 7,3.5 0,7" fill="currentColor"/></marker></defs>
|
|
|
- </g></svg>
|
|
|
- <figcaption class="small" style="color:var(--muted);margin-top:6px">
|
|
|
- 动力: 蓄能器储压 → 比例阀 → 油缸 → 变桨轴承 → 桨叶。压力侧失效看蓄能带宽与泄压率,
|
|
|
- 执行侧看桨距跟踪偏差, 三叶不一致指向单支路。</figcaption></figure>`;
|
|
|
-}
|
|
|
-
|
|
|
-const ANAT = {'主轴承': anatMainBearing, '齿轮箱': anatGearbox, '变桨': anatPitch};
|
|
|
-
|
|
|
-// 链条缩略版 (表格用: 列宽有限, 全剖面被挤成49px不可读) — 同一图形语言的 sparkline。
|
|
|
-// 2026-08-28 用户"更清楚": 八个一样的圆点分不出段落 → 节点形状语义化 (轴承=双环 · 齿轮=带齿圆)
|
|
|
-// + 三段分区底纹 (主轴 / 齿轮箱 / 发电机), 命中环放大填色+外发光, 仍零文字。
|
|
|
-const dtMini=(comp,sev,W,T2)=>{
|
|
|
- const c=String(comp||''), w=W||208;
|
|
|
- const hit=n=>({'主轴承前':/主轴承[((]前|主轴承前|主轴承[++]|主轴承(?![((前后])/,'主轴承后':/主轴承[((]后|主轴承后/,
|
|
|
- '行星':/行星|二级行星/,'中间':/中间轴|中速/,'高速':/高速轴|高速(?!.*发电)/,
|
|
|
- '发电机DE':/发电机DE|发电机[((]|发电机(?!NDE)/,'发电机NDE':/发电机NDE|NDE/}[n].test(c));
|
|
|
- const gball=/齿轮箱/.test(c)&&!/行星|中间轴|中速|高速轴/.test(c);
|
|
|
- const on=n=>hit(n)||(gball&&['行星','中间','高速'].includes(n));
|
|
|
- const Y=20, MU='var(--muted)', PN='var(--panel)', LN='var(--line)';
|
|
|
- const zone=(x,w2,dash)=>`<rect x="${x}" y="5" width="${w2}" height="30" rx="6" fill="var(--muted)" fill-opacity="0.13"
|
|
|
- stroke="${dash&&gball?sev:LN}" stroke-opacity="${dash&&gball?0.9:0.5}" ${dash?'stroke-dasharray="3 2"':''}/>`;
|
|
|
- // 轴承节点 = 双环 (外圈+滚道); 齿轮节点 = 圆+四齿
|
|
|
- const node=(x,n,kind)=>{
|
|
|
- const k=on(n), col=k?sev:MU, r=k?7:5.4;
|
|
|
- const teeth=kind==='g'?[0,90,180,270].map(a=>{const t=a*Math.PI/180;
|
|
|
- return `<line x1="${(x+Math.cos(t)*r).toFixed(1)}" y1="${(Y+Math.sin(t)*r).toFixed(1)}" x2="${(x+Math.cos(t)*(r+2.6)).toFixed(1)}" y2="${(Y+Math.sin(t)*(r+2.6)).toFixed(1)}" stroke="${col}" stroke-width="${k?1.8:1.2}"/>`}).join(''):'';
|
|
|
- const core=(k?`<circle class="dt-halo" cx="${x}" cy="${Y}" r="${r+6}" fill="${sev}"/>`:'')
|
|
|
- +`<circle cx="${x}" cy="${Y}" r="${r}" fill="${k?sev:PN}" fill-opacity="${k?0.9:1}" stroke="${col}" stroke-opacity="${k?1:0.85}" stroke-width="${k?2.2:1.4}"/>`
|
|
|
- +(kind==='b'?`<circle cx="${x}" cy="${Y}" r="${(r*0.42).toFixed(1)}" fill="${k?'#fff':MU}" fill-opacity="${k?0.85:0.7}" stroke="none"/>`:teeth);
|
|
|
- return (k&&T2)?`<g class="dt-hot" onclick="event.stopPropagation();popPart('${T2}','${n}')"><title>${n} — 点看曲线与四源微视图</title>
|
|
|
- <circle cx="${x}" cy="${Y}" r="${r+7}" fill="transparent"/>${core}</g>`:core;
|
|
|
- };
|
|
|
- return `<svg class="dt" viewBox="0 0 320 40" style="display:block;width:${w}px;max-width:100%;height:auto">
|
|
|
- ${zone(28,74)}${zone(112,90,1)}${zone(212,96)}
|
|
|
- <line x1="20" y1="${Y}" x2="304" y2="${Y}" stroke="${LN}" stroke-width="3" stroke-linecap="round"/>
|
|
|
- <path d="M20,13 L11,4 M20,27 L11,36 M20,${Y} L9,${Y}" stroke="${MU}" stroke-width="1.6" fill="none"/>
|
|
|
- ${node(46,'主轴承前','b')}${node(88,'主轴承后','b')}
|
|
|
- ${node(133,'行星','g')}${node(157,'中间','g')}${node(181,'高速','g')}
|
|
|
- ${node(236,'发电机DE','b')}${node(282,'发电机NDE','b')}
|
|
|
- </svg>`};
|
|
|
-const dtSVG=(comp,sev,W)=>{
|
|
|
- const c=String(comp||''), w=W||660;
|
|
|
- const hit=n=>({'主轴承前':/主轴承[((]前|主轴承前|主轴承[++]|主轴承(?![((前后])/,'主轴承后':/主轴承[((]后|主轴承后/,
|
|
|
- '行星':/行星|二级行星/,'中间':/中间轴|中速/,'高速':/高速轴|高速(?!.*发电)/,
|
|
|
- '发电机DE':/发电机DE|发电机[((]|发电机(?!NDE)/,'发电机NDE':/发电机NDE|NDE/}[n].test(c));
|
|
|
- const gball=/齿轮箱/.test(c)&&!/行星|中间轴|中速|高速轴/.test(c);
|
|
|
- const on=n=>hit(n)||(gball&&['行星','中间','高速'].includes(n));
|
|
|
- const LN='var(--line)', MU='var(--muted)', PN='var(--panel)';
|
|
|
- const K=660/w, FS=(11*K).toFixed(1), SW=(1.5*K).toFixed(2); // 缩放补偿: 显示字号/线宽与图宽无关
|
|
|
- // 部件绘制器: 命中→光晕+sev描边加粗+标名; 未命中→灰描边无字 (纯图形: 只有出问题的那一环有字)
|
|
|
- const part=(n,label,lx,ly,body,halo)=>{
|
|
|
- const k=on(n), col=k?sev:MU;
|
|
|
- return (k&&halo?`<rect class="dt-halo" x="${halo[0]}" y="${halo[1]}" width="${halo[2]}" height="${halo[3]}" rx="8" fill="${sev}"/>`:'')
|
|
|
- +`<g stroke="${col}" stroke-width="${k?SW:0.9*K}" fill="${PN}" fill-opacity="1">${body}</g>`
|
|
|
- +(k?`<text x="${lx}" y="${ly}" text-anchor="middle" font-size="${FS}" font-weight="600" fill="${sev}">${label}</text>`:'');
|
|
|
- };
|
|
|
- // 轴承剖面符号 (上下外圈+滚子+内圈)
|
|
|
- const brg=(x,n,label,ly)=>part(n,label,x,ly,
|
|
|
- `<rect x="${x-17}" y="47" width="34" height="7" rx="1.5"/><rect x="${x-17}" y="96" width="34" height="7" rx="1.5"/>`+
|
|
|
- [-11,0,11].map(d=>`<circle cx="${x+d}" cy="60" r="4.2"/><circle cx="${x+d}" cy="90" r="4.2"/>`).join('')+
|
|
|
- `<rect x="${x-17}" y="66" width="34" height="4" rx="1"/><rect x="${x-17}" y="80" width="34" height="4" rx="1"/>`,[x-24,40,48,70]);
|
|
|
- // 齿轮 = 竖矩形 + 齿纹
|
|
|
- const gear=(x,cy,D,n,label,ly)=>part(n,label,x+6,ly,
|
|
|
- `<rect x="${x}" y="${cy-D/2}" width="13" height="${D}" rx="2"/>`+
|
|
|
- Array.from({length:Math.max(3,Math.round(D/9))},(_,i)=>{const yy=cy-D/2+4+i*(D-8)/Math.max(2,Math.round(D/9)-1);
|
|
|
- return `<line x1="${x}" y1="${yy}" x2="${x+13}" y2="${yy}" stroke-width="${0.7*K}" stroke-opacity="0.55"/>`}).join(''),[x-7,cy-D/2-7,27,D+14]);
|
|
|
- const shaft=(x1,x2,y,th)=>`<line x1="${x1}" y1="${y}" x2="${x2}" y2="${y}" stroke="${LN}" stroke-width="${th||9}" stroke-linecap="round"/>`;
|
|
|
- return `<svg class="dt" viewBox="0 0 660 150" style="display:block;width:${w}px;max-width:100%;height:auto;margin:6px auto">
|
|
|
- <!-- 轮毂+叶根 -->
|
|
|
- <g stroke="${MU}" fill="${PN}"><path d="M22,52 L48,62 L48,88 L22,98 Z" stroke-width="1"/>
|
|
|
- <line x1="22" y1="75" x2="6" y2="75" stroke-width="3"/><line x1="24" y1="60" x2="10" y2="34" stroke-width="3"/>
|
|
|
- <line x1="24" y1="90" x2="10" y2="116" stroke-width="3"/></g>
|
|
|
- ${shaft(46,206,75,12)}
|
|
|
- ${brg(88,'主轴承前','主轴承 前',126)}${brg(154,'主轴承后','主轴承 后',126)}
|
|
|
- <!-- 齿轮箱壳 -->
|
|
|
- <rect x="196" y="16" width="242" height="118" rx="7" fill="none"
|
|
|
- stroke="${gball?sev:LN}" stroke-width="${gball?1.6:1}" stroke-dasharray="4 3" stroke-opacity="${gball?0.9:0.6}"/>
|
|
|
- <!-- 一级行星: 齿圈(上下)+行星轮+太阳轮+行星架 -->
|
|
|
- ${part('行星','行星级',236,146,
|
|
|
- `<rect x="210" y="24" width="52" height="8" rx="2"/><rect x="210" y="118" width="52" height="8" rx="2"/>
|
|
|
- <rect x="228" y="38" width="14" height="26" rx="2"/><rect x="228" y="86" width="14" height="26" rx="2"/>
|
|
|
- <rect x="228" y="62" width="14" height="26" rx="2"/>
|
|
|
- <path d="M210,75 L228,51 M210,75 L228,99" stroke-width="${1.4*K}" fill="none"/>`,[203,17,66,116])}
|
|
|
- ${shaft(256,306,75,7)}
|
|
|
- <!-- 二级 中速→中间轴 (节点相切) -->
|
|
|
- ${gear(300,75,46,'中间','中间轴',146)}
|
|
|
- ${gear(300,37,30,'中间','',0)}
|
|
|
- ${shaft(306,352,37,6)}
|
|
|
- <!-- 三级 中间轴→高速轴 -->
|
|
|
- ${gear(348,37,50,'高速','高速轴',146)}
|
|
|
- ${gear(348,75,26,'高速','',0)}
|
|
|
- ${shaft(360,438,75,8)}
|
|
|
- <!-- 联轴器 -->
|
|
|
- <g stroke="${MU}" fill="${PN}"><rect x="438" y="62" width="6" height="26" rx="1"/><rect x="452" y="62" width="6" height="26" rx="1"/>
|
|
|
- <line x1="444" y1="75" x2="452" y2="75" stroke-width="2"/></g>
|
|
|
- <!-- 发电机: 壳+定子+转子+两端轴承 -->
|
|
|
- <rect x="462" y="34" width="184" height="82" rx="7" fill="none" stroke="${LN}" stroke-dasharray="4 3" stroke-opacity="0.6"/>
|
|
|
- <g stroke="${MU}" fill="${PN}"><rect x="486" y="42" width="136" height="12" rx="2"/><rect x="486" y="96" width="136" height="12" rx="2"/>
|
|
|
- <rect x="492" y="60" width="124" height="30" rx="3" fill-opacity="0.35"/></g>
|
|
|
- ${shaft(430,652,75,8)}
|
|
|
- ${part('发电机DE','发电机 DE',478,146,`<rect x="470" y="56" width="16" height="12" rx="2"/><rect x="470" y="82" width="16" height="12" rx="2"/>
|
|
|
- <circle cx="478" cy="64" r="3.4"/><circle cx="478" cy="86" r="3.4"/>`,[464,48,28,54])}
|
|
|
- ${part('发电机NDE','发电机 NDE',628,146,`<rect x="620" y="56" width="16" height="12" rx="2"/><rect x="620" y="82" width="16" height="12" rx="2"/>
|
|
|
- <circle cx="628" cy="64" r="3.4"/><circle cx="628" cy="86" r="3.4"/>`,[614,48,28,54])}
|
|
|
- </svg>`};
|
|
|
-
|
|
|
-function chip(s){return `<span class="chip" style="--c:${C[s]||'#9aa4b0'}">${s}</span>`}
|
|
|
-function sc(v,lo,hi,a,b){return hi>lo? a+(b-a)*(v-lo)/(hi-lo) : (a+b)/2}
|
|
|
-// 图幅分级 (2026-08-28 用户令"根据系统和分类要合理大小"):
|
|
|
-// L=整行 (传动链/振动标量/功率曲线 — 需要横向空间, 但图身封顶 880/760 不随容器无限拉伸)
|
|
|
-// M=常规格 (月度趋势/计数条 — 两张并排可比)
|
|
|
-const FIGW = {vib: 880, vibbar: 900, xy: 760, band: 620, bar: 620, line: 620};
|
|
|
-const isL = f => f.size === 'L' || ['vib', 'vibbar', 'xy'].includes(f.kind);
|
|
|
-const figWrap = (f, inner, note) => `<div class="card"${isL(f) ? ' style="grid-column:1/-1"' : ''}>
|
|
|
- <h3>${f.title || ''}</h3><div style="max-width:${FIGW[f.kind] || 620}px;margin:0 auto">${inner}</div>
|
|
|
- ${note ? `<p class="small" style="margin-top:6px;color:var(--muted)">${note}</p>` : ''}</div>`;
|
|
|
-
|
|
|
-// 故障模式四象限 (经典可靠性分析)。x=发生条数(对数) y=单次中位时长(对数);
|
|
|
-// 右下=高频短时(抖动/刷屏, 调参或屏蔽) 左上=低频长时(硬故障, 备件/检修)
|
|
|
-// 右上=高频长时(重点治理) 左下=低频短时(本底)。点面积=影响台数, 分离单台刷屏与批次共性。
|
|
|
-function quadChart(items){
|
|
|
- const it=(items||[]).filter(x=>x&&x.n>0&&x.med>0);
|
|
|
- if(it.length<3) return '<p class="small">时间窗内码数不足, 不作四象限</p>';
|
|
|
- const W=720,H=330,ML=58,MR=16,MB=44,MT=16;
|
|
|
- const lx=x=>Math.log10(x), xs=it.map(d=>lx(d.n)), ys=it.map(d=>lx(d.med));
|
|
|
- const x0=Math.min(...xs)-0.15,x1=Math.max(...xs)+0.15,y0=Math.min(...ys)-0.15,y1=Math.max(...ys)+0.15;
|
|
|
- const PX=v=>ML+(lx(v)-x0)/(x1-x0)*(W-ML-MR), PY=v=>H-MB-(lx(v)-y0)/(y1-y0)*(H-MT-MB);
|
|
|
- // 分界: 条数中位 与 单次时长 600s(10min, 一个SCADA采样格) — 时长界是物理刻度不是分位数
|
|
|
- const srt=[...it].map(d=>d.n).sort((a,b)=>a-b), nmid=srt[Math.floor(srt.length/2)];
|
|
|
- const cx=PX(nmid), cy=PY(600);
|
|
|
- const mt=Math.max(...it.map(d=>d.nt));
|
|
|
- const pts=it.map(d=>{
|
|
|
- const r=3+Math.sqrt(d.nt/mt)*7, hot=(d.n>=nmid&&d.med>=600);
|
|
|
- return `<circle cx="${PX(d.n).toFixed(1)}" cy="${PY(d.med).toFixed(1)}" r="${r.toFixed(1)}"
|
|
|
- fill="${hot?'var(--bad)':'var(--accent)'}" fill-opacity="0.5" stroke="${hot?'var(--bad)':'var(--accent)'}" stroke-width="1">
|
|
|
- <title>${d.code} ${d.k}
|
|
|
-${d.n} 条 · 单次中位 ${d.med}s · 累计 ${d.h}h · ${d.nt} 台</title></circle>`;}).join('');
|
|
|
- const lab=it.filter(d=>d.n>=nmid||d.med>=600).slice(0,8).map(d=>
|
|
|
- `<text x="${(PX(d.n)+9).toFixed(1)}" y="${(PY(d.med)+3).toFixed(1)}" font-size="9" fill="var(--muted)">${d.code}</text>`).join('');
|
|
|
- const qn=(x,y,t,sub)=>`<text x="${x}" y="${y}" font-size="10" fill="var(--muted)" opacity="0.85">${t}</text>
|
|
|
- <text x="${x}" y="${y+12}" font-size="8.5" fill="var(--muted)" opacity="0.6">${sub}</text>`;
|
|
|
- const tick=v=>v>=3600?(v/3600)+'h':(v>=60?(v/60)+'min':v+'s');
|
|
|
- const yt=[10,60,600,3600,36000].filter(v=>lx(v)>=y0&&lx(v)<=y1).map(v=>
|
|
|
- `<line x1="${ML}" y1="${PY(v).toFixed(1)}" x2="${W-MR}" y2="${PY(v).toFixed(1)}" stroke="var(--line)" stroke-opacity="0.4"/>
|
|
|
- <text x="${ML-6}" y="${(PY(v)+3).toFixed(1)}" font-size="9" fill="var(--muted)" text-anchor="end">${tick(v)}</text>`).join('');
|
|
|
- const xt=[1,10,100,1000,10000].filter(v=>lx(v)>=x0&&lx(v)<=x1).map(v=>
|
|
|
- `<text x="${PX(v).toFixed(1)}" y="${H-MB+14}" font-size="9" fill="var(--muted)" text-anchor="middle">${v}</text>`).join('');
|
|
|
- return `<svg class="dt" viewBox="0 0 ${W} ${H}" style="width:${W}px;max-width:100%;height:auto">
|
|
|
- ${yt}${xt}
|
|
|
- <line x1="${cx.toFixed(1)}" y1="${MT}" x2="${cx.toFixed(1)}" y2="${H-MB}" stroke="var(--muted)" stroke-dasharray="3 3" stroke-opacity="0.5"/>
|
|
|
- <line x1="${ML}" y1="${cy.toFixed(1)}" x2="${W-MR}" y2="${cy.toFixed(1)}" stroke="var(--muted)" stroke-dasharray="3 3" stroke-opacity="0.5"/>
|
|
|
- ${qn(ML+6,MT+12,'低频长时 · 硬故障','备件与检修窗')}
|
|
|
- ${qn(W-MR-96,MT+12,'高频长时 · 重点治理','优先派工')}
|
|
|
- ${qn(ML+6,H-MB-18,'低频短时 · 本底','不单独处置')}
|
|
|
- ${qn(W-MR-96,H-MB-18,'高频短时 · 重复触发','调整参数或屏蔽, 非硬件故障')}
|
|
|
- ${pts}${lab}
|
|
|
- <text x="${(ML+(W-MR-ML)/2).toFixed(1)}" y="${H-6}" font-size="9.5" fill="var(--muted)" text-anchor="middle">发生条数 (对数)</text>
|
|
|
- <text x="14" y="${(MT+(H-MB-MT)/2).toFixed(1)}" font-size="9.5" fill="var(--muted)" text-anchor="middle" transform="rotate(-90 14 ${(MT+(H-MB-MT)/2).toFixed(1)})">单次中位时长 (对数)</text>
|
|
|
- </svg>`;
|
|
|
-}
|
|
|
-
|
|
|
-// 月度故障率图 (2026-08-28 审核逮的反向误读)。
|
|
|
-// 原图画"月度条数", 末月 1371 条看着是全时间窗最低=在改善; 但 2026-07 只有 6 天数据(覆盖 19.4%),
|
|
|
-// 按日均归一后是 228 条/日 = 全时间窗第二高, 是 6 月(75)的 3 倍 — 方向完全相反。
|
|
|
-// ⇒ 残月绝不能与整月并列比较。柱=日均(可比), 灰斜纹=数据不满月, 数字标总条数(原始事实)。
|
|
|
-function rateChart(mo){
|
|
|
- const ms=mo.months, vs=mo.vals, cov=mo.cov||ms.map(()=>1), pd=mo.per_day||[];
|
|
|
- const W=720,H=250,ML=48,MR=14,MB=52,MT=18, n=ms.length;
|
|
|
- const mx=Math.max(...pd.filter(x=>x!=null),1), bw=(W-ML-MR)/n;
|
|
|
- let bars='', lab='';
|
|
|
- ms.forEach((m,i)=>{
|
|
|
- const v=pd[i], part=cov[i]<0.5;
|
|
|
- if(v==null) return;
|
|
|
- const hh=v/mx*(H-MT-MB), bx=ML+i*bw+bw*0.16, by=H-MB-hh;
|
|
|
- bars+=`<rect x="${bx.toFixed(1)}" y="${by.toFixed(1)}" width="${(bw*0.68).toFixed(1)}" height="${hh.toFixed(1)}" rx="2"
|
|
|
- fill="${part?'var(--muted)':'var(--accent)'}" fill-opacity="${part?0.32:0.8}"
|
|
|
- stroke="${part?'var(--warn)':'none'}" stroke-dasharray="${part?'3 2':''}">
|
|
|
- <title>${m}: ${v} 条/日 (总 ${vs[i]} 条, 数据覆盖 ${(cov[i]*100).toFixed(0)}%)</title></rect>`;
|
|
|
- lab+=`<text x="${(bx+bw*0.34).toFixed(1)}" y="${(by-4).toFixed(1)}" font-size="9" fill="var(--muted)" text-anchor="middle">${v}</text>`
|
|
|
- +`<text x="${(bx+bw*0.34).toFixed(1)}" y="${H-MB+13}" font-size="9" fill="var(--muted)" text-anchor="middle">${m.slice(5)}月</text>`
|
|
|
- +`<text x="${(bx+bw*0.34).toFixed(1)}" y="${H-MB+25}" font-size="8" fill="var(--muted)" opacity="0.65" text-anchor="middle">${vs[i]}条</text>`
|
|
|
- +(part?`<text x="${(bx+bw*0.34).toFixed(1)}" y="${H-MB+37}" font-size="8" fill="var(--warn)" text-anchor="middle">仅${Math.round(cov[i]*30)}天</text>`:'');
|
|
|
- });
|
|
|
- const yt=[0,.5,1].map(f=>`<text x="${ML-6}" y="${(H-MB-f*(H-MT-MB)+3).toFixed(1)}" font-size="9" fill="var(--muted)" text-anchor="end">${Math.round(mx*f)}</text>`).join('');
|
|
|
- return `<svg class="dt" viewBox="0 0 ${W} ${H}" style="width:${W}px;max-width:100%;height:auto">
|
|
|
- <line x1="${ML}" y1="${H-MB}" x2="${W-MR}" y2="${H-MB}" stroke="var(--line)"/>
|
|
|
- <line x1="${ML}" y1="${MT}" x2="${ML}" y2="${H-MB}" stroke="var(--line)"/>
|
|
|
- ${bars}${lab}${yt}
|
|
|
- <text x="14" y="${(MT+(H-MB-MT)/2).toFixed(1)}" font-size="9.5" fill="var(--muted)" text-anchor="middle"
|
|
|
- transform="rotate(-90 14 ${(MT+(H-MB-MT)/2).toFixed(1)})">条/日</text></svg>`;
|
|
|
-}
|
|
|
-
|
|
|
-// 能量账瀑布 (经典发电量分析)。四行工况表回答不了业主唯一关心的问题: 电去哪了。
|
|
|
-// 左=理论可发, 中间逐段扣损(按损失降序), 右=实际上网; 段高即电量, 一眼看出谁是大头。
|
|
|
-// 物理闸: 理论可发不得超 台数×额定×窗时长 (能量守恒, memory ledger-loss-numbers-need-physical-cap-check)。
|
|
|
-const LOSSPLAIN = {
|
|
|
- '限电·绑定':'限电 (命令与实际功率对得上)',
|
|
|
- '限电命令·未绑定':'限电命令期 · 功率未被压制',
|
|
|
- '停机(调度令)':'调度令停机',
|
|
|
- '停机':'故障与检修停机',
|
|
|
- '正常发电':'正常发电时的欠发',
|
|
|
- '低风待机':'低风待机'};
|
|
|
-function waterfall(e){
|
|
|
- if(!e||!e.theo) return '';
|
|
|
- const W=720,H=300,ML=8,MR=8,MT=30,MB=64, items=(e.items||[]).filter(x=>x.loss>0);
|
|
|
- const cols=items.length+2, cw=(W-ML-MR)/cols, sc=(H-MT-MB)/e.theo;
|
|
|
- let run=e.theo, h='', i=0;
|
|
|
- const bar=(idx,y,hh,fill,op)=>`<rect x="${(ML+idx*cw+cw*0.14).toFixed(1)}" y="${y.toFixed(1)}"
|
|
|
- width="${(cw*0.72).toFixed(1)}" height="${Math.max(hh,1.2).toFixed(1)}" rx="2" fill="${fill}" fill-opacity="${op}"/>`;
|
|
|
- const lab=(idx,y,t,v,c)=>`<text x="${(ML+idx*cw+cw*0.5).toFixed(1)}" y="${(y-6).toFixed(1)}" font-size="10"
|
|
|
- fill="${c||'var(--fg)'}" text-anchor="middle" font-weight="600">${v}</text>`
|
|
|
- +`<text x="${(ML+idx*cw+cw*0.5).toFixed(1)}" y="${H-MB+14}" font-size="8.5" fill="var(--muted)" text-anchor="middle">${t}</text>`;
|
|
|
- h+=bar(0,H-MB-e.theo*sc,e.theo*sc,'var(--info)',0.45)
|
|
|
- +lab(0,H-MB-e.theo*sc,'理论可发',Math.round(e.theo).toLocaleString());
|
|
|
- items.forEach(x=>{ i++;
|
|
|
- const top=H-MB-run*sc, hh=x.loss*sc; run-=x.loss;
|
|
|
- const nm=LOSSPLAIN[x.k]||x.k;
|
|
|
- h+=bar(i,top,hh,'var(--bad)',0.55)
|
|
|
- +`<line x1="${(ML+(i-1)*cw+cw*0.86).toFixed(1)}" y1="${top.toFixed(1)}" x2="${(ML+i*cw+cw*0.14).toFixed(1)}" y2="${top.toFixed(1)}"
|
|
|
- stroke="var(--muted)" stroke-dasharray="2 2" stroke-opacity="0.6"/>`
|
|
|
- +lab(i,top,nm.length>10?nm.slice(0,9)+'…':nm,'−'+Math.round(x.loss).toLocaleString(),'var(--bad)')
|
|
|
- +`<title>${nm}: 损失 ${x.loss.toLocaleString()} MWh, 占理论 ${(x.loss/e.theo*100).toFixed(1)}%, 时长 ${x.h.toLocaleString()}h</title>`;
|
|
|
- });
|
|
|
- h+=bar(cols-1,H-MB-e.act*sc,e.act*sc,'var(--ok)',0.6)
|
|
|
- +lab(cols-1,H-MB-e.act*sc,'实际上网',Math.round(e.act).toLocaleString(),'var(--ok)');
|
|
|
- return `<svg class="dt" viewBox="0 0 ${W} ${H}" style="width:${W}px;max-width:100%;height:auto">
|
|
|
- <line x1="${ML}" y1="${H-MB}" x2="${W-MR}" y2="${H-MB}" stroke="var(--line)"/>
|
|
|
- <text x="${ML}" y="14" font-size="9.5" fill="var(--muted)">MWh</text>${h}</svg>`;
|
|
|
-}
|
|
|
-
|
|
|
-// 可靠性-可维修性象限 (经典 RAM 分析, 2026-08-28 用户令: 停机事件化与可靠性可视化)。
|
|
|
-// x=MTBF 平均无故障间隔(可靠性), y=MDT 平均单次停机时长(可维修性), 点面积=累计停机时长。
|
|
|
-// 四象限的处置方向完全不同 — 表格按"停机时长"排序把这层结构整个抹掉了:
|
|
|
-// 左上 常坏且难修 → 最高优先级 左下 频发易修 → 查根因(治本, 换件无用)
|
|
|
-// 右上 罕发难修 → 备件与作业窗 右下 少发好修 → 维持
|
|
|
-function ramChart(tb){
|
|
|
- const it=(tb||[]).filter(x=>x.MTBF_h&&x.MDT_h);
|
|
|
- if(it.length<3) return '<p class="small">部件数不足, 不作象限</p>';
|
|
|
- const W=720,H=340,ML=56,MR=112,MB=48,MT=16;
|
|
|
- const lg=v=>Math.log10(v), xs=it.map(d=>lg(d.MTBF_h)), ys=it.map(d=>d.MDT_h);
|
|
|
- const x0=Math.min(...xs)-0.08,x1=Math.max(...xs)+0.08,y0=0,y1=Math.max(...ys)*1.12;
|
|
|
- const PX=v=>ML+(lg(v)-x0)/(x1-x0)*(W-ML-MR), PY=v=>H-MB-(v-y0)/(y1-y0)*(H-MT-MB);
|
|
|
- const md=a=>{const z=[...a].sort((p,q)=>p-q);return z[Math.floor(z.length/2)]};
|
|
|
- const cx=PX(md(it.map(d=>d.MTBF_h))), cy=PY(md(ys));
|
|
|
- const mh=Math.max(...it.map(d=>d.停机时长h));
|
|
|
- const pts=it.map(d=>{
|
|
|
- const r=4+Math.sqrt(d.停机时长h/mh)*13;
|
|
|
- const ext=d.外部, hot=(d.MTBF_h<md(it.map(v=>v.MTBF_h))&&d.MDT_h>=md(ys));
|
|
|
- const c=ext?'var(--muted)':(hot?'var(--bad)':'var(--accent)');
|
|
|
- return `<circle cx="${PX(d.MTBF_h).toFixed(1)}" cy="${PY(d.MDT_h).toFixed(1)}" r="${r.toFixed(1)}"
|
|
|
- fill="${c}" fill-opacity="${ext?0.3:0.42}" stroke="${c}" stroke-width="1.2"
|
|
|
- stroke-dasharray="${ext?'3 2':''}"><title>${d.部件}: 平均无故障间隔(MTBF)${d.MTBF_h}h · 单次停机时长(MDT)${d.MDT_h}h · 停机 ${d.停机时长h}h · ${d.事件数}起 · ${d.影响台数} 台机组${ext?' · 外部类不计设备可靠性':''}</title></circle>
|
|
|
- <text x="${(PX(d.MTBF_h)+r+4).toFixed(1)}" y="${(PY(d.MDT_h)+3.5).toFixed(1)}" font-size="9.5"
|
|
|
- fill="${ext?'var(--muted)':'currentColor'}">${d.部件.slice(0,7)}</text>`;}).join('');
|
|
|
- const qn=(x,y,a,t,sub)=>`<text x="${x}" y="${y}" font-size="10" fill="var(--muted)" text-anchor="${a}" opacity="0.9">${t}</text>
|
|
|
- <text x="${x}" y="${y+12}" font-size="8.5" fill="var(--muted)" text-anchor="${a}" opacity="0.62">${sub}</text>`;
|
|
|
- const xt=[1000,2000,5000,10000,20000].filter(v=>lg(v)>=x0&&lg(v)<=x1).map(v=>
|
|
|
- `<line x1="${PX(v).toFixed(1)}" y1="${MT}" x2="${PX(v).toFixed(1)}" y2="${H-MB}" stroke="var(--line)" stroke-opacity="0.3"/>
|
|
|
- <text x="${PX(v).toFixed(1)}" y="${H-MB+14}" font-size="9" fill="var(--muted)" text-anchor="middle">${v>=1000?(v/1000)+'k':v}</text>`).join('');
|
|
|
- const yt=[0,5,10,15,20].filter(v=>v<=y1).map(v=>
|
|
|
- `<text x="${ML-6}" y="${(PY(v)+3).toFixed(1)}" font-size="9" fill="var(--muted)" text-anchor="end">${v}</text>`).join('');
|
|
|
- return `<svg class="dt" viewBox="0 0 ${W} ${H}" style="width:${W}px;max-width:100%;height:auto">
|
|
|
- ${xt}${yt}
|
|
|
- <line x1="${ML}" y1="${H-MB}" x2="${W-MR}" y2="${H-MB}" stroke="var(--line)"/>
|
|
|
- <line x1="${ML}" y1="${MT}" x2="${ML}" y2="${H-MB}" stroke="var(--line)"/>
|
|
|
- <line x1="${cx.toFixed(1)}" y1="${MT}" x2="${cx.toFixed(1)}" y2="${H-MB}" stroke="var(--muted)" stroke-dasharray="3 3" stroke-opacity="0.45"/>
|
|
|
- <line x1="${ML}" y1="${cy.toFixed(1)}" x2="${W-MR}" y2="${cy.toFixed(1)}" stroke="var(--muted)" stroke-dasharray="3 3" stroke-opacity="0.45"/>
|
|
|
- ${qn(ML+6,MT+12,'start','常坏且难修','最高优先级')}
|
|
|
- ${qn(W-MR-6,MT+12,'end','罕发难修','备件与作业窗')}
|
|
|
- ${qn(ML+6,H-MB-16,'start','频发易修','查根因, 换件无用')}
|
|
|
- ${qn(W-MR-6,H-MB-16,'end','少发好修','维持')}
|
|
|
- ${pts}
|
|
|
- <text x="${(ML+(W-MR-ML)/2).toFixed(1)}" y="${H-8}" font-size="9.5" fill="var(--muted)" text-anchor="middle">平均无故障间隔 MTBF (h, 对数) → 越右越可靠</text>
|
|
|
- <text x="13" y="${(MT+(H-MB-MT)/2).toFixed(1)}" font-size="9.5" fill="var(--muted)" text-anchor="middle"
|
|
|
- transform="rotate(-90 13 ${(MT+(H-MB-MT)/2).toFixed(1)})">单次停机时长(MDT,h)→ 越上越难修</text>
|
|
|
- <circle cx="${W-MR+16}" cy="${MT+40}" r="5" fill="var(--accent)" fill-opacity="0.42" stroke="var(--accent)"/>
|
|
|
- <text x="${W-MR+26}" y="${MT+44}" font-size="9" fill="var(--muted)">设备类</text>
|
|
|
- <circle cx="${W-MR+16}" cy="${MT+58}" r="5" fill="var(--muted)" fill-opacity="0.3" stroke="var(--muted)" stroke-dasharray="3 2"/>
|
|
|
- <text x="${W-MR+26}" y="${MT+62}" font-size="9" fill="var(--muted)">外部类</text>
|
|
|
- <text x="${W-MR+6}" y="${MT+84}" font-size="8.5" fill="var(--muted)">圆面积 = 累计停机时长</text>
|
|
|
- <text x="${W-MR+6}" y="${MT+98}" font-size="8.5" fill="var(--muted)">虚线 = 本场中位分界</text>
|
|
|
- </svg>`;
|
|
|
-}
|
|
|
-
|
|
|
-// 归因覆盖率条: "无记录停机" 是最大单项却因不给 MTBF 而在可靠性排序里隐身 (规划 P1-2)
|
|
|
-function attribBar(tb, tot){
|
|
|
- const nore=(tb||[]).find(x=>x.部件==='无记录停机');
|
|
|
- const unk=nore?nore.停机时长h:0, un2=(tb||[]).filter(x=>x.部件==='未归类').reduce((a,b)=>a+b.停机时长h,0);
|
|
|
- const ok=tot-unk-un2, W=720,H=54;
|
|
|
- const seg=(x,w,c,op)=>`<rect x="${x.toFixed(1)}" y="14" width="${Math.max(w,1).toFixed(1)}" height="22" rx="3" fill="${c}" fill-opacity="${op}"/>`;
|
|
|
- const w1=ok/tot*W, w2=un2/tot*W, w3=unk/tot*W;
|
|
|
- return `<svg class="dt" viewBox="0 0 ${W} ${H}" style="width:${W}px;max-width:100%;height:auto">
|
|
|
- ${seg(0,w1,'var(--accent)',0.55)}${seg(w1,w2,'var(--warn)',0.5)}${seg(w1+w2,w3,'var(--bad)',0.5)}
|
|
|
- <text x="4" y="10" font-size="9" fill="var(--muted)">已归因到部件 ${Math.round(ok)}h (${(ok/tot*100).toFixed(0)}%)</text>
|
|
|
- <text x="${(w1+w2+w3-2).toFixed(1)}" y="10" font-size="9" fill="var(--bad)" text-anchor="end">无记录 ${Math.round(unk)}h (${(unk/tot*100).toFixed(0)}%)</text>
|
|
|
- <text x="${(w1+w2/2).toFixed(1)}" y="50" font-size="9" fill="var(--warn)" text-anchor="middle">未归类 ${Math.round(un2)}h</text>
|
|
|
- </svg>`;
|
|
|
-}
|
|
|
-
|
|
|
-// 失败必须响亮 (2026-08-28 全系统盘点逮): 后端 m8/m9/rel/fus 四块都是
|
|
|
-// `except: X = {err: ...}`, 前端一律 `&&!d.X.err` 直接跳过渲染 —— 界面看不出"这块没算出来",
|
|
|
-// 看起来只是"这一页没这个内容"。reliability 的 bool 位取反 bug 就是这样活下来的。
|
|
|
-// ⇒ 出错时改为渲染一张醒目失败卡: 说清哪块没了、意味着什么缺失、错在哪。
|
|
|
-const BLOCKDESC = {
|
|
|
- rel: ['部件停机贡献与可靠性', '本页看不到部件级平均无故障间隔(MTBF)/单次停机时长(MDT)与停机归因'],
|
|
|
- m8: ['停机事件化与故障季节性', '故障统计页缺停机归因与季节形态'],
|
|
|
- m9: ['控制参数一致性', '发电性能页缺封顶分组与桨距调度差'],
|
|
|
- fus: ['振动四源融合', '振动融合页缺逐台判级矩阵'],
|
|
|
-};
|
|
|
-function blockErr(key, err){
|
|
|
- const d = BLOCKDESC[key] || [key, ''];
|
|
|
- return `<div class="card mb" style="border-left:3px solid var(--bad)">
|
|
|
- <h3 style="color:var(--bad)">⚠ ${d[0]} — 未算出</h3>
|
|
|
- <p class="small" style="margin-bottom:6px">${d[1]}。<b>非"本时间窗无数据", 属计算失败</b> — 下方数字不完整, 不要据此下结论。</p>
|
|
|
- <pre class="small" style="white-space:pre-wrap;color:var(--muted);background:var(--bg);padding:8px 10px;border-radius:6px;margin:0">${String(err).slice(0,300)}</pre></div>`;
|
|
|
-}
|
|
|
-
|
|
|
-// 逐页窗声明 (2026-08-28 审核逮的交互欺骗)。
|
|
|
-// 顶部窗选择器对"发电性能"完全无效 — 那 19 张图是预计算产物, 窗在生成时钉死为 2025-07~2026-01;
|
|
|
-// 用户切到"2026年"图纹丝不动却毫无提示。"控制策略"曾更糟(工况态跟随+参数件固定时间窗并列),
|
|
|
-// 两个窗并列在同一页。窗对不齐 = claim窗≠证据窗, 是判据失效的头号来源, 必须写在脸上。
|
|
|
-const WINSPEC = {
|
|
|
- '发电性能': {mode:'fixed', win:'2025-07 ~ 2025-12',
|
|
|
- why:'限电前干净窗 — 2026 起交易策略主导, 曲线与控制参数含策略性少发不可作设备判据。本页含特性曲线与控制参数一致性两件, 同窗'},
|
|
|
- // 2026-08-28 用户裁: 控制参数件(固定时间窗)迁入发电性能, "电量算账"只留跟随时间窗内容
|
|
|
- // → 每页只有一个窗制, 混合窗从根上消掉 (原来是本系统唯一的双窗页)
|
|
|
-};
|
|
|
-function winBanner(tab, curWin, extra){
|
|
|
- const w = WINSPEC[tab];
|
|
|
- if(!w) return `<div class="winb"><b>时间窗</b> ${curWin} · 本页全部数字随上方窗切换${extra?' | '+extra:''}</div>`;
|
|
|
- const c = w.mode==='fixed' ? 'var(--warn)' : 'var(--info)';
|
|
|
- const tag = w.mode==='fixed' ? '固定时间窗 · 不随上方切换' : '本页含两个窗';
|
|
|
- return `<div class="winb" style="border-left-color:${c}">
|
|
|
- <b style="color:${c}">${tag}</b> ${w.win}
|
|
|
- <div class="small" style="color:var(--muted);margin-top:3px">${w.why}${extra?' | '+extra:''}</div></div>`;
|
|
|
-}
|
|
|
-
|
|
|
-// 控制参数三图 (2026-08-28 用户令: 控制参数一致性要图形, 在发电性能展示)。
|
|
|
-// 原来是三段密文字, 关键结论"离散成组≠连续分布"完全看不出来 — 而这正是判"参数级差异"的依据。
|
|
|
-// A 封顶参数数轴: 台数落在几个离散位置上 = 配置/版本差异; 若是磨损或工况会是连续散布。
|
|
|
-// B 桨距同档差: 零中线偏差条, 长度即幅度, 与全场中位同尺比。
|
|
|
-// C 尺子巡检: 极差/σ 比 — 38 台正态抽样的极差期望约 3.7σ, 明显超出即分布非单峰(佐证 A 的分组)。
|
|
|
-function capAxis(P, W9){
|
|
|
- const W=500, H=158, ML=54, MR=34;
|
|
|
- const row=(y, spec, unit, label)=>{
|
|
|
- const gs=Object.entries(spec.分组).map(([k,v])=>({v:parseFloat(k), n:v, raw:k}));
|
|
|
- if(!gs.length) return '';
|
|
|
- const lo=Math.min(...gs.map(g=>g.v)), hi=Math.max(...gs.map(g=>g.v));
|
|
|
- const pad=(hi-lo)*0.22||1, x0=lo-pad, x1=hi+pad;
|
|
|
- const PX=v=>ML+(v-x0)/(x1-x0)*(W-ML-MR);
|
|
|
- const mn=Math.max(...gs.map(g=>g.n));
|
|
|
- const dots=gs.map(g=>{const r=5+Math.sqrt(g.n/mn)*9;
|
|
|
- return `<circle cx="${PX(g.v).toFixed(1)}" cy="${y}" r="${r.toFixed(1)}" fill="var(--accent)" fill-opacity="0.42" stroke="var(--accent)"/>
|
|
|
- <text x="${PX(g.v).toFixed(1)}" y="${y-r-5}" font-size="9" text-anchor="middle" fill="currentColor">${g.raw}</text>
|
|
|
- <text x="${PX(g.v).toFixed(1)}" y="${y+3.5}" font-size="9" text-anchor="middle" fill="var(--panel)" font-weight="600">${g.n}</text>`}).join('');
|
|
|
- const med=spec['中位'];
|
|
|
- return `<text x="${ML-6}" y="${y+3}" font-size="9.5" text-anchor="end" fill="var(--muted)">${label}</text>
|
|
|
- <line x1="${ML}" y1="${y}" x2="${W-MR}" y2="${y}" stroke="var(--line)"/>
|
|
|
- <line x1="${PX(med).toFixed(1)}" y1="${y-24}" x2="${PX(med).toFixed(1)}" y2="${y+18}" stroke="var(--muted)" stroke-dasharray="3 3" stroke-opacity="0.55"/>
|
|
|
- <text x="${PX(med).toFixed(1)}" y="${y+28}" font-size="8.5" text-anchor="middle" fill="var(--muted)">中位 ${med}</text>
|
|
|
- ${dots}`;
|
|
|
- };
|
|
|
- return `<svg class="dt" viewBox="0 0 ${W} ${H}" style="width:${W}px;max-width:100%;height:auto">
|
|
|
- ${row(44, P, 'kW', '满发封顶')}
|
|
|
- ${row(120, W9, 'rpm', '转速封顶')}
|
|
|
- <text x="${W-MR}" y="12" font-size="8.5" text-anchor="end" fill="var(--muted)">圆内数字=台数, 圆面积∝台数</text></svg>`;
|
|
|
-}
|
|
|
-function betaBar(b){
|
|
|
- const outs=(b.离群||[]), med=b['全场同档差中位'];
|
|
|
- const W=500, H=34+outs.length*26, ML=52, MR=58;
|
|
|
- const mx=Math.max(med, ...outs.map(o=>o.平均偏))*1.18||1;
|
|
|
- const PX=v=>ML+v/mx*(W-ML-MR);
|
|
|
- const rows=outs.map((o,i)=>{const y=30+i*26;
|
|
|
- return `<text x="${ML-6}" y="${y+10}" font-size="9.5" text-anchor="end" fill="currentColor">${o.t.replace('WTG','')}#</text>
|
|
|
- <rect x="${ML}" y="${y+1}" width="${(PX(o.平均偏)-ML).toFixed(1)}" height="13" rx="2" fill="var(--warn)" fill-opacity="0.62"/>
|
|
|
- <text x="${(PX(o.平均偏)+5).toFixed(1)}" y="${y+11}" font-size="9" fill="var(--warn)">${o.平均偏}°</text>`}).join('');
|
|
|
- return `<svg class="dt" viewBox="0 0 ${W} ${H}" style="width:${W}px;max-width:100%;height:auto">
|
|
|
- <line x1="${PX(med).toFixed(1)}" y1="18" x2="${PX(med).toFixed(1)}" y2="${H-4}" stroke="var(--ok)" stroke-dasharray="3 3"/>
|
|
|
- <text x="${PX(med).toFixed(1)}" y="12" font-size="8.5" fill="var(--ok)">全场中位 ${med}°</text>
|
|
|
- <line x1="${ML}" y1="18" x2="${ML}" y2="${H-4}" stroke="var(--line)"/>
|
|
|
- ${rows}</svg>`;
|
|
|
-}
|
|
|
-function rulerBar(rl){
|
|
|
- const ks=Object.entries(rl); const W=500, H=26+ks.length*30, ML=88, MR=76;
|
|
|
- const NORM=3.7; // 38 台正态抽样极差期望 ≈ 3.7σ
|
|
|
- const mxr=Math.max(NORM*1.35, ...ks.map(([,v])=>v.极差/v.σ));
|
|
|
- const PX=v=>ML+v/mxr*(W-ML-MR);
|
|
|
- const rows=ks.map(([k,v],i)=>{const y=22+i*30, ratio=v.极差/v.σ, wide=ratio>NORM;
|
|
|
- return `<text x="${ML-6}" y="${y+11}" font-size="9.5" text-anchor="end" fill="currentColor">${k}</text>
|
|
|
- <rect x="${ML}" y="${y+2}" width="${(PX(ratio)-ML).toFixed(1)}" height="12" rx="2"
|
|
|
- fill="${wide?'var(--warn)':'var(--accent)'}" fill-opacity="0.55"/>
|
|
|
- <text x="${(PX(ratio)+5).toFixed(1)}" y="${y+11}" font-size="9" fill="var(--muted)">${ratio.toFixed(1)}σ σ=${v.σ}</text>`}).join('');
|
|
|
- return `<svg class="dt" viewBox="0 0 ${W} ${H}" style="width:${W}px;max-width:100%;height:auto">
|
|
|
- <line x1="${PX(NORM).toFixed(1)}" y1="14" x2="${PX(NORM).toFixed(1)}" y2="${H-4}" stroke="var(--ok)" stroke-dasharray="3 3"/>
|
|
|
- <text x="${PX(NORM).toFixed(1)}" y="10" font-size="8.5" fill="var(--ok)" text-anchor="middle">正态期望 3.7σ</text>
|
|
|
- ${rows}</svg>`;
|
|
|
-}
|
|
|
-
|
|
|
-// 责任分账 (2026-08-28 用户令: 电量算账优化显示)。
|
|
|
-// 瀑布回答"电量流向", 但业主真正要问的下一句是"谁的责任" — 原来只在一段小字里写了个百分比。
|
|
|
-// 三分账: 交易与调度侧(限电两项+调度令停机) / 设备侧(故障与检修停机) / 运行侧(正常发电欠发+低风)。
|
|
|
-function blameBar(e){
|
|
|
- const cat = k => (k.indexOf('限电')>=0 || k.indexOf('调度')>=0) ? 0 : (k==='停机' ? 1 : 2);
|
|
|
- const NM = ['交易与调度侧','设备侧','运行侧'];
|
|
|
- const NOTE = ['限电与调度令, 不进设备责任','故障与检修停机, 唯一进设备考核','正常发电欠发与低风待机'];
|
|
|
- const CL = ['var(--info)','var(--bad)','var(--muted)'];
|
|
|
- const g = [0,0,0];
|
|
|
- (e.items||[]).forEach(x => { g[cat(x.k)] += x.loss; });
|
|
|
- const tot = g.reduce((a,b)=>a+b,0) || 1;
|
|
|
- const W=500, H=44;
|
|
|
- let x=0, seg='';
|
|
|
- g.forEach((v,i)=>{ const w=v/tot*W;
|
|
|
- seg += `<rect x="${x.toFixed(1)}" y="12" width="${Math.max(w,1).toFixed(1)}" height="26" rx="3" fill="${CL[i]}" fill-opacity="0.55"><title>${NM[i]}: ${Math.round(v).toLocaleString()} MWh</title></rect>`
|
|
|
- + (w>46 ? `<text x="${(x+w/2).toFixed(1)}" y="29" font-size="10.5" fill="currentColor" text-anchor="middle" font-weight="600">${(v/tot*100).toFixed(0)}%</text>` : '');
|
|
|
- x += w; });
|
|
|
- // 图例走 HTML 而非 SVG text: SVG 里估算中文字宽不准, 三段说明会互相压字 (实测重叠)
|
|
|
- const leg = g.map((v,i)=>`<div style="display:flex;align-items:baseline;gap:7px;margin-top:5px">
|
|
|
- <i style="flex:none;width:9px;height:9px;border-radius:2px;background:${CL[i]};opacity:.55"></i>
|
|
|
- <span style="font-weight:600">${NM[i]}</span>
|
|
|
- <span class="cnt">${Math.round(v).toLocaleString()} MWh</span>
|
|
|
- <span class="small" style="color:var(--muted)">${NOTE[i]}</span></div>`).join('');
|
|
|
- return `<svg class="dt" viewBox="0 0 ${W} ${H}" style="width:${W}px;max-width:100%;height:auto">
|
|
|
- <text x="0" y="8" font-size="9" fill="var(--muted)">损失 ${Math.round(tot).toLocaleString()} MWh 按责任归属</text>
|
|
|
- ${seg}</svg>${leg}`;
|
|
|
-}
|
|
|
-
|
|
|
-// 帕累托图 (2026-08-28 用户令: 故障统计按经典分析展示)。
|
|
|
-// 经典做法 = 条形按降序 + 右轴累计百分比折线 + 80% 参考线 → 一眼看出"关键少数";
|
|
|
-// 原来只有两列排序表, 看不出前几项就占了大头。
|
|
|
-function paretoChart(items, unit, note){
|
|
|
- const it = (items || []).filter(x => x && x.v > 0);
|
|
|
- if(!it.length) return '<p class="small">时间窗内无数据</p>';
|
|
|
- // 尺寸 (2026-08-28 排布优化): 原 720x260 + 旋转长标签, 两列并排时被缩到 0.60 倍 = 字高 6.5px 不可读。
|
|
|
- // 改紧凑版 520x236: x 轴只标码号(水平, 不旋转 → 底部留白 86→40), 全名进悬停与图注前三行。
|
|
|
- const W = 520, H = 236, ML = 46, MR = 40, MB = 40, MT = 14;
|
|
|
- const n = it.length, tot = it.reduce((a, x) => a + x.v, 0);
|
|
|
- const mx = Math.max(...it.map(x => x.v));
|
|
|
- const bw = (W - ML - MR) / n;
|
|
|
- const short = k => { const m = String(k).match(/^([0-9]+)/); return m ? m[1] : String(k).slice(0, 4); };
|
|
|
- let cum = 0, bars = '', line = [], dots = '', k80 = null;
|
|
|
- it.forEach((x, i) => {
|
|
|
- const hh = (x.v / mx) * (H - MT - MB);
|
|
|
- const bx = ML + i * bw + bw * 0.14, by = H - MB - hh;
|
|
|
- bars += `<rect x="${bx.toFixed(1)}" y="${by.toFixed(1)}" width="${(bw * 0.72).toFixed(1)}" height="${hh.toFixed(1)}"
|
|
|
- rx="2" fill="var(--accent)" fill-opacity="${i < 3 ? 0.85 : 0.45}"><title>${x.k}: ${x.v}${unit}</title></rect>`;
|
|
|
- cum += x.v;
|
|
|
- const pct = cum / tot;
|
|
|
- if(k80 === null && pct >= 0.8) k80 = i + 1;
|
|
|
- const px = ML + i * bw + bw / 2, py = MT + (1 - pct) * (H - MT - MB);
|
|
|
- line.push(`${px.toFixed(1)},${py.toFixed(1)}`);
|
|
|
- dots += `<circle cx="${px.toFixed(1)}" cy="${py.toFixed(1)}" r="2.4" fill="var(--warn)"><title>${x.k} · 累计 ${(pct*100).toFixed(0)}%</title></circle>`;
|
|
|
- });
|
|
|
- const y80 = MT + 0.2 * (H - MT - MB);
|
|
|
- const xt = it.map((x, i) => `<text x="${(ML + i*bw + bw/2).toFixed(1)}" y="${H - MB + 13}" font-size="9"
|
|
|
- fill="var(--muted)" text-anchor="middle">${short(x.k)}</text>`).join('');
|
|
|
- const yt = [0, .5, 1].map(f => `<text x="${ML-5}" y="${(H-MB-(f*(H-MT-MB))+3).toFixed(1)}" font-size="9"
|
|
|
- fill="var(--muted)" text-anchor="end">${Math.round(mx*f).toLocaleString()}</text>`).join('')
|
|
|
- + [0, .5, 1].map(f => `<text x="${W-MR+5}" y="${(MT+(1-f)*(H-MT-MB)+3).toFixed(1)}" font-size="9"
|
|
|
- fill="var(--warn)">${Math.round(f*100)}%</text>`).join('');
|
|
|
- const top3 = it.slice(0, 3).map(x => `<b>${short(x.k)}</b> ${String(x.k).replace(/^[0-9]+ */, '').slice(0, 13)}`).join(' · ');
|
|
|
- return `<svg class="dt" viewBox="0 0 ${W} ${H}" style="width:${W}px;max-width:100%;height:auto">
|
|
|
- <line x1="${ML}" y1="${H-MB}" x2="${W-MR}" y2="${H-MB}" stroke="var(--line)"/>
|
|
|
- <line x1="${ML}" y1="${MT}" x2="${ML}" y2="${H-MB}" stroke="var(--line)"/>
|
|
|
- ${bars}
|
|
|
- <line x1="${ML}" y1="${y80.toFixed(1)}" x2="${W-MR}" y2="${y80.toFixed(1)}" stroke="var(--warn)"
|
|
|
- stroke-dasharray="4 3" stroke-opacity="0.7"/>
|
|
|
- <text x="${W-MR-3}" y="${(y80-4).toFixed(1)}" font-size="9" fill="var(--warn)" text-anchor="end">累计 80%</text>
|
|
|
- <polyline points="${line.join(' ')}" fill="none" stroke="var(--warn)" stroke-width="1.5"/>${dots}
|
|
|
- ${xt}${yt}
|
|
|
- <text x="${(ML+(W-MR-ML)/2).toFixed(1)}" y="${H-3}" font-size="8.5" fill="var(--muted)" text-anchor="middle">报警码 (悬停看全名)</text></svg>
|
|
|
- <p class="small" style="color:var(--muted);margin-top:5px;line-height:1.6">
|
|
|
- ${k80 ? `<b>关键少数: 前 ${k80} 项占 80%</b> (共 ${n} 项, 合计 ${Math.round(tot).toLocaleString()}${unit})` : `共 ${n} 项, 合计 ${Math.round(tot).toLocaleString()}${unit}`}
|
|
|
- <br>前三: ${top3}${note ? '<br>' + note : ''}</p>`;
|
|
|
-}
|
|
|
-
|
|
|
-// ── 风场阵列 (2026-08-31 用户令: 风机与环境形象要用视觉体现) ──────────────
|
|
|
-// 视觉语言与推介页 hero 同源: 细线几何 + stroke-opacity 造景深, 不写实。
|
|
|
-// 它同时是**信息图**不只是插画 —— 每台按其最严重系统状态着色, 全场健康分布一眼可见。
|
|
|
-// 快照版总览原本只有一句"内嵌页不可用", 这里正好补上。
|
|
|
-// ── 运维成熟度与四判据 (2026-08-31) ───────────────────────────────
|
|
|
-// 为什么放在检修决策页顶端: 这四条是"系统会主动拦下来"的判据, 不是又一张统计图。
|
|
|
-// 那三个问题(复发/共性/记录断层)在本场存在五年没被发现, 不是没人懂,
|
|
|
-// 是没有任何环节会主动报出来。
|
|
|
-// 本体里的文本用 **x** 标重点 (那是给人读源文件时用的)。直接插进 HTML 会把星号
|
|
|
-// 原样印在客户交付页上。这里只转粗体, 不引入 markdown 解析器 —— 需求就这一条。
|
|
|
-// 用字符类 [*] 而非反斜杠转义 —— CHART_JS 是**非 raw** 的 Python 三引号串,
|
|
|
-// 反斜杠在这里会被 Python 当转义处理: 反斜杠加星号触发 SyntaxWarning, 反斜杠加 n
|
|
|
-// 直接变成真换行, 把单行注释劈成两行 —— 第二行不再是注释, 当场语法错、页面白屏。
|
|
|
-// (写这条注释时我为了举例又写了一次那个转义, 于是当场复现了它描述的 bug,
|
|
|
-// 被成品页语法闸逮到。所以: 这个字符串里能不写反斜杠就不写, 举例也别写。)
|
|
|
-// 层级后缀单独成常量 —— 原来写成 `${lv}层` 时那个 `层` 是模板里的**字面量孤字**,
|
|
|
-// 数据侧的 `机理` 译成了 Domain model 而它没译, 页面上就是 'Domain model层'。
|
|
|
-// 提成常量后它是一个可查表的完整词。
|
|
|
-const LAYER_SFX = '层';
|
|
|
-const TIMES_SFX = '倍';
|
|
|
-
|
|
|
-const mdB = s => String(s == null ? '' : s).replace(/[*][*]([^*]+)[*][*]/g, '<b>$1</b>');
|
|
|
-
|
|
|
-// 快照版总览面板: 用已固化的数据汇总, 而不是道歉。
|
|
|
-// 逐系统两个键: 裸名 = 有问题的机组, `·良好` = 判良好的机组 —— 汇总时要分开数。
|
|
|
-function snapOverview(d){
|
|
|
- const SY = d.systems || {};
|
|
|
- const rows = [];
|
|
|
- Object.keys(SY).forEach(function(k){
|
|
|
- if (k.indexOf('·') >= 0) return; // 只遍历裸名, 良好那半单独取
|
|
|
- const bad = (SY[k] || []).length;
|
|
|
- const good = (SY[k + '·良好'] || []).length;
|
|
|
- if (bad + good) rows.push({sys: k, bad: bad, good: good});
|
|
|
- });
|
|
|
- rows.sort(function(a, b){ return b.bad - a.bad; });
|
|
|
- const K = d.kpi || {};
|
|
|
- const M = (d.m8 && d.m8.mtbf) || {};
|
|
|
- let h = '<div class="card mb"><div class="eyebrow">Where to look</div>';
|
|
|
- h += '<div style="font-size:12px;color:var(--muted);margin:2px 0 12px;line-height:1.6">'
|
|
|
- + '本页为固化快照: 上方每个标签页的数据都已随页面打包, 断网也能看。'
|
|
|
- + '下表按系统汇总当前状态, 点系统名进该系统的机组清单。</div>';
|
|
|
- if (rows.length){
|
|
|
- h += '<table class="tbl"><thead><tr><th>系统</th><th>需跟踪</th><th>判良好</th>'
|
|
|
- + '<th>占全场</th></tr></thead><tbody>';
|
|
|
- rows.forEach(function(r){
|
|
|
- const tot = r.bad + r.good;
|
|
|
- // 用 data 属性 + 委托事件, **不写嵌套引号** —— CHART_JS 是非 raw 的 Python
|
|
|
- // 三引号串, 里面的反斜杠转义会被 Python 吃掉, 反引号/单引号一嵌套就断串。
|
|
|
- // (今天第三次栽在这条上, 每次都是成品页语法闸拦下来的。)
|
|
|
- h += '<tr><td><a class="tl" href="#" data-gotab="部件问题">'
|
|
|
- + r.sys + '</a></td>'
|
|
|
- + '<td class="num"><b style="color:' + (r.bad ? 'var(--warn)' : 'var(--muted)') + '">'
|
|
|
- + r.bad + '</b></td><td class="num">' + r.good + '</td>'
|
|
|
- + '<td class="num small">' + tot + '</td></tr>';
|
|
|
- });
|
|
|
- h += '</tbody></table>';
|
|
|
- }
|
|
|
- h += '<div class="kpis" style="margin-top:12px">'
|
|
|
- + '<div class="kpi"><b>' + (K['报警台'] != null ? K['报警台'] : '—') + '</b>'
|
|
|
- + '<span>机组带报警级判定</span></div>'
|
|
|
- + '<div class="kpi"><b>' + (K['关注台'] != null ? K['关注台'] : '—') + '</b>'
|
|
|
- + '<span>机组在跟踪清单上</span></div>'
|
|
|
- + '<div class="kpi"><b>' + (M['停机事件数'] != null ? M['停机事件数'] : '—') + '</b>'
|
|
|
- + '<span>停机事件 (本时间窗)</span></div></div>';
|
|
|
- h += '<div style="font-size:12px;color:var(--muted);margin-top:14px;line-height:1.75">'
|
|
|
- + '<b>各屏回答什么</b><br>'
|
|
|
- + '电量算账 — 电去哪了, 每一度归到哪个原因<br>'
|
|
|
- + '部件问题 — 哪台哪系统该管, 依据是什么<br>'
|
|
|
- + '振动融合分析 — 振动与 SCADA 对上没有<br>'
|
|
|
- + '发电性能 — 曲线偏了没有, 偏在哪一段<br>'
|
|
|
- + '故障统计 — 报警的关键少数与可靠性象限<br>'
|
|
|
- + '检修决策 — 数据本身可不可信, 运维体系差在哪</div>';
|
|
|
- h += '</div>';
|
|
|
- return h;
|
|
|
-}
|
|
|
-
|
|
|
-// 总览表里的系统名跳到对应标签页。委托绑定, 只挂一次。
|
|
|
-document.addEventListener('click', function(e){
|
|
|
- var a = e.target && e.target.closest ? e.target.closest('a[data-gotab]') : null;
|
|
|
- if (!a) return;
|
|
|
- e.preventDefault();
|
|
|
- TAB = a.getAttribute('data-gotab');
|
|
|
- render();
|
|
|
-});
|
|
|
-
|
|
|
-function dqFindings(d){
|
|
|
- // 数据可信度 —— 系统自查出的数据质量与口径问题。
|
|
|
- // 放在判据面板之前: 先说"这些数字可不可信", 再说"数字说明什么"。
|
|
|
- // 一个只报结论不报数据缺陷的系统, 是在把风险留给读者。
|
|
|
- if(!d) return '<div class="card">数据可信度加载中…</div>';
|
|
|
- if(d.err) return '<div class="card">数据可信度: '+d.err+'</div>';
|
|
|
- const KIND={data_quality:['数据质量','var(--bad)'], caliber:['口径','var(--warn)'],
|
|
|
- system_coverage:['覆盖','var(--accent)'], tier_trend:['结构趋势','var(--ok)']};
|
|
|
- const GR={established:['定论','var(--bad)'], candidate:['候选','var(--warn)'],
|
|
|
- reference:['参考','var(--muted)']};
|
|
|
- let h='<div class="card"><div class="eyebrow">数据可信度 · 系统自查发现</div>';
|
|
|
- h+='<div style="font-size:12px;color:var(--muted);margin:2px 0 12px;line-height:1.6">'
|
|
|
- +'先回答"这些数字可不可信", 再谈数字说明什么。以下每条都是在做分析的过程中被判据拦下来的, '
|
|
|
- +'不是事后补的说明。</div>';
|
|
|
- (d.rows||[]).forEach(r=>{
|
|
|
- const k=KIND[r.kind]||['其他','var(--muted)'];
|
|
|
- const g=GR[r.grade]||[r.grade,'var(--muted)'];
|
|
|
- h+=`<div style="border:1px solid var(--line);border-left:3px solid ${k[1]};border-radius:var(--r);
|
|
|
- padding:12px 14px;margin-bottom:10px;background:var(--panel-2)">`;
|
|
|
- h+=`<div style="display:flex;gap:8px;align-items:baseline;flex-wrap:wrap;margin-bottom:6px">
|
|
|
- <span style="font-size:11px;padding:1px 8px;border-radius:10px;background:${k[1]};color:#fff">${k[0]}</span>
|
|
|
- <span style="font-size:11px;color:${g[1]};border:1px solid ${g[1]};padding:0 6px;border-radius:10px">${g[0]}</span>
|
|
|
- <span style="font-size:11px;color:var(--muted);font-family:var(--num)">${r.claim_window||''}</span>
|
|
|
- </div>`;
|
|
|
- h+=`<div style="font-size:13px;font-weight:600;line-height:1.5;margin-bottom:6px">${mdB(r.name)}</div>`;
|
|
|
- if((r.evidence||[]).length){
|
|
|
- h+='<ul style="margin:4px 0 6px 16px;padding:0;font-size:12px;color:var(--ink);line-height:1.65">';
|
|
|
- (r.evidence||[]).slice(0,4).forEach(e=>{h+=`<li>${mdB(e)}</li>`});
|
|
|
- h+='</ul>';
|
|
|
- }
|
|
|
- if(r.impact) h+=`<div style="font-size:12px;color:var(--ok);line-height:1.6;margin-top:4px">
|
|
|
- <b>判别 / 处置</b> ${mdB(r.impact)}</div>`;
|
|
|
- if(r.got_wrong) h+=`<div style="font-size:12px;color:var(--warn);line-height:1.6;margin-top:6px;
|
|
|
- padding:6px 10px;background:var(--panel);border-radius:6px;border:1px dashed var(--line)">
|
|
|
- <b>首版判错</b> ${mdB(r.got_wrong)}</div>`;
|
|
|
- // 数据质量标志 + 稳健性复算: 削弱结论的证据, 与证据同级显示
|
|
|
- if(r.quality_flag) h+=`<div style="font-size:12px;color:var(--ink);line-height:1.6;margin-top:6px;
|
|
|
- padding:6px 10px;background:var(--panel);border-radius:6px;border:1px dashed var(--warn)">
|
|
|
- <b style="color:var(--warn)">记录质量</b> ${mdB(r.quality_flag)}</div>`;
|
|
|
- if(r.robust) h+=`<div style="font-size:12px;color:var(--muted);line-height:1.6;margin-top:4px">
|
|
|
- <b>稳健性复算</b> ${mdB(r.robust)}</div>`;
|
|
|
- if(r.basis) h+=`<div style="font-size:12px;color:var(--muted);line-height:1.6;margin-top:4px">
|
|
|
- <b>分类依据</b> ${mdB(r.basis)}</div>`;
|
|
|
- if(r.sensitivity) h+=`<div style="font-size:12px;color:var(--ink);line-height:1.6;margin-top:6px;
|
|
|
- padding:6px 10px;background:var(--panel);border-radius:6px;border:1px dashed var(--accent)">
|
|
|
- <b style="color:var(--accent)">敏感性检验</b> ${mdB(r.sensitivity)}</div>`;
|
|
|
- if(r.age_basis) h+=`<div style="font-size:12px;color:var(--muted);line-height:1.6;margin-top:4px">
|
|
|
- <b>场龄口径</b> ${mdB(r.age_basis)}</div>`;
|
|
|
- // ★边界从 11px/muted 的脚注提到 12px/正文色 (2026-08-31 独立审逮):
|
|
|
- // 它**限定结论成立的范围**, 不是补充说明。原样式让它成为卡片里最小最淡的元素,
|
|
|
- // 排在一串证据数字之后 —— 读者读完数字就走了, 限定语等于没写。
|
|
|
- if(r.caveat) h+=`<div style="font-size:12px;color:var(--ink);line-height:1.6;margin-top:6px;
|
|
|
- padding:6px 10px;background:var(--panel);border-radius:6px;border-left:3px solid var(--muted)">
|
|
|
- <b>结论边界</b> ${mdB(r.caveat)}</div>`;
|
|
|
- h+='</div>';
|
|
|
- });
|
|
|
- h+='</div>';
|
|
|
- return h;
|
|
|
-}
|
|
|
-
|
|
|
-function maintFw(d){
|
|
|
- // 运维管理体系三维骨架 — 架构取自一套跨厂商通用的培训编号体系, 实证取自本场五年工单。
|
|
|
- // 放在四判据之后: 判据回答"哪台该管", 本面板回答"这个场的运维能力结构长什么样"。
|
|
|
- // 配色一律走 CSS 变量 —— 页面有明暗两套 :root, 硬编码色在另一套下不可读。
|
|
|
- if(!d) return '<div class="card">运维体系加载中…</div>';
|
|
|
- if(d.err) return '<div class="card">运维体系: '+d.err+'</div>';
|
|
|
- const c=d.coverage||{}, g=d.gaps||{};
|
|
|
- const tierName={common:'通用 · 换机型仍有效', platform:'机型专属 · 绑定当前机型', basic:'技术基础', eng:'工程改造'};
|
|
|
- const tierCol={common:'var(--accent)', platform:'var(--warn)', basic:'var(--muted)', eng:'var(--unknown)'};
|
|
|
- let h='<div class="card"><div class="eyebrow">运维管理体系 · 系统 × 阶段 × 记录</div>';
|
|
|
- // ★跨厂商借用必须在**用它的地方**声明 (2026-08-31 双票独立命中)。
|
|
|
- // ★这套码是**借来的跨厂商分类法**, 不点任何厂商名 (点了等于用否定把机型漏回去)。
|
|
|
- // 声明写在别的页面等于没声明: 读者是在这一屏做判断的。
|
|
|
- h+='<div style="font-size:12px;color:var(--muted);margin:2px 0 12px;line-height:1.6">'
|
|
|
- +'三位系统码分两层 (3xx 跨机型通用 / 4xx 机型专属), 判据与缺口取自本场五年工单实证。'
|
|
|
- +'<b style="color:var(--warn)">口径声明</b>: '
|
|
|
- // ★整句不嵌 <b> —— 中间插标签会把这段切成多个 PH 片段, 逐片查表再拼回去,
|
|
|
- // 结果是"arenot"这种缺空格拼接 + 某一片查不到就留中文 (实测两种都发生了)。
|
|
|
- // 要强调就整句强调, 别在句中加标记。整段一个 key, 查得到就整段换, 查不到就整段留。
|
|
|
- // ★整句放在**一个**字符串字面量里。apply() 跑在**源码文本**上, 拆成多个字面量拼接时,
|
|
|
- // 源码里它们被换行加 + 号隔开 ⇒ PH 切出的段与渲染后看到的整句**不是同一个东西**,
|
|
|
- // (注意: CHART_JS 是非 raw 的 Python 三引号串, 注释里写反斜杠-n 会变成**真换行**,
|
|
|
- // 把 // 注释劈成两行 —— 第二行不再是注释, 直接语法错。本行原来就踩了这个坑。)
|
|
|
- // 我按渲染后的整句注册 key, 页面上却按源码分段查表 → 全部 miss。
|
|
|
- // (实测踩到: 助手 --segments 报"命中", 页面上仍是中文。验证跑在渲染后, 翻译跑在源码上。)
|
|
|
- +'三位码仅用于能力分级与培训投资参照。部件到系统码的映射由本项目建立、未经确认, 因此结构趋势比绝对占比可靠。</div>';
|
|
|
-
|
|
|
- // ① 覆盖率 — 本轮最实的一条发现
|
|
|
- h+='<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:16px">';
|
|
|
- h+=`<div style="flex:1;min-width:180px;padding:12px 14px;border-radius:var(--r);background:var(--panel-2);border:1px solid var(--line);border-left:3px solid var(--bad)">
|
|
|
- <div style="font-size:28px;font-weight:600;color:var(--bad);font-family:var(--num)">${c.system_pct}%</div>
|
|
|
- <div style="font-size:12px;color:var(--muted);margin-top:2px">工单可归属到系统<br><span style="font-family:var(--num)">${c.mapped} / ${c.n_orders}</span></div></div>`;
|
|
|
- h+=`<div style="flex:1;min-width:180px;padding:12px 14px;border-radius:var(--r);background:var(--panel-2);border:1px solid var(--line);border-left:3px solid var(--warn)">
|
|
|
- <div style="font-size:28px;font-weight:600;color:var(--warn);font-family:var(--num)">${c.part_field_pct}%</div>
|
|
|
- <div style="font-size:12px;color:var(--muted);margin-top:2px">工单填了部件字段<br><span style="font-family:var(--num)">${c.with_part} / ${c.n_orders}</span></div></div>`;
|
|
|
- h+=`<div style="flex:2.2;min-width:280px;padding:12px 14px;border-radius:var(--r);background:var(--accent-soft);border:1px solid var(--line);border-left:3px solid var(--ok)">
|
|
|
- <div style="font-size:12px;color:var(--ok);font-weight:600;margin-bottom:4px">怎么补</div>
|
|
|
- <div style="font-size:12px;color:var(--ink);line-height:1.65">工单模板增加系统码必填项 (31 个系统码下拉), 此后每条工单按构造自动归属。
|
|
|
- <div style="color:var(--muted);margin-top:4px">归属率低是模板缺字段, 不是维修没做 — 后者要贵得多。</div></div></div>`;
|
|
|
- h+='</div>';
|
|
|
-
|
|
|
- // ② 通用 vs 机型专属 — 决定培训投资与合同边界
|
|
|
- h+='<div style="font-size:13px;font-weight:600;margin:16px 0 4px;color:var(--ink)">作业量结构 · 通用能力 vs 机型专属</div>';
|
|
|
- h+='<div style="font-size:12px;color:var(--muted);margin-bottom:8px">通用能力换机型仍成立, 属场站资产; 机型专属绑定当前机队, 更适合合同覆盖</div>';
|
|
|
- const byT=g.by_tier||{}, tks=Object.keys(byT).sort((a,b)=>byT[b].jobs-byT[a].jobs);
|
|
|
- h+='<div style="display:flex;height:24px;border-radius:4px;overflow:hidden;margin-bottom:6px;border:1px solid var(--line)">';
|
|
|
- tks.forEach(t=>{h+=`<div style="width:${byT[t].share}%;background:${tierCol[t]||'var(--muted)'};display:flex;align-items:center;justify-content:center;font-size:11px;color:#fff;font-family:var(--num)">${byT[t].share}%</div>`;});
|
|
|
- h+='</div><div style="font-size:11px;color:var(--muted);margin-bottom:14px">';
|
|
|
- tks.forEach(t=>{h+=`<span style="margin-right:16px"><span style="display:inline-block;width:9px;height:9px;background:${tierCol[t]||'var(--muted)'};border-radius:2px;margin-right:5px;vertical-align:middle"></span>${tierName[t]||t} · <span style="font-family:var(--num)">${byT[t].jobs}</span> 次</span>`;});
|
|
|
- h+='</div>';
|
|
|
-
|
|
|
- // ③ 系统作业量排行
|
|
|
- h+='<div style="font-size:13px;font-weight:600;margin:16px 0 6px;color:var(--ink)">按系统码的作业量</div>';
|
|
|
- h+='<table class="tbl"><tr><th>码</th><th>系统</th><th>类别</th><th>作业次数</th><th>台数</th><th>次/台</th></tr>';
|
|
|
- (d.systems||[]).slice(0,10).forEach(r=>{
|
|
|
- h+=`<tr><td style="font-family:var(--num)">${r.code}</td><td>${r.zh}<div style="font-size:11px;color:var(--muted)">${r.en}</div></td>
|
|
|
- <td style="color:${tierCol[r.tier]||'var(--muted)'};font-size:12px">${tierName[r.tier]||r.tier}</td>
|
|
|
- <td style="font-family:var(--num)">${r.jobs}</td><td style="font-family:var(--num)">${r.n_units}</td><td style="font-family:var(--num)">${r.per_unit}</td></tr>`;});
|
|
|
- h+='</table>';
|
|
|
-
|
|
|
- // ④ 阶段 × 出口闸 × 记录
|
|
|
- h+='<div style="font-size:13px;font-weight:600;margin:18px 0 4px;color:var(--ink)">六个运维阶段与各自的放行条件</div>';
|
|
|
- h+='<div style="font-size:12px;color:var(--muted);margin-bottom:8px">放行条件 = 不满足就不该进下一阶段; 记录 = 该阶段必须留下的凭据</div>';
|
|
|
- h+='<table class="tbl"><tr><th style="width:15%">阶段</th><th style="width:38%">放行条件</th><th>必备记录</th></tr>';
|
|
|
- (d.stages||[]).forEach(st=>{
|
|
|
- const rn=(st.records||[]).map(r=>((d.records||{})[r]||{}).zh||r)
|
|
|
- .map(x=>`<span style="display:inline-block;padding:1px 7px;margin:1px 3px 1px 0;border-radius:10px;background:var(--panel-2);border:1px solid var(--line);font-size:11px">${x}</span>`).join('');
|
|
|
- // ★英文侧 zh 已被 _proj 投影成英文 (裸 zh/en 成对的处理), 两行会显示成一样的字 ——
|
|
|
- // 相同就只出一行。出口闸那列同理: 页面原读 st.gate_zh, 而投影后字段名是 gate,
|
|
|
- // 于是**整列 undefined** —— 而"出口闸"正是这张表最要紧的一列 (无闸的阶段划分只是标签)。
|
|
|
- h+=`<tr><td><span style="font-family:var(--num);color:var(--muted)">${st.seq}</span> ${st.zh||st.en||''}
|
|
|
- ${(st.en&&st.en!==st.zh)?`<div style="font-size:11px;color:var(--muted)">${st.en}</div>`:''}</td>
|
|
|
- <td style="font-size:12px;line-height:1.5">${st.gate||st.gate_zh||''}</td><td>${rn}</td></tr>`;});
|
|
|
- h+='</table>';
|
|
|
-
|
|
|
- // ⑤ 未映射 — 不猜归属
|
|
|
- const um=(g.unmapped_top||[]);
|
|
|
- if(um.length){
|
|
|
- h+='<div style="font-size:11px;color:var(--muted);margin-top:12px;line-height:1.6">未映射到系统码的部件 (不猜归属, 留待补映射表): '
|
|
|
- + um.slice(0,10).map(x=>`${x[0]}<span style="font-family:var(--num)">×${x[1]}</span>`).join('、')+'</div>';}
|
|
|
- h+='</div>';
|
|
|
- return h;
|
|
|
-}
|
|
|
-
|
|
|
-function maintStd(d){
|
|
|
- if(!d) return '<div class="card"><p class="small">载入中…</p></div>';
|
|
|
- if(d.err) return `<div class="card"><p class="small" style="color:var(--bad)">${d.err}</p></div>`;
|
|
|
- const S=d.score||{}; let h='';
|
|
|
- const dims=(S.dims||[]).map(x=>{
|
|
|
- // ★"不可评估"要在**出现的地方**就自明 —— 用户读到这四个字问"什么意思",
|
|
|
- // 说明把解释放在上一行的口径说明里不够。它不是"做得差", 是"没法判":
|
|
|
- // 工单模板里没有记录该证据的位置, 所以既不能记功也不能记过。
|
|
|
- if(!x.evaluable) return `<div class="kpi"><b style="color:var(--muted)">—</b>
|
|
|
- <span>${x.dim} · <b style="color:var(--warn)">不可评估</b>
|
|
|
- <span style="color:var(--muted)">(工单里没有记录这项证据的字段, 所以既不能算做到、也不能算没做到 — 不是 0 分, 是无法判断)</span><br>
|
|
|
- <span style="color:var(--muted)">${mdB(x.detail)}</span></span></div>`;
|
|
|
- const pct=Math.round(x.score/x.full*100);
|
|
|
- const cls=pct>=70?'ok':pct>=40?'':'bad';
|
|
|
- return `<div class="kpi ${cls}"><b>${x.score}<span style="font-size:14px;color:var(--muted)">/${x.full}</span></b>
|
|
|
- <span>${x.dim}<br><span style="color:var(--muted)">${mdB(x.detail)}</span></span></div>`;
|
|
|
- }).join('');
|
|
|
- // 两个分并排、各自带标签; 口径与理由见 src/windscada/maint/standards.py。
|
|
|
- // (页面源码随交付件一起发出去, 内部审核讨论不留在这里。)
|
|
|
- const _en=(typeof window!=='undefined'&&window.__EN);
|
|
|
- const twoScore=(lab,val,ev,n,tone)=>`<div style="flex:1;min-width:190px;padding:12px 14px;
|
|
|
- border-radius:var(--r);background:var(--panel-2);border:1px solid var(--line);border-left:3px solid ${tone}">
|
|
|
- <div style="font-size:12px;color:var(--muted)">${lab||''}</div>
|
|
|
- <div style="font-size:30px;font-weight:600;font-family:var(--num);color:${tone};line-height:1.15">
|
|
|
- ${val!=null?val:'—'}<span style="font-size:14px;color:var(--muted);font-weight:400">/100</span></div>
|
|
|
- <div style="font-size:11.5px;color:var(--muted);margin-top:2px">${ev} / ${n} ${_en?'dimensions measurable':'维可测'}</div></div>`;
|
|
|
- h+=`<div class="card mb"><h3>${_en?'Maintenance scorecard':'运维评分卡'}
|
|
|
- <span class="badge" style="margin-left:10px">${S.stage_label||('第 '+S.stage+' 阶')} · ${S.stage_name}</span></h3>
|
|
|
- <div style="display:flex;gap:12px;flex-wrap:wrap;margin:4px 0 12px">
|
|
|
- ${twoScore(S.record_label,S.record_score,S.record_evaluable,S.record_dims,'var(--bad)')}
|
|
|
- ${twoScore(S.exec_label,S.exec_score,S.exec_evaluable,S.exec_dims,'var(--ok)')}
|
|
|
- </div>
|
|
|
- ${S.score_basis?`<p class="small" style="color:var(--muted);margin:0 2px 10px;line-height:1.6">${mdB(S.score_basis)}</p>`:''}
|
|
|
- <div class="kpis">${dims}</div>
|
|
|
- <p class="small" style="color:var(--warn);margin:8px 2px 0">${mdB(S.caveat)}</p>
|
|
|
- ${S.model_source?`<p class="small" style="color:var(--muted);margin:6px 2px 0;line-height:1.6">
|
|
|
- <b>评分方法来源</b> ${mdB(S.model_source)}</p>`:''}
|
|
|
- ${S.bottleneck?`<p class="small" style="margin:4px 2px 0">当前瓶颈: <b>${S.bottleneck}</b> — 先补这一项, 其余维度的提升都受它限制。</p>`:''}</div>`;
|
|
|
- // R1 复发
|
|
|
- const R=(d.recurrence||[]).slice(0,10);
|
|
|
- if(R.length) h+=`<div class="card mb"><h3>复发 · ${d.recurrence.length} 组</h3>
|
|
|
- <p class="small" style="color:var(--muted)">同一机组同一部件多次更换 — 换件未解决产生它的原因。按作业日去重计数, 非工单条数。</p>
|
|
|
- <table class="tbl"><thead><tr><th>机组</th><th>部件</th><th>作业次数</th><th>首次</th><th>末次</th><th>最短间隔</th></tr></thead><tbody>
|
|
|
- ${R.map(x=>`<tr><td>${x.turbine}</td><td>${x.part}</td><td><b>${x.n_jobs}</b></td>
|
|
|
- <td>${x.first}</td><td>${x.last}</td><td>${x.min_gap_months} 月</td></tr>`).join('')}
|
|
|
- </tbody></table></div>`;
|
|
|
- // R2 批次候选
|
|
|
- const B=(d.batch||[]).slice(0,8);
|
|
|
- if(B.length) h+=`<div class="card mb"><h3>共性候选 · ${d.batch.length} 项</h3>
|
|
|
- <p class="small" style="color:var(--warn)">仅候选。分布形态无法区分 批次 / 共模应力 / 耗损期 — 三者形态相似而处置方向完全不同(追索赔 / 改运行条件 / 排寿命更换)。定性需物料证据。</p>
|
|
|
- <table class="tbl"><thead><tr><th>部件</th><th>涉及台数</th><th>作业次数</th><th>每台</th><th>缺什么才能定性</th></tr></thead><tbody>
|
|
|
- ${B.map(x=>`<tr><td>${x.part}</td><td>${x.n_units}</td><td>${x.n_jobs}</td>
|
|
|
- <td>${x.per_unit}</td><td class="small" style="color:var(--muted)">${x.needs}</td></tr>`).join('')}
|
|
|
- </tbody></table></div>`;
|
|
|
- // R4 断层
|
|
|
- const D=d.discontinuity||{};
|
|
|
- if((D.breaks||[]).length) h+=`<div class="card mb"><h3>记录断层</h3>
|
|
|
- <p class="small">${D.note}</p>
|
|
|
- ${D.breaks.map(b=>`<p class="small"><b>${b.before} → ${b.after}</b>: ${b.n_before} → ${b.n_after} 条 (<b style="color:var(--bad)">${b.ratio}×</b>) — 机组不会一年之间变坏这么多, 变的是记录制度。</p>`).join('')}
|
|
|
- </div>`;
|
|
|
- return h;
|
|
|
-}
|
|
|
-
|
|
|
-function farmScene(d){
|
|
|
- const RANK={'危险':4,'报警':3,'不可判':2,'良好':1,'优秀':0};
|
|
|
- const COL={'危险':'var(--bad)','报警':'var(--bad)','不可判':'var(--unknown)',
|
|
|
- '良好':'var(--ok)','优秀':'var(--st-exc)'};
|
|
|
- const S={};
|
|
|
- Object.entries((d&&d.systems)||{}).forEach(([sys,rows])=>(rows||[]).forEach(r=>{
|
|
|
- if(!r||!r.t) return;
|
|
|
- const st=String(r.st||'').split('·')[0].trim();
|
|
|
- if(!(st in RANK)) return;
|
|
|
- if(!(r.t in S) || RANK[st] > RANK[S[r.t]]) S[r.t]=st;
|
|
|
- }));
|
|
|
- // 补全: 预烤 key 里有全场台号; 在线模式下退回已知台集合
|
|
|
- let all=Object.keys(S);
|
|
|
- if(typeof window.__SNAP__!=='undefined'){
|
|
|
- const ks=new Set();
|
|
|
- Object.keys(window.__SNAP__).forEach(k=>{const m=/[?&]t=([A-Za-z0-9]+)/.exec(k); if(m)ks.add(m[1])});
|
|
|
- if(ks.size>all.length) all=[...ks];
|
|
|
- }
|
|
|
- all=all.sort();
|
|
|
- if(!all.length) return '';
|
|
|
- const n=all.length, W=980, H=190, m=30;
|
|
|
- const per=Math.ceil(n/2), gap=(W-m*2)/(per-1||1);
|
|
|
- let g='';
|
|
|
- all.forEach((t,i)=>{
|
|
|
- const row=i<per?0:1, idx=i<per?i:i-per;
|
|
|
- const x=m+idx*gap+(row?gap/2:0);
|
|
|
- const base=row?150:126, hh=row?30:38, op=row?.55:.92, bw=row?12:15;
|
|
|
- const st=S[t]||'优秀', c=COL[st]||'var(--unknown)';
|
|
|
- const hub=base-hh;
|
|
|
- g+=`<g opacity="${op}"><title>${t} · ${st}</title>`
|
|
|
- +`<path d="M${x} ${base} L${x} ${hub}" stroke="${c}" stroke-width="${row?1:1.3}" fill="none" stroke-linecap="round"/>`
|
|
|
- +`<path d="M${x} ${hub} L${x} ${hub-bw}" stroke="${c}" stroke-width="${row?.9:1.1}" fill="none" stroke-linecap="round"/>`
|
|
|
- +`<path d="M${x} ${hub} L${(x+bw*0.87).toFixed(1)} ${(hub+bw*0.5).toFixed(1)}" stroke="${c}" stroke-width="${row?.9:1.1}" fill="none" stroke-linecap="round"/>`
|
|
|
- +`<path d="M${x} ${hub} L${(x-bw*0.87).toFixed(1)} ${(hub+bw*0.5).toFixed(1)}" stroke="${c}" stroke-width="${row?.9:1.1}" fill="none" stroke-linecap="round"/>`
|
|
|
- +`<circle cx="${x}" cy="${hub}" r="${row?1.5:1.9}" fill="${c}"/></g>`;
|
|
|
- });
|
|
|
- const cnt={};
|
|
|
- Object.values(S).forEach(v=>cnt[v]=(cnt[v]||0)+1);
|
|
|
- const legend=['危险','报警','不可判','良好','优秀'].filter(k=>cnt[k])
|
|
|
- .map(k=>`<span style="display:inline-flex;align-items:center;gap:5px">
|
|
|
- <i style="width:7px;height:7px;border-radius:50%;background:${COL[k]};display:inline-block"></i>${k} ${cnt[k]}</span>`).join('');
|
|
|
- return `<div class="card mb"><svg viewBox="0 0 ${W} ${H}" style="width:100%;height:auto;display:block"
|
|
|
- role="img" aria-label="Fleet layout: each turbine coloured by its most severe system state">
|
|
|
- <defs><linearGradient id="fsea" x1="0" y1="0" x2="0" y2="1">
|
|
|
- <stop offset="0" stop-color="var(--accent)" stop-opacity=".14"/>
|
|
|
- <stop offset="1" stop-color="var(--accent)" stop-opacity="0"/></linearGradient></defs>
|
|
|
- <path d="M0 150 H${W}" stroke="var(--line)" stroke-width="1" fill="none"/>
|
|
|
- <path d="M0 126 H${W}" stroke="var(--line)" stroke-width="1" fill="none" opacity=".5"/>
|
|
|
- <rect x="0" y="150" width="${W}" height="${H-150}" fill="url(#fsea)"/>
|
|
|
- ${g}</svg>
|
|
|
- <p class="small" style="color:var(--muted);display:flex;gap:16px;flex-wrap:wrap;margin:10px 2px 0">
|
|
|
- <b style="color:var(--ink)">全场态势</b>${legend}
|
|
|
- <span style="margin-left:auto">按各台最严重系统状态着色 · 悬停看台号</span></p></div>`;
|
|
|
-}
|
|
|
-
|
|
|
-function drawFig(f){
|
|
|
- if(f.kind==='vib'){return figWrap(f, dtSVG(f.comp||f.text||'',f.sev||'var(--warn)',880), (f.text||'').replace(/</g,'<'))}
|
|
|
- if(f.kind==='text'){return `<div class="card"><h3>${f.title||''}</h3><p class="small">${(f.text||'').replace(/</g,'<')}</p></div>`}
|
|
|
- if(f.kind==='vibbar'){
|
|
|
- // 倍中位条 (1× 为基准竖线; 绝对值量级差 3 个数量级时按绝对值画条会全部顶满 — 速度有效值 0.001 实例)
|
|
|
- const rows=f.rows||[]; if(!rows.length) return '';
|
|
|
- const sig=v=>{const a=Math.abs(v); return a>=100?v.toFixed(0):a>=10?v.toFixed(1):a>=1?v.toFixed(2):a>=0.01?v.toFixed(3):v.toExponential(1)};
|
|
|
- const byM={}; rows.forEach(r=>{(byM[r.指标]=byM[r.指标]||[]).push(r)});
|
|
|
- const LO=0.5, HI=2.5, W2=190, xr=v=>((Math.max(LO,Math.min(HI,v))-LO)/(HI-LO))*W2;
|
|
|
- let inner=`<div class="small" style="color:var(--muted);margin-bottom:8px">条为本台与全场中位之比, 竖线取 1× 基准, 右端封顶 2.5×; 色按 z (稳健离群度): 绿<2 · 橙2~3 · 红≥3</div>`;
|
|
|
- for(const [mk,rs] of Object.entries(byM)){
|
|
|
- inner+=`<div style="margin-bottom:9px;overflow-x:auto"><b class="small" style="color:var(--muted)">${mk}</b><table style="width:100%;margin-top:2px">`;
|
|
|
- rs.forEach(r=>{
|
|
|
- const v=r.倍中位||1, col=Math.abs(r.z)>=3?'var(--bad)':Math.abs(r.z)>=2?'var(--warn)':'var(--ok)';
|
|
|
- const x1=xr(1), xv=xr(v), left=Math.min(x1,xv), wid=Math.max(Math.abs(xv-x1),1.5);
|
|
|
- inner+=`<tr><td class="small" style="width:104px">${r.测点}</td>
|
|
|
- <td style="width:${W2+10}px"><svg class="dt" viewBox="0 0 ${W2} 16" style="width:${W2}px;height:16px">
|
|
|
- <line x1="0" y1="8" x2="${W2}" y2="8" stroke="var(--line)" stroke-width="1"/>
|
|
|
- <line x1="${x1}" y1="1" x2="${x1}" y2="15" stroke="var(--muted)" stroke-width="1"/>
|
|
|
- <rect x="${left}" y="4" width="${wid}" height="8" rx="2" fill="${col}" fill-opacity="0.9"/>
|
|
|
- ${v>HI?`<path d="M${W2-1},8 l-6,-4 v8 z" fill="${col}"/>`:''}</svg></td>
|
|
|
- <td class="num small" style="width:60px;color:${col};font-weight:600">${r.倍中位!=null?r.倍中位+'×':'—'}</td>
|
|
|
- <td class="num small" style="width:80px">${sig(r.本台)}</td>
|
|
|
- <td class="num small" style="width:78px;color:var(--muted)">中位 ${sig(r.全场中位)}</td>
|
|
|
- <td class="num small" style="width:56px;color:${Math.abs(r.z)>=3?'var(--bad)':'var(--muted)'}">z ${r.z}</td>
|
|
|
- <td class="num small" style="width:64px;color:${r.六窗比>1.15?'var(--warn)':'var(--muted)'}">${r.六窗比!=null?'趋势'+r.六窗比+'×':'趋势—'}</td></tr>`;
|
|
|
- });
|
|
|
- inner+='</table></div>';
|
|
|
- }
|
|
|
- return figWrap(f, inner, f.note);
|
|
|
- }
|
|
|
- const W=620,H=250,ML=52,MB=34;
|
|
|
- let xs=f.months||f.x, n=xs.length; if(!n) return '';
|
|
|
- const X=i=>sc(i,0,Math.max(n-1,1),ML,W-12);
|
|
|
- let all=[];
|
|
|
- if(f.kind==='band'){all=[...(f.self||[]),...(f.med||[]),...(f.q1||[]),...(f.q3||[])]}
|
|
|
- else if(f.kind==='bars'){all=[0,...(f.vals||[])]}
|
|
|
- else if(f.kind==='xy'){all=[...(f.self||[]),...(f.med||[])]}
|
|
|
- else {(f.series||[]).forEach(s=>all.push(...s.vals))}
|
|
|
- (f.thresholds||[]).forEach(t=>all.push(t.v));
|
|
|
- all=all.filter(v=>v!==null&&v===v); if(!all.length) return '';
|
|
|
- let lo=Math.min(...all),hi=Math.max(...all); if(lo===hi){lo-=1;hi+=1} const pad=(hi-lo)*0.08; lo-=pad; hi+=pad;
|
|
|
- const Y=v=>sc(v,lo,hi,H-MB,14);
|
|
|
- const line=(vals,st)=>{let p=vals.map((v,i)=>v===null||v!==v?null:`${X(i).toFixed(1)},${Y(v).toFixed(1)}`);
|
|
|
- let segs=[],cur=[]; p.forEach(q=>{if(q)cur.push(q);else{if(cur.length)segs.push(cur);cur=[]}}); if(cur.length)segs.push(cur);
|
|
|
- return segs.map(sg=>`<polyline points="${sg.join(' ')}" style="fill:none;${st}"/>`).join('')}
|
|
|
- let body='';
|
|
|
- if(f.kind==='band'&&f.q1){
|
|
|
- let up=f.q3.map((v,i)=>v===null?null:`${X(i).toFixed(1)},${Y(v).toFixed(1)}`).filter(Boolean);
|
|
|
- let dn=f.q1.map((v,i)=>v===null?null:`${X(i).toFixed(1)},${Y(v).toFixed(1)}`).filter(Boolean).reverse();
|
|
|
- if(up.length&&dn.length) body+=`<polygon points="${up.join(' ')} ${dn.join(' ')}" fill="#cbd5e1" fill-opacity="0.45"/>`;
|
|
|
- }
|
|
|
- if(f.kind==='bars'){const bw=Math.max(3,(W-ML-20)/n*0.6);
|
|
|
- body+=f.vals.map((v,i)=>`<rect x="${(X(i)-bw/2).toFixed(1)}" y="${Y(v).toFixed(1)}" width="${bw.toFixed(1)}" height="${(Y(Math.max(lo,0))-Y(v)).toFixed(1)}" fill="#0f4c75" fill-opacity="0.75"/>`).join('');}
|
|
|
- if(f.med) body+=line(f.med,'stroke:#64748b;stroke-width:1.7');
|
|
|
- if(f.self) body+=line(f.self,'stroke:#c0392b;stroke-width:2.1')+
|
|
|
- f.self.map((v,i)=>v===null||v!==v?'':`<circle cx="${X(i)}" cy="${Y(v)}" r="2.4" fill="var(--bad)"/>`).join('');
|
|
|
- const cols=['#0f4c75','#ef6c00','#0e6e66','#7c5295'];
|
|
|
- (f.series||[]).forEach((s,k)=>{body+=line(s.vals,`stroke:${cols[k%4]};stroke-width:1.8`)});
|
|
|
- (f.thresholds||[]).forEach(t=>{body+=`<line x1="${ML}" x2="${W-12}" y1="${Y(t.v)}" y2="${Y(t.v)}" stroke="var(--muted)" stroke-dasharray="5 4"/>`+
|
|
|
- (t.label?`<text x="${ML+6}" y="${Y(t.v)-4}" font-size="10" fill="var(--muted)">${t.label}</text>`:'')});
|
|
|
- const step=Math.max(1,Math.ceil(n/8));
|
|
|
- let xt=''; for(let i=0;i<n;i+=step) xt+=`<text x="${X(i)}" y="${H-10}" font-size="10" fill="var(--muted)" text-anchor="middle">${String(xs[i]).includes('-')?String(xs[i]).slice(2,7):String(xs[i])}</text>`;
|
|
|
- let yt=''; for(let k=0;k<5;k++){const v=lo+(hi-lo)*k/4; yt+=`<text x="${ML-6}" y="${Y(v)+3}" font-size="10" fill="var(--muted)" text-anchor="end">${v.toFixed(Math.abs(hi)<10?1:0)}</text>`+
|
|
|
- `<line x1="${ML}" x2="${W-12}" y1="${Y(v)}" y2="${Y(v)}" stroke="var(--line-2)"/>`}
|
|
|
- if(f.band&&f.band.q1){const q1=f.band.q1,q3=f.band.q3;
|
|
|
- let up='',dn='';
|
|
|
- for(let i=0;i<n;i++){if(q3[i]!=null)up+=`${X(i)},${Y(q3[i])} `}
|
|
|
- for(let i=n-1;i>=0;i--){if(q1[i]!=null)dn+=`${X(i)},${Y(q1[i])} `}
|
|
|
- body=`<polygon points="${up}${dn}" fill="var(--unknown)" fill-opacity="0.16"/>`+body}
|
|
|
- if(f.xlab) body+=`<text x="${(ML+W)/2}" y="${H-1}" font-size="10" fill="var(--unknown)" text-anchor="middle">${f.xlab}</text>`;
|
|
|
- let leg = f.kind==='band' ? `<text x="${W-190}" y="12" font-size="10" fill="var(--bad)">— 本台</text><text x="${W-130}" y="12" font-size="10" fill="var(--muted)">— 全场中位</text><text x="${W-60}" y="12" font-size="10" fill="var(--unknown)">▧ 四分位</text>` :
|
|
|
- ((f.series&&f.series.length<=2)?(()=>{const n2=f.series.length,gap=Math.min(118,Math.floor((W-ML-20)/n2));
|
|
|
- return f.series.map((s,k)=>`<text x="${W-12-(n2-1-k)*gap}" y="12" font-size="10" fill="${cols[k%4]}" text-anchor="end">— ${s.name}</text>`).join('')})():'');
|
|
|
- const legendHtml=(f.series&&f.series.length>2)?`<div class="legend">`+f.series.map((s,k)=>
|
|
|
- `<span class="lgi"><i style="background:${cols[k%4]}"></i>${s.name}</span>`).join('')+`</div>`:'';
|
|
|
- // 统一走 figWrap: 图身按类别封顶宽度 (曲线760/月度620), 不再随容器拉伸成巨图
|
|
|
- return figWrap(f, `<svg viewBox="0 0 ${W} ${H}">${yt}${body}${xt}${leg}</svg>`+legendHtml,
|
|
|
- f.note||'').replace('<h3>'+(f.title||''), '<h3>'+(f.title||'')+(f.unit?' ('+f.unit+')':''));
|
|
|
-}
|
|
|
-
|
|
|
-function ordersHtml(o){
|
|
|
- if(!o) return '';
|
|
|
- let h=`<div class="card mb"><h3>工单处理史 · 该系统 (处理 ${o.n} 次${o.窗?'; 窗 '+o.窗:''})</h3>`;
|
|
|
- if(o.n===0){h+=`<p class="small">该时间窗内无工单记录。${o.note||''}</p></div>`; return h}
|
|
|
- const acts=Object.entries(o.动作统计||{}).map(([k,v])=>k+'×'+v).join(' / ');
|
|
|
- if(acts) h+=`<p class="small" style="margin-bottom:6px">处置动作统计: <b>${acts}</b></p>`;
|
|
|
- h+=`<table><tr><th style="width:92px">日期</th><th>故障/工作</th><th>排查发现</th><th>处置</th><th>类别</th></tr>`;
|
|
|
- o.rows.forEach(r=>{h+=`<tr><td class="num" style="text-align:left">${r.日期}</td><td class="small">${r.故障}</td><td class="small">${r.排查&&r.排查!=='nan'?r.排查:'—'}</td><td class="small">${r.处置||'—'}</td><td class="small">${r.类别||''}</td></tr>`});
|
|
|
- h+=`</table><p class="small">${o.note||''}</p></div>`;
|
|
|
- return h;
|
|
|
-}
|
|
|
-let ONT=null;
|
|
|
-async function loadDbase(){
|
|
|
- const el=document.getElementById('dbase'); if(!el) return;
|
|
|
- if(!ONT) ONT=await (await fetch('/api/ontology')).json();
|
|
|
- const C=ONT.chain29; let h='';
|
|
|
- // ① 机制链横图 (决策的证据底座, 图形化)
|
|
|
- const node=(label,val,color)=>`<div style="min-width:120px;flex:1;text-align:center">
|
|
|
- <div style="border:1.5px solid ${color};border-radius:10px;padding:8px;background:var(--panel)">
|
|
|
- <div class="small" style="color:var(--muted);letter-spacing:.05em">${label}</div>
|
|
|
- <div style="font-weight:600;font-size:13.5px;margin-top:4px">${val}</div></div></div>`;
|
|
|
- const arrow=`<div style="align-self:center;color:var(--muted);font-size:16px;padding:0 4px">→</div>`;
|
|
|
- // 每格加一行"这一格回答什么" (2026-08-28 用户问"什么意思" — 六个词现场看不出是干嘛的)
|
|
|
- const node2=(t,v,c,q)=>`<div style="flex:none;min-width:132px;border:1px solid ${c};border-radius:10px;padding:9px 12px;text-align:center">
|
|
|
- <div class="small" style="color:var(--muted)">${t}</div>
|
|
|
- <div style="font-weight:600;margin:4px 0 3px">${v}</div>
|
|
|
- <div class="small" style="color:var(--muted);line-height:1.45">${q}</div></div>`;
|
|
|
- // 决策链进度盘 (2026-08-28 用户问"这个到底能怎么用, 我没看太懂")。
|
|
|
- // 原来是 29# 一台的静态六格 — 打开它做不了任何决定, 是展示牌不是工作面。
|
|
|
- // 改为: 全部需跟踪台各走到第几步 / 卡在哪一步 / 下一步具体做什么 / 点台号进该台。
|
|
|
- // 这样它回答的是"今天该管谁、卡在哪、谁去做什么", 而不是"29# 的故事是怎样的"。
|
|
|
- const BD=(window.D&&D.fus&&D.fus.链盘)?D.fus.链盘:(D.fus&&D.fus.链盘);
|
|
|
- if(BD&&BD.rows&&BD.rows.length){
|
|
|
- const SK=Object.entries(BD.stuck||{}).sort((a,b)=>b[1]-a[1]);
|
|
|
- const tot=BD.rows.length;
|
|
|
- const cell=st=>st==='done'?`<i style="display:inline-block;width:13px;height:13px;border-radius:3px;background:var(--accent);opacity:.8"></i>`
|
|
|
- :st==='unknown'?`<i style="display:inline-block;width:13px;height:13px;border-radius:3px;border:1.5px dashed var(--muted);opacity:.75"></i>`
|
|
|
- :`<i style="display:inline-block;width:13px;height:13px;border-radius:3px;border:1.5px solid var(--line)"></i>`;
|
|
|
- h+=`<div class="card mb"><h3>决策链进度盘 · ${tot} 台需跟踪</h3>
|
|
|
- <p class="small" style="color:var(--muted);margin-bottom:10px">
|
|
|
- 一台机组从发现异常到修完验收要走六步。这张盘不讲某一台的故事, 用于确认<b>当前需处置机组、各自所处环节、后续动作</b>。
|
|
|
- <span style="color:var(--accent)">■</span> 已达 ·
|
|
|
- <span style="color:var(--muted)">▢ 虚线</span> 数据不足不可判 ·
|
|
|
- <span style="color:var(--muted)">▢ 实线</span> 未达</p>
|
|
|
- <div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:12px;
|
|
|
- padding:9px 12px;border-radius:8px;background:var(--bg);border:1px solid var(--line)">
|
|
|
- <b class="small">瓶颈</b>${SK.map(([k,n])=>`<span class="small">
|
|
|
- <b style="color:${n/tot>0.5?'var(--bad)':'var(--warn)'}">${n} 台</b> 卡在「${k}」</span>`).join('<span class="small" style="color:var(--muted)">·</span>')}
|
|
|
- ${SK.length&&SK[0][1]/tot>0.5?`<span class="small" style="color:var(--muted)">— 瓶颈集中在一步, 不是"再多看点数据"能解的</span>`:''}
|
|
|
- <div style="flex-basis:100%;height:0"></div>
|
|
|
- ${SK.map(([k,n])=>{const r=BD.rows.find(x=>x.stuck===k);
|
|
|
- /* 值为空就整块不出 —— 原来无条件渲染, 没 next 的两个环节在页面上留下
|
|
|
- "「机制」24 台的下一步:" 后面空着的悬空标签 (中英文版同样露)。 */
|
|
|
- return (r&&r.next)?`<div class="small" style="color:var(--muted);margin-top:3px">
|
|
|
- <b>「${k}」${n} 台的下一步:</b> ${r.next}</div>`:''}).join('')}</div>
|
|
|
- <table><tr><th style="width:52px">机组</th><th style="width:64px">状态</th>
|
|
|
- <th style="width:126px">六步进度</th><th style="width:70px">卡在</th><th>本台情况</th></tr>`;
|
|
|
- BD.rows.forEach(r=>{
|
|
|
- const ris=r.rising;
|
|
|
- h+=`<tr><td><a class="tl" href="javascript:void(0)" onclick="popChain('${r.t}')" title="点开看这一台的六步决策链"><b>${r.t.replace('WTG','')}#</b></a>
|
|
|
- ${ris?`<span class="small" style="color:var(--bad)" title="${ris.component} 末三窗/早三窗 ${ris.ratio}×">↑${ris.ratio}×</span>`:''}</td>
|
|
|
- <td>${chip(r.ostate||'—')}</td>
|
|
|
- <td style="white-space:nowrap">${r.steps.map((x,i)=>`<span title="${i+1} ${x.k}: ${x.v} — ${x.d}" style="margin-right:3px">${cell(x.st)}</span>`).join('')}
|
|
|
- <span class="cnt">${r.done}/6</span></td>
|
|
|
- <td class="small" style="color:${r.stuck_kind==='unknown'?'var(--muted)':'var(--warn)'}">${r.stuck||'全通'}${r.stuck_kind==='unknown'?' <span title="非未执行, 系统无法判定">?</span>':''}</td>
|
|
|
- <td class="small" style="color:var(--muted)">${r.part&&r.part.indexOf('未收录')<0?`<b style="color:var(--ink)">${r.part}</b> · `:''}证据 ${(r.srcs||[]).join('/')||'—'}
|
|
|
- ${ris?` · <span style="color:var(--bad)">${ris.component}末三窗/早三窗 ${ris.ratio}×${ris.工单可对===false?', 待现场/工单确认':''}</span>`:''}
|
|
|
- ${r.hist_loop?` · <span style="color:var(--ok)">${r.hist_loop.replace_date} 有过闭环 ${r.hist_loop.ratio}×</span>`:''}</td></tr>`;});
|
|
|
- h+=`</table>
|
|
|
- <p class="small" style="color:var(--muted);margin-top:8px">
|
|
|
- <b>「动作」一列全场都是不可判</b>, 不是偷懒: ${BD.wo_bound}, 而判级时间窗在 2026 —
|
|
|
- 台账里的历史工单不可能是针对本次问题的, 用"有没有工单"判已派工会造出假的完成态。要填这一列须向现场要 25~26 工单。</p>`;
|
|
|
- if(BD.closed&&BD.closed.length){
|
|
|
- h+=`<div style="margin-top:12px;padding:10px 12px;border-radius:8px;background:var(--bg);border:1px solid var(--line)">
|
|
|
- <div class="small" style="font-weight:600;margin-bottom:5px">已走完全程并退出盘面 · ${BD.closed.length} 台 (端到端闭环实证)</div>
|
|
|
- ${BD.closed.map(x=>`<div class="small" style="color:var(--muted)">
|
|
|
- <a class="tl" href="/turbine/${x.turbine}"><b>${x.turbine.replace('WTG','')}#</b></a>
|
|
|
- ${x.component}${x.title?' · '+x.title:''} ${x.replace_date} 换件 →
|
|
|
- 振动 ${x.pre} 降至 ${x.post} = <b style="color:var(--ok)">${x.ratio}×</b></div>`).join('')}
|
|
|
- <div class="small" style="color:var(--muted);margin-top:5px">
|
|
|
- 这三台正是算法当初的检出台 ⇒ <b>故障前能看见、换件后能看见恢复</b>, 这条链是通的; 闭环成功的台会从盘面消失。</div></div>`;
|
|
|
- }
|
|
|
- h+=`</div>`;
|
|
|
- }
|
|
|
- // ② 四情景损失对比 (log 尺度条形; 数值解析电费口径区间取中值)
|
|
|
- const sc=C.scenario.scenarios||[];
|
|
|
- const parseWan=t=>{const m=/([0-9.]+)~([0-9.]+) 万元/.exec(t||'');return m?(+m[1]+ +m[2])/2:null};
|
|
|
- const rows=sc.map(x=>{
|
|
|
- let v=parseWan(x.停机损失&&x.停机损失.电费口径), extra=0, lab=x.id;
|
|
|
- if(x.停机损失&&x.停机损失.大部件成本){extra=350; lab+=' (+大部件成本200-500万)'; // 用 - 不用 ~ : ~ 不在 PH 续接字符类里, 会把整串切断
|
|
|
- }
|
|
|
- return {lab, v:(v||0)+extra, base:v||0, extra};});
|
|
|
- if(rows.length){
|
|
|
- const mx=Math.max(...rows.map(r=>r.v));
|
|
|
- h+=`<div class="card mb"><h3>排期沙盘 · 四情景损失对比 (万元, 电价区间中值; 建议级待人裁)</h3>`;
|
|
|
- rows.forEach(r=>{
|
|
|
- const w=Math.max(3,Math.round(Math.log10(1+r.v)/Math.log10(1+mx)*320));
|
|
|
- const col=r.extra?'var(--bad)':(r.lab.startsWith('A')?'var(--ok)':'var(--warn)');
|
|
|
- h+=`<div style="display:flex;align-items:center;gap:10px;margin:8px 0">
|
|
|
- <div class="small" style="width:170px;text-align:right">${r.lab}</div>
|
|
|
- <div style="height:16px;width:${w}px;background:${col};border-radius:4px;opacity:.85"></div>
|
|
|
- <div class="small" style="font-variant-numeric:tabular-nums"><b>${r.v>=100?r.v.toFixed(0):r.v.toFixed(1)}</b> 万元</div></div>`});
|
|
|
- h+=`<p class="small">条长=log 尺度 (跨两个数量级); D 含大部件成本中值 350 万 (grade D 弱源)。早修成本被限电稀释, 晚修成本被窗口与船期放大。</p></div>`;
|
|
|
- }
|
|
|
- // ③ 天气窗热力条 (近12月, CTV/吊装 双行)
|
|
|
- const W=ONT.weather||{}; const ms=Object.keys(W).sort().slice(-12);
|
|
|
- if(ms.length){
|
|
|
- const cell=(v,d)=>{const r=v/d; const bg=r>0.9?'var(--ok)':r>0.7?'var(--warn)':'var(--bad)';
|
|
|
- return `<td style="text-align:center;padding:4px"><div style="background:${bg};opacity:${0.25+r*0.6};border-radius:4px;padding:4px 0;font-size:11.5px;font-variant-numeric:tabular-nums">${v}</div></td>`};
|
|
|
- h+=`<div class="card mb"><h3>海上作业天气窗 · 近12月可作业日 (公开再分析统计, 非派工依据)</h3>
|
|
|
- <div style="overflow-x:auto"><table><tr><th style="width:90px"></th>${ms.map(m=>`<th class="small" style="text-align:center">${m.slice(2)}</th>`).join('')}</tr>`;
|
|
|
- h+=`<tr><td class="small">CTV人员转运</td>${ms.map(m=>cell(W[m]['CTV人员转运']||0,W[m]['天数']||30)).join('')}</tr>`;
|
|
|
- h+=`<tr><td class="small">吊装作业</td>${ms.map(m=>cell(W[m]['吊装作业']||0,W[m]['天数']||30)).join('')}</tr>`;
|
|
|
- h+=`</table></div><p class="small">CTV 级小修全年窗宽; 吊装级受台风季约束, 色深表示可作业占比。限值取行业常规 (CTV Hs≤1.5m∧风速≤12m/s, 吊装 1.2/8), 非本场合同值。</p></div>`;
|
|
|
- }
|
|
|
- el.innerHTML=h;
|
|
|
-}
|
|
|
-async function renderOntology(){
|
|
|
- const root=document.getElementById('root');
|
|
|
- if(!ONT){
|
|
|
- root.innerHTML='<div class="card"><div class="skel" style="width:40%"></div><div class="skel" style="width:70%;margin-top:9px"></div></div>';
|
|
|
- ONT=await (await fetch('/api/ontology')).json();
|
|
|
- }
|
|
|
- const O=ONT; let h='';
|
|
|
- h+=`<div class="eyebrow">语义本体 · 对象/机制/决策/约束 (Palantir 思路轻量自建; 判读纪律=写入闸, 违反写不进库)</div>`;
|
|
|
- h+=`<div class="card mb" style="border-left:3px solid var(--accent)"><b>六条写入约束 (库级保证)</b><p class="small">${O.constraints}</p></div>`;
|
|
|
- h+=`<div class="kpis">`+Object.entries(O.stats).map(([k,v])=>`<div class="kpi"><b>${v}</b><span>${k}</span></div>`).join('')+`</div>`;
|
|
|
-
|
|
|
- const C=O.chain29;
|
|
|
- h+=`<div class="card mb"><h3>机制链视图 · 29# 范例 (证据 → 机制 → 判级 → 沙盘 → 动作 → 验收)</h3>
|
|
|
- <div style="display:flex;gap:10px;overflow-x:auto;padding:6px 0">`;
|
|
|
- h+=`<div style="min-width:240px;flex:1"><p class="small"><b>① 证据 ×${C.evidence.length} (${C.mechanism.independent_source_count||4} 观测面)</b></p>`;
|
|
|
- C.evidence.forEach(e=>{h+=`<div class="small" style="border:1px solid var(--line);border-left:3px solid var(--info);border-radius:6px;padding:6px 8px;margin-bottom:6px"><b>${e.facet}</b> · 窗 ${e.claim_window}<br>${(e.summary||'').slice(0,90)}…<br><span style="color:var(--muted)">${e.id}</span></div>`});
|
|
|
- h+=`</div>`;
|
|
|
- const box=(t,color,inner)=>`<div style="min-width:210px;flex:1"><p class="small"><b>${t}</b></p><div class="small" style="border:1px solid var(--line);border-left:3px solid ${color};border-radius:6px;padding:8px">${inner}</div></div>`;
|
|
|
- h+=box('② 机制','var(--warn)',`<b>${C.mechanism.name}</b><br>源数=观测面去重 ${C.mechanism.independent_source_count} (证据${C.evidence.length}条)<br><span style="color:var(--muted)">${C.mechanism.id}</span>`);
|
|
|
- h+=box('③ 判级','var(--bad)',`<b>${C.verdict.level}</b> · 审级 ${C.verdict.review_tier}<br>${(C.verdict.title||'').slice(0,110)}…<br><span style="color:var(--muted)">${C.verdict.id}</span>`);
|
|
|
- h+=box('④ 沙盘','var(--accent)',`${(C.scenario.recommendation||'').slice(0,130)}…<br><span style="color:var(--muted)">${C.scenario.id}</span>`);
|
|
|
- h+=box('⑤ 动作','var(--warn)',`${(C.action.name||'').slice(0,90)}<br>闸: ${C.action.gate?C.action.gate.reversibility+' · '+C.action.gate.review_tier:''} · ${C.action.status||''}<br><span style="color:var(--muted)">${C.action.id}</span>`);
|
|
|
- h+=box('⑥ 验收状态机','var(--ok)',`状态 <b>${C.acceptance.state||'—'}</b> 分支 ${C.acceptance.branch||'—'}<br>${(C.acceptance.note||'').slice(0,110)}<br><span style="color:var(--muted)">acceptance/WTG29/state</span>`);
|
|
|
- h+=`</div><p class="small">判读纪律: 六个观测量=一条机制链 (C3 源数按观测面), 禁写多源印证; 判级只在 L1/审级产生, 本体转录带审级。</p></div>`;
|
|
|
-
|
|
|
- h+=`<div class="card mb"><h3>失效机制库 (${O.mechanisms.length} 族)</h3><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:10px">`;
|
|
|
- O.mechanisms.forEach(m=>{h+=`<div class="small" style="border:1px solid var(--line);border-radius:8px;padding:9px"><b>${m.name}</b><br>状态: ${m.status||''}<br>观测面: ${(m.facets||[]).join('/')}<br>涉及: ${(m.affects||[]).map(a=>a.split('/').pop()).join(', ')||'fleet'}<br><span style="color:var(--muted)">${m.id}</span></div>`});
|
|
|
- h+=`</div></div>`;
|
|
|
-
|
|
|
- h+=`<div class="card mb"><h3>决策台 (决策捕获 + 月度滚动)</h3><table><tr><th>决策</th><th>裁决人</th><th>时间</th><th>状态</th></tr>`;
|
|
|
- O.decisions.forEach(x=>{const on=x.status==='pending_user';
|
|
|
- h+=`<tr><td class="small">${(x.decision||'').slice(0,130)}</td><td>${x.decided_by}</td><td class="small">${x.when}</td>
|
|
|
- <td><span class="badge" style="border-color:${on?'var(--warn)':'var(--ok)'};color:${on?'var(--warn)':'var(--ok)'}">${x.status}</span></td></tr>`});
|
|
|
- h+=`</table><p class="small">月度滚动: 数据月 ${O.roll.data_month||'—'} · 上次动作 ${(O.roll.last_roll_actions||[]).join('; ')||'—'} · 变化 ${(O.roll.last_changes||[]).join('; ')||'—'}</p></div>`;
|
|
|
-
|
|
|
- h+=`<div class="card mb"><h3>对象浏览器 (${Object.values(O.stats).reduce((a,b)=>a+b,0)} 对象)</h3>
|
|
|
- <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px">
|
|
|
- <select id="obType" style="padding:8px;border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--ink)"><option value="">全部类型</option>${Object.keys(O.stats).map(k=>`<option>${k}</option>`).join('')}</select>
|
|
|
- <input id="obT" placeholder="台号 如 29" style="padding:8px 12px;border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--ink);width:110px">
|
|
|
- <button onclick="obGo()" style="cursor:pointer;padding:8px 16px;border-radius:8px;border:1px solid var(--accent);background:var(--accent);color:#fff">查询</button>
|
|
|
- </div><div id="obOut" class="small">选类型/台号后查询; 点结果行看对象全文与链接。</div></div>`;
|
|
|
- root.innerHTML=h;
|
|
|
- document.getElementById('foot').innerHTML='本体=只读语义层; 判级权在 L1+审级, 模型与网页均零判级权';
|
|
|
-}
|
|
|
-async function obGo(){
|
|
|
- const t=document.getElementById('obType').value, tb=document.getElementById('obT').value;
|
|
|
- const d=await (await fetch(`/api/ont_list?type=${encodeURIComponent(t)}&t=${encodeURIComponent(tb)}&limit=60`)).json();
|
|
|
- const el=document.getElementById('obOut');
|
|
|
- if(!d.rows||!d.rows.length){el.textContent='无结果';return}
|
|
|
- el.innerHTML=`<table><tr><th>id</th><th>类型</th><th>要点</th></tr>`+d.rows.map(r=>
|
|
|
- `<tr style="cursor:pointer" onclick="obOpen('${r.id}')"><td class="small">${r.id}</td><td>${r.type}</td><td class="small">${r.name||r.level||r.status||''} ${r.review_tier?'· '+r.review_tier:''}</td></tr>`).join('')+`</table><div id="obDetail"></div>`;
|
|
|
-}
|
|
|
-async function obOpen(id){
|
|
|
- const d=await (await fetch('/api/ont_obj?id='+encodeURIComponent(id))).json();
|
|
|
- const el=document.getElementById('obDetail'); if(!el) return;
|
|
|
- el.innerHTML=`<pre style="white-space:pre-wrap;background:var(--ground);border:1px solid var(--line);border-radius:8px;padding:10px;margin-top:8px">${JSON.stringify(d,null,1).replace(/</g,'<')}</pre>`;
|
|
|
-}
|
|
|
-// 模型档位 (2026-08-28 用户令: 模型切换的应用你设计好; 去 30B 用 27B)。
|
|
|
-// 按"这个问题值不值得等"选, 而非凭型号大小猜 — 每档写清 在哪跑 / 要多久 / 适合什么。
|
|
|
-// 云端档单独标"内容会外发", 因为那是一个需要用户知情的选择而不是性能选项。
|
|
|
-// ★页面用的是**这一份** (JS), 上面 L1158 那份是 Python 的 —— 两份同名不同源。
|
|
|
-// 我第一次只改了 Python 那份, 页面一点没变 (deepseek-chat 在成品件里 0 命中)。
|
|
|
-// 同名双实现: 改一处不报错、不生效, 只能靠回查产物发现。
|
|
|
-const MODEL_SPEC = {
|
|
|
- 'qwen3:8b': {nm:'轻量 8B', loc:'本地', t:'约 30 到 60 秒', use:'默认档: 快速简答问答', warn:''},
|
|
|
- 'qwen3.8:27b': {nm:'标准 27B', loc:'本地', t:'约 2 到 4 分钟', use:'复杂问题档: 多台横向比对与机制推断', warn:''},
|
|
|
- 'qwen3:30b-a3b': {nm:'快 30B (a3b)', loc:'本地', t:'约 20 到 60 秒', use:'8B 成文校验未过时自动升档目标', warn:''},
|
|
|
- 'deepseek-r1:14b': {nm:'本机 DeepSeek 14B', loc:'本地', t:'约 1 到 3 分钟', use:'交叉审核审核档 (作答请优先 27B)', warn:''},
|
|
|
- 'deepseek-r1:32b': {nm:'本机 DeepSeek 32B', loc:'本地', t:'约 3 到 8 分钟', use:'备用: 更强更慢, 引用格式不稳', warn:''},
|
|
|
- 'deepseek-chat': {nm:'DeepSeek', loc:'云端', t:'约 3 到 10 秒', use:'默认档, 状态查询与机制推断', warn:'内容会外发到模型厂商, 场名与台号已去标识化'},
|
|
|
- 'kimi-fast': {nm:'Kimi', loc:'云端', t:'约 5 到 20 秒', use:'长文归纳', warn:'内容会外发到模型厂商, 场名与台号已去标识化'},
|
|
|
- 'qwen-plus': {nm:'通义千问', loc:'云端', t:'约 5 到 20 秒', use:'备用云端档', warn:'内容会外发到模型厂商, 场名与台号已去标识化'},
|
|
|
- 'deepseek-v4-pro': {nm:'云端 DS', loc:'云端', t:'约 2 到 6 分钟', use:'复杂推理与交叉审核', warn:'内容会外发到模型厂商, 场名与台号已去标识化'},
|
|
|
-};
|
|
|
-function mdlNote(){
|
|
|
- const el=document.getElementById('mdlnote'), sel=document.getElementById('askm');
|
|
|
- if(!el||!sel) return;
|
|
|
- const v=MODEL_SPEC[sel.value]||{};
|
|
|
- // ★查不到档位时**不能**默认"零外发" —— 未知档只可能是新加的云端模型。宁可说重不说轻。
|
|
|
- const note = v.warn ? `<span style="color:var(--warn)">${v.warn}</span>`
|
|
|
- : (v.loc==='本地' ? '全程零外发' : '<span style="color:var(--warn)">未知档位, 按内容会外发处理</span>');
|
|
|
- el.innerHTML=`${v.use||''} · ${note}`;
|
|
|
-}
|
|
|
-async function askGo(){
|
|
|
- const q=document.getElementById('askq').value, m=document.getElementById('askm').value;
|
|
|
- if(!q.trim()){document.getElementById('askout').textContent='问题为空';return}
|
|
|
- const r=await fetch('/api/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({q,model:m,cat:window.ASKCAT||'',xrev:(document.getElementById('askxrev')||{}).checked||false})});
|
|
|
- const d=await r.json();
|
|
|
- if(d.err){document.getElementById('askout').textContent=d.err;return}
|
|
|
- askPoll();
|
|
|
-}
|
|
|
-// ★这句提示原文是「本地直连约 45~250 秒 (复合问题更久; 全程零外发)」—— 那是本地模型
|
|
|
-// 时期的文案, 两处都已失真 (2026-09-01 用户说"太慢", 实测才发现):
|
|
|
-// ① 时间: 云端档实测 3~7 秒, 而提示写 45~250 秒 —— **是提示语在告诉用户它很慢**
|
|
|
-// ② 「全程零外发」现在是**假的**: 云端档的问题会发到厂商 API。
|
|
|
-// 向用户保证一件不成立的事, 比写慢了严重得多。
|
|
|
-// 故改为按档给话, 本地档才说零外发。
|
|
|
-function MDLNOTE(m){
|
|
|
- // ★不用正则: CHART_JS 是**非 raw** 的 Python 三引号串, 反斜杠转义序列会被 Python
|
|
|
- // 吃掉并报 SyntaxWarning/SyntaxError。本地档判别用朴素前缀/子串即可。
|
|
|
- // (本会话第 4 次踩这个坑 —— 而这一次是在**写这条警告的注释里**踩的。)
|
|
|
- const s0=String(m||'').toLowerCase();
|
|
|
- const local = s0.indexOf(':')>=0 || ['qwen3','llama','gemma','deepseek-r1'].some(x=>s0.indexOf(x)===0);
|
|
|
- return local ? '本地直连, 约 45 到 250 秒 (复合问题更久), 全程零外发'
|
|
|
- : '云端模型, 通常 3 到 10 秒 (复合问题更久), 问题与检索到的证据会发往模型厂商';
|
|
|
-}
|
|
|
-async function askPoll(){
|
|
|
- const el=document.getElementById('askout'); if(!el) return;
|
|
|
- const r=await fetch('/api/ask_status'); const s=await r.json();
|
|
|
- if((s.state==='running'||s.state==='partial')){
|
|
|
- // 进度上屏 (2026-08-28 用户"刚发现给不出结果"): 原来只有一个秒数在跳, 本地模型一问 45~250s,
|
|
|
- // 看不见它在干什么就会被读成卡死。现在报当前阶段(预取/推理第n轮/取证工具名/成文/校闸)与轨迹。
|
|
|
- const st=(s.steps||[]);
|
|
|
- el.innerHTML=`<b>推理中…</b> ${s.elapsed}s · <span style="color:var(--accent)">${s.step||'启动'}</span>
|
|
|
- <div class="small" style="color:var(--muted);margin-top:5px">${s.model} · ${MDLNOTE(s.model)}</div>
|
|
|
- ${st.length?`<div class="small" style="color:var(--muted);margin-top:4px">${st.slice(-6).join(' → ')}</div>`:''}
|
|
|
- <div class="small" style="color:var(--muted);margin-top:4px">问题: ${(s.q||'').slice(0,90)}</div>`;
|
|
|
- setTimeout(askPoll, 2000);
|
|
|
- }else if(s.state==='done'){
|
|
|
- const v=s.verify||{};
|
|
|
- const badge=v.全通过?`<span class="badge" style="border-color:var(--ok);color:var(--ok)">溯源核验 ${v.引用id数}/${v.引用id数} 全真</span>`
|
|
|
- :`<span class="badge" style="border-color:var(--warn);color:var(--warn)">溯源核验未全过 (引用${v.引用id数||0})</span>`;
|
|
|
- const R=s.review;
|
|
|
- const rv=R?`<div class="card mb" style="border-left:3px solid ${R.issues&&R.issues.length?'var(--warn)':'var(--ok)'};margin-top:10px">
|
|
|
- <h3>交叉审核 · ${R.model} <span class="small" style="font-weight:400;color:${R.issues&&R.issues.length?'var(--warn)':'var(--ok)'}">${R.verdict}</span></h3>
|
|
|
- <p class="small" style="color:var(--muted);margin-bottom:${R.issues&&R.issues.length?'8px':'0'}">
|
|
|
- 由另一本地模型基于<b>同一份证据</b>复核上述答案。溯源核验仅校验数字与机组号是否越界, 本项校验结论是否有证据支撑。</p>
|
|
|
- ${(R.issues||[]).map(i=>`<div class="small" style="padding:5px 0;border-top:1px solid var(--line-2)">
|
|
|
- <span class="badge" style="border-color:var(--warn);color:var(--warn)">${i.type}</span>
|
|
|
- <b>「${i.quote}」</b> <span style="color:var(--muted)">${i.why}</span></div>`).join('')}
|
|
|
- ${R.err?`<p class="small" style="color:var(--bad)">审核未完成: ${R.err}</p>`:''}</div>`:'';
|
|
|
- el.innerHTML=`<p>${badge} · 模型 ${s.model} · 用时 ${s.elapsed}s · ${(s.steps||[]).length} 步</p><pre style="white-space:pre-wrap;background:var(--ground);padding:12px;border-radius:8px;border:1px solid var(--line)">${(s.answer||'').replace(/</g,'<')}</pre>${rv}`;
|
|
|
- }else if(s.state==='error'){
|
|
|
- el.textContent='出错: '+s.err;
|
|
|
- }
|
|
|
-}
|
|
|
-function winSel(cur){
|
|
|
- const base=['近30日','近90日','2026年','2026H1','2025H2','全程'];
|
|
|
- const ms=(window.ALLMONTHS&&ALLMONTHS.length)?ALLMONTHS:[]; // 按月份过滤和选择 (用户令): 预设窗+逐月
|
|
|
- let h=`<select id="win" onchange="reload(this.value)"><optgroup label="预设窗">`+
|
|
|
- base.map(w=>`<option ${w===cur?'selected':''}>${w}</option>`).join('')+`</optgroup>`;
|
|
|
- if(ms.length) h+=`<optgroup label="按月">`+ms.slice().reverse().map(m=>`<option ${m===cur?'selected':''}>${m}</option>`).join('')+`</optgroup>`;
|
|
|
- else if(!base.includes(cur)) h+=`<option selected>${cur}</option>`; // 台页/问题页无月轴时保显当前月窗
|
|
|
- return h+`</select>`}
|