Vue2.6+VueCli4.3+CubeUI 完成小D课堂移动端

Vue2.6+VueCli4.3+CubeUI 完成小D课堂移动端

0. 项目效果

在这里插入图片描述
在这里插入图片描述

1. 搭建项目架构

  • 创建 vue 项目
    • vue create iclass-web
  • 选择 feature 模式,安装 vuex/vue-router
    • 安装 axios
      • npm install axis --save
    • npm install <package_name> --save
      • 表示将这个包名及对应的版本添加到 package.jsondependencies
    • npm install <package_name> --save-dev
      • 表示将这个包名及对应的版本添加到 package.jsondevDependencies
  • 添加 cube-ui 依赖
    • vue add cube-ui

2. 项目目录结构创建

2.1 分析前端需求
  • 底部导航
  • 首页 Banner
  • 首页视频列表
  • 视频详情模块
  • 注册模块
  • 登陆模块
  • 个人信息模块
  • 下单模块
  • 订单列表模块
2.2 介绍目录结构
  • 创建新目录
    • api/router/views
    • views
      • CourseDetail
      • Home
      • Register
      • Login
      • Order
      • Pay
      • Personal

3. 基于浏览器和 nodeJS 的 http 客户端 Axios 模块

3.1 什么是 Axios
  • 基于 promise 用于浏览器nodeJShttp 客户端
    • 支持浏览器nodeJS
    • 支持 Promise API
    • 支持拦截请求和响应
    • 支持转换请求和响应数据
    • JSON 数据的自动转换
    • 客户端支持以防止 XSRF
  • 文档地址:<http://www.axios-js.com/zh-cn/docs/
  • 安装 Axios
    • npm install axios
3.2 GET 请求
// 为给定 ID 的 user 创建请求
axios.get('/user?ID=12345')
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

// 上面的请求也可以这样做
axios.get('/user', {
    params: {
      ID: 12345
    }
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });
3.3 POST 请求
axios.post('/user', {
    firstName: 'Fred',
    lastName: 'Flintstone'
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });
3.4 创建实例
  • 可以使用自定义配置新建一个 axios 实例
  • axios.create([config])
const instance = axios.create({
  baseURL: 'https://some-domain.com/api/',
  timeout: 1000,
  headers: {'X-Custom-Header': 'foobar'}
});

4. Axios 封装通用后端请求 API 模块

  • getData.js
import axios from '../request'

//注册接口
export const registerApi = (phone, pwd , name)=> axios.post("/api/v1/pri/user/register",{
    "phone":phone,
    "pwd":pwd,
    "name":name
})

//登录接口
export const loginApi = (phone, pwd) => axios.post("/api/v1/pri/user/login",{
    phone,
    pwd
})


//轮播图接口
export const getBanner = () => axios.get("/api/v1/pub/video/list_banner")

//视频列表接口
export const getVideoList = ()=> axios.get("/api/v1/pub/video/list")


//视频详情
export const getVideoDetail = (vid)=> axios.get("/api/v1/pub/video/find_detail_by_id?",{
    params: {
        video_id:vid
    }
})

//下单接口
export const saveOrder = (token, vid)=>axios.post("/api/v1/pri/order/save",{
    "video_id":vid
},{
    headers:{
        "token":token
    }
})

//订单列表
export const getOrderList = (token)=>axios.get("/api/v1/pri/order/list",{
    params:{
        "token":token
    }
})

//用户信息接口
export const getUserInfo = (token)=>axios.get("/api/v1/pri/user/find_by_token",{
    params:{
        "token":token
    }
})
  • request.js
// 导入 axios 模块
import axios from 'axios'


// 创建 axios 实例
const service = axios.create({
    // url = baseURL + request url
    // 根目录
    baseURL: 'xxxx',

    // 配置请求超时时间
    timeout: 5000
})

// 导出 service
export default service

5. Vue-Router 开发路由

5.1 vue-router 介绍
  • Vue.js 官方的路由管理器,和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌
  • 官方文档https://router.vuejs.org/zh/
5.2 vue-router 常见 API
  • 文档https://router.vuejs.org/zh/api/
  • router.path
    • 获取当前的路由
  • router.go(n)
    • 这个方法的参数是一个整数,表示在 history 记录中向前或者向后退多少步
    • 类似 window.history.go(n) 方法
  • router.push(path)
    • 导航到不同的 path 路径
    • 这个方法会向 history 栈添加一个新的记录
    • 所以当用户点击浏览器后退按钮时,则回到之前的 URL
5.3 路由对象属性
5.3.1 $route.path
  • 类型:string
  • 字符串,对应当前路由的路径,总是解析为绝对路径,如:"/foo/bar"
5.3.2 $route.params
  • 类型:Object
  • 一个 key/value 对象,包含了动态片段和全匹配片段如果没有路由参数,就是一个空对象
5.3.3 $route.query
  • 类型:Object
  • 一个 key/value 对象,表示 URL 查询参数
  • 例如,对于路径 /foo?user=1,则有 $route.query.user == 1
  • 如果没有查询参数,则是个空对象
