此项目为前后端分离的电商后台管理项目,其主要的功能模块为:
本项目是基于Vue的SPA(单页应用程序)项目,其所用到的
前端技术栈包括:Vue,Vue-Router,Element-UI,Axios,Echarts,下面将介绍各页面的开发逻辑和核心代码。
在项目开始前,需要先清理不必要的组件并配置好基本的路由结构。用户首先进入到的是登录页,所以先创建Login.vue页面,然后在router.js中导入组件并设置规则、在App.vue中添加路由占位符:
const router = new Router({
routes: [
{ path: '/', redirect: '/login' },
{ path: '/login', component: Login }
]
})
实现用户登录功能的逻辑是:
①在登录页面输入账号和密码进行登录,将数据发送给服务器
②服务器返回登录的结果,登录成功则返回数据中带有token
③客户端得到token并进行保存,后续的请求都需要将此token发送给服务器,服务器会验证token以保证用户身份。
登录表单的结构如下:
登录
重置
表单中输入的数据将被绑定到data中,同时还需要在data中定义表单的校验规则,且 Form-Item 的 prop 属性设置为需校验的字段名:
//表单验证规则
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'
}
]
}
登录功能需要向服务器发送数据请求,用到了axios,所以先在main.js文件中对axios进行全局的配置:
import axios from 'axios'
// 配置请求的根路径
axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'
// 为axios设置拦截器
axios.interceptors.request.use(config => {
// 为config做预处理,为请求头对象添加验证字段token,有权限的API就可以正常使用了
config.headers.Authorization = window.sessionStorage.getItem('token')
return config
})
Vue.prototype.$http = axios
登录的结果需要进行弹窗提示,所以也需要提前进行挂载:
// 将弹框组件全局挂载
Vue.prototype.$message = Message
当点击登录按钮时,首先调用validate方法验证表单内容是否有误,然后发送请求进行登录(需要查看接口文档),用await方法简化promise操作:
login() {
//点击登录的时候先调用validate方法验证表单内容是否有误
this.$refs.LoginFormRef.validate(async valid => {
console.log(this.loginFormRules)
//如果valid参数为true则验证通过
if (!valid) {
return
}
//发送请求进行登录
const { data: res } = await this.$http.post('login', this.loginForm)
// console.log(res);
if (res.meta.status !== 200) {
return this.$message.error('登录失败:' + res.meta.msg) //console.log("登录失败:"+res.meta.msg)
}
this.$message.success('登录成功')
console.log(res)
//保存token
window.sessionStorage.setItem('token', res.data.token)
// 导航至/home
this.$router.push('/home')
})
}
}
注意:使用sessionStorage保存token,关闭页面即会被清除!!!
需创建跳转到的home页面,这里不加赘述。
然后在router.js中添加路由守卫,如果用户没有登录,则不能访问/home,如果用户通过url地址直接访问,则强制跳转到登录页面:
//挂载路由导航守卫,to表示将要访问的路径,from表示从哪里来,next是下一个要做的操作
router.beforeEach((to,from,next)=>{
if(to.path === '/login')
return next();
//获取token
const tokenStr = window.sessionStorage.getItem('token');
if(!tokenStr)
return next('/login');
next();
})
退出事件绑定在home页面的按钮上:
logout(){
window.sessionStorage.clear();
this.$router.push('/login');
}
首先构建出首页的基本页面结构,这里用到了elementUI里的多个组件。
头部区域的基本构造如下:
电商后台管理系统
退出
侧边栏使用多级菜单的结构,构造如下:
导航一
子菜单一
axios请求拦截器
后台除了登录接口之外,都需要token权限验证,我们可以通过添加axios请求拦截器来添加token,以保证拥有获取数据的权限。在main.js中添加代码,在将axios挂载到vue原型之前添加下面的代码:
//请求在到达服务器之前,先会调用use中的这个回调函数来添加请求头信息
axios.interceptors.request.use(config=>{
//为请求头对象,添加token验证的Authorization字段
config.headers.Authorization = window.sessionStorage.getItem("token")
return config
})
在生命周期created中获取侧边栏所需要的数据:
async getMenuList() {
// 发送请求获取左侧菜单数据
const { data: res } = await this.$http.get('menus')
if (res.meta.status !== 200) return this.$message.error(res.meta.msg)
this.menuList = res.data
console.log(res)
}
利用获取到的数据进行侧边栏菜单渲染,使用双层v-for进行循环:
{{item.authName}}
{{subItem.authName}}
新增子级路由组件Welcome.vue,在router.js中导入子级路由组件,并设置路由规则以及子级路由的默认重定向:
path: '/home',
component: Home,
// 一开启就显示欢迎页面
redirect: '/welcome',
children: [{
// welcome组件是home的子组件
path: '/welcome',
component: Welcome
}]
制作好了Welcome子级路由之后,我们需要将所有的侧边栏二级菜单都改造成子级路由链接:只需要将el-menu的router属性设置为true就可以了,此时当我们点击二级菜单的时候,就会根据菜单的index属性(:index=“‘/’+subItem.path”)进行路由跳转!!!
使用element-ui面包屑组件完成顶部导航路径:
首页
用户管理
用户列表
使用element-ui卡片组件完成主体表格,再使用element-ui输入框完成搜索框及搜索按钮,此时我们需要使用栅格布局来划分结构。然后再使用el-button制作操作的按钮:
添加用户
获取数据,然后使用表格组件进行渲染:
async getUserList() {
//发送请求获取用户列表数据
const { res: data } = await this.$http.get('users', {
params: this.queryInfo
})
//如果返回状态为异常状态则报错并返回
if (res.meta.status !== 200)
return this.$message.error('获取用户列表失败')
//如果返回状态正常,将请求的数据保存在data中
this.userList = res.data.users;
this.total = res.data.total;
}
作用域插槽其实就是带数据的插槽,即带参数的插槽,简单的来说就是子组件提供给父组件的参数,该参数仅限于插槽中使用,父组件可根据子组件传过来的插槽数据来进行不同的方式展现和填充插槽内容。在这里slot-scope="scope"是为了获取对应行的数据。
其中,当每页的容量和页码发生改变的时候,应该重新请求最新的数据:
handleSizeChange(newSize) {
//pagesize改变时触发,当pagesize发生改变的时候,我们应该
//以最新的pagesize来请求数据并展示数据
// console.log(newSize)
this.queryInfo.pagesize = newSize;
//重新按照pagesize发送请求,请求最新的数据
this.getUserList();
},
handleCurrentChange( current ) {
//页码发生改变时触发当current发生改变的时候,我们应该
//以最新的current页码来请求数据并展示数据
// console.log(current)
this.queryInfo.pagenum = current;
//重新按照pagenum发送请求,请求最新的数据
this.getUserList();
}
实现搜索功能①添加数据绑定,添加搜索按钮的点击事件(当用户点击搜索按钮的时候,调用getUserList方法根据文本框内容重新请求用户列表数据)。②删除搜索关键字并重新获取所有的用户列表数据,只需要给文本框添加clearable属性并添加clear事件,在clear事件中重新请求数据即可
实现添加用户①当我们点击添加用户按钮的时候,弹出一个对话框来实现添加用户的功能,首先我们需要复制对话框组件的代码并在element.js文件中引入Dialog组件②接下来我们要为“添加用户”按钮添加点击事件,在事件中将addDialogVisible设置为true,即显示对话框③更改Dialog组件中的内容
//验证邮箱的规则
var checkEmail = (rule, value, cb) => {
const regEmail = /^\w+@\w+(\.\w+)+$/
if (regEmail.test(value)) {
return cb()
}
//返回一个错误提示
cb(new Error('请输入合法的邮箱'))
}
//验证手机号码的规则
var checkMobile = (rule, value, cb) => {
const regMobile = /^1[34578]\d{9}$/
if (regMobile.test(value)) {
return cb()
}
//返回一个错误提示
cb(new Error('请输入合法的手机号码'))
⑤点击对话框中的确定按钮,发送请求完成添加用户的操作
首先给确定按钮添加点击事件,在点击事件中完成业务逻辑代码
addUser(){
//点击确定按钮,添加新用户
//调用validate进行表单验证
this.$refs.addFormRef.validate( async valid => {
if(!valid) return this.$message.error("请填写完整用户信息");
//发送请求完成添加用户的操作
const {data:res} = await this.$http.post("users",this.addForm)
//判断如果添加失败,就做提示
if (res.meta.status !== 200)
return this.$message.error('添加用户失败')
//添加成功的提示
this.$message.success("添加用户成功")
//关闭对话框
this.addDialogVisible = false
//重新请求最新的数据
this.getUserList()
})
}
修改用户信息的步骤与添加用户的类似,先为用户列表中的修改按钮绑定点击事件,然后在页面中添加修改用户对话框,并修改对话框的属性。不同之处在于要根据id查询需要修改的用户数据并显示在修改对话框当中:
//展示编辑用户的对话框
async showEditDialog(id) {
//发送请求根据id获取用户信息
const { data: res } = await this.$http.get('users/' + id)
//判断如果添加失败,就做提示
if (res.meta.status !== 200) return this.$message.error('获取用户信息失败')
//将获取到的数据保存到数据editForm中
this.editForm = res.data
//显示弹出窗
this.editDialogVisible = true
}
然后是添加修改用户信息的表单并做响应的数据绑定以及数据验证,这里不加赘述。
实现删除用户,在点击删除按钮的时候,我们应该跳出提示信息框,让用户确认要进行删除操作。如果想要使用确认取消提示框,我们需要先将提示信息框挂载到vue中。①导入MessageBox组件,并将MessageBox组件挂载到实例②给用户列表中的删除按钮添加事件,并在事件处理函数中弹出确定取消窗,最后再根据id发送删除用户的请求
async removeUserById(id){
//弹出确定取消框,是否删除用户
const confirmResult = await this.$confirm('请问是否要永久删除该用户','删除提示',{
confirmButtonText:'确认删除',
cancelButtonText:'取消',
type:'warning'
}).catch(err=>err)
//如果用户点击确认,则confirmResult 为'confirm'
//如果用户点击取消, 则confirmResult获取的就是catch的错误消息'cancel'
if(confirmResult != "confirm"){
return this.$message.info("已经取消删除")
}
//发送请求根据id完成删除操作
const {data:res} = await this.$http.delete('users/'+id)
//判断如果删除失败,就做提示
if (res.meta.status !== 200) return this.$message.error('删除用户失败')
//修改成功的提示
this.$message.success('删除用户成功')
//重新请求最新的数据
this.getUserList()
}
首先完成基本的创建页面、添加路由、引入组件、请求数据。
页面的表格结构如下:
一级权限
二级权限
三级权限
这里也用到了作用域插槽,用以标识权限的等级。
编辑
删除
分配权限
其中涉及到了展开列的效果
这种效果是使用elementUI中的栅栏布局组件el-row、el-col和el-tag且嵌套三重for循环来实现的:
{{item1.authName}}
{{item2.authName}}
{{item3.authName}}
完成权限分配功能,先给分配权限按钮添加事件,在函数中请求权限树数据并显示对话框:
async showSetRightDialog() {
//当点击分配权限按钮时,展示对应的对话框
this.setRightDialogVisible = true;
//获取所有权限的数据
const {data:res} = await this.$http.get('rights/tree')
//如果返回状态为异常状态则报错并返回
if (res.meta.status !== 200)
return this.$message.error('获取权限树失败')
//如果返回状态正常,将请求的数据保存在data中
this.rightsList = res.data
}
data() {
return {
//角色列表数据
roleList: [],
//控制分配权限对话框的显示
setRightDialogVisible: false,
//权限树数据
rightsList: [],
//树形控件的属性绑定对象
treeProps: {
//通过label设置树形节点文本展示authName
label: 'authName',
//设置通过children属性展示子节点信息
children: 'children'
},
//设置树形控件中默认选中的内容
defKeys: [],
//保存正在操作的角色id
roleId:''
}
},
在商品分类页面用到了第三方插件vue-table-with-tree-grid以展示分类的数据,使用的步骤为:
1).在vue 控制台中点击依赖->安装依赖->运行依赖->输入vue-table-with-tree-gird->点击安装
2).打开main.js,导入vue-table-with-tree-grid
import TreeTable from 'vue-table-with-tree-grid'
.....
Vue.config.productionTip = false
//全局注册组件
Vue.component('tree-table', TreeTable)
3).使用组件展示分类数据
在配置好路由、获取了数据之后,使用此插件渲染数据:
在数据中添加自定义模板列:
columns: [
{label:'分类名称',prop:'cat_name'},
//type:'template'(将该列设置为模板列),template:'isok'(设置该列模板的名称为isok)
{label:'是否有效',prop:'',type:'template',template:'isok'},
{label:'排序',prop:'',type:'template',template:'order'},
{label:'操作',prop:'',type:'template',template:'opt'}
]
然后将下列模板列加入到页面的tree-table结构中:
一级
二级
三级
编辑
删除
parentCateChange(){
//级联菜单中选择项发生变化时触发
console.log(this.selectedKeys)
//如果用户选择了父级分类
if(this.selectedKeys.length > 0){
//则将数组中的最后一项设置为父级分类
this.addCateForm.cat_pid = this.selectedKeys[this.selectedKeys.length - 1]
//level也要跟着发生变化
this.addCateForm.cat_level = this.selectedKeys.length
return
}else{
this.addCateForm.cat_pid = 0
this.addCateForm.cat_level = 0
return
}
}
参数管理部分只允许给三级分类内容设置参数,参数分为动态参数和静态参数属性,在Params.vue中完成。动态和静态参数的面板切换由tab页签完成:
添加参数
编辑
删除
添加属性
编辑
删除
且下方参数中的数据取决于上方级联菜单中的选项,所以在请求数据之前要根据用户所选的分类来获取
async handleChange() {
//当用户在级联菜单中选择内容改变时触发
console.log(this.selectedCateKeys)
//发送请求,根据用户选择的三级分类和面板获取参数数据
const { data: res } = await this.$http.get(
`categories/${this.cateId}/attributes`,
{ params: { sel: this.activeName } }
)
if (res.meta.status !== 200) {
return this.$message.error('获取参数列表数据失败')
}
console.log(res.data)
if (this.activeName === 'many') {
//获取的是动态参数
this.manyTableData = res.data
} else if (this.activeName === 'only') {
//获取的是静态属性
this.onlyTableData = res.data
}
}
展开行中循环生成el-tag来展示参数:
{{item}}
+ New Tag
商品列表页面涉及到年月日时间的显示
创建过滤器对请求回来的时间数据进行改写:
//创建过滤器将秒数过滤为年月日,时分秒
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}`
})
其增删改查的操作就是根据接口文档进行编写,此处不做赘述。
点击商品管理中的添加商品按钮,会跳转到相应的页面,该页面使用到了Steps组件:
而下方卡片主体区域采用了竖向的tab栏:
在商品图片tan栏下使用upload组件打开文件选择器可以选择本地的图片上传:
因为upload组件进行图片上传的时候并不是使用axios发送请求,所以,我们需要手动为上传图片的请求添加token,即为upload组件添加headers属性:
//在页面中添加upload组件,并设置对应的事件和属性
点击上传
//在el-card卡片视图下面添加对话框用来预览图片
所涉及到的数据有:
//上传图片的url地址
uploadURL: 'http://127.0.0.1:8888/api/private/v1/upload',
//图片上传组件的headers请求头对象
headerObj: { Authorization: window.sessionStorage.getItem('token') },
//保存预览图片的url地址
previewPath: '',
//控制预览图片对话框的显示和隐藏
previewVisible:false
商品内容tab栏下用到了富文本编辑器:
为了使用富文本插件vue-quill-editor,就必须先从依赖安装该插件,引入并注册vue-quill-editor,打开main.js,编写如下代码:
//导入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'
......
//全局注册组件
Vue.component('tree-table', TreeTable)
//全局注册富文本组件
Vue.use(VueQuillEditor)
再在页面添加如下结构:
添加商品
完成添加商品的操作:在添加商品之前,为了避免goods_cat数组转换字符串之后导致级联选择器报错,我们需要打开vue控制条,点击依赖,安装lodash,把addForm进行深拷贝。
点击按钮,发送请求完成添加商品的操作:
//给添加商品按钮绑定点击事件
添加商品
//编写点击事件完成商品添加
add(){
this.$refs.addFormRef.validate(async valid=>{
if(!valid) return this.$message.error("请填写必要的表单项!")
//将addForm进行深拷贝,避免goods_cat数组转换字符串之后导致级联选择器报错
const form = _.cloneDeep(this.addForm)
//将goods_cat从数组转换为"1,2,3"字符串形式
form.goods_cat = form.goods_cat.join(",")
//处理attrs数组,数组中需要包含商品的动态参数和静态属性
//将manyTableData(动态参数)处理添加到attrs
this.manyTableData.forEach(item=>{
form.attrs.push({ attr_id:item.attr_id, attr_value:item.attr_vals.join(" ") })
})
//将onlyTableData(静态属性)处理添加到attrs
this.onlyTableData.forEach(item=>{
form.attrs.push({ attr_id:item.attr_id, attr_value:item.attr_vals })
})
//发送请求完成商品的添加,商品名称必须是唯一的
const {data:res} = await this.$http.post('goods',form)
if(res.meta.status !== 201){
return this.$message.error('添加商品失败')
}
this.$message.success('添加商品成功')
//编程式导航跳转到商品列表
this.$router.push('/goods')
})
}
导入相应的组件,按照官方文档配置相应的数据,将请求回来的数据以表格的形式呈现:
//导入echarts
import echarts from 'echarts'
//导入lodash
import _ from 'lodash'
export default {
data() {
return {
//需要跟请求的折线图数据合并的options
options: {
title: {
text: '用户来源'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#E9EEF3'
}
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: [
{
boundaryGap: false
}
],
yAxis: [
{
type: 'value'
}
]
}
}
},
created() {},
async mounted() {
//在页面dom元素加载完毕之后执行的钩子函数mounted
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('main'))
//准备数据和配置项
//发送请求获取折线图数据
const { data: res } = await this.$http.get('reports/type/1')
if (res.meta.status !== 200) {
return this.$message.error('获取折线图数据失败')
}
//合并res.data和this.options
const result = _.merge(res.data,this.options)
// 使用获取的数据展示图表
myChart.setOption(result)
},
methods: {}
}
实现步骤:
A.生成打包报告,根据报告优化项目
B.第三方库启用CDN:默认情况下,依赖项的所有第三方包都会被打包到js/chunk-vendors.js文件中,导致该js文件过大,那么我们可以通过externals排除这些包,使它们不被打包到js/chunk-vendors。
C.Element-UI组件按需加载
D.路由懒加载:当路由被访问时才加载对应的路由文件,就是路由懒加载
在终端运行命令:vue ui,可以进行图形化界面创建vue项目,之后的安装依赖、生成打包报告等多种功能也可在此实现。
默认情况下,ESLint和vscode格式化工具有冲突,需要添加配置文件解决冲突。
在项目根目录添加 .prettierrc 文件
{
"semi":false,
"singleQuote":true
}
打开.eslintrc.js文件,禁用对 space-before-function-paren 的检查:
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'space-before-function-paren' : 0
},
新建一个项目终端,输入命令‘git status’查看修改过的与新增的文件内容
将所有文件添加到暂存区:git add .
将所有代码提交到本地仓库:git commit -m “完成了XXX功能”
查看分支: git branch 发现所有代码都被提交到了相应分支
将相应分支代码合并到master主分支,先切换到master:git checkout master
在master分支进行代码合并:git merge login
将本地的master推送到远端的码云:git push
推送本地的子分支到码云,先切换到子分支:git checkout 分支名
然后推送到码云:git push -u origin 远端分支名
总结:此项目是一个综合性较强的vue前端项目,涉及到了vue全家桶、elementUI等多重技术栈,适合新手学习,便于理解vue组件化的思想。
项目地址:vue电商后台管理项目