feat(workbench): expand SCADA monitoring and investigation presentation
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import { Crosshair, LocateFixed, MousePointer2, Trash2, ZoomIn } from "lucide-react";
|
||||
import { useId, useState } from "react";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { FieldSelect } from "@/shared/ui/field-select";
|
||||
import { Textarea } from "@/shared/ui/textarea";
|
||||
import { MAX_MAP_FEATURE_IDS, MAX_MAP_FEATURE_ID_LENGTH } from "../map/map-feature-query";
|
||||
import { AVAILABLE_WATER_NETWORK_SOURCE_IDS, SUPPLY_LAYER_CATALOG, type AvailableWaterNetworkSourceId } from "../map/sources";
|
||||
import type { FeatureBatchTarget, WorkbenchMapCommands, WorkbenchMapControllerState } from "../map/workbench-map-controller";
|
||||
import type { DetailFeature } from "../types";
|
||||
import { FEATURE_LOCATION_ERROR_LABELS } from "../map/map-feature-feedback";
|
||||
|
||||
type FeatureLocationDraft = {
|
||||
sourceId: AvailableWaterNetworkSourceId;
|
||||
featureId: string;
|
||||
};
|
||||
|
||||
export type FeatureLocationPanelProps = {
|
||||
commands: Pick<WorkbenchMapCommands, "highlight" | "zoomToFeature" | "locateAndHighlight" | "clearHighlight">;
|
||||
controllerState: Pick<WorkbenchMapControllerState, "pending" | "targets" | "targetResult" | "errorCode">;
|
||||
detailFeature: DetailFeature | null;
|
||||
enabled?: boolean;
|
||||
value?: FeatureLocationDraft;
|
||||
onValueChange?: (value: FeatureLocationDraft) => void;
|
||||
onUseSelectedFeature?: () => void;
|
||||
showError?: boolean;
|
||||
contentClassName?: string;
|
||||
};
|
||||
|
||||
export function FeatureLocationPanel({ commands, controllerState, detailFeature, enabled = true,
|
||||
value, onValueChange, onUseSelectedFeature, showError = true, contentClassName }: FeatureLocationPanelProps) {
|
||||
const inputId = useId();
|
||||
const helpId = useId();
|
||||
const [draft, setDraft] = useState<FeatureLocationDraft>({ sourceId: "pipes", featureId: "" });
|
||||
const { sourceId, featureId } = value ?? draft;
|
||||
function updateDraft(next: FeatureLocationDraft) {
|
||||
if (!value) setDraft(next);
|
||||
onValueChange?.(next);
|
||||
}
|
||||
const featureIds = [...new Set(featureId.split(/[\s,,;;、]+/u).filter(Boolean))];
|
||||
const invalidIds = featureIds.length > MAX_MAP_FEATURE_IDS || featureIds.some((id) => id.length > MAX_MAP_FEATURE_ID_LENGTH);
|
||||
const canLocate = enabled && featureIds.length > 0 && !invalidIds && !controllerState.pending;
|
||||
const canUseSelected = Boolean(detailFeature && AVAILABLE_WATER_NETWORK_SOURCE_IDS.includes(detailFeature.layer as AvailableWaterNetworkSourceId));
|
||||
const target: FeatureBatchTarget = { sourceId, featureIds };
|
||||
const result = controllerState.targetResult;
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<FieldSelect label="业务源" value={sourceId} contentClassName={contentClassName}
|
||||
onValueChange={(nextSourceId) => updateDraft({ sourceId: nextSourceId as AvailableWaterNetworkSourceId, featureId: "" })}
|
||||
options={SUPPLY_LAYER_CATALOG.filter((layer) => layer.available).map((layer) => ({ value: layer.id, label: layer.label }))}
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<label htmlFor={inputId} className="text-xs font-medium text-[var(--ink-muted)]">要素 ID(可多个)</label>
|
||||
<span className="text-[11px] tabular-nums text-[var(--ink-muted)]">{featureIds.length} / {MAX_MAP_FEATURE_IDS}</span>
|
||||
</div>
|
||||
<Textarea id={inputId} value={featureId} rows={3} placeholder="粘贴要素 ID,支持多个"
|
||||
onChange={(event) => updateDraft({ sourceId, featureId: event.target.value })} aria-describedby={helpId} aria-invalid={invalidIds}
|
||||
className="min-h-24 resize-y border-[var(--surface-border)] bg-[var(--surface-reading)] text-xs shadow-none md:text-xs"
|
||||
/>
|
||||
<p id={helpId} className="text-[11px] leading-5 text-[var(--ink-muted)]">逗号、空格或换行分隔,自动去重。</p>
|
||||
</div>
|
||||
{invalidIds ? <p role="alert" className="text-xs leading-5 text-[var(--status-danger-foreground)]">单次最多 {MAX_MAP_FEATURE_IDS} 个,每个 ID 最长 {MAX_MAP_FEATURE_ID_LENGTH} 字符。</p> : null}
|
||||
<Button type="button" variant="secondary" className="w-full text-xs" disabled={!canUseSelected}
|
||||
onClick={() => {
|
||||
if (!detailFeature || !canUseSelected) return;
|
||||
updateDraft({ sourceId: detailFeature.layer as AvailableWaterNetworkSourceId, featureId: detailFeature.id });
|
||||
onUseSelectedFeature?.();
|
||||
}}>
|
||||
<MousePointer2 aria-hidden="true" />使用当前地图选中
|
||||
</Button>
|
||||
<div className="space-y-2 border-t border-[var(--surface-divider)] pt-3">
|
||||
<Button type="button" className="w-full text-xs" disabled={!canLocate}
|
||||
loading={controllerState.pending} loadingLabel="正在定位要素"
|
||||
onClick={() => void commands.locateAndHighlight(target)}>
|
||||
<LocateFixed aria-hidden="true" />定位并高亮
|
||||
</Button>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button type="button" variant="outline" className="text-xs" disabled={!canLocate} onClick={() => void commands.highlight(target)}>
|
||||
<Crosshair aria-hidden="true" />仅高亮
|
||||
</Button>
|
||||
<Button type="button" variant="outline" className="text-xs" disabled={!canLocate} onClick={() => void commands.zoomToFeature(target)}>
|
||||
<ZoomIn aria-hidden="true" />缩放到要素
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{!enabled ? <p role="status" className="text-xs text-[var(--ink-muted)]">等待地图加载后即可定位</p> : null}
|
||||
{showError && controllerState.errorCode && FEATURE_LOCATION_ERROR_LABELS[controllerState.errorCode] ? (
|
||||
<p role="alert" className="text-xs leading-5 text-[var(--status-danger-foreground)]">{FEATURE_LOCATION_ERROR_LABELS[controllerState.errorCode]}</p>
|
||||
) : null}
|
||||
{result ? <p role="status" className="break-words text-xs leading-5 text-[var(--ink-muted)]">
|
||||
本次命中 {result.featureIds.length} 个{result.missingIds.length ? `,未找到 ${result.missingIds.length} 个:${result.missingIds.join("、")}` : ",全部找到"}
|
||||
</p> : null}
|
||||
<div className="flex items-center justify-between gap-2 border-t border-[var(--surface-divider)] pt-2">
|
||||
<p className="text-[11px] text-[var(--ink-muted)]">已高亮 {controllerState.targets.length} 个要素</p>
|
||||
<Button type="button" variant="ghost" className="shrink-0 text-xs" onClick={() => commands.clearHighlight()}>
|
||||
<Trash2 aria-hidden="true" />清除高亮
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user