Files changed (1) hide show
  1. index.html +323 -433
index.html CHANGED
@@ -6,7 +6,7 @@
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;
@@ -140,106 +140,39 @@
140
  </div>
141
 
142
  <script>
143
- // ============================================
144
- // 应用配置
145
- // ============================================
146
- const CONFIG = {
147
- DB_NAME: 'BananaProDB_v3',
148
- DB_VERSION: 1,
149
- STORE_NAME: 'artworks',
150
- MAX_IMAGES: 16,
151
- 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>'
152
- };
153
-
154
  // ============================================
155
  // 全局状态管理
156
  // ============================================
157
- class AppState {
158
- constructor() {
159
- this.db = null;
160
- this.currentImages = [];
161
- this.galleryData = [];
162
- this.currentModalItem = null;
163
- this._listeners = new Map();
164
- }
165
-
166
- // 简单的事件系统
167
- on(event, callback) {
168
- if (!this._listeners.has(event)) {
169
- this._listeners.set(event, new Set());
170
- }
171
- this._listeners.get(event).add(callback);
172
- }
173
-
174
- emit(event, data) {
175
- if (this._listeners.has(event)) {
176
- this._listeners.get(event).forEach(callback => callback(data));
177
- }
178
- }
179
- }
180
-
181
- const appState = new AppState();
182
-
183
- // ============================================
184
- // 工具函数
185
- // ============================================
186
- const Utils = {
187
- // 防抖函数
188
- debounce(func, wait) {
189
- let timeout;
190
- return function executedFunction(...args) {
191
- const later = () => {
192
- clearTimeout(timeout);
193
- func(...args);
194
- };
195
- clearTimeout(timeout);
196
- timeout = setTimeout(later, wait);
197
- };
198
- },
199
-
200
- // 创建元素的辅助函数
201
- createElement(tag, attrs = {}, children = []) {
202
- const el = document.createElement(tag);
203
- Object.entries(attrs).forEach(([key, value]) => {
204
- if (key === 'className') {
205
- el.className = value;
206
- } else if (key === 'textContent') {
207
- el.textContent = value;
208
- } else if (key.startsWith('on')) {
209
- el.addEventListener(key.slice(2).toLowerCase(), value);
210
- } else {
211
- el.setAttribute(key, value);
212
- }
213
- });
214
- children.forEach(child => {
215
- if (typeof child === 'string') {
216
- el.appendChild(document.createTextNode(child));
217
- } else {
218
- el.appendChild(child);
219
- }
220
- });
221
- return el;
222
- }
223
  };
224
 
 
 
 
 
225
  // ============================================
226
- // IndexedDB 模块
227
  // ============================================
228
- class Database {
229
- static async init() {
230
  return new Promise((resolve, reject) => {
231
- const request = indexedDB.open(CONFIG.DB_NAME, CONFIG.DB_VERSION);
 
 
232
 
233
- request.onerror = () => reject(new Error('数据库打开失败'));
234
  request.onsuccess = () => {
235
- appState.db = request.result;
236
  resolve();
237
  };
238
 
239
  request.onupgradeneeded = (event) => {
240
  const db = event.target.result;
241
- if (!db.objectStoreNames.contains(CONFIG.STORE_NAME)) {
242
- const store = db.createObjectStore(CONFIG.STORE_NAME, {
243
  keyPath: 'id',
244
  autoIncrement: true
245
  });
@@ -247,205 +180,195 @@
247
  }
248
  };
249
  });
250
- }
251
 
252
- static async transaction(storeName, mode, operation) {
253
  return new Promise((resolve, reject) => {
254
- const tx = appState.db.transaction([storeName], mode);
255
- const store = tx.objectStore(storeName);
256
-
257
- tx.oncomplete = () => resolve(result);
258
- tx.onerror = () => reject(new Error('事务执行失败'));
259
 
260
- let result;
261
- try {
262
- result = operation(store);
263
- } catch (error) {
264
- reject(error);
265
- }
266
- });
267
- }
268
-
269
- static async save(item) {
270
- return this.transaction(CONFIG.STORE_NAME, 'readwrite', (store) => {
271
  const record = {
272
- ...item,
 
 
273
  timestamp: Date.now()
274
  };
275
- return store.add(record);
 
 
 
276
  });
277
- }
278
 
279
- static async getAll() {
280
  return new Promise((resolve, reject) => {
281
- const tx = appState.db.transaction([CONFIG.STORE_NAME], 'readonly');
282
- const store = tx.objectStore(CONFIG.STORE_NAME);
283
  const request = store.getAll();
284
 
285
  request.onsuccess = () => {
 
286
  const results = request.result.sort((a, b) => b.timestamp - a.timestamp);
287
  resolve(results);
288
  };
289
- request.onerror = () => reject(new Error('读取数据失败'));
290
  });
291
- }
292
 
293
- static async delete(id) {
294
- return this.transaction(CONFIG.STORE_NAME, 'readwrite', (store) => {
295
- return store.delete(id);
 
 
 
 
296
  });
297
- }
298
 
299
- static async getById(id) {
300
  return new Promise((resolve, reject) => {
301
- const tx = appState.db.transaction([CONFIG.STORE_NAME], 'readonly');
302
- const store = tx.objectStore(CONFIG.STORE_NAME);
303
  const request = store.get(id);
304
-
305
  request.onsuccess = () => resolve(request.result);
306
- request.onerror = () => reject(new Error('获取数据失败'));
307
  });
308
  }
309
- }
310
 
