生产管理系统前端 - 提交空间数据管理开发页面

This commit is contained in:
2025-10-30 09:10:44 +08:00
parent 3239f819d0
commit 71bc00cc4e
17 changed files with 6496 additions and 13 deletions

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

View File

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

View File

@@ -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"