/** * 消息操作菜单 */ import { getSettings, SUMMARY_MARKER_PREFIX, splitAIMessages } from './config.js'; import { requestSave } from './save-manager.js'; import { currentChatIndex, openChat, showTypingIndicator, hideTypingIndicator, appendMessage } from './chat.js'; import { showToast } from './toast.js'; import { getContext } from '../../../extensions.js'; import { formatQuoteDate } from './utils.js'; import { isInGroupChat, getCurrentGroupIndex, openGroupChat } from './group-chat.js'; // 当前显示菜单的消息索引 let currentMenuMsgIndex = -1; // 长按计时器 let longPressTimer = null; // 是否正在长按 let isLongPress = false; // 待引用的消息 let pendingQuote = null; // 菜单项配置 const menuItems = [ { id: 'copy', icon: 'copy', text: '复制' }, { id: 'transcribe', icon: 'transcribe', text: '转文字', voiceOnly: true }, { id: 'quote', icon: 'quote', text: '引用' }, { id: 'recall', icon: 'recall', text: '撤回', userOnly: true }, { id: 'regenerate', icon: 'regenerate', text: '重新生成', userOnly: true }, { id: 'multiselect', icon: 'multiselect', text: '多选' } ]; // 图标SVG const icons = { copy: ``, transcribe: ``, quote: ``, recall: ``, regenerate: ``, multiselect: `` }; // 创建菜单DOM function createMenuElement(isUserMessage = false, isVoiceMessage = false, voiceTextVisible = false) { const menu = document.createElement('div'); menu.className = 'wechat-msg-menu hidden'; menu.id = 'wechat-msg-menu'; const menuContent = document.createElement('div'); menuContent.className = 'wechat-msg-menu-content'; menuItems.forEach(item => { // 跳过仅用户可用的菜单项(如果当前不是用户消息) if (item.userOnly && !isUserMessage) return; // 跳过仅语音消息可用的菜单项(如果当前不是语音消息) if (item.voiceOnly && !isVoiceMessage) return; const menuItem = document.createElement('div'); menuItem.className = 'wechat-msg-menu-item'; menuItem.dataset.action = item.id; // 转文字按钮根据状态显示不同文本 let text = item.text; if (item.id === 'transcribe' && voiceTextVisible) { text = '收起文字'; } menuItem.innerHTML = `
`; menuContent.appendChild(menuItem); }); menu.appendChild(menuContent); return menu; } // 显示菜单 export function showMessageMenu(msgElement, msgIndex, event) { hideMessageMenu(); currentMenuMsgIndex = msgIndex; // 检查是否为用户消息 const settings = getSettings(); const groupIndex = getCurrentGroupIndex(); let msg; if (groupIndex >= 0) { // 群聊模式 const groupChat = settings.groupChats?.[groupIndex]; msg = groupChat?.chatHistory?.[msgIndex]; } else { // 单聊模式 const contact = settings.contacts[currentChatIndex]; msg = contact?.chatHistory?.[msgIndex]; } // 优先从历史记录判断,其次从元素属性判断(处理分割显示的消息) let isUserMessage = msg?.role === 'user'; if (msg === undefined) { // 如果找不到消息记录,尝试从元素属性获取 const roleAttr = msgElement?.dataset?.msgRole || msgElement?.closest?.('[data-msg-role]')?.dataset?.msgRole; isUserMessage = roleAttr === 'user'; } // 检测是否是语音消息 const voiceBubble = msgElement.classList?.contains('wechat-voice-bubble') ? msgElement : msgElement.querySelector?.('.wechat-voice-bubble'); const isVoiceMessage = !!voiceBubble || msg?.isVoice === true; // 检测语音转文字是否已显示 let voiceTextVisible = false; if (voiceBubble) { const voiceId = voiceBubble.dataset?.voiceId; if (voiceId) { const textEl = document.getElementById(voiceId); voiceTextVisible = textEl?.classList.contains('visible') || false; } } // 移除旧菜单并创建新菜单(根据消息类型动态生成) let menu = document.getElementById('wechat-msg-menu'); if (menu) { menu.remove(); } menu = createMenuElement(isUserMessage, isVoiceMessage, voiceTextVisible); // 存储语音相关数据 if (voiceBubble) { menu.dataset.voiceId = voiceBubble.dataset?.voiceId || ''; menu.dataset.voiceContent = voiceBubble.dataset?.voiceContent || ''; } document.querySelector('.wechat-phone').appendChild(menu); bindMenuEvents(menu); // 计算位置 const msgRect = msgElement.getBoundingClientRect(); const phoneEl = document.querySelector('.wechat-phone'); const phoneRect = phoneEl.getBoundingClientRect(); // 相对于手机容器的位置 const relativeTop = msgRect.top - phoneRect.top; const relativeLeft = msgRect.left - phoneRect.left; menu.classList.remove('hidden'); // 获取菜单尺寸 const menuRect = menu.getBoundingClientRect(); // 默认显示在消息上方 let top = relativeTop - menuRect.height - 8; let left = relativeLeft + (msgRect.width / 2) - (menuRect.width / 2); // 如果上方空间不够,显示在下方 if (top < 50) { top = relativeTop + msgRect.height + 8; } // 左右边界检查 if (left < 10) left = 10; if (left + menuRect.width > phoneRect.width - 10) { left = phoneRect.width - menuRect.width - 10; } menu.style.top = `${top}px`; menu.style.left = `${left}px`; // 点击其他地方关闭菜单 setTimeout(() => { document.addEventListener('click', handleOutsideClick); document.addEventListener('touchstart', handleOutsideClick); }, 10); } // 隐藏菜单 export function hideMessageMenu() { const menu = document.getElementById('wechat-msg-menu'); if (menu) { menu.classList.add('hidden'); } currentMenuMsgIndex = -1; document.removeEventListener('click', handleOutsideClick); document.removeEventListener('touchstart', handleOutsideClick); } // 点击外部关闭 function handleOutsideClick(e) { const menu = document.getElementById('wechat-msg-menu'); if (menu && !menu.contains(e.target)) { hideMessageMenu(); } } // 绑定菜单事件 function bindMenuEvents(menu) { menu.addEventListener('click', (e) => { const menuItem = e.target.closest('.wechat-msg-menu-item'); if (!menuItem) return; const action = menuItem.dataset.action; // 传递菜单上存储的语音数据 const voiceId = menu.dataset.voiceId; const voiceContent = menu.dataset.voiceContent; handleMenuAction(action, currentMenuMsgIndex, voiceId, voiceContent); hideMessageMenu(); }); } // 处理菜单操作 function handleMenuAction(action, msgIndex, voiceId = '', voiceContent = '') { const settings = getSettings(); const groupIndex = getCurrentGroupIndex(); let chatHistory, contact, groupChat; if (groupIndex >= 0) { // 群聊模式 groupChat = settings.groupChats?.[groupIndex]; if (!groupChat || !groupChat.chatHistory || msgIndex < 0) return; chatHistory = groupChat.chatHistory; } else { // 单聊模式 contact = settings.contacts[currentChatIndex]; if (!contact || !contact.chatHistory || msgIndex < 0) return; chatHistory = contact.chatHistory; } const msg = chatHistory[msgIndex]; if (!msg) return; switch (action) { case 'copy': copyMessage(msg.content); break; case 'transcribe': // 切换语音转文字显示 if (voiceId) { const textEl = document.getElementById(voiceId); if (textEl) { const isVisible = textEl.classList.contains('visible'); if (isVisible) { textEl.classList.remove('visible'); textEl.classList.add('hidden'); } else { textEl.classList.remove('hidden'); textEl.classList.add('visible'); } } } break; case 'quote': quoteMessage(msg, groupIndex >= 0, groupChat); break; case 'recall': if (groupIndex >= 0) { recallGroupMessage(msgIndex, groupChat); } else { recallMessage(msgIndex, contact); } break; case 'regenerate': if (groupIndex >= 0) { regenerateGroupMessage(msgIndex, groupChat); } else { regenerateMessage(msgIndex, contact); } break; case 'multiselect': showToast('多选功能开发中'); break; } } // 复制消息 function copyMessage(content) { navigator.clipboard.writeText(content).then(() => { showToast('已复制'); }).catch(() => { // 降级方案 const textarea = document.createElement('textarea'); textarea.value = content; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); showToast('已复制'); }); } // 引用消息 - 设置待引用状态 function quoteMessage(msg, isGroupChat = false, groupChat = null) { // 不允许引用撤回的消息 if (msg.isRecalled) { showToast('无法引用已撤回的消息'); return; } const settings = getSettings(); const context = getContext(); // 确定发送者名称 let senderName; if (msg.role === 'user') { senderName = context?.name1 || '我'; } else if (isGroupChat) { // 群聊模式:使用消息中存储的角色名 senderName = msg.characterName || '群成员'; } else { // 单聊模式:使用联系人名称 const contact = settings.contacts[currentChatIndex]; senderName = contact?.name || '对方'; } // 格式化日期 const date = formatQuoteDate(msg.timestamp); // 设置待引用消息 const isMusic = msg.isMusic === true; let quoteContent = msg.content; if (isMusic && msg.musicInfo) { const artist = (msg.musicInfo.artist || '').toString().trim(); const name = (msg.musicInfo.name || '').toString().trim(); quoteContent = artist && name ? `${artist}-${name}` : (name || artist || msg.content); } pendingQuote = { content: quoteContent, sender: senderName, date: date, isVoice: msg.isVoice === true, isPhoto: msg.isPhoto === true, isSticker: msg.isSticker === true, isMusic: isMusic }; // 显示引用预览条 showQuotePreview(); // 聚焦输入框 const input = document.getElementById('wechat-input'); if (input) { input.focus(); } } // 显示引用预览条 function showQuotePreview() { if (!pendingQuote) return; // 移除已有的预览条 hideQuotePreview(); const inputArea = document.querySelector('.wechat-chat-input'); if (!inputArea) return; const previewBar = document.createElement('div'); previewBar.className = 'wechat-quote-preview'; previewBar.id = 'wechat-quote-preview'; // 根据消息类型生成显示文本 let contentText; if (pendingQuote.isVoice) { const seconds = Math.max(2, Math.min(60, Math.ceil(pendingQuote.content.length / 3))); contentText = `[语音] ${seconds}"`; } else if (pendingQuote.isPhoto) { contentText = '[照片]'; } else if (pendingQuote.isSticker) { contentText = '[表情]'; } else { contentText = pendingQuote.content.length > 25 ? pendingQuote.content.substring(0, 25) + '...' : pendingQuote.content; } previewBar.innerHTML = `