uni-app(神领物流)项目实战 - 第3天

学习目标:

  • 能够独立完成任务通知、公告、任务列表的功能
  • 能够在 uni-app 中实现下拉刷新的交互功能
  • 能够在 uni-app 中实现上拉分页的交互功能
  • 能够配合 v-if 和 v-show 实现组件缓存的功能
  • 能够独立完成任务详情展示的功能
  • 能够利用条件编译控制页面内容的展示

一、【神领物流】消息

在 uni-app 跨端开发时,小程序不支持内置组件open in new window Component 和 Keep-Alive,在实现类似标签页切换的功能时就变得十分的麻烦,在本项目中采用的是组合 v-ifv-show 指令来实现。

  • v-if 指令保证组件只被加载一次
  • v-show 指令控制组件的显示/隐藏

关于这部分交互的代码逻辑我已经添加到项目中了,在这里特别给大家交待的目的是要大家知道 uni-app 在跨端开发的时候需要考虑到一些兼容性。

<!-- pages/message/index.vue -->
<script setup>
  import { ref, reactive } from 'vue'
  
  import slNotify from './components/notify'
  import slAnnounce from './components/announce'

  // Tab 标签页索引
  const tabIndex = ref(0)
  const tabMetas = reactive([
    {title: '任务通知',rendered: true},
    { title: '公告',rendered: false},
  ])
  // 切换标签页
  function onTabChange(index) {
    tabMetas[index].rendered = true
    tabIndex.value = index
  }
</script>

<template>
  <view class="page-container">
    ...
    <view v-show="tabIndex === 0" v-if="tabMetas[0].rendered" class="message-list">
      <sl-notify />
    </view>
    <view v-show="tabIndex === 1" v-if="tabMetas[1].rendered" class="message-list">
      <sl-announce />
    </view>
  </view>
</template>
  • rendered 属性为 true 表明组件加载过,配合 v-if 来使用
  • tabIndex 变量控制当前显示哪个组件,配合 v-show 来使用

1.1 任务通知

任务通知是后台系统给司机推送的运输任务,除了获取通知列表外还包含了一系列的交互,如下拉刷新、上拉分页,我们分别来实现。

1.1.1 通知列表

根据文档调用接口即可获取到通知列表数据,将数据渲染到页面当中即可,这一步相对要容易实现一些,文档的详细说明在这里open in new window,分成两个步骤实现:

  1. apis 目录中添加一个专门用于消息的接口请求模块
// apis/message.js
// 引入网络请求模块
import { uniFetch } from './uni-fetch'

export default {
  /**
   * 消息列表
   * @property {string} contentType - 消息类型
   * @property {string} page - 消息数据对应的页码
   * @property {string} pageSize - 每页包含消息数据的条数
   */
  list(contentType, page = 1, pageSize = 10) {
    if (!contentType) return
    return uniFetch.get('/driver/messages/page', { contentType, page, pageSize })
  },
}
  1. 到消息列表页面中调用 API 来获取列表数据并渲染
<!-- pages/message/components/notify.vue -->
<script setup>
  import { ref, onMounted } from 'vue'
  import messageApi from '@/apis/message'

  // 任务列表数据
  const notifyList = ref([])
  // 下一页页码
  const nextPage = ref(1)
  // 每页包含数据条数
  const pageSize = ref(5)
  // 是否为空列表
  const isEmpty = ref(false)
	
  // 生命周期
  onMounted(() => {
    getNotifyList()
  })

  // 任务列表
  async function getNotifyList(page = 1, pageSize = 10) {
    const { code, data } = await messageApi.list(201, page, pageSize)
    // 检测接口是否调用成功
    if (code !== 200) return uni.utils.toast('获取通知失败,稍后重试!')
    // 渲染数据
    notifyList.value = data.items || []
    // 是否为空列表
    isEmpty.value = notifyList.value.length === 0
  }
