486CHD commited on
Commit
b9c06e2
·
verified ·
1 Parent(s): a997d49

Upload index.html (#14)

Browse files

- Upload index.html (c2db0ed3158f74fa19f4c692ff5304abf41980df)

Files changed (1) hide show
  1. index.html +539 -539
index.html CHANGED
@@ -1,119 +1,119 @@
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
- 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">
84
- <div class="login-card glass-panel">
85
- <h1 style="font-size: 2rem;">🍌</h1>
86
- <h3 style="margin: 10px 0; color: #cbd5e1;">Banana Pro</h3>
87
- <input type="password" id="pwd" placeholder="Password" onkeypress="if(event.key==='Enter')doLogin()">
88
- <button class="btn-3d" onclick="doLogin()" style="width: 100%; padding: 10px;">Unlock</button>
89
- </div>
90
- </div>
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>
98
-
99
- <!-- 下部分:操作栏 -->
100
- <div class="control-bar">
101
- <button class="upload-trigger" id="upload-btn" title="上传参考图">📷</button>
102
- <textarea id="prompt" class="main-input" rows="1" placeholder="描述画面... (支持拖拽图片)"></textarea>
103
- <button class="btn-3d send-btn" id="send-btn">
104
- <span>生成</span>
105
- <div class="loader"></div>
106
- </button>
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>
@@ -132,30 +132,30 @@
132
  </section>
133
  </div>
134
 
135
- <input type="file" id="file-input" multiple accept="image/*" hidden>
136
- </div>
137
-
138
- <!-- 详情弹窗 -->
139
- <div class="modal" id="modal">
140
- <div class="modal-content glass-panel">
141
- <button class="close-modal" onclick="closeModal()">×</button>
142
- <div class="modal-img-area">
143
- <img id="m-img" src="">
144
- </div>
145
- <div class="modal-footer">
146
- <div class="input-refs" id="m-refs"></div>
147
- <div class="prompt-display" id="m-prompt"></div>
148
- <button class="btn-3d" id="m-reuse" style="width: 100%; padding: 12px;">
149
- 🎨 复用参数与图片
150
- </button>
151
- </div>
152
- </div>
153
- </div>
154
-
155
- <script>
156
- // ============================================
157
- // 全局状态管理
158
- // ============================================
159
  const AppState = {
160
  db: null,
161
  currentImages: [], // 当前上传的图片 Base64 数组
@@ -173,91 +173,91 @@
173
  const DB_NAME = 'BananaProDB_v3';
174
  const DB_VERSION = 1;
175
 
176
- const STORE_NAME = 'artworks';
177
-
178
- // ============================================
179
- // IndexedDB 模块(使用 Blob 存储避免 Base64 问题)
180
- // ============================================
181
- const Database = {
182
- async init() {
183
- return new Promise((resolve, reject) => {
184
- const request = indexedDB.open(DB_NAME, DB_VERSION);
185
-
186
- request.onerror = () => reject(request.error);
187
-
188
- request.onsuccess = () => {
189
- AppState.db = request.result;
190
- resolve();
191
- };
192
-
193
- request.onupgradeneeded = (event) => {
194
- const db = event.target.result;
195
- if (!db.objectStoreNames.contains(STORE_NAME)) {
196
- const store = db.createObjectStore(STORE_NAME, {
197
- keyPath: 'id',
198
- autoIncrement: true
199
- });
200
- store.createIndex('timestamp', 'timestamp', { unique: false });
201
- }
202
- };
203
- });
204
- },
205
-
206
- async save(item) {
207
- return new Promise((resolve, reject) => {
208
- const tx = AppState.db.transaction([STORE_NAME], 'readwrite');
209
- const store = tx.objectStore(STORE_NAME);
210
-
211
- // 直接存储完整对象,不做任何转换
212
- const record = {
213
- prompt: item.prompt,
214
- image: item.image, // 完整的 data:image/... 字符串
215
- inputImages: item.inputImages || [],
216
- timestamp: Date.now()
217
- };
218
-
219
- const request = store.add(record);
220
- request.onsuccess = () => resolve(request.result);
221
- request.onerror = () => reject(request.error);
222
- });
223
- },
224
-
225
- async getAll() {
226
- return new Promise((resolve, reject) => {
227
- const tx = AppState.db.transaction([STORE_NAME], 'readonly');
228
- const store = tx.objectStore(STORE_NAME);
229
- const request = store.getAll();
230
-
231
- request.onsuccess = () => {
232
- // 按时间戳倒序
233
- const results = request.result.sort((a, b) => b.timestamp - a.timestamp);
234
- resolve(results);
235
- };
236
- request.onerror = () => reject(request.error);
237
- });
238
- },
239
-
240
- async delete(id) {
241
- return new Promise((resolve, reject) => {
242
- const tx = AppState.db.transaction([STORE_NAME], 'readwrite');
243
- const store = tx.objectStore(STORE_NAME);
244
- const request = store.delete(id);
245
- request.onsuccess = () => resolve();
246
- request.onerror = () => reject(request.error);
247
- });
248
- },
249
-
250
- async getById(id) {
251
- return new Promise((resolve, reject) => {
252
- const tx = AppState.db.transaction([STORE_NAME], 'readonly');
253
- const store = tx.objectStore(STORE_NAME);
254
- const request = store.get(id);
255
- request.onsuccess = () => resolve(request.result);
256
- request.onerror = () => reject(request.error);
257
- });
258
- }
259
- };
260
-
261
  // ============================================
262
  // 图片处理模块
263
  // ============================================
@@ -318,99 +318,99 @@
318
 
319
  PreviewManager.render();
320
  },
321
-
322
- // 移除指定索引的图片
323
- removeAt(index) {
324
- AppState.currentImages.splice(index, 1);
325
- PreviewManager.render();
326
- },
327
-
328
- // 清空所有上传的图片
329
- clear() {
330
- AppState.currentImages = [];
331
- PreviewManager.render();
332
- },
333
-
334
- // 设置图片(用于复用功能)
335
- setImages(images) {
336
- AppState.currentImages = images ? [...images] : [];
337
- PreviewManager.render();
338
- }
339
- };
340
-
341
- // ============================================
342
- // 预览条管理模块
343
- // ============================================
344
- const PreviewManager = {
345
- container: null,
346
- uploadBtn: null,
347
- statusBar: null,
348
-
349
- init() {
350
- this.container = document.getElementById('preview-bar');
351
- this.uploadBtn = document.getElementById('upload-btn');
352
- this.statusBar = document.getElementById('status-bar');
353
- },
354
-
355
- render() {
356
- // 清空容器
357
- this.container.innerHTML = '';
358
-
359
- const images = AppState.currentImages;
360
-
361
- if (images.length === 0) {
362
- this.container.classList.remove('visible');
363
- this.uploadBtn.classList.remove('active');
364
- this.statusBar.textContent = 'Ready';
365
- return;
366
- }
367
-
368
- this.container.classList.add('visible');
369
- this.uploadBtn.classList.add('active');
370
- this.statusBar.textContent = `已选择 ${images.length}/16 张图片`;
371
-
372
- // 使用 DOM API 创建元素,避免 innerHTML 导致的编码问题
373
- images.forEach((imgData, index) => {
374
- const wrapper = document.createElement('div');
375
- wrapper.className = 'thumb-wrapper';
376
-
377
- const img = document.createElement('img');
378
- img.src = imgData; // 直接设置 src,不经过字符串拼接
379
-
380
- const removeBtn = document.createElement('div');
381
- removeBtn.className = 'thumb-remove';
382
- removeBtn.textContent = '×';
383
- removeBtn.onclick = (e) => {
384
- e.stopPropagation();
385
- ImageHandler.removeAt(index);
386
- };
387
-
388
- wrapper.appendChild(img);
389
- wrapper.appendChild(removeBtn);
390
- this.container.appendChild(wrapper);
391
- });
392
- }
393
- };
394
-
395
- // ============================================
396
- // 画廊管理模块
397
- // ============================================
398
- const GalleryManager = {
399
- container: null,
400
-
401
- init() {
402
- this.container = document.getElementById('gallery');
403
- },
404
-
405
- async load() {
406
- try {
407
- AppState.galleryData = await Database.getAll();
408
- this.render();
409
- } catch (err) {
410
- console.error('加载画廊失败:', err);
411
- }
412
- },
413
-
414
  render() {
415
  this.container.innerHTML = '';
416
 
@@ -431,7 +431,7 @@
431
  });
