123 lines
4.0 KiB
TypeScript
123 lines
4.0 KiB
TypeScript
import { Eye, EyeOff, Layers3, Lock } from "lucide-react";
|
|
import { cn } from "@/shared/ui/cn";
|
|
import {
|
|
mapOptionInteractionVariants,
|
|
MAP_COMPACT_RADIUS_CLASS_NAME,
|
|
MAP_ICON_CELL_RADIUS_CLASS_NAME,
|
|
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME,
|
|
MAP_TOOL_PANEL_SURFACE_CLASS_NAME
|
|
} from "./map-control-styles";
|
|
|
|
export type MapLayerControlItem = {
|
|
id: string;
|
|
label: string;
|
|
description?: string;
|
|
featureCount?: number;
|
|
minZoom?: number;
|
|
visible: boolean;
|
|
disabled?: boolean;
|
|
locked?: boolean;
|
|
group?: string;
|
|
groupLabel?: string;
|
|
groupDescription?: string;
|
|
groupDefaultOpen?: boolean;
|
|
collection?: string;
|
|
collectionLabel?: string;
|
|
visual?: MapLayerControlVisual;
|
|
};
|
|
|
|
export type MapLayerControlVisual = {
|
|
kind: "line" | "line-stack" | "point" | "ring" | "area" | "image";
|
|
color?: string;
|
|
colors?: string[];
|
|
dashed?: boolean;
|
|
imageSrc?: string;
|
|
};
|
|
|
|
type MapLayerControlProps = {
|
|
title?: string;
|
|
items: MapLayerControlItem[];
|
|
onToggleLayer: (id: string, visible: boolean) => void;
|
|
className?: string;
|
|
};
|
|
|
|
export function MapLayerControl({
|
|
title = "图层",
|
|
items,
|
|
onToggleLayer,
|
|
className = ""
|
|
}: MapLayerControlProps) {
|
|
return (
|
|
<section
|
|
aria-label={title}
|
|
className={cn(
|
|
"pointer-events-auto w-[276px] p-3",
|
|
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME,
|
|
MAP_TOOL_PANEL_SURFACE_CLASS_NAME,
|
|
className
|
|
)}
|
|
>
|
|
<div className="flex items-center justify-between gap-3 px-1">
|
|
<div className="flex min-w-0 items-center gap-2">
|
|
<span className={cn("grid h-8 w-8 shrink-0 place-items-center bg-[var(--action-soft)] text-[var(--action-blue-hover)]", MAP_ICON_CELL_RADIUS_CLASS_NAME)}>
|
|
<Layers3 size={17} aria-hidden="true" />
|
|
</span>
|
|
<div className="min-w-0">
|
|
<h2 className="truncate text-sm font-semibold leading-tight text-slate-950">{title}</h2>
|
|
<p className="mt-0.5 text-xs text-[var(--ink-muted)]">
|
|
{items.filter((item) => item.visible).length}/{items.length} 可见
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-3 space-y-1.5">
|
|
{items.map((item) => {
|
|
const Icon = item.visible ? Eye : EyeOff;
|
|
const disabled = item.disabled || item.locked;
|
|
|
|
return (
|
|
<button
|
|
key={item.id}
|
|
type="button"
|
|
data-map-option=""
|
|
disabled={disabled}
|
|
aria-pressed={item.visible}
|
|
onClick={() => onToggleLayer(item.id, !item.visible)}
|
|
className={cn(
|
|
"flex min-h-12 w-full items-center gap-3 border px-2.5 text-left text-sm",
|
|
MAP_COMPACT_RADIUS_CLASS_NAME,
|
|
item.visible
|
|
? "surface-control border-[var(--surface-border)] text-slate-900"
|
|
: "surface-well border-transparent text-[var(--ink-muted)]",
|
|
mapOptionInteractionVariants({ state: item.visible ? "selected" : "default" })
|
|
)}
|
|
>
|
|
<span
|
|
className={cn(
|
|
"grid h-8 w-8 shrink-0 place-items-center",
|
|
MAP_ICON_CELL_RADIUS_CLASS_NAME,
|
|
item.visible ? "bg-[var(--action-blue)] text-white" : "bg-[var(--surface-control)] text-inherit"
|
|
)}
|
|
>
|
|
{item.locked ? <Lock size={15} aria-hidden="true" /> : <Icon size={15} aria-hidden="true" />}
|
|
</span>
|
|
<span className="min-w-0 flex-1">
|
|
<span className="block truncate font-semibold">{item.label}</span>
|
|
{item.description ? <span className="block truncate text-xs text-[var(--ink-muted)]">{item.description}</span> : null}
|
|
</span>
|
|
<span
|
|
className={cn(
|
|
"h-2.5 w-2.5 rounded-full",
|
|
item.visible ? "bg-[var(--action-blue)]" : "bg-slate-300"
|
|
)}
|
|
aria-hidden="true"
|
|
/>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|