/android-chrome-512x512.pngSiykt的博客

Contents

微信小程序 Map 组件点聚合里的弯弯绕绕

included in JavaScript Vue
5640 words 12 minutes

微信小程序 Map 组件点聚合里的弯弯绕绕

总所周知,微信小程序的文档看了等于没看,它总能很巧妙的避开你会遇到的问题,所以很多时候只能你自己慢慢的去摸索(人工适配)。

本文为我在开发时遇到各种问题的总结,也会尝试带你实现 Map 组件的点聚合功能。

解决 Map 组件的类型定义

!在 Typescript 5.1.3 中,此定义无效

<map></map> 组件默认是 html 中用于定义一个图像映射(一个可点击的链接区域)的标准组件,详见 MDN - <map>

这在没有使用 ts 开发的项目中不会产生什么问题, 但在诸如 uni-app-ts-vue3 等 ts 项目中,map组件可能就会导致类型检查错误(一般来说都是由于 vue vscode 插件 volar 产生的,建议在出现问题的时候切换版本为 v1.2.0 发布版本)

那么我们就需要扩展 map 组件为小程序中应有的类型定义。

从微信文档中定义 Map 组件

虽然吐槽了微信小程序的文档,但开发的过程却也逃不出它的魔爪…

我在 npm 上检索了一下,没有发现有现有的类型 package,所以就只能手写加一些 HTML 数据处理(dom.query)了。 最终得到了以下的类型定义,虽然不是全部,但实现点聚合应该是没有遗漏了:

import type { SVGAttributes } from 'vue';
export interface Point {
 latitude: number;
 longitude: number;
}
export interface MarkerCallout {
 /** 文本 */
 content?: string;
 /** 文本颜色 */
 color?: string;
 /** 文字大小 */
 fontSize?: number;
 /** 边框圆角 */
 borderRadius?: number;
 /** 边框宽度 */
 borderWidth?: number;
 /** 边框颜色 */
 borderColor?: string;
 /** 背景色 */
 bgColor?: string;
 /** 文本边缘留白 */
 padding?: number;
 /** 'BYCLICK':点击显示; 'ALWAYS':常显 */
 display?: 'BYCLICK' | 'ALWAYS';
 /** 文本对齐方式。有效值: left, right, center */
 textAlign?: string;
 /** 横向偏移量,向右为正数 */
 anchorX?: number;
 /** 纵向偏移量,向下为正数 */
 anchorY?: number;
}
export interface MarkerCustomCallout {
 /** 'BYCLICK':点击显示; 'ALWAYS':常显 */
 display?: 'BYCLICK' | 'ALWAYS';
 /** 横向偏移量,向右为正数 */
 anchorX?: number;
 /** 纵向偏移量,向下为正数 */
 anchorY?: number;
}
export interface MarkerLabel {
 width?: number;
 height?: number;
 /** 文本 */
 content?: string;
 /** 文本颜色 */
 color?: string;
 /** 文字大小 */
 fontSize?: number;
 /** label的坐标(废弃) */
 x?: number;
 /** label的坐标(废弃) */
 y?: number;
 /** label的坐标,原点是 marker 对应的经纬度 */
 anchorX?: number;
 /** label的坐标,原点是 marker 对应的经纬度 */
 anchorY?: number;
 /** 边框宽度 */
 borderWidth?: number;
 /** 边框颜色 */
 borderColor?: string;
 /** 边框圆角 */
 borderRadius?: number;
 /** 背景色 */
 bgColor?: string;
 /** 文本边缘留白 */
 padding?: number;
 /** 文本对齐方式。有效值: left, right, center */
 textAlign?: string;
}
export interface Marker {
 /** 标记点 id */
 id?: number;
 /** 聚合簇的 id */
 clusterId?: Number;
 /** 是否参与点聚合 */
 joinCluster?: Boolean;
 /** 纬度 */
 latitude: number;
 /** 经度 */
 longitude: number;
 /** 标注点名 */
 title?: string;
 /** 显示层级 */
 zIndex?: number;
 /** 显示的图标 */
 iconPath?: string;
 /** 旋转角度 */
 rotate?: number;
 /** 标注的透明度 */
 alpha?: number;
 /** 标注图标宽度 */
 width?: number | string;
 /** 标注图标高度 */
 height?: number | string;
 /** 标记点上方的气泡窗口 */
 callout?: MarkerCallout;
 /** 自定义气泡窗口 */
 customCallout?: MarkerCustomCallout;
 /** 为标记点旁边增加标签 */
 label?: MarkerLabel;
 /** 经纬度在标注图标的锚点,默认底边中点 */
 anchor?: {
 /** x 表示横向(0-1) */
 x: number;
 /** y 表示纵向(0-1) */
 y: number;
 };
 /** 无障碍访问,(属性)元素的额外描述 */
 ariaLabel?: string;
}
export interface TextStyle {
 /** 文本颜色 */
 textColor?: string;
 /** 描边颜色	 */
 strokeColor?: string;
 /** 文本大小 */
 fontSize?: number;
}
export interface SegmentText {
 /** 名称 */
 name?: string;
 /** 起点 */
 startIndex?: number;
 /** 终点 */
 endIndex?: number;
}
export interface Polyline {
 /** 经纬度数组 */
 points: Point[];
 /** 线的颜色 */
 color?: string;
 /** 彩虹线 */
 colorList?: string[];
 /** 线的宽度 */
 width?: number;
 /** 是否虚线 */
 dottedLine?: boolean;
 /** 带箭头的线 */
 arrowLine?: boolean;
 /** 更换箭头图标 */
 arrowIconPath?: string;
 /** 线的边框颜色 */
 borderColor?: string;
 /** 线的厚度 */
 borderWidth?: number;
 /** 压盖关系 */
 level?: string;
 /** 文字样式 */
 textStyle?: TextStyle;
 /** 分段文本 */
 segmentTexts?: SegmentText[];
}
export interface Polygon {
 /** 边线虚线 */
 dashArray?: number[];
 /** 经纬度数组 */
 points: Point[];
 /** 描边的宽度 */
 strokeWidth?: number;
 /** 描边的颜色 */
 strokeColor?: string;
 /** 填充颜色 */
 fillColor?: string;
 /** 设置多边形 Z 轴数值 */
 zIndex?: number;
 /** 压盖关系 */
 level?: string;
}
export interface Circle {
 /** 纬度 */
 latitude: number;
 /** 经度 */
 longitude: number;
 /** 描边的颜色 */
 color?: string;
 /** 填充颜色 */
 fillColor?: string;
 /** 半径 */
 radius: number;
 /** 描边的宽度 */
 strokeWidth?: number;
 /** 压盖关系 */
 level?: string;
}
export interface Control {
 /** 控件id */
 id?: number;
 /** 控件在地图的位置 */
 position: object;
 /** 显示的图标 */
 iconPath: string;
 /** 是否可点击 */
 clickable?: boolean;
}
export interface Position {
 /** 距离地图的左边界多远 */
 left?: number;
 /** 距离地图的上边界多远 */
 top?: number;
 /** 控件宽度 */
 width?: number;
 /** 控件高度 */
 height?: number;
}
export interface MarkerEvent {
 detail: {
 markerId: number;
 };
}
export interface TapEvent {
 detail: {
 name: string;
 longitude: number;
 latitude: number;
 };
}
export interface ControlEvent {
 detail: {
 controlId: number;
 };
}
export interface RegionChangeBeginEvent {
 type: 'begin';
 causedBy: 'gesture' | 'update';
 detail: {
 rotate: number;
 skew: number;
 scale: number;
 centerLocation: number;
 region: number;
 };
}
export interface RegionChangeEndEvent {
 type: 'end';
 causedBy: 'drag' | 'scale' | 'update';
 detail: {
 rotate: number;
 skew: number;
 scale: number;
 centerLocation: number;
 region: number;
 };
}
export type RegionChangeEvent = RegionChangeBeginEvent | RegionChangeEndEvent;
export interface MapElement extends SVGAttributes {
 /** 中心经度 */
 longitude: number;
 /** 中心纬度 */
 latitude: number;
 /** 缩放级别,取值范围为3-20 */
 scale?: number;
 /** 最小缩放级别 */
 minScale?: number;
 /** 最大缩放级别 */
 maxScale?: number;
 /** 标记点 */
 markers?: Marker[];
 /** 路线 */
 polyline?: Polyline[];
 /** 圆 */
 circles?: Circle[];
 /** 控件(即将废弃,建议使用 cover-view 代替) */
 controls?: Control[];
 /** 缩放视野以包含所有给定的坐标点 */
 includePoints?: Point[];
 /** 显示带有方向的当前定位点 */
 showLocation?: boolean;
 /** 多边形 */
 polygons?: Polygon[];
 /** 个性化地图使用的key */
 subkey?: string;
 /** 个性化地图配置的 style,不支持动态修改 */
 layerStyle?: number;
 /** 旋转角度,范围 0 ~ 360, 地图正北和设备 y 轴角度的夹角 */
 rotate?: number;
 /** 倾斜角度,范围 0 ~ 40 , 关于 z 轴的倾角 */
 skew?: number;
 /** 展示3D楼块 */
 enable3D?: boolean;
 /** 显示指南针 */
 showCompass?: boolean;
 /** 显示比例尺,工具暂不支持 */
 showScale?: boolean;
 /** 开启俯视 */
 enableOverlooking?: boolean;
 /** 开启最大俯视角,俯视角度从 45 度拓展到 75 度 */
 enableAutoMaxOverlooking?: boolean;
 /** 是否支持缩放 */
 enableZoom?: boolean;
 /** 是否支持拖动 */
 enableScroll?: boolean;
 /** 是否支持旋转 */
 enableRotate?: boolean;
 /** 是否开启卫星图 */
 enableSatellite?: boolean;
 /** 是否开启实时路况 */
 enableTraffic?: boolean;
 /** 是否展示 POI 点 */
 enablePoi?: boolean;
 /** 是否展示建筑物 */
 enableBuilding?: boolean;
 /** 配置项 */
 setting?: object;
 /** 点击地图时触发,2.9.0起返回经纬度信息 */
 onTap?: (event: TapEvent) => void;
 /** 点击标记点时触发,e.detail = {markerId} */
 onMarkertap?: (event: MarkerEvent) => void;
 /** 点击label时触发,e.detail = {markerId} */
 onLabeltap?: (event: MarkerEvent) => void;
 /** 点击控件时触发,e.detail = {controlId} */
 onControltap?: (event: ControlEvent) => void;
 /** 点击标记点对应的气泡时触发e.detail = {markerId} */
 onCallouttap?: (event: MarkerEvent) => void;
 /** 在地图渲染更新完成时触发 */
 onUpdated?: (event: RegionChangeEvent) => void;
 /** 视野发生变化时触发, */
 onRegionchange?: (event: RegionChangeEvent) => void;
 /** 点击地图poi点时触发,e.detail = {name, longitude, latitude} */
 onPoitap?: (event: TapEvent) => void;
 /** 点击定位标时触发,e.detail = {longitude, latitude} */
 onAnchorpointtap?: (event: TapEvent) => void;
}
export type MapContext = ReturnType<typeof uni.createMapContext>;
export type MapContextAddMarkersOptions = Parameters<MapContext['addMarkers']>[0];
export interface MarkerCluster {
 clusters: {
 center: Marker;
 clusterId: number;
 markerIds: number[];
 }[];
}

