Skip to content

在线报修与无感刷新(二) ​

本页对应 在线报修与无感刷新,共 6 段视频,约 1 小时 6 分。

学习目标 ​

  • 能使用 Vant Weapp 的 Upload 组件完成报修图片选择与上传,并在上传成功后更新 attachment/fileList 数据,使图片在页面中正确预览。
  • 能监听上传组件的删除事件,根据回调中的 index 过滤图片数组并通过 setData 更新页面,实现指定图片删除。
  • 能配置上传前校验(如 before-read 与回调),根据文件大小拦截超限图片并给出提示,验证超限时不会发起上传请求。
  • 能使用小程序 map 组件配置中心点经纬度、缩放级别和 markers,展示地图中心点、标记点及自定义标记图标。
  • 能调用腾讯位置服务路线规划接口,以骑行模式传入起点和终点坐标,获取并识别返回的压缩路线数据。
  • 能按地图组件 polyline.points 格式解压并转换路线坐标,通过 setData 动态绑定数据,在地图上绘制骑行路线。

知识点 ​

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

为什么需要:路线规划用来在地图上展示两点之间的行进路线,例如报修详情页里展示报修人与上门师傅之间的骑行路线。只放两个标记点不够,用户还需要看到“怎么走”。

怎么想:这个功能由两部分组成:小程序 map 组件负责渲染地图、标记点和路线;腾讯位置服务的“路线规划”负责计算两点之间的坐标点。地图组件本身不会自动算路线,路线点必须由位置服务返回。

怎么做:先熟悉 map 组件的基础属性,再调用 qqMap.direction 获取路线坐标,最后把压缩坐标解压并转换成 polyline 需要的数据结构,通过 setData 渲染。

易错点:不要把路线点写死;不要以为 polyline 可以直接接收位置服务返回的原始压缩数组;调用位置服务前要确认 SDK 版本和导入路径。

map 组件的基础属性 ​

为什么需要:地图要先确定“看哪里、放多大、显示哪些点、画哪条线”,否则页面上的地图没有明确内容。

怎么想:latitude 指定地图中心点纬度,longitude 指定地图中心点经度,scale 指定初始缩放比例,取值范围是 3–20;markers 是地图上的标记点数组;polyline 是地图上的路线数组。转写稿里调试时把 scale 设为 3,地图会缩到世界地图;设为 20 会更具体。默认值通常是 16,实际可按业务微调。

怎么做:在页面 data 中准备经纬度和缩放级别,再绑定到 map 组件。中心点由经度和纬度共同控制,修改它们就能改变初次渲染的位置。

易错点:经度是 longitude,不是“经功”;纬度是 latitude;scale 不要超出 3–20;中心点两个属性要一起看,不能只改一个就以为位置一定准确。

markers 标记点 ​

为什么需要:报修详情里至少要有报修人位置和师傅位置,标记点让用户一眼看到两个关键位置。

怎么想:markers 是数组,每个标记点是一个对象。每个标记点必须指定 id,否则会产生错误;经纬度也是确定点位的关键;iconPath 可以自定义图标,width/height 控制图标尺寸。转写稿里验证过:不设置 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。

用腾讯位置服务获取路线坐标 ​

为什么需要:polyline 需要很多个坐标点组成一条线,自己模拟不现实,必须通过位置服务的路线规划方法计算。

怎么想:调用路线规划时,mode 选择出行方式,本节用骑行 bicycling;from 和 to 指定起点和终点;成功后在 success 回调里取 result.routes[0].polyline。转写稿里特别提醒:路线规划功能要检查微信小程序 JS SDK 是否为 v1.2,版本不对会调不了;该 SDK 不支持 Promise,所以用传统的 success 回调。调试时先 console.log 结果,确认 routes[0].polyline 存在,再处理数据。

怎么做:保留笔记中的调用与处理代码:

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

易错点:from、to 的顺序不要反;先打印 result 再取深层字段;SDK 导入路径移动后极易出错,要重新检查;polyline 返回的是压缩数据,不能直接绑定。

坐标解压与 polyline 数据加工 ​

为什么需要:位置服务返回的 polyline 是经过前向差分压缩的坐标串,目的是减少传输体积;而小程序 map 的 polyline 要求的是 points 数组,里面每个点要写成 { latitude, longitude }。

怎么想:先按官方公式解压:从下标 2 开始,用 coors[i - 2] + coors[i] / 1000000 还原;解压后再两个一组读取,偶数下标作纬度,下一个作经度,push 成对象数组。转写稿调试时先打印解压前后的数据,确认最后变成“数组里嵌对象”的结构,才符合地图组件要求。