432
  this.container.appendChild(fragment);
433
  },
434
-
435
  createCard(item) {
436
  const el = document.createElement('div');
437
  el.className = 'history-item';
@@ -500,109 +500,109 @@
500
 
501
  return el;
502
  },
503
-
504
- downloadImage(item) {
505
- const link = document.createElement('a');
506
- link.href = item.image;
507
- link.download = `banana-pro-${item.id}-${Date.now()}.png`;
508
- document.body.appendChild(link);
509
- link.click();
510
- document.body.removeChild(link);
511
- },
512
-
513
- async deleteItem(id) {
514
- if (!confirm('确定要删除这张图片吗?')) return;
515
-
516
- try {
517
- await Database.delete(id);
518
- await this.load();
519
- } catch (err) {
520
- console.error('删除失败:', err);
521
- alert('删除失败');
522
- }
523
- }
524
- };
525
-
526
- // ============================================
527
- // 弹窗管理模块
528
- // ============================================
529
- const ModalManager = {
530
- modal: null,
531
- imgEl: null,
532
- promptEl: null,
533
- refsEl: null,
534
- reuseBtn: null,
535
-
536
- init() {
537
- this.modal = document.getElementById('modal');
538
- this.imgEl = document.getElementById('m-img');
539
- this.promptEl = document.getElementById('m-prompt');
540
- this.refsEl = document.getElementById('m-refs');
541
- this.reuseBtn = document.getElementById('m-reuse');
542
-
543
- // 点击背景关闭
544
- this.modal.onclick = (e) => {
545
- if (e.target === this.modal) this.close();
546
- };
547
-
548
- // ESC 关闭
549
- document.addEventListener('keydown', (e) => {
550
- if (e.key === 'Escape') this.close();
551
- });
552
- },
553
-
554
- open(item) {
555
- AppState.currentModalItem = item;
556
-
557
- // 设置主图 - 直接赋值
558
- this.imgEl.src = item.image;
559
-
560
- // 设置提示词
561
- this.promptEl.textContent = item.prompt;
562
-
563
- // 渲染参考图
564
- this.refsEl.innerHTML = '';
565
- if (item.inputImages && item.inputImages.length > 0) {
566
- item.inputImages.forEach(imgData => {
567
- const thumb = document.createElement('img');
568
- thumb.className = 'ref-thumb';
569
- thumb.src = imgData; // 直接赋值
570
- thumb.onclick = () => window.open(imgData, '_blank');
571
- this.refsEl.appendChild(thumb);
572
- });
573
- }
574
-
575
- // 绑定复用按钮
576
- this.reuseBtn.onclick = () => this.reuse();
577
-
578
- this.modal.style.display = 'flex';
579
- },
580
-
581
- close() {
582
- this.modal.style.display = 'none';
583
- AppState.currentModalItem = null;
584
- },
585
-
586
- reuse() {
587
- const item = AppState.currentModalItem;
588
- if (!item) return;
589
-
590
- // 复用提示词
591
- const textarea = document.getElementById('prompt');
592
- textarea.value = item.prompt;
593
- textarea.style.height = 'auto';
594
- textarea.style.height = textarea.scrollHeight + 'px';
595
-
596
- // 复用参考图
597
- if (item.inputImages && item.inputImages.length > 0) {
598
- ImageHandler.setImages(item.inputImages);
599
- } else {
600
- ImageHandler.clear();
601
- }
602
-
603
- this.close();
604
- textarea.focus();
605
- }
606
  };
