主题
在线报修与无感刷新(一)
本页对应 在线报修与无感刷新,共 6 段视频,约 52 分钟。
学习目标
- 能根据维修项目选中状态,使用三元表达式动态绑定
active类名,并完成项目列表请求、渲染、点击选择与表单回显。 - 能使用 Field 组件配置
maxlength、type="textarea"、show-word-limit,实现手机号和问题描述输入,并通过双向绑定收集mobile、description。 - 能使用弹出层和日期选择器组件,配置
type、min-date、max-date,基于当前时间戳计算“当天起至 7 天内”的可选范围。 - 能通过日期选择器的确定、取消事件关闭弹层,获取选中时间戳,并将日期回显到表单。
- 能将时间戳格式化为后端要求的
YYYY-MM-DD格式,完成月份和日期的补零处理。 - 能封装日期格式化工具函数到
util中并导出复用,验证格式化结果与表单提交字段一致。
知识点
在线报修表单的整体思路
为什么需要:在线报修不是单个输入框,而是“房屋、维修项目、联系电话、上门时间、问题描述、附件”的组合表单。数据既要收集完整,又要保证提交格式符合接口要求。
怎么想:先准备表单字段,再分别获取房屋列表、维修项目列表,随后处理日期、附件,最后统一校验并提交。
怎么做:页面中维护 houseList、repairItem、attachment 等数据,进入页面时请求列表数据。
易错点:小程序 model:value 双向绑定只能绑定 data 的第一级字段,不要把 mobile、description 等写到二级对象里。
获取房屋列表并选择房屋
为什么需要:报修必须关联具体房屋,且通常只能选择审核通过的房屋。
怎么想:进入页面先请求 /house,把返回列表交给 van-action-sheet 渲染;用户选择后保存 id 和 name。
怎么做:
javascript
// repair_pkg/pages/form/index.js
Page({
data: {
// ...
houseList: []
},
// 获取房屋列表(必须是通过审核的房屋)
async getHouseList() {
// 调用接口
const { code, data: houseList } = await wx.http.get('/house')
// 检测接口是否调用成功
if (code !== 10000) return wx.utils.toast()
// 渲染数据
this.setData({ houseList })
},
})xml
<van-action-sheet
bind:close="closeHouseLayer"
bind:cancel="closeHouseLayer"
bind:select="selectHouseInfo"
round
show="{{houseLayerVisible}}"
actions="{{houseList}}"
cancel-text="取消"
title="选择报修房屋"
/>javascript
// repair_pkg/pages/form/index.js
Page({
data: {
houseId: '',
houseName: ''
},
async getHouseList() {
// ...
},
// 获用户选择的房屋
selectHouseInfo(ev) {
// 数据渲染
this.setData({
houseId: ev.detail.id,
houseName: ev.detail.name,
})
},
})xml
<van-cell-group border="{{false}}" title="报修房屋">
<van-cell
value="{{houseName || '请选择报修房屋'}}"
value-class="{{houseName && 'active-cell'}}"
bind:click="openHouseLayer"
is-link
border="{{false}}"
/>
</van-cell-group>易错点:actions 中每项要有 id、name 字段;选中文字要高亮时,用三元表达式按是否有 houseName 决定是否加 active-cell,不要无条件加类名。
获取维修项目并回显
为什么需要:维修项目决定报修类型,也必须从接口数据中选择,不能手填。
怎么想:与房屋列表同理,请求 /repairItem,用第二个 van-action-sheet 渲染;选择事件里保存项目 id 和 name。
怎么做:
javascript
// repair_pkg/pages/form/index.js
Page({
data: {
repairItem: [],
},
onLoad() {
// 获取维修项目
this.getRepairItem()
},
// 获取维修项目
async getRepairItem() {
// 调用接口
const { code, data: repairItem } = await wx.http.get('/repairItem')
// 检测接口是否调用成功
if (code !== 10000) return wx.utils.toast()
// 渲染数据
this.setData({ repairItem })
},
})xml
<van-action-sheet
bind:close="closeRepairLayer"
bind:cancel="closeRepairLayer"
bind:select="selectRepairItemInfo"
round
show="{{repairLayerVisible}}"
actions="{{repairItem}}"
cancel-text="取消"
title="选择维修项目"
/>javascript
// repair_pkg/pages/form/index.js
Page({
data: {
repairItemId: '',
repairItemName: ''
},
onLoad() {
// 获取维修项目
this.getRepairItem()
},
// 获取待修改的报修信息
async getRepairDetail(id) {
// ...
},
// 获取用户选择的维修项目
selectRepairItemInfo(ev) {
this.setData({
repairItemId: ev.detail.id,
repairItemName: ev.detail.name,
})
},
})xml
<van-cell
title-width="100"
title="维修项目"
value-class="{{repairItemName && 'active-cell'}}"
bind:click="openRepairLayer"
value="{{repairItemName || '请选择维修项目'}}"
is-link
/>易错点:选择事件对象是 ev.detail,不是 ev.target;列表请求失败要提示并返回,避免渲染空数据。
日期选择:范围、回填与格式化
为什么需要:上门日期不能选今天之前,且接口通常要求“年-月-日”格式。
怎么想:用 van-popup 包住 van-datetime-picker,type="date" 只选日期;min-date 设为当前时间,max-date 按业务计算可选范围。用户确认后拿到时间戳,再格式化为接口需要的字符串。
怎么做:
xml
<van-popup
bind:close="closeDateLayer"
round
show="{{dateLayerVisible}}"
position="bottom"
>
<van-datetime-picker
bind:cancel="closeDateLayer"
bind:confirm="selectDateInfo"
type="date"
value="{{currentDate}}"
min-date="{{currentDate}}"
/>
</van-popup>javascript
// repair_pkg/pages/form/index.js
Page({
data: {
appointment: '',
dateLayerVisible: false
},
onLoad() {
// ...
},
// 获取用户选择的预约日期
selectDateInfo(ev) {
// console.log(ev)
// 渲染数据
this.setData({
appointment: wx.utils.dataFormat(ev.detail),
dateLayerVisible: false,
})
},
})调试时可先 console.log(ev.detail),会发现 confirm 返回的是时间戳。若用 new Date() 拼年月日,月份要 +1;补零可封装到 wx.utils.dataFormat 中,判断小于 10 时前面补 0。
易错点:min-date 不设可能选到过去;max-date 不要写死,可基于当前时间做毫秒计算;格式化时注意月份从 0 开始。
上传附件
为什么需要:报修问题常需要图片说明,附件能帮助维修人员判断。
怎么想:van-uploader 负责选择和预览,wx.uploadFile 负责真正上传;上传成功的数据追加到 file-list。
怎么做:
xml
<van-uploader
bind:after-read="uploadPicture"
max-count="6"
preview-size="100"
file-list="{{attachment}}"
/>javascript
// repair_pkg/pages/form/index.js
Page({
// 上传文件
uploadPicture(ev) {
// 上传文件的信息
const { file } = ev.detail
// 调用 API 实现文件上传
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 })
},
})
},
})易错点:wx.uploadFile 返回的 result.data 是字符串,要 JSON.parse;上传接口需要带 token;附件不是必填项,校验规则里不要加 required。
表单校验与提交
为什么需要:避免空房屋、空项目、错误手机号、空日期、空描述直接提交。
怎么想:用 wechat-validate 的 behaviors 和 rules 统一校验,校验通过再提取接口字段提交。
怎么做:
javascript
// repair_pkg/pages/form/index.js
// 导入表单验证插件
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: '请填写问题描述!'}
],
},
})javascript
// repair_pkg/pages/form/index.js
// 导入表单验证插件
import wxValidate from 'wechat-validate'
Page({
behaviors: [wxValidate],
data: {
houseId: '',
repairItemId: '',
mobile: '',
appointment: '',
description: '',
attachment: []
},
// ...
rules: {
// ...
},
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() 再提交;提交字段名要与接口一致;成功后用 wx.redirectTo 跳转报修列表页。
页面登录态与数据准备
为什么需要:报修列表接口要求用户保持登录状态,页面不能一上来就把列表结构写死;同时列表数据是异步返回的,必须先在 data 中准备容器,否则接口回来后没有地方渲染。
怎么想:转写稿里反复强调,这类列表/表单业务和前面做过的业务“非常像,几乎一样”。所以推导顺序应是:先确认页面有登录态包裹,再准备 repairList,最后在 onLoad 首次加载时调用接口。口述里的“repair list”对应代码中的 repairList,口述里的“是否为空”对应代码中的 isEmpty。
怎么做:页面结构外层用 <authorization> 包住主体,保证登录状态;data 中先放 repairList: [];onLoad 中调用 this.getRepairList()。代码保留如下:
javascript
// repqir_pkg/pages/list/index.js
Page({
data: {
repairList: []
},
onLoad() {
// 获取报修列表数据
this.getRepairList()
},易错点:不要在没有登录态的情况下直接请求列表;repairList 初始值应是数组,不能写成 null,否则后续 length 判断容易出错;onLoad 只负责首次加载,不要把请求写到会重复触发的生命周期里。
请求报修列表接口
为什么需要:列表内容来自后端,必须按接口文档传参并判断业务状态码,不能拿到响应就直接渲染。
怎么想:接口文档地址是 接口文档地址。示例中请求 /repair,参数为 current: 1, pageSize: 10,返回后从 data.rows 取列表。转写稿中的调试思路是:先准备 data,首次加载调用接口,检查接口是否成功,再把数据绑定到页面;不要跳过 code 判断。
怎么做:保留示例代码,先解构 code 和 data.rows,判断 code !== 10000 时提示,再 setData 渲染,并同时计算 isEmpty:
javascript
// 报修列表接口
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,
})
},
})易错点:不要把 rows 写成 row 或直接取 data;判断成功码要用接口约定的 10000;接口失败时 data 可能没有 rows,实际调试要先确认返回结构,不能盲目解构;isEmpty 要基于 repairList.length 计算,不能写死。
渲染列表与状态标签
为什么需要:接口只给数据,用户看到的是房屋信息、报修项目、预约时间、电话和状态。状态有 4 种,必须用不同类名区分,否则列表可读性差。
怎么想:用 wx:for 渲染 repairList,wx:key="id" 保证列表更新稳定;每个 van-cell-group 绑定 mark:id 和 bind:tap="goDetail",保留进入详情的交互。状态按笔记映射:0 已取消 cancel,1 受理中 info,2 上门中 success,3 已完成 complete。
怎么做:保留渲染代码,注意 wx:if 判断状态,空列表用 wx:else 展示提示:
xml
<authorization>
<block wx:if="{{!isEmpty}}">
<scroll-view show-scrollbar="{{false}}" enhanced scroll-y>
<view class="repairs">
<view class="repairs-title">我的报修</view>
<view class="repairs-list">
<van-cell-group
wx:for="{{repairList}}"
wx:key="id"
border="{{false}}"
mark:id="{{item.id}}"
bind:tap="goDetail"
>
<van-cell size="large" title="{{item.houseInfo}}">
<text wx:if="{{item.status === 0}}" class="tag cancel">已取消</text>
<text wx:if="{{item.status === 1}}" class="tag info">受理中</text>
<text wx:if="{{item.status === 2}}" class="tag success">上门中</text>
<text wx:if="{{item.status === 3}}" class="tag complete">已完成</text>
</van-cell>
<van-cell title="报修项目" border="{{false}}" value="{{item.repairItemName}}" />
<van-cell title="预约时间" border="{{false}}" value="{{item.appointment}}" />
<van-cell title="电话号码" border="{{false}}" value="{{item.mobile}}" />
</van-cell-group>
</view>
</view>
</scroll-view>
<view class="toolbar" bind:tap="addRepair">
<text class="enjoy-icon icon-repair"></text>
<text class="button-text">在线报修</text>
</view>
</block>
<view wx:else class="blank">
您还没有报修记录,请点击
<navigator hover-class="none" class="link" url="/repair_pkg/pages/form/index">添加</navigator>
</view>
</authorization>易错点:wx:key 不要用索引;状态判断用 ===,不要漏掉 0 这种假值判断;类名要和样式约定一致;bind:tap 不要写成 bindtap 混用;空状态链接路径要检查,避免点“添加”跳错页面。
空状态与交互入口
为什么需要:新用户没有报修记录时,如果只渲染空列表,页面会显得“坏了”;需要明确提示并给出添加入口。
怎么想:用 isEmpty 控制主列表和空状态二选一。转写稿里说列表和详情“没有专门再讲一遍的必要”,说明列表重点是接口、渲染和空状态,详情交互保留入口即可。
怎么做:当 !isEmpty 时展示 scroll-view、列表和底部“在线报修”工具栏;否则展示 wx:else 的“您还没有报修记录,请点击 添加”。点击工具栏走 addRepair,空状态里的 navigator 直接去 /repair_pkg/pages/form/index。
易错点:不要同时显示空状态和列表;isEmpty 初值未设置时,首次渲染可能闪烁,实际可结合加载状态处理;空状态文案里的“添加”要保留可点击链接,不要只写文字。
从列表进入详情:先拿到地址参数
为什么需要:报修详情页不能把某一条报修写死,列表里点哪一条,详情页就要展示哪一条。因此详情页必须先拿到报修 ID,再按 ID 请求数据。转写稿里也明确说,列表、详情这一块不再专门重复讲,大家要自己完成;但详情里涉及小程序地图,路径规划会单独讲。所以这一节的重点不是“再写一遍列表”,而是把“ID 从地址参数传进来”这条链路接好。
怎么想:列表跳详情时把 id 放在 URL 后面,详情页在 onLoad 的 query 参数里接收。onLoad({ id }) 是解构写法,等价于先拿 options 再取 options.id。拿到后不要立刻渲染,而是先校验,再请求接口。
怎么做:在 onLoad 中调用 getRepairDetail(id);接口地址是 /repair/ 拼接 id。请求成功后判断 code === 10000,再把 data 赋给 repairDetail。
易错点:转写稿里“地址参数据”应理解为“地址参数”;ID 缺失时不能继续请求,否则会请求到 /repair/undefined。另外 async/await 要写在 async 函数里,this.setData 不要漏。
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 })
},
})请求详情数据:校验、请求、渲染三步走
为什么需要:详情页的数据字段多,房屋信息、维修项目、手机号、预约日期、问题描述、附件都依赖接口返回。如果请求失败还继续渲染,页面会出现空值或旧数据。
怎么想:把请求拆成“参数校验 → 发请求 → 判断业务码 → setData”。wx.http.get 返回的 code 是业务状态码,data 才是详情对象,所以用解构并重命名 data: repairDetail,避免和页面 data 混淆。
怎么做:按上面代码执行。调试时可在 onLoad 里先打印 id,确认列表跳转是否带参;请求后再打印 code 和 repairDetail,确认字段名与页面绑定一致。
易错点:不要只看 HTTP 成功,还要看业务 code 是否为 10000;wx.utils.toast() 后要 return,避免继续执行。转写稿里“科论肽”是口误/同音,实际这里不涉及 currentTime,不要混入日期计算。
详情页渲染:房屋信息、报修信息与状态标签
为什么需要:接口只给数据,页面要按业务字段展示。状态不能用数字直接给用户看,要映射成“已取消、受理中、上门中、已完成”。
怎么想:房屋信息放一个 van-cell-group,报修信息放另一个;状态用 wx:if 判断 repairDetail.status。附件是数组,用 wx:for 渲染图片,wx:key="id"。
怎么做:保留原结构,把 repairDetail.houseInfo、repairItemName、mobile、appointment、description、attachment 绑定到对应组件。状态 0/1/2/3 分别对应四个标签。
易错点:wx:if 是严格判断,接口返回的 status 若是字符串 "2",=== 2 不成立;要确认接口类型。附件图片字段是 item.url,不要写成 item.src。
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>地图的条件展示:status 为 2 才显示
为什么需要:不是所有报修都需要地图。只有“上门中”才需要看师傅位置或路径规划,其他状态显示地图会干扰信息。
怎么想:用 repairDetail.status === 2 控制外层 view.polyline,地图只在这个容器里出现。转写稿提到“路径规划涉及小程序地图”,所以这里的地图是状态驱动,不是无条件渲染。
怎么做:保留 wx:if,地图的 latitude、longitude 由页面数据提供。注意本节 data 只初始化了 repairDetail,若接口未返回经纬度,需要按课程后续安排补充;不能凭空写死坐标。
易错点:状态判断写错会导致地图一直不出现或一直出现;地图宽高要给明确值,否则可能显示异常。
底部操作栏:修改常显,取消受状态限制
为什么需要:用户能否取消报修取决于业务状态。笔记明确:只有受理中(status === 1)才允许取消报修;“修改信息”则常显。
怎么想:修改按钮直接绑定 editRepair;取消按钮用 wx:if="{{repairDetail.status === 1}}" 控制展示。转写稿说修改、取消由大家自行完成,所以本节重点是“条件展示”,取消接口和事件需按后续业务补齐。
怎么做:保留 bind:tap="editRepair";取消按钮当前只有展示,没有绑定事件,实际项目要补 bind:tap 并调用取消接口,成功后刷新详情。
易错点:不要把所有状态都显示“取消报修”;取消后要重新请求详情或更新状态,不能只改页面文字。
路线规划要解决什么问题
为什么需要:路线规划用来在地图上展示两点之间的路线。它不是一个组件就能完成的功能,而是要把“地图渲染”和“路线坐标计算”分开处理:小程序 map 组件负责把地图、标记点和路线画出来,腾讯位置服务负责计算两点路线经过的坐标点。
怎么想:先明确两部分知识。第一部分是小程序提供的 map 组件,用来在页面渲染地图;第二部分是腾讯位置服务,用来计算两点路线的所有坐标点(经纬度)。地图组件只负责“画”,位置服务负责“算”,算出来的坐标再交给 polyline 渲染。
怎么做:页面里使用 map 组件,给它中心点经纬度、缩放比例、标记点 markers 和路线 polyline;在 onLoad 中调用路线规划方法获取坐标点,处理后 setData 给地图。
易错点:不要把 map 组件当成能自动算路线的组件;路线坐标必须通过位置服务计算。笔记中“经功”应为“经度”,“规订”应为“规定”,“提从”应为“提供”。
用 map 组件渲染地图
为什么需要:地图要先有中心点、缩放级别,才能把用户视线放到正确区域。map 组件提供 latitude 指定地图中心点纬度,longitude 指定地图中心点经度,scale 指定地图初始缩放比例,取值范围 3 - 20;markers 是地图上的标记点,polyline 是地图上的路线。
怎么想:latitude、longitude、scale 相对容易理解,先保证地图能正常显示;重点再处理 markers 和 polyline。标记点用于标出起点、终点或关键位置,路线用于展示两点间行进轨迹。
怎么做:在页面 data 中准备地图中心点和标记点数据。笔记示例:
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 定义标记点大小尺寸。不要把 id 漏掉,也不要把经纬度写反。
用 polyline 画出路线
为什么需要:markers 只能标点,不能表达两点之间怎么走。polyline 用来在地图上展示两点间的行进路线,需要传递路线对应的坐标点,也就是很多个点组成的线。
怎么想:polyline 本身不计算路线,它只接收坐标点数组。获取这些坐标点需要通过位置服务计算得到。所以顺序是:先调用路线规划拿到压缩坐标串,再解压成真正经纬度,最后加工成小程序地图组件能用的 points。
怎么做:调用位置服务的路线规划方法,把返回结果处理后设置到 polyline。笔记示例中 polyline 最终写成 { points, color: '#5591af', width: 4 },其中 points 是 { latitude, longitude } 对象数组。
易错点:不要把接口返回的原始压缩串直接塞给 polyline;也不要忘记 color、width 等渲染配置。路线不显示时,先检查 points 是否为空、经纬度字段名是否正确。
调用路线规划并解压坐标
为什么需要:位置服务返回的路线点串是经过压缩的,不能直接使用。笔记明确说明:计算出来的坐标点是经过压缩的,需要按官方指定方式解压才能获取真正坐标点,并且为了适应小程序地图组件用法,还需要二次加工。
怎么想:先看返回结构,路线在 result.routes[0].polyline;这个 polyline 是压缩点串。解压采用前向差分:从第 3 项开始,用前两项累加当前项除以 1000000。解压后再按每两个值取一组经纬度。
怎么做:笔记代码如下:
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},
],
})
},
})
},
})易错点:解压循环从 i = 2 开始,不能从 0 开始;Number() 转换不能漏;第二个循环步长是 i += 2;points[30] 取地图中心点时要确保数组长度足够。success 中用了箭头函数,能保持 this 指向页面实例,否则 this.setData 容易出错。
调试顺序与课堂简化说明
为什么需要:路线规划涉及地图组件和位置服务两部分,任何一部分数据不对都可能导致地图正常但路线不显示。转写稿中明确路径规划放在详情页讲,涉及小程序地图;课堂项目为了容易观察结果,把起点和终点写得离自己位置近一些。
怎么想:调试时先确认 markers 能否显示,再确认 qqMap.direction 是否成功返回 result.routes[0].polyline;然后确认解压后的 coors 和 points 是否有值;最后确认 setData 中 polyline 是否传入。不要一上来就怀疑地图组件。
怎么做:按笔记代码保留 from、to、mode: 'bicycling',成功后先解压再加工,最后 setData 更新 latitude、longitude 和 polyline。笔记也说明:真实企业项目中路线起点和终点由接口提供,课堂项目稍作简化,自己定义起点和终点,对知识点掌握没有影响。
易错点:课堂写死的 from、to 不能直接当成真实项目做法;真实项目要由接口提供起点终点。还要注意 points[30] 只是示例取点,实际数据不足时会报错。整体上,数据处理按腾讯位置服务规定和官方提供的方法应用即可,开发者不需要自己发明压缩规则。
课件图示








