import { apiFetch } from "./api.js";
import { dom } from "./dom.js";
import { formatValue } from "./points.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" };
export function sigDotClass(role, quality, valueText) {
if (!quality || quality.toLowerCase() !== "good") return "sig-dot sig-warn";
const v = String(valueText ?? "").trim().toLowerCase();
const on = v === "1" || v === "true" || v === "on";
if (!on) return "sig-dot";
return role === "flt" ? "sig-dot sig-fault" : "sig-dot sig-on";
}
function runtimeBadge(runtime) {
if (!runtime) return 'OFFLINE';
if (runtime.comm_locked) return 'COMM ERR';
if (runtime.fault_locked) return 'FAULT';
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 `${labels[runtime.state] ?? runtime.state}`;
}
export function renderOpsUnits() {
if (!dom.opsUnitList) return;
dom.opsUnitList.innerHTML = "";
if (!state.units.length) {
dom.opsUnitList.innerHTML = '
暂无控制单元
';
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 = `
${unit.code} / ${unit.name}
${runtimeBadge(runtime)}
${unit.enabled ? "EN" : "DIS"}
${runtime ? `Acc ${Math.floor(runtime.display_acc_sec / 1000)}s` : ""}
`;
item.addEventListener("click", () => selectOpsUnit(unit.id));
const actions = item.querySelector(".ops-unit-item-actions");
const isAutoOn = runtime?.auto_enabled;
const autoBtn = document.createElement("button");
autoBtn.className = isAutoOn ? "danger" : "secondary";
autoBtn.textContent = isAutoOn ? "Stop Auto" : "Start Auto";
autoBtn.title = 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 = "Ack Fault";
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);
});
}
async function selectOpsUnit(unitId) {
state.selectedOpsUnitId = unitId === state.selectedOpsUnitId ? null : unitId;
renderOpsUnits();
if (!state.selectedOpsUnitId) {
await loadAllEquipmentCards();
return;
}
dom.opsEquipmentArea.innerHTML = '加载中...
';
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 = '暂无控制单元
';
return;
}
dom.opsEquipmentArea.innerHTML = '加载中...
';
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 = '该单元下暂无设备
';
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 `
${ROLE_LABELS[role] || role}
`;
}).join("");
const canControl = eq.kind === "coal_feeder" || eq.kind === "distributor";
card.innerHTML = `
${eq.code}
${eq.kind || "--"}
${signalRowsHtml || '无绑定信号'}
${canControl ? `` : ""}
`;
if (canControl) {
const actions = card.querySelector(".ops-eq-card-actions");
const autoOn = !!(eq.unit_id && state.runtimes.get(eq.unit_id)?.auto_enabled);
const startBtn = document.createElement("button");
startBtn.className = "secondary";
startBtn.textContent = "Start";
startBtn.disabled = autoOn;
startBtn.title = autoOn ? "自动控制运行中,请先停止自动" : "";
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.disabled = autoOn;
stopBtn.title = autoOn ? "自动控制运行中,请先停止自动" : "";
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 dotEl = card.querySelector(`[data-ops-dot="${point.id}"]`);
if (dotEl) {
state.opsPointEls.set(point.id, { dotEl });
if (point.point_monitor) {
const m = point.point_monitor;
dotEl.className = sigDotClass(role, m.quality, m.value_text);
}
}
});
});
}
export function startOps() {
renderOpsUnits();
loadAllEquipmentCards();
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 — syncs manual button disabled state. */
export function syncEquipmentButtonsForUnit(unitId, autoEnabled) {
if (!dom.opsEquipmentArea) return;
dom.opsEquipmentArea
.querySelectorAll(`.ops-eq-card-actions[data-unit-id="${unitId}"]`)
.forEach((actions) => {
actions.querySelectorAll("button").forEach((btn) => {
btn.disabled = autoEnabled;
btn.title = autoEnabled ? "自动控制运行中,请先停止自动" : "";
});
});
}