前端【uniapp】06【练习项目 · 神领物流】【任务【交付】【回车登记】【已完成列表】】

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

学习目标:

  • 能够独立完成回交付、回车登记的功能

  • 能够自定义回车登记交互组件

  • 能够使用 Pinia 实现组件间数据共享

  • 能够打包发布 H5、小程序和 App 项目应用

  • 能够配置App的图标及启动屏幕

一、【神领物流】任务

1、交付

  司机在将货物运达目的地后会与接货人办理交付手续,在手续办结后司机需要将交付相关的单据及交付现场的照片上传管理后台。

  该功能模块的实现步骤与提货是完全一致的,区别在于所调用的接口不同,接口的详细说明在这里。

1.1 上传图片

  使用 uni-file-picker 将图片上传到云空间,务必保证已经创建并关联了 uniCloud 空间

 1 <!-- subpkg_task/delivery/index.vue -->
 2 <script setup>
 3   import { ref } from 'vue'
 4   import { onLoad } from '@dcloudio/uni-app'
 5   // 提货凭证照片
 6   const receiptPictrues = ref([])
 7   // 提货商品照片
 8   const goodsPictrues = ref([])
 9 </script>
10 <template>
11   <view class="page-container">
12     <view class="receipt-info">
13       <uni-file-picker
14         v-model="receiptPictrues"
15         file-extname="jpg,webp,gif,png"
16         limit="3"
17         title="请拍照上传回单凭证"
18       ></uni-file-picker>
19       <uni-file-picker
20         v-model="goodsPictrues"
21         file-extname="jpg,webp,gif,png"
22         limit="3"
23         title="请拍照上传货品照片"
24       ></uni-file-picker>
25     </view>
26     <button disabled class="button">提交</button>
27   </view>
28 </template>

1.2 表单数据

  本小节需要完成两个任务,一是处理接口所需的参数,二是验证是否至少上传了一张图片。

  1. 数据验证

 1 <!-- subpkg_task/delivery/index.vue -->
 2 <script setup>
 3   import { ref, computed } from 'vue'
 4   import { onLoad } from '@dcloudio/uni-app'
 5  6   // 任务ID
 7   const id = ref('')
 8  9   // 提货凭证照片
10   const receiptPictrues = ref([])
11   // 提货商品照片
12   const goodsPictrues = ref([])
13 14   // 凭证和商品都至少上传一张图片
15   const enableSubmit = computed(() => {
16     return goodsPictrues.value.length > 0 && receiptPictrues.value.length > 0
17   })
18 </script>
19 <template>
20   <view class="page-container">
21     ...
22     <button :disabled="!enableSubmit" class="button">提交</button>
23   </view>
24 </template>
  1. 处理表单的数据

  • 任务ID是通地址参数传递的

  • certificatePictureList 数组只包含 url 属性

  • deliverPictureList 数组只包含 url 属性

 1 <!-- subpkg_task/delivery/index.vue -->
 2 <script setup>
 3   import { ref, computed } from 'vue'
 4   import { onLoad } from '@dcloudio/uni-app'
 5   
 6   // 任务ID
 7   const id = ref('')
 8  9   // 提货凭证照片
10   const receiptPictrues = ref([])
11   // 过滤掉多余的数据,只保留 url
12   const certificatePictureList = computed(() => {
13     return receiptPictrues.value.map(({ url }) => {
14       return { url }
15     })
16   })
17 18   // 提货商品照片
19   const goodsPictrues = ref([])
20   // 过滤掉多余的数据,只保留 url
21   const deliverPictureList = computed(() => {
22     return goodsPictrues.value.map(({ url }) => {
23       return { url }
24     })
25   })
26 27     // 省略中间部分代码...
28 29   // 获取地址中的参数
30   onLoad((query) => {
31     // 任务ID
32     id.value = query.value
33   })
34 </script>
35 <template>
36   <view class="page-container">
37     ...
38   </view>
39 </template>

1.3 提交数据

  将云空间存储图片的路径发送给服务端接口即可。

  1. 封装调用接口的方法

 1 // apis/task.js
 2  3 export default {
 4   // 省略中间部分代码...
 5   
 6   /**
 7    * 交付
 8    * @property {Object} data - 接口参数
 9    */
10   deliver(data) {
11     if (!data.id) return
12     return uniFetch.post('/driver/tasks/deliver', data)
13   },
14 }
  1. 在交付页面点击提交按钮后提交数据

 1 <!-- subpkg_task/delivery/index.vue -->
 2 <script setup>
 3   import { ref, computed } from 'vue'
 4   import { onLoad } from '@dcloudio/uni-app'
 5   import taskApi from '@/apis/task'
 6  7     // 省略中间部分代码...
 8  9   // 获取地址中的参数
10   onLoad((query) => {
11     // 任务ID
12     id.value = query.value
13   })
14   
15   // 提交交付数据
16   async function onSubmitForm() {
17     // 表单数据
18     const formData = {
19       id: id.value,
20       certificatePictureList: certificatePictureList.value,
21       deliverPictureList: deliverPictureList.value,
22     }
23     // 调用接口
24     const { code } = await taskApi.deliver(formData)
25     if (code !== 200) return uni.utils.toast('上传图片失败!')
26     // 去到任务列表(查看在途任务)
27     uni.reLaunch({ url: '/pages/task/index' })
28   }
29 </script>
30 <template>
31   <view class="page-container">
32     ...
33     <button @click="onSubmitForm" :disabled="!enableSubmit" class="button">提交</button>
34   </view>
35 </template>

1.4 在途列表

  司机在完成运输交付后,运输任的状态会变成 4 ,此时在【在途列表】和【任务详情】中显示的操作应该是【回车登记】。

 1 <!-- pages/task/components/delivery/index.vue -->
 2 <script setup>
 3   // 此处不需要添加代码
 4 </script>
 5 <template>
 6     <scroll-view scroll-y refresher-enabled class="scroll-view">
 7     <view class="scroll-view-wrapper">
 8         <view v-for="delivery in deliveryList" :key="delivery.id" class="task-card">
 9         ...
