Skip to content

uniapp项目

学堂项目

配置tabbar

ts
创建首页页面 index
创建课程页面 course
创建我的页面 my
配置pages.json
	修改页面的标题,增加页面的路由,设置全局样式
	配置tabbar,配置页面,配置图标

配置全局样式:app.vue的style标签中 
/**全局的样式**/
page{
  width: 100%;
  height: 100%;
}

src/pages.json

json
{
	"pages": [
		{
			"path": "pages/index/index",
			"style": {
				"navigationBarTitleText": "首页"
			}
		},
		{
			"path": "pages/course/course",
			"style": {
				"navigationBarTitleText": "课程"
			}
		},
		{
			"path": "pages/my/my",
			"style": {
				"navigationBarTitleText": "个人中心"
			}
		}
	],
	"globalStyle": {
		"navigationBarTextStyle": "black",
		"navigationBarTitleText": "雨落辰潇",
		"navigationBarBackgroundColor": "#3cc",
		"backgroundColor": "#F8F8F8"
	},
	"tabBar": {
		"list": [
			{
				"pagePath": "pages/index/index",
				"text": "首页",
				"iconPath":"/static/images/tabbar/home-off.png",
				"selectedIconPath":"/static/images/tabbar/home-on.png"
			},
			{
				"pagePath": "pages/my/my",
				"text": "个人中心",
				"iconPath":"/static/images/tabbar/home-off.png",
				"selectedIconPath":"/static/images/tabbar/home-on.png"
			},
			{
				"pagePath": "pages/course/course",
				"text": "课程中心",
				"iconPath":"/static/images/tabbar/home-off.png",
				"selectedIconPath":"/static/images/tabbar/home-on.png"
			}
		]
	}
	
}

配置全局样式

src/App.vue

vue
<script setup lang="ts">
import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
onLaunch(() => {
  console.log("App Launch");
});
onShow(() => {
  console.log("App Show");
});
onHide(() => {
  console.log("App Hide");
});
</script>
<style>
/**全局的样式**/
page{
  width: 100%;
  height: 100%;
}
</style>

首页页面

静态搭建

ts
顶部轮播图:使用uni中的 swiper组件

渲染分类数据

使用card组件,并传参,使用插槽,并在插槽处传递一个 item组件,在item组件上传递prop
	list:列表数据
    type:标记区分是 课程 还是 老师

mock假数据

ts
使用mock创建后端假数据
src/mock/index.ts
里边是ts代码,没有通过服务的形式运行,是通过js代码的方式运行的
import { bannerCateList, hotCateList } from "@/mock/index";

easycom规范

组件中的 文件夹名字 和 组件名字 一致时,直接使用(符合uniapp easycom规范)
uniapp 规范 组件-easycom规范 文档中有规范

封装card组件

ts
热门课程封装为一个组件(组件和页面的区别,组件没有路由,页面有路由)
src/components/
组件中的 文件夹名字 和 组件名字 一致时,直接使用(符合uniapp easycom规范)
uniapp 规范 组件-easycom规范 文档中有规范


搭建v-card组件静态 src/components/v-card/v-card.vue
v-card组件样式配置
	box-sizing:border-box(怪异模型)

v-card设置插槽,把课程内容或老师内容,通过父组件使用插槽显示

src/components/v-card/v-card.vue

vue
<template>
    <view class="card">
        <!-- 卡片顶部的标题 -->
        <view class="header">
            <text class="left">{{ title }}</text>
            <text class="right">{{ subtitle }}</text>
        </view>
        <!-- 插槽展示课程的地方 -->
        <slot name="content"></slot>
    </view>
</template>

<script setup lang='ts'>
// 接收父组件传来的 props
const props = defineProps(['title','subtitle'])
</script>

<style scoped lang="less">
.card{
    width: 100%;
    padding: 20rpx;
    box-sizing: border-box;
}
.card .header{
    display: flex;
    justify-content: space-between;
    background: #3cc;
    color: white;
    border-radius: 10rpx;
    font-size: 30rpx;
    height: 80rpx;
    line-height: 80rpx;
}
</style>

封装item组件

ts
搭建v-item组件静态:用于 父组件填充card组件插槽,展示老师和课程内容
样式配置
	white-space:nowrap 不换行 
    overflow:hidden 超出范围隐藏
    text-overflow:ellipsis 超出范围显示...
    font-size: 34rpx设置字体大小
    
通过 defineProps获取父组件传递的 props, 
    list 数据列表
    type 标记是"课程"还是"老师"
如果 type是"课程" 展示课程内容,
如果 type是"老师" 真实老师内容
vue
<template>
    <view class="box">
        <view class="item" v-for="(item, index) in list" :key="item.id"">
            <image class="img" :src="type == 'course' ? item.cover : item.avatar" mode="widthFix" />
            <!-- 课程的结构 -->
            <view v-if="type == 'course'">
                <view class="title">{{ item.title }}</view>
                <view class="study">已有{{ item.viewCount }}学习</view>
                <view class="footer">
                    <view class="price">¥{{ item.price }}</view>
                    <view class="buy">已有{{ item.buyCount }}购买</view>
                </view>
            </view>
            <!-- 老师的结构-->
            <view v-else>
                <view class="info">{{ item.intro }}</view>
                <view class="name">{{ item.name }}</view>
            </view>
        </view>
    </view>
</template>

<script setup lang='ts'>
// 接收父组件传递的 列表数据
const props = defineProps(["list", "type"]);
</script>

<style scoped lang="less">
.box {
    width: 100%;
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
}