311
  // ============================================
312
  // 图片处理模块
313
  // ============================================
314
- class ImageHandler {
315
- static fileToBase64(file) {
 
316
  return new Promise((resolve, reject) => {
317
  const reader = new FileReader();
318
  reader.onload = () => resolve(reader.result);
319
- reader.onerror = reject;
320
  reader.readAsDataURL(file);
321
  });
322
- }
323
 
324
- static async processFiles(files) {
 
 
325
  const imageFiles = Array.from(files).filter(f => f.type.startsWith('image/'));
326
- const promises = [];
327
 
328
  for (const file of imageFiles) {
329
- if (appState.currentImages.length >= CONFIG.MAX_IMAGES) {
330
- alert(`最多只能上传 ${CONFIG.MAX_IMAGES} 张图片`);
331
  break;
332
  }
333
- promises.push(this.fileToBase64(file));
 
 
 
 
 
 
334
  }
335
 
336
- try {
337
- const results = await Promise.all(promises);
338
- appState.currentImages.push(...results);
339
- appState.emit('imagesChanged');
340
- } catch (err) {
341
- console.error('图片读取失败:', err);
342
- alert('部分图片读取失败');
343
- }
344
- }
345
-
346
- static removeAt(index) {
347
- appState.currentImages.splice(index, 1);
348
- appState.emit('imagesChanged');
349
- }
350
 
351
- static clear() {
352
- appState.currentImages = [];
353
- appState.emit('imagesChanged');
354
- }
 
355
 
356
- static setImages(images) {
357
- appState.currentImages = images ? [...images] : [];
358
- appState.emit('imagesChanged');
359
- }
360
- }
361
-
362
- // ============================================
363
- // UI组件基类
364
- // ============================================
365
- class Component {
366
- constructor(container) {
367
- this.container = typeof container === 'string'
368
- ? document.getElementById(container)
369
- : container;
370
- }
371
 
372
- render() {
373
- throw new Error('render method must be implemented');
 
 
374
  }
375
- }
376
 
377
  // ============================================
378
  // 预览条管理模块
379
  // ============================================
380
- class PreviewManager extends Component {
381
- constructor() {
382
- super('preview-bar');
 
 
 
 
383
  this.uploadBtn = document.getElementById('upload-btn');
384
  this.statusBar = document.getElementById('status-bar');
385
-
386
- // 监听图片变化
387
- appState.on('imagesChanged', () => this.render());
388
- }
389
 
390
  render() {
391
- const images = appState.currentImages;
 
392
 
393
- // 使用 DocumentFragment 减少重排
394
- const fragment = document.createDocumentFragment();
395
 
396
  if (images.length === 0) {
397
  this.container.classList.remove('visible');
398
  this.uploadBtn.classList.remove('active');
399
  this.statusBar.textContent = 'Ready';
400
- this.container.innerHTML = '';
401
  return;
402
  }
403
 
404
  this.container.classList.add('visible');
405
  this.uploadBtn.classList.add('active');
406
- this.statusBar.textContent = `已选择 ${images.length}/${CONFIG.MAX_IMAGES} 张图片`;
407
 
 
408
  images.forEach((imgData, index) => {
409
- const wrapper = Utils.createElement('div', { className: 'thumb-wrapper' }, [
410
- Utils.createElement('img', { src: imgData }),
411
- Utils.createElement('div', {
412
- className: 'thumb-remove',
413
- textContent: '×',
414
- onClick: (e) => {
415
- e.stopPropagation();
416
- ImageHandler.removeAt(index);
417
- }
418
- })
419
- ]);
420
- fragment.appendChild(wrapper);
 
 
 
 
 
421
  });
422
-
423
- this.container.innerHTML = '';
424
- this.container.appendChild(fragment);
425
  }
426
- }
427
 
428
  // ============================================
429
  // 画廊管理模块
430
  // ============================================
