// 全局 JS 工具:API 调用(自动带 CSRF)、toast、弹窗、表格分页
function csrf() {
return sessionStorage.getItem('csrf') || document.querySelector('meta[name=csrf]')?.content || '';
}
async function api(method, url, body) {
const opt = { method, headers: {} };
if (body !== undefined) {
opt.headers['Content-Type'] = 'application/json';
opt.body = JSON.stringify(body);
}
if (method !== 'GET' && method !== 'HEAD') {
opt.headers['X-CSRF-Token'] = csrf();
}
const r = await fetch(url, opt);
if (r.status === 401) { location.href = '/admin/login'; throw new Error('未登录'); }
const j = await r.json().catch(() => ({ ok: false, error: 'HTTP ' + r.status }));
if (!j.ok) throw new Error(j.error || ('HTTP ' + r.status));
return j.data;
}
function toast(msg, isErr) {
const box = document.getElementById('toast');
const d = document.createElement('div');
d.className = 'toast-msg' + (isErr ? ' err' : '');
d.textContent = msg;
box.appendChild(d);
setTimeout(() => d.remove(), 3500);
}
async function logout() {
try { await api('POST', '/api/auth/logout'); } catch (e) {}
sessionStorage.removeItem('csrf');
location.href = '/admin/login';
}
function esc(s) {
return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
}
function fmtBytes(b) {
b = Number(b) || 0;
const u = ['B','KB','MB','GB','TB','PB'];
let i = 0;
while (b >= 1024 && i < u.length - 1) { b /= 1024; i++; }
return (i === 0 ? b : b.toFixed(1)) + ' ' + u[i];
}
// UTC "YYYY-MM-DD HH:MM:SS" → 本地显示(默认 +8)
const TZ = 8;
function fmtTime(s) {
if (!s) return '-';
const d = new Date(s.replace(' ', 'T') + 'Z');
if (isNaN(d)) return s;
const l = new Date(d.getTime() + TZ * 3600 * 1000);
return l.toISOString().slice(0, 19).replace('T', ' ');
}
function openModal(title, bodyHtml, onOk, okText) {
const root = document.getElementById('modal-root');
root.innerHTML = `
${esc(title)}
${bodyHtml}
`;
document.getElementById('modal-ok').onclick = async () => {
try { await onOk(); closeModal(); } catch (e) { toast(e.message, true); }
};
}
function closeModal() { document.getElementById('modal-root').innerHTML = ''; }
async function confirmDo(msg, fn, okText) {
openModal('确认', `${esc(msg)}
`, fn, okText || '确定');
}
function pagerHtml(total, page, pageSize, fnName) {
const pages = Math.max(1, Math.ceil(total / pageSize));
return ``;
}
function qs(obj) {
return Object.entries(obj).filter(([, v]) => v !== undefined && v !== null && v !== '')
.map(([k, v]) => encodeURIComponent(k) + '=' + encodeURIComponent(v)).join('&');
}
function badgeFor(enabled, reason) {
if (enabled) return '启用';
const m = { manual: '手动停用', expired: '已过期', quota: '流量用尽' };
return `${m[reason] || '停用'}`;
}
// ---- P11 配额 / 到期单元格 ----
function quotaCell(r) {
const q = r.quota;
if (!q || !q.quota_bytes) return '-';
const pct = Math.min(100, Math.round(q.used_bytes * 100 / q.quota_bytes));
const cls = q.status === 'exceeded' ? 'danger' : (pct >= 80 ? 'warn' : '');
const st = q.status === 'exceeded' ? ' 超限' : '';
const period = q.period === 'monthly' ? ' 月' : '';
return ``
+ `${pct}%${period}${st}`;
}
function expireCell(r) {
if (!r.expires_at) return '永久';
const past = r.expires_at <= __nowStr();
return `${esc(r.expires_at)}`;
}
function __nowStr() {
const d = new Date();
const p = n => String(n).padStart(2, '0');
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
}
function copyText(t) {
navigator.clipboard.writeText(t).then(() => toast('已复制'), () => toast('复制失败', true));
}
// ---- P8 运行时状态徽章 ----
let __rtCache = null, __rtCacheAt = 0;
async function loadRuntimeStatus(force) {
const now = Date.now();
if (!force && __rtCache && now - __rtCacheAt < 5000) return __rtCache;
try {
const d = await api('GET', '/api/runtime/status');
__rtCache = {};
for (const it of d.items) __rtCache[it.entry_type + ':' + it.entry_id] = it;
__rtCacheAt = now;
} catch (e) { __rtCache = __rtCache || {}; }
return __rtCache;
}
function runtimeBadge(etype, id) {
const m = (__rtCache || {})[etype + ':' + id] || (__rtCache || {})[etype + '_udp:' + id];
if (!m) return '-';
const st = m.status || '';
if (st === 'running') return `运行中`;
if (st.startsWith('idle')) return '待机';
if (st.startsWith('error:')) return `异常`;
return `${esc(st)}`;
}
async function refreshRuntimeColumn(etype) {
await loadRuntimeStatus(true);
document.querySelectorAll('[data-rt]').forEach(td => {
const [t, id] = td.dataset.rt.split(':');
td.innerHTML = runtimeBadge(t, +id);
});
}
function fmtBps(bps) {
if (bps == null) return '—';
const b = bps / 8;
if (b < 1024) return b.toFixed(0) + ' B/s';
if (b < 1048576) return (b / 1024).toFixed(1) + ' KB/s';
if (b < 1073741824) return (b / 1048576).toFixed(1) + ' MB/s';
return (b / 1073741824).toFixed(2) + ' GB/s';
}