// 引用 vue,vue-router 模块
import Vue from 'vue'
import VueRouter from 'vue-router'

// 引入组件模块
import Home from '../views/Home/Home.vue'
import CourseDetail from '../views/CourseDetail/CourseDetail.vue'
import Login from '../views/Login/Login.vue'
import Order from '../views/Order/Order.vue'
import Pay from '../views/Pay/Pay.vue'
import Personal from '../views/Personal/Personal.vue'
import Register from '../views/Register/Register.vue'

// 使用 VueRouter 
Vue.use(VueRouter)

// 单独提出 routes
// 定义路由关系
const routes = [
    {
        path: "/",
        name: "Home",
        component: Home
    },
    {
        path: "/coursedetail",
        name: "CourseDetail",
        //按需加载
        component: () => import("../views/CourseDetail/CourseDetail.vue")
        //component:CourseDetail
    },
    {
        path: "/login",
        name: "Login",
        component: Login
    },
    {
        path: "/order",
        name: "Order",
        component: Order,
        meta: { requiresAuth: true }
    }, {
        path: "/pay",
        name: "Pay",
        component: Pay,
        meta: { requiresAuth: true }
    }, {
        path: "/personal",
        name: "Personal",
        component: Personal,
        meta: { requiresAuth: true }

    }, {
        path: "/register",
        name: "Register",
        component: Register
    }
]

// 创建路由实例
const router = new VueRouter({
    // 定义路由关系
    routes
})

// 导出 router
export default router

6. 通用底部选项卡 CommonsFooter 开发

6.1 查看 cube-ui 文档
6.2 cube-tab-bar 组件
6.3 底部选项卡 cube-tab-bar
  • template 开发
    • Mac 格式化代码:shift + option + F
    • windows 格式化代码:shift + alt + F
<template>
  <div class="tab">
    <cube-tab-bar v-model="selectedLabelSlots" @click="changHandler">
      <!-- 具体插槽 -->
      <cube-tab
        v-for="(item) in tabs"
        :icon="item.icon"
        :label="item.label"
        :key="item.path"
        :value="item.path"
      ></cube-tab>
    </cube-tab-bar>
  </div>
</template>
export default {
  data() {
    return {
      // 默认选中首页
      selectedLabelSlots: "/",
      tabs: [
        {
          label: "首页",
          icon: "cubeic-home",
          path: "/"
        },
        {
          label: "我的订单",
          icon: "cubeic-like",
          path: "/order"
        },
        {
          label: "个人中心",
          icon: "cubeic-person",
          path: "/personal"
        }
      ]
    };
  }
};
  • 开发方法
methods: {
   changHandler(path){
       //this.$route.path是当前路径
       if(path !== this.$route.path){
           // 当点击的时,传入对应路径
           // 如果页面当前路径不是点击对应的路径
           // 则将点击的路径推到路由中
           this.$router.push(path)
       }
   }
},
 //vue实例生命周期 created:在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图
 //vue实例生命周期 mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行额外的操作    
 //https://cn.vuejs.org/v2/guide/instance.html#%E5%AE%9E%E4%BE%8B%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E9%92%A9%E5%AD%90
created(){
   //默认路由选择器,比如刷新页面,需要重新进到当前路由
   this.selectedLabelSlots = this.$route.path
}
  • 配置样式
<!--SCSS是一种CSS预处理语言, scoped 是指这个scss样式 只作用于当前组件-->
<style lang="scss" scoped>
.tab {
  position: fixed;
  bottom: 0;
  z-index: 999;
  background-color:#fff;
  width: 100%;
  border-top: 1px solid rgba($color: #000000, $alpha: 0.1);
} 
.cube-tab_active {
  color: #3bb149;
}
</style>

7. 首页 Home 模块开发

7.1 拆分子组件
  • Home
  • banner
  • videoList
  • 注意点
    • 指令属性里面取data里面的数据是不用加{{}}
    • html标签内容体中间则需要加{{}}
7.2 template 开发
<template>
	<div>
	<!-- 轮播图组件 -->
	<home-banner :banners="banners"></home-banner>
	<!-- 视频列表组件 -->
	<video-list :courselist="courselist"></video-list>
	<!-- 底部导航栏组件 -->
	<common-footer></common-footer>
	</div>
</template>
7.3 await async 知识点
// 导入组件模块
import HomeBanner from "./Component/Banner";
import VideoList from "./Component/VideoList";
import CommonFooter from "@/components/CommonFooter";

// 导入接口API
import { getBanner, getVideoList } from "@/api/getData.js";

export default {
  //注册组件
  components: {
    HomeBanner,
    VideoList,
    CommonFooter,
  },
  //声明数据源
  data() {
    return {
      // 轮播图与视频列表数据都是数组形式
      banners: [],
      videoList: [],
    };
  },
  // 定义方法
  methods: {
    // 异步调用 getBanner 接口
    // 获取轮播图数据
    async getBannerData() {
      // 捕获异常
      try {
        // 等待异步方法执行完成
        const result = await getBanner();
        console.log(result);
        console.log(result.data.code == 0)
        if (result.data.code == 0) {
          this.banners = result.data.data;
        }
      }catch(error){
          console.lo(error)
      }
    },

    //获取视频列表
    async getVList(){
        try{
            const result = await getVideoList();
            if (result.data.code == 0) {
                this.videoList = result.data.data;
            }
        }catch(error){
            console.lo(error)
        }
    }
  },
  mounted(){
      //当页面渲染完成时调用方法获取数据
      this.getBannerData();
      this.getVList()
  }
  
};

8. 首页轮播图 Banner 模块开发

<template>
  <div>
    <cube-slide :data="banners">
      <cube-slide-item
        v-for="(item, index) in banners"
        :key="index">

        <a :href="item.url">
          <img :src="item.img"  style="width:100%"/>
        </a>
      
      </cube-slide-item>
    </cube-slide>
  </div>
</template>

<script>
export default {
    //获取父组件传递过来的值
    props:{
        banners:{
            type:Array,
            required:true
        }
    }
};
</script>

<style lang="scss" scoped>
</style>

9. 首页视频列表 VideoList 模块开发

9.1 router-link 讲解
<template>
  <div class="list-content">
    <div class="list">
      <!-- 遍历视频 -->
      <!-- 跳转到视频详情页,需要传入对应的 ID -->
      <router-link
        :key="item.id"
        :to="{ path: '/coursedetail', query: { video_id: item.id } }"
        class="course"
        v-for="item in videoList"
      >
        <div class="item_img">
          <img :src="item.cover_img" />
        </div>
        <div class="video_info">
          <div class="c_title">{{ item.title }}</div>
          <div class="price">¥ {{ item.price / 100 }}</div>
        </div>
      </router-link>
    </div>
  </div>
</template>
  • script
export default {
  // 获取父组件传递过来的值
  props: {
    videoList: {
      type: Array,
      required: true,
    },
  },
};
  • style
//列表包裹层边距
.list-content {
  margin-top: 20px;
  padding: 0 13px;
}
//视频包括层
.list {
  display: flex; //设置flex布局
  flex-wrap: wrap; //换行排列
  justify-content: space-between; //两端对齐
  padding-bottom: 55px;
}
//视频个体层
.course {
  width: 48%;
  margin-bottom: 17px;
}
//视频图片
.item_img {
  font-size: 0; //消除图片元素产生的间隙
  box-shadow: 0 4px 11px 0 rgba(43, 51, 59, 0.6); //设置图片阴影,rgba前三个参数是颜色编码,最后一个是透明度
  border-radius: 8px; //设置图片圆角
  img {
    width: 100%;
    border-radius: 8px;
  }
}
.c_title {
  //设置超过两行隐藏 start
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  word-break: break-all;
  //设置超过两行隐藏 end
  font-size: 11px;
  height: 26px;
  line-height: 13px;
  margin-top: 10px;
  color: #2b333b;
}
//价格
.price {
  margin-top: 8px;
  font-size: 12px;
  color: #d93f30;
}

10. 视频详情 CourseDetail 模块开发

  • 拆分组件

    • CourseDetail.vue
      • Header.vue
      • Course.vue
      • Tab.vue
        • Summary.vue
        • Calalog
  • 开发 CourseDetailtemplate

<template>
    <div>
        
        <!--顶部返回组件-->
        <detail-header :videoInfo="videoInfo"></detail-header>

        <!--视频介绍组件-->
        <detail-course :videoInfo="videoInfo"></detail-course>

        <!--视频tab简介组件-->
         <detail-tab :videoInfo="videoInfo" :chapterList="chapterList"></detail-tab> 

        <!--底部立刻购买-->
        <footer>
            <router-link :to="{path:'/pay',query:{video_id:this.$route.query.video_id}}" class="user_buy">
                <button>立刻购买</button>            
            </router-link>
        </footer> 

    </div>
</template>
  • 开发 CourseDetailscript
//引入组件
import DetailHeader from './Components/Header'
import DetailCourse from './Components/Course'
import DetailTab from './Components/Tab'

import { getVideoDetail } from "@/api/getData.js";


export default {
    //注册组件
    components:{
        DetailHeader,
        DetailCourse,
        DetailTab
    },

    data(){
        return {
            //视频信息数据
            videoInfo:{},
            //章集数据
            chapterList:[]
        }
    },

    methods:{
        // 获取视频详情
        // 传入 vid
        async getDetail(vid){
            try{
               const result =  await getVideoDetail(vid)
               if(result.data.code == 0){
                   this.videoInfo = result.data.data;
                   this.chapterList = result.data.data.chapter_list;
               }

            }catch(error){
                console.log(error)
            }
        }
    },

    mounted(){
        //渲染完成后拿数据
        console.log(this.$route.query.video_id)
        this.getDetail(this.$route.query.video_id);

    }
}
  • 配置样式
//底部
footer {
  // fixed固定在底部
  position: fixed;
  bottom: 0;
  width: 100%;
  padding: 8px 0;
  background-color: #fff;
  z-index: 999;
  box-shadow: 0 -2px 4px 0 rgba(0, 0, 0, 0.05);
}
//设置购买按钮样式
button {
  display: block;
  color: #fff;
  margin: 0 auto;
  background-color: #d93f30;
  height: 34px;
  line-height: 34px;
  border-radius: 17px;
  width: 80%;
  border: none;
  font-size: 15px;
  text-align: center;
}

11. 视频详情页 Header 模块开发

  • 代码
<template>
    <div>
        <header>
            <div class="header">
                <!-- 返回箭头 -->
                <!-- 可以返回上一页 -->
                <span @click="$router.back(-1)"> <i class="cubeic-back"></i> </span>
                <div class="title">
                    {{videoInfo.title}}
                </div>
            </div>
        </header>

    </div>
</template>

<script>
export default {
    // 获取父组件中的数据
    props:{
        videoInfo:{
            type:Object,
            required:true
        }
    }


}
</script>

<style lang="scss" scoped>
.header {
    display: flex;//flex左右布局
    background-color: #07111b;
    padding: 10px 20px;
    color: #fff;
}  
// 返回箭头
.cubeic-back {
    color: #fff;
    margin-right:5px;
}
//视频标题
.title {
    font-size: 16px;
    width: 80%;
    //超出省略
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
}
</style>

12. 视频详情页 Course 模块开发

  • 代码
<template>
    <div class="c_wrapper">
        <!-- 视频信息缩略层 -->
        <div class="course">

            <div class="l_img">
                <img :src="videoInfo.cover_img" :title="videoInfo.title">
            </div>

            <div class="r_txt">
                <div class="txt">
                <span>综合评分:</span>
                <p>{{ videoInfo.point }}</p>

                </div>
                <div class="txt">
                <span>价格:</span>
                <p>{{ videoInfo.price/100 }}</p>
                </div>
                
            </div>


        </div>
  </div>
</template>

<script>
    export default {
        // 从父组件获取视频信息
        props: {
            videoInfo: {
                type: Object,
                required: true
            }
        }
    }
</script>

<style lang="scss" scoped>
//包裹层
.c_wrapper {
    padding: 0 14px;
}
//视频信息包裹层
.course {
  margin:14px 0;
  display:flex;//设置flex,左右布局
}
//视频左边图片层
.l_img {
  height:88px;
  margin-right:14px;
  & img {
    height:100%;
    border-radius:15px;
  }   
}
// 视频右边文字包裹层
.r_txt {
    padding:6px 0;
    font-size:12px;
    flex:1;//设置1可自动伸缩占用剩余空间
}
//每行文字层(综合评分、价格)
.txt {
    // 设置flex让文字两端对齐
    display:flex;
    justify-content:space-between;
    line-height:16px;
    & p {
        text-align:center;
        width:40%;
        color:#3bb149;
    }   
    & i {
        color:#666;
        font-weight:bolder;
        width:60%;
        & span {
            color:#2b333b;
            font-size:12px;
        }
    }      
}
  
</style>

13. 视频详情页 Tab 模块开发

13.1 什么是 vue 动态组件
13.2 组件的过渡
<template>
    
    <div>
        <cube-tab-bar v-model="selectedLabel" show-slider>
            <cube-tab v-for="item in tabs" :label="item.label" :key="item.label">
            </cube-tab>
        </cube-tab-bar>
        
        <component :videoInfo="videoInfo" :chapterList="chapterList" :is='selectedLabel==="简介"?"Summary":"Catalog" '>
        </component>

    </div>

</template>
  • script
import Summary from './Summary'
import Catalog from './Catalog'

export default {

    components:{
        Summary,
        Catalog
    },
    // 获取父组件的数据
    props:{
        videoInfo:{
            type:Object,
            required:true
        },
        chapterList:{
            type:Array,
            required:true
        }

    },

    data(){
        return{
            selectedLabel:"简介",
            tabs:[
                {
                    label:"简介"
                },{
                    label:"目录"
                }
            ]
        }
    }
}

14. 视频详情页 summary 子组件开发

  • template
<template>
    <div> <img class="summary" :src="videoInfo.summary"/> </div>
</template>
  • script
export default {
    props:{
        videoInfo:{
            type:Object,
            required:true
        }
    }
}
  • style
.summary {
  width:100%;
  padding-bottom:50px;
  margin:15px 0;
}

15. 视频详情页 Catalog 目录子组件开发

  • template
<template>
    <div class="cate_box">

        <div>
            <!-- 双重for循环  1. 先遍历章的id  2. 后遍历节的id -->
            <ul class="content" v-for="(item, ind) in chapterList" :key="item.id">
              <h1> 第{{ind +1}}章 &nbsp;{{item.title}} </h1>

              <!-- 拿到对应章的id里面的节的id -->
              <li class="sub_cate" v-for="(item,subind) in chapterList[ind].episode_list" :key="item.id">
                <span class="sub_title">{{ind+1}}-{{subind+1}} &nbsp;{{item.title}}  </span>    
              </li>
            </ul>

        </div>

    </div>

</template>
  • script
export default {
    //从父组获取章集信息
    props:{
        chapterList:{
            type:Array,
            required:true
        }
    }

}
  • style
// 目录包裹层设置边距
.cate_box {
  padding: 0 15px 50px;
  background-color: #fff;
  margin: 15px 0;
}

//每一章包裹层
.content {
  padding: 10px;
  // 章标题
  & h1 {
    font-size: 16px;
    width: 100%;
    margin-bottom: 15px;
    font-weight: bolder;
    // 设置章标题过长,超过行宽度省略隐藏
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
  }
}

//集包裹层
.sub_cate {
  font-size: 12px;
  padding: 10px 0;
  //集标题
  .sub_title {
    // 设置集标题过长,超过行宽度省略隐藏
    display: block;
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
  }
}

16. 视频详情页 footer 立刻购买按钮开发

  • CourseDetail.vue
...
<!--底部立刻购买-->
<footer>
  <!-- 点击跳转链接 -->
  <!-- 传入对应的id -->
  <router-link
    :to="{ path: '/pay', query: { video_id: this.$route.query.video_id } }"
    class="user_buy"
  >
    <button>立刻购买</button>
  </router-link>
</footer>
...
<style lang="scss" scoped>
//底部
footer {
  // fixed固定在底部
  position: fixed;
  bottom: 0;
  width: 100%;
  padding: 8px 0;
  background-color: #fff;
  z-index: 999;
  box-shadow: 0 -2px 4px 0 rgba(0, 0, 0, 0.05);
}
//设置购买按钮样式
button {
  display: block;
  color: #fff;
  margin: 0 auto;
  background-color: #d93f30;
  height: 34px;
  line-height: 34px;
  border-radius: 17px;
  width: 80%;
  border: none;
  font-size: 15px;
  text-align: center;
}
</style>

17. 用户模块注册功能开发

17.1 Cube-UI 的 form 表单
<template>
  <div class="main">
    <cube-form :model="model" @submit="submitHandler">
      <cube-form-group>
        <!--名称-->
        <cube-form-item :field="fields[0]"></cube-form-item>
        <!--手机号-->
        <cube-form-item :field="fields[1]"></cube-form-item>
        <!--密码-->
        <cube-form-item :field="fields[2]"></cube-form-item>
      </cube-form-group>

      <cube-form-group>
        <cube-button type="submit">注册</cube-button>
      </cube-form-group>
    </cube-form>
    <!-- 跳转到登陆链接 -->
    <router-link to="/login" class="reg">登录</router-link>
  </div>
</template>
  • script
//注册接口
import { registerApi } from "@/api/getData.js";
export default {
  data() {
    return {
      model: {
        phoneValue: "",
        pwdValue: "",
        nameValue: "",
      },
      // 校验规则部分
      fields: [
        {
          type: "input",
          modelKey: "nameValue",
          label: "名称",
          props: {
            // 属性
            placeholder: "请输入名称",
          },
          rules: {
            // 规则
            required: true,
            notWhitespace: true,
          },
          messages: {
            // 错误信息
            required: "名称不能为空",
            notWhitespace: "名称不能为空白符",
          },
        },
        {
          type: "input",
          modelKey: "phoneValue",
          label: "手机号",
          props: {
            placeholder: "请输入手机",
          },
          rules: {
            required: true,
            len: 11,
            pattern: /^1[3456789]\d{9}$/,
          },
          messages: {
            pattern: "请输入正确的手机号",
          },
        },
        {
          type: "input",
          modelKey: "pwdValue",
          label: "密码",
          props: {
            placeholder: "请输入密码",
            type: "password",
            eye: {
              open: false,
            },
          },
          rules: {
            pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[^]{8,16}$/,
          },
          messages: {
            pattern: "密码至少8-16个字符,包含大小字母和数字",
          },
        },
      ],
    };
  },
  methods: {
    // 传入 model 参数
    submitHandler(e, model) {
      // preventDefault 方法
      // 取消事件的默认动作(阻止冒泡)
      e.preventDefault();
      //调用注册接口
      registerApi(model.phoneValue, model.pwdValue, model.nameValue).then(
        (res) => {
          if (res.data.code === 0) {
            // Toast组件主要用于非模态信息提醒,无需用户交互
            // time 字段决定了 Toast 显示的时间,如果设置为 0,则不会消失,需要手动调用组件的 hide 方法
            const toast = this.$createToast({
              txt: "注册成功",
              type: "correct",
              time: 1500,
              onTimeout: () => {
                this.$router.push({ path: "login" });
              },
            });
            toast.show();
          }
        }
      );
    },
  },
};
  • style
.main {
  padding: 50px 5% 0;
  text-align: center;
}
//注册
.cube-btn {
  margin-top: 20px;
}
// 登录
.reg {
  display: inline-block;
  margin-top: 30px;
  font-size: 18px;
}

18. 用户模块注登陆功能开发

<template>
  <div class="main">
    <cube-form :model="model" @submit="submitHandler">
      
      <cube-form-group>
       
        <!--手机号-->
        <cube-form-item :field="fields[0]"></cube-form-item>
        <!--密码-->
        <cube-form-item :field="fields[1]"></cube-form-item>
      </cube-form-group>

      <cube-form-group>
        <cube-button type="submit">登录</cube-button>
      </cube-form-group>

    </cube-form>
    <router-link to="/register" class="reg">注册</router-link> 
  </div>
</template>

<script>
//登录接口
import { loginApi } from "@/api/getData.js";
export default {
  data() {
    return {
      model: {
        phoneValue: "",
        pwdValue: ""
      },
      fields: [
        {
          type: "input",
          modelKey: "phoneValue",
          label: "手机号",
          props: {
            placeholder: "请输入手机"
          },
          rules: {
            required: true,
            len: 11,
            pattern: /^1[3456789]\d{9}$/,
          },
          messages: {
            required: "手机号不能为空",
            pattern: "请输入正确的手机号",
          }
        },
        {
          type: "input",
          modelKey: "pwdValue",
          label: "密码",
          props: {
            placeholder: "请输入密码",
            type: "password",
            eye: {
              open: false
            }
          },
          rules: {
            required: true,
            pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[^]{8,16}$/
          },
          messages: {
            required: "密码不能为空",
            pattern: "密码至少8-16个字符,包含大小字母和数字"
          }
        }
      ]
    };
  },
  methods: {
    submitHandler(e, model) {
      e.preventDefault();
      //调用注册接口
      loginApi(model.phoneValue, model.pwdValue).then(
        res => {
          if (res.data.code === 0) {
            // 登录成功,跳转到个人中心
            // 拿到 token,存储到本地
            localStorage.setItem('token',res.data.data)
            
            this.$store.dispatch('setToken',res.data.data)

            // 跳转页面, 根据业务需要
            this.$router.push({path:'/personal'})

          }else{
             const toast = this.$createToast({
              txt: "登录失败",
              type: "error",
              time: 1500
            });
            toast.show();
          }
        }
      );
    }
  }
};
</script>
<style lang="scss" scoped>
.main {
  padding: 50px 5% 0;
  text-align: center;
}
// 登录
.cube-btn {
  margin-top: 20px;
}
//注册
.reg {
  display: inline-block;
  margin-top: 30px;
  font-size: 18px;
}
</style>

19. Vuex 状态管理

19.1 vuex 是什么?
19.2 开发 store 中 index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    // 拿到 token 值
    token: localStorage.getItem("token") || ''
  },
  // 同步修改state里面的值
  mutations: {
    SET_TOKEN:(state, token)=>{
      state.token = token
    }
  },

  // 异步调用mutations里面的方法
  // contxt.commit 利用上下文触发mutations某个方法
  // vue代码里面 this.$store.dispatch触发action里面的定义的方法
  actions: {
    setToken(context,token){
      context.commit('SET_TOKEN',token)
    },

    clearToken(context){
      context.commit('SET_TOKEN','')
    }

  },
  modules: {
  }
})
19.3 整合 Login 登陆存储 token
  • Login.vue