10         <view class="footer">
11           <view class="label">到货时间</view>
12           <view class="time">{{ delivery.planArrivalTime }}</view>
13           <navigator
14             v-if="delivery.status === 2"
15             hover-class="none"
16             :url="`/subpkg_task/delivery/index?id=${delivery.id}`"
17             class="action"
18           >
19             交付
20           </navigator>
21           <navigator
22             v-if="delivery.status === 4"
23             hover-class="none"
24             :url="`/subpkg_task/record/index?transportTaskId=${delivery.transportTaskId}`"
25             class="action"
26           >
27             回车登记
28           </navigator>
29         </view>
30       </view>
31       <view v-if="isEmpty" class="task-blank">无在途货物</view>
32     </view>
33   </scroll-view>
34 </template>

1.5 任务详情

  司机在交付运输任务时上传了交付相关的凭证和物品照片,此时到详情中进行展示了。

 1 <!-- subpkg_task/detail/index.vue -->
 2 <script setup>
 3   // 此时不需要添加代码...
 4 </script>
 5 <template>
 6   <view class="page-container">
 7     <view class="search-bar">
 8       <!-- #ifdef H5 -->
 9       <text class="iconfont icon-search"></text>
10       <!-- #endif -->
11 12       <!-- #ifdef APP-PLUS | MP -->
13       <text class="iconfont icon-scan"></text>
14       <!-- #endif -->
15       <input class="input" type="text" placeholder="输入运单号" />
16     </view>
17     <scroll-view scroll-y class="task-detail">
18       <view class="scroll-view-wrapper">
19         <view class="basic-info panel">
20           <view class="panel-title">基本信息</view>
21           ...
22         </view>
23 24         <view v-if="taskDetail.exceptionList?.length" class="except-info panel">
25           <view class="panel-title">异常信息</view>
26           ...
27         </view>
28 29         <view v-if="taskDetail.status >= 2" class="panel pickup-info">
30           <view class="panel-title">提货信息</view>
31           ...
32         </view>
33 34         <view
35           v-if="taskDetail.status === 4 || taskDetail.status === 6"
36           class="delivery-info panel"
37         >
38           <view class="panel-title">交货信息</view>
39           <view class="label">交货凭证</view>
40           <view class="pictures">
41             <image
42               v-for="certificate in taskDetail.certificatePictureList"
43               :key="certificate.url"
44               class="picture"
45               :src="certificate.url"
46             ></image>
47             <view v-if="false" class="picture-blank">暂无图片</view>
48           </view>
49           <view class="label">货品照片</view>
50           <view class="pictures">
51             <image
52               v-for="delivery in taskDetail.deliverPictureList"
53               :key="delivery.url"
54               class="picture"
55               :src="delivery.url"
56             ></image>
57             <view v-if="false" class="picture-blank">暂无图片</view>
58           </view>
59         </view>
60       </view>
61     </scroll-view>
62         ...
63   </view>
64 </template>

  上述代码中在运输任务状态处于 46 时都是允许查看交付信息中的图片的。

2、回车登记

  回车登记是在司机完运输交付要完成的最后一项操作,该功能是让司机对整个运输过程情况做补充说明,如运输途中有没有交通违章、交通事故、车辆故障等。

2.1 出车时间

  出车时间需要通过地址参数进行传递,在途列表和任务详情都会跳转到回车登记页面,我们去补充上地址参数的传递。

  1. 在途列表页面

 1 <!-- pages/task/components/delivery/index.vue -->
 2 <script setup>
 3   // 此处不需要添加代码
 4 </script>
 5 <template>
 6     <scroll-view scroll-y refresher-enabled class="scroll-view">
 7     <view class="scroll-view-wrapper">
 8         <view v-for="delivery in deliveryList" :key="delivery.id" class="task-card">
 9         ...
10         <view class="footer">
11           <view class="label">到货时间</view>
12           <view class="time">{{ delivery.planArrivalTime }}</view>
13           ...
14           <navigator
15             v-if="delivery.status === 4"
16             hover-class="none"
17             :url="`/subpkg_task/record/index?transportTaskId=${delivery.transportTaskId}&actualDepartureTime=${delivery.actualDepartureTime}`"
18             class="action"
19           >
20             回车登记
21           </navigator>
22         </view>
23       </view>
24       <view v-if="isEmpty" class="task-blank">无在途货物</view>
25     </view>
26   </scroll-view>
27 </template>
  1. 任务详情页面

 1 <!-- subpkg_task/detail/index.vue -->
 2 <script setup>
 3   // 此时不需要添加代码...
 4 </script>
 5 <template>
 6   <view class="page-container">
 7     <view class="search-bar">
 8             ...
 9     </view>
10     <scroll-view scroll-y class="task-detail">
11       <view class="scroll-view-wrapper">
12         ...
13       </view>
14     </scroll-view>
15         ...
16     <view class="toolbar" v-if="taskDetail.status === 4">
17       <navigator
18         :url="`/subpkg_task/record/index?transportTaskId=${taskDetail.transportTaskId}&actualDepartureTime=${taskDetail.actualDepartureTime}`"
19         hover-class="none"
20         class="button primary block"
21       >
22         回车登记
23       </navigator>
24     </view>
25   </view>
26 </template>
  1. 在 onLoad 生命周期中获取地址参数

 1 <!-- subpkg_task/record/index.vue -->
 2 <script setup>
 3   import { ref } from 'vue'
 4   import { onLoad } from '@dcloudio/uni-app'
 5     
 6   // 省略中间部分代码...
 7  8   // 获取地址参数
 9   onLoad((query) => {
10     // 查看地址中的参数
11    consolel.log(query)
12   })
13 </script>

