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 <noreply@anthropic.com>
This commit is contained in:
parent
0e8d194a70
commit
e304fd342d
|
|
@ -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<crate::telemetry::PointMonitorInfo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct EquipmentListItem {
|
||||
#[serde(flatten)]
|
||||
pub equipment: crate::model::Equipment,
|
||||
pub point_count: i64,
|
||||
pub role_points: Vec<SignalRolePoint>,
|
||||
}
|
||||
|
||||
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<uuid::Uuid> = 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<uuid::Uuid, Vec<SignalRolePoint>> =
|
||||
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::<Vec<_>>();
|
||||
|
||||
Ok(Json(PaginatedResponse::new(
|
||||
data,
|
||||
total,
|
||||
|
|
|
|||
|
|
@ -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<Vec<EquipmentSignalRole>, 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,
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ pub async fn get_equipment_paginated(
|
|||
updated_at: row.get("updated_at"),
|
||||
},
|
||||
point_count: row.get::<i64, _>("point_count"),
|
||||
role_points: vec![],
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = '<div class="muted ops-placeholder">加载中...</div>';
|
||||
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 = '<div class="muted ops-placeholder">暂无控制单元</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
dom.opsEquipmentArea.innerHTML = '<div class="muted ops-placeholder">加载中...</div>';
|
||||
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 `
|
||||
<div class="ops-signal-row">
|
||||
<span class="ops-signal-label">${ROLE_LABELS[role] || role}</span>
|
||||
<span class="sig-dot sig-warn" data-ops-dot="${point.id}" data-ops-role="${role}"></span>
|
||||
<span class="sig-dot sig-warn" data-ops-dot="${point.point_id}" data-ops-role="${role}"></span>
|
||||
</div>`;
|
||||
}).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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue