feat(admin): 历史记录可在后台逐条删除/清空
此前后台**没有任何历史管理入口**,只有一条需要 admin token 的 DELETE /api/history/(清空全部),所以用户根本无法删除记录。 - history-server: 新增 DELETE /item?url=<url> 删除单条并重建分组; CORS 允许 Authorization 头 - admin-server: 新增 /admin/api/history(GET 列表 / DELETE 清空)与 /admin/api/history/item(DELETE 单条),服务端带 token 转发给 history-server —— 令牌只在容器内部使用,浏览器侧无需保存 - admin UI: 「历史访问」卡片,列出记录 + 逐条 ✕ + 清空全部(均二次确认) 标题为空或本身是 URL 时显示简洁域名
This commit is contained in:
@ -104,6 +104,14 @@ details summary{cursor:pointer;font-size:13px;font-weight:600;color:var(--tx2)}
|
||||
<div style="margin-top:14px"><button class="primary" id="save-info">保存站点信息</button></div>
|
||||
</div>
|
||||
|
||||
<!-- 历史访问 -->
|
||||
<div class="card">
|
||||
<h2>历史访问</h2>
|
||||
<p class="sub" style="margin:0 0 12px">删除单条或清空全部浏览记录;删除后 Dashy 的「历史访问」分组会立即重建。</p>
|
||||
<div id="hist"><div class="muted">读取中…</div></div>
|
||||
<div style="margin-top:14px"><button class="ghost" id="clear-hist">清空全部历史</button></div>
|
||||
</div>
|
||||
|
||||
<!-- 添加服务 -->
|
||||
<div class="card">
|
||||
<h2>添加服务</h2>
|
||||
@ -300,6 +308,63 @@ document.getElementById('save-info').addEventListener('click', async () => {
|
||||
else toast('保存失败: ' + (await r.text()).slice(0, 120), '#ef4444');
|
||||
});
|
||||
|
||||
/* ── 历史访问管理 ── */
|
||||
let HIST = [];
|
||||
|
||||
/* 标题为空或本身就是 URL 时,显示简洁域名(与服务端 friendlyHistoryTitle 一致)*/
|
||||
function friendlyTitle(it) {
|
||||
const t = (it.title || '').trim();
|
||||
if (t && !/^https?:\/\//i.test(t)) return t;
|
||||
try { return new URL(it.url).hostname.replace(/^www\./, ''); } catch (e) { return t || it.url; }
|
||||
}
|
||||
|
||||
function renderHistory() {
|
||||
const el = document.getElementById('hist');
|
||||
if (!HIST.length) { el.innerHTML = '<div class="muted">暂无历史记录</div>'; return; }
|
||||
el.innerHTML = HIST.map((it, i) => `
|
||||
<div class="itm">
|
||||
<div class="itm-i">
|
||||
<div class="itm-t">${esc(friendlyTitle(it))}</div>
|
||||
<div class="itm-u">${esc(it.url)} · ${Number(it.total_visits) || 0} 次</div>
|
||||
</div>
|
||||
<button class="icon-btn danger" data-hist="${i}" title="删除这条记录">✕</button>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
async function loadHistory() {
|
||||
try {
|
||||
const r = await fetch(A + '/history', { headers: authHeaders() });
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
const d = await r.json();
|
||||
HIST = (d && d.items) || [];
|
||||
} catch (e) {
|
||||
HIST = [];
|
||||
document.getElementById('hist').innerHTML = '<div class="muted">读取历史失败:' + esc(e.message) + '</div>';
|
||||
return;
|
||||
}
|
||||
renderHistory();
|
||||
}
|
||||
|
||||
document.getElementById('hist').addEventListener('click', async (ev) => {
|
||||
const btn = ev.target.closest('[data-hist]');
|
||||
if (!btn) return;
|
||||
const it = HIST[Number(btn.dataset.hist)];
|
||||
if (!it) return;
|
||||
if (!confirm('删除这条历史记录?\n' + friendlyTitle(it))) return;
|
||||
const r = await fetch(A + '/history/item?url=' + encodeURIComponent(it.url), {
|
||||
method: 'DELETE', headers: authHeaders(),
|
||||
});
|
||||
if (r.ok) { toast('已删除该记录'); await loadHistory(); }
|
||||
else toast('删除失败: ' + (await r.text()).slice(0, 120), '#ef4444');
|
||||
});
|
||||
|
||||
document.getElementById('clear-hist').addEventListener('click', async () => {
|
||||
if (!confirm('清空全部浏览历史?此操作无法撤销;下次插件同步会重新累积。')) return;
|
||||
const r = await fetch(A + '/history', { method: 'DELETE', headers: authHeaders() });
|
||||
if (r.ok) { toast('已清空历史'); await loadHistory(); }
|
||||
else toast('清空失败: ' + (await r.text()).slice(0, 120), '#ef4444');
|
||||
});
|
||||
|
||||
/* ── 分组与条目 ── */
|
||||
async function loadSections() {
|
||||
const r = await fetch(A + '/sections', { headers: authHeaders() });
|
||||
@ -507,6 +572,7 @@ document.getElementById('file-wall').addEventListener('change', async (ev) => {
|
||||
await loadPageInfo();
|
||||
await loadSections();
|
||||
await loadWallpaper();
|
||||
await loadHistory();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user