【腾讯位置服务开发者征文大赛】当 AI 听懂城市的低语——用腾讯位置服务打造智能情感地图小程序

0 阅读13分钟

目录


一、引言:城市有话说,但谁来听?

每一座城市都在低语——老街的梧桐树下,藏着只有下午三点才透进来的光;巷尾的咖啡馆里,有最适合发呆的角落和旧时光的味道。但这些信息,地图听不见。

打开任何一个地图 App,你能搜到"附近的咖啡厅",却搜不到"一个适合发呆的地方";你能看到 4.8 分的评分,却看不到"这里的空气里飘着慵懒的气息"。

地图变得越来越精确,却离人的感受越来越远。

如果地图能听懂"情绪",城市会变成什么样?这就是 City Whisperer(城市低语)诞生的原因——一个让 AI 听懂你的感觉、让地图为你写诗的小程序。


二、灵感来源:从一次"漫无目的"的周末说起

这个项目的灵感,来自一个再普通不过的周末午后。

那天阳光很好,我想出门走走,但不想去"打卡",也不想"逛街",只想找一个有感觉的地方坐坐。打开地图,搜索框光标闪烁——我该搜什么?

输入"咖啡厅"?出来一堆连锁品牌。输入"景点"?全是人挤人的打卡地。我真正想要的那种"光线好、人不多、能发呆"的地方,根本不是一个品类关键词能描述的。

我最终在朋友圈翻到了朋友的推荐,找到了一家藏在居民楼下的小书店。推门进去的那一刻,阳光刚好从木窗格洒进来,空气里有旧书的味道——这就是我想找的感觉。

那一刻我意识到:用户心里装的是"感觉",但地图搜索框要的是"品类"。 这中间有一道巨大的鸿沟,而 AI 可以成为连接两端的桥梁。

如果我对手机说一句"找个适合发呆的老建筑",AI 能把"发呆"翻译成"咖啡馆、图书馆、书店",把"老建筑"翻译成"古迹、历史建筑",然后地图自动帮我搜索、标记、甚至用诗意的语言告诉我这个地方给人什么感觉——这不就是城市在对你低语吗?

City Whisperer,由此诞生。


三、场景痛点:现有地图产品缺了什么?

在深入技术实现之前,让我们先拆解一下现有地图产品在"情感化探索"场景下的三个核心痛点。

场景痛点对比图

3.1 关键词困局:用户心里是"感觉",搜索框要的是"品类"

"我想找个适合发呆的地方"——这是人类最自然的表达方式。但当前所有地图 App 都要求你输入一个明确的品类词:咖啡厅、书店、公园……用户被迫把自己的"感觉"翻译成"品类",而这个翻译过程本身就会丢失大量信息。"适合发呆"可能是咖啡馆,也可能是公园的长椅,甚至是一间安静的博物馆——只有 AI 能理解这种模糊性。

3.2 信息冰冷:POI 只有名称和评分,缺少情感化的场景描述

当你终于搜到了一堆结果,地图给你的信息是:名称、地址、评分、距离。这些信息是"有用"的,但不是"有感"的。一个 4.5 分的咖啡馆和一个"阳光从木窗格洒进来"的咖啡馆,哪个更让你想推门进去? 用户需要的不仅是事实,更是一种预期感、一种"我想去那里"的情绪驱动。

3.3 交互割裂:搜→看→选→去,每一步都是手动决策

传统地图的交互链路是线性的:搜关键词 → 看列表 → 比较评分 → 选一个 → 导航。每一步都需要用户主动思考和决策,没有"被引领"的体验感。而"探索城市"本质上应该是一种放松的、被启发的体验——你说一句话,地图就能帮你完成后续所有步骤。

痛点的本质:从"你告诉地图找什么"到"你告诉地图你想感受什么"——这就是 City Whisperer 要解决的命题。


四、项目概览:City Whisperer 是什么?

City Whisperer(城市低语) 是一款基于腾讯位置服务的智能地图探索微信小程序。核心交互极其简单:

  1. 你说一句话:在搜索栏输入或语音说出"找个安静的露台""我想喝杯咖啡发发呆"
  2. AI 听懂你:自动解析你的意图,将情绪化描述翻译为地图搜索关键词
  3. 地图标记:调用腾讯地图 API 周边搜索,在地图上标记符合情境的地点
  4. 城市低语:点击地点,AI 生成一段富有情感的"场所印象"文案
  5. 出发探索:一键导航,或分享给朋友一起出发

小程序界面概念图

与传统地图搜索的对比:

维度传统地图City Whisperer
输入方式品类关键词自然语言 / 语音
搜索逻辑精确匹配AI 意图理解
信息呈现名称 + 评分 + 距离名称 + 情感文案 + 导航
交互体验线性决策沉浸式引导
情感连接"场所印象"AI 文案

