一、项目简介

1、后台管理系统的功能划分

电商后台管理系统用于管理用户账号、商品分类、商品信息、订单、数据统计等业务功能。

2、后台管理系统的开发模式(前后端分离)

前后端分离之后,开发流程将如下图所示。

在开发期间前后端共同商定好数据接口的交互形式和数据格式。然后实现前后端的并行开发,其中前端工程师再开发完成之后可以独自进行mock测试,而后端也可以使用接口测试平台进行接口自测,然后前后端一起进行功能联调并校验格式,最终进行自动化测试

3、电商后台管理系统的技术选型

  • 前端项目技术栈

    Vue

    Vue-router

    Element-UI

    Axios

    Echarts

  • 后端项目技术栈

    Node.js

    Express

    Jwt

    Mysql

    Sequelize

二、登录/退出功能

1、登录业务流程

① 在登录页面输入用户名和密码

② 调用后台接口进行验证

③ 通过验证之后,根据后台的响应状态跳转到项目主页

2、登录业务的相关技术点

http 是无状态的,怎样记录用户的登录状态?

通过 cookie 在客户端记录状态

通过 session 在服务器端记录状态

通过token方式维持状态

在跨域的情况下,推荐使用token方式。

3、token原理分析

现在我们面临的一个问题就是,只要任何一个用户知道了服务端接口的地址,都可以进行访问,而我们有时候希望的是登录的用户才能够访问服务端的接口,所以这里需要加上相应的认证机制。关于认证机制,我们这里使用JWT.

JSON Web Token(JWT)是一个开放的标准(RFC 7519),它定义了一个紧凑且自包含的方式,用于在各方之间作为JSON对象安全地传输信息。由于此信息是经过数字签名的,因此可以被验证和信任。

传统认证流程

1、用户向服务器发送用户名和密码。

2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。

3、服务器向用户返回一个 session_id,写入用户的 Cookie。

4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。

5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。所以说,在集群或者是跨域的应用环境下,推荐使用token的模式校验。

举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?

一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。

另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。

JWT认证流程

4、路由创建

src目录下面,创建router.js文件,文件定义路由规则

import Vue from "vue";
import Router from "vue-router";
import Login from "./components/Login.vue";
Vue.use(Router);
export default new Router({
routes: [
{ path: "/", redirect: "/login" },
{ path: "/login", component: Login },
],
});

输入“/”的时候,会重定向到登录组件。

下面需要在App.vue组件中,定义路由占位符。

<template>
<div id="app">
<!--路由占位符-->
<router-view></router-view>
</div>
</template>

<script>
export default {
name: "App",
};
</script>

<style>
</style>

最后,需要在main.js文件中导入路由,并且在创建vue实例的时候,完成路由的注册。

import router from "./router";

new Vue({
router,
render: (h) => h(App),
}).$mount("#app");

5、登录页面构建

Element UI按需加载

第一、安装babel-plugin-component插件

第二:在src目录下面创建plugins目录,在该目录下面创建element.js文件,在该文件中导入所需要的组件。

import Vue from "vue";
import { Button, Form, FormItem, Input } from "element-ui";

Vue.use(Button);
Vue.use(Form);
Vue.use(FormItem);
Vue.use(Input);

第三:在main.js文件中导入element.js文件。

import "./plugins/element.js";

第四:在项目根目录下的babel.config.js文件中添加如下plugins的配置信息。

module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
plugins: [
[
"component",
{
libraryName: "element-ui",
styleLibraryName: "theme-chalk",
},
],
],
};

在整个登录表单构建过程中,还需要注意以下几点:

第一:数据绑定

首先给el-form添加model属性,动态绑定表单元素所需要的数据。

:model="loginForm"

loginForm数据定义如下:

data() {
return {
// 这是登录表单的数据绑定对象
loginForm: {
username: "admin",
password: "123456",
},
}

通过给文本框等表单元素添加v-model来绑定具体的数据。

<el-input
prefix-icon="iconfont icon-user"
v-model="loginForm.username"
></el-input>

<el-input
prefix-icon="iconfont icon-3702mima"
v-model="loginForm.password"
type="password"
></el-input>

第二: 表单校验

首先给<el-form>添加rules属性,该属性动态绑定的是表单校验的规则对象。

: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属性,该属性的值为对应的校验规则属性。

<el-form-item prop="username">
<el-input
prefix-icon="iconfont icon-user"
v-model="loginForm.username"
></el-input>
</el-form-item>
<!-- 密码 -->
<el-form-item prop="password">
<el-input
prefix-icon="iconfont icon-3702mima"
v-model="loginForm.password"
type="password"
></el-input>
</el-form-item>

第三:表单重置

首先给“重置”按钮绑定单击事件,在其所对应的回调处理函数中,通过resetFields( )方法完成表单内容的重置。

<el-button type="info" @click="resetLoginForm">重置</el-button>

那么怎样调用resetFields( )方法呢?

需要获取el-form这个表单的实例。

所以,给el-form表单添加ref属性。

ref="loginFormRef"

resetLoginForm这个方法中,我们通过当前组件的实例,获取$refs属性,然后通过该属性获取表单的实例,从而完成对resetFields方法的调用。

methods: {
// 点击重置按钮,重置登录表单
resetLoginForm() {
// console.log(this);
this.$refs.loginFormRef.resetFields();
},
},

完整登录组件的整体布局如下:

<template>
<div class="login_container">
<div class="login_box">
<!-- 头像区域 -->
<div class="avatar_box">
<img src="../assets/logo.png" alt />
</div>
<!-- 登录表单区域 -->
<el-form
label-width="0px"
class="login_form"
:model="loginForm"
:rules="loginFormRules"
ref="loginFormRef"
>
<!-- 用户名 -->
<el-form-item prop="username">
<el-input
prefix-icon="iconfont icon-user"
v-model="loginForm.username"
></el-input>
</el-form-item>
<!-- 密码 -->
<el-form-item prop="password">
<el-input
prefix-icon="iconfont icon-3702mima"
v-model="loginForm.password"
type="password"
></el-input>
</el-form-item>
<!-- 按钮区域 -->
<el-form-item class="btns">
<el-button type="primary">登录</el-button>
<el-button type="info" @click="resetLoginForm">重置</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script>
export default {
data() {
return {
// 这是登录表单的数据绑定对象
loginForm: {
username: "admin",
password: "123456",
},
// 这是表单的验证规则对象
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",
},
],
},
};
},
methods: {
// 点击重置按钮,重置登录表单
resetLoginForm() {
// console.log(this);
this.$refs.loginFormRef.resetFields();
},
},
};
</script>
<style scoped>
.login_container {
background-color: #2b4b6b;
height: 100%;
}
.login_box {
width: 450px;
height: 300px;
background-color: #fff;
border-radius: 3px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.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;
}
.login_form {
position: absolute;
bottom: 0;
width: 100%;
padding: 0 20px;
box-sizing: border-box;
}

.btns {
display: flex;
justify-content: flex-end;
}
</style>

关于全局样式,定义在assets/css/global.css文件中,在main.js文件中导入全局样式

import "./assets/css/global.css";

global.css中的初步代码:

html,
body,
#app {
height: 100%;
margin: 0;
padding: 0;
}

关于让文本框或者是密码框显示icon图标,也非常简单,可以使用第三方的图标库。

下载好的图标库(fonts)拷贝到assets目录下面,在main.js中引入对应的样式。

import "./assets/fonts/iconfont.css";

通过prefix-icon属性为文本框或密码框前面添加对应的图标。

<el-input
prefix-icon="iconfont icon-user"
v-model="loginForm.username"
></el-input>

具体的第三方图标使用方式可以参考:assets/fonts/demo_fontclass.html文件。

6、登录前的表单校验

当用户单击“登录”按钮的时候,不是立即发送请求,而是先进行校验,校验用户在表单中输入的数据是否正确,正确了才会向服务端发送请求。

当用户单击“登录”按钮的时候,是通过validate方法来完成对整个表单进行校验。

给登录按钮添加单击事件

<el-button type="primary" @click="login">登录</el-button>

login方法实现

login() {
this.$refs.loginFormRef.validate((valid) => {
console.log(valid);
});
},

通过表单实例来调用validate方法,来完成表单校验,注意的一点就是该方法的参数是一个回调函数,回调函数的参数表示校验的结果,如果校验成功参数validtrue,否则为false.

7、配置axios发起登录请求

安装axios

npm install axios

main.js文件中,导入axios,并且将其挂载到prototype原型上,同时配置所要访问的服务端的根路径

import axios from "axios";
// 配置请求的跟路径
axios.defaults.baseURL = "http://127.0.0.1:8888/api/private/v1/";
Vue.prototype.$http = axios;

下面就可以在Login.vue组件中的login方法,通过axios发送请求。

login() {
this.$refs.loginFormRef.validate(async (valid) => {
// console.log(valid);
if (!valid) return;//出错,就终止
const { data: res } = await this.$http.post("login", this.loginForm);
if (res.meta.status !== 200) return console.log("登录失败!");
console.log("登录成功");
});
},

由于axios挂载到了Vue的原型的$http上,所以在每个组件中都可以通过this.$http来获取axios,然后调用其中的post或者是get等方法来发送请求。

请求的数据就是用户在表单中输入的,而这里表单已经与loginForm对象进行了绑定,所以可以通过loginForm对象来获取用户在登录表单中输入的数据,

post请求返回的结果是Promise对象,这里使用asyncawait简化其处理的过程。

最后判断其状态码,从而决定用户是否登录成功。

8、配置Message提示框

plugins/element.js文件中导入Message组件,并且将其挂载到Vue实例的原型上。

import Vue from "vue";
import { Button, Form, FormItem, Input, Message } from "element-ui";

Vue.use(Button);
Vue.use(Form);
Vue.use(FormItem);
Vue.use(Input);

Vue.prototype.$message = Message;

修改Login.vue组件中的login方法

login() {
this.$refs.loginFormRef.validate(async (valid) => {
// console.log(valid);
if (!valid) return;
const { data: res } = await this.$http.post("login", this.loginForm);
if (res.meta.status !== 200) return this.$message.error("登录失败!");
this.$message.success("登录成功");
});
},

在登录成功后调用的是Message组件中的success方法,登录失败调用的是error方法。

9、客户端存储token信息

前面在讲解token的原理的时候讲解过,在客户端要存储服务端返回的token信息。

因为项目中除了登录之外的其它API接口,必须在登录之后才能访问。这样,在访问其它的接口的时候,浏览器只要将存储的token信息发送到服务端,服务端校验成功,就允许客户端访问指定的接口。

那么,token信息是存储在sessionStorage中呢?还是localStorage中呢?

token信息只在当前网站打开期间生效,所以将token信息存储到sessionStorage中。

下面我们来看一下,具体的实现。

login() {
this.$refs.loginFormRef.validate(async (valid) => {
// console.log(valid);
if (!valid) return;
const { data: res } = await this.$http.post("login", this.loginForm);
if (res.meta.status !== 200) return this.$message.error("登录失败!");
this.$message.success("登录成功");
//存储token信息
window.sessionStorage.setItem("token", res.data.token);
//跳转到home页面
this.$router.push("/home");
});
},

当用户登录成功后,将token信息存储到sessionStorage中,同时,跳转到home页面。

components目录下创建Home.vue.

基本的代码如下:

<template>
<div>Home组件</div>
</template>

<script>
export default {};
</script>

<style scoped>
</style>

同时在route.js文件中,定义基本的路由规则

import Vue from "vue";
import Router from "vue-router";
import Login from "./components/Login.vue";
import Home from "./components/Home.vue";
Vue.use(Router);
export default new Router({
routes: [
{ path: "/", redirect: "/login" },
{ path: "/login", component: Login },
{ path: "/home", component: Home },
],
});

10、通过路由导航守卫控制页面访问权限

通过前面的讲解,我们知道要想访问Home组件的内容,必须要登录。

但是,现在面临的一个问题就是,如果某个用户知道了访问Home组件的URL地址,那么他可以在地址栏中直接输入该地址,也可以访问Home组件(这里可以删除token信息来演示一下),这样整个登录就没有任何的效果。

怎样避免这个问题呢?

这就需要用到路由导航守卫来解决这个问题。也就是如果用户没有登录,但是直接通过URL访问特定页面,需要重新跳转到登录页面,进行登录。登录以后才能访问。

路由导航守卫基本用法。

// 为路由对象,添加`beforeEach`导航守卫
router.beforeEach((to,from,next)=>{
//如果用户访问的是登录页,直接放行
if(to.path==='/login') return next()
const tokenStr=window.sessionStorage.getItem('token')
// 没有token,强制跳转到登录页
if(!tokenStr) return next('/login')
next()
})

路由导航守卫本质就是beforeEach函数,该函数需要一个回调函数作为参数,回调函数中第一个参数to,表示将要访问的地址,from:表示是从哪个地址跳转过来的。next 表示继续执行的函数。

下面看一下router.js文件的代码改造

import Vue from "vue";
import Router from "vue-router";
import Login from "./components/Login.vue";
import Home from "./components/Home.vue";
Vue.use(Router);
const router = new Router({
routes: [
{ path: "/", redirect: "/login" },
{ path: "/login", component: Login },
{ path: "/home", component: Home },
],
});
// 挂载路由导航守卫
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();
});
export default router;

11、实现退出功能

基于token的方式实现退出功能比较简单,只要销毁本地的token即可。这样,后续的请求就不会懈怠token,必须重新登录以后,生成一个新的token之后才可以访问其它的页面。

// 清空token   
window.sessionStorage.clear()
// 跳转到登录页
this.$router.push('/login')

Home组件中,添加一个“退出”按钮,实现退出功能。

<template>
<div>
<el-button type="info" @click="logout">退出</el-button>
</div>
</template>

<script>
export default {
methods: {
logout() {
window.sessionStorage.clear();
this.$router.push("/login");
},
},
};
</script>

<style scoped>
</style>

三、主页布局和功能实现

1、主页基本布局实现

这里主要是使用了element-ui中的布局组件完成的。

Home.vue组件的布局如下:

<template>
<el-container class="home-container">
<!-- 头部区域 -->
<el-header>
<el-button type="info" @click="logout">退出</el-button>Header
</el-header>
<!-- 页面主体区域 -->
<el-container>
<!-- 侧边栏 -->
<el-aside width="200px">Aside</el-aside>
<!-- 右侧内容主体 -->
<el-main>Main</el-main>
</el-container>
</el-container>
</template>

<script>
export default {
methods: {
logout() {
window.sessionStorage.clear();
this.$router.push("/login");
},
},
};
</script>

<style scoped>
.home-container {
height: 100%;
}
.el-header {
background-color: #373d41;
}
.el-aside {
background-color: #333744;
}
.el-main {
background-color: #eaedf1;
}
</style>

注意:在plugins/element.js文件中要导入相应的布局组件。

import Vue from "vue";
import {
Button,
Form,
FormItem,
Input,
Message,
Container,
Header,
Aside,
Main,
} from "element-ui";

Vue.use(Button);
Vue.use(Form);
Vue.use(FormItem);
Vue.use(Input);
Vue.use(Container);
Vue.use(Header);
Vue.use(Aside);
Vue.use(Main);
Vue.prototype.$message = Message;

2、头部区域布局设计

首先对头部区域的结构做了一个简单的修改,添加了logo图标,以及相应的文字。

<!-- 头部区域 -->
<el-header>
<div class="header-div">
<img src="../assets/logo.png" alt="logo" />
<span>电商后台管理系统</span>
</div>
<el-button type="info" @click="logout">退出</el-button>
</el-header>

下面就是对头部区域的样式处理

.el-header {
background-color: #373d41;
display: flex;
justify-content: space-between;
padding-left: 0;
align-items: center;
color: #fff;
font-size: 20px;
}
.header-div {
display: flex;
align-items: center;
}

3、实现导航菜单的基本结构

这里需要了解element-ui中导航菜单的基本使用就可以。

<!-- 页面主体区域 -->
<el-container>
<!-- 侧边栏 -->
<el-aside width="200px">
<!--菜单区域-->
<el-menu
default-active="2"
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 index="1-1">
<!-- 图标 -->
<i class="el-icon-location"></i>
<!-- 文本 -->
<span>导航一</span>
</el-menu-item>
</el-submenu>
</el-menu>
</el-aside>
<!-- 右侧内容主体 -->
<el-main>Main</el-main>
</el-container>

这里我们是在<el-aside>左侧区域添加了一个菜单,而且这里要求菜单只保留到二级菜单。

element.js文件中,添加对菜单组件的注册。

import Vue from "vue";
import {
Button,
Form,
FormItem,
Input,
Message,
Container,
Header,
Aside,
Main,
Menu,
Submenu,
MenuItem,
} from "element-ui";

Vue.use(Button);
Vue.use(Form);
Vue.use(FormItem);
Vue.use(Input);
Vue.use(Container);
Vue.use(Header);
Vue.use(Aside);
Vue.use(Main);
Vue.use(Menu);
Vue.use(Submenu);
Vue.use(MenuItem);
Vue.prototype.$message = Message;

4、通过axios 拦截器添加token验证

现在我们面临的一个问题,就是如果想要访问受保护的API应该怎样处理呢?

对了,可以将我们存在sessionStorage中的token信息发送到服务端,服务端就可以进行校验,如果合法,运行访问其对应的接口,

关键是怎样将token信息发送到服务端呢?

必须在请求头中使用Authorization字段来保存token数据。这时通过该字段,可以将token数据发送到服务端。

而我们知道,在我们的系统中,除了登录接口不需要token数据以外,其它的接口都是需要的,这就需要在每个请求服务端的API接口中都要加上Authorization字段。

那问题是怎样在每个请求中都加上Authorization字段呢?

这里可以通过axios请求拦截器添加token信息。

基本的语法:

 // axios请求拦截   
