Files
next-tjwater-drainage-frontend/features/workbench/components/feature-insight-panel.tsx
T

174 lines
8.4 KiB
TypeScript

"use client";
import { Target, TriangleAlert, X } from "lucide-react";
import Image from "next/image";
import { useMemo } from "react";
import { cn } from "@/lib/utils";
import { SCADA_ICON_PATHS } from "../map/scada";
import type { DetailFeature } from "../types";
import {
formatFeaturePropertyValue,
getLocalizedFeatureProperties
} from "../utils/feature-properties";
type FeatureInsightPanelProps = {
feature: DetailFeature | null;
onClose: () => void;
};
export function FeatureInsightPanel({ feature, onClose }: FeatureInsightPanelProps) {
const metrics = useMemo(() => {
if (!feature) {
return [];
}
switch (feature.layer) {
case "conduits":
return [
["管径", formatFeaturePropertyValue("diameter", feature.properties.diameter)],
["长度", formatFeaturePropertyValue("length", feature.properties.length)],
["粗糙系数", formatFeaturePropertyValue("roughness", feature.properties.roughness)],
["状态", formatFeaturePropertyValue("status", feature.properties.status)]
];
case "junctions":
return [
["最大深度", formatFeaturePropertyValue("max_depth", feature.properties.max_depth)],
["井底高程", formatFeaturePropertyValue("invert_elevation", feature.properties.invert_elevation)],
["对象类型", "检查井"],
["状态", "在线"]
];
case "orifices":
return [
["孔口类型", formatFeaturePropertyValue("orifice_type", feature.properties.orifice_type)],
["断面形状", formatFeaturePropertyValue("shape", feature.properties.shape)],
["流量系数", formatFeaturePropertyValue("discharge_coeff", feature.properties.discharge_coeff)],
["闸门", formatFeaturePropertyValue("gated", feature.properties.gated)]
];
case "pumps":
return [
["状态", formatFeaturePropertyValue("status", feature.properties.status)],
["泵曲线", formatFeaturePropertyValue("pump_curve", feature.properties.pump_curve)],
["启动水深", formatFeaturePropertyValue("startup_depth", feature.properties.startup_depth)],
["停泵水深", formatFeaturePropertyValue("shutoff_depth", feature.properties.shutoff_depth)]
];
case "outfalls":
return [
["排放类型", formatFeaturePropertyValue("outfall_type", feature.properties.outfall_type)],
["井底高程", formatFeaturePropertyValue("invert_elevation", feature.properties.invert_elevation)],
["阶段数据", formatFeaturePropertyValue("stage_data", feature.properties.stage_data)],
["状态", "在线"]
];
case "scada":
return [
["设备类型", formatFeaturePropertyValue("device_type_name", feature.properties.device_type_name)],
["运行状态", formatFeaturePropertyValue("active", feature.properties.active)],
["关联检查井", formatFeaturePropertyValue("junction_id", feature.properties.junction_id)],
["同步时间", formatFeaturePropertyValue("synced_at", feature.properties.synced_at)]
];
}
}, [feature]);
const propertyEntries = useMemo(() => {
if (!feature) {
return [];
}
return getLocalizedFeatureProperties(feature.properties);
}, [feature]);
return (
<aside className="pointer-events-auto flex h-full min-h-0 flex-col rounded border border-white/60 bg-white/88 shadow-glass backdrop-blur">
<div className="border-b border-slate-200/80 p-4">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-xs font-medium tracking-[0.14em] text-slate-500">要素详情</p>
<h2 className="mt-1 text-lg font-semibold text-slate-950">
{feature ? feature.title : "选择一个管网对象"}
</h2>
</div>
<button
type="button"
aria-label="关闭详情"
title="关闭详情"
onClick={onClose}
className="grid h-9 w-9 place-items-center rounded border border-slate-200 bg-white text-slate-500 hover:text-slate-900"
>
<X size={16} aria-hidden="true" />
</button>
</div>
<p className="mt-2 text-sm text-slate-600">
{feature ? feature.subtitle : "点击地图上的管线或节点后,这里会展示属性、SCADA 摘要和 Agent 解释。"}
</p>
</div>
<div className="min-h-0 flex-1 overflow-auto p-4">
{feature ? (
<>
<div className="grid grid-cols-2 gap-2">
{metrics.map(([label, value]) => (
<div key={label} className="rounded border border-slate-200 bg-white p-3">
<p className="text-xs text-slate-500">{label}</p>
<p className="mt-1 truncate text-sm font-semibold text-slate-900">{value}</p>
</div>
))}
</div>
{feature.layer === "scada" ? (
<section className={cn("mt-4 rounded border p-3", getScadaPresentation(feature.properties.device_type_id).sectionClassName)}>
<div className="flex items-center gap-3">
<Image src={getScadaPresentation(feature.properties.device_type_id).iconPath} alt="" width={42} height={42} />
<div className="min-w-0">
<p className={cn("truncate text-sm font-semibold", getScadaPresentation(feature.properties.device_type_id).labelClassName)}>{formatFeaturePropertyValue("point_name", feature.properties.point_name)}</p>
<p className="mt-0.5 truncate text-xs text-slate-500 tabular-nums">{formatFeaturePropertyValue("device_external_id", feature.properties.device_external_id)}</p>
</div>
</div>
<p className="mt-2 text-sm leading-6 text-slate-700">当前点位由管网密度聚类生成,仅用于空间展示。图层暂未提供实时测值或历史曲线。</p>
</section>
) : null}
<section className="mt-4 rounded border border-orange-100 bg-orange-50/70 p-3">
<div className="flex items-center gap-2 text-sm font-semibold text-orange-900">
<TriangleAlert size={16} aria-hidden="true" />
Agent 解释
</div>
<p className="mt-2 text-sm leading-6 text-slate-700">
该对象来自 GeoServer MVT。当前仅执行前端预览,不会写入样式、模型参数或调度工单。
</p>
</section>
<section className="mt-4">
<h3 className="text-sm font-semibold text-slate-900">属性</h3>
<dl className="mt-2 divide-y divide-slate-100 rounded border border-slate-200 bg-white">
{propertyEntries.map((entry) => (
<div key={entry.key} className="grid grid-cols-[110px_1fr] gap-2 px-3 py-2 text-sm">
<dt className="truncate text-slate-500">{entry.label}</dt>
<dd className="truncate font-medium text-slate-800">{entry.value}</dd>
</div>
))}
</dl>
</section>
</>
) : (
<div className="flex h-full flex-col items-center justify-center text-center">
<div className="grid h-12 w-12 place-items-center rounded border border-slate-200 bg-white text-slate-500">
<Target size={22} aria-hidden="true" />
</div>
<p className="mt-3 text-sm font-medium text-slate-800">等待地图选择</p>
<p className="mt-1 max-w-[260px] text-sm leading-6 text-slate-500">
选择要素后,可查看基础属性、模拟入口与 Agent 会话联动结果。
</p>
</div>
)}
</div>
</aside>
);
}
function getScadaPresentation(rawTypeId: unknown) {
const typeId = Number(rawTypeId);
if (typeId === 72) return { iconPath: SCADA_ICON_PATHS.waterQuality, sectionClassName: "border-teal-200 bg-teal-50/70", labelClassName: "text-teal-800" };
if (typeId === 58) return { iconPath: SCADA_ICON_PATHS.radarLevel, sectionClassName: "border-blue-200 bg-blue-50/70", labelClassName: "text-blue-800" };
if (typeId === 45) return { iconPath: SCADA_ICON_PATHS.ultrasonicFlow, sectionClassName: "border-orange-200 bg-orange-50/70", labelClassName: "text-orange-800" };
return { iconPath: SCADA_ICON_PATHS.unknown, sectionClassName: "border-slate-200 bg-slate-50", labelClassName: "text-slate-800" };
}