Skip to content

访客管理(二) ​

本页对应 访客管理,共 5 段视频,约 40 分钟。

学习目标 ​

  • 能说出未登录用户通过分享链接查看访客详情二维码的实现思路,并在自定义分享路径上拼接登录态返回的加密参数,再在 onLoad 中取出该参数,调用带 share 路径的接口获取数据并渲染页面。
  • 能使用 wx:if 依据返回数据中是否存在加密字段来控制分享按钮的显示与隐藏,使页面本身由分享打开时不再展示分享入口。
  • 能独立完成“保存二维码到相册”功能:在小程序管理后台添加 download 服务器域名,依次调用 wx.getImageInfo(传入详情数据中的图片 url)与 wx.saveImageToPhotosAlbum(传入返回的 path),并用 try/catch 捕获失败后通过 wx.showToast 提示“保存图片失败,稍后重试”。
  • 能调用访客邀请列表接口(GET、携带 current 与 pageSize 分页参数),从返回结果中取出 rows 渲染列表,并根据 status(1 生效中 / 0 已失效)切换文案与样式类名,列表为空时展示提示内容。
  • 能安装并构建小程序端节流库,用其创建节流函数作为 scroll-view 滚动到底部事件的回调,验证重复滚动时只触发一次请求。
  • 能实现上拉加载分页:在实例上维护当前页码并逐次加一传入接口,将新数据与旧列表 concat 追加后重新 setData,并通过比较当前页码与返回的 pages 设置 hasMore,为 false 时终止后续请求。

知识点 ​

功能目标与两个 API ​

这一节要实现的是:用户点击“保存到本地”后,把页面上的二维码保存到手机相册。这个功能不是直接拿一个 URL 就能完成,而是要用两个小程序 API 组合实现:

  1. wx.getImageInfo:通过图片路径读取图片信息,成功后会返回图片的本地临时路径 path。
  2. wx.saveImageToPhotosAlbum:把指定 filePath 的图片保存到系统相册。

为什么需要两个 API?因为保存相册的 API 需要的是可保存的图片路径,而页面数据里拿到的是二维码地址。先读取图片信息,拿到返回的 path,再交给保存 API,链路才完整。

怎么想?先绑定按钮点击事件,再在事件回调里读取二维码图片信息,最后保存。怎么做?先看页面结构。

页面按钮与事件绑定 ​

笔记中的工具栏结构如下,其中“保存到本地”按钮通过 bind:tap="saveQRCode" 绑定点击事件:

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 bind:tap="saveQRCode" class="button-save">
    <text class="enjoy-icon icon-save"></text>
    <text class="text">保存到本地</text>
  </button>
</view>

这里的 wx:if="&#123;&#123;passport.encryptedData&#125;&#125;" 用于控制工具栏是否显示。转写稿中说明:如果当前页面本身就是通过分享打开的,就不需要再次分享,因此根据加密数据判断按钮显示与否。这个判断不是保存图片的核心,但属于本节页面结构的一部分。

易错点:按钮事件名要和 JS 中的方法名一致;不要误写成 bindtap 之外的其他事件名。保存图片逻辑应写在 saveQRCode 中。

读取图片信息 ​

为什么需要 wx.getImageInfo?因为保存相册不能直接使用页面展示的二维码地址,需要先读取图片信息,取得返回的 path。转写稿中的调试过程是:先调用 wx.getImageInfo,把结果打印出来,展开后可以看到宽高尺寸,以及一个 path 字段,这个 path 恰好就是后续保存要用的路径。

怎么想?二维码路径来自访客详情数据中的 passport.url,因此 src 应传 this.data.passport.url。怎么做?笔记代码如下:

javascript
wx.getImageInfo({
  src: 'https://domain.com/path/xxx.jpg',
  success(result) {
    console.log(result)
  }
})

实际项目中,src 换成二维码路径。转写稿中先用 await 拿结果并打印,确认返回结构后再解构 path。

易错点:调用 wx.getImageInfo 时,需要在小程序管理后台添加 download 域名,否则 API 无法正常使用。转写稿中演示了到后台修改 download 合法域名,扫码授权后保存并提交。另一个易错点是 src 不要写成页面路径,而应是二维码图片路径。

保存到相册 ​

