a550d6d0fd
- Next.js + MapLibre GL 3D 地球 - CARTO dark-matter vector 底图 - 动态星空背景(闪烁、漂移、流星) - 经纬网格线(graticule) - 文物数据图层与朝代/类别筛选 - 中国风信息弹窗 - 中英双语 i18n - LayerPanel + MapControls 浮动面板
104 lines
3.9 KiB
TypeScript
104 lines
3.9 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useTranslations } from 'next-intl';
|
|
import { Search, Target, MapPin, Plus, Minus, X } from 'lucide-react';
|
|
|
|
interface MapControlsProps {
|
|
onSearch?: (query: string) => void;
|
|
onZoomIn?: () => void;
|
|
onZoomOut?: () => void;
|
|
onLocate?: () => void;
|
|
onSpotlight?: () => void;
|
|
}
|
|
|
|
export function MapControls({
|
|
onSearch,
|
|
onZoomIn,
|
|
onZoomOut,
|
|
onLocate,
|
|
onSpotlight,
|
|
}: MapControlsProps) {
|
|
const t = useTranslations('map');
|
|
const [searchOpen, setSearchOpen] = useState(false);
|
|
const [query, setQuery] = useState('');
|
|
|
|
const submitSearch = () => {
|
|
if (onSearch && query.trim()) {
|
|
onSearch(query.trim());
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="absolute top-16 left-4 z-10 flex flex-col gap-2">
|
|
{/* Search */}
|
|
<div className="flex flex-col gap-2">
|
|
<button
|
|
onClick={() => setSearchOpen(!searchOpen)}
|
|
className="w-9 h-9 rounded-lg bg-[#06060c]/90 backdrop-blur-md border border-[var(--map-border)] flex items-center justify-center text-[var(--map-text-muted)] hover:text-white hover:bg-white/10 transition-colors shadow-lg"
|
|
aria-label="Open search"
|
|
>
|
|
{searchOpen ? <X className="w-4 h-4" /> : <Search className="w-4 h-4" />}
|
|
</button>
|
|
|
|
{searchOpen && (
|
|
<div className="flex items-center gap-1 p-1 rounded-lg bg-[#06060c]/90 backdrop-blur-md border border-[var(--map-border)] shadow-lg">
|
|
<input
|
|
type="text"
|
|
value={query}
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
onKeyDown={(e) => e.key === 'Enter' && submitSearch()}
|
|
placeholder={t('search')}
|
|
className="w-48 px-2 py-1.5 bg-transparent text-sm text-[var(--map-text)] placeholder:text-[var(--map-text-muted)] focus:outline-none"
|
|
autoFocus
|
|
/>
|
|
<button
|
|
onClick={submitSearch}
|
|
className="w-7 h-7 rounded-md bg-[var(--map-panel)] hover:bg-[var(--map-panel-hover)] flex items-center justify-center text-[var(--map-text-muted)] hover:text-white transition-colors"
|
|
>
|
|
<Search className="w-3.5 h-3.5" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Spotlight selection */}
|
|
<button
|
|
onClick={onSpotlight}
|
|
className="w-9 h-9 rounded-lg bg-[#06060c]/90 backdrop-blur-md border border-[var(--map-border)] flex items-center justify-center text-[var(--map-text-muted)] hover:text-white hover:bg-white/10 transition-colors shadow-lg"
|
|
aria-label="Enable spotlight selection"
|
|
>
|
|
<Target className="w-4 h-4" />
|
|
</button>
|
|
|
|
{/* Zoom to my location */}
|
|
<button
|
|
onClick={onLocate}
|
|
className="w-9 h-9 rounded-lg bg-[#06060c]/90 backdrop-blur-md border border-[var(--map-border)] flex items-center justify-center text-[var(--map-text-muted)] hover:text-white hover:bg-white/10 transition-colors shadow-lg"
|
|
aria-label="Zoom to my location"
|
|
>
|
|
<MapPin className="w-4 h-4" />
|
|
</button>
|
|
|
|
{/* Zoom in/out */}
|
|
<div className="flex flex-col rounded-lg bg-[#06060c]/90 backdrop-blur-md border border-[var(--map-border)] shadow-lg overflow-hidden">
|
|
<button
|
|
onClick={onZoomIn}
|
|
className="w-9 h-9 flex items-center justify-center text-[var(--map-text-muted)] hover:text-white hover:bg-white/10 transition-colors"
|
|
aria-label={t('zoomIn')}
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
</button>
|
|
<div className="h-px bg-[var(--map-border)]" />
|
|
<button
|
|
onClick={onZoomOut}
|
|
className="w-9 h-9 flex items-center justify-center text-[var(--map-text-muted)] hover:text-white hover:bg-white/10 transition-colors"
|
|
aria-label={t('zoomOut')}
|
|
>
|
|
<Minus className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|