.box .item {
    width: 340rpx;
    margin: 20rpx 0rpx;
}

.box .item .img {
    width: 100%;
}

.box .item .title {
    font-weight: 900;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: 34rpx;
}

.box .item .study {
    font-size: 24rpx;
    margin: 10rpx 0rpx;
}

.box .item .footer {
    display: flex;
    justify-content: space-between;
    font-size: 24rpx;
}

.box .item .footer .price {
    color: red;
}

.box .item .info {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    overflow: hidden;
    text-overflow: ellipsis;
    -webkit-box-orient: vertical;
    font-size: 24rpx;
}

.box .item .name {
    color: #3cc;
    font-size: 36rpx;
    font-weight: 900;
    margin-top: 10rpx;
}
</style>

封装v-top组件

vue
<template>
    <image class="btn" src="https://cdn-cos-ke.myoed.com/ke_proj/mobilev2/m-core/03de0936a7aafee76646b8b2d5fa5b4f.png"
        mode="scaleToFill" @click="toTop" />
</template>

<script setup lang="ts">

const toTop = () => {
    //点击图片页面滚动到顶部
    uni.pageScrollTo({
        scrollTop: 0,
        duration: 200,
    })
}
</script>

<style scoped>
.btn {
    position: fixed;
    right: 0rpx;
    bottom: 100rpx;
    width: 100rpx;
    height: 100rpx;
}
</style>

封装网络请求

网络请求二次封装 通过类的方式

加载组件

ts
显示 loading 提示框, 需主动调用 uni.hideLoading 才能关闭提示框
uni.showLoading({
    title: '加载....'
})

网络请求组件

uni-app-API-网络

ts
发起网络请求
uni.request({
	
})

获取token

ts
uni.getStorageSync('TOKEN');
从本地缓存中同步获取指定 key 对应的内容。

utils/service.ts

ts
class Servive{
    // 原型对象的方法
    API(params:any):any{
        // 定义全局基础路由
        baseUrl:
        // 开启加载效果
        
        // 返回一个promise对象
        return new Promise(()=>{
            // 发送request请求
            uni.request({
                // 地址
                url:baseUrl + params.url,
                // 方法
                method:params.method || 'GET',
                // 请求体
                data:params.data || {},
                // 成功回调
                success(res){
                	// 执行成功的回调
            	},
                // 失败回调
                fail(){
                    // 执行失败的回调
                },
                // 都会执行的回调
                complete(){
                    // 关闭加载效果
                    uni.hideLoading
                }
            })
        })
    }
    // get请求方法
    get(params:any){
        params.method = "GET"
        return this.API(params)
    }
    // post请求方法
    get(params:any){
        params.method = "post"
        return this.API(params)
    }
    // delete 请求方法
    get(params:any){
        params.method = "delete"
        return this.API(params)
    }
}
// 暴漏数据
export default Servive
ts
//ES6类
class Service {
    //原型对象的方法
    API(params: any): any {
        // 定义全局基础路由
        const baseURL = 'https://gmall-prod.atguigu.cn/skb';
        //加载效果开启
        uni.showLoading({
            title: '加载中....'
        })

        //判断用户是否登录:如有token携带
        const token = uni.getStorageSync('TOKEN');
        // 定义请求头
        var header: any = {};
        // 如果token存在,则携带请求头
        if (token) {
            header.token = token;
        }

        // 返回一个promise对象
        return new Promise((resolve, reject) => {
            //uni-app 提供的API进行网络发请求
            uni.request({
                url: baseURL + params.url,// 请求的地址
                method: params.method || 'GET',// 请求方式
                data: params.data || {},// 携带的参数
                header, // 请求头
                success(res) {// 请求成功的回调
                    resolve(res.data); // 返回一个成功的Promise对象,同时简化数据
                },
                fail(error) { // 请求失败的回调
                    reject(error);
                },
                complete() { // 成功失败都执行
                    uni.hideLoading(); // 隐藏加载动画
                }
            })

        })
    }
    // get请求方法
    get(params: any) {
        params.method = "GET";
        return this.API(params);
    }
    // post请求方法
    post(params: any) {
        params.method = "POST";
        return this.API(params);
    }
    // delete 请求方法
    delete(params: any) {
        params.method = "DELETE";
        return this.API(params);
    }
}
// 暴漏数据
export default Service;

封装获取首页api

ts
src/api/index.ts
// 引入 Service 
import Service from ''
// 类的继承:Request的实例可以使用Service的方法
class Request extends Service{
    // 获取首页数据
    reqHome(){
        return this.get({
            url:'',
            
        })
    }
}
// 暴漏Request类的实例
export default new Request
ts
// 引入请求需要的类
import Service from "@/utils/service";
// 类的继承
class Request extends Service {
    //获取首页老师和课程的数据
    reqEdu() {
        return this.get({ url: '/api/edu/index' })
    }
    //获取课程接口
    reqCouse(page: number, limit: number) {
        return this.get({ url: `/api/edu/course/${page}/${limit}` })
    }
}
// 暴露Request类的实例
export default new Request;

获取并渲染首页数据

ts
引入uniapp的页面生命周期函数
import {onload} from "@dcloudio/uni-app"
import req from "@/api/index"
onLoad(()=>{
    // 获取首页的数据,发送请求获取首页数据
    getHomeData()
})

// 定义一个 获取首页的 函数
const async getHomeData(){
    // 发送请求
    const result = await req.reqHome()
}

// 存储课程数据 存储老师数据
courseList
teachList

