页面目录:src/pages/course/detail.vue
这个页面涉及的逻辑就比较复杂一点点了,具体可以参考需求文档中的课程详情

获取参数

因为这个页面是课程详情,所以需要在做跳转的时候把courseId带过来,这里我们在onLoad()当中获取到参数。

  1. onLoad(option) {
  2. this.options = option;
  3. },

请求数据

定义data

  1. data() {
  2. return {
  3. /** 章节列表 */
  4. chapterList: [],
  5. /** 课程详情-data */
  6. course: {},
  7. /** 课程详情-course */
  8. courseDetail: {},
  9. }
  10. }

这里我们需要调用两个接口,来渲染我们的页面,接口地址为:课程详情评论列表

  1. onShow() {
  2. this.getCourseDetail();
  3. this.getCommentList();
  4. },
  5. /**
  6. * @description: 获取评论列表
  7. * @return {*}
  8. */
  9. async getCommentList() {
  10. try {
  11. const res = await userService.commentList({
  12. page: 1,
  13. limit: 10,
  14. courseId: this.options.id,
  15. });
  16. this.comment = res.data;
  17. this.getTopValue();
  18. } catch (e) {
  19. console.log("e", e);
  20. }
  21. },
  22. /**
  23. * @description: 获取课程详情
  24. * @return {*}
  25. */
  26. async getCourseDetail() {
  27. try {
  28. const res = await courseService.courseDetail({
  29. id: this.options.id,
  30. // id: '1192252213659774977',
  31. });
  32. this.chapterList = res.data.chapterList;
  33. this.course = res.data;
  34. this.courseDetail = res.data.course;
  35. this.isCollect = res.data.isCollect;
  36. } catch (e) {
  37. console.log("e", e);
  38. }
  39. },

渲染基础信息

  1. <view class="banner">
  2. <image :src="courseDetail.cover" />
  3. </view>
  4. <view class="info bg_white">
  5. <view class="price">
  6. <h3>
  7. <text>¥</text>
  8. {{ courseDetail.price || 0 }}
  9. </h3>
  10. <view class="buy_count"
  11. >已有{{ courseDetail.buyCount || 0 }}人购买</view
  12. >
  13. </view>
  14. <h3 class="name">
  15. {{ courseDetail.title || "" }}
  16. </h3>
  17. <view class="tag_list">
  18. <view class="tag_item">{{ courseDetail.subjectLevelTwo || "" }}</view>
  19. </view>
  20. </view>

基础信息如下图
image.png

tab组件封装

**介绍、目录、评论**我们做成一个tab组件,在切换的时候,需要实现以下的功能:

  • 对应目标区域会做锚点定位
  • 如果滚动的是你,tab会做吸顶
  • 切换元素样式改变

如下图:
image.png

  1. <template>
  2. <view class="tabs_bar" :style="{ background: background }">
  3. <view
  4. v-for="item in tabList"
  5. :key="item.index"
  6. :class="['tabs_item', current == item.index ? 'active' : '']"
  7. :data-current="item.index"
  8. @click="clickTab"
  9. >{{ item.name }}</view
  10. >
  11. </view>
  12. </template>
  13. <script>
  14. export default {
  15. name: 'v-tabs',
  16. props: {
  17. currentTab: {
  18. type: [String, Number],
  19. default: 0,
  20. },
  21. tabList: {
  22. type: Array,
  23. default: [],
  24. },
  25. background: {
  26. type: String,
  27. default: '#fff',
  28. },
  29. },
  30. data() {
  31. return {
  32. current: 0,
  33. };
  34. },
  35. mounted() {
  36. this.current = this.currentTab;
  37. },
  38. methods: {
  39. clickTab(e) {
  40. // console.log('e', e);
  41. if (this.current == e.target.dataset.current) {
  42. // 点击自身不做任何处理
  43. return false;
  44. } else {
  45. this.current = e.target.dataset.current;
  46. this.$emit('clickTab', this.current);
  47. }
  48. },
  49. },
  50. };
  51. </script>
  52. <style scoped lang="scss">
  53. .tabs_bar {
  54. height: 40px;
  55. width: 100%;
  56. display: flex;
  57. justify-content: space-around;
  58. .tabs_item {
  59. position: relative;
  60. // width: 100%;
  61. text-align: center;
  62. color: #666c80;
  63. font-size: 12px;
  64. line-height: 34px;
  65. z-index: 1001;
  66. }
  67. .active {
  68. color: #3e414d;
  69. &::after {
  70. content: '';
  71. position: absolute;
  72. display: block;
  73. width: 20px;
  74. height: 3px;
  75. bottom: 3px;
  76. left: 50%;
  77. margin-left: -10px;
  78. background-color: #2080f7;
  79. border-radius: 6px;
  80. }
  81. }
  82. }
  83. </style>

