@daimazun/hardware-info 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +907 -0
  2. package/bin/OpenHardwareMonitorLib.dll +0 -0
  3. package/index.js +192 -0
  4. package/lib/allhardware.js +35 -0
  5. package/lib/backend-usage.js +103 -0
  6. package/lib/battery.js +76 -0
  7. package/lib/disk-watcher.js +156 -0
  8. package/lib/disk.js +77 -0
  9. package/lib/gpu.js +70 -0
  10. package/lib/hardware-service.js +284 -0
  11. package/lib/memory.js +59 -0
  12. package/lib/monitor.js +176 -0
  13. package/lib/network.js +75 -0
  14. package/lib/ohm-daemon.js +208 -0
  15. package/lib/ohm.js +41 -0
  16. package/lib/perfctr.js +43 -0
  17. package/lib/process-icon.js +76 -0
  18. package/lib/process-ops.js +218 -0
  19. package/lib/processes.js +232 -0
  20. package/lib/ps.js +93 -0
  21. package/lib/public-ip.js +124 -0
  22. package/lib/services.js +43 -0
  23. package/lib/sysinfo-daemon.js +188 -0
  24. package/lib/system.js +77 -0
  25. package/lib/usb.js +49 -0
  26. package/lib/win32-procs.js +241 -0
  27. package/package.json +50 -0
  28. package/scripts/get-all-hardware.ps1 +98 -0
  29. package/scripts/get-lhm-temp.ps1 +119 -0
  30. package/scripts/ohm-daemon.ps1 +141 -0
  31. package/scripts/sysinfo-daemon.ps1 +386 -0
  32. package/test/check-bugs.js +182 -0
  33. package/test/public/gj.pay.ali.jpg +0 -0
  34. package/test/public/gj.pay.wx.jpg +0 -0
  35. package/test/public/index.html +996 -0
  36. package/test/public/zdl.pay.ali.jpg +0 -0
  37. package/test/public/zdl.pay.wx.jpg +0 -0
  38. package/test/scan-encoding.js +77 -0
  39. package/test/server.js +255 -0
  40. package/test/test-all.js +180 -0
  41. package/test/test-daemon.js +51 -0
  42. package/test/test-kill-name.js +30 -0
  43. package/test/test-monitor.js +78 -0
  44. package/test/test-new-features.js +93 -0
  45. package/test/test-service.js +91 -0
  46. package/test/test-sysinfo-daemon.js +95 -0
  47. package/test/test.js +63 -0
