-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
161 lines (161 loc) · 78 KB
/
Copy pathindex.html
File metadata and controls
161 lines (161 loc) · 78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Log Analysis · 镀膜日志分析工作台</title>
<style>
:root{--bg:#f1f4f7;--surface:#fff;--soft:#f7f9fb;--line:#d8e0e7;--strong:#bcc9d4;--text:#17242e;--muted:#5b6d7c;--accent:#075e91;--accent2:#064b73;--accent-soft:#e6f1f7;--success:#267451;--warning:#9a5b12;--danger:#b42318;--radius:10px;--header-h:68px;--source-h:48px}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.5 "Segoe UI","Microsoft YaHei",Arial,sans-serif}::selection{background:#cbe5f2;color:#063b5a}button,a.button,input,select{font:inherit}button,a.button{min-height:38px;border:1px solid transparent;border-radius:8px;background:var(--accent);color:#fff;padding:7px 14px;cursor:pointer;text-decoration:none;white-space:nowrap}button:hover,a.button:hover{background:var(--accent2)}button:active,a.button:active{transform:translateY(1px)}button:disabled{opacity:.52;cursor:wait}.secondary{border-color:var(--line)!important;background:#fff!important;color:#294252!important}.secondary:hover{background:var(--accent-soft)!important;border-color:#9ebfd2!important}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,summary:focus-visible{outline:3px solid rgba(7,94,145,.28);outline-offset:2px}input,select{min-height:38px;padding:7px 9px;border:1px solid var(--strong);border-radius:8px;background:#fff;color:var(--text)}input[readonly]{background:var(--soft)}.hidden,[hidden]{display:none!important}.muted{color:var(--muted)}
.app-header{position:sticky;top:0;z-index:40;height:var(--header-h);background:rgba(255,255,255,.98);border-bottom:1px solid var(--line)}.header-inner{height:100%;padding:10px 22px;display:grid;grid-template-columns:auto minmax(260px,1fr) auto auto;gap:12px;align-items:center}.brand{display:flex;align-items:center;gap:9px}.brand-mark{width:34px;height:34px;border-radius:8px;background:var(--accent);display:grid;place-items:center;color:#fff;font-size:12px;font-weight:800}.brand h1{font-size:17px;line-height:1.15;margin:0}.brand p,.path-field span{margin:2px 0 0;color:var(--muted);font-size:11px}.path-field{min-width:0}.path-field span{display:block;margin-bottom:2px}.path{width:100%;height:34px;min-height:34px;text-overflow:ellipsis}.header-actions{display:flex;gap:8px}.service{display:flex;align-items:center;gap:7px;color:var(--muted);font-size:12px;white-space:nowrap}.status-dot{width:8px;height:8px;border-radius:50%;background:#96a5b2}.service[data-state=online] .status-dot{background:var(--success)}.service[data-state=busy] .status-dot{background:var(--warning)}.service[data-state=offline] .status-dot{background:var(--danger)}
.source-switch{position:sticky;top:var(--header-h);z-index:38;height:var(--source-h);display:flex;align-items:center;gap:12px;padding:6px 22px;background:#e9eef2;border-bottom:1px solid var(--line)}.source-switch>strong{font-size:12px;color:#405463}.scope-options{display:flex;gap:2px;padding:2px;border:1px solid var(--strong);border-radius:8px;background:#fff}.scope-options button{min-height:32px;padding:4px 16px;background:transparent;color:#4b6070}.scope-options button:hover{background:var(--accent-soft);color:var(--accent)}.scope-options button.active{background:var(--accent);color:#fff;font-weight:700}.source-note{margin-left:auto;color:var(--muted);font-size:12px}.workspace{display:grid;grid-template-columns:206px minmax(0,1fr);min-height:calc(100dvh - var(--header-h) - var(--source-h))}.sidebar{position:sticky;top:calc(var(--header-h) + var(--source-h));align-self:start;height:calc(100dvh - var(--header-h) - var(--source-h));padding:18px 12px;background:#e9eef2;border-right:1px solid var(--line)}.sidebar-label{margin:0 10px 9px;color:#718290;font-size:11px;font-weight:700;letter-spacing:.08em}.module-nav{display:grid;gap:4px}.module-nav button{width:100%;text-align:left;background:transparent;color:#405463;border-color:transparent;padding:9px 12px}.module-nav button:hover{background:#fff;color:var(--accent)}.module-nav button.active{background:#fff;border-color:var(--line);color:var(--accent);font-weight:700;box-shadow:inset 3px 0 var(--accent)}.content-shell{min-width:0;padding:0 22px 40px}.notice{display:grid;grid-template-columns:1fr auto;gap:12px;align-items:center;padding:12px 14px;border:1px solid #efc3bf;border-radius:var(--radius);background:#fff4f2;color:#76251d;margin:14px 0}.notice p{margin:2px 0 0}.analysis-progress{display:grid;grid-template-columns:minmax(120px,1fr) minmax(180px,2fr) auto;gap:12px;align-items:center;margin:12px 0;padding:10px 12px;border:1px solid var(--line);border-radius:var(--radius);background:#fff;color:var(--muted);font-size:12px}.analysis-progress strong{color:var(--text)}.analysis-progress progress{width:100%;height:8px;accent-color:var(--accent)}.progress-time{font-variant-numeric:tabular-nums}.empty-state{min-height:calc(100dvh - var(--header-h) - var(--source-h) - 40px);display:grid;place-items:center;text-align:center}.empty-state h2{font-size:25px;margin:0 0 8px}.empty-state p{color:var(--muted);margin:0}.skeleton{display:grid;gap:10px;width:min(480px,80vw);margin:24px auto 0}.skeleton span{height:12px;border-radius:5px;background:#e5ebf0}.skeleton span:nth-child(2){width:72%}
.filters{position:sticky;top:calc(var(--header-h) + var(--source-h));z-index:30;display:flex;gap:10px;flex-wrap:wrap;align-items:end;margin:0 -22px 16px;padding:9px 22px;background:rgba(241,244,247,.97);border-bottom:1px solid var(--line)}.filter-field{display:grid;gap:4px}.filter-field>span{font-size:11px;font-weight:700;color:#516676}.filters input[type=number]{width:88px;height:36px;min-height:36px}.filter-spacer{flex:1}.segments{height:36px;display:flex;border:1px solid var(--strong);border-radius:8px;background:#fff;padding:2px}.segments label{position:relative;cursor:pointer}.segments input{position:absolute;opacity:0;pointer-events:none}.segments span{height:30px;min-width:42px;padding:5px 10px;display:grid;place-items:center;border-radius:6px;color:#5b6d7c;font-size:12px;font-weight:650}.segments input:checked+span{background:var(--accent-soft);color:var(--accent)}.segments input:disabled+span{opacity:.45;cursor:not-allowed}.segments input:focus-visible+span{outline:3px solid rgba(7,94,145,.28)}.download-group{display:flex;gap:7px}.download-group a{min-height:36px;padding:6px 11px;font-size:12px}
.page-head{margin:18px 0 12px}.page-head h2{font-size:22px;margin:0}.page-head p{color:var(--muted);margin:3px 0 0}.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(155px,1fr));border:1px solid var(--line);border-radius:var(--radius);background:#fff;margin-bottom:14px;overflow:hidden}.card{min-height:94px;padding:14px 15px;border-right:1px solid var(--line);color:var(--muted);font-size:12px}.card b{display:block;font-size:21px;line-height:1.2;color:var(--text);margin:7px 0 3px}.panel,.chart-panel{min-width:0;background:#fff;border:1px solid var(--line);border-radius:var(--radius)}.panel{padding:14px}.grid,.optical-detail-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.stack{display:grid;gap:14px}.section-title{font-size:16px;margin:22px 0 10px}.chart-panel{position:relative;overflow:hidden}.chart-toolbar{position:absolute;z-index:5;top:9px;right:10px;display:flex;gap:7px;align-items:center}.chart-toolbar label{display:flex;gap:6px;align-items:center;color:#526878;font-size:12px}.chart-toolbar select{height:33px;min-height:33px;max-width:220px}.focus-chart{min-height:33px;padding:5px 10px;font-size:12px}.chart{height:370px;min-width:0}.chart.tall{height:430px}.chart-panel.focused{position:fixed;inset:10px;z-index:101;border-color:#9ab0c0;box-shadow:0 28px 80px rgba(8,30,43,.28);border-radius:12px}.chart-panel.focused .chart{height:calc(100dvh - 22px)!important}.chart-panel.focused .focus-chart{background:#fff!important}.chart-backdrop{position:fixed;inset:0;z-index:100;background:rgba(9,25,35,.58);backdrop-filter:blur(2px)}body.chart-focus-open{overflow:hidden}
table{border-collapse:collapse;width:100%;background:#fff;font-variant-numeric:tabular-nums}th,td{padding:9px 11px;border-bottom:1px solid #e8edf1;text-align:left;white-space:nowrap}th{position:sticky;top:0;z-index:1;background:#f5f8fa;color:#405667;font-size:12px}th abbr{text-decoration:none}tbody tr:hover{background:#f5fafc}.table-wrap{overflow:auto;max-height:460px;border:1px solid var(--line);border-radius:8px}.detail-head{display:flex;gap:8px;align-items:center;flex-wrap:wrap;padding:13px 14px;border:1px solid var(--line);border-radius:var(--radius);background:#fff}.detail-head h3{font-size:16px;margin:0 10px 0 0}.image-panel{padding:14px}.image{max-width:100%;max-height:620px;display:block;margin:auto}.device-layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:14px}.metric-rail{padding:13px}.metric-group+.metric-group{margin-top:15px}.metric-group h3{margin:0 0 6px;color:#738391;font-size:11px;letter-spacing:.06em}.metric-group button{width:100%;min-height:34px;margin-top:3px;padding:5px 9px;text-align:left;background:transparent;color:#405463;border-color:transparent}.metric-group button:hover{background:var(--accent-soft);color:var(--accent)}.metric-group button.active{background:var(--accent);color:#fff;font-weight:650}.device-main{min-width:0;display:grid;gap:14px}.view-toggle{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:10px 12px;border:1px solid var(--line);border-radius:var(--radius);background:#fff}.view-toggle>span{font-size:12px;font-weight:700;color:#516676}.view-toggle .segments{margin-right:auto}.status-line{display:flex;gap:9px;align-items:center;flex-wrap:wrap}.status-badge{display:inline-flex;align-items:center;min-height:28px;padding:3px 9px;border:1px solid var(--line);border-radius:999px;background:var(--soft);color:#405463;font-size:12px;font-weight:700}.status-badge.success{border-color:#9bc7b2;background:#edf8f2;color:var(--success)}.status-badge.warning{border-color:#e5c38f;background:#fff8eb;color:#7f4c10}.status-badge.danger{border-color:#efb4ae;background:#fff2f0;color:var(--danger)}.source-partitions{display:grid;gap:18px}.source-partition{padding-top:4px}.source-partition h3{margin:0 0 8px;font-size:16px}.table-note{margin:8px 0 0;color:var(--muted);font-size:12px}details{margin-top:18px;border-top:1px solid var(--line);padding-top:12px}summary{cursor:pointer;color:var(--accent);font-weight:650;padding:6px 0}.direct-file{min-height:100dvh;display:grid;place-items:center;padding:24px}.direct-card{width:min(620px,100%);padding:30px;border:1px solid var(--line);border-radius:var(--radius);background:#fff}.direct-card h1{font-size:26px;margin:0}.launch-file{display:block;margin-top:18px;padding:13px;border:1px solid var(--line);border-radius:8px;background:var(--soft);font-weight:600;user-select:all}
@media(max-width:1100px){.header-inner{grid-template-columns:auto minmax(220px,1fr) auto}.service{display:none}.grid{grid-template-columns:1fr}}@media(max-width:980px){.optical-detail-grid{grid-template-columns:1fr}}@media(max-width:900px){:root{--header-h:116px}.header-inner{grid-template-columns:1fr auto;height:auto}.path-field{grid-column:1/-1;grid-row:2}.source-switch{overflow-x:auto;padding-inline:14px}.source-note{display:none}.workspace{display:block}.sidebar{position:sticky;top:calc(var(--header-h) + var(--source-h));z-index:32;width:auto;height:auto;padding:7px 14px;border-right:0;border-bottom:1px solid var(--line)}.sidebar-label{display:none}.module-nav{display:flex;overflow-x:auto}.module-nav button{width:auto;min-width:max-content;text-align:center}.module-nav button.active{box-shadow:inset 0 -2px var(--accent)}.device-layout{grid-template-columns:1fr}.metric-rail{display:flex;gap:12px;overflow-x:auto}.metric-group{min-width:140px}.metric-group+.metric-group{margin-top:0}}@media(max-width:620px){:root{--header-h:158px;--source-h:46px}.header-inner{grid-template-columns:1fr}.header-actions{grid-row:3}.source-switch>strong{display:none}.scope-options button{padding-inline:12px}.filters{position:static;margin-inline:-12px;padding-inline:12px}.content-shell{padding-inline:12px}.analysis-progress{grid-template-columns:1fr}.download-group{width:100%}.download-group a{flex:1;text-align:center}.cards{grid-template-columns:repeat(2,1fr)}.chart{height:330px}.chart-toolbar label{display:none}.chart-panel.focused{inset:4px}.chart-panel.focused .chart{height:calc(100dvh - 10px)!important}.notice{grid-template-columns:1fr}}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important;animation:none!important}}
.detail-image-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding-bottom:12px;margin-bottom:14px;border-bottom:1px solid var(--line)}.detail-image-head h3{margin:0 10px 0 0;font-size:16px}.detail-image-layer{color:var(--muted);font-variant-numeric:tabular-nums}.chart-panel>.chart-toolbar{position:static;min-height:46px;padding:6px 10px;justify-content:flex-end;border-bottom:1px solid var(--line);background:var(--soft)}.chart-panel.focused .chart{height:calc(100dvh - 70px)!important}
.about{position:relative;margin:0;padding:0;border:0}.about>summary{min-height:38px;display:grid;place-items:center;padding:7px 12px;border:1px solid var(--line);border-radius:8px;background:#fff;color:#294252;list-style:none}.about>summary::-webkit-details-marker{display:none}.about-panel{position:absolute;right:0;top:44px;width:260px;padding:14px;border:1px solid var(--line);border-radius:10px;background:#fff;box-shadow:0 16px 38px rgba(18,43,57,.18);color:var(--muted);font-size:12px}.about-panel strong{display:block;color:var(--text);font-size:15px}.about-panel p{margin:7px 0}.about-panel a{color:var(--accent);text-underline-offset:3px}
.selection-empty{padding:34px;text-align:center}.selection-empty h3{margin:0 0 6px;font-size:17px}.selection-empty p{margin:0;color:var(--muted)}
.chart,.chart.tall{height:430px}.device-layout{grid-template-columns:210px minmax(0,1fr);align-items:stretch}.device-layout>.metric-rail,.device-layout>.device-main{min-height:620px}.device-layout>.device-main>.chart-panel:not(.focused){height:100%}.device-layout>.device-main>.chart-panel:not(.focused)>.chart{height:calc(100% - 47px)}.device-table-panel{margin-top:14px}@media(max-width:900px){.device-layout{grid-template-columns:1fr;align-items:start}.device-layout>.metric-rail,.device-layout>.device-main{min-height:0}.device-layout>.device-main>.chart-panel:not(.focused){height:auto}.device-layout>.device-main>.chart-panel:not(.focused)>.chart{height:430px}}@media(max-width:620px){.chart,.chart.tall,.device-layout>.device-main>.chart-panel:not(.focused)>.chart{height:330px}}
</style></head><body>
<section id="direct-file" class="direct-file hidden"><div class="direct-card"><h1>需要先启动 Log Analysis</h1><p>当前页面是直接从 HTML 文件打开的,浏览器无法连接 Python 分析服务。</p><p>请关闭此页面,然后双击:</p><code class="launch-file">启动镀膜分析.bat</code></div></section>
<div id="app-shell"><header class="app-header"><div class="header-inner"><div class="brand"><span class="brand-mark">LA</span><div><h1>Log Analysis</h1><p>镀膜日志分析工作台</p></div></div><label class="path-field"><span>日志文件夹</span><input id="path" class="path" readonly placeholder="请选择日志文件夹"></label><div class="header-actions"><button id="choose" class="secondary">选择文件夹</button><button id="analyze" disabled>开始分析</button><details class="about"><summary>关于</summary><div class="about-panel"><strong>Log Analysis</strong><p id="app-version">正在读取版本…</p><p>单文件夹、本地只读分析。复核提示不代表质量判定。</p><a id="release-link" href="https://github.com/Reyppp/log-analysis/releases" target="_blank" rel="noreferrer">检查新版本</a></div></details></div><div id="service" class="service" data-state="checking"><span class="status-dot"></span><span id="status">正在连接本地服务</span></div></div></header>
<section id="source-switch" class="source-switch hidden" aria-label="日志分析范围"><strong>分析范围</strong><div id="scope-options" class="scope-options" role="tablist"></div><span id="source-note" class="source-note"></span></section>
<div class="workspace"><aside id="sidebar" class="sidebar hidden"><p class="sidebar-label">分析模块</p><nav id="modules" class="module-nav" aria-label="分析模块"></nav></aside><div class="content-shell"><section id="analysis-progress" class="analysis-progress hidden" aria-live="polite"><strong id="progress-label">准备分析</strong><progress id="progress-bar"></progress><span id="progress-time" class="progress-time">00:00</span></section><div id="error" role="alert"></div><section id="empty" class="empty-state"><div><h2>选择日志文件夹开始分析</h2><p>程序只读扫描日志,不会修改任何原始文件。</p><div id="skeleton" class="skeleton hidden"><span></span><span></span><span></span></div></div></section>
<section id="controls" class="filters hidden" aria-label="分析筛选"><label class="filter-field layer-filter"><span>起始层</span><input id="layer-min" type="number"></label><label class="filter-field layer-filter"><span>结束层</span><input id="layer-max" type="number"></label><div class="filter-field material-filter"><span>材料</span><div id="material" class="segments" role="group"></div></div><div class="filter-field method-filter"><span>监控方式</span><div id="method" class="segments" role="group"></div></div><label class="filter-field threshold-filter"><span>异常阈值</span><input id="threshold" type="number" min="2.5" max="5" step=".1" value="3.5"></label><label class="filter-field machine-filter hidden"><span>区段</span><select id="machine-session"></select></label><span class="filter-spacer"></span><div class="download-group"><a id="download-summary" class="button secondary" href="/api/export?kind=summary">下载逐层汇总</a><a id="download-anomalies" class="button secondary" href="/api/export?kind=anomalies">下载异常清单</a></div></section><main id="page" aria-live="polite"></main></div></div></div>
<script>
const SESSION_TOKEN=new URLSearchParams(location.search).get('token')||'';const withToken=url=>{if(!SESSION_TOKEN)return url;const join=url.includes('?')?'&':'?';return `${url}${join}token=${encodeURIComponent(SESSION_TOKEN)}`};
const FIELD_META={layer:{label:'层号',digits:0},material:{label:'材料'},method:{label:'监控方式'},phy_thick:{label:'物理厚度',unit:'nm',digits:2},recipe_rate:{label:'理论速率',unit:'nm/s',digits:3},planned_time:{label:'理论时间',unit:'s',digits:1},start_t:{label:'理论 Start T',unit:'%',digits:2,percent:true},end_t:{label:'理论 End T',unit:'%',digits:2,percent:true},recipe_extreme:{label:'理论极值状态'},final_meas:{label:'终点 Meas',unit:'%',digits:2,percent:true},final_calc:{label:'终点动态 Calc',unit:'%',digits:2,percent:true},fit_end_residual:{label:'终点 Meas−动态 Calc',unit:'%',digits:2,percent:true},recipe_end_offset:{label:'Meas−理论 End T',unit:'%',digits:2,percent:true},fit_mae:{label:'全层 MAE',unit:'%',digits:2,percent:true},fit_rmse:{label:'全层 RMSE',unit:'%',digits:2,percent:true},fit_p95_abs:{label:'P95 绝对残差',unit:'%',digits:2,percent:true},fit_max_abs:{label:'最大绝对残差',unit:'%',digits:2,percent:true},actual_time:{label:'实际镀膜时间',unit:'s',digits:1},time_delta:{label:'实际−理论时间',unit:'s',digits:1},actual_rate:{label:'实际速率',unit:'nm/s',digits:3},rate_delta:{label:'实际−理论速率',unit:'nm/s',digits:3},actual_extreme:{label:'实际极值状态'},signal_median:{label:'信号中位数',unit:'%',digits:2,percent:true},signal_std:{label:'信号标准差',unit:'%',digits:2,percent:true},signal_cv:{label:'信号变异系数',unit:'%',digits:2,percent:true},optical_rows:{label:'光学采样数',digits:0},substrate_shutter_mismatch:{label:'基片挡板不一致次数',digits:0},material_shutter_mismatch:{label:'材料挡板不一致次数',digits:0},shutter_mismatch:{label:'挡板不一致次数',digits:0},layer_start:{label:'层开始时间'},layer_end:{label:'层结束时间'},machine_span:{label:'设备记录跨度',unit:'s',digits:1},machine_rows:{label:'设备采样数',digits:0},severity:{label:'级别'},scope:{label:'范围'},issue:{label:'问题'},detail:{label:'详情'},metric:{label:'指标'},value:{label:'数值',digits:4},change:{label:'相邻层变化',digits:4},group:{label:'分组基准'},baseline:{label:'分组中位数',digits:4},mad:{label:'MAD',digits:4},robust_z:{label:'稳健 Z 分数',digits:2},level:{label:'复核等级'},reason:{label:'异常依据'},source:{label:'数据源'},field:{label:'字段'},count:{label:'样本数',digits:0},mean:{label:'均值',digits:4},min:{label:'最小值',digits:4},max:{label:'最大值',digits:4},SamTime:{label:'采样时间',unit:'s'},thickness:{label:'理论厚度',unit:'nm'},calculated_t:{label:'理论透过率',unit:'%',percent:true}};
const DEVICE_BASE={power:{label:'有效功率',unit:'kW',digits:3},current:{label:'有效电流',unit:'A',digits:3},voltage:{label:'有效电压',unit:'V',digits:2},vacuum:{label:'腔体真空',unit:'日志原始值',digits:4},chamber_temp:{label:'腔体温度',unit:'°C',digits:2},water_in:{label:'进水温度',unit:'°C',digits:2},water_out:{label:'出水温度',unit:'°C',digits:2},motor:{label:'转速',unit:'rpm',digits:1},o2:{label:'O₂',unit:'日志原始值',digits:3},ar:{label:'Ar',unit:'日志原始值',digits:3}},SUFFIX={mean:'均值',std:'标准差',min:'最小值',max:'最大值',range:'范围'};for(const [base,b] of Object.entries(DEVICE_BASE))for(const [suffix,s] of Object.entries(SUFFIX))FIELD_META[`${base}_${suffix}`]={label:`${b.label}${s}`,unit:b.unit,digits:b.digits};
Object.assign(FIELD_META,{session_id:{label:'区段编号',digits:0},session_name:{label:'区段'},start:{label:'开始时间'},end:{label:'结束时间'},elapsed_seconds:{label:'记录跨度',unit:'s',digits:0},target_seconds:{label:'靶材工作时间',unit:'s',digits:0},is_default:{label:'默认区段'},rows:{label:'采样数',digits:0},category:{label:'类别'},time:{label:'开始时间'},end_time:{label:'结束时间'},motor_source:{label:'转速来源'},target_power_seconds:{label:'靶材工作时间',unit:'s',digits:0},target_time_delta:{label:'靶材工作时间 - 实际镀膜时间',unit:'s',digits:0},machine_segment_id:{label:'工控工作时段',digits:0},alignment:{label:'关联状态'},confidence:{label:'关联状态'},coverage:{label:'关联覆盖'},planned_layers:{label:'理论层数',digits:0},completed_layers:{label:'已完成层数',digits:0},matched_segments:{label:'已关联层数',digits:0},matched_layer_start:{label:'关联起始层',digits:0},matched_layer_end:{label:'关联结束层',digits:0},matched_layer_range:{label:'关联层范围'},ignored_machine_edge_segments:{label:'忽略的首尾工控时段'},candidate_ranges:{label:'候选层范围'},candidate_count:{label:'候选序列数',digits:0},duration_correlation:{label:'持续时间相关系数',digits:5},time_offset_seconds:{label:'时钟偏移',unit:'s',digits:1},offset_std_seconds:{label:'偏移标准差',unit:'s',digits:2},sample_gap:{label:'采样时间缺口',unit:'s',digits:0},simultaneous_power:{label:'H/L 同时工作',unit:'s',digits:0},'H Power':{label:'H 材料功率',unit:'kW',digits:3},'L Power':{label:'L 材料功率',unit:'kW',digits:3},'H Current':{label:'H 材料电流',unit:'A',digits:3},'L Current':{label:'L 材料电流',unit:'A',digits:3},'H Voltage':{label:'H 材料电压',unit:'V',digits:2},'L Voltage':{label:'L 材料电压',unit:'V',digits:2},'Motor Speed':{label:'转速',unit:'rpm',digits:1},'Chammber Vacuum':{label:'腔体真空',unit:'日志原始值',digits:6},'Pipe Vacuum':{label:'管道真空',unit:'日志原始值',digits:6},'Chammber Temperture':{label:'腔体温度',unit:'°C',digits:1},'Water-In':{label:'进水温度',unit:'°C',digits:2},'Water-Out':{label:'出水温度',unit:'°C',digits:2},'Gas-O2(1)':{label:'O₂ 通道 1',unit:'日志原始值',digits:3},'Gas-O2(4)':{label:'O₂ 通道 4',unit:'日志原始值',digits:3},'Gas-Ar(2)':{label:'Ar 通道 2',unit:'日志原始值',digits:3},'Gas-Ar(3)':{label:'Ar 通道 3',unit:'日志原始值',digits:3}});for(const suffix of Object.keys(SUFFIX))FIELD_META[`monitor_motor_${suffix}`]={label:`监控log转速参考值${SUFFIX[suffix]}`,unit:'rpm',digits:1};
const DEVICE_GROUPS={电气:['power_mean','current_mean','voltage_mean'],环境:['vacuum_mean'],温度:['chamber_temp_mean','water_in_mean','water_out_mean'],转速:['motor_mean'],气体:['o2_mean','ar_mean']},COLORS=['#075e91','#c5691a','#6d68a8','#267451'];
const NAV={combined:[['combined-overview','总览'],['combined-device','设备']],machine:[['machine-overview','总览'],['machine-trend','设备']],monitor:[['overview','总览'],['theory','理论趋势'],['optical','光学'],['device','设备'],['anomaly','异常复核']]};
const MACHINE_METRICS={power:{label:'功率',unit:'kW',group:'电气',api:'power_mean',columns:['H Power','L Power']},current:{label:'电流',unit:'A',group:'电气',api:'current_mean',columns:['H Current','L Current']},voltage:{label:'电压',unit:'V',group:'电气',api:'voltage_mean',columns:['H Voltage','L Voltage']},'Chammber Vacuum':{label:'腔体真空',unit:'日志原始值',group:'真空',api:'vacuum_mean',columns:['Chammber Vacuum']},'Pipe Vacuum':{label:'管道真空',unit:'日志原始值',group:'真空',api:'pipe_vacuum',columns:['Pipe Vacuum']},'Chammber Temperture':{label:'腔体温度',unit:'°C',group:'温度',api:'chamber_temp_mean',columns:['Chammber Temperture']},'Water-In':{label:'进水温度',unit:'°C',group:'温度',api:'water_in_mean',columns:['Water-In']},'Water-Out':{label:'出水温度',unit:'°C',group:'温度',api:'water_out_mean',columns:['Water-Out']},'Motor Speed':{label:'转速',unit:'rpm',group:'转速',api:'motor_mean',columns:['Motor Speed']},o2:{label:'O₂',unit:'日志原始值',group:'气体',api:'o2_mean',columns:['Gas-O2(1)','Gas-O2(4)']},ar:{label:'Ar',unit:'日志原始值',group:'气体',api:'ar_mean',columns:['Gas-Ar(2)','Gas-Ar(3)']}};
const viewState={scope:'combined',activeByScope:{combined:'combined-overview',machine:'machine-overview',monitor:'overview'},active:'combined-overview',selectedLayer:1,opticalResidual:'fit_end_residual',opticalSignal:'signal_median',deviceMetric:'power_mean',anomalyMetric:'',combinedDeviceAxis:'layer',machineMetric:'power',machineSession:'',focused:null,focusReturn:null,scrollY:0};let payload=null,progressTimer=null,progressHideTimer=null;
function materialColor(material){if(material==='H')return COLORS[0];if(material==='L')return COLORS[1];const order=[...new Set((payload?.layer_summary||[]).map(row=>row.material).filter(Boolean))],index=order.indexOf(material);return COLORS[(index>=0?index:2)%COLORS.length]}
const TRACE_COLORS=new Proxy({}, {get:(_,channel)=>{if(/^H\s/.test(channel))return materialColor('H');if(/^L\s/.test(channel))return materialColor('L');if(channel==='Gas-O2(1)')return '#075e91';if(channel==='Gas-O2(4)')return '#4f9cc3';if(channel==='Gas-Ar(2)')return '#c5691a';if(channel==='Gas-Ar(3)')return '#e09b59'}});
const $=id=>document.getElementById(id),num=v=>v==null||v===''?null:Number(v),esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])),meta=key=>FIELD_META[key]||{label:`未命名字段(${key})`,digits:4},label=key=>{const m=meta(key);return m.unit?`${m.label}(${m.unit})`:m.label};
const fmtSec=v=>{if(v==null)return '暂无';const n=Math.round(v),h=Math.floor(n/3600),m=Math.floor(n%3600/60),s=n%60;return [h,m,s].map(x=>String(x).padStart(2,'0')).join(':')};function formatValue(key,v){if(v==null||v==='')return '暂无';if(typeof v==='boolean')return v?'是':'否';if(['metric','target','driver','field'].includes(key)&&typeof v==='string')return meta(v).label;if(typeof v!=='number'||!Number.isFinite(v))return String(v);const m=meta(key);return m.percent?`${(v*100).toFixed(m.digits??2)}%`:v.toLocaleString('zh-CN',{maximumFractionDigits:m.digits??4})+(m.unit?` ${m.unit}`:'')}function cellValue(row,key){return ['value','change','baseline','mad'].includes(key)&&row.metric?formatValue(row.metric,row[key]):formatValue(key,row[key])}
class ApiError extends Error{constructor(message,code='request_failed',recovery='请稍后重试。'){super(message);this.code=code;this.recovery=recovery}}function setService(state,text){$('service').dataset.state=state;$('status').textContent=text}function setBusy(kind,on,text){if(kind==='choose')$('choose').disabled=on;else{$('choose').disabled=$('analyze').disabled=on;$('threshold').disabled=on}$('skeleton').classList.toggle('hidden',!(on&&kind==='analyze'));setService(on?'busy':'online',text||'本地服务已连接')}async function api(url,options={}){let r;const headers={...(options.headers||{})};if(SESSION_TOKEN)headers['X-Log-Analysis-Token']=SESSION_TOKEN;try{r=await fetch(url,{...options,headers})}catch(error){if(error?.name==='AbortError')throw error;throw new ApiError('无法连接本地分析服务。','service_unavailable','请重新启动 Log Analysis,然后点击“重新连接”。')}let d;try{d=await r.json()}catch{throw new ApiError('本地服务返回了无法识别的响应。','invalid_response','请重新启动分析工具后重试。')}if(!r.ok)throw new ApiError(d.error||'请求未完成。',d.code||'request_failed',d.recovery||'请检查炉次文件夹后重试。');return d}
function resetAnalysisView(){if(viewState.focused)toggleChartFocus(viewState.focused.querySelector('.focus-chart'));payload=null;Object.assign(viewState,{scope:'combined',activeByScope:{combined:'combined-overview',machine:'machine-overview',monitor:'overview'},active:'combined-overview',selectedLayer:1,opticalResidual:'fit_end_residual',opticalSignal:'signal_median',deviceMetric:'power_mean',anomalyMetric:'',combinedDeviceAxis:'layer',machineMetric:'power',machineSession:''});$('controls').classList.add('hidden');$('source-switch').classList.add('hidden');$('sidebar').classList.add('hidden');$('modules').replaceChildren();$('scope-options').replaceChildren();$('layer-min').value=$('layer-max').value='';$('threshold').value='3.5';$('material').replaceChildren();$('method').replaceChildren();$('page').replaceChildren();$('empty').classList.remove('hidden');$('empty').querySelector('h2').textContent='日志文件夹已选择,请开始分析';$('empty').querySelector('p').textContent='已清空上一份分析结果。原始日志保持只读。'}
function availableScopes(){const s=payload?.sources||{};return s.combined?['combined','monitor','machine']:s.machine?['machine']:['monitor']}
function alignmentLabel(alignment=payload?.alignment||{}){if(alignment.status!=='matched')return '关联失败';const pending=alignment.confidence==='中可信',partial=alignment.coverage==='partial_completed';if(partial)return pending?'已关联(部分区间,待复核)':'已关联(部分区间)';return pending?'已关联(待复核)':'已关联'}
function renderSourceSwitch(){const names={combined:'综合',machine:'工控log',monitor:'监控log'},available=availableScopes();$('scope-options').innerHTML=available.map(scope=>`<button role="tab" aria-selected="${scope===viewState.scope}" data-scope="${scope}" class="${scope===viewState.scope?'active':''}">${names[scope]}</button>`).join('');const s=payload.sources;$('source-note').textContent=s.combined?`${alignmentLabel()} ${payload.alignment.matched_segments||0} 个镀膜层`:s.machine?'当前仅包含工控log':'当前仅包含监控log';$('source-switch').classList.remove('hidden')}
function renderNavigation(){const items=NAV[viewState.scope]||[];if(!items.some(([key])=>key===viewState.active))viewState.active=viewState.activeByScope[viewState.scope]||items[0]?.[0];$('modules').innerHTML=items.map(([key,name])=>`<button data-page="${key}" class="${key===viewState.active?'active':''}">${name}</button>`).join('')}
function setScope(scope){if(!availableScopes().includes(scope))return;viewState.activeByScope[viewState.scope]=viewState.active;viewState.scope=scope;viewState.active=viewState.activeByScope[scope]||NAV[scope][0][0];renderSourceSwitch();renderNavigation();updateToolbar();renderActivePage()}
function showProgress(p){const total=Number(p.total)||0,current=Number(p.current)||0,bar=$('progress-bar');$('analysis-progress').classList.remove('hidden');$('progress-label').textContent=total?`${p.phase||'分析'} · ${current}/${total}`:p.phase||'准备分析';$('progress-time').textContent=`${Math.floor((p.elapsed||0)/60).toString().padStart(2,'0')}:${Math.floor((p.elapsed||0)%60).toString().padStart(2,'0')}`;if(total){bar.max=total;bar.value=current}else{bar.removeAttribute('value')}setService('busy',$('progress-label').textContent)}
async function pollProgress(){try{showProgress(await api('/api/progress'))}catch{}}
function startProgress(){clearTimeout(progressHideTimer);$('analysis-progress').classList.remove('hidden');showProgress({phase:'准备分析',current:0,total:0,elapsed:0});clearInterval(progressTimer);progressTimer=setInterval(pollProgress,400)}function stopProgress(success=false){clearInterval(progressTimer);progressTimer=null;if(success){$('progress-label').textContent='分析完成';progressHideTimer=setTimeout(()=>{if(!progressTimer)$('analysis-progress').classList.add('hidden')},900)}else $('analysis-progress').classList.add('hidden')}
async function choose(){clearError();try{setBusy('choose',true,'等待选择日志文件夹');const d=window.pywebview?.api?.choose_folder?await window.pywebview.api.choose_folder():await api('/api/choose-folder',{method:'POST'});if(d.error)throw new ApiError(d.error,d.code,d.recovery);if(d.path)$('path').value=d.path;if(d.changed)resetAnalysisView();$('analyze').disabled=!$('path').value;setService('online',d.cancelled?'已取消,保留原路径':d.changed?'已选择新日志文件夹,旧结果已清空':d.path?'日志文件夹已选择':'请选择日志文件夹')}catch(e){showError(e)}finally{$('choose').disabled=false}}
async function analyze(){if(!$('path').value)return showError(new ApiError('请先选择日志文件夹。','missing_folder','点击“选择文件夹”。'));clearError();startProgress();try{setBusy('analyze',true,'正在识别并汇总日志');payload=await api('/api/analyze',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({path:$('path').value,threshold:num($('threshold').value)||3.5})});setupFilters();const scopes=availableScopes();viewState.scope=scopes.includes('combined')?'combined':scopes[0];viewState.active=viewState.activeByScope[viewState.scope];renderSourceSwitch();renderNavigation();updateToolbar();$('empty').classList.add('hidden');renderActivePage();stopProgress(true);setService('online','分析完成')}catch(e){stopProgress();showError(e);if(e.code!=='service_unavailable')setService('online','分析未完成')}finally{$('choose').disabled=$('analyze').disabled=$('threshold').disabled=false;$('skeleton').classList.add('hidden')}}function showError(e){const x=e instanceof Error?e:new ApiError(String(e)),retry=x.code==='service_unavailable'?'<button class="secondary" id="retry-service">重新连接</button>':'';$('error').innerHTML=`<div class="notice"><div><strong>${esc(x.message)}</strong><p>${esc(x.recovery||'请重试。')}</p></div>${retry}</div>`;if(retry)$('retry-service').onclick=connect;if(x.code==='service_unavailable')setService('offline','本地服务未连接')}function clearError(){$('error').innerHTML=''}
async function saveExport(kind,event){if(window.pywebview?.api){event.preventDefault();const result=await window.pywebview.api.save_export(kind,num($('threshold').value)||3.5);if(!result.ok)showError(new ApiError(result.error));else if(!result.cancelled)setService('online',`已保存到 ${result.path}`);return}event.currentTarget.href=withToken(`/api/export?kind=${encodeURIComponent(kind)}&threshold=${encodeURIComponent($('threshold').value)}`)}function segmentOptions(id,name,values){$(id).innerHTML=values.map(x=>`<label><input type="checkbox" name="${name}" value="${esc(x)}" checked><span>${esc(x)}</span></label>`).join('')}function checked(name){return [...document.querySelectorAll(`input[name="${name}"]:checked`)].map(x=>x.value)}async function refreshAnomalies(){if(!payload)return;clearError();$('threshold').disabled=true;setService('busy','正在更新异常阈值');try{const d=await api('/api/anomalies?threshold='+encodeURIComponent(num($('threshold').value)||3.5));payload.anomalies=d.anomalies;if(['overview','anomaly'].includes(viewState.active))renderActivePage();setService('online','异常阈值已更新')}catch(e){showError(e);if(e.code!=='service_unavailable')setService('online','异常阈值更新失败')}finally{$('threshold').disabled=false}}
function linkedSessionIds(){return new Set((payload?.machine_segments||[]).filter(x=>x.layer!=null).map(x=>Number(x.session_id)))}function sessionName(id,linked=linkedSessionIds()){return `${linked.has(Number(id))?'镀膜区段':'区段'} ${id}`}
function setupFilters(){const s=payload.monitor_summary||payload.summary||[],layers=s.map(x=>x.layer).filter(Number.isFinite);if(layers.length){$('layer-min').value=Math.min(...layers);$('layer-max').value=Math.max(...layers);viewState.selectedLayer=layers[0]}const monitorMaterials=[...new Set(s.map(x=>x.material).filter(Boolean))],machineMaterials=[...new Set((payload.machine_segments||[]).map(x=>x.material).filter(x=>x==='H'||x==='L'))];segmentOptions('material','material',(monitorMaterials.length?monitorMaterials:machineMaterials).sort());segmentOptions('method','method',[...new Set(s.map(x=>x.method).filter(Boolean))].sort());const sessions=payload.machine_sessions||[],defaultSession=payload.machine_summary?.default_session_id,linked=linkedSessionIds();viewState.machineSession=String(defaultSession||'all');$('machine-session').innerHTML=sessions.map(x=>`<option value="${x.session_id}" ${x.session_id===defaultSession?'selected':''}>${sessionName(x.session_id,linked)} | ${new Date(x.start).toLocaleString('zh-CN',{hour12:false})} | ${fmtSec(x.elapsed_seconds)}</option>`).join('')+'<option value="all">全部时间</option>';$('machine-session').value=viewState.machineSession;$('controls').classList.remove('hidden');$('sidebar').classList.remove('hidden');for(const el of [$('layer-min'),$('layer-max'),...document.querySelectorAll('.segments input')])el.onchange=renderActivePage;$('threshold').onchange=refreshAnomalies;$('machine-session').onchange=e=>{viewState.machineSession=e.target.value;renderActivePage()}}
function updateToolbar(){const aligned=payload?.alignment?.status==='matched',machine=viewState.scope==='machine';document.querySelectorAll('.layer-filter,.method-filter').forEach(x=>x.classList.toggle('hidden',machine&&!aligned));document.querySelectorAll('.material-filter').forEach(x=>x.classList.remove('hidden'));document.querySelectorAll('.threshold-filter').forEach(x=>x.classList.toggle('hidden',viewState.scope!=='monitor'));document.querySelectorAll('.machine-filter').forEach(x=>x.classList.toggle('hidden',!machine));const config=viewState.scope==='monitor'?[['monitor_summary','下载逐层汇总'],['monitor_anomalies','下载异常清单']]:[null,null];[$('download-summary'),$('download-anomalies')].forEach((link,index)=>{const item=config[index];link.classList.toggle('hidden',!item);if(!item)return;const [kind,text]=item;link.textContent=text;link.href=withToken(`/api/export?kind=${kind}`);link.onclick=e=>saveExport(kind,e)})}
function filteredLayerRows(source=payload?.summary||[]){const lo=num($('layer-min').value),hi=num($('layer-max').value),materials=checked('material'),methods=checked('method');return source.filter(row=>row.layer>=lo&&row.layer<=hi&&materials.includes(row.material)&&methods.includes(row.method))}
function rows(){if(!payload||viewState.scope==='machine')return [];return filteredLayerRows(viewState.scope==='monitor'?(payload.monitor_summary||payload.summary):payload.summary)}
function filteredMachineSegments(){if(!payload)return [];const materials=checked('material'),session=viewState.machineSession||'all',aligned=payload.alignment?.status==='matched',layerSet=aligned?new Set(filteredLayerRows(payload.summary||[]).map(row=>Number(row.layer))):null;return (payload.machine_segments||[]).filter(row=>(session==='all'||String(row.session_id)===String(session))&&materials.some(material=>String(row.material||'').includes(material))&&(!aligned||(row.layer!=null&&layerSet.has(Number(row.layer)))))}
const sumField=(records,key)=>records.reduce((sum,row)=>sum+(Number(row[key])||0),0),selectionEmpty=()=>'<section class="panel selection-empty"><h3>当前筛选无数据</h3><p>请调整层范围、材料、监控方式或区段。</p></section>';
const pageHeader=(t,d)=>`<div class="page-head"><h2>${esc(t)}</h2><p>${esc(d)}</p></div>`,chartPanel=(id,title,{className='',controls=''}={})=>`<section class="chart-panel ${className}" data-chart-title="${esc(title)}"><div class="chart-toolbar">${controls}<button class="secondary focus-chart" onclick="toggleChartFocus(this)" aria-label="放大查看:${esc(title)}">放大查看</button></div><div id="${id}" class="chart${className.includes('tall')?' tall':''}"></div></section>`;
function axis(key,title){const m=key?meta(key):{};return {title:{text:title||label(key),standoff:10},automargin:true,gridcolor:'#e7edf2',zerolinecolor:'#c8d3dc',linecolor:'#c8d3dc',tickfont:{color:'#5b6d7c',size:11},tickformat:m.percent?'.2%':undefined,exponentformat:'power',showexponent:'last'}}function plot(id,traces,{title,xTitle='层号',xMetric=null,yTitle=null,yMetric=null,hovermode='x unified',margin={t:64,l:72,r:28,b:58},uirevision=id,modebar={},...rest}={}){if(!$(id))return Promise.resolve();return Plotly.react($(id),traces,{title:{text:title||'',x:.025,xanchor:'left',y:.95,yref:'container',yanchor:'top',font:{size:16}},margin,paper_bgcolor:'#fff',plot_bgcolor:'#fff',font:{family:'Segoe UI, Microsoft YaHei, Arial',color:'#243746',size:12},hoverlabel:{bgcolor:'#19394b',font:{color:'#fff'}},hovermode,colorway:COLORS,xaxis:axis(xMetric,xTitle),yaxis:axis(yMetric,yTitle),legend:{orientation:'h',x:0,y:1.04,yanchor:'bottom'},uirevision,modebar:{uirevision,...modebar},...rest},{responsive:true,displaylogo:false,modeBarButtonsToRemove:['lasso2d','select2d'],toImageButtonOptions:{filename:(title||'镀膜图表').replace(/[\\/:*?"<>|]/g,'_'),scale:2}})}function deviceMenuName(key){return {power_mean:'功率',current_mean:'电流',voltage_mean:'电压'}[key]||(key==='motor_mean'?'转速':meta(key).label.replace('均值',''))}function deviceDataName(key,detail){const base=deviceMenuName(key);return ['power_mean','current_mean','voltage_mean'].includes(key)?(detail==='mean'?`有效${base}均值`:`${base}值`):(detail==='mean'?`${base}均值`:`${base}值`)}function deviceTraceColor(item,index){return TRACE_COLORS[item.channel]||(item.source==='monitor'?'#6d68a8':'#075e91')||COLORS[index%COLORS.length]}function byMaterial(r,key,smooth=false){return [...new Set(r.map(x=>x.material))].map((m,i)=>lineTrace(r.filter(x=>x.material===m),key,`${m} 材料`,COLORS[i],smooth))}function table(records,columns){if(!records.length)return '<p class="muted">没有可显示的数据。</p>';return '<div class="table-wrap"><table><thead><tr>'+columns.map(c=>`<th><abbr title="内部字段:${esc(c)}">${esc(label(c))}</abbr></th>`).join('')+'</tr></thead><tbody>'+records.slice(0,120).map(r=>'<tr>'+columns.map(c=>`<td>${esc(cellValue(r,c))}</td>`).join('')+'</tr>').join('')+'</tbody></table></div>'}
function toggleChartFocus(button){const panel=button.closest('.chart-panel'),closing=panel.classList.contains('focused'),resize=()=>{const chart=panel.querySelector('.chart');Promise.resolve(Plotly.Plots.resize(chart)).then(()=>requestAnimationFrame(()=>refreshLayerAxis(chart)))};if(closing){panel.classList.remove('focused');panel.removeAttribute('role');panel.removeAttribute('aria-modal');panel.removeAttribute('aria-label');button.textContent='放大查看';document.body.classList.remove('chart-focus-open');document.querySelector('.chart-backdrop')?.remove();requestAnimationFrame(()=>{resize();window.scrollTo(0,viewState.scrollY);viewState.focusReturn?.focus()});viewState.focused=null;return}if(viewState.focused)toggleChartFocus(viewState.focused.querySelector('.focus-chart'));viewState.scrollY=scrollY;viewState.focusReturn=document.activeElement;viewState.focused=panel;const b=document.createElement('div');b.className='chart-backdrop';b.onclick=()=>toggleChartFocus(button);document.body.appendChild(b);panel.classList.add('focused');panel.setAttribute('role','dialog');panel.setAttribute('aria-modal','true');panel.setAttribute('aria-label',`放大图表:${panel.dataset.chartTitle}`);button.textContent='退出放大';document.body.classList.add('chart-focus-open');button.focus();requestAnimationFrame(resize)}document.addEventListener('keydown',e=>{if(e.key==='Escape'&&viewState.focused)toggleChartFocus(viewState.focused.querySelector('.focus-chart'))});window.addEventListener('resize',()=>requestAnimationFrame(()=>document.querySelectorAll('.chart').forEach(refreshLayerAxis)));
function renderOverview(r){const b=payload.batch,layerSet=new Set(r.map(row=>row.layer)),selectedAnomalies=payload.anomalies.filter(row=>layerSet.has(row.layer)),priority=new Set(selectedAnomalies.filter(row=>row.level==='重点复核').map(row=>row.layer)),h=r.filter(row=>row.material==='H').length,l=r.filter(row=>row.material==='L').length;$('page').innerHTML=pageHeader('总览','当前筛选统计与整炉状态')+`<div class="cards"><div class="card">炉号(整炉)<b>${esc(b.lot_id)}</b></div><div class="card">当前层数<b>${r.length}</b>H ${h} / L ${l}</div><div class="card">完成状态(整炉)<b>${b.finished?'已完成':'需核验'}</b></div><div class="card">当前累计镀膜<b>${fmtSec(sumField(r,'actual_time'))}</b>理论 ${fmtSec(sumField(r,'planned_time'))}</div><div class="card">当前重点复核层<b>${priority.size}</b></div><div class="card">数据质量(整炉)<b>${b.quality_errors} 错误</b>${b.quality_warnings} 警告</div></div>${chartPanel('overview-chart','原始理论终点偏移概览',{className:'tall'})}<h3 class="section-title">优先复核</h3>${table(selectedAnomalies,['layer','material','method','metric','value','level','robust_z','reason'])}<details><summary>整炉数据质量与忽略字段</summary>${table(payload.quality,['severity','scope','layer','issue','detail'])}<p>恒定字段(默认不绘图)</p>${table(payload.constants,['source','field','value','reason'])}</details>`;plot('overview-chart',byMaterial(r,'recipe_end_offset'),{title:'原始理论终点偏移概览',yMetric:'recipe_end_offset'})}
function lineTrace(r,key,name,color,smooth=false){const yValue=meta(key).percent?'%{y:.2%}':'%{y:.4g}';return {x:r.map(x=>x.layer),y:r.map(x=>x[key]),customdata:r.map(x=>[x.material,x.method]),name:name||label(key),mode:'lines+markers',line:{shape:smooth?'spline':'linear',smoothing:smooth?.45:0,color,width:2},marker:{size:5,color,line:{width:1,color:'#fff'}},hovertemplate:`${label(key)} ${yValue} · %{customdata[0]}材料 · %{customdata[1]}<extra>%{fullData.name}</extra>`}}
const sortMaterials=values=>[...new Set(values.filter(Boolean))].sort((a,b)=>(a==='H'?0:a==='L'?1:2)-(b==='H'?0:b==='L'?1:2)||String(a).localeCompare(String(b),'zh-CN'));
byMaterial=(r,key,smooth=false)=>sortMaterials(r.map(x=>x.material)).map(material=>lineTrace(r.filter(x=>x.material===material),key,`${material} 材料`,materialColor(material),smooth));
function renderTheory(r){
$('page').innerHTML=pageHeader('理论趋势','逐层对比理论设定、实际结果与材料分布')+`<div class="stack">${chartPanel('band','材料与监控方式',{className:'tall'})}<div class="grid">${chartPanel('thick','物理厚度')}${chartPanel('rate','理论与实际速率')}${chartPanel('time','理论与实际时间')}${chartPanel('delta','实际−理论时间')}</div></div>`;
plot('band',[{x:r.map(x=>x.layer),y:r.map(x=>x.material),customdata:r.map(x=>x.method),mode:'markers',marker:{size:9,color:r.map(x=>materialColor(x.material))},name:'材料 / 监控方式',hovertemplate:'%{y}材料 · %{customdata}<extra>%{fullData.name}</extra>'}],{title:'材料与监控方式',yTitle:'材料'});
plot('thick',byMaterial(r,'phy_thick'),{title:'物理厚度',yMetric:'phy_thick'});
plot('rate',[lineTrace(r,'recipe_rate','理论速率',COLORS[0]),lineTrace(r,'actual_rate','实际速率',COLORS[1])],{title:'理论与实际速率',yTitle:'速率(nm/s)'});
plot('time',[lineTrace(r,'planned_time','理论时间',COLORS[0]),lineTrace(r,'actual_time','实际时间',COLORS[1])],{title:'理论与实际时间',yTitle:'时间(s)'});
plot('delta',byMaterial(r,'time_delta'),{title:'实际−理论时间',yMetric:'time_delta'});
}
function renderOptical(r){
const layers=r.map(x=>x.layer);
$('page').innerHTML=pageHeader('光学分析','终点偏差、拟合残差与单层完整曲线');
if(!layers.length)return $('page').insertAdjacentHTML('beforeend','<p class="muted">当前筛选无层数据。</p>');
if(!layers.includes(viewState.selectedLayer))viewState.selectedLayer=layers[0];
const rs=['fit_end_residual','recipe_end_offset','fit_mae','fit_p95_abs','fit_max_abs'],ss=['signal_median','signal_std','signal_cv'];
if(!rs.includes(viewState.opticalResidual))viewState.opticalResidual=rs[0];
const rc=`<label>指标 <select id="residual">${rs.map(x=>`<option value="${x}" ${x===viewState.opticalResidual?'selected':''}>${meta(x).label}</option>`).join('')}</select></label>`;
const sc=`<label>指标 <select id="signal">${ss.map(x=>`<option value="${x}" ${x===viewState.opticalSignal?'selected':''}>${meta(x).label}</option>`).join('')}</select></label>`;
$('page').insertAdjacentHTML('beforeend',`<div class="stack">${chartPanel('endpoint','终点透过率趋势',{className:'tall'})}<div class="grid">${chartPanel('residual-chart',meta(viewState.opticalResidual).label,{controls:rc})}${chartPanel('signal-chart',meta(viewState.opticalSignal).label,{controls:sc})}</div><div class="detail-head"><h3>单层详情</h3><button class="secondary" id="prev">前一层</button><label>层号 <select id="layer">${layers.map(x=>`<option ${x===viewState.selectedLayer?'selected':''}>${x}</option>`).join('')}</select></label><button class="secondary" id="next">后一层</button></div><div class="optical-detail-grid">${chartPanel('detail-meas',`${viewState.selectedLayer}层实测与动态计算`,{className:'tall'})}${chartPanel('detail-calc','理论厚度与透过率曲线',{className:'tall'})}</div><section id="detail-image-slot" class="panel image-panel"><div class="detail-image-head"><h3>原始截图</h3><button class="secondary" id="image-prev">前一层</button><span id="image-layer" class="detail-image-layer">${viewState.selectedLayer}层</span><button class="secondary" id="image-next">后一层</button></div><div id="detail-image-content"></div></section></div>`);
plot('endpoint',[lineTrace(r,'final_meas','终点 Meas',COLORS[0]),lineTrace(r,'final_calc','终点动态 Calc',COLORS[1]),lineTrace(r,'end_t','理论 End T',COLORS[2])],{title:'终点透过率趋势',yTitle:'透过率(%)',yMetric:'final_meas'});
drawOpt('residual-chart',r,viewState.opticalResidual);drawOpt('signal-chart',r,viewState.opticalSignal);
$('residual').onchange=e=>{viewState.opticalResidual=e.target.value;drawOpt('residual-chart',r,e.target.value)};
$('signal').onchange=e=>{viewState.opticalSignal=e.target.value;drawOpt('signal-chart',r,e.target.value)};
$('layer').onchange=e=>{viewState.selectedLayer=Number(e.target.value);loadDetail()};$('prev').onclick=()=>moveLayer(layers,-1);$('next').onclick=()=>moveLayer(layers,1);$('image-prev').onclick=()=>moveLayer(layers,-1);$('image-next').onclick=()=>moveLayer(layers,1);loadDetail();
}
function drawOpt(id,r,key){
const panel=$(id)?.closest('.chart-panel');
if(panel){panel.dataset.chartTitle=meta(key).label;panel.querySelector('.focus-chart').setAttribute('aria-label',`放大查看:${meta(key).label}`)}
plot(id,byMaterial(r,key),{title:meta(key).label,yMetric:key});
}
function moveLayer(layers,d){const i=layers.indexOf(viewState.selectedLayer);viewState.selectedLayer=layers[Math.max(0,Math.min(layers.length-1,i+d))];$('layer').value=viewState.selectedLayer;loadDetail()}
async function loadDetail(){
try{
const d=await api('/api/layer?layer='+viewState.selectedLayer);
plot('detail-meas',[{x:d.meas.map(x=>x.SamTime),y:d.meas.map(x=>x['MeasVal(1)']),name:'实测 Meas',mode:'lines',hovertemplate:'实测 Meas %{y:.2%}<extra>%{fullData.name}</extra>'},{x:d.meas.map(x=>x.SamTime),y:d.meas.map(x=>x['CalcVal(1)']),name:'动态 Calc',mode:'lines',hovertemplate:'动态 Calc %{y:.2%}<extra>%{fullData.name}</extra>'}],{title:`${viewState.selectedLayer}层实测与动态计算`,xTitle:'采样时间(s)',yTitle:'透过率(%)',yMetric:'final_meas'});
plot('detail-calc',[{x:d.calc.map(x=>x.thickness),y:d.calc.map(x=>x.calculated_t),name:'理论曲线',mode:'lines',hovertemplate:'理论透过率 %{y:.2%} · 理论厚度 %{x} nm<extra>%{fullData.name}</extra>'}],{title:'理论厚度与透过率曲线',xTitle:'理论厚度(nm)',yTitle:'透过率(%)',yMetric:'calculated_t',hovermode:'closest'});
$('image-layer').textContent=`${viewState.selectedLayer}层`;
$('detail-image-content').innerHTML=d.has_image?`<img class="image" src="${withToken(`/api/image?layer=${viewState.selectedLayer}`)}" alt="${viewState.selectedLayer}层镀膜截图">`:'<p class="muted">该层没有对应截图。</p>';
}catch(e){showError(e)}
}
function renderDevice(r){const available=Object.values(DEVICE_GROUPS).flat().filter(k=>r.some(x=>x[k]!=null));if(!available.includes(viewState.deviceMetric))viewState.deviceMetric=available[0]||'';const menuName=key=>key==='motor_mean'?'监控log转速参考值':deviceMenuName(key),dataName=key=>key==='motor_mean'?'监控log转速参考值均值':meta(key).label;const rail=Object.entries(DEVICE_GROUPS).map(([g,keys])=>{const v=keys.filter(k=>available.includes(k));return v.length?`<section class="metric-group"><h3>${g}</h3>${v.map(k=>`<button data-metric="${k}" class="${k===viewState.deviceMetric?'active':''}">${menuName(k)}</button>`).join('')}</section>`:''}).join('');$('page').innerHTML=pageHeader('设备分析','按设备类别选择指标,比较 H/L 材料的逐层均值与波动')+`<div class="device-layout"><aside class="panel metric-rail">${rail}</aside><div class="device-main">${chartPanel('device-chart',dataName(viewState.deviceMetric),{className:'tall'})}</div></div><section class="panel device-table-panel"><h3 style="margin-top:0">H/L 分组统计</h3><div id="device-table"></div></section>`;const draw=key=>{viewState.deviceMetric=key;document.querySelectorAll('[data-metric]').forEach(x=>x.classList.toggle('active',x.dataset.metric===key));const selected={...meta(key),label:dataName(key)},panel=$('device-chart').closest('.chart-panel');panel.dataset.chartTitle=`${selected.label}逐层趋势`;panel.querySelector('.focus-chart').setAttribute('aria-label',`放大查看:${selected.label}逐层趋势`);plot('device-chart',byMaterial(r,key,true),{title:`${selected.label}逐层趋势`,yMetric:key});FIELD_META.mean={label:'分组均值',unit:selected.unit,digits:selected.digits};FIELD_META.min={label:'逐层最小值',unit:selected.unit,digits:selected.digits};FIELD_META.max={label:'逐层最大值',unit:selected.unit,digits:selected.digits};const stats=[...new Set(r.map(x=>x.material))].map(material=>{const v=r.filter(x=>x.material===material).map(x=>x[key]).filter(Number.isFinite);return {material,count:v.length,mean:v.length?v.reduce((a,b)=>a+b,0)/v.length:null,min:v.length?Math.min(...v):null,max:v.length?Math.max(...v):null}});$('device-table').innerHTML=table(stats,['material','count','mean','min','max'])};if(viewState.deviceMetric)draw(viewState.deviceMetric);document.querySelectorAll('[data-metric]').forEach(b=>b.onclick=()=>draw(b.dataset.metric))}
function renderAnomaly(r){
const layerSet=new Set(r.map(x=>x.layer)),a=payload.anomalies.filter(x=>layerSet.has(x.layer)),metrics=[...new Set(a.map(x=>x.metric))].sort();
if(!metrics.includes(viewState.anomalyMetric))viewState.anomalyMetric=metrics[0]||'';
const mc=`<label>指标 <select id="anomaly-metric">${metrics.map(x=>`<option value="${x}" ${x===viewState.anomalyMetric?'selected':''}>${meta(x).label}</option>`).join('')}</select></label>`;
$('page').innerHTML=pageHeader('异常复核','统计异常仅表示相对同类层偏离,不代表质量判定')+`${chartPanel('anomaly-chart','异常指标',{controls:mc,className:'tall'})}<h3 class="section-title">异常排名</h3>${table(a,['layer','material','method','metric','value','group','robust_z','level','reason'])}`;
const drawA=key=>{viewState.anomalyMetric=key;if(!key)return $('anomaly-chart').innerHTML='<p class="muted">当前筛选未发现统计异常。</p>';plot('anomaly-chart',[...byMaterial(r,key),{x:a.filter(x=>x.metric===key).map(x=>x.layer),y:a.filter(x=>x.metric===key).map(x=>x.value),name:'统计异常',mode:'markers',marker:{color:'#b42318',size:12,symbol:'circle-open'}}],{title:`异常指标:${meta(key).label}`,yMetric:key})};
drawA(viewState.anomalyMetric);
if($('anomaly-metric'))$('anomaly-metric').onchange=e=>drawA(e.target.value);
}
const fmtDate=value=>value?new Date(value).toLocaleString('zh-CN',{hour12:false}):'暂无';
function availableMachineMetrics(){const available=new Set(payload.machine_summary?.available_metrics||[]);return Object.entries(MACHINE_METRICS).filter(([,spec])=>spec.columns.every(column=>available.has(column))).map(([key])=>key)}
function machineMetricRail(active,handler='data-machine-metric'){const available=availableMachineMetrics(),groups=[...new Set(available.map(key=>MACHINE_METRICS[key].group))];return groups.map(group=>`<section class="metric-group"><h3>${group}</h3>${available.filter(key=>MACHINE_METRICS[key].group===group).map(key=>`<button ${handler}="${esc(key)}" class="${key===active?'active':''}">${esc(MACHINE_METRICS[key].label)}</button>`).join('')}</section>`).join('')}
let deviceSeriesController=null,deviceSeriesGeneration=0;
function cancelDeviceSeries(){deviceSeriesGeneration++;deviceSeriesController?.abort();deviceSeriesController=null}
function deviceTimeQuery(metric,sources,{range=null,detail='overview'}={}){const p=new URLSearchParams({metric,sources:sources.join(','),max_points:'8000',detail}),aligned=payload.alignment?.status==='matched';if(aligned){p.set('layer_min',$('layer-min').value);p.set('layer_max',$('layer-max').value);const methods=checked('method');p.set('methods',methods.length?methods.join(','):'__none__')}const materials=checked('material');p.set('materials',materials.length?materials.join(','):'__none__');p.set('session',viewState.scope==='machine'?(viewState.machineSession||'all'):'all');if(range?.length===2){p.set('start',new Date(range[0]).toISOString());p.set('end',new Date(range[1]).toISOString())}return p.toString()}
function layerGuideRows(boundaries,range,maxLabels,xaxis=null,minGap=68){let visible=boundaries||[];if(range?.length===2){const lo=Date.parse(range[0]),hi=Date.parse(range[1]);visible=visible.filter(x=>{const time=Date.parse(x.time);return time>=lo&&time<=hi})}if(xaxis?.d2p){const spaced=[];let last=-Infinity;for(const item of visible){const position=xaxis.d2p(item.time);if(Number.isFinite(position)&&position-last>=minGap){spaced.push(item);last=position}}visible=spaced}if(visible.length<=maxLabels)return visible;if(maxLabels<=1)return visible.slice(0,1);return [...new Set(Array.from({length:maxLabels},(_,index)=>visible[Math.round(index*(visible.length-1)/(maxLabels-1))]))]}
function timeBreakLayout(breaks){return (breaks||[]).map(item=>({values:[item.start],dvalue:Math.max(1,Date.parse(item.end)-Date.parse(item.start))}))}
function layerAxisLayout(boundaries,breaks){if(!boundaries?.length)return null;const ticks=layerGuideRows(boundaries,null,12);return {...axis(null,'层数'),title:{text:''},automargin:false,type:'date',side:'top',overlaying:'x',matches:'x',tickmode:'array',tickangle:0,tickvals:ticks.map(item=>item.time),ticktext:ticks.map(item=>`${item.layer}层`),rangebreaks:breaks,showgrid:false,showline:true,ticks:'outside',ticklen:5,tickcolor:'#c8d3dc'}}
function refreshLayerAxis(chart){const state=chart?._layerAxisState,xaxis=chart?._fullLayout?.xaxis;if(!state||!xaxis||!chart?._fullLayout?.xaxis2||chart._layerAxisUpdating)return;const maxLabels=chart.closest('.chart-panel')?.classList.contains('focused')?20:12,ticks=layerGuideRows(state.boundaries,state.range||xaxis.range,maxLabels,xaxis,68),tickvals=ticks.map(item=>item.time),ticktext=ticks.map(item=>`${item.layer}层`),key=tickvals.join('|');if(key===state.tickKey)return;state.tickKey=key;chart._layerAxisUpdating=true;Plotly.relayout(chart,{'xaxis2.tickvals':tickvals,'xaxis2.ticktext':ticktext}).finally(()=>{chart._layerAxisUpdating=false})}
async function drawDeviceTimeSeries(id,metric,sources,title,unit,{markersOnly=false}={}){
cancelDeviceSeries();
const generation=deviceSeriesGeneration,state={range:null,dragmode:'zoom'};
setService('busy','正在加载设备数据');
const controller=new AbortController();deviceSeriesController=controller;
let data;try{data=await api(`/api/device-series?${deviceTimeQuery(metric,sources)}`,{signal:controller.signal})}catch(error){if(error?.name==='AbortError')return null;setService('online','设备数据加载失败');throw error}
if(generation!==deviceSeriesGeneration||!$(id))return null;
const name=['power_mean','current_mean','voltage_mean','motor_mean','o2_mean','ar_mean'].includes(metric)?deviceDataName(metric,'overview'):`${title}值`,hasBreaks=(data.time_breaks||[]).length>0,splitByMaterial=!['o2_mean','ar_mean'].includes(metric),selectedMaterials=checked('material').filter(material=>material==='H'||material==='L'),plotItems=data.traces.flatMap(item=>{if(!splitByMaterial)return [item];return selectedMaterials.map(material=>({...item,seriesMaterial:material,value:item.value.map((value,index)=>String(item.material[index]||'').includes(material)?value:null)})).filter(part=>part.value.some(Number.isFinite))});
const traces=plotItems.map((item,index)=>{const sourceName=item.source==='machine'?'工控log':'监控log',color=item.seriesMaterial?materialColor(item.seriesMaterial):deviceTraceColor(item,index),channelName=item.channel===metric?name:meta(item.channel).label,traceBase=item.seriesMaterial?`${item.seriesMaterial} 材料`:channelName,traceName=sources.length>1?`${traceBase} · ${sourceName}`:traceBase,customdata=item.time.map((_,position)=>[item.layer[position]==null?'—':`${item.layer[position]}层`,item.material[position]??'—',item.method[position]??'—']);return {x:item.time,y:item.value,customdata,name:traceName,type:hasBreaks?'scatter':'scattergl',mode:markersOnly?'markers':'lines+markers',connectgaps:false,...(markersOnly?{}:{line:{width:2,color,dash:item.source==='monitor'?'dot':'solid'}}),marker:{size:markersOnly?5:3,color,symbol:item.source==='monitor'?'circle-open':'circle'},hovertemplate:`${channelName} %{y:.6g} ${unit} · ${sourceName} · 镀膜层:%{customdata[0]} · 材料:%{customdata[1]} · 监控方式:%{customdata[2]}<extra>%{fullData.name}</extra>`}});
const shapes=(data.boundaries||[]).map(item=>({type:'line',xref:'x',yref:'paper',x0:item.time,x1:item.time,y0:0,y1:1,line:{color:'rgba(7,94,145,.13)',width:1}})),breaks=timeBreakLayout(data.time_breaks),xaxis={...axis(null,'时间'),type:'date',rangebreaks:breaks},xaxis2=layerAxisLayout(data.boundaries,breaks);
if(xaxis2&&traces.length){const boundaryTimes=(data.boundaries||[]).map(item=>item.time),anchorValue=traces.flatMap(trace=>trace.y||[]).find(Number.isFinite);if(boundaryTimes.length&&anchorValue!=null)traces.push({x:[boundaryTimes[0],boundaryTimes.at(-1)],y:[anchorValue,anchorValue],xaxis:'x2',yaxis:'y',type:'scatter',mode:'markers',marker:{opacity:0},showlegend:false,hoverinfo:'skip'})}
await plot(id,traces,{title:`${name}时间趋势`,xTitle:'时间',yTitle:`${name}(${unit})`,hovermode:'x unified',margin:{t:148},xaxis,...(xaxis2?{xaxis2}:{}),shapes,dragmode:state.dragmode,uirevision:`${id}:${metric}:${sources.join('-')}:${markersOnly?'markers':'lines'}`});
const chart=$(id),panel=chart.closest('.chart-panel');chart._layerAxisState=xaxis2?{boundaries:data.boundaries||[],range:null,tickKey:''}:null;panel.dataset.chartTitle=`${name}时间趋势`;panel.querySelector('.focus-chart')?.setAttribute('aria-label',`放大查看:${name}时间趋势`);refreshLayerAxis(chart);setService('online','分析完成');
chart.removeAllListeners?.('plotly_relayout');chart.on?.('plotly_relayout',event=>{if(chart._layerAxisUpdating)return;if(event.dragmode)state.dragmode=event.dragmode;const directRange=event['xaxis.range[0]']!=null&&event['xaxis.range[1]']!=null?[event['xaxis.range[0]'],event['xaxis.range[1]']]:event['xaxis2.range[0]']!=null&&event['xaxis2.range[1]']!=null?[event['xaxis2.range[0]'],event['xaxis2.range[1]']]:Array.isArray(event['xaxis.range'])?event['xaxis.range']:Array.isArray(event['xaxis2.range'])?event['xaxis2.range']:null;if(directRange)state.range=directRange;else if(event['xaxis.autorange']||event['xaxis2.autorange'])state.range=null;if(chart._layerAxisState)chart._layerAxisState.range=state.range;requestAnimationFrame(()=>refreshLayerAxis(chart))});
return data
}
function renderMachineOverview(){
const m=payload.machine_summary,segments=filteredMachineSegments(),linked=linkedSessionIds(),sourceSessions=new Map((payload.machine_sessions||[]).map(row=>[Number(row.session_id),row])),groups=new Map();
for(const segment of segments){const id=Number(segment.session_id),current=groups.get(id)||{session_id:id,start:segment.start,end:segment.end,target_seconds:0,rows:0};current.start=new Date(segment.start)<new Date(current.start)?segment.start:current.start;current.end=new Date(segment.end)>new Date(current.end)?segment.end:current.end;current.target_seconds+=(Number(segment.target_seconds)||0);current.rows+=(Number(segment.rows)||0);groups.set(id,current)}
const sessions=[...groups.values()].sort((a,b)=>a.session_id-b.session_id).map(row=>({...row,session_name:sessionName(row.session_id,linked),elapsed_seconds:(new Date(row.end)-new Date(row.start))/1000+(Number(m.cadence_seconds)||0),is_default:Boolean(sourceSessions.get(row.session_id)?.is_default)})),start=segments.length?segments.reduce((value,row)=>new Date(row.start)<new Date(value)?row.start:value,segments[0].start):null,end=segments.length?segments.reduce((value,row)=>new Date(row.end)>new Date(value)?row.end:value,segments[0].end):null,span=start&&end?(new Date(end)-new Date(start))/1000+(Number(m.cadence_seconds)||0):null,h=segments.filter(row=>String(row.material).includes('H')).length,l=segments.filter(row=>String(row.material).includes('L')).length,quality=payload.machine_quality||[];
$('page').innerHTML=pageHeader('工控log总览','当前筛选工作数据与整炉质量')+`<div class="cards"><div class="card">日期文件(整炉)<b>${m.file_count}</b>${m.files?.join('、')||'暂无'}</div><div class="card">当前工作段<b>${segments.length}</b>H ${h} / L ${l}</div><div class="card">当前采样数<b>${Math.round(sumField(segments,'rows')).toLocaleString()}</b>整炉间隔 ${m.cadence_seconds||0} s</div><div class="card">当前时间范围<b>${fmtSec(span)}</b>${fmtDate(start)} 至 ${fmtDate(end)}</div><div class="card">当前靶材工作时间<b>${fmtSec(sumField(segments,'target_seconds'))}</b>H/L 有效工作数据</div><div class="card">数据质量(整炉)<b>${m.quality_errors||0} 错误</b>${m.quality_warnings||0} 警告</div></div><h3 class="section-title">当前区段</h3>${table(sessions,['session_name','start','end','elapsed_seconds','target_seconds','is_default'])}<details><summary>整炉工控log结构与时间质量</summary>${quality.length?table(quality,['severity','scope','issue','detail']):'<p class="muted">检查完成:未发现文件、表头、数值或时间连续性问题。</p>'}</details>`
}
function renderMachineTrend(){cancelDeviceSeries();const available=availableMachineMetrics();if(!available.includes(viewState.machineMetric))viewState.machineMetric=available[0]||'power';const spec=MACHINE_METRICS[viewState.machineMetric];$('page').innerHTML=pageHeader('设备','按区段与材料查看工控采样;关联成功时显示镀膜层')+`<div class="device-layout"><aside class="panel metric-rail">${machineMetricRail(viewState.machineMetric)}</aside><div class="device-main">${chartPanel('machine-trend-chart',spec.label,{className:'tall'})}</div></div>`;drawDeviceTimeSeries('machine-trend-chart',spec.api,['machine'],spec.label,spec.unit,{markersOnly:true}).catch(showError);document.querySelectorAll('[data-machine-metric]').forEach(button=>button.onclick=()=>{viewState.machineMetric=button.dataset.machineMetric;renderMachineTrend()})}
function alignmentBadge(){const a=payload.alignment||{},cls=a.status!=='matched'?'danger':a.confidence==='高可信'?'success':'warning';return `<span class="status-badge ${cls}">${esc(alignmentLabel(a))}</span>`}
function renderCombinedOverview(r){const a=payload.alignment||{},range=a.matched_layer_start!=null&&a.matched_layer_end!=null?`${a.matched_layer_start}–${a.matched_layer_end}层`:'暂无',ranges=(a.candidate_ranges||[]).map(item=>`${item.layer_start}–${item.layer_end}层${item.confidence?`(${item.confidence})`:''}`).join(' / ')||'无',displayAlignment={...a,confidence:alignmentLabel(a),coverage:a.coverage==='all_completed'?'全部已完成层':a.coverage==='partial_completed'?'部分已完成层':'未关联',matched_layer_range:range,ignored_machine_edge_segments:(a.ignored_machine_edge_segments||[]).join('、')||'无',candidate_ranges:ranges},layers=new Set(r.map(row=>Number(row.layer))),segments=(payload.machine_segments||[]).filter(row=>row.layer!=null&&layers.has(Number(row.layer))),completed=r.filter(row=>row.layer_start&&row.layer_end&&Number.isFinite(Number(row.actual_time))),matched=r.filter(row=>row.target_power_seconds!=null&&Number.isFinite(Number(row.target_power_seconds))),start=segments.length?segments.reduce((value,row)=>new Date(row.start)<new Date(value)?row.start:value,segments[0].start):null,end=segments.length?segments.reduce((value,row)=>new Date(row.end)>new Date(value)?row.end:value,segments[0].end):null,wallSeconds=start&&end?(new Date(end)-new Date(start))/1000:null,h=r.filter(row=>row.material==='H').length,l=r.filter(row=>row.material==='L').length;$('page').innerHTML=pageHeader('综合总览','当前筛选双源统计与整炉关联状态')+`<div class="status-line">${alignmentBadge()}<span>整炉关联:${esc(a.reason||'暂无关联说明')}</span></div><div class="cards"><div class="card">当前理论层数<b>${r.length}</b>H ${h} / L ${l}</div><div class="card">当前已完成层数<b>${completed.length}</b>具备完整镀膜时间</div><div class="card">当前已关联层数<b>${matched.length}</b>工控log已映射</div><div class="card">整炉关联范围<b>${range}</b>${esc(alignmentLabel(a))}</div><div class="card">当前实际镀膜时间<b>${fmtSec(sumField(completed,'actual_time'))}</b>来自监控log</div><div class="card">当前靶材工作时间<b>${fmtSec(sumField(matched,'target_power_seconds'))}</b>来自工控log</div><div class="card">当前镀膜层墙钟时间<b>${fmtSec(wallSeconds)}</b>已关联首层开始至末层结束</div></div>${chartPanel('combined-overview-chart','实际镀膜时间与靶材工作时间',{className:'tall'})}<details><summary>整炉关联诊断</summary>${table([displayAlignment],['confidence','coverage','planned_layers','completed_layers','matched_segments','matched_layer_range','ignored_machine_edge_segments','candidate_ranges','candidate_count','duration_correlation','time_offset_seconds','offset_std_seconds','reason'])}</details>`;plot('combined-overview-chart',[lineTrace(r,'actual_time','监控实际镀膜时间',COLORS[0]),lineTrace(r,'target_power_seconds','工控靶材工作时间',COLORS[1])],{title:'实际镀膜时间与靶材工作时间',yTitle:'时间(s)'})}
function renderCombinedDevice(r){
cancelDeviceSeries();
const available=Object.values(DEVICE_GROUPS).flat().filter(key=>r.some(x=>x[key]!=null));
if(!available.includes(viewState.deviceMetric))viewState.deviceMetric=available[0]||'';
const rail=Object.entries(DEVICE_GROUPS).map(([group,keys])=>{const current=keys.filter(key=>available.includes(key));return current.length?`<section class="metric-group"><h3>${group}</h3>${current.map(key=>`<button data-combined-metric="${key}" class="${key===viewState.deviceMetric?'active':''}">${deviceMenuName(key)}</button>`).join('')}</section>`:''}).join('');
const timeMode=viewState.combinedDeviceAxis==='time',axisToggle=`<div class="view-toggle"><span>横坐标</span><div class="segments" role="radiogroup" aria-label="设备横坐标"><label><input type="radio" name="combined-device-axis" value="layer" ${timeMode?'':'checked'}><span>层数</span></label><label><input type="radio" name="combined-device-axis" value="time" ${timeMode?'checked':''}><span>时间</span></label></div></div>`;
$('page').innerHTML=pageHeader('设备',timeMode?'按监控log时间对齐双源真实采样点':'按镀膜层比较双源统计值')+axisToggle+`<div class="device-layout"><aside class="panel metric-rail">${rail}</aside><div class="device-main">${chartPanel('combined-device-chart',meta(viewState.deviceMetric).label,{className:'tall'})}</div></div>${timeMode?'':'<section class="panel device-table-panel"><h3 style="margin-top:0">H/L 分组对照</h3><div id="combined-device-table"></div></section>'}`;
const drawLayer=key=>{const segmentMap=new Map((payload.machine_segments||[]).filter(x=>x.layer!=null).map(x=>[x.layer,x])),monitorKey=key==='motor_mean'?'monitor_motor_mean':key,name=key==='motor_mean'?'转速均值':meta(key).label,materials=sortMaterials(r.map(x=>x.material)),traces=[];FIELD_META.combined_value={label:name,unit:meta(key).unit,digits:meta(key).digits};for(const material of materials){const materialRows=r.filter(x=>x.material===material),color=materialColor(material),monitorRows=materialRows.map(row=>({...row,combined_value:row[monitorKey]})),machineRows=materialRows.map(row=>({...row,combined_value:segmentMap.get(row.layer)?.[key]??null})),monitorTrace=lineTrace(monitorRows,'combined_value',`${material} 材料 · 监控log`,color,true),machineTrace=lineTrace(machineRows,'combined_value',`${material} 材料 · 工控log`,color,true);monitorTrace.line.dash='dot';monitorTrace.marker.symbol='circle-open';traces.push(monitorTrace,machineTrace)}plot('combined-device-chart',traces,{title:`${name}双源逐层对照`,yMetric:key});const monitorValues=r.map(x=>x[monitorKey]),machineValues=r.map(x=>segmentMap.get(x.layer)?.[key]??null);FIELD_META.monitor_value={label:'监控log均值',unit:meta(key).unit,digits:meta(key).digits};FIELD_META.machine_value={label:'工控log均值',unit:meta(key).unit,digits:meta(key).digits};FIELD_META.difference={label:'工控log - 监控log',unit:meta(key).unit,digits:meta(key).digits};const stats=materials.map(material=>{const indices=r.map((x,i)=>x.material===material?i:-1).filter(i=>i>=0),mv=indices.map(i=>Number(monitorValues[i])).filter(Number.isFinite),cv=indices.map(i=>Number(machineValues[i])).filter(Number.isFinite),mean=values=>values.length?values.reduce((a,b)=>a+b,0)/values.length:null,monitorValue=mean(mv),machineValue=mean(cv);return {material,count:Math.min(mv.length,cv.length),monitor_value:monitorValue,machine_value:machineValue,difference:monitorValue==null||machineValue==null?null:machineValue-monitorValue}});$('combined-device-table').innerHTML=table(stats,['material','count','monitor_value','machine_value','difference'])};
if(viewState.deviceMetric){if(timeMode){const key=viewState.deviceMetric,spec={label:deviceMenuName(key),unit:meta(key).unit};drawDeviceTimeSeries('combined-device-chart',key,['machine','monitor'],spec.label,spec.unit,{markersOnly:true}).catch(showError)}else drawLayer(viewState.deviceMetric)}
document.querySelectorAll('[data-combined-metric]').forEach(button=>button.onclick=()=>{viewState.deviceMetric=button.dataset.combinedMetric;renderCombinedDevice(rows())});
document.querySelectorAll('input[name="combined-device-axis"]').forEach(input=>input.onchange=e=>{viewState.combinedDeviceAxis=e.target.value;renderCombinedDevice(rows())});
}
const RENDERERS={overview:renderOverview,theory:renderTheory,optical:renderOptical,device:renderDevice,anomaly:renderAnomaly,'machine-overview':renderMachineOverview,'machine-trend':renderMachineTrend,'combined-overview':renderCombinedOverview,'combined-device':renderCombinedDevice};function renderActivePage(){if(!payload)return;cancelDeviceSeries();clearError();if(viewState.focused)toggleChartFocus(viewState.focused.querySelector('.focus-chart'));document.querySelectorAll('#modules button').forEach(x=>x.classList.toggle('active',x.dataset.page===viewState.active));$('page').replaceChildren();const currentRows=rows(),empty=viewState.scope==='machine'?!filteredMachineSegments().length:!currentRows.length;if(empty){const title=(NAV[viewState.scope]||[]).find(([key])=>key===viewState.active)?.[1]||'分析';$('page').innerHTML=pageHeader(title,'当前筛选结果')+selectionEmpty();return}const renderer=RENDERERS[viewState.active];if(renderer)renderer(currentRows)}$('choose').onclick=choose;$('analyze').onclick=analyze;$('release-link').onclick=e=>{if(window.pywebview?.api){e.preventDefault();window.pywebview.api.open_releases()}};$('modules').onclick=e=>{const b=e.target.closest('[data-page]');if(b){viewState.active=b.dataset.page;viewState.activeByScope[viewState.scope]=viewState.active;renderActivePage()}};$('scope-options').onclick=e=>{const b=e.target.closest('[data-scope]');if(b)setScope(b.dataset.scope)};
let plotlyPromise;function loadPlotly(){if(window.Plotly)return Promise.resolve();if(plotlyPromise)return plotlyPromise;plotlyPromise=new Promise((resolve,reject)=>{const s=document.createElement('script');s.src='/plotly.min.js';s.onload=resolve;s.onerror=()=>reject(new ApiError('图表组件加载失败。'));document.head.appendChild(s)});return plotlyPromise}async function connect(){clearError();setService('checking','正在连接本地服务');try{const [h]=await Promise.all([api('/api/health'),loadPlotly()]);if(h.service!=='coating-analyzer')throw new ApiError('当前端口不是 Log Analysis 服务。','wrong_service','请重新启动应用。');$('app-version').textContent=`版本 ${h.app_version} · ${h.build_channel==='beta'?'测试版':h.build_channel}`;const s=await api('/api/state');$('path').value=s.path||'';$('analyze').disabled=!s.path;setService('online',s.path?'已恢复上次炉次路径':'本地服务已连接')}catch(e){showError(e)}}
const plotWithDefaultSpacing=plot;
plot=function(id,traces,options={}){const margin={t:122,l:72,r:28,b:58,...(options.margin||{})};const legend={orientation:'h',x:.025,y:.89,yref:'container',yanchor:'top',font:{size:11},bgcolor:'rgba(255,255,255,0)',borderwidth:0,...(options.legend||{})};return plotWithDefaultSpacing(id,traces,{...options,margin,legend})};
if(location.protocol==='file:'){$('direct-file').classList.remove('hidden');$('app-shell').classList.add('hidden')}else connect();
</script></body></html>