2.2 回车时间

  回车时间用到了扩展组件 uni-datetime-picker,该组件提供了 v-model 来获取用户所选择的日期时间。

 1 <!-- subpkg_task/record/index.vue -->
 2 <script setup>
 3   import { ref, computed } from 'vue'
 4   import { onLoad } from '@dcloudio/uni-app'
 5  6   // 省略中间部分代码...
 7  8   // 回车时间(临时性的)
 9   const endTime = ref('')
10 11   // 省略中间部分代码...
12   
13 </script>
14 <template>
15   <view class="page-container">
16     <scroll-view class="scroll-view" scroll-y>
17       <view class="scroll-view-wrapper">
18         <uni-list class="base-info">
19           ...
20           <uni-list-item show-arrow title="回车时间">
21             <template v-slot:footer>
22               <uni-datetime-picker v-model="endTime">
23                 <view class="picker-value">{{ endTime || '请选择' }}</view>
24               </uni-datetime-picker>
25             </template>
26           </uni-list-item>
27         </uni-list>
28                 ...
29       </view>
30     </scroll-view>
31         ...
32   </view>
33 </template>

2.3 组件交互

  交通违章、车辆故障、交通事故都是独立的组件,并且这些组件中包含了两个交互,一个是显示/隐藏选项、另一个是用户点击选择选项,以交通违章为例给大家进行说明。

  1. 显示/隐藏选项

 1 <!-- subpkg_task/record/components/vehicle-violation.vue -->
 2 <script setup>
 3   import { ref } from 'vue'
 4  5   // 是不显示详细的选项
 6   const show = ref(false)
 7  8  // 省略了中间部分代码...
 9 10   function onRadioChange(ev) {
11     // 展开详细的选项
12     show.value = !!parseInt(ev.detail.value)
13   }
14 </script>
15 <template>
16   <view class="vehicle-panel">
17     <view class="vehicle-panel-header">
18       <view class="label">交通违章</view>
19       <radio-group class="radio-group" @change="onRadioChange">
20         <label class="label">
21           <radio class="radio" value="1" color="#EF4F3F" />
22           <text>是</text>
23         </label>
24         <label class="label">
25           <radio class="radio" checked value="0" color="#EF4F3F" />
26           <text>否</text>
27         </label>
28       </radio-group>
29     </view>
30     <view v-show="show" class="vehicle-panel-body">
31       ...
32     </view>
33   </view>
34 </template>
  1. 自定义公共组件,在交通违章、车辆故障、交通事件中包含了共同点击选择的交互,我们将这部分的交互封装到组件当中。

 1 <!-- subpkg_task/record/components/vehicle-options.vue -->
 2 <script setup>
 3   import { ref } from 'vue'
 4  5   // 当前被选中选项的索引值
 6   const tabIndex = ref(-1)
 7  8   // 接收传入组件的数据
 9   const props = defineProps({
10     types: Array,
11   })
12 13   // 点击选中选项
14   function onOptionSelect(index) {
15     // 高亮显示选中的选项
16     tabIndex.value = index
17   }
18 </script>
19 20 <template>
21   <view class="vehicle-options">
22     <view
23       class="option"
24       :class="{ active: tabIndex === index }"
25       v-for="(option, index) in props.types"
26       :key="option.id"
27       @click="onOptionSelect(index)"
28     >
29       {{ option.text }}
30     </view>
31   </view>
32 </template>
33 34 <style lang="scss" scoped>
35   .vehicle-options {
36     display: flex;
37     flex-wrap: wrap;
38     font-size: $uni-font-size-small;
39 40     .option {
41       width: 180rpx;
42       height: 70rpx;
43       text-align: center;
44       line-height: 72rpx;
45       margin-top: 30rpx;
46       margin-right: 38rpx;
47       color: $uni-secondary-color;
48       border: 2rpx solid $uni-bg-color;
49       background-color: $uni-bg-color;
50       border-radius: 20rpx;
51 52       &:nth-child(3n) {
53         margin-right: 0;
54       }
55 56       &.active {
57         color: $uni-primary;
58         border: 2rpx solid $uni-primary;
59         background-color: #ffe0dd;
60       }
61     }
62   }
63 </style>

  组件封装完毕后,分别在交通违章、车辆事故、交通事故页面引入该组件

========================== 直接拷贝以下部分代码 ===========================

  1 <!-- subpkg_task/record/components/vehicle-violation.vue -->
  2 <script setup>
  3   import { ref } from 'vue'
  4   import vehicleOptions from './vehicle-options'
  5   6   // 是不显示详细的选项
  7   const show = ref(false)
  8   // 构造数据
  9   const initialData = ref([
 10     {
 11       title: '违章类型',
 12       key: 'breakRulesType',
 13       types: [
 14         { id: 1, text: '闯红灯' },
 15         { id: 2, text: '无证驾驶' },
 16         { id: 3, text: '超载' },
 17         { id: 4, text: '酒后驾驶' },
 18         { id: 5, text: '超速驾驶' },
 19         { id: 6, text: '其它' },
 20       ],
 21     },
 22     {
 23       title: '罚款金额',
 24       key: 'penaltyAmount',
 25       types: [
 26         { id: '0', text: '0元' },
 27         { id: '100', text: '100元' },
 28         { id: '200', text: '200元' },
 29         { id: '300', text: '300元' },
 30         { id: '500', text: '500元' },
 31         { id: '1000', text: '1000元' },
 32         { id: '2000', text: '2000元' },
 33       ],
 34     },
 35     {
 36       title: '扣分',
 37       key: 'deductPoints',
 38       types: ['0分', '1分', '2分', '3分', '6分', '12分'],
 39       types: [
 40         { id: '0', text: '0分' },
 41         { id: '1', text: '1分' },
 42         { id: '2', text: '2分' },
 43         { id: '3', text: '3分' },
 44         { id: '6', text: '6分' },
 45         { id: '12', text: '12分' },
 46       ],
 47     },
 48   ])
 49     
 50   // 显示/隐藏选项
 51   function onRadioChange(ev) {
 52     // 展开详细的选项
 53     show.value = !!parseInt(ev.detail.value)
 54   }
 55 </script>
 56  57 <template>
 58   <view class="vehicle-panel">
 59     <view class="vehicle-panel-header">
 60       <view class="label">交通违章</view>
 61       <radio-group class="radio-group" @change="onRadioChange">
 62         <label class="label">
 63           <radio class="radio" value="1" color="#EF4F3F" />
 64           <text>是</text>
 65         </label>
 66         <label class="label">
 67           <radio class="radio" checked value="0" color="#EF4F3F" />
 68           <text>否</text>
 69         </label>
 70       </radio-group>
 71     </view>
 72     <view v-show="show" class="vehicle-panel-body">
 73       <uni-list>
 74         <uni-list-item
 75           v-for="item in initialData"
 76           direction="column"
 77           :border="false"
 78           :title="item.title"
 79         >
 80           <template v-slot:footer>
 81             <vehicle-options :types="item.types" />
 82           </template>
 83         </uni-list-item>
 84       </uni-list>
 85     </view>
 86   </view>
 87 </template>
 88 <style lang="scss" scoped>
 89   @import './styles/vehicle-panel.scss';
 90   @import './styles/vehicle-violation.scss';
 91 </style>
 92 
 93 <!-- subpkg_task/record/components/vehicle-breakdown.vue -->
 94 <script setup>
 95   import { ref } from 'vue'
 96   import vehicleOptions from './vehicle-options'
 97  98   // 是不显示详细的选项
 99   const show = ref(false)
