diff --git a/src/components/devices/DPadControl.tsx b/src/components/devices/DPadControl.tsx index 27aeac7..2a876fa 100644 --- a/src/components/devices/DPadControl.tsx +++ b/src/components/devices/DPadControl.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ChevronUp, ChevronDown, ChevronLeft, ChevronRight, Circle } from 'lucide-react'; @@ -15,6 +15,22 @@ interface DPadControlProps { disabled?: boolean; } +// 键盘按键映射 +const KEY_MAP: Record = { + ArrowUp: 'up', + ArrowDown: 'down', + ArrowLeft: 'left', + ArrowRight: 'right', + w: 'up', + s: 'down', + a: 'left', + d: 'right', + W: 'up', + S: 'down', + A: 'left', + D: 'right', +}; + const DPadControl: React.FC = ({ onGamepadUpdate, onSpeedChange, disabled = false }) => { const { t } = useTranslation(); const [pressed, setPressed] = useState>({}); @@ -23,6 +39,8 @@ const DPadControl: React.FC = ({ onGamepadUpdate, onSpeedChang // 用 ref 存储最新状态,避免 setInterval 闭包捕获旧值 const pressedRef = useRef>({}); const speedRef = useRef(0.5); + // 记录当前按下的键盘按键,避免 keydown 重复触发 + const activeKeysRef = useRef>(new Set()); // 构建状态:只传输 xy 坐标(axes),buttons 全部为 false const buildState = useCallback( @@ -86,6 +104,53 @@ const DPadControl: React.FC = ({ onGamepadUpdate, onSpeedChang [disabled, emit], ); + // 键盘控制:方向键 / WASD + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // 焦点在输入框时不触发 + const target = e.target as HTMLElement; + if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) { + return; + } + const key = KEY_MAP[e.key]; + if (!key) return; + // 防止 keydown 持续触发(系统自动重复) + if (activeKeysRef.current.has(key)) return; + activeKeysRef.current.add(key); + e.preventDefault(); + startPress(key); + }; + + const handleKeyUp = (e: KeyboardEvent) => { + const key = KEY_MAP[e.key]; + if (!key) return; + activeKeysRef.current.delete(key); + e.preventDefault(); + endPress(key); + }; + + // 窗口失焦时清空所有按键状态 + const handleBlur = () => { + activeKeysRef.current.forEach((key) => endPress(key)); + activeKeysRef.current.clear(); + }; + + window.addEventListener('keydown', handleKeyDown); + window.addEventListener('keyup', handleKeyUp); + window.addEventListener('blur', handleBlur); + + return () => { + window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener('keyup', handleKeyUp); + window.removeEventListener('blur', handleBlur); + // 清理定时器 + if (pressTimer.current) { + window.clearInterval(pressTimer.current); + pressTimer.current = null; + } + }; + }, [startPress, endPress]); + const btnBase = 'flex items-center justify-center w-8 h-8 rounded-xl border transition-all duration-150 select-none active:scale-95'; const btnActive = 'border-cyan-400 bg-cyan-500/30 text-cyan-300 shadow-[0_0_16px_rgba(34,211,238,0.4)]'; diff --git a/src/components/devices/DeviceStatusPage.tsx b/src/components/devices/DeviceStatusPage.tsx index 66c1db5..a242659 100644 --- a/src/components/devices/DeviceStatusPage.tsx +++ b/src/components/devices/DeviceStatusPage.tsx @@ -50,6 +50,7 @@ import { useLocation } from 'react-router-dom'; import XboxController from './XboxController'; import RobotFeatureButtons from './RobotFeatureButtons'; import DPadControl from './DPadControl'; +import ReverseCameraOverlay from './ReverseCameraOverlay'; import DroneVideoPlayer from './DroneVideoPlayer'; import CesiumMap from '../Map.jsx'; import { useSelector } from 'react-redux'; @@ -118,6 +119,7 @@ export default function DeviceStatusPage() { }); const [mowerView, setMowerView] = useState('front'); + const [steerXY, setSteerXY] = useState({ x: 0, y: 0 }); const [uavState, setUavState] = useState(null); const [points, setPoints] = useState([]); const [clearNavLine, setClearNavLine] = useState(0); @@ -632,6 +634,10 @@ export default function DeviceStatusPage() { const handleGamepadUpdate = useCallback((state) => { console.log('Gamepad state updated:', state); + // 记录方向盘 xy 坐标,用于倒车影像叠加层 + if (state?.axes) { + setSteerXY({ x: state.axes[0] || 0, y: state.axes[1] || 0 }); + } controllerRef.current?.sendGamepad(state); }, []); @@ -1268,6 +1274,11 @@ export default function DeviceStatusPage() { ) )} + {/* 倒车影像叠加层:根据方向盘 xy 显示转向辅助线 */} + {/*{isVideoPlaying && (*/} + + {/*//)}*/} + {/* 视频未加载 / 停止播放的占位 */} {!isVideoPlaying && (
{/* Xbox 手柄 - 左侧,缩小显示 */} -
+
- {/* 方向盘 + 滑块 - 右侧,居中不挤压 */} -
- {/*

- {t('devices.dpad.title')} -

*/} + {/* 方向盘 + 滑块 - 右侧 */} +
{ handleGamepadUpdate(state); - //console.log('[DeviceStatusPage] DPad state:', { - // buttons: state.buttons.map((b, i) => ({ idx: i, pressed: b })).filter(b => b.pressed), - // axes: state.axes, - // ts: state.ts, - //}); }} onSpeedChange={(speed) => { //console.log(`[DeviceStatusPage] Speed slider: ${Math.round(speed * 100)}%`); }} - //disabled={!isControlled} />
+ {/* 功能按钮 单独一个面板 */} = ({ x, y, visible = true }) => { + const { t } = useTranslation(); + if (!visible) return null; + + // 将 x 限制在 [-1, 1] + const clamp = (v: number) => Math.max(-1, Math.min(1, v)); + const steer = clamp(x || 0); + const move = clamp(y || 0); + + // 转向角度(用于辅助线弯曲):steer * 35deg + const steerAngle = steer * 35; + // 后退时显示倒车线,前进时减弱 + const reverseOpacity = move > 0 ? 1 : 0.35; + + return ( +
+ + + + + + + + + {/* 倒车网格(透视梯形) */} + + {/* 横向网格线 */} + {[0, 1, 2, 3, 4, 5].map((i) => { + const yPos = 240 - i * 30; + const inset = i * 18; + return ( + + ); + })} + {/* 纵向网格线 */} + {[-3, -2, -1, 0, 1, 2, 3].map((i) => { + const bottomX = 200 + i * 40; + const topX = 200 + i * 8; + return ( + + ); + })} + + + {/* 两侧倒车轨迹线(随转向弯曲) */} + + {/* 左侧轨迹线 */} + + {/* 右侧轨迹线 */} + + + + {/* 安全区域(中间梯形) */} + + + + {/* 顶部方向指示 */} +
+ {move > 0 ? ( + <> + R + {t('devices.reverse')} + + ) : move < 0 ? ( + <> + D + {t('devices.forward')} + + ) : ( + <> + N + {t('devices.neutral')} + + )} +
+ + {/* 左右转向箭头 */} + {steer < -0.1 && ( +
+ ◀ +
+ )} + {steer > 0.1 && ( +
+ ▶ +
+ )} + + {/* 底部距离 / 速度 指示条 */} +
+ + {t('devices.steer')}: {(steer * 100).toFixed(0)}% + + + + {t('devices.throttle')}: = 0 ? '#f59e0b' : '#22c55e' }}>{(move * 100).toFixed(0)}% + +
+
+ ); +}; + +export default ReverseCameraOverlay;