axios.interceptors.request.use(config => {
// 为请求头对象,添加 Token 验证的 Authorization 字段
config.headers.Authorization = window.sessionStorage.getItem('token')
return config
}

axios中的interceptors 属性中有一个request,它就是axios的请求拦截器。

也就是说,每次使用axios向服务器发送请求,都会先执行request这个拦截器。这时会调用use函数(也就是请求在到达服务端之前,先执行use函数),在该函数的回调函数中,对请求进行处理,处理完成后继续向下执行,也就是将请求的内容做了一次处理后,在发送到服务端。

具体实现如下,在main.js文件中,在将axios挂载到Vue原型对象上之前,启用axios的拦截器。

import axios from "axios";
// 配置请求的跟路径
axios.defaults.baseURL = "http://127.0.0.1:8888/api/private/v1/";
//axios拦截器
axios.interceptors.request.use((config) => {
// console.log(config)
config.headers.Authorization = window.sessionStorage.getItem("token");
//最后必须返回config
return config;
});
Vue.prototype.$http = axios;

5、获取左侧菜单数据

Home.vue中发送请求,获取菜单数据。实现代码如下:

<script>
export default {
data() {
return {
menulist: [],//菜单数据
};
},
created() {
//调用getMenuList方法获取菜单数据
this.getMenuList();
},
methods: {
logout() {
window.sessionStorage.clear();
this.$router.push("/login");
},
//获取菜单数据
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);
},
},
};
</script>

在组件创建完成后,发送请求获取菜单数据。

6、渲染菜单结构

在上面的案例中,我们已经获取到了菜单数据。下面要将菜单数据渲染到页面中。

注意:服务端返回的菜单数据的格式。

<!-- 一级菜单 -->
<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">
<!-- 图标 -->
<i class="el-icon-location"></i>
<!-- 文本 -->
<span>{{subItem.authName}}</span>
</el-menu-item>
</el-submenu>

由于服务端返回的菜单数据只要两级,所以这里通过两个for循环嵌套,就可以获取到所有的菜单数据。

首先,第一层循环遍历menulist,获取一级菜单的内容,注意:index属性的取值只能为字符串,不能能为数字,并且要唯一,

然后,第二层循环遍历item.children获取二级菜单内容。

思考:如果是多层菜单(不止二层)应该怎样处理?

7、菜单图标处理

Home.vue组件中更换菜单名称前面的图标。

这里一级菜单的图标,使用的是第三方的图标。

data() {
return {
menulist: [],
// 菜单图标
iconsObj: {
"125": "iconfont icon-user",
"103": "iconfont icon-tijikongjian",
"101": "iconfont icon-shangpin",
"102": "iconfont icon-danju",
"145": "iconfont icon-baobiao",
},
};
},

iconsObj 对象中存储的是一级菜单的编号与图标样式的对应关系。

<!-- 图标 -->
<i :class="iconsObj[item.id]"></i>

这里对一级菜单的样式进行动态的绑定,从iconsObj对象中根据菜单编号获取具体的样式。

二级菜单采用固定的图标

<!-- 图标 -->
<i class="el-icon-menu"></i>

调整图标与菜单名称之间的填充距。

.iconfont {
margin-right: 10px;
}

下面我们要实现的效果就是每次只打开一个菜单项。

这里需要给<el-menu>添加unique-opened属性就可以了。

同时,当我们单击,二级菜单的时候,发现菜单超出了指定区域,这里只需要将<el-menu>的边框去掉就可以了。

.el-menu {
border-right: none;
}

8、左侧菜单的折叠与展开效果

首先在侧边栏下面添加一个折叠的图标。

<!-- 侧边栏 -->
<el-aside width=" 200px">
<div class="toggle-button" @click="toggleCollapse">|||</div>

下面定义对应的样式

.toggle-button {
background-color: #4a5064;
font-size: 10px;
line-height: 24px;
color: #fff;
text-align: center;
letter-spacing: 0.2em;
cursor: pointer;
}

控制菜单的折叠与展开,需要给el-menu添加collapse属性,该属性为true,表示展开菜单,为false折叠菜单。

同时,可以给el-menu添加属性collapse-transition将菜单折叠的动画去掉,为false的时候就可以去掉。

<el-menu
default-active="2"
background-color="#545c64"
text-color="#fff"
active-text-color="#409EFF"
unique-opened
:collapse="isCollapse"
:collapse-transition="false"
>

isCollapse属性的定义如下:

data() {
return {
menulist: [],
// 菜单图标
iconsObj: {
"125": "iconfont icon-user",
"103": "iconfont icon-tijikongjian",
"101": "iconfont icon-shangpin",
"102": "iconfont icon-danju",
"145": "iconfont icon-baobiao",
},
// 是否折叠
isCollapse: false,
};

isCollapse属性默认值为false,表示菜单是不折叠的。

当单击了按钮后,在toggleCollapse方法中修改isCollapse属性,控制菜单的隐藏域展示

// 点击按钮,切换菜单的折叠与展开
toggleCollapse() {
this.isCollapse = !this.isCollapse;
},

由于整个菜单所在的左侧区域的宽度在这里,都固定死了,为200px.

<el-aside width=" 200px">

这样导致的结果就是,当菜单折叠起来以后,整个左侧区域的宽度没有改变,这样效果比较差。

<el-aside :width="isCollapse ? '64px' : '200px'">

所以这里根据菜单是否折叠,动态修改左侧区域的宽度。

9、实现首页路由的重定向

当用户登录成功后,会展示Home组件的内容,但是这里我们还想展示一个欢迎组件中的内容。

这里可以使用子路由以及路由重定向来实现。

下面,先在components中定义一个欢迎的组件Welcome.vue

<template>
<div>
<h2>欢迎登录电商管理系统</h2>
</div>
</template>

注意:如果组件中只是展示固定的内容,并且没有样式,可以只写一个template模板。

修改router.js文件中的路由规则:

import Vue from "vue";
import Router from "vue-router";
import Login from "./components/Login.vue";
import Home from "./components/Home.vue";
//导入Welcome组件
import Welcome from "./components/Welcome.vue";
Vue.use(Router);
const router = new Router({
routes: [
{ path: "/", redirect: "/login" },
{ path: "/login", component: Login },
{
path: "/home",
component: Home,
redirect: "/welcome",
children: [{ path: "/welcome", component: Welcome }],
},
],
});

当用户访问/home的时候,先呈现出Home组件的内容,然后重定向到/welcome,这时会展示Welcome组件的内容。

那么Welcome组件的内容是在Home组件中进行展示,所以需要在Home组件中使用 <router-view>添加一个占位符。

<!-- 右侧内容主体 -->
<el-main>
<router-view></router-view>
</el-main>

这里在Home组件的右侧内容主体区域展示Welcome组件的内容。

10、启用菜单链接功能

要想让element-ui的菜单具有链接功能,需要为菜单添加 router属性,默认值为true,表示启用超链接功能。

<el-menu
default-active="2"
background-color="#545c64"
text-color="#fff"
active-text-color="#409EFF"
unique-opened
:collapse="isCollapse"
:collapse-transition="false"
router <!--启用链接-->
>

当单击二级菜单的时候,要跳转到具体的页面。

那么地址应该怎样确定呢?

当单击二级菜单的时候,发现模拟的值为<el-menu-item>index属性的取值。

所以这里需要将index的值,修改成菜单的地址,而这个地址数据是服务端返回的。

所以修改后的内容如下:

<el-menu-item
:index="'/' + subItem.path"
v-for="subItem in item.children"
:key="subItem.id"
>
<!-- 图标 -->
<i class="el-icon-menu"></i>
<!-- 文本 -->
<span>{{subItem.authName}}</span>
</el-menu-item>

在上面的代码中,修改了index属性的取值,注意路径前面要加上/.

四、用户列表布局和功能实现

1、用户列表基本展示

当单击菜单“用户列表”的时候,将用户列表组件,在其右侧进行展示。

首先,在components目录下面创建user目录,该目录存放的就是用户列表组件Users.vue.

<template>
<div>用户列表</div>
</template>
<script>
export default {};
</script>
<style scoped>
</style>

router.js文件中定义路由规则。

import Vue from "vue";
import Router from "vue-router";
import Login from "./components/Login.vue";
import Home from "./components/Home.vue";
import Welcome from "./components/Welcome.vue";
import Users from "./components/user/Users.vue";
Vue.use(Router);
const router = new Router({
routes: [
{ path: "/", redirect: "/login" },
{ path: "/login", component: Login },
{
path: "/home",
component: Home,
redirect: "/welcome",
children: [
{ path: "/welcome", component: Welcome },
{ path: "/users", component: Users },
],
},
],
});

Home添加子路由,这样如果用户输入的是/users,那么会展示User组件的内容,并且是在Home组件的”右侧内容主体”区域展示

<!-- 右侧内容主体 -->
<el-main>
<router-view></router-view>
</el-main>

2、保存菜单的激活状态

当单击了某个菜单项以后,应该让该菜单高亮显示,同时如果用户单击刷新按钮,也应该能够保持菜单的高亮显示。

这里需要给<el-menu>添加default-active属性来实现,如果该属性的值为/users(菜单的路径,也就是index属性的值),表明用户列表这个菜单被选中。

首先,让default-active属性绑定一个动态值activePath,默认值为空字符串。

<el-menu
<!--添加default-active-->
:default-active="activePath"
background-color="#545c64"
text-color="#fff"
active-text-color="#409EFF"
unique-opened
:collapse="isCollapse"
:collapse-transition="false"
router
>

activePath属性的定义如下:

data() {
return {
menulist: [],
iconsObj: {
"125": "iconfont icon-user",
"103": "iconfont icon-tijikongjian",
"101": "iconfont icon-shangpin",
"102": "iconfont icon-danju",
"145": "iconfont icon-baobiao",
},
isCollapse: false,
// 被激活的链接地址
activePath: "",
};
},

下面,我们要考虑的就是,单击了哪个二级菜单,就需要将对应的地址赋值给activePath属性。

<!-- 二级菜单 -->
<el-menu-item
:index="'/' + subItem.path"
v-for="subItem in item.children"
:key="subItem.id"
@click="saveNavState('/' + subItem.path)"
>

在上面的代码找中,我们给二级菜单添加了单击事件,当事件触发后执行saveNavState方法,将所单击的菜单的路径作为参数传递到该方法中,

在该方法中,将传递过来的菜单的地址赋值给activePath属性。

// 保存链接的激活状态
saveNavState(activePath) {
window.sessionStorage.setItem('activePath', activePath)
this.activePath = activePath
}

同时,这里还要考虑当单击刷新按钮的时候,也要保持当前所单击菜单的选中状态,也就是高亮状态,所以将所单击的菜单的地址存储到了sessionStorage中。

而当点击浏览器的刷新按钮的时候,会执行created这个钩子函数,所以这里需要在该钩子函数中,把sessionStorage中存储的地址取出来,交给activePath属性。

3、用户列表基本布局实现

在用户列表的基本布局中,创建了面包屑,同时创建了卡片区域,在卡片区域中通过el-rowel-col进行了栅格布局。

<template>
<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>
<!-- 搜索与添加区域,gutter:栅格间隔 -->
<el-row :gutter="20">
<!--占8列-->
<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>
</template>
<script>
export default {};
</script>
<style scoped>
</style>

同时需要在element.js文件中完成组件的注册。

Vue.use(Breadcrumb);
Vue.use(BreadcrumbItem);
Vue.use(Card);
Vue.use(Row);
Vue.use(Col);

如果需要修改全局的样式,定义在assets/css/global.css文件中。

/* 全局样式表 */
html,
body,
#app {
height: 100%;
margin: 0;
padding: 0;
}
.el-breadcrumb {
margin-bottom: 15px;
font-size: 12px;
}

.el-card {
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15) !important;
}

4、获取用户列表数据

Users.vue组件中的created钩子函数中,构建请求,获取用户数据,这次请求的方式为get请求,并且需要参数。

<script>
export default {
data() {
return {
// 获取用户列表的参数对象
queryInfo: {
query: "",
// 当前的页数
pagenum: 1,
// 当前每页显示多少条数据
pagesize: 2,
},
userlist: [],
total: 0,
};
},
created() {
this.getUserList();
},
methods: {
async getUserList() {
const { data: res } = await this.$http.get("users", {
//通过params组织参数。
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);
},
},
};
</script>

5、使用表格展示用户数据

表格基本使用比较简单,具体的细节可以参考文档。

我们是在el-card中,直接添加了表格内容。

<!-- 卡片视图区域 -->
<el-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-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="状态"></el-table-column>
<el-table-column label="操作"></el-table-column>
</el-table>
</el-card>

给表格指定了数据源,边框,以及各行换色的功能。

同时给表格添加了表头,以及通过prop指定每列所展示的数据。

element.js文件中完成对表格组件的注册。

Vue.use(Table);
Vue.use(TableColumn);

可以在global.css中对表格的样式进行重写。

.el-table {
margin-top: 15px;
font-size: 12px;
}

6、自定义状态列的显示效果

在表格的状态这一列上添加一个switch开关,如果获取到的状态是true,则让switch开关处于打开状态,否则处于关闭状态。

在表格的状态这一列中,添加一个作用域的插槽(可以通过scope来获取到当前行的数据),在插槽中使用了el-switch组件。

<el-table-column label="状态">
<template slot-scope="scope">
<el-switch v-model="scope.row.mg_state"></el-switch>
</template>
</el-table-column>

element.js文件中也需要完成Switch组件的注册

Vue.use(Switch);

插槽的问题。

7、自定义操作列

关于用户列表中的操作列,也是通过作用域插槽来完成的。

因为,我们在单击删除按钮,或者是编辑按钮的时候是可以通过scope来获取对应用户的编号的。

在这里,我们先把基本结构创建出来,后期在完善作用域插槽。

<el-table-column label="操作" width="180px">
<template>
<!-- 修改按钮 -->
<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" :enterable="false">
<el-button type="warning" icon="el-icon-setting" size="mini"></el-button>
</el-tooltip>
</template>
</el-table-column>

同时在element.js文件中导入Tooltip组件。

Vue.use(Tooltip);

8、实现分页效果

Users.vue组件中使用分页组件完成分页。

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>

对应处理函数

// 监听 pagesize 改变的事件,当每页显示的记录数发生改变了,重新获取数据。
handleSizeChange(newSize) {
// console.log(newSize)
this.queryInfo.pagesize = newSize;
this.getUserList();
},
// 监听 页码值 改变的事件,当页码值发生改变了,重新获取数据
handleCurrentChange(newPage) {
console.log(newPage);
this.queryInfo.pagenum = newPage;
this.getUserList();
},

注册Pagination组件

Vue.use(Pagination);

也可以在global.css中修改对应的样式。

9、修改用户状态

当单击Switch组件的时候,需要完成用户状态的更新。

Switch组件改变的时候,会触发change事件。

<template slot-scope="scope">
<el-switch v-model="scope.row.mg_state" @change="userStateChanged(scope.row)"></el-switch>
</template>

在这里我们将scope.row.mg_stateswitch进行了双向数据绑定,如果mg_state属性的值为false,表示switch关闭状态,否则就是打开状态。

反之,如果我们手动的修改了switch组件的状态,那么mg_state 的值也会发生变化。

假如我们现在将一个switch组件的状态有关闭状态修改成打开的状态,那么mg_state的值也会有false变成true,那么接下来要做的就是发送一个异步的请求,将这个数据发送到服务端,从而完成当前用户状态的更新。

// 监听 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("更新用户状态成功!");
},

注意:这里发送请求的方式为put.

同时还需要注意:如果更新数据库失败了,我们要将switch组件的状态进行还原。也就是说,我们手动的修改 了switch组件的状态(假如默认状态是关闭状态),在页面上已经呈现了打开状态,但是,如果数据库更新失败了,需要将switch组件的状态有打开状态还原到关闭状态。

10、用户搜索功能实现

<el-col :span="8">
<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>

将搜索框与queryInfo.query属性进行双向数据绑定,同时给搜索框右侧添加一个删除的图标,单击删除的图标会将用户在搜索框中输入的内容清空,同时触发@clear事件,调用getUserList方法,这时就会查询出所有的用户数据。

当用户在搜索框中输入完搜索的条件后,单击搜索按钮触发单击事件,调用getUserList方法,这时由于搜索框与query属性进行了双向数据绑定,所以query属性中存储了用户输入的搜索条件,这样在调用getUserList方法的时候,会将搜索条件发送服务端,从而完成数据的搜索。

五、用户添加、编辑、删除功能实现

1、展示用户添加对话框

对话框的展示需要用到Dialog组件。

el-card组件下面添加对话框

<!-- 添加用户的对话框 -->
<el-dialog title="添加用户" :visible.sync="addDialogVisible" width="50%">
<!-- 底部区域 -->
<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控制对话框的显示与隐藏。

data() {
return {
// 获取用户列表的参数对象
queryInfo: {
query: "",
// 当前的页数
pagenum: 1,
// 当前每页显示多少条数据
pagesize: 2,
},
userlist: [],
total: 0,
// 控制添加用户对话框的显示与隐藏
addDialogVisible: false,
};

当单击对话框中的“取消”按钮和“确定”按钮的时候,都会修改addDialogVisible属性的值为false,关闭对话框。

<el-col :span="4">
<el-button type="primary" @click="addDialogVisible = true">添加用户</el-button>
</el-col>

当单击“添加用户”按钮的时候,修改addDialogVisible属性的值为true,展示出对应的对话框。

Vue.use(Dialog)

2、展示添加用户表单

在对话框中添加用户表单

<!-- 添加用户的对话框 -->
<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>

定义表单数据属性与校验规则

data() {
return {
// 获取用户列表的参数对象
queryInfo: {
query: "",
// 当前的页数
pagenum: 1,
// 当前每页显示多少条数据
pagesize: 2,
},
userlist: [],
total: 0,
// 控制添加用户对话框的显示与隐藏
addDialogVisible: false,
// 添加用户的表单数据
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" }],
},
};

3、自定义校验规则

现在需要对邮箱与手机号码进行规则校验。具体的使用方式,可以参考官方文档:https://element.eleme.cn/#/zh-CN/component/form

中,对表单的自定义校验规则的实现。

data中定义校验的函数。

data() {
// 验证邮箱的规则
var 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("请输入合法的邮箱"));
};