// 渲染课程数据

首页页面给v-item组件传参 通过 props传参
v-item组件接收数据,渲染数据

同一个组件,但是渲染的数据结构不一样
	通过设置一个 type="teacher" 标识,然后通过type的标识结合 v-if 展示相应的内容
    通过设置一个 type="course" 标识,然后通过type的标识结合 v-if 展示相应的内容
    相同的结构,但是数据的结构不一样:可以通过 三元表达式 :src="type==='course' ? '':''"
ts
首页触底 显示 回到顶部
封装置顶组件 v-back组件
搭建静态页面
设置静态样式
	设置定位:固定定位,固定定位位置
监听页面滚动的钩子事件

定义响应式数据控制 置顶组件的显示与隐藏 const back = ref(true)
// 父组件控制子组件的显示与隐藏
uni-app-教程-页面
	onLoad(){
        
    }
	onPageScroll(event){
        console.log(event)
        // 如果滚动的距离大于200像素
        if(event.scrollTop>200){
            // 显示
            back.value = true
        }else{
            // 隐藏
            back.value = false
        }
    }

// 子组件回到顶部的事件
uni-app-教程-
    const toTop = ()=>{
        // 调用uni API
        uni.pageScrollTo({
            // 滚动的位置
            scrollTop:0
            // 持续的事件
            duration:1000
        })
    }
vue
<template>
	<view class="home">
		<!-- 顶部的轮播 -->
		<swiper class="swiper" indicator-dots autoplay circular>
			<swiper-item v-for="(item, index) in bannerList" :key="index">
				<image class="img" :src="item.src" mode="scaleToFill" />
			</swiper-item>
		</swiper>
		<!-- 学习课程分类 -->
		<view class="study">
			<view class="item" v-for="(item, index) in categoryList" :key="index">
				<image class="img" :src="item.src" mode="scaleToFill" />
				<text class="title">{{ item.name }}</text>
			</view>
		</view>

		<!-- 热门课程组件 -->
		<!-- 传递 title和subtitle -->
		<v-card title="热门课程" subtitle="全部课程>">
			<!-- 使用 具名插槽 -->
			<template #content>
				<v-item :list="courseArr" type="course"></v-item>
			</template>
		</v-card>

		<!-- 展示老师的地方 -->
		<v-card title="名师大咖" subtitle="全部名师>">
			<template #content>
				<v-item :list="teacherArr" type="teacher"></v-item>
			</template>
    	</v-card>

		<!-- 回到顶部 -->
		 <!-- 底部指定的组件 -->
		 <v-top v-show="back"></v-top>
	</view>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { bannerCateList, hotCateList } from "@/mock/index";
//页面生命周期函数
import { onLoad, onPageScroll } from "@dcloudio/uni-app";
import ajax from "@/api/index.ts";


// 首页轮播图
const bannerList = ref(bannerCateList);
const categoryList = ref(hotCateList);
// 存储课程的数据
const courseArr = ref([]);
// 老师的数据
const teacherArr = ref([]);
// 回到顶部组件状态
const back = ref(false);

const getHomeData = async () => {
	//获取首页的数据
	const result = await ajax.reqEdu();
	if (result.code == 200) {
		courseArr.value = result.data.courseList;
		teacherArr.value = result.data.teacherList;
	}
};

onLoad(() => {
	//获取首页的数据(课程与老师的数据)
	getHomeData();
});
//监听页面的滚动
onPageScroll((event) => {
	if (event.scrollTop >= 200) {
		back.value = true;
	} else {
		back.value = false;
	}
})


</script>

<style scoped lang="less">
.home {
	width: 100%;
}

.home .swiper {
	width: 100%;
	height: 300rpx;
}

.home .swiper .img {
	width: 100%;
	height: 100%;
}

.home .study {
	display: flex;
	padding: 20rpx;
}

.home .study .item {
	flex: 1;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
}

.home .study .item .img {
	width: 72rpx;
	height: 72rpx;
}

.home .study .item .title {
	color: hotpink;
}
</style>

点击课程跳转到课程详情

ts
<view class="item" v-for="(item, index) in list" :key="item.id" @click="goCourseDetial(item.id)">
    
//点击课程进入课程详情页
const goCourseDetial = (id: string) => {
    if (props.type == "course") {
        //跳转课程详情页-->携带id
        uni.navigateTo({
            url: `/pages/course/detail/detail?courseId=${id}`,
        })
    }
}

课程页面

静态搭建

ts
在课程静态页面 src/pages/course/index.vue
导入二次封装的 api接口
import ajax from '@/api/index'
导入 onLoad 生命周期函数
import {onLoad} from '@dcloudio/uni-app'
定义响应式数据
const page = ref()
const limit = ref()
onLoad(){
    
}
定义获取课程函数
const getCourse = async ()=>{
    const res = await ajax.reqCourse(page.value,limit.value)
    console.log(res)
    如果 res.data.items.length === 10 存在下一次
    	
    否则 没有更多了
    把 当前课程列表 拼接上新的课程列表
}
定义响应式状态 下拉状态 
const status = ref("")
渲染 课程列表 数据

监听 页面触底 
onReachbottom(){
    如果 status 的值为more时,发送请求
    如果 status 的值为nomore时,不发送请求
}

当 status的 数据状态为 nomore 时,使用v-if显示 没有更多

显示更多静态搭建 定义一个 view 标签
	设置 宽度,设置高度为2rpx,设置背景颜色为red
