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>[]>)。 - 当命中提示项时,返回的
option为ComputedRef<T | undefined>,内容为该项除content、cursor、visible、fixedFeatureCenter、offset、priority以外的所有自定义字段。
示例:
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
| 名称 | 类型 | 说明 |
|---|---|---|
| visible | ComputedRef<boolean> | 提示是否可见 |
| offset | ComputedRef<{ x: number, y: number }> | 位置偏移 |
| position | ComputedRef<PointermovePosition> | 提示当前位置(像素坐标) |
| originalPosition | ComputedRef<PointermovePosition> | 原始位置(鼠标事件触发位置) |
| feature | ComputedRef<FeatureLike | undefined> | 当前命中的要素 |
| content | ComputedRef<(() => VNodeChild) | string> | 提示内容,支持函数或字符串 |
| coordinate | ComputedRef<Coordinate | undefined> | 当前命中的地图坐标 |
| hide | () => void | 关闭提示并恢复鼠标样式 |
| option | ComputedRef<T | undefined> | 当前命中的提示项自定义配置(来自匹配项的其他字段) |
PointermoveItem
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| content | ((params: PointermoveContentParams) => VNodeChild) | string | - | 提示内容,支持函数动态生成 |
| visible | ((params: PointermoveContentParams) => boolean) | boolean | true | 是否显示提示 |
| offset | { x?: number, y?: number } | { x: 0, y: 0 } | 位置偏移 |
| priority | number | 0 | 优先级,数字越大优先显示 |
| cursor | CSSProperties['cursor'] | ((params: PointermoveContentParams) => CSSProperties['cursor']) | - | 鼠标样式,如 pointer、crosshair 等 |
| fixedFeatureCenter | boolean | true | 是否固定在要素中心位置 |
| 其他字段 | ...args: Option | - | 通过泛型扩展的自定义属性 |
PointermoveContentParams
| 名称 | 类型 | 说明 |
|---|---|---|
| map | OLMap | 地图实例 |
| position | PointermovePosition | 提示位置(像素坐标) |
| coordinate | Coordinate | 当前命中的地图坐标 |
| feature | FeatureLike | 当前命中的要素 |
| layer | LayerLike | undefined | 要素所在图层 |
PointermovePosition
| 名称 | 类型 | 说明 |
|---|---|---|
| x | number | 横坐标(像素) |
| y | number | 纵坐标(像素) |
使用说明
- 仅当指针位于某个要素上时才显示提示;离开要素则隐藏。
- 当有多个提示项匹配时,按
priority从高到低选择一个显示。 content为函数时将接收 PointermoveContentParams,可按需生成内容或返回组件。cursor可设置地图视口的鼠标样式;调用hide会恢复原始样式。fixedFeatureCenter为true时,提示位置会锚定在要素中心;否则锚定在当前鼠标位置。
源代码
点我查看代码
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