Skip to content

用户中心与房屋信息(二) ​

本页对应 用户中心与房屋信息,共 6 段视频,约 1 小时 0 分。

学习目标 ​

  • 能使用 button 的 open-type="chooseAvatar" 与 bindchooseavatar 获取用户选择的头像临时路径,并完成头像回显与页面数据更新。
  • 能分别使用 wx.uploadFile 和封装后的 http.upload 上传头像,按接口文档正确设置 url、name=file、filePath、Authorization 请求头及 type=avatar 附加参数,并解析返回值更新头像。
  • 能在小程序后台分别配置 request 与 uploadFile 合法域名,并通过项目设置刷新验证配置生效,能定位“不在合法域名列表”错误。
  • 能完成腾讯位置服务账号注册与认证、创建应用和 Key,并为地点搜索、地址解析等接口分配配额,说明未分配配额时无法正常调用。
  • 能下载并引入腾讯位置服务微信小程序 SDK,创建 QQMapWX 实例,配置 api.map.qq.com 请求域名,调用地点搜索接口获取住宅小区结果,并排查授权拒绝、SDK 路径错误等常见问题。

知识点 ​

为什么需要位置服务:先明确 LBS 的定位 ​

位置服务(LBS)是基于用户位置来提供服务的技术。房屋管理里要添加房屋、选择社区、查看附近小区,都离不开“用户在哪儿”。小程序自身能拿到经纬度,但像周边搜索、逆地址解析、路径规划这类能力,通常要配合第三方地图服务实现,比如腾讯地图、高德地图、百度地图。本项目采用腾讯位置服务,申请入口是:https://lbs.qq.com/。

怎么想:先把“定位”和“地图能力”分开。定位由小程序 API 负责,地图检索与地址解析由第三方 SDK 负责。
怎么做:按笔记流程走——注册账号、创建应用、生成 key、在小程序管理后台添加合法域名。步骤参考视频和官方文档,不在这里赘述。
易错点:key 不是随便填的;合法域名不配置,SDK 底层请求会被小程序拦截。

申请 key 与配置合法域名:准备工作不能省 ​

为什么需要:腾讯位置服务要识别是哪个应用在调用,也要限制调用来源;小程序则要求网络请求域名合法。
怎么想:创建应用后生成 key;SDK 底层会向腾讯位置服务域名发请求,因此要在小程序后台“开发管理 → 开发设置 → 服务器域名”里把对应请求域名加入 request 合法域名。转写里口述为“API map 点 QQ 点 com”,实际配置以官方文档和控制台提示为准,常见为 https://apis.map.qq.com。
怎么做:登录小程序管理后台,找到开发设置,修改 request 合法域名并保存;回到开发者工具,在“详情 → 项目设置”里刷新,直到能看到新域名。
易错点:只加 request 不够时,要区分 uploadFile 等独立接口;位置服务这里重点是 request 合法域名。另:腾讯位置服务控制台还要给“搜索”“逆地址解析”等接口做配额分配,不分配额度可能调用不了。

获取用户位置:先声明,再调用 API ​

为什么需要:使用获取位置 API 前,必须先在 app.json 声明,并在小程序管理后台申请,否则会受限制,相关限制见:https://developers.weixin.qq.com/community/develop/doc/000a02f2c5026891650e7f40351c01。
怎么想:把“声明”和“调用”看成两步,缺一不可。
怎么做:在 app.json 中加入:

json
{
  "requiredPrivateInfos": [
    "getLocation",
    "chooseLocation"
  ],
  "permission": {
    "scope.userLocation": {
      "desc": "你的位置信息将用于小程序位置接口的效果展示"
   }
}

调用 wx.getLocation() 获取当前位置,该 API 支持返回 Promise:

javascript
Page({
  async getLocation() {
    // 调用 API
    const {latitude, longitude} = await wx.getLocation()
    console.log(latitude, longitude)
  }
})

调用 wx.chooseLocation 会打开地图,由用户自由选择位置后返回经纬度,该 API 也支持 Promise:

javascript
Page({
  async chooseLocation() {
    // 调用小程序 API 获取新的位置
    const {latitude, longitude} = await wx.chooseLocation()
		console.log(latitude, longitude)
  },
})

易错点:首次授权弹窗要点“允许”;误点拒绝后,可在开发者工具里清除缓存重置授权。转写中还提醒:JS 里导入文件要用相对路径,不能像 WXML/WXSS 那样随便用斜杠代表根路径。

导入 SDK 并实例化:把 key 收口到工具文件 ​

为什么需要:腾讯位置服务提供的是封装好的 JavaScript SDK,直接调用官方方法比手写请求更稳。
怎么想:把 SDK 放到 libs,把实例化收口到 utils/qqmap.js,页面只导入实例,避免每个页面都写 key。
怎么做:下载官方 SDK:https://mapapi.qq.com/web/miniprogram/JSSDK/qqmap-wx-jssdk1.2.zip,它是官方封装好的 .js 文件。然后:

javascript
// utils/qqmap.js
// 导入腾讯位置服务 SDK
import QQMapWX from '../libs/qqmap-wx-jssdk'
// 实例化位置服务(使用个人申请的 key)
export default new QQMapWX({
  key: '填写自已的 KEY',
})

易错点:官方示例里可能用旧语法 require,项目里可转换为 import;路径写错会报“模块找不到”,要按自己项目实际目录检查。

search 搜索周边小区:经纬度转成业务数据 ​

为什么需要:添加房屋时要让用户看到附近社区,不能只显示经纬度。
怎么想:先拿用户经纬度,再以该点为中心搜索“住宅小区”,最后只保留页面需要的字段。
怎么做:调用 SDK 的 search 实现搜索周边小区,文档:https://lbs.qq.com/miniProgram/jsSdk/jsSdkGuide/methodSearch。

javascript
// 导入腾讯位置服务
import QQMap from '../../../utils/qqmap'

Page({
  onLoad() {
    // 获取用户经纬度
    this.getLocation()
  },
  async getLoaction() {
    // 调用小和序 API 获取用户位置
    const { latitude, longitude } = await wx.getLocation()
    // 获取周边小区
    this.getPoint(latitude, longitude)
  },
  getPoint(latitude, longitude) {
    // search 是实现地点搜索功能的方法
    QQMap.search({
      keyword: '住宅小区', //搜索关键词
      location: [latitude, longitude].join(','), //设置周边搜索中心点
      page_size: 5,
      success: (result) => {
        // 过滤掉多余的数据
        const points = result.data.map(({ id, title, _distance }) => {
          return { id, title, _distance }
        })
        // 渲染数据
        this.setData({ points })
      },
    })
  }
})

将获取的小区信息渲染到页面上:

xml
<!-- 检测用户登录状态 -->
<authorization>
  <view class="locate">
    <van-cell-group border="{{false}}" title="当前地点">
      ...
    </van-cell-group>
    <van-cell-group border="{{false}}" title="附近社区">
      <van-cell
        wx:for="{{points}}"
        wx:key="id"
        title="{{item.title}}"
        link-type="navigateTo"
        url="/house_pkg/pages/building/index"
        is-link
      />
    </van-cell-group>
  </view>
</authorization>

易错点:onLoad 里调用的是 this.getLocation(),但方法名写成了 getLoaction,大小写/拼写不一致会找不到方法;success 里要用箭头函数,否则 this.setData 的 this 可能不对。

reverseGeocoder 逆地址解析:经纬度变成具体地址 ​

为什么需要:页面上要显示“当前地点”,只有经纬度用户看不懂。
怎么想:逆地址解析就是根据经纬度获取具体地址信息。
怎么做:在 getPoint 里同时调用 reverseGeocoder,文档:https://lbs.qq.com/miniProgram/jsSdk/jsSdkGuide/methodReverseGeocoder。

javascript
// 导入腾讯位置服务
import QQMap from '../../../utils/qqmap'
Page({
  // ...
  getPoint(latitude, longitude) {
    // 逆地址解析(根据经纬度来获取地址)
    QQMap.reverseGeocoder({
      location: [latitude, longitude].join(','),
      success: ({ result: { address } }) => {
        // 数据数据
        this.setData({ address })
      },
    })
    
    // search 是实现地点搜索功能的方法
    QQMap.search({
      keyword: '住宅小区', //搜索关键词
      location: [latitude, longitude].join(','), //设置周边搜索中心点
      page_size: 5,
      success: (result) => {
        // 过滤掉多余的数据
        const points = result.data.map(({ id, title, _distance }) => {
          return { id, title, _distance }
        })
        // 渲染数据
        this.setData({ points })
      },
    })
  }
})

易错点:location 要传“纬度,经度”的字符串,用 [latitude, longitude].join(',') 拼;控制台要给逆地址解析分配额度。

chooseLocation 重新选择位置:复用 getPoint ​

为什么需要:用户可能不在当前位置,需要手动选点。
怎么想:选完新位置后,仍然复用 getPoint 重新搜索和解析,避免重复逻辑。
怎么做:

javascript
// 导入腾讯位置服务
import QQMap from '../../../utils/qqmap'
Page({
  // ...
	chooseLocation() {
    // 调用小程序 API 获取新的位置
    const { latitude, longitude } = await wx.chooseLocation()
    // 获取新的位置附近的小区
    this.getPoint(latitude, longitude)
  },
  getPoint(latitude, longitude) {
    // ...
  }
})

易错点:chooseLocation 是 async 方法,别漏 await;选点后要重新调用 getPoint,否则页面还是旧数据。调试时若报模块错误,先查导入路径;若定位不弹窗,查授权缓存和 app.json 声明。

本节目标与数据来源 ​

为什么需要:完整的房屋信息由 3 个部分组成:小区名称、楼栋号、房间号。真实开发中,这 3 部分通常需要后端分别提供接口获取;但全国小区数据量特别大,后端很难短时间整理完成,所以当前阶段暂时没有接口,只能先在前端临时随机生成数据,把“添加房屋”的流程跑通。

怎么想:先把页面流转和数据结构打通,等后端接口具备后再替换随机逻辑。小区名称来自上一步位置服务获取到的地点信息,楼栋号和房间号先用随机函数生成。转写稿中提到的腾讯位置服务、地点搜索“住宅小区”、配置合法域名等,都是为“小区名称”这一来源做准备的;本节代码里的 point 承接上一步传入的小区/地点名称,再拼到后续 URL 和标题中。

怎么做:进入楼栋页时,通过 onLoad 接收地址参数,调用 fake 生成假数据,再渲染到页面。假数据只用于课堂演示,不能当成正式业务逻辑。

易错点:不要把随机数据当成最终方案;point 是上一步带过来的地点名称,不是经纬度;页面跳转时如果不在 URL 上拼参数,下一页的 onLoad({ point, building }) 会解构不到值。

楼栋信息的随机生成与渲染 ​

为什么需要:楼栋页要展示“某小区下的若干栋楼”,用户点击后才能进入房间页。当前没有后端接口,所以先随机生成楼栋数量和楼栋名称后缀。

怎么想:用 Math.random() 生成 3~6 栋楼;再根据数量决定显示“栋”还是“号楼”。这只是演示逻辑,真实开发应调用接口获取楼栋列表。

怎么做:

javascript
// house_pkg/pages/building/index.js
Page({
  data: {
    size: 0,
    point: '',
    type: '',
  },
  // 获取地址参数
  onLoad({ point }) {
    // 生成假数据
    this.fake(point)
  },
  fake(point) {
    // 生成楼栋数(用于上课)
    const size = Math.floor(Math.random() * 4) + 3
    // 楼栋名称(xx小区 / xx栋)
    const type = size > 4 ? '号楼' : '栋'
    // 数据渲染
    this.setData({ size, type, point })
  },
})

将生成的楼栋信息渲染到页面中:

xml
<view class="building">
  <van-cell-group border="{{false}}">
    <van-cell
      wx:for="{{size}}"
      wx:key="*this"
      title="{{point}}{{item + 1}}{{type}}"
      link-type="navigateTo"
      url="/house_pkg/pages/room/index"
      is-link
    />
  </van-cell-group>
</view>

易错点:Math.floor(Math.random() * 4) + 3 得到的是 3~6,不是 3~7;wx:for 绑定数字时会按数量循环,item 可理解为索引;当前 url 没有拼接 point、building 等参数,若直接照抄,房间页可能拿不到小区和楼栋信息,实际跳转时应通过 query 或点击事件补参。

房间信息的随机生成与渲染 ​

为什么需要:房间页要展示某栋楼下的房间号,用户选择房间后进入业主信息表单。当前同样没有后端接口,所以继续用随机函数生成房间号。

怎么想:先随机生成房间数量,再循环生成楼层号和房号,拼成完整房间号,并尽量保证不重复。渲染时把小区、楼栋和房间号拼在一起展示。

怎么做:

javascript
// house_pkg/pages/room/index.ts
Page({
  data: {
    point: '',
    building: '',
    rooms: [],
  },
  onLoad({ point, building }) {
    // 创建房间
    this.fake(point, building)
  },
  fake(point, building) {
    // 生成多少个房间
    const size = Math.floor(Math.random() * 5) + 4
    const rooms = []
    for (let i = 0; i < size; i++) {
      // 楼层号生成 1 ~ 20
      const floor = Math.floor(Math.random() * 19) + 1
      // 具体的房间号生成 1 ~ 3
      const No = Math.floor(Math.random() * 2) + 1
      const room = [floor, 0, No].join('')
      // 检测是否有重复的房间号
      if (rooms.includes(room)) return
      // 记录生成完整的房间号
      rooms.push(room)
    }
    // 渲染数据
    this.setData({ rooms, point, building })
  },
})

将生成的房间号信息渲染到页面当中:

xml
<view class="room">
  <van-cell-group border="{{false}}">
    <van-cell
      wx:for="{{rooms}}"
      wx:key="*this"
      title="{{point}}{{building}} {{item}}"
      link-type="navigateTo"
      url="/house_pkg/pages/form/index"
      is-link
    />
  </van-cell-group>
</view>

易错点:注释写“1~20”,但 Math.floor(Math.random() * 19) + 1 实际是 1~19;注释写“1~3”,但 Math.floor(Math.random() * 2) + 1 实际是 1~2。更关键的是,if (rooms.includes(room)) return 会直接结束整个 fake 函数,而不是跳过本次循环;若想继续生成,应改成 continue,或使用 while 直到数量足够。房间页跳转表单页时也应确认参数是否携带完整,否则后续表单页无法知道当前选中的是哪个小区、哪栋楼、哪个房间。

调试与联调提醒 ​

本节流程能跑通的前提,是上一步位置服务已经拿到小区名称。结合转写稿,使用腾讯位置服务时要先创建应用、添加 key、分配搜索等接口配额,下载并引入小程序 SDK,并在小程序后台把 api.map.qq.com 配到合法域名中;调试搜索“住宅小区”时若弹授权,要选择允许,误点拒绝可在工具里清除缓存后重试。转写稿里的“回选”应为“回显”,“封袋”应为 FormData,“app file”应为 wx.uploadFile,这些同音/口误在正式代码里要写准确。

最后要明确:当前楼栋、房间号都是临时假数据,正式开发时应把 fake 替换为后端接口,并补充 loading、空数据、异常处理;页面跳转时把 point、building、房间号等通过 URL query 或事件参数传下去,才能保证房屋信息 3 个组成部分完整串联。

先处理未登录:优化 authorization 组件 ​

为什么需要:添加房屋必须基于登录用户,未登录时既不能提交,也不应让页面在 onLoad、onShow 中先发出请求。否则会出现“页面还没跳登录,请求已经发出”的无效请求甚至报错。

怎么想:登录态统一从 getApp().token 判断。authorization 组件在 attached 时已经能拿到当前页面栈,因此可以在未登录时把当前页面的 onLoad、onShow 覆盖成空函数,再重定向到登录页,并带上回跳地址。

怎么做:

javascript
// /components/authorization/index.js
Component({
  // ...
  lifetimes: {
    attached() {
      // 获取登录状态
      const isLogin = !!getApp().token
      // 变更登录状态
      this.setData({ isLogin })
      // 获取页面栈
      const pageStack = getCurrentPages()
      // 获取页面路径
      const currentPage = pageStack.pop()
      // 未登录的情况下跳转到登录页面
      if (!isLogin) {
        // 使用空白函数覆盖原生的生命周期 onLoad onShow
        currentPage.onLoad = () => {}
        currentPage.onShow = () => {}
        wx.redirectTo({
          url: '/pages/login/index?redirectURL=/' + currentPage.route,
        })
      }
    },
  },
})

易错点:getCurrentPages() 取到的是页面栈,当前页要用 pop() 取;覆盖生命周期必须在页面请求发出前完成;登录成功后要能根据 redirectURL 回到添加房屋页。

获取房屋基础信息与表单数据 ​

为什么需要:房屋的地址信息(如 point、building、room)通常由上一页通过地址参数带入,不需要用户重复填写;业主姓名、性别、手机号等则要在表单中采集。

怎么想:地址参数在 onLoad({ point, building, room }) 中直接接收并 setData;表单字段用 model:value 双向绑定,提交时从 this.data 统一取。

怎么做:

javascript
// house_pkg/pages/form/index.js
Page({
  onLoad({point, building, room}) {
    // 获取房屋信息数据
    this.setData({point, building, room})
  }
})

易错点:地址参数名要和上一页传参一致;model:value 绑定的字段要提前在 data 中声明;性别可给默认值,避免验证时为空。

上传身份证照片:chooseMedia + uploadFile ​

为什么需要:身份证正反面需要先让用户从相册选择或拍照,再把临时文件上传到服务器,最终表单里保存的是服务器返回的图片 URL。

怎么想:先用 wx.chooseMedia 拿到 tempFiles[0].tempFilePath,再调用 wx.uploadFile。它与普通 wx.request 不同:请求地址要手动拼 wx.http.baseURL,header 要手动带 Authorization,文件字段用 name: 'file',文件路径用 filePath。转写稿中上传头像的调试经验同样适用于这里:uploadFile 返回的 result.data 是字符串,要先 JSON.parse;若取不到 URL,检查是否少取了一级 data,即 data.data.url。

怎么做:

javascript
// house_pkg/pages/form/index.js
Page({
	onLoad({point, building, room}) {
    // 获取房屋信息数据
    this.setData({point, building, room})
  },
  // 上传身份证照片
  async uploadPicture(ev) {
    // 区分用户上传的是正面或反面
    const type = ev.mark.type

    try {
      // 打开相册或拍照
      const media = await wx.chooseMedia({
        count: 1,
        mediaType: ['image'],
        sizeType: ['compressed'],
      })

      // 调用 API 上传图片
      wx.uploadFile({
        url: wx.http.baseURL + '/upload',
        filePath: media.tempFiles[0].tempFilePath,
        name: 'file',
        header: {
          Authorization: 'Bearer ' + getApp().token,
        },
        success: (result) => {
          // 处理返回的 json 数据
          const data = JSON.parse(result.data)
          // 判断接口是否调用成功
          if (data.code !== 10000) return wx.utils.toast('上传图片失败!')
          // 渲染数据
          this.setData({[type]: data.data.url})
        },
      })
    } catch (err) {
      // 获取图片失败
      console.log(err)
    }
  },
})

易错点:name 必须按接口要求写 file,不能按普通请求传 FormData;filePath 是临时路径;未配置 uploadFile 合法域名会提示“不在 uploadFile 合法域名列表”,需到小程序后台“开发管理 → 开发设置”添加,并在开发者工具“详情 → 项目设置”刷新;ev.mark.type 用来区分正反面,模板上要正确传 data-type 或 mark:type。

表单验证与提交 ​

为什么需要:姓名、手机号、身份证照片必须校验,避免无效数据提交到后端。

怎么想:地址信息来自参数、性别有默认值,可不校验;其余字段用 wechat-validate。提交前先 this.validate(),通过后再剔除 this.data 中可能多余的 __webviewId__、status,最后 POST /room。

怎么做:

javascript
// house_pkg/pages/form/index.js
// 导入表单验证插件
import wxValidate from 'wechat-validate'
Page({
  behaviors: [wxValidate],
  data: {
    point: '',
    building: '',
    room: '',
    name: '',
    gender: 1,
    mobile: '',
    idcardFrontUrl: '',
    idcardBackUrl: '',
  },
  rules: {
    name: [
      { required: true, message: '业主姓名不能为空!' },
      { pattern: /^[\u4e00-\u9fa5]{2,5}$/, message: '业主姓名只能为中文!' },
    ],
    mobile: [
      { required: true, message: '业主手机号不能为空!' },
      { pattern: /^1[3-8]\d{9}$/, message: '请填写正确的手机号!' },
    ],
    idcardFrontUrl: [
      { required: true, message: '请上传身份证国徽面!' }
    ],
    idcardBackUrl: [
      { required: true, message: '请上传身份证照片面!' }
    ],
  },
  // 提交审核
  async submitForm() {
    // 验证数据
    if (!this.validate()) return
    // 获取全部的数据(剔除可能多余参数 __webviewId__)
    const { __webviewId__, status, ...data } = this.data
    // 调用接口
    const { code } = await wx.http.post('/room', data)
    // 检测接口是否调用成功
    if (code !== 10000) return wx.utils.toast('提交数据失败!')
    // 返回房屋列表页面
    wx.navigateBack({ delta: 4 })
  },
})

接口文档地址保留:接口文档地址。

易错点:不验证直接提交会绕过必填;this.data 可能带框架字段,必须解构剔除;code !== 10000 要提示并终止;navigateBack({ delta: 4 }) 的 delta 表示返回几步,层数要与实际页面栈一致,否则回不到房屋列表页。

本节目标与页面关系 ​

为什么需要:添加房屋成功后,页面会通过 wx.navigateBack 返回房屋列表;此时列表必须重新查询一次,否则用户看不到刚添加的房屋。同时,房屋数据属于用户私有数据,查询时必须处于登录状态。

怎么想:列表页首次进入要查,返回列表页也要查。onLoad 只在页面初次加载时执行,navigateBack 返回时不会再次触发;而 onShow 在页面每次显示时都会执行,所以把查询逻辑放在 onShow 中。接口文档地址见:接口文档地址。

怎么做:在 data 中准备 houseList、isEmpty、dialogVisible,在 onShow 中调用 getHouseList,请求 /room,根据 code 判断成功后再渲染。

javascript
Page({
  data: {
    houseList: [],
    isEmpty: false,
    dialogVisible: false,
  },
  onShow() {
    // 获取用房屋列表
    this.getHouseList()
  },
  // 用户房屋列表
  async getHouseList() {
    // 调用接口
    const { code, data: houseList } = await wx.http.get('/room')
    // 检测接口是否调用成功
    if (code !== 10000) return wx.utils.toast()
    // 渲染数据
    this.setData({
      houseList,
      isEmpty: houseList.length === 0,
    })
  },
	// ...
})

易错点:不要把 getHouseList 只写在 onLoad 里,否则添加房屋返回后列表不刷新;不要漏掉 code !== 10000 的判断;setData 时要把 houseList 和 isEmpty 一起更新,避免列表为空但空状态不显示。转写稿中反复强调“按接口文档写参数、按封装库方式调用”,这里使用 wx.http.get 就是沿用封装请求库,路径可直接写 /room,不要误写成原生 wx.request 后忘记拼基地址和登录头。

空状态与最多五条的限制 ​

为什么需要:新用户没有认证房屋时,列表为空,不能只显示一个空白页;同时业务上最多允许添加 5 条房屋,达到上限后不应继续展示“添加新房屋”按钮。

怎么想:空状态由 houseList.length === 0 推导,用 isEmpty 控制 wx:if。添加按钮是否显示,用 houseList.length < 5 判断;列表本身仍按接口返回渲染。

怎么做:非空时渲染 scroll-view 和房屋列表;为空时展示提示和跳转添加页的 navigator;按钮放在 toolbar 中,并用 wx:if 控制。

xml
<authorization>
  <block wx:if="{{!isEmpty}}">
    <scroll-view show-scrollbar="{{false}}" enhanced scroll-y>
      <view class="houses">
        <view class="houses-title">房屋信息</view>
        <view class="houses-list">
          <van-swipe-cell
            wx:for="{{houseList}}"
            wx:key="id"
            mark:index="{{index}}"
            mark:id="{{item.id}}"
            async-close
            bind:close="swipeClose"
            right-width="{{70}}"
          >
            <van-cell-group mark:id="{{item.id}}" bind:tap="goDetail" border="{{false}}">
              <van-cell size="large" title="{{item.point}}">
                <text wx:if="{{item.status === 1}}" class="tag info">正在审核</text>
                <text wx:if="{{item.status === 2}}" class="tag success">审核通过</text>
                <text wx:if="{{item.status === 3}}" class="tag fail">审核失败</text>
              </van-cell>
              <van-cell
                title="房间号"
                border="{{false}}"
                value="{{item.building}}{{item.room}}"
              />
              <van-cell title="业主" border="{{false}}" value="{{item.name}}" />
            </van-cell-group>
            <view slot="right">删除</view>
          </van-swipe-cell>
        </view>
      </view>
    </scroll-view>
    <view wx:if="{{houseList.length < 5}}" class="toolbar" bind:tap="addHouse">
      <text class="enjoy-icon icon-add"></text>
      <text class="button-text">添加新房屋</text>
    </view>
  </block>
  <view wx:else class="blank">
    您还没有认证房屋,请点击
    <navigator hover-class="none" class="link" url="/house_pkg/pages/locate/index">添加</navigator>
  </view>
</authorization>

易错点:isEmpty 初值不要长期写成 true 或忘记更新;wx:if 与 wx:else 要成对使用;按钮条件是 < 5,不是 <= 5;wx:key="id" 要保留,避免列表更新时复用错项。

列表渲染与状态标签 ​

为什么需要:列表要展示房屋地址、房间号、业主和审核状态。审核状态用数字 1/2/3 返回,页面需要转成可读文案。

怎么想:point 作为大标题;房间号由 building 和 room 拼接;业主用 name;状态分别映射“正在审核”“审核通过”“审核失败”。左滑删除用 van-swipe-cell,点击进入详情用 bind:tap="goDetail"。

怎么做:保留上面的 WXML 结构,status 判断使用严格等于 ===,状态文案与类名对应。

易错点:不要把 building 和 room 写反;status 是数字时不要和字符串比较;mark:id、mark:index 是给事件处理函数取参数用的,删除和详情逻辑不要漏掉对应方法。

调试思路与常见问题 ​

为什么需要:列表页问题通常集中在“有没有请求”“请求是否成功”“数据是否渲染”三处。

怎么想:先看 onShow 是否执行,再看 /room 返回的 code 是否为 10000,最后看 setData 后 houseList 和 isEmpty 是否正确。若添加房屋返回后不刷新,优先检查是否误用了 onLoad;若请求失败,检查登录态和请求库拦截器是否正常带上 Authorization。转写稿中提到的“合法域名、配额、清缓存”等准备工作主要服务于添加房屋时的定位流程,若添加流程本身走不通,列表自然也不会有新数据。

怎么做:在 getHouseList 中临时打印返回值,确认 data 是数组;确认 wx.navigateBack 返回后触发的是 onShow。

易错点:不要把接口成功判断写成 code === 10000 之外还继续渲染;空列表时不要仍显示添加按钮区域;达到 5 条后只隐藏“添加新房屋”按钮,不要误删列表数据。

课件图示 ​

课件第8页:多⼀句没有,少⼀句不⾏,用更短时间,教会更实用的技术! ⾼级软件⼈才培训专家 l

课件第11页:多⼀句没有,少⼀句不⾏,用更短时间,教会更实用的技术! ⾼级软件⼈才培训专家 享

课件第13页:多⼀句没有,少⼀句不⾏,用更短时间,教会更实用的技术! ⾼级软件⼈才培训专家 享

课件第12页:多⼀句没有,少⼀句不⾏,用更短时间,教会更实用的技术! ⾼级软件⼈才培训专家 享

课件第14页:多⼀句没有,少⼀句不⾏,用更短时间,教会更实用的技术! ⾼级软件⼈才培训专家 享

课件第15页:多⼀句没有,少⼀句不⾏,用更短时间,教会更实用的技术! ⾼级软件⼈才培训专家 享

课件第16页:多⼀句没有,少⼀句不⾏,用更短时间,教会更实用的技术! ⾼级软件⼈才培训专家 享

课件第17页:多⼀句没有,少⼀句不⾏,用更短时间,教会更实用的技术! ⾼级软件⼈才培训专家 享

代码示例 ​

1. 位置服务:获取位置、逆地址解析、搜索周边小区 ​

json
// app.json(片段)
{
  "requiredPrivateInfos": [
    "getLocation",
    "chooseLocation"
  ],
  "permission": {
    "scope.userLocation": {
      "desc": "你的位置信息将用于小程序位置接口的效果展示"
    }
  }
}
javascript
// utils/qqmap.js
// 需先将腾讯位置服务小程序 SDK 放到 libs/qqmap-wx-jssdk.js
import QQMapWX from '../libs/qqmap-wx-jssdk'

export default new QQMapWX({
  key: '填写自已的 KEY',
})
javascript
// house_pkg/pages/locate/index.js
import QQMap from '../../../utils/qqmap'

Page({
  data: {
    address: '',
    points: [],
  },

  onLoad() {
    this.getLocation()
  },

  async getLocation() {
    const { latitude, longitude } = await wx.getLocation()
    this.getPoint(latitude, longitude)
  },

  getPoint(latitude, longitude) {
    // 逆地址解析:经纬度 -> 具体地址
    QQMap.reverseGeocoder({
      location: [latitude, longitude].join(','),
      success: ({ result: { address } }) => {
        this.setData({ address })
      },
    })

    // 搜索周边住宅小区
    QQMap.search({
      keyword: '住宅小区',
      location: [latitude, longitude].join(','),
      page_size: 5,
      success: (result) => {
        const points = result.data.map(({ id, title, _distance }) => {
          return { id, title, _distance }
        })
        this.setData({ points })
      },
    })
  },

  async chooseLocation() {
    const { latitude, longitude } = await wx.chooseLocation()
    this.getPoint(latitude, longitude)
  },
})
xml
<!-- house_pkg/pages/locate/index.wxml -->
<view class="locate">
  <view class="address" bindtap="chooseLocation">
    当前地点:{{address || '点击选择位置'}}
  </view>

  <view class="title">附近社区</view>
  <view wx:for="{{points}}" wx:key="id" class="item">
    {{item.title}} <text>{{item._distance}}m</text>
  </view>
</view>

关键行与易错点:

  • requiredPrivateInfos 和 permission 必须先在 app.json 中声明,否则 wx.getLocation / wx.chooseLocation 无法正常使用。
  • 腾讯位置服务要单独申请 key,并在小程序后台把 https://apis.map.qq.com 加入 request 合法域名。
  • QQMap.search 的 location 要求是 "纬度,经度" 字符串,所以用 [latitude, longitude].join(',')。
  • reverseGeocoder 的返回值在 result.result.address,示例中通过 { result: { address } } 解构。
  • 用户如果误点“拒绝授权”,需要在小程序开发工具中清除缓存后重新授权。

2. 添加房屋:表单验证、身份证图片上传、提交 ​

javascript
// house_pkg/pages/form/index.js
import wxValidate from 'wechat-validate'

Page({
  behaviors: [wxValidate],

  data: {
    point: '',
    building: '',
    room: '',
    name: '',
    gender: 1,
    mobile: '',
    idcardFrontUrl: '',
    idcardBackUrl: '',
  },

  rules: {
    name: [
      { required: true, message: '业主姓名不能为空!' },
      { pattern: /^[\u4e00-\u9fa5]{2,5}$/, message: '业主姓名只能为中文!' },
    ],
    mobile: [
      { required: true, message: '业主手机号不能为空!' },
      { pattern: /^1[3-8]\d{9}$/, message: '请填写正确的手机号!' },
    ],
    idcardFrontUrl: [
      { required: true, message: '请上传身份证国徽面!' },
    ],
    idcardBackUrl: [
      { required: true, message: '请上传身份证照片面!' },
    ],
  },

  onLoad({ point, building, room }) {
    this.setData({ point, building, room })
  },

  async uploadPicture(ev) {
    // 通过 mark:type 区分正反面
    const type = ev.mark.type

    try {
      const media = await wx.chooseMedia({
        count: 1,
        mediaType: ['image'],
        sizeType: ['compressed'],
      })

      wx.uploadFile({
        url: wx.http.baseURL + '/upload',
        filePath: media.tempFiles[0].tempFilePath,
        name: 'file',
        header: {
          Authorization: 'Bearer ' + getApp().token,
        },
        success: (result) => {
          const data = JSON.parse(result.data)

          if (data.code !== 10000) {
            return wx.utils.toast('上传图片失败!')
          }

          this.setData({ [type]: data.data.url })
        },
      })
    } catch (err) {
      console.log(err)
    }
  },

  async submitForm() {
    if (!this.validate()) return

    // 剔除可能多余参数
    const { __webviewId__, status, ...data } = this.data

    const { code } = await wx.http.post('/room', data)

    if (code !== 10000) {
      return wx.utils.toast('提交数据失败!')
    }

    wx.navigateBack({ delta: 4 })
  },
})
xml
<!-- house_pkg/pages/form/index.wxml -->
<view class="form">
  <view>房屋:{{point}}{{building}}{{room}}</view>

  <input model:value="{{name}}" placeholder="业主姓名" />

  <radio-group model:value="{{gender}}">
    <label><radio value="{{1}}" />男</label>
    <label><radio value="{{2}}" />女</label>
  </radio-group>

  <input model:value="{{mobile}}" type="number" placeholder="业主手机号" />

  <view>
    <button mark:type="idcardFrontUrl" bindtap="uploadPicture">
      上传身份证国徽面
    </button>
    <image wx:if="{{idcardFrontUrl}}" src="{{idcardFrontUrl}}" mode="aspectFill" />
  </view>

  <view>
    <button mark:type="idcardBackUrl" bindtap="uploadPicture">
      上传身份证照片面
    </button>
    <image wx:if="{{idcardBackUrl}}" src="{{idcardBackUrl}}" mode="aspectFill" />
  </view>

  <button bindtap="submitForm">提交</button>
</view>

关键行与易错点:

  • ev.mark.type 来自 WXML 中的 mark:type,不能用 data-type 代替。
  • wx.uploadFile 不支持 Promise,所以上传结果要写在 success 回调里。
  • result.data 是字符串,需要 JSON.parse 后才能取 code 和 data.url。
  • name: 'file' 必须与后端接口要求的文件字段一致,不能随意写。
  • filePath 使用 media.tempFiles[0].tempFilePath,不要写成 media.tempFiles[0]。
  • wx.uploadFile 不会自动带 token,需要手动在 header 中加 Authorization。
  • 上传接口和普通 request 接口要分别配置合法域名,uploadFile 合法域名不配置会报“不在合法域名列表”。
  • 提交前必须 this.validate(),否则空表单也会提交。

3. 房屋列表:登录拦截、onShow 获取列表、空状态 ​

javascript
// components/authorization/index.js
Component({
  data: {
    isLogin: false,
  },

  lifetimes: {
    attached() {
      const isLogin = !!getApp().token
      this.setData({ isLogin })

      const pageStack = getCurrentPages()
      const currentPage = pageStack.pop()

      if (!isLogin) {
        // 未登录时覆盖页面生命周期,避免页面发出请求
        currentPage.onLoad = () => {}
        currentPage.onShow = () => {}

        wx.redirectTo({
          url: '/pages/login/index?redirectURL=/' + currentPage.route,
        })
      }
    },
  },
})
xml
<!-- components/authorization/index.wxml -->
<slot wx:if="{{isLogin}}"></slot>
json
// components/authorization/index.json
{
  "component": true
}
javascript
// house_pkg/pages/list/index.js
Page({
  data: {
    houseList: [],
    isEmpty: false,
  },

  onShow() {
    // 添加房屋成功后通过 navigateBack 返回,会触发 onShow
    this.getHouseList()
  },

  async getHouseList() {
    const { code, data: houseList } = await wx.http.get('/room')

    if (code !== 10000) {
      return wx.utils.toast()
    }

    this.setData({
      houseList,
      isEmpty: houseList.length === 0,
    })
  },

  addHouse() {
    wx.navigateTo({
      url: '/house_pkg/pages/locate/index',
    })
  },
})
xml
<!-- house_pkg/pages/list/index.wxml -->
<authorization>
  <block wx:if="{{!isEmpty}}">
    <scroll-view show-scrollbar="{{false}}" enhanced scroll-y>
      <view class="houses">
        <view class="houses-title">房屋信息</view>

        <view class="houses-list">
          <view wx:for="{{houseList}}" wx:key="id" class="house-item">
            <view>
              {{item.point}}
              <text wx:if="{{item.status === 1}}" class="tag info">正在审核</text>
              <text wx:if="{{item.status === 2}}" class="tag success">审核通过</text>
              <text wx:if="{{item.status === 3}}" class="tag fail">审核失败</text>
            </view>

            <view>房间号:{{item.building}}{{item.room}}</view>
            <view>业主:{{item.name}}</view>
          </view>
        </view>
      </view>
    </scroll-view>

    <view wx:if="{{houseList.length < 5}}" class="toolbar" bindtap="addHouse">
      <text class="button-text">添加新房屋</text>
    </view>
  </block>

  <view wx:else class="blank">
    您还没有认证房屋,请点击
    <navigator hover-class="none" class="link" url="/house_pkg/pages/locate/index">
      添加
    </navigator>
  </view>
</authorization>
json
// house_pkg/pages/list/index.json
{
  "usingComponents": {
    "authorization": "/components/authorization/index"
  },
  "navigationBarTitleText": "我的房屋"
}

关键行与易错点:

  • 列表接口必须放在 onShow 中调用,因为添加房屋后是 wx.navigateBack 返回,onLoad 不会再次触发。
  • authorization 组件在未登录时覆盖 onLoad 和 onShow,防止页面在未登录状态下继续发请求。
  • isEmpty 用来控制空状态,houseList.length === 0 时显示“您还没有认证房屋”。
  • 列表最多展示 5 条,houseList.length < 5 时才显示“添加新房屋”按钮。
  • 如果项目已引入 Vant,可将普通 view 替换为笔记中的 van-cell-group、van-cell、van-swipe-cell 结构。
  • wx.http.get('/room') 依赖项目已封装 wx.http,并且请求拦截器已自动带 token。

常见错误 ​

1. 上传头像报"xxx 不在 uploadFile 合法域名列表"

  • 现象:普通接口请求一切正常,一上传就 fail。
  • 原因:小程序把 request 与 uploadFile 的合法域名分开管理,只配了前者。
  • 修法:管理后台 → 开发管理 → 开发设置 → 服务器域名,在 uploadFile 合法域名里补上;回工具"详情 → 项目设置 → 刷新",直到列表里能看到该域名。

2. 用 wx.request / http.post 传文件,后端收不到 file 字段

  • 现象:接口通了但拿不到图片地址,或直接参数错误。
  • 原因:上传走的是独立 API,参数结构是 name + filePath + formData,不是普通 data。
  • 修法:改用 wx.uploadFile 或 http.upload,name 按文档写死为 file,头像场景在 formData 里带 type: 'avatar'。

3. success 回调里取 url 得到 undefined / JSON.parse 报错

  • 现象:result.data.url 为 undefined。
  • 原因:wx.uploadFile 返回的 data 是字符串,且未走拦截器,层级比封装接口多一层。
  • 修法:JSON.parse(result.data) 后再取 data.data.url;或直接用 http.upload 让库替你拆包。

4. 位置接口直接 fail,或"拒绝"后再也弹不出授权框

  • 现象:wx.getLocation 调用失败;或第一次点了拒绝后永远拿不到位置。
  • 原因:app.json 缺 requiredPrivateInfos / permission 声明;拒绝状态被本地缓存。
  • 修法:补齐声明并在后台申请;开发阶段用"工具 → 清除缓存 → 全部清除"重置,线上引导 wx.openSetting。

5. 腾讯位置服务 SDK 报"模块找不到",或 search 返回空

  • 现象:import 直接报错;或能跑但结果为空。
  • 原因:JS 里不能用 / 开头的根路径(那是 WXML/WXSS 的写法),只能用相对路径;另外未给具体接口分配配额。
  • 修法:改成相对路径引入 SDK;到"账户额度 → 配额分配"给 search、reverseGeocoder 分别配额度。

6. search 的 success 里 this.setData 报 undefined

  • 现象:Cannot read property 'setData' of undefined。
  • 原因:success 用了普通函数,this 指向丢失。
  • 修法:改成箭头函数,或在调用前先存一份 this。

7. 添加完房屋返回列表,列表不刷新

  • 现象:navigateBack 回来还是空列表。
  • 原因:请求写在 onLoad,返回时不触发该生命周期。
  • 修法:搬到 onShow。

8. 未登录状态页面仍狂发请求

  • 现象:控制台一片 401。
  • 原因:authorization 组件只做了跳转,没拦住页面自身生命周期。
  • 修法:未登录时先用空函数覆盖当前页的 onLoad / onShow,再 redirectTo。

9. 点击头像没反应

  • 现象:绑了事件但不触发。
  • 原因:小程序事件名全小写,写成 bindChooseAvatar 无效。
  • 修法:改为 bindchooseavatar。

练习 ​

  1. 域名自查:把自己的项目从零跑一遍上传头像,故意先不配 uploadFile 域名,记录报错原文;再补齐并刷新项目设置,确认恢复。写出完整操作路径。

  2. 两种上传写法对比:用 wx.uploadFile 和 http.upload 各实现一次头像上传,对比代码行数、返回值处理、header 携带方式的差异,并说明封装库在底层替你做了哪三件事。

  3. 位置服务全链路:在 app.json 补齐位置相关声明,用 wx.chooseLocation 选点后调用 reverseGeocoder 拿到地址、search 拿到 5 条周边小区,渲染到页面;再点一次"重新选择位置"刷新数据。要求 search 的 success 中不能出现 this 指向错误。

  4. 登录拦截验证:给添加房屋页套上 authorization 组件,退出登录后进入该页,打开 Network 面板确认没有任何请求发出,再登录验证恢复正常。

  5. 列表刷新时机:把房屋列表请求从 onShow 临时改到 onLoad,走一遍"添加 → 返回",观察不刷新的现象并记录原因,再改回。


原视频对照 ​

#视频时长
1用户中心-获取头像和上传头像20:52
2用户中心-uploadFile合法域名配置04:36
3用户中心-通过http.upload实现上传头像07:29
4腾讯位置服务-注册和创建应用09:04
5腾讯位置服务-配额分配03:40
6腾讯位置服务-入门使用14:58

小结 ​

  • 小程序把普通请求和文件上传拆成两套 API、两套合法域名,配置和调用都要成对处理,缺一不可。
  • 上传接口的三个关键参数是 name(字段名)、filePath(本地临时路径)、formData(附加参数),它们替代了普通请求里的 data。
  • 原生 API 返回的是未拆包的字符串,封装库的价值就在于拆包 + 统一错误处理 + 自动带 token,能用封装就别写原生。
  • 位置能力是"三层门槛":app.json 声明 → 管理后台申请 → 第三方平台配额分配,任何一层缺失都表现为"调不通"。
  • 第三方 SDK 以独立 js 文件形式引入,JS 中只能用相对路径;官方示例多为旧语法,需要自行转换成 import / 箭头函数。
  • 页面数据的刷新时机要跟导航方式对齐:navigateTo 用 onLoad,navigateBack / switchTab 必须用 onShow。
  • 提交表单前要清洗数据,剔除 __webviewId__ 这类框架注入字段,避免后端校验失败。
  • 涉及隐私的能力(位置、相册、摄像头)在开发期最容易被缓存"坑",善用清除缓存 + openSetting 兜底。