为什么需要 wx.saveImageToPhotosAlbum?因为读取图片信息只是拿到路径,真正写入系统相册要靠这个 API。怎么想?把 wx.getImageInfo 返回的 path 传给 filePath。怎么做?笔记代码如下:

javascript
wx.saveImageToPhotosAlbum({
  filePath: '待保存的图片路径(通过 wx.getImageInfo 读取到的)',
  success(result) {
    console.log(result)
  }
})

转写稿中的调试过程是:第一次点击保存时会弹出授权窗口,点击允许后选择保存位置,例如保存到“下载”,再点存储;随后到下载目录中检查,确实出现了刚刚保存的二维码,说明功能实现成功。

易错点:filePath 必须使用读取到的 path,不要传原 URL;第一次调用会弹授权窗口,用户拒绝后保存会失败,因此后续要加异常处理。

组合实现与异常处理 ​

把两个 API 组合起来,笔记中的完整实现如下:

javascript
// visitor_pkg/pages/passport/index.js
Page({
  data: {},
  onLoad() {},
  // 保存二维码
  async saveQRCode() {
    try {
      // 读取图片信息
      const { path } = await wx.getImageInfo({
        // 二维码的图片路径
        src: this.data.passport.url,
      })
      // 保存图片到相册
      wx.saveImageToPhotosAlbum({ filePath: path })
    } catch (err) {
      wx.utils.toast('保存图片失败,稍后重试!')
    }
  }
})

为什么加 try/catch?转写稿中明确说,这段代码并不是永远都会执行成功,如果执行失败,要给用户提示。怎么想?把读取图片信息和保存相册放在 try 中,任何一步失败都进入 catch,通过 wx.utils.toast 提示“保存图片失败,稍后重试!”。怎么做?使用 async/await 让读取图片信息的异步流程更直观,再在 catch 中统一处理。

易错点:不要只写成功回调而忽略失败分支;wx.utils.toast 是项目中的工具方法,不是原生 API;提示语要按笔记保留。调试时可以先打印 getImageInfo 的返回结果确认 path,再正式保存。

调试过程与验证 ​

转写稿中的验证顺序很清楚:先在页面点击“保存到本地”,观察 getImageInfo 的返回结果;确认其中有 path 后,再把它传给保存相册 API;第一次保存时允许授权,选择保存位置;最后到下载目录中检查二维码是否真的保存成功。这个顺序能帮助定位问题:如果读取图片信息失败,先检查图片路径和 download 域名;如果保存失败,再检查授权状态和 filePath 是否正确。

易错点汇总 ​

第一,忘记在小程序管理后台配置 download 域名,导致 wx.getImageInfo 不可用。第二,把二维码 URL 直接传给 wx.saveImageToPhotosAlbum,而不是传 getImageInfo 返回的 path。第三,没有处理用户拒绝授权或保存失败的情况。第四,按钮事件名与 saveQRCode 不一致。第五,调试时只看控制台,不实际到相册或下载目录验证保存结果。按“读取图片信息 → 取 path → 保存相册 → 失败提示”的顺序实现,功能就完整了。

本节目标与整体思路 ​

为什么需要: 访客记录页要展示“我的访客”列表,数据来自后端接口;接口需要用户保持登录状态,并支持分页获取。列表为空时还要给用户“去添加”的引导,否则页面只剩空白。

怎么想: 先调接口拿第一页数据,把 rows 渲染成列表;再用 isEmpty 控制“列表/空态”;最后监听 scroll-view 的 scrolltolower 事件,滚动到底部时请求下一页,并用节流避免重复请求。

怎么做: 本节接口文档地址保留如下:接口文档地址。先获取第一页,再补分页。

易错点: 分页请求不能直接覆盖 visitorList,否则只剩最后一页;状态值映射要以接口文档和实际返回为准。

调用接口获取第一页数据 ​

为什么需要: 页面初始没有数据,必须在 onLoad 时调用 /visitor 接口,传入 current、pageSize,拿到 data.rows 作为访客列表。

怎么想: 虽然本节先看“不分页”的列表,但接口本身支持分页,所以方法参数先给默认值 current = 1、pageSize = 5,后面实现上拉加载时可以直接复用。

