158 lines
5.4 KiB
TypeScript
158 lines
5.4 KiB
TypeScript
import { Check, Eye, EyeOff, Lock, Map } from "lucide-react";
|
||
import { cn } from "@/lib/utils";
|
||
import type { BaseLayerOption } from "./base-layers-control";
|
||
import type { MapLayerControlItem } from "./layer-control";
|
||
import {
|
||
MAP_COMPACT_RADIUS_CLASS_NAME,
|
||
MAP_ICON_CELL_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 MapLayerPanelProps = {
|
||
layerItems: MapLayerControlItem[];
|
||
baseLayerOptions: BaseLayerOption[];
|
||
activeBaseLayerId: string;
|
||
onSelectBaseLayer: (id: string) => void;
|
||
onToggleLayer: (id: string, visible: boolean) => void;
|
||
};
|
||
|
||
export function MapLayerPanel({
|
||
layerItems,
|
||
baseLayerOptions,
|
||
activeBaseLayerId,
|
||
onSelectBaseLayer,
|
||
onToggleLayer
|
||
}: MapLayerPanelProps) {
|
||
return (
|
||
<>
|
||
<LayerVisibilitySection items={layerItems} onToggleLayer={onToggleLayer} />
|
||
<BaseLayerSection
|
||
options={baseLayerOptions}
|
||
activeLayerId={activeBaseLayerId}
|
||
onSelectLayer={onSelectBaseLayer}
|
||
/>
|
||
</>
|
||
);
|
||
}
|
||
|
||
type LayerVisibilitySectionProps = {
|
||
items: MapLayerControlItem[];
|
||
onToggleLayer: (id: string, visible: boolean) => void;
|
||
};
|
||
|
||
function LayerVisibilitySection({ items, onToggleLayer }: LayerVisibilitySectionProps) {
|
||
return (
|
||
<div>
|
||
<div className="mb-1.5 px-1 text-xs font-semibold text-slate-500">业务图层</div>
|
||
<div className="grid grid-cols-3 gap-2">
|
||
{items.map((item) => {
|
||
const Icon = item.visible ? Eye : EyeOff;
|
||
const disabled = item.disabled || item.locked;
|
||
|
||
return (
|
||
<button
|
||
key={item.id}
|
||
type="button"
|
||
disabled={disabled}
|
||
aria-pressed={item.visible}
|
||
aria-label={`${item.label}${item.description ? `,${item.description}` : ""}`}
|
||
title={item.description}
|
||
onClick={() => onToggleLayer(item.id, !item.visible)}
|
||
className={cn(
|
||
"relative flex min-h-[76px] w-full flex-col items-center justify-center gap-1.5 border px-1.5 py-2 text-center transition duration-150 active:translate-y-px active:scale-[0.98]",
|
||
MAP_COMPACT_RADIUS_CLASS_NAME,
|
||
item.visible
|
||
? "border-blue-200 bg-blue-50/90 text-blue-950 shadow-sm shadow-blue-950/5"
|
||
: "border-white/70 bg-white/90 text-slate-500 hover:border-slate-200 hover:bg-white",
|
||
disabled && "cursor-not-allowed opacity-60 hover:bg-transparent"
|
||
)}
|
||
>
|
||
<span
|
||
className={cn(
|
||
"grid h-8 w-8 shrink-0 place-items-center transition-colors",
|
||
MAP_ICON_CELL_RADIUS_CLASS_NAME,
|
||
item.visible ? "bg-blue-600 text-white" : "bg-slate-100 text-slate-400"
|
||
)}
|
||
>
|
||
{item.locked ? <Lock size={15} aria-hidden="true" /> : <Icon size={15} aria-hidden="true" />}
|
||
</span>
|
||
<span className="block w-full truncate text-xs font-semibold leading-tight">{item.label}</span>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
type BaseLayerSectionProps = {
|
||
options: BaseLayerOption[];
|
||
activeLayerId: string;
|
||
onSelectLayer: (id: string) => void;
|
||
};
|
||
|
||
function BaseLayerSection({ options, activeLayerId, onSelectLayer }: BaseLayerSectionProps) {
|
||
return (
|
||
<div>
|
||
<div className="mb-1.5 flex items-center justify-between px-1">
|
||
<span className="text-xs font-semibold text-slate-500">底图</span>
|
||
<Map size={14} aria-hidden="true" className="text-slate-400" />
|
||
</div>
|
||
<div className="grid grid-cols-3 gap-2">
|
||
{options.map((option) => {
|
||
const active = option.id === activeLayerId;
|
||
|
||
return (
|
||
<button
|
||
key={option.id}
|
||
type="button"
|
||
disabled={option.disabled}
|
||
aria-pressed={active}
|
||
onClick={() => onSelectLayer(option.id)}
|
||
className={cn(
|
||
"flex min-h-[76px] flex-col p-1 text-left transition duration-150",
|
||
MAP_READABLE_RADIUS_CLASS_NAME,
|
||
active ? MAP_READABLE_SURFACE_STRONG_CLASS_NAME : MAP_READABLE_SURFACE_CLASS_NAME,
|
||
active
|
||
? "border-blue-500 text-blue-700 ring-2 ring-blue-100"
|
||
: "text-slate-700 hover:border-blue-300 hover:bg-blue-50/40",
|
||
option.disabled && "cursor-not-allowed opacity-50"
|
||
)}
|
||
>
|
||
<BaseLayerSwatch option={option} active={active} />
|
||
<span className="mt-1 block w-full truncate px-0.5 text-xs font-semibold leading-none">
|
||
{option.label}
|
||
</span>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
type BaseLayerSwatchProps = {
|
||
option: BaseLayerOption;
|
||
active: boolean;
|
||
};
|
||
|
||
function BaseLayerSwatch({ option, active }: BaseLayerSwatchProps) {
|
||
return (
|
||
<span
|
||
className={cn(
|
||
"relative block min-h-0 flex-1 overflow-hidden border border-white shadow-inner",
|
||
MAP_ICON_CELL_RADIUS_CLASS_NAME,
|
||
option.swatchClassName ?? "bg-slate-100"
|
||
)}
|
||
>
|
||
{active ? (
|
||
<span className="absolute right-0.5 top-0.5 grid h-4 w-4 place-items-center rounded-full bg-blue-600 text-white">
|
||
<Check size={10} aria-hidden="true" />
|
||
</span>
|
||
) : null}
|
||
</span>
|
||
);
|
||
}
|