commit 960210d96187ff9ab0b71cdc1c98a71ca390e552 Author: 史悦 Date: Fri Aug 29 11:15:44 2025 +0800 初始提交 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..761b9f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,126 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Production build +dist/ +build/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +public + +# Storybook build outputs +.out +.storybook-out + +# Temporary folders +tmp/ +temp/ + +# Editor directories and files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Project specific +*.local +*.secret +*.key +*.pem +*.p12 +*.pfx + +# Cache +.cache/ +*.cache + +# Test files +test/ +tests/ +*.test.* +*.spec.* + +# Documentation +docs/ +*.md +!README.md + +# Backup files +*.bak +*.backup +*.swp +*.swo \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9ea1819 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 OVINC CN + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..096329a --- /dev/null +++ b/README.md @@ -0,0 +1,185 @@ +# OpenRouter Image Generator Web应用 + +这是一个基于OpenRouter API的图像生成Web应用,实现了与原始Python代码相同的功能,但提供了用户友好的Web界面。 + +## 功能特性 + +### 核心功能 +- 🎨 **图像生成**: 通过OpenRouter API生成高质量图像 +- 📤 **图像上传**: 支持拖拽上传参考图像 +- 💬 **对话界面**: 直观的聊天式交互界面 +- 🖼️ **图像画廊**: 展示和管理生成的图像 +- ⚙️ **API配置**: 灵活的API设置和连接测试 + +### 技术特性 +- 🌐 **响应式设计**: 适配各种设备屏幕 +- 🎯 **实时预览**: 即时显示上传的图像 +- 💾 **本地存储**: 自动保存用户设置 +- 🔗 **连接状态**: 实时显示API连接状态 +- 📱 **移动友好**: 支持触摸操作 + +## 快速开始 + +### 1. 获取OpenRouter API密钥 + +1. 访问 [OpenRouter官网](https://openrouter.ai/) +2. 注册账户并登录 +3. 在控制台中生成API密钥 + +### 2. 配置应用 + +1. 打开 `index.html` 文件 +2. 在"API设置"面板中输入您的API密钥 +3. 点击"测试连接"按钮验证配置 + +### 3. 使用应用 + +#### 上传参考图像(可选) +- 拖拽图像文件到上传区域 +- 或点击"选择图像"按钮 +- 支持多种图像格式(JPG、PNG、GIF等) + +#### 生成图像 +1. 在对话输入框中输入您的图像生成请求 +2. 点击"发送"按钮或按Enter键 +3. 等待API处理并生成结果 + +#### 管理生成的图像 +- 在图像画廊中查看所有生成的图像 +- 悬停在图像上显示操作按钮 +- 下载图像或复制图像URL + +## 配置选项 + +### API设置 +- **API Key**: 您的OpenRouter API密钥 +- **Base URL**: OpenRouter API的基础URL(默认:https://openrouter.ai/api/v1) +- **模型**: 选择使用的AI模型 + - Google Gemini 2.5 Flash Image Preview (Free) + - OpenAI GPT-4 Vision Preview + - Anthropic Claude 3 Sonnet +- **超时时间**: 请求超时时间(秒) +- **代理**: 可选的代理服务器设置 + +### 支持的模型 +- `google/gemini-2.5-flash-image-preview:free` - 免费的Google Gemini模型 +- `openai/gpt-4-vision-preview` - OpenAI的GPT-4视觉预览版 +- `anthropic/claude-3-sonnet-20240229` - Anthropic的Claude 3 Sonnet模型 + +## 使用示例 + +### 基本图像生成 +``` +请生成一张美丽的日落风景图像 +``` + +### 基于参考图像的生成 +1. 上传一张参考图像 +2. 输入提示:`请根据上传的图像风格,生成一张类似的现代建筑图像` + +### 复杂场景生成 +``` +请生成一张未来城市的图像,包含飞行汽车、玻璃建筑和绿色植物,风格要写实且具有科技感 +``` + +## 技术实现 + +### 前端技术栈 +- **HTML5**: 语义化标记和现代Web API +- **CSS3**: 响应式设计和动画效果 +- **JavaScript**: 原生JS实现所有交互功能 +- **Bootstrap 5**: UI组件和响应式布局 +- **Font Awesome**: 图标库 + +### 核心功能实现 + +#### API调用 +```javascript +async function generateImage(message) { + const payload = { + model: model, + messages: messages, + stream: false + }; + + const response = await fetch(`${baseUrl}/chat/completions`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }); + + return await response.json(); +} +``` + +#### 图像处理 +- 支持Base64编码的图像数据 +- 文件拖拽上传功能 +- 图像预览和管理 +- 图像下载和URL复制 + +#### 状态管理 +- 本地存储用户设置 +- 实时连接状态显示 +- 加载状态指示器 +- 错误处理和用户反馈 + +## 浏览器兼容性 + +- ✅ Chrome 80+ +- ✅ Firefox 75+ +- ✅ Safari 13+ +- ✅ Edge 80+ + +## 注意事项 + +1. **API限制**: 请注意OpenRouter API的使用限制和费用 +2. **图像大小**: 上传的图像文件大小建议不超过10MB +3. **网络要求**: 需要稳定的网络连接以访问OpenRouter API +4. **隐私安全**: API密钥存储在浏览器本地,请勿在公共设备上使用 + +## 故障排除 + +### 常见问题 + +**Q: 连接测试失败** +- 检查API密钥是否正确 +- 确认网络连接正常 +- 验证Base URL设置 + +**Q: 图像生成失败** +- 检查输入的提示词是否合适 +- 确认选择的模型支持图像生成 +- 查看浏览器控制台错误信息 + +**Q: 图像上传失败** +- 检查图像格式是否支持 +- 确认图像文件大小是否合理 +- 尝试使用不同的浏览器 + +### 调试技巧 +1. 打开浏览器开发者工具(F12) +2. 查看Console标签的错误信息 +3. 检查Network标签的API请求状态 +4. 验证API响应数据格式 + +## 许可证 + +本项目基于MIT许可证开源,详见LICENSE文件。 + +## 贡献 + +欢迎提交Issue和Pull Request来改进这个项目! + +## 联系方式 + +如有问题或建议,请通过以下方式联系: +- GitHub Issues +- Email: [您的邮箱] + +--- + +**注意**: 这是一个前端演示应用,生产环境使用时请注意API密钥的安全性。 \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..ff35b7e --- /dev/null +++ b/config.json @@ -0,0 +1,75 @@ +{ + "app": { + "name": "OpenRouter Image Generator", + "version": "1.0.0", + "description": "基于OpenRouter API的图像生成Web应用", + "author": "OVINC CN", + "license": "MIT" + }, + "api": { + "default_base_url": "https://openrouter.ai/api/v1", + "default_timeout": 600, + "default_model": "google/gemini-2.5-flash-image-preview:free", + "supported_models": [ + { + "id": "google/gemini-2.5-flash-image-preview:free", + "name": "Google Gemini 2.5 Flash Image Preview", + "description": "免费的Google Gemini模型,支持图像生成和视觉理解", + "pricing": "free" + }, + { + "id": "openai/gpt-4-vision-preview", + "name": "OpenAI GPT-4 Vision Preview", + "description": "OpenAI的GPT-4视觉预览版,强大的图像理解和生成能力", + "pricing": "paid" + }, + { + "id": "anthropic/claude-3-sonnet-20240229", + "name": "Anthropic Claude 3 Sonnet", + "description": "Anthropic的Claude 3 Sonnet模型,优秀的图像分析能力", + "pricing": "paid" + } + ] + }, + "ui": { + "theme": { + "primary_color": "#667eea", + "secondary_color": "#764ba2", + "background_gradient": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)" + }, + "features": { + "drag_drop": true, + "image_preview": true, + "chat_history": true, + "image_gallery": true, + "settings_panel": true, + "connection_status": true + }, + "limits": { + "max_file_size": 10485760, + "max_files_per_upload": 5, + "supported_image_formats": ["image/jpeg", "image/png", "image/gif", "image/webp"], + "max_chat_history": 100, + "max_generated_images": 50 + } + }, + "storage": { + "settings_key": "openRouterSettings", + "chat_history_key": "openRouterChatHistory", + "generated_images_key": "openRouterGeneratedImages" + }, + "endpoints": { + "models": "/models", + "chat_completions": "/chat/completions", + "image_generation": "/images/generations" + }, + "error_messages": { + "no_api_key": "请先输入API Key", + "connection_failed": "连接失败,请检查网络和API设置", + "invalid_response": "API响应格式错误", + "image_generation_failed": "图像生成失败", + "file_too_large": "文件大小超过限制", + "unsupported_format": "不支持的文件格式", + "upload_failed": "文件上传失败" + } +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..ecffd32 --- /dev/null +++ b/index.html @@ -0,0 +1,162 @@ + + + + + + OpenRouter Image Generator + + + + + +
+
+
+
+
+

