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,
|
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)]
|
#[derive(Serialize)]
|
||||||
pub struct EquipmentListItem {
|
pub struct EquipmentListItem {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub equipment: crate::model::Equipment,
|
pub equipment: crate::model::Equipment,
|
||||||
pub point_count: i64,
|
pub point_count: i64,
|
||||||
|
pub role_points: Vec<SignalRolePoint>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_equipment_list(
|
pub async fn get_equipment_list(
|
||||||
|
|
@ -36,7 +44,7 @@ pub async fn get_equipment_list(
|
||||||
query.validate()?;
|
query.validate()?;
|
||||||
|
|
||||||
let total = crate::service::get_equipment_count(&state.pool, query.keyword.as_deref()).await?;
|
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,
|
&state.pool,
|
||||||
query.keyword.as_deref(),
|
query.keyword.as_deref(),
|
||||||
query.pagination.page_size,
|
query.pagination.page_size,
|
||||||
|
|
@ -44,6 +52,38 @@ pub async fn get_equipment_list(
|
||||||
)
|
)
|
||||||
.await?;
|
.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(
|
Ok(Json(PaginatedResponse::new(
|
||||||
data,
|
data,
|
||||||
total,
|
total,
|
||||||
|
|
|
||||||
|
|
@ -343,6 +343,43 @@ pub async fn get_points_by_equipment_ids(
|
||||||
.await
|
.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(
|
pub async fn get_equipment_role_points(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
equipment_id: Uuid,
|
equipment_id: Uuid,
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,7 @@ pub async fn get_equipment_paginated(
|
||||||
updated_at: row.get("updated_at"),
|
updated_at: row.get("updated_at"),
|
||||||
},
|
},
|
||||||
point_count: row.get::<i64, _>("point_count"),
|
point_count: row.get::<i64, _>("point_count"),
|
||||||
|
role_points: vec![],
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -162,11 +162,12 @@ function bindEvents() {
|
||||||
document.addEventListener("equipments-updated", () => {
|
document.addEventListener("equipments-updated", () => {
|
||||||
renderUnits();
|
renderUnits();
|
||||||
renderOpsUnits();
|
renderOpsUnits();
|
||||||
|
if (!state.selectedOpsUnitId) loadAllEquipmentCards();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("units-loaded", () => {
|
document.addEventListener("units-loaded", () => {
|
||||||
renderOpsUnits();
|
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;
|
state.selectedOpsUnitId = unitId === state.selectedOpsUnitId ? null : unitId;
|
||||||
renderOpsUnits();
|
renderOpsUnits();
|
||||||
|
state.opsPointEls.clear();
|
||||||
|
|
||||||
if (!state.selectedOpsUnitId) {
|
if (!state.selectedOpsUnitId) {
|
||||||
await loadAllEquipmentCards();
|
renderOpsEquipments(state.equipments);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dom.opsEquipmentArea.innerHTML = '<div class="muted ops-placeholder">加载中...</div>';
|
const filtered = state.equipments.filter((eq) => eq.unit_id === unitId);
|
||||||
state.opsPointEls.clear();
|
renderOpsEquipments(filtered);
|
||||||
|
|
||||||
const detail = await apiFetch(`/api/unit/${state.selectedOpsUnitId}/detail`);
|
|
||||||
renderOpsEquipments(detail.equipments || []);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadAllEquipmentCards() {
|
export function loadAllEquipmentCards() {
|
||||||
if (!dom.opsEquipmentArea) return;
|
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();
|
state.opsPointEls.clear();
|
||||||
|
renderOpsEquipments(state.equipments);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderOpsEquipments(equipments) {
|
function renderOpsEquipments(equipments) {
|
||||||
|
|
@ -122,10 +109,10 @@ function renderOpsEquipments(equipments) {
|
||||||
const card = document.createElement("div");
|
const card = document.createElement("div");
|
||||||
card.className = "ops-eq-card";
|
card.className = "ops-eq-card";
|
||||||
|
|
||||||
// Build role → point map
|
// Build role → point map from role_points
|
||||||
const roleMap = {};
|
const roleMap = {};
|
||||||
(eq.points || []).forEach((p) => {
|
(eq.role_points || []).forEach((p) => {
|
||||||
if (p.signal_role) roleMap[p.signal_role] = p;
|
roleMap[p.signal_role] = p;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Signal rows HTML (placeholders; WS will fill values)
|
// Signal rows HTML (placeholders; WS will fill values)
|
||||||
|
|
@ -135,7 +122,7 @@ function renderOpsEquipments(equipments) {
|
||||||
return `
|
return `
|
||||||
<div class="ops-signal-row">
|
<div class="ops-signal-row">
|
||||||
<span class="ops-signal-label">${ROLE_LABELS[role] || role}</span>
|
<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>`;
|
</div>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
|
|
||||||
|
|
@ -178,9 +165,9 @@ function renderOpsEquipments(equipments) {
|
||||||
SIGNAL_ROLES.forEach((role) => {
|
SIGNAL_ROLES.forEach((role) => {
|
||||||
const point = roleMap[role];
|
const point = roleMap[role];
|
||||||
if (!point) return;
|
if (!point) return;
|
||||||
const dotEl = card.querySelector(`[data-ops-dot="${point.id}"]`);
|
const dotEl = card.querySelector(`[data-ops-dot="${point.point_id}"]`);
|
||||||
if (dotEl) {
|
if (dotEl) {
|
||||||
state.opsPointEls.set(point.id, { dotEl });
|
state.opsPointEls.set(point.point_id, { dotEl });
|
||||||
if (point.point_monitor) {
|
if (point.point_monitor) {
|
||||||
const m = point.point_monitor;
|
const m = point.point_monitor;
|
||||||
dotEl.className = sigDotClass(role, m.quality, m.value_text);
|
dotEl.className = sigDotClass(role, m.quality, m.value_text);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue