拉钩教育移动端项目实战 - 项目搭建和选课功能

292 阅读3分钟

笔记来源:拉勾教育 - 大前端就业集训营

文章内容:学习过程中的笔记、感悟、和经验

项目搭建和选课功能

项目准备

cetGX4.png

移动端项目 :使用调试工具使用移动端调试

总体分为三大块:选课(无需登录)、学习和我(需要登录才能查看)

项目构建

  • 创建新项目:vue create 名字 - 手动选择配置预设 - 创建项目
  • 删除清空项目:删除不需要的文件、修改文件内容
  • 初始化项目:添加接口封装文件夹(视频里添加了utils和services)
  • 设置git:我这里不使用课程里的命令行而是使用sourcetree进行项目的管理,如果需要可以查看上一个项目

Vant组件库

和Element类似,Vant组件库是一个适用于移动端的Vue组件库,还在维护中,而且支持vue3和微信小程序

使用Vant

  • 使用vue-cli脚手架工具安装Vant - vue ui进入图形化界面安装依赖
  • 这里选择导入所有组件,因为我们需要使用很多Vant组件
// src/main.js  引入并注册Vant

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

// 引入Vant
import Vant from 'vant'
import 'vant/lib/index.css'
// 将Vant注册为vue插件
Vue.use(Vant)

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

浏览器适配

使用rem布局 - 根据不同视口宽度设置rem

需要使用两个插件

配置使用

  • 在项目根目录下书写postcss配置文件 - .postcssrc.js
  • 把浏览器配置信息书写在.browserslistrc中
  • 注意修改配置文件需要重启serve
// src/main.js  安装amfe-flexible后引入

......
// 引入amfe-flexible自动设置根元素rem
import 'amfe-flexible'
......
// .postcssrc.js  根目录下创建配置文件

// 配置固定写法
module.exports = {
  // 插件配置
  plugins: {
    // 自动添加浏览器前缀
    'autoprefixer': {
      // 这里vue不建议些在这里,所以放到 .browserslistrc 中去
      // browsers: ['Android >= 4.0, 'iOS >= 8']
    },
    // postcss-pxtorem配置
    'postcss-pxtorem': {
      // 默认rem
      rootValue: 37.5,
      // 需要转换为rem的属性,*表示全部
      propList: ['*']
    }
  }
}
.browserslistrc  根目录下添加浏览器配置

> 1%
last 2 versions
not dead
Android >= 4.0
iOS >= 8
src/App.vue  在组件中使用Vant的按钮组件试验一下两个插件是否生效

<template>
    <div>
        <van-button>按钮</van-button>
        <div class="box"></div>
    </div>
    <!-- <router-view/> -->
</template>

<style lang="scss" scoped>
.box {
    width: 100px;
    height: 100px;
    background: #ccc;
}
</style>

注意

  • postcss-pxtorem无法转换行内的px属性
  • 这里我遇见了postcss-pxtorem报错,将版本更改为5.1.1就可以了

路由处理

页面分析

  • 登陆页为单独页面
  • 选课、学习、我三个页面
  • 错误页面为单独页面
  • 其他页面功能后期添加

创建文件和路由规则设置

  • 选课组件为根目录,因为选课即使不登陆也能查看并且默认打开就是这个页面
  • 因为选课组件是默认首页,所以可以不使用路由懒加载而使用传统方式
  • 因为是移动端就不再书写包含关系了
在views文件夹下创建五个页面文件夹并且创建index,下面是login的index.vue示例,其他页面大致相同
src/views/login/index.vue

<template>
  <div>登陆页</div>
</template>

<script>
export default {
  name: 'login'
}
</script>

// src/router/index.js 创建五个页面的路由

import Vue from 'vue'
import VueRouter from 'vue-router'
// 选课页面不使用懒加载
import Course from '@/views/course'

Vue.use(VueRouter)

const routes = [
  { // 登陆页
    name: 'login',
    path: '/login',
    // 使用路由懒加载,按需加载,使用魔法命名
    component: () => import(/* webpackChunkName: 'login' */'@/views/login')
  },
  { // 选课页面,根页面
    name: 'course',
    path: '/',
    component: Course
  },
  { // 学习页面
    name: 'study',
    path: '/study',
    component: () => import(/* webpackChunkName: 'study' */'@/views/study')
  },
  { // 用户页面
    name: 'user',
    path: '/user',
    component: () => import(/* webpackChunkName: 'user' */'@/views/user')
  },
  { // 错误页面
    name: 'error',
    path: '*',
    component: () => import(/* webpackChunkName: 'error' */'@/views/error')
  }
]

const router = new VueRouter({
  routes
})

export default router

封装请求模块- 接口文档

使用axios进行接口AJAX请求

  • 安装axios插件,npm install axios
  • 创建js文件,文件中使用axios创建示例,设置url前缀,并导出axios实例
// src/api/axios.js  新建文件封装请求模块,以后所有的接口相关功能都封装在api文件夹中

// 引入axios插件
import axiosPlug from 'axios'

// 创建axios实例
const axios = axiosPlug.create({
  // 设置url前缀
  baseURL: 'http://edufront.lagou.com'
})

// 导出实例
export default axios

src/App.vue  测试我们封装的接口是否生效,打开浏览器network查看XHR是否成功

<template>
  <router-view/>
</template>

下面用完就可以删掉了
<script>
// 引入axios
import axios from '@/api/axios'
// 发起请求
axios({
  method: 'get',
  // 这个接口不需要登陆也可以请求
  url: '/front/ad/getAdList'
})
</script>

公共组件 - 底栏

使用Vant中的标签栏组件,组件提供了路由模式可进行路由跳转

公共组件要单独封装,在需要使用的三个组件中引入三个组件

图标可以使用Vant中的图标组件替换

直接修改标签栏组件中的to属性即可实现点击跳转到指定组件

src/common/foot-bar.vue  单独封装底部标签栏组件作为公共组件,公共组件全部放在common目录

<template>
  <!-- 底部标签栏,开启路由功能 -->
  <van-tabbar route>
    <!-- to设置跳转路由,icon图标 -->
    <van-tabbar-item replace to="/" icon="cart-o">选课</van-tabbar-item>
    <van-tabbar-item replace to="/study" icon="records">学习</van-tabbar-item>
    <van-tabbar-item replace to="/user" icon="user-o">我</van-tabbar-item>
  </van-tabbar>
</template>

<script>
export default {
  name: 'footBar'
}
</script>

src/views/course/index.vue  选课页面引入并使用标签栏公共组件

<template>
  <div class="course">
    <!-- 创建标签栏子组件实例 -->
    <footBar/>
  </div>
</template>

<script>
// 引入标签栏子组件
import footBar from '@/common/foot-bar'
export default {
  name: 'Course',
  // 注册组件
  components: {
    footBar
  }
}
</script>

选课功能

选课功能区分为上下两部分

  • 顶部置顶展示区
  • 底部全部课程列表展示区

新建两部分组件,将两部分组件分别引入到index.vue中使用

头部置顶组件

头部只有一个logo展示区,可以使用Vant图片组件

图片可以使用静态资源,使用:src = require(图片路径)引入

可能需要进行一些样式调整,Vant可以直接使用组件名作为类名进行属性设置

注意:我这里直接把顶部组件写在了index内部,就不单独写子组件了

src/views/course/index.vue  使用图片组件设置顶部logo

<template>
  <div class="course">
    <!-- 顶部 logo,使用require引入静态图片资源 -->
    <van-image
      width="180"
      :src="require('@/assets/logo.png')"
    />
    <!-- 创建标签栏子组件实例 -->
    <footBar/>
  </div>
</template>
.............
<style lang="scss" scoped>
// 顶部logo
.van-image {
  margin-left: -20px;
}
</style>

广告轮播图

使用Vant中的轮播组件

轮播图需要使用的数据使用广告接口

  • 获取所有广告位,这个就不封装了,直接网页打开查找一下首页轮播的id即可 - spaceKey
  • 封装获取广告位及其对应的广告接口,传递spaceKey参数即可获取广告列表

在组件创建后获取广告列表,将广告列表存储起来,使用v-for进行轮播结构创建即可

注意:广告有status状态,如果状态为0则代表该条广告下架状态,不进行展示,所以保存列表的时候需要判断是否需要展示

视频里使用的是计算属性筛选,我认为最简单的应该是获取数据的时候直接进行筛选

最后需要进行一些样式调整

src/views/course/index.vue  添加轮播图

<template>
  <div class="course">
    <!-- 顶部 logo,使用require引入静态图片资源 -->
    <van-image
      width="180"
      :src="require('@/assets/logo.png')"
    />
    <!-- 轮播图 -->
    <van-swipe class="my-swipe" :autoplay="3000" indicator-color="white">
      <!-- 使用 v-for 遍历计算属性 -->
      <van-swipe-item v-for="item in swipeListDate" :key="item.id">
        <img :src="item.img" :alt="item.name">
      </van-swipe-item>
    </van-swipe>
    <!-- 创建标签栏子组件实例 -->
    <footBar/>
  </div>
</template>