431
- class GalleryManager extends Component {
432
- constructor() {
433
- super('gallery');
434
- this.renderDebounced = Utils.debounce(() => this.render(), 100);
435
- }
 
436
 
437
  async load() {
438
  try {
439
- appState.galleryData = await Database.getAll();
440
  this.render();
441
  } catch (err) {
442
  console.error('加载画廊失败:', err);
443
- alert('加载画廊失败,请刷新页面重试');
444
  }
445
- }
446
 
447
  render() {
448
- if (appState.galleryData.length === 0) {
 
 
449
  this.container.innerHTML = `
450
  <div style="grid-column: 1/-1; text-align: center; color: var(--text-sub); padding: 60px 20px;">
451
  <div style="font-size: 48px; margin-bottom: 10px;">🎨</div>
@@ -455,75 +378,74 @@
455
  return;
456
  }
457
 
458
- const fragment = document.createDocumentFragment();
459
- appState.galleryData.forEach(item => {
460
- fragment.appendChild(this.createCard(item));
461
  });
462
-
463
- this.container.innerHTML = '';
464
- this.container.appendChild(fragment);
465
- }
466
 
467
  createCard(item) {
468
- const card = Utils.createElement('div', {
469
- className: 'history-item',
470
- 'data-id': item.id,
471
- onClick: () => ModalManager.open(item)
472
- });
473
 
474
  // 参考图标记
475
- if (item.inputImages?.length > 0) {
476
- card.appendChild(Utils.createElement('div', {
477
- className: 'item-badge',
478
- textContent: `📎 ${item.inputImages.length}`
479
- }));
480
  }
481
 
482
- // 主图片
483
- const img = Utils.createElement('img', {
484
- loading: 'lazy',
485
- src: item.image,
486
- onError: function() {
487
- this.src = CONFIG.ERROR_IMAGE;
488
- console.error('图片加载失败, ID:', item.id);
489
- }
490
- });
491
- card.appendChild(img);
492
 
493
  // 操作按钮
494
- const actions = Utils.createElement('div', { className: 'item-actions' }, [
495
- Utils.createElement('button', {
496
- className: 'icon-btn',
497
- innerHTML: '',
498
- onClick: (e) => {
499
- e.stopPropagation();
500
- this.downloadImage(item);
501
- }
502
- }),
503
- Utils.createElement('button', {
504
- className: 'icon-btn',
505
- style: 'background: rgba(239,68,68,0.8)',
506
- innerHTML: '🗑',
507
- onClick: (e) => {
508
- e.stopPropagation();
509
- this.deleteItem(item.id);
510
- }
511
- })
512
- ]);
513
- card.appendChild(actions);
514
 
515
- return card;
516
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
 
518
  downloadImage(item) {
519
- const link = Utils.createElement('a', {
520
- href: item.image,
521
- download: `banana-pro-${item.id}-${Date.now()}.png`
522
- });
523
  document.body.appendChild(link);
524
  link.click();
525
  document.body.removeChild(link);
526
- }
527
 
528
  async deleteItem(id) {
529
  if (!confirm('确定要删除这张图片吗?')) return;
@@ -533,125 +455,135 @@
533
  await this.load();
534
  } catch (err) {
535
  console.error('删除失败:', err);
536
- alert('删除失败,请重试');
537
  }
538
  }
539
- }
540
 
541
  // ============================================
542
  // 弹窗管理模块
543
  // ============================================
544
- class ModalManager {
545
- static init() {
 
 
 
 
 
 
546
  this.modal = document.getElementById('modal');
547
  this.imgEl = document.getElementById('m-img');
548
  this.promptEl = document.getElementById('m-prompt');
549
  this.refsEl = document.getElementById('m-refs');
550
  this.reuseBtn = document.getElementById('m-reuse');
551
 
552
- // 事件绑定
553
- this.modal.addEventListener('click', (e) => {
554
  if (e.target === this.modal) this.close();
555
- });
556
 
 
557
  document.addEventListener('keydown', (e) => {
558
- if (e.key === 'Escape' && this.modal.style.display === 'flex') {
559
- this.close();
560
- }
561
  });
562
-
563
- this.reuseBtn.addEventListener('click', () => this.reuse());
564
- }
565
 
566
- static open(item) {
567
- appState.currentModalItem = item;
568
 
 
569
  this.imgEl.src = item.image;
 
 
570
  this.promptEl.textContent = item.prompt;
571
 
572
  // 渲染参考图
573
- const fragment = document.createDocumentFragment();
574
- if (item.inputImages?.length > 0) {
575
  item.inputImages.forEach(imgData => {
576
- const thumb = Utils.createElement('img', {
577
- className: 'ref-thumb',
578
- src: imgData,
579
- onClick: () => window.open(imgData, '_blank')
580
- });
581
- fragment.appendChild(thumb);
582
  });
583
  }
584
- this.refsEl.innerHTML = '';
585
- this.refsEl.appendChild(fragment);
 
586
 
587
  this.modal.style.display = 'flex';
588
- }
589
 
590
- static close() {
591
  this.modal.style.display = 'none';
592
- appState.currentModalItem = null;
593
- }
594
 
595
- static reuse() {
596
- const item = appState.currentModalItem;
597
  if (!item) return;
598
 
 
599
  const textarea = document.getElementById('prompt');
600
  textarea.value = item.prompt;
601
- this.adjustTextareaHeight(textarea);
 
602
 
603
- ImageHandler.setImages(item.inputImages || []);
 
 
 
 
 
604
 
605
  this.close();
606
  textarea.focus();
607
  }
608
-
609
- static adjustTextareaHeight(textarea) {
610
- textarea.style.height = 'auto';
611
- textarea.style.height = textarea.scrollHeight + 'px';
612
- }
613
- }
614
 
615
  // ============================================
616
  // 生成请求模块
617
  // ============================================
618
- class Generator {
619
- constructor() {
 
 
 
620
  this.sendBtn = document.getElementById('send-btn');
621
  this.textarea = document.getElementById('prompt');
622
- this.isGenerating = false;
623
 
624
- this.bindEvents();
625
- }
626
-
627
- bindEvents() {
628
- this.sendBtn.addEventListener('click', () => this.generate());
629
 
630
  // 输入框自动高度
631
  this.textarea.addEventListener('input', () => {
632
- ModalManager.adjustTextareaHeight(this.textarea);
 
633
  });
634
 
635
- // 回车发送
636
  this.textarea.addEventListener('keydown', (e) => {
637
- if (e.key === 'Enter' && !e.shiftKey && !this.isGenerating) {
638
  e.preventDefault();
639
  this.generate();
640
  }
641
  });
642
- }
643
 
644
  setLoading(loading) {
645
- this.isGenerating = loading;
646
- this.sendBtn.classList.toggle('loading', loading);
647
- this.sendBtn.disabled = loading;
648
- }
 
 
 
 
649
 
650
  async generate() {
651
  const prompt = this.textarea.value.trim();
652
  if (!prompt) {
653
  alert('请输入提示词');
654
- this.textarea.focus();
655
  return;
656
  }
657
 
@@ -663,29 +595,30 @@
663
  headers: { 'Content-Type': 'application/json' },
664
  body: JSON.stringify({
665
  prompt: prompt,
666
- images: appState.currentImages
667
  })
668
  });
669
 
670
- if (!response.ok) {
671
- throw new Error(`HTTP error! status: ${response.status}`);
672
- }
673
-
674
  const data = await response.json();
675
 
676
- if (!data.success || !data.image?.startsWith('data:image')) {
677
- throw new Error(data.message || '生成失败:返回的图片数据无效');
 
 
 
 
 
678
  }
679
 
680
  // 保存到数据库
681
  await Database.save({
682
  prompt: prompt,
683
  image: data.image,
684
- inputImages: [...appState.currentImages]
685
  });
686
 
687
  // 刷新画廊
688
- await galleryManager.load();
689
 
690
  // 清空输入
691
  this.textarea.value = '';
@@ -694,158 +627,123 @@
694
 
695
  } catch (err) {
696
  console.error('生成失败:', err);
697
- alert(`生成失败: ${err.message}`);
698
  } finally {
699
  this.setLoading(false);
700
  }
701
  }
702
- }
703
 
704
  // ============================================
705
  // 认证模块
706
  // ============================================
707
- class Auth {
708
- static async check() {
709
  try {
710
  const res = await fetch('/api/check-auth');
711
  const data = await res.json();
712
- return data.authenticated === true;
713
  } catch {
714
  return false;
715
  }
716
- }
717
 
718
- static async login(password) {
719
- try {
720
- const res = await fetch('/api/login', {
721
- method: 'POST',
722
- headers: { 'Content-Type': 'application/json' },
723
- body: JSON.stringify({ password })
724
- });
725
- const data = await res.json();
726
- return data.success === true;
727
- } catch {
728
- return false;
729
- }
730
- }
731
 
732
- static unlock() {
733
  document.getElementById('login-overlay').style.display = 'none';
734
  const app = document.getElementById('app');
735
  app.style.filter = 'none';
736
  app.style.pointerEvents = 'all';
737
  }
738
- }
739
 
740
  // ============================================
741
  // 拖拽上传模块
742
  // ============================================
743
- class DragDrop {
744
- constructor(dropZone) {
745
- this.dropZone = document.getElementById(dropZone);
746
- this.dragCounter = 0;
747
- this.bindEvents();
748
- }
749
 
750
- bindEvents() {
751
- const events = ['dragenter', 'dragover', 'dragleave', 'drop'];
752
 
753
- events.forEach(event => {
754
- this.dropZone.addEventListener(event, this.preventDefaults);
 
 
 
 
755
  });
756
 
757
- this.dropZone.addEventListener('dragenter', () => {
758
- this.dragCounter++;
759
- this.highlight();
 
 
 
760
  });
761
 
762
- this.dropZone.addEventListener('dragleave', () => {
763
- this.dragCounter--;
764
- if (this.dragCounter === 0) {
765
- this.unhighlight();
766
- }
 
767
  });
768
 
 
769
  this.dropZone.addEventListener('drop', async (e) => {
770
- this.dragCounter = 0;
771
- this.unhighlight();
772
-
773
  const files = e.dataTransfer.files;
774
  if (files.length > 0) {
775
  await ImageHandler.processFiles(files);
776
  }
777
  });
778
  }
779
-
780
- preventDefaults(e) {
781
- e.preventDefault();
782
- e.stopPropagation();
783
- }
784
-
785
- highlight() {
786
- this.dropZone.style.borderColor = 'var(--accent-color)';
787
- this.dropZone.style.background = 'rgba(59, 130, 246, 0.1)';
788
- }
789
-
790
- unhighlight() {
791
- this.dropZone.style.borderColor = '';
792
- this.dropZone.style.background = '';
793
- }
794
- }
795
 
796
  // ============================================
797
  // 文件选择模块
798
  // ============================================
799
- class FileSelector {
800
- constructor() {
 
 
 
801
  this.input = document.getElementById('file-input');
802
  this.btn = document.getElementById('upload-btn');
803
 
804
- this.bindEvents();
805
- }
806
-
807
- bindEvents() {
808
- this.btn.addEventListener('click', () => this.input.click());
809
 
810
- this.input.addEventListener('change', async () => {
811
  if (this.input.files.length > 0) {
812
  await ImageHandler.processFiles(this.input.files);
813
  }
814
- this.input.value = '';
815
- });
816
  }
817
- }
818
-
819
- // ============================================
820
- // 全局实例
821
- // ============================================
822
- let previewManager, galleryManager, generator, dragDrop, fileSelector;
823
 
824
  // ============================================
825
- // 全局函数
826
  // ============================================
827
  async function doLogin() {
828
- const pwd = document.getElementById('pwd').value.trim();
829
- if (!pwd) {
830
- document.getElementById('pwd').focus();
831
- return;
832
- }
833
-
834
- const button = event.target;
835
- button.disabled = true;
836
 
837
- try {
838
- const success = await Auth.login(pwd);
839
- if (success) {
840
- Auth.unlock();
841
- await galleryManager.load();
842
- } else {
843
- alert('密码错误');
844
- document.getElementById('pwd').value = '';
845
- document.getElementById('pwd').focus();
846
- }
847
- } finally {
848
- button.disabled = false;
849
  }
850
  }
851
 
@@ -862,36 +760,28 @@
862
  await Database.init();
863
 
864
  // 初始化各模块
865
- previewManager = new PreviewManager();
866
- galleryManager = new GalleryManager();
867
  ModalManager.init();
868
- generator = new Generator();
869
- dragDrop = new DragDrop('drop-zone');
870
- fileSelector = new FileSelector();
871
 
872
  // 检查认证状态
873
  const isAuth = await Auth.check();
874
  if (isAuth) {
875
  Auth.unlock();
876
- await galleryManager.load();
877
- } else {
878
- // 聚焦到密码输入框
879
- document.getElementById('pwd').focus();
880
  }
881
 
882
  console.log('App initialized successfully');
883
  } catch (err) {
884
  console.error('App initialization failed:', err);
885
- alert('应用初始化失败,请刷新页面重试');
886
  }
887
  }
888
 
889
  // 启动应用
890
- if (document.readyState === 'loading') {
891
- document.addEventListener('DOMContentLoaded', initApp);
892
- } else {
893
- initApp();
894
- }
895
  </script>
896
  </body>
897
  </html>
 
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;
 
140
  </div>
141
 
142
  <script>
 
 
 
 
 
 
 
 
 
 
 
143
  // ============================================
144
  // 全局状态管理
145
  // ============================================
146
+ const AppState = {
147
+ db: null,
148
+ currentImages: [], // 当前上传的图片 Base64 数组
149
+ galleryData: [], // 画廊数据缓存
150
+ currentModalItem: null // 当前弹窗显示的项目
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  };
152
 
153
+ const DB_NAME = 'BananaProDB_v3';
154
+ const DB_VERSION = 1;
155
+ const STORE_NAME = 'artworks';
156
+
157
  // ============================================
158
+ // IndexedDB 模块(使用 Blob 存储避免 Base64 问题)
159
  // ============================================
160
+ const Database = {
161
+ async init() {
162
  return new Promise((resolve, reject) => {
163
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
164
+
165
+ request.onerror = () => reject(request.error);
166
 
 
167
  request.onsuccess = () => {
168
+ AppState.db = request.result;
169
  resolve();
170
  };
171
 
172
  request.onupgradeneeded = (event) => {
173
  const db = event.target.result;
174
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
175
+ const store = db.createObjectStore(STORE_NAME, {
176
  keyPath: 'id',
177
  autoIncrement: true
178
  });
 
180
  }
181
  };
182
  });
183
+ },
184
 
185
+ async save(item) {
186
  return new Promise((resolve, reject) => {
187
+ const tx = AppState.db.transaction([STORE_NAME], 'readwrite');
188
+ const store = tx.objectStore(STORE_NAME);
 
 
 
189
 
190
+ // 直接存储完整对象,不做任何转换
 
 
 
 
 
 
 
 
 
 
191
  const record = {
192
+ prompt: item.prompt,
193
+ image: item.image, // 完整的 data:image/... 字符串
194
+ inputImages: item.inputImages || [],
195
  timestamp: Date.now()
196
  };
197
+
198
+ const request = store.add(record);
199
+ request.onsuccess = () => resolve(request.result);
200
+ request.onerror = () => reject(request.error);
201
  });
202
+ },
203
 
204
+ async getAll() {
205
  return new Promise((resolve, reject) => {
206
+ const tx = AppState.db.transaction([STORE_NAME], 'readonly');
207
+ const store = tx.objectStore(STORE_NAME);
208
  const request = store.getAll();
209
 
210
  request.onsuccess = () => {
211
+ // 按时间戳倒序
212
  const results = request.result.sort((a, b) => b.timestamp - a.timestamp);
213
  resolve(results);
214
  };
215
+ request.onerror = () => reject(request.error);
216
  });
217
+ },
218
 
219
+ async delete(id) {
220
+ return new Promise((resolve, reject) => {
221
+ const tx = AppState.db.transaction([STORE_NAME], 'readwrite');
222
+ const store = tx.objectStore(STORE_NAME);
223
+ const request = store.delete(id);
224
+ request.onsuccess = () => resolve();
225
+ request.onerror = () => reject(request.error);
226
  });
227
+ },
228
 
229
+ async getById(id) {
230
  return new Promise((resolve, reject) => {
231
+ const tx = AppState.db.transaction([STORE_NAME], 'readonly');
232
+ const store = tx.objectStore(STORE_NAME);
233
  const request = store.get(id);
 
234
  request.onsuccess = () => resolve(request.result);
235
+ request.onerror = () => reject(request.error);
236
  });
237
  }
238
+ };
239
 
240
  // ============================================
241
  // 图片处理模块
242
  // ============================================
243
+ const ImageHandler = {
244
+ // 文件转 Base64
245
+ fileToBase64(file) {
246
  return new Promise((resolve, reject) => {
247
  const reader = new FileReader();
248
  reader.onload = () => resolve(reader.result);
249
+ reader.onerror = () => reject(reader.error);
250
  reader.readAsDataURL(file);
251
  });
252
+ },
253
 
254
+ // 处理上传的文件
255
+ async processFiles(files) {
256
+ const maxImages = 16;
257
  const imageFiles = Array.from(files).filter(f => f.type.startsWith('image/'));
 
258
 
259
  for (const file of imageFiles) {
260
+ if (AppState.currentImages.length >= maxImages) {
261
+ alert(`最多只能上传 ${maxImages} 张图片`);
262
  break;
263
  }
264
+
265
+ try {
266
+ const base64 = await this.fileToBase64(file);
267
+ AppState.currentImages.push(base64);
268
+ } catch (err) {
269
+ console.error('图片读取失败:', err);
270
+ }
271
  }
272
 
273
+ PreviewManager.render();
274
+ },
 
 
 
 
 
 
 
 
 
 
 
 
275
 
276
+ // 移除指定索引的图片
277
+ removeAt(index) {
278
+ AppState.currentImages.splice(index, 1);
279
+ PreviewManager.render();
280
+ },
281
 
282
+ // 清空所有上传的图片
283
+ clear() {
284
+ AppState.currentImages = [];
285
+ PreviewManager.render();
286
+ },
 
 
 
 
 
 
 
 
 
 
287
 
288
+ // 设置图片(用于复用功能)
289
+ setImages(images) {
290
+ AppState.currentImages = images ? [...images] : [];
291
+ PreviewManager.render();
292
  }
293
+ };
294
 
295
  // ============================================
296
  // 预览条管理模块
297
  // ============================================
298
+ const PreviewManager = {
299
+ container: null,
300
+ uploadBtn: null,
301
+ statusBar: null,
302
+
303
+ init() {
304
+ this.container = document.getElementById('preview-bar');
305
  this.uploadBtn = document.getElementById('upload-btn');
306
  this.statusBar = document.getElementById('status-bar');
307
+ },
 
 
 
308
 
309
  render() {
310
+ // 清空容器
311
+ this.container.innerHTML = '';
312
 
313
+ const images = AppState.currentImages;
 
314
 
315
  if (images.length === 0) {
316
  this.container.classList.remove('visible');
317
  this.uploadBtn.classList.remove('active');
318
  this.statusBar.textContent = 'Ready';
 
319
  return;
320
  }
321
 
322
  this.container.classList.add('visible');
323
  this.uploadBtn.classList.add('active');
324
+ this.statusBar.textContent = `已选择 ${images.length}/16 张图片`;
325
 
326
+ // 使用 DOM API 创建元素,避免 innerHTML 导致的编码问题
327
  images.forEach((imgData, index) => {
328
+ const wrapper = document.createElement('div');
329
+ wrapper.className = 'thumb-wrapper';
330
+
331
+ const img = document.createElement('img');
332
+ img.src = imgData; // 直接设置 src,不经过字符串拼接
333
+
334
+ const removeBtn = document.createElement('div');
335
+ removeBtn.className = 'thumb-remove';
336
+ removeBtn.textContent = '×';
337
+ removeBtn.onclick = (e) => {
338
+ e.stopPropagation();
339
+ ImageHandler.removeAt(index);
340
+ };
341
+
342
+ wrapper.appendChild(img);
343
+ wrapper.appendChild(removeBtn);
344
+ this.container.appendChild(wrapper);
345
  });
 
 
 
346
  }
347
+ };
348
 
349
  // ============================================
350
  // 画廊管理模块
351
  // ============================================
352
+ const GalleryManager = {
353
+ container: null,
354
+
355
+ init() {
356
+ this.container = document.getElementById('gallery');
357
+ },
358
 
359
  async load() {
360
  try {
361
+ AppState.galleryData = await Database.getAll();
362
  this.render();
363
  } catch (err) {
364
  console.error('加载画廊失败:', err);
 
365
  }
366
+ },
367
 
368
  render() {
369
+ this.container.innerHTML = '';
370
+
371
+ if (AppState.galleryData.length === 0) {
372
  this.container.innerHTML = `
373
  <div style="grid-column: 1/-1; text-align: center; color: var(--text-sub); padding: 60px 20px;">
374
  <div style="font-size: 48px; margin-bottom: 10px;">🎨</div>
 
378
  return;
379
  }
380
 
381
+ AppState.galleryData.forEach(item => {
382
+ const card = this.createCard(item);
383
+ this.container.appendChild(card);
384
  });
385
+ },
 
 
 
386
 
387
  createCard(item) {
388
+ const el = document.createElement('div');
389
+ el.className = 'history-item';
390
+ el.dataset.id = item.id;
 
 
391
 
392
  // 参考图标记
393
+ if (item.inputImages && item.inputImages.length > 0) {
394
+ const badge = document.createElement('div');
395
+ badge.className = 'item-badge';
396
+ badge.textContent = `📎 ${item.inputImages.length}`;
397
+ el.appendChild(badge);
398
  }
399
 
400
+ // 主图片 - 使用 DOM API 设置 src
401
+ const img = document.createElement('img');
402
+ img.loading = 'lazy';
403
+ img.src = item.image; // 关键:直接赋值,不用模板字符串
404
+ img.onerror = () => {
405
+ console.error('图片加载失败, ID:', item.id);
406
+ 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>';
407
+ };
408
+ el.appendChild(img);
 
409
 
410
  // 操作按钮
411
+ const actions = document.createElement('div');
412
+ actions.className = 'item-actions';
413
+
414
+ const downloadBtn = document.createElement('button');
415
+ downloadBtn.className = 'icon-btn';
416
+ downloadBtn.innerHTML = '⬇';
417
+ downloadBtn.onclick = (e) => {
418
+ e.stopPropagation();
419
+ this.downloadImage(item);
420
+ };
 
 
 
 
 
 
 
 
 
 
421
 
422
+ const deleteBtn = document.createElement('button');
423
+ deleteBtn.className = 'icon-btn';
424
+ deleteBtn.style.background = 'rgba(239,68,68,0.8)';
425
+ deleteBtn.innerHTML = '🗑';
426
+ deleteBtn.onclick = (e) => {
427
+ e.stopPropagation();
428
+ this.deleteItem(item.id);
429
+ };
430
+
431
+ actions.appendChild(downloadBtn);
432
+ actions.appendChild(deleteBtn);
433
+ el.appendChild(actions);
434
+
435
+ // 点击打开弹窗
436
+ el.onclick = () => ModalManager.open(item);
437
+
438
+ return el;
439
+ },
440
 
441
  downloadImage(item) {
442
+ const link = document.createElement('a');
443
+ link.href = item.image;
444
+ link.download = `banana-pro-${item.id}-${Date.now()}.png`;
 
445
  document.body.appendChild(link);
446
  link.click();
447
  document.body.removeChild(link);
448
+ },
449
 
450
  async deleteItem(id) {
451
  if (!confirm('确定要删除这张图片吗?')) return;
 
455
  await this.load();
456
  } catch (err) {
457
  console.error('删除失败:', err);
458
+ alert('删除失败');
459
  }
460
  }
