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.
- package/client/icons/activity.vue +9 -9
- package/client/icons/index.ts +4 -4
- package/client/index.scss +4 -4
- package/client/index.ts +22 -22
- package/client/vue/chat-logic.ts +3784 -3784
- package/client/vue/index.vue +361 -361
- package/client/vue/style.css +1997 -1973
- package/dist/index.js +1 -1
- package/dist/style.css +1 -1
- package/lib/index.js +20 -3
- package/package.json +40 -40
- package/readme.md +25 -25
- package/src/api-handlers.ts +657 -657
- package/src/config.ts +49 -49
- package/src/file-manager.ts +168 -168
- package/src/index.ts +125 -125
- package/src/message-handler.ts +304 -282
- package/src/types.ts +59 -59
- package/src/utils.ts +99 -99
- package/lib/config.d.ts +0 -14
package/client/vue/index.vue
CHANGED
|
@@ -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>
|