vue
<template>
	<view class="course">
		<view class="item" v-for="item in courseArr" :key="item.id">
			<view class="left">
				<image class="img" :src="item.cover" mode="widthFix" />
			</view>
			<view class="right">
				<view class="title">
					<uni-icons type="fire" size="16" color="red"></uni-icons>{{ item.title }}
				</view>
				<view class="bottom">
					<view class="price">¥{{ item.price }}</view>
					<view class="buy">已有{{ item.buyCount }}人购买</view>
				</view>
			</view>
		</view>
		<view class="status" v-if="status == 'nomore'">
			<view class="line"></view>
			<view class="text">没有更多</view>
			<view class="line"></view>
		</view>
	</view>
</template>

<script setup lang="ts">
import ajax from "../../api/index"
import { ref } from "vue"
import { onLoad, onReachBottom } from "@dcloudio/uni-app"
//默认页码
const page = ref(1);
//默认一页几条数据
const limit = ref(10);
//存储课程的数据
const courseArr: any = ref([]);
//存储下拉状态
const status = ref("");

//获取课程的方法
const getCourse = async () => {
	const result = await ajax.reqCouse(page.value, limit.value);
	// 先判断触底是否有下一次请求
	if (result.data.items.length == 10) {
		status.value = "more";
	} else {
		status.value = "nomore";
	}

	courseArr.value.push(...result.data.items);
};

//页面加载完毕
onLoad(() => {
	getCourse();
})

// 监听到底函数
onReachBottom(() => {
	if (status.value == "more") {
		page.value++
		getCourse()
	}
})

</script>

<style scoped>
.course {
	background: #ccc;
	width: 100%;
}

.course .item {
	background: white;
	margin: 10rpx 0rpx;
	display: flex;
	padding: 20rpx;
}

.course .item .img {
	width: 220rpx;
}

.course .item .right {
	margin-left: 20rpx;
	flex: 1;
	display: flex;
	flex-direction: column;
	justify-content: space-between;
}

.course .item .right .title {
	font-weight: 900;
}

.course .bottom {
	display: flex;
	justify-content: space-between;
}

.course .bottom .price {
	color: red;
}

.status {
	color: red;
	font-weight: 900;
	text-align: center;
	display: flex;
	justify-content: center;
	align-items: center;
}

.status .line {
	width: 200rpx;
	height: 2rpx;
	background: red;
}
</style>

uni组件库

ts
uniapp 中使用UI组件库
	uniapp官网-组件-推展组件
	在 标题 旁边设置图标
    npm安装uniapp组件库
    配置vue.config.js
	安装sass
    安装sass-loader
	安装uni-ui
	配置easycom
    使用uniapp中的组件
    <uni-icon></uni-icon>
	设置图标的类型和颜色
ts
<view class="title">
    <uni-icons type="fire" size="16" color="red"></uni-icons>{{ item.title }}
</view>

触底重发请求

ts
// 监听到底函数
onReachBottom(() => {
	if (status.value == "more") {
		page.value++
		getCourse()
	}
})

跳转到课程详情

ts
<view class="item" v-for="item in courseArr" :key="item.id" @click="goCourseDetial(item.id)">
    
//点击课程进入课程详情页
const goCourseDetial = (id: string) => {
	uni.navigateTo({
		url: `/pages/course/detail/detail?courseId=${id}`,
	})
}

课程详情页面

配置路由

ts
{
    "path": "pages/course/detail/detail",
    "style": {
        "navigationBarTitleText": "课程详情"
    }
}

静态骨架

ts

首页和课程列表跳转

ts
在 v-item组件上 绑定单击事件 
const goCourseDetail = ()=>{
    if(props.type == 'course'){
        跳转到课程详情页面 带参数 [id]
    }
}
在 课程列表上 绑定单击事件 
const goCourseDetail = (id)=>{
    跳转到课程详情页面 带参数 [id]
}

封装课程详情和评价接口

ts
定义 course 存储课程详情对象
定义 获取课程详情 的函数
const getCourse = (courseId) =>{
    发送 获取课程详情 请求
    获取数据,存储数据
}
ts
// 引入请求需要的类
import Service from "@/utils/service";
// 类的继承
class Request extends Service {
    //获取首页老师和课程的数据
    reqEdu() {
        return this.get({ url: '/api/edu/index' })
    }
    //获取课程接口
    reqCouse(page: number, limit: number) {
        return this.get({ url: `/api/edu/course/${page}/${limit}` })
    }
    // 获取某一个课程详情
    reqCourseDetail(id: string) {
        return this.get({ url: `/api/edu/course/${id}` })
    }
    //获取某一个课程的评价
    reqComment(page: number, limit: number, courseId: string) {
        return this.get({ url: `/api/edu/comment/${page}/${limit}?courseId=${courseId}` })
    }
}
// 暴露Request类的实例
export default new Request;

获取课程详情和评价数据

ts
定义 courseId 存储课程ID
课程详情页面 在 onLoad(){} 钩子中获取 课程详情id
onLoad(option){
    console.log(option)
    // 存储课程id
    courseId = option.id
    调用 获取课程详情 函数
}
ts
// 获取课程详情函数
const getCourseDetail = async () => {
    const result = await ajax.reqCourseDetail(courseId.value);
    if (result.code == 200) {
        course.value = result.data;
    }
    console.log(course.value)
}
//获取学员的评价
const getComment = async () => {
    const result = await ajax.reqComment(1, 10, courseId.value);
    if (result.code == 200) {
        total.value = result.data.total;
        recommentArr.value = result.data.items;
    }
}
onLoad((options) => {
    //存储用户点击的课程的ID
    courseId.value = options.courseId
    //获取课程详情
    getCourseDetail()
    //获取学员对于某一个课程的评价
    getComment()
})

