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.
@@ -601,7 +601,7 @@ export function useChatLogic() {
601
601
  return h('span', {
602
602
  class: 'message-at',
603
603
  title: element.attrs.name
604
- }, `${element.attrs.name || element.attrs.id}`)
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' | null
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: null,
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
- const exists = chatData.value.messages[channelKey].find(m => m.id === sentEvent.messageId)
2677
- if (!exists) {
2678
- const botMessage: MessageInfo = {
2679
- id: sentEvent.messageId,
2680
- content: sentEvent.content,
2681
- userId: sentEvent.selfId,
2682
- username: sentEvent.botUsername,
2683
- avatar: sentEvent.botAvatar,
2684
- timestamp: sentEvent.timestamp,
2685
- channelId: sentEvent.channelId,
2686
- selfId: sentEvent.selfId,
2687
- elements: sentEvent.elements,
2688
- isBot: true, // 标记为机器人发送的消息
2689
- quote: sentEvent.quote
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
- const messages = chatData.value.messages[channelKey]
2694
- let insertIndex = messages.length
3038
+ // 按时间戳排序插入消息
3039
+ let insertIndex = messages.length;
2695
3040
 
2696
- // 找到正确的插入位置(按时间戳排序)
2697
- for (let i = messages.length - 1; i >= 0; i--) {
2698
- if (messages[i].timestamp <= sentEvent.timestamp) {
2699
- insertIndex = i + 1
2700
- break
2701
- }
2702
- if (i === 0) {
2703
- insertIndex = 0
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
- messages.splice(insertIndex, 0, botMessage)
3052
+ messages.splice(insertIndex, 0, botMessage);
2708
3053
 
2709
- // 保持消息数量限制
2710
- if (messages.length > 100) {
2711
- chatData.value.messages[channelKey] = messages.slice(-100)
2712
- }
3054
+ // 保持消息数量限制
3055
+ if (messages.length > 100) {
3056
+ chatData.value.messages[channelKey] = messages.slice(-100);
3057
+ }
2713
3058
 
2714
- // 更新频道消息数量缓存
2715
- channelMessageCounts.value[channelKey] = messages.length
3059
+ // 更新频道消息数量缓存
3060
+ channelMessageCounts.value[channelKey] = messages.length;
2716
3061
 
2717
- // 在添加新消息前检查是否在底部附近
2718
- const wasNearBottom = isNearBottom()
3062
+ // 在添加新消息前检查是否在底部附近
3063
+ const wasNearBottom = isNearBottom();
2719
3064
 
2720
- // 智能滚动:基于添加消息前的位置状态来决定是否滚动
2721
- nextTick(() => {
2722
- // 再次等待,确保新消息的DOM已经渲染
2723
- setTimeout(() => {
2724
- if (wasNearBottom) {
2725
- scrollToBottom()
2726
- }
2727
- }, 10)
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
  }
@@ -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.quote.user.avatar" :src="message.quote.user.avatar"
93
- :alt="message.quote.user.username" :channel-key="currentChannelKey" />
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.quote.user.username.charAt(0).toUpperCase() }}
97
+ getQuoteUser(message, currentMessages).username.charAt(0).toUpperCase() }}
96
98
  </div>
97
99
  </div>
98
- <span class="quote-username">{{ message.quote.user.username }}</span>
99
- <span class="quote-time">{{ formatTime(message.quote.timestamp) }}</span>
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 v-if="message.quote.elements && message.quote.elements.length > 0">
103
- <MessageElement v-for="(element, index) in message.quote.elements" :key="`quote-${index}`"
104
- :element="element" :channel-key="currentChannelKey" />
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.quote.content }}
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.content }}
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>