Skip to content

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

本页对应 表单验证与任务通知,共 6 段视频,约 51 分钟。

学习目标 ​

  1. 能够根据接口文档定位测试接口返回 500 的原因,说明缺失必传参数(如配置、分页参数、contentType 等)与 500 响应的关系,并通过补齐参数验证接口恢复 200。
  2. 能够独立封装用户信息查询 API,在页面加载生命周期中调用该 API,检查响应状态码后将用户信息以响应式数据渲染到页面。
  3. 能够说明 uni-app 中 keep-alive 的跨端限制,并使用 v-if 与 v-show 配合实现消息模块组件的首次加载与后续显示/隐藏缓存。
  4. 能够按“先封装接口方法、再在组件中调用、最后在生命周期中触发”的流程完成任务通知列表数据获取,并正确传入 contentType、页码和每页条数等参数。
  5. 能够使用 v-for 与唯一 key 渲染任务通知列表,根据 isRead 控制未读样式,并通过列表长度判断和展示空数据提示。

知识点 ​

作业目标:把各小节标注的任务落到代码里 ​

本节作业不是新增一个大功能,而是完成今天各小节标注的任务:先解释并处理测试接口返回 500 的问题,再完成“我的”用户信息接口调用与渲染,理解消息模块的通知/公告共用接口,处理 uni-app 中组件缓存问题,最后完成任务通知列表的接口封装、调用、渲染和空状态判断。下面按“为什么需要 → 怎么想 → 怎么做 → 易错点”整理。

测试接口返回 500 的排查与处理 ​

为什么需要:登录后测试接口仍报 500,容易让人误以为登录逻辑没写对。转写中明确说“这不赖我们了,这是后端接口的问题”,但必须学会看网络面板定位原因。

怎么想:先看请求 URL 和接口文档。转写中调用的是任务列表 driver task list 一类接口,文档要求必须传 contentType、page、pageSize 等参数;缺参时后端直接返回 500。测试阶段 401、500 不影响登录功能本身,因为除登录接口外,多数接口都要登录后才能用。

怎么做:在测试请求的 data 中补齐必传参数。转写中 pageSize 给 10,page 给 1,contentType 按接口文档或已有测试值填写。补完后重新保存、刷新,状态码从 500 变为 200,并能返回 10 条数据。测试代码若不再需要可删除;若保留,就必须把参数补齐。

js
// 测试请求中补齐接口要求的参数(具体值以接口文档为准)
data: {
  contentType: '按文档填写',
  page: 1,
  pageSize: 10
}

易错点:只看控制台不看网络面板;把缺参导致的 500 误判为登录失败;参数名写成 content、pageSize 拼错;测试代码保留却不补参数。

用户信息接口封装与页面渲染 ​

为什么需要:“我的”页面要展示头像、姓名、司机编号、手机号,这些数据不能写死,必须调用户信息接口。

怎么想:接口文档说明用户信息用 GET,不需要业务参数,但必须带 token。按“先封装接口,再在页面调用”的顺序做。转写中强调:先 console.log 看完整返回,再解构,不要上来就解构。

怎么做:在 apis/user.js 增加 profile 方法;在 my/index 页面用组合式 API 的 onLoad 调用;检测 code 是否为 200,再把对象数据赋给 ref。转写中通过 Vue 调试工具确认 userProfile 已有数据,再渲染到页面。

js
// apis/user.js
export const profile = () => {
  return uni.request({
    url: '按接口文档填写',
    method: 'GET'
  })
}

// my/index
import { onLoad } from '@dcloudio/uni-app'
import { profile } from '@/apis/user'
import { ref } from 'vue'

const userProfile = ref({})

const getUser = async () => {
  const res = await profile()
  console.log(res) // 先看结构,再解构
  if (res.code !== 200) return
  userProfile.value = res.data
}

onLoad(() => {
  getUser()
})

易错点:组合式 API 中不要用 this;小程序没有 Vue Router 概念,不要照搬 vue-router;先打印再解构;不判断 code 就继续执行;头像、姓名、司机编号、手机号字段名要双击复制,避免手写错。