渲染数据

ts

isCollect 控制是否收藏

课程章节字段静态渲染
    使用uniapp-组件中的 uni-collapse 手风琴效果 开启手风琴效果
    通过isbuy 控制 锁图标的显示与隐藏

回到顶部

ts
定义 isShowBack 控制置顶按钮显示与隐藏
ts
<!-- 回到顶部 -->
<v-top v-show="isShowBack"></v-top>
//控制课程详情页v-back的显示与隐藏
const isShowBack = ref(false);
//监听课程详情页的滚动
onPageScroll((event) => {
    if (event.scrollTop >= 200) {
        isShowBack.value = true;
    } else {
        isShowBack.value = false;
    }
})

底部按钮

样式的条件渲染

购买/学习按钮和收藏

ts
课程详情页面底部的渲染
收藏渲染和去学习渲染 使用三元表达式控制显示与隐藏

通过 && 表达式控制样式的条件渲染
<view :class="['bg', course.isCollect && 'active']"></view>


收藏和取消收藏功能

isCollect 字段控制未收藏和已收藏的功能
在按钮上绑定单击事件 @click="handle"

封装 课程收藏 的接口api 参数 [课程id,token]
封装 课程取消收藏 的接口api 参数 [课程id,token]

const handle = () =>{
    // 拿到课程的状态,如果是真,点击后变为假,发送取消收藏的请求,提示取消收藏
    // 拿到课程的状态,如果是假,点击后变为真,发送收藏的请求,提示收藏
    console.log(course.value.isCollect)
    再次获取课程数据
    把课程收藏取反(仅前端显示)
}

封装收藏接口

ts
//收藏
reqSave(courseId:string){
    return this.post({url:`/api/edu/courseCollect/auth/save/${courseId}`})
}

//取消收藏
reqDeleteSave(id:string){
    return this.delete({url:`/api/edu/courseCollect/auth/remove/${id}`})
}

切换收藏状态

vue
<view class="bottom_tabbar">
    <view class="bottom_wrap">
        <view class="bottom_button">
            <!-- 收藏 -->
            <view class="favo_button" @click="changeCollect">
                <view :class="['bg', course.isCollect && 'active']"></view>
                {{ course.isCollect ? "已收藏" : "未收藏" }}
            </view>
        </view>
        <view class="bottom_main">
            <view class="buy_button" @click="goBuyOrLearn">
                {{ course.isBuy ? "去学习" : "去购买" }}
            </view>
        </view>
    </view>
</view>
ts
// 收藏回调函数
const changeCollect = async() =>{
    // 如果已经收藏
    if (course.value.isCollect) {
        await ajax.reqDeleteSave(courseId.value)
        //提示
        uni.showToast({ title: "取消收藏" });
    }else {
        //添加收藏
        await ajax.reqSave(courseId.value);
        uni.showToast({ title: "收藏成功" });
    }
    //第一种做法:再一次获取课程的数据
    //第二种:把课程的收集的字段取反
    course.value.isCollect = !course.value.isCollect;
}

跳转到课程评价页面

在 更多评价 的navigate标签上 设置url跳转到对应页面,通过query拼接参数

vue
<view class="title">
    <view class="title_left"> 学员评价({{ total }})</view>
    <navigator
        :url="`/pages/course/comments/index?courseId=${courseId}&teacherId=${course.course?.teacherId}`"
        class="title_right">
        查看全部<uni-icons type="right" color="#666c80" size="12"></uni-icons>
    </navigator>
</view>

课程评价页面

封装评价相关接口

ts
//获取某一个课程的评价
reqComment(page: number, limit: number, courseId: string) {
    return this.get({ url: `/api/edu/comment/${page}/${limit}?courseId=${courseId}` })
}

//添加评论
reqAddComment(data:any){
      return this.post({url:`/api/edu/comment/auth/save`,data});
}

新建评价页面和配置路由

ts
{
    "path": "pages/course/comments/comments",
    "style": {
        "navigationBarTitleText": "学员评价"
    }
}

展示评价

获取课程id

ts
onLoad((options) => {
    //课程的ID
    courseId.value = options.courseId;
    //存储老师的ID
    teacherId.value = options.teacherId;
    console.log(courseId.value)
});

获取评价数据

ts
//当前默认页码
const page = ref(1);
//每一页展示学员评价个数
const limit = ref(10);
const teacherId = ref("")
const courseId = ref("")
//收集学员评价的内容
const commentsList = ref([])
//相应数据判断是否有发下一次请求,触底重新发送请求
const status = ref("");
//收集学员评价的内容
const content = ref("");

onLoad((options) => {
    //课程的ID
    courseId.value = options.courseId;
    //存储老师的ID
    teacherId.value = options.teacherId;
    // 获取全部评价
    getCommentsById()
})

//获取学员的评价
const getCommentsById = async () => {
    const result = await ajax.reqComment(page.value, limit.value, courseId.value);
    if (result.data.items.length < 10) {
        status.value = "nomore";
    } else {
        status.value = "more";
    }
    //存储学员评价
    commentsList.value.push(...result.data.items);
};

渲染数据

