Skip to content

报修业务(一) ​

本页对应 报修业务,共 8 段视频,约 1 小时 22 分。

学习目标 ​

  1. 能使用 ActionSheet、Popup、DatetimePicker 组件完成房屋选择、维修项目选择、预约时间选择等交互,并正确绑定显示状态、数据源及 select/confirm/cancel/close 事件。
  2. 能调用房屋列表与维修项目接口获取数据,将返回数据渲染到 ActionSheet,并在选择后更新页面展示及根据选中状态切换文字样式。
  3. 能限制预约时间不早于当天,获取用户选择的时间戳,并封装日期格式化方法输出“年-月-日”格式且补齐小于 10 的月份和日期。
  4. 能使用 Uploader 组件配置文件列表、上传数量限制和预览尺寸,在 after-read 事件中调用上传接口,将上传成功的图片追加到附件数组并实现预览。
  5. 能获取手机号、问题描述等表单数据,定义并应用验证规则,在提交时根据验证结果决定是否调用新增报修接口。
  6. 能按接口要求提交房屋、维修项目、手机号、预约时间、问题描述和附件参数,根据返回 code 判断成功与否,并在成功后跳转到报修列表页面。

知识点 ​

在线报修页面要完成什么 ​

为什么需要:业主报修时要填写房屋、维修项目、联系电话、上门时间和问题描述,还可能上传图片;这些信息必须能选择、能校验、能提交。
怎么想:页面先获取“可报修房屋”和“维修项目”两类基础数据,再用 Vant 弹层组件完成选择,最后统一校验并提交。
怎么做:本页用到 van-action-sheet 展示列表选择,van-popup 作为底部弹层容器,van-datetimepicker 选择日期,van-uploader 上传附件。进入页面前要保证用户已登录;房屋列表只返回审核通过的房屋。
易错点:van-action-sheet 的 actions 数据项必须包含 name 才能显示文字;控制显示隐藏的是 show,不是“受”;未登录时不要继续发请求。

获取并选择房屋列表 ​

为什么需要:报修必须落到具体房屋上,所以要先查询当前用户可报修的房屋。
怎么想:页面加载时调 /house,把返回数组交给 actions;用户点选后监听 select,从 ev.detail 取 id 和 name。
怎么做:

javascript
// repair_pkg/pages/form/index.js
Page({
  data: {
    houseList: [],
    houseId: '',
    houseName: ''
  },
  async getHouseList() {
    const { code, data: houseList } = await wx.http.get('/house')
    if (code !== 10000) return wx.utils.toast()
    this.setData({ houseList })
  },
  selectHouseInfo(ev) {
    this.setData({
      houseId: ev.detail.id,
      houseName: ev.detail.name,
    })
  },
})
xml
<van-action-sheet
  bind:close="closeHouseLayer"
  bind:cancel="closeHouseLayer"
  bind:select="selectHouseInfo"
  round
  show="{{houseLayerVisible}}"
  actions="{{houseList}}"
  cancel-text="取消"
  title="选择报修房屋"
/>

<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>

易错点:close、cancel 都要负责关闭弹层;value-class 要用条件,否则初始提示文字也会变成深色。调试时先 console.log(ev.detail) 确认字段,不要猜。

获取并选择维修项目 ​

为什么需要:维修项目决定师傅上门处理什么问题。
怎么想:与房屋选择同构:接口取数组,actions 渲染,select 取选择结果。
怎么做:

javascript
Page({
  data: {
    repairItem: [],
    repairItemId: '',
    repairItemName: ''
  },
  onLoad() {
    this.getRepairItem()
  },
  async getRepairItem() {
    const { code, data: repairItem } = await wx.http.get('/repairItem')
    if (code !== 10000) return wx.utils.toast()
    this.setData({ repairItem })
  },
  selectRepairItemInfo(ev) {
    this.setData({
      repairItemId: ev.detail.id,
      repairItemName: ev.detail.name,
    })
  },
})
xml
<van-action-sheet
  bind:close="closeRepairLayer"
  bind:cancel="closeRepairLayer"
  bind:select="selectRepairItemInfo"
  round
  show="{{repairLayerVisible}}"
  actions="{{repairItem}}"
  cancel-text="取消"
  title="选择维修项目"
/>

<van-cell
  title-width="100"
  title="维修项目"
  value-class="{{repairItemName && 'active-cell'}}"
  bind:click="openRepairLayer"
  value="{{repairItemName || '请选择维修项目'}}"
  is-link
/>

易错点:事件名必须是 selectRepairItemInfo,转写中曾把 select 误写成 get 导致不打印;字段名 repairItemId 提交时也不能写错。

预约时间与日期格式化 ​

为什么需要:上门时间必须是将来时间,且展示要友好。
怎么想:用 van-popup 从底部包住 van-datetimepicker,min-date 设为当天,confirm 取时间戳并格式化。
怎么做:

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,
  })
}

易错点:ev.detail 是时间戳;格式化时月份要 +1,小于 10 要补 0;点“确认”后也要关闭弹层。

