Vue2.6+VueCli4.3+CubeUI 完成小D课堂移动端
- 0. 项目效果
- 1. 搭建项目架构
- 2. 项目目录结构创建
- 3. 基于浏览器和 nodeJS 的 http 客户端 Axios 模块
- 4. Axios 封装通用后端请求 API 模块
- 5. Vue-Router 开发路由
- 6. 通用底部选项卡 CommonsFooter 开发
- 7. 首页 Home 模块开发
- 8. 首页轮播图 Banner 模块开发
- 9. 首页视频列表 VideoList 模块开发
- 10. 视频详情 CourseDetail 模块开发
- 11. 视频详情页 Header 模块开发
- 12. 视频详情页 Course 模块开发
- 13. 视频详情页 Tab 模块开发
- 14. 视频详情页 summary 子组件开发
- 15. 视频详情页 Catalog 目录子组件开发
- 16. 视频详情页 footer 立刻购买按钮开发
- 17. 用户模块注册功能开发
- 18. 用户模块注登陆功能开发
- 19. Vuex 状态管理
- 20. 用户模块个人中心开发
- 21. 路由拦截功能开发
- 22. 下单模块开发
- 23. 订单模块开发
- 24. 前后端项目云服务器生产环境部署核心知识
- 25. 前端部署线上Linux云服务器
0. 项目效果
1. 搭建项目架构
创建
vue 项目vue create iclass-web
- 选择
feature 模式
,安装vuex/vue-router
- 安装
axios
npm install axis --save
npm install <package_name> --save
- 表示将这个包名及对应的版本添加到
package.json
的dependencies
- 表示将这个包名及对应的版本添加到
npm install <package_name> --save-dev
- 表示将这个包名及对应的版本添加到
package.json
的devDependencies
- 表示将这个包名及对应的版本添加到
- 安装
- 添加
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
用于浏览器
和nodeJS
的http 客户端
- 支持
浏览器
和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>
script 开发
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 知识点
async
用于声明一个function
是异步
的await
用于等待一个异步方法执行完成
(发起请求,如果查询数据库,发起 http 等)- 参考文章
// 导入组件模块
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 模块开发
- 轮播图组件
cube-slide
讲解 - 代码编写
<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 讲解
- 用于
路径跳转
- 文档:https://router.vuejs.org/zh/api/#router-link-props
template
<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
-
开发
CourseDetail
的template
<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>
- 开发
CourseDetail
的script
//引入组件
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}}章 {{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}} {{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 是什么?
- vuex 是适用于在 Vue 项目开发时使用的
状态管理工具
- 官方文档:https://vuex.vuejs.org/zh/
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
里面配置需要登陆的路由
meta: {requiresAuth: true}
main.js
里面配置路由拦截
// 路由拦截,拦截全部路由,每次操作路由都是被拦截进行判断
// 路由前置守卫
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">
¥: {{ (videoinfo.price / 100).toFixed(2) }}
</p>
</div>
</div>
</div>
<!--顶部支付-->
<div class="footer">
<p class="money">
实付: {{ (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 工具
putty
xshell
security
参考资料
24.2.3 苹果系统 MAC
终端登陆
ssh root@ip
回车后输入密码- cd / (根路径)
- cd software/
- cd 项目路径
pwd
(查看当前文件夹的路径)vim nginx.conf
(编辑)../sbin/nginx -s reload
24.2.4 linux 图形操作工具 (用于远程连接上传文件)
Mac: filezilla
windows: winscp
资料