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

373 lines
9.8 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 admin-layout">
<!-- 顶部导航 -->
<div class="admin-header">
<div class="header-left">
<h1>🧭 ToNav 管理后台</h1>
<span class="username" id="username">加载中...</span>
</div>
<button class="btn btn-primary" onclick="location.href='/admin/logout'">退出登录</button>
</div>
<!-- 主内容区 -->
<div class="main-content">
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon">📡</div>
<div class="stat-info">
<div class="stat-value" id="totalServices">0</div>
<div class="stat-label">总服务数</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon"></div>
<div class="stat-info">
<div class="stat-value" id="enabledServices">0</div>
<div class="stat-label">已启用</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">📂</div>
<div class="stat-info">
<div class="stat-value" id="totalCategories">0</div>
<div class="stat-label">分类数</div>
</div>
</div>
</div>
<!-- 快捷操作 -->
<div class="quick-actions">
<button class="action-btn" onclick="location.href='/admin/services'">
<span class="action-icon">📡</span>
<span class="action-label">服务管理</span>
</button>
<button class="action-btn" onclick="location.href='/admin/categories'">
<span class="action-icon">📂</span>
<span class="action-label">分类管理</span>
</button>
<button class="action-btn" onclick="runHealthCheck()">
<span class="action-icon">🔍</span>
<span class="action-label">健康检测</span>
</button>
</div>
</div>
<!-- 修改密码 -->
<div class="settings-card">
<h2>修改密码</h2>
<form id="changePasswordForm">
<div class="form-row">
<input type="password" id="oldPassword" class="input" placeholder="旧密码" required>
<input type="password" id="newPassword" class="input" placeholder="新密码至少6位" required minlength="6">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">修改密码</button>
</div>
</form>
</div>
</div>
<style>
.admin-layout {
max-width: 900px;
}
.admin-header {
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
color: #fff;
padding: 25px 30px;
border-radius: 20px 20px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left h1 {
font-size: 22px;
font-weight: 700;
margin-bottom: 5px;
}
.username {
font-size: 13px;
color: #8c8c8c;
}
.admin-content {
background: #fff;
padding: 30px;
}
.main-content {
padding: 30px;
background: #fff;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-bottom: 20px;
}
.stat-card {
display: flex;
align-items: center;
gap: 15px;
padding: 20px;
background: #fafafa;
border-radius: 15px;
transition: all 0.3s;
height: 100px;
}
.stat-card:hover {
background: #f0f0f0;
transform: translateY(-2px);
}
.stat-icon {
font-size: 36px;
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 32px;
font-weight: 800;
color: var(--main-red);
line-height: 1;
}
.stat-label {
font-size: 13px;
color: #8c8c8c;
margin-top: 5px;
}
.quick-actions {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
}
.action-btn {
background: #fff;
border: 2px solid #f0f0f0;
border-radius: 15px;
padding: 20px;
cursor: pointer;
transition: all 0.3s;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
height: 100px;
}
.action-btn:hover {
border-color: var(--main-red);
background: #fff2f0;
transform: translateY(-3px);
box-shadow: 0 4px 15px rgba(255, 77, 79, 0.2);
}
.action-icon {
font-size: 32px;
}
.action-label {
font-size: 14px;
font-weight: 600;
color: #262626;
}
.settings-card {
background: #fff;
padding: 30px;
border-radius: 0 0 20px 20px;
}
.settings-card h2 {
font-size: 18px;
font-weight: 600;
margin-bottom: 20px;
color: #262626;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-bottom: 15px;
}
.input {
width: 100%;
padding: 12px 15px;
border: 1px solid #d9d9d9;
border-radius: 10px;
font-size: 14px;
transition: all 0.3s;
}
.input:focus {
outline: none;
border-color: var(--main-red);
box-shadow: 0 0 0 3px rgba(255, 77, 79, 0.1);
}
.form-actions {
display: flex;
gap: 10px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: var(--main-red);
color: #fff;
box-shadow: 0 4px 15px rgba(255, 77, 79, 0.4);
}
.btn-primary:hover {
background: #ff7875;
}
@media (max-width: 768px) {
.stats-grid,
.quick-actions,
.form-row {
grid-template-columns: 1fr;
}
.admin-header {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.stat-card,
.action-btn {
height: auto;
}
}
</style>
{% endblock %}
{% block scripts %}
<script>
// 初始化
document.addEventListener('DOMContentLoaded', function() {
loadStats();
loadUsername();
});
// 加载用户名
async function loadUsername() {
try {
const response = await fetch('/api/admin/login/status');
const data = await response.json();
if (data.logged_in) {
document.getElementById('username').textContent = data.username;
} else {
window.location.href = '/admin/login';
}
} catch (err) {
window.location.href = '/admin/login';
}
}
// 加载统计数据
async function loadStats() {
try {
const [servicesResp, categoriesResp] = await Promise.all([
fetch('/api/admin/services'),
fetch('/api/admin/categories')
]);
const services = await servicesResp.json();
const categories = await categoriesResp.json();
document.getElementById('totalServices').textContent = services.length;
document.getElementById('enabledServices').textContent =
services.filter(s => s.is_enabled).length;
document.getElementById('totalCategories').textContent = categories.length;
} catch (err) {
console.error('加载统计失败:', err);
}
}
// 健康检测
async function runHealthCheck() {
const btn = event.target.closest('.action-btn');
const icon = btn.querySelector('.action-icon');
const label = btn.querySelector('.action-label');
try {
icon.textContent = '⏳';
label.textContent = '检测中...';
const response = await fetch('/api/admin/health-check', {
method: 'POST'
});
const data = await response.json();
if (data.results) {
const online = data.results.filter(r => r.status === 'online').length;
const offline = data.results.filter(r => r.status === 'offline').length;
alert(`检测完成:\n在线: ${online}\n离线: ${offline}`);
}
} catch (err) {
alert('检测失败: ' + err.message);
} finally {
icon.textContent = '🔍';
label.textContent = '健康检测';
}
}
// 修改密码
document.getElementById('changePasswordForm').addEventListener('submit', async function(e) {
e.preventDefault();
const oldPassword = document.getElementById('oldPassword').value;
const newPassword = document.getElementById('newPassword').value;
try {
const response = await fetch('/api/admin/change-password', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({old_password: oldPassword, new_password: newPassword})
});
const data = await response.json();
if (response.ok) {
alert('密码修改成功,请重新登录');
this.reset();
} else {
alert(data.error || '修改失败');
}
} catch (err) {
alert('请求失败: ' + err.message);
}
});
</script>
{% endblock %}