feat: redesign admin interface

This commit is contained in:
2026-09-15 06:41:26 +08:00
parent 32daa27712
commit 68e3d0bfce
6 changed files with 73 additions and 896 deletions
+9 -175
View File
@@ -1,176 +1,10 @@
<!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>
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Categories - ToNav</title><link rel="stylesheet" href="/static/css/admin.css"><script>document.documentElement.classList.toggle('dark',localStorage.getItem('tonav-theme')==='dark')</script></head><body><div class="admin-shell"><aside class="admin-sidebar"><div class="admin-brand">ToNav</div><div class="admin-label">OVERVIEW</div><a class="admin-nav" href="/admin/dashboard">Overview</a><div class="admin-label" style="margin-top:22px">SERVICES</div><a class="admin-nav" href="/admin/services">Services</a><a class="admin-nav active" href="/admin/categories">Categories</a><div class="admin-label" style="margin-top:22px">SYSTEM</div><a class="admin-nav" href="/admin/dashboard#settings">Settings</a><div class="admin-sidebar-foot"><a href="/">← Open navigation</a><br><a href="/admin/logout">Sign out</a></div></aside><main class="admin-content"><div class="admin-main"><header class="admin-topbar"><div><h1>Categories</h1><div class="admin-sub">Organize services into groups</div></div><div class="top-actions"><a class="btn" href="/admin/services">Services</a><button class="btn btn-primary" onclick="openModal()">+ Add category</button></div></header><section class="section"><div class="panel" id="list"><div class="empty-state">Loading...</div></div></section></div></main></div>
<div id="modal" class="modal-overlay"><div class="modal-content"><div class="modal-head"><h2 id="modalTitle">Add category</h2><button class="close" onclick="closeModal()">×</button></div><input type="hidden" id="editId"><div class="form-group"><label>Name</label><input id="catName" class="input" autofocus></div><div class="form-group"><label>Sort order</label><input id="catOrder" type="number" class="input" value="0"></div><div class="modal-actions"><button class="btn" onclick="closeModal()">Cancel</button><button class="btn btn-primary" onclick="save()">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>
async function load(){const r=await fetch('/admin/api/categories'),d=await r.json();if(!d.data?.length){list.innerHTML='<div class="empty-state">No categories yet.</div>';return}list.innerHTML=d.data.map(c=>`<div style="display:flex;align-items:center;justify-content:space-between;padding:14px;border-bottom:1px solid var(--line)"><div><strong>${escapeHTML(c.name)}</strong><span class="tag" style="margin-left:9px">order ${c.sort_order}</span></div><div><button class="btn" onclick='edit(${JSON.stringify(c)})'>Edit</button> <button class="btn btn-danger" onclick="del(${c.id},'${escapeHTML(c.name)}')">Delete</button></div></div>`).join('')}
function openModal(id,name,order){editId.value=id||'';catName.value=name||'';catOrder.value=order||0;modalTitle.textContent=id?'Edit category':'Add category';modal.style.display='flex';catName.focus()}
function edit(c){openModal(c.id,c.name,c.sort_order)}function closeModal(){modal.style.display='none'}
async function save(){const id=editId.value,name=catName.value.trim();if(!name)return alert('Name is required');const body={name,sort_order:+catOrder.value||0};if(id)body.id=+id;const r=await fetch(id?'/admin/api/categories/'+id:'/admin/api/categories',{method:id?'PUT':'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}),d=await r.json();if(d.success){closeModal();load()}else alert(d.message||'Save failed')}
async function del(id,name){if(!confirm('Delete '+name+'?'))return;const r=await fetch('/admin/api/categories/'+id,{method:'DELETE'}),d=await r.json();if(d.success)load();else alert(d.message||'Delete failed')}
function escapeHTML(s){const d=document.createElement('div');d.textContent=s||'';return d.innerHTML}document.addEventListener('keydown',e=>{if(e.key==='Escape')closeModal()});load();
</script></body></html>