607
 
608
  // ============================================
@@ -1071,104 +1071,104 @@
1071
  }
1072
  }
1073
  };
1074
-
1075
- // ============================================
1076
- // 认证模块
1077
- // ============================================
1078
- const Auth = {
1079
- async check() {
1080
- try {
1081
- const res = await fetch('/api/check-auth');
1082
- const data = await res.json();
1083
- return data.authenticated;
1084
- } catch {
1085
- return false;
1086
- }
1087
- },
1088
-
1089
- async login(password) {
1090
- const res = await fetch('/api/login', {
1091
- method: 'POST',
1092
- headers: { 'Content-Type': 'application/json' },
1093
- body: JSON.stringify({ password })
1094
- });
1095
- const data = await res.json();
1096
- return data.success;
1097
- },
1098
-
1099
- unlock() {
1100
- document.getElementById('login-overlay').style.display = 'none';
1101
- const app = document.getElementById('app');
1102
- app.style.filter = 'none';
1103
- app.style.pointerEvents = 'all';
1104
- }
1105
- };
1106
-
1107
- // ============================================
1108
- // 拖拽上传模块
1109
- // ============================================
1110
- const DragDrop = {
1111
- dropZone: null,
1112
-
1113
- init() {
1114
- this.dropZone = document.getElementById('drop-zone');
1115
-
1116
- // 阻止默认行为
1117
- ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(event => {
1118
- this.dropZone.addEventListener(event, (e) => {
1119
- e.preventDefault();
1120
- e.stopPropagation();
1121
- });
1122
- });
1123
-
1124
- // 拖入高亮
1125
- ['dragenter', 'dragover'].forEach(event => {
1126
- this.dropZone.addEventListener(event, () => {
1127
- this.dropZone.style.borderColor = 'var(--accent-color)';
1128
- this.dropZone.style.background = 'rgba(59, 130, 246, 0.1)';
1129
- });
1130
- });
1131
-
1132
- // 拖出恢复
1133
- ['dragleave', 'drop'].forEach(event => {
1134
- this.dropZone.addEventListener(event, () => {
1135
- this.dropZone.style.borderColor = '';
1136
- this.dropZone.style.background = '';
1137
- });
1138
- });
1139
-
1140
- // 放下处理
1141
- this.dropZone.addEventListener('drop', async (e) => {
1142
- const files = e.dataTransfer.files;
1143
- if (files.length > 0) {
1144
- await ImageHandler.processFiles(files);
1145
- }
1146
- });
1147
- }
1148
- };
1149
-
1150
- // ============================================
1151
- // 文件选择模块
1152
- // ============================================
1153
- const FileSelector = {
1154
- input: null,
1155
- btn: null,
1156
-
1157
- init() {
1158
- this.input = document.getElementById('file-input');
1159
- this.btn = document.getElementById('upload-btn');
1160
-
1161
- this.btn.onclick = () => this.input.click();
1162
-
1163
- this.input.onchange = async () => {
1164
- if (this.input.files.length > 0) {
1165
- await ImageHandler.processFiles(this.input.files);
1166
- }
1167
- this.input.value = ''; // 重置以便重复选择
1168
- };
1169
- }
1170
- };
1171
-
1172
  // ============================================
