# -*- coding: utf-8 -*- """分析工作台首页: 左机组树 + 右主视图 (2026-08-28 用户令, 参照经典振动分析系统的形式)。 两版**分别生成两个 HTML 文件**, 不做同页 JS 切换 — 前端隐藏的内容 F12 就能看到, 那不算脱敏 (memory client-deliverable-hide-core-parameters)。 - 客户版 `index.html` : 一句话结论 + 部件图 + 一条趋势 + 建议动作。不放谱图/特征频率/阈值。 - 工程师版 `index_eng.html`: 上述 + 判据明细 (过闸线/Hz/证据族) + 报警事件 + 逐测点趋势。 形式取自经典振动系统 (Bently System 1 / SKF @ptitude / G&J TCM): 左树导航, 右主视图, 每台带严重度色条; 但**内容按客户可读性裁剪** — 客户版不放频谱/包络谱/倒谱 (用户 2026-08-28: "给客户用的, 别太复杂")。 """ import pandas as pd from . import report_std, svg, tcm, ui from .config import SENSORS, SENSOR_CN from .report import STATE_CLASS from .redact import redact_always from .ui import esc COMP_SEG = {'主轴': [('前', 'Main_bearing_front'), ('后', 'Main_bearing_rear')], '齿轮箱': [('行星', 'Gear_planet'), ('中间', 'Gear_IMS'), ('高速', 'Gear_HS_rotor_side'), ('高速', 'Gear_HS_generator_side')], '发电机': [('DE', 'Generator_DE'), ('NDE', 'Generator_NDE')]} COMP_OF = {'主轴': ('主轴承前', '主轴承后'), '齿轮箱': ('齿轮箱',), '发电机': ('发电机',)} ATTN = ('危险', '报警', '不可判') STATE_COLOR = {'优秀': 'var(--st-exc)', '良好': 'var(--st-good)', '报警': 'var(--st-warn)', '危险': 'var(--st-bad)', '不可判': 'var(--st-na)'} # 客户版主趋势: 每台只画**一条** — 挑该台最能说明问题的测点 (状态最差的那个), 客户不需要看 8 条 PRIMARY_MEAS = 'Peak' def _worst_sensor(detail_t, reg_row): """挑该台状态最差的测点作主趋势 (并列时取 SENSORS 顺序靠前的)。""" l0 = detail_t.get('L0') or {} best, best_i = None, 99 for sen in SENSORS: st = None imp = (detail_t.get('冲击') or {}).get(sen) if imp and imp.get('type') not in (None, '正常', 'INSUFFICIENT'): st = report_std.impact_state(imp) lv = [r['定级'] for r in (detail_t.get('线') or []) if r.get('测点') == sen] if lv: s2 = report_std._worst_state([report_std.line_state(x) for x in lv]) st = s2 if st is None else report_std._worst_state([st, s2]) if st is None and l0.get(sen, '').startswith('可用'): st = '优秀' if st is None: continue i = report_std.STATE_ORDER.index(st) if st in report_std.STATE_ORDER else 98 if i < best_i: best, best_i = sen, i return best or 'Main_bearing_rear' def _trend_series(c, t, sen, meas=PRIMARY_MEAS): """跨窗趋势: 逐窗中位 + 机群同档中位 (作参照带)。返回 (x 标签, 自身, fleet)。""" try: q, _ = tcm.trend(c['scalars'], c['masks'], 'CGN Rudong', t, sen, meas) except Exception: return [], [], None if q is None or not len(q): return [], [], None per = q.groupby('window').scalar_value.median() try: ref = tcm.fleet_ref(c['scalars'], sen, meas) fleet = float(ref.get('fleet_median')) if ref else None except Exception: fleet = None return list(per.index), [float(v) for v in per.values], fleet def _trend_svg(xs, ys, fleet, unit='m/s²', title=''): if not xs: return '
该测点在当前数据窗内无可用趋势点。
' hl = [(fleet, '机群同档中位', 'var(--ref-line)')] if fleet else [] return svg.line_chart([{'name': '本机逐窗中位', 'x': list(range(len(xs))), 'y': ys, 'xlabels': xs}], hlines=hl, title=title, w=620, h=210, ylabel=unit, ymin0=True) def _seg_colors(t, detail_t, comp, cell_fn): seg, tips = {}, [] for k, sen in COMP_SEG[comp]: st, _ = cell_fn(t, sen) if st in ATTN: cur = seg.get(k + '_st') if cur is None or report_std.STATE_ORDER.index(st) < report_std.STATE_ORDER.index(cur): seg[k], seg[k + '_st'] = STATE_COLOR[st], st tips.append(f'{SENSOR_CN[sen]} {st}') return {k: v for k, v in seg.items() if not k.endswith('_st')}, (';'.join(tips) or '各测点未见异常') def tree_html(reg): """左树: 需关注在前, 每台一条严重度色条。展开到部件为止 (客户不下钻测点)。""" def node(r): t, st = r['机组'], r['状态等级'] comps = ''.join( f'趋势数据不可用。
', {} if q is None or not len(q): return '该测点在当前数据窗内无趋势点。
', {} q = q.copy() q['ts'] = pd.to_datetime(q['trigger_time'], errors='coerce') # 逐 (窗, 日) 中位 —— 不用 groupby('window'): 单窗时那只会得到一个点, 把整段区间压成一个时刻 # (2026-09-19 实逮: 3 月的数据在图上"消失"就是这个原因, 不是数据没进来) q = q.dropna(subset=['ts']) q['day'] = q['ts'].dt.floor('D') per = (q.groupby(['window', 'day'], as_index=False) .agg(ts=('ts', 'median'), v=('scalar_value', 'median')) .sort_values('ts')) if not len(per): return '趋势点时间戳缺失。
', {} series = [{'name': '本机逐日中位', 'x': list(per['ts']), 'y': [float(x) for x in per['v']]}] hl = [] try: ref = tcm.fleet_ref(c['scalars'], sen, meas) if ref and ref.get('fleet_median'): hl.append((float(ref['fleet_median']), '机群同档中位', 'var(--ref-line)')) except Exception: pass note = '' if engineer and th: mt = str(th.get('mask_time', ''))[:10] if th.get('yellow'): hl.append((float(th['yellow']), f'厂商黄线({mt})', 'var(--th-yellow)')) if th.get('red'): hl.append((float(th['red']), f'厂商红线({mt})', 'var(--th-red)')) note = f'厂商阈值设定于 {esc(mt)},与本数据窗不同期,仅作对照。
' elif engineer: note = '该测量厂商未设阈值(masks 为空),故不画阈值线;判读以机群同档中位与自身纵向变化为准。
' n_alarm = int((q.alarm_type.isin(['RedMask', 'YellowMask'])).sum()) if 'alarm_type' in q else 0 unit = 'm/s²' if meas in ('Peak', 'Rms_HP', 'iso_rms') else ('m/s' if 'Vel' in meas else '') fig = svg.line_chart(series, hlines=hl, title=title or f'{SENSOR_CN.get(sen, sen)} · {meas}', w=620, h=210, ylabel=unit, ymin0=True) return fig + note, {'n_alarm': n_alarm, 'n_win': len(per), 'first': per['ts'].iloc[0], 'last': per['ts'].iloc[-1]} def panel_html(cfg, t, reg_row, detail_t, c, cell_fn, engineer=False): """单台右主视图。客户版=结论+部件图+一条趋势+建议; 工程师版另加判据明细与报警统计。""" st = reg_row['状态等级'] try: analysis, concl, act, accept = report_std.turbine_narrative(cfg, t, detail_t, reg_row, c) except Exception as e: analysis, concl, act, accept = f'(叙述生成失败:{type(e).__name__})', '', '', '' concl_txt = concl.replace('结论:', '', 1) if concl else '—' if not engineer: # 客户版强制脱敏 (不看 WINDCMS_INTERNAL — 交付物脱敏必须是结构性的) concl_txt, act, analysis = redact_always(concl_txt), redact_always(act), redact_always(analysis) glyphs = ''.join( f'厂商报警记录:{meta["n_alarm"]} 条(该测点,全数据窗)。
' b = [f'{esc(concl_txt)}
'] if act: b.append(f'建议动作 {esc(act)}
') b.append(f'{esc(analysis)}
') lines = detail_t.get('线') or [] if lines: b.append('| 测点 | 线 | Hz | 域 | ' '绝对量 | ×fleet | 定级 |
|---|---|---|---|---|---|---|
| {esc(SENSOR_CN.get(r["测点"], r["测点"]))} | {esc(r["线"])} | ' f'{esc(r["hz"])} | {esc(report_std.dom_cn(r["域"]))} | ' f'{esc(r["绝对量"])} {esc(r["单位"])} | {esc(r["xfleet"])} | ' f'{esc(r["定级"])} |
无过闸特征线。
') if accept: b.append(f'验收判据 {esc(accept)}
') b.append('