vue
<template>
    <view class="container comment">
        <!-- 学员的更多的评价 -->
        <view class="comment_wrapper">
            <view class="comment_list">
                <!-- 每一个结构代表的是一个学员评价 -->
                <view class="comment_item" v-for="(item, index) in commentsList" :key="item.id">
                    <view class="logo">
                        <image :src="item.avatar" />
                    </view>
                    <view class="content_wrap">
                        <view class="name">{{ item.nickname }}</view>
                        <veiw class="date">
                            {{ item.gmtCreate }}
                            <uni-rate :touchable="false" :value="3.5" size="12" />
                        </veiw>
                        <view class="content">{{ item.content }}</view>
                    </view>
                </view>
            </view>
        </view>
        <!-- 新增评论 -->
        <view class="comment_bar">
            <input class="input_box" placeholder="请点击输入" cursor-spacing="10" v-model="content" />
            <uni-icons class="input_icon" type="paperplane-filled" size="20" @click="addComments" />
        </view>
        <!-- 没有更多 -->
        <view v-if="status == 'nomore'" class="more">----没有更多----</view>
    </view>
</template>

添加评价

ts
渲染评价列表
使用 uni-rate 组件 实现评分的功能
ts
// 新增评价
const addComments = async () => {
    // 先判断是否是空内容
    if (content.value.trim() == "") {
        uni.showToast({ title: "评论内容不能为空", icon: "error" });
        return;
    }
    // 发送 添加评价请求
    const result = await ajax.reqAddComment({
        courseId: courseId.value,
        teacherId: teacherId.value,
        content: content.value,
    })
    // 再次获取更新后全部评价
    if (result.code == 200) {
        // 清空评价列表
        commentsList.value = [];
        page.value = 1;
        // 清空评论框内容
        content.value = '';
        getCommentsById();
    }
}

删除评价

ts

触底重新发送请求

ts
监听触底事件 
onReachBottom(){
	如果状态为更多,则再次发送请求
}
ts
onReachBottom(() => {
    //如果状态为更多在发请求
    if (status.value == "more") {
        page.value++;
        getCommentsById()
    }
})

完整代码

vue
<template>
    <view class="container comment">
        <!-- 学员的更多的评价 -->
        <view class="comment_wrapper">
            <view class="comment_list">
                <!-- 每一个结构代表的是一个学员评价 -->
                <view class="comment_item" v-for="(item, index) in commentsList" :key="item.id">
                    <view class="logo">
                        <image :src="item.avatar" />
                    </view>
                    <view class="content_wrap">
                        <view class="name">{{ item.nickname }}</view>
                        <veiw class="date">
                            {{ item.gmtCreate }}
                            <uni-rate :touchable="false" :value="3.5" size="12" />
                        </veiw>
                        <view class="content">{{ item.content }}</view>
                    </view>
                </view>
            </view>
        </view>
        <!-- 新增评论 -->
        <view class="comment_bar">
            <input class="input_box" placeholder="请点击输入" cursor-spacing="10" v-model="content" />
            <uni-icons class="input_icon" type="paperplane-filled" size="20" @click="addComments" />
        </view>
        <!-- 没有更多 -->
        <view v-if="status == 'nomore'" class="more">----没有更多----</view>
    </view>
</template>

<script setup lang='ts'>
import { ref } from 'vue'
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
import ajax from "@/api/index";
//当前默认页码
const page = ref(1);
//每一页展示学员评价个数
const limit = ref(10);
const teacherId = ref("")
const courseId = ref("")
//收集学员评价的内容
const commentsList = ref([])
//相应数据判断是否有发下一次请求,触底重新发送请求
const status = ref("");
//收集学员评价的内容
const content = ref("");

onLoad((options) => {
    //课程的ID
    courseId.value = options.courseId;
    //存储老师的ID
    teacherId.value = options.teacherId;
    // 获取全部评价
    getCommentsById()
})

//获取学员的评价
const getCommentsById = async () => {
    const result = await ajax.reqComment(page.value, limit.value, courseId.value);
    if (result.data.items.length < 10) {
        status.value = "nomore";
    } else {
        status.value = "more";
    }
    //存储学员评价
    commentsList.value.push(...result.data.items);
};

// 新增评价
const addComments = async () => {
    // 先判断是否是空内容
    if (content.value.trim() == "") {
        uni.showToast({ title: "评论内容不能为空", icon: "error" });
        return;
    }
    // 发送 添加评价请求
    const result = await ajax.reqAddComment({
        courseId: courseId.value,
        teacherId: teacherId.value,
        content: content.value,
    })
    // 再次获取更新后全部评价
    if (result.code == 200) {
        // 清空评价列表
        commentsList.value = [];
        page.value = 1;
        // 清空评论框内容
        content.value = '';
        getCommentsById();
    }
}
onReachBottom(() => {
    //如果状态为更多在发请求
    if (status.value == "more") {
        page.value++;
        getCommentsById()
    }
})

</script>

<style lang="less" scoped>
.comment_list {
    .comment_item {
        display: flex;
        padding: 15px 0;
        border-bottom: 0.07143rem solid #ececec;

        .logo {
            border-radius: 50%;
            width: 32px;
            height: 32px;
            margin-right: 8px;

            image {
                border-radius: 50%;
                height: 100%;
                width: 100%;
            }
        }

        .content_wrap {
            font-size: 12px;
            color: #000;
            flex: 1;

            .date {
                color: #a1a5b2;
                display: flex;
                justify-content: space-between;
                align-items: center;
            }

            .content {
                margin-top: 8px;
            }
        }
    }
}

