主题
异常上报(一)
本页对应 异常上报,共 8 段视频,约 1 小时 12 分。
学习目标
- 能够定位并修复延迟提货中“按钮启用条件”和“字数超限变红条件”混用的问题,分别用“大于0且不超过50”和“仅大于50”实现验证,并演示输入、删除、超限时按钮与文字颜色的变化。
- 能够封装延迟提货接口调用方法,在提交时传递任务ID、延迟时间和延迟原因,并根据返回状态码给出失败提示或使用
reLaunch跳转到首页。 - 能够结合页面栈说明
navigateTo、redirectTo、reLaunch的区别,并通过示例演示三者在页面返回行为上的不同。 - 能够完成 uniCloud 服务空间的创建、项目关联和 uni-app AppID 获取与配置,并说明未关联或未重启导致上传失败时的排查方法。
- 能够使用
uni-file-picker配置title、limit、file-extname、v-model,实现图片上传、类型限制、数量限制、回显及上传结果获取,并在云存储中查看文件。 - 能够实现提货页面提交逻辑:用计算属性校验两组图片数组均至少一项,通过
onLoad获取任务ID,使用map将上传结果过滤为仅含url的数据,调用提货接口并根据状态码提示或跳转。
知识点
交付模块与提货模块的关系
为什么需要:司机把货物运到目的地后,要与接货人办理交付手续,并把交付单据、现场照片上传到管理后台。这个流程和提货非常相似,但调用的接口不同。笔记中明确给出交付接口说明:接口的详细说明。
怎么想:不要重新设计一套完全不同的页面逻辑,而是沿用“上传图片 → 处理表单数据 → 提交接口 → 跳转列表”的套路。交付与提货的差异集中在接口地址、参数名和提交成功后的页面状态上。
怎么做:在 subpkg_task/delivery/index.vue 中完成交付页面;在 apis/task.js 中封装 deliver 方法;提交成功后跳转到任务列表。
易错点:把提货接口误用于交付;参数名照抄提货;上传成功后忘记处理图片数组,直接把组件返回的完整对象传给后端。
上传图片:使用 uni-file-picker
为什么需要:交付现场需要上传回单凭证和货品照片。传统做法是自己调用上传接口,而 uni-file-picker 可以在 uni-app 中配合 uniCloud 云空间完成图片上传,组件会把上传结果通过 v-model 交回来。
怎么想:先保证项目已经创建并关联 uniCloud 服务空间。转写稿中反复强调:使用组件前要先开通云空间、在项目里创建 uniCloud 环境、关联云空间,并获取 uni-app 的 APPID;如果项目启动时还没建好云空间,上传可能失败,需要重新启动项目再试。
怎么做:
vue
<!-- subpkg_task/delivery/index.vue -->
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
// 提货凭证照片
const receiptPictrues = ref([])
// 提货商品照片
const goodsPictrues = ref([])
</script>
<template>
<view class="page-container">
<view class="receipt-info">
<uni-file-picker
v-model="receiptPictrues"
file-extname="jpg,webp,gif,png"
limit="3"
title="请拍照上传回单凭证"
></uni-file-picker>
<uni-file-picker
v-model="goodsPictrues"
file-extname="jpg,webp,gif,png"
limit="3"
title="请拍照上传货品照片"
></uni-file-picker>
</view>
<button disabled class="button">提交</button>
</view>
</template>易错点:file-extname 用来限制可选文件类型;limit 控制上传数量;v-model 是获取上传结果的关键。转写稿中演示过:去掉 webp 后,webp 图片就选不到;上传成功后数组里会出现包含 url 的对象。若上传没有成功,先检查云空间关联和项目是否重启。
表单数据与提交按钮验证
为什么需要:接口要求交付凭证和货品照片都至少上传一张,否则不应允许提交。转写稿还提到一个通用经验:验证逻辑要分清“控制按钮是否可用”和“控制文字是否变红”,不要把所有条件混在一个地方。
怎么想:用计算属性 enableSubmit 判断两个数组是否都大于 0。按钮的 disabled 取反:!enableSubmit。
怎么做:
vue
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
// 任务ID
const id = ref('')
// 提货凭证照片
const receiptPictrues = ref([])
// 提货商品照片
const goodsPictrues = ref([])
// 凭证和商品都至少上传一张图片
const enableSubmit = computed(() => {
return goodsPictrues.value.length > 0 && receiptPictrues.value.length > 0
})
</script>
<template>
<view class="page-container">
<button :disabled="!enableSubmit" class="button">提交</button>
</view>
</template>易错点:不要写成 receiptPictrues.length,ref 数组在脚本中要用 .value;模板中会自动解包。转写稿中调试时出现“上传了但数据为空”,检查后发现是计算属性或模板渲染位置的问题,说明写完一小段就要验证一次。
处理接口参数:只保留 url
为什么需要:组件返回的图片对象包含很多字段,但交付接口只要求 certificatePictureList 和 deliverPictureList 中每项只包含 url。转写稿形象地说:后端“就要 5 块,你给 10 块不行”,多余字段可能导致接口异常。
怎么想:用 map 遍历数组,解构出 url,返回 { url }。任务 ID 通过地址参数传入,在 onLoad 中读取。
怎么做:
vue
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
const id = ref('')
const receiptPictrues = ref([])
const goodsPictrues = ref([])
const certificatePictureList = computed(() => {
return receiptPictrues.value.map(({ url }) => {
return { url }
})
})
const deliverPictureList = computed(() => {
return goodsPictrues.value.map(({ url }) => {
return { url }
})
})
onLoad((query) => {
id.value = query.value
})
</script>易错点:map 中返回的是新数组,不要忘记 return { url };地址参数名要与跳转时一致,笔记中写的是 query.value,跳转处则是 ?id=${delivery.id},实际项目要以接口和路由约定为准,调试时先 console.log(query) 确认。
封装并提交交付接口
为什么需要:把接口调用集中到 apis/task.js,页面只负责准备数据和处理结果,便于维护。
怎么做:
javascript
// apis/task.js
export default {
/**
* 交付
* @property {Object} data - 接口参数
*/
deliver(data) {
if (!data.id) return
return uniFetch.post('/driver/tasks/deliver', data)
},
}页面中监听提交:
vue
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import taskApi from '@/apis/task'
const id = ref('')
const receiptPictrues = ref([])
const goodsPictrues = ref([])
const certificatePictureList = computed(() =>
receiptPictrues.value.map(({ url }) => ({ url }))
)
const deliverPictureList = computed(() =>
goodsPictrues.value.map(({ url }) => ({ url }))
)
const enableSubmit = computed(
() => goodsPictrues.value.length > 0 && receiptPictrues.value.length > 0
)
onLoad((query) => {
id.value = query.value
})
async function onSubmitForm() {
const formData = {
id: id.value,
certificatePictureList: certificatePictureList.value,
deliverPictureList: deliverPictureList.value,
}
const { code } = await taskApi.deliver(formData)
if (code !== 200) return uni.utils.toast('上传图片失败!')
uni.reLaunch({ url: '/pages/task/index' })
}
</script>
<template>
<view class="page-container">
<button @click="onSubmitForm" :disabled="!enableSubmit" class="button">提交</button>
</view>
</template>易错点:提交时要用处理后的 certificatePictureList.value 和 deliverPictureList.value,不要直接传原始图片数组。转写稿中调试提交时先看网络面板,返回 200 表示成功;成功后用 reLaunch 清空页面栈回到任务列表,用户不能再返回到已提交的交付页。
在途列表与任务详情展示
为什么需要:交付后任务状态变为 4,在途列表和任务详情中应显示“回车登记”;详情页还要展示交付凭证和货品照片。
怎么做:在在途列表卡片中按状态切换操作:
vue
<navigator
v-if="delivery.status === 2"
hover-class="none"
:url="`/subpkg_task/delivery/index?id=${delivery.id}`"
class="action"
>
交付
</navigator>
<navigator
v-if="delivery.status === 4"
hover-class="none"
:url="`/subpkg_task/record/index?transportTaskId=${delivery.transportTaskId}`"
class="action"
>
回车登记
</navigator>详情页在状态为 4 或 6 时展示交货信息:
vue
<view v-if="taskDetail.status === 4 || taskDetail.status === 6" class="delivery-info panel">
<view class="panel-title">交货信息</view>
<view class="label">交货凭证</view>
<view class="pictures">
<image
v-for="certificate in taskDetail.certificatePictureList"
:key="certificate.url"
class="picture"
:src="certificate.url"
></image>
<view v-if="false" class="picture-blank">暂无图片</view>
</view>
<view class="label">货品照片</view>
<view class="pictures">
<image
v-for="delivery in taskDetail.deliverPictureList"
:key="delivery.url"
class="picture"
:src="delivery.url"
></image>
<view v-if="false" class="picture-blank">暂无图片</view>
</view>
</view>易错点:状态判断不要漏掉 6;列表跳转参数与详情页读取参数要对应。转写稿中路由部分强调:navigateTo 会压入页面栈,redirectTo 替换当前页,reLaunch 清空页面栈。交付提交成功后用 reLaunch 是合适的,避免用户返回到已提交页面。
1.2 回车登记:先明确业务与数据流
为什么需要:司机完成运输交付后,需要补充运输途中是否发生交通违章、车辆故障、交通事故等情况,并登记回车时间,形成完整运输记录。
怎么想:页面要接收两个入口传来的任务信息,再收集回车时间、三类异常数据和图片,最后统一提交。
怎么做:先补地址参数,再做表单组件,最后用 Pinia 汇总数据并调用接口。
易错点:列表页和详情页都要带 transportTaskId、actualDepartureTime,不能只改一个入口。
vue
<!-- pages/task/components/delivery/index.vue -->
<navigator
v-if="delivery.status === 4"
hover-class="none"
:url="`/subpkg_task/record/index?transportTaskId=${delivery.transportTaskId}&actualDepartureTime=${delivery.actualDepartureTime}`"
class="action"
>
回车登记
</navigator>vue
<!-- subpkg_task/detail/index.vue -->
<navigator
:url="`/subpkg_task/record/index?transportTaskId=${taskDetail.transportTaskId}&actualDepartureTime=${taskDetail.actualDepartureTime}`"
hover-class="none"
class="button primary block"
>
回车登记
</navigator>出车时间:用 onLoad 接收地址参数
为什么需要:出车时间不是用户在本页手填,而是从上个页面带过来。
怎么想:页面加载时先打印 query,确认参数名和值都到了,再写入 Pinia。
怎么做:onLoad((query) => {...}) 中取 transportTaskId 和 actualDepartureTime。
易错点:转写稿里口误说成“地址参数”,实际是“地址栏参数/路由参数”;调试时写 console.log(query),不要写成 consolel.log。
vue
<!-- subpkg_task/record/index.vue -->
<script setup>
import { onLoad } from '@dcloudio/uni-app'
import { storeToRefs } from 'pinia'
import { useTaskStore } from '@/stores/task'
const { recordData } = storeToRefs(useTaskStore())
onLoad((query) => {
console.log(query)
recordData.value.id = query.transportTaskId
recordData.value.startTime = query.actualDepartureTime
})
</script>回车时间:uni-datetime-picker 的 v-model
为什么需要:回车时间要由用户选择,且要直接进入最终表单数据。
怎么想:先用临时 endTime 理解组件,再改为 recordData.endTime,减少重复同步。
怎么做:用扩展组件 uni-datetime-picker,通过 v-model 绑定。
易错点:v-model 不要写成“微杠model”;未选择时显示“请选择”。
vue
<uni-list-item show-arrow title="回车时间">
<template v-slot:footer>
<uni-datetime-picker v-model="recordData.endTime">
<view class="picker-value">{{ recordData.endTime || '请选择' }}</view>
</uni-datetime-picker>
</template>
</uni-list-item>违章/故障/事故:显示隐藏与选项复用
为什么需要:三类异常结构相似,都是“是/否”控制详细选项,再让用户点选具体类型。
怎么想:把“是/否”留在各自组件,把“选项高亮、回传选中值”抽成公共 vehicle-options。
怎么做:radio-group 的 change 里用 parseInt(ev.detail.value) 控制 show;选项组件用 tabIndex 高亮,用 dataKey 区分字段。
易错点:show.value = ev.detail.value 可能得到字符串,笔记中违章用 !!parseInt(...) 更稳;data-key 要和 Store 字段一致。
vue
<!-- vehicle-options.vue 核心 -->
<script setup>
import { ref } from 'vue'
import { useTaskStore } from '@/stores/task'
const taskStore = useTaskStore()
const tabIndex = ref(-1)
const props = defineProps({ types: Array, dataKey: String })
function onOptionSelect(index, id, text) {
tabIndex.value = index
console.log(props.dataKey, text)
taskStore.recordData[props.dataKey] = id
}
</script>用 Pinia 汇总表单数据
为什么需要:父页面、三个异常组件、图片组件都要写同一份提交数据,靠父子传参会很散。
怎么想:把接口需要的字段统一放 recordData,各组件直接写对应字段。
怎么做:Store 中定义 id、startTime、endTime、isBreakRules、isFault、isAccident 及描述、图片数组。
易错点:isBreakRules 等布尔值由“是/否”写入;选项值写 id,不要误写 text。
js
// stores/task.js
const recordData = ref({
id: '', startTime: '', endTime: '',
isBreakRules: false, breakRulesType: null, penaltyAmount: null, deductPoints: null,
isFault: false, faultType: null, faultDescription: '', faultImagesList: [],
isAccident: false, accidentType: null, accidentDescription: '', accidentImagesList: [],
})提交数据:过滤图片、判断 code、reLaunch
为什么需要:图片组件回传的是对象数组,而接口只要求 { url };提交后要回到任务列表。
怎么想:转写稿中提货提交也强调“只保留 url”,这里同理;接口成功判断 code === 200,失败 toast。
怎么做:提交前 map(({ url }) => ({ url })),再调 taskApi.record,成功后 uni.reLaunch。
易错点:reLaunch 会清空页面栈,适合回任务列表;不要写成 relench。record 方法中 if (!data.id) return 防止无任务 ID 提交。
js
// apis/task.js
record(data) {
if (!data.id) return
return uniFetch.post('/driver/tasks/truckRegistration', data)
}vue
<script setup>
async function onFormSubmit() {
const { accidentImagesList, faultImagesList } = recordData.value
recordData.value.accidentImagesList = accidentImagesList.map(({ url }) => ({ url }))
recordData.value.faultImagesList = faultImagesList.map(({ url }) => ({ url }))
const { code } = await taskApi.record(recordData.value)
if (code !== 200) return uni.utils.toast('回车登记失败!')
uni.reLaunch({ url: '/pages/task/index' })
}
</script>代码示例
示例 1:交付页面(上传图片 + 表单校验 + 提交)
javascript
// apis/task.js
import { uniFetch } from './uni-fetch'
export default {
// 省略中间部分代码...
/**
* 交付
* @property {Object} data - 接口参数
*/
deliver(data) {
if (!data.id) return
return uniFetch.post('/driver/tasks/deliver', data)
},
}vue
<!-- subpkg_task/delivery/index.vue -->
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import taskApi from '@/apis/task'
// 任务ID
const id = ref('')
// 交付凭证照片
const receiptPictrues = ref([])
// 过滤掉多余的数据,只保留 url
const certificatePictureList = computed(() => {
return receiptPictrues.value.map(({ url }) => {
return { url }
})
})
// 货品照片
const goodsPictrues = ref([])
// 过滤掉多余的数据,只保留 url
const deliverPictureList = computed(() => {
return goodsPictrues.value.map(({ url }) => {
return { url }
})
})
// 凭证和商品都至少上传一张图片
const enableSubmit = computed(() => {
return goodsPictrues.value.length > 0 && receiptPictrues.value.length > 0
})
// 获取地址中的参数
onLoad((query) => {
// 任务ID
id.value = query.value
})
// 提交交付数据
async function onSubmitForm() {
// 表单数据
const formData = {
id: id.value,
certificatePictureList: certificatePictureList.value,
deliverPictureList: deliverPictureList.value,
}
// 调用接口
const { code } = await taskApi.deliver(formData)
if (code !== 200) return uni.utils.toast('上传图片失败!')
// 去到任务列表(查看在途任务)
uni.reLaunch({ url: '/pages/task/index' })
}
</script>
<template>
<view class="page-container">
<view class="receipt-info">
<uni-file-picker
v-model="receiptPictrues"
file-extname="jpg,webp,gif,png"
limit="3"
title="请拍照上传回单凭证"
></uni-file-picker>
<uni-file-picker
v-model="goodsPictrues"
file-extname="jpg,webp,gif,png"
limit="3"
title="请拍照上传货品照片"
></uni-file-picker>
</view>
<button @click="onSubmitForm" :disabled="!enableSubmit" class="button">提交</button>
</view>
</template>要点解释与易错点
uni-file-picker的v-model拿到的是完整对象数组(含name、extname、uuid、url等),而接口的certificatePictureList/deliverPictureList只接受{ url },所以必须用map(({ url }) => ({ url }))过滤,多传字段服务端会挂掉。uni-file-picker自动把图片传到云空间,前提是项目已创建并关联 uniCloud 空间、manifest.json里已获取 uni-app 的 APPID;关联后必须重新运行项目,否则上传会一直失败(表现为没有上传进度条)。file-extname是文件类型白名单,不写webp时用户就选不到 webp 图片;limit控制最多上传张数。- 交付页的 ID 来自地址参数,笔记中交付页取的是
query.value,回车登记页取的是query.transportTaskId,参数名要对齐跳转处,写错会拿不到 ID,接口直接return。 - 校验用
computed返回布尔值,模板里写:disabled="!enableSubmit",两个数组都length > 0才允许提交。
示例 2:回车登记(Pinia 存表单 + 时间选择 + 提交)
javascript
// stores/task.js
import { ref } from 'vue'
import { defineStore } from 'pinia'
export const useTaskStore = defineStore('task', () => {
// 这里定义的数据全部是接口所需要的数据
const recordData = ref({
id: '',
startTime: '',
endTime: '',
/*** 违章 ***/
isBreakRules: false,
breakRulesType: null,
penaltyAmount: null,
deductPoints: null,
/*** 违章 ***/
/*** 故障 ***/
isFault: false,
faultType: null,
faultDescription: '',
faultImagesList: [],
/*** 故障 ***/
/*** 事故 ***/
isAccident: false,
accidentType: null,
accidentDescription: '',
accidentImagesList: [],
/*** 事故 ***/
})
return { recordData }
})vue
<!-- subpkg_task/record/index.vue -->
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { storeToRefs } from 'pinia'
import { useTaskStore } from '@/stores/task'
import taskApi from '@/apis/task'
// 回车登记的全部数据
const { recordData } = storeToRefs(useTaskStore())
// 获取地址参数
onLoad((query) => {
// 任务ID
recordData.value.id = query.transportTaskId
// 发车时间
recordData.value.startTime = query.actualDepartureTime
})
// 提交回车登记
async function onFormSubmit() {
// 过滤掉图片多余的数据,只保留 url
const { accidentImagesList, faultImagesList } = recordData.value
// 事故照片
recordData.value.accidentImagesList = accidentImagesList.map(({ url }) => {
return { url }
})
// 故障照片
recordData.value.faultImagesList = faultImagesList.map(({ url }) => {
return { url }
})
// 调用接口提交数据
const { code } = await taskApi.record(recordData.value)
// 检测接口否调用成功
if (code !== 200) return uni.utils.toast('回车登记失败!')
// 跳转到任务列表
uni.reLaunch({ url: '/pages/task/index' })
}
</script>
<template>
<view class="page-container">
<scroll-view class="scroll-view" scroll-y>
<view class="scroll-view-wrapper">
<uni-list class="base-info">
<uni-list-item
title="出车时间"
show-arrow
:right-text="recordData.startTime"
/>
<uni-list-item show-arrow title="回车时间">
<template v-slot:footer>
<uni-datetime-picker v-model="recordData.endTime">
<view class="picker-value">{{
recordData.endTime || '请选择'
}}</view>
</uni-datetime-picker>
</template>
</uni-list-item>
</uni-list>
</view>
</scroll-view>
<view class="toolbar">
<button @click="onFormSubmit" class="button">提交登记</button>
</view>
</view>
</template>要点解释与易错点
- Store 里用
ref包裹整个对象,页面中必须用storeToRefs()解构,直接const { recordData } = useTaskStore()会丢掉响应性,模板里的recordData.endTime就不会更新。 - 跳转处必须同时带上两个参数,缺一个页面就取不到值:
vue
<!-- pages/task/components/delivery/index.vue 与 subpkg_task/detail/index.vue -->
<navigator
:url="`/subpkg_task/record/index?transportTaskId=${delivery.transportTaskId}&actualDepartureTime=${delivery.actualDepartureTime}`"
hover-class="none"
class="action"
>
回车登记
</navigator>uni-datetime-picker用v-model直接绑定recordData.endTime,值就自动进了 Pinia,不需要再额外赋值;插槽里做|| '请选择'的占位显示。- 提交前必须把
accidentImagesList、faultImagesList用map压成{ url },uni-file-picker给的原始对象字段太多,直接提交会 500。 taskApi.record()内部有if (!data.id) return,若onLoad参数名写错,会静默返回undefined,const { code } = await ...直接报错——先console.log(query)确认参数。- 提交成功后用
uni.reLaunch,它会清空整个页面栈,所以回到任务列表后没有返回按钮,这是业务上期望的行为(区别于navigateTo、redirectTo)。
示例 3:回车登记的可选组件(公共选项组件 + 交通违章面板)
vue
<!-- subpkg_task/record/components/vehicle-options.vue -->
<script setup>
import { ref } from 'vue'
import { useTaskStore } from '@/stores/task'
const taskStore = useTaskStore()
// 当前被选中选项的索引值
const tabIndex = ref(-1)
// 接收传入组件的数据
const props = defineProps({
types: Array,
dataKey: String,
})
// 点击选中选项
function onOptionSelect(index, id, text) {
// 高亮显示选中的选项
tabIndex.value = index
// 用户选择的是哪个类型
console.log(props.dataKey)
// 用户选择的是哪个值
console.log(text)
// 将数据存入 Pinia
taskStore.recordData[props.dataKey] = id
}
</script>
<template>
<view class="vehicle-options">
<view
class="option"
:class="{ active: tabIndex === index }"
v-for="(option, index) in props.types"
:key="option.id"
@click="onOptionSelect(index, option.id, option.text)"
>
{{ option.text }}
</view>
</view>
</template>
<style lang="scss" scoped>
.vehicle-options {
display: flex;
flex-wrap: wrap;
font-size: $uni-font-size-small;
.option {
width: 180rpx;
height: 70rpx;
text-align: center;
line-height: 72rpx;
margin-top: 30rpx;
margin-right: 38rpx;
color: $uni-secondary-color;
border: 2rpx solid $uni-bg-color;
background-color: $uni-bg-color;
border-radius: 20rpx;
&:nth-child(3n) {
margin-right: 0;
}
&.active {
color: $uni-primary;
border: 2rpx solid $uni-primary;
background-color: #ffe0dd;
}
}
}
</style>vue
<!-- subpkg_task/record/components/vehicle-violation.vue -->
<script setup>
import { ref } from 'vue'
import vehicleOptions from './vehicle-options'
import { useTaskStore } from '@/stores/task'
const taskStore = useTaskStore()
// 是不显示详细的选项
const show = ref(false)
// 构造数据
const initialData = ref([
{
title: '违章类型',
key: 'breakRulesType',
types: [
{ id: 1, text: '闯红灯' },
{ id: 2, text: '无证驾驶' },
{ id: 3, text: '超载' },
{ id: 4, text: '酒后驾驶' },
{ id: 5, text: '超速驾驶' },
{ id: 6, text: '其它' },
],
},
{
title: '罚款金额',
key: 'penaltyAmount',
types: [
{ id: '0', text: '0元' },
{ id: '100', text: '100元' },
{ id: '200', text: '200元' },
{ id: '300', text: '300元' },
{ id: '500', text: '500元' },
{ id: '1000', text: '1000元' },
{ id: '2000', text: '2000元' },
],
},
{
title: '扣分',
key: 'deductPoints',
types: [
{ id: '0', text: '0分' },
{ id: '1', text: '1分' },
{ id: '2', text: '2分' },
{ id: '3', text: '3分' },
{ id: '6', text: '6分' },
{ id: '12', text: '12分' },
],
},
])
function onRadioChange(ev) {
// 展开详细的选项
show.value = !!parseInt(ev.detail.value)
// 是否有交通违章
taskStore.recordData.isBreakRules = show.value
}
</script>
<template>
<view class="vehicle-panel">
<view class="vehicle-panel-header">
<view class="label">交通违章</view>
<radio-group class="radio-group" @change="onRadioChange">
<label class="label">
<radio class="radio" value="1" color="#EF4F3F" />
<text>是</text>
</label>
<label class="label">
<radio class="radio" checked value="0" color="#EF4F3F" />
<text>否</text>
</label>
</radio-group>
</view>
<view v-show="show" class="vehicle-panel-body">
<uni-list>
<uni-list-item
v-for="item in initialData"
:key="item.key"
direction="column"
:border="false"
:title="item.title"
>
<template v-slot:footer>
<vehicle-options :data-key="item.key" :types="item.types" />
</template>
</uni-list-item>
</uni-list>
</view>
</view>
</template>
<style lang="scss" scoped>
@import './styles/vehicle-panel.scss';
@import './styles/vehicle-violation.scss';
</style>要点解释与易错点
radio-group的change事件里ev.detail.value是字符串:'0'在 JS 里是 truthy,直接show.value = ev.detail.value会导致选“否”时选项照样展开。必须!!parseInt(ev.detail.value)转成布尔(笔记里vehicle-breakdown、vehicle-accident用的是show.value = ev.detail.value,属于同一个坑)。dataKey是区分“哪个类型的哪个值”的关键:vehicle-options内部用taskStore.recordData[props.dataKey] = id动态写 Pinia,父组件分别传data-key="breakRulesType"、data-key="faultType"、data-key="accidentType",注意data-key是 kebab-case,defineProps里声明的是dataKey。- 模板里
:class="{ active: tabIndex === index }"不需要写.value,模板会自动解包 ref;写成tabIndex.value === index反而报错。 v-for一定要加:key(用option.id或item.key),否则高亮状态在重新渲染时可能错位。types里id有的是数字1、有的是字符串'0',接口若按字符串接收需统一,这是很容易漏掉的类型不一致问题。vehicle-options里tabIndex初始为-1,保证首次进入没有任何选项被高亮。
常见错误
现象:延迟提货页初始未输入时字数提示变红,或输入 1 个字符后提交按钮仍不可用。
原因:把“按钮可用条件”和“字数超限提示条件”写成同一个表达式。
修法:拆成两个条件:按钮用length > 0 && length <= 50;文字变红只用length > 50。现象:交付/提货提交后接口报错,或后端提示参数格式不正确。
原因:直接把uni-file-picker的v-model数组传给接口,数组中包含name、extname、uuid等多余字段。
修法:提交前用map(({ url }) => ({ url }))过滤,只传接口要求的{ url }数组。现象:交付/回车登记页提交时
id为空,接口未调用或返回失败。
原因:onLoad中取的地址参数名与跳转 URL 不一致,或未对id判空。
修法:按跳转 URL 的参数名取值,如query.id、query.transportTaskId;接口封装中保留if (!data.id) return。现象:
uni-file-picker选择图片后上传失败、无回显,或上传后页面数据不更新。
原因:未创建并关联 uniCloud 服务空间、未获取 uni-app AppID,或创建空间后未重启项目。
修法:创建并关联云空间,在manifest.json获取 uni-app AppID,重启项目后再验证上传与v-model回显。现象:回车登记中交通违章/车辆故障/交通事故选择“否”后详情仍展开,或选择项写入 Pinia 的字段不对。
原因:radio的ev.detail.value是字符串,"0"被当作真值;组件未传data-key或写入时未使用选项id。
修法:用!!parseInt(ev.detail.value)转布尔;vehicle-options接收dataKey,点击时写入taskStore.recordData[dataKey] = option.id。现象:提交成功后返回任务列表,仍能返回到已提交页面,或页面栈残留。
原因:跳转 API 选错,把navigateTo、redirectTo、reLaunch混用。
修法:完成后回任务列表用uni.reLaunch({ url: '/pages/task/index' });替换当前页用redirectTo;普通入栈用navigateTo。
练习
- 延迟提货页分别输入空、1 个字符、51 个字符时,写出提交按钮禁用条件和字数提示变红条件,并说明页面表现。
- 在交付页上传两类图片后,构造提交参数
formData,要求id来自地址参数,两个图片数组只包含{ url }。 - 在回车登记页完成交通违章、车辆故障、交通事故的选项点击与“是/否”切换,列出应写入 Pinia 的字段及关键写入逻辑。
- 根据在途列表
status为2、4和任务详情status为4、6,说明应显示的操作或信息,并写出对应跳转 URL 的参数。 - 分别说明从提交成功回任务列表、替换当前页、普通进入详情应选哪个路由 API,并描述页面栈变化。
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 关于延迟提货数据验证的bug说明 | 06:09 |
| 2 | 神领物流-延迟提货 | 09:55 |
| 3 | 补充小程序路由跳转方式的说明 | 11:51 |
| 4 | 神领物流-新建服务空间 | 07:13 |
| 5 | 神领物流-uniCloud服务空间-2 | 05:21 |
| 6 | 神领物流-uni-file-picker | 11:42 |
| 7 | 神领物流-提货-1 | 07:02 |
| 8 | 神领物流-提货-2 | 13:36 |
小结
- 表单校验要区分“按钮是否可用”和“提示样式是否触发”,否则初始态和边界值容易冲突。
- 上传组件的
v-model结果不能直接当接口参数,提交前必须按接口契约裁剪字段。 - 交付和回车登记都依赖地址参数,
onLoad取参后要判空并贯穿到接口参数。 uni-file-picker的可用性依赖 uniCloud 空间关联和 uni-app AppID,环境变更后要重启验证。- 回车登记跨组件数据用 Pinia 统一收口,组件只负责按
dataKey写入对应字段。 - 路由 API 要按页面栈语义选择,提交完成后回任务列表优先用
reLaunch。 - 状态驱动视图:在途列表和任务详情要按
status显示交付、回车登记、交付信息。 - 联调时按“上传→取参→过滤→提交”小步验证,先看网络请求和返回值再排查页面逻辑。