plc_control/web/js/ops.js

149 lines
5.2 KiB
JavaScript

import { apiFetch } from "./api.js";
import { dom } from "./dom.js";
import { formatValue } from "./points.js";
import { state } from "./state.js";
const SIGNAL_ROLES = ["rem", "run", "flt"];
const ROLE_LABELS = { rem: "REM", run: "RUN", flt: "FLT" };
export function renderOpsUnits() {
if (!dom.opsUnitList) return;
dom.opsUnitList.innerHTML = "";
if (!state.units.length) {
dom.opsUnitList.innerHTML = '<div class="muted" style="padding:12px">暂无控制单元</div>';
return;
}
state.units.forEach((unit) => {
const runtime = state.runtimes.get(unit.id);
const item = document.createElement("div");
item.className = `ops-unit-item${state.selectedOpsUnitId === unit.id ? " selected" : ""}`;
item.innerHTML = `
<div class="ops-unit-item-name">${unit.code} / ${unit.name}</div>
<div class="ops-unit-item-meta">
<span class="badge ${unit.enabled ? "" : "offline"}">${unit.enabled ? "EN" : "DIS"}</span>
${runtime ? `<span>Acc ${Math.floor(runtime.accumulated_run_sec / 1000)}s</span>` : ""}
</div>
`;
item.addEventListener("click", () => selectOpsUnit(unit.id));
dom.opsUnitList.appendChild(item);
});
}
async function selectOpsUnit(unitId) {
state.selectedOpsUnitId = unitId === state.selectedOpsUnitId ? null : unitId;
renderOpsUnits();
if (!state.selectedOpsUnitId) {
await loadAllEquipmentCards();
return;
}
dom.opsEquipmentArea.innerHTML = '<div class="muted ops-placeholder">加载中...</div>';
state.opsPointEls.clear();
const detail = await apiFetch(`/api/unit/${state.selectedOpsUnitId}/detail`);
renderOpsEquipments(detail.equipments || []);
}
export async function loadAllEquipmentCards() {
if (!dom.opsEquipmentArea) return;
if (!state.units.length) {
dom.opsEquipmentArea.innerHTML = '<div class="muted ops-placeholder">暂无控制单元</div>';
return;
}
dom.opsEquipmentArea.innerHTML = '<div class="muted ops-placeholder">加载中...</div>';
state.opsPointEls.clear();
const details = await Promise.all(
state.units.map((u) => apiFetch(`/api/unit/${u.id}/detail`).catch(() => ({ equipments: [] })))
);
const allEquipments = details.flatMap((d) => d.equipments || []);
renderOpsEquipments(allEquipments);
}
function renderOpsEquipments(equipments) {
dom.opsEquipmentArea.innerHTML = "";
if (!equipments.length) {
dom.opsEquipmentArea.innerHTML = '<div class="muted ops-placeholder">该单元下暂无设备</div>';
return;
}
equipments.forEach((eq) => {
const card = document.createElement("div");
card.className = "ops-eq-card";
// Build role → point map
const roleMap = {};
(eq.points || []).forEach((p) => {
if (p.signal_role) roleMap[p.signal_role] = p;
});
// Signal rows HTML (placeholders; WS will fill values)
const signalRowsHtml = SIGNAL_ROLES.map((role) => {
const point = roleMap[role];
if (!point) return "";
return `
<div class="ops-signal-row">
<span class="ops-signal-label">${ROLE_LABELS[role] || role}</span>
<span class="badge quality-unknown" data-ops-quality="${point.id}">?</span>
<span class="ops-signal-value" data-ops-value="${point.id}">--</span>
</div>`;
}).join("");
const canControl = eq.kind === "coal_feeder" || eq.kind === "distributor";
card.innerHTML = `
<div class="ops-eq-card-head">
<strong title="${eq.name}">${eq.code}</strong>
<span class="badge">${eq.kind || "--"}</span>
</div>
<div class="ops-signal-rows">${signalRowsHtml || '<span class="muted" style="font-size:11px;padding:2px 0">无绑定信号</span>'}</div>
${canControl ? '<div class="ops-eq-card-actions"></div>' : ""}
`;
if (canControl) {
const actions = card.querySelector(".ops-eq-card-actions");
const startBtn = document.createElement("button");
startBtn.className = "secondary";
startBtn.textContent = "Start";
startBtn.addEventListener("click", () =>
apiFetch(`/api/control/equipment/${eq.id}/start`, { method: "POST" }).catch(() => {})
);
const stopBtn = document.createElement("button");
stopBtn.className = "danger";
stopBtn.textContent = "Stop";
stopBtn.addEventListener("click", () =>
apiFetch(`/api/control/equipment/${eq.id}/stop`, { method: "POST" }).catch(() => {})
);
actions.append(startBtn, stopBtn);
}
dom.opsEquipmentArea.appendChild(card);
// Register DOM elements for WS updates, then seed from cached monitor data
SIGNAL_ROLES.forEach((role) => {
const point = roleMap[role];
if (!point) return;
const valueEl = card.querySelector(`[data-ops-value="${point.id}"]`);
const qualityEl = card.querySelector(`[data-ops-quality="${point.id}"]`);
if (valueEl && qualityEl) {
state.opsPointEls.set(point.id, { valueEl, qualityEl });
if (point.point_monitor) {
const m = point.point_monitor;
valueEl.textContent = formatValue(m);
qualityEl.className = `badge quality-${(m.quality || "unknown").toLowerCase()}`;
qualityEl.textContent = (m.quality || "unknown").toUpperCase();
}
}
});
});
}
export function startOps() {
renderOpsUnits();
loadAllEquipmentCards();
}