1173
  // 全局函数(供 HTML onclick 调用)
1174
  // ============================================
@@ -1196,18 +1196,18 @@
1196
  }
1197
  }
1198
 
1199
- function closeModal() {
1200
- ModalManager.close();
1201
- }
1202
-
1203
- // ============================================
1204
- // 应用初始化
1205
- // ============================================
1206
- async function initApp() {
1207
- try {
1208
- // 初始化数据库
1209
- await Database.init();
1210
-
1211
  // 初始化各模块
1212
  PreviewManager.init();
1213
  GalleryManager.init();
@@ -1224,12 +1224,12 @@
1224
  await loadWorkspaceData();
1225
  }
1226
 
1227
- console.log('App initialized successfully');
1228
- } catch (err) {
1229
- console.error('App initialization failed:', err);
1230
- }
1231
- }
1232
-
1233
  // 启动应用
1234
  if (document.readyState === 'loading') {
1235
  document.addEventListener('DOMContentLoaded', initApp);
@@ -1237,5 +1237,5 @@
1237
  initApp();
1238
  }
1239
  </script>
1240
- </body>
1241
  </html>
 
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
+ 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">
84
+ <div class="login-card glass-panel">
85
+ <h1 style="font-size: 2rem;">🍌</h1>
86
+ <h3 style="margin: 10px 0; color: #cbd5e1;">Banana Pro</h3>
87
+ <input type="password" id="pwd" placeholder="Password" onkeypress="if(event.key==='Enter')doLogin()">
88
+ <button class="btn-3d" onclick="doLogin()" style="width: 100%; padding: 10px;">Unlock</button>
89
+ </div>
90
+ </div>
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>
98
+
99
+ <!-- 下部分:操作栏 -->
100
+ <div class="control-bar">
101
+ <button class="upload-trigger" id="upload-btn" title="上传参考图">📷</button>
102
+ <textarea id="prompt" class="main-input" rows="1" placeholder="描述画面... (支持拖拽图片)"></textarea>
103
+ <button class="btn-3d send-btn" id="send-btn">
104
+ <span>生成</span>
105
+ <div class="loader"></div>
106
+ </button>
107
+ </div>
108
+ </div>
109
+
110
+ <header>
111
+ <div>
112
+ <h2>创意画板 提示词用法库https://nanobananamaker.com/</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>
 
