功能: - 前台导航: 分类Tab切换、实时搜索、健康状态指示、响应式适配 - 后台管理: 服务/分类CRUD、系统设置、登录认证(bcrypt) - 健康检查: 定时检测(5min)、独立检查URL、三态指示(在线/离线/未检测) - 云端备份: WebDAV上传/下载/恢复/删除、定时自动备份、本地备份管理 技术栈: Go + Gin + GORM + SQLite
261 lines
13 KiB
HTML
261 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>ToNav 管理后台</title>
|
|
<style>
|
|
:root { --main-red: #ff4d4f; --primary: #667eea; }
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: "PingFang SC", -apple-system, sans-serif; background: #f0f2f5; min-height: 100vh; padding: 20px; }
|
|
a { text-decoration: none; color: inherit; }
|
|
.container { max-width: 900px; margin: 0 auto; }
|
|
.admin-header { display: flex; justify-content: space-between; align-items: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; padding: 25px 30px; border-radius: 20px; margin-bottom: 25px; }
|
|
.header-actions { display: flex; gap: 10px; }
|
|
.btn { padding: 10px 20px; border-radius: 10px; border: none; cursor: pointer; font-weight: 600; display: inline-block; font-size: 14px; transition: all .2s; }
|
|
.btn:hover { transform: translateY(-1px); }
|
|
.btn-light { background: rgba(255,255,255,0.2); color: #fff; }
|
|
.btn-light:hover { background: rgba(255,255,255,0.3); }
|
|
.btn-primary { background: var(--main-red); color: #fff; }
|
|
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px; margin-bottom: 25px; }
|
|
.stat-card { background: #fff; border-radius: 16px; padding: 25px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.06); transition: transform .2s; }
|
|
.stat-card:hover { transform: translateY(-3px); }
|
|
.stat-value { font-size: 36px; font-weight: 700; color: #262626; }
|
|
.stat-label { font-size: 14px; color: #8c8c8c; margin-top: 5px; }
|
|
.stat-online .stat-value { color: #52c41a; }
|
|
.stat-offline .stat-value { color: #ff4d4f; }
|
|
.quick-actions { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px; }
|
|
.action-btn { background: #fff; border-radius: 16px; padding: 30px 20px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.06); cursor: pointer; transition: all .2s; font-size: 15px; font-weight: 500; border: none; }
|
|
.action-btn:hover { transform: translateY(-3px); box-shadow: 0 6px 20px rgba(0,0,0,0.12); }
|
|
.input { width: 100%; padding: 10px 12px; border: 1px solid #d9d9d9; border-radius: 8px; box-sizing: border-box; margin-bottom: 12px; font-size: 14px; }
|
|
.input:focus { border-color: var(--primary); outline: none; box-shadow: 0 0 0 2px rgba(102,126,234,0.2); }
|
|
|
|
/* 弹窗 */
|
|
.modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); align-items: center; justify-content: center; z-index: 100; }
|
|
.modal-content { background: #fff; padding: 30px; border-radius: 20px; width: 90%; max-width: 600px; max-height: 80vh; overflow-y: auto; }
|
|
|
|
@media (max-width: 640px) {
|
|
body { padding: 12px; }
|
|
.admin-header { flex-direction: column; gap: 15px; text-align: center; padding: 20px; }
|
|
.admin-header h1 { font-size: 20px; }
|
|
.header-actions { flex-wrap: wrap; justify-content: center; }
|
|
.header-actions .btn { padding: 8px 14px; font-size: 13px; }
|
|
.stats-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; }
|
|
.stat-card { padding: 18px 10px; }
|
|
.stat-value { font-size: 28px; }
|
|
.stat-label { font-size: 12px; }
|
|
.quick-actions { grid-template-columns: repeat(2, 1fr); gap: 10px; }
|
|
.action-btn { padding: 20px 10px; font-size: 14px; }
|
|
.modal-content { padding: 20px; border-radius: 16px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="admin-header">
|
|
<h1>🧭 ToNav 管理后台</h1>
|
|
<div class="header-actions">
|
|
<a href="/" class="btn btn-light">查看前台</a>
|
|
<a href="/admin/change-password" class="btn btn-light">修改密码</a>
|
|
<a href="/admin/logout" class="btn btn-primary">退出登录</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-value">{{ .service_count }}</div>
|
|
<div class="stat-label">总服务数</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{{ .category_count }}</div>
|
|
<div class="stat-label">分类数</div>
|
|
</div>
|
|
<div class="stat-card stat-online">
|
|
<div class="stat-value">{{ .online_count }}</div>
|
|
<div class="stat-label">在线服务</div>
|
|
</div>
|
|
<div class="stat-card stat-offline">
|
|
<div class="stat-value">{{ .offline_count }}</div>
|
|
<div class="stat-label">离线服务</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="quick-actions">
|
|
<a href="/admin/services" class="action-btn">
|
|
<div style="font-size:32px; margin-bottom:10px">📡</div>
|
|
<div>服务管理</div>
|
|
</a>
|
|
<a href="/admin/categories" class="action-btn">
|
|
<div style="font-size:32px; margin-bottom:10px">📂</div>
|
|
<div>分类管理</div>
|
|
</a>
|
|
<div class="action-btn" onclick="runBackup()">
|
|
<div style="font-size:32px; margin-bottom:10px">☁️</div>
|
|
<div>立即云备份</div>
|
|
</div>
|
|
<div class="action-btn" onclick="showSettings()">
|
|
<div style="font-size:32px; margin-bottom:10px">⚙️</div>
|
|
<div>系统设置</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 系统设置弹窗 -->
|
|
<div id="settingsModal" class="modal-overlay">
|
|
<div class="modal-content">
|
|
<h2 style="margin-bottom:20px">⚙️ 系统设置 & 云端备份</h2>
|
|
|
|
<div style="margin-bottom:20px; padding:15px; background:#f9f9f9; border-radius:10px">
|
|
<h3 style="font-size:14px; margin-bottom:10px">站点配置</h3>
|
|
<label style="font-size:12px; color:#666">站点标题</label>
|
|
<input type="text" id="cfg_site_title" class="input" placeholder="ToNav">
|
|
</div>
|
|
|
|
<div style="margin-bottom:20px; padding:15px; background:#f9f9f9; border-radius:10px">
|
|
<h3 style="font-size:14px; margin-bottom:10px">WebDAV 配置</h3>
|
|
<label style="font-size:12px; color:#666">WebDAV URL</label>
|
|
<input type="text" id="cfg_url" class="input">
|
|
<label style="font-size:12px; color:#666">用户名</label>
|
|
<input type="text" id="cfg_user" class="input">
|
|
<label style="font-size:12px; color:#666">密码</label>
|
|
<input type="password" id="cfg_pass" class="input">
|
|
</div>
|
|
|
|
<div style="margin-bottom:20px; padding:15px; background:#f9f9f9; border-radius:10px">
|
|
<h3 style="font-size:14px; margin-bottom:10px">自动备份</h3>
|
|
<div style="display:flex; align-items:center; gap:8px">
|
|
<input type="checkbox" id="cfg_auto_backup" style="width:16px; height:16px">
|
|
<label for="cfg_auto_backup" style="margin:0; font-size:13px">每天凌晨 3:00 自动备份到云端</label>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="btn btn-primary" onclick="saveSettings()" style="width:100%; margin-bottom:15px">保存设置</button>
|
|
|
|
<div style="padding:15px; border:1px solid #eee; border-radius:10px">
|
|
<h3 style="font-size:14px; margin-bottom:10px">📜 云端备份历史</h3>
|
|
<div id="backupList" style="font-size:13px; color:#666">正在获取列表...</div>
|
|
</div>
|
|
|
|
<button class="btn" onclick="document.getElementById('settingsModal').style.display='none'" style="background:#eee; width:100%; margin-top:15px">关闭</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
async function showSettings() {
|
|
const resp = await fetch('/admin/api/settings');
|
|
const data = await resp.json();
|
|
document.getElementById('cfg_site_title').value = data.site_title || '';
|
|
document.getElementById('cfg_url').value = data.webdav_url || '';
|
|
document.getElementById('cfg_user').value = data.webdav_user || '';
|
|
document.getElementById('cfg_pass').value = data.webdav_password || '';
|
|
document.getElementById('cfg_auto_backup').checked = data.auto_backup === 'true';
|
|
document.getElementById('settingsModal').style.display = 'flex';
|
|
loadCloudBackups();
|
|
}
|
|
|
|
async function loadCloudBackups() {
|
|
const listDiv = document.getElementById('backupList');
|
|
try {
|
|
const resp = await fetch('/admin/api/backup/list');
|
|
const data = await resp.json();
|
|
if (data.files && data.files.length > 0) {
|
|
listDiv.innerHTML = data.files.map(f => `
|
|
<div style="padding:10px 0; border-bottom:1px solid #f0f0f0">
|
|
<div style="display:flex; justify-content:space-between; align-items:center">
|
|
<div>
|
|
<div style="font-weight:500; color:#333">${f.name}</div>
|
|
<div style="font-size:11px; color:#999; margin-top:2px">
|
|
${formatTime(f.mod_time)}${f.size ? ' · ' + formatSize(f.size) : ''}
|
|
</div>
|
|
</div>
|
|
<div style="display:flex; gap:6px">
|
|
<button onclick="restoreBackup('${f.name}')" style="background:none; border:1px solid #1890ff; color:#1890ff; padding:4px 10px; border-radius:6px; cursor:pointer; font-size:12px">恢复</button>
|
|
<button onclick="deleteBackup('${f.name}')" style="background:none; border:1px solid #ff4d4f; color:#ff4d4f; padding:4px 10px; border-radius:6px; cursor:pointer; font-size:12px">删除</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
} else {
|
|
listDiv.innerHTML = '暂无云端备份';
|
|
}
|
|
} catch(e) { listDiv.innerHTML = '获取失败'; }
|
|
}
|
|
|
|
function formatSize(bytes) {
|
|
if (!bytes || bytes === 0) return '';
|
|
if (bytes < 1024) return bytes + ' B';
|
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
return (bytes / 1024 / 1024).toFixed(1) + ' MB';
|
|
}
|
|
|
|
function formatTime(str) {
|
|
if (!str) return '';
|
|
try {
|
|
const d = new Date(str);
|
|
if (isNaN(d.getTime())) return str;
|
|
return d.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', year:'numeric', month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit', second:'2-digit' });
|
|
} catch(e) { return str; }
|
|
}
|
|
|
|
async function saveSettings() {
|
|
const data = {
|
|
site_title: document.getElementById('cfg_site_title').value,
|
|
webdav_url: document.getElementById('cfg_url').value,
|
|
webdav_user: document.getElementById('cfg_user').value,
|
|
webdav_password: document.getElementById('cfg_pass').value,
|
|
auto_backup: document.getElementById('cfg_auto_backup').checked ? 'true' : 'false'
|
|
};
|
|
const resp = await fetch('/admin/api/settings', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(data)
|
|
});
|
|
const res = await resp.json();
|
|
alert(res.message || '保存成功');
|
|
}
|
|
|
|
async function runBackup() {
|
|
if (!confirm('确定执行云端备份?')) return;
|
|
const btn = event.currentTarget;
|
|
const oldContent = btn.innerHTML;
|
|
btn.innerHTML = '<div style="font-size:32px; margin-bottom:10px">⌛</div><div>正在备份...</div>';
|
|
btn.style.pointerEvents = 'none';
|
|
try {
|
|
const resp = await fetch('/admin/api/backup/webdav', {method: 'POST'});
|
|
const res = await resp.json();
|
|
alert(res.message);
|
|
} catch(e) { alert('备份失败'); }
|
|
btn.innerHTML = oldContent;
|
|
btn.style.pointerEvents = '';
|
|
}
|
|
|
|
async function restoreBackup(name) {
|
|
if (!confirm(`⚠️ 确定从「${name}」恢复?\n\n当前数据库会先自动备份,然后被覆盖为所选版本。`)) return;
|
|
try {
|
|
const resp = await fetch(`/admin/api/backup/restore?name=${encodeURIComponent(name)}`, {method: 'POST'});
|
|
const res = await resp.json();
|
|
if (res.success) {
|
|
alert('✅ ' + res.message + '\n\n页面将刷新...');
|
|
location.reload();
|
|
} else {
|
|
alert('❌ ' + res.message);
|
|
}
|
|
} catch(e) { alert('恢复失败'); }
|
|
}
|
|
|
|
async function deleteBackup(name) {
|
|
if (!confirm(`确定删除云端备份「${name}」?此操作不可撤销。`)) return;
|
|
try {
|
|
const resp = await fetch(`/admin/api/backup/delete?name=${encodeURIComponent(name)}`, {method: 'DELETE'});
|
|
const res = await resp.json();
|
|
if (res.success) {
|
|
loadCloudBackups();
|
|
} else {
|
|
alert(res.message);
|
|
}
|
|
} catch(e) { alert('删除失败'); }
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|