461
+ };
462
 
463
  // ============================================
464
  // 弹窗管理模块
465
  // ============================================
466
+ const ModalManager = {
467
+ modal: null,
468
+ imgEl: null,
469
+ promptEl: null,
470
+ refsEl: null,
471
+ reuseBtn: null,
472
+
473
+ init() {
474
  this.modal = document.getElementById('modal');
475
  this.imgEl = document.getElementById('m-img');
476
  this.promptEl = document.getElementById('m-prompt');
477
  this.refsEl = document.getElementById('m-refs');
478
  this.reuseBtn = document.getElementById('m-reuse');
479
 
480
+ // 点击背景关闭
481
+ this.modal.onclick = (e) => {
482
  if (e.target === this.modal) this.close();
483
+ };
484
 
485
+ // ESC 关闭
486
  document.addEventListener('keydown', (e) => {
487
+ if (e.key === 'Escape') this.close();
 
 
488
  });
489
+ },
 
 
490
 
491
+ open(item) {
492
+ AppState.currentModalItem = item;
493
 
494
+ // 设置主图 - 直接赋值
495
  this.imgEl.src = item.image;
496
+
497
+ // 设置提示词
498
  this.promptEl.textContent = item.prompt;
499
 
500
  // 渲染参考图
501
+ this.refsEl.innerHTML = '';
502
+ if (item.inputImages && item.inputImages.length > 0) {
503
  item.inputImages.forEach(imgData => {
504
+ const thumb = document.createElement('img');
505
+ thumb.className = 'ref-thumb';
506
+ thumb.src = imgData; // 直接赋值
507
+ thumb.onclick = () => window.open(imgData, '_blank');
508
+ this.refsEl.appendChild(thumb);
 
509
  });
510
  }
511
+
512
+ // 绑定复用按钮
513
+ this.reuseBtn.onclick = () => this.reuse();
514
 
515
  this.modal.style.display = 'flex';
516
+ },
517
 
518
+ close() {
519
  this.modal.style.display = 'none';
520
+ AppState.currentModalItem = null;
521
+ },
522
 
523
+ reuse() {
524
+ const item = AppState.currentModalItem;
525
  if (!item) return;
526
 
527
+ // 复用提示词
528
  const textarea = document.getElementById('prompt');
529
  textarea.value = item.prompt;
530
+ textarea.style.height = 'auto';
531
+ textarea.style.height = textarea.scrollHeight + 'px';
532
 
533
+ // 复用参考图
534
+ if (item.inputImages && item.inputImages.length > 0) {
535
+ ImageHandler.setImages(item.inputImages);
536
+ } else {
537
+ ImageHandler.clear();
538
+ }
539
 
540
  this.close();
541
  textarea.focus();
542
  }
543
+ };
 
 
 
 
 
