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 '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 startBlocked = !isAutoOn && (runtime?.fault_locked || runtime?.manual_ack_required); const autoBtn = document.createElement("button"); autoBtn.className = isAutoOn ? "danger" : "secondary"; autoBtn.textContent = isAutoOn ? "Stop Auto" : "Start Auto"; autoBtn.disabled = startBlocked; autoBtn.title = startBlocked ? (runtime?.fault_locked ? "设备故障中,无法启动自动控制" : "需人工确认故障后才可启动自动控制") : (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); }); } 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 = '
该单元下暂无设备
'; 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 `${ROLE_LABELS[role] || role}`; }).join(""); const canControl = eq.kind === "coal_feeder" || eq.kind === "distributor"; const unitId = eq.unit_id ?? null; card.innerHTML = `
${eq.code} ${eq.kind || "--"}
${signalRowsHtml || '无绑定信号'}
${canControl ? `
` : ""} `; 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 = "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); 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()); }