banber / style.css
486CHD's picture
Upload style.css
04bdef9 verified
raw
history blame
30.4 kB
<!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 {
bottom: 60px !important;
}
.history-container {
padding-bottom: 220px !important;
}
@media (max-width: 768px) {
.input-section {
bottom: 0 !important;
}
.history-container {
padding-bottom: 160px !important;
}
}
</style>
</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 CONFIG = {
DB_NAME: 'BananaProDB_v3',
DB_VERSION: 1,
STORE_NAME: 'artworks',
MAX_IMAGES: 16,
ERROR_IMAGE: '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>'
};
// ============================================
// 全局状态管理
// ============================================
class AppState {
constructor() {
this.db = null;
this.currentImages = [];
this.galleryData = [];
this.currentModalItem = null;
this._listeners = new Map();
}
// 简单的事件系统
on(event, callback) {
if (!this._listeners.has(event)) {
this._listeners.set(event, new Set());
}
this._listeners.get(event).add(callback);
}
emit(event, data) {
if (this._listeners.has(event)) {
this._listeners.get(event).forEach(callback => callback(data));
}
}
}
const appState = new AppState();
// ============================================
// 工具函数
// ============================================
const Utils = {
// 防抖函数
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
// 创建元素的辅助函数
createElement(tag, attrs = {}, children = []) {
const el = document.createElement(tag);
Object.entries(attrs).forEach(([key, value]) => {
if (key === 'className') {
el.className = value;
} else if (key === 'textContent') {
el.textContent = value;
} else if (key.startsWith('on')) {
el.addEventListener(key.slice(2).toLowerCase(), value);
} else {
el.setAttribute(key, value);
}
});
children.forEach(child => {
if (typeof child === 'string') {
el.appendChild(document.createTextNode(child));
} else {
el.appendChild(child);
}
});
return el;
}
};
// ============================================
// IndexedDB 模块
// ============================================
class Database {
static async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(CONFIG.DB_NAME, CONFIG.DB_VERSION);
request.onerror = () => reject(new Error('数据库打开失败'));
request.onsuccess = () => {
appState.db = request.result;
resolve();
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(CONFIG.STORE_NAME)) {
const store = db.createObjectStore(CONFIG.STORE_NAME, {
keyPath: 'id',
autoIncrement: true
});
store.createIndex('timestamp', 'timestamp', { unique: false });
}
};
});
}
static async transaction(storeName, mode, operation) {
return new Promise((resolve, reject) => {
const tx = appState.db.transaction([storeName], mode);
const store = tx.objectStore(storeName);
tx.oncomplete = () => resolve(result);
tx.onerror = () => reject(new Error('事务执行失败'));
let result;
try {
result = operation(store);
} catch (error) {
reject(error);
}
});
}
static async save(item) {
return this.transaction(CONFIG.STORE_NAME, 'readwrite', (store) => {
const record = {
...item,
timestamp: Date.now()
};
return store.add(record);
});
}
static async getAll() {
return new Promise((resolve, reject) => {
const tx = appState.db.transaction([CONFIG.STORE_NAME], 'readonly');
const store = tx.objectStore(CONFIG.STORE_NAME);
const request = store.getAll();
request.onsuccess = () => {
const results = request.result.sort((a, b) => b.timestamp - a.timestamp);
resolve(results);
};
request.onerror = () => reject(new Error('读取数据失败'));
});
}
static async delete(id) {
return this.transaction(CONFIG.STORE_NAME, 'readwrite', (store) => {
return store.delete(id);
});
}
static async getById(id) {
return new Promise((resolve, reject) => {
const tx = appState.db.transaction([CONFIG.STORE_NAME], 'readonly');
const store = tx.objectStore(CONFIG.STORE_NAME);
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(new Error('获取数据失败'));
});
}
}
// ============================================
// 图片处理模块
// ============================================
class ImageHandler {
static fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
static async processFiles(files) {
const imageFiles = Array.from(files).filter(f => f.type.startsWith('image/'));
const promises = [];
for (const file of imageFiles) {
if (appState.currentImages.length >= CONFIG.MAX_IMAGES) {
alert(`最多只能上传 ${CONFIG.MAX_IMAGES} 张图片`);
break;
}
promises.push(this.fileToBase64(file));
}
try {
const results = await Promise.all(promises);
appState.currentImages.push(...results);
appState.emit('imagesChanged');
} catch (err) {
console.error('图片读取失败:', err);
alert('部分图片读取失败');
}
}
static removeAt(index) {
appState.currentImages.splice(index, 1);
appState.emit('imagesChanged');
}
static clear() {
appState.currentImages = [];
appState.emit('imagesChanged');
}
static setImages(images) {
appState.currentImages = images ? [...images] : [];
appState.emit('imagesChanged');
}
}
// ============================================
// UI组件基类
// ============================================
class Component {
constructor(container) {
this.container = typeof container === 'string'
? document.getElementById(container)
: container;
}
render() {
throw new Error('render method must be implemented');
}
}
// ============================================
// 预览条管理模块
// ============================================
class PreviewManager extends Component {
constructor() {
super('preview-bar');
this.uploadBtn = document.getElementById('upload-btn');
this.statusBar = document.getElementById('status-bar');
// 监听图片变化
appState.on('imagesChanged', () => this.render());
}
render() {
const images = appState.currentImages;
// 使用 DocumentFragment 减少重排
const fragment = document.createDocumentFragment();
if (images.length === 0) {
this.container.classList.remove('visible');
this.uploadBtn.classList.remove('active');
this.statusBar.textContent = 'Ready';
this.container.innerHTML = '';
return;
}
this.container.classList.add('visible');
this.uploadBtn.classList.add('active');
this.statusBar.textContent = `已选择 ${images.length}/${CONFIG.MAX_IMAGES} 张图片`;
images.forEach((imgData, index) => {
const wrapper = Utils.createElement('div', { className: 'thumb-wrapper' }, [
Utils.createElement('img', { src: imgData }),
Utils.createElement('div', {
className: 'thumb-remove',
textContent: '×',
onClick: (e) => {
e.stopPropagation();
ImageHandler.removeAt(index);
}
})
]);
fragment.appendChild(wrapper);
});
this.container.innerHTML = '';
this.container.appendChild(fragment);
}
}
// ============================================
// 画廊管理模块
// ============================================
class GalleryManager extends Component {
constructor() {
super('gallery');
this.renderDebounced = Utils.debounce(() => this.render(), 100);
}
async load() {
try {
appState.galleryData = await Database.getAll();
this.render();
} catch (err) {
console.error('加载画廊失败:', err);
alert('加载画廊失败,请刷新页面重试');
}
}
render() {
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 => {
fragment.appendChild(this.createCard(item));
});
this.container.innerHTML = '';
this.container.appendChild(fragment);
}
createCard(item) {
const card = Utils.createElement('div', {
className: 'history-item',
'data-id': item.id,
onClick: () => ModalManager.open(item)
});
// 参考图标记
if (item.inputImages?.length > 0) {
card.appendChild(Utils.createElement('div', {
className: 'item-badge',
textContent: `📎 ${item.inputImages.length}`
}));
}
// 主图片
const img = Utils.createElement('img', {
loading: 'lazy',
src: item.image,
onError: function() {
this.src = CONFIG.ERROR_IMAGE;
console.error('图片加载失败, ID:', item.id);
}
});
card.appendChild(img);
// 操作按钮
const actions = Utils.createElement('div', { className: 'item-actions' }, [
Utils.createElement('button', {
className: 'icon-btn',
innerHTML: '⬇',
onClick: (e) => {
e.stopPropagation();
this.downloadImage(item);
}
}),
Utils.createElement('button', {
className: 'icon-btn',
style: 'background: rgba(239,68,68,0.8)',
innerHTML: '🗑',
onClick: (e) => {
e.stopPropagation();
this.deleteItem(item.id);
}
})
]);
card.appendChild(actions);
return card;
}
downloadImage(item) {
const link = Utils.createElement('a', {
href: item.image,
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('删除失败,请重试');
}
}
}
// ============================================
// 弹窗管理模块
// ============================================
class ModalManager {
static 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.addEventListener('click', (e) => {
if (e.target === this.modal) this.close();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.modal.style.display === 'flex') {
this.close();
}
});
this.reuseBtn.addEventListener('click', () => this.reuse());
}
static open(item) {
appState.currentModalItem = item;
this.imgEl.src = item.image;
this.promptEl.textContent = item.prompt;
// 渲染参考图
const fragment = document.createDocumentFragment();
if (item.inputImages?.length > 0) {
item.inputImages.forEach(imgData => {
const thumb = Utils.createElement('img', {
className: 'ref-thumb',
src: imgData,
onClick: () => window.open(imgData, '_blank')
});
fragment.appendChild(thumb);
});
}
this.refsEl.innerHTML = '';
this.refsEl.appendChild(fragment);
this.modal.style.display = 'flex';
}
static close() {
this.modal.style.display = 'none';
appState.currentModalItem = null;
}
static reuse() {
const item = appState.currentModalItem;
if (!item) return;
const textarea = document.getElementById('prompt');
textarea.value = item.prompt;
this.adjustTextareaHeight(textarea);
ImageHandler.setImages(item.inputImages || []);
this.close();
textarea.focus();
}
static adjustTextareaHeight(textarea) {
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 'px';
}
}
// ============================================
// 生成请求模块
// ============================================
class Generator {
constructor() {
this.sendBtn = document.getElementById('send-btn');
this.textarea = document.getElementById('prompt');
this.isGenerating = false;
this.bindEvents();
}
bindEvents() {
this.sendBtn.addEventListener('click', () => this.generate());
// 输入框自动高度
this.textarea.addEventListener('input', () => {
ModalManager.adjustTextareaHeight(this.textarea);
});
// 回车发送
this.textarea.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey && !this.isGenerating) {
e.preventDefault();
this.generate();
}
});
}
setLoading(loading) {
this.isGenerating = loading;
this.sendBtn.classList.toggle('loading', loading);
this.sendBtn.disabled = loading;
}
async generate() {
const prompt = this.textarea.value.trim();
if (!prompt) {
alert('请输入提示词');
this.textarea.focus();
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
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (!data.success || !data.image?.startsWith('data:image')) {
throw new Error(data.message || '生成失败:返回的图片数据无效');
}
// 保存到数据库
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);
}
}
}
// ============================================
// 认证模块
// ============================================
class Auth {
static async check() {
try {
const res = await fetch('/api/check-auth');
const data = await res.json();
return data.authenticated === true;
} catch {
return false;
}
}
static async login(password) {
try {
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 === true;
} catch {
return false;
}
}
static unlock() {
document.getElementById('login-overlay').style.display = 'none';
const app = document.getElementById('app');
app.style.filter = 'none';
app.style.pointerEvents = 'all';
}
}
// ============================================
// 拖拽上传模块
// ============================================
class DragDrop {
constructor(dropZone) {
this.dropZone = document.getElementById(dropZone);
this.dragCounter = 0;
this.bindEvents();
}
bindEvents() {
const events = ['dragenter', 'dragover', 'dragleave', 'drop'];
events.forEach(event => {
this.dropZone.addEventListener(event, this.preventDefaults);
});
this.dropZone.addEventListener('dragenter', () => {
this.dragCounter++;
this.highlight();
});
this.dropZone.addEventListener('dragleave', () => {
this.dragCounter--;
if (this.dragCounter === 0) {
this.unhighlight();
}
});
this.dropZone.addEventListener('drop', async (e) => {
this.dragCounter = 0;
this.unhighlight();
const files = e.dataTransfer.files;
if (files.length > 0) {
await ImageHandler.processFiles(files);
}
});
}
preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
highlight() {
this.dropZone.style.borderColor = 'var(--accent-color)';
this.dropZone.style.background = 'rgba(59, 130, 246, 0.1)';
}
unhighlight() {
this.dropZone.style.borderColor = '';
this.dropZone.style.background = '';
}
}
// ============================================
// 文件选择模块
// ============================================
class FileSelector {
constructor() {
this.input = document.getElementById('file-input');
this.btn = document.getElementById('upload-btn');
this.bindEvents();
}
bindEvents() {
this.btn.addEventListener('click', () => this.input.click());
this.input.addEventListener('change', async () => {
if (this.input.files.length > 0) {
await ImageHandler.processFiles(this.input.files);
}
this.input.value = '';
});
}
}
// ============================================
// 全局实例
// ============================================
let previewManager, galleryManager, generator, dragDrop, fileSelector;
// ============================================
// 全局函数
// ============================================
async function doLogin() {
const pwd = document.getElementById('pwd').value.trim();
if (!pwd) {
document.getElementById('pwd').focus();
return;
}
const button = event.target;
button.disabled = true;
try {
const success = await Auth.login(pwd);
if (success) {
Auth.unlock();
await galleryManager.load();
} else {
alert('密码错误');
document.getElementById('pwd').value = '';
document.getElementById('pwd').focus();
}
} finally {
button.disabled = false;
}
}
function closeModal() {
ModalManager.close();
}
// ============================================
// 应用初始化
// ============================================
async function initApp() {
try {
// 初始化数据库
await Database.init();
// 初始化各模块
previewManager = new PreviewManager();
galleryManager = new GalleryManager();
ModalManager.init();
generator = new Generator();
dragDrop = new DragDrop('drop-zone');
fileSelector = new FileSelector();
// 检查认证状态
const isAuth = await Auth.check();
if (isAuth) {
Auth.unlock();
await galleryManager.load();
} else {
// 聚焦到密码输入框
document.getElementById('pwd').focus();
}
console.log('App initialized successfully');
} catch (err) {
console.error('App initialization failed:', err);
alert('应用初始化失败,请刷新页面重试');
}
}
// 启动应用
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initApp);
} else {
initApp();
}
</script>
</body>
</html>