Vue2电商后台管理 项目总结
一、项目介绍
此项目为前后端分离的电商后台管理项目,其主要的功能模块为:
本项目是基于Vue的SPA(单页应用程序)项目,其所用到的
前端技术栈包括:Vue,Vue-Router,Element-UI,Axios,Echarts,下面将介绍各页面的开发逻辑和核心代码。
二、实现过程
1、路由准备
在项目开始前,需要先清理不必要的组件并配置好基本的路由结构。用户首先进入到的是登录页,所以先创建Login.vue页面,然后在router.js中导入组件并设置规则、在App.vue中添加路由占位符:
const router = new Router({
routes: [
{ path: '/', redirect: '/login' },
{ path: '/login', component: Login }
]
})
<template>
<div id="app">
<!-- 路由占位符 -->
<router-view></router-view>
</div>
</template>
2、登录与退出功能(表单验证、axios配置、token操作、路由守卫)
实现用户登录功能的逻辑是:
①在登录页面输入账号和密码进行登录,将数据发送给服务器
②服务器返回登录的结果,登录成功则返回数据中带有token
③客户端得到token并进行保存,后续的请求都需要将此token发送给服务器,服务器会验证token以保证用户身份。
登录表单的结构如下:
<!-- 登录表单 -->
<el-form :model="loginForm" ref="LoginFormRef" :rules="loginFormRules" label-width="0px" class="login_form">
<!-- 用户名 -->
<el-form-item prop="username">
<el-input v-model="loginForm.username" prefix-icon="iconfont icon-user" ></el-input>
</el-form-item>
<!-- 密码 -->
<el-form-item prop="password">
<el-input type="password" v-model="loginForm.password" prefix-icon="iconfont icon-3702mima"></el-input>
</el-form-item>
<!-- 按钮 -->
<el-form-item class="btns">
<el-button type="primary" @click="login">登录</el-button>
<el-button type="info" @click="resetLoginForm">重置</el-button>
</el-form-item>
</el-form>
表单中输入的数据将被绑定到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');
}
3、首页菜单渲染(多级菜单、请求拦截、二级路由)
首先构建出首页的基本页面结构,这里用到了elementUI里的多个组件。
头部区域的基本构造如下:
<!-- 头部区域 -->
<el-header>
<div>
<!-- 顶部标题 -->
<span>电商后台管理系统</span>
</div>
<el-button type="info" @click="logout"> 退出 </el-button>
</el-header>
侧边栏使用多级菜单的结构,构造如下:
<!-- 侧边栏 -->
<el-aside width="200px">
<!-- 侧边栏菜单 -->
<el-menu
background-color="#333744"
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 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>
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进行循环:
<el-menu
background-color="#333744"
text-color="#fff"
active-text-color="#ffd04b">
<!-- 一级菜单 -->
<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>
</el-menu>
新增子级路由组件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”)进行路由跳转!!!
4、用户列表基本结构(面包屑、卡片、栅格布局、表格、分页、作用域插槽)
使用element-ui面包屑组件完成顶部导航路径:
<!-- 面包屑导航 -->
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>用户管理</el-breadcrumb-item>
<el-breadcrumb-item>用户列表</el-breadcrumb-item>
</el-breadcrumb>
使用element-ui卡片组件完成主体表格,再使用element-ui输入框完成搜索框及搜索按钮,此时我们需要使用栅格布局来划分结构。然后再使用el-button制作操作的按钮:
<!-- 卡片视图区域 -->
<el-card>
<!-- 搜索与添加区域 -->
<el-row :gutter="20">
<el-col :span="7">
<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>
获取数据,然后使用表格组件进行渲染:
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;
}
<!-- 用户列表(表格)区域 -->
<el-table :data="userList" border stripe>
<el-table-column type="index"></el-table-column>
<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="状态">
<template slot-scope="scope">
<el-switch v-model="scope.row.mg_state"></el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="180px">
<template slot-scope="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 class="item" effect="dark" content="分配角色" placement="top" :enterable="false">
<el-button type="warning" icon="el-icon-setting" size='mini'></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<!-- 分页区 -->
<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>
作用域插槽其实就是带数据的插槽,即带参数的插槽,简单的来说就是子组件提供给父组件的参数,该参数仅限于插槽中使用,父组件可根据子组件传过来的插槽数据来进行不同的方式展现和填充插槽内容。在这里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();
}
5、用户列表的增删改查(对话框、验证规则)
实现搜索功能①添加数据绑定,添加搜索按钮的点击事件(当用户点击搜索按钮的时候,调用getUserList方法根据文本框内容重新请求用户列表数据)。②删除搜索关键字并重新获取所有的用户列表数据,只需要给文本框添加clearable属性并添加clear事件,在clear事件中重新请求数据即可
<el-col :span="7">
<el-input placeholder="请输入内容" v-model="queryInfo.query" clearable @clear="getUserList">
<el-button slot="append" icon="el-icon-search" @click="getUserList"></el-button>
</el-input>
</el-col>
实现添加用户①当我们点击添加用户按钮的时候,弹出一个对话框来实现添加用户的功能,首先我们需要复制对话框组件的代码并在element.js文件中引入Dialog组件②接下来我们要为“添加用户”按钮添加点击事件,在事件中将addDialogVisible设置为true,即显示对话框③更改Dialog组件中的内容
<el-dialog title="添加用户" :visible.sync="addDialogVisible" width="50%">
<!-- 对话框主体区域 -->
<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>
<!-- 对话框底部区域 -->
<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>
④添加数据绑定和校验规则:
//验证邮箱的规则
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()
}
6、权限管理页面(权限列表)
首先完成基本的创建页面、添加路由、引入组件、请求数据。
页面的表格结构如下:
<el-table :data="rightsList" stripe>
<el-table-column type="index"></el-table-column>
<el-table-column label="权限名称" prop="authName"></el-table-column>
<el-table-column label="路径" prop="path"></el-table-column>
<el-table-column label="权限等级" prop="level">
<template slot-scope="scope">
<el-tag v-if="scope.row.level === 0">一级权限</el-tag>
<el-tag v-if="scope.row.level === 1" type="success">二级权限</el-tag>
<el-tag v-if="scope.row.level === 2" type="warning">三级权限</el-tag>
</template>
</el-table-column>
</el-table>
这里也用到了作用域插槽,用以标识权限的等级。
7、权限管理页面(角色列表)
本页面的表格结构如下:
<el-table row-key="id" :data="roleList" border>
<!-- 添加展开列 -->
<el-table-column type="expand"></el-table-column>
<el-table-column type="index"></el-table-column>
<el-table-column label="角色名称" prop="roleName"></el-table-column>
<el-table-column label="角色描述" prop="roleDesc"></el-table-column>
<el-table-column label="操作" width="300px">
<template slot-scope="scope">
<el-button size="mini" type="primary" icon="el-icon-edit">编辑</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete">删除</el-button>
<el-button size="mini" type="warning" icon="el-icon-setting">分配权限</el-button>
</template>
</el-table-column>
</el-table>
其中涉及到了展开列的效果
这种效果是使用elementUI中的栅栏布局组件el-row、el-col和el-tag且嵌套三重for循环来实现的:
<!-- 添加展开列 -->
<el-table-column type="expand">
<template slot-scope="scope">
<el-row :class="['bdbottom',i1===0?'bdtop':'']" v-for="(item1,i1) in scope.row.children" :key="item1.id">
<!-- 渲染一级权限 -->
<el-col :span="5">
<el-tag>
{{item1.authName}}
</el-tag>
<i class="el-icon-caret-right"></i>
</el-col>
<!-- 渲染二,三级权限 -->
<el-col :span="19">
<!-- 通过for循环嵌套渲染二级权限 -->
<el-row :class="[i2===0?'':'bdtop' ]" v-for="(item2,i2) in item1.children" :key="item2.id">
<el-col :span="6">
<el-tag type="success">{{item2.authName}}</el-tag>
<i class="el-icon-caret-right"></i>
</el-col>
<el-col :span="18">
<el-tag type="warning" v-for="(item3) in item2.children" :key="item3.id">
{{item3.authName}}
</el-tag>
</el-col>
</el-row>
</el-col>
</el-row>
</template>
</el-table-column>
完成权限分配功能,先给分配权限按钮添加事件,在函数中请求权限树数据并显示对话框:
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
}
在对话框中使用树形组件来渲染数据:
<!-- 分配权限对话框 -->
<el-dialog title="分配权限" :visible.sync="setRightDialogVisible" width="50%" @close="setRightDialogClose">
<!-- 树形组件
show-checkbox:显示复选框
node-key:设置选中节点对应的值
default-expand-all:是否默认展开所有节点
:default-checked-keys 设置默认选中项的数组
ref:设置引用 -->
<el-tree :data="rightsList" :props="treeProps" show-checkbox node-key="id" default-expand-all :default-checked-keys="defKeys" ref="treeRef"></el-tree>
<span slot="footer" class="dialog-footer">
<el-button @click="setRightDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="allotRights">确 定</el-button>
</span>
</el-dialog>
data() {
return {
//角色列表数据
roleList: [],
//控制分配权限对话框的显示
setRightDialogVisible: false,
//权限树数据
rightsList: [],
//树形控件的属性绑定对象
treeProps: {
//通过label设置树形节点文本展示authName
label: 'authName',
//设置通过children属性展示子节点信息
children: 'children'
},
//设置树形控件中默认选中的内容
defKeys: [],
//保存正在操作的角色id
roleId:''
}
},
7、vue-table-with-tree-grid展示分类数据
在商品分类页面用到了第三方插件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).使用组件展示分类数据
在配置好路由、获取了数据之后,使用此插件渲染数据:
<!-- 分类表格
:data(设置数据源) :columns(设置表格中列配置信息) :selection-type(是否有复选框)
:expand-type(是否展开数据) show-index(是否设置索引列) index-text(设置索引列头)
border(是否添加纵向边框) :show-row-hover(是否鼠标悬停高亮) -->
<tree-table :data="cateList" :columns="columns" :selection-type="false"
:expand-type="false" show-index index-text="#" border :show-row-hover="false">
</tree-table>
在数据中添加自定义模板列:
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结构中:
<!-- 是否有效区域, 设置对应的模板列: slot="isok"(与columns中设置的template一致) -->
<template slot="isok" slot-scope="scope">
<i class="el-icon-success" v-if="scope.row.cat_deleted === false" style="color:lightgreen"></i>
<i class="el-icon-error" v-else style="color:red"></i>
</template>
<!-- 排序 -->
<template slot="order" slot-scope="scope">
<el-tag size="mini" v-if="scope.row.cat_level===0">一级</el-tag>
<el-tag size="mini" type="success" v-else-if="scope.row.cat_level===1">二级</el-tag>
<el-tag size="mini" type="warning" v-else>三级</el-tag>
</template>
<!-- 操作 -->
<template slot="opt" slot-scope="scope">
<el-button size="mini" type="primary" icon="el-icon-edit">编辑</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete">删除</el-button>
</template>
添加分类的对话框中使用到了级联菜单组件
<el-form-item label="父级分类" prop="cat_pid">
<!-- expandTrigger='hover'(鼠标悬停触发级联) v-model(设置级联菜单绑定数据) :options(指定级联菜单数据源) :props(用来配置数据显示的规则)
clearable(提供“X”号完成删除文本功能) change-on-select(是否可以选中任意一级的菜单) -->
<el-cascader expandTrigger='hover' v-model="selectedKeys" :options="parentCateList" :props="cascaderProps" @change="parentCateChange" clearable change-on-select></el-cascader>
</el-form-item>
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
}
}
8、参数管理(tab页签)
参数管理部分只允许给三级分类内容设置参数,参数分为动态参数和静态参数属性,在Params.vue中完成。动态和静态参数的面板切换由tab页签完成:
<!-- tab页签区域 -->
<el-tabs v-model="activeName" @tab-click="handleTabClick">
<!-- 添加动态参数的面板 将标签页改为many -->
<el-tab-pane label="动态参数" name="many">
<el-button size="mini" type="primary" :disabled="isButtonDisabled">添加参数</el-button>
<!-- 动态参数表格 -->
<el-table :data="manyTableData" border stripe>
<!-- 展开行 -->
<el-table-column type="expand"></el-table-column>
<!-- 索引列 -->
<el-table-column type="index"></el-table-column>
<el-table-column label="参数名称" prop="attr_name"></el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" type="primary" icon="el-icon-edit">编辑</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<!-- 添加静态属性的面板 将标签页改为only -->
<el-tab-pane label="静态属性" name="only">
<el-button size="mini" type="primary" :disabled="isButtonDisabled">添加属性</el-button>
<!-- 静态属性表格 -->
<el-table :data="onlyTableData" border stripe>
<!-- 展开行 -->
<el-table-column type="expand"></el-table-column>
<!-- 索引列 -->
<el-table-column type="index"></el-table-column>
<el-table-column label="属性名称" prop="attr_name"></el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" type="primary" icon="el-icon-edit">编辑</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
且下方参数中的数据取决于上方级联菜单中的选项,所以在请求数据之前要根据用户所选的分类来获取
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来展示参数:
<!-- 展开行 -->
<el-table-column type="expand">
<template slot-scope="scope">
<!-- 循环生成的el-tag -->
<el-tag v-for="(item,i) in scope.row.attr_vals" :key="i" closable>{{item}}</el-tag>
<!-- 输入框 -->
<el-input class="input-new-tag" v-if="scope.row.inputVisible" v-model="scope.row.inputValue" ref="saveTagInput" size="small" @keyup.enter.native="handleInputConfirm(scope.row)" @blur="handleInputConfirm(scope.row)">
</el-input>
<!-- 添加按钮 -->
<el-button v-else class="button-new-tag" size="small" @click="showInput(scope.row)">+ New Tag</el-button>
</template>
</el-table-column>
商品列表页面涉及到年月日时间的显示
创建过滤器对请求回来的时间数据进行改写:
//创建过滤器将秒数过滤为年月日,时分秒
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}`
})
其增删改查的操作就是根据接口文档进行编写,此处不做赘述。
9、添加商品(Steps组件、upload组件、富文本插件、深拷贝)
点击商品管理中的添加商品按钮,会跳转到相应的页面,该页面使用到了Steps组件:
<!-- 步骤条组件 -->
<!-- align-center(居中效果) -->
<el-steps :space="200" :active="activeIndex - 0" finish-status="success" align-center>
<el-step title="基本信息"></el-step>
<el-step title="商品参数"></el-step>
<el-step title="商品属性"></el-step>
<el-step title="商品图片"></el-step>
<el-step title="商品内容"></el-step>
<el-step title="完成"></el-step>
</el-steps>
而下方卡片主体区域采用了竖向的tab栏:
<el-tab-pane label="基本信息" name="0">
<el-form-item label="商品名称" prop="goods_name">
<el-input v-model="addForm.goods_name"></el-input>
</el-form-item>
<el-form-item label="商品价格" prop="goods_price">
<el-input v-model="addForm.goods_price" type="number"></el-input>
</el-form-item>
<el-form-item label="商品重量" prop="goods_weight">
<el-input v-model="addForm.goods_weight" type="number"></el-input>
</el-form-item>
<el-form-item label="商品数量" prop="goods_number">
<el-input v-model="addForm.goods_number" type="number"></el-input>
</el-form-item>
<el-form-item label="商品分类" prop="goods_cat">
<!-- 选择商品分类的级联选择框 -->
<el-cascader expandTrigger='hover' v-model="addForm.goods_cat" :options="cateList" :props="cateProps" @change="handleChange" clearable></el-cascader>
</el-form-item>
</el-tab-pane>
在商品图片tan栏下使用upload组件打开文件选择器可以选择本地的图片上传:
因为upload组件进行图片上传的时候并不是使用axios发送请求,所以,我们需要手动为上传图片的请求添加token,即为upload组件添加headers属性:
//在页面中添加upload组件,并设置对应的事件和属性
<el-tab-pane label="商品图片" name="3">
<!-- 商品图片上传
action:指定图片上传api接口
:on-preview : 当点击图片时会触发该事件进行预览操作,处理图片预览
:on-remove : 当用户点击图片右上角的X号时触发执行
:on-success:当用户点击上传图片并成功上传时触发
list-type :设置预览图片的方式
:headers :设置上传图片的请求头 -->
<el-upload :action="uploadURL" :on-preview="handlePreview" :on-remove="handleRemove" :on-success="handleSuccess" list-type="picture" :headers="headerObj">
<el-button size="small" type="primary">点击上传</el-button>
</el-upload>
</el-tab-pane>
//在el-card卡片视图下面添加对话框用来预览图片
<!-- 预览图片对话框 -->
<el-dialog title="图片预览" :visible.sync="previewVisible" width="50%">
<img :src="previewPath" class="previewImg" />
</el-dialog>
所涉及到的数据有:
//上传图片的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)
再在页面添加如下结构:
<!-- 富文本编辑器组件 -->
<el-tab-pane label="商品内容" name="4">
<!-- 富文本编辑器组件 -->
<quill-editor v-model="addForm.goods_introduce"></quill-editor>
<!-- 添加商品按钮 -->
<el-button type="primary" class="btnAdd">添加商品</el-button>
</el-tab-pane>
完成添加商品的操作:在添加商品之前,为了避免goods_cat数组转换字符串之后导致级联选择器报错,我们需要打开vue控制条,点击依赖,安装lodash,把addForm进行深拷贝。
点击按钮,发送请求完成添加商品的操作:
//给添加商品按钮绑定点击事件
<!-- 添加商品按钮 -->
<el-button type="primary" class="btnAdd" @click="add">添加商品</el-button>
//编写点击事件完成商品添加
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')
})
}
</script>
10、数据报表(ECharts)
导入相应的组件,按照官方文档配置相应的数据,将请求回来的数据以表格的形式呈现:
<el-card>
<div id="main" style="width: 750px; height: 400px"></div>
</el-card>
//导入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: {}
}
</script>
11、项目优化
实现步骤:
A.生成打包报告,根据报告优化项目
B.第三方库启用CDN:默认情况下,依赖项的所有第三方包都会被打包到js/chunk-vendors.js文件中,导致该js文件过大,那么我们可以通过externals排除这些包,使它们不被打包到js/chunk-vendors。
C.Element-UI组件按需加载
D.路由懒加载:当路由被访问时才加载对应的路由文件,就是路由懒加载
基于图形化界面创建Vue项目
在终端运行命令:vue ui,可以进行图形化界面创建vue项目,之后的安装依赖、生成打包报告等多种功能也可在此实现。
处理ESLint警告!!
默认情况下,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电商后台管理项目
版权归原作者 藤井粟 所有, 如有侵权,请联系我们删除。