feat: initialize TJWater WebGIS frontend
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
"use client";
|
||||
|
||||
import { Check, Copy, Database, X } from "lucide-react";
|
||||
import type { ComponentType } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
MAP_FOCUS_SURFACE_CLASS_NAME,
|
||||
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME
|
||||
} from "@/features/map/core/components/map-control-styles";
|
||||
import { showMapNotice } from "@/features/map/core/components/notice";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { WaterNetworkSourceId } from "../map/sources";
|
||||
import type { DetailFeature } from "../types";
|
||||
import {
|
||||
getFeaturePanelModel,
|
||||
type FeaturePanelBadgeTone
|
||||
} from "../utils/feature-properties";
|
||||
import {
|
||||
ConduitFeatureIcon,
|
||||
JunctionFeatureIcon,
|
||||
PumpFeatureIcon,
|
||||
ReservoirFeatureIcon,
|
||||
ScadaFeatureIcon,
|
||||
ValveFeatureIcon,
|
||||
type DrainageFeatureIconProps
|
||||
} from "./drainage-feature-icons";
|
||||
import { FeaturePropertyList } from "./feature-property-list";
|
||||
|
||||
type FeaturePopoverProps = {
|
||||
feature: DetailFeature | null;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const FEATURE_LAYER_META: Record<
|
||||
WaterNetworkSourceId,
|
||||
{
|
||||
icon: ComponentType<DrainageFeatureIconProps>;
|
||||
label: string;
|
||||
headerClassName: string;
|
||||
labelClassName: string;
|
||||
iconClassName: string;
|
||||
}
|
||||
> = {
|
||||
pipes: { icon: ConduitFeatureIcon, label: "管线", headerClassName: "bg-teal-50/70", labelClassName: "text-teal-800", iconClassName: "bg-teal-700 text-white ring-teal-900/10" },
|
||||
junctions: { icon: JunctionFeatureIcon, label: "节点", headerClassName: "bg-blue-50/70", labelClassName: "text-blue-800", iconClassName: "bg-sky-700 text-white ring-sky-900/10" },
|
||||
valves: { icon: ValveFeatureIcon, label: "阀门", headerClassName: "bg-violet-50/70", labelClassName: "text-violet-800", iconClassName: "bg-violet-600 text-white ring-violet-900/10" },
|
||||
reservoirs: { icon: ReservoirFeatureIcon, label: "水库", headerClassName: "bg-sky-50/70", labelClassName: "text-sky-900", iconClassName: "bg-sky-800 text-white ring-sky-900/10" },
|
||||
scada: { icon: ScadaFeatureIcon, label: "SCADA", headerClassName: "bg-slate-50/80", labelClassName: "text-slate-800", iconClassName: "bg-slate-700 text-white ring-slate-900/10" },
|
||||
pumps: { icon: PumpFeatureIcon, label: "泵", headerClassName: "bg-cyan-50/70", labelClassName: "text-cyan-800", iconClassName: "bg-cyan-800 text-white ring-cyan-900/10" },
|
||||
tanks: { icon: ReservoirFeatureIcon, label: "水箱", headerClassName: "bg-teal-50/70", labelClassName: "text-teal-800", iconClassName: "bg-teal-700 text-white ring-teal-900/10" }
|
||||
};
|
||||
|
||||
const BADGE_CLASS_NAMES: Record<FeaturePanelBadgeTone, string> = {
|
||||
active: "bg-emerald-50 text-emerald-700 ring-emerald-600/15",
|
||||
inactive: "bg-slate-100 text-slate-600 ring-slate-500/15",
|
||||
warning: "bg-orange-50 text-orange-700 ring-orange-600/15",
|
||||
critical: "bg-red-50 text-red-700 ring-red-600/15",
|
||||
info: "bg-blue-50 text-blue-700 ring-blue-600/15"
|
||||
};
|
||||
|
||||
const BADGE_DOT_CLASS_NAMES: Record<FeaturePanelBadgeTone, string> = {
|
||||
active: "bg-emerald-500",
|
||||
inactive: "bg-slate-400",
|
||||
warning: "bg-orange-500",
|
||||
critical: "bg-red-500",
|
||||
info: "bg-blue-500"
|
||||
};
|
||||
|
||||
export function FeaturePopover({ feature, onClose }: FeaturePopoverProps) {
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||
const copyResetTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (copyResetTimeoutRef.current) {
|
||||
clearTimeout(copyResetTimeoutRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!feature) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const panelModel = getFeaturePanelModel(feature.layer, feature.properties, feature.id);
|
||||
const layerMeta = FEATURE_LAYER_META[feature.layer];
|
||||
const LayerIcon = layerMeta.icon;
|
||||
const copied = copiedId === panelModel.id;
|
||||
const canCopy = panelModel.id !== "暂无";
|
||||
|
||||
const handleCopyId = async () => {
|
||||
try {
|
||||
if (!navigator.clipboard) {
|
||||
throw new Error("Clipboard API unavailable");
|
||||
}
|
||||
|
||||
await navigator.clipboard.writeText(panelModel.id);
|
||||
setCopiedId(panelModel.id);
|
||||
|
||||
if (copyResetTimeoutRef.current) {
|
||||
clearTimeout(copyResetTimeoutRef.current);
|
||||
}
|
||||
|
||||
copyResetTimeoutRef.current = setTimeout(() => setCopiedId(null), 1500);
|
||||
} catch {
|
||||
showMapNotice({
|
||||
tone: "error",
|
||||
title: "无法复制编号",
|
||||
message: "请手动选择编号后复制。"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
"pointer-events-auto absolute left-1/2 top-[92px] z-20 w-[min(380px,calc(100vw-24px))] -translate-x-1/2 overflow-hidden",
|
||||
MAP_FOCUS_SURFACE_CLASS_NAME,
|
||||
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME
|
||||
)}
|
||||
>
|
||||
<header className={cn("px-4 pb-3 pt-4", layerMeta.headerClassName)}>
|
||||
<div className="grid grid-cols-[48px_minmax(0,1fr)_auto] items-start gap-3">
|
||||
<span className={cn("grid h-12 w-12 place-items-center rounded-full ring-1", layerMeta.iconClassName)} aria-hidden="true">
|
||||
<LayerIcon size={24} />
|
||||
</span>
|
||||
|
||||
<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", layerMeta.labelClassName)}>{layerMeta.label}</span>
|
||||
{panelModel.badge ? (
|
||||
<span className={cn("inline-flex shrink-0 items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-semibold leading-4 ring-1 ring-inset", BADGE_CLASS_NAMES[panelModel.badge.tone])}>
|
||||
<span className={cn("h-1.5 w-1.5 rounded-full", BADGE_DOT_CLASS_NAMES[panelModel.badge.tone])} aria-hidden="true" />
|
||||
{panelModel.badge.label}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<h3 className="mt-1 truncate text-base font-semibold text-slate-950">{feature.title}</h3>
|
||||
<p className="mt-0.5 truncate text-xs text-slate-600 tabular-nums">编号 {panelModel.id}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-0.5">
|
||||
{canCopy ? (
|
||||
<button type="button" aria-label={copied ? "编号已复制" : "复制编号"} title={copied ? "编号已复制" : "复制编号"} onClick={() => void handleCopyId()} className="relative grid h-10 w-10 shrink-0 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="关闭要素信息" title="关闭要素信息" onClick={onClose} className="grid h-10 w-10 shrink-0 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">
|
||||
{panelModel.attributes.length > 0 ? (
|
||||
<>
|
||||
<div className="mb-2 flex items-center gap-2 text-xs font-semibold text-slate-500">
|
||||
<Database size={14} aria-hidden="true" />
|
||||
设施属性
|
||||
</div>
|
||||
<FeaturePropertyList entries={panelModel.attributes} variant="popover" />
|
||||
</>
|
||||
) : (
|
||||
<p className="py-3 text-center text-sm text-slate-500">暂无可展示属性</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<span className="sr-only" aria-live="polite">
|
||||
{copied ? "编号已复制" : ""}
|
||||
</span>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user