544
 
545
  // ============================================
546
  // 生成请求模块
547
  // ============================================
548
+ const Generator = {
549
+ sendBtn: null,
550
+ textarea: null,
551
+
552
+ init() {
553
  this.sendBtn = document.getElementById('send-btn');
554
  this.textarea = document.getElementById('prompt');
 
555
 
556
+ this.sendBtn.onclick = () => this.generate();
 
 
 
 
557
 
558
  // 输入框自动高度
559
  this.textarea.addEventListener('input', () => {
560
+ this.textarea.style.height = 'auto';
561
+ this.textarea.style.height = this.textarea.scrollHeight + 'px';
562
  });
563
 
564
+ // 回车发送(Shift+Enter 换行)
565
  this.textarea.addEventListener('keydown', (e) => {
566
+ if (e.key === 'Enter' && !e.shiftKey) {
567
  e.preventDefault();
568
  this.generate();
569
  }
570
  });
571
+ },
572
 
573
  setLoading(loading) {
574
+ if (loading) {
575
+ this.sendBtn.classList.add('loading');
576
+ this.sendBtn.disabled = true;
577
+ } else {
578
+ this.sendBtn.classList.remove('loading');
579
+ this.sendBtn.disabled = false;
580
+ }
581
+ },
582
 
583
  async generate() {
584
  const prompt = this.textarea.value.trim();
585
  if (!prompt) {
586
  alert('请输入提示词');
 
587
  return;
588
  }
589
 
 
595
  headers: { 'Content-Type': 'application/json' },
596
  body: JSON.stringify({
597
  prompt: prompt,
598
+ images: AppState.currentImages
599
  })
600
  });