132
  </section>
133
  </div>
134
 
135
+ <input type="file" id="file-input" multiple accept="image/*" hidden>
136
+ </div>
137
+
138
+ <!-- 详情弹窗 -->
139
+ <div class="modal" id="modal">
140
+ <div class="modal-content glass-panel">
141
+ <button class="close-modal" onclick="closeModal()">×</button>
142
+ <div class="modal-img-area">
143
+ <img id="m-img" src="">
144
+ </div>
145
+ <div class="modal-footer">
146
+ <div class="input-refs" id="m-refs"></div>
147
+ <div class="prompt-display" id="m-prompt"></div>
148
+ <button class="btn-3d" id="m-reuse" style="width: 100%; padding: 12px;">
149
+ 🎨 复用参数与图片
150
+ </button>
151
+ </div>
152
+ </div>
153
+ </div>
154
+
155
+ <script>
156
+ // ============================================
157
+ // 全局状态管理
158
+ // ============================================
159
  const AppState = {
160
  db: null,
161
  currentImages: [], // 当前上传的图片 Base64 数组
 
173
  const DB_NAME = 'BananaProDB_v3';
174
  const DB_VERSION = 1;
175
 
176
+ const STORE_NAME = 'artworks';
177
+
178
+ // ============================================
179
+ // IndexedDB 模块(使用 Blob 存储避免 Base64 问题)
180
+ // ============================================
181
+ const Database = {
182
+ async init() {
183
+ return new Promise((resolve, reject) => {
184
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
185
+
186
+ request.onerror = () => reject(request.error);
187
+
188
+ request.onsuccess = () => {
189
+ AppState.db = request.result;
190
+ resolve();
191
+ };
192
+
193
+ request.onupgradeneeded = (event) => {
194
+ const db = event.target.result;
195
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
196
+ const store = db.createObjectStore(STORE_NAME, {
197
+ keyPath: 'id',
198
+ autoIncrement: true
199
+ });
200
+ store.createIndex('timestamp', 'timestamp', { unique: false });
201
+ }
202
+ };
203
+ });
204
+ },
205
+
206
+ async save(item) {
207
+ return new Promise((resolve, reject) => {
208
+ const tx = AppState.db.transaction([STORE_NAME], 'readwrite');
209
+ const store = tx.objectStore(STORE_NAME);
210
+
211
+ // 直接存储完整对象,不做任何转换
212
+ const record = {
213
+ prompt: item.prompt,
214
+ image: item.image, // 完整的 data:image/... 字符串
215
+ inputImages: item.inputImages || [],
216
+ timestamp: Date.now()
217
+ };
218
+
219
+ const request = store.add(record);
220
+ request.onsuccess = () => resolve(request.result);
221
+ request.onerror = () => reject(request.error);
222
+ });
223
+ },
224
+
225
+ async getAll() {
226
+ return new Promise((resolve, reject) => {
227
+ const tx = AppState.db.transaction([STORE_NAME], 'readonly');
228
+ const store = tx.objectStore(STORE_NAME);
229
+ const request = store.getAll();
230
+
231
+ request.onsuccess = () => {
232
+ // 按时间戳倒序
233
+ const results = request.result.sort((a, b) => b.timestamp - a.timestamp);
234
+ resolve(results);
235
+ };
236
+ request.onerror = () => reject(request.error);
237
+ });
238
+ },
239
+
240
+ async delete(id) {
241
+ return new Promise((resolve, reject) => {
242
+ const tx = AppState.db.transaction([STORE_NAME], 'readwrite');
243
+ const store = tx.objectStore(STORE_NAME);
244
+ const request = store.delete(id);
245
+ request.onsuccess = () => resolve();
246
+ request.onerror = () => reject(request.error);
247
+ });
248
+ },
249
+
250
+ async getById(id) {
251
+ return new Promise((resolve, reject) => {
252
+ const tx = AppState.db.transaction([STORE_NAME], 'readonly');
253
+ const store = tx.objectStore(STORE_NAME);
254
+ const request = store.get(id);
255
+ request.onsuccess = () => resolve(request.result);
256
+ request.onerror = () => reject(request.error);
257
+ });
258
+ }
259
+ };
260
+
261
  // ============================================
