import { apiFetch, withStatus } from "./api.js"; import { dom } from "./dom.js"; import { loadEvents } from "./events.js"; import { loadEquipments, renderEquipments } from "./equipment.js"; import { state } from "./state.js"; function equipmentOf(item) { return item && item.equipment ? item.equipment : item; } function equipmentCount(unitId) { return state.equipments.filter((item) => { const equipment = equipmentOf(item); return equipment.unit_id === unitId; }).length; } function boundEquipments(unitId) { return state.equipments .map(equipmentOf) .filter((e) => e.unit_id === unitId); } export function renderUnitOptions(selected = "", target = dom.equipmentUnitId, includeEmpty = true) { if (!target) { return; } const options = []; if (includeEmpty) { options.push(''); } state.units.forEach((unit) => { const isSelected = unit.id === selected ? "selected" : ""; options.push(``); }); target.innerHTML = options.join(""); } export function resetUnitForm() { dom.unitForm.reset(); dom.unitId.value = ""; dom.unitEnabled.checked = true; dom.unitManualAck.checked = true; dom.unitRunTimeSec.value = "10"; dom.unitStopTimeSec.value = "10"; dom.unitAccTimeSec.value = "20"; dom.unitBlTimeSec.value = "10"; } function openUnitModal() { dom.unitModal.classList.remove("hidden"); } export function closeUnitModal() { dom.unitModal.classList.add("hidden"); } export function openCreateUnitModal() { resetUnitForm(); openUnitModal(); } function openEditUnitModal(unit) { dom.unitId.value = unit.id || ""; dom.unitCode.value = unit.code || ""; dom.unitName.value = unit.name || ""; dom.unitDescription.value = unit.description || ""; dom.unitEnabled.checked = !!unit.enabled; dom.unitRunTimeSec.value = String(unit.run_time_sec ?? 0); dom.unitStopTimeSec.value = String(unit.stop_time_sec ?? 0); dom.unitAccTimeSec.value = String(unit.acc_time_sec ?? 0); dom.unitBlTimeSec.value = String(unit.bl_time_sec ?? 0); dom.unitManualAck.checked = !!unit.require_manual_ack_after_fault; openUnitModal(); } async function selectUnit(unitId) { state.selectedUnitId = state.selectedUnitId === unitId ? null : unitId; renderUnits(); renderEquipments(); await loadEvents(); } function runtimeBadge(runtime) { if (!runtime) return 'OFFLINE'; if (runtime.comm_locked) return 'COMM ERR'; if (runtime.fault_locked) return 'FAULT'; const stateLabels = { stopped: 'STOPPED', running: 'RUNNING', distributor_running: 'DIST RUN', fault_locked: 'FAULT', comm_locked: 'COMM ERR', }; const stateCls = { stopped: '', running: 'online', distributor_running: 'online', fault_locked: 'danger', comm_locked: 'offline', }; const label = stateLabels[runtime.state] ?? runtime.state; const cls = stateCls[runtime.state] ?? ''; return `${label}`; } function buildUnitCard(unit, mode) { const card = document.createElement("div"); const selected = mode === "interactive" && state.selectedUnitId === unit.id; card.className = `list-item unit-card ${selected ? "selected" : ""}`; const runtime = state.runtimes.get(unit.id); const bound = boundEquipments(unit.id); const equipTags = bound.length ? bound.map((e) => `${e.code}`).join("") : '无设备'; card.innerHTML = `
${unit.code} ${runtimeBadge(runtime)} ${unit.enabled ? "EN" : "DIS"}
${unit.name}
设备 ${bound.length} 台 | Acc ${runtime ? Math.floor(runtime.display_acc_sec / 1000) : 0}s
Run ${unit.run_time_sec}s / Stop ${unit.stop_time_sec}s / Acc ${unit.acc_time_sec}s / BL ${unit.bl_time_sec}s
${mode === "config" ? `
${equipTags}
` : ""}
`; if (mode === "interactive") { card.addEventListener("click", () => { selectUnit(unit.id).catch((error) => { dom.statusText.textContent = error.message; }); }); } const actions = card.querySelector(".unit-card-actions"); const editBtn = document.createElement("button"); editBtn.className = "secondary"; editBtn.textContent = "Edit"; editBtn.addEventListener("click", (event) => { event.stopPropagation(); openEditUnitModal(unit); }); const deleteBtn = document.createElement("button"); deleteBtn.className = "danger"; deleteBtn.textContent = "Delete"; deleteBtn.addEventListener("click", (event) => { event.stopPropagation(); deleteUnit(unit.id).catch((error) => { dom.statusText.textContent = error.message; }); }); actions.append(editBtn, deleteBtn); if (mode === "config") { const selectEquipBtn = document.createElement("button"); selectEquipBtn.className = "secondary"; selectEquipBtn.textContent = "选择设备"; selectEquipBtn.addEventListener("click", (e) => { e.stopPropagation(); openUnitEquipmentModal(unit); }); actions.append(selectEquipBtn); } 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 ? "Stop Auto" : "Start Auto"; autoBtn.disabled = startBlocked; autoBtn.title = startBlocked ? (runtime?.fault_locked ? "设备故障中,无法启动自动控制" : runtime?.rem_local ? "设备处于本地模式(REM关),无法启动自动控制" : "需人工确认故障后才可启动自动控制") : (isAutoOn ? "停止自动控制" : "启动自动控制"); autoBtn.addEventListener("click", (e) => { e.stopPropagation(); const url = `/api/control/unit/${unit.id}/${isAutoOn ? "stop-auto" : "start-auto"}`; apiFetch(url, { 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); } return card; } function renderToContainer(container, mode) { if (!container) return; container.innerHTML = ""; if (!state.units.length) { container.innerHTML = '
暂无控制单元
'; return; } state.units.forEach((unit) => { container.appendChild(buildUnitCard(unit, mode)); }); } export function renderUnits() { renderToContainer(dom.unitList, "interactive"); renderToContainer(dom.unitConfigList, "config"); } export async function loadUnits() { const response = await apiFetch("/api/unit?page=1&page_size=-1"); state.units = response.data || []; state.unitMap = new Map(state.units.map((unit) => [unit.id, unit])); if (state.selectedUnitId && !state.unitMap.has(state.selectedUnitId)) { state.selectedUnitId = null; } state.units.forEach((unit) => { if (unit.runtime) state.runtimes.set(unit.id, unit.runtime); }); renderUnits(); renderUnitOptions(dom.equipmentUnitId?.value || "", dom.equipmentUnitId); document.dispatchEvent(new Event("units-loaded")); } export async function saveUnit(event) { event.preventDefault(); const payload = { code: dom.unitCode.value.trim(), name: dom.unitName.value.trim(), description: dom.unitDescription.value.trim() || null, enabled: dom.unitEnabled.checked, run_time_sec: Number(dom.unitRunTimeSec.value || 0), stop_time_sec: Number(dom.unitStopTimeSec.value || 0), acc_time_sec: Number(dom.unitAccTimeSec.value || 0), bl_time_sec: Number(dom.unitBlTimeSec.value || 0), require_manual_ack_after_fault: dom.unitManualAck.checked, }; const id = dom.unitId.value; await apiFetch(id ? `/api/unit/${id}` : "/api/unit", { method: id ? "PUT" : "POST", body: JSON.stringify(payload), }); closeUnitModal(); await loadUnits(); renderEquipments(); await loadEvents(); } export async function deleteUnit(unitId) { if (!window.confirm("Delete this unit?")) { return; } await apiFetch(`/api/unit/${unitId}`, { method: "DELETE" }); if (state.selectedUnitId === unitId) { state.selectedUnitId = null; } closeUnitModal(); await loadUnits(); renderEquipments(); await loadEvents(); } // ── Unit Equipment Selection Modal ── let _unitEquipmentTargetId = null; const _unitEquipmentSelected = new Set(); function openUnitEquipmentModal(unit) { _unitEquipmentTargetId = unit.id; _unitEquipmentSelected.clear(); const allEquipments = state.equipments.map(equipmentOf); const bound = new Set(boundEquipments(unit.id).map((e) => e.id)); bound.forEach((id) => _unitEquipmentSelected.add(id)); dom.unitEquipmentList.innerHTML = ""; dom.unitEquipmentList.className = "unit-equip-grid"; allEquipments.forEach((e) => { const item = document.createElement("label"); item.className = "unit-equip-item"; const checked = bound.has(e.id) ? "checked" : ""; item.innerHTML = `${e.code}`; item.title = e.name; item.querySelector("input").addEventListener("change", (ev) => { if (ev.target.checked) _unitEquipmentSelected.add(e.id); else _unitEquipmentSelected.delete(e.id); }); dom.unitEquipmentList.appendChild(item); }); dom.unitEquipmentModal.classList.remove("hidden"); } function closeUnitEquipmentModal() { dom.unitEquipmentModal.classList.add("hidden"); _unitEquipmentTargetId = null; } async function confirmUnitEquipment() { if (!_unitEquipmentTargetId) return; const previouslyBound = new Set(boundEquipments(_unitEquipmentTargetId).map((e) => e.id)); const toBind = [..._unitEquipmentSelected].filter((id) => !previouslyBound.has(id)); const toUnbind = [...previouslyBound].filter((id) => !_unitEquipmentSelected.has(id)); if (toBind.length > 0) { await apiFetch("/api/equipment/batch/set-unit", { method: "PUT", body: JSON.stringify({ equipment_ids: toBind, unit_id: _unitEquipmentTargetId }), }); } if (toUnbind.length > 0) { await apiFetch("/api/equipment/batch/set-unit", { method: "PUT", body: JSON.stringify({ equipment_ids: toUnbind, unit_id: null }), }); } closeUnitEquipmentModal(); await loadEquipments(); await loadUnits(); } export function bindUnitEquipmentModalEvents() { dom.closeUnitEquipmentModalBtn.addEventListener("click", closeUnitEquipmentModal); dom.cancelUnitEquipmentBtn.addEventListener("click", closeUnitEquipmentModal); dom.confirmUnitEquipmentBtn.addEventListener("click", () => withStatus(confirmUnitEquipment())); }