Skip to content

房屋管理与报修(二) ​

本页对应 房屋管理与报修,共 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漏”;空状态初始值、接口失败分支、登录失效分支都要有反馈,避免用户看到空白页。

课件图示 ​

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

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

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

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

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

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

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

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

代码示例 ​

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="&#123;&#123;item.id&#125;&#125;" 是小程序里传递自定义数据的方式,接收时用 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' 必不可少,否则左滑关闭时也会弹窗。

常见错误 ​

  1. 点国徽面却更新到正面字段(或 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 时确认没有阻止到自身的点击。
  2. 提交表单后端报 ID is not allowed to be empty / 参数校验失败

    • 现象:新增房屋点提交,接口返回参数错误;或编辑时后端把状态也改了。
    • 原因:直接把整个 this.data 当请求体,小程序自动注入的 __webviewId__ 等内部字段被一起提交;编辑场景还多带了 status。
    • 修法:const { __webviewId__, status, id, ...body } = this.data 后再 post('/room', body);新增时 id 一并剔除,编辑时按接口要求单独传 id。
  3. 编辑页表单空白,数据没回显

    • 现象:从详情点“修改房屋信息”进来,输入框全是空的,但接口确实返回了数据。
    • 原因:把详情存成了 this.setData({ houseDetail }),而表单是 model:value 绑在一级字段(point、building、room)上;小程序双向绑定不支持 houseDetail.point 这种路径。
    • 修法:this.setData({ ...houseDetail }) 或 this.setData(houseDetail) 把对象平铺到 data 一级。
  4. 添加成功后回退到分步表单中间页,而不是房屋列表

    • 现象:提交成功,页面退到“填写房间号”那一步,列表页没刷新到新数据。
    • 原因:分步表单是 4 层页面栈,delta 数少了;或者用 redirectTo 只替换当前页,中间页面全留在栈里。
    • 修法:从入口数到列表页共 4 级,wx.navigateBack({ delta: 4 });不要用 reLaunch 图省事,会丢掉列表页状态。
  5. 页面里写了两个 onLoad,新增流程突然坏了

    • 现象:改造编辑回显后,从分步表单进来添加房屋,point/building/room 不再渲染。
    • 原因:Page({}) 里后写的 onLoad 覆盖了前一个,参数只剩 id。
    • 修法:合并进同一个 onLoad,用 if (id) return this.getHouseDetail(id) 做分支,其余参数继续走新增渲染。
  6. 状态标签全显示 / 一个都不显示

    • 现象:同一条房屋三个标签叠在一起;或状态明明是 2 却显示“正在审核”。
    • 原因:三个状态都写了 wx:if 而不是 wx:if / wx:elif / wx:else;或后端返回数字 2,模板里写成 status === '2'。
    • 修法:改成 wx:if="&#123;&#123;status === 1&#125;&#125;" / wx:elif="&#123;&#123;status === 2&#125;&#125;" / wx:else,注意不要加引号包数字。
  7. 删除后列表没变化,或删掉了别的行

    • 现象:弹窗点确认,接口 10000,但页面上那条还在;或者删掉了相邻一条。
    • 原因:this.data.houseList.splice() 后没 setData;index 用的是渲染时的旧索引,列表变动后错位。
    • 修法:mark:id 和 mark:index 一起传并存在实例上,splice 后立即 setData({ houseList, isEmpty })。
  8. 辅助接口改了状态,列表页还是旧状态

    • 现象:调完辅助接口,重新进入小程序能看到变化,但从详情返回列表看不到。
    • 原因:列表数据写在 onLoad 里,只执行一次。
    • 修法:数据会变的页面用 onShow 拉取;onLoad 只放一次性逻辑。

练习 ​

  1. 上传逻辑复用:把身份证正反面合并成一个 uploadPicture 处理函数,用 mark:type 区分,落到 this.setData({ [type + 'Url']: data.data })。要求不用 if/else 分支写两份 setData,并说明中括号在这里的作用。

  2. 参数清洗:写一个 submitForm,先校验再提交。要求:剔除 __webviewId__、status 等非接口字段;说明新增与编辑分别需要传哪些字段;成功后计算并写出正确的 delta,并在注释里写清为什么不用 navigateTo 回列表。

  3. 状态渲染改造:把房屋列表里的三个状态从并列 wx:if 改成 wx:if / wx:elif / wx:else,接上 tag info / tag success / tag fail。改完后用辅助接口把某条房屋依次改成 1、2、3,验证文案和颜色都正确。

  4. 编辑回显:实现 onLoad({ id, point, building, room }) 的分支逻辑。要求说明:为什么有 id 时不需要 point/building/room;回显时为什么不能存成嵌套对象;以及页面标题怎么从“添加房屋”变成“编辑房屋信息”。

  5. 删除交互:用 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

小结 ​

  1. 同一段上传逻辑服务多个入口时,用 mark:* 传参区分,再配合对象动态 key([type + 'Url'])落到不同字段,比复制两遍函数更可靠。
  2. 请求体不能直接丢 this.data:小程序的 data 混有框架内部字段,提交前必须显式剔除;编辑场景额外剔除 status。
  3. 编辑页与新增页共用同一个表单时,靠 URL 上有没有 id 分流;分流必须写在同一个 onLoad 里,否则会互相覆盖。
  4. 表单双向绑定要求字段平铺在 data 一级,所以回显用展开赋值,不要嵌套成 houseDetail.xxx。
  5. 分步表单提交成功后用 navigateBack 多级回退,既回到正确页面,又把中间页销毁;页面栈有 10 层上限,别靠不断 navigateTo 堆叠。
  6. 后端状态字段一律是数字,前端用 wx:if / wx:elif / wx:else 翻译成文案 + 类名;这套模式可平移到会员等级、订单状态等任意多态渲染。
  7. 涉及状态、列表这类会被其他页面改动的数据,用 onShow 而非 onLoad 拉取;开发阶段用后端辅助接口造数据,上线前会被屏蔽,不要写进业务逻辑。