164 lines
4.8 KiB
TypeScript
164 lines
4.8 KiB
TypeScript
import type { LucideIcon } from "lucide-react";
|
|
import { Copy, Ruler, Trash2 } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
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-7 text-xs font-semibold transition",
|
|
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;
|
|
}
|