主题
房屋管理与报修(一)
本页对应 房屋管理与报修,共 7 段视频,约 1 小时 15 分。
学习目标
- 能使用
git log、git checkout <提交号>或 VS Code GitLens 将本地代码切换到指定提交,并验证当前提交记录与目标版本一致。 - 能为需登录的小程序页面接入 auth 鉴权,并在未登录时重写
onLoad、onShow、onReady等生命周期,验证页面跳转登录后不再执行定位、搜索等原业务请求。 - 能依据接口文档在
data中声明添加房屋所需字段,并使用model:value双向绑定收集姓名、手机号、性别等表单数据,通过调试器验证字段值实时同步。 - 能配置
wechat-validate校验规则并在提交时调用validate校验姓名、手机号等字段,验证非法输入被拦截、合法输入通过。 - 能调用
chooseMedia选择身份证图片,使用封装后的upload按接口要求上传并将返回 URL 更新到页面;能使用catchtap阻止子元素事件冒泡,验证删除图片时不再触发上传。 - 能在上传前读取临时文件
size,按约定阈值判断并提示,验证超过大小限制的图片不会发起上传请求。
知识点
位置服务与腾讯位置服务准备
位置服务(LBS)要基于用户位置提供“附近小区、当前地址”等能力,单靠小程序自身不够,需要配合腾讯地图、高德地图、百度地图等第三方服务。本项目采用腾讯位置服务。为什么需要先申请?因为后续 SDK 调用要绑定 key。怎么想?把“账号—应用—key—合法域名”看成一条准备链,缺一环都会调不通。怎么做?按笔记步骤:注册账号、创建应用、生成 key、在小程序管理后台添加合法域名;具体操作参考视频和官方文档。易错点:不要跳过合法域名配置;key 要填自己申请的那一个,不要照抄示例。
声明位置权限
使用获取位置 API 前,必须先在 app.json 中声明,并在小程序管理后台申请,限制参考官方文档。为什么需要?因为位置属于敏感能力,不声明就不允许调用。怎么想?把“隐私声明”和“接口调用”分开:先声明,再调用。怎么做?在 app.json 中加入:
json
{
"requiredPrivateInfos": [
"getLocation",
"chooseLocation"
],
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于小程序位置接口的效果展示"
}
}易错点:只写代码不在后台申请,或 desc 写得与实际用途不符,都会影响审核与调用。
获取用户位置
后续搜索周边、逆地址解析都需要经纬度。怎么想?先拿 latitude、longitude,再交给 SDK。怎么做?wx.getLocation() 支持 Promise:
javascript
Page({
async getLocation() {
// 调用 API
const {latitude, longitude} = await wx.getLocation()
console.log(latitude, longitude)
}
})易错点:使用 await 的方法必须声明 async;笔记后面出现 getLoaction 拼写,实际应统一为 getLocation,否则 onLoad 调不到。
选择位置
用户可能觉得定位不准,需要手动选点。wx.chooseLocation 会打开地图,由用户选择后返回经纬度,且支持 Promise:
javascript
Page({
async chooseLocation() {
// 调用小程序 API 获取新的位置
const {latitude, longitude} = await wx.chooseLocation()
console.log(latitude, longitude)
},
})易错点:这里同样要 async 才能 await;选择新位置后要重新搜索周边和逆地址解析,不能只打印经纬度。
导入 SDK 并实例化
为什么要导入 SDK?因为搜索周边、逆地址解析等方法由腾讯位置服务封装。怎么想?把 key 集中放在 utils/qqmap.js,页面只导入实例。怎么做?导入官方 .js SDK 并实例化:
javascript
// utils/qqmap.js
// 导入腾讯位置服务 SDK
import QQMapWX from '../libs/qqmap-wx-jssdk'
// 实例化位置服务(使用个人申请的 key)
export default new QQMapWX({
key: '填写自已的 KEY',
})易错点:SDK 路径要对;key 不要写成示例值;页面导入路径要与项目目录一致。
搜索周边小区并渲染
为什么需要 search?因为要把经纬度变成“附近社区”列表。怎么想?先 getLocation,再把经纬度传给 getPoint,用 search 搜索关键词“住宅小区”,并过滤出 id、title、_distance。怎么做?合并笔记代码:
javascript
// 导入腾讯位置服务
import QQMap from '../../../utils/qqmap'
Page({
onLoad() {
// 获取用户经纬度
this.getLocation()
},
async getLocation() {
// 调用小程序 API 获取用户位置
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 })
},
})
// 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 })
},
})
},
async chooseLocation() {
// 调用小程序 API 获取新的位置
const { latitude, longitude } = await wx.chooseLocation()
// 获取新的位置附近的小区
this.getPoint(latitude, longitude)
},
})页面渲染:
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>易错点:location 要用逗号拼成字符串;wx:key 用 id;转写稿提到页面跳登录后,onLoad 仍可能执行并发出腾讯地图搜索请求,消耗 key 调用次数,因此要在鉴权组件未登录时重写 onLoad、onShow、onReady 等生命周期,避免不必要的搜索调用。
房屋信息为什么拆成三段
为什么需要:完整的房屋信息由小区名称、楼栋号、房间号三部分组成。真实开发中,这三部分通常需要后端分别提供接口获取;但全国小区数据量很大,后端短期难以整理完成,所以当前阶段只能临时随机生成数据,先把添加房屋的流程跑通。
怎么想:先保证页面流程完整:位置服务拿到小区名称 → 进入楼栋选择 → 进入房间选择 → 再补充业主信息。假数据只是过渡方案,字段和页面结构要尽量贴近真实接口,后续替换接口时少改代码。
怎么做:小区名称通过位置服务已经获取到,将它拼到 URL 上;楼栋和房间先用随机函数生成。进入楼栋页时通过 onLoad({ point }) 接收地址参数,再调用 fake 生成楼栋数据。
易错点:假数据不能当成正式接口逻辑;point 为空时要先检查上一页是否传参;未登录时页面跳转到登录页后,页面 JS 的 onLoad 仍可能执行。转写稿中调试到:虽然页面已跳到登录页,但仍向腾讯地图发送了搜索请求,浪费调用次数。解决思路是在鉴权组件中重写页面生命周期,把未登录时无需执行的 onLoad、onShow、onReady 等覆盖为空函数,避免不必要的业务执行。
楼栋信息:从小区名称生成楼栋列表
为什么需要:用户选完小区后,需要继续选择楼栋;没有楼栋数据,房间选择就无法继续。
怎么想:楼栋数量不需要特别设计,用随机函数生成即可;楼栋名称根据数量决定用“栋”还是“号楼”。这段代码只用于课堂演示,能看懂逻辑即可。
怎么做:
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 })
},
})易错点:Math.random() 的边界要算清楚,Math.floor(Math.random() * 4) + 3 得到的是 3~6;size > 4 时用“号楼”,否则用“栋”。point 要随 setData 保存,否则渲染和后续跳转拿不到小区名称。
楼栋列表渲染
为什么需要:生成楼栋数据后,要渲染成可点击的列表,用户才能进入房间页。
怎么想:用 van-cell 循环渲染,标题拼接“小区名称 + 楼栋号 + 栋/号楼”,点击后跳转房间页。
怎么做:
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>易错点:wx:for 传数字时的 item 取值规则要以当前基础库和文档为准,调试时先确认渲染出的楼栋号是否从 1 开始,避免 item + 1 造成偏移。另一个关键点是:房间页 onLoad 需要 point 和 building,如果 url 没有带参数,房间页会拿不到小区和楼栋信息,需要检查跳转参数是否完整。
房间信息:随机生成房间号
为什么需要:选完楼栋后还要选具体房间,房间号是房屋信息最后一段。
怎么想:随机生成若干房间,楼层号和房号拼成房间号;生成后要检测重复,避免列表出现相同房间。
怎么做:
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 })
},
})易错点:注释写“1~20”,但 Math.floor(Math.random() * 19) + 1 实际只到 19;注释写“1~3”,但 Math.floor(Math.random() * 2) + 1 实际只到 2。若要覆盖注释范围,应分别用 * 20 和 * 3。重复房间号时这里用了 return,会直接结束整个 fake,导致本次生成提前停止、房间数量不足;更合理的调试思路是改成 continue 或使用 while 重新生成,直到数量满足。
房间列表渲染
为什么需要:房间数据生成后要展示给用户选择,点击后进入表单页补充业主信息。
怎么想:标题拼接“小区 + 楼栋 + 房间号”,点击跳转到表单页。
怎么做:
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>易错点:跳转表单页时同样要确认是否把 point、building、room 等参数带过去;wx:key="*this" 适用于当前简单字符串数组,但房间号若可能重复,应先解决重复问题。到此房屋信息的三段数据就处理完毕,接下来再补充业主信息,才能完成房屋添加功能;后续表单字段要按接口文档命名,必传字段不能少,性别等字段的值也要按后端要求传。
登录态与请求屏蔽
为什么需要:未登录用户不应添加房屋,否则不知道房屋归到谁名下;而且未登录时页面里的定位、搜索等请求仍会消耗接口次数。
怎么想:先用 authorization 组件检测登录态;未登录不仅跳登录页,还要阻止页面 JS 里的 onLoad、onShow 自动执行业务。转写稿调试时发现:虽然已经跳到登录页,但腾讯地图搜索请求仍发出,原因是页面结构被加载后,JS 生命周期先执行了。
怎么做:在组件 attached 中取 getApp().token,通过 getCurrentPages() 取页面栈并 pop() 当前页,未登录时用空函数覆盖当前页的 onLoad、onShow,再 redirectTo 登录页。
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,
})
}
},
},
})易错点:getCurrentPages() 要取当前页;覆盖生命周期要在跳转前完成;不要只做跳转而忽略原页面请求。
收集房屋与业主信息
为什么需要:提交接口要求字段名与后端一致,缺字段或字段类型不对都会失败。
怎么想:房屋 point/building/room 来自地址参数;业主姓名、手机号、性别用 model:value 双向绑定。model:value 只适合简单绑定,所以字段要放在 data 一级。
怎么做:
javascript
// house_pkg/pages/form/index.js
Page({
onLoad({ point, building, room }) {
this.setData({ point, building, room })
},
})性别按接口要求用数字,默认 gender: 1;手机号输入可加 maxlength。
易错点:字段名不要自行命名;性别不要写成字符串;不要把绑定字段套在对象里。
上传身份证图片
为什么需要:身份证正反面要上传后把返回 URL 写入表单。
怎么想:用 wx.chooseMedia 选图,再用 wx.uploadFile 上传;用 ev.mark.type 区分正反面。转写稿中调试确认路径在 media.tempFiles[0].tempFilePath,上传字段名按接口为 file。
怎么做:
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)
}
}转写稿中还补了大小限制:从 tempFiles[0].size 判断,超限直接 toast 并退出;课堂测试先按 1M 限制,接口说明为 2M,具体以后端确认为准。
易错点:取消选图会进入 catch;不要取错临时路径;上传成功要判断 code === 10000。
表单校验
为什么需要:提交前必须拦截空姓名、错误手机号、未上传身份证等情况。
怎么想:继续用 wechat-validate,规则与 data 同级。
怎么做:
javascript
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: '请上传身份证照片面!' }],
},
})易错点:rules 不要写进 data;性别有默认值可不校验;提交时先 if (!this.validate()) return。
提交与返回列表
为什么需要:把全部表单数据提交给后端,成功后回到房屋列表。
怎么想:剔除页面内部字段,避免多余参数;按接口文档提交。
怎么做:
javascript
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 })
}接口文档地址:https://apifox.com/apidoc/shared-8d66c345-7a9a-4844-9a5a-1201852f6faa/api-41400753
易错点:delta 是返回层数;不要把 __webviewId__ 等内部字段提交。
事件冒泡补充
转写稿中点击删除图片的叉,却再次触发上传区选择图片,原因是父子元素事件嵌套发生冒泡。小程序没有 .stop 修饰符,把 bindtap 改为 catchtap 即可阻止冒泡。易错点:上传区用 bindtap 默认冒泡;删除按钮要用 catchtap。
2.4 房屋列表:先明确目标与登录前提
本节目标是:添加房屋成功后,在房屋列表页查询并展示当前用户的房屋数据,且用户必须处于登录状态。接口文档地址保留如下:接口文档地址。
为什么需要:房屋数据属于用户私有数据,未登录时既不知道“查谁的房屋”,也不应该发起列表请求。
怎么想:列表页外层用 <authorization> 包裹,未登录时跳转登录;同时要注意,不能只靠 WXML 不渲染来阻止 JS 业务。转写稿中调试过类似问题:清空用户信息后直达页面会跳登录,但页面里的网络请求仍然发出,原因是页面 JS 的生命周期仍会执行。解决办法是在鉴权组件未登录时重写页面生命周期,避免不必要的接口调用。
怎么做:列表页继续使用鉴权组件;列表数据通过 wx.http.get('/room') 获取;接口失败统一 toast。
易错点:以为 <authorization> 包住结构就不会请求;忘记未登录时页面生命周期仍可能执行;接口返回未判断 code。
在 onShow 中获取房屋列表
为什么需要:添加房屋页提交成功后通过 wx.navigateBack 返回列表页,此时列表页不会重新触发 onLoad,但会触发 onShow。
怎么想:凡是“从子页面返回后需要刷新”的列表,应放在 onShow 中拉取。
怎么做:在 onShow 调用 getHouseList;用 async/await 调接口;判断 code !== 10000 后 toast;把 houseList 和 isEmpty 一起 setData。
易错点:写在 onLoad 导致返回后列表不更新;忘记初始化 houseList: [];isEmpty 与数组长度不一致。
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,
})
},
// ...
})渲染列表与审核状态
为什么需要:列表要展示房屋地址、房间号、业主和审核状态,让用户快速识别房屋信息。
怎么想:用 wx:for 渲染,wx:key="id";status 为 1/2/3 时分别显示“正在审核”“审核通过”“审核失败”。
怎么做:使用 van-swipe-cell 提供左滑删除结构,内部用 van-cell-group 和 van-cell 展示字段;点击单元格绑定 goDetail。
易错点:wx:key 用 index;状态判断漏写;滑动删除的 mark:id 与事件参数对应不上。
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 条,超过后不再展示“添加新房屋”。
怎么想:用 isEmpty 控制空状态;用 houseList.length < 5 控制添加按钮。
怎么做:isEmpty 在接口成功后由 houseList.length === 0 计算;空状态用 wx:else 展示提示和 navigator;非空且不足 5 条时显示 toolbar。
易错点:手动改 isEmpty 导致不同步;空状态仍显示添加按钮;长度判断写成 <= 5 或 > 5。
列表跳详情:把房屋 ID 带进 URL
接口文档地址:
https://apifox.com/apidoc/shared-8d66c345-7a9a-4844-9a5a-1201852f6faa/api-41400751
为什么需要: 详情页不能凭空知道用户点的是哪一套房,必须把列表项对应的房屋 ID 传过去,详情接口才能按 ID 查询。
怎么想: 列表页只负责“带参数跳转”,详情页负责“取参数、调接口、渲染”。跳转时把 ID 拼在 URL 后面,详情页在 onLoad 里接收。
怎么做: 在列表项上通过 mark:id 传数据,点击事件里用 ev.mark.id 取 ID,再拼到 url 上。笔记代码如下:
javascript
// house_pkg/pages/list/index.js
Page({
// ...
goDetail(ev) {
// 通过 mark:id 来传递数据
wx.navigateTo({
url: '/house_pkg/pages/detail/index?id=' + ev.mark.id,
})
},
})易错点: ev.mark.id 依赖列表项上的 mark:id,如果模板里没写或写错,取到的是 undefined;URL 里 ?id= 不能漏;wx.navigateTo 只能跳非 tabBar 页面。
详情页取 ID 并调用详情接口
为什么需要: 详情页打开后要立刻拿到 ID 请求后端,否则页面没有真实数据可渲染。
怎么想: onLoad 是页面加载时接收参数的好位置;接口成功要判断业务状态码,失败要提示;成功后用 setData 更新页面。
怎么做: 在 onLoad({ id }) 中拿到 ID,调用 getHouseDetail(id);方法内先判断 ID 是否存在,再请求 /room/ + id,判断 code !== 10000,最后把返回的 houseDetail 放进 data。笔记代码如下:
javascript
// house_pkg/pages/detail/index.js
Page({
data: {
houseDetail: {}
},
onLoad({id}) {
// 获取房屋详情
this.getHouseDetail(id)
},
// 房屋详情接口
async getHouseDetail(id) {
if (!id) return wx.utils.toast('参数有误!')
// 调用接口
const { code, data: houseDetail } = await wx.http.get('/room/' + id)
// 检测接口是否调用成功
if (code !== 10000) return wx.utils.toast()
// 渲染数据
this.setData({ houseDetail })
},
})易错点: 不要只看 HTTP 请求成功,还要看业务 code;data: houseDetail 是解构重命名,别把整个响应体直接塞给页面;setData 后要确认字段名和 WXML 一致。
渲染详情:房屋信息、业主信息与状态标签
为什么需要: 接口数据只有落到页面上,用户才能看到房屋状态、房间号、业主、手机号和身份证照片。
怎么想: 按接口字段分组展示;状态是数字,需要在模板里映射成文字和样式类名;图片字段直接绑定 src。
怎么做: 用 scroll-view 承载内容,用 van-cell-group/van-cell 展示信息。状态对应关系是:1 正在审核,类名 info;2 审核通过,类名 success;3 审核失败,类名 fail。笔记代码如下:
xml
<scroll-view scroll-y enhanced show-scrollbar="{{false}}">
<view class="house-detail">
<van-cell-group border="{{false}}" title="房屋信息">
<van-cell title="{{houseDetail.point}}">
<text wx:if="{{houseDetail.status === 1}}" class="tag info">正在审核</text>
<text wx:if="{{houseDetail.status === 2}}" class="tag success">审核通过</text>
<text wx:if="{{houseDetail.status === 3}}" class="tag fail">审核失败</text>
</van-cell>
</van-cell-group>
<van-cell-group title="业主信息" border="{{false}}">
<van-cell
title-width="200rpx"
title="房间号"
value="{{houseDetail.building}}{{houseDetail.room}}"
/>
<van-cell title-width="200rpx" title="业主" value="{{houseDetail.name}}" />
<van-cell
title-width="200rpx"
border="{{false}}"
title="手机号"
value="{{houseDetail.mobile}}"
/>
</van-cell-group>
<view class="id-card">
<van-cell title="本人身份证照片" />
<view class="id-card-front">
<image src="{{houseDetail.idcardFrontUrl}}" />
</view>
<view class="id-card-back">
<image src="{{houseDetail.idcardBackUrl}}" />
</view>
</view>
</view>
</scroll-view>
<view class="toolbar" bind:tap="editHouse">
<text class="enjoy-icon icon-edit"></text>
<text class="button-text">修改房屋信息</text>
</view>易错点: 状态判断要用 === 和数字比较,不要写成字符串;三个 wx:if 要覆盖 1/2/3;房间号由 building 和 room 拼接,别只绑一个;身份证图片字段名不要写错。底部 editHouse 在笔记中只出现在模板里,方法未给出,不要自行编造实现。
调试过程与易错点复盘
为什么需要: 详情页问题通常出在“ID 没带到”“接口没发”“字段名不对”“状态没映射”四类。
怎么想: 按链路排查:列表点击 → URL 参数 → 网络请求 → 返回值 → setData → 页面渲染。转写稿里项目调试也反复强调先看网络请求和返回值,再回到页面验证数据;其中“健全/截权”这类同音词应纠正为“鉴权”,但本节详情接口不涉及鉴权,不要混入。
怎么做: 点击列表项后先看 URL 是否带 id;再看网络面板是否发出 /room/:id;然后看返回 code 是否为 10000;最后在调试器看 houseDetail 是否被 setData,并核对 WXML 字段。
易错点: 参数缺失时已有 wx.utils.toast('参数有误!'),不要静默失败;接口失败不要继续渲染旧数据;状态标签类名要对应 info/success/fail;图片字段要确认接口返回的是可访问 URL。
课件图示