代码示例
示例一:在线报修表单页(WXML + JS + 日期格式化工具)
js
// utils/index.js —— 日期格式化工具(供表单页、访客页等复用)
export function formatDate(date) {
// 统一先转成 Date 对象:时间戳、日期字符串都能处理,更稳妥
const d = new Date(date)
const year = d.getFullYear()
const month = d.getMonth() + 1 // getMonth() 从 0 开始,必须 +1
const day = d.getDate()
// 月份、日期不足两位时补 0
return `${year}-${month < 10 ? '0' + month : month}-${day < 10 ? '0' + day : day}`
}xml
<!-- repair_pkg/pages/form/index.wxml -->
<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-field
model:value="{{mobile}}"
type="digit"
maxlength="11"
label="手机号码"
placeholder="请填写手机号码"
/>
<van-cell
title="预约日期"
value="{{appointment || '请选择预约日期'}}"
value-class="{{appointment && 'active-cell'}}"
bind:click="openDateLayer"
is-link
/>
<van-field
model:value="{{description}}"
type="textarea"
maxlength="200"
show-word-limit
label="问题描述"
placeholder="请填写问题描述"
/>
<van-uploader
bind:after-read="uploadPicture"
max-count="6"
preview-size="100"
file-list="{{attachment}}"
/>
<van-button type="primary" bind:click="submitForm">提交报修</van-button>
<!-- 选择报修房屋 -->
<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="{{minDate}}"
max-date="{{maxDate}}"
/>
</van-popup>js
// repair_pkg/pages/form/index.js
import wxValidate from 'wechat-validate'
import { formatDate } from '../../../utils/index'
Page({
behaviors: [wxValidate],
data: {
// 房屋
houseId: '',
houseName: '',
houseList: [],
houseLayerVisible: false,
// 维修项目
repairItemId: '',
repairItemName: '',
repairItem: [],
repairLayerVisible: false,
// 其他表单字段(必须放在 data 一级,双向绑定只认一级)
mobile: '',
appointment: '',
description: '',
attachment: [],
// 日期
dateLayerVisible: false,
currentDate: Date.now(),
minDate: Date.now(), // 最小可选:今天
maxDate: Date.now() + 1000 * 60 * 60 * 24 * 6, // 最大可选:今天往后 6 天
},
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 })
},
selectHouseInfo(ev) {
this.setData({
houseId: ev.detail.id,
houseName: ev.detail.name,
houseLayerVisible: false,
})
},
// ===== 维修项目 =====
openRepairLayer() {
this.setData({ repairLayerVisible: true })
},
closeRepairLayer() {
this.setData({ repairLayerVisible: false })
},
selectRepairItemInfo(ev) {
this.setData({
repairItemId: ev.detail.id,
repairItemName: ev.detail.name,
repairLayerVisible: false,
})
},
// ===== 预约日期 =====
openDateLayer() {
this.setData({ dateLayerVisible: true })
},
closeDateLayer() {
this.setData({ dateLayerVisible: false })
},
// confirm 事件返回的是时间戳,需要格式化成 YYYY-MM-DD 再提交
selectDateInfo(ev) {
this.setData({
appointment: formatDate(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) => {
// uploadFile 返回的是字符串,需要手动 JSON.parse
const data = JSON.parse(result.data)
if (data.code !== 10000) return wx.utils.toast('文件上传失败!')
// 先取已有的图片,再追加,最后整体 setData
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="{{houseList}}"渲染列表,选中数据从ev.detail.id / ev.detail.name取,不是ev.currentTarget。value-class="{{houseName && 'active-cell'}}":三元/短路表达式按条件加类名,未选择时保持默认灰色,选中后高亮,这是小程序里绑定类名的常用写法。- 日期是“弹出层 + 选择器”两层组件嵌套,
van-popup负责位置(position="bottom"),van-datetime-picker负责取值;type="date"只出年月日。 bind:confirm拿到的ev.detail是时间戳,后端要YYYY-MM-DD,必须格式化;getMonth()从 0 开始,忘+1会导致月份错一位。min-date设为Date.now()防止预约今天之前;max-date用Date.now() + 1000*60*60*24*6限定可选范围。wx.uploadFile的result.data是字符串,必须JSON.parse;成功后要把新地址push进file-list,组件才会渲染预览图。model:value双向绑定的字段必须写在data第一级,写成二级对象会失效。- 附件不是必填项,因此
rules里不写attachment。
示例二:报修列表页(WXML + JS)
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>js
// repair_pkg/pages/list/index.js
Page({
data: {
repairList: [],
isEmpty: false,
},
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,
})
},
// 进入详情页(mark:id 会以 id 的形式带到事件里)
goDetail(ev) {
wx.navigateTo({ url: `/repair_pkg/pages/detail/index?id=${ev.currentTarget.dataset.id}` })
},
// 去在线报修
addRepair() {
wx.navigateTo({ url: '/repair_pkg/pages/form/index' })
},
})关键行与易错点
- 列表数据在
data.rows里,解构时写成data: { rows: repairList },不要直接取data。 - 空列表要单独给用户提示,用
isEmpty = repairList.length === 0配合wx:if / wx:else控制。 - 状态是数字 0/1/2/3,分别对应
cancel / info / success / complete四个类名,不要写成字符串'0'。 mark:id="{{item.id}}"会渲染成data-id,点击时通过ev.currentTarget.dataset.id取值。- 报修提交成功后用的是
wx.redirectTo,避免用户返回到已提交的表单页。
示例三:报修详情页 + 路线规划(WXML + JS)
xml
<!-- repair_pkg/pages/detail/index.wxml -->
<scroll-view scroll-y enhanced show-scrollbar="{{false}}">
<view class="repair-detail">
<!-- 只有状态为“上门中”(2) 才展示地图与路线 -->
<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>
<!-- 只有受理中(1) 才允许取消报修 -->
<view wx:if="{{repairDetail.status === 1}}" class="button-text">取消报修</view>
</view>js
// repair_pkg/pages/detail/index.js
Page({
data: {
repairDetail: {},
// 地图标记点:每个标记点必须有 id
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 }) {
// 参数校验,防止直接打开页面时 id 为空
if (!id) return wx.utils.toast('参数有误!')
this.getRepairDetail(id)
// 生成路线
this.getPolyline()
},
// 报修详情接口
async getRepairDetail(id) {
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 }],
})
},
})
},
editRepair() {
const { id } = this.data.repairDetail
wx.navigateTo({ url: `/repair_pkg/pages/form/index?id=${id}` })
},
})关键行与易错点
onLoad({ id })直接解构地址参数,先判断if (!id)再发请求,避免拿undefined拼出错误接口地址。- 地图只在
status === 2(上门中)时渲染,取消按钮只在status === 1(受理中)时出现,条件写错会出现不该有的按钮。 markers里每个标记点必须指定id,否则地图会报错;iconPath可自定义图标,width/height控制尺寸。polyline需要的是坐标点数组,腾讯位置服务返回的polyline是压缩过的,必须按官方方式先做前向差分解压(Number(coors[i-2]) + Number(coors[i]) / 1000000),再两两组装成{ latitude, longitude }。map组件的latitude/longitude是地图中心点,这里取路线中间某个点(points[30]),保证整条路线基本在可视区域内。
常见错误
1. 选完房屋/维修项目后,文字仍是灰色,看不出"已选中"
- 现象:数据已存进
data,value也渲染出来了,但文字颜色跟未选择时一样。 - 原因:
value-class被写成了固定字符串(如value-class="active-cell"),或三元表达式判断的是错误的字段(判断了houseId却渲染houseName)。 - 修法:
value-class="{{houseName && 'active-cell'}}",判断条件必须和value里渲染的那个变量保持一致;active-cell定义在全局app.wxss里,确认已生效。
2. 预约日期提交后被后端打回,或详情页显示成 1726xxxxxxxx
- 现象:接口返回参数错误,或列表里预约时间是一串数字。
- 原因:
van-datetime-picker的bind:confirm事件里ev.detail是毫秒时间戳,不是字符串,直接赋给appointment就提交了。 - 修法:在
confirm回调里先格式化再setData,并同时关闭弹层:appointment: wx.utils.formatDate(ev.detail), dateLayerVisible: false。
3. 日期少一个月 / 出现 2024-9-7 这种不补零的格式
- 现象:选 10 月,显示成 9 月;或后端要求
YYYY-MM-DD但拿到2024-9-7。 - 原因:
Date.prototype.getMonth()返回值是 0–11;手拼字符串时没做补零。 - 修法:月份
getMonth() + 1;对月、日统一走String(v).padStart(2, '0')或v < 10 ? '0' + v : v。用toLocaleDateString()得到的是2024/9/7(斜杠),需.replaceAll('/', '-')。
4. 图片上传成功后预览区不出现新图
- 现象:
wx.uploadFile的 success 已执行,attachment数组长度也变了,但页面没变化,甚至要退出重进才显示。 - 原因:
attachment.push(...)是原地修改,数组引用没变,setData检测不到差异;或把接口返回的整个data.data结构弄错(file-list需要{ url }形式的对象数组)。 - 修法:
this.setData({ attachment: [...this.data.attachment, data.data] });同时确认data.code === 10000再追加,失败时不要污染列表。
5. 表单点提交直接通过校验,或必填项没选也提交成功
- 现象:什么都不填点提交,直接调了接口。
- 原因:
rules里的 key 与data里的字段名不一致(如rules写houseId、data写houseid);或behaviors: [wxValidate]漏写;或if (!this.validate()) return写成了this.validate()单独一行。 - 修法:逐字核对
rules的 key 与data字段名;validate()必须作为if条件并return;attachment非必填就不要写进rules。
6. 报修列表为空时白屏,空状态提示不出现
- 现象:没有报修记录时页面什么都不显示。
- 原因:
data里没给isEmpty初始值,首次渲染!isEmpty为true,走了"有数据"分支;或接口失败时提前return,isEmpty永远停在初始值。 - 修法:
data中初始化isEmpty: false(或直接改用wx:if="{{repairList.length}}"/wx:else),并在接口成功分支里统一setData({ repairList, isEmpty: repairList.length === 0 })。
7. 详情页 onLoad({ id }) 拿到 undefined
- 现象:进详情页提示"参数有误"。
- 原因:列表页跳转时把 id 拼在了路径里而非 query(
/detail/index/123而不是?id=123);或用了wx.redirectTo导致详情页返回按钮回不到列表。 - 修法:跳转统一
url: '/repair_pkg/pages/detail/index?id=' + item.id;列表→详情用navigateTo,提交成功→列表用redirectTo。
8. 地图上路线画不出来
- 现象:
map组件正常显示,markers有图标,但没有线。 - 原因:腾讯位置服务返回的
polyline是压缩后的差分坐标,未解压直接塞给points;或markers缺少id。 - 修法:严格按官方示例做前向差分还原,再两两分组转成
{ latitude, longitude };每个 marker 必须带唯一id。
练习
练习 1|日期格式化工具 在 utils.js 中实现并导出 formatDate(timestamp, separator = '-'),输出 YYYY-MM-DD,要求月、日补零,分隔符可配置。再用它改造表单页的 selectDateInfo,并在报修列表、详情页复用同一函数渲染 appointment。
提示:验证
formatDate(Date.now())、formatDate(new Date('2024-01-05').getTime())两个用例。
练习 2|预约日期可选范围 把日期选择器限制为"今天起 7 天内可选"(含今天),并让 max-date 在页面每次打开时都基于当天重新计算,而不是写死时间戳。
提示:
data中无法写表达式,需在onLoad里算好再setData;跨月/跨年时手动验一遍。
练习 3|上传附件的删除与限额 给 van-uploader 增加删除能力(bind:delete),删除后同步从 attachment 中移除对应项;并保证达到 6 张上限时给出提示而不是静默失效。
提示:
delete事件的ev.detail.index是待删项在file-list中的下标。
练习 4|列表状态与空态复用 把"报修状态 → 文案 + 类名"的映射抽成一个函数或 wxs,让列表页与详情页共用,避免两处 wx:if 各写一遍;同时补上接口失败时的空态处理。
提示:返回
{ text: '受理中', className: 'info' },模板里用{{statusMap[item.status].text}}。
练习 5|访客表单迁移 参照在线报修的写法,独立完成访客登记表单(房屋、访客姓名、手机号、访问日期、事由),要求复用 wechat-validate 规则和刚封装的 formatDate,不新写重复代码。
提示:完成后对比两份
rules与submitForm,找出能抽成公共 behavior 的部分。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 反馈-学习目标 | 04:40 |
| 2 | 在线报修-选择维修项目 | 09:55 |
| 3 | 在线报修-手机号和问题描述 | 07:33 |
| 4 | 在线报修-日期选择器-选择范围 | 11:05 |
| 5 | 在线报修-日期选择 | 12:12 |
| 6 | 在线报修-日期格式化 | 07:13 |
小结
- 这一类表单页的骨架高度同构:打开弹层 → 选中数据 → 存 id + name → 条件类名高亮 → 校验 → 提交 → 重定向,掌握一个即可批量复制。
- 所有"选择型"字段都要成对保存:
xxId给接口,xxName给视图,缺一个就会出现"提交成功但内容为空"或"显示正常但后端报错"。 - 高亮的本质是条件类名,条件表达式必须与渲染变量同源,样式类放全局以便多页复用。
- 组件事件的返回值形态要先打印再使用:
action-sheet给对象、datetime-picker给时间戳、uploader给{ file: { url } },直接臆测必然踩坑。 - 时间处理只有一条主线:时间戳 ↔ 显示字符串,格式化函数必须封装成工具,禁止在每个页面手拼
getFullYear + getMonth + 1。 setData只认引用变化,数组增删一律用展开/filter生成新数组,禁止push后直接setData原数组。- 状态码、空态、加载失败是列表页的"三件套",模板里用
wx:if / wx:else显式覆盖,不要依赖undefined的隐式转换。 - 提交成功后用
redirectTo而非navigateTo,避免用户回退到已提交的表单页重复下单。