plc_control/web/ops/js/segments.js

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("<", "&lt;")
.replaceAll(">", "&gt;");
}
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();
});
}
}