【腾讯位置服务开发者征文大赛】AI 赋能小程序地图开发:腾讯地图 Miniprogram Skill 实战记录

友友们,早上好。最近2个月我一直在探索大模型如何低成本、高效率调用地图能力。前段时间我使用了腾讯地图的TMap Miniprogram Skill,它将官方文档、最佳实践与可验证代码模板封装为 AI 技能文件,让 Cursor、Claude、CodeBuddy 等工具精准理解小程序地图 API,大幅提升开发效率与代码质量。那么实际使用效果如何呢?今天就来和我一起看一下我的实战记录吧,希望文章对大家有所帮助。

文章目录

一、TMap Miniprogram Skill是什么?

简单来说TMap Miniprogram Skill 是专为 AI IDE 设计的微信小程序地图开发技能包,它把零散的地图能力整合成 AI 可直接调用的结构化知识,解决传统开发中查文档慢、写代码易出错、兼容性难保障等问题,实现自然语言描述需求到一键生成合规代码的开发新模式。
优势:

能力全覆盖: 整合地图组件、MapContext API、位置服务、LBS 后端 SDK,一站式满足地图展示、定位、POI 搜索、路线规划等需求。
模块化设计: 按功能拆分文档,AI 按需调用,回答更精准,避免无效信息干扰。
代码可验证: 内置 3 个完整示例项目,所有代码经过官方验证,AI 生成代码可直接运行。
深度集成: 无缝对接腾讯位置服务,支持地理编码、行政区划、距离计算等高级能力。

二、核心能力有哪些?

我看了一下文档,Skill 覆盖小程序地图开发全场景,总结就是四大模块:

1. 地图组件:基础可视化能力

支持地图渲染、覆盖物绘制、事件交互,满足基础地图展示需求:

2. MapContext API:32 个精细化控制接口

提供地图运行时操控能力,实现动态交互,比如:

地图控制:获取中心点、移动视野、设置边界

标记管理:添加 / 移除 / 平移标记、轨迹回放

图层操作:图片图层、可视化图层、自定义图层

坐标转换:经纬度与屏幕坐标互转、拉起导航

3. 位置服务:微信原生定位能力

对接微信官方位置接口,稳定获取位置信息:

基础定位:当前位置、模糊位置获取

交互选点:地图选点、POI 选择、位置展示

持续定位:后台定位、位置变化监听

4. LBS 后端服务:腾讯位置服务核心能力

基于 SDK 实现业务级地理服务:

搜索:POI 周边搜索、关键词提示

解析:地理编码 / 逆地理编码

路线:驾车 / 步行 / 骑行 / 公交规划

工具:距离计算、行政区划获取

三、实战记录:

第一步:获取 Skill 文件

推荐 Git 克隆,保证版本更新:

运行

复制代码
#我们 先克隆skill仓库到本地
git clone https://github.com/TencentLBS/TencentMap_miniprogram_skills.git

# 然后进入目录
cd TencentMap_miniprogram_skills

当然这里,也可直接下载压缩包解压使用。

下载的目录结构大概是这样:

第二步:AI IDE 配置

这里我以WorkBuddy 为例,当然它同时支持Cursor、CodeBuddy 和 Claude Desktop 支持通过 skills 目录加载自定义技能。步骤基本差不多,但是我实测和官方的文档有所区别,好像没有找到官方文档说的某些文件夹,不知道是不是后续改了没更新,大家按照我实测的来即可。如果有变化我也会同步更新这里的操作。

导入技能

你使用什么软件,就在什么软件导入技能

然后将刚才git下来的文件夹里面的skill,导入即可

成功后你在技能这里应该可以看到:

这里官方也有命令行的相关操作,但是它里面提到的/tmap-miniprogram我没有找到,估计是没更新吧。大家这里整体导入即可。

第三步:验证安装

下面我们开始验证是否成功,打开 AI IDE,输入测试指令比如:

帮我使用微信小程序地图组件开发一个功能:

  1. 显示当前位置的地图
  2. 添加一个自定义标记点
  3. 点击标记显示信息气泡

AI 能引用 Skill 并生成完整代码,即导入技能成功。

第四步:申请开发者 Key

登录腾讯位置服务控制台

注册并登录账号,进入「应用管理」→「我的应用」,创建新应用并添加 Key,选择「微信小程序」服务类型

,获取 Key。

我们后续使用腾讯位置服务 SDK 都是要需要配置有效的 Key。