methods: {
    submitHandler(e, model) {
      e.preventDefault();
      //调用登陆接口
      loginApi(model.phoneValue, model.pwdValue).then(
        res => {
          if (res.data.code === 0) {
            // 登录成功,跳转到个人中心
            // 拿到 token,存储到本地
            localStorage.setItem('token',res.data.data)
            
            // 触发 Vuex 定义的方法
            this.$store.dispatch('setToken',res.data.data)

            // 跳转页面, 根据业务需要
            this.$router.push({path:'/personal'})

          }else{
             const toast = this.$createToast({
              txt: "登录失败",
              type: "error",
              time: 1500
            });
            toast.show();
          }
        }
      );
    }
  }

20. 用户模块个人中心开发

<template>
  <div>
    <div class="container">
      <div class="p_top">
        <div>
            <!-- 如果用户头像没设置,就设置默认图片 -->
          <img :src="info.head_img || defaultHeadImg" alt="头像" />
          <!-- 判断用户是否登陆 token为空即未登陆,显示立即登陆,否则显示用户名-->
          <router-link to="/login" v-if="getToken === ''">
            <p>立刻登录</p>
          </router-link>
          <p v-else>{{ info.name }}</p>
        </div>
      </div>
      <!-- 判断用户token不等于空,才会退出登陆 -->
      <button v-if="getToken !== ''" class="green" @click="signOut">
        退出登录
      </button>
    </div>
    <common-footer></common-footer>
  </div>
