Skip to content

usePointermove

简介

在地图要素上移动鼠标时,提供悬浮提示(tooltip)逻辑。支持优先级匹配、可见性动态判断、位置偏移、是否固定在要素中心、以及自定义鼠标样式等。提示的 UI 需自行结合组件库实现(示例使用 NaiveUI 的 Popover)。

使用示例

点我查看代码
vue
<script lang="ts" setup>
import {
  createPointFeature,
  createVectorLayer,
  getOSMLayer,
  OlMap,
} from '@summeruse/ol'
import { NCard, NPopover } from 'naive-ui'
import { Map as OLMap } from 'ol'
import { h } from 'vue'
import { usePointermove } from '.'

const olMap = new OLMap()
olMap.addLayer(getOSMLayer())

const feature = createPointFeature([116.3912, 39.9072], {
  id: 'feature1',
  styleOptions: {
    circleOptions: {
      radius: 10,
      fillOptions: {
        color: 'red',
      },
      strokeOptions: {
        color: 'red',
        width: 10,
      },
    },
  },
  data: {
    name: '天安门',
    src: 'https://q6.itc.cn/images01/20251010/fc56df887f104e34b860f88976b12b74.jpeg',
  },
})

const feature2 = createPointFeature([116.4074, 39.9042], {
  id: 'feature2',
  styleOptions: {
    circleOptions: {
      radius: 10,
      fillOptions: {
        color: 'green',
      },
      strokeOptions: {
        color: 'green',
        width: 10,
      },
    },
  },
})

const { layer, source } = createVectorLayer()
source.addFeature(feature)
source.addFeature(feature2)

olMap.addLayer(layer)
const { visible, position, content, option } = usePointermove<{ raw?: boolean, showArrow?: boolean }>(olMap, [{
  raw: true,
  showArrow: false,
  offset: {
    x: 0,
    y: -20,
  },
  content: ({ feature }) => {
    const { name, src } = feature.get('data') || {}
    return h(NCard, {
      title: name,
      content() {
        return h('img', {
          src,
          style: {
            width: '300px',
            height: '200px',
          },
        })
      },
    })
  },
  priority: 99,
  cursor: 'pointer',
  visible: ({ feature }) => feature.get('id') === 'feature1',
}, {
  content: ({ feature }) => feature.get('id'),
  cursor: 'progress',
  visible: ({ feature }) => feature.get('id') !== undefined,
}])
</script>

<template>
  <OlMap class="w-100% h-400px" :center="[116.3912, 39.9072]" :zoom="14" projection="EPSG:4326" :ol-map />
  <NPopover
    v-bind="option" :arrow-style="{ pointerEvents: 'none' }" style="pointer-events: none;" trigger="manual"
    :show="visible" :x="position.x" :y="position.y"
  >
    <template v-if="typeof content === 'function'">
      <component :is="content" />
    </template>
    <template v-else>
      {{ content }}
    </template>
  </NPopover>
</template>

泛型扩展

  • 支持通过泛型携带自定义配置类型:usePointermove<T extends Record<string, any>>(mapRef, items: MaybeRefOrGetter<PointermoveItem<T>[]>)
  • 当命中提示项时,返回的 optionComputedRef<T | undefined>,内容为该项除 contentcursorvisiblefixedFeatureCenteroffsetpriority 以外的所有自定义字段。

示例:

ts
const { option } = usePointermove<{ id: string }>(olMap, [{
  content: '信息',
  id: 'feature-id',
}])
// option.value?.id === 'feature-id'

API

名称类型
usePointermove<T>(...args: UsePointermoveParams<T>) => UsePointermoveReturn

UsePointermoveParams<T>

名称类型默认值说明
args[0]MaybeRefOrGetter<OLMap | undefined>-地图实例
args[1]MaybeRefOrGetter<PointermoveItem<T>[]>-提示项配置列表

UsePointermoveReturn

名称类型说明
visibleComputedRef<boolean>提示是否可见
offsetComputedRef<{ x: number, y: number }>位置偏移
positionComputedRef<PointermovePosition>提示当前位置(像素坐标)
originalPositionComputedRef<PointermovePosition>原始位置(鼠标事件触发位置)
featureComputedRef<FeatureLike | undefined>当前命中的要素
contentComputedRef<(() => VNodeChild) | string>提示内容,支持函数或字符串
coordinateComputedRef<Coordinate | undefined>当前命中的地图坐标
hide() => void关闭提示并恢复鼠标样式
optionComputedRef<T | undefined>当前命中的提示项自定义配置(来自匹配项的其他字段)