代码示例
示例 1:位置服务(声明权限 + 获取定位 + 腾讯地图 SDK 搜索周边小区 / 逆地址解析)
app.json(先声明权限,否则接口直接失败)
json
{
"requiredPrivateInfos": [
"getLocation",
"chooseLocation"
],
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于小程序位置接口的效果展示"
}
}
}utils/qqmap.js(实例化 SDK,只实例化一次)
javascript
// 导入腾讯位置服务 SDK(官方封装好的 qqmap-wx-jssdk.js)
import QQMapWX from '../libs/qqmap-wx-jssdk'
// 实例化位置服务(使用个人申请的 key)
export default new QQMapWX({
key: '填写自已的 KEY',
})house_pkg/pages/locate/index.js(页面逻辑)
javascript
// 导入腾讯位置服务
import QQMap from '../../../utils/qqmap'
Page({
data: {
address: '',
points: [],
},
onLoad() {
// 获取用户经纬度
this.getLocation()
},
// 获取用户当前位置
async getLocation() {
// 调用小程序 API 获取用户位置(支持 Promise)
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 })
},
})
// 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 })
},
})
},
// 重新选择位置
async chooseLocation() {
// 调用小程序 API 获取新的位置
const { latitude, longitude } = await wx.chooseLocation()
// 获取新的位置附近的小区
this.getPoint(latitude, longitude)
},
})house_pkg/pages/locate/index.wxml(渲染)
xml
<!-- 检测用户登录状态 -->
<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"
is-link
/>
</van-cell-group>
</view>
</authorization>要点解释与易错点
requiredPrivateInfos与permission.scope.userLocation必须先在app.json声明,并在小程序管理后台申请开通,否则wx.getLocation直接报错;desc是弹窗给用户看的说明文案,不能省略。wx.getLocation()、wx.chooseLocation()都支持返回 Promise,所以可以直接await解构latitude / longitude;若不await,解构出来的就是undefined。location参数必须是"纬度,经度"拼接的字符串([latitude, longitude].join(',')),直接传数组会搜索不到结果。- SDK 的
success回调务必用箭头函数,否则this不指向页面实例,this.setData会报错。 - 笔记里
getLoaction是笔误,实际方法名要和onLoad中调用的名字保持一致(getLocation)。 - key 只用于教学演示,真实项目不要硬编码提交到仓库;
search的调用次数受 key 配额限制,不要在没有定位结果时反复调用。
示例 2:authorization 组件重写生命周期 + 添加房屋(收集数据、校验、上传身份证、提交)
components/authorization/index.js(未登录时不执行页面业务代码)
javascript
Component({
data: {
isLogin: false,
},
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,
})
}
},
},
})components/authorization/index.wxml
xml
<slot wx:if="{{isLogin}}"></slot>house_pkg/pages/form/index.js(表单页)
javascript
// 导入表单验证插件
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 })
},
// 上传身份证照片(wxml 上用 mark:type 区分正面 / 反面)
async uploadPicture(ev) {
// 区分用户上传的是正面或反面
const type = ev.mark.type
try {
// 打开相册或拍照
const media = await wx.chooseMedia({
count: 1,
mediaType: ['image'],
sizeType: ['compressed'],
})
// 本地图片大小判断(单位 B),避免上传超大图片
const size = media.tempFiles[0].size
if (size > 2 * 1024 * 1024) return wx.utils.toast('图片不能超过两兆!')
// 调用 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 })
},
})要点解释与易错点
- 组件
attached里getCurrentPages().pop()拿到的是当前页面实例,给它的onLoad/onShow赋空函数,才能真正阻止页面 JS 中的业务(如wx.getLocation、腾讯地图search)被调用;只加<authorization>包裹只能控制结构渲染,管不住 JS 里的生命周期。 - 必须用
wx.redirectTo而不是wx.navigateTo,否则登录页会把原页面压在页面栈里,登录后返回逻辑混乱;redirectURL拼上currentPage.route便于登录后回跳。 wechat-validate的三件套是固定写法:import→behaviors: [wxValidate]→rules。rules必须与data同级,写进data里会校验不到。model:value双向绑定只支持 data 的一级字段,所以name / gender / mobile / idcardFrontUrl / idcardBackUrl都必须平铺在data中,不能嵌套成对象。- 手机号正则写成
^1[3-8]\d{9}$是为了保持宽松,不要写得过死,否则新号段用户无法通过前端校验。 ev.mark.type依赖 wxml 上的mark:type="idcardFrontUrl"/mark:type="idcardBackUrl",setData({ [type]: url })才能按正反面写入对应字段。- 上传时
name: 'file'必须与接口文档中的字段名一致;wx.uploadFile返回的result.data是字符串,需要JSON.parse后再判断code。 - 提交前用
this.validate()做整体校验(返回布尔值);提交时用解构剔除__webviewId__、status等多余字段,避免后端报参数错误。 - 上传前先用
tempFiles[0].size判断大小(2 * 1024 * 1024字节),超限直接return,不要发出请求。
示例 3:房屋列表与房屋详情
house_pkg/pages/list/index.js
javascript
Page({
data: {
houseList: [],
isEmpty: false,
dialogVisible: 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,
})
},
goDetail(ev) {
// 通过 mark:id 来传递数据
wx.navigateTo({
url: '/house_pkg/pages/detail/index?id=' + ev.mark.id,
})
},
})house_pkg/pages/list/index.wxml
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
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>house_pkg/pages/detail/index.js
javascript
Page({
data: {
houseDetail: {},
},
onLoad({ id }) {
// 获取房屋详情
this.getHouseDetail(id)
},
// 房屋详情接口
async getHouseDetail(id) {
if (!id) return wx.utils.toast('参数有误!')
// 调用接口
const { code, data: houseDetail } = await wx.http.get('/room/' + id)
// 检测接口是否调用成功
if (code !== 10000) return wx.utils.toast()
// 渲染数据
this.setData({ houseDetail })
},
})house_pkg/pages/detail/index.wxml
xml
<scroll-view scroll-y enhanced show-scrollbar="{{false}}">
<view class="house-detail">
<van-cell-group border="{{false}}" title="房屋信息">
<van-cell title="{{houseDetail.point}}">
<text wx:if="{{houseDetail.status === 1}}" class="tag info">正在审核</text>
<text wx:if="{{houseDetail.status === 2}}" class="tag success">审核通过</text>
<text wx:if="{{houseDetail.status === 3}}" class="tag fail">审核失败</text>
</van-cell>
</van-cell-group>
<van-cell-group title="业主信息" border="{{false}}">
<van-cell
title-width="200rpx"
title="房间号"
value="{{houseDetail.building}}{{houseDetail.room}}"
/>
<van-cell title-width="200rpx" title="业主" value="{{houseDetail.name}}" />
<van-cell
title-width="200rpx"
border="{{false}}"
title="手机号"
value="{{houseDetail.mobile}}"
/>
</van-cell-group>
<view class="id-card">
<van-cell title="本人身份证照片" />
<view class="id-card-front">
<image src="{{houseDetail.idcardFrontUrl}}" />
</view>
<view class="id-card-back">
<image src="{{houseDetail.idcardBackUrl}}" />
</view>
</view>
</view>
</scroll-view>
<view class="toolbar" bind:tap="editHouse">
<text class="enjoy-icon icon-edit"></text>
<text class="button-text">修改房屋信息</text>
</view>要点解释与易错点
- 列表接口必须放在
onShow里:添加房屋成功后是wx.navigateBack({ delta: 4 })回来的,此时只触发onShow,放在onLoad里数据不会刷新。 isEmpty要显式根据houseList.length === 0设置,否则空列表会渲染成空白页而不是提示文案。- 列表最多展示 5 条,
wx:if="{{houseList.length < 5}}"控制“添加新房屋”按钮的显隐。 - 跳详情用
mark:id传参(ev.mark.id),比data-id更适合在组件(van-cell-group)上使用;URL 上拼?id=,详情页onLoad({ id })直接解构。 - 详情接口要先判断
if (!id),防止从其他入口进入时参数为空还发请求。 - 房屋状态 1/2/3 分别对应
info/success/fail三个类名,三处(列表、详情)都要保持一致,否则标签样式错乱。 wx.http.get('/room/' + id)是路径参数,注意别写成/room?id=。
常见错误
1. 页面已跳登录页,腾讯地图请求照样发出,key 配额被白白消耗
- 现象:未登录直接进入
/house_pkg/pages/locate/index,虽然被authorization拦到登录页,但 Network 面板里仍有一条apis.map.qq.com的搜索请求,控制台还有onLoad里的日志。 - 原因:小程序的页面 JS 在跳转前就已经执行,
attached里的wx.redirectTo只能拦住渲染,拦不住已经注册的onLoad/onShow/onReady。 - 修法:在
authorization的attached中getCurrentPages()取出当前页实例,未登录时把currentPage.onLoad / onShow / onReady覆盖为空函数,再执行跳转;不要只依赖wx:if控制结构。
2. model:value 绑到嵌套对象上,输入框有内容但 data 里始终是空
- 现象:
<input model:value="{{form.name}}" />,输入后this.data.form.name不变,提交时业主姓名为空。 - 原因:小程序简易双向绑定只支持
data的一级字段,不支持路径表达式,也不支持数组下标。 - 修法:把
name / gender / mobile / idcardFrontUrl / idcardBackUrl全部拍平成data顶层字段;确需嵌套时改用bindinput手动setData。
3. 校验规则写了但 validate() 永远返回 true
- 现象:姓名为空点提交照样通过,或者报
this.validate is not a function。 - 原因:一是
rules被写进了data或methods里(必须与data同级);二是behaviors: [wxValidate]忘记注册或写在了Component之外;三是字段名与data里的 key 对不上(如规则写mobile、data 里叫phone)。 - 修法:
import wxValidate from 'wechat-validate'→behaviors: [wxValidate]→rules提到与data同级 → 提交前if (!this.validate()) return;规则 key 与 data key 严格一一对应。
4. 点身份证图片右上角的删除,图删了但选择相册的弹窗又弹出来
- 现象:点 × 后
chooseMedia被再次触发。 - 原因:
bind:tap绑在外层容器上,删除按钮是它的子节点,tap事件冒泡回父元素。 - 修法:把删除按钮的绑定改成
catch:tap(小程序没有.stop修饰符,catch即阻止冒泡);或者干脆把上传事件绑到图片本身,从结构上避开嵌套。
5. 上传大图后接口无响应 / 返回一段 HTML,页面卡住且没有任何提示
- 现象:选一张几 MB 的照片,
wx.uploadFile的success里JSON.parse(result.data)直接抛异常。 - 原因:后端对超限文件没有返回规范 JSON,前端也没做大小预判,把不符合要求的文件白白传了一遍。
- 修法:
chooseMedia返回后先取media.tempFiles[0].size,与上限(如2 * 1024 * 1024)比较,超限就wx.utils.toast('图片不能超过 2M')并return;JSON.parse外面套try/catch,同时把chooseMedia的取消(Promise reject)也一并捕获。
6. 新增房屋成功后返回列表,列表里看不到新数据
- 现象:
wx.navigateBack({ delta: 4 })回到列表页,数据还是旧的。 - 原因:列表数据写在
onLoad里,navigateBack只会重新触发onShow,onLoad不会再次执行。 - 修法:列表查询统一放
onShow;同时确认delta的层数与实际页面栈一致(定位 → 楼栋 → 房间 → 表单,返回 4 层),层数写错会退到意料之外的页面。
7. 详情页拿不到 id,/room/undefined 报参数错误
- 现象:
onLoad({ id })里id为undefined。 - 原因:跳转时用了
ev.mark.id,但标签上只写了data-id没写mark:id;或用了data-id却读ev.mark。 - 修法:
mark:id配ev.mark.id,data-id配ev.currentTarget.dataset.id,两者不要混用;详情页再加一层if (!id) return wx.utils.toast('参数有误!')兜底。
练习
练习 1|给鉴权组件做一次"零请求"验收 清空 token,分别直达 locate、building、room、form 四个页面,打开 Network 面板记录请求数量;改造 authorization 后再次验证请求数为 0。要求:写出改造前后两次的请求条数对比,并说明被覆盖的是哪几个生命周期、为什么 onReady 也要一起覆盖。
练习 2|身份证正反面上传合并为一个方法 用 mark:type="idcardFrontUrl" / mark:type="idcardBackUrl" 区分正反面,共用一个 uploadPicture 函数,通过 this.setData({ [type]: url }) 动态写回字段。同时补齐三件事:超过 2M 拦截、用户取消时不报红、点击删除不触发重新选图。完成后录一段操作视频自检。
练习 3|给房屋列表加删除与下拉刷新 使用 van-swipe-cell 的 bind:close + mark:id,调用删除接口后重新拉取列表并同步 isEmpty;再给 scroll-view 加 refresher-enabled,下拉时重新请求 /room。要求:删除最后一条后页面能正确切到"您还没有认证房屋"的空状态。
练习 4|表单校验加固 把手机号正则放宽为只校验 ^1\d{10}$,姓名改为 2–5 位中文并给出明确提示;再补一条自定义校验:两张身份证照片必须都已上传,否则 toast 阻止提交。提交前打印最终 payload,确认不含 __webviewId__ 等调试字段。
练习 5|位置服务串起来 在 locate 页同时调用 reverseGeocoder 展示当前详细地址、search 展示附近 5 个小区,并加"重新选择位置"按钮走 wx.chooseLocation 后刷新两块数据。要求:把两个回调改造成 Promise 包装,用 Promise.all 统一 setData,并说明为什么不能直接 await QQMap.search(...)。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 反馈-git切换到提交历史-学习目标 | 12:27 |
| 2 | auth鉴权组件-重写页面生命周期 | 11:37 |
| 3 | 添加房屋-双向绑定收集表单数据 | 15:16 |
| 4 | 添加房屋-表单校验 | 10:59 |
| 5 | 添加房屋-上传身份证图 | 14:27 |
| 6 | 小程序事件系统-阻止冒泡 | 04:51 |
| 7 | 添加房屋-上传图片-图片大小限制 | 06:04 |
小结
- 小程序的页面 JS 一定先于路由跳转执行,凡是"未登录不该跑"的逻辑,靠
wx:if藏结构是不够的,必须把页面生命周期本身覆盖掉。 - 凡是"从别处返回后需要看到最新数据"的列表,查询写
onShow而不是onLoad,这是本项目里最容易被忽略的一条。 - 表单数据要先按接口文档对齐字段名和类型(
gender是 number、必传字段一个都不能少),再谈收集方式;字段名写错的返工成本远高于多看一眼文档。 - 简易双向绑定只服务于扁平
data,字段一多就老老实实拍平,别为了"好看"去嵌套对象。 - 前端校验的定位是"拦住明显不合规",正则要写宽松些(尤其手机号),不要把真实存在的号码挡在门外;
validate()的返回值必须显式判断并return。 - 上传图片是一条完整链路:选图 → 体积预判 → 上传 → 解析响应 → 回写字段 → 可删除,任何一环缺失都会表现为"传不上去"或"控制台红一片"。
- 事件冒泡在小程序里同样存在,只是没有
.stop修饰符,用catch:tap代替bind:tap;写嵌套结构时先想清楚这一层点击该由谁响应。 - 位置服务、上传、表单校验这类能力都是"外部依赖 + 配额/体积限制",开发期就要把失败分支(无权限、域名未配、超限、用户取消)走一遍,别等联调时才补。