diff --git a/env.js b/env.js index 17630ec..465aa70 100644 --- a/env.js +++ b/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', diff --git a/src/api/device.ts b/src/api/device.ts index 75ace55..e4bda2b 100644 --- a/src/api/device.ts +++ b/src/api/device.ts @@ -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); +} + diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 4af2a91..7ee055b 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -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 <>
; -} +}); + +export default CesiumMap; diff --git a/src/components/devices/DeviceStatusPage.tsx b/src/components/devices/DeviceStatusPage.tsx index d860ce2..b5c57ff 100644 --- a/src/components/devices/DeviceStatusPage.tsx +++ b/src/components/devices/DeviceStatusPage.tsx @@ -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(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({
{contextHolder} + {/* 页面顶部按钮区域 */} + {!isUAV && ( +
+
+ 路径规划: + 已标记 {markedPoints.length} 个点 +
+ + + + + + +
+ )} +

{modalContent.content}

+ + {/* 保存路径模态框 */} + setIsSaveModalVisible(false)} + okText="保存" + cancelText="取消" + confirmLoading={isSaving} + > +
+ + setPlotName(e.target.value)} + placeholder="请输入地块名称" + style={{ + width: '100%', + padding: '8px 12px', + border: '1px solid #d9d9d9', + borderRadius: 4, + fontSize: 14, + }} + /> +
+ {isSaving && ( +
+
+
保存中...
+
+ )} + +
); } diff --git a/src/components/devices/RobotTaskPage.tsx b/src/components/devices/RobotTaskPage.tsx index 3df1cee..7799668 100644 --- a/src/components/devices/RobotTaskPage.tsx +++ b/src/components/devices/RobotTaskPage.tsx @@ -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' } + })} /> )}