// js/chatbot/chatbot-image-utils.js /** * ChatbotImageUtils 聊天机器人图片工具集 * * 主要功能: * 1. 管理用户在聊天输入中选择的图片(预览、选择、压缩等)。 * 2. 支持图片选择弹窗、图片压缩、图片预览弹窗等操作。 * 3. 限制图片选择数量,处理图片压缩失败等异常。 */ window.ChatbotImageUtils = { /** * 当前已选中的图片信息数组。 * 每个元素包含 originalSrc、fullBase64、thumbnailBase64。 */ selectedChatbotImages: [], /** * 更新聊天输入区已选图片的预览区域。 * * 主要逻辑: * 1. 获取预览容器元素。 * 2. 清空之前的预览内容。 * 3. 若有已选图片,则依次生成缩略图并展示。 * 4. 若无已选图片,则隐藏预览区域。 */ updateSelectedImagesPreview: function() { const previewContainer = document.getElementById('chatbot-selected-images-preview'); if (!previewContainer) { // console.error('ChatbotImageUtils: chatbot-selected-images-preview element not found.'); return; } previewContainer.innerHTML = ''; // 清空之前的预览 if (this.selectedChatbotImages && this.selectedChatbotImages.length > 0) { previewContainer.style.display = 'flex'; previewContainer.style.flexWrap = 'wrap'; previewContainer.style.gap = '8px'; previewContainer.style.paddingBottom = '8px'; this.selectedChatbotImages.forEach(imgInfo => { const imgElement = document.createElement('img'); imgElement.src = imgInfo.thumbnailBase64 || imgInfo.fullBase64; // 优先使用缩略图 imgElement.alt = 'Selected image'; imgElement.style.width = '50px'; imgElement.style.height = '50px'; imgElement.style.objectFit = 'cover'; imgElement.style.borderRadius = '4px'; previewContainer.appendChild(imgElement); }); } else { previewContainer.style.display = 'none'; } }, /** * 打开图片选择弹窗,允许用户从文档图片中选择。 * * 主要逻辑: * 1. 若弹窗不存在则动态创建。 * 2. 展示所有可选图片,支持点击选择/取消选择。 * 3. 限制最多选择5张图片。 * 4. 选择后自动压缩图片并生成缩略图。 * 5. 选择完成后关闭弹窗并刷新预览。 */ openImageSelectionModal: function() { let modal = document.getElementById('chatbot-image-selection-modal'); if (!modal) { modal = document.createElement('div'); modal.id = 'chatbot-image-selection-modal'; modal.style.position = 'fixed'; modal.style.top = '0'; modal.style.left = '0'; modal.style.width = '100vw'; modal.style.height = '100vh'; modal.style.backgroundColor = 'rgba(0,0,0,0.5)'; modal.style.zIndex = '100002'; modal.style.display = 'flex'; modal.style.alignItems = 'center'; modal.style.justifyContent = 'center'; // 点击遮罩关闭弹窗 modal.onclick = function(e) { if (e.target === modal) modal.style.display = 'none'; }; const contentDiv = document.createElement('div'); contentDiv.style.background = 'white'; contentDiv.style.padding = '20px'; contentDiv.style.borderRadius = '8px'; contentDiv.style.maxWidth = '80vw'; contentDiv.style.maxHeight = '80vh'; contentDiv.style.overflowY = 'auto'; // 阻止内容区点击冒泡到遮罩 contentDiv.onclick = function(e) { e.stopPropagation(); }; const title = document.createElement('h3'); title.textContent = '选择要添加到消息的图片'; title.style.marginTop = '0'; contentDiv.appendChild(title); const imageGrid = document.createElement('div'); imageGrid.id = 'chatbot-doc-image-grid'; imageGrid.style.display = 'grid'; imageGrid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(120px, 1fr))'; imageGrid.style.gap = '10px'; imageGrid.style.marginTop = '15px'; contentDiv.appendChild(imageGrid); const footer = document.createElement('div'); footer.style.marginTop = '20px'; footer.style.textAlign = 'right'; const closeBtn = document.createElement('button'); closeBtn.textContent = '完成选择'; closeBtn.style.padding = '8px 16px'; closeBtn.style.border = 'none'; closeBtn.style.background = '#3b82f6'; closeBtn.style.color = 'white'; closeBtn.style.borderRadius = '6px'; closeBtn.style.cursor = 'pointer'; const self = this; // 用于回调中访问this closeBtn.onclick = function() { modal.style.display = 'none'; self.updateSelectedImagesPreview(); }; footer.appendChild(closeBtn); contentDiv.appendChild(footer); modal.appendChild(contentDiv); document.body.appendChild(modal); } const imageGrid = modal.querySelector('#chatbot-doc-image-grid'); imageGrid.innerHTML = ''; // 清空之前的图片 // 获取文档图片数据 const docImages = (window.data && window.data.images) ? window.data.images : []; if (docImages.length === 0) { imageGrid.innerHTML = '
当前文档没有图片可供选择。
'; } else { const self = this; // 用于异步回调 docImages.forEach((imgData, index) => { const imgContainer = document.createElement('div'); imgContainer.style.position = 'relative'; imgContainer.style.border = '2px solid transparent'; imgContainer.style.borderRadius = '6px'; imgContainer.style.cursor = 'pointer'; imgContainer.style.transition = 'border-color 0.2s'; // 判断当前图片是否已被选中 const isSelected = self.selectedChatbotImages.some(sImg => sImg.originalSrc === (imgData.name || `doc-img-${index}`)); if (isSelected) { imgContainer.style.borderColor = '#3b82f6'; } const imgElement = document.createElement('img'); let imgSrc = ''; if(imgData.data && imgData.data.startsWith('data:image')) { imgSrc = imgData.data; } else if (imgData.data) { imgSrc = 'data:image/png;base64,' + imgData.data; } imgElement.src = imgSrc; imgElement.style.width = '100%'; imgElement.style.height = 'auto'; imgElement.style.maxHeight = '120px'; imgElement.style.objectFit = 'contain'; imgElement.style.display = 'block'; imgElement.style.borderRadius = '4px'; imgContainer.appendChild(imgElement); // 压缩参数 const MAX_IMAGE_SIZE_BYTES = 1 * 1024 * 1024; // 1MB const MAX_THUMBNAIL_SIZE_BYTES = 60 * 1024; // 60KB const MAX_DIMENSION = 1024; const THUMB_DIMENSION = 200; // 点击图片选择/取消选择 imgContainer.onclick = async function() { const originalSrcIdentifier = imgData.name || `doc-img-${index}`; const selectedIndex = self.selectedChatbotImages.findIndex(sImg => sImg.originalSrc === originalSrcIdentifier); if (selectedIndex > -1) { // 已选中则取消选择 self.selectedChatbotImages.splice(selectedIndex, 1); imgContainer.style.borderColor = 'transparent'; } else { // 限制最多选择5张 if (self.selectedChatbotImages.length >= 5) { if (typeof ChatbotUtils !== 'undefined' && ChatbotUtils.showToast) { ChatbotUtils.showToast('最多选择 5 张图片。'); } else { alert('最多选择 5 张图片。'); } return; } imgContainer.style.borderColor = '#3b82f6'; try { // 压缩原图和缩略图 const fullBase64 = await self.compressImage(imgSrc, MAX_IMAGE_SIZE_BYTES, MAX_DIMENSION, 0.85); const thumbnailBase64 = await self.compressImage(imgSrc, MAX_THUMBNAIL_SIZE_BYTES, THUMB_DIMENSION, 0.7); self.selectedChatbotImages.push({ originalSrc: originalSrcIdentifier, fullBase64: fullBase64, thumbnailBase64: thumbnailBase64, }); } catch (error) { if (typeof ChatbotUtils !== 'undefined' && ChatbotUtils.showToast) { ChatbotUtils.showToast('图片处理失败: ' + error.message); } else { alert('图片处理失败: ' + error.message); } imgContainer.style.borderColor = 'transparent'; } } }; imageGrid.appendChild(imgContainer); }); } modal.style.display = 'flex'; }, /** * 压缩图片到目标大小和尺寸。 * * 主要逻辑: * 1. 加载图片并按最大宽高等比缩放。 * 2. 通过 canvas 反复调整压缩质量,直到文件大小不超过目标值或达到最小质量。 * 3. 返回压缩后的 base64 数据。 * * @param {string} base64Src - base64 编码的图片数据。 * @param {number} targetSizeBytes - 目标文件大小(字节)。 * @param {number} maxDimension - 最大宽/高。 * @param {number} initialQuality - 初始压缩质量(0-1)。 * @returns {Promise