486CHD commited on
Commit
5a75e2c
·
verified ·
1 Parent(s): baf9c6e

Upload 2 files

Browse files
Files changed (2) hide show
  1. index.html +84 -13
  2. style.css +312 -833
index.html CHANGED
@@ -5,8 +5,79 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
  <title>Banana Pro AI</title>
7
  <link rel="stylesheet" href="style.css">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  </head>
9
  <body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  <!-- 登录界面 -->
12
  <div id="login-overlay">
@@ -20,19 +91,7 @@
20
 
21
  <!-- 主应用 -->
22
  <div class="app-container" id="app" style="filter: blur(10px); pointer-events: none;">
23
- <header>
24
- <div>
25
- <h2>创意画板</h2>
26
- <div style="font-size: 12px; color: var(--text-sub);" id="status-bar">Ready</div>
27
- </div>
28
- </header>
29
-
30
- <!-- 历史画廊 -->
31
- <div class="history-container">
32
- <div class="grid-layout" id="gallery"></div>
33
- </div>
34
-
35
- <!-- 底部输入与预览组合区 -->
36
  <div class="input-section glass-panel" id="drop-zone">
37
  <!-- 上部分:预览条 -->
38
  <div class="preview-bar" id="preview-bar"></div>
@@ -48,6 +107,18 @@
48
  </div>
49
  </div>
50
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  <input type="file" id="file-input" multiple accept="image/*" hidden>
52
  </div>
53
 
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
  <title>Banana Pro AI</title>
7
  <link rel="stylesheet" href="style.css">
8
+ <style>
9
+ /* 顶部固定布局 */
10
+ .input-section {
11
+ position: fixed !important;
12
+ top: 0 !important;
13
+ bottom: auto !important;
14
+ left: 0 !important;
15
+ right: 0 !important;
16
+ transform: none !important;
17
+ width: 100% !important;
18
+ max-width: 100% !important;
19
+ border-radius: 0 !important;
20
+ z-index: 1000 !important;
21
+ box-shadow: 0 2px 20px rgba(0,0,0,0.3) !important;
22
+ }
23
+
24
+ header {
25
+ margin-top: 80px; /* 为输入区留空间 */
26
+ }
27
+
28
+ .history-container {
29
+ padding-bottom: 20px !important;
30
+ }
31
+
32
+ /* 调整输入区内部布局 */
33
+ .control-bar {
34
+ max-width: 1200px;
35
+ margin: 0 auto;
36
+ padding: 12px 20px !important;
37
+ }
38
+
39
+ .preview-bar {
40
+ max-width: 1200px;
41
+ margin: 0 auto;
42
+ padding: 0 20px;
43
+ }
44
+
45
+ .preview-bar.visible {
46
+ padding: 12px 20px;
47
+ }
48
+
49
+ /* 动态调整header间距 */
50
+ .has-preview header {
51
+ margin-top: 180px;
52
+ }
53
+
54
+ @media (max-width: 768px) {
55
+ header {
56
+ margin-top: 70px;
57
+ }
58
+ .has-preview header {
59
+ margin-top: 150px;
60
+ }
61
+ }
62
+ </style>
63
  </head>
64
  <body>
65
+ <!-- 在预览栏显示时添加 has-preview 类 -->
66
+ <script>
67
+ // 监听预览栏变化
68
+ const observePreviewBar = () => {
69
+ const previewBar = document.getElementById('preview-bar');
70
+ const observer = new MutationObserver(() => {
71
+ if (previewBar.classList.contains('visible')) {
72
+ document.body.classList.add('has-preview');
73
+ } else {
74
+ document.body.classList.remove('has-preview');
75
+ }
76
+ });
77
+ observer.observe(previewBar, { attributes: true, attributeFilter: ['class'] });
78
+ };
79
+ document.addEventListener('DOMContentLoaded', observePreviewBar);
80
+ </script>
81
 
82
  <!-- 登录界面 -->
83
  <div id="login-overlay">
 
91
 
92
  <!-- 主应用 -->
93
  <div class="app-container" id="app" style="filter: blur(10px); pointer-events: none;">
94
+ <!-- 顶部固定输入区 -->
 
 
 
 
 
 
 
 
 
 
 
 
95
  <div class="input-section glass-panel" id="drop-zone">
96
  <!-- 上部分:预览条 -->
97
  <div class="preview-bar" id="preview-bar"></div>
 
107
  </div>
108
  </div>
109
 
110
+ <header>
111
+ <div>
112
+ <h2>创意画板</h2>
113
+ <div style="font-size: 12px; color: var(--text-sub);" id="status-bar">Ready</div>
114
+ </div>
115
+ </header>
116
+
117
+ <!-- 历史画廊 -->
118
+ <div class="history-container">
119
+ <div class="grid-layout" id="gallery"></div>
120
+ </div>
121
+
122
  <input type="file" id="file-input" multiple accept="image/*" hidden>
123
  </div>
124
 