扩展 map 组件为小程序的类型定义

要扩展 map 组件就需要先了解它是怎么被定义的,我们可以通过 Ctrl + 左键查看定义它类型的上下文:

interface IntrinsicElementAttributes {
 // ...
 link: LinkHTMLAttributes;
 main: HTMLAttributes;
 map: MapHTMLAttributes;
 mark: HTMLAttributes;
 menu: MenuHTMLAttributes;
 meta: MetaHTMLAttributes;
 // ...
}

IntrinsicElementAttributes 这里是所有的内部元素标签的定义,可以看到很多熟悉的元素标签。

不过它并没有 export,那我们得再看看它在那儿使用了:

type ReservedProps = {
 key?: string | number | symbol;
 ref?: RuntimeCore.VNodeRef;
 ref_for?: boolean;
 ref_key?: string;
};
type ElementAttrs<T> = T & ReservedProps;
type NativeElements = {
 [K in keyof IntrinsicElementAttributes]: ElementAttrs<IntrinsicElementAttributes[K]>;
};

它在 NativeElements 使用并扩展了 ref 等 Vue Component 扩展类型,这里其实就是 Vue 中所有内部元素的类型实现。

并且它在全局环境 JSX 命名空间中覆盖了默认类型定义:

declare global {
 namespace JSX {
 interface Element extends VNode {}
 interface ElementClass {
 $props: {};
 }
 interface ElementAttributesProperty {
 $props: {};
 }
 // 这里覆盖了默认的 IntrinsicElements
 interface IntrinsicElements extends NativeElements {
 // allow arbitrary elements
 // @ts-ignore suppress ts:2374 = Duplicate string index signature.
 [name: string]: any;
 }
 interface IntrinsicAttributes extends ReservedProps {}
 }
}

