Files
next-tjwater-frontend/src/features/map/core/components/measure-panel.tsx
T
jiang f4318a9b9c feat: refine workbench visuals and map controls
Unify the Agent history extension with the header acrylic surface, preserve the full conversation body, and consolidate shared control and status styling.

Restore map flow and SCADA controller behavior, remove obsolete rendering paths, and extend regression coverage. Button press coverage now releases outside the target so state assertions cannot accidentally toggle the control.
2026-07-28 16:39:36 +08:00

164 lines
4.8 KiB
TypeScript

import type { LucideIcon } from "lucide-react";
import { Copy, Ruler, Trash2 } from "lucide-react";
import { cn } from "@/shared/ui/cn";
import {
MapActionRow,
MapModeButton,
MapPanelSection
} from "./control-panel";
import {
MAP_COMPACT_RADIUS_CLASS_NAME,
MAP_READABLE_RADIUS_CLASS_NAME,
MAP_READABLE_SURFACE_CLASS_NAME,
MAP_READABLE_SURFACE_STRONG_CLASS_NAME
} from "./map-control-styles";
export type MapMeasureMode = {
id: string;
label: string;
icon: LucideIcon;
disabled?: boolean;
};
export type MapMeasureUnit = {
id: string;
label: string;
};
export type MapMeasureResultMetric = {
label: string;
value: string;
};
export type MapMeasureAction = {
id: string;
label: string;
description: string;
icon?: LucideIcon;
strong?: boolean;
variant?: "default" | "primary";
tone?: "default" | "danger";
disabled?: boolean;
onClick?: () => void;
};
export type MapMeasurePanelProps = {
modes: MapMeasureMode[];
activeModeId: string;
resultLabel: string;
resultValue: string;
resultUnit: string;
metrics?: MapMeasureResultMetric[];
units: MapMeasureUnit[];
activeUnitId: string;
actions: MapMeasureAction[];
onSelectMode?: (id: string) => void;
onSelectUnit?: (id: string) => void;
};
export function MapMeasurePanel({
modes,
activeModeId,
resultLabel,
resultValue,
resultUnit,
metrics = [],
units,
activeUnitId,
actions,
onSelectMode,
onSelectUnit
}: MapMeasurePanelProps) {
return (
<div className="space-y-3">
<MapPanelSection title="模式">
<div className={cn("surface-well grid grid-cols-3 gap-1.5 border p-1.5", MAP_READABLE_RADIUS_CLASS_NAME)}>
{modes.map((mode) => (
<MapModeButton
key={mode.id}
icon={mode.icon}
label={mode.label}
active={mode.id === activeModeId}
disabled={mode.disabled}
onClick={() => onSelectMode?.(mode.id)}
/>
))}
</div>
</MapPanelSection>
<MapPanelSection title="结果">
<div className={cn("border border-blue-100 bg-blue-50/95 p-2", MAP_READABLE_RADIUS_CLASS_NAME)}>
<div className={cn("p-3", MAP_READABLE_RADIUS_CLASS_NAME, MAP_READABLE_SURFACE_STRONG_CLASS_NAME)}>
<div className="min-w-0 border-b border-slate-100 pb-3">
<div className="text-xs font-semibold text-blue-700">{resultLabel}</div>
<div className="mt-1 flex min-w-0 items-baseline gap-1.5 text-slate-950">
<span className="truncate text-2xl font-semibold leading-none">{resultValue}</span>
<span className="shrink-0 text-xs font-semibold text-slate-500">{resultUnit}</span>
</div>
</div>
{metrics.length > 0 ? (
<div className="mt-2 grid grid-cols-[repeat(auto-fit,minmax(92px,1fr))] gap-1.5">
{metrics.map((metric) => (
<div
key={metric.label}
className={cn("min-w-0 px-2.5 py-1.5 text-left", MAP_COMPACT_RADIUS_CLASS_NAME, MAP_READABLE_SURFACE_CLASS_NAME)}
>
<div className="truncate text-xs text-slate-500">{metric.label}</div>
<div className="mt-0.5 truncate text-sm font-semibold text-slate-900">{metric.value}</div>
</div>
))}
</div>
) : null}
</div>
<div className="mt-3 grid grid-cols-[repeat(auto-fit,minmax(0,1fr))] gap-1.5">
{units.map((unit) => (
<button
key={unit.id}
type="button"
onClick={() => onSelectUnit?.(unit.id)}
className={cn(
"h-10 text-xs font-semibold",
MAP_COMPACT_RADIUS_CLASS_NAME,
unit.id === activeUnitId
? "bg-blue-600 text-white"
: "surface-control text-slate-600 hover:text-blue-700"
)}
>
{unit.label}
</button>
))}
</div>
</div>
</MapPanelSection>
<MapPanelSection title="操作">
{actions.map((action) => (
<MapActionRow
key={action.id}
icon={action.icon ?? getDefaultActionIcon(action.id)}
label={action.label}
description={action.description}
strong={action.strong}
variant={action.variant}
tone={action.tone}
disabled={action.disabled}
onClick={action.onClick}
/>
))}
</MapPanelSection>
</div>
);
}
function getDefaultActionIcon(actionId: string) {
if (actionId.includes("copy")) {
return Copy;
}
if (actionId.includes("clear") || actionId.includes("delete")) {
return Trash2;
}
return Ruler;
}