四、实战效果:自然语言生成业务代码

下面来看我第一次生成的效果:

首先是给我生成了完整小程序目录如下:

项目生成的位置,在c盘的user下面。当然你也可以直接问WorkBuddy,它会告诉你。

这里我将生成的小程序,导入到我的微信开发者工具后,发现直接运行就不行。大概看了一下几个主要文件,发现是配置文件里,入口页面还是index,但是index里面又没有什么实际内容,生成的地图页面功能都在map中。

于是只能手动自己改一下,入口页面指向map。接着就成功了,我们看一下效果

可以看到提出的几个功能点还都是实现了。大概使用了一下,完成度都不错,当然了,ui设计还是得自己再美化一下。

接下来我又提了几个其他的补充需求:
比如:

1.显示用户当前位置地图,支持缩放拖动

2.实现搜索框,输入关键词查找周边5公里餐厅,标记展示结果

3.100个随机标记,优化渲染性能等等。

当然中间测试跑的时候,很多代码实现都不太理想,功能并没有达到我的预期,只能实现基本显示。要想完全再配合调用其他能力还得手搓改动。最终我想达到的效果就是一个能够实现水上交通用的小程序。

地图页面代码:

复制代码
<!--pages/map_chat/map_chat.wxml-->
<!--公告消息-->
 <view class="news_row">
  <view  style="width:100rpx;" bindtap="toNewsInfo">公告栏|</view>
   <view style="width:50rpx;"  >
    <cover-image style="width: 50rpx;height: 50rpx;" src="../../images/new.gif" mode="aspectFill" bindtap="addMarker"></cover-image>
   </view>
  <view style="flex:3;" >
         <swiper vertical="true" autoplay="true" interval="5000" style="height: 60rpx;" >
  <block wx:for="{{news_list}}">
    <swiper-item>
     <view class='news_content' data-msg="{{item.msg}}" bindtap="showInfo">{{item.msg}}</view>
    </swiper-item>
  </block>
</swiper>
  </view>
</view> 
  
<!--地图-->
<view class="tui-map" style="bottom:{{m_height_rpx}}">
  <map id="map"  longitude="{{currentLo}}" latitude="{{currentLa}}" enable-satellite="{{satemap}}" scale="{{scale}}" circles="{{circles}}" markers="{{markers}}" polyline="{{polyline}}" covers="{{covers}}"  include-points="{{includePoints}}"  bindregionchange='changeSee'
  show-scale ="true" show-location="true" style="width: 100%; height: 100%;" bindtap='searchMap' bindmarkertap='markerOnclick'>
  <cover-view class="icon" style="display:none">
		<cover-image style="width: 50rpx;height: 50rpx;" src="../../images/index/{{loaction_img?'location':'open'}}.png" mode="aspectFill" bindtap="openMapLocation"></cover-image>
 
	</cover-view>
  <cover-view class="icon2" >
		<cover-image style="width: 50rpx;height: 50rpx;" src="../../images/index/add_scale2.png" mode="aspectFill" bindtap="addScale"> </cover-image>

	</cover-view>
  <cover-view class="icon3" >
		<cover-image style="width: 50rpx;height: 50rpx;" src="../../images/index/del_scale2.png" mode="aspectFill" bindtap="delScale"></cover-image>
	</cover-view>
  <cover-view class="c_v start_nav" >
		<cover-image style="width: 50rpx;height: 50rpx;" src="../../images/index/{{start_nav_img?'start_nav':'stop_nav'}}.png" mode="aspectFill" bindtap="changeNavigationType"> </cover-image>
	</cover-view>
  <cover-view class="c_v up_stream" style="display:{{start_nav_img?'':'none'}}">
		<cover-image style="width: 50rpx;height: 50rpx;" src="../../images/index/{{upstream_img?'upstream_c':'upstream'}}.png" mode="aspectFill" bindtap="changeUpStreamType"> </cover-image>
	</cover-view>
  <cover-view class="c_v down_stream" style="display:{{start_nav_img?'':'none'}}">
		<cover-image style="width: 50rpx;height: 50rpx;" src="../../images/index/{{downstream_img?'downstream_c':'downstream'}}.png" mode="aspectFill" bindtap="changeDownStreamType"></cover-image>
	</cover-view>
   <cover-view class="c_v boat_stream" style="display:{{start_nav_img?'':'none'}}">
		<cover-image style="width: 50rpx;height: 50rpx;" src="../../images/index/{{boat_img?'boat_z':'boat_x'}}.png" mode="aspectFill" bindtap="changeBoatType"></cover-image>
	</cover-view>
  <cover-view class="c_v search_num" style="display:{{start_nav_img?'':'none'}}"  bindtouchstart="touchStart" bindtouchend="touchEnd" catchtap="changeSearchNum">
		{{search_txt}}
	</cover-view>
  <cover-view class="c_v search_num2" style="display:{{start_nav_img?'':'none'}}"  bindtouchstart="touchStart" bindtouchend="touchEnd" catchtap="changeSearchNum2">
		{{search_txt2}}
	</cover-view>
   <cover-view class="c_v boat_stop" style="display:{{start_nav_img?'':'none'}}">
		<cover-image style="width: 50rpx;height: 50rpx;" src="../../images/index/{{boat_stop?'open':'b_stop'}}.png" mode="aspectFill" bindtap="stopVboat"></cover-image>
	</cover-view>
   <cover-view class="icon4" >
		<cover-image style="width: 50rpx;height: 50rpx;" src="../../images/index/location.png" mode="aspectFill" bindtap="loactionByGPS"></cover-image>
	</cover-view>
    <cover-view class="cover_txt_scale" >
		{{scale_txt}}
	</cover-view>
  <cover-view class="cover_txt_location" bindtap="changeSatemap" >
		{{satemap?'卫星':'街道'}}
	</cover-view>
   <cover-view class="cover_txt_boatinfo" style="width:{{userinfo_img?'45rpx':'600rpx'}}" bindtap="" >
   <cover-image style="width:35rpx;height:35rpx;float:left" src="../../images/index/{{userinfo_img?'left_jiantou':'right_jiantou'}}.png" mode="aspectFill" bindtap="openMapUserInfoShow"></cover-image>
	  <cover-view style="width:550rpx;;margin-left:10rpx;display:{{userinfo_img?'none':''}}" bindtap="toPersonalChat">{{userinfo_text}}</cover-view>
	</cover-view>
  
    <cover-view class="cover_txt_first_info" style="display:{{firstObj.isShow&&start_nav_img?'':'none'}};background-color:{{firstObj.color}}" bindtap="" >
  	    <cover-view class=".cover_txt_boxborder">
      <cover-view style="width:550rpx;height:50rpx;margin-left:10rpx;margin-top:20rpx;">{{firstObj.txt1}}</cover-view>
	 	  <cover-view style="width:550rpx;height:50rpx;;margin-left:10rpx;">{{firstObj.txt2}}
       </cover-view>
    <cover-view style="width:550rpx;;margin-left:10rpx;">{{firstObj.txt3}}</cover-view>

     </cover-view>
	  </cover-view>
     <cover-view class="cover_txt_second_info" style="display:{{secondObj.isShow&&start_nav_img?'':'none'}};background-color:{{secondObj.color}}" bindtap="" >
  	    <cover-view class=".cover_txt_boxborder">
      <cover-view style="width:550rpx;height:50rpx;margin-left:10rpx;margin-top:20rpx;">{{secondObj.txt1}}</cover-view>
	 	  <cover-view style="width:550rpx;height:50rpx;;margin-left:10rpx;">{{secondObj.txt2}}
       </cover-view>
    <cover-view style="width:550rpx;;margin-left:10rpx;">{{secondObj.txt3}}</cover-view>
    </cover-view>
	  </cover-view>
     <cover-view class="cover_txt_third_info" style="display:{{thirdObj.isShow&&start_nav_img?'':'none'}};background-color:{{thirdObj.color}}" bindtap="" >
  	    <cover-view class=".cover_txt_boxborder">
      <cover-view style="width:550rpx;height:50rpx;margin-left:10rpx;margin-top:20rpx;">{{thirdObj.txt1}}</cover-view>
	 	  <cover-view style="width:550rpx;height:50rpx;;margin-left:10rpx;">{{thirdObj.txt2}}
       </cover-view>
    <cover-view style="width:550rpx;;margin-left:10rpx;">{{thirdObj.txt3}}</cover-view>
    </cover-view>
	  </cover-view>
  </map>

</view>
 
<view class='contentv' style="height:{{m_height_rpx}}" >
 <view class="layout_set_height">
  <view class="set_height" style="flex:1;" bindtap='upHeightMax' >最大化</view>
  <view class="set_height" style="flex:1;" bindtap='upHeight'>+</view>
  <view class="set_height" style="flex:1;" bindtap='downHeight'>-</view>
  <view class="set_height" style="flex:1;" bindtap='downHeightMin' >最小化</view>
