主题
访客管理(二)
本页对应 访客管理,共 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 组合实现:
wx.getImageInfo:通过图片路径读取图片信息,成功后会返回图片的本地临时路径path。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="{{passport.encryptedData}}" 用于控制工具栏是否显示。转写稿中说明:如果当前页面本身就是通过分享打开的,就不需要再次分享,因此根据加密数据判断按钮显示与否。这个判断不是保存图片的核心,但属于本节页面结构的一部分。
易错点:按钮事件名要和 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="{{!isEmpty}}" 的取反不要漏。
分页监听与节流处理
为什么需要: 用户滚动到底部时要加载更多;但 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 是否开启、节流回调是否已赋值。
课件图示







代码示例
示例 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="{{passport.encryptedData}}"是分享页判断:页面本身是分享打开的(无加密数据)时不显示分享/保存按钮。
示例 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="{{!isEmpty}}"显示列表、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,否则报模块找不到。
常见错误
点击“保存到本地”无反应或直接进 catch 现象:真机上调用
wx.getImageInfo立即失败,控制台报域名不合法。 原因:只写了代码,没在小程序管理后台把图片所在域名加入downloadFile合法域名。 修法:后台「开发管理 → 服务器域名 → downloadFile 合法域名」添加域名并保存提交,重新编译后再验证;注意download与request是两套域名,不能互相顶替。把二维码的网络 URL 直接塞给
wx.saveImageToPhotosAlbum现象:报filePath无效 /saveImageToPhotosAlbum:fail file not found。 原因:该 API 的filePath只接受本地临时文件路径,网络地址必须先下载。 修法:先await wx.getImageInfo({ src })取返回的path,再把它传给saveImageToPhotosAlbum,两步串联不可省。用户拒绝过一次授权后,再点保存永远失败且没有任何提示 现象:第一次弹窗点了“拒绝”,之后点按钮静默失败。 原因:
saveImageToPhotosAlbum的授权被拒后不会再次弹窗,而代码没有catch分支。 修法:try/catch包住整段逻辑,在catch中区分“用户取消”与“系统失败”,前者引导用户去设置页手动开启相册权限,后者给wx.utils.toast('保存图片失败,稍后重试!')。上拉加载后列表数据被“替换”而不是“追加” 现象:触底一次,列表条数仍是 5 条,第二页数据把第一页顶掉了。 原因:
setData({ visitorList })直接赋新数组。 修法:先取旧数据再拼接,visitorList: this.data.visitorList.concat(newList),并注意isEmpty要基于拼接后的长度重新计算。页码永远是 1,或触底后疯狂重复请求同一页 现象:Network 面板里连续多条
current=1,或页码在短时间内跳 1、2、3 但页面数据错乱。 原因:页码没有持久化在实例上(只存在于getVisitorList的形参默认值里);同时scrolltolower是高频事件,未节流。 修法:在onLoad里把页码挂到实例上(如this.current = 1),每次请求前自增;把触底回调替换为throttle生成的函数再赋给this.getMoreVisitor,从源头削掉重复触发。数据取完了还在请求,或反过来第一页就判定“没有更多” 现象:一直触底一直发请求,
code=10000但返回空数组;或首次进入就hasMore=false。 原因:没拿接口返回的总页数与当前页码做比较,或比较方向写反。 修法:请求成功后用current < pageCount计算hasMore并setData,触底回调开头先if (!this.data.hasMore) return;注意hasMore的初值应为true,否则第一页之后就无法继续加载。访客状态标签的文案与颜色错位 现象:状态为“生效中”的记录显示成灰色
cancel,已失效的反而显示绿色。 原因:status的 0/1 语义与success/cancel类名对应关系写反,且没有对照接口文档核实。 修法:以接口文档为准逐条核对(0 与 1 谁是生效),把判断写成两个互斥的wx:if,并在本地造两条不同状态的数据各验证一次,别只用一种状态的数据测试。分享出去的页面里“分享给朋友”按钮还在,且二维码区域空白 现象:从分享链接打开,按钮仍然显示,但拿不到数据。 原因:按钮的显示条件没有绑定登录态标识;或分享路径上没有拼上那个加密参数,导致未登录接口拿不到数据。 修法:按钮容器用
wx:if="{{passport.encryptedData}}"控制;分享路径上务必拼上该参数,并在onLoad里从options取到后传给未登录态接口,用“清掉登录态再打开分享页”的方式回归验证一次。
练习
保存图片的完整链路排错:把
saveQRCode中两步 API 的顺序故意调换、把src改成网络地址直接传给保存 API,各跑一次,记录两次报错信息,然后说明为什么这两步不能合并或调换。授权拒绝的兜底:在保存二维码的流程里补上“用户拒绝相册授权”的分支,点击后给出可再次开启权限的引导(提示 + 跳转设置页),并用真机分别验证“首次允许 / 首次拒绝 / 拒绝后再点”三种路径。
分页状态机手写一遍:不看笔记,为访客列表实现
current、hasMore两个状态,要求在 Network 面板中能观察到 1→2→3…递增、到最后一页后停止发请求;再故意把hasMore初值改成false,观察并解释现象。节流前后对比:把触底回调先写成普通函数,快速连续滑动触发多次,记录请求条数;再换成节流函数重复同样操作,对比请求条数,说明节流在这里省掉的到底是什么。
空列表与状态标签的双向验证:构造两种数据场景——① 一条访客记录都没有;② 同时存在
status为 0 和 1 的记录。验证空态提示文案出现、状态标签的文案与颜色各自正确,并说明你是怎么确认 0/1 与“生效中/已失效”对应关系的。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 享+生活-访客详情-2 | 09:50 |
| 2 | 享+生活-访客详情-3 | 07:24 |
| 3 | 享+生活-访客列表-1 | 09:43 |
| 4 | 享+生活-访客列表-2 | 05:57 |
| 5 | 享+生活-访客列表-3 | 07:38 |
小结
- 保存图片是「读信息 → 存相册」两步串联,中间那个本地
path是唯一合法输入,任何一步跳过都会在真机暴露。 download合法域名属于后台配置项,代码写得再对,域名没加就是必失败,联调前先把这类“环境前置条件”过一遍。- 相册保存是强授权能力,必须假设用户会拒绝,
try/catch加明确提示是标配而不是可选项。 - 分页的核心不在请求本身,而在三件事:页码要挂在实例上累加、新数据要
concat追加、hasMore要能真正拦住后续请求。 scrolltolower天然高频,节流是防止重复请求的第一道闸,缺了它后面所有分页逻辑都会被并发冲乱。- 空态、状态标签这类“展示分支”必须用双向数据验证(有数据 / 无数据、状态 A / 状态 B),单侧测试很容易把 0/1 映射写反还察觉不到。
- 分享页的可用性依赖两件事同时成立:分享路径带上加密参数、按钮显示条件绑定登录态标识,两者都要在“清空登录态”的前提下回归。
- 时间戳这类跨端字段要留意单位,接口给秒、前端要毫秒,缺一个
*1000就会在页面上变成明显的错误日期。