Skip to content

表单验证与任务通知(二) ​

本页对应 表单验证与任务通知,共 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 把默认的覆盖掉了。

常见错误 ​

  1. 现象:请求头里看不到 authorization,或 token 偶发丢失。原因:合并时直接覆盖了 options,或没有合并回 options.header。修法:先构造默认头,再与 options.header 合并后赋回 options.header,并保证调用方显式 header 的优先级符合预期。
  2. 现象:登录后接口仍返回 500/401,排查发现 token 不对。原因:测试时在某个请求里手写了 authorization,把拦截器默认 token 覆盖了。修法:删除局部硬编码 header,统一依赖拦截器;确需覆盖时明确业务优先级并补测试。
  3. 现象:401 能跳登录页,但登录成功后回不到原页面。原因:跳登录时没拼 redirectUrl,或登录页没通过 onLoad 接收并保存。修法:响应拦截器用页面栈最后一项取当前页路径,拼 redirectUrl;登录页 onLoad 读取,登录成功后按该地址回跳。
  4. 现象:登录成功回跳时报错,无法跳转。原因:目标页是 tabBar 页面,却统一用了 redirectTo。修法:维护 tabBar 路径数组,用 includes 判断;tabBar 用 switchTab,非 tabBar 用 redirectTo,并把跳转方式传给登录页。
  5. 现象:上拉加载后旧列表消失,只剩新一页。原因:分页请求后直接 pickUpList.value = data.items。修法:追加 [...pickUpList.value, ...(data.items || [])];第 1 页刷新时先清空,再追加。
  6. 现象:下拉刷新动画不结束或重复触发。原因:isTriggered 未复位,或请求未 await 就关闭动画。修法:刷新回调中先置 true,await 列表请求,结束后置 false,并绑定 :refresher-triggered。
  7. 现象:详情页所有状态都显示“提货/延迟提货”等按钮。原因:工具栏条件写成 v-if="true" 或状态判断遗漏。修法:按 status 的 1/2/4 分别控制待提货、在途、已交付工具栏。
  8. 现象:异常上报、回车登记跳转后接口参数为空或 404。原因:把 id 和 transportTaskId 混用。修法:详情、提货、延迟提货用 id;异常上报、回车登记按文档用 transportTaskId。

练习 ​

  1. 复现并修复“请求头无 authorization”问题:分别用错误覆盖写法与正确合并写法各请求一次,对比 Network 请求头。
  2. 完成 401 回跳链路:清空 token 触发 401,确认登录页地址带 redirectUrl;登录成功后分别测试普通页与 tabBar 页回跳。
  3. 改造待提货列表:模拟接口返回多页数据,验证上拉追加、下拉刷新清空重载、hasMore 停止请求、空列表提示四项行为。
  4. 按 status 拆分任务详情工具栏:准备 1/2/4 三种状态数据,检查按钮文案、跳转地址参数名是否正确。
  5. 给任务详情搜索栏做条件编译检查:分别切换 H5、App、小程序端,确认图标与编译条件符合项目要求。

课堂画面 ​

课堂画面 01

课堂画面 02

课堂画面 03

课堂画面 04

课堂画面 05

课堂画面 06

原视频对照 ​

#视频时长
1神领物流-配置请求拦截器13:53
2关于optiosn.header解答07:43
3神领物流-响应拦截器-109:02
4补充getCurrentPage的使用11:11
5神领物流-响应拦截器-212:50
6神领物流-响应拦截器-311:49

小结 ​

  1. 拦截器里 header 合并是高频坑,重点验收“默认 token 是否稳定带上”和“局部 header 是否误覆盖”。
  2. 401 处理不是只跳登录,必须把回跳地址和回跳方式一起设计,否则登录闭环不完整。
  3. tabBar 页面跳转要单独判断,统一 redirectTo 会在真实登录回跳时暴露问题。
  4. 列表页要同时验收首屏、分页、刷新、空态和停止加载,不能只测第一页能显示。
  5. 任务详情按钮由状态驱动,模板里的 v-if 条件和跳转参数名要按接口文档逐项核对。
  6. 条件编译要分平台验收,不能只在当前调试端通过就认为完成。