</view> 
<!--聊天-->
<view class="news" bindtap='outbtn' style="height:{{new_bottom}}">
   <view class="chat-notice"></view>
   <view class="historycon">
   <scroll-view scroll-y="true" class="history" scroll-top="{{scrollTop}}">
    
       <block wx:for="{{newslist}}" wx:key>
          <view></view>
          <!--自己的消息  -->
          <view class="chat-news" wx:if="{{item.userId == userId}}">
              <view style="text-align: right;padding-right: 20rpx;">
                  <text class="name" bindtap="msgNotSend2" data-msgid='{{item.msgId}}'>{{ item.name }} {{ item.time}}</text>
                  <image class='new_img' src="{{item.url? item.url:'../../images/h04.jpg'}}"></image>
              </view>
              <view class='my_right'>
                  <block wx:if="{{item.type=='0'}}">      
                    <view class='new_txt'>{{item.content}}</view>
                  </block>
                  <block wx:if="{{item.type=='1'}}">
                    <image class="selectImg" src="{{item.content}}" data-src="{{item.content}}" lazy-load="true" bindtap="previewImg"></image>  
                  </block> 
                  <block wx:if="{{item.type=='2'}}">
                       <view class='new_txt'>
                       	<text class="voice_time">"{{cur_voice_id==item.msgId?cur_voice_time:item.remark}}</text> 
                        <image class="voice_img2" src="../../images/index/v2{{cur_voice_id==item.msgId?voiceImg:3}}.png" bindtap='playVoiceList'  data-file='{{item.content}}' data-id='{{item.msgId}}' data-time='{{item.remark}}'></image> 

                       </view>
                     
                  </block>
                   <block wx:if="{{item.type=='3'}}">                 
                        <video class="video_img2"  show-fullscreen-btn="{{true}}" bindfullscreenchange="screenChange"
                        controls="{{video_controls}}" id="video{{item.msgId}}"src='{{item.content}}'> </video>
                    
                  </block>
              </view>
          </view> 
          <!-- 别人的消息  -->
            <view class="chat-news" wx:else>
              <view style="text-align: left;padding-left: 20rpx;">
                  <image class='new_img' src="{{item.url? item.url:'../../images/h04.jpg'}}"></image>
                  <text class="name" bindtap="msgNotSend" data-msgid='{{item.msgId}}'> {{item.time}} {{ item.name }}</text>
              </view>
              <view class='you_left'>
                  <block wx:if="{{item.type=='0'}}">      
                    <view class='new_txt'>{{item.content}}</view>
                  </block>
                  <block wx:if="{{item.type=='1'}}">
                    <image class="selectImg" src="{{item.content}}" data-src="{{item.content}}" lazy-load="true" bindtap="previewImg"></image>  
                  </block>
                   <block wx:if="{{item.type=='2'}}">
                       <view class='new_txt'>
                        <image class="voice_img2" src="../../images/index/v{{cur_voice_id==item.msgId?voiceImg:3}}.png" bindtap='playVoiceList' data-file='{{item.content}}' data-id='{{item.msgId}}' data-time='{{item.remark}}'></image> 
                        	<text class="voice_time">{{cur_voice_id==item.msgId?cur_voice_time:item.remark}}"</text> 
                       </view>
                        </block>
                         <block wx:if="{{item.type=='3'}}">
                      
                        <video class="video_img"  show-fullscreen-btn="{{true}}" bindfullscreenchange="screenChange"
                        controls="{{video_controls}}" id="video{{item.msgId}}"src='{{item.content}}'> </video>
                    
                  </block>
              </view>
          </view>  
       </block>
   </scroll-view>
  </view>  