消息模块与组件缓存 ​

为什么需要:消息模块包含任务通知和公告,两块共用一个列表接口,通过 contentType 区分;页面切换时如果组件反复销毁重建,交互状态会丢。

怎么想:转写中先想用 keep-alive,但 uni-app 小程序端不支持,只能 H5 用。于是改用 v-if 首次加载、v-show 后续显隐:第一次点开时用 v-if 把组件加载出来,之后只切换显示隐藏,不再销毁。

怎么做:定义两个 tab 数据项,每项有 render 标记;点击某个 tab 时把对应 render 设为 true,且不再设回 false;模板中第一个组件写 v-if="tabs[0].render" v-show="activeIndex === 0",第二个同理。

html
<first v-if="tabs[0].render" v-show="activeIndex === 0" />
<second v-if="tabs[1].render" v-show="activeIndex === 1" />

易错点:以为 keep-alive 在所有端都能用;把 render 又设回 false 导致组件销毁;v-if 和 v-show 条件写反;只写 v-show 会导致首次不加载。

任务通知列表:先封装接口再调用 ​

为什么需要:任务通知列表要展示通知数据,并支持分页、下拉刷新、上拉加载、全部已读等交互,第一步必须拿到列表数据。

怎么想:转写中反复强调“凡是涉及接口,第一步先封装调用接口的方法;第二步到页面或组件中调用;第三步结合生命周期取数据”。消息相关接口放到 apis/message.js,列表方法参数为 contentType、page、pageSize,默认 page=1、pageSize=10。

怎么做:在 notify 组件中引入 message 的 list 方法,在 onMounted 中调用;先打印结果,再判断 code。转写中第一次漏传 contentType,页面报 500;补上后请求成功,返回 30 条,当前拿到 0—9 条。

js
// apis/message.js
export const list = (contentType, page = 1, pageSize = 10) => {
  return uni.request({
    url: '按接口文档填写',
    method: 'GET',
    data: { contentType, page, pageSize }
  })
}

易错点:参数名 contentType 写错;漏传必传参数导致 500;组件中用错生命周期;先解构后打印,出错时看不出原始结构。

任务通知列表渲染与空状态 ​

为什么需要:列表可能有数据也可能没有数据,严谨页面必须有空提示;未读消息还要显示小红点。

怎么想:先把列表数据赋给 ref,再判断 notifyList.value.length === 0 得到 isEmpty;模板中 v-for 渲染列表,key 用 id;未读判断看 isRead,0 表示未读,取反后绑定类名。

怎么做:请求成功后把真正的列表数组赋给 notifyList.value,不要写顺手写成整个 data;空状态变量初始为 false,请求后按长度判断;跳转详情地址改为任务详情并带 id。

html
<div v-for="item in notifyList" :key="item.id">
  <div :class="{ unread: !item.isRead }">{{ item.content }}</div>
  <div>{{ item.created }}</div>
</div>
<div v-if="isEmpty">暂无消息</div>

易错点:列表数据层级看错;key 不用 id;未读判断漏了取反;空状态初始值写错;有数据时看到 isEmpty 为 false 就以为写错,其实这是正常结果。

作业检查清单 ​

完成后逐项检查:测试接口 500 是否已解释并补参;用户信息接口是否封装、调用、渲染成功;消息模块是否理解通知/公告共用接口;组件缓存是否用 v-if + v-show 替代 keep-alive;任务通知列表是否先封装再调用;contentType、page、pageSize 是否齐全;列表是否渲染、是否处理空状态和未读红点。转写中代码已给出参考,作业时不要只抄结果,要按“先封装、再调用、先打印、再解构、再渲染”的顺序自己走一遍。

代码示例 ​

示例 1:用户信息接口封装 + “我的”页面加载渲染 ​

src/apis/user.js

js
import { http } from '@/utils/uni-fetch'

// 登录(上午已写)
export const login = (data) => {
  return http({ url: '/driver/login/account', method: 'POST', data })
}

// 用户信息:不需要任何参数,只要请求头带 token 就能拿到数据
export const profile = () => {
  return http({
    url: '/driver/profile', // 以接口文档为准
    method: 'GET'
  })
}

