Skip to content

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

本页对应 在线报修与无感刷新,共 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="&#123;&#123;repairDetail.status === 1&#125;&#125;" 控制展示。转写稿说修改、取消由大家自行完成,所以本节重点是“条件展示”,取消接口和事件需按后续业务补齐。

怎么做:保留 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] 只是示例取点,实际数据不足时会报错。整体上,数据处理按腾讯位置服务规定和官方提供的方法应用即可,开发者不需要自己发明压缩规则。

课件图示 ​

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

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

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

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

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

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

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

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

代码示例 ​

示例一:在线报修表单页(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="&#123;&#123;houseList&#125;&#125;" 渲染列表,选中数据从 ev.detail.id / ev.detail.name 取,不是 ev.currentTarget。
  • value-class="&#123;&#123;houseName && 'active-cell'&#125;&#125;":三元/短路表达式按条件加类名,未选择时保持默认灰色,选中后高亮,这是小程序里绑定类名的常用写法。
  • 日期是“弹出层 + 选择器”两层组件嵌套,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="&#123;&#123;item.id&#125;&#125;" 会渲染成 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="&#123;&#123;houseName && 'active-cell'&#125;&#125;",判断条件必须和 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="&#123;&#123;repairList.length&#125;&#125;" / 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' },模板里用 &#123;&#123;statusMap[item.status].text&#125;&#125;。

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

小结 ​

  1. 这一类表单页的骨架高度同构:打开弹层 → 选中数据 → 存 id + name → 条件类名高亮 → 校验 → 提交 → 重定向,掌握一个即可批量复制。
  2. 所有"选择型"字段都要成对保存:xxId 给接口,xxName 给视图,缺一个就会出现"提交成功但内容为空"或"显示正常但后端报错"。
  3. 高亮的本质是条件类名,条件表达式必须与渲染变量同源,样式类放全局以便多页复用。
  4. 组件事件的返回值形态要先打印再使用:action-sheet 给对象、datetime-picker 给时间戳、uploader 给 { file: { url } },直接臆测必然踩坑。
  5. 时间处理只有一条主线:时间戳 ↔ 显示字符串,格式化函数必须封装成工具,禁止在每个页面手拼 getFullYear + getMonth + 1。
  6. setData 只认引用变化,数组增删一律用展开/filter 生成新数组,禁止 push 后直接 setData 原数组。
  7. 状态码、空态、加载失败是列表页的"三件套",模板里用 wx:if / wx:else 显式覆盖,不要依赖 undefined 的隐式转换。
  8. 提交成功后用 redirectTo 而非 navigateTo,避免用户回退到已提交的表单页重复下单。