怎么做:把解压后的 points 放进 polyline 数组,每项包含 points、color、width,再 setData。转写稿里还发现 polyline 本身应是数组,因为可能同时画多条路线;其中一条路线对象里再放很多点。中心点也可以顺手设成 points[30] 附近,便于观察路线。

易错点:解压循环从 i = 2 开始;coors[i - 2] 不能写错;步长是 2;不要把压缩数组直接传给 polyline;polyline 不是单个对象而是数组;小程序更新页面要用 setData。

修改报修的整体流程 ​

为什么需要:在线报修提交后,用户可能发现房屋、报修项、预约时间、手机号、描述或附件有误,需要回到表单修改。新增和修改如果做成两个页面,逻辑会重复,维护成本高。

怎么想:复用同一个报修表单页面。进入页面时通过 URL 是否带 id 判断:没有 id 是新增,有 id 是修改。修改时先用 id 拉取原报修详情,回填表单;用户改完再提交,提交数据中带上 id,后端据此区分新增与修改。

怎么做:在 onLoad 中解构 id,存在则调用 getRepairDetail(id):

javascript
// repair_pkg/pages/form/index.js
Page({
  data: {},
  onLoad({id}) {
    // ...
    // 如果有id表明是修改操作
    if (id) this.getRepairDetail(id)
  },
  // 获取待修改的报修信息
  async getRepairDetail(id) {
    // 调用接口
    const { code, data: repairDetail } = await wx.http.get('/repair/' + id)
    // 检测是否调用成功
    if (code !== 10000) return wx.utils.toast()
    // 渲染数据
    this.setData({ ...repairDetail })
  },
})

易错点:onLoad 参数要解构准确;不要无条件请求详情;setData 展开的是详情对象,字段名必须和表单 data 对应,否则页面看似请求成功却不回显。

先调整字段名:houseName 改成 houseInfo ​

为什么需要:修改报修要回填报修房屋的默认值,而表单依赖的字段名是 houseInfo。如果详情数据仍叫 houseName,回填后房屋默认值不会展示。

怎么想:详情接口返回的数据最终要 setData 到表单数据中,因此字段名必须与表单绑定、提交参数保持一致。笔记明确这个名称修改涉及 4 个位置,应参考视频逐一修改。

怎么做:在项目中搜索 houseName,按视频定位到的 4 个位置改为 houseInfo,重点检查详情回填、表单展示和提交参数相关代码,改完后用修改报修页面验证房屋默认值是否出现。

易错点:不要只改模板或只改 JS 一处;也不要全局盲替换,确认这些位置都属于报修房屋字段。字段名不一致是“接口有数据但不回显”的典型原因。

提交时带上 id 区分新增和修改 ​

为什么需要:后端接口根据提交数据中是否包含 id 判断是添加报修还是修改报修。修改时漏掉 id,后端会当成新增,产生一条新报修。

怎么想:提交前仍要执行表单校验;从 data 中提取接口需要的字段,并把 id 一起提交。成功后跳转报修列表页。

怎么做:

javascript
// repair_pkg/pages/form/index.js
Page({
  data: {},
  onLoad() {},
  getRepairDetail(id) {
    // ...
  },
  async submitForm() {
    // 验证表单数据
    if (!this.validate()) return
    // 提取接口需要的数据
    const {
      id, houseId, repairItemId, mobile, appointment, description, attachment
    } = this.data
    // 调用接口
    const { code } = await wx.http.post('/repair', {
      id,
      houseId,
      repairItemId,
      mobile,
      appointment,
      description,
      attachment,
    })
    // 检测接口是否调用成功
    if (code !== 10000) return wx.utils.toast('在线报修失败!')
    // 跳转到报修列表页面
    wx.redirectTo({
      url: '/repair_pkg/pages/list/index',
    })
  },
})

易错点:解构时漏写 id;提交对象中漏传 id;未校验直接提交;接口失败仍跳转。调试时先看详情接口 /repair/:id 是否返回 code: 10000,再看提交请求的 payload 中是否确实带上了 id 和 houseInfo。转写稿里也反复提醒过,代码移动或路径变化后路径极易出错,修改报修同样要核对页面路径和接口路径。

需求边界:受理中才允许取消 ​

为什么需要:取消报修不是任何状态都能做。笔记明确:当在线报修处于“受理中”时,允许取消本次报修。若页面不限制状态,用户可能对已取消、已完成等单据继续点取消,造成无效请求和状态混乱。

怎么想:先确认“受理中”在数据里的判断条件。笔记给出的条件是 repairDetail.status === 1。因此页面展示层用条件渲染控制“取消报修”按钮,事件层仍然要依赖接口校验,不能只靠前端按钮隐藏。

怎么做:在详情页 toolbar 中保留“修改信息”,并给“取消报修”加 wx:if。模板代码如下,保留笔记原代码:

xml
<view class="toolbar">
  <view
    mark:id="{{repairDetail.id}}"
    bind:tap="editRepair"
    class="button-text active"
  >修改信息</view>
  <view
    wx:if="{{repairDetail.status === 1}}"
    mark:id="{{repairDetail.id}}"
    bind:tap="cancelRepair"
    class="button-text"
   >取消报修</view>
</view>

易错点:status 的实际类型要确认。如果后端返回字符串 "1",严格等于 1 不会显示按钮。笔记代码使用 === 1,应保持与接口字段类型一致。另外,按钮隐藏只是体验层控制,不能替代接口校验。

事件参数:用 mark:id 传单据 id ​

为什么需要:取消接口需要知道取消哪一条报修。详情页数据在 repairDetail 中,点击按钮时应把 repairDetail.id 带到事件里。

怎么想:模板里使用 mark:id="&#123;&#123;repairDetail.id&#125;&#125;",事件对象中通过 ev.mark.id 读取。不要混用 data-id 的读取方式,否则会取不到值。

怎么做:两个按钮都绑定 mark:id,取消按钮绑定 bind:tap="cancelRepair"。事件方法接收 ev,后续请求路径拼 ev.mark.id。

易错点:不要写成 ev.currentTarget.dataset.id,因为本节模板用的是 mark:id。同时确认 repairDetail.id 已渲染前存在,避免拼出 undefined。

调用取消接口:PUT 路径并判断业务码 ​

为什么需要:点击按钮后要真正通知后端取消。笔记给出的接口文档地址是:接口文档地址。示例使用 wx.http.put('/cancel/repaire/' + ev.mark.id)。

怎么想:取消是写操作,使用 PUT;单据 id 放在路径中;接口返回后先取 code,只有 code === 10000 才视为成功。失败时提示并终止后续跳转。

怎么做:保留笔记代码:

javascript
// repair_pkg/pages/detail/index.js
Page({
  data: {},
  onLoad() {},
  // 取消报修
  async cancelRepair(ev) {
    // 调用接口
    const { code } = await wx.http.put('/cancel/repaire/' + ev.mark.id)
    // 检测接口是否调用成功
    if (code !== 10000) return wx.utils.toast()
    // 跳转到报修列表页面
    wx.navigateTo({
      url: '/repair_pkg/pages/list/index',
    })
  },
})

易错点:第一,路径中 repaire 的拼写来自笔记代码,不要擅自改成 repair,否则可能 404。第二,await 不能漏,否则 code 不是接口结果。第三,失败分支要 return,否则失败后仍会执行跳转。第四,wx.utils.toast() 是项目封装,按项目实际用法调用,不虚构提示文案。

成功后跳转:进入报修列表 ​

为什么需要:取消成功后,详情页数据已过期,用户需要到列表页查看最新状态。

怎么想:取消是写操作,成功后离开详情页。笔记明确“取消报修成功后会跳转到报修列表页面”,示例使用 wx.navigateTo。

怎么做:在 code === 10000 后执行 wx.navigateTo({ url: '/repair_pkg/pages/list/index' })。路径前导斜杠要保留。

易错点:不要漏写页面路径;不要在没有确认成功时跳转;按本节代码使用 navigateTo,若产品要求避免页面栈重复,应另行确认,不能擅自改成其他跳转方式。

调试顺序与说明 ​

本节转写稿主要围绕附件上传、删除、地图等内容,未包含“取消报修”的调试片段,因此不补写不存在的接口返回和日志。可按笔记代码排查:先看 status === 1 时按钮是否渲染;再看点击后是否进入 cancelRepair 且 ev.mark.id 有值;再看网络面板中是否为 PUT /cancel/repaire/{id};最后看 code 是否为 10000,成功后才应跳转列表页。这样能定位“按钮不显示、id 取不到、接口失败、失败后误跳转”四类常见问题。

本节目标与素材边界 ​

访客邀请单的核心是:用户必须保持登录状态,填写访客姓名、性别、手机号、到访房屋、到访日期,校验通过后提交到 /visitor,再跳转访客详情页。本节笔记没有图片,因此不涉及图片保留。需要说明:提供的转写稿主要讲在线报修的图片上传、删除、上传前校验以及地图与路线规划,和“访客邀请”不是同一业务;其中“vent/up file/cho/fi list/低条”等应分别理解为 Vant、upload、true、file-list、detail。为避免编造,下面不把这些报修逻辑硬套到访客邀请中,只依据本节笔记展开推导与调试。

登录态与表单数据设计 ​