.comment {
    background: white;
    padding: 16px 16px 60px 16px;

    .comment_bar {
        position: fixed;
        bottom: 0;
        left: 0;
        width: 100%;
        z-index: 999;
        height: 60px;
        display: flex;
        align-items: center;
        justify-content: center;
        background: linear-gradient(180deg,
                hsla(0, 0%, 86.7%, 0) 0,
                hsla(0, 0%, 86.7%, 0.2) 0.42857rem,
                hsla(0, 0%, 86.7%, 0.2) 0.57143rem,
                #fff 0.64286rem,
                #fff);

        .input_box {
            width: 80%;
            height: 30px;
            line-height: 30px;
            background-color: #f2f3f7;
            border-radius: 20px;
            // text-indent: 10px;
            padding-left: 10px;
            font-size: 16px;
        }

        .input_icon {
            margin-left: 8px;
        }
    }
}

.more {
    text-align: center;
    color: yellowgreen;
}
</style>

个人中心页面

ts
个人中心页面静态渲染,样式搭建
在个人中心页面的 onShow(){} 钩子中 判断是否存在 token
token不存在,则跳转到 登录页,token存在则获取用户信息数据,展示个人中心页面

封装 获取用户信息 接口api 参数[token]

定义 获取用户信息 函数
	如果存在token则发送 
    	如果用户信息不存在 userInfo.value.nickname存在再发请求
            获取用户信息数据 请求
            存储用户数据信息 userInfo 
    
在 utils/service.ts 中
	判断本地储存中是否存在token,
        如果有,则在header中添加token
渲染个人中心数据,如果有数据则展示,没有数据则显示默认,通过 &&表达式

封装获取个人信息接口

ts
//根据token获取用户信息
reqUserInfo() {
    return this.get({ url: `/api/ucenter/member/auth/getLoginInfo` });
}

配置路由和新建页面

vue
<template>
	<view class="container user">
		<view class="info dark-mode">
			<h1 class="info_title">
				<image class="info_title_logo" :src="userInfo.avatar ? userInfo.avatar : '/static/logo.png'" />
				<text class="info_title_name">{{
					userInfo.nickname ? userInfo.nickname : "请登录"
				}}</text>
			</h1>
			<view class="info_list">
				<view class="info_item">
					<navigator class="item_a info-link" url="/pages/order/list/index">
						<uni-icons type="shop-filled" size="24" color="#333"></uni-icons>
						<text class="item_dsc">我的订单</text>
					</navigator>
				</view>
				<view class="info_item">
					<navigator class="item_a info-link" url="/pages/course/collect/index">
						<uni-icons type="vip-filled" size="24" color="#333"></uni-icons>
						<text class="item_dsc">我的收藏</text>
					</navigator>
				</view>
			</view>
		</view>
		<view class="list"></view>
		<view class="logout dark-mode info-link">退出登陆</view>
	</view>
</template>

<script lang="ts" setup>
import ajax from "@/api/index";
import { onShow } from "@dcloudio/uni-app";
import { ref } from "vue";
//存储用户信息
const userInfo = ref<any>({});

// 获取个人信息
const getUserInfo = async () => {
	// 判断是否存在token
	const token = uni.getStorageSync("TOKEN");
	if (token) {
		// 获取用户信息:头像昵称,前提没有发一次,有了用户信息不需要再发请求获取用户信息
		if (!userInfo.value.nickname) {
			const result = await ajax.reqUserInfo();
			if (result.code == 200) {
				userInfo.value = result.data.item;
			}
		}
	} else { // token不存在
		uni.navigateTo({
			url: "/pages/login/login",
		});
	}
};

// 生命周期 onshow触发
onShow(() => {
	getUserInfo()
})

</script>

<style lang="less" scoped>
.user {
	height: 100vh;
	background: #eee;
}

.info {
	background: #00cc99;

	&_title {
		height: 97px;
		padding: 0 24px;
		display: flex;
		align-items: center;
		border-bottom: 1rpx solid #eee;
		border-top: 1rpx solid #eee;

		&_logo {
			height: 60px;
			width: 60px;
			border-radius: 50%;
			background: #fff;
		}

		&_name {
			margin-left: 10px;
			color: #333;
			font-size: 20px;
			flex: 1;
			overflow: hidden;
			white-space: nowrap;
			text-overflow: ellipsis;
			word-wrap: normal;
		}
	}

	&_list {
		display: flex;

		.info_item {
			width: 50%;
			text-align: center;

			.item_a {
				display: flex;
				flex-direction: column;
				align-items: center;
				padding: 24px 0;
			}

			.item_dsc {
				font-size: 14px;
				margin-top: 8px;
			}
		}
	}
}

.logout {
	height: 52px;
	line-height: 52px;
	text-align: center;
	margin-top: 10px;
	background: #00cc99;
}

.popup_height {
	height: 100px;
	width: 100%;
	background: #fff;
}
</style>

登陆页面

封装登陆接口

ts
// 根据临时凭证code获取token
reqToken(code: string) {
    return this.get({ url: `/api/ucenter/webChat/callback?code=${code}` });
}

配置登陆路由

ts

定义响应式数据 复选框勾选状态 
const agree = ref(false)
在 用户协议 复选框处 绑定单击事件

定义 登陆按钮的 回调
const login = ()=>{
    判断是否勾选了 用户协议
    如果没有 勾选
    	使用 uni.showToast() 提示消息
    如果勾选了 用户协议
		使用 uni.login() 获取code
        uni.login({
            success(res){
                // 获取code
                getToken(res.code)
            }
        })
}

封装用户登陆的接口api
reqToken(code){
    return 
}

定义 获取用户身份token
const getToken = async (code) =>{
    发送请求
    如果 响应成功
    把token存储到本地
    返回 上一页
    uni.
}

新建登陆页面

vue
<template>
    <view class="container login_container">
        <!-- 登录页面顶部 -->
        <view class="login_content">
            <view class="login_logo"></view>
            <view class="login_main_type">
                <button class="login_main_type_item" @click="login">
                    <text>微信登陆</text>
                </button>
            </view>
        </view>
        <!-- 登录底部:带有协议 -->
        <view class="login_footer">
            <view class="login_protocol">
                <view :class="['login_check_icon', agree && 'checked']" @click="agree = !agree"></view>
                <view class="login_block">
                    我已阅读并同意
                    <navigator class="link">用户协议</navigator>

                    <navigator class="link">隐私声明</navigator>
                </view>
            </view>
        </view>
    </view>
</template>

安装less

ts
npm install less

发送请求

ts
<script setup lang='ts'>
import ajax from "@/api/index";
import { ref } from "vue";
// 控制复选框的勾选状态
const agree = ref(false);


//微信登录按钮的回调 获取code
const login = () => {
    // 微信授权登录先判断是都协议同意了!!
    if (!agree.value) {
        uni.showToast({
            title: "请务必勾选协议", //标题
            icon: "error", //图标
        });
        return;
    }

    //用户同意协议:获取微信登录临时凭证
    uni.login({
        provider: 'weixin', //使用微信登录
        success(res: any) { // 获取code
            //获取token
            getToken(res.code);
        },
    })
}

//获取用户身份凭证token
const getToken = async (code: string) => {
    const result = await ajax.reqToken(code);
    if (result.code == 200) {
        uni.setStorageSync("TOKEN", result.data.token);
        //返回上一页
        uni.navigateBack();
    }
};

</script>

课程学习页面

新建课程学习页面和配置路由

封装课程学习相关接口

ts
//获取视频学习的地址
reqPlayUrl(videoSourceId:string){
    return this.get({url:`/api/vod/video/getPlayURL/${videoSourceId}`})
}

获取课程学习数据

ts
import ajax from "@/api/index";
import { onLoad } from "@dcloudio/uni-app";
import { ref } from "vue";
const videoSourceId = ref("");
const chapterList = ref([]);
//存储播放视频地址
const playUrl = ref("");
//存储课程的id
const courseId = ref("");

// 获取视频
const getVideo = async () => {
    // 切记:微信小程序、uniapp,播放视频不能用本地资源
    const reuslt = await ajax.reqPlayUrl(videoSourceId.value);
    if (reuslt.code == 200) {
        playUrl.value = reuslt.data.playURL;
    }
}

// 获取课程详情数据
const getCourse = async () => {
    const result = await ajax.reqCourseDetail(courseId.value);
    if (result.code == 200) {
        chapterList.value = result.data.chapterList;
    }
}

onLoad((options) => {
    videoSourceId.value = options.videoSourceId;
    courseId.value = options.courseId;
    //获取第一个视播放地址
    getVideo()
    //获取课程详情
    getCourse()
})

渲染数据

vue
<template>
    <view class="container">
        <!-- 学习的视频 -->
        <view>
            <video class="video" :src="playUrl"
                poster="https://www.gulixueyuan.com/files/course/2022/11-15/10000666c9a9690562.jpg" object-fit="fill"
                :controls="true" autoplay page-gesture enable-play-gesture>
            </video>
        </view>
        <!-- 学习某一个课程内容 -->
        <view class="catalogue">
            <view class="title"> 课程目录 </view>
            <view class="catalogue_list">
                <uni-collapse ref="collapse">
                    <uni-collapse-item :title="item.title" v-for="item in chapterList" :key="item.id">
                        <view class="task_list">
                            <view class="task_items" v-for="info in item.children" :key="info.id">
                                <image class="task_type"
                                    src="https://cdn-cos-ke.myoed.com/ke_proj/mobilev2/m-core/f1c59a1527e075f6ebfba3d7ac605f07.png" />
                                <view class="task_title">{{ info.title }}</view>
                            </view>
                        </view>
                    </uni-collapse-item>
                </uni-collapse>
            </view>
        </view>
    </view>
</template>

订单页面

封装新建订单接口

ts

配置路由和新建页面

ts

发送请求

ts
uni.requestPayment({
    "provider": "wxpay",
    "orderInfo": {
        "appid": "wx499********7c70e",  // 微信开放平台 - 应用 - AppId,注意和微信小程序、公众号 AppId 可能不一致
        "noncestr": "c5sEwbaNPiXAF3iv", // 随机字符串
        "package": "Sign=WXPay",        // 固定值
        "partnerid": "148*****52",      // 微信支付商户号
        "prepayid": "wx202254********************fbe90000", // 统一下单订单号
        "timestamp": 1597935292,        // 时间戳(单位:秒)
        "sign": "A842B45937F6EFF60DEC7A2EAA52D5A0" // 签名,这里用的 MD5/RSA 签名
    },
    success(res) {},
    fail(e) {}
})

支付流程

ts
发请求获取订单号 给自己的后台发送请求获取订单号

发请求获取参数 给自己的后台发送请求获取支付参数

发请求支付 调用uni支付API发起支付
uni.requestPayment({
    provide:'',
    
})

支付成功查询状态
ts
uni.requestPayment({
    provide('weixin')
})
ts
发请求 拿订单号
发请求 参数[订单号] 获取支付参数
调用支付方法
支付成功,查询支付状态