From af4741fb395307e05a960ba5d669b6ed79fabce5 Mon Sep 17 00:00:00 2001 From: qinjian Date: Thu, 21 Aug 2025 10:18:28 +0800 Subject: [PATCH] =?UTF-8?q?fix=20&=20feat:=20-=20=E6=91=84=E5=83=8F?= =?UTF-8?q?=E5=A4=B4=E6=B5=81=E5=88=B7=E6=96=B0=E5=87=BA=E7=8E=B0=E6=AE=8B?= =?UTF-8?q?=E5=BD=B1=E9=97=AE=E9=A2=98=20-=20=E5=AE=9E=E6=97=B6=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=9B=BE=E8=A1=A8=E6=98=BE=E7=A4=BA=E9=87=87=E6=A0=B7?= =?UTF-8?q?=E9=A2=91=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wuyuanbiaoba/components/CameraView.jsx | 98 ++++++++++++++----- .../components/RealtimeCharts.jsx | 73 +++++++++----- .../wuyuanbiaoba/hooks/useTargetStorage.js | 4 +- 3 files changed, 125 insertions(+), 50 deletions(-) diff --git a/client/src/sections/wuyuanbiaoba/components/CameraView.jsx b/client/src/sections/wuyuanbiaoba/components/CameraView.jsx index 9b26fb8..fdf8434 100644 --- a/client/src/sections/wuyuanbiaoba/components/CameraView.jsx +++ b/client/src/sections/wuyuanbiaoba/components/CameraView.jsx @@ -16,8 +16,8 @@ const CameraView = ({ const [scale, setScale] = useState(1.0); const [translateX, setTranslateX] = useState(0); const [translateY, setTranslateY] = useState(0); - const [videoNaturalWidth, setVideoNaturalWidth] = useState(1920); - const [videoNaturalHeight, setVideoNaturalHeight] = useState(1080); + const [videoNaturalWidth, setVideoNaturalWidth] = useState(0); + const [videoNaturalHeight, setVideoNaturalHeight] = useState(0); const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); // 交互状态 @@ -37,6 +37,7 @@ const CameraView = ({ const [currentDrawingRect, setCurrentDrawingRect] = useState(null); const [hoveredRectIndex, setHoveredRectIndex] = useState(-1); const [isSaving, setIsSaving] = useState(false); // 保存状态管理 + const [streamError, setStreamError] = useState(false); // 视频流断开状态 // 使用WebSocket连接 const { isConnected, sendMessage } = useWebSocket(); @@ -76,7 +77,7 @@ const CameraView = ({ canvas.style.width = rect.width + "px"; canvas.style.height = rect.height + "px"; - // console.log("resizeCanvas: 画布大小已调整", { width: rect.width, height: rect.height }); + // console.log("resizeCanvas: 画布大小已调整", { width: rect.width, height: rect.height }); // 延迟调用applyTransform,确保画布已经完全设置好 setTimeout(() => { @@ -87,12 +88,13 @@ const CameraView = ({ // 图片加载完成 const handleImageLoad = () => { + setStreamError(false); // 恢复正常 if (imgRef.current) { const img = imgRef.current; if (img.naturalWidth && img.naturalHeight) { setVideoNaturalWidth(img.naturalWidth); setVideoNaturalHeight(img.naturalHeight); - // console.log(`handleImageLoad: 视频原始分辨率: ${img.naturalWidth}x${img.naturalHeight}`); + // console.log(`handleImageLoad: 视频原始分辨率: ${img.naturalWidth}x${img.naturalHeight}`); } resizeCanvas(); // 视频加载完成后,确保重新绘制矩形框 @@ -103,6 +105,18 @@ const CameraView = ({ } }; + // 图片加载失败 + const handleImageError = () => { + setStreamError(true); + // 自动重连,3秒后尝试刷新图片src + setTimeout(() => { + setStreamError(false); + if (imgRef.current) { + imgRef.current.src = streamUrl + "?t=" + Date.now(); + } + }, 3000); + }; + // 鼠标按下 const handleMouseDown = (e) => { const container = containerRef.current; @@ -224,7 +238,7 @@ const CameraView = ({ (target) => target.id === clickedRectangle.id ); if (targetData) { - // console.log("点击矩形框,弹出标靶详情:", targetData); + // console.log("点击矩形框,弹出标靶详情:", targetData); onRectangleClick(targetData); } } @@ -272,7 +286,8 @@ const CameraView = ({ const templateParams = selectedTemplate ? { // 从模板中获取参数 - name: selectedTemplate.name || `target${rectangles.length + 1}`, + name: + selectedTemplate.name || `target${rectangles.length + 1}`, radius: selectedTemplate.physicalRadius || 40.0, isReferencePoint: selectedTemplate.isBaseline || false, gradientThreshold: @@ -312,13 +327,13 @@ const CameraView = ({ setRectangles((prev) => [...prev, newRect]); - // console.log("新建矩形:", newRect); + // console.log("新建矩形:", newRect); if (selectedTemplate) { - // console.log("使用模板参数:", selectedTemplate); + // console.log("使用模板参数:", selectedTemplate); } else { - // console.log("使用默认参数"); + // console.log("使用默认参数"); } - // logRectangleData(); + // logRectangleData(); } }; // 拖拽已存在的矩形 const dragExistingRectangle = (mouseX, mouseY) => { @@ -542,11 +557,15 @@ const CameraView = ({ ctx.clearRect(0, 0, canvas.width, canvas.height); // 如果视频没有加载完成,直接返回 - if (!videoNaturalWidth || !videoNaturalHeight) { - console.log("redrawAllRectangles: 视频尺寸未准备好", { - videoNaturalWidth, - videoNaturalHeight, - }); + if ( + !videoNaturalWidth || + !videoNaturalHeight || + videoNaturalWidth < 100 || + videoNaturalHeight < 100 + ) { + // 清空画布,防止残影 + const ctx = canvas.getContext("2d"); + ctx.clearRect(0, 0, canvas.width, canvas.height); return; } @@ -790,7 +809,14 @@ const CameraView = ({ setIsSaving(false); }, 1000); } - }, [rectangles, isConnected, sendMessage, refreshTargets, onClearSelection, isSaving]); + }, [ + rectangles, + isConnected, + sendMessage, + refreshTargets, + onClearSelection, + isSaving, + ]); // 全局滚轮事件处理,防止alt+滚轮触发页面滚动 useEffect(() => { @@ -843,7 +869,6 @@ const CameraView = ({ return () => cancelAnimationFrame(rafId); }, [rectangles]); - // 从标靶数据初始化矩形框 useEffect(() => { // console.log("targets 或 loading 状态变化:", { @@ -852,7 +877,13 @@ const CameraView = ({ // targetsLength: targets?.length, // }); - if (!loading && targets && targets.length > 0) { + if ( + !loading && + targets && + targets.length > 0 && + videoNaturalWidth > 0 && + videoNaturalHeight > 0 + ) { // console.log("从标靶数据初始化矩形框:", targets); const initialRectangles = targets @@ -912,7 +943,7 @@ const CameraView = ({ setRectangles([]); // console.log("标靶数据为空,已清空矩形框"); } - }, [targets, loading]); // 只依赖targets和loading状态 + }, [targets, loading, videoNaturalWidth, videoNaturalHeight]); // 只依赖targets和loading状态 // 专门监听rectangles变化并重绘 useEffect(() => { @@ -1058,7 +1089,7 @@ const CameraView = ({ src={streamUrl} alt="MJPEG 视频流" onLoad={handleImageLoad} - onError={() => console.error("视频流加载失败")} + onError={handleImageError} style={{ position: "absolute", left: 0, @@ -1070,6 +1101,27 @@ const CameraView = ({ pointerEvents: "none", }} /> + {/* 视频流断开提示 */} + {streamError && ( +
+ 视频流断开,正在尝试重连... +
+ )} {/* 绘制画布 */} @@ -1121,7 +1173,9 @@ const CameraView = ({ position: "absolute", top: "10px", right: "10px", - backgroundColor: isSaving ? "rgba(128, 128, 128, 0.7)" : "rgba(0, 100, 0, 0.7)", + backgroundColor: isSaving + ? "rgba(128, 128, 128, 0.7)" + : "rgba(0, 100, 0, 0.7)", color: "white", border: "1px solid rgba(255, 255, 255, 0.3)", borderRadius: "4px", @@ -1162,7 +1216,7 @@ const CameraView = ({ }} >
- 分辨率: {videoNaturalWidth} × {videoNaturalHeight} + 摄像头分辨率: {videoNaturalWidth} × {videoNaturalHeight}
缩放: {Math.round(scale * 100)}%
diff --git a/client/src/sections/wuyuanbiaoba/components/RealtimeCharts.jsx b/client/src/sections/wuyuanbiaoba/components/RealtimeCharts.jsx index 324e4d2..10520bf 100644 --- a/client/src/sections/wuyuanbiaoba/components/RealtimeCharts.jsx +++ b/client/src/sections/wuyuanbiaoba/components/RealtimeCharts.jsx @@ -60,7 +60,7 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => { labels: [], deviceIds: [], timeGroups: {}, - sortedTimes: [] + sortedTimes: [], }; } @@ -70,7 +70,9 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => { data.forEach((item) => { if (!item || !item.updateTime) return; - const timeKey = Math.floor(new Date(item.updateTime).getTime() / 1000); // 按秒分组 + const timeKey = Math.floor( + new Date(item.updateTime).getTime() / 1000 + ); // 按秒分组 if (!timeGroups[timeKey]) { timeGroups[timeKey] = []; } @@ -84,25 +86,30 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => { .reverse(); // 获取所有设备ID - const deviceIds = [...new Set(data.map((item) => item?.deviceId).filter(Boolean))].sort(); + const deviceIds = [ + ...new Set(data.map((item) => item?.deviceId).filter(Boolean)), + ].sort(); // 生成时间标签 const labels = sortedTimes.map((timeKey) => { - return new Date(Number(timeKey) * 1000).toLocaleTimeString("zh-CN", { - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }); + return new Date(Number(timeKey) * 1000).toLocaleTimeString( + "zh-CN", + { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + } + ); }); return { labels, deviceIds, timeGroups, sortedTimes }; } catch (error) { - console.error('数据采样出错:', error); + console.error("数据采样出错:", error); return { labels: [], deviceIds: [], timeGroups: {}, - sortedTimes: [] + sortedTimes: [], }; } }; @@ -110,7 +117,8 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => { // 准备X轴图表数据 const xChartData = useMemo(() => { try { - const { labels, deviceIds, timeGroups, sortedTimes } = sampleData(tableData); + const { labels, deviceIds, timeGroups, sortedTimes } = + sampleData(tableData); if (!deviceIds || deviceIds.length === 0) { return { labels: [], datasets: [] }; @@ -122,7 +130,9 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => { const deviceData = timeData.find( (item) => item.deviceId === deviceId ); - return deviceData && deviceData.xValue !== undefined ? parseFloat(deviceData.xValue) : null; + return deviceData && deviceData.xValue !== undefined + ? parseFloat(deviceData.xValue) + : null; }); return { @@ -140,7 +150,7 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => { return { labels, datasets }; } catch (error) { - console.error('准备X轴图表数据出错:', error); + console.error("准备X轴图表数据出错:", error); return { labels: [], datasets: [] }; } }, [tableData]); @@ -148,7 +158,8 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => { // 准备Y轴图表数据 const yChartData = useMemo(() => { try { - const { labels, deviceIds, timeGroups, sortedTimes } = sampleData(tableData); + const { labels, deviceIds, timeGroups, sortedTimes } = + sampleData(tableData); if (!deviceIds || deviceIds.length === 0) { return { labels: [], datasets: [] }; @@ -160,7 +171,9 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => { const deviceData = timeData.find( (item) => item.deviceId === deviceId ); - return deviceData && deviceData.yValue !== undefined ? parseFloat(deviceData.yValue) : null; + return deviceData && deviceData.yValue !== undefined + ? parseFloat(deviceData.yValue) + : null; }); return { @@ -178,7 +191,7 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => { return { labels, datasets }; } catch (error) { - console.error('准备Y轴图表数据出错:', error); + console.error("准备Y轴图表数据出错:", error); return { labels: [], datasets: [] }; } }, [tableData]); @@ -301,9 +314,9 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => { // 添加调试信息 // useEffect(() => { - // console.log('RealtimeCharts - tableData:', tableData); - // console.log('RealtimeCharts - xChartData:', xChartData); - // console.log('RealtimeCharts - yChartData:', yChartData); + // console.log('RealtimeCharts - tableData:', tableData); + // console.log('RealtimeCharts - xChartData:', xChartData); + // console.log('RealtimeCharts - yChartData:', yChartData); // }, [tableData, xChartData, yChartData]); // 如果没有数据,显示空状态 @@ -325,13 +338,15 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => { style={{ marginLeft: "16px", fontSize: "12px" }} /> -
+

等待实时数据...

@@ -356,7 +371,13 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => { text={`实时更新 - 最后更新: ${lastUpdateTime.toLocaleTimeString()}`} style={{ marginLeft: "16px", fontSize: "12px" }} /> + +
{ if (targetDataResponse && targetDataResponse.values) { try { const responseValues = targetDataResponse.values; - console.log('收到标靶数据:', responseValues); + // console.log('收到标靶数据:', responseValues); // 检查是否为空对象 if (!responseValues || Object.keys(responseValues).length === 0) { @@ -101,7 +101,7 @@ export const useTargetStorage = () => { setTargets(formattedTargets); setError(null); - console.log('解析到标靶数据:', formattedTargets); + // console.log('解析到标靶数据:', formattedTargets); } catch (err) { console.error('处理标靶数据失败:', err); setError('处理标靶数据失败');