panel / templates /dashboard.html
epii-1
12345
6a2845d
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HF Space Manager -控制面板</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
body {
background-color: #f5f5f7;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
position: fixed;
top: 0;
left: 0;
right: 0;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);z-index: 1000;
}
.header h1 {
font-size: 1.5rem;
color: #1d1d1f;
font-weight: 600;
}
.logout {
background: none;
border: none;
color: #0071e3;
font-size: 1rem;
cursor: pointer;
padding: 8px 16px;
border-radius: 8px;
transition: all 0.3s ease;text-decoration: none;
}
.logout:hover {
background: rgba(0, 113, 227, 0.1);
}
.container {
flex: 1;
width: 100%;
max-width: 1200px;
margin: 100px auto 0;
padding: 0 2rem 2rem;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 5px solid #f3f3f3;
border-top: 5px solid #0071e3;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.owner-section {
margin-bottom: 2rem;background: white;
border-radius: 18px;
padding: 1.5rem;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
}
.owner-name {
font-size: 1.3rem;
font-weight: 600;
color: #1d1d1f;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #e5e5e7;
display: flex;
justify-content: space-between;align-items: center;
}
.space-count {
font-size: 0.9rem;
color: #86868b;}
.space-status-count {
margin-left: 10px;
}
.space-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
width: 100%;
}
.space-card {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
display: flex;
flex-direction: column;}
.space-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.space-name {
font-size: 1.1rem;
font-weight: 500;
color: #1d1d1f;
margin-bottom: 1rem;
}
.space-info {
font-size: 0.9rem;
color: #86868b;
margin-bottom: 1rem;
flex-grow: 1;
}
.space-info p {
margin-bottom: 0.5rem;
}
.status-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-weight: 500;}
.status-BUILDING {
background-color: #ff9500;
color: white;
}
.status-RUNNING {
background-color: #34c759;
color: white;
}
.status-SLEEPING {
background-color: #007aff;
color: white;
}
.status-STOPPED {
background-color: #8e8e93;
color: white;
}
.status-FAILED {
background-color: #ff3b30;
color: white;
}
.status-BUILD_ERROR {
background-color: #ff3b30;
color: white;
}
.status-UNKNOWN {
background-color: #8e8e93;
color: white;
}
.action-buttons {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
margin-top: auto;
}
.action-button {
padding: 8px 12px;
border-radius: 8px;
border: none;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.3s ease;text-align: center;
text-decoration: none;
}
.view {
background: #e5e5e7;
color: #1d1d1f;
}
.view:hover {
background: #d5d5d7;
}
.restart {
background: #0071e3;
color: white;
}
.restart:hover {
background: #0077ED;
}
.rebuild {
background: #f5f5f7;
color: #1d1d1f;
}
.rebuild:hover {
background: #e5e5e7;
}
.footer {
text-align: center;
padding: 2rem;
color: #86868b;
font-size: 0.9rem;
background: white;
border-top: 1px solid #e5e5e7;
}
.footer a {
color: #0071e3;
text-decoration: none;transition: color 0.3s ease;
}
.footer a:hover {
color: #0077ED;
text-decoration: underline;
}
@media (max-width: 768px) {
.container {
padding: 0 1rem;
}
.owner-name {
flex-direction: column;
align-items: flex-start;
}
.space-count {
margin-top: 0.5rem;
}
}
</style>
</head>
<body>
<div id="loading" class="loading-overlay">
<div class="loading-spinner"></div>
</div>
<header class="header">
<h1>HF Space Manager</h1>
<a href="/logout" class="logout">退出登录</a>
</header>
<div class="container">
{% if spaces %}
{% set grouped_spaces = {} %}
{% for space in spaces %}
{% if space.owner not in grouped_spaces %}
{% set _ = grouped_spaces.update({space.owner: []}) %}
{% endif %}
{% set _ = grouped_spaces[space.owner].append(space) %}
{% endfor %}
{% for owner, owner_spaces in grouped_spaces.items() %}
{% set sorted_spaces = owner_spaces %}
{% set running_count = sorted_spaces | selectattr('status','equalto','RUNNING') | list | length %}
{% set building_count = sorted_spaces | selectattr('status','equalto','BUILDING') | list | length %}
{% set sleeping_count = sorted_spaces | selectattr('status','equalto','SLEEPING') | list | length %}
{% set stopped_count = sorted_spaces | selectattr('status','equalto','STOPPED') | list | length %}
{% set failed_count = sorted_spaces | selectattr('status','equalto','BUILD_ERROR') | list | length %}
<div class="owner-section">
<div class="owner-name">
<span>{{ owner }}</span>
<span class="space-count">
总数: {{ sorted_spaces | length }}
<span class="space-status-count">运行:{{ running_count }}</span>
<span class="space-status-count">休眠:{{ sleeping_count }}</span>
<span class="space-status-count">停止:{{ stopped_count }}</span>
<span class="space-status-count">失败:{{ failed_count }}</span>
</span>
</div><div class="space-grid">
{% for space in sorted_spaces %}
<div class="space-card" data-space-id="{{ space.repo_id }}"><div class="space-name">{{ space.name }}</div>
<div class="space-info">
<p>ID: {{ space.repo_id }}</p>
<p>状态: <span class="status-badge status-{{ space.status }}">{{ space.status }}</span></p><p>创建时间: {{ space.created_at }}</p>
<p>最后修改: {{ space.last_modified }}</p>
<p>SDK: {{ space.sdk }}</p><p>App端口: {{ space.app_port }}</p>
{% if space.tags %}
<p>标签:{% for tag in space.tags %}<span class="tag">{{ tag }}</span>
{% endfor %}
</p>
{% endif %}<p>私有: {{ '是' if space.private else '否' }}</p>
</div><div class="action-buttons">
<a href="{{ space.url }}" target="_blank" class="action-button view">查看</a>
<button onclick="confirmAction('restart', '{{ space.repo_id }}')" class="action-button restart">重启</button>
<button onclick="confirmAction('rebuild', '{{ space.repo_id }}')" class="action-button rebuild">重建</button>
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% else %}
<div class="owner-section">
<p style="text-align: center; color: #86868b;">没有找到任何 Spaces。请确保你的账户中有创建的Spaces,并且提供的 token 有正确的权限。</p>
</div>
{% endif %}
</div>
<footer class="footer">
<a href="https://github.com/ssfun/hf-space-manager">HF Space Manager</a>
<a href="https://github.com/ssfun">ssfun</a> 构建,源代码遵循
<a href="https://opensource.org/license/mit">MIT 协议</a>
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
<script>
// 连接 WebSocket
const socket = io();
socket.on('connect', () => {
console.log('Connected to server');
});
socket.on('disconnect', () => {
console.log('Disconnected from server');
});
// 当收到缓存更新通知时刷新数据
socket.on('spaces_updated', (data) => {
updateSpaceStatuses();
});
// 页面可见性变化处理
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
// 页面隐藏时断开连接
socket.disconnect();
} else {
// 页面可见时重新连接
socket.connect();
}
});
// 页面加载完成后隐藏加载动画
window.addEventListener('load', function() {
document.getElementById('loading').style.display = 'none';
});
// 定期更新状态
function updateSpaceStatuses() {document.querySelectorAll('.space-card').forEach(card => {
const spaceId = card.dataset.spaceId;
fetch(`/api/space/${spaceId}/status`)
.then(response => response.json())
.then(data => {
const statusElement = card.querySelector('.status-badge');
if (statusElement && data.status) {
statusElement.className = `status-badge status-${data.status}`;statusElement.textContent = data.status;
}
})
.catch(error => console.error('Error updating status:', error));
});
}
// 在进行操作时显示加载动画
function confirmAction(action, spaceId) {
var actionText = action === 'restart' ? '重启' : '重建';
if (confirm(`确定要${actionText} "${spaceId}" 吗?`)) {document.getElementById('loading').style.display = 'flex';
window.location.href = `/action/${action}/${spaceId}`;
}
}
// 每30秒更新一次状态
setInterval(updateSpaceStatuses, 30000);
</script>
</body>
</html>