From e304fd342d90c8d4acc14cf04d0eb2172cf14266 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Thu, 26 Mar 2026 09:34:59 +0800 Subject: [PATCH] feat(ops): embed role_points in equipment list, remove unit detail API calls Equipment list response now includes signal-role points with monitor data, so the ops view can render signal dots directly from state.equipments without fetching /api/unit/:id/detail. Co-Authored-By: Claude Sonnet 4.6 --- src/handler/equipment.rs | 42 +++++++++++++++++++++++++++++++++++++++- src/service/control.rs | 37 +++++++++++++++++++++++++++++++++++ src/service/equipment.rs | 1 + web/js/app.js | 3 ++- web/js/ops.js | 39 +++++++++++++------------------------ 5 files changed, 94 insertions(+), 28 deletions(-) diff --git a/src/handler/equipment.rs b/src/handler/equipment.rs index c657a43..3c0ad01 100644 --- a/src/handler/equipment.rs +++ b/src/handler/equipment.rs @@ -22,11 +22,19 @@ pub struct GetEquipmentListQuery { pub pagination: PaginationParams, } +#[derive(Serialize)] +pub struct SignalRolePoint { + pub point_id: uuid::Uuid, + pub signal_role: String, + pub point_monitor: Option, +} + #[derive(Serialize)] pub struct EquipmentListItem { #[serde(flatten)] pub equipment: crate::model::Equipment, pub point_count: i64, + pub role_points: Vec, } pub async fn get_equipment_list( @@ -36,7 +44,7 @@ pub async fn get_equipment_list( query.validate()?; let total = crate::service::get_equipment_count(&state.pool, query.keyword.as_deref()).await?; - let data = crate::service::get_equipment_paginated( + let items = crate::service::get_equipment_paginated( &state.pool, query.keyword.as_deref(), query.pagination.page_size, @@ -44,6 +52,38 @@ pub async fn get_equipment_list( ) .await?; + let equipment_ids: Vec = items.iter().map(|item| item.equipment.id).collect(); + let role_point_rows = + crate::service::get_signal_role_points_batch(&state.pool, &equipment_ids).await?; + + let monitor_guard = state + .connection_manager + .get_point_monitor_data_read_guard() + .await; + + let mut role_points_map: std::collections::HashMap> = + std::collections::HashMap::new(); + for rp in role_point_rows { + role_points_map + .entry(rp.equipment_id) + .or_default() + .push(SignalRolePoint { + point_id: rp.point_id, + signal_role: rp.signal_role, + point_monitor: monitor_guard.get(&rp.point_id).cloned(), + }); + } + + let data = items + .into_iter() + .map(|item| EquipmentListItem { + role_points: role_points_map + .remove(&item.equipment.id) + .unwrap_or_default(), + ..item + }) + .collect::>(); + Ok(Json(PaginatedResponse::new( data, total, diff --git a/src/service/control.rs b/src/service/control.rs index 3455219..fee1d4c 100644 --- a/src/service/control.rs +++ b/src/service/control.rs @@ -343,6 +343,43 @@ pub async fn get_points_by_equipment_ids( .await } +pub struct EquipmentSignalRole { + pub equipment_id: Uuid, + pub point_id: Uuid, + pub signal_role: String, +} + +/// Batch fetch all signal-role points for multiple equipment IDs in one query. +pub async fn get_signal_role_points_batch( + pool: &PgPool, + equipment_ids: &[Uuid], +) -> Result, sqlx::Error> { + if equipment_ids.is_empty() { + return Ok(vec![]); + } + let rows = sqlx::query( + r#" + SELECT p.equipment_id, p.id AS point_id, p.signal_role + FROM point p + WHERE p.equipment_id = ANY($1) + AND p.signal_role IS NOT NULL + ORDER BY p.equipment_id, p.created_at + "#, + ) + .bind(equipment_ids) + .fetch_all(pool) + .await?; + + Ok(rows + .into_iter() + .map(|row| EquipmentSignalRole { + equipment_id: row.get("equipment_id"), + point_id: row.get("point_id"), + signal_role: row.get("signal_role"), + }) + .collect()) +} + pub async fn get_equipment_role_points( pool: &PgPool, equipment_id: Uuid, diff --git a/src/service/equipment.rs b/src/service/equipment.rs index c46d694..a1d6045 100644 --- a/src/service/equipment.rs +++ b/src/service/equipment.rs @@ -139,6 +139,7 @@ pub async fn get_equipment_paginated( updated_at: row.get("updated_at"), }, point_count: row.get::("point_count"), + role_points: vec![], }) .collect()) } diff --git a/web/js/app.js b/web/js/app.js index cbdc1f2..7832d79 100644 --- a/web/js/app.js +++ b/web/js/app.js @@ -162,11 +162,12 @@ function bindEvents() { document.addEventListener("equipments-updated", () => { renderUnits(); renderOpsUnits(); + if (!state.selectedOpsUnitId) loadAllEquipmentCards(); }); document.addEventListener("units-loaded", () => { renderOpsUnits(); - if (!state.selectedOpsUnitId) loadAllEquipmentCards(); + if (state.equipments.length > 0 && !state.selectedOpsUnitId) loadAllEquipmentCards(); }); } diff --git a/web/js/ops.js b/web/js/ops.js index d49172c..8015cb7 100644 --- a/web/js/ops.js +++ b/web/js/ops.js @@ -78,37 +78,24 @@ export function renderOpsUnits() { }); } -async function selectOpsUnit(unitId) { +function selectOpsUnit(unitId) { state.selectedOpsUnitId = unitId === state.selectedOpsUnitId ? null : unitId; renderOpsUnits(); + state.opsPointEls.clear(); if (!state.selectedOpsUnitId) { - await loadAllEquipmentCards(); + renderOpsEquipments(state.equipments); return; } - dom.opsEquipmentArea.innerHTML = '
加载中...
'; - state.opsPointEls.clear(); - - const detail = await apiFetch(`/api/unit/${state.selectedOpsUnitId}/detail`); - renderOpsEquipments(detail.equipments || []); + const filtered = state.equipments.filter((eq) => eq.unit_id === unitId); + renderOpsEquipments(filtered); } -export async function loadAllEquipmentCards() { +export function loadAllEquipmentCards() { if (!dom.opsEquipmentArea) return; - if (!state.units.length) { - dom.opsEquipmentArea.innerHTML = '
暂无控制单元
'; - return; - } - - dom.opsEquipmentArea.innerHTML = '
加载中...
'; state.opsPointEls.clear(); - - const details = await Promise.all( - state.units.map((u) => apiFetch(`/api/unit/${u.id}/detail`).catch(() => ({ equipments: [] }))) - ); - const allEquipments = details.flatMap((d) => d.equipments || []); - renderOpsEquipments(allEquipments); + renderOpsEquipments(state.equipments); } function renderOpsEquipments(equipments) { @@ -122,10 +109,10 @@ function renderOpsEquipments(equipments) { const card = document.createElement("div"); card.className = "ops-eq-card"; - // Build role → point map + // Build role → point map from role_points const roleMap = {}; - (eq.points || []).forEach((p) => { - if (p.signal_role) roleMap[p.signal_role] = p; + (eq.role_points || []).forEach((p) => { + roleMap[p.signal_role] = p; }); // Signal rows HTML (placeholders; WS will fill values) @@ -135,7 +122,7 @@ function renderOpsEquipments(equipments) { return `
${ROLE_LABELS[role] || role} - +
`; }).join(""); @@ -178,9 +165,9 @@ function renderOpsEquipments(equipments) { SIGNAL_ROLES.forEach((role) => { const point = roleMap[role]; if (!point) return; - const dotEl = card.querySelector(`[data-ops-dot="${point.id}"]`); + const dotEl = card.querySelector(`[data-ops-dot="${point.point_id}"]`); if (dotEl) { - state.opsPointEls.set(point.id, { dotEl }); + state.opsPointEls.set(point.point_id, { dotEl }); if (point.point_monitor) { const m = point.point_monitor; dotEl.className = sigDotClass(role, m.quality, m.value_text);