koishi-plugin-chat-patch 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/vue/chat-logic.ts +410 -52
- package/client/vue/index.vue +44 -14
- package/client/vue/style.css +75 -46
- package/dist/index.js +8 -8
- package/dist/style.css +1 -1
- package/lib/index.js +28 -2
- package/package.json +1 -1
- package/src/api-handlers.ts +29 -3
- package/src/index.ts +2 -2
package/client/vue/chat-logic.ts
CHANGED
|
@@ -601,7 +601,7 @@ export function useChatLogic() {
|
|
|
601
601
|
return h('span', {
|
|
602
602
|
class: 'message-at',
|
|
603
603
|
title: element.attrs.name
|
|
604
|
-
},
|
|
604
|
+
}, `@${(element.attrs.name || element.attrs.id).replace("@", '')}`)
|
|
605
605
|
|
|
606
606
|
case 'json':
|
|
607
607
|
return h('div', { class: 'message-image-container' }, [
|
|
@@ -767,16 +767,18 @@ export function useChatLogic() {
|
|
|
767
767
|
show: boolean
|
|
768
768
|
x: number
|
|
769
769
|
y: number
|
|
770
|
-
type: 'bot' | 'channel' |
|
|
770
|
+
type: 'bot' | 'channel' | 'message'
|
|
771
771
|
targetId: string
|
|
772
772
|
isSecondClick: boolean
|
|
773
|
+
message?: MessageInfo
|
|
773
774
|
}>({
|
|
774
775
|
show: false,
|
|
775
776
|
x: 0,
|
|
776
777
|
y: 0,
|
|
777
|
-
type:
|
|
778
|
+
type: 'bot',
|
|
778
779
|
targetId: '',
|
|
779
|
-
isSecondClick: false
|
|
780
|
+
isSecondClick: false,
|
|
781
|
+
message: undefined
|
|
780
782
|
})
|
|
781
783
|
|
|
782
784
|
// 置顶状态管理
|
|
@@ -934,6 +936,125 @@ export function useChatLogic() {
|
|
|
934
936
|
}
|
|
935
937
|
}
|
|
936
938
|
|
|
939
|
+
// 解析内联引用消息
|
|
940
|
+
function parseQuoteMessage(content: string, allMessages: MessageInfo[]): { quotedMessage: MessageInfo | null, restContent: string } {
|
|
941
|
+
// 匹配 <quote id="..."/> 格式
|
|
942
|
+
const quoteRegex = /^<quote\s+id="([^"]+)"\s*\/>(.*)/;
|
|
943
|
+
const match = content.match(quoteRegex);
|
|
944
|
+
|
|
945
|
+
if (match) {
|
|
946
|
+
const quotedMessageId = match[1];
|
|
947
|
+
const restContent = match[2];
|
|
948
|
+
|
|
949
|
+
// 在所有消息中查找被引用的消息
|
|
950
|
+
const quotedMessage = allMessages.find(msg => msg.id === quotedMessageId) || null;
|
|
951
|
+
|
|
952
|
+
return { quotedMessage, restContent };
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
return { quotedMessage: null, restContent: content };
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// 在计算属性中添加解析内联引用消息的函数
|
|
959
|
+
function parseInlineQuote(content: string, allMessages: MessageInfo[]): { quotedMessage: MessageInfo | null, restContent: string } {
|
|
960
|
+
return parseQuoteMessage(content, allMessages);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// 获取内联引用消息
|
|
964
|
+
function getInlineQuoteMessage(message: MessageInfo, allMessages: MessageInfo[]): MessageInfo | null {
|
|
965
|
+
if (!message || !message.content) return null;
|
|
966
|
+
|
|
967
|
+
// 使用 parseInlineQuote 函数解析内联引用
|
|
968
|
+
const parsed = parseInlineQuote(message.content, allMessages);
|
|
969
|
+
return parsed.quotedMessage;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// 获取引用消息的用户信息
|
|
973
|
+
function getQuoteUser(message: MessageInfo, allMessages: MessageInfo[]): { avatar: string, username: string } {
|
|
974
|
+
// 如果有现成的引用消息,直接使用
|
|
975
|
+
if (message.quote && message.quote.user) {
|
|
976
|
+
return {
|
|
977
|
+
avatar: message.quote.user.avatar || '',
|
|
978
|
+
username: message.quote.user.username || ''
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// 检查是否有内联引用消息
|
|
983
|
+
const inlineQuote = getInlineQuoteMessage(message, allMessages);
|
|
984
|
+
if (inlineQuote) {
|
|
985
|
+
return {
|
|
986
|
+
avatar: inlineQuote.avatar || '',
|
|
987
|
+
username: inlineQuote.username || ''
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// 默认返回空对象
|
|
992
|
+
return {
|
|
993
|
+
avatar: '',
|
|
994
|
+
username: ''
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// 获取引用消息的时间戳
|
|
999
|
+
function getQuoteTimestamp(message: MessageInfo, allMessages: MessageInfo[]): number {
|
|
1000
|
+
// 如果有现成的引用消息,直接使用
|
|
1001
|
+
if (message.quote && message.quote.timestamp) {
|
|
1002
|
+
return message.quote.timestamp;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// 检查是否有内联引用消息
|
|
1006
|
+
const inlineQuote = getInlineQuoteMessage(message, allMessages);
|
|
1007
|
+
if (inlineQuote) {
|
|
1008
|
+
return inlineQuote.timestamp;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// 默认返回当前时间
|
|
1012
|
+
return Date.now();
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// 获取引用消息的内容
|
|
1016
|
+
function getQuoteContent(message: MessageInfo, allMessages: MessageInfo[]): string {
|
|
1017
|
+
// 如果有现成的引用消息,直接使用
|
|
1018
|
+
if (message.quote && message.quote.content) {
|
|
1019
|
+
return message.quote.content;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// 检查是否有内联引用消息
|
|
1023
|
+
const inlineQuote = getInlineQuoteMessage(message, allMessages);
|
|
1024
|
+
if (inlineQuote) {
|
|
1025
|
+
return inlineQuote.content;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// 默认返回空字符串
|
|
1029
|
+
return '';
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// 获取引用消息的元素
|
|
1033
|
+
function getQuoteElements(message: MessageInfo, allMessages: MessageInfo[]): MessageElement[] {
|
|
1034
|
+
// 如果有现成的引用消息,直接使用
|
|
1035
|
+
if (message.quote && message.quote.elements) {
|
|
1036
|
+
return message.quote.elements;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// 检查是否有内联引用消息
|
|
1040
|
+
const inlineQuote = getInlineQuoteMessage(message, allMessages);
|
|
1041
|
+
if (inlineQuote) {
|
|
1042
|
+
return inlineQuote.elements || [];
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// 默认返回空数组
|
|
1046
|
+
return [];
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// 获取不包含引用部分的消息内容
|
|
1050
|
+
function getMessageContentWithoutQuote(message: MessageInfo, allMessages: MessageInfo[]): string {
|
|
1051
|
+
if (!message || !message.content) return '';
|
|
1052
|
+
|
|
1053
|
+
// 使用 parseInlineQuote 函数解析内联引用
|
|
1054
|
+
const parsed = parseInlineQuote(message.content, allMessages);
|
|
1055
|
+
return parsed.restContent || message.content;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
937
1058
|
// 右键菜单相关方法
|
|
938
1059
|
function handleBotRightClick(event: MouseEvent, botId: string) {
|
|
939
1060
|
event.preventDefault()
|
|
@@ -969,6 +1090,23 @@ export function useChatLogic() {
|
|
|
969
1090
|
showContextMenu(event, 'channel', channelId)
|
|
970
1091
|
}
|
|
971
1092
|
|
|
1093
|
+
function handleMessageRightClick(event: MouseEvent, message: MessageInfo) {
|
|
1094
|
+
event.preventDefault()
|
|
1095
|
+
event.stopPropagation()
|
|
1096
|
+
|
|
1097
|
+
// 检查是否是第二次右键点击同一个目标
|
|
1098
|
+
const isSecondClick = contextMenu.value.show &&
|
|
1099
|
+
contextMenu.value.type === 'message' &&
|
|
1100
|
+
contextMenu.value.message?.id === message.id
|
|
1101
|
+
|
|
1102
|
+
if (isSecondClick) {
|
|
1103
|
+
// 第二次右键,隐藏自定义菜单,让浏览器显示原生菜单
|
|
1104
|
+
hideContextMenu()
|
|
1105
|
+
return
|
|
1106
|
+
}
|
|
1107
|
+
showMessageContextMenu(event, message)
|
|
1108
|
+
}
|
|
1109
|
+
|
|
972
1110
|
function showContextMenu(event: MouseEvent, type: 'bot' | 'channel', targetId: string) {
|
|
973
1111
|
// 确保菜单不会超出屏幕边界
|
|
974
1112
|
const menuWidth = 180
|
|
@@ -997,6 +1135,35 @@ export function useChatLogic() {
|
|
|
997
1135
|
document.addEventListener('keydown', handleKeyDown)
|
|
998
1136
|
}
|
|
999
1137
|
|
|
1138
|
+
function showMessageContextMenu(event: MouseEvent, message: MessageInfo) {
|
|
1139
|
+
// 确保菜单不会超出屏幕边界
|
|
1140
|
+
const menuWidth = 180
|
|
1141
|
+
const menuHeight = 120
|
|
1142
|
+
let x = event.clientX
|
|
1143
|
+
let y = event.clientY
|
|
1144
|
+
|
|
1145
|
+
if (x + menuWidth > window.innerWidth) {
|
|
1146
|
+
x = window.innerWidth - menuWidth - 10
|
|
1147
|
+
}
|
|
1148
|
+
if (y + menuHeight > window.innerHeight) {
|
|
1149
|
+
y = window.innerHeight - menuHeight - 10
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
contextMenu.value = {
|
|
1153
|
+
show: true,
|
|
1154
|
+
x,
|
|
1155
|
+
y,
|
|
1156
|
+
type: 'message',
|
|
1157
|
+
targetId: message.id,
|
|
1158
|
+
isSecondClick: false,
|
|
1159
|
+
message: message
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// 添加全局事件监听器来隐藏菜单
|
|
1163
|
+
document.addEventListener('click', hideContextMenu, { once: true })
|
|
1164
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1000
1167
|
function hideContextMenu() {
|
|
1001
1168
|
contextMenu.value.show = false
|
|
1002
1169
|
document.removeEventListener('click', hideContextMenu)
|
|
@@ -1150,6 +1317,9 @@ export function useChatLogic() {
|
|
|
1150
1317
|
const messageContent = inputMessage.value.trim()
|
|
1151
1318
|
if (!messageContent && uploadedImages.value.length === 0) return
|
|
1152
1319
|
|
|
1320
|
+
// 保存当前频道信息,用于发送后重新加载
|
|
1321
|
+
const currentChannelId = selectedChannel.value
|
|
1322
|
+
|
|
1153
1323
|
// 设置发送状态
|
|
1154
1324
|
isSending.value = true
|
|
1155
1325
|
|
|
@@ -1179,6 +1349,15 @@ export function useChatLogic() {
|
|
|
1179
1349
|
uploadedImages.value = []
|
|
1180
1350
|
showActionMenu.value = false
|
|
1181
1351
|
|
|
1352
|
+
// 消息发送成功后,重新选择当前频道以完全重新渲染
|
|
1353
|
+
if (currentChannelId) {
|
|
1354
|
+
// 临时清空选择
|
|
1355
|
+
selectedChannel.value = ''
|
|
1356
|
+
await nextTick()
|
|
1357
|
+
// 重新选择当前频道,触发完整的重新渲染流程
|
|
1358
|
+
selectChannel(currentChannelId)
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1182
1361
|
// 消息发送成功后,主动通知后端清理临时文件
|
|
1183
1362
|
if (result.tempImageIds && result.tempImageIds.length > 0) {
|
|
1184
1363
|
try {
|
|
@@ -1215,7 +1394,6 @@ export function useChatLogic() {
|
|
|
1215
1394
|
}
|
|
1216
1395
|
showActionMenu.value = false
|
|
1217
1396
|
}
|
|
1218
|
-
|
|
1219
1397
|
async function handleFileSelect(event: Event) {
|
|
1220
1398
|
const target = event.target as HTMLInputElement
|
|
1221
1399
|
const files = target.files
|
|
@@ -1347,6 +1525,155 @@ export function useChatLogic() {
|
|
|
1347
1525
|
})
|
|
1348
1526
|
}
|
|
1349
1527
|
|
|
1528
|
+
// 消息右键菜单处理函数
|
|
1529
|
+
async function handlePlusOne(message: MessageInfo | undefined) {
|
|
1530
|
+
if (!message) return;
|
|
1531
|
+
hideContextMenu()
|
|
1532
|
+
|
|
1533
|
+
try {
|
|
1534
|
+
// 构造要发送的内容,保持原始类型
|
|
1535
|
+
let content = ''
|
|
1536
|
+
const elements = message.elements || []
|
|
1537
|
+
|
|
1538
|
+
// 如果有elements,处理各种元素类型
|
|
1539
|
+
if (elements.length > 0) {
|
|
1540
|
+
for (const element of elements) {
|
|
1541
|
+
switch (element.type) {
|
|
1542
|
+
case 'img':
|
|
1543
|
+
case 'image':
|
|
1544
|
+
// 对于图片类型,转换为img标签
|
|
1545
|
+
const imageUrl = element.attrs.src || element.attrs.url || element.attrs.file
|
|
1546
|
+
if (imageUrl) {
|
|
1547
|
+
content += `<img src="${imageUrl}"/>`
|
|
1548
|
+
}
|
|
1549
|
+
break
|
|
1550
|
+
case 'mface':
|
|
1551
|
+
// 对于mface,转换为img标签
|
|
1552
|
+
const mfaceUrl = element.attrs.src || element.attrs.url || element.attrs.file
|
|
1553
|
+
if (mfaceUrl) {
|
|
1554
|
+
content += `<img src="${mfaceUrl}"/>`
|
|
1555
|
+
}
|
|
1556
|
+
break
|
|
1557
|
+
case 'face':
|
|
1558
|
+
if (element.children && element.children[0]?.attrs?.src) {
|
|
1559
|
+
const faceUrl = element.children[0].attrs.src
|
|
1560
|
+
content += `<img src="${faceUrl}"/>`
|
|
1561
|
+
} else {
|
|
1562
|
+
content += `[${element.attrs.name || element.attrs.id}]`
|
|
1563
|
+
}
|
|
1564
|
+
break
|
|
1565
|
+
case 'at':
|
|
1566
|
+
// 对于@消息,保持原始格式
|
|
1567
|
+
content += `<at id="${element.attrs.id}" name="${element.attrs.name}"/>`
|
|
1568
|
+
break
|
|
1569
|
+
case 'text':
|
|
1570
|
+
// 对于文本元素,直接添加文本内容
|
|
1571
|
+
content += element.attrs.content || ''
|
|
1572
|
+
break
|
|
1573
|
+
default:
|
|
1574
|
+
// 其他类型保持原始内容
|
|
1575
|
+
content += message.content
|
|
1576
|
+
break
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
} else {
|
|
1580
|
+
// 如果没有elements,使用原始内容
|
|
1581
|
+
content = message.content
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// 设置输入框内容
|
|
1585
|
+
inputMessage.value = content
|
|
1586
|
+
|
|
1587
|
+
// 触发发送
|
|
1588
|
+
await sendMessage()
|
|
1589
|
+
} catch (error: any) {
|
|
1590
|
+
console.error('+1操作失败:', error)
|
|
1591
|
+
showNotification('操作失败: ' + (error?.message || String(error)), 'error')
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
async function handleCopyMessage(message: MessageInfo | undefined) {
|
|
1596
|
+
if (!message) return;
|
|
1597
|
+
hideContextMenu()
|
|
1598
|
+
|
|
1599
|
+
try {
|
|
1600
|
+
// 复制消息内容到剪贴板
|
|
1601
|
+
let content = ''
|
|
1602
|
+
const elements = message.elements || []
|
|
1603
|
+
|
|
1604
|
+
// 如果有elements,处理各种元素类型
|
|
1605
|
+
if (elements.length > 0) {
|
|
1606
|
+
for (const element of elements) {
|
|
1607
|
+
switch (element.type) {
|
|
1608
|
+
case 'img':
|
|
1609
|
+
case 'image':
|
|
1610
|
+
const imageUrl = element.attrs.src || element.attrs.url || element.attrs.file
|
|
1611
|
+
if (imageUrl) {
|
|
1612
|
+
content += `<img src="${imageUrl}"/>`
|
|
1613
|
+
}
|
|
1614
|
+
break
|
|
1615
|
+
case 'mface':
|
|
1616
|
+
const mfaceUrl = element.attrs.src || element.attrs.url || element.attrs.file
|
|
1617
|
+
if (mfaceUrl) {
|
|
1618
|
+
content += `<img src="${mfaceUrl}"/>`
|
|
1619
|
+
}
|
|
1620
|
+
break
|
|
1621
|
+
case 'face':
|
|
1622
|
+
if (element.children && element.children[0]?.attrs?.src) {
|
|
1623
|
+
const faceUrl = element.children[0].attrs.src
|
|
1624
|
+
content += `<img src="${faceUrl}"/>`
|
|
1625
|
+
} else {
|
|
1626
|
+
content += `[${element.attrs.name || element.attrs.id}]`
|
|
1627
|
+
}
|
|
1628
|
+
break
|
|
1629
|
+
case 'at':
|
|
1630
|
+
// 对于@消息,保持原始格式
|
|
1631
|
+
content += `<at id="${element.attrs.id}" name="${element.attrs.name}"/>`
|
|
1632
|
+
break
|
|
1633
|
+
case 'text':
|
|
1634
|
+
// 对于文本元素,直接添加文本内容
|
|
1635
|
+
content += element.attrs.content || ''
|
|
1636
|
+
break
|
|
1637
|
+
default:
|
|
1638
|
+
// 其他类型保持原始内容
|
|
1639
|
+
content += message.content
|
|
1640
|
+
break
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
} else {
|
|
1644
|
+
// 如果没有elements,使用原始内容
|
|
1645
|
+
content = message.content
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
await navigator.clipboard.writeText(content)
|
|
1649
|
+
showNotification('已复制到剪贴板', 'success')
|
|
1650
|
+
} catch (error: any) {
|
|
1651
|
+
console.error('复制失败:', error)
|
|
1652
|
+
showNotification('复制失败: ' + (error?.message || String(error)), 'error')
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
function handleReplyMessage(message: MessageInfo | undefined) {
|
|
1657
|
+
if (!message) return;
|
|
1658
|
+
hideContextMenu()
|
|
1659
|
+
|
|
1660
|
+
try {
|
|
1661
|
+
// 在输入框中添加引用
|
|
1662
|
+
const quote = `<quote id="${(message.id).replace("bot-msg-", "")}"/>`
|
|
1663
|
+
inputMessage.value = quote + inputMessage.value
|
|
1664
|
+
|
|
1665
|
+
// 聚焦输入框
|
|
1666
|
+
nextTick(() => {
|
|
1667
|
+
if (messageInput.value) {
|
|
1668
|
+
messageInput.value.focus()
|
|
1669
|
+
}
|
|
1670
|
+
})
|
|
1671
|
+
} catch (error: any) {
|
|
1672
|
+
console.error('回复操作失败:', error)
|
|
1673
|
+
showNotification('操作失败: ' + (error?.message || String(error)), 'error')
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1350
1677
|
function getChannelTypeText(type: number | string): string {
|
|
1351
1678
|
if (typeof type === 'number') {
|
|
1352
1679
|
switch (type) {
|
|
@@ -2672,64 +2999,83 @@ export function useChatLogic() {
|
|
|
2672
2999
|
chatData.value.messages[channelKey] = []
|
|
2673
3000
|
}
|
|
2674
3001
|
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
3002
|
+
const messages = chatData.value.messages[channelKey];
|
|
3003
|
+
|
|
3004
|
+
// 查找是否有使用临时ID的消息(以bot-msg-开头)
|
|
3005
|
+
const tempMessageIndex = messages.findIndex(m => m.id.startsWith('bot-msg-') && Math.abs(m.timestamp - sentEvent.timestamp) < 5000);
|
|
3006
|
+
|
|
3007
|
+
if (tempMessageIndex !== -1) {
|
|
3008
|
+
// 找到了临时消息,更新它的ID和其他信息
|
|
3009
|
+
const tempMessage = messages[tempMessageIndex];
|
|
3010
|
+
tempMessage.id = sentEvent.messageId; // 更新为真实ID
|
|
3011
|
+
tempMessage.content = sentEvent.content;
|
|
3012
|
+
tempMessage.userId = sentEvent.selfId;
|
|
3013
|
+
tempMessage.username = sentEvent.botUsername;
|
|
3014
|
+
tempMessage.avatar = sentEvent.botAvatar;
|
|
3015
|
+
tempMessage.channelId = sentEvent.channelId;
|
|
3016
|
+
tempMessage.selfId = sentEvent.selfId;
|
|
3017
|
+
tempMessage.elements = sentEvent.elements;
|
|
3018
|
+
tempMessage.isBot = true;
|
|
3019
|
+
tempMessage.quote = sentEvent.quote;
|
|
3020
|
+
} else {
|
|
3021
|
+
// 没有找到临时消息,检查是否已存在真实ID的消息
|
|
3022
|
+
const exists = messages.find(m => m.id === sentEvent.messageId);
|
|
3023
|
+
if (!exists) {
|
|
3024
|
+
const botMessage: MessageInfo = {
|
|
3025
|
+
id: sentEvent.messageId,
|
|
3026
|
+
content: sentEvent.content,
|
|
3027
|
+
userId: sentEvent.selfId,
|
|
3028
|
+
username: sentEvent.botUsername,
|
|
3029
|
+
avatar: sentEvent.botAvatar,
|
|
3030
|
+
timestamp: sentEvent.timestamp,
|
|
3031
|
+
channelId: sentEvent.channelId,
|
|
3032
|
+
selfId: sentEvent.selfId,
|
|
3033
|
+
elements: sentEvent.elements,
|
|
3034
|
+
isBot: true, // 标记为机器人发送的消息
|
|
3035
|
+
quote: sentEvent.quote
|
|
3036
|
+
};
|
|
2691
3037
|
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
let insertIndex = messages.length
|
|
3038
|
+
// 按时间戳排序插入消息
|
|
3039
|
+
let insertIndex = messages.length;
|
|
2695
3040
|
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
3041
|
+
// 找到正确的插入位置(按时间戳排序)
|
|
3042
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3043
|
+
if (messages[i].timestamp <= sentEvent.timestamp) {
|
|
3044
|
+
insertIndex = i + 1;
|
|
3045
|
+
break;
|
|
3046
|
+
}
|
|
3047
|
+
if (i === 0) {
|
|
3048
|
+
insertIndex = 0;
|
|
3049
|
+
}
|
|
2704
3050
|
}
|
|
2705
|
-
}
|
|
2706
3051
|
|
|
2707
|
-
|
|
3052
|
+
messages.splice(insertIndex, 0, botMessage);
|
|
2708
3053
|
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
3054
|
+
// 保持消息数量限制
|
|
3055
|
+
if (messages.length > 100) {
|
|
3056
|
+
chatData.value.messages[channelKey] = messages.slice(-100);
|
|
3057
|
+
}
|
|
2713
3058
|
|
|
2714
|
-
|
|
2715
|
-
|
|
3059
|
+
// 更新频道消息数量缓存
|
|
3060
|
+
channelMessageCounts.value[channelKey] = messages.length;
|
|
2716
3061
|
|
|
2717
|
-
|
|
2718
|
-
|
|
3062
|
+
// 在添加新消息前检查是否在底部附近
|
|
3063
|
+
const wasNearBottom = isNearBottom();
|
|
2719
3064
|
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
3065
|
+
// 智能滚动:基于添加消息前的位置状态来决定是否滚动
|
|
3066
|
+
nextTick(() => {
|
|
3067
|
+
// 再次等待,确保新消息的DOM已经渲染
|
|
3068
|
+
setTimeout(() => {
|
|
3069
|
+
if (wasNearBottom) {
|
|
3070
|
+
scrollToBottom();
|
|
3071
|
+
}
|
|
3072
|
+
}, 10);
|
|
3073
|
+
});
|
|
3074
|
+
}
|
|
2729
3075
|
}
|
|
2730
3076
|
|
|
2731
3077
|
// 触发响应式更新
|
|
2732
|
-
chatData.value = { ...chatData.value }
|
|
3078
|
+
chatData.value = { ...chatData.value };
|
|
2733
3079
|
}
|
|
2734
3080
|
|
|
2735
3081
|
// 处理机器人消息事件
|
|
@@ -3371,6 +3717,7 @@ export function useChatLogic() {
|
|
|
3371
3717
|
selectChannel,
|
|
3372
3718
|
handleBotRightClick,
|
|
3373
3719
|
handleChannelRightClick,
|
|
3720
|
+
handleMessageRightClick,
|
|
3374
3721
|
showContextMenu,
|
|
3375
3722
|
hideContextMenu,
|
|
3376
3723
|
handleKeyDown,
|
|
@@ -3408,6 +3755,16 @@ export function useChatLogic() {
|
|
|
3408
3755
|
handleTouchEnd,
|
|
3409
3756
|
handleInputFocus,
|
|
3410
3757
|
loadMoreMessages,
|
|
3758
|
+
handlePlusOne,
|
|
3759
|
+
handleCopyMessage,
|
|
3760
|
+
handleReplyMessage,
|
|
3761
|
+
parseInlineQuote,
|
|
3762
|
+
getInlineQuoteMessage,
|
|
3763
|
+
getQuoteUser,
|
|
3764
|
+
getQuoteTimestamp,
|
|
3765
|
+
getQuoteContent,
|
|
3766
|
+
getQuoteElements,
|
|
3767
|
+
getMessageContentWithoutQuote,
|
|
3411
3768
|
|
|
3412
3769
|
// 图片缓存相关
|
|
3413
3770
|
getCachedImageUrl,
|
|
@@ -3421,6 +3778,7 @@ export function useChatLogic() {
|
|
|
3421
3778
|
isFileUrl,
|
|
3422
3779
|
loadHistoryMessages,
|
|
3423
3780
|
handleMessageEvent,
|
|
3781
|
+
handleBotMessageSentEvent,
|
|
3424
3782
|
saveSelectionState,
|
|
3425
3783
|
restoreSelectionState
|
|
3426
3784
|
}
|
package/client/vue/index.vue
CHANGED
|
@@ -71,9 +71,10 @@
|
|
|
71
71
|
<div class="loading-spinner"></div>
|
|
72
72
|
<span>加载更多消息中...</span>
|
|
73
73
|
</div>
|
|
74
|
-
|
|
74
|
+
|
|
75
75
|
<div v-for="message in currentMessages" :key="message.id"
|
|
76
|
-
:class="['message-item', { 'bot-message': message.isBot }]"
|
|
76
|
+
:class="['message-item', { 'bot-message': message.isBot }]"
|
|
77
|
+
@contextmenu="message.isBot ? null : handleMessageRightClick($event, message)">
|
|
77
78
|
<div class="message-avatar">
|
|
78
79
|
<AvatarComponent v-if="message.avatar" :src="message.avatar" :alt="message.username"
|
|
79
80
|
:channel-key="currentChannelKey" />
|
|
@@ -86,25 +87,27 @@
|
|
|
86
87
|
</div>
|
|
87
88
|
|
|
88
89
|
<!-- 引用消息显示 -->
|
|
89
|
-
<div v-if="message.quote" class="message-quote">
|
|
90
|
+
<div v-if="message.quote || getInlineQuoteMessage(message, currentMessages)" class="message-quote">
|
|
90
91
|
<div class="quote-header">
|
|
91
92
|
<div class="quote-avatar">
|
|
92
|
-
<AvatarComponent v-if="message
|
|
93
|
-
:
|
|
93
|
+
<AvatarComponent v-if="getQuoteUser(message, currentMessages).avatar"
|
|
94
|
+
:src="getQuoteUser(message, currentMessages).avatar"
|
|
95
|
+
:alt="getQuoteUser(message, currentMessages).username" :channel-key="currentChannelKey" />
|
|
94
96
|
<div v-else class="avatar-placeholder">{{
|
|
95
|
-
message.
|
|
97
|
+
getQuoteUser(message, currentMessages).username.charAt(0).toUpperCase() }}
|
|
96
98
|
</div>
|
|
97
99
|
</div>
|
|
98
|
-
<span class="quote-username">{{ message.
|
|
99
|
-
<span class="quote-time">{{ formatTime(message
|
|
100
|
+
<span class="quote-username">{{ getQuoteUser(message, currentMessages).username }}</span>
|
|
101
|
+
<span class="quote-time">{{ formatTime(getQuoteTimestamp(message, currentMessages)) }}</span>
|
|
100
102
|
</div>
|
|
101
103
|
<div class="quote-content">
|
|
102
|
-
<template
|
|
103
|
-
|
|
104
|
-
|
|
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" />
|
|
105
108
|
</template>
|
|
106
109
|
<template v-else>
|
|
107
|
-
{{ message
|
|
110
|
+
{{ getQuoteContent(message, currentMessages) }}
|
|
108
111
|
</template>
|
|
109
112
|
</div>
|
|
110
113
|
</div>
|
|
@@ -115,7 +118,7 @@
|
|
|
115
118
|
:channel-key="currentChannelKey" />
|
|
116
119
|
</template>
|
|
117
120
|
<template v-else>
|
|
118
|
-
{{ message
|
|
121
|
+
{{ getMessageContentWithoutQuote(message, currentMessages) }}
|
|
119
122
|
</template>
|
|
120
123
|
</div>
|
|
121
124
|
</div>
|
|
@@ -205,6 +208,19 @@
|
|
|
205
208
|
彻底删除此频道所有数据
|
|
206
209
|
</div>
|
|
207
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>
|
|
208
224
|
</div>
|
|
209
225
|
|
|
210
226
|
<!-- 滑动指示器 -->
|
|
@@ -317,6 +333,12 @@ const {
|
|
|
317
333
|
handleTouchEnd,
|
|
318
334
|
handleInputFocus,
|
|
319
335
|
|
|
336
|
+
// 消息右键菜单处理函数
|
|
337
|
+
handleMessageRightClick,
|
|
338
|
+
handlePlusOne,
|
|
339
|
+
handleCopyMessage,
|
|
340
|
+
handleReplyMessage,
|
|
341
|
+
|
|
320
342
|
// 图片缓存相关
|
|
321
343
|
getCachedImageUrl,
|
|
322
344
|
cacheImage,
|
|
@@ -327,6 +349,14 @@ const {
|
|
|
327
349
|
// 其他工具函数
|
|
328
350
|
isFileUrl,
|
|
329
351
|
loadHistoryMessages,
|
|
330
|
-
handleMessageEvent
|
|
352
|
+
handleMessageEvent,
|
|
353
|
+
handleBotMessageSentEvent,
|
|
354
|
+
parseInlineQuote,
|
|
355
|
+
getInlineQuoteMessage,
|
|
356
|
+
getQuoteUser,
|
|
357
|
+
getQuoteTimestamp,
|
|
358
|
+
getQuoteContent,
|
|
359
|
+
getQuoteElements,
|
|
360
|
+
getMessageContentWithoutQuote
|
|
331
361
|
} = chatLogic
|
|
332
362
|
</script>
|