Compare commits

...

2 Commits

Author SHA1 Message Date
4602ec9e74 style: 调整pad端横屏竖屏的问题 2026-08-05 16:54:26 +08:00
520811e883 fix(map): 修复平板触控与信息窗定位
统一鼠标、触摸和触控笔的画布坐标转换。修复 autofit 及浏览器缩放下的信息窗偏移,并支持平板点击机器人显示详情、点击空白隐藏。
2026-08-05 15:35:01 +08:00
3 changed files with 148 additions and 36 deletions

View File

@ -18,6 +18,7 @@ const autoFitOptions = {
} }
let viewportWidth = document.documentElement.clientWidth let viewportWidth = document.documentElement.clientWidth
let autoFitResetTimer: ReturnType<typeof setTimeout> | undefined
const initAutoFit = () => { const initAutoFit = () => {
autofit.init(autoFitOptions, false) autofit.init(autoFitOptions, false)
@ -29,29 +30,36 @@ const resetAutoFit = () => {
viewportWidth = document.documentElement.clientWidth viewportWidth = document.documentElement.clientWidth
} }
const scheduleAutoFitReset = () => {
clearTimeout(autoFitResetTimer)
// resize autofit
autoFitResetTimer = setTimeout(resetAutoFit, 50)
}
const handleResize = () => { const handleResize = () => {
const nextWidth = document.documentElement.clientWidth const nextWidth = document.documentElement.clientWidth
// //
if (Math.abs(nextWidth - viewportWidth) <= 2) return if (Math.abs(nextWidth - viewportWidth) <= 2) return
resetAutoFit() scheduleAutoFitReset()
} }
onMounted(() => { onMounted(() => {
initAutoFit() initAutoFit()
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
window.addEventListener('orientationchange', resetAutoFit) window.addEventListener('orientationchange', scheduleAutoFitReset)
document.addEventListener('fullscreenchange', resetAutoFit) document.addEventListener('fullscreenchange', scheduleAutoFitReset)
document.addEventListener('webkitfullscreenchange', resetAutoFit) document.addEventListener('webkitfullscreenchange', scheduleAutoFitReset)
}) })
onUnmounted(() => { onUnmounted(() => {
autofit.off('#app') autofit.off('#app')
clearTimeout(autoFitResetTimer)
window.removeEventListener('resize', handleResize) window.removeEventListener('resize', handleResize)
window.removeEventListener('orientationchange', resetAutoFit) window.removeEventListener('orientationchange', scheduleAutoFitReset)
document.removeEventListener('fullscreenchange', resetAutoFit) document.removeEventListener('fullscreenchange', scheduleAutoFitReset)
document.removeEventListener('webkitfullscreenchange', resetAutoFit) document.removeEventListener('webkitfullscreenchange', scheduleAutoFitReset)
}) })
</script> </script>

View File

@ -25,6 +25,8 @@ import { onBeforeUnmount, onMounted, ref } from 'vue'
const isPortraitTablet = ref(false) const isPortraitTablet = ref(false)
let portraitMediaQuery let portraitMediaQuery
let orientationFrame
const orientationTimers = new Set()
const isTablet = () => { const isTablet = () => {
const userAgent = navigator.userAgent const userAgent = navigator.userAgent
@ -40,11 +42,32 @@ const isTablet = () => {
} }
const updateOrientation = () => { const updateOrientation = () => {
const isPortrait = portraitMediaQuery?.matches ?? window.innerHeight > window.innerWidth const viewport = window.visualViewport
const viewportWidth = viewport?.width || window.innerWidth
const viewportHeight = viewport?.height || window.innerHeight
const isPortrait = viewportHeight > viewportWidth
isPortraitTablet.value = isTablet() && isPortrait isPortraitTablet.value = isTablet() && isPortrait
document.documentElement.classList.toggle('is-portrait-tablet', isPortraitTablet.value) document.documentElement.classList.toggle('is-portrait-tablet', isPortraitTablet.value)
} }
const scheduleOrientationUpdate = () => {
updateOrientation()
cancelAnimationFrame(orientationFrame)
orientationFrame = requestAnimationFrame(updateOrientation)
orientationTimers.forEach(clearTimeout)
orientationTimers.clear()
// iPad
for (const delay of [80, 180, 350]) {
const timer = setTimeout(() => {
orientationTimers.delete(timer)
updateOrientation()
}, delay)
orientationTimers.add(timer)
}
}
const tryLockLandscape = async () => { const tryLockLandscape = async () => {
if (!isTablet() || !screen.orientation?.lock) return if (!isTablet() || !screen.orientation?.lock) return
@ -60,17 +83,22 @@ onMounted(() => {
updateOrientation() updateOrientation()
tryLockLandscape() tryLockLandscape()
portraitMediaQuery.addEventListener?.('change', updateOrientation) portraitMediaQuery.addEventListener?.('change', scheduleOrientationUpdate)
window.addEventListener('resize', updateOrientation) window.addEventListener('resize', scheduleOrientationUpdate)
window.addEventListener('orientationchange', updateOrientation) window.addEventListener('orientationchange', scheduleOrientationUpdate)
window.visualViewport?.addEventListener('resize', scheduleOrientationUpdate)
document.addEventListener('fullscreenchange', tryLockLandscape) document.addEventListener('fullscreenchange', tryLockLandscape)
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
document.documentElement.classList.remove('is-portrait-tablet') document.documentElement.classList.remove('is-portrait-tablet')
portraitMediaQuery?.removeEventListener?.('change', updateOrientation) cancelAnimationFrame(orientationFrame)
window.removeEventListener('resize', updateOrientation) orientationTimers.forEach(clearTimeout)
window.removeEventListener('orientationchange', updateOrientation) orientationTimers.clear()
portraitMediaQuery?.removeEventListener?.('change', scheduleOrientationUpdate)
window.removeEventListener('resize', scheduleOrientationUpdate)
window.removeEventListener('orientationchange', scheduleOrientationUpdate)
window.visualViewport?.removeEventListener('resize', scheduleOrientationUpdate)
document.removeEventListener('fullscreenchange', tryLockLandscape) document.removeEventListener('fullscreenchange', tryLockLandscape)
}) })
</script> </script>

View File

@ -94,6 +94,9 @@ const containerRef = ref(null)
const mapCanvasRef = ref(null) const mapCanvasRef = ref(null)
let resizeObserver let resizeObserver
let eventsBound = false let eventsBound = false
let orientationObserver
let animationFrameId = null
let isRendering = false
const init = () => { const init = () => {
mapCanvas = mapCanvasRef.value; mapCanvas = mapCanvasRef.value;
@ -106,11 +109,14 @@ const init = () => {
*/ */
function resizeCanvasToContainer() { function resizeCanvasToContainer() {
if (!mapCanvas || !containerRef.value) return; if (!mapCanvas || !containerRef.value) return;
const containerRect = containerRef.value.getBoundingClientRect(); // // clientWidth/clientHeight autofit.js CSS transform
if (!containerRect.width || !containerRect.height) return; // getBoundingClientRect() Canvas
const containerWidth = containerRef.value.clientWidth;
const containerHeight = containerRef.value.clientHeight;
if (!containerWidth || !containerHeight) return;
applicationState.devicePixelRatio = window.devicePixelRatio || 1; // applicationState.devicePixelRatio = window.devicePixelRatio || 1; //
applicationState.canvasWidthPx = containerRect.width; // applicationState.canvasWidthPx = containerWidth; //
applicationState.canvasHeightPx = containerRect.height; // = - applicationState.canvasHeightPx = containerHeight; // = -
mapCanvas.width = applicationState.canvasWidthPx * applicationState.devicePixelRatio; // Canvas mapCanvas.width = applicationState.canvasWidthPx * applicationState.devicePixelRatio; // Canvas
mapCanvas.height = applicationState.canvasHeightPx * applicationState.devicePixelRatio; // Canvas mapCanvas.height = applicationState.canvasHeightPx * applicationState.devicePixelRatio; // Canvas
mapCanvas.style.width = applicationState.canvasWidthPx + 'px'; // CSS mapCanvas.style.width = applicationState.canvasWidthPx + 'px'; // CSS
@ -211,6 +217,8 @@ async function loadMapIntoApplication(parsedMap) {
* @param {number} timestamp - requestAnimationFrame 提供的时间戳毫秒 * @param {number} timestamp - requestAnimationFrame 提供的时间戳毫秒
*/ */
function renderFrame(timestamp) { function renderFrame(timestamp) {
if (!isRendering || !canvasCtx) return;
// //
applicationState.animationTimeSeconds = (timestamp || 0) / 1000; applicationState.animationTimeSeconds = (timestamp || 0) / 1000;
@ -220,7 +228,7 @@ function renderFrame(timestamp) {
// //
if (!applicationState.isMapLoaded) { if (!applicationState.isMapLoaded) {
requestAnimationFrame(renderFrame); // animationFrameId = requestAnimationFrame(renderFrame); //
return; return;
} }
@ -270,7 +278,36 @@ function renderFrame(timestamp) {
drawMapBoundary(canvasCtx, map, applicationState); drawMapBoundary(canvasCtx, map, applicationState);
// //
requestAnimationFrame(renderFrame); animationFrameId = requestAnimationFrame(renderFrame);
}
const stopRendering = () => {
isRendering = false
if (animationFrameId !== null) {
cancelAnimationFrame(animationFrameId)
animationFrameId = null
}
}
const startRendering = () => {
if (isRendering || !applicationState.isMapLoaded || document.documentElement.classList.contains('is-portrait-tablet')) return
isRendering = true
animationFrameId = requestAnimationFrame(renderFrame)
}
const handleOrientationVisibility = () => {
if (document.documentElement.classList.contains('is-portrait-tablet')) {
stopRendering()
return
}
// v-show Vue
nextTick(() => {
requestAnimationFrame(() => {
resizeCanvasToContainer()
startRendering()
})
})
} }
const drawAllRobots = (ctx) => { const drawAllRobots = (ctx) => {
@ -292,12 +329,25 @@ const bindEvent = () => {
let activePointerId = null let activePointerId = null
let lastPointerX = 0 let lastPointerX = 0
let lastPointerY = 0 let lastPointerY = 0
let pointerStartX = 0
let pointerStartY = 0
let hasDragged = false
const getCanvasPointerPosition = (pointerEvent) => {
const canvasRect = mapCanvas.getBoundingClientRect()
const scaleX = applicationState.canvasWidthPx / canvasRect.width
const scaleY = applicationState.canvasHeightPx / canvasRect.height
return {
x: (pointerEvent.clientX - canvasRect.left) * scaleX,
y: (pointerEvent.clientY - canvasRect.top) * scaleY
}
}
// Pointer Events Pad // Pointer Events Pad
mapCanvas.addEventListener('pointermove', (pointerEvent) => { mapCanvas.addEventListener('pointermove', (pointerEvent) => {
const canvasRect = mapCanvas.getBoundingClientRect(); // Canvas const pointerPosition = getCanvasPointerPosition(pointerEvent)
applicationState.mouseState.screenX = pointerEvent.clientX - canvasRect.left; applicationState.mouseState.screenX = pointerPosition.x;
applicationState.mouseState.screenY = pointerEvent.clientY - canvasRect.top; applicationState.mouseState.screenY = pointerPosition.y;
// //
const worldPos = screenToWorld(applicationState.mouseState.screenX, applicationState.mouseState.screenY, applicationState); const worldPos = screenToWorld(applicationState.mouseState.screenX, applicationState.mouseState.screenY, applicationState);
applicationState.mouseState.worldX = worldPos.x; applicationState.mouseState.worldX = worldPos.x;
@ -305,12 +355,15 @@ const bindEvent = () => {
if (applicationState.mouseState.isDragging && pointerEvent.pointerId === activePointerId) { if (applicationState.mouseState.isDragging && pointerEvent.pointerId === activePointerId) {
pointerEvent.preventDefault() pointerEvent.preventDefault()
const movementX = pointerEvent.clientX - lastPointerX const movementX = pointerPosition.x - lastPointerX
const movementY = pointerEvent.clientY - lastPointerY const movementY = pointerPosition.y - lastPointerY
if (Math.hypot(pointerPosition.x - pointerStartX, pointerPosition.y - pointerStartY) > 6) {
hasDragged = true
}
applicationState.camera.centerX -= movementX / applicationState.camera.pixelsPerMeter; applicationState.camera.centerX -= movementX / applicationState.camera.pixelsPerMeter;
applicationState.camera.centerY += movementY / applicationState.camera.pixelsPerMeter; applicationState.camera.centerY += movementY / applicationState.camera.pixelsPerMeter;
lastPointerX = pointerEvent.clientX lastPointerX = pointerPosition.x
lastPointerY = pointerEvent.clientY lastPointerY = pointerPosition.y
} }
if (pointerEvent.pointerType === 'mouse') { if (pointerEvent.pointerType === 'mouse') {
@ -319,9 +372,7 @@ const bindEvent = () => {
}); });
mapCanvas.addEventListener('click', (mouseEvent) => { mapCanvas.addEventListener('click', (mouseEvent) => {
const canvasRect = mapCanvas.getBoundingClientRect(); // Canvas const { x: screenX, y: screenY } = getCanvasPointerPosition(mouseEvent)
const screenX = mouseEvent.clientX - canvasRect.left; // Canvas X
const screenY = mouseEvent.clientY - canvasRect.top; // Canvas Y
const worldPos = screenToWorld(screenX, screenY, applicationState); const worldPos = screenToWorld(screenX, screenY, applicationState);
}) })
@ -331,15 +382,33 @@ const bindEvent = () => {
return return
} }
pointerEvent.preventDefault() pointerEvent.preventDefault()
const pointerPosition = getCanvasPointerPosition(pointerEvent)
activePointerId = pointerEvent.pointerId activePointerId = pointerEvent.pointerId
lastPointerX = pointerEvent.clientX lastPointerX = pointerPosition.x
lastPointerY = pointerEvent.clientY lastPointerY = pointerPosition.y
pointerStartX = pointerPosition.x
pointerStartY = pointerPosition.y
hasDragged = false
applicationState.mouseState.isDragging = true; applicationState.mouseState.isDragging = true;
mapCanvas.setPointerCapture(pointerEvent.pointerId) mapCanvas.setPointerCapture(pointerEvent.pointerId)
}); });
const endPointerDrag = (pointerEvent) => { const endPointerDrag = (pointerEvent, cancelled = false) => {
if (pointerEvent.pointerId !== activePointerId) return if (pointerEvent.pointerId !== activePointerId) return
// Pad/
if (!cancelled && !hasDragged && pointerEvent.pointerType !== 'mouse') {
const pointerPosition = getCanvasPointerPosition(pointerEvent)
applicationState.mouseState.screenX = pointerPosition.x
applicationState.mouseState.screenY = pointerPosition.y
const worldPos = screenToWorld(
applicationState.mouseState.screenX,
applicationState.mouseState.screenY,
applicationState
)
updateTooltipDisplay(hitTestRobots(worldPos.x, worldPos.y, applicationState))
}
applicationState.mouseState.isDragging = false; applicationState.mouseState.isDragging = false;
if (mapCanvas.hasPointerCapture(pointerEvent.pointerId)) { if (mapCanvas.hasPointerCapture(pointerEvent.pointerId)) {
mapCanvas.releasePointerCapture(pointerEvent.pointerId) mapCanvas.releasePointerCapture(pointerEvent.pointerId)
@ -347,8 +416,8 @@ const bindEvent = () => {
activePointerId = null activePointerId = null
} }
mapCanvas.addEventListener('pointerup', endPointerDrag); mapCanvas.addEventListener('pointerup', (pointerEvent) => endPointerDrag(pointerEvent));
mapCanvas.addEventListener('pointercancel', endPointerDrag); mapCanvas.addEventListener('pointercancel', (pointerEvent) => endPointerDrag(pointerEvent, true));
// //
mapCanvas.addEventListener('wheel', (wheelEvent) => { mapCanvas.addEventListener('wheel', (wheelEvent) => {
@ -460,6 +529,11 @@ onMounted(() => {
resizeObserver = new ResizeObserver(resizeCanvasToContainer) resizeObserver = new ResizeObserver(resizeCanvasToContainer)
resizeObserver.observe(containerRef.value) resizeObserver.observe(containerRef.value)
window.addEventListener('resize', resizeCanvasToContainer) window.addEventListener('resize', resizeCanvasToContainer)
orientationObserver = new MutationObserver(handleOrientationVisibility)
orientationObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
})
}) })
watch(() => props.mapName, watch(() => props.mapName,
@ -467,7 +541,7 @@ watch(() => props.mapName,
if (newVal) { if (newVal) {
await loadSourceMap(newVal) await loadSourceMap(newVal)
addRobot(robotStore.ip, robotStore.ip, robotStore.position.x, robotStore.position.y, robotStore.position.theta, getThemeColor('--color-canvas-robot')) addRobot(robotStore.ip, robotStore.ip, robotStore.position.x, robotStore.position.y, robotStore.position.theta, getThemeColor('--color-canvas-robot'))
requestAnimationFrame(renderFrame); startRendering()
} }
}, { }, {
immediate: true immediate: true
@ -480,8 +554,10 @@ defineExpose({
}) })
onUnmounted(() => { onUnmounted(() => {
stopRendering()
window.removeEventListener('resize', resizeCanvasToContainer) window.removeEventListener('resize', resizeCanvasToContainer)
resizeObserver?.disconnect() resizeObserver?.disconnect()
orientationObserver?.disconnect()
canvasCtx = null canvasCtx = null
}) })
</script> </script>