更新记录

1.0.202605031(2026-05-03)

  1. 修正已知问题

1.0.20260504(2026-05-03)

  1. 优化README

1.0.20260503(2026-05-03)

  1. 三端插件发布
查看更多

平台兼容性

uni-app(5.0)

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
- × × × ×
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
× × × × × × × × × × × ×

uni-app x(5.0)

Chrome Safari Android iOS 鸿蒙 微信小程序
- - -

cms-mpnav 三端地图导航插件

cms-mpnav 是一个基于 UTS 的 App 端地图导航唤起插件,支持 Android、iOS、HarmonyOS 三端调用第三方地图或系统地图进行路线导航。

  • 支持 Android、iOS、HarmonyOS。
  • 支持高德地图、百度地图、腾讯地图。
  • iOS 支持苹果地图兜底。
  • HarmonyOS 支持花瓣地图兜底。
  • Android 在未指定目标地图时优先打开已安装的第三方地图,无可用第三方地图时尝试系统 geo 导航。
  • 支持驾车、步行、公交、骑行导航类型。
  • 支持 gcj02wgs84bd09 坐标传入,插件内部会按目标地图自动转换坐标。
  • 提供地图安装检测和可用地图列表查询能力。
  • 插件支持uniapp & uniappx

平台支持

平台 支持地图
Android 高德地图、百度地图、腾讯地图
iOS 高德地图、百度地图、腾讯地图、苹果地图
HarmonyOS 高德地图、百度地图、腾讯地图、花瓣地图

说明:第三方地图是否能被唤起,取决于用户设备是否已安装对应地图 App,以及系统是否允许当前应用拉起目标 App。

引入插件

在插件市场点击“试用”或“购买”后导入项目,然后制作自定义基座包进行真机测试。

import {
  getAvailableMaps,
  isMapInstalled,
  openNavigation
} from '@/uni_modules/cms-mpnav'

也可以一次性引入:

import * as mpnav from '@/uni_modules/cms-mpnav'

API 说明

方法名 描述
openNavigation(options) 唤起地图进行导航
getAvailableMaps(): AvailableMapInfo[] 获取当前系统已安装地图
isMapInstalled(mapId) 检测地图是否安装

openNavigation(options)

打开地图导航。

openNavigation(options: OpenNavigationOptions): Promise<boolean>

返回值:

返回值 说明
Promise resolve(true) 已成功发起地图唤起请求
Promise reject({ code, msg }) 参数错误、目标地图未安装、当前平台不支持目标地图或唤起失败

注意:resolve(true) 表示已经向系统发起打开地图请求,不代表用户一定完成了导航。

OpenNavigationOptions

参数 类型 必填 默认值 说明
destination MpnavPoint - 终点坐标
origin MpnavPoint \| null 不传 起点坐标,不传时由目标地图自行使用当前位置
coordType 'gcj02' \| 'wgs84' \| 'bd09' gcj02 传入坐标的坐标系
navType 'drive' 驾车 \| 'walk' 步行 \| 'bus' 公交 \| 'bike' 骑行 drive 导航方式
targetMap 'amap' 高德 \| 'baidu' 百度 \| 'tencent' 腾讯 \| 'apple' 苹果 \| 'petal' 花瓣 自动选择 指定目标地图
success () => void - 成功发起导航时回调
fail (code: number, msg: string) => void - 调用失败回调,code 为错误码,msg 为中文错误信息
complete () => void - 调用结束回调,成功失败都会执行

MpnavPoint

参数 类型 必填 说明
lat number 纬度,范围 -9090
lng number 经度,范围 -180180
title string \| null 地点名称,会展示在目标地图中

targetMap 可选值

地图 Android iOS HarmonyOS
amap 高德地图 支持 支持 支持
baidu 百度地图 支持 支持 支持
tencent 腾讯地图 支持 支持 支持
apple 苹果地图 不支持 支持 不支持
petal 花瓣地图 不支持 不支持 支持

不传 targetMap 时:

  • Android:优先使用已安装的高德、百度、腾讯地图
  • iOS:优先使用已安装的高德、百度、腾讯地图;都不可用时使用苹果地图。
  • HarmonyOS:优先使用已安装的高德、百度、腾讯地图;都不可用时使用花瓣地图。

getAvailableMaps()

获取当前设备可用地图列表。

getAvailableMaps(): AvailableMapInfo[]

AvailableMapInfo

