主题
个人中心登录业务与完成课程的详情页的模块(Day8 1/2)
本页对应 day8,共 5 段视频,约 2 小时 1 分。
学习目标
- 能独立完成个人中心授权登录流程:配置登录页路由,校验用户协议勾选状态,调用微信登录获取临时凭证
code,使用code请求并持久化存储token,登录成功后返回个人中心。 - 能在个人中心页面使用
onShow判断本地token是否存在,未登录时跳转登录页,已登录时请求并渲染用户头像与昵称,且避免重复请求用户信息。 - 能实现课程详情页跳转:从首页课程卡片和课程列表点击进入详情页,路由跳转时携带课程 ID,并在详情页通过路由参数获取该 ID。
- 能根据课程 ID 调用课程详情接口,完成课程封面、价格、购买人数、标题、标签、讲师头像/姓名/介绍、课程富文本详情及章节折叠面板的数据渲染。
- 能根据课程详情接口返回的购买状态和收藏状态,动态渲染底部“去学习/去支付”“已收藏/未收藏”等按钮文案与样式,并实现课程详情页滚动监听控制返回顶部按钮显示。
- 能根据课程 ID 调用学员评价列表接口,分页获取评价数据,渲染评价总数、评价人头像、昵称、时间、内容与评分,并使用 UI 组件完成折叠面板、评分和“查看更多”等展示。
知识点
登录页与路由准备
为什么需要:个人中心不是一进来就展示用户信息,而是先判断有没有授权登录。没有登录时,应进入登录页;登录成功后再回到个人中心。因此必须先有登录页和对应路由。
怎么想:转写稿里明确说,uni-app 在 VS Code 中新建页面时,路由不会自动创建,需要自己补到 pages 配置里。登录页静态和样式不用手写,可以直接从项目图度中拿过来。样式用了 less,而脚手架默认支持 CSS,所以需要安装 less 依赖。
怎么做:新建 pages/login/index.vue,在 pages.json 中补登录页路由,标题写“授权登录”。如果页面样式是 less 嵌套语法,先安装 less。登录页打开后,先检查静态结构是否正常显示。
易错点:忘记补路由会导致页面打不开;只写 less 语法但不装依赖,页面会编译报错。转写中就是先发现静态没出来,回头检查到 less 依赖问题后才恢复。
协议勾选与登录前置判断
为什么需要:登录页底部有“我已阅读并同意用户协议”的勾选。没有勾选时不能继续微信登录,否则业务上等于用户未同意协议。
怎么想:勾选状态用一个响应式变量控制,例如 agree,默认 false。模板中用动态类名控制小勾图标是否出现;点击图标时切换 agree。点击“微信登录”时先判断 agree,未勾选就提示并 return,不再执行后面的 uni.login。
怎么做:
js
const agree = ref(false)
function toggleAgree() {
agree.value = !agree.value
}
function handleLogin() {
if (!agree.value) {
uni.showToast({ title: '请务必勾选上协议' })
return
}
// 继续微信登录
}模板中动态类名可写成 :class="{ checked: agree }",具体类名按登录页原有样式保留。
易错点:只提示但忘记 return,后面仍会执行登录;默认把 agree 写成 true,不符合“用户自己勾选”的流程。转写中特别强调:不同意协议时,提示后不能继续往下走。
微信登录流程:code → token → 用户信息
为什么需要:个人中心要展示头像和昵称,必须先确认用户身份。uni-app 的微信登录不能直接拿用户信息,而是先拿临时凭证 code,再用 code 换 token,最后用 token 请求用户信息。
怎么想:本节笔记给出的流程是:
text
微信登录流程: uniapp
uni.login: code 获取用户登录临时凭证
code[网络请求]: 获取 token[用户已经授权登录成功] -> 本地持久化存储
token[网络请求]: 获取用户信息所以顺序不能乱:先 uni.login 拿 code,再请求后端换 token,把 token 存本地,最后带 token 请求用户信息。
怎么做:在协议判断通过后调用 uni.login,成功回调里取 res.code,再调用换 token 的接口。接口返回成功后,用 uni.setStorageSync 持久化 token,然后跳回个人中心。转写中的调用结构可整理为:
js
uni.login({
success(res) {
const code = res.code
getToken(code)
}
})
async function getToken(code) {
const result = await reqToken(code)
if (result.code === 200) {
uni.setStorageSync('token', result.data.token)
uni.navigateTo({ url: '/pages/user/index' }) // 路径按项目实际路由
}
}请求用户信息时,要把 token 放进请求头,字段名以接口文档为准。转写中强调要检查 Network 里的请求头,确认 token 是否真的带上。
易错点:把 uni.login 拿到的 code 当成 token 直接存;换 token 成功后忘记本地持久化;请求用户信息时没带 token,导致接口失败。转写中就是通过 Network 发现请求头没带 token,回头补请求头后才拿到用户信息。
个人中心鉴权与用户信息渲染
为什么需要:用户可能第一次访问个人中心,也可能已经登录过。每次进入个人中心都要判断本地有没有 token:没有就去登录页,有就请求或读取用户信息。
怎么想:转写中比较了 onLoad 和 onShow。个人中心每次显示都可能需要重新判断登录状态,所以用 onShow 更合理。拿到 token 后请求用户信息;如果已经有用户信息,就不必每次重复发请求。渲染时做兜底:有头像显示头像,没有头像显示本地 logo;有昵称显示昵称,没有昵称显示“请授权登录”。
怎么做:
js
onShow(() => {
const token = uni.getStorageSync('token')
if (!token) {
uni.navigateTo({ url: '/pages/login/index' })
return
}
if (!userInfo.value?.name) {
getUserInfo()
}
})渲染可写:
html
<img :src="userInfo?.avatar || '/static/logo.png'" alt="用户头像" />
<text>{{ userInfo?.name || '请授权登录' }}</text>易错点:没有 token 时忘记 return,继续请求用户信息;每次 onShow 都重复请求用户信息;头像地址多复制了引号导致图片不显示。转写中就出现过头像字段带多余双引号、图片渲染异常的情况,需要打印 userInfo 检查真实值。
从课程列表进入详情页:路由必须带课程 ID
为什么需要:课程详情页要展示“某一个课程”的封面、价格、、目录和评价,如果不把课程 ID 带过去,详情页就不知道要请求哪门课。
怎么想:点击首页课程卡片或课程列表项时,先判断当前点击的是课程还是;只有课程才跳课程详情,并在 URL 上拼 courseId。
怎么做:在课程项上绑定点击事件,例如 @click="goCourseDetail(item.id)",方法里使用 uni.navigateTo,路径写成课程详情页路由,并追加 ?courseId=...。页面路由要在 pages.json 中手动补上,因为 uni-app 新建页面时路由不一定自动生成。
易错点:路由路径不要多写或少写斜杠;跳转 URL 要带参数,页面路由配置里不写参数;卡片和课程卡片要分开处理,不能都跳课程详情。
先封装“获取某一个课程详情”的 API
为什么需要:详情页数据多,直接把请求写在页面里不利于复用和排错,先封装接口更清晰。
怎么想:接口文档中课程详情通常需要课程 ID,属于 GET 请求;页面进入后先拿到路由参数中的 courseId,再发请求。
怎么做:在 API 文件中新增“获取某一个课程详情”的方法,接收课程 ID,拼到请求地址上;页面里引入该方法,在进入详情页时调用。请求成功后把 result.data 存入响应式对象,例如 courseInfo。
易错点:课程 ID 要在发请求前保存或直接从路由参数取;不要等请求发完才存 ID;接口返回的字段要先在 Network 里看清,再决定渲染哪个字段。
详情页数据渲染:先拿数据再渲染,处理 undefined
为什么需要:接口数据是异步返回的,页面初次渲染时 courseInfo 可能还是空对象,直接写 courseInfo.cover 容易报“Cannot read property”。
怎么想:模板中要么用 v-if 控制区域,要么用可选链 courseInfo?.cover;转写中封面报错就是数据未回来时直接取属性导致的。
怎么做:封面、价格、购买人数、课程标题、标签、头像/名字/介绍分别绑定接口字段;课程详情富文本用 v-html 渲染,不能用普通插值,否则标签会当文本显示。
易错点:富文本字段里可能包含 HTML 标签,必须用 v-html;价格、购买人数等字段名要以接口为准;不要凭感觉写字段名,先打印 result.data 确认。
课程目录用 uni-ui 折叠面板
为什么需要:课程目录有“大章节—小章节”的层级,自己手写折叠交互成本高。
怎么想:转写中明确使用 uni-ui 的折叠面板组件,外层用 uni-collapse,每一项用 uni-collapse-item;需要只展开一个时加手风琴属性。
怎么做:用 v-for 遍历大章节,每个 uni-collapse-item 展示大章节标题;内部再 v-for 遍历小章节,展示小章节标题。小章节是否显示小锁,要根据课程是否购买判断。
易错点:内外层 v-for 的变量名不要重复;v-for 要写 :key;折叠组件要确认已安装/引入 uni-ui;小锁显示条件要跟购买状态字段对应。
底部状态:收藏与去学习/去支付
为什么需要:同一门课对已购买和未购买用户应展示不同操作;收藏状态也要即时区分。
怎么想:接口中通常有是否购买、是否收藏字段,例如转写中提到的 isBuy、isCollection。已购买显示“去学习”,未购买显示“去支付”;已收藏显示高亮,未收藏显示灰色。
怎么做:用条件渲染切换文字和图标;收藏点击时根据当前状态切换并提示。支付流程按转写思路是:先提交订单,再根据订单信息调起支付。笔记中的支付代码要保留:
js
uni.requestPayment({
provider:'alipay'
.......
})易错点:不要把所有课都写死成“去学习”;支付代码只是调起支付,前面还需要订单信息;收藏图标切换要跟数据状态一致,不能只改样式不改状态。
学员评价:单独接口、分页参数、列表与星级
为什么需要:课程详情接口里没有评价数据,评价要单独请求,否则评价区域无法动态渲染。
怎么想:评价接口一般需要当前页、每页条数和课程 ID;详情页默认取第一页若干条,同时保存总条数和评价列表。
怎么做:封装评论列表 API,页面进入时传 page=1、limit=10、courseId;成功后保存 total 和列表数组。模板中顶部显示“学员评价共多少个”,下面 v-for 渲染头像、昵称、时间、内容,星级可用 uni-rate。
易错点:课程 ID 不要漏传;total 和列表字段名要按接口返回写;时间戳需要格式化;星级测试时不要随意写超过 5 的分值,真实分数应由接口数据决定。调试时多看 Network 的请求参数、请求头和返回数据,不要只盯页面效果。
课堂画面