为什么需要:提交访客邀请时后端要知道“谁在邀请”,未登录会失败或跳登录页;表单还需要把展示数据和提交数据分开。
怎么想:先保证登录态,再把页面拆成四类数据:房屋列表与选中房屋、到访日期、访客基本信息、校验规则。
怎么做:data 中准备 name/gender/mobile/houseId/houseInfo/visitDate/houseList/currentDate/maxDate/dateLayerVisible。其中 houseId 用于提交,houseInfo 用于页面展示。
易错点:不要把 houseId 和 houseInfo 混用;日期要格式化成接口需要的字符串;houseList 初始值应为数组。

获取房屋列表并选择房屋 ​

为什么需要:房屋来自后端接口,不能写死;用户需要通过弹层选择到访房屋。
怎么想:页面加载时请求 /house,用 van-action-sheet 展示;监听 select 事件,把 ev.detail.id/name 回填。笔记中“坚挺 select”应理解为“监听 select”。
怎么做:

javascript
// visitor_pkg/pages/form/index.js
Page({
  data: { houseList: [] },
  onLoad() {
    this.getHouseList()
  },
  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,
      houseInfo: ev.detail.name,
    })
  },
})

易错点:接口失败要先判断 code;select 事件参数在 ev.detail;弹层关闭后仍要保留已选房屋。

选择到访日期 ​

为什么需要:到访日期不能任意选,业务要求最多提前 3 天申请。
怎么想:用 van-datepicker,把最大值设为 Date.now() + 1000 * 3600 * 24 * 3,监听 confirm 获取选择结果并格式化。
怎么做:

javascript
// visitor_pkg/pages/form/index.js
Page({
  data: {
    currentDate: Date.now(),
    maxDate: Date.now() + 1000 * 3600 * 24 * 3,
  },
  selectDateInfo(ev) {
    this.setData({
      visitDate: wx.utils.dataFormat(ev.detail),
      dateLayerVisible: false,
    })
  },
})

易错点:时间单位是毫秒;maxDate 要在合理时机计算;选择后要关闭弹层;提交前确认 visitDate 已格式化。

表单校验 ​

为什么需要:减少无效请求,尽早提示用户填写问题。
怎么想:使用 wechat-validate 的 behaviors 和 rules,把姓名、手机号、房屋、日期分别约束。
怎么做:

javascript
// visitor_pkg/pages/form/index.js
import wxValidate from 'wechat-validate'
Page({
  behaviors: [wxValidate],
  data: {
    name: '',
    gender: 1,
    mobile: '',
    houseId: '',
    visitDate: '',
  },
  rules: {
    name: [
      { required: true, message: '访客姓名不能为空!' },
      { pattern: /[\u4e00-\u9fa5]{2,5}/, message: '访客姓名只能为中文!' },
    ],
    mobile: [
      { required: true, message: '访客手机号不能为空!' },
      { pattern: /^1[3-8]\d{9}$/, message: '请填写正确的手机号码!' },
    ],
    houseId: [{ required: true, message: '请选择到访的房屋!' }],
    visitDate: [{ required: true, message: '请选择到访的日期!' }],
  },
})

易错点:validate() 返回假值要直接 return;手机号正则要匹配当前业务;姓名规则是中文 2 到 5 位,不要随意放宽。

提交表单并跳转详情 ​

为什么需要:校验通过后创建访客记录,并让用户进入详情页查看结果。
怎么想:先 validate(),再解构接口字段,POST /visitor,成功后用返回的 id 跳转。
怎么做:

javascript
// visitor_pkg/pages/form/index.js
import wxValidate from 'wechat-validate'
Page({
  behaviors: [wxValidate],
  async goPassport() {
    if (!this.validate()) return
    const { name, gender, mobile, houseId, visitDate } = this.data
    const {
      code,
      data: { id },
    } = await wx.http.post('/visitor', { name, gender, mobile, houseId, visitDate })
    if (code !== 10000) return wx.utils.toast()
    wx.reLaunch({
      url: '/visitor_pkg/pages/passport/index?id=' + id,
    })
  },
})

易错点:提交字段要和接口文档一致;code !== 10000 要提示;详情页需要 id;用 reLaunch 会关闭当前页面栈,确认业务确实需要这样跳转。

调试与排错 ​

为什么需要:访客邀请涉及登录、接口、弹层、校验和跳转,任何一环出错都会表现为“提交没反应”。
怎么想:先看登录态,再看 /house 是否返回、houseId/visitDate 是否被回填,最后看 validate() 和 /visitor 返回。
怎么做:在 getHouseList 后检查 houseList;在 selectHouseInfo/selectDateInfo 后检查 houseId/visitDate;提交前打印解构字段;接口失败看 code 与提示。
易错点:不要只改页面展示而忘记 setData;不要把报修上传、地图路线的调试经验直接套到本节;转写稿与本节业务不一致时,应以本节接口和字段为准。

场景拆解:访客详情为什么分两种入口 ​

