mistpe commited on
Commit
0e8f914
·
verified ·
1 Parent(s): 6044e02

Update templates/dashboard.html

Browse files
Files changed (1) hide show
  1. templates/dashboard.html +308 -427
templates/dashboard.html CHANGED
@@ -7,39 +7,17 @@
7
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
8
  <style>
9
  :root {
10
- /* 深色主题变量 */
11
- --primary-color: #00fff2;
12
- --primary-glow: rgba(0, 255, 242, 0.15);
13
- --secondary-color: #ff2aff;
14
- --background-color: #030714;
15
- --card-background: #0a101f;
16
- --surface-color: #111827;
17
  --text-primary: #ffffff;
18
- --text-secondary: #94a3b8;
19
- --border-color: #1e293b;
20
- --success-color: #04d94f;
21
- --warning-color: #ff9500;
22
  --danger-color: #ff2d55;
23
- --sleeping-color: #3b82f6;
24
- --header-blur: saturate(180%) blur(10px);
25
- --card-blur: saturate(180%) blur(5px);
26
- }
27
-
28
- /* 日间模式 */
29
- [data-theme="light"] {
30
- --primary-color: #0066ff;
31
- --primary-glow: rgba(0, 102, 255, 0.15);
32
- --secondary-color: #7c3aed;
33
- --background-color: #f1f5f9;
34
- --card-background: #ffffff;
35
- --surface-color: #f8fafc;
36
- --text-primary: #0f172a;
37
- --text-secondary: #475569;
38
- --border-color: #e2e8f0;
39
- --success-color: #10b981;
40
- --warning-color: #f59e0b;
41
- --danger-color: #ef4444;
42
- --sleeping-color: #3b82f6;
43
  }
44
 
