Skip to content

异常上报(一) ​

本页对应 异常上报,共 8 段视频,约 1 小时 12 分。

学习目标 ​

  1. 能够定位并修复延迟提货中“按钮启用条件”和“字数超限变红条件”混用的问题,分别用“大于0且不超过50”和“仅大于50”实现验证,并演示输入、删除、超限时按钮与文字颜色的变化。
  2. 能够封装延迟提货接口调用方法,在提交时传递任务ID、延迟时间和延迟原因,并根据返回状态码给出失败提示或使用 reLaunch 跳转到首页。
  3. 能够结合页面栈说明 navigateTo、redirectTo、reLaunch 的区别,并通过示例演示三者在页面返回行为上的不同。
  4. 能够完成 uniCloud 服务空间的创建、项目关联和 uni-app AppID 获取与配置,并说明未关联或未重启导致上传失败时的排查方法。
  5. 能够使用 uni-file-picker 配置 title、limit、file-extname、v-model,实现图片上传、类型限制、数量限制、回显及上传结果获取,并在云存储中查看文件。
  6. 能够实现提货页面提交逻辑:用计算属性校验两组图片数组均至少一项,通过 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. 现象:延迟提货页初始未输入时字数提示变红,或输入 1 个字符后提交按钮仍不可用。
    原因:把“按钮可用条件”和“字数超限提示条件”写成同一个表达式。
    修法:拆成两个条件:按钮用 length > 0 && length <= 50;文字变红只用 length > 50。

  2. 现象:交付/提货提交后接口报错,或后端提示参数格式不正确。
    原因:直接把 uni-file-picker 的 v-model 数组传给接口,数组中包含 name、extname、uuid 等多余字段。
    修法:提交前用 map(({ url }) => ({ url })) 过滤,只传接口要求的 { url } 数组。

  3. 现象:交付/回车登记页提交时 id 为空,接口未调用或返回失败。
    原因:onLoad 中取的地址参数名与跳转 URL 不一致,或未对 id 判空。
    修法:按跳转 URL 的参数名取值,如 query.id、query.transportTaskId;接口封装中保留 if (!data.id) return。

  4. 现象:uni-file-picker 选择图片后上传失败、无回显,或上传后页面数据不更新。
    原因:未创建并关联 uniCloud 服务空间、未获取 uni-app AppID,或创建空间后未重启项目。
    修法:创建并关联云空间,在 manifest.json 获取 uni-app AppID,重启项目后再验证上传与 v-model 回显。

  5. 现象:回车登记中交通违章/车辆故障/交通事故选择“否”后详情仍展开,或选择项写入 Pinia 的字段不对。
    原因:radio 的 ev.detail.value 是字符串,"0" 被当作真值;组件未传 data-key 或写入时未使用选项 id。
    修法:用 !!parseInt(ev.detail.value) 转布尔;vehicle-options 接收 dataKey,点击时写入 taskStore.recordData[dataKey] = option.id。

  6. 现象:提交成功后返回任务列表,仍能返回到已提交页面,或页面栈残留。
    原因:跳转 API 选错,把 navigateTo、redirectTo、reLaunch 混用。
    修法:完成后回任务列表用 uni.reLaunch({ url: '/pages/task/index' });替换当前页用 redirectTo;普通入栈用 navigateTo。

练习 ​

  1. 延迟提货页分别输入空、1 个字符、51 个字符时,写出提交按钮禁用条件和字数提示变红条件,并说明页面表现。
  2. 在交付页上传两类图片后,构造提交参数 formData,要求 id 来自地址参数,两个图片数组只包含 { url }。
  3. 在回车登记页完成交通违章、车辆故障、交通事故的选项点击与“是/否”切换,列出应写入 Pinia 的字段及关键写入逻辑。
  4. 根据在途列表 status 为 2、4 和任务详情 status 为 4、6,说明应显示的操作或信息,并写出对应跳转 URL 的参数。
  5. 分别说明从提交成功回任务列表、替换当前页、普通进入详情应选哪个路由 API,并描述页面栈变化。

课堂画面 ​

课堂画面 01

课堂画面 02

课堂画面 03

课堂画面 04

课堂画面 05

课堂画面 06

原视频对照 ​

#视频时长
1关于延迟提货数据验证的bug说明06:09
2神领物流-延迟提货09:55
3补充小程序路由跳转方式的说明11:51
4神领物流-新建服务空间07:13
5神领物流-uniCloud服务空间-205:21
6神领物流-uni-file-picker11:42
7神领物流-提货-107:02
8神领物流-提货-213:36

小结 ​

  • 表单校验要区分“按钮是否可用”和“提示样式是否触发”,否则初始态和边界值容易冲突。
  • 上传组件的 v-model 结果不能直接当接口参数,提交前必须按接口契约裁剪字段。
  • 交付和回车登记都依赖地址参数,onLoad 取参后要判空并贯穿到接口参数。
  • uni-file-picker 的可用性依赖 uniCloud 空间关联和 uni-app AppID,环境变更后要重启验证。
  • 回车登记跨组件数据用 Pinia 统一收口,组件只负责按 dataKey 写入对应字段。
  • 路由 API 要按页面栈语义选择,提交完成后回任务列表优先用 reLaunch。
  • 状态驱动视图:在途列表和任务详情要按 status 显示交付、回车登记、交付信息。
  • 联调时按“上传→取参→过滤→提交”小步验证,先看网络请求和返回值再排查页面逻辑。