koishi-plugin-chat-patch 2.1.1 → 2.2.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/client/vue/chat-logic.ts +683 -678
- package/client/vue/composables/useVideoCache.ts +130 -0
- package/client/vue/index.vue +658 -612
- package/dist/index.js +2 -2
- package/lib/api-handlers.d.ts +1 -0
- package/lib/index.js +91 -20
- package/lib/utils.d.ts +13 -0
- package/package.json +52 -49
- package/src/api-handlers.ts +750 -704
- package/src/index.ts +148 -125
- package/src/message-handler.ts +413 -407
- package/src/utils.ts +163 -144
package/client/vue/chat-logic.ts
CHANGED
|
@@ -1,678 +1,683 @@
|
|
|
1
|
-
import { ref, computed, onMounted, nextTick, onUnmounted, reactive, watch } from 'vue'
|
|
2
|
-
import { receive, send } from '@koishijs/client'
|
|
3
|
-
import { useChatData } from './composables/useChatData'
|
|
4
|
-
import { useChatActions } from './composables/useChatActions'
|
|
5
|
-
import { useImageCache } from './composables/useImageCache'
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
else if (mobileView.value === '
|
|
111
|
-
else if (mobileView.value === '
|
|
112
|
-
else if (mobileView.value === '
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
mobileView.value =
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
imageZoom.value = Math.
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
a
|
|
166
|
-
a.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
await
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
menu.value.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
textArea
|
|
346
|
-
textArea.
|
|
347
|
-
textArea.style.
|
|
348
|
-
|
|
349
|
-
textArea.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if (
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
} else if (action === '
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
await
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
window.addEventListener('
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
window.removeEventListener('resize',
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
1
|
+
import { ref, computed, onMounted, nextTick, onUnmounted, reactive, watch } from 'vue'
|
|
2
|
+
import { receive, send } from '@koishijs/client'
|
|
3
|
+
import { useChatData } from './composables/useChatData'
|
|
4
|
+
import { useChatActions } from './composables/useChatActions'
|
|
5
|
+
import { useImageCache } from './composables/useImageCache'
|
|
6
|
+
import { useVideoCache } from './composables/useVideoCache'
|
|
7
|
+
import { ElMessage } from 'element-plus'
|
|
8
|
+
|
|
9
|
+
export function useChatLogic() {
|
|
10
|
+
const {
|
|
11
|
+
bots, pinnedBots, pinnedChannels, getChannels, getMessages,
|
|
12
|
+
loadInitialData, loadConfig, addMessage, loadHistory, getPagination
|
|
13
|
+
} = useChatData()
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
isSending, sendMessage, deleteBotData, deleteChannelData,
|
|
17
|
+
togglePinBot, togglePinChannel
|
|
18
|
+
} = useChatActions()
|
|
19
|
+
|
|
20
|
+
const { getCachedImageUrl, cacheImage } = useImageCache()
|
|
21
|
+
const { loadVideo, isVideoLoading, isVideoLoaded } = useVideoCache()
|
|
22
|
+
|
|
23
|
+
// 状态管理
|
|
24
|
+
const menu = ref({ show: false, x: 0, y: 0, type: '', id: '', isPinned: false, hasMedia: false })
|
|
25
|
+
const selectedBot = ref('')
|
|
26
|
+
const selectedChannel = ref('')
|
|
27
|
+
const inputText = ref('')
|
|
28
|
+
const uploadedImages = ref<any[]>([])
|
|
29
|
+
const scrollRef = ref<any>(null)
|
|
30
|
+
const inputRef = ref<any>(null)
|
|
31
|
+
const isMobile = ref(false)
|
|
32
|
+
const mobileView = ref<'bots' | 'channels' | 'messages' | 'forward' | 'image' | 'profile' | 'raw'>('bots')
|
|
33
|
+
const isLoadingHistory = ref(false)
|
|
34
|
+
const keyboardHeight = ref(0) // 键盘高度
|
|
35
|
+
|
|
36
|
+
// 合并转发详情状态
|
|
37
|
+
const forwardData = reactive({
|
|
38
|
+
messages: [] as any[]
|
|
39
|
+
})
|
|
40
|
+
const forwardDialogVisible = ref(false)
|
|
41
|
+
|
|
42
|
+
// 图片查看器状态
|
|
43
|
+
const imageViewer = reactive({
|
|
44
|
+
url: ''
|
|
45
|
+
})
|
|
46
|
+
const imageViewerVisible = ref(false)
|
|
47
|
+
const imageZoom = ref(1)
|
|
48
|
+
|
|
49
|
+
// 原始消息查看状态
|
|
50
|
+
const rawMessage = reactive({
|
|
51
|
+
content: ''
|
|
52
|
+
})
|
|
53
|
+
const rawMessageVisible = ref(false)
|
|
54
|
+
|
|
55
|
+
// 引用/回复状态
|
|
56
|
+
const replyingTo = ref<any>(null)
|
|
57
|
+
|
|
58
|
+
// 用户资料状态
|
|
59
|
+
const userProfile = reactive({
|
|
60
|
+
data: null as any
|
|
61
|
+
})
|
|
62
|
+
const userProfileVisible = ref(false)
|
|
63
|
+
|
|
64
|
+
// 计算属性
|
|
65
|
+
const currentChannels = computed(() => getChannels(selectedBot.value))
|
|
66
|
+
const currentMessages = computed(() => getMessages(selectedBot.value, selectedChannel.value))
|
|
67
|
+
const currentChannelName = computed(() => {
|
|
68
|
+
const c = currentChannels.value.find(i => i.id === selectedChannel.value)
|
|
69
|
+
return c ? c.name : ''
|
|
70
|
+
})
|
|
71
|
+
const selectedBotPlatform = computed(() => {
|
|
72
|
+
const bot = bots.value.find(b => b.selfId === selectedBot.value)
|
|
73
|
+
return bot?.platform || 'unknown'
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// 方法
|
|
77
|
+
const selectBot = (id: string) => {
|
|
78
|
+
selectedBot.value = id
|
|
79
|
+
selectedChannel.value = ''
|
|
80
|
+
if (isMobile.value) mobileView.value = 'channels'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const selectChannel = async (id: string) => {
|
|
84
|
+
selectedChannel.value = id
|
|
85
|
+
if (isMobile.value) mobileView.value = 'messages'
|
|
86
|
+
|
|
87
|
+
// 如果没有消息,加载第一页
|
|
88
|
+
if (currentMessages.value.length === 0) {
|
|
89
|
+
await loadHistory(selectedBot.value, id)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await nextTick()
|
|
93
|
+
scrollToBottom()
|
|
94
|
+
|
|
95
|
+
// 针对图片加载导致的滚动偏移,在 300ms 和 800ms 后再次校准底部
|
|
96
|
+
setTimeout(scrollToBottom, 300)
|
|
97
|
+
setTimeout(scrollToBottom, 800)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const goBack = () => {
|
|
101
|
+
if (isMobile.value && mobileView.value !== 'bots') {
|
|
102
|
+
// 手机端使用 history.back(),由 popstate 监听器处理视图切换
|
|
103
|
+
window.history.back()
|
|
104
|
+
} else {
|
|
105
|
+
// PC 端或初始页面的逻辑
|
|
106
|
+
if (mobileView.value === 'image') {
|
|
107
|
+
mobileView.value = 'messages'
|
|
108
|
+
imageZoom.value = 1
|
|
109
|
+
}
|
|
110
|
+
else if (mobileView.value === 'forward') mobileView.value = 'messages'
|
|
111
|
+
else if (mobileView.value === 'profile') mobileView.value = 'messages'
|
|
112
|
+
else if (mobileView.value === 'raw') mobileView.value = 'messages'
|
|
113
|
+
else if (mobileView.value === 'messages') mobileView.value = 'channels'
|
|
114
|
+
else if (mobileView.value === 'channels') mobileView.value = 'bots'
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 处理手机物理返回键
|
|
119
|
+
const handlePopState = (e: PopStateEvent) => {
|
|
120
|
+
if (!isMobile.value) return
|
|
121
|
+
if (e.state && e.state.view) {
|
|
122
|
+
mobileView.value = e.state.view
|
|
123
|
+
} else {
|
|
124
|
+
mobileView.value = 'bots'
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 监听视图变化,同步到 history state
|
|
129
|
+
watch(mobileView, (newView, oldView) => {
|
|
130
|
+
if (!isMobile.value) return
|
|
131
|
+
// 只有在非 popstate 导致的改变时才 pushState (简单判断:如果当前 state 不匹配则 push)
|
|
132
|
+
if (window.history.state?.view !== newView) {
|
|
133
|
+
window.history.pushState({ view: newView }, '')
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const showForward = (elements: any[]) => {
|
|
138
|
+
forwardData.messages = elements.filter(e => e.type === 'message')
|
|
139
|
+
if (isMobile.value) {
|
|
140
|
+
mobileView.value = 'forward'
|
|
141
|
+
} else {
|
|
142
|
+
forwardDialogVisible.value = true
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const openImageViewer = (url: string) => {
|
|
147
|
+
imageViewer.url = url
|
|
148
|
+
imageZoom.value = 1
|
|
149
|
+
if (isMobile.value) {
|
|
150
|
+
mobileView.value = 'image'
|
|
151
|
+
} else {
|
|
152
|
+
imageViewerVisible.value = true
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const handleImageWheel = (e: WheelEvent) => {
|
|
157
|
+
if (e.deltaY < 0) {
|
|
158
|
+
imageZoom.value = Math.min(imageZoom.value + 0.1, 3)
|
|
159
|
+
} else {
|
|
160
|
+
imageZoom.value = Math.max(imageZoom.value - 0.1, 0.5)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const downloadImage = (url: string) => {
|
|
165
|
+
const a = document.createElement('a')
|
|
166
|
+
a.href = url
|
|
167
|
+
a.download = `chat-image-${Date.now()}.png`
|
|
168
|
+
a.click()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const scrollToBottom = () => {
|
|
172
|
+
if (scrollRef.value) {
|
|
173
|
+
const wrap = scrollRef.value.wrapRef
|
|
174
|
+
if (wrap) wrap.scrollTop = wrap.scrollHeight
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const handleScroll = async ({ scrollTop }: { scrollTop: number }) => {
|
|
179
|
+
if (scrollTop <= 10 && !isLoadingHistory.value && selectedBot.value && selectedChannel.value) {
|
|
180
|
+
const pagination = getPagination(selectedBot.value, selectedChannel.value)
|
|
181
|
+
if (pagination.hasMore) {
|
|
182
|
+
isLoadingHistory.value = true
|
|
183
|
+
const wrap = scrollRef.value?.wrapRef
|
|
184
|
+
const oldHeight = wrap?.scrollHeight || 0
|
|
185
|
+
|
|
186
|
+
await loadHistory(selectedBot.value, selectedChannel.value)
|
|
187
|
+
|
|
188
|
+
await nextTick()
|
|
189
|
+
// 保持滚动位置
|
|
190
|
+
if (wrap) {
|
|
191
|
+
wrap.scrollTop = wrap.scrollHeight - oldHeight
|
|
192
|
+
}
|
|
193
|
+
isLoadingHistory.value = false
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const handleSend = async () => {
|
|
199
|
+
if (!selectedBot.value || !selectedChannel.value) return
|
|
200
|
+
if (!inputText.value.trim() && !uploadedImages.value.length) return
|
|
201
|
+
|
|
202
|
+
let content = inputText.value
|
|
203
|
+
if (replyingTo.value) {
|
|
204
|
+
// 优先使用真实 ID 进行引用
|
|
205
|
+
const quoteId = replyingTo.value.realId || replyingTo.value.id
|
|
206
|
+
content = `<quote id="${quoteId}"/>${content}`
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const res = await sendMessage(selectedBot.value, selectedChannel.value, content, uploadedImages.value)
|
|
210
|
+
if (res?.success) {
|
|
211
|
+
inputText.value = ''
|
|
212
|
+
uploadedImages.value = []
|
|
213
|
+
replyingTo.value = null
|
|
214
|
+
scrollToBottom()
|
|
215
|
+
} else {
|
|
216
|
+
ElMessage.error(res?.error || '发送失败')
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 复读消息 (+1)
|
|
221
|
+
const repeatMessage = async (msg: any) => {
|
|
222
|
+
if (!selectedBot.value || !selectedChannel.value) return
|
|
223
|
+
|
|
224
|
+
// 复读消息内容
|
|
225
|
+
let content = msg.content
|
|
226
|
+
|
|
227
|
+
// 处理引用逻辑
|
|
228
|
+
if (msg.quote) {
|
|
229
|
+
// 如果原消息有引用,保留引用
|
|
230
|
+
const quoteId = msg.quote.id
|
|
231
|
+
// 检查引用ID是否为临时ID
|
|
232
|
+
if (quoteId && !quoteId.startsWith('bot-msg-')) {
|
|
233
|
+
content = `<quote id="${quoteId}"/>${content}`
|
|
234
|
+
} else {
|
|
235
|
+
// 引用ID是临时ID,不添加引用,只发送内容
|
|
236
|
+
content = msg.content
|
|
237
|
+
}
|
|
238
|
+
} else if (msg.isBot || msg.userId === selectedBot.value) {
|
|
239
|
+
// 如果是复读机器人自己的消息
|
|
240
|
+
const messageId = msg.realId || msg.id
|
|
241
|
+
|
|
242
|
+
// 检查消息ID是否为临时ID(以bot-msg-开头)
|
|
243
|
+
if (messageId && messageId.startsWith('bot-msg-')) {
|
|
244
|
+
// 临时ID,不使用引用,直接发送内容
|
|
245
|
+
content = msg.content
|
|
246
|
+
} else if (messageId) {
|
|
247
|
+
// 真实ID,可以使用引用
|
|
248
|
+
content = `<quote id="${messageId}"/>${content}`
|
|
249
|
+
} else {
|
|
250
|
+
// 没有ID,只发送内容
|
|
251
|
+
content = msg.content
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const res = await sendMessage(selectedBot.value, selectedChannel.value, content, [])
|
|
256
|
+
if (res?.success) {
|
|
257
|
+
scrollToBottom()
|
|
258
|
+
} else {
|
|
259
|
+
ElMessage.error(res?.error || '复读失败')
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 机器人右键菜单
|
|
264
|
+
const onBotMenu = (e: MouseEvent, bot: any) => {
|
|
265
|
+
e.preventDefault()
|
|
266
|
+
e.stopPropagation()
|
|
267
|
+
menu.value = {
|
|
268
|
+
show: true,
|
|
269
|
+
x: e.clientX,
|
|
270
|
+
y: e.clientY,
|
|
271
|
+
type: 'bot',
|
|
272
|
+
id: bot.selfId,
|
|
273
|
+
isPinned: pinnedBots.value.has(bot.selfId),
|
|
274
|
+
hasMedia: false
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 频道右键菜单
|
|
279
|
+
const onChannelMenu = (e: MouseEvent, channel: any) => {
|
|
280
|
+
e.preventDefault()
|
|
281
|
+
e.stopPropagation()
|
|
282
|
+
menu.value = {
|
|
283
|
+
show: true,
|
|
284
|
+
x: e.clientX,
|
|
285
|
+
y: e.clientY,
|
|
286
|
+
type: 'channel',
|
|
287
|
+
id: channel.id,
|
|
288
|
+
isPinned: pinnedChannels.value.has(`${selectedBot.value}:${channel.id}`),
|
|
289
|
+
hasMedia: false
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 消息右键菜单
|
|
294
|
+
const onMessageMenu = (e: MouseEvent, msg: any) => {
|
|
295
|
+
// 如果菜单已经显示,则关闭它并允许原生菜单弹出(第二次右键逻辑)
|
|
296
|
+
if (menu.value.show && menu.value.type === 'message' && menu.value.id === msg.id) {
|
|
297
|
+
menu.value.show = false
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
e.preventDefault()
|
|
302
|
+
e.stopPropagation()
|
|
303
|
+
|
|
304
|
+
const hasMedia = msg.elements?.some((el: any) => ['image', 'img', 'mface', 'audio', 'video'].includes(el.type))
|
|
305
|
+
menu.value = {
|
|
306
|
+
show: true,
|
|
307
|
+
x: e.clientX,
|
|
308
|
+
y: e.clientY,
|
|
309
|
+
type: 'message',
|
|
310
|
+
id: msg.id,
|
|
311
|
+
isPinned: false,
|
|
312
|
+
data: msg,
|
|
313
|
+
hasMedia
|
|
314
|
+
} as any
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 统一处理菜单动作
|
|
318
|
+
const handleMenuAction = async (action: string) => {
|
|
319
|
+
const type = menu.value.type
|
|
320
|
+
const id = menu.value.id
|
|
321
|
+
const isPinned = menu.value.isPinned
|
|
322
|
+
menu.value.show = false
|
|
323
|
+
|
|
324
|
+
if (action === 'pin') {
|
|
325
|
+
if (type === 'bot') await togglePinBot(id, isPinned, pinnedBots.value)
|
|
326
|
+
else await togglePinChannel(selectedBot.value, id, isPinned, pinnedChannels.value)
|
|
327
|
+
} else if (action === 'delete') {
|
|
328
|
+
// 删除逻辑保持在 index.vue 中通过 ElMessageBox 确认,或者这里直接处理
|
|
329
|
+
if (type === 'bot') await deleteBotData(id)
|
|
330
|
+
else await deleteChannelData(selectedBot.value, id)
|
|
331
|
+
location.reload()
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 兼容手机端的复制函数
|
|
336
|
+
const copyToClipboard = (text: string) => {
|
|
337
|
+
if (!text) {
|
|
338
|
+
ElMessage.warning('未复制任何内容')
|
|
339
|
+
return Promise.resolve()
|
|
340
|
+
}
|
|
341
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
342
|
+
return navigator.clipboard.writeText(text)
|
|
343
|
+
} else {
|
|
344
|
+
// 回退方案
|
|
345
|
+
const textArea = document.createElement("textarea")
|
|
346
|
+
textArea.value = text
|
|
347
|
+
textArea.style.position = "fixed"
|
|
348
|
+
textArea.style.left = "-999999px"
|
|
349
|
+
textArea.style.top = "-999999px"
|
|
350
|
+
document.body.appendChild(textArea)
|
|
351
|
+
textArea.focus()
|
|
352
|
+
textArea.select()
|
|
353
|
+
return new Promise<void>((res, rej) => {
|
|
354
|
+
document.execCommand('copy') ? res() : rej()
|
|
355
|
+
textArea.remove()
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const handleMessageAction = async (action: string) => {
|
|
361
|
+
const msg = (menu.value as any).data
|
|
362
|
+
menu.value.show = false
|
|
363
|
+
if (!msg) return
|
|
364
|
+
|
|
365
|
+
if (action === 'copy') {
|
|
366
|
+
// 移除 HTML 标签
|
|
367
|
+
const text = (msg.content || '').replace(/<[^>]+>/g, '')
|
|
368
|
+
if (!text) {
|
|
369
|
+
ElMessage.warning('未复制任何内容')
|
|
370
|
+
} else {
|
|
371
|
+
copyToClipboard(text).then(() => ElMessage.success('已复制到剪贴板'))
|
|
372
|
+
}
|
|
373
|
+
} else if (action === 'copy-raw') {
|
|
374
|
+
// 查看原始消息,包含引用标签
|
|
375
|
+
let raw = msg.content || ''
|
|
376
|
+
if (msg.quote) {
|
|
377
|
+
raw = `<quote id="${msg.quote.id}"/>${raw}`
|
|
378
|
+
}
|
|
379
|
+
rawMessage.content = raw
|
|
380
|
+
if (isMobile.value) {
|
|
381
|
+
mobileView.value = 'raw'
|
|
382
|
+
} else {
|
|
383
|
+
rawMessageVisible.value = true
|
|
384
|
+
}
|
|
385
|
+
} else if (action === 'plus1') {
|
|
386
|
+
await repeatMessage(msg)
|
|
387
|
+
} else if (action === 'reply') {
|
|
388
|
+
replyingTo.value = msg
|
|
389
|
+
// 自动聚焦输入框
|
|
390
|
+
nextTick(() => {
|
|
391
|
+
// Element Plus 的 el-input 需要访问其内部的 textarea
|
|
392
|
+
const inputEl = inputRef.value?.$el?.querySelector('textarea') || inputRef.value?.ref
|
|
393
|
+
if (inputEl) {
|
|
394
|
+
inputEl.focus()
|
|
395
|
+
} else {
|
|
396
|
+
inputRef.value?.focus?.()
|
|
397
|
+
}
|
|
398
|
+
})
|
|
399
|
+
} else if (action === 'download') {
|
|
400
|
+
const media = msg.elements?.find((el: any) => ['image', 'img', 'mface', 'audio', 'video'].includes(el.type))
|
|
401
|
+
const url = media?.attrs?.src || media?.attrs?.url || media?.attrs?.file
|
|
402
|
+
if (url) downloadImage(url)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 显示用户资料
|
|
407
|
+
const showUserProfile = async (msg: any) => {
|
|
408
|
+
if (!selectedBot.value) return
|
|
409
|
+
|
|
410
|
+
const res = await (send as any)('get-user-info', {
|
|
411
|
+
selfId: selectedBot.value,
|
|
412
|
+
userId: msg.userId,
|
|
413
|
+
guildId: msg.guildId
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
if (res.success) {
|
|
417
|
+
userProfile.data = res.data
|
|
418
|
+
if (isMobile.value) {
|
|
419
|
+
mobileView.value = 'profile'
|
|
420
|
+
} else {
|
|
421
|
+
userProfileVisible.value = true
|
|
422
|
+
}
|
|
423
|
+
} else {
|
|
424
|
+
ElMessage.error(res.error || '获取用户信息失败')
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// 定位消息并高亮
|
|
429
|
+
const scrollToMessage = async (id: string) => {
|
|
430
|
+
// 尝试在当前列表中查找,优先匹配 data-id
|
|
431
|
+
let el = document.querySelector(`[data-id="${id}"]`) || document.getElementById(id)
|
|
432
|
+
|
|
433
|
+
if (!el) {
|
|
434
|
+
// 如果没找到,尝试向上加载历史记录
|
|
435
|
+
if (isLoadingHistory.value) return
|
|
436
|
+
|
|
437
|
+
ElMessage.info('正在向上查找历史消息...')
|
|
438
|
+
|
|
439
|
+
// 最多尝试向上查找 3 次
|
|
440
|
+
for (let i = 0; i < 3; i++) {
|
|
441
|
+
await loadHistory(selectedBot.value, selectedChannel.value)
|
|
442
|
+
// 等待 DOM 更新
|
|
443
|
+
await new Promise(resolve => setTimeout(resolve, 150))
|
|
444
|
+
el = document.querySelector(`[data-id="${id}"]`) || document.getElementById(id)
|
|
445
|
+
if (el) break
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (el) {
|
|
450
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
451
|
+
// 找到消息内容容器进行高亮
|
|
452
|
+
const contentEl = el.querySelector('.cursor-context-menu') || el
|
|
453
|
+
contentEl.classList.add('message-highlight')
|
|
454
|
+
setTimeout(() => contentEl.classList.remove('message-highlight'), 1500)
|
|
455
|
+
} else {
|
|
456
|
+
ElMessage.warning('消息太久远,已不在当前列表中')
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// 处理粘贴图片
|
|
461
|
+
const handlePaste = async (event: ClipboardEvent) => {
|
|
462
|
+
const items = event.clipboardData?.items
|
|
463
|
+
if (!items) return
|
|
464
|
+
|
|
465
|
+
for (let i = 0; i < items.length; i++) {
|
|
466
|
+
if (items[i].type.indexOf('image') !== -1) {
|
|
467
|
+
const file = items[i].getAsFile()
|
|
468
|
+
if (file) {
|
|
469
|
+
// 模拟文件上传逻辑
|
|
470
|
+
const reader = new FileReader()
|
|
471
|
+
reader.onload = async (e) => {
|
|
472
|
+
const base64 = e.target?.result as string
|
|
473
|
+
const res = await (send as any)('upload-image', {
|
|
474
|
+
file: base64,
|
|
475
|
+
filename: `pasted_image_${Date.now()}.png`,
|
|
476
|
+
mimeType: file.type
|
|
477
|
+
})
|
|
478
|
+
if (res.success) {
|
|
479
|
+
uploadedImages.value.push({
|
|
480
|
+
tempId: res.tempId,
|
|
481
|
+
preview: URL.createObjectURL(file),
|
|
482
|
+
filename: `pasted_image_${Date.now()}.png`
|
|
483
|
+
})
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
reader.readAsDataURL(file)
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const checkMobile = () => {
|
|
493
|
+
isMobile.value = window.innerWidth <= 768
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 监听键盘弹出(通过 visualViewport 或 window resize)
|
|
497
|
+
const updateKeyboardHeight = () => {
|
|
498
|
+
if (!isMobile.value) {
|
|
499
|
+
keyboardHeight.value = 0
|
|
500
|
+
return
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// 使用 visualViewport API(现代浏览器)
|
|
504
|
+
if (window.visualViewport) {
|
|
505
|
+
const viewportHeight = window.visualViewport.height
|
|
506
|
+
const windowHeight = window.innerHeight
|
|
507
|
+
const calculatedHeight = Math.max(0, windowHeight - viewportHeight)
|
|
508
|
+
|
|
509
|
+
// 只有当键盘高度变化超过50px时才更新(避免小幅抖动)
|
|
510
|
+
if (Math.abs(calculatedHeight - keyboardHeight.value) > 50) {
|
|
511
|
+
keyboardHeight.value = calculatedHeight
|
|
512
|
+
}
|
|
513
|
+
} else {
|
|
514
|
+
keyboardHeight.value = 0
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// 生命周期
|
|
519
|
+
let dispose: any[] = []
|
|
520
|
+
|
|
521
|
+
onMounted(async () => {
|
|
522
|
+
checkMobile()
|
|
523
|
+
window.addEventListener('resize', checkMobile)
|
|
524
|
+
window.addEventListener('click', () => menu.value.show = false)
|
|
525
|
+
window.addEventListener('popstate', handlePopState)
|
|
526
|
+
|
|
527
|
+
// 监听键盘弹出
|
|
528
|
+
if (window.visualViewport) {
|
|
529
|
+
window.visualViewport.addEventListener('resize', updateKeyboardHeight)
|
|
530
|
+
window.visualViewport.addEventListener('scroll', updateKeyboardHeight)
|
|
531
|
+
}
|
|
532
|
+
window.addEventListener('resize', updateKeyboardHeight)
|
|
533
|
+
|
|
534
|
+
// 监听输入框聚焦和失焦
|
|
535
|
+
const handleFocus = () => {
|
|
536
|
+
// 延迟更新,等待键盘完全弹出
|
|
537
|
+
setTimeout(updateKeyboardHeight, 300)
|
|
538
|
+
}
|
|
539
|
+
const handleBlur = () => {
|
|
540
|
+
// 延迟更新,等待键盘完全收起
|
|
541
|
+
setTimeout(() => {
|
|
542
|
+
keyboardHeight.value = 0
|
|
543
|
+
}, 100)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// 为输入框添加事件监听
|
|
547
|
+
nextTick(() => {
|
|
548
|
+
const textarea = inputRef.value?.$el?.querySelector('textarea')
|
|
549
|
+
if (textarea) {
|
|
550
|
+
textarea.addEventListener('focus', handleFocus)
|
|
551
|
+
textarea.addEventListener('blur', handleBlur)
|
|
552
|
+
dispose.push(() => {
|
|
553
|
+
textarea.removeEventListener('focus', handleFocus)
|
|
554
|
+
textarea.removeEventListener('blur', handleBlur)
|
|
555
|
+
})
|
|
556
|
+
}
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
// 初始化 history state
|
|
560
|
+
if (isMobile.value) {
|
|
561
|
+
window.history.replaceState({ view: mobileView.value }, '')
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
await loadConfig()
|
|
565
|
+
await loadInitialData()
|
|
566
|
+
|
|
567
|
+
const d1 = receive('chat-message-event', (ev: any) => {
|
|
568
|
+
addMessage(ev)
|
|
569
|
+
if (ev.selfId === selectedBot.value && ev.channelId === selectedChannel.value) {
|
|
570
|
+
// 只有在底部附近才自动滚动
|
|
571
|
+
const wrap = scrollRef.value?.wrapRef
|
|
572
|
+
if (wrap && wrap.scrollHeight - wrap.scrollTop - wrap.clientHeight < 100) {
|
|
573
|
+
nextTick(scrollToBottom)
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
})
|
|
577
|
+
if (typeof d1 === 'function') dispose.push(d1)
|
|
578
|
+
|
|
579
|
+
const d2 = receive('bot-message-sent-event', (ev: any) => {
|
|
580
|
+
addMessage(ev)
|
|
581
|
+
if (ev.selfId === selectedBot.value && ev.channelId === selectedChannel.value) {
|
|
582
|
+
nextTick(scrollToBottom)
|
|
583
|
+
}
|
|
584
|
+
})
|
|
585
|
+
if (typeof d2 === 'function') dispose.push(d2)
|
|
586
|
+
|
|
587
|
+
const d3 = receive('chat-bot-message-event', (ev: any) => {
|
|
588
|
+
addMessage(ev)
|
|
589
|
+
if (ev.selfId === selectedBot.value && ev.channelId === selectedChannel.value) {
|
|
590
|
+
nextTick(scrollToBottom)
|
|
591
|
+
}
|
|
592
|
+
})
|
|
593
|
+
if (typeof d3 === 'function') dispose.push(d3)
|
|
594
|
+
|
|
595
|
+
// 监听机器人消息更新(发送成功后从虚拟 ID 转为真实 ID)
|
|
596
|
+
const d4 = receive('bot-message-updated', (data: any) => {
|
|
597
|
+
const channelKey = `${selectedBot.value}:${selectedChannel.value}`
|
|
598
|
+
if (data.channelKey !== channelKey) return
|
|
599
|
+
|
|
600
|
+
const msg = currentMessages.value.find(m => m.id === data.tempId)
|
|
601
|
+
if (msg) {
|
|
602
|
+
msg.sending = false
|
|
603
|
+
msg.realId = data.realId
|
|
604
|
+
}
|
|
605
|
+
})
|
|
606
|
+
if (typeof d4 === 'function') dispose.push(d4)
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
onUnmounted(() => {
|
|
610
|
+
window.removeEventListener('resize', checkMobile)
|
|
611
|
+
window.removeEventListener('popstate', handlePopState)
|
|
612
|
+
window.removeEventListener('resize', updateKeyboardHeight)
|
|
613
|
+
if (window.visualViewport) {
|
|
614
|
+
window.visualViewport.removeEventListener('resize', updateKeyboardHeight)
|
|
615
|
+
window.visualViewport.removeEventListener('scroll', updateKeyboardHeight)
|
|
616
|
+
}
|
|
617
|
+
dispose.forEach(d => d?.())
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
return {
|
|
621
|
+
// 数据
|
|
622
|
+
bots,
|
|
623
|
+
selectedBot,
|
|
624
|
+
selectedChannel,
|
|
625
|
+
currentChannels,
|
|
626
|
+
currentMessages,
|
|
627
|
+
currentChannelName,
|
|
628
|
+
inputText,
|
|
629
|
+
uploadedImages,
|
|
630
|
+
isSending,
|
|
631
|
+
pinnedBots,
|
|
632
|
+
pinnedChannels,
|
|
633
|
+
scrollRef,
|
|
634
|
+
isMobile,
|
|
635
|
+
mobileView,
|
|
636
|
+
forwardData,
|
|
637
|
+
imageViewer,
|
|
638
|
+
imageZoom,
|
|
639
|
+
rawMessage,
|
|
640
|
+
isLoadingHistory,
|
|
641
|
+
forwardDialogVisible,
|
|
642
|
+
imageViewerVisible,
|
|
643
|
+
rawMessageVisible,
|
|
644
|
+
replyingTo,
|
|
645
|
+
userProfile,
|
|
646
|
+
userProfileVisible,
|
|
647
|
+
selectedBotPlatform,
|
|
648
|
+
menu,
|
|
649
|
+
keyboardHeight,
|
|
650
|
+
|
|
651
|
+
// 方法
|
|
652
|
+
selectBot,
|
|
653
|
+
selectChannel,
|
|
654
|
+
handleSend,
|
|
655
|
+
togglePinBot,
|
|
656
|
+
togglePinChannel,
|
|
657
|
+
deleteBotData,
|
|
658
|
+
deleteChannelData,
|
|
659
|
+
getCachedImageUrl,
|
|
660
|
+
cacheImage,
|
|
661
|
+
loadVideo,
|
|
662
|
+
isVideoLoading,
|
|
663
|
+
isVideoLoaded,
|
|
664
|
+
getMessages,
|
|
665
|
+
goBack,
|
|
666
|
+
showForward,
|
|
667
|
+
openImageViewer,
|
|
668
|
+
handleImageWheel,
|
|
669
|
+
downloadImage,
|
|
670
|
+
handleScroll,
|
|
671
|
+
repeatMessage,
|
|
672
|
+
handlePaste,
|
|
673
|
+
copyToClipboard,
|
|
674
|
+
onBotMenu,
|
|
675
|
+
onChannelMenu,
|
|
676
|
+
onMessageMenu,
|
|
677
|
+
handleMenuAction,
|
|
678
|
+
handleMessageAction,
|
|
679
|
+
showUserProfile,
|
|
680
|
+
inputRef,
|
|
681
|
+
scrollToMessage
|
|
682
|
+
}
|
|
683
|
+
}
|