Skip to content

房屋管理与报修(一) ​

本页对应 房屋管理与报修,共 7 段视频,约 1 小时 15 分。

学习目标 ​

  1. 能使用 git log、git checkout <提交号> 或 VS Code GitLens 将本地代码切换到指定提交,并验证当前提交记录与目标版本一致。
  2. 能为需登录的小程序页面接入 auth 鉴权,并在未登录时重写 onLoad、onShow、onReady 等生命周期,验证页面跳转登录后不再执行定位、搜索等原业务请求。
  3. 能依据接口文档在 data 中声明添加房屋所需字段,并使用 model:value 双向绑定收集姓名、手机号、性别等表单数据,通过调试器验证字段值实时同步。
  4. 能配置 wechat-validate 校验规则并在提交时调用 validate 校验姓名、手机号等字段,验证非法输入被拦截、合法输入通过。
  5. 能调用 chooseMedia 选择身份证图片,使用封装后的 upload 按接口要求上传并将返回 URL 更新到页面;能使用 catchtap 阻止子元素事件冒泡,验证删除图片时不再触发上传。
  6. 能在上传前读取临时文件 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。

课件图示 ​

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

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

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

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

代码示例 ​

示例 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="&#123;&#123;houseList.length < 5&#125;&#125;" 控制“添加新房屋”按钮的显隐。
  • 跳详情用 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="&#123;&#123;form.name&#125;&#125;" />,输入后 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
2auth鉴权组件-重写页面生命周期11:37
3添加房屋-双向绑定收集表单数据15:16
4添加房屋-表单校验10:59
5添加房屋-上传身份证图14:27
6小程序事件系统-阻止冒泡04:51
7添加房屋-上传图片-图片大小限制06:04

小结 ​

  1. 小程序的页面 JS 一定先于路由跳转执行,凡是"未登录不该跑"的逻辑,靠 wx:if 藏结构是不够的,必须把页面生命周期本身覆盖掉。
  2. 凡是"从别处返回后需要看到最新数据"的列表,查询写 onShow 而不是 onLoad,这是本项目里最容易被忽略的一条。
  3. 表单数据要先按接口文档对齐字段名和类型(gender 是 number、必传字段一个都不能少),再谈收集方式;字段名写错的返工成本远高于多看一眼文档。
  4. 简易双向绑定只服务于扁平 data,字段一多就老老实实拍平,别为了"好看"去嵌套对象。
  5. 前端校验的定位是"拦住明显不合规",正则要写宽松些(尤其手机号),不要把真实存在的号码挡在门外;validate() 的返回值必须显式判断并 return。
  6. 上传图片是一条完整链路:选图 → 体积预判 → 上传 → 解析响应 → 回写字段 → 可删除,任何一环缺失都会表现为"传不上去"或"控制台红一片"。
  7. 事件冒泡在小程序里同样存在,只是没有 .stop 修饰符,用 catch:tap 代替 bind:tap;写嵌套结构时先想清楚这一层点击该由谁响应。
  8. 位置服务、上传、表单校验这类能力都是"外部依赖 + 配额/体积限制",开发期就要把失败分支(无权限、域名未配、超限、用户取消)走一遍,别等联调时才补。