功能: - 前台导航: 分类Tab切换、实时搜索、健康状态指示、响应式适配 - 后台管理: 服务/分类CRUD、系统设置、登录认证(bcrypt) - 健康检查: 定时检测(5min)、独立检查URL、三态指示(在线/离线/未检测) - 云端备份: WebDAV上传/下载/恢复/删除、定时自动备份、本地备份管理 技术栈: Go + Gin + GORM + SQLite
177 lines
7.8 KiB
HTML
177 lines
7.8 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; padding: 20px; }
|
|
a { text-decoration: none; color: inherit; }
|
|
.container { max-width: 900px; margin: 0 auto; }
|
|
.header { display: flex; justify-content: space-between; align-items: center; background: #fff; padding: 20px 25px; border-radius: 15px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
|
|
.btn { padding: 10px 20px; border-radius: 8px; border: none; cursor: pointer; font-weight: bold; display: inline-block; font-size: 14px; transition: all .2s; }
|
|
.btn:hover { transform: translateY(-1px); }
|
|
.btn-primary { background: var(--main-red); color: #fff; }
|
|
.btn-back { background: #eee; color: #333; }
|
|
.header-actions { display: flex; gap: 10px; }
|
|
.list-card { background: #fff; border-radius: 15px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
|
|
.item { padding: 15px; border-bottom: 1px solid #f0f0f0; display: flex; justify-content: space-between; align-items: center; transition: background .15s; }
|
|
.item:hover { background: #fafafa; }
|
|
.item:last-child { border-bottom: none; }
|
|
.item-info { display: flex; align-items: center; gap: 12px; }
|
|
.item-order { background: #f0f0f0; border-radius: 6px; padding: 2px 8px; font-size: 12px; color: #999; }
|
|
.item-name { font-weight: 600; font-size: 16px; }
|
|
.item-actions { display: flex; gap: 8px; }
|
|
.item-actions button { background: none; border: none; cursor: pointer; padding: 5px 10px; border-radius: 6px; font-size: 13px; transition: background .15s; }
|
|
.btn-edit { color: var(--primary); }
|
|
.btn-edit:hover { background: rgba(102,126,234,0.1); }
|
|
.btn-del { color: var(--main-red); }
|
|
.btn-del:hover { background: rgba(255,77,79,0.1); }
|
|
.empty { text-align: center; padding: 40px; color: #999; }
|
|
|
|
/* 弹窗 */
|
|
.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: 450px; }
|
|
.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-actions { display: flex; gap: 10px; margin-top: 15px; }
|
|
.modal-actions .btn { flex: 1; }
|
|
|
|
@media (max-width: 640px) {
|
|
body { padding: 12px; }
|
|
.header { flex-direction: column; gap: 12px; text-align: center; padding: 15px; }
|
|
.header h1 { font-size: 20px; }
|
|
.header-actions { width: 100%; }
|
|
.header-actions .btn { flex: 1; text-align: center; }
|
|
.item { flex-direction: column; align-items: flex-start; gap: 10px; padding: 12px; }
|
|
.item-actions { align-self: flex-end; }
|
|
.modal-content { padding: 20px; border-radius: 16px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>📂 分类管理</h1>
|
|
<div class="header-actions">
|
|
<button class="btn btn-primary" onclick="openModal()">+ 新增分类</button>
|
|
<a href="/admin/dashboard" class="btn btn-back">返回</a>
|
|
</div>
|
|
</div>
|
|
<div class="list-card" id="list">
|
|
<div class="empty">加载中...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 新增/编辑弹窗 -->
|
|
<div id="modal" class="modal-overlay">
|
|
<div class="modal-content">
|
|
<h2 id="modalTitle" style="margin-bottom:20px">新增分类</h2>
|
|
<input type="hidden" id="editId">
|
|
<label style="font-size:13px; color:#666; margin-bottom:4px; display:block">分类名称</label>
|
|
<input type="text" id="catName" class="input" placeholder="如:内网服务" autofocus>
|
|
<label style="font-size:13px; color:#666; margin-bottom:4px; display:block">排序(数字越大越靠前)</label>
|
|
<input type="number" id="catOrder" class="input" placeholder="0" value="0">
|
|
<div class="modal-actions">
|
|
<button class="btn" style="background:#eee" onclick="closeModal()">取消</button>
|
|
<button class="btn btn-primary" onclick="save()">保存</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
async function load() {
|
|
const res = await fetch('/admin/api/categories');
|
|
const data = await res.json();
|
|
const list = document.getElementById('list');
|
|
if (!data.data || data.data.length === 0) {
|
|
list.innerHTML = '<div class="empty">暂无分类,点击上方"+ 新增分类"添加</div>';
|
|
return;
|
|
}
|
|
list.innerHTML = data.data.map(c => `
|
|
<div class="item">
|
|
<div class="item-info">
|
|
<span class="item-order">${c.sort_order}</span>
|
|
<span class="item-name">${escapeHTML(c.name)}</span>
|
|
</div>
|
|
<div class="item-actions">
|
|
<button class="btn-edit" onclick='edit(${JSON.stringify(c)})'>编辑</button>
|
|
<button class="btn-del" onclick="del(${c.id}, '${escapeHTML(c.name)}')">删除</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function openModal(id, name, order) {
|
|
document.getElementById('editId').value = id || '';
|
|
document.getElementById('catName').value = name || '';
|
|
document.getElementById('catOrder').value = order || 0;
|
|
document.getElementById('modalTitle').textContent = id ? '编辑分类' : '新增分类';
|
|
document.getElementById('modal').style.display = 'flex';
|
|
document.getElementById('catName').focus();
|
|
}
|
|
|
|
function closeModal() {
|
|
document.getElementById('modal').style.display = 'none';
|
|
}
|
|
|
|
function edit(c) {
|
|
openModal(c.id, c.name, c.sort_order);
|
|
}
|
|
|
|
async function save() {
|
|
const id = document.getElementById('editId').value;
|
|
const name = document.getElementById('catName').value.trim();
|
|
const order = parseInt(document.getElementById('catOrder').value) || 0;
|
|
|
|
if (!name) { alert('分类名称不能为空'); return; }
|
|
|
|
const body = { name, sort_order: order };
|
|
if (id) body.id = parseInt(id);
|
|
|
|
const url = id ? `/admin/api/categories/${id}` : '/admin/api/categories';
|
|
const method = id ? 'PUT' : 'POST';
|
|
|
|
const resp = await fetch(url, {
|
|
method,
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(body)
|
|
});
|
|
const res = await resp.json();
|
|
if (res.success) {
|
|
closeModal();
|
|
load();
|
|
} else {
|
|
alert(res.message || '保存失败');
|
|
}
|
|
}
|
|
|
|
async function del(id, name) {
|
|
if (!confirm(`确定删除分类「${name}」?`)) return;
|
|
const resp = await fetch('/admin/api/categories/' + id, { method: 'DELETE' });
|
|
const res = await resp.json();
|
|
if (res.success) {
|
|
load();
|
|
} else {
|
|
alert(res.message || '删除失败');
|
|
}
|
|
}
|
|
|
|
function escapeHTML(str) {
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// ESC 关闭弹窗
|
|
document.addEventListener('keydown', e => {
|
|
if (e.key === 'Escape') closeModal();
|
|
});
|
|
|
|
load();
|
|
</script>
|
|
</body>
|
|
</html>
|