src/pages/my/index.vue

vue
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { profile } from '@/apis/user'

// 页面要渲染的数据必须是响应式的
const userProfile = ref({})

const getUser = async () => {
  const res = await profile()
  console.log(res) // 第一步先打印,别急着解构

  if (res.code !== 200) return // code 不对,后面逻辑白执行

  userProfile.value = res.data
}

// 页面加载时调用
onLoad(() => {
  getUser()
})
</script>

<template>
  <view class="user-info">
    <image class="avatar" :src="userProfile.avatar" />
    <view class="name">{{ userProfile.name }}</view>
    <view class="number">{{ userProfile.number }}</view>
    <view class="mobile">{{ userProfile.mobile }}</view>
  </view>
</template>

关键行与易错点

  • import { onLoad } from '@dcloudio/uni-app':小程序里没有 vue-router,onLoad 是 uni-app 的生命周期,页面加载即发请求。
  • const userProfile = ref({}):数据要渲染到页面,必须用 ref 包成响应式;赋值时不能写 userProfile = res.data,必须写 userProfile.value = res.data(Vue3 新手最容易漏 .value)。
  • if (res.code !== 200) return:先校验 code 再赋值,避免脏数据。
  • 调试时先 console.log(res) 看结构,不要一上来就解构,否则解构不出数据时很难定位问题。
  • 相关易错点(视频 1):调任务列表 list 接口时,文档要求 contentType、page、pageSize 三个参数都是必传,少传后端直接返回 500。测试请求也要把参数补齐:
js
const res = await http({
  url: '/driver/task/list',
  method: 'GET',
  data: { contentType: '1', page: 1, pageSize: 10 }
})

示例 2:消息列表接口封装 + 任务通知列表渲染(含空提示、未读红点) ​

src/apis/message.js

js
import { http } from '@/utils/uni-fetch'

export default {
  // 消息列表:contentType 200=任务通知,201=公告
  // page/pageSize 给默认值,调用时可以只传 contentType
  list(contentType, page = 1, pageSize = 10) {
    return http({
      url: '/driver/message/list',
      method: 'GET',
      data: { contentType, page, pageSize }
    })
  }
}

src/pages/message/components/notify.vue

vue
<script setup>
import { ref, onMounted } from 'vue'
import messageApi from '@/apis/message'

const notifyList = ref([])   // 任务通知列表
const isEmpty = ref(false)   // 列表是否为空

const getNotify = async () => {
  const res = await messageApi.list(201)
  console.log(res)

  if (res.code !== 200) {
    uni.showToast({ title: '获取消息失败', icon: 'none' })
    return
  }

  // 真正的列表数据在 data.items 里,不是 data 本身
  notifyList.value = res.data.items
  isEmpty.value = notifyList.value.length === 0
}

// 组件中发请求,推荐使用 vue 的生命周期
onMounted(() => {
  getNotify()
})

const toDetail = (id) => {
  uni.navigateTo({ url: `/subpkg/task/detail?id=${id}` })
}
</script>

<template>
  <view class="notify">
    <view
      v-for="item in notifyList"
      :key="item.id"
      class="card"
      @click="toDetail(item.id)"
    >
      <view class="content">{{ item.content }}</view>
      <view class="time">{{ item.created }}</view>
      <!-- isRead 为 0 表示未读,取反后为 true 才加 unread 类名 -->
      <view class="dot" :class="{ unread: !item.isRead }"></view>
    </view>

    <!-- 空提示:v-for 本身不会渲染,这里不需要写 v-else -->
    <view v-if="isEmpty" class="empty">暂无消息</view>
  </view>
</template>

