/** * 振动融合(vibration)子视图的**拼装**(P11 迁移)。 * * ★ 口径:旧 `app.js` 的这些视图本来就是拼 HTML 字符串;这里**逐字转写**并用 `v-html` 渲染, * 于是 DOM 与文本节点边界与重构前一致 —— 严格对拍要的正是这个(不是"看起来差不多")。 * 本文件当前只含 `matrix` 子视图(默认),其余子视图(loop/units/events/cms)待迁。 */ import { t } from '../i18n/zh' import { unitNo, winName } from './render' import { partName, sysName } from './render2' import { VIBJARGON } from './vibjargon' export const esc = (s: unknown): string => String(s == null ? '' : s) .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') /** 旧 `displayText(s)`(app.js:36-43):英文页面走译表,中文口径**原样返回** */ export const displayText = (s: unknown): string => String(s == null ? '' : s) /** 旧 `dv()`(中文口径 = esc) */ export const dv = (s: unknown): string => esc(s) const ST: Record = { 危险: 'bad', 报警: 'warn', 不可判: 'unk', 良好: 'ok', 优秀: 'ex' } const ST_ALIAS: Record = { critical: 'bad', alarm: 'warn', undeterminable: 'unk', indeterminate: 'unk', 'not evaluable': 'unk', good: 'ok', excellent: 'ex', warn: 'warn', bad: 'bad', ok: 'ok', unk: 'unk', ex: 'ex', } const stateKey = (v: unknown): string | undefined => ST[String(v)] || ST_ALIAS[String(v)] export const STC: Record = { bad: 'var(--critical)', warn: 'var(--warning)', unk: 'var(--unknown)', ok: 'var(--good)', ex: 'var(--excellent)' } const CST: Record = { 优秀: 'var(--excellent)', 良好: 'var(--good)', 报警: 'var(--warning)', 危险: 'var(--critical)', 不可判: 'var(--unknown)', 监测不可用: 'var(--unknown)', 无监测: 'var(--unknown)' } const CLV: Record = { bad: 'var(--critical)', warn: 'var(--warning)', note: 'var(--muted)', ok: 'var(--good)', unlisted: 'var(--muted)', stale: 'var(--muted)', meta: 'var(--muted)' } const TIER: Record = { 定论: 'confirmed', '准定论·预警': 'near', '候选·新发': 'candidate_new', 候选: 'candidate', '候选·记基线': 'candidate_baseline', 参考: 'reference', INSUFFICIENT: 'insufficient', 正常: 'normal', 撤回: 'withdrawn', } export const stKey = (k: string): string => 'st.' + (k === 'ex' ? 'excellent' : k) export const stWord = (zh: unknown): string => { const k = stateKey(zh) return k ? t(stKey(k)) : dv(zh) } /** 旧 `tierWord`(中文分支=语言包有则取,否则 dv) */ export const tierWord = (zh: unknown): string => { const k = TIER[String(zh)] const key = 'tier.' + k return k ? t(key) : (String(zh) === '—' ? '—' : dv(zh)) } export const duo = (st: string, ev: string, col: string): string => `${st}${ev}` /** 旧 `vibPlain`:中文口径下逐条替换技术速记 */ export const vibPlain = (x: unknown): string => { let s = String(x == null ? '' : x) for (const [a, b] of VIBJARGON) s = s.replace(a, b) return s } export const attrPlain = (x: unknown): string => vibPlain(x) /** 旧 `spark`(历史曲线小图,逐字转写) */ const spark = (hist: number[] | undefined, color: string): string => { if (!hist || hist.filter((x) => x != null).length < 3) return '' const vs = hist.filter((x) => x != null) const mn = Math.min(...vs), mx = Math.max(...vs), sp = (mx - mn) || 1 const W = 84, H = 22, n = hist.length let d = '', last: number[] | null = null hist.forEach((v, i) => { if (v == null) return const x = i / (n - 1) * W, y = H - 2 - (v - mn) / sp * (H - 4) d += (d ? 'L' : 'M') + x.toFixed(1) + ' ' + y.toFixed(1) last = [x, y] }) return `` } const FAMCO: Record = { 齿轮箱: '#8A6D3B', 发电机: '#4A6FA5', 主轴承: '#2E7D6B', 变桨叶片: '#4A6FA5', 偏航: '#2E7D6B', 测风: '#7C5295', 其他: '#5A6470' } const famBar = (fam: any[] | undefined, n: number, maxN: number): string => { if (!n) return `${t('fus.no_alarm')}` const W = 118, w = Math.max(6, Math.round(n / Math.max(maxN || n, 1) * W)) return `
${(fam || []).map((f) => ``).join('')}
${(fam || []).slice(0, 2).map((f) => sysName(f.k) + ' ' + f.n).join(' · ')}
` } /** 旧 `fusMatrix(F)` —— 逐字转写(默认子视图) */ export function matrixHtml(F: any): string { const months = ((F.列窗 || {}).months) || [] let h = `
${t('fus.rules_head')}

