生产管理系统前端 - gis地图管理开发
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import { Card } from '@/components/ui/card';
|
||||
|
||||
export function FeatureDescription() {
|
||||
return (
|
||||
<Card className="p-4 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
|
||||
<h4 className="text-blue-900 dark:text-blue-400 mb-2">✨ GIS地图功能特性</h4>
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-1 text-sm text-blue-800 dark:text-blue-200">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-blue-600 dark:bg-blue-400"></div>
|
||||
<span>支持多种地图底图(卫星、电子、地形、混合)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-blue-600 dark:bg-blue-400"></div>
|
||||
<span>实时切换地图图层,无缝过渡</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-blue-600 dark:bg-blue-400"></div>
|
||||
<span>地图缩放、平移、全屏等基础操作</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-blue-600 dark:bg-blue-400"></div>
|
||||
<span>比例尺、坐标、图例动态显示</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-blue-600 dark:bg-blue-400"></div>
|
||||
<span>地块边界自动渲染,支持交互</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-blue-600 dark:bg-blue-400"></div>
|
||||
<span>点击地块查看详细信息</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 pt-3 border-t border-blue-300 dark:border-blue-700">
|
||||
<p className="text-xs text-blue-700 dark:text-blue-300">
|
||||
<strong>地图引擎:</strong>默认使用 Leaflet + OpenStreetMap(开源免费),也支持切换到高德地图(需配置Key)
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { BaseMap, BaseMapRef } from '@/components/shared/BaseMap';
|
||||
import { GISMapEngine, Marker, Polygon } from '@/lib/gisMapEngine';
|
||||
import { MapLayer } from '@/lib/gisMapEngine';
|
||||
import { Field } from './gisMapReducer';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface MapContainerProps {
|
||||
currentLayer: MapLayer;
|
||||
showLegend: boolean;
|
||||
fields: Field[];
|
||||
selectedField: Field | null;
|
||||
onMapReady: (engine: GISMapEngine) => void;
|
||||
onLayerChange: (layer: MapLayer) => void;
|
||||
onFieldSelect: (field: Field) => void;
|
||||
}
|
||||
|
||||
export function MapContainer({
|
||||
currentLayer,
|
||||
showLegend,
|
||||
fields,
|
||||
selectedField,
|
||||
onMapReady,
|
||||
onLayerChange,
|
||||
onFieldSelect,
|
||||
}: MapContainerProps) {
|
||||
const mapRef = useRef<BaseMapRef>(null);
|
||||
const engineRef = useRef<GISMapEngine | null>(null);
|
||||
|
||||
// 地图就绪回调
|
||||
const handleMapReady = (engine: GISMapEngine) => {
|
||||
engineRef.current = engine;
|
||||
onMapReady(engine);
|
||||
|
||||
// 添加地块多边形
|
||||
fields.forEach(field => {
|
||||
const polygon: Polygon = {
|
||||
id: field.id,
|
||||
path: field.coordinates,
|
||||
fillColor: field.color,
|
||||
strokeColor: field.color,
|
||||
fillOpacity: 0.3,
|
||||
strokeWeight: 2,
|
||||
onClick: () => {
|
||||
onFieldSelect(field);
|
||||
toast.success(`已选择: ${field.name}`);
|
||||
},
|
||||
};
|
||||
engine.addPolygon(polygon);
|
||||
});
|
||||
|
||||
// 添加地块中心点标记
|
||||
fields.forEach(field => {
|
||||
const centerLat = field.coordinates.reduce((sum, p) => sum + p.lat, 0) / field.coordinates.length;
|
||||
const centerLng = field.coordinates.reduce((sum, p) => sum + p.lng, 0) / field.coordinates.length;
|
||||
|
||||
const marker: Marker = {
|
||||
id: `marker-${field.id}`,
|
||||
position: { lat: centerLat, lng: centerLng },
|
||||
title: field.name,
|
||||
color: field.color,
|
||||
onClick: () => {
|
||||
onFieldSelect(field);
|
||||
toast.success(`已选择: ${field.name}`);
|
||||
},
|
||||
};
|
||||
engine.addMarker(marker);
|
||||
});
|
||||
|
||||
toast.success('地图加载成功,已添加3个地块');
|
||||
};
|
||||
|
||||
const handleLayerChange = (layer: MapLayer) => {
|
||||
onLayerChange(layer);
|
||||
};
|
||||
|
||||
// 当选中地块变化时,可以添加高亮效果
|
||||
useEffect(() => {
|
||||
if (engineRef.current && selectedField) {
|
||||
// 这里可以添加选中地块的高亮逻辑
|
||||
console.log('选中地块:', selectedField.name);
|
||||
}
|
||||
}, [selectedField]);
|
||||
|
||||
return (
|
||||
<BaseMap
|
||||
ref={mapRef}
|
||||
provider="leaflet"
|
||||
initialCenter={[116.4074, 39.9042]}
|
||||
initialZoom={13}
|
||||
initialLayer={currentLayer}
|
||||
height="600px"
|
||||
showControls={true}
|
||||
showLayerSwitcher={true}
|
||||
showLegend={showLegend}
|
||||
showScale={true}
|
||||
showCoordinates={true}
|
||||
onMapReady={handleMapReady}
|
||||
onLayerChange={handleLayerChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
'use client';
|
||||
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Map, Layers, MapPin, FileJson } from 'lucide-react';
|
||||
import { MapLayer } from '@/lib/gisMapEngine';
|
||||
import { Field } from './gisMapReducer';
|
||||
|
||||
interface MapInfoPanelProps {
|
||||
currentLayer: MapLayer;
|
||||
fields: Field[];
|
||||
}
|
||||
|
||||
export function MapInfoPanel({ currentLayer, fields }: MapInfoPanelProps) {
|
||||
const getLayerName = (layer: MapLayer): string => {
|
||||
const names: Record<MapLayer, string> = {
|
||||
satellite: '卫星影像',
|
||||
street: '电子地图',
|
||||
terrain: '地形图',
|
||||
hybrid: '混合图层',
|
||||
};
|
||||
return names[layer];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card className="p-4 bg-card hover:bg-muted transition-colors">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-green-50 dark:bg-green-950 rounded-lg">
|
||||
<Map className="w-5 h-5 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">地图引擎</div>
|
||||
<div className="mt-1">Leaflet + OSM</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4 bg-card hover:bg-muted transition-colors">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-blue-50 dark:bg-blue-950 rounded-lg">
|
||||
<Layers className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">当前图层</div>
|
||||
<div className="mt-1">{getLayerName(currentLayer)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4 bg-card hover:bg-muted transition-colors">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-purple-50 dark:bg-purple-950 rounded-lg">
|
||||
<MapPin className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">显示地块</div>
|
||||
<div className="mt-1">{fields.length} 个</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4 bg-card hover:bg-muted transition-colors">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-orange-50 dark:bg-orange-950 rounded-lg">
|
||||
<FileJson className="w-5 h-5 text-orange-600 dark:text-orange-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">总面积</div>
|
||||
<div className="mt-1">
|
||||
{fields.reduce((sum, f) => sum + f.area, 0).toFixed(1)} 亩
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
'use client';
|
||||
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Field } from './gisMapReducer';
|
||||
|
||||
interface SelectedFieldInfoProps {
|
||||
selectedField: Field | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function SelectedFieldInfo({ selectedField, onClose }: SelectedFieldInfoProps) {
|
||||
if (!selectedField) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="p-6 bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-green-800 dark:text-green-400 mb-1">{selectedField.name}</h3>
|
||||
<Badge
|
||||
style={{
|
||||
backgroundColor: selectedField.color + '20',
|
||||
color: selectedField.color,
|
||||
borderColor: selectedField.color,
|
||||
}}
|
||||
className="border font-light"
|
||||
>
|
||||
{selectedField.plantingMode}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onClose}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">地块面积</span>
|
||||
<div className="mt-1">{selectedField.area} 亩</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">土壤类型</span>
|
||||
<div className="mt-1">{selectedField.soilType}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">种植模式</span>
|
||||
<div className="mt-1">{selectedField.plantingMode}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
'use client';
|
||||
|
||||
import { useReducer } from 'react';
|
||||
import { MapLayer } from '@/lib/gisMapEngine';
|
||||
|
||||
// 地块数据接口
|
||||
export interface Field {
|
||||
id: string;
|
||||
name: string;
|
||||
area: number;
|
||||
coordinates: { lng: number; lat: number }[];
|
||||
soilType: string;
|
||||
plantingMode: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
// GIS状态接口
|
||||
export interface GISMapState {
|
||||
mapEngine: any;
|
||||
currentLayer: MapLayer;
|
||||
selectedField: Field | null;
|
||||
showLegend: boolean;
|
||||
fields: Field[];
|
||||
}
|
||||
|
||||
// Action类型
|
||||
export type GISMapAction =
|
||||
| { type: 'SET_MAP_ENGINE'; payload: any }
|
||||
| { type: 'SET_CURRENT_LAYER'; payload: MapLayer }
|
||||
| { type: 'SET_SELECTED_FIELD'; payload: Field | null }
|
||||
| { type: 'TOGGLE_LEGEND' }
|
||||
| { type: 'SET_FIELDS'; payload: Field[] };
|
||||
|
||||
// 初始状态
|
||||
const initialState: GISMapState = {
|
||||
mapEngine: null,
|
||||
currentLayer: 'satellite',
|
||||
selectedField: null,
|
||||
showLegend: true,
|
||||
fields: [
|
||||
{
|
||||
id: 'field-1',
|
||||
name: '地块A - 露地种植',
|
||||
area: 125.5,
|
||||
coordinates: [
|
||||
{ lng: 116.400, lat: 39.910 },
|
||||
{ lng: 116.420, lat: 39.910 },
|
||||
{ lng: 116.420, lat: 39.900 },
|
||||
{ lng: 116.400, lat: 39.900 },
|
||||
],
|
||||
soilType: '沙土',
|
||||
plantingMode: '露地',
|
||||
color: '#22c55e',
|
||||
},
|
||||
{
|
||||
id: 'field-2',
|
||||
name: '地块B - 大棚种植',
|
||||
area: 89.3,
|
||||
coordinates: [
|
||||
{ lng: 116.410, lat: 39.895 },
|
||||
{ lng: 116.425, lat: 39.895 },
|
||||
{ lng: 116.425, lat: 39.885 },
|
||||
{ lng: 116.410, lat: 39.885 },
|
||||
],
|
||||
soilType: '壤土',
|
||||
plantingMode: '大棚',
|
||||
color: '#3b82f6',
|
||||
},
|
||||
{
|
||||
id: 'field-3',
|
||||
name: '地块C - 果园',
|
||||
area: 156.8,
|
||||
coordinates: [
|
||||
{ lng: 116.395, lat: 39.890 },
|
||||
{ lng: 116.408, lat: 39.890 },
|
||||
{ lng: 116.408, lat: 39.878 },
|
||||
{ lng: 116.395, lat: 39.878 },
|
||||
],
|
||||
soilType: '粘土',
|
||||
plantingMode: '果园',
|
||||
color: '#f97316',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Reducer函数
|
||||
export function gisMapReducer(state: GISMapState, action: GISMapAction): GISMapState {
|
||||
switch (action.type) {
|
||||
case 'SET_MAP_ENGINE':
|
||||
return { ...state, mapEngine: action.payload };
|
||||
|
||||
case 'SET_CURRENT_LAYER':
|
||||
return { ...state, currentLayer: action.payload };
|
||||
|
||||
case 'SET_SELECTED_FIELD':
|
||||
return { ...state, selectedField: action.payload };
|
||||
|
||||
case 'TOGGLE_LEGEND':
|
||||
return { ...state, showLegend: !state.showLegend };
|
||||
|
||||
case 'SET_FIELDS':
|
||||
return { ...state, fields: action.payload };
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出初始状态和类型
|
||||
export { initialState };
|
||||
export type { GISMapAction, Field, GISMapState };
|
||||
@@ -1,18 +1,93 @@
|
||||
'use client';
|
||||
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { useReducer } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Layers } from 'lucide-react';
|
||||
import {
|
||||
gisMapReducer,
|
||||
initialState,
|
||||
GISMapState,
|
||||
GISMapAction,
|
||||
Field
|
||||
} from './components/gisMapReducer';
|
||||
import { MapContainer } from './components/MapContainer';
|
||||
import { SelectedFieldInfo } from './components/SelectedFieldInfo';
|
||||
import { MapInfoPanel } from './components/MapInfoPanel';
|
||||
import { FeatureDescription } from './components/FeatureDescription';
|
||||
|
||||
export default function GISMapPage() {
|
||||
const [state, dispatch] = useReducer(gisMapReducer, initialState);
|
||||
|
||||
// 地图就绪回调
|
||||
const handleMapReady = (engine: any) => {
|
||||
dispatch({ type: 'SET_MAP_ENGINE', payload: engine });
|
||||
};
|
||||
|
||||
// 图层切换
|
||||
const handleLayerChange = (layer: any) => {
|
||||
dispatch({ type: 'SET_CURRENT_LAYER', payload: layer });
|
||||
};
|
||||
|
||||
// 地块选择
|
||||
const handleFieldSelect = (field: Field) => {
|
||||
dispatch({ type: 'SET_SELECTED_FIELD', payload: field });
|
||||
};
|
||||
|
||||
// 切换图例显示
|
||||
const handleToggleLegend = () => {
|
||||
dispatch({ type: 'TOGGLE_LEGEND' });
|
||||
};
|
||||
|
||||
// 关闭选中地块
|
||||
const handleCloseSelectedField = () => {
|
||||
dispatch({ type: 'SET_SELECTED_FIELD', payload: null });
|
||||
};
|
||||
|
||||
export default function GisPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<h2 className="text-xl font-semibold">地块GIS地图</h2>
|
||||
<div className="p-3 bg-muted rounded-lg mt-3">
|
||||
<p className="text-sm">
|
||||
<strong>页面路径:</strong> /land-information/map/gis
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-green-800 dark:text-green-400">GIS地图管理</h2>
|
||||
<p className="text-muted-foreground">
|
||||
集成多种底图的智慧农业GIS地图系统
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleToggleLegend}
|
||||
>
|
||||
<Layers className="w-4 h-4 mr-2" />
|
||||
{state.showLegend ? '隐藏' : '显示'}图例
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 地图组件 */}
|
||||
<MapContainer
|
||||
currentLayer={state.currentLayer}
|
||||
showLegend={state.showLegend}
|
||||
fields={state.fields}
|
||||
selectedField={state.selectedField}
|
||||
onMapReady={handleMapReady}
|
||||
onLayerChange={handleLayerChange}
|
||||
onFieldSelect={handleFieldSelect}
|
||||
/>
|
||||
|
||||
{/* 选中地块信息 */}
|
||||
<SelectedFieldInfo
|
||||
selectedField={state.selectedField}
|
||||
onClose={handleCloseSelectedField}
|
||||
/>
|
||||
|
||||
{/* 地图信息面板 */}
|
||||
<MapInfoPanel
|
||||
currentLayer={state.currentLayer}
|
||||
fields={state.fields}
|
||||
/>
|
||||
|
||||
{/* 功能说明 */}
|
||||
<FeatureDescription />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user