主题
在线报修与无感刷新(二)
本页对应 在线报修与无感刷新,共 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="{{repairDetail.id}}",事件对象中通过 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,否则容易把“分享页参数”和“详情数据字段”混淆。
课件图示








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