${t('fus.rule1')}
${t('fus.rule2')}

` h += `

${t('fus.matrix_head')}

${t('fus.matrix_note1')}

${t('fus.matrix_note2', { n: months.length, m0: months[0] || '?' })}

${t('fus.legend_state')} ${['bad', 'warn', 'ok', 'ex', 'unk'].map((k) => `${t(stKey(k))}`).join('')}
${t('fus.legend_tier')} ${['定论', '准定论·预警', '候选·新发', '候选', '候选·记基线', '参考', 'INSUFFICIENT'].map((k) => `${tierWord(k)}`).join('')}
${t('fus.legend_note')}
` const shortWin = (w: unknown): string => { const s = String(w || '') const m = s.match(/20[0-9][0-9]-[0-9][0-9](?:~20[0-9][0-9]-[0-9][0-9])?|至[ ]*20[0-9][0-9]-[0-9][0-9]|20[0-9][0-9]~20[0-9][0-9]/) return m ? (m[0].startsWith('至') ? t('win.through', { date: m[0].replace(/^至\s*/, '') }) : m[0]) : s.slice(0, 14) } const th = (name: string, win: string, extra?: string): string => `
${name}
${dv(shortWin(win))}${extra || ''}
` const LW = F.列窗 || {} h += `
${''.repeat(6)}${th(t('fus.col.vib'), LW.振动, t('fus.col.fixed'))}${th(t('fus.col.temp'), LW.温度)}${th(t('fus.col.lub'), LW.润滑, t('fus.col.display_only'))}${th(t('fus.col.oil'), LW.油液)}${th(t('fus.col.alarm'), LW.报警)}${th(t('fus.col.wo'), LW.工单)}` const mat = F.矩阵 || [] const MAXAL = Math.max(1, ...mat.map((r: any) => (r.报警 || {}).n || 0)) const MAXWO = Math.max(1, ...mat.map((r: any) => (r.工单 || {}).n || 0)) const WC = F.色标 || {} const shortOf = (c: any): string => { if (c.ostate && c.ostate !== '不可判') return stWord(c.ostate) const n = String(c.note || '') if (/数据不足|不可判|nsufficient/.test(n)) return t('fus.short.nodata') if (/未评估|ot assessed/.test(n)) return t('fus.short.uneval') if (/无样|未取样|o sample/.test(n)) return t('fus.short.nosample') if (/样龄|stale/.test(n)) return t('fus.short.stale') const m: Record = { ok: t('fus.short.normal'), unlisted: t('fus.short.unlisted'), stale: t('fus.short.stale') } return m[c.level] || '—' } const cell = (c0: any): string => { const c = c0 || {} const col = CST[c.ostate] || WC[c.wlevel] || CLV[c.level] || 'var(--muted)' const full = vibPlain(c.note) const isProb = ['bad', 'warn', 'note'].includes(c.level) const tip = esc(full) + t('fus.tip_state', { s: stWord(c.ostate || '?') }) if (!isProb) return `` const parts = full.split(/[::]/) const brief = (parts.length > 1 ? parts.slice(1).join(':') : full).trim() const cleanBrief = (/^[ ]*(评估报告|两口径差异|⚠|ssessment)/.test(brief) || brief.length > 16) ? '' : brief const shortCut = cleanBrief ? dv(cleanBrief) : (c.ostate && c.ostate !== '不可判' ? '' : shortOf(c)) const ev = (c.wlevel && c.wlevel !== '—' && c.wlevel !== '正常' && c.ostate) ? duo(stWord(c.ostate), tierWord(c.wlevel), col) : '' return `` } mat.forEach((r: any) => { const al = r.报警 || { n: 0, fam: [] }, wo = r.工单 || { n: 0, acts: [] } const alc = al.rank && al.rank <= 5 ? 'var(--warning)' : 'var(--line)' const bu = al.burst const altip = esc((al.items || []).map((x: any) => `${x.code} ${vibPlain(x.text)} — ${x.n} (${x.first}~${x.last})`).join('\n')) + (bu ? '\n' + esc(t('fus.burst_tip', { c: bu.code, p: (bu.share * 100).toFixed(0), m: bu.med_s })) : '') h += `${cell(r.振动)}${cell(r.温度)}${cell(r.润滑)}${cell(r.油液)} ` }) return h + `
${t('ui.unit')}
${shortOf(c)}
${shortCut}
${ev}
${spark(c.hist, c.level === 'bad' ? 'var(--critical)' : c.level === 'warn' ? 'var(--warning)' : 'var(--muted)')}
${unitNo(r.turbine)}${t('fus.alarm_n', { n: al.n })}${al.rank ? `${t('fus.alarm_rank', { r: al.rank, t: al.tot })}` : ''}${bu ? `
${t('fus.burst', { p: (bu.share * 100).toFixed(0), c: bu.code })}
` : ''}${(al.rank && al.rank <= 10) || bu ? famBar(al.fam, al.n, MAXAL) : ''}
${wo.n ? `${t('fus.wo_n', { n: wo.n })}${wo.last ? ` · ${esc(wo.last)}` : ''}
${(wo.acts || []).slice(0, 2).map((a: any) => dv(a.k) + '×' + a.n).join(' · ')}
` : `—`}