代码示例
1. 登录授权页 pages/login/index.vue(协议勾选 + 微信登录三步走)
vue
<template>
<view class="login-page">
<!-- 顶部 logo 区(静态资源,直接沿用原页面样式) -->
<image class="logo" src="/static/logo.png" mode="aspectFit"></image>
<!-- 微信登录按钮 -->
<view class="login-btn" @click="handleWxLogin">微信登录</view>
<!-- 底部协议区 -->
<view class="footer">
<view
class="checkbox"
:class="{ checked: isAgree }"
@click="isAgree = !isAgree"
></view>
<text>我已阅读并同意</text>
<text class="link">用户协议</text>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import ajax from '@/api/login'
// 协议是否勾选,默认不勾选
const isAgree = ref<boolean>(false)
const handleWxLogin = () => {
// 1. 没勾选协议直接拦截,必须 return,否则后面照样执行
if (!isAgree.value) {
uni.showToast({
title: '请务必勾选用户协议',
icon: 'error'
})
return
}
// 2. uni.login 获取登录临时凭证 code
uni.login({
provider: 'weixin',
success: async (res) => {
const { code } = res
// 3. code 换 token
const tokenRes: any = await ajax.reqGetToken({ code })
if (tokenRes.code === 200) {
// 4. token 本地持久化
uni.setStorageSync('token', tokenRes.data.token)
// 5. 回到个人中心
uni.navigateBack()
}
}
})
}
</script>
<style lang="less" scoped>
.checkbox {
width: 24px;
height: 24px;
margin-right: 8px;
background: url('/static/check.png') no-repeat;
background-size: 100% 200%;
// 精灵图:未勾选取下半张,勾选取上半张
background-position: 0 -24px;
}
.checkbox.checked {
background-position: 0 0;
}
</style>关键行与易错点
if (!isAgree.value) { ... return }:转写里反复强调「少了个 return」,不写 return 的话提示弹完了登录逻辑照样往下跑。uni.login({ provider: 'weixin' })的success回调里才有code,这个code就是「用户登录临时凭证」,是后面换 token 的唯一依据。uni.setStorageSync('token', ...)必须用Sync版本,异步版本在跳转前可能还没写完,个人中心onShow就读不到 token。- 协议小勾选框用的是动态类名 + 精灵图 background-position,不是换图标;
:class="{ checked: isAgree }"里isAgree是 ref,模板中自动解包,不要写isAgree.value。 - 登录页用的是 less 嵌套语法,必须装 less 依赖,否则页面白屏不渲染。
- 返回个人中心:
uni.navigateBack()适用于页面栈来源;如果个人中心是 tabBar 页,要换成uni.switchTab。
2. 个人中心 pages/user/index.vue(onShow 判登录 + 拉用户信息)
vue
<template>
<view class="user-page">
<image
class="avatar"
:src="userInfo.avatar || '/static/logo.png'"
mode="aspectFill"
></image>
<text class="nickname">{{ userInfo.nickName || '请授权登录' }}</text>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import ajax from '@/api/user'
// 用户信息,初始给空对象,模板里用 || 兜底
const userInfo = ref<any>({})
// 只有页面显示时才执行,且每次切回都执行
onShow(() => {
const token = uni.getStorageSync('token')
// 1. 没 token,说明没授权过,去登录页
if (!token) {
uni.navigateTo({ url: '/pages/login/index' })
return
}
// 2. 有 token 但没用户信息,才发请求(避免每次切 tab 都请求一次)
if (!userInfo.value.nickName) {
getUserInfo()
}
})
const getUserInfo = async () => {
const res: any = await ajax.reqUserInfo()
if (res.code === 200) {
// 3. 存一份到内存,同时持久化到本地
userInfo.value = res.data
uni.setStorageSync('userInfo', res.data)
}
}
</script>关键行与易错点
- 用
onShow而不是onLoad:onLoad只在页面第一次加载执行,从登录页返回时不会再走;onShow每次页面显示都执行一次。 if (!token)后面必须return,否则会继续往下走getUserInfo(),而这时请求头里根本没有 token。if (!userInfo.value.nickName)这个判断是性能优化点:不加的话每次切 tab 回个人中心都发一次请求,发 1 次和发 100 次效果完全一样。:src="userInfo.avatar || '/static/logo.png'":数据回来前userInfo是空对象,直接写userInfo.avatar不会报错但会是空图片,用||兜底成本地图。- 模板中
{{ userInfo.nickName || '请授权登录' }},同样是为了「有数据展示数据,没数据显示占位文案」。 - 请求头带 token 是在 axios/uni.request 拦截器里统一处理的(判断本地有无 token,有就往 headers 塞
token字段),页面里不用手动传。
3. 课程详情页 pages/course/detail/index.vue(取 ID → 请求详情 + 评价 → 渲染)
vue
<template>
<view class="course-detail">
<!-- 1. 课程头部:封面 / 价格 / 购买人数 / 标题 / 标签 -->
<view v-if="course.course">
<image class="cover" :src="course.course.cover" mode="aspectFill"></image>
<view class="price-box">
<text class="price">¥{{ course.course.price }}</text>
<text class="buy">已有 {{ course.course.buyCount }} 人购买</text>
</view>
<view class="title">{{ course.course.title }}</view>
<view class="tag">{{ course.course.tagName }}</view>
<!-- 2. 讲师介绍 -->
<image class="teacher-avatar" :src="course.teacher.avatar"></image>
<text class="teacher-name">{{ course.teacher.name }}</text>
<text class="teacher-intro">{{ course.teacher.intro }}</text>
<!-- 3. 课程详情:富文本必须用 rich-text -->
<rich-text :nodes="course.course.description"></rich-text>
</view>
<!-- 4. 课程目录:折叠卡片 / 手风琴,accordion 保证同时只展开一个 -->
<uni-collapse accordion>
<uni-collapse-item
v-for="item in course.chapters"
:key="item.id"
:title="item.title"
>
<view
v-for="info in item.children"
:key="info.id"
class="section-item"
>
<text>{{ info.title }}</text>
<!-- 没买过才显示小锁 -->
<text v-if="!course.isBuy" class="lock">🔒</text>
</view>
</uni-collapse-item>
</uni-collapse>
<!-- 5. 学员评价 -->
<view class="comment-head">
<text>学员评价({{ commentTotal }})</text>
<text class="more">查看全部 ></text>
</view>
<view v-for="item in commentList" :key="item.id" class="comment-item">
<image class="avatar" :src="item.avatar"></image>
<text class="name">{{ item.nickName }}</text>
<text class="time">{{ formatTime(item.createTime) }}</text>
<text class="content">{{ item.content }}</text>
<uni-rate :value="item.score" readonly />
</view>
<!-- 6. 底部:收藏 + 购买状态 -->
<view class="bottom">
<text :class="['collect', { active: course.isCollection }]">
{{ course.isCollection ? '已收藏' : '未收藏' }}
</text>
<text v-if="course.isBuy" class="btn">去学习</text>
<text v-else class="btn">去支付</text>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import ajax from '@/api/course'
const courseId = ref<string | number>('')
const course = ref<any>({})
const commentList = ref<any[]>([])
const commentTotal = ref<number>(0)
onLoad((options: any) => {
// 先存 ID,再发请求,顺序不能反
courseId.value = options.courseId
getCourseDetail()
getComment()
})
// 获取课程详情
const getCourseDetail = async () => {
const res: any = await ajax.reqCourseDetail(courseId.value)
if (res.code === 200) {
course.value = res.data
}
}
// 获取学员评价:页码 1、每页 10、带课程 ID
const getComment = async () => {
const res: any = await ajax.reqComment({
page: 1,
limit: 10,
courseId: courseId.value
})
if (res.code === 200) {
commentTotal.value = res.data.total
commentList.value = res.data.items
}
}
// 时间戳格式化(moment / dayjs 均可)
const formatTime = (t: number) => {
const d = new Date(t)
return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`
}
</script>关键行与易错点
onLoad((options) => { courseId.value = options.courseId }):列表页跳转时拼的是?courseId=xxx,参数名必须和详情页接收名一致,否则拿到undefined,接口直接 400。- 模板外层加
v-if="course.course":数据没回来时course是空对象,直接写course.course.cover会报Cannot read property 'cover' of undefined。这就是转写里那个红色波浪线的来源,用v-if或可选链?.都能解决。 - 课程详情是富文本(带标签),必须用
<rich-text :nodes="...">,写成{{ }}插值会把标签当纯文本原样输出。 - 折叠面板用 uni-ui 的
uni-collapse+uni-collapse-item,accordion属性实现「手风琴」效果(同时只展开一个面板),不用自己写。 - 小锁
v-if="!course.isBuy":isBuy这个字段同时决定「去学习/去支付」按钮和章节列表的锁,一个字段两处用。 - 收藏高亮用
:class="['collect', { active: course.isCollection }]",靠isCollection布尔值切换样式。 uni-rate的:value需要 0–5 的数字,接口没给评分时要自己兜底,注意它支持半星(3.5)。v-for一定要写:key="item.id",且两层嵌套时内层变量名别和外层重名(外层item,内层就叫info),重名会取到外层数据。
常见错误
- 现象:登录页样式错乱或编译报错。原因:页面使用 less 语法但项目未安装 less 依赖。修法:安装 less 后重启/重新运行项目,确认构建通过再继续写业务。
- 现象:登录页或课程详情页访问 404/空白。原因:新建页面后未在 pages 中手动补路由。修法:补齐路径、页面标题与入口,跳转路径与 pages 配置逐字核对。
- 现象:未勾选协议仍能继续登录。原因:点击登录后未先判断协议状态,或判断后未
return。修法:先校验协议状态,不通过时提示并中断后续登录逻辑。 - 现象:登录成功但后续接口 401/拿不到用户信息。原因:token 未持久化,或请求拦截/请求头未带 token。修法:登录成功立即存储 token,统一在请求头注入,并用 Network 确认请求头。
- 现象:课程详情页报
Cannot read property ... of undefined或模板红波浪。原因:接口数据未返回时模板直接访问对象深层属性。修法:模板使用可选链/条件渲染,数据回来后再渲染关键区域。 - 现象:个人中心每次切换都重复请求用户信息。原因:
onShow中未判断已有用户信息。修法:有 token 且已有用户信息时跳过请求,仅缺失时拉取。 - 现象:课程详情/评论数据不对或为空。原因:路由跳转未带课程 ID,或请求参数漏传/拼错。修法:跳转时携带课程 ID,详情与评论接口都显式传参,联调时核对请求参数。
- 现象:富文本详情把标签原样显示。原因:使用插值渲染 HTML 字符串。修法:改用富文本渲染方式,并确认字段内容与渲染容器匹配。
练习
- 登录页协议与登录链路:实现协议勾选切换、未勾选提示、勾选后走登录并持久化 token。验收:未勾选不发起登录;成功后本地有 token;失败有明确提示。
- 个人中心登录态判断:进入个人中心时判断 token:有 token 拉取用户信息并展示头像昵称,无 token 跳登录页。验收:首次未登录会跳转;登录回来能展示;再次进入不重复请求用户信息。
- 课程详情路由传参:从首页课程卡片和课程列表两处进入详情页,均携带课程 ID。验收:详情页能拿到 ID;刷新/直接进入不丢参;详情接口按 ID 请求成功。
- 详情页空数据处理:在课程详情未返回前,封面、价格、购买人数、讲师信息不报错。验收:慢网/首屏不出现 undefined 报错;数据回来后正常渲染。
- 详情页底部与评价区:实现收藏/未收藏、去学习/去支付二态展示,并渲染评价总数与评价列表。验收:状态字段变化时按钮文案和样式同步;评价列表有数据可循环渲染,无数据不报错。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 登录授权模块的业务 | 40:29 |
| 2 | 课程详情页页面跳转 | 16:10 |
| 3 | 获取课程详情的数据并且渲染 | 36:15 |
| 4 | 课程详情页底部的渲染 | 13:47 |
| 5 | 课程详情页展示学员的评价 | 15:01 |
小结
- 登录业务的关键不是“能点”,而是协议校验、token 持久化、请求头携带、登录态回跳四件事闭环。
- 页面能否打开先查 pages 路由配置,再查跳转路径,最后查参数是否带到。
- 个人中心优先用
onShow做登录态判断,但必须加“已有用户信息则不再请求”的条件。 - 详情页渲染要默认接口可能慢/可能失败,模板访问深层字段前先做空值保护。
- 课程详情、评论列表都依赖课程 ID,路由传参和接口传参要前后一致。
- 收藏、购买、学习按钮本质是状态映射,先确认字段含义,再做二态/多态渲染。
- 联调时先看 Network 的请求方式、参数、请求头、响应结构,再改代码,避免凭感觉排错。