那么我们扩展 map 组件也是同样的思路,在全局环境 JSX 命名空间中覆盖 map 的定义。

  1. 创建一个 global.d.ts 文件

    import type { MapElement } from './models/Map/Core';
    declare global {
     namespace JSX {
     interface IntrinsicElements {
     // 扩展小程序内置map组件
     map: MapElement;
     }
     }
    }
    
  2. 将其导入至 tsconfig.json 中:

    {
     "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "src/**/*.nvue"]
    }
    

这样我们就可以在 IDE 中获得小程序的 map 组件的类型定义了。

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d0d94209b86242bd970d63300181e044~tplv-k3u1fbpfcp-zoom-1.image

创建 MapContext

实现点聚合只能通过 MapContext 的方式,我们可以通过 wx.createMapContext 获取,由于我用的是 uniApp,对应的 API 为:uni.createMapContext

MapContext 通过 id 跟一个 map 组件绑定,操作对应的 map 组件。

<script setup lang="ts">
 // createMapContext的第一个参数为对应的 map id,在这里即为 centerMap
 const mapCtx = ref<MapContext>();
 onMounted(() => {
 mapCtx.value = uni.createMapContext('centerMap');
 });
</script>
<template>
 <map id="centerMap" />
</template>

这里有个小坑,当 map 不是在页面直接使用,而是在某个组件中使用时,uni.createMapContext 将返回 null!

所以我们需要把它,也只能把它放到页面中来使用,这在封装 hooks 的时候也需要注意。

创建 useMapContext hooks

我们知道 vue3 推出了 组合式 API(Composition API),这让我们很容易拆离部分逻辑代码。

那我们就可以将 uni.createMapContext 相关的逻辑独立出去,让组件内部的代码更加简洁。

lib/hooks/map.ts

/**
 * 创建MapContext
 * @param mapId Map组件的id
 */
export function useMapContext(mapId: string) {
 // MapContext 实例
 const mapCtx = ref<MapContext>();
 // 在 map 组件渲染后创建 Context
 onMounted(() => {
 mapCtx.value = uni.createMapContext(mapId);
 if (!mapCtx.value) {
 console.error('请检查是否为组件内部调用,请将其添加至页面内部');
 }
 });
 return {
 mapCtx,
 };
}

在组件中使用也很方便:

<script setup lang="ts">
 const { mapCtx } = useMapContext('centerMap');
</script>
<template>
 <map id="centerMap" />
</template>

这样我们就完成了 MapContext 的创建,并很好的将其作为 hooks 独立在页面代码之外了。

实现点聚合

点聚合功能是基于图层标记 Markers 的,它是 Markers 过多时的一种优化方式。

在用户进行平移、缩放等操作时,动态的计算可视区域的 Markers 图层,其中的算法实现感兴趣的同学可以看看这篇文章 地图兴趣点聚合算法的探索与实践

封装操作 Markers 工具与点聚合初始化

我们在点聚合初始化之前可以做一些预备工作,封装一些与 Markers 相关的工具方法

lib/hooks/map.ts

