主题
上拉分页与延迟提货(二)
本页对应 上拉分页与延迟提货,共 7 段视频,约 1 小时 9 分。
学习目标
- 能依据接口文档封装任务列表与任务详情 API 方法,正确传递
status/page/size及路径参数id,并通过浏览器 Network 面板验证请求参数、响应状态码与返回数据结构。 - 能在待提货列表组件中调用接口,解构
data.item渲染任务卡片,补充空列表提示,并说明上拉分页与下拉刷新的实现要点。 - 能根据任务状态
status为 1 或 2 条件渲染提货/交付联系人及预计、实际提货/送达时间,并验证不同状态下展示内容正确。 - 能按
status为 1、2、4 控制延迟提货/提货、异常上报/交付、回车登记按钮的显示,并正确区分跳转参数使用transportId还是id。 - 能使用 uni-app 条件编译实现 H5 显示搜索图标、APP/小程序显示扫码图标,并说明条件编译语法及兼容处理作用。
- 能通过为详情页时间线区域设置最小高度修复数据加载前后的布局抖动,并验证无数据与有数据时页面布局稳定。
知识点
1.2.1 开通 uniCloud:先把图片上传的“仓库”准备好
为什么需要:司机到现场提货后,要上传回单凭证和货品照片。图片不能只存在手机本地,否则后端拿不到、后续审核也无法查看。本节使用 uniCloud 云开发服务保存图片,因此要先开通免费云空间。
怎么想:先有云服务空间,再让项目关联服务空间,最后拿到 AppID 并重启项目。转写稿里也强调过,本项目会用到云开发,例如把图片上传到云服务器。
怎么做:
- 使用前面课程注册好的账号登录 uniCloud 控制台。

- 新建服务空间。

- 新建 uniCloud 开发环境。

- 关联服务空间。


- 获取 AppID。