${t('fus.matrix_foot')}

` } export const baBar = (pre: number, post: number, col: string): string => { const M = Math.max(pre, post, 7), W = 120, sc = (v: number) => Math.max(2, v / M * (W - 4)) return `` } export const chainMini = (comp: unknown, col: string): string => { const c = String(comp || '').replace(/main bearing[, ]*[(]?(?:rear|back)[)]?/ig, '主轴承后').replace(/main bearing[, ]*[(]?(?:front|forward)[)]?/ig, '主轴承前').replace(/main bearing/ig, '主轴承').replace(/generator[, ]*(?:NDE|non-drive[- ]end)/ig, '发电机NDE').replace(/generator[, ]*(?:DE|drive[- ]end)/ig, '发电机DE').replace(/generator/ig, '发电机').replace(/gearbox/ig, '齿轮箱').replace(/planetary/ig, '行星').replace(/intermediate[- ](?:speed[- ])?shaft/ig, '中间轴').replace(/high[- ]speed shaft/ig, '高速轴'), gball = /齿轮箱/.test(c) && !/行星|中间轴|中速|高速轴/.test(c); return `${CHAIN7.map(([k, re]) => { const on = re.test(c) || (gball && ['ps', 'ims', 'hss'].includes(k)); return ``; }).join('')}`; }; /** 旧 `fusLoop(F)` —— 端到端闭环(逐字转写,app.js:912-922) */ export function loopHtml(F: any): string { const LP = (F.能力 && F.能力.loop && F.能力.loop['换件闭环 (检出→换件→恢复)']) ? F.能力.loop : null if (!LP) return `

${t('fus.loop_none')}

` const cl = LP['换件闭环 (检出→换件→恢复)'] || [] const up = LP['当前在升 (无工单可对)'] || [] const neg = LP['负面对照 (工单后持平)'] return `

${t('fus.loop_head')}

${t('fus.loop_note')}

${t('fus.loop_closed', { n: cl.length })}