export function useMapContext() {
 // ...
 // 用于记录 marker
 const markerMap = ref(new Map<number, Marker>());
 /**
 * 添加多个 marker
 * @param options 选项
 */
 const addMarkers = (options: Omit<MapContextAddMarkersOptions, 'success' | 'fail'>) => {
 return new Promise<{ errMsg: string }>((resolve, reject) => {
 if (!mapCtx.value) reject('mapContext 不存在,请检查函数调用情况!');
 else {
 mapCtx.value.addMarkers({ ...options, success: resolve, fail: reject });
 options.markers.forEach(marker => markerMap.value.set(marker.id as number, marker));
 }
 });
 };
 /**
 * 移除指定的多个 marker
 * @param options 选项
 */
 const removeMarkers = (options: { markerIds: number[] }) => {
 return new Promise<{ errMsg: string }>((resolve, reject) => {
 if (!mapCtx.value) reject('mapContext 不存在,请检查函数调用情况!');
 else if (options.markerIds.some(id => !markerMap.value.has(id))) reject('markerId 不存在!');
 else {
 options.markerIds.forEach(id => markerMap.value.delete(id));
 mapCtx.value.removeMarkers({ ...options, success: resolve, fail: reject });
 }
 });
 };
 /**
 * 修改指定的 marker
 * @param options 选项
 */
 const modifyMarker = async (options: { markerId: number; marker: Partial<Omit<Marker, 'id'>> }) => {
 if (!mapCtx.value) throw new Error('mapContext 不存在,请检查函数调用情况!');
 const mm = unref(markerMap);
 if (!mm.has(options.markerId)) throw new Error('markerId 不存在!');
 const modifiedMarker = { ...mm.get(options.markerId), ...options.marker } as Marker;
 await addMarkers({ markers: [modifiedMarker], clear: false });
 mm.set(options.markerId, modifiedMarker);
 return modifiedMarker;
 };
 /**
 * 查找指定的 marker
 * @param markerId markerId
 */
 const findMarker = (markerId: number) => {
 return markerMap.value.get(markerId);
 };
 // ...
}

有了工具,我们再封装 useMarkerCluster hooks,独立点聚合方法并实现初始化:

lib/hooks/map.ts

/**
 * Map点聚合
 * @param mapId Map组件的id
 */
export function useMarkerCluster(mapId: string) {
 const { mapCtx, addMarkers, ...othersTools } = useMapContext(mapId);
 const addMarkersUseJoin = (markers: Marker[]) => {
 return addMarkers({ markers: markers.map(item => ({ ...item, joinCluster: true })), clear: true });
 };
 onMounted(() => {
 // 初始化聚合
 mapCtx.value?.initMarkerCluster({
 enableDefaultStyle: false,
 zoomOnClick: true,
 gridSize: 60,
 complete: res => {
 console.log('initMarkerCluster', res);
 },
 });
 });
 return {
 mapCtx,
 addMarkers: addMarkersUseJoin,
 ...othersTools,
 };
}

注册聚合点创建事件

当用户的操作触发了聚合事件后,会同步触发 markerClusterCreate 事件,在这里我们需要添加新的 Markers 以实现聚合功能。

MapContext 上注册 markerClusterCreate 事件:

lib/hooks/map.ts

import MapMarkerClusterPNG from '@/static/map-marker-cluster.png';
// ...
export function useMarkerCluster(mapId: string) {
 // ...
 onMounted(() => {
 // ...
 // 注册聚合创建事件
 mapCtx.value?.on('markerClusterCreate', async (res: MarkerCluster) => {
 const clusters = res.clusters;
 if (!clusters || !clusters.length) return;
 // 添加聚合点
 await addMarkers({
 markers: clusters.map(({ center, clusterId, markerIds }) => ({
 ...center,
 // 聚合点大小
 width: 33,
 height: 38,
 clusterId,
 // 你的聚合点 icon
 iconPath: MapMarkerClusterPNG,
 })),
 clear: false,
 });
 });
 });
 // ...
}

在添加聚合点时有个大坑!重要的事情说三遍!

不能修改原有的 marker id,否则会在 android 平台出现无法更新 Markers 的情况。

不能修改原有的 marker id,否则会在 android 平台出现无法更新 Markers 的情况。

不能修改原有的 marker id,否则会在 android 平台出现无法更新 Markers 的情况。

marker label 样式渲染器

为了方便渲染样式,我们可以在使用 useMarkerCluster 的时候传入一个自定义的 label 样式渲染器,这样可以减少编写样式的样板代码。

