325 lines
9.8 KiB
JavaScript
325 lines
9.8 KiB
JavaScript
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('<option value="">未绑定单元</option>');
|
|
}
|
|
|
|
state.units.forEach((unit) => {
|
|
const isSelected = unit.id === selected ? "selected" : "";
|
|
options.push(`<option value="${unit.id}" ${isSelected}>${unit.code} / ${unit.name}</option>`);
|
|
});
|
|
|
|
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 '<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 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 `<span class="badge ${cls}">${label}</span>`;
|
|
}
|
|
|
|
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) => `<span class="badge">${e.code}</span>`).join("")
|
|
: '<span class="muted">无设备</span>';
|
|
|
|
card.innerHTML = `
|
|
<div class="row">
|
|
<strong>${unit.code}</strong>
|
|
${runtimeBadge(runtime)}
|
|
<span class="badge ${unit.enabled ? "" : "offline"}">${unit.enabled ? "EN" : "DIS"}</span>
|
|
</div>
|
|
<div>${unit.name}</div>
|
|
<div class="muted">设备 ${bound.length} 台 | 累计 ${runtime ? Math.floor(runtime.display_acc_sec / 1000) : 0}s</div>
|
|
<div class="muted">运行 ${unit.run_time_sec}s / 停止 ${unit.stop_time_sec}s / 累计 ${unit.acc_time_sec}s / 间隔 ${unit.bl_time_sec}s</div>
|
|
${mode === "config" ? `<div class="unit-equipment-tags">${equipTags}</div>` : ""}
|
|
<div class="row unit-card-actions"></div>
|
|
`;
|
|
|
|
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 = "编辑";
|
|
editBtn.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
openEditUnitModal(unit);
|
|
});
|
|
|
|
const deleteBtn = document.createElement("button");
|
|
deleteBtn.className = "danger";
|
|
deleteBtn.textContent = "删除";
|
|
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);
|
|
}
|
|
|
|
return card;
|
|
}
|
|
|
|
function renderToContainer(container, mode) {
|
|
if (!container) return;
|
|
container.innerHTML = "";
|
|
|
|
if (!state.units.length) {
|
|
container.innerHTML = '<div class="list-item"><div class="muted">暂无控制单元</div></div>';
|
|
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("确认删除该单元?")) {
|
|
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 = `<input type="checkbox" ${checked} /><span>${e.code}</span>`;
|
|
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()));
|
|
}
|