上传附件 ​

为什么需要:图片能帮助维修师傅快速判断问题。
怎么想:file-list 保存已上传成功图片,after-read 拿到文件后调用 wx.uploadFile,成功后追加到 attachment。
怎么做:

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 字符串,要 JSON.parse;filePath 用 file.url。

表单校验与提交 ​

为什么需要:避免空房屋、空项目、错误手机号直接提交。
怎么想:用 wechat-validate 定义规则,提交前 this.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' })
  },
})

易错点:附件不是必填项,不必加规则;validate() 返回假要直接 return;提交参数名必须与接口一致,转写中曾把 repairItemId 写错导致失败。提交成功后跳转报修列表页。

先明确目标:报修列表要展示什么 ​

为什么需要:用户提交在线报修后,需要在一个页面里集中看到自己报修过的记录,并能继续进入详情或新增报修。
怎么想:列表页的核心不是“写死几条数据”,而是“进入页面就请求接口,把接口返回的 rows 渲染出来”。
怎么做:在 onLoad 中调用 getRepairList,接口使用 wx.http.get('/repair', { current: 1, pageSize: 10 }),成功条件按接口文档判断为 code === 10000。
易错点:转写稿里“季录”应理解为“记录”;不要把接口返回的 data.rows 误写成 data 本身,否则 wx:for 拿不到数组。

接口文档地址保留如下:接口文档地址

登录态与页面结构 ​

为什么需要:报修列表接口要求用户保持登录状态,未登录时直接请求没有意义,也会得到失败结果。
怎么想:列表页应先用 authorization 组件包住需要登录后才能看的内容,未登录时由组件引导去登录;登录后再请求列表。转写稿中也强调过:用户没有登录时不允许在线报修,登录检测用 authorization 组件。
怎么做:模板中用 <authorization> 包裹列表主体和空状态分支;onLoad 里请求列表。
易错点:不要把弹层、隐藏内容随意包进授权区;转写稿说“默认隐藏的弹层不需要包裹”,列表主体才需要。还要注意“受”是 show 的口语误写,本节不涉及弹层显示控制。

调用接口获取列表 ​

为什么需要:列表数据来自服务端,不能靠前端假数据。
怎么想:先封装 getRepairList,在方法里请求接口,再判断 code,最后 setData 渲染。
怎么做:保留示例代码:

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,
    })
  },
})

易错点:code !== 10000 时直接 return,不要再 setData;rows 可能为空数组,必须同时维护 isEmpty。转写稿中“接口是否用成功”应纠正为“接口是否调用成功”。

空状态判断 ​

为什么需要:用户没有报修记录时,如果只渲染空列表,页面会显得像坏掉了。
怎么想:用 isEmpty 作为分支条件:有数据渲染列表,无数据渲染提示和“添加”入口。
怎么做:模板中用 <block wx:if="&#123;&#123;!isEmpty&#125;&#125;"> 包列表,<view wx:else class="blank"> 显示“您还没有报修记录,请点击 添加”。
易错点:isEmpty 要在接口成功后根据 repairList.length === 0 计算;不要只写 wx:if="&#123;&#123;repairList.length&#125;&#125;" 而忘记空状态文案。转写稿中“添加”入口对应的是报修表单页,路径以模板中的 navigator 为准。

状态标签渲染 ​

为什么需要:报修记录有不同进度,用户要一眼看出当前状态。
怎么想:接口状态是数字,模板按 status 分支显示文字和类名。
怎么做:状态对应关系为:0 已取消,类名 cancel;1 受理中,类名 info;2 上门中,类名 success;3 已完成,类名 complete。
易错点:不要用 = 判断,要写 ===;四个 wx:if 要覆盖完整,避免状态值异常时什么都不显示。转写稿中“内幕”是 name 的误写,模板里真正使用的是 item.status、item.houseInfo 等字段。

列表模板与交互入口 ​

为什么需要:列表不仅要展示,还要能点进详情、点底部按钮新增报修。
怎么想:用 van-cell-group 循环 repairList,wx:key="id";每个分组绑定 bind:tap="goDetail",底部工具栏绑定 bind:tap="addRepair"。
怎么做:保留模板代码:

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>

易错点:模板里已经绑定了 goDetail 和 addRepair,页面逻辑中要补齐对应方法,否则点击无响应;wx:key 要用唯一 id,不要用索引。调试时按转写稿思路:先看是否发出 /repair 请求,再看返回 code,再看 rows 是否进入 repairList,最后检查空状态和状态标签。提交报修成功后转写稿使用跳转到列表页,因此列表页 onLoad 立即请求是合理的调试入口。

先明确报修详情页要解决什么问题 ​

报修详情页不是孤立页面,它通常从报修列表点击某条记录进入。列表页只知道自己点了“哪一条”,详情页则需要根据这条记录的报修 ID 去服务端获取完整数据。因此这里有两个核心动作:第一,在页面加载时拿到地址参数中的 id;第二,用这个 id 调详情接口,把返回的报修信息渲染到页面。

