Skip to content

完成课程的详情页的模块(Day8 2/2) ​

本页对应 day8,共 5 段视频,约 1 小时 24 分。

学习目标 ​

  • 能基于课程 ID 配置“更多学员评价”页面路由,并实现从课程详情页携带课程 ID 跳转与参数接收。
  • 能使用分页请求完成学员评价列表的触底加载,并根据返回数据与状态值控制“是否还有更多”的判断与展示。
  • 能完成学员评论发布业务:校验评论内容、携带课程 ID/讲师 ID 等参数发起请求,并在成功后重置页码、刷新评价列表且清空输入内容。
  • 能调用收藏与取消收藏接口,根据课程 isCollect 状态切换操作,并在请求成功后同步更新本地课程收藏状态。
  • 能完成学员学习页面:携带课程与视频参数进入页面,获取远程视频播放地址,展示课程章节目录并支持切换小节播放。
  • 能说明并实现 uniapp 支付流程与微信小程序支付流程的一致性,包括获取订单号、获取支付参数、调用 uni.requestPayment 发起支付及支付成功后的处理。

知识点 ​

课程详情页模块的整体思路 ​

为什么需要:课程详情页不是只把标题和图片渲染出来,它还要承载“更多评价、发表评论、收藏、学习、支付”等入口。入口背后都依赖同一个东西:当前课程 ID。没有课程 ID,后端不知道你评价的是哪门课、收藏的是哪门课、播放的是哪门课的视频。

怎么想:先把详情页当成“数据驱动页面”。进入页面前拿到课程 ID,请求课程详情,渲染章节、评价、收藏状态;子页面和子业务再围绕这个 ID 展开。转写稿里反复说“要不要带参”,纠正同音字就是“要不要带参数”,答案是要带课程 ID,评论业务还要带 ID。

怎么做:详情页中所有跳转都拼上 courseID,例如更多评价页、学习页、订单页。跳转方式可用声明式导航 <navigator>,也可用编程式导航 uni.navigateTo;转写稿里“商品式导航”应纠正为“声明式导航”,“nvy/nag”应理解为 navigator。

易错点:只写固定路径不拼 ID;把 courseID 存在跳转之后才赋值;路由参数名前后不一致。调试时先打印路由参数,确认拿到 ID 再发请求。

更多学员评价:复用评价接口并带课程 ID ​

为什么需要:详情页只展示少量评价,用户点“查看更多”要进入完整评价列表。这个列表仍然查“某门课的评价”,所以接口不变,变的是页面和参数。

怎么想:更多评价页不是新业务,而是同一接口的分页展示。进入页面先拿 courseID,挂载时请求第一页;触底时判断是否还有更多,再请求下一页。

怎么做:路由配置 course/comment,跳转时拼 ?courseID=xxx。页面 onLoad 保存课程 ID,维护 page、pageSize、hasMore、commentList。请求成功后用 list.push(...res.data.items) 追加,不能用二维数组直接塞。根据返回条数判断 hasMore。

易错点:页码写死为 1,导致永远加载第一页;追加时忘记解构;触底不判断 hasMore 重复请求。调试时打印 res.data.items.length 和当前页码,确认分页是否推进。

学员评论:提交成功后要刷新第一页 ​

为什么需要:用户发表评论后,如果只把新评论 push 到列表尾部,页面看起来“没出现”,因为最新评论应在前面;刷新页面后又会发现数据对不上。

怎么想:评论提交是写操作,成功后应让列表回到“最新第一页”的状态,而不是在本地随意插入。转写稿里调试时发现添加成功后看不到新评论,原因是列表已有 10 条,新数据被放到后面。

怎么做:点击发表前先判断内容是否为空,空内容提示“请打字儿/请输入内容”,不发请求。请求参数带 courseID、teacherID、content。成功后清空 commentList、page 归 1,再重新拉第一页;同时清空输入框。