style.css CHANGED
@@ -1,845 +1,324 @@
1
- <!DOCTYPE html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
- <title>Banana Pro AI</title>
7
- <link rel="stylesheet" href="style.css">
8
- <style>
9
- /* 仅添加需要覆盖的样式 */
10
- .input-section {
11
- bottom: 60px !important;
12
- }
13
-
14
- .history-container {
15
- padding-bottom: 220px !important;
16
- }
17
-
18
- @media (max-width: 768px) {
19
- .input-section {
20
- bottom: 0 !important;
21
- }
22
- .history-container {
23
- padding-bottom: 160px !important;
24
- }
25
- }
26
- </style>
27
- </head>
28
- <body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
- <!-- 登录界面 -->
31
- <div id="login-overlay">
32
- <div class="login-card glass-panel">
33
- <h1 style="font-size: 2rem;">🍌</h1>
34
- <h3 style="margin: 10px 0; color: #cbd5e1;">Banana Pro</h3>
35
- <input type="password" id="pwd" placeholder="Password" onkeypress="if(event.key==='Enter')doLogin()">
36
- <button class="btn-3d" onclick="doLogin()" style="width: 100%; padding: 10px;">Unlock</button>
37
- </div>
38
- </div>
39
-
40
- <!-- 主应用 -->
41
- <div class="app-container" id="app" style="filter: blur(10px); pointer-events: none;">
42
- <header>
43
- <div>
44
- <h2>创意画板</h2>
45
- <div style="font-size: 12px; color: var(--text-sub);" id="status-bar">Ready</div>
46
- </div>
47
- </header>
48
-
49
- <!-- 历史画廊 -->
50
- <div class="history-container">
51
- <div class="grid-layout" id="gallery"></div>
52
- </div>
53
-
54
- <!-- 底部输入与预览组合区 -->
55
- <div class="input-section glass-panel" id="drop-zone">
56
- <!-- 上部分:预览条 -->
57
- <div class="preview-bar" id="preview-bar"></div>
58
-
59
- <!-- 下部分:操作栏 -->
60
- <div class="control-bar">
61
- <button class="upload-trigger" id="upload-btn" title="上传参考图">📷</button>
62
- <textarea id="prompt" class="main-input" rows="1" placeholder="描述画面... (支持拖拽图片)"></textarea>
63
- <button class="btn-3d send-btn" id="send-btn">
64
- <span>生成</span>
65
- <div class="loader"></div>
66
- </button>
67
- </div>
68
- </div>
69
-
70
- <input type="file" id="file-input" multiple accept="image/*" hidden>
71
- </div>
72
-
73
- <!-- 详情弹窗 -->
74
- <div class="modal" id="modal">
75
- <div class="modal-content glass-panel">
76
- <button class="close-modal" onclick="closeModal()">×</button>
77
- <div class="modal-img-area">
78
- <img id="m-img" src="">
79
- </div>
80
- <div class="modal-footer">
81
- <div class="input-refs" id="m-refs"></div>
82
- <div class="prompt-display" id="m-prompt"></div>
83
- <button class="btn-3d" id="m-reuse" style="width: 100%; padding: 12px;">
84
- 🎨 复用参数与图片
85
- </button>
86
- </div>
87
- </div>
88
- </div>
89
-
90
- <script>
91
- // ============================================
92
- // 应用配置
93
- // ============================================
94
- const CONFIG = {
95
- DB_NAME: 'BananaProDB_v3',
96
- DB_VERSION: 1,
97
- STORE_NAME: 'artworks',
98
- MAX_IMAGES: 16,
99
- 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>'
100
- };
101
-
102
- // ============================================
103
- // 全局状态管理
104
- // ============================================
105
- class AppState {
106
- constructor() {
107
- this.db = null;
108
- this.currentImages = [];
109
- this.galleryData = [];
110
- this.currentModalItem = null;
111
- this._listeners = new Map();
112
- }
113
-
114
- // 简单的事件系统
115
- on(event, callback) {
116
- if (!this._listeners.has(event)) {
117
- this._listeners.set(event, new Set());
118
- }
119
- this._listeners.get(event).add(callback);
120
- }
121
-
122
- emit(event, data) {
123
- if (this._listeners.has(event)) {
124
- this._listeners.get(event).forEach(callback => callback(data));
125
- }
126
- }
127
  }
128
 
