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