fix: stabilize agent UI and SCADA card

This commit is contained in:
2026-07-20 17:17:55 +08:00
parent df2fb3ca38
commit 319c604e9d
12 changed files with 408 additions and 700 deletions
@@ -47,6 +47,17 @@ export function JunctionFeatureIcon(props: DrainageFeatureIconProps) {
);
}
export function ScadaFeatureIcon(props: DrainageFeatureIconProps) {
return (
<DrainageIconFrame {...props}>
<circle cx="12" cy="7.5" r="1.75" />
<path d="M8.25 4.25a5.25 5.25 0 0 0 0 6.5M15.75 4.25a5.25 5.25 0 0 1 0 6.5" />
<path d="M5.25 2.25a9 9 0 0 0 0 10.5M18.75 2.25a9 9 0 0 1 0 10.5" />
<path d="M12 9.25v5.25M3.5 18h4l1.5-3 2.75 6 2.5-5 1.25 2h5" />
</DrainageIconFrame>
);
}
export function OrificeFeatureIcon(props: DrainageFeatureIconProps) {
return (
<DrainageIconFrame {...props}>
@@ -1,14 +1,6 @@
"use client";
import {
Check,
Copy,
Database,
MapPin,
Radio,
X
} from "lucide-react";
import Image from "next/image";
import { Check, Copy, Database, X } from "lucide-react";
import type { ComponentType } from "react";
import { useEffect, useRef, useState } from "react";
import {
@@ -17,11 +9,9 @@ import {
} from "@/features/map/core/components/map-control-styles";
import { showMapNotice } from "@/features/map/core/components/notice";
import { cn } from "@/lib/utils";
import { SCADA_ICON_PATHS } from "../map/scada";
import type { WaterNetworkSourceId } from "../map/sources";
import type { DetailFeature } from "../types";
import {
formatFeaturePropertyValue,
getFeaturePanelModel,
type FeaturePanelBadgeTone
} from "../utils/feature-properties";
@@ -31,6 +21,7 @@ import {
OrificeFeatureIcon,
OutfallFeatureIcon,
PumpFeatureIcon,
ScadaFeatureIcon,
type DrainageFeatureIconProps
} from "./drainage-feature-icons";
@@ -54,7 +45,7 @@ const FEATURE_LAYER_META: Record<
orifices: { icon: OrificeFeatureIcon, label: "孔口", headerClassName: "bg-slate-50/70", labelClassName: "text-slate-700", iconClassName: "bg-slate-600 text-white ring-slate-900/10" },
outfalls: { icon: OutfallFeatureIcon, label: "排放口", headerClassName: "bg-blue-50/70", labelClassName: "text-blue-800", iconClassName: "bg-slate-700 text-white ring-slate-900/10" },
pumps: { icon: PumpFeatureIcon, label: "泵", headerClassName: "bg-teal-50/70", labelClassName: "text-teal-800", iconClassName: "bg-cyan-800 text-white ring-cyan-950/10" },
scada: { icon: JunctionFeatureIcon, label: "SCADA 测点", headerClassName: "bg-blue-50/70", labelClassName: "text-blue-800", iconClassName: "bg-blue-700 text-white ring-blue-900/10" }
scada: { icon: ScadaFeatureIcon, label: "SCADA 测点", headerClassName: "bg-cyan-50/70", labelClassName: "text-cyan-800", iconClassName: "bg-cyan-800 text-white ring-cyan-950/10" }
};
const BADGE_CLASS_NAMES: Record<FeaturePanelBadgeTone, string> = {
@@ -116,18 +107,6 @@ export function FeaturePopover({ feature, onClose }: FeaturePopoverProps) {
}
};
if (feature.layer === "scada") {
return (
<ScadaFeaturePopover
feature={feature}
copied={copied}
canCopy={canCopy}
onCopy={() => void handleCopyId()}
onClose={onClose}
/>
);
}
return (
<aside
className={cn(
@@ -199,104 +178,3 @@ export function FeaturePopover({ feature, onClose }: FeaturePopoverProps) {
</aside>
);
}
type ScadaFeaturePopoverProps = {
feature: DetailFeature;
copied: boolean;
canCopy: boolean;
onCopy: () => void;
onClose: () => void;
};
function ScadaFeaturePopover({ feature, copied, canCopy, onCopy, onClose }: ScadaFeaturePopoverProps) {
const properties = feature.properties;
const presentation = getScadaPresentation(properties.kind);
const active = properties.active === true || String(properties.active).toLowerCase() === "true";
const scadaId = formatFeaturePropertyValue("scada_id", properties.scada_id ?? feature.id);
const attributes = [
["站点编号", 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)]
];
return (
<aside
className={cn(
"pointer-events-auto absolute left-1/2 top-[92px] z-20 w-[min(380px,calc(100vw-24px))]",
MAP_FOCUS_SURFACE_CLASS_NAME,
"-translate-x-1/2 overflow-hidden",
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME
)}
>
<header className={cn("px-4 pb-3 pt-4", presentation.headerClassName)}>
<div className="grid grid-cols-[48px_minmax(0,1fr)_auto] items-start gap-3">
<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("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",
active ? "bg-emerald-500/10" : "bg-slate-500/10",
active ? "text-emerald-700 ring-emerald-600/20" : "text-slate-600 ring-slate-500/20"
)}>
<span className={cn("h-1.5 w-1.5 rounded-full", active ? "bg-emerald-500" : "bg-slate-400")} />
{active ? "在线" : "离线"}
</span>
</div>
<h3 className="mt-1 truncate text-base font-semibold text-slate-950">
{formatFeaturePropertyValue("name", properties.name)}
</h3>
<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 ? (
<button type="button" aria-label={copied ? "测点编号已复制" : "复制测点编号"} title={copied ? "测点编号已复制" : "复制测点编号"} onClick={onCopy} className="relative grid h-10 w-10 place-items-center rounded-xl text-slate-500 transition-[background-color,color,transform] duration-150 [@media(hover:hover)]:hover:bg-white/70 [@media(hover:hover)]:hover:text-slate-950 active:scale-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600/25">
<Copy size={15} aria-hidden="true" className={cn("absolute transition-[opacity,transform] duration-150", copied ? "scale-90 opacity-0" : "scale-100 opacity-100")} />
<Check size={16} aria-hidden="true" className={cn("absolute text-emerald-600 transition-[opacity,transform] duration-150", copied ? "scale-100 opacity-100" : "scale-90 opacity-0")} />
</button>
) : null}
<button type="button" aria-label="关闭 SCADA 测点信息" title="关闭 SCADA 测点信息" onClick={onClose} className="grid h-10 w-10 place-items-center rounded-xl text-slate-500 transition-[background-color,color,transform] duration-150 [@media(hover:hover)]:hover:bg-white/70 [@media(hover:hover)]:hover:text-slate-950 active:scale-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600/25">
<X size={16} aria-hidden="true" />
</button>
</div>
</div>
</header>
<div className="px-4 py-3">
<div className="mb-2 flex items-center gap-2 text-xs font-semibold text-slate-500">
<Radio size={14} aria-hidden="true" />
</div>
<dl className="surface-reading divide-y divide-slate-100 rounded-xl px-3">
{attributes.map(([label, value]) => (
<div key={label} className="grid min-h-10 grid-cols-[92px_minmax(0,1fr)] items-baseline gap-3 py-2.5">
<dt className="text-xs font-medium text-slate-500">{label}</dt>
<dd className={cn("break-words text-right text-sm font-semibold text-slate-800 tabular-nums", value === "暂无" && "font-medium text-slate-400")}>{value}</dd>
</div>
))}
</dl>
<div className="surface-reading mt-3 grid grid-cols-[18px_1fr] gap-2 rounded-xl px-3 py-2.5 text-xs leading-5 text-slate-600">
<MapPin size={15} className="mt-0.5 text-slate-500" aria-hidden="true" />
<p></p>
</div>
<div className="mt-2 flex items-center gap-2 px-1 text-xs text-slate-400">
<Database size={13} aria-hidden="true" />
线
</div>
</div>
<span className="sr-only" aria-live="polite">{copied ? "测点编号已复制" : ""}</span>
</aside>
);
}
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" };
}