262
  // 图片处理模块
263
  // ============================================
 
318
 
319
  PreviewManager.render();
320
  },
321
+
322
+ // 移除指定索引的图片
323
+ removeAt(index) {
324
+ AppState.currentImages.splice(index, 1);
325
+ PreviewManager.render();
326
+ },
327
+
328
+ // 清空所有上传的图片
329
+ clear() {
330
+ AppState.currentImages = [];
331
+ PreviewManager.render();
332
+ },
333
+
334
+ // 设置图片(用于复用功能)
335
+ setImages(images) {
336
+ AppState.currentImages = images ? [...images] : [];
337
+ PreviewManager.render();
338
+ }
339
+ };
340
+
341
+ // ============================================
342
+ // 预览条管理模块
343
+ // ============================================
344
+ const PreviewManager = {
345
+ container: null,
346
+ uploadBtn: null,
347
+ statusBar: null,
348
+
349
+ init() {
350
+ this.container = document.getElementById('preview-bar');
351
+ this.uploadBtn = document.getElementById('upload-btn');
352
+ this.statusBar = document.getElementById('status-bar');
353
+ },
354
+
355
+ render() {
356
+ // 清空容器
357
+ this.container.innerHTML = '';
358
+
359
+ const images = AppState.currentImages;
360
+
361
+ if (images.length === 0) {
362
+ this.container.classList.remove('visible');
363
+ this.uploadBtn.classList.remove('active');
364
+ this.statusBar.textContent = 'Ready';
365
+ return;
366
+ }
367
+
368
+ this.container.classList.add('visible');
369
+ this.uploadBtn.classList.add('active');
370
+ this.statusBar.textContent = `已选择 ${images.length}/16 张图片`;
371
+
372
+ // 使用 DOM API 创建元素,避免 innerHTML 导致的编码问题
373
+ images.forEach((imgData, index) => {
374
+ const wrapper = document.createElement('div');
375
+ wrapper.className = 'thumb-wrapper';
376
+
377
+ const img = document.createElement('img');
378
+ img.src = imgData; // 直接设置 src,不经过字符串拼接
379
+
380
+ const removeBtn = document.createElement('div');
381
+ removeBtn.className = 'thumb-remove';
382
+ removeBtn.textContent = '×';
383
+ removeBtn.onclick = (e) => {
384
+ e.stopPropagation();
385
+ ImageHandler.removeAt(index);
386
+ };
387
+
388
+ wrapper.appendChild(img);
389
+ wrapper.appendChild(removeBtn);
390
+ this.container.appendChild(wrapper);
391
+ });
392
+ }
393
+ };
394
+
395
+ // ============================================
396
+ // 画廊管理模块
397
+ // ============================================
398
+ const GalleryManager = {
399
+ container: null,
400
+
401
+ init() {
402
+ this.container = document.getElementById('gallery');
403
+ },
404
+
405
+ async load() {
406
+ try {
407
+ AppState.galleryData = await Database.getAll();
408
+ this.render();
409
+ } catch (err) {
410
+ console.error('加载画廊失败:', err);
411
+ }
412
+ },
413
+
414
  render() {
415
  this.container.innerHTML = '';
416
 
 
431
  });