关键行与易错点

  • page = 1, pageSize = 10:给形参默认值,调用时 messageApi.list(201) 只传一个参数即可,代码更干净。
  • 调用接口必须传 contentType(200 通知 / 201 公告),不传同样返回 500;参数名和值直接从接口文档复制,不要手敲。
  • res.data.items:分页接口返回里还有 count、pageSize 等字段,列表数据在 items 里,顺手写 res.data 会渲染不出来。
  • :class="{ unread: !item.isRead }":isRead 为 0(未读)时取反得 true,才显示小红点;为 1 时不加类名。
  • v-for 必须写 :key="item.id",优先用数据里自带的 id。
  • 空提示用 v-if="isEmpty" 单独判断即可,v-for 遍历空数组本来就不渲染,不需要再写 v-else。
  • 列表跳转地址注意是任务详情(/subpkg/task/detail)而不是消息详情,并把 id 通过 query 带过去。

示例 3:消息页 Tab 切换 + 组件缓存(v-if 配合 v-show) ​

src/pages/message/index.vue

vue
<script setup>
import { ref } from 'vue'
import Notify from './components/notify.vue'
import Notice from './components/notice.vue'

// 每个 tab 带一个 render 标记:是否已经首次加载过
const tabList = ref([
  { title: '任务通知', render: false },
  { title: '公告', render: false }
])
const currentIndex = ref(0)

const switchTab = (index) => {
  currentIndex.value = index
  // 只置为 true,不再改回 false —— 组件一旦加载就一直保留
  tabList.value[index].render = true
}
</script>

<template>
  <view class="message">
    <view class="tab-bar">
      <view
        v-for="(item, index) in tabList"
        :key="index"
        class="tab"
        :class="{ active: currentIndex === index }"
        @click="switchTab(index)"
      >
        {{ item.title }}
      </view>
    </view>

    <!-- v-if 负责首次加载,v-show 负责之后显示/隐藏,变相实现 keep-alive -->
    <Notify v-if="tabList[0].render" v-show="currentIndex === 0" />
    <Notice v-if="tabList[1].render" v-show="currentIndex === 1" />
  </view>
</template>

关键行与易错点

  • uni-app 小程序端 keep-alive 不支持(只支持 H5),所以不能用原生组件缓存,这里用 v-if + v-show 组合替代。
  • v-if="tabList[0].render":第一次点开该 tab 时才创建组件(render 由 false 变 true),组件内的 onMounted 只执行一次。
  • v-show="currentIndex === 0":之后切换只做显示隐藏,组件实例和数据都保留,切回来不用重新请求。
  • 易错点:render 只置 true,千万不要在 switchTab 里把它改回 false,否则等于每次切换都销毁重建,缓存就失效了。
  • v-if 和 v-show 同时写在一个标签上时,v-if 优先级更高,先判断是否加载、再判断是否显示,顺序不能反。
  • 同样的思路后面任务模块的切换也能复用。

常见错误 ​

  1. 测试任务列表接口报 500,误以为登录逻辑写错

    • 现象:登录成功、token 已带上,但 driverTask/list 请求返回 500,页面报错。
    • 原因:该接口有三个必传参数(configType、page、pageSize),测试时用 uni.request 直接发请求没走 data 传参,后端缺参直接 500。
    • 修法:在请求配置里补 data: { configType: 1, page: 1, pageSize: 10 };或者干脆删掉这段测试代码——它只是验证拦截器用的,正式页面不会用。
  2. 拿到接口响应后直接解构,结果 undefined 或渲染空白

    • 现象:const { code, data } = res 之后 data.xxx 报错,或页面什么都不显示。
    • 原因:没确认返回结构就写解构,字段层级(res.data.items / res.data.counts)猜错了。
    • 修法:先 console.log(res) 打印完整响应,在浏览器 Vue Devtools 里确认层级后再解构;解构前先判 code !== 200 直接 return + uni.showToast,避免后续逻辑白跑。
  3. 列表渲染不出来,或把整个分页对象 v-for 了一遍

    • 现象:v-for 无内容,或页面上出现奇怪的对象字段。
    • 原因:把 res.data 整个赋给了列表变量,而真正的数组在 res.data.items 里(同级还有 counts、pageSize)。
    • 修法:list.value = res.data.items;赋值后立刻在 Devtools 里检查该 ref 是否有 10 个元素、是否被 v-for 正确消费;同时补上 :key="item.id",不要用 index。
  4. 在组件里调接口用了 onLoad,请求不触发

    • 现象:页面能进,但接口一直没发出去,数据为空。
    • 原因:onLoad 是页面生命周期,在 components/ 下的组件中不生效;组件里要用 Vue 生命周期。
    • 修法:页面用 onLoad,组件内用 onMounted(从 vue 引入);团队内约定"组件一律用 Vue 生命周期",避免混乱。
  5. 切换 tab 时组件状态丢失,想用 keep-alive 但小程序端不生效

    • 现象:两个 tab 来回切,已加载的列表重新请求、滚动位置丢失。
    • 原因:keep-alive 在 uni-app 中仅 H5 支持,小程序端不支持,跨端方案里不能依赖它。
    • 修法:v-if + v-show 组合——首次进入用 v-if 挂载(配合一个 render[i] 标记置 true),之后显隐全部交给 v-show;标记一旦置 true 就不再回退。
  6. 未读小红点判断写反,已读的反而显示红点

    • 现象:所有消息都带红点,或已读消息也标红。
    • 原因:接口 isRead 中 0 表示未读、1 表示已读,直接 :class="{ unread: item.isRead }" 语义就颠倒了。
    • 修法:写成 :class="{ unread: !item.isRead }";并在 Devtools 里挑一条 isRead=1 的数据验证类名确实消失。