+ + OpenRouter Image Generator +

+

基于OpenRouter API的智能图像生成工具

+
+
+ +
+
API设置
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + 未连接 +
+
+ + +
+
+
+ + +
+
上传参考图像
+
+
+
+ +

拖拽图像到此处或点击选择文件

+ + +
+
+
+
+

未选择图像

+
+
+
+
+ + +
+
对话输入
+
+ + +
+
+ + +
+
+ 加载中... +
+

正在生成图像,请稍候...

+
+ + +
+
对话历史
+
+
+ + +
+
生成的图像
+ +
+
+
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/script.js b/script.js new file mode 100644 index 0000000..d2f3fe7 --- /dev/null +++ b/script.js @@ -0,0 +1,1354 @@ +// 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: '' +}; + +// 配置常量 +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 = { + // 格式化文件大小 + formatFileSize: function(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }, + + // 显示通知 + showNotification: function(message, type = 'info') { + const notification = document.createElement('div'); + notification.className = `alert alert-${type} alert-dismissible fade show position-fixed`; + notification.style.cssText = 'top: 20px; right: 20px; z-index: 9999; max-width: 300px;'; + notification.innerHTML = ` + ${message} + + `; + document.body.appendChild(notification); + + // 自动移除 + setTimeout(() => { + if (notification.parentNode) { + notification.parentNode.removeChild(notification); + } + }, 5000); + }, + + // 验证文件 + validateFile: function(file) { + if (file.size > CONFIG.MAX_FILE_SIZE) { + throw new Error(ERROR_MESSAGES.FILE_TOO_LARGE); + } + if (!CONFIG.SUPPORTED_IMAGE_FORMATS.includes(file.type)) { + throw new Error(ERROR_MESSAGES.UNSUPPORTED_FORMAT); + } + return true; + }, + + // 防抖函数 + debounce: function(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + }, + + // 检查和清理存储空间 + checkAndCleanStorage: async function() { + if (!indexedDBStorage.isSupported) { + console.warn('IndexedDB 不支持,无法进行存储检查'); + return; + } + + try { + // 检查 IndexedDB 存储并清理过期数据 + await indexedDBStorage.cleanupOldMessages(); + await indexedDBStorage.cleanupOldImages(); + + console.log('IndexedDB 存储空间检查和清理完成'); + } catch (error) { + console.error('存储空间检查失败:', error); + } + }, + + // 检查存储状态 - 调试用 + checkStorageStatus: async function() { + console.log('=== 存储状态检查 ==='); + console.log('IndexedDB 支持:', indexedDBStorage.isSupported); + console.log('IndexedDB 数据库:', indexedDBStorage.db); + + if (indexedDBStorage.isSupported) { + try { + const settings = await indexedDBStorage.getSettings(); + const chatHistory = await indexedDBStorage.getChatHistory(); + const images = await indexedDBStorage.getGeneratedImages(); + + console.log('IndexedDB 数据统计:'); + console.log(`- 设置: ${settings ? '已保存' : '未保存'}`); + console.log(`- 聊天记录: ${chatHistory.length} 条`); + console.log(`- 生成图像: ${images.length} 个`); + + chatHistory.forEach((msg, idx) => { + console.log(` 聊天 ${idx}: ${msg.role} - ${msg.content.substring(0, 50)}...`); + }); + + images.forEach((img, idx) => { + console.log(` 图像 ${idx}: URL长度 ${img.url ? img.url.length : 0}`); + }); + } catch (error) { + console.error('读取 IndexedDB 数据失败:', error); + } + } + }, + + // 检查事件绑定 - 调试用 + checkEventBindings: function() { + console.log('=== 事件绑定检查 ==='); + const containers = document.querySelectorAll('.image-clickable-container'); + console.log(`找到 ${containers.length} 个可点击图像容器`); + + containers.forEach((container, idx) => { + const img = container.querySelector('img'); + const tooltip = container.querySelector('.image-preview-tooltip'); + console.log(`容器 ${idx}:`, { + src: img ? img.src.substring(0, 50) + '...' : '无图片', + hasTooltip: !!tooltip, + tooltipText: tooltip ? tooltip.textContent : '无', + classList: container.classList.toString() + }); + }); + + // 测试点击第一个容器 + if (containers.length > 0) { + console.log('尝试点击第一个图像容器...'); + containers[0].click(); + } + } +}; + +// IndexedDB存储管理模块 +const indexedDBStorage = { + db: null, + initPromise: null, + isSupported: false, + + // 初始化数据库 + init: function() { + if (this.initPromise) { + return this.initPromise; + } + + this.initPromise = new Promise((resolve, reject) => { + if (!window.indexedDB) { + console.warn('IndexedDB 不支持,将使用 localStorage 作为后备'); + this.db = null; + this.isSupported = false; + resolve(false); + return; + } + + const request = indexedDB.open(CONFIG.INDEXED_DB.NAME, CONFIG.INDEXED_DB.VERSION); + + request.onerror = (event) => { + console.error('IndexedDB 初始化失败:', event.target.error); + this.db = null; + this.isSupported = false; + resolve(false); + }; + + request.onsuccess = (event) => { + this.db = event.target.result; + this.isSupported = true; + console.log('IndexedDB 初始化成功'); + resolve(true); + }; + + request.onupgradeneeded = (event) => { + const db = event.target.result; + console.log('正在创建/升级数据库结构...'); + + // 创建设置存储 + if (!db.objectStoreNames.contains(CONFIG.INDEXED_DB.STORES.SETTINGS)) { + const settingsStore = db.createObjectStore(CONFIG.INDEXED_DB.STORES.SETTINGS, { keyPath: 'id' }); + settingsStore.createIndex('timestamp', 'timestamp', { unique: false }); + } + + // 创建聊天历史存储 + if (!db.objectStoreNames.contains(CONFIG.INDEXED_DB.STORES.CHAT_HISTORY)) { + const chatStore = db.createObjectStore(CONFIG.INDEXED_DB.STORES.CHAT_HISTORY, { keyPath: 'id', autoIncrement: true }); + chatStore.createIndex('timestamp', 'timestamp', { unique: false }); + chatStore.createIndex('role', 'role', { unique: false }); + } + + // 创建图像记录存储 + if (!db.objectStoreNames.contains(CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES)) { + const imagesStore = db.createObjectStore(CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES, { keyPath: 'id', autoIncrement: true }); + imagesStore.createIndex('timestamp', 'timestamp', { unique: false }); + imagesStore.createIndex('url', 'url', { unique: false }); + } + }; + }); + + return this.initPromise; + }, + + // 保存设置 + saveSettings: async function(settings) { + if (!this.isSupported) { + return false; + } + + try { + const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.SETTINGS], 'readwrite'); + const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.SETTINGS); + const data = { id: 'current', ...settings, timestamp: Date.now() }; + + await new Promise((resolve, reject) => { + const request = store.put(data); + request.onsuccess = () => resolve(true); + request.onerror = () => reject(request.error); + }); + + return true; + } catch (error) { + console.error('IndexedDB 保存设置失败:', error); + return false; + } + }, + + // 获取设置 + getSettings: async function() { + if (!this.isSupported) { + return null; + } + + try { + const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.SETTINGS], 'readonly'); + const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.SETTINGS); + + return new Promise((resolve, reject) => { + const request = store.get('current'); + request.onsuccess = () => { + const result = request.result; + if (result) { + // 移除 id 和 timestamp 字段 + const { id, timestamp, ...settings } = result; + resolve(settings); + } else { + resolve(null); + } + }; + request.onerror = () => reject(request.error); + }); + } catch (error) { + console.error('IndexedDB 获取设置失败:', error); + return null; + } + }, + + // 添加聊天消息 + addChatMessage: async function(role, content) { + if (!this.isSupported) { + return false; + } + + try { + const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.CHAT_HISTORY], 'readwrite'); + const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.CHAT_HISTORY); + + const message = { + role: role, + content: content, + timestamp: Date.now() + }; + + await new Promise((resolve, reject) => { + const request = store.add(message); + request.onsuccess = () => resolve(true); + request.onerror = () => reject(request.error); + }); + + // 清理旧消息 + await this.cleanupOldMessages(); + return true; + } catch (error) { + console.error('IndexedDB 添加聊天消息失败:', error); + return false; + } + }, + + // 获取聊天历史 + getChatHistory: async function() { + if (!this.isSupported) { + return []; + } + + try { + const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.CHAT_HISTORY], 'readonly'); + const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.CHAT_HISTORY); + const index = store.index('timestamp'); + + return new Promise((resolve, reject) => { + const request = index.getAll(); + request.onsuccess = () => { + const messages = request.result || []; + // 按时间戳排序 + messages.sort((a, b) => a.timestamp - b.timestamp); + resolve(messages); + }; + request.onerror = () => reject(request.error); + }); + } catch (error) { + console.error('IndexedDB 获取聊天历史失败:', error); + return []; + } + }, + + // 添加生成的图像 + addGeneratedImage: async function(imageId, imageUrl) { + if (!this.isSupported) { + return false; + } + + try { + 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: imageUrl, + 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) => a.timestamp - b.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; + } + } +}; + +// 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 = []; + + // 提取图像数据 + if (choice.message.images) { + choice.message.images.forEach(img => { + images.push(img.image_url.url); + }); + } + + 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'; + }, + + // 添加聊天消息 + 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(); + messageDiv.innerHTML = ` +
+
+ ${role === 'user' ? '用户' : '助手'} + ${timestamp} +
+
+
${this.escapeHtml(content)}
+ `; + + chatHistoryDiv.appendChild(messageDiv); + chatHistoryDiv.scrollTop = chatHistoryDiv.scrollHeight; + + // 添加到内存中的聊天历史 + chatHistory.push({ role: role, content: content }); + + // 保存到存储 - 优先使用 IndexedDB + await this.saveChatHistory(role, content); + }, + + // 转义HTML + escapeHtml: function(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + }, + + // 添加生成的图像 + addGeneratedImage: async function(imageUrl) { + const gallery = document.getElementById('imageGallery'); + const imageDiv = document.createElement('div'); + imageDiv.className = 'image-item fade-in'; + + const imageId = Date.now() + Math.random(); + imageDiv.innerHTML = ` + Generated Image +
+
+
+ + +
+
+ + +
+
+
+ `; + + gallery.appendChild(imageDiv); + + // 添加事件监听器 + 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'); + + 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', (e) => { + e.stopPropagation(); + app.removeGeneratedImage(imageId); + }); + } + + // 添加到内存中的图像记录 + generatedImages.push({ id: imageId, url: imageUrl }); + + // 保存到存储 - 优先使用 IndexedDB + await this.saveGeneratedImages(imageId, imageUrl); + }, + + + // 新的查看大图功能 + viewLargeImage: 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'); + + if (!modalImage) { + utils.showNotification('无法打开图像查看:找不到图像元素', 'danger'); + return; + } + + // 设置图像源 + modalImage.src = imageUrl; + modalImage.onerror = function() { + utils.showNotification('图像加载失败', 'danger'); + }; + + // 设置下载按钮功能 + if (downloadButton) { + downloadButton.onclick = function() { + app.downloadImage(imageUrl); + }; + } + + // 创建并显示模态框 + const modal = new bootstrap.Modal(modalElement); + modal.show(); + + } catch (error) { + utils.showNotification(`无法打开图像查看: ${error.message}`, 'danger'); + + // 备用方案:在新窗口中打开图像 + try { + window.open(imageUrl, '_blank'); + } catch (e) { + // 静默处理错误 + } + } + }, + + // 显示图像预览 + displayImagePreview: function(imageData) { + const preview = document.getElementById('imagePreview'); + preview.innerHTML = ` +
+ ${imageData.name} +
+
+ ${utils.formatFileSize(imageData.size)} + +
+
+
+ `; + }, + + // 保存聊天历史 - 新增消息时调用 + 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(); + messageDiv.innerHTML = ` +
+
+ ${msg.role === 'user' ? '用户' : '助手'} + ${timestamp} +
+
+
${self.escapeHtml(msg.content)}
+ `; + + chatHistoryDiv.appendChild(messageDiv); + }); + + // 加载生成的图像 + const indexedImages = await indexedDBStorage.getGeneratedImages(); + if (indexedImages && indexedImages.length > 0) { + generatedImages = indexedImages; + } + + // 重建图像UI + generatedImages.forEach(img => { + const gallery = document.getElementById('imageGallery'); + const imageDiv = document.createElement('div'); + imageDiv.className = 'image-item fade-in'; + + imageDiv.innerHTML = ` + Generated Image +
+
+
+ + +
+
+ + +
+
+
+ `; + gallery.appendChild(imageDiv); + + // 添加事件监听器 + 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'); + + 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', (e) => { + e.stopPropagation(); + app.removeGeneratedImage(img.id); + }); + } + }); + } catch (error) { + console.error('加载设置失败:', error); + } + }, + + // 移除生成的图像 + removeGeneratedImage: 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) { + element.remove(); + } + }); + + // 不需要再更新存储,因为 IndexedDB 会自动管理 + }, + + // 保存设置 + 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(); + 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 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() { + const messageInput = document.getElementById('messageInput'); + const message = messageInput.value.trim(); + + if (!message) { + utils.showNotification('请输入消息', 'warning'); + return; + } + + const apiKey = document.getElementById('apiKey').value; + if (!apiKey) { + utils.showNotification(ERROR_MESSAGES.NO_API_KEY, 'warning'); + return; + } + + // 显示加载状态 + uiController.showLoading(true); + + // 添加用户消息到聊天历史 + uiController.addChatMessage('user', message); + + try { + const response = await apiService.generateImage(message, uploadedImages, currentSettings); + + // 添加助手回复到聊天历史 + uiController.addChatMessage('assistant', response.content); + + // 显示生成的图像 + if (response.images && response.images.length > 0) { + response.images.forEach(img => { + uiController.addGeneratedImage(img); + }); + + // 检查存储空间 + await utils.checkAndCleanStorage(); + + // 提示用户存储空间有限 + utils.showNotification('已生成图像,但请注意本地存储空间有限,建议及时下载重要图像', 'info'); + } + + // 清空输入框 + messageInput.value = ''; + + // 显示使用统计 + if (response.usage) { + console.log('API Usage:', response.usage); + } + + } catch (error) { + console.error('生成图像失败:', error); + uiController.addChatMessage('assistant', `生成图像失败: ${error.message}`); + utils.showNotification(`生成图像失败: ${error.message}`, 'danger'); + } finally { + uiController.showLoading(false); + } + }, + + // 移除上传的图像 + removeImage: function(imageId) { + uploadedImages = uploadedImages.filter(img => img.id !== imageId); + const preview = document.getElementById('imagePreview'); + if (uploadedImages.length === 0) { + preview.innerHTML = '

未选择图像

'; + } else { + preview.innerHTML = ''; + uploadedImages.forEach(img => uiController.displayImagePreview(img)); + } + }, + + // 移除生成的图像 + removeGeneratedImage: function(imageId) { + uiController.removeGeneratedImage(imageId); + }, + + // 下载图像 + downloadImage: function(imageUrl) { + const link = document.createElement('a'); + link.href = imageUrl; + link.download = `generated-image-${Date.now()}.png`; + link.click(); + }, + + // 复制图像URL + copyImageUrl: function(imageUrl) { + navigator.clipboard.writeText(imageUrl).then(() => { + utils.showNotification('图像URL已复制到剪贴板', 'success'); + }).catch(() => { + utils.showNotification('复制失败,请手动复制', 'warning'); + }); + }, + + // 处理文件 + handleFiles: function(files) { + fileHandler.handleFiles(files); + }, + + + // 清除存储数据 + 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.downloadImage = (url) => app.downloadImage(url); +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); + } +}; \ No newline at end of file diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..b1d4c47 --- /dev/null +++ b/styles.css @@ -0,0 +1,462 @@ +/* OpenRouter Image Generator - 自定义样式 */ + +/* 全局样式 */ +:root { + --primary-color: #667eea; + --secondary-color: #764ba2; + --success-color: #28a745; + --danger-color: #dc3545; + --warning-color: #ffc107; + --info-color: #17a2b8; + --light-color: #f8f9fa; + --dark-color: #343a40; + --border-radius: 15px; + --box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + --transition: all 0.3s ease; +} + +/* 基础样式重置 */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); + min-height: 100vh; + line-height: 1.6; + color: var(--dark-color); +} + +/* 主容器 */ +.main-container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +/* 卡片样式 */ +.card { + border: none; + border-radius: var(--border-radius); + box-shadow: var(--box-shadow); + backdrop-filter: blur(10px); + background: rgba(255, 255, 255, 0.95); + transition: var(--transition); +} + +.card:hover { + transform: translateY(-5px); + box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15); +} + +.card-header { + background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); + color: white; + border-radius: var(--border-radius) var(--border-radius) 0 0 !important; + padding: 20px; + border: none; +} + +/* 表单控件样式 */ +.form-control, .form-select { + border-radius: 10px; + border: 2px solid #e9ecef; + padding: 12px; + transition: var(--transition); +} + +.form-control:focus, .form-select:focus { + border-color: var(--primary-color); + box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25); + outline: none; +} + +/* 按钮样式 */ +.btn { + border-radius: 10px; + padding: 12px 30px; + font-weight: 600; + transition: var(--transition); + border: none; +} + +.btn-primary { + background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); + color: white; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); + background: linear-gradient(135deg, var(--secondary-color) 0%, var(--primary-color) 100%); +} + +.btn-outline-primary { + border: 2px solid var(--primary-color); + color: var(--primary-color); +} + +.btn-outline-primary:hover { + background: var(--primary-color); + border-color: var(--primary-color); +} + +.btn-outline-danger { + border: 2px solid var(--danger-color); + color: var(--danger-color); +} + +.btn-outline-danger:hover { + background: var(--danger-color); + border-color: var(--danger-color); +} + +/* 图像预览样式 */ +.image-preview { + max-width: 100%; + border-radius: 10px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); + transition: var(--transition); +} + +.image-preview:hover { + transform: scale(1.05); +} + +/* 图像预览区域 */ +#imagePreview { + max-height: 20rem; +} + +/* 拖拽区域样式 */ +#dropZone { + border: 2px dashed #ccc; + border-radius: 10px; + transition: var(--transition); + cursor: pointer; +} + +#dropZone:hover { + border-color: var(--primary-color); + background: rgba(102, 126, 234, 0.05); +} + +#dropZone.dragover { + border-color: var(--primary-color); + background: rgba(102, 126, 234, 0.1); +} + +/* 聊天消息样式 */ +.chat-message { + background: var(--light-color); + border-radius: 15px; + padding: 15px; + margin-bottom: 15px; + border-left: 4px solid var(--primary-color); + transition: var(--transition); +} + +.chat-message:hover { + transform: translateX(5px); +} + +.chat-message.user { + background: #e3f2fd; + border-left-color: #2196f3; +} + +.chat-message.assistant { + background: #f3e5f5; + border-left-color: #9c27b0; +} + +/* 加载动画 */ +.loading-spinner { + display: none; + text-align: center; + padding: 20px; +} + +.spinner-border { + width: 3rem; + height: 3rem; + border-width: 0.3rem; +} + +/* 设置面板样式 */ +.settings-panel { + background: var(--light-color); + border-radius: 10px; + padding: 20px; + margin-bottom: 20px; + border: 1px solid #e9ecef; +} + +/* 图像画廊样式 */ +.image-gallery { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 15px; + margin-top: 20px; +} + +.image-item { + position: relative; + border-radius: 10px; + overflow: hidden; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); + transition: var(--transition); +} + +.image-item:hover { + transform: translateY(-5px); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); +} + +.image-item img { + width: 100%; + height: 200px; + object-fit: cover; +} + +.image-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.3s; +} + +.image-item:hover .image-overlay { + opacity: 1; +} + +/* 状态指示器 */ +.status-indicator { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: 5px; + animation: pulse 2s infinite; +} + +.status-connected { + background-color: var(--success-color); +} + +.status-disconnected { + background-color: var(--danger-color); +} + +/* 动画效果 */ +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(40, 167, 69, 0.7); + } + 70% { + box-shadow: 0 0 0 10px rgba(40, 167, 69, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(40, 167, 69, 0); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.fade-in { + animation: fadeIn 0.5s ease-in; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .main-container { + padding: 10px; + } + + .card { + margin-bottom: 20px; + } + + .image-gallery { + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 10px; + } + + .btn { + padding: 10px 20px; + font-size: 14px; + } + + .form-control, .form-select { + padding: 10px; + } +} + +@media (max-width: 480px) { + .image-gallery { + grid-template-columns: 1fr; + } + + .settings-panel { + padding: 15px; + } + + .card-header { + padding: 15px; + } +} + +/* 工具提示样式 */ +.tooltip { + position: relative; + display: inline-block; +} + +.tooltip .tooltiptext { + visibility: hidden; + width: 200px; + background-color: #555; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 5px; + position: absolute; + z-index: 1; + bottom: 125%; + left: 50%; + margin-left: -100px; + opacity: 0; + transition: opacity 0.3s; +} + +.tooltip:hover .tooltiptext { + visibility: visible; + opacity: 1; +} + +/* 滚动条样式 */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 10px; +} + +::-webkit-scrollbar-thumb { + background: var(--primary-color); + border-radius: 10px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--secondary-color); +} + +/* 特殊效果 */ +.glass-effect { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.text-gradient { + background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* 加载骨架屏 */ +.skeleton { + background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); + background-size: 200% 100%; + animation: loading 1.5s infinite; +} + +@keyframes loading { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +/* 图像查看模态框样式 */ +#viewerModalImage { + max-height: 80vh; + object-fit: contain; + border-radius: 8px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); + transition: transform 0.3s ease; +} + +#viewerModalImage:hover { + transform: scale(1.02); +} + +/* 图像查看模态框特定样式 */ +#imageViewerModal .modal-content { + border-radius: 15px; + overflow: hidden; +} + +#imageViewerModal .modal-header { + background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); + color: white; + border: none; +} + +#imageViewerModal .modal-footer { + background: rgba(0, 0, 0, 0.05); + border: none; +} + +/* 按钮网格布局 - 2排2列 */ +.btn-grid { + display: flex; + flex-direction: column; + gap: 5px; +} + +.btn-grid-row { + display: flex; + gap: 5px; +} + +.btn-grid-row .btn { + flex: 1; + min-width: 0; +} + +/* 错误状态 */ +.error-state { + border-color: var(--danger-color) !important; + background-color: rgba(220, 53, 69, 0.1) !important; +} + +/* 成功状态 */ +.success-state { + border-color: var(--success-color) !important; + background-color: rgba(40, 167, 69, 0.1) !important; +} diff --git a/test.html b/test.html new file mode 100644 index 0000000..531f218 --- /dev/null +++ b/test.html @@ -0,0 +1,138 @@ + + + + + + OpenRouter Image Generator - 测试页面 + + + + + +
+
+
+
+
+

OpenRouter Image Generator - 测试页面

+
+
+
+ + 这是测试页面,用于验证应用是否正常工作。请检查浏览器控制台是否有错误信息。 +
+ +
+
功能测试清单:
+
    +
  • + + 页面加载正常 +
  • +
  • + + JavaScript文件加载成功 +
  • +
  • + + 事件监听器初始化完成 +
  • +
  • + + UI控制器正常工作 +
  • +
  • + + 文件处理功能正常 +
  • +
+
+ +
+ + + 返回主应用 + + +
+ +
+
+
+
+
+
+ + + + + \ No newline at end of file