</script>
<template>
  <scroll-view class="scroll-view" refresher-enabled scroll-y>
    <view class="scroll-view-wrapper">
      <view class="message-action">
        <text class="iconfont icon-clear"></text> 全部已读
      </view>
      <uni-card
        v-for="notify in notifyList"
        :key="notify.id"
        :border="false"
        :is-shadow="false"
      >
        <view class="brief">{{ notify.content }}</view>
        <view class="extra">
          <text class="time">{{ notify.created }}</text>
          <navigator
            hover-class="none"
            class="link"
            :url="`/subpkg_task/detail/index?id=${notify.id}`"
          >查看详情</navigator>
        </view>
        <template v-slot:title>
          <view :class="{ unread: !notify.isRead }" class="title unread"
            >您有新的运输任务</view
          >
        </template>
      </uni-card>
      <view v-if="isEmpty" class="message-blank">暂无消息</view>
    </view>
  </scroll-view>
</template>

在对数据渲染时需要注意以下几个关键数据:

  • isEmpty 变量,判断列表数据是否为空,如果为空则显示一个占位提示信息
  • isRead 属性,判断当前任务数据是否为已读,值为1表示已读,值为0表示未读

另外还有一点大家要注意,在上述代码中关于生命命周期的使用,由于是在组件中使用所以我们用的是 Vue 的生命周期函数 onMounted(用其它的也可以)。

1.1.2 上拉分页

在大多的项目中列表数据都会支持分页获取数据,在移动设备是常常配合页面的滚动来分页获取数据,在 scroll-view 组件中通过 scrolltolower 监听页面是否滚动到达底部,进而触发新的请求来获取分页的数据。

  1. 监听 scrolltolower 事件
<!-- pages/message/components/notify.vue -->
<script setup>
  import { ref, onMounted } from 'vue'
  import messageApi from '@/apis/message'

  // 任务列表数据
  const notifyList = ref([])
  // 下一页页码
  const nextPage = ref(1)
  // 每页包含数据条数
  const pageSize = ref(5)
  // 是否为空列表
  const isEmpty = ref(false)

  onMounted(() => {
    getNotifyList()
  })

  // 上拉分页
  function onScrollToLower() {
    // 获取下一页数据
    getNotifyList()
  }

  // 任务列表
  async function getNotifyList(page = 1, pageSize = 10) {
    // 省略中间部分代码...
  }
</script>
<template>
  <scroll-view
    @scrolltolower="onScrollToLower"
    class="scroll-view"
    refresher-enabled
    scroll-y
  >
    ...
  </scroll-view>
</template>
  1. 计算下一页页码

页码是数字具具有连续性,我们只需要热行加 1 操作即可

<!-- pages/message/components/notify.vue -->
<script setup>
  import { ref, onMounted } from 'vue'
  import messageApi from '@/apis/message'

  // 任务列表数据
  const notifyList = ref([])
  // 下一页页码
  const nextPage = ref(1)
  // 每页包含数据条数
  const pageSize = ref(5)

  onMounted(() => {
    getNotifyList()
  })

  // 上拉分页
  function onScrollToLower() {
    // 获取下一页数据
    getNotifyList(nextPage.value)
  }

  // 任务列表
  async function getNotifyList(page = 1, pageSize = 10) {
    const { code, data } = await messageApi.list(201, page, pageSize)
    // 检测接口是否调用成功
    if (code !== 200) return uni.utils.toast('获取通知失败,稍后重试!')
    // 渲染数据
    notifyList.value = [...notifyList.value, ...(data.items || [])]
    // 更新下一页页码
    nextPage.value = ++data.page
    // 是否为空列表
    isEmpty.value = notifyList.value.length === 0
  }
</script>

上述代码中有两点需要注意:

  • 更新页面是根据返回数据中的 page 加 1 的方式处理的
  • 分页请求来的下一页数据需要追加到原数组中,在这里用的 ... 运算符,也可以使用数组的 concat 方法
  1. 判断是否还有更多的数据

如果没有更多数据,滚动页面到达底部时无需请求数据。通过对比总的页码与当前页面的大小来判断。