100   // 故障类型
101   const types = ref([
102     { id: 1, text: '启动困难' },
103     { id: 2, text: '不着车' },
104     { id: 3, text: '漏油' },
105     { id: 4, text: '漏水' },
106     { id: 5, text: '照明失灵' },
107     { id: 6, text: '有异响' },
108     { id: 7, text: '排烟异常' },
109     { id: 8, text: '温度异常' },
110     { id: 9, text: '其他' },
111   ])
112 113   function onRadioChange(ev) {
114     // 展开详细的选项
115     show.value = ev.detail.value
116   }
117 </script>
118 119 <template>
120   <view class="vehicle-panel">
121     <view class="vehicle-panel-header">
122       <view class="label">车辆故障</view>
123       <radio-group class="radio-group" @change="onRadioChange">
124         <label class="label">
125           <radio class="radio" value="1" color="#EF4F3F" />
126           <text>是</text>
127         </label>
128         <label class="label">
129           <radio class="radio" checked value="0" color="#EF4F3F" />
130           <text>否</text>
131         </label>
132       </radio-group>
133     </view>
134     <view v-show="show" class="vehicle-panel-body">
135       <uni-list>
136         <uni-list-item direction="column" :border="false" title="故障类型">
137           <template v-slot:footer>
138             <vehicle-options :types="types" />
139             <view class="textarea-wrapper">
140               <textarea
141                 class="textarea"
142                 placeholder="请输入异常描述"
143               ></textarea>
144               <view class="words-count">0/50</view>
145             </view>
146           </template>
147         </uni-list-item>
148         <uni-list-item direction="column" :border="false" title="请拍照">
149           <template v-slot:footer>
150             <uni-file-picker limit="6"></uni-file-picker>
151           </template>
152         </uni-list-item>
153       </uni-list>
154     </view>
155   </view>
156 </template>
157 158 <style lang="scss" scoped>
159   @import './styles/vehicle-panel.scss';
160   @import 'styles/vehicle-breakdown.scss';
161 </style>
162 
163 <!-- subpkg_task/record/components/vehicle-accident.vue -->
164 <script setup>
165   import { ref } from 'vue'
166   import vehicleOptions from './vehicle-options'
167 168   // 是不显示详细的选项
169   const show = ref(false)
170 171   // 事故类型
172   const types = ref([
173     { id: 1, text: '直行事故' },
174     { id: 2, text: '追尾事故' },
175     { id: 3, text: '超车事故' },
176     { id: 4, text: '左转弯事故' },
177     { id: 5, text: '右转弯事故' },
178     { id: 6, text: '弯道事故' },
179     { id: 7, text: '坡道事故' },
180     { id: 8, text: '会车事故' },
181     { id: 9, text: '其他' },
182   ])
183 184   function onRadioChange(ev) {
185     // 展开详细的选项
186     show.value = ev.detail.value
187   }
188 </script>
189 190 <template>
191   <view class="vehicle-panel">
192     <view class="vehicle-panel-header">
193       <view class="label">交通事故</view>
194       <radio-group class="radio-group" @change="onRadioChange">
195         <label class="label">
196           <radio class="radio" value="1" color="#EF4F3F" />
197           <text>是</text>
198         </label>
199         <label class="label">
200           <radio class="radio" checked value="0" color="#EF4F3F" />
201           <text>否</text>
202         </label>
203       </radio-group>
204     </view>
205     <view v-show="show" class="vehicle-panel-body">
206       <uni-list>
207         <uni-list-item direction="column" :border="false" title="事故类型">
208           <template v-slot:footer>
209             <vehicle-options :types="types" />
210             <view class="textarea-wrapper">
211               <textarea
212                 class="textarea"
213                 placeholder="请输入异常描述"
214               ></textarea>
215               <view class="words-count">0/50</view>
216             </view>
217           </template>
218         </uni-list-item>
219         <uni-list-item direction="column" :border="false" title="请拍照">
220           <template v-slot:footer>
221             <uni-file-picker limit="6"></uni-file-picker>
222           </template>
223         </uni-list-item>
224       </uni-list>
225     </view>
226   </view>
227 </template>
228 229 <style lang="scss" scoped>
230   @import './styles/vehicle-panel.scss';
231   @import './styles/vehicle-accident.scss';
232 </style>

  以上代码大家就直接粘贴到项目中替换掉原来的代码就可以了,后续我会去更新 git 仓库中的静态模板代码。

========================== 直接拷贝以上部分代码 ===========================