为什么需要: 访客详情页既可能由业主在登录状态下打开,也可能由被分享的新用户直接打开。前者有身份和访客 id,后者没有登录态,只能依赖分享链接里的加密参数取数。
怎么想: 先判断页面参数:有 id 走登录态详情接口;有 encryptedData 走分享详情接口。两个方法都要先做参数存在性判断,避免无效请求。
怎么做: 在 onLoad 中同时接收 id 和 encryptedData,分别调用对应方法。
易错点: 不要把 onLoad 里的 encryptedData 和详情数据里的 passport.encryptedData 混为一谈;前者是页面参数,后者用于控制分享按钮显示。

说明:本节转写稿主要讨论报修图片上传、删除、校验和地图路线,与“访客详情”主题不一致;以下只依据本节笔记和代码扩写,不把无关内容硬套进来,避免编造。

登录态:按 ID 获取访客详情 ​

为什么需要: 登录用户从列表进入详情时,最直接的定位方式是访客 id。
怎么想: 先检测 id 是否存在,再请求 /visitor/{id};接口成功码为 10000 才渲染。
怎么做: 参考笔记代码:

javascript
// visitor_pkg/pages/passport/index.js
Page({
  data: {
    passport: {}
  },
  onLoad({id}) {
        // 获取访客详情
    this.getPassport(id)
  },
  // 获取访客详情(通行证)
  async getPassport(id) {
    // 检测是否存在 id
    if (!id) return
    // 调用接口
    const { code, data: passport } = await wx.http.get('/visitor/' + id)
    // 检测接口是否调用成功
    if (code !== 10000) return wx.utils.toast()
    // 渲染数据
    this.setData({ passport })
  },
})

接口文档地址:接口文档地址

易错点: 缺少 id 时不要发请求;不要只看请求是否发出,还要判断 code;渲染时用 setData 更新 passport,不要直接改 this.data。

页面渲染:倒计时、二维码与失效蒙层 ​

为什么需要: 通行证二维码有时效,用户需要看到剩余有效时间;过期后必须明确提示“二维码失效”。
怎么想: validTime > 0 时展示倒计时;否则展示 00:00:00;validTime === -1 时在二维码上覆盖蒙层。
怎么做: 将接口返回的详情渲染到页面:

xml
<view class="passport">
  <view class="countdown">
    <van-count-down wx:if="{{passport.validTime > 0}}" time="{{passport.validTime * 1000}}" />
    <view wx:else class="van-count-down">00:00:00</view>
    <view class="label">通行证有效时间</view>
  </view>
  <view class="qrcode">
    <image src="{{passport.url}}"></image>
    <view wx:if="{{passport.validTime === -1}}" class="mask">二维码失效</view>
  </view>
  <view class="description">
    <view class="house">{{passport.houseInfo}}</view>
    <view class="tips">将此二维码分享给访客,访客扫码即可开门</view>
  </view>
</view>
<view class="toolbar">
  <button class="button-share" open-type="share">
    <text class="enjoy-icon icon-share"></text>
    <text class="text">分享给朋友</text>
  </button>
  <button class="button-save">
    <text class="enjoy-icon icon-save"></text>
    <text class="text">保存到本地</text>
  </button>
</view>

易错点: validTime 单位按笔记代码需要乘 1000 再传给倒计时组件;判断过期用 === -1;图片地址字段是 passport.url,不要写成不存在的字段。

分享入口:为什么要拼 encryptedData ​

为什么需要: 新用户未登录时不能靠 id 查详情,需要分享链接携带加密数据,让分享页能换取详情。
怎么想: 在 onShareAppMessage 中从当前详情数据取 encryptedData,拼到分享路径上。
怎么做:

javascript
// visitor_pkg/pages/passport/index.js
Page({
  data: {},
  onLoad() {},
  // 自定义分享
  onShareAppMessage() {
    // 获取加密数据
    const { encryptedData } = this.data.passport
    return {
      title: '查看通行证',
      path: '/visitor_pkg/pages/passport/index?encryptedData=' + encryptedData,
      imageUrl: 'https://enjoy-plus.oss-cn-beijing.aliyuncs.com/images/share_poster.png',
    }
  },
})

易错点: 分享路径参数名要与 onLoad 接收名一致;encryptedData 从 this.data.passport 取,不要从页面参数误取。

分享页取数:用 encryptedData 换详情 ​

为什么需要: 被分享用户可能未登录,必须走分享专用接口。
怎么想: onLoad 同时接收 id 和 encryptedData,两个获取方法内部各自判断参数,谁存在谁执行。
怎么做:

javascript
// visitor_pkg/pages/passport/index.js
Page({
  onLoad({ id, encryptedData }) {
    // 获取访客详情
    this.getPassport(id)
    this.getPassportShare(encryptedData)
  },
  async getPassport(id) {},
  async getPassportShare(encryptedData) {
    // 检测是否存在 id
    if (!encryptedData) return
    // 调用接口
    const { code, data: passport } = await wx.http.get('/visitor/share/' + encryptedData)
    // 检测接口是否调用成功
    if (code !== 10000) return wx.utils.toast()
    // 渲染数据
    this.setData({ passport })
  },
})

接口文档地址:接口文档地址

易错点: 两个方法都要做空值 return;分享接口路径是 /visitor/share/{encryptedData},不要漏掉 share。

分享页渲染:隐藏再次分享按钮 ​

为什么需要: 分享页场景不应让用户继续二次分享,避免入口混乱。
怎么想: 用详情数据中的 passport.encryptedData 控制工具栏是否显示;登录态详情具备该字段时显示分享按钮,分享页取回的数据不满足条件时隐藏。
怎么做:

xml
<view class="toolbar" wx:if="{{passport.encryptedData}}">
  <button class="button-share" open-type="share">
    <text class="enjoy-icon icon-share"></text>
    <text class="text">分享给朋友</text>
  </button>
  <button class="button-save">
    <text class="enjoy-icon icon-save"></text>
    <text class="text">保存到本地</text>
  </button>
</view>

易错点: 不要用页面参数 encryptedData 直接控制按钮;按笔记代码应使用 passport.encryptedData,否则容易把“分享页参数”和“详情数据字段”混淆。

课件图示 ​

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

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

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

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

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

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

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

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

代码示例 ​

1. 地图组件 + 腾讯位置服务路线规划(报修详情页) ​

xml
<!-- repair_pkg/pages/detail/index.wxml -->
<map
  class="map"
  latitude="{{latitude}}"
  longitude="{{longitude}}"
  scale="{{16}}"
  markers="{{markers}}"
  polyline="{{polyline}}"
/>
javascript
// repair_pkg/pages/detail/index.js
import qqMap from '../../../utils/qqmap' // 已封装导出的腾讯位置服务 SDK

Page({
  data: {
    latitude: 40.22077,
    longitude: 116.23128,
    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,
      },
    ],
    polyline: [],
  },
  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 },
          ],
        })
      },
    })
  },
})

关键行与易错点

  • latitude / longitude 是地图中心点,两者都是必填,缺一不可;scale 取值 3–20,默认 16,值越大越具体。
  • markers 每一项必须带 id,否则报错;iconPath 可自定义图标,width/height 控制尺寸。
  • polyline 是数组(可同时画多条线),每项里的点数组属性名是 points,格式必须是 { latitude, longitude },直接拿腾讯返回的原始数据绑不上去。
  • 坐标是压缩过的:coors[i] = Number(coors[i-2]) + Number(coors[i]) / 1000000,这一步漏了画出来的线就是错的;解压后再按 i += 2 两两取出经纬度。
  • 注意 SDK 版本必须是 1.2,1.1 没有 direction 方法;qqMap.direction 不支持 Promise,只能用 success 回调(不能用 await)。
  • 封装 SDK 时挪了文件位置,务必检查 import 路径,这是最容易出错的地方。

2. van-uploader 图片上传:选择前校验 + 上传 + 删除 ​

xml
<!-- repair_pkg/pages/form/index.wxml -->
<van-uploader
  file-list="{{attachment}}"
  use-before-read="{{true}}"
  bind:before-read="beforeRead"
  bind:after-read="afterRead"
  bind:delete="uploaderDelete"
/>
javascript
// repair_pkg/pages/form/index.js
Page({
  data: {
    attachment: [],
  },

  // 读取文件前校验(性能优化:超限直接拦截,不发请求)
  beforeRead(ev) {
    const { file, callback } = ev.detail
    // 超过 1.5M 不允许上传(阈值与后端确认)
    if (file.size > 1024 * 1024 * 1.5) {
      wx.utils.toast('文件不能超过 1.5M')
      callback(false) // 主动拦截,后续 after-read 不再触发
      return
    }
    callback(true) // 必须回调 true,否则读取流程终止
  },

  // 文件选择完成(组件已自动拿到临时路径)
  async afterRead(ev) {
    const file = ev.detail.file
    // 路径以接口文档「公共-上传文件」为准
    const { code, data } = await wx.http.upload('/upload', {
      name: 'file',
      filePath: file.url,
    })
    if (code !== 10000) return wx.utils.toast('上传失败')
    // 接口返回对象中带 url 字段,正好满足 file-list 的要求
    this.setData({
      attachment: [...this.data.attachment, data],
    })
  },

  // 删除图片
  uploaderDelete(ev) {
    const { index } = ev.detail
    this.setData({
      attachment: this.data.attachment.filter((item, i) => i !== index),
    })
  },
})

