feat(workbench): integrate runtime data and map tooling

This commit is contained in:
2026-09-22 10:45:46 +08:00
parent bf44cc0f25
commit bebcf2a06c
163 changed files with 15642 additions and 1312 deletions
@@ -1,7 +1,10 @@
import { useEffect, useState } from "react";
import { ChevronDown, Crosshair, RotateCcw, X, ZoomIn } from "lucide-react";
import { Crosshair, RotateCcw, X, ZoomIn } from "lucide-react";
import { Button } from "@/shared/ui/button";
import { Checkbox } from "@/shared/ui/checkbox";
import { cn } from "@/shared/ui/cn";
import { FieldSelect } from "@/shared/ui/field-select";
import { Input } from "@/shared/ui/input";
import {
LAYER_GROUP_GEOMETRIES,
type LayerGroupId,
@@ -16,6 +19,7 @@ import type {
} from "../map/workbench-map-controller";
import {
AVAILABLE_WATER_NETWORK_SOURCE_IDS,
SUPPLY_LAYER_CATALOG,
type AvailableWaterNetworkSourceId
} from "../map/sources";
import type { DetailFeature } from "../types";
@@ -24,18 +28,13 @@ type MapDevPanelProps = {
commands: WorkbenchMapCommands;
controllerState: WorkbenchMapControllerState;
detailFeature: DetailFeature | null;
flowDataSource?: "realtime" | "mock";
onClose: () => void;
};
const GROUP_LABELS: Record<AvailableWaterNetworkSourceId, string> = {
pipes: "管线",
junctions: "节点",
valves: "阀门",
reservoirs: "水库",
scada: "SCADA",
pumps: "水泵",
tanks: "水箱"
};
const GROUP_LABELS = Object.fromEntries(
SUPPLY_LAYER_CATALOG.map((layer) => [layer.id, layer.label])
) as Record<AvailableWaterNetworkSourceId, string>;
const STYLE_GROUP_IDS = Object.keys(LAYER_GROUP_GEOMETRIES) as LayerGroupId[];
@@ -102,6 +101,7 @@ export function MapDevPanel({
commands,
controllerState,
detailFeature,
flowDataSource = "mock",
onClose
}: MapDevPanelProps) {
const [sourceId, setSourceId] = useState<AvailableWaterNetworkSourceId>("pipes");
@@ -130,7 +130,7 @@ export function MapDevPanel({
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") onClose();
if (event.key === "Escape" && !event.defaultPrevented) onClose();
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
@@ -171,8 +171,8 @@ export function MapDevPanel({
const allowedProperties = new Set(
LABEL_PRESETS[nextGroupId].options.map((option) => option.value)
);
const firstNumeric = getNumericFeatureProperties(detailFeature.properties).find(
([name]) => allowedProperties.has(name)
const firstNumeric = getNumericFeatureProperties(detailFeature.properties).find(([name]) =>
allowedProperties.has(name)
);
if (firstNumeric) setProperty(firstNumeric[0]);
}
@@ -262,24 +262,20 @@ export function MapDevPanel({
<PanelSection title="Feature" description="WFS 验证后执行独立高亮或相机定位。">
<div className="grid grid-cols-[120px_1fr] gap-2">
<Field label="业务源">
<select
value={sourceId}
onChange={(event) => {
setSourceId(event.target.value as AvailableWaterNetworkSourceId);
setFeatureId("");
}}
className={inputClassName}
>
{AVAILABLE_WATER_NETWORK_SOURCE_IDS.map((id) => (
<option key={id} value={id}>
{GROUP_LABELS[id]}
</option>
))}
</select>
</Field>
<FieldSelect
label="业务源"
value={sourceId}
onValueChange={(value) => {
setSourceId(value as AvailableWaterNetworkSourceId);
setFeatureId("");
}}
options={AVAILABLE_WATER_NETWORK_SOURCE_IDS.map((id) => ({
value: id,
label: GROUP_LABELS[id]
}))}
/>
<Field label="Feature ID">
<input
<Input
value={featureId}
onChange={(event) => setFeatureId(event.target.value)}
className={inputClassName}
@@ -341,7 +337,9 @@ export function MapDevPanel({
<PanelSection
title="全网水流"
description="开启时为全部管道生成正反向 mock;有符号流速可覆盖单管方向。"
description={flowDataSource === "realtime"
? "已读取后端最近 15 分钟的管道流速;可用有符号流速覆盖单管方向。"
: "后端流速不可用,开启时使用正反向演示流向;有符号流速可覆盖单管方向。"}
>
<div className="grid grid-cols-2 gap-2">
<Button
@@ -364,14 +362,14 @@ export function MapDevPanel({
</div>
<div className="grid grid-cols-2 gap-2">
<Field label="管道 ID">
<input
<Input
value={flowPipeId}
onChange={(event) => setFlowPipeId(event.target.value)}
className={inputClassName}
/>
</Field>
<Field label="有符号流速">
<input
<Input
type="number"
step="any"
value={flowVelocity}
@@ -409,13 +407,13 @@ export function MapDevPanel({
</div>
{flowSummary.total > 0 ? (
<StatusText>
全网 mock · 已赋值 {flowSummary.total} · 正向 {flowSummary.forward} · 反向{" "}
{flowDataSource === "realtime" ? "后端实时流速" : "演示流向"} · 已赋值 {flowSummary.total} · 正向 {flowSummary.forward} · 反向{" "}
{flowSummary.reverse} · 停止 {flowSummary.stopped}
</StatusText>
) : controllerState.flowVisible ? (
<StatusText>全网 mock 已启用 · 正反向交错显示</StatusText>
<StatusText>演示流向已启用 · 正反向交错显示</StatusText>
) : (
<StatusText>开启后自动显示全网 mock 流向</StatusText>
<StatusText>开启后显示演示流向</StatusText>
)}
</PanelSection>
@@ -423,25 +421,12 @@ export function MapDevPanel({
title="样式编辑"
description="统一配置业务几何与整层 MVT 标注,交互语义色保持不变。"
>
<Field label="业务图层组">
<div className="relative">
<select
value={groupId}
onChange={(event) => selectStyleGroup(event.target.value as LayerGroupId)}
className={cn(inputClassName, "appearance-none pr-8")}
>
{STYLE_GROUP_IDS.map((id) => (
<option key={id} value={id}>
{GROUP_LABELS[id]}
</option>
))}
</select>
<ChevronDown
size={14}
className="pointer-events-none absolute right-2.5 top-2.5 text-slate-400"
/>
</div>
</Field>
<FieldSelect
label="业务图层组"
value={groupId}
onValueChange={(value) => selectStyleGroup(value as LayerGroupId)}
options={STYLE_GROUP_IDS.map((id) => ({ value: id, label: GROUP_LABELS[id] }))}
/>
{geometry === "line" ? (
<>
<ColorField
@@ -467,19 +452,16 @@ export function MapDevPanel({
setDraft((current) => ({ ...current, widthMultiplier: value }))
}
/>
<Field label="线型">
<select
value={draft.dash}
onChange={(event) =>
setDraft((current) => ({ ...current, dash: event.target.value }))
}
className={inputClassName}
>
<option value="original">原始</option>
<option value="solid">实线</option>
<option value="dashed">虚线</option>
</select>
</Field>
<FieldSelect
label="线型"
value={String(draft.dash)}
onValueChange={(value) => setDraft((current) => ({ ...current, dash: value }))}
options={[
{ value: "original", label: "原始" },
{ value: "solid", label: "实线" },
{ value: "dashed", label: "虚线" }
]}
/>
</>
) : (
<>
@@ -555,22 +537,15 @@ export function MapDevPanel({
</div>
{showValueLabels ? (
<>
<Field label="数值属性">
<select
value={property}
onChange={(event) => setProperty(event.target.value)}
className={inputClassName}
>
{LABEL_PRESETS[groupId].options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</Field>
<FieldSelect
label="数值属性"
value={property}
onValueChange={setProperty}
options={LABEL_PRESETS[groupId].options}
/>
<div className="grid grid-cols-2 gap-2">
<Field label="精度 0-3">
<input
<Input
type="number"
min={0}
max={3}
@@ -580,7 +555,7 @@ export function MapDevPanel({
/>
</Field>
<Field label="单位">
<input
<Input
maxLength={16}
value={unit}
onChange={(event) => setUnit(event.target.value)}
@@ -703,11 +678,10 @@ function ToggleField({
}) {
return (
<label className="flex h-9 cursor-pointer items-center gap-2 rounded-lg bg-white/80 px-2.5 text-[11px] font-medium text-slate-700 shadow-[inset_0_0_0_1px_rgba(203,213,225,0.9)]">
<input
type="checkbox"
<Checkbox
checked={checked}
onChange={(event) => onChange(event.target.checked)}
className="h-4 w-4 accent-blue-600"
className="size-4 rounded"
/>
{label}
</label>
@@ -781,4 +755,4 @@ function StatusText({ children }: { children: React.ReactNode }) {
}
const inputClassName =
"h-9 w-full rounded-lg border border-slate-200 bg-white/80 px-2.5 text-xs text-slate-800 outline-hidden transition focus:border-blue-400 focus:ring-2 focus:ring-blue-500/15";
"h-10 w-full rounded-lg border border-slate-200 bg-white/80 px-2.5 text-xs text-slate-800 outline-hidden transition focus:border-blue-400 focus:ring-2 focus:ring-blue-500/15";