lib/hooks/map.ts

// 默认聚合点的样式渲染器
const DEFAULT_CLUSTER_RENDERER = (count: number): MarkerLabel => ({
 fontSize: 17,
 textAlign: 'center',
 color: '#fff',
 content: count.toString(),
 anchorY: -33,
});
/**
 * Map点聚合
 * @param mapId Map组件的id
 * @param rendererCluster 聚合点的样式渲染器
 */
export function useMarkerCluster(mapId: string, rendererCluster = DEFAULT_CLUSTER_RENDERER) {
 const rendererClusterRef = ref(rendererCluster);
 // ...
 onMounted(() => {
 // ...
 // 注册聚合创建事件
 mapCtx.value?.on('markerClusterCreate', async (res: MarkerCluster) => {
 // ...
 await addMarkers({
 markers: clusters.map(({ center, clusterId, markerIds }) => ({
 // ...
 // 使用样式渲染器渲染 label
 label: rendererClusterRef.value(markerIds.length),
 })),
 });
 });
 });
 // ...
}

在页面中使用

这里写一个简单的例子给大家参考:

<script setup lang="ts">
 // 地图点聚合与标点
 const { addMarkers, modifyMarker, mapCtx, findMarker } = useMarkerCluster('centerMap');
 // 点击 marker 事件处理
 const handleMarkerTap = ({ detail: { markerId } }: MarkerEvent) => {
 // 查找 Marker
 const marker = findMarker(markerId) as Marker;
 console.log('OldMarker ->', marker);
 // 修改 Marker
 await modifyMarker({ markerId, marker: { width: 46, height: 52 } });
 // 移动视图中心至 marker 的位置
 mapCtx.value?.moveToLocation({
 longitude: marker.longitude,
 latitude: marker.latitude,
 });
 };
 // 获取 Markers
 const getMarkers = async () => {
 const yourMapMarkers = await fetchMarkers();
 console.log('GetMockMarkers', await addMarkers(yourMapMarkers));
 };
 getMarkers();
</script>
<template>
 <map id="centerMap" @markertap="handleMarkerTap" />
</template>

Android 平台的适配

小程序的 map 组件在 Android 平台和 ios 的实现有着极大的差异,就如上文所提到的 修改marker id导致无法更新 Markers 的情况。

还有一些问题也是只在 Android 平台才会出现,我们可以通过 uni.getSystemInfoSync 方法获取平台信息,这里我总结了一些遇到的问题方便各位 debug 。

获取平台信息

const { platform } = uni.getSystemInfoSync();
// Android 平台
const isAndroidPlatform = platform === 'android';

modifyMarker 需要移除原有的 marker

在 Android 平台不能通过 addMarkers 相同 id 的 marker 实现更新,这样会导致存在两个重叠的 marker。

所以我们需要主动移除对应的 marker :

lib/hooks/map.ts

// ...
/**
 * 修改指定的 marker
 * @param options 选项
 */
const modifyMarker = async (options: { markerId: number; marker: Partial<Omit<Marker, 'id'>> }) => {
 if (!mapCtx.value) throw new Error('mapContext 不存在,请检查函数调用情况!');
 const mm = unref(markerMap);
 if (!mm.has(options.markerId)) throw new Error('markerId 不存在!');
 const modifiedMarker = { ...mm.get(options.markerId), ...options.marker } as Marker;
 // Android 平台需要主动移除对应的 marker
 if (isAndroidPlatform) removeMarkers({ markerIds: [options.markerId] });
 await addMarkers({ markers: [modifiedMarker], clear: false });
 mm.set(options.markerId, modifiedMarker);
 return modifiedMarker;
};
// ...

label 的 anchorX 坐标差异

在 Android 平台上,marker 的坐标与 ios 的实现不同,它们的差异大概是宽度的一半。

如果你需要调整 label anchorX 就需要做一些适配的工作:

lib/hooks/map.ts

// ...
// 默认聚合点的样式渲染器
const DEFAULT_CLUSTER_RENDERER = (count: number, isAndroidPlatform): MarkerLabel => ({
 width: 33,
 height: 38,
 fontSize: 17,
 textAlign: 'center',
 color: '#fff',
 content: count.toString(),
 anchorY: -33,
 // 差异为宽度的一半,你需要减去这些才能与 ios 保持一直
 anchorX: isAndroidPlatform ? -17 : 0,
});
// ...