关键行与易错点

  • file-list 绑定的数组每一项必须有 url 字段,否则页面上看不到图;后端返回的字段若不是 url(如 imageUrl),要改名后再追加。
  • 用了 before-read 就必须同时写 use-before-read="&#123;&#123;true&#125;&#125;",只绑事件不设属性不生效。
  • before-read 里必须手动调用 callback(true),否则读取流程被拦截,after-read 永远不会触发(表现为"加了校验后图片传不上去")。
  • after-read 里用 ev.detail.file.url 拿临时路径,不需要自己再调 wx.chooseMedia,组件已封装好。
  • 小程序更新数组不能用 push(返回值是长度),要用 [...old, new] 或 filter 后整体 setData,否则视图不刷新。
  • 删除用 filter((item, i) => i !== index),不要用 splice 直接改原数组再 setData。

3. 修改报修:回显详情 + 带 id 提交 ​

javascript
// repair_pkg/pages/form/index.js
Page({
  data: {
    id: '',
    houseId: '',
    houseInfo: '',
    repairItemId: '',
    mobile: '',
    appointment: '',
    description: '',
    attachment: [],
  },

  onLoad({ id }) {
    // 如果有 id 表明是修改操作
    if (id) this.getRepairDetail(id)
  },

  // 获取待修改的报修信息
  async getRepairDetail(id) {
    const { code, data: repairDetail } = await wx.http.get('/repair/' + id)
    if (code !== 10000) return wx.utils.toast()
    // 渲染数据(表单自动回显)
    this.setData({ ...repairDetail })
  },

  async submitForm() {
    // 验证表单数据
    if (!this.validate()) return
    // 提取接口需要的数据
    const {
      id, houseId, repairItemId, mobile, appointment, description, attachment,
    } = this.data
    // 调用接口
    const { code } = await wx.http.post('/repair', {
      id,
      houseId,
      repairItemId,
      mobile,
      appointment,
      description,
      attachment,
    })
    // 检测接口是否调用成功
    if (code !== 10000) return wx.utils.toast('在线报修失败!')
    // 跳转到报修列表页面
    wx.redirectTo({
      url: '/repair_pkg/pages/list/index',
    })
  },
})

关键行与易错点

  • 新增和修改共用同一个页面,靠 onLoad({ id }) 里的 id 区分:有 id 就拉详情回显,没有就是新增。
  • 房屋字段名必须是 houseInfo(不是 houseName),涉及 4 处,改漏了房屋默认值就显示不出来。
  • 提交时必须把 id 一起带上,后端靠有没有 id 判断是新增还是修改;解构时别把 id 漏掉。
  • 详情数据用 this.setData({ ...repairDetail }) 一次性铺开,字段名要和 data 里的保持一致才能回显。
  • 提交成功后用 wx.redirectTo(关闭当前页),避免用户返回又看到旧表单。
  • 报修详情页的「取消报修」按钮用 wx:if="&#123;&#123;repairDetail.status === 1&#125;&#125;" 控制显示,并用 mark:id="&#123;&#123;repairDetail.id&#125;&#125;" 传参,在事件里通过 ev.mark.id 取到,接口是 wx.http.put('/cancel/repaire/' + ev.mark.id)。

常见错误 ​

1. markers 缺少 id,标记点不显示或控制台报错

  • 现象:地图能渲染、中心点正确,但一个标记点都看不到,或真机上报错。
  • 原因:markers 每项必须有唯一 id,组件内部靠它做 diff 和事件回传。
  • 修法:每项补 id,且同一数组内不可重复;调试时先只放一个 marker 验证,再加第二个。

2. 把 direction 返回的 routes[0].polyline 直接绑给 polyline

  • 现象:地图上画不出线,或线跑到非洲/完全错乱。
  • 原因:两个"polyline"同名不同义——接口返回的是压缩差分坐标串(一维数字数组),组件要的是 [{ points: [{latitude, longitude}], color, width }]。
  • 修法:先解压、再两两配对成 points、最后包成数组赋给 polyline,三步都不能省。

3. 解压循环从 i = 0 开始

  • 现象:解压出来的坐标整体偏移,路线起终点和 markers 对不上。
  • 原因:差分压缩串的前两个值是绝对经纬度,从第 3 个开始才是相对前两位的偏移量。
  • 修法:for (let i = 2; i < coors.length; i++),且公式必须是 coors[i] = Number(coors[i - 2]) + Number(coors[i]) / 1000000。

4. 用 push 更新 attachment

  • 现象:图片传上去了、接口也返回了,但页面上不出现。
  • 原因:push 返回的是新长度(数字),setData 收到数字自然不会刷新视图。
  • 修法:this.setData({ attachment: [...this.data.attachment, res.data] });删除同理用 filter 生成新数组再 setData。

