fix: refine site terminology, favicon and translations

This commit is contained in:
2026-09-15 07:21:50 +08:00
parent ad6d817f19
commit 2464222c94
8 changed files with 31 additions and 30 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
<!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>
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><link rel="icon" href="/static/favicon.svg?v=2" type="image/svg+xml"><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">SITES</div><a class="admin-nav" href="/admin/services">Sites</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 sites into groups</div></div><div class="top-actions"><a class="btn" href="/admin/services">Sites</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 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('')}
@@ -7,4 +7,4 @@ function edit(c){openModal(c.id,c.name,c.sort_order)}function closeModal(){modal
async function save(){const id=editId.value,name=catName.value.trim();if(!name)return alert(t('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||t('Save failed'))}
async function del(id,name){if(!confirm(t('Delete ')+name+t('?')))return;const r=await fetch('/admin/api/categories/'+id,{method:'DELETE'}),d=await r.json();if(d.success)load();else alert(d.message||t('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><script src="/static/js/i18n.js?v=2"></script></body></html>
</script><script src="/static/js/i18n.js?v=3"></script></body></html>
+1 -1
View File
@@ -1 +1 @@
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Change password - 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="login-shell"><div class="login-box"><div class="login-brand">Change password</div><div class="login-sub">Update your administrator credentials</div><div class="alert">For security, the initial password must be changed before continuing.</div>{{ if .error }}<div class="error-box">{{ .error }}</div>{{ end }}<form method="POST"><div class="form-group"><label>Current password</label><input class="input" type="password" name="old_password" required autocomplete="current-password"></div><div class="form-group"><label>New password</label><input class="input" type="password" name="new_password" minlength="6" required autocomplete="new-password"></div><div class="form-group"><label>Confirm new password</label><input class="input" type="password" name="confirm_password" minlength="6" required autocomplete="new-password"></div><button class="btn btn-primary" type="submit">Update password</button></form></div></div><script src="/static/js/i18n.js?v=2"></script></body></html>
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><link rel="icon" href="/static/favicon.svg?v=2" type="image/svg+xml"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Change password - 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="login-shell"><div class="login-box"><div class="login-brand">Change password</div><div class="login-sub">Update your administrator credentials</div><div class="alert">For security, the initial password must be changed before continuing.</div>{{ if .error }}<div class="error-box">{{ .error }}</div>{{ end }}<form method="POST"><div class="form-group"><label>Current password</label><input class="input" type="password" name="old_password" required autocomplete="current-password"></div><div class="form-group"><label>New password</label><input class="input" type="password" name="new_password" minlength="6" required autocomplete="new-password"></div><div class="form-group"><label>Confirm new password</label><input class="input" type="password" name="confirm_password" minlength="6" required autocomplete="new-password"></div><button class="btn btn-primary" type="submit">Update password</button></form></div></div><script src="/static/js/i18n.js?v=3"></script></body></html>
+7 -7
View File
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta charset="UTF-8"><link rel="icon" href="/static/favicon.svg?v=2" type="image/svg+xml"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Overview - ToNav</title><link rel="stylesheet" href="/static/css/admin.css">
<script>document.documentElement.classList.toggle('dark',localStorage.getItem('tonav-theme')==='dark');</script>
</head>
@@ -11,19 +11,19 @@
<div class="admin-brand">ToNav</div>
<div class="admin-label">OVERVIEW</div>
<a class="admin-nav active" href="/admin/dashboard">Overview</a>
<div class="admin-label" style="margin-top:22px">SERVICES</div>
<a class="admin-nav" href="/admin/services">Services</a>
<div class="admin-label" style="margin-top:22px">SITES</div>
<a class="admin-nav" href="/admin/services">Sites</a>
<a class="admin-nav" href="/admin/categories">Categories</a>
<div class="admin-label" style="margin-top:22px">SYSTEM</div>
<a class="admin-nav" href="#settings" onclick="showSettings();return false">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>Overview</h1><div class="admin-sub">Service directory and system status</div></div><div class="top-actions"><button class="icon-button" onclick="toggleTheme()" title="Toggle theme" aria-label="Toggle theme">◐</button><a class="btn" href="/">Open site</a></div></header>
<header class="admin-topbar"><div><h1>Overview</h1><div class="admin-sub">Site directory and system status</div></div><div class="top-actions"><button class="icon-button" onclick="toggleTheme()" title="Toggle theme" aria-label="Toggle theme">◐</button><a class="btn" href="/">Open site</a></div></header>
<section class="section"><div class="section-title">System status</div>
<div class="metric-strip"><div class="metric"><div class="metric-value">{{ .service_count }}</div><div class="metric-label">Services</div></div><div class="metric"><div class="metric-value">{{ .category_count }}</div><div class="metric-label">Categories</div></div><div class="metric"><div class="metric-value green">{{ .online_count }}</div><div class="metric-label">Online</div></div><div class="metric"><div class="metric-value red">{{ .offline_count }}</div><div class="metric-label">Offline</div></div></div></section>
<div class="metric-strip"><div class="metric"><div class="metric-value">{{ .service_count }}</div><div class="metric-label">Sites</div></div><div class="metric"><div class="metric-value">{{ .category_count }}</div><div class="metric-label">Categories</div></div><div class="metric"><div class="metric-value green">{{ .online_count }}</div><div class="metric-label">Online</div></div><div class="metric"><div class="metric-value red">{{ .offline_count }}</div><div class="metric-label">Offline</div></div></div></section>
<section class="section"><div class="section-title">Quick actions</div><div class="panel action-list">
<a class="action" href="/admin/services"><strong>Services</strong><span>Manage service entries</span></a>
<a class="action" href="/admin/services"><strong>Sites</strong><span>Manage site entries</span></a>
<a class="action" href="/admin/categories"><strong>Categories</strong><span>Organize your directory</span></a>
<a class="action" href="#settings" onclick="showSettings();return false"><strong>Settings</strong><span>Site, backup and MCP</span></a>
<div class="action" onclick="runBackup()"><strong>Backup</strong><span>Run cloud backup now</span></div>
@@ -55,4 +55,4 @@ async function runBackup(){if(!confirm(t('Run cloud backup now?')))return;const
async function restoreBackup(name){name=decodeURIComponent(name);if(!confirm(t('Restore ')+name+t('? Current database will be backed up first.')))return;const r=await fetch('/admin/api/backup/restore?name='+encodeURIComponent(name),{method:'POST'}),d=await r.json();alert(d.message||t('Done'));if(d.success)location.reload()}
async function deleteBackup(name){name=decodeURIComponent(name);if(!confirm(t('Delete ')+name+t('?')))return;const r=await fetch('/admin/api/backup/delete?name='+encodeURIComponent(name),{method:'DELETE'}),d=await r.json();if(d.success)loadCloudBackups();else alert(d.message)}
document.addEventListener('keydown',e=>{if(e.key==='Escape')closeSettings()});
</script><script src="/static/js/i18n.js?v=2"></script></body></html>
</script><script src="/static/js/i18n.js?v=3"></script></body></html>
+1 -1
View File
@@ -1 +1 @@
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Sign in - 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="login-shell"><div class="login-box"><div class="login-brand">ToNav</div><div class="login-sub">Administration</div>{{ if .error }}<div class="error-box">{{ .error }}</div>{{ end }}<form method="POST"><div class="form-group"><label>Username</label><input class="input" type="text" name="username" required autofocus autocomplete="username"></div><div class="form-group"><label>Password</label><input class="input" type="password" name="password" required autocomplete="current-password"></div><button class="btn btn-primary" type="submit">Sign in</button></form></div></div><script src="/static/js/i18n.js?v=2"></script></body></html>
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><link rel="icon" href="/static/favicon.svg?v=2" type="image/svg+xml"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Sign in - 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="login-shell"><div class="login-box"><div class="login-brand">ToNav</div><div class="login-sub">Administration</div>{{ if .error }}<div class="error-box">{{ .error }}</div>{{ end }}<form method="POST"><div class="form-group"><label>Username</label><input class="input" type="text" name="username" required autofocus autocomplete="username"></div><div class="form-group"><label>Password</label><input class="input" type="password" name="password" required autocomplete="current-password"></div><button class="btn btn-primary" type="submit">Sign in</button></form></div></div><script src="/static/js/i18n.js?v=3"></script></body></html>
+5 -5
View File
@@ -1,9 +1,9 @@
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Services - 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 active" href="/admin/services">Services</a><a class="admin-nav" 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>Services</h1><div class="admin-sub">Manage entries in your service directory</div></div><div class="top-actions"><a class="btn" href="/">Open site</a><button class="btn btn-primary" onclick="openModal()">+ Add service</button></div></header><section class="section"><div class="table-toolbar"><input id="filter" class="search-input" placeholder="Search services..." oninput="load()"></div><div class="panel table-wrap"><table class="data-table"><thead><tr><th>Service</th><th>Category</th><th>Status</th><th>Order</th><th>Actions</th></tr></thead><tbody id="list"></tbody></table><div id="emptyMsg" class="empty-state" style="display:none">No services found.</div></div></section></div></main></div>
<div id="modal" class="modal-overlay"><div class="modal-content"><div class="modal-head"><h2 id="modalTitle">Add service</h2><button class="close" onclick="closeModal()">×</button></div><input type="hidden" id="editId"><div class="form-group"><label>Name *</label><input id="svcName" class="input"></div><div class="form-group"><label>URL *</label><input id="svcUrl" type="url" class="input" placeholder="https://example.com"></div><div class="form-row"><div class="form-group"><label>Icon</label><input id="svcIcon" class="input" maxlength="8" placeholder="◎"></div><div class="form-group"><label>Category</label><select id="svcCategory" class="input"><option value="0">No category</option>{{ range .categories }}<option value="{{ .ID }}">{{ .Name }}</option>{{ end }}</select></div></div><div class="form-group"><label>Description</label><input id="svcDesc" class="input"></div><div class="form-group"><label>Tags (comma separated)</label><input id="svcTags" class="input"></div><div class="form-row"><div class="form-group"><label>Sort order</label><input id="svcOrder" type="number" class="input" value="0"></div><div class="form-group"><label>Health check URL</label><input id="svcHealthUrl" type="url" class="input"></div></div><div class="form-group"><label><input type="checkbox" id="svcEnabled" checked> Enabled</label></div><div class="form-group"><label><input type="checkbox" id="svcHealthEnabled"> Health check enabled</label></div><div class="modal-actions"><button class="btn" onclick="closeModal()">Cancel</button><button class="btn btn-primary" onclick="save()">Save</button></div></div></div>
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><link rel="icon" href="/static/favicon.svg?v=2" type="image/svg+xml"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Sites - 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">SITES</div><a class="admin-nav active" href="/admin/services">Sites</a><a class="admin-nav" 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>Sites</h1><div class="admin-sub">Manage entries in your site directory</div></div><div class="top-actions"><a class="btn" href="/">Open site</a><button class="btn btn-primary" onclick="openModal()">+ Add site</button></div></header><section class="section"><div class="table-toolbar"><input id="filter" class="search-input" placeholder="Search sites..." oninput="load()"></div><div class="panel table-wrap"><table class="data-table"><thead><tr><th>Site</th><th>Category</th><th>Status</th><th>Order</th><th>Actions</th></tr></thead><tbody id="list"></tbody></table><div id="emptyMsg" class="empty-state" style="display:none">No sites found.</div></div></section></div></main></div>
<div id="modal" class="modal-overlay"><div class="modal-content"><div class="modal-head"><h2 id="modalTitle">Add site</h2><button class="close" onclick="closeModal()">×</button></div><input type="hidden" id="editId"><div class="form-group"><label>Name *</label><input id="svcName" class="input"></div><div class="form-group"><label>URL *</label><input id="svcUrl" type="url" class="input" placeholder="https://example.com"></div><div class="form-row"><div class="form-group"><label>Icon</label><input id="svcIcon" class="input" maxlength="8" placeholder="◎"></div><div class="form-group"><label>Category</label><select id="svcCategory" class="input"><option value="0">No category</option>{{ range .categories }}<option value="{{ .ID }}">{{ .Name }}</option>{{ end }}</select></div></div><div class="form-group"><label>Description</label><input id="svcDesc" class="input"></div><div class="form-group"><label>Tags (comma separated)</label><input id="svcTags" class="input"></div><div class="form-row"><div class="form-group"><label>Sort order</label><input id="svcOrder" type="number" class="input" value="0"></div><div class="form-group"><label>Health check URL</label><input id="svcHealthUrl" type="url" class="input"></div></div><div class="form-group"><label><input type="checkbox" id="svcEnabled" checked> Enabled</label></div><div class="form-group"><label><input type="checkbox" id="svcHealthEnabled"> Health check enabled</label></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>const categoryMap={};{{ range .categories }}categoryMap[{{ .ID }}]={{ printf "%q" .Name }};{{ end }}
async function load(){const r=await fetch('/admin/api/services'),d=await r.json(),q=filter.value.toLowerCase();const rows=(d.data||[]).filter(s=>!q||[s.name,s.description,s.tags,s.url].some(x=>(x||'').toLowerCase().includes(q)));emptyMsg.style.display=rows.length?'none':'block';list.innerHTML=rows.map(s=>`<tr><td><div style="display:flex;align-items:center;gap:10px"><span class="service-icon">${escapeHTML(s.icon||'◎')}</span><span><strong>${escapeHTML(s.name)}</strong><small style="display:block;color:var(--muted)">${escapeHTML(s.url)}</small></span></div></td><td>${escapeHTML(categoryMap[s.category_id]||'—')}</td><td><span class="status-inline"><i class="dot ${s.status}"></i>${s.status==='online'?'Online':s.status==='offline'?'Offline':'Unknown'}</span></td><td>${s.sort_order}</td><td><button class="btn" onclick='editSvc(${JSON.stringify(s)})'>Edit</button> <button class="btn btn-danger" onclick="del(${s.id},'${escapeHTML(s.name)}')">Delete</button></td></tr>`).join('')}
function openModal(){editId.value='';svcName.value='';svcUrl.value='';svcIcon.value='';svcCategory.value='0';svcDesc.value='';svcTags.value='';svcOrder.value='0';svcHealthUrl.value='';svcEnabled.checked=true;svcHealthEnabled.checked=false;modalTitle.textContent=t('Add service');modal.style.display='flex';svcName.focus()}
function editSvc(s){editId.value=s.id;svcName.value=s.name;svcUrl.value=s.url;svcIcon.value=s.icon||'';svcCategory.value=s.category_id||'0';svcDesc.value=s.description||'';svcTags.value=s.tags||'';svcOrder.value=s.sort_order||0;svcHealthUrl.value=s.health_check_url||'';svcEnabled.checked=s.is_enabled;svcHealthEnabled.checked=s.health_check_enabled;modalTitle.textContent=t('Edit service');modal.style.display='flex'}function closeModal(){modal.style.display='none'}
function openModal(){editId.value='';svcName.value='';svcUrl.value='';svcIcon.value='';svcCategory.value='0';svcDesc.value='';svcTags.value='';svcOrder.value='0';svcHealthUrl.value='';svcEnabled.checked=true;svcHealthEnabled.checked=false;modalTitle.textContent=t('Add site');modal.style.display='flex';svcName.focus()}
function editSvc(s){editId.value=s.id;svcName.value=s.name;svcUrl.value=s.url;svcIcon.value=s.icon||'';svcCategory.value=s.category_id||'0';svcDesc.value=s.description||'';svcTags.value=s.tags||'';svcOrder.value=s.sort_order||0;svcHealthUrl.value=s.health_check_url||'';svcEnabled.checked=s.is_enabled;svcHealthEnabled.checked=s.health_check_enabled;modalTitle.textContent=t('Edit site');modal.style.display='flex'}function closeModal(){modal.style.display='none'}
async function save(){const id=editId.value,name=svcName.value.trim(),url=svcUrl.value.trim();if(!name||!url)return alert(t('Name and URL are required'));const body={name,url,icon:svcIcon.value.trim(),category_id:+svcCategory.value||0,description:svcDesc.value.trim(),tags:svcTags.value.trim(),sort_order:+svcOrder.value||0,health_check_url:svcHealthUrl.value.trim(),is_enabled:svcEnabled.checked,health_check_enabled:svcHealthEnabled.checked};if(id)body.id=+id;const r=await fetch(id?'/admin/api/services/'+id:'/admin/api/services',{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||t('Save failed'))}
async function del(id,name){if(!confirm(t('Delete ')+name+t('?')))return;const r=await fetch('/admin/api/services/'+id,{method:'DELETE'}),d=await r.json();if(d.success)load();else alert(d.message||t('Delete failed'))}
function escapeHTML(s){const d=document.createElement('div');d.textContent=s||'';return d.innerHTML}function toggleTheme(){const dark=!document.documentElement.classList.contains('dark');document.documentElement.classList.toggle('dark',dark);localStorage.setItem('tonav-theme',dark?'dark':'light')}document.addEventListener('keydown',e=>{if(e.key==='Escape')closeModal()});load();</script><script src="/static/js/i18n.js?v=2"></script></body></html>
function escapeHTML(s){const d=document.createElement('div');d.textContent=s||'';return d.innerHTML}function toggleTheme(){const dark=!document.documentElement.classList.contains('dark');document.documentElement.classList.toggle('dark',dark);localStorage.setItem('tonav-theme',dark?'dark':'light')}document.addEventListener('keydown',e=>{if(e.key==='Escape')closeModal()});load();</script><script src="/static/js/i18n.js?v=3"></script></body></html>
+9 -9
View File
@@ -1,13 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>{{ .site_title }}</title><link rel="stylesheet" href="/static/css/tonav.css?v=mobile2"></head>
<html lang="zh-CN"><head><meta charset="UTF-8"><link rel="icon" href="/static/favicon.svg?v=2" type="image/svg+xml"><meta name="viewport" content="width=device-width,initial-scale=1"><title>{{ .site_title }}</title><link rel="stylesheet" href="/static/css/tonav.css?v=mobile2"></head>
<body class="app-shell">
<div class="app-layout">
<aside class="sidebar"><a class="brand" href="/">{{ .site_title }}</a><div class="sidebar-section"><div class="section-label">SERVICES</div><button class="nav-item active" data-category="all">All <span id="allCount"></span></button><div id="categoryNav"></div></div><div class="sidebar-footer"><a href="/admin/login">Admin sign in <span>↗</span></a></div></aside>
<main class="main"><header class="topbar"><div><div class="eyebrow">SERVICES</div><h1>Service directory</h1></div><div class="top-actions"><a class="admin-link" href="/admin/login">Sign in</a><button class="icon-button" id="themeBtn" title="Toggle theme" aria-label="Toggle theme">◐</button></div></header>
<div class="mobile-categories" id="mobileCategories"></div><div class="toolbar"><label class="search"><span>⌕</span><input id="searchInput" placeholder="Search services, tags, descriptions…" autocomplete="off"><kbd>/</kbd></label><div class="result-count" id="resultCount"></div></div>
<section class="service-list" id="servicesGrid"><div class="empty-state">Loading…</div></section>
<aside class="sidebar"><a class="brand" href="/">{{ .site_title }}</a><div class="sidebar-section"><div class="section-label">SITES</div><button class="nav-item active" data-category="all">All <span id="allCount"></span></button><div id="categoryNav"></div></div><div class="sidebar-footer"><a href="/admin/login">Admin sign in <span>↗</span></a></div></aside>
<main class="main"><header class="topbar"><div><div class="eyebrow">SITES</div><h1>Site directory</h1></div><div class="top-actions"><a class="admin-link" href="/admin/login">Sign in</a><button class="icon-button" id="themeBtn" title="Toggle theme" aria-label="Toggle theme">◐</button></div></header>
<div class="mobile-categories" id="mobileCategories"></div><div class="toolbar"><label class="search"><span>⌕</span><input id="searchInput" placeholder="Search sites, tags, descriptions…" autocomplete="off"><kbd>/</kbd></label><div class="result-count" id="resultCount"></div></div>
<section class="service-list" id="sitesGrid"><div class="empty-state">Loading…</div></section>
<footer class="page-footer"><span>ToNav</span><span id="lastCheckTime"></span></footer></main></div>
<script>const categoriesData={{ .categories_json }};const servicesData={{ .services_json }};let currentTab='all',currentKeyword='';const map={};categoriesData.forEach(c=>map[c.id]=c.name);const esc=s=>{const d=document.createElement('div');d.textContent=s||'';return d.innerHTML};
function renderNav(){const n=document.getElementById('categoryNav');const m=document.getElementById('mobileCategories');document.getElementById('allCount').textContent=servicesData.length;const items=categoriesData.map(c=>`<button class="nav-item" data-category="${esc(c.name)}">${esc(c.name)} <span>${servicesData.filter(s=>s.category_id===c.id).length}</span></button>`).join('');n.innerHTML=items;m.innerHTML=`<button class="mobile-category active" data-category="all">All <span>${servicesData.length}</span></button>`+categoriesData.map(c=>`<button class="mobile-category" data-category="${esc(c.name)}">${esc(c.name)} <span>${servicesData.filter(s=>s.category_id===c.id).length}</span></button>`).join('');document.querySelectorAll('.nav-item,.mobile-category').forEach(b=>b.onclick=()=>{currentTab=b.dataset.category;document.querySelectorAll('.nav-item,.mobile-category').forEach(x=>x.classList.toggle('active',x.dataset.category===currentTab));render()})}
function render(){let a=currentTab==='all'?servicesData:servicesData.filter(s=>map[s.category_id]===currentTab);if(currentKeyword)a=a.filter(s=>(s.name+' '+(s.description||'')+' '+(s.tags||'')).toLowerCase().includes(currentKeyword));document.getElementById('resultCount').textContent=`${a.length} services`;document.getElementById('servicesGrid').innerHTML=a.length?a.map(s=>`<a class="service-row" href="${esc(s.url)}" target="_blank" rel="noopener"><span class="service-icon">${esc(s.icon)||'↗'}</span><span class="service-main"><strong>${esc(s.name)}</strong><span>${esc(s.description||s.url)}</span></span><span class="service-meta"><span class="status ${s.status==='online'?'online':s.status==='offline'?'offline':''}"></span>${esc(map[s.category_id]||'Uncategorized')}</span><span class="service-arrow">→</span></a>`).join(''):'<div class="empty-state">No services found</div>'}
document.getElementById('searchInput').oninput=e=>{currentKeyword=e.target.value.toLowerCase().trim();render()};document.addEventListener('keydown',e=>{if(e.key==='/'&&document.activeElement.tagName!=='INPUT'){e.preventDefault();document.getElementById('searchInput').focus()}});document.getElementById('themeBtn').onclick=()=>{document.documentElement.classList.toggle('dark');localStorage.setItem('tonav-theme',document.documentElement.classList.contains('dark')?'dark':'light')};if(localStorage.getItem('tonav-theme')==='dark')document.documentElement.classList.add('dark');renderNav();render();document.getElementById('lastCheckTime').textContent=new Date().toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit'});</script><script src="/static/js/i18n.js?v=2"></script></body></html>
<script>const categoriesData={{ .categories_json }};const sitesData={{ .services_json }};let currentTab='all',currentKeyword='';const map={};categoriesData.forEach(c=>map[c.id]=c.name);const esc=s=>{const d=document.createElement('div');d.textContent=s||'';return d.innerHTML};
function renderNav(){const n=document.getElementById('categoryNav');const m=document.getElementById('mobileCategories');document.getElementById('allCount').textContent=sitesData.length;const items=categoriesData.map(c=>`<button class="nav-item" data-category="${esc(c.name)}">${esc(c.name)} <span>${sitesData.filter(s=>s.category_id===c.id).length}</span></button>`).join('');n.innerHTML=items;m.innerHTML=`<button class="mobile-category active" data-category="all">All <span>${sitesData.length}</span></button>`+categoriesData.map(c=>`<button class="mobile-category" data-category="${esc(c.name)}">${esc(c.name)} <span>${sitesData.filter(s=>s.category_id===c.id).length}</span></button>`).join('');document.querySelectorAll('.nav-item,.mobile-category').forEach(b=>b.onclick=()=>{currentTab=b.dataset.category;document.querySelectorAll('.nav-item,.mobile-category').forEach(x=>x.classList.toggle('active',x.dataset.category===currentTab));render()})}
function render(){let a=currentTab==='all'?sitesData:sitesData.filter(s=>map[s.category_id]===currentTab);if(currentKeyword)a=a.filter(s=>(s.name+' '+(s.description||'')+' '+(s.tags||'')).toLowerCase().includes(currentKeyword));document.getElementById('resultCount').textContent=`${a.length} sites`;document.getElementById('sitesGrid').innerHTML=a.length?a.map(s=>`<a class="service-row" href="${esc(s.url)}" target="_blank" rel="noopener"><span class="service-icon">${esc(s.icon)||'↗'}</span><span class="service-main"><strong>${esc(s.name)}</strong><span>${esc(s.description||s.url)}</span></span><span class="service-meta"><span class="status ${s.status==='online'?'online':s.status==='offline'?'offline':''}"></span>${esc(map[s.category_id]||'Uncategorized')}</span><span class="service-arrow">→</span></a>`).join(''):'<div class="empty-state">No sites found</div>'}
document.getElementById('searchInput').oninput=e=>{currentKeyword=e.target.value.toLowerCase().trim();render()};document.addEventListener('keydown',e=>{if(e.key==='/'&&document.activeElement.tagName!=='INPUT'){e.preventDefault();document.getElementById('searchInput').focus()}});document.getElementById('themeBtn').onclick=()=>{document.documentElement.classList.toggle('dark');localStorage.setItem('tonav-theme',document.documentElement.classList.contains('dark')?'dark':'light')};if(localStorage.getItem('tonav-theme')==='dark')document.documentElement.classList.add('dark');renderNav();render();document.getElementById('lastCheckTime').textContent=new Date().toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit'});</script><script src="/static/js/i18n.js?v=3"></script></body></html>