为什么要强调“地址参数”?因为详情页不能把某一条报修数据写死。列表中的每条记录都有自己的 ID,跳转时把 ID 带过来,详情页才能复用同一个页面展示不同记录。怎么想?先看页面生命周期 onLoad,它接收页面参数;怎么做?在 onLoad({ id }) 中解构出 ID,再调用 getRepairDetail(id)。易错点是:不要直接假设 id 一定存在,参数缺失时要给出提示并终止请求。

javascript
// repair_pkg/pages/detail/index.js
Page({
  data: {
    repairDetail: {}
  },
  onLoad({ id }) {
    // 获取报修详情的数据
    this.getRepairDetail(id)
  },
  // 报修详情接口
  async getRepairDetail(id) {
    if (!id) return wx.utils.toast('参数有误!')
    // 调用接口
    const { code, data: repairDetail } = await wx.http.get('/repair/' + id)
    // 检测接口是否调用成功
    if (code !== 10000) return wx.utils.toast()
    // 渲染数据
    this.setData({ repairDetail })
  },
})

调详情接口时先校验参数,再判断业务状态码 ​

为什么需要先判断 id?因为详情接口路径是 /repair/ + id,如果 id 为空,请求路径就没有意义,既浪费一次请求,也不利于排查问题。怎么想?把“参数校验”放在请求之前,把“接口业务状态码校验”放在请求之后。怎么做?if (!id) return wx.utils.toast('参数有误!') 先兜底;请求返回后解构 code 和 data,只有 code === 10000 才 setData。易错点有两个:一是把 data 重命名为 repairDetail 后,渲染时仍要使用 repairDetail;二是接口失败时不要继续渲染,否则页面可能显示空数据或旧数据。转写稿中演示过从报修列表点记录进入详情,详情要展示报修的详细信息和当前状态,这里的接口调用就是数据来源。

用状态字段驱动详情页展示 ​

详情页除了展示房屋信息、维修项目、手机号、预约日期、问题描述、附件外,还要展示当前报修状态。笔记中用 repairDetail.status 判断:0 已取消、1 受理中、2 上门中、3 已完成。为什么需要状态标签?用户进入详情后最关心“现在处理到哪一步”。怎么想?状态是数据,不要用多个布尔变量重复描述;直接用 status 做条件渲染。怎么做?在 van-cell 中并列写多个 wx:if,每个状态对应不同 class。易错点是:wx:if="&#123;&#123;repairDetail.status === 0&#125;&#125;" 是严格等于,接口若返回字符串 "0" 就不会命中,需要确认接口字段类型;另外初始 repairDetail 是 {},首次渲染时状态标签不会出现,这是正常的,数据回来后会更新。

只有“上门中”才展示地图 ​

笔记特别提醒:当报修状态为上门中,即 status 值为 2 时,才展示地图组件。为什么?因为“上门中”阶段才更需要位置/路线信息,其他状态展示地图意义不大。怎么想?把地图区域包在 wx:if="&#123;&#123;repairDetail.status === 2&#125;&#125;" 的容器里。怎么做?外层 .polyline 条件渲染,内部放 map,并设置 scale、latitude、longitude。易错点是:笔记中的 map 使用了 latitude 和 longitude,但示例 setData 只设置了 repairDetail,实际项目中要确认详情数据里是否包含经纬度,或是否在页面中另行赋值;如果没有经纬度,地图可能不显示或定位异常。不要无条件渲染地图,否则不符合状态条件。

底部操作区要按状态控制“取消报修” ​

底部工具栏有“修改信息”和“取消报修”。笔记要求:只有在线报修状态为受理中,即 status === 1 时,才允许取消报修。为什么?因为已取消、上门中、已完成再取消都不合理,业务上应限制入口。怎么想?把“取消报修”按钮用 wx:if="&#123;&#123;repairDetail.status === 1&#125;&#125;" 控制;“修改信息”按钮绑定 bind:tap="editRepair"。怎么做?按笔记保留结构即可。易错点是:editRepair 方法需要在页面逻辑中实现,笔记只给渲染代码,不能误以为只写模板就能完成修改;另外不要用 status == 1 代替严格判断,避免类型不一致导致按钮显示错误。