<!-- pages/message/components/notify.vue -->
<script setup>
  import { ref, onMounted } from 'vue'
  import messageApi from '@/apis/message'

  // 省略中间部分代码...
  
  // 是否还有更多数据
  const hasMore = ref(true)
	
	// 省略中间部分代码...

  // 上拉分页
  function onScrollToLower() {
    if(!hasMore.value) return
    // 获取下一页数据
    getNotifyList(nextPage.value)
  }

  // 任务列表
  async function getNotifyList(page = 1, pageSize = 10) {
    const { code, data } = await messageApi.list(201, page, pageSize)
    // 检测接口是否调用成功
    if (code !== 200) return uni.utils.toast('获取通知失败,稍后重试!')
    // 渲染数据
    notifyList.value = [...notifyList.value, ...(data.items || [])]
    // 更新下一页页码
    nextPage.value = ++data.page
    // 是否还有更多数据
    hasMore.value = nextPage.value <= data.pages
    // 是否为空列表
    isEmpty.value = notifyList.value.length === 0
  }
</script>

上述代码中需要注意判断还有没有更多数据,是根据接口返回数据中的总页码 pages 进行判断的,如果下一页的页码 nextPage 小于等于总页码 data.page 时,表明还有更多的数据,否则没有更多数据了。

1.1.3 下拉刷新

下拉刷新帮助用户入时获取最新的数据,在 scroll-view 中监听 refresherrefresh 事件,能够知道用户是否执行了下拉的动作,进而重新获取页面的数据。

  1. 监听用户的下拉操作,通过监听 refresherrefresh 事件
<!-- pages/message/components/notify.vue -->
<script setup>
  import { ref, onMounted } from 'vue'
  import messageApi from '@/apis/message'

  // 省略中间部分代码...

  // 下拉刷新
  async function onScrollViewRefresh() {
    await getNotifyList()
  }

  // 上拉分页
  function onScrollToLower() {
    // 省略中间部分代码...
  }

  // 任务列表
  async function getNotifyList(page = 1, pageSize = 10) {
    const { code, data } = await messageApi.list(201, page, pageSize)
    // 检测接口是否调用成功
    if (code !== 200) return uni.utils.toast('获取通知失败,稍后重试!')
    // 清空原始的数据
    if (page === 1) notifyList.value = []
    // 渲染数据
    notifyList.value = [...notifyList.value, ...(data.items || [])]
    // 更新下一页页码
    nextPage.value = ++data.page
    // 是否为空列表
    isEmpty.value = notifyList.value.length === 0
    // 是否有更多数据
    hasMore.value = nextPage.value <= data.pages
  }
</script>
<template>
  <scroll-view
    @refresherrefresh="onScrollViewRefresh"
    @scrolltolower="onScrollToLower"
    class="scroll-view"
    refresher-enabled
    scroll-y
  >
    ...
  </scroll-view>
</template>

上述代码中需要关注的两个方面:

  • 刷新请求实际上是重新请求第 1 页的数据
  • 在进行数据渲染时需要清空原列表中的数据
  1. 关闭下拉刷新的动画交互
<!-- pages/message/components/notify.vue -->
<script setup>
  import { ref, onMounted } from 'vue'
  import messageApi from '@/apis/message'

  // 省略中间部分代码...
	
  // 是否加载完成
  const isTriggered = ref(false)

	// 省略中间部分代码...

  // 下拉刷新
  async function onScrollViewRefresh() {
    isTriggered.value = true
    await getNotifyList()
    isTriggered.value = false
  }

  // 上拉分页
  function onScrollToLower() {
    // 省略中间部分代码...
  }

  // 任务列表
  async function getNotifyList(page = 1, pageSize = 10) {
    // 省略中间部分代码...
  }
</script>
<template>
  <scroll-view
    @refresherrefresh="onScrollViewRefresh"
    @scrolltolower="onScrollToLower"
    :refresher-triggered="isTriggered"
    class="scroll-view"
    refresher-enabled
    scroll-y
  >
  ...   
  </scroll-view>
</template>

上述代码中要注意:

  • scroll-view 下拉刷新的动画交互需要通过 refresher-triggiered 来打开或关闭,如果值为 true 时打开,值为 false 时关闭
  • 需要为调用方法指定 async/await 在请求结束后再关闭动画交互

