如何用Vanilla JS实现聊天机器人的文件附件输入框UI?
聊天机器人附件功能:UI设计方案与Vanilla JS实现
一、UI设计思路
核心交互逻辑
- 极简布局:在聊天输入框旁放置「回形针图标按钮」,点击触发文件选择;输入框下方预留区域展示已选文件,不占用输入空间。
- 格式限制提示:按钮hover时显示支持的格式(PDF/CSV等),文件选择时直接过滤不符合的格式,避免无效操作。
- 已选文件管理:每个已选文件显示为小型卡片,包含文件名、格式化后的文件大小、删除按钮;支持多选,超出数量可横向滚动或折叠。
- 状态反馈:文件选中后立即显示预览;若格式错误,弹出内联提示(不使用弹窗);发送时若文件未处理完成,禁用发送按钮并显示加载状态。
响应式适配
- 桌面端:附件按钮放在输入框左侧,发送按钮在右侧,文件预览区横向排列。
- 移动端:附件按钮和发送按钮合并到输入框右侧的下拉菜单,文件预览区垂直堆叠,节省横向空间。
二、Vanilla JS 完整实现代码
HTML 结构
<div class="chat-input-container"> <!-- 隐藏原生文件选择框,用自定义按钮触发 --> <input type="file" id="fileInput" multiple accept=".pdf,.csv" style="display: none;"> <button id="attachBtn" class="attach-btn" title="添加PDF/CSV文件">📎</button> <textarea id="messageInput" class="message-input" placeholder="输入消息..."></textarea> <button id="sendBtn" class="send-btn">发送</button> </div> <!-- 已选文件预览区 --> <div id="filePreviewContainer" class="file-preview-container"></div>
CSS 样式
.chat-input-container { display: flex; align-items: center; gap: 8px; padding: 10px; border: 1px solid #e0e0e0; border-radius: 20px; max-width: 600px; margin: 20px auto; } .attach-btn, .send-btn { width: 36px; height: 36px; border: none; border-radius: 50%; background-color: #f0f0f0; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 16px; } .attach-btn:hover, .send-btn:hover { background-color: #e0e0e0; } .message-input { flex: 1; border: none; resize: none; padding: 8px 12px; font-size: 14px; min-height: 20px; } .message-input:focus { outline: none; } .file-preview-container { max-width: 600px; margin: 0 auto 10px; display: flex; gap: 10px; flex-wrap: wrap; } .file-preview-card { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background-color: #f5f5f5; border-radius: 12px; font-size: 14px; } .delete-file-btn { background: none; border: none; cursor: pointer; color: #666; font-size: 16px; } .delete-file-btn:hover { color: #ff4444; } .send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
JavaScript 逻辑
// 获取DOM元素 const attachBtn = document.getElementById('attachBtn'); const fileInput = document.getElementById('fileInput'); const filePreviewContainer = document.getElementById('filePreviewContainer'); const sendBtn = document.getElementById('sendBtn'); const messageInput = document.getElementById('messageInput'); // 存储已选文件 let selectedFiles = []; // 点击附件按钮触发文件选择 attachBtn.addEventListener('click', () => { fileInput.click(); }); // 处理文件选择事件 fileInput.addEventListener('change', (e) => { const files = Array.from(e.target.files); // 过滤已存在的文件(避免重复选择) const newFiles = files.filter(file => !selectedFiles.some(f => f.name === file.name && f.size === file.size)); if (newFiles.length === 0) return; // 添加到已选文件列表 selectedFiles = [...selectedFiles, ...newFiles]; // 渲染预览卡片 renderFilePreviews(); }); // 渲染已选文件预览 function renderFilePreviews() { filePreviewContainer.innerHTML = ''; selectedFiles.forEach((file, index) => { const card = document.createElement('div'); card.className = 'file-preview-card'; // 文件信息:名称 + 格式化大小 const fileInfo = document.createElement('span'); const fileSize = formatFileSize(file.size); fileInfo.textContent = `${file.name} (${fileSize})`; // 删除按钮 const deleteBtn = document.createElement('button'); deleteBtn.className = 'delete-file-btn'; deleteBtn.textContent = '×'; deleteBtn.addEventListener('click', () => { selectedFiles.splice(index, 1); renderFilePreviews(); }); card.appendChild(fileInfo); card.appendChild(deleteBtn); filePreviewContainer.appendChild(card); }); // 有文件或文本时启用发送按钮 sendBtn.disabled = selectedFiles.length === 0 && messageInput.value.trim() === ''; } // 格式化文件大小(KB/MB) function formatFileSize(bytes) { if (bytes < 1024) return bytes + ' B'; else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; else return (bytes / 1048576).toFixed(1) + ' MB'; } // 发送按钮点击事件 sendBtn.addEventListener('click', () => { const message = messageInput.value.trim(); // 处理文件上传逻辑 if (selectedFiles.length > 0) { const formData = new FormData(); selectedFiles.forEach(file => formData.append('attachments', file)); if (message) formData.append('message', message); // 模拟上传状态 sendBtn.disabled = true; sendBtn.textContent = '发送中...'; setTimeout(() => { // 上传完成后清空状态 messageInput.value = ''; selectedFiles = []; renderFilePreviews(); sendBtn.disabled = false; sendBtn.textContent = '发送'; alert('消息和文件已发送'); // 实际项目替换为聊天消息展示逻辑 }, 1500); } else if (message) { // 仅发送文本消息 alert('文本消息已发送'); messageInput.value = ''; sendBtn.disabled = true; } }); // 监听输入框内容变化,控制发送按钮状态 messageInput.addEventListener('input', () => { sendBtn.disabled = selectedFiles.length === 0 && messageInput.value.trim() === ''; });
内容的提问来源于stack exchange,提问作者riri
相关产品推荐
相关产品推荐