怎么做: 参考示例代码如下,保留原代码:

javascript
// visitor_pkg/pages/list/index.js
Page({
  data: {
    visitorList: [],
    isEmpty: false,
  },
  onLoad() {
    // 获取访客列表
    this.getVisitorList()
  },
  // 访客列表接口
  async getVisitorList(current = 1, pageSize = 5) {
    // 调用接口
    const {
      code,
      data: {rows: visitorList},
    } = await wx.http.get('/visitor', {current, pageSize})
    // 检测接口是否调用成功
    if (code !== 10000) return wx.utils.toast()
    // 渲染数据
    this.setData({
      isEmpty: visitorList.length === 0,
      visitorList,
    })
  },
})

易错点: 解构的是 data.rows,不是整个 data;接口失败要先 return,不能继续 setData。转写稿里“扣的/扣德”指 code,“rose”指 rows,写代码时要按接口字段写。

渲染列表与空态提示 ​

为什么需要: 有数据时展示访客卡片;没有数据时展示“还没有访客记录,请点击添加”,否则用户不知道下一步做什么。

怎么想: 用 isEmpty 做条件:!isEmpty 显示列表,否则显示空态。访客状态用 status 区分标签样式。

怎么做: 保留渲染代码如下:

xml
<block wx:if="{{!isEmpty}}">
  <scroll-view show-scrollbar="{{false}}" enhanced scroll-y>
    <view class="visitor">
      <view class="visitor-title">我的访客</view>
      <view class="visitor-list">
        <van-cell-group
          wx:for="{{visitorList}}"
          wx:key="id"
          border="{{false}}"
          bind:tap="goPassport"
        >
          <van-cell size="large" title="{{item.houseInfo}}">
            <text wx:if="{{item.status === 1}}" class="tag success">生效中</text>
            <text wx:if="{{item.status === 0}}" class="tag cancel">已失效</text>
          </van-cell>
          <van-cell title="访客姓名" border="{{false}}" value="{{item.name}}" />
          <van-cell title="手机号码" border="{{false}}" value="{{item.mobile}}" />
          <van-cell title="访问日期" border="{{false}}" value="{{item.visitDate}}" />
        </van-cell-group>
      </view>
    </view>
  </scroll-view>
</block>
<view wx:else class="blank">
  您还没有访客记录,请点击
  <navigator hover-class="none" class="link" url="/visitor_pkg/pages/form/index">添加</navigator>
</view>

易错点: 笔记文字写成“0 表示生效中、1 表示已失效”,但同页 WXML 和转写稿的接口说明是“1 生效中、0 已失效”,当前代码采用 status === 1 显示“生效中”。实际开发以接口文档和返回值为准。wx:if="&#123;&#123;!isEmpty&#125;&#125;" 的取反不要漏。

分页监听与节流处理 ​

为什么需要: 用户滚动到底部时要加载更多;但 scrolltolower 可能短时间重复触发,不节流会发出多次无意义请求。

怎么想: 把事件绑在 scroll-view 上,回调里请求下一页;回调用 miniprogram-licia 的节流函数包装,限制触发频率。

怎么做: 在 scroll-view 上补事件:

xml
<block wx:if="{{!isEmpty}}">
  <scroll-view
    bind:scrolltolower="getMoreVisitor"
    show-scrollbar="{{false}}"
    enhanced
    scroll-y
  >
    <view class="visitor">
      ....
    </view>
  </scroll-view>
</block>
<view wx:else class="blank">
  您还没有访客记录,请点击
  <navigator hover-class="none" class="link" url="/visitor_pkg/pages/form/index">添加</navigator>
</view>

安装 miniprogram-licia 后,在小程序开发者工具中执行“构建 npm”,再按库文档导入节流方法创建回调。转写稿里的“截流”应写“节流”,“s view”是 scroll-view。关键写法如下(字段名以接口文档为准):

javascript
this.current = 1
this.getMoreVisitor = throttle(() => {
  if (!this.data.hasMore) return
  this.current += 1
  this.getVisitorList(this.current)
}, 500)

易错点: 事件名是 bind:scrolltolower;节流函数要赋给 this.getMoreVisitor,WXML 才能找到回调;this 指向要用箭头函数保持。

页码递增、追加数据与终止请求 ​

