111
This commit is contained in:
67
src/components/Device/CoordinateMappingStatus.tsx
Normal file
67
src/components/Device/CoordinateMappingStatus.tsx
Normal 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
|
||||
221
src/components/Device/DeviceCamera.tsx
Normal file
221
src/components/Device/DeviceCamera.tsx
Normal 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
|
||||
1009
src/components/Device/DeviceScreen.tsx
Normal file
1009
src/components/Device/DeviceScreen.tsx
Normal file
File diff suppressed because it is too large
Load Diff
1529
src/components/Device/ScreenReader.tsx
Normal file
1529
src/components/Device/ScreenReader.tsx
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user