路径规划打点功能和 点击任务池中数据在地图上显示 初步

This commit is contained in:
mmc
2026-06-15 20:54:41 +08:00
parent 6e312871b8
commit a72ab15c56
5 changed files with 399 additions and 10 deletions

16
env.js
View File

@@ -9,17 +9,23 @@ const currentEnv = import.meta.env?.MODE || process.env.NODE_ENV;
// 环境配置映射
const envConfig = {
[ENV.DEV]: {
baseURL: 'http://192.168.2.59:8081',//http://192.168.2.56:8081
//baseURL: 'http://1.95.137.212:59015',//http://192.168.2.56:8081
WS_URL: 'ws://192.168.2.59:9002/ws/',
mqtt_url: "ws://192.168.2.59:8083/mqtt",
geoserver_url: "http://192.168.2.59:7000/geoserver"
//baseURL: 'http://192.168.2.59:8081',//http://192.168.2.56:8081
////baseURL: 'http://1.95.137.212:59015',//http://192.168.2.56:8081
//WS_URL: 'ws://192.168.2.59:9002/ws/',
//mqtt_url: "ws://192.168.2.59:8083/mqtt",
//geoserver_url: "http://192.168.2.59:7000/geoserver"
//baseURL: 'http://1.95.137.212:8081',//http://192.168.2.56:8081
////baseURL: 'https://serviceri.satabot.com',
//WS_URL: 'ws://1.95.137.212:9002/ws/',
//mqtt_url: "ws://1.95.137.212:59021/mqtt",
//geoserver_url: "http://1.95.137.212:59018/geoserver"
baseURL: 'http://1.95.137.212:59015',//http://192.168.2.56:8081
//baseURL: 'https://serviceri.satabot.com',
WS_URL: 'ws://1.95.137.212:59017/ws/',
mqtt_url: "ws://1.95.137.212:59021/mqtt",
geoserver_url: "http://1.95.137.212:59018/geoserver"
},
[ENV.PROD]: {
//baseURL: 'https://pvcloudservice.satabot.com',

View File

@@ -402,6 +402,25 @@ export function getRouteByTask(params) {
})
}
//增加预设路径iot/workRecord/add
export function addRoute(data) {
const config = {
url: '/iot/workRecord/add', // 修正为正确的添加接口
method: 'post',
data
};
// 如果是 FormData,不需要设置 Content-Type,axios 会自动设置
if (!(data instanceof FormData)) {
config.headers = {
'Content-Type': 'application/json'
};
}
return request(config);
}

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState, useMemo } from 'react';
import React, { useEffect, useRef, useState, useMemo, useImperativeHandle, forwardRef, useCallback } from 'react';
import carImg from '../assets/images/car.png';
import inspectImg from "../assets/images/inspectCar.png"
import planeImg from '../assets/images/plane.png';
@@ -26,7 +26,7 @@ const CYAN_GLOW_COLOR = new window.Cesium.Color(
);
window.Cesium.Ion.defaultAccessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJmNjk0NjI1MC0wZGYyLTQ1YzItYjJmZi0wY2M5ODEwODllYzIiLCJpZCI6Mzk3MTQxLCJpYXQiOjE3NzI1MjkwMjV9.U6qmakhDgXJVXwLFyWzSxAUOFZRrDkIsRUPP6CCdnKg";
export default function CesiumMap({ ifClear, ifClearPlanLine, ifClearNavLine, finishPoint, drawRealTime, realtimePosition, realtimeType, pathPoints = [], devices = [], onCameraClick, focusLocation, points = [], onPathChange, from, onClearDraw }) {
const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine, finishPoint, drawRealTime, realtimePosition, realtimeType, pathPoints = [], devices = [], onCameraClick, focusLocation, points = [], markedPoints = [], onPathChange, from, onClearDraw }, ref) => {
const containerRef = useRef(null);
const viewerRef = useRef(null);
const tooltipRef = useRef(null);
@@ -37,6 +37,7 @@ export default function CesiumMap({ ifClear, ifClearPlanLine, ifClearNavLine, fi
const iotMapRef = useRef(new Map());
const planLineRef = useRef(null); // points 规划航线
const navLineRef = useRef(null); // 完成点实时轨迹
const markedPointsRef = useRef([]); // 标记点实体
const realtimeEntityRef = useRef(null);
const realtimeLineRef = useRef(null);
@@ -295,6 +296,49 @@ export default function CesiumMap({ ifClear, ifClearPlanLine, ifClearNavLine, fi
}
}, [points]);
// 渲染绿色标记点
const drawMarkedPoints = (points) => {
const viewer = viewerRef.current;
if (!viewer) return;
// 先清空之前的标记点
markedPointsRef.current.forEach(id => {
viewer.entities.removeById(id);
});
markedPointsRef.current = [];
if (!points || points.length === 0) return;
points.forEach((p, i) => {
const id = `marked-point-${i}`;
viewer.entities.add({
id,
position: window.Cesium.Cartesian3.fromDegrees(p.lon, p.lat),
point: {
color: window.Cesium.Color.GREEN,
pixelSize: 15,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
label: {
text: `${i + 1}`,
font: '14px sans-serif',
pixelOffset: new window.Cesium.Cartesian2(0, -20),
fillColor: window.Cesium.Color.WHITE,
outlineColor: window.Cesium.Color.BLACK,
outlineWidth: 2,
style: window.Cesium.LabelStyle.FILL_AND_OUTLINE,
}
});
markedPointsRef.current.push(id);
});
};
useEffect(() => {
if (viewerRef.current) {
drawMarkedPoints(markedPoints);
}
}, [markedPoints]);
function showRobotTooltip(tooltip, robot, position) {
const statusMap = {
1: { text: "工作中", color: "#10b981" },
@@ -1753,6 +1797,29 @@ export default function CesiumMap({ ifClear, ifClearPlanLine, ifClearNavLine, fi
viewer.scene.requestRender();
}, [realtimePosition]);
// 截图功能
const captureScreenshot = useCallback(() => {
return new Promise((resolve) => {
const viewer = viewerRef.current;
if (!viewer) {
resolve(null);
return;
}
// 使用 Cesium 的截图功能
viewer.scene.render();
const canvas = viewer.scene.canvas;
const dataUrl = canvas.toDataURL('image/jpeg', 0.8);
resolve(dataUrl);
});
}, []);
// 暴露 ref 方法
useImperativeHandle(ref, () => ({
captureScreenshot
}));
return <>
<div ref={containerRef} className="w-full h-full bg-slate-900 overflow-hidden" />
<div
@@ -1764,4 +1831,6 @@ export default function CesiumMap({ ifClear, ifClearPlanLine, ifClearNavLine, fi
}}
/>
</>;
}
});
export default CesiumMap;

View File

@@ -41,6 +41,7 @@ import {
getUAVState,
getSiteDevice,
getSiteUAVList,
addRoute,
} from '../../api/device.ts';
import XboxController from './XboxController';
@@ -312,6 +313,14 @@ export default function DeviceStatusPage({
const [clearNavLine, setClearNavLine] = useState(0);
const [clearPlanLine, setClearPlanLine] = useState(0);
const timerRef = useRef<any>(null);
// 打点相关状态
const [markedPoints, setMarkedPoints] = useState([]); // 标记的点
const [completeFlag, setCompleteFlag] = useState(false); // 是否完成路径规划
const [savePath, setSavePath] = useState(null); // 保存的路径数据
const [isSaveModalVisible, setIsSaveModalVisible] = useState(false); // 保存模态框可见性
const [plotName, setPlotName] = useState(''); // 地块名称
const [isSaving, setIsSaving] = useState(false); // 是否正在保存
const mapRef = useRef(null); // 地图 ref
const runningStatus = useMemo(() => {
// 判断对象是否为空
@@ -549,6 +558,147 @@ export default function DeviceStatusPage({
return Number(val).toFixed(2);
};
// 打点功能
const handleMarkPoint = () => {
if (!realtimePosition) {
message.warning('无法获取实时位置,请检查设备连接');
return;
}
const newPoint = {
lon: realtimePosition.lon,
lat: realtimePosition.lat,
};
setMarkedPoints([...markedPoints, newPoint]);
messageApi.success(`已标记第 ${markedPoints.length + 1} 个点`);
};
// 清空标记点
const handleClearPoints = () => {
setMarkedPoints([]);
setCompleteFlag(false);
setSavePath(null);
};
// 完成功能 - 调用路径规划接口
const handleComplete = async () => {
if (markedPoints.length < 3) {
messageApi.warning('请至少标记 3 个点');
return;
}
try {
// 准备路径规划请求参数
const positionPoints = [...markedPoints];
const requestParams = {
reference: {
lat: positionPoints[0].lat,
lon: positionPoints[0].lon,
},
heading: 0,
outer: {
position: positionPoints.map(point => ({
lat: point.lat,
lon: point.lon,
})),
sideWidth: 0.0,
},
holes: [],
workType: 0,
};
const response = await fetch('https://servicepathplan.satabot.com/api/path', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestParams),
});
const result = await response.json();
if (result) {
setPoints(result?.data?.points);
setSavePath({
path: result?.data?.path, // 存储返回的路径结果
outer: requestParams?.outer?.position,
planModel: 0,
});
setCompleteFlag(true);
messageApi.success('路径规划完成');
}
} catch (error) {
console.error('路径规划失败:', error);
messageApi.error('路径规划失败,请重试');
}
};
// 打开保存模态框
const handleOpenSaveModal = () => {
if (!completeFlag) {
messageApi.warning('请先点击完成生成路径');
return;
}
setPlotName('');
setIsSaveModalVisible(true);
};
// 保存功能
const handleSave = async () => {
if (!plotName.trim()) {
messageApi.warning('请输入地块名称');
return;
}
setIsSaving(true);
try {
// 1. 获取地图截图
let screenshotDataUrl = null;
if (mapRef.current) {
screenshotDataUrl = await mapRef.current.captureScreenshot();
}
// 2. 创建 FormData
const formData = new FormData();
// 3. 添加文件
if (screenshotDataUrl) {
// 将 dataUrl 转换为 Blob
const response = await fetch(screenshotDataUrl);
const blob = await response.blob();
formData.append('file', blob, 'screenshot.jpg');
}
// 4. 添加 workRecord
const workRecord = {
workName: plotName,
siteId: stationId,
jsonData: JSON.stringify(savePath),
};
formData.append('workRecord', JSON.stringify(workRecord));
// 5. 调用保存接口
const res = await addRoute(formData);
if (res.code === 200) {
messageApi.success('保存成功');
setIsSaveModalVisible(false);
// 重置状态
setMarkedPoints([]);
setCompleteFlag(false);
setSavePath(null);
} else {
messageApi.error(res.msg || '保存失败');
}
} catch (error) {
console.error('保存失败:', error);
messageApi.error('保存失败');
} finally {
setIsSaving(false);
}
};
const realtimeType = useMemo(() => {
return isUAV ? 'plane' : 'car';
}, [isUAV]);
@@ -568,6 +718,57 @@ export default function DeviceStatusPage({
<div style={{ padding: 0 }}>
{contextHolder}
{/* 页面顶部按钮区域 */}
{!isUAV && (
<div
style={{
marginBottom: 16,
padding: '12px 16px',
background: '#f5f7fa',
borderRadius: 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 12,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<Text strong>路径规划:</Text>
<Text type="secondary">已标记 {markedPoints.length} 个点</Text>
</div>
<Space size="middle">
<Button
type="primary"
onClick={handleMarkPoint}
disabled={!realtimePosition}
>
打点
</Button>
<Button
type="default"
onClick={handleClearPoints}
>
清空标记
</Button>
<Button
type="primary"
ghost
onClick={handleComplete}
disabled={markedPoints.length < 3}
>
完成
</Button>
<Button
type="primary"
onClick={handleOpenSaveModal}
disabled={!completeFlag}
>
保存
</Button>
</Space>
</div>
)}
<div
style={{
marginBottom: 16,
@@ -739,8 +940,10 @@ export default function DeviceStatusPage({
}}
>
<CesiumMap
ref={mapRef}
devices={[mapDevices]}
points={points}
markedPoints={markedPoints}
realtimePosition={realtimePosition}
realtimeType={realtimeType}
drawRealTime={true}
@@ -911,6 +1114,57 @@ export default function DeviceStatusPage({
>
<p>{modalContent.content}</p>
</Modal>
{/* 保存路径模态框 */}
<Modal
open={isSaveModalVisible}
title="保存路径"
onOk={handleSave}
onCancel={() => setIsSaveModalVisible(false)}
okText="保存"
cancelText="取消"
confirmLoading={isSaving}
>
<div style={{ marginBottom: 16 }}>
<label style={{ display: 'block', marginBottom: 8 }}>地块名称</label>
<input
type="text"
value={plotName}
onChange={(e) => setPlotName(e.target.value)}
placeholder="请输入地块名称"
style={{
width: '100%',
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: 4,
fontSize: 14,
}}
/>
</div>
{isSaving && (
<div style={{ textAlign: 'center', padding: '16px 0' }}>
<div style={{
display: 'inline-block',
width: 30,
height: 30,
border: '3px solid #1890ff',
borderTopColor: 'transparent',
borderRadius: '50%',
animation: 'spin 0.8s linear infinite',
}} />
<div style={{ marginTop: 8, color: '#666' }}>保存中...</div>
</div>
)}
<style>
{`
@keyframes spin {
to {
transform: rotate(360deg);
}
}
`}
</style>
</Modal>
</div >
);
}

View File

@@ -31,6 +31,7 @@ import {
recoverRoute,
getDeviceTastPool,
getSiteDeviceTaskHistory,
getRouteByTask,
} from '../../api/device';
import dayjs from 'dayjs';
import DraggableBox from '../Draggable';
@@ -142,7 +143,37 @@ export default function RobotTaskPage() {
}
const handleFinishEdPoint = (point) => {
setArrivedPoint(point);
}
};
// 根据任务ID获取路径并显示在地图上
const loadTaskPath = async (taskId) => {
return;
if (!taskId) return;
try {
const res = await getRouteByTask({ taskId });
if (res?.code === 200 && res?.data) {
let pathPoints = [];
try {
if (res.data.jsonData) {
const data = typeof res.data.jsonData === 'string'
? JSON.parse(res.data.jsonData)
: res.data.jsonData;
pathPoints = data?.path || [];
}
} catch (err) {
console.error('解析任务路径数据失败:', err);
}
setPoints(pathPoints);
}
} catch (err) {
console.error('获取任务路径失败:', err);
}
};
// 任务池项点击事件
const handlePoolTaskClick = (record) => {
loadTaskPath(record.id);
};
useEffect(() => {
@@ -279,7 +310,6 @@ export default function RobotTaskPage() {
}
};
useEffect(() => {
// 如果没有选中路线,清空点位
if (!selectedPathWork?.id) {
setPoints([]);
return;
@@ -319,6 +349,13 @@ export default function RobotTaskPage() {
fetchRouteDetail();
}, [selectedPathWork]);
// 监听任务池列表变化,自动加载第一个任务的路径
useEffect(() => {
if (taskPoolList && taskPoolList.length > 0) {
loadTaskPath(taskPoolList[0].id);
}
}, [taskPoolList]);
// 获取路线列表
const fetchRouteList = async () => {
if (!stationId) return;
@@ -1225,6 +1262,10 @@ export default function RobotTaskPage() {
dataSource={taskPoolList}
pagination={false}
scroll={{ x: 800, y: 150 }}
onRow={(record) => ({
onClick: () => handlePoolTaskClick(record),
style: { cursor: 'pointer' }
})}
/>
</>
)}