生产管理系统前端 - 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,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 + OpenStreetMapKey
</p>
</div>
</Card>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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