2.4 交通违章

  用户点击选择了选项后,我们需要记录用户所选择的是哪个类型的哪个值。

  1. 用户选择的哪个值 ,在用户进行点击时通过参数传入

 1 <!-- subpkg_task/record/components/vehicle-options.vue -->
 2 <script setup>
 3   import { ref } from 'vue'
 4  5     // 此处省略中间部分代码...
 6  7   // 点击选中选项
 8   function onOptionSelect(index, text) {
 9     // 高亮显示选中的选项
10     tabIndex.value = index
11     // 用户选择了哪个值
12     console.log(text)
13   }
14 </script>
15 16 <template>
17   <view class="vehicle-options">
18     <view
19       class="option"
20       :class="{ active: tabIndex === index }"
21       v-for="(option, index) in props.types"
22       :key="option.id"
23       @click="onOptionSelect(index, option.text)"
24     >
25       {{ option.text }}
26     </view>
27   </view>
28 </template>
  1. 确定用户选择了哪个类型,为组件自定义一个属性 dataKey 通过 dataKey 来区分用户当前点击的是哪个类型

 1 <!-- subpkg_task/record/components/vehicle-options.vue -->
 2 <script setup>
 3   import { ref } from 'vue'
 4  5     // 此处省略中间部分代码...
 6   
 7   // 接收传入组件的数据
 8   const props = defineProps({
 9     types: Array,
10     dataKey: String,
11   })
12 13   // 点击选中选项
14   function onOptionSelect(index, text) {
15     // 高亮显示选中的选项
16     tabIndex.value = index
17     
18     // 用户选择的是哪个类型
19     console.log(props.dataKey)
20     // 用户选择了哪个值
21     console.log(text)
22   }
23 </script>
24 25 <template>
26   <view class="vehicle-options">
27     <view
28       class="option"
29       :class="{ active: tabIndex === index }"
30       v-for="(option, index) in props.types"
31       :key="option.id"
32       @click="onOptionSelect(index, option.text)"
33     >
34       {{ option.text }}
35     </view>
36   </view>
37 </template>

  在应用组件 vehicle-options 组件,为其传入一个 data-key 属性,该属性的值用来区分所选择的值是哪个类型的。

 1 <!-- subpkg_task/record/components/vehicle-violation.vue -->
 2 <script setup>
 3     // 这里不需要添加新代码...
 4 </script>
 5  6 <template>
 7   <view class="vehicle-panel">
 8     <view class="vehicle-panel-header">
 9       ....
10     </view>
11     <view v-show="show" class="vehicle-panel-body">
12       <uni-list>
13         <uni-list-item
14           v-for="item in initialData"
15           direction="column"
16           :border="false"
17           :title="item.title"
18         >
19           <template v-slot:footer>
20             <vehicle-options :data-key="item.key" :types="item.types" />
21           </template>
22         </uni-list-item>
23       </uni-list>
24     </view>
25   </view>
26 </template>

2.5 车辆故障

  用户所选择的车辆故障的数据也需要记录下来,同样的需要传入 data-key 属性

 1 <!-- subpkg_task/record/components/vehicle-breakdown.vue -->
 2 <script setup>
 3     // 这里不需要添加新代码...
 4 </script>
 5 <template>
 6   <view class="vehicle-panel">
 7     <view class="vehicle-panel-header">
 8       ...
 9     </view>
10     <view v-show="show" class="vehicle-panel-body">
11       <uni-list>
12         <uni-list-item direction="column" :border="false" title="故障类型">
13           <template v-slot:footer>
14             <vehicle-options data-key="faultType" :types="types" />
15             ...
16           </template>
17         </uni-list-item>
18         ...
19       </uni-list>
20     </view>
21   </view>
22 </template>

2.6 交通事故

  用户所选择的交通事故的数据也需要记录下来,同样的需要传入 data-key 属性

 1 <!-- subpkg_task/record/components/vehicle-accident.vue -->
 2 <script setup>
 3     // 这里不需要添加新代码...
 4 </script>
 5 <template>
 6   <view class="vehicle-panel">
 7     <view class="vehicle-panel-header">
 8       ...
 9     </view>
10     <view v-show="show" class="vehicle-panel-body">
11       <uni-list>
12         <uni-list-item direction="column" :border="false" title="事故类型">
13           <template v-slot:footer>
14             <vehicle-options data-key="accidentType" :types="types" />
15             ...
16           </template>
17         </uni-list-item>
18         ...
19       </uni-list>
20     </view>
21   </view>
22 </template>

2.7 表单数据

  在处理回车登记数时涉及到了组件的数据的传递,我们来通过 Pinia 来解决组件数据通信的问题。

  1. 定义 Store 及数据

 1 // stores/task.js
 2 import { ref } from 'vue'
 3 import { defineStore } from 'pinia'
 4  5 export const useTaskStore = defineStore('task', () => {
 6   // 这里定义的数据全部是接口所需要的数据
 7   const recordData = ref({
 8     id: '',
 9     startTime: '',
10     endTime: '',
11     /*** 违章 ***/
12     isBreakRules: false,
13     breakRulesType: null,
14     penaltyAmount: null,
15     deductPoints: null,
16     /*** 违章 ***/
17 18     /*** 故障 ***/
19     isFault: false,
20     faultType: null,
21     faultDescription: '',
22     faultImagesList: [],
23     /*** 故障 ***/
24 25     /*** 事故 ***/
26     isAccident: false,
27     accidentType: null,
28     accidentDescription: '',
29     accidentImagesList: [],
30     /*** 事故 ***/
31   })
32 33   return { recordData }
34 })
  1. 将用户点击选择的选项存入 Pinia 中

 1 <!-- subpkg_task/record/components/vehicle-options.vue -->
 2 <script setup>
 3   import { ref } from 'vue'
 4   import { useTaskStore } from '@/stores/task'
 5  6   const taskStore = useTaskStore()
 7  8   // 当前被选中选项的索引值
 9   const tabIndex = ref(-1)
