Browse Source

fix & feat:

- 摄像头流刷新出现残影问题
- 实时数据图表显示采样频率
master
qinjian 4 days ago
parent
commit
af4741fb39
  1. 98
      client/src/sections/wuyuanbiaoba/components/CameraView.jsx
  2. 73
      client/src/sections/wuyuanbiaoba/components/RealtimeCharts.jsx
  3. 4
      client/src/sections/wuyuanbiaoba/hooks/useTargetStorage.js

98
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);
// 3src
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]); // targetsloading
}, [targets, loading, videoNaturalWidth, videoNaturalHeight]); // targetsloading
// 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 && (
<div
style={{
position: "absolute",
left: 0,
top: 0,
width: "100%",
height: "100%",
background: "rgba(0,0,0,0.7)",
color: "#fff",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 24,
zIndex: 100,
}}
>
视频流断开正在尝试重连...
</div>
)}
</div>
{/* 绘制画布 */}
@ -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 = ({
}}
>
<div>
分辨率: {videoNaturalWidth} × {videoNaturalHeight}
摄像头分辨率: {videoNaturalWidth} × {videoNaturalHeight}
</div>
<div>缩放: {Math.round(scale * 100)}%</div>
<div>

73
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" }}
/>
</Title>
<div style={{
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
minHeight: "500px"
}}>
<div
style={{
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
minHeight: "500px",
}}
>
<div style={{ textAlign: "center", color: "#999" }}>
<p>等待实时数据...</p>
</div>
@ -356,7 +371,13 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => {
text={`实时更新 - 最后更新: ${lastUpdateTime.toLocaleTimeString()}`}
style={{ marginLeft: "16px", fontSize: "12px" }}
/>
<Badge
status="default"
text={`图表数据采样频率:1Hz`}
style={{ marginLeft: "16px", fontSize: "12px" }}
/>
</Title>
<div
style={{
flex: 1,

4
client/src/sections/wuyuanbiaoba/hooks/useTargetStorage.js

@ -59,7 +59,7 @@ export const useTargetStorage = () => {
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('处理标靶数据失败');

Loading…
Cancel
Save