mistpe commited on
Commit
95720c9
·
verified ·
1 Parent(s): ae7166a

Update templates/dashboard.html

Browse files
Files changed (1) hide show
  1. templates/dashboard.html +167 -528
templates/dashboard.html CHANGED
@@ -1,375 +1,31 @@
1
- <!DOCTYPE html>
2
- <html lang="zh">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>HF Space Manager - 控制面板</title>
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
- * {
24
- margin: 0;
25
- padding: 0;
26
- box-sizing: border-box;
27
- font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
28
- }
29
-
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;
45
- right: 0;
46
- z-index: 1000;
47
- border-bottom: 1px solid var(--border-color);
48
- }
49
-
50
- .header-content {
51
- max-width: 1400px;
52
- margin: 0 auto;
53
- padding: 1rem 2rem;
54
- display: flex;
55
- justify-content: space-between;
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;
68
- color: var(--text-primary);
69
- display: flex;
70
- align-items: center;
71
- gap: 0.5rem;
72
- }
73
-
74
- .logo i {
75
- color: var(--primary-color);
76
- text-shadow: 0 0 10px var(--primary-color);
77
- }
78
-
79
- .search-bar {
80
- position: relative;
81
- width: 300px;
82
- }
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 {
124
- max-width: 1400px;
125
- margin: 80px auto 0;
126
- padding: 2rem;
127
- }
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 {
165
- color: var(--text-secondary);
166
- font-size: 0.9rem;
167
- text-transform: uppercase;
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;
205
- font-size: 0.875rem;
206
- font-weight: 500;
207
- display: flex;
208
- align-items: center;
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 {
254
- display: flex;
255
- flex-direction: column;
256
- gap: 0.25rem;
257
- }
258
-
259
- .info-label {
260
- color: var(--text-secondary);
261
- font-size: 0.8rem;
262
- text-transform: uppercase;
263
- letter-spacing: 0.5px;
264
- }
265
-
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 {
296
- display: grid;
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;
309
- align-items: center;
310
- justify-content: center;
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 {
326
- border-color: var(--primary-color);
327
- color: var(--primary-color);
328
- }
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,10 +34,11 @@
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,52 +47,19 @@
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,21 +70,19 @@
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,6 +101,23 @@
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,106 +148,69 @@
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,35 +223,80 @@
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,6 +316,10 @@
669
  socket.connect();
670
  }
671
  });
 
 
 
 
672
 
