feat(workbench): integrate runtime data and map tooling
This commit is contained in:
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user