plc_control/web/feeder/js/points.js

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();
}