<script>
// 引入接口:获取广告位及对应广告
import { getAllAds } from '@/api/course'
// 引入标签栏子组件
import footBar from '@/common/foot-bar'
export default {
  name: 'Course',
  data () {
    return {
      // 广告列表
      swipeList: []
    }
  },
  // 注册组件
  components: {
    footBar
  },
  // 钩子函数
  created () {
    this.getAds()
  },
  methods: {
    // 获取轮播广告列表
    async getAds () {
      // 调用接口
      const { data } = await getAllAds(999)
      // 将获取到的数据传给data
      this.swipeList = data.content[0].adDTOList
    }
  },
  // 计算属性
  computed: {
    // 过滤真正需要展示的轮播广告
    swipeListDate () {
      return this.swipeList.filter(item => item.status === 1)
    }
  }
}
</script>

<style lang="scss" scoped>
// 顶部logo
.van-image {
  margin-left: -20px;
}
// 轮播图图片
.van-swipe .van-swipe-item img {
  width: 100%;
  height: 200px;
}
</style>

// src/api/course.js  新建js文件封装课程相关接口

// 引入封装的 axios
import axios from './axios'

// 根据 id 获取广告位及其对应广告
export const getAllAds = spaceKeys => {
  return axios({
    method: 'get',
    url: `/front/ad/getAllAds?spaceKeys=${spaceKeys}`
  })
}

课程列表基础布局

课程列表需要单独封装组件

使用Vant中的列表组件

src/views/course/index.vue - 中引入注册并使用列表组件
<script>
// 引入课程列表组件
import courseList from './son/course-list'
// 注册组件
components: {
  footBar,
  courseList
}</script>
<!-- 创建课程列表组件实例 -->
<courseList></courseList>


src/views/course/son/course-list.vue  新建文件书写课程列表组件
<template>
  <!-- 列表组件 -->
  <van-list
    v-model="loading"
    :finished="finished"
    finished-text="没有更多了"
    @load="onLoad"
  >
  <!-- 列表项 -->
    <van-cell v-for="item in listData" :key="item" :title="item" />
  </van-list>
</template>

<script>
export default {
  name: 'courseList',
  data () {
    return {
      // 信号值 - 标记是否列表加载完毕
      finished: false,
      // 信号值 - 标记列表是否处于刷新状态
      loading: false,
      // 列表数据
      listData: [1, 2, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3]
    }
  },
  methods: {
    // 列表触底刷新事件
    onLoad () {

    }
  }
}
</script>

固定列表处理

列表滚动不应让其他部分跟着滚动:设置列表固定定位,垂直方向允许滚动

因为列表可能要复用,所有有些样式属性可能就需要在引入的组件中设置

src/views/course/son/course-list.vue  列表组件设置公共样式

<style lang="scss" scoped>
// 列表定位
.van-list {
  position: fixed;
  left: 0;
  right: 0;
  // 开启Y轴滚动条
  overflow-y: auto;
}
</style>
src/views/course/index.vue  使用组件的父组件设置私有样式,这样就不会影响组件在其他父组件中的样式

<style lang="scss" scoped>
................
// 课程列表顶部和底部定位
.van-list {
  top: 260px;
  bottom: 50px;
}
</style>

接口封装

使用分页查询课程信息接口吧(前台接口有点问题,使用后台接口)

直接在列表组件onload中发请求,并且处理相应属性

//src/api/course.js  封装课程分页查询接口

// 分页查询课程信息
export const getQueryCourses = data => {
  return axios({
    method: 'post',
    url: '/boss/course/getQueryCourses',
    data
  })
}

单个课程结构布局与数据绑定

将获取到的数据添加到列表的数据中

单个列表设置结构和样式以及数据绑定

src/views/course/son/course-list.vue  书写结构并绑定数据、调整样式

<template>
  <!-- 列表组件 -->
  <van-list
    v-model="loading"
    :finished="finished"
    finished-text="没有更多了"
    @load="onLoad"
  >
    <!-- 列表项,使用for循环 -->
    <van-cell v-for="item in listData" :key="item.id">
      <!-- 左侧图片 -->
      <img :src="item.courseImgUrl" alt="">
      <!-- 右侧信息 -->
      <div class="r">
        <h3>{{item.courseName}}</h3>
        <p class="FirstField">{{item.previewFirstField}}</p>
        <p>
          <span class="discounts">¥{{item.discounts}}</span>
          <!-- 直接使用删除线 -->
          <s>¥{{item.price}}</s>
        </p>
      </div>
    </van-cell>
  </van-list>
</template>

<script>
// 引入接口:分页查询课程
import { getQueryCourses } from '@/api/course'
export default {
  name: 'courseList',
  data () {
    return {
      // 信号值 - 标记是否列表加载完毕
      finished: false,
      // 信号值 - 标记列表是否处于刷新状态
      loading: false,
      // 列表数据
      listData: [],
      // 课程查询页码
      currentPage: 1
    }
  },
  methods: {
    // 列表触底刷新事件,第一次加载的时候就会默认执行一次
    async onLoad () {
      // 调用接口
      const { data } = await getQueryCourses({
        currentPage: this.currentPage,
        pageSize: 10,
        status: 1
      })
      console.log(data)
      if (data.code === '000000') {
        // 如果获取成功,将获取到的数据添加进列表数据中
        this.listData.push(...data.data.records)
        // 页数 +1
        this.currentPage++
      }
      // 加载状态结束
      this.loading = false
      if (this.listData.length >= data.data.total) {
        this.finished = true
      }
    }
  }
}
</script>