432
  this.container.appendChild(fragment);
433
  },
434
+
435
  createCard(item) {
436
  const el = document.createElement('div');
437
  el.className = 'history-item';
 
500
 
501
  return el;
502
  },
503
+
504
+ downloadImage(item) {
505
+ const link = document.createElement('a');
506
+ link.href = item.image;
507
+ link.download = `banana-pro-${item.id}-${Date.now()}.png`;
508
+ document.body.appendChild(link);
509
+ link.click();
510
+ document.body.removeChild(link);
511
+ },
512
+
513
+ async deleteItem(id) {
514
+ if (!confirm('确定要删除这张图片吗?')) return;
515
+
516
+ try {
517
+ await Database.delete(id);
518
+ await this.load();
519
+ } catch (err) {
520
+ console.error('删除失败:', err);
521
+ alert('删除失败');
522
+ }
523
+ }
524
+ };
525
+
526
+ // ============================================
527
+ // 弹窗管理模块
528
+ // ============================================
529
+ const ModalManager = {
530
+ modal: null,
531
+ imgEl: null,
532
+ promptEl: null,
533
+ refsEl: null,
534
+ reuseBtn: null,
535
+
536
+ init() {
537
+ this.modal = document.getElementById('modal');
538
+ this.imgEl = document.getElementById('m-img');
539
+ this.promptEl = document.getElementById('m-prompt');
540
+ this.refsEl = document.getElementById('m-refs');
541
+ this.reuseBtn = document.getElementById('m-reuse');
542
+
543
+ // 点击背景关闭
544
+ this.modal.onclick = (e) => {
545
+ if (e.target === this.modal) this.close();
546
+ };
547
+
548
+ // ESC 关闭
549
+ document.addEventListener('keydown', (e) => {
550
+ if (e.key === 'Escape') this.close();
551
+ });
552
+ },
553
+
554
+ open(item) {
555
+ AppState.currentModalItem = item;
556
+
557
+ // 设置主图 - 直接赋值
558
+ this.imgEl.src = item.image;
559
+
560
+ // 设置提示词
561
+ this.promptEl.textContent = item.prompt;
562
+
563
+ // 渲染参考图
564
+ this.refsEl.innerHTML = '';
565
+ if (item.inputImages && item.inputImages.length > 0) {
566
+ item.inputImages.forEach(imgData => {
567
+ const thumb = document.createElement('img');
568
+ thumb.className = 'ref-thumb';
569
+ thumb.src = imgData; // 直接赋值
570
+ thumb.onclick = () => window.open(imgData, '_blank');
571
+ this.refsEl.appendChild(thumb);
572
+ });
573
+ }
574
+
575
+ // 绑定复用按钮
576
+ this.reuseBtn.onclick = () => this.reuse();
577
+
578
+ this.modal.style.display = 'flex';
579
+ },
580
+
581
+ close() {
582
+ this.modal.style.display = 'none';
583
+ AppState.currentModalItem = null;
584
+ },
585
+
586
+ reuse() {
587
+ const item = AppState.currentModalItem;
588
+ if (!item) return;
589
+
590
+ // 复用提示词
591
+ const textarea = document.getElementById('prompt');
592
+ textarea.value = item.prompt;
593
+ textarea.style.height = 'auto';
594
+ textarea.style.height = textarea.scrollHeight + 'px';
595
+
596
+ // 复用参考图
597
+ if (item.inputImages && item.inputImages.length > 0) {
598
+ ImageHandler.setImages(item.inputImages);
599
+ } else {
600
+ ImageHandler.clear();
601
+ }
602
+
603
+ this.close();
604
+ textarea.focus();
605
+ }
606
  };
607
 
608
  // ============================================
 
1071
  }
1072
  }
1073
  };
