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

643 lines
18 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>📡 服务管理</h1>
<a href="/admin" class="back-link">← 返回首页</a>
</div>
<button class="btn btn-primary" onclick="showCreateModal()">+ 新建服务</button>
</div>
<!-- 服务列表 -->
<div class="services-list">
<div class="loading">加载中...</div>
</div>
<!-- 创建/编辑弹窗 -->
<div class="modal" id="serviceModal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modalTitle">新建服务</h2>
<button class="close-btn" onclick="closeModal()">×</button>
</div>
<form id="serviceForm" class="modal-body">
<input type="hidden" id="serviceId">
<div class="form-group">
<label>服务名称 *</label>
<input type="text" id="serviceName" class="input" required>
</div>
<div class="form-group">
<label>访问URL *</label>
<input type="url" id="serviceUrl" class="input" required placeholder="http://">
</div>
<div class="form-group">
<label>描述</label>
<input type="text" id="serviceDesc" class="input" placeholder="简短描述">
</div>
<div class="form-row">
<div class="form-group">
<label>图标emoji</label>
<input type="text" id="serviceIcon" class="input" placeholder="📡">
</div>
<div class="form-group">
<label>分类</label>
<select id="serviceCategory" class="input">
<option value="默认">默认</option>
<option value="内网服务">内网服务</option>
<option value="开发工具">开发工具</option>
<option value="测试环境">测试环境</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>排序权重</label>
<input type="number" id="serviceSort" class="input" value="0">
</div>
<div class="form-group">
<label>状态</label>
<label class="checkbox-label">
<input type="checkbox" id="serviceEnabled" checked>
<span>启用</span>
</label>
</div>
</div>
<div class="form-divider">健康检测设置</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="healthCheckEnabled">
<span>启用健康检测</span>
</label>
</div>
<div class="form-group" id="healthUrlGroup" style="display: none;">
<label>检测URL</label>
<input type="url" id="healthCheckUrl" class="input" placeholder="留空则使用主URL">
</div>
<div class="form-actions">
<button type="button" class="btn" onclick="closeModal()">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
</div>
<style>
:root {
--main-red: #ff4d4f;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
.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;
}
.back-link {
color: #8c8c8c;
font-size: 13px;
text-decoration: none;
}
.back-link:hover {
color: #bfbfbf;
}
.services-list {
background: #fff;
padding: 30px;
border-radius: 0 0 20px 20px;
min-height: 400px;
}
.service-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: #fafafa;
border-radius: 12px;
margin-bottom: 15px;
transition: all 0.3s;
}
.service-item:hover {
background: #f0f0f0;
transform: translateX(5px);
}
.service-info {
display: flex;
align-items: center;
gap: 15px;
flex: 1;
}
.service-icon-large {
font-size: 36px;
}
.service-details {
flex: 1;
}
.service-name {
font-size: 18px;
font-weight: 600;
color: #262626;
margin-bottom: 5px;
}
.service-url {
font-size: 13px;
color: #8c8c8c;
}
.service-meta {
display: flex;
gap: 10px;
font-size: 12px;
color: #999;
}
.tag {
padding: 2px 8px;
border-radius: 4px;
background: #e6f7ff;
color: #1890ff;
}
.service-actions {
display: flex;
gap: 8px;
}
.action-btn-sm {
padding: 8px 14px;
border: 1px solid #d9d9d9;
background: #fff;
border-radius: 8px;
font-size: 13px;
cursor: pointer;
transition: all 0.3s;
}
.action-btn-sm:hover {
border-color: var(--main-red);
color: var(--main-red);
}
.action-btn-sm.danger:hover {
background: #fff2f0;
border-color: #ff4d4f;
color: #ff4d4f;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #d9d9d9;
}
.status-dot.enabled {
background: #52c41a;
}
/* Modal */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.6);
z-index: 1000;
align-items: center;
justify-content: center;
animation: fadeIn 0.3s ease;
}
.modal.active {
display: flex;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.modal-content {
background: #fff;
border-radius: 20px;
max-width: 600px;
width: 90%;
max-height: 85vh;
overflow-y: auto;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from { transform: translateY(50px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.modal-header {
padding: 20px 25px;
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
color: #fff;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 {
font-size: 18px;
font-weight: 600;
}
.close-btn {
background: none;
border: none;
color: #fff;
font-size: 28px;
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.close-btn:hover {
background: rgba(255,255,255,0.2);
}
.modal-body {
padding: 25px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
color: #262626;
}
.input, .input[type="text"], .input[type="url"], .input[type="number"], select.input {
width: 100%;
padding: 10px 12px;
border: 1px solid #d9d9d9;
border-radius: 8px;
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-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.form-divider {
margin: 25px 0;
padding-top: 20px;
border-top: 1px solid #f0f0f0;
font-size: 14px;
font-weight: 600;
color: #8c8c8c;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.checkbox-label input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #f0f0f0;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
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;
}
.loading {
text-align: center;
padding: 60px;
color: #999;
}
.empty-state {
text-align: center;
padding: 60px;
color: #999;
}
@media (max-width: 768px) {
.service-item {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.service-actions {
width: 100%;
justify-content: flex-end;
}
.form-row {
grid-template-columns: 1fr;
}
}
</style>
{% endblock %}
{% block scripts %}
<script>
let allServices = [];
// 初始化
document.addEventListener('DOMContentLoaded', function() {
loadServices();
setupForm();
});
// 加载服务列表
async function loadServices() {
try {
const response = await fetch('/api/admin/services');
if (!response.ok) {
window.location.href = '/admin/login';
return;
}
allServices = await response.json();
renderServices();
} catch (err) {
console.error('加载服务失败:', err);
window.location.href = '/admin/login';
}
}
// 渲染服务列表
function renderServices() {
const container = document.querySelector('.services-list');
if (allServices.length === 0) {
container.innerHTML = '<div class="empty-state">暂无服务</div>';
return;
}
let html = '';
allServices.forEach(service => {
html += `
<div class="service-item">
<div class="service-info">
<span class="service-icon-large">${service.icon || '📡'}</span>
<div class="service-details">
<div class="service-name">
${service.name}
<span class="status-dot ${service.is_enabled ? 'enabled' : ''}"></span>
</div>
<div class="service-url">${service.url}</div>
<div class="service-meta">
<span class="tag">${service.category}</span>
<span>排序: ${service.sort_order}</span>
${service.health_check_enabled ? '<span>🔍 检测中</span>' : ''}
</div>
</div>
</div>
<div class="service-actions">
<button class="action-btn-sm" onclick="editService(${service.id})">编辑</button>
<button class="action-btn-sm" onclick="toggleService(${service.id})">
${service.is_enabled ? '禁用' : '启用'}
</button>
<button class="action-btn-sm danger" onclick="deleteService(${service.id}, '${service.name}')">删除</button>
</div>
</div>
`;
});
container.innerHTML = html;
}
// 设置表单
function setupForm() {
// 健康检测URL显示/隐藏
document.getElementById('healthCheckEnabled').addEventListener('change', function() {
document.getElementById('healthUrlGroup').style.display = this.checked ? 'block' : 'none';
});
// 表单提交
document.getElementById('serviceForm').addEventListener('submit', function(e) {
e.preventDefault();
saveService();
});
}
// 显示创建弹窗
function showCreateModal() {
document.getElementById('modalTitle').textContent = '新建服务';
document.getElementById('serviceId').value = '';
document.getElementById('serviceForm').reset();
document.getElementById('serviceEnabled').checked = true;
document.getElementById('serviceSort').value = '0';
document.getElementById('healthUrlGroup').style.display = 'none';
document.getElementById('serviceModal').classList.add('active');
}
// 编辑服务
function editService(id) {
const service = allServices.find(s => s.id === id);
if (!service) return;
document.getElementById('modalTitle').textContent = '编辑服务';
document.getElementById('serviceId').value = service.id;
document.getElementById('serviceName').value = service.name;
document.getElementById('serviceUrl').value = service.url;
document.getElementById('serviceDesc').value = service.description || '';
document.getElementById('serviceIcon').value = service.icon || '';
document.getElementById('serviceCategory').value = service.category;
document.getElementById('serviceSort').value = service.sort_order;
document.getElementById('serviceEnabled').checked = service.is_enabled === 1;
document.getElementById('healthCheckEnabled').checked = service.health_check_enabled === 1;
document.getElementById('healthCheckUrl').value = service.health_check_url || '';
document.getElementById('healthUrlGroup').style.display = service.health_check_enabled ? 'block' : 'none';
document.getElementById('serviceModal').classList.add('active');
}
// 保存服务
async function saveService() {
const id = document.getElementById('serviceId').value;
const data = {
name: document.getElementById('serviceName').value,
url: document.getElementById('serviceUrl').value,
description: document.getElementById('serviceDesc').value,
icon: document.getElementById('serviceIcon').value,
category: document.getElementById('serviceCategory').value,
sort_order: parseInt(document.getElementById('serviceSort').value) || 0,
is_enabled: document.getElementById('serviceEnabled').checked ? 1 : 0,
health_check_enabled: document.getElementById('healthCheckEnabled').checked ? 1 : 0,
health_check_url: document.getElementById('healthCheckUrl').value
};
try {
const url = id ? `/api/admin/services/${id}` : '/api/admin/services';
const method = id ? 'PUT' : 'POST';
const response = await fetch(url, {
method: method,
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
if (response.ok) {
closeModal();
loadServices();
} else {
const result = await response.json();
alert(result.error || '保存失败');
}
} catch (err) {
alert('请求失败: ' + err.message);
}
}
// 切换服务状态
async function toggleService(id) {
if (!confirm('确定要切换服务状态吗?')) return;
try {
const response = await fetch(`/api/admin/services/${id}/toggle`, {
method: 'POST'
});
if (response.ok) {
loadServices();
} else {
alert('操作失败');
}
} catch (err) {
alert('请求失败: ' + err.message);
}
}
// 删除服务
async function deleteService(id, name) {
if (!confirm(`确定要删除服务"${name}"吗?此操作不可恢复。`)) return;
try {
const response = await fetch(`/api/admin/services/${id}`, {
method: 'DELETE'
});
if (response.ok) {
loadServices();
} else {
alert('删除失败');
}
} catch (err) {
alert('请求失败: ' + err.message);
}
}
// 关闭弹窗
function closeModal() {
document.getElementById('serviceModal').classList.remove('active');
}
// 点击外部关闭弹窗
document.getElementById('serviceModal').addEventListener('click', function(e) {
if (e.target === this) {
closeModal();
}
});
</script>
{% endblock %}