10 11   // 接收传入组件的数据
12   const props = defineProps({
13     types: Array,
14     dataKey: String,
15   })
16 17   // 点击选中选项
18   function onOptionSelect(index, id, text) {
19     // 高亮显示选中的选项
20     tabIndex.value = index
21     // 用户选择的是哪个类型
22     console.log(props.dataKey)
23     // 用户选择的是哪个值
24     console.log(text)
25     // 将数据存入 Pinia
26     taskStore.recordData[props.dataKey] = id
27   }
28 </script>
29 <template>
30   <view class="vehicle-options">
31     <view
32       class="option"
33       :class="{ active: tabIndex === index }"
34       v-for="(option, index) in props.types"
35       :key="option.id"
36       @click="onOptionSelect(index, option.id, option.text)"
37     >
38       {{ option.text }}
39     </view>
40   </view>
41 </template>
  1. 是否有车辆故障、交通事故、车辆故障,记录到 Pinia 中

 1 <!-- subpkg_task/record/components/vehicle-violation.vue -->
 2 <script setup>
 3   import { ref } from 'vue'
 4   import vehicleOptions from './vehicle-options'
 5   import { useTaskStore } from '@/stores/task'
 6  7   const taskStore = useTaskStore()
 8     
 9   // 中间部分代码省略...
10 11   function onRadioChange(ev) {
12     // 展开详细的选项
13     show.value = !!parseInt(ev.detail.value)
14     // 是否有交通违章
15     taskStore.recordData.isBreakRules = show.value
16   }
17 </script>
18 
19 <!-- subpkg_task/record/components/vehicle-breakdown.vue -->
20 <script setup>
21   import { ref } from 'vue'
22   import vehicleOptions from './vehicle-options'
23   import { useTaskStore } from '@/stores/task'
24 25   const taskStore = useTaskStore()
26     
27   // 中间部分代码省略...
28 29   function onRadioChange(ev) {
30     // 展开详细的选项
31     show.value = !!parseInt(ev.detail.value)
32     // 是否有交通违章
33     taskStore.recordData.isFault = show.value
34   }
35 </script>
36 
37 <!-- subpkg_task/record/components/vehicle-accident.vue -->
38 <script setup>
39   import { ref } from 'vue'
40   import vehicleOptions from './vehicle-options'
41   import { useTaskStore } from '@/stores/task'
42 43   const taskStore = useTaskStore()
44     
45   // 中间部分代码省略...
46 47   function onRadioChange(ev) {
48     // 展开详细的选项
49     show.value = !!parseInt(ev.detail.value)
50     // 是否有交通违章
51     taskStore.recordData.isAccident = show.value
52   }
53 </script>
  1. 出车/回车时间记录到 Pinia 中

 1 <!-- subpkg_task/record/index.vue -->
 2 <script setup>
 3   import { ref, computed } from 'vue'
 4   import { onLoad } from '@dcloudio/uni-app'
 5   import { storeToRefs } from 'pinia'
 6   import { useTaskStore } from '@/stores/task'
 7  8     // 省略中间部分代码
 9 10   // 回车登记的全部数据
11   const { recordData } = storeToRefs(useTaskStore())
12 13   // 获取地址参数
14   onLoad((query) => {
15     // 任务ID
16     recordData.value.id = query.transportTaskId
17     // 发车时间
18     recordData.value.startTime = query.actualDepartureTime
19   })
20 </script>
21 <template>
22   <view class="page-container">
23     <scroll-view class="scroll-view" scroll-y>
24       <view class="scroll-view-wrapper">
25         <uni-list class="base-info">
26           <uni-list-item
27             title="出车时间"
28             show-arrow
29             :right-text="recordData.startTime"
30           />
31           <uni-list-item show-arrow title="回车时间">
32             <template v-slot:footer>
33               <uni-datetime-picker v-model="recordData.endTime">
34                 <view class="picker-value">{{
35                   recordData.endTime || '请选择'
36                 }}</view>
37               </uni-datetime-picker>
38             </template>
39           </uni-list-item>
40         </uni-list>
41                 ...
42       </view>
43     </scroll-view>
44     ...
45   </view>
46 </template>
  1. 将车辆故障描述、交通事故描述及图片存入 Pinia

 1 <!-- subpkg_task/record/components/vehicle-accident.vue -->
 2 <script setup>
 3   // 这里不需要填加新代码...
 4 </script>
 5  6 <template>
 7   <view class="vehicle-panel">
 8     ...
 9     <view v-show="show" class="vehicle-panel-body">
10       <uni-list>
11         <uni-list-item direction="column" :border="false" title="事故类型">
12           <template v-slot:footer>
13             <vehicle-options data-key="accidentType" :types="types" />
14             <view class="textarea-wrapper">
15               <textarea
16                 v-model="taskStore.recordData.accidentDescription"
17                 class="textarea"
18                 placeholder="请输入事故描述"
19               ></textarea>
20               <view class="words-count">0/50</view>
21             </view>
22           </template>
23         </uni-list-item>
24         <uni-list-item
25           direction="column"
26           :border="false"
27           title="请上传事故现场照片"
28         >
29           <template v-slot:footer>
30             <uni-file-picker
31               v-model="taskStore.recordData.accidentImagesList"
32               file-extname="jpg,webp,gif,png"
33               limit="3"
34             ></uni-file-picker>
35           </template>
36         </uni-list-item>
37       </uni-list>
38     </view>
39   </view>
40 </template>
41 
42 <!-- subpkg_task/record/components/vehicle-breakdown.vue -->
43 <script setup>
44   // 这里不需要填加新代码...
45 </script>
46 47 <template>
48   <view class="vehicle-panel">
49     ...
50     <view v-show="show" class="vehicle-panel-body">
51       <uni-list>
52         <uni-list-item direction="column" :border="false" title="故障类型">
53           <template v-slot:footer>
54             <vehicle-options data-key="faultType" :types="types" />
55             <view class="textarea-wrapper">
56               <textarea
57                 v-model="taskStore.recordData.faultDescription"
58                 class="textarea"
59                 placeholder="请输入故障描述"
60               ></textarea>
61               <view class="words-count">0/50</view>
62             </view>
63           </template>
64         </uni-list-item>
65         <uni-list-item
66           direction="column"
67           :border="false"
68           title="请上传车辆故障照片"
69         >
70           <template v-slot:footer>
71             <uni-file-picker
72               v-model="taskStore.recordData.faultImagesList"
73               file-extname="jpg,webp,gif,png"
74               limit="3"
75             ></uni-file-picker>
76           </template>
77         </uni-list-item>
78       </uni-list>
79     </view>
80   </view>
81 </template>