129
- const appState = new AppState();
130
-
131
- // ============================================
132
- // 工具函数
133
- // ============================================
134
- const Utils = {
135
- // 防抖函数
136
- debounce(func, wait) {
137
- let timeout;
138
- return function executedFunction(...args) {
139
- const later = () => {
140
- clearTimeout(timeout);
141
- func(...args);
142
- };
143
- clearTimeout(timeout);
144
- timeout = setTimeout(later, wait);
145
- };
146
- },
147
-
148
- // 创建元素的辅助函数
149
- createElement(tag, attrs = {}, children = []) {
150
- const el = document.createElement(tag);
151
- Object.entries(attrs).forEach(([key, value]) => {
152
- if (key === 'className') {
153
- el.className = value;
154
- } else if (key === 'textContent') {
155
- el.textContent = value;
156
- } else if (key.startsWith('on')) {
157
- el.addEventListener(key.slice(2).toLowerCase(), value);
158
- } else {
159
- el.setAttribute(key, value);
160
- }
161
- });
162
- children.forEach(child => {
163
- if (typeof child === 'string') {
164
- el.appendChild(document.createTextNode(child));
165
- } else {
166
- el.appendChild(child);
167
- }
168
- });
169
- return el;
170
- }
171
- };
172
-
173
- // ============================================
174
- // IndexedDB 模块
175
- // ============================================
176
- class Database {
177
- static async init() {
178
- return new Promise((resolve, reject) => {
179
- const request = indexedDB.open(CONFIG.DB_NAME, CONFIG.DB_VERSION);
180
-
181
- request.onerror = () => reject(new Error('数据库打开失败'));
182
- request.onsuccess = () => {
183
- appState.db = request.result;
184
- resolve();
185
- };
186
-
187
- request.onupgradeneeded = (event) => {
188
- const db = event.target.result;
189
- if (!db.objectStoreNames.contains(CONFIG.STORE_NAME)) {
190
- const store = db.createObjectStore(CONFIG.STORE_NAME, {
191
- keyPath: 'id',
192
- autoIncrement: true
193
- });
194
- store.createIndex('timestamp', 'timestamp', { unique: false });
195
- }
196
- };
197
- });
198
- }
199
-
200
- static async transaction(storeName, mode, operation) {
201
- return new Promise((resolve, reject) => {
202
- const tx = appState.db.transaction([storeName], mode);
203
- const store = tx.objectStore(storeName);
204
-
205
- tx.oncomplete = () => resolve(result);
206
- tx.onerror = () => reject(new Error('事务执行失败'));
207
-
208
- let result;
209
- try {
210
- result = operation(store);
211
- } catch (error) {
212
- reject(error);
213
- }
214
- });
215
- }
216
-
217
- static async save(item) {
218
- return this.transaction(CONFIG.STORE_NAME, 'readwrite', (store) => {
219
- const record = {
220
- ...item,
221
- timestamp: Date.now()
222
- };
223
- return store.add(record);
224
- });
225
- }
226
-
227
- static async getAll() {
228
- return new Promise((resolve, reject) => {
229
- const tx = appState.db.transaction([CONFIG.STORE_NAME], 'readonly');
230
- const store = tx.objectStore(CONFIG.STORE_NAME);
231
- const request = store.getAll();
232
-
233
- request.onsuccess = () => {
234
- const results = request.result.sort((a, b) => b.timestamp - a.timestamp);
235
- resolve(results);
236
- };
237
- request.onerror = () => reject(new Error('读取数据失败'));
238
- });
239
- }
240
-
241
- static async delete(id) {
242
- return this.transaction(CONFIG.STORE_NAME, 'readwrite', (store) => {
243
- return store.delete(id);
244
- });
245
- }
246
-
247
- static async getById(id) {
248
- return new Promise((resolve, reject) => {
249
- const tx = appState.db.transaction([CONFIG.STORE_NAME], 'readonly');
250
- const store = tx.objectStore(CONFIG.STORE_NAME);
251
- const request = store.get(id);
252
-
253
- request.onsuccess = () => resolve(request.result);
254
- request.onerror = () => reject(new Error('获取数据失败'));
255
- });
256
- }
257
  }
258
 
