feat: ToNav-go v1.0.0 - 内部服务导航系统
功能: - 前台导航: 分类Tab切换、实时搜索、健康状态指示、响应式适配 - 后台管理: 服务/分类CRUD、系统设置、登录认证(bcrypt) - 健康检查: 定时检测(5min)、独立检查URL、三态指示(在线/离线/未检测) - 云端备份: WebDAV上传/下载/恢复/删除、定时自动备份、本地备份管理 技术栈: Go + Gin + GORM + SQLite
This commit is contained in:
176
templates/admin/categories.html
Normal file
176
templates/admin/categories.html
Normal file
@@ -0,0 +1,176 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user