主题
表单验证与任务通知(二)
本页对应 表单验证与任务通知,共 6 段视频,约 1 小时 6 分。
学习目标
- 能够在前端项目的请求拦截器中,从 Pinia 用户状态中读取 token,并通过
authorization请求头将其携带到后续请求中。 - 能够使用对象合并方式配置
options.header,正确设置合并顺序,确保请求自身传入的 header 能覆盖默认 header。 - 能够在响应拦截器中判断响应状态码是否为
401,并在未登录或登录失效时跳转到登录页。 - 能够使用
getCurrentPage()获取页面栈,取最后一个页面单元中的路径,并将其作为登录成功后的回跳地址参数。 - 能够根据回跳地址判断目标页面是否为 tabBar 页面,并分别使用
switchTab或redirectTo完成登录成功后的页面跳转。
知识点
任务模块与待提货状态
为什么需要:任务模块是神领物流的核心业务,后台把运输任务派给司机,司机按状态完成提货、交付、登记。待提货是任务流程的起点。
怎么想:先按状态取任务列表,待提货对应 status = 1;列表项要能进入详情,满足条件时进入提货页。
怎么做:封装任务接口,在待提货组件中挂载时请求第一页数据并渲染。
易错点:详情、提货跳转参数名是 id;是否允许提货看 enablePickUp,不要只看状态。
封装任务列表接口
为什么需要:页面不直接写请求地址,统一放到 apis/task.js,便于维护。
怎么想:列表接口需要 status、page、pageSize,用项目封装的 uniFetch 发 GET。
怎么做:
javascript
// apis/task.js
import { uniFetch } from './uni-fetch'
export default {
list(status, page = 1, pageSize = 10) {
return uniFetch.get('/driver/tasks/list', { status, page, pageSize });
},
}易错点:待提货传 status = 1;页码和每页条数要有默认值。
待提货列表渲染与按钮兼容
为什么需要:把接口数据渲染成任务卡片,并让司机能进入详情或提货。
怎么想:列表数据用 ref([]) 保存;接口成功后再赋值;空列表给空状态。
怎么做:
vue
<!-- pages/task/components/pickup.vue -->
<script setup>
import { ref, onMounted } from 'vue'
import taskApi from '@/apis/task'
const pickUpList = ref([])
const nextPage = ref(1)
const pageSize = ref(5)
const isEmpty = ref(false)
onMounted(() => {
getPickUpList()
})
async function getPickUpList(page = 1, pageSize = 5) {
const { code, data } = await taskApi.list(1, page, pageSize)
if (code !== 200) return uni.utils.toast('获取列表失败,稍后重试!')
pickUpList.value = data.items || []
isEmpty.value = pickUpList.value.length === 0
}
</script>模板中详情和提货跳转:
vue
<navigator :url="`/subpkg_task/detail/index?id=${pickup.id}`">
...
</navigator>
<navigator
v-if="pickup.enablePickUp"
:url="`/subpkg_task/pickup/index?id=${pickup.id}`"
class="action"
>
提货
</navigator>
<navigator v-else url=" " class="action disabled">提货</navigator>易错点:微信小程序里 disabled 属性控制样式可能不生效,笔记改为类名控制:
css
.action {
position: absolute;
right: 0;
top: 50%;
display: flex;
align-items: center;
height: 64rpx;
padding: 0 40rpx;
background-color: $uni-primary;
color: #fff;
font-size: $uni-font-size-small;
border-radius: 64rpx;
transform: translate(0, -50%);
&.disabled {
background-color: #fadcd9;
}
}上拉分页
为什么需要:任务可能很多,一次全量请求不现实。
怎么想:监听 scrolltolower,请求下一页并追加到原数组。
怎么做:
vue
<script setup>
function onScrollToLower() {
if (!hasMore.value) return
getPickUpList(nextPage.value)
}
async function getPickUpList(page = 1, pageSize = 5) {
const { code, data } = await taskApi.list(1, page, pageSize)
if (code !== 200) return uni.utils.toast('获取列表失败,稍后重试!')
pickUpList.value = [...pickUpList.value, ...(data.items || [])]
nextPage.value = ++data.page
isEmpty.value = pickUpList.value.length === 0
hasMore.value = nextPage.value <= data.pages
}
</script>
<scroll-view @scrolltolower="onScrollToLower" scroll-y refresher-enabled class="scroll-view">
...
</scroll-view>易错点:下一页数据必须追加,不能直接覆盖;是否还有更多用 nextPage <= data.pages 判断;没有更多时直接 return,避免重复请求。
下拉刷新
为什么需要:司机在现场可能希望重新拉取最新任务。
怎么想:刷新本质是重新请求第 1 页,并清空旧列表。
怎么做:
vue
<script setup>
const isTriggered = ref(false)
async function onScrollViewRefresh() {
isTriggered.value = true
await getPickUpList()
isTriggered.value = false
}
async function getPickUpList(page = 1, pageSize = 5) {
const { code, data } = await taskApi.list(1, page, pageSize)
if (code !== 200) return uni.utils.toast('获取列表失败,稍后重试!')
if (page === 1) pickUpList.value = []
pickUpList.value = [...pickUpList.value, ...(data.items || [])]
nextPage.value = ++data.page
isEmpty.value = pickUpList.value.length === 0
hasMore.value = nextPage.value <= data.pages
}
</script>
<scroll-view
@scrolltolower="onScrollToLower"
@refresherrefresh="onScrollViewRefresh"
:refresher-triggered="isTriggered"
scroll-y
refresher-enabled
class="scroll-view"
>
...
</scroll-view>易错点:刷新时不清空会重复追加;关闭动画要等请求结束,因此用 async/await。
任务详情
为什么需要:详情页要展示起点/终点、对接人、异常、凭证等完整信息。
怎么想:列表跳转已带 id,详情页在 onLoad 取参,再按 id 请求详情。
怎么做:
javascript
// apis/task.js
detail(id) {
if (!id) return
return uniFetch.get(`/driver/tasks/details/${id}`);
}vue
<!-- subpkg_task/detail/index.vue -->
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import taskApi from '@/apis/task'
const taskDetail = ref({})
onLoad((params) => {
getTaskDetail(params.id)
})
async function getTaskDetail(id) {
if (!id) return
const { code, data } = await taskApi.detail(id)
if (code !== 200) return uni.utils.toast('获取数据失败, 稍后重试 !')
taskDetail.value = data
}
</script>模板按状态控制:status === 1 待提货,展示提货联系人、预计/实际提货时间,操作是延迟提货、提货;status === 2 在途,展示交付联系人、预计/实际送达时间,操作是异常上报、交付;status === 4 已交付,操作是回车登记。异常上报和回车登记传 transportTaskId,详情/提货/延迟提货传 id。
易错点:待提货状态下异常信息、提货信息、交付信息可能为空;模板中按钮文案要与业务状态一致,笔记里“支付”按上下文应为“交付”,以接口和设计为准。
条件编译
为什么需要:H5 端扫码能力受限,不同平台展示不同入口。
怎么想:用条件编译区分 H5、App、小程序。
怎么做:
vue
<view class="search-bar">
<!-- #ifdef H5 -->
<text class="iconfont icon-search"></text>
<!-- #endif -->
<!-- #ifdef APP-PLUS | MP -->
<text class="iconfont icon-scan"></text>
<!-- #endif -->
<input class="input" type="text" placeholder="输入运单号" />
</view>易错点:APP-PLUS 是 App 端,MP 是小程序端;条件编译注释不能写错平台名。
请求与登录回跳的调试补充
转写稿中调试到:任务接口需要携带 token,请求拦截器里用 Authorization 头,合并时注意顺序:
javascript
const headers = {}
headers.Authorization = userStore.token
options.header = Object.assign({}, headers, options.header)若写错成覆盖整个 options,会把 url 等参数丢掉;调试时先看网络请求头是否出现 Authorization。响应拦截器判断 401 跳登录,并用 getCurrentPages() 最后一个页面的 route 拼 redirectUrl;登录成功后,若是 tabBar 页面用 switchTab,否则用 redirectTo。
本节作业目标
本节作业是完成今天各小节标注的任务:配置请求拦截器,让后续请求自动携带 token;配置响应拦截器,在接口返回 401 时跳转登录页;登录成功后还要能回到原来的页面,并区分 tabBar 页面与普通页面选择不同跳转方式。下面按“为什么需要 → 怎么想 → 怎么做 → 易错点”梳理实现过程。
配置请求拦截器携带 token
为什么需要:登录后 token 已经持久化保存,但后续请求如果不带 token,后端无法识别用户身份,所以要在请求发出前统一加上请求头。
怎么想:请求拦截器会在每个请求发出前执行。通过自定义请求头 Authorization 传递 token,值从用户相关的 store 中读取。转写稿中反复提到“pinia”,实际就是通过 store 取数据。
怎么做:在 apis 下的统一请求封装文件中找到请求拦截器,先定义默认头信息,再与本次请求的 options.headers 合并:
js
import { useUserStore } from '@/store/user'
const userStore = useUserStore()
const defaultHeaders = {
Authorization: userStore.token
}
// 请求拦截器内
options.headers = Object.assign({}, defaultHeaders, options.headers)这样写的好处是:目前只有 Authorization,以后要加其他头信息,直接扩展 defaultHeaders 即可。
易错点:不要只定义变量却不合并到 options 上;也不要直接把 options 整个覆盖掉。转写稿中演示时曾写成覆盖 options,导致原来的 url、method 等信息丢失,后来修正为只处理 options.headers。
options.headers 合并与调试
为什么需要:请求拦截器里的 options 是本次请求真正使用的配置,只有把定义好的头信息合并进去,才会真正随请求发送。
怎么想:先打印 options 看结构,确认其中有 url、method,但不一定有 headers;再用 Object.assign 合并。合并顺序很关键:后面的对象会覆盖前面的同名属性,所以应让“本次请求自己的 headers”覆盖默认值,保证特殊接口可以自定义。
怎么做:可先调试:
js
console.log(options)确认后再合并。合并后再次发请求,在浏览器 Network 的请求头中检查 Authorization 是否出现。转写稿中还验证了扩展性:在默认头中加 abc: 123,请求头中也能看到。
易错点:Object.assign 的顺序写反会导致默认值覆盖调用方传入的值;调试时不要只看控制台,还要看 Network 请求头。
响应拦截器处理 401
为什么需要:用户未登录或 token 过期时,后端通常返回 401。这时不能只报错,而要引导用户重新登录。
怎么想:所有响应都会经过响应拦截器,因此可在响应拦截器中统一判断状态码。若为 401,说明未登录或登录失效,跳转登录页。
怎么做:
js
// 响应拦截器内
if (response.statusCode === 401) {
uni.navigateTo({
url: '/pages/login/index'
})
}测试时先清掉本地 token,再访问首页触发请求,观察是否跳到登录页。
易错点:判断字段要以后端实际返回结构为准;转写稿中使用的是 statusCode,不要与业务 code 混淆。测试前不清 token,可能看不到 401 效果。
页面栈与回跳原页面
为什么需要:直接跳登录页后,登录成功如果固定回首页,用户体验不好。应记录从哪个页面跳走,登录后回到原页面。
怎么想:用 getCurrentPages() 获取页面栈。转写稿总结:页面栈是数组,存放已打开页面,按打开顺序排列;返回会关闭一个页面;最后一个单元即当前页面。因此当前页面路径可从最后一个单元的 route 取。
怎么做:
js
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const redirectUrl = currentPage.route
uni.navigateTo({
url: `/pages/login/index?redirectUrl=/${redirectUrl}`
})登录页通过 onLoad 接收参数:
js
import { onLoad } from '@dcloudio/uni-app'
onLoad((query) => {
redirectUrl.value = query.redirectUrl
})易错点:不要写死取第 0 项,应取 pages.length - 1;地址参数要在 onLoad 中获取;拼参数时注意斜杠和编码。
tabBar 页面跳转方式区分
为什么需要:登录成功回跳时,如果原页面是 tabBar 页面,使用 redirectTo 会失败,因为 tabBar 页面要用 switchTab。
怎么想:先列出项目中的 tabBar 页面路径,判断回跳地址是否包含在数组里;包含则用 switchTab,否则用 redirectTo。
怎么做:
js
const tabBarPages = [
'/pages/task/index',
'/pages/message/index',
'/pages/my/index'
]
const rootType = tabBarPages.includes(redirectUrl)
? 'switchTab'
: 'redirectTo'
uni[rootType]({
url: redirectUrl
})易错点:includes 判断的是完整路径;tabBar 路径可从 pages.json 的 tabBar 配置中确认;不要把 switchTab 写成 switchType。
作业检查清单
完成后再检查:请求头是否带 Authorization;options.headers 是否只合并头信息而未覆盖整个 options;清掉 token 后是否返回 401 并跳登录;登录页是否拿到 redirectUrl;回跳 tabBar 页面是否使用 switchTab,普通页面是否使用 redirectTo。调试中出现覆盖、跳转失败都属正常过程,按打印结果逐步定位即可。
代码示例
1. 待提货任务列表(列表渲染 + 上拉分页 + 下拉刷新)
vue
<!-- pages/task/components/pickup.vue -->
<script setup>
import { ref, onMounted } from 'vue'
import taskApi from '@/apis/task'
// 任务列表数据
const pickUpList = ref([])
// 下一页页码
const nextPage = ref(1)
// 每页包含数据条数
const pageSize = ref(5)
// 是否为空列表
const isEmpty = ref(false)
// 是否还有更多数据
const hasMore = ref(true)
// 下拉刷新动画开关
const isTriggered = ref(false)
// 生命周期
onMounted(() => {
getPickUpList()
})
// 获取任务列表
async function getPickUpList(page = 1, pageSize = 5) {
const { code, data } = await taskApi.list(1, page, pageSize)
// 检测接口是否调用成功
if (code !== 200) return uni.utils.toast('获取列表失败,稍后重试!')
// 页面为 1 时,清空数组
if (page === 1) pickUpList.value = []
// 渲染任务列表(新数据追加到原数组后面)
pickUpList.value = [...pickUpList.value, ...(data.items || [])]
// 计算下一页页码
nextPage.value = ++data.page
// 判断列表是否为空
isEmpty.value = pickUpList.value.length === 0
// 判断还有没有更多的数据
hasMore.value = nextPage.value <= data.pages
}
// 监听页面是否滚动到底部
function onScrollToLower() {
// 没有更多数据时则不需要再请求了
if (!hasMore.value) return
// 获取下一页数据
getPickUpList(nextPage.value)
}
// 监听用户的下拉操作
async function onScrollViewRefresh() {
isTriggered.value = true
await getPickUpList()
// 关闭动画交互
isTriggered.value = false
}
</script>
<template>
<scroll-view
@scrolltolower="onScrollToLower"
@refresherrefresh="onScrollViewRefresh"
:refresher-triggered="isTriggered"
scroll-y
refresher-enabled
class="scroll-view"
>
<view class="scroll-view-wrapper">
<view v-for="pickup in pickUpList" :key="pickup.id" class="task-card">
<navigator
hover-class="none"
:url="`/subpkg_task/detail/index?id=${pickup.id}`"
>
<view class="header">
<text class="no">任务编号: {{ pickup.transportTaskId }}</text>
</view>
<view class="body">
<view class="timeline">
<view class="line">{{ pickup.startAddress }}</view>
<view class="line">{{ pickup.endAddress }}</view>
</view>
</view>
</navigator>
<view class="footer">
<view class="label">提货时间</view>
<view class="time">{{ pickup.planDepartureTime }}</view>
<navigator
v-if="pickup.enablePickUp"
hover-class="none"
:url="`/subpkg_task/pickup/index?id=${pickup.id}`"
class="action"
>
提货
</navigator>
<navigator v-else hover-class="none" url="" class="action disabled">
提货
</navigator>
</view>
</view>
<view v-if="isEmpty" class="task-blank">无待提货物</view>
</view>
</scroll-view>
</template>
<style lang="scss">
/* pages/task/components/style.scss */
.action {
position: absolute;
right: 0;
top: 50%;
display: flex;
align-items: center;
height: 64rpx;
padding: 0 40rpx;
background-color: $uni-primary;
color: #fff;
font-size: $uni-font-size-small;
border-radius: 64rpx;
transform: translate(0, -50%);
&.disabled {
background-color: #fadcd9;
}
}
</style>关键行与易错点
nextPage.value = ++data.page:下一页页码由接口返回的page自增得到,不要自己维护一个page++,否则与后端分页不一致。pickUpList.value = [...pickUpList.value, ...(data.items || [])]:分页数据必须追加到原数组,直接赋值会只剩最后一页;data.items可能为空,要兜底|| []。if (page === 1) pickUpList.value = []:下拉刷新本质是重新请求第 1 页,必须先清空原列表,否则数据重复。hasMore.value = nextPage.value <= data.pages:用总页数pages判断是否还有更多,onScrollToLower里先判断hasMore再请求。:refresher-triggered="isTriggered":true打开动画、false关闭,并且onScrollViewRefresh必须用async/await等请求结束再置false,否则动画立刻收回去。- 提货按钮禁用:微信小程序上
disabled属性控制样式不生效,改成用class="action disabled"由类名控制背景色。 - 跳转参数统一用
id,详情页和提货页都靠它取数据。
2. 任务详情页(onLoad 取 id + 详情接口 + 状态条件渲染)
vue
<!-- subpkg_task/detail/index.vue -->
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import taskApi from '@/apis/task'
// 任务详情
const taskDetail = ref({})
// 获取地址参数
onLoad((params) => {
// 获取任务 ID 并获取详情数据
getTaskDetail(params.id)
})
// 获取任务详情数据
async function getTaskDetail(id) {
if (!id) return
const { code, data } = await taskApi.detail(id)
if (code !== 200) return uni.utils.toast('获取数据失败, 稍后重试 !')
// 渲染数据
taskDetail.value = data
}
</script>
<template>
<view class="page-container">
<view class="search-bar">
<!-- #ifdef H5 -->
<text class="iconfont icon-search"></text>
<!-- #endif -->
<!-- #ifdef APP-PLUS | MP -->
<text class="iconfont icon-scan"></text>
<!-- #endif -->
<input class="input" type="text" placeholder="输入运单号" />
</view>
<scroll-view scroll-y class="task-detail">
<view class="scroll-view-wrapper">
<view class="basic-info panel">
<view class="panel-title">基本信息</view>
<view class="timeline">
<view class="line">{{ taskDetail.startAddress }}</view>
<view class="line">{{ taskDetail.endAddress }}</view>
<navigator hover-class="none" url="/subpkg_task/guide/index" class="guide">
<text class="iconfont icon-guide"></text>
<text>开始导航</text>
</navigator>
</view>
<view class="info-list">
<view class="info-list-item">
<text class="label">任务编号</text>
<text class="value">{{ taskDetail.transportTaskId }}</text>
</view>
<!-- 待提货展示数据 -->
<template v-if="taskDetail.status === 1">
<view class="info-list-item">
<text class="label">联系人</text>
<text class="value">{{ taskDetail.startHandoverName }}</text>
</view>
<view class="info-list-item">
<text class="label">联系电话</text>
<text class="value">{{ taskDetail.startHandoverPhone }}</text>
</view>
<view class="info-list-item">
<text class="label">预计提货时间</text>
<text class="value">{{ taskDetail.planDepartureTime }}</text>
</view>
<view class="info-list-item">
<text class="label">实际提货时间</text>
<text class="value">{{ taskDetail.actualDepartureTime }}</text>
</view>
</template>
<!-- 在途展示数据 -->
<template v-if="taskDetail.status === 2">
<view class="info-list-item">
<text class="label">交付联系人</text>
<text class="value">{{ taskDetail.finishHandoverName }}</text>
</view>
<view class="info-list-item">
<text class="label">联系电话</text>
<text class="value">{{ taskDetail.finishHandoverPhone }}</text>
</view>
<view class="info-list-item">
<text class="label">预计送达时间</text>
<text class="value">{{ taskDetail.planArrivalTime }}</text>
</view>
<view class="info-list-item">
<text class="label">实际送达时间</text>
<text class="value">{{ taskDetail.actualArrivalTime }}</text>
</view>
</template>
</view>
</view>
<view class="except-info panel">
<view class="panel-title">异常信息</view>
</view>
<view class="panel pickup-info">
<view class="panel-title">提货信息</view>
</view>
<view class="delivery-info panel">
<view class="panel-title">交货信息</view>
</view>
</view>
</scroll-view>
<!-- 待提货:延迟提货 + 提货 -->
<view class="toolbar" v-if="taskDetail.status === 1">
<navigator
:url="`/subpkg_task/delay/index?id=${taskDetail.id}`"
hover-class="none"
class="button secondary"
>
延迟提货
</navigator>
<navigator
:url="`/subpkg_task/pickup/index?id=${taskDetail.id}`"
hover-class="none"
class="button primary"
>
提货
</navigator>
</view>
<!-- 在途:异常上报 + 交付 -->
<view class="toolbar" v-if="taskDetail.status === 2">
<navigator
:url="`/subpkg_task/except/index?transportTaskId=${taskDetail.transportTaskId}`"
hover-class="none"
class="button secondary"
>
异常上报
</navigator>
<navigator
:url="`/subpkg_task/delivery/index?id=${taskDetail.id}`"
hover-class="none"
class="button primary"
>
交付
</navigator>
</view>
<!-- 已交付:回车登记 -->
<view class="toolbar" v-if="taskDetail.status === 4">
<navigator
:url="`/subpkg_task/record/index?transportTaskId=${taskDetail.transportTaskId}`"
hover-class="none"
class="button primary block"
>
回车登记
</navigator>
</view>
</view>
</template>关键行与易错点
onLoad((params) => getTaskDetail(params.id)):uni-app 里取地址参数只能用onLoad,参数名必须和列表页?id=保持一致。if (!id) return:无 id 直接返回,避免拿undefined拼出/details/undefined这种无效请求。- 状态值
1 / 2 / 4 / 6分别对应待提货、在途、已交付、已完成,各块信息用v-if分开,不要用v-show混写。 - 参数名区别:延迟提货、提货、交付传的是
id(任务主键),异常上报、回车登记传的是transportTaskId(任务编号),传错后端查不到数据。 - 待提货状态下,异常信息、提货信息、交付信息都是空的,属于正常现象,不要误以为接口没返回。
- 条件编译:
#ifdef H5显示搜索图标、#ifdef APP-PLUS | MP显示扫码图标,H5 端扫码能力受限,所以用编译条件区分。
3. 请求 / 响应拦截器(携带 token + 401 回跳登录)
javascript
// apis/uni-fetch.js
import { useUserStore } from '@/store/user'
// 当前项目所有 tabBar 页面(与 pages.json 中 tabBar.list 对应)
const tabBarPages = [
'/pages/task/index',
'/pages/message/index',
'/pages/my/index',
]
// 请求拦截器
const requestInterceptor = (options) => {
// 通过 pinia 读取 token
const userStore = useUserStore()
// 统一管理头信息,方便后续扩展(如再加 abc: 123)
const defaultHeader = {}
defaultHeader.authorization = userStore.token
// 合并:默认头信息 + 请求自带的头信息(后者优先级更高)
options.header = Object.assign({}, defaultHeader, options.header)
return options
}
// 响应拦截器
const responseInterceptor = (response) => {
// 401:用户未登录 或 登录时效已过
if (response.statusCode === 401) {
// 页面栈的最后一个单元即为当前页面
const pageStack = getCurrentPages()
const currentPage = pageStack[pageStack.length - 1]
// 拼接成绝对路径
const redirectURL = '/' + currentPage.route
// tabBar 页面只能用 switchTab 跳转,其余用 redirectTo
const routeType = tabBarPages.includes(redirectURL)
? 'switchTab'
: 'redirectTo'
uni.redirectTo({
url: `/pages/login/index?redirectURL=${redirectURL}&routeType=${routeType}`,
})
}
return response
}javascript
// pages/login/index.vue —— 登录成功后回跳到原页面
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
// 要跳回的页面路径
const redirectURL = ref('')
// 页面跳转方式(redirectTo / switchTab)
const routeType = ref('redirectTo')
// 获取地址中的参数
onLoad((query) => {
redirectURL.value = query.redirectURL
routeType.value = query.routeType
})
// 登录成功之后执行
uni[routeType.value]({ url: redirectURL.value })关键行与易错点
options.header = Object.assign({}, defaultHeader, options.header):必须合并到options.header上。如果写成options = Object.assign({}, ...)会把整个options(含url、method)覆盖掉;如果写成options.header = Object.assign({}, options.header)又丢了 token。Object.assign是后面的覆盖前面的,所以options.header放最后:默认每个请求都带 token,个别接口传了自己的authorization时能覆盖默认值,扩展性和灵活性才够。getCurrentPages()是全局函数,返回页面栈数组,最后一个单元才是当前页面,取pageStack[pageStack.length - 1],不要写死[0](页面跳转多次后[0]是首页)。redirectURL前要手动加/,否则redirectTo会按相对路径解析,跳错页面。- tabBar 页面不能用
redirectTo跳转,否则报错;要用switchTab,所以要先用tabBarPages.includes(redirectURL)判断,再决定跳转方式。 uni[routeType.value]({ url: redirectURL.value })用中括号取方法名,把「跳去哪」和「怎么跳」两个参数都从地址里取,登录页不用写死跳转方式。- 调试时如果发现 token 没带过去,先检查是不是某个请求自己写了
header把默认的覆盖掉了。
常见错误
- 现象:请求头里看不到
authorization,或 token 偶发丢失。原因:合并时直接覆盖了options,或没有合并回options.header。修法:先构造默认头,再与options.header合并后赋回options.header,并保证调用方显式 header 的优先级符合预期。 - 现象:登录后接口仍返回 500/401,排查发现 token 不对。原因:测试时在某个请求里手写了
authorization,把拦截器默认 token 覆盖了。修法:删除局部硬编码 header,统一依赖拦截器;确需覆盖时明确业务优先级并补测试。 - 现象:401 能跳登录页,但登录成功后回不到原页面。原因:跳登录时没拼
redirectUrl,或登录页没通过onLoad接收并保存。修法:响应拦截器用页面栈最后一项取当前页路径,拼redirectUrl;登录页onLoad读取,登录成功后按该地址回跳。 - 现象:登录成功回跳时报错,无法跳转。原因:目标页是 tabBar 页面,却统一用了
redirectTo。修法:维护 tabBar 路径数组,用includes判断;tabBar 用switchTab,非 tabBar 用redirectTo,并把跳转方式传给登录页。 - 现象:上拉加载后旧列表消失,只剩新一页。原因:分页请求后直接
pickUpList.value = data.items。修法:追加[...pickUpList.value, ...(data.items || [])];第 1 页刷新时先清空,再追加。 - 现象:下拉刷新动画不结束或重复触发。原因:
isTriggered未复位,或请求未await就关闭动画。修法:刷新回调中先置true,await列表请求,结束后置false,并绑定:refresher-triggered。 - 现象:详情页所有状态都显示“提货/延迟提货”等按钮。原因:工具栏条件写成
v-if="true"或状态判断遗漏。修法:按status的 1/2/4 分别控制待提货、在途、已交付工具栏。 - 现象:异常上报、回车登记跳转后接口参数为空或 404。原因:把
id和transportTaskId混用。修法:详情、提货、延迟提货用id;异常上报、回车登记按文档用transportTaskId。
练习
- 复现并修复“请求头无
authorization”问题:分别用错误覆盖写法与正确合并写法各请求一次,对比 Network 请求头。 - 完成 401 回跳链路:清空 token 触发 401,确认登录页地址带
redirectUrl;登录成功后分别测试普通页与 tabBar 页回跳。 - 改造待提货列表:模拟接口返回多页数据,验证上拉追加、下拉刷新清空重载、
hasMore停止请求、空列表提示四项行为。 - 按
status拆分任务详情工具栏:准备 1/2/4 三种状态数据,检查按钮文案、跳转地址参数名是否正确。 - 给任务详情搜索栏做条件编译检查:分别切换 H5、App、小程序端,确认图标与编译条件符合项目要求。
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 神领物流-配置请求拦截器 | 13:53 |
| 2 | 关于optiosn.header解答 | 07:43 |
| 3 | 神领物流-响应拦截器-1 | 09:02 |
| 4 | 补充getCurrentPage的使用 | 11:11 |
| 5 | 神领物流-响应拦截器-2 | 12:50 |
| 6 | 神领物流-响应拦截器-3 | 11:49 |
小结
- 拦截器里 header 合并是高频坑,重点验收“默认 token 是否稳定带上”和“局部 header 是否误覆盖”。
- 401 处理不是只跳登录,必须把回跳地址和回跳方式一起设计,否则登录闭环不完整。
- tabBar 页面跳转要单独判断,统一
redirectTo会在真实登录回跳时暴露问题。 - 列表页要同时验收首屏、分页、刷新、空态和停止加载,不能只测第一页能显示。
- 任务详情按钮由状态驱动,模板里的
v-if条件和跳转参数名要按接口文档逐项核对。 - 条件编译要分平台验收,不能只在当前调试端通过就认为完成。