2.8 提交数据

  到此终于把所需的数处理完整了,调用接口把数据提交即可,接口文档的详细说明在这里。

  1. 封装调用接口的方法

 1 // apis/task.js
 2 // 引入网络请求模块
 3 import { uniFetch } from './uni-fetch'
 4  5 export default {
 6     // 省略中间部分代码...
 7  8   /**
 9    * 回车登记
10    * @param {Object} data - 接口数据
11    */
12   record(data) {
13     if (!data.id) return
14     return uniFetch.post('/driver/tasks/truckRegistration', data)
15   },
16 }
17
  1. 调用接口提交数据

 1 <!-- sbupkg_task/record/index.vue -->
 2 <script setup>
 3   import { ref, computed } from 'vue'
 4   import { onLoad } from '@dcloudio/uni-app'
 5   import { storeToRefs } from 'pinia'
 6   import { useTaskStore } from '@/stores/task'
 7   import taskApi from '@/apis/task'
 8     
 9   // 省略中间部分代码...
10 11   // 提交回车登记
12   async function onFormSubmit() {
13     // 过滤掉图片多余的数据,只保留 url
14     const { accidentImagesList, faultImagesList } = recordData.value
15     // 事故照片
16     recordData.value.accidentImagesList = accidentImagesList.map(({ url }) => {
17       return { url }
18     })
19     // 故障照片
20     recordData.value.faultImagesList = faultImagesList.map(({ url }) => {
21       return { url }
22     })
23 24     // 调用接口提交数据
25     const { code } = await taskApi.record(recordData.value)
26     // 检测接口否调用成功
27     if (code !== 200) return uni.utils.toast('回车登记失败!')
28     // 跳转到任务列表
29     uni.reLaunch({ url: '/pages/task/index' })
30   }
31 </script>
32 <template>
33   <view class="page-container">
34     ...
35     <view class="toolbar">
36       <button @click="onFormSubmit" class="button">提交登记</button>
37     </view>
38   </view>
39 </template>

3、已完成

  司机的运输任务完成回成登记后状态会变成 6 即已完成的状态。

3.1 任务列表

  调用接口时传入状态值 6 即可获取已完成的任务列表了

 1 <!-- pages/task/components/complete.vue -->
 2 <script setup>
 3   import { ref, onMounted } from 'vue'
 4   import taskApi from '@/apis/task'
 5  6   // 已完成任务列表
 7   const completeList = ref([])
 8   // 在途列任务列表是否为空
 9   const isEmpty = ref(false)
10 11   // 生命周期(获取数据)
12   onMounted(() => {
13     getCompleteList()
14   })
15 16   // 在途任务列表
17   async function getCompleteList(page = 1, pageSize = 5) {
18     const { code, data } = await taskApi.list(6, page, pageSize)
19     if (code !== 200) return uni.utils.toast('已完成任务获取失败!')
20     // 渲染数据
21     completeList.value = data.items || []
22     isEmpty.value = completeList.value.length === 0
23   }
24 </script>
25 26 <template>
27   <view class="task-search">
28     <view class="search-bar">
29       <text class="iconfont icon-search"></text>
30       <input class="input" type="text" placeholder="输入任务编号" />
31     </view>
32     <view class="filter-bar">
33       <picker class="picker" mode="date">2023.05.20</picker>
34       <text class="text">至</text>
35       <picker class="picker" mode="date">结束时间</picker>
36       <button disabled class="button">筛选</button>
37     </view>
38   </view>
39   <scroll-view scroll-y refresher-enabled class="scroll-view">
40     <view class="scroll-view-wrapper">
41       <view
42         v-for="complete in completeList"
43         :key="complete.id"
44         class="task-card"
45       >
46         <navigator
47           hover-class="none"
48           :url="`/subpkg_task/detail/index?id=${complete.id}`"
49         >
50           <view class="header">
51             <text class="no">任务编号: {{ complete.transportTaskId }}</text>
52           </view>
53           <view class="body">
54             <view class="timeline">
55               <view class="line">{{ complete.startAddress }}</view>
56               <view class="line">{{ complete.endAddress }}</view>
57             </view>
58           </view>
59         </navigator>
60         <view class="footer flex">
61           <view class="label">提货时间</view>
62           <view class="time">{{ complete.created }}</view>
63         </view>
64       </view>
65       <view v-if="isEmpty" class="task-blank">无完成货物</view>
66     </view>
67   </scroll-view>
68 </template>

3.2 上拉分页

  1. 监听 scrolltolower 事件

 1 <!-- pages/task/components/complete.vue -->
 2 <script setup>
 3   import { ref, onMounted } from 'vue'
 4   import taskApi from '@/apis/task'
 5  6     // 省略中间部分代码...
 7  8   // 上拉分页
 9   function onScrollToLower() {
10     // 获取下一页数据
11     getCompleteList()
12   }
13 14   // 任务列表
15   async function getCompleteList(page = 1, pageSize = 5) {
16     // 省略中间部分代码
17   }
18 </script>
19 <template>
20   <scroll-view
21     @scrolltolower="onScrollToLower"
22     class="scroll-view"
23     refresher-enabled
24     scroll-y
25   >
26     ...
27   </scroll-view>
28 </template>
  1. 计算下一页页码

 1 <!-- pages/task/components/complete.vue -->
 2 <script setup>
 3   import { ref, onMounted } from 'vue'
 4   import taskApi from '@/apis/task'
 5  6     // 省略中间部分代码...
 7   
 8   const nextPage = ref(1)
 9 10     // 省略中间部分代码...