- 重新启动项目,!!!非常重要!!!
易错点:关联后不重启项目,上传可能仍指向旧配置;服务空间、开发环境、AppID 要对应同一个项目,不要拿错账号或空间。
1.2.2 使用 uni-file-picker 上传图片
为什么需要:自己写选择图片、上传、回显、删除会比较繁琐。uni-file-picker 是 uni-app 扩展组件,能较方便地把图片上传到云空间并回显。
怎么想:页面里有两类图片:提货凭证图片、货品图片。每类都要限制类型和数量,并用 v-model 绑定数组,后续才能验证和提交。
怎么做:
vue
<!-- subpkg_task/pickup/index.vue -->
<script setup>
import { ref } from 'vue'
// 提货凭证图片
const receiptPictrues = ref([])
// 货品图片
const goodsPictrues = ref([])
</script>
<template>
<view class="page-container">
<view class="receipt-info">
<uni-file-picker
v-model="receiptPictrues"
file-extname="jpg,webp,gif,png"
limit="3"
title="请拍照上传回单凭证"
></uni-file-picker>
<uni-file-picker
v-model="goodsPictrues"
file-extname="jpg,webp,gif,png"
limit="3"
title="请拍照上传货品照片"
></uni-file-picker>
</view>
<button disabled class="button">提交</button>
</view>
</template>title:提示上传内容。limit:限制上传数量。file-extname:限制文件类型。v-model:用于回显上传的图片。@success、@fail:上传成功/失败回调,了解即可。
易错点:第二个上传组件必须绑定 v-model="goodsPictrues",否则后面验证货品图片时数组一直为空。笔记片段中第二个组件未写绑定,实际要按数据验证需要补上。
1.2.3 数据验证:两类图片都至少 1 张
为什么需要:如果司机只传了回单凭证、没传货品照片,提交后后端数据不完整,现场证据也不充分。因此提交前要校验两类图片都至少 1 张。
怎么想:用计算属性 enableSubmit 判断两个数组长度是否都大于 0,按钮的 disabled 取反。
怎么做:
vue
<!-- subpkg_task/pickup/index.vue -->
<script setup>
import { ref, computed } from 'vue'
// 提货凭证图片
const receiptPictrues = ref([])
// 货品图片
const goodsPictrues = ref([])
// 数据验证
const enableSubmit = computed(() => {
return goodsPictrues.value.length > 0 && receiptPictrues.value.length > 0
})
</script>
<template>
<view class="page-container">
<button :disabled="!enableSubmit" class="button">提交</button>
</view>
</template>易错点:limit="3" 只限制最多 3 张,不代表至少 1 张;最少数量要靠 enableSubmit 判断。不要漏写 .value。
1.2.4 提交表单:只把图片 url 交给后端
为什么需要:上传组件里的图片对象包含很多字段,但接口通常只需要图片地址。任务 ID 通过地址参数传入,提交时也要一并带上。
怎么想:先准备表单数据:id、cargoPickUpPictureList、cargoPictureList;两个图片数组用 map 二次处理,只保留 url。然后封装 pickup 接口并调用。
怎么做:
vue
<!-- subpkg_task/pickup/index.vue -->
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import taskApi from '@/apis/task'
// 任务ID
const id = ref('')
// 提货凭证图片
const receiptPictrues = ref([])
const cargoPickUpPictureList = computed(() => {
return receiptPictrues.value.map(({ url }) => {
return { url }
})
})
// 货品图片
const goodsPictrues = ref([])
const cargoPictureList = computed(() => {
return goodsPictrues.value.map(({ url }) => {
return { url }
})
})
onLoad((query) => {
id.value = query.id
})
const enableSubmit = computed(() => {
return goodsPictrues.value.length > 0 && receiptPictrues.value.length > 0
})
async function onSubmitForm() {
const formData = {
id: id.value,
cargoPickUpPictureList: cargoPickUpPictureList.value,
cargoPictureList: cargoPictureList.value,
}
const { code } = await taskApi.pickup(formData)
if (code !== 200) return uni.utils.toast('提交数据失败!')
uni.reLaunch({ url: '/subpkg_task/detail/index?id=' + id.value })
}
</script>
<template>
<view class="page-container">
<button @click="onSubmitForm" :disabled="!enableSubmit" class="button">提交</button>
</view>
</template>接口封装:
javascript
// apis/task.js
export default {
/**
* 提货
* @property {Object} data - 接口参数
*/
pickup(data) {
if (!data.id) return
return uniFetch.post('/driver/tasks/takeDelivery', data);
},
}易错点:转写稿里反复提醒:请求有没有带参数,不要只盯代码,要看请求地址或请求头;拿到数据先验证成功再解构;报错先读提示,例如 REF 未定义就是没引入。提交时若 id 为空、图片数组未处理成 { url },或未判断 code !== 200 就跳转,都会导致提交失败或页面状态错误。
1.3 在途:先明确状态与复用关系
转写稿中的“再涂/待途”按上下文应为“在途”,“代提货/待提过”应为“待提货”,“神领/所宁”应为“神领物流”。本节的核心是:司机完成提货后,运输任务状态 status 的值会变成 2;获取在途列表时使用的接口与待提货相同,区别只是传入的状态值不同。
为什么需要:待提货列表只展示还没提货的任务;司机提货后,任务进入在途,必须有一个列表继续承载运输中的任务。
怎么想:既然接口相同,就不要重复封装一套请求,而是复用 taskApi.list,把状态参数传成 2。
怎么做:在 delivery.vue 中挂载时请求列表,解构 code、data,成功后将 data.items 赋给列表,并维护空状态。
易错点:状态值不要误传 1;不要写成 data 而不是 data.items;空列表要单独提示;如果发现请求发了两次,转写稿提醒要检查旧测试代码是否没删干净。
vue
<!-- pages/task/components/delivery.vue -->
<script setup>
import { ref, onMounted } from 'vue'
import taskApi from '@/apis/task'
// 在途任务列表
const deliveryList = ref([])
// 在途列任务列表是否为空
const isEmpty = ref(false)
// 生命周期(获取数据)
onMounted(() => {
getDeliveryList()
})
// 在途任务列表
async function getDeliveryList() {
const { code, data } = await taskApi.list(2)
if (code !== 200) return uni.utils.toast('在途任务获取失败!')
// 渲染数据
deliveryList.value = data.items || []
isEmpty.value = deliveryList.value.length === 0
}
</script>
<template>
<scroll-view scroll-y class="scroll-view">
<view class="scroll-view-wrapper">
<view
v-for="delivery in deliveryList"
:key="delivery.id"
class="task-card"
>
<navigator
hover-class="none"
:url="`/subpkg_task/detail/index?id=${delivery.id}`"
>
<view class="header">
<text class="no">任务编号: {{ delivery.transportTaskId }}</text>
</view>
<view class="body">
<view class="timeline">
<view class="line">{{ delivery.startAddress }}</view>
<view class="line">{{ delivery.endAddress }}</view>
</view>
</view>
</navigator>
<view class="footer">
<view class="label">到货时间</view>
<view class="time">{{ delivery.planArrivalTime }}</view>
<navigator
hover-class="none"
:url="`/subpkg_task/delivery/index?id=${delivery.id}`"
class="action"
>
交付
</navigator>
</view>
</view>
<view v-if="isEmpty" class="task-blank">无在途货物</view>
</view>
</scroll-view>
</template>任务列表渲染与调试过程
为什么需要:接口拿到数据只是第一步,页面必须把任务编号、起止地址、到货时间渲染出来,并让用户能进入详情或交付。
怎么想:转写稿强调先确认请求成功,再做解构和渲染;看 Network 中请求地址/参数是否真的带上了状态值,而不是只盯着代码猜。
怎么做:用 v-for 遍历 deliveryList,key 用 delivery.id;任务编号取 transportTaskId,起止地址取 startAddress、endAddress,到货时间取 planArrivalTime;两个 navigator 分别跳详情和交付页。
易错点:字段名不要拼错;详情和交付跳转都要带 id;空状态用 isEmpty 控制,不要只写死“无在途货物”。
任务详情:状态大于等于 2 才展示提货信息
为什么需要:在途任务已经完成提货,详情页需要展示提货时提交的照片,方便后续交付核对。
怎么想:转写稿说明任务详情是共享页面,待提货和在途都会进入,因此要用状态做条件判断;提货信息只有提货后才会有数据,所以在途应展示。
怎么做:用 v-if="taskDetail.status >= 2" 包住“提货信息”面板;凭证照片遍历 cargoPictureList,货品照片遍历 cargoPickUpPictureList,key 和 src 都取 url。
易错点:两个图片字段不要混用;status 是数字比较;没有照片时列表为空但不应报错;不要把提货信息错误地展示在待提货状态。
vue
<!-- subpkg_task/detail/index.vue -->
<script setup>
//
</script>
<template>
<view class="page-container">
...
<scroll-view scroll-y class="task-detail">
<view class="scroll-view-wrapper">
...
<view v-if="taskDetail.status >= 2" class="panel pickup-info">
<view class="panel-title">提货信息</view>
<view class="label">提货凭证</view>
<view class="pictures">
<image
v-for="receipt in taskDetail.cargoPictureList"
:key="receipt.url"
class="picture"
:src="receipt.url"
/>
</view>
<view class="label">货品照片</view>
<view class="pictures">
<image
v-for="goods in taskDetail.cargoPickUpPictureList"
:key="goods.url"
class="picture"
:src="goods.url"
/>
</view>
</view>
...
</view>
</scroll-view>
...
</view>
</template>详情数据与易错点收束
任务详情中,cargoPictureList 提供提货凭证照片,cargoPickUpPictureList 提供提货商品照片。转写稿还提醒:凡是获取详情数据需要 ID 的,要先观察请求地址上有没有正确拼上 ID;先确认响应里有 data,再去解构和渲染,否则容易把“请求失败”误判成“渲染写错”。本节不额外添加笔记中不存在的图片;代码保持原样,便于对照实现。
代码示例
示例 1:提货页(图片上传 + 数据验证 + 提交表单)
vue
<!-- subpkg_task/pickup/index.vue -->
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import taskApi from '@/apis/task'
// 任务ID
const id = ref('')
// 提货凭证图片
const receiptPictrues = ref([])
// 货品图片
const goodsPictrues = ref([])
// 数据二次处理,只保留 url 属性
const cargoPickUpPictureList = computed(() => {
return receiptPictrues.value.map(({ url }) => {
return { url }
})
})
// 数据二次处理,只保留 url 属性
const cargoPictureList = computed(() => {
return goodsPictrues.value.map(({ url }) => {
return { url }
})
})
// 数据验证:每种类型的图片最少 1 张
const enableSubmit = computed(() => {
return goodsPictrues.value.length > 0 && receiptPictrues.value.length > 0
})
onLoad((query) => {
// 获取任务ID
id.value = query.id
})
// 提交表单数据
async function onSubmitForm() {
// 表单数据
const formData = {
id: id.value,
cargoPickUpPictureList: cargoPickUpPictureList.value,
cargoPictureList: cargoPictureList.value,
}
const { code } = await taskApi.pickup(formData)
if (code !== 200) return uni.utils.toast('提交数据失败!')
// 去到任务详情
uni.reLaunch({ url: '/subpkg_task/detail/index?id=' + id.value })
}
</script>
<template>
<view class="page-container">
<view class="receipt-info">
<uni-file-picker
v-model="receiptPictrues"
file-extname="jpg,webp,gif,png"
limit="3"
title="请拍照上传回单凭证"
></uni-file-picker>
<uni-file-picker
v-model="goodsPictrues"
file-extname="jpg,webp,gif,png"
limit="3"
title="请拍照上传货品照片"
></uni-file-picker>
</view>
<button @click="onSubmitForm" :disabled="!enableSubmit" class="button">
提交
</button>
</view>
</template>
<style lang="scss" scoped>
/* 笔记未给出样式,这里补充最小样式保证布局可运行 */
.page-container {
padding: 20rpx;
}
.receipt-info {
background-color: #fff;
padding: 20rpx;
border-radius: 12rpx;
}
.button {
margin-top: 40rpx;
}
</style>关键行与易错点
v-model="receiptPictrues":组件上传成功后会把{ url, name, size... }对象自动推入绑定的数组,回显和取数据都靠它;必须绑定ref数组,写成普通变量无法响应。file-extname限制文件类型、limit="3"限制数量,二者要与后面的验证逻辑(最少 1 张、最多 3 张)保持一致。computed里map(({ url }) => ({ url })):接口只要求传url,把组件产生的其他字段过滤掉;注意是解构取值后重新返回对象,不要直接返回原对象。enableSubmit依赖.value.length,模板中写:disabled="!enableSubmit"(computed在模板里自动解包,不能写enableSubmit.value)。- 笔记中按钮初始为
disabled静态属性,改成动态:disabled后才有“满足条件才可点”的效果;disabled状态下@click不触发。 - 易错:
ref数组忘了.value、onLoad里忘了接收query参数、提交时图片还在上传(数组里还没有url)都会导致提交数据为空。 uni.reLaunch跳详情时要拼回id,否则详情页取不到任务 ID。
示例 2:接口封装(apis/task.js)
javascript
// apis/task.js
import uniFetch from './uni-fetch'
export default {
/**
* 提货
* @property {Object} data - 接口参数
*/
pickup(data) {
if (!data.id) return
return uniFetch.post('/driver/tasks/takeDelivery', data)
},
}关键行与易错点
if (!data.id) return:没有任务 ID 直接返回,避免发出一个必然失败的请求,这是函数健壮性的兜底。- 使用统一封装的
uniFetch,域名、token已在请求拦截器里统一处理,页面里不需要再拼 baseURL。 - 用
export default {}导出对象,页面里import taskApi from '@/apis/task'后直接taskApi.pickup(...)调用。 - 易错:
import路径写成相对路径容易出错,统一用@/apis/task;方法名与页面调用名要一致(pickup不要写成pickUp)。 - 返回值是 Promise,页面中必须
await并解构{ code, data },code !== 200时用uni.utils.toast提示并return中断后续逻辑。
示例 3:在途任务列表(delivery.vue)
vue
<!-- pages/task/components/delivery.vue -->
<script setup>
import { ref, onMounted } from 'vue'
import taskApi from '@/apis/task'
// 在途任务列表
const deliveryList = ref([])
// 在途任务列表是否为空
const isEmpty = ref(false)
// 生命周期(获取数据)
onMounted(() => {
getDeliveryList()
})
// 在途任务列表
async function getDeliveryList() {
const { code, data } = await taskApi.list(2)
if (code !== 200) return uni.utils.toast('在途任务获取失败!')
// 渲染数据
deliveryList.value = data.items || []
isEmpty.value = deliveryList.value.length === 0
}
</script>
<template>
<scroll-view scroll-y class="scroll-view">
<view class="scroll-view-wrapper">
<view v-for="delivery in deliveryList" :key="delivery.id" class="task-card">
<navigator
hover-class="none"
:url="`/subpkg_task/detail/index?id=${delivery.id}`"
>
<view class="header">
<text class="no">任务编号: {{ delivery.transportTaskId }}</text>
</view>
<view class="body">
<view class="timeline">
<view class="line">{{ delivery.startAddress }}</view>
<view class="line">{{ delivery.endAddress }}</view>
</view>
</view>
</navigator>
<view class="footer">
<view class="label">到货时间</view>
<view class="time">{{ delivery.planArrivalTime }}</view>
<navigator
hover-class="none"
:url="`/subpkg_task/delivery/index?id=${delivery.id}`"
class="action"
>
交付
</navigator>
</view>
</view>
<view v-if="isEmpty" class="task-blank">无在途货物</view>
</view>
</scroll-view>
</template>关键行与易错点
taskApi.list(2):在途列表与待提货列表用的是同一个接口,区别只是状态值(待提货传1,在途传2)。- 列表组件在
onMounted中请求;待提货组件同为列表页,写法完全一致,只是状态值不同。 data.items || []:接口无数据时items可能为undefined,加默认值避免v-for报错;isEmpty用length === 0判断后再展示空提示。:key="delivery.id"必须用唯一值,不能用索引。- 两个跳转链接要分开写:点卡片主体进详情(
/subpkg_task/detail/index?id=),点“交付”按钮进交付页(/subpkg_task/delivery/index?id=)。第二个navigator放在外层view里、与第一个navigator同级,不要嵌套在第一个navigator内部,否则点击区域会互相干扰。 - 模板字符串用反引号拼
id,写成单引号会导致参数拼不进去。 - 易错:把
onMounted写成onLoad(组件中不可用),或忘记await导致data为undefined解构报错。 - 详情页展示提货照片时(
taskDetail.status >= 2)用cargoPictureList(提货凭证)和cargoPickUpPictureList(货品照片),注意这两个字段名与提交时的字段含义对应,别写反。
常见错误
现象: 提货提交后图片字段为空或接口报错。
原因: 直接把uni-file-picker的完整对象提交,或两类图片字段映射反了。
修法: 提交前映射为{ url },分别核对cargoPickUpPictureList对应提货凭证、cargoPictureList对应货品照片,提交前打印formData检查。现象: 选图后上传失败,拿不到云存储地址。
原因: uniCloud 服务空间未创建/未关联、AppID 未配置,或配置后未重启项目。
修法: 按控制台完成服务空间创建与关联,检查 AppID,重启项目后重新选图上传,确认返回url。现象: 待提货/在途列表请求参数不生效或列表不显示。
原因:status、page、pageSize未传或传错,或解构数据时没取data.items。
修法: 以 Network 面板请求 URL 为准核对参数;待提货传status=1、在途传status=2;渲染取data.items || []并处理空列表。现象: 详情页联系人、电话、时间显示错乱。
原因: 共享详情页未按status分支,状态 1 和状态 2 使用了同一套字段。
修法: 先列状态-字段矩阵:状态 1 用提货联系人/提货时间,状态 2 用交付联系人/送达时间;用v-if分开展示并分别验证。现象: 详情页按钮跳转后新页面取不到 ID,接口失败。
原因: 5 个操作按钮中,异常上报、回车登记传transportTaskId,其余按钮传id,发生混用。
修法: 逐个按钮对照接口文档和 Network 请求,统一参数命名;跳转后在新页面onLoad打印query验证。现象: H5 页面出现扫码图标,或条件编译不生效/报错。
原因: 平台条件写错、条件注释未成对闭合,或误以为可用“并且”组合平台。
修法: 用#ifdef APP-PLUS || MP与#ifdef H5成对编写;H5 显示搜索图标,APP/小程序显示扫码图标;切换运行平台验证。现象: 详情页首屏抖动,两个
line叠在一起。
原因: 无数据时容器未被撑开,数据回来后才改变高度。
修法: 给时间线容器设置min-height: 105rpx,用空数据和慢网场景复测。现象: 列表请求重复发送。
原因: 之前测试请求代码未删除,和正式请求同时执行。
修法: 清理遗留测试代码,在 Network 面板确认页面加载只发一次目标请求。
练习
在提货页完成两类图片上传,提交前打印
formData,确认只包含url且字段映射正确;用 0 张、1 张、超过 3 张验证提交按钮状态。分别在待提货、在途列表请求
status=1和status=2,补齐空列表提示,并用 Network 核对status/page/pageSize。用同一详情页分别造
status=1、status=2数据,验证联系人、联系电话、提货/送达时间分支;列出状态 4、6 待补内容。逐个点击详情页 5 个操作按钮,在新页面
onLoad打印query,确认transportTaskId与id没有混用。在任务列表页用条件编译处理图标:H5 显示搜索图标,APP/小程序显示扫码图标;切换运行平台验证。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 神领物流-待提货列表-1 | 12:59 |
| 2 | 神领物流-待提货列表-1 | 10:31 |
| 3 | 神领物流-任务详情-1 | 14:24 |
| 4 | 神领物流-任务详情-2 | 11:14 |
| 5 | 神领物流-任务详情-3 | 05:40 |
| 6 | 神领物流-任务详情-4 | 05:40 |
| 7 | 神领物流-条件编译应用 | 08:38 |
小结
- 排查参数问题先看 Network,请求 URL 或载荷里没有的参数,等于没传。
- 接口封装先判必传参数,页面调用先判
code,确认成功后再解构渲染。 - 共享详情页先列状态矩阵,再写
v-if,不要靠记忆混用字段。 - 图片上传要测完整链路:选图→上传成功→提交映射→详情回显。
- 多按钮页面逐个核对 ID 来源,参数错误会延迟到下一个页面才暴露。
- uniCloud、AppID 等环境配置变更后必须重启并复测上传。
- 空列表、无数据、慢网首屏是必测场景,样式要先占位避免抖动。
- 分页和下拉刷新可后置,但主流程状态顺序不能乱:待提货→在途→已完成。