From 9065e012250d4b85545a88c16b152eeeef1636c1 Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Sat, 25 Apr 2026 20:32:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E5=8F=B0=E9=A1=B5=E9=9D=A2=E7=8A=B6=E6=80=81=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E6=A0=8F=E5=9C=A8=E4=B8=8D=E5=90=8C=E5=AE=BD=E5=BA=A6=E7=BD=91?= =?UTF-8?q?=E9=A1=B5=E4=B8=8B=E7=9A=84=E8=87=AA=E9=80=82=E5=BA=94=E8=83=BD?= =?UTF-8?q?=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/ActionBar.vue | 300 ++++++++++++++++++++++++------- web/src/views/ConsoleView.vue | 105 ++++++++++- 2 files changed, 334 insertions(+), 71 deletions(-) diff --git a/web/src/components/ActionBar.vue b/web/src/components/ActionBar.vue index 1db72c57..8685deb1 100644 --- a/web/src/components/ActionBar.vue +++ b/web/src/components/ActionBar.vue @@ -1,5 +1,5 @@ diff --git a/web/src/views/ConsoleView.vue b/web/src/views/ConsoleView.vue index c4b1c718..1ddb2f0a 100644 --- a/web/src/views/ConsoleView.vue +++ b/web/src/views/ConsoleView.vue @@ -137,6 +137,9 @@ const mousePosition = ref({ x: 0, y: 0 }) const lastMousePosition = ref({ x: 0, y: 0 }) // Track last position for relative mode const isPointerLocked = ref(false) // Track pointer lock state +/** Local overlay crosshair position (px, relative to video container); HID uses mousePosition separately */ +const localCrosshairPos = ref<{ x: number; y: number } | null>(null) + // Mouse move throttling (60 Hz = ~16.67ms interval) const DEFAULT_MOUSE_MOVE_SEND_INTERVAL_MS = 16 let mouseMoveSendIntervalMs = DEFAULT_MOUSE_MOVE_SEND_INTERVAL_MS @@ -1982,7 +1985,43 @@ function getAbsoluteMousePosition(e: MouseEvent) { } } +function updateLocalCrosshairFromEvent(e: MouseEvent) { + if (!cursorVisible.value) { + localCrosshairPos.value = null + return + } + const container = videoContainerRef.value + if (!container) return + + const rect = container.getBoundingClientRect() + if (rect.width <= 0 || rect.height <= 0) return + + if (mouseMode.value === 'relative' && isPointerLocked.value) { + const cur = localCrosshairPos.value + const nx = cur ? cur.x + e.movementX : rect.width / 2 + const ny = cur ? cur.y + e.movementY : rect.height / 2 + localCrosshairPos.value = { + x: Math.max(0, Math.min(rect.width, nx)), + y: Math.max(0, Math.min(rect.height, ny)), + } + return + } + + localCrosshairPos.value = { + x: Math.max(0, Math.min(rect.width, e.clientX - rect.left)), + y: Math.max(0, Math.min(rect.height, e.clientY - rect.top)), + } +} + +function handleMouseLeaveVideo() { + if (!isPointerLocked.value) { + localCrosshairPos.value = null + } +} + function handleMouseMove(e: MouseEvent) { + updateLocalCrosshairFromEvent(e) + const videoElement = getActiveVideoElement() if (!videoElement) return @@ -2192,6 +2231,12 @@ function handlePointerLockChange() { if (isPointerLocked.value) { // Reset mouse position display when locked mousePosition.value = { x: 0, y: 0 } + if (cursorVisible.value && container) { + const r = container.getBoundingClientRect() + if (r.width > 0 && r.height > 0) { + localCrosshairPos.value = { x: r.width / 2, y: r.height / 2 } + } + } toast.info(t('console.pointerLocked'), { description: t('console.pointerLockedDesc'), duration: 3000, @@ -2222,6 +2267,9 @@ function handleBlur() { function handleCursorVisibilityChange(e: Event) { const customEvent = e as CustomEvent<{ visible: boolean }> cursorVisible.value = customEvent.detail.visible + if (!cursorVisible.value) { + localCrosshairPos.value = null + } } function clampMouseMoveSendIntervalMs(ms: number): number { @@ -2654,10 +2702,10 @@ onUnmounted(() => { }" :class="{ 'opacity-60': videoLoading || videoError, - 'cursor-crosshair': cursorVisible, - 'cursor-none': !cursorVisible + 'cursor-none': true, }" tabindex="0" + @mouseleave="handleMouseLeaveVideo" @mousemove="handleMouseMove" @mousedown="handleMouseDown" @mouseup="handleMouseUp" @@ -2693,6 +2741,59 @@ onUnmounted(() => { alt="" /> + + +