486CHD commited on
Commit
a9e04af
·
verified ·
1 Parent(s): eef8af8

Upload 13 files

Browse files
Files changed (3) hide show
  1. index.html +96 -71
  2. stats.html +175 -92
  3. style.css +51 -34
index.html CHANGED
@@ -22,14 +22,14 @@
22
  }
23
 
24
  header {
25
- margin-top: 115px; /* 为输入区留空间 */
26
  }
27
 
28
  .history-container {
29
  padding-bottom: 20px !important;
30
  }
31
 
32
- /* 调整输入区内部布局 */
33
  .control-bar {
34
  max-width: 1400px;
35
  margin: 0 auto;
@@ -46,37 +46,64 @@
46
  padding: 12px 20px;
47
  }
48
 
49
- /* 动态调整header间距 */
50
  .has-preview header {
51
- margin-top: 260px;
52
  }
53
 
54
  @media (max-width: 768px) {
55
  header {
56
- margin-top: 170px;
57
  }
58
  .has-preview header {
59
- margin-top: 320px;
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
  <!-- 登录界面 -->
@@ -89,16 +116,16 @@
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="ghost-btn" id="clear-btn" title="清空输入" aria-label="清空输入">清空</button>
104
  <button class="btn-3d send-btn" id="send-btn">
@@ -107,8 +134,8 @@
107
  </button>
108
  </div>
109
  <div class="tips-row" id="tips-row">
110
- <span class="tip-pill">提示:写明主体 + 风格 + 光线</span>
111
- <span class="tip-pill">支持参考图:先上传再描述</span>
112
  <span class="tip-pill">画面比例 / 构图 写清楚更稳定</span>
113
  </div>
114
  </div>
@@ -141,7 +168,7 @@
141
  <div class="section-title">
142
  <div>
143
  <h3>社区创意画廊</h3>
144
- <p class="section-subtitle" id="public-gallery-hint">分享你的作品,欣赏社区灵感</p>
145
  </div>
146
  <div class="section-actions">
147
  <button class="icon-btn" id="refresh-public-gallery" title="刷新公共画廊" aria-label="刷新公共画廊">
@@ -175,22 +202,21 @@
175
  <button class="icon-btn" id="m-download" title="保存图片" aria-label="保存图片">保存</button>
176
  </div>
177
  <button class="btn-3d" id="m-reuse" style="width: 100%; padding: 12px;">
178
- 复用参数与图片
179
- </button>
180
  </div>
181
  </div>
182
  </div>
183
 
184
  <script>
185
  // ============================================
186
- // 全局状态管理
187
  // ============================================
188
  const AppState = {
189
  db: null,
190
- currentImages: [], // 当前上传图片的图片 Base64 数组
191
  galleryData: [], // 个人画廊数据缓存
192
  publicGalleryData: [], // 公共画廊数据缓存
193
- currentModalItem: null // 当前弹窗显示的项目
194
  };
195
 
196
  const Device = {
@@ -202,7 +228,7 @@
202
  publicGalleryTokens: 'BananaPro_PublicGallery_Tokens_v1'
203
  };
204
 
205
- const DEFAULT_PUBLIC_HINT = '分享你的作品,欣赏社区灵感';
206
 
207
  const DB_NAME = 'BananaProDB_v3';
208
  const DB_VERSION = 1;
@@ -242,13 +268,13 @@
242
  const tx = AppState.db.transaction([STORE_NAME], 'readwrite');
243
  const store = tx.objectStore(STORE_NAME);
244
 
245
- // 直接存储完整对象,不做任何转换
246
  const record = {
247
  prompt: item.prompt,
248
- image: item.image, // data:image/... 或 /generated/... URL
249
  imageUrl: item.imageUrl || null,
250
  imageId: item.imageId || null,
251
- thumb: item.thumb || null, // 缩略图(用于加速列表渲染)
252
  inputImages: item.inputImages || [],
253
  timestamp: Date.now()
254
  };
@@ -319,7 +345,7 @@
319
  // 图片处理模块
320
  // ============================================
321
  const ImageHandler = {
322
- // 文件转 Base64
323
  fileToBase64(file) {
324
  return new Promise((resolve, reject) => {
325
  const reader = new FileReader();
@@ -329,7 +355,7 @@
329
  });
330
  },
331
 
332
- // 压缩图片(手机端优化)
333
  async compressImage(base64Data, maxWidth = 1280, quality = 0.8) {
334
  return new Promise((resolve) => {
335
  const img = new Image();
@@ -460,14 +486,14 @@
460
  item.displayUrl = null;
461
  },
462
 
463
- // 处理上传图片的文件
464
  async processFiles(files) {
465
  const maxImages = 16;
466
  const imageFiles = Array.from(files).filter(f => f.type.startsWith('image/'));
467
 
468
  for (const file of imageFiles) {
469
  if (AppState.currentImages.length >= maxImages) {
470
- alert(`最多只能上传图片 ${maxImages} 张图片`);
471
  break;
472
  }
473
 
@@ -485,13 +511,13 @@
485
  PreviewManager.render();
486
  },
487
 
488
- // 移除指定索引的图片
489
  removeAt(index) {
490
  AppState.currentImages.splice(index, 1);
491
  PreviewManager.render();
492
  },
493
 
494
- // 清空所有上传图片的图片
495
  clear() {
496
  AppState.currentImages = [];
497
  PreviewManager.render();
@@ -533,7 +559,7 @@
533
 
534
  this.container.classList.add('visible');
535
  this.uploadBtn.classList.add('active');
536
- StatusBar.setText("已选择 " + images.length + '/16 ' + "张图片");
537
 
538
  images.forEach((imgData, index) => {
539
  const wrapper = document.createElement('div');
@@ -792,7 +818,7 @@
792
  this.container.innerHTML = `
793
  <div style="grid-column: 1/-1; text-align: center; color: var(--text-sub); padding: 60px 20px;">
794
  <div style="font-size: 48px; margin-bottom: 10px;">暂无</div>
795
- <div>暂无作品,开始创作吧!</div>
796
  </div>
797
  `;
798
  return;
@@ -837,7 +863,7 @@
837
  if (item.inputImages && item.inputImages.length > 0) {
838
  const badge = document.createElement('div');
839
  badge.className = 'item-badge';
840
- badge.textContent = `参考 ${item.inputImages.length}`;
841
  el.appendChild(badge);
842
  }
843
 
@@ -862,8 +888,8 @@
862
  const shareBtn = document.createElement('button');
863
  shareBtn.className = 'icon-btn share-btn';
864
  shareBtn.textContent = '发布';
865
- shareBtn.title = '发布到公共画廊';
866
- shareBtn.setAttribute('aria-label', '发布到公共画廊');
867
  shareBtn.onclick = (e) => {
868
  e.stopPropagation();
869
  PublicGalleryManager.share(item, shareBtn);
@@ -970,7 +996,7 @@
970
  },
971
 
972
  async deleteItem(id) {
973
- if (!confirm('确定要删除这张图片吗?')) return;
974
 
975
  try {
976
  const targetItem = (AppState.galleryData || []).find((item) => item.id === id);
@@ -1027,19 +1053,19 @@
1027
  open(item) {
1028
  AppState.currentModalItem = item;
1029
 
1030
- // 设置主图 - 直接赋值
1031
  this.imgEl.src = ImageHandler.getDisplayImage(item) || item.thumb || '';
1032
 
1033
- // 设置提示词
1034
  this.promptEl.textContent = item.prompt;
1035
 
1036
- // 渲染参考图
1037
  this.refsEl.innerHTML = '';
1038
  if (item.inputImages && item.inputImages.length > 0) {
1039
  item.inputImages.forEach(imgData => {
1040
  const thumb = document.createElement('img');
1041
  thumb.className = 'ref-thumb';
1042
- thumb.src = imgData; // 直接赋值
1043
  thumb.onclick = () => window.open(imgData, '_blank');
1044
  this.refsEl.appendChild(thumb);
1045
  });
@@ -1071,13 +1097,13 @@
1071
  const item = AppState.currentModalItem;
1072
  if (!item) return;
1073
 
1074
- // 复用提示词
1075
  const textarea = document.getElementById('prompt');
1076
  textarea.value = item.prompt;
1077
  textarea.style.height = 'auto';
1078
  textarea.style.height = textarea.scrollHeight + 'px';
1079
 
1080
- // 复用参考图
1081
  if (item.inputImages && item.inputImages.length > 0) {
1082
  ImageHandler.setImages(item.inputImages);
1083
  } else {
@@ -1240,7 +1266,7 @@
1240
  },
1241
 
1242
  startRealtimeSync() {
1243
- // 实时同步已禁用 - 改为手动刷新提高性能
1244
  },
1245
 
1246
  stopRealtimeSync() {
@@ -1308,7 +1334,7 @@
1308
  async fetch() {
1309
  if (!this.container) return;
1310
 
1311
- // 防止过于频繁的请求
1312
  const now = Date.now();
1313
  if (now - this.lastFetchTime < this.minFetchInterval) {
1314
  return;
@@ -1340,8 +1366,8 @@
1340
 
1341
  let errorMessage = '加载失败,请稍后重试';
1342
  if (error.name === 'AbortError') {
1343
- errorMessage = '加载超时,请检查网络';
1344
- this.setHint('加载超时,请检查网络', 'error');
1345
  } else {
1346
  this.setHint(errorMessage, 'error');
1347
  }
@@ -1548,7 +1574,7 @@
1548
  async share(item, triggerBtn) {
1549
  if (!item) return;
1550
 
1551
- const confirmShare = confirm('确认将这张作品发布到公共画廊?提示词将对所有人可见。');
1552
  if (!confirmShare) {
1553
  return;
1554
  }
@@ -1593,7 +1619,7 @@
1593
  this.render();
1594
  this.setHint('作品已发布至公共画廊', 'success');
1595
  } catch (error) {
1596
- console.error('发布到公共画廊失败:', error);
1597
  alert('发布失败: ' + error.message);
1598
  this.setHint('发布失败,请稍后重试', 'error');
1599
  } finally {
@@ -1604,7 +1630,7 @@
1604
  },
1605
 
1606
  async delete(id) {
1607
- const confirmDelete = confirm('确定要删除这张公共画廊作品吗?');
1608
  if (!confirmDelete) {
1609
  return;
1610
  }
@@ -1628,7 +1654,7 @@
1628
  AppState.publicGalleryData = AppState.publicGalleryData.filter(item => item.id !== id);
1629
  this.removeOwnership(id);
1630
  this.render();
1631
- this.setHint('作品已删除', 'success');
1632
  } catch (error) {
1633
  console.error('删除公共画廊作品失败:', error);
1634
  alert('删除失败: ' + error.message);
@@ -1706,7 +1732,7 @@
1706
  if (this.isGenerating) {
1707
  this.setLoading(false);
1708
  StatusBar.flash("生成超时");
1709
- alert("生成超时,请检查网络后重试");
1710
  }
1711
  }, uiTimeoutMs);
1712
  } else {
@@ -1968,7 +1994,7 @@
1968
  };
1969
 
1970
  // ============================================
1971
- // 全局函数(供 HTML onclick 调用)
1972
  // ============================================
1973
  async function loadWorkspaceData() {
1974
  try {
@@ -2001,12 +2027,11 @@
2001
  }
2002
 
2003
  // ============================================
2004
- // 应用初始化
2005
  // ============================================
2006
  async function initApp() {
2007
  try {
2008
- // 初始化数据库(允许失败,避免移动端被阻塞)
2009
- try {
2010
  await Database.init();
2011
  } catch (dbErr) {
2012
  console.warn('Database init failed:', dbErr);
@@ -2024,8 +2049,7 @@
2024
  DragDrop.init();
2025
  FileSelector.init();
2026
 
2027
- // 检查认证状态
2028
- const isAuth = await Auth.check();
2029
  if (isAuth) {
2030
  Auth.unlock();
2031
  await loadWorkspaceData();
@@ -2046,3 +2070,4 @@
2046
  </script>
2047
  </body>
2048
  </html>
 
 
22
  }
23
 
24
  header {
25
+ margin-top: var(--input-offset, 115px); /* 为输入区留空�?*/
26
  }
27
 
28
  .history-container {
29
  padding-bottom: 20px !important;
30
  }
31
 
32
+ /* 调整输入区内部布�? */
33
  .control-bar {
34
  max-width: 1400px;
35
  margin: 0 auto;
 
46
  padding: 12px 20px;
47
  }
48
 
49
+ /* 动�?�调整header间距 */
50
  .has-preview header {
51
+ margin-top: var(--input-offset, 260px);
52
  }
53
 
54
  @media (max-width: 768px) {
55
  header {
56
+ margin-top: var(--input-offset, 170px);
57
  }
58
  .has-preview header {
59
+ margin-top: var(--input-offset, 320px);
60
  }
61
  }
62
  </style>
63
  </head>
64
  <body>
65
+ <!-- 在预览栏显示时添�?has-preview �?-->
66
+ <script>
67
+ // Observe preview bar changes and input height.
68
+ const observePreviewBar = () => {
69
+ const previewBar = document.getElementById('preview-bar');
70
+ if (!previewBar) return;
71
+ const observer = new MutationObserver(() => {
72
+ if (previewBar.classList.contains('visible')) {
73
+ document.body.classList.add('has-preview');
74
+ } else {
75
+ document.body.classList.remove('has-preview');
76
+ }
77
+ });
78
+ observer.observe(previewBar, { attributes: true, attributeFilter: ['class'] });
79
+ };
80
+
81
+ const updateInputOffset = () => {
82
+ const inputSection = document.querySelector('.input-section');
83
+ if (!inputSection) return;
84
+ const gap = 16;
85
+ const nextOffset = inputSection.offsetHeight + gap;
86
+ document.documentElement.style.setProperty('--input-offset', nextOffset + 'px');
87
+ };
88
+
89
+ const observeInputSection = () => {
90
+ const inputSection = document.querySelector('.input-section');
91
+ if (!inputSection) return;
92
+ updateInputOffset();
93
+ if ('ResizeObserver' in window) {
94
+ const observer = new ResizeObserver(() => {
95
+ requestAnimationFrame(updateInputOffset);
96
+ });
97
+ observer.observe(inputSection);
98
+ } else {
99
+ window.addEventListener('resize', updateInputOffset);
100
+ }
101
+ };
102
+
103
+ document.addEventListener('DOMContentLoaded', () => {
104
+ observePreviewBar();
105
+ observeInputSection();
106
+ });
107
  </script>
108
 
109
  <!-- 登录界面 -->
 
116
  </div>
117
  </div>
118
 
119
+ <!-- 主应�?-->
120
  <div class="app-container" id="app" style="filter: blur(10px); pointer-events: none;">
121
+ <!-- 顶部固定输入�?-->
122
  <div class="input-section glass-panel" id="drop-zone">
123
+ <!-- 上部分:预览�?-->
124
  <div class="preview-bar" id="preview-bar"></div>
125
 
126
+ <!-- 下部分:操作�?-->
127
  <div class="control-bar">
128
+ <button class="upload-trigger" id="upload-btn" title="上传参�?�图">上传图片</button>
129
  <textarea id="prompt" class="main-input" rows="1" placeholder="描述画面... (支持拖拽图片)"></textarea>
130
  <button class="ghost-btn" id="clear-btn" title="清空输入" aria-label="清空输入">清空</button>
131
  <button class="btn-3d send-btn" id="send-btn">
 
134
  </button>
135
  </div>
136
  <div class="tips-row" id="tips-row">
137
+ <span class="tip-pill">提示:写明主�?+ 风格 + 光线</span>
138
+ <span class="tip-pill">支持参�?�图:先上传再描�?/span>
139
  <span class="tip-pill">画面比例 / 构图 写清楚更稳定</span>
140
  </div>
141
  </div>
 
168
  <div class="section-title">
169
  <div>
170
  <h3>社区创意画廊</h3>
171
+ <p class="section-subtitle" id="public-gallery-hint">分享你的作品,欣赏社区灵�?/p>
172
  </div>
173
  <div class="section-actions">
174
  <button class="icon-btn" id="refresh-public-gallery" title="刷新公共画廊" aria-label="刷新公共画廊">
 
202
  <button class="icon-btn" id="m-download" title="保存图片" aria-label="保存图片">保存</button>
203
  </div>
204
  <button class="btn-3d" id="m-reuse" style="width: 100%; padding: 12px;">
205
+ 复用参数与图�? </button>
 
206
  </div>
207
  </div>
208
  </div>
209
 
210
  <script>
211
  // ============================================
212
+ // 全局状�?�管�?
213
  // ============================================
214
  const AppState = {
215
  db: null,
216
+ currentImages: [], // 当前上传图片的图�?Base64 数组
217
  galleryData: [], // 个人画廊数据缓存
218
  publicGalleryData: [], // 公共画廊数据缓存
219
+ currentModalItem: null // 当前弹窗显示的项�?
220
  };
221
 
222
  const Device = {
 
228
  publicGalleryTokens: 'BananaPro_PublicGallery_Tokens_v1'
229
  };
230
 
231
+ const DEFAULT_PUBLIC_HINT = '分享你的作品,欣赏社区灵�?;
232
 
233
  const DB_NAME = 'BananaProDB_v3';
234
  const DB_VERSION = 1;
 
268
  const tx = AppState.db.transaction([STORE_NAME], 'readwrite');
269
  const store = tx.objectStore(STORE_NAME);
270
 
271
+ // 直接存储完整对象,不做任何转�?
272
  const record = {
273
  prompt: item.prompt,
274
+ image: item.image, // data:image/... �?/generated/... URL
275
  imageUrl: item.imageUrl || null,
276
  imageId: item.imageId || null,
277
+ thumb: item.thumb || null, // 缩略图(用于加�?�列表渲染)
278
  inputImages: item.inputImages || [],
279
  timestamp: Date.now()
280
  };
 
345
  // 图片处理模块
346
  // ============================================
347
  const ImageHandler = {
348
+ // 文件�?Base64
349
  fileToBase64(file) {
350
  return new Promise((resolve, reject) => {
351
  const reader = new FileReader();
 
355
  });
356
  },
357
 
358
+ // 压缩图片(手机端优化�?
359
  async compressImage(base64Data, maxWidth = 1280, quality = 0.8) {
360
  return new Promise((resolve) => {
361
  const img = new Image();
 
486
  item.displayUrl = null;
487
  },
488
 
489
+ // 处理上传图片的文�?
490
  async processFiles(files) {
491
  const maxImages = 16;
492
  const imageFiles = Array.from(files).filter(f => f.type.startsWith('image/'));
493
 
494
  for (const file of imageFiles) {
495
  if (AppState.currentImages.length >= maxImages) {
496
+ alert(`�?多只能上传图�?${maxImages} 张图片`);
497
  break;
498
  }
499
 
 
511
  PreviewManager.render();
512
  },
513
 
514
+ // 移除指定索引的图�?
515
  removeAt(index) {
516
  AppState.currentImages.splice(index, 1);
517
  PreviewManager.render();
518
  },
519
 
520
+ // 清空�?有上传图片的图片
521
  clear() {
522
  AppState.currentImages = [];
523
  PreviewManager.render();
 
559
 
560
  this.container.classList.add('visible');
561
  this.uploadBtn.classList.add('active');
562
+ StatusBar.setText("已�?�择 " + images.length + '/16 ' + "张图�?);
563
 
564
  images.forEach((imgData, index) => {
565
  const wrapper = document.createElement('div');
 
818
  this.container.innerHTML = `
819
  <div style="grid-column: 1/-1; text-align: center; color: var(--text-sub); padding: 60px 20px;">
820
  <div style="font-size: 48px; margin-bottom: 10px;">暂无</div>
821
+ <div>暂无作品,开始创作吧�?/div>
822
  </div>
823
  `;
824
  return;
 
863
  if (item.inputImages && item.inputImages.length > 0) {
864
  const badge = document.createElement('div');
865
  badge.className = 'item-badge';
866
+ badge.textContent = `参�??${item.inputImages.length}`;
867
  el.appendChild(badge);
868
  }
869
 
 
888
  const shareBtn = document.createElement('button');
889
  shareBtn.className = 'icon-btn share-btn';
890
  shareBtn.textContent = '发布';
891
+ shareBtn.title = '发布到公共画�?;
892
+ shareBtn.setAttribute('aria-label', '发布到公共画�?);
893
  shareBtn.onclick = (e) => {
894
  e.stopPropagation();
895
  PublicGalleryManager.share(item, shareBtn);
 
996
  },
997
 
998
  async deleteItem(id) {
999
+ if (!confirm('确定要删除这张图片吗�?)) return;
1000
 
1001
  try {
1002
  const targetItem = (AppState.galleryData || []).find((item) => item.id === id);
 
1053
  open(item) {
1054
  AppState.currentModalItem = item;
1055
 
1056
+ // 设置主图 - 直接赋�??
1057
  this.imgEl.src = ImageHandler.getDisplayImage(item) || item.thumb || '';
1058
 
1059
+ // 设置提示�?
1060
  this.promptEl.textContent = item.prompt;
1061
 
1062
+ // 渲染参�?�图
1063
  this.refsEl.innerHTML = '';
1064
  if (item.inputImages && item.inputImages.length > 0) {
1065
  item.inputImages.forEach(imgData => {
1066
  const thumb = document.createElement('img');
1067
  thumb.className = 'ref-thumb';
1068
+ thumb.src = imgData; // 直接赋�??
1069
  thumb.onclick = () => window.open(imgData, '_blank');
1070
  this.refsEl.appendChild(thumb);
1071
  });
 
1097
  const item = AppState.currentModalItem;
1098
  if (!item) return;
1099
 
1100
+ // 复用提示�?
1101
  const textarea = document.getElementById('prompt');
1102
  textarea.value = item.prompt;
1103
  textarea.style.height = 'auto';
1104
  textarea.style.height = textarea.scrollHeight + 'px';
1105
 
1106
+ // 复用参�?�图
1107
  if (item.inputImages && item.inputImages.length > 0) {
1108
  ImageHandler.setImages(item.inputImages);
1109
  } else {
 
1266
  },
1267
 
1268
  startRealtimeSync() {
1269
+ // 实时同步已禁�?- 改为手动刷新提高性能
1270
  },
1271
 
1272
  stopRealtimeSync() {
 
1334
  async fetch() {
1335
  if (!this.container) return;
1336
 
1337
+ // 防止过于频繁的请�?
1338
  const now = Date.now();
1339
  if (now - this.lastFetchTime < this.minFetchInterval) {
1340
  return;
 
1366
 
1367
  let errorMessage = '加载失败,请稍后重试';
1368
  if (error.name === 'AbortError') {
1369
+ errorMessage = '加载超时,请�?查网�?;
1370
+ this.setHint('加载超时,请�?查网�?, 'error');
1371
  } else {
1372
  this.setHint(errorMessage, 'error');
1373
  }
 
1574
  async share(item, triggerBtn) {
1575
  if (!item) return;
1576
 
1577
+ const confirmShare = confirm('确认将这张作品发布到公共画廊?提示词将对�?有人可见�?);
1578
  if (!confirmShare) {
1579
  return;
1580
  }
 
1619
  this.render();
1620
  this.setHint('作品已发布至公共画廊', 'success');
1621
  } catch (error) {
1622
+ console.error('发布到公共画廊失�?', error);
1623
  alert('发布失败: ' + error.message);
1624
  this.setHint('发布失败,请稍后重试', 'error');
1625
  } finally {
 
1630
  },
1631
 
1632
  async delete(id) {
1633
+ const confirmDelete = confirm('确定要删除这张公共画廊作品吗�?);
1634
  if (!confirmDelete) {
1635
  return;
1636
  }
 
1654
  AppState.publicGalleryData = AppState.publicGalleryData.filter(item => item.id !== id);
1655
  this.removeOwnership(id);
1656
  this.render();
1657
+ this.setHint('作品已删�?, 'success');
1658
  } catch (error) {
1659
  console.error('删除公共画廊作品失败:', error);
1660
  alert('删除失败: ' + error.message);
 
1732
  if (this.isGenerating) {
1733
  this.setLoading(false);
1734
  StatusBar.flash("生成超时");
1735
+ alert("生成超时,请�?查网络后重试");
1736
  }
1737
  }, uiTimeoutMs);
1738
  } else {
 
1994
  };
1995
 
1996
  // ============================================
1997
+ // 全局函数(供 HTML onclick 调用�?
1998
  // ============================================
1999
  async function loadWorkspaceData() {
2000
  try {
 
2027
  }
2028
 
2029
  // ============================================
2030
+ // 应用初始�?
2031
  // ============================================
2032
  async function initApp() {
2033
  try {
2034
+ // 初始化数据库(允许失败,避免移动端被阻塞�? try {
 
2035
  await Database.init();
2036
  } catch (dbErr) {
2037
  console.warn('Database init failed:', dbErr);
 
2049
  DragDrop.init();
2050
  FileSelector.init();
2051
 
2052
+ // �?查认证状�? const isAuth = await Auth.check();
 
2053
  if (isAuth) {
2054
  Auth.unlock();
2055
  await loadWorkspaceData();
 
2070
  </script>
2071
  </body>
2072
  </html>
2073
+
stats.html CHANGED
@@ -6,24 +6,49 @@
6
  <title>Banana Pro - &#x7AD9;&#x70B9;&#x7EDF;&#x8BA1;</title>
7
  <link rel="stylesheet" href="style.css">
8
  <style>
9
- body.stats-page {
10
- background: var(--bg-color);
11
- }
12
-
13
- .stats-shell {
14
- max-width: 1200px;
15
- margin: 0 auto;
16
- padding: 120px 20px 80px;
17
- width: 100%;
18
- }
19
-
20
- .stats-header {
21
- display: flex;
22
- align-items: flex-start;
23
- justify-content: space-between;
24
- gap: 16px;
25
- flex-wrap: wrap;
26
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  .stats-title {
29
  margin: 0;
@@ -35,14 +60,24 @@
35
  background-clip: text;
36
  }
37
 
38
- .stats-meta {
39
- display: flex;
40
- gap: 16px;
41
- flex-wrap: wrap;
42
- margin-top: 6px;
43
- color: var(--text-sub);
44
- font-size: 12px;
45
- }
 
 
 
 
 
 
 
 
 
 
46
 
47
  .stats-actions {
48
  display: flex;
@@ -51,34 +86,55 @@
51
  flex-wrap: wrap;
52
  }
53
 
54
- .stats-grid {
55
- display: grid;
56
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
57
- gap: 16px;
58
- margin-top: 24px;
59
- }
60
-
61
- .stats-card {
62
- background: var(--panel-bg);
63
- border: 1px solid var(--panel-border);
64
- border-radius: 18px;
65
- padding: 16px 18px;
66
- box-shadow: 0 12px 30px rgba(0, 0, 0, 0.2);
67
- }
68
-
69
- .stats-label {
70
- color: var(--text-sub);
71
- font-size: 11px;
72
- letter-spacing: 1px;
73
- text-transform: uppercase;
74
- }
75
-
76
- .stats-value {
77
- font-size: 1.8rem;
78
- font-weight: 700;
79
- margin-top: 10px;
80
- color: var(--text-main);
81
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
  .stats-sub {
84
  margin-top: 8px;
@@ -89,28 +145,45 @@
89
  flex-wrap: wrap;
90
  }
91
 
92
- .stats-panel {
93
- margin-top: 20px;
94
- background: var(--panel-bg);
95
- border: 1px solid var(--panel-border);
96
- border-radius: 20px;
97
- padding: 18px 20px;
98
- }
99
-
100
- .panel-title {
101
- font-size: 0.95rem;
102
- color: var(--text-main);
103
- font-weight: 600;
104
- }
105
-
106
- .bars {
107
- margin-top: 16px;
108
- display: grid;
109
- grid-template-columns: repeat(7, 1fr);
110
- gap: 10px;
111
- height: 190px;
112
- align-items: end;
113
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
  .stats-legend {
116
  margin-top: 10px;
@@ -165,14 +238,24 @@
165
  height: 100%;
166
  }
167
 
168
- .bar-fill {
169
- width: 100%;
170
- border-radius: 10px 10px 6px 6px;
171
- background: linear-gradient(180deg, var(--accent-color), var(--accent-secondary));
172
- min-height: 10px;
173
- transition: height 0.4s ease;
174
- box-shadow: 0 8px 20px rgba(59, 130, 246, 0.3);
175
- }
 
 
 
 
 
 
 
 
 
 
176
 
177
  .bar-fill.share {
178
  background: linear-gradient(180deg, #22c55e, #16a34a);
@@ -205,19 +288,19 @@
205
  color: rgba(148, 163, 184, 0.7);
206
  }
207
 
208
- @media (max-width: 768px) {
209
- .stats-shell {
210
- padding: 100px 16px 60px;
211
- }
212
 
213
  .stats-title {
214
  font-size: 1.35rem;
215
  }
216
 
217
- .bars {
218
- height: 140px;
219
- }
220
- }
221
  </style>
222
  </head>
223
  <body class="stats-page">
 
6
  <title>Banana Pro - &#x7AD9;&#x70B9;&#x7EDF;&#x8BA1;</title>
7
  <link rel="stylesheet" href="style.css">
8
  <style>
9
+ body.stats-page {
10
+ background:
11
+ radial-gradient(700px 360px at 12% -10%, rgba(59, 130, 246, 0.18), transparent 60%),
12
+ radial-gradient(700px 420px at 88% -20%, rgba(34, 211, 238, 0.2), transparent 55%),
13
+ var(--bg-color);
14
+ min-height: 100vh;
15
+ }
16
+
17
+ .stats-shell {
18
+ max-width: 1200px;
19
+ margin: 0 auto;
20
+ padding: 120px 20px 80px;
21
+ width: 100%;
22
+ position: relative;
23
+ z-index: 0;
24
+ }
25
+
26
+ .stats-shell::before {
27
+ content: '';
28
+ position: absolute;
29
+ inset: 60px 0 0;
30
+ background:
31
+ radial-gradient(480px 280px at 15% 0%, rgba(59, 130, 246, 0.16), transparent 70%),
32
+ radial-gradient(520px 320px at 85% 10%, rgba(34, 211, 238, 0.12), transparent 70%);
33
+ opacity: 0.75;
34
+ pointer-events: none;
35
+ z-index: 0;
36
+ }
37
+
38
+ .stats-shell > * {
39
+ position: relative;
40
+ z-index: 1;
41
+ }
42
+
43
+ .stats-header {
44
+ display: flex;
45
+ align-items: flex-start;
46
+ justify-content: space-between;
47
+ gap: 16px;
48
+ flex-wrap: wrap;
49
+ padding-bottom: 12px;
50
+ border-bottom: 1px solid var(--panel-border);
51
+ }
52
 
53
  .stats-title {
54
  margin: 0;
 
60
  background-clip: text;
61
  }
62
 
63
+ .stats-meta {
64
+ display: flex;
65
+ gap: 16px;
66
+ flex-wrap: wrap;
67
+ margin-top: 6px;
68
+ color: var(--text-sub);
69
+ font-size: 12px;
70
+ }
71
+
72
+ .stats-meta span {
73
+ display: inline-flex;
74
+ align-items: center;
75
+ gap: 6px;
76
+ padding: 4px 10px;
77
+ border-radius: 999px;
78
+ background: rgba(15, 23, 42, 0.55);
79
+ border: 1px solid rgba(148, 163, 184, 0.2);
80
+ }
81
 
82
  .stats-actions {
83
  display: flex;
 
86
  flex-wrap: wrap;
87
  }
88
 
89
+ .stats-grid {
90
+ display: grid;
91
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
92
+ gap: 18px;
93
+ margin-top: 24px;
94
+ }
95
+
96
+ .stats-card {
97
+ background: var(--panel-bg);
98
+ border: 1px solid var(--panel-border);
99
+ border-radius: 18px;
100
+ padding: 16px 18px;
101
+ box-shadow: 0 14px 32px rgba(0, 0, 0, 0.25);
102
+ position: relative;
103
+ overflow: hidden;
104
+ transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
105
+ }
106
+
107
+ .stats-card::before {
108
+ content: '';
109
+ position: absolute;
110
+ top: 0;
111
+ left: 0;
112
+ right: 0;
113
+ height: 3px;
114
+ background: linear-gradient(90deg, rgba(59, 130, 246, 0.9), rgba(34, 211, 238, 0.9));
115
+ opacity: 0.75;
116
+ }
117
+
118
+ .stats-card:hover {
119
+ transform: translateY(-4px);
120
+ box-shadow: 0 18px 40px rgba(15, 23, 42, 0.35);
121
+ border-color: var(--panel-border-strong);
122
+ }
123
+
124
+ .stats-label {
125
+ color: var(--text-sub);
126
+ font-size: 11px;
127
+ letter-spacing: 1px;
128
+ text-transform: uppercase;
129
+ }
130
+
131
+ .stats-value {
132
+ font-size: 2rem;
133
+ font-weight: 700;
134
+ margin-top: 10px;
135
+ color: var(--text-main);
136
+ letter-spacing: 0.5px;
137
+ }
138
 
139
  .stats-sub {
140
  margin-top: 8px;
 
145
  flex-wrap: wrap;
146
  }
147
 
148
+ .stats-panel {
149
+ margin-top: 20px;
150
+ background: linear-gradient(135deg, rgba(15, 23, 42, 0.7), rgba(15, 23, 42, 0.95));
151
+ border: 1px solid var(--panel-border);
152
+ border-radius: 20px;
153
+ padding: 18px 20px;
154
+ box-shadow: 0 16px 36px rgba(0, 0, 0, 0.25);
155
+ }
156
+
157
+ .panel-title {
158
+ font-size: 0.95rem;
159
+ color: var(--text-main);
160
+ font-weight: 600;
161
+ display: flex;
162
+ align-items: center;
163
+ gap: 8px;
164
+ }
165
+
166
+ .panel-title::before {
167
+ content: '';
168
+ width: 10px;
169
+ height: 10px;
170
+ border-radius: 999px;
171
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.9), rgba(34, 211, 238, 0.9));
172
+ box-shadow: 0 0 10px rgba(34, 211, 238, 0.45);
173
+ }
174
+
175
+ .bars {
176
+ margin-top: 16px;
177
+ display: grid;
178
+ grid-template-columns: repeat(7, 1fr);
179
+ gap: 10px;
180
+ height: 190px;
181
+ align-items: end;
182
+ padding: 12px;
183
+ border-radius: 16px;
184
+ background: rgba(15, 23, 42, 0.35);
185
+ border: 1px solid rgba(148, 163, 184, 0.18);
186
+ }
187
 
188
  .stats-legend {
189
  margin-top: 10px;
 
238
  height: 100%;
239
  }
240
 
241
+ .bar-fill {
242
+ width: 100%;
243
+ border-radius: 10px 10px 6px 6px;
244
+ background: linear-gradient(180deg, var(--accent-color), var(--accent-secondary));
245
+ min-height: 10px;
246
+ transition: height 0.4s ease;
247
+ box-shadow: 0 8px 20px rgba(59, 130, 246, 0.3);
248
+ position: relative;
249
+ overflow: hidden;
250
+ }
251
+
252
+ .bar-fill::after {
253
+ content: '';
254
+ position: absolute;
255
+ inset: 0;
256
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.25), transparent 65%);
257
+ opacity: 0.55;
258
+ }
259
 
260
  .bar-fill.share {
261
  background: linear-gradient(180deg, #22c55e, #16a34a);
 
288
  color: rgba(148, 163, 184, 0.7);
289
  }
290
 
291
+ @media (max-width: 768px) {
292
+ .stats-shell {
293
+ padding: 100px 16px 60px;
294
+ }
295
 
296
  .stats-title {
297
  font-size: 1.35rem;
298
  }
299
 
300
+ .bars {
301
+ height: 140px;
302
+ }
303
+ }
304
  </style>
305
  </head>
306
  <body class="stats-page">
style.css CHANGED
@@ -55,29 +55,29 @@ body {
55
  will-change: transform;
56
  }
57
 
58
- @media (max-width: 768px) {
59
- .status-meter {
60
- width: 120px;
61
-
62
- .tips-row {
63
- padding: 0 16px 10px;
64
- }
65
-
66
- .tip-pill {
67
- font-size: 11px;
68
- }
69
- }
70
-
71
- .prompt-link {
72
- font-size: 0.75rem;
73
- padding: 5px 10px;
74
- }
75
-
76
- .glass-panel {
77
- backdrop-filter: blur(10px) saturate(120%);
78
- -webkit-backdrop-filter: blur(10px) saturate(120%);
79
- }
80
- }
81
 
82
  .glass-panel::before {
83
  content: '';
@@ -183,7 +183,7 @@ header {
183
  justify-content: space-between;
184
  align-items: center;
185
  flex-shrink: 0;
186
- margin-top: 190px;
187
  transition: margin-top var(--transition-normal);
188
  }
189
 
@@ -314,7 +314,7 @@ header h2 {
314
 
315
 
316
  body.has-preview header {
317
- margin-top: 260px;
318
  }
319
 
320
  /* --- History Gallery --- */
@@ -1340,11 +1340,28 @@ body.has-preview header {
1340
  }
1341
 
1342
  /* --- Mobile Responsive Design --- */
1343
- @media (max-width: 768px) {
1344
- .grid-layout {
1345
- grid-template-columns: 1fr;
1346
- gap: 12px;
1347
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1348
 
1349
  .public-gallery-section {
1350
  padding: 20px;
@@ -1461,7 +1478,7 @@ body.has-preview header {
1461
  }
1462
 
1463
  /* --- Tablet Responsive Design --- */
1464
- @media (min-width: 768px) and (max-width: 1024px) {
1465
  .grid-layout {
1466
  grid-template-columns: repeat(2, 1fr);
1467
  gap: 16px;
@@ -1483,13 +1500,13 @@ body.has-preview header {
1483
  }
1484
 
1485
  header {
1486
- margin-top: 190px;
1487
  }
1488
 
1489
  body.has-preview header {
1490
- margin-top: 360px;
1491
  }
1492
- }
1493
 
1494
  /* --- Large Screen Optimization --- */
1495
  @media (min-width: 1200px) {
 
55
  will-change: transform;
56
  }
57
 
58
+ @media (max-width: 768px) {
59
+ .status-meter {
60
+ width: 120px;
61
+ }
62
+
63
+ .tips-row {
64
+ padding: 0 16px 10px;
65
+ }
66
+
67
+ .tip-pill {
68
+ font-size: 11px;
69
+ }
70
+
71
+ .prompt-link {
72
+ font-size: 0.75rem;
73
+ padding: 5px 10px;
74
+ }
75
+
76
+ .glass-panel {
77
+ backdrop-filter: blur(10px) saturate(120%);
78
+ -webkit-backdrop-filter: blur(10px) saturate(120%);
79
+ }
80
+ }
81
 
82
  .glass-panel::before {
83
  content: '';
 
183
  justify-content: space-between;
184
  align-items: center;
185
  flex-shrink: 0;
186
+ margin-top: var(--input-offset, 190px);
187
  transition: margin-top var(--transition-normal);
188
  }
189
 
 
314
 
315
 
316
  body.has-preview header {
317
+ margin-top: var(--input-offset, 260px);
318
  }
319
 
320
  /* --- History Gallery --- */
 
1340
  }
1341
 
1342
  /* --- Mobile Responsive Design --- */
1343
+ @media (max-width: 768px) {
1344
+ body {
1345
+ height: auto;
1346
+ min-height: 100vh;
1347
+ overflow-y: auto;
1348
+ overflow-x: hidden;
1349
+ -webkit-overflow-scrolling: touch;
1350
+ }
1351
+
1352
+ .app-container {
1353
+ height: auto;
1354
+ min-height: 100vh;
1355
+ }
1356
+
1357
+ .history-container {
1358
+ overflow: visible;
1359
+ }
1360
+
1361
+ .grid-layout {
1362
+ grid-template-columns: 1fr;
1363
+ gap: 12px;
1364
+ }
1365
 
1366
  .public-gallery-section {
1367
  padding: 20px;
 
1478
  }
1479
 
1480
  /* --- Tablet Responsive Design --- */
1481
+ @media (min-width: 768px) and (max-width: 1024px) {
1482
  .grid-layout {
1483
  grid-template-columns: repeat(2, 1fr);
1484
  gap: 16px;
 
1500
  }
1501
 
1502
  header {
1503
+ margin-top: var(--input-offset, 190px);
1504
  }
1505
 
1506
  body.has-preview header {
1507
+ margin-top: var(--input-offset, 360px);
1508
  }
1509
+ }
1510
 
1511
  /* --- Large Screen Optimization --- */
1512
  @media (min-width: 1200px) {