参数 类型 说明
mapId string 地图标识,例如 amapbaidutencentapplepetal
name string 地图名称
scheme string 地图唤起协议
packageName string \| null Android 包名,非 Android 平台可能为 null
isInstalled boolean 是否可用

isMapInstalled(mapId)

判断指定地图在当前平台是否可用。

isMapInstalled(mapId: string): boolean

示例:

console.log(isMapInstalled('amap'))
console.log(isMapInstalled('tencent'))

uniapp完整示例

<template>
    <view class="page">
        <view class="card">
            <view class="title">cms-mpnav 地图导航示例</view>
            <view class="desc">选择地图、起点和终点后发起导航。起点默认当前定位,终点默认正弘城。</view>

            <view class="section">
                <view class="section-title">地图选择</view>
                <view v-if="availableMaps.length > 0" class="map-list">
                    <view
                        v-for="item in availableMaps"
                        :key="item.mapId"
                        class="map-item"
                        :class="[item.mapId, { active: selectedMapId === item.mapId }]"
                        @click="handleSelectMap(item.mapId)"
                    >
                        <view class="map-icon">{{ item.shortName }}</view>
                        <view class="map-info">
                            <view class="map-name">{{ item.name }}</view>
                            <view class="map-status">{{ item.isInstalled ? "已安装" : "未安装" }}</view>
                        </view>
                    </view>
                </view>
                <view v-else class="empty-text">当前平台暂无可展示地图。</view>
                <button class="btn" @click="refreshMaps">刷新地图列表</button>
            </view>

            <view class="section">
                <view class="section-title">起点</view>
                <view class="location-card">
                    <block v-if="origin.title">
                        <view class="location-title">{{ origin.title || "当前位置" }}</view>
                        <view class="location-coord">{{ formatPoint(origin) }}</view>
                    </block>
                    <block v-else>
                        当前位置
                    </block>
                </view>
                <view class="btn-row">
                    <button class="btn half" @click="useCurrentLocation">使用当前位置</button>
                    <button class="btn half" @click="chooseOrigin">选择起点</button>
                </view>
            </view>

            <view class="section">
                <view class="section-title">终点</view>
                <view class="location-card">
                    <view class="location-title">{{ destination.title }}</view>
                    <view class="location-coord">{{ formatPoint(destination) }}</view>
                </view>
                <button class="btn" @click="chooseDestination">选择终点</button>
            </view>

            <view class="section">
                <view class="section-title">导航方式</view>
                <view class="nav-list">
                    <view
                        v-for="item in navTypeOptions"
                        :key="item.value"
                        class="nav-item"
                        :class="{ active: navType === item.value }"
                        @click="navType = item.value"
                    >
                        {{ item.label }}
                    </view>
                </view>
            </view>

            <button class="btn primary" @click="openSelectedNavigation">开始导航</button>

            <view class="result">{{ result }}</view>
        </view>
    </view>
</template>

<script setup>
import { onMounted, ref } from "vue";
import {
    isMapInstalled,
    openNavigation
} from "@/uni_modules/cms-mpnav";

const defaultDestination = {
    lat: 34.795836,
    lng: 113.680473,
    title: "正弘城"
};

const defaultOrigin = {
    lat: 34.789401,
    lng: 113.685214,
    title: "郑州市动物园"
};

const availableMaps = ref([]);
const selectedMapId = ref("");
const navType = ref("drive");
const result = ref("等待操作");
const origin = ref({
    ...defaultOrigin
});
const destination = ref({
    ...defaultDestination
});

const navTypeOptions = [
    { label: "驾车", value: "drive" },
    { label: "步行", value: "walk" },
    { label: "公交", value: "bus" },
    { label: "骑行", value: "bike" }
];

const supportedMapMeta = {
    amap: { mapId: "amap", name: "高德地图", shortName: "高" },
    baidu: { mapId: "baidu", name: "百度地图", shortName: "百" },
    tencent: { mapId: "tencent", name: "腾讯地图", shortName: "腾" },
    apple: { mapId: "apple", name: "苹果地图", shortName: "苹" },
    petal: { mapId: "petal", name: "花瓣地图", shortName: "花" }
};

function setResult(text) {
    result.value = text;
}

function formatPoint(point) {
    return `${Number(point.lat).toFixed(6)}, ${Number(point.lng).toFixed(6)}`;
}

