基于
Vue
和Element-UI
的电商后台管理系统
用户登录/退出
用户管理
权限管理
商品管理
订单管理
数据统计
电商后台管理里系统整体采用前后端分离的开发模式,其中前端是基于
Vue
技术栈的 SPA 项目
Vue
脚手架Vue
脚手架创建项目Vue
路由Element-UI
组件库axios
库git
远程仓库Gitee
中布局代码
登录
重置
实现页面
用户在输入账号和密码后,点击登录时表单会进行预验证,判断用户输入的账号和密码是否符合规范,验证通过后向服务器发送
axios
请求
验证规则
// 用户名的验证规则
username: [
{ required: true, message: '请输入用户名称', trigger: 'blur' },
{ min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
],
// 密码的验证规则
password: [
{ required: true, message: '请输入用户密码', trigger: 'blur' },
{ min: 6, max: 16, message: '长度在 6 到 16 个字符', trigger: 'blur' }
]
实现效果
在登录成功后,服务器会向我们返回一个
token
,我们需要将这个token
保存到客户端的sessionStorage
中
发送请求并保存 token
注意
token
token
就证明我们已经登录了token
保存在 sessionStorage
中
sessionStorage
是会话期间的存储机制,关闭浏览器过后, sessionStorage
就会被清空,token
只应在当前网站打开期间生效,所以将 token
保存在 sessionStorage
中如果用户没有登录,但是直接通过 URL 访问特定页面,需要重新导航到登录页面。
在 index.js
中挂载路由导航守卫
// 挂载路由导航守卫
// to 代表将要访问的页面路径,from 代表从哪个页面路径跳转而来,next 代表一个放行的函数
router.beforeEach((to, from, next) => {
// 如果用户访问的是登录页,那么直接放行
if (to.path === '/login') return next()
// 获取 token
const tokenStr = window.sessionStorage.getItem('token')
// 没有 token,强制跳转到登录页面
if (!tokenStr) return next('/login')
next()
})
基于
token
的方式在退出时需要销毁本地的token
。这样,后续的请求就不会携带token
,必须重新登录生成一个新的token
之后才可以访问页面。
退出代码
logout() {
// 销毁本地 token
window.sessionStorage.removeItem('token')
// 通过编程式导航返回到上一页
this.$router.go(-1)
}
引入
Element-UI
中的Header
Aside
Main
组件
样式代码
实现效果
向服务器发送
axios
请求获取菜单数据
注意
Authorization
字段提供的 token
令牌,那些授权的 API 才能被正常调用Authorization
字段
axios
请求拦截器添加 token
,保证拥有获取数据的权限在 main.js
中添加拦截器
// axios 请求拦截
axios.interceptors.request.use(config => {
// 为请求头对象,添加 Token 验证的 Authorization 字段
config.headers.Authorization = window.sessionStorage.getItem('token')
// 最后必须 return config
return config
})
发起请求获取所有菜单数据
渲染到页面
<el-menu
background-color="#333744"
text-color="#fff"
active-text-color="#409Eff"
:collapse="isCollapse"
:collapse-transition="false"
router
:default-active="activePath">
<el-submenu :index="item.id + ''" v-for="item in menulist" :key="item.id">
<template slot="title">
<i :class="iconObj[item.id]">i>
<span>{{ item.authName }}span>
template>
<el-menu-item :index="'/' + secitem.path" v-for="secitem in item.children" :key="secitem.id" @click="savaNavState('/' + secitem.path)">
<i class="el-icon-menu">i>
<span>{{ secitem.authName }}span>
el-menu-item>
el-submenu>
el-menu>
通过
Element-UI
为菜单名称添加图标
实现效果
引入
Element-UI
中的Breadcrumb
,BreadcrumbItem
,Card
,Row
,Col
组件,实现面包屑导航和卡片视图
样式代码
<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>
el-card>
<style>
.el-breadcrumb {
margin-bottom: 15px;
font-size: 12px;
}
.el-card {
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1) !important;
}
style>
实现效果
向服务器发送请求获取用户数据列表
引入
Table
,TableColumn
将用户数据渲染到表格中,引入Pagination
实现分页效果
实现效果
引入
Dialog
结合表单展示一个添加用户的对话框
实现效果
为表单添加验证规则
实现效果
向服务器发送添加用户请求
MessageBox
提示用户实现效果
向服务器发送删除用户请求
实现效果
点击确定按钮向服务器发送编辑用户请求
queryInfo
中的 query
属性和输入框动态绑定,然后向服务器发送获取用户列表的请求布局和用户列表一致
向服务器发送请求获取权限数据列表
实现效果
布局和用户列表一致
向服务器发送请求获取角色数据列表
实现效果
Dialog
组件结合表单展示一个分配角色的对话框Select
、Option
组件展示一个选择角色的下拉选择器具体代码
实现效果
和用户的增删改查一致,只是调用接口不一样。
当用户点击某个角色的下拉箭头时,该角色的所有权限数据会以类似于树结构的形式展示出来。
用户也可以删除该角色下的某个权限。
效果如图
Scoped slot
可以开启展开行功能,el-table-column
的模板会被渲染成为展开行的内容,展开行可访问的属性与使用自定义列模板时的 Scoped slot
相同。scope.row
可以获取该行也就是该角色的数据
<el-table-column type="expand">
<template slot-scope="scope">
{{ scope.row }}
template>
el-table-column>
效果如图
布局思路
Element-UI
中的 Layout 布局,可以实现基础的 24 分栏,迅速简便地创建布局。业务逻辑
scope.row
获取的数据就是该角色的所有信息,数据是一个对象,每一个对象下都有一个 children
属性,这个 children
属性就是该角色的所有权限了,children
是一个数组,每一个 children
属性下又嵌套这一个 children
属性,一共嵌套三层,这分别就是该角色下的一级、二级、三级权限了。children
下的每个对象,就可以把一级权限渲染出来,在每一个一级权限中又嵌套着二级权限,所以,要想渲染出所有的一级、二级、三级权限需要使用三层 v-for
循环的嵌套。具体实现
引入
Tag
组件将权限名称以标签的形式展示,并且将closable
设置为true
,每个权限标签后面就会显示一个叉号,为后面的删除权限功能做铺垫。为每一个权限标签后面添加
进行美化。
<el-table-column type="expand">
<template slot-scope="scope">
<el-row :class="['bdbottom', i1 === 0 ? 'bdtop' : '', 'vcenter']" v-for="(item1,i1) in scope.row.children" :key="item1.id" class="first-row">
<el-col :span="5">
<el-tag closable @close="removeRightById(scope.row, item1.id)">{{ item1.authName }}el-tag>
<i class="el-icon-caret-right">i>
el-col>
<el-col :span="19">
<el-row :class="[i2 === 0 ? '' : 'bdtop', 'vcenter']" v-for="(item2,i2) in item1.children" :key="item2.id">
<el-col :span="6">
<el-tag type="success" closable @close="removeRightById(scope.row, item2.id)">{{ item2.authName }}el-tag>
<i class="el-icon-caret-right">i>
el-col>
<el-col :span="18">
<el-tag v-for="item3 in item2.children" :key="item3.id" type="warning" closable @close="removeRightById(scope.row, item3.id)">{{ item3.authName }}el-tag>
el-col>
el-row>
el-col>
el-row>
template>
el-table-column>
<style>
// 添加边框
// 上边框
.bdtop {
border-top: 1px solid #eee;
}
// 下边框
.bdbottom {
border-bottom: 1px solid #eee;
}
// 上下居中
.vcenter {
display: flex;
align-items: center;
}
style>
效果如图
使用
MessageBox
提示用户
点击确定按钮时分别将该角色的信息和权限 id 作为参数传递过来
引用 Tag
组件将权限列表渲染成树形结构
效果如下
提交分配权限
布局
和角色列表布局一致
表格部分引用第三方组件 vue-table-with-tree-grid
(树形表格组件),可以使商品分类以树形结构分级展示
安装 vue-table-with-tree-grid
npm i vue-table-with-tree-grid -S
在 main.js
中引入 vue-table-with-tree-grid
// 引入列表树
import TreeTable from 'vue-table-with-tree-grid'
// 使用列表树
Vue.component('tree-table', TreeTable)
以组件组件标签的形式使用 vue-table-with-tree-grid
向服务器发送请求获取商品分类数据列表
效果如图
设置自定义列
设置自定义列需要将 columns
绑定的对应列的 type
属性设置为 template
,将 template
属性设置为当前列使用的模板名称
一级
二级
三级
编辑
删除
效果如下
添加分类
编辑分类
删除分类
布局
获取商品分类列表
获取商品分类列表,并渲染到级联选择器当中
当用户选中商品分类时,向服务器发送请求获取商品参数了列表并在表格中展示该分类的所有参数
效果如图
添加分类参数
分类参数包括动态参数和静态属性
当用户点击添加参数/属性时,弹出对话框
因为添加参数和添加属性的对话框布局一样,所以可以共用一个对话框
提交添加操作
删除分类参数
当用户点击删除按钮时,通过作用域插槽获取当前分类参数的 id
提交删除操作
编辑分类参数
id
传递过来id
作为参数向服务器发送请求获取当前要编辑的分类参数的名称
编辑
效果如图
添加分类参数的属性
Tag
组件,循环渲染每一个标签
{{ item }}
+ New Tag
效果如图
+ New Tag
按钮展示文本框,并且隐藏按钮Enter
键后,向服务器发送请求,提交此次添加操作
删除分类参数的属性
id
作为参数传递过来id
作为参数向服务器发送删除请求
添加商品页面布局
Step
步骤条组件,使用 Tab
标签页
效果如图
checkbox-group
复选框组组件展示参数
效果如图
效果如图
Upload
上传组件实现图片上传的功能
点击上传
效果如图
vue-quill-editor
富文本编辑器插件main.js
中导入并注册// 导入富文本编辑器
import VueQuillEditor from 'vue-quill-editor'
// 导入富文本编辑器对应的样式
import 'quill/dist/quill.core.css' // import styles
import 'quill/dist/quill.snow.css' // for snow theme
import 'quill/dist/quill.bubble.css' // for bubble theme
// 使用富文本编辑器
Vue.use(VueQuillEditor)
添加商品
效果如图
提交添加商品操作
已付款
未付款
{{ scope.row.create_time | dateFormat }}
效果如图
citydata.js
包,渲染到表单的级联选择器当中
取 消
确 定
效果如图
Timeline
时间线组件API
无法使用,这里使用了 Mock.js
根据接口文档的响应数据模拟了查看物流进度的接口// 使用 Mock
var Mock = require('mockjs')
var menuMock = Mock.mock({
data: [
{
time: '2018-05-10 09:39:00',
ftime: '2018-05-10 09:39:00',
context: '已签收,感谢使用顺丰,期待再次为您服务',
location: ''
},
{
time: '2018-05-10 08:23:00',
ftime: '2018-05-10 08:23:00',
context: '[北京市]北京海淀育新小区营业点派件员 顺丰速运 95338正在为您派件',
location: ''
},
{
time: '2018-05-10 07:32:00',
ftime: '2018-05-10 07:32:00',
context: '快件到达 [北京海淀育新小区营业点]',
location: ''
},
{
time: '2018-05-10 02:03:00',
ftime: '2018-05-10 02:03:00',
context: '快件在[北京顺义集散中心]已装车,准备发往 [北京海淀育新小区营业点]',
location: ''
},
{
time: '2018-05-09 23:05:00',
ftime: '2018-05-09 23:05:00',
context: '快件到达 [北京顺义集散中心]',
location: ''
},
{
time: '2018-05-09 21:21:00',
ftime: '2018-05-09 21:21:00',
context: '快件在[北京宝胜营业点]已装车,准备发往 [北京顺义集散中心]',
location: ''
},
{
time: '2018-05-09 13:07:00',
ftime: '2018-05-09 13:07:00',
context: '顺丰速运 已收取快件',
location: ''
},
{
time: '2018-05-09 12:25:03',
ftime: '2018-05-09 12:25:03',
context: '卖家发货',
location: ''
},
{
time: '2018-05-09 12:22:24',
ftime: '2018-05-09 12:22:24',
context: '您的订单将由HLA(北京海淀区清河中街店)门店安排发货。',
location: ''
},
{
time: '2018-05-08 21:36:04',
ftime: '2018-05-08 21:36:04',
context: '商品已经下单',
location: ''
}
],
meta: { status: 200, message: '获取物流信息成功!' }
})
Mock.mock('http://127.0.0.1:8888/api/private/v1/mock/process', 'get', menuMock)
{{item.context}}
效果如图
Apache ECharts
数据可视化插件
效果如图
生成打包报告
通过命令行参数的形式生成报告
// 通过 vue-cli 的命令选项可以生成打包报告
// --report 选项可以生成 report.html 以帮助分析打包内容
vue-cli-service build --report
通过可视化的 UI 面板直接查看报告 推荐
在可视化的 UI 面板中,通过**控制台**和分析面板,可以方便地看到项目中所存在的问题
通过 externals 加载外部 CDN 资源
默认情况下,通过 import 语法导入的第三方依赖包最终会被打包合并到同一个文件中,从而导致打包成功后,单文件体积过大大的问题。
为了解决上述的问题,可以通过 webpack 的 externals 节点,来配置并加载外部的 CDN 资源。
module.exports = {
chainWebpack:config=>{
//发布模式
config.when(process.env.NODE_ENV === 'production',config=>{
//entry找到默认的打包入口,调用clear则是删除默认的打包入口
//add添加新的打包入口
config.entry('app').clear().add('./src/main-prod.js')
//使用externals设置排除项
config.set('externals',{
vue:'Vue',
'vue-router':'VueRouter',
axios:'axios',
lodash:'_',
echarts:'echarts',
nprogress:'NProgress',
'vue-quill-editor':'VueQuillEditor'
})
})
//开发模式
config.when(process.env.NODE_ENV === 'development',config=>{
config.entry('app').clear().add('./src/main-dev.js')
})
}
设置好排除之后,为了使我们可以使用vue,axios等内容,我们需要加载外部CDN的形式解决引入依赖项。
main-prod.js
,删除掉默认的引入代码import Vue from 'vue'
import App from './App.vue'
import router from './router'
// import './plugins/element.js'
//导入字体图标
import './assets/fonts/iconfont.css'
//导入全局样式
import './assets/css/global.css'
//导入第三方组件vue-table-with-tree-grid
import TreeTable from 'vue-table-with-tree-grid'
//导入进度条插件
import NProgress from 'nprogress'
//导入进度条样式
// import 'nprogress/nprogress.css'
// //导入axios
import axios from 'axios'
// //导入vue-quill-editor(富文本编辑器)
import VueQuillEditor from 'vue-quill-editor'
// //导入vue-quill-editor的样式
// import 'quill/dist/quill.core.css'
// import 'quill/dist/quill.snow.css'
// import 'quill/dist/quill.bubble.css'
axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'
//请求在到达服务器之前,先会调用use中的这个回调函数来添加请求头信息
axios.interceptors.request.use(config => {
//当进入request拦截器,表示发送了请求,我们就开启进度条
NProgress.start()
//为请求头对象,添加token验证的Authorization字段
config.headers.Authorization = window.sessionStorage.getItem("token")
//必须返回config
return config
})
//在response拦截器中,隐藏进度条
axios.interceptors.response.use(config =>{
//当进入response拦截器,表示请求已经结束,我们就结束进度条
NProgress.done()
return config
})
Vue.prototype.$http = axios
Vue.config.productionTip = false
//全局注册组件
Vue.component('tree-table', TreeTable)
//全局注册富文本组件
Vue.use(VueQuillEditor)
//创建过滤器将秒数过滤为年月日,时分秒
Vue.filter('dateFormat',function(originVal){
const dt = new Date(originVal)
const y = dt.getFullYear()
const m = (dt.getMonth()+1+'').padStart(2,'0')
const d = (dt.getDate()+'').padStart(2,'0')
const hh = (dt.getHours()+'').padStart(2,'0')
const mm = (dt.getMinutes()+'').padStart(2,'0')
const ss = (dt.getSeconds()+'').padStart(2,'0')
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
})
new Vue({
router,
render: h => h(App)
}).$mount('#app')
DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>电商后台管理系统title>
<link rel="stylesheet" href="https://cdn.staticfile.org/nprogress/0.2.0/nprogress.min.css" />
<link rel="stylesheet" href="https://cdn.staticfile.org/quill/1.3.4/quill.core.min.css" />
<link rel="stylesheet" href="https://cdn.staticfile.org/quill/1.3.4/quill.snow.min.css" />
<link rel="stylesheet" href="https://cdn.staticfile.org/quill/1.3.4/quill.bubble.min.css" />
<link rel="stylesheet" href="https://cdn.staticfile.org/element-ui/2.8.2/theme-chalk/index.css" />
<script src="https://cdn.staticfile.org/vue/2.5.22/vue.min.js">script>
<script src="https://cdn.staticfile.org/vue-router/3.0.1/vue-router.min.js">script>
<script src="https://cdn.staticfile.org/axios/0.18.0/axios.min.js">script>
<script src="https://cdn.staticfile.org/lodash.js/4.17.11/lodash.min.js">script>
<script src="https://cdn.staticfile.org/echarts/4.1.0/echarts.min.js">script>
<script src="https://cdn.staticfile.org/nprogress/0.2.0/nprogress.min.js">script>
<script src="https://cdn.staticfile.org/quill/1.3.4/quill.min.js">script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue-quill-editor.js">script>
<script src="https://cdn.staticfile.org/element-ui/2.8.2/index.js">script>
head>
<body>
<noscript>
<strong>We're sorry but vue_shop doesn't work properly without JavaScript enabled. Please enable it to continue.strong>
noscript>
<div id="app">div>
body>
html>
Element-UI 组件按需加载
路由懒加载
首页内容定制