路径规划打点功能和 点击任务池中数据在地图上显示 初步
This commit is contained in:
16
env.js
16
env.js
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 >
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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' }
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user