fix(map): make zoom controls continuously responsive
This commit is contained in:
@@ -6,7 +6,6 @@ export function loadAMap() {
|
|||||||
version: '2.0',
|
version: '2.0',
|
||||||
plugins: [
|
plugins: [
|
||||||
'AMap.Scale',
|
'AMap.Scale',
|
||||||
'AMap.ToolBar',
|
|
||||||
'AMap.ControlBar',
|
'AMap.ControlBar',
|
||||||
'AMap.Geolocation',
|
'AMap.Geolocation',
|
||||||
'AMap.Marker',
|
'AMap.Marker',
|
||||||
|
|||||||
Vendored
+2
-2
@@ -6,7 +6,7 @@ declare namespace AMap {
|
|||||||
remove(layer: unknown): void
|
remove(layer: unknown): void
|
||||||
destroy(): void
|
destroy(): void
|
||||||
setCenter(center: [number, number]): void
|
setCenter(center: [number, number]): void
|
||||||
setZoom(zoom: number): void
|
setZoom(zoom: number, immediately?: boolean, duration?: number): void
|
||||||
setPitch(pitch: number): void
|
setPitch(pitch: number): void
|
||||||
getPitch(): number
|
getPitch(): number
|
||||||
setRotation(rotation: number): void
|
setRotation(rotation: number): void
|
||||||
@@ -14,7 +14,7 @@ declare namespace AMap {
|
|||||||
on(event: string, callback: (...args: unknown[]) => void): void
|
on(event: string, callback: (...args: unknown[]) => void): void
|
||||||
off(event: string, callback: (...args: unknown[]) => void): void
|
off(event: string, callback: (...args: unknown[]) => void): void
|
||||||
getCenter(): { lng: number; lat: number }
|
getCenter(): { lng: number; lat: number }
|
||||||
getZoom(): number
|
getZoom(digits?: number): number
|
||||||
getAllOverlays(type?: string): unknown[]
|
getAllOverlays(type?: string): unknown[]
|
||||||
addOverlay(overlay: unknown): void
|
addOverlay(overlay: unknown): void
|
||||||
removeOverlay(overlay: unknown): void
|
removeOverlay(overlay: unknown): void
|
||||||
|
|||||||
+216
-4
@@ -8,7 +8,7 @@ import { loadAMap } from '@/lib/amap'
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import type { ApiCloud } from '@/types/api'
|
import type { ApiCloud } from '@/types/api'
|
||||||
import { NIcon } from 'naive-ui'
|
import { NIcon } from 'naive-ui'
|
||||||
import { Adjustments, Calendar, CloudUpload, Refresh, Map, Satellite, X } from '@vicons/tabler'
|
import { Adjustments, Calendar, CloudUpload, Minus, Plus, Refresh, Map, Satellite, X } from '@vicons/tabler'
|
||||||
|
|
||||||
interface CloudMarkerData {
|
interface CloudMarkerData {
|
||||||
id: string
|
id: string
|
||||||
@@ -40,10 +40,15 @@ const archiveMonth = ref(formatMonthInput(new Date()))
|
|||||||
const currentMinuteOfDay = ref(getMinuteOfDay(new Date()))
|
const currentMinuteOfDay = ref(getMinuteOfDay(new Date()))
|
||||||
const selectedMinuteOfDay = ref(currentMinuteOfDay.value)
|
const selectedMinuteOfDay = ref(currentMinuteOfDay.value)
|
||||||
const sliderDragging = ref(false)
|
const sliderDragging = ref(false)
|
||||||
|
const currentZoom = ref(5)
|
||||||
|
|
||||||
const VISIBLE_WINDOW_MS = 2 * 60 * 60 * 1000
|
const VISIBLE_WINDOW_MS = 2 * 60 * 60 * 1000
|
||||||
const MIN_MARKER_OPACITY = 0.3
|
const MIN_MARKER_OPACITY = 0.3
|
||||||
const HIDE_HEADER_EVENT = 'opencloud:hide-header'
|
const HIDE_HEADER_EVENT = 'opencloud:hide-header'
|
||||||
|
const MIN_MAP_ZOOM = 4
|
||||||
|
const MAX_MAP_ZOOM = 18
|
||||||
|
const ZOOM_HOLD_DELAY_MS = 180
|
||||||
|
const ZOOM_CONTINUOUS_SPEED = 2.2
|
||||||
|
|
||||||
let AMapLib: typeof AMap | null = null
|
let AMapLib: typeof AMap | null = null
|
||||||
let mapInst: AMap.Map | null = null
|
let mapInst: AMap.Map | null = null
|
||||||
@@ -53,6 +58,13 @@ let mks: AMap.Marker[] = []
|
|||||||
let hoverIW: AMap.InfoWindow | null = null
|
let hoverIW: AMap.InfoWindow | null = null
|
||||||
let allClouds: CloudMarkerData[] = []
|
let allClouds: CloudMarkerData[] = []
|
||||||
let redrawTimer: number | null = null
|
let redrawTimer: number | null = null
|
||||||
|
let zoomHoldDelayTimer: number | null = null
|
||||||
|
let zoomAnimationFrame: number | null = null
|
||||||
|
let activeZoomPointerId: number | null = null
|
||||||
|
let zoomContinuousDirection: -1 | 1 | null = null
|
||||||
|
let zoomAnimationValue = currentZoom.value
|
||||||
|
let zoomLastFrameTime = 0
|
||||||
|
let zoomHoldStarted = false
|
||||||
|
|
||||||
const rarityColors: Record<string, string> = {
|
const rarityColors: Record<string, string> = {
|
||||||
common: '#cbd5e1',
|
common: '#cbd5e1',
|
||||||
@@ -243,6 +255,165 @@ function hideHeader() {
|
|||||||
window.dispatchEvent(new CustomEvent(HIDE_HEADER_EVENT))
|
window.dispatchEvent(new CustomEvent(HIDE_HEADER_EVENT))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncCurrentZoom() {
|
||||||
|
if (!mapInst) return
|
||||||
|
currentZoom.value = mapInst.getZoom(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeZoom(direction: -1 | 1, step = 1) {
|
||||||
|
if (!mapInst) return false
|
||||||
|
|
||||||
|
const zoom = mapInst.getZoom(2)
|
||||||
|
const nextZoom = Math.min(MAX_MAP_ZOOM, Math.max(MIN_MAP_ZOOM, zoom + direction * step))
|
||||||
|
if (Math.abs(nextZoom - zoom) < 0.001) return false
|
||||||
|
|
||||||
|
mapInst.setZoom(nextZoom, true)
|
||||||
|
currentZoom.value = nextZoom
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopZoomAnimation() {
|
||||||
|
if (zoomAnimationFrame !== null) {
|
||||||
|
window.cancelAnimationFrame(zoomAnimationFrame)
|
||||||
|
zoomAnimationFrame = null
|
||||||
|
}
|
||||||
|
zoomContinuousDirection = null
|
||||||
|
zoomLastFrameTime = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearZoomHoldDelay() {
|
||||||
|
if (zoomHoldDelayTimer !== null) {
|
||||||
|
window.clearTimeout(zoomHoldDelayTimer)
|
||||||
|
zoomHoldDelayTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearZoomHold() {
|
||||||
|
clearZoomHoldDelay()
|
||||||
|
stopZoomAnimation()
|
||||||
|
activeZoomPointerId = null
|
||||||
|
zoomHoldStarted = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function runContinuousZoomFrame(timestamp: number) {
|
||||||
|
if (!mapInst || zoomContinuousDirection === null) {
|
||||||
|
zoomAnimationFrame = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zoomLastFrameTime === 0) {
|
||||||
|
zoomLastFrameTime = timestamp
|
||||||
|
zoomAnimationFrame = window.requestAnimationFrame(runContinuousZoomFrame)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsedSeconds = Math.min((timestamp - zoomLastFrameTime) / 1000, 0.05)
|
||||||
|
zoomLastFrameTime = timestamp
|
||||||
|
const nextZoom = Math.min(
|
||||||
|
MAX_MAP_ZOOM,
|
||||||
|
Math.max(
|
||||||
|
MIN_MAP_ZOOM,
|
||||||
|
zoomAnimationValue + zoomContinuousDirection * ZOOM_CONTINUOUS_SPEED * elapsedSeconds,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Math.abs(nextZoom - zoomAnimationValue) < 0.0001) {
|
||||||
|
stopZoomAnimation()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
zoomAnimationValue = nextZoom
|
||||||
|
mapInst.setZoom(nextZoom, true)
|
||||||
|
currentZoom.value = nextZoom
|
||||||
|
|
||||||
|
if (nextZoom === MIN_MAP_ZOOM || nextZoom === MAX_MAP_ZOOM) {
|
||||||
|
stopZoomAnimation()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
zoomAnimationFrame = window.requestAnimationFrame(runContinuousZoomFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
function beginContinuousZoom(direction: -1 | 1) {
|
||||||
|
if (!mapInst) return
|
||||||
|
|
||||||
|
zoomHoldStarted = true
|
||||||
|
zoomContinuousDirection = direction
|
||||||
|
zoomAnimationValue = mapInst.getZoom(4)
|
||||||
|
zoomLastFrameTime = 0
|
||||||
|
zoomAnimationFrame = window.requestAnimationFrame(runContinuousZoomFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
function startZoomHold(direction: -1 | 1, event: PointerEvent) {
|
||||||
|
if (event.pointerType === 'mouse' && event.button !== 0) return
|
||||||
|
if (activeZoomPointerId !== null) return
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
clearZoomHold()
|
||||||
|
activeZoomPointerId = event.pointerId
|
||||||
|
|
||||||
|
const button = event.currentTarget as HTMLButtonElement
|
||||||
|
button.setPointerCapture?.(event.pointerId)
|
||||||
|
|
||||||
|
const zoom = mapInst?.getZoom(4)
|
||||||
|
if (
|
||||||
|
zoom === undefined
|
||||||
|
|| (direction === 1 && zoom >= MAX_MAP_ZOOM)
|
||||||
|
|| (direction === -1 && zoom <= MIN_MAP_ZOOM)
|
||||||
|
) {
|
||||||
|
activeZoomPointerId = null
|
||||||
|
if (button.hasPointerCapture?.(event.pointerId)) {
|
||||||
|
button.releasePointerCapture(event.pointerId)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
zoomHoldDelayTimer = window.setTimeout(() => {
|
||||||
|
zoomHoldDelayTimer = null
|
||||||
|
beginContinuousZoom(direction)
|
||||||
|
}, ZOOM_HOLD_DELAY_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishZoomHold(direction: -1 | 1, event: PointerEvent) {
|
||||||
|
if (activeZoomPointerId === null || event.pointerId !== activeZoomPointerId) return
|
||||||
|
|
||||||
|
const pointerId = activeZoomPointerId
|
||||||
|
const wasContinuous = zoomHoldStarted
|
||||||
|
activeZoomPointerId = null
|
||||||
|
clearZoomHoldDelay()
|
||||||
|
stopZoomAnimation()
|
||||||
|
zoomHoldStarted = false
|
||||||
|
|
||||||
|
const button = event.currentTarget as HTMLButtonElement
|
||||||
|
if (button.hasPointerCapture?.(pointerId)) {
|
||||||
|
button.releasePointerCapture(pointerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wasContinuous) {
|
||||||
|
changeZoom(direction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelZoomHold(event: PointerEvent) {
|
||||||
|
if (activeZoomPointerId === null || event.pointerId !== activeZoomPointerId) return
|
||||||
|
|
||||||
|
const pointerId = activeZoomPointerId
|
||||||
|
activeZoomPointerId = null
|
||||||
|
clearZoomHoldDelay()
|
||||||
|
stopZoomAnimation()
|
||||||
|
zoomHoldStarted = false
|
||||||
|
|
||||||
|
const button = event.currentTarget as HTMLButtonElement
|
||||||
|
if (button.hasPointerCapture?.(pointerId)) {
|
||||||
|
button.releasePointerCapture(pointerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleZoomClick(direction: -1 | 1, event: MouseEvent) {
|
||||||
|
if (event.detail === 0) {
|
||||||
|
changeZoom(direction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toCloudMarker(row: ApiCloud): CloudMarkerData {
|
function toCloudMarker(row: ApiCloud): CloudMarkerData {
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
@@ -423,14 +594,16 @@ async function returnRealtime() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
window.addEventListener('blur', clearZoomHold)
|
||||||
try {
|
try {
|
||||||
AMapLib = await loadAMap()
|
AMapLib = await loadAMap()
|
||||||
|
AMapLib.getConfig().appname = 'amap-jsapi-skill'
|
||||||
mapInst = new AMapLib.Map(mapEl.value!, {
|
mapInst = new AMapLib.Map(mapEl.value!, {
|
||||||
viewMode: '3D',
|
viewMode: '3D',
|
||||||
pitch: 0,
|
pitch: 0,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
zoom: 5,
|
zoom: currentZoom.value,
|
||||||
zooms: [4, 18],
|
zooms: [MIN_MAP_ZOOM, MAX_MAP_ZOOM],
|
||||||
center: [104.07, 30.67],
|
center: [104.07, 30.67],
|
||||||
mapStyle: 'amap://styles/normal',
|
mapStyle: 'amap://styles/normal',
|
||||||
features: ['bg', 'road', 'building', 'point'],
|
features: ['bg', 'road', 'building', 'point'],
|
||||||
@@ -440,12 +613,13 @@ onMounted(async () => {
|
|||||||
} as AMap.MapOptions)
|
} as AMap.MapOptions)
|
||||||
|
|
||||||
mapInst.addControl(new AMapLib.Scale())
|
mapInst.addControl(new AMapLib.Scale())
|
||||||
mapInst.addControl(new AMapLib.ToolBar({ position: 'LT' } as Record<string, unknown>))
|
|
||||||
mapInst.addControl(new AMapLib.ControlBar({ position: { right: '50px', bottom: '224px' } } as Record<string, unknown>))
|
mapInst.addControl(new AMapLib.ControlBar({ position: { right: '50px', bottom: '224px' } } as Record<string, unknown>))
|
||||||
mapInst.on('click', () => { previewCloud.value = null; hideHoverCard() })
|
mapInst.on('click', () => { previewCloud.value = null; hideHoverCard() })
|
||||||
mapInst.on('zoomstart', hideHeader)
|
mapInst.on('zoomstart', hideHeader)
|
||||||
|
mapInst.on('zoomchange', syncCurrentZoom)
|
||||||
mapInst.on('movestart', hideHeader)
|
mapInst.on('movestart', hideHeader)
|
||||||
mapInst.on('dragstart', hideHeader)
|
mapInst.on('dragstart', hideHeader)
|
||||||
|
syncCurrentZoom()
|
||||||
await refresh()
|
await refresh()
|
||||||
startMarkerDecayTimer()
|
startMarkerDecayTimer()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -454,10 +628,13 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('blur', clearZoomHold)
|
||||||
|
clearZoomHold()
|
||||||
if (redrawTimer !== null) {
|
if (redrawTimer !== null) {
|
||||||
window.clearInterval(redrawTimer)
|
window.clearInterval(redrawTimer)
|
||||||
redrawTimer = null
|
redrawTimer = null
|
||||||
}
|
}
|
||||||
|
mapInst?.off('zoomchange', syncCurrentZoom)
|
||||||
mapInst?.destroy()
|
mapInst?.destroy()
|
||||||
mapInst = null
|
mapInst = null
|
||||||
AMapLib = null
|
AMapLib = null
|
||||||
@@ -468,6 +645,41 @@ onUnmounted(() => {
|
|||||||
<div class="relative h-[100dvh] min-h-screen">
|
<div class="relative h-[100dvh] min-h-screen">
|
||||||
<div ref="mapEl" class="w-full h-full"></div>
|
<div ref="mapEl" class="w-full h-full"></div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="absolute left-4 top-32 z-20 flex w-10 flex-col gap-2 md:top-20"
|
||||||
|
role="group"
|
||||||
|
:aria-label="`地图缩放控制,当前级别 ${currentZoom.toFixed(2)}`"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="oc-map-button touch-none select-none"
|
||||||
|
title="放大地图(按住可连续放大)"
|
||||||
|
aria-label="放大地图,按住可连续放大"
|
||||||
|
:disabled="currentZoom >= MAX_MAP_ZOOM"
|
||||||
|
@pointerdown="startZoomHold(1, $event)"
|
||||||
|
@pointerup="finishZoomHold(1, $event)"
|
||||||
|
@pointercancel="cancelZoomHold"
|
||||||
|
@lostpointercapture="cancelZoomHold"
|
||||||
|
@click="handleZoomClick(1, $event)"
|
||||||
|
>
|
||||||
|
<NIcon size="20"><Plus /></NIcon>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="oc-map-button touch-none select-none"
|
||||||
|
title="缩小地图(按住可连续缩小)"
|
||||||
|
aria-label="缩小地图,按住可连续缩小"
|
||||||
|
:disabled="currentZoom <= MIN_MAP_ZOOM"
|
||||||
|
@pointerdown="startZoomHold(-1, $event)"
|
||||||
|
@pointerup="finishZoomHold(-1, $event)"
|
||||||
|
@pointercancel="cancelZoomHold"
|
||||||
|
@lostpointercapture="cancelZoomHold"
|
||||||
|
@click="handleZoomClick(-1, $event)"
|
||||||
|
>
|
||||||
|
<NIcon size="20"><Minus /></NIcon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="absolute bottom-6 right-7 z-20 flex w-10 flex-col items-center gap-2">
|
<div class="absolute bottom-6 right-7 z-20 flex w-10 flex-col items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user