11 12   // 上拉分页
13   function onScrollToLower() {
14     // 获取下一页数据
15     getCompleteList(nextPage.value)
16   }
17 18   // 任务列表
19   async function getCompleteList(page = 1, pageSize = 10) {
20         const { code, data } = await taskApi.list(6, page, pageSize)
21     // 检测接口是否调用成功
22     if (code !== 200) return uni.utils.toast('获取列表失败,稍后重试!')
23     // 渲染数据
24     completeList.value = [...completeList.value, ...(data.items || [])]
25     // 更新下一页页码
26     nextPage.value = ++data.page
27     // 是否为空列表
28     isEmpty.value = completeList.value.length === 0
29   }
30 </script>
31 <template>
32   <scroll-view
33     @scrolltolower="onScrollToLower"
34     class="scroll-view"
35     refresher-enabled
36     scroll-y
37   >
38     ...
39   </scroll-view>
40 </template>

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

  • 更新页面是根据返回数据中的 page 加 1 的方式处理的

  • 分页请求来的下一页数据需要追加到原数组中,在这里用的 ... 运算符,也可以使用数组的 concat 方法

  1. 判断还有没有更多的数据

 1 <!-- pages/task/components/complete.vue -->
 2 <script setup>
 3   import { ref, onMounted } from 'vue'
 4   import taskApi from '@/apis/task'
 5  6     // 省略中间部分代码...
 7   
 8   const nextPage = ref(1)
 9   const hasMore = ref(true)
10 11     // 省略中间部分代码...
12 13   // 上拉分页
14   function onScrollToLower() {
15     if(!hasMore.value) return
16     // 获取下一页数据
17     getCompleteList(nextPage.value)
18   }
19 20   // 任务列表
21   async function getCompleteList(page = 1, pageSize = 10) {
22         const { code, data } = await taskApi.list(6, page, pageSize)
23     // 检测接口是否调用成功
24     if (code !== 200) return uni.utils.toast('获取列表失败,稍后重试!')
25     // 渲染数据
26     completeList.value = [...completeList.value, ...(data.items || [])]
27     // 更新下一页页码
28     nextPage.value = ++data.page
29     // 是否为空列表
30     isEmpty.value = completeList.value.length === 0
31     // 是否有更多数据
32     hasMore.value = nextPage.value <= data.pages
33   }
34 </script>
35 <template>
36   <scroll-view
37     @scrolltolower="onScrollToLower"
38     class="scroll-view"
39     refresher-enabled
40     scroll-y
41   >
42     ...
43   </scroll-view>
44 </template>

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

3.3 下拉刷新

  1. 监听用户的下拉操作,通过监听 refresherrefresh 来实现

 1 <!-- pages/task/components/complete.vue -->
 2 <script setup>
 3   import { ref, onMounted } from 'vue'
 4   import taskApi from '@/apis/task'
 5   
 6   // 省略了中间部分代码...
 7  8   // 监听页面是否滚动到底部
 9   function onScrollToLower() {
10     // 省略中间部分代码
11   }
12   
13   // 监听用户的下拉操作
14   async function onScrollViewRefresh() {
15     await getCompleteList()
16   }
17 18   // 获取任务列表
19   async function getCompleteList(page = 1, pageSize = 5) {
20     const { code, data } = await taskApi.list(6, page, pageSize)
21     // 检测接口是否调用成功
22     if (code !== 200) return uni.utils.toast('获取列表失败,稍后重试!')
23     // 页面为 1 时,清空数组
24     if (page === 1) completeList.value = []
25     // 渲染任务列表
26     completeList.value = [...completeList.value, ...(data.items || [])]
27     // 计算下一页页码
28     nextPage.value = ++data.page
29     // 判断列表是否为空
30     isEmpty.value = completeList.value.length === 0
31     // 判断还有没有更多的数据
32     hasMore.value = nextPage.value <= data.pages
33   }
34 </script>
35 <template>
36   <scroll-view
37     @scrolltolower="onScrollToLower"
38     @refresherrefresh="onScrollViewRefresh"
39     scroll-y
40     refresher-enabled
41     class="scroll-view"
42   >
43     ...
44   </scroll-view>
45 </template>

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

  • 刷新请求实际上是重新请求第 1 页的数据

  • 在进行数据渲染时需要清空原列表中的数据

  1. 关闭下拉刷新的动画交互

 1 <!-- pages/task/components/pickup.vue -->
 2 <script setup>
 3   import { ref, onMounted } from 'vue'
 4   import taskApi from '@/apis/task'
 5   
 6   // 省略了中间部分代码...
 7  8   // 监听页面是否滚动到底部
 9   function onScrollToLower() {
10     // 省略中间部分代码
11   }
12   
13   // 监听用户的下拉操作
14   async function onScrollViewRefresh() {
15     isTriggered.value = true
16     await getCompleteList()
17     // 关闭动画交互
18     isTriggered.value = false
19   }
20 21   // 获取任务列表
22   async function getCompleteList(page = 1, pageSize = 5) {
23     // 省略中间部分代码...
24   }
25 </script>
26 <template>
27   <scroll-view
28     @scrolltolower="onScrollToLower"
29     @refresherrefresh="onScrollViewRefresh"
30     :refresher-triggered="isTriggered"
31     scroll-y
32     refresher-enabled
33     class="scroll-view"
34   >
35     ...
36   </scroll-view>
37 </template>

  上述代码中要注意:

  • scroll-view 下拉刷新的动画交互需要通过 refresher-triggiered 来打开或关闭,如果值为 true 时打开,值为 false 时关闭

  • 需要为调用方法指定 async/await 在请求结束后再关闭动画交互

 

posted @ 2024-04-23 09:01  为你编程  阅读(101)  评论(0编辑  收藏  举报