Files changed (3) hide show
  1. index.html +19 -10
  2. server.js +10 -13
  3. style.css +169 -127
index.html CHANGED
@@ -121,7 +121,7 @@
121
  <section class="public-gallery-section glass-panel">
122
  <div class="section-title">
123
  <div>
124
- <h3>公共画廊</h3>
125
  <p class="section-subtitle" id="public-gallery-hint">分享你的作品,欣赏社区灵感</p>
126
  </div>
127
  <div class="section-actions">
@@ -585,6 +585,8 @@
585
  isLoading: false,
586
  defaultHint: DEFAULT_PUBLIC_HINT,
587
  hintTimer: null,
 
 
588
 
589
  init() {
590
  this.container = document.getElementById('public-gallery');
@@ -599,6 +601,20 @@
599
  if (this.refreshBtn) {
600
  this.refreshBtn.onclick = () => this.fetch();
601
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
602
  },
603
 
604
  loadTokens() {
@@ -746,7 +762,7 @@
746
  },
747
 
748
  canDelete(id) {
749
- return Boolean(this.tokens[id]);
750
  },
751
 
752
  markOwned(id, token) {
@@ -807,12 +823,6 @@
807
  },
808
 
809
  async delete(id) {
810
- const token = this.tokens[id];
811
- if (!token) {
812
- alert('无法获取删除凭证,无法删除该作品');
813
- return;
814
- }
815
-
816
  const confirmDelete = confirm('确定要删除这张公共画廊作品吗?');
817
  if (!confirmDelete) {
818
  return;
@@ -821,8 +831,7 @@
821
  try {
822
  const res = await fetch(`/api/public-gallery/${id}`, {
823
  method: 'DELETE',
824
- headers: { 'Content-Type': 'application/json' },
825
- body: JSON.stringify({ deleteToken: token })
826
  });
827
 
828
  const data = await res.json();
 
121
  <section class="public-gallery-section glass-panel">
122
  <div class="section-title">
123
  <div>
124
+ <h3>✨ 社区创意画廊</h3>
125
  <p class="section-subtitle" id="public-gallery-hint">分享你的作品,欣赏社区灵感</p>
126
  </div>
127
  <div class="section-actions">
 
585
  isLoading: false,
586
  defaultHint: DEFAULT_PUBLIC_HINT,
587
  hintTimer: null,
588
+ syncTimer: null,
589
+ syncInterval: 10000,
590
 
591
  init() {
592
  this.container = document.getElementById('public-gallery');
 
601
  if (this.refreshBtn) {
602
  this.refreshBtn.onclick = () => this.fetch();
603
  }
604
+
605
+ this.startRealtimeSync();
606
+ },
607
+
608
+ startRealtimeSync() {
609
+ this.fetch();
610
+ this.syncTimer = setInterval(() => this.fetch(), this.syncInterval);
611
+ },
612
+
613
+ stopRealtimeSync() {
614
+ if (this.syncTimer) {
615
+ clearInterval(this.syncTimer);
616
+ this.syncTimer = null;
617
+ }
618
  },
619
 
620
  loadTokens() {
 
762
  },
763
 
764
  canDelete(id) {
765
+ return true;
766
  },
767
 
768
  markOwned(id, token) {
 
823
  },
824
 
825
  async delete(id) {
 
 
 
 
 
 
826
  const confirmDelete = confirm('确定要删除这张公共画廊作品吗?');
827
  if (!confirmDelete) {
828
  return;
 
831
  try {
832
  const res = await fetch(`/api/public-gallery/${id}`, {
833
  method: 'DELETE',
834
+ headers: { 'Content-Type': 'application/json' }
 
835
  });
836
 
837
  const data = await res.json();
server.js CHANGED
@@ -478,33 +478,30 @@ app.post('/api/public-gallery', authMiddleware, async (req, res) => {
478
  }
479
  });
480
 
481
- // 公共画廊 - 删除作品
482
- app.delete('/api/public-gallery/:id', authMiddleware, async (req, res) => {
483
- const { deleteToken } = req.body || {};
484
 
485
- if (!deleteToken || typeof deleteToken !== 'string') {
486
  return res.status(400).json({
487
  success: false,
488
- message: '缺少删除凭证'
489
  });
490
  }
491
 
492
  try {
493
- const result = await PublicGalleryStore.remove(req.params.id, deleteToken);
 
494
 
495
- if (!result.found) {
496
  return res.status(404).json({
497
  success: false,
498
  message: '作品不存在或已被删除'
499
  });
500
  }
501
 
502
- if (result.authorized === false) {
503
- return res.status(403).json({
504
- success: false,
505
- message: '无权删除该作品'
506
- });
507
- }
508
 
509
  res.json({ success: true, message: '删除成功' });
510
  } catch (error) {
 
478
  }
479
  });
480
 
481
+ // 公共画廊 - 删除作品(允许任何人删除)
482
+ app.delete('/api/public-gallery/:id', async (req, res) => {
483
+ const id = req.params.id;
484
 
485
+ if (!id || typeof id !== 'string') {
486
  return res.status(400).json({
487
  success: false,
488
+ message: '无效的作品 ID'
489
  });
490
  }
491
 
492
  try {
493
+ const items = await PublicGalleryStore.readData();
494
+ const targetIndex = items.findIndex(item => item.id === id);
495
 
496
+ if (targetIndex === -1) {
497
  return res.status(404).json({
498
  success: false,
499
  message: '作品不存在或已被删除'
500
  });
501
  }
502
 
503
+ items.splice(targetIndex, 1);
504
+ await PublicGalleryStore.writeData(items);
 
 
 
 
505
 
506
  res.json({ success: true, message: '删除成功' });
507
  } catch (error) {
style.css CHANGED
@@ -206,12 +206,27 @@ body.has-preview header {
206
  }
207
 
208
  .public-gallery-section {
209
- margin-top: 30px;
210
- padding: 20px;
211
- border-radius: 20px;
212
  border: 1px solid var(--panel-border);
213
- background: rgba(15, 23, 42, 0.7);
214
- box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  }
216
 
217
  .section-title {
@@ -220,50 +235,60 @@ body.has-preview header {
220
  align-items: flex-start;
221
  gap: 16px;
222
  flex-wrap: wrap;
 
223
  }
224
 
225
  .section-title h3 {
226
  margin: 0;
227
- font-size: 1rem;
228
  letter-spacing: 0.5px;
 
 
 
 
 
229
  }
230
 
231
  .section-subtitle {
232
- margin-top: 6px;
233
- font-size: 0.9rem;
234
  color: var(--text-sub);
235
  transition: color var(--transition-fast);
236
  }
237
 
238
  .section-subtitle.is-success {
239
  color: #34d399;
 
240
  }
241
 
242
  .section-subtitle.is-error {
243
  color: #f87171;
 
244
  }
245
 
246
  .section-actions {
247
  display: flex;
248
  align-items: center;
249
- gap: 10px;
250
  }
251
 
252
  .public-grid {
253
- margin-top: 20px;
254
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
255
- gap: 18px;
256
  }
257
 
258
  .public-card {
259
  position: relative;
260
- border-radius: 14px;
261
  overflow: hidden;
262
  border: 1px solid var(--panel-border);
263
  background: rgba(15, 23, 42, 0.85);
264
  aspect-ratio: 16 / 9;
265
  cursor: pointer;
266
- transition: transform var(--transition-normal), box-shadow var(--transition-normal);
 
 
267
  }
268
 
269
  .public-card img {
@@ -275,43 +300,58 @@ body.has-preview header {
275
  }
276
 
277
  .public-card:hover {
278
- transform: translateY(-4px);
279
- box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45);
 
280
  }
281
 
282
  .public-card:hover img {
283
- transform: scale(1.05);
284
  }
285
 
 
 
 
 
 
 
 
286
  .public-card-info {
287
  position: absolute;
288
  left: 0;
289
  right: 0;
290
  bottom: 0;
291
- padding: 14px;
292
- background: linear-gradient(180deg, rgba(15, 23, 42, 0) 0%, rgba(15, 23, 42, 0.85) 60%, rgba(15, 23, 42, 0.95) 100%);
293
  display: flex;
294
  flex-direction: column;
295
- gap: 8px;
 
 
 
 
 
 
296
  }
297
 
298
  .public-card-prompt {
299
  margin: 0;
300
  font-size: 0.9rem;
301
- color: #f8fafc;
302
  line-height: 1.4;
303
  display: -webkit-box;
304
- -webkit-line-clamp: 3;
305
  -webkit-box-orient: vertical;
306
  overflow: hidden;
 
307
  }
308
 
309
  .public-card-footer {
310
  display: flex;
311
  justify-content: space-between;
312
  align-items: center;
313
- font-size: 0.8rem;
314
- color: var(--text-sub);
315
  gap: 10px;
316
  }
317
 
@@ -319,16 +359,31 @@ body.has-preview header {
319
  grid-column: 1 / -1;
320
  text-align: center;
321
  color: var(--text-sub);
322
- padding: 40px 20px;
323
- background: rgba(15, 23, 42, 0.4);
324
- border-radius: 16px;
325
- border: 1px dashed var(--panel-border);
 
 
 
 
 
 
326
  }
327
 
328
  .icon-btn.small {
329
- width: 32px;
330
- height: 32px;
331
  font-size: 14px;
 
 
 
 
 
 
 
 
 
332
  }
333
 
334
  .icon-btn.share-btn {
@@ -341,8 +396,8 @@ body.has-preview header {
341
  }
342
 
343
  .icon-btn.public-delete-btn {
344
- background: rgba(239, 68, 68, 0.85);
345
- border-color: rgba(248, 113, 113, 0.6);
346
  }
347
 
348
  .icon-btn.spinning {
@@ -964,119 +1019,82 @@ body.has-preview header {
964
 
965
  /* --- Mobile Responsive Design --- */
966
  @media (max-width: 768px) {
967
- .app-container {
968
- padding: 0;
969
- }
970
-
971
- .history-container {
972
- padding: 10px;
973
- padding-bottom: 10px;
974
- }
975
-
976
- header {
977
- margin-top: 60px;
978
- padding: 12px 15px;
979
  }
980
-
981
- header h2 {
982
  font-size: 1.1rem;
983
  }
984
-
985
- body.has-preview header {
986
- margin-top: 140px;
987
- }
988
-
989
- .grid-layout {
990
- grid-template-columns: 1fr;
991
- gap: 15px;
992
- }
993
-
994
- .public-gallery-section {
995
- margin-top: 15px;
996
- padding: 15px;
997
  }
998
-
999
  .public-grid {
1000
- grid-template-columns: 1fr;
1001
- }
1002
-
1003
- .control-bar {
1004
- padding: 10px;
1005
- gap: 8px;
1006
  }
1007
-
1008
- .preview-bar.visible {
1009
- max-height: 80px;
1010
- padding: 10px;
1011
- }
1012
-
1013
- .send-btn {
1014
- padding: 0 15px;
1015
- min-width: 70px;
1016
- font-size: 14px;
1017
  }
1018
-
1019
- .thumb-wrapper {
1020
- width: 50px;
1021
- height: 50px;
1022
  }
1023
-
1024
- .upload-trigger {
1025
- width: 40px;
1026
- height: 40px;
1027
- font-size: 18px;
1028
  }
1029
-
1030
- .main-input {
1031
- font-size: 15px;
1032
- padding: 8px 5px;
 
1033
  }
1034
-
1035
- .modal-content {
1036
- width: 98%;
1037
- height: 95%;
1038
  border-radius: 16px;
 
1039
  }
1040
-
1041
- .modal-footer {
1042
- padding: 15px;
1043
- gap: 10px;
1044
  }
1045
-
1046
- .close-modal {
1047
- width: 36px;
1048
- height: 36px;
1049
- font-size: 20px;
1050
- top: 10px;
1051
- right: 10px;
1052
  }
1053
-
1054
- .login-card {
1055
- width: 90%;
1056
- max-width: 320px;
1057
- padding: 30px 25px;
1058
  }
1059
-
1060
- .login-card h1 {
1061
- font-size: 2.5rem;
1062
  }
1063
-
1064
- /* Reduce glass effects on mobile for performance */
1065
- .glass-panel {
1066
- backdrop-filter: blur(12px) saturate(150%);
1067
- -webkit-backdrop-filter: blur(12px) saturate(150%);
1068
  }
1069
-
1070
- /* Better touch targets */
1071
- .icon-btn {
1072
- width: 40px;
1073
- height: 40px;
1074
- font-size: 18px;
1075
  }
1076
-
1077
- .item-badge {
1078
- font-size: 9px;
1079
- padding: 3px 6px;
1080
  }
1081
  }
1082
 
@@ -1086,6 +1104,16 @@ body.has-preview header {
1086
  grid-template-columns: repeat(2, 1fr);
1087
  gap: 16px;
1088
  }
 
 
 
 
 
 
 
 
 
 
1089
 
1090
  .modal-content {
1091
  width: 90%;
@@ -1107,6 +1135,20 @@ body.has-preview header {
1107
  grid-template-columns: repeat(3, 1fr);
1108
  gap: 20px;
1109
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1110
 
1111
  .history-container {
1112
  padding: 15px 30px;
 
206
  }
207
 
208
  .public-gallery-section {
209
+ margin-top: 40px;
210
+ padding: 32px;
211
+ border-radius: 24px;
212
  border: 1px solid var(--panel-border);
213
+ background: linear-gradient(135deg, rgba(15, 23, 42, 0.8) 0%, rgba(20, 30, 50, 0.8) 100%);
214
+ backdrop-filter: blur(20px) saturate(180%);
215
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
216
+ animation: slideUp 0.6s ease-out;
217
+ position: relative;
218
+ overflow: hidden;
219
+ }
220
+
221
+ .public-gallery-section::before {
222
+ content: '';
223
+ position: absolute;
224
+ top: 0;
225
+ left: 0;
226
+ right: 0;
227
+ height: 1px;
228
+ background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.1) 50%, transparent 100%);
229
+ opacity: 0.5;
230
  }
231
 
232
  .section-title {
 
235
  align-items: flex-start;
236
  gap: 16px;
237
  flex-wrap: wrap;
238
+ position: relative;
239
  }
240
 
241
  .section-title h3 {
242
  margin: 0;
243
+ font-size: 1.3rem;
244
  letter-spacing: 0.5px;
245
+ background: linear-gradient(135deg, var(--text-main) 0%, var(--text-sub) 100%);
246
+ -webkit-background-clip: text;
247
+ -webkit-text-fill-color: transparent;
248
+ background-clip: text;
249
+ font-weight: 700;
250
  }
251
 
252
  .section-subtitle {
253
+ margin-top: 8px;
254
+ font-size: 0.95rem;
255
  color: var(--text-sub);
256
  transition: color var(--transition-fast);
257
  }
258
 
259
  .section-subtitle.is-success {
260
  color: #34d399;
261
+ font-weight: 500;
262
  }
263
 
264
  .section-subtitle.is-error {
265
  color: #f87171;
266
+ font-weight: 500;
267
  }
268
 
269
  .section-actions {
270
  display: flex;
271
  align-items: center;
272
+ gap: 12px;
273
  }
274
 
275
  .public-grid {
276
+ margin-top: 28px;
277
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
278
+ gap: 24px;
279
  }
280
 
281
  .public-card {
282
  position: relative;
283
+ border-radius: 18px;
284
  overflow: hidden;
285
  border: 1px solid var(--panel-border);
286
  background: rgba(15, 23, 42, 0.85);
287
  aspect-ratio: 16 / 9;
288
  cursor: pointer;
289
+ transition: all var(--transition-normal);
290
+ box-shadow: 0 10px 35px rgba(0, 0, 0, 0.3);
291
+ animation: scaleIn 0.5s ease-out backwards;
292
  }
293
 
294
  .public-card img {
 
300
  }
301
 
302
  .public-card:hover {
303
+ transform: translateY(-8px);
304
+ box-shadow: 0 20px 50px rgba(59, 130, 246, 0.3);
305
+ border-color: rgba(59, 130, 246, 0.4);
306
  }
307
 
308
  .public-card:hover img {
309
+ transform: scale(1.08);
310
  }
311
 
312
+ .public-card:nth-child(1) { animation-delay: 0.05s; }
313
+ .public-card:nth-child(2) { animation-delay: 0.1s; }
314
+ .public-card:nth-child(3) { animation-delay: 0.15s; }
315
+ .public-card:nth-child(4) { animation-delay: 0.2s; }
316
+ .public-card:nth-child(5) { animation-delay: 0.25s; }
317
+ .public-card:nth-child(6) { animation-delay: 0.3s; }
318
+
319
  .public-card-info {
320
  position: absolute;
321
  left: 0;
322
  right: 0;
323
  bottom: 0;
324
+ padding: 16px;
325
+ background: linear-gradient(180deg, rgba(15, 23, 42, 0) 0%, rgba(15, 23, 42, 0.7) 40%, rgba(15, 23, 42, 0.95) 100%);
326
  display: flex;
327
  flex-direction: column;
328
+ gap: 10px;
329
+ transition: all var(--transition-normal);
330
+ }
331
+
332
+ .public-card:hover .public-card-info {
333
+ padding-bottom: 20px;
334
+ background: linear-gradient(180deg, rgba(15, 23, 42, 0) 0%, rgba(15, 23, 42, 0.85) 30%, rgba(15, 23, 42, 0.98) 100%);
335
  }
336
 
337
  .public-card-prompt {
338
  margin: 0;
339
  font-size: 0.9rem;
340
+ color: #f1f5f9;
341
  line-height: 1.4;
342
  display: -webkit-box;
343
+ -webkit-line-clamp: 2;
344
  -webkit-box-orient: vertical;
345
  overflow: hidden;
346
+ font-weight: 500;
347
  }
348
 
349
  .public-card-footer {
350
  display: flex;
351
  justify-content: space-between;
352
  align-items: center;
353
+ font-size: 0.75rem;
354
+ color: rgba(148, 163, 184, 0.9);
355
  gap: 10px;
356
  }
357
 
 
359
  grid-column: 1 / -1;
360
  text-align: center;
361
  color: var(--text-sub);
362
+ padding: 60px 30px;
363
+ background: linear-gradient(135deg, rgba(15, 23, 42, 0.5) 0%, rgba(20, 30, 50, 0.5) 100%);
364
+ border-radius: 20px;
365
+ border: 2px dashed var(--panel-border);
366
+ font-size: 1.1rem;
367
+ }
368
+
369
+ .public-gallery-empty > div:first-child {
370
+ font-size: 48px;
371
+ margin-bottom: 16px;
372
  }
373
 
374
  .icon-btn.small {
375
+ width: 36px;
376
+ height: 36px;
377
  font-size: 14px;
378
+ background: rgba(239, 68, 68, 0.9);
379
+ border-color: rgba(248, 113, 113, 0.7);
380
+ transition: all var(--transition-fast);
381
+ }
382
+
383
+ .icon-btn.small:hover {
384
+ background: rgba(239, 68, 68, 0.95);
385
+ transform: scale(1.15);
386
+ box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
387
  }
388
 
389
  .icon-btn.share-btn {
 
396
  }
397
 
398
  .icon-btn.public-delete-btn {
399
+ background: rgba(239, 68, 68, 0.9);
400
+ border-color: rgba(248, 113, 113, 0.7);
401
  }
402
 
403
  .icon-btn.spinning {
 
1019
 
1020
  /* --- Mobile Responsive Design --- */
1021
  @media (max-width: 768px) {
1022
+ .public-gallery-section {
1023
+ padding: 20px;
1024
+ margin-top: 30px;
1025
+ border-radius: 18px;
 
 
 
 
 
 
 
 
1026
  }
1027
+
1028
+ .section-title h3 {
1029
  font-size: 1.1rem;
1030
  }
1031
+
1032
+ .section-subtitle {
1033
+ font-size: 0.85rem;
 
 
 
 
 
 
 
 
 
 
1034
  }
1035
+
1036
  .public-grid {
1037
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
1038
+ gap: 14px;
1039
+ margin-top: 16px;
 
 
 
1040
  }
1041
+
1042
+ .public-card {
1043
+ border-radius: 14px;
 
 
 
 
 
 
 
1044
  }
1045
+
1046
+ .public-card-prompt {
1047
+ font-size: 0.8rem;
1048
+ -webkit-line-clamp: 2;
1049
  }
1050
+
1051
+ .public-card-footer {
1052
+ font-size: 0.7rem;
 
 
1053
  }
1054
+
1055
+ .icon-btn.small {
1056
+ width: 32px;
1057
+ height: 32px;
1058
+ font-size: 12px;
1059
  }
1060
+
1061
+ .public-gallery-empty {
1062
+ padding: 40px 20px;
 
1063
  border-radius: 16px;
1064
+ font-size: 0.95rem;
1065
  }
1066
+
1067
+ .public-gallery-empty > div:first-child {
1068
+ font-size: 36px;
1069
+ margin-bottom: 12px;
1070
  }
1071
+ }
1072
+
1073
+ @media (max-width: 600px) {
1074
+ .public-gallery-section {
1075
+ padding: 16px;
1076
+ margin-top: 20px;
 
1077
  }
1078
+
1079
+ .section-title {
1080
+ flex-direction: column;
 
 
1081
  }
1082
+
1083
+ .section-title h3 {
1084
+ font-size: 1rem;
1085
  }
1086
+
1087
+ .public-grid {
1088
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
1089
+ gap: 12px;
 
1090
  }
1091
+
1092
+ .public-card-info {
1093
+ padding: 12px;
 
 
 
1094
  }
1095
+
1096
+ .public-card:hover .public-card-info {
1097
+ padding-bottom: 12px;
 
1098
  }
1099
  }
1100
 
 
1104
  grid-template-columns: repeat(2, 1fr);
1105
  gap: 16px;
1106
  }
1107
+
1108
+ .public-grid {
1109
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
1110
+ gap: 18px;
1111
+ }
1112
+
1113
+ .public-gallery-section {
1114
+ padding: 28px;
1115
+ margin-top: 35px;
1116
+ }
1117
 
1118
  .modal-content {
1119
  width: 90%;
 
1135
  grid-template-columns: repeat(3, 1fr);
1136
  gap: 20px;
1137
  }
1138
+
1139
+ .public-grid {
1140
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
1141
+ gap: 28px;
1142
+ }
1143
+
1144
+ .public-gallery-section {
1145
+ padding: 40px;
1146
+ margin-top: 50px;
1147
+ }
1148
+
1149
+ .section-title h3 {
1150
+ font-size: 1.5rem;
1151
+ }
1152
 
1153
  .history-container {
1154
  padding: 15px 30px;