koishi-plugin-chat-patch 1.0.18 → 1.1.1
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 +292 -87
- package/client/vue/index.vue +7 -0
- package/client/vue/style.css +24 -0
- package/dist/index.js +7 -7
- package/dist/style.css +1 -1
- package/lib/config.d.ts +14 -0
- package/lib/index.js +30 -8
- package/package.json +2 -2
- package/src/api-handlers.ts +36 -8
- package/src/config.ts +6 -1
- package/src/index.ts +7 -1
- package/src/message-handler.ts +1 -0
package/client/vue/chat-logic.ts
CHANGED
|
@@ -449,7 +449,7 @@ export function useChatLogic() {
|
|
|
449
449
|
isExpanded.value = !isExpanded.value
|
|
450
450
|
}
|
|
451
451
|
|
|
452
|
-
const getPreviewText = () => {
|
|
452
|
+
const getPreviewText = (): { previews: string[]; messageCount: number } => {
|
|
453
453
|
if (!props.element.children || props.element.children.length === 0) {
|
|
454
454
|
return {
|
|
455
455
|
previews: [],
|
|
@@ -488,7 +488,7 @@ export function useChatLogic() {
|
|
|
488
488
|
}
|
|
489
489
|
}
|
|
490
490
|
|
|
491
|
-
const renderForwardedMessage = (message: any, index: number) => {
|
|
491
|
+
const renderForwardedMessage = (message: any, index: number): any => {
|
|
492
492
|
const nickname = message.attrs?.nickname || '用户'
|
|
493
493
|
const userId = message.attrs?.userId || 'unknown'
|
|
494
494
|
|
|
@@ -551,7 +551,7 @@ export function useChatLogic() {
|
|
|
551
551
|
}
|
|
552
552
|
},
|
|
553
553
|
setup(props) {
|
|
554
|
-
const renderElement = (element: MessageElement) => {
|
|
554
|
+
const renderElement = (element: MessageElement): any => {
|
|
555
555
|
switch (element.type) {
|
|
556
556
|
case 'text':
|
|
557
557
|
return h('span', { class: 'message-text-content' }, element.attrs.content || '')
|
|
@@ -614,7 +614,7 @@ export function useChatLogic() {
|
|
|
614
614
|
case 'p':
|
|
615
615
|
// 处理段落元素,递归渲染子元素
|
|
616
616
|
if (element.children && element.children.length > 0) {
|
|
617
|
-
const childElements = element.children.map((child, index) =>
|
|
617
|
+
const childElements: any[] = element.children.map((child, index) =>
|
|
618
618
|
h(MessageElement, {
|
|
619
619
|
key: index,
|
|
620
620
|
element: child,
|
|
@@ -654,6 +654,14 @@ export function useChatLogic() {
|
|
|
654
654
|
// 存储每个频道的真实消息数量
|
|
655
655
|
const channelMessageCounts = ref<Record<string, number>>({})
|
|
656
656
|
|
|
657
|
+
// 存储每个频道的分页状态
|
|
658
|
+
const channelPagination = ref<Record<string, {
|
|
659
|
+
offset: number
|
|
660
|
+
hasMore: boolean
|
|
661
|
+
loading: boolean
|
|
662
|
+
}>>({})
|
|
663
|
+
|
|
664
|
+
// Config默认值,实际值将从后端获取
|
|
657
665
|
const pluginConfig = ref<{
|
|
658
666
|
maxMessagesPerChannel: number
|
|
659
667
|
keepMessagesOnClear: number
|
|
@@ -663,12 +671,14 @@ export function useChatLogic() {
|
|
|
663
671
|
exactMatch: boolean
|
|
664
672
|
}>
|
|
665
673
|
chatContainerHeight: number
|
|
674
|
+
clearIndexedDBOnStart: boolean
|
|
666
675
|
}>({
|
|
667
676
|
maxMessagesPerChannel: 1000,
|
|
668
677
|
keepMessagesOnClear: 50,
|
|
669
678
|
loggerinfo: false,
|
|
670
679
|
blockedPlatforms: [],
|
|
671
|
-
chatContainerHeight: 80
|
|
680
|
+
chatContainerHeight: 80,
|
|
681
|
+
clearIndexedDBOnStart: true
|
|
672
682
|
})
|
|
673
683
|
|
|
674
684
|
// 图片缓存 - IndexedDB
|
|
@@ -682,6 +692,8 @@ export function useChatLogic() {
|
|
|
682
692
|
|
|
683
693
|
// 内存中的URL缓存
|
|
684
694
|
const imageBlobUrls = ref<Record<string, string>>({})
|
|
695
|
+
// 正在加载的图片URL锁,防止并发请求
|
|
696
|
+
const loadingImages = new Map<string, Promise<string | null>>()
|
|
685
697
|
|
|
686
698
|
// 内存管理配置
|
|
687
699
|
const MAX_MEMORY_USAGE = 100 * 1024 * 1024 // 100MB 最大内存使用量
|
|
@@ -736,6 +748,7 @@ export function useChatLogic() {
|
|
|
736
748
|
const showScrollButton = ref<boolean>(false)
|
|
737
749
|
const isUserScrolling = ref<boolean>(false)
|
|
738
750
|
const isSending = ref<boolean>(false)
|
|
751
|
+
const isLoadingMore = ref<boolean>(false)
|
|
739
752
|
|
|
740
753
|
// 拖拽相关状态
|
|
741
754
|
const draggingChannel = ref<string>('')
|
|
@@ -1113,9 +1126,13 @@ export function useChatLogic() {
|
|
|
1113
1126
|
mobileView.value = 'messages'
|
|
1114
1127
|
}
|
|
1115
1128
|
|
|
1116
|
-
//
|
|
1129
|
+
// 清除该频道的分页状态
|
|
1130
|
+
const channelKey = `${selectedBot.value}:${channelId}`
|
|
1131
|
+
delete channelPagination.value[channelKey]
|
|
1132
|
+
|
|
1133
|
+
// 先获取历史消息(只加载最新的50条)
|
|
1117
1134
|
if (selectedBot.value) {
|
|
1118
|
-
await loadHistoryMessages(selectedBot.value, channelId)
|
|
1135
|
+
await loadHistoryMessages(selectedBot.value, channelId, 50, 0)
|
|
1119
1136
|
}
|
|
1120
1137
|
|
|
1121
1138
|
nextTick(() => {
|
|
@@ -1366,6 +1383,59 @@ export function useChatLogic() {
|
|
|
1366
1383
|
// 滚动到底部时,重置滚动状态
|
|
1367
1384
|
isUserScrolling.value = false
|
|
1368
1385
|
}
|
|
1386
|
+
|
|
1387
|
+
// 检查是否滚动到顶部,如果是则加载更多消息
|
|
1388
|
+
if (scrollTop <= 10 && selectedBot.value && selectedChannel.value) {
|
|
1389
|
+
loadMoreMessages()
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// 加载更多消息的函数
|
|
1395
|
+
async function loadMoreMessages() {
|
|
1396
|
+
if (!selectedBot.value || !selectedChannel.value) return
|
|
1397
|
+
|
|
1398
|
+
const channelKey = `${selectedBot.value}:${selectedChannel.value}`
|
|
1399
|
+
|
|
1400
|
+
// 检查是否正在加载或没有更多消息
|
|
1401
|
+
const pagination = channelPagination.value[channelKey]
|
|
1402
|
+
if (isLoadingMore.value || (pagination && !pagination.hasMore)) return
|
|
1403
|
+
|
|
1404
|
+
// 更新加载状态
|
|
1405
|
+
if (!channelPagination.value[channelKey]) {
|
|
1406
|
+
channelPagination.value[channelKey] = {
|
|
1407
|
+
offset: 0,
|
|
1408
|
+
hasMore: true,
|
|
1409
|
+
loading: false
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// 设置为加载中状态
|
|
1414
|
+
isLoadingMore.value = true
|
|
1415
|
+
channelPagination.value[channelKey].loading = true
|
|
1416
|
+
|
|
1417
|
+
try {
|
|
1418
|
+
// 获取当前offset
|
|
1419
|
+
const currentOffset = pagination?.offset || 0
|
|
1420
|
+
|
|
1421
|
+
// 加载下一批消息(50条)
|
|
1422
|
+
const result = await loadHistoryMessages(selectedBot.value, selectedChannel.value, 50, currentOffset)
|
|
1423
|
+
|
|
1424
|
+
if (result) {
|
|
1425
|
+
// 更新分页状态已在loadHistoryMessages中处理
|
|
1426
|
+
} else {
|
|
1427
|
+
// 加载失败,重置加载状态
|
|
1428
|
+
channelPagination.value[channelKey].loading = false
|
|
1429
|
+
}
|
|
1430
|
+
} catch (error) {
|
|
1431
|
+
console.error('加载更多消息失败:', error)
|
|
1432
|
+
// 加载失败,重置加载状态
|
|
1433
|
+
if (channelPagination.value[channelKey]) {
|
|
1434
|
+
channelPagination.value[channelKey].loading = false
|
|
1435
|
+
}
|
|
1436
|
+
} finally {
|
|
1437
|
+
// 重置加载状态
|
|
1438
|
+
isLoadingMore.value = false
|
|
1369
1439
|
}
|
|
1370
1440
|
}
|
|
1371
1441
|
|
|
@@ -2197,102 +2267,177 @@ export function useChatLogic() {
|
|
|
2197
2267
|
|
|
2198
2268
|
// 图片缓存管理函数
|
|
2199
2269
|
async function getCachedImageUrl(channelKey: string, originalUrl: string): Promise<string | null> {
|
|
2270
|
+
// 检查是否正在加载中,如果是则等待加载完成
|
|
2271
|
+
if (loadingImages.has(originalUrl)) {
|
|
2272
|
+
return loadingImages.get(originalUrl) || null
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2200
2275
|
// 首先检查内存中的blob URL
|
|
2201
2276
|
const existingBlobUrl = imageBlobUrls.value[originalUrl]
|
|
2202
2277
|
if (existingBlobUrl) {
|
|
2203
2278
|
return existingBlobUrl
|
|
2204
2279
|
}
|
|
2205
2280
|
|
|
2206
|
-
//
|
|
2207
|
-
const
|
|
2208
|
-
|
|
2281
|
+
// 使用立即执行的异步函数来处理加载过程
|
|
2282
|
+
const loadingPromise = (async () => {
|
|
2283
|
+
try {
|
|
2284
|
+
// 再次检查内存中的blob URL(防止在创建Promise期间其他地方已经缓存了)
|
|
2285
|
+
const cachedBlobUrl = imageBlobUrls.value[originalUrl]
|
|
2286
|
+
if (cachedBlobUrl) {
|
|
2287
|
+
return cachedBlobUrl
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
// 从IndexedDB获取
|
|
2291
|
+
const cacheItem = await getImageFromDB(originalUrl)
|
|
2292
|
+
if (!cacheItem) {
|
|
2293
|
+
return null
|
|
2294
|
+
}
|
|
2209
2295
|
|
|
2210
|
-
|
|
2211
|
-
|
|
2296
|
+
// 检查内存使用情况
|
|
2297
|
+
checkAndCleanupMemory()
|
|
2212
2298
|
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2299
|
+
// 创建blob URL并缓存到内存
|
|
2300
|
+
const blobUrl = URL.createObjectURL(cacheItem.blob)
|
|
2301
|
+
imageBlobUrls.value[originalUrl] = blobUrl
|
|
2216
2302
|
|
|
2217
|
-
|
|
2218
|
-
|
|
2303
|
+
// 更新内存使用量
|
|
2304
|
+
updateMemoryUsage(estimateBlobSize(cacheItem.blob))
|
|
2219
2305
|
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2306
|
+
// 更新访问时间
|
|
2307
|
+
cacheItem.timestamp = Date.now()
|
|
2308
|
+
await saveImageToDB(cacheItem)
|
|
2223
2309
|
|
|
2224
|
-
|
|
2310
|
+
return blobUrl
|
|
2311
|
+
} catch (error) {
|
|
2312
|
+
console.error('获取缓存图片失败:', error)
|
|
2313
|
+
return null
|
|
2314
|
+
} finally {
|
|
2315
|
+
// 加载完成后从loadingImages中移除
|
|
2316
|
+
loadingImages.delete(originalUrl)
|
|
2317
|
+
}
|
|
2318
|
+
})()
|
|
2319
|
+
|
|
2320
|
+
// 立即添加到loadingImages中,防止并发请求
|
|
2321
|
+
loadingImages.set(originalUrl, loadingPromise)
|
|
2322
|
+
|
|
2323
|
+
// 等待加载完成并返回结果
|
|
2324
|
+
return loadingPromise
|
|
2225
2325
|
}
|
|
2226
2326
|
|
|
2227
2327
|
async function cacheImage(channelKey: string, originalUrl: string): Promise<string | null> {
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
return cached
|
|
2233
|
-
}
|
|
2328
|
+
// 检查是否正在加载中,如果是则等待加载完成
|
|
2329
|
+
if (loadingImages.has(originalUrl)) {
|
|
2330
|
+
return loadingImages.get(originalUrl) || null
|
|
2331
|
+
}
|
|
2234
2332
|
|
|
2235
|
-
|
|
2236
|
-
|
|
2333
|
+
// 首先检查内存中的blob URL
|
|
2334
|
+
const existingBlobUrl = imageBlobUrls.value[originalUrl]
|
|
2335
|
+
if (existingBlobUrl) {
|
|
2336
|
+
return existingBlobUrl
|
|
2337
|
+
}
|
|
2237
2338
|
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2339
|
+
// 使用立即执行的异步函数来处理加载过程
|
|
2340
|
+
const loadingPromise = (async () => {
|
|
2341
|
+
try {
|
|
2342
|
+
// 再次检查内存中的blob URL(防止在创建Promise期间其他地方已经缓存了)
|
|
2343
|
+
const cachedBlobUrl = imageBlobUrls.value[originalUrl]
|
|
2344
|
+
if (cachedBlobUrl) {
|
|
2345
|
+
return cachedBlobUrl
|
|
2346
|
+
}
|
|
2242
2347
|
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2348
|
+
// 从IndexedDB获取
|
|
2349
|
+
const cachedItem = await getImageFromDB(originalUrl)
|
|
2350
|
+
if (cachedItem) {
|
|
2351
|
+
// 检查内存使用情况
|
|
2352
|
+
checkAndCleanupMemory()
|
|
2246
2353
|
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
for (let i = 0; i < byteCharacters.length; i++) {
|
|
2251
|
-
byteNumbers[i] = byteCharacters.charCodeAt(i)
|
|
2252
|
-
}
|
|
2253
|
-
const byteArray = new Uint8Array(byteNumbers)
|
|
2354
|
+
// 创建blob URL并缓存到内存
|
|
2355
|
+
const blobUrl = URL.createObjectURL(cachedItem.blob)
|
|
2356
|
+
imageBlobUrls.value[originalUrl] = blobUrl
|
|
2254
2357
|
|
|
2255
|
-
|
|
2256
|
-
|
|
2358
|
+
// 更新内存使用量
|
|
2359
|
+
updateMemoryUsage(estimateBlobSize(cachedItem.blob))
|
|
2257
2360
|
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
return null
|
|
2262
|
-
}
|
|
2361
|
+
// 更新访问时间
|
|
2362
|
+
cachedItem.timestamp = Date.now()
|
|
2363
|
+
await saveImageToDB(cachedItem)
|
|
2263
2364
|
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
url: originalUrl,
|
|
2267
|
-
blob: blob,
|
|
2268
|
-
timestamp: Date.now(),
|
|
2269
|
-
size: blob.size,
|
|
2270
|
-
channelKey: channelKey
|
|
2271
|
-
}
|
|
2365
|
+
return blobUrl
|
|
2366
|
+
}
|
|
2272
2367
|
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
if (!saved) {
|
|
2276
|
-
console.error('保存图片到IndexedDB失败')
|
|
2277
|
-
return null
|
|
2278
|
-
}
|
|
2368
|
+
// 获取图片数据
|
|
2369
|
+
const result = await (send as any)('fetch-image', { url: originalUrl })
|
|
2279
2370
|
|
|
2280
|
-
|
|
2281
|
-
|
|
2371
|
+
if (!result.success) {
|
|
2372
|
+
return null
|
|
2373
|
+
}
|
|
2282
2374
|
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2375
|
+
// 将base64转换为blob
|
|
2376
|
+
const base64Data = result.base64
|
|
2377
|
+
const contentType = result.contentType || 'image/jpeg'
|
|
2286
2378
|
|
|
2287
|
-
|
|
2288
|
-
|
|
2379
|
+
// 解码base64
|
|
2380
|
+
const byteCharacters = atob(base64Data)
|
|
2381
|
+
const byteNumbers = new Array(byteCharacters.length)
|
|
2382
|
+
for (let i = 0; i < byteCharacters.length; i++) {
|
|
2383
|
+
byteNumbers[i] = byteCharacters.charCodeAt(i)
|
|
2384
|
+
}
|
|
2385
|
+
const byteArray = new Uint8Array(byteNumbers)
|
|
2289
2386
|
|
|
2290
|
-
|
|
2387
|
+
// 保持原始MIME类型,特别是对于GIF
|
|
2388
|
+
const blob = new Blob([byteArray], { type: contentType })
|
|
2291
2389
|
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2390
|
+
// 检查blob大小
|
|
2391
|
+
if (blob.size > MAX_IMAGE_SIZE) {
|
|
2392
|
+
return null
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
// 再次检查内存中是否已经存在(防止并发请求)
|
|
2396
|
+
const concurrentBlobUrl = imageBlobUrls.value[originalUrl]
|
|
2397
|
+
if (concurrentBlobUrl) {
|
|
2398
|
+
return concurrentBlobUrl
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
// 创建缓存项
|
|
2402
|
+
const cacheItem: ImageCacheItem = {
|
|
2403
|
+
url: originalUrl,
|
|
2404
|
+
blob: blob,
|
|
2405
|
+
timestamp: Date.now(),
|
|
2406
|
+
size: blob.size,
|
|
2407
|
+
channelKey: channelKey
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
// 保存到IndexedDB
|
|
2411
|
+
const saved = await saveImageToDB(cacheItem)
|
|
2412
|
+
if (!saved) {
|
|
2413
|
+
return null
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
// 检查内存使用情况
|
|
2417
|
+
checkAndCleanupMemory()
|
|
2418
|
+
|
|
2419
|
+
// 创建blob URL并缓存到内存
|
|
2420
|
+
const blobUrl = URL.createObjectURL(blob)
|
|
2421
|
+
imageBlobUrls.value[originalUrl] = blobUrl
|
|
2422
|
+
|
|
2423
|
+
// 更新内存使用量
|
|
2424
|
+
updateMemoryUsage(estimateBlobSize(blob))
|
|
2425
|
+
|
|
2426
|
+
return blobUrl
|
|
2427
|
+
} catch (error) {
|
|
2428
|
+
console.error('缓存图片失败:', error)
|
|
2429
|
+
return null
|
|
2430
|
+
} finally {
|
|
2431
|
+
// 加载完成后从loadingImages中移除
|
|
2432
|
+
loadingImages.delete(originalUrl)
|
|
2433
|
+
}
|
|
2434
|
+
})()
|
|
2435
|
+
|
|
2436
|
+
// 立即添加到loadingImages中,防止并发请求
|
|
2437
|
+
loadingImages.set(originalUrl, loadingPromise)
|
|
2438
|
+
|
|
2439
|
+
// 等待加载完成并返回结果
|
|
2440
|
+
return loadingPromise
|
|
2296
2441
|
}
|
|
2297
2442
|
|
|
2298
2443
|
// 清理频道的所有图片缓存
|
|
@@ -2808,13 +2953,21 @@ export function useChatLogic() {
|
|
|
2808
2953
|
}
|
|
2809
2954
|
|
|
2810
2955
|
// 获取历史消息
|
|
2811
|
-
async function loadHistoryMessages(botId: string, channelId: string) {
|
|
2956
|
+
async function loadHistoryMessages(botId: string, channelId: string, limit?: number, offset?: number) {
|
|
2812
2957
|
try {
|
|
2813
2958
|
|
|
2814
|
-
const
|
|
2959
|
+
const requestData: any = {
|
|
2815
2960
|
selfId: botId,
|
|
2816
2961
|
channelId: channelId
|
|
2817
|
-
}
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
// 如果提供了分页参数,则添加到请求中
|
|
2965
|
+
if (limit !== undefined) {
|
|
2966
|
+
requestData.limit = limit
|
|
2967
|
+
requestData.offset = offset || 0
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
const result = await (send as any)('get-history-messages', requestData)
|
|
2818
2971
|
|
|
2819
2972
|
if (result.success && result.messages) {
|
|
2820
2973
|
const channelKey = `${botId}:${channelId}`
|
|
@@ -2834,14 +2987,51 @@ export function useChatLogic() {
|
|
|
2834
2987
|
quote: msg.quote
|
|
2835
2988
|
}))
|
|
2836
2989
|
|
|
2837
|
-
//
|
|
2838
|
-
|
|
2990
|
+
// 确保offset有默认值
|
|
2991
|
+
const actualOffset = offset || 0
|
|
2839
2992
|
|
|
2840
|
-
//
|
|
2841
|
-
|
|
2993
|
+
// 如果是分页加载,需要合并到现有消息中
|
|
2994
|
+
if (limit !== undefined) {
|
|
2995
|
+
// 按时间戳排序
|
|
2996
|
+
messages.sort((a, b) => a.timestamp - b.timestamp)
|
|
2997
|
+
|
|
2998
|
+
// 如果是第一页(offset为0),则替换现有消息
|
|
2999
|
+
if (offset === 0) {
|
|
3000
|
+
chatData.value.messages[channelKey] = messages
|
|
3001
|
+
// 初始化分页状态
|
|
3002
|
+
channelPagination.value[channelKey] = {
|
|
3003
|
+
offset: messages.length,
|
|
3004
|
+
hasMore: messages.length >= limit && result.total > messages.length,
|
|
3005
|
+
loading: false
|
|
3006
|
+
}
|
|
3007
|
+
} else {
|
|
3008
|
+
// 否则是加载更多,需要合并到现有消息的开头(因为是历史消息)
|
|
3009
|
+
const existingMessages = chatData.value.messages[channelKey] || []
|
|
3010
|
+
chatData.value.messages[channelKey] = [...messages, ...existingMessages]
|
|
3011
|
+
// 更新分页状态
|
|
3012
|
+
const actualOffset = offset || 0
|
|
3013
|
+
channelPagination.value[channelKey] = {
|
|
3014
|
+
offset: actualOffset + messages.length,
|
|
3015
|
+
hasMore: messages.length >= limit && result.total > (actualOffset + messages.length),
|
|
3016
|
+
loading: false
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
} else {
|
|
3020
|
+
// 按时间戳排序
|
|
3021
|
+
messages.sort((a, b) => a.timestamp - b.timestamp)
|
|
3022
|
+
|
|
3023
|
+
// 设置历史消息
|
|
3024
|
+
chatData.value.messages[channelKey] = messages
|
|
3025
|
+
// 初始化分页状态
|
|
3026
|
+
channelPagination.value[channelKey] = {
|
|
3027
|
+
offset: messages.length,
|
|
3028
|
+
hasMore: false, // 不分页时没有更多消息
|
|
3029
|
+
loading: false
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
2842
3032
|
|
|
2843
3033
|
// 更新频道消息数量缓存
|
|
2844
|
-
channelMessageCounts.value[channelKey] = messages.length
|
|
3034
|
+
channelMessageCounts.value[channelKey] = result.total || messages.length
|
|
2845
3035
|
|
|
2846
3036
|
// 触发响应式更新
|
|
2847
3037
|
chatData.value = { ...chatData.value }
|
|
@@ -3012,6 +3202,20 @@ export function useChatLogic() {
|
|
|
3012
3202
|
// 添加点击外部关闭菜单的监听器
|
|
3013
3203
|
document.addEventListener('click', handleClickOutside)
|
|
3014
3204
|
|
|
3205
|
+
// 首先加载插件配置
|
|
3206
|
+
await loadPluginConfig()
|
|
3207
|
+
|
|
3208
|
+
// 检查是否需要清空 IndexedDB
|
|
3209
|
+
if (pluginConfig.value.clearIndexedDBOnStart) {
|
|
3210
|
+
console.log('启动时清空 IndexedDB 缓存...')
|
|
3211
|
+
const clearResult = await clearAllIndexedDBData()
|
|
3212
|
+
if (clearResult) {
|
|
3213
|
+
console.log('IndexedDB 缓存已清空')
|
|
3214
|
+
} else {
|
|
3215
|
+
console.warn('清空 IndexedDB 缓存失败')
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3015
3219
|
// 初始化IndexedDB
|
|
3016
3220
|
const dbInitialized = await initImageDB()
|
|
3017
3221
|
if (!dbInitialized) {
|
|
@@ -3030,9 +3234,6 @@ export function useChatLogic() {
|
|
|
3030
3234
|
}, 5 * 60 * 1000)
|
|
3031
3235
|
}
|
|
3032
3236
|
|
|
3033
|
-
// 首先加载插件配置
|
|
3034
|
-
await loadPluginConfig()
|
|
3035
|
-
|
|
3036
3237
|
// 然后加载历史数据
|
|
3037
3238
|
await loadChatData()
|
|
3038
3239
|
|
|
@@ -3120,6 +3321,7 @@ export function useChatLogic() {
|
|
|
3120
3321
|
// 响应式数据
|
|
3121
3322
|
chatData,
|
|
3122
3323
|
channelMessageCounts,
|
|
3324
|
+
channelPagination,
|
|
3123
3325
|
pluginConfig,
|
|
3124
3326
|
selectedBot,
|
|
3125
3327
|
selectedChannel,
|
|
@@ -3140,6 +3342,7 @@ export function useChatLogic() {
|
|
|
3140
3342
|
showScrollButton,
|
|
3141
3343
|
isUserScrolling,
|
|
3142
3344
|
isSending,
|
|
3345
|
+
isLoadingMore,
|
|
3143
3346
|
draggingChannel,
|
|
3144
3347
|
dragStartPos,
|
|
3145
3348
|
dragCurrentPos,
|
|
@@ -3204,11 +3407,13 @@ export function useChatLogic() {
|
|
|
3204
3407
|
handleTouchMove,
|
|
3205
3408
|
handleTouchEnd,
|
|
3206
3409
|
handleInputFocus,
|
|
3410
|
+
loadMoreMessages,
|
|
3207
3411
|
|
|
3208
3412
|
// 图片缓存相关
|
|
3209
3413
|
getCachedImageUrl,
|
|
3210
3414
|
cacheImage,
|
|
3211
3415
|
clearChannelImageCache,
|
|
3416
|
+
clearAllIndexedDBData,
|
|
3212
3417
|
getMemoryStats,
|
|
3213
3418
|
getCacheStats,
|
|
3214
3419
|
|
package/client/vue/index.vue
CHANGED
|
@@ -66,6 +66,12 @@
|
|
|
66
66
|
<div v-else class="message-content">
|
|
67
67
|
<!-- 消息历史 -->
|
|
68
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
|
+
|
|
69
75
|
<div v-for="message in currentMessages" :key="message.id"
|
|
70
76
|
:class="['message-item', { 'bot-message': message.isBot }]">
|
|
71
77
|
<div class="message-avatar">
|
|
@@ -245,6 +251,7 @@ const {
|
|
|
245
251
|
showScrollButton,
|
|
246
252
|
isUserScrolling,
|
|
247
253
|
isSending,
|
|
254
|
+
isLoadingMore,
|
|
248
255
|
draggingChannel,
|
|
249
256
|
dragStartPos,
|
|
250
257
|
dragCurrentPos,
|
package/client/vue/style.css
CHANGED
|
@@ -1918,3 +1918,27 @@ body.dragging-bubble-global {
|
|
|
1918
1918
|
background-color: rgba(255, 255, 255, .7)
|
|
1919
1919
|
}
|
|
1920
1920
|
}
|
|
1921
|
+
|
|
1922
|
+
.loading-more-indicator {
|
|
1923
|
+
display: flex;
|
|
1924
|
+
align-items: center;
|
|
1925
|
+
justify-content: center;
|
|
1926
|
+
padding: 12px;
|
|
1927
|
+
color: #6c757d;
|
|
1928
|
+
font-size: 14px;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
.loading-spinner {
|
|
1932
|
+
width: 20px;
|
|
1933
|
+
height: 20px;
|
|
1934
|
+
border: 2px solid #f3f3f3;
|
|
1935
|
+
border-top: 2px solid #2196f3;
|
|
1936
|
+
border-radius: 50%;
|
|
1937
|
+
animation: spin 1s linear infinite;
|
|
1938
|
+
margin-right: 8px;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
@keyframes spin {
|
|
1942
|
+
0% { transform: rotate(0deg); }
|
|
1943
|
+
100% { transform: rotate(360deg); }
|
|
1944
|
+
}
|