Files
ToNav/templates/index.html
OpenClaw Agent 872526505e Initial commit: ToNav Personal Navigation Page
- Flask + SQLite 个人导航页系统
- 前台导航页(分类Tab、卡片展示)
- 管理后台(服务管理、分类管理、健康检测)
- 响应式设计
- Systemd 服务配置
2026-02-12 21:57:15 +08:00

336 lines
8.4 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}ToNav - 个人导航页{% endblock %}
{% block content %}
<div class="container">
<!-- 头部 -->
<div class="header">
<h1>🧭 ToNav</h1>
<div class="subtitle">个人导航站</div>
<div class="status-bar" id="statusBar">
<span id="lastCheckTime">检测中...</span>
</div>
</div>
<!-- 分类 Tabs -->
<div class="tabs" id="categoryTabs">
<button class="tab-btn active" data-category="all">全部</button>
<button class="tab-btn" data-category="内网服务">内网服务</button>
<button class="tab-btn" data-category="开发工具">开发工具</button>
<button class="tab-btn" data-category="测试环境">测试环境</button>
</div>
<!-- 服务卡片网格 -->
<div class="services-grid" id="servicesGrid">
<div class="loading">加载中...</div>
</div>
<!-- 底部 -->
<div class="footer">
<div>© 2026 ToNav - <a href="/admin">管理后台</a></div>
</div>
</div>
<style>
.header {
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
color: #fff;
padding: 25px 20px;
border-radius: 20px 20px 0 0;
text-align: center;
}
.header h1 {
font-size: 24px;
font-weight: 700;
margin-bottom: 5px;
}
.subtitle {
font-size: 13px;
color: #8c8c8c;
margin-bottom: 10px;
}
.status-bar {
font-size: 12px;
color: #595959;
}
.tabs {
display: flex;
background: #262626;
padding: 8px;
gap: 5px;
}
.tab-btn {
flex: 1;
padding: 12px 10px;
color: #8c8c8c;
border: none;
background: none;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
font-weight: 600;
border-radius: 10px;
}
.tab-btn:hover {
color: #bfbfbf;
background: rgba(255,255,255,0.05);
}
.tab-btn.active {
color: #fff;
background: var(--main-red);
box-shadow: 0 4px 15px rgba(255, 77, 79, 0.4);
}
.services-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 15px;
padding: 20px 0;
min-height: 300px;
}
.service-card {
background: #fff;
border-radius: 15px;
padding: 20px;
transition: all 0.3s ease;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
animation: fadeInUp 0.5s ease backwards;
display: flex;
flex-direction: column;
gap: 8px;
}
.service-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0,0,0,0.2);
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-icon {
font-size: 32px;
}
.card-status {
width: 12px;
height: 12px;
border-radius: 50%;
background: #d9d9d9;
}
.card-status.online {
background: #52c41a;
box-shadow: 0 0 8px rgba(82, 196, 26, 0.5);
}
.card-status.offline {
background: #ff4d4f;
box-shadow: 0 0 8px rgba(255, 77, 79, 0.5);
}
.card-name {
font-size: 18px;
font-weight: 600;
color: #262626;
}
.card-desc {
font-size: 13px;
color: #8c8c8c;
flex: 1;
}
.card-footer {
font-size: 11px;
color: #bfbfbf;
}
.loading {
grid-column: 1 / -1;
text-align: center;
padding: 60px;
color: #999;
}
.empty-state {
grid-column: 1 / -1;
text-align: center;
padding: 60px;
color: #999;
}
.footer {
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
color: #8c8c8c;
padding: 20px;
border-radius: 0 0 20px 20px;
text-align: center;
font-size: 13px;
}
.footer a {
color: var(--main-red);
transition: opacity 0.3s;
}
.footer a:hover {
opacity: 0.8;
}
@media (max-width: 480px) {
.services-grid {
grid-template-columns: 1fr;
}
.tab-btn {
font-size: 13px;
padding: 10px 8px;
}
}
</style>
{% endblock %}
{% block scripts %}
<script>
let allServices = [];
let allCategories = [];
let healthStatus = {}; // 存储健康状态
// 初始化
document.addEventListener('DOMContentLoaded', function() {
loadCategories();
loadServices();
setupTabs();
});
// 加载分类
async function loadCategories() {
try {
const response = await fetch('/api/categories');
allCategories = await response.json();
renderTabs();
} catch (err) {
console.error('加载分类失败:', err);
}
}
// 加载服务
async function loadServices() {
try {
const response = await fetch('/api/services');
allServices = await response.json();
renderServices(window.currentTab || 'all');
updateLastCheckTime();
} catch (err) {
console.error('加载服务失败:', err);
document.getElementById('servicesGrid').innerHTML =
'<div class="loading">加载失败,请刷新页面</div>';
}
}
// 渲染分类 Tabs
function renderTabs() {
const tabsContainer = document.getElementById('categoryTabs');
let html = '<button class="tab-btn active" data-category="all">全部</button>';
allCategories.forEach(cat => {
html += `<button class="tab-btn" data-category="${cat.name}">${cat.name}</button>`;
});
tabsContainer.innerHTML = html;
setupTabs();
}
// 设置 Tab 点击事件
function setupTabs() {
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', function() {
const category = this.dataset.category;
window.currentTab = category;
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
renderServices(category);
});
});
}
// 渲染服务卡片
function renderServices(category) {
const container = document.getElementById('servicesGrid');
const filteredServices = category === 'all'
? allServices
: allServices.filter(s => s.category === category);
if (filteredServices.length === 0) {
container.innerHTML = '<div class="empty-state">暂无服务</div>';
return;
}
let html = '';
filteredServices.forEach((service, index) => {
const status = healthStatus[service.id] || 'unknown';
const statusClass = status === 'online' ? 'online' : (status === 'offline' ? 'offline' : '');
html += `
<a href="${service.url}" target="_blank" class="service-card" style="animation-delay: ${index * 0.05}s">
<div class="card-header">
<span class="card-icon">${service.icon || '📡'}</span>
<span class="card-status ${statusClass}"></span>
</div>
<div class="card-name">${service.name}</div>
<div class="card-desc">${service.description || ''}</div>
<div class="card-footer">${service.category}</div>
</a>
`;
});
container.innerHTML = html;
}
// 更新最后检测时间
function updateLastCheckTime() {
const now = new Date();
const timeStr = now.toLocaleDateString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
document.getElementById('lastCheckTime').textContent = `最后更新: ${timeStr}`;
}
// 定时刷新每30秒
setInterval(() => {
loadServices();
}, 30000);
</script>
{% endblock %}