// 验证手机号的规则
var checkMobile = (rule, value, cb) => {
// 验证手机号的正则表达式
const regMobile = /^(0|86|17951)?(13[0-9]|15[012356789]|17[678]|18[0-9]|14[57])[0-9]{8}$/;

if (regMobile.test(value)) {
return cb();
}

cb(new Error("请输入合法的手机号"));
};



return {
// 获取用户列表的参数对象
queryInfo: {
query: "",
// 当前的页数
pagenum: 1,
// 当前每页显示多少条数据
pagesize: 2,
},
userlist: [],
total: 0,
// 控制添加用户对话框的显示与隐藏
addDialogVisible: false,
// 添加用户的表单数据
addForm: {
username: "",
password: "",
email: "",
mobile: "",
},

使用规则校验的函数。

email: [
{ required: true, message: "请输入邮箱", trigger: "blur" },
{ validator: checkEmail, trigger: "blur" },
],
mobile: [
{ required: true, message: "请输入手机号", trigger: "blur" },
{ validator: checkMobile, trigger: "blur" },
],

这里是通过validator来使用校验的函数。

4、实现表单重置操作

如果在表单中输入了内容,然后单击了“取消”按钮,这时再次单击“添加”按钮后,弹出的表单中还保留了上次输入的内容。

而像这种情况,表单应该是呈现出最开始的默认状态。

具体的实现如下:

<el-dialog title="添加用户" :visible.sync="addDialogVisible" width="50%" @close="addDialogClosed">

dialog对话框添加了@close事件,当关闭窗口的时候会触发该事件。

addDialogClosed方法中,将表单的内容进行重置。在methods中,定义如下的方法。

// 监听添加用户对话框的关闭事件
addDialogClosed() {
this.$refs.addFormRef.resetFields();
},

5、完成用户添加

首先,修改添加对话框中的“确定”按钮,为其添加单击事件。

<!-- 底部区域 -->
<span slot="footer" class="dialog-footer">
<el-button @click="addDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="addUser">确 定</el-button>
</span>

addUser方法的实现如下:

// 点击按钮,添加新用户
addUser() {
this.$refs.addFormRef.validate(async (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();
});
},

6、展示修改用户的对话框

在添加用户对话框的下面,再次创建一个对话框表示修改用户信息的对话框。

<!-- 修改用户的对话框 -->
<el-dialog title="修改用户" :visible.sync="editDialogVisible" width="50%">
修改用户信息
<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属性控制对话框的显示与隐藏。

data中定义该属性,默认取值为false

// 控制修改用户对话框的显示与隐藏
editDialogVisible: false,

为表格中的操作列中的编辑按键添加单击事件,该事件触发后弹出修改用户的窗口。

<!-- 修改按钮 -->
<el-button type="primary" icon="el-icon-edit" size="mini" @click="showEditDialog"></el-button>

showEditDialog方法的实现如下

// 展示编辑用户的对话框
async showEditDialog() {
this.editDialogVisible = true;
},

这样就实现了单击修改按钮,弹出窗口的效果。

下面要实现的功能就是在修改的窗口中添加表单,然后将要修改的用户数据填充到表单中。

7、根据用户编号查询用户信息

当单击“修改””按钮的时候,首先先获取用户的编号,然后根据该编号查询出对应的用户数据。

<template slot-scope="scope">
<!-- 修改按钮 -->
<el-button
type="primary"
icon="el-icon-edit"
size="mini"
@click="showEditDialog(scope.row.id)"
></el-button>

通过作用域插槽,将要编辑的用户编号传递到showEditDialog方法中。

// 展示编辑用户的对话框
async showEditDialog(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;
},

根据传递过来的哟用户编号,发送异步请求,获取具体的用户数据,然后赋值给editForm属性。

data中定义editForm属性来保存要编辑的用户数据。

// 查询到的要编辑的用户信息对象
editForm: {},

8、展示修改用户的表单

在修改用户的对话框中创建修改的表单,展示要修改的数据。


<!-- 修改用户的对话框 -->
<el-dialog title="修改用户" :visible.sync="editDialogVisible" width="50%">
<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>
<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>

在表单中,model属性已经绑定了editForm对象,而该对象中存储了要修改的用户数据,然后将editForm中的属性与文本框进行双向绑定,这样文本框中就会展示出要修改的用户数据。

同时给el-form表单添加了rules属性,指定了校验规则,校验规则有editFormRules对象完成定义,让每一个el-form-item表单项通过prop属性与校验规则对象editFormRules中的属性进行绑定,从而完成校验。

// 查询到的要编辑的用户信息对象
editForm: {},
// 修改表单的验证规则对象
editFormRules: {
email: [
{ required: true, message: "请输入用户邮箱", trigger: "blur" },
{ validator: checkEmail, trigger: "blur" },
],
mobile: [
{ required: true, message: "请输入用户手机", trigger: "blur" },
{ validator: checkMobile, trigger: "blur" },
],
},

9、完成用户信息编辑操作

首先给用户编辑窗口中的确定按钮,添加单击事件,对应的处理函数为editUserInfo

<el-button type="primary" @click="editUserInfo">确 定</el-button>

editUserInfo方法的实现如下:

// 修改用户信息并提交
editUserInfo() {
this.$refs.editFormRef.validate(async (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("更新用户信息成功!");
});
},

10、删除用户数据

在删除具体的用户数据之前,应该先给用户一个提示信息。

首先先找到表格中的操作列,然后在找到删除按钮,给该按钮添加单击事件。

<!-- 删除按钮 -->
<el-button
type="danger"
icon="el-icon-delete"
size="mini"
@click="removeUserById(scope.row.id)"
></el-button>

在调用removeUserById方法的时候,将用户编号作为参数。

// 根据Id删除对应的用户信息
async removeUserById(id) {
// 弹框询问用户是否删除数据
//由于Confirm挂载到了`Vue`的原型上,所以这里可以直接使用this
const confirmResult = await this.$confirm(
"此操作将永久删除该用户, 是否继续?",
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",//窗口中的图标
}
).catch((err) => err); //处理取消的情况。

// 如果用户确认删除,则返回值为字符串 confirm
// 如果用户取消了删除,则返回值为字符串 cancel
// console.log(confirmResult)
if (confirmResult !== "confirm") {
return this.$message.info("已取消删除");
}

this.$message.success("删除用户成功!" + id);
},

element.js文件中导入MessageBox,并且将confirm挂载到Vue的原型上。

import {MessageBox} from "element-ui"
Vue.prototype.$confirm = MessageBox.confirm;

下面要实现的就是发送异步请求删除用户数据。

修改后的removeUserById的方法如下:

   // 根据Id删除对应的用户信息
async removeUserById(id) {
// 弹框询问用户是否删除数据
//由于Confirm挂载到了`Vue`的原型上,所以这里可以直接使用this
const confirmResult = await this.$confirm(
"此操作将永久删除该用户, 是否继续?",
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}
).catch((err) => err); //处理取消的情况。

// 如果用户确认删除,则返回值为字符串 confirm
// 如果用户取消了删除,则返回值为字符串 cancel
// console.log(confirmResult)
if (confirmResult !== "confirm") {
return this.$message.info("已取消删除");
}
//指定删除接口,发送delete请求
const { data: res } = await this.$http.delete("users/" + id);

if (res.meta.status !== 200) {
return this.$message.error("删除用户失败!");
}

this.$message.success("删除用户成功!");
this.getUserList();
},

六、权限列表

1、创建权限列表组件

首先先创建一个基本的权限组件,并且指定对应的路由规则。

components目录下面创建power目录,在该目录下面创建Rights.vue文件。

<template>
<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>
<el-table :data="rightsList" border 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 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>
</div>
</template>

<script>
export default {
data() {
return {
// 权限列表
rightsList: [],
};
},
created() {
// 获取所有的权限
this.getRightsList();
},
methods: {
// 获取权限列表
async getRightsList() {
const { data: res } = await this.$http.get("rights/list");
if (res.meta.status !== 200) {
return this.$message.error("获取权限列表失败!");
}

this.rightsList = res.data;
console.log(this.rightsList);
},
},
};
</script>

<style scoped>
</style>

路由设置:

import Vue from "vue";
import Router from "vue-router";
import Login from "./components/Login.vue";
import Home from "./components/Home.vue";
import Welcome from "./components/Welcome.vue";
import Users from "./components/user/Users.vue";
import Rights from "./components/power/Rights.vue";
Vue.use(Router);
const router = new Router({
routes: [
{ path: "/", redirect: "/login" },
{ path: "/login", component: Login },
{
path: "/home",
component: Home,
redirect: "/welcome",
children: [
{ path: "/welcome", component: Welcome },
{ path: "/users", component: Users },
{ path: "/rights", component: Rights },//权限组件
],
},
],
});

Vue.use(Tag);

2、用户角色权限关系介绍

七、角色列表

1、展示角色数据

components/power目录下面创建Roles.vue

整个的组件结构如下:

<template>
<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>
<!-- 添加角色按钮区域 -->
<el-row>
<el-col>
<el-button type="primary">添加角色</el-button>
</el-col>
</el-row>
<!-- 角色列表区域 -->
<el-table :data="rolelist" border stripe>
<!-- 展开列 -->
<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"
@click="showSetRightDialog(scope.row)"
>分配权限</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script>
export default {
data() {
return {
// 所有角色列表数据
rolelist: [],
};
},
created() {
this.getRolesList();
},
methods: {
// 获取所有角色的列表
async getRolesList() {
const { data: res } = await this.$http.get("roles");

if (res.meta.status !== 200) {
return this.$message.error("获取角色列表失败!");
}

this.rolelist = res.data;

// console.log(this.rolelist);
},
},
};
</script>

在上面的表格中,我们添加了一个展开列。

<!-- 展开列 -->
<el-table-column type="expand"></el-table-column>

路由设置:

import Vue from "vue";
import Router from "vue-router";
import Login from "./components/Login.vue";
import Home from "./components/Home.vue";
import Welcome from "./components/Welcome.vue";
import Users from "./components/user/Users.vue";
import Rights from "./components/power/Rights.vue";
import Roles from "./components/power/Roles.vue";

Vue.use(Router);
const router = new Router({
routes: [
{ path: "/", redirect: "/login" },
{ path: "/login", component: Login },
{
path: "/home",
component: Home,
redirect: "/welcome",
children: [
{ path: "/welcome", component: Welcome },
{ path: "/users", component: Users },
{ path: "/rights", component: Rights },
{ path: "/roles", component: Roles },
],
},
],
});

添加了Roles.vue这个组件对应的路由内容。

2、渲染一级权限

这里重点要注意的就是,整个数据的结构。

当点击“展开列”时候,会展示出当前角色具有的权限。

而系统中,权限分为了三级,分别为1级权限,二级权限和三级权限。

整个权限的布采用的是栅格布局,一级权限占用5列,二级与三级权限占用19列。

在”展开列”中,通过作用域插槽来完成权限的展示。

<!-- 展开列 -->
<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"></el-col>
</el-row>
</template>
</el-table-column>

在上面的代码中我们通过scope.row.children获取的是当前角色具有的权限数据,下面通过for循环进行遍历。

基本的样式如下

<style scoped>
.el-tag {
margin: 7px;
}

.bdtop {
border-top: 1px solid #eee;
}

.bdbottom {
border-bottom: 1px solid #eee;
}
</style>

3、渲染二级权限

<!-- 展开列 -->
<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-col>
</el-row>
</el-col>
</el-row>
</template>
</el-table-column>

这里通过对一级权限下的chilren属性进行遍历,从而完成二级权限的渲染。

4、渲染三级权限

<!-- 展开列 -->
<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>

这里通过对二级权限下的chilren属性进行遍历,从而完成三级权限的渲染。

5、删除指定角色下的权限

首先给每个权限名称右上角添加一个叉号按钮,当单击该叉号按钮后,删除对应的权限。