为什么需要: 每次上拉都要请求“下一页”,并把新数据追加到旧列表;全部页请求完后必须停止,否则会继续发无效请求。

怎么想: 用 this.current 记录当前页,触发上拉时先判断 hasMore,再 current += 1 调接口;接口返回总页数后比较 current < pageTotal,更新 hasMore。

怎么做: 在 getVisitorList 中把渲染改为“第一页覆盖、后续追加”,并更新 hasMore:

javascript
const { code, data: { rows: visitorList, pageTotal } } = await wx.http.get('/visitor', { current, pageSize })
if (code !== 10000) return wx.utils.toast()
this.setData({
  isEmpty: visitorList.length === 0 && current === 1,
  visitorList: current === 1 ? visitorList : this.data.visitorList.concat(visitorList),
  hasMore: current < pageTotal,
})

易错点: 分页时直接 visitorList 会覆盖旧数据;hasMore 初始应为 true;转写稿中“配置t”指总页数字段,“toto”指总条数字段,实际字段名要对照接口文档。

调试与验收 ​

转写稿的调试过程可作为验收:进入页面先请求第一页,在 Network 中看到 rows 和总页数;滚动到底部时页码从 1 变 2、3、4、5,列表数量从 5 增到 10、15、20;请求到第 5 页后继续滚动不再发请求。列表为空时显示空态提示;状态标签按实际返回值显示“生效中/已失效”。如果滚动不触发,先检查事件是否绑在 scroll-view 上、scroll-y 是否开启、节流回调是否已赋值。

课件图示 ​

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

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

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

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

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

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

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

代码示例 ​

示例 1:保存二维码到相册(visitor_pkg/pages/passport) ​

xml
<!-- visitor_pkg/pages/passport/index.wxml -->
<view class="passport">
  <image class="qr-code" src="{{passport.url}}" />

  <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 bind:tap="saveQRCode" class="button-save">
      <text class="enjoy-icon icon-save"></text>
      <text class="text">保存到本地</text>
    </button>
  </view>
</view>
javascript
// visitor_pkg/pages/passport/index.js
Page({
  data: {
    passport: {}, // 访客详情数据,其中 url 就是二维码图片地址
  },

  onLoad() {},

  // 保存二维码
  async saveQRCode() {
    try {
      // 读取图片信息,返回结果中的 path 才是可用的本地路径
      const { path } = await wx.getImageInfo({
        // 二维码的图片路径
        src: this.data.passport.url,
      })
      // 保存图片到相册
      wx.saveImageToPhotosAlbum({ filePath: path })
    } catch (err) {
      wx.utils.toast('保存图片失败,稍后重试!')
    }
  },
})

关键行与易错点

  • wx.getImageInfo({ src }) 的 src 用 this.data.passport.url,即访客详情接口返回的二维码地址,不要直接拿页面上的 <image> 元素。
  • 必须把 wx.getImageInfo 返回结果里的 path(而不是原 src)传给 wx.saveImageToPhotosAlbum 的 filePath,两者混用会保存失败。
  • 这两个 API 都支持 Promise,所以能用 async/await 解构 path;整个流程用 try...catch 包住,失败时 wx.utils.toast 提示用户,避免静默失败。
  • 调用 wx.getImageInfo 前要先去小程序管理后台把图片域名加到 download 合法域名,否则接口直接报错。
  • wx.saveImageToPhotosAlbum 第一次调用会弹出授权窗口,用户拒绝后再次调用仍会失败,所以必须走 catch 分支。
  • 工具栏上的 wx:if="&#123;&#123;passport.encryptedData&#125;&#125;" 是分享页判断:页面本身是分享打开的(无加密数据)时不显示分享/保存按钮。

示例 2:访客列表获取与渲染(visitor_pkg/pages/list) ​

