目录
还有商品列表,分类参数,订单列表,数据报表,这几个模块使用的都是请求接口渲染,和增删改查
项目介绍
这是一个电商类的后台管理系统,主要作用是用来进行用户的管理,商品订单的管理,和权限管理,是内部员工进行商品记录,和分类
在这个项目用到的技术
vue2全家桶,element ui,axios,vue-table-with-tree-grid
一、项目的主要模块
有登录页面和主页
主页有用户管理,权限管理,商品管理,订单管理
二、开发准备工作
创建项目并下载项目中所需要用的第三方组件
三、开发流程
看接口是否用到跨域,如果需要跨域需要到 跨域需要创建vue.config.js文件,在里面配置devServer,在devServer属性⾥⾯的proxy属性⾥⾯配置,⼀共配置三个属性,分别是代理名称 代理地址 开启跨域 重写路径,这个项目是不用跨域的
- 二次封装axios和配置api
- 配置路由
- 下载项目所需要的组件 比如element ui
四、二次封装axios
在src下创建一个http文件夹,在这个文件夹下面创建request.js文件,在request文件import引入axios,引入element ui用来实现loading加载
- 引入axios,和需要的组件
//封装axios import axios from "axios"; // element ui 提示信息和loading加载 import { Message, Loading } from 'element-ui'; //引入路由 import router from "../router"
- 封装loading开启和结束的函数
// 封装loading开启和结束函数 let loading; function startLoading(){ loading = Loading.service({ lock:true, text:'拼命加载中...', background:'rgba(0,0,0,0.7)' }) } function endLoading(){ loading.close() }
- 创建axios实例
//创建axios实例 const service = axios.create({ //基地址 baseURL:" 接口地址", //baseURL:env.dev.baseUrl, settimeout: 5000, });
- 请求拦截 :请求拦截使用interceptors.request .use()在发送请求之前添加请求头并且开启loading,请求失败关闭loading,
//2:请求拦截 service.interceptors.request .use((config) => { //在发送请求之前做些什么,比如验证token之类的 if(localStorage.eleToken){ config.headers.Authorization = localStorage.eleToken } startLoading(); return config; },(error) => { //对错误请求做些什么 // endLoading(); loading.close(); return Promise.reject(error) })
- 响应拦截:响应拦截使用interceptors.response.use(),响应成功返回数据并关闭loading,请求失败返回失败的原因,比如用户过期 需要删除存放的token,并且提示用户用户过期,然后返回到登录页面
//3:响应拦截 service.interceptors.response.use( //请求成功关闭loading 并返回数据 (response) =>{ endLoading(); return response; }, (error) => { //对错误请求做些什么 const {status} = error.response //判断用户是否过期,如果过期了清楚token if(status == 401){ Message.error('用户过期,请重新登录!') localStorage.removeItem("eleToken") router.push("/login") } //关闭loading 返回信息提示用户登录过期 endLoading(); console.log(error) Message.error(error.response.data.msg) return Promise.reject(error) })
- 抛出对象
//抛出axios对象实例
export default service;
五、配置api
在http文件夹新建api.js文件,在文件引入封装好的axios
// 封装api
// 引入封装好的axios
import request from "./request"
// 向各个接口分发axios
export function getList (){
return request({
url:'', // 这个地址是去掉公共地址剩下的地址
method:'',// 请求方式 支持多种方式 get post put delete 等等
})
}
六、首页
首页样式
实现方式:
- 登录样式
<template> <div class="login_container"> <!-- 父元素 --> <div class="login_box"> <!-- 头像框 --> <div class="avatar_box"> <img src="../assets/logo.png" alt="" /> </div> <!-- 登录 --> <div class=""> <el-form :model="loginForm" :rules="loginFormRules" ref="loginFormRef" class="login_form" > <!-- 用户名 --> <el-form-item prop="username"> <el-input prefix-icon="el-icon-user-solid" type="text" v-model="loginForm.username" placeholder="请输入用户名" ></el-input> </el-form-item> <!-- 密码 --> <el-form-item prop="password"> <el-input prefix-icon="el-icon-s-goods" type="password" v-model="loginForm.password" placeholder="请输入密码" ></el-input> </el-form-item> <el-form-item class="btns"> <el-button type="primary" @click="loginFormBtn('loginFormRef')" >登录</el-button> <el-button @click="resetForm">重置</el-button> </el-form-item> </el-form> </div> </div> </div> </template> <style lang="less" scoped> .login_container { background: #2b4b6b; height: 100%; } .login_box { width: 450px; height: 360px; background: #fff; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); .avatar_box { height: 130px; width: 130px; border: 1px solid #ccc; border-radius: 50%; position: absolute; padding: 10px; left: 50%; transform: translate(-50%, -50%); box-shadow: 0px 0px 10px #ddd; background: #fff; img { width: 100%; height: 100%; border-radius: 50%; background: #eee; } } .login_form { width: 100%; position: absolute; bottom: 60px; padding: 0 40px; .btns { display: flex; justify-content: center; } } } </style>
- input框的校验
data() { return { loginForm: { username: "admin", password: "123456", }, // 校验 loginFormRules: { username: [ { required: true, message: "请输入用户名", trigger: "blur" }, { min: 4, max: 10, message: "用户名在4到10个字符之间" ,trigger: "blur" }, ], password: [ { required: true, message: "请输入密码", trigger: "blur" }, { min: 6, max: 10, message: "密码在6到10个字符之间",trigger: "blur" }, ], }, }; },
-
在api.js文件夹配置登录的接口 并在引入api
点击登录进项发起请求接口在点击登录按钮时触发按钮的点击事件,向后台发送登录请求,请求成功后把token解析并存放到本地,如果页面多处需要用到token的信息,需要存放到vuex,并跳转到主页
解析token可以使用插件 用npm install jwt-decode 在需要解析的页面引入
import { xxx } from '../http/api' loginFormBtn() { this.$refs.loginFormRef.validate((valid) => { if(!valid) return if (valid) { loginForm(this.loginForm).then((res) => { // 判断状态码是否等于200 if (res.data.meta.status == 200) { // 获取本地中的token值 localStorage.setItem("eleToken", res.data.data.token); let decode = jwt_decode(localStorage.eleToken) this.$store.dispatch('decode', decode) // 弹出提示信息 this.$message.success(res.data.meta.msg); // 将页面跳转至首页 this.$router.push("/home"); } else { // 否则提示错误信息 this.$message.error(res.data.meta.msg); return; } }); } else { console.log("error submit!!"); return false; } }); },
七、页面鉴权
为了避免用户没有登录就跳转到主页面,需要在router文件夹下面的index.js设置路由守卫
// 全局路由守卫前置钩子
router.beforeEach((to,form,next)=>{
// 判断token是否存在
const isLogin = localStorage.eleToken ? true : false
// 判断是否为登录页面
if(to.path == '/Login'){
next()
}else{
// 如果token值存在就让放行 否则就强制回到登录页面
isLogin ? next() : next('/Login')
}
})
八、主页面
主页面主要分为三个模块 ,头部导航 左侧导航,主题内容,
1、头部导航
就是显示logo,用户信息,退出登录,退出登录就是清除本地和vuex存放的token,并跳转到登录页面
2、左侧导航
是用element ui 的组件来实现的
左侧菜单的数据是后台给我们发送的,所以要在页面创建后就获取到数据信息,在methods定义一个事件,在created里面调用(created是创建后执行的函数),把获取到的数据放到data里面,然后再左侧导航的组件用v-for循环遍历,然后再router下的文件夹下的index.js配置对应的路由
<!-- 侧边栏 -->
<el-aside width="200px">
<el-menu
:collapse="isCollapse"
unique-opened
text-color="#fff"
background-color="#333744"
active-text-color="#409FFF"
:default-active="activePath"
:collapse-transition="false"
router
>
<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
@click="saveNavState('/' + subItem.path)"
:index="'/' + subItem.path"
v-for="subItem in item.children"
:key="subItem.id"
>
<template slot="title">
<i :class="iconObj[subItem.id]"></i>
<span>{
{ subItem.authName }}</span>
</template>
</el-menu-item>
</el-submenu>
</el-menu>
</el-aside>
//data的变量
menuList: [],
async getMyMenus() {
const { data: res } = await getMenus();
if (res.meta.status != 200) return;
this.menuList = res.data;
},
3、主题内容
用户管理>用户列表
- 查找
<!-- 搜索 添加 --> <el-row :gutter="20"> <el-col :span="6"> <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> <el-col :span="4"> <el-button type="primary" @click="addDialogVisible.show = true" >添加用户</el-button > </el-col> </el-row>
//搜索和添加使用的是同一个接口 async getUserList() { const { data: res } = await getUsers(this.queryInfo); if (res.meta.status !== 200) return this.$message.error("获取用户列表失败!"); this.userlist = res.data.users; this.total = res.data.total; },
- 删除 需要把这一行的id发送给后台
// 删除用户 async removeUserById(id) { const confirmResult = await this.$confirm( "此操作将永久删除该用户, 是否继续?", "提示", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning", } ).catch((err) => err); // 点击确定 返回值为:confirm // 点击取消 返回值为: cancel if (confirmResult !== "confirm") { return this.$message.info("已取消删除"); } const { data: res } = await deleteUsers(id); if (res.meta.status !== 200) return this.$message.error("删除用户失败!"); this.$message.success("删除用户成功!"); this.getUserList(); },
- 添加 点击添加的时候会打开模态框
<!-- 添加用户的对话框 --> <el-dialog title="添加用户" center :visible.sync="addDialogVisible.show" width="50%" @close="addDialogClosed" > <!-- 内容主体 --> <el-form :model="addUserForm" ref="addUserFormRef" :rules="addUserFormRules" label-width="100px" > <el-form-item label="用户名" prop="username"> <el-input v-model="addUserForm.username"></el-input> </el-form-item> <el-form-item label="密码" prop="password"> <el-input v-model="addUserForm.password"></el-input> </el-form-item> <el-form-item label="邮箱" prop="email"> <el-input v-model="addUserForm.email"></el-input> </el-form-item> <el-form-item label="手机" prop="mobile"> <el-input v-model="addUserForm.mobile"></el-input> </el-form-item> </el-form> <span slot="footer" class="dialog-footer"> <el-button @click="addDialogVisible.show = false">取 消</el-button> <el-button type="primary" @click="addUser">确 定</el-button> </span> </el-dialog>
// 添加用户 addUser() { // 提交请求前,表单预验证 this.$refs.addUserFormRef.validate(async (valid) => { // console.log(valid) // 表单预校验失败 if (!valid) return; const { data: res } = await addUsers(this.addUserForm); console.log(this.addUserForm); if (res.meta.status != 201) { this.$message.error("添加用户失败!"); return; } this.$message.success("添加用户成功!"); // 隐藏添加用户对话框 this.addDialogVisible.show = false; // this.getUserList(); this.$parent.getUserList(); }); },
- 编辑 点击编辑打开模态框
// 编辑 async editorCate(row) { this.dialogEditor = false; this.dialogVisible = true; this.dialog = { show: true, title: "编辑用户", option: "edit", }; this.addCateForm.cat_pid = row.cat_id; this.addCateForm.cat_name = row.cat_name; }, //点击确定 addCate() { this.$refs.cateForm.validate(async (valid) => { if (valid) { const { data: res } = await editorCate(this.addCateForm); if (res.meta.status !== 200) { return this.$message.error("编辑失败!"); } this.$message.success("编辑成功!"); this.dialogVisible = false; this.addCateForm = { cat_name: "", //分类名 cat_pid: 0, //父级id cat_level: 0, //分类等级 }; this.selectedKeys = {}; } else { console.log("error submit!!"); return false; } }); },
用户管理>角色列表
除了增删改查还有树形菜单
实现方式
点击下拉显示 用到了element ui
样式实现
<el-table-column type="expand">
<template slot-scope="scope">
<!-- <pre>{
{ scope.row }}</pre> -->
<el-row
:class="[i1 === 0 ? '' : 'bdtop', 'vcenter']"
v-for="(item1, i1) in scope.row.children"
:key="item1.id"
>
<!-- 一级 -->
<el-col :span="5">
<el-tag closable @close="removeRightById(scope.row, item1.id)">{
{
item1.authName
}}</el-tag>
</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
closable
@close="removeRightById(scope.row, item2.id)"
type="success"
>{
{ item2.authName }}</el-tag
>
</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>
获取数据信息
//调取角色数据
async getAllRoles() {
let { data: res } = await getRolesList();
this.rolesList = res.data;
},
点击分配权限,打开模态框,回显用的是递归的方法实现的
分配权限的模态框样式
<!-- 分配权限 -->
<el-dialog
title="分配权限"
:visible.sync="dialogVisible"
width="50%"
@close="clearDefaultKeys"
>
<!-- defaultKeys 默认勾选数组 -->
<el-tree
:data="rightsTree"
show-checkbox
default-expand-all
node-key="id"
:props="treesProps"
:default-checked-keys="defaultKeys"
ref="treeData"
></el-tree>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="addRights">确 定</el-button>
</span>
</el-dialog>
// 打开模态框
async showDialog(role) {
this.roleID = role.id;
this.dialogVisible = true;
let { data: res } = await getRightsTree();
if (res.meta.status !== 200) {
return this.$message.error("获取用户权限列表失败");
}
this.rightsTree = res.data;
// defaultKeys 默认勾选数组
this.getleafKeys(role, this.defaultKeys);
console.log(res, "treeData");
},
//递归遍历三级节点 如果没有chiledren就是三级节点 那么渲染数组
getleafKeys(node, arr) {
if (!node.children) {
return arr.push(node.id);
}
node.children.forEach((item) => {
this.getleafKeys(item, arr);
});
},
权限管理 ,权限列表
这个页面主要的效果就是展示
效果图
<el-card>
<el-table :data="rightsList" border stripe>
<el-table-column type="index"></el-table-column>
<el-table-column prop="authName" label="权限名称"></el-table-column>
<el-table-column prop="path" label="路径"></el-table-column>
<el-table-column prop="level" label="权限等级">
<template slot-scope="scope">
<el-tag v-if="scope.row.level == 0">一级</el-tag>
<el-tag type="success" v-else-if="scope.row.level == 1">二级</el-tag>
<el-tag type="warning" v-else>三级</el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
商品管理,商品分类
简单的增删改查,主要的是树形结构
这个功能用到了一个插件 下载需要cmd打开黑窗口 输入vue ui 跳转到网页下载 vue-table-with-tree-grid 下载完之后在main.js引入
<!-- 表格 -->
<tree-table
:expand-type="false"
:selection-type="false"
show-index
index-text="#"
border
:data="cateList"
:columns="columns"
>
</tree-table>
cateList: [],
columns: [
{
label: "分类名称",
prop: "cat_name",
},
{
label: "是否有效",
type: "template",
template: "isok",
},
{
label: "排序",
type: "template",
template: "order",
},
{
label: "操作",
type: "template",
template: "opt",
},
],
async getAllCategories() {
let { data: res } = await getCategoriesList(this.queryInfo);
if (res.meta.status !== 200) {
return this.$message.error("获取数据失败");
}
this.$message.success("获取分类成功");
this.cateList = res.data.result;
console.log(res, "分类数据");
},
添加分类
模态框,用到了级联选择器
效果图
实现方法:
渲染级联选择权
<!-- 添加分类弹框 -->
<el-dialog title="dialog.title" :visible.sync="dialogVisible" width="50%">
<el-form
ref="cateForm"
:model="addCateForm"
:rules="addCateFormRules"
label-width="100px"
>
<el-form-item label="分类名称:" prop="cat_name">
<el-input
v-model="addCateForm.cat_name"
placeholder="请输入分类名字"
></el-input>
</el-form-item>
<!--
options:数据源
props:靶子对象{
value:选中的属性
label:显示的名称
children:嵌套的结构
...
}
v-model:选中数组
@changed:选择项发生变化时候 触发的函数
-->
<el-form-item label="父级分类:" v-if="dialogEditor">
<el-cascader
v-model="selectedKeys"
:options="parentCateList"
:props="cascaderProps"
@change="parentCateListChanged"
change-on-select
></el-cascader>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="addCate">确 定</el-button>
</span>
</el-dialog>
//添加分类表单
addCateForm: {
cat_name: "", //分类名
cat_pid: 0, //父级id
cat_level: 0, //分类等级
},
async showDialog() {
this.dialog = {
title: "添加用户",
option: "add",
};
this.dialogEditor = true;
let { data: res } = await getCategoriesList({ type: 2 });
if (res.meta.status !== 200) {
return this.$message.error("获取父级分类数据失败");
}
this.$message.success("获取父级分类数据成功!");
this.parentCateList = res.data;
this.dialogVisible = true;
},
绑定级联选
//父级分类
parentCateList: [],
//选中的数组
selectedKeys: [],
//级联靶子对象
cascaderProps: {
value: "cat_id",
label: "cat_name",
children: "children",
expandTrigger: "hover",
},
//选择项发生变化时候触发
parentCateListChanged() {
console.log(this.selectedKeys,"级联选择器变化了!!!")
if (this.selectedKeys.length > 0) {
this.addCateForm.cat_pid = this.selectedKeys[this.selectedKeys.length - 1];
//为当前分类的等级赋值
this.addCateForm.cat_level = this.selectedKeys.length;
return;
} else {
this.addCateForm.cat_pid = 0;
this.addCateForm.cat_level = 0;
}
},
//点击确定
addCate() {
this.$refs.cateForm.validate(async (valid) => {
if (valid) {
if (this.dialog.option == "edit") {
const { data: res } = await editorCate(this.addCateForm);
if (res.meta.status !== 200) {
return this.$message.error("编辑失败!");
}
this.$message.success("编辑成功!");
} else if (this.dialog.option == "add") {
const { data: res } = await addCateList(this.addCateForm);
if (res.meta.status !== 201) {
return this.$message.error("添加失败!");
}
this.$message.success("添加成功!");
}
this.getAllCategories();
this.dialogVisible = false;
this.addCateForm = {
cat_name: "", //分类名
cat_pid: 0, //父级id
cat_level: 0, //分类等级
};
this.selectedKeys = {};
} else {
console.log("error submit!!");
return false;
}
});
},
还有商品列表,分类参数,订单列表,数据报表,这几个模块使用的都是请求接口渲染,和增删改查
九、项目难点:
-
难点:角色列表的分配权限回显,
实现思路:当点击分配权限的时候打开模态框的时候进行渲染数据,和选中的对象
实现方法:点击打开模态框的时候传递选中的对象,在点击打开模态框的定义方法中执行递归函数,进行判断遍历,如果有chiledren就循环遍历函数自身,如果没有chiledren就是三级节点 那么渲染数组,
-
难点:商品分类的用户添加,级联选择器
实现思路:当点击添加用户分类的时候,渲染级联选择器,当我们选择级联选择器的时候,需要绑定选中的id
实现方法:调用接口获取信息,绑定 options:数据源,设置靶子对象,用v-model绑定选中数组,设置选择项发生变化时候 触发的函数,当选择器发生改变的时候,进行if判断,如果选中的数组长度大于0,让该数值的id减1,意思就是添加的数据父级的id,放到新数组里,再把分类的层级和添加的分类名称存放到新的数组,
-
难点:按钮权限
实现思路: 在某个菜单的界⾯中, 我们需要根据按钮权限数据, 展示出可进⾏操作的按钮,⽐如删除, 修改, 增 加等按钮
实现方法: 如果要实现按钮的权限控制,我们需要使⽤vue的⾃定义指令去实现 , ⾸先需要创建⼀个按钮权限控制的指令,我们定义这个指令的名称为: v-a, 在这个指令的内部获取到vuex⾥⾯存储的按钮权限数据 , 在通过binding.value获取到⾃定义制定属性值的数据 , 判断从vuex⾥⾯获取到的按钮权限数据是否包含了⾃定义指令包含的权限 , 如果不包含,我们在设置el.style.display = “none”,并且使⽤el.parentNode.removeChild(el)删除当前按钮元素
Vue.directive('a', { beforeMount: function (el, binding) { //获取vuex或者本地存放的按钮数据 let actionList = storage.getItem('actionList'); //控制指令的按钮数据 let value = binding.value; //includes()方法用来判断一个数组是否包含一个指定的值,如果是返回 true,否则false。 let hasPermission = actionList.includes(value) //判断是否存在如有有就显示按钮,如果不存在就隐藏 if (!hasPermission) { el.style = 'display:none'; setTimeout(() => { el.parentNode.removeChild(el); }, 0) } } })