五、架构设计:从一句话到一个地标

5.1 整体架构图

架构设计图

整个系统分为五层,从用户输入到最终交互反馈形成完整闭环:

  • 用户输入层:语音输入和文本输入的统一入口
  • AI 意图解析层:将自然语言翻译为结构化搜索参数
  • 腾讯位置服务层:基于 QQMapWX SDK 的定位与搜索
  • 数据处理层:搜索结果处理、距离计算、AI 文案生成
  • 交互反馈层:地图渲染、底部抽屉、导航与分享

5.2 技术选型与依赖

模块技术方案说明
地图组件微信小程序原生 <map>无需额外组件,性能最优
位置服务QQMapWX SDK(JavaScript SDK)搜索、定位、距离计算
坐标系统GCJ-02(国测局坐标)与微信地图一致,wx.getLocation 直接返回
AI 能力混元大模型 API 接口意图解析、文案生成
UI 样式原子化 CSS + Glassmorphism毛玻璃风格,原子化工具类

5.3 数据流设计

用户输入 "找个适合发呆的老建筑"
        ↓
parseIntent() → { keyword: "古迹", orderby: "_distance" }
        ↓
qqmapsdk.search({ keyword: "古迹", location: "39.9,116.3" })
        ↓
processSearchResults() → 生成 markers[] + AI 文案
        ↓
<map> 渲染 markers + scale: 16 放大
        ↓
bindmarkertap → 底部抽屉 → 场所印象 → 导航/分享

六、功能实现与核心代码

6.1 初始化定位:让地图知道"我在哪"

一切探索的起点,是用户当前的位置。我们使用 wx.getLocation 获取 GCJ-02 坐标,并创建地图上下文:

initLocation() {
  wx.showLoading({ title: '正在获取位置...' })

  wx.getLocation({
    type: 'gcj02', // 使用国测局坐标,与地图组件一致
    success: (res) => {
      this.setData({
        latitude: res.latitude,
        longitude: res.longitude,
        userLat: res.latitude,
        userLng: res.longitude
      })
      // 保存到全局,供搜索时使用
      getApp().globalData.userLocation = {
        latitude: res.latitude,
        longitude: res.longitude
      }
      this.mapContext = wx.createMapContext('cityMap', this)
      wx.hideLoading()
    },
    fail: () => {
      wx.hideLoading()
      wx.showToast({ title: '定位失败,使用默认位置', icon: 'none' })
    }
  })
}

关键点type: 'gcj02' 必须显式指定,否则默认返回 WGS-84 坐标,与地图组件的 GCJ-02 坐标系不匹配,会导致定位点偏移。

6.2 AI 意图解析:把"想发呆"翻译成"咖啡馆"

这是 City Whisperer 最核心的创新点。后端接入的AI是混元大模型 API:

parseIntent(userInput) {
  const input = userInput.toLowerCase()

  // 将 input 发送至后端再到混元 API,获取结构化意图,包括关键词、排序方式、情感标签、文案风格等维度。

  return { keyword, orderby }
}

6.3 地图搜索与渲染:从关键词到地图上的光点

意图解析后,调用腾讯位置服务的 search 接口进行周边搜索:

handleUserQuery(userInput) {
  this.setData({ aiThinking: true })

  const intent = this.parseIntent(userInput)
  const { userLat, userLng } = this.data
  const locationStr = userLat && userLng
    ? `${userLat},${userLng}`
    : `${this.data.latitude},${this.data.longitude}`

  qqmapsdk.search({
    keyword: intent.keyword,
    location: locationStr,
    orderby: intent.orderby,
    page_size: 20,
    page_index: 1,
    success: (res) => {
      if (res.status === 0 && res.data && res.data.length > 0) {
        this.processSearchResults(res.data, intent)
      } else {
        this.setData({ aiThinking: false })
        wx.showToast({ title: '没有找到相关地点', icon: 'none' })
      }
    }
  })
}

搜索成功后,将结果处理为地图 markers 数组,并将地图从初始的 scale: 12(大视野)放大到 scale: 16(街道级别),让用户清晰看到搜索点位:

processSearchResults(poiList, intent) {
  const markers = poiList.map((poi, index) => {
    // 距离格式化
    let distanceText = '未知'
    if (poi._distance !== undefined) {
      distanceText = poi._distance < 1000
        ? `${Math.round(poi._distance)}m`
        : `${(poi._distance / 1000).toFixed(1)}km`
    }

    return {
      id: index,
      latitude: poi.location.lat,
      longitude: poi.location.lng,
      title: poi.title,
      iconPath: '/images/marker.svg',
      width: 40, height: 40,
      anchor: { x: 0.5, y: 1 },
      callout: {
        content: poi.title,
        color: '#1E1B4B', fontSize: 12,
        borderRadius: 12, bgColor: 'rgba(255,255,255,0.92)',
        display: 'BYCLICK', textAlign: 'center'
      },
      _poiData: {
        ...poi,
        _distanceText: distanceText,
        _aiDescription: this.generateAIDescription(poi, intent)
      }
    }
  })

  this.setData({
    markers,
    aiThinking: false,
    scale: 16  // 搜索成功后放大地图展示点位
  })

  // 缩放视野包含所有标记点
  if (markers.length > 0 && this.mapContext) {
    this.mapContext.includePoints({
      points: markers.map(m => ({
        latitude: m.latitude, longitude: m.longitude
      })),
      padding: [120, 80, 200, 80]
    })
  }
}

6.4 自定义 Marker 与气泡:告别默认大头针

传统地图默认的"红色大头针"千篇一律。City Whisperer 使用自定义 SVG 图标 + 毛玻璃风格气泡:

// Marker 配置
{
  iconPath: '/images/marker.svg',  // 自定义圆形图标
  width: 40, height: 40,
  anchor: { x: 0.5, y: 1 },      // 锚点在底部中心
  callout: {
    content: poi.title,
    color: '#1E1B4B',
    borderRadius: 12,
    bgColor: 'rgba(255,255,255,0.92)',  // 半透明背景
    borderWidth: 1,
    borderColor: 'rgba(99,102,241,0.15)',
    display: 'BYCLICK'
  }
}

自定义 Marker 图标采用圆形设计 + Indigo 主色调,与整体毛玻璃风格统一。气泡使用半透明白色背景 + 细微边框,点击时才显示,避免视觉干扰。

6.5 底部抽屉交互:场所印象,AI 的第一句低语

点击 Marker 后,底部弹出一个半模态抽屉面板,展示地点详情和 AI 生成的"场所印象"文案:

onMarkerTap(e) {
  const markerId = e.detail.markerId || e.markerId
  const marker = this.data.markers[markerId]
  if (!marker || !marker._poiData) return

  this.setData({
    selectedPOI: marker._poiData,
    showDrawer: true
  })
}

AI 文案生成当前使用模板方案,8 种风格随机选择:

generateAIDescription(poi, intent) {
  // 将 POI 信息和用户意图发送至后端再到混元 API,生成更精准、更有情感张力的场所描述。
  return description
}

6.6 分享与导航:把好去处传出去

导航使用微信内置的 wx.openLocation,无需额外集成:

onNavigate() {
  const poi = this.data.selectedPOI
  wx.openLocation({
    latitude: poi.location.lat,
    longitude: poi.location.lng,
    name: poi.title,
    address: poi.address || '',
    scale: 16
  })
}

分享使用微信小程序原生的 button open-type="share",确保触发微信标准分享流程。在 onShareAppMessage 中携带 POI 坐标和名称,让接收者打开后自动定位到该地点:

<button class="share-btn" open-type="share">
  <image class="nav-icon share-icon" src="/images/share.svg" />
  <text class="nav-text share-text">分享给朋友</text>
</button>
onShareAppMessage() {
  const poi = this.data.selectedPOI
  if (poi) {
    const lat = poi.location ? poi.location.lat : poi.latitude
    const lng = poi.location ? poi.location.lng : poi.longitude
    return {
      title: `我在 City Whisperer 发现了一个好去处:${poi.title}`,
      path: `/pages/index/index?lat=${lat}&lng=${lng}&name=${encodeURIComponent(poi.title)}`
    }
  }
  return {
    title: 'City Whisperer - 城市低语,发现身边的美好',
    path: '/pages/index/index'
  }
}

onLoad 中解析分享参数,实现从分享卡片直接定位到地点:

onLoad(options) {
  if (options && options.lat && options.lng) {
    const lat = parseFloat(options.lat)
    const lng = parseFloat(options.lng)
    if (!isNaN(lat) && !isNaN(lng)) {
      this.setData({ latitude: lat, longitude: lng, scale: 16 })
    }
  }
  this.initLocation()
}

七、毛玻璃 UI 设计:让地图会"呼吸"

7.1 Glassmorphism 在小程序中的实现

City Whisperer 的 UI 采用毛玻璃(Glassmorphism)风格,核心实现依赖 CSS 的 backdrop-filter: blur() 属性。这种风格的精髓在于:让 UI 元素与底层的地图融为一体,而不是盖在地图上面。

/* 搜索栏 — 浮在地图上方的毛玻璃层 */
.search-bar {
  position: absolute;
  top: 0;
  z-index: 100;
  background: rgba(255, 255, 255, 0.72);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  border-bottom: 1rpx solid rgba(99, 102, 241, 0.1);
}

