深入解析:Tailwind CSS 实战:基于 Kooboo 构建 AI 对话框页面(七):消息框交互功能添加
Tailwind CSS 实战,基于Kooboo构建AI对话框页面(一)
Tailwind CSS 实战,基于Kooboo构建AI对话框页面(二):实现交互功能
Tailwind CSS 实战,基于 Kooboo 构建 AI 对话框页面(三):实现暗黑模式主题切换
Tailwind CSS 实战,基于 Kooboo 构建 AI 对话框页面(四):语音识别输入功能
Tailwind CSS 实战:基于 Kooboo 构建 AI 对话框页面(五):语音合成输出与交互增强
Tailwind CSS 实战:基于 Kooboo 构建 AI 对话框页面(六):图片上传交互功能
在《Tailwind CSS 实战》系列 前六篇内容中,我们围绕Kooboo 平台,逐步构建起 AI 对话框页面的基础框架、语音交互、主题切换等能力。本篇作为第七篇,聚焦纯 JavaScript 驱动的消息交互功能迭代,在不改动 HTML 结构的前提下,为消息框新增 复制、编辑、删除(用户消息)及复制、重新生成(AI 消息)功能,深度优化对话交互体验。实现效果如下:
一、功能需求分析
(一)用户消息操作
- 复制内容:点击复制按钮,调用浏览器剪贴板 API,将消息文本复制,支持后续粘贴复用。
- 编辑消息:允许对已发送的消息进行编辑,编辑完成并发送后,在消息上显示 “(已编辑)” 标记,清晰区分原始与修改后内容。
- 删除消息:用户可主动移除自身发送的消息,触发删除操作时,通过
confirm
弹窗二次确认,避免误删。
(二)AI 消息操作
- 复制内容:与用户消息复制逻辑复用,快速提取 AI 回复文本到剪贴板。
- 重新生成:点击按钮后,展示加载动画,模拟 AI 重新处理逻辑,完成后替换原有回复内容,为用户提供多样化 AI 回答。
二、实现思路
- 不改动 HTML 结构:依赖 Tailwind CSS 预设的
.message
.copy-button
等类名,通过 JavaScript 动态绑定 事件。 - 交互逻辑解耦:封装通用函数(如复制、加载状态),避免重复代码,适配用户与 AI 消息的差异化交互。
三、核心 Javascript 逻辑代码
1. 初始化与事件绑定
// 初始化消息操作事件 function setupMessageActions() { const messages = document.querySelectorAll('.message'); messages.forEach(message => { const messageType = message.dataset.messageType; const actions = message.querySelector('.absolute.top-2.right-2'); // 鼠标悬停显示操作按钮 message.addEventListener('mouseenter', () => { actions.classList.remove('opacity-0'); actions.classList.add('opacity-100'); }); message.addEventListener('mouseleave', () => { actions.classList.remove('opacity-100'); actions.classList.add('opacity-0'); }); // 复制按钮点击 const copyButton = message.querySelector('.copy-button'); if (copyButton) { copyButton.addEventListener('click', () => { copyMessageContent(copyButton); }); } // 编辑按钮点击(用户消息) if (messageType === 'user') { const editButton = message.querySelector('.edit-button'); const deleteButton = message.querySelector('.delete-button'); editButton.addEventListener('click', () => { const messageId = message.dataset.messageId; const messageText = message.querySelector('.message-content p').textContent; // 设置当前编辑的消息ID currentEditingMessageId = messageId; // 将消息内容放入输入框 messageInput.value = messageText; messageInput.focus(); // 保持发送按钮文本不变 sendButton.textContent = '发送'; }); // 删除消息 deleteButton.addEventListener('click', () => { if (confirm('确定要删除这条消息吗?')) { message.remove(); } }); } // 重新生成按钮点击(AI消息) if (messageType === 'ai') { const regenerateButton = message.querySelector('.regenerate-button'); regenerateButton.addEventListener('click', () => { const originalText = message.querySelector('.message-content code').textContent.trim(); regenerateAIResponse(message, originalText); }); } }); }
核心逻辑:
- 事件处理函数仅负责状态变更(如设置
currentEditingMessageId
)和触发行为(如调用copyMessageContent
)。 - 具体的 DOM 更新(如修改消息内容、添加标记)集中在其他函数(如
sendMessage
)中处理,避免事件回调过于臃肿。
2. 复制功能实现
function copyMessageContent(element) { const messageContent = element.closest('.message').querySelector('.message-content'); let textToCopy = ''; // 智能识别文本类型(普通文本或代码块) const paragraphs = messageContent.querySelectorAll('p'); if (paragraphs.length > 0) { paragraphs.forEach(p => textToCopy += p.textContent + '\n'); } else { const codeBlocks = messageContent.querySelectorAll('code'); if (codeBlocks.length > 0) { codeBlocks.forEach(code => textToCopy += code.textContent + '\n'); } } // 使用现代剪贴板 API navigator.clipboard.writeText(textToCopy.trim()) .then(() => showToast(' 内容已复制到剪贴板')) .catch(err => { console.error('无法复制内容:', err); showToast('复制失败,请重试'); });}
核心逻辑:
- 通过
closest('.message')
找到最近的消息容器,确保能正确定位内容 - 自动识别内容类型(普通文本
p
或代码块code
) - 使用
navigator.clipboard.writeText
实现跨平台复制 - 成功 / 失败都有明确的视觉反馈(toast 提示)
3. 编辑功能实现
// 全局变量跟踪当前编辑状态let currentEditingMessageId = null; // 编辑按钮点击事件editButton.addEventListener('click', () => { const messageId = message.dataset.messageId; const messageText = message.querySelector('.message-content p').textContent; // 设置编辑状态并填充内容 currentEditingMessageId = messageId; messageInput.value = messageText; messageInput.focus();}); // 发送消息时处理编辑状态function sendMessage(text) { if (!text) return; if (currentEditingMessageId) { // 找到原消息并标记为已编辑 const originalMessage = document.querySelector(`.message[data-message-id="${currentEditingMessageId}"]`); const editIndicator = document.createElement('span'); editIndicator.className = 'text-xs text-blue-300 ml-2'; editIndicator.textContent = '(已编辑)'; // 添加编辑标记并隐藏操作按钮 const timestamp = originalMessage.querySelector('.message-timestamp'); timestamp.parentNode.insertBefore(editIndicator, timestamp.nextSibling); originalMessage.querySelector('.absolute.top-2.right-2').style.display = 'none'; // 清除编辑状态 currentEditingMessageId = null; } // 正常发送消息逻辑...}
核心逻辑:
- 使用全局变量
currentEditingMessageId
跟踪编辑状态 - 点击编辑时,将消息内容回填到输入框
- 发送消息时,检查编辑状态并处理原消息:
- 添加 "(已编辑)" 标记
- 隐藏编辑 / 删除按钮(防止重复编辑)
- 编辑完成后自动清除状态
4. 删除功能实现
deleteButton.addEventListener('click', () => { if (confirm('确定要删除这条消息吗?')) { message.remove(); }});
核心逻辑:
- 使用浏览器内置的
confirm
对话框进行二次确认 - 确认后直接从 DOM 中移除消息元素
- 简单高效,防止误操作
5. 重新生成功能实现
regenerateButton.addEventListener('click', () => { const originalText = message.querySelector('.message-content code').textContent.trim(); const contentDiv = message.querySelector('.message-content'); // 显示加载状态 contentDiv.innerHTML = ` 正在重新生成... `; // 模拟API请求延迟 setTimeout(() => { const newResponse = generateAIResponse(originalText); contentDiv.innerHTML = ` ${newResponse}
`; // 更新时间戳 const timestamp = message.querySelector('.message-timestamp'); timestamp.textContent = getCurrentTime(); // 自动播放语音(如果启用) if (speechSettings.autoPlay && !synth.speaking) { speak(newResponse); } }, 1000);});核心逻辑:
- 点击后立即显示加载动画(旋转的蓝色圆点)
- 使用
setTimeout
模拟 API 请求延迟(实际项目中替换为真实 API 调用) - 生成新内容后:
- 更新消息内容
- 更新时间戳
- 自动播放语音(如果启用)
- 整个过程保持界面响应,用户可随时取消
6. 提示信息显示
function showToast(message) { const toast = document.createElement('div'); toast.className = 'fixed top-4 left-1/2 transform -translate-x-1/2 bg-gray-800 text-white px-4 py-2 rounded-lg shadow-lg z-50 transition-all duration-300 translate-y-[-20px] opacity-0'; toast.innerHTML = `${message}`; document.body.appendChild(toast); setTimeout(() => { toast.classList.remove('translate-y-[-20px]', 'opacity-0'); toast.classList.add('translate-y-0', 'opacity-100'); }, 10); setTimeout(() => { toast.classList.add('translate-y-[-20px]', 'opacity-0'); setTimeout(() => { document.body.removeChild(toast); }, 300); }, 3000);}
核心逻辑:
- 创建顶部居中的提示框
- 使用 Tailwind CSS 类控制动画:
- 淡入:从 opacity-0 到 opacity-100
- 上移:从 translate-y-[-20px] 到 translate-y-0
- 3 秒后自动淡出并移除
7. 全局状态管理
// 全局变量跟踪状态let currentEditingMessageId = null;let lastPlayedButton = null;let lastPlayedText = ''; // 点击非消息区域时取消编辑状态document.addEventListener('click', (e) => { if (!e.target.closest('.message') && !e.target.closest('#messageInput')) { currentEditingMessageId = null; }});
核心逻辑:
- 使用全局变量跟踪关键状态(编辑、语音播放等)
- 点击其他区域时自动取消编辑状态
- 确保系统状态与用户操作保持同步
四、功能验证
- 复制功能:点击用户 / AI 消息的 “复制” 按钮,检查剪贴板内容是否正确,提示框是否正常显示 / 消失。
- 编辑功能:编辑用户消息后发送,确认 “(已编辑)” 标记显示、操作按钮隐藏。
- 删除功能:删除用户消息时,确认二次确认弹窗,及消息节点是否从 DOM 移除。
- 重新生成功能:点击 AI 消息 “重新生成”,检查加载动画、新内容渲染是否正常。
五、总结
本次功能的实现采用了以下关键技术和设计模式:
- 状态管理模式:使用全局变量跟踪编辑状态
- 事件委托模式:为动态生成的元素绑定事件
- 异步处理:使用 Promise 和 setTimeout 处理剪贴板操作和 API 请求
- UI 状态机:通过修改 DOM 结构实现加载状态和编辑标记
- 响应式设计:使用 Tailwind 的响应式类名适配不同屏幕尺寸
这些功能通过精心设计的交互流程和视觉反馈,为用户提供了直观、高效的消息管理体验,同时保持了代码的可维护性和扩展性。