105 lines
4.0 KiB
JavaScript
105 lines
4.0 KiB
JavaScript
const activeLease = document.querySelector("#activeLease");
|
|
const pendingRequests = document.querySelector("#pendingRequests");
|
|
const pendingCount = document.querySelector("#pendingCount");
|
|
const revokeButton = document.querySelector("#revokeButton");
|
|
const message = document.querySelector("#message");
|
|
let snapshot = null;
|
|
|
|
function escapeHtml(value) {
|
|
const node = document.createElement("span");
|
|
node.textContent = String(value ?? "");
|
|
return node.innerHTML;
|
|
}
|
|
|
|
function showMessage(value, error = false) {
|
|
message.textContent = value;
|
|
message.style.color = error ? "#ff8797" : "#7fbbff";
|
|
}
|
|
|
|
async function api(path, body) {
|
|
const response = await fetch(path, {
|
|
method: body ? "POST" : "GET",
|
|
credentials: "same-origin",
|
|
headers: body ? { "Content-Type": "application/json" } : {},
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
if (response.status === 401) {
|
|
location.replace(`/login?next=${encodeURIComponent(location.pathname)}`);
|
|
throw new Error("请先登录本机服务");
|
|
}
|
|
const payload = await response.json();
|
|
if (!response.ok) throw new Error(payload.message || payload.error);
|
|
return payload;
|
|
}
|
|
|
|
function render(data) {
|
|
snapshot = data;
|
|
pendingCount.textContent = String(data.pending.length);
|
|
if (data.active) {
|
|
const lease = data.active;
|
|
activeLease.className = "lease-details";
|
|
activeLease.innerHTML = `
|
|
<div class="identity"><strong>${escapeHtml(lease.label)}</strong><code>${escapeHtml(lease.client_id)}</code><span>来源 ${escapeHtml(lease.client_address)} · 最近执行 ${escapeHtml(lease.last_activity_at)}</span></div>
|
|
<div class="timer">${Number(lease.idle_remaining_seconds)}s</div>`;
|
|
revokeButton.hidden = false;
|
|
revokeButton.dataset.clientId = lease.client_id;
|
|
} else {
|
|
activeLease.className = "empty";
|
|
activeLease.textContent = "当前无人获批使用真实台架。";
|
|
revokeButton.hidden = true;
|
|
}
|
|
if (!data.pending.length) {
|
|
pendingRequests.className = "request-list empty";
|
|
pendingRequests.textContent = "暂时没有申请。";
|
|
return;
|
|
}
|
|
pendingRequests.className = "request-list";
|
|
pendingRequests.innerHTML = data.pending.map((request) => `
|
|
<article class="request">
|
|
<div class="identity"><strong>${escapeHtml(request.label)}</strong><code>${escapeHtml(request.client_id)}</code><span>来源 ${escapeHtml(request.client_address)} · ${escapeHtml(request.requested_at)}</span></div>
|
|
<div class="actions">
|
|
<button type="button" data-action="approve" data-client-id="${escapeHtml(request.client_id)}">同意</button>
|
|
<button type="button" class="secondary" data-action="deny" data-client-id="${escapeHtml(request.client_id)}">拒绝</button>
|
|
</div>
|
|
</article>`).join("");
|
|
}
|
|
|
|
async function refresh() {
|
|
try {
|
|
render(await api("/api/local/hardware-access"));
|
|
document.querySelector("#clock").textContent = new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
|
} catch (error) {
|
|
showMessage(error.message, true);
|
|
}
|
|
}
|
|
|
|
pendingRequests.addEventListener("click", async (event) => {
|
|
const button = event.target.closest("button[data-action]");
|
|
if (!button) return;
|
|
button.disabled = true;
|
|
try {
|
|
const action = button.dataset.action;
|
|
render(await api(`/api/local/hardware-access/${action}`, { client_id: button.dataset.clientId }));
|
|
showMessage(action === "approve" ? "已授予该终端真实台架使用权。" : "已拒绝该终端的申请。");
|
|
} catch (error) {
|
|
showMessage(error.message, true);
|
|
} finally {
|
|
button.disabled = false;
|
|
}
|
|
});
|
|
|
|
revokeButton.addEventListener("click", async () => {
|
|
revokeButton.disabled = true;
|
|
try {
|
|
render(await api("/api/local/hardware-access/revoke", { client_id: revokeButton.dataset.clientId }));
|
|
showMessage("已停止该终端的真实台架使用权;再次使用必须重新申请。");
|
|
} catch (error) {
|
|
showMessage(error.message, true);
|
|
} finally {
|
|
revokeButton.disabled = false;
|
|
}
|
|
});
|
|
|
|
refresh();
|
|
setInterval(refresh, 2000);
|