koishi-plugin-chat-patch 1.2.0 → 1.3.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.
@@ -1,362 +1,362 @@
1
- <style scoped src="./style.css"></style>
2
-
3
- <template>
4
- <div class="chat-container" :class="mobileViewClass" :style="chatContainerStyle" @touchstart="handleTouchStart"
5
- @touchmove="handleTouchMove" @touchend="handleTouchEnd">
6
- <!-- 左侧机器人列表 -->
7
- <div class="bot-list">
8
- <div class="panel-header">
9
- <h3>机器人</h3>
10
- </div>
11
- <div class="bot-items">
12
- <div v-for="bot in bots" :key="bot.selfId"
13
- :class="['bot-item', { active: selectedBot === bot.selfId, pinned: pinnedBots.has(bot.selfId) }]"
14
- @click="selectBot(bot.selfId)" @contextmenu="handleBotRightClick($event, bot.selfId)">
15
- <div class="bot-avatar">
16
- <AvatarComponent v-if="bot.avatar" :src="bot.avatar" :alt="bot.username" :channel-key="'bot-list'" />
17
- <div v-else class="avatar-placeholder">{{ bot.username.charAt(0).toUpperCase() }}</div>
18
- </div>
19
- <div class="bot-info">
20
- <div class="bot-name">{{ bot.username }}</div>
21
- <div class="bot-platform">{{ bot.platform }}</div>
22
- </div>
23
- <div :class="['bot-status', bot.status]"></div>
24
- </div>
25
- </div>
26
- </div>
27
-
28
- <!-- 中间频道列表 -->
29
- <div class="channel-list">
30
- <div class="panel-header">
31
- <h3>频道</h3>
32
- </div>
33
- <div v-if="!selectedBot" class="empty-state">
34
- 请选择一个机器人
35
- </div>
36
- <div v-else class="channel-items">
37
- <div v-for="channel in currentChannels" :key="channel.id"
38
- :class="['channel-item', { active: selectedChannel === channel.id, pinned: pinnedChannels.has(`${selectedBot}:${channel.id}`) }]"
39
- :data-channel-id="channel.id" @click="selectChannel(channel.id)"
40
- @contextmenu="handleChannelRightClick($event, channel.id)">
41
- <div class="channel-info">
42
- <div class="channel-name">{{ channel.name }}</div>
43
- <div class="channel-type">{{ getChannelTypeText(channel.type) }}</div>
44
- </div>
45
- <div v-if="getChannelMessageCount(channel.id) > 0" class="channel-message-count draggable-bubble" :class="{
46
- 'dragging': draggingChannel === channel.id,
47
- 'will-delete': draggingChannel === channel.id && getDragDistance(channel.id) > dragThreshold
48
- }" @mousedown="startDrag($event, channel.id)" @touchstart="startDrag($event, channel.id)"
49
- :style="getDragStyle(channel.id)"
50
- :title="draggingChannel === channel.id ? (getDragDistance(channel.id) > dragThreshold ? '松开清理历史记录' : '拖拽更远以清理历史记录') : '拖拽清理历史记录'">
51
- {{ getChannelMessageCount(channel.id) }}
52
-
53
- </div>
54
- </div>
55
- </div>
56
- </div>
57
-
58
- <!-- 右侧消息区域 -->
59
- <div class="message-area">
60
- <div class="panel-header">
61
- <h3>{{ currentChannelName || '选择频道' }}</h3>
62
- </div>
63
- <div v-if="!selectedBot || !selectedChannel" class="empty-state">
64
- 请选择机器人和频道
65
- </div>
66
- <div v-else class="message-content">
67
- <!-- 消息历史 -->
68
- <div class="message-history" ref="messageHistory">
69
- <!-- 加载更多指示器 -->
70
- <div v-if="isLoadingMore" class="loading-more-indicator">
71
- <div class="loading-spinner"></div>
72
- <span>加载更多消息中...</span>
73
- </div>
74
-
75
- <div v-for="message in currentMessages" :key="message.id"
76
- :class="['message-item', { 'bot-message': message.isBot }]"
77
- @contextmenu="message.isBot ? null : handleMessageRightClick($event, message)">
78
- <div class="message-avatar">
79
- <AvatarComponent v-if="message.avatar" :src="message.avatar" :alt="message.username"
80
- :channel-key="currentChannelKey" />
81
- <div v-else class="avatar-placeholder">{{ message.username.charAt(0).toUpperCase() }}</div>
82
- </div>
83
- <div class="message-content-wrapper">
84
- <div class="message-header">
85
- <span class="message-username">{{ message.username }}</span>
86
- <span class="message-time">{{ formatTime(message.timestamp) }}</span>
87
- </div>
88
-
89
- <!-- 引用消息显示 -->
90
- <div v-if="message.quote || getInlineQuoteMessage(message, currentMessages)" class="message-quote">
91
- <div class="quote-header">
92
- <div class="quote-avatar">
93
- <AvatarComponent v-if="getQuoteUser(message, currentMessages).avatar"
94
- :src="getQuoteUser(message, currentMessages).avatar"
95
- :alt="getQuoteUser(message, currentMessages).username" :channel-key="currentChannelKey" />
96
- <div v-else class="avatar-placeholder">{{
97
- getQuoteUser(message, currentMessages).username.charAt(0).toUpperCase() }}
98
- </div>
99
- </div>
100
- <span class="quote-username">{{ getQuoteUser(message, currentMessages).username }}</span>
101
- <span class="quote-time">{{ formatTime(getQuoteTimestamp(message, currentMessages)) }}</span>
102
- </div>
103
- <div class="quote-content">
104
- <template
105
- v-if="getQuoteElements(message, currentMessages) && getQuoteElements(message, currentMessages).length > 0">
106
- <MessageElement v-for="(element, index) in getQuoteElements(message, currentMessages)"
107
- :key="`quote-${index}`" :element="element" :channel-key="currentChannelKey" />
108
- </template>
109
- <template v-else>
110
- {{ getQuoteContent(message, currentMessages) }}
111
- </template>
112
- </div>
113
- </div>
114
-
115
- <div class="message-text">
116
- <template v-if="message.elements && message.elements.length > 0">
117
- <MessageElement v-for="(element, index) in message.elements" :key="index" :element="element"
118
- :channel-key="currentChannelKey" />
119
- </template>
120
- <template v-else>
121
- {{ getMessageContentWithoutQuote(message, currentMessages) }}
122
- </template>
123
- </div>
124
- </div>
125
- </div>
126
- </div>
127
-
128
- <!-- 悬浮的滚动到底部按钮 -->
129
- <div class="floating-scroll-button" v-show="showScrollButton" @click="scrollToBottom">
130
- <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
131
- <path d="M7 10l5 5 5-5z" />
132
- </svg>
133
- </div>
134
-
135
- <!-- 输入框 -->
136
- <div class="message-input">
137
- <!-- 图片预览区域 -->
138
- <div v-if="uploadedImages.length > 0" class="image-preview-container">
139
- <div v-for="image in uploadedImages" :key="image.tempId" class="image-preview-item">
140
- <img :src="image.preview" :alt="image.filename" class="preview-image" />
141
- <button class="remove-image-btn" @click="removeImage(image.tempId)" title="删除图片">
142
- <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
143
- <path
144
- 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" />
145
- </svg>
146
- </button>
147
- </div>
148
- </div>
149
-
150
- <div class="input-row">
151
- <!-- 加号按钮 -->
152
- <div class="input-actions">
153
- <button class="add-button" @click="toggleActionMenu" :class="{ active: showActionMenu }" title="更多操作">
154
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"
155
- stroke-linecap="round" stroke-linejoin="round">
156
- <line x1="12" y1="5" x2="12" y2="19"></line>
157
- <line x1="5" y1="12" x2="19" y2="12"></line>
158
- </svg>
159
- </button>
160
-
161
- <!-- 操作菜单 -->
162
- <div v-if="showActionMenu" class="action-menu" @click.stop>
163
- <button class="action-menu-item" @click="triggerImageUpload">
164
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
165
- stroke-linecap="round" stroke-linejoin="round">
166
- <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
167
- <circle cx="8.5" cy="8.5" r="1.5"></circle>
168
- <polyline points="21,15 16,10 5,21"></polyline>
169
- </svg>
170
- 上传图片
171
- </button>
172
- </div>
173
- </div>
174
-
175
- <input v-model="inputMessage" type="text" :placeholder="inputPlaceholder" @keyup.enter="sendMessage"
176
- :disabled="!canInputMessage" ref="messageInput" @paste="handlePaste" @focus="handleInputFocus" />
177
- <button @click="sendMessage" :disabled="!canSendMessage" :class="{ 'is-sending': isSending }">
178
- {{ isSending ? '发送中...' : '发送' }}
179
- </button>
180
- </div>
181
-
182
- <!-- 隐藏的文件输入 -->
183
- <input type="file" ref="fileInput" @change="handleFileSelect" accept="image/*" multiple
184
- style="display: none;" />
185
- </div>
186
- </div>
187
- </div>
188
-
189
- <!-- 右键菜单 -->
190
- <div v-if="contextMenu.show" class="context-menu" :style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"
191
- @click.stop>
192
- <!-- 机器人右键菜单 -->
193
- <template v-if="contextMenu.type === 'bot'">
194
- <div class="context-menu-item" @click="toggleBotPin(contextMenu.targetId)">
195
- {{ pinnedBots.has(contextMenu.targetId) ? '取消置顶' : '置顶' }}
196
- </div>
197
- <div class="context-menu-item danger" @click="deleteBotMessages(contextMenu.targetId)">
198
- 彻底删除此机器人所有数据
199
- </div>
200
- </template>
201
-
202
- <!-- 频道右键菜单 -->
203
- <template v-if="contextMenu.type === 'channel'">
204
- <div class="context-menu-item" @click="toggleChannelPin(contextMenu.targetId)">
205
- {{ pinnedChannels.has(`${selectedBot}:${contextMenu.targetId}`) ? '取消置顶' : '置顶' }}
206
- </div>
207
- <div class="context-menu-item danger" @click="deleteChannelMessages(contextMenu.targetId)">
208
- 彻底删除此频道所有数据
209
- </div>
210
- </template>
211
-
212
- <!-- 消息右键菜单 -->
213
- <template v-if="contextMenu.type === 'message' && contextMenu.message">
214
- <div class="context-menu-item" @click="handlePlusOne(contextMenu.message)">
215
- +1
216
- </div>
217
- <div class="context-menu-item" @click="handleCopyMessage(contextMenu.message)">
218
- 复制
219
- </div>
220
- <div class="context-menu-item" @click="handleReplyMessage(contextMenu.message)">
221
- 回复
222
- </div>
223
- </template>
224
- </div>
225
-
226
- <!-- 滑动指示器 -->
227
- <div class="swipe-indicator" :class="{ show: swipeIndicator.show }">
228
- {{ swipeIndicator.text }}
229
- </div>
230
- </div>
231
- </template>
232
-
233
- <script setup lang="ts">
234
- import { useChatLogic } from './chat-logic'
235
-
236
- const chatLogic = useChatLogic()
237
-
238
- // 解构
239
- const {
240
- // 组件
241
- AvatarComponent,
242
- ImageComponent,
243
- JsonCardComponent,
244
- ForwardMessageComponent,
245
- MessageElement,
246
-
247
- // 响应式数据
248
- chatData,
249
- channelMessageCounts,
250
- pluginConfig,
251
- selectedBot,
252
- selectedChannel,
253
- inputMessage,
254
- imageBlobUrls,
255
- pinnedBots,
256
- pinnedChannels,
257
- uploadedImages,
258
- showActionMenu,
259
- isMobile,
260
- mobileView,
261
- touchStart,
262
- touchCurrent,
263
- isSwipeActive,
264
- swipeIndicator,
265
- messageHistory,
266
- messageInput,
267
- showScrollButton,
268
- isUserScrolling,
269
- isSending,
270
- isLoadingMore,
271
- draggingChannel,
272
- dragStartPos,
273
- dragCurrentPos,
274
- dragElementInitialPos,
275
- dragOffset,
276
- dragThreshold,
277
- isDragReady,
278
- draggedBubbleElement,
279
- contextMenu,
280
- fileInput,
281
-
282
- // 计算属性
283
- bots,
284
- currentChannels,
285
- currentMessages,
286
- currentChannelName,
287
- currentChannelKey,
288
- canSendMessage,
289
- canInputMessage,
290
- mobileViewClass,
291
- inputPlaceholder,
292
- chatContainerStyle,
293
-
294
- // 方法
295
- selectBot,
296
- selectChannel,
297
- handleBotRightClick,
298
- handleChannelRightClick,
299
- showContextMenu,
300
- hideContextMenu,
301
- handleKeyDown,
302
- toggleBotPin,
303
- toggleChannelPin,
304
- deleteBotMessages,
305
- deleteChannelMessages,
306
- sendMessage,
307
- toggleActionMenu,
308
- triggerImageUpload,
309
- handleFileSelect,
310
- handlePaste,
311
- uploadImage,
312
- removeImage,
313
- fileToBase64,
314
- handleClickOutside,
315
- formatTime,
316
- getChannelTypeText,
317
- scrollToBottom,
318
- checkScrollPosition,
319
- isNearBottom,
320
- getChannelMessageCount,
321
- startDrag,
322
- handleDragMove,
323
- handleDragEnd,
324
- resetDragState,
325
- getDragStyle,
326
- getDragDistance,
327
- clearChannelHistory,
328
- showNotification,
329
- createThresholdCircle,
330
- removeThresholdCircle,
331
- handleTouchStart,
332
- handleTouchMove,
333
- handleTouchEnd,
334
- handleInputFocus,
335
-
336
- // 消息右键菜单处理函数
337
- handleMessageRightClick,
338
- handlePlusOne,
339
- handleCopyMessage,
340
- handleReplyMessage,
341
-
342
- // 图片缓存相关
343
- getCachedImageUrl,
344
- cacheImage,
345
- clearChannelImageCache,
346
- getMemoryStats,
347
- getCacheStats,
348
-
349
- // 其他工具函数
350
- isFileUrl,
351
- loadHistoryMessages,
352
- handleMessageEvent,
353
- handleBotMessageSentEvent,
354
- parseInlineQuote,
355
- getInlineQuoteMessage,
356
- getQuoteUser,
357
- getQuoteTimestamp,
358
- getQuoteContent,
359
- getQuoteElements,
360
- getMessageContentWithoutQuote
361
- } = chatLogic
1
+ <style scoped src="./style.css"></style>
2
+
3
+ <template>
4
+ <div class="chat-container" :class="mobileViewClass" :style="chatContainerStyle" @touchstart="handleTouchStart"
5
+ @touchmove="handleTouchMove" @touchend="handleTouchEnd">
6
+ <!-- 左侧机器人列表 -->
7
+ <div class="bot-list">
8
+ <div class="panel-header">
9
+ <h3>机器人</h3>
10
+ </div>
11
+ <div class="bot-items">
12
+ <div v-for="bot in bots" :key="bot.selfId"
13
+ :class="['bot-item', { active: selectedBot === bot.selfId, pinned: pinnedBots.has(bot.selfId) }]"
14
+ @click="selectBot(bot.selfId)" @contextmenu="handleBotRightClick($event, bot.selfId)">
15
+ <div class="bot-avatar">
16
+ <AvatarComponent v-if="bot.avatar" :src="bot.avatar" :alt="bot.username" :channel-key="'bot-list'" />
17
+ <div v-else class="avatar-placeholder">{{ bot.username.charAt(0).toUpperCase() }}</div>
18
+ </div>
19
+ <div class="bot-info">
20
+ <div class="bot-name">{{ bot.username }}</div>
21
+ <div class="bot-platform">{{ bot.platform }}</div>
22
+ </div>
23
+ <div :class="['bot-status', bot.status]"></div>
24
+ </div>
25
+ </div>
26
+ </div>
27
+
28
+ <!-- 中间频道列表 -->
29
+ <div class="channel-list">
30
+ <div class="panel-header">
31
+ <h3>频道</h3>
32
+ </div>
33
+ <div v-if="!selectedBot" class="empty-state">
34
+ 请选择一个机器人
35
+ </div>
36
+ <div v-else class="channel-items">
37
+ <div v-for="channel in currentChannels" :key="channel.id"
38
+ :class="['channel-item', { active: selectedChannel === channel.id, pinned: pinnedChannels.has(`${selectedBot}:${channel.id}`) }]"
39
+ :data-channel-id="channel.id" @click="selectChannel(channel.id)"
40
+ @contextmenu="handleChannelRightClick($event, channel.id)">
41
+ <div class="channel-info">
42
+ <div class="channel-name">{{ channel.name }}</div>
43
+ <div class="channel-type">{{ getChannelTypeText(channel.type) }}</div>
44
+ </div>
45
+ <div v-if="getChannelMessageCount(channel.id) > 0" class="channel-message-count draggable-bubble" :class="{
46
+ 'dragging': draggingChannel === channel.id,
47
+ 'will-delete': draggingChannel === channel.id && getDragDistance(channel.id) > dragThreshold
48
+ }" @mousedown="startDrag($event, channel.id)" @touchstart="startDrag($event, channel.id)"
49
+ :style="getDragStyle(channel.id)"
50
+ :title="draggingChannel === channel.id ? (getDragDistance(channel.id) > dragThreshold ? '松开清理历史记录' : '拖拽更远以清理历史记录') : '拖拽清理历史记录'">
51
+ {{ getChannelMessageCount(channel.id) }}
52
+
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </div>
57
+
58
+ <!-- 右侧消息区域 -->
59
+ <div class="message-area">
60
+ <div class="panel-header">
61
+ <h3>{{ currentChannelName || '选择频道' }}</h3>
62
+ </div>
63
+ <div v-if="!selectedBot || !selectedChannel" class="empty-state">
64
+ 请选择机器人和频道
65
+ </div>
66
+ <div v-else class="message-content">
67
+ <!-- 消息历史 -->
68
+ <div class="message-history" ref="messageHistory">
69
+ <!-- 加载更多指示器 -->
70
+ <div v-if="isLoadingMore" class="loading-more-indicator">
71
+ <div class="loading-spinner"></div>
72
+ <span>加载更多消息中...</span>
73
+ </div>
74
+
75
+ <div v-for="message in currentMessages" :key="message.id"
76
+ :class="['message-item', { 'bot-message': message.isBot }]"
77
+ @contextmenu="message.isBot ? null : handleMessageRightClick($event, message)">
78
+ <div class="message-avatar">
79
+ <AvatarComponent v-if="message.avatar" :src="message.avatar" :alt="message.username"
80
+ :channel-key="currentChannelKey" />
81
+ <div v-else class="avatar-placeholder">{{ message.username.charAt(0).toUpperCase() }}</div>
82
+ </div>
83
+ <div class="message-content-wrapper">
84
+ <div class="message-header">
85
+ <span class="message-username">{{ message.username }}</span>
86
+ <span class="message-time">{{ formatTime(message.timestamp) }}</span>
87
+ </div>
88
+
89
+ <!-- 引用消息显示 -->
90
+ <div v-if="message.quote || getInlineQuoteMessage(message, currentMessages)" class="message-quote">
91
+ <div class="quote-header">
92
+ <div class="quote-avatar">
93
+ <AvatarComponent v-if="getQuoteUser(message, currentMessages).avatar"
94
+ :src="getQuoteUser(message, currentMessages).avatar"
95
+ :alt="getQuoteUser(message, currentMessages).username" :channel-key="currentChannelKey" />
96
+ <div v-else class="avatar-placeholder">{{
97
+ getQuoteUser(message, currentMessages).username.charAt(0).toUpperCase() }}
98
+ </div>
99
+ </div>
100
+ <span class="quote-username">{{ getQuoteUser(message, currentMessages).username }}</span>
101
+ <span class="quote-time">{{ formatTime(getQuoteTimestamp(message, currentMessages)) }}</span>
102
+ </div>
103
+ <div class="quote-content">
104
+ <template
105
+ v-if="getQuoteElements(message, currentMessages) && getQuoteElements(message, currentMessages).length > 0">
106
+ <MessageElement v-for="(element, index) in getQuoteElements(message, currentMessages)"
107
+ :key="`quote-${index}`" :element="element" :channel-key="currentChannelKey" />
108
+ </template>
109
+ <template v-else>
110
+ {{ getQuoteContent(message, currentMessages) }}
111
+ </template>
112
+ </div>
113
+ </div>
114
+
115
+ <div class="message-text">
116
+ <template v-if="message.elements && message.elements.length > 0">
117
+ <MessageElement v-for="(element, index) in message.elements" :key="index" :element="element"
118
+ :channel-key="currentChannelKey" />
119
+ </template>
120
+ <template v-else>
121
+ {{ getMessageContentWithoutQuote(message, currentMessages) }}
122
+ </template>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ <!-- 悬浮的滚动到底部按钮 -->
129
+ <div class="floating-scroll-button" v-show="showScrollButton" @click="scrollToBottom">
130
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
131
+ <path d="M7 10l5 5 5-5z" />
132
+ </svg>
133
+ </div>
134
+
135
+ <!-- 输入框 -->
136
+ <div class="message-input">
137
+ <!-- 图片预览区域 -->
138
+ <div v-if="uploadedImages.length > 0" class="image-preview-container">
139
+ <div v-for="image in uploadedImages" :key="image.tempId" class="image-preview-item">
140
+ <img :src="image.preview" :alt="image.filename" class="preview-image" />
141
+ <button class="remove-image-btn" @click="removeImage(image.tempId)" title="删除图片">
142
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
143
+ <path
144
+ 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" />
145
+ </svg>
146
+ </button>
147
+ </div>
148
+ </div>
149
+
150
+ <div class="input-row">
151
+ <!-- 加号按钮 -->
152
+ <div class="input-actions">
153
+ <button class="add-button" @click="toggleActionMenu" :class="{ active: showActionMenu }" title="更多操作">
154
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"
155
+ stroke-linecap="round" stroke-linejoin="round">
156
+ <line x1="12" y1="5" x2="12" y2="19"></line>
157
+ <line x1="5" y1="12" x2="19" y2="12"></line>
158
+ </svg>
159
+ </button>
160
+
161
+ <!-- 操作菜单 -->
162
+ <div v-if="showActionMenu" class="action-menu" @click.stop>
163
+ <button class="action-menu-item" @click="triggerImageUpload">
164
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
165
+ stroke-linecap="round" stroke-linejoin="round">
166
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
167
+ <circle cx="8.5" cy="8.5" r="1.5"></circle>
168
+ <polyline points="21,15 16,10 5,21"></polyline>
169
+ </svg>
170
+ 上传图片
171
+ </button>
172
+ </div>
173
+ </div>
174
+
175
+ <input v-model="inputMessage" type="text" :placeholder="inputPlaceholder" @keyup.enter="sendMessage"
176
+ :disabled="!canInputMessage" ref="messageInput" @paste="handlePaste" @focus="handleInputFocus" />
177
+ <button @click="sendMessage" :disabled="!canSendMessage" :class="{ 'is-sending': isSending }">
178
+ {{ isSending ? '发送中...' : '发送' }}
179
+ </button>
180
+ </div>
181
+
182
+ <!-- 隐藏的文件输入 -->
183
+ <input type="file" ref="fileInput" @change="handleFileSelect" accept="image/*" multiple
184
+ style="display: none;" />
185
+ </div>
186
+ </div>
187
+ </div>
188
+
189
+ <!-- 右键菜单 -->
190
+ <div v-if="contextMenu.show" class="context-menu" :style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"
191
+ @click.stop>
192
+ <!-- 机器人右键菜单 -->
193
+ <template v-if="contextMenu.type === 'bot'">
194
+ <div class="context-menu-item" @click="toggleBotPin(contextMenu.targetId)">
195
+ {{ pinnedBots.has(contextMenu.targetId) ? '取消置顶' : '置顶' }}
196
+ </div>
197
+ <div class="context-menu-item danger" @click="deleteBotMessages(contextMenu.targetId)">
198
+ 彻底删除此机器人所有数据
199
+ </div>
200
+ </template>
201
+
202
+ <!-- 频道右键菜单 -->
203
+ <template v-if="contextMenu.type === 'channel'">
204
+ <div class="context-menu-item" @click="toggleChannelPin(contextMenu.targetId)">
205
+ {{ pinnedChannels.has(`${selectedBot}:${contextMenu.targetId}`) ? '取消置顶' : '置顶' }}
206
+ </div>
207
+ <div class="context-menu-item danger" @click="deleteChannelMessages(contextMenu.targetId)">
208
+ 彻底删除此频道所有数据
209
+ </div>
210
+ </template>
211
+
212
+ <!-- 消息右键菜单 -->
213
+ <template v-if="contextMenu.type === 'message' && contextMenu.message">
214
+ <div class="context-menu-item" @click="handlePlusOne(contextMenu.message)">
215
+ +1
216
+ </div>
217
+ <div class="context-menu-item" @click="handleCopyMessage(contextMenu.message)">
218
+ 复制
219
+ </div>
220
+ <div class="context-menu-item" @click="handleReplyMessage(contextMenu.message)">
221
+ 回复
222
+ </div>
223
+ </template>
224
+ </div>
225
+
226
+ <!-- 滑动指示器 -->
227
+ <div class="swipe-indicator" :class="{ show: swipeIndicator.show }">
228
+ {{ swipeIndicator.text }}
229
+ </div>
230
+ </div>
231
+ </template>
232
+
233
+ <script setup lang="ts">
234
+ import { useChatLogic } from './chat-logic'
235
+
236
+ const chatLogic = useChatLogic()
237
+
238
+ // 解构
239
+ const {
240
+ // 组件
241
+ AvatarComponent,
242
+ ImageComponent,
243
+ JsonCardComponent,
244
+ ForwardMessageComponent,
245
+ MessageElement,
246
+
247
+ // 响应式数据
248
+ chatData,
249
+ channelMessageCounts,
250
+ pluginConfig,
251
+ selectedBot,
252
+ selectedChannel,
253
+ inputMessage,
254
+ imageBlobUrls,
255
+ pinnedBots,
256
+ pinnedChannels,
257
+ uploadedImages,
258
+ showActionMenu,
259
+ isMobile,
260
+ mobileView,
261
+ touchStart,
262
+ touchCurrent,
263
+ isSwipeActive,
264
+ swipeIndicator,
265
+ messageHistory,
266
+ messageInput,
267
+ showScrollButton,
268
+ isUserScrolling,
269
+ isSending,
270
+ isLoadingMore,
271
+ draggingChannel,
272
+ dragStartPos,
273
+ dragCurrentPos,
274
+ dragElementInitialPos,
275
+ dragOffset,
276
+ dragThreshold,
277
+ isDragReady,
278
+ draggedBubbleElement,
279
+ contextMenu,
280
+ fileInput,
281
+
282
+ // 计算属性
283
+ bots,
284
+ currentChannels,
285
+ currentMessages,
286
+ currentChannelName,
287
+ currentChannelKey,
288
+ canSendMessage,
289
+ canInputMessage,
290
+ mobileViewClass,
291
+ inputPlaceholder,
292
+ chatContainerStyle,
293
+
294
+ // 方法
295
+ selectBot,
296
+ selectChannel,
297
+ handleBotRightClick,
298
+ handleChannelRightClick,
299
+ showContextMenu,
300
+ hideContextMenu,
301
+ handleKeyDown,
302
+ toggleBotPin,
303
+ toggleChannelPin,
304
+ deleteBotMessages,
305
+ deleteChannelMessages,
306
+ sendMessage,
307
+ toggleActionMenu,
308
+ triggerImageUpload,
309
+ handleFileSelect,
310
+ handlePaste,
311
+ uploadImage,
312
+ removeImage,
313
+ fileToBase64,
314
+ handleClickOutside,
315
+ formatTime,
316
+ getChannelTypeText,
317
+ scrollToBottom,
318
+ checkScrollPosition,
319
+ isNearBottom,
320
+ getChannelMessageCount,
321
+ startDrag,
322
+ handleDragMove,
323
+ handleDragEnd,
324
+ resetDragState,
325
+ getDragStyle,
326
+ getDragDistance,
327
+ clearChannelHistory,
328
+ showNotification,
329
+ createThresholdCircle,
330
+ removeThresholdCircle,
331
+ handleTouchStart,
332
+ handleTouchMove,
333
+ handleTouchEnd,
334
+ handleInputFocus,
335
+
336
+ // 消息右键菜单处理函数
337
+ handleMessageRightClick,
338
+ handlePlusOne,
339
+ handleCopyMessage,
340
+ handleReplyMessage,
341
+
342
+ // 图片缓存相关
343
+ getCachedImageUrl,
344
+ cacheImage,
345
+ clearChannelImageCache,
346
+ getMemoryStats,
347
+ getCacheStats,
348
+
349
+ // 其他工具函数
350
+ isFileUrl,
351
+ loadHistoryMessages,
352
+ handleMessageEvent,
353
+ handleBotMessageSentEvent,
354
+ parseInlineQuote,
355
+ getInlineQuoteMessage,
356
+ getQuoteUser,
357
+ getQuoteTimestamp,
358
+ getQuoteContent,
359
+ getQuoteElements,
360
+ getMessageContentWithoutQuote
361
+ } = chatLogic
362
362
  </script>