function getPlatformSupportedMapIds() {
    const systemInfo = uni.getSystemInfoSync();
    const platform = String(systemInfo.platform || "").toLowerCase();
    if (platform == "android") {
        return ["amap", "baidu", "tencent"];
    }
    if (platform == "ios") {
        return ["amap", "baidu", "tencent", "apple"];
    }
    if (platform.indexOf("harmony") >= 0 || platform.indexOf("ohos") >= 0) {
        return ["amap", "baidu", "tencent", "petal"];
    }
    return [];
}

function refreshMaps() {
    const mapIds = getPlatformSupportedMapIds();
    const maps = mapIds.map((mapId) => {
        const meta = supportedMapMeta[mapId];
        let installed = false;
        try {
            installed = isMapInstalled(mapId);
        } catch (err) {
            installed = false;
        }
        return {
            mapId: meta.mapId,
            name: meta.name,
            shortName: meta.shortName,
            isInstalled: installed
        };
    });
    availableMaps.value = maps;
    if (maps.length > 0) {
        const currentSelected = maps.find((item) => item.mapId === selectedMapId.value);
        if (!currentSelected) {
            selectedMapId.value = maps[0].mapId;
        }
        setResult(`当前平台支持地图:${maps.map(item => item.name).join("、")}`);
        return;
    }
    selectedMapId.value = "";
    setResult("当前平台暂无可用地图范围");
}

function handleSelectMap(mapId) {
    selectedMapId.value = mapId;
    const target = availableMaps.value.find((item) => item.mapId === mapId);
    if (!isMapInstalled(mapId)) {
        setResult(`未安装${target ? target.name : mapId},调用导航时会进入 fail 回调`);
        return;
    }
    setResult(`当前选择地图:${target ? target.name : mapId}`);
}

function useCurrentLocation() {
    origin.value.title = "";
    origin.value.lat = "";
    origin.value.lng = "";
}

function choosePoint(onSuccess) {
    uni.chooseLocation({
        success(res) {
            onSuccess({
                lat: Number(res.latitude),
                lng: Number(res.longitude),
                title: res.name || res.address || "已选择位置"
            });
        },
        fail(err) {
            console.error("chooseLocation failed:", err);
            setResult("选择位置失败");
        }
    });
}

function chooseOrigin() {
    choosePoint((point) => {
        origin.value = point;
        setResult(`已选择起点:${point.title}`);
    });
}

function chooseDestination() {
    choosePoint((point) => {
        destination.value = point;
        setResult(`已选择终点:${point.title}`);
    });
}

async function openSelectedNavigation() {
    let params = {
        coordType: "gcj02",
        destination: destination.value,
        navType: navType.value,
        targetMap: selectedMapId.value || null,
        success() {
            setResult("success 回调:已发起导航");
        },
        fail(code, msg) {
            setResult(`fail 回调:${code},${msg}`);
        },
        complete() {
            console.log("openNavigation complete");
        }
    };

    if (origin.value.title) {
        params.origin = origin.value;
    }

    try {
        await openNavigation(params);
        console.log("openNavigation Promise resolved");
    } catch (err) {
        console.error("openNavigation failed:", err);
    }
}

onMounted(() => {
    refreshMaps();
});
</script>

