主题
房屋管理与报修(二)
本页对应 房屋管理与报修,共 7 段视频,约 1 小时 13 分。
学习目标
- 能够通过事件传参区分身份证正面与国徽面上传,并使用对象动态 key 将图片地址写入对应字段。
- 能够完成房屋表单校验、剔除小程序 data 中的多余字段、提交新增房屋请求,并在成功后回退多级销毁中间页面。
- 能够调用房屋列表接口完成列表渲染,使用条件渲染展示审核状态并应用对应状态样式。
- 能够使用辅助接口修改房屋审核状态,并通过列表页面验证状态文案与颜色正确更新。
- 能够从列表页传 ID 进入详情页,在 onLoad 中接收参数并查询、渲染房屋详情信息。
- 能够在编辑页根据 ID 查询并回显表单数据,区分新增与编辑逻辑,处理双向绑定字段层级并动态设置页面标题。
知识点
从列表进入详情:先明确“详情页需要什么”
为什么需要:房屋详情不是固定页面,而是“某一条房屋”的详情。列表里有多条房屋,点击不同房屋必须打开对应数据,所以详情页必须知道当前房屋的唯一标识。
怎么想:沿用公告详情的思路:列表页负责把房屋 ID 传到 URL,详情页从页面参数中取出 ID,再调用详情接口。
怎么做:在列表项事件里通过 mark:id 拿到当前房屋 ID,拼到跳转地址后面。
易错点:不要只跳页面不传 ID;参数名要与详情页接收名一致;mark 的值要在标签上声明,不能凭空从事件对象里取。
javascript
// house_pkg/pages/list/index.js
Page({
// ...
goDetail(ev) {
// 通过 mark:id 来传递数据
wx.navigateTo({
url: '/house_pkg/pages/detail/index?id=' + ev.mark.id,
})
},
})转写稿中的调试过程是:先在页面路径面板切到“页面参数”,点击不同房屋,确认 URL 后面已经带上 id。这一步很关键,因为如果列表页没传成功,后面详情页怎么请求都不对。
详情页接收 ID 并请求详情
为什么需要:详情接口通常要求路径参数,例如 /room/:id。没有 ID 就无法查询指定房屋。
怎么想:小程序页面接收参数要用 onLoad,其他生命周期拿不到页面参数。拿到 ID 后先做空值判断,再发请求。
怎么做:在 onLoad({ id }) 中调用 getHouseDetail(id);接口成功时判断 code === 10000,把返回的 data 存到 houseDetail。
易错点:参数解构名写成 ID、houseId 等会取不到;接口失败要 return,不要继续 setData;data 要重命名,避免和页面 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 })
},
})接口文档地址见笔记:接口文档地址。
把详情数据渲染到页面
为什么需要:请求成功只代表数据到了 JS 里,用户还看不到,必须通过数据绑定渲染。
怎么想:按页面结构分组:房屋信息、业主信息、身份证照片。状态是数字,展示时要转成中文标签。
怎么做:用 houseDetail.point 显示小区/地址,用 building + room 拼房间号,用 name、mobile 显示业主信息,用 idcardFrontUrl、idcardBackUrl 显示身份证正反面。
易错点:building 和 room 是两个字段,不能只写一个;图片字段名不要写错;scroll-view 要允许纵向滚动。
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>审核状态:数字转文案与样式
为什么需要:后端常返回数字状态,用户看不懂 1/2/3,前端必须转成“正在审核/审核通过/审核失败”。
怎么想:状态互斥,只显示其中一个。笔记用三个 wx:if,分别对应 status === 1/2/3,并配合 info/success/fail 类名控制颜色。
怎么做:保留笔记写法即可;转写稿也提到可用 wx:if / wx:elif / wx:else 优化性能,但本节笔记代码以三个 wx:if 为准。
易错点:不要用 = 代替 ===;状态类名不要混用;三种状态测试时可用辅助接口改状态后重新进入小程序验证。转写稿中“审核中、审核成功、审核失败”对应数字 1、2、3,颜色类名分别为 info、success、fail。
底部“修改房屋信息”入口
为什么需要:详情页除了查看,还要能进入编辑。编辑页需要知道编辑哪一条房屋,所以仍然要传 ID。
怎么想:详情页底部按钮绑定 editHouse,点击时把当前 houseDetail.id 拼到编辑页 URL 上。
怎么做:笔记只保留了入口结构和 bind:tap="editHouse",具体编辑回显与提交属于后续编辑业务。
易错点:不要在详情页把整份对象塞进 URL;传 ID 即可,编辑页再按 ID 查询回显。转写稿也强调:有 ID 是编辑,没有 ID 是新增,两者共用表单页时要注意 onLoad 不要覆盖原逻辑。
编辑房屋的业务定位
为什么需要: 编辑房屋不是重新添加一套房,而是在原有房屋信息上修改。用户先要看到原数据,改完后再提交;否则表单是空的,用户无法准确修改。
怎么想: 编辑与新增可以共用同一个表单页:地址里带 id 就是编辑,不带 id 就是新增。后端也通过参数里是否包含 id 来区分添加或修改。
怎么做: 在 onLoad 中接收 point、building、room、id,有 id 时优先查详情,否则按新增渲染地址参数。接口文档见:接口文档地址。
易错点: 不要再写一个 onLoad 覆盖原逻辑,应在原有 onLoad 里加分支;编辑时从详情页传 id 就够了,其他字段可由详情接口查出。
在 onLoad 中区分新增与编辑
为什么需要: 同一个页面承担两种业务,必须先判断当前是新增还是编辑,才能决定是否请求详情。
怎么想: 判断依据就是 id 是否存在。存在则调用 getHouseDetail(id),不存在则把分步表单传过来的 point、building、room 渲染到页面。
怎么做: 保留笔记中的代码结构:
javascript
// house_pkg/pages/form/index.js
Page({
// ...
onLoad({ point, building, room, id }) {
// 根据 id 判断是否为修改房屋
if (id) return this.getHouseDetail(id)
// 添加房屋 (渲染数据)
this.setData({ point, building, room })
},
// 房屋详情接口
async getHouseDetail(id) {
// 调用接口
const { code, data: houseDetail } = await wx.http.get('/room/' + id)
// 检测接口是否调用成功
if (code !== 10000) return wx.utils.toast()
// 渲染数据
this.setData({ ...houseDetail })
},
})易错点: 调试时先打印 id,确认从详情页跳转时参数确实传进来了;不要用其他生命周期替代 onLoad 接收页面参数。
获取详情并回显到表单
为什么需要: 编辑页要展示原来的小区、楼栋、房间号、业主、图片等信息,用户才能基于原数据修改。
怎么想: 从详情页点击“编辑房屋”时把 id 拼到地址上,表单页 onLoad 接收到 id 后调用详情接口,再把返回对象展开写入 data。
怎么做: getHouseDetail 中调用 wx.http.get('/room/' + id),判断 code === 10000 后执行 this.setData({ ...houseDetail })。转写稿中特别强调:编辑页有双向绑定,不能把详情存到 detail 对象里再绑定 detail.xxx,因为小程序双向绑定不支持对象点属性;应把返回对象展开成一级字段。
易错点: 若写成 this.setData({ detail: houseDetail }),表单回显可能不生效;另外标题可从“添加房屋”改为“编辑房屋信息”,可用 wx.setNavigationBarTitle,避免标题突然变化影响体验。
提交修改并剔除不可改字段
为什么需要: 用户改完表单后要再次提交;但提交参数必须符合后端要求,不能把页面内部字段和不允许修改的状态传过去。
怎么想: 提交前先校验;再剔除小程序自动加的 __webviewId__ 和修改时不允许改的 status;最后调用与新增相同的 POST /room。
怎么做: 笔记代码如下:
javascript
Page({
onLoad() {
// ...
},
getHouseDetail(id) {
// ...
},
// 提交审核
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 })
},
})易错点: 转写稿调试时曾把整个 data 直接当请求体,后端提示参数不符合要求;要用剩余参数只提交后端需要的字段。修改房屋时 status 不允许修改,必须剔除。
返回列表与页面栈处理
为什么需要: 提交成功后不应停留在表单页,也不应保留分步表单的中间页。
怎么想: 添加/编辑入口经过多级分步页面,成功后应直接回房屋列表,并销毁中间页面。
怎么做: 使用 wx.navigateBack({ delta: 4 }) 回退 4 级到列表页。转写稿中说明:回退会销毁中间页面,避免浪费性能;小程序页面栈也有限制。
易错点: delta 要按实际页面层级数清楚;不要用重新打开列表页的方式保留中间页。
删除入口:为什么用 van-swipe-cell 和 van-dialog
删除房屋是房屋管理的最后一个功能,核心是根据房屋 ID 删除数据。为什么需要滑动单元格和确认弹窗?因为列表中的删除不能一点就删,否则容易误操作;滑动提供入口,弹窗负责二次确认。
怎么想:把“触发删除”拆成两步——先滑动到右侧,记录待删除房屋;再在弹窗中确认,才真正调用接口。怎么做:使用 Vant 的 van-swipe-cell 和 van-dialog,二者都有 close 事件,分别监听滑动关闭和弹窗关闭。易错点:不要在一次滑动事件里直接删除;要区分 position,只有滑到右侧才进入删除流程。
javascript
Page({
swipeClose(ev) {
const { position, instance } = ev.detail
if (position === 'right') {
// 显示 Dialog 对话框
this.setData({
dialogVisible: true,
})
// 记录房屋的 id 和索引,在调用接口的时使用
this.cellId = ev.mark.id
this.cellIndex = ev.mark.index
// swiper-cell 滑块关闭
instance.close()
}
},
dialogClose(ev) {
// 当用户点了确认按钮时调用方法删除数据
if (ev.detail === 'confirm') this.deleteHouse()
},
})swipeClose:记录待删除房屋的 ID 和索引
为什么需要记录 ID 和索引?ID 用于请求后端删除;索引用于删除成功后从前端 houseList 中移除对应项。怎么想:滑动关闭时只是“选中”了某一行,还没有删除,所以先把信息暂存到页面实例 this 上,供后续 deleteHouse 使用。
怎么做:从 ev.detail 解构 position 和 instance;判断 position === 'right';打开弹窗;通过模板中的 mark:id、mark:index 取到 ev.mark.id、ev.mark.index,保存为 this.cellId、this.cellIndex;最后调用 instance.close() 关闭滑块。
易错点:mark 数据要在模板中正确标记;this.cellId 不要只放在 data 里又忘记同步;instance.close() 不能漏,否则弹窗关闭后滑块可能仍停在打开状态。转写稿中列表页通过 onShow 获取最新数据,说明列表数据会随页面返回变化;删除是当前页内操作,因此这里用索引局部更新,而不是重新请求整页。
dialogClose:只在确认时删除
为什么需要单独监听弹窗关闭?因为用户可能点“取消”。怎么想:van-dialog 的 close 事件会带关闭结果,只有 confirm 才代表确认删除。怎么做:if (ev.detail === 'confirm') this.deleteHouse()。易错点:不要写成“弹窗一关闭就删除”;confirm 是字符串,不是布尔值。
deleteHouse:调接口并同步列表
为什么需要封装删除方法?因为接口调用、成功判断、列表移除、空状态更新要放在一处。怎么想:先校验 cellId,再请求删除;接口成功后再改前端数据。怎么做:检查 this.cellId,不存在就提示“参数有误”;调用 wx.http.delete('/room/' + this.cellId);判断 code !== 10000 则提示;成功后用 splice(this.cellIndex, 1) 移除,再 setData 更新 houseList 和 isEmpty。
javascript
Page({
swipeClose() {
// ...
},
dialogClose() {
// ...
},
// 删除房屋
async deleteHouse() {
// 检测id是否存在
if (!this.cellId) return wx.utils.toast('参数有误!')
// 调用接口
const { code } = await wx.http.delete('/room/' + this.cellId)
// 检测接口是否调用成功
if (code !== 10000) return wx.utils.toast()
// 从 AppData 中将删除的房屋从数组移除
this.data.houseList.splice(this.cellIndex, 1)
// 渲染数据
this.setData({
houseList: this.data.houseList,
isEmpty: this.data.houseList.length === 0,
})
},
})易错点:splice 后必须 setData,不能只改 this.data 就认为视图会更新;isEmpty 要在移除后计算,删除最后一条时才能显示空状态;接口失败时不要移除本地数据。转写稿里列表调试时通过控制台查看返回 data,删除功能也应同样确认接口返回 code 和当前列表长度,确保“后端删成功、前端同步删”一致。
在线报修页面与数据前置
为什么需要:在线报修不是孤立表单,房屋、维修项目、上门时间、联系电话等都要作为提交参数;且报修必须基于当前登录业主,所以进入页面前要保证登录状态,并先取到表单可选数据。
怎么想:先把“可选数据”和“用户输入数据”分开。房屋列表、维修项目由接口获取;房屋、项目、日期、手机号、描述、附件由用户选择或填写。接口返回先判断 code,成功再 setData 渲染。
怎么做:
javascript
// repair_pkg/pages/form/index.js
Page({
data: {
// ...
houseList: []
},
// 获取房屋列表(必须是通过审核的房屋)
async getHouseList() {
const { code, data: houseList } = await wx.http.get('/house')
if (code !== 10000) return wx.utils.toast()
this.setData({ houseList })
},
})接口文档见笔记中的房屋列表接口。
易错点:不要拿到 data 就直接渲染,先校验 code;房屋必须是可报修房屋,前端按接口返回展示,不要自行伪造过滤条件;登录态失效时要按项目统一方式处理。
用 van-action-sheet 选择房屋
为什么需要:房屋可能有多套,用弹出选择层比下拉框更适合移动端,也能复用 Vant 的列表交互。
怎么想:van-action-sheet 的 actions 绑定 houseList,用户点击某一项触发 select,事件对象 ev.detail 中带有所选数据。选择后同时保存 houseId 和 houseName:前者提交接口,后者页面展示。
怎么做:
xml
<van-action-sheet
bind:close="closeHouseLayer"
bind:cancel="closeHouseLayer"
bind:select="selectHouseInfo"
round
show="{{houseLayerVisible}}"
actions="{{houseList}}"
cancel-text="取消"
title="选择报修房屋"
/>javascript
Page({
data: {
houseId: '',
houseName: ''
},
selectHouseInfo(ev) {
this.setData({
houseId: ev.detail.id,
houseName: ev.detail.name,
})
},
})展示时用 value-class 切换选中后的文字颜色:
xml
<van-cell-group border="{{false}}" title="报修房屋">
<van-cell
value="{{houseName || '请选择报修房屋'}}"
value-class="{{houseName && 'active-cell'}}"
bind:click="openHouseLayer"
is-link
border="{{false}}"
/>
</van-cell-group>易错点:show 控制变量、close/cancel/select 三个事件要配套;ev.detail 才是选中项;只存名称会导致提交缺 houseId。
获取并选择维修项目
为什么需要:维修项目决定报修类型,必须由接口统一维护,不能写死在前端。
怎么想:与房屋选择同构:onLoad 拉取 repairItem,用 van-action-sheet 渲染,select 后保存 repairItemId 与 repairItemName。
怎么做:
javascript
Page({
data: {
repairItem: [],
},
onLoad() {
this.getRepairItem()
},
async getRepairItem() {
const { code, data: repairItem } = await wx.http.get('/repairItem')
if (code !== 10000) return wx.utils.toast()
this.setData({ repairItem })
},
})xml
<van-action-sheet
bind:close="closeRepairLayer"
bind:cancel="closeRepairLayer"
bind:select="selectRepairItemInfo"
round
show="{{repairLayerVisible}}"
actions="{{repairItem}}"
cancel-text="取消"
title="选择维修项目"
/>javascript
selectRepairItemInfo(ev) {
this.setData({
repairItemId: ev.detail.id,
repairItemName: ev.detail.name,
})
}易错点:不要把房屋列表和维修项目列表的事件处理混用;repairItemName 只负责展示,提交仍以 repairItemId 为准。
预约上门时间
为什么需要:报修需要上门日期,用底部弹层加日期选择器符合移动端操作习惯。
怎么想:van-popup 负责弹层,van-datetime-picker 负责选日期;监听 confirm,把 ev.detail 格式化后保存,并关闭弹层。min-date 设为当前日期,避免选过去时间。
怎么做:
xml
<van-popup
bind:close="closeDateLayer"
round
show="{{dateLayerVisible}}"
position="bottom"
>
<van-datetime-picker
bind:cancel="closeDateLayer"
bind:confirm="selectDateInfo"
type="date"
value="{{currentDate}}"
min-date="{{currentDate}}"
/>
</van-popup>javascript
selectDateInfo(ev) {
this.setData({
appointment: wx.utils.dataFormat(ev.detail),
dateLayerVisible: false,
})
}易错点:confirm 后要关闭弹层;提交前确认 appointment 是接口需要的格式;不要只存原始时间对象导致展示异常。
上传附件
为什么需要:报修问题常需图片说明,使用 van-uploader 可选择、预览、限制数量。
怎么想:file-list 记录已上传成功图片;after-read 拿到临时文件后调用 wx.uploadFile。上传成功把返回图片地址追加到 attachment,再 setData 触发渲染。附件不是必填项,校验时可不校验。
怎么做:
xml
<van-uploader
bind:after-read="uploadPicture"
max-count="6"
preview-size="100"
file-list="{{attachment}}"
/>javascript
uploadPicture(ev) {
const { file } = ev.detail
wx.uploadFile({
url: wx.http.baseURL + '/upload',
filePath: file.url,
name: 'file',
header: {
Authorization: 'Bearer ' + getApp().token,
},
success: (result) => {
const data = JSON.parse(result.data)
if (data.code !== 10000) return wx.utils.toast('文件上传失败!')
const { attachment } = this.data
attachment.push(data.data)
this.setData({ attachment })
},
})
}易错点:result.data 常是字符串,需要 JSON.parse;attachment.push 后必须 setData;上传要带 token。转写稿中上传身份证正反面时通过事件参数区分字段、再用对象 key 动态绑定更新不同字段,这类“多类型上传复用同一逻辑”的思路可借鉴,但不要照搬无关业务。
表单校验与提交
为什么需要:提交前校验能减少无效请求,并给用户明确提示。
怎么想:用 wechat-validate 的 behaviors 和 rules,必填项包括房屋、项目、手机号、日期、描述;附件非必填。提交时先 validate(),再只取接口字段提交。
怎么做:
javascript
import wxValidate from 'wechat-validate'
Page({
behaviors: [wxValidate],
data: {
houseId: '',
repairItemId: '',
mobile: '',
appointment: '',
description: '',
attachment: []
},
rules: {
houseId: [{required: true, message: '请选择报修房屋!'}],
repairItemId: [{required: true, message: '请选择维修的项目!'}],
mobile: [
{required: true, message: '请填写手机号码!'},
{pattern: /^1[3-8]\d{9}$/, message: '请填写正确的手机号码!'},
],
appointment: [{required: true, message: '请选择预约日期!'}],
description: [{required: true, message: '请填写问题描述!'}],
},
async submitForm() {
if (!this.validate()) return
const {houseId, repairItemId, mobile, appointment, description, attachment} = this.data
const {code} = await wx.http.post('/repair', {
houseId,
repairItemId,
mobile,
appointment,
description,
attachment,
})
if (code !== 10000) return wx.utils.toast('在线报修失败!')
wx.redirectTo({
url: '/repair_pkg/pages/list/index',
})
},
})易错点:不要直接把整个 this.data 提交,里面可能混入页面内部字段;成功后用 redirectTo 到报修列表,避免用普通跳转把表单页堆在页面栈里。转写稿调试提交参数时也强调:参数必须符合后端要求,多余字段会导致报错。
接口与登录态:先确认“能拿到谁的数据”
为什么需要:报修列表属于用户个人数据,接口要求保持登录状态。没有登录态时,请求可能被拦截或返回失败;即使页面能打开,也拿不到当前用户的报修记录。转写稿里提到的“健全组件”应纠正为“鉴权组件”,列表页要先检查项目里是否已加鉴权。
怎么想:进入列表页先调用 /repair 接口,传分页参数 current、pageSize;接口返回后先判断 code,成功再从 data.rows 取列表。登录态一般由项目封装的 wx.http 拦截器统一带 Authorization,本节不额外编造登录代码。
怎么做:按笔记示例在 onLoad 中调用 getRepairList:
javascript
// repqir_pkg/pages/list/index.js
Page({
data: {
repairList: []
},
onLoad() {
// 获取报修列表数据
this.getRepairList()
},
// 报修列表接口
async getRepairList() {
// 调用接口
const {
code,
data: { rows: repairList },
} = await wx.http.get('/repair', { current: 1, pageSize: 10 })
// 检测接口是否用成功
if (code !== 10000) return wx.utils.toast()
// 渲染数据
this.setData({
repairList,
isEmpty: repairList.length === 0,
})
},
})接口文档地址保留:接口文档地址。
易错点:不要只看 data 不看 code;解构 rows 时确认返回结构;isEmpty 建议在 data 中先初始化为 false,否则首屏可能先闪空状态;转写稿里的“on漏”应为 onLoad,“透s”应为 toast。
列表渲染与空状态:有数据展示,无数据引导
为什么需要:用户没有报修记录时不能只显示空白页,要给“去添加”的入口;有数据时则要按报修记录逐条展示房屋信息、报修项目、预约时间、电话和状态。
怎么想:用 isEmpty 做分支:!isEmpty 时渲染 scroll-view 列表,否则渲染空状态。列表用 wx:for 循环 repairList,wx:key="id" 保证复用稳定。转写稿里反复强调列表渲染要折叠结构、确认同级标签,因为小程序模板结构多一个少一个标签都容易整页挂掉。
怎么做:按笔记模板渲染:
xml
<authorization>
<block wx:if="{{!isEmpty}}">
<scroll-view show-scrollbar="{{false}}" enhanced scroll-y>
<view class="repairs">
<view class="repairs-title">我的报修</view>
<view class="repairs-list">
<van-cell-group
wx:for="{{repairList}}"
wx:key="id"
border="{{false}}"
mark:id="{{item.id}}"
bind:tap="goDetail"
>
<van-cell size="large" title="{{item.houseInfo}}">
<text wx:if="{{item.status === 0}}" class="tag cancel">已取消</text>
<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 complete">已完成</text>
</van-cell>
<van-cell title="报修项目" border="{{false}}" value="{{item.repairItemName}}" />
<van-cell title="预约时间" border="{{false}}" value="{{item.appointment}}" />
<van-cell title="电话号码" border="{{false}}" value="{{item.mobile}}" />
</van-cell-group>
</view>
</view>
</scroll-view>
<view class="toolbar" bind:tap="addRepair">
<text class="enjoy-icon icon-repair"></text>
<text class="button-text">在线报修</text>
</view>
</block>
<view wx:else class="blank">
您还没有报修记录,请点击
<navigator hover-class="none" class="link" url="/repair_pkg/pages/form/index">添加</navigator>
</view>
</authorization>易错点:wx:key 用唯一 id,不要用索引;空状态里的 navigator 路径要和项目目录一致;scroll-view 需要明确高度或父级布局,否则滚动可能不生效;mark:id 与 bind:tap="goDetail" 是给详情跳转传参用的,点击事件里要通过事件对象取 mark.id。
四种状态:把后端数字翻译成用户能懂的中文
为什么需要:接口返回的是数字状态,用户看不懂 0/1/2/3,必须转成“已取消、受理中、上门中、已完成”,并用不同颜色区分。转写稿里处理房屋审核状态时也是同一思路:后端返回数字,前端用条件渲染显示中文。
怎么想:四个状态互斥,可以用四个 wx:if,也可以用 wx:elif 优化。类名按笔记约定:0 -> cancel,1 -> info,2 -> success,3 -> complete。转写稿里“叉值”应纠正为“插值”,这里主要用的是条件渲染和插值表达式。
怎么做:模板中已按 item.status === 0/1/2/3 分别渲染对应 text 和类名。若项目已定义全局 .tag 基础样式与状态颜色类,直接使用即可。
易错点:状态判断用 ===;不要漏掉某个状态导致该条记录没有标签;四个 wx:if 虽然能跑,但同级别更推荐 wx:elif 减少判断;类名拼写要与全局样式一致。转写稿提到开发阶段可用辅助接口修改维修状态来验证展示,具体接口以文档为准,改完后刷新页面检查文案和颜色是否都正确。
调试顺序:先看返回,再渲染,再验状态
为什么需要:列表页问题通常出在登录态、返回结构或空状态判断,不先打印就改模板容易越改越乱。
怎么想:先确认页面是否被鉴权组件包裹;再在 getRepairList 中打印返回结果,确认 data.rows 是数组;然后检查 setData 后 repairList 是否有值;最后用不同状态的记录验证标签。转写稿里列表页还会根据数据是否变化选择 onShow,如果从新增报修页返回后需要看到最新列表,可把获取逻辑放到 onShow,但要注意避免重复请求。本节示例按笔记放在 onLoad。
怎么做:调试时依次看控制台返回、repairList、isEmpty、页面结构;状态不对时先确认后端值,再查模板条件。
易错点:不要把“鉴权”写成“健全”;不要把 onLoad 写成“on漏”;空状态初始值、接口失败分支、登录失效分支都要有反馈,避免用户看到空白页。
课件图示








