| <!DOCTYPE html> |
| <html lang="zh-CN"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| <title>Banana Pro AI</title> |
| <link rel="stylesheet" href="style.css"> |
| </head> |
| <body> |
|
|
| |
| <div id="login-overlay"> |
| <div class="login-card glass-panel"> |
| <h1 style="font-size: 2rem;">🍌</h1> |
| <h3 style="margin: 10px 0; color: #cbd5e1;">Banana Pro</h3> |
| <input type="password" id="pwd" placeholder="Password" onkeypress="if(event.key==='Enter')doLogin()"> |
| <button class="btn-3d" onclick="doLogin()" style="width: 100%; padding: 10px;">Unlock</button> |
| </div> |
| </div> |
|
|
| |
| <div class="app-container" id="app" style="filter: blur(10px); pointer-events: none;"> |
| <header> |
| <div> |
| <h2>创意画板</h2> |
| <div style="font-size: 12px; color: var(--text-sub);" id="status-bar">Ready</div> |
| </div> |
| </header> |
|
|
| |
| <div class="history-container"> |
| <div class="grid-layout" id="gallery"></div> |
| </div> |
|
|
| |
| <div class="input-section glass-panel" id="drop-zone"> |
| |
| <div class="preview-bar" id="preview-bar"></div> |
|
|
| |
| <div class="control-bar"> |
| <button class="upload-trigger" id="upload-btn" title="上传参考图">📷</button> |
| <textarea id="prompt" class="main-input" rows="1" placeholder="描述画面... (支持拖拽图片)"></textarea> |
| <button class="btn-3d send-btn" id="send-btn"> |
| <span>生成</span> |
| <div class="loader"></div> |
| </button> |
| </div> |
| </div> |
|
|
| <input type="file" id="file-input" multiple accept="image/*" hidden> |
| </div> |
|
|
| |
| <div class="modal" id="modal"> |
| <div class="modal-content glass-panel"> |
| <button class="close-modal" onclick="closeModal()">×</button> |
| <div class="modal-img-area"> |
| <img id="m-img" src=""> |
| </div> |
| <div class="modal-footer"> |
| <div class="input-refs" id="m-refs"></div> |
| <div class="prompt-display" id="m-prompt"></div> |
| <button class="btn-3d" id="m-reuse" style="width: 100%; padding: 12px;"> |
| 🎨 复用参数与图片 |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| |
| |
| const AppState = { |
| db: null, |
| currentImages: [], |
| galleryData: [], |
| currentModalItem: null |
| }; |
| |
| const DB_NAME = 'BananaProDB_v3'; |
| const DB_VERSION = 1; |
| const STORE_NAME = 'artworks'; |
| |
| |
| |
| |
| const Database = { |
| async init() { |
| return new Promise((resolve, reject) => { |
| const request = indexedDB.open(DB_NAME, DB_VERSION); |
| |
| request.onerror = () => reject(request.error); |
| |
| request.onsuccess = () => { |
| AppState.db = request.result; |
| resolve(); |
| }; |
| |
| request.onupgradeneeded = (event) => { |
| const db = event.target.result; |
| if (!db.objectStoreNames.contains(STORE_NAME)) { |
| const store = db.createObjectStore(STORE_NAME, { |
| keyPath: 'id', |
| autoIncrement: true |
| }); |
| store.createIndex('timestamp', 'timestamp', { unique: false }); |
| } |
| }; |
| }); |
| }, |
| |
| async save(item) { |
| return new Promise((resolve, reject) => { |
| const tx = AppState.db.transaction([STORE_NAME], 'readwrite'); |
| const store = tx.objectStore(STORE_NAME); |
| |
| |
| const record = { |
| prompt: item.prompt, |
| image: item.image, |
| inputImages: item.inputImages || [], |
| timestamp: Date.now() |
| }; |
| |
| const request = store.add(record); |
| request.onsuccess = () => resolve(request.result); |
| request.onerror = () => reject(request.error); |
| }); |
| }, |
| |
| async getAll() { |
| return new Promise((resolve, reject) => { |
| const tx = AppState.db.transaction([STORE_NAME], 'readonly'); |
| const store = tx.objectStore(STORE_NAME); |
| const request = store.getAll(); |
| |
| request.onsuccess = () => { |
| |
| const results = request.result.sort((a, b) => b.timestamp - a.timestamp); |
| resolve(results); |
| }; |
| request.onerror = () => reject(request.error); |
| }); |
| }, |
| |
| async delete(id) { |
| return new Promise((resolve, reject) => { |
| const tx = AppState.db.transaction([STORE_NAME], 'readwrite'); |
| const store = tx.objectStore(STORE_NAME); |
| const request = store.delete(id); |
| request.onsuccess = () => resolve(); |
| request.onerror = () => reject(request.error); |
| }); |
| }, |
| |
| async getById(id) { |
| return new Promise((resolve, reject) => { |
| const tx = AppState.db.transaction([STORE_NAME], 'readonly'); |
| const store = tx.objectStore(STORE_NAME); |
| const request = store.get(id); |
| request.onsuccess = () => resolve(request.result); |
| request.onerror = () => reject(request.error); |
| }); |
| } |
| }; |
| |
| |
| |
| |
| const ImageHandler = { |
| |
| fileToBase64(file) { |
| return new Promise((resolve, reject) => { |
| const reader = new FileReader(); |
| reader.onload = () => resolve(reader.result); |
| reader.onerror = () => reject(reader.error); |
| reader.readAsDataURL(file); |
| }); |
| }, |
| |
| |
| async processFiles(files) { |
| const maxImages = 16; |
| const imageFiles = Array.from(files).filter(f => f.type.startsWith('image/')); |
| |
| for (const file of imageFiles) { |
| if (AppState.currentImages.length >= maxImages) { |
| alert(`最多只能上传 ${maxImages} 张图片`); |
| break; |
| } |
| |
| try { |
| const base64 = await this.fileToBase64(file); |
| AppState.currentImages.push(base64); |
| } catch (err) { |
| console.error('图片读取失败:', err); |
| } |
| } |
| |
| PreviewManager.render(); |
| }, |
| |
| |
| removeAt(index) { |
| AppState.currentImages.splice(index, 1); |
| PreviewManager.render(); |
| }, |
| |
| |
| clear() { |
| AppState.currentImages = []; |
| PreviewManager.render(); |
| }, |
| |
| |
| setImages(images) { |
| AppState.currentImages = images ? [...images] : []; |
| PreviewManager.render(); |
| } |
| }; |
| |
| |
| |
| |
| const PreviewManager = { |
| container: null, |
| uploadBtn: null, |
| statusBar: null, |
| |
| init() { |
| this.container = document.getElementById('preview-bar'); |
| this.uploadBtn = document.getElementById('upload-btn'); |
| this.statusBar = document.getElementById('status-bar'); |
| }, |
| |
| render() { |
| |
| this.container.innerHTML = ''; |
| |
| const images = AppState.currentImages; |
| |
| if (images.length === 0) { |
| this.container.classList.remove('visible'); |
| this.uploadBtn.classList.remove('active'); |
| this.statusBar.textContent = 'Ready'; |
| return; |
| } |
| |
| this.container.classList.add('visible'); |
| this.uploadBtn.classList.add('active'); |
| this.statusBar.textContent = `已选择 ${images.length}/16 张图片`; |
| |
| |
| images.forEach((imgData, index) => { |
| const wrapper = document.createElement('div'); |
| wrapper.className = 'thumb-wrapper'; |
| |
| const img = document.createElement('img'); |
| img.src = imgData; |
| |
| const removeBtn = document.createElement('div'); |
| removeBtn.className = 'thumb-remove'; |
| removeBtn.textContent = '×'; |
| removeBtn.onclick = (e) => { |
| e.stopPropagation(); |
| ImageHandler.removeAt(index); |
| }; |
| |
| wrapper.appendChild(img); |
| wrapper.appendChild(removeBtn); |
| this.container.appendChild(wrapper); |
| }); |
| } |
| }; |
| |
| |
| |
| |
| const GalleryManager = { |
| container: null, |
| |
| init() { |
| this.container = document.getElementById('gallery'); |
| }, |
| |
| async load() { |
| try { |
| AppState.galleryData = await Database.getAll(); |
| this.render(); |
| } catch (err) { |
| console.error('加载画廊失败:', err); |
| } |
| }, |
| |
| render() { |
| this.container.innerHTML = ''; |
| |
| if (AppState.galleryData.length === 0) { |
| this.container.innerHTML = ` |
| <div style="grid-column: 1/-1; text-align: center; color: var(--text-sub); padding: 60px 20px;"> |
| <div style="font-size: 48px; margin-bottom: 10px;">🎨</div> |
| <div>暂无作品,开始创作吧!</div> |
| </div> |
| `; |
| return; |
| } |
| |
| AppState.galleryData.forEach(item => { |
| const card = this.createCard(item); |
| this.container.appendChild(card); |
| }); |
| }, |
| |
| createCard(item) { |
| const el = document.createElement('div'); |
| el.className = 'history-item'; |
| el.dataset.id = item.id; |
| |
| |
| if (item.inputImages && item.inputImages.length > 0) { |
| const badge = document.createElement('div'); |
| badge.className = 'item-badge'; |
| badge.textContent = `📎 ${item.inputImages.length}`; |
| el.appendChild(badge); |
| } |
| |
| |
| const img = document.createElement('img'); |
| img.loading = 'lazy'; |
| img.src = item.image; |
| img.onerror = () => { |
| console.error('图片加载失败, ID:', item.id); |
| img.src = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect fill="%23333" width="100" height="100"/><text fill="%23666" x="50%" y="50%" text-anchor="middle" dy=".3em">Error</text></svg>'; |
| }; |
| el.appendChild(img); |
| |
| |
| const actions = document.createElement('div'); |
| actions.className = 'item-actions'; |
| |
| const downloadBtn = document.createElement('button'); |
| downloadBtn.className = 'icon-btn'; |
| downloadBtn.innerHTML = '⬇'; |
| downloadBtn.onclick = (e) => { |
| e.stopPropagation(); |
| this.downloadImage(item); |
| }; |
| |
| const deleteBtn = document.createElement('button'); |
| deleteBtn.className = 'icon-btn'; |
| deleteBtn.style.background = 'rgba(239,68,68,0.8)'; |
| deleteBtn.innerHTML = '🗑'; |
| deleteBtn.onclick = (e) => { |
| e.stopPropagation(); |
| this.deleteItem(item.id); |
| }; |
| |
| actions.appendChild(downloadBtn); |
| actions.appendChild(deleteBtn); |
| el.appendChild(actions); |
| |
| |
| el.onclick = () => ModalManager.open(item); |
| |
| return el; |
| }, |
| |
| downloadImage(item) { |
| const link = document.createElement('a'); |
| link.href = item.image; |
| link.download = `banana-pro-${item.id}-${Date.now()}.png`; |
| document.body.appendChild(link); |
| link.click(); |
| document.body.removeChild(link); |
| }, |
| |
| async deleteItem(id) { |
| if (!confirm('确定要删除这张图片吗?')) return; |
| |
| try { |
| await Database.delete(id); |
| await this.load(); |
| } catch (err) { |
| console.error('删除失败:', err); |
| alert('删除失败'); |
| } |
| } |
| }; |
| |
| |
| |
| |
| const ModalManager = { |
| modal: null, |
| imgEl: null, |
| promptEl: null, |
| refsEl: null, |
| reuseBtn: null, |
| |
| init() { |
| this.modal = document.getElementById('modal'); |
| this.imgEl = document.getElementById('m-img'); |
| this.promptEl = document.getElementById('m-prompt'); |
| this.refsEl = document.getElementById('m-refs'); |
| this.reuseBtn = document.getElementById('m-reuse'); |
| |
| |
| this.modal.onclick = (e) => { |
| if (e.target === this.modal) this.close(); |
| }; |
| |
| |
| document.addEventListener('keydown', (e) => { |
| if (e.key === 'Escape') this.close(); |
| }); |
| }, |
| |
| open(item) { |
| AppState.currentModalItem = item; |
| |
| |
| this.imgEl.src = item.image; |
| |
| |
| this.promptEl.textContent = item.prompt; |
| |
| |
| this.refsEl.innerHTML = ''; |
| if (item.inputImages && item.inputImages.length > 0) { |
| item.inputImages.forEach(imgData => { |
| const thumb = document.createElement('img'); |
| thumb.className = 'ref-thumb'; |
| thumb.src = imgData; |
| thumb.onclick = () => window.open(imgData, '_blank'); |
| this.refsEl.appendChild(thumb); |
| }); |
| } |
| |
| |
| this.reuseBtn.onclick = () => this.reuse(); |
| |
| this.modal.style.display = 'flex'; |
| }, |
| |
| close() { |
| this.modal.style.display = 'none'; |
| AppState.currentModalItem = null; |
| }, |
| |
| reuse() { |
| const item = AppState.currentModalItem; |
| if (!item) return; |
| |
| |
| const textarea = document.getElementById('prompt'); |
| textarea.value = item.prompt; |
| textarea.style.height = 'auto'; |
| textarea.style.height = textarea.scrollHeight + 'px'; |
| |
| |
| if (item.inputImages && item.inputImages.length > 0) { |
| ImageHandler.setImages(item.inputImages); |
| } else { |
| ImageHandler.clear(); |
| } |
| |
| this.close(); |
| textarea.focus(); |
| } |
| }; |
| |
| |
| |
| |
| const Generator = { |
| sendBtn: null, |
| textarea: null, |
| |
| init() { |
| this.sendBtn = document.getElementById('send-btn'); |
| this.textarea = document.getElementById('prompt'); |
| |
| this.sendBtn.onclick = () => this.generate(); |
| |
| |
| this.textarea.addEventListener('input', () => { |
| this.textarea.style.height = 'auto'; |
| this.textarea.style.height = this.textarea.scrollHeight + 'px'; |
| }); |
| |
| |
| this.textarea.addEventListener('keydown', (e) => { |
| if (e.key === 'Enter' && !e.shiftKey) { |
| e.preventDefault(); |
| this.generate(); |
| } |
| }); |
| }, |
| |
| setLoading(loading) { |
| if (loading) { |
| this.sendBtn.classList.add('loading'); |
| this.sendBtn.disabled = true; |
| } else { |
| this.sendBtn.classList.remove('loading'); |
| this.sendBtn.disabled = false; |
| } |
| }, |
| |
| async generate() { |
| const prompt = this.textarea.value.trim(); |
| if (!prompt) { |
| alert('请输入提示词'); |
| return; |
| } |
| |
| this.setLoading(true); |
| |
| try { |
| const response = await fetch('/api/generate', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| prompt: prompt, |
| images: AppState.currentImages |
| }) |
| }); |
| |
| const data = await response.json(); |
| |
| if (!response.ok || !data.success) { |
| throw new Error(data.message || '生成失败'); |
| } |
| |
| |
| if (!data.image || !data.image.startsWith('data:image')) { |
| throw new Error('返回的图片数据无效'); |
| } |
| |
| |
| await Database.save({ |
| prompt: prompt, |
| image: data.image, |
| inputImages: [...AppState.currentImages] |
| }); |
| |
| |
| await GalleryManager.load(); |
| |
| |
| this.textarea.value = ''; |
| this.textarea.style.height = 'auto'; |
| ImageHandler.clear(); |
| |
| } catch (err) { |
| console.error('生成失败:', err); |
| alert('生成失败: ' + err.message); |
| } finally { |
| this.setLoading(false); |
| } |
| } |
| }; |
| |
| |
| |
| |
| const Auth = { |
| async check() { |
| try { |
| const res = await fetch('/api/check-auth'); |
| const data = await res.json(); |
| return data.authenticated; |
| } catch { |
| return false; |
| } |
| }, |
| |
| async login(password) { |
| const res = await fetch('/api/login', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ password }) |
| }); |
| const data = await res.json(); |
| return data.success; |
| }, |
| |
| unlock() { |
| document.getElementById('login-overlay').style.display = 'none'; |
| const app = document.getElementById('app'); |
| app.style.filter = 'none'; |
| app.style.pointerEvents = 'all'; |
| } |
| }; |
| |
| |
| |
| |
| const DragDrop = { |
| dropZone: null, |
| |
| init() { |
| this.dropZone = document.getElementById('drop-zone'); |
| |
| |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(event => { |
| this.dropZone.addEventListener(event, (e) => { |
| e.preventDefault(); |
| e.stopPropagation(); |
| }); |
| }); |
| |
| |
| ['dragenter', 'dragover'].forEach(event => { |
| this.dropZone.addEventListener(event, () => { |
| this.dropZone.style.borderColor = 'var(--accent-color)'; |
| this.dropZone.style.background = 'rgba(59, 130, 246, 0.1)'; |
| }); |
| }); |
| |
| |
| ['dragleave', 'drop'].forEach(event => { |
| this.dropZone.addEventListener(event, () => { |
| this.dropZone.style.borderColor = ''; |
| this.dropZone.style.background = ''; |
| }); |
| }); |
| |
| |
| this.dropZone.addEventListener('drop', async (e) => { |
| const files = e.dataTransfer.files; |
| if (files.length > 0) { |
| await ImageHandler.processFiles(files); |
| } |
| }); |
| } |
| }; |
| |
| |
| |
| |
| const FileSelector = { |
| input: null, |
| btn: null, |
| |
| init() { |
| this.input = document.getElementById('file-input'); |
| this.btn = document.getElementById('upload-btn'); |
| |
| this.btn.onclick = () => this.input.click(); |
| |
| this.input.onchange = async () => { |
| if (this.input.files.length > 0) { |
| await ImageHandler.processFiles(this.input.files); |
| } |
| this.input.value = ''; |
| }; |
| } |
| }; |
| |
| |
| |
| |
| async function doLogin() { |
| const pwd = document.getElementById('pwd').value; |
| if (!pwd) return; |
| |
| const success = await Auth.login(pwd); |
| if (success) { |
| Auth.unlock(); |
| await GalleryManager.load(); |
| } else { |
| alert('密码错误'); |
| } |
| } |
| |
| function closeModal() { |
| ModalManager.close(); |
| } |
| |
| |
| |
| |
| async function initApp() { |
| try { |
| |
| await Database.init(); |
| |
| |
| PreviewManager.init(); |
| GalleryManager.init(); |
| ModalManager.init(); |
| Generator.init(); |
| DragDrop.init(); |
| FileSelector.init(); |
| |
| |
| const isAuth = await Auth.check(); |
| if (isAuth) { |
| Auth.unlock(); |
| await GalleryManager.load(); |
| } |
| |
| console.log('App initialized successfully'); |
| } catch (err) { |
| console.error('App initialization failed:', err); |
| } |
| } |
| |
| |
| document.addEventListener('DOMContentLoaded', initApp); |
| </script> |
| </body> |
| </html> |
|
|