1.1.4 全部已读

封装接口

apis/message.js


/** 全部已读
   * @param {Object} contentType
   */
  readAll(contentType) {
    if (!contentType) return
    return uniFetch.put(`/driver/messages/readAll/${contentType}`)
  },

找到全部已读按钮

<view class="message-action" @click="readAll">

添加方法

//全部已读
  const readAll = async () => {
    await messageApi.readAll(201)
    //刷新列表
    isTriggered.value = true
  }

1.2 公告

公告是后台系统给司机发送的能知,如公司的政策、节假日注意事项等,其所包含的功能逻辑与消息通知非常类似。

1.2.1 公告列表

根据文档调用接口即可获取到公告列表数据,将数据渲染到页面当中即可,文档的详细说明在这里open in new window,不难发现公告和任务通知是共用相同的接口,只是传递的参数值不同,contentType 值为 201 时表示任务通知,值为 200 时表示公告。

  1. 到页面中调接口获取数据并渲染
<!-- pages/message/components/announce.vue -->
<script setup>
  import { ref, onMounted } from 'vue'
  import messageApi from '@/apis/message'

  // 任务列表数据
  const announceList = ref([])
  // 下一页页码
  const nextPage = ref(1)
  // 每页包含数据条数
  const pageSize = ref(5)

  // 生命周期
  onMounted(() => {
    getAnnounceList()
  })

  // 任务列表
  async function getAnnounceList(page = 1, pageSize = 10) {
    const { code, data } = await messageApi.list(200, page, pageSize)
    // 检测接口是否调用成功
    if (code !== 200) return uni.utils.toast('获取通知失败,稍后重试!')
    // 渲染数据
    announceList.value = data.items || []
  }
</script>
<template>
  <scroll-view class="scroll-view" refresher-enabled scroll-y>
    <view class="scroll-view-wrapper">
      <view class="message-action">
        <text class="iconfont icon-clear"></text>
        全部已读
      </view>
      <uni-list :border="false">
        <uni-list-item
          v-for="announce in announceList"
          :key="announce.id"
          :to="`/subpkg_message/content/index?id=${announce.id}`"
          ellipsis="1"
          :title="announce.title"
          :right-text="announce.created"
        >
          <template v-slot:header>
            <text v-if="!announce.isRead" class="dot"></text>
          </template>
        </uni-list-item>
      </uni-list>
      <view v-if="isEmpty" class="message-blank">暂无消息</view>
    </view>
  </scroll-view>
</template>

和任务通知一样,在对数据渲染时需要注意以下几个关键数据:

  • isEmpty 变量,判断列表数据是否为空,如果为空则显示一个占位提示信息
  • isRead 属性,判断当前任务数据是否为已读,值为1表示已读,值为0表示未读

1.2.2 上拉分页

此时实现上拉分面加载的思路与任务通知完全一样,先去监听 scrolltolower 事件,然后发起请求,再然后判断是否有更多数据。

  1. 监听 scrolltolower 事件
<!-- pages/message/components/announce.vue -->
<script setup>
  import { ref, onMounted } from 'vue'
  import messageApi from '@/apis/message'

	// 省略中间部分代码...

  // 上拉分页
  function onScrollToLower() {
    // 获取下一页数据
    getAnnounceList()
  }

  // 任务列表
  async function getAnnounceList(page = 1, pageSize = 10) {
    // 省略中间部分代码
  }
</script>
<template>
  <scroll-view
    @scrolltolower="onScrollToLower"
    class="scroll-view"
    refresher-enabled
    scroll-y
  >
    ...
  </scroll-view>
</template>
  1. 计算下一页页码
