usePointermove
简介
在地图要素上移动鼠标时,提供悬浮提示(tooltip)逻辑。支持优先级匹配、可见性动态判断、位置偏移、是否固定在要素中心、以及自定义鼠标样式等。提示的 UI 需自行结合组件库实现(示例使用 NaiveUI 的 Popover)。
使用示例
点我查看代码
vue
<script lang="ts" setup>
import {
createOpenStreetMapLayer,
createPointFeature,
createVectorLayer,
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(createOpenStreetMapLayer())
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 }>(
{
mapRef: olMap,
items: [{
raw: true,
showArrow: false,
offset: {
x: 0,
y: -20,
},
content: ({ feature }) => {
// console.log(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,
fixedFeatureCenter: false,
}],
forceUpdate: true,
},
)
</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 { MapBrowserEvent } from 'ol'
import type { Coordinate } from 'ol/coordinate'
import type { FeatureLike } from 'ol/Feature'
import type { CSSProperties, MaybeRefOrGetter, VNodeChild } from 'vue'
import type { LayerLike, OLMap, Option } 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 | undefined | void) | boolean
/** 位置偏移 */
offset?: { x?: number, y?: number }
/** 优先级,数字越大优先级越高,当多个 tooltip 匹配时,显示优先级最高的 */
priority?: number
/** 鼠标样式,如 'pointer', 'crosshair', 'move' 等 */
cursor?: Cursor | ((params: PointermoveContentParams) => Cursor)
/** 固定在feature center 默认启用,若要关闭需要同时开启强制更新 */
fixedFeatureCenter?: boolean
/** Hit-detection 容差(css像素),用于扩大检测范围 */
hitTolerance?: number
} & T
export type PointermoveList<T extends Option = Option> = PointermoveItem<T>[]
export interface UsePointermoveOptions<T extends Option = Option> {
/** 地图实例 */
mapRef: MaybeRefOrGetter<OLMap | undefined>
/** 提示配置列表 */
items: MaybeRefOrGetter<PointermoveList<T>>
/** 强制更新 (开启后,无论 feature 是否变化,都强制更新提示) */
forceUpdate?: boolean
}
export function usePointermove<T extends Option>(
{ mapRef, items, forceUpdate = false }: UsePointermoveOptions<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 originalCoordinate = ref<Coordinate>()
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 viewport: HTMLElement | undefined
let originalCursor: string = ''
/** 按优先级排序,然后按相邻相同 tolerance 分组 */
function groupItemsByPriorityWithToleranceMerge(tooltips: PointermoveList<T>): Array<{
tolerance: number
items: PointermoveList<T>
}> {
// 第一步:按优先级排序(从高到低)
const sortedByPriority = [...tooltips].sort(
(a, b) => (b.priority ?? 0) - (a.priority ?? 0),
)
// 第二步:按相邻相同 tolerance 分组(保持优先级顺序)
const groups: Array<{ tolerance: number, items: PointermoveList<T> }> = []
let currentGroup: { tolerance: number, items: PointermoveList<T> } | null = null
sortedByPriority.forEach((item) => {
const tolerance = item.hitTolerance ?? 0
if (!currentGroup || currentGroup.tolerance !== tolerance) {
// 新的 tolerance,创建新分组
currentGroup = { tolerance, items: [item] }
groups.push(currentGroup)
}
else {
// 相同 tolerance,加入当前分组
currentGroup.items.push(item)
}
})
return groups
}
/** 显示提示 */
function show(evt: MapBrowserEvent) {
if (evt.dragging) {
return
}
if (!currentMap || !viewport)
return
const _coordinate = evt.coordinate
originalCoordinate.value = _coordinate
coordinate.value = _coordinate
let pixel = evt.pixel
const tooltips = toValue(items)
// 按优先级排序,相邻相同 tolerance 的配置合并
const groupedItems = groupItemsByPriorityWithToleranceMerge(tooltips)
const { top, left } = viewport.getBoundingClientRect()
let matchedPointermove: PointermoveItem<T> | undefined
let foundFeature: FeatureLike | undefined
let foundLayer: LayerLike | undefined
// 遍历分组(按优先级顺序)
for (const group of groupedItems) {
// 用该分组的 hitTolerance 一次性检测所有相邻的配置
currentMap.forEachFeatureAtPixel(
pixel,
(feature, layer) => {
foundFeature = feature
foundLayer = layer
return true
},
{ hitTolerance: group.tolerance },
)
if (!foundFeature) {
// 该分组没找到 feature,继续下一组
foundFeature = undefined
foundLayer = undefined
continue
}
// 找到了 feature,在该分组内查找第一个 visible=true 的配置
const params = {
map: currentMap,
position: { x: pixel[0] + left, y: pixel[1] + top },
coordinate: _coordinate,
feature: foundFeature,
layer: foundLayer!,
}
for (const item of group.items) {
const shouldShow = item.visible
const isVisible = typeof shouldShow === 'function' ? shouldShow(params) : (shouldShow ?? true)
if (isVisible) {
// 优先级最高且符合 visible 条件的配置
matchedPointermove = item
break
}
}
if (matchedPointermove) {
break // 找到了,停止搜索其他分组
}
// 该分组的所有配置都不匹配 visible 条件,重置继续下一组
foundFeature = undefined
foundLayer = undefined
}
// 如果没有找到匹配的配置,隐藏提示
if (!foundFeature || !matchedPointermove) {
hide()
return
}
if (!forceUpdate && feature.value && feature.value.getId() === foundFeature.getId()) {
return
}
feature.value = foundFeature
const { content: _content, cursor: _cursor, visible: _visible, fixedFeatureCenter: _fixedFeatureCenter, offset: _offset, priority: _priority, hitTolerance: _hitTolerance, originalIndex, ...rest } = matchedPointermove
option.value = { ...rest } as unknown as T
// 计算位置(带偏移)
const offsetX = matchedPointermove.offset?.x ?? 0
const offsetY = matchedPointermove.offset?.y ?? 0
offset.value = { x: offsetX, y: offsetY }
const fixedFeatureCenter = forceUpdate === false ? true : (matchedPointermove.fixedFeatureCenter ?? true)
const geometry = foundFeature.getGeometry()
if (fixedFeatureCenter && geometry) {
const extent = geometry.getExtent()
const center = getCenter(extent)
coordinate.value = center
pixel = currentMap.getPixelFromCoordinate(center)
}
originalPosition.value.x = pixel[0] + left
originalPosition.value.y = pixel[1] + top
// 设置内容
const tooltipContent = matchedPointermove.content
content.value = typeof tooltipContent === 'function'
? () => tooltipContent({
map: currentMap!,
coordinate: coordinate.value!,
position: position.value,
feature: foundFeature!,
layer: foundLayer!,
})
: tooltipContent
// 设置鼠标样式
const cursor = matchedPointermove.cursor
const cursorStyle = typeof cursor === 'function'
? cursor({
map: currentMap,
coordinate: coordinate.value!,
position: position.value,
feature: foundFeature,
layer: foundLayer!,
})
: cursor
if (cursorStyle !== undefined && cursorStyle !== viewport.style.cursor) {
viewport.style.cursor = cursorStyle
}
visible.value = true
}
/** 隐藏提示 */
function hide() {
visible.value = false
feature.value = undefined
// 恢复原始鼠标样式
if (viewport && viewport.style.cursor !== originalCursor) {
viewport.style.cursor = originalCursor
}
}
/** 绑定事件 */
function bindMapEvents(map: OLMap) {
map.on('pointermove', show)
}
/** 解绑事件 */
function unbindMapEvents(map: OLMap) {
map.un('pointermove', show)
}
/** 监听 mapRef 变化 */
watch(
() => toValue(mapRef),
(newMap, oldMap) => {
if (oldMap) {
unbindMapEvents(oldMap)
}
if (oldMap !== newMap) {
currentMap = newMap
if (newMap) {
viewport = newMap.getViewport()
bindMapEvents(newMap)
originalCursor = viewport.style.cursor
}
}
},
{ immediate: true },
)
onBeforeUnmount(() => {
if (currentMap) {
unbindMapEvents(currentMap)
}
})
return {
visible: computed(() => visible.value),
position: computed(() => position.value),
offset: computed(() => offset.value),
originalPosition: computed(() => originalPosition.value),
feature: computed(() => feature.value),
content: computed(() => content.value),
originalCoordinate: computed(() => originalCoordinate.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