// OpenRouter Image Generator - 主要JavaScript逻辑 // 全局变量 let uploadedImages = []; let chatHistory = []; let generatedImages = []; let currentSettings = { apiKey: '', baseUrl: 'https://openrouter.ai/api/v1', model: 'google/gemini-2.5-flash-image-preview:free', timeout: 600, proxy: '' }; let currentImageIndex = 0; // 当前查看的图像索引 let currentModalInstance = null; // 当前模态框实例 // 配置常量 const CONFIG = { MAX_FILE_SIZE: 10485760, // 10MB MAX_FILES_PER_UPLOAD: 10, // 限制为最多上传10张图片 SUPPORTED_IMAGE_FORMATS: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], MAX_CHAT_HISTORY: 50, // 减少聊天历史记录数量以节省存储空间 MAX_GENERATED_IMAGES: 600, // 减少存储的图像数量以防止存储空间溢出 STORAGE_KEYS: { SETTINGS: 'openRouterSettings', CHAT_HISTORY: 'openRouterChatHistory', GENERATED_IMAGES: 'openRouterGeneratedImages' }, INDEXED_DB: { NAME: 'OpenRouterImageDB', VERSION: 1, STORES: { SETTINGS: 'settings', CHAT_HISTORY: 'chatHistory', GENERATED_IMAGES: 'generatedImages' } } }; // 错误消息 const ERROR_MESSAGES = { NO_API_KEY: '请先输入API Key', CONNECTION_FAILED: '连接失败,请检查网络和API设置', INVALID_RESPONSE: 'API响应格式错误', IMAGE_GENERATION_FAILED: '图像生成失败', FILE_TOO_LARGE: '文件大小超过限制', UNSUPPORTED_FORMAT: '不支持的文件格式', UPLOAD_FAILED: '文件上传失败' }; // 工具函数 const utils = { // 将base64转换为Blob URL base64ToBlobUrl: function(base64Data) { try { // 检查是否已经是Blob URL if (base64Data.startsWith('blob:')) { return base64Data; } // 检查是否是base64数据格式 if (!base64Data.startsWith('data:image/')) { console.warn('Not a valid base64 image format, returning original data'); return base64Data; } // 提取base64数据的MIME类型和纯数据部分 const parts = base64Data.split(';base64,'); if (parts.length !== 2) { console.warn('Invalid base64 format, returning original data'); return base64Data; } const contentType = parts[0].split(':')[1]; const byteCharacters = atob(parts[1]); // 转换为ArrayBuffer const byteNumbers = new Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); // 创建Blob和URL const blob = new Blob([byteArray], { type: contentType }); const blobUrl = URL.createObjectURL(blob); // 存储原始base64数据以便后续使用(如下载) blobUrl._originalBase64 = base64Data; return blobUrl; } catch (error) { console.error('Error converting base64 to Blob URL:', error); return base64Data; // 出错时返回原始数据 } }, // 格式化文件大小 formatFileSize: function(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }, // 显示通知 showNotification: function(message, type = 'info') { const notification = document.createElement('div'); notification.className = `alert alert-${type} alert-dismissible fade show position-fixed`; notification.style.cssText = 'top: 20px; right: 20px; z-index: 9999; max-width: 300px;'; notification.innerHTML = ` ${message} `; document.body.appendChild(notification); // 自动移除 setTimeout(() => { if (notification.parentNode) { notification.parentNode.removeChild(notification); } }, 5000); }, // 验证文件 validateFile: function(file) { if (file.size > CONFIG.MAX_FILE_SIZE) { throw new Error(ERROR_MESSAGES.FILE_TOO_LARGE); } if (!CONFIG.SUPPORTED_IMAGE_FORMATS.includes(file.type)) { throw new Error(ERROR_MESSAGES.UNSUPPORTED_FORMAT); } return true; }, // 防抖函数 debounce: function(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }, // 检查和清理存储空间 checkAndCleanStorage: async function() { if (!indexedDBStorage.isSupported) { console.warn('IndexedDB 不支持,无法进行存储检查'); return; } try { // 检查 IndexedDB 存储并清理过期数据 await indexedDBStorage.cleanupOldMessages(); await indexedDBStorage.cleanupOldImages(); //console.log('IndexedDB 存储空间检查和清理完成'); } catch (error) { console.error('存储空间检查失败:', error); } }, // 检查存储状态 - 调试用 checkStorageStatus: async function() { //console.log('=== 存储状态检查 ==='); //console.log('IndexedDB 支持:', indexedDBStorage.isSupported); //console.log('IndexedDB 数据库:', indexedDBStorage.db); if (indexedDBStorage.isSupported) { try { const settings = await indexedDBStorage.getSettings(); const chatHistory = await indexedDBStorage.getChatHistory(); const images = await indexedDBStorage.getGeneratedImages(); //console.log('IndexedDB 数据统计:'); //console.log(`- 设置: ${settings ? '已保存' : '未保存'}`); //console.log(`- 聊天记录: ${chatHistory.length} 条`); //console.log(`- 生成图像: ${images.length} 个`); chatHistory.forEach((msg, idx) => { //console.log(` 聊天 ${idx}: ${msg.role} - ${msg.content.substring(0, 50)}...`); }); images.forEach((img, idx) => { //console.log(` 图像 ${idx}: URL长度 ${img.url ? img.url.length : 0}`); }); } catch (error) { console.error('读取 IndexedDB 数据失败:', error); } } }, // 检查事件绑定 - 调试用 checkEventBindings: function() { //console.log('=== 事件绑定检查 ==='); const containers = document.querySelectorAll('.image-clickable-container'); //console.log(`找到 ${containers.length} 个可点击图像容器`); containers.forEach((container, idx) => { const img = container.querySelector('img'); const tooltip = container.querySelector('.image-preview-tooltip'); // console.log(`容器 ${idx}:`, { // src: img ? img.src.substring(0, 50) + '...' : '无图片', // hasTooltip: !!tooltip, // tooltipText: tooltip ? tooltip.textContent : '无', // classList: container.classList.toString() // }); }); // 测试点击第一个容器 if (containers.length > 0) { //console.log('尝试点击第一个图像容器...'); containers[0].click(); } } }; // IndexedDB存储管理模块 const indexedDBStorage = { db: null, initPromise: null, isSupported: false, // 初始化数据库 init: function() { if (this.initPromise) { return this.initPromise; } this.initPromise = new Promise((resolve, reject) => { if (!window.indexedDB) { console.warn('IndexedDB 不支持,将使用 localStorage 作为后备'); this.db = null; this.isSupported = false; resolve(false); return; } const request = indexedDB.open(CONFIG.INDEXED_DB.NAME, CONFIG.INDEXED_DB.VERSION); request.onerror = (event) => { console.error('IndexedDB 初始化失败:', event.target.error); this.db = null; this.isSupported = false; resolve(false); }; request.onsuccess = (event) => { this.db = event.target.result; this.isSupported = true; //console.log('IndexedDB 初始化成功'); resolve(true); }; request.onupgradeneeded = (event) => { const db = event.target.result; //console.log('正在创建/升级数据库结构...'); // 创建设置存储 if (!db.objectStoreNames.contains(CONFIG.INDEXED_DB.STORES.SETTINGS)) { const settingsStore = db.createObjectStore(CONFIG.INDEXED_DB.STORES.SETTINGS, { keyPath: 'id' }); settingsStore.createIndex('timestamp', 'timestamp', { unique: false }); } // 创建聊天历史存储 if (!db.objectStoreNames.contains(CONFIG.INDEXED_DB.STORES.CHAT_HISTORY)) { const chatStore = db.createObjectStore(CONFIG.INDEXED_DB.STORES.CHAT_HISTORY, { keyPath: 'id', autoIncrement: true }); chatStore.createIndex('timestamp', 'timestamp', { unique: false }); chatStore.createIndex('role', 'role', { unique: false }); } // 创建图像记录存储 if (!db.objectStoreNames.contains(CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES)) { const imagesStore = db.createObjectStore(CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES, { keyPath: 'id', autoIncrement: true }); imagesStore.createIndex('timestamp', 'timestamp', { unique: false }); imagesStore.createIndex('url', 'url', { unique: false }); } }; }); return this.initPromise; }, // 保存设置 saveSettings: async function(settings) { if (!this.isSupported) { return false; } try { const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.SETTINGS], 'readwrite'); const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.SETTINGS); const data = { id: 'current', ...settings, timestamp: Date.now() }; await new Promise((resolve, reject) => { const request = store.put(data); request.onsuccess = () => resolve(true); request.onerror = () => reject(request.error); }); return true; } catch (error) { console.error('IndexedDB 保存设置失败:', error); return false; } }, // 获取设置 getSettings: async function() { if (!this.isSupported) { return null; } try { const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.SETTINGS], 'readonly'); const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.SETTINGS); return new Promise((resolve, reject) => { const request = store.get('current'); request.onsuccess = () => { const result = request.result; if (result) { // 移除 id 和 timestamp 字段 const { id, timestamp, ...settings } = result; resolve(settings); } else { resolve(null); } }; request.onerror = () => reject(request.error); }); } catch (error) { console.error('IndexedDB 获取设置失败:', error); return null; } }, // 添加聊天消息 addChatMessage: async function(role, content) { if (!this.isSupported) { return false; } try { const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.CHAT_HISTORY], 'readwrite'); const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.CHAT_HISTORY); const message = { role: role, content: content, timestamp: Date.now() }; await new Promise((resolve, reject) => { const request = store.add(message); request.onsuccess = () => resolve(true); request.onerror = () => reject(request.error); }); // 清理旧消息 await this.cleanupOldMessages(); return true; } catch (error) { console.error('IndexedDB 添加聊天消息失败:', error); return false; } }, // 获取聊天历史 getChatHistory: async function() { if (!this.isSupported) { return []; } try { const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.CHAT_HISTORY], 'readonly'); const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.CHAT_HISTORY); const index = store.index('timestamp'); return new Promise((resolve, reject) => { const request = index.getAll(); request.onsuccess = () => { const messages = request.result || []; // 按时间戳排序 messages.sort((a, b) => a.timestamp - b.timestamp); resolve(messages); }; request.onerror = () => reject(request.error); }); } catch (error) { console.error('IndexedDB 获取聊天历史失败:', error); return []; } }, // 添加生成的图像 addGeneratedImage: async function(imageId, imageUrl) { if (!this.isSupported) { return false; } try { // 首先处理图像URL,获取要存储的数据 let storedUrl = imageUrl; if (imageUrl.startsWith('blob:') && imageUrl._originalBase64) { storedUrl = imageUrl._originalBase64; console.log('Storing original base64 data for image'); } else if (imageUrl.startsWith('data:image/')) { // 已经是base64格式,直接存储 console.log('Image is already in base64 format, storing directly'); } else if (imageUrl.startsWith('blob:')) { // Blob URL没有原始base64数据,需要从Blob URL中提取base64数据 console.warn('Blob URL has no original base64 data, attempting to extract base64 data from Blob'); try { // 通过fetch获取Blob数据,然后转换为base64 const response = await fetch(imageUrl); const blob = await response.blob(); // 将Blob转换为base64 const reader = new FileReader(); const base64Promise = new Promise((resolve, reject) => { reader.onload = () => resolve(reader.result); reader.onerror = reject; }); reader.readAsDataURL(blob); storedUrl = await base64Promise; console.log('Successfully extracted base64 data from Blob URL'); } catch (error) { console.error('Failed to extract base64 data from Blob URL:', error); // 如果转换失败,仍然存储原始URL,但记录警告 console.warn('Storing original Blob URL, which may cause issues after page reload'); } } // 现在创建事务并存储数据 const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES], 'readwrite'); const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES); const image = { imageId: imageId, url: storedUrl, timestamp: Date.now() }; await new Promise((resolve, reject) => { const request = store.add(image); request.onsuccess = () => resolve(true); request.onerror = () => reject(request.error); }); // 清理旧图像 await this.cleanupOldImages(); return true; } catch (error) { console.error('IndexedDB 添加图像记录失败:', error); return false; } }, // 获取生成的图像 getGeneratedImages: async function() { if (!this.isSupported) { return []; } try { const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES], 'readonly'); const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES); const index = store.index('timestamp'); return new Promise((resolve, reject) => { const request = index.getAll(); request.onsuccess = () => { const images = request.result || []; // 按时间戳倒序排序(最新的在前) images.sort((a, b) => b.timestamp - a.timestamp); resolve(images.map(img => ({ id: img.imageId, url: img.url }))); }; request.onerror = () => reject(request.error); }); } catch (error) { console.error('IndexedDB 获取图像记录失败:', error); return []; } }, // 清理旧聊天消息 cleanupOldMessages: async function() { try { const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.CHAT_HISTORY], 'readwrite'); const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.CHAT_HISTORY); const countRequest = store.count(); countRequest.onsuccess = () => { const count = countRequest.result; if (count > CONFIG.MAX_CHAT_HISTORY) { const index = store.index('timestamp'); const request = index.openCursor(); let deleteCount = count - CONFIG.MAX_CHAT_HISTORY; request.onsuccess = (event) => { const cursor = event.target.result; if (cursor && deleteCount > 0) { cursor.delete(); deleteCount--; cursor.continue(); } }; } }; } catch (error) { console.error('清理旧聊天消息失败:', error); } }, // 清理旧图像记录 cleanupOldImages: async function() { try { const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES], 'readwrite'); const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES); const countRequest = store.count(); countRequest.onsuccess = () => { const count = countRequest.result; if (count > CONFIG.MAX_GENERATED_IMAGES) { const index = store.index('timestamp'); const request = index.openCursor(); let deleteCount = count - CONFIG.MAX_GENERATED_IMAGES; request.onsuccess = (event) => { const cursor = event.target.result; if (cursor && deleteCount > 0) { cursor.delete(); deleteCount--; cursor.continue(); } }; } }; } catch (error) { console.error('清理旧图像记录失败:', error); } }, // 清除所有数据 clearAllData: async function() { if (!this.isSupported) { return false; } try { const transaction = this.db.transaction([ CONFIG.INDEXED_DB.STORES.SETTINGS, CONFIG.INDEXED_DB.STORES.CHAT_HISTORY, CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES ], 'readwrite'); const promises = [ new Promise((resolve) => { const request = transaction.objectStore(CONFIG.INDEXED_DB.STORES.SETTINGS).clear(); request.onsuccess = () => resolve(); }), new Promise((resolve) => { const request = transaction.objectStore(CONFIG.INDEXED_DB.STORES.CHAT_HISTORY).clear(); request.onsuccess = () => resolve(); }), new Promise((resolve) => { const request = transaction.objectStore(CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES).clear(); request.onsuccess = () => resolve(); }) ]; await Promise.all(promises); return true; } catch (error) { console.error('IndexedDB 清除所有数据失败:', error); return false; } }, // 数据迁移:从localStorage到IndexedDB migrateFromLocalStorage: async function() { if (!this.isSupported) { return false; } try { //console.log('开始从 localStorage 迁移数据到 IndexedDB...'); // 迁移设置 const savedSettings = localStorage.getItem(CONFIG.STORAGE_KEYS.SETTINGS); if (savedSettings) { const settings = JSON.parse(savedSettings); await this.saveSettings(settings); //console.log('设置已迁移到 IndexedDB'); } // 迁移聊天历史 const savedChatHistory = localStorage.getItem(CONFIG.STORAGE_KEYS.CHAT_HISTORY); if (savedChatHistory) { const messages = JSON.parse(savedChatHistory); for (const msg of messages) { await this.addChatMessage(msg.role, msg.content); } //console.log(`已迁移 ${messages.length} 条聊天记录到 IndexedDB`); } // 迁移图像记录 const savedImages = localStorage.getItem(CONFIG.STORAGE_KEYS.GENERATED_IMAGES); if (savedImages) { const images = JSON.parse(savedImages); for (const img of images) { await this.addGeneratedImage(img.id, img.url); } //console.log(`已迁移 ${images.length} 条图像记录到 IndexedDB`); } //console.log('数据迁移完成'); return true; } catch (error) { console.error('数据迁移失败:', error); return false; } }, // 删除生成的图像记录 deleteGeneratedImage: async function(imageId) { if (!this.isSupported) { return false; } try { const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES], 'readwrite'); const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES); return new Promise((resolve, reject) => { // 遍历所有记录查找匹配的imageId const request = store.openCursor(); request.onsuccess = (event) => { const cursor = event.target.result; if (cursor) { const record = cursor.value; if (record.imageId === imageId) { // 找到匹配的记录,删除它 const deleteRequest = cursor.delete(); deleteRequest.onsuccess = () => resolve(true); deleteRequest.onerror = () => reject(deleteRequest.error); } else { // 继续查找下一条记录 cursor.continue(); } } else { // 遍历完成但没有找到记录 resolve(false); } }; request.onerror = () => reject(request.error); }); } catch (error) { console.error('IndexedDB 删除图像记录失败:', error); return false; } } }; // API服务 const apiService = { // 测试连接 testConnection: async function(apiKey, baseUrl) { try { const response = await fetch(`${baseUrl}/models`, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); if (response.ok) { return { success: true, data: await response.json() }; } else { return { success: false, error: response.statusText }; } } catch (error) { return { success: false, error: error.message }; } }, // 生成图像 generateImage: async function(message, images, settings) { const messages = [ { role: 'user', content: [ { type: 'text', text: message } ] } ]; // 添加上传的图像 images.forEach(img => { messages[0].content.push({ type: 'image_url', image_url: { url: img.data } }); }); const payload = { model: settings.model, messages: messages, stream: false, "modalities": ["image","text"] }; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), settings.timeout * 1000); try { const response = await fetch(`${settings.baseUrl}/chat/completions`, { method: 'POST', headers: { 'Authorization': `Bearer ${settings.apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify(payload), signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); // 解析响应 const choice = data.choices[0]; const messageContent = choice.message.content; const images = []; // 提取图像数据并转换为Blob URL,但保留原始base64数据 if (choice.message.images) { choice.message.images.forEach(img => { // 确保原始数据是base64格式 const originalBase64 = img.image_url.url; const blobUrl = utils.base64ToBlobUrl(originalBase64); images.push(blobUrl); }); } return { success: true, content: messageContent, images: images, usage: data.usage }; } catch (error) { clearTimeout(timeoutId); throw error; } } }; // UI控制器 const uiController = { // 更新连接状态 updateConnectionStatus: function(connected) { const statusIndicator = document.getElementById('connectionStatus'); const connectionText = document.getElementById('connectionText'); if (connected) { statusIndicator.className = 'status-indicator status-connected'; connectionText.textContent = '已连接'; } else { statusIndicator.className = 'status-indicator status-disconnected'; connectionText.textContent = '未连接'; } }, // 显示加载状态 showLoading: function(show) { const loadingSpinner = document.getElementById('loadingSpinner'); loadingSpinner.style.display = show ? 'block' : 'none'; }, // 隐藏传统加载状态,用于批量生成模式 hideTraditionalLoading: function() { const loadingSpinner = document.getElementById('loadingSpinner'); loadingSpinner.style.display = 'none'; }, // 添加聊天消息 addChatMessage: async function(role, content) { const chatHistoryDiv = document.getElementById('chatHistory'); const messageDiv = document.createElement('div'); messageDiv.className = `chat-message ${role} fade-in`; const timestamp = new Date().toLocaleTimeString(); const messageId = `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // 为用户消息添加操作按钮 const actionButtons = role === 'user' ? `
` : ''; messageDiv.innerHTML = `
${role === 'user' ? '用户' : '助手'} ${timestamp}
${actionButtons}
${this.escapeHtml(content)}
`; chatHistoryDiv.appendChild(messageDiv); chatHistoryDiv.scrollTop = chatHistoryDiv.scrollHeight; // 添加到内存中的聊天历史 chatHistory.push({ role: role, content: content }); // 保存到存储 - 优先使用 IndexedDB await this.saveChatHistory(role, content); }, // 转义HTML escapeHtml: function(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }, // 添加生成的图像 addGeneratedImage: async function(imageUrl) { const gallery = document.getElementById('imageGallery'); const imageDiv = document.createElement('div'); imageDiv.className = 'image-item fade-in'; const imageId = Date.now() + Math.random(); // 确保imageUrl是Blob URL用于显示,但保存原始URL用于存储 let displayUrl; let originalUrl = imageUrl; // 原始URL,可能是base64或Blob URL if (imageUrl.startsWith('blob:')) { displayUrl = imageUrl; } else if (imageUrl.startsWith('data:image/')) { // 这是base64数据,转换为Blob URL displayUrl = utils.base64ToBlobUrl(imageUrl); } else { // 其他格式,直接使用 displayUrl = imageUrl; } // 确保Blob URL有原始base64数据 if (displayUrl.startsWith('blob:') && !displayUrl._originalBase64 && originalUrl.startsWith('data:image/')) { displayUrl._originalBase64 = originalUrl; } imageDiv.innerHTML = ` Generated Image
`; gallery.insertBefore(imageDiv, gallery.firstChild); // 添加事件监听器 const viewLargeBtn = imageDiv.querySelector('.view-large-btn'); const downloadBtn = imageDiv.querySelector('.download-btn'); const copyBtn = imageDiv.querySelector('.copy-url-btn'); const removeBtn = imageDiv.querySelector('.remove-btn'); // 添加移动端触摸交互 this.addMobileTouchInteraction(imageDiv); if (viewLargeBtn) { viewLargeBtn.addEventListener('click', (e) => { e.stopPropagation(); this.viewLargeImage(imageUrl); }); } if (downloadBtn) { downloadBtn.addEventListener('click', (e) => { e.stopPropagation(); app.downloadImage(imageUrl); }); } if (copyBtn) { copyBtn.addEventListener('click', (e) => { e.stopPropagation(); app.copyImageUrl(imageUrl); }); } if (removeBtn) { removeBtn.addEventListener('click', async (e) => { e.stopPropagation(); await app.removeGeneratedImage(imageId); }); } // 添加到内存中的图像记录(插入到最前面) generatedImages.unshift({ id: imageId, url: imageUrl }); // 显示"全部下载"按钮 this.updateDownloadAllButtonVisibility(); // 保存到存储 - 优先使用 IndexedDB await this.saveGeneratedImages(imageId, imageUrl); }, // 更新"全部下载"按钮的可见性 updateDownloadAllButtonVisibility: function() { const downloadAllBtn = document.getElementById('downloadAllImagesBtn'); if (downloadAllBtn) { downloadAllBtn.style.display = generatedImages.length > 0 ? 'block' : 'none'; } }, // 添加占位图像 addImagePlaceholder: function(index, total) { const gallery = document.getElementById('imageGallery'); const placeholderDiv = document.createElement('div'); placeholderDiv.className = 'image-placeholder fade-in'; placeholderDiv.id = `placeholder-${index}`; placeholderDiv.innerHTML = `
正在生成图像 ${index + 1}/${total}
`; gallery.insertBefore(placeholderDiv, gallery.firstChild); return placeholderDiv; }, // 替换占位图像为实际图像 replacePlaceholderWithImage: function(placeholderId, imageUrl) { const placeholder = document.getElementById(placeholderId); if (!placeholder) return; const imageId = Date.now() + Math.random(); placeholder.className = 'image-item fade-in'; placeholder.id = ''; // 确保imageUrl是Blob URL用于显示,但保存原始URL用于存储 let displayUrl; let originalUrl = imageUrl; // 原始URL,可能是base64或Blob URL if (imageUrl.startsWith('blob:')) { displayUrl = imageUrl; } else if (imageUrl.startsWith('data:image/')) { // 这是base64数据,转换为Blob URL displayUrl = utils.base64ToBlobUrl(imageUrl); } else { // 其他格式,直接使用 displayUrl = imageUrl; } // 确保Blob URL有原始base64数据 if (displayUrl.startsWith('blob:') && !displayUrl._originalBase64 && originalUrl.startsWith('data:image/')) { displayUrl._originalBase64 = originalUrl; } placeholder.innerHTML = ` Generated Image
`; // 添加事件监听器 const viewLargeBtn = placeholder.querySelector('.view-large-btn'); const downloadBtn = placeholder.querySelector('.download-btn'); const copyBtn = placeholder.querySelector('.copy-url-btn'); const removeBtn = placeholder.querySelector('.remove-btn'); // 添加移动端触摸交互 this.addMobileTouchInteraction(placeholder); if (viewLargeBtn) { viewLargeBtn.addEventListener('click', (e) => { e.stopPropagation(); this.viewLargeImage(imageUrl); }); } if (downloadBtn) { downloadBtn.addEventListener('click', (e) => { e.stopPropagation(); app.downloadImage(imageUrl); }); } if (copyBtn) { copyBtn.addEventListener('click', (e) => { e.stopPropagation(); app.copyImageUrl(imageUrl); }); } if (removeBtn) { removeBtn.addEventListener('click', async (e) => { e.stopPropagation(); await app.removeGeneratedImage(imageId); }); } // 添加到内存中的图像记录(插入到最前面) generatedImages.unshift({ id: imageId, url: imageUrl }); // 显示"全部下载"按钮 this.updateDownloadAllButtonVisibility(); // 保存到存储 - 优先使用 IndexedDB this.saveGeneratedImages(imageId, imageUrl); }, // 显示批量生成状态 showBatchStatus: function(message, show = true) { let statusDiv = document.getElementById('batchStatus'); if (!statusDiv) { statusDiv = document.createElement('div'); statusDiv.id = 'batchStatus'; statusDiv.className = 'batch-status'; document.body.appendChild(statusDiv); } statusDiv.textContent = message; if (show) { statusDiv.classList.add('show'); } else { statusDiv.classList.remove('show'); } }, // 新的查看大图功能 viewLargeImage: async function(imageUrl) { try { const modalElement = document.getElementById('imageViewerModal'); if (!modalElement) { utils.showNotification('无法打开图像查看:找不到模态框元素', 'danger'); return; } // 检查 Bootstrap 是否可用 if (typeof bootstrap === 'undefined') { utils.showNotification('无法打开图像查看:Bootstrap 未加载', 'danger'); return; } const modalImage = document.getElementById('viewerModalImage'); const downloadButton = document.getElementById('viewerDownloadImage'); const deleteButton = document.getElementById('viewerDeleteImage'); const prevButton = document.getElementById('prevImageBtn'); const nextButton = document.getElementById('nextImageBtn'); if (!modalImage) { utils.showNotification('无法打开图像查看:找不到图像元素', 'danger'); return; } // 找到当前图像在generatedImages中的索引 currentImageIndex = generatedImages.findIndex(img => img.url === imageUrl); // 设置图像源 - 确保正确处理base64和Blob URL let displayUrl; if (imageUrl.startsWith('blob:')) { displayUrl = imageUrl; } else if (imageUrl.startsWith('data:image/')) { // 这是base64数据,转换为Blob URL displayUrl = utils.base64ToBlobUrl(imageUrl); } else { // 其他格式,直接使用 displayUrl = imageUrl; } modalImage.src = displayUrl; modalImage.onerror = function() { utils.showNotification('图像加载失败', 'danger'); }; // 设置下载按钮功能 if (downloadButton) { downloadButton.onclick = function() { app.downloadImage(generatedImages[currentImageIndex].url); }; } // 设置删除按钮功能 if (deleteButton) { deleteButton.onclick = async () => { const currentImageId = generatedImages[currentImageIndex].id; await app.removeGeneratedImage(currentImageId); if (generatedImages.length === 0) { currentModalInstance.hide(); } else { // 调整索引 if (currentImageIndex >= generatedImages.length) { currentImageIndex = generatedImages.length - 1; } // 重新加载当前图像 if (currentImageIndex >= 0 && currentImageIndex < generatedImages.length) { const displayUrl = generatedImages[currentImageIndex].url.startsWith('blob:') ? generatedImages[currentImageIndex].url : utils.base64ToBlobUrl(generatedImages[currentImageIndex].url); modalImage.src = displayUrl; } this.updateNavigationButtons(); } }; } // 设置翻页按钮功能 if (prevButton) { prevButton.onclick = () => { this.navigateToImage(-1); }; } if (nextButton) { nextButton.onclick = () => { this.navigateToImage(1); }; } // 更新翻页按钮状态 this.updateNavigationButtons(); // 添加键盘事件监听器 this.addModalKeyboardListeners(); // 添加触摸事件监听器 this.addModalTouchListeners(modalImage); // 创建并显示模态框 currentModalInstance = new bootstrap.Modal(modalElement); // 添加模态框关闭事件监听器 modalElement.addEventListener('hidden.bs.modal', () => { this.removeModalKeyboardListeners(); this.removeModalTouchListeners(modalImage); }); currentModalInstance.show(); } catch (error) { utils.showNotification(`无法打开图像查看: ${error.message}`, 'danger'); // 备用方案:在新窗口中打开图像 try { window.open(imageUrl, '_blank'); } catch (e) { // 静默处理错误 } } }, // 更新翻页按钮状态 updateNavigationButtons: function() { const prevButton = document.getElementById('prevImageBtn'); const nextButton = document.getElementById('nextImageBtn'); // 如果有多张图片,始终显示翻页按钮(因为支持循环导航) if (prevButton) { prevButton.style.display = generatedImages.length > 1 ? 'block' : 'none'; } if (nextButton) { nextButton.style.display = generatedImages.length > 1 ? 'block' : 'none'; } }, // 添加模态框键盘事件监听器 addModalKeyboardListeners: function() { // 移除之前的监听器(如果存在) this.removeModalKeyboardListeners(); // 添加键盘事件监听器 this.modalKeyboardHandler = (e) => { if (!currentModalInstance || !document.getElementById('imageViewerModal').classList.contains('show')) { return; } switch(e.key) { case 'ArrowLeft': case 'ArrowUp': e.preventDefault(); this.navigateToImage(-1); break; case 'ArrowRight': case 'ArrowDown': e.preventDefault(); this.navigateToImage(1); break; case 'Escape': e.preventDefault(); currentModalInstance.hide(); break; } }; document.addEventListener('keydown', this.modalKeyboardHandler); }, // 移除模态框键盘事件监听器 removeModalKeyboardListeners: function() { if (this.modalKeyboardHandler) { document.removeEventListener('keydown', this.modalKeyboardHandler); this.modalKeyboardHandler = null; } }, // 添加模态框触摸事件监听器 addModalTouchListeners: function(modalImage) { if (!modalImage) return; // 移除之前的监听器(如果存在) this.removeModalTouchListeners(modalImage); let startX = 0; let startY = 0; let isMoving = false; const minSwipeDistance = 50; // 最小滑动距离 const maxVerticalDistance = 100; // 最大垂直距离,超过则不视为翻页 this.modalTouchStartHandler = (e) => { if (e.touches.length === 1) { startX = e.touches[0].clientX; startY = e.touches[0].clientY; isMoving = false; } }; this.modalTouchMoveHandler = (e) => { if (e.touches.length === 1) { isMoving = true; // 防止默认滚动行为 e.preventDefault(); } }; this.modalTouchEndHandler = (e) => { if (isMoving && e.changedTouches.length === 1) { const endX = e.changedTouches[0].clientX; const endY = e.changedTouches[0].clientY; const deltaX = endX - startX; const deltaY = endY - startY; // 检查是否为有效的水平滑动 if (Math.abs(deltaX) > minSwipeDistance && Math.abs(deltaY) < maxVerticalDistance) { if (deltaX > 0) { // 向右滑动 - 上一张图片 this.navigateToImage(-1); } else { // 向左滑动 - 下一张图片 this.navigateToImage(1); } } } isMoving = false; }; modalImage.addEventListener('touchstart', this.modalTouchStartHandler, { passive: false }); modalImage.addEventListener('touchmove', this.modalTouchMoveHandler, { passive: false }); modalImage.addEventListener('touchend', this.modalTouchEndHandler, { passive: true }); }, // 移除模态框触摸事件监听器 removeModalTouchListeners: function(modalImage) { if (!modalImage) return; if (this.modalTouchStartHandler) { modalImage.removeEventListener('touchstart', this.modalTouchStartHandler); this.modalTouchStartHandler = null; } if (this.modalTouchMoveHandler) { modalImage.removeEventListener('touchmove', this.modalTouchMoveHandler); this.modalTouchMoveHandler = null; } if (this.modalTouchEndHandler) { modalImage.removeEventListener('touchend', this.modalTouchEndHandler); this.modalTouchEndHandler = null; } }, // 导航到指定图片 navigateToImage: function(direction) { if (generatedImages.length <= 1) return; let newIndex = currentImageIndex + direction; // 循环导航 if (newIndex < 0) { newIndex = generatedImages.length - 1; } else if (newIndex >= generatedImages.length) { newIndex = 0; } currentImageIndex = newIndex; // 更新图片 const modalImage = document.getElementById('viewerModalImage'); const deleteButton = document.getElementById('viewerDeleteImage'); const downloadButton = document.getElementById('viewerDownloadImage'); if (modalImage && generatedImages[currentImageIndex]) { // 确保正确处理base64和Blob URL const imageUrl = generatedImages[currentImageIndex].url; let displayUrl; if (imageUrl.startsWith('blob:')) { displayUrl = imageUrl; } else if (imageUrl.startsWith('data:image/')) { // 这是base64数据,转换为Blob URL displayUrl = utils.base64ToBlobUrl(imageUrl); } else { // 其他格式,直接使用 displayUrl = imageUrl; } modalImage.src = displayUrl; // 更新删除按钮对应的图像ID if (deleteButton) { deleteButton.onclick = async function() { const currentImageId = generatedImages[currentImageIndex].id; await app.removeGeneratedImage(currentImageId); if (generatedImages.length === 0) { currentModalInstance.hide(); } else { if (currentImageIndex >= generatedImages.length) { currentImageIndex = generatedImages.length - 1; } if (currentImageIndex >= 0 && currentImageIndex < generatedImages.length) { const imageUrl = generatedImages[currentImageIndex].url; let newDisplayUrl; if (imageUrl.startsWith('blob:')) { newDisplayUrl = imageUrl; } else if (imageUrl.startsWith('data:image/')) { // 这是base64数据,转换为Blob URL newDisplayUrl = utils.base64ToBlobUrl(imageUrl); } else { // 其他格式,直接使用 newDisplayUrl = imageUrl; } modalImage.src = newDisplayUrl; deleteButton.onclick = arguments.callee; } uiController.updateNavigationButtons(); } }; } // 更新下载按钮对应的图像URL if (downloadButton) { downloadButton.onclick = function() { app.downloadImage(generatedImages[currentImageIndex].url); }; } } // 更新导航按钮状态 this.updateNavigationButtons(); }, // 添加移动端触摸交互 addMobileTouchInteraction: function(imageElement) { if (!imageElement) return; let touchTimeout = null; let isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; if (!isTouchDevice) return; // 非触摸设备不需要添加此交互 // 添加触摸开始事件 const touchStartHandler = (e) => { // 清除之前的超时 if (touchTimeout) { clearTimeout(touchTimeout); } // 添加touched类 imageElement.classList.add('touched'); // 设置超时,3秒后自动移除touched类 touchTimeout = setTimeout(() => { imageElement.classList.remove('touched'); }, 3000); }; // 添加点击外部移除touched类的逻辑 const documentClickHandler = (e) => { if (!imageElement.contains(e.target)) { imageElement.classList.remove('touched'); if (touchTimeout) { clearTimeout(touchTimeout); } } }; // 绑定事件 imageElement.addEventListener('touchstart', touchStartHandler, { passive: true }); imageElement.addEventListener('click', touchStartHandler); // 也支持点击 // 存储清理函数,以便后续清理 imageElement._cleanupMobileTouch = () => { imageElement.removeEventListener('touchstart', touchStartHandler); imageElement.removeEventListener('click', touchStartHandler); document.removeEventListener('click', documentClickHandler); if (touchTimeout) { clearTimeout(touchTimeout); } }; // 添加到全局点击监听器(延迟添加避免立即触发) setTimeout(() => { document.addEventListener('click', documentClickHandler); }, 100); }, // 显示图像预览 displayImagePreview: function(imageData) { const preview = document.getElementById('imagePreview'); const placeholderContainer = preview.querySelector('.col-12.d-flex'); if (placeholderContainer) { placeholderContainer.remove(); } // 确保图像数据是Blob URL用于显示,但保持原始数据用于其他操作 let displayUrl; if (imageData.data.startsWith('blob:')) { displayUrl = imageData.data; } else if (imageData.data.startsWith('data:image/')) { // 这是base64数据,转换为Blob URL displayUrl = utils.base64ToBlobUrl(imageData.data); } else { // 其他格式,直接使用 displayUrl = imageData.data; } const wrapper = document.createElement('div'); wrapper.className = 'col-md-4 mb-3'; wrapper.dataset.imageId = imageData.id; // Store imageId here wrapper.innerHTML = `
${imageData.name}
${imageData.name}
${utils.formatFileSize(imageData.size)}
`; preview.appendChild(wrapper); }, // 保存聊天历史 - 新增消息时调用 saveChatHistory: async function(role, content) { if (!indexedDBStorage.isSupported) { console.warn('IndexedDB 不支持,无法保存聊天历史'); return; } try { const saved = await indexedDBStorage.addChatMessage(role, content); if (!saved) { console.error('IndexedDB 保存聊天历史失败'); } } catch (error) { console.error('保存聊天历史失败:', error); } }, // 保存生成的图像 - 新增图像时调用 saveGeneratedImages: async function(imageId, imageUrl) { if (!indexedDBStorage.isSupported) { console.warn('IndexedDB 不支持,无法保存图像记录'); return; } try { const saved = await indexedDBStorage.addGeneratedImage(imageId, imageUrl); if (!saved) { console.error('IndexedDB 保存图像记录失败'); } } catch (error) { console.error('保存生成的图像失败:', error); } }, // 加载设置 loadSettings: async function() { if (!indexedDBStorage.isSupported) { console.warn('IndexedDB 不支持,无法加载设置'); return; } try { // 从IndexedDB加载设置 const indexedSettings = await indexedDBStorage.getSettings(); if (indexedSettings) { currentSettings = { ...currentSettings, ...indexedSettings }; } // 更新UI document.getElementById('apiKey').value = currentSettings.apiKey; document.getElementById('baseUrl').value = currentSettings.baseUrl; document.getElementById('model').value = currentSettings.model; document.getElementById('timeout').value = currentSettings.timeout; document.getElementById('proxy').value = currentSettings.proxy; // 加载聊天历史 const indexedChatHistory = await indexedDBStorage.getChatHistory(); if (indexedChatHistory && indexedChatHistory.length > 0) { chatHistory = indexedChatHistory; } // 重建聊天UI const self = this; chatHistory.forEach(msg => { const chatHistoryDiv = document.getElementById('chatHistory'); const messageDiv = document.createElement('div'); messageDiv.className = `chat-message ${msg.role} fade-in`; const timestamp = new Date(msg.timestamp || Date.now()).toLocaleTimeString(); const messageId = `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // 为用户消息添加操作按钮 const actionButtons = msg.role === 'user' ? `
` : ''; messageDiv.innerHTML = `
${msg.role === 'user' ? '用户' : '助手'} ${timestamp}
${actionButtons}
${self.escapeHtml(msg.content)}
`; chatHistoryDiv.appendChild(messageDiv); }); // 加载生成的图像 const indexedImages = await indexedDBStorage.getGeneratedImages(); if (indexedImages && indexedImages.length > 0) { generatedImages = indexedImages; } // 重建图像UI generatedImages.forEach(img => { const gallery = document.getElementById('imageGallery'); const imageDiv = document.createElement('div'); imageDiv.className = 'image-item fade-in'; // 从IndexedDB加载的数据应该是base64格式,直接使用 // 如果是base64数据,创建一个新的Blob URL用于显示 let displayUrl; if (img.url.startsWith('data:image/')) { // 这是base64数据 displayUrl = utils.base64ToBlobUrl(img.url); } else { // 这可能是其他格式的URL,直接使用 displayUrl = img.url; } imageDiv.innerHTML = ` Generated Image
`; gallery.appendChild(imageDiv); // 由于数据库查询已经是倒序,这里用appendChild保持顺序 // 添加事件监听器 const viewLargeBtn = imageDiv.querySelector('.view-large-btn'); const downloadBtn = imageDiv.querySelector('.download-btn'); const copyBtn = imageDiv.querySelector('.copy-url-btn'); const removeBtn = imageDiv.querySelector('.remove-btn'); // 添加移动端触摸交互 this.addMobileTouchInteraction(imageDiv); if (viewLargeBtn) { viewLargeBtn.addEventListener('click', (e) => { e.stopPropagation(); uiController.viewLargeImage(img.url); }); } if (downloadBtn) { downloadBtn.addEventListener('click', (e) => { e.stopPropagation(); app.downloadImage(img.url); }); } if (copyBtn) { copyBtn.addEventListener('click', (e) => { e.stopPropagation(); app.copyImageUrl(img.url); }); } if (removeBtn) { removeBtn.addEventListener('click', async (e) => { e.stopPropagation(); await app.removeGeneratedImage(img.id); }); } }); } catch (error) { console.error('加载设置失败:', error); } }, // 移除生成的图像 removeGeneratedImage: async function(imageId) { // 从内存中移除 generatedImages = generatedImages.filter(img => img.id !== imageId); // 从DOM中移除 const gallery = document.getElementById('imageGallery'); const imageElements = gallery.querySelectorAll('.image-item'); imageElements.forEach(element => { const removeBtn = element.querySelector('.remove-btn'); if (removeBtn && removeBtn.dataset.imageId == imageId) { // 清理移动端触摸事件监听器 if (element._cleanupMobileTouch) { element._cleanupMobileTouch(); } element.remove(); } }); // 从IndexedDB中移除 if (indexedDBStorage.isSupported) { try { await indexedDBStorage.deleteGeneratedImage(imageId); } catch (error) { console.error('从IndexedDB删除图像记录失败:', error); } } // 更新"全部下载"按钮的可见性 this.updateDownloadAllButtonVisibility(); }, // 保存设置 saveSettings: async function() { if (!indexedDBStorage.isSupported) { console.warn('IndexedDB 不支持,无法保存设置'); return; } try { currentSettings.apiKey = document.getElementById('apiKey').value; currentSettings.baseUrl = document.getElementById('baseUrl').value; currentSettings.model = document.getElementById('model').value; currentSettings.timeout = parseInt(document.getElementById('timeout').value); currentSettings.proxy = document.getElementById('proxy').value; const saved = await indexedDBStorage.saveSettings(currentSettings); if (!saved) { console.error('IndexedDB 保存设置失败'); } } catch (error) { console.error('保存设置失败:', error); } } }; // 文件处理 const fileHandler = { // 处理拖拽 handleDragOver: function(e) { e.preventDefault(); e.currentTarget.classList.add('dragover'); }, handleDragLeave: function(e) { e.currentTarget.classList.remove('dragover'); }, handleDrop: function(e) { e.preventDefault(); e.currentTarget.classList.remove('dragover'); const files = e.dataTransfer.files; app.handleFiles(files); }, // 处理文件选择 handleFileSelect: function(e) { const files = e.target.files; app.handleFiles(files); }, // 处理文件 handleFiles: function(files) { if (uploadedImages.length + files.length > CONFIG.MAX_FILES_PER_UPLOAD) { utils.showNotification(`最多只能上传 ${CONFIG.MAX_FILES_PER_UPLOAD} 张图片。`, 'warning'); return; } const filesToProcess = Array.from(files); filesToProcess.forEach(file => { try { utils.validateFile(file); if (file.type.startsWith('image/')) { const reader = new FileReader(); reader.onload = function(e) { const imageData = { id: Date.now() + Math.random(), data: e.target.result, name: file.name, type: file.type, size: file.size }; uploadedImages.push(imageData); uiController.displayImagePreview(imageData); }; reader.readAsDataURL(file); } } catch (error) { utils.showNotification(`${file.name}: ${error.message}`, 'danger'); } }); } }; // 主应用对象 const app = { // 初始化 init: async function() { this.initializeEventListeners(); // 初始化 IndexedDB try { //console.log('正在初始化 IndexedDB...'); const indexedDBSupported = await indexedDBStorage.init(); if (indexedDBSupported) { //console.log('IndexedDB 初始化成功,将优先使用 IndexedDB 存储'); // 尝试迁移数据 await indexedDBStorage.migrateFromLocalStorage(); } else { console.warn('IndexedDB 不可用,将使用 localStorage 作为后备'); } } catch (error) { console.error('IndexedDB 初始化失败:', error); } // 启动时检查存储空间 await utils.checkAndCleanStorage(); await utils.checkStorageStatus(); await uiController.loadSettings(); // 初始化"全部下载"按钮的可见性 uiController.updateDownloadAllButtonVisibility(); console.log('OpenRouter Image Generator initialized'); }, // 初始化事件监听器 initializeEventListeners: function() { // 文件拖拽 const dropZone = document.getElementById('dropZone'); dropZone.addEventListener('dragover', fileHandler.handleDragOver); dropZone.addEventListener('dragleave', fileHandler.handleDragLeave); dropZone.addEventListener('drop', fileHandler.handleDrop); // 文件选择 document.getElementById('imageInput').addEventListener('change', fileHandler.handleFileSelect); // 回车发送消息 const messageInput = document.getElementById('messageInput'); const self = this; messageInput.addEventListener('keypress', function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); self.sendMessage(); } }); // 全部下载按钮事件 const downloadAllBtn = document.getElementById('downloadAllImagesBtn'); if (downloadAllBtn) { downloadAllBtn.addEventListener('click', function() { app.downloadAllImages(); }); } // 设置变化时自动保存 const inputs = ['apiKey', 'baseUrl', 'model', 'timeout', 'proxy']; inputs.forEach(id => { document.getElementById(id).addEventListener('change', utils.debounce(() => { uiController.saveSettings(); }, 1000)); }); // 页面卸载时保存设置 window.addEventListener('beforeunload', () => { uiController.saveSettings(); }); }, // 测试连接 testConnection: async function() { const apiKey = document.getElementById('apiKey').value; const baseUrl = document.getElementById('baseUrl').value; if (!apiKey) { utils.showNotification(ERROR_MESSAGES.NO_API_KEY, 'warning'); return; } uiController.showLoading(true); try { const result = await apiService.testConnection(apiKey, baseUrl); if (result.success) { uiController.updateConnectionStatus(true); utils.showNotification('连接成功!', 'success'); } else { uiController.updateConnectionStatus(false); utils.showNotification(`连接失败:${result.error}`, 'danger'); } } catch (error) { uiController.updateConnectionStatus(false); utils.showNotification(`连接失败:${error.message}`, 'danger'); } finally { uiController.showLoading(false); } }, // 发送消息 - 生成一张图像(使用批量生成方法) sendMessage: async function() { // 临时设置批量数量为1 const batchCountInput = document.getElementById('batchCount'); const originalBatchCount = batchCountInput.value; batchCountInput.value = 1; // 调用批量生成方法 await this.sendBatchMessage(); // 恢复原始批量数量 batchCountInput.value = originalBatchCount; }, // 批量发送消息 sendBatchMessage: async function() { const messageInput = document.getElementById('messageInput'); const batchCountInput = document.getElementById('batchCount'); const message = messageInput.value.trim(); const batchCount = parseInt(batchCountInput.value) || 6; if (!message) { utils.showNotification('请输入消息', 'warning'); return; } if (batchCount < 1 || batchCount > 20) { utils.showNotification('请输入有效的生成数量(1-20)', 'warning'); return; } const apiKey = document.getElementById('apiKey').value; if (!apiKey) { utils.showNotification(ERROR_MESSAGES.NO_API_KEY, 'warning'); return; } // 隐藏传统加载状态,使用占位图像代替 uiController.hideTraditionalLoading(); // 添加用户消息到聊天历史 uiController.addChatMessage('user', message); // 创建占位图像 const placeholders = []; for (let i = 0; i < batchCount; i++) { const placeholder = uiController.addImagePlaceholder(i, batchCount); placeholders.push(placeholder); } // 显示批量生成状态 uiController.showBatchStatus(`正在批量生成 ${batchCount} 张图像...`); let successCount = 0; let failCount = 0; let firstResponse = null; let completedCount = 0; // 同时发送所有请求,但每个请求完成后立即处理 const promises = []; for (let i = 0; i < batchCount; i++) { promises.push( this.generateImageWithRetry(message, uploadedImages, currentSettings, i, batchCount) .then(result => { if (result.success) { // 保存第一个响应用于添加聊天消息 if (!firstResponse) { firstResponse = result.response; uiController.addChatMessage('assistant', result.response.content); } // 显示生成的图像 if (result.response.images && result.response.images.length > 0) { // 如果返回多个图像,为每个图像创建新的占位符 if (result.response.images.length > 1) { // 移除原始占位符 const originalPlaceholder = document.getElementById(`placeholder-${i}`); if (originalPlaceholder) { originalPlaceholder.remove(); } // 为每个图像添加新的图像项 result.response.images.forEach((img, imgIndex) => { // 确保Blob URL有原始base64数据 if (img.startsWith('blob:') && img._originalBase64) { uiController.addGeneratedImage(img); } else { // 如果没有原始base64数据,需要从API响应中获取 uiController.addGeneratedImage(img); } }); } else { // 只有一个图像,直接替换占位符 uiController.replacePlaceholderWithImage(`placeholder-${i}`, result.response.images[0]); } successCount++; } else { // 没有图像数据,但请求成功,这种情况应该不会发生,因为generateImageWithRetry已经检查了 failCount++; } } else { // 生成失败,检查是否需要保留占位符 if (!result.keepPlaceholder) { // 移除占位符(旧的行为) const placeholder = document.getElementById(`placeholder-${i}`); if (placeholder) { placeholder.remove(); } } failCount++; } // 更新完成计数和状态 completedCount++; uiController.showBatchStatus(`已完成 ${completedCount}/${batchCount} 张图像...`); return { index: i, success: result.success }; }) ); } // 等待所有请求完成 await Promise.allSettled(promises); // 检查存储空间 await utils.checkAndCleanStorage(); // 清空输入框 messageInput.value = ''; // 隐藏批量生成状态 setTimeout(() => { uiController.showBatchStatus('', false); }, 3000); // 显示最终结果 if (failCount === 0) { utils.showNotification(`批量生成完成!成功生成 ${successCount} 张图像`, 'success'); } else { utils.showNotification(`批量生成完成!成功 ${successCount} 张,失败 ${failCount} 张`, 'warning'); } }, // 带重试机制的图像生成方法 generateImageWithRetry: async function(message, images, settings, imageIndex, totalImages) { const maxRetries = 15; let retryCount = 0; while (retryCount <= maxRetries) { try { const response = await apiService.generateImage(message, images, settings); // 检查响应中是否包含图像 if (!response.images || response.images.length === 0) { throw new Error('生成响应中没有图像数据'); } return { success: true, response: response }; } catch (error) { retryCount++; console.error(`生成第 ${imageIndex + 1} 张图像失败,重试次数 ${retryCount}/${maxRetries}:`, error); if (retryCount <= maxRetries) { // 更新占位符状态,显示重试信息 const placeholder = document.getElementById(`placeholder-${imageIndex}`); if (placeholder) { const placeholderText = placeholder.querySelector('.image-placeholder-text'); if (placeholderText) { placeholderText.textContent = `正在重试生成图像 ${imageIndex + 1}/${totalImages} (${retryCount}/${maxRetries})`; } } // 等待一段时间后重试,使用指数退避策略 const delayTime = 1000 * Math.pow(2, retryCount - 1); await new Promise(resolve => setTimeout(resolve, delayTime)); } else { // 重试次数用完,返回失败,但不要移除占位符 // 更新占位符显示最终失败状态 const placeholder = document.getElementById(`placeholder-${imageIndex}`); if (placeholder) { const placeholderText = placeholder.querySelector('.image-placeholder-text'); if (placeholderText) { placeholderText.textContent = `生成图像 ${imageIndex + 1}/${totalImages} 失败,已重试 ${maxRetries} 次`; } const placeholderIcon = placeholder.querySelector('.image-placeholder-icon'); if (placeholderIcon) { placeholderIcon.className = 'fas fa-exclamation-triangle image-placeholder-icon'; placeholderIcon.style.color = '#dc3545'; } } return { success: false, error: error, keepPlaceholder: true }; } } } // 重试次数用完,返回失败,但不要移除占位符 return { success: false, error: new Error('重试次数用完'), keepPlaceholder: true }; }, // 移除上传的图像 removeImage: function(imageId) { uploadedImages = uploadedImages.filter(img => img.id !== imageId); const preview = document.getElementById('imagePreview'); const imageElement = preview.querySelector(`[data-image-id="${imageId}"]`); if (imageElement) { imageElement.remove(); } if (uploadedImages.length === 0) { preview.innerHTML = '

未选择图像

'; } }, // 移除生成的图像 removeGeneratedImage: async function(imageId) { await uiController.removeGeneratedImage(imageId); }, // 下载图像 downloadImage: function(imageUrl) { // 检查是否是Blob URL并且有原始base64数据 if (imageUrl.startsWith('blob:') && imageUrl._originalBase64) { // 使用原始base64数据下载 const link = document.createElement('a'); link.href = imageUrl._originalBase64; link.download = `generated-image-${Date.now()}.png`; link.click(); } else if (imageUrl.startsWith('blob:')) { // 如果没有原始base64数据,通过fetch获取Blob数据 fetch(imageUrl) .then(response => response.blob()) .then(blob => { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `generated-image-${Date.now()}.png`; link.click(); // 清理临时URL setTimeout(() => URL.revokeObjectURL(url), 100); }) .catch(error => { console.error('Error downloading image:', error); utils.showNotification('下载图像失败', 'danger'); }); } else { // 如果是base64数据,直接下载 const link = document.createElement('a'); link.href = imageUrl; link.download = `generated-image-${Date.now()}.png`; link.click(); } }, // 全部下载图像 downloadAllImages: function() { if (generatedImages.length === 0) { utils.showNotification('没有可下载的图像', 'warning'); return; } // 创建一个临时通知 utils.showNotification(`正在下载 ${generatedImages.length} 张图像...`, 'info'); // 逐个下载图像 generatedImages.forEach((img, index) => { setTimeout(() => { // 使用与单个图像下载相同的逻辑 if (img.url.startsWith('blob:') && img.url._originalBase64) { // 使用原始base64数据下载 const link = document.createElement('a'); link.href = img.url._originalBase64; link.download = `generated-image-${Date.now()}-${index + 1}.png`; link.click(); } else if (img.url.startsWith('blob:')) { // 如果没有原始base64数据,通过fetch获取Blob数据 fetch(img.url) .then(response => response.blob()) .then(blob => { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `generated-image-${Date.now()}-${index + 1}.png`; link.click(); // 清理临时URL setTimeout(() => URL.revokeObjectURL(url), 100); }) .catch(error => { console.error('Error downloading image:', error); utils.showNotification('下载图像失败', 'danger'); }); } else { // 如果是base64数据,直接下载 const link = document.createElement('a'); link.href = img.url; link.download = `generated-image-${Date.now()}-${index + 1}.png`; link.click(); } // 最后一个图像下载完成后显示通知 if (index === generatedImages.length - 1) { setTimeout(() => { utils.showNotification(`已成功下载 ${generatedImages.length} 张图像`, 'success'); }, 500); } }, index * 200); // 每个图像下载间隔200ms,避免浏览器阻止多个下载 }); }, // 复制图像URL copyImageUrl: function(imageUrl) { navigator.clipboard.writeText(imageUrl).then(() => { utils.showNotification('图像URL已复制到剪贴板', 'success'); }).catch(() => { utils.showNotification('复制失败,请手动复制', 'warning'); }); }, // 处理文件 handleFiles: function(files) { fileHandler.handleFiles(files); }, // 复制消息内容到剪贴板 copyMessageContent: function(messageId) { const messageElement = document.getElementById(messageId); if (!messageElement) { utils.showNotification('找不到消息内容', 'danger'); return; } const content = messageElement.textContent; navigator.clipboard.writeText(content).then(() => { utils.showNotification('已复制到剪贴板', 'success'); }).catch(() => { utils.showNotification('复制失败,请手动复制', 'danger'); }); }, // 编辑消息内容,将其填入文本框 editMessageContent: function(messageId) { const messageElement = document.getElementById(messageId); if (!messageElement) { utils.showNotification('找不到消息内容', 'danger'); return; } const content = messageElement.textContent; const messageInput = document.getElementById('messageInput'); if (messageInput) { messageInput.value = content; messageInput.focus(); utils.showNotification('内容已填入文本框', 'success'); } else { utils.showNotification('找不到文本输入框', 'danger'); } }, // 清除存储数据 clearStorage: async function() { if (confirm('确定要清除所有存储的数据吗?这将删除所有聊天记录和生成的图像记录。')) { try { if (indexedDBStorage.isSupported) { // 清除 IndexedDB 数据 const cleared = await indexedDBStorage.clearAllData(); if (cleared) { //console.log('IndexedDB 数据已清除'); } else { console.error('IndexedDB 清除失败'); } } // 清空内存中的数据 chatHistory = []; generatedImages = []; uploadedImages = []; // 更新UI document.getElementById('chatHistory').innerHTML = ''; document.getElementById('imageGallery').innerHTML = ''; document.getElementById('imagePreview').innerHTML = ''; utils.showNotification('已成功清除所有存储数据', 'success'); } catch (error) { console.error('清除存储数据失败:', error); utils.showNotification('清除存储数据失败', 'danger'); } } } }; // 页面加载完成后初始化 document.addEventListener('DOMContentLoaded', async () => { await app.init(); }); // 全局函数(供HTML调用) window.testConnection = () => app.testConnection(); window.sendMessage = () => app.sendMessage(); window.sendBatchMessage = () => app.sendBatchMessage(); window.downloadImage = (url) => app.downloadImage(url); window.downloadAllImages = () => app.downloadAllImages(); window.copyImageUrl = (url) => app.copyImageUrl(url); window.removeImage = (id) => app.removeImage(id); window.removeGeneratedImage = (id) => app.removeGeneratedImage(id); window.clearStorage = () => app.clearStorage(); // 调试函数 window.checkStorageStatus = () => utils.checkStorageStatus(); window.checkEventBindings = () => utils.checkEventBindings(); window.clearIndexedDB = async () => { if (indexedDBStorage.isSupported) { const cleared = await indexedDBStorage.clearAllData(); //console.log('IndexedDB 已清空:', cleared); } };