<!-- pages/message/components/announce.vue -->
<script setup>
  import { ref, onMounted } from 'vue'
  import messageApi from '@/apis/message'

	// 省略中间部分代码...
  
  const nextPage = ref(1)

	// 省略中间部分代码...

  // 上拉分页
  function onScrollToLower() {
    // 获取下一页数据
    getAnnounceList(nextPage.value)
  }

  // 任务列表
  async function getAnnounceList(page = 1, pageSize = 10) {
		const { code, data } = await messageApi.list(200, page, pageSize)
    // 检测接口是否调用成功
    if (code !== 200) return uni.utils.toast('获取通知失败,稍后重试!')
    // 渲染数据
    announceList.value = [...announceList.value, ...(data.items || [])]
    // 更新下一页页码
    nextPage.value = ++data.page
    // 是否为空列表
    isEmpty.value = announceList.value.length === 0
  }
</script>
<template>
  <scroll-view
    @scrolltolower="onScrollToLower"
    class="scroll-view"
    refresher-enabled
    scroll-y
  >
    ...
  </scroll-view>
</template>

上述代码中有两点需要注意:

  • 更新页面是根据返回数据中的 page 加 1 的方式处理的
  • 分页请求来的下一页数据需要追加到原数组中,在这里用的 ... 运算符,也可以使用数组的 concat 方法
  1. 判断还有没有更多的数据
<!-- pages/message/components/announce.vue -->
<script setup>
  import { ref, onMounted } from 'vue'
  import messageApi from '@/apis/message'

	// 省略中间部分代码...
  
  const nextPage = ref(1)
  const hasMore = ref(true)

	// 省略中间部分代码...

  // 上拉分页
  function onScrollToLower() {
    if(!hasMore.value) return
    // 获取下一页数据
    getAnnounceList(nextPage.value)
  }

  // 任务列表
  async function getAnnounceList(page = 1, pageSize = 10) {
		const { code, data } = await messageApi.list(200, page, pageSize)
    // 检测接口是否调用成功
    if (code !== 200) return uni.utils.toast('获取通知失败,稍后重试!')
    // 渲染数据
    announceList.value = [...announceList.value, ...(data.items || [])]
    // 更新下一页页码
    nextPage.value = ++data.page
    // 是否为空列表
    isEmpty.value = announceList.value.length === 0
    // 是否有更多数据
    hasMore.value = nextPage.value <= data.pages
  }
</script>
<template>
  <scroll-view
    @scrolltolower="onScrollToLower"
    class="scroll-view"
    refresher-enabled
    scroll-y
  >
    ...
  </scroll-view>
</template>

上述代码中需要注意判断还有没有更多数据,是根据接口返回数据中的总页码 pages 进行判断的,如果下一页的页码 nextPage 小于等于总页码 data.page 时,表明还有更多的数据,否则没有更多数据了。

1.2.3 下拉刷新

下拉刷新的实现也与任务通知一致,监听 refresherrefresh 事件,在用执行下拉操作时重新请求数据,通过 refresher-triggered 属性来控制下拉交互的开关。

  1. 监听用户的下拉操作,通过监听 refresherrefresh 事件实现
<!-- pages/message/components/announce.vue -->
<script setup>
  import { ref, onMounted } from 'vue'
  import messageApi from '@/apis/message'

 	// 省略中间部分代码...

  // 下拉刷新
  async function onScrollViewRefresh() {
    // 重新请求第1页数据
    await getAnnounceList()
  }

  // 上拉分页
  function onScrollToLower() {
    // 省略中间部分代码
  }

  // 任务列表
  async function getAnnounceList(page = 1, pageSize = 10) {
    const { code, data } = await messageApi.list(201, page, pageSize)
    // 检测接口是否调用成功
    if (code !== 200) return uni.utils.toast('获取通知失败,稍后重试!')
    if (page === 1) announceList.value = []
    // 渲染数据
    announceList.value = [...announceList.value, ...(data.items || [])]
    // 更新下一页页码
    nextPage.value = ++data.page
    // 是否为空列表
    isEmpty.value = announceList.value.length === 0
    // 是否有更多数据
    hasMore.value = nextPage.value <= data.pages
  }
</script>
<template>
  <scroll-view
    @refresherrefresh="onScrollViewRefresh"
    @scrolltolower="onScrollToLower"
    class="scroll-view"
    refresher-enabled
    scroll-y
  >
  	...
  </scroll-view>