673
  function updateSpaceStatuses() {
674
  document.querySelectorAll('.space-card').forEach(card => {
@@ -694,19 +345,7 @@
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>
 
1
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
+ /* 自定义滚动条 */
4
+ ::-webkit-scrollbar {
5
+ width: 8px;
6
+ height: 8px;
7
+ background: var(--surface-color);
8
  }
9
 
10
+ ::-webkit-scrollbar-thumb {
11
+ background: var(--border-color);
 
 
 
 
12
  border-radius: 4px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  }
14
 
15
+ ::-webkit-scrollbar-thumb:hover {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  background: var(--primary-color);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  }
18
 
19
+ /* 加载动画 */
20
  .loading-overlay {
21
  position: fixed;
22
  top: 0;
23
  left: 0;
24
  right: 0;
25
  bottom: 0;
26
+ background: rgba(3, 7, 20, 0.8);
27
  backdrop-filter: blur(5px);
28
+ -webkit-backdrop-filter: blur(5px);
29
  display: flex;
30
  justify-content: center;
31
  align-items: center;
 
34
 
35
  .loading-spinner {
36
  position: relative;
37
+ width: 100px;
38
+ height: 100px;
39
  }
40
 
41
+ .loading-spinner::before,
42
  .loading-spinner::after {
43
  content: '';
44
  position: absolute;
 
47
  width: 100%;
48
  height: 100%;
49
  border-radius: 50%;
50
+ border: 2px solid transparent;
51
  border-top-color: var(--primary-color);
52
+ animation: spin 1.5s linear infinite;
53
  }
54
 
55
+ .loading-spinner::after {
56
+ border-top-color: var(--secondary-color);
57
+ animation-delay: 0.75s;
58
  }
59
 
60
+ @keyframes spin {
61
+ 0% { transform: rotate(0deg); }
62
+ 100% { transform: rotate(360deg); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  }
64
  </style>
65
  </head>
 
70
 
71
  <header class="header">
72
  <div class="header-content">
73
+ <div class="logo">
74
+ <i class="fas fa-server"></i>
75
+ HF Space Manager
76
+ </div>
77
+ <div class="nav-actions">
78
  <div class="search-bar">
79
  <i class="fas fa-search"></i>
80
  <input type="text" placeholder="搜索 Spaces..." id="spaceSearch">
81
  </div>
82
+ <button class="theme-toggle" id="themeToggle">
 
 
83
  <i class="fas fa-moon"></i>
84
  </button>
85
+ <a href="/logout" class="action-btn">
86
  <i class="fas fa-sign-out-alt"></i>
87
  退出
88
  </a>
 
101
  {% endfor %}
102
 
103
  <div class="dashboard-header">
104
+ <div class="bulk-actions" id="bulkActions" style="display: none;">
105
+ <div class="selection-info">
106
+ <i class="fas fa-check-square"></i>
107
+ 已选择 <span id="selectedCount">0</span> 个 Spaces
108
+ </div>
109
+ <div class="bulk-buttons">
110
+ <button class="bulk-btn" onclick="bulkRestart()">
111
+ <i class="fas fa-sync-alt"></i>
112
+ 批量重启
113
+ </button>
114
+ <button class="bulk-btn danger" onclick="bulkStop()">
115
+ <i class="fas fa-stop-circle"></i>
116
+ 批量停止
117
+ </button>
118
+ </div>
119
+ </div>
120
+
121
  <div class="stats-grid">
122
  {% set total_spaces = spaces|length %}
123
  {% set running_spaces = spaces|selectattr('status', 'equalto', 'RUNNING')|list|length %}
 
148
  </div>
149
  </div>
150
 
151
+ <div class="space-grid">
152
+ {% for space in spaces %}
153
+ <div class="space-card" data-space-id="{{ space.repo_id }}">
154
+ <input type="checkbox" class="card-checkbox" onchange="updateSelection(this)">
155
+ <div class="space-header">
156
+ <div class="space-name">
157
+ <i class="fas fa-cube"></i>
158
+ {{ space.name }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  </div>
160
+ <span class="status-badge status-{{ space.status }}">
161
+ <i class="fas fa-circle"></i>
162
+ {{ space.status }}
163
+ </span>
164
  </div>
165
 
166
+ <div class="space-content">
167
+ <div class="space-info">
168
+ <div class="info-item">
169
+ <span class="info-label">Space ID</span>
170
+ <span class="info-value">{{ space.repo_id }}</span>
 
 
 
 
 
 
 
171
  </div>
172
+ <div class="info-item">
173
+ <span class="info-label">创建时间</span>
174
+ <span class="info-value">{{ space.created_at }}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  </div>
176
+ <div class="info-item">
177
+ <span class="info-label">最后修改</span>
178
+ <span class="info-value">{{ space.last_modified }}</span>
179
+ </div>
180
+ <div class="info-item">
181
+ <span class="info-label">SDK 版本</span>
182
+ <span class="info-value">{{ space.sdk }}</span>
183
+ </div>
184
+ <div class="info-item">
185
+ <span class="info-label">应用端口</span>
186
+ <span class="info-value">{{ space.app_port }}</span>
187
+ </div>
188
+ <div class="info-item">
189
+ <span class="info-label">访问权限</span>
190
+ <span class="info-value">{{ '私有' if space.private else '公开' }}</span>
191
  </div>
192
  </div>
193
+ </div>
194
+
195
+ <div class="action-buttons">
196
+ <a href="{{ space.url }}" target="_blank" class="action-button">
197
+ <i class="fas fa-external-link-alt"></i>
198
+ 查看
199
+ </a>
200
+ <button onclick="confirmAction('restart', '{{ space.repo_id }}')" class="action-button restart">
201
+ <i class="fas fa-sync-alt"></i>
202
+ 重启
203
+ </button>
204
+ <button onclick="confirmAction('rebuild', '{{ space.repo_id }}')" class="action-button">
205
+ <i class="fas fa-tools"></i>
206
+ 重建
207
+ </button>
208
  </div>
209
  </div>
210
+ {% endfor %}
211
+ </div>
212
  {% else %}
213
+ <div class="dashboard-header">
214
  <p style="text-align: center; padding: 2rem; color: var(--text-secondary);">
215
  <i class="fas fa-info-circle"></i>
216
  没有找到任何 Spaces。请确保你的账户中有创建的 Spaces,并且提供的 token 有正确的权限。
 
223
  <script>
224
  const socket = io();
225
 
226
+ // 主题切换
227
+ const themeToggle = document.getElementById('themeToggle');
228
+ const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
229
+
230
+ // 初始化主题
231
+ if (localStorage.getItem('theme')) {
232
+ document.body.dataset.theme = localStorage.getItem('theme');
233
+ updateThemeIcon();
234
+ } else {
235
+ document.body.dataset.theme = prefersDarkScheme.matches ? 'dark' : 'light';
236
+ updateThemeIcon();
237
+ }
238
+
239
+ themeToggle.addEventListener('click', () => {
240
+ const currentTheme = document.body.dataset.theme;
241
+ const newTheme = currentTheme === 'light' ? 'dark' : 'light';
242
+ document.body.dataset.theme = newTheme;
243
+ localStorage.setItem('theme', newTheme);
244
+ updateThemeIcon();
245
  });
246
 
247
+ function updateThemeIcon() {
248
+ const icon = themeToggle.querySelector('i');
249
+ icon.className = document.body.dataset.theme === 'light' ? 'fas fa-moon' : 'fas fa-sun';
250
+ }
251
+
252
+ // 批量操作相关
253
+ let selectedSpaces = new Set();
254
+
255
+ function updateSelection(checkbox) {
256
+ const spaceCard = checkbox.closest('.space-card');
257
+ const spaceId = spaceCard.dataset.spaceId;
258
+
259
+ if (checkbox.checked) {
260
+ selectedSpaces.add(spaceId);
261
+ spaceCard.classList.add('selected');
262
+ } else {
263
+ selectedSpaces.delete(spaceId);
264
+ spaceCard.classList.remove('selected');
265
+ }
266
+
267
+ const selectedCount = selectedSpaces.size;
268
+ document.getElementById('selectedCount').textContent = selectedCount;
269
+ document.getElementById('bulkActions').style.display = selectedCount > 0 ? 'flex' : 'none';
270
+ }
271
+
272
+ function bulkRestart() {
273
+ if (confirm(`确定要重启选中的 ${selectedSpaces.size} 个 Spaces 吗?`)) {
274
+ document.getElementById('loading').style.display = 'flex';
275
+ // 这里添加批量重启的 API 调用
276
+ console.log('Restarting spaces:', Array.from(selectedSpaces));
277
+ }
278
+ }
279
+
280
+ function bulkStop() {
281
+ if (confirm(`确定要停止选中的 ${selectedSpaces.size} 个 Spaces 吗?`)) {
282
+ document.getElementById('loading').style.display = 'flex';
283
+ // 这里添加批量停止的 API 调用
284
+ console.log('Stopping spaces:', Array.from(selectedSpaces));
285
+ }
286
+ }
287
+
288
  // 搜索功能
289
  document.getElementById('spaceSearch').addEventListener('input', function(e) {
290
  const searchTerm = e.target.value.toLowerCase();
291
  document.querySelectorAll('.space-card').forEach(card => {
292
  const spaceName = card.querySelector('.space-name').textContent.toLowerCase();
293
  const spaceId = card.dataset.spaceId.toLowerCase();
294
+ const visible = spaceName.includes(searchTerm) || spaceId.includes(searchTerm);
295
+ card.style.display = visible ? '' : 'none';
 
 
 
296
  });
297
  });
298
 
299
+ // WebSocket 和状态更新
 
 
 
 
 
 
 
 
 
300
  socket.on('connect', () => {
301
  console.log('Connected to server');
302
  });
 
316
  socket.connect();
317
  }
318
  });
319
+
320
+ window.addEventListener('load', function() {
321
+ document.getElementById('loading').style.display = 'none';
322
+ });
323
 
324
  function updateSpaceStatuses() {
325
  document.querySelectorAll('.space-card').forEach(card => {
 
345
  }
346
  }
347
 
 
348
  setInterval(updateSpaceStatuses, 30000);
 
 
 
 
 
 
 
 
 
 
 
349
  </script>
350
  </body>
351
  </html>