2335 lines
94 KiB
JavaScript
2335 lines
94 KiB
JavaScript
// 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);
|
||
}
|
||
}; |