吸顶组件封装

我们再封装一个吸顶组件,用来实现tab的吸顶功能
在封装之前,先分析一下吸顶的原理是什么?

  • 首先设置吸顶元素停留的位置top值
  • 当监听页面滚动的时候,比较scrollTop跟top值的大小
  • 如果scrollTop>=top,则吸顶元素的定位修改成fixed
  • 反之取消fixed定位的值

下面是具体的封装代码:

  1. <template>
  2. <view
  3. :class="{ sticky: fixedParams.isFixed }"
  4. :id="sid"
  5. :style="{ top: top, zIndex: zIndex }"
  6. >
  7. <slot></slot>
  8. </view>
  9. </template>
  10. <script>
  11. export default {
  12. name: 'v-sticky',
  13. props: {
  14. /** 距离顶部的距离 */
  15. top: {
  16. type: [String, Number],
  17. default: 0,
  18. },
  19. zIndex: {
  20. type: [String, Number],
  21. default: 999,
  22. },
  23. /** 默认id,不带# */
  24. sid: {
  25. type: String,
  26. default: '',
  27. },
  28. },
  29. data() {
  30. return {
  31. /** 吸顶元素属性 */
  32. fixedParams: {
  33. fixedH: 0, // 元素高度
  34. fixedTop: 0, // 元素距离顶部的距离
  35. isFixed: false, // 是否吸顶
  36. },
  37. };
  38. },
  39. mounted() {
  40. // 获取吸顶元素距离顶部的距离
  41. const _this = this;
  42. // console.log('id', this.sid);
  43. // 在组件中查找元素需要添加.in(this)
  44. this.$nextTick(() => {
  45. const query = uni.createSelectorQuery().in(this);
  46. query
  47. .select(`#${_this.sid}`)
  48. .boundingClientRect((e) => {
  49. _this.fixedParams.fixedTop = e.top;
  50. _this.fixedParams.fixedH = e.height;
  51. })
  52. .exec();
  53. });
  54. /** 监听滚动事件 */
  55. uni.$on('onPageScroll', (e) => {
  56. if (this.fixedParams.fixedTop > e.scrollTop) {
  57. this.fixedParams.isFixed = false;
  58. } else {
  59. this.fixedParams.isFixed = true;
  60. }
  61. });
  62. },
  63. methods: {},
  64. };
  65. </script>
  66. <style lang="scss" scoped>
  67. .sticky {
  68. width: 100%;
  69. position: fixed;
  70. top: 0px;
  71. z-index: 999;
  72. }
  73. </style>

注意<slot></slot>插槽的使用,就是为了在sticky里面嵌套tab

下面是具体的使用方式:

  1. <view class="tab_wrapper bg_white">
  2. <v-sticky sid="tab">
  3. <v-tabs :tabList="tabList" @click-tab="clickTab"></v-tabs>
  4. </v-sticky>
  5. </view>

还需要事件监听的方式去监听页面滚动

  1. onPageScroll(e) {
  2. uni.$emit("onPageScroll", e);
  3. },

现在我们已经实现了tab功能,而且能够吸顶,接下来当点击tab的是要滚动到对应内容区域,也就是类似锚点定位的功能了。

锚点定位

首先分析,当点击tab的时候如何跳转到对应的内容区域的?
举个例子:

  1. <tab>
  2. <tab1>1</tab1>
  3. <tab1>1</tab1>
  4. <tab1>1</tab1>
  5. <tab>
  6. <view>
  7. <view>1</view>
  8. <view>2</view>
  9. <view>3</view>
  10. </view>

当点击tab1,视图滚动到view1,点击tab2,视图滚动到view2…
所以,需要先获取每个view的距离顶部的top值,然后利用uni.pageScrollTop()滚动到对应top的位置
下面是具体代码的实现