1074
+
1075
+ // ============================================
1076
+ // 认证模块
1077
+ // ============================================
1078
+ const Auth = {
1079
+ async check() {
1080
+ try {
1081
+ const res = await fetch('/api/check-auth');
1082
+ const data = await res.json();
1083
+ return data.authenticated;
1084
+ } catch {
1085
+ return false;
1086
+ }
1087
+ },
1088
+
1089
+ async login(password) {
1090
+ const res = await fetch('/api/login', {
1091
+ method: 'POST',
1092
+ headers: { 'Content-Type': 'application/json' },
1093
+ body: JSON.stringify({ password })
1094
+ });
1095
+ const data = await res.json();
1096
+ return data.success;
1097
+ },
1098
+
1099
+ unlock() {
1100
+ document.getElementById('login-overlay').style.display = 'none';
1101
+ const app = document.getElementById('app');
1102
+ app.style.filter = 'none';
1103
+ app.style.pointerEvents = 'all';
1104
+ }
1105
+ };
1106
+
1107
+ // ============================================
1108
+ // 拖拽上传模块
1109
+ // ============================================
1110
+ const DragDrop = {
1111
+ dropZone: null,
1112
+
1113
+ init() {
1114
+ this.dropZone = document.getElementById('drop-zone');
1115
+
1116
+ // 阻止默认行为
1117
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(event => {
1118
+ this.dropZone.addEventListener(event, (e) => {
1119
+ e.preventDefault();
1120
+ e.stopPropagation();
1121
+ });
1122
+ });
1123
+
1124
+ // 拖入高亮
1125
+ ['dragenter', 'dragover'].forEach(event => {
1126
+ this.dropZone.addEventListener(event, () => {
1127
+ this.dropZone.style.borderColor = 'var(--accent-color)';
1128
+ this.dropZone.style.background = 'rgba(59, 130, 246, 0.1)';
1129
+ });
1130
+ });
1131
+
1132
+ // 拖出恢复
1133
+ ['dragleave', 'drop'].forEach(event => {
1134
+ this.dropZone.addEventListener(event, () => {
1135
+ this.dropZone.style.borderColor = '';
1136
+ this.dropZone.style.background = '';
1137
+ });
1138
+ });
1139
+
1140
+ // 放下处理
1141
+ this.dropZone.addEventListener('drop', async (e) => {
1142
+ const files = e.dataTransfer.files;
1143
+ if (files.length > 0) {
1144
+ await ImageHandler.processFiles(files);
1145
+ }
1146
+ });
1147
+ }
1148
+ };
1149
+
1150
+ // ============================================
1151
+ // 文件选择模块
1152
+ // ============================================
1153
+ const FileSelector = {
1154
+ input: null,
1155
+ btn: null,
1156
+
1157
+ init() {
1158
+ this.input = document.getElementById('file-input');
1159
+ this.btn = document.getElementById('upload-btn');
1160
+
1161
+ this.btn.onclick = () => this.input.click();
1162
+
1163
+ this.input.onchange = async () => {
1164
+ if (this.input.files.length > 0) {
1165
+ await ImageHandler.processFiles(this.input.files);
1166
+ }
1167
+ this.input.value = ''; // 重置以便重复选择
1168
+ };
1169
+ }
1170
+ };
1171
+
1172
  // ============================================
1173
  // 全局函数(供 HTML onclick 调用)
1174
  // ============================================
 
1196
  }
1197
  }
1198
 
1199
+ function closeModal() {
1200
+ ModalManager.close();
1201
+ }
1202
+
1203
+ // ============================================
1204
+ // 应用初始化
1205
+ // ============================================
1206
+ async function initApp() {
1207
+ try {
1208
+ // 初始化数据库
1209
+ await Database.init();
1210
+
1211
  // 初始化各模块
1212
  PreviewManager.init();
1213
  GalleryManager.init();
 
1224
  await loadWorkspaceData();
1225
  }
1226
 
1227
+ console.log('App initialized successfully');
1228
+ } catch (err) {
1229
+ console.error('App initialization failed:', err);
1230
+ }
1231
+ }
1232
+
1233
  // 启动应用
1234
  if (document.readyState === 'loading') {
1235
  document.addEventListener('DOMContentLoaded', initApp);
 
1237
  initApp();
1238
  }
1239
  </script>
1240
+ </body>
1241
  </html>