refactor: organize supply frontend structure

This commit is contained in:
2026-07-22 18:16:43 +08:00
parent 699a0bced4
commit b9bc9c3d39
96 changed files with 917 additions and 745 deletions
@@ -1,11 +1,9 @@
"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { AnimatePresence, motion, type Variants } from "motion/react";
import { cn } from "@/lib/utils";
import { cn } from "@/shared/ui/cn";
import type {
ScheduledConditionRecord
} from "@/features/workbench/types";
} from "../types";
import {
createVisibleTickerCards,
type TickerStackCard
@@ -1,7 +1,7 @@
import type {
ScheduledConditionRiskLevel,
ScheduledConditionStatus
} from "@/features/workbench/types";
} from "../types";
export function getConditionReportRiskLevel(
status: ScheduledConditionStatus,
@@ -1,113 +0,0 @@
import type { ReactNode, SVGProps } from "react";
export type DrainageFeatureIconProps = SVGProps<SVGSVGElement> & {
size?: number;
};
type DrainageIconFrameProps = DrainageFeatureIconProps & {
children: ReactNode;
};
function DrainageIconFrame({ children, size = 20, ...props }: DrainageIconFrameProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
focusable="false"
{...props}
>
{children}
</svg>
);
}
export function ConduitFeatureIcon(props: DrainageFeatureIconProps) {
return (
<DrainageIconFrame {...props}>
<circle cx="3.5" cy="12" r="1.75" />
<circle cx="20.5" cy="12" r="1.75" />
<path d="M5.25 9.5h13.5M5.25 14.5h13.5" />
</DrainageIconFrame>
);
}
export function JunctionFeatureIcon(props: DrainageFeatureIconProps) {
return (
<DrainageIconFrame {...props}>
<circle cx="12" cy="12" r="5" />
<circle cx="12" cy="12" r="1.75" />
<path d="M12 2.5V7M2.5 12H7M17 12h4.5M12 17v4.5" />
</DrainageIconFrame>
);
}
export function ValveFeatureIcon(props: DrainageFeatureIconProps) {
return (
<DrainageIconFrame {...props}>
<path d="M3.5 12h4.75M15.75 12h4.75" />
<path d="m8.25 7 7.5 5-7.5 5V7Z" />
<path d="m15.75 7-7.5 5 7.5 5V7Z" />
<path d="M12 7V3.5M9.5 3.5h5" />
</DrainageIconFrame>
);
}
export function ReservoirFeatureIcon(props: DrainageFeatureIconProps) {
return (
<DrainageIconFrame {...props}>
<path d="M5 8.5c0-2.2 14-2.2 14 0v7c0 2.2-14 2.2-14 0v-7Z" />
<path d="M5 8.5c0 2.2 14 2.2 14 0" />
<path d="M5 12c0 2.2 14 2.2 14 0" />
<path d="M8 19.5h8" />
</DrainageIconFrame>
);
}
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}>
<path d="M2.5 12H9M15 12h6.5" />
<path d="M9.5 4v5.35M9.5 14.65V20M14.5 4v5.35M14.5 14.65V20" />
<circle cx="12" cy="12" r="3" />
</DrainageIconFrame>
);
}
export function PumpFeatureIcon(props: DrainageFeatureIconProps) {
return (
<DrainageIconFrame {...props}>
<circle cx="9.5" cy="13.5" r="6" />
<circle cx="9.5" cy="13.5" r="1.25" />
<path d="M9.5 12.25V8.5M10.75 13.5h3.75M8.65 14.4 6.2 16.85" />
<path d="M9.5 7.5V3.5h10v5M3.5 13.5H1.75" />
<path d="m17 6 2.5 2.5L22 6" />
</DrainageIconFrame>
);
}
export function OutfallFeatureIcon(props: DrainageFeatureIconProps) {
return (
<DrainageIconFrame {...props}>
<path d="M4 4.5h10v7h5" />
<path d="m16.5 8.75 2.75 2.75-2.75 2.75" />
<path d="M3 18c1.5-1.25 3-1.25 4.5 0s3 1.25 4.5 0 3-1.25 4.5 0 3 1.25 4.5 0M5 21c1.15-.75 2.3-.75 3.45 0s2.3.75 3.45 0 2.3-.75 3.45 0 2.3.75 3.45 0" />
</DrainageIconFrame>
);
}
@@ -1,5 +1,3 @@
"use client";
import { Target, TriangleAlert, X } from "lucide-react";
import { useMemo } from "react";
import type { DetailFeature } from "../types";
@@ -1,14 +1,12 @@
"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-actions";
import { cn } from "@/lib/utils";
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME,
showMapNotice
} from "@/features/map/core";
import { cn } from "@/shared/ui/cn";
import type { WaterNetworkSourceId } from "../map/sources";
import type { DetailFeature } from "../types";
import {
@@ -16,14 +14,14 @@ import {
type FeaturePanelBadgeTone
} from "../utils/feature-properties";
import {
ConduitFeatureIcon,
PipeFeatureIcon,
JunctionFeatureIcon,
PumpFeatureIcon,
ReservoirFeatureIcon,
ScadaFeatureIcon,
ValveFeatureIcon,
type DrainageFeatureIconProps
} from "./drainage-feature-icons";
type SupplyFeatureIconProps
} from "./supply-feature-icons";
import { FeaturePropertyList } from "./feature-property-list";
type FeaturePopoverProps = {
@@ -34,14 +32,14 @@ type FeaturePopoverProps = {
const FEATURE_LAYER_META: Record<
WaterNetworkSourceId,
{
icon: ComponentType<DrainageFeatureIconProps>;
icon: ComponentType<SupplyFeatureIconProps>;
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" },
pipes: { icon: PipeFeatureIcon, 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" },
@@ -1,4 +1,4 @@
import { cn } from "@/lib/utils";
import { cn } from "@/shared/ui/cn";
import type { LocalizedFeatureProperty } from "../utils/feature-properties";
type FeaturePropertyListProps = {
@@ -1,8 +1,6 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { ChevronDown, Crosshair, LocateFixed, RotateCcw, X } from "lucide-react";
import { cn } from "@/lib/utils";
import { cn } from "@/shared/ui/cn";
import {
LAYER_GROUP_GEOMETRIES,
type LayerGroupId,
@@ -1,5 +1,3 @@
"use client";
import {
AlertTriangle,
Bot,
@@ -20,13 +18,13 @@ import {
} from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
import { showMapNotice } from "@/features/map/core/components/notice-actions";
import { cn } from "@/shared/ui/cn";
import { showMapNotice } from "@/features/map/core";
import {
getRunningWorkflowDefinition,
getRunningWorkflowState,
getWorkflowStepStatus
} from "@/features/workbench/data/running-workflows";
} from "../data/running-workflows";
import type {
ScheduledConditionAnalysisInsight,
ScheduledConditionItem,
@@ -34,7 +32,7 @@ import type {
ScheduledConditionRecord,
ScheduledConditionRiskLevel,
ScheduledWorkOrderItem
} from "@/features/workbench/types";
} from "../types";
import {
formatDuration,
formatTime,
@@ -10,7 +10,7 @@ import type {
ScheduledConditionStatus,
ScheduledWorkOrderItem,
ScheduledWorkOrderStage
} from "@/features/workbench/types";
} from "../types";
export const CONDITION_FILTER_ALL = "__all__";
@@ -1,5 +1,3 @@
"use client";
import {
CalendarClock,
ChevronDown,
@@ -10,12 +8,11 @@ import {
} from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { MAP_TOOL_PANEL_SURFACE_CLASS_NAME } from "@/features/map/core/components/map-control-styles";
import { showMapNotice } from "@/features/map/core/components/notice-actions";
import { isGeneratedScheduledConditionSessionId } from "@/features/workbench/data/scheduled-conditions";
import type { ScheduledConditionItem } from "@/features/workbench/types";
import { createConditionConversationPrompt } from "@/features/workbench/utils/scheduled-condition-prompts";
import { cn } from "@/lib/utils";
import { MAP_TOOL_PANEL_SURFACE_CLASS_NAME, showMapNotice } from "@/features/map/core";
import { isGeneratedScheduledConditionSessionId } from "../data/scheduled-conditions";
import type { ScheduledConditionItem } from "../types";
import { createConditionConversationPrompt } from "../utils/scheduled-condition-prompts";
import { cn } from "@/shared/ui/cn";
import {
DropdownMenu,
DropdownMenuContent,
@@ -0,0 +1,93 @@
import type { ReactNode, SVGProps } from "react";
export type SupplyFeatureIconProps = SVGProps<SVGSVGElement> & {
size?: number;
};
type SupplyIconFrameProps = SupplyFeatureIconProps & {
children: ReactNode;
};
function SupplyIconFrame({ children, size = 20, ...props }: SupplyIconFrameProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
focusable="false"
{...props}
>
{children}
</svg>
);
}
export function PipeFeatureIcon(props: SupplyFeatureIconProps) {
return (
<SupplyIconFrame {...props}>
<circle cx="3.5" cy="12" r="1.75" />
<circle cx="20.5" cy="12" r="1.75" />
<path d="M5.25 9.5h13.5M5.25 14.5h13.5" />
</SupplyIconFrame>
);
}
export function JunctionFeatureIcon(props: SupplyFeatureIconProps) {
return (
<SupplyIconFrame {...props}>
<circle cx="12" cy="12" r="5" />
<circle cx="12" cy="12" r="1.75" />
<path d="M12 2.5V7M2.5 12H7M17 12h4.5M12 17v4.5" />
</SupplyIconFrame>
);
}
export function ValveFeatureIcon(props: SupplyFeatureIconProps) {
return (
<SupplyIconFrame {...props}>
<path d="M3.5 12h4.75M15.75 12h4.75" />
<path d="m8.25 7 7.5 5-7.5 5V7Z" />
<path d="m15.75 7-7.5 5 7.5 5V7Z" />
<path d="M12 7V3.5M9.5 3.5h5" />
</SupplyIconFrame>
);
}
export function ReservoirFeatureIcon(props: SupplyFeatureIconProps) {
return (
<SupplyIconFrame {...props}>
<path d="M5 8.5c0-2.2 14-2.2 14 0v7c0 2.2-14 2.2-14 0v-7Z" />
<path d="M5 8.5c0 2.2 14 2.2 14 0" />
<path d="M5 12c0 2.2 14 2.2 14 0" />
<path d="M8 19.5h8" />
</SupplyIconFrame>
);
}
export function ScadaFeatureIcon(props: SupplyFeatureIconProps) {
return (
<SupplyIconFrame {...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" />
</SupplyIconFrame>
);
}
export function PumpFeatureIcon(props: SupplyFeatureIconProps) {
return (
<SupplyIconFrame {...props}>
<circle cx="9.5" cy="13.5" r="6" />
<circle cx="9.5" cy="13.5" r="1.25" />
<path d="M9.5 12.25V8.5M10.75 13.5h3.75M8.65 14.4 6.2 16.85" />
<path d="M9.5 7.5V3.5h10v5M3.5 13.5H1.75" />
<path d="m17 6 2.5 2.5L22 6" />
</SupplyIconFrame>
);
}
@@ -23,21 +23,20 @@ import {
import { useEffect, useMemo, useState } from "react";
import {
MapAnnotationPanel,
type MapAnnotationItem,
type MapAnnotationShareAction
} from "@/features/map/core/components/annotation-panel";
import type { BaseLayerOption } from "@/features/map/core/components/base-layers-control";
import {
MapActionRow,
MapControlPanel,
MapDrawToolbar,
MapLayerPanel,
MapLegendList,
MapMetricTile,
MapPanelSection
} from "@/features/map/core/components/control-panel";
import { MapDrawToolbar } from "@/features/map/core/components/draw-toolbar";
import { MapLayerPanel } from "@/features/map/core/components/layer-panel";
import type { MapLayerControlItem } from "@/features/map/core/components/layer-control";
import { MapLegendList, type MapLegendItem } from "@/features/map/core/components/legend";
import { MapMeasurePanel } from "@/features/map/core/components/measure-panel";
MapMeasurePanel,
MapPanelSection,
type BaseLayerOption,
type MapAnnotationItem,
type MapAnnotationShareAction,
type MapLayerControlItem,
type MapLegendItem
} from "@/features/map/core";
import {
type WorkbenchDrawingAnnotation,
type WorkbenchDrawMode
@@ -0,0 +1,118 @@
import { AgentCollapsedRail, AgentCommandPanel, AgentPersona } from "@/features/agent";
import {
MAP_CONTROL_SURFACE_CLASS_NAME,
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME
} from "@/features/map/core";
import { cn } from "@/shared/ui/cn";
import type { ComponentProps } from "react";
import { WORKBENCH_LAYOUT, getWorkbenchViewportLayout } from "../layout/workbench-layout";
import { useAgentPanelResize } from "../hooks/use-agent-panel-resize";
type AgentPanelProps = Omit<ComponentProps<typeof AgentCommandPanel>, "collapsing" | "onCollapse">;
type WorkbenchAgentPanelsProps = {
panelProps: AgentPanelProps;
panelOpen: boolean;
panelCollapsing: boolean;
mobileOpen: boolean;
mobilePanelCollapsing: boolean;
personaState: ComponentProps<typeof AgentPersona>["state"];
statusLabel: string;
viewportWidth: number;
onCollapsePanel: () => void;
onCloseMobilePanel: () => void;
onExpandPanel: () => void;
onOpenMobilePanel: () => void;
};
export function WorkbenchAgentPanels({
panelProps,
panelOpen,
panelCollapsing,
mobileOpen,
mobilePanelCollapsing,
personaState,
statusLabel,
viewportWidth,
onCollapsePanel,
onCloseMobilePanel,
onExpandPanel,
onOpenMobilePanel
}: WorkbenchAgentPanelsProps) {
const resize = useAgentPanelResize(viewportWidth);
const defaultWidth = getWorkbenchViewportLayout(viewportWidth).agentWidth;
return (
<>
<div
ref={resize.panelRef}
className="pointer-events-none absolute bottom-4 left-3 top-24 z-20 hidden w-[var(--workbench-agent-width)] max-w-[50vw] 2xl:w-[var(--workbench-agent-width-wide)] lg:block"
style={resize.width === null ? undefined : { width: resize.width }}
>
{panelOpen ? (
<>
<AgentCommandPanel
{...panelProps}
collapsing={panelCollapsing}
onCollapse={onCollapsePanel}
/>
<div
aria-label="调整 Agent 面板宽度"
aria-orientation="vertical"
aria-valuemax={Math.round(viewportWidth * WORKBENCH_LAYOUT.maxAgentViewportRatio)}
aria-valuemin={defaultWidth}
aria-valuenow={Math.round(resize.width ?? defaultWidth)}
className={cn(
"group pointer-events-auto absolute -right-3 top-4 bottom-4 z-10 flex w-6 cursor-ew-resize touch-none items-center justify-center outline-hidden",
resize.resizing && "[&>span]:bg-blue-500 [&>span]:opacity-100"
)}
role="separator"
tabIndex={0}
title="拖拽调整 Agent 面板宽度"
onKeyDown={resize.handleKeyDown}
onPointerDown={resize.handlePointerDown}
>
<span className="h-14 w-1 rounded-full bg-slate-500/60 opacity-50 shadow-xs transition-[height,background-color,opacity] group-hover:h-20 group-hover:bg-blue-500 group-hover:opacity-100 group-focus-visible:h-20 group-focus-visible:bg-blue-500 group-focus-visible:opacity-100" />
</div>
</>
) : (
<AgentCollapsedRail
personaState={personaState}
statusLabel={statusLabel}
onExpand={onExpandPanel}
/>
)}
</div>
<div className="absolute bottom-20 left-3 right-3 z-30 lg:hidden">
{mobileOpen ? (
<div className="h-[calc(100dvh-8.5rem)] min-h-[420px]">
<AgentCommandPanel
{...panelProps}
collapsing={mobilePanelCollapsing}
onCollapse={onCloseMobilePanel}
/>
</div>
) : null}
</div>
{!mobileOpen ? (
<div className="absolute bottom-20 left-3 z-30 lg:hidden">
<button
type="button"
aria-label="打开 Agent 面板"
title="打开 Agent 面板"
onClick={onOpenMobilePanel}
className={cn(
"grid h-12 w-12 place-items-center text-violet-700",
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME,
MAP_CONTROL_SURFACE_CLASS_NAME
)}
>
<AgentPersona className="pointer-events-none h-10 w-10" state={personaState} />
</button>
</div>
) : null}
</>
);
}
@@ -1,5 +1,3 @@
"use client";
import {
Activity,
Bell,
@@ -24,8 +22,8 @@ import {
MAP_ICON_CELL_RADIUS_CLASS_NAME,
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME,
MAP_READABLE_RADIUS_CLASS_NAME
} from "@/features/map/core/components/map-control-styles";
import { cn } from "@/lib/utils";
} from "@/features/map/core";
import { cn } from "@/shared/ui/cn";
import {
DropdownMenu,
DropdownMenuContent,
@@ -1,5 +1,3 @@
"use client";
import { useState } from "react";
import {
Box,
@@ -13,8 +11,8 @@ import {
} from "lucide-react";
import {
MAP_ICON_CELL_RADIUS_CLASS_NAME
} from "@/features/map/core/components/map-control-styles";
import { cn } from "@/lib/utils";
} from "@/features/map/core";
import { cn } from "@/shared/ui/cn";
import type { WorkbenchAlert, WorkbenchScenario, WorkbenchUser } from "../types";
import { AlertMenu, ScenarioMenu, UserMenu } from "./workbench-top-bar-menus";
import {