@@ -0,0 +1,996 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>hardware-info 仪表盘</title>
7
+ <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
8
+ <style>
9
+ * { margin: 0; padding: 0; box-sizing: border-box; }
10
+ body {
11
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
12
+ background: #0f1117; color: #e0e0e0; min-height: 100vh; padding: 20px;
13
+ }
14
+ .header {
15
+ display: flex; justify-content: space-between; align-items: center;
16
+ margin-bottom: 24px; padding-bottom: 16px; border-bottom: 1px solid #2a2d3a;
17
+ }
18
+ .header h1 { font-size: 22px; font-weight: 600; color: #fff; }
19
+ .header-right { display: flex; align-items: center; gap: 16px; }
20
+ .status { font-size: 13px; color: #888; display: flex; align-items: center; gap: 6px; }
21
+ .status .dot {
22
+ display: inline-block; width: 8px; height: 8px; border-radius: 50%;
23
+ background: #4ade80; animation: pulse 2s infinite;
24
+ }
25
+ .status .dot.paused { background: #facc15; animation: none; }
26
+ @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
27
+ .clock { font-size: 13px; color: #888; }
28
+ .refresh-control { display: flex; align-items: center; gap: 10px; }
29
+ .refresh-label { font-size: 12px; color: #888; }
30
+ .refresh-slider {
31
+ -webkit-appearance: none; appearance: none; width: 140px; height: 4px;
32
+ background: #2a2d3a; border-radius: 2px; outline: none; cursor: pointer;
33
+ }
34
+ .refresh-slider::-webkit-slider-thumb {
35
+ -webkit-appearance: none; appearance: none; width: 14px; height: 14px;
36
+ background: #3b82f6; border-radius: 50%; cursor: pointer; transition: transform 0.1s;
37
+ }
38
+ .refresh-slider::-webkit-slider-thumb:hover { transform: scale(1.2); }
39
+ .refresh-slider::-moz-range-thumb {
40
+ width: 14px; height: 14px; background: #3b82f6; border-radius: 50%;
41
+ cursor: pointer; border: none;
42
+ }
43
+ .refresh-value { font-size: 13px; color: #3b82f6; font-weight: 600; min-width: 42px; }
44
+ .pause-btn {
45
+ padding: 4px 12px; font-size: 12px; border: 1px solid #2a2d3a; background: #1a1d29;
46
+ color: #aaa; border-radius: 6px; cursor: pointer; transition: all 0.15s;
47
+ }
48
+ .pause-btn:hover { border-color: #f87171; color: #f87171; }
49
+ .pause-btn.paused { background: #f87171; border-color: #f87171; color: #fff; }
50
+ .raw-toggle {
51
+ padding: 4px 12px; font-size: 12px; border: 1px solid #2a2d3a; background: #1a1d29;
52
+ color: #aaa; border-radius: 6px; cursor: pointer; transition: all 0.15s;
53
+ }
54
+ .raw-toggle:hover { border-color: #a855f7; color: #a855f7; }
55
+ .raw-toggle.active { background: #a855f7; border-color: #a855f7; color: #fff; }
56
+ .raw-panel {
57
+ margin-top: 24px; background: #1a1d29; border: 1px solid #2a2d3a; border-radius: 12px;
58
+ padding: 16px;
59
+ }
60
+ .raw-tabs { display: flex; gap: 4px; margin-bottom: 12px; flex-wrap: wrap; }
61
+ .raw-tab {
62
+ padding: 4px 10px; font-size: 12px; border: 1px solid #2a2d3a; background: #0f1117;
63
+ color: #888; border-radius: 6px; cursor: pointer; transition: all 0.15s;
64
+ }
65
+ .raw-tab:hover { border-color: #a855f7; color: #fff; }
66
+ .raw-tab.active { background: #a855f7; border-color: #a855f7; color: #fff; }
67
+ .raw-json {
68
+ background: #0f1117; border-radius: 8px; padding: 16px; overflow: auto;
69
+ max-height: 500px; font-family: 'Consolas', 'Monaco', monospace; font-size: 12px;
70
+ color: #a5f3fc; line-height: 1.5; white-space: pre;
71
+ }
72
+ .grid {
73
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
74
+ gap: 16px;
75
+ }
76
+ .card {
77
+ background: #1a1d29; border: 1px solid #2a2d3a; border-radius: 12px;
78
+ padding: 20px; transition: border-color 0.2s;
79
+ }
80
+ .card:hover { border-color: #3b82f6; }
81
+ .card-title {
82
+ font-size: 13px; color: #888; text-transform: uppercase; letter-spacing: 0.5px;
83
+ margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center;
84
+ }
85
+ .card-title .badge {
86
+ font-size: 11px; padding: 2px 8px; border-radius: 10px;
87
+ background: #2a2d3a; color: #aaa; text-transform: none; letter-spacing: 0;
88
+ }
89
+ .big-number { font-size: 42px; font-weight: 700; color: #fff; line-height: 1; }
90
+ .big-number .unit { font-size: 18px; color: #888; font-weight: 400; margin-left: 4px; }
91
+ .sub-info { font-size: 13px; color: #aaa; margin-top: 8px; }
92
+ .progress-bar {
93
+ width: 100%; height: 8px; background: #2a2d3a; border-radius: 4px;
94
+ overflow: hidden; margin: 10px 0;
95
+ }
96
+ .progress-fill { height: 100%; border-radius: 4px; transition: width 0.5s ease; }
97
+ .fill-green { background: linear-gradient(90deg, #4ade80, #22c55e); }
98
+ .fill-yellow { background: linear-gradient(90deg, #facc15, #eab308); }
99
+ .fill-red { background: linear-gradient(90deg, #f87171, #ef4444); }
100
+ .fill-blue { background: linear-gradient(90deg, #60a5fa, #3b82f6); }
101
+ .core-temps { display: flex; gap: 6px; margin-top: 12px; flex-wrap: wrap; }
102
+ .core-temp {
103
+ flex: 1; min-width: 60px; text-align: center; padding: 8px 4px;
104
+ background: #0f1117; border-radius: 6px;
105
+ }
106
+ .core-temp .core-name { font-size: 10px; color: #888; margin-bottom: 4px; }
107
+ .core-temp .core-val { font-size: 16px; font-weight: 600; color: #fff; }
108
+ .net-speed { display: flex; gap: 20px; margin-top: 8px; }
109
+ .net-speed-item { flex: 1; }
110
+ .net-speed-label { font-size: 11px; color: #888; margin-bottom: 4px; }
111
+ .net-speed-value { font-size: 20px; font-weight: 600; color: #fff; }
112
+ .net-speed-value .unit { font-size: 12px; color: #888; }
113
+ .net-down { color: #4ade80; }
114
+ .net-up { color: #60a5fa; }
115
+ .info-row {
116
+ display: flex; justify-content: space-between; padding: 6px 0;
117
+ font-size: 13px; border-bottom: 1px solid #2a2d3a;
118
+ }
119
+ .info-row:last-child { border-bottom: none; }
120
+ .info-row .label { color: #888; }
121
+ .info-row .value { color: #e0e0e0; font-weight: 500; }
122
+ .process-list { margin-top: 4px; }
123
+ .process-item {
124
+ display: flex; justify-content: space-between; align-items: center;
125
+ padding: 6px 0; font-size: 13px; border-bottom: 1px solid #2a2d3a;
126
+ }
127
+ .process-item:last-child { border-bottom: none; }
128
+ .process-name { color: #e0e0e0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 180px; }
129
+ .process-mem { color: #facc15; font-weight: 600; font-size: 12px; }
130
+ .process-info { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
131
+ .process-icon { width: 24px; height: 24px; flex-shrink: 0; margin-right: 8px; object-fit: contain; }
132
+ .process-icon-placeholder { background: #2a2d3a; border-radius: 4px; }
133
+ .process-cpu { font-weight: 700; font-size: 13px; }
134
+ .process-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
135
+ .proc-toolbar { background: #1a1c25; border-radius: 6px; padding: 10px 12px; margin-bottom: 10px; }
136
+ .proc-toolbar-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
137
+ .proc-toolbar-row:last-child { margin-bottom: 0; }
138
+ .proc-toolbar-label { font-size: 12px; color: #888; width: 32px; flex-shrink: 0; }
139
+ .proc-input { background: #252833; border: 1px solid #3a3d4a; color: #ddd; border-radius: 4px; padding: 4px 8px; font-size: 12px; outline: none; }
140
+ .proc-input:focus { border-color: #4a9eff; }
141
+ .proc-select { background: #252833; border: 1px solid #3a3d4a; color: #ddd; border-radius: 4px; padding: 4px 6px; font-size: 12px; outline: none; cursor: pointer; }
142
+ .proc-toolbar .btn-mini { width: auto; padding: 4px 10px; font-size: 11px; }
143
+ .proc-toolbar-result { font-size: 11px; color: #6bcb77; margin-top: 4px; padding-top: 6px; border-top: 1px solid #2a2d3a; word-break: break-all; }
144
+ .btn-mini {
145
+ background: #2a2d3a; border: 1px solid #3a3d4a; color: #aaa;
146
+ width: 24px; height: 24px; border-radius: 4px; cursor: pointer;
147
+ font-size: 12px; display: flex; align-items: center; justify-content: center;
148
+ padding: 0; transition: all 0.15s;
149
+ }
150
+ .btn-mini:hover { background: #3a3d4a; color: #fff; }
151
+ .btn-mini.btn-danger:hover { background: #dc2626; border-color: #dc2626; color: #fff; }
152
+ .disk-event-list { margin-top: 4px; }
153
+ .disk-event { display: flex; align-items: center; gap: 10px; padding: 5px 0; font-size: 12px; border-bottom: 1px solid #2a2d3a; }
154
+ .disk-event:last-child { border-bottom: none; }
155
+ .disk-event-time { color: #666; font-family: monospace; }
156
+ .disk-event-type { padding: 2px 8px; border-radius: 3px; font-weight: 600; }
157
+ .disk-event-type.add { background: rgba(74,222,128,0.15); color: #4ade80; }
158
+ .disk-event-type.remove { background: rgba(248,113,113,0.15); color: #f87171; }
159
+ .disk-event-disks { color: #e0e0e0; }
160
+ .empty-hint { color: #555; font-size: 12px; padding: 8px 0; text-align: center; }
161
+ .detail-section { margin-top: 16px; background: #1a1c28; border-radius: 10px; padding: 16px; }
162
+ .detail-tabs { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 16px; border-bottom: 1px solid #2a2d3a; padding-bottom: 8px; }
163
+ .detail-tab { background: transparent; border: 1px solid #3a3d4a; color: #888; padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 12px; transition: all 0.15s; }
164
+ .detail-tab:hover { color: #ccc; border-color: #555; }
165
+ .detail-tab.active { background: #3b82f6; border-color: #3b82f6; color: #fff; }
166
+ .detail-title { font-size: 14px; font-weight: 600; color: #e0e0e0; margin-bottom: 12px; }
167
+ .detail-table { width: 100%; border-collapse: collapse; font-size: 12px; }
168
+ .detail-table th { text-align: left; padding: 8px 6px; color: #888; font-weight: 500; border-bottom: 1px solid #2a2d3a; }
169
+ .detail-table td { padding: 6px; color: #ccc; border-bottom: 1px solid #1f2130; }
170
+ .detail-table tr:hover td { background: #1f2130; }
171
+ .usb-list { display: flex; flex-direction: column; gap: 4px; }
172
+ .usb-item { display: flex; justify-content: space-between; align-items: center; padding: 6px 8px; background: #1f2130; border-radius: 4px; font-size: 12px; }
173
+ .usb-name { color: #ccc; }
174
+ .usb-status { font-weight: 600; }
175
+ .usb-status.ok { color: #4ade80; }
176
+ .usb-status.warn { color: #facc15; }
177
+ .service-filters { display: flex; gap: 6px; margin-bottom: 12px; }
178
+ .filter-btn { background: #2a2d3a; border: 1px solid #3a3d4a; color: #888; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 11px; }
179
+ .filter-btn.active { background: #3b82f6; border-color: #3b82f6; color: #fff; }
180
+ .service-list { max-height: 400px; overflow-y: auto; }
181
+ .service-item { display: flex; align-items: center; gap: 8px; padding: 5px 8px; font-size: 12px; border-bottom: 1px solid #1f2130; }
182
+ .service-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
183
+ .service-dot.running { background: #4ade80; }
184
+ .service-dot.stopped { background: #555; }
185
+ .service-name { flex: 1; color: #ccc; }
186
+ .service-state { color: #666; font-size: 11px; }
187
+ .ohm-list { display: flex; flex-direction: column; gap: 12px; }
188
+ .ohm-hardware { background: #1f2130; border-radius: 6px; padding: 10px; }
189
+ .ohw-hw-title { font-size: 13px; font-weight: 600; color: #60a5fa; margin-bottom: 8px; }
190
+ .ohw-sensor { display: flex; justify-content: space-between; padding: 3px 0; font-size: 11px; border-bottom: 1px solid #2a2d3a; }
191
+ .ohw-sensor:last-child { border-bottom: none; }
192
+ .ohw-sensor-name { color: #aaa; }
193
+ .ohw-sensor-value { color: #facc15; font-weight: 600; }
194
+ .monitor-status .info-row { padding: 5px 0; }
195
+ .monitor-status .ok { color: #4ade80; }
196
+ .monitor-status .warn { color: #facc15; }
197
+ .disk-list { margin-top: 4px; }
198
+ .disk-item { margin-bottom: 12px; }
199
+ .disk-item:last-child { margin-bottom: 0; }
200
+ .disk-header { display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 6px; }
201
+ .disk-name { color: #e0e0e0; font-weight: 500; }
202
+ .disk-usage { color: #888; }
203
+ .temp-high { color: #f87171 !important; }
204
+ .temp-mid { color: #facc15 !important; }
205
+ .temp-low { color: #4ade80 !important; }
206
+ .public-ip { color: #60a5fa; margin-top: 8px; font-size: 13px; }
207
+ .footer { text-align: center; margin-top: 24px; font-size: 12px; color: #555; }
208
+ </style>
209
+ </head>
210
+ <body>
211
+ <div id="app">
212
+ <div class="header">
213
+ <h1>hardware-info 仪表盘</h1>
214
+ <div class="header-right">
215
+ <div class="status">
216
+ <span class="dot" :class="{ paused: isPaused }"></span>
217
+ <span>{{ isPaused ? '已暂停' : '已连接' }}</span>
218
+ </div>
219
+ <div class="clock">{{ clock }}</div>
220
+ <div class="refresh-control">
221
+ <span class="refresh-label">刷新:</span>
222
+ <input type="range" class="refresh-slider" v-model.number="intervalSec" min="0.1" max="3" step="0.1">
223
+ <span class="refresh-value">{{ intervalSec.toFixed(1) }}s</span>
224
+ <button class="pause-btn" :class="{ paused: isPaused }" @click="togglePause">
225
+ {{ isPaused ? '继续' : '暂停' }}
226
+ </button>
227
+ <button class="raw-toggle" :class="{ active: showRawData }" @click="showRawData = !showRawData">
228
+ {{ showRawData ? '隐藏原始数据' : '原始数据' }}
229
+ </button>
230
+ </div>
231
+ </div>
232
+ </div>
233
+
234
+ <div class="grid">
235
+ <!-- CPU 温度 -->
236
+ <div class="card">
237
+ <div class="card-title">CPU 温度 <span class="badge">{{ cpuTemp.source === 'openhardwaremonitor' ? '真实核心' : 'ACPI热区' }}</span></div>
238
+ <div class="big-number">
239
+ <span :class="tempClass(cpuTemp.temperature?.maxCore)">{{ (cpuTemp.temperature?.maxCore ?? '--').toFixed ? (cpuTemp.temperature.maxCore).toFixed(0) : cpuTemp.temperature?.maxCore }}</span>
240
+ <span class="unit">°C</span>
241
+ </div>
242
+ <div class="sub-info">封装: {{ cpuTemp.temperature?.package != null ? cpuTemp.temperature.package.toFixed(1) + '°C' : '--' }} <span v-if="cpuTemp.cpuName">| {{ cpuTemp.cpuName }}</span></div>
243
+ <div class="core-temps" v-if="cpuTemp.temperature?.cores?.length">
244
+ <div class="core-temp" v-for="c in cpuTemp.temperature.cores" :key="c.name">
245
+ <div class="core-name">{{ c.name.replace('CPU Core #', 'C') }}</div>
246
+ <div class="core-val" :class="tempClass(c.value)">{{ c.value }}°</div>
247
+ </div>
248
+ </div>
249
+ <div class="info-row" v-if="cpuLoad != null"><span class="label">负载</span><span class="value">{{ cpuLoad.toFixed(1) }}%</span></div>
250
+ <div class="info-row" v-if="cpuClockAvg != null"><span class="label">平均频率</span><span class="value">{{ cpuClockAvg.toFixed(0) }} MHz</span></div>
251
+ <div class="info-row" v-if="cpuPower != null"><span class="label">封装功耗</span><span class="value">{{ cpuPower.toFixed(1) }} W</span></div>
252
+ </div>
253
+
254
+ <!-- 内存 -->
255
+ <div class="card">
256
+ <div class="card-title">内存 <span class="badge">{{ memory.stickCount }}条内存</span></div>
257
+ <div class="big-number">{{ memory.usedPercent?.toFixed(2) }}<span class="unit">%</span></div>
258
+ <div class="progress-bar"><div class="progress-fill" :class="barClass(memory.usedPercent)" :style="{ width: (memory.usedPercent || 0) + '%' }"></div></div>
259
+ <div class="sub-info">已用: {{ memory.usedPhysicalGB }}GB / {{ memory.totalPhysicalGB }}GB | 可用: {{ memory.freePhysicalGB }}GB</div>
260
+ <div class="info-row" v-if="memory.totalVirtualGB"><span class="label">虚拟内存</span><span class="value">{{ memory.totalVirtualGB }}GB / 可用 {{ memory.freeVirtualGB }}GB</span></div>
261
+ <div class="info-row" v-if="memory.array?.maxCapacityGB"><span class="label">最大支持</span><span class="value">{{ memory.array.maxCapacityGB }}GB / {{ memory.array.memoryDevices }}插槽</span></div>
262
+ </div>
263
+
264
+ <!-- 网络 -->
265
+ <div class="card">
266
+ <div class="card-title">网络 <span class="badge">{{ network.activeAdapterCount }}活跃</span></div>
267
+ <div class="net-speed">
268
+ <div class="net-speed-item">
269
+ <div class="net-speed-label">下载</div>
270
+ <div class="net-speed-value net-down">{{ formatSpeed(network.totalDownloadBytesPerSec) }}</div>
271
+ </div>
272
+ <div class="net-speed-item">
273
+ <div class="net-speed-label">上传</div>
274
+ <div class="net-speed-value net-up">{{ formatSpeed(network.totalUploadBytesPerSec) }}</div>
275
+ </div>
276
+ </div>
277
+ <div class="sub-info" style="margin-top:12px;">
278
+ <div class="info-row" v-for="a in network.activeAdapters" :key="a.connectionName">
279
+ <span class="label">{{ a.connectionName || a.name }}</span>
280
+ <span class="value">{{ a.ipAddresses?.[0] || '--' }} <span style="color:#666;">{{ a.speedMbps }}Mbps</span></span>
281
+ </div>
282
+ </div>
283
+ <div class="info-row" v-if="network.wifi?.ssId"><span class="label">WiFi</span><span class="value">{{ network.wifi.ssId }} (信号 {{ network.wifi.signalPercent }}%)</span></div>
284
+ <div class="public-ip" v-if="publicIp">公网 IP: {{ publicIp }}</div>
285
+ </div>
286
+
287
+ <!-- 硬盘 -->
288
+ <div class="card">
289
+ <div class="card-title">硬盘 <span class="badge">{{ disk.diskCount }}块</span></div>
290
+ <div class="disk-list">
291
+ <div class="disk-item" v-for="ld in disk.logicalDrives" :key="ld.drive">
292
+ <div class="disk-header">
293
+ <span class="disk-name">{{ ld.drive }} {{ ld.volumeName || '' }}</span>
294
+ <span class="disk-usage">{{ ld.usedGB }}/{{ ld.sizeGB }}GB</span>
295
+ </div>
296
+ <div class="progress-bar"><div class="progress-fill" :class="barClass(ld.usedPercent)" :style="{ width: (ld.usedPercent || 0) + '%' }"></div></div>
297
+ </div>
298
+ </div>
299
+ <div class="info-row" v-if="disk.physicalDisks?.length"><span class="label">磁盘</span><span class="value" style="font-size:11px;">{{ disk.physicalDisks[0].model }}</span></div>
300
+ <div class="info-row"><span class="label">读写</span><span class="value">{{ formatSpeed(diskReadSpeed) }} / {{ formatSpeed(diskWriteSpeed) }}</span></div>
301
+ </div>
302
+
303
+ <!-- 电池 -->
304
+ <div class="card" v-if="battery.hasBattery">
305
+ <div class="card-title">电池 <span class="badge">{{ battery.isCharging ? '充电中' : '未充电' }}</span></div>
306
+ <div class="big-number">{{ battery.overallChargePercent?.toFixed(0) }}<span class="unit">%</span></div>
307
+ <div class="progress-bar"><div class="progress-fill" :class="barClass(100 - (battery.overallChargePercent || 0))" :style="{ width: (battery.overallChargePercent || 0) + '%' }"></div></div>
308
+ <div class="sub-info">{{ battery.batteries?.[0]?.name || '' }} | 健康度: {{ battery.batteries?.[0]?.healthPercent || '--' }}% | 循环: {{ battery.batteries?.[0]?.cycleCount || '--' }}次</div>
309
+ <div class="info-row" v-if="battery.batteries?.[0]?.voltagemV"><span class="label">电压</span><span class="value">{{ (battery.batteries[0].voltagemV / 1000).toFixed(2) }}V</span></div>
310
+ <div class="info-row" v-if="battery.batteries?.[0]?.fullChargeCapacitymWh"><span class="label">容量</span><span class="value">{{ (battery.batteries[0].fullChargeCapacitymWh / 1000).toFixed(0) }}Wh / 设计 {{ (battery.batteries[0].designCapacitymWh / 1000).toFixed(0) }}Wh</span></div>
311
+ </div>
312
+
313
+ <!-- 显卡 -->
314
+ <div class="card">
315
+ <div class="card-title">显卡/显示器 <span class="badge">{{ gpu.gpuCount }}卡 / {{ gpu.monitorCount }}显示器</span></div>
316
+ <div class="info-row" v-for="g in gpu.gpus" :key="g.name">
317
+ <span class="label" style="font-size:12px;">{{ g.name }}</span>
318
+ <span class="value" :style="{ color: g.isVirtual ? '#666' : '#e0e0e0' }">{{ displayModeText(g.displayMode) }}</span>
319
+ </div>
320
+ <div class="info-row" v-for="g in gpu.gpus.filter(x => !x.isVirtual)" :key="'detail-' + g.name">
321
+ <span class="label">显存/驱动</span>
322
+ <span class="value">{{ g.adapterRAMMB ? g.adapterRAMMB + 'MB' : '--' }} / {{ g.driverVersion || '--' }}</span>
323
+ </div>
324
+ </div>
325
+
326
+ <!-- 系统信息 -->
327
+ <div class="card">
328
+ <div class="card-title">系统信息</div>
329
+ <div class="info-row"><span class="label">操作系统</span><span class="value">{{ system.os?.caption }} (build {{ system.os?.buildNumber }})</span></div>
330
+ <div class="info-row"><span class="label">CPU</span><span class="value" style="font-size:12px;">{{ system.cpu?.name }}</span></div>
331
+ <div class="info-row"><span class="label">核心/线程</span><span class="value">{{ system.cpu?.numberOfCores }}核 / {{ system.cpu?.numberOfLogicalProcessors }}线程</span></div>
332
+ <div class="info-row"><span class="label">缓存</span><span class="value">L2 {{ system.cpu?.l2CacheKB }}KB / L3 {{ system.cpu?.l3CacheKB }}KB</span></div>
333
+ <div class="info-row"><span class="label">计算机</span><span class="value">{{ system.computer?.manufacturer }} {{ system.computer?.model }}</span></div>
334
+ <div class="info-row"><span class="label">主板</span><span class="value">{{ system.motherboard?.manufacturer }} {{ system.motherboard?.product }}</span></div>
335
+ <div class="info-row"><span class="label">BIOS</span><span class="value">{{ system.bios?.version }} ({{ system.bios?.releaseDate?.slice(0,10) }})</span></div>
336
+ <div class="info-row"><span class="label">开机时长</span><span class="value">{{ system.uptimeDays }}天</span></div>
337
+ <div class="info-row"><span class="label">电源计划</span><span class="value">{{ system.powerPlan || '--' }}</span></div>
338
+ </div>
339
+
340
+ <!-- 进程 Top 8(按CPU) -->
341
+ <div class="card">
342
+ <div class="card-title">进程 Top 8(CPU)<span class="badge">共{{ processes.total }}个 | {{ processes.backend }}</span></div>
343
+
344
+ <!-- 进程操作工具条 -->
345
+ <div class="proc-toolbar">
346
+ <div class="proc-toolbar-row">
347
+ <span class="proc-toolbar-label">端口</span>
348
+ <input class="proc-input" v-model="portInput" placeholder="如 3000" style="width:70px;" />
349
+ <select class="proc-select" v-model="portProtocol">
350
+ <option value="all">all</option>
351
+ <option value="tcp">tcp</option>
352
+ <option value="udp">udp</option>
353
+ </select>
354
+ <button class="btn-mini" @click="findByPort">查PID</button>
355
+ <button class="btn-mini btn-danger" @click="killByPort">杀</button>
356
+ </div>
357
+ <div class="proc-toolbar-row">
358
+ <span class="proc-toolbar-label">名称</span>
359
+ <input class="proc-input" v-model="nameInput" placeholder="如 notepad / chrome" style="width:160px;" />
360
+ <button class="btn-mini" @click="findByName">查PID</button>
361
+ <button class="btn-mini btn-danger" @click="killByName">杀</button>
362
+ </div>
363
+ <div class="proc-toolbar-result" v-if="procLookupResult">{{ procLookupResult }}</div>
364
+ </div>
365
+
366
+ <div class="process-list">
367
+ <div class="process-item" v-for="p in processes.processes" :key="p.name">
368
+ <img v-if="p.exePath && iconCache[p.exePath]" class="process-icon" :src="`data:image/png;base64,${iconCache[p.exePath]}`" />
369
+ <div v-else class="process-icon process-icon-placeholder"></div>
370
+ <div class="process-info">
371
+ <span class="process-name" style="cursor:pointer;" @click="expandProcess(p)" title="点击查看实例明细">{{ p.name }} <span style="color:#666;font-size:11px;">{{ p.pids?.length || 1 }}实例 | {{ p.threads }}线程</span></span>
372
+ <span class="process-cpu" :style="{ color: p.cpuPercent > 50 ? '#ff6b6b' : p.cpuPercent > 20 ? '#ffd93d' : '#6bcb77' }">{{ p.cpuPercent?.toFixed(1) }}%</span>
373
+ </div>
374
+ <div class="process-actions">
375
+ <span class="process-mem">{{ p.memoryMB?.toFixed(0) }}MB</span>
376
+ <button class="btn-mini" @click="revealProcess(p)" title="打开文件位置">📂</button>
377
+ <button class="btn-mini btn-danger" @click="killProcess(p)" title="结束进程">✕</button>
378
+ </div>
379
+ </div>
380
+ </div>
381
+ </div>
382
+
383
+ <!-- 磁盘事件 -->
384
+ <div class="card">
385
+ <div class="card-title">磁盘插拔事件<span class="badge">{{ diskEvents.length }}条</span></div>
386
+ <div class="disk-event-list" v-if="diskEvents.length">
387
+ <div class="disk-event" v-for="(e, i) in diskEvents.slice(-5).reverse()" :key="i">
388
+ <span class="disk-event-time">{{ e.time?.slice(11,19) }}</span>
389
+ <span class="disk-event-type" :class="e.added?.length ? 'add' : 'remove'">{{ e.added?.length ? '插入' : '拔出' }}</span>
390
+ <span class="disk-event-disks">{{ (e.added || e.removed)?.join(', ') }}</span>
391
+ </div>
392
+ </div>
393
+ <div class="empty-hint" v-else>暂无磁盘插拔事件(插入/拔出 U 盘时会显示)</div>
394
+ </div>
395
+ </div>
396
+
397
+ <!-- 详细信息区(Tab 切换,覆盖所有后端 func) -->
398
+ <div class="detail-section">
399
+ <div class="detail-tabs">
400
+ <button class="detail-tab" v-for="t in detailTabs" :key="t.key" :class="{ active: activeDetailTab === t.key }" @click="switchDetailTab(t.key)">{{ t.label }}</button>
401
+ </div>
402
+ <div class="detail-content">
403
+
404
+ <!-- 内存条详情 -->
405
+ <div v-if="activeDetailTab === 'memory-sticks'">
406
+ <div class="detail-title">内存条详情 <span class="badge">{{ memory.stickCount }}条 / 最大支持 {{ memory.array?.maxCapacityGB }}GB</span></div>
407
+ <table class="detail-table" v-if="memory.sticks?.length">
408
+ <thead><tr><th>插槽</th><th>容量</th><th>频率</th><th>厂商</th><th>型号</th><th>位置</th></tr></thead>
409
+ <tbody>
410
+ <tr v-for="s in memory.sticks" :key="s.deviceLocator">
411
+ <td>{{ s.deviceLocator || '--' }}</td>
412
+ <td>{{ s.capacityGB }}GB</td>
413
+ <td>{{ s.speedMHz }}MHz</td>
414
+ <td>{{ s.manufacturer || '--' }}</td>
415
+ <td style="font-size:11px;">{{ s.partNumber || '--' }}</td>
416
+ <td style="font-size:11px;">{{ s.bankLabel || '--' }}</td>
417
+ </tr>
418
+ </tbody>
419
+ </table>
420
+ <div class="empty-hint" v-else>暂无内存条数据</div>
421
+ </div>
422
+
423
+ <!-- 物理磁盘详情 -->
424
+ <div v-if="activeDetailTab === 'physical-disks'">
425
+ <div class="detail-title">物理磁盘详情 <span class="badge">{{ disk.physicalDisks?.length || 0 }}块</span></div>
426
+ <table class="detail-table" v-if="disk.physicalDisks?.length">
427
+ <thead><tr><th>磁盘</th><th>型号</th><th>容量</th><th>接口</th><th>盘符</th><th>读速度</th><th>写速度</th><th>活跃</th></tr></thead>
428
+ <tbody>
429
+ <tr v-for="pd in disk.physicalDisks" :key="pd.index">
430
+ <td>磁盘{{ pd.index }}</td>
431
+ <td style="font-size:11px;">{{ pd.model || '--' }}</td>
432
+ <td>{{ pd.sizeGB }}GB</td>
433
+ <td>{{ pd.interfaceType || '--' }}</td>
434
+ <td>{{ pd.drives?.join(',') || '--' }}</td>
435
+ <td>{{ formatSpeed(pd.readBytesPerSec) }}</td>
436
+ <td>{{ formatSpeed(pd.writeBytesPerSec) }}</td>
437
+ <td>{{ pd.percentActive != null ? pd.percentActive.toFixed(0) + '%' : '--' }}</td>
438
+ </tr>
439
+ </tbody>
440
+ </table>
441
+ <div class="empty-hint" v-else>暂无物理磁盘数据</div>
442
+ </div>
443
+
444
+ <!-- USB 设备 -->
445
+ <div v-if="activeDetailTab === 'usb'">
446
+ <div class="detail-title">USB 设备 <span class="badge">{{ usb.deviceCount }}设备 / {{ usb.controllerCount }}控制器</span></div>
447
+ <div class="usb-list" v-if="usb.devices?.length">
448
+ <div class="usb-item" v-for="d in usb.devices" :key="d.name">
449
+ <span class="usb-name">{{ d.name }}</span>
450
+ <span class="usb-status" :class="d.status === 'OK' ? 'ok' : 'warn'">{{ d.status }}</span>
451
+ </div>
452
+ </div>
453
+ <div class="empty-hint" v-else>暂无 USB 设备</div>
454
+ </div>
455
+
456
+ <!-- 服务列表 -->
457
+ <div v-if="activeDetailTab === 'services'">
458
+ <div class="detail-title">服务列表 <span class="badge">运行 {{ services.running }} / 停止 {{ services.stopped }} / 共 {{ services.total }}</span></div>
459
+ <div class="service-filters">
460
+ <button class="filter-btn" :class="{active: serviceFilter === 'all'}" @click="serviceFilter = 'all'">全部</button>
461
+ <button class="filter-btn" :class="{active: serviceFilter === 'running'}" @click="serviceFilter = 'running'">运行中</button>
462
+ <button class="filter-btn" :class="{active: serviceFilter === 'stopped'}" @click="serviceFilter = 'stopped'">已停止</button>
463
+ </div>
464
+ <div class="service-list" v-if="filteredServices.length">
465
+ <div class="service-item" v-for="s in filteredServices" :key="s.name">
466
+ <span class="service-dot" :class="s.state === 'Running' ? 'running' : 'stopped'"></span>
467
+ <span class="service-name">{{ s.displayName || s.name }}</span>
468
+ <span class="service-state">{{ s.state === 'Running' ? '运行中' : '已停止' }}</span>
469
+ </div>
470
+ </div>
471
+ <div class="empty-hint" v-else>暂无服务数据</div>
472
+ </div>
473
+
474
+ <!-- OHM 全硬件传感器 -->
475
+ <div v-if="activeDetailTab === 'ohm'">
476
+ <div class="detail-title">OHM 全硬件传感器 <span class="badge">{{ allHardware.hardware?.length || 0 }}硬件</span></div>
477
+ <div class="ohm-list" v-if="allHardware.hardware?.length">
478
+ <div class="ohm-hardware" v-for="h in allHardware.hardware" :key="h.name">
479
+ <div class="ohw-hw-title">[{{ h.hardwareType }}] {{ h.name }} <span class="badge">{{ h.sensors?.length || 0 }}传感器</span></div>
480
+ <div class="ohw-sensor" v-for="s in h.sensors" :key="s.name + s.sensorType">
481
+ <span class="ohw-sensor-name">{{ s.name }} [{{ s.sensorType }}]</span>
482
+ <span class="ohw-sensor-value">{{ s.value }}</span>
483
+ </div>
484
+ </div>
485
+ </div>
486
+ <div class="empty-hint" v-else>暂无 OHM 数据(需管理员权限获取真实传感器)</div>
487
+ </div>
488
+
489
+ <!-- 服务状态 -->
490
+ <div v-if="activeDetailTab === 'monitor'">
491
+ <div class="detail-title">统一服务状态</div>
492
+ <div class="monitor-status" v-if="monitorStatus">
493
+ <div class="info-row"><span class="label">就绪</span><span class="value">{{ monitorStatus.ready ? '是' : '否' }}</span></div>
494
+ <div class="info-row"><span class="label">SysInfoDaemon</span><span class="value" :class="monitorStatus.status?.sysInfoReady ? 'ok' : 'warn'">{{ monitorStatus.status?.sysInfoReady ? '运行中' : '未就绪' }}</span></div>
495
+ <div class="info-row"><span class="label">CPU温度监控</span><span class="value" :class="monitorStatus.status?.cpuTempRunning ? 'ok' : 'warn'">{{ monitorStatus.status?.cpuTempRunning ? '运行中' : '未启动' }}</span></div>
496
+ <div class="info-row"><span class="label">CPU温度数据</span><span class="value">{{ monitorStatus.status?.cpuTempHasData ? '有数据' : '无数据' }}</span></div>
497
+ <div class="info-row"><span class="label">磁盘监听</span><span class="value" :class="monitorStatus.status?.diskWatcherRunning ? 'ok' : 'warn'">{{ monitorStatus.status?.diskWatcherRunning ? '运行中' : '未启动' }}</span></div>
498
+ <div class="info-row"><span class="label">当前盘符</span><span class="value">{{ monitorStatus.status?.currentDisks?.join(', ') || '--' }}</span></div>
499
+ </div>
500
+
501
+ <!-- 后端资源占用 -->
502
+ <div class="detail-title" style="margin-top:20px;">后端资源占用 <span class="badge">{{ backendUsage?.total?.processCount }}进程</span></div>
503
+ <div class="monitor-status" v-if="backendUsage">
504
+ <div class="info-row"><span class="label">Node.js (PID {{ backendUsage.node?.pid }})</span><span class="value">{{ backendUsage.node?.memory?.workingSetMB }}MB | CPU {{ backendUsage.node?.cpuPercent }}% | {{ backendUsage.node?.threads }}线程</span></div>
505
+ <div class="info-row"><span class="label">堆内存</span><span class="value">{{ backendUsage.node?.memory?.heapUsedMB }} / {{ backendUsage.node?.memory?.heapTotalMB }}MB</span></div>
506
+ <div class="info-row"><span class="label">子进程 ({{ backendUsage.children?.length }}个)</span><span class="value">{{ backendUsage.children?.reduce((s,c)=>s+c.memoryMB,0).toFixed(1) }}MB | CPU {{ backendUsage.children?.reduce((s,c)=>s+c.cpuPercent,0).toFixed(1) }}%</span></div>
507
+ <div class="info-row"><span class="label" style="color:#4ade80;font-weight:600;">总计</span><span class="value" style="color:#4ade80;font-weight:600;">{{ backendUsage.total?.memoryMB }}MB | CPU {{ backendUsage.total?.cpuPercent }}% | {{ backendUsage.total?.threads }}线程</span></div>
508
+ <table class="detail-table" v-if="backendUsage.children?.length" style="margin-top:10px;">
509
+ <thead><tr><th>进程</th><th>PID</th><th>内存</th><th>CPU</th><th>线程</th></tr></thead>
510
+ <tbody>
511
+ <tr v-for="c in backendUsage.children" :key="c.pid">
512
+ <td>{{ c.name }}</td>
513
+ <td>{{ c.pid }}</td>
514
+ <td>{{ c.memoryMB }}MB</td>
515
+ <td>{{ c.cpuPercent }}%</td>
516
+ <td>{{ c.threads }}</td>
517
+ </tr>
518
+ </tbody>
519
+ </table>
520
+ </div>
521
+ <div class="empty-hint" v-else>暂无服务状态</div>
522
+ </div>
523
+
524
+ <!-- 进程实例明细 -->
525
+ <div v-if="activeDetailTab === 'process-detail'">
526
+ <div class="detail-title">进程实例明细 <span class="badge">点击进程卡片的名称展开</span></div>
527
+ <div v-if="expandedProcess">
528
+ <div class="info-row"><span class="label">进程名</span><span class="value">{{ expandedProcess.name }}</span></div>
529
+ <div class="info-row"><span class="label">总内存</span><span class="value">{{ expandedProcess.memoryMB?.toFixed(1) }}MB ({{ expandedProcess.percent }}%)</span></div>
530
+ <div class="info-row"><span class="label">CPU</span><span class="value">{{ expandedProcess.cpuPercent?.toFixed(1) }}%</span></div>
531
+ <div class="info-row"><span class="label">实例数</span><span class="value">{{ expandedProcess.pids?.length || 1 }}</span></div>
532
+ <div class="info-row"><span class="label">主进程路径</span><span class="value" style="font-size:11px;word-break:break-all;">{{ expandedProcess.exePath || '--' }}</span></div>
533
+ <div class="info-row" v-if="expandedProcess.installTime"><span class="label">安装时间</span><span class="value">{{ expandedProcess.installTime }}</span></div>
534
+ <table class="detail-table" v-if="expandedProcess.instances?.length" style="margin-top:12px;">
535
+ <thead><tr><th>PID</th><th>内存</th><th>CPU</th><th>父PID</th><th>线程</th></tr></thead>
536
+ <tbody>
537
+ <tr v-for="inst in expandedProcess.instances" :key="inst.pid">
538
+ <td>{{ inst.pid }}</td>
539
+ <td>{{ inst.memoryMB?.toFixed(1) }}MB</td>
540
+ <td>{{ inst.cpuPercent?.toFixed(1) }}%</td>
541
+ <td>{{ inst.parentPid || '--' }}</td>
542
+ <td>{{ inst.threads || '--' }}</td>
543
+ </tr>
544
+ </tbody>
545
+ </table>
546
+ </div>
547
+ <div class="empty-hint" v-else>点击进程卡片上的进程名查看实例明细</div>
548
+ </div>
549
+
550
+ </div>
551
+ </div>
552
+
553
+ <!-- 原始数据调试面板 -->
554
+ <div class="raw-panel" v-if="showRawData">
555
+ <div class="raw-tabs">
556
+ <button class="raw-tab" v-for="t in rawTabs" :key="t.key" :class="{ active: activeRawTab === t.key }" @click="activeRawTab = t.key">{{ t.label }}</button>
557
+ </div>
558
+ <pre class="raw-json">{{ rawJson }}</pre>
559
+ </div>
560
+
561
+ <div class="footer">
562
+ hardware-info | 常驻进程模式 | 动态数据每 {{ intervalSec.toFixed(1) }} 秒刷新 | 静态数据每 {{ staticIntervalSec }} 秒刷新
563
+ </div>
564
+ </div>
565
+
566
+ <script>
567
+ const { createApp, ref, reactive, computed, watch, onMounted, onUnmounted } = Vue;
568
+
569
+ createApp({
570
+ setup() {
571
+ // 刷新间隔(秒)
572
+ const intervalSec = ref(0.5);
573
+ const isPaused = ref(false);
574
+ const clock = ref('');
575
+ const showRawData = ref(false);
576
+ const activeRawTab = ref('system');
577
+
578
+ // 数据
579
+ const cpuTemp = reactive({ temperature: {}, source: '' });
580
+ const memory = reactive({});
581
+ const network = reactive({ adapters: [] });
582
+ const disk = reactive({ logicalDrives: [] });
583
+ const battery = reactive({});
584
+ const gpu = reactive({ gpus: [] });
585
+ const system = reactive({ os: {}, cpu: {}, computer: {} });
586
+ const processes = reactive({ processes: [], total: 0, backend: '' });
587
+ const publicIp = ref('');
588
+ const usb = reactive({ devices: [] });
589
+ const services = reactive({ services: [] });
590
+ const allData = reactive({});
591
+ const diskEvents = ref([]);
592
+ const iconCache = reactive({}); // exePath -> base64
593
+
594
+ // 详细信息 Tab
595
+ const detailTabs = [
596
+ { key: 'memory-sticks', label: '内存条' },
597
+ { key: 'physical-disks', label: '物理磁盘' },
598
+ { key: 'usb', label: 'USB设备' },
599
+ { key: 'services', label: '服务列表' },
600
+ { key: 'ohm', label: 'OHM传感器' },
601
+ { key: 'process-detail', label: '进程明细' },
602
+ { key: 'monitor', label: '服务状态' }
603
+ ];
604
+ const activeDetailTab = ref('memory-sticks');
605
+ const allHardware = reactive({ hardware: [] });
606
+ const monitorStatus = ref(null);
607
+ const backendUsage = ref(null);
608
+ const serviceFilter = ref('all');
609
+ const expandedProcess = ref(null);
610
+ const portInput = ref('');
611
+ const portProtocol = ref('all');
612
+ const nameInput = ref('');
613
+ const procLookupResult = ref('');
614
+
615
+ const filteredServices = computed(() => {
616
+ if (!services.services?.length) return [];
617
+ if (serviceFilter.value === 'running') return services.services.filter(s => s.state === 'Running');
618
+ if (serviceFilter.value === 'stopped') return services.services.filter(s => s.state !== 'Running');
619
+ return services.services;
620
+ });
621
+
622
+ async function fetchAllHardware() {
623
+ try {
624
+ const res = await fetch('/api/all-hardware');
625
+ const json = await res.json();
626
+ if (json.success) Object.assign(allHardware, json.data);
627
+ } catch (e) { console.error('OHM数据获取失败:', e); }
628
+ }
629
+
630
+ async function fetchMonitorStatus() {
631
+ try {
632
+ const res = await fetch('/api/status');
633
+ const json = await res.json();
634
+ if (json.success) monitorStatus.value = json;
635
+ } catch (e) { console.error('服务状态获取失败:', e); }
636
+ try {
637
+ const res2 = await fetch('/api/backend-usage');
638
+ const json2 = await res2.json();
639
+ if (json2.success) backendUsage.value = json2.data;
640
+ } catch (e) { console.error('后端占用获取失败:', e); }
641
+ }
642
+
643
+ function switchDetailTab(key) {
644
+ activeDetailTab.value = key;
645
+ if (key === 'ohm') fetchAllHardware();
646
+ if (key === 'monitor') fetchMonitorStatus();
647
+ if (key === 'process-detail' && !expandedProcess.value && processes.processes?.length) {
648
+ expandedProcess.value = processes.processes[0];
649
+ }
650
+ }
651
+
652
+ function expandProcess(p) {
653
+ expandedProcess.value = p;
654
+ activeDetailTab.value = 'process-detail';
655
+ }
656
+
657
+ // 定时器
658
+ let dynamicTimer = null;
659
+ let staticTimer = null;
660
+ let clockTimer = null;
661
+
662
+ // 静态数据间隔 = 动态的 5 倍,最小 10 秒
663
+ const staticIntervalSec = computed(() => Math.max(intervalSec.value * 5, 10).toFixed(0));
664
+
665
+ // ========== 数据获取 ==========
666
+ async function fetchCpuTemp() {
667
+ try {
668
+ const res = await fetch('/api/cpu-temp');
669
+ const json = await res.json();
670
+ if (json.success) Object.assign(cpuTemp, json.data);
671
+ } catch (e) { console.error('CPU温度获取失败:', e); }
672
+ }
673
+
674
+ async function fetchDynamic() {
675
+ try {
676
+ const [memRes, netRes, diskRes, procRes] = await Promise.all([
677
+ fetch('/api/memory'), fetch('/api/network'), fetch('/api/disk'),
678
+ fetch('/api/processes?top=8&sort=cpu&group=1')
679
+ ]);
680
+ const [mem, net, dsk, proc] = await Promise.all([memRes.json(), netRes.json(), diskRes.json(), procRes.json()]);
681
+ if (mem.success) Object.assign(memory, mem.data);
682
+ if (net.success) Object.assign(network, net.data);
683
+ if (dsk.success) Object.assign(disk, dsk.data);
684
+ if (proc.success) {
685
+ Object.assign(processes, proc.data);
686
+ // 批量获取新进程图标(不在缓存中的)
687
+ const newPaths = proc.data.processes
688
+ .map(p => p.exePath)
689
+ .filter(p => p && !iconCache[p]);
690
+ if (newPaths.length) {
691
+ fetch('/api/processes/icons', {
692
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
693
+ body: JSON.stringify({ paths: newPaths })
694
+ }).then(r => r.json()).then(json => {
695
+ if (json.success) Object.assign(iconCache, json.icons);
696
+ }).catch(() => {});
697
+ }
698
+ }
699
+ } catch (e) { console.error('动态数据获取失败:', e); }
700
+ }
701
+
702
+ async function fetchStatic() {
703
+ try {
704
+ const [sysRes, batRes, gpuRes, ipRes, usbRes, svcRes] = await Promise.all([
705
+ fetch('/api/system'), fetch('/api/battery'), fetch('/api/gpu'), fetch('/api/public-ip'), fetch('/api/usb'),
706
+ fetch('/api/services')
707
+ ]);
708
+ const [sys, bat, g, ip, u, svc] = await Promise.all([sysRes.json(), batRes.json(), gpuRes.json(), ipRes.json(), usbRes.json(), svcRes.json()]);
709
+ if (sys.success) Object.assign(system, sys.data);
710
+ if (bat.success) Object.assign(battery, bat.data);
711
+ if (g.success) Object.assign(gpu, g.data);
712
+ if (ip.success && ip.ip) publicIp.value = ip.ip;
713
+ if (u.success) Object.assign(usb, u.data);
714
+ if (svc.success) Object.assign(services, svc.data);
715
+ } catch (e) { console.error('静态数据获取失败:', e); }
716
+ }
717
+
718
+ // All 汇总数据(切换到 All tab 时获取)
719
+ async function fetchAllData() {
720
+ try {
721
+ const res = await fetch('/api/all');
722
+ const data = await res.json();
723
+ if (data.success) Object.assign(allData, data.data);
724
+ } catch (e) { console.error('All数据获取失败:', e); }
725
+ }
726
+
727
+ // 杀进程
728
+ async function killProcess(p) {
729
+ if (!confirm(`确定结束进程 "${p.name}" 吗?`)) return;
730
+ try {
731
+ const pid = p.pids?.[0] || p.pid;
732
+ const res = await fetch('/api/processes/kill', {
733
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
734
+ body: JSON.stringify({ pid })
735
+ });
736
+ const json = await res.json();
737
+ if (json.success) alert(`已结束 ${p.name}`);
738
+ else alert('结束失败: ' + (json.error || ''));
739
+ } catch (e) { alert('操作失败: ' + e.message); }
740
+ }
741
+
742
+ // 按端口杀进程
743
+ async function killByPort() {
744
+ const port = parseInt(portInput.value, 10);
745
+ if (!port || port < 1 || port > 65535) { procLookupResult.value = '无效的端口号'; return; }
746
+ if (!confirm(`确定结束占用端口 ${port} 的进程吗?`)) return;
747
+ try {
748
+ const res = await fetch('/api/processes/kill', {
749
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
750
+ body: JSON.stringify({ port, protocol: portProtocol.value })
751
+ });
752
+ const json = await res.json();
753
+ if (json.success) procLookupResult.value = `已结束端口 ${port} 的 ${json.killed} 个进程 (PID: ${json.pids?.join(', ') || '无'})`;
754
+ else procLookupResult.value = '失败: ' + (json.message || json.error || '');
755
+ } catch (e) { procLookupResult.value = '操作失败: ' + e.message; }
756
+ }
757
+
758
+ // 按名称杀进程
759
+ async function killByName() {
760
+ const name = nameInput.value.trim();
761
+ if (!name) { procLookupResult.value = '请输入进程名'; return; }
762
+ if (!confirm(`确定结束所有名为 "${name}" 的进程吗?`)) return;
763
+ try {
764
+ const res = await fetch('/api/processes/kill', {
765
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
766
+ body: JSON.stringify({ name })
767
+ });
768
+ const json = await res.json();
769
+ if (json.success) procLookupResult.value = `已结束 ${json.killed} 个 "${name}" 进程 (PID: ${json.pids?.join(', ') || '无'})`;
770
+ else procLookupResult.value = '失败: ' + (json.message || json.error || '');
771
+ } catch (e) { procLookupResult.value = '操作失败: ' + e.message; }
772
+ }
773
+
774
+ // 查找占用端口的 PID
775
+ async function findByPort() {
776
+ const port = parseInt(portInput.value, 10);
777
+ if (!port || port < 1 || port > 65535) { procLookupResult.value = '无效的端口号'; return; }
778
+ try {
779
+ const res = await fetch(`/api/processes/find-by-port?port=${port}&protocol=${portProtocol.value}`);
780
+ const json = await res.json();
781
+ if (json.success && json.pids?.length) procLookupResult.value = `端口 ${port} (${portProtocol.value}) 被 ${json.pids.length} 个进程占用: PID ${json.pids.join(', ')}`;
782
+ else procLookupResult.value = `端口 ${port} 未被占用`;
783
+ } catch (e) { procLookupResult.value = '查询失败: ' + e.message; }
784
+ }
785
+
786
+ // 按名称查找 PID
787
+ async function findByName() {
788
+ const name = nameInput.value.trim();
789
+ if (!name) { procLookupResult.value = '请输入进程名'; return; }
790
+ try {
791
+ const res = await fetch(`/api/processes/find-by-name?name=${encodeURIComponent(name)}`);
792
+ const json = await res.json();
793
+ if (json.success && json.pids?.length) procLookupResult.value = `"${name}" 找到 ${json.pids.length} 个进程: PID ${json.pids.join(', ')}`;
794
+ else procLookupResult.value = `未找到名为 "${name}" 的进程`;
795
+ } catch (e) { procLookupResult.value = '查询失败: ' + e.message; }
796
+ }
797
+
798
+ // 打开文件所在位置
799
+ async function revealProcess(p) {
800
+ try {
801
+ const exePath = p.exePath;
802
+ if (!exePath) { alert('未找到进程路径(系统进程或权限不足)'); return; }
803
+ await fetch('/api/processes/reveal', {
804
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
805
+ body: JSON.stringify({ path: exePath })
806
+ });
807
+ } catch (e) { alert('操作失败: ' + e.message); }
808
+ }
809
+
810
+ // SSE 磁盘事件监听
811
+ let diskEventSource = null;
812
+ function initDiskEvents() {
813
+ try {
814
+ diskEventSource = new EventSource('/api/disk-events');
815
+ diskEventSource.onmessage = (e) => {
816
+ const data = JSON.parse(e.data);
817
+ if (data.type === 'history') {
818
+ diskEvents.value = data.events || [];
819
+ } else {
820
+ diskEvents.value.push(data);
821
+ if (diskEvents.value.length > 50) diskEvents.value.shift();
822
+ }
823
+ };
824
+ } catch (e) { console.error('磁盘事件SSE初始化失败:', e); }
825
+ }
826
+ watch(activeRawTab, (val) => {
827
+ if (val === 'all') fetchAllData();
828
+ });
829
+
830
+ // ========== 定时器控制 ==========
831
+ function clearTimers() {
832
+ if (dynamicTimer) clearInterval(dynamicTimer);
833
+ if (staticTimer) clearInterval(staticTimer);
834
+ dynamicTimer = null;
835
+ staticTimer = null;
836
+ }
837
+
838
+ function setupTimers() {
839
+ clearTimers();
840
+ if (isPaused.value) return;
841
+ const dynamicMs = intervalSec.value * 1000;
842
+ const staticMs = Math.max(dynamicMs * 5, 10000);
843
+ dynamicTimer = setInterval(() => { fetchCpuTemp(); fetchDynamic(); }, dynamicMs);
844
+ staticTimer = setInterval(fetchStatic, staticMs);
845
+ }
846
+
847
+ // 监听间隔变化
848
+ watch(intervalSec, () => {
849
+ if (isPaused.value) {
850
+ isPaused.value = false; // 拖动滑块自动恢复
851
+ }
852
+ setupTimers();
853
+ });
854
+
855
+ // 暂停/继续
856
+ function togglePause() {
857
+ isPaused.value = !isPaused.value;
858
+ if (isPaused.value) {
859
+ clearTimers();
860
+ } else {
861
+ setupTimers();
862
+ // 恢复后立即刷新
863
+ fetchCpuTemp();
864
+ fetchDynamic();
865
+ fetchStatic();
866
+ }
867
+ }
868
+
869
+ // ========== 工具函数 ==========
870
+ function tempClass(temp) {
871
+ if (temp >= 80) return 'temp-high';
872
+ if (temp >= 60) return 'temp-mid';
873
+ return 'temp-low';
874
+ }
875
+
876
+ function barClass(percent) {
877
+ if (percent >= 85) return 'fill-red';
878
+ if (percent >= 60) return 'fill-yellow';
879
+ return 'fill-green';
880
+ }
881
+
882
+ function formatSpeed(bytes) {
883
+ if (bytes == null) return '--';
884
+ if (bytes >= 1048576) return (bytes / 1048576).toFixed(2) + ' MB/s';
885
+ if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB/s';
886
+ return bytes.toFixed(0) + ' B/s';
887
+ }
888
+
889
+ function displayModeText(mode) {
890
+ if (!mode) return '--';
891
+ const map = { 'Virtual Display': '虚拟显示器', 'No Active Output': '无活动输出' };
892
+ return map[mode] || mode;
893
+ }
894
+
895
+ // ========== 生命周期 ==========
896
+ onMounted(() => {
897
+ // 时钟
898
+ clockTimer = setInterval(() => {
899
+ clock.value = new Date().toLocaleString('zh-CN', { hour12: false });
900
+ }, 1000);
901
+ clock.value = new Date().toLocaleString('zh-CN', { hour12: false });
902
+
903
+ // 初始数据
904
+ fetchCpuTemp();
905
+ fetchDynamic();
906
+ fetchStatic();
907
+
908
+ // 启动定时器
909
+ setupTimers();
910
+
911
+ // 磁盘插拔事件 SSE
912
+ initDiskEvents();
913
+ });
914
+
915
+ onUnmounted(() => {
916
+ clearTimers();
917
+ if (clockTimer) clearInterval(clockTimer);
918
+ if (diskEventSource) diskEventSource.close();
919
+ });
920
+
921
+ // 辅助计算
922
+ const cpuLoad = computed(() => {
923
+ const total = cpuTemp.load?.find(l => l.name === 'CPU Total');
924
+ return total ? total.value : null;
925
+ });
926
+ const cpuClockAvg = computed(() => {
927
+ const cores = cpuTemp.clock?.filter(c => c.name.startsWith('CPU Core'));
928
+ if (!cores || !cores.length) return null;
929
+ return cores.reduce((s, c) => s + c.value, 0) / cores.length;
930
+ });
931
+ const cpuPower = computed(() => {
932
+ const pkg = cpuTemp.power?.find(p => p.name === 'CPU Package');
933
+ return pkg ? pkg.value : null;
934
+ });
935
+ const diskReadSpeed = computed(() => {
936
+ return disk.physicalDisks?.reduce((s, d) => s + (d.readBytesPerSec || 0), 0) || 0;
937
+ });
938
+ const diskWriteSpeed = computed(() => {
939
+ return disk.physicalDisks?.reduce((s, d) => s + (d.writeBytesPerSec || 0), 0) || 0;
940
+ });
941
+
942
+ // 原始数据调试面板
943
+ const rawTabs = [
944
+ { key: 'system', label: 'System' },
945
+ { key: 'cpuTemp', label: 'CPU温度' },
946
+ { key: 'memory', label: 'Memory' },
947
+ { key: 'disk', label: 'Disk' },
948
+ { key: 'network', label: 'Network' },
949
+ { key: 'battery', label: 'Battery' },
950
+ { key: 'gpu', label: 'GPU' },
951
+ { key: 'usb', label: 'USB' },
952
+ { key: 'processes', label: 'Processes' },
953
+ { key: 'services', label: 'Services' },
954
+ { key: 'publicIp', label: 'PublicIP' },
955
+ { key: 'all', label: 'All汇总' },
956
+ ];
957
+ const rawDataMap = {
958
+ get system() { return system; },
959
+ get cpuTemp() { return cpuTemp; },
960
+ get memory() { return memory; },
961
+ get disk() { return disk; },
962
+ get network() { return network; },
963
+ get battery() { return battery; },
964
+ get gpu() { return gpu; },
965
+ get usb() { return usb; },
966
+ get processes() { return processes; },
967
+ get services() { return services; },
968
+ get publicIp() { return { ip: publicIp.value }; },
969
+ get all() { return allData; },
970
+ };
971
+ const rawJson = computed(() => {
972
+ const data = rawDataMap[activeRawTab.value];
973
+ if (!data) return '(暂无数据)';
974
+ try { return JSON.stringify(data, null, 2); }
975
+ catch (e) { return String(data); }
976
+ });
977
+
978
+ return {
979
+ intervalSec, isPaused, clock,
980
+ cpuTemp, memory, network, disk, battery, gpu, system, processes, publicIp,
981
+ usb, services, allData, diskEvents, iconCache,
982
+ detailTabs, activeDetailTab, allHardware, monitorStatus, backendUsage, serviceFilter, expandedProcess,
983
+ filteredServices, switchDetailTab, expandProcess,
984
+ staticIntervalSec,
985
+ togglePause, tempClass, barClass, formatSpeed, displayModeText,
986
+ cpuLoad, cpuClockAvg, cpuPower, diskReadSpeed, diskWriteSpeed,
987
+ showRawData, activeRawTab, rawTabs, rawJson,
988
+ killProcess, revealProcess,
989
+ portInput, portProtocol, nameInput, procLookupResult,
990
+ findByPort, findByName, killByPort, killByName
991
+ };
992
+ }
993
+ }).mount('#app');
994
+ </script>
995
+ </body>
996
+ </html>