xml
<scroll-view scroll-y enhanced show-scrollbar="{{false}}">
  <view class="repair-detail">
    <view wx:if="{{repairDetail.status === 2}}" class="polyline">
      <map
        style="width: 100%; height: 100%;"
        scale="15"
        latitude="{{latitude}}"
        longitude="{{longitude}}"
      ></map>
    </view>
    <van-cell-group border="{{false}}" title="房屋信息">
      <van-cell title="{{repairDetail.houseInfo}}">
        <text wx:if="{{repairDetail.status === 0}}" class="tag cancel">已取消</text>
        <text wx:if="{{repairDetail.status === 1}}" class="tag info">受理中</text>
        <text wx:if="{{repairDetail.status === 2}}" class="tag success">上门中</text>
        <text wx:if="{{repairDetail.status === 3}}" class="tag complete">已完成</text>
      </van-cell>
    </van-cell-group>
    <van-cell-group title="报修信息" border="{{false}}">
      <van-cell title-width="200rpx" title="维修项目" value="{{repairDetail.repairItemName}}" />
      <van-cell title-width="200rpx" title="手机号码" value="{{repairDetail.mobile}}" />
      <van-cell title-width="200rpx" title="预约日期" value="{{repairDetail.appointment}}" />
      <van-cell title="问题描述" label="{{repairDetail.description}}" />
    </van-cell-group>
    <view class="attachment">
      <view class="title">问题附件</view>
      <scroll-view scroll-x>
        <image wx:for="{{repairDetail.attachment}}" wx:key="id" src="{{item.url}}"></image>
      </scroll-view>
    </view>
  </view>
</scroll-view>
<view class="toolbar">
  <view bind:tap="editRepair" class="button-text active">修改信息</view>
  <view wx:if="{{repairDetail.status === 1}}" class="button-text">取消报修</view>
</view>

调试时按“参数—接口—渲染—状态”顺序排查 ​

调试详情页时,先确认从列表跳转时是否带上了 id,再看 onLoad 是否收到;然后看请求路径是否为 /repair/具体ID,返回 code 是否为 10000;最后看 repairDetail 是否被 setData 更新。若页面空白,优先检查 id 和接口返回;若状态标签或取消按钮不出现,优先检查 status 的类型和值;若地图不显示,优先检查 status 是否为 2 以及 latitude、longitude 是否有值。转写稿中强调详情页要展示报修详细信息和当前状态,并可修改、取消报修,因此模板中的状态判断和底部操作条件都要与业务规则一致,不能只做静态页面。

路线规划要解决什么问题 ​

路线规划的目标是:在地图上把两个点之间的行进路线画出来。笔记把它拆成两部分:小程序 map 组件负责“显示地图、标记点、路线”,腾讯位置服务负责“算出路线经过的经纬度点”。

为什么要这样拆?因为 map 只负责渲染,不会替你计算路线;位置服务只返回坐标数据,不会直接显示。怎么想?先把地图和标记点显示出来,再把位置服务返回的坐标点交给 polyline。怎么做?先掌握 map 的 latitude、longitude、scale、markers、polyline,再调用路线规划方法。易错点是:不要把“地图中心点”和“标记点”混为一谈,也不要把路线坐标直接塞给 map。

先用 map 组件把地图和标记点渲染出来 ​

latitude 指定地图中心点纬度,longitude 指定地图中心点经度,scale 指定初始缩放比例,取值范围 3–20。markers 是地图上的标记点,polyline 是地图上的路线。

为什么需要 markers?因为用户要知道起点、终点或关键位置在哪里。怎么想?每个标记点至少要有唯一 id、经纬度和尺寸;需要自定义图标时用 iconPath。怎么做?笔记示例:

javascript
// repqir_pkg/pages/detail/index.js
Page({
  data: {
    markers: [
      {
        id: 1,
        latitude: 40.22077,
        longitude: 116.23128,
        width: 24,
        height: 30,
      },
      {
        id: 2,
        latitude: 40.225857999999995,
        longitude: 116.23246699999999,
        iconPath: '/static/images/marker.png',
        width: 40,
        height: 40,
      },
    ],
  }
})

易错点:笔记明确说“每个标记点必须要指定 ID 属性,否则会有错误产生”;iconPath、width/height 要按实际图片和尺寸配置。转写稿里反复强调“先保存代码测试、看事件对象/返回数据里到底有什么”,这里同样适用:先把 markers 渲染出来,再处理路线,不要一次混在一起排错。

用 polyline 画出路线 ​

polyline 用来展示两点间的行进路线,它需要的是“很多个坐标点组成的线”。为什么不能只给起点终点?因为真实道路不是直线,必须由位置服务算出一串路径点。怎么想?先拿到 points 数组,再交给 polyline,同时设置颜色和线宽。怎么做?笔记在成功回调里组装:

javascript
polyline: [
  {points, color: '#5591af', width: 4},
]

易错点:polyline 的 points 必须是 { latitude, longitude } 形式;颜色、宽度是显示配置,不要和坐标数据混在一起。

调用腾讯位置服务做路线规划 ​

为什么需要位置服务?因为路线坐标不是手写的,而是由路线规划接口计算。怎么想?调用 qqMap.direction,指定出行方式 mode、起点 from、终点 to,在 success 里处理返回结果。怎么做?笔记代码:

javascript
// repqir_pkg/pages/detail/index.js
Page({
  data: {},
  onLoad() {
    // 生成路线
    this.getPolyline()
  },
  // 调用位置服务(路线规划)
  getPolyline() {
    qqMap.direction({
      mode: 'bicycling',
      from: '40.227978,116.22998',
      to: '40.22077,116.23128',
      success: ({ result }) => {
        const coors = result.routes[0].polyline
        const points = []
        //坐标解压(返回的点串坐标,通过前向差分进行压缩)
        for (let i = 2; i < coors.length; i++) {
          coors[i] = Number(coors[i - 2]) + Number(coors[i]) / 1000000
        }
        // 获取经纬度
        for (let i = 0; i < coors.length; i += 2) {
          points.push({ latitude: coors[i], longitude: coors[i + 1] })
        }
        // 渲染数据
        this.setData({
          latitude: points[30].latitude,
          longitude: points[30].longitude,
          polyline: [
            {points, color: '#5591af', width: 4},
          ],
        })
      },
    })
  },
})

易错点:result.routes[0].polyline 要先确认存在;mode、from、to 的格式要和文档一致;points[30] 依赖返回点数量,调试时先打印 points.length,避免越界。

坐标解压与二次加工 ​

为什么返回坐标还要处理?笔记说明:计算出来的坐标点是经过压缩的,要按官方指定方式解压,才能得到真正坐标点;为了适应小程序地图组件,还要二次加工成 { latitude, longitude }。怎么想?先做前向差分解压,再两两一组转成对象。怎么做?就是上面两个循环:第一个循环从 i = 2 开始累加还原,第二个循环步长为 2 组装经纬度。易错点:解压循环从 2 开始、除以 1000000、Number 转换都不能漏;i += 2 时不要误取 coors[i + 1] 越界。笔记也提醒:这些处理是腾讯位置服务规定好的,开发者按官方文档实现即可。

调试与易错点 ​

转写稿没有单独演示路线规划,但其中反复出现的调试习惯可以迁移:先 log 看返回结构,再 setData 渲染;接口或回调里先根据返回结果判断成功与否;名字、参数、路径要逐一核对,转写稿里就出现过方法名写成 get、参数名写错导致失败的情况。路线规划同样要这样排:先确认 qqMap.direction 是否成功,再确认 polyline 是否存在,再确认解压后的 points 数量和首尾坐标,最后才看地图显示。

还要注意笔记中的说明:课堂项目简化了起点和终点,真实企业项目中起点、终点通常由接口提供;这不影响本知识点掌握。把地图渲染、标记点、路线数据、坐标解压四件事分开调试,比一次性排错更稳。

课件图示 ​

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

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

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

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

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

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

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

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

代码示例 ​

一、在线报修表单(房屋 / 维修项目 / 预约时间 / 附件 / 校验 / 提交) ​

xml
<!-- repair_pkg/pages/form/index.wxml -->
<authorization>
  <!-- 选择报修房屋 -->
  <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>

  <!-- 选择维修项目 -->
  <van-cell
    title-width="100"
    title="维修项目"
    value-class="{{repairItemName && 'active-cell'}}"
    bind:click="openRepairLayer"
    value="{{repairItemName || '请选择维修项目'}}"
    is-link
  />

  <!-- 选择预约时间 -->
  <van-cell
    title-width="100"
    title="预约时间"
    value-class="{{appointment && 'active-cell'}}"
    bind:click="openDateLayer"
    value="{{appointment || '请选择预约时间'}}"
    is-link
  />

  <!-- 联系电话、问题描述 -->
  <van-field model:value="{{mobile}}" label="联系电话" placeholder="请填写联系电话" />
  <van-field model:value="{{description}}" label="问题描述" type="textarea" placeholder="请填写问题描述" />

  <!-- 上传附件 -->
  <van-uploader
    bind:after-read="uploadPicture"
    max-count="6"
    preview-size="100"
    file-list="{{attachment}}"
  />

  <view class="toolbar">
    <view class="button-text" bind:tap="submitForm">提交报修</view>
  </view>

  <!-- 房屋列表弹层 -->
  <van-action-sheet
    bind:close="closeHouseLayer"
    bind:cancel="closeHouseLayer"
    bind:select="selectHouseInfo"
    round
    show="{{houseLayerVisible}}"
    actions="{{houseList}}"
    cancel-text="取消"
    title="选择报修房屋"
  />

  <!-- 维修项目弹层 -->
  <van-action-sheet
    bind:close="closeRepairLayer"
    bind:cancel="closeRepairLayer"
    bind:select="selectRepairItemInfo"
    round
    show="{{repairLayerVisible}}"
    actions="{{repairItem}}"
    cancel-text="取消"
    title="选择维修项目"
  />

  <!-- 日期选择弹层 -->
  <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>
</authorization>
javascript
// repair_pkg/pages/form/index.js
// 导入表单验证插件
import wxValidate from 'wechat-validate'

