一、项目简介
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属性,动态绑定表单元素所需要的数据。
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属性,该属性动态绑定的是表单校验的规则对象。
下面定义校验规则对象。
     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属性。
在resetLoginForm这个方法中,我们通过当前组件的实例,获取$refs属性,然后通过该属性获取表单的实例,从而完成对resetFields方法的调用。
methods: {      resetLoginForm() {          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方法,来完成表单校验,注意的一点就是该方法的参数是一个回调函数,回调函数的参数表示校验的结果,如果校验成功参数valid为true,否则为false.
7、配置axios发起登录请求
安装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) => {              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对象,这里使用async与await简化其处理的过程。
最后判断其状态码,从而决定用户是否登录成功。
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) => {              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) => {            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("登录成功");            window.sessionStorage.setItem("token", res.data.token);            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访问特定页面,需要重新跳转到登录页面,进行登录。登录以后才能访问。
路由导航守卫基本用法。
 router.beforeEach((to,from,next)=>{          if(to.path==='/login') return next()     const tokenStr=window.sessionStorage.getItem('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) => {            
    if (to.path === "/login") return next();      const tokenStr = window.sessionStorage.getItem("token");   if (!tokenStr) return next("/login");   next(); }); export default router;
 
   | 
 
11、实现退出功能
基于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.interceptors.request.use(config => {              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.interceptors.request.use((config) => {      config.headers.Authorization = window.sessionStorage.getItem("token");      return config; }); Vue.prototype.$http = axios;
   | 
 
5、获取左侧菜单数据
在Home.vue中发送请求,获取菜单数据。实现代码如下:
<script> export default {   data() {     return {       menulist: [],     };   },   created() {            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";
  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-row与el-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>              <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-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: 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组件的注册
插槽的问题。
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组件。
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>
 
  | 
 
对应处理函数
   handleSizeChange(newSize) {          this.queryInfo.pagesize = newSize;     this.getUserList();   },      handleCurrentChange(newPage) {     console.log(newPage);     this.queryInfo.pagenum = newPage;     this.getUserList();   },
 
  | 
 
注册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_state与switch进行了双向数据绑定,如果mg_state属性的值为false,表示switch关闭状态,否则就是打开状态。
反之,如果我们手动的修改了switch组件的状态,那么mg_state 的值也会发生变化。
假如我们现在将一个switch组件的状态有关闭状态修改成打开的状态,那么mg_state的值也会有false变成true,那么接下来要做的就是发送一个异步的请求,将这个数据发送到服务端,从而完成当前用户状态的更新。
  async userStateChanged(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属性来保存要编辑的用户数据。
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方法的时候,将用户编号作为参数。
    async removeUserById(id) {                  const confirmResult = await this.$confirm(        "此操作将永久删除该用户, 是否继续?",        "提示",        {          confirmButtonText: "确定",          cancelButtonText: "取消",          type: "warning",        }      ).catch((err) => err); 
                         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的方法如下:
        async removeUserById(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("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">                                <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处理函数,将角色的信息以及权限编号传递到该方法中。
  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("删除权限失败!");    }
         role.children = res.data;   },
 
  | 
 
八、分配权限、角色
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("获取权限数据失败!");   }
       this.rightslist = res.data;   console.log(this.rightslist);   console.log(role);      this.setRightDialogVisible = true; },
 
  | 
 
把获取到的权限数据赋值给了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组件
接下来,给树形控件添加了如下属性
<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",       },              defKeys: [],     };
  | 
 
怎样将某个角色已经有的权限选中呢(这里是将三级权限选中),也就是说怎样将某个角色已有的三级权限编号存储到defKeys数组中?
   getLeafKeys(node, arr) {          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("获取权限数据失败!");       }
               this.rightslist = res.data;                            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("获取权限数据失败!");      }
             this.rightslist = res.data;                        this.getLeafKeys(role, this.defKeys);
             this.setRightDialogVisible = true;    },
 
  | 
 
在data中定义roleId 属性
data() {     return {              rolelist: [],              setRightDialogVisible: false,              rightslist: [],              treeProps: {         label: "authName",         children: "children",       },              defKeys: [],              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属性。
同时将Select与Option组件进行注册
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";
  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) => {      config.headers.Authorization = window.sessionStorage.getItem("token");      return config; }); Vue.prototype.$http = axios;
  Vue.config.productionTip = false;
  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,              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的定义如下
      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>
   | 
 
对应的数据如下:
    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>
   | 
 
对应方法的实现
     handleSizeChange(newSize) {       this.querInfo.pagesize = newSize;       this.getCateList();     },          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,            addCateForm: {                cat_name: "",                cat_pid: 0,                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);      this.parentCateList = res.data; },
 
  | 
 
getParentCateList方法的调用是在窗口打开的时候被调用的。
    showAddCateDialog() {            this.getParentCateList();            this.addCateDialogVisible = true;    },
 
  | 
 
同时在data中定义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: "cat_id",              label: "cat_name",              children: "children",     },
 
  | 
 
selectedKeys定义
parentCateChanged方法的实现
    parentCateChanged() {      console.log(this.selectedKeys);    },
 
  | 
 
最后需要注册:Vue.use(Cascader);
10、根据父分类的变化处理表单中的数据
我们知道,当我们单击“添加分类”窗口中的“确定”按钮的时候,需要将类别表单中的数据发送到服务端。
而表单是与addCateForm对象绑定在一起的。该对象在data中的定义如下:
      addCateForm: {                cat_name: "",                cat_pid: 0,                cat_level: 0,      },
 
  | 
 
首先cat_name属性的值已经确定了,就是用户在“分类名称”文本框中输入的内容。
现在cat_pid与cat_level属性的值没有确定。
但是要想确定这两个属性的值,就需要考虑到el-cascader组件,也就是用户在该组件中选择的父类别。
我们知道当选择el-cascader中的不同类别时会触发change事件,在对应的事件处理函数parentCateChanged中,确定cat_pid与cat_level这两个属性的值。
   parentCateChanged() {     console.log(this.selectedKeys);               if (this.selectedKeys.length > 0) {              this.addCateForm.cat_pid = this.selectedKeys[         this.selectedKeys.length - 1       ];              this.addCateForm.cat_level = this.selectedKeys.length;       return;     } else {              this.addCateForm.cat_pid = 0;              this.addCateForm.cat_level = 0;     }   },
 
  | 
 
首先判断selectedKeys数组中是否有值,我们知道当用户选择下拉框中的类别数据时,对应的所选择的类别的编号会存储到selectedKeys数组中,如果该数组没有值,表明用户没有选择任何类别。那也就是说现在用户所添加的类别是根类别,所以cat_pid与cat_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() {        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文件中注册一下
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
单击页签会执行handleTabClick处理函数。
   handleTabClick() {     console.log(this.activeName);        },
 
  | 
 
同时要完成页签组件的注册。
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: {      isBtnDisabled() {     if (this.selectedCateKeys.length !== 3) {       return true;     }     return false;   }, },
  | 
 
如果数组的长度不为3,返回true.表明要禁用按钮。
5、切换Tab页签获取数据
下面要实现的就是,当单击动态参数与静态属性这两个页签的时候,要查询到对应的数据,然后绑定到相应的表格中。
要想获得对应的数据,这里需要确定两个参数,一个是用户所选择的下拉框中第三类别的编号,再有一个就是所选择的Tab页签。
怎样获取用户所选择的“商品分类”下拉中用户选择的第三个类别的编号呢?
这里,也是通过计算属性来完成的。
computed: {      isBtnDisabled() {     if (this.selectedCateKeys.length !== 3) {       return true;     }     return false;   },      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() {            this.getParamsData();    },        handleTabClick() {      console.log(this.activeName);      this.getParamsData();    },
         async getParamsData() {                  if (this.selectedCateKeys.length !== 3) {        this.selectedCateKeys = [];        return;      }            console.log(this.selectedCateKeys);            const { data: res } = await this.$http.get(        `categories/${this.cateId}/attributes`,        {          params: { sel: this.activeName },        }      );
       if (res.meta.status !== 200) {        return this.$message.error("获取参数列表失败!");      }
       console.log(res.data);      if (this.activeName === "many") {        this.manyTableData = res.data;      } else {        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: {          isBtnDisabled() {       if (this.selectedCateKeys.length !== 3) {         return true;       }       return false;     },          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、删除参数
当单击表格中的“删除”按钮的时候,完成参数数据的删除。
 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);              const { data: res } = await this.$http.get(         `categories/${this.cateId}/attributes`,         {           params: { sel: this.activeName },         }       );
        if (res.meta.status !== 200) {         return this.$message.error("获取参数列表失败!");       }              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.inputVisible为true,展示输入框,否则展示添加按钮。
下面看一下scope.row.inputVisible是哪来的呢?
       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方法的实现如下:
    async handleInputConfirm(row) {      console.log(row);    },
 
  | 
 
当点击按钮的时候,会执行showInput方法,该方法的实现如下:
 showInput(row) {      row.inputVisible = true; },
 
  | 
 
这样将inputVisible设置为true后,会展示出对应的文本框。
现在能够完成添加按钮与文本框的切换了,接下来要实现的就是让文本框能够自动获取到焦点。
   showInput(row) {          row.inputVisible = true;          this.$nextTick(() => {       this.$refs.saveTagInput.$refs.input.focus();     });   },
 
  | 
 
在showInput方法中,添加了$nextTick函数。
当  row.inputVisible = true;设置为true后,并不能保证立即获取到文本框,而当$nextTick方法执行了,说明文本框渲染完了。
下面要实现的就是属性的添加了
当用户在文本框中输入了值以后,按下回车键或者让文本框失去焦点,就需要将用户在文本框中输入的内容发送到服务端,来完成对应的属性信息的添加操作。
下面需要修改handleInputConfirm方法,
  async handleInputConfirm(row) {        if (row.inputValue.trim().length === 0) {      row.inputValue = "";      row.inputVisible = false;      return;    }        row.attr_vals.push(row.inputValue.trim());    row.inputValue = "";    row.inputVisible = false;         this.saveAttrVals(row);  },    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,                  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) {                         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() {                  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() {               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);              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() {                  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);                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中属性的定义
      uploadURL: 'http://127.0.0.1:8888/api/private/v1/upload',            headerObj: {        Authorization: window.sessionStorage.getItem('token')      },
 
  | 
 
对应的方法处理
   handlePreview() {},      handleRemove() {},      handleSuccess(response) {     console.log(response);          const picInfo = { pic: response.data.tmp_path };          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) {                     const filePath = file.response.data.tmp_path;              const i = this.addForm.pics.findIndex((x) => x.pic === filePath);              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";
  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("请填写必要的表单项!");         }                           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的安装如下:
安装好以后可以进行导入
下面还需要注意的一点就是,根据文档的要求,动态参数与静态属性都要放到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文件
 import NProgress from "nprogress"; import "nprogress/nprogress.css";
 
  | 
 
在发送请求的时候开启进度条,在获取响应后关闭进度条
 axios.interceptors.request.use((config) => {      NProgress.start();   config.headers.Authorization = window.sessionStorage.getItem("token");      return config; });
  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文件,做如下的配置
 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
 configureWebpack 和 chainWebpack
在 vue.config.js 导出的配置对象中,新增 configureWebpack或 chainWebpack 节点,来自定义webpack 的打包配置。 
在这里, configureWebpack 和 chainWebpack 的作用相同,唯一的区别就是它们修改 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-ui 的 js 和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");
               config.plugin("html").tap((args) => {         args[0].isProd = false;         return args;       });     });   }, };
 
  | 
 
配置好以后,就可以修改index.html中的内容了。
<title>      <%= htmlWebpackPlugin.options.isProd?'':'dev-' %>电商后台管理系统    </title>              <% if(htmlWebpackPlugin.options.isProd){ %>        <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( './Foo.vue')  const Bar = () => import( './Bar.vue')  const Baz = () => import( './Baz.vue') 
   | 
 
以上内容语法需要安装了@babel/plugin-syntax-dynamic-import才能使用,import方法有两部分组成,后面的是组件的路径,前面部分表示的分组。
我们可以看到Foo.vue 与Bar.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文件中配置上面包
 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";
 
 
 
 
 
 
 
 
 
 
 
  const Login = () =>   import( "./components/Login.vue"); const Home = () =>   import( "./components/Home.vue"); const Welcome = () =>   import(      "./components/Welcome.vue"   ); const Users = () =>   import(      "./components/user/Users.vue"   ); const Rights = () =>   import(      "./components/power/Rights.vue"   ); const Roles = () =>   import(      "./components/power/Roles.vue"   );
  const Cate = () =>   import( "./components/goods/Cate.vue");
  const Params = () =>   import( "./components/goods/Params.vue"); const GoodsList = () =>   import( "./components/goods/List.vue"); const Add = () =>   import( "./components/goods/Add.vue"); const Order = () =>   import( "./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') 
  const app = express()   
  app.use(express.static('./dist'))   
  app.listen(80, () => {    console.log('web server running at http://127.0.0.1')  }) 
  | 
 
具体实现过程如下:
首先新建一个文件夹,例如:vue_shop_server
然后在这个文件夹下面创建一个node项目来托管发布后的Vue项目。
在vue_shop_server目录下,进行项目的初始化
然后安装express,通过express快速启动服务。
把打包好的Vue项目中的文件夹dist拷贝到vue_shop_server目录下。
同时在该目录下创建app.js文件,在该文件中添加如下代码:
const express = require('express') 
  const app = express()   
  app.use(express.static('./dist'))   
  app.listen(80, () => {    console.log('web server running at http://127.0.0.1')  }) 
  | 
 
通过node app.js命令启动以上服务,这时就可以访问我们打包好的vue项目。
2、开启 gzip 配置
使用 gzip 可以减小文件体积,使传输速度更快。 
 可以通过服务器端使用Express做 gzip 压缩。其配置如下: 
 npm install compression -S  
  const compression = require('compression');  
  app.use(compression()); 
 
  | 
 
具体实现如下所示,修改app.js文件中的代码
const express = require("express");
  const app = express(); const compression = require("compression");
  app.use(compression());
  app.use(express.static("./dist"));
 
  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 自定义名称