banber / index.html
486CHD's picture
Upload index.html (#14)
b9c06e2 verified
<!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">
<style>
/* 顶部固定布局样式 */
.input-section {
position: fixed !important;
top: 0 !important;
bottom: auto !important;
left: 0 !important;
right: 0 !important;
transform: none !important;
width: 100% !important;
max-width: 100% !important;
border-radius: 0 !important;
z-index: 1000 !important;
box-shadow: 0 2px 20px rgba(0,0,0,0.3) !important;
}
header {
margin-top: 80px; /* 为输入区留空间 */
}
.history-container {
padding-bottom: 20px !important;
}
/* 调整输入区内部布局 */
.control-bar {
max-width: 1200px;
margin: 0 auto;
padding: 12px 20px !important;
}
.preview-bar {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
.preview-bar.visible {
padding: 12px 20px;
}
/* 动态调整header间距 */
.has-preview header {
margin-top: 180px;
}
@media (max-width: 768px) {
header {
margin-top: 70px;
}
.has-preview header {
margin-top: 150px;
}
}
</style>
</head>
<body>
<!-- 在预览栏显示时添加 has-preview 类 -->
<script>
// 监听预览栏变化
const observePreviewBar = () => {
const previewBar = document.getElementById('preview-bar');
const observer = new MutationObserver(() => {
if (previewBar.classList.contains('visible')) {
document.body.classList.add('has-preview');
} else {
document.body.classList.remove('has-preview');
}
});
observer.observe(previewBar, { attributes: true, attributeFilter: ['class'] });
};
document.addEventListener('DOMContentLoaded', observePreviewBar);
</script>
<!-- 登录界面 -->
<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;">
<!-- 顶部固定输入区 -->
<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>
<header>
<div>
<h2>创意画板 提示词用法库https://nanobananamaker.com/</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>
<section class="public-gallery-section glass-panel">
<div class="section-title">
<div>
<h3>✨ 社区创意画廊</h3>
<p class="section-subtitle" id="public-gallery-hint">分享你的作品,欣赏社区灵感</p>
</div>
<div class="section-actions">
<button class="icon-btn" id="refresh-public-gallery" title="刷新公共画廊" aria-label="刷新公共画廊"></button>
</div>
</div>
<div class="grid-layout public-grid" id="public-gallery"></div>
</section>
</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: [], // 当前上传的图片 Base64 数组
galleryData: [], // 个人画廊数据缓存
publicGalleryData: [], // 公共画廊数据缓存
currentModalItem: null // 当前弹窗显示的项目
};
const STORAGE_KEYS = {
publicGalleryTokens: 'BananaPro_PublicGallery_Tokens_v1'
};
const DEFAULT_PUBLIC_HINT = '分享你的作品,欣赏社区灵感';
const DB_NAME = 'BananaProDB_v3';
const DB_VERSION = 1;
const STORE_NAME = 'artworks';
// ============================================
// IndexedDB 模块(使用 Blob 存储避免 Base64 问题)
// ============================================
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, // 完整的 data: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 = {
// 文件转 Base64
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 compressImage(base64Data, maxWidth = 1280) {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
if (img.width <= maxWidth && base64Data.length < 500000) {
resolve(base64Data);
return;
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const scale = Math.min(1, maxWidth / img.width);
canvas.width = img.width * scale;
canvas.height = img.height * scale;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
resolve(canvas.toDataURL('image/jpeg', 0.8));
};
img.onerror = () => resolve(base64Data);
img.src = base64Data;
});
},
// 处理上传的文件
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 {
let base64 = await this.fileToBase64(file);
base64 = await this.compressImage(base64);
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 张图片`;
// 使用 DOM API 创建元素,避免 innerHTML 导致的编码问题
images.forEach((imgData, index) => {
const wrapper = document.createElement('div');
wrapper.className = 'thumb-wrapper';
const img = document.createElement('img');
img.src = imgData; // 直接设置 src,不经过字符串拼接
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;
}
const fragment = document.createDocumentFragment();
AppState.galleryData.forEach(item => {
const card = this.createCard(item);
fragment.appendChild(card);
});
this.container.appendChild(fragment);
},
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);
}
// 主图片 - 使用 DOM API 设置 src
const img = document.createElement('img');
img.loading = 'lazy';
img.decoding = 'async';
img.src = item.image;
img.alt = `作品 ${item.id}`;
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 shareBtn = document.createElement('button');
shareBtn.className = 'icon-btn share-btn';
shareBtn.textContent = '🌐';
shareBtn.title = '发布到公共画廊';
shareBtn.setAttribute('aria-label', '发布到公共画廊');
shareBtn.onclick = (e) => {
e.stopPropagation();
PublicGalleryManager.share(item, shareBtn);
};
const downloadBtn = document.createElement('button');
downloadBtn.className = 'icon-btn';
downloadBtn.textContent = '⬇';
downloadBtn.title = '下载图片';
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.textContent = '🗑';
deleteBtn.title = '删除图片';
deleteBtn.onclick = (e) => {
e.stopPropagation();
this.deleteItem(item.id);
};
actions.appendChild(shareBtn);
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();
};
// ESC 关闭
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 PublicGalleryManager = {
container: null,
refreshBtn: null,
hintEl: null,
tokens: {},
isLoading: false,
defaultHint: DEFAULT_PUBLIC_HINT,
hintTimer: null,
syncTimer: null,
syncInterval: 0,
loadTimeout: null,
lastFetchTime: 0,
minFetchInterval: 5000,
init() {
this.container = document.getElementById('public-gallery');
this.refreshBtn = document.getElementById('refresh-public-gallery');
this.hintEl = document.getElementById('public-gallery-hint');
this.tokens = this.loadTokens();
if (this.hintEl && this.hintEl.textContent) {
this.defaultHint = this.hintEl.textContent;
}
this.setHint(this.defaultHint);
if (this.refreshBtn) {
this.refreshBtn.onclick = () => this.fetch();
}
this.initialFetch();
},
async initialFetch() {
await this.fetch();
},
startRealtimeSync() {
// 实时同步已禁用 - 改为手动刷新提高性能
},
stopRealtimeSync() {
if (this.syncTimer) {
clearInterval(this.syncTimer);
this.syncTimer = null;
}
},
loadTokens() {
try {
const raw = localStorage.getItem(STORAGE_KEYS.publicGalleryTokens);
return raw ? JSON.parse(raw) : {};
} catch (error) {
console.warn('无法读取公共画廊令牌:', error);
return {};
}
},
saveTokens() {
try {
localStorage.setItem(STORAGE_KEYS.publicGalleryTokens, JSON.stringify(this.tokens));
} catch (error) {
console.warn('无法保存公共画廊令牌:', error);
}
},
setHint(message, mode = 'default') {
if (!this.hintEl) return;
this.hintEl.textContent = message;
this.hintEl.classList.remove('is-error', 'is-success');
if (mode === 'error') {
this.hintEl.classList.add('is-error');
} else if (mode === 'success') {
this.hintEl.classList.add('is-success');
}
clearTimeout(this.hintTimer);
if (mode !== 'default') {
this.hintTimer = setTimeout(() => {
this.setHint(this.defaultHint);
}, 4000);
}
},
setLoading(loading) {
this.isLoading = loading;
if (this.refreshBtn) {
this.refreshBtn.classList.toggle('spinning', loading);
this.refreshBtn.disabled = loading;
}
// 防止加载超时 - 30秒后强制停止
if (loading) {
clearTimeout(this.loadTimeout);
this.loadTimeout = setTimeout(() => {
if (this.isLoading) {
this.setLoading(false);
this.setHint('加载超时,请稍后重试', 'error');
}
}, 30000);
} else {
clearTimeout(this.loadTimeout);
}
},
async fetch() {
if (!this.container) return;
// 防止过于频繁的请求
const now = Date.now();
if (now - this.lastFetchTime < this.minFetchInterval) {
return;
}
this.lastFetchTime = now;
this.setLoading(true);
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 15000);
const res = await fetch('/api/public-gallery', {
signal: controller.signal
});
clearTimeout(timeoutId);
const data = await res.json();
if (!res.ok || !data.success) {
throw new Error(data.message || '无法加载公共画廊');
}
AppState.publicGalleryData = data.items || [];
this.render();
this.setHint(this.defaultHint);
} catch (error) {
console.error('公共画廊加载失败:', error);
let errorMessage = '加载失败,请稍后重试';
if (error.name === 'AbortError') {
errorMessage = '加载超时,请检查网络';
this.setHint('加载超时,请检查网络', 'error');
} else {
this.setHint(errorMessage, 'error');
}
if (AppState.publicGalleryData.length === 0) {
this.showEmpty(errorMessage);
}
} finally {
this.setLoading(false);
}
},
showEmpty(message) {
if (!this.container) return;
this.container.innerHTML = `
<div class="public-gallery-empty">
<div>🌌</div>
<p>${message}</p>
</div>
`;
},
render() {
if (!this.container) return;
if (!AppState.publicGalleryData || AppState.publicGalleryData.length === 0) {
this.showEmpty('还没有人分享作品,成为第一个吧!');
return;
}
this.container.innerHTML = '';
const fragment = document.createDocumentFragment();
AppState.publicGalleryData.forEach(item => {
const card = this.createCard(item);
fragment.appendChild(card);
});
this.container.appendChild(fragment);
},
createCard(item) {
const card = document.createElement('div');
card.className = 'public-card';
card.dataset.id = item.id;
const img = document.createElement('img');
img.loading = 'lazy';
img.decoding = 'async';
img.src = item.image;
img.alt = item.prompt || '创意作品';
card.appendChild(img);
const info = document.createElement('div');
info.className = 'public-card-info';
const promptText = document.createElement('p');
promptText.className = 'public-card-prompt';
promptText.textContent = item.prompt || '未提供提示词';
promptText.title = item.prompt;
info.appendChild(promptText);
const footer = document.createElement('div');
footer.className = 'public-card-footer';
const timeText = document.createElement('span');
timeText.textContent = this.formatTimestamp(item.timestamp);
footer.appendChild(timeText);
if (this.canDelete(item.id)) {
const deleteBtn = document.createElement('button');
deleteBtn.className = 'icon-btn small public-delete-btn';
deleteBtn.textContent = '🗑';
deleteBtn.title = '删除这张作品';
deleteBtn.onclick = (e) => {
e.stopPropagation();
this.delete(item.id);
};
footer.appendChild(deleteBtn);
}
info.appendChild(footer);
card.appendChild(info);
card.onclick = () => ModalManager.open(item);
return card;
},
formatTimestamp(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
if (Number.isNaN(date.getTime())) {
return '';
}
return date.toLocaleString();
},
canDelete(id) {
return true;
},
markOwned(id, token) {
if (!id || !token) return;
this.tokens[id] = token;
this.saveTokens();
},
removeOwnership(id) {
if (this.tokens[id]) {
delete this.tokens[id];
this.saveTokens();
}
},
async share(item, triggerBtn) {
if (!item) return;
const confirmShare = confirm('确认将这张作品发布到公共画廊?提示词将对所有人可见。');
if (!confirmShare) {
return;
}
if (triggerBtn) {
triggerBtn.disabled = true;
}
try {
const res = await fetch('/api/public-gallery', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: item.prompt,
image: item.image,
inputImages: item.inputImages || []
})
});
const data = await res.json();
if (!res.ok || !data.success) {
throw new Error(data.message || '发布失败');
}
AppState.publicGalleryData = [data.item, ...(AppState.publicGalleryData || [])];
this.markOwned(data.item.id, data.deleteToken);
this.render();
this.setHint('作品已发布至公共画廊', 'success');
} catch (error) {
console.error('发布到公共画廊失败:', error);
alert('发布失败: ' + error.message);
this.setHint('发布失败,请稍后重试', 'error');
} finally {
if (triggerBtn) {
triggerBtn.disabled = false;
}
}
},
async delete(id) {
const confirmDelete = confirm('确定要删除这张公共画廊作品吗?');
if (!confirmDelete) {
return;
}
try {
const res = await fetch(`/api/public-gallery/${id}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' }
});
const data = await res.json();
if (!res.ok || !data.success) {
throw new Error(data.message || '删除失败');
}
AppState.publicGalleryData = AppState.publicGalleryData.filter(item => item.id !== id);
this.removeOwnership(id);
this.render();
this.setHint('作品已删除', 'success');
} catch (error) {
console.error('删除公共画廊作品失败:', error);
alert('删除失败: ' + error.message);
this.setHint('删除失败,请稍后再试', 'error');
}
}
};
// ============================================
// 生成请求模块
// ============================================
const Generator = {
sendBtn: null,
textarea: null,
statusBar: null,
isGenerating: false,
generateTimeout: null,
init() {
this.sendBtn = document.getElementById('send-btn');
this.textarea = document.getElementById('prompt');
this.statusBar = document.getElementById('status-bar');
this.sendBtn.onclick = () => this.generate();
// 输入框自动高度
this.textarea.addEventListener('input', () => {
this.textarea.style.height = 'auto';
this.textarea.style.height = this.textarea.scrollHeight + 'px';
});
// 回车发送(Shift+Enter 换行)
this.textarea.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.generate();
}
});
},
setLoading(loading) {
this.isGenerating = loading;
if (loading) {
this.sendBtn.classList.add('loading');
this.sendBtn.disabled = true;
this.statusBar.textContent = '⏳ 生成中...';
// 防止生成超时 - 3分钟后强制停止
clearTimeout(this.generateTimeout);
this.generateTimeout = setTimeout(() => {
if (this.isGenerating) {
this.setLoading(false);
this.statusBar.textContent = '生成超时,请重试';
alert('生成超时,请检查网络后重试');
}
}, 180000);
} else {
this.sendBtn.classList.remove('loading');
this.sendBtn.disabled = false;
this.statusBar.textContent = 'Ready';
clearTimeout(this.generateTimeout);
}
},
async generate() {
if (this.isGenerating) return;
const prompt = this.textarea.value.trim();
if (!prompt) {
alert('请输入提示词');
return;
}
this.setLoading(true);
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 120000);
const response = await fetch('/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: prompt,
images: AppState.currentImages
}),
signal: controller.signal
});
clearTimeout(timeoutId);
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();
this.statusBar.textContent = '✅ 生成成功';
} catch (err) {
console.error('生成失败:', err);
let errorMessage = '生成失败,请重试';
if (err.message.includes('API认证失败')) {
errorMessage = 'API认证失败,请联系管理员检查API配置';
} else if (err.message.includes('无法连接到API服务器')) {
errorMessage = '无法连接到API服务器,请检查网络连接';
} else if (err.message.includes('API请求超时')) {
errorMessage = 'API请求超时,请稍后重试';
} else if (err.message) {
errorMessage = err.message;
}
if (err.name === 'AbortError') {
this.statusBar.textContent = '⚠️ 生成超时';
alert('生成超时,请检查网络后重试');
} else {
this.statusBar.textContent = '❌ 生成失败';
alert(errorMessage);
}
} finally {
this.setLoading(false);
setTimeout(() => {
if (this.statusBar.textContent !== 'Ready') {
this.statusBar.textContent = 'Ready';
}
}, 3000);
}
}
};
// ============================================
// 认证模块
// ============================================
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 = ''; // 重置以便重复选择
};
}
};
// ============================================
// 全局函数(供 HTML onclick 调用)
// ============================================
async function loadWorkspaceData() {
try {
await Promise.all([
GalleryManager.load(),
PublicGalleryManager.fetch()
]);
} catch (error) {
console.error('加载画廊数据失败:', error);
}
}
async function doLogin() {
const pwd = document.getElementById('pwd').value;
if (!pwd) return;
const success = await Auth.login(pwd);
if (success) {
Auth.unlock();
await loadWorkspaceData();
} else {
alert('密码错误');
}
}
function closeModal() {
ModalManager.close();
}
// ============================================
// 应用初始化
// ============================================
async function initApp() {
try {
// 初始化数据库
await Database.init();
// 初始化各模块
PreviewManager.init();
GalleryManager.init();
ModalManager.init();
PublicGalleryManager.init();
Generator.init();
DragDrop.init();
FileSelector.init();
// 检查认证状态
const isAuth = await Auth.check();
if (isAuth) {
Auth.unlock();
await loadWorkspaceData();
}
console.log('App initialized successfully');
} catch (err) {
console.error('App initialization failed:', err);
}
}
// 启动应用
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initApp);
} else {
initApp();
}
</script>
</body>
</html>