This commit is contained in:
wdvipa
2026-02-09 16:33:52 +08:00
parent 52c6322a24
commit 28040495c8
63 changed files with 26743 additions and 301 deletions

View File

@@ -0,0 +1,67 @@
/**
* 坐标映射状态监控组件
*/
import React, { useState, useEffect } from 'react'
import { SafeCoordinateMapper } from '../../utils/SafeCoordinateMapper'
interface CoordinateMappingStatusProps {
coordinateMapper?: SafeCoordinateMapper | null
}
const CoordinateMappingStatus: React.FC<CoordinateMappingStatusProps> = ({
coordinateMapper
}) => {
const [performanceReport, setPerformanceReport] = useState<string>('')
useEffect(() => {
if (!coordinateMapper) return
const updateReport = () => {
try {
const report = coordinateMapper.getPerformanceReport()
setPerformanceReport(report)
} catch (error) {
console.error('获取性能报告失败:', error)
}
}
updateReport()
const interval = setInterval(updateReport, 5000)
return () => clearInterval(interval)
}, [coordinateMapper])
if (!coordinateMapper) {
return (
<div style={{
padding: '8px 12px',
background: '#f0f0f0',
borderRadius: '4px',
fontSize: '12px',
color: '#666'
}}>
</div>
)
}
return (
<div style={{
padding: '8px 12px',
background: '#f9f9f9',
borderRadius: '4px',
fontSize: '12px',
fontFamily: 'monospace'
}}>
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
📊
</div>
<pre style={{ margin: 0, fontSize: '11px', whiteSpace: 'pre-wrap' }}>
{performanceReport || '正在加载...'}
</pre>
</div>
)
}
export default CoordinateMappingStatus

View File