601
 
 
 
 
 
602
  const data = await response.json();
603
 
604
+ if (!response.ok || !data.success) {
605
+ throw new Error(data.message || '生成失败');
606
+ }
607
+
608
+ // 验证返回的图片数据
609
+ if (!data.image || !data.image.startsWith('data:image')) {
610
+ throw new Error('返回的图片数据无效');
611
  }
612
 
613
  // 保存到数据库
614
  await Database.save({
615
  prompt: prompt,
616
  image: data.image,
617
+ inputImages: [...AppState.currentImages]
618
  });
619
 
620
  // 刷新画廊
621
+ await GalleryManager.load();
622
 
623
  // 清空输入
624
  this.textarea.value = '';
 
627
 
628
  } catch (err) {
629
  console.error('生成失败:', err);
630
+ alert('生成失败: ' + err.message);
631
  } finally {
632
  this.setLoading(false);
633
  }
634
  }
635
+ };
636
 
637
  // ============================================
638
  // 认证模块
639
  // ============================================
640
+ const Auth = {
641
+ async check() {
642
  try {
643
  const res = await fetch('/api/check-auth');
644
  const data = await res.json();
645
+ return data.authenticated;
646
  } catch {
647
  return false;
648
  }
649
+ },
650
 
651
+ async login(password) {
652
+ const res = await fetch('/api/login', {
653
+ method: 'POST',
654
+ headers: { 'Content-Type': 'application/json' },
655
+ body: JSON.stringify({ password })
656
+ });
657
+ const data = await res.json();
658
+ return data.success;
659
+ },
 
 
 
 
660
 
661
+ unlock() {
662
  document.getElementById('login-overlay').style.display = 'none';
663
  const app = document.getElementById('app');
664
  app.style.filter = 'none';
665
  app.style.pointerEvents = 'all';
666
  }
667
+ };
668
 