Page({
  behaviors: [wxValidate],
  data: {
    houseList: [],
    repairItem: [],
    houseId: '',
    houseName: '',
    repairItemId: '',
    repairItemName: '',
    mobile: '',
    appointment: '',
    description: '',
    attachment: [],
    // 三个弹层的显示状态
    houseLayerVisible: false,
    repairLayerVisible: false,
    dateLayerVisible: false,
    currentDate: Date.now(),
  },

  onLoad() {
    // 获取房屋列表、维修项目
    this.getHouseList()
    this.getRepairItem()
  },

  // 获取房屋列表(必须是通过审核的房屋)
  async getHouseList() {
    const { code, data: houseList } = await wx.http.get('/house')
    if (code !== 10000) return wx.utils.toast()
    this.setData({ houseList })
  },

  // 获取维修项目
  async getRepairItem() {
    const { code, data: repairItem } = await wx.http.get('/repairItem')
    if (code !== 10000) return wx.utils.toast()
    this.setData({ repairItem })
  },

  // 弹层显示 / 隐藏
  openHouseLayer() { this.setData({ houseLayerVisible: true }) },
  closeHouseLayer() { this.setData({ houseLayerVisible: false }) },
  openRepairLayer() { this.setData({ repairLayerVisible: true }) },
  closeRepairLayer() { this.setData({ repairLayerVisible: false }) },
  openDateLayer() { this.setData({ dateLayerVisible: true }) },
  closeDateLayer() { this.setData({ dateLayerVisible: false }) },

  // 获取用户选择的房屋
  selectHouseInfo(ev) {
    this.setData({
      houseId: ev.detail.id,
      houseName: ev.detail.name,
    })
  },

  // 获取用户选择的维修项目
  selectRepairItemInfo(ev) {
    this.setData({
      repairItemId: ev.detail.id,
      repairItemName: ev.detail.name,
    })
  },

  // 获取用户选择的预约日期
  selectDateInfo(ev) {
    this.setData({
      appointment: wx.utils.dataFormat(ev.detail),
      dateLayerVisible: false,
    })
  },

  // 上传文件
  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) => {
        // 处理返回的 json 数据
        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 })
      },
    })
  },

  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' })
  },
})

要点解释

  • van-action-sheet 的 actions 绑定数组数据,数组元素必须含 name 字段,否则列表项不显示文字;show 为 true 才显示弹层。
  • 三个弹层共用一套"显示/隐藏"套路:点击 cell → setData({ xxxVisible: true });select/cancel/close 三种事件里把对应变量置为 false。
  • select 事件通过 ev.detail 拿到用户选中项(含 id、name),分别存入 houseId/houseName、repairItemId/repairItemName。
  • value-class="&#123;&#123;houseName && 'active-cell'&#125;&#125;" 是短路运算:未选择时值为空串,不加类名(浅色);选择后才加 active-cell 变深色。value 里的 || 提供占位提示文字,这是易错点(很多人写成固定类名导致初始就是深色)。
  • van-datetime-picker 的 min-date="&#123;&#123;currentDate&#125;&#125;"(Date.now())把可选范围限制为当天及以后;confirm 事件 ev.detail 是时间戳,必须用 wx.utils.dataFormat 格式化成 年-月-日 再展示。
  • van-uploader 中 file-list 用于回显已上传图片、max-count="6" 限制数量(满 6 张后上传按钮自动隐藏)、preview-size="100" 控制预览区宽高。
  • wx.uploadFile 返回的 result.data 是 JSON 字符串,必须 JSON.parse 后判断 code;上传成功要把 data.data(含 url)push 进 attachment 再 setData 才回显。
  • behaviors: [wxValidate] + rules 配合 this.validate() 使用;附件不是必填项,因此不在 rules 中定义。
  • 提交时字段名必须与接口文档一致(如 repairItemId,写错成别的名字会报"参数不存在");成功后用 wx.redirectTo 跳列表页,且提交前要把 attachment 的示例默认图清空成空数组。

二、报修列表(含空状态与四种状态标签) ​