5. 绑了 before-read 事件却没让属性生效,或校验后不调 callback

  • 现象:选完图片后流程"卡住",after-read 再也不触发,上传请求根本不发。
  • 原因:该事件需同时满足两个条件——属性与事件同名绑定,且回调里必须主动放行,否则流程终止。
  • 修法:模板上 before-read="&#123;&#123;true&#125;&#125;" bind:before-read="beforeRead";JS 里校验通过 callback(true),不通过则 toast 后直接 return(不调用即为拦截)。

6. 修改报修时提交数据漏了 id

  • 现象:改完提交,列表里多出一条新记录,原记录纹丝不动。
  • 原因:新增与修改共用 POST /repair,后端靠请求体中是否含 id 判断语义。
  • 修法:提交时显式解构并带上 id;同时确认 houseName → houseInfo 已全局替换(共 4 处),否则房屋默认值回填不出来。

7. 访客详情页两个请求无脑都发

  • 现象:登录态下打开自己的通行证,控制台多一条 404/失败的 share 请求;分享页则多一条无 id 的请求。
  • 原因:onLoad 里把 getPassport(id) 和 getPassportShare(encryptedData) 并列调用,未做参数互斥。
  • 修法:两个方法内部各自先判空 if (!id) return / if (!encryptedData) return,或直接用 if / else if 二选一。

8. 分享打开的页面仍显示"分享给朋友"

  • 现象:从分享链接进入的通行证页,底部工具栏照样有分享按钮。
  • 原因:wx:if 的判据写反或用了不可靠的字段。
  • 修法:用 passport.encryptedData(或登录态下才有的 id)取反做条件,确保"分享进来的页面不能再分享"。

练习 ​

练习 1|地图视野自适应 给定起点和终点两个坐标,画出骑行路线后,把地图中心点和 scale 调整到"整条路线刚好落在可视区域内"。要求:中心点从 points 中取,不要写死数字;并说明为什么不能沿用初始的 scale: 16。

练习 2|上传的三重限制 在 van-uploader 上追加三条约束:最多 3 张、单张不超过 1MB、仅允许图片。要求指出每条约束分别落在哪个时机(选择前 / 选择后 / 提交前),并写出拦截失败时的用户反馈方式。

练习 3|新增与修改共用一个页面 画出该页面 onLoad 的分支逻辑(带 id / 不带 id),并列出"修改模式"下提交参数与"新增模式"下提交参数的差集。再回答:如果把区分依据从 id 换成别的字段,会带来什么风险?

练习 4|通行证页的三种入口 梳理该页面可能被打开的三种情况:登录态带 id、分享带 encryptedData、两者都没有。分别写出:调用哪个接口、页面应展示什么、底部工具栏是否显示、倒计时组件如何渲染。

练习 5|跳转方式选型 针对"取消报修成功后跳列表"和"填写访客邀请成功后跳详情"两处,分别说明应选 navigateTo / redirectTo / reLaunch 中的哪一个,并从页面栈角度说明理由。

原视频对照 ​

#视频时长
1在线报修-报修图片上传12:52
2在线报修-删除图片05:20
3在线报修-上传图片前校验07:46
4报修详情-地图组件14:45
5报修详情-获取骑行路线12:16
6报修详情-绘制骑行路线13:56

小结 ​

  1. 地图组件只有三件事:中心点+缩放定视野、markers 定点、polyline 定线。前两个是纯数字、几乎不会错;后两个是结构化数组,格式坑集中在这里,出问题先打印数据看结构,别急着改样式。
  2. 路线坐标从来不是"拿到就能用":压缩串 → 解压 → 重组成 points 是腾讯侧的传输协议决定的,属于照文档抄的部分,不要试图自己推导公式。
  3. 位置服务的 SDK 必须抽成独立模块导出,页面只负责 import。抽离后第一件事是回归路径,这是整套流程里最容易出错的一步,比业务逻辑本身更容易翻车。
  4. 上传类需求的重心不在"传",而在传之前拦、传之后取:before-read 负责把非法文件挡在请求之外,after-read 负责拿到临时路径,回调不放行则整条链路直接断掉。
  5. 新增与修改共用一个页面、一个接口是常态,区分依据是 id;而字段名对齐是默认值能否回填的前提,houseName/houseInfo 这类改名必须全局搜一遍确认。
  6. 同一个页面被"登录打开"和"分享打开"时,入口参数不同 → 接口不同 → 页面能力也不同(能否再分享、倒计时是否可用)。这类差异必须显式分支,不能靠接口返回兜底。
  7. 小程序里更新数组一律走 setData 造新数组,push/splice 这类原地修改方法在视图层是失效的——这条结论适用于本模块所有列表型数据。