Spaces:
Sleeping
Sleeping
<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> | |