Files
ToNav/templates/index.html
OpenClaw Agent 521cd9ba42 docs: add comprehensive README.md and fix bugs
- add README.md with usage and deployment guide
- fix category sync logic in backend
- fix URL overflow in admin services list
- fix data caching issues in front-end and back-end
- add 'View Front-end' button in admin dashboard
2026-02-13 07:00:49 +08:00

377 lines
9.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">
<div class="loading" style="color: #8c8c8c; font-size: 12px; padding: 10px;">加载分类...</div>
</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: 14px;
height: 14px;
border-radius: 50%;
background: #d9d9d9;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
flex-shrink: 0;
}
.card-status.online {
background: #52c41a;
color: #fff;
box-shadow: 0 0 10px rgba(82, 196, 26, 0.6);
}
.card-status.offline {
background: #ff4d4f;
color: #fff;
box-shadow: 0 0 10px rgba(255, 77, 79, 0.6);
}
.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?t=${new Date().getTime()}`);
allCategories = await response.json();
renderTabs();
} catch (err) {
console.error('加载分类失败:', err);
}
}
// 加载服务
async function loadServices() {
try {
const response = await fetch(`/api/services?t=${new Date().getTime()}`);
allServices = await response.json();
renderServices(window.currentTab || 'all');
updateLastCheckTime();
loadHealthStatus();
} catch (err) {
console.error('加载服务失败:', err);
document.getElementById('servicesGrid').innerHTML =
'<div class="loading">加载失败,请刷新页面</div>';
}
}
// 加载健康状态
async function loadHealthStatus() {
try {
const response = await fetch('/api/admin/health-check', {
method: 'POST'
});
const data = await response.json();
if (data.results) {
data.results.forEach(result => {
healthStatus[result.id] = result.status;
});
// 重新渲染以显示状态
renderServices(window.currentTab || 'all');
}
} catch (err) {
console.error('健康检测失败:', err);
}
}
// 渲染分类 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';
let statusClass = '';
let statusIcon = '';
if (status === 'online') {
statusClass = 'online';
statusIcon = '✓';
} else if (status === 'offline') {
statusClass = 'offline';
statusIcon = '✗';
}
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}">${statusIcon}</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);
// 页面显示时刷新
document.addEventListener('visibilitychange', function() {
if (!document.hidden) {
loadServices();
}
});
</script>
{% endblock %}