364 lines
11 KiB
JavaScript
364 lines
11 KiB
JavaScript
import { apiFetch } from "./api.js";
|
|
import { openChart } from "./chart.js";
|
|
import { dom } from "./dom.js";
|
|
import {
|
|
loadEquipments,
|
|
renderBatchBindingDefaults,
|
|
renderBindingEquipmentOptions,
|
|
} from "./equipment.js";
|
|
import { renderRoleOptions } from "./roles.js";
|
|
import { state } from "./state.js";
|
|
|
|
function updatePointSourceNodeCount() {
|
|
const count = dom.nodeTree.querySelectorAll("details").length;
|
|
dom.pointSourceNodeCount.textContent = `节点: ${count}`;
|
|
}
|
|
|
|
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} 个节点`;
|
|
}
|
|
|
|
export function updateSelectedPointSummary() {
|
|
const count = state.selectedPointIds.size;
|
|
dom.selectedPointCount.textContent = `已选中 ${count} 个点位`;
|
|
dom.batchBindingSummary.textContent = `已选中 ${count} 个点位`;
|
|
dom.openBatchBindingBtn.disabled = count === 0;
|
|
}
|
|
|
|
export function updatePointFilterSummary() {
|
|
const filters = [];
|
|
if (state.selectedEquipmentId) {
|
|
const equipment = state.equipmentMap.get(state.selectedEquipmentId);
|
|
filters.push(`设备:${equipment?.name || equipment?.code || "未知"}`);
|
|
}
|
|
if (state.selectedSourceId) {
|
|
const source = state.sources.find((item) => item.id === state.selectedSourceId);
|
|
filters.push(`数据源:${source?.name || "未知"}`);
|
|
}
|
|
|
|
dom.pointFilterSummary.textContent = filters.length
|
|
? `当前筛选: ${filters.join(" / ")}`
|
|
: "当前筛选: 全部点位";
|
|
}
|
|
|
|
export function clearSelectedPoints() {
|
|
state.selectedPointIds.clear();
|
|
dom.toggleAllPoints.checked = false;
|
|
dom.pointList
|
|
.querySelectorAll('input[data-point-select="true"]')
|
|
.forEach((input) => (input.checked = false));
|
|
updateSelectedPointSummary();
|
|
}
|
|
|
|
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 function openPointCreateModal() {
|
|
dom.pointModal.classList.remove("hidden");
|
|
if (dom.pointSourceSelect) {
|
|
dom.pointSourceSelect.value = state.selectedSourceId || "";
|
|
}
|
|
dom.nodeTree.innerHTML = '<div class="muted">选择数据源并加载节点</div>';
|
|
dom.pointSourceNodeCount.textContent = "节点: 0";
|
|
state.selectedNodeIds.clear();
|
|
renderSelectedNodes();
|
|
}
|
|
|
|
export async function loadTree() {
|
|
const sourceId = dom.pointSourceSelect.value || state.selectedSourceId;
|
|
if (!sourceId) {
|
|
dom.nodeTree.innerHTML = '<div class="muted">请选择数据源</div>';
|
|
dom.pointSourceNodeCount.textContent = "节点: 0";
|
|
return;
|
|
}
|
|
|
|
state.selectedSourceId = sourceId;
|
|
const data = await apiFetch(`/api/source/${sourceId}/node-tree`);
|
|
dom.nodeTree.innerHTML = "";
|
|
(data || []).forEach((node) => dom.nodeTree.appendChild(renderNode(node)));
|
|
updatePointSourceNodeCount();
|
|
}
|
|
|
|
export async function browseAndLoadTree() {
|
|
const sourceId = dom.pointSourceSelect.value || state.selectedSourceId;
|
|
if (!sourceId) {
|
|
throw new Error("请先选择数据源");
|
|
}
|
|
|
|
state.selectedSourceId = sourceId;
|
|
await apiFetch(`/api/source/${sourceId}/browse`, { method: "POST" });
|
|
await loadTree();
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
function setPointSelected(pointId, checked) {
|
|
if (checked) {
|
|
state.selectedPointIds.add(pointId);
|
|
} else {
|
|
state.selectedPointIds.delete(pointId);
|
|
}
|
|
updateSelectedPointSummary();
|
|
}
|
|
|
|
export async function loadPoints() {
|
|
const params = new URLSearchParams({
|
|
page: String(state.pointsPage),
|
|
page_size: String(state.pointsPageSize),
|
|
});
|
|
if (state.selectedSourceId) {
|
|
params.set("source_id", state.selectedSourceId);
|
|
}
|
|
if (state.selectedEquipmentId) {
|
|
params.set("equipment_id", state.selectedEquipmentId);
|
|
}
|
|
|
|
const data = await apiFetch(`/api/point?${params.toString()}`);
|
|
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="7" class="empty-state">暂无点位</td></tr>';
|
|
dom.pointsPageInfo.textContent = `${state.pointsPage} / 1`;
|
|
clearSelectedPoints();
|
|
updatePointFilterSummary();
|
|
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></td>
|
|
<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 selectCell = tr.children[0];
|
|
const checkbox = document.createElement("input");
|
|
checkbox.type = "checkbox";
|
|
checkbox.dataset.pointSelect = "true";
|
|
checkbox.checked = state.selectedPointIds.has(point.id);
|
|
checkbox.addEventListener("click", (event) => event.stopPropagation());
|
|
checkbox.addEventListener("change", () => setPointSelected(point.id, checkbox.checked));
|
|
selectCell.appendChild(checkbox);
|
|
|
|
const actionCell = tr.lastElementChild;
|
|
actionCell.className = "point-actions";
|
|
const editBtn = document.createElement("button");
|
|
editBtn.className = "secondary";
|
|
editBtn.textContent = "编辑";
|
|
editBtn.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(editBtn, 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(6) .muted"),
|
|
});
|
|
});
|
|
|
|
const totalPages = Math.max(1, Math.ceil(state.pointsTotal / state.pointsPageSize));
|
|
dom.pointsPageInfo.textContent = `${state.pointsPage} / ${totalPages}`;
|
|
const pageCheckboxes = dom.pointList.querySelectorAll('input[data-point-select="true"]');
|
|
dom.toggleAllPoints.checked =
|
|
pageCheckboxes.length > 0 && Array.from(pageCheckboxes).every((input) => input.checked);
|
|
updateSelectedPointSummary();
|
|
updatePointFilterSummary();
|
|
}
|
|
|
|
export function openPointBinding(point) {
|
|
dom.bindingPointId.value = point.id;
|
|
dom.bindingPointName.value = point.name || "";
|
|
dom.bindingPointName.disabled = false;
|
|
const modalTitle = dom.pointBindingModal.querySelector("h3");
|
|
if (modalTitle) {
|
|
modalTitle.textContent = "编辑点位";
|
|
}
|
|
if (dom.clearPointBindingBtn) {
|
|
dom.clearPointBindingBtn.textContent = "清除设备";
|
|
}
|
|
const saveButton = dom.pointBindingForm?.querySelector('button[type="submit"]');
|
|
if (saveButton) {
|
|
saveButton.textContent = "保存";
|
|
}
|
|
renderBindingEquipmentOptions(point.equipment_id || "");
|
|
dom.bindingSignalRole.innerHTML = renderRoleOptions(point.signal_role || "");
|
|
dom.pointBindingModal.classList.remove("hidden");
|
|
}
|
|
|
|
export async function savePointBinding(event) {
|
|
event.preventDefault();
|
|
|
|
await apiFetch(`/api/point/${dom.bindingPointId.value}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({
|
|
name: dom.bindingPointName.value.trim() || null,
|
|
equipment_id: dom.bindingEquipmentId.value || null,
|
|
signal_role: dom.bindingSignalRole.value || null,
|
|
}),
|
|
});
|
|
|
|
dom.pointBindingModal.classList.add("hidden");
|
|
await loadEquipments();
|
|
await loadPoints();
|
|
}
|
|
|
|
export function openBatchBinding() {
|
|
if (!state.selectedPointIds.size) {
|
|
return;
|
|
}
|
|
renderBatchBindingDefaults();
|
|
updateSelectedPointSummary();
|
|
dom.batchBindingModal.classList.remove("hidden");
|
|
}
|
|
|
|
export async function saveBatchBinding(event) {
|
|
event.preventDefault();
|
|
|
|
if (!state.selectedPointIds.size) {
|
|
return;
|
|
}
|
|
|
|
await apiFetch("/api/point/batch/set-equipment", {
|
|
method: "PUT",
|
|
body: JSON.stringify({
|
|
point_ids: Array.from(state.selectedPointIds),
|
|
equipment_id: dom.batchBindingEquipmentId.value || null,
|
|
signal_role: dom.batchBindingSignalRole.value || null,
|
|
}),
|
|
});
|
|
|
|
dom.batchBindingModal.classList.add("hidden");
|
|
clearSelectedPoints();
|
|
await loadEquipments();
|
|
await loadPoints();
|
|
}
|
|
|
|
export async function clearBatchBinding() {
|
|
if (!state.selectedPointIds.size) {
|
|
return;
|
|
}
|
|
|
|
await apiFetch("/api/point/batch/set-equipment", {
|
|
method: "PUT",
|
|
body: JSON.stringify({
|
|
point_ids: Array.from(state.selectedPointIds),
|
|
equipment_id: null,
|
|
signal_role: null,
|
|
}),
|
|
});
|
|
|
|
dom.batchBindingModal.classList.add("hidden");
|
|
clearSelectedPoints();
|
|
await loadEquipments();
|
|
await loadPoints();
|
|
}
|
|
|
|
export async function deletePoint(pointId) {
|
|
if (!window.confirm("确认删除该点位?")) {
|
|
return;
|
|
}
|
|
|
|
await apiFetch(`/api/point/${pointId}`, { method: "DELETE" });
|
|
state.selectedPointIds.delete(pointId);
|
|
await loadPoints();
|
|
}
|