javascript
// visitor_pkg/pages/list/index.js
Page({
  data: {
    visitorList: [],
    isEmpty: false,
  },

  onLoad() {
    // 获取访客列表
    this.getVisitorList()
  },

  // 访客列表接口
  async getVisitorList(current = 1, pageSize = 5) {
    // 调用接口
    const {
      code,
      data: { rows: visitorList },
    } = await wx.http.get('/visitor', { current, pageSize })

    // 检测接口是否调用成功
    if (code !== 10000) return wx.utils.toast()

    // 渲染数据
    this.setData({
      isEmpty: visitorList.length === 0,
      visitorList,
    })
  },
})
xml
<!-- visitor_pkg/pages/list/index.wxml -->
<block wx:if="{{!isEmpty}}">
  <scroll-view show-scrollbar="{{false}}" enhanced scroll-y>
    <view class="visitor">
      <view class="visitor-title">我的访客</view>
      <view class="visitor-list">
        <van-cell-group
          wx:for="{{visitorList}}"
          wx:key="id"
          border="{{false}}"
          bind:tap="goPassport"
        >
          <van-cell size="large" title="{{item.houseInfo}}">
            <text wx:if="{{item.status === 1}}" class="tag success">生效中</text>
            <text wx:if="{{item.status === 0}}" class="tag cancel">已失效</text>
          </van-cell>
          <van-cell title="访客姓名" border="{{false}}" value="{{item.name}}" />
          <van-cell title="手机号码" border="{{false}}" value="{{item.mobile}}" />
          <van-cell title="访问日期" border="{{false}}" value="{{item.visitDate}}" />
        </van-cell-group>
      </view>
    </view>
  </scroll-view>
</block>

<view wx:else class="blank">
  您还没有访客记录,请点击
  <navigator hover-class="none" class="link" url="/visitor_pkg/pages/form/index">添加</navigator>
</view>

关键行与易错点

  • 接口返回结构是 { code, message, data: { rows, pageTotal, total } },列表数据在 data.rows 里,所以解构时写成 data: { rows: visitorList } 并重命名,别直接取 data。
  • code !== 10000 时先 return wx.utils.toast() 中断流程,避免把错误响应当列表渲染。
  • isEmpty: visitorList.length === 0 是空状态开关,WXML 里用 wx:if="&#123;&#123;!isEmpty&#125;&#125;" 显示列表、wx:else 显示提示,取反别写漏。
  • 状态判断必须与接口文档一致:status === 1 生效中配 success,status === 0 已失效配 cancel(笔记与转写稿此处描述相反,以接口文档为准,务必核对后再写类名)。
  • 页面是访客邀请记录,必须处于登录状态才能拿到数据,未登录时不调用该接口。

示例 3:上拉加载更多(分页 + 节流 + 终止请求) ​

xml
<!-- visitor_pkg/pages/list/index.wxml -->
<block wx:if="{{!isEmpty}}">
  <scroll-view
    bind:scrolltolower="getMoreVisitor"
    show-scrollbar="{{false}}"
    enhanced
    scroll-y
  >
    <view class="visitor">
      <view class="visitor-title">我的访客</view>
      <view class="visitor-list">
        <van-cell-group
          wx:for="{{visitorList}}"
          wx:key="id"
          border="{{false}}"
          bind:tap="goPassport"
        >
          <van-cell size="large" title="{{item.houseInfo}}">
            <text wx:if="{{item.status === 1}}" class="tag success">生效中</text>
            <text wx:if="{{item.status === 0}}" class="tag cancel">已失效</text>
          </van-cell>
          <van-cell title="访客姓名" border="{{false}}" value="{{item.name}}" />
          <van-cell title="手机号码" border="{{false}}" value="{{item.mobile}}" />
          <van-cell title="访问日期" border="{{false}}" value="{{item.visitDate}}" />
        </van-cell-group>
      </view>
    </view>
  </scroll-view>
</block>

<view wx:else class="blank">
  您还没有访客记录,请点击
  <navigator hover-class="none" class="link" url="/visitor_pkg/pages/form/index">添加</navigator>
</view>
javascript
// visitor_pkg/pages/list/index.js
import { throttle } from 'miniprogram-licia'