易错点:字段名写错,如 content 写成 conl; ID 没从详情页带过来;成功后只提示不刷新。调试时先打印请求参数和 res.code,再检查列表是否重置。

收藏与取消收藏:不能只改本地状态 ​

为什么需要:收藏按钮要在“已收藏/未收藏”之间切换。若只改本地 isCollect,页面刷新就恢复原状,因为数据库没变。

怎么想:点击时先看当前 isCollect:为真说明已收藏,应调取消收藏接口;为假说明未收藏,应调收藏接口。成功后同步本地字段,避免再发一次详情请求。

怎么做:封装 REQcollect(courseID) 和 REQdeleteCollect(courseID)。点击处理中根据 course.isCollect 分支调用,成功后执行 course.isCollect = !course.isCollect,并提示“收藏/取消收藏”。转写稿验证:本地切换后刷新丢失,改走接口后刷新仍保持。

易错点:只写 isCollect = !isCollect 不发请求;收藏和取消收藏接口方法写反;成功后重新拉详情造成多余请求。调试时看 Network 请求和刷新后的字段。

学员学习页:视频地址必须是远程地址 ​

为什么需要:已购买课程点“去学习”进入视频页;未购买进入订单支付。学习页要展示章节,并默认播放第一章第一小节。

怎么想:学习页需要课程 ID 和视频 ID。进入时先拿课程详情渲染章节,再根据默认小节 ID 请求播放地址。转写稿强调:uni-app/微信小程序播放视频不能用本地资源,必须远程地址。

怎么做:路由跳转带 courseID 和默认 videoID。页面 onLoad 保存 ID,请求课程详情和 REQplayerURL(videoID)。拿到地址存 playerURL,视频组件绑定远程地址。点击章节时拿该小节 ID 再请求播放地址。

易错点:字段大小写写错,转写稿调试时就是 playerURL 没拿到;把视频文件放本地;章节切换不更新视频 ID。调试先打印接口返回字段名。

支付入口:流程与微信小程序一致,provider 按平台填 ​

为什么需要:未购买课程要进入订单页,先拿订单号,再拿支付参数,最后调起支付。转写稿说支付流程和微信小程序一模一样,区别是 uni-app 用 uni.requestPayment。

怎么想:不要一上来调支付,要先完成“下单/取支付参数”。支付成功后还要查询状态或处理业务。provider 是平台标识,按文档填,支付宝是 alipay,微信小程序是 wxpay,不要凭口写中文。

怎么做:订单页点击支付后按顺序发请求,拿到参数再调用:

js
uni.requestPayment({
    provider:'alipay'
    .......
})

易错点:参数没带全;provider 写错;支付成功不处理后续业务;只看前端提示不验证状态。调试时按“订单号→支付参数→调起支付→查询状态”的顺序打印。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

示例一:课程「更多学员评价」页 —— 分页 + 触底加载 ​

vue
<!-- /pages/course/comment/index.vue -->
<template>
  <view class="comment-page">
    <view class="comment-item" v-for="item in commentList" :key="item.id">
      <view class="item-header">
        <image class="avatar" :src="item.avatar" />
        <text class="nickname">{{ item.nickname }}</text>
      </view>
      <text class="content">{{ item.content }}</text>
    </view>
    <view class="more" v-if="hasMore === 'no'">没有更多了</view>
  </view>
</template>

<script setup>
import { ref } from 'vue'
import { onLoad, onReachBottom } from '@dcloudio/uni-app'
import { REQStudentComment } from '@/api/index.js'

const courseId = ref('')       // 看的是哪个课程的评价
const page = ref(1)            // 当前页码
const pageSize = ref(10)       // 每页展示条数
const commentList = ref([])    // 学员评价列表
const hasMore = ref('more')    // 'more' 还有更多 / 'no' 没有更多