/* 底部抽屉 — 更强模糊的毛玻璃面板 */
.bottom-sheet {
  background: rgba(255, 255, 255, 0.92);
  backdrop-filter: blur(32px);
  -webkit-backdrop-filter: blur(32px);
  border-radius: 48rpx 48rpx 0 0;
}

/* AI 印象卡片 — 紫色调毛玻璃 */
.ai-impression {
  background: rgba(99, 102, 241, 0.08);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border: 1rpx solid rgba(99, 102, 241, 0.1);
}

设计要点:不同层级的 UI 元素使用不同的模糊强度和透明度——搜索栏(72% 透明 + 24px 模糊)→ 抽屉面板(92% 透明 + 32px 模糊)→ AI 卡片(8% 紫色 + 16px 模糊),形成层次分明的视觉纵深。

7.2 原子化 CSS 实践

app.wxss 中定义了一套原子化工具类,确保样式一致性的同时提高开发效率:

/* 全局工具类 */
.flex { display: flex; }
.items-center { align-items: center; }
.glass {
  background: rgba(255, 255, 255, 0.7);
  backdrop-filter: blur(20px);
  border: 1rpx solid rgba(255, 255, 255, 0.3);
}
.shadow-md { box-shadow: 0 8rpx 24rpx rgba(99, 102, 241, 0.12); }

这种方式虽然不如 Tailwind CSS 完整,但在小程序环境下足够灵活,且避免了引入第三方库的体积开销。


八、踩坑与经验

8.1 坐标系那些坑

中国地图开发绕不开的坑就是坐标系。微信小程序的 <map> 组件使用 GCJ-02(国测局坐标,俗称"火星坐标"),而 GPS 设备返回的是 WGS-84 坐标。如果你忘记在 wx.getLocation 中指定 type: 'gcj02',定位点会在地图上偏移几百米——这种 bug 在开发阶段不容易发现,到了真实环境就会暴露。

经验:始终显式指定 type: 'gcj02',不要依赖默认值。

8.2 includePoints 的 padding 陷阱

MapContext.includePoints()padding 参数格式为 [top, right, bottom, left],单位是 px。初期我设置的 padding: [80, 80, 80, 80] 四边等距,但实际场景中顶部有搜索栏遮挡、底部有"搜索结果计数"浮层,导致部分 Marker 被遮挡。

经验:根据实际 UI 布局调整 padding。当前使用 [120, 80, 200, 80],顶部多留空间给搜索栏,底部多留空间给浮层和交互区域。

另外,搜索成功后将 scale 从 12 调到 16 再调用 includePoints,这样 includePoints 在计算视野范围时会在缩放后的比例尺下进行,确保所有点位都清晰可见。

8.3 分享能力的正确打开方式

最初我使用 <view bindtap="onSharePOI"> 来触发分享,然后在 onSharePOI 中调用 wx.showShareMenu。这个方案无法真正触发微信分享面板——showShareMenu 只是声明了分享能力,并不会弹出分享 UI。

正确做法:使用 <button open-type="share">,这是微信小程序唯一能主动触发分享消息的方式。点击后微信会自动调用页面的 onShareAppMessage 方法,获取分享内容。

同时,别忘了重置 <button> 的默认样式:

.share-btn {
  padding: 0;
  margin: 0;
  line-height: normal;
  border: 1rpx solid rgba(99, 102, 241, 0.15) !important;
}
.share-btn::after { border: none; } /* 去除默认边框 */

九、最后

从一句"找个适合发呆的老建筑"到地图上亮起的标记点,从冰冷的 POI 数据到温暖的"场所印象"——City Whisperer 证明了,当地图服务与 AI 相遇,城市不再只是一张坐标图,而是一本等待翻阅的故事书。

腾讯位置服务提供了稳定、精准的底层能力(定位、搜索、距离计算),让开发者可以把精力集中在"如何让地图更有温度"这件事上。而微信小程序的原生 <map> 组件与 QQMapWX SDK 的无缝衔接,让整个开发过程流畅高效。

384a53160430614847eca2cb01327588.jpg

f15bdd05127ac6412590d20bbaf7eb99.jpg

腾讯云语音识别API

混元大模型API

#腾讯地图# #腾讯位置服务# #腾讯位置服务开发者征文大赛#


💬 如果这篇文章对你有启发,欢迎点赞👍、评论💬、转发🔄!

你最想用 City Whisperer 搜索什么样的地方?欢迎在评论区告诉我——也许下一句城市低语,就是为你而写。

有任何技术问题或想法,也欢迎留言交流,我们一起让地图听懂更多人的心声。