233 lines
9.0 KiB
JavaScript
233 lines
9.0 KiB
JavaScript
import { apiFetch } from "./api.js";
|
|
import { dom } from "./dom.js";
|
|
import { state } from "./state.js";
|
|
import { loadUnits } from "./units.js";
|
|
|
|
const SIGNAL_ROLES = ["rem", "run", "flt"];
|
|
const ROLE_LABELS = { rem: "REM", run: "RUN", flt: "FLT" };
|
|
|
|
function isSignalOn(quality, valueText) {
|
|
if (!quality || quality.toLowerCase() !== "good") return false;
|
|
const v = String(valueText ?? "").trim().toLowerCase();
|
|
return v === "1" || v === "true" || v === "on";
|
|
}
|
|
|
|
export function sigPillClass(role, quality, valueText) {
|
|
if (!quality || quality.toLowerCase() !== "good") return "sig-pill sig-warn";
|
|
const on = isSignalOn(quality, valueText);
|
|
if (!on) return "sig-pill";
|
|
return role === "flt" ? "sig-pill sig-fault" : "sig-pill sig-on";
|
|
}
|
|
|
|
function runtimeBadge(runtime) {
|
|
if (!runtime) return '<span class="badge offline">OFFLINE</span>';
|
|
if (runtime.comm_locked) return '<span class="badge offline">COMM ERR</span>';
|
|
if (runtime.fault_locked) return '<span class="badge danger">FAULT</span>';
|
|
const labels = { stopped: "STOPPED", running: "RUNNING", distributor_running: "DIST RUN", fault_locked: "FAULT", comm_locked: "COMM ERR" };
|
|
const cls = { stopped: "", running: "online", distributor_running: "online", fault_locked: "danger", comm_locked: "offline" };
|
|
return `<span class="badge ${cls[runtime.state] ?? ""}">${labels[runtime.state] ?? runtime.state}</span>`;
|
|
}
|
|
|
|
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">
|
|
${runtimeBadge(runtime)}
|
|
<span class="badge ${unit.enabled ? "" : "offline"}">${unit.enabled ? "EN" : "DIS"}</span>
|
|
${runtime ? `<span class="muted">Acc ${Math.floor(runtime.display_acc_sec / 1000)}s</span>` : ""}
|
|
</div>
|
|
<div class="ops-unit-item-actions"></div>
|
|
`;
|
|
item.addEventListener("click", () => selectOpsUnit(unit.id));
|
|
|
|
const actions = item.querySelector(".ops-unit-item-actions");
|
|
|
|
const isAutoOn = runtime?.auto_enabled;
|
|
const startBlocked = !isAutoOn && (runtime?.fault_locked || runtime?.manual_ack_required || runtime?.rem_local);
|
|
const autoBtn = document.createElement("button");
|
|
autoBtn.className = isAutoOn ? "danger" : "secondary";
|
|
autoBtn.textContent = isAutoOn ? "停止自动" : "启动自动";
|
|
autoBtn.disabled = startBlocked;
|
|
autoBtn.title = startBlocked
|
|
? (runtime?.fault_locked ? "设备故障中,无法启动自动控制"
|
|
: runtime?.rem_local ? "设备处于本地模式(REM关),无法启动自动控制"
|
|
: "需人工确认故障后才可启动自动控制")
|
|
: (isAutoOn ? "停止自动控制" : "启动自动控制");
|
|
autoBtn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
apiFetch(`/api/control/unit/${unit.id}/${isAutoOn ? "stop-auto" : "start-auto"}`, { method: "POST" })
|
|
.then(() => loadUnits()).catch(() => {});
|
|
});
|
|
actions.append(autoBtn);
|
|
|
|
if (runtime?.manual_ack_required) {
|
|
const ackBtn = document.createElement("button");
|
|
ackBtn.className = "danger";
|
|
ackBtn.textContent = "故障确认";
|
|
ackBtn.title = "人工确认解除故障锁定";
|
|
ackBtn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
apiFetch(`/api/control/unit/${unit.id}/ack-fault`, { method: "POST" })
|
|
.then(() => loadUnits()).catch(() => {});
|
|
});
|
|
actions.append(ackBtn);
|
|
}
|
|
|
|
dom.opsUnitList.appendChild(item);
|
|
});
|
|
}
|
|
|
|
function selectOpsUnit(unitId) {
|
|
state.selectedOpsUnitId = unitId === state.selectedOpsUnitId ? null : unitId;
|
|
renderOpsUnits();
|
|
state.opsPointEls.clear();
|
|
|
|
if (!state.selectedOpsUnitId) {
|
|
renderOpsEquipments(state.units.flatMap((u) => u.equipments || []));
|
|
return;
|
|
}
|
|
|
|
const unit = state.unitMap.get(unitId);
|
|
renderOpsEquipments(unit ? (unit.equipments || []) : []);
|
|
}
|
|
|
|
export function loadAllEquipmentCards() {
|
|
if (!dom.opsEquipmentArea) return;
|
|
state.opsPointEls.clear();
|
|
renderOpsEquipments(state.units.flatMap((u) => u.equipments || []));
|
|
}
|
|
|
|
function renderOpsEquipments(equipments) {
|
|
dom.opsEquipmentArea.innerHTML = "";
|
|
state.opsUnitSyncFns.clear();
|
|
|
|
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";
|
|
|
|
const roleMap = {};
|
|
(eq.role_points || []).forEach((p) => { roleMap[p.signal_role] = p; });
|
|
|
|
// Signal pills — one pill per bound role, text label inside
|
|
const signalRowsHtml = SIGNAL_ROLES.map((role) => {
|
|
const point = roleMap[role];
|
|
if (!point) return "";
|
|
return `<span class="sig-pill sig-warn" data-ops-dot="${point.point_id}" data-ops-role="${role}">${ROLE_LABELS[role] || role}</span>`;
|
|
}).join("");
|
|
|
|
const canControl = eq.kind === "coal_feeder" || eq.kind === "distributor";
|
|
const unitId = eq.unit_id ?? null;
|
|
|
|
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" data-unit-id="${unitId || ""}"></div>` : ""}
|
|
`;
|
|
|
|
let syncBtns = null;
|
|
|
|
if (canControl) {
|
|
const actions = card.querySelector(".ops-eq-card-actions");
|
|
const remPointId = roleMap["rem"]?.point_id ?? null;
|
|
const fltPointId = roleMap["flt"]?.point_id ?? null;
|
|
|
|
const startBtn = document.createElement("button");
|
|
startBtn.className = "secondary";
|
|
startBtn.textContent = "启动";
|
|
startBtn.addEventListener("click", () =>
|
|
apiFetch(`/api/control/equipment/${eq.id}/start`, { method: "POST" }).catch(() => {})
|
|
);
|
|
const stopBtn = document.createElement("button");
|
|
stopBtn.className = "danger";
|
|
stopBtn.textContent = "停止";
|
|
stopBtn.addEventListener("click", () =>
|
|
apiFetch(`/api/control/equipment/${eq.id}/stop`, { method: "POST" }).catch(() => {})
|
|
);
|
|
actions.append(startBtn, stopBtn);
|
|
|
|
syncBtns = function () {
|
|
const autoOn = !!(unitId && state.runtimes.get(unitId)?.auto_enabled);
|
|
const remSig = remPointId ? state.opsSignalCache.get(remPointId) : null;
|
|
const fltSig = fltPointId ? state.opsSignalCache.get(fltPointId) : null;
|
|
const remOk = !remPointId || isSignalOn(remSig?.quality, remSig?.value_text);
|
|
const fltActive = !!(fltPointId && isSignalOn(fltSig?.quality, fltSig?.value_text));
|
|
const disabled = autoOn || !remOk || fltActive;
|
|
const title = autoOn ? "自动控制运行中,请先停止自动"
|
|
: !remOk ? "设备未切换至远程模式"
|
|
: fltActive ? "设备故障中"
|
|
: "";
|
|
startBtn.disabled = disabled;
|
|
stopBtn.disabled = disabled;
|
|
startBtn.title = title;
|
|
stopBtn.title = title;
|
|
};
|
|
}
|
|
|
|
dom.opsEquipmentArea.appendChild(card);
|
|
|
|
// Register pills for WS updates; seed signal cache from initial point_monitor data
|
|
SIGNAL_ROLES.forEach((role) => {
|
|
const point = roleMap[role];
|
|
if (!point) return;
|
|
const pillEl = card.querySelector(`[data-ops-dot="${point.point_id}"]`);
|
|
if (!pillEl) return;
|
|
if (point.point_monitor) {
|
|
const m = point.point_monitor;
|
|
state.opsSignalCache.set(point.point_id, { quality: m.quality, value_text: m.value_text });
|
|
pillEl.className = sigPillClass(role, m.quality, m.value_text);
|
|
}
|
|
const isSyncRole = canControl && (role === "rem" || role === "flt");
|
|
state.opsPointEls.set(point.point_id, { pillEl, syncBtns: isSyncRole ? syncBtns : null });
|
|
});
|
|
|
|
if (canControl) {
|
|
syncBtns();
|
|
if (unitId) {
|
|
if (!state.opsUnitSyncFns.has(unitId)) state.opsUnitSyncFns.set(unitId, new Set());
|
|
state.opsUnitSyncFns.get(unitId).add(syncBtns);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
export function startOps() {
|
|
renderOpsUnits();
|
|
|
|
dom.batchStartAutoBtn?.addEventListener("click", () => {
|
|
apiFetch("/api/control/unit/batch-start-auto", { method: "POST" })
|
|
.then(() => loadUnits())
|
|
.catch(() => {});
|
|
});
|
|
|
|
dom.batchStopAutoBtn?.addEventListener("click", () => {
|
|
apiFetch("/api/control/unit/batch-stop-auto", { method: "POST" })
|
|
.then(() => loadUnits())
|
|
.catch(() => {});
|
|
});
|
|
}
|
|
|
|
/** Called by WS handler when a unit's runtime changes — re-evaluates all equipment button states. */
|
|
export function syncEquipmentButtonsForUnit(unitId) {
|
|
state.opsUnitSyncFns.get(unitId)?.forEach((fn) => fn());
|
|
}
|