生产管理系统前端 - gis地图管理开发

This commit is contained in:
2025-10-29 16:02:42 +08:00
parent 9340252c25
commit e14f03cf79
9 changed files with 1554 additions and 8 deletions

View File

@@ -0,0 +1,388 @@
import { useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import {
ZoomIn,
ZoomOut,
Maximize,
Minimize,
Layers,
Satellite,
Grid3x3,
Mountain,
MapPin,
Ruler,
X
} from 'lucide-react';
import {
GISMapEngine,
MapProvider,
MapLayer,
Marker,
Polygon,
MapPosition
} from '@/lib/gisMapEngine';
import { toast } from 'sonner';
interface BaseMapProps {
provider?: MapProvider;
initialCenter?: [number, number];
initialZoom?: number;
initialLayer?: MapLayer;
height?: string;
showControls?: boolean;
showLayerSwitcher?: boolean;
showLegend?: boolean;
showScale?: boolean;
showCoordinates?: boolean;
onMapReady?: (mapEngine: GISMapEngine) => void;
onLayerChange?: (layer: MapLayer) => void;
className?: string;
}
export interface BaseMapRef {
getMapEngine: () => GISMapEngine | null;
addMarker: (marker: Marker) => void;
addPolygon: (polygon: Polygon) => void;
setCenter: (position: MapPosition, zoom?: number) => void;
setZoom: (zoom: number) => void;
}
export const BaseMap = forwardRef<BaseMapRef, BaseMapProps>(({
provider = 'leaflet',
initialCenter = [116.4074, 39.9042],
initialZoom = 13,
initialLayer = 'satellite',
height = '600px',
showControls = true,
showLayerSwitcher = true,
showLegend = false,
showScale = true,
showCoordinates = true,
onMapReady,
onLayerChange,
className = '',
}, ref) => {
const mapContainerRef = useRef<HTMLDivElement>(null);
const mapEngineRef = useRef<GISMapEngine | null>(null);
const [mapLayer, setMapLayer] = useState<MapLayer>(initialLayer);
const [zoomLevel, setZoomLevel] = useState(initialZoom);
const [isFullscreen, setIsFullscreen] = useState(false);
const [coordinates, setCoordinates] = useState<MapPosition>({
lng: initialCenter[0],
lat: initialCenter[1],
});
const [measuring, setMeasuring] = useState(false);
const [isLoading, setIsLoading] = useState(true);
// 暴露给父组件的方法
useImperativeHandle(ref, () => ({
getMapEngine: () => mapEngineRef.current,
addMarker: (marker: Marker) => {
mapEngineRef.current?.addMarker(marker);
},
addPolygon: (polygon: Polygon) => {
mapEngineRef.current?.addPolygon(polygon);
},
setCenter: (position: MapPosition, zoom?: number) => {
mapEngineRef.current?.setCenter(position, zoom);
},
setZoom: (zoom: number) => {
mapEngineRef.current?.setZoom(zoom);
setZoomLevel(zoom);
},
}));
useEffect(() => {
if (!mapContainerRef.current) return;
const initMap = async () => {
setIsLoading(true);
try {
// 初始化地图引擎
const mapEngine = new GISMapEngine({
provider,
container: mapContainerRef.current!,
center: initialCenter,
zoom: initialZoom,
layer: initialLayer,
});
mapEngineRef.current = mapEngine;
// 通知父组件地图已就绪
if (onMapReady) {
onMapReady(mapEngine);
}
} catch (error) {
console.error('地图初始化失败:', error);
} finally {
// 延迟设置加载完成,给地图渲染一些时间
setTimeout(() => setIsLoading(false), 500);
}
};
initMap();
// 清理
return () => {
if (mapEngineRef.current) {
mapEngineRef.current.destroy();
}
};
}, []);
const handleLayerChange = (layer: string) => {
const newLayer = layer as MapLayer;
setMapLayer(newLayer);
mapEngineRef.current?.setLayer(newLayer);
toast.success(`已切换到${getLayerName(newLayer)}`);
if (onLayerChange) {
onLayerChange(newLayer);
}
};
const handleZoomIn = () => {
const newZoom = Math.min(zoomLevel + 1, 18);
setZoomLevel(newZoom);
mapEngineRef.current?.setZoom(newZoom);
};
const handleZoomOut = () => {
const newZoom = Math.max(zoomLevel - 1, 1);
setZoomLevel(newZoom);
mapEngineRef.current?.setZoom(newZoom);
};
const handleFullscreen = () => {
setIsFullscreen(!isFullscreen);
};
const getLayerName = (layer: MapLayer): string => {
const names: Record<MapLayer, string> = {
satellite: '卫星影像',
street: '电子地图',
terrain: '地形图',
hybrid: '混合图层',
};
return names[layer];
};
const getLayerIcon = (layer: MapLayer) => {
switch (layer) {
case 'satellite': return <Satellite className="w-4 h-4" />;
case 'street': return <Grid3x3 className="w-4 h-4" />;
case 'terrain': return <Mountain className="w-4 h-4" />;
case 'hybrid': return <Layers className="w-4 h-4" />;
}
};
return (
<div className={`relative ${className}`}>
<Card className={`overflow-hidden ${isFullscreen ? 'fixed inset-0 z-50' : ''}`}>
<div
ref={mapContainerRef}
className="relative w-full"
style={{ height: isFullscreen ? '100vh' : height }}
>
{/* 加载提示 */}
{isLoading && (
<div className="absolute inset-0 bg-background/80 backdrop-blur-sm flex items-center justify-center z-50">
<div className="text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-green-600 mb-4"></div>
<p className="text-sm text-muted-foreground">...</p>
</div>
</div>
)}
{/* 顶部工具栏 */}
<div className="absolute top-4 left-4 right-4 z-10 flex items-center justify-between gap-2">
{/* 图层指示器 */}
<Card className="p-2 shadow-lg bg-white/95 dark:bg-gray-800/95 backdrop-blur">
<div className="flex items-center gap-2">
{getLayerIcon(mapLayer)}
<span className="text-sm">{getLayerName(mapLayer)}</span>
</div>
</Card>
{/* 右侧控制按钮 */}
<div className="flex items-center gap-2">
{/* 图层切换 */}
{showLayerSwitcher && (
<Select value={mapLayer} onValueChange={handleLayerChange}>
<SelectTrigger className="w-40 bg-white/95 dark:bg-gray-800/95 backdrop-blur">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="satellite">
<div className="flex items-center gap-2">
<Satellite className="w-4 h-4" />
</div>
</SelectItem>
<SelectItem value="street">
<div className="flex items-center gap-2">
<Grid3x3 className="w-4 h-4" />
</div>
</SelectItem>
<SelectItem value="terrain">
<div className="flex items-center gap-2">
<Mountain className="w-4 h-4" />
</div>
</SelectItem>
<SelectItem value="hybrid">
<div className="flex items-center gap-2">
<Layers className="w-4 h-4" />
</div>
</SelectItem>
</SelectContent>
</Select>
)}
{/* 测距工具 */}
{showControls && (
<Button
variant={measuring ? 'default' : 'outline'}
size="sm"
onClick={() => setMeasuring(!measuring)}
className="bg-white/95 dark:bg-gray-800/95 backdrop-blur"
>
<Ruler className="w-4 h-4" />
</Button>
)}
{/* 全屏切换 */}
{showControls && (
<Button
variant="outline"
size="sm"
onClick={handleFullscreen}
className="bg-white/95 dark:bg-gray-800/95 backdrop-blur"
>
{isFullscreen ? (
<Minimize className="w-4 h-4" />
) : (
<Maximize className="w-4 h-4" />
)}
</Button>
)}
{/* 全屏模式关闭按钮 */}
{isFullscreen && (
<Button
variant="outline"
size="sm"
onClick={handleFullscreen}
className="bg-white/95 dark:bg-gray-800/95 backdrop-blur"
>
<X className="w-4 h-4" />
</Button>
)}
</div>
</div>
{/* 缩放控制 */}
{showControls && (
<div className="absolute top-20 right-4 z-10">
<Card className="p-2 shadow-lg bg-white/95 dark:bg-gray-800/95 backdrop-blur">
<div className="flex flex-col gap-1">
<Button
variant="ghost"
size="sm"
onClick={handleZoomIn}
disabled={zoomLevel >= 18}
>
<ZoomIn className="w-4 h-4" />
</Button>
<div className="text-center text-xs text-muted-foreground py-1 px-2">
{zoomLevel}
</div>
<Button
variant="ghost"
size="sm"
onClick={handleZoomOut}
disabled={zoomLevel <= 1}
>
<ZoomOut className="w-4 h-4" />
</Button>
</div>
</Card>
</div>
)}
{/* 比例尺 */}
{showScale && (
<div className="absolute bottom-4 left-4 z-10">
<Card className="p-2 shadow-lg bg-white/95 dark:bg-gray-800/95 backdrop-blur">
<div className="flex items-center gap-2">
<div className="w-24 h-0.5 bg-black dark:bg-white"></div>
<span className="text-xs">
{Math.round(500 / Math.pow(2, zoomLevel - 13))}m
</span>
</div>
</Card>
</div>
)}
{/* 坐标显示 */}
{showCoordinates && (
<div className="absolute bottom-4 right-4 z-10">
<Card className="px-3 py-2 shadow-lg bg-white/95 dark:bg-gray-800/95 backdrop-blur">
<div className="text-xs text-muted-foreground flex items-center gap-1">
<MapPin className="w-3 h-3" />
{coordinates.lat.toFixed(4)}°N, {coordinates.lng.toFixed(4)}°E
</div>
</Card>
</div>
)}
{/* 图例 */}
{showLegend && (
<div className="absolute top-20 left-4 z-10">
<Card className="p-3 shadow-lg bg-white/95 dark:bg-gray-800/95 backdrop-blur">
<h4 className="text-sm mb-2"></h4>
<div className="space-y-1.5 text-xs">
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-green-500 bg-opacity-30 border-2 border-green-500 rounded"></div>
<span></span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-blue-500 bg-opacity-30 border-2 border-blue-500 rounded"></div>
<span></span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-orange-500 bg-opacity-30 border-2 border-orange-500 rounded"></div>
<span></span>
</div>
</div>
</Card>
</div>
)}
{/* 测距提示 */}
{measuring && (
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-10">
<Card className="p-4 shadow-lg bg-white/95 dark:bg-gray-800/95 backdrop-blur">
<div className="text-center">
<Ruler className="w-8 h-8 mx-auto mb-2 text-green-600" />
<p className="text-sm"></p>
<p className="text-xs text-muted-foreground mt-1">
</p>
</div>
</Card>
</div>
)}
</div>
</Card>
</div>
);
});
BaseMap.displayName = 'BaseMap';