Single-container navigation service combining: - Dashy dashboard + SearXNG metasearch + history sync API + admin WebUI - Go services for history/admin APIs, nginx as unified entry - Browser extensions (Chrome MV3 / Firefox MV2) with 30s batched sync - Production guide: portal-nginx unified auth reverse proxy + webui4frpc/frp tunnel - Fully sanitized (no internal IPs/domains/credentials)
63 lines
4.8 KiB
HTML
63 lines
4.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>导航管理</title>
|
|
<style>
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{font-family:system-ui,-apple-system,sans-serif;background:#f0f9ff;min-height:100vh;padding:24px}
|
|
.wrap{max-width:800px;margin:0 auto}
|
|
h1{font-size:22px;font-weight:800;color:#0f172a;margin-bottom:16px}
|
|
.card{background:rgba(255,255,255,.9);border-radius:14px;padding:18px;margin-bottom:14px;border:1px solid rgba(0,0,0,.06);box-shadow:0 2px 8px rgba(0,0,0,.04)}
|
|
.sec-hd{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}
|
|
.sec-nm{font-weight:700;color:#0f172a}
|
|
.badge{background:#0ea5e9;color:#fff;font-size:11px;padding:2px 7px;border-radius:9px;font-weight:600}
|
|
.itm{display:flex;align-items:center;gap:10px;padding:8px 12px;border-radius:10px;margin-bottom:4px;background:rgba(255,255,255,.5);transition:.15s}
|
|
.itm:hover{background:rgba(255,255,255,.85)}
|
|
.itm-i{flex:1;min-width:0}
|
|
.itm-t{font-weight:600;color:#1e293b;font-size:13px}
|
|
.itm-u{color:#64748b;font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
.btn-x{background:none;border:none;color:#ef4444;cursor:pointer;font-size:16px;padding:2px 6px;border-radius:6px}
|
|
.btn-x:hover{background:rgba(239,68,68,.1)}
|
|
.frm{background:rgba(255,255,255,.9);border-radius:14px;padding:18px;margin-top:18px;border:1px solid rgba(0,0,0,.06)}
|
|
.frm h2{font-size:16px;font-weight:700;margin-bottom:12px}
|
|
.row{display:flex;gap:10px;margin-bottom:10px;flex-wrap:wrap}
|
|
.fg{display:flex;flex-direction:column;gap:3px;flex:1;min-width:180px}
|
|
.fg label{font-size:12px;font-weight:600;color:#334155}
|
|
.fg input,.fg select{padding:8px 12px;border-radius:10px;border:1px solid rgba(0,0,0,.1);font-size:13px}
|
|
.btn-add{background:linear-gradient(135deg,#0ea5e9,#38bdf8);color:#fff;border:none;padding:10px 24px;border-radius:10px;font-weight:700;cursor:pointer;margin-top:6px}
|
|
.toast{position:fixed;top:16px;right:16px;background:#22c55e;color:#fff;padding:10px 16px;border-radius:10px;font-weight:600;z-index:9999;animation:sl .3s}
|
|
@keyframes sl{from{transform:translateX(100%);opacity:0}to{transform:none;opacity:1}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
<h1> 导航管理</h1>
|
|
<div id="app"><p style="color:#64748b;text-align:center;padding:32px">加载中…</p></div>
|
|
<div class="frm">
|
|
<h2> 添加导航项</h2>
|
|
<div class="row"><div class="fg"><label>所属分组</label><select id="sel"></select></div></div>
|
|
<div class="row">
|
|
<div class="fg"><label>名称 *</label><input id="ti" placeholder="Gitea" required></div>
|
|
<div class="fg"><label>描述</label><input id="de" placeholder="代码仓库"></div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="fg"><label>URL *</label><input id="ur" placeholder="https://example.com" required></div>
|
|
<div class="fg" style="max-width:180px"><label>图标</label><input id="ic" placeholder="fas fa-star"></div>
|
|
</div>
|
|
<button class="btn-add" onclick="add()">添加</button>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
let S=[];const A='/admin/api';
|
|
async function L(){const r=await fetch(A+'/sections');S=await r.json();R();document.getElementById('sel').innerHTML=S.map((s,i)=>`<option value="${i}">${s.name}</option>`).join('')}
|
|
function R(){document.getElementById('app').innerHTML=S.map((s,si)=>`<div class="card"><div class="sec-hd"><span class="sec-nm">${s.name}</span><span class="badge">${s.items.length}</span></div>${s.items.map((it,ii)=>`<div class="itm"><div class="itm-i"><div class="itm-t">${it.title}</div><div class="itm-u">${it.url}</div></div><button class="btn-x" onclick="D(${si},${ii})" title="删除">✕</button></div>`).join('')}</div>`).join('')}
|
|
async function add(){const t=document.getElementById('ti').value.trim(),u=document.getElementById('ur').value.trim();if(!t||!u){toast('请填写名称和URL','#ef4444');return}const si=+document.getElementById('sel').value;const r=await fetch(A+'/item',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sectionIndex:si,item:{title:t,description:document.getElementById('de').value.trim(),icon:document.getElementById('ic').value.trim(),url:u}})});if(r.ok){toast('已添加');['ti','de','ur','ic'].forEach(id=>document.getElementById(id).value='');L()}else toast('添加失败','#ef4444')}
|
|
async function D(si,ii){if(!confirm('确定删除「'+S[si].items[ii].title+'」?'))return;const r=await fetch(A+'/item/delete',{method:'DELETE',headers:{'Content-Type':'application/json'},body:JSON.stringify({sectionIndex:si,itemIndex:ii})});if(r.ok){toast('已删除');L()}}
|
|
function toast(m,b='#22c55e'){const d=document.createElement('div');d.className='toast';d.textContent=m;d.style.background=b;document.body.appendChild(d);setTimeout(()=>d.remove(),2500)}
|
|
L()
|
|
</script>
|
|
</body>
|
|
</html>
|