koishi-plugin-chat-patch 0.8.2 → 1.0.6
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/index.vue +608 -22
- package/client/vue/style.css +355 -0
- package/dist/index.js +8 -8
- package/dist/style.css +1 -1
- package/lib/api-handlers.d.ts +17 -0
- package/lib/config.d.ts +13 -0
- package/lib/file-manager.d.ts +17 -0
- package/lib/index.d.ts +4 -13
- package/lib/index.js +857 -454
- package/lib/message-handler.d.ts +16 -0
- package/lib/types.d.ts +53 -0
- package/lib/utils.d.ts +7 -0
- package/package.json +3 -3
- package/src/api-handlers.ts +593 -0
- package/src/config.ts +45 -0
- package/src/file-manager.ts +162 -0
- package/src/index.ts +45 -753
- package/src/message-handler.ts +247 -0
- package/src/types.ts +58 -0
- package/src/utils.ts +43 -0
package/client/vue/index.vue
CHANGED
|
@@ -125,13 +125,54 @@
|
|
|
125
125
|
|
|
126
126
|
<!-- 输入框 -->
|
|
127
127
|
<div class="message-input">
|
|
128
|
+
<!-- 图片预览区域 -->
|
|
129
|
+
<div v-if="uploadedImages.length > 0" class="image-preview-container">
|
|
130
|
+
<div v-for="image in uploadedImages" :key="image.tempId" class="image-preview-item">
|
|
131
|
+
<img :src="image.preview" :alt="image.filename" class="preview-image" />
|
|
132
|
+
<button class="remove-image-btn" @click="removeImage(image.tempId)" title="删除图片">
|
|
133
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
|
134
|
+
<path
|
|
135
|
+
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
|
|
136
|
+
</svg>
|
|
137
|
+
</button>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
128
141
|
<div class="input-row">
|
|
142
|
+
<!-- 加号按钮 -->
|
|
143
|
+
<div class="input-actions">
|
|
144
|
+
<button class="add-button" @click="toggleActionMenu" :class="{ active: showActionMenu }" title="更多操作">
|
|
145
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"
|
|
146
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
147
|
+
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
148
|
+
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
149
|
+
</svg>
|
|
150
|
+
</button>
|
|
151
|
+
|
|
152
|
+
<!-- 操作菜单 -->
|
|
153
|
+
<div v-if="showActionMenu" class="action-menu" @click.stop>
|
|
154
|
+
<button class="action-menu-item" @click="triggerImageUpload">
|
|
155
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
156
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
157
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
158
|
+
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
|
159
|
+
<polyline points="21,15 16,10 5,21"></polyline>
|
|
160
|
+
</svg>
|
|
161
|
+
上传图片
|
|
162
|
+
</button>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
129
166
|
<input v-model="inputMessage" type="text" :placeholder="inputPlaceholder" @keyup.enter="sendMessage"
|
|
130
|
-
:disabled="!canInputMessage" ref="messageInput" />
|
|
167
|
+
:disabled="!canInputMessage" ref="messageInput" @paste="handlePaste" />
|
|
131
168
|
<button @click="sendMessage" :disabled="!canSendMessage" :class="{ 'is-sending': isSending }">
|
|
132
169
|
{{ isSending ? '发送中...' : '发送' }}
|
|
133
170
|
</button>
|
|
134
171
|
</div>
|
|
172
|
+
|
|
173
|
+
<!-- 隐藏的文件输入 -->
|
|
174
|
+
<input type="file" ref="fileInput" @change="handleFileSelect" accept="image/*" multiple
|
|
175
|
+
style="display: none;" />
|
|
135
176
|
</div>
|
|
136
177
|
</div>
|
|
137
178
|
</div>
|
|
@@ -171,6 +212,16 @@
|
|
|
171
212
|
import { ref, computed, onMounted, onUnmounted, nextTick, watch, defineComponent, h } from 'vue'
|
|
172
213
|
import { useContext, receive, send } from '@koishijs/client'
|
|
173
214
|
|
|
215
|
+
// 辅助函数:检查是否为文件 URL
|
|
216
|
+
function isFileUrl(url: string): boolean {
|
|
217
|
+
try {
|
|
218
|
+
const parsedUrl = new URL(url)
|
|
219
|
+
return parsedUrl.protocol === 'file:'
|
|
220
|
+
} catch {
|
|
221
|
+
return false
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
174
225
|
|
|
175
226
|
// 头像组件
|
|
176
227
|
const AvatarComponent = defineComponent({
|
|
@@ -311,6 +362,13 @@ const ImageComponent = defineComponent({
|
|
|
311
362
|
return
|
|
312
363
|
}
|
|
313
364
|
|
|
365
|
+
// 检查是否是本地文件路径,如果是则直接使用代理请求
|
|
366
|
+
if (isFileUrl(props.src)) {
|
|
367
|
+
console.log('ImageComponent: 检测到本地文件,使用代理请求:', props.src)
|
|
368
|
+
await loadWithCache()
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
|
|
314
372
|
// 尝试直接加载原图
|
|
315
373
|
const testImg = new Image()
|
|
316
374
|
testImg.crossOrigin = 'anonymous'
|
|
@@ -509,6 +567,115 @@ const JsonCardComponent = defineComponent({
|
|
|
509
567
|
}
|
|
510
568
|
})
|
|
511
569
|
|
|
570
|
+
// 合并转发消息组件
|
|
571
|
+
const ForwardMessageComponent = defineComponent({
|
|
572
|
+
props: {
|
|
573
|
+
element: {
|
|
574
|
+
type: Object as () => MessageElement,
|
|
575
|
+
required: true
|
|
576
|
+
},
|
|
577
|
+
channelKey: {
|
|
578
|
+
type: String,
|
|
579
|
+
required: true
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
setup(props) {
|
|
583
|
+
const isExpanded = ref(false)
|
|
584
|
+
|
|
585
|
+
const toggleExpanded = () => {
|
|
586
|
+
isExpanded.value = !isExpanded.value
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const getPreviewText = () => {
|
|
590
|
+
if (!props.element.children || props.element.children.length === 0) {
|
|
591
|
+
return {
|
|
592
|
+
previews: [],
|
|
593
|
+
messageCount: 0
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const messages = props.element.children.filter((child: any) => child.type === 'message')
|
|
598
|
+
const messageCount = messages.length
|
|
599
|
+
|
|
600
|
+
// 生成预览文本
|
|
601
|
+
const previews = messages.slice(0, 3).map((msg: any) => {
|
|
602
|
+
const nickname = msg.attrs?.nickname || '用户'
|
|
603
|
+
let content = ''
|
|
604
|
+
|
|
605
|
+
if (msg.children && msg.children.length > 0) {
|
|
606
|
+
const firstChild = msg.children[0]
|
|
607
|
+
if (firstChild.type === 'text') {
|
|
608
|
+
content = (firstChild.attrs?.content || '').substring(0, 20)
|
|
609
|
+
if (content.length > 15) content += '...'
|
|
610
|
+
} else if (firstChild.type === 'img') {
|
|
611
|
+
content = '[图片]'
|
|
612
|
+
} else if (firstChild.type === 'video') {
|
|
613
|
+
content = '[视频]'
|
|
614
|
+
} else {
|
|
615
|
+
content = `[${firstChild.type}]`
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return `${nickname}:${content}`
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
return {
|
|
623
|
+
previews,
|
|
624
|
+
messageCount
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const renderForwardedMessage = (message: any, index: number) => {
|
|
629
|
+
const nickname = message.attrs?.nickname || '用户'
|
|
630
|
+
const userId = message.attrs?.userId || 'unknown'
|
|
631
|
+
|
|
632
|
+
return h('div', {
|
|
633
|
+
key: index,
|
|
634
|
+
class: 'forwarded-message-item'
|
|
635
|
+
}, [
|
|
636
|
+
h('div', { class: 'forwarded-message-header' }, [
|
|
637
|
+
h('span', { class: 'forwarded-message-nickname' }, nickname),
|
|
638
|
+
h('span', { class: 'forwarded-message-userid' }, `(${userId})`)
|
|
639
|
+
]),
|
|
640
|
+
h('div', { class: 'forwarded-message-content' },
|
|
641
|
+
message.children?.map((child: any, childIndex: number) =>
|
|
642
|
+
h(MessageElement, {
|
|
643
|
+
key: childIndex,
|
|
644
|
+
element: child,
|
|
645
|
+
channelKey: props.channelKey
|
|
646
|
+
})
|
|
647
|
+
) || []
|
|
648
|
+
)
|
|
649
|
+
])
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
return () => {
|
|
653
|
+
const { previews, messageCount } = getPreviewText()
|
|
654
|
+
|
|
655
|
+
return h('div', { class: 'forward-message-container' }, [
|
|
656
|
+
h('div', {
|
|
657
|
+
class: 'forward-message-preview',
|
|
658
|
+
onClick: toggleExpanded
|
|
659
|
+
}, [
|
|
660
|
+
h('div', { class: 'forward-message-title' }, '聊天记录'),
|
|
661
|
+
...previews.map((preview, index) =>
|
|
662
|
+
h('div', { key: index, class: 'forward-message-preview-item' }, preview)
|
|
663
|
+
),
|
|
664
|
+
h('div', { class: 'forward-message-footer' }, [
|
|
665
|
+
h('span', { class: 'forward-message-count' }, `查看${messageCount}条转发消息`),
|
|
666
|
+
h('span', { class: 'forward-message-toggle' }, isExpanded.value ? '▲' : '▼')
|
|
667
|
+
])
|
|
668
|
+
]),
|
|
669
|
+
isExpanded.value && h('div', { class: 'forward-message-expanded' },
|
|
670
|
+
props.element.children
|
|
671
|
+
?.filter((child: any) => child.type === 'message')
|
|
672
|
+
.map((message: any, index: number) => renderForwardedMessage(message, index)) || []
|
|
673
|
+
)
|
|
674
|
+
])
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
})
|
|
678
|
+
|
|
512
679
|
const MessageElement = defineComponent({
|
|
513
680
|
props: {
|
|
514
681
|
element: {
|
|
@@ -532,6 +699,16 @@ const MessageElement = defineComponent({
|
|
|
532
699
|
case 'img':
|
|
533
700
|
case 'image':
|
|
534
701
|
const imageUrl = element.attrs.src || element.attrs.url || element.attrs.file
|
|
702
|
+
|
|
703
|
+
// 调试信息:记录图片 URL 类型
|
|
704
|
+
if (imageUrl && isFileUrl(imageUrl)) {
|
|
705
|
+
console.log('检测到本地文件路径:', imageUrl)
|
|
706
|
+
} else if (imageUrl?.startsWith('http')) {
|
|
707
|
+
console.log('检测到网络图片:', imageUrl)
|
|
708
|
+
} else {
|
|
709
|
+
console.log('检测到其他类型图片URL:', imageUrl)
|
|
710
|
+
}
|
|
711
|
+
|
|
535
712
|
return h('div', { class: 'message-image-container' }, [
|
|
536
713
|
h(ImageComponent, {
|
|
537
714
|
src: imageUrl,
|
|
@@ -543,6 +720,12 @@ const MessageElement = defineComponent({
|
|
|
543
720
|
|
|
544
721
|
case 'mface':
|
|
545
722
|
const mfaceimageUrl = element.attrs.src || element.attrs.url || element.attrs.file
|
|
723
|
+
|
|
724
|
+
// 调试信息:记录表情图片 URL 类型
|
|
725
|
+
if (mfaceimageUrl && isFileUrl(mfaceimageUrl)) {
|
|
726
|
+
console.log('检测到本地表情文件路径:', mfaceimageUrl)
|
|
727
|
+
}
|
|
728
|
+
|
|
546
729
|
return h('div', { class: 'message-image-container' }, [
|
|
547
730
|
h(ImageComponent, {
|
|
548
731
|
src: mfaceimageUrl,
|
|
@@ -580,12 +763,34 @@ const MessageElement = defineComponent({
|
|
|
580
763
|
channelKey: props.channelKey
|
|
581
764
|
})
|
|
582
765
|
])
|
|
766
|
+
|
|
767
|
+
case 'p':
|
|
768
|
+
// 处理段落元素,递归渲染子元素
|
|
769
|
+
if (element.children && element.children.length > 0) {
|
|
770
|
+
const childElements = element.children.map((child, index) =>
|
|
771
|
+
h(MessageElement, {
|
|
772
|
+
key: index,
|
|
773
|
+
element: child,
|
|
774
|
+
channelKey: props.channelKey
|
|
775
|
+
})
|
|
776
|
+
)
|
|
777
|
+
return h('div', { class: 'message-paragraph' }, childElements)
|
|
778
|
+
} else {
|
|
779
|
+
return h('div', { class: 'message-paragraph' }, '')
|
|
780
|
+
}
|
|
781
|
+
case 'figure':
|
|
782
|
+
// 处理合并转发消息,创建可折叠的消息组件
|
|
783
|
+
return h(ForwardMessageComponent, {
|
|
784
|
+
element: element,
|
|
785
|
+
channelKey: props.channelKey
|
|
786
|
+
})
|
|
583
787
|
default:
|
|
584
788
|
// 未知类型
|
|
585
789
|
return h('span', {
|
|
586
790
|
class: 'message-unknown',
|
|
587
791
|
title: `未知消息类型: ${element.type}`
|
|
588
792
|
}, element.attrs.content || `[${element.type}]`)
|
|
793
|
+
|
|
589
794
|
}
|
|
590
795
|
}
|
|
591
796
|
|
|
@@ -597,6 +802,7 @@ interface SendMessageResponse {
|
|
|
597
802
|
success: boolean
|
|
598
803
|
messageId?: string
|
|
599
804
|
error?: string
|
|
805
|
+
tempImageIds?: string[]
|
|
600
806
|
}
|
|
601
807
|
|
|
602
808
|
interface BotInfo {
|
|
@@ -673,13 +879,13 @@ const pluginConfig = ref<{
|
|
|
673
879
|
platformName: string
|
|
674
880
|
exactMatch: boolean
|
|
675
881
|
}>
|
|
676
|
-
chatContainerHeight: number
|
|
882
|
+
chatContainerHeight: number
|
|
677
883
|
}>({
|
|
678
884
|
maxMessagesPerChannel: 1000,
|
|
679
885
|
keepMessagesOnClear: 50,
|
|
680
886
|
loggerinfo: false,
|
|
681
887
|
blockedPlatforms: [],
|
|
682
|
-
chatContainerHeight: 80
|
|
888
|
+
chatContainerHeight: 80
|
|
683
889
|
})
|
|
684
890
|
|
|
685
891
|
// 图片缓存 - IndexedDB
|
|
@@ -695,6 +901,59 @@ interface ImageCacheItem {
|
|
|
695
901
|
const imageBlobUrls = ref<Record<string, string>>({})
|
|
696
902
|
const maxImagesPerChannel = 200 // 每个频道最大缓存图片数量
|
|
697
903
|
|
|
904
|
+
// 内存管理配置
|
|
905
|
+
const MAX_MEMORY_USAGE = 100 * 1024 * 1024 // 100MB 最大内存使用量
|
|
906
|
+
const MAX_BLOB_COUNT = 50 // 最大blob URL数量
|
|
907
|
+
let currentMemoryUsage = 0 // 当前内存使用量估算
|
|
908
|
+
|
|
909
|
+
// 内存管理函数
|
|
910
|
+
function estimateBlobSize(blob: Blob): number {
|
|
911
|
+
return blob.size || 0
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function updateMemoryUsage(sizeChange: number) {
|
|
915
|
+
currentMemoryUsage += sizeChange
|
|
916
|
+
if (pluginConfig.value.loggerinfo) {
|
|
917
|
+
console.log(`内存使用量变化: ${sizeChange > 0 ? '+' : ''}${(sizeChange / 1024 / 1024).toFixed(2)}MB, 总计: ${(currentMemoryUsage / 1024 / 1024).toFixed(2)}MB`)
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// 清理最旧的blob URL以释放内存
|
|
922
|
+
function cleanupOldestBlobs(targetCount: number = 10) {
|
|
923
|
+
const blobEntries = Object.entries(imageBlobUrls.value)
|
|
924
|
+
if (blobEntries.length <= targetCount) return
|
|
925
|
+
|
|
926
|
+
// 简单的LRU策略:清理最早创建的blob
|
|
927
|
+
const toRemove = blobEntries.slice(0, blobEntries.length - targetCount)
|
|
928
|
+
|
|
929
|
+
let freedMemory = 0
|
|
930
|
+
toRemove.forEach(([url, blobUrl]) => {
|
|
931
|
+
URL.revokeObjectURL(blobUrl)
|
|
932
|
+
delete imageBlobUrls.value[url]
|
|
933
|
+
freedMemory += 500 * 1024 // 估算每个图片500KB
|
|
934
|
+
if (pluginConfig.value.loggerinfo) {
|
|
935
|
+
console.log('清理旧blob URL:', url)
|
|
936
|
+
}
|
|
937
|
+
})
|
|
938
|
+
|
|
939
|
+
updateMemoryUsage(-freedMemory)
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// 检查内存使用情况并清理
|
|
943
|
+
function checkAndCleanupMemory() {
|
|
944
|
+
const blobCount = Object.keys(imageBlobUrls.value).length
|
|
945
|
+
|
|
946
|
+
// 如果blob数量过多,清理一些
|
|
947
|
+
if (blobCount > MAX_BLOB_COUNT) {
|
|
948
|
+
cleanupOldestBlobs(Math.floor(MAX_BLOB_COUNT * 0.7)) // 清理到70%
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// 如果估算内存使用过高,也进行清理
|
|
952
|
+
if (currentMemoryUsage > MAX_MEMORY_USAGE) {
|
|
953
|
+
cleanupOldestBlobs(Math.floor(MAX_BLOB_COUNT * 0.5)) // 清理到50%
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
698
957
|
// IndexedDB
|
|
699
958
|
let imageDB: IDBDatabase | null = null
|
|
700
959
|
const DB_NAME = 'ChatImageCache'
|
|
@@ -705,6 +964,16 @@ const selectedBot = ref<string>('')
|
|
|
705
964
|
const selectedChannel = ref<string>('')
|
|
706
965
|
const inputMessage = ref<string>('')
|
|
707
966
|
|
|
967
|
+
// 图片上传相关状态
|
|
968
|
+
const uploadedImages = ref<Array<{
|
|
969
|
+
tempId: string
|
|
970
|
+
filename: string
|
|
971
|
+
preview: string
|
|
972
|
+
size: number
|
|
973
|
+
}>>([])
|
|
974
|
+
const showActionMenu = ref<boolean>(false)
|
|
975
|
+
const fileInput = ref<HTMLInputElement>()
|
|
976
|
+
|
|
708
977
|
// 手机端状态管理
|
|
709
978
|
const isMobile = ref<boolean>(false)
|
|
710
979
|
const mobileView = ref<'bots' | 'channels' | 'messages'>('bots')
|
|
@@ -807,7 +1076,7 @@ const currentChannelKey = computed(() => {
|
|
|
807
1076
|
})
|
|
808
1077
|
|
|
809
1078
|
const canSendMessage = computed(() => {
|
|
810
|
-
return selectedBot.value && selectedChannel.value && inputMessage.value.trim() && !isSending.value
|
|
1079
|
+
return selectedBot.value && selectedChannel.value && (inputMessage.value.trim() || uploadedImages.value.length > 0) && !isSending.value
|
|
811
1080
|
})
|
|
812
1081
|
|
|
813
1082
|
const canInputMessage = computed(() => {
|
|
@@ -831,7 +1100,7 @@ const mobileViewClass = computed(() => {
|
|
|
831
1100
|
// 输入框提示文字
|
|
832
1101
|
const inputPlaceholder = computed(() => {
|
|
833
1102
|
if (isMobile.value) {
|
|
834
|
-
return '
|
|
1103
|
+
return '输入消息...(屏幕左滑返回)'
|
|
835
1104
|
} else {
|
|
836
1105
|
return '输入消息...'
|
|
837
1106
|
}
|
|
@@ -1066,22 +1335,48 @@ async function sendMessage() {
|
|
|
1066
1335
|
if (!canSendMessage.value) return
|
|
1067
1336
|
|
|
1068
1337
|
const messageContent = inputMessage.value.trim()
|
|
1069
|
-
if (!messageContent) return
|
|
1338
|
+
if (!messageContent && uploadedImages.value.length === 0) return
|
|
1070
1339
|
|
|
1071
1340
|
// 设置发送状态
|
|
1072
1341
|
isSending.value = true
|
|
1073
1342
|
|
|
1343
|
+
// 保存当前的图片信息,用于后续清理
|
|
1344
|
+
const currentImages = [...uploadedImages.value]
|
|
1345
|
+
|
|
1074
1346
|
try {
|
|
1075
1347
|
// 调用后端 API 发送消息
|
|
1076
1348
|
const result = await (send as any)('send-message', {
|
|
1077
1349
|
selfId: selectedBot.value,
|
|
1078
1350
|
channelId: selectedChannel.value,
|
|
1079
|
-
content: messageContent
|
|
1080
|
-
|
|
1351
|
+
content: messageContent,
|
|
1352
|
+
images: currentImages.map(img => ({
|
|
1353
|
+
tempId: img.tempId,
|
|
1354
|
+
filename: img.filename
|
|
1355
|
+
}))
|
|
1356
|
+
}) as SendMessageResponse & { tempImageIds?: string[] }
|
|
1081
1357
|
|
|
1082
1358
|
if (result.success) {
|
|
1083
|
-
//
|
|
1359
|
+
// 清空输入框和图片预览
|
|
1084
1360
|
inputMessage.value = ''
|
|
1361
|
+
|
|
1362
|
+
// 释放blob URL
|
|
1363
|
+
currentImages.forEach(img => {
|
|
1364
|
+
URL.revokeObjectURL(img.preview)
|
|
1365
|
+
})
|
|
1366
|
+
uploadedImages.value = []
|
|
1367
|
+
showActionMenu.value = false
|
|
1368
|
+
|
|
1369
|
+
// 消息发送成功后,主动通知后端清理临时文件
|
|
1370
|
+
if (result.tempImageIds && result.tempImageIds.length > 0) {
|
|
1371
|
+
try {
|
|
1372
|
+
await (send as any)('cleanup-temp-images', {
|
|
1373
|
+
tempImageIds: result.tempImageIds
|
|
1374
|
+
})
|
|
1375
|
+
console.log('临时图片清理完成:', result.tempImageIds)
|
|
1376
|
+
} catch (cleanupError) {
|
|
1377
|
+
console.warn('清理临时图片失败:', cleanupError)
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1085
1380
|
} else {
|
|
1086
1381
|
console.error('消息发送失败:', result.error)
|
|
1087
1382
|
// 使用showNotification显示错误提示
|
|
@@ -1096,6 +1391,128 @@ async function sendMessage() {
|
|
|
1096
1391
|
}
|
|
1097
1392
|
}
|
|
1098
1393
|
|
|
1394
|
+
// 图片上传相关方法
|
|
1395
|
+
function toggleActionMenu() {
|
|
1396
|
+
showActionMenu.value = !showActionMenu.value
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function triggerImageUpload() {
|
|
1400
|
+
if (fileInput.value) {
|
|
1401
|
+
fileInput.value.click()
|
|
1402
|
+
}
|
|
1403
|
+
showActionMenu.value = false
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
async function handleFileSelect(event: Event) {
|
|
1407
|
+
const target = event.target as HTMLInputElement
|
|
1408
|
+
const files = target.files
|
|
1409
|
+
if (!files || files.length === 0) return
|
|
1410
|
+
|
|
1411
|
+
for (const file of Array.from(files)) {
|
|
1412
|
+
await uploadImage(file)
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// 清空文件输入,允许重复选择同一文件
|
|
1416
|
+
target.value = ''
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
async function handlePaste(event: ClipboardEvent) {
|
|
1420
|
+
const items = event.clipboardData?.items
|
|
1421
|
+
if (!items) return
|
|
1422
|
+
|
|
1423
|
+
for (const item of Array.from(items)) {
|
|
1424
|
+
if (item.type.startsWith('image/')) {
|
|
1425
|
+
event.preventDefault()
|
|
1426
|
+
const file = item.getAsFile()
|
|
1427
|
+
if (file) {
|
|
1428
|
+
await uploadImage(file)
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
async function uploadImage(file: File) {
|
|
1435
|
+
try {
|
|
1436
|
+
// 检查文件大小 (限制为10MB)
|
|
1437
|
+
if (file.size > 10 * 1024 * 1024) {
|
|
1438
|
+
showNotification('图片文件过大,请选择小于10MB的图片', 'error')
|
|
1439
|
+
return
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// 检查文件类型
|
|
1443
|
+
if (!file.type.startsWith('image/')) {
|
|
1444
|
+
showNotification('请选择图片文件', 'error')
|
|
1445
|
+
return
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// 转换为base64
|
|
1449
|
+
const base64 = await fileToBase64(file)
|
|
1450
|
+
|
|
1451
|
+
// 创建预览URL
|
|
1452
|
+
const preview = URL.createObjectURL(file)
|
|
1453
|
+
|
|
1454
|
+
// 调用后端API上传图片
|
|
1455
|
+
const result = await (send as any)('upload-image', {
|
|
1456
|
+
file: base64,
|
|
1457
|
+
filename: file.name,
|
|
1458
|
+
mimeType: file.type
|
|
1459
|
+
})
|
|
1460
|
+
|
|
1461
|
+
if (result.success) {
|
|
1462
|
+
uploadedImages.value.push({
|
|
1463
|
+
tempId: result.tempId,
|
|
1464
|
+
filename: file.name,
|
|
1465
|
+
preview: preview,
|
|
1466
|
+
size: file.size
|
|
1467
|
+
})
|
|
1468
|
+
} else {
|
|
1469
|
+
URL.revokeObjectURL(preview)
|
|
1470
|
+
showNotification('图片上传失败: ' + result.error, 'error')
|
|
1471
|
+
}
|
|
1472
|
+
} catch (error: any) {
|
|
1473
|
+
console.error('上传图片失败:', error)
|
|
1474
|
+
showNotification('图片上传失败: ' + (error?.message || String(error)), 'error')
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
async function removeImage(tempId: string) {
|
|
1479
|
+
try {
|
|
1480
|
+
// 从列表中移除
|
|
1481
|
+
const imageIndex = uploadedImages.value.findIndex(img => img.tempId === tempId)
|
|
1482
|
+
if (imageIndex !== -1) {
|
|
1483
|
+
const image = uploadedImages.value[imageIndex]
|
|
1484
|
+
// 释放预览URL
|
|
1485
|
+
URL.revokeObjectURL(image.preview)
|
|
1486
|
+
uploadedImages.value.splice(imageIndex, 1)
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// 调用后端API删除临时文件
|
|
1490
|
+
await (send as any)('delete-temp-image', { tempId })
|
|
1491
|
+
} catch (error: any) {
|
|
1492
|
+
console.error('删除图片失败:', error)
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
function fileToBase64(file: File): Promise<string> {
|
|
1497
|
+
return new Promise((resolve, reject) => {
|
|
1498
|
+
const reader = new FileReader()
|
|
1499
|
+
reader.onload = () => resolve(reader.result as string)
|
|
1500
|
+
reader.onerror = reject
|
|
1501
|
+
reader.readAsDataURL(file)
|
|
1502
|
+
})
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// 点击外部关闭菜单
|
|
1506
|
+
function handleClickOutside(event: Event) {
|
|
1507
|
+
const target = event.target as HTMLElement
|
|
1508
|
+
if (!target.closest('.input-actions')) {
|
|
1509
|
+
showActionMenu.value = false
|
|
1510
|
+
}
|
|
1511
|
+
if (!target.closest('.context-menu')) {
|
|
1512
|
+
hideContextMenu()
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1099
1516
|
function formatTime(timestamp: number): string {
|
|
1100
1517
|
const date = new Date(timestamp)
|
|
1101
1518
|
return date.toLocaleTimeString('zh-CN', {
|
|
@@ -1128,7 +1545,7 @@ function checkScrollPosition() {
|
|
|
1128
1545
|
const { scrollTop, scrollHeight, clientHeight } = messageHistory.value
|
|
1129
1546
|
const distanceFromBottom = scrollHeight - (scrollTop + clientHeight)
|
|
1130
1547
|
const isAtBottom = distanceFromBottom <= 50
|
|
1131
|
-
|
|
1548
|
+
|
|
1132
1549
|
const shouldShowButton = !isAtBottom
|
|
1133
1550
|
|
|
1134
1551
|
showScrollButton.value = shouldShowButton
|
|
@@ -1655,10 +2072,16 @@ async function getCachedImageUrl(channelKey: string, originalUrl: string): Promi
|
|
|
1655
2072
|
const cacheItem = await getImageFromDB(originalUrl)
|
|
1656
2073
|
if (!cacheItem) return null
|
|
1657
2074
|
|
|
2075
|
+
// 检查内存使用情况
|
|
2076
|
+
checkAndCleanupMemory()
|
|
2077
|
+
|
|
1658
2078
|
// 创建blob URL并缓存到内存
|
|
1659
2079
|
const blobUrl = URL.createObjectURL(cacheItem.blob)
|
|
1660
2080
|
imageBlobUrls.value[originalUrl] = blobUrl
|
|
1661
2081
|
|
|
2082
|
+
// 更新内存使用量
|
|
2083
|
+
updateMemoryUsage(estimateBlobSize(cacheItem.blob))
|
|
2084
|
+
|
|
1662
2085
|
// 更新访问时间
|
|
1663
2086
|
cacheItem.timestamp = Date.now()
|
|
1664
2087
|
await saveImageToDB(cacheItem)
|
|
@@ -1729,9 +2152,16 @@ async function cacheImage(channelKey: string, originalUrl: string): Promise<stri
|
|
|
1729
2152
|
return null
|
|
1730
2153
|
}
|
|
1731
2154
|
|
|
2155
|
+
// 检查内存使用情况
|
|
2156
|
+
checkAndCleanupMemory()
|
|
2157
|
+
|
|
1732
2158
|
// 创建blob URL并缓存到内存
|
|
1733
2159
|
const blobUrl = URL.createObjectURL(blob)
|
|
1734
2160
|
imageBlobUrls.value[originalUrl] = blobUrl
|
|
2161
|
+
|
|
2162
|
+
// 更新内存使用量
|
|
2163
|
+
updateMemoryUsage(estimateBlobSize(blob))
|
|
2164
|
+
|
|
1735
2165
|
return blobUrl
|
|
1736
2166
|
|
|
1737
2167
|
} catch (error) {
|
|
@@ -1746,6 +2176,7 @@ async function clearChannelImageCache(channelKey: string) {
|
|
|
1746
2176
|
// 获取频道的所有图片
|
|
1747
2177
|
const channelImages = await getChannelImagesFromDB(channelKey)
|
|
1748
2178
|
|
|
2179
|
+
let freedMemory = 0
|
|
1749
2180
|
// 删除IndexedDB中的数据
|
|
1750
2181
|
for (const item of channelImages) {
|
|
1751
2182
|
await deleteImageFromDB(item.url)
|
|
@@ -1753,13 +2184,32 @@ async function clearChannelImageCache(channelKey: string) {
|
|
|
1753
2184
|
if (imageBlobUrls.value[item.url]) {
|
|
1754
2185
|
URL.revokeObjectURL(imageBlobUrls.value[item.url])
|
|
1755
2186
|
delete imageBlobUrls.value[item.url]
|
|
2187
|
+
freedMemory += item.size || 0
|
|
1756
2188
|
}
|
|
1757
2189
|
}
|
|
2190
|
+
|
|
2191
|
+
// 更新内存使用量
|
|
2192
|
+
if (freedMemory > 0) {
|
|
2193
|
+
updateMemoryUsage(-freedMemory)
|
|
2194
|
+
}
|
|
1758
2195
|
} catch (error) {
|
|
1759
2196
|
console.error('清理频道图片缓存失败:', error)
|
|
1760
2197
|
}
|
|
1761
2198
|
}
|
|
1762
2199
|
|
|
2200
|
+
// 获取内存使用统计
|
|
2201
|
+
function getMemoryStats() {
|
|
2202
|
+
const blobCount = Object.keys(imageBlobUrls.value).length
|
|
2203
|
+
return {
|
|
2204
|
+
blobCount,
|
|
2205
|
+
estimatedMemoryUsage: currentMemoryUsage,
|
|
2206
|
+
maxMemoryLimit: MAX_MEMORY_USAGE,
|
|
2207
|
+
maxBlobLimit: MAX_BLOB_COUNT,
|
|
2208
|
+
memoryUsagePercent: (currentMemoryUsage / MAX_MEMORY_USAGE * 100).toFixed(1),
|
|
2209
|
+
blobUsagePercent: (blobCount / MAX_BLOB_COUNT * 100).toFixed(1)
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
|
|
1763
2213
|
// 获取缓存统计信息
|
|
1764
2214
|
async function getCacheStats() {
|
|
1765
2215
|
if (!imageDB) return { totalImages: 0, totalSize: 0, channels: 0 }
|
|
@@ -1919,7 +2369,7 @@ function handleMessageEvent(messageEvent: any) {
|
|
|
1919
2369
|
chatData.value = { ...chatData.value }
|
|
1920
2370
|
}
|
|
1921
2371
|
|
|
1922
|
-
//
|
|
2372
|
+
// 处理机器人发送消息成功事件(通过前端发送消息API触发)
|
|
1923
2373
|
function handleBotMessageSentEvent(sentEvent: any) {
|
|
1924
2374
|
const channelKey = `${sentEvent.selfId}:${sentEvent.channelId}`
|
|
1925
2375
|
if (!chatData.value.messages[channelKey]) {
|
|
@@ -1986,6 +2436,132 @@ function handleBotMessageSentEvent(sentEvent: any) {
|
|
|
1986
2436
|
chatData.value = { ...chatData.value }
|
|
1987
2437
|
}
|
|
1988
2438
|
|
|
2439
|
+
// 处理机器人消息事件(通过before-send事件监听触发)
|
|
2440
|
+
function handleBotMessageEvent(botMessageEvent: any) {
|
|
2441
|
+
// 更新机器人信息
|
|
2442
|
+
if (!chatData.value.bots[botMessageEvent.selfId]) {
|
|
2443
|
+
chatData.value.bots[botMessageEvent.selfId] = {
|
|
2444
|
+
selfId: botMessageEvent.selfId,
|
|
2445
|
+
platform: botMessageEvent.platform,
|
|
2446
|
+
username: botMessageEvent.bot?.name || `Bot-${botMessageEvent.selfId}`,
|
|
2447
|
+
avatar: botMessageEvent.bot?.avatar,
|
|
2448
|
+
status: 'online'
|
|
2449
|
+
}
|
|
2450
|
+
} else {
|
|
2451
|
+
// 更新机器人状态和信息
|
|
2452
|
+
const existingBot = chatData.value.bots[botMessageEvent.selfId]
|
|
2453
|
+
existingBot.status = 'online'
|
|
2454
|
+
if (botMessageEvent.bot?.name && existingBot.username !== botMessageEvent.bot.name) {
|
|
2455
|
+
existingBot.username = botMessageEvent.bot.name
|
|
2456
|
+
}
|
|
2457
|
+
if (botMessageEvent.bot?.avatar && existingBot.avatar !== botMessageEvent.bot.avatar) {
|
|
2458
|
+
existingBot.avatar = botMessageEvent.bot.avatar
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
// 更新频道信息
|
|
2463
|
+
if (!chatData.value.channels[botMessageEvent.selfId]) {
|
|
2464
|
+
chatData.value.channels[botMessageEvent.selfId] = {}
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
if (botMessageEvent.channelId && !chatData.value.channels[botMessageEvent.selfId][botMessageEvent.channelId]) {
|
|
2468
|
+
const channelName = botMessageEvent.guildId
|
|
2469
|
+
? `${botMessageEvent.guildName || botMessageEvent.guildId} (${botMessageEvent.channelId})`
|
|
2470
|
+
: `私信 ${botMessageEvent.channelId}`
|
|
2471
|
+
|
|
2472
|
+
chatData.value.channels[botMessageEvent.selfId][botMessageEvent.channelId] = {
|
|
2473
|
+
id: botMessageEvent.channelId,
|
|
2474
|
+
name: channelName,
|
|
2475
|
+
type: botMessageEvent.channelType || 0,
|
|
2476
|
+
guildId: botMessageEvent.guildId,
|
|
2477
|
+
guildName: botMessageEvent.guildName || botMessageEvent.guildId || '私聊'
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
// 添加机器人消息
|
|
2482
|
+
if (botMessageEvent.messageId && botMessageEvent.content && botMessageEvent.channelId) {
|
|
2483
|
+
const channelKey = `${botMessageEvent.selfId}:${botMessageEvent.channelId}`
|
|
2484
|
+
if (!chatData.value.messages[channelKey]) {
|
|
2485
|
+
chatData.value.messages[channelKey] = []
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
// 检查消息是否已存在
|
|
2489
|
+
const exists = chatData.value.messages[channelKey].find(m => m.id === botMessageEvent.messageId)
|
|
2490
|
+
if (!exists) {
|
|
2491
|
+
const message: MessageInfo = {
|
|
2492
|
+
id: botMessageEvent.messageId,
|
|
2493
|
+
content: botMessageEvent.content,
|
|
2494
|
+
userId: botMessageEvent.userId,
|
|
2495
|
+
username: botMessageEvent.username,
|
|
2496
|
+
avatar: botMessageEvent.avatar,
|
|
2497
|
+
timestamp: botMessageEvent.timestamp,
|
|
2498
|
+
channelId: botMessageEvent.channelId,
|
|
2499
|
+
selfId: botMessageEvent.selfId,
|
|
2500
|
+
elements: botMessageEvent.elements,
|
|
2501
|
+
isBot: true, // 标记为机器人消息
|
|
2502
|
+
quote: botMessageEvent.quote
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
// 按时间戳排序插入消息
|
|
2506
|
+
const messages = chatData.value.messages[channelKey]
|
|
2507
|
+
let insertIndex = messages.length
|
|
2508
|
+
|
|
2509
|
+
// 找到正确的插入位置(按时间戳排序)
|
|
2510
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2511
|
+
if (messages[i].timestamp <= botMessageEvent.timestamp) {
|
|
2512
|
+
insertIndex = i + 1
|
|
2513
|
+
break
|
|
2514
|
+
}
|
|
2515
|
+
if (i === 0) {
|
|
2516
|
+
insertIndex = 0
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
messages.splice(insertIndex, 0, message)
|
|
2521
|
+
|
|
2522
|
+
// 保持消息数量限制
|
|
2523
|
+
if (messages.length > 100) {
|
|
2524
|
+
chatData.value.messages[channelKey] = messages.slice(-100)
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
// 更新频道消息数量缓存
|
|
2528
|
+
channelMessageCounts.value[channelKey] = messages.length
|
|
2529
|
+
|
|
2530
|
+
// 在添加新消息前检查用户是否在底部附近
|
|
2531
|
+
const wasNearBottom = isNearBottom()
|
|
2532
|
+
|
|
2533
|
+
// 智能滚动:基于添加消息前的位置状态来决定是否滚动
|
|
2534
|
+
nextTick(() => {
|
|
2535
|
+
// 再次等待,确保新消息的DOM已经渲染
|
|
2536
|
+
setTimeout(() => {
|
|
2537
|
+
if (wasNearBottom) {
|
|
2538
|
+
scrollToBottom()
|
|
2539
|
+
}
|
|
2540
|
+
}, 10)
|
|
2541
|
+
})
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
// 异步预缓存消息中的图片
|
|
2546
|
+
if (botMessageEvent.elements && botMessageEvent.elements.length > 0) {
|
|
2547
|
+
const channelKey = `${botMessageEvent.selfId}:${botMessageEvent.channelId}`
|
|
2548
|
+
botMessageEvent.elements.forEach((element: any) => {
|
|
2549
|
+
if ((element.type === 'img' || element.type === 'image' || element.type === 'mface') && element.attrs) {
|
|
2550
|
+
const imageUrl = element.attrs.src || element.attrs.url || element.attrs.file
|
|
2551
|
+
if (imageUrl) {
|
|
2552
|
+
// 异步缓存,不阻塞消息显示
|
|
2553
|
+
cacheImage(channelKey, imageUrl).catch(error => {
|
|
2554
|
+
console.warn('预缓存图片失败:', imageUrl, error)
|
|
2555
|
+
})
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
})
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
// 触发响应式更新
|
|
2562
|
+
chatData.value = { ...chatData.value }
|
|
2563
|
+
}
|
|
2564
|
+
|
|
1989
2565
|
// 监听消息变化,只在切换频道时自动滚动到底部
|
|
1990
2566
|
watch(currentMessages, (newMessages, oldMessages) => {
|
|
1991
2567
|
// 只有在切换频道时(消息数组完全不同)才自动滚动
|
|
@@ -2027,10 +2603,8 @@ async function loadChatData() {
|
|
|
2027
2603
|
// 按时间戳排序
|
|
2028
2604
|
convertedChannelMessages.sort((a, b) => a.timestamp - b.timestamp)
|
|
2029
2605
|
|
|
2030
|
-
//
|
|
2031
|
-
|
|
2032
|
-
const frontendChannelKey = `${selfId}:${channelId}`
|
|
2033
|
-
convertedMessages[frontendChannelKey] = convertedChannelMessages
|
|
2606
|
+
// 现在后端已经使用冒号格式,直接使用即可
|
|
2607
|
+
convertedMessages[channelKey] = convertedChannelMessages
|
|
2034
2608
|
}
|
|
2035
2609
|
|
|
2036
2610
|
// 更新聊天数据
|
|
@@ -2063,9 +2637,8 @@ async function loadAllChannelMessageCounts() {
|
|
|
2063
2637
|
// 转换格式:从 "selfId-channelId" 到 "selfId:channelId"
|
|
2064
2638
|
const convertedCounts: Record<string, number> = {}
|
|
2065
2639
|
for (const [channelKey, count] of Object.entries(result.counts)) {
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
convertedCounts[frontendChannelKey] = count as number
|
|
2640
|
+
// 现在后端已经使用冒号格式,直接使用即可
|
|
2641
|
+
convertedCounts[channelKey] = count as number
|
|
2069
2642
|
}
|
|
2070
2643
|
|
|
2071
2644
|
channelMessageCounts.value = convertedCounts
|
|
@@ -2177,8 +2750,8 @@ function handleTouchMove(event: TouchEvent) {
|
|
|
2177
2750
|
isSwipeActive.value = true
|
|
2178
2751
|
|
|
2179
2752
|
// 显示滑动指示器
|
|
2180
|
-
const swipeDistance = Math.min(deltaX,
|
|
2181
|
-
const threshold =
|
|
2753
|
+
const swipeDistance = Math.min(deltaX, 200)
|
|
2754
|
+
const threshold = 150 // 增加阈值,减少误触
|
|
2182
2755
|
|
|
2183
2756
|
if (swipeDistance > threshold) {
|
|
2184
2757
|
swipeIndicator.value = { show: true, text: '松开返回' }
|
|
@@ -2207,9 +2780,9 @@ function handleTouchEnd(event: TouchEvent) {
|
|
|
2207
2780
|
const deltaY = touchCurrent.value.y - touchStart.value.y
|
|
2208
2781
|
|
|
2209
2782
|
// 检查是否满足返回条件 (水平滑动)
|
|
2210
|
-
const isRightSwipe = deltaX >
|
|
2783
|
+
const isRightSwipe = deltaX > 150 // 滑动距离超过150px,减少误触
|
|
2211
2784
|
const isHorizontal = Math.abs(deltaX) > Math.abs(deltaY) // 水平滑动
|
|
2212
|
-
const isFastHorizontalSwipe = duration < 300 && deltaX >
|
|
2785
|
+
const isFastHorizontalSwipe = duration < 300 && deltaX > 80 // 快速水平滑动,也增加阈值
|
|
2213
2786
|
|
|
2214
2787
|
if ((isRightSwipe && isHorizontal) || isFastHorizontalSwipe) {
|
|
2215
2788
|
performSwipeBack()
|
|
@@ -2250,6 +2823,9 @@ onMounted(async () => {
|
|
|
2250
2823
|
checkMobile()
|
|
2251
2824
|
window.addEventListener('resize', checkMobile)
|
|
2252
2825
|
|
|
2826
|
+
// 添加点击外部关闭菜单的监听器
|
|
2827
|
+
document.addEventListener('click', handleClickOutside)
|
|
2828
|
+
|
|
2253
2829
|
// 初始化IndexedDB
|
|
2254
2830
|
const dbInitialized = await initImageDB()
|
|
2255
2831
|
if (!dbInitialized) {
|
|
@@ -2265,6 +2841,7 @@ onMounted(async () => {
|
|
|
2265
2841
|
// 然后开始监听消息事件
|
|
2266
2842
|
const dispose1 = receive('chat-message-event', handleMessageEvent) as (() => void) | undefined
|
|
2267
2843
|
const dispose2 = receive('bot-message-sent-event', handleBotMessageSentEvent) as (() => void) | undefined
|
|
2844
|
+
const dispose3 = receive('chat-bot-message-event', handleBotMessageEvent) as (() => void) | undefined
|
|
2268
2845
|
|
|
2269
2846
|
// 添加滚动监听
|
|
2270
2847
|
watch(selectedChannel, (newChannelId) => {
|
|
@@ -2283,9 +2860,15 @@ onMounted(async () => {
|
|
|
2283
2860
|
}
|
|
2284
2861
|
}, { immediate: true }); // immediate: true 确保在组件挂载时也执行一次
|
|
2285
2862
|
|
|
2863
|
+
// 定期检查和清理内存(每2分钟)
|
|
2864
|
+
setInterval(() => {
|
|
2865
|
+
checkAndCleanupMemory()
|
|
2866
|
+
}, 2 * 60 * 1000)
|
|
2867
|
+
|
|
2286
2868
|
// 在组件卸载时清理监听器
|
|
2287
2869
|
onUnmounted(() => {
|
|
2288
2870
|
window.removeEventListener('resize', checkMobile)
|
|
2871
|
+
document.removeEventListener('click', handleClickOutside)
|
|
2289
2872
|
|
|
2290
2873
|
if (dispose1 && typeof dispose1 === 'function') {
|
|
2291
2874
|
dispose1()
|
|
@@ -2293,6 +2876,9 @@ onMounted(async () => {
|
|
|
2293
2876
|
if (dispose2 && typeof dispose2 === 'function') {
|
|
2294
2877
|
dispose2()
|
|
2295
2878
|
}
|
|
2879
|
+
if (dispose3 && typeof dispose3 === 'function') {
|
|
2880
|
+
dispose3()
|
|
2881
|
+
}
|
|
2296
2882
|
|
|
2297
2883
|
// 确保在卸载时移除监听器
|
|
2298
2884
|
if (messageHistory.value) {
|