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:
HomeDash Bot
2026-09-11 17:03:33 +08:00
parent 80b0e5f51e
commit c04d00d4de
3 changed files with 196 additions and 1 deletions

View File

@ -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>