</template>

上述代码中需要关注的两个方面:

  • 刷新请求实际上是重新请求第 1 页的数据
  • 在进行数据渲染时需要清空原列表中的数据
  1. 关闭下拉刷新的动画交互
<!-- pages/message/components/announce.vue -->
<script setup>
  import { ref, onMounted } from 'vue'
  import messageApi from '@/apis/message'

 	// 省略中间部分代码...
  
  // 是否加载完成
  const isTriggered = ref(false)

  // 省略中间部分代码...

  // 下拉刷新
  async function onScrollViewRefresh() {
    // 重新请求第1页数据
    isTriggered.value = true
    await getAnnounceList()
    isTriggered.value = false
  }

  // 上拉分页
  function onScrollToLower() {
    // 省略中间部分代码
  }

  // 任务列表
  async function getAnnounceList(page = 1, pageSize = 10) {
    const { code, data } = await messageApi.list(201, page, pageSize)
    // 检测接口是否调用成功
    if (code !== 200) return uni.utils.toast('获取通知失败,稍后重试!')
    if (page === 1) getAnnounceList.value = []
    // 渲染数据
    getAnnounceList.value = [...announceList.value, ...(data.items || [])]
    // 更新下一页页码
    nextPage.value = ++data.page
    // 是否为空列表
    isEmpty.value = getAnnounceList.value.length === 0
    // 是否有更多数据
    hasMore.value = nextPage.value <= data.pages
  }
</script>
<template>
  <scroll-view
    @refresherrefresh="onScrollViewRefresh"
    @scrolltolower="onScrollToLower"
    :refresher-triggered="isTriggered"
    class="scroll-view"
    refresher-enabled
    scroll-y
  >
  	...
  </scroll-view>
</template>

上述代码中要注意:

  • scroll-view 下拉刷新的动画交互需要通过 refresher-triggiered 来打开或关闭,如果值为 true 时打开,值为 false 时关闭
  • 需要为调用方法指定 async/await 在请求结束后再关闭动画交互

1.2.4 全部已读

找到全部已读按钮

<view class="message-action" @click="readAll">

添加方法

//全部已读
  const readAll = async () => {
    await messageApi.readAll(200)
    //刷新列表
    isTriggered.value = true
  }

二、【神领物流】任务

本项目中最核心的功能模块即任务模块,该模块的主要功能是后台系统给司机派发运输任务,司机看到运输任务后完成提货、交付、登记等运输流程。

2.1 待提货

当司机收到运输任务后,初始为待提货的状态,等待司机去现场装车提货,司机核对始发地和终点地后按时间去指定的地点提货。

2.1.1 任务列表

待提货是以列表的形式呈现的,通过调用后端提供的接口获取数据然后渲染到页面即可,接口文档的详细说明在这里open in new window,分成两个步骤来实现。

  1. 封装调用接口的方法
// apis/task.js
// 引入网络请求模块
import { uniFetch } from './uni-fetch'
export default {
  /**
   * 任务列表
   * @param {number} status - 任务状态
   * @param {string} page - 数据页码
   * @param {string} pageSize - 每页数据条件
   */
  list(status, page = 1, pageSize = 10) {
    return uniFetch.get('/driver/tasks/list', { status, page, pageSize });
  },
}
  1. 到任务页面中调用接口
<!-- 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>
<template>
  <scroll-view 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>
            <!-- <text class="status">已延迟</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>

在对数据进行渲染时有两个地方需要注意:

  • 跳转到详情和提货页面时地址传的参数为 id
  • 根据 enablePickUp 判断当前任务是否允许提货

兼容性:提货按钮在微信小程序样式禁用时未生效,由原来的 disabled 属性控制样式

改成通过通过类名来控制,修改原来的样式文件

