@bolloon/bolloon-agent 0.1.14 → 0.1.16

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.
@@ -1,657 +0,0 @@
1
- /**
2
- * P2P Modal Web Component
3
- */
4
- import { p2pManager } from './p2p-manager.js';
5
- export class P2PModal extends HTMLElement {
6
- shadow = this.attachShadow({ mode: 'open' });
7
- activeTab = 'identity';
8
- initialized = false;
9
- connectedCallback() {
10
- this.render();
11
- this.attachEvents();
12
- }
13
- show() {
14
- this.classList.add('show');
15
- if (!this.initialized) {
16
- this.initP2P();
17
- this.initialized = true;
18
- }
19
- }
20
- hide() {
21
- this.classList.remove('show');
22
- }
23
- async initP2P() {
24
- const statusIndicator = this.shadow.getElementById('p2p-status-indicator');
25
- const statusText = this.shadow.getElementById('p2p-status-text');
26
- statusIndicator?.classList.add('connecting');
27
- if (statusText)
28
- statusText.textContent = '初始化中...';
29
- try {
30
- const identity = await p2pManager.init();
31
- this.updateIdentityDisplay(identity);
32
- this.updateStatus('online', '已连接');
33
- statusIndicator?.classList.add('online');
34
- }
35
- catch (e) {
36
- this.updateStatus('error', '初始化失败');
37
- }
38
- }
39
- updateStatus(status, text) {
40
- const statusIndicator = this.shadow.getElementById('p2p-status-indicator');
41
- const statusText = this.shadow.getElementById('p2p-status-text');
42
- if (statusIndicator) {
43
- statusIndicator.className = 'status-indicator ' + status;
44
- }
45
- if (statusText)
46
- statusText.textContent = text;
47
- }
48
- updateIdentityDisplay(identity) {
49
- const didEl = this.shadow.getElementById('p2p-did');
50
- const cidEl = this.shadow.getElementById('p2p-cid');
51
- const nodeIdEl = this.shadow.getElementById('p2p-node-id');
52
- const shareLinkEl = this.shadow.getElementById('p2p-invite-link');
53
- const sharePanel = this.shadow.getElementById('p2p-share-panel');
54
- if (didEl && identity.did)
55
- didEl.textContent = identity.did;
56
- if (cidEl && identity.cid)
57
- cidEl.textContent = identity.cid;
58
- if (nodeIdEl && identity.irohNodeId)
59
- nodeIdEl.textContent = identity.irohNodeId;
60
- if (shareLinkEl && identity.did && identity.cid) {
61
- shareLinkEl.value = `bolloon://connect?did=${encodeURIComponent(identity.did)}&cid=${encodeURIComponent(identity.cid)}`;
62
- }
63
- if (sharePanel)
64
- sharePanel.style.display = 'block';
65
- }
66
- switchTab(tab) {
67
- this.activeTab = tab;
68
- this.render();
69
- this.attachEvents();
70
- this.loadActiveTab();
71
- }
72
- async loadActiveTab() {
73
- switch (this.activeTab) {
74
- case 'history':
75
- await this.loadHistory();
76
- break;
77
- case 'messages':
78
- await this.loadMessages();
79
- break;
80
- case 'connect':
81
- await this.loadPeers();
82
- break;
83
- }
84
- }
85
- async loadHistory() {
86
- const historyList = this.shadow.getElementById('p2p-history-list');
87
- if (!historyList)
88
- return;
89
- try {
90
- const history = await p2pManager.getHistory();
91
- this.renderHistory(history, historyList);
92
- }
93
- catch (e) {
94
- console.error('[P2P Modal] 加载历史失败:', e);
95
- }
96
- }
97
- renderHistory(history, container) {
98
- if (history.length === 0) {
99
- container.innerHTML = '<div class="empty-hint">暂无连接历史</div>';
100
- return;
101
- }
102
- const fragment = document.createDocumentFragment();
103
- history.forEach(item => {
104
- const div = document.createElement('div');
105
- div.className = `history-item ${item.isPinned ? 'pinned' : ''}`;
106
- div.innerHTML = `
107
- <div class="history-item-icon">💬</div>
108
- <div class="history-item-info">
109
- <div class="history-item-name">
110
- ${this.escapeHtml(item.name || 'Unknown')}
111
- ${item.isPinned ? '<span class="pin-icon">📌</span>' : ''}
112
- </div>
113
- <div class="history-item-meta">
114
- <span>上次: ${new Date(item.lastConnectedAt).toLocaleString()}</span>
115
- <span>消息: ${item.totalMessages || 0}</span>
116
- </div>
117
- </div>
118
- <div class="history-item-actions">
119
- <button class="btn-sm btn-secondary" data-action="connect" data-cid="${this.escapeHtml(item.cid)}">连接</button>
120
- <button class="btn-sm btn-secondary" data-action="pin" data-id="${item.id}" data-pinned="${!item.isPinned}">
121
- ${item.isPinned ? '取消置顶' : '置顶'}
122
- </button>
123
- <button class="btn-sm btn-secondary" data-action="delete" data-id="${item.id}">删除</button>
124
- </div>
125
- `;
126
- fragment.appendChild(div);
127
- });
128
- container.innerHTML = '';
129
- container.appendChild(fragment);
130
- this.attachHistoryEvents(container);
131
- }
132
- attachHistoryEvents(container) {
133
- container.querySelectorAll('[data-action]').forEach(btn => {
134
- btn.addEventListener('click', async (e) => {
135
- const el = e.currentTarget;
136
- const action = el.dataset.action;
137
- if (action === 'connect') {
138
- const cid = el.dataset.cid;
139
- if (cid) {
140
- const connectInput = this.shadow.getElementById('p2p-connect-input');
141
- if (connectInput)
142
- connectInput.value = cid;
143
- this.switchTab('connect');
144
- }
145
- }
146
- else if (action === 'pin') {
147
- const id = el.dataset.id;
148
- const pinned = el.dataset.pinned === 'true';
149
- if (id)
150
- await p2pManager.updateHistory(id, { isPinned: pinned });
151
- await this.loadHistory();
152
- }
153
- else if (action === 'delete') {
154
- const id = el.dataset.id;
155
- if (id)
156
- await p2pManager.deleteHistory(id);
157
- await this.loadHistory();
158
- }
159
- });
160
- });
161
- }
162
- async loadMessages() {
163
- const messagesList = this.shadow.getElementById('p2p-messages-list');
164
- const unreadBadge = this.shadow.getElementById('p2p-unread-badge');
165
- if (!messagesList)
166
- return;
167
- try {
168
- const messages = await p2pManager.getMessages();
169
- this.renderMessages(messages, messagesList);
170
- const unread = p2pManager.getUnreadCount();
171
- if (unreadBadge) {
172
- unreadBadge.textContent = String(unread);
173
- unreadBadge.style.display = unread > 0 ? 'inline-flex' : 'none';
174
- }
175
- }
176
- catch (e) {
177
- console.error('[P2P Modal] 加载消息失败:', e);
178
- }
179
- }
180
- renderMessages(messages, container) {
181
- if (messages.length === 0) {
182
- container.innerHTML = '<div class="empty-hint">暂无消息</div>';
183
- return;
184
- }
185
- const fragment = document.createDocumentFragment();
186
- messages.slice(-20).forEach(msg => {
187
- const div = document.createElement('div');
188
- div.className = `message-item ${!msg.isRead ? 'unread' : ''}`;
189
- div.innerHTML = `
190
- <div class="message-header">
191
- <span class="message-sender">${this.escapeHtml(msg.fromName || msg.fromDid)}</span>
192
- <span class="message-time">${new Date(msg.timestamp).toLocaleString()}</span>
193
- </div>
194
- <div class="message-content">${this.escapeHtml(msg.content.substring(0, 200))}</div>
195
- `;
196
- fragment.appendChild(div);
197
- });
198
- container.innerHTML = '';
199
- container.appendChild(fragment);
200
- }
201
- async loadPeers() {
202
- const peersList = this.shadow.getElementById('p2p-peers-list');
203
- const peerCount = this.shadow.getElementById('p2p-peer-count');
204
- if (!peersList)
205
- return;
206
- const peers = p2pManager.getConnectedPeers();
207
- if (peerCount)
208
- peerCount.textContent = String(peers.length);
209
- if (peers.length === 0) {
210
- peersList.innerHTML = '<div class="empty-hint">暂无连接</div>';
211
- return;
212
- }
213
- const fragment = document.createDocumentFragment();
214
- peers.forEach(peer => {
215
- const div = document.createElement('div');
216
- div.className = 'peer-item';
217
- div.innerHTML = `
218
- <div class="peer-status"><span class="dot online"></span></div>
219
- <div class="peer-info">
220
- <div class="peer-name">${this.escapeHtml(peer.info?.name || 'Unknown')}</div>
221
- <div class="peer-meta">${(peer.nodeId || '').substring(0, 16)}...</div>
222
- </div>
223
- `;
224
- fragment.appendChild(div);
225
- });
226
- peersList.innerHTML = '';
227
- peersList.appendChild(fragment);
228
- }
229
- async handleConnect() {
230
- const input = this.shadow.getElementById('p2p-connect-input');
231
- const progressDiv = this.shadow.getElementById('p2p-connect-progress');
232
- const progressFill = this.shadow.getElementById('p2p-progress-fill');
233
- const progressText = this.shadow.getElementById('p2p-progress-text');
234
- const resultDiv = this.shadow.getElementById('p2p-connect-result');
235
- if (!input?.value.trim())
236
- return;
237
- if (progressDiv)
238
- progressDiv.style.display = 'block';
239
- const updateProgress = (progress) => {
240
- if (progressFill)
241
- progressFill.style.width = `${progress.percent}%`;
242
- if (progressText)
243
- progressText.textContent = progress.message;
244
- };
245
- try {
246
- const result = await p2pManager.connect(input.value, updateProgress);
247
- if (result.success) {
248
- this.showResult(resultDiv, 'success', `已连接到 ${result.name || '节点'}`);
249
- input.value = '';
250
- await this.loadHistory();
251
- await this.loadPeers();
252
- }
253
- else {
254
- this.showResult(resultDiv, 'error', result.error || '连接失败');
255
- }
256
- }
257
- catch (e) {
258
- this.showResult(resultDiv, 'error', e.message);
259
- }
260
- finally {
261
- setTimeout(() => {
262
- if (progressDiv)
263
- progressDiv.style.display = 'none';
264
- }, 2000);
265
- }
266
- }
267
- showResult(el, type, text) {
268
- if (!el)
269
- return;
270
- el.className = `connect-result ${type} show`;
271
- el.textContent = text;
272
- }
273
- escapeHtml(text) {
274
- const div = document.createElement('div');
275
- div.textContent = text;
276
- return div.innerHTML;
277
- }
278
- showToast(message) {
279
- const toast = document.createElement('div');
280
- toast.className = 'toast';
281
- toast.textContent = message;
282
- document.body.appendChild(toast);
283
- setTimeout(() => toast.remove(), 2000);
284
- }
285
- render() {
286
- this.shadow.innerHTML = `
287
- <style>
288
- :host {
289
- display: none;
290
- position: fixed;
291
- inset: 0;
292
- background: rgba(0, 0, 0, 0.7);
293
- z-index: 1000;
294
- align-items: center;
295
- justify-content: center;
296
- }
297
- :host(.show) {
298
- display: flex;
299
- }
300
- .modal {
301
- width: 90%;
302
- max-width: 700px;
303
- max-height: 90vh;
304
- background: var(--bg-secondary, #1e1e2e);
305
- border: 1px solid var(--border, #3a3a4a);
306
- border-radius: 12px;
307
- padding: 24px;
308
- overflow-y: auto;
309
- }
310
- .modal-header {
311
- display: flex;
312
- justify-content: space-between;
313
- align-items: center;
314
- margin-bottom: 20px;
315
- }
316
- .modal-header h2 {
317
- margin: 0;
318
- font-size: 18px;
319
- color: var(--text-primary, #e0e0e0);
320
- }
321
- .modal-close {
322
- background: none;
323
- border: none;
324
- font-size: 24px;
325
- cursor: pointer;
326
- color: var(--text-secondary, #888);
327
- }
328
- .tabs {
329
- display: flex;
330
- gap: 4px;
331
- margin-bottom: 20px;
332
- border-bottom: 1px solid var(--border, #3a3a4a);
333
- padding-bottom: 8px;
334
- }
335
- .tab {
336
- padding: 8px 16px;
337
- background: transparent;
338
- border: none;
339
- border-radius: 6px 6px 0 0;
340
- color: var(--text-secondary, #888);
341
- font-size: 14px;
342
- cursor: pointer;
343
- }
344
- .tab:hover { color: var(--text-primary, #e0e0e0); }
345
- .tab.active {
346
- color: var(--accent, #7c3aed);
347
- background: var(--bg-hover, #2a2a3a);
348
- }
349
- .tab-content { display: none; }
350
- .tab-content.active { display: block; }
351
- .identity-card {
352
- background: var(--bg-hover, #2a2a3a);
353
- border: 1px solid var(--border, #3a3a4a);
354
- border-radius: 8px;
355
- padding: 20px;
356
- margin-bottom: 16px;
357
- }
358
- .status-row {
359
- display: flex;
360
- align-items: center;
361
- gap: 8px;
362
- margin-bottom: 16px;
363
- }
364
- .status-indicator {
365
- width: 10px;
366
- height: 10px;
367
- border-radius: 50%;
368
- background: var(--text-muted, #666);
369
- }
370
- .status-indicator.online { background: var(--success, #22c55e); }
371
- .status-indicator.connecting { background: var(--warning, #eab308); animation: pulse 1s infinite; }
372
- .status-indicator.error { background: var(--error, #ef4444); }
373
- @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
374
- .info-row {
375
- display: flex;
376
- align-items: center;
377
- gap: 8px;
378
- padding: 8px 0;
379
- border-bottom: 1px solid var(--border, #3a3a4a);
380
- }
381
- .info-row:last-child { border-bottom: none; }
382
- .info-label { min-width: 80px; color: var(--text-secondary, #888); font-size: 13px; }
383
- .info-value { flex: 1; font-size: 13px; word-break: break-all; font-family: monospace; }
384
- .copy-btn {
385
- padding: 4px 8px;
386
- background: var(--bg-primary, #252536);
387
- border: 1px solid var(--border, #3a3a4a);
388
- border-radius: 4px;
389
- cursor: pointer;
390
- font-size: 12px;
391
- }
392
- .copy-btn:hover { background: var(--accent, #7c3aed); color: white; }
393
- .btn-primary {
394
- padding: 12px 24px;
395
- background: var(--accent, #7c3aed);
396
- color: white;
397
- border: none;
398
- border-radius: 8px;
399
- cursor: pointer;
400
- font-size: 14px;
401
- width: 100%;
402
- }
403
- .btn-primary:hover { opacity: 0.9; }
404
- .share-panel {
405
- background: var(--bg-hover, #2a2a3a);
406
- border: 1px solid var(--border, #3a3a4a);
407
- border-radius: 8px;
408
- padding: 16px;
409
- margin-bottom: 16px;
410
- }
411
- .share-panel h4 { margin: 0 0 12px 0; color: var(--text-primary, #e0e0e0); }
412
- .share-actions { display: flex; gap: 8px; flex-wrap: wrap; }
413
- .btn-secondary {
414
- padding: 8px 16px;
415
- background: var(--bg-primary, #252536);
416
- border: 1px solid var(--border, #3a3a4a);
417
- border-radius: 6px;
418
- cursor: pointer;
419
- font-size: 13px;
420
- }
421
- .btn-secondary:hover { border-color: var(--accent, #7c3aed); }
422
- .btn-sm { padding: 6px 12px; font-size: 12px; }
423
- .connect-form { display: flex; gap: 8px; margin-bottom: 16px; }
424
- .connect-form input {
425
- flex: 1;
426
- padding: 12px 16px;
427
- background: var(--bg-hover, #2a2a3a);
428
- border: 1px solid var(--border, #3a3a4a);
429
- border-radius: 8px;
430
- color: var(--text-primary, #e0e0e0);
431
- font-size: 14px;
432
- }
433
- .progress {
434
- display: none;
435
- margin-bottom: 16px;
436
- padding: 16px;
437
- background: var(--bg-hover, #2a2a3a);
438
- border-radius: 8px;
439
- }
440
- .progress.show { display: block; }
441
- .progress-bar {
442
- height: 6px;
443
- background: var(--bg-primary, #252536);
444
- border-radius: 3px;
445
- overflow: hidden;
446
- margin-bottom: 8px;
447
- }
448
- .progress-fill {
449
- height: 100%;
450
- background: var(--accent, #7c3aed);
451
- border-radius: 3px;
452
- transition: width 0.3s;
453
- }
454
- .progress-text { color: var(--text-secondary, #888); font-size: 13px; }
455
- .connect-result {
456
- display: none;
457
- padding: 12px;
458
- border-radius: 8px;
459
- font-size: 14px;
460
- margin-bottom: 16px;
461
- }
462
- .connect-result.show { display: block; }
463
- .connect-result.success { background: rgba(34, 197, 94, 0.1); border: 1px solid var(--success, #22c55e); color: var(--success, #22c55e); }
464
- .connect-result.error { background: rgba(239, 68, 68, 0.1); border: 1px solid var(--error, #ef4444); color: var(--error, #ef4444); }
465
- .empty-hint { color: var(--text-muted, #666); font-size: 13px; padding: 32px; text-align: center; }
466
- .history-item, .peer-item {
467
- display: flex;
468
- align-items: center;
469
- gap: 12px;
470
- padding: 12px;
471
- background: var(--bg-hover, #2a2a3a);
472
- border: 1px solid var(--border, #3a3a4a);
473
- border-radius: 8px;
474
- margin-bottom: 8px;
475
- }
476
- .history-item.pinned { border-left: 3px solid var(--accent, #7c3aed); }
477
- .history-item-icon { font-size: 20px; }
478
- .history-item-info { flex: 1; }
479
- .history-item-name { font-weight: 500; display: flex; align-items: center; gap: 6px; }
480
- .history-item-meta { font-size: 12px; color: var(--text-secondary, #888); display: flex; gap: 12px; margin-top: 4px; }
481
- .history-item-actions { display: flex; gap: 8px; }
482
- .peer-status { display: flex; align-items: center; }
483
- .peer-status .dot { width: 8px; height: 8px; border-radius: 50%; }
484
- .peer-status .dot.online { background: var(--success, #22c55e); }
485
- .peer-info { flex: 1; }
486
- .peer-name { font-weight: 500; }
487
- .peer-meta { font-size: 12px; color: var(--text-secondary, #888); }
488
- .message-item {
489
- padding: 12px;
490
- background: var(--bg-hover, #2a2a3a);
491
- border: 1px solid var(--border, #3a3a4a);
492
- border-radius: 8px;
493
- margin-bottom: 8px;
494
- }
495
- .message-item.unread { border-left: 3px solid var(--accent, #7c3aed); }
496
- .message-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
497
- .message-sender { font-weight: 500; }
498
- .message-time { font-size: 12px; color: var(--text-secondary, #888); }
499
- .message-content { font-size: 14px; line-height: 1.5; }
500
- .peers-section {
501
- margin-top: 24px;
502
- padding-top: 16px;
503
- border-top: 1px solid var(--border, #3a3a4a);
504
- }
505
- .peers-section h4 { margin-bottom: 12px; color: var(--text-secondary, #888); font-size: 13px; }
506
- .toolbar { margin-bottom: 12px; display: flex; justify-content: flex-end; }
507
- </style>
508
-
509
- <div class="modal">
510
- <div class="modal-header">
511
- <h2>P2P 网络</h2>
512
- <button class="modal-close" id="modal-close">&times;</button>
513
- </div>
514
-
515
- <div class="tabs">
516
- <button class="tab ${this.activeTab === 'identity' ? 'active' : ''}" data-tab="identity">我的身份</button>
517
- <button class="tab ${this.activeTab === 'connect' ? 'active' : ''}" data-tab="connect">连接</button>
518
- <button class="tab ${this.activeTab === 'history' ? 'active' : ''}" data-tab="history">历史记录</button>
519
- <button class="tab ${this.activeTab === 'messages' ? 'active' : ''}" data-tab="messages">
520
- 消息 <span id="p2p-unread-badge" class="unread-badge" style="display:none">0</span>
521
- </button>
522
- </div>
523
-
524
- <!-- 我的身份 -->
525
- <div class="tab-content ${this.activeTab === 'identity' ? 'active' : ''}" id="tab-identity">
526
- <div class="identity-card">
527
- <div class="status-row">
528
- <span class="status-indicator" id="p2p-status-indicator"></span>
529
- <span id="p2p-status-text">未初始化</span>
530
- </div>
531
- <div class="info-row">
532
- <span class="info-label">DID:</span>
533
- <code class="info-value" id="p2p-did">-</code>
534
- <button class="copy-btn" data-copy="p2p-did">📋</button>
535
- </div>
536
- <div class="info-row">
537
- <span class="info-label">CID:</span>
538
- <code class="info-value" id="p2p-cid">-</code>
539
- <button class="copy-btn" data-copy="p2p-cid">📋</button>
540
- </div>
541
- <div class="info-row">
542
- <span class="info-label">Node ID:</span>
543
- <code class="info-value" id="p2p-node-id">-</code>
544
- <button class="copy-btn" data-copy="p2p-node-id">📋</button>
545
- </div>
546
- </div>
547
-
548
- <button class="btn-primary" id="p2p-init-btn">初始化 P2P</button>
549
-
550
- <div class="share-panel" id="p2p-share-panel" style="display:none">
551
- <h4>分享给好友</h4>
552
- <div class="share-actions">
553
- <button class="btn-secondary" id="p2p-copy-link">📋 复制链接</button>
554
- <button class="btn-secondary" id="p2p-export-file">📁 导出文件</button>
555
- </div>
556
- <div class="info-row" style="margin-top: 12px; display:none" id="p2p-share-link-row">
557
- <input type="text" id="p2p-invite-link" readonly style="flex:1; padding:8px; background:var(--bg-primary); border:1px solid var(--border); border-radius:6px); color:var(--text-primary); font-size:12px; font-family:monospace;">
558
- </div>
559
- </div>
560
- </div>
561
-
562
- <!-- 连接 -->
563
- <div class="tab-content ${this.activeTab === 'connect' ? 'active' : ''}" id="tab-connect">
564
- <div class="connect-form">
565
- <input type="text" id="p2p-connect-input" placeholder="粘贴 CID 或链接...">
566
- <button class="btn-secondary" id="p2p-connect-btn">连接 ▶</button>
567
- </div>
568
- <div class="progress" id="p2p-connect-progress">
569
- <div class="progress-bar"><div class="progress-fill" id="p2p-progress-fill"></div></div>
570
- <span class="progress-text" id="p2p-progress-text">验证输入格式...</span>
571
- </div>
572
- <div class="connect-result" id="p2p-connect-result"></div>
573
- </div>
574
-
575
- <!-- 历史记录 -->
576
- <div class="tab-content ${this.activeTab === 'history' ? 'active' : ''}" id="tab-history">
577
- <div class="toolbar">
578
- <button class="btn-secondary btn-sm" id="p2p-history-refresh">🔄 刷新</button>
579
- </div>
580
- <div id="p2p-history-list"></div>
581
- </div>
582
-
583
- <!-- 消息 -->
584
- <div class="tab-content ${this.activeTab === 'messages' ? 'active' : ''}" id="tab-messages">
585
- <div class="toolbar">
586
- <button class="btn-secondary btn-sm" id="p2p-mark-all-read">全部已读</button>
587
- </div>
588
- <div id="p2p-messages-list"></div>
589
- </div>
590
-
591
- <!-- 已连接节点 -->
592
- <div class="peers-section">
593
- <h4>已连接节点 (<span id="p2p-peer-count">0</span>)</h4>
594
- <div id="p2p-peers-list"></div>
595
- </div>
596
- </div>
597
- `;
598
- }
599
- attachEvents() {
600
- // 关闭按钮
601
- this.shadow.getElementById('modal-close')?.addEventListener('click', () => this.hide());
602
- // 点击外部关闭
603
- this.addEventListener('click', (e) => {
604
- if (e.target === this)
605
- this.hide();
606
- });
607
- // 初始化按钮
608
- this.shadow.getElementById('p2p-init-btn')?.addEventListener('click', () => this.initP2P());
609
- // 复制按钮
610
- this.shadow.querySelectorAll('[data-copy]').forEach(btn => {
611
- btn.addEventListener('click', async (e) => {
612
- const el = e.currentTarget;
613
- const targetId = el.dataset.copy;
614
- const targetEl = this.shadow.getElementById(targetId);
615
- if (targetEl?.textContent && targetEl.textContent !== '-') {
616
- await navigator.clipboard.writeText(targetEl.textContent);
617
- this.showToast('已复制');
618
- }
619
- });
620
- });
621
- // 复制链接
622
- this.shadow.getElementById('p2p-copy-link')?.addEventListener('click', async () => {
623
- const shareLink = this.shadow.getElementById('p2p-invite-link');
624
- if (shareLink?.value) {
625
- await navigator.clipboard.writeText(shareLink.value);
626
- this.showToast('链接已复制');
627
- }
628
- });
629
- // 导出文件
630
- this.shadow.getElementById('p2p-export-file')?.addEventListener('click', () => {
631
- p2pManager.identity.exportIdentityFile();
632
- });
633
- // 标签页切换
634
- this.shadow.querySelectorAll('.tab').forEach(btn => {
635
- btn.addEventListener('click', () => {
636
- const tab = btn.dataset.tab;
637
- if (tab)
638
- this.switchTab(tab);
639
- });
640
- });
641
- // 连接按钮
642
- this.shadow.getElementById('p2p-connect-btn')?.addEventListener('click', () => this.handleConnect());
643
- // 回车连接
644
- this.shadow.getElementById('p2p-connect-input')?.addEventListener('keypress', (e) => {
645
- if (e.key === 'Enter')
646
- this.handleConnect();
647
- });
648
- // 刷新历史
649
- this.shadow.getElementById('p2p-history-refresh')?.addEventListener('click', () => this.loadHistory());
650
- // 全部已读
651
- this.shadow.getElementById('p2p-mark-all-read')?.addEventListener('click', async () => {
652
- await p2pManager.messages.markAllRead();
653
- this.loadMessages();
654
- });
655
- }
656
- }
657
- customElements.define('p2p-modal', P2PModal);