${cl.map((r: any) => ``).join('')}
${t('ui.unit')}${t('fus.col.part')}${t('fus.loop_item')}${t('fus.loop_date')}${t('fus.loop_ba')}${t('fus.loop_ratio')}${t('fus.loop_verdict')}
${unitNo(r.turbine)}${chainMini(r.component + (r.title || ''), 'var(--good)')} ${partName(r.component)}${dv(r.title)}${esc(r.replace_date)}${baBar(r.pre, r.post, 'var(--good)')}${r.pre} → ${r.post}${r.ratio}×${dv(r.verdict)}

${t('fus.loop_neg', { n: neg })}

${t('fus.loop_rising', { n: up.length })}

${up.map((r: any) => ``).join('')}
${t('ui.unit')}${t('fus.col.part')}${t('fus.loop_el')}${t('fus.loop_x')}${t('fus.loop_state')}
${unitNo(r.turbine)}${chainMini(r.component, 'var(--warning)')} ${partName(r.component)}${baBar(r.early, r.late, 'var(--warning)')}${r.early} → ${r.late}${r.ratio}×${t('rpt.units.rising', { r: r.ratio })}

${t('fus.loop_bound', { b: dv(String(LP['★数据边界'] || '').split('**').join('')) })}

` } export const devBar = (ch: unknown, dev: number | null, vd: unknown, raw: unknown): string => { if (dev == null) return `${dv(displayText(raw).slice(0, 30))}`; const M = 6, W = 104, mid = W / 2, k = Math.max(-1, Math.min(1, dev / M)), bw = Math.abs(k) * (mid - 8), strong = Math.abs(dev) >= 3; const col = !strong ? 'var(--muted)' : (/候选|andidate/.test(vd || '') ? 'var(--warning)' : /记基线|aseline/.test(vd || '') ? 'var(--accent)' : /不可判|返服|ndeterm/.test(vd || '') ? 'var(--unknown)' : 'var(--muted)'), over = Math.abs(dev) > M; return `
${over ? `` : ''}${dev > 0 ? '+' : ''}${dev}K
${esc(ch || '')}
`; }; export const winBar = (spans: any[] | undefined, raw: unknown): string => { if (!spans || !spans.length) return `${t('fus.win_none')}`; const T0 = Date.parse('2026-01-01'), T1 = Date.parse('2026-09-01'), W = 118, px = d => 3 + Math.max(0, Math.min(1, (Date.parse(d) - T0) / (T1 - T0))) * (W - 6); return `
${spans.map(([a, b]) => { const x1 = px(a), x2 = px(b); return ``; }).join('')}
${t('fus.win_n', { n: spans.length })}
`; }; export const oilPill = (last: unknown, fresh: unknown, raw: unknown): string => `
${esc(last || t('fus.oil_none'))}
${last ? (fresh ? t('fus.oil_fresh') : t('fus.oil_stale')) : '—'}
`; /** 旧 `stateChip`(app.js:84) */ export const stateChip = (zh: unknown, cls?: string): string => { const k = stateKey(zh) return k ? `${t(stKey(k))}` : (zh ? `${esc(zh)}` : '') } /** 旧 `fusUnits(F)` —— 逐台情况(逐字转写,app.js:924-944);filter 对应旧 `FUS.filter` */ export function unitsHtml(F: any, filter: string | null): string { const lv = (v: unknown): string => /定论|报警|onfirmed|larm/.test(String(v)) ? 'bad' : /预警|arning/.test(String(v)) ? 'warn' : /销案|无新证|保留|losed|o new/.test(String(v)) ? 'ok' : 'note' const tb = F.表 || [], mat = F.矩阵 || [] if (filter === 'unlisted') { const un = mat.filter((r: any) => (r.振动 || {}).level === 'unlisted') return `

${t('fus.unlisted_head', { n: un.length })} ${t('fus.clear')}

${un.map((r: any) => ``).join('')}
${t('ui.unit')}${t('fus.temp_now')}${t('fus.lub_now')}
${unitNo(r.turbine)}${dv(vibPlain((r.温度 || {}).note))}${dv(vibPlain((r.润滑 || {}).note))}
` } let rows = tb if (filter) rows = tb.filter((r: any) => lv(r.振动结论) === filter) const filt = filter ? t('fus.units_filter', { l: t('fus.lv.' + filter) }) + ` ${t('fus.clear')}` : '' let h = `