<style>
.page {
    min-height: 100vh;
    padding: 20rpx;
    box-sizing: border-box;
    background: linear-gradient(180deg, #eef4ff 0%, #f8fafc 100%);
}

.card {
    padding: 22rpx;
    border-radius: 18rpx;
    background: #ffffff;
    box-shadow: 0 12rpx 48rpx rgba(15, 23, 42, 0.08);
}

.title {
    font-size: 32rpx;
    font-weight: 700;
    color: #0f172a;
}

.desc {
    margin-top: 8rpx;
    color: #475569;
    font-size: 22rpx;
    line-height: 1.5;
}

.section {
    margin-top: 20rpx;
}

.section-title {
    margin-bottom: 10rpx;
    font-size: 26rpx;
    font-weight: 600;
    color: #1e293b;
}

.map-list {
    display: flex;
    flex-direction: column;
    gap: 10rpx;
}

.map-item {
    display: flex;
    align-items: center;
    padding: 14rpx;
    border: 2rpx solid #dbe4f0;
    border-radius: 14rpx;
    background: #f8fafc;
}

.map-item.active {
    border-color: #1677ff;
    background: #eef6ff;
}

.map-icon {
    width: 56rpx;
    height: 56rpx;
    border-radius: 16rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    color: #ffffff;
    font-size: 24rpx;
    font-weight: 700;
}

.map-item.amap .map-icon {
    background: #1677ff;
}

.map-item.baidu .map-icon {
    background: #d93026;
}

.map-item.tencent .map-icon {
    background: #19be6b;
}

.map-item.apple .map-icon {
    background: #111111;
}

.map-item.petal .map-icon {
    background: #ff6a00;
}

.map-info {
    margin-left: 14rpx;
}

.map-name {
    font-size: 24rpx;
    color: #0f172a;
    font-weight: 600;
}

.map-status {
    margin-top: 4rpx;
    font-size: 20rpx;
    color: #64748b;
}

.empty-text {
    padding: 14rpx;
    border-radius: 14rpx;
    background: #f8fafc;
    color: #64748b;
    font-size: 20rpx;
}

.location-card {
    padding: 14rpx;
    border-radius: 14rpx;
    background: #f8fafc;
    border: 2rpx solid #e2e8f0;
}

.location-title {
    font-size: 24rpx;
    color: #0f172a;
    font-weight: 600;
}

.location-coord {
    margin-top: 4rpx;
    font-size: 20rpx;
    color: #64748b;
}

.nav-list {
    display: flex;
    flex-wrap: wrap;
    gap: 10rpx;
}

.nav-item {
    padding: 10rpx 18rpx;
    border-radius: 999rpx;
    border: 2rpx solid #dbe4f0;
    background: #f8fafc;
    color: #334155;
    font-size: 22rpx;
    flex: 1;
    text-align: center;
}

.nav-item.active {
    border-color: #1677ff;
    background: #1677ff;
    color: #ffffff;
}

.btn {
    margin-top: 10rpx;
    transform: scale(0.96);
    transform-origin: center top;
}

.btn-row {
    display: flex;
    gap: 10rpx;
}

.half {
    flex: 1;
}

.primary {
    background: #1677ff;
    color: #ffffff;
}

.result {
    margin-top: 20rpx;
    padding: 16rpx;
    border-radius: 12rpx;
    background: #0f172a;
    color: #e2e8f0;
    font-size: 20rpx;
    line-height: 1.5;
    word-break: break-all;
    white-space: pre-wrap;
}
</style>

坐标系说明

请根据你业务中保存的坐标类型正确传入 coordType

坐标系 说明 常见来源
gcj02 国测局坐标 高德地图、腾讯地图、uni.getLocation 设置 type: 'gcj02'
wgs84 GPS 原始坐标 GPS 设备、部分海外地图数据、uni.getLocation 设置 type: 'wgs84'
bd09 百度坐标 百度地图、百度定位

如果坐标系传错,地图中显示的位置会出现偏移。

错误码

openNavigation 参数校验失败或唤起失败时会进入 fail(code, msg) 回调,并返回 Promise reject({ code, msg })

错误码 说明
9001001 终点坐标无效,请检查 destination.latdestination.lng
9001002 起点坐标无效,请检查 origin.latorigin.lng
9001003 targetMap 参数无效
9001004 当前设备没有可用地图,且系统地图打开失败
9001005 当前平台不支持该地图
9001006 未安装目标地图,请下载安装后使用
9001007 打开目标地图失败,请稍后重试

常见问题

为什么真机没有反应?

请确认已经制作并运行自定义基座。UTS 原生能力需要在 App 真机环境中测试,普通浏览器、H5、小程序环境不支持。

为什么会进入 fail 回调或 Promise reject?

常见原因:

  • 用户设备未安装指定地图 App。
  • 当前平台不支持该地图,例如 Android 指定 apple、iOS 指定 petal
  • 传入的经纬度无效。
  • 系统限制当前应用打开外部 App。

建议先调用 isMapInstalled(mapId)getAvailableMaps() 判断可用地图,再展示对应入口。

坐标为什么偏移?

大概率是 coordType 传错。插件会根据 coordType 做坐标转换,请确保传入坐标与声明的坐标系一致。

是否需要申请地图开放平台 Key?

本插件是唤起用户设备上已安装的地图 App 导航,不是在 App 内渲染地图,因此基础导航唤起通常不需要集成地图 SDK Key。部分地图平台可能对来源字段、referer、应用标识有自己的规则,如你的应用有更严格的上架或合规要求,请以对应地图开放平台说明为准。

需要帮助?有其他插件需求?联系我

隐私、权限声明

1. 本插件需要申请的系统权限列表:

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。