669
  // ============================================
670
  // 拖拽上传模块
671
  // ============================================
672
+ const DragDrop = {
673
+ dropZone: null,
 
 
 
 
674
 
675
+ init() {
676
+ this.dropZone = document.getElementById('drop-zone');
677
 
678
+ // 阻止默认行为
679
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(event => {
680
+ this.dropZone.addEventListener(event, (e) => {
681
+ e.preventDefault();
682
+ e.stopPropagation();
683
+ });
684
  });
685
 
686
+ // 拖入高亮
687
+ ['dragenter', 'dragover'].forEach(event => {
688
+ this.dropZone.addEventListener(event, () => {
689
+ this.dropZone.style.borderColor = 'var(--accent-color)';
690
+ this.dropZone.style.background = 'rgba(59, 130, 246, 0.1)';
691
+ });
692
  });
693
 
694
+ // 拖出恢复
695
+ ['dragleave', 'drop'].forEach(event => {
696
+ this.dropZone.addEventListener(event, () => {
697
+ this.dropZone.style.borderColor = '';
698
+ this.dropZone.style.background = '';
699
+ });
700
  });
701
 
702
+ // 放下处理
703
  this.dropZone.addEventListener('drop', async (e) => {
 
 
 
704
  const files = e.dataTransfer.files;
705
  if (files.length > 0) {
706
  await ImageHandler.processFiles(files);
707
  }
708
  });
709
  }
710
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
711
 
712
  // ============================================
713
  // 文件选择模块
714
  // ============================================
715
+ const FileSelector = {
716
+ input: null,
717
+ btn: null,
718
+
719
+ init() {
720
  this.input = document.getElementById('file-input');
721
  this.btn = document.getElementById('upload-btn');
722
 
723
+ this.btn.onclick = () => this.input.click();
 
 
 
 
724
 
725
+ this.input.onchange = async () => {
726
  if (this.input.files.length > 0) {
727
  await ImageHandler.processFiles(this.input.files);
728
  }
729
+ this.input.value = ''; // 重置以便重复选择
730
+ };
731
  }
732
+ };
 
 
 
 
 
733
 
734
  // ============================================
735
+ // 全局函数(供 HTML onclick 调用)
736
  // ============================================
737
  async function doLogin() {
738
+ const pwd = document.getElementById('pwd').value;
739
+ if (!pwd) return;
 
 
 
 
 
 
740
 
741
+ const success = await Auth.login(pwd);
742
+ if (success) {
743
+ Auth.unlock();
744
+ await GalleryManager.load();
745
+ } else {
746
+ alert('密码错误');
 
 
 
 
 
 
747
  }
748
  }
749
 
 
760
  await Database.init();
761
 
762
  // 初始化各模块
763
+ PreviewManager.init();
764
+ GalleryManager.init();
765
  ModalManager.init();
766
+ Generator.init();
767
+ DragDrop.init();
768
+ FileSelector.init();
769
 
770
  // 检查认证状态
771
  const isAuth = await Auth.check();
772
  if (isAuth) {
773
  Auth.unlock();
774
+ await GalleryManager.load();
 
 
 
775
  }
776
 
777
  console.log('App initialized successfully');
778
  } catch (err) {
779
  console.error('App initialization failed:', err);
 
780
  }
781
  }
782
 
783
  // 启动应用
784
+ document.addEventListener('DOMContentLoaded', initApp);
 
 
 
 
785
  </script>
786
  </body>
787
  </html>