xml
<!-- repair_pkg/pages/list/index.wxml -->
<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>
javascript
// repair_pkg/pages/list/index.js
Page({
  data: {
    repairList: [],
    isEmpty: true,
  },
  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.rows,解构时要用 data: { rows: repairList } 改名,别直接写 data,否则渲染的是分页对象而不是数组。
  • isEmpty 必须由接口返回结果计算(repairList.length === 0),不能用 wx:if="&#123;&#123;repairList&#125;&#125;",空数组在模板里是"真值",会导致空白页面。
  • 状态与类名对应关系:0 → cancel(已取消)、1 → info(受理中)、2 → success(上门中)、3 → complete(已完成),用四个 wx:if 分别渲染。
  • wx:key="id" 与 mark:id 配合,方便后续在列表项上做定位/更新;整组绑定 bind:tap="goDetail" 跳详情。

三、报修详情 + 路线规划 ​

xml
<!-- repair_pkg/pages/detail/index.wxml -->
<scroll-view scroll-y enhanced show-scrollbar="{{false}}">
  <view class="repair-detail">
    <!-- 仅"上门中"才显示地图与路线 -->
    <view wx:if="{{repairDetail.status === 2}}" class="polyline">
      <map
        style="width: 100%; height: 100%;"
        scale="15"
        latitude="{{latitude}}"
        longitude="{{longitude}}"
        markers="{{markers}}"
        polyline="{{polyline}}"
      ></map>
    </view>

    <van-cell-group border="{{false}}" title="房屋信息">
      <van-cell title="{{repairDetail.houseInfo}}">
        <text wx:if="{{repairDetail.status === 0}}" class="tag cancel">已取消</text>
        <text wx:if="{{repairDetail.status === 1}}" class="tag info">受理中</text>
        <text wx:if="{{repairDetail.status === 2}}" class="tag success">上门中</text>
        <text wx:if="{{repairDetail.status === 3}}" class="tag complete">已完成</text>
      </van-cell>
    </van-cell-group>

    <van-cell-group title="报修信息" border="{{false}}">
      <van-cell title-width="200rpx" title="维修项目" value="{{repairDetail.repairItemName}}" />
      <van-cell title-width="200rpx" title="手机号码" value="{{repairDetail.mobile}}" />
      <van-cell title-width="200rpx" title="预约日期" value="{{repairDetail.appointment}}" />
      <van-cell title="问题描述" label="{{repairDetail.description}}" />
    </van-cell-group>

    <view class="attachment">
      <view class="title">问题附件</view>
      <scroll-view scroll-x>
        <image wx:for="{{repairDetail.attachment}}" wx:key="id" src="{{item.url}}"></image>
      </scroll-view>
    </view>
  </view>
</scroll-view>

<view class="toolbar">
  <view bind:tap="editRepair" class="button-text active">修改信息</view>
  <view wx:if="{{repairDetail.status === 1}}" class="button-text">取消报修</view>
</view>
javascript
// repair_pkg/pages/detail/index.js
Page({
  data: {
    repairDetail: {},
    markers: [
      {
        id: 1,
        latitude: 40.22077,
        longitude: 116.23128,
        width: 24,
        height: 30,
      },
      {
        id: 2,
        latitude: 40.225857999999995,
        longitude: 116.23246699999999,
        iconPath: '/static/images/marker.png',
        width: 40,
        height: 40,
      },
    ],
  },

  onLoad({ id }) {
    // 获取报修详情的数据
    this.getRepairDetail(id)
    // 生成路线
    this.getPolyline()
  },

  // 报修详情接口
  async getRepairDetail(id) {
    if (!id) return wx.utils.toast('参数有误!')
    const { code, data: repairDetail } = await wx.http.get('/repair/' + id)
    if (code !== 10000) return wx.utils.toast()
    this.setData({ repairDetail })
  },

  // 调用位置服务(路线规划)
  getPolyline() {
    qqMap.direction({
      mode: 'bicycling',
      from: '40.227978,116.22998',
      to: '40.22077,116.23128',
      success: ({ result }) => {
        const coors = result.routes[0].polyline
        const points = []
        // 坐标解压(返回的点串坐标,通过前向差分进行压缩)
        for (let i = 2; i < coors.length; i++) {
          coors[i] = Number(coors[i - 2]) + Number(coors[i]) / 1000000
        }
        // 获取经纬度
        for (let i = 0; i < coors.length; i += 2) {
          points.push({ latitude: coors[i], longitude: coors[i + 1] })
        }
        // 渲染数据
        this.setData({
          latitude: points[30].latitude,
          longitude: points[30].longitude,
          polyline: [{ points, color: '#5591af', width: 4 }],
        })
      },
    })
  },
})

要点解释

  • 详情 ID 来自地址参数,onLoad({ id }) 直接解构;没有 id 时提前 return 并提示,避免发出 /repair/undefined 这样的请求。
  • 地图与"取消报修"按钮都有条件:地图仅在 status === 2(上门中)展示,取消按钮仅在 status === 1(受理中)展示,用 wx:if 控制。
  • markers 中每个标记点必须指定 id,否则会报错;iconPath 可自定义图标,width/height 控制尺寸。
  • 路线坐标是压缩过的,必须按官方方式做前向差分解压:coors[i] = Number(coors[i-2]) + Number(coors[i]) / 1000000,再从下标 0 开始两两取 latitude/longitude,这是最容易出错的地方。
  • 解压后再二次加工成小程序 map 组件需要的 polyline: [{ points, color, width }];地图中心点取路线中间某点(如 points[30])的经纬度。
  • scale 取值范围 3–20,课程中用 15 观察效果较合适。

常见错误 ​

  1. 弹层打开后列表空白 现象:点"请选择报修房屋"能弹出弹层,但里面没有任何选项。 原因:van-action-sheet 只认每项里的 name 字段作为展示文案,接口字段名与它对不上时就渲染不出来。 修法:渲染前先 console.log 看数组结构,必要时 map 成 { name: item.xxx, ...item };同时给空数组补一个"暂无可选数据"的兜底。

  2. 选中后文字颜色不变,或初始状态就已是深色 现象:选了房屋,文字还是灰的;或者一进页面提示语就显示成选中色。 原因:value-class 写成了固定值,或条件表达式漏了判断。 修法:value-class="&#123;&#123;houseName ? 'active-cell' : ''&#125;&#125;",用三元而不是短路拼接,条件成立与否一目了然。

  3. 图片上传成功但页面没有新预览图 现象:接口返回 10000,页面仍只有旧图;连续上传后只保留一张。 原因:push 之后忘了重新 setData;或追加进去的对象没有 url 字段。 修法:const attachment = [...this.data.attachment, data.data] 后 this.setData({ attachment }),并确认追加项含 url。

  4. 上传接口 401,或 success 里取 code 得到 undefined 现象:图片传不上去,控制台报鉴权失败;或接口其实成功了却走进失败分支。 原因:header 里没带 token 或漏了 Bearer 前缀(含空格);result.data 是 JSON 字符串,不是对象。 修法:header: { Authorization: 'Bearer ' + getApp().token },取数据前先 JSON.parse(result.data)。

  5. 预约日期显示成一串数字 现象:确认日期后单元格里是 1699999999999 这类数字。 原因:datetimepicker 的 confirm 事件 detail 是时间戳,不是格式化字符串。 修法:统一走 wx.utils.dataFormat,实现时注意 getMonth() + 1 与个位数补零。

  6. 表单校验"永远通过"或"永远不通过" 现象:必填项没填也能提交;或者全填完了仍提示请选择报修房屋。 原因:behaviors 里没注册 wxValidate;或 rules 的键名与 data 字段名不一致;或 if (!this.validate()) return 判断写反。 修法:三处逐一核对——注册、键名同名、返回值取反后 return。

  7. 提交接口报参数错误,或列表里查不到刚提交的记录 现象:校验全过,提交却失败;或提交成功但字段对不上。 原因:提交时字段名与文档不一致(如 repairItemCode / repairItemId 混用),或解构时漏字段。 修法:提交前把 payload 打出来,逐字段与文档比对;解构写全六个字段。

  8. 详情页地图/路线不显示 现象:状态为上门中,但地图区域空白或报错。 原因:markers 缺 id;polyline 直接用了未解压的坐标串;status 用了字符串 '2' 与数字比较。 修法:marker 必带 id,坐标按官方前向差分方式解压后再组装 points,状态判断保持数字类型。

练习 ​

  1. 弹层健壮性:给"选择房屋""选择维修项目"两个弹层补上空态与失败态——接口返回空数组时弹层内给出提示;并在打开弹层时若数据为空则自动重新拉取一次,说明这样做的必要性。

  2. 上传增强:为 van-uploader 增加删除图片(bind:delete)与上传中的 loading 状态;上传失败时把该项从 file-list 中移除并 toast。思考:为什么失败时不能只弹一个提示就结束?

  3. 提交防重与回填:提交按钮加 loading/disabled,避免连点产生重复报修单;再从详情页"修改信息"跳回表单,用 getRepairDetail 回填全部字段(含附件),验证修改流程可复用同一套表单。

  4. 列表分页:给报修列表加上拉加载更多与下拉刷新,current 递增、pageSize 固定;说明为什么 current 不能像现在这样写死在 onLoad 里。

  5. 路线规划降级:把 getPolyline 的起点终点从写死改成由报修数据驱动,并处理 direction 失败或无路线的场景——只渲染 markers 不渲染 polyline,保证地图区域不空白。

原视频对照 ​

#视频时长
1享+生活-在线报修(组件交互)12:01
2享+生活-在线报修(房屋列表)07:04
3享+生活-在线报修(维修项目)05:09
4享+生活-在线报修(选择房屋和维修项目)11:14
5享+生活-在线报修(选择预约时间)13:30
6享+生活-在线报修(上传附件)14:59
7享+生活-在线报修(表单验证)10:08
8享+生活-在线报修(提交表单)08:26

小结 ​

  1. 表单页的数据流是单向的:组件事件 → 回调 → setData → 视图。任何"没反应"的 bug,排查顺序都应是"事件是否触发 → 回调是否执行 → 数据是否落到 data → 视图是否绑定",而不是先怀疑样式。
  2. 校验的时机在提交动作内,校验规则的键名必须与 data 字段严格同名。键名错位不会报错,只会静默放行,这类问题最难靠肉眼发现。
  3. 接口契约优先于本地命名习惯。字段名、必填项、返回结构在写第一行代码时就对齐文档,能省掉绝大部分联调时间。
  4. 列表页面必须同时设计"有数据"和"无数据"两套视图,空态不是锦上添花,而是功能的一部分。
  5. 状态值应当驱动 UI:状态文案、类名、按钮显隐、地图展示都从 status 派生,避免同一套判断散落在多处,改一处漏三处。
  6. 异步操作要显式覆盖三态(进行中 / 成功 / 失败)。只写成功路径的功能,在真实网络下等同于不可用。
  7. 演示用的初始数据(如默认附件)必须在联调前清空,否则会污染真实数据,且故障现象与真实原因相距很远。
  8. 第三方能力(Vant 组件、位置服务)以文档为准,事件参数结构、坐标解压这类"规定动作"不要凭经验猜,猜错的代价通常是一整轮调试。