feat(web): add unit and event management views
This commit is contained in:
parent
4e3d325437
commit
1f29eb3871
|
|
@ -7,5 +7,11 @@
|
|||
<input id="equipmentKeyword" placeholder="搜索编码或名称" />
|
||||
<button type="button" class="secondary" id="refreshEquipmentBtn">刷新</button>
|
||||
</div>
|
||||
<div class="toolbar equipment-batch-toolbar">
|
||||
<div class="muted" id="selectedEquipmentSummary">已选 0 台设备</div>
|
||||
<select id="equipmentBatchUnitId"></select>
|
||||
<button type="button" class="secondary" id="clearEquipmentSelectionBtn">清空选择</button>
|
||||
<button type="button" id="applyEquipmentUnitBtn">批量设单元</button>
|
||||
</div>
|
||||
<div class="list equipment-list" id="equipmentList"></div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,18 @@
|
|||
<section class="panel bottom-middle">
|
||||
<div class="stack-panel">
|
||||
<div class="stack-section event-section">
|
||||
<div class="panel-head">
|
||||
<h2>系统事件</h2>
|
||||
<button type="button" class="secondary" id="refreshEventBtn">刷新</button>
|
||||
</div>
|
||||
<div class="list event-list" id="eventList"></div>
|
||||
</div>
|
||||
|
||||
<div class="stack-section stack-section-bordered log-section">
|
||||
<div class="panel-head">
|
||||
<h2>实时日志</h2>
|
||||
</div>
|
||||
<div class="log" id="logView"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,55 @@
|
|||
<div class="modal hidden" id="unitModal">
|
||||
<div class="modal-content modal-sm">
|
||||
<div class="modal-head">
|
||||
<h3>控制单元配置</h3>
|
||||
<button class="secondary" id="closeUnitModal">X</button>
|
||||
</div>
|
||||
<form id="unitForm" class="form">
|
||||
<input type="hidden" id="unitId" />
|
||||
<label>
|
||||
编码
|
||||
<input id="unitCode" required />
|
||||
</label>
|
||||
<label>
|
||||
名称
|
||||
<input id="unitName" required />
|
||||
</label>
|
||||
<label>
|
||||
说明
|
||||
<input id="unitDescription" />
|
||||
</label>
|
||||
<label class="check-row">
|
||||
<input type="checkbox" id="unitEnabled" checked />
|
||||
<span>启用</span>
|
||||
</label>
|
||||
<label>
|
||||
投煤运行时间(秒)
|
||||
<input id="unitRunTimeSec" type="number" min="0" value="0" />
|
||||
</label>
|
||||
<label>
|
||||
投煤停止时间(秒)
|
||||
<input id="unitStopTimeSec" type="number" min="0" value="0" />
|
||||
</label>
|
||||
<label>
|
||||
投煤累计阈值(秒)
|
||||
<input id="unitAccTimeSec" type="number" min="0" value="0" />
|
||||
</label>
|
||||
<label>
|
||||
布料机运行时间(秒)
|
||||
<input id="unitBlTimeSec" type="number" min="0" value="0" />
|
||||
</label>
|
||||
<label class="check-row">
|
||||
<input type="checkbox" id="unitManualAck" checked />
|
||||
<span>故障恢复后需人工确认</span>
|
||||
</label>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="secondary" id="unitReset">清空</button>
|
||||
<button type="submit" id="unitSubmit">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal hidden" id="equipmentModal">
|
||||
<div class="modal-content modal-sm">
|
||||
<div class="modal-head">
|
||||
|
|
@ -6,6 +58,10 @@
|
|||
</div>
|
||||
<form id="equipmentForm" class="form">
|
||||
<input type="hidden" id="equipmentId" />
|
||||
<label>
|
||||
所属单元
|
||||
<select id="equipmentUnitId"></select>
|
||||
</label>
|
||||
<label>
|
||||
编码
|
||||
<input id="equipmentCode" required />
|
||||
|
|
@ -45,7 +101,7 @@
|
|||
<div class="tree" id="nodeTree"></div>
|
||||
<div class="modal-foot">
|
||||
<div class="muted" id="selectedCount">已选中 0 个节点</div>
|
||||
<button id="createPoints">创建点位</button>
|
||||
<button id="createPoints">创建设备点位</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,22 @@
|
|||
<section class="panel bottom-left">
|
||||
<div class="stack-panel">
|
||||
<div class="stack-section">
|
||||
<div class="panel-head">
|
||||
<h2>控制单元</h2>
|
||||
<div class="toolbar">
|
||||
<button type="button" class="secondary" id="refreshUnitBtn">刷新</button>
|
||||
<button type="button" id="newUnitBtn">+ 新增</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list unit-list" id="unitList"></div>
|
||||
</div>
|
||||
|
||||
<div class="stack-section stack-section-bordered">
|
||||
<div class="panel-head">
|
||||
<h2>数据源</h2>
|
||||
<button id="openSourceForm">+ 新增</button>
|
||||
<button type="button" id="openSourceForm">+ 新增</button>
|
||||
</div>
|
||||
<div class="source-panels" id="sourceList"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@ import { withStatus } from "./api.js";
|
|||
import { openChart, renderChart } from "./chart.js";
|
||||
import { dom } from "./dom.js";
|
||||
import { closeApiDocDrawer, openApiDocDrawer } from "./docs.js";
|
||||
import { loadEvents } from "./events.js";
|
||||
import {
|
||||
applyBatchEquipmentUnit,
|
||||
clearEquipmentFilter,
|
||||
clearPointBinding,
|
||||
clearSelectedEquipments,
|
||||
closeEquipmentModal,
|
||||
loadEquipments,
|
||||
openCreateEquipmentModal,
|
||||
|
|
@ -29,19 +32,28 @@ 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";
|
||||
|
||||
function bindEvents() {
|
||||
dom.unitForm.addEventListener("submit", (event) => withStatus(saveUnit(event)));
|
||||
dom.sourceForm.addEventListener("submit", (event) => withStatus(saveSource(event)));
|
||||
dom.equipmentForm.addEventListener("submit", (event) => withStatus(saveEquipment(event)));
|
||||
dom.pointBindingForm.addEventListener("submit", (event) => withStatus(savePointBinding(event)));
|
||||
dom.batchBindingForm.addEventListener("submit", (event) => withStatus(saveBatchBinding(event)));
|
||||
|
||||
dom.unitResetBtn.addEventListener("click", resetUnitForm);
|
||||
dom.refreshUnitBtn.addEventListener("click", () => withStatus(loadUnits().then(loadEvents)));
|
||||
dom.newUnitBtn.addEventListener("click", openCreateUnitModal);
|
||||
dom.closeUnitModalBtn.addEventListener("click", closeUnitModal);
|
||||
|
||||
dom.sourceResetBtn.addEventListener("click", () => dom.sourceForm.reset());
|
||||
dom.equipmentResetBtn.addEventListener("click", resetEquipmentForm);
|
||||
dom.refreshEquipmentBtn.addEventListener("click", () => withStatus(loadEquipments()));
|
||||
dom.newEquipmentBtn.addEventListener("click", openCreateEquipmentModal);
|
||||
dom.closeEquipmentModalBtn.addEventListener("click", closeEquipmentModal);
|
||||
dom.clearEquipmentFilterBtn.addEventListener("click", () => withStatus(clearEquipmentFilter()));
|
||||
dom.applyEquipmentUnitBtn.addEventListener("click", () => withStatus(applyBatchEquipmentUnit()));
|
||||
dom.clearEquipmentSelectionBtn.addEventListener("click", clearSelectedEquipments);
|
||||
|
||||
dom.openPointModalBtn.addEventListener("click", openPointCreateModal);
|
||||
dom.pointSourceSelect.addEventListener("change", () => {
|
||||
|
|
@ -82,6 +94,7 @@ function bindEvents() {
|
|||
|
||||
dom.openApiDocBtn.addEventListener("click", () => withStatus(openApiDocDrawer()));
|
||||
dom.closeApiDocBtn.addEventListener("click", closeApiDocDrawer);
|
||||
dom.refreshEventBtn.addEventListener("click", () => withStatus(loadEvents()));
|
||||
|
||||
dom.refreshChartBtn.addEventListener("click", () => {
|
||||
if (!state.chartPointId) {
|
||||
|
|
@ -111,6 +124,10 @@ function bindEvents() {
|
|||
withStatus(loadEquipments());
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("equipments-updated", () => {
|
||||
renderUnits();
|
||||
});
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
|
|
@ -122,8 +139,10 @@ async function bootstrap() {
|
|||
startLogs();
|
||||
startPointSocket();
|
||||
|
||||
await withStatus(loadUnits());
|
||||
await withStatus(loadSources());
|
||||
await withStatus(loadEquipments());
|
||||
await withStatus(loadEvents());
|
||||
await withStatus(loadPoints());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ const byId = (id) => document.getElementById(id);
|
|||
export const dom = {
|
||||
statusText: byId("statusText"),
|
||||
sourceList: byId("sourceList"),
|
||||
unitList: byId("unitList"),
|
||||
eventList: byId("eventList"),
|
||||
nodeTree: byId("nodeTree"),
|
||||
pointList: byId("pointList"),
|
||||
pointsPageInfo: byId("pointsPageInfo"),
|
||||
|
|
@ -18,11 +20,24 @@ export const dom = {
|
|||
chartTitle: byId("chartTitle"),
|
||||
chartSummary: byId("chartSummary"),
|
||||
pointModal: byId("pointModal"),
|
||||
unitModal: byId("unitModal"),
|
||||
sourceModal: byId("sourceModal"),
|
||||
equipmentModal: byId("equipmentModal"),
|
||||
pointBindingModal: byId("pointBindingModal"),
|
||||
batchBindingModal: byId("batchBindingModal"),
|
||||
apiDocDrawer: byId("apiDocDrawer"),
|
||||
unitForm: byId("unitForm"),
|
||||
unitId: byId("unitId"),
|
||||
unitCode: byId("unitCode"),
|
||||
unitName: byId("unitName"),
|
||||
unitDescription: byId("unitDescription"),
|
||||
unitEnabled: byId("unitEnabled"),
|
||||
unitRunTimeSec: byId("unitRunTimeSec"),
|
||||
unitStopTimeSec: byId("unitStopTimeSec"),
|
||||
unitAccTimeSec: byId("unitAccTimeSec"),
|
||||
unitBlTimeSec: byId("unitBlTimeSec"),
|
||||
unitManualAck: byId("unitManualAck"),
|
||||
unitResetBtn: byId("unitReset"),
|
||||
sourceForm: byId("sourceForm"),
|
||||
sourceId: byId("sourceId"),
|
||||
sourceName: byId("sourceName"),
|
||||
|
|
@ -31,14 +46,23 @@ export const dom = {
|
|||
sourceResetBtn: byId("sourceReset"),
|
||||
equipmentForm: byId("equipmentForm"),
|
||||
equipmentId: byId("equipmentId"),
|
||||
equipmentUnitId: byId("equipmentUnitId"),
|
||||
equipmentCode: byId("equipmentCode"),
|
||||
equipmentName: byId("equipmentName"),
|
||||
equipmentKind: byId("equipmentKind"),
|
||||
equipmentDescription: byId("equipmentDescription"),
|
||||
equipmentResetBtn: byId("equipmentReset"),
|
||||
equipmentKeyword: byId("equipmentKeyword"),
|
||||
equipmentBatchUnitId: byId("equipmentBatchUnitId"),
|
||||
selectedEquipmentSummary: byId("selectedEquipmentSummary"),
|
||||
equipmentList: byId("equipmentList"),
|
||||
refreshUnitBtn: byId("refreshUnitBtn"),
|
||||
newUnitBtn: byId("newUnitBtn"),
|
||||
closeUnitModalBtn: byId("closeUnitModal"),
|
||||
closeEquipmentModalBtn: byId("closeEquipmentModal"),
|
||||
refreshEventBtn: byId("refreshEventBtn"),
|
||||
applyEquipmentUnitBtn: byId("applyEquipmentUnitBtn"),
|
||||
clearEquipmentSelectionBtn: byId("clearEquipmentSelectionBtn"),
|
||||
pointBindingForm: byId("pointBindingForm"),
|
||||
bindingPointId: byId("bindingPointId"),
|
||||
bindingPointName: byId("bindingPointName"),
|
||||
|
|
|
|||
|
|
@ -8,9 +8,61 @@ function equipmentOf(item) {
|
|||
return item && item.equipment ? item.equipment : item;
|
||||
}
|
||||
|
||||
function currentUnitLabel(unitId) {
|
||||
if (!unitId) {
|
||||
return "未绑定单元";
|
||||
}
|
||||
const unit = state.unitMap.get(unitId);
|
||||
return unit ? `${unit.code} / ${unit.name}` : "未知单元";
|
||||
}
|
||||
|
||||
function filteredEquipments() {
|
||||
if (!state.selectedUnitId) {
|
||||
return state.equipments;
|
||||
}
|
||||
|
||||
return state.equipments.filter((item) => {
|
||||
const equipment = equipmentOf(item);
|
||||
return equipment.unit_id === state.selectedUnitId;
|
||||
});
|
||||
}
|
||||
|
||||
function renderEquipmentUnitOptions(selected = "", target = dom.equipmentUnitId) {
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = ['<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("");
|
||||
}
|
||||
|
||||
function renderBatchUnitOptions(selected = "") {
|
||||
if (!dom.equipmentBatchUnitId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = ['<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>`);
|
||||
});
|
||||
dom.equipmentBatchUnitId.innerHTML = options.join("");
|
||||
}
|
||||
|
||||
function updateSelectedEquipmentSummary() {
|
||||
if (!dom.selectedEquipmentSummary) {
|
||||
return;
|
||||
}
|
||||
dom.selectedEquipmentSummary.textContent = `已选 ${state.selectedEquipmentIds.size} 台设备`;
|
||||
}
|
||||
|
||||
export function renderBindingEquipmentOptions(selected = "", target = dom.bindingEquipmentId) {
|
||||
const options = ['<option value="">Unbound</option>'];
|
||||
state.equipments.forEach((item) => {
|
||||
filteredEquipments().forEach((item) => {
|
||||
const equipment = equipmentOf(item);
|
||||
const isSelected = equipment.id === selected ? "selected" : "";
|
||||
options.push(
|
||||
|
|
@ -28,6 +80,7 @@ export function renderBatchBindingDefaults() {
|
|||
export function resetEquipmentForm() {
|
||||
dom.equipmentForm.reset();
|
||||
dom.equipmentId.value = "";
|
||||
renderEquipmentUnitOptions("");
|
||||
}
|
||||
|
||||
function openEquipmentModal() {
|
||||
|
|
@ -45,6 +98,7 @@ export function openCreateEquipmentModal() {
|
|||
|
||||
function openEditEquipmentModal(equipment) {
|
||||
dom.equipmentId.value = equipment.id || "";
|
||||
dom.equipmentUnitId.value = equipment.unit_id || "";
|
||||
dom.equipmentCode.value = equipment.code || "";
|
||||
dom.equipmentName.value = equipment.name || "";
|
||||
dom.equipmentKind.value = equipment.kind || "";
|
||||
|
|
@ -61,6 +115,21 @@ async function selectEquipment(equipmentId) {
|
|||
await loadPoints();
|
||||
}
|
||||
|
||||
function toggleEquipmentSelection(equipmentId, checked) {
|
||||
if (checked) {
|
||||
state.selectedEquipmentIds.add(equipmentId);
|
||||
} else {
|
||||
state.selectedEquipmentIds.delete(equipmentId);
|
||||
}
|
||||
updateSelectedEquipmentSummary();
|
||||
}
|
||||
|
||||
export function clearSelectedEquipments() {
|
||||
state.selectedEquipmentIds.clear();
|
||||
renderEquipments();
|
||||
updateSelectedEquipmentSummary();
|
||||
}
|
||||
|
||||
export function clearEquipmentFilter() {
|
||||
state.selectedEquipmentId = null;
|
||||
state.pointsPage = 1;
|
||||
|
|
@ -71,29 +140,37 @@ export function clearEquipmentFilter() {
|
|||
|
||||
export function renderEquipments() {
|
||||
dom.equipmentList.innerHTML = "";
|
||||
updateSelectedEquipmentSummary();
|
||||
|
||||
const activeEquipment = state.selectedEquipmentId
|
||||
? state.equipmentMap.get(state.selectedEquipmentId) || null
|
||||
: null;
|
||||
dom.clearEquipmentFilterBtn.textContent = activeEquipment
|
||||
? `设备筛选: ${activeEquipment.name}`
|
||||
: "设备筛选: 全部";
|
||||
? `设备筛选 ${activeEquipment.name}`
|
||||
: "设备筛选 全部";
|
||||
|
||||
if (!state.equipments.length) {
|
||||
const items = filteredEquipments();
|
||||
if (!items.length) {
|
||||
dom.equipmentList.innerHTML = '<div class="list-item"><div class="muted">No equipment</div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
state.equipments.forEach((item) => {
|
||||
items.forEach((item) => {
|
||||
const equipment = equipmentOf(item);
|
||||
const box = document.createElement("div");
|
||||
box.className = `list-item equipment-card ${state.selectedEquipmentId === equipment.id ? "selected" : ""}`;
|
||||
box.innerHTML = `
|
||||
<label class="equipment-select-row">
|
||||
<input type="checkbox" data-equipment-select="true" ${state.selectedEquipmentIds.has(equipment.id) ? "checked" : ""} />
|
||||
<span class="muted">批量选择</span>
|
||||
</label>
|
||||
<div class="row">
|
||||
<strong>${equipment.code}</strong>
|
||||
<span class="badge">${item.point_count ?? 0} pts</span>
|
||||
</div>
|
||||
<div>${equipment.name}</div>
|
||||
<div class="muted">${equipment.kind || "No type"}</div>
|
||||
<div class="muted">单元: ${currentUnitLabel(equipment.unit_id)}</div>
|
||||
<div class="row equipment-card-actions"></div>
|
||||
`;
|
||||
|
||||
|
|
@ -103,6 +180,14 @@ export function renderEquipments() {
|
|||
});
|
||||
});
|
||||
|
||||
const checkbox = box.querySelector('input[data-equipment-select="true"]');
|
||||
checkbox.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
});
|
||||
checkbox.addEventListener("change", (event) => {
|
||||
toggleEquipmentSelection(equipment.id, event.target.checked);
|
||||
});
|
||||
|
||||
const actionRow = box.querySelector(".equipment-card-actions");
|
||||
|
||||
const editBtn = document.createElement("button");
|
||||
|
|
@ -141,6 +226,15 @@ export async function loadEquipments() {
|
|||
return [equipment.id, equipment];
|
||||
}),
|
||||
);
|
||||
|
||||
state.selectedEquipmentIds.forEach((id) => {
|
||||
if (!state.equipmentMap.has(id)) {
|
||||
state.selectedEquipmentIds.delete(id);
|
||||
}
|
||||
});
|
||||
|
||||
renderEquipmentUnitOptions(dom.equipmentUnitId?.value || "");
|
||||
renderBatchUnitOptions(dom.equipmentBatchUnitId?.value || "");
|
||||
renderBindingEquipmentOptions();
|
||||
renderBatchBindingDefaults();
|
||||
if (state.selectedEquipmentId && !state.equipmentMap.has(state.selectedEquipmentId)) {
|
||||
|
|
@ -148,12 +242,15 @@ export async function loadEquipments() {
|
|||
}
|
||||
renderEquipments();
|
||||
updatePointFilterSummary();
|
||||
document.dispatchEvent(new Event("equipments-updated"));
|
||||
}
|
||||
|
||||
export async function saveEquipment(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const unitId = dom.equipmentUnitId.value || null;
|
||||
const payload = {
|
||||
unit_id: unitId,
|
||||
code: dom.equipmentCode.value.trim(),
|
||||
name: dom.equipmentName.value.trim(),
|
||||
kind: dom.equipmentKind.value.trim() || null,
|
||||
|
|
@ -176,6 +273,25 @@ export async function saveEquipment(event) {
|
|||
await loadPoints();
|
||||
}
|
||||
|
||||
export async function applyBatchEquipmentUnit() {
|
||||
if (!state.selectedEquipmentIds.size) {
|
||||
throw new Error("请先选择设备");
|
||||
}
|
||||
|
||||
const value = dom.equipmentBatchUnitId.value;
|
||||
await apiFetch("/api/equipment/batch/set-unit", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
equipment_ids: Array.from(state.selectedEquipmentIds),
|
||||
unit_id: value || null,
|
||||
}),
|
||||
});
|
||||
|
||||
clearSelectedEquipments();
|
||||
renderBatchUnitOptions("");
|
||||
await loadEquipments();
|
||||
}
|
||||
|
||||
export async function deleteEquipment(equipmentId) {
|
||||
if (!window.confirm("Delete this equipment?")) {
|
||||
return;
|
||||
|
|
@ -185,6 +301,7 @@ export async function deleteEquipment(equipmentId) {
|
|||
if (state.selectedEquipmentId === equipmentId) {
|
||||
state.selectedEquipmentId = null;
|
||||
}
|
||||
state.selectedEquipmentIds.delete(equipmentId);
|
||||
resetEquipmentForm();
|
||||
closeEquipmentModal();
|
||||
clearSelectedPoints();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
import { apiFetch } from "./api.js";
|
||||
import { dom } from "./dom.js";
|
||||
import { state } from "./state.js";
|
||||
|
||||
function formatTime(value) {
|
||||
if (!value) {
|
||||
return "--";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function renderEvents() {
|
||||
dom.eventList.innerHTML = "";
|
||||
|
||||
if (!state.events.length) {
|
||||
dom.eventList.innerHTML = '<div class="list-item"><div class="muted">暂无事件</div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
state.events.forEach((item) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "list-item event-card";
|
||||
row.innerHTML = `
|
||||
<div class="row">
|
||||
<strong>${item.event_type}</strong>
|
||||
<span class="badge">${(item.level || "info").toUpperCase()}</span>
|
||||
</div>
|
||||
<div>${item.message}</div>
|
||||
<div class="muted">${formatTime(item.created_at)}</div>
|
||||
`;
|
||||
dom.eventList.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadEvents() {
|
||||
const params = new URLSearchParams({
|
||||
page: "1",
|
||||
page_size: "20",
|
||||
});
|
||||
|
||||
if (state.selectedUnitId) {
|
||||
params.set("unit_id", state.selectedUnitId);
|
||||
}
|
||||
|
||||
const response = await apiFetch(`/api/event?${params.toString()}`);
|
||||
state.events = response.data || [];
|
||||
renderEvents();
|
||||
}
|
||||
|
|
@ -1,8 +1,13 @@
|
|||
export const state = {
|
||||
units: [],
|
||||
unitMap: new Map(),
|
||||
selectedUnitId: null,
|
||||
sources: [],
|
||||
events: [],
|
||||
equipments: [],
|
||||
equipmentMap: new Map(),
|
||||
selectedEquipmentId: null,
|
||||
selectedEquipmentIds: new Set(),
|
||||
selectedSourceId: null,
|
||||
selectedNodeIds: new Set(),
|
||||
selectedPointIds: new Set(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,185 @@
|
|||
import { apiFetch } from "./api.js";
|
||||
import { dom } from "./dom.js";
|
||||
import { loadEvents } from "./events.js";
|
||||
import { renderEquipments } from "./equipment.js";
|
||||
import { state } from "./state.js";
|
||||
|
||||
function equipmentCount(unitId) {
|
||||
return state.equipments.filter((item) => {
|
||||
const equipment = item.equipment || item;
|
||||
return equipment.unit_id === unitId;
|
||||
}).length;
|
||||
}
|
||||
|
||||
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 = "0";
|
||||
dom.unitStopTimeSec.value = "0";
|
||||
dom.unitAccTimeSec.value = "0";
|
||||
dom.unitBlTimeSec.value = "0";
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
export function renderUnits() {
|
||||
dom.unitList.innerHTML = "";
|
||||
|
||||
if (!state.units.length) {
|
||||
dom.unitList.innerHTML = '<div class="list-item"><div class="muted">暂无控制单元</div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
state.units.forEach((unit) => {
|
||||
const card = document.createElement("div");
|
||||
const selected = state.selectedUnitId === unit.id;
|
||||
card.className = `list-item unit-card ${selected ? "selected" : ""}`;
|
||||
card.innerHTML = `
|
||||
<div class="row">
|
||||
<strong>${unit.code}</strong>
|
||||
<span class="badge ${unit.enabled ? "" : "offline"}">${unit.enabled ? "ENABLED" : "DISABLED"}</span>
|
||||
</div>
|
||||
<div>${unit.name}</div>
|
||||
<div class="muted">设备 ${equipmentCount(unit.id)} 台</div>
|
||||
<div class="muted">Run ${unit.run_time_sec}s / Stop ${unit.stop_time_sec}s / Acc ${unit.acc_time_sec}s / BL ${unit.bl_time_sec}s</div>
|
||||
<div class="row unit-card-actions"></div>
|
||||
`;
|
||||
|
||||
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);
|
||||
dom.unitList.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
renderUnits();
|
||||
renderUnitOptions(dom.equipmentUnitId?.value || "", dom.equipmentUnitId);
|
||||
renderUnitOptions(dom.equipmentBatchUnitId?.value || "", dom.equipmentBatchUnitId);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
|
@ -101,6 +101,24 @@ body {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stack-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.stack-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.stack-section-bordered {
|
||||
border-top: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
/* ── Panel Header ───────────────────────────────── */
|
||||
|
||||
.panel-head {
|
||||
|
|
@ -408,6 +426,25 @@ button.danger:hover { background: var(--danger-hover); }
|
|||
background: var(--surface);
|
||||
}
|
||||
|
||||
.equipment-batch-toolbar {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.equipment-batch-toolbar .muted {
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.equipment-batch-toolbar select {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
/* ── Form ─────────────────────────────────────────── */
|
||||
|
||||
.form {
|
||||
|
|
@ -623,6 +660,43 @@ button.danger:hover { background: var(--danger-hover); }
|
|||
max-height: 50vh;
|
||||
}
|
||||
|
||||
.unit-list,
|
||||
.event-list {
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.unit-card-actions {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.event-card {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.event-card:hover {
|
||||
background: var(--surface);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.event-section {
|
||||
flex-basis: 42%;
|
||||
}
|
||||
|
||||
.log-section {
|
||||
flex-basis: 58%;
|
||||
}
|
||||
|
||||
.equipment-select-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.equipment-select-row input {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.drawer-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
|
|
|||
Loading…
Reference in New Issue