</template>

<script>
import CommonFooter from "@/components/CommonFooter";
import { getUserInfo } from "@/api/getData.js";
import defaultHeadImg from "@/assets/logo.png";

export default {
  components: {
    CommonFooter,
  },

  data() {
    return {
      info: {},
      defaultHeadImg: defaultHeadImg,
    };
  },
  // 通过数据源获取token
  // 缓存功能
  computed: {
    getToken() {
      return this.$store.state.token;
    },
  },

  methods: {
    //获取用户信息
    async getInfo() {
      try {
        // 传入 token
        const result = await getUserInfo(this.getToken);
        if (result.data.code === 0) {
          this.info = result.data.data;
        }
      } catch (error) {
        console.log(error);
      }
    },

    //退出登录
    async signOut() {
      //清除token
      await this.$store.dispatch("clearToken");
      // 本地存储里清除token
      localStorage.removeItem("token");

      //刷新页面
      location.reload();
    },
  },

  mounted() {
    // 如果有token,就获取用户信息
    if (this.getToken) {
      this.getInfo();
    }
  },
};
</script>

<style lang="scss" scoped>
.container {
  // 顶部头像区域
  .p_top {
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 20px 0;
    background-color: #2c3f54;
    div {
      text-align: center;
      img {
        width: 60px;
        height: 60px;
        border-radius: 50px;
      }
      p {
        font-size: 16px;
        color: #fff;
        margin-top: 10px;
      }
    }
  }
}
// 退出登录
.green {
  display: block;
  background-color: #3bb149;
  border: none;
  outline: none;
  width: 80%;
  height: 40px;
  margin: 20px auto 0;
  color: #fff;
  border-radius: 20px;
}
</style>