PointermoveItem

名称类型默认值说明
content((params: PointermoveContentParams) => VNodeChild) | string-提示内容,支持函数动态生成
visible((params: PointermoveContentParams) => boolean) | booleantrue是否显示提示
offset{ x?: number, y?: number }{ x: 0, y: 0 }位置偏移
prioritynumber0优先级,数字越大优先显示
cursorCSSProperties['cursor'] | ((params: PointermoveContentParams) => CSSProperties['cursor'])-鼠标样式,如 pointercrosshair
fixedFeatureCenterbooleantrue是否固定在要素中心位置
其他字段...args: Option-通过泛型扩展的自定义属性

PointermoveContentParams

名称类型说明
mapOLMap地图实例
positionPointermovePosition提示位置(像素坐标)
coordinateCoordinate当前命中的地图坐标
featureFeatureLike当前命中的要素
layerLayerLike | undefined要素所在图层

PointermovePosition

名称类型说明
xnumber横坐标(像素)
ynumber纵坐标(像素)

使用说明

  • 仅当指针位于某个要素上时才显示提示;离开要素则隐藏。
  • 当有多个提示项匹配时,按 priority 从高到低选择一个显示。
  • content 为函数时将接收 PointermoveContentParams,可按需生成内容或返回组件。
  • cursor 可设置地图视口的鼠标样式;调用 hide 会恢复原始样式。
  • fixedFeatureCentertrue 时,提示位置会锚定在要素中心;否则锚定在当前鼠标位置。

源代码

点我查看代码
ts
import type { Coordinate } from 'ol/coordinate'
import type { FeatureLike } from 'ol/Feature'
import type { CSSProperties, MaybeRefOrGetter, VNodeChild } from 'vue'
import type { LayerLike, OLMap } from '../../types'
import { getCenter } from 'ol/extent'
import { computed, onBeforeUnmount, ref, toValue, watch } from 'vue'

export interface PointermovePosition {
  x: number
  y: number
}

interface PointermoveContentParams {
  map: OLMap
  coordinate: Coordinate
  position: PointermovePosition
  feature: FeatureLike
  layer?: LayerLike
}

type Cursor = CSSProperties['cursor']

export type PointermoveItem<T extends Option = Option> = {
  /** 提示内容,支持函数动态生成 */
  content: ((params: PointermoveContentParams) => VNodeChild) | string
  /** 是否显示提示,可根据 feature 动态判断 */
  visible?: ((params: PointermoveContentParams) => boolean) | boolean
  /** 位置偏移 */
  offset?: { x?: number, y?: number }
  /** 优先级,数字越大优先级越高,当多个 tooltip 匹配时,显示优先级最高的 */
  priority?: number
  /** 鼠标样式,如 'pointer', 'crosshair', 'move' 等 */
  cursor?: Cursor | ((params: PointermoveContentParams) => Cursor)
  /** 固定在feature center  */
  fixedFeatureCenter?: boolean
} & T

export type PointermoveList<T extends Option = Option> = PointermoveItem<T>[]

export interface Option {
  [key: string]: any
}