Page({
  data: {
    visitorList: [],
    isEmpty: false,
    hasMore: true, // 是否还有更多数据
  },

  onLoad() {
    // 记录当前页码,挂在 this 上方便在方法里读取
    this.cur = 1

    this.getVisitorList()

    // 用节流函数充当 scrolltolower 的事件回调
    this.getMoreVisitor = throttle(() => {
      // 没有更多数据就不再请求
      if (!this.data.hasMore) return
      // 页码累加后再请求
      this.getVisitorList(++this.cur)
    }, 500)
  },

  // 访客列表接口
  async getVisitorList(current = 1, pageSize = 5) {
    const {
      code,
      data: { rows: visitorList, pageTotal },
    } = await wx.http.get('/visitor', { current, pageSize })

    if (code !== 10000) return wx.utils.toast()

    this.setData({
      isEmpty: visitorList.length === 0,
      // 第一页直接覆盖,其余页追加到已有数据后面
      visitorList: current === 1 ? visitorList : [...this.data.visitorList, ...visitorList],
      // 总页数大于当前页,说明还有更多数据
      hasMore: pageTotal > current,
    })
  },
})

关键行与易错点

  • bind:scrolltolower 绑在 scroll-view 上(不是页面),并且 scroll-view 必须 scroll-y 且配合固定高度样式才能真正滚动到底触发。
  • this.cur = 1 挂在实例上而不是 data 里,页码只参与逻辑、不参与渲染,放 data 会触发无意义的重渲染。
  • this.getMoreVisitor = throttle(fn, 500) 是把节流函数赋值给事件回调名,WXML 中 bind:scrolltolower="getMoreVisitor" 才能找到它;scrolltolower 会短时间内重复触发,没有节流会发出一串重复请求。
  • 页码用 ++this.cur 先自增再传参:第 1 页已在 onLoad 用 current = 1 请求过,第一次触底应该是第 2 页。
  • 追加数据用 [...this.data.visitorList, ...visitorList],不能直接 setData({ visitorList }),否则上一页数据会被覆盖掉。
  • 终止请求靠 hasMore: pageTotal > current:当 pageTotal(转写稿里说的配置总页数)不大于当前页时置为 false,回调里 if (!this.data.hasMore) return 直接拦住后续请求。
  • miniprogram-licia 安装后要在开发者工具里执行一次「构建 npm」才能 import,否则报模块找不到。

常见错误 ​

  1. 点击“保存到本地”无反应或直接进 catch 现象:真机上调用 wx.getImageInfo 立即失败,控制台报域名不合法。 原因:只写了代码,没在小程序管理后台把图片所在域名加入 downloadFile 合法域名。 修法:后台「开发管理 → 服务器域名 → downloadFile 合法域名」添加域名并保存提交,重新编译后再验证;注意 download 与 request 是两套域名,不能互相顶替。

  2. 把二维码的网络 URL 直接塞给 wx.saveImageToPhotosAlbum 现象:报 filePath 无效 / saveImageToPhotosAlbum:fail file not found。 原因:该 API 的 filePath 只接受本地临时文件路径,网络地址必须先下载。 修法:先 await wx.getImageInfo({ src }) 取返回的 path,再把它传给 saveImageToPhotosAlbum,两步串联不可省。

  3. 用户拒绝过一次授权后,再点保存永远失败且没有任何提示 现象:第一次弹窗点了“拒绝”,之后点按钮静默失败。 原因:saveImageToPhotosAlbum 的授权被拒后不会再次弹窗,而代码没有 catch 分支。 修法:try/catch 包住整段逻辑,在 catch 中区分“用户取消”与“系统失败”,前者引导用户去设置页手动开启相册权限,后者给 wx.utils.toast('保存图片失败,稍后重试!')。

  4. 上拉加载后列表数据被“替换”而不是“追加” 现象:触底一次,列表条数仍是 5 条,第二页数据把第一页顶掉了。 原因:setData({ visitorList }) 直接赋新数组。 修法:先取旧数据再拼接,visitorList: this.data.visitorList.concat(newList),并注意 isEmpty 要基于拼接后的长度重新计算。

  5. 页码永远是 1,或触底后疯狂重复请求同一页 现象:Network 面板里连续多条 current=1,或页码在短时间内跳 1、2、3 但页面数据错乱。 原因:页码没有持久化在实例上(只存在于 getVisitorList 的形参默认值里);同时 scrolltolower 是高频事件,未节流。 修法:在 onLoad 里把页码挂到实例上(如 this.current = 1),每次请求前自增;把触底回调替换为 throttle 生成的函数再赋给 this.getMoreVisitor,从源头削掉重复触发。

  6. 数据取完了还在请求,或反过来第一页就判定“没有更多” 现象:一直触底一直发请求,code=10000 但返回空数组;或首次进入就 hasMore=false。 原因:没拿接口返回的总页数与当前页码做比较,或比较方向写反。 修法:请求成功后用 current < pageCount 计算 hasMore 并 setData,触底回调开头先 if (!this.data.hasMore) return;注意 hasMore 的初值应为 true,否则第一页之后就无法继续加载。

  7. 访客状态标签的文案与颜色错位 现象:状态为“生效中”的记录显示成灰色 cancel,已失效的反而显示绿色。 原因:status 的 0/1 语义与 success/cancel 类名对应关系写反,且没有对照接口文档核实。 修法:以接口文档为准逐条核对(0 与 1 谁是生效),把判断写成两个互斥的 wx:if,并在本地造两条不同状态的数据各验证一次,别只用一种状态的数据测试。

  8. 分享出去的页面里“分享给朋友”按钮还在,且二维码区域空白 现象:从分享链接打开,按钮仍然显示,但拿不到数据。 原因:按钮的显示条件没有绑定登录态标识;或分享路径上没有拼上那个加密参数,导致未登录接口拿不到数据。 修法:按钮容器用 wx:if="&#123;&#123;passport.encryptedData&#125;&#125;" 控制;分享路径上务必拼上该参数,并在 onLoad 里从 options 取到后传给未登录态接口,用“清掉登录态再打开分享页”的方式回归验证一次。