259
- // ============================================
260
- // 图片处理模块
261
- // ============================================
262
- class ImageHandler {
263
- static fileToBase64(file) {
264
- return new Promise((resolve, reject) => {
265
- const reader = new FileReader();
266
- reader.onload = () => resolve(reader.result);
267
- reader.onerror = reject;
268
- reader.readAsDataURL(file);
269
- });
270
- }
271
-
272
- static async processFiles(files) {
273
- const imageFiles = Array.from(files).filter(f => f.type.startsWith('image/'));
274
- const promises = [];
275
-
276
- for (const file of imageFiles) {
277
- if (appState.currentImages.length >= CONFIG.MAX_IMAGES) {
278
- alert(`最多只能上传 ${CONFIG.MAX_IMAGES} 张图片`);
279
- break;
280
- }
281
- promises.push(this.fileToBase64(file));
282
- }
283
-
284
- try {
285
- const results = await Promise.all(promises);
286
- appState.currentImages.push(...results);
287
- appState.emit('imagesChanged');
288
- } catch (err) {
289
- console.error('图片读取失败:', err);
290
- alert('部分图片读取失败');
291
- }
292
- }
293
-
294
- static removeAt(index) {
295
- appState.currentImages.splice(index, 1);
296
- appState.emit('imagesChanged');
297
- }
298
-
299
- static clear() {
300
- appState.currentImages = [];
301
- appState.emit('imagesChanged');
302
- }
303
-
304
- static setImages(images) {
305
- appState.currentImages = images ? [...images] : [];
306
- appState.emit('imagesChanged');
307
- }
308
- }
309
-
310
- // ============================================
311
- // UI组件基类
312
- // ============================================
313
- class Component {
314
- constructor(container) {
315
- this.container = typeof container === 'string'
316
- ? document.getElementById(container)
317
- : container;
318
- }
319
-
320
- render() {
321
- throw new Error('render method must be implemented');
322
- }
323
- }
324
-
325
- // ============================================
326
- // 预览条管理模块
327
- // ============================================
328
- class PreviewManager extends Component {
329
- constructor() {
330
- super('preview-bar');
331
- this.uploadBtn = document.getElementById('upload-btn');
332
- this.statusBar = document.getElementById('status-bar');
333
-
334
- // 监听图片变化
335
- appState.on('imagesChanged', () => this.render());
336
- }
337
-
338
- render() {
339
- const images = appState.currentImages;
340
-
341
- // 使用 DocumentFragment 减少重排
342
- const fragment = document.createDocumentFragment();
343
-
344
- if (images.length === 0) {
345
- this.container.classList.remove('visible');
346
- this.uploadBtn.classList.remove('active');
347
- this.statusBar.textContent = 'Ready';
348
- this.container.innerHTML = '';
349
- return;
350
- }
351
-
352
- this.container.classList.add('visible');
353
- this.uploadBtn.classList.add('active');
354
- this.statusBar.textContent = `已选择 ${images.length}/${CONFIG.MAX_IMAGES} 张图片`;
355
-
356
- images.forEach((imgData, index) => {
357
- const wrapper = Utils.createElement('div', { className: 'thumb-wrapper' }, [
358
- Utils.createElement('img', { src: imgData }),
359
- Utils.createElement('div', {
360
- className: 'thumb-remove',
361
- textContent: '×',
362
- onClick: (e) => {
363
- e.stopPropagation();
364
- ImageHandler.removeAt(index);
365
- }
366
- })
367
- ]);
368
- fragment.appendChild(wrapper);
369
- });
370
-
371
- this.container.innerHTML = '';
372
- this.container.appendChild(fragment);
373
- }
374
- }
375
-
376
- // ============================================
377
- // 画廊管理模块
378
- // ============================================
379
- class GalleryManager extends Component {
380
- constructor() {
381
- super('gallery');
382
- this.renderDebounced = Utils.debounce(() => this.render(), 100);
383
- }
384
-
385
- async load() {
386
- try {
387
- appState.galleryData = await Database.getAll();
388
- this.render();
389
- } catch (err) {
390
- console.error('加载画廊失败:', err);
391
- alert('加载画廊失败,请刷新页面重试');
392
- }
393
- }
394
-
395
- render() {
396
- if (appState.galleryData.length === 0) {
397
- this.container.innerHTML = `
398
- <div style="grid-column: 1/-1; text-align: center; color: var(--text-sub); padding: 60px 20px;">
399
- <div style="font-size: 48px; margin-bottom: 10px;">🎨</div>
400
- <div>暂无作品,开始创作吧!</div>
401
- </div>
402
- `;
403
- return;
404
- }
405
-
406
- const fragment = document.createDocumentFragment();
407
- appState.galleryData.forEach(item => {
408
- fragment.appendChild(this.createCard(item));
409
- });
410
-
411
- this.container.innerHTML = '';
412
- this.container.appendChild(fragment);
413
- }
414
-
415
- createCard(item) {
416
- const card = Utils.createElement('div', {
417
- className: 'history-item',
418
- 'data-id': item.id,
419
- onClick: () => ModalManager.open(item)
420
- });
421
-
422
- // 参考图标记
423
- if (item.inputImages?.length > 0) {
424
- card.appendChild(Utils.createElement('div', {
425
- className: 'item-badge',
426
- textContent: `📎 ${item.inputImages.length}`
427
- }));
428
- }
429
-
430
- // 主图片
431
- const img = Utils.createElement('img', {
432
- loading: 'lazy',
433
- src: item.image,
434
- onError: function() {
435
- this.src = CONFIG.ERROR_IMAGE;
436
- console.error('图片加载失败, ID:', item.id);
437
- }
438
- });
439
- card.appendChild(img);
440
-
441
- // 操作按钮
442
- const actions = Utils.createElement('div', { className: 'item-actions' }, [
443
- Utils.createElement('button', {
444
- className: 'icon-btn',
445
- innerHTML: '⬇',
446
- onClick: (e) => {
447
- e.stopPropagation();
448
- this.downloadImage(item);
449
- }
450
- }),
451
- Utils.createElement('button', {
452
- className: 'icon-btn',
453
- style: 'background: rgba(239,68,68,0.8)',
454
- innerHTML: '🗑',
455
- onClick: (e) => {
456
- e.stopPropagation();
457
- this.deleteItem(item.id);
458
- }
459
- })
460
- ]);
461
- card.appendChild(actions);
462
-
463
- return card;
464
- }
465
-
466
- downloadImage(item) {
467
- const link = Utils.createElement('a', {
468
- href: item.image,
469
- download: `banana-pro-${item.id}-${Date.now()}.png`
470
- });
471
- document.body.appendChild(link);
472
- link.click();
473
- document.body.removeChild(link);
474
- }
475
-
476
- async deleteItem(id) {
477
- if (!confirm('确定要删除这张图片吗?')) return;
478
-
479
- try {
480
- await Database.delete(id);
481
- await this.load();
482
- } catch (err) {
483
- console.error('删除失败:', err);
484
- alert('删除失败,请重试');
485
- }
486
- }
487
- }
488
-
489
- // ============================================
490
- // 弹窗管理模块
491
- // ============================================
492
- class ModalManager {
493
- static init() {
494
- this.modal = document.getElementById('modal');
495
- this.imgEl = document.getElementById('m-img');
496
- this.promptEl = document.getElementById('m-prompt');
497
- this.refsEl = document.getElementById('m-refs');
498
- this.reuseBtn = document.getElementById('m-reuse');
499
-
500
- // 事件绑定
501
- this.modal.addEventListener('click', (e) => {
502
- if (e.target === this.modal) this.close();
503
- });
504
-
505
- document.addEventListener('keydown', (e) => {
506
- if (e.key === 'Escape' && this.modal.style.display === 'flex') {
507
- this.close();
508
- }
509
- });
510
-
511
- this.reuseBtn.addEventListener('click', () => this.reuse());
512
- }
513
-
514
- static open(item) {
515
- appState.currentModalItem = item;
516
-
517
- this.imgEl.src = item.image;
518
- this.promptEl.textContent = item.prompt;
519
-
520
- // 渲染参考图
521
- const fragment = document.createDocumentFragment();
522
- if (item.inputImages?.length > 0) {
523
- item.inputImages.forEach(imgData => {
524
- const thumb = Utils.createElement('img', {
525
- className: 'ref-thumb',
526
- src: imgData,
527
- onClick: () => window.open(imgData, '_blank')
528
- });
529
- fragment.appendChild(thumb);
530
- });
531
- }
532
- this.refsEl.innerHTML = '';
533
- this.refsEl.appendChild(fragment);
534
-
535
- this.modal.style.display = 'flex';
536
- }
537
-
538
- static close() {
539
- this.modal.style.display = 'none';
540
- appState.currentModalItem = null;
541
- }
542
-
543
- static reuse() {
544
- const item = appState.currentModalItem;
545
- if (!item) return;
546
-
547
- const textarea = document.getElementById('prompt');
548
- textarea.value = item.prompt;
549
- this.adjustTextareaHeight(textarea);
550
-
551
- ImageHandler.setImages(item.inputImages || []);
552
-
553
- this.close();
554
- textarea.focus();
555
- }
556
-
557
- static adjustTextareaHeight(textarea) {
558
- textarea.style.height = 'auto';
559
- textarea.style.height = textarea.scrollHeight + 'px';
560
- }
561
- }
562
-
563
- // ============================================
564
- // 生成请求模块
565
- // ============================================
566
- class Generator {
567
- constructor() {
568
- this.sendBtn = document.getElementById('send-btn');
569
- this.textarea = document.getElementById('prompt');
570
- this.isGenerating = false;
571
-
572
- this.bindEvents();
573
- }
574
-
575
- bindEvents() {
576
- this.sendBtn.addEventListener('click', () => this.generate());
577
-
578
- // 输入框自动高度
579
- this.textarea.addEventListener('input', () => {
580
- ModalManager.adjustTextareaHeight(this.textarea);
581
- });
582
-
583
- // 回车发送
584
- this.textarea.addEventListener('keydown', (e) => {
585
- if (e.key === 'Enter' && !e.shiftKey && !this.isGenerating) {
586
- e.preventDefault();
587
- this.generate();
588
- }
589
- });
590
- }
591
-
592
- setLoading(loading) {
593
- this.isGenerating = loading;
594
- this.sendBtn.classList.toggle('loading', loading);
595
- this.sendBtn.disabled = loading;
596
- }
597
-
598
- async generate() {
599
- const prompt = this.textarea.value.trim();
600
- if (!prompt) {
601
- alert('请输入提示词');
602
- this.textarea.focus();
603
- return;
604
- }
605
-
606
- this.setLoading(true);
607
-
608
- try {
609
- const response = await fetch('/api/generate', {
610
- method: 'POST',
611
- headers: { 'Content-Type': 'application/json' },
612
- body: JSON.stringify({
613
- prompt: prompt,
614
- images: appState.currentImages
615
- })
616
- });
617
-
618
- if (!response.ok) {
619
- throw new Error(`HTTP error! status: ${response.status}`);
620
- }
621
-
622
- const data = await response.json();
623
-
624
- if (!data.success || !data.image?.startsWith('data:image')) {
625
- throw new Error(data.message || '生成失败:返回的图片数据无效');
626
- }
627
-
628
- // 保存到数据库
629
- await Database.save({
630
- prompt: prompt,
631
- image: data.image,
632
- inputImages: [...appState.currentImages]
633
- });
634
-
635
- // 刷新画廊
636
- await galleryManager.load();
637
-
638
- // 清空输入
639
- this.textarea.value = '';
640
- this.textarea.style.height = 'auto';
641
- ImageHandler.clear();
642
-
643
- } catch (err) {
644
- console.error('生成失败:', err);
645
- alert(`生成失败: ${err.message}`);
646
- } finally {
647
- this.setLoading(false);
648
- }
649
- }
650
- }
651
-
652
- // ============================================
653
- // 认证模块
654
- // ============================================
655
- class Auth {
656
- static async check() {
657
- try {
658
- const res = await fetch('/api/check-auth');
659
- const data = await res.json();
660
- return data.authenticated === true;
661
- } catch {
662
- return false;
663
- }
664
- }
665
-
666
- static async login(password) {
667
- try {
668
- const res = await fetch('/api/login', {
669
- method: 'POST',
670
- headers: { 'Content-Type': 'application/json' },
671
- body: JSON.stringify({ password })
672
- });
673
- const data = await res.json();
674
- return data.success === true;
675
- } catch {
676
- return false;
677
- }
678
- }
679
-
680
- static unlock() {
681
- document.getElementById('login-overlay').style.display = 'none';
682
- const app = document.getElementById('app');
683
- app.style.filter = 'none';
684
- app.style.pointerEvents = 'all';
685
- }
686
- }
687
-
688
- // ============================================
689
- // 拖拽上传模块
690
- // ============================================
691
- class DragDrop {
692
- constructor(dropZone) {
693
- this.dropZone = document.getElementById(dropZone);
694
- this.dragCounter = 0;
695
- this.bindEvents();
696
- }
697
-
698
- bindEvents() {
699
- const events = ['dragenter', 'dragover', 'dragleave', 'drop'];
700
-
701
- events.forEach(event => {
702
- this.dropZone.addEventListener(event, this.preventDefaults);
703
- });
704
-
705
- this.dropZone.addEventListener('dragenter', () => {
706
- this.dragCounter++;
707
- this.highlight();
708
- });
709
-
710
- this.dropZone.addEventListener('dragleave', () => {
711
- this.dragCounter--;
712
- if (this.dragCounter === 0) {
713
- this.unhighlight();
714
- }
715
- });
716
-
717
- this.dropZone.addEventListener('drop', async (e) => {
718
- this.dragCounter = 0;
719
- this.unhighlight();
720
-
721
- const files = e.dataTransfer.files;
722
- if (files.length > 0) {
723
- await ImageHandler.processFiles(files);
724
- }
725
- });
726
- }
727
-
728
- preventDefaults(e) {
729
- e.preventDefault();
730
- e.stopPropagation();
731
- }
732
-
733
- highlight() {
734
- this.dropZone.style.borderColor = 'var(--accent-color)';
735
- this.dropZone.style.background = 'rgba(59, 130, 246, 0.1)';
736
- }
737
-
738
- unhighlight() {
739
- this.dropZone.style.borderColor = '';
740
- this.dropZone.style.background = '';
741
- }
742
- }
743
-
744
- // ============================================
745
- // 文件选择模块
746
- // ============================================
747
- class FileSelector {
748
- constructor() {
749
- this.input = document.getElementById('file-input');
750
- this.btn = document.getElementById('upload-btn');
751
-
752
- this.bindEvents();
753
- }
754
-
755
- bindEvents() {
756
- this.btn.addEventListener('click', () => this.input.click());
757
-
758
- this.input.addEventListener('change', async () => {
759
- if (this.input.files.length > 0) {
760
- await ImageHandler.processFiles(this.input.files);
761
- }
762
- this.input.value = '';
763
- });
764
- }
765
- }
766
-
767
- // ============================================
768
- // 全局实例
769
- // ============================================
770
- let previewManager, galleryManager, generator, dragDrop, fileSelector;
771
-
772
- // ============================================
773
- // 全局函数
774
- // ============================================
775
- async function doLogin() {
776
- const pwd = document.getElementById('pwd').value.trim();
777
- if (!pwd) {
778
- document.getElementById('pwd').focus();
779
- return;
780
- }
781
-
782
- const button = event.target;
783
- button.disabled = true;
784
-
785
- try {
786
- const success = await Auth.login(pwd);
787
- if (success) {
788
- Auth.unlock();
789
- await galleryManager.load();
790
- } else {
791
- alert('密码错误');
792
- document.getElementById('pwd').value = '';
793
- document.getElementById('pwd').focus();
794
- }
795
- } finally {
796
- button.disabled = false;
797
- }
798
  }