@@ -0,0 +1,221 @@
import React, { useRef, useEffect, useState, useCallback } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { setCameraActive, addGalleryImage } from '../../store/slices/uiSlice'
import type { RootState } from '../../store/store'
interface DeviceCameraProps {
deviceId: string
onActiveChange?: (active: boolean) => void
}
/**
* 设备摄像头显示组件
*/
const DeviceCamera: React.FC<DeviceCameraProps> = ({ deviceId, onActiveChange }) => {
const canvasRef = useRef<HTMLCanvasElement>(null)
const [lastFrameTime, setLastFrameTime] = useState(0)
const [hasFrame, setHasFrame] = useState(false)
const [imageSize, setImageSize] = useState<{ width: number, height: number } | null>(null)
const dispatch = useDispatch()
const { webSocket } = useSelector((state: RootState) => state.connection)
const { connectedDevices } = useSelector((state: RootState) => state.devices)
const { screenDisplay } = useSelector((state: RootState) => state.ui)
const device = connectedDevices.find(d => d.id === deviceId)
const drawFrame = useCallback((frameData: any) => {
const canvas = canvasRef.current
if (!canvas) return
try {
// 验证帧数据格式
if (!frameData?.data || !frameData?.format) {
console.warn('收到无效的摄像头数据:', frameData)
return
}
// 创建图像对象
const img = new Image()
img.onload = () => {
try {
// 固定显示尺寸为接收图片的原始尺寸
canvas.width = img.width
canvas.height = img.height
const ctx = canvas.getContext('2d')
if (!ctx) return
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 根据显示模式调整绘制方式
switch (screenDisplay.fitMode) {
case 'fit':
drawFitMode(ctx, img, canvas)
break
case 'fill':
drawFillMode(ctx, img, canvas)
break
case 'stretch':
drawStretchMode(ctx, img, canvas)
break
case 'original':
drawOriginalMode(ctx, img, canvas)
break
}
setImageSize({ width: img.width, height: img.height })
console.debug(`✅ 成功绘制摄像头帧: ${img.width}x${img.height}, 格式: ${frameData.format}`)
} catch (drawError) {
console.error('绘制摄像头图像失败:', drawError)
}
}
img.onerror = (error) => {
console.error('摄像头图像加载失败:', error)
}
// 设置图像数据源
if (typeof frameData.data === 'string') {
img.src = `data:image/${frameData.format.toLowerCase()};base64,${frameData.data}`
} else {
// 处理二进制数据
const blob = new Blob([frameData.data], { type: `image/${frameData.format.toLowerCase()}` })
img.src = URL.createObjectURL(blob)
}
} catch (error) {
console.error('绘制摄像头帧数据失败:', error)
}
}, [screenDisplay.fitMode])
// 监听摄像头数据的独立useEffect
useEffect(() => {
if (!webSocket) return
const handleCameraData = (data: any) => {
if (data.deviceId === deviceId) {
drawFrame(data)
setLastFrameTime(Date.now())
if (!hasFrame) {
setHasFrame(true)
onActiveChange && onActiveChange(true)
}
}
}
// 监听相册图片保存事件
const handleGalleryImageSaved = (data: any) => {
if (data.deviceId === deviceId) {
console.log('收到相册图片保存事件:', data)
dispatch(addGalleryImage(data))
}
}
webSocket.on('camera_data', handleCameraData)
webSocket.on('gallery_image_saved', handleGalleryImageSaved)
return () => {
webSocket.off('camera_data', handleCameraData)
webSocket.off('gallery_image_saved', handleGalleryImageSaved)
}
}, [webSocket, deviceId, hasFrame, onActiveChange, drawFrame])
// 当首次接收到帧时,设置摄像头为激活状态
useEffect(() => {
if (hasFrame) {
dispatch(setCameraActive(true))
}
}, [hasFrame, dispatch])
// 取消“无数据自动隐藏”逻辑:不再因短暂丢帧而隐藏
// 通知父组件卸载或无数据时不活跃
useEffect(() => {
return () => {
onActiveChange && onActiveChange(false)
// 摄像头数据流不活跃
dispatch(setCameraActive(false))
}
}, [onActiveChange])
const drawFitMode = (ctx: CanvasRenderingContext2D, img: HTMLImageElement, canvas: HTMLCanvasElement) => {
const scale = Math.min(canvas.width / img.width, canvas.height / img.height)
const x = (canvas.width - img.width * scale) / 2
const y = (canvas.height - img.height * scale) / 2
ctx.drawImage(img, x, y, img.width * scale, img.height * scale)
}
const drawFillMode = (ctx: CanvasRenderingContext2D, img: HTMLImageElement, canvas: HTMLCanvasElement) => {
const scale = Math.max(canvas.width / img.width, canvas.height / img.height)
const x = (canvas.width - img.width * scale) / 2
const y = (canvas.height - img.height * scale) / 2
ctx.drawImage(img, x, y, img.width * scale, img.height * scale)
}
const drawStretchMode = (ctx: CanvasRenderingContext2D, img: HTMLImageElement, canvas: HTMLCanvasElement) => {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
}
const drawOriginalMode = (ctx: CanvasRenderingContext2D, img: HTMLImageElement, canvas: HTMLCanvasElement) => {
const x = (canvas.width - img.width) / 2
const y = (canvas.height - img.height) / 2
ctx.drawImage(img, x, y)
}
if (!device) {
return null
}
return (
<>
{hasFrame && (
<div
style={{
position: 'relative',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-start',
overflow: 'hidden'
}}
>
{/* 顶部信息栏 */}
<div style={{
position: 'absolute',
top: '20px',
left: '20px',
zIndex: 20,
color: '#fff',
backgroundColor: 'rgba(0, 0, 0, 0.7)',
padding: '8px 16px',
borderRadius: '4px',
fontSize: '14px'
}}>
<div style={{ fontSize: '12px', opacity: 0.8 }}>
FPS: {lastFrameTime ? Math.round(1000 / (Date.now() - lastFrameTime)) : 0}
</div>
</div>
<canvas
ref={canvasRef}
width={imageSize?.width || device?.screenWidth || 360}
height={imageSize?.height || device?.screenHeight || 640}
style={{
width: imageSize ? `${imageSize.width}px` : '100%',
height: imageSize ? `${imageSize.height}px` : 'auto',
objectFit: 'none',
display: 'block'
}}
onContextMenu={(e) => e.preventDefault()}
/>
</div>
)}
</>
)
}
export default DeviceCamera

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff