feat: 隐藏后台面板 + /api/stats 运行统计
- 连点页脚提示 6 次唤出管理面板:本机偏好/播放状态 + 服务器统计 - server.py 新增 GET /api/stats:运行时长、访客 IP(X-Forwarded-For 末位取真实 IP 防伪造)、TTS 请求与缓存命中率、缓存体积、经典数 - 计数为进程内存统计,重启清零 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -951,3 +951,89 @@ const hashId = location.hash.replace(/^#/, "");
|
||||
: (SUTRAS.some((s) => s.id === prefs.sutra) ? prefs.sutra : SUTRAS[0]?.id);
|
||||
if (initSutraId) selectSutra(initSutraId);
|
||||
})();
|
||||
|
||||
// ===== 隐藏后台:2 秒内连点页脚提示 6 次打开管理面板(来源链接点击不计数) =====
|
||||
const footHint = document.getElementById("foot-hint");
|
||||
let hintClicks = 0;
|
||||
let hintTimer = null;
|
||||
footHint.addEventListener("click", (e) => {
|
||||
if (e.target.closest("a")) return;
|
||||
hintClicks += 1;
|
||||
clearTimeout(hintTimer);
|
||||
hintTimer = setTimeout(() => { hintClicks = 0; }, 2000);
|
||||
if (hintClicks >= 6) {
|
||||
hintClicks = 0;
|
||||
openAdmin();
|
||||
}
|
||||
});
|
||||
|
||||
/** 字节数转可读文本。 */
|
||||
function fmtBytes(n) {
|
||||
if (n >= 1 << 20) return `${(n / (1 << 20)).toFixed(1)} MB`;
|
||||
if (n >= 1 << 10) return `${(n / (1 << 10)).toFixed(1)} KB`;
|
||||
return `${n} B`;
|
||||
}
|
||||
|
||||
/** 秒数转可读运行时长(如 2天3时 / 4时12分 / 35秒)。 */
|
||||
function fmtUptime(sec) {
|
||||
const d = Math.floor(sec / 86400);
|
||||
const h = Math.floor((sec % 86400) / 3600);
|
||||
const m = Math.floor((sec % 3600) / 60);
|
||||
if (d) return `${d}天${h}时`;
|
||||
if (h) return `${h}时${m}分`;
|
||||
if (m) return `${m}分${sec % 60}秒`;
|
||||
return `${sec}秒`;
|
||||
}
|
||||
|
||||
/** 打开管理面板:本机偏好/播放状态 + 服务器 /api/stats 统计,可点"刷新"重拉。 */
|
||||
function openAdmin() {
|
||||
const mask = document.createElement("div");
|
||||
mask.className = "admin-mask";
|
||||
const card = document.createElement("div");
|
||||
card.className = "admin-card";
|
||||
card.setAttribute("role", "dialog");
|
||||
card.setAttribute("aria-label", "管理面板");
|
||||
const voiceName = voiceSelect.options[voiceSelect.selectedIndex]?.textContent || "-";
|
||||
const bgmName = bgmSelect.options[bgmSelect.selectedIndex]?.textContent || "关";
|
||||
card.innerHTML = `
|
||||
<div class="admin-title">后台</div>
|
||||
<div class="admin-sec">本机</div>
|
||||
<div class="admin-rows">
|
||||
<div class="admin-row"><span>经典</span><b>${sutra ? sutra.name : "-"}</b></div>
|
||||
<div class="admin-row"><span>音色</span><b>${voiceName}</b></div>
|
||||
<div class="admin-row"><span>语速</span><b>${rateSelect.value}×</b></div>
|
||||
<div class="admin-row"><span>配乐</span><b>${bgmName}</b></div>
|
||||
<div class="admin-row"><span>循环 / 夜间</span><b>${loopMode ? "开" : "关"} / ${document.body.classList.contains("night") ? "开" : "关"}</b></div>
|
||||
<div class="admin-row"><span>播放</span><b>${playing ? `第 ${currentSeg + 1}/${segments.length} 段` : "未在播"}</b></div>
|
||||
<div class="admin-row"><span>浏览器</span><b>${/Mobile/.test(navigator.userAgent) ? "移动端" : "桌面端"}${navigator.serviceWorker?.controller ? " · PWA" : ""}</b></div>
|
||||
</div>
|
||||
<div class="admin-sec">服务器</div>
|
||||
<div class="admin-rows" id="admin-server"><div class="admin-row"><span>载入中…</span></div></div>
|
||||
<div class="admin-actions">
|
||||
<button type="button" class="admin-btn" id="admin-refresh">刷新</button>
|
||||
<button type="button" class="admin-btn" id="admin-close">关闭</button>
|
||||
</div>`;
|
||||
mask.appendChild(card);
|
||||
document.body.appendChild(mask);
|
||||
const close = () => mask.remove();
|
||||
mask.addEventListener("click", (e) => { if (e.target === mask) close(); });
|
||||
card.querySelector("#admin-close").addEventListener("click", close);
|
||||
|
||||
const loadStats = async () => {
|
||||
const box = card.querySelector("#admin-server");
|
||||
try {
|
||||
const s = await (await fetch("/api/stats")).json();
|
||||
const hitRate = s.tts_requests ? `${Math.round((s.cache_hits / s.tts_requests) * 100)}%` : "-";
|
||||
box.innerHTML = `
|
||||
<div class="admin-row"><span>运行时长</span><b>${fmtUptime(s.uptime_sec)}</b></div>
|
||||
<div class="admin-row"><span>访客 IP</span><b>${s.visitors}</b></div>
|
||||
<div class="admin-row"><span>TTS 请求</span><b>${s.tts_requests}(命中 ${s.cache_hits} · ${hitRate})</b></div>
|
||||
<div class="admin-row"><span>语音缓存</span><b>${s.cache_files} 个 · ${fmtBytes(s.cache_bytes)}</b></div>
|
||||
<div class="admin-row"><span>经典数 / 模型</span><b>${s.sutra_count} 部 · ${s.model}</b></div>`;
|
||||
} catch {
|
||||
box.innerHTML = '<div class="admin-row"><span>服务器统计不可用</span></div>';
|
||||
}
|
||||
};
|
||||
loadStats();
|
||||
card.querySelector("#admin-refresh").addEventListener("click", loadStats);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user