799
 
800
- function closeModal() {
801
- ModalManager.close();
 
802
  }
803
 
804
- // ============================================
805
- // 应用初始化
806
- // ============================================
807
- async function initApp() {
808
- try {
809
- // 初始化数据库
810
- await Database.init();
811
-
812
- // 初始化各模块
813
- previewManager = new PreviewManager();
814
- galleryManager = new GalleryManager();
815
- ModalManager.init();
816
- generator = new Generator();
817
- dragDrop = new DragDrop('drop-zone');
818
- fileSelector = new FileSelector();
819
-
820
- // 检查认证状态
821
- const isAuth = await Auth.check();
822
- if (isAuth) {
823
- Auth.unlock();
824
- await galleryManager.load();
825
- } else {
826
- // 聚焦到密码输入框
827
- document.getElementById('pwd').focus();
828
- }
829
-
830
- console.log('App initialized successfully');
831
- } catch (err) {
832
- console.error('App initialization failed:', err);
833
- alert('应用初始化失败,请刷新页面重试');
834
- }
835
  }
836
 
837
- // 启动应用
838
- if (document.readyState === 'loading') {
839
- document.addEventListener('DOMContentLoaded', initApp);
840
- } else {
841
- initApp();
 
 
 
 
842
  }
843
- </script>
844
- </body>
845
- </html>
 
 
 
 
1
+ /* style.css */
2
+ :root {
3
+ --bg-color: #0f172a;
4
+ --panel-bg: rgba(30, 41, 59, 0.75);
5
+ --panel-border: rgba(148, 163, 184, 0.15);
6
+ --accent-color: #3b82f6;
7
+ --text-main: #f1f5f9;
8
+ --text-sub: #94a3b8;
9
+ --btn-shadow: #1e3a8a;
10
+ }
11
+
12
+ * { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
13
+ body {
14
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
15
+ background: var(--bg-color) radial-gradient(circle at 50% 0%, #1e293b 0%, var(--bg-color) 70%);
16
+ color: var(--text-main);
17
+ height: 100vh;
18
+ display: flex;
19
+ flex-direction: column;
20
+ overflow: hidden;
21
+ }
22
+
23
+ /* --- 通用组件 --- */
24
+ .glass-panel {
25
+ background: var(--panel-bg);
26
+ backdrop-filter: blur(16px);
27
+ -webkit-backdrop-filter: blur(16px);
28
+ border: 1px solid var(--panel-border);
29
+ }
30
+
31
+ .btn-3d {
32
+ background: var(--accent-color);
33
+ color: white;
34
+ border: none;
35
+ border-radius: 12px;
36
+ font-weight: 600;
37
+ box-shadow: 0 4px 0 var(--btn-shadow), 0 5px 10px rgba(0,0,0,0.2);
38
+ transition: transform 0.1s, box-shadow 0.1s;
39
+ cursor: pointer;
40
+ display: inline-flex;
41
+ align-items: center;
42
+ justify-content: center;
43
+ }
44
+ .btn-3d:active { transform: translateY(4px); box-shadow: 0 0 0 var(--btn-shadow); }
45
+ .btn-3d:disabled { background: #475569; box-shadow: none; transform: none; opacity: 0.7; }
46
+
47
+ /* --- 布局 --- */
48
+ .app-container {
49
+ max-width: 1200px;
50
+ margin: 0 auto;
51
+ width: 100%;
52
+ height: 100%;
53
+ display: flex;
54
+ flex-direction: column;
55
+ position: relative;
56
+ }
57
+
58
+ header {
59
+ padding: 15px 20px;
60
+ display: flex;
61
+ justify-content: space-between;
62
+ align-items: center;
63
+ flex-shrink: 0;
64
+ margin-top: 70px; /* 为顶部固定输入区留空间 */
65
+ transition: margin-top 0.3s ease;
66
+ }
67
+ header h2 { font-size: 1.2rem; font-weight: 700; letter-spacing: 0.5px; }
68
+
69
+ /* 当有预览图时增加顶部间距 */
70
+ body.has-preview header {
71
+ margin-top: 170px;
72
+ }
73
+
74
+ /* --- 历史记录区域 --- */
75
+ .history-container {
76
+ flex: 1;
77
+ overflow-y: auto;
78
+ padding: 10px 20px;
79
+ padding-bottom: 20px; /* 顶部布局不需要底部留空 */
80
+ }
81
+
82
+ .grid-layout {
83
+ display: grid;
84
+ grid-template-columns: repeat(2, 1fr); /* 默认两列 */
85
+ gap: 16px;
86
+ }
87
+
88
+ .history-item {
89
+ position: relative;
90
+ aspect-ratio: 16 / 9;
91
+ border-radius: 12px;
92
+ overflow: hidden;
93
+ border: 1px solid var(--panel-border);
94
+ background: rgba(0,0,0,0.2);
95
+ cursor: pointer;
96
+ }
97
+ .history-item img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.3s; }
98
+ .history-item:hover img { transform: scale(1.05); }
99
+
100
+ .item-badge {
101
+ position: absolute;
102
+ top: 8px; left: 8px;
103
+ background: rgba(59, 130, 246, 0.9);
104
+ font-size: 10px; padding: 2px 6px;
105
+ border-radius: 4px; z-index: 2;
106
+ }
107
+
108
+ .item-actions {
109
+ position: absolute;
110
+ bottom: 0; right: 0; left: 0;
111
+ padding: 10px;
112
+ background: linear-gradient(transparent, rgba(0,0,0,0.8));
113
+ display: flex;
114
+ justify-content: flex-end;
115
+ gap: 8px;
116
+ opacity: 0;
117
+ transition: opacity 0.2s;
118
+ }
119
+ .history-item:hover .item-actions { opacity: 1; }
120
+ /* 移动端默认显示操作栏 */
121
+ @media (hover: none) { .item-actions { opacity: 1; } }
122
+
123
+ .icon-btn {
124
+ width: 32px; height: 32px;
125
+ border-radius: 8px;
126
+ background: rgba(255,255,255,0.15);
127
+ color: white;
128
+ border: none;
129
+ display: flex; align-items: center; justify-content: center;
130
+ backdrop-filter: blur(4px);
131
+ }
132
+
133
+ /* --- 输入区域 (顶部固定设计) --- */
134
+ .input-section {
135
+ position: fixed; /* 固定在顶部 */
136
+ top: 0;
137
+ left: 0;
138
+ right: 0;
139
+ width: 100%;
140
+ display: flex;
141
+ flex-direction: column;
142
+ border-radius: 0;
143
+ box-shadow: 0 2px 20px rgba(0,0,0,0.3);
144
+ z-index: 100;
145
+ border-bottom: 1px solid var(--panel-border);
146
+ }
147
+
148
+ /* 1. 预览层 (位于输入框上方) */
149
+ .preview-bar {
150
+ padding: 0;
151
+ max-height: 0;
152
+ overflow-x: auto;
153
+ overflow-y: hidden;
154
+ background: rgba(15, 23, 42, 0.4);
155
+ display: flex;
156
+ gap: 8px;
157
+ transition: all 0.3s ease;
158
+ white-space: nowrap;
159
+ max-width: 1200px;
160
+ margin: 0 auto;
161
+ width: 100%;
162
+ }
163
+ .preview-bar.visible {
164
+ padding: 12px 20px;
165
+ max-height: 100px; /* 展开 */
166
+ }
167
+
168
+ .thumb-wrapper {
169
+ position: relative;
170
+ width: 64px; height: 64px;
171
+ flex-shrink: 0;
172
+ border-radius: 8px;
173
+ border: 1px solid rgba(255,255,255,0.1);
174
+ overflow: hidden;
175
+ }
176
+ .thumb-wrapper img { width: 100%; height: 100%; object-fit: cover; }
177
+ .thumb-remove {
178
+ position: absolute; top: 2px; right: 2px;
179
+ width: 16px; height: 16px;
180
+ background: rgba(239,68,68,0.9);
181
+ border-radius: 50%;
182
+ display: flex; align-items: center; justify-content: center;
183
+ font-size: 12px; cursor: pointer;
184
+ }
185
+
186
+ /* 2. 控制层 (按钮+输入框) */
187
+ .control-bar {
188
+ display: flex;
189
+ align-items: flex-end;
190
+ padding: 12px 20px;
191
+ gap: 10px;
192
+ max-width: 1200px;
193
+ margin: 0 auto;
194
+ width: 100%;
195
+ }
196
+
197
+ .upload-trigger {
198
+ width: 44px; height: 44px;
199
+ border-radius: 12px;
200
+ background: rgba(255,255,255,0.05);
201
+ border: 1px dashed rgba(255,255,255,0.3);
202
+ color: var(--text-sub);
203
+ display: flex; align-items: center; justify-content: center;
204
+ font-size: 20px;
205
+ flex-shrink: 0;
206
+ transition: all 0.2s;
207
+ }
208
+ .upload-trigger:hover, .upload-trigger.active {
209
+ background: rgba(59, 130, 246, 0.2);
210
+ border-color: var(--accent-color);
211
+ color: var(--accent-color);
212
+ }
213
+
214
+ .main-input {
215
+ flex: 1;
216
+ background: transparent;
217
+ border: none;
218
+ color: white;
219
+ font-size: 16px;
220
+ padding: 10px 5px;
221
+ resize: none;
222
+ max-height: 120px;
223
+ outline: none;
224
+ line-height: 1.5;
225
+ }
226
+ .main-input::placeholder { color: rgba(255,255,255,0.3); }
227
+
228
+ .send-btn {
229
+ height: 44px;
230
+ padding: 0 20px;
231
+ font-size: 15px;
232
+ flex-shrink: 0;
233
+ }
234
+
235
+ /* 加载动画 */
236
+ .loader {
237
+ width: 18px; height: 18px;
238
+ border: 2px solid rgba(255,255,255,0.3);
239
+ border-top-color: white;
240
+ border-radius: 50%;
241
+ animation: spin 0.8s linear infinite;
242
+ display: none;
243
+ }
244
+ .loading .loader { display: block; }
245
+ .loading span { display: none; }
246
+ @keyframes spin { to { transform: rotate(360deg); } }
247
+
248
+ /* --- 弹窗 --- */
249
+ .modal {
250
+ display: none;
251
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
252
+ background: rgba(0,0,0,0.9);
253
+ z-index: 1000;
254
+ justify-content: center; align-items: center;
255
+ backdrop-filter: blur(5px);
256
+ }
257
+ .modal-content {
258
+ background: #1e293b;
259
+ width: 95%; max-width: 1000px; height: 90%;
260
+ border-radius: 16px;
261
+ display: flex; flex-direction: column;
262
+ position: relative;
263
+ border: 1px solid var(--panel-border);
264
+ }
265
+ .close-modal {
266
+ position: absolute; top: 15px; right: 15px;
267
+ width: 36px; height: 36px;
268
+ border-radius: 50%; background: rgba(0,0,0,0.5);
269
+ color: white; border: 1px solid rgba(255,255,255,0.2);
270
+ z-index: 10; font-size: 20px;
271
+ }
272
+ .modal-img-area { flex: 1; display: flex; justify-content: center; align-items: center; background: #000; overflow: hidden; border-radius: 16px 16px 0 0; }
273
+ .modal-img-area img { max-width: 100%; max-height: 100%; object-fit: contain; }
274
+ .modal-footer { padding: 15px; background: #1e293b; border-top: 1px solid var(--panel-border); display: flex; flex-direction: column; gap: 10px; }
275
+ .input-refs { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 5px; }
276
+ .ref-thumb { width: 40px; height: 40px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.2); cursor: pointer; }
277
+ .prompt-display { background: rgba(0,0,0,0.2); padding: 10px; border-radius: 8px; color: #cbd5e1; font-size: 0.9rem; max-height: 80px; overflow-y: auto; }
278
+
279
+ /* --- 手机响应式设计 --- */
280
+ @media (max-width: 768px) {
281
+ .app-container { padding: 0; }
282
+ .history-container { padding: 10px; padding-bottom: 10px; }
283
 
284
+ /* 移动端顶部间距调整 */
285
+ header {
286
+ margin-top: 60px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  }
288
 
289
+ body.has-preview header {
290
+ margin-top: 140px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  }
292
 
293
+ /* 历史记录一行一个 */
294
+ .grid-layout {
295
+ grid-template-columns: 1fr;
296
+ gap: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  }
298
 
299
+ /* 输入区样式调整 */
300
+ .control-bar {
301
+ padding: 10px;
302
  }
303
 
304
+ .preview-bar.visible {
305
+ max-height: 80px;
306
+ padding: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  }
308
 
309
+ /* 调整按钮点击区域 */
310
+ .send-btn { padding: 0 15px; min-width: 80px; }
311
+ .thumb-wrapper { width: 50px; height: 50px; }
312
+ }
313
+
314
+ /* --- 大屏幕优化 --- */
315
+ @media (min-width: 1200px) {
316
+ .grid-layout {
317
+ grid-template-columns: repeat(3, 1fr); /* 大屏幕显示三列 */
318
  }
319
+ }
320
+
321
+ /* 登录层 */
322
+ #login-overlay { position: fixed; inset: 0; z-index: 5000; display: flex; justify-content: center; align-items: center; background: #0f172a; }
323
+ .login-card { width: 300px; padding: 30px; text-align: center; }
324
+ .login-card input { width: 100%; padding: 12px; margin: 15px 0; background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1); color: white; border-radius: 8px; }