</view>
<view id="flag"></view> 
<!-- 聊天输入 -->
<view class="message">
    <form bindreset="cleanInput" class="sendMessage">
     <image class='voice_img' src="../../images/index/{{msg_type?'text_type':'voice_type'}}.png" bindtap='changeMsgType'></image> 
        <input type="text" style="display:{{msg_type?'':'none'}}" placeholder="请输入聊天内容.." value="{{message}}" bindinput='bindChange'></input>
        <button class="voice_but"  style="display:{{msg_type?'none':''}}" size="small" bindtap='startVoice'>{{voice_flag?"停止录音":"开始录音"}}</button>
         <view class="add" bindtap='increase'>{{increase?'-':'+'}}</view> 
        <button class ="send_but"  bindtap='send'>发送</button>
    </form>
    <view class='increased {{aniStyle?"slideup":"slidedown"}}' wx:if="{{increase}}">
       <view class="image" bindtap='chooseImage'>相册 </view>  
       <view class="image" bindtap='changeMsgType'>{{msg_type?"录音":"文字"}}</view>  
        <view class="image" bindtap='chooseVideo'>拍摄 </view>  
    </view>
     <view class='increased {{aniStyle?"slideup":"slidedown"}}' wx:if="{{increase}}">
      
    </view>
</view>

</view>

调用机器人:

bash 复制代码
Page({
  data: {
    messages: [], // 对话列表
    inputValue: '',
    loading: false
  },

  // 输入
  onInput(e) {
    this.setData({ inputValue: e.detail.value })
  },

  // 发送消息
  async sendMessage() {
    const { inputValue, messages } = this.data
    if (!inputValue.trim() || this.data.loading) return

    // 加入用户消息
    this.setData({
      messages: [...messages, { role: 'user', content: inputValue }],
      inputValue: '',
      loading: true
    })

    try {
      // 调用云开发AI机器人
      const res = await wx.cloud.extend.AI.bot.sendMessage({
        data: {
          botId: '你的botId',
          msg: inputValue
        }
      })

      // 流式接收回复
      let reply = ''
      for await (const chunk of res.textStream) {
        reply += chunk
        this.setData({
          messages: [...this.data.messages.slice(0, -1),
            { role: 'assistant', content: reply }]
        })
      }
    } catch (err) {
      console.error('调用失败', err)
    }
    this.setData({ loading: false })
  }
})

最后给大家展示一下完整的实现效果吧:

集合了地图的基础功能放大,缩小、级别显示、定位、导航。然后公告接入了天气信息、新闻消息。同时内置了聊天功能,可以发送消息和智能体进行沟通交流。这里智能体需要云开发功能。有需要,可以看下小程序的官方文档。

五、开发避坑

常见问题和解决方案

几个我遇到的常见问题:

AI 未调用 Skill:检查目录路径、SKILL.md 完整性,提问时明确 "微信小程序地图" 关键词。

定位失败:app.json 声明权限、引导用户授权、用 wx.openSetting 开启权限。

性能优化

这里性能是小程序的性能。

标记点超量:优先用点聚合,避免卡顿

数据更新:减少 setData 频率,批量更新

路线渲染:大量点位做抽稀处理

内存管理:及时销毁无用图层与覆盖物

六、总结

最后简单总结一下,TMap Miniprogram Skill 对于小程序地图开发确实很方便,尤其是初始化一个基本框架的地图小程序。

对新手:降低 API 学习门槛,自然语言生成代码,快速上手;对资深开发者:省去查文档、调试兼容时间,聚焦业务逻辑

;对项目:代码符合官方规范,减少线上 bug,提升稳定性。当然个别不足,就是和ai工具的联动可能还不太全面

相关推荐
blackorbird2 小时前
AI工作流自动化平台n8n正被大规模网络武器化
运维·网络·人工智能·自动化
阿杰学AI2 小时前
AI核心知识126—大语言模型之 CrewAI 和 AutoGen(简洁且通俗易懂版)
人工智能·语言模型·自然语言处理·agent·多智能体·智能体·多智能体协作框架
企业架构师老王2 小时前
2026年国内AI Agent选型指南:企业数字化转型中的非侵入式架构方案深度评测
人工智能·ai·架构
黎阳之光2 小时前
黎阳之光受邀出席上海口岸联合会2026智慧口岸研讨班 无感通关方案获盛赞
大数据·人工智能·算法·安全·数字孪生
hsg772 小时前
简述:地理深度学习全域训练PyTorch2.7+TorchGeo等基线
人工智能·深度学习
有梦想的牛牛2 小时前
GPT-6 能力畅想:当 AI 跨越“理解”走向“共生”
人工智能·gpt
米猴设计师2 小时前
PS电商详情页高效制作:Nano Banana一键生成电商高转化套图(附实操教程)
大数据·图像处理·人工智能·ai·aigc·startai·banana修图
落羽的落羽2 小时前
【Linux系统】深入线程:多线程的互斥与同步原理,封装实现两种生产者消费者模型
java·linux·运维·服务器·c++·人工智能·python