198 lines
5.9 KiB
JavaScript
198 lines
5.9 KiB
JavaScript
import { apiFetch } from "./api.js";
|
|
import { openChart } from "./chart.js";
|
|
import { dom } from "./dom.js";
|
|
import { loadEquipments, renderEquipmentOptions } from "./equipment.js";
|
|
import { state } from "./state.js";
|
|
|
|
export function formatValue(monitor) {
|
|
if (!monitor) {
|
|
return "--";
|
|
}
|
|
if (monitor.value_text) {
|
|
return monitor.value_text;
|
|
}
|
|
if (monitor.value === null || monitor.value === undefined) {
|
|
return "--";
|
|
}
|
|
return typeof monitor.value === "string" ? monitor.value : JSON.stringify(monitor.value);
|
|
}
|
|
|
|
export function renderSelectedNodes() {
|
|
dom.selectedCount.textContent = `已选中 ${state.selectedNodeIds.size} 个节点`;
|
|
}
|
|
|
|
function renderNode(node) {
|
|
const details = document.createElement("details");
|
|
const summary = document.createElement("summary");
|
|
|
|
if (node.children?.length) {
|
|
summary.classList.add("has-children");
|
|
}
|
|
|
|
const checkbox = document.createElement("input");
|
|
checkbox.type = "checkbox";
|
|
checkbox.checked = state.selectedNodeIds.has(node.id);
|
|
checkbox.addEventListener("change", () => {
|
|
if (checkbox.checked) {
|
|
state.selectedNodeIds.add(node.id);
|
|
} else {
|
|
state.selectedNodeIds.delete(node.id);
|
|
}
|
|
renderSelectedNodes();
|
|
});
|
|
|
|
const label = document.createElement("span");
|
|
label.className = "node-label";
|
|
label.textContent = `${node.display_name || node.browse_name} (${node.node_class})`;
|
|
|
|
summary.append(checkbox, label);
|
|
details.appendChild(summary);
|
|
|
|
(node.children || []).forEach((child) => {
|
|
details.appendChild(renderNode(child));
|
|
});
|
|
|
|
return details;
|
|
}
|
|
|
|
export async function loadTree() {
|
|
if (!state.selectedSourceId) {
|
|
dom.nodeTree.innerHTML = '<div class="muted">请选择数据源</div>';
|
|
return;
|
|
}
|
|
|
|
const data = await apiFetch(`/api/source/${state.selectedSourceId}/node-tree`);
|
|
dom.nodeTree.innerHTML = "";
|
|
(data || []).forEach((node) => dom.nodeTree.appendChild(renderNode(node)));
|
|
}
|
|
|
|
export async function createPoints() {
|
|
if (!state.selectedNodeIds.size) {
|
|
return;
|
|
}
|
|
|
|
await apiFetch("/api/point/batch", {
|
|
method: "POST",
|
|
body: JSON.stringify({ node_ids: Array.from(state.selectedNodeIds) }),
|
|
});
|
|
|
|
state.selectedNodeIds.clear();
|
|
renderSelectedNodes();
|
|
dom.pointModal.classList.add("hidden");
|
|
await loadPoints();
|
|
}
|
|
|
|
export async function loadPoints() {
|
|
const sourceQuery = state.selectedSourceId ? `&source_id=${state.selectedSourceId}` : "";
|
|
const data = await apiFetch(
|
|
`/api/point?page=${state.pointsPage}&page_size=${state.pointsPageSize}${sourceQuery}`,
|
|
);
|
|
|
|
const items = data.data || [];
|
|
state.pointsTotal = typeof data.total === "number" ? data.total : items.length;
|
|
state.pointEls.clear();
|
|
dom.pointList.innerHTML = "";
|
|
|
|
if (!items.length) {
|
|
dom.pointList.innerHTML = '<tr><td colspan="6" class="empty-state">暂无点位</td></tr>';
|
|
dom.pointsPageInfo.textContent = `${state.pointsPage} / 1`;
|
|
return;
|
|
}
|
|
|
|
items.forEach((item) => {
|
|
const point = item.point || item;
|
|
const monitor = item.point_monitor || null;
|
|
const equipment = point.equipment_id ? state.equipmentMap.get(point.equipment_id) : null;
|
|
const tr = document.createElement("tr");
|
|
|
|
tr.addEventListener("click", () => {
|
|
openChart(point.id, point.name).catch((error) => {
|
|
dom.statusText.textContent = error.message;
|
|
});
|
|
});
|
|
|
|
tr.innerHTML = `
|
|
<td>
|
|
<div class="point-name">${point.name}</div>
|
|
<div class="point-id">${point.node_id}</div>
|
|
</td>
|
|
<td><span class="point-value">${formatValue(monitor)}</span></td>
|
|
<td><span class="badge quality-${(monitor?.quality || "unknown").toLowerCase()}">${(monitor?.quality || "unknown").toUpperCase()}</span></td>
|
|
<td>
|
|
<div class="point-meta">
|
|
<div>${equipment ? equipment.name : '<span class="muted">未绑定</span>'}</div>
|
|
<div class="point-role">${point.signal_role || "--"}</div>
|
|
</div>
|
|
</td>
|
|
<td><span class="muted">${monitor?.timestamp || "--"}</span></td>
|
|
<td></td>
|
|
`;
|
|
|
|
const actionCell = tr.lastElementChild;
|
|
|
|
const bindBtn = document.createElement("button");
|
|
bindBtn.className = "secondary";
|
|
bindBtn.textContent = "绑定";
|
|
bindBtn.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
openPointBinding(point);
|
|
});
|
|
|
|
const deleteBtn = document.createElement("button");
|
|
deleteBtn.className = "danger";
|
|
deleteBtn.textContent = "删除";
|
|
deleteBtn.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
deletePoint(point.id).catch((error) => {
|
|
dom.statusText.textContent = error.message;
|
|
});
|
|
});
|
|
|
|
actionCell.append(bindBtn, deleteBtn);
|
|
dom.pointList.appendChild(tr);
|
|
|
|
state.pointEls.set(point.id, {
|
|
row: tr,
|
|
value: tr.querySelector(".point-value"),
|
|
quality: tr.querySelector(".badge"),
|
|
time: tr.querySelector("td:nth-child(5) .muted"),
|
|
});
|
|
});
|
|
|
|
const totalPages = Math.max(1, Math.ceil(state.pointsTotal / state.pointsPageSize));
|
|
dom.pointsPageInfo.textContent = `${state.pointsPage} / ${totalPages}`;
|
|
}
|
|
|
|
export function openPointBinding(point) {
|
|
dom.bindingPointId.value = point.id;
|
|
dom.bindingPointName.value = point.name || "";
|
|
dom.bindingSignalRole.value = point.signal_role || "";
|
|
renderEquipmentOptions(point.equipment_id || "");
|
|
dom.pointBindingModal.classList.remove("hidden");
|
|
}
|
|
|
|
export async function savePointBinding(event) {
|
|
event.preventDefault();
|
|
|
|
await apiFetch(`/api/point/${dom.bindingPointId.value}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({
|
|
equipment_id: dom.bindingEquipmentId.value || null,
|
|
signal_role: dom.bindingSignalRole.value.trim() || null,
|
|
}),
|
|
});
|
|
|
|
dom.pointBindingModal.classList.add("hidden");
|
|
await loadEquipments();
|
|
await loadPoints();
|
|
}
|
|
|
|
export async function deletePoint(pointId) {
|
|
if (!window.confirm("确认删除该点位?")) {
|
|
return;
|
|
}
|
|
|
|
await apiFetch(`/api/point/${pointId}`, { method: "DELETE" });
|
|
await loadPoints();
|
|
}
|