主题
房屋管理(二)
本页对应 房屋管理,共 7 段视频,约 1 小时 16 分。
学习目标
- 能够调用逆地址解析方法,根据经纬度获取详细地址并渲染到页面,同时使用
wx.showLoading和wx.hideLoading控制加载提示。 - 能够使用
wx.chooseLocation实现重新定位,获取新位置的经纬度,并调用已封装方法刷新周边小区信息。 - 能够通过页面地址参数传递小区名、楼栋名和房间号,并在目标页面的
onLoad中接收、setData渲染,使用wx:for遍历数字生成楼栋和房间列表。 - 能够使用
authorization组件检测用户登录状态,并在未登录时通过空函数覆盖onLoad、onUnload阻止无效请求。 - 能够使用
behaviors引入表单验证插件,定义姓名、手机号、身份证照片的验证规则,获取表单数据并调用validate完成校验。 - 能够使用
mark属性区分身份证正反面,调用wx.chooseMedia获取图片临时路径,并通过try/catch处理用户取消选择等异常。
知识点
位置服务要解决什么问题
为什么需要:添加房屋前要先确定小区、楼栋和房间,而小区信息通常要围绕用户当前位置来查。只靠手填体验差,也不利于后续按位置展示数据。
怎么想:先拿到用户经纬度,再用第三方位置服务做“周边搜索”和“逆地址解析”,最后把结果渲染到页面。
怎么做:本项目使用腾讯位置服务。按笔记步骤注册账号、创建应用、生成 key,并到小程序管理后台添加合法域名。
易错点:key 要填自己申请的;合法域名没配置会导致请求失败;不要直接把示例 key 当成项目 key。
先声明位置权限
为什么需要:wx.getLocation、wx.chooseLocation 属于隐私相关接口,不声明、不在后台申请权限,开发时可能调不通,上线审核也会受影响。
怎么想:把“用到的位置接口”和“授权说明”提前写进 app.json,让用户知道位置信息的用途。
怎么做:在 app.json 中声明:
json
{
"requiredPrivateInfos": [
"getLocation",
"chooseLocation"
],
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于小程序位置接口的效果展示"
}
}易错点:requiredPrivateInfos 里写的接口要和实际调用一致;desc 要写清楚用途;还要按文档到小程序管理后台申请接口权限。
获取用户当前位置
为什么需要:周边搜索和逆地址解析都依赖经纬度,所以第一步是拿到用户位置。
怎么想:wx.getLocation() 支持 Promise,可以直接配合 async/await,拿到 latitude、longitude。
怎么做:
javascript
Page({
async getLocation() {
// 调用 API
const {latitude, longitude} = await wx.getLocation()
console.log(latitude, longitude)
}
})易错点:经纬度顺序是 latitude, longitude,后面拼给位置服务时不要传反;用户拒绝授权时要考虑失败分支,不能只写成功路径。
让用户选择位置
为什么需要:用户不一定在“当前定位点”附近找房,需要手动选一个地点。
怎么想:wx.chooseLocation 会打开地图,用户选好后返回新位置的经纬度和详细地址。
怎么做:
javascript
Page({
async chooseLocation() {
// 调用小程序 API 获取新的位置
const {latitude, longitude} = await wx.chooseLocation()
console.log(latitude, longitude)
},
})易错点:chooseLocation 可以不传经纬度;拿到新经纬度后要重新调用周边搜索,否则页面还显示旧数据;经纬度顺序同样不能反。
初始化腾讯位置服务 SDK
为什么需要:小程序原生 API 只负责取位置,周边搜索、逆地址解析要用腾讯位置服务 SDK。
怎么想:把官方 .js SDK 放到项目中,用个人 key 实例化一次,页面里导入这个实例即可。
怎么做:
javascript
// utils/qqmap.js
// 导入腾讯位置服务 SDK
import QQMapWX from '../libs/qqmap-wx-jssdk'
// 实例化位置服务(使用个人申请的 key)
export default new QQMapWX({
key: '填写自已的 KEY',
})易错点:路径要按项目实际目录调整;key 不要写错;实例化文件建议统一导出,避免每个页面重复 new。
搜索周边小区
为什么需要:页面要展示“附近社区”,需要按关键词和中心点搜索。
怎么想:用 QQMap.search,关键词设为“住宅小区”,中心点用经纬度逗号拼接,只保留页面需要的字段。
怎么做:
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 })
},
})
}
})易错点:location 必须是 纬度,经度 的字符串;result.data 可能为空,渲染前要有初始 points: [];方法名 getLoaction 是笔记中的拼写,实际项目建议改成 getLocation 保持一致。
逆地址解析出详细地址
为什么需要:只拿经纬度用户看不懂,需要展示“当前地点”的具体地址。
怎么想:用 reverseGeocoder 根据经纬度反查地址,结果在 result.address。转写稿中调试时也是先打印 address,确认拿到后再 setData。
怎么做:
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 })
},
})
}
})易错点:address 要在 data 中先初始化为空字符串,否则首次渲染可能看不到;转写稿中提到的“结果在 result 下面的 address”要按实际回调结构解构,不要写成 result.address 以外的不存在字段。
重新定位并刷新周边
为什么需要:用户点了“重新定位”后,应打开地图选择新位置,并刷新地址和附近小区。
怎么想:wx.chooseLocation 返回新经纬度,把它传给已经封装好的 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,否则页面不会更新。
页面渲染与 loading 优化
为什么需要:数据请求有等待时间,给用户 loading 提示体验更好;模板中还要把地址和小区列表展示出来。
怎么想:进入页面先显示 loading,在 complete 回调里关闭,因为无论成功失败都会执行。
怎么做:可在 getPoint 开始时 wx.showLoading({ title: '加载中...' }),在 complete 中 wx.hideLoading()。模板保留笔记结构:
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>易错点:loading 要在 complete 关闭,不要只写在 success;wx:key 用 id;points、address 都要有初始值。转写稿调试时也强调:数据返回慢时 loading 会多显示一会儿,返回后必须关闭。
2.2 房屋信息:先明确数据结构
为什么需要:添加房屋时,必须先确定“小区名称、楼栋号、房间号”三部分信息。真实开发中这三部分通常要由后端分别提供接口获取,但全国小区数据量大,后端短期无法完成,所以本节先用随机生成的假数据把交互和页面跑通。
怎么想:先不要纠结假数据是否真实,而是把页面跳转链路和数据传递链路搭好:小区名称从位置服务结果里带过来,楼栋号在楼栋页生成,房间号在房间页生成。
怎么做:小区名称通过 URL 参数 point 传递;楼栋页接收 point 后随机生成楼栋数;房间页接收 point 和 building 后随机生成房间号。
易错点:假数据只是课堂临时方案,正式开发要替换为接口数据;参数名要统一,point、building、room 不要写错。
楼栋信息:接收小区名并随机生成楼栋
为什么需要:用户已经选好小区,下一步要看到该小区有哪些楼栋可选。后端暂不提供楼栋接口,所以先随机生成。
怎么想:转写稿中先在 building 页的 onLoad 里解构 point,通过 console.log(point) 或 AppData 确认小区名已传过来;再用 fake 方法生成楼栋数和楼栋名称。
怎么做:保留笔记代码:
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 })
},
})易错点:size 表示“有几栋楼”,不是某一个楼栋号;Math.floor(Math.random() * 4) + 3 的结果是 3 到 6,转写稿说范围大致 3 到 7,是口述中的近似说法,以代码为准;type 大于 4 显示“号楼”,否则显示“栋”,这个 4 也是课堂随意设定的分界值。
楼栋渲染:wx:for 可直接遍历数字
为什么需要:生成 size 后要把它变成页面上的多个楼栋入口。
怎么想:转写稿调试时看到 size=6,页面就应出现 6 条楼栋数据,说明 wx:for 可以直接遍历数字。遍历单元 item 从 0 开始,所以标题里要 item + 1。
怎么做:保留笔记 WXML:
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>易错点:不要写成 item 直接显示,否则会出现“0栋”;wx:key="*this" 在这里沿用笔记;跳转房间页时还要继续把小区名和楼栋名带过去,否则房间页拿不到 point 和 building。
房间信息:先接参,再分别生成楼层和房号
为什么需要:选定楼栋后要展示该楼栋下的房间号。房间号有规律,例如 201 表示二层第 01 个房间,202 表示二层第 02 个房间,所以可以拆成“楼层号 + 具体房号”分别生成再拼接。
怎么想:转写稿中先在 room 页 onLoad({ point, building }) 接收两个参数并打印确认;再在 fake 中先生成房间数量 size,循环生成楼层号和具体房号,拼成完整房间号后放入 rooms 数组,最后 setData 渲染。
怎么做:保留笔记代码:
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 })
},
})易错点:rooms.includes(room) 用于判断重复;笔记代码中重复时直接 return,会结束整个 fake,可能导致房间数量少于 size,课堂理解逻辑即可,正式代码可考虑跳过本次或重新生成;Math.random() * 19 实际楼层范围以代码为准,注释“1~20”是课堂说明,调试时以 AppData 中 rooms 为准;[floor, 0, No].join('') 中 0 是字符串拼接用的占位,不是数值 0。
房间渲染:把小区、楼栋、房间号拼成标题
为什么需要:生成 rooms 后要展示给用户选择,并且标题要能看出“哪个小区、哪栋、哪个房间”。
怎么想:转写稿中在 AppData 里确认 point、building、rooms 都有值后,再到 WXML 中用 wx:for="{{rooms}}" 遍历,标题由 point + building + item 拼成。
怎么做:保留笔记 WXML:
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>易错点:rooms 是数组,遍历单元是 item;point 和 building 要在 setData 中一并渲染,否则标题只剩房间号;跳转表单页时还要继续传递房屋信息,为后续表单提交做准备。到此房屋信息的三部分就处理完毕,接下来再补充业主信息即可完成房屋添加功能。
先明确本节要完成什么
“添加房屋”不是只写一个表单,而是把一串流程串起来:未登录不能进入、进入后拿到房屋位置参数、填写业主信息、上传身份证正反面、校验表单、提交接口,最后回到房屋列表。
为什么需要:后端新增房屋接口要求登录,且表单数据不合法时不应提交。
怎么想:先拦登录,再取数据,再上传图片,最后统一验证并提交。
怎么做:用 authorization 组件检测登录,用地址参数接收 point/building/room,用 model:value 收表单值,用 wx.chooseMedia + wx.uploadFile 上传图片,用 wechat-validate 验证。
易错点:未登录时页面仍可能在 onLoad/onShow 发请求;图片上传返回的是字符串要 JSON.parse;提交前要剔除多余字段。
检测登录:未登录时不要发出页面请求
为什么需要:用户未登录时,即使马上跳登录页,原页面的 onLoad 或 onShow 仍可能先发请求,这些请求没有意义。
怎么想:authorization 组件在 attached 里判断 getApp().token,未登录就跳登录页,同时把当前页面实例的 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) {
currentPage.onLoad = () => {}
currentPage.onShow = () => {}
wx.redirectTo({
url: '/pages/login/index?redirectURL=/' + currentPage.route,
})
}
},
},
})易错点:要在跳转前覆盖生命周期;覆盖的是“当前页面实例”,不是全局 Page;token 为空字符串时 !! 才是 false。转写稿调试时清空 storage 后重新编译,能直接看到跳登录页;再观察 Network,未登录进入选择社区页时不再发位置请求。
接收房屋信息与表单数据
为什么需要:房屋的小区、楼栋、房间号来自上一步地址参数;业主姓名、性别、手机号来自表单。
怎么想:地址参数在 onLoad 解构接收;表单值用 model:value 双向绑定。
怎么做:
javascript
// house_pkg/pages/form/index.js
Page({
data: {
point: '',
building: '',
room: '',
name: '',
gender: 1,
mobile: '',
idcardFrontUrl: '',
idcardBackUrl: '',
},
onLoad({ point, building, room }) {
this.setData({ point, building, room })
},
})模板中姓名、手机号用 model:value;性别可用 radio-group 的 model:value。
易错点:转写稿实测 radio-group 的 model:value 可用,但文档未明确说明;若遇到兼容问题,应改用 change 事件取值。调试时可用编译模式设置启动参数,避免每次手动选择。
上传身份证照片:先区分正反面,再上传
为什么需要:身份证分国徽面和照片面,要分别存入 idcardFrontUrl、idcardBackUrl。
怎么想:按钮上用 mark 标记 type,事件里通过 ev.mark.type 区分;用 wx.chooseMedia 选图,再用 wx.uploadFile 上传。
怎么做:
javascript
async uploadPicture(ev) {
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)
}
}易错点:chooseMedia 取消选择会进入 catch,用 try/catch 可避免未处理错误;uploadFile 的 data 是字符串,必须 JSON.parse;header 要带 Bearer;type 要与 data 字段名对应。
表单验证与提交
为什么需要:姓名、手机号、身份证图片不合法时不能提交。
怎么想:用 wechat-validate 的 behaviors 注册,rules 写规则,提交时 this.validate() 统一校验。
怎么做:
javascript
import wxValidate from 'wechat-validate'
Page({
behaviors: [wxValidate],
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
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 })
},
})易错点:rules 字段名必须和 data 一致;手机号正则是 11 位;验证不通过时不要继续请求;提交数据要剔除 __webviewId__ 等多余字段;navigateBack 的 delta: 4 表示返回四步,调试时要确认页面栈层级。接口文档见:接口文档地址。
先明确目标与登录前提
本节要完成“房屋列表”:用户成功添加房屋后,在列表页查询并展示当前用户的全部房屋数据。接口文档地址保留如下:
为什么需要:房屋数据属于用户私有数据,后端要求必须登录后才能查询;未登录时发请求没有意义,还会产生无效请求。
怎么想:沿用转写稿中“检测登录”的思路,用 authorization 组件包住页面,让它作为根节点负责登录检测与跳转。转写稿还强调:未登录时 onLoad、onShow 里的请求没有意义,可通过空函数覆盖生命周期来阻止无效请求。列表页若出现未登录仍触发 onShow 请求,就按这个思路检查,而不是在 getHouseList 里堆登录判断。
怎么做:模板最外层使用 <authorization>,列表内容放在其内部。
易错点:不要把 authorization 放到列表内容里面;不要自己再写一套重复登录判断,避免和组件逻辑冲突。
为什么列表请求要放在 onShow
为什么需要:添加房屋成功后通过 wx.navigateBack 返回列表页,此时页面不会重新 onLoad,但会触发 onShow。
怎么想:转写稿明确提到返回时触发的是 onShow 生命周期,因此列表刷新必须放在 onShow,否则新增房屋后列表可能还是旧数据。
怎么做:在 onShow 中调用 this.getHouseList()。
易错点:只写在 onLoad 中,首次进入能显示,但添加后返回不刷新;也不要在 onShow 里重复绑定事件。
参考示例代码保留如下:
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,
})
},
// ...
})接口调用与成功码判断
为什么需要:接口可能失败,不能直接把返回值当列表渲染。
怎么想:先解构 code 和 data,只有 code === 10000 才认为成功;失败时用统一 toast 提示并提前返回。
怎么做:使用 await wx.http.get('/room'),成功后 setData 渲染 houseList,同时根据 houseList.length === 0 设置 isEmpty。
易错点:把 data 解构成 houseList 后仍写 data.houseList;忘记 return 导致失败后继续渲染;空数组判断写成 houseList.length。
渲染列表、状态标签与空状态
为什么需要:用户要看到房屋位置、房间号、业主和审核状态;没有数据时要给出明确引导。
怎么想:用 wx:if="{{!isEmpty}}" 展示列表,否则展示空状态;审核状态用 status 分别渲染“正在审核 / 审核通过 / 审核失败”。
怎么做:按笔记模板保留 van-swipe-cell、van-cell-group、van-cell 结构,状态用三个 wx:if 判断。
易错点:wx:key 要用唯一 id;mark:id 与 mark:index 不要写错;空状态链接要指向添加房屋页面。
模板代码保留如下:
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>最多 5 条与添加按钮限制
为什么需要:业务限制列表最多 5 条,超过后不再展示“添加新房屋”。
怎么想:按钮显示条件用 houseList.length < 5,不要用 <= 5。
怎么做:在工具栏 wx:if 中判断长度小于 5。
易错点:空状态和按钮同时出现;长度判断边界写错;isEmpty 没有随列表更新。
课件图示