21. 路由拦截功能开发

21. 前置守卫
// 路由拦截,拦截全部路由,每次操作路由都是被拦截进行判断
// 路由前置守卫
router.beforeEach((to,from,next)=>{
  // 从本地存储中拿token
  const token = localStorage.getItem("token")

  // 筛选需要传token的路由,匹配route里面需要登陆的路径,如果匹配到的就是true
  if( to.matched.some(record => record.meta.requiresAuth)){
    // 如果登陆了,执行下一步,没有登陆,跳转到登陆路由
    // 根据token是否有,判断是否需要调到登录页面
    if(token){
      next()
    }else{
      next({path:'/login'})
    }
  }else{
    next()
  }
})

22. 下单模块开发

<template>
  <div>
    <!--视频信息-->
    <div class="info">
      <p class="info_title">商品信息</p>
      <div class="box">
        <div class="imgdiv">
          <img alt="课程照片" :src="videoinfo.cover_img" />
        </div>
        <div class="textdiv">
          <p class="c_title">{{ videoinfo.title }}</p>
          <p class="price">
            ¥:&nbsp;&nbsp; {{ (videoinfo.price / 100).toFixed(2) }}
          </p>
        </div>
      </div>
    </div>
    <!--顶部支付-->
    <div class="footer">
      <p class="money">
        实付:&nbsp;&nbsp; {{ (videoinfo.price / 100).toFixed(2) }}
      </p>
      <p class="submit" @click="pay">立刻支付</p>
    </div>
  </div>