/* 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;
  }
}

2.1.2 上拉分页

上拉分页的功能与前面小节中学习的消息列表中的实现是一致的,实现的思路与步骤也完全一致。

  1. 监听页面是否滚动到了底部,监听事件 scrolltolower
<!-- pages/task/components/pickup.vue -->
<script setup>
  import { ref, onMounted } from 'vue'
  
  // 这里省略了中间的代码...
  
  // 监听页面是否滚动到底部
  function onScrollToLower() {
    // 获取下一页数据
    getPickUpList()
  }
  
  // 获取任务列表
  async function getPickUpList(page = 1, pageSize = 5) {
    // 省略中间部分代码...
  }
</script>
<template>
  <scroll-view
    @scrolltolower="onScrollToLower"
    scroll-y
    refresher-enabled
    class="scroll-view"
  >
    ...
	</scroll-view>
</template>
  1. 计算下一页的页码
<!-- pages/task/components/pickup.vue -->
<script setup>
  import { ref, onMounted } from 'vue'
  import taskApi from '@/apis/task'
  
  // 省略了中间部分代码...

  // 下一页页码
  const nextPage = ref(1)
  
  // 省略了中间部分代码...

  // 监听页面是否滚动到底部
  function onScrollToLower() {
    // 获取下一页数据
    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 = data.items
    pickUpList.value = [...pickUpList.value, ...(data.items || [])]
    // 计算下一页页码
    nextPage.value = ++data.page
    // 判断列表是否为空
    isEmpty.value = pickUpList.value.length === 0
  }
</script>
<template>
  <scroll-view
    @scrolltolower="onScrollToLower"
    scroll-y
    refresher-enabled
    class="scroll-view"
  >
    ...
  </scroll-view>
</template>

上述代码中有两点需要注意:

  • 更新页面是根据返回数据中的 page 加 1 的方式处理的
  • 分页请求来的下一页数据需要追加到原数组中,在这里用的 ... 运算符,也可以使用数组的 concat 方法
  1. 判断还没有更多的数据
<!-- pages/task/components/pickup.vue -->
<script setup>
  import { ref, onMounted } from 'vue'
  import taskApi from '@/apis/task'
  
  // 省略了中间部分代码...

  // 下一页页码
  const nextPage = ref(1)
  //是否有更多数据
  const hasMore = ref(true)
  
  // 省略了中间部分代码...

  // 生命周期(也可以用其它的生命周期)
  onMounted(() => {
    getPickUpList()
  })

  // 监听页面是否滚动到底部
  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 = data.items
    pickUpList.value = [...pickUpList.value, ...(data.items || [])]
    // 计算下一页页码
    nextPage.value = ++data.page
    // 判断列表是否为空
    isEmpty.value = pickUpList.value.length === 0
    // 判断还有没有更多的数据
    hasMore.value = nextPage.value <= data.pages
  }
</script>
<template>
  <scroll-view
    @scrolltolower="onScrollToLower"
    scroll-y
    refresher-enabled
    class="scroll-view"
  >
    ...
  </scroll-view>
</template>

上述代码中需要注意判断还有没有更多数据,是根据接口返回数据中的总页码 pages 进行判断的,如果下一页的页码 nextPage 小于等于总页码 data.page 时,表明还有更多的数据,否则没有更多数据了。

2.1.3 下拉刷新

下拉刷新的交互也与消息列表中是一致的,实现思路和步骤也是一模一样的。

  1. 监听用户的下拉操作,通过监听 refresherrefresh 来实现
<!-- pages/task/components/pickup.vue -->
<script setup>
  import { ref, onMounted } from 'vue'
  import taskApi from '@/apis/task'
  
  // 省略了中间部分代码...

  // 监听页面是否滚动到底部
  function onScrollToLower() {
    // 省略中间部分代码
  }
  
  // 监听用户的下拉操作
  async function onScrollViewRefresh() {
    await 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 = data.items
    pickUpList.value = [...pickUpList.value, ...(data.items || [])]
    // 计算下一页页码
    nextPage.value = ++data.page
    // 判断列表是否为空
    isEmpty.value = pickUpList.value.length === 0
    // 判断还有没有更多的数据
    hasMore.value = nextPage.value <= data.pages
  }
</script>
<template>
  <scroll-view
    @scrolltolower="onScrollToLower"
    @refresherrefresh="onScrollViewRefresh"
    scroll-y
    refresher-enabled
    class="scroll-view"
  >
    ...
  </scroll-view>
</template>

上述代码中需要关注的两个方面:

  • 刷新请求实际上是重新请求第 1 页的数据
  • 在进行数据渲染时需要清空原列表中的数据
  1. 关闭下拉刷新的动画交互
<!-- pages/task/components/pickup.vue -->
<script setup>
  import { ref, onMounted } from 'vue'
  import taskApi from '@/apis/task'
  
  // 省略了中间部分代码...

  // 监听页面是否滚动到底部
  function onScrollToLower() {
    // 省略中间部分代码
  }
  
  // 监听用户的下拉操作
  async function onScrollViewRefresh() {
    isTriggered.value = true
    await getPickUpList()
    // 关闭动画交互
    isTriggered.value = false
  }

  // 获取任务列表
  async function getPickUpList(page = 1, pageSize = 5) {
    // 省略中间部分代码...
  }
</script>
<template>
  <scroll-view
    @scrolltolower="onScrollToLower"
    @refresherrefresh="onScrollViewRefresh"
    :refresher-triggered="isTriggered"
    scroll-y
    refresher-enabled
    class="scroll-view"
  >
    ...
  </scroll-view>
</template>

上述代码中要注意:

  • scroll-view 下拉刷新的动画交互需要通过 refresher-triggiered 来打开或关闭,如果值为 true 时打开,值为 false 时关闭
  • 需要为调用方法指定 async/await 在请求结束后再关闭动画交互

2.2 任务详情

在任务详情页面展示任务的全部信息,包含运输起点/终点、提货/交付对接人、运输异常、运输凭证图片等。根据任务的 ID 调用接口open in new window即可获取相应的数据。

2.2.1 获取任务ID

在待提货列表章节我们已经拼凑到地址上了

<!-- subpkg_task/detail/index.vue -->
<script setup>
  import { onLoad } from '@dcloudio/uni-app'

  // 获取地址参数
  onLoad((params) => {
    console.log(params.id)
  })
</script>

2.2.2 获取任务详情

  1. 封装调用接口的方法,接口的详细说明在这里open in new window
// apis/task.js

// 引入网络请求模块
import { uniFetch } from './uni-fetch'
export default {
	// 省略了中间部分代码...
  
  /**
   * 任务详情
   * @param {string} id - 任务ID
   */
  detail(id) {
    if(!id) return
    return uniFetch.get(`/driver/tasks/details/${id}`);
  },
}
  1. 到页面中调用接口获取任务详情的数据
