生产管理系统前端 - 提交空间数据管理开发页面
This commit is contained in:
238
crop-x/src/components/field/MapPointPicker.tsx
Normal file
238
crop-x/src/components/field/MapPointPicker.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* 地图选点组件
|
||||
* 用于在地图上选择坐标点
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Point } from '../../lib/spatialDataService';
|
||||
import { MapPin, X, Check } from 'lucide-react';
|
||||
|
||||
interface MapPointPickerProps {
|
||||
points: Point[];
|
||||
mode: 'polygon' | 'single';
|
||||
onPointsChange: (points: Point[]) => void;
|
||||
onClose?: () => void;
|
||||
height?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export function MapPointPicker({
|
||||
points,
|
||||
mode,
|
||||
onPointsChange,
|
||||
onClose,
|
||||
height = '500px',
|
||||
title = '在地图上选择坐标点'
|
||||
}: MapPointPickerProps) {
|
||||
const mapRef = useRef<HTMLDivElement>(null);
|
||||
const [map, setMap] = useState<any>(null);
|
||||
const [markers, setMarkers] = useState<any[]>([]);
|
||||
const [polygon, setPolygon] = useState<any>(null);
|
||||
|
||||
// 初始化地图
|
||||
useEffect(() => {
|
||||
if (!mapRef.current || map) return;
|
||||
|
||||
// 检查高德地图是否已加载
|
||||
if (typeof window.AMap === 'undefined') {
|
||||
console.log('💡 使用演示地图模式');
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算中心点
|
||||
const centerLat = points && points.length > 0
|
||||
? points.reduce((sum, p) => sum + p.lat, 0) / points.length
|
||||
: 39.9042;
|
||||
const centerLng = points && points.length > 0
|
||||
? points.reduce((sum, p) => sum + p.lng, 0) / points.length
|
||||
: 116.4074;
|
||||
|
||||
// 创建地图实例
|
||||
const mapInstance = new window.AMap.Map(mapRef.current, {
|
||||
zoom: 14,
|
||||
center: [centerLng, centerLat],
|
||||
mapStyle: 'amap://styles/normal',
|
||||
viewMode: '2D'
|
||||
});
|
||||
|
||||
setMap(mapInstance);
|
||||
|
||||
return () => {
|
||||
if (mapInstance) {
|
||||
mapInstance.destroy();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 绘制标记和多边形
|
||||
useEffect(() => {
|
||||
if (!map || !points) return;
|
||||
|
||||
// 清除旧标记
|
||||
markers.forEach(marker => marker.setMap(null));
|
||||
if (polygon) {
|
||||
polygon.setMap(null);
|
||||
}
|
||||
|
||||
// 添加新标记
|
||||
const newMarkers = points.map((point, index) => {
|
||||
const marker = new window.AMap.Marker({
|
||||
position: [point.lng, point.lat],
|
||||
map: map,
|
||||
title: `点 ${index + 1}`,
|
||||
label: {
|
||||
content: `${index + 1}`,
|
||||
direction: 'top'
|
||||
}
|
||||
});
|
||||
|
||||
// 点击标记删除(仅多边形模式且点数>3)
|
||||
if (mode === 'polygon' && points.length > 3) {
|
||||
marker.on('click', () => {
|
||||
const newPoints = points.filter((_, i) => i !== index);
|
||||
onPointsChange(newPoints);
|
||||
});
|
||||
}
|
||||
|
||||
return marker;
|
||||
});
|
||||
|
||||
setMarkers(newMarkers);
|
||||
|
||||
// 绘制多边形(仅多边形模式)
|
||||
if (mode === 'polygon' && points.length >= 3) {
|
||||
const path = points.map(p => [p.lng, p.lat]);
|
||||
const poly = new window.AMap.Polygon({
|
||||
path: path,
|
||||
strokeColor: '#22c55e',
|
||||
strokeWeight: 2,
|
||||
strokeOpacity: 0.8,
|
||||
fillColor: '#22c55e',
|
||||
fillOpacity: 0.2,
|
||||
map: map
|
||||
});
|
||||
setPolygon(poly);
|
||||
}
|
||||
|
||||
// 自适应显示
|
||||
if (points && points.length > 0) {
|
||||
map.setFitView();
|
||||
}
|
||||
}, [map, points]);
|
||||
|
||||
// 地图点击事件
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
|
||||
const clickHandler = (e: any) => {
|
||||
const lng = e.lnglat.getLng();
|
||||
const lat = e.lnglat.getLat();
|
||||
|
||||
if (mode === 'single') {
|
||||
// 单点模式:替换唯一的点
|
||||
onPointsChange([{ lat, lng }]);
|
||||
} else {
|
||||
// 多边形模式:添加新点
|
||||
onPointsChange([...(points || []), { lat, lng }]);
|
||||
}
|
||||
};
|
||||
|
||||
map.on('click', clickHandler);
|
||||
|
||||
return () => {
|
||||
map.off('click', clickHandler);
|
||||
};
|
||||
}, [map, points, mode]);
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden">
|
||||
{/* 工具栏 */}
|
||||
<div className="p-3 bg-gray-50 border-b flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="w-4 h-4 text-green-600" />
|
||||
<span className="text-sm font-medium">{title}</span>
|
||||
<Badge variant="outline">
|
||||
{mode === 'polygon' ? `${points.length} 个点` : '单点选择'}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{mode === 'polygon' && points.length > 0 && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onPointsChange([])}
|
||||
>
|
||||
清除所有点
|
||||
</Button>
|
||||
)}
|
||||
{onClose && (
|
||||
<Button size="sm" onClick={onClose}>
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
完成选择
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 提示信息 */}
|
||||
<div className="px-3 py-2 bg-blue-50 border-b">
|
||||
<p className="text-xs text-blue-700">
|
||||
{mode === 'polygon'
|
||||
? '💡 点击地图添加坐标点,点击标记删除该点(至少保留3个点)'
|
||||
: '💡 点击地图选择坐标点位置'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 地图容器 */}
|
||||
<div
|
||||
ref={mapRef}
|
||||
style={{ height }}
|
||||
className="w-full relative"
|
||||
>
|
||||
{/* 地图加载提示 */}
|
||||
{!map && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-gray-50">
|
||||
<div className="text-center">
|
||||
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-green-600 mb-3"></div>
|
||||
<p className="text-sm text-muted-foreground">地图加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 坐标列表 */}
|
||||
{points && points.length > 0 && (
|
||||
<div className="p-3 bg-gray-50 border-t max-h-32 overflow-y-auto">
|
||||
<div className="text-xs font-medium text-muted-foreground mb-2">
|
||||
选中的坐标点:
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{points.map((point, index) => (
|
||||
<div key={index} className="text-xs font-mono flex items-center justify-between">
|
||||
<span>
|
||||
点{index + 1}: {point.lat.toFixed(6)}, {point.lng.toFixed(6)}
|
||||
</span>
|
||||
{mode === 'polygon' && points.length > 3 && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-5 w-5 p-0"
|
||||
onClick={() => {
|
||||
const newPoints = points.filter((_, i) => i !== index);
|
||||
onPointsChange(newPoints);
|
||||
}}
|
||||
>
|
||||
<X className="w-3 h-3 text-red-500" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -25,9 +25,9 @@ export function ThemeToggle() {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled
|
||||
className="transition-colors"
|
||||
className="transition-colors h-10 w-10"
|
||||
>
|
||||
<Sun className="w-5 h-5" />
|
||||
<Sun className="size-5" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -38,12 +38,12 @@ export function ThemeToggle() {
|
||||
size="icon"
|
||||
onClick={toggleTheme}
|
||||
title={theme === 'light' ? '切换到深色模式' : '切换到浅色模式'}
|
||||
className="transition-colors"
|
||||
className="transition-colors h-10 w-10"
|
||||
>
|
||||
{theme === 'light' ? (
|
||||
<Moon className="w-5 h-5" />
|
||||
<Moon className="size-5" />
|
||||
) : (
|
||||
<Sun className="w-5 h-5" />
|
||||
<Sun className="size-5" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -134,7 +134,8 @@ export function MessageBell({ onMessageClick }: MessageBellProps) {
|
||||
<Popover open={showMessages} onOpenChange={setShowMessages}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="relative">
|
||||
<Bell className="w-5 h-5" />
|
||||
<Bell
|
||||
className="w-5 h-5" />
|
||||
{unreadCount > 0 && (
|
||||
<Badge
|
||||
className="absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center p-0 bg-red-500 text-white text-xs"
|
||||
|
||||
Reference in New Issue
Block a user