目录
1.电商业务概述
根据不同的应用场景,电商系统一般都提供了 PC 端、移动 APP、移动 Web、微信小程序等多种终端访问方式。
- 客户使用的业务服务:PC端,小程序,移动web,移动app
- 管理员使用的业务服务:PC后台管理端。
- PC后台管理端的功能:管理用户账号(登录,退出,用户管理,权限管理),商品管理(商品分类,分类参数,商品信息,订单),数据统计
- 电商后台管理系统采用前后端分离(后端写接口,前端调接口)的开发模式
- 前端项目是基于Vue的SPA(单页应用程序)项目
- 前端技术栈:Vue,Vue-Router,Element-UI,Axios,Echarts
- 后端技术栈:Node.js,Express,Jwt(模拟session(登录记录功能),状态保持工具),Mysql,Sequelize(操作数据库的框架)
一般电商系统是由不同终端组成,共用同一份数据库,同一份接口服务器,通过PC后台管理来管理不同终端访问的数据。
2.电商后台管理系统的功能
电商后台管理系统用于管理用户账号、商品分类、商品信息、订单、数据统计等业务功能。
3.项目初始化步骤
- A.安装Vue脚手架
-
npm i -g @vue/cli
- B.通过脚手架创建项目
-
//使用可视化的形式创建项目 vue ui
手动配置项目->Babel,Router,Linter/Formatter(代码格式校验,风格一致),使用配置文件
-
- C.配置路由
- D.配置Element-UI:在插件中安装,搜索vue-cli-plugin-element(根据自己需要,选择导入方式:按需导入)
- E.配置Axios:在依赖中安装,搜索axios(运行依赖)
- F.初始化git仓库
- 创建仓库,用管理员运行终端,进行Git 全局设置(创建仓库成功后,会显示需要执行的命令)
- G.将本地项目托管到github或者码云中
- 打开项目实战的文件夹,打开powerShell终端,执行git status检查项目状态;在将所有文件放入暂存区git add . ;在本地做一次提交 git commit -m "add files"
- 检查状态,正处于主分支,工作区干净的git status
这只是本地仓库,选择需要跟gittee仓库连通
- 执行gitee简易的命令行内容
git remote add origin https://gitee.com...git git push -u origin "master"
4.后台项目的环境安装配置
A.使用Navicat导入mysql数据库数据
B.安装Node.js环境,配置后台项目,从终端打开后台项目vue_api_server 然后在终端中输入命令安装项目依赖包:npm install,在执行node .\app.js
C.使用postman测试api接口
4.1. API V1 接口说明
-
接口基准地址:
http://127.0.0.1:8888/api/private/v1/
-
服务端已开启 CORS 跨域支持
-
API V1 认证统一使用 Token 认证
-
需要授权的 API ,必须在请求头中使用
Authorization
字段提供token
令牌 -
使用 HTTP Status Code 标识状态
-
数据返回格式统一使用 JSON
4.2. 支持的请求方法
-
GET(SELECT):从服务器取出资源(一项或多项)。
-
POST(CREATE):在服务器新建一个资源。
-
PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
-
PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
-
DELETE(DELETE):从服务器删除资源。
-
HEAD:获取资源的元数据。
-
OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。
4.3. 通用返回状态说明
状态码 | 含义 | 说明 |
---|---|---|
200 | OK | 请求成功 |
201 | CREATED | 创建成功 |
204 | DELETED | 删除成功 |
400 | BAD REQUEST | 请求的地址不存在或者包含不支持的参数 |
401 | UNAUTHORIZED | 未授权 |
403 | FORBIDDEN | 被禁止访问 |
404 | NOT FOUND | 请求的资源不存在 |
422 | Unprocesable entity | [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误 |
500 | INTERNAL SERVER ERROR | 内部错误 |
5.测试后台接口是否正常
修改mysql数据库密码为自己电脑数据库的密码
在default.json中有后端的数据库配置文件,本机中的数据库名称,账号,密码与数据库配置文件中的保持一致,后台服务器才能访问到该数据库。
6.登录/退出功能
1.登录概述
1.登录业务流程
- 在登录页面输入用户名和密码
- 调用后台接口进行验证
- 通过验证之后,根据后台的响应状态跳转到项目主页
2.登录业务的相关技术点
- http是无状态的
- 通过cookie在客户端记录状态
- 通过session在服务器端记录状态
- 通过token方式维持状态
存在跨域问题,推荐使用token;不存在跨域问题,推荐使用cookie,session。
- A.登录状态保持 如果服务器和客户端同源,建议可以使用cookie或者session来保持登录状态 如果客户端和服务器跨域了,建议使用token进行维持登录状态。
- B.登录逻辑: 在登录页面输入账号和密码进行登录,将数据发送给服务器 服务器返回登录的结果,登录成功则返回数据中带有token 客户端得到token并进行保存,后续的请求都需要将此token发送给服务器,服务器会验证token以保证用户身份。
- C.添加新分支login,在login分支中开发当前项目vue_shop: 打开vue_shop终端,使用git status确定当前项目状态。 确定当前工作目录是干净的之后,创建一个分支进行开发,开发完毕之后将其合并到master git checkout -b login 然后查看新创建的分支:git branch 确定我们正在使用login分支进行开发
3.token原理
2.登录功能实现
登录页面的布局,通过——Element-UI组件实现布局
- el-form
- el-form-item
- el-input
- el-button
- 字体图标
1.终端执行git status:查看当前工作区是否干净
2.用分支开发登录功能
创建分支
git checkout -b login
查看分支
git branch
3.查看项目运行效果
点击任务->serve运行->启动app
4.删除不需要的组件
index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
]
const router = new VueRouter({
routes
})
export default router
App.vue
<template>
<div id="app">App根组件</div>
</template>
<script>
export default {
name: 'app'
}
</script>
<style>
</style>
5.创建登录组件
components/Login.vue
<template>
<div>登录组件</div>
</template>
src/router/index.js
import Login from '@/components/Login.vue'
const routes = [
{ path: '/login', component: Login }
]
App.vue
<template>
<div id="app">App根组件
<!-- 放路由占位符 -->
<router-view></router-view>
</div>
</template>
component name XXX should always be multi-word 报错
.eslintrc.js
// 在rules中添加自定义规则
rules: {
// 关闭组件命名规则
// 'vue/multi-word-component-names': 'off'
// 关闭命名规则,不会校验组件名;最好设置:根据组件名进行忽略
// 忽略部分组件名
// 添加组件命名忽略规则
"vue/multi-word-component-names": ["error", {
"ignores": ["Login"]//需要忽略的组件名
}]
}
重定向规则,让它默认为login页面
const routes = [
{ path: '/', redirect: '/login' },
{ path: '/login', component: Login }
]
6.设置背景颜色
<template>
<div class="login_container">登录组件</div>
</template>
<style lang="less" scoped>
.login_container {
background-color: pink;
height: 100%;
}
.login_box {
width: 450px;
height: 300px;
background-color: #fff;
border-radius: 3px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
</style>
报错Can't resolve 'less-loader' in,需要添加依赖
终端下载依赖(开发依赖)---版本出错
npm i less@3.9.0 less-loader@4.1.0 --save-dev
vue ui下载开发依赖less和less-loader ---正常使用
src/assets/css/global.css
/* 全局样式表 */
html,
body,
#app{
height: 100%;
margin: 0;
padding:0;
}
main.js
// 导入全局样式表
import './assets/css/global.css'
7.组件头像布局
<div class="login_container">
<div class="login_box">
<div class="avatar_box">
<img src="../assets/logo.png" alt="" />
</div>
</div>
</div>
.avatar_box {
height: 130px;
width: 130px;
border: 1px solid #eee;
border-radius: 50%;
padding: 10px;
box-shadow: 0 0 10px #ddd;
position: absolute;
left: 50%;
transform: translate(-50%, -50%);
background-color: #fff;
img {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #eee;
}
}
8.登录组件表单布局
<!-- 登录表单区域 -->
<el-form label-width="0" class="login_form">
<!-- 用户名 -->
<el-form-item>
<el-input prefix-icon="el-icon-search"></el-input>
</el-form-item>
<!-- 密码 -->
<el-form-item>
<el-input prefix-icon="el-icon-search"></el-input>
</el-form-item>
<!-- 按钮区域 -->
<el-form-item class="btns">
<el-button type="primary">登录</el-button>
<el-button type="info">重置</el-button>
</el-form-item>
</el-form>
.login_form {
position: absolute;
bottom: 0;
// 只占左边一半,需要设置宽度
width: 100%;
padding: 0 20px;
// 超出区域了,需要设置border-box
box-sizing: border-box;
}
.btns {
// 居右对齐
display: flex;
justify-content: flex-end;
}
src/plugins/element.js
import { Button, Form, FormItem, Input } from 'element-ui'
//Vue.use(Button, Form, FormItem, Input)错误,要分别使用
Vue.use(Button)
Vue.use(Form)
Vue.use(FormItem)
Vue.use(Input)
9.表单小图标布局
prefix-icon="el-icon-search"
elment图标不完整,使用iconfont图标下载图标形式,使用图标,
- 将图标文件夹fonts放入assets文件夹
- 在main.js中导入字体图标
-
// 导入字体图标 import './assets/fonts/iconfont.css'
- 在Login.vue进行使用
-
<el-form-item> <el-input prefix-icon="iconfont icon-user"></el-input> </el-form-item> <el-form-item> <el-input prefix-icon="iconfont icon-3702mima"></el-input> </el-form-item>
10.表单的数据绑定
- 1.el-form添加:model属性绑定,指向一个数据对象
- 2.为每一个表单项通过v-model绑定到数据对象上对应的属性中
<el-form :model="loginForm">
data () {
return {
// 这是登录表单的数据绑定对象
loginForm:{
username: '',
password: ''
}
}
},
<el-input v-model="loginForm.username"></el-input>
<el-input v-model="loginForm.password"type="password"></el-input>
11.表单的数据验证
- 1.为el-form通过属性绑定,指定一个:rules校验对象
- 2.在data数据中定义这个校验对象,每一个属性都是一个验证规则
- 3.为不同的表单item项el-form-item,通过props指定不同的验证规则,来进行表单的验证
<el-form :rules="loginFormRules">
// 这是表单的验证规则对象
loginFormRules: {
// 验证用户名是否合法
username: [
{ required: true, message: '请输入登录名称', trigger: 'blur' },
{ min: 3, max: 10, message: '长度在 3 到 10 个字符', trigger: 'blur' }
],
// 验证密码是否合法
password: [
{ required: true, message: '请输入登录密码', trigger: 'blur' },
{ min: 6, max: 15, message: '长度在 6 到 15 个字符', trigger: 'blur' }
]
}
<el-form-item prop="username">
<el-form-item prop="password">
12.表单的重置
- 为表单添加ref引用,ref的值就是组件的实例对象(拿到表单的实例对象,使用ref绑定组件(ref相当于给当前标签取名字,用这个名字可以直接获取到这个元素并且操作它里面的数据))
- 重置按钮绑定事件,通过this访问到$refs,点出表单的引用对象loginFormRef,直接调用resetFields( )方法
- 即可重置表单
<el-form ref="loginFormRef">
<el-button type="info" @click="resetLoginForm">重置</el-button>
methods: {
// 点击重置按钮,重置登录表单
resetLoginForm () {
console.log(this)// 通过this,可以直接获取引用对象(就是这个组件的实例)
this.$refs.loginFormRef.resetFields()
}
}
13.登录前的预验证
当点击登录按钮,要进行一下验证,验证通过才能进行登录。
- 为登录按钮绑定点击事件login
- 调用表单的引用对象loginFormRef,调用validate函数进行验证,接收一个回调函数,从而拿到验证结果
- 符合要求则输出true,不符合要求则输出false(布尔值)
<el-button type="primary" @click="login">登录</el-button>
login () {
this.$refs.loginFormRef.validate((valid) => {
console.log(valid)
})
}
14.根据预验证是否发起请求
login () {
this.$refs.loginFormRef.validate(valid => {
// console.log(valid)
// valid为false则return,不发起请求
if (!valid) return
})
}
添加到vue.prototype.$http后,可以在每一个组件上通过this.$http获取axios,不必在每个页面都import axios,$http是自定义属性名,可以随便起名。
main.js
// 对axios进行全局配置
import axios from 'axios'
// 配置请求根路径
axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'
Vue.prototype.$http = axios
返回结果是promise,可以用async await来简化promise操作。
只有data才是服务器返回的数据,可以直接解构data
login () {
this.$refs.loginFormRef.validate(async valid => {
// console.log(valid)
// valid为false则return,不发起请求
if (!valid) return
// loginForm当成请求参数,因为用户填写的数据会同步到loginForm
// const result = await this.$http.post('login', this.loginForm)
// console.log(result)
const { data: res } = await this.$http.post('login', this.loginForm)
console.log(res)
if (res.meta.status !== 200) return console.log('登录失败')
console.log('登录成功')
})
}
15.配置弹窗提示
src/plugins/element.js
// 导入弹框提示组件
import { Message } from 'element-ui'
// 在Message挂载到Vue原型上,每个组件都可以直接通过this,访问到Message
Vue.prototype.$message = Message
if (res.meta.status !== 200) return this.$message.error('登录失败')
this.$message.success('登录成功')
16.登录成功后的行为
- 将登录后的token,保存到客户端的sessionStorage中
- 项目中除了登录之外的其他API接口,必须在登录之后才能访问
- token只应在当前网站打开期间生效,所以将token保存在sessionStorage中
- 通过编程式导航跳转到后台主页,路由地址是 /home
// 1.将登录后的token,保存到客户端的sessionStorage中
// 1.1项目中除了登录之外的其他API接口,必须在登录之后才能访问
console.log(res)
// 1.2token只应在当前网站打开期间生效,所以将token保存在sessionStorage中
window.sessionStorage.setItem('token', res.data.token)
// 2.通过编程式导航跳转到后台主页,路由地址是/home
this.$router.push('/home')
17.创建home组件
18.路由导航守卫控制访问权限
只允许在登录成功后才能看见home组件。
如果用户没有登录,但是直接通过URL访问特定页面,需要重新导航到登录页面。
- to:将要访问的那个页面路径
- from:从哪个页面路径跳转而来的
- next:放行函数
//为路由对象,添加beforeEach导航守卫
router.beforeEach((to,from,next)=>{
//如果用户访问的登录页,直接放行
if(to.path === '/login') return next()
//从sessionStorage中获取到保存的token值
const tokenStr = window.sessionStorage.getItem('token')
//没有token,强制跳转到登录页
if(!tokenStr) return next('/login')
next()
})
src/router/index.js
// 挂载路由导航守卫
router.beforeEach((to, from, next) => {
// to 将要访问的路径
// from 代表从哪个路径跳转而来
// next 是一个函数,表示放行
// next() 放行 next('/login') 强制跳转
if (to.path === '/login') return next()
// 获取token
const tokenStr = window.sessionStorage.getItem('token')
if (!tokenStr) return next('/login')
next()
})
19.退出功能的实现
基于token的方式实现退出比较简单,只需要摧毁本地的token即可。这样,后续的请求就不会携带token,必须重新登录生成一个新的token之后才可以访问。
//清空token
window.sessionStorage.clear()
//跳转到登录页,重定向到登录页
this.$router.push('/login')
Home.vue
<template>
<div>
<el-button type="info" @click="logout">退出</el-button>
</div>
</template>
logout () {
window.sessionStorage.clear()
this.$router.push('/login')
}
20.语法警告问题(无问题)
shift+alt+f格式化文件
创建格式化配置文件.prettierrc
{
// 末尾不加分号
"semi": false,
// 启用单引号的格式化方式
"singleQuote": true
}
.eslintrrc.js关闭某语法规则
'space-before-function-paren': 0
我在setting json中已经全局设置了
21.提交登录功能代码
1.git status 检查当前代码
2.git add . 添加代码到暂存区
3.git status
4.git commit -m "完成了登录功能" 把暂存区的代码提交到本地仓库中
5.git branch 检查所处分支
6.把login分支代码合并到master主分支中
- 1.切换到master主分支
- git checkout master
- 检查发现处于master分支
- 2.再从主分支合并login分支
- git merge login
7.远程推送,把本地的master分支推送到远程的码云中 git push
8.把本地的login分支推送到远程码云中(远程码云只有master分支)
- git checkout login 切换到login子分支
- 检查分支状态 git branch
- 推送到码云中,需要使用 git push -u origin login(将本地的login分支推送到远程的origin仓储里面,并且叫做login子分支,进行保存)
7.主页部分
1.主页布局
Home.vue
<el-container>
<!-- 头部区域 -->
<el-header>
<el-button type="info" @click="logout">退出</el-button>
</el-header>
<!-- 页面主体区域 -->
<el-container>
<!-- 侧边栏 -->
<el-aside width="200px">Aside</el-aside>
<!-- 右侧内容主体 -->
<el-main>Main</el-main>
</el-container>
</el-container>
<style lang="less" scoped>
// 没有撑满全屏,需要添加home-container
.home-container {
height: 100%;
}
.el-header {
background-color: rgb(69, 14, 65);
}
.el-aside {
background-color: rgb(165, 59, 154);
}
.el-main {
background-color: rgb(241, 192, 241);
}
</style>
element.js注册组件
import { Container,Header,Aside,Main } from 'element-ui'
Vue.use(Container)
Vue.use(Header)
Vue.use(Aside)
Vue.use(Main)
1.主页Header布局
左右布局,flex布局。
<el-header>
<div>
<img src="../assets/logo.png" alt="" />
<span>电商后台管理系统</span>
</div>
<el-button type="info" @click="logout">退出</el-button>
</el-header>
.el-header {
background-color: rgb(69, 14, 65);
display: flex;
justify-content: space-between;
padding-left: 0;
align-items: center;
color: #fff;
font-size: 20px;
// 嵌套div
> div {
display: flex;
align-items: center;
span {
margin-left: 15px;
}
}
img {
height: 50px;
}
}
2.左侧菜单布局
<el-aside width="200px">
<!-- 侧边栏菜单区 -->
<el-menu
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
>
<el-submenu index="1">
<template slot="title">
<i class="el-icon-location"></i>
<span>导航一</span>
</template>
<el-menu-item-group>
<template slot="title">分组一</template>
<el-menu-item index="1-1">选项1</el-menu-item>
<el-menu-item index="1-2">选项2</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="分组2">
<el-menu-item index="1-3">选项3</el-menu-item>
</el-menu-item-group>
<el-submenu index="1-4">
<template slot="title">选项4</template>
<el-menu-item index="1-4-1">选项1</el-menu-item>
</el-submenu>
</el-submenu>
<el-menu-item index="2">
<i class="el-icon-menu"></i>
<span slot="title">导航二</span>
</el-menu-item>
<el-menu-item index="3" disabled>
<i class="el-icon-document"></i>
<span slot="title">导航三</span>
</el-menu-item>
<el-menu-item index="4">
<i class="el-icon-setting"></i>
<span slot="title">导航四</span>
</el-menu-item>
</el-menu>
</el-aside>
按需导入element.js
import { Menu, Submenu, MenuItem } from 'element-ui'
Vue.use(Menu)
Vue.use(Submenu)
Vue.use(MenuItem)
梳理结构
<!-- 侧边栏 -->
<el-aside width="200px">
<!-- 侧边栏菜单区 -->
<el-menu
background-color="rgb(165, 59, 154)"
text-color="#fff"
active-text-color="rgb(69, 14, 65)"
>
<!-- 一级菜单 -->
<el-submenu index="1">
<!-- 一级菜单的模板区域 -->
<template slot="title">
<!-- 图标 -->
<i class="el-icon-location"></i>
<!-- 文本 -->
<span>导航一</span>
</template>
<!-- 二级菜单 -->
<el-menu-item index="1-4-1">
<template slot="title">
<!-- 图标 -->
<i class="el-icon-location"></i>
<!-- 文本 -->
<span>导航一</span>
</template>
</el-menu-item>
</el-submenu>
</el-menu>
</el-aside>
3.通过接口获取菜单数据
接口说明:需要授权的API,必须在请求头中使用Authorization字段提供token令牌。
所以需要:通过axios请求拦截器添加token,保证拥有获取数据的权限。
后台除了登录接口之外,都需要token权限验证,我们可以通过添加axios请求拦截器来添加token,以保证拥有获取数据的权限
//axios请求拦截
//请求在到达服务器之前,先会调用use中的这个回调函数来添加请求头信息
axios.interceptors.request.use(config => {
//为请求头对象,添加Token验证的Authorization字段
config.headers.Authorization = window.sessionStorage.getItem('token')
return config
})
在重新登录或会获取到此数据
发起login的登录请求,request请求头Headers中包括Authorization:null(登录期间是null,服务器没有颁发令牌;如果登录之后,再用其他接口,此时这个值就不是null)(符合要求才会执行请求 ,不符合要求或者请求不存在,则服务器驳回请求)
main.js
// 配置请求根路径
axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'
// 在挂载到原型对象之前,先设置一个拦截器
axios.interceptors.request.use(config => {
config.headers.Authorization = window.sessionStorage.getItem('token')
console.log(config)// 获取headers数据
// 在最后必须要return config
return config
})
Vue.prototype.$http = axios
这时,有权限的API就能调用成功了。
4.发起请求获取左侧菜单数据
左侧菜单权限
- 请求路径:menus
- 请求方法:get
- 响应数据
{
"data":
{
"id": 101,
"authName": "商品管理",
"path": null,
"children": [
{
"id": 104,
"authName": "商品列表",
"path": null,
"children": []
}
]
}
"meta": {
"msg": "获取菜单列表成功",
"status": 200
}
}
请求左侧菜单:在刚一加载时就应该获取左侧菜单(所以在created时要进行调用)。
data () {
return {
// 左侧菜单数据
menulist: []
}
},
created () {
this.getMenuList()
},
methods: {
// 获取所有的菜单
async getMenuList () {
const { data: res } = await this.$http.get('menus')
// 获取到的数据立即挂载到自己的data中,赋值
if (res.meta.status !== 200) return this.$message.error(res.meta.msg)
this.menulist = res.data
console.log(res)
}
}
5.渲染UI结构
<!-- 一级菜单 -->
<!-- index相同的话,点开其中一个,所有都会展开,每一个都应该有一个独属于自己的index值:报错,index只接收字符串不接收数值,所有需要转成字符串 +'' -->
<el-submenu
:index="item.id + ''"
v-for="item in menulist"
:key="item.id"
>
<!-- 一级菜单的模板区域 -->
<template slot="title">
<!-- 图标 -->
<i class="el-icon-location"></i>
<!-- 文本 -->
<span>{{ item.authName }}</span>
</template>
<!-- 二级菜单 -->
<el-menu-item
:index="subItem.id + ''"
v-for="subItem in item.children"
:key="subItem.id"
>
<template slot="title">
<!-- 图标 -->
<i class="el-icon-location"></i>
<!-- 文本 -->
<span>{{ subItem.authName }}</span>
</template>
</el-menu-item>
</el-submenu>
6.左侧菜单格式美化
二级菜单小图标修改;一级菜单图标,每一个一级菜单图标都不一样,element不能满足需求,使用iconfont,将字体图标按照顺序更改到一级菜单图标上。在自动生成期间,加上图标,使用data初始化图标,以id为key,id对应图标为它的值。
<i :class="iconObj[item.id]"></i>
data () {
return {
// 左侧菜单数据
menulist: [],
iconObj: {
125: 'iconfont icon-user',
103: 'iconfont icon-tijikongjian',
101: 'iconfont icon-shangpin',
102: 'iconfont icon-shangpin',
145: 'iconfont icon-baobiao'
}
}
},
.iconfont {
margin-right: 10px;
}
如果想要实现,点击一个一级菜单,再点击第二个一级菜单,之前点击的一级菜单收回情况(每次只展开一个),可以通过修改下面属性
<el-menu
background-color="rgb(165, 59, 154)"
text-color="#fff"
active-text-color="rgb(69, 14, 65)"
unique-opened
>
解决细微的对齐问题
.el-aside {
background-color: rgb(165, 59, 154);
.el-menu {
border-right: none;
}
}
7.左侧菜单的折叠和展开功能
//侧边栏不能跟着宽度变化,需要动态绑定在isCollapse。折叠,则为64px;不折叠,则为200px
<el-aside :width="isCollapse ? '64px' : '200px'">
<div class="toggle-button" @click="toggleCollaapse">|||</div>
<el-menu
:collapse="isCollapse"
:collapse-transition="false"
>
// 是否折叠
isCollapse: false
// 点击按钮,切换菜单的折叠和展开
toggleCollaapse () {
this.isCollapse = !this.isCollapse
}
.toggle-button {
background-color: rgb(217, 119, 225);
font-size: 10px;
line-height: 24px;
color: #fff;
text-align: center;
letter-spacing: 0.2em;
cursor: pointer;
}
8.首页的路由重定向
登录成功后,在main区域显示欢迎页面;home路由中嵌套显示了main子路由组件。
只要访问了home这个地址,就显示Welcome子路由规则。
router/index.js
{
path: '/home',
component: Home,
redirect: '/welcome',
children: [
{ path: '/welcome', component: Welcome }
]
}
Home.vue
<!-- 右侧内容主体 -->
<el-main>
<!-- 路由占位符 -->
<router-view></router-view>
</el-main>
9.左侧菜单改为路由链接
把每一个设置为router-link(麻烦),通过element router属性完成,为整个侧边栏开启路由模式。(简单)
<!-- 侧边栏菜单区 -->
<el-menu
router
>
path作为唯一的跳转路径合适,比id好;path不是以‘/’开头的,需要我们对齐补充
<!-- 二级菜单 -->
<el-menu-item
:index="'/' + subItem.path"
>
点击二级菜单可以进行跳转
10.用户列表开发
router.js
import Users from '@/components/user/Users.vue'
{
path: '/home',
component: Home,
redirect: '/welcome',
children: [
{ path: '/welcome', component: Welcome },
{ path: '/users', component: Users }
]
}
此时点击用户列表,右侧会显示用户列表的组件内容
解决无高亮现象(高亮效果的动态切换)
点击链接时把对应的地址保存到sessionStorage中,在刷新时(home杠创建的时候)就立即把值取出来,赋值到左边菜单即可。
default-active="/users"//写死
- 给二级菜单绑定单击事件
- 定义点击事件函数,保存链接的激活状态(把对应的index值保存到sessionStorage中)
- 把值重新取出来,在data中定义数据activePath,保存激活的链接
- 把activePath地址动态保存到el-menu上(:default-active="activePath")
- 给它动态赋值,在整个home组件一被创建时,就把sessionStorage中的值取出来,给它赋值上去
- 在点击不同链接时,应该为activePath重新赋值
<!-- 侧边栏菜单区 -->
<el-menu
:default-active="activePath"
>
<!-- 二级菜单 -->
<el-menu-item
@click="saveNavState('/' + subItem.path)"
>
// 被激活的链接地址
activePath: ''
created () {
this.activePath = window.sessionStorage.getItem('activePath')
}
// 保存链接的激活状态
saveNavState (activePath) {
window.sessionStorage.setItem('activePath', activePath)
this.activePath = activePath
}
点击用户列表,就会多一个键
保存记录你所选择的path(提留的界面),然后储存在sessionStorage中,重新加载时就从sessionStorage中查看你的历史记录,你浏览了其他的path则需要通过sessionStorage更新一下。
11.用户列表的基本UI结构
import { Breadcrumb, BreadcrumbItem, Card, Row, Col } from 'element-ui'
Vue.use(BreadcrumbItem)
Vue.use(Breadcrumb)
Vue.use(Card)
Vue.use(Row)
Vue.use(Col)
<div>
<!-- 面包屑导航区域 -->
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>用户管理</el-breadcrumb-item>
<el-breadcrumb-item>用户列表</el-breadcrumb-item>
</el-breadcrumb>
<!-- 卡片视图区域 -->
<el-card class="box-card">
<!-- 搜索与添加区域 -->
<el-row :gutter="20">
<el-col :span="8">
<el-input placeholder="请输入内容">
<el-button slot="append" icon="el-icon-search"></el-button>
</el-input>
</el-col>
<el-col :span="4">
<el-button type="primary">添加用户</el-button>
</el-col>
</el-row>
</el-card>
</div>
src/assets/css/global.css
.el-breadcrumb {
margin-bottom: 15px;
font-size: 12px;
}
.el-card {
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15) !important;
}
12.获取用户列表数据
// 获取用户列表的参数对象
queryInfo: {
query: '',
pagenum: 1,
pagesize: 2
},
userlist: [],
total: 0
created () {
this.getUserList()
},
async getUserList () {
const { data: res } = await this.$http.get('users', { params: this.queryInfo })
if (res.meta.status !== 200) {
return this.$message.error('获取用户列表失败!')
}
this.userlist = res.data.users
this.total = res.data.total
console.log(res)
}
13.渲染用户列表数据
import {Table,TableColumn } from 'element-ui'
Vue.use(Table)
Vue.use(TableColumn)
<!-- 用户列表区域 -->
<el-table :data="userlist" border stripe>
//label:当前页的标题;prop:指向这一列的数据
<el-table-column label="姓名" prop="username"></el-table-column>
<el-table-column label="邮箱" prop="email"></el-table-column>
<el-table-column label="电话" prop="mobile"></el-table-column>
<el-table-column label="角色" prop="role_name"></el-table-column>
<el-table-column label="状态" prop="mg_state"></el-table-column>
<el-table-column label="操作"></el-table-column>
</el-table>
src/assets/css/global.css
.el-table {
margin-top: 15px;
font-size: 12px;
}
13.用户列表添加索引列
<el-table-column type="index" label="#"></el-table-column>
14.状态列的按需展示
通过作用域插槽来渲染当前状态列。
- 先在状态列中定义一个作用域插槽,通过slot-scope接收当前作用域的数据
- 通过scope.row拿到对应这一行的数据
- 需要把整个开关的状态绑定到哪个属性身上,v-model上绑定具体地方属性值即可
- 此时,prop="mg_state"可以删除,作用域插槽会覆盖prop
import { Switch } from 'element-ui'
Vue.use(Switch)
<el-table-column label="状态">
<template slot-scope="scope">
<!-- {{ scope.row }} -->
<!-- 通过作用域插槽接收了scope,scope.row就等于这一行对应的数据 -->
<el-switch v-model="scope.row.mg_state"> </el-switch>
</template>
</el-table-column>
15.插槽形式自定义操作列的渲染
import { Tooltip } from 'element-ui'
Vue.use(Tooltip)
<el-table-column label="操作" width="180">
<template slot-scope="">
<!-- 修改按钮 -->
<el-button
type="primary"
icon="el-icon-edit"
size="mini"
></el-button>
<!-- 删除按钮 -->
<el-button
type="danger"
icon="el-icon-delete"
size="mini"
></el-button>
<!-- 分配角色按钮 -->
<el-tooltip
effect="dark"
content="分配角色"
placement="top-start"
:enterable="false"
>
<el-button
type="warning"
icon="el-icon-setting"
size="mini"
></el-button>
</el-tooltip>
</template>
</el-table-column>
16.数据的分页形式
使用element ui分页组件。
- 按需导入这个分页组件
- 定义 size-change的handleSizeChange事件( 监听最新的pagesize),定义current-change的handleCurrentChange事件(监听最新的页码值pagenum)
- 只要页码值pagenum或者pagesize发生改变,就立即发起一次新的数据请求
- 通过不同的属性配置页码条
import { Pagination } from 'element-ui'
Vue.use(Pagination)
<!-- 分页区域 -->
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="queryInfo.pagenum"
:page-sizes="[1, 2, 5, 10]"
:page-size="queryInfo.pagesize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
>
</el-pagination>
// 获取用户列表的参数对象
queryInfo: {
query: '',
// 当前的页数
pagenum: 1,
// 当前每页显示多少条数据
pagesize: 2
},
userlist: [],
total: 0
// 监听pagesize改变的事件
handleSizeChange (newsize) {
// console.log(newsize)
this.queryInfo.pagesize = newsize
this.getUserList()
},
// 监听页码值改变的事件,可以拿到最新的页码值
handleCurrentChange (newPage) {
// console.log(newPage)
this.queryInfo.pagenum = newPage
this.getUserList()
}
assets/css/global.css
.el-pagination {
margin-top: 15px;
}
17.用户状态的修改
把用户状态同步到数据库中进行保存。
-
监听switch开关状态的改变,从而拿到状态
-
调用对应的API接口,把最新的状态保存到数据库中
<!-- 只要调用了这个函数,就可以把当前用户的信息传过去,因为当前switch是通过v-model双向绑定到了scope.row.mg_state上,只要状态改变了,mg_state也会改变,所以直接把这一行的数据scope.row传到函数userStateChanged中 -->
<el-switch
v-model="scope.row.mg_state"
@change="userStateChanged(scope.row)">
</el-switch>
// 监听switch开关状态的改变
async userStateChanged (userinfo) {
console.log(userinfo)
const { data: res } = await this.$http.put(`users/${userinfo.id}/state/${userinfo.mg_state}`)
// 如果获取用户状态失败了,应该把状态重置回去
if (res.meta.status !== 200) {
userinfo.mg_state = !userinfo.mg_state
return this.$message.error('更新用户状态失败!')
}
this.$message.success('更新用户状态成功')
}
修改用户状态的API
-
请求路径:users/:uId/state/:type
-
请求方法:put
-
请求参数
参数名 | 参数说明 | 备注 |
---|---|---|
uId | 用户 ID | 不能为空携带在url中 |
type | 用户状态 | 不能为空携带在url中 ,值为 true 或者 false |
-
响应数据
{
"data": {
"id": 566,
"rid": 30,
"username": "admin",
"mobile": "123456",
"email": "bb@itcast.com",
"mg_state": 0
},
"meta": {
"msg": "设置状态成功",
"status": 200
}
}
18.实现搜索的功能
- 在input上使用v-model双向绑定queryInfo
- 为搜索按钮绑定点击事件getUserList,点击了一次这个按钮,就会发起一次查询用户的请求
<el-input
placeholder="请输入内容"
v-model="queryInfo.query"
clearable
@clear="getUserList">
<el-button
@click="getUserList"
></el-button>
</el-input>
19.实现添加用户的功能
- 按需导入对话框组件
- 复制粘贴对话框结构
- 点击按钮可以隐藏对话框和显示对话框
import { Dialog } from 'element-ui'
Vue.use(Dialog)
<el-button type="primary" @click="addDialogVisible = true"
<!-- 添加用户的对话框 -->
<el-dialog
title="提示"
:visible.sync="addDialogVisible"
width="50%"
>
<!-- 内容主体区域 -->
<span>这是一段信息</span>
<!-- 底部区域 -->
<span slot="footer" class="dialog-footer">
<el-button @click="addDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="addDialogVisible = false"
>确 定</el-button
>
</span>
</el-dialog>
// 控制添加用户对话框的显示和隐藏
addDialogVisible: false
20.添加用户对话框中渲染一个添加用户的表单
<!-- 内容主体区域 -->
<el-form
:model="addForm"
:rules="addFormRules"
ref="addFormRef"
label-width="70px"
>
<el-form-item label="用户名" prop="username">
<el-input v-model="addForm.username"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="addForm.password"></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="addForm.email"></el-input>
</el-form-item>
<el-form-item label="手机" prop="mobile">
<el-input v-model="addForm.mobile"></el-input>
</el-form-item>
</el-form>
// 添加用户的表单数据
addForm: {
username: '',
password: '',
email: '',
mobile: ''
},
// 添加表单的验证规则对象
addFormRules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur'},
{ min: 3, max: 10, message: '用户名的长度在3-10个字符之间', trigger: 'blur'}
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur'},
{ min: 6, max: 15, message: '用户名的长度在6-15个字符之间', trigger: 'blur'}
],
email: [
{required: true, message: '请输入邮箱', trigger: 'blur'}
],
mobile: [
{required: true, message: '请输入手机号', trigger: 'blur'}
]
}
21.实现自定义校验规则
- 定义一个箭头函数,写自定义校验规则
- 在具体的校验规则中,通过validator来使用上面自定义校验规则
data () {
// 验证邮箱的规则
// rule:规则;value:待检验的value值;cb:回调函数
const checkEmail = (rule, value, cb) => {
// 验证邮箱的正则表达式
const regEmail = /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(\.[a-zA-Z0-9_-])+/
if (regEmail.test(value)) {
// 合法的邮箱
return cb()
}
cb(new Error('请输入合法的邮箱'))
}
// 验证手机号的规则
const checkMobile = (rule, value, cb) => {
// 验证手机号的正则表达式
const regMobile = /^(0|86|17951)?(13[0-9]|15[0123456789]|17[678]|18[0-9]|14[57])[0-9]{8}$/
// 如果时合法的手机号,则直接return cb()
if (regMobile.test(value)) {
return cb()
}
// 不合法,调用cb,传递错误对象
cb(new Error('请输入合法的手机号'))
}
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur'},
{validator: checkEmail, trigger: 'blur'}
],
mobile: [
{required: true, message: '请输入手机号', trigger: 'blur'},
{ validator: checkMobile, trigger: 'blur'}
]
}
22.添加用户表单的重置功能
监听对话框的关闭事件以及对话框的重置事件。
<el-dialog @close="addDialogClosed">
// 监听添加用户对话框的关闭事件
addDialogClosed () {
this.$refs.addFormRef.resetFields()
}
- 要监听对话框的close事件
- 在close事件的处理函数中,拿到表单的引用$refs
- 调用resetFields( )方法
23.添加用户的预验证功能
在点击确认按钮后,不应该直接关闭对话框,应该在点击这个按钮的时候调用函数,在函数中对整个表单进行预验证。
<el-button @click="addUser">确 定</el-button>
// 点击按钮添加新用户
addUser () {
this.$refs.addFormRef.validate(valid => {
// 接收校验结果
console.log(valid)// 布尔值
if (!valid) return
// 可以发起添加用户的网络请求
})
}
24.发起请求添加一个新用户
-
请求路径:users
-
请求方法:post
-
请求参数(跟addForm相同)
参数名 | 参数说明 | 备注 |
---|---|---|
username | 用户名称 | 不能为空 |
password | 用户密码 | 不能为空 |
邮箱 | 可以为空 | |
mobile | 手机号 | 可以为空 |
-
响应参数
参数名 | 参数说明 | 备注 |
---|---|---|
id | 用户 ID | |
rid | 用户角色 ID | |
username | 用户名 | |
mobile | 手机号 | |
邮箱 |
- 响应数据
{
"data": {
"id": 28,
"username": "tige1200",
"mobile": "test",
"type": 1,
"openid": "",
"email": "test@test.com",
"create_time": "2017-11-10T03:47:13.533Z",
"modify_time": null,
"is_delete": false,
"is_active": false
},
"meta": {
"msg": "用户创建成功",
"status": 201
}
}
// 点击按钮添加新用户
addUser () {
this.$refs.addFormRef.validate(async valid => {
// 接收校验结果
console.log(valid)// 布尔值
if (!valid) return
// 可以发起添加用户的网络请求
const { data: res } = await this.$http.post('users', this.addForm)
if (res.meta.status !== 201) {
this.$message.error('添加用户失败!')
}
this.$message.success('添加用户成功!')
// 隐藏添加用户的对话框
this.addDialogVisible = false
// 刷新列表,重新获取用户列表数据
this.getUserList()
})
}
25.添加用户修改的操作
为修改按钮绑定点击事件,点击按钮就会弹出修改的对话框。
<!-- 修改按钮 -->
<el-button @click="showEditDialog()"></el-button>
<!-- 修改用户的对话框 -->
<el-dialog title="修改用户" :visible.sync="editDialogVisible" width="50%">
<span>这是一段信息</span>
<span slot="footer" class="dialog-footer">
<el-button @click="editDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="editDialogVisible = false"
>确 定</el-button>
</span>
</el-dialog>
// 控制修改用户对话框的显示与隐藏
editDialogVisible: false
// 展示编辑用户的对话框
showEditDialog () {
this.editDialogVisible = true
}
26.根据id查询到用户信息
根据用户的id查询到用户的旧数据,并且保存起来。
- 先在调用方法的时候,把用户的id通过scope.row.id将其传进去
- 接收到id后,调用API 接口查询用户信息
- 把查询到的用户信息存到editForm表单数据对象上
<template slot-scope="scope">
<!-- 修改按钮 -->
<el-button
type="primary"
icon="el-icon-edit"
size="mini"
@click="showEditDialog(scope.row.id)"
></el-button>
// 查询到的用户信息对象
editForm: {}
// 展示编辑用户的对话框
async showEditDialog (id) {
console.log(id)
const { data: res } = await this.$http.get('users/' + id)
if (res.meta.status !== 200) {
return this.$message.error('查询用户信息失败!')
}
// 给它进行赋值
this.editForm = res.data
this.editDialogVisible = true
}
-
请求路径:users/:id
-
请求方法:get
-
请求参数
参数名 | 参数说明 | 备注 |
---|---|---|
id | 用户 ID | 不能为空携带在url中 |
-
响应参数
参数名 | 参数说明 | 备注 |
---|---|---|
id | 用户 ID | |
role_id | 角色 ID | |
mobile | 手机号 | |
邮箱 |
-
响应数据
{
"data": {
"id": 503,
"username": "admin3",
"role_id": 0,
"mobile": "00000",
"email": "new@new.com"
},
"meta": {
"msg": "查询成功",
"status": 200
}
}
27.绘制修改用户的表单
- 通过element ui把表单项粘贴过来
- 提供:model="editForm"; :rules="editFormRules";ref="editFormRef"
-
提供修改表单的验证规则对象editFormRules
<el-form
:model="editForm"
:rules="editFormRules"
ref="editFormRef"
label-width="70px"
>
<el-form-item label="用户名">
<el-input v-model="editForm.username" disabled></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="editForm.email"></el-input>
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input v-model="editForm.mobile"></el-input>
</el-form-item>
</el-form>
// 修改表单的验证规则对象
editFormRules: {
email: [
{
required: true,
message: '请输入用户邮箱',
trigger: 'blur'
},
{ validator: checkEmail, trigger: 'blur' }
],
mobile: [
{
required: true,
message: '请输入用户手机',
trigger: 'blur'
},
{ validator: checkMobile, trigger: 'blur' }
]
}
28.修改表单的关闭后的重置操作
- 监听对话框的close事件
- 在close事件中应该重置表单
<el-dialog @close="editDialogClosed">
// 监听修改用户对话框的关闭事件
editDialogClosed () {
this.$refs.editFormRef.resetFields()
}
29.修改用户信息的操作
-
提交修改之前表单预验证操作
-
然后再修改用户信息的操作
<el-button type="primary" @click="editUserInfo">确 定</el-button>
// 修改用户信息并提交
editUserInfo () {
this.$refs.editFormRef.validate(async valid => {
// console.log(valid)
if (!valid) return
// 发起修改用户信息的数据请求
const { data: res } = await this.$http.put('users/' + this.editForm.id, {
email: this.editForm.email,
mobile: this.editForm.mobile
})
if (res.meta.status !== 200) {
return this.$message.error('更新用户信息失败!')
}
// 关闭对话框
this.editDialogVisible = false
// 刷新数据列表
this.getUserList()
// 提示修改成功
this.$message.success('更新用户信息成功!')
})
}
-
请求路径:users/:id
-
请求方法:put
-
请求参数(editForm修改的数据)
参数名 | 参数说明 | 备注 |
---|---|---|
id | 用户 id | 不能为空 参数是url参数:id |
邮箱 | 可以为空 | |
mobile | 手机号 | 可以为空 |
-
响应参数
参数名 | 参数说明 | 备注 |
---|---|---|
id | 用户 ID | |
role_id | 角色 ID | |
mobile | 手机号 | |
邮箱 |
-
响应数据
/* 200表示成功,500表示失败 */
{
"data": {
"id": 503,
"username": "admin3",
"role_id": 0,
"mobile": "111",
"email": "123@123.com"
},
"meta": {
"msg": "更新成功",
"status": 200
}
}
30.实现删除用户的操作
plugins/element.js
import { MessageBox } from 'element-ui'
//在每一个组件中都可以通过this.$confirm来弹出确认弹出的提示框
Vue.prototype.$confirm = MessageBox.confirm
<!-- 删除按钮 -->
<el-button @click="removeUserById(scope.row.id)"></el-button>
// 根据Id删除对应的用户信息
async removeUserById (id) {
console.log(id)
// 弹框询问用户是否需要删除数据
const confirmResult = await this.$confirm('此操作将永久删除该用户, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).catch(err => err)
// (err => err)为(err => {return err})的简写形式
// 如果用户确认删除,则返回值为字符串confirm
// 如果用户取消删除,则返回值为字符串cancel
console.log(confirmResult)
if (confirmResult !== 'confirm') {
return this.$message.info('已取消删除')
}
// 在终端里显示了确认删除
//console.log('确认了删除')
const { data: res } = await this.$http.delete('users/' + id)
if (res.meta.status !== 200) {
return this.$message.error('删除用户失败!')
}
this.$message.success('删除用户成功!')
// 删除成功之后,需要刷新用户列表,直接调用getUserList()
this.getUserList()
}
31.提交用户列表功能
提交代码到git仓库。
- 检查当前所处的分支 git branch
- 创建新分支并切换checkout到新分支上 git checkout -b user
- 再所有修改操作添加到暂存区 git status检查当前user分支文件的状态 git add .
- 检查状态git status 所有文件都被添加到了暂存区
- 将user提交到仓库中 git commit -m "完成用户列表功能的开发"
- 检查状态 git status
- 本地user的代码就是最新的了
- 码云中还没有记录user分支,检查当前所处的分支 git branch
- 把本地分支推送到云端(第一次推送新分支user)git push -u origin user (把本地的user分支推送到云端的origin仓库中,同时以user分支来进行保存)
- 此时master主分支还是旧的
- 检查当前所处分支 git branch
- 切换到master主分支 git checkout master
- 检查是否处于master主分支 git branch
- 从主分支上把user代码合并过来 git merge user
- 把本地的master分支代码推送到云端 git push (有master分支,不是第一次了)