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