主题
用户中心与房屋信息(二)
本页对应 用户中心与房屋信息,共 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 条后只隐藏“添加新房屋”按钮,不要误删列表数据。
课件图示








代码示例
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。
练习
域名自查:把自己的项目从零跑一遍上传头像,故意先不配 uploadFile 域名,记录报错原文;再补齐并刷新项目设置,确认恢复。写出完整操作路径。
两种上传写法对比:用
wx.uploadFile和http.upload各实现一次头像上传,对比代码行数、返回值处理、header 携带方式的差异,并说明封装库在底层替你做了哪三件事。位置服务全链路:在
app.json补齐位置相关声明,用wx.chooseLocation选点后调用reverseGeocoder拿到地址、search拿到 5 条周边小区,渲染到页面;再点一次"重新选择位置"刷新数据。要求 search 的 success 中不能出现 this 指向错误。登录拦截验证:给添加房屋页套上 authorization 组件,退出登录后进入该页,打开 Network 面板确认没有任何请求发出,再登录验证恢复正常。
列表刷新时机:把房屋列表请求从
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兜底。