代码示例
1. 房屋列表跳转详情 + 详情页获取并渲染
javascript
// house_pkg/pages/list/index.js
Page({
data: {
houseList: []
},
onShow() {
// 列表数据可能被“添加/删除”改变,用 onShow 每次取最新
this.getHouseList()
},
// 查询我的房屋列表
async getHouseList() {
const { code, data: houseList } = await wx.http.get('/room')
if (code !== 10000) return wx.utils.toast()
this.setData({ houseList })
},
// 跳转房屋详情:通过 mark:id 传递数据
goDetail(ev) {
wx.navigateTo({
url: '/house_pkg/pages/detail/index?id=' + ev.mark.id,
})
},
})xml
<!-- house_pkg/pages/list/index.wxml -->
<view class="house-list">
<van-cell-group
wx:for="{{houseList}}"
wx:key="id"
border="{{false}}"
mark:id="{{item.id}}"
bind:tap="goDetail"
>
<van-cell title="{{item.point}}" value="{{item.building}}{{item.room}}" />
</van-cell-group>
</view>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 })
},
})xml
<!-- house_pkg/pages/detail/index.wxml -->
<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>要点与易错点
mark:id="{{item.id}}"是小程序里传递自定义数据的方式,接收时用ev.mark.id,不要写成ev.currentTarget.dataset.id。- 详情页用
onLoad({ id })解构页面参数,只有onLoad能拿到 URL 上的参数,onShow拿不到。 - 接口返回用
data: houseDetail重命名后再setData,避免把整个res塞进data。 - 状态码是数字(1/2/3),页面里用
wx:if转成文案;三种状态互斥,也可以用wx:elif/wx:else减少判断。 - 列表用
onShow而不是onLoad,因为从详情页返回后数据可能已变化。
2. 编辑房屋:表单回显 + 提交(与添加共用同一页面)
javascript
// house_pkg/pages/form/index.js
Page({
data: {
// 表单字段(直接作为一级属性,方便双向绑定)
point: '',
building: '',
room: '',
name: '',
mobile: '',
idcardFrontUrl: '',
idcardBackUrl: '',
},
onLoad({ point, building, room, id }) {
// 根据 id 判断是否为修改房屋
if (id) return this.getHouseDetail(id)
// 添加房屋(渲染数据)
this.setData({ point, building, room })
},
// 房屋详情接口(回显)
async getHouseDetail(id) {
const { code, data: houseDetail } = await wx.http.get('/room/' + id)
if (code !== 10000) return wx.utils.toast()
// 详情对象要“平铺”到 data 上,双向绑定才生效
this.setData({ ...houseDetail })
// 修改页面标题
wx.setNavigationBarTitle({ title: '编辑房屋信息' })
},
// 提交审核
async submitForm() {
// 验证数据
if (!this.validate()) return
// 剔除多余参数 __webviewId__ 与不允许修改的 status
const { __webviewId__, status, ...data } = this.data
// 调用接口(有 id 即修改,无 id 即新增)
const { code } = await wx.http.post('/room', data)
if (code !== 10000) return wx.utils.toast('提交数据失败!')
// 返回房屋列表页面
wx.navigateBack({ delta: 4 })
},
})要点与易错点
- 添加与编辑共用同一个页面:
onLoad里if (id) return this.getHouseDetail(id),有 id 就是编辑,没有就是新增。 - 回显必须把详情对象平铺进
data({ ...houseDetail })。小程序的model:value不支持houseDetail.xxx这种对象点属性,存成detail对象会导致表单不回显。 - 提交前剔除
__webviewId__(小程序自动加的,不是我们写的)和status(状态不允许前端改),用解构 + 剩余参数最省事;用delete会改动原对象。 - 提交成功后用
wx.navigateBack({ delta: 4 })一次退回列表页,中间的分步表单页面会被销毁;不要用navigateTo再打开列表,页面栈最多 10 层且会浪费性能。 - 详情页跳编辑页时只需拼
id,其它字段后端按 id 就能查出来,不必一路传。
3. 删除房屋:滑动单元格 + 确认对话框
javascript
// house_pkg/pages/list/index.js
Page({
data: {
houseList: [],
isEmpty: false,
dialogVisible: false,
},
swipeClose(ev) {
const { position, instance } = ev.detail
if (position === 'right') {
// 显示 Dialog 对话框
this.setData({ dialogVisible: true })
// 记录房屋的 id 和索引,在调用接口时使用
this.cellId = ev.mark.id
this.cellIndex = ev.mark.index
// swiper-cell 滑块关闭
instance.close()
}
},
dialogClose(ev) {
// 当用户点了确认按钮时调用方法删除数据
if (ev.detail === 'confirm') this.deleteHouse()
},
// 删除房屋
async deleteHouse() {
// 检测 id 是否存在
if (!this.cellId) return wx.utils.toast('参数有误!')
// 调用接口
const { code } = await wx.http.delete('/room/' + this.cellId)
if (code !== 10000) return wx.utils.toast()
// 从 AppData 中将删除的房屋从数组移除
this.data.houseList.splice(this.cellIndex, 1)
// 渲染数据
this.setData({
houseList: this.data.houseList,
isEmpty: this.data.houseList.length === 0,
})
},
})xml
<!-- 关键绑定:swipe-cell 与 dialog 共用 close 事件 -->
<van-swipe-cell
wx:for="{{houseList}}"
wx:key="id"
mark:id="{{item.id}}"
mark:index="{{index}}"
bind:close="swipeClose"
>
<van-cell title="{{item.point}}" value="{{item.building}}{{item.room}}" />
</van-swipe-cell>
<van-dialog
show="{{dialogVisible}}"
show-cancel-button
bind:close="dialogClose"
title="提示"
message="确认删除该房屋吗?"
/>要点与易错点
van-swipe-cell和van-dialog都有close事件,但含义不同:前者ev.detail.position === 'right'才表示“滑出了删除按钮”,后者ev.detail === 'confirm'才表示用户点了确认。- 待删除的
id与index要存到this(this.cellId/this.cellIndex)上,因为dialogClose里拿不到滑动时的mark参数;存到data里也行,但要setData。 - 删除成功后本地
splice再setData整个数组,同时更新isEmpty控制空状态展示。 - 判断
position === 'right'必不可少,否则左滑关闭时也会弹窗。
常见错误
点国徽面却更新到正面字段(或
type为 undefined)- 现象:上传完背面照片,页面显示在正面区域;
ev.mark打印为undefined。 - 原因:两个
image绑了同一个bindtap,但标签上没写mark:type;或解构时写成ev.currentTarget.dataset.type(mark传参不在dataset里)。 - 修法:标签上加
mark:type="front"/mark:type="back",处理函数里const { type } = ev.mark;用catchtap时确认没有阻止到自身的点击。
- 现象:上传完背面照片,页面显示在正面区域;
提交表单后端报
ID is not allowed to be empty/ 参数校验失败- 现象:新增房屋点提交,接口返回参数错误;或编辑时后端把状态也改了。
- 原因:直接把整个
this.data当请求体,小程序自动注入的__webviewId__等内部字段被一起提交;编辑场景还多带了status。 - 修法:
const { __webviewId__, status, id, ...body } = this.data后再post('/room', body);新增时id一并剔除,编辑时按接口要求单独传id。
编辑页表单空白,数据没回显
- 现象:从详情点“修改房屋信息”进来,输入框全是空的,但接口确实返回了数据。
- 原因:把详情存成了
this.setData({ houseDetail }),而表单是model:value绑在一级字段(point、building、room)上;小程序双向绑定不支持houseDetail.point这种路径。 - 修法:
this.setData({ ...houseDetail })或this.setData(houseDetail)把对象平铺到data一级。
添加成功后回退到分步表单中间页,而不是房屋列表
- 现象:提交成功,页面退到“填写房间号”那一步,列表页没刷新到新数据。
- 原因:分步表单是 4 层页面栈,
delta数少了;或者用redirectTo只替换当前页,中间页面全留在栈里。 - 修法:从入口数到列表页共 4 级,
wx.navigateBack({ delta: 4 });不要用reLaunch图省事,会丢掉列表页状态。
页面里写了两个
onLoad,新增流程突然坏了- 现象:改造编辑回显后,从分步表单进来添加房屋,
point/building/room不再渲染。 - 原因:
Page({})里后写的onLoad覆盖了前一个,参数只剩id。 - 修法:合并进同一个
onLoad,用if (id) return this.getHouseDetail(id)做分支,其余参数继续走新增渲染。
- 现象:改造编辑回显后,从分步表单进来添加房屋,
状态标签全显示 / 一个都不显示
- 现象:同一条房屋三个标签叠在一起;或状态明明是 2 却显示“正在审核”。
- 原因:三个状态都写了
wx:if而不是wx:if / wx:elif / wx:else;或后端返回数字 2,模板里写成status === '2'。 - 修法:改成
wx:if="{{status === 1}}"/wx:elif="{{status === 2}}"/wx:else,注意不要加引号包数字。
删除后列表没变化,或删掉了别的行
- 现象:弹窗点确认,接口 10000,但页面上那条还在;或者删掉了相邻一条。
- 原因:
this.data.houseList.splice()后没setData;index用的是渲染时的旧索引,列表变动后错位。 - 修法:
mark:id和mark:index一起传并存在实例上,splice后立即setData({ houseList, isEmpty })。
辅助接口改了状态,列表页还是旧状态
- 现象:调完辅助接口,重新进入小程序能看到变化,但从详情返回列表看不到。
- 原因:列表数据写在
onLoad里,只执行一次。 - 修法:数据会变的页面用
onShow拉取;onLoad只放一次性逻辑。
练习
上传逻辑复用:把身份证正反面合并成一个
uploadPicture处理函数,用mark:type区分,落到this.setData({ [type + 'Url']: data.data })。要求不用 if/else 分支写两份setData,并说明中括号在这里的作用。参数清洗:写一个
submitForm,先校验再提交。要求:剔除__webviewId__、status等非接口字段;说明新增与编辑分别需要传哪些字段;成功后计算并写出正确的delta,并在注释里写清为什么不用navigateTo回列表。状态渲染改造:把房屋列表里的三个状态从并列
wx:if改成wx:if / wx:elif / wx:else,接上tag info/tag success/tag fail。改完后用辅助接口把某条房屋依次改成 1、2、3,验证文案和颜色都正确。编辑回显:实现
onLoad({ id, point, building, room })的分支逻辑。要求说明:为什么有id时不需要point/building/room;回显时为什么不能存成嵌套对象;以及页面标题怎么从“添加房屋”变成“编辑房屋信息”。删除交互:用
van-swipe-cell+van-dialog实现滑动删除。要求回答:为什么id和index存在this上而不是data上;swipeClose里为什么必须调instance.close()。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 添加房屋-上传身份证图-国徽面 | 09:47 |
| 2 | 添加房屋-提交表单和后退多级 | 16:25 |
| 3 | 房屋列表-列表渲染 | 09:46 |
| 4 | 房屋列表-房屋审核状态 | 09:23 |
| 5 | 房屋列表-辅助修改状态接口 | 06:41 |
| 6 | 房屋详情-房屋信息渲染 | 10:32 |
| 7 | 房屋编辑-表单回显 | 10:50 |
小结
- 同一段上传逻辑服务多个入口时,用
mark:*传参区分,再配合对象动态 key([type + 'Url'])落到不同字段,比复制两遍函数更可靠。 - 请求体不能直接丢
this.data:小程序的data混有框架内部字段,提交前必须显式剔除;编辑场景额外剔除status。 - 编辑页与新增页共用同一个表单时,靠 URL 上有没有
id分流;分流必须写在同一个onLoad里,否则会互相覆盖。 - 表单双向绑定要求字段平铺在
data一级,所以回显用展开赋值,不要嵌套成houseDetail.xxx。 - 分步表单提交成功后用
navigateBack多级回退,既回到正确页面,又把中间页销毁;页面栈有 10 层上限,别靠不断navigateTo堆叠。 - 后端状态字段一律是数字,前端用
wx:if / wx:elif / wx:else翻译成文案 + 类名;这套模式可平移到会员等级、订单状态等任意多态渲染。 - 涉及状态、列表这类会被其他页面改动的数据,用
onShow而非onLoad拉取;开发阶段用后端辅助接口造数据,上线前会被屏蔽,不要写进业务逻辑。