From 960210d96187ff9ab0b71cdc1c98a71ca390e552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E6=82=A6?= Date: Fri, 29 Aug 2025 11:15:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 126 +++++ LICENSE | 21 + README.md | 185 +++++++ config.json | 75 +++ index.html | 162 ++++++ script.js | 1354 +++++++++++++++++++++++++++++++++++++++++++++++++++ styles.css | 462 ++++++++++++++++++ test.html | 138 ++++++ 8 files changed, 2523 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 config.json create mode 100644 index.html create mode 100644 script.js create mode 100644 styles.css create mode 100644 test.html 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