首先当数据请求完毕以后,保存每个需要锚点定位的元素的top值

  1. <template>
  2. <view class="course_card'>
  3. <view class="intro card" id="anchor0"></view>
  4. <view class="intro card" id="anchor1"></view>
  5. <view class="intro card" id="anchor2"></view>
  6. </view>
  7. </template>
  8. <script>
  9. export default {
  10. data() {
  11. return {
  12. /** 锚点元素top值 */
  13. enchorParams: {
  14. enchorTop1: 0,
  15. enchorTop2: 0,
  16. enchorTop3: 0,
  17. },
  18. }
  19. },
  20. methods: {
  21. /**
  22. * @description: 获取评论列表
  23. * @return {*}
  24. */
  25. async getCommentList() {
  26. try {
  27. const res = await userService.commentList({
  28. page: 1,
  29. limit: 10,
  30. courseId: this.options.id,
  31. });
  32. this.comment = res.data;
  33. this.getTopValue();
  34. } catch (e) {
  35. console.log("e", e);
  36. }
  37. },
  38. getTopValue() {
  39. const _this = this;
  40. // 提前保存锚点元素的top值
  41. this.$nextTick(() => {
  42. const query = uni.createSelectorQuery().in(_this);
  43. query
  44. .select("#anchor0")
  45. .boundingClientRect((e) => {
  46. console.log("e1", e);
  47. this.enchorParams.enchorTop1 = e.top;
  48. })
  49. .select("#anchor1")
  50. .boundingClientRect((e) => {
  51. console.log("e2", e);
  52. _this.enchorParams.enchorTop2 = e.top;
  53. })
  54. .select("#anchor2")
  55. .boundingClientRect((e) => {
  56. console.log("e2", e);
  57. this.enchorParams.enchorTop3 = e.top;
  58. })
  59. .exec();
  60. });
  61. },
  62. }
  63. }
  64. </script>

点击tab的时候触发事件,滚动到对应的区域。

  1. clickTab(index) {
  2. // 点击tab的时候触发
  3. const _this = this;
  4. let top = 0;
  5. if (index == 0) {
  6. top = this.enchorParams.enchorTop1;
  7. } else if (index == 1) {
  8. top = this.enchorParams.enchorTop2;
  9. } else {
  10. top = this.enchorParams.enchorTop3;
  11. }
  12. // 锚点定位,滚动到对应的位置
  13. uni.pageScrollTo({
  14. duration: 500,
  15. scrollTop: top - 58,
  16. });
  17. },

课程权限控制

当点击目录的时候,有两种情况

  • 没有登陆的,点击弹出提示框,去登陆

image.png

  • 未购买课程,点击弹出提示框,去购买,目录显示锁定

image.png

登陆态检测

那么这个登陆权限控制是如何做的呢?其实只需要在store中定义一个方法,去检测登陆态,而不是在在页面逻辑中去做这件事情,下面是store中添加的方法:

  1. goLogin(context, callBack) {
  2. if (!this.state.token) {
  3. uni.showModal({
  4. title: '提示',
  5. content: '请登录',
  6. success: function (res) {
  7. if (res.confirm) {
  8. uni.navigateTo({
  9. url: '/pages/login/index',
  10. });
  11. uni.clearStorageSync();
  12. } else if (res.cancel) {
  13. console.log('用户不想登陆');
  14. }
  15. },
  16. });
  17. } else {
  18. callBack();
  19. }

这里接受了两个参数,第一个就是store本身,必须传,第二个参数是一个callback,这个callback有什么妙用呢?
当我们检测到token没有,说明当前是未登陆状态,直接弹出提示框,让用户去登陆。如果是登陆了,就执行传递过来的callback,页面流程正常继续执行。下面的是页面如何使用goLogin的示例:

  1. handleFavo() {
  2. this.$store.dispatch("goLogin", () => {
  3. // 如果是登陆,执行回调函数里面的逻辑
  4. if (!this.isCollect) {
  5. this.collectSave();
  6. } else {
  7. this.collectRemove();
  8. }
  9. });
  10. },

上面是一个点击收藏按钮的功能,点击收藏的时候,是需要用户登陆的,这时候就需要检测一下,直接使用上面的用法就能很快的实现。
同样的,点击购买的时候也需要检测登陆态

  1. handleBuy() {
  2. this.$store.dispatch("goLogin", () => {
  3. // 点击购买
  4. // 判断是否登陆,是否已经购买,已经购买了,按钮显示去学习
  5. if (this.course.isBuy) {
  6. const videoSourceId = this.chapterList[0].children[0].videoSourceId;
  7. uni.navigateTo({
  8. url: `/pages/video/index?id=${this.options.id}&videoSourceId=${videoSourceId}`,
  9. });
  10. } else {
  11. uni.navigateTo({
  12. url: `/pages/order/index?courseId=${this.courseDetail.id}`,
  13. });
  14. }
  15. });
  16. },