nv-log-bw 1.0.0 → 1.0.1

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.
@@ -0,0 +1,2011 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>NV Log Viewer 示例</title>
5
+ <script>class NvLogViewer extends HTMLElement {
6
+ static get observedAttributes() {
7
+ return ['max-logs', 'theme', 'show-time', 'time-format', 'auto-scroll', 'show-stats', 'hide-button', 'auto-destroy', 'draggable'];
8
+ }
9
+
10
+ constructor() {
11
+ super();
12
+ this._state = {
13
+ logs: [],
14
+ showLogs: true,
15
+ filters: {
16
+ info: true,
17
+ success: true,
18
+ warning: true,
19
+ error: true,
20
+ ignore: true
21
+ },
22
+ isOpen: false,
23
+ isMinimized: false,
24
+ originalPosition: { x: 0, y: 0 },
25
+ originalSize: { width: '800px', height: '600px' },
26
+ dragInfo: {
27
+ isDragging: false,
28
+ startX: 0,
29
+ startY: 0,
30
+ initialLeft: 0,
31
+ initialTop: 0
32
+ },
33
+ isDraggingEnabled: true
34
+ };
35
+
36
+ this._eventListeners = new Map();
37
+
38
+ // 创建外部文件选择器
39
+ this._createFileInput();
40
+
41
+ this.attachShadow({ mode: 'open' });
42
+ this._config = this._getConfig();
43
+ this._render();
44
+ this._setupEventListeners();
45
+ }
46
+
47
+ attributeChangedCallback(name, oldValue, newValue) {
48
+ if (oldValue === newValue) return;
49
+
50
+ switch (name) {
51
+ case 'max-logs':
52
+ this._config.maxLogs = Math.max(1, parseInt(newValue, 10) || 100);
53
+ this._trimLogs();
54
+ this._updateLogsDisplay();
55
+ this._updateStats();
56
+ break;
57
+ case 'theme':
58
+ this._config.theme = newValue === 'light' ? 'light' : 'dark';
59
+ this._updateTheme();
60
+ break;
61
+ case 'show-time':
62
+ this._config.showTime = newValue !== 'false';
63
+ this._updateLogsDisplay();
64
+ break;
65
+ case 'time-format':
66
+ this._config.timeFormat = newValue || 'HH:mm:ss';
67
+ this._updateLogsDisplay();
68
+ break;
69
+ case 'auto-scroll':
70
+ this._config.autoScroll = newValue !== 'false';
71
+ break;
72
+ case 'show-stats':
73
+ this._config.showStats = newValue !== 'false';
74
+ this._updateStatsVisibility();
75
+ break;
76
+ case 'hide-button':
77
+ this._config.hideButton = newValue !== 'false';
78
+ this._updateButtonsVisibility();
79
+ break;
80
+ case 'auto-destroy':
81
+ this._config.autoDestroy = newValue !== 'false';
82
+ this._updateCloseButton();
83
+ break;
84
+ case 'draggable':
85
+ this._config.draggable = newValue !== 'false';
86
+ this._updateDraggableState();
87
+ break;
88
+ }
89
+ }
90
+
91
+ connectedCallback() {
92
+ this._config = this._getConfig();
93
+ this._render();
94
+ this._setupEventListeners();
95
+
96
+ if (this._config.autoDestroy) {
97
+ window.addEventListener('beforeunload', () => this._cleanup());
98
+ }
99
+ }
100
+
101
+ disconnectedCallback() {
102
+ this._cleanup();
103
+ }
104
+
105
+ _createFileInput() {
106
+ this._fileInput = document.createElement('input');
107
+ this._fileInput.type = 'file';
108
+ this._fileInput.accept = '.json,.txt,.log';
109
+ this._fileInput.style.position = 'fixed';
110
+ this._fileInput.style.top = '-1000px';
111
+ this._fileInput.style.left = '-1000px';
112
+ this._fileInput.style.opacity = '0';
113
+ this._fileInput.style.pointerEvents = 'none';
114
+ this._fileInput.style.zIndex = '-1000';
115
+
116
+ this._fileInput.addEventListener('change', (e) => this._handleFileImport(e));
117
+ document.body.appendChild(this._fileInput);
118
+ }
119
+
120
+ _getConfig() {
121
+ return {
122
+ maxLogs: Math.max(1, parseInt(this.getAttribute('max-logs') || 100, 10)),
123
+ theme: this.getAttribute('theme') === 'light' ? 'light' : 'dark',
124
+ showTime: this.getAttribute('show-time') !== 'false',
125
+ timeFormat: this.getAttribute('time-format') || 'HH:mm:ss',
126
+ autoScroll: this.getAttribute('auto-scroll') !== 'false',
127
+ showStats: this.getAttribute('show-stats') !== 'false',
128
+ hideButton: this.getAttribute('hide-button') === 'true',
129
+ autoDestroy: this.getAttribute('auto-destroy') === 'true',
130
+ draggable: this.getAttribute('draggable') !== 'false'
131
+ };
132
+ }
133
+
134
+ _trimLogs() {
135
+ if (this._state.logs.length > this._config.maxLogs) {
136
+ this._state.logs = this._state.logs.slice(-this._config.maxLogs);
137
+ }
138
+ }
139
+
140
+ _updateLogsDisplay() {
141
+ const logContent = this.shadowRoot.querySelector('#logContent');
142
+ if (!logContent) return;
143
+
144
+ const filteredLogs = this._state.logs.filter(log => this._state.filters[log.type]);
145
+
146
+ if (filteredLogs.length === 0) {
147
+ logContent.innerHTML = '<div class="empty-state">📝 没有日志记录</div>';
148
+ return;
149
+ }
150
+
151
+ logContent.innerHTML = '';
152
+
153
+ filteredLogs.forEach(log => {
154
+ const logEntry = document.createElement('div');
155
+ logEntry.className = `log-entry ${log.type}`;
156
+
157
+ let timeText = '';
158
+ if (this._config.showTime) {
159
+ const date = new Date(log.time);
160
+ if (this._config.timeFormat === 'relative') {
161
+ timeText = this._getRelativeTime(log.time);
162
+ } else if (this._config.timeFormat === 'HH:mm:ss') {
163
+ timeText = date.toLocaleTimeString('zh-CN', { hour12: false });
164
+ } else if (this._config.timeFormat === 'HH:mm:ss.ms') {
165
+ const time = date.toLocaleTimeString('zh-CN', { hour12: false });
166
+ const ms = String(date.getMilliseconds()).padStart(3, '0');
167
+ timeText = `${time}.${ms}`;
168
+ } else {
169
+ timeText = date.toLocaleString('zh-CN');
170
+ }
171
+ }
172
+
173
+ logEntry.innerHTML = `
174
+ ${this._config.showTime ? `<span class="log-time">${timeText}</span>` : ''}
175
+ <span class="log-message">${this._escapeHtml(String(log.message))}</span>
176
+ `;
177
+
178
+ logContent.appendChild(logEntry);
179
+ });
180
+
181
+ if (this._config.autoScroll) {
182
+ logContent.scrollTop = logContent.scrollHeight;
183
+ }
184
+ }
185
+
186
+ _escapeHtml(text) {
187
+ const div = document.createElement('div');
188
+ div.textContent = text;
189
+ return div.innerHTML;
190
+ }
191
+
192
+ _getRelativeTime(timestamp) {
193
+ const now = Date.now();
194
+ const diff = now - timestamp;
195
+
196
+ if (diff < 60000) {
197
+ return `${Math.floor(diff / 1000)}秒前`;
198
+ } else if (diff < 3600000) {
199
+ return `${Math.floor(diff / 60000)}分钟前`;
200
+ } else if (diff < 86400000) {
201
+ return `${Math.floor(diff / 3600000)}小时前`;
202
+ } else {
203
+ return `${Math.floor(diff / 86400000)}天前`;
204
+ }
205
+ }
206
+
207
+ _updateDraggableState() {
208
+ const dragHandle = this.shadowRoot.querySelector('#dragHandle');
209
+ const logHeader = this.shadowRoot.querySelector('#logHeader');
210
+ const minimizedTitle = this.shadowRoot.querySelector('#minimizedTitle');
211
+ const popupContainer = this.shadowRoot.querySelector('#popupContainer');
212
+
213
+ if (dragHandle) {
214
+ if (this._config.draggable) {
215
+ dragHandle.style.display = 'block';
216
+ dragHandle.style.cursor = 'move';
217
+ if (logHeader) {
218
+ logHeader.style.cursor = 'move';
219
+ }
220
+ if (minimizedTitle) {
221
+ minimizedTitle.style.cursor = 'move';
222
+ }
223
+ if (popupContainer) {
224
+ popupContainer.style.cursor = 'default';
225
+ }
226
+ } else {
227
+ dragHandle.style.display = 'none';
228
+ dragHandle.style.cursor = 'default';
229
+ if (logHeader) {
230
+ logHeader.style.cursor = 'default';
231
+ }
232
+ if (minimizedTitle) {
233
+ minimizedTitle.style.cursor = 'default';
234
+ }
235
+ if (popupContainer) {
236
+ popupContainer.style.cursor = 'default';
237
+ }
238
+ }
239
+ }
240
+ }
241
+
242
+ _render() {
243
+ const style = document.createElement('style');
244
+ style.textContent = this._getStyles();
245
+
246
+ this.shadowRoot.innerHTML = '';
247
+ this.shadowRoot.appendChild(style);
248
+
249
+ this.shadowRoot.innerHTML += `
250
+ <!-- 浮动按钮 -->
251
+ <div id="floatingContainer" class="floating-container" style="display: ${this._config.hideButton ? 'none' : 'block'}">
252
+ ${!this._state.isOpen ? `
253
+ <div class="floating-button" id="floatingButton" title="打开日志">
254
+ 📄
255
+ </div>
256
+ ` : ''}
257
+ </div>
258
+
259
+ <!-- 弹出窗口 -->
260
+ <div class="popup-container" id="popupContainer" style="display: none;">
261
+ <!-- 完整的日志容器 -->
262
+ <div class="log-container ${this._state.showLogs ? 'expanded' : 'collapsed'} ${this._config.theme}">
263
+ <div class="log-header" id="logHeader">
264
+ <div class="log-title">
265
+ ${this._config.draggable ? '<span class="drag-handle" id="dragHandle" title="拖动">↔</span>' : ''}
266
+ <span class="log-icon">📄</span>
267
+ <h3>日志查看器</h3>
268
+ <span class="log-count" id="logCount">0</span>
269
+ </div>
270
+ <div class="log-actions">
271
+ <button class="toggle-btn" title="${this._state.showLogs ? '隐藏日志' : '显示日志'}">
272
+ ${this._state.showLogs ? '👁️' : '👁️‍🗨️'}
273
+ </button>
274
+ <button class="clear-btn" title="清空日志">🗑️</button>
275
+ <button class="export-btn" title="导出日志">📥</button>
276
+ <button class="import-btn" title="导入日志">📤</button>
277
+ <button class="copy-btn" title="复制日志">📋</button>
278
+ <button class="popup-btn" id="minimizeBtn" title="最小化">_</button>
279
+ <button class="popup-btn" id="closeBtn" title="${this._config.autoDestroy ? '关闭并销毁' : '隐藏'}">✕</button>
280
+ </div>
281
+ </div>
282
+
283
+ <div class="log-content" id="logContent">
284
+ <div class="empty-state">📝 没有日志记录</div>
285
+ </div>
286
+
287
+ <div class="log-footer">
288
+ <div class="log-filter">
289
+ <label class="filter-label">
290
+ <input type="checkbox" class="filter-checkbox" data-type="info" checked>
291
+ <span class="filter-badge info">信息</span>
292
+ </label>
293
+ <label class="filter-label">
294
+ <input type="checkbox" class="filter-checkbox" data-type="success" checked>
295
+ <span class="filter-badge success">成功</span>
296
+ </label>
297
+ <label class="filter-label">
298
+ <input type="checkbox" class="filter-checkbox" data-type="warning" checked>
299
+ <span class="filter-badge warning">警告</span>
300
+ </label>
301
+ <label class="filter-label">
302
+ <input type="checkbox" class="filter-checkbox" data-type="error" checked>
303
+ <span class="filter-badge error">错误</span>
304
+ </label>
305
+ <label class="filter-label">
306
+ <input type="checkbox" class="filter-checkbox" data-type="ignore" checked>
307
+ <span class="filter-badge ignore">忽略</span>
308
+ </label>
309
+ </div>
310
+ <div class="log-stats">
311
+ <div class="stat-row">
312
+ <span>总计: <span id="totalCount">0</span></span>
313
+ <span>信息: <span id="infoCount">0</span></span>
314
+ <span>成功: <span id="successCount">0</span></span>
315
+ <span>警告: <span id="warningCount">0</span></span>
316
+ <span>错误: <span id="errorCount">0</span></span>
317
+ <span>忽略: <span id="ignoreCount">0</span></span>
318
+ </div>
319
+ </div>
320
+ </div>
321
+ </div>
322
+
323
+ <!-- 最小化时的标题栏 -->
324
+ <div class="minimized-title" id="minimizedTitle" style="display: none;">
325
+ <div class="minimized-title-left">
326
+ <span>📄 日志查看器</span>
327
+ <span class="minimized-log-count" id="minimizedLogCount">0</span>
328
+ <div class="minimized-actions">
329
+ <button class="minimized-popup-btn" id="maximizeBtn" title="最大化">□</button>
330
+ <button class="minimized-popup-btn" id="closeMinimizedBtn" title="${this._config.autoDestroy ? '关闭并销毁' : '隐藏'}">✕</button>
331
+ </div>
332
+ </div>
333
+ </div>
334
+
335
+ <div class="resize-handle" id="resizeHandle"></div>
336
+ </div>
337
+ `;
338
+
339
+ this._updateTheme();
340
+ this._updateDraggableState();
341
+ }
342
+
343
+ _getStyles() {
344
+ return `
345
+ :host {
346
+ display: block;
347
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
348
+ font-size: 12px;
349
+ width: 100%;
350
+ }
351
+
352
+ /* 浮动按钮样式 */
353
+ .floating-container {
354
+ position: fixed;
355
+ bottom: 20px;
356
+ right: 20px;
357
+ z-index: 9998;
358
+ }
359
+
360
+ .floating-button {
361
+ width: 50px;
362
+ height: 50px;
363
+ border-radius: 50%;
364
+ background: #1890ff;
365
+ color: white;
366
+ display: flex;
367
+ align-items: center;
368
+ justify-content: center;
369
+ font-size: 20px;
370
+ cursor: pointer;
371
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
372
+ transition: all 0.3s ease;
373
+ user-select: none;
374
+ }
375
+
376
+ .floating-button:hover {
377
+ background: #40a9ff;
378
+ transform: scale(1.1);
379
+ }
380
+
381
+ .floating-button:active {
382
+ transform: scale(0.95);
383
+ }
384
+
385
+ /* 弹出窗口样式 */
386
+ .popup-container {
387
+ position: fixed;
388
+ z-index: 9999;
389
+ width: 800px;
390
+ height: 600px;
391
+ display: none;
392
+ border-radius: 8px;
393
+ overflow: hidden;
394
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
395
+ resize: both;
396
+ min-width: 400px;
397
+ min-height: 300px;
398
+ }
399
+
400
+ .popup-container.open {
401
+ display: block;
402
+ }
403
+
404
+ .popup-container.minimized {
405
+ width: 320px !important;
406
+ height: 40px !important;
407
+ min-height: 40px !important;
408
+ min-width: 320px !important;
409
+ resize: none;
410
+ border-radius: 6px;
411
+ display: block;
412
+ }
413
+
414
+ .popup-container.minimized .log-container {
415
+ display: none !important;
416
+ }
417
+
418
+ .popup-container.minimized .resize-handle {
419
+ display: none;
420
+ }
421
+
422
+ /* 最小化时的标题栏样式 */
423
+ .minimized-title {
424
+ position: absolute;
425
+ top: 0;
426
+ left: 0;
427
+ right: 0;
428
+ bottom: 0;
429
+ display: none;
430
+ align-items: center;
431
+ padding: 0 12px;
432
+ background: var(--header-bg);
433
+ color: var(--text-color);
434
+ font-size: 12px;
435
+ font-weight: 600;
436
+ cursor: move;
437
+ border-radius: 6px;
438
+ overflow: hidden;
439
+ z-index: 1000;
440
+ }
441
+
442
+ .popup-container.minimized .minimized-title {
443
+ display: flex;
444
+ }
445
+
446
+ .minimized-title-left {
447
+ display: flex;
448
+ align-items: center;
449
+ gap: 8px;
450
+ flex: 1;
451
+ }
452
+
453
+ .minimized-log-count {
454
+ background: var(--button-bg);
455
+ color: var(--button-text);
456
+ padding: 2px 8px;
457
+ border-radius: 10px;
458
+ font-size: 11px;
459
+ font-weight: 500;
460
+ min-width: 20px;
461
+ text-align: center;
462
+ }
463
+
464
+ .minimized-actions {
465
+ display: flex;
466
+ align-items: center;
467
+ gap: 4px;
468
+ margin-left: auto;
469
+ }
470
+
471
+ .minimized-popup-btn {
472
+ background: var(--button-bg);
473
+ border: none;
474
+ border-radius: 4px;
475
+ color: var(--button-text);
476
+ cursor: pointer;
477
+ font-size: 12px;
478
+ width: 24px;
479
+ height: 24px;
480
+ display: flex;
481
+ align-items: center;
482
+ justify-content: center;
483
+ transition: all 0.2s;
484
+ opacity: 0.8;
485
+ font-family: monospace;
486
+ }
487
+
488
+ .minimized-popup-btn:hover {
489
+ background: var(--button-hover);
490
+ transform: translateY(-1px);
491
+ opacity: 1;
492
+ }
493
+
494
+ .resize-handle {
495
+ position: absolute;
496
+ width: 20px;
497
+ height: 20px;
498
+ right: 0;
499
+ bottom: 0;
500
+ cursor: se-resize;
501
+ z-index: 10000;
502
+ }
503
+
504
+ .popup-container.minimized .resize-handle {
505
+ display: none;
506
+ }
507
+
508
+ /* 暗色主题 */
509
+ .log-container.dark {
510
+ --bg-color: #1e1e1e;
511
+ --header-bg: #252526;
512
+ --border-color: #3e3e42;
513
+ --text-color: #d4d4d4;
514
+ --text-muted: #8c8c8c;
515
+ --log-entry-bg: #2d2d30;
516
+ --hover-bg: #323234;
517
+ --info-color: #3794ff;
518
+ --info-bg: rgba(55, 148, 255, 0.1);
519
+ --success-color: #4ec9b0;
520
+ --success-bg: rgba(78, 201, 176, 0.1);
521
+ --warning-color: #dcdcaa;
522
+ --warning-bg: rgba(220, 220, 170, 0.1);
523
+ --error-color: #f48771;
524
+ --error-bg: rgba(244, 135, 113, 0.1);
525
+ --ignore-color: #8a8a8a;
526
+ --ignore-bg: rgba(138, 138, 138, 0.1);
527
+ --button-bg: #3e3e42;
528
+ --button-hover: #4a4a4e;
529
+ --button-text: #d4d4d4;
530
+ --filter-badge-bg: #3e3e42;
531
+ }
532
+
533
+ /* 亮色主题 */
534
+ .log-container.light {
535
+ --bg-color: #ffffff;
536
+ --header-bg: #f5f5f5;
537
+ --border-color: #e0e0e0;
538
+ --text-color: #333333;
539
+ --text-muted: #666666;
540
+ --log-entry-bg: #f9f9f9;
541
+ --hover-bg: #f0f0f0;
542
+ --info-color: #1890ff;
543
+ --info-bg: rgba(24, 144, 255, 0.08);
544
+ --success-color: #52c41a;
545
+ --success-bg: rgba(82, 196, 26, 0.08);
546
+ --warning-color: #faad14;
547
+ --warning-bg: rgba(250, 173, 20, 0.08);
548
+ --error-color: #ff4d4f;
549
+ --error-bg: rgba(255, 77, 79, 0.08);
550
+ --ignore-color: #999999;
551
+ --ignore-bg: rgba(153, 153, 153, 0.08);
552
+ --button-bg: #e8e8e8;
553
+ --button-hover: #d9d9d9;
554
+ --button-text: #595959;
555
+ --filter-badge-bg: #e8e8e8;
556
+ }
557
+
558
+ .log-container {
559
+ border: 1px solid var(--border-color);
560
+ border-radius: 8px;
561
+ overflow: hidden;
562
+ transition: all 0.3s ease;
563
+ background: var(--bg-color);
564
+ color: var(--text-color);
565
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
566
+ width: 100%;
567
+ height: 100%;
568
+ display: flex;
569
+ flex-direction: column;
570
+ }
571
+
572
+ .log-container.collapsed {
573
+ height: auto;
574
+ min-height: 0;
575
+ flex: 0 0 auto;
576
+ }
577
+
578
+ .log-container.collapsed .log-header {
579
+ border-bottom: none;
580
+ }
581
+
582
+ .log-container.collapsed .log-content,
583
+ .log-container.collapsed .log-footer {
584
+ display: none;
585
+ }
586
+
587
+ .log-container.expanded {
588
+ flex: 1;
589
+ min-height: 0;
590
+ }
591
+
592
+ .log-container.expanded .log-content,
593
+ .log-container.expanded .log-footer {
594
+ display: block;
595
+ }
596
+
597
+ .log-header {
598
+ display: flex;
599
+ justify-content: space-between;
600
+ align-items: center;
601
+ padding: 10px 16px;
602
+ background: var(--header-bg);
603
+ border-bottom: 1px solid var(--border-color);
604
+ user-select: none;
605
+ transition: background-color 0.2s;
606
+ flex-shrink: 0;
607
+ }
608
+
609
+ .log-header:hover {
610
+ background: var(--hover-bg);
611
+ }
612
+
613
+ .log-title {
614
+ display: flex;
615
+ align-items: center;
616
+ gap: 10px;
617
+ }
618
+
619
+ .drag-handle {
620
+ cursor: move;
621
+ opacity: 0.6;
622
+ font-size: 16px;
623
+ padding: 4px;
624
+ border-radius: 4px;
625
+ transition: all 0.2s;
626
+ }
627
+
628
+ .drag-handle:hover {
629
+ opacity: 1;
630
+ background: var(--button-bg);
631
+ }
632
+
633
+ .log-icon {
634
+ font-size: 16px;
635
+ opacity: 0.8;
636
+ }
637
+
638
+ .log-title h3 {
639
+ margin: 0;
640
+ font-size: 14px;
641
+ font-weight: 600;
642
+ }
643
+
644
+ .log-count {
645
+ background: var(--button-bg);
646
+ color: var(--button-text);
647
+ padding: 2px 8px;
648
+ border-radius: 10px;
649
+ font-size: 11px;
650
+ font-weight: 500;
651
+ min-width: 20px;
652
+ text-align: center;
653
+ }
654
+
655
+ .log-actions {
656
+ display: flex;
657
+ gap: 6px;
658
+ }
659
+
660
+ /* 按钮样式 */
661
+ .popup-btn,
662
+ .toggle-btn,
663
+ .clear-btn,
664
+ .export-btn,
665
+ .import-btn,
666
+ .copy-btn {
667
+ background: var(--button-bg);
668
+ border: none;
669
+ border-radius: 4px;
670
+ color: var(--button-text);
671
+ cursor: pointer;
672
+ font-size: 13px;
673
+ width: 28px;
674
+ height: 28px;
675
+ display: flex;
676
+ align-items: center;
677
+ justify-content: center;
678
+ transition: all 0.2s;
679
+ opacity: 0.8;
680
+ }
681
+
682
+ .popup-btn:hover,
683
+ .toggle-btn:hover,
684
+ .clear-btn:hover,
685
+ .export-btn:hover,
686
+ .import-btn:hover,
687
+ .copy-btn:hover {
688
+ background: var(--button-hover);
689
+ transform: translateY(-1px);
690
+ opacity: 1;
691
+ }
692
+
693
+ /* 日志内容样式 */
694
+ .log-content {
695
+ overflow-y: auto;
696
+ flex: 1;
697
+ padding: 8px;
698
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', Consolas, 'Courier New', monospace;
699
+ line-height: 1.5;
700
+ word-break: break-word;
701
+ white-space: pre-wrap;
702
+ scroll-behavior: smooth;
703
+ min-height: 0;
704
+ }
705
+
706
+ .log-entry {
707
+ margin-bottom: 4px;
708
+ padding: 8px 10px;
709
+ border-radius: 4px;
710
+ border-left: 3px solid transparent;
711
+ background: var(--log-entry-bg);
712
+ transition: all 0.2s ease;
713
+ animation: fadeIn 0.3s ease;
714
+ position: relative;
715
+ }
716
+
717
+ .log-entry:hover {
718
+ background: var(--hover-bg);
719
+ transform: translateX(2px);
720
+ }
721
+
722
+ .log-entry.info {
723
+ border-left-color: var(--info-color);
724
+ background: var(--info-bg);
725
+ }
726
+
727
+ .log-entry.info:hover {
728
+ background: var(--info-bg);
729
+ filter: brightness(1.1);
730
+ }
731
+
732
+ .log-entry.success {
733
+ border-left-color: var(--success-color);
734
+ background: var(--success-bg);
735
+ }
736
+
737
+ .log-entry.success:hover {
738
+ background: var(--success-bg);
739
+ filter: brightness(1.1);
740
+ }
741
+
742
+ .log-entry.warning {
743
+ border-left-color: var(--warning-color);
744
+ background: var(--warning-bg);
745
+ }
746
+
747
+ .log-entry.warning:hover {
748
+ background: var(--warning-bg);
749
+ filter: brightness(1.1);
750
+ }
751
+
752
+ .log-entry.error {
753
+ border-left-color: var(--error-color);
754
+ background: var(--error-bg);
755
+ }
756
+
757
+ .log-entry.error:hover {
758
+ background: var(--error-bg);
759
+ filter: brightness(1.1);
760
+ }
761
+
762
+ .log-entry.ignore {
763
+ border-left-color: var(--ignore-color);
764
+ background: var(--ignore-bg);
765
+ opacity: 0.7;
766
+ }
767
+
768
+ .log-entry.ignore:hover {
769
+ background: var(--ignore-bg);
770
+ filter: brightness(1.1);
771
+ opacity: 0.9;
772
+ }
773
+
774
+ .log-time {
775
+ color: var(--text-muted);
776
+ font-size: 10px;
777
+ margin-right: 10px;
778
+ user-select: none;
779
+ font-variant-numeric: tabular-nums;
780
+ }
781
+
782
+ .log-message {
783
+ font-size: 12px;
784
+ word-break: break-word;
785
+ }
786
+
787
+ .log-footer {
788
+ padding: 10px 16px;
789
+ border-top: 1px solid var(--border-color);
790
+ background: var(--header-bg);
791
+ font-size: 11px;
792
+ flex-shrink: 0;
793
+ }
794
+
795
+ .log-filter {
796
+ display: flex;
797
+ flex-wrap: wrap;
798
+ gap: 12px;
799
+ margin-bottom: 8px;
800
+ }
801
+
802
+ .filter-label {
803
+ display: flex;
804
+ align-items: center;
805
+ gap: 5px;
806
+ cursor: pointer;
807
+ color: var(--text-muted);
808
+ transition: opacity 0.2s;
809
+ }
810
+
811
+ .filter-label:hover {
812
+ opacity: 0.8;
813
+ }
814
+
815
+ .filter-checkbox {
816
+ margin: 0;
817
+ cursor: pointer;
818
+ }
819
+
820
+ .filter-badge {
821
+ padding: 2px 8px;
822
+ border-radius: 3px;
823
+ font-size: 10px;
824
+ font-weight: 500;
825
+ background: var(--filter-badge-bg);
826
+ }
827
+
828
+ .filter-badge.info {
829
+ background: var(--info-bg);
830
+ color: var(--info-color);
831
+ }
832
+
833
+ .filter-badge.success {
834
+ background: var(--success-bg);
835
+ color: var(--success-color);
836
+ }
837
+
838
+ .filter-badge.warning {
839
+ background: var(--warning-bg);
840
+ color: var(--warning-color);
841
+ }
842
+
843
+ .filter-badge.error {
844
+ background: var(--error-bg);
845
+ color: var(--error-color);
846
+ }
847
+
848
+ .filter-badge.ignore {
849
+ background: var(--ignore-bg);
850
+ color: var(--ignore-color);
851
+ }
852
+
853
+ .log-stats {
854
+ color: var(--text-muted);
855
+ }
856
+
857
+ .stat-row {
858
+ display: flex;
859
+ flex-wrap: wrap;
860
+ gap: 12px;
861
+ justify-content: space-between;
862
+ }
863
+
864
+ .stat-row span {
865
+ display: inline-flex;
866
+ align-items: center;
867
+ gap: 4px;
868
+ }
869
+
870
+ .stat-row #totalCount {
871
+ font-weight: 600;
872
+ color: var(--text-color);
873
+ }
874
+
875
+ .stat-row #infoCount {
876
+ color: var(--info-color);
877
+ font-weight: 600;
878
+ }
879
+
880
+ .stat-row #successCount {
881
+ color: var(--success-color);
882
+ font-weight: 600;
883
+ }
884
+
885
+ .stat-row #warningCount {
886
+ color: var(--warning-color);
887
+ font-weight: 600;
888
+ }
889
+
890
+ .stat-row #errorCount {
891
+ color: var(--error-color);
892
+ font-weight: 600;
893
+ }
894
+
895
+ .stat-row #ignoreCount {
896
+ color: var(--ignore-color);
897
+ font-weight: 600;
898
+ }
899
+
900
+ .empty-state {
901
+ text-align: center;
902
+ color: var(--text-muted);
903
+ padding: 40px 20px;
904
+ font-style: italic;
905
+ user-select: none;
906
+ }
907
+
908
+ .import-notice {
909
+ background: var(--info-bg);
910
+ border-left: 3px solid var(--info-color);
911
+ color: var(--text-color);
912
+ padding: 10px;
913
+ margin: 8px;
914
+ border-radius: 4px;
915
+ font-size: 11px;
916
+ animation: fadeIn 0.3s ease;
917
+ }
918
+
919
+ .import-notice.success {
920
+ background: var(--success-bg);
921
+ border-left-color: var(--success-color);
922
+ }
923
+
924
+ .import-notice.error {
925
+ background: var(--error-bg);
926
+ border-left-color: var(--error-color);
927
+ }
928
+
929
+ @keyframes fadeIn {
930
+ from {
931
+ opacity: 0;
932
+ transform: translateY(-5px);
933
+ }
934
+ to {
935
+ opacity: 1;
936
+ transform: translateY(0);
937
+ }
938
+ }
939
+
940
+ @keyframes pulse {
941
+ 0%, 100% {
942
+ opacity: 1;
943
+ }
944
+ 50% {
945
+ opacity: 0.5;
946
+ }
947
+ }
948
+
949
+ /* 滚动条样式 */
950
+ .log-content::-webkit-scrollbar {
951
+ width: 6px;
952
+ }
953
+
954
+ .log-content::-webkit-scrollbar-track {
955
+ background: var(--bg-color);
956
+ }
957
+
958
+ .log-content::-webkit-scrollbar-thumb {
959
+ background: var(--border-color);
960
+ border-radius: 3px;
961
+ }
962
+
963
+ .log-content::-webkit-scrollbar-thumb:hover {
964
+ background: var(--text-muted);
965
+ }
966
+ `;
967
+ }
968
+
969
+ _setupEventListeners() {
970
+ // 清理之前的事件监听器
971
+ this._eventListeners.forEach(({ type, handler, target }) => {
972
+ if (target && target.removeEventListener) {
973
+ target.removeEventListener(type, handler);
974
+ }
975
+ });
976
+ this._eventListeners.clear();
977
+
978
+ const clickHandler = (e) => {
979
+ if (e.target.classList.contains('toggle-btn')) {
980
+ e.stopPropagation();
981
+ e.preventDefault();
982
+ this.toggleVisibility();
983
+ } else if (e.target.classList.contains('clear-btn')) {
984
+ e.stopPropagation();
985
+ e.preventDefault();
986
+ this.clearLogs();
987
+ } else if (e.target.classList.contains('export-btn')) {
988
+ e.stopPropagation();
989
+ e.preventDefault();
990
+ this.exportLogs();
991
+ } else if (e.target.classList.contains('import-btn')) {
992
+ e.stopPropagation();
993
+ e.preventDefault();
994
+ this._triggerImport();
995
+ } else if (e.target.classList.contains('copy-btn')) {
996
+ e.stopPropagation();
997
+ e.preventDefault();
998
+ this.copyLogs();
999
+ } else if (e.target.classList.contains('filter-checkbox')) {
1000
+ e.stopPropagation();
1001
+ this._applyFilters();
1002
+ } else if (e.target.id === 'minimizeBtn') {
1003
+ e.stopPropagation();
1004
+ e.preventDefault();
1005
+ this._minimize();
1006
+ } else if (e.target.id === 'closeBtn') {
1007
+ e.stopPropagation();
1008
+ e.preventDefault();
1009
+ this.clos();
1010
+ } else if (e.target.id === 'closeMinimizedBtn') {
1011
+ e.stopPropagation();
1012
+ e.preventDefault();
1013
+ this.clos();
1014
+ } else if (e.target.id === 'maximizeBtn') {
1015
+ e.stopPropagation();
1016
+ e.preventDefault();
1017
+ this._restore();
1018
+ } else if (e.target.id === 'floatingButton') {
1019
+ e.stopPropagation();
1020
+ e.preventDefault();
1021
+ this.open();
1022
+ }
1023
+ };
1024
+
1025
+ this.shadowRoot.addEventListener('click', clickHandler);
1026
+ this._eventListeners.set('click', { type: 'click', handler: clickHandler, target: this.shadowRoot });
1027
+
1028
+ this._setupFloatingButtonDrag();
1029
+ this._setupDrag();
1030
+ this._setupResize();
1031
+ }
1032
+
1033
+ _setupFloatingButtonDrag() {
1034
+ const floatingButton = this.shadowRoot.querySelector('#floatingButton');
1035
+ const floatingContainer = this.shadowRoot.querySelector('#floatingContainer');
1036
+
1037
+ if (!floatingButton || !floatingContainer) return;
1038
+
1039
+ let isDragging = false;
1040
+ let startX, startY;
1041
+ let initialLeft, initialTop;
1042
+
1043
+ floatingButton.addEventListener('mousedown', (e) => {
1044
+ e.preventDefault();
1045
+ e.stopPropagation();
1046
+
1047
+ const rect = floatingContainer.getBoundingClientRect();
1048
+ startX = e.clientX;
1049
+ startY = e.clientY;
1050
+ initialLeft = rect.left;
1051
+ initialTop = rect.top;
1052
+
1053
+ isDragging = true;
1054
+
1055
+ const onMouseMove = (e) => {
1056
+ if (!isDragging) return;
1057
+
1058
+ const dx = e.clientX - startX;
1059
+ const dy = e.clientY - startY;
1060
+
1061
+ let newLeft = initialLeft + dx;
1062
+ let newTop = initialTop + dy;
1063
+
1064
+ const maxX = window.innerWidth - rect.width;
1065
+ const maxY = window.innerHeight - rect.height;
1066
+
1067
+ newLeft = Math.max(0, Math.min(newLeft, maxX));
1068
+ newTop = Math.max(0, Math.min(newTop, maxY));
1069
+
1070
+ floatingContainer.style.left = `${newLeft}px`;
1071
+ floatingContainer.style.top = `${newTop}px`;
1072
+ floatingContainer.style.right = 'auto';
1073
+ floatingContainer.style.bottom = 'auto';
1074
+ };
1075
+
1076
+ const onMouseUp = () => {
1077
+ isDragging = false;
1078
+ document.removeEventListener('mousemove', onMouseMove);
1079
+ document.removeEventListener('mouseup', onMouseUp);
1080
+ };
1081
+
1082
+ document.addEventListener('mousemove', onMouseMove);
1083
+ document.addEventListener('mouseup', onMouseUp);
1084
+ });
1085
+ }
1086
+
1087
+ _setupDrag() {
1088
+ if (!this._config.draggable) return;
1089
+
1090
+ const popupContainer = this.shadowRoot.querySelector('#popupContainer');
1091
+ const dragHandle = this.shadowRoot.querySelector('#dragHandle');
1092
+ const logHeader = this.shadowRoot.querySelector('#logHeader');
1093
+ const minimizedTitle = this.shadowRoot.querySelector('#minimizedTitle');
1094
+
1095
+ if (!popupContainer) return;
1096
+
1097
+ const startDrag = (e) => {
1098
+ e.preventDefault();
1099
+ e.stopPropagation();
1100
+
1101
+ const rect = popupContainer.getBoundingClientRect();
1102
+ this._state.dragInfo.isDragging = true;
1103
+ this._state.dragInfo.startX = e.clientX;
1104
+ this._state.dragInfo.startY = e.clientY;
1105
+ this._state.dragInfo.initialLeft = rect.left;
1106
+ this._state.dragInfo.initialTop = rect.top;
1107
+
1108
+ const onMouseMove = (e) => {
1109
+ if (!this._state.dragInfo.isDragging) return;
1110
+
1111
+ const dx = e.clientX - this._state.dragInfo.startX;
1112
+ const dy = e.clientY - this._state.dragInfo.startY;
1113
+
1114
+ let newLeft = this._state.dragInfo.initialLeft + dx;
1115
+ let newTop = this._state.dragInfo.initialTop + dy;
1116
+
1117
+ const maxX = window.innerWidth - rect.width;
1118
+ const maxY = window.innerHeight - rect.height;
1119
+
1120
+ newLeft = Math.max(0, Math.min(newLeft, maxX));
1121
+ newTop = Math.max(0, Math.min(newTop, maxY));
1122
+
1123
+ popupContainer.style.left = `${newLeft}px`;
1124
+ popupContainer.style.top = `${newTop}px`;
1125
+ popupContainer.style.transform = 'none';
1126
+ };
1127
+
1128
+ const onMouseUp = () => {
1129
+ this._state.dragInfo.isDragging = false;
1130
+ document.removeEventListener('mousemove', onMouseMove);
1131
+ document.removeEventListener('mouseup', onMouseUp);
1132
+ };
1133
+
1134
+ document.addEventListener('mousemove', onMouseMove);
1135
+ document.addEventListener('mouseup', onMouseUp);
1136
+ };
1137
+
1138
+ if (dragHandle) {
1139
+ dragHandle.addEventListener('mousedown', startDrag);
1140
+ }
1141
+
1142
+ if (logHeader) {
1143
+ logHeader.addEventListener('mousedown', (e) => {
1144
+ if (!this._config.draggable) return;
1145
+ if (e.target !== dragHandle && !e.target.classList.contains('toggle-btn') &&
1146
+ !e.target.classList.contains('clear-btn') && !e.target.classList.contains('export-btn') &&
1147
+ !e.target.classList.contains('import-btn') && !e.target.classList.contains('copy-btn') &&
1148
+ !e.target.classList.contains('popup-btn')) {
1149
+ startDrag(e);
1150
+ }
1151
+ });
1152
+ }
1153
+
1154
+ if (minimizedTitle) {
1155
+ minimizedTitle.addEventListener('mousedown', (e) => {
1156
+ if (!this._config.draggable) return;
1157
+ if (!e.target.classList.contains('minimized-popup-btn')) {
1158
+ startDrag(e);
1159
+ }
1160
+ });
1161
+ }
1162
+ }
1163
+
1164
+ _setupResize() {
1165
+ const container = this.shadowRoot.querySelector('#popupContainer');
1166
+ const resizeHandle = this.shadowRoot.querySelector('#resizeHandle');
1167
+
1168
+ if (!container || !resizeHandle) return;
1169
+
1170
+ let isResizing = false;
1171
+ let startWidth, startHeight, startX, startY;
1172
+
1173
+ const startResize = (e) => {
1174
+ if (container.classList.contains('minimized')) return;
1175
+
1176
+ isResizing = true;
1177
+ const rect = container.getBoundingClientRect();
1178
+ startWidth = rect.width;
1179
+ startHeight = rect.height;
1180
+ startX = e.clientX;
1181
+ startY = e.clientY;
1182
+
1183
+ e.preventDefault();
1184
+ };
1185
+
1186
+ const doResize = (e) => {
1187
+ if (!isResizing) return;
1188
+
1189
+ const width = startWidth + (e.clientX - startX);
1190
+ const height = startHeight + (e.clientY - startY);
1191
+
1192
+ container.style.width = `${Math.max(400, width)}px`;
1193
+ container.style.height = `${Math.max(300, height)}px`;
1194
+
1195
+ e.preventDefault();
1196
+ };
1197
+
1198
+ const stopResize = () => {
1199
+ isResizing = false;
1200
+ };
1201
+
1202
+ resizeHandle.addEventListener('mousedown', startResize);
1203
+ document.addEventListener('mousemove', doResize);
1204
+ document.addEventListener('mouseup', stopResize);
1205
+ }
1206
+
1207
+ _cleanup() {
1208
+ this._eventListeners.forEach(({ type, handler, target }) => {
1209
+ if (target && target.removeEventListener) {
1210
+ target.removeEventListener(type, handler);
1211
+ }
1212
+ });
1213
+ this._eventListeners.clear();
1214
+
1215
+ if (this._fileInput && this._fileInput.parentNode) {
1216
+ this._fileInput.parentNode.removeChild(this._fileInput);
1217
+ }
1218
+
1219
+ if (this._config.autoDestroy) {
1220
+ this.remove();
1221
+ }
1222
+ }
1223
+
1224
+ _updateTheme() {
1225
+ const container = this.shadowRoot.querySelector('.log-container');
1226
+ if (container) {
1227
+ container.className = `log-container ${this._state.showLogs ? 'expanded' : 'collapsed'} ${this._config.theme}`;
1228
+ }
1229
+ }
1230
+
1231
+ _updateStatsVisibility() {
1232
+ const logFooter = this.shadowRoot.querySelector('.log-footer');
1233
+ if (logFooter) {
1234
+ logFooter.style.display = this._config.showStats ? 'block' : 'none';
1235
+ }
1236
+ }
1237
+
1238
+ _updateButtonsVisibility() {
1239
+ const floatingContainer = this.shadowRoot.querySelector('#floatingContainer');
1240
+ if (floatingContainer) {
1241
+ if (this._config.hideButton) {
1242
+ floatingContainer.style.display = 'none';
1243
+ } else {
1244
+ floatingContainer.style.display = this._state.isOpen ? 'none' : 'block';
1245
+ }
1246
+ }
1247
+ }
1248
+
1249
+ _updateCloseButton() {
1250
+ const closeBtn = this.shadowRoot.querySelector('#closeBtn');
1251
+ const closeMinimizedBtn = this.shadowRoot.querySelector('#closeMinimizedBtn');
1252
+
1253
+ const title = this._config.autoDestroy ? '关闭并销毁' : '隐藏';
1254
+
1255
+ if (closeBtn) {
1256
+ closeBtn.title = title;
1257
+ }
1258
+ if (closeMinimizedBtn) {
1259
+ closeMinimizedBtn.title = title;
1260
+ }
1261
+ }
1262
+
1263
+ _updateStats() {
1264
+ const counts = {
1265
+ total: this._state.logs.length,
1266
+ info: 0,
1267
+ success: 0,
1268
+ warning: 0,
1269
+ error: 0,
1270
+ ignore: 0
1271
+ };
1272
+
1273
+ this._state.logs.forEach(log => {
1274
+ if (counts[log.type] !== undefined) {
1275
+ counts[log.type]++;
1276
+ }
1277
+ });
1278
+
1279
+ const logCount = this.shadowRoot.querySelector('#logCount');
1280
+ const minimizedLogCount = this.shadowRoot.querySelector('#minimizedLogCount');
1281
+ const totalCount = this.shadowRoot.querySelector('#totalCount');
1282
+ const infoCount = this.shadowRoot.querySelector('#infoCount');
1283
+ const successCount = this.shadowRoot.querySelector('#successCount');
1284
+ const warningCount = this.shadowRoot.querySelector('#warningCount');
1285
+ const errorCount = this.shadowRoot.querySelector('#errorCount');
1286
+ const ignoreCount = this.shadowRoot.querySelector('#ignoreCount');
1287
+
1288
+ if (logCount) logCount.textContent = counts.total;
1289
+ if (minimizedLogCount) minimizedLogCount.textContent = counts.total;
1290
+ if (totalCount) totalCount.textContent = counts.total;
1291
+ if (infoCount) infoCount.textContent = counts.info;
1292
+ if (successCount) successCount.textContent = counts.success;
1293
+ if (warningCount) warningCount.textContent = counts.warning;
1294
+ if (errorCount) errorCount.textContent = counts.error;
1295
+ if (ignoreCount) ignoreCount.textContent = counts.ignore;
1296
+ }
1297
+
1298
+ _applyFilters() {
1299
+ const checkboxes = this.shadowRoot.querySelectorAll('.filter-checkbox');
1300
+ checkboxes.forEach(checkbox => {
1301
+ const type = checkbox.dataset.type;
1302
+ this._state.filters[type] = checkbox.checked;
1303
+ });
1304
+
1305
+ this._updateLogsDisplay();
1306
+ }
1307
+
1308
+ _triggerImport() {
1309
+ this._fileInput.click();
1310
+ }
1311
+
1312
+ async _handleFileImport(event) {
1313
+ const file = event.target.files[0];
1314
+ if (!file) return;
1315
+
1316
+ try {
1317
+ this.addLog('开始导入日志文件...', 'info');
1318
+
1319
+ const text = await file.text();
1320
+ let importedLogs = [];
1321
+ let format = 'unknown';
1322
+
1323
+ try {
1324
+ const parsed = JSON.parse(text);
1325
+ if (Array.isArray(parsed)) {
1326
+ importedLogs = parsed;
1327
+ format = 'json';
1328
+ } else if (typeof parsed === 'object' && parsed.logs && Array.isArray(parsed.logs)) {
1329
+ importedLogs = parsed.logs;
1330
+ format = 'json-with-metadata';
1331
+ }
1332
+ } catch (e) {
1333
+ const lines = text.split('\n').filter(line => line.trim());
1334
+ importedLogs = this._parseTextLogs(lines);
1335
+ format = 'text';
1336
+ }
1337
+
1338
+ if (importedLogs.length === 0) {
1339
+ throw new Error('文件中没有找到有效的日志数据');
1340
+ }
1341
+
1342
+ const validLogs = importedLogs
1343
+ .map(log => this._normalizeLogEntry(log))
1344
+ .filter(log => log !== null);
1345
+
1346
+ if (validLogs.length === 0) {
1347
+ throw new Error('文件中没有有效的日志条目');
1348
+ }
1349
+
1350
+ this._state.logs.push(...validLogs);
1351
+ this._trimLogs();
1352
+
1353
+ this._updateLogsDisplay();
1354
+ this._updateStats();
1355
+
1356
+ this._showImportNotice(`成功导入 ${validLogs.length} 条日志 (${format}格式)`, 'success');
1357
+ this.addLog(`从文件导入 ${validLogs.length} 条日志`, 'success');
1358
+
1359
+ this.dispatchEvent(new CustomEvent('nv-log-import', {
1360
+ detail: {
1361
+ count: validLogs.length,
1362
+ format,
1363
+ logs: validLogs
1364
+ }
1365
+ }));
1366
+
1367
+ } catch (error) {
1368
+ console.error('导入日志失败:', error);
1369
+ this._showImportNotice(`导入失败: ${error.message}`, 'error');
1370
+ this.addLog(`导入日志失败: ${error.message}`, 'error');
1371
+ }
1372
+ }
1373
+
1374
+ _parseTextLogs(lines) {
1375
+ const logs = [];
1376
+ const timestampRegex = /^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?)\]/;
1377
+ const timeRegex = /^\[(\d{2}:\d{2}:\d{2}(?:\.\d+)?)\]/;
1378
+ const typeRegex = /\[(INFO|SUCCESS|WARNING|ERROR|IGNORE)\]/i;
1379
+
1380
+ lines.forEach((line, index) => {
1381
+ if (!line.trim()) return;
1382
+
1383
+ let time = Date.now();
1384
+ let type = 'info';
1385
+ let message = line;
1386
+
1387
+ let timeMatch = line.match(timestampRegex) || line.match(timeRegex);
1388
+ if (timeMatch) {
1389
+ try {
1390
+ time = new Date(timeMatch[1]).getTime() || Date.now() - (lines.length - index) * 1000;
1391
+ } catch (e) {
1392
+ time = Date.now() - (lines.length - index) * 1000;
1393
+ }
1394
+ message = line.substring(timeMatch[0].length).trim();
1395
+ }
1396
+
1397
+ const typeMatch = message.match(typeRegex);
1398
+ if (typeMatch) {
1399
+ const typeStr = typeMatch[1].toLowerCase();
1400
+ if (['info', 'success', 'warning', 'error', 'ignore'].includes(typeStr)) {
1401
+ type = typeStr;
1402
+ }
1403
+ message = message.replace(typeRegex, '').trim();
1404
+ }
1405
+
1406
+ logs.push({ time, type, message });
1407
+ });
1408
+
1409
+ return logs;
1410
+ }
1411
+
1412
+ _normalizeLogEntry(log) {
1413
+ if (log === null || log === undefined) {
1414
+ return null;
1415
+ }
1416
+
1417
+ if (typeof log === 'string') {
1418
+ return {
1419
+ time: Date.now(),
1420
+ type: 'info',
1421
+ message: log
1422
+ };
1423
+ }
1424
+
1425
+ if (typeof log === 'object') {
1426
+ const normalized = {
1427
+ time: log.time || log.timestamp || Date.now(),
1428
+ type: (log.type || log.level || 'info').toLowerCase(),
1429
+ message: String(log.message || log.msg || log.text || JSON.stringify(log))
1430
+ };
1431
+
1432
+ if (!['info', 'success', 'warning', 'error', 'ignore'].includes(normalized.type)) {
1433
+ normalized.type = 'info';
1434
+ }
1435
+
1436
+ if (typeof normalized.time !== 'number' || isNaN(normalized.time)) {
1437
+ normalized.time = Date.now();
1438
+ }
1439
+
1440
+ return normalized;
1441
+ }
1442
+
1443
+ return null;
1444
+ }
1445
+
1446
+ _showImportNotice(message, type = 'info') {
1447
+ const logContent = this.shadowRoot.querySelector('#logContent');
1448
+ if (!logContent) return;
1449
+
1450
+ const notice = document.createElement('div');
1451
+ notice.className = `import-notice ${type}`;
1452
+ notice.textContent = message;
1453
+
1454
+ logContent.appendChild(notice);
1455
+
1456
+ setTimeout(() => {
1457
+ if (notice.parentNode === logContent) {
1458
+ logContent.removeChild(notice);
1459
+ }
1460
+ }, 3000);
1461
+ }
1462
+
1463
+ /**
1464
+ * 添加日志
1465
+ */
1466
+ addLog(message, type = 'info', timestamp = Date.now()) {
1467
+ let normalizedLog;
1468
+
1469
+ if (typeof message === 'object' && message !== null) {
1470
+ normalizedLog = this._normalizeLogEntry(message);
1471
+ if (!normalizedLog) {
1472
+ console.warn('无效的日志对象:', message);
1473
+ return;
1474
+ }
1475
+ } else {
1476
+ if (!['info', 'success', 'warning', 'error', 'ignore'].includes(type)) {
1477
+ type = 'info';
1478
+ }
1479
+
1480
+ normalizedLog = {
1481
+ message: String(message),
1482
+ type,
1483
+ time: timestamp
1484
+ };
1485
+ }
1486
+
1487
+ this._state.logs.push(normalizedLog);
1488
+ this._trimLogs();
1489
+
1490
+ this._updateLogsDisplay();
1491
+ this._updateStats();
1492
+
1493
+ this.dispatchEvent(new CustomEvent('nv-log-add', {
1494
+ detail: {
1495
+ log: normalizedLog,
1496
+ total: this._state.logs.length,
1497
+ stats: this._getStats()
1498
+ }
1499
+ }));
1500
+ }
1501
+
1502
+ /**
1503
+ * 清空所有日志
1504
+ */
1505
+ clearLogs() {
1506
+ const clearedCount = this._state.logs.length;
1507
+ this._state.logs = [];
1508
+
1509
+ this._updateLogsDisplay();
1510
+ this._updateStats();
1511
+
1512
+ this.dispatchEvent(new CustomEvent('nv-log-clear', {
1513
+ detail: { clearedCount }
1514
+ }));
1515
+ }
1516
+
1517
+ /**
1518
+ * 切换显示/隐藏
1519
+ */
1520
+ toggleVisibility() {
1521
+ this._state.showLogs = !this._state.showLogs;
1522
+
1523
+ const container = this.shadowRoot.querySelector('.log-container');
1524
+ if (container) {
1525
+ container.className = `log-container ${this._state.showLogs ? 'expanded' : 'collapsed'} ${this._config.theme}`;
1526
+ }
1527
+
1528
+ const toggleBtn = this.shadowRoot.querySelector('.toggle-btn');
1529
+ if (toggleBtn) {
1530
+ toggleBtn.title = this._state.showLogs ? '隐藏日志' : '显示日志';
1531
+ toggleBtn.textContent = this._state.showLogs ? '👁️' : '👁️‍🗨️';
1532
+ }
1533
+
1534
+ this.dispatchEvent(new CustomEvent('nv-log-toggle', {
1535
+ detail: { visible: this._state.showLogs }
1536
+ }));
1537
+ }
1538
+
1539
+ /**
1540
+ * 导出日志为文件
1541
+ */
1542
+ exportLogs(format = 'json') {
1543
+ try {
1544
+ if (this._state.logs.length === 0) {
1545
+ this.addLog('没有日志可导出', 'warning');
1546
+ return;
1547
+ }
1548
+
1549
+ let content = '';
1550
+ let mimeType = '';
1551
+ let extension = '';
1552
+
1553
+ if (format === 'json') {
1554
+ const exportData = {
1555
+ exportTime: new Date().toISOString(),
1556
+ totalLogs: this._state.logs.length,
1557
+ logs: this._state.logs
1558
+ };
1559
+ content = JSON.stringify(exportData, null, 2);
1560
+ mimeType = 'application/json';
1561
+ extension = 'json';
1562
+ } else {
1563
+ content = this._state.logs.map(log => {
1564
+ const time = new Date(log.time).toLocaleString('zh-CN');
1565
+ return `[${time}] [${log.type.toUpperCase()}] ${log.message}`;
1566
+ }).join('\n');
1567
+ mimeType = 'text/plain;charset=utf-8';
1568
+ extension = 'txt';
1569
+ }
1570
+
1571
+ const blob = new Blob([content], { type: mimeType });
1572
+ const url = URL.createObjectURL(blob);
1573
+ const a = document.createElement('a');
1574
+
1575
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1576
+ a.href = url;
1577
+ a.download = `logs_${timestamp}.${extension}`;
1578
+ a.click();
1579
+
1580
+ URL.revokeObjectURL(url);
1581
+
1582
+ this.addLog(`日志已导出 (${this._state.logs.length} 条, ${format}格式)`, 'success');
1583
+
1584
+ } catch (error) {
1585
+ this.addLog(`导出日志失败: ${error.message}`, 'error');
1586
+ }
1587
+ }
1588
+
1589
+ /**
1590
+ * 导入日志
1591
+ */
1592
+ async importLogs(content, format = 'auto') {
1593
+ try {
1594
+ let importedLogs = [];
1595
+ let detectedFormat = 'unknown';
1596
+
1597
+ if (format === 'auto' || format === 'json') {
1598
+ try {
1599
+ const parsed = JSON.parse(content);
1600
+ if (Array.isArray(parsed)) {
1601
+ importedLogs = parsed;
1602
+ detectedFormat = 'json';
1603
+ } else if (typeof parsed === 'object' && parsed.logs && Array.isArray(parsed.logs)) {
1604
+ importedLogs = parsed.logs;
1605
+ detectedFormat = 'json-with-metadata';
1606
+ }
1607
+ } catch (e) {
1608
+ if (format === 'json') {
1609
+ throw new Error('JSON格式解析失败');
1610
+ }
1611
+ }
1612
+ }
1613
+
1614
+ if (importedLogs.length === 0) {
1615
+ const lines = content.split('\n').filter(line => line.trim());
1616
+ importedLogs = this._parseTextLogs(lines);
1617
+ detectedFormat = 'text';
1618
+ }
1619
+
1620
+ if (importedLogs.length === 0) {
1621
+ throw new Error('没有找到有效的日志数据');
1622
+ }
1623
+
1624
+ const validLogs = importedLogs
1625
+ .map(log => this._normalizeLogEntry(log))
1626
+ .filter(log => log !== null);
1627
+
1628
+ if (validLogs.length === 0) {
1629
+ throw new Error('没有有效的日志条目');
1630
+ }
1631
+
1632
+ this._state.logs.push(...validLogs);
1633
+ this._trimLogs();
1634
+
1635
+ this._updateLogsDisplay();
1636
+ this._updateStats();
1637
+
1638
+ this.addLog(`导入 ${validLogs.length} 条日志 (${detectedFormat}格式)`, 'success');
1639
+
1640
+ this.dispatchEvent(new CustomEvent('nv-log-import', {
1641
+ detail: {
1642
+ count: validLogs.length,
1643
+ format: detectedFormat,
1644
+ logs: validLogs
1645
+ }
1646
+ }));
1647
+
1648
+ return { success: true, count: validLogs.length, format: detectedFormat };
1649
+
1650
+ } catch (error) {
1651
+ console.error('导入日志失败:', error);
1652
+ this.addLog(`导入日志失败: ${error.message}`, 'error');
1653
+ throw error;
1654
+ }
1655
+ }
1656
+
1657
+ /**
1658
+ * 复制日志到剪贴板
1659
+ */
1660
+ async copyLogs() {
1661
+ try {
1662
+ const logsText = this._state.logs.map(log => {
1663
+ const time = new Date(log.time).toLocaleString('zh-CN');
1664
+ return `[${time}] [${log.type.toUpperCase()}] ${log.message}`;
1665
+ }).join('\n');
1666
+
1667
+ await navigator.clipboard.writeText(logsText);
1668
+ this.addLog('日志已复制到剪贴板', 'success');
1669
+ } catch (error) {
1670
+ this.addLog(`复制日志失败: ${error.message}`, 'error');
1671
+ }
1672
+ }
1673
+
1674
+ /**
1675
+ * 获取所有日志
1676
+ */
1677
+ getLogs() {
1678
+ return [...this._state.logs];
1679
+ }
1680
+
1681
+ /**
1682
+ * 获取过滤后的日志
1683
+ */
1684
+ getLogsByType(types) {
1685
+ if (!Array.isArray(types)) {
1686
+ types = [types];
1687
+ }
1688
+
1689
+ return this._state.logs.filter(log => types.includes(log.type));
1690
+ }
1691
+
1692
+ /**
1693
+ * 获取统计信息
1694
+ */
1695
+ _getStats() {
1696
+ const counts = {
1697
+ total: this._state.logs.length,
1698
+ info: 0,
1699
+ success: 0,
1700
+ warning: 0,
1701
+ error: 0,
1702
+ ignore: 0
1703
+ };
1704
+
1705
+ this._state.logs.forEach(log => {
1706
+ if (counts[log.type] !== undefined) {
1707
+ counts[log.type]++;
1708
+ }
1709
+ });
1710
+
1711
+ return counts;
1712
+ }
1713
+
1714
+ /**
1715
+ * 设置最大日志数量
1716
+ */
1717
+ setMaxLogs(max) {
1718
+ this._config.maxLogs = Math.max(1, parseInt(max, 10) || 100);
1719
+ this._trimLogs();
1720
+ this._updateLogsDisplay();
1721
+ this._updateStats();
1722
+ }
1723
+
1724
+ /**
1725
+ * 设置主题
1726
+ */
1727
+ setTheme(theme) {
1728
+ this._config.theme = theme === 'light' ? 'light' : 'dark';
1729
+ this._updateTheme();
1730
+ }
1731
+
1732
+ /**
1733
+ * 是否显示时间
1734
+ */
1735
+ setShowTime(show) {
1736
+ this._config.showTime = !!show;
1737
+ this._updateLogsDisplay();
1738
+ }
1739
+
1740
+ /**
1741
+ * 设置时间格式
1742
+ */
1743
+ setTimeFormat(format) {
1744
+ this._config.timeFormat = format;
1745
+ this._updateLogsDisplay();
1746
+ }
1747
+
1748
+ /**
1749
+ * 是否自动滚动
1750
+ */
1751
+ setAutoScroll(autoScroll) {
1752
+ this._config.autoScroll = !!autoScroll;
1753
+ }
1754
+
1755
+ /**
1756
+ * 是否显示统计
1757
+ */
1758
+ setShowStats(show) {
1759
+ this._config.showStats = !!show;
1760
+ this._updateStatsVisibility();
1761
+ }
1762
+
1763
+ /**
1764
+ * 设置是否隐藏按钮
1765
+ */
1766
+ setHideButton(hide) {
1767
+ this._config.hideButton = !!hide;
1768
+ this._updateButtonsVisibility();
1769
+ }
1770
+
1771
+ /**
1772
+ * 设置是否自动销毁
1773
+ */
1774
+ setAutoDestroy(autoDestroy) {
1775
+ this._config.autoDestroy = !!autoDestroy;
1776
+ this._updateCloseButton();
1777
+ }
1778
+
1779
+ /**
1780
+ * 设置是否启用拖拽
1781
+ */
1782
+ setDraggable(draggable) {
1783
+ this._config.draggable = !!draggable;
1784
+ this._updateDraggableState();
1785
+ }
1786
+
1787
+ /**
1788
+ * 打开日志查看器
1789
+ */
1790
+ open() {
1791
+ if (!this._state.isOpen) {
1792
+ const popupContainer = this.shadowRoot.querySelector('#popupContainer');
1793
+ if (popupContainer) {
1794
+ popupContainer.style.display = 'block';
1795
+ popupContainer.classList.add('open');
1796
+ popupContainer.classList.remove('minimized');
1797
+
1798
+ const minimizedTitle = this.shadowRoot.querySelector('#minimizedTitle');
1799
+ if (minimizedTitle) {
1800
+ minimizedTitle.style.display = 'none';
1801
+ }
1802
+
1803
+ const width = 800;
1804
+ const height = 600;
1805
+ const left = Math.max(0, (window.innerWidth - width) / 2);
1806
+ const top = Math.max(0, (window.innerHeight - height) / 2);
1807
+
1808
+ popupContainer.style.width = `${width}px`;
1809
+ popupContainer.style.height = `${height}px`;
1810
+ popupContainer.style.left = `${left}px`;
1811
+ popupContainer.style.top = `${top}px`;
1812
+ popupContainer.style.transform = 'none';
1813
+
1814
+ this._state.isMinimized = false;
1815
+ this._state.showLogs = true;
1816
+ this._updateLogsDisplay();
1817
+ this._updateTheme();
1818
+ this._updateDraggableState();
1819
+
1820
+ if (!this._config.hideButton) {
1821
+ const floatingContainer = this.shadowRoot.querySelector('#floatingContainer');
1822
+ if (floatingContainer) {
1823
+ floatingContainer.style.display = 'none';
1824
+ }
1825
+ }
1826
+ }
1827
+
1828
+ this._state.isOpen = true;
1829
+
1830
+ this.addLog('📁 日志查看器已打开', 'info');
1831
+ }
1832
+ }
1833
+
1834
+ /**
1835
+ * 关闭日志查看器
1836
+ */
1837
+ clos() {
1838
+ if (this._state.isOpen) {
1839
+ const popupContainer = this.shadowRoot.querySelector('#popupContainer');
1840
+ if (popupContainer) {
1841
+ this._state.originalPosition = {
1842
+ x: parseInt(popupContainer.style.left) || 0,
1843
+ y: parseInt(popupContainer.style.top) || 0
1844
+ };
1845
+
1846
+ popupContainer.classList.remove('open');
1847
+ popupContainer.style.display = 'none';
1848
+
1849
+ const minimizedTitle = this.shadowRoot.querySelector('#minimizedTitle');
1850
+ if (minimizedTitle) {
1851
+ minimizedTitle.style.display = 'none';
1852
+ }
1853
+ }
1854
+
1855
+ this._state.isOpen = false;
1856
+
1857
+ if (!this._config.hideButton) {
1858
+ const floatingContainer = this.shadowRoot.querySelector('#floatingContainer');
1859
+ if (floatingContainer) {
1860
+ floatingContainer.style.display = 'block';
1861
+ }
1862
+ }
1863
+
1864
+ this.addLog('📁 日志查看器已关闭', 'info');
1865
+
1866
+ if (this._config.autoDestroy) {
1867
+ setTimeout(() => {
1868
+ this._cleanup();
1869
+ }, 100);
1870
+ }
1871
+ }
1872
+ }
1873
+
1874
+ /**
1875
+ * 最小化日志查看器
1876
+ */
1877
+ _minimize() {
1878
+ const popupContainer = this.shadowRoot.querySelector('#popupContainer');
1879
+ const minimizedTitle = this.shadowRoot.querySelector('#minimizedTitle');
1880
+
1881
+ if (popupContainer && minimizedTitle) {
1882
+ this._state.originalSize = {
1883
+ width: popupContainer.style.width || '800px',
1884
+ height: popupContainer.style.height || '600px',
1885
+ minHeight: popupContainer.style.minHeight
1886
+ };
1887
+
1888
+ popupContainer.classList.add('minimized');
1889
+ popupContainer.style.width = '320px';
1890
+ popupContainer.style.height = '40px';
1891
+ popupContainer.style.minHeight = '40px';
1892
+ popupContainer.style.minWidth = '320px';
1893
+ minimizedTitle.style.display = 'flex';
1894
+
1895
+ this._state.isMinimized = true;
1896
+
1897
+ this.addLog('📱 日志查看器已最小化', 'info');
1898
+ }
1899
+ }
1900
+
1901
+ /**
1902
+ * 恢复日志查看器
1903
+ */
1904
+ _restore() {
1905
+ const popupContainer = this.shadowRoot.querySelector('#popupContainer');
1906
+ const minimizedTitle = this.shadowRoot.querySelector('#minimizedTitle');
1907
+
1908
+ if (popupContainer && minimizedTitle) {
1909
+ popupContainer.classList.remove('minimized');
1910
+ minimizedTitle.style.display = 'none';
1911
+
1912
+ if (this._state.originalSize) {
1913
+ popupContainer.style.width = this._state.originalSize.width;
1914
+ popupContainer.style.height = this._state.originalSize.height;
1915
+ if (this._state.originalSize.minHeight) {
1916
+ popupContainer.style.minHeight = this._state.originalSize.minHeight;
1917
+ } else {
1918
+ popupContainer.style.minHeight = '300px';
1919
+ }
1920
+ } else {
1921
+ popupContainer.style.width = '800px';
1922
+ popupContainer.style.height = '600px';
1923
+ popupContainer.style.minHeight = '300px';
1924
+ }
1925
+
1926
+ popupContainer.style.minWidth = '400px';
1927
+ this._state.isMinimized = false;
1928
+
1929
+ this.addLog('📱 日志查看器已恢复', 'info');
1930
+ }
1931
+ }
1932
+ }
1933
+
1934
+
1935
+ NvLogViewer.ELEMENT_NAME = 'nv-log-viewer';
1936
+
1937
+ /**
1938
+ * 单次使用的日志查看器
1939
+ * @param {Object} options 配置选项
1940
+ * @returns {Promise<NvLogViewer>} 日志查看器实例
1941
+ */
1942
+ const once = async (options = {}) => {
1943
+ let el = document.createElement('nv-log-viewer');
1944
+
1945
+ el.setAttribute("hide-button", options.hideButton !== false ? "true" : "false");
1946
+ el.setAttribute("auto-destroy", options.autoDestroy !== false ? "true" : "false");
1947
+
1948
+ if (options.maxLogs) el.setAttribute("max-logs", options.maxLogs);
1949
+ if (options.theme) el.setAttribute("theme", options.theme);
1950
+ if (options.showTime !== undefined) el.setAttribute("show-time", options.showTime);
1951
+ if (options.timeFormat) el.setAttribute("time-format", options.timeFormat);
1952
+ if (options.showStats !== undefined) el.setAttribute("show-stats", options.showStats);
1953
+ if (options.autoScroll !== undefined) el.setAttribute("auto-scroll", options.autoScroll);
1954
+
1955
+ document.body.appendChild(el);
1956
+
1957
+ await new Promise(resolve => setTimeout(resolve, 100));
1958
+
1959
+ el.open();
1960
+
1961
+ return el;
1962
+ };
1963
+
1964
+ // 注册组件
1965
+ if (!customElements.get('nv-log-viewer')) {
1966
+ customElements.define('nv-log-viewer', NvLogViewer);
1967
+ }
1968
+
1969
+ if (typeof module !== 'undefined' && module.exports) {
1970
+ module.exports = { NvLogViewer, once };
1971
+ }
1972
+ </script>
1973
+ </head>
1974
+ <body>
1975
+ <h1>NV Log Viewer 演示</h1>
1976
+
1977
+ <button onclick="testOnce()">打开一次性日志查看器</button>
1978
+ <button onclick="testManual()">手动创建日志查看器</button>
1979
+
1980
+ <script>
1981
+ async function testOnce() {
1982
+ const logger = await nvlogbw.once({
1983
+ maxLogs: 500,
1984
+ theme: 'dark',
1985
+ hideButton: true,
1986
+ autoDestroy: true
1987
+ });
1988
+
1989
+ logger.addLog('🎉 一次性日志查看器已打开', 'success');
1990
+ logger.addLog('🚀 可以正常记录日志', 'info');
1991
+ logger.addLog('⚠️ 关闭窗口将自动销毁', 'warning');
1992
+ }
1993
+
1994
+ async function testManual() {
1995
+ const logger = document.createElement('nv-log-viewer');
1996
+ logger.setAttribute('max-logs', '1000');
1997
+ logger.setAttribute('theme', 'light');
1998
+ logger.setAttribute('auto-destroy', 'false');
1999
+
2000
+ document.body.appendChild(logger);
2001
+ await new Promise(resolve => setTimeout(resolve, 100));
2002
+
2003
+ logger.open();
2004
+ logger.addLog('🔧 手动创建的日志查看器', 'info');
2005
+ logger.addLog('💾 关闭后可以重新打开', 'success');
2006
+
2007
+ window.logger = logger;
2008
+ }
2009
+ </script>
2010
+ </body>
2011
+ </html>