diff --git a/web/core/styles.css b/web/core/styles.css index caa41d7..5025bcc 100644 --- a/web/core/styles.css +++ b/web/core/styles.css @@ -143,6 +143,31 @@ body { .grid-app-config .panel.app-config-main { grid-column: 1; grid-row: 1; } +.unit-config-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 8px; + padding: 8px; + align-content: start; +} + +.unit-config-list .unit-card { + border: 1px solid var(--border); + border-radius: 4px; + padding: 10px; +} + +.unit-config-list .unit-card .unit-equipment-tags { + display: flex; + flex-wrap: wrap; + gap: 4px; + padding-top: 4px; +} + +.unit-config-list .unit-card .unit-equipment-tags .badge { + font-size: 12px; +} + /* config view slot assignments */ .grid-config .panel.top-left { grid-column: 1; grid-row: 1; } .grid-config .panel.top-right { grid-column: 2 / 4; grid-row: 1; } diff --git a/web/feeder/index.html b/web/feeder/index.html index 0412de8..d4ea2c7 100644 --- a/web/feeder/index.html +++ b/web/feeder/index.html @@ -22,6 +22,20 @@
+ +
diff --git a/web/feeder/js/app.js b/web/feeder/js/app.js index dc6b147..c7df84a 100644 --- a/web/feeder/js/app.js +++ b/web/feeder/js/app.js @@ -32,7 +32,7 @@ import { } from "./points.js"; import { state } from "./state.js"; import { loadSources, saveSource } from "./sources.js"; -import { closeUnitModal, loadUnits, openCreateUnitModal, resetUnitForm, renderUnits, saveUnit } from "./units.js"; +import { bindUnitEquipmentModalEvents, closeUnitModal, loadUnits, openCreateUnitModal, resetUnitForm, renderUnits, saveUnit } from "./units.js"; let _configLoaded = false; let _appConfigLoaded = false; @@ -83,7 +83,7 @@ function switchView(view) { if (view === "app-config") { if (!_appConfigLoaded) { _appConfigLoaded = true; - withStatus(loadUnits()); + withStatus(Promise.all([loadUnits(), loadEquipments()])); } } } @@ -185,6 +185,7 @@ function bindEvents() { dom.refreshUnitBtn2.addEventListener("click", () => withStatus(loadUnits().then(loadEvents))); dom.newUnitBtn2.addEventListener("click", openCreateUnitModal); + bindUnitEquipmentModalEvents(); document.addEventListener("equipments-updated", () => { renderUnits(); diff --git a/web/feeder/js/dom.js b/web/feeder/js/dom.js index 633f5ed..7f22f17 100644 --- a/web/feeder/js/dom.js +++ b/web/feeder/js/dom.js @@ -69,6 +69,11 @@ export const dom = { refreshUnitBtn2: byId("refreshUnitBtn2"), newUnitBtn2: byId("newUnitBtn2"), unitConfigList: byId("unitConfigList"), + unitEquipmentModal: byId("unitEquipmentModal"), + unitEquipmentList: byId("unitEquipmentList"), + closeUnitEquipmentModalBtn: byId("closeUnitEquipmentModal"), + cancelUnitEquipmentBtn: byId("cancelUnitEquipment"), + confirmUnitEquipmentBtn: byId("confirmUnitEquipment"), closeUnitModalBtn: byId("closeUnitModal"), closeEquipmentModalBtn: byId("closeEquipmentModal"), refreshEventBtn: byId("refreshEventBtn"), diff --git a/web/feeder/js/units.js b/web/feeder/js/units.js index 30006bb..ebec9a0 100644 --- a/web/feeder/js/units.js +++ b/web/feeder/js/units.js @@ -1,16 +1,26 @@ -import { apiFetch } from "./api.js"; +import { apiFetch, withStatus } from "./api.js"; import { dom } from "./dom.js"; import { loadEvents } from "./events.js"; -import { renderEquipments } from "./equipment.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 = item.equipment || 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; @@ -97,11 +107,17 @@ function runtimeBadge(runtime) { return `${label}`; } -function buildUnitCard(unit, interactive) { +function buildUnitCard(unit, mode) { const card = document.createElement("div"); - const selected = interactive && state.selectedUnitId === unit.id; + 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} @@ -109,12 +125,13 @@ function buildUnitCard(unit, interactive) { ${unit.enabled ? "EN" : "DIS"}
${unit.name}
-
设备 ${equipmentCount(unit.id)} 台 | Acc ${runtime ? Math.floor(runtime.display_acc_sec / 1000) : 0}s
+
设备 ${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 (interactive) { + if (mode === "interactive") { card.addEventListener("click", () => { selectUnit(unit.id).catch((error) => { dom.statusText.textContent = error.message; @@ -144,6 +161,17 @@ function buildUnitCard(unit, interactive) { 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"); @@ -178,7 +206,7 @@ function buildUnitCard(unit, interactive) { return card; } -function renderToContainer(container, interactive) { +function renderToContainer(container, mode) { if (!container) return; container.innerHTML = ""; @@ -188,13 +216,13 @@ function renderToContainer(container, interactive) { } state.units.forEach((unit) => { - container.appendChild(buildUnitCard(unit, interactive)); + container.appendChild(buildUnitCard(unit, mode)); }); } export function renderUnits() { - renderToContainer(dom.unitList, true); - renderToContainer(dom.unitConfigList, false); + renderToContainer(dom.unitList, "interactive"); + renderToContainer(dom.unitConfigList, "config"); } export async function loadUnits() { @@ -257,3 +285,71 @@ export async function deleteUnit(unitId) { 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 = ""; + allEquipments.forEach((e) => { + const row = document.createElement("label"); + row.className = "list-item"; + row.style.cssText = "display:flex;align-items:center;gap:8px;cursor:pointer;padding:6px 10px"; + const checked = bound.has(e.id) ? "checked" : ""; + row.innerHTML = ` ${e.code} / ${e.name}`; + row.querySelector("input").addEventListener("change", (ev) => { + if (ev.target.checked) _unitEquipmentSelected.add(e.id); + else _unitEquipmentSelected.delete(e.id); + }); + dom.unitEquipmentList.appendChild(row); + }); + + 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())); +}