waeef commited on
Commit
ef373a5
·
verified ·
1 Parent(s): bef3e3d

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +12 -0
  2. index.html +716 -0
  3. package.json +13 -0
  4. server.js +370 -0
  5. style.css +299 -0
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:18-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package*.json ./
6
+ RUN npm install
7
+ COPY . .
8
+
9
+ ENV PORT=7860
10
+ EXPOSE 7860
11
+
12
+ CMD ["npm", "start"]
index.html ADDED
@@ -0,0 +1,716 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ </head>
9
+ <body>
10
+
11
+ <!-- 登录界面 -->
12
+ <div id="login-overlay">
13
+ <div class="login-card glass-panel">
14
+ <h1 style="font-size: 2rem;">🍌</h1>
15
+ <h3 style="margin: 10px 0; color: #cbd5e1;">Banana Pro</h3>
16
+ <input type="password" id="pwd" placeholder="Password" onkeypress="if(event.key==='Enter')doLogin()">
17
+ <button class="btn-3d" onclick="doLogin()" style="width: 100%; padding: 10px;">Unlock</button>
18
+ </div>
19
+ </div>
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>
39
+
40
+ <!-- 下部分:操作栏 -->
41
+ <div class="control-bar">
42
+ <button class="upload-trigger" id="upload-btn" title="上传参考图">📷</button>
43
+ <textarea id="prompt" class="main-input" rows="1" placeholder="描述画面... (支持拖拽图片)"></textarea>
44
+ <button class="btn-3d send-btn" id="send-btn">
45
+ <span>生成</span>
46
+ <div class="loader"></div>
47
+ </button>
48
+ </div>
49
+ </div>
50
+
51
+ <input type="file" id="file-input" multiple accept="image/*" hidden>
52
+ </div>
53
+
54
+ <!-- 详情弹窗 -->
55
+ <div class="modal" id="modal">
56
+ <div class="modal-content glass-panel">
57
+ <button class="close-modal" onclick="closeModal()">×</button>
58
+ <div class="modal-img-area">
59
+ <img id="m-img" src="">
60
+ </div>
61
+ <div class="modal-footer">
62
+ <div class="input-refs" id="m-refs"></div>
63
+ <div class="prompt-display" id="m-prompt"></div>
64
+ <button class="btn-3d" id="m-reuse" style="width: 100%; padding: 12px;">
65
+ 🎨 复用参数与图片
66
+ </button>
67
+ </div>
68
+ </div>
69
+ </div>
70
+
71
+ <script>
72
+ // ============================================
73
+ // 全局状态管理
74
+ // ============================================
75
+ const AppState = {
76
+ db: null,
77
+ currentImages: [], // 当前上传的图片 Base64 数组
78
+ galleryData: [], // 画廊数据缓存
79
+ currentModalItem: null // 当前弹窗显示的项目
80
+ };
81
+
82
+ const DB_NAME = 'BananaProDB_v3';
83
+ const DB_VERSION = 1;
84
+ const STORE_NAME = 'artworks';
85
+
86
+ // ============================================
87
+ // IndexedDB 模块(使用 Blob 存储避免 Base64 问题)
88
+ // ============================================
89
+ const Database = {
90
+ async init() {
91
+ return new Promise((resolve, reject) => {
92
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
93
+
94
+ request.onerror = () => reject(request.error);
95
+
96
+ request.onsuccess = () => {
97
+ AppState.db = request.result;
98
+ resolve();
99
+ };
100
+
101
+ request.onupgradeneeded = (event) => {
102
+ const db = event.target.result;
103
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
104
+ const store = db.createObjectStore(STORE_NAME, {
105
+ keyPath: 'id',
106
+ autoIncrement: true
107
+ });
108
+ store.createIndex('timestamp', 'timestamp', { unique: false });
109
+ }
110
+ };
111
+ });
112
+ },
113
+
114
+ async save(item) {
115
+ return new Promise((resolve, reject) => {
116
+ const tx = AppState.db.transaction([STORE_NAME], 'readwrite');
117
+ const store = tx.objectStore(STORE_NAME);
118
+
119
+ // 直接存储完整对象,不做任何转换
120
+ const record = {
121
+ prompt: item.prompt,
122
+ image: item.image, // 完整的 data:image/... 字符串
123
+ inputImages: item.inputImages || [],
124
+ timestamp: Date.now()
125
+ };
126
+
127
+ const request = store.add(record);
128
+ request.onsuccess = () => resolve(request.result);
129
+ request.onerror = () => reject(request.error);
130
+ });
131
+ },
132
+
133
+ async getAll() {
134
+ return new Promise((resolve, reject) => {
135
+ const tx = AppState.db.transaction([STORE_NAME], 'readonly');
136
+ const store = tx.objectStore(STORE_NAME);
137
+ const request = store.getAll();
138
+
139
+ request.onsuccess = () => {
140
+ // 按时间戳倒序
141
+ const results = request.result.sort((a, b) => b.timestamp - a.timestamp);
142
+ resolve(results);
143
+ };
144
+ request.onerror = () => reject(request.error);
145
+ });
146
+ },
147
+
148
+ async delete(id) {
149
+ return new Promise((resolve, reject) => {
150
+ const tx = AppState.db.transaction([STORE_NAME], 'readwrite');
151
+ const store = tx.objectStore(STORE_NAME);
152
+ const request = store.delete(id);
153
+ request.onsuccess = () => resolve();
154
+ request.onerror = () => reject(request.error);
155
+ });
156
+ },
157
+
158
+ async getById(id) {
159
+ return new Promise((resolve, reject) => {
160
+ const tx = AppState.db.transaction([STORE_NAME], 'readonly');
161
+ const store = tx.objectStore(STORE_NAME);
162
+ const request = store.get(id);
163
+ request.onsuccess = () => resolve(request.result);
164
+ request.onerror = () => reject(request.error);
165
+ });
166
+ }
167
+ };
168
+
169
+ // ============================================
170
+ // 图片处理模块
171
+ // ============================================
172
+ const ImageHandler = {
173
+ // 文件转 Base64
174
+ fileToBase64(file) {
175
+ return new Promise((resolve, reject) => {
176
+ const reader = new FileReader();
177
+ reader.onload = () => resolve(reader.result);
178
+ reader.onerror = () => reject(reader.error);
179
+ reader.readAsDataURL(file);
180
+ });
181
+ },
182
+
183
+ // 处理上传的文件
184
+ async processFiles(files) {
185
+ const maxImages = 16;
186
+ const imageFiles = Array.from(files).filter(f => f.type.startsWith('image/'));
187
+
188
+ for (const file of imageFiles) {
189
+ if (AppState.currentImages.length >= maxImages) {
190
+ alert(`最多只能上传 ${maxImages} 张图片`);
191
+ break;
192
+ }
193
+
194
+ try {
195
+ const base64 = await this.fileToBase64(file);
196
+ AppState.currentImages.push(base64);
197
+ } catch (err) {
198
+ console.error('图片读取失败:', err);
199
+ }
200
+ }
201
+
202
+ PreviewManager.render();
203
+ },
204
+
205
+ // 移除指定索引的图片
206
+ removeAt(index) {
207
+ AppState.currentImages.splice(index, 1);
208
+ PreviewManager.render();
209
+ },
210
+
211
+ // 清空所有上传的图片
212
+ clear() {
213
+ AppState.currentImages = [];
214
+ PreviewManager.render();
215
+ },
216
+
217
+ // 设置图片(用于复用功能)
218
+ setImages(images) {
219
+ AppState.currentImages = images ? [...images] : [];
220
+ PreviewManager.render();
221
+ }
222
+ };
223
+
224
+ // ============================================
225
+ // 预览条管理模块
226
+ // ============================================
227
+ const PreviewManager = {
228
+ container: null,
229
+ uploadBtn: null,
230
+ statusBar: null,
231
+
232
+ init() {
233
+ this.container = document.getElementById('preview-bar');
234
+ this.uploadBtn = document.getElementById('upload-btn');
235
+ this.statusBar = document.getElementById('status-bar');
236
+ },
237
+
238
+ render() {
239
+ // 清空容器
240
+ this.container.innerHTML = '';
241
+
242
+ const images = AppState.currentImages;
243
+
244
+ if (images.length === 0) {
245
+ this.container.classList.remove('visible');
246
+ this.uploadBtn.classList.remove('active');
247
+ this.statusBar.textContent = 'Ready';
248
+ return;
249
+ }
250
+
251
+ this.container.classList.add('visible');
252
+ this.uploadBtn.classList.add('active');
253
+ this.statusBar.textContent = `已选择 ${images.length}/16 张图片`;
254
+
255
+ // 使用 DOM API 创建元素,避免 innerHTML 导致的编码问题
256
+ images.forEach((imgData, index) => {
257
+ const wrapper = document.createElement('div');
258
+ wrapper.className = 'thumb-wrapper';
259
+
260
+ const img = document.createElement('img');
261
+ img.src = imgData; // 直接设置 src,不经过字符串拼接
262
+
263
+ const removeBtn = document.createElement('div');
264
+ removeBtn.className = 'thumb-remove';
265
+ removeBtn.textContent = '×';
266
+ removeBtn.onclick = (e) => {
267
+ e.stopPropagation();
268
+ ImageHandler.removeAt(index);
269
+ };
270
+
271
+ wrapper.appendChild(img);
272
+ wrapper.appendChild(removeBtn);
273
+ this.container.appendChild(wrapper);
274
+ });
275
+ }
276
+ };
277
+
278
+ // ============================================
279
+ // 画廊管理模块
280
+ // ============================================
281
+ const GalleryManager = {
282
+ container: null,
283
+
284
+ init() {
285
+ this.container = document.getElementById('gallery');
286
+ },
287
+
288
+ async load() {
289
+ try {
290
+ AppState.galleryData = await Database.getAll();
291
+ this.render();
292
+ } catch (err) {
293
+ console.error('加载画廊失败:', err);
294
+ }
295
+ },
296
+
297
+ render() {
298
+ this.container.innerHTML = '';
299
+
300
+ if (AppState.galleryData.length === 0) {
301
+ this.container.innerHTML = `
302
+ <div style="grid-column: 1/-1; text-align: center; color: var(--text-sub); padding: 60px 20px;">
303
+ <div style="font-size: 48px; margin-bottom: 10px;">🎨</div>
304
+ <div>暂无作品,开始创作吧!</div>
305
+ </div>
306
+ `;
307
+ return;
308
+ }
309
+
310
+ AppState.galleryData.forEach(item => {
311
+ const card = this.createCard(item);
312
+ this.container.appendChild(card);
313
+ });
314
+ },
315
+
316
+ createCard(item) {
317
+ const el = document.createElement('div');
318
+ el.className = 'history-item';
319
+ el.dataset.id = item.id;
320
+
321
+ // 参考图标记
322
+ if (item.inputImages && item.inputImages.length > 0) {
323
+ const badge = document.createElement('div');
324
+ badge.className = 'item-badge';
325
+ badge.textContent = `📎 ${item.inputImages.length}`;
326
+ el.appendChild(badge);
327
+ }
328
+
329
+ // 主图片 - 使用 DOM API 设置 src
330
+ const img = document.createElement('img');
331
+ img.loading = 'lazy';
332
+ img.src = item.image; // 关键:直接赋值,不用模板字符串
333
+ img.onerror = () => {
334
+ console.error('图片加载失败, ID:', item.id);
335
+ 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>';
336
+ };
337
+ el.appendChild(img);
338
+
339
+ // 操作按钮
340
+ const actions = document.createElement('div');
341
+ actions.className = 'item-actions';
342
+
343
+ const downloadBtn = document.createElement('button');
344
+ downloadBtn.className = 'icon-btn';
345
+ downloadBtn.innerHTML = '⬇';
346
+ downloadBtn.onclick = (e) => {
347
+ e.stopPropagation();
348
+ this.downloadImage(item);
349
+ };
350
+
351
+ const deleteBtn = document.createElement('button');
352
+ deleteBtn.className = 'icon-btn';
353
+ deleteBtn.style.background = 'rgba(239,68,68,0.8)';
354
+ deleteBtn.innerHTML = '🗑';
355
+ deleteBtn.onclick = (e) => {
356
+ e.stopPropagation();
357
+ this.deleteItem(item.id);
358
+ };
359
+
360
+ actions.appendChild(downloadBtn);
361
+ actions.appendChild(deleteBtn);
362
+ el.appendChild(actions);
363
+
364
+ // 点击打开弹窗
365
+ el.onclick = () => ModalManager.open(item);
366
+
367
+ return el;
368
+ },
369
+
370
+ downloadImage(item) {
371
+ const link = document.createElement('a');
372
+ link.href = item.image;
373
+ link.download = `banana-pro-${item.id}-${Date.now()}.png`;
374
+ document.body.appendChild(link);
375
+ link.click();
376
+ document.body.removeChild(link);
377
+ },
378
+
379
+ async deleteItem(id) {
380
+ if (!confirm('确定要删除这张图片吗?')) return;
381
+
382
+ try {
383
+ await Database.delete(id);
384
+ await this.load();
385
+ } catch (err) {
386
+ console.error('删除失败:', err);
387
+ alert('删除失败');
388
+ }
389
+ }
390
+ };
391
+
392
+ // ============================================
393
+ // 弹窗管理模块
394
+ // ============================================
395
+ const ModalManager = {
396
+ modal: null,
397
+ imgEl: null,
398
+ promptEl: null,
399
+ refsEl: null,
400
+ reuseBtn: null,
401
+
402
+ init() {
403
+ this.modal = document.getElementById('modal');
404
+ this.imgEl = document.getElementById('m-img');
405
+ this.promptEl = document.getElementById('m-prompt');
406
+ this.refsEl = document.getElementById('m-refs');
407
+ this.reuseBtn = document.getElementById('m-reuse');
408
+
409
+ // 点击背景关闭
410
+ this.modal.onclick = (e) => {
411
+ if (e.target === this.modal) this.close();
412
+ };
413
+
414
+ // ESC 关闭
415
+ document.addEventListener('keydown', (e) => {
416
+ if (e.key === 'Escape') this.close();
417
+ });
418
+ },
419
+
420
+ open(item) {
421
+ AppState.currentModalItem = item;
422
+
423
+ // 设置主图 - 直接赋值
424
+ this.imgEl.src = item.image;
425
+
426
+ // 设置提示词
427
+ this.promptEl.textContent = item.prompt;
428
+
429
+ // 渲染参考图
430
+ this.refsEl.innerHTML = '';
431
+ if (item.inputImages && item.inputImages.length > 0) {
432
+ item.inputImages.forEach(imgData => {
433
+ const thumb = document.createElement('img');
434
+ thumb.className = 'ref-thumb';
435
+ thumb.src = imgData; // 直接赋值
436
+ thumb.onclick = () => window.open(imgData, '_blank');
437
+ this.refsEl.appendChild(thumb);
438
+ });
439
+ }
440
+
441
+ // 绑定复用按钮
442
+ this.reuseBtn.onclick = () => this.reuse();
443
+
444
+ this.modal.style.display = 'flex';
445
+ },
446
+
447
+ close() {
448
+ this.modal.style.display = 'none';
449
+ AppState.currentModalItem = null;
450
+ },
451
+
452
+ reuse() {
453
+ const item = AppState.currentModalItem;
454
+ if (!item) return;
455
+
456
+ // 复用提示词
457
+ const textarea = document.getElementById('prompt');
458
+ textarea.value = item.prompt;
459
+ textarea.style.height = 'auto';
460
+ textarea.style.height = textarea.scrollHeight + 'px';
461
+
462
+ // 复用参考图
463
+ if (item.inputImages && item.inputImages.length > 0) {
464
+ ImageHandler.setImages(item.inputImages);
465
+ } else {
466
+ ImageHandler.clear();
467
+ }
468
+
469
+ this.close();
470
+ textarea.focus();
471
+ }
472
+ };
473
+
474
+ // ============================================
475
+ // 生成请求模块
476
+ // ============================================
477
+ const Generator = {
478
+ sendBtn: null,
479
+ textarea: null,
480
+
481
+ init() {
482
+ this.sendBtn = document.getElementById('send-btn');
483
+ this.textarea = document.getElementById('prompt');
484
+
485
+ this.sendBtn.onclick = () => this.generate();
486
+
487
+ // 输入框自动高度
488
+ this.textarea.addEventListener('input', () => {
489
+ this.textarea.style.height = 'auto';
490
+ this.textarea.style.height = this.textarea.scrollHeight + 'px';
491
+ });
492
+
493
+ // 回车发送(Shift+Enter 换行)
494
+ this.textarea.addEventListener('keydown', (e) => {
495
+ if (e.key === 'Enter' && !e.shiftKey) {
496
+ e.preventDefault();
497
+ this.generate();
498
+ }
499
+ });
500
+ },
501
+
502
+ setLoading(loading) {
503
+ if (loading) {
504
+ this.sendBtn.classList.add('loading');
505
+ this.sendBtn.disabled = true;
506
+ } else {
507
+ this.sendBtn.classList.remove('loading');
508
+ this.sendBtn.disabled = false;
509
+ }
510
+ },
511
+
512
+ async generate() {
513
+ const prompt = this.textarea.value.trim();
514
+ if (!prompt) {
515
+ alert('请输入提示词');
516
+ return;
517
+ }
518
+
519
+ this.setLoading(true);
520
+
521
+ try {
522
+ const response = await fetch('/api/generate', {
523
+ method: 'POST',
524
+ headers: { 'Content-Type': 'application/json' },
525
+ body: JSON.stringify({
526
+ prompt: prompt,
527
+ images: AppState.currentImages
528
+ })
529
+ });
530
+
531
+ const data = await response.json();
532
+
533
+ if (!response.ok || !data.success) {
534
+ throw new Error(data.message || '生成失败');
535
+ }
536
+
537
+ // 验证返回的图片数据
538
+ if (!data.image || !data.image.startsWith('data:image')) {
539
+ throw new Error('返回的图片数据无效');
540
+ }
541
+
542
+ // 保存到数据库
543
+ await Database.save({
544
+ prompt: prompt,
545
+ image: data.image,
546
+ inputImages: [...AppState.currentImages]
547
+ });
548
+
549
+ // 刷新画廊
550
+ await GalleryManager.load();
551
+
552
+ // 清空输入
553
+ this.textarea.value = '';
554
+ this.textarea.style.height = 'auto';
555
+ ImageHandler.clear();
556
+
557
+ } catch (err) {
558
+ console.error('生成失败:', err);
559
+ alert('生成失败: ' + err.message);
560
+ } finally {
561
+ this.setLoading(false);
562
+ }
563
+ }
564
+ };
565
+
566
+ // ============================================
567
+ // 认证模块
568
+ // ============================================
569
+ const Auth = {
570
+ async check() {
571
+ try {
572
+ const res = await fetch('/api/check-auth');
573
+ const data = await res.json();
574
+ return data.authenticated;
575
+ } catch {
576
+ return false;
577
+ }
578
+ },
579
+
580
+ async login(password) {
581
+ const res = await fetch('/api/login', {
582
+ method: 'POST',
583
+ headers: { 'Content-Type': 'application/json' },
584
+ body: JSON.stringify({ password })
585
+ });
586
+ const data = await res.json();
587
+ return data.success;
588
+ },
589
+
590
+ unlock() {
591
+ document.getElementById('login-overlay').style.display = 'none';
592
+ const app = document.getElementById('app');
593
+ app.style.filter = 'none';
594
+ app.style.pointerEvents = 'all';
595
+ }
596
+ };
597
+
598
+ // ============================================
599
+ // 拖拽上传模块
600
+ // ============================================
601
+ const DragDrop = {
602
+ dropZone: null,
603
+
604
+ init() {
605
+ this.dropZone = document.getElementById('drop-zone');
606
+
607
+ // 阻止默认行为
608
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(event => {
609
+ this.dropZone.addEventListener(event, (e) => {
610
+ e.preventDefault();
611
+ e.stopPropagation();
612
+ });
613
+ });
614
+
615
+ // 拖入高亮
616
+ ['dragenter', 'dragover'].forEach(event => {
617
+ this.dropZone.addEventListener(event, () => {
618
+ this.dropZone.style.borderColor = 'var(--accent-color)';
619
+ this.dropZone.style.background = 'rgba(59, 130, 246, 0.1)';
620
+ });
621
+ });
622
+
623
+ // 拖出恢复
624
+ ['dragleave', 'drop'].forEach(event => {
625
+ this.dropZone.addEventListener(event, () => {
626
+ this.dropZone.style.borderColor = '';
627
+ this.dropZone.style.background = '';
628
+ });
629
+ });
630
+
631
+ // 放下处理
632
+ this.dropZone.addEventListener('drop', async (e) => {
633
+ const files = e.dataTransfer.files;
634
+ if (files.length > 0) {
635
+ await ImageHandler.processFiles(files);
636
+ }
637
+ });
638
+ }
639
+ };
640
+
641
+ // ============================================
642
+ // 文件选择模块
643
+ // ============================================
644
+ const FileSelector = {
645
+ input: null,
646
+ btn: null,
647
+
648
+ init() {
649
+ this.input = document.getElementById('file-input');
650
+ this.btn = document.getElementById('upload-btn');
651
+
652
+ this.btn.onclick = () => this.input.click();
653
+
654
+ this.input.onchange = async () => {
655
+ if (this.input.files.length > 0) {
656
+ await ImageHandler.processFiles(this.input.files);
657
+ }
658
+ this.input.value = ''; // 重置以便重复选择
659
+ };
660
+ }
661
+ };
662
+
663
+ // ============================================
664
+ // 全局函数(供 HTML onclick 调用)
665
+ // ============================================
666
+ async function doLogin() {
667
+ const pwd = document.getElementById('pwd').value;
668
+ if (!pwd) return;
669
+
670
+ const success = await Auth.login(pwd);
671
+ if (success) {
672
+ Auth.unlock();
673
+ await GalleryManager.load();
674
+ } else {
675
+ alert('密码错误');
676
+ }
677
+ }
678
+
679
+ function closeModal() {
680
+ ModalManager.close();
681
+ }
682
+
683
+ // ============================================
684
+ // 应用初始化
685
+ // ============================================
686
+ async function initApp() {
687
+ try {
688
+ // 初始化数据库
689
+ await Database.init();
690
+
691
+ // 初始化各模块
692
+ PreviewManager.init();
693
+ GalleryManager.init();
694
+ ModalManager.init();
695
+ Generator.init();
696
+ DragDrop.init();
697
+ FileSelector.init();
698
+
699
+ // 检查认证状态
700
+ const isAuth = await Auth.check();
701
+ if (isAuth) {
702
+ Auth.unlock();
703
+ await GalleryManager.load();
704
+ }
705
+
706
+ console.log('App initialized successfully');
707
+ } catch (err) {
708
+ console.error('App initialization failed:', err);
709
+ }
710
+ }
711
+
712
+ // 启动应用
713
+ document.addEventListener('DOMContentLoaded', initApp);
714
+ </script>
715
+ </body>
716
+ </html>
package.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "banana-pro-ai",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "start": "node server.js"
7
+ },
8
+ "dependencies": {
9
+ "cookie-parser": "^1.4.6",
10
+ "dotenv": "^16.3.1",
11
+ "express": "^4.18.2"
12
+ }
13
+ }
server.js ADDED
@@ -0,0 +1,370 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * server.js
3
+ * Banana Pro AI 生图平台 - 后端服务
4
+ * 支持多图上传的 ES6 模块化版本
5
+ */
6
+
7
+ import express from 'express';
8
+ import cookieParser from 'cookie-parser';
9
+ import dotenv from 'dotenv';
10
+ import path from 'path';
11
+ import { fileURLToPath } from 'url';
12
+
13
+ // ============================================
14
+ // 配置初始化模块
15
+ // ============================================
16
+ dotenv.config();
17
+
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = path.dirname(__filename);
20
+ const CONFIG = {
21
+ port: process.env.PORT || 3000,
22
+ apiKey: process.env.OPENAI_API_KEY || 'sk-123456',
23
+ apiUrl: process.env.OPENAI_API_URL || 'http://127.0.0.1:8000/v1/chat/completions',
24
+ sitePassword: process.env.SITE_PASSWORD || '123456',
25
+ modelName: process.env.MODEL_NAME || 'banana-pro',
26
+ maxImages: 16
27
+ };
28
+
29
+ // ============================================
30
+ // Express 应用初始化
31
+ // ============================================
32
+ const app = express();
33
+
34
+ app.use(express.json({ limit: '200mb' }));
35
+ app.use(express.urlencoded({ extended: true, limit: '200mb' }));
36
+ app.use(cookieParser());
37
+ app.use(express.static(__dirname));
38
+
39
+ // ============================================
40
+ // 认证中间件
41
+ // ============================================
42
+ const authMiddleware = (req, res, next) => {
43
+ const token = req.cookies.auth_token;
44
+ if (token === CONFIG.sitePassword) {
45
+ next();
46
+ } else {
47
+ res.status(401).json({
48
+ success: false,
49
+ error: 'Unauthorized',
50
+ message: '请先登录'
51
+ });
52
+ }
53
+ };
54
+
55
+ // ============================================
56
+ // 图片数据解析模块
57
+ // ============================================
58
+ const ImageParser = {
59
+ /**
60
+ * 从 assistant content 中提取 base64 图片数据
61
+ * 格式: ![Generated Image](data:image/png;base64,xxxxx)
62
+ */
63
+ extractBase64FromMarkdown(content) {
64
+ if (!content || typeof content !== 'string') {
65
+ return null;
66
+ }
67
+
68
+ // 匹配 Markdown 图片格式
69
+ const markdownPattern = /!\[.*?\]\((data:image\/[a-zA-Z]+;base64,[A-Za-z0-9+/=]+)\)/g;
70
+ const matches = content.match(markdownPattern);
71
+
72
+ if (matches && matches.length > 0) {
73
+ const dataUrlMatch = matches[0].match(/\((data:image\/[a-zA-Z]+;base64,[A-Za-z0-9+/=]+)\)/);
74
+ if (dataUrlMatch && dataUrlMatch[1]) {
75
+ return dataUrlMatch[1];
76
+ }
77
+ }
78
+
79
+ // 备用:直接匹配 data:image 格式
80
+ const directPattern = /(data:image\/[a-zA-Z]+;base64,[A-Za-z0-9+/=]+)/;
81
+ const directMatch = content.match(directPattern);
82
+ if (directMatch && directMatch[1]) {
83
+ return directMatch[1];
84
+ }
85
+
86
+ return null;
87
+ },
88
+
89
+ isValidBase64Image(base64Data) {
90
+ if (!base64Data) return false;
91
+ return base64Data.startsWith('data:image/');
92
+ }
93
+ };
94
+
95
+ // ============================================
96
+ // 消息构建模块
97
+ // ============================================
98
+ const MessageBuilder = {
99
+ /**
100
+ * 构建包含图片的 OpenAI 格式消息
101
+ * @param {string} prompt - 文本提示词
102
+ * @param {Array<string>} images - base64 图片数组
103
+ * @returns {Array} - OpenAI messages 格式
104
+ */
105
+ buildMessages(prompt, images = []) {
106
+ // 如果没有图片,返回纯文本消息
107
+ if (!images || images.length === 0) {
108
+ return [
109
+ {
110
+ role: 'user',
111
+ content: prompt
112
+ }
113
+ ];
114
+ }
115
+
116
+ // 构��多模态消息内容
117
+ const contentParts = [];
118
+
119
+ // 添加所有图片
120
+ images.forEach((imageData, index) => {
121
+ // 提取 base64 数据和 MIME 类型
122
+ const matches = imageData.match(/^data:(image\/[a-zA-Z]+);base64,(.+)$/);
123
+ if (matches) {
124
+ contentParts.push({
125
+ type: 'image_url',
126
+ image_url: {
127
+ url: imageData
128
+ }
129
+ });
130
+ }
131
+ });
132
+
133
+ // 添加文本提示词
134
+ contentParts.push({
135
+ type: 'text',
136
+ text: prompt
137
+ });
138
+
139
+ return [
140
+ {
141
+ role: 'user',
142
+ content: contentParts
143
+ }
144
+ ];
145
+ }
146
+ };
147
+
148
+ // ============================================
149
+ // API 请求模块
150
+ // ============================================
151
+ const APIService = {
152
+ /**
153
+ * 调用生图 API
154
+ * @param {string} prompt - 用户提示词
155
+ * @param {Array<string>} images - 上传的图片数组
156
+ */
157
+ async generateImage(prompt, images = []) {
158
+ const messages = MessageBuilder.buildMessages(prompt, images);
159
+
160
+ const payload = {
161
+ model: CONFIG.modelName,
162
+ messages: messages
163
+ };
164
+
165
+ console.log(`[${new Date().toISOString()}] 请求模型: ${CONFIG.modelName}`);
166
+ console.log(`[${new Date().toISOString()}] 提示词: ${prompt.substring(0, 100)}...`);
167
+ console.log(`[${new Date().toISOString()}] 携带图片数量: ${images.length}`);
168
+
169
+ const response = await fetch(CONFIG.apiUrl, {
170
+ method: 'POST',
171
+ headers: {
172
+ 'Content-Type': 'application/json',
173
+ 'Authorization': `Bearer ${CONFIG.apiKey}`
174
+ },
175
+ body: JSON.stringify(payload)
176
+ });
177
+
178
+ if (!response.ok) {
179
+ const errorText = await response.text();
180
+ throw new Error(`API 请求失败: ${response.status} - ${errorText}`);
181
+ }
182
+
183
+ return await response.json();
184
+ },
185
+
186
+ /**
187
+ * 从响应中提取图片
188
+ */
189
+ extractImageFromResponse(data) {
190
+ if (data.choices && data.choices[0] && data.choices[0].message) {
191
+ const content = data.choices[0].message.content;
192
+
193
+ console.log(`[${new Date().toISOString()}] 收到响应内容长度: ${content?.length || 0}`);
194
+
195
+ const imageData = ImageParser.extractBase64FromMarkdown(content);
196
+
197
+ if (imageData && ImageParser.isValidBase64Image(imageData)) {
198
+ console.log(`[${new Date().toISOString()}] 成功提取图片数据`);
199
+ return imageData;
200
+ }
201
+ }
202
+
203
+ if (data.data && data.data[0]) {
204
+ if (data.data[0].b64_json) {
205
+ return `data:image/png;base64,${data.data[0].b64_json}`;
206
+ }
207
+ if (data.data[0].url) {
208
+ return data.data[0].url;
209
+ }
210
+ }
211
+
212
+ throw new Error('无法从 API 响应中提取图片数据');
213
+ }
214
+ };
215
+
216
+ // ============================================
217
+ // 路由
218
+ // ============================================
219
+
220
+ // 登录
221
+ app.post('/api/login', (req, res) => {
222
+ const { password } = req.body;
223
+
224
+ if (!password) {
225
+ return res.status(400).json({
226
+ success: false,
227
+ message: '请输入密码'
228
+ });
229
+ }
230
+
231
+ if (password === CONFIG.sitePassword) {
232
+ res.cookie('auth_token', password, {
233
+ maxAge: 30 * 24 * 60 * 60 * 1000,
234
+ httpOnly: true,
235
+ sameSite: 'strict'
236
+ });
237
+
238
+ console.log(`[${new Date().toISOString()}] 用户登录成功`);
239
+
240
+ res.json({
241
+ success: true,
242
+ message: '登录成功'
243
+ });
244
+ } else {
245
+ res.status(403).json({
246
+ success: false,
247
+ message: '密码错误'
248
+ });
249
+ }
250
+ });
251
+
252
+ // 验证状态
253
+ app.get('/api/check-auth', (req, res) => {
254
+ const token = req.cookies.auth_token;
255
+ res.json({ authenticated: token === CONFIG.sitePassword });
256
+ });
257
+
258
+ // 生图接口(支持多图上传)
259
+ app.post('/api/generate', authMiddleware, async (req, res) => {
260
+ const { prompt, images } = req.body;
261
+
262
+ // 验证提示词
263
+ if (!prompt || typeof prompt !== 'string') {
264
+ return res.status(400).json({
265
+ success: false,
266
+ message: '请提供有效的提示词'
267
+ });
268
+ }
269
+
270
+ const trimmedPrompt = prompt.trim();
271
+ if (trimmedPrompt.length === 0) {
272
+ return res.status(400).json({
273
+ success: false,
274
+ message: '提示词不能为空'
275
+ });
276
+ }
277
+
278
+ if (trimmedPrompt.length > 32000) {
279
+ return res.status(400).json({
280
+ success: false,
281
+ message: '提示词过长,请限制在 32000 字符以内'
282
+ });
283
+ }
284
+
285
+ // 验证图片数量
286
+ const uploadedImages = images || [];
287
+ if (uploadedImages.length > CONFIG.maxImages) {
288
+ return res.status(400).json({
289
+ success: false,
290
+ message: `最多只能上传 ${CONFIG.maxImages} 张图片`
291
+ });
292
+ }
293
+
294
+ // 验证图片格式
295
+ for (let i = 0; i < uploadedImages.length; i++) {
296
+ if (!ImageParser.isValidBase64Image(uploadedImages[i])) {
297
+ return res.status(400).json({
298
+ success: false,
299
+ message: `第 ${i + 1} 张图片格式无效`
300
+ });
301
+ }
302
+ }
303
+
304
+ try {
305
+ console.log(`[${new Date().toISOString()}] 开始生成图片...`);
306
+
307
+ const apiResponse = await APIService.generateImage(trimmedPrompt, uploadedImages);
308
+ const imageData = APIService.extractImageFromResponse(apiResponse);
309
+
310
+ console.log(`[${new Date().toISOString()}] 图片生成成功`);
311
+
312
+ res.json({
313
+ success: true,
314
+ image: imageData,
315
+ prompt: trimmedPrompt,
316
+ inputImages: uploadedImages, // 返回用户上传的图片,用于历史记录
317
+ timestamp: new Date().toISOString()
318
+ });
319
+
320
+ } catch (error) {
321
+ console.error(`[${new Date().toISOString()}] 生成失败:`, error.message);
322
+
323
+ res.status(500).json({
324
+ success: false,
325
+ message: error.message || '图片生成失败,请稍后重试'
326
+ });
327
+ }
328
+ });
329
+
330
+ // 登出
331
+ app.post('/api/logout', (req, res) => {
332
+ res.clearCookie('auth_token');
333
+ res.json({ success: true, message: '已退出登录' });
334
+ });
335
+
336
+ // 健康检查
337
+ app.get('/api/health', (req, res) => {
338
+ res.json({
339
+ status: 'ok',
340
+ timestamp: new Date().toISOString(),
341
+ model: CONFIG.modelName,
342
+ maxImages: CONFIG.maxImages
343
+ });
344
+ });
345
+
346
+ // 错误处理
347
+ app.use((err, req, res, next) => {
348
+ console.error(`[${new Date().toISOString()}] 服务器错误:`, err);
349
+ res.status(500).json({ success: false, message: '服务器内部错误' });
350
+ });
351
+
352
+ app.use((req, res) => {
353
+ res.status(404).json({ success: false, message: '接口不存在' });
354
+ });
355
+
356
+ // ============================================
357
+ // 启动服务器
358
+ // ============================================
359
+ app.listen(CONFIG.port, () => {
360
+ console.log('==========================================');
361
+ console.log('🍌 Banana Pro AI Studio 服务已启动');
362
+ console.log('==========================================');
363
+ console.log(`📡 服务地址: http://localhost:${CONFIG.port}`);
364
+ console.log(`🤖 使用模型: ${CONFIG.modelName}`);
365
+ console.log(`🖼️ 最大图片: ${CONFIG.maxImages} 张`);
366
+ console.log(`🔗 API 地址: ${CONFIG.apiUrl}`);
367
+ console.log('==========================================');
368
+ });
369
+
370
+ export default app;
style.css ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
65
+ header h2 { font-size: 1.2rem; font-weight: 700; letter-spacing: 0.5px; }
66
+
67
+ /* --- 历史记录区域 --- */
68
+ .history-container {
69
+ flex: 1;
70
+ overflow-y: auto;
71
+ padding: 10px 20px;
72
+ padding-bottom: 180px; /* 给底部输入栏留空间 */
73
+ }
74
+
75
+ .grid-layout {
76
+ display: grid;
77
+ grid-template-columns: repeat(2, 1fr); /* 默认两列 */
78
+ gap: 16px;
79
+ }
80
+
81
+ .history-item {
82
+ position: relative;
83
+ aspect-ratio: 16 / 9;
84
+ border-radius: 12px;
85
+ overflow: hidden;
86
+ border: 1px solid var(--panel-border);
87
+ background: rgba(0,0,0,0.2);
88
+ cursor: pointer;
89
+ }
90
+ .history-item img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.3s; }
91
+ .history-item:hover img { transform: scale(1.05); }
92
+
93
+ .item-badge {
94
+ position: absolute;
95
+ top: 8px; left: 8px;
96
+ background: rgba(59, 130, 246, 0.9);
97
+ font-size: 10px; padding: 2px 6px;
98
+ border-radius: 4px; z-index: 2;
99
+ }
100
+
101
+ .item-actions {
102
+ position: absolute;
103
+ bottom: 0; right: 0; left: 0;
104
+ padding: 10px;
105
+ background: linear-gradient(transparent, rgba(0,0,0,0.8));
106
+ display: flex;
107
+ justify-content: flex-end;
108
+ gap: 8px;
109
+ opacity: 0;
110
+ transition: opacity 0.2s;
111
+ }
112
+ .history-item:hover .item-actions { opacity: 1; }
113
+ /* 移动端默认显示操作烂 */
114
+ @media (hover: none) { .item-actions { opacity: 1; } }
115
+
116
+ .icon-btn {
117
+ width: 32px; height: 32px;
118
+ border-radius: 8px;
119
+ background: rgba(255,255,255,0.15);
120
+ color: white;
121
+ border: none;
122
+ display: flex; align-items: center; justify-content: center;
123
+ backdrop-filter: blur(4px);
124
+ }
125
+
126
+ /* --- 输入区域 (新设计) --- */
127
+ .input-section {
128
+ position: absolute; /* 浮动在底部 */
129
+ bottom: 20px;
130
+ left: 50%;
131
+ transform: translateX(-50%);
132
+ width: calc(100% - 40px);
133
+ max-width: 800px;
134
+ display: flex;
135
+ flex-direction: column;
136
+ border-radius: 20px;
137
+ overflow: hidden;
138
+ box-shadow: 0 10px 30px rgba(0,0,0,0.3);
139
+ z-index: 100;
140
+ }
141
+
142
+ /* 1. 预览层 (位于输入框上方) */
143
+ .preview-bar {
144
+ padding: 0;
145
+ max-height: 0;
146
+ overflow-x: auto;
147
+ overflow-y: hidden;
148
+ background: rgba(15, 23, 42, 0.4);
149
+ display: flex;
150
+ gap: 8px;
151
+ transition: all 0.3s ease;
152
+ white-space: nowrap;
153
+ }
154
+ .preview-bar.visible {
155
+ padding: 12px;
156
+ max-height: 100px; /* 展开 */
157
+ border-bottom: 1px solid var(--panel-border);
158
+ }
159
+
160
+ .thumb-wrapper {
161
+ position: relative;
162
+ width: 64px; height: 64px;
163
+ flex-shrink: 0;
164
+ border-radius: 8px;
165
+ border: 1px solid rgba(255,255,255,0.1);
166
+ overflow: hidden;
167
+ }
168
+ .thumb-wrapper img { width: 100%; height: 100%; object-fit: cover; }
169
+ .thumb-remove {
170
+ position: absolute; top: 2px; right: 2px;
171
+ width: 16px; height: 16px;
172
+ background: rgba(239,68,68,0.9);
173
+ border-radius: 50%;
174
+ display: flex; align-items: center; justify-content: center;
175
+ font-size: 12px; cursor: pointer;
176
+ }
177
+
178
+ /* 2. 控制层 (按钮+输入框) */
179
+ .control-bar {
180
+ display: flex;
181
+ align-items: flex-end;
182
+ padding: 12px;
183
+ gap: 10px;
184
+ }
185
+
186
+ .upload-trigger {
187
+ width: 44px; height: 44px;
188
+ border-radius: 12px;
189
+ background: rgba(255,255,255,0.05);
190
+ border: 1px dashed rgba(255,255,255,0.3);
191
+ color: var(--text-sub);
192
+ display: flex; align-items: center; justify-content: center;
193
+ font-size: 20px;
194
+ flex-shrink: 0;
195
+ transition: all 0.2s;
196
+ }
197
+ .upload-trigger:hover, .upload-trigger.active {
198
+ background: rgba(59, 130, 246, 0.2);
199
+ border-color: var(--accent-color);
200
+ color: var(--accent-color);
201
+ }
202
+
203
+ .main-input {
204
+ flex: 1;
205
+ background: transparent;
206
+ border: none;
207
+ color: white;
208
+ font-size: 16px;
209
+ padding: 10px 5px;
210
+ resize: none;
211
+ max-height: 120px;
212
+ outline: none;
213
+ line-height: 1.5;
214
+ }
215
+ .main-input::placeholder { color: rgba(255,255,255,0.3); }
216
+
217
+ .send-btn {
218
+ height: 44px;
219
+ padding: 0 20px;
220
+ font-size: 15px;
221
+ flex-shrink: 0;
222
+ }
223
+
224
+ /* 加载动画 */
225
+ .loader {
226
+ width: 18px; height: 18px;
227
+ border: 2px solid rgba(255,255,255,0.3);
228
+ border-top-color: white;
229
+ border-radius: 50%;
230
+ animation: spin 0.8s linear infinite;
231
+ display: none;
232
+ }
233
+ .loading .loader { display: block; }
234
+ .loading span { display: none; }
235
+ @keyframes spin { to { transform: rotate(360deg); } }
236
+
237
+ /* --- 弹窗 --- */
238
+ .modal {
239
+ display: none;
240
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
241
+ background: rgba(0,0,0,0.9);
242
+ z-index: 1000;
243
+ justify-content: center; align-items: center;
244
+ backdrop-filter: blur(5px);
245
+ }
246
+ .modal-content {
247
+ background: #1e293b;
248
+ width: 95%; max-width: 1000px; height: 90%;
249
+ border-radius: 16px;
250
+ display: flex; flex-direction: column;
251
+ position: relative;
252
+ border: 1px solid var(--panel-border);
253
+ }
254
+ .close-modal {
255
+ position: absolute; top: 15px; right: 15px;
256
+ width: 36px; height: 36px;
257
+ border-radius: 50%; background: rgba(0,0,0,0.5);
258
+ color: white; border: 1px solid rgba(255,255,255,0.2);
259
+ z-index: 10; font-size: 20px;
260
+ }
261
+ .modal-img-area { flex: 1; display: flex; justify-content: center; align-items: center; background: #000; overflow: hidden; border-radius: 16px 16px 0 0; }
262
+ .modal-img-area img { max-width: 100%; max-height: 100%; object-fit: contain; }
263
+ .modal-footer { padding: 15px; background: #1e293b; border-top: 1px solid var(--panel-border); display: flex; flex-direction: column; gap: 10px; }
264
+ .input-refs { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 5px; }
265
+ .ref-thumb { width: 40px; height: 40px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.2); cursor: pointer; }
266
+ .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; }
267
+
268
+ /* --- 手机响应式设计 --- */
269
+ @media (max-width: 768px) {
270
+ .app-container { padding: 0; }
271
+ .history-container { padding: 10px; }
272
+
273
+ /* 历史记录一行一个 */
274
+ .grid-layout {
275
+ grid-template-columns: 1fr;
276
+ gap: 20px;
277
+ }
278
+
279
+ /* 输入框贴底全宽 */
280
+ .input-section {
281
+ bottom: 0;
282
+ width: 100%;
283
+ max-width: 100%;
284
+ border-radius: 20px 20px 0 0;
285
+ border-bottom: none;
286
+ }
287
+
288
+ .history-container { padding-bottom: 160px; } /* 增加底部留白防止遮挡 */
289
+
290
+ /* 调整按钮点击区域 */
291
+ .send-btn { padding: 0 15px; min-width: 80px; }
292
+ .preview-bar.visible { max-height: 80px; }
293
+ .thumb-wrapper { width: 50px; height: 50px; }
294
+ }
295
+
296
+ /* 登录层 */
297
+ #login-overlay { position: fixed; inset: 0; z-index: 5000; display: flex; justify-content: center; align-items: center; background: #0f172a; }
298
+ .login-card { width: 300px; padding: 30px; text-align: center; }
299
+ .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; }