45
  * {
@@ -52,17 +30,15 @@
52
  body {
53
  background-color: var(--background-color);
54
  background-image:
55
- radial-gradient(circle at 0% 0%, var(--primary-glow) 0%, transparent 25%),
56
- radial-gradient(circle at 100% 100%, rgba(255, 42, 255, 0.1) 0%, transparent 25%);
57
  min-height: 100vh;
58
  color: var(--text-primary);
59
- transition: background-color 0.3s ease;
60
  }
61
 
62
  .header {
63
- background: rgba(10, 16, 31, 0.7);
64
- backdrop-filter: var(--header-blur);
65
- -webkit-backdrop-filter: var(--header-blur);
66
  position: fixed;
67
  top: 0;
68
  left: 0;
@@ -71,10 +47,6 @@
71
  border-bottom: 1px solid var(--border-color);
72
  }
73
 
74
- [data-theme="light"] .header {
75
- background: rgba(255, 255, 255, 0.7);
76
- }
77
-
78
  .header-content {
79
  max-width: 1400px;
80
  margin: 0 auto;
@@ -84,6 +56,12 @@
84
  align-items: center;
85
  }
86
 
 
 
 
 
 
 
87
  .logo {
88
  font-size: 1.5rem;
89
  font-weight: 600;
@@ -91,19 +69,6 @@
91
  display: flex;
92
  align-items: center;
93
  gap: 0.5rem;
94
- position: relative;
95
- }
96
-
97
- .logo::before {
98
- content: '';
99
- position: absolute;
100
- top: 50%;
101
- left: -10px;
102
- transform: translateY(-50%);
103
- width: 3px;
104
- height: 70%;
105
- background: var(--primary-color);
106
- box-shadow: 0 0 10px var(--primary-color);
107
  }
108
 
109
  .logo i {
@@ -111,12 +76,6 @@
111
  text-shadow: 0 0 10px var(--primary-color);
112
  }
113
 
114
- .nav-actions {
115
- display: flex;
116
- align-items: center;
117
- gap: 1.5rem;
118
- }
119
-
120
  .search-bar {
121
  position: relative;
122
  width: 300px;
@@ -124,61 +83,41 @@
124
 
125
  .search-bar input {
126
  width: 100%;
127
- padding: 0.75rem 1rem 0.75rem 2.5rem;
128
- background: var(--surface-color);
129
  border: 1px solid var(--border-color);
130
- border-radius: 8px;
131
  color: var(--text-primary);
132
  font-size: 0.9rem;
133
- transition: all 0.3s ease;
134
- }
135
-
136
- .search-bar input:focus {
137
- border-color: var(--primary-color);
138
- box-shadow: 0 0 0 3px var(--primary-glow);
139
- outline: none;
140
  }
141
 
142
  .search-bar i {
143
  position: absolute;
144
- left: 1rem;
145
  top: 50%;
146
  transform: translateY(-50%);
147
  color: var(--text-secondary);
148
  }
149
 
150
- .action-btn {
151
- padding: 0.5rem;
152
- border-radius: 8px;
153
- background: transparent;
154
- border: 1px solid var(--border-color);
155
- color: var(--text-primary);
156
- cursor: pointer;
157
- transition: all 0.3s ease;
158
  display: flex;
159
  align-items: center;
160
- gap: 0.5rem;
161
- }
162
-
163
- .action-btn:hover {
164
- border-color: var(--primary-color);
165
- background: var(--primary-glow);
166
  }
167
 
168
  .theme-toggle {
169
- font-size: 1.2rem;
170
- padding: 0.5rem;
171
- border-radius: 8px;
172
- background: transparent;
173
- border: 1px solid var(--border-color);
174
- color: var(--text-primary);
175
  cursor: pointer;
 
 
176
  transition: all 0.3s ease;
177
  }
178
 
179
  .theme-toggle:hover {
180
- border-color: var(--primary-color);
181
- background: var(--primary-glow);
182
  }
183
 
184
  .container {
@@ -189,117 +128,37 @@
189
 
190
  .dashboard-header {
191
  margin-bottom: 2rem;
 
192
  background: var(--card-background);
193
- border-radius: 16px;
194
  border: 1px solid var(--border-color);
195
- overflow: hidden;
196
- position: relative;
197
- }
198
-
199
- .dashboard-header::before {
200
- content: '';
201
- position: absolute;
202
- top: 0;
203
- left: 0;
204
- right: 0;
205
- height: 2px;
206
- background: linear-gradient(90deg,
207
- transparent,
208
- var(--primary-color),
209
- var(--secondary-color),
210
- transparent
211
- );
212
- }
213
-
214
- .bulk-actions {
215
- padding: 1.5rem;
216
- border-bottom: 1px solid var(--border-color);
217
- display: flex;
218
- justify-content: space-between;
219
- align-items: center;
220
- background: var(--surface-color);
221
- }
222
-
223
- .selection-info {
224
- display: flex;
225
- align-items: center;
226
- gap: 1rem;
227
- }
228
-
229
- .bulk-buttons {
230
- display: flex;
231
- gap: 1rem;
232
- }
233
-
234
- .bulk-btn {
235
- padding: 0.5rem 1rem;
236
- border-radius: 8px;
237
- background: var(--primary-glow);
238
- border: 1px solid var(--primary-color);
239
- color: var(--primary-color);
240
- cursor: pointer;
241
- transition: all 0.3s ease;
242
- display: flex;
243
- align-items: center;
244
- gap: 0.5rem;
245
- }
246
-
247
- .bulk-btn:hover {
248
- background: var(--primary-color);
249
- color: var(--background-color);
250
- }
251
-
252
- .bulk-btn.danger {
253
- background: rgba(255, 45, 85, 0.1);
254
- border-color: var(--danger-color);
255
- color: var(--danger-color);
256
- }
257
-
258
- .bulk-btn.danger:hover {
259
- background: var(--danger-color);
260
- color: white;
261
  }
262
 
263
  .stats-grid {
264
  display: grid;
265
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
266
  gap: 1rem;
267
- padding: 1.5rem;
268
  }
269
 
270
  .stat-card {
271
- background: var(--surface-color);
272
  padding: 1.5rem;
273
- border-radius: 12px;
274
  border: 1px solid var(--border-color);
275
  transition: all 0.3s ease;
276
- position: relative;
277
- overflow: hidden;
278
  }
279
 
280
- .stat-card::after {
281
- content: '';
282
- position: absolute;
283
- top: 0;
284
- right: 0;
285
- width: 3px;
286
- height: 100%;
287
- background: var(--primary-color);
288
- opacity: 0.5;
289
- transition: all 0.3s ease;
290
- }
291
-
292
- .stat-card:hover::after {
293
- opacity: 1;
294
- box-shadow: 0 0 10px var(--primary-color);
295
  }
296
 
297
  .stat-value {
298
  font-size: 2rem;
299
  font-weight: 600;
300
- color: var(--primary-color);
301
  margin-bottom: 0.5rem;
302
- text-shadow: 0 0 10px var(--primary-glow);
303
  }
304
 
305
  .stat-label {
@@ -309,60 +168,37 @@
309
  letter-spacing: 0.5px;
310
  }
311
 
312
- .space-grid {
313
- display: grid;
314
- grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
315
- gap: 1.5rem;
316
- margin-top: 2rem;
317
- }
318
-
319
- .space-card {
320
  background: var(--card-background);
321
- border-radius: 16px;
 
322
  border: 1px solid var(--border-color);
323
  overflow: hidden;
324
- transition: all 0.3s ease;
325
- position: relative;
326
  }
327
 
328
- .space-card:hover {
329
- transform: translateY(-5px);
330
- box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
331
- }
332
-
333
- .space-card.selected {
334
- border-color: var(--primary-color);
335
- box-shadow: 0 0 0 2px var(--primary-glow);
336
- }
337
-
338
- .card-checkbox {
339
- position: absolute;
340
- top: 1rem;
341
- right: 1rem;
342
- width: 20px;
343
- height: 20px;
344
- cursor: pointer;
345
- z-index: 1;
346
- }
347
-
348
- .space-header {
349
  padding: 1.5rem;
350
- background: var(--surface-color);
351
  border-bottom: 1px solid var(--border-color);
352
  display: flex;
353
  justify-content: space-between;
354
  align-items: center;
 
355
  }
356
 
357
- .space-name {
358
- font-size: 1.1rem;
359
  font-weight: 600;
360
- color: var(--text-primary);
361
  display: flex;
362
  align-items: center;
363
  gap: 0.5rem;
364
  }
365
 
 
 
 
 
 
 
366
  .status-badge {
367
  padding: 0.25rem 0.75rem;
368
  border-radius: 6px;
@@ -373,15 +209,45 @@
373
  gap: 0.5rem;
374
  }
375
 
376
- .space-content {
 
 
 
377
  padding: 1.5rem;
378
  }
379
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  .space-info {
381
  display: grid;
382
  grid-template-columns: repeat(2, 1fr);
383
  gap: 1rem;
384
- margin-bottom: 1.5rem;
385
  }
386
 
387
  .info-item {
@@ -400,7 +266,30 @@
400
  .info-value {
401
  color: var(--text-primary);
402
  font-size: 0.9rem;
403
- font-weight: 500;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
  }
405
 
406
  .action-buttons {
@@ -408,13 +297,12 @@
408
  grid-template-columns: repeat(3, 1fr);
409
  gap: 0.75rem;
410
  padding: 1rem;
411
- background: var(--surface-color);
412
- border-top: 1px solid var(--border-color);
413
  }
414
 
415
  .action-button {
416
  padding: 0.75rem;
417
- border-radius: 8px;
418
  font-size: 0.9rem;
419
  font-weight: 500;
420
  display: flex;
@@ -423,15 +311,15 @@
423
  gap: 0.5rem;
424
  cursor: pointer;
425
  transition: all 0.3s ease;
426
- background: transparent;
427
  border: 1px solid var(--border-color);
 
428
  color: var(--text-primary);
429
  text-decoration: none;
430
  }
431
 
432
  .action-button:hover {
433
  border-color: var(--primary-color);
434
- background: var(--primary-glow);
435
  }
436
 
437
  .action-button.restart {
@@ -441,71 +329,47 @@
441
 
442
  .action-button.restart:hover {
443
  background: var(--primary-color);
444
- color: var(--card-background);
445
  }
446
 
447
- @media (max-width: 768px) {
448
- .header-content {
449
- flex-direction: column;
450
- gap: 1rem;
451
- }
452
-
453
- .nav-actions {
454
- width: 100%;
455
- flex-direction: column;
456
- gap: 1rem;
457
- }
458
-
459
- .search-bar {
460
- width: 100%;
461
- }
462
-
463
- .bulk-actions {
464
- flex-direction: column;
465
- gap: 1rem;
466
- }
467
-
468
- .stats-grid {
469
- grid-template-columns: 1fr;
470
- }
471
-
472
- .space-grid {
473
- grid-template-columns: 1fr;
474
- }
475
  }
476
 
477
- /* 自定义滚动条 */
478
- ::-webkit-scrollbar {
479
- width: 8px;
480
- height: 8px
481
  }
482
 
483
- /* 自定义滚动条 */
484
- ::-webkit-scrollbar {
485
- width: 8px;
486
- height: 8px;
487
- background: var(--surface-color);
488
  }
489
 
490
- ::-webkit-scrollbar-thumb {
491
- background: var(--border-color);
492
- border-radius: 4px;
 
493
  }
494
 
495
- ::-webkit-scrollbar-thumb:hover {
496
- background: var(--primary-color);
 
 
497
  }
498
 
499
- /* 加载动画 */
500
  .loading-overlay {
501
  position: fixed;
502
  top: 0;
503
  left: 0;
504
  right: 0;
505
  bottom: 0;
506
- background: rgba(3, 7, 20, 0.8);
507
  backdrop-filter: blur(5px);
508
- -webkit-backdrop-filter: blur(5px);
509
  display: flex;
510
  justify-content: center;
511
  align-items: center;
@@ -514,11 +378,10 @@
514
 
515
  .loading-spinner {
516
  position: relative;
517
- width: 100px;
518
- height: 100px;
519
  }
520
 
521
- .loading-spinner::before,
522
  .loading-spinner::after {
523
  content: '';
524
  position: absolute;
@@ -527,19 +390,52 @@
527
  width: 100%;
528
  height: 100%;
529
  border-radius: 50%;
530
- border: 2px solid transparent;
531
  border-top-color: var(--primary-color);
532
- animation: spin 1.5s linear infinite;
533
  }
534
 
535
- .loading-spinner::after {
536
- border-top-color: var(--secondary-color);
537
- animation-delay: 0.75s;
538
  }
539
 
540
- @keyframes spin {
541
- 0% { transform: rotate(0deg); }
542
- 100% { transform: rotate(360deg); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
543
  }
544
  </style>
545
  </head>
@@ -550,19 +446,21 @@
550
 
551
  <header class="header">
552
  <div class="header-content">
553
- <div class="logo">
554
- <i class="fas fa-server"></i>
555
- HF Space Manager
556
- </div>
557
- <div class="nav-actions">
558
  <div class="search-bar">
559
  <i class="fas fa-search"></i>
560
  <input type="text" placeholder="搜索 Spaces..." id="spaceSearch">
561
  </div>
562
- <button class="theme-toggle" id="themeToggle">
 
 
563
  <i class="fas fa-moon"></i>
564
  </button>
565
- <a href="/logout" class="action-btn">
566
  <i class="fas fa-sign-out-alt"></i>
567
  退出
568
  </a>
@@ -581,23 +479,6 @@
581
  {% endfor %}
582
 
583
  <div class="dashboard-header">
584
- <div class="bulk-actions" id="bulkActions" style="display: none;">
585
- <div class="selection-info">
586
- <i class="fas fa-check-square"></i>
587
- 已选择 <span id="selectedCount">0</span> 个 Spaces
588
- </div>
589
- <div class="bulk-buttons">
590
- <button class="bulk-btn" onclick="bulkRestart()">
591
- <i class="fas fa-sync-alt"></i>
592
- 批量重启
593
- </button>
594
- <button class="bulk-btn danger" onclick="bulkStop()">
595
- <i class="fas fa-stop-circle"></i>
596
- 批量停止
597
- </button>
598
- </div>
599
- </div>
600
-
601
  <div class="stats-grid">
602
  {% set total_spaces = spaces|length %}
603
  {% set running_spaces = spaces|selectattr('status', 'equalto', 'RUNNING')|list|length %}
@@ -628,69 +509,106 @@
628
  </div>
629
  </div>
630
 
631
- <div class="space-grid">
632
- {% for space in spaces %}
633
- <div class="space-card" data-space-id="{{ space.repo_id }}">
634
- <input type="checkbox" class="card-checkbox" onchange="updateSelection(this)">
635
- <div class="space-header">
636
- <div class="space-name">
637
- <i class="fas fa-cube"></i>
638
- {{ space.name }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
  </div>
640
- <span class="status-badge status-{{ space.status }}">
641
- <i class="fas fa-circle"></i>
642
- {{ space.status }}
643
- </span>
644
  </div>
645
 
646
- <div class="space-content">
647
- <div class="space-info">
648
- <div class="info-item">
649
- <span class="info-label">Space ID</span>
650
- <span class="info-value">{{ space.repo_id }}</span>
651
- </div>
652
- <div class="info-item">
653
- <span class="info-label">创建时间</span>
654
- <span class="info-value">{{ space.created_at }}</span>
 
 
 
655
  </div>
656
- <div class="info-item">
657
- <span class="info-label">最后修改</span>
658
- <span class="info-value">{{ space.last_modified }}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
659
  </div>
660
- <div class="info-item">
661
- <span class="info-label">SDK 版本</span>
662
- <span class="info-value">{{ space.sdk }}</span>
663
- </div>
664
- <div class="info-item">
665
- <span class="info-label">应用端口</span>
666
- <span class="info-value">{{ space.app_port }}</span>
667
- </div>
668
- <div class="info-item">
669
- <span class="info-label">访问权限</span>
670
- <span class="info-value">{{ '私有' if space.private else '公开' }}</span>
 
 
 
671
  </div>
672
  </div>
673
- </div>
674
-
675
- <div class="action-buttons">
676
- <a href="{{ space.url }}" target="_blank" class="action-button">
677
- <i class="fas fa-external-link-alt"></i>
678
- 查看
679
- </a>
680
- <button onclick="confirmAction('restart', '{{ space.repo_id }}')" class="action-button restart">
681
- <i class="fas fa-sync-alt"></i>
682
- 重启
683
- </button>
684
- <button onclick="confirmAction('rebuild', '{{ space.repo_id }}')" class="action-button">
685
- <i class="fas fa-tools"></i>
686
- 重建
687
- </button>
688
  </div>
689
  </div>
690
- {% endfor %}
691
- </div>
692
  {% else %}
693
- <div class="dashboard-header">
694
  <p style="text-align: center; padding: 2rem; color: var(--text-secondary);">
695
  <i class="fas fa-info-circle"></i>
696
  没有找到任何 Spaces。请确保你的账户中有创建的 Spaces,并且提供的 token 有正确的权限。
@@ -703,80 +621,35 @@
703
  <script>
704
  const socket = io();
705
 
706
- // 主题切换
707
- const themeToggle = document.getElementById('themeToggle');
708
- const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
709
-
710
- // 初始化主题
711
- if (localStorage.getItem('theme')) {
712
- document.body.dataset.theme = localStorage.getItem('theme');
713
- updateThemeIcon();
714
- } else {
715
- document.body.dataset.theme = prefersDarkScheme.matches ? 'dark' : 'light';
716
- updateThemeIcon();
717
- }
718
-
719
- themeToggle.addEventListener('click', () => {
720
- const currentTheme = document.body.dataset.theme;
721
- const newTheme = currentTheme === 'light' ? 'dark' : 'light';
722
- document.body.dataset.theme = newTheme;
723
- localStorage.setItem('theme', newTheme);
724
- updateThemeIcon();
725
  });
726
 
727
- function updateThemeIcon() {
728
- const icon = themeToggle.querySelector('i');
729
- icon.className = document.body.dataset.theme === 'light' ? 'fas fa-moon' : 'fas fa-sun';
730
- }
731
-
732
- // 批量操作相关
733
- let selectedSpaces = new Set();
734
-
735
- function updateSelection(checkbox) {
736
- const spaceCard = checkbox.closest('.space-card');
737
- const spaceId = spaceCard.dataset.spaceId;
738
-
739
- if (checkbox.checked) {
740
- selectedSpaces.add(spaceId);
741
- spaceCard.classList.add('selected');
742
- } else {
743
- selectedSpaces.delete(spaceId);
744
- spaceCard.classList.remove('selected');
745
- }
746
-
747
- const selectedCount = selectedSpaces.size;
748
- document.getElementById('selectedCount').textContent = selectedCount;
749
- document.getElementById('bulkActions').style.display = selectedCount > 0 ? 'flex' : 'none';
750
- }
751
-
752
- function bulkRestart() {
753
- if (confirm(`确定要重启选中的 ${selectedSpaces.size} 个 Spaces 吗?`)) {
754
- document.getElementById('loading').style.display = 'flex';
755
- // 这里添加批量重启的 API 调用
756
- console.log('Restarting spaces:', Array.from(selectedSpaces));
757
- }
758
- }
759
-
760
- function bulkStop() {
761
- if (confirm(`确定要停止选中的 ${selectedSpaces.size} 个 Spaces 吗?`)) {
762
- document.getElementById('loading').style.display = 'flex';
763
- // 这里添加批量停止的 API 调用
764
- console.log('Stopping spaces:', Array.from(selectedSpaces));
765
- }
766
- }
767
-
768
  // 搜索功能
769
  document.getElementById('spaceSearch').addEventListener('input', function(e) {
770
  const searchTerm = e.target.value.toLowerCase();
771
  document.querySelectorAll('.space-card').forEach(card => {
772
  const spaceName = card.querySelector('.space-name').textContent.toLowerCase();
773
  const spaceId = card.dataset.spaceId.toLowerCase();
774
- const visible = spaceName.includes(searchTerm) || spaceId.includes(searchTerm);
775
- card.style.display = visible ? '' : 'none';
 
 
 
776
  });
777
  });
778
 
779
- // WebSocket 和状态更新
 
 
 
 
 
 
 
 
 
780
  socket.on('connect', () => {
781
  console.log('Connected to server');
782
  });
@@ -796,10 +669,6 @@
796
  socket.connect();
797
  }
798
  });
799
-
800
- window.addEventListener('load', function() {
801
- document.getElementById('loading').style.display = 'none';
802
- });
803
 
804
  function updateSpaceStatuses() {
805
  document.querySelectorAll('.space-card').forEach(card => {
@@ -825,7 +694,19 @@
825
  }
826
  }
827
 
 
828
  setInterval(updateSpaceStatuses, 30000);
 
 
 
 
 
 
 
 
 
 
 
829
  </script>
830
  </body>
831
  </html>
 
7
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
8
  <style>
9
  :root {
10
+ --primary-color: #00ff9d;
11
+ --primary-dark: #00cc7d;
12
+ --background-color: #0a0b0f;
13
+ --card-background: #12141c;
 
 
 
14
  --text-primary: #ffffff;
15
+ --text-secondary: #7f8ea3;
16
+ --border-color: #1e2029;
17
+ --success-color: #00ff9d;
18
+ --warning-color: #ff9d00;
19
  --danger-color: #ff2d55;
20
+ --sleeping-color: #00ffff;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  }
22
 
23
  * {
 
30
  body {
31
  background-color: var(--background-color);
32
  background-image:
33
+ radial-gradient(circle at 10% 20%, rgba(0, 255, 157, 0.03) 0%, transparent 20%),
34
+ radial-gradient(circle at 90% 80%, rgba(255, 0, 255, 0.03) 0%, transparent 20%);
35
  min-height: 100vh;
36
  color: var(--text-primary);
 
37
  }
38
 
39
  .header {
40
+ background: rgba(18, 20, 28, 0.95);
41
+ backdrop-filter: blur(10px);
 
42
  position: fixed;
43
  top: 0;
44
  left: 0;
 
47
  border-bottom: 1px solid var(--border-color);
48
  }
49
 
 
 
 
 
50
  .header-content {
51
  max-width: 1400px;
52
  margin: 0 auto;
 
56
  align-items: center;
57
  }
58
 
59
+ .nav-section {
60
+ display: flex;
61
+ align-items: center;
62
+ gap: 2rem;
63
+ }
64
+
65
  .logo {
66
  font-size: 1.5rem;
67
  font-weight: 600;
 
69
  display: flex;
70
  align-items: center;
71
  gap: 0.5rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  }
73
 
74
  .logo i {
 
76
  text-shadow: 0 0 10px var(--primary-color);
77
  }
78
 
 
 
 
 
 
 
79
  .search-bar {
80
  position: relative;
81
  width: 300px;
 
83
 
84
  .search-bar input {
85
  width: 100%;
86
+ padding: 0.5rem 1rem 0.5rem 2.5rem;
87
+ background: rgba(255, 255, 255, 0.05);
88
  border: 1px solid var(--border-color);
89
+ border-radius: 6px;
90
  color: var(--text-primary);
91
  font-size: 0.9rem;
 
 
 
 
 
 
 
92
  }
93
 
94
  .search-bar i {
95
  position: absolute;
96
+ left: 0.8rem;
97
  top: 50%;
98
  transform: translateY(-50%);
99
  color: var(--text-secondary);
100
  }
101
 
102
+ .user-section {
 
 
 
 
 
 
 
103
  display: flex;
104
  align-items: center;
105
+ gap: 1rem;
 
 
 
 
 
106
  }
107
 
108
  .theme-toggle {
109
+ background: none;
110
+ border: none;
111
+ color: var(--text-secondary);
 
 
 
112
  cursor: pointer;
113
+ padding: 0.5rem;
114
+ border-radius: 4px;
115
  transition: all 0.3s ease;
116
  }
117
 
118
  .theme-toggle:hover {
119
+ color: var(--primary-color);
120
+ background: rgba(0, 255, 157, 0.1);
121
  }
122
 
123
  .container {
 
128
 
129
  .dashboard-header {
130
  margin-bottom: 2rem;
131
+ padding: 1.5rem;
132
  background: var(--card-background);
133
+ border-radius: 12px;
134
  border: 1px solid var(--border-color);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  }
136
 
137
  .stats-grid {
138
  display: grid;
139
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
140
  gap: 1rem;
141
+ margin-top: 1rem;
142
  }
143
 
144
  .stat-card {
145
+ background: rgba(255, 255, 255, 0.03);
146
  padding: 1.5rem;
147
+ border-radius: 8px;
148
  border: 1px solid var(--border-color);
149
  transition: all 0.3s ease;
 
 
150
  }
151
 
152
+ .stat-card:hover {
153
+ transform: translateY(-2px);
154
+ border-color: var(--primary-color);
 
 
 
 
 
 
 
 
 
 
 
 
155
  }
156
 
157
  .stat-value {
158
  font-size: 2rem;
159
  font-weight: 600;
 
160
  margin-bottom: 0.5rem;
161
+ color: var(--primary-color);
162
  }
163
 
164
  .stat-label {
 
168
  letter-spacing: 0.5px;
169
  }
170
 
171
+ .owner-section {
 
 
 
 
 
 
 
172
  background: var(--card-background);
173
+ border-radius: 12px;
174
+ margin-bottom: 2rem;
175
  border: 1px solid var(--border-color);
176
  overflow: hidden;
 
 
177
  }
178
 
179
+ .owner-header {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  padding: 1.5rem;
 
181
  border-bottom: 1px solid var(--border-color);
182
  display: flex;
183
  justify-content: space-between;
184
  align-items: center;
185
+ background: rgba(255, 255, 255, 0.02);
186
  }
187
 
188
+ .owner-name {
189
+ font-size: 1.25rem;
190
  font-weight: 600;
 
191
  display: flex;
192
  align-items: center;
193
  gap: 0.5rem;
194
  }
195
 
196
+ .status-stats {
197
+ display: flex;
198
+ gap: 1rem;
199
+ flex-wrap: wrap;
200
+ }
201
+
202
  .status-badge {
203
  padding: 0.25rem 0.75rem;
204
  border-radius: 6px;
 
209
  gap: 0.5rem;
210
  }
211
 
212
+ .space-grid {
213
+ display: grid;
214
+ grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
215
+ gap: 1.5rem;
216
  padding: 1.5rem;
217
  }
218
 
219
+ .space-card {
220
+ background: rgba(255, 255, 255, 0.02);
221
+ border-radius: 10px;
222
+ border: 1px solid var(--border-color);
223
+ overflow: hidden;
224
+ transition: all 0.3s ease;
225
+ }
226
+
227
+ .space-card:hover {
228
+ transform: translateY(-2px);
229
+ border-color: var(--primary-color);
230
+ box-shadow: 0 0 20px rgba(0, 255, 157, 0.1);
231
+ }
232
+
233
+ .space-header {
234
+ padding: 1rem;
235
+ background: rgba(255, 255, 255, 0.02);
236
+ border-bottom: 1px solid var(--border-color);
237
+ display: flex;
238
+ justify-content: space-between;
239
+ align-items: center;
240
+ }
241
+
242
+ .space-content {
243
+ padding: 1rem;
244
+ }
245
+
246
  .space-info {
247
  display: grid;
248
  grid-template-columns: repeat(2, 1fr);
249
  gap: 1rem;
250
+ margin-bottom: 1rem;
251
  }
252
 
253
  .info-item {
 
266
  .info-value {
267
  color: var(--text-primary);
268
  font-size: 0.9rem;
269
+ }
270
+
271
+ .space-metrics {
272
+ padding: 1rem;
273
+ background: rgba(255, 255, 255, 0.02);
274
+ border-radius: 8px;
275
+ margin-bottom: 1rem;
276
+ display: flex;
277
+ justify-content: space-around;
278
+ }
279
+
280
+ .metric-item {
281
+ text-align: center;
282
+ }
283
+
284
+ .metric-value {
285
+ font-size: 1.25rem;
286
+ font-weight: 600;
287
+ color: var(--primary-color);
288
+ }
289
+
290
+ .metric-label {
291
+ font-size: 0.8rem;
292
+ color: var(--text-secondary);
293
  }
294
 
295
  .action-buttons {
 
297
  grid-template-columns: repeat(3, 1fr);
298
  gap: 0.75rem;
299
  padding: 1rem;
300
+ background: rgba(0, 0, 0, 0.2);
 
301
  }
302
 
303
  .action-button {
304
  padding: 0.75rem;
305
+ border-radius: 6px;
306
  font-size: 0.9rem;
307
  font-weight: 500;
308
  display: flex;
 
311
  gap: 0.5rem;
312
  cursor: pointer;
313
  transition: all 0.3s ease;
 
314
  border: 1px solid var(--border-color);
315
+ background: transparent;
316
  color: var(--text-primary);
317
  text-decoration: none;
318
  }
319
 
320
  .action-button:hover {
321
  border-color: var(--primary-color);
322
+ background: rgba(0, 255, 157, 0.1);
323
  }
324
 
325
  .action-button.restart {
 
329
 
330
  .action-button.restart:hover {
331
  background: var(--primary-color);
332
+ color: var(--background-color);
333
  }
334
 
335
+ .status-RUNNING {
336
+ background: rgba(0, 255, 157, 0.1);
337
+ border: 1px solid var(--success-color);
338
+ color: var(--success-color);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  }
340
 
341
+ .status-BUILDING {
342
+ background: rgba(255, 157, 0, 0.1);
343
+ border: 1px solid var(--warning-color);
344
+ color: var(--warning-color);
345
  }
346
 
347
+ .status-SLEEPING {
348
+ background: rgba(0, 255, 255, 0.1);
349
+ border: 1px solid var(--sleeping-color);
350
+ color: var(--sleeping-color);
 
351
  }
352
 
353
+ .status-STOPPED {
354
+ background: rgba(127, 142, 163, 0.1);
355
+ border: 1px solid var(--text-secondary);
356
+ color: var(--text-secondary);
357
  }
358
 
359
+ .status-FAILED {
360
+ background: rgba(255, 45, 85, 0.1);
361
+ border: 1px solid var(--danger-color);
362
+ color: var(--danger-color);
363
  }
364
 
 
365
  .loading-overlay {
366
  position: fixed;
367
  top: 0;
368
  left: 0;
369
  right: 0;
370
  bottom: 0;
371
+ background: rgba(10, 11, 15, 0.8);
372
  backdrop-filter: blur(5px);
 
373
  display: flex;
374
  justify-content: center;
375
  align-items: center;
 
378
 
379
  .loading-spinner {
380
  position: relative;
381
+ width: 60px;
382
+ height: 60px;
383
  }
384
 
 
385
  .loading-spinner::after {
386
  content: '';
387
  position: absolute;
 
390
  width: 100%;
391
  height: 100%;
392
  border-radius: 50%;
393
+ border: 3px solid var(--border-color);
394
  border-top-color: var(--primary-color);
395
+ animation: spin 1s infinite linear;
396
  }
397
 
398
+ @keyframes spin {
399
+ to { transform: rotate(360deg); }
 
400
  }
401
 
402
+ @media (max-width: 768px) {
403
+ .header-content {
404
+ flex-direction: column;
405
+ gap: 1rem;
406
+ padding: 1rem;
407
+ }
408
+
409
+ .nav-section {
410
+ width: 100%;
411
+ flex-direction: column;
412
+ gap: 1rem;
413
+ }
414
+
415
+ .search-bar {
416
+ width: 100%;
417
+ }
418
+
419
+ .container {
420
+ padding: 1rem;
421
+ }
422
+
423
+ .stats-grid {
424
+ grid-template-columns: 1fr;
425
+ }
426
+
427
+ .space-grid {
428
+ grid-template-columns: 1fr;
429
+ }
430
+
431
+ .space-info {
432
+ grid-template-columns: 1fr;
433
+ }
434
+
435
+ .status-stats {
436
+ flex-direction: column;
437
+ align-items: flex-start;
438
+ }
439
  }
440
  </style>
441
  </head>
 
446
 
447
  <header class="header">
448
  <div class="header-content">
449
+ <div class="nav-section">
450
+ <div class="logo">
451
+ <i class="fas fa-server"></i>
452
+ HF Space Manager
453
+ </div>
454
  <div class="search-bar">
455
  <i class="fas fa-search"></i>
456
  <input type="text" placeholder="搜索 Spaces..." id="spaceSearch">
457
  </div>
458
+ </div>
459
+ <div class="user-section">
460
+ <button class="theme-toggle" title="切换主题">
461
  <i class="fas fa-moon"></i>
462
  </button>
463
+ <a href="/logout" class="action-button">
464
  <i class="fas fa-sign-out-alt"></i>
465
  退出
466
  </a>
 
479
  {% endfor %}
480
 
481
  <div class="dashboard-header">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
  <div class="stats-grid">
483
  {% set total_spaces = spaces|length %}
484
  {% set running_spaces = spaces|selectattr('status', 'equalto', 'RUNNING')|list|length %}
 
509
  </div>
510
  </div>
511
 
512
+ {% for owner, owner_spaces in grouped_spaces.items() %}
513
+ {% set running_count = owner_spaces|selectattr('status', 'equalto', 'RUNNING')|list|length %}
514
+ {% set building_count = owner_spaces|selectattr('status', 'equalto', 'BUILDING')|list|length %}
515
+ {% set sleeping_count = owner_spaces|selectattr('status', 'equalto', 'SLEEPING')|list|length %}
516
+ {% set stopped_count = owner_spaces|selectattr('status', 'equalto', 'STOPPED')|list|length %}
517
+ {% set failed_count = owner_spaces|selectattr('status', 'equalto', 'FAILED')|list|length %}
518
+
519
+ <div class="owner-section">
520
+ <div class="owner-header">
521
+ <div class="owner-name">
522
+ <i class="fas fa-user-circle"></i>
523
+ {{ owner }}
524
+ </div>
525
+ <div class="status-stats">
526
+ <span class="status-badge status-RUNNING">
527
+ <i class="fas fa-play-circle"></i>
528
+ 运行中: {{ running_count }}
529
+ </span>
530
+ <span class="status-badge status-SLEEPING">
531
+ <i class="fas fa-moon"></i>
532
+ 休眠: {{ sleeping_count }}
533
+ </span>
534
+ <span class="status-badge status-STOPPED">
535
+ <i class="fas fa-stop-circle"></i>
536
+ 停止: {{ stopped_count }}
537
+ </span>
538
+ <span class="status-badge status-FAILED">
539
+ <i class="fas fa-exclamation-circle"></i>
540
+ 失败: {{ failed_count }}
541
+ </span>
542
  </div>
 
 
 
 
543
  </div>
544
 
545
+ <div class="space-grid">
546
+ {% for space in owner_spaces %}
547
+ <div class="space-card" data-space-id="{{ space.repo_id }}">
548
+ <div class="space-header">
549
+ <div class="space-name">
550
+ <i class="fas fa-cube"></i>
551
+ {{ space.name }}
552
+ </div>
553
+ <span class="status-badge status-{{ space.status }}">
554
+ <i class="fas fa-circle"></i>
555
+ {{ space.status }}
556
+ </span>
557
  </div>
558
+
559
+ <div class="space-content">
560
+ <div class="space-info">
561
+ <div class="info-item">
562
+ <span class="info-label">Space ID</span>
563
+ <span class="info-value">{{ space.repo_id }}</span>
564
+ </div>
565
+ <div class="info-item">
566
+ <span class="info-label">创建时间</span>
567
+ <span class="info-value">{{ space.created_at }}</span>
568
+ </div>
569
+ <div class="info-item">
570
+ <span class="info-label">最后修改</span>
571
+ <span class="info-value">{{ space.last_modified }}</span>
572
+ </div>
573
+ <div class="info-item">
574
+ <span class="info-label">应用端口</span>
575
+ <span class="info-value">{{ space.app_port }}</span>
576
+ </div>
577
+ </div>
578
+
579
+ <div class="space-metrics">
580
+ <div class="metric-item">
581
+ <div class="metric-value">{{ space.sdk }}</div>
582
+ <div class="metric-label">SDK 版本</div>
583
+ </div>
584
+ <div class="metric-item">
585
+ <div class="metric-value">{{ '私有' if space.private else '公开' }}</div>
586
+ <div class="metric-label">访问权限</div>
587
+ </div>
588
+ </div>
589
  </div>
590
+
591
+ <div class="action-buttons">
592
+ <a href="{{ space.url }}" target="_blank" class="action-button">
593
+ <i class="fas fa-external-link-alt"></i>
594
+ 查看
595
+ </a>
596
+ <button onclick="confirmAction('restart', '{{ space.repo_id }}')" class="action-button restart">
597
+ <i class="fas fa-sync-alt"></i>
598
+ 重启
599
+ </button>
600
+ <button onclick="confirmAction('rebuild', '{{ space.repo_id }}')" class="action-button">
601
+ <i class="fas fa-tools"></i>
602
+ 重建
603
+ </button>
604
  </div>
605
  </div>
606
+ {% endfor %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
607
  </div>
608
  </div>
609
+ {% endfor %}
 
610
  {% else %}
611
+ <div class="owner-section">
612
  <p style="text-align: center; padding: 2rem; color: var(--text-secondary);">
613
  <i class="fas fa-info-circle"></i>
614
  没有找到任何 Spaces。请确保你的账户中有创建的 Spaces,并且提供的 token 有正确的权限。
 
621
  <script>
622
  const socket = io();
623
 
624
+ // 页面加载完成后隐藏加载动画
625
+ window.addEventListener('load', function() {
626
+ document.getElementById('loading').style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
627
  });
628
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
629
  // 搜索功能
630
  document.getElementById('spaceSearch').addEventListener('input', function(e) {
631
  const searchTerm = e.target.value.toLowerCase();
632
  document.querySelectorAll('.space-card').forEach(card => {
633
  const spaceName = card.querySelector('.space-name').textContent.toLowerCase();
634
  const spaceId = card.dataset.spaceId.toLowerCase();
635
+ if (spaceName.includes(searchTerm) || spaceId.includes(searchTerm)) {
636
+ card.style.display = '';
637
+ } else {
638
+ card.style.display = 'none';
639
+ }
640
  });
641
  });
642
 
643
+ // 主题切换
644
+ const themeToggle = document.querySelector('.theme-toggle');
645
+ themeToggle.addEventListener('click', function() {
646
+ document.body.classList.toggle('light-theme');
647
+ const icon = this.querySelector('i');
648
+ icon.classList.toggle('fa-sun');
649
+ icon.classList.toggle('fa-moon');
650
+ });
651
+
652
+ // WebSocket 连接
653
  socket.on('connect', () => {
654
  console.log('Connected to server');
655
  });
 
669
  socket.connect();
670
  }
671
  });
 
 
 
 
672
 
673
  function updateSpaceStatuses() {
674
  document.querySelectorAll('.space-card').forEach(card => {
 
694
  }
695
  }
696
 
697
+ // 每30秒更新状态
698
  setInterval(updateSpaceStatuses, 30000);
699
+
700
+ // 添加卡片动画
701
+ document.querySelectorAll('.space-card').forEach(card => {
702
+ card.addEventListener('mouseenter', function() {
703
+ this.style.transform = 'translateY(-5px)';
704
+ });
705
+
706
+ card.addEventListener('mouseleave', function() {
707
+ this.style.transform = 'translateY(0)';
708
+ });
709
+ });
710
  </script>
711
  </body>
712
  </html>