</template>

<script>
import { getVideoDetail, saveOrder } from "@/api/getData.js";

export default {
  data() {
    return {
      videoinfo: {},
    };
  },
  methods: {
    //获取视频详情
    async getDetail(vid) {
      try {
        const result = await getVideoDetail(vid);
        if (result.data.code == 0) {
          this.videoinfo = result.data.data;
        }
      } catch (error) {
        console.log(error);
      }
    },

    //下单
    async pay() {
      try {
        // 下单接口
        // 传入token,video_id
        const result = await saveOrder(
          this.$store.state.token,
          this.$route.query.video_id
        );

        if (result.data.code == 0) {
          const toast = this.$createToast({
            txt: "购买成功",
            type: "correct",
            time: 2000,
            // 跳转到订单页
            onTimeout: () => {
              this.$router.push({ path: "order" });
            },
          });
          toast.show();
        } else {
          const toast = this.$createToast({
            txt: "下单失败",
            type: "error",
            time: 1500,
          });
          toast.show();
        }
      } catch (error) {
        console.log(error);
      }
    },
  },
  mounted() {
    this.getDetail(this.$route.query.video_id);
  },
};
</script>


<style lang="scss" scoped>
// 视频标题
.info_title {
  padding: 10px 20px;
  background-color: #fff;
  border-bottom: 1px solid #d9dde1;
}