${t('fus.units_head', { u: new Set(tb.map((x: any) => x.turbine)).size, n: rows.length + (filter ? '/' + tb.length : ''), f: filt })}

${['bad', 'warn', 'note', 'ok'].map((k) => ``).join('')}
${t('fus.eval_head')}
${t('fus.eval_state')}${t('fus.eval_tier')}${t('fus.eval_meaning')}
${stateChip('危险')}${t('tier.confirmed.s')}${t('fus.m.bad')} (${t('fus.m.bad_e')})
${stateChip('报警')}${t('tier.near')} / ${t('tier.candidate_new')}${t('fus.m.warn')} (${t('fus.m.warn_e')})
${stateChip('良好')}${t('tier.candidate')} / ${t('tier.candidate_baseline')}${t('fus.m.ok')} (${t('fus.m.ok_e')})
${stateChip('优秀')}${t('tier.reference')} / ${t('tier.normal')} / ${t('tier.withdrawn')}${t('fus.m.ex')} (${t('fus.m.ex_e')})
${stateChip('不可判')}${t('tier.insufficient')}${t('fus.m.unk')} (${t('fus.m.unk_e')})

${t('fus.eval_note')}

` rows.forEach((r: any) => { const sev = CLV[r.级] || 'var(--accent)', col = CST[r.设备状态] || 'var(--muted)' h += `` }) return h + `
${t('ui.unit')}${t('fus.col.part')}${t('fus.col.state_concl')}${t('fus.col.claim')} ${t('fus.col.claim_axis')}${t('fus.col.signal')}${t('fus.col.tempaxis')} ${t('fus.col.tempaxis_note')}${t('fus.col.oil')}
${unitNo(r.turbine)}${chainMini(r.部件, sev)} ${partName(r.部件)} ${duo(stWord(r.设备状态 || '—'), dv(r.振动结论), col)}${winBar(r.窗区间, r.claim_window)} ${dv(vibPlain(r.振动信号))}${devBar(r.温度通道, r.温度dev, r.温度判, r.温度轴)}${oilPill(r.油末样, r.油新鲜, r.油样轴)}
` } export const CHAIN7 = [['mbf', /主轴承[((]前|主轴承前|主轴承[++]|主轴承(?![((前后])/], ['mba', /主轴承[((]后|主轴承后/], ['ps', /行星|二级行星/], ['ims', /中间轴|中速/], ['hss', /高速轴|高速(?!.*发电)/], ['gde', /发电机DE|发电机[((]|发电机(?!NDE)/], ['gnde', /发电机NDE|NDE/]]; export const gc = st => STC[stateKey(st)] || null; export const chainStates = (map) => `${Object.entries(map).map(([k, col]) => ``).join('')}`; export const evLadder = (x) => { const T = String(x || ''), n = /准定论|near[- ](?:conf|establish)/i.test(T) ? 3 : /确诊|onfirmed/.test(T) ? 4 : /定论|ettled|stablished/.test(T) ? 3 : /候选|andidate/.test(T) ? 2 : /参考|eference/.test(T) ? 1 : 0, col = n >= 4 ? 'var(--critical)' : n === 3 ? 'var(--warning)' : n === 2 ? 'var(--accent)' : n === 1 ? 'var(--muted)' : 'var(--unknown)'; return `
${[0, 1, 2, 3].map(i => `= n ? 'stroke-dasharray="2 1.5"' : ''}/>`).join('')}${dv(T)}
` }; /** 旧 `fusEvents(F, d)` —— 报警工单(逐字转写,app.js:946-958) */ export function eventsHtml(F: any, d: any): string { if (!F.事件) return `

${t('ui.empty')}

` let h = `

${t('fus.ev_win', { win: winName(d.win) })}

` for (const [comp, E0] of Object.entries(F.事件 as Record)) { const E = E0 as any const A = E.报警 || {}, W = E.工单 || {} h += `

${t('fus.ev_head', { c: sysName(comp), an: A.总条数 || 0, au: A.台数 || 0, wn: W.总条数 || 0, wu: W.台数 || 0 })}

