186 lines
6.1 KiB
JavaScript
186 lines
6.1 KiB
JavaScript
import { fetchOverview, segmentControl } from "./api.js";
|
|
|
|
const STATE_LABEL = {
|
|
idle: "空闲",
|
|
checking: "校验",
|
|
executing: "执行",
|
|
confirming: "等待确认",
|
|
resetting: "复位",
|
|
completed: "完成",
|
|
blocked: "阻塞",
|
|
faulted: "故障",
|
|
manual_ack_required: "待人工确认",
|
|
};
|
|
|
|
const STATE_CLASS = {
|
|
idle: "state-idle",
|
|
checking: "state-active",
|
|
executing: "state-active",
|
|
confirming: "state-active",
|
|
resetting: "state-active",
|
|
completed: "state-active",
|
|
blocked: "state-warn",
|
|
faulted: "state-error",
|
|
manual_ack_required: "state-warn",
|
|
};
|
|
|
|
const segments = new Map();
|
|
|
|
function escapeHtml(text) {
|
|
if (text === null || text === undefined) return "";
|
|
return String(text)
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">");
|
|
}
|
|
|
|
function renderState(runtime) {
|
|
const state = runtime?.state || "idle";
|
|
const label = STATE_LABEL[state] || state;
|
|
const cls = STATE_CLASS[state] || "state-idle";
|
|
return `<span class="state-badge ${cls}">${escapeHtml(label)}</span>`;
|
|
}
|
|
|
|
function renderActions(seg) {
|
|
const runtime = seg.runtime || {};
|
|
const autoOn = runtime.auto_enabled === true;
|
|
const state = runtime.state || "idle";
|
|
const canAck = state === "faulted" || state === "manual_ack_required";
|
|
const canReset = canAck || state === "blocked";
|
|
return `
|
|
<div class="card-actions">
|
|
<button data-action="start-auto" data-id="${seg.segment.id}" ${autoOn ? "disabled" : ""}>启动</button>
|
|
<button data-action="stop-auto" data-id="${seg.segment.id}" ${autoOn ? "" : "disabled"}>停止</button>
|
|
<button data-action="ack-fault" data-id="${seg.segment.id}" ${canAck ? "" : "disabled"}>故障确认</button>
|
|
<button data-action="reset" data-id="${seg.segment.id}" ${canReset ? "" : "disabled"}>复位</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderCard(seg) {
|
|
const segment = seg.segment;
|
|
const runtime = seg.runtime || {};
|
|
const note = runtime.fault_message || runtime.blocked_reason || "";
|
|
const lineTag = segment.line_code ? `<span class="badge">${escapeHtml(segment.line_code)}</span>` : "";
|
|
const modeTag = `<span class="badge">${escapeHtml(segment.mode)}</span>`;
|
|
const autoTag = runtime.auto_enabled
|
|
? `<span class="badge badge-accent">AUTO</span>`
|
|
: "";
|
|
const stepText = runtime.current_step_no === null || runtime.current_step_no === undefined
|
|
? "—"
|
|
: `Step ${runtime.current_step_no}`;
|
|
return `
|
|
<article class="ops-card" data-segment-id="${segment.id}">
|
|
<header class="card-head">
|
|
<div class="card-title">
|
|
<strong>${escapeHtml(segment.code)}</strong>
|
|
<span class="muted">${escapeHtml(segment.name)}</span>
|
|
</div>
|
|
<div class="card-tags">${lineTag}${modeTag}${autoTag}${renderState(runtime)}</div>
|
|
</header>
|
|
<div class="card-body">
|
|
<div class="card-row"><span class="muted">当前步骤</span><span>${escapeHtml(stepText)}</span></div>
|
|
${note ? `<div class="card-note">${escapeHtml(note)}</div>` : ""}
|
|
</div>
|
|
${renderActions(seg)}
|
|
</article>
|
|
`;
|
|
}
|
|
|
|
function renderAll() {
|
|
const root = document.getElementById("segmentList");
|
|
if (!root) return;
|
|
const items = Array.from(segments.values());
|
|
items.sort((a, b) => a.segment.code.localeCompare(b.segment.code));
|
|
if (items.length === 0) {
|
|
root.innerHTML = `<div class="muted card-empty">尚无段配置;执行种子或通过配置页新增段。</div>`;
|
|
return;
|
|
}
|
|
root.innerHTML = items.map(renderCard).join("");
|
|
}
|
|
|
|
function setBanner(message, level = "info") {
|
|
const root = document.getElementById("segmentList");
|
|
if (!root) return;
|
|
const existing = root.querySelector(".ops-banner");
|
|
if (existing) existing.remove();
|
|
const div = document.createElement("div");
|
|
div.className = `ops-banner banner-${level}`;
|
|
div.textContent = message;
|
|
root.prepend(div);
|
|
window.setTimeout(() => div.remove(), 4000);
|
|
}
|
|
|
|
async function callAndRefresh(label, fn) {
|
|
try {
|
|
await fn();
|
|
setBanner(`${label} 成功`, "info");
|
|
} catch (err) {
|
|
setBanner(err.message || String(err), "error");
|
|
}
|
|
}
|
|
|
|
function handleAction(event) {
|
|
const button = event.target.closest("button[data-action]");
|
|
if (!button) return;
|
|
const action = button.dataset.action;
|
|
const id = button.dataset.id;
|
|
switch (action) {
|
|
case "start-auto":
|
|
return callAndRefresh("启动自动控制", () => segmentControl.startAuto(id));
|
|
case "stop-auto":
|
|
return callAndRefresh("停止自动控制", () => segmentControl.stopAuto(id));
|
|
case "ack-fault":
|
|
return callAndRefresh("故障确认", () => segmentControl.ackFault(id));
|
|
case "reset":
|
|
return callAndRefresh("复位", () => segmentControl.reset(id));
|
|
default:
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
export async function loadSegments() {
|
|
const data = await fetchOverview();
|
|
segments.clear();
|
|
(data?.segments || []).forEach((entry) => {
|
|
segments.set(entry.segment.id, entry);
|
|
});
|
|
renderAll();
|
|
}
|
|
|
|
/// Apply a SegmentRuntime payload pushed via WebSocket app_event.
|
|
export function applyRuntimeUpdate(runtime) {
|
|
if (!runtime?.segment_id) return;
|
|
const entry = segments.get(runtime.segment_id);
|
|
if (!entry) {
|
|
// Unknown segment — refresh from overview so we pick it up.
|
|
void loadSegments();
|
|
return;
|
|
}
|
|
entry.runtime = runtime;
|
|
renderAll();
|
|
}
|
|
|
|
export function bindSegmentEvents() {
|
|
const root = document.getElementById("segmentList");
|
|
if (root) root.addEventListener("click", handleAction);
|
|
const refreshBtn = document.getElementById("refreshSegmentsBtn");
|
|
if (refreshBtn) {
|
|
refreshBtn.addEventListener("click", () => callAndRefresh("刷新", loadSegments));
|
|
}
|
|
const batchStart = document.getElementById("batchStartAutoBtn");
|
|
if (batchStart) {
|
|
batchStart.addEventListener("click", async () => {
|
|
await callAndRefresh("批量启动", () => segmentControl.batchStart());
|
|
await loadSegments();
|
|
});
|
|
}
|
|
const batchStop = document.getElementById("batchStopAutoBtn");
|
|
if (batchStop) {
|
|
batchStop.addEventListener("click", async () => {
|
|
await callAndRefresh("批量停止", () => segmentControl.batchStop());
|
|
await loadSegments();
|
|
});
|
|
}
|
|
}
|