feat(scada): align map asset fields

This commit is contained in:
2026-07-15 18:30:12 +08:00
parent 16ecb69d00
commit 055335e404
11 changed files with 153 additions and 71 deletions
@@ -1,8 +1,9 @@
"use client";
import { Target, TriangleAlert, X } from "lucide-react";
import { Check, Copy, Target, TriangleAlert, X } from "lucide-react";
import Image from "next/image";
import { useMemo } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { showMapNotice } from "@/features/map/core/components/notice";
import { cn } from "@/lib/utils";
import { SCADA_ICON_PATHS } from "../map/scada";
import type { DetailFeature } from "../types";
@@ -17,6 +18,15 @@ type FeatureInsightPanelProps = {
};
export function FeatureInsightPanel({ feature, onClose }: FeatureInsightPanelProps) {
const [copiedScadaUuid, setCopiedScadaUuid] = useState(false);
const copyResetTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => () => {
if (copyResetTimeoutRef.current) {
clearTimeout(copyResetTimeoutRef.current);
}
}, []);
const metrics = useMemo(() => {
if (!feature) {
return [];
@@ -60,7 +70,7 @@ export function FeatureInsightPanel({ feature, onClose }: FeatureInsightPanelPro
];
case "scada":
return [
["设备类型", formatFeaturePropertyValue("device_type_name", feature.properties.device_type_name)],
["设备类型", formatFeaturePropertyValue("kind", feature.properties.kind)],
["运行状态", formatFeaturePropertyValue("active", feature.properties.active)],
["关联检查井", formatFeaturePropertyValue("junction_id", feature.properties.junction_id)],
["同步时间", formatFeaturePropertyValue("synced_at", feature.properties.synced_at)]
@@ -73,9 +83,43 @@ export function FeatureInsightPanel({ feature, onClose }: FeatureInsightPanelPro
return [];
}
return getLocalizedFeatureProperties(feature.properties);
const properties = feature.layer === "scada" && feature.properties.scada_id === undefined
? { scada_id: feature.id, ...feature.properties }
: feature.properties;
return getLocalizedFeatureProperties(properties);
}, [feature]);
const scadaQueryUuid = feature?.layer === "scada"
? getCopyablePropertyValue(feature.properties.scada_id ?? feature.id)
: null;
const handleCopyScadaUuid = async () => {
if (!scadaQueryUuid) {
return;
}
try {
if (!navigator.clipboard) {
throw new Error("Clipboard API unavailable");
}
await navigator.clipboard.writeText(scadaQueryUuid);
setCopiedScadaUuid(true);
if (copyResetTimeoutRef.current) {
clearTimeout(copyResetTimeoutRef.current);
}
copyResetTimeoutRef.current = setTimeout(() => setCopiedScadaUuid(false), 1500);
} catch {
showMapNotice({
tone: "error",
title: "无法复制 UUID",
message: "请手动选择查询 UUID 后复制。"
});
}
};
return (
<aside className="acrylic-panel pointer-events-auto flex h-full min-h-0 flex-col rounded border">
<div className="border-b border-slate-200/80 p-4">
@@ -114,14 +158,33 @@ export function FeatureInsightPanel({ feature, onClose }: FeatureInsightPanelPro
</div>
{feature.layer === "scada" ? (
<section className={cn("mt-4 rounded border p-3", getScadaPresentation(feature.properties.device_type_id).sectionClassName)}>
<section className={cn("mt-4 rounded border p-3", getScadaPresentation(feature.properties.kind).sectionClassName)}>
<div className="flex items-center gap-3">
<Image src={getScadaPresentation(feature.properties.device_type_id).iconPath} alt="" width={42} height={42} />
<Image src={getScadaPresentation(feature.properties.kind).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>
<p className={cn("truncate text-sm font-semibold", getScadaPresentation(feature.properties.kind).labelClassName)}>{formatFeaturePropertyValue("name", feature.properties.name)}</p>
<p className="mt-0.5 truncate text-xs text-slate-500 tabular-nums">{formatFeaturePropertyValue("external_id", feature.properties.external_id)}</p>
</div>
</div>
{scadaQueryUuid ? (
<div className="mt-3 rounded border border-white/70 bg-white/65 px-3 py-2 shadow-sm">
<div className="flex items-center justify-between gap-2">
<p className="text-xs font-semibold text-slate-500"> UUID</p>
<button
type="button"
aria-label={copiedScadaUuid ? "编号 UUID 已复制" : "复制编号 UUID"}
title={copiedScadaUuid ? "编号 UUID 已复制" : "复制编号 UUID"}
onClick={() => void handleCopyScadaUuid()}
className="relative grid h-8 w-8 shrink-0 place-items-center rounded border border-slate-200/80 bg-white/80 text-slate-500 transition-[background-color,color,transform] duration-150 hover:text-slate-950 active:scale-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600/25"
>
<Copy size={14} aria-hidden="true" className={cn("absolute transition-[opacity,transform] duration-150", copiedScadaUuid ? "scale-90 opacity-0" : "scale-100 opacity-100")} />
<Check size={15} aria-hidden="true" className={cn("absolute text-emerald-600 transition-[opacity,transform] duration-150", copiedScadaUuid ? "scale-100 opacity-100" : "scale-90 opacity-0")} />
</button>
</div>
<p className="mt-1 break-all font-mono text-xs font-semibold leading-5 text-slate-900">{scadaQueryUuid}</p>
<p className="mt-1 text-xs leading-5 text-slate-500">SCADA asset_ids</p>
</div>
) : null}
<p className="mt-2 text-sm leading-6 text-slate-700">线</p>
</section>
) : null}
@@ -141,8 +204,11 @@ export function FeatureInsightPanel({ feature, onClose }: FeatureInsightPanelPro
<dl className="surface-reading mt-2 divide-y divide-slate-100 rounded border">
{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>
<dt className="truncate text-slate-500">{feature.layer === "scada" && entry.key === "scada_id" ? "编号 UUID" : entry.label}</dt>
<dd className={cn(
"font-medium text-slate-800",
feature.layer === "scada" && entry.key === "scada_id" ? "break-all font-mono text-xs leading-5" : "truncate"
)}>{entry.value}</dd>
</div>
))}
</dl>
@@ -160,14 +226,27 @@ export function FeatureInsightPanel({ feature, onClose }: FeatureInsightPanelPro
</div>
)}
</div>
<span className="sr-only" aria-live="polite">{copiedScadaUuid ? "编号 UUID 已复制" : ""}</span>
</aside>
);
}
function getScadaPresentation(rawTypeId: unknown) {
const typeId = Number(rawTypeId);
if (typeId === 72) return { iconPath: SCADA_ICON_PATHS.waterQuality, sectionClassName: "material-tone-normal border-teal-200", labelClassName: "text-teal-800" };
if (typeId === 58) return { iconPath: SCADA_ICON_PATHS.radarLevel, sectionClassName: "material-tone-info border-blue-200", labelClassName: "text-blue-800" };
if (typeId === 45) return { iconPath: SCADA_ICON_PATHS.ultrasonicFlow, sectionClassName: "material-tone-warning border-orange-200", labelClassName: "text-orange-800" };
function getCopyablePropertyValue(value: unknown) {
if (typeof value === "string" && value.trim().length > 0) {
return value.trim();
}
if (typeof value === "number" || typeof value === "bigint") {
return String(value);
}
return null;
}
function getScadaPresentation(rawKind: unknown) {
const kind = String(rawKind ?? "");
if (kind === "water_quality") return { iconPath: SCADA_ICON_PATHS.waterQuality, sectionClassName: "material-tone-normal border-teal-200", labelClassName: "text-teal-800" };
if (kind === "radar_level") return { iconPath: SCADA_ICON_PATHS.radarLevel, sectionClassName: "material-tone-info border-blue-200", labelClassName: "text-blue-800" };
if (kind === "ultrasonic_flow") return { iconPath: SCADA_ICON_PATHS.ultrasonicFlow, sectionClassName: "material-tone-warning border-orange-200", labelClassName: "text-orange-800" };
return { iconPath: SCADA_ICON_PATHS.unknown, sectionClassName: "surface-control border-transparent", labelClassName: "text-slate-800" };
}
@@ -210,11 +210,12 @@ type ScadaFeaturePopoverProps = {
function ScadaFeaturePopover({ feature, copied, canCopy, onCopy, onClose }: ScadaFeaturePopoverProps) {
const properties = feature.properties;
const presentation = getScadaPresentation(properties.device_type_id);
const presentation = getScadaPresentation(properties.kind);
const active = properties.active === true || String(properties.active).toLowerCase() === "true";
const pointId = formatFeaturePropertyValue("point_external_id", properties.point_external_id ?? feature.id);
const scadaId = formatFeaturePropertyValue("scada_id", properties.scada_id ?? feature.id);
const attributes = [
["设备编号", formatFeaturePropertyValue("device_external_id", properties.device_external_id)],
["站点编号", formatFeaturePropertyValue("site_external_id", properties.site_external_id)],
["设备编号", formatFeaturePropertyValue("external_id", properties.external_id)],
["关联检查井", formatFeaturePropertyValue("junction_id", properties.junction_id)],
["同步时间", formatFeaturePropertyValue("synced_at", properties.synced_at)]
];
@@ -233,8 +234,8 @@ function ScadaFeaturePopover({ feature, copied, canCopy, onCopy, onClose }: Scad
<Image src={presentation.iconPath} alt="" width={48} height={48} className="drop-shadow-sm" />
<div className="min-w-0">
<div className="flex min-w-0 flex-nowrap items-center gap-1.5">
<span className={cn("min-w-0 truncate text-xs font-semibold", presentation.labelClassName)} title={formatFeaturePropertyValue("device_type_name", properties.device_type_name)}>
{formatFeaturePropertyValue("device_type_name", properties.device_type_name)}
<span className={cn("min-w-0 truncate text-xs font-semibold", presentation.labelClassName)} title={formatFeaturePropertyValue("kind", properties.kind)}>
{formatFeaturePropertyValue("kind", properties.kind)}
</span>
<span className={cn(
"inline-flex shrink-0 items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-semibold ring-1 ring-inset",
@@ -246,9 +247,9 @@ function ScadaFeaturePopover({ feature, copied, canCopy, onCopy, onClose }: Scad
</span>
</div>
<h3 className="mt-1 truncate text-base font-semibold text-slate-950">
{formatFeaturePropertyValue("point_name", properties.point_name)}
{formatFeaturePropertyValue("name", properties.name)}
</h3>
<p className="mt-0.5 truncate text-xs text-slate-600 tabular-nums"> {pointId}</p>
<p className="mt-0.5 truncate text-xs text-slate-600 tabular-nums"> UUID {scadaId}</p>
</div>
<div className="flex items-center gap-0.5">
{canCopy ? (
@@ -292,10 +293,10 @@ function ScadaFeaturePopover({ feature, copied, canCopy, onCopy, onClose }: Scad
);
}
function getScadaPresentation(rawTypeId: unknown) {
const typeId = Number(rawTypeId);
if (typeId === 72) return { iconPath: SCADA_ICON_PATHS.waterQuality, headerClassName: "bg-teal-50/70", labelClassName: "text-teal-800" };
if (typeId === 58) return { iconPath: SCADA_ICON_PATHS.radarLevel, headerClassName: "bg-blue-50/70", labelClassName: "text-blue-800" };
if (typeId === 45) return { iconPath: SCADA_ICON_PATHS.ultrasonicFlow, headerClassName: "bg-orange-50/70", labelClassName: "text-orange-800" };
function getScadaPresentation(rawKind: unknown) {
const kind = String(rawKind ?? "");
if (kind === "water_quality") return { iconPath: SCADA_ICON_PATHS.waterQuality, headerClassName: "bg-teal-50/70", labelClassName: "text-teal-800" };
if (kind === "radar_level") return { iconPath: SCADA_ICON_PATHS.radarLevel, headerClassName: "bg-blue-50/70", labelClassName: "text-blue-800" };
if (kind === "ultrasonic_flow") return { iconPath: SCADA_ICON_PATHS.ultrasonicFlow, headerClassName: "bg-orange-50/70", labelClassName: "text-orange-800" };
return { iconPath: SCADA_ICON_PATHS.unknown, headerClassName: "bg-slate-50/70", labelClassName: "text-slate-700" };
}