Map 组件中遇到的问题

顺便记录在使用 Map 组件中遇到的一些其他问题。

scale 的计算与显示

如果你的地图需要展示用户缩放的情况,并且需要让它可以动态的调整大小,你写的代码可能是这样的:

<script setup lang="ts">
 // 缩放
 const scale = ref(12);
 // 修改 Scale
 const handleChangeScale = (add = true) => {
 // 设置 scale 的最大、最小值
 scale.value = Math.max(Math.min(scale.value + (add ? 1 : -1), 20), 3);
 };
 // 处理RegionChange事件
 const handleRegionChange = (env: RegionChangeEvent) => {
 if (env.type === 'end' && env.causedBy === 'scale') {
 scale.value = Math.round(env.detail.scale);
 }
 };
</script>
<template>
 <map id="centerMap" :scale="scale" @regionchange="handleRegionChange" />
</template>

这里就出现了一个 bug !在监听 map regionChange 事件时,直接修改 scale 会导致用户的视图重新定位到中心位置

那怎么解决呢?我们可以定义一个只供展示的 showScale 避免这种情况:

<script setup lang="ts">
 // 缩放
 const scale = ref(12);
 const showScale = ref(12);
 // 修改 Scale
 const handleChangeScale = (add = true) => {
 // 设置 scale 的最大、最小值
 // scale.value = Math.max(Math.min(scale.value + (add ? 1 : -1), 20), 3);
 // 修改时使用 showScale 的值
 scale.value = Math.max(Math.min(showScale.value + (add ? 1 : -1), 20), 3);
 // 同步 showScale
 showScale.value = scale.value;
 };
 // 处理RegionChange事件
 const handleRegionChange = (env: RegionChangeEvent) => {
 if (env.type === 'end' && env.causedBy === 'scale') {
 // scale.value = Math.round(env.detail.scale);
 // 仅修改 showScale
 showScale.value = Math.round(env.detail.scale);
 }
 };
</script>
<template>
 <map id="centerMap" :scale="scale" show-scale @regionchange="handleRegionChange" />
</template>

getLocation 的授权检查

如果你需要获取用户的定位信息,直接调用 uni.getLocation 是会提示授权失败的,所以我们需要先检查用户的授权情况。

实现的代码如下:

<script setup lang="ts">
 // 获取用户地理位置
 const latitude = ref(0);
 const longitude = ref(0);
 async function getLocation() {
 try {
 // 请求授权,如果用户未授权则会报错,这时就需要我们提示用户开启授权
 await uni.authorize({ scope: 'scope.userLocation' });
 const res = await uni.getLocation({});
 latitude.value = res.latitude;
 longitude.value = res.longitude;
 } catch {
 const { confirm } = await uni.showModal({
 title: '提示',
 content: '请授权地理位置',
 });
 if (!confirm) {
 console.log('用户取消了授权');
 return;
 }
 const { authSetting } = await uni.openSetting();
 // 当 authSetting 不存在 scope.userLocation 时,代表用户没有同一授权
 if (!('scope.userLocation' in authSetting)) console.log('用户取消了授权');
 }
 }
</script>
<template>
 <!-- show-location是展示定位点的必要参数 -->
 <map id="centerMap" show-location />
</template>

总结

这篇文章主要是记录了我在使用小程序的 map 组件实现点聚合功能时遇到的一些问题。

可以看出写代码还是不能停留在文档,得多实践,才能感受到双重的折磨~~~

这里给大家提供一个附带工程化的 uniApp vue3 的 vite template:vite-uniapp-template

如果你想在 vscode 中支持 uniApp nvue 文件的开发适配,可以参考我的这篇文章 实现在 VSCode 中无痛开发 nvue:语法高亮、代码提示、eslint 配置及插件 Patch

输出文章不易,希望大家能多多收藏、评论与点赞!谢谢大家!

参考

微信官方文档 - 小程序 - map

Updated on 2023年06月28日

AltStyle によって変換されたページ (->オリジナル) /