练习 ​

  1. 保存图片的完整链路排错:把 saveQRCode 中两步 API 的顺序故意调换、把 src 改成网络地址直接传给保存 API,各跑一次,记录两次报错信息,然后说明为什么这两步不能合并或调换。

  2. 授权拒绝的兜底:在保存二维码的流程里补上“用户拒绝相册授权”的分支,点击后给出可再次开启权限的引导(提示 + 跳转设置页),并用真机分别验证“首次允许 / 首次拒绝 / 拒绝后再点”三种路径。

  3. 分页状态机手写一遍:不看笔记,为访客列表实现 current、hasMore 两个状态,要求在 Network 面板中能观察到 1→2→3…递增、到最后一页后停止发请求;再故意把 hasMore 初值改成 false,观察并解释现象。

  4. 节流前后对比:把触底回调先写成普通函数,快速连续滑动触发多次,记录请求条数;再换成节流函数重复同样操作,对比请求条数,说明节流在这里省掉的到底是什么。

  5. 空列表与状态标签的双向验证:构造两种数据场景——① 一条访客记录都没有;② 同时存在 status 为 0 和 1 的记录。验证空态提示文案出现、状态标签的文案与颜色各自正确,并说明你是怎么确认 0/1 与“生效中/已失效”对应关系的。

原视频对照 ​

#视频时长
1享+生活-访客详情-209:50
2享+生活-访客详情-307:24
3享+生活-访客列表-109:43
4享+生活-访客列表-205:57
5享+生活-访客列表-307:38

小结 ​

  • 保存图片是「读信息 → 存相册」两步串联,中间那个本地 path 是唯一合法输入,任何一步跳过都会在真机暴露。
  • download 合法域名属于后台配置项,代码写得再对,域名没加就是必失败,联调前先把这类“环境前置条件”过一遍。
  • 相册保存是强授权能力,必须假设用户会拒绝,try/catch 加明确提示是标配而不是可选项。
  • 分页的核心不在请求本身,而在三件事:页码要挂在实例上累加、新数据要 concat 追加、hasMore 要能真正拦住后续请求。
  • scrolltolower 天然高频,节流是防止重复请求的第一道闸,缺了它后面所有分页逻辑都会被并发冲乱。
  • 空态、状态标签这类“展示分支”必须用双向数据验证(有数据 / 无数据、状态 A / 状态 B),单侧测试很容易把 0/1 映射写反还察觉不到。
  • 分享页的可用性依赖两件事同时成立:分享路径带上加密参数、按钮显示条件绑定登录态标识,两者都要在“清空登录态”的前提下回归。
  • 时间戳这类跨端字段要留意单位,接口给秒、前端要毫秒,缺一个 *1000 就会在页面上变成明显的错误日期。