Files
OpenRouter_Image/script.js

2335 lines
94 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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: 1, // 限制为只上传一张图片
SUPPORTED_IMAGE_FORMATS: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
MAX_CHAT_HISTORY: 50, // 减少聊天历史记录数量以节省存储空间
MAX_GENERATED_IMAGES: 20, // 减少存储的图像数量以防止存储空间溢出
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}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
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
};
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' ? `
<div class="message-actions">
<button class="btn btn-sm btn-outline-secondary me-1" onclick="app.copyMessageContent('${messageId}')" title="复制">
<i class="fas fa-copy"></i>
</button>
<button class="btn btn-sm btn-outline-primary" onclick="app.editMessageContent('${messageId}')" title="编辑">
<i class="fas fa-edit"></i>
</button>
</div>
` : '';
messageDiv.innerHTML = `
<div class="d-flex justify-content-between align-items-start">
<div>
<strong>${role === 'user' ? '用户' : '助手'}</strong>
<small class="text-muted ms-2">${timestamp}</small>
</div>
${actionButtons}
</div>
<div class="mt-2 message-content" id="${messageId}">${this.escapeHtml(content)}</div>
`;
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 = `
<img src="${displayUrl}" alt="Generated Image" loading="lazy" class="generated-image">
<div class="image-overlay">
<div class="btn-grid">
<div class="btn-grid-row">
<button class="btn btn-sm btn-outline-light view-large-btn" data-image-url="${imageUrl}" title="查看大图">
<i class="fas fa-search-plus"></i>
</button>
<button class="btn btn-sm btn-outline-light download-btn" data-image-url="${imageUrl}" title="下载图像">
<i class="fas fa-download"></i>
</button>
</div>
<div class="btn-grid-row">
<button class="btn btn-sm btn-outline-light copy-url-btn" data-image-url="${imageUrl}" title="复制URL">
<i class="fas fa-copy"></i>
</button>
<button class="btn btn-sm btn-outline-light remove-btn" data-image-id="${imageId}" title="删除">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
`;
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 = `
<div class="image-placeholder-content">
<div class="image-placeholder-spinner"></div>
<i class="fas fa-image image-placeholder-icon"></i>
<div class="image-placeholder-text">
正在生成图像 ${index + 1}/${total}
</div>
</div>
`;
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 = `
<img src="${displayUrl}" alt="Generated Image" loading="lazy" class="generated-image">
<div class="image-overlay">
<div class="btn-grid">
<div class="btn-grid-row">
<button class="btn btn-sm btn-outline-light view-large-btn" data-image-url="${imageUrl}" title="查看大图">
<i class="fas fa-search-plus"></i>
</button>
<button class="btn btn-sm btn-outline-light download-btn" data-image-url="${imageUrl}" title="下载图像">
<i class="fas fa-download"></i>
</button>
</div>
<div class="btn-grid-row">
<button class="btn btn-sm btn-outline-light copy-url-btn" data-image-url="${imageUrl}" title="复制URL">
<i class="fas fa-copy"></i>
</button>
<button class="btn btn-sm btn-outline-light remove-btn" data-image-id="${imageId}" title="删除">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
`;
// 添加事件监听器
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');
// 确保图像数据是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;
}
preview.innerHTML = `
<div class="card h-100">
<img src="${displayUrl}" class="card-img-top image-preview h-75" alt="${imageData.name}" loading="lazy" style="object-fit: contain;">
<div class="card-body p-2">
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">${utils.formatFileSize(imageData.size)}</small>
<button class="btn btn-sm btn-outline-danger" onclick="app.removeImage(${imageData.id})" title="删除">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
`;
},
// 保存聊天历史 - 新增消息时调用
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' ? `
<div class="message-actions">
<button class="btn btn-sm btn-outline-secondary me-1" onclick="app.copyMessageContent('${messageId}')" title="复制">
<i class="fas fa-copy"></i>
</button>
<button class="btn btn-sm btn-outline-primary" onclick="app.editMessageContent('${messageId}')" title="编辑">
<i class="fas fa-edit"></i>
</button>
</div>
` : '';
messageDiv.innerHTML = `
<div class="d-flex justify-content-between align-items-start">
<div>
<strong>${msg.role === 'user' ? '用户' : '助手'}</strong>
<small class="text-muted ms-2">${timestamp}</small>
</div>
${actionButtons}
</div>
<div class="mt-2 message-content" id="${messageId}">${self.escapeHtml(msg.content)}</div>
`;
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 = `
<img src="${displayUrl}" alt="Generated Image" loading="lazy" class="generated-image">
<div class="image-overlay">
<div class="btn-grid">
<div class="btn-grid-row">
<button class="btn btn-sm btn-outline-light view-large-btn" data-image-url="${img.url}" title="查看大图">
<i class="fas fa-search-plus"></i>
</button>
<button class="btn btn-sm btn-outline-light download-btn" data-image-url="${img.url}" title="下载图像">
<i class="fas fa-download"></i>
</button>
</div>
<div class="btn-grid-row">
<button class="btn btn-sm btn-outline-light copy-url-btn" data-image-url="${img.url}" title="复制URL">
<i class="fas fa-copy"></i>
</button>
<button class="btn btn-sm btn-outline-light remove-btn" data-image-id="${img.id}" title="删除">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
`;
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) {
const preview = document.getElementById('imagePreview');
preview.innerHTML = '';
// 限制文件数量
const filesToProcess = Array.from(files).slice(0, CONFIG.MAX_FILES_PER_UPLOAD);
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');
if (uploadedImages.length === 0) {
preview.innerHTML = '<p class="text-muted">未选择图像</p>';
} else {
preview.innerHTML = '';
uploadedImages.forEach(img => uiController.displayImagePreview(img));
}
},
// 移除生成的图像
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);
}
};