代码示例
示例一:位置服务(获取位置、逆地址解析、搜索周边小区、重新定位)
js
// utils/qqmap.js
import QQMapWX from '../libs/qqmap-wx-jssdk'
// 实例化位置服务(使用个人申请的 key)
export default new QQMapWX({
key: '填写自已的 KEY',
})js
// 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) {
// 显示 loading
wx.showLoading({ title: '正在加载...' })
// 逆地址解析(根据经纬度获取具体地址)
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 })
},
complete: () => {
// 成功或失败都会执行,关闭 loading
wx.hideLoading()
},
})
},
// 重新选择位置
async chooseLocation() {
const { latitude, longitude } = await wx.chooseLocation()
this.getPoint(latitude, longitude)
},
})xml
<!-- house_pkg/pages/locate/index.wxml -->
<authorization>
<view class="locate">
<van-cell-group border="{{false}}" title="当前地点">
<van-cell title="{{address}}" bind:tap="chooseLocation" is-link />
</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?point={{item.title}}"
is-link
/>
</van-cell-group>
</view>
</authorization>要点解释:
app.json中必须先声明requiredPrivateInfos: ["getLocation", "chooseLocation"]与permission.scope.userLocation,并到小程序管理后台申请权限,否则上线审核无法通过。wx.getLocation/wx.chooseLocation都支持返回 Promise,因此可直接await解构出latitude, longitude。reverseGeocoder的结果地址在result.address中,代码用({ result: { address } }) => {}解构;search返回的数据需map过滤成{ id, title, _distance }。- 经纬度拼成字符串时必须用逗号并且顺序是纬度在前、经度在后,传反了会出错。
loading放在complete回调里关闭,保证成功失败都会结束等待。- 跳转到楼栋页时用问号拼参数
?point=xxx,小区名就是这样传到下一页面。
示例二:房屋信息(楼栋号与房间号随机生成)
js
// 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
<!-- house_pkg/pages/building/index.wxml -->
<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?point={{point}}&building={{point}}{{item + 1}}{{type}}"
is-link
/>
</van-cell-group>
</view>js
// house_pkg/pages/room/index.js
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
<!-- house_pkg/pages/room/index.wxml -->
<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?point={{point}}&building={{building}}&room={{item}}"
is-link
/>
</van-cell-group>
</view>要点解释:
wx:for="{{size}}"可以直接遍历一个数字,size是 6 就生成 6 个楼栋;item从 0 开始,所以标题里用{{item + 1}}让楼栋从 1 开始。size > 4 ? '号楼' : '栋'是随意的假数据规则,只为上课演示,真实开发需调接口。- 房间号由“楼层 + 0 + 房间序号”拼接:
[floor, 0, No].join(''),如202表示 2 层第 02 个房间。 - 易错点:去重处
if (rooms.includes(room)) return会直接结束整个fake函数,导致setData不执行、页面空白,更合理的写法是continue。 - 多参数跳转用
&拼接:?point=xx&building=xx&room=xx,接收端onLoad({ point, building, room })解构即可。
示例三:添加房屋(上传身份证照片、表单验证、提交)
js
// 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) {
// 区分用户上传的是正面或反面
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)
}
},
// 提交审核
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 })
},
})xml
<!-- house_pkg/pages/form/index.wxml(关键部分) -->
<authorization>
<van-field label="业主姓名" model:value="{{name}}" placeholder="请填写业主姓名" />
<van-radio-group model:value="{{gender}}">
<van-radio name="{{1}}">男</van-radio>
<van-radio name="{{2}}">女</van-radio>
</van-radio-group>
<van-field label="业主手机号" model:value="{{mobile}}" placeholder="请填写手机号" />
<view class="idcard">
<image wx:if="{{idcardFrontUrl}}" src="{{idcardFrontUrl}}" />
<view wx:else bind:tap="uploadPicture" mark:type="idcardFrontUrl">上传国徽面</view>
<image wx:if="{{idcardBackUrl}}" src="{{idcardBackUrl}}" />
<view wx:else bind:tap="uploadPicture" mark:type="idcardBackUrl">上传照片面</view>
</view>
<van-button type="primary" bind:tap="submitForm">提交审核</van-button>
</authorization>要点解释:
- 页面用
authorization组件做根节点检测登录;该组件在未登录时会用空白函数覆盖onLoad/onShow,从而阻止页面在未登录状态下发出请求。 ev.mark.type来自 WXML 的mark:type="idcardFrontUrl",正反面共用同一个uploadPicture,靠这个值区分,最后用this.setData({ [type]: data.data.url })动态写回对应字段。wx.chooseMedia的mediaType: ['image']、sizeType: ['compressed']、count: 1分别控制类型、压缩、数量;用户点取消会抛错,所以用try...catch包住。wx.uploadFile的filePath取media.tempFiles[0].tempFilePath,name必须是后端约定的file,请求头带Bearertoken;返回的result.data是字符串,要JSON.parse。rules里name用正则/^[\u4e00-\u9fa5]{2,5}$/限制中文,mobile用/^1[3-8]\d{9}$/;gender默认值为 1(男),已有值故未写校验规则。- 提交前
if (!this.validate()) return校验全部数据;const { __webviewId__, status, ...data } = this.data剔除多余参数后再post('/room', data),成功后wx.navigateBack({ delta: 4 })退回房屋列表。 model:value用于van-field取值;van-radio-group文档未说明支持model:value,实测可用,若出问题需改为监听change事件取值。
常见错误
位置接口直接 fail,或提审被驳回
- 现象:真机报
getLocation:fail auth deny,代码逻辑本身没问题,但审核不通过。 - 原因:只在 JS 里调了 API,
app.json的requiredPrivateInfos与permission没写全,后台"接口权限"也没申请。 - 修法:
app.json里getLocation、chooseLocation都要列进requiredPrivateInfos并配scope.userLocation的desc,后台申请对应接口权限,两处都做完再测。
- 现象:真机报
await wx.chooseMedia()拿到undefined,或用户点取消后整段逻辑中断- 现象:选完图取不到
tempFiles;点"取消"控制台抛红。 - 原因:没注意该 API 的 Promise 化差异;用户取消时 Promise 走 reject 而不是返回空结果。
- 修法:用
try/catch包裹,成功分支取media.tempFiles[0].tempFilePath,catch里静默或 toast,不要让它冒泡打断后续代码。
- 现象:选完图取不到
未登录时页面仍然发出了位置服务和小区搜索请求
- 现象:清掉 token 进入 locate 页面,Network 里照样有 qqmap 的请求。
- 原因:
authorization的attached里覆盖生命周期的对象不对(没拿到当前页面实例),或只覆盖了onLoad没覆盖onShow。 - 修法:用
getCurrentPages().pop()取当前页面实例再赋空函数,onLoad和onShow都要覆盖;新增的请求也不要写进attached本身。
生成房间号时条数比
size少- 现象:
size是 5,页面只渲染出 3 条房间。 - 原因:循环里检测到重复房间号时写了
return,直接把整个fake函数结束了。 - 修法:改成
continue,只跳过当前这一次循环。
- 现象:
楼栋从"0栋"开始
- 现象:显示 0栋、1栋、2栋……且总条数看起来多了一条。
- 原因:
wx:for遍历数字时item从 0 起。 - 修法:模板里写
{{item + 1}},或在生成数据时就产出从 1 开始的数组。
上传身份证后图片不显示,提交时一直提示未上传
- 现象:
setData执行了,image还是空;验证反复报"请上传身份证国徽面"。 - 原因:
ev.mark.type的值和data里的字段名对不上(写成frontUrl而字段是idcardFrontUrl),动态 key 设到了不存在的属性上;或uploadFile的success里没判断code就取了data.data.url。 - 修法:
mark的值直接写成data里的字段名,用[type]计算属性赋值;success里先判code === 10000再setData,并确认带了Authorization头。
- 现象:
添加房屋成功返回后列表不刷新
- 现象:
navigateBack回来,列表还是旧数据。 - 原因:
getHouseList写在了onLoad。 - 修法:挪到
onShow。
- 现象:
提交表单被后端拒绝(参数多余或缺失)
- 现象:接口返回参数校验错误,或把脏字段一起存进了库。
- 原因:把
this.data整个丢给接口,带上了__webviewId__这类框架字段;或validate()没通过也继续提交了。 - 修法:
if (!this.validate()) return先拦住,再解构剔除多余字段后提交。
练习
给位置页补 loading 在
getPoint开始处wx.showLoading,在search与reverseGeocoder都结束之后wx.hideLoading。 验收:两个请求都返回后 loading 才消失,不是任意一个回来就关。把回调改成 Promise 并并发 把
QQMap.search和QQMap.reverseGeocoder各自包一层 Promise,用Promise.all并发执行,统一在.finally里关 loading。 验收:代码中不再出现嵌套 success,address和points一次setData完成。重写房间号生成 用
while (rooms.length < size)的方式重写fake,保证一定生成size条且无重复。 验收:连续刷新 20 次,每次渲染条数都等于size。验证登录拦截是否彻底 清空 Storage 的 token 后进入 locate 页,打开 Network 面板确认没有位置服务请求;再在页面里临时加一个
onShow请求,验证它同样被拦住。 验收:两种生命周期下的请求都不发出。补全滑动删除 给
van-swipe-cell的bind:close回调补上:取mark:id→ 调删除接口 → 成功后就地更新houseList与isEmpty→ 失败则回弹。 验收:删除最后一条后页面自动切到空状态提示,不需要手动刷新。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 享+生活-添加房屋(位置服务)-5 | 06:12 |
| 2 | 享+生活-添加房屋(位置服务)-6 | 06:41 |
| 3 | 享+生活-添加房屋(房屋信息)-1 | 14:02 |
| 4 | 享+生活-添加房屋(房屋信息)-2 | 13:39 |
| 5 | 享+生活-添加房屋(检测登录) | 08:44 |
| 6 | 享+生活-添加房屋(表单验证) | 18:14 |
| 7 | 享+生活-添加房屋(上传图片)-1 | 09:12 |
小结
- 位置类 API 能否用,取决于"
app.json声明 + 后台申请"两件事同时成立,缺任何一件在真机和审核环节都会暴露。 wx.getLocation给的是当前点,wx.chooseLocation给的是用户选的点,两者返回结构一致,都可以直接喂给同一个getPoint——差别只在交互,不在数据。- 经纬度在 SDK 里是以
latitude,longitude逗号拼接的字符串传的,顺序写反不会报错,只会静默返回错数据,属于最难查的一类问题。 - 用假数据占位的模块,必须把"假"隔离在一个函数里,接口到位时只替换这一个函数,模板层零改动。
authorization覆盖生命周期是"防无效请求"的手段而不是目的,判断标准很简单:未登录时 Network 里不该有任何业务请求。- 页面数据的刷新时机由导航方式决定:
navigateTo进去的看onLoad,navigateBack回来的必须看onShow。 - 表单提交前统一做两件事——先
validate()拦一道,再剔除框架注入的冗余字段,能挡掉绝大多数后端 400。 - 动态 key(
[type]: url)是这类"一图两用"场景的标准解法,前提是mark的值与data字段名严格一致。