Files
next-tjwater-frontend/src/features/map/core/components/layer-control.tsx
T
jiang 0ae0a41e61
Generic Container CI/CD / test-build-publish (push) Successful in 2m7s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 2m8s
feat(charts): manage investigation charts in a scrolling workspace
2026-11-10 17:56:23 +08:00

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>
);
}