<style lang="scss" scoped>
// 列表整体定位
.van-list {
  position: fixed;
  left: 0;
  right: 0;
  overflow-y: auto; // 开启Y轴滚动条
  // 单个课程
  .van-cell .van-cell__value {
    display: flex;
    // 清除默认效果
    h3, p {
      margin: 0;
    }
    img {
      height: 100px;
      width: 75px;
      border-radius: 5px;
    }
    .r {
      display: flex;
      flex-direction: column;
      margin-left: 10px;
      .FirstField {
        flex-grow: 1;
      }
      p .discounts {
        margin-right: 10px;
        color: #ff7452;
      }
      p s {
        color: #ccc;
      }
    }
  }
}
</style>

下拉刷新

列表组件提供了下拉刷新案例

刷新的时候重置参数,替换数据

添加提示信息

添加判断信息,判断是否获取到了数据并且数据不为空再设置数据

src/views/course/son/course-list.vue  添加下拉刷新功能

<template>
  <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
    <!-- 列表组件 -->
    <van-list
      v-model="loading"
      :finished="finished"
      finished-text="没有更多了"
      @load="onLoad"
    >
      <!-- 列表项,使用for循环 -->
      <van-cell v-for="item in listData" :key="item.id">
        <!-- 左侧图片 -->
        <img :src="item.courseImgUrl" alt="">
        <!-- 右侧信息 -->
        <div class="r">
          <h3>{{item.courseName}}</h3>
          <p class="FirstField">{{item.previewFirstField}}</p>
          <p>
            <span class="discounts">¥{{item.discounts}}</span>
            <!-- 直接使用删除线 -->
            <s>¥{{item.price}}</s>
          </p>
        </div>
      </van-cell>
    </van-list>
  </van-pull-refresh>
</template>

<script>
// 引入接口:分页查询课程
import { getQueryCourses } from '@/api/course'
export default {
  name: 'courseList',
  data () {
    return {
      // 信号值 - 标记是否列表加载完毕
      finished: false,
      // 信号值 - 标记列表是否处于刷新状态
      loading: false,
      // 列表数据
      listData: [],
      // 课程查询页码
      currentPage: 1,
      // 信号值 - 下拉刷新
      refreshing: false
    }
  },
  methods: {
    // 下拉刷新函数
    async onRefresh () {
      // 重置页码为 1
      this.currentPage = 1
      // 修改触底为 false
      this.finished = false
      // 调用接口获取数据
      const { data } = await getQueryCourses({
        currentPage: this.currentPage,
        pageSize: 10,
        status: 1
      })
      // 添加条件,如果获取成功并且数据部位空使用数据
      if (data.code === '000000' && data.data.records !== 0) {
        // 直接替换数据
        this.listData = data.data.records
        // 页数 +1
        this.currentPage++
        // 修改下拉刷新信号值
        this.refreshing = false
        // 弹出提示
        this.$toast('刷新成功')
      }
    },
    // 列表触底刷新事件,第一次加载的时候就会默认执行一次
    async onLoad () {
      // 调用接口
      const { data } = await getQueryCourses({
        currentPage: this.currentPage,
        pageSize: 10,
        status: 1
      })
      if (data.code === '000000' && data.data.records !== 0) {
        // 如果获取成功,将获取到的数据添加进列表数据中
        this.listData.push(...data.data.records)
        // 页数 +1
        this.currentPage++
      }
      // 加载状态结束
      this.loading = false
      if (this.listData.length >= data.data.total) {
        this.finished = true
      }
    }
  }
}
</script>

<style lang="scss" scoped>
// 列表整体定位
.van-pull-refresh {
  position: fixed;
  left: 0;
  right: 0;
  overflow-y: auto; // 开启Y轴滚动条
  // 单个课程
  .van-cell .van-cell__value {
    display: flex;
    // 清除默认效果
    h3, p {
      margin: 0;
    }
    img {
      height: 100px;
      width: 75px;
      border-radius: 5px;
    }
    .r {
      display: flex;
      flex-direction: column;
      margin-left: 10px;
      .FirstField {
        flex-grow: 1;
      }
      p .discounts {
        margin-right: 10px;
        color: #ff7452;
      }
      p s {
        color: #ccc;
      }
    }
  }
}
</style>