.box {
  background-color: #fff;
  box-sizing: border-box;
  padding: 20px;
  display: flex;
  margin-bottom: 15px;
  .imgdiv {
    width: 105px;
    height: 59px;
    flex-shrink: 0;
    img {
      width: 100%;
      height: 100%;
    }
  }

  .textdiv {
    margin-left: 20px;
    height: 59px;
    flex-grow: 1;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    .price {
      flex-shrink: 0;
    }
  }
}
//底部
.footer {
  position: fixed;
  bottom: 0;
  width: 100%;
  height: 50px;
  background-color: #fff;
  display: flex;
  justify-content: space-between;
  box-shadow: 0 -2px 4px 0 rgba(0, 0, 0, 0.1);
  font-size: 16px;
  .money {
    height: 50px;
    line-height: 50px;
    flex: 2;
    text-align: center;
    background-color: #fff;
  }
  .submit {
    height: 50px;
    line-height: 50px;
    flex: 1;
    text-align: center;
    background-color: #ff2d50;
    color: #fff;
  }
}
</style>

23. 订单模块开发

<template>
  <div class="main">
    <!--订单列表-->
    <!-- 如果有订单,就显示 -->
    <div class="list" v-if="orders.length > 0">
      <div class="box" v-for="(item, index) of orders" :key="index">
        <router-link
          :to="{ path: '/coursedetail', query: { video_id: item.video_id } }"
        >
          <div class="smallbox">
            <div class="imgdiv">
              <img :src="item.video_img" alt="小滴课堂课程图片" />
            </div>

            <div class="textdiv">
              <p class="title">{{ item.video_title }}</p>
              <p class="price">{{ (item.total_fee / 100).toFixed(2) }}</p>
            </div>
          </div>
        </router-link>
      </div>
    </div>

    <!-- 没有订单就显示 -->
    <div class="no_order" v-else>
      <p>暂未购买课程</p>
    </div>

    <!--底部导航-->
    <common-footer></common-footer>
  </div>
</template>
<script>
import CommonFooter from "@/components/CommonFooter";
import { getOrderList } from "@/api/getData.js";

export default {
  components: {
    CommonFooter,
  },

  data() {
    return {
      orders: [],
    };
  },

  methods: {
    //获取订单列表
    async getOrderList() {
      try {
        const result = await getOrderList(this.$store.state.token);
        if (result.data.code == 0) {
          this.orders = result.data.data || [];
        }
      } catch (error) {
        console.log(error);
      }
    },
  },
  mounted() {
    this.getOrderList();
  },
};
</script>

<style lang="scss" scoped>
.list {
  padding: 0 20px;
}

// 视频个体
.box {
  padding: 20px 0;
  background-color: #fff;
  border-bottom: 1px solid #ddd;
  // 标题
  .title {
    font-size: 14px;
    margin-bottom: 15px;
  }
  // 订单详情
  .smallbox {
    //flex左右排列,两端对齐
    display: flex;
    justify-content: space-between;
    .imgdiv {
      width: 90px;
      height: 69px;
      flex-shrink: 0;
      img {
        width: 100%;
        height: 100%;
        border-radius: 10px;
      }
    }
    .textdiv {
      width: 100%;
      p {
        width: 96%;
        margin-top: 10px;
        padding-left: 20px;
      }
    }
  }
}

.no_order {
  margin-top: 50px;
  text-align: center;
}
</style>

24. 前后端项目云服务器生产环境部署核心知识

24.1 应用部署到公网访问需要的知识
24.1.1 http 请求基本流程
  • 客户端通过发起域名资源请求 -> DNS 解析获得IP -> 寻找服务器获得资源
24.1.2 域名和 IP 的关系,DNS 作用
  • DNS
    • Domain Name Server 域名服务器
    • 域名虽然便于人们记忆,但网络中的计算机之间只能互相认识 IP 地址
    • 它们之间的转换工作称为域名解析
      • 域名解析需要由专门的域名解析服务器来完成
      • DNS 就是进行域名解析的服务器
24.1.3 什么是 cname 和 a 记录
  • a 记录
    • 用户可以在此设置域名指向到自己的目标主机地址
    • 从而实现通过域名找到服务器(也叫 ip 指向域名配置
  • cname
    • 别名指向,可以为一个主机设置别名
    • 比如设置 open1024.com,用来指向一个主机 xdclass.net,那么以后就可以用 open1024.com 来代替访问 xdclass.net
    • http://www.xdclass.net --> xdclass.net
24.1.4 购买服务器,阿里云,腾讯云,亚马逊云 aws
24.1.5 购买域名,备案
24.1.6 安装项目依赖的基本环境
24.1.7 配置域名解析到服务器
24.2 阿里云服务器远程登陆和常用工具
24.2.1 控制台修改阿里云远程连接密码
24.2.2 windows 工具
24.2.3 苹果系统 MAC
  • 终端登陆
    • ssh root@ip 回车后输入密码
    • cd / (根路径)
    • cd software/
    • cd 项目路径
    • pwd(查看当前文件夹的路径)
    • vim nginx.conf (编辑)
    • ../sbin/nginx -s reload
24.2.4 linux 图形操作工具 (用于远程连接上传文件)

25. 前端部署线上Linux云服务器

25.1 前端项目总体部署架构和阿里云域名解析A记录配置
  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值