feat: redesign admin interface
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user