` if (A.总条数 > 0) { h += `
${(A.码统计 || []).map((c: any) => ``).join('')}
${t('fus.ev_code')}${t('fus.ev_text')}${t('ui.count')}${t('fus.ev_units')}${t('fus.ev_dur')}
${esc(c.code)}${dv(vibPlain(c.text))}${c.条数}${c.台数}${c.时长h}

${t('fus.ev_top')}${Object.entries(A.top台 || {}).map(([u, n]) => `${unitNo(u)}×${n}`).join(' · ')}

${t('fus.ev_recent', { n: (A.近况 || []).length })}
${(A.近况 || []).map((x: any) => ``).join('')}
${t('ui.unit')}${t('fus.ev_time')}${t('fus.ev_code')}${t('fus.ev_text')}${t('fus.ev_min')}
${unitNo(x.t)}${esc(x.on)}${esc(x.code)}${dv(vibPlain(x.text))}${x.dur_min ?? '—'}
` } else h += `

${t('fus.ev_no_alarm')}

` if (W.总条数 > 0) h += `
${(W.明细 || []).map((x: any) => ``).join('')}
${t('ui.unit')}${t('ui.date')}${t('fus.ev_wo_name')}${t('fus.ev_wo_act')}${t('fus.ev_wo_cause')}
${unitNo(x.t)}${esc(x.date)}${dv(x.name)}${dv(x.act)}${dv(x.cause)}
` else h += `

${t('fus.ev_no_wo')}

` h += `
` } return h } /** 旧 `fusCms()`(app.js:960-976)—— 数据来自 `/api/vibcms`(按需异步取) */ export function cmsHtml(V: any, err?: string | null): string { if (err) return `

${esc(err)}

` if (!V) return `

${t('fus.cms_loading')}

` if (V.error) return `

${dv(V.error)}

` const CK: Record = { '主轴承前': 'mbf', '主轴承后': 'mba', '齿轮箱': 'gb', '发电机': 'gen', '发电机DE': 'gde', '发电机NDE': 'gnde', 'Main bearing, front': 'mbf', 'Main bearing, rear': 'mba', 'Gearbox': 'gb', 'Generator': 'gen', 'Generator DE': 'gde', 'Generator NDE': 'gnde', } return `

${t('fus.cms_head', { d: esc(V.date || '') })}

${V['时间范围'] ? `

${t('fus.cms_window', { r: esc(V['时间范围']) })}

` : ''}

${dv(V.overall || '')} ${t('fus.cms_note')}

${t('fus.cms_action')}

${(V.action || []).map((r: any) => { const cm: Record = {} String(r[2]).split(/[;;]/).forEach((seg: string) => { const m = seg.split(/[::]/) if (m.length > 1) { const k = CK[m[0].trim()] || null; if (k) cm[k] = gc(m[1].trim()) } }) return `` }).join('')}
${t('ui.unit')}${t('fus.loop_state')}${t('fus.cms_focus')} ${t('fus.cms_focus_note')}${t('fus.cms_evidence')}${t('fus.cms_act')}
${unitNo(r[0])}${stateChip(r[1])}${chainStates(cm)} ${dv(r[2])}${evLadder(r[3])}${/P0/.test(r[4]) ? `${esc(r[4])}` : esc(r[4])}

${t('fus.cms_grades')}

${(V.grades || []).map((g: any) => ``).join('')}
${t('ui.unit')}${t('fus.cms_chain')} ${t('fus.cms_chain_note')}${t('fus.cms_overall')}${t('fus.cms_act')}
${unitNo(g.t)}${chainStates({ mbf: gc(g.主轴承前), mba: gc(g.主轴承后), gb: gc(g.齿轮箱), gen: gc(g.发电机) })}${stateChip(g.综合)}${esc(g.行动)}
${t('fus.cms_basis')} ${t('fus.cms_basis_note', { a: ((V.diff || {}).only_cms || []).map(unitNo).join(', ') || t('fus.none_word'), b: (((V.diff || {}).only_vibration_package || (V.diff || {})['only_' + 'hand' + 'off']) || []).map(unitNo).join(', ') || t('fus.none_word') })}

${t('fus.cms_link')} ${t('fus.cms_open')} ${t('fus.cms_static')}

` } export const FUS_SUBS = ['matrix', 'loop', 'units', 'events', 'cms'] as const