统一交互组件 #7
@@ -6,7 +6,6 @@ export function loadAMap() {
|
||||
version: '2.0',
|
||||
plugins: [
|
||||
'AMap.Scale',
|
||||
'AMap.ToolBar',
|
||||
'AMap.ControlBar',
|
||||
'AMap.Geolocation',
|
||||
'AMap.Marker',
|
||||
|
||||
Vendored
+2
-2
@@ -6,7 +6,7 @@ declare namespace AMap {
|
||||
remove(layer: unknown): void
|
||||
destroy(): void
|
||||
setCenter(center: [number, number]): void
|
||||
setZoom(zoom: number): void
|
||||
setZoom(zoom: number, immediately?: boolean, duration?: number): void
|
||||
setPitch(pitch: number): void
|
||||
getPitch(): number
|
||||
setRotation(rotation: number): void
|
||||
@@ -14,7 +14,7 @@ declare namespace AMap {
|
||||
on(event: string, callback: (...args: unknown[]) => void): void
|
||||
off(event: string, callback: (...args: unknown[]) => void): void
|
||||
getCenter(): { lng: number; lat: number }
|
||||
getZoom(): number
|
||||
getZoom(digits?: number): number
|
||||
getAllOverlays(type?: string): unknown[]
|
||||
addOverlay(overlay: unknown): void
|
||||
removeOverlay(overlay: unknown): void
|
||||
|
||||
+216
-4
@@ -8,7 +8,7 @@ import { loadAMap } from '@/lib/amap'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import type { ApiCloud } from '@/types/api'
|
||||
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 {
|
||||
id: string
|
||||
@@ -40,10 +40,15 @@ const archiveMonth = ref(formatMonthInput(new Date()))
|
||||
const currentMinuteOfDay = ref(getMinuteOfDay(new Date()))
|
||||
const selectedMinuteOfDay = ref(currentMinuteOfDay.value)
|
||||
const sliderDragging = ref(false)
|
||||
const currentZoom = ref(5)
|
||||
|
||||
const VISIBLE_WINDOW_MS = 2 * 60 * 60 * 1000
|
||||
const MIN_MARKER_OPACITY = 0.3
|
||||
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 mapInst: AMap.Map | null = null
|
||||
@@ -53,6 +58,13 @@ let mks: AMap.Marker[] = []
|
||||
let hoverIW: AMap.InfoWindow | null = null
|
||||
let allClouds: CloudMarkerData[] = []
|
||||
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> = {
|
||||
common: '#cbd5e1',
|
||||
@@ -243,6 +255,165 @@ function hideHeader() {
|
||||
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 {
|
||||
return {
|
||||
id: row.id,
|
||||
@@ -423,14 +594,16 @@ async function returnRealtime() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener('blur', clearZoomHold)
|
||||
try {
|
||||
AMapLib = await loadAMap()
|
||||
AMapLib.getConfig().appname = 'amap-jsapi-skill'
|
||||
mapInst = new AMapLib.Map(mapEl.value!, {
|
||||
viewMode: '3D',
|
||||
pitch: 0,
|
||||
rotation: 0,
|
||||
zoom: 5,
|
||||
zooms: [4, 18],
|
||||
zoom: currentZoom.value,
|
||||
zooms: [MIN_MAP_ZOOM, MAX_MAP_ZOOM],
|
||||
center: [104.07, 30.67],
|
||||
mapStyle: 'amap://styles/normal',
|
||||
features: ['bg', 'road', 'building', 'point'],
|
||||
@@ -440,12 +613,13 @@ onMounted(async () => {
|
||||
} as AMap.MapOptions)
|
||||
|
||||
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.on('click', () => { previewCloud.value = null; hideHoverCard() })
|
||||
mapInst.on('zoomstart', hideHeader)
|
||||
mapInst.on('zoomchange', syncCurrentZoom)
|
||||
mapInst.on('movestart', hideHeader)
|
||||
mapInst.on('dragstart', hideHeader)
|
||||
syncCurrentZoom()
|
||||
await refresh()
|
||||
startMarkerDecayTimer()
|
||||
} catch (e) {
|
||||
@@ -454,10 +628,13 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('blur', clearZoomHold)
|
||||
clearZoomHold()
|
||||
if (redrawTimer !== null) {
|
||||
window.clearInterval(redrawTimer)
|
||||
redrawTimer = null
|
||||
}
|
||||
mapInst?.off('zoomchange', syncCurrentZoom)
|
||||
mapInst?.destroy()
|
||||
mapInst = null
|
||||
AMapLib = null
|
||||
@@ -468,6 +645,41 @@ onUnmounted(() => {
|
||||
<div class="relative h-[100dvh] min-h-screen">
|
||||
<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">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user