练习 ​

  1. 封装消息列表 API:在 apis/message.js 中新增 list 方法,三个必传参数中 page、pageSize 给默认值(1 / 10),并补上 JSDoc 注释说明参数含义。写完后在调用处验证:只传 configType 也能正常拿到 10 条数据。

  2. 列表空态:给任务通知列表加 isEmpty 判断,要求"接口返回后再判定、初始不显示空提示",并在 Devtools 中确认有数据时该值为 false、模拟空数组时提示正常出现。思考:为什么空态不需要写 v-else。

  3. 组件缓存改造:用 v-if + v-show + render 数组实现"任务通知/公告"两个 tab 的缓存,要求首次切换时组件才挂载、二次切换不再重新请求接口。写完后说明:如果强行用 keep-alive 在小程序端会发生什么。

  4. 未读红点:根据 isRead 字段完成小红点绑定,并额外实现"全部已读"点击后把列表中所有项标记为已读(本地先改数据,再考虑是否需要重新请求接口)。

  5. 故障排查:故意删掉 configType 参数发起请求,按"看 Network 请求 URL 与 query → 看响应体 → 对照接口文档必填项"的顺序定位问题,把排查过程写成三行以内的结论。

课堂画面 ​

课堂画面 01

课堂画面 02

课堂画面 03

课堂画面 04

课堂画面 05

课堂画面 06

原视频对照 ​

#视频时长
1解释测试接口返回500的原因04:09
2神领物流-用户信息12:11
3神领物流-消息模块介绍03:32
4神领物流-消息组件缓存07:08
5神领物流-任务通知列表-111:51
6神领物流-任务通知列表-212:57

小结 ​

  1. 接口报错先分清责任方:500 优先怀疑参数缺失/后端校验,401 优先怀疑 token 与拦截器,不要一上来就改登录逻辑。
  2. 调接口固定三步:先封装 API 方法(带注释和默认参数)→ 在页面/组件中写方法调用 → 由生命周期触发,顺序别乱。
  3. "先打印、再解构、先判 code、再取数"应固化成肌肉记忆,能省掉大部分调试时间。
  4. 列表功能的标准三件套:正确取到数组字段、:key 用业务 id、空态提示——缺一个都算没写完。
  5. 跨端项目要提前确认 API 的端能力边界(如 keep-alive 仅 H5),能力缺失时用组合方案兜底而不是硬扛。
  6. 小程序环境没有 vue-router,跳转与传参要走平台自己的路由方式,跳转地址和携带的 id 都要逐字核对。
  7. Vue Devtools 是验证响应式数据的首选手段,比 console.log 更直观,应作为每次渲染前的固定检查动作。
  8. 为验证某个机制而写的临时代码(如登录后的测试请求),完成后要么删掉、要么补齐参数,别留成"看起来有 bug"的历史包袱。