<!-- 展开列 -->
<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 closable @close="removeRightById(scope.row, item1.id)">{{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"
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
type="warning"
v-for="(item3) in item2.children"
:key="item3.id"
closable
@close="removeRightById(scope.row, item3.id)"
>{{item3.authName}}</el-tag>
</el-col>
</el-row>
</el-col>
</el-row>
</template>
</el-table-column>

在上面的代码中,给el-tag组件添加了closable属性,同时指定了@close事件。

事件触发后,执行removeRightById处理函数,将角色的信息以及权限编号传递到该方法中。

// 根据Id删除对应的权限
async removeRightById(role, rightId) {
// 弹框提示用户是否要删除
const confirmResult = await this.$confirm(
"此操作将永久删除该文件, 是否继续?",
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}
).catch((err) => err);

if (confirmResult !== "confirm") {
return this.$message.info("取消了删除!");
}

const { data: res } = await this.$http.delete(
`roles/${role.id}/rights/${rightId}`
);

if (res.meta.status !== 200) {
return this.$message.error("删除权限失败!");
}

// this.getRolesList()
role.children = res.data; //将返回的最新数据重新赋值给children属性。
},

八、分配权限、角色

1、分配权限—展示权限信息

当用户点击分配权限按钮的时候,会弹出一个对话框,在这个对话框中以树形方式展示出所有的权限。

el-card组件下面添加窗口组件。

<!-- 分配权限的对话框 -->
<el-dialog title="分配权限" :visible.sync="setRightDialogVisible" width="50%">
<span slot="footer" class="dialog-footer">
<el-button @click="setRightDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="setRightDialogVisible = false">确 定</el-button>
</span>
</el-dialog>

定义setRightDialogVisible属性,该属性的默认值为false.

// 控制分配权限对话框的显示与隐藏
setRightDialogVisible: false,

当单击操作列中的“分配权限”按钮弹出窗口,同时获取所有的权限信息。

<el-button
size="mini"
type="warning"
icon="el-icon-setting"
@click="showSetRightDialog(scope.row)"
>分配权限</el-button>

在调用showSetRightDialog方法的时候,传递了对应的角色信息。


// 展示分配权限的对话框
async showSetRightDialog(role) {
// 获取所有权限的数据
const { data: res } = await this.$http.get("rights/tree");

if (res.meta.status !== 200) {
return this.$message.error("获取权限数据失败!");
}

// 把获取到的权限数据保存到 data 中
this.rightslist = res.data;
console.log(this.rightslist);
console.log(role);
// 展示出对话框
this.setRightDialogVisible = true;
},

把获取到的权限数据赋值给了rightslist属性。

// 所有权限的数据
rightslist: [],

2、使用树形方式展示权限数据

下面要做的就是,在弹出的窗口中使用树形控件展示权限数据。

<!-- 分配权限的对话框 -->
<el-dialog title="分配权限" :visible.sync="setRightDialogVisible" width="50%">
<!-- 树形控件 -->
<el-tree :data="rightslist" :props="treeProps"></el-tree>

<span slot="footer" class="dialog-footer">
<el-button @click="setRightDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="setRightDialogVisible = false">确 定</el-button>
</span>
</el-dialog>

在上面的代码中,为对话框添加了el-tree控件,data属性指定了数据源,props属性指定了树形控件中所要展示的内容以及父子关系。

data() {
return {
// 所有角色列表数据
rolelist: [],
// 控制分配权限对话框的显示与隐藏
setRightDialogVisible: false,
// 所有权限的数据
rightslist: [],
// 树形控件的属性绑定对象
treeProps: {
label: "authName",
children: "children",
},
};

element.js中注册Tree组件

Vue.use(Tree)

接下来,给树形控件添加了如下属性

<el-tree :data="rightslist" :props="treeProps" show-checkbox node-key="id" default-expand-all></el-tree>

show-checkbox:每一个权限名称前面添加复选框。

default-expand-all: 将整棵树全部展开。

node-key:选中获取的是权限的编号。

3、将某个角色已有权限选中

如果想要让el-tree中权限名称前面的复选框选中,需要用到default-checked-keys属性, 该属性的值是一个数组,数组中存放的就是要选中的权限的编号。

<el-tree
:data="rightslist"
:props="treeProps"
show-checkbox
node-key="id"
default-expand-all
:default-checked-keys="defKeys"
></el-tree>

定义defKeys数组

data() {
return {
// 所有角色列表数据
rolelist: [],
// 控制分配权限对话框的显示与隐藏
setRightDialogVisible: false,
// 所有权限的数据
rightslist: [],
// 树形控件的属性绑定对象
treeProps: {
label: "authName",
children: "children",
},
// 默认选中的节点Id值数组
defKeys: [],
};

怎样将某个角色已经有的权限选中呢(这里是将三级权限选中),也就是说怎样将某个角色已有的三级权限编号存储到defKeys数组中?

// 通过递归的形式,获取角色下所有三级权限的id,并保存到 defKeys 数组中
getLeafKeys(node, arr) {
// 如果当前 node 节点不包含 children 属性,则是三级节点
if (!node.children) {
return arr.push(node.id);
}

node.children.forEach((item) => this.getLeafKeys(item, arr));
},

这里就是通过递归的方式,看一下某个节点是否还有children属性,如果没有表示就是三级权限,这样就将对应的权限编号存储到一个数组中,如果有

children属性,继续遍历,并且再次调用getLeafKeys函数,判断通过循环取出来的节点是否有children属性,不断重复这个过程。

下面要思考的就是,什么时候调用getLeafKeys方法呢?

// 展示分配权限的对话框
async showSetRightDialog(role) {
// 获取所有权限的数据
const { data: res } = await this.$http.get("rights/tree");

if (res.meta.status !== 200) {
return this.$message.error("获取权限数据失败!");
}

// 把获取到的权限数据保存到 data 中
this.rightslist = res.data;
// console.log(this.rightslist);
// console.log(role);
// 递归获取三级节点的Id(开始的节点为当前角色)
this.getLeafKeys(role, this.defKeys);

// 展示出对话框
this.setRightDialogVisible = true;
},

我们是在showSetRightDialog方法中调用的getLeafKeys方法,也就是说,当用户单击分配权限按钮,弹出对话框前,就应该获取当前角色已有的权限编号,

并且存储到了defkeys数组中,这样当对话框展示出来以后,就会在对话框中以树形结构展示所有权限,并且将角色已经有的权限前面的复选框选中。

最后,这里有一个小的Bug需要解决,就是我们这里是不断的向defKeys数组中添加内容,这样就会出现如下的问题:当给第一个角色分配完权限后,关闭窗口,

又给第二个角色分配权限,这样所有的权限编号都累加到了defkeys数组中,所以这里在窗口关闭后,应该将defKeys数组中的内容清空。

// 监听分配权限对话框的关闭事件
setRightDialogClosed() {
this.defKeys = []
},

关闭窗口的时候,调用上面的方法

<!-- 分配权限的对话框 -->
<el-dialog
title="分配权限"
:visible.sync="setRightDialogVisible"
width="50%"
@close="setRightDialogClosed"
>

4、完成权限的分配

当单击窗口中的“确定”按钮的时候,完成权限的分配。

<span slot="footer" class="dialog-footer">
<el-button @click="setRightDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="allotRights">确 定</el-button>
</span>

点击“确定”按钮后,执行allotRights方法。

// 点击为角色分配权限
async allotRights() {
const keys = [
...this.$refs.treeRef.getCheckedKeys(),
...this.$refs.treeRef.getHalfCheckedKeys(),
];

const idStr = keys.join(",");

const { data: res } = await this.$http.post(
`roles/${this.roleId}/rights`,
{ rids: idStr }
);

if (res.meta.status !== 200) {
return this.$message.error("分配权限失败!");
}

this.$message.success("分配权限成功!");
this.getRolesList();
this.setRightDialogVisible = false;
},

发送到服务端的权限编号,要求有选中的和半选中(如果子级权限没有全部选中,则父级权限对应的复选框是处于半选中状态)的权限编号,这里可以通过el-tree组件的getCheckedKeys方法获取全选的权限编号,通过getHalfCheckedKeys方法获取半选的权限编号。

所以这里还需要给el-tree添加ref引用。

<el-tree
:data="rightslist"
:props="treeProps"
show-checkbox
node-key="id"
default-expand-all
:default-checked-keys="defKeys"
ref="treeRef"//添加ref引用
></el-tree>

获取到权限的全选的权限编号与半选的权限编号后,需要拼接成字符串,并且用逗号分隔。

当然,在发送到服务端中的内容除了权限编号的内容,还要有对应的角色编号。

当用户单击“分配权限”按钮,打开窗口的时候,我们就获取到了角色信息,在这里可以将获取到的角色的编号存储到data状态属性中。

// 展示分配权限的对话框
async showSetRightDialog(role) {
//获取要分配权限的角色编号。
this.roleId = role.id;
// 获取所有权限的数据
const { data: res } = await this.$http.get("rights/tree");

if (res.meta.status !== 200) {
return this.$message.error("获取权限数据失败!");
}

// 把获取到的权限数据保存到 data 中
this.rightslist = res.data;
// console.log(this.rightslist);
// console.log("role=", role);
// 递归获取三级节点的Id
this.getLeafKeys(role, this.defKeys);

// 展示出对话框
this.setRightDialogVisible = true;
},

data中定义roleId 属性

data() {
return {
// 所有角色列表数据
rolelist: [],
// 控制分配权限对话框的显示与隐藏
setRightDialogVisible: false,
// 所有权限的数据
rightslist: [],
// 树形控件的属性绑定对象
treeProps: {
label: "authName",
children: "children",
},
// 默认选中的节点Id值数组
defKeys: [],
// 当前即将分配权限的角色id
roleId: "",
};

以上完成了对角色分配权限的功能。

5、为用户分配角色

展示为用户分配角色的对话框

在这里我们需要返回components/user/Users.vue组件,。

首先给用户表格中,操作列中的分配角色按钮添加单击事件。

<!-- 分配角色按钮 -->
<el-tooltip effect="dark" content="分配角色" placement="top" :enterable="false">
<el-button
type="warning"
icon="el-icon-setting"
size="mini"
@click="setRole(scope.row)"
></el-button>
</el-tooltip>

这时会将用户的信息传递到setRole 方法中。

在该方法中,我们会查询出所有的角色,然后弹出一个窗口,在这个窗口中,会显示要分配角色的用户信息,同时将查询出的角色最终会绑定到下拉框中。

// 展示分配角色的对话框
async setRole(userInfo) {
this.userInfo = userInfo;

// 在展示对话框之前,获取所有角色的列表
const { data: res } = await this.$http.get("roles");
if (res.meta.status !== 200) {
return this.$message.error("获取角色列表失败!");
}

this.rolesList = res.data;

this.setRoleDialogVisible = true;
},
},

在调用setRole方法的时候,会将对应的要分配角色的用户信息传递过来,在这里我们给了userInfo这个属性,最终会将这个属性中存储的内容传递到窗口中进行展示。

接下来,会查询所有的角色信息,并且交给roleList这个属性存储。最后展示对应的对话框。

对话框内容

<!-- 分配角色的对话框 -->
<el-dialog title="分配角色" :visible.sync="setRoleDialogVisible" width="50%">
<div>
<p>当前的用户:{{userInfo.username}}</p>
<p>当前的角色:{{userInfo.role_name}}</p>
<p></p>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="setRoleDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="saveRoleInfo">确 定</el-button>
</span>
</el-dialog>

对应的属性定义如下:

// 控制分配角色对话框的显示与隐藏
setRoleDialogVisible: false,
// 需要被分配角色的用户信息
userInfo: {},
// 所有角色的数据列表
rolesList: [],

将查询出来的角色数据绑定到下拉框中

在分别角色的对话框中,我们添加了一个下拉框,

<!-- 分配角色的对话框 -->
<el-dialog title="分配角色" :visible.sync="setRoleDialogVisible" width="50%">
<div>
<p>当前的用户:{{userInfo.username}}</p>
<p>当前的角色:{{userInfo.role_name}}</p>
<p>
分配新角色:
<el-select v-model="selectedRoleId" placeholder="请选择">
<el-option
v-for="item in rolesList"
:key="item.id"
:label="item.roleName"
:value="item.id"
></el-option>
</el-select>
</p>

</div>
<span slot="footer" class="dialog-footer">
<el-button @click="setRoleDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="saveRoleInfo">确 定</el-button>
</span>
</el-dialog>

在上面的代码中,我们通过循环的方式,将rolesList中存储的角色数据全部取出来绑定到了select中。 label属性表示的是下拉框中展示的内容,value:表示选择后的值,同时选择后的值也就是所选择的角色编号会存储到selectedRoleId中。

需要在data中定义selectedRoleId属性。

// 已选中的角色Id值
selectedRoleId: "",

同时将SelectOption组件进行注册

Vue.use(Select)
Vue.use(Option)

下面要实现的就是单击分配角色对话框中的“确定”按钮后,完成角色的分配。

完成用户角色的分配

saveRoleInfo方法的实现如下

// 点击按钮,分配角色
async saveRoleInfo() {
if (!this.selectedRoleId) {
return this.$message.error("请选择要分配的角色!");
}

const { data: res } = await this.$http.put(
`users/${this.userInfo.id}/role`,
{
rid: this.selectedRoleId,
}
);

if (res.meta.status !== 200) {
return this.$message.error("更新角色失败!");
}

this.$message.success("更新角色成功!");
this.getUserList();
this.setRoleDialogVisible = false;
},

九、商品分类

1、商品分类功能介绍

商品分类用于在购物时,快速找到所要购买的商品,可以通过电商平台主页直观的看到。

2、商品分类组件基本创建

components目录下面创建goods目录,在该目录下面创建Cate.vue作为商品分类组件。

基本结构如下:

<template>
<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>
<el-row>
<el-col>
<el-button type="primary">添加分类</el-button>
</el-col>
</el-row>

<!-- 表格区域 -->
<!-- 分页区域 -->
</el-card>
</div>
</template>
<script>
export default {};
</script>
<style scoped>
</style>

设置对应的路由内容

import Vue from "vue";
import Router from "vue-router";
import Login from "./components/Login.vue";
import Home from "./components/Home.vue";
import Welcome from "./components/Welcome.vue";
import Users from "./components/user/Users.vue";
import Rights from "./components/power/Rights.vue";
import Roles from "./components/power/Roles.vue";
import Cate from "./components/goods/Cate.vue";
Vue.use(Router);
const router = new Router({
routes: [
{ path: "/", redirect: "/login" },
{ path: "/login", component: Login },
{
path: "/home",
component: Home,
redirect: "/welcome",
children: [
{ path: "/welcome", component: Welcome },
{ path: "/users", component: Users },
{ path: "/rights", component: Rights },
{ path: "/roles", component: Roles },
{ path: "/categories", component: Cate },
],
},
],
});

3、获取商品分类数据

<template>
<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>
<el-row>
<el-col>
<el-button type="primary">添加分类</el-button>
</el-col>
</el-row>

<!-- 表格区域 -->
<!-- 分页区域 -->
</el-card>
</div>
</template>
<script>
export default {
data() {
return {
// 查询条件
querInfo: {
type: 3,//表示展示的是三级分类
pagenum: 1,
pagesize: 5,
},
// 商品分类的数据列表,默认为空
catelist: [],
// 总数据条数
total: 0,
};
},
created() {
this.getCateList();
},
methods: {
// 获取商品分类数据
async getCateList() {
const { data: res } = await this.$http.get("categories", {
params: this.querInfo,
});

if (res.meta.status !== 200) {
return this.$message.error("获取商品分类失败!");
}

console.log(res.data);
// 把数据列表,赋值给 catelist
this.catelist = res.data.result;
// 为总数据条数赋值
this.total = res.data.total;
},
},
};
</script>
<style scoped>
</style>

4、使用vue-table-width-tree-grid展示数据

vue-table-with-tree-grid 是一个树形的表格插件

安装

npm i vue-table-with-tree-grid

安装好以后,在main.js文件中注册该组件

导入vue-table-with-tree-grid组件

import TreeTable from "vue-table-with-tree-grid";

注册组件

//注册TreeTable组件
Vue.component("tree-table", TreeTable);
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import "./plugins/element.js";
import "./assets/css/global.css";
import "./assets/fonts/iconfont.css";
//导入vue-table-with-tree-grid
import TreeTable from "vue-table-with-tree-grid";
import axios from "axios";
// 配置请求的跟路径
axios.defaults.baseURL = "http://127.0.0.1:8888/api/private/v1/";
axios.interceptors.request.use((config) => {
// console.log(config)
config.headers.Authorization = window.sessionStorage.getItem("token");
//最后必须返回config
return config;
});
Vue.prototype.$http = axios;

Vue.config.productionTip = false;
//注册TreeTable组件
Vue.component("tree-table", TreeTable);
new Vue({
router,
render: (h) => h(App),
}).$mount("#app");

Cate.vue中使用表格

<!-- 表格区域 -->

<tree-table
:data="catelist"
:columns="columns"
:selection-type="false"
:expand-type="false"
show-index
index-text="#"
border
:show-row-hover="false"
>
</tree-table>

配置表格中的列(columns)

export default {
data() {
return {
// 查询条件
querInfo: {
type: 3,
pagenum: 1,
pagesize: 5,
},
// 商品分类的数据列表,默认为空
catelist: [],
// 总数据条数
total: 0,
// 为table指定列的定义
columns: [
{
label: "分类名称",
prop: "cat_name",
},
],
};
},

5、使用自定义模板列渲染表格数据

下面我们需要在商品分类这个表格中展示”是否有效”这一列的数据。而这一列数据需要展示出相应的图标内容,

所以需要用到自定义模板列来渲染对应的数据。

<!-- 表格区域 -->

<tree-table
:data="catelist"
:columns="columns"
:selection-type="false"
:expand-type="false"
show-index
index-text="#"
border
:show-row-hover="false"
>
<!-- 是否有效 -->
<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>
</tree-table>

在表格中,我们增加了一个模板列同时通过作用域插槽获取对应的数据。

关于插槽isok的定义如下

// 为table指定列的定义
columns: [
{
label: "分类名称",
prop: "cat_name",
},
{
label: "是否有效",
// 表示,将当前列定义为模板列
type: "template",
// 表示当前这一列使用模板名称
template: "isok",
},
],

通过label定义当前列的标题,type:'template':表示当前列为模板列,同时template表示这一列使用模板的名称。

下面我们要做的就是使用同样的方式,将“排序”列与“操作”列的内容给构建出来。

<!-- 表格区域 -->

<tree-table
:data="catelist"
:columns="columns"
:selection-type="false"
:expand-type="false"
show-index
index-text="#"
border
:show-row-hover="false"
>
<!-- 是否有效 -->
<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
type="success"
size="mini"
v-else-if="scope.row.cat_level === 1"
>二级</el-tag
>
<el-tag type="warning" size="mini" v-else>三级</el-tag>
</template>
<!-- 操作 -->
<template slot="opt">
<el-button type="primary" icon="el-icon-edit" size="mini"
>编辑</el-button
>
<el-button type="danger" icon="el-icon-delete" size="mini"
>删除</el-button
>
</template>
</tree-table>

对应的数据如下:

// 为table指定列的定义
columns: [
{
label: "分类名称",
prop: "cat_name",
},
{
label: "是否有效",
// 表示,将当前列定义为模板列
type: "template",
// 表示当前这一列使用模板名称
template: "isok",
},
{
label: "排序",
// 表示,将当前列定义为模板列
type: "template",
// 表示当前这一列使用模板名称
template: "order",
},
{
label: "操作",
// 表示,将当前列定义为模板列
type: "template",
// 表示当前这一列使用模板名称
template: "opt",
},
],

6、分页功能实现

<!-- 分页区域 -->
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="querInfo.pagenum"
:page-sizes="[3, 5, 10, 15]"
:page-size="querInfo.pagesize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
>
</el-pagination>

对应方法的实现

// 监听 pagesize 改变
handleSizeChange(newSize) {
this.querInfo.pagesize = newSize;
this.getCateList();
},
// 监听 pagenum 改变
handleCurrentChange(newPage) {
this.querInfo.pagenum = newPage;
this.getCateList();
},

7、构建添加分类的对话框与表单

当单击“添加分类”按钮,会弹出一个窗口,在这个窗口中展示对应的表单。

下面先创建添加分类的对话框,并且在对话框中添加表单

<!-- 添加分类的对话框 -->
<el-dialog
title="添加分类"
:visible.sync="addCateDialogVisible"
width="50%"
>
<!-- 添加分类的表单 -->
<el-form
:model="addCateForm"
:rules="addCateFormRules"
ref="addCateFormRef"
label-width="100px"
>
<el-form-item label="分类名称:" prop="cat_name">
<el-input v-model="addCateForm.cat_name"></el-input>
</el-form-item>
<el-form-item label="父级分类:"> </el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="addCateDialogVisible = false">取 消</el-button>
<el-button type="primary">确 定</el-button>
</span>
</el-dialog>

下面定义对应的数据,以及校验规则

// 控制添加分类对话框的显示与隐藏
addCateDialogVisible: false,
// 添加分类的表单数据对象(这里需要参数API文档)
addCateForm: {
// 将要添加的分类的名称
cat_name: "",
// 父级分类的Id
cat_pid: 0,
// 分类的等级,默认要添加的是1级分类
cat_level: 0,
},
// 添加分类表单的验证规则对象
addCateFormRules: {
cat_name: [
{ required: true, message: "请输入分类名称", trigger: "blur" },
],
},

当单击“添加分类”按钮后,弹出窗口

<el-button type="primary" @click="showAddCateDialog"
>添加分类</el-button
>

showAddCateDialog方法的实现如下

// 点击按钮,展示添加分类的对话框
showAddCateDialog() {
// 再展示出对话框
this.addCateDialogVisible = true;
},

8、获取父级分类数据

在弹出的添加分类窗口中,还需要为其添加一个下拉框,在下拉框中展示父级的类别数据,这样我们在添加某个类别的时候,可以确定其父类。

现在先获取系统中所有的父类的数据(这里只获取前两级,系统共分为3级)

首先定义获取父级类别数据的方法

// 获取父级分类的数据列表
async getParentCateList() {
const { data: res } = await this.$http.get("categories", {
params: { type: 2 },//这里只获取两级
});

if (res.meta.status !== 200) {
return this.$message.error("获取父级分类数据失败!");
}

console.log(res.data);
//把获取到的父类数据赋值给parentCateList属性
this.parentCateList = res.data;
},

getParentCateList方法的调用是在窗口打开的时候被调用的。

// 点击按钮,展示添加分类的对话框
showAddCateDialog() {
// 先获取父级分类的数据列表
this.getParentCateList();
// 再展示出对话框
this.addCateDialogVisible = true;
},

同时在data中定义parentCateList属性,存储获取到的类别数据。

// 父级分类的列表
parentCateList: [],

下面我们要做的就是将parentCateList中存储的类别数据绑定到下拉框中。

9、渲染级联选择器

下面我们需要在弹出的窗口的表单中添加一个级联的组件el-cascader

<!-- 添加分类的对话框 -->
<el-dialog
title="添加分类"
:visible.sync="addCateDialogVisible"
width="50%"
>
<!-- 添加分类的表单 -->
<el-form
:model="addCateForm"
:rules="addCateFormRules"
ref="addCateFormRef"
label-width="100px"
>
<el-form-item label="分类名称:" prop="cat_name">
<el-input v-model="addCateForm.cat_name"></el-input>
</el-form-item>
<el-form-item label="父级分类:">
<!-- options 用来指定数据源 -->
<!-- props 用来指定配置对象, -->
<!-- v-model="selectedKeys":在级联框中选择的类别的编号都存储到selectedKeys数组中-->
<!-- 当选择不同的内容,会触发change事件,这时`selectedKeys`数组中存储的就是选择项的id -->
<el-cascader
expand-trigger="hover"
:options="parentCateList"
:props="cascaderProps"
v-model="selectedKeys"
@change="parentCateChanged"
clearable

>
</el-cascader>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="addCateDialogVisible = false">取 消</el-button>
<el-button type="primary">确 定</el-button>
</span>
</el-dialog>

props中的cascaderProps定义如下:

// 指定级联选择器的配置对象
cascaderProps: {
// value:表示选择一项后获取到的值
value: "cat_id",
//表示在级联窗口中显示的内容
label: "cat_name",
//children表示的是父子关系,这里也是通过服务端返回的`children`来指定内容的父子关系
children: "children",
},

selectedKeys定义

// 选中的父级分类的Id数组
selectedKeys: [],

parentCateChanged方法的实现

// 选择项发生变化触发这个函数
parentCateChanged() {
console.log(this.selectedKeys);
},

最后需要注册:Vue.use(Cascader);

10、根据父分类的变化处理表单中的数据

我们知道,当我们单击“添加分类”窗口中的“确定”按钮的时候,需要将类别表单中的数据发送到服务端。

而表单是与addCateForm对象绑定在一起的。该对象在data中的定义如下:

// 添加分类的表单数据对象(这里需要参数API文档)
addCateForm: {
// 将要添加的分类的名称
cat_name: "",
// 父级分类的Id
cat_pid: 0,
// 分类的等级,默认要添加的是1级分类
cat_level: 0,
},

首先cat_name属性的值已经确定了,就是用户在“分类名称”文本框中输入的内容。

现在cat_pidcat_level属性的值没有确定。

但是要想确定这两个属性的值,就需要考虑到el-cascader组件,也就是用户在该组件中选择的父类别。

我们知道当选择el-cascader中的不同类别时会触发change事件,在对应的事件处理函数parentCateChanged中,确定cat_pidcat_level这两个属性的值。

// 选择项发生变化触发这个函数
parentCateChanged() {
console.log(this.selectedKeys);
// 如果 selectedKeys 数组中的 length 大于0,证明选中的父级分类
// 反之,就说明没有选中任何父级分类
if (this.selectedKeys.length > 0) {
// 父级分类的Id
this.addCateForm.cat_pid = this.selectedKeys[
this.selectedKeys.length - 1
];
// 为当前分类的等级赋值
this.addCateForm.cat_level = this.selectedKeys.length;
return;
} else {
// 父级分类的Id
this.addCateForm.cat_pid = 0;
// 为当前分类的等级赋值
this.addCateForm.cat_level = 0;
}
},

首先判断selectedKeys数组中是否有值,我们知道当用户选择下拉框中的类别数据时,对应的所选择的类别的编号会存储到selectedKeys数组中,如果该数组没有值,表明用户没有选择任何类别。那也就是说现在用户所添加的类别是根类别,所以cat_pidcat_level的值都为0.

如果selectedKeys数组中有值,我们可以取出数组中的最后一个id值作为当前所添加类别的父类别编号。

例如:如果selectedKeys数组中的值为:[12,13] 表明用户选择了编号为12这个根下的编号为13的类别,这时新添加的类别就是编号为13的子类别。

通过以上的处理,我们已经能够确定出cat_pid这个属性的值。

下面思考一下怎样确定cat_level这个属性的值?

该属性表示的是当前类别的等级。如果是一级则该属性的值为0,二级为1,三级为2.所以这里可以根据数组的长度来确定出cat_level的值。还是以上面的例子来说明,如果selectedKeys数组中的值为[12,13],那么获取到的数组长度的值为2,表明现在所添加的类别的等级值为2,也就是三级。

最后可以测试一下:

给窗口的确定按钮,添加单击事件

<span slot="footer" class="dialog-footer">
<el-button @click="addCateDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="addCate">确 定</el-button>
</span>

addCate方法的实现如下:

// 点击按钮,添加新的分类
addCate() {
console.log(this.addCateForm);
},

可以打印addCateForm中的值。

11、完成类别添加

最终的addCate方法的实现如下:

// 点击按钮,添加新的分类
addCate() {
// console.log(this.addCateForm);
this.$refs.addCateFormRef.validate(async (valid) => {
if (!valid) return;
const { data: res } = await this.$http.post(
"categories",
this.addCateForm
);

if (res.meta.status !== 201) {
return this.$message.error("添加分类失败!");
}

this.$message.success("添加分类成功!");
this.getCateList();
this.addCateDialogVisible = false;
});
},

addCateForm中存储的数据通过post方式发送到服务端。

十、分类参数

1、分类参数功能介绍

商品参数用于显示商品的固定的特征信息,可以通过电商平台商品详情页面直观看到。

2、构建分类参数组件

<template>
<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>
<!-- 警告区域 -->
<el-alert
show-icon
title="注意:只允许为第三级分类设置相关参数!"
type="warning"
:closable="false"
></el-alert>

<!-- 选择商品分类区域 -->
<el-row class="cat_opt">
<el-col>
<span>选择商品分类:</span>
<!-- 选择商品分类的级联选择框 -->
<el-cascader
expand-trigger="hover"
:options="catelist"
:props="cateProps"
v-model="selectedCateKeys"
@change="handleChange"
>
</el-cascader>
</el-col>
</el-row>
</el-card>
</div>
</template>
<script>
export default {
data() {
return {
// 商品分类列表
catelist: [],
// 级联选择框的配置对象
cateProps: {
value: "cat_id",
label: "cat_name",
children: "children",
},
// 级联选择框双向绑定到的数组
selectedCateKeys: [],
};
},
created() {
this.getCateList();
},

methods: {
// 获取所有的商品分类列表
async getCateList() {
const { data: res } = await this.$http.get("categories");
if (res.meta.status !== 200) {
return this.$message.error("获取商品分类失败!");
}

this.catelist = res.data;

console.log(this.catelist);
},
// 级联选择框选中项变化,会触发这个函数
handleChange() {
// console.log(this.selectedCateKeys);
this.getParamsData();
},
// 获取参数的列表数据
async getParamsData() {
// 证明选中的不是三级分类
//这里只允许选择第三个类别。
if (this.selectedCateKeys.length !== 3) {
this.selectedCateKeys = [];
return;
}
},
},
};
</script>
<style scoped>
.cat_opt {
margin: 15px 0;
}
</style>

在上面的代码中,构建了级联选择框,并且获取了类别数据,填充到级联选择框中。

同时,这里对级联选择框中的类别选择有要求,只能选择第三个类别。

下面创建路由

import Vue from "vue";
import Router from "vue-router";
import Login from "./components/Login.vue";
import Home from "./components/Home.vue";
import Welcome from "./components/Welcome.vue";
import Users from "./components/user/Users.vue";
import Rights from "./components/power/Rights.vue";
import Roles from "./components/power/Roles.vue";
import Cate from "./components/goods/Cate.vue";
import Params from "./components/goods/Params.vue";
Vue.use(Router);
const router = new Router({
routes: [
{ path: "/", redirect: "/login" },
{ path: "/login", component: Login },
{
path: "/home",
component: Home,
redirect: "/welcome",
children: [
{ path: "/welcome", component: Welcome },
{ path: "/users", component: Users },
{ path: "/rights", component: Rights },
{ path: "/roles", component: Roles },
{ path: "/categories", component: Cate },
{ path: "/params", component: Params },
],
},
],
});

同时,由于在整个页面中,使用了Alter组件,所以需要在element.js文件中注册一下

Vue.use(Alert);

3、渲染Tab页签。

关于分类参数的展示,是通过Tab页签来实现的。

在选择商品分类区域的下面添加el-tabs组件。

<!-- tab 页签区域 -->
<el-tabs v-model="activeName" @tab-click="handleTabClick">
<!-- 添加动态参数的面板 -->
<el-tab-pane label="动态参数" name="many"></el-tab-pane>
<el-tab-pane label="静态属性" name="only"></el-tab-pane>
</el-tabs>

v-model所绑定的属性指的被激活的页签的名称。

data中定义activeName

// 被激活的页签的名称
activeName: "many",

单击页签会执行handleTabClick处理函数。

// tab 页签点击事件的处理函数
handleTabClick() {
console.log(this.activeName);
// this.getParamsData();
},

同时要完成页签组件的注册。

Vue.use(Tabs);
Vue.use(TabPane);

4、渲染添加参数与属性按钮

现在已经将基本的tab页签渲染出来了,下面要做的就是在”动态参数”与“静态属性”这两个页签下面添加相应的按钮,分别为“添加参数”按钮与“添加属性”按钮。

并且当用户选择了“商品分类”下拉框中的第三个类别的时候,这些按钮才会启用。

所以这里通过计算属性来判断数组selectedCateKeys中的长度是否为3,如果为3,表明用户选择了三级类别。

<!-- tab 页签区域 -->
<el-tabs v-model="activeName" @tab-click="handleTabClick">
<!-- 添加动态参数的面板 -->
<el-tab-pane label="动态参数" name="many">
<!-- 添加参数的按钮 -->
<el-button type="primary" size="mini" :disabled="isBtnDisabled">添加参数</el-button>
</el-tab-pane>
<el-tab-pane label="静态属性" name="only">
<el-button type="primary" size="mini" :disabled="isBtnDisabled">添加属性</el-button>
</el-tab-pane>
</el-tabs>

在上面的代码中,我们在页签中添加了相应的按钮。并且让disabled动态绑定了计算属性isBtnDisabled

computed: {
// 如果按钮需要被禁用,则返回true,否则返回false
isBtnDisabled() {
if (this.selectedCateKeys.length !== 3) {
return true;
}
return false;
},
},

如果数组的长度不为3,返回true.表明要禁用按钮。

5、切换Tab页签获取数据

下面要实现的就是,当单击动态参数静态属性这两个页签的时候,要查询到对应的数据,然后绑定到相应的表格中。

要想获得对应的数据,这里需要确定两个参数,一个是用户所选择的下拉框中第三类别的编号,再有一个就是所选择的Tab页签。

怎样获取用户所选择的“商品分类”下拉中用户选择的第三个类别的编号呢?

这里,也是通过计算属性来完成的。

computed: {
// 如果按钮需要被禁用,则返回true,否则返回false
isBtnDisabled() {
if (this.selectedCateKeys.length !== 3) {
return true;
}
return false;
},
// 当前选中的三级分类的Id
cateId() {
if (this.selectedCateKeys.length === 3) {
return this.selectedCateKeys[2];
}
return null;
},
},

关于确定用户到底选择了哪个Tab页签,其实前面我们已经处理过了,

我们给el-tabs标签添加了v-model="activeName" ,并且 activeName: "many",表示的就是用户激活的页签的名称。

同时每一个el-tab-pane 都添加了name 属性

现在,两个参数都已经确定好了,下面要考虑的就是什么时候将这两个参数发送到服务端呢?

第一种情况是:用户选择了“商品分类”下拉框中的某个第三级类别时,就应该发送请求,为什么呢?

因为,我们这是默认会有一个页签是选中的。这是可以将默认选中页签的名称与类别编号发送到服务端。

第二种情况是:用户选择了某个三级类别后,后期有选择了另外一个页签,这是也应该发送请求。

// 级联选择框选中项变化,会触发这个函数
handleChange() {
// console.log(this.selectedCateKeys);
this.getParamsData();
},
// tab 页签点击事件的处理函数
handleTabClick() {
console.log(this.activeName);
this.getParamsData();
},

// 获取参数的列表数据
async getParamsData() {
// 证明选中的不是三级分类
//这里只允许选择第三个类别。
if (this.selectedCateKeys.length !== 3) {
this.selectedCateKeys = [];
return;
}
// 证明选中的是三级分类
console.log(this.selectedCateKeys);
// 根据所选分类的Id,和当前所处的面板,获取对应的参数
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 {
this.onlyTableData = res.data;
}
},

如果用户选择的是”动态参数”,将查询到的数据存储到manyTableData这个状态属性上,否则存储到onlyTableData这个属性上,因为这里我们要将数据绑定到两个表格中。

6、渲染表格

现在,已经将数据全部取出来了,下面将这些数据渲染到相应的表格中。

<!-- tab 页签区域 -->
<el-tabs v-model="activeName" @tab-click="handleTabClick">
<!-- 添加动态参数的面板 -->
<el-tab-pane label="动态参数" name="many">
<!-- 添加参数的按钮 -->
<el-button type="primary" size="mini" :disabled="isBtnDisabled">添加参数</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"
@click="showEditDialog(scope.row.attr_id)"
>编辑</el-button>
<el-button
size="mini"
type="danger"
icon="el-icon-delete"
@click="removeParams(scope.row.attr_id)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="静态属性" name="only">
<el-button type="primary" size="mini" :disabled="isBtnDisabled">添加属性</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"
@click="showEditDialog(scope.row.attr_id)"
>编辑</el-button>
<el-button
size="mini"
type="danger"
icon="el-icon-delete"
@click="removeParams(scope.row.attr_id)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>

在上面的el-tabs页签中,分别添加了两个表格,分别渲染”动态参数”“与”静态属性“相应的数据。

7、渲染添加参数的对话框

这里的参数分为“动态参数“与”静态属性“两种情况,这里我们都是通过一个窗口完成添加的。

el-card组件下面

首先添加一个窗口组件。

<!-- 添加参数的对话框 -->
<el-dialog
:title="'添加' + titleText"
:visible.sync="addDialogVisible"
width="50%"
@close="addDialogClosed"
>
<!-- 添加参数的对话框 -->
<el-form :model="addForm" :rules="addFormRules" ref="addFormRef" label-width="100px">
<el-form-item :label="titleText" prop="attr_name">
<el-input v-model="addForm.attr_name"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="addDialogVisible = false">取 消</el-button>
<el-button type="primary">确 定</el-button>
</span>
</el-dialog>

这里,我们给窗口的标题采用了动态绑定的方式,也是使用了计算属性。

computed: {
// 如果按钮需要被禁用,则返回true,否则返回false
isBtnDisabled() {
if (this.selectedCateKeys.length !== 3) {
return true;
}
return false;
},
// 当前选中的三级分类的Id
cateId() {
if (this.selectedCateKeys.length === 3) {
return this.selectedCateKeys[2];
}
return null;
},
// 动态计算标题的文本
titleText() {
if (this.activeName === "many") {
return "动态参数";
}
return "静态属性";
},
},

还有就是监听窗口关闭事件触发后,对应的处理函数。

// 监听添加对话框的关闭事件
addDialogClosed() {
this.$refs.addFormRef.resetFields();
},

对应的data属性的定义如下:

// 控制添加对话框的显示与隐藏
addDialogVisible: false,
// 添加参数的表单数据对象
addForm: {
attr_name: "",
},
// 添加表单的验证规则对象
addFormRules: {
attr_name: [
{ required: true, message: "请输入参数名称", trigger: "blur" },
],
},

单击“添加属性”与“添加参数”按钮将对话框展示出来。

<!-- 添加参数的按钮 -->
<el-button
type="primary"
size="mini"
:disabled="isBtnDisabled"
@click="addDialogVisible=true"
>添加参数</el-button>
<el-button
type="primary"
size="mini"
:disabled="isBtnDisabled"
@click="addDialogVisible=true"
>添加属性</el-button>

8、完成参数的添加

当单击弹出的窗口中的”添加”按钮的时候,完成对应的添加。

<el-button type="primary" @click="addParams">确 定</el-button>

对应的addParams方法的实现如下:

// 点击按钮,添加参数
addParams() {
this.$refs.addFormRef.validate(async (valid) => {
if (!valid) return;
const { data: res } = await this.$http.post(
`categories/${this.cateId}/attributes`,
{
attr_name: this.addForm.attr_name,//获取表单中的数据
attr_sel: this.activeName,//获取对应的页签名称
}
);

if (res.meta.status !== 201) {
return this.$message.error("添加参数失败!");
}

this.$message.success("添加参数成功!");
this.addDialogVisible = false;
this.getParamsData();
});
},

9、修改参数

在具体修改参数之前,首先会弹出一个对话框,在该对话框中展示要修改的参数。

<!-- 修改参数的对话框 -->
<el-dialog
:title="'修改' + titleText"
:visible.sync="editDialogVisible"
width="50%"
@close="editDialogClosed"
>
<!-- 添加参数的对话框 -->
<el-form :model="editForm" :rules="editFormRules" ref="editFormRef" label-width="100px">
<el-form-item :label="titleText" prop="attr_name">
<el-input v-model="editForm.attr_name"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="editDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="editParams">确 定</el-button>
</span>
</el-dialog>

添加对应的data属性。

// 控制修改对话框的显示与隐藏
editDialogVisible: false,
// 修改的表单数据对象
editForm: {},
// 修改表单的验证规则对象
editFormRules: {
attr_name: [
{ required: true, message: "请输入参数名称", trigger: "blur" },
],
},

给两个表格中的修改按钮,绑定对应的单击事件,弹出上面指定的窗口。

<el-button
size="mini"
type="primary"
icon="el-icon-edit"
@click="showEditDialog(scope.row.attr_id)"
>编辑</el-button>

showEditDialog方法如下:

// 点击按钮,展示修改的对话框
async showEditDialog(attr_id) {
// 查询当前参数的信息
const { data: res } = await this.$http.get(
`categories/${this.cateId}/attributes/${attr_id}`,
{
params: { attr_sel: this.activeName },
}
);

if (res.meta.status !== 200) {
return this.$message.error("获取参数信息失败!");
}

this.editForm = res.data;
this.editDialogVisible = true;
},
// 重置修改的表单
editDialogClosed() {
this.$refs.editFormRef.resetFields();
},

下面就是单击窗口中的“确定”按钮的时候,完成参数信息的更新。

<el-button type="primary" @click="editParams">确 定</el-button>

editParams方法的实现如下:

// 点击按钮,修改参数信息
editParams() {
this.$refs.editFormRef.validate(async (valid) => {
if (!valid) return;
const {
data: res,
} = await this.$http.put(
`categories/${this.cateId}/attributes/${this.editForm.attr_id}`,
{ attr_name: this.editForm.attr_name, attr_sel: this.activeName }
);

if (res.meta.status !== 200) {
return this.$message.error("修改参数失败!");
}

this.$message.success("修改参数成功!");
this.getParamsData();
this.editDialogVisible = false;
});
},

10、删除参数

当单击表格中的“删除”按钮的时候,完成参数数据的删除。

// 根据Id删除对应的参数项
async removeParams(attr_id) {
const confirmResult = await this.$confirm(
'此操作将永久删除该参数, 是否继续?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).catch(err => err)

// 用户取消了删除的操作
if (confirmResult !== 'confirm') {
return this.$message.info('已取消删除!')
}

// 删除的业务逻辑
const { data: res } = await this.$http.delete(
`categories/${this.cateId}/attributes/${attr_id}`
)

if (res.meta.status !== 200) {
return this.$message.error('删除参数失败!')
}

this.$message.success('删除参数成功!')
this.getParamsData()
}

以上就是完成数据删除的业务操作。

下面需要给两个表格中的“删除”按钮添加对应的事件。

<el-button
size="mini"
type="danger"
icon="el-icon-delete"
@click="removeParams(scope.row.attr_id)"
>删除</el-button>

11、渲染参数下的可选项

当单击表格中第一列,展开列的时候,应该能够展示出每个类别下的对应的属性。

在展开列中展示的这些属性,都是保存在attr_vals这个属性中的,并且是以空格进行分隔,所以这里,我们可以以“空格”来作为分隔符,分隔成数组,然后通过遍历的方式,再将其渲染到页面中。

// 获取参数的列表数据
async getParamsData() {
// 证明选中的不是三级分类
//这里只允许选择第三个类别。
if (this.selectedCateKeys.length !== 3) {
this.selectedCateKeys = [];
return;
}
// 证明选中的是三级分类
console.log(this.selectedCateKeys);
// 根据所选分类的Id,和当前所处的面板,获取对应的参数
const { data: res } = await this.$http.get(
`categories/${this.cateId}/attributes`,
{
params: { sel: this.activeName },
}
);

if (res.meta.status !== 200) {
return this.$message.error("获取参数列表失败!");
}
//对attr_vals的内容进行分隔。
res.data.forEach((item) => {
item.attr_vals = item.attr_vals ? item.attr_vals.split(" ") : [];
});
console.log(res.data);
if (this.activeName === "many") {
this.manyTableData = res.data;
} else {
this.onlyTableData = res.data;
}
},

getdParamsData方法中,对服务端返回的内容进行遍历,然后获取每一项中的attr_vals属性,然后按照空格进行分隔。

分隔完后,返回的是一个数组,交给了item.attr_vals

在前端对item.attr_vals进行循环遍历。

在两个表格中的展开列中,添加如下的内容。

<!-- 展开行 -->
<el-table-column type="expand">
<template slot-scope="scope">
<!-- 循环渲染Tag标签 -->
<el-tag
v-for="(item, i) in scope.row.attr_vals"
:key="i"
closable
@close="handleClose(i, scope.row)"
>{{item}}</el-tag>
</template>
</el-table-column>

12、实现指定参数属性的添加

经过上面的操作,现在已经实现了,单击展开列,会展示出相应的属性。下面实现属性的添加。

首先先展示出添加的窗口。

<!-- 展开行 -->
<el-table-column type="expand">
<template slot-scope="scope">
<!-- 循环渲染Tag标签 -->
<el-tag
v-for="(item, i) in scope.row.attr_vals"
:key="i"
closable
@close="handleClose(i, scope.row)"
>{{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>

在两个表格中的展开行中,添加输入框与按钮。

如果scope.row.inputVisibletrue,展示输入框,否则展示添加按钮。

下面看一下scope.row.inputVisible是哪来的呢?

//对attr_vals的内容进行分隔。
res.data.forEach((item) => {
item.attr_vals = item.attr_vals ? item.attr_vals.split(" ") : [];
// 控制文本框的显示与隐藏
item.inputVisible = false;
// 文本框中输入的值
item.inputValue = "";
});

getParamsData方法中,在对返回的数据进行遍历,取出每一项后,按照空格分隔,然后在给item对象添加inputVisible属性设置了false,inputValue设置为空字符串。

这样做的目的就是保证了scope.row.inputVisible,值的唯一,也就是每行中的值都是唯一的。

handleInputConfirm方法的实现如下:

// 文本框失去焦点,或摁下了 Enter 都会触发
async handleInputConfirm(row) {
console.log(row);
},

当点击按钮的时候,会执行showInput方法,该方法的实现如下:

// 点击按钮,展示文本输入框
showInput(row) {
// console.log(row);
row.inputVisible = true;
},

这样将inputVisible设置为true后,会展示出对应的文本框。

现在能够完成添加按钮与文本框的切换了,接下来要实现的就是让文本框能够自动获取到焦点。

// 点击按钮,展示文本输入框
showInput(row) {
// console.log(row);
row.inputVisible = true;
// $nextTick 方法的作用,就是当页面上元素被重新渲染之后,才会执行回调函数中的代码
this.$nextTick(() => {
this.$refs.saveTagInput.$refs.input.focus();
});
},

showInput方法中,添加了$nextTick函数。

row.inputVisible = true;设置为true后,并不能保证立即获取到文本框,而当$nextTick方法执行了,说明文本框渲染完了。

下面要实现的就是属性的添加了

当用户在文本框中输入了值以后,按下回车键或者让文本框失去焦点,就需要将用户在文本框中输入的内容发送到服务端,来完成对应的属性信息的添加操作。

下面需要修改handleInputConfirm方法,

// 文本框失去焦点,或摁下了 Enter 都会触发
async handleInputConfirm(row) {
// console.log(row);
if (row.inputValue.trim().length === 0) {
row.inputValue = "";
row.inputVisible = false;
return;
}
// 如果没有return,则证明输入的内容,需要做后续处理
row.attr_vals.push(row.inputValue.trim());
row.inputValue = "";//清空文本框中的内容
row.inputVisible = false; //隐藏文本框。
// 需要发起请求,保存这次操作
this.saveAttrVals(row);
},
// 将对 attr_vals 的操作,保存到数据库
async saveAttrVals(row) {
// 需要发起请求,保存这次操作
const { data: res } = await this.$http.put(
`categories/${this.cateId}/attributes/${row.attr_id}`,
{
attr_name: row.attr_name,
attr_sel: row.attr_sel,
//row.attr_vals是一个数组,而服务端需要的是一个以空格进行分隔的字符串。
attr_vals: row.attr_vals.join(" "),
}
);

if (res.meta.status !== 200) {
return this.$message.error("修改参数项失败!");
}

this.$message.success("修改参数项成功!");
},

handleInputConfirm方法中,首先判断一下inputValue中去掉空格以后的长度是否为0,如果为零,说明用户没有输入任何内容或者是输入了很多的空格,

这时清空内容将,文本框隐藏。

如果输入了内容后,将输入的内容清除空格,然后添加到attr_vals数组中,该数组中存储的就是属性信息。然后将文本框中的内容清空,同时隐藏文本框。

下面调用saveAttrVals方法,将内容发送到服务端,保存到数据库中.

13、删除参数

给属性所在的el-tag 标签添加删除事件。

我们知道,给el-tag标签添加了 closable属性以后,在有上角会出现,叉号,而点击叉号就会触发close事件。

<el-tag
v-for="(item, i) in scope.row.attr_vals"
:key="i"
closable
@close="handleClose(i, scope.row)"
>{{item}}</el-tag>

对应的handleClose方法如下:

// 删除对应的参数可选项
handleClose(i, row) {
//移除指定的内容。
row.attr_vals.splice(i, 1);
this.saveAttrVals(row);
},

十一、商品列表

1、商品数据展示

components目录下的goods目录下创建List.vue文件,实现整体的结构如下:

<template>
<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>
<el-row :gutter="20">
<el-col :span="8">
<el-input placeholder="请输入内容" v-model="queryInfo.query" clearable @clear="getGoodsList">
<el-button slot="append" icon="el-icon-search" @click="getGoodsList"></el-button>
</el-input>
</el-col>
<el-col :span="4">
<el-button type="primary" @click="goAddpage">添加商品</el-button>
</el-col>
</el-row>

<!-- table表格区域 -->
<el-table :data="goodslist" border stripe>
<el-table-column type="index"></el-table-column>
<el-table-column label="商品名称" prop="goods_name"></el-table-column>
<el-table-column label="商品价格(元)" prop="goods_price" width="95px"></el-table-column>
<el-table-column label="商品重量" prop="goods_weight" width="70px"></el-table-column>
<el-table-column label="创建时间" prop="add_time" width="140px">
<template slot-scope="scope">{{scope.row.add_time | dateFormat}}</template>
</el-table-column>
<el-table-column label="操作" width="130px">
<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"
@click="removeById(scope.row.goods_id)"
></el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页区域 -->
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="queryInfo.pagenum"
:page-sizes="[5, 10, 15, 20]"
:page-size="queryInfo.pagesize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
background
></el-pagination>
</el-card>
</div>
</template>
<script>
export default {
data() {
return {
// 查询参数对象
queryInfo: {
query: "",
pagenum: 1,
pagesize: 10,
},
// 商品列表
goodslist: [],
// 总数据条数
total: 0,
};
},
created() {
this.getGoodsList();
},
methods: {
// 根据分页获取对应的商品列表
async getGoodsList() {
const { data: res } = await this.$http.get("goods", {
params: this.queryInfo,
});

if (res.meta.status !== 200) {
return this.$message.error("获取商品列表失败!");
}

this.$message.success("获取商品列表成功!");
console.log(res.data);
this.goodslist = res.data.goods;
this.total = res.data.total;
},
handleSizeChange(newSize) {
this.queryInfo.pagesize = newSize;
this.getGoodsList();
},
handleCurrentChange(newPage) {
this.queryInfo.pagenum = newPage;
this.getGoodsList();
},
},
};
</script>

路由设置:

import Vue from "vue";
import Router from "vue-router";
import Login from "./components/Login.vue";
import Home from "./components/Home.vue";
import Welcome from "./components/Welcome.vue";
import Users from "./components/user/Users.vue";
import Rights from "./components/power/Rights.vue";
import Roles from "./components/power/Roles.vue";
import Cate from "./components/goods/Cate.vue";
import Params from "./components/goods/Params.vue";
import GoodsList from "./components/goods/List.vue";
Vue.use(Router);
const router = new Router({
routes: [
{ path: "/", redirect: "/login" },
{ path: "/login", component: Login },
{
path: "/home",
component: Home,
redirect: "/welcome",
children: [
{ path: "/welcome", component: Welcome },
{ path: "/users", component: Users },
{ path: "/rights", component: Rights },
{ path: "/roles", component: Roles },
{ path: "/categories", component: Cate },
{ path: "/params", component: Params },
//商品列表
{ path: "/goods", component: GoodsList },
],
},

2、全局过滤器的创建

main.js文件中创建全局的过滤器,对时间进行处理。

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}`;
});

在表格的作用域插槽中,获取时间数据,然后使用上面的过滤器,进行时间日期的过滤。

<el-table-column label="创建时间" prop="add_time" width="140px">
<template slot-scope="scope">{{scope.row.add_time | dateFormat}}</template>
</el-table-column>

3、删除商品信息

具体的删除操作如下:

async removeById(id) {
const confirmResult = await this.$confirm(
'此操作将永久删除该商品, 是否继续?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).catch(err => err)

if (confirmResult !== 'confirm') {
return this.$message.info('已经取消删除!')
}

const { data: res } = await this.$http.delete(`goods/${id}`)

if (res.meta.status !== 200) {
return this.$message.error('删除失败!')
}

this.$message.success('删除成功!')
this.getGoodsList()
},

单击表格中的删除按钮的时候,调用上面的方法。

<el-button type="danger" icon="el-icon-delete" size="mini" @click="removeById(scope.row.goods_id)"></el-button>

十二、商品添加

1、商品添加页面构建

当单击商品列表页面中的“添加商品”按钮,会跳转到商品添加页面。

<el-col :span="4">
<el-button type="primary" @click="goAddpage">添加商品</el-button>
</el-col>

goAddpage方法实现如下:

goAddpage() {
this.$router.push("/goods/add");
},

路由设置如下:

import Vue from "vue";
import Router from "vue-router";
import Login from "./components/Login.vue";
import Home from "./components/Home.vue";
import Welcome from "./components/Welcome.vue";
import Users from "./components/user/Users.vue";
import Rights from "./components/power/Rights.vue";
import Roles from "./components/power/Roles.vue";
import Cate from "./components/goods/Cate.vue";
import Params from "./components/goods/Params.vue";
import GoodsList from "./components/goods/List.vue";
import Add from "./components/goods/Add.vue";
Vue.use(Router);
const router = new Router({
routes: [
{ path: "/", redirect: "/login" },
{ path: "/login", component: Login },
{
path: "/home",
component: Home,
redirect: "/welcome",
children: [
{ path: "/welcome", component: Welcome },
{ path: "/users", component: Users },
{ path: "/rights", component: Rights },
{ path: "/roles", component: Roles },
{ path: "/categories", component: Cate },
{ path: "/params", component: Params },
{ path: "/goods", component: GoodsList },
//添加商品
{ path: "/goods/add", component: Add },
],
},
],
});

component目录下的goods目录下面创建Add.vue这个组件,基本的布局如下:

<template>
<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>
<!-- 提示区域 -->
<el-alert title="添加商品信息" type="info" center show-icon :closable="false"></el-alert>
<!-- 步骤条区域 -->
<el-steps :space="200" :active="activeIndex" 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-tabs :tab-position="'left'" style="height:200px">
<el-tab-pane label="基本信息">基本信息</el-tab-pane>
<el-tab-pane label="商品参数">商品参数</el-tab-pane>
<el-tab-pane label="商品属性">商品属性</el-tab-pane>
<el-tab-pane label="商品图片">商品图片</el-tab-pane>
<el-tab-pane label="商品内容">商品内容</el-tab-pane>
</el-tabs>
</el-card>
</div>
</template>
<script>
export default {
data() {
return {
activeIndex: 0,
};
},
};
</script>

element.js文件中完成对步骤组件的注册。

Vue.use(Steps);
Vue.use(Step);

下面要实现的是单击tab栏,对应的步骤条也发生改变,也就是实现tab栏与步骤条之间的联动效果。

首先给tabs栏添加v-model属性,该属性绑定的值为activeIndex,这样当我们选择tabs中的不同的项的时候对应的name属性的值会赋值给activeIndex这个data属性。

<!-- tab栏区域 -->
<el-tabs :tab-position="'left'" style="height:200px" v-model="activeIndex">
<el-tab-pane label="基本信息" name="0">基本信息</el-tab-pane>
<el-tab-pane label="商品参数" name="1">商品参数</el-tab-pane>
<el-tab-pane label="商品属性" name="2">商品属性</el-tab-pane>
<el-tab-pane label="商品图片" name="3">商品图片</el-tab-pane>
<el-tab-pane label="商品内容" name="4">商品内容</el-tab-pane>
</el-tabs>

下面看一下步骤条的更改

<el-steps :space="200" :active="activeIndex-0" finish-status="success" align-center>

当选择步骤条中的某一项时,对应的activeIndex中存储的就是这一项的一个编号(从0开始计算)。

这时我们可以看到步骤条与导航条共享了activeIndex这个状态属性。

但是要注意的就是步骤条中需要的activeIndex是一个数字,所以这里可以减去0,通过这种方式来转换成数字。

而导航条中使用的activeIndex属性的值为字符串。

2、基本表单实现

在这里,我们将表单的内容都分发到不同的页签下面。

首先构建的是第一个页签“基本信息”的表单。

<template>
<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>
<!-- 提示区域 -->
<el-alert title="添加商品信息" type="info" center show-icon :closable="false"></el-alert>
<!-- 步骤条区域 -->
<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-form
:model="addForm"
:rules="addFormRules"
ref="addFormRef"
label-width="100px"
label-position="top"
>
<el-tabs :tab-position="'left'" v-model="activeIndex">
<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
expand-trigger="hover"
:options="catelist"
:props="cateProps"
v-model="addForm.goods_cat"
@change="handleChange"
></el-cascader>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="商品参数" name="1">商品参数</el-tab-pane>
<el-tab-pane label="商品属性" name="2">商品属性</el-tab-pane>
<el-tab-pane label="商品图片" name="3">商品图片</el-tab-pane>
<el-tab-pane label="商品内容" name="4">商品内容</el-tab-pane>
</el-tabs>
</el-form>
</el-card>
</div>
</template>
<script>
export default {
data() {
return {
activeIndex: 0,
// 添加商品的表单数据对象
addForm: {
goods_name: "",
goods_price: 0,
goods_weight: 0,
goods_number: 0,
// 商品所属的分类数组
goods_cat: [],
},
addFormRules: {
goods_name: [
{ required: true, message: "请输入商品名称", trigger: "blur" },
],
goods_price: [
{ required: true, message: "请输入商品价格", trigger: "blur" },
],
goods_weight: [
{ required: true, message: "请输入商品重量", trigger: "blur" },
],
goods_number: [
{ required: true, message: "请输入商品数量", trigger: "blur" },
],
goods_cat: [
{ required: true, message: "请选择商品分类", trigger: "blur" },
],
},
// 商品分类列表
catelist: [],
cateProps: {
label: "cat_name",
value: "cat_id",
children: "children",
},
};
},
created() {
this.getCateList();
},
methods: {
// 获取所有商品分类数据
async getCateList() {
const { data: res } = await this.$http.get("categories");

if (res.meta.status !== 200) {
return this.$message.error("获取商品分类数据失败!");
}

this.catelist = res.data;
console.log(this.catelist);
},
// 级联选择器选中项变化,会触发这个函数
handleChange() {
console.log(this.addForm.goods_cat);
if (this.addForm.goods_cat.length !== 3) {
this.addForm.goods_cat = [];
}
},
},
};
</script>

在上面的代码中,最主要的就是使用”表单”把整个tabs给包裹起来了,同时在“基本信息”这一个选项中,构建了基本的表单元素。

3、阻止标签页切换

如果用户在访问“基本信息”这一个页签中的内容时候,如果没有选择下拉菜单中的商品分类,并且是没有选择三级分类,然后又去点击其它的页签内容,这时应该给出相应的提示,禁止切换。

这里主要是使用了before-leave这个事件,该事件的触发是在切换页签的时候发生。我们可以在该事件对应的处理函数中,做相应的判断,判断是否选择了下拉框中的商品类别。如果没有,则禁止浏览其它页签。

<el-tabs :tab-position="'left'" v-model="activeIndex" :before-leave="beforeTabLeave">

beforeTabLeave方法的实现如下:

beforeTabLeave(activeName, oldActiveName) {
// console.log('即将离开的标签页名字是:' + oldActiveName)
// console.log('即将进入的标签页名字是:' + activeName)
// return false
//如果现在访问的是第一个页签(基本信息),并且没有选择三级类别,禁止访问其它的内容。
if (oldActiveName === "0" && this.addForm.goods_cat.length !== 3) {
this.$message.error("请先选择商品分类!");
return false;
}
},

4、获取动态参数列表数据

当用户单击“商品参数”这个页签的时候,要获取对应的动态参数。

这时给el-tabs页签,添加tab-click事件,该事件是在单击页签的时候触发,在其对应的函数中,根据在“基本信息”这个页签中,选择的三级商品类别的编号来查询出对应的动态参数。

<el-tabs v-model="activeIndex" :tab-position="'left'" :before-leave="beforeTabLeave" @tab-click="tabClicked">

在上面的代码中,给el-tabs组件添加tab-click事件。

tabClicked对应的处理函数如下:

async tabClicked() {
// console.log(this.activeIndex)
// 证明访问的是动态参数面板,cateId类别编号,可以通过计算属性获取。
if (this.activeIndex === "1") {
const { data: res } = await this.$http.get(
`categories/${this.cateId}/attributes`,
{
params: { sel: "many" },
}
);

if (res.meta.status !== 200) {
return this.$message.error("获取动态参数列表失败!");
}

console.log(res.data);

this.manyTableData = res.data;
}
},

计算属性如下:

computed: {
cateId() {
//条件成立表明选择了三级类别。
if (this.addForm.goods_cat.length === 3) {
//获取最后一个类别编号
return this.addForm.goods_cat[2];
}
return null;
},
},

现在已经将动态数据查询出来了,下面需要将其渲染到页面中。

<el-tab-pane label="商品参数" name="1">
<!-- 渲染表单的Item项 -->
<el-form-item :label="item.attr_name" v-for="item in manyTableData" :key="item.attr_id">
<!-- 复选框组 -->
<el-checkbox-group v-model="item.attr_vals">
<el-checkbox :label="cb" v-for="(cb, i) in item.attr_vals" :key="i" border></el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-tab-pane>

在“商品参数”这个页签中,展示出对应的动态参数,我们知道每个参数名称前面都会有一个复选框,所以这里需要构建一个表单的项el-form-item.

然后对manyTableData中存储的动态数据进行循环遍历,遍历的就是复选框,复选框在el-checkbox-group中就是一组,而且该标签需要一个v-model进行双向数据绑定,绑定的类型必须是一个数组。

el-form-item循环,展示的是属性名称(例如颜色),对复选框进行换行展示的就是动态参数(例如,红色,黑色等)。

在什么时候将attr_vals转换成数组呢?

tabClicked方法中,将attr_vals转换成数组。

  async tabClicked() {
// console.log(this.activeIndex)
// 证明访问的是动态参数面板,cateId类别编号,可以通过计算属性获取。
if (this.activeIndex === "1") {
const { data: res } = await this.$http.get(
`categories/${this.cateId}/attributes`,
{
params: { sel: "many" },
}
);

if (res.meta.status !== 200) {
return this.$message.error("获取动态参数列表失败!");
}

console.log("aaaa=", res.data);
//将attr_vals内容转换成数组
res.data.forEach((item) => {
item.attr_vals =
item.attr_vals.length === 0 ? [] : item.attr_vals.split(" ");
});
this.manyTableData = res.data;
}
},
},

5、获取静态属性

这里我们主要实现的就是商品属性这个页签中数据的展示操作。

这里是将静态属性的内容填充到表单中。

在第4小节的课程中,我们获取的是动态参数列表。

在获取动态参数的时候,在tabClicked 方法中我们是判断this.activeIndex的取值是否为1,如果为1,表示的是用户选择了“商品参数”这个页签,如果this.activeIndex 的取值为2,表示的是用户选择了“商品属性”这个标签。

这需要获取对应的静态属性数据。

async tabClicked() {
// console.log(this.activeIndex)
// 证明访问的是动态参数面板,cateId类别编号,可以通过计算属性获取。
if (this.activeIndex === "1") {
const { data: res } = await this.$http.get(
`categories/${this.cateId}/attributes`,
{
params: { sel: "many" },
}
);

if (res.meta.status !== 200) {
return this.$message.error("获取动态参数列表失败!");
}

console.log("aaaa=", res.data);
//将attr_vals内容转换成数组
res.data.forEach((item) => {
item.attr_vals =
item.attr_vals.length === 0 ? [] : item.attr_vals.split(" ");
});
this.manyTableData = res.data;
} else if (this.activeIndex === "2") {
//获取静态属性
const { data: res } = await this.$http.get(
`categories/${this.cateId}/attributes`,
{
params: { sel: "only" },
}
);

if (res.meta.status !== 200) {
return this.$message.error("获取静态属性失败!");
}

console.log(res.data);
this.onlyTableData = res.data;
}
},

在上面的代码中将获取到的静态属性数据赋值给了onlyTableData这个状态属性。

下面将该属性中的内容渲染到页面中就可以了。

<el-tab-pane label="商品属性" name="2">
<el-form-item :label="item.attr_name" v-for="item in onlyTableData" :key="item.attr_id">
<el-input v-model="item.attr_vals"></el-input>
</el-form-item>
</el-tab-pane>

这里的attr_vals不需要为数组。

6、图片上传

在商品图片这一个页签中,主要的就是实现图片的上传功能。

下面看一下具体的实现过程

这里使用的Upload组件来实现文件上传。

<el-tab-pane label="商品图片" name="3">
<!-- action 表示图片要上传到的后台API地址 -->
<el-upload
:action="uploadURL"
:on-preview="handlePreview"
:on-remove="handleRemove"
list-type="picture"
:headers="headerObj"
:on-success="handleSuccess"
>
<el-button size="small" type="primary">点击上传</el-button>
</el-upload>
</el-tab-pane>

on-preview表示的是处理图片预览效果

on-remove表示的是处理移除图片的操作

list-type:表示的上传组件展示的效果

headers: 图片上传组件的headers请求头对象,这里非常关键,完成API权限校验

on-success:图片上传成功后的处理。

对应的data中属性的定义

// 上传图片的URL地址
uploadURL: 'http://127.0.0.1:8888/api/private/v1/upload',
// 图片上传组件的headers请求头对象
headerObj: {
Authorization: window.sessionStorage.getItem('token')
},

对应的方法处理

// 处理图片预览效果
handlePreview() {},
// 处理移除图片的操作
handleRemove() {},
// 监听图片上传成功的事件
handleSuccess(response) {
console.log(response);
// 1. 拼接得到一个图片信息对象
const picInfo = { pic: response.data.tmp_path };
// 2. 将图片信息对象,push 到pics数组中
this.addForm.pics.push(picInfo);
console.log(this.addForm);
},

上传成功后,我们要获取服务端返回的图片的路径,将其存储到一个数组pics中(存储的元素格式为对象,具体格式参考API文档),最后提交表单的时候会发送到服务端。而发送服务端的内容不仅包含了上传成功的图片路径,还有前面表单中输入的数据,表单已经与addForm属性绑定了,所以这里可以将pics数组也添加到addForm这个表单中,最后将addForm中的内容发送到服务端就可以了。

// 添加商品的表单数据对象
addForm: {
goods_name: "",
goods_price: 0,
goods_weight: 0,
goods_number: 0,
// 商品所属的分类数组
goods_cat: [],
// 图片的数组
pics: [],
},

7、图片删除

可以删除图片。

// 处理移除图片的操作
handleRemove(file) {
// console.log(file)
// 1. 获取将要删除的图片的临时路径
const filePath = file.response.data.tmp_path;
// 2. 从 pics 数组中,找到这个图片对应的索引值
const i = this.addForm.pics.findIndex((x) => x.pic === filePath);
// 3. 调用数组的 splice 方法,把图片信息对象,从 pics 数组中移除
this.addForm.pics.splice(i, 1);
console.log(this.addForm);
},

8、图片预览

当单击图片的名称的时候,可以实现图片预览的效果。

// 处理图片预览效果
handlePreview(file) {
console.log(file);
this.previewPath = file.response.data.url;
this.previewVisible = true;
},

将服务端返回的绝对路径给previewPath属性,同时让窗口打开,把previewVisible属性设置为true.

属性定义

previewPath: "",
previewVisible: false,

窗口的定义

<!-- 图片预览 -->
<el-dialog title="图片预览" :visible.sync="previewVisible" width="50%">
<img :src="previewPath" alt class="previewImg" />
</el-dialog>

9、富文本编辑器使用

首先安装富文本编辑器

npm install vue-quill-editor --save

下面可以进行全局配置。

main.js文件中注册组件,并导入相应的样式

// 导入富文本编辑器
import VueQuillEditor from "vue-quill-editor";
// require styles 导入富文本编辑器对应的样式
import "quill/dist/quill.core.css";
import "quill/dist/quill.snow.css";
import "quill/dist/quill.bubble.css";

// 将富文本编辑器,注册为全局可用的组件
Vue.use(VueQuillEditor);

下面具体使用富文本编辑器

<el-tab-pane label="商品内容" name="4">
<!-- 富文本编辑器组件 -->
<quill-editor v-model="addForm.goods_introduce"></quill-editor>
<!-- 添加商品的按钮 -->
<el-button type="primary" class="btnAdd" @click="add"
>添加商品</el-button
>
</el-tab-pane>

在上面的代码中,将富文本编辑器与addForm中的goods_introduce 属性进行绑定,这样可以通过该属性获取用户在富文本编辑器中输入的内容。

同时在“商品内容”这个页签中添加了“添加商品”按钮,单击该按钮可以完成商品的添加。

addForm: {
goods_name: "",
goods_price: 0,
goods_weight: 0,
goods_number: 0,
// 商品所属的分类数组
goods_cat: [],
// 图片的数组
pics: [],
// 商品的详情描述
goods_introduce: "",
},

global.css文件中,定义样式来控制编辑器的高度。

.ql-editor {
min-height: 300px;
}

10、完成商品的添加

下面要处理的就是商品的添加。

当单击“添加商品”按钮的时候,会执行add方法,该方法的具体实现如下:

// 添加商品
add() {
this.$refs.addFormRef.validate(async (valid) => {
if (!valid) {
return this.$message.error("请填写必要的表单项!");
}
// 执行添加的业务逻辑
// lodash cloneDeep(obj)
const form = _.cloneDeep(this.addForm);
form.goods_cat = form.goods_cat.join(",");
// 处理动态参数
this.manyTableData.forEach((item) => {
const newInfo = {
attr_id: item.attr_id,
attr_value: item.attr_vals.join(" "),
};
this.addForm.attrs.push(newInfo);
});
// 处理静态属性
this.onlyTableData.forEach((item) => {
const newInfo = { attr_id: item.attr_id, attr_value: item.attr_vals };
this.addForm.attrs.push(newInfo);
});
form.attrs = this.addForm.attrs;
console.log(form);

// 发起请求添加商品
// 商品的名称,必须是唯一的
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");
});
},

在上面的代码中

首先先完成表单的校验

然后使用lodash中的cloneDeep方法对addForm这个对象进行了深拷贝。这里为什么会对addForm对象进行深拷贝呢?

因为在向服务端发送数据时,要求goods_cat中的内容必须是字符串(以逗号作为分隔),而该属性是数组。

那么如果采用如下的写法是否可以呢?

this.addForm.goods_cat=this.addForm.goods_cat.join(',')

以上写法是不可以的,因为在向服务端发送的数据中,要求goods_cat是字符串,但是在el-cascader组件中使用goods_cat 必须为数组。

所以这里只能通过深拷贝重新拷贝出一个对象。最终将该对象(form)中的内容发送到服务端。

lodash的安装如下:

npm i --save lodash

安装好以后可以进行导入

import _ from 'lodash'

下面还需要注意的一点就是,根据文档的要求,动态参数与静态属性都要放到addForm 对象中的attrs这个数组中。

所以这里首先对manyTableData进行循环遍历,创建newInfo对象来组织数据,注意:attr_value需要的字符串,所以将attr_vals转换成了字符串。

对于静态属性处理的时候,attr_vals本身就是字符串,无序转换。

下面将处理好的addForm中的attrs中的数据赋值给拷贝对象中的atrrs, 最后将其发送到服务端。

addForm中定义attrs数组

// 添加商品的表单数据对象
addForm: {
goods_name: '',
goods_price: 0,
goods_weight: 0,
goods_number: 0,
// 商品所属的分类数组
goods_cat: [],
// 图片的数组
pics: [],
// 商品的详情描述
goods_introduce: '',
attrs: []
},

十三、订单列表

1、订单数据展示

实现订单数据的展示,并且完成相应的分页处理。

列表展示的代码如下:

<template>
<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>
<el-row>
<el-col :span="8">
<el-input placeholder="请输入内容">
<el-button slot="append" icon="el-icon-search"></el-button>
</el-input>
</el-col>
</el-row>

<!-- 订单列表数据 -->
<el-table :data="orderlist" border stripe>
<el-table-column type="index"></el-table-column>
<el-table-column label="订单编号" prop="order_number"></el-table-column>
<el-table-column label="订单价格" prop="order_price"></el-table-column>
<el-table-column label="是否付款" prop="pay_status">
<template slot-scope="scope">
<el-tag type="success" v-if="scope.row.pay_status === '1'">已付款</el-tag>
<el-tag type="danger" v-else>未付款</el-tag>
</template>
</el-table-column>
<el-table-column label="是否发货" prop="is_send">
<template slot-scope="scope">
<template>{{scope.row.is_send}}</template>
</template>
</el-table-column>
<el-table-column label="下单时间" prop="create_time">
<template slot-scope="scope">{{scope.row.create_time | dateFormat}}</template>
</el-table-column>
<el-table-column label="操作">
<template>
<el-button size="mini" type="primary" icon="el-icon-edit" @click="showBox"></el-button>
<el-button size="mini" type="success" icon="el-icon-location" @click="showProgressBox"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页区域 -->
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="queryInfo.pagenum"
:page-sizes="[5, 10, 15]"
:page-size="queryInfo.pagesize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
></el-pagination>
</el-card>
</div>
</template>
<script>
export default {
data() {
return {
queryInfo: {
query: "",
pagenum: 1,
pagesize: 10,
},
total: 0,
orderlist: [],
};
},
created() {
this.getOrderList();
},
methods: {
async getOrderList() {
const { data: res } = await this.$http.get("orders", {
params: this.queryInfo,
});

if (res.meta.status !== 200) {
return this.$message.error("获取订单列表失败!");
}

console.log(res);
this.total = res.data.total;
this.orderlist = res.data.goods;
},
handleSizeChange(newSize) {
this.queryInfo.pagesize = newSize;
this.getOrderList();
},
handleCurrentChange(newPage) {
this.queryInfo.pagenum = newPage;
this.getOrderList();
},
},
};
</script>

基本路由设置

import Vue from "vue";
import Router from "vue-router";
import Login from "./components/Login.vue";
import Home from "./components/Home.vue";
import Welcome from "./components/Welcome.vue";
import Users from "./components/user/Users.vue";
import Rights from "./components/power/Rights.vue";
import Roles from "./components/power/Roles.vue";
import Cate from "./components/goods/Cate.vue";
import Params from "./components/goods/Params.vue";
import GoodsList from "./components/goods/List.vue";
import Add from "./components/goods/Add.vue";
import Order from "./components/order/Order.vue";
Vue.use(Router);
const router = new Router({
routes: [
{ path: "/", redirect: "/login" },
{ path: "/login", component: Login },
{
path: "/home",
component: Home,
redirect: "/welcome",
children: [
{ path: "/welcome", component: Welcome },
{ path: "/users", component: Users },
{ path: "/rights", component: Rights },
{ path: "/roles", component: Roles },
{ path: "/categories", component: Cate },
{ path: "/params", component: Params },
{ path: "/goods", component: GoodsList },
{ path: "/goods/add", component: Add },
{ path: "/orders", component: Order },
],
},
],
});

2、地址编辑

这里主要是实现地址的三级联动

当单击表格中的编辑按钮的时候弹出如下的表格

<el-button size="mini" type="primary" icon="el-icon-edit" @click="showBox"></el-button>
<!-- 修改地址的对话框 -->
<el-dialog
title="修改地址"
:visible.sync="addressVisible"
width="50%"
@close="addressDialogClosed"
>
<el-form
:model="addressForm"
:rules="addressFormRules"
ref="addressFormRef"
label-width="100px"
>
<el-form-item label="省市区/县" prop="address1">
<el-cascader :options="cityData" v-model="addressForm.address1"></el-cascader>
</el-form-item>
<el-form-item label="详细地址" prop="address2">
<el-input v-model="addressForm.address2"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="addressVisible = false">取 消</el-button>
<el-button type="primary" @click="addressVisible = false">确 定</el-button>
</span>
</el-dialog>

关于表单相关的属性定义

addressVisible: false,
addressForm: {
address1: [],//用来存储在下拉框中选择的地址信息,这里需要用数组
address2: "",
},
addressFormRules: {
address1: [
{ required: true, message: "请选择省市区县", trigger: "blur" },
],
address2: [
{ required: true, message: "请填写详细地址", trigger: "blur" },
],
},
cityData,//下拉框展示的地址数据源

el-cascader 组件展示的省市地区,来外部文件

import cityData from "./citydata.js";

同时在data中定义cityData

showBox方法的实现如下

// 展示修改地址的对话框
showBox() {
this.addressVisible = true;
},

addressDialogClosed方法实现如下。

addressDialogClosed() {
this.$refs.addressFormRef.resetFields();
},

十四、项目优化

项目优化的策略

  • 生成打包报告
  • 第三方库启用CDN
  • Element-UI组件按需加载
  • 路由懒加载
  • 首页内容定制

1、进度条使用

当数据加载慢的时候,可以展示出一个进度条。

npm install --save nprogress

main.js文件中导入js文件与css文件

// 导入 NProgress 包对应的JS和CSS
import NProgress from "nprogress";
import "nprogress/nprogress.css";

在发送请求的时候开启进度条,在获取响应后关闭进度条

// 在 request 拦截器中,展示进度条 NProgress.start()
axios.interceptors.request.use((config) => {
// console.log(config)
NProgress.start();
config.headers.Authorization = window.sessionStorage.getItem("token");
//最后必须返回config
return config;
});
// 在 response 拦截器中,隐藏进度条 NProgress.done()
axios.interceptors.response.use((config) => {
NProgress.done();
return config;
});

2、移除console.log指令

在项目开发的时候,我们为了测试,写了很多的console.log输出打印,但是当项目发布的时候,就应该将这些内容删除掉。

通过 babel-plugin-transform-remove-console 这个包来完成。

安装

npm install babel-plugin-transform-remove-console --save-dev

在项目的根目录下面找到babel.config.js文件,做如下的配置

// 这是项目发布阶段需要用到的 babel 插件
const prodPlugins = [];
if (process.env.NODE_ENV === "production") {
prodPlugins.push("transform-remove-console");
}

module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
plugins: [
[
"component",
{
libraryName: "element-ui",
styleLibraryName: "theme-chalk",
},
],
// 发布产品时候的插件数组
...prodPlugins,
],
};

在上面的代码中,首先判断是否在生产阶段,如果是采用babel-plugin-transform-remove-console插件。

将其添加到prodPlugins数组中,然后在plugins中进行配置。

3、生成打包报告

打包时,为了直观地发现项目中存在的问题,可以在打包时生成报告。生成报告的方式有两种:

第一种:通过命令行参数的形式生成报告

// 通过 vue-cli 的命令选项可以生成打包报告 
// --report 选项可以生成 report.html 以帮助分析包内容
vue-cli-service build --report

第二种:通过可视化的UI面板直接查看报告(推荐)

在可视化的UI面板中,通过控制台和分析面板,可以方便地看到项目中所存在的问题。

例如: 依赖的文件(js,css)比较大,页面打开速度非常慢。

4、通过 vue.config.js 修改 webpack 的默认配置

通过脚手架工具生成的项目,默认隐藏了所有 webpack 的配置项,目的是为了屏蔽项目的配置过程,让程 序员把工作的重心,放到具体功能和业务逻辑的实现上。

如果程序员有修改 webpack 默认配置的需求,可以在项目根目录中,按需创建 vue.config.js 这个配置文件,从 而对项目的打包发布过程做自定义的配置(具体配置参考 https://cli.vuejs.org/zh/config/#vue-config-js)。

// vue.config.js 
// 这个文件中,应该导出一个包含了自定义配置选项的对象
module.exports = {
// 选项...
}

5、为开发模式与发布模式指定不同的打包入口

默认情况下,Vue项目的开发模式与发布模式,共用同一个打包的入口文件(即 src/main.js)。

为了将项目 的开发过程与发布过程分离,我们可以为两种模式,各自指定打包的入口文件,即:

① 开发模式的入口文件为 src/main-dev.js

② 发布模式的入口文件为src/main-prod.js

configureWebpackchainWebpack

vue.config.js 导出的配置对象中,新增 configureWebpackchainWebpack 节点,来自定义webpack 的打包配置。

在这里, configureWebpackchainWebpack 的作用相同,唯一的区别就是它们修改 webpack 配置的方 式不同:

第一:chainWebpack 通过链式编程的形式,来修改默认的 webpack 配置

第二:configureWebpack 通过操作对象的形式,来修改默认的webpack配置

两者具体的使用差异,可参考如下网址:https://cli.vuejs.org/zh/guide/webpack.html#webpack-%E7%9B%B8%E5%85%B3

6、 通过 chainWebpack 自定义打包入口

示例代码如下所示:

module.exports = { 
chainWebpack: config => {
config.when(process.env.NODE_ENV === 'production', config => {
//移除原有默认的入口,添加新的入口文件
config.entry('app').clear().add('./src/main-prod.js')
})
config.when(process.env.NODE_ENV === 'development', config => {
config.entry('app').clear().add('./src/main-dev.js')
})
}
}

具体实现的方式如下:

在项目的根目录下面创建vue.config.js文件,并且添加如上代码。

同时将原有的src/main.js文件,重新命名为main-dev.js, 同时在新建一个main-prod.js文件,这两个文件内容是一样的。

7、通过 externals 加载外部 CDN 资源

默认情况下,通过 import 语法导入的第三方依赖包,最终会被打包合并到同一个文件中,从而导致打包成功 后,单文件体积过大的问题。

为了解决上述问题,可以通过 webpack externals 节点,来配置并加载外部的CDN资源。凡是声明在 externals 中的第三方依赖包,都不会被打包。

具体配置代码如下:

config.set('externals', { 
vue: 'Vue',
'vue-router': 'VueRouter',
axios: 'axios',
lodash: '_',
echarts: 'echarts',
nprogress: 'NProgress',
'vue-quill-editor': 'VueQuillEditor'
})

以上内容在vue.config.js文件中的发布模式下进行配置,那么在打包的时候,这些内容就不会被打包。

如下代码所示:

module.exports = {
chainWebpack: (config) => {
config.when(process.env.NODE_ENV === "production", (config) => {
//移除原有默认的入口,添加新的入口文件
config
.entry("app")
.clear()
.add("./src/main-prod.js");

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");
});
},
};

那么现在就有一个问题了,如果在发布模式下没有对这些内容进行打包,那么这些包就无法使用了,那么应该怎样进行解决呢?

解决的方案就是在 public/index.html 文件的头部,添加如下的CDN资源引用:

<!-- nprogress 的样式表文件 --> <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" />



<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>
<!-- 富 文本编辑器的 js 文件 --> <script src="https://cdn.staticfile.org/quill/1.3.4/quill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-quill-editor@3.0.4/dist/vue-quill-editor.js"></script>

这样既可以使用vue,axios等,同时最终打包成功后的文件也变小了。

注意:需要将/src/main-prod.js 中的关于进度条样式与富文本编辑器的样式去掉(这些样式最终也会打包到一个文件中)。

8、通过 CDN 优化 ElementUI 的打包

虽然在开发阶段,我们启用了element-ui 组件的按需加载,尽可能的减少了打包的体积,但是那些被按需加 载的组件,还是占用了较大的文件体积。此时,我们可以将element-ui 中的组件,也通过CDN的形式来加 载,这样能够进一步减小打包后的文件体积。

具体操作流程如下:

① 在 main-prod.js 中,注释掉 element-ui 按需加载的代码 (import "./plugins/element.js";)

② 在index.html的头部区域中,通过CDN加载 element-uijs css样式

<!-- element-ui 的样式表文件 --> 
<link rel="stylesheet" href="https://cdn.staticfile.org/element-ui/2.8.2/theme
chalk/index.css" />
<!-- element-ui 的 js 文件 -->
<script src="https://cdn.staticfile.org/element-ui/2.8.2/index.js"></script>

9、首页内容定制

不同的打包环境下,首页内容可能会有所不同。我们可以通过插件的方式进行定制,插件配置如下:

chainWebpack: config => {   
config.when(process.env.NODE_ENV === 'production', config => {
config.plugin('html').tap(args => {
args[0].isProd = true
return args
})
})

config.when(process.env.NODE_ENV === 'development', config => {
config.plugin('html').tap(args => {
args[0].isProd = false
return args
})
})
}

动态添加了一个参数:isProd,如果在发布模式中为true,否则为false.

public/index.html 首页中,可以根据isProd的值,来决定如何渲染页面结构

<!– 按需渲染页面的标题 --> 
<title><%= htmlWebpackPlugin.options.isProd ? '' : 'dev - ' %>电商后台管理系统</title>

<!– 按需加载外部的 CDN 资源 -->
<% if(htmlWebpackPlugin.options.isProd) { %> <!—通过 externals 加载的外部 CDN 资源--> <% } %>

下面来具体实现一下

vue.config.js文件中,添加html插件的配置。

module.exports = {
chainWebpack: (config) => {
config.when(process.env.NODE_ENV === "production", (config) => {
//移除原有默认的入口,添加新的入口文件
config
.entry("app")
.clear()
.add("./src/main-prod.js");

config.set("externals", {
vue: "Vue",
"vue-router": "VueRouter",
axios: "axios",
lodash: "_",
nprogress: "NProgress",
"vue-quill-editor": "VueQuillEditor",
});

config.plugin("html").tap((args) => {
args[0].isProd = true;
return args;
});
});
config.when(process.env.NODE_ENV === "development", (config) => {
config
.entry("app")
.clear()
.add("./src/main-dev.js");

//通过html插件动态添加isProd属性,来判断是开发模式还是发布模式
config.plugin("html").tap((args) => {
args[0].isProd = false;
return args;
});
});
},
};

配置好以后,就可以修改index.html中的内容了。

<title>
<%= htmlWebpackPlugin.options.isProd?'':'dev-' %>电商后台管理系统
</title>

<!-- 如果条件成立表示为发布模式,这时使用cdn模式引入外部文件 -->
<% if(htmlWebpackPlugin.options.isProd){ %>
<!-- nprogress 的样式表文件 -->
<link
rel="stylesheet"
href="https://cdn.staticfile.org/nprogress/0.2.0/nprogress.min.css"
/>
.........

<%}%>

10、路由懒加载

当打包构建项目时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成 不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。(默认情况下是加载所有的组件,但是实现了路由懒加载后,可以实现当路由被访问的时候才加载对应组件)

具体需要 3 步:

① 安装 @babel/plugin-syntax-dynamic-import 包。

② 在 babel.config.js 配置文件中声明该插件。

③ 将路由改为按需加载的形式,示例代码如下

const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue') 
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-boo" */ './Baz.vue')

以上内容语法需要安装了@babel/plugin-syntax-dynamic-import才能使用,import方法有两部分组成,后面的是组件的路径,前面部分表示的分组。

我们可以看到Foo.vueBar.vue这两个组件是同一组,那么在打包的时候,这两个组件会打包到同一个js文件中。如果请求Foo这个组件的时候,也会加载Bar这个组件。当然请求Bar这个组件的时候,也会加载Foo这个组件。

Baz组件会被单独的打包到一个js文件中。

关于路由懒加载的详细文档,可参考如下链接:
https://router.vuejs.org/zh/guide/advanced/lazy-loading.html

下面 安装 @babel/plugin-syntax-dynamic-import

npm install --save-dev @babel/plugin-syntax-dynamic-import

babel.config.js文件中配置上面包

// 这是项目发布阶段需要用到的 babel 插件
const prodPlugins = [];
if (process.env.NODE_ENV === "production") {
prodPlugins.push("transform-remove-console");
}

module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
plugins: [
[
"component",
{
libraryName: "element-ui",
styleLibraryName: "theme-chalk",
},
],
// 发布产品时候的插件数组
...prodPlugins,
// 实现路由懒加载
"@babel/plugin-syntax-dynamic-import",
],
};

下面改造src/router.js文件中的内容

import Vue from "vue";
import Router from "vue-router";
// import Login from "./components/Login.vue";
// import Home from "./components/Home.vue";
// import Welcome from "./components/Welcome.vue";
// import Users from "./components/user/Users.vue";
// import Rights from "./components/power/Rights.vue";
// import Roles from "./components/power/Roles.vue";
// import Cate from "./components/goods/Cate.vue";
// import Params from "./components/goods/Params.vue";
// import GoodsList from "./components/goods/List.vue";
// import Add from "./components/goods/Add.vue";
// import Order from "./components/order/Order.vue";
//下面实现路由懒加载
const Login = () =>
import(/* webpackChunkName: "login_home_welcome" */ "./components/Login.vue");
const Home = () =>
import(/* webpackChunkName: "login_home_welcome" */ "./components/Home.vue");
const Welcome = () =>
import(
/* webpackChunkName: "login_home_welcome" */ "./components/Welcome.vue"
);
const Users = () =>
import(
/* webpackChunkName: "Users_Rights_Roles" */ "./components/user/Users.vue"
);
const Rights = () =>
import(
/* webpackChunkName: "Users_Rights_Roles" */ "./components/power/Rights.vue"
);
const Roles = () =>
import(
/* webpackChunkName: "Users_Rights_Roles" */ "./components/power/Roles.vue"
);

const Cate = () =>
import(/* webpackChunkName: "Cate_Params" */ "./components/goods/Cate.vue");

const Params = () =>
import(/* webpackChunkName: "Cate_Params" */ "./components/goods/Params.vue");
const GoodsList = () =>
import(/* webpackChunkName: "GoodsList_Add" */ "./components/goods/List.vue");
const Add = () =>
import(/* webpackChunkName: "GoodsList_Add" */ "./components/goods/Add.vue");
const Order = () =>
import(/* webpackChunkName: "Order" */ "./components/order/Order.vue");
Vue.use(Router);
const router = new Router({
routes: [
{ path: "/", redirect: "/login" },
{ path: "/login", component: Login },
{
path: "/home",
component: Home,
redirect: "/welcome",
children: [
{ path: "/welcome", component: Welcome },
{ path: "/users", component: Users },
{ path: "/rights", component: Rights },
{ path: "/roles", component: Roles },
{ path: "/categories", component: Cate },
{ path: "/params", component: Params },
{ path: "/goods", component: GoodsList },
{ path: "/goods/add", component: Add },
{ path: "/orders", component: Order },
],
},
],
});

实现了路由懒加载后,打包后的文件体积也会变小。

以上的优化配置已经完成了,下面可以上线,当然在上线之前,需要将项目进行打包

npm run build

命令完成项目的打包,打包后的项目会在dist目录下。下面需要进行部署上线。

十五、项目部署上线

关于项目部署上线,主要完成以下的配置

第一: 通过 node 创建 web 服务器。

第二: 开启 gzip 配置。

第三:配置 https 服务

第四:使用 pm2 管理应用

1、通过 node 创建 web 服务器

创建 node 项目,并安装 express,通过express 快速创建 web 服务器,将 vue 打包生成的 dist 文件夹,

托管为静态资源即可,关键代码如下:

const express = require('express') 
// 创建 web 服务器
const app = express()

// 托管静态资源
app.use(express.static('./dist'))

// 启动 web 服务器
app.listen(80, () => {
console.log('web server running at http://127.0.0.1')
})

具体实现过程如下:

首先新建一个文件夹,例如:vue_shop_server

然后在这个文件夹下面创建一个node项目来托管发布后的Vue项目。

vue_shop_server目录下,进行项目的初始化

npm init -y

然后安装express,通过express快速启动服务。

npm i express -s

把打包好的Vue项目中的文件夹dist拷贝到vue_shop_server目录下。

同时在该目录下创建app.js文件,在该文件中添加如下代码:

const express = require('express') 
// 创建 web 服务器
const app = express()

// 托管静态资源
app.use(express.static('./dist'))

// 启动 web 服务器
app.listen(80, () => {
console.log('web server running at http://127.0.0.1')
})

通过node app.js命令启动以上服务,这时就可以访问我们打包好的vue项目。

2、开启 gzip 配置

使用 gzip 可以减小文件体积,使传输速度更快。

可以通过服务器端使用Expressgzip 压缩。其配置如下:

// 安装相应包   
npm install compression -S
// 导入包
const compression = require('compression');
// 启用中间件
app.use(compression());

具体实现如下所示,修改app.js文件中的代码

const express = require("express");
// 创建 web 服务器
const app = express();
const compression = require("compression");
// 一定要把这一行代码,写到 静态资源托管之前(注意顺序)
app.use(compression());
// 托管静态资源
app.use(express.static("./dist"));

// 启动 web 服务器
app.listen(8080, () => {
console.log("web server running at http://127.0.0.1");
});

重新启动服务。

3、配置 HTTPS 服务

为什么要启用 HTTPS 服务?

传统的 HTTP 协议传输的数据都是明文,不安全

采用 HTTPS 协议对传输的数据进行了加密处理,可以防止数据被中间人窃取,使用更安全

申请 SSL 证书(https://freessl.org)

① 进入 https://freessl.cn/ 官网,输入要申请的域名并选择品牌。

② 输入自己的邮箱并选择相关选项。

③ 验证 DNS(在域名管理后台添加 TXT 记录)。

④ 验证通过之后,下载 SSL 证书( full_chain.pem 公钥;private.key 私钥)。

在后台项目中导入证书

const https = require('https'); 
const fs = require('fs');
const options = {
cert: fs.readFileSync('./full_chain.pem'),
key: fs.readFileSync('./private.key')
}
https.createServer(options, app).listen(443);

4、使用 pm2 管理应用

现在我们要启动node服务,需要开启一个指令窗口,如果将该窗口关闭,那么整个服务也就停止了。

同时,窗口的管理也很麻烦。

① 在服务器中安装 pm2:npm i pm2 -g

② 启动项目:pm2 start 脚本 --name 自定义名称 //脚本表示node项目起始文件(app.js)

③ 查看运行项目:pm2 ls

④ 重启项目:pm2 restart 自定义名称

⑤ 停止项目:pm2 stop 自定义名称

⑥ 删除项目:pm2 delete 自定义名称