<!-- 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>

2.2.3 详情数据渲染

在对详情数据进行渲染时需要注意不同的任务状态所展示的内存在着差异,通过 v-if 条件来控制展示的内容,有以下几部分需要注意:

  • 任务的状态 status 值可能 1246,分别对应待提货、在途、已交付、已完成
  • 待提货对应的操作为【延迟提货】和【提货】
  • 在途对应的操作为【异常上报】和【交付】
  • 已交付对应的操作为【回车登记】
  • 异常上报和回车登记传递的 id 参数为 transportTaskId
  • 异常信息、提货信息、交付信息在待【提货状态】均为空
<!-- subpkg_task/detail/index.vue -->
<script setup>
  // 省略中间部分代码...
</script>

<template>
  <view class="page-container">
    <view class="search-bar">
      <text class="iconfont icon-scan"></text>
      <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>

2.2.4 条件编译应用

在任务详情页面中包含了一个根据任务 ID 查询商品的功能,该功能可扩展为通过扫码来读取任务ID,但是 H5 端扫码的功能是受限的,因此可以通过条件编译的方式来针对不同的平台展示不同的页面内容。注:具体的扫码功能暂时还没有支持。

<!-- subpkg_task/detail/index.vue -->
<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">
      ...
    </scroll-view>
    <view class="toolbar" v-if="true">
      ...
  	</view>
  </view>
</template>

以上代码中的编译条件:

  • H5 对应浏览器端
  • APP-PLUS 对应 App 端
  • MP 对应所有小程序端