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
This commit is contained in:
@@ -10,7 +10,10 @@
|
||||
<h1>📂 分类管理</h1>
|
||||
<a href="/admin" class="back-link">← 返回首页</a>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="showCreateModal()">+ 新建分类</button>
|
||||
<div class="header-right">
|
||||
<button class="btn btn-outline" onclick="window.open('/', '_blank')" style="margin-right: 10px;">查看前台 ↗</button>
|
||||
<button class="btn btn-primary" onclick="showCreateModal()">+ 新建分类</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分类列表 -->
|
||||
@@ -272,6 +275,17 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
<h1>🧭 ToNav 管理后台</h1>
|
||||
<span class="username" id="username">加载中...</span>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="location.href='/admin/logout'">退出登录</button>
|
||||
<div class="header-actions">
|
||||
<button class="btn btn-outline" onclick="window.open('/', '_blank')">查看前台 ↗</button>
|
||||
<button class="btn btn-primary" onclick="location.href='/admin/logout'">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
@@ -230,6 +233,23 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
<h1>📡 服务管理</h1>
|
||||
<a href="/admin" class="back-link">← 返回首页</a>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="showCreateModal()">+ 新建服务</button>
|
||||
<div class="header-right">
|
||||
<button class="btn btn-outline" onclick="window.open('/', '_blank')" style="margin-right: 10px;">查看前台 ↗</button>
|
||||
<button class="btn btn-primary" onclick="showCreateModal()">+ 新建服务</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 服务列表 -->
|
||||
@@ -193,6 +196,10 @@
|
||||
.service-url {
|
||||
font-size: 13px;
|
||||
color: #8c8c8c;
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.service-meta {
|
||||
@@ -386,6 +393,17 @@
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
@@ -450,7 +468,8 @@
|
||||
// 加载服务列表
|
||||
async function loadServices() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/services');
|
||||
// 添加时间戳防止缓存
|
||||
const response = await fetch(`/api/admin/services?t=${new Date().getTime()}`);
|
||||
if (!response.ok) {
|
||||
window.location.href = '/admin/login';
|
||||
return;
|
||||
@@ -520,22 +539,55 @@
|
||||
});
|
||||
}
|
||||
|
||||
// 加载分类列表到下拉框
|
||||
async function loadCategoriesToSelect() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/categories');
|
||||
const categories = await response.json();
|
||||
|
||||
const select = document.getElementById('serviceCategory');
|
||||
const currentValue = select.value;
|
||||
select.innerHTML = '';
|
||||
|
||||
categories.forEach(cat => {
|
||||
const option = document.createElement('option');
|
||||
option.value = cat.name;
|
||||
option.textContent = cat.name;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
// 尝试恢复之前选中的值
|
||||
if (currentValue) {
|
||||
const exists = categories.find(c => c.name === currentValue);
|
||||
if (exists) {
|
||||
select.value = currentValue;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载分类失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示创建弹窗
|
||||
function showCreateModal() {
|
||||
async 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';
|
||||
|
||||
await loadCategoriesToSelect();
|
||||
document.getElementById('serviceModal').classList.add('active');
|
||||
}
|
||||
|
||||
// 编辑服务
|
||||
function editService(id) {
|
||||
async function editService(id) {
|
||||
const service = allServices.find(s => s.id === id);
|
||||
if (!service) return;
|
||||
|
||||
await loadCategoriesToSelect();
|
||||
|
||||
document.getElementById('modalTitle').textContent = '编辑服务';
|
||||
document.getElementById('serviceId').value = service.id;
|
||||
document.getElementById('serviceName').value = service.name;
|
||||
|
||||
@@ -15,10 +15,7 @@
|
||||
|
||||
<!-- 分类 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 class="loading" style="color: #8c8c8c; font-size: 12px; padding: 10px;">加载分类...</div>
|
||||
</div>
|
||||
|
||||
<!-- 服务卡片网格 -->
|
||||
@@ -137,20 +134,27 @@
|
||||
}
|
||||
|
||||
.card-status {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
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;
|
||||
box-shadow: 0 0 8px rgba(82, 196, 26, 0.5);
|
||||
color: #fff;
|
||||
box-shadow: 0 0 10px rgba(82, 196, 26, 0.6);
|
||||
}
|
||||
|
||||
.card-status.offline {
|
||||
background: #ff4d4f;
|
||||
box-shadow: 0 0 8px rgba(255, 77, 79, 0.5);
|
||||
color: #fff;
|
||||
box-shadow: 0 0 10px rgba(255, 77, 79, 0.6);
|
||||
}
|
||||
|
||||
.card-name {
|
||||
@@ -231,7 +235,7 @@
|
||||
// 加载分类
|
||||
async function loadCategories() {
|
||||
try {
|
||||
const response = await fetch('/api/categories');
|
||||
const response = await fetch(`/api/categories?t=${new Date().getTime()}`);
|
||||
allCategories = await response.json();
|
||||
renderTabs();
|
||||
} catch (err) {
|
||||
@@ -242,10 +246,11 @@
|
||||
// 加载服务
|
||||
async function loadServices() {
|
||||
try {
|
||||
const response = await fetch('/api/services');
|
||||
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 =
|
||||
@@ -253,6 +258,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 加载健康状态
|
||||
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');
|
||||
@@ -297,13 +322,22 @@
|
||||
let html = '';
|
||||
filteredServices.forEach((service, index) => {
|
||||
const status = healthStatus[service.id] || 'unknown';
|
||||
const statusClass = status === 'online' ? 'online' : (status === 'offline' ? 'offline' : '');
|
||||
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}"></span>
|
||||
<span class="card-status ${statusClass}">${statusIcon}</span>
|
||||
</div>
|
||||
<div class="card-name">${service.name}</div>
|
||||
<div class="card-desc">${service.description || ''}</div>
|
||||
@@ -331,5 +365,12 @@
|
||||
setInterval(() => {
|
||||
loadServices();
|
||||
}, 30000);
|
||||
|
||||
// 页面显示时刷新
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (!document.hidden) {
|
||||
loadServices();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user