// 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 = `
`;
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 = `
`;
// 添加事件监听器
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}
${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 = `
`;
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);
}
};