export function usePointermove<T extends Option>(
  mapRef: MaybeRefOrGetter<OLMap | undefined>,
  items: MaybeRefOrGetter<PointermoveList<T>>,
) {
  const visible = ref(false)
  // 原始位置
  const originalPosition = ref<PointermovePosition>({ x: 0, y: 0 })
  const feature = ref<FeatureLike>()
  const content = ref<(() => VNodeChild) | string>()
  const offset = ref<{ x: number, y: number }>({ x: 0, y: 0 })
  const coordinate = ref<Coordinate>()
  const option = ref<T>()
  const position = computed(() => ({
    x: originalPosition.value.x + offset.value.x,
    y: originalPosition.value.y + offset.value.y,
  }))

  let currentMap: OLMap | undefined
  let originalCursor: string = ''

  /** 查找匹配的 tooltip 配置 */
  function findMatchingPointermove(params: PointermoveContentParams): PointermoveItem<T> | null {
    const tooltips = toValue(items)

    // 过滤出可见的 tooltip
    const options = tooltips.filter((item) => {
      const shouldShow = item.visible
      if (typeof shouldShow === 'function') {
        return shouldShow(params)
      }
      return shouldShow ?? true
    })

    if (options.length === 0) {
      return null
    }

    // 按优先级排序,返回优先级最高的
    return options.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))[0]
  }

  /** 显示提示 */
  function show(evt: MouseEvent) {
    if (!currentMap)
      return

    const _coordinate = currentMap.getEventCoordinate(evt)
    coordinate.value = _coordinate
    const pixel = currentMap.getEventPixel(evt)

    let foundFeature: FeatureLike | undefined
    let foundLayer: LayerLike | undefined

    currentMap.forEachFeatureAtPixel(pixel, (feature, layer) => {
      foundFeature = feature
      foundLayer = layer
      return true
    })

    // 如果没有找到 feature,不显示提示
    if (!foundFeature) {
      hide()
      return
    }

    feature.value = foundFeature

    const params = {
      map: currentMap,
      position: { x: evt.clientX, y: evt.clientY },
      coordinate: _coordinate,
      feature: foundFeature,
      layer: foundLayer,
    }

    // 查找匹配的 tooltip 配置
    const matchedPointermove = findMatchingPointermove(params)
    if (matchedPointermove) {
      const { content, cursor, visible, fixedFeatureCenter, offset, priority, ...rest } = matchedPointermove
      option.value = { ...rest } as unknown as T
    }
    if (!matchedPointermove) {
      hide()
      return
    }

    // 计算位置(带偏移)
    const offsetX = matchedPointermove.offset?.x ?? 0
    const offsetY = matchedPointermove.offset?.y ?? 0
    offset.value = { x: offsetX, y: offsetY }
    const fixedFeatureCenter = matchedPointermove.fixedFeatureCenter ?? true
    const geometry = foundFeature.getGeometry()
    if (fixedFeatureCenter && geometry) {
      const extent = geometry.getExtent()
      const center = getCenter(extent)
      const pixel = currentMap.getPixelFromCoordinate(center)
      const { top, left } = currentMap.getViewport().getBoundingClientRect()
      originalPosition.value.x = pixel[0] + left
      originalPosition.value.y = pixel[1] + top
    }
    else {
      originalPosition.value = { x: evt.clientX, y: evt.clientY }
    }
    // 设置内容
    const tooltipContent = matchedPointermove.content
    content.value = typeof tooltipContent === 'function'
      ? () => tooltipContent(params)
      : tooltipContent

    // 设置鼠标样式
    const cursor = matchedPointermove.cursor
    const cursorStyle = typeof cursor === 'function'
      ? cursor(params)
      : cursor

    if (cursorStyle && currentMap) {
      const viewport = currentMap.getViewport()
      if (!originalCursor) {
        originalCursor = viewport.style.cursor
      }
      viewport.style.cursor = cursorStyle
    }
    visible.value = true
  }

  /** 隐藏提示 */
  function hide() {
    visible.value = false
    feature.value = undefined
    // 恢复原始鼠标样式
    if (currentMap && originalCursor !== undefined) {
      const viewport = currentMap.getViewport()
      viewport.style.cursor = originalCursor
      originalCursor = ''
    }
  }

  /** 绑定事件 */
  function bindMapEvents(map?: OLMap) {
    if (!map)
      return

    const el = map.getViewport()
    el.addEventListener('pointermove', show)
    el.addEventListener('pointerout', hide)
  }

  /** 解绑事件 */
  function unbindMapEvents(map?: OLMap) {
    if (!map)
      return

    const el = map.getViewport()
    el.removeEventListener('pointermove', show)
    el.removeEventListener('pointerout', hide)
  }

  /** 监听 mapRef 变化 */
  watch(
    () => toValue(mapRef),
    (newMap, oldMap) => {
      if (oldMap !== newMap) {
        unbindMapEvents(oldMap)
        bindMapEvents(newMap)
        currentMap = newMap
      }
    },
    { immediate: true },
  )

  onBeforeUnmount(() => {
    unbindMapEvents(currentMap)
  })

  return {
    visible: computed(() => visible.value),
    position: computed(() => position.value),
    originalPosition: computed(() => originalPosition.value),
    feature: computed(() => feature.value),
    content: computed(() => content.value),
    coordinate: computed(() => coordinate.value),
    option: computed(() => option.value),
    hide,
  }
}

export type UsePointermoveReturn = ReturnType<typeof usePointermove>
export type UsePointermoveParams<T extends Option> = Parameters<typeof usePointermove<T>>
export type UsePointermoveFn<T extends Option> = (...args: UsePointermoveParams<T>) => UsePointermoveReturn

Released under the ISC License.