生产管理系统前端 - 更新瓦力提交的产品原型到参考目录
This commit is contained in:
486
src/GEOMETRY_MAP_PICKER_IMPLEMENTATION.md
Normal file
486
src/GEOMETRY_MAP_PICKER_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,486 @@
|
||||
# 🔧 几何计算工具 - 地图选点实现指南
|
||||
|
||||
## 📋 实现概述
|
||||
|
||||
本文档说明如何在几何计算工具中集成地图选点功能。
|
||||
|
||||
---
|
||||
|
||||
## ✅ 已完成的工作
|
||||
|
||||
### 1. 创建MapPointPicker组件
|
||||
|
||||
**文件位置:** `/components/field/MapPointPicker.tsx`
|
||||
|
||||
**功能:**
|
||||
- ✅ 高德地图集成
|
||||
- ✅ 多边形和单点两种模式
|
||||
- ✅ 标记管理和多边形绘制
|
||||
- ✅ 点击添加/删除坐标点
|
||||
- ✅ 坐标列表显示
|
||||
|
||||
### 2. 更新状态管理
|
||||
|
||||
**在 FieldSpatialQuery.tsx 中添加:**
|
||||
|
||||
```typescript
|
||||
// 地图选点模式
|
||||
const [mapPickMode, setMapPickMode] = useState<'polygon' | 'distance-p1' | 'distance-p2' | null>(null);
|
||||
const [editingPointIndex, setEditingPointIndex] = useState<number | null>(null);
|
||||
```
|
||||
|
||||
### 3. 导入组件
|
||||
|
||||
```typescript
|
||||
import { MapPointPicker } from './MapPointPicker';
|
||||
import { MousePointer2, Edit3 } from 'lucide-react';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔨 需要手动完成的集成
|
||||
|
||||
由于文件中存在重复的TabsContent结构,需要手动修改。以下是集成步骤:
|
||||
|
||||
### 步骤1: 修改面积计算TabsContent
|
||||
|
||||
**找到这部分代码(约第1470行):**
|
||||
|
||||
```typescript
|
||||
{/* 面积计算 */}
|
||||
<TabsContent value="area" className="space-y-4">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3>多边形坐标点</h3>
|
||||
<Button size="sm" onClick={handleAddGeomPoint}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
添加点
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||
{geomPoints.map((point, index) => (
|
||||
// ... 坐标输入框
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button onClick={handleCalculateArea} ...>
|
||||
计算面积 (ST_Area)
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
{/* 结果显示 */}
|
||||
</TabsContent>
|
||||
```
|
||||
|
||||
**替换为:**
|
||||
|
||||
```typescript
|
||||
{/* 面积计算 */}
|
||||
<TabsContent value="area" className="space-y-4">
|
||||
{/* 选择方式切换 */}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={mapPickMode === 'polygon' ? 'default' : 'outline'}
|
||||
onClick={() => setMapPickMode(mapPickMode === 'polygon' ? null : 'polygon')}
|
||||
className="flex-1"
|
||||
>
|
||||
<MousePointer2 className="w-4 h-4 mr-2" />
|
||||
{mapPickMode === 'polygon' ? '正在使用地图选点' : '地图选点'}
|
||||
</Button>
|
||||
<Button
|
||||
variant={mapPickMode === null ? 'default' : 'outline'}
|
||||
onClick={() => setMapPickMode(null)}
|
||||
className="flex-1"
|
||||
>
|
||||
<Edit3 className="w-4 h-4 mr-2" />
|
||||
手动输入坐标
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{mapPickMode === 'polygon' ? (
|
||||
/* 地图选点模式 */
|
||||
<MapPointPicker
|
||||
points={geomPoints}
|
||||
mode="polygon"
|
||||
onPointsChange={setGeomPoints}
|
||||
height="400px"
|
||||
title="在地图上选择多边形顶点"
|
||||
/>
|
||||
) : (
|
||||
/* 手动输入模式 */
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3>多边形坐标点</h3>
|
||||
<Button size="sm" onClick={handleAddGeomPoint}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
添加点
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||
{geomPoints.map((point, index) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="w-16">
|
||||
点 {index + 1}
|
||||
</Badge>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.000001"
|
||||
value={point.lat}
|
||||
onChange={(e) => handleUpdateGeomPoint(index, 'lat', e.target.value)}
|
||||
placeholder="纬度"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.000001"
|
||||
value={point.lng}
|
||||
onChange={(e) => handleUpdateGeomPoint(index, 'lng', e.target.value)}
|
||||
placeholder="经度"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleRemoveGeomPoint(index)}
|
||||
disabled={geomPoints.length <= 3}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-red-500" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleCalculateArea}
|
||||
className="w-full bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
<Calculator className="w-4 h-4 mr-2" />
|
||||
计算面积 (ST_Area)
|
||||
</Button>
|
||||
|
||||
{/* 结果显示部分保持不变 */}
|
||||
{geomResult && geomResult.type === 'area' && (
|
||||
// ... 原有的结果显示代码
|
||||
)}
|
||||
</TabsContent>
|
||||
```
|
||||
|
||||
### 步骤2: 修改周长计算TabsContent
|
||||
|
||||
使用相同的模式修改周长计算部分(约第1550行)
|
||||
|
||||
### 步骤3: 修改中心点计算TabsContent
|
||||
|
||||
使用相同的模式修改中心点计算部分(约第1610行)
|
||||
|
||||
### 步骤4: 修改包围盒计算TabsContent
|
||||
|
||||
使用相同的模式修改包围盒计算部分(最后一个多边形计算)
|
||||
|
||||
### 步骤5: 修改距离计算TabsContent
|
||||
|
||||
**找到距离计算部分:**
|
||||
|
||||
```typescript
|
||||
{/* 距离计算 */}
|
||||
<TabsContent value="distance" className="space-y-4">
|
||||
<Card className="p-6">
|
||||
<h3 className="mb-4">两点坐标</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="mb-2 font-medium">点1坐标</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-sm text-muted-foreground">纬度</label>
|
||||
<Input ... />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-muted-foreground">经度</label>
|
||||
<Input ... />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 点2坐标 - 类似结构 */}
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
```
|
||||
|
||||
**替换为:**
|
||||
|
||||
```typescript
|
||||
{/* 距离计算 */}
|
||||
<TabsContent value="distance" className="space-y-4">
|
||||
{/* 点1选择 */}
|
||||
<Card className="p-6">
|
||||
<h3 className="mb-4">点1坐标</h3>
|
||||
|
||||
{/* 切换按钮 */}
|
||||
<div className="flex gap-2 mb-4">
|
||||
<Button
|
||||
variant={mapPickMode === 'distance-p1' ? 'default' : 'outline'}
|
||||
onClick={() => setMapPickMode(mapPickMode === 'distance-p1' ? null : 'distance-p1')}
|
||||
className="flex-1"
|
||||
>
|
||||
<MousePointer2 className="w-4 h-4 mr-2" />
|
||||
{mapPickMode === 'distance-p1' ? '正在使用地图选点' : '地图选点'}
|
||||
</Button>
|
||||
<Button
|
||||
variant={mapPickMode === null || mapPickMode === 'distance-p2' ? 'default' : 'outline'}
|
||||
onClick={() => setMapPickMode(null)}
|
||||
className="flex-1"
|
||||
>
|
||||
<Edit3 className="w-4 h-4 mr-2" />
|
||||
手动输入坐标
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{mapPickMode === 'distance-p1' ? (
|
||||
<MapPointPicker
|
||||
points={[distPoint1]}
|
||||
mode="single"
|
||||
onPointsChange={(points) => setDistPoint1(points[0] || distPoint1)}
|
||||
height="300px"
|
||||
title="在地图上选择点1位置"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-sm text-muted-foreground">纬度</label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.000001"
|
||||
value={distPoint1.lat}
|
||||
onChange={(e) => setDistPoint1({ ...distPoint1, lat: parseFloat(e.target.value) || 0 })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-muted-foreground">经度</label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.000001"
|
||||
value={distPoint1.lng}
|
||||
onChange={(e) => setDistPoint1({ ...distPoint1, lng: parseFloat(e.target.value) || 0 })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 点2选择 - 类似结构,使用 mapPickMode === 'distance-p2' 和 distPoint2 */}
|
||||
<Card className="p-6">
|
||||
<h3 className="mb-4">点2坐标</h3>
|
||||
|
||||
<div className="flex gap-2 mb-4">
|
||||
<Button
|
||||
variant={mapPickMode === 'distance-p2' ? 'default' : 'outline'}
|
||||
onClick={() => setMapPickMode(mapPickMode === 'distance-p2' ? null : 'distance-p2')}
|
||||
className="flex-1"
|
||||
>
|
||||
<MousePointer2 className="w-4 h-4 mr-2" />
|
||||
{mapPickMode === 'distance-p2' ? '正在使用地图选点' : '地图选点'}
|
||||
</Button>
|
||||
<Button
|
||||
variant={mapPickMode === null || mapPickMode === 'distance-p1' ? 'default' : 'outline'}
|
||||
onClick={() => setMapPickMode(null)}
|
||||
className="flex-1"
|
||||
>
|
||||
<Edit3 className="w-4 h-4 mr-2" />
|
||||
手动输入坐标
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{mapPickMode === 'distance-p2' ? (
|
||||
<MapPointPicker
|
||||
points={[distPoint2]}
|
||||
mode="single"
|
||||
onPointsChange={(points) => setDistPoint2(points[0] || distPoint2)}
|
||||
height="300px"
|
||||
title="在地图上选择点2位置"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-sm text-muted-foreground">纬度</label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.000001"
|
||||
value={distPoint2.lat}
|
||||
onChange={(e) => setDistPoint2({ ...distPoint2, lat: parseFloat(e.target.value) || 0 })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-muted-foreground">经度</label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.000001"
|
||||
value={distPoint2.lng}
|
||||
onChange={(e) => setDistPoint2({ ...distPoint2, lng: parseFloat(e.target.value) || 0 })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Button
|
||||
onClick={handleCalculateDistance}
|
||||
className="w-full bg-orange-600 hover:bg-orange-700"
|
||||
>
|
||||
<MapIcon className="w-4 h-4 mr-2" />
|
||||
计算距离 (ST_Distance)
|
||||
</Button>
|
||||
|
||||
{/* 结果显示部分保持不变 */}
|
||||
</TabsContent>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 修改清单
|
||||
|
||||
需要修改的5个TabsContent:
|
||||
|
||||
- [ ] **面积计算** (area) - 多边形模式
|
||||
- [ ] **周长计算** (perimeter) - 多边形模式
|
||||
- [ ] **中心点计算** (centroid) - 多边形模式
|
||||
- [ ] **包围盒计算** (bbox) - 多边形模式
|
||||
- [ ] **距离计算** (distance) - 双点模式
|
||||
|
||||
---
|
||||
|
||||
## 🎯 关键要点
|
||||
|
||||
### 1. 模式切换按钮
|
||||
|
||||
```typescript
|
||||
<Button
|
||||
variant={mapPickMode === 'polygon' ? 'default' : 'outline'}
|
||||
onClick={() => setMapPickMode(mapPickMode === 'polygon' ? null : 'polygon')}
|
||||
>
|
||||
地图选点
|
||||
</Button>
|
||||
```
|
||||
|
||||
### 2. 条件渲染
|
||||
|
||||
```typescript
|
||||
{mapPickMode === 'polygon' ? (
|
||||
<MapPointPicker ... />
|
||||
) : (
|
||||
<Card>手动输入</Card>
|
||||
)}
|
||||
```
|
||||
|
||||
### 3. MapPointPicker属性
|
||||
|
||||
```typescript
|
||||
<MapPointPicker
|
||||
points={geomPoints} // 坐标数组
|
||||
mode="polygon" // 或 "single"
|
||||
onPointsChange={setGeomPoints} // 更新回调
|
||||
height="400px" // 地图高度
|
||||
title="选择坐标点" // 标题
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 测试检查
|
||||
|
||||
完成集成后,测试以下功能:
|
||||
|
||||
### 面积计算
|
||||
- [ ] 切换到地图选点模式
|
||||
- [ ] 在地图上点击添加4个点
|
||||
- [ ] 验证多边形自动绘制
|
||||
- [ ] 点击标记删除点
|
||||
- [ ] 切换回手动输入模式
|
||||
- [ ] 验证坐标保留
|
||||
- [ ] 计算面积成功
|
||||
|
||||
### 周长计算
|
||||
- [ ] 使用相同测试流程
|
||||
|
||||
### 中心点计算
|
||||
- [ ] 使用相同测试流程
|
||||
|
||||
### 距离计算
|
||||
- [ ] 点1使用地图选择
|
||||
- [ ] 点2使用地图选择
|
||||
- [ ] 验证两点标记显示
|
||||
- [ ] 计算距离成功
|
||||
|
||||
### 包围盒计算
|
||||
- [ ] 使用相同测试流程
|
||||
|
||||
---
|
||||
|
||||
## 🐛 可能的问题
|
||||
|
||||
### 问题1: 地图未显示
|
||||
|
||||
**原因:** 高德地图SDK未加载
|
||||
**解决:** 检查mapLoader.ts是否正常工作
|
||||
|
||||
### 问题2: 点击地图无反应
|
||||
|
||||
**原因:** 事件监听未生效
|
||||
**解决:** 检查MapPointPicker组件的useEffect
|
||||
|
||||
### 问题3: 标记位置不准
|
||||
|
||||
**原因:** 经纬度顺序错误
|
||||
**解决:** 高德地图使用[lng, lat]顺序
|
||||
|
||||
### 问题4: 多边形未绘制
|
||||
|
||||
**原因:** 点数不足3个
|
||||
**解决:** 至少添加3个点才能形成多边形
|
||||
|
||||
---
|
||||
|
||||
## 🎉 完成后的效果
|
||||
|
||||
用户可以:
|
||||
|
||||
1. **灵活选择输入方式**
|
||||
- 地图直观选点
|
||||
- 手动精确输入
|
||||
- 随时切换
|
||||
|
||||
2. **实时可视化反馈**
|
||||
- 标记即时显示
|
||||
- 多边形自动绘制
|
||||
- 坐标实时更新
|
||||
|
||||
3. **便捷操作**
|
||||
- 点击添加
|
||||
- 点击删除
|
||||
- 一键清除
|
||||
|
||||
4. **保留原有功能**
|
||||
- 所有计算功能不变
|
||||
- 精度保持不变
|
||||
- 结果显示不变
|
||||
|
||||
---
|
||||
|
||||
## 📚 相关文件
|
||||
|
||||
- `/components/field/MapPointPicker.tsx` - 地图选点组件
|
||||
- `/components/field/FieldSpatialQuery.tsx` - 空间数据管理主组件
|
||||
- `/lib/spatialDataService.ts` - 空间计算服务
|
||||
- `/lib/mapLoader.ts` - 地图SDK加载器
|
||||
|
||||
---
|
||||
|
||||
**创建日期:** 2025-10-18
|
||||
**版本:** v4.0
|
||||
**状态:** 组件已创建,需手动集成
|
||||
**维护团队:** 智慧农业研发中心
|
||||
Reference in New Issue
Block a user