const getStudentComment = async () => {
  const result = await REQStudentComment({
    page: page.value,
    limit: pageSize.value,
    courseId: courseId.value
  })
  if (result.code === 200) {
    const list = result.data.item
    // 拿到的条数小于每页条数 => 没有下一页了
    hasMore.value = list.length < pageSize.value ? 'no' : 'more'
    commentList.value = [...commentList.value, ...list]
  }
}

onLoad((options) => {
  courseId.value = options.courseId   // 从路由参数里取课程ID
  getStudentComment()
})

onReachBottom(() => {
  if (hasMore.value === 'no') return  // 没有更多就不发请求
  page.value++                        // 只有触底才让页码 +1
  getStudentComment()
})
</script>

<style>
.comment-page { padding: 20rpx; }
.comment-item { padding: 20rpx 0; border-bottom: 1rpx solid #eee; }
.item-header { display: flex; align-items: center; }
.avatar { width: 60rpx; height: 60rpx; border-radius: 50%; margin-right: 16rpx; }
.content { display: block; margin-top: 12rpx; color: #333; }
.more { text-align: center; color: #999; padding: 24rpx 0; }
</style>

要点与易错点:

  • courseId 必须从 onLoad(options) 里取。课程详情页的「查看更多」跳转时要拼上 ?courseId=xxx,否则进来请求的是“哪个课程”都不知道。
  • page++ 只能写在 onReachBottom 里。如果写在 getStudentComment 内部,第一次挂载也会变成第 2 页,永远漏第一页。
  • 拼接数组一定要用 [...commentList.value, ...list]。直接 push(list) 会把整个数组塞进去,变成二维数组,页面渲染直接崩。
  • 判断“还有没有更多”用 list.length < pageSize。拿到满 10 条就认为可能还有下一页,转写稿里说的就是这个“状态值”(more / no)。
  • onReachBottom 是页面级钩子,写在组件里不生效,必须写在页面文件里。

示例二:学员评价页里「添加评论」 ​

vue
<!-- 同上页面 /pages/course/comment/index.vue 中追加 -->
<template>
  <view class="comment-add">
    <textarea v-model="content" class="input" placeholder="说说你的学习感受吧" />
    <text class="send" @click="ADD">发表</text>
  </view>
</template>

<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { REQADDComment, REQStudentComment } from '@/api/index.js'

const courseId = ref('')
const teacherId = ref('')
const content = ref('')          // 双向绑定评论内容
const page = ref(1)
const pageSize = ref(10)
const commentList = ref([])
const hasMore = ref('more')

onLoad((options) => {
  courseId.value = options.courseId
  teacherId.value = options.teacherId   // 后端要求带ID,跳转时一起带过来
})

const ADD = async () => {
  // 1. 先判断内容是否为空,空的不发请求
  if (content.value.trim() === '') {
    uni.showToast({ title: '请输入评论内容', icon: 'none' })
    return
  }
  // 2. 发请求:课程ID + ID + 内容
  const result = await REQADDComment({
    courseId: courseId.value,
    teacherId: teacherId.value,
    content: content.value
  })
  // 3. 添加成功:清空输入框 + 页码归 1 + 清空列表 + 重新拉一次
  if (result.code === 200) {
    uni.showToast({ title: '评论成功' })
    content.value = ''
    page.value = 1
    commentList.value = []
    hasMore.value = 'more'
    const res = await REQStudentComment({
      page: page.value,
      limit: pageSize.value,
      courseId: courseId.value
    })
    if (res.code === 200) {
      commentList.value = res.data.item
    }
  }
}
</script>

<style>
.comment-add { display: flex; align-items: center; padding: 20rpx; border-top: 1rpx solid #eee; }
.input { flex: 1; height: 80rpx; background: #f5f5f5; padding: 10rpx; border-radius: 8rpx; }
.send { margin-left: 20rpx; color: #1989fa; }
</style>

要点与易错点:

  • 空内容必须拦在发请求之前。转写稿强调“先判断内容是否为空,为空既不能发表也不能发请求”。
  • 成功后不能只 push 新评论。列表用的是 push(尾部追加),新评论会跑到列表最下面看不见,所以必须 page = 1 + commentList = [] 后重新拉第一页。
  • page 和 hasMore 要一起归位,否则重新加载后触底逻辑会错乱。
  • teacherId 是后端接口要求的参数,课程详情页跳「更多评价」时要把 courseId 和 teacherId 都拼进 URL,缺一个后端就报参数错误。
  • 输入框用 v-model 收集,别用 @input 手动赋值,容易漏掉 trim 判断。

示例三:收藏 / 取消收藏 + 去学习与支付分流 ​

vue
<!-- /pages/course/detail/index.vue 中的底部操作栏逻辑 -->
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import {
  REQCourseDetail,     // 获取课程详情
  REQCollect,          // 收藏(POST)
  REQDeleteCollect,    // 取消收藏(DELETE)
  REQOrderNo           // 下单,拿支付参数
} from '@/api/index.js'

const courseId = ref('')
const courseDetail = ref({})

const getCourseDetail = async () => {
  const result = await REQCourseDetail({ courseId: courseId.value })
  if (result.code === 200) courseDetail.value = result.data
}

// 收藏 / 取消收藏
const handlerCollect = async () => {
  if (courseDetail.value.isCollect) {
    // 已收藏 -> 取消收藏
    const res = await REQDeleteCollect({ courseId: courseId.value })
    if (res.code === 200) {
      courseDetail.value.isCollect = false      // 同步本地字段,省一次详情请求
      uni.showToast({ title: '取消收藏' })
    }
  } else {
    // 未收藏 -> 收藏
    const res = await REQCollect({ courseId: courseId.value })
    if (res.code === 200) {
      courseDetail.value.isCollect = true
      uni.showToast({ title: '收藏成功' })
    }
  }
}

// 已购买去学习,未购买去订单页
const handlerLearn = () => {
  if (courseDetail.value.isBuy) {
    uni.navigateTo({
      url: `/pages/course/video/index?courseId=${courseId.value}`
    })
  } else {
    uni.navigateTo({
      url: `/pages/order/index?courseId=${courseId.value}`
    })
  }
}

// 订单页发起支付
const pay = async () => {
  const orderRes = await REQOrderNo({ courseId: courseId.value })
  if (orderRes.code !== 200) return
  const payParams = orderRes.data

  uni.requestPayment({
    provider: 'alipay',          // 笔记示例;微信小程序按文档填 wxpay
    timeStamp: payParams.timeStamp,
    nonceStr: payParams.nonceStr,
    package: payParams.package,
    signType: payParams.signType,
    paySign: payParams.paySign,
    success: () => {
      uni.showToast({ title: '支付成功' })
    },
    fail: () => {
      uni.showToast({ title: '支付取消', icon: 'none' })
    }
  })
}

onLoad((options) => {
  courseId.value = options.courseId
  getCourseDetail()
})
</script>

要点与易错点:

  • 不要只改本地 isCollect 做“假收藏”。转写稿里明确说过:本地切一下看着有效果,一刷新就没了。必须走收藏 / 取消收藏两个接口。
  • 接口成功后再同步本地字段(courseDetail.value.isCollect = false),这样可以少发一次详情请求,且字段与数据库一致。
  • 收藏用 POST、取消收藏用 DELETE,两个是不同接口,别写成一个。
  • 学习按钮要按 isBuy 分流:已购买进 /pages/course/video/index(学习页),未购买进订单页;学习页跳转要带上 courseId,进去后才能拉章节和第一个视频的播放地址。
  • uni.requestPayment 比原生 wx.requestPayment 多一个 provider 参数,微信小程序用 uni 写时必须带;支付宝则是 alipay(笔记原样),具体取值以支付平台文档为准。
  • 支付参数(timeStamp / nonceStr / package / signType / paySign)一律来自后端下单接口返回,前端不要自己拼。

常见错误 ​

  1. 现象:从课程详情跳更多评价页后,请求拿到空课程ID或评价列表不加载。→ 原因:路由跳转未带课程ID,或评价页未在 onLoad 中保存参数。→ 修法:跳转时拼 courseId,进入页面后先存课程ID,再发起评价列表请求。
  2. 现象:添加评论成功后,新评论看不到或出现在列表最底部。→ 原因:成功后只做 push,未按“最新评论”刷新列表;页码也未归1。→ 修法:成功后清空输入、页码重置为1、重新拉第一页并替换列表。
  3. 现象:收藏按钮点击后状态会变,但刷新后又恢复。→ 原因:只改了本地 isCollect,没有调用收藏/取消收藏接口,或成功后未同步本地字段。→ 修法:按当前状态分别调用收藏/取消收藏接口,成功后同步 course.isCollect。
  4. 现象:学习页视频不播放或播放地址为空。→ 原因:视频地址字段大小写写错,或默认章节/小节取值路径不对。→ 修法:核对接口返回字段名,按“第一章第一个小节”的路径取 videoId,再请求播放地址。
  5. 现象:评价页触底加载重复请求或没有更多时仍请求。→ 原因:未用状态值控制是否还有下一页,或页码未递增。→ 修法:用 isMore/noMore 判断,触底时先判断状态,再递增页码并请求。
  6. 现象:uni-app 支付在小程序端调不起。→ 原因:uni.requestPayment 缺少 provider,或参数数量按原生小程序习惯少传。→ 修法:按文档补齐参数,微信小程序传 provider:'wxpay',支付宝传对应 provider。
  7. 现象:评论页请求参数中ID为空,添加评论失败。→ 原因:课程详情跳评论页时只带了课程ID,未带 teacherId。→ 修法:两个“查看更多评价”入口都补 teacherId,评论页进入后保存并使用。
  8. 现象:收藏/取消收藏接口请求方法或参数位置写错。→ 原因:收藏用 POST、取消用 DELETE,课程ID放置位置与文档不一致。→ 修法:对照接口文档封装 POST/DELETE,确认课程ID放在 query 还是 body。

练习 ​

  1. 给“查看更多评价”的两个入口补齐路由参数:courseId 与 teacherId,并在评价页 onLoad 中保存后发起列表请求。
  2. 实现评价列表触底加载:第一页10条,状态为“还有更多”时页码+1并请求,状态为“没有更多”时展示提示且不再请求。
  3. 完成添加评论:内容为空时提示并阻止请求;成功后清空输入、页码归1、重新拉第一页替换列表。
  4. 完成收藏/取消收藏:根据 isCollect 调不同接口,成功后同步本地字段,刷新后状态仍正确。
  5. 从课程详情进入学习页:带上 courseId 和默认第一个小节的 videoId,请求播放地址并用远程视频地址播放。

原视频对照 ​

#视频时长
1更多的学员评价20:13
2完成学员评论的业务18:16
3完成收藏与取消收藏11:18
4学员学习页面25:20
5uniapp支付流程与微信小程序一致09:04

小结 ​

  1. 详情页相关跳转必须检查参数是否完整,尤其是课程ID、ID、视频ID。
  2. 评价列表的分页核心是“页码 + 是否还有更多 + 触底判断”,三者缺一容易重复请求或漏加载。
  3. 添加评论成功后不要只追加,按最新评论场景应重置分页并重新拉第一页。
  4. 收藏状态不能只做本地切换,必须与接口结果和本地字段同步。
  5. 学习页默认播放依赖章节/小节数据路径,字段名和取值层级要按接口返回核对。
  6. uni-app 支付流程与微信小程序一致,但 uni.requestPayment 的参数和 provider 必须按文档补齐。
  7. 涉及远程视频时不要使用本地资源,播放地址应从接口获取。
  8. 联调阶段优先打印关键参数和接口返回,能快速定位空ID、字段大小写、请求方法等低级错误。