DayNode(VueShopEleM)

862 阅读9分钟

前期配置-后端

vue脚手架

基于ui界面创建Vue项目

终端输入:

vue ui

Vue脚手架的自定义配置

A.通过 package.json 进行配置 [不推荐使用]
    "vue":{
        "devServer":{
            "port":"9990",
            "open":true
        }
    }
B.通过单独的配置文件进行配置,创建vue.config.js
    module.exports = {
        devServer:{
            port:8888,
            open:true
        }
    }

Element-UI

官网

A.安装: --终端
npm install element-ui -S 

B.导入使用: --main.js
import ElementUI from "element-ui"; 
import "element-ui/lib/theme-chalk/index.css";

// 全局注册
Vue.use(ElementUI)

后台配置

截屏2021-08-11 下午4.44.52.png

配置mysql文件

截屏2021-08-11 下午4.21.39.png 代码运行完后,点左侧右上角 刷新 按钮

启动后台 app.js

截屏2021-08-11 下午4.37.01.png

截屏2021-08-11 下午4.40.57.png

postman测试数据

截屏2021-08-11 下午4.45.44.png

项目配置-前端

登录token

截屏2021-08-11 下午6.38.15.png

登录状态

如果服务器和客户端同源,建议可以使用cookie或者session来保持登录状态

如果客户端和服务器跨域了,建议使用token进行维持登录状态。

路由router

  1. 下载插件
yarn add vue-router
  1. 新建scr/router/index.js 文件,配置路由并导出
import Vue from 'vue'
import VueRouter 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/right/Rights.vue'
import Roles from '@/components/right/Roles.vue'

// 全局 注册路由组件
Vue.use(VueRouter)

//  创建路由规则
const 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
            }
        ]
    }
]
const router = new VueRouter({
    routes
})

//挂载路由导航守卫,to表示将要访问的路径,from表示从哪里来,next是下一个要做的操作
router.beforeEach((to, from, next) => {
    if (to.path == '/login') return next()

    // 获取token
    const tokenStr = sessionStorage.getItem('token')

    // 如果没有token
    if (!tokenStr) return next('/login')

    next()
})

// 创建 并导出 路由管理器对象
export default router
  1. main.js中引入并挂在路由
import Vue from 'vue'
import App from './App.vue'

// 导入路由管理器
import router from '@/router'

new Vue({
    router, // 注册路由
    render: h => h(App)
}).$mount('#app')

api接口配置

  1. 下载axios插件
yarn add axios
  1. 新建src/utils/request.js文件,进行请求基本配置
// 导入axios
import axios from 'axios'
// 设置请求的根路径
axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'

//请求在到达服务器之前,先会调用use中的这个回调函数来添加请求头信息
axios.interceptors.request.use(config => {
    //为请求头对象,添加token验证的Authorization字段
    config.headers.Authorization = window.sessionStorage.getItem('token')
    return config
})

// 导出
export default axios
  1. 新建src/api/Login.js,配置登录相关的api请求,并导出
// 提供登录 注册 相关的api方法
import axios from '@/utils/request.js'

// 导出login请求
export const login = data =>
    axios({
        url: 'login',
        method: 'post',
        data
    })
  1. 新建src/api/index.js,汇总所有请求
import { login } from '@/api/Login.js'
import { getMenus } from '@/api/Home.js'

export const loginAPI = login
export const getMenusAPI = getMenus
  1. src/components/Login.vue组件中 按需导入组件并使用api
<script>
// 按需导入 api中的方法
import { loginAPI } from '@/api'
export default {}
</script>

Element UI

按需引入

  1. 下载插件
npm i element-ui -S

2.新建 src/plugs/element.js 文件

import Vue from 'vue'

// 按需导入  组件
import { Button, Form, FormItem, Input, Message } from 'element-ui'

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

// 进行全局挂载:
Vue.prototype.$message = Message
  1. main.js中引入 样式
import Vue from 'vue'
import App from './App.vue'

// 引入elementUI组件和样式文件
import 'element-ui/lib/theme-chalk/index.css'
import '@/plugs/element.js'

// 引入其他样式
// 引入全局样式
import '@/assets/css/global.css'
// 引入字体样式
import '@/assets/fonts/iconfont.css'
// 导入 第三方插件vue-table-with-tree-grid 展示插件
import TreeTable from 'vue-table-with-tree-grid'

Container 布局容器

截屏2021-08-20 上午11.14.40.png

  1. 后台api请求数据
{
    "data":
        {
            "id": 101,
            "authName": "商品管理",
            "path": null,
            "children": [
                {
                    "id": 104,
                    "authName": "商品列表",
                    "path": null,
                    "children": []
                }
            ]
        }
    "meta": {
        "msg": "获取菜单列表成功",
        "status": 200
    }
}
  1. Home.vue
<template>
  <div>
    <el-container class="home-container">
      <!-- 头部区域 -->
      <el-header>
        <!-- 左侧logo -->
        <div class="left">
          <div class="logo">J.</div>
          <span>Jeanhome</span>
        </div>
        <!-- 右侧 退出登录button -->
        <el-button type="info" @click="logout" plain round size="mini">退出</el-button>
      </el-header>
      <!-- 页面主体区域 -->
      <el-container>
        <!-- 侧边栏 ------------------------------------------------------- -->
        <el-aside :width="isCollapse?'64px':'200px'">
          <!-- 伸缩侧边栏按钮 -->
          <div class="toggle-button" @click="isCollapse=!isCollapse">
            <i :class="isCollapse?'el-icon-moon-night':'el-icon-cloudy-and-sunny'"></i>
          </div>
          <!-- 侧边栏菜单 -->
          <el-menu
            background-color="#fafafa"
            text-color="#B4B4B4"
            active-text-color="#f7d182"
            unique-opened
            :collapse="isCollapse"
            :collapse-transition="false"
            :router="true"
            :default-active="this.activePath"
          >
            <!-- 一级菜单 -->
            <!-- 注意:绑定的index必须是 字符串 -->
            <el-submenu
              :index="item.id.toString()"
              :key="item.id"
              v-for="item in menuList"
              class="first-item"
              active-background-color="#fff"
            >
              <!-- 一级菜单模板 -->
              <template slot="title">
                <!-- 图标 -->
                <i :class="item.iconclass"></i>
                <!-- 文本 -->
                <span>{{item.authName}}</span>
              </template>
              <!-- 二级子菜单 -->
              <el-menu-item
                :index="'/'+subItem.path"
                :key="subItem.id"
                v-for="subItem in item.children"
                @click="changePath('/'+subItem.path)"
              >
                <!-- 二级菜单模板 -->
                <template slot="title">
                  <!-- 图标 -->
                  <i class="el-icon-set-up"></i>
                  <!-- 文本 -->
                  <span>{{subItem.authName}}</span>
                </template>
              </el-menu-item>
            </el-submenu>
          </el-menu>
        </el-aside>
        <!-- 主体结构 -->
        <el-main>
          <!-- 路由占位符 -->
          <router-view></router-view>
        </el-main>
      </el-container>
    </el-container>
  </div>
</template>

<script>
import { getMenusAPI } from '@/api'
export default {
  data() {
    return {
      // 左侧菜单数据
      menuList: [],
      isCollapse: true, // 是否合并左侧菜单栏,false-展开,true-合并
      activePath: null // 当前激活菜单的path
    }
  },
  async created() {
    // 获取 菜单列表的数据
    const { data: res } = await getMenusAPI()
    if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

    this.menuList = res.data

    // 读取本地缓存的菜单path
    this.activePath = window.sessionStorage.getItem('activePath')
  },
  methods: {
    // 点击退出 button
    logout() {
      window.sessionStorage.clear()
      this.$router.push('/login')
    },
    // 点击菜单,切换并保存 当前点击菜单 的index(path)
    changePath(path) {
      window.sessionStorage.setItem('activePath', path)
      this.activePath = path // 让当前菜单栏高亮
    }
  }
}
</script>

<style lang='less' scoped>
</style>

Dialog+Form 表单

截屏2021-08-20 上午10.32.37.png

<template>
  <div>
    <!-- 新增对话框------------------------------------------------ -->
    <el-dialog title="添加新用户" :visible.sync="isShowAdd" width="50%" :before-close="handleClose" @close="addDialogClosed">
      <!-- 对话框主体区域 -->
      <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" required>
          <el-input v-model="addForm.email"></el-input>
        </el-form-item>
        <el-form-item label="电话" prop="mobile" required>
          <el-input v-model="addForm.mobile"></el-input>
        </el-form-item>
      </el-form>
      <!-- 对话框底部--------- -->
      <span slot="footer" class="dialog-footer">
        <el-button @click="isShowAdd= false">取 消</el-button>
        <el-button type="primary" @click="addUser">确 定</el-button>
      </span>
    </el-dialog>
  </div>
</template>

<script>
import { addUserAPI } from '@/api'
export default {
  data() {
    //验证邮箱的规则
    var checkEmail = (rule, value, cb) => {
      if (value.trim().length == 0) return cb(new Error('请输入邮箱'))
      const regEmail = /^\w+@\w+(\.\w+)+$/
      if (regEmail.test(value)) {
        return cb()
      }
      //返回一个错误提示
      cb(new Error('请输入合法的邮箱'))
    }
    //验证手机号码的规则
    var checkMobile = (rule, value, cb) => {
      if (!value) return cb(new Error('请输入手机号'))

      const regMobile = /^1[34578]\d{9}$/
      if (regMobile.test(value)) {
        return cb()
      }
      //返回一个错误提示
      cb(new Error('请输入合法的手机号码'))
    }
    return {
      isShowAdd: 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' },
          { validator: checkEmail, trigger: 'blur' }
        ],
        mobile: [
          //   { required: true, message: '请输入手机号码', trigger: 'blur' },
          { validator: checkMobile, trigger: 'blur' }
        ]
      }
    }
  },

  methods: {
    handleSizeChange(val) {
      //以最新的pagesize来请求数据并展示数据
      this.queryInfo.pagesize = val
      this.getUserList()
    },
    handleCurrentChange(val) {
      //以最新的val页码来请求数据并展示数据
      this.queryInfo.pagenum = val
      this.getUserList()
    },

    // 添加用户
    addUser() {
      // 点击 确定按钮时,调用validate进行表单验证
      this.$refs.addFormRef.validate(async valid => {
        if (!valid) return this.$message.error('请填写完整用户信息')

        const { data: res } = await addUserAPI(this.addForm)

        //判断如果添加失败,就做提示
        if (res.meta.status !== 201) return this.$message.error('添加用户失败')
        //添加成功的提示
        this.$message.success('添加用户成功')
        //关闭对话框
        this.isShowAdd = false
      })
    },
    //对话框关闭之后,重置表单
    addDialogClosed() {
      this.$refs.addFormRef.resetFields()
    }
  }
}
</script>

<style lang='less' scoped>
</style>

Table+Tree

table 截屏2021-08-20 上午10.30.40.png tree 树形控件 截屏2021-08-20 上午10.31.10.png

<template>
  <div>
    <!-- 卡片容器区域--------------------------------- -->
    <el-card class="box-card">
      <div class="text item">
        <!-- 角色列表区域 ----------------------------------------->
        <el-table :data="roleList" border>
          <!-- !!!添加展开列 ------------------->
          <el-table-column type="expand">
            <template slot-scope="scope">
              <!-- 渲染一级权限 -->
              <!-- 只有第一行 需要加上边框 -->
              <el-row :class="['bottomBorder',i1==0?'topBorder':'']" :key="item1.id" v-for="(item1,i1) in scope.row.children">
                <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?'topBorder':'']" :key="item2.id" v-for="(item2,i2) in item1.children">
                    <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
                        closable
                        @close="removeRightById(scope.row,item3.id)"
                        type="warning"
                        :key="item3.id"
                        v-for="item3 in item2.children"
                      >{{item3.authName}}</el-tag>
                    </el-col>
                  </el-row>
                </el-col>
              </el-row>
            </template>
          </el-table-column>
          <el-table-column type="index" label="#"></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="success" icon="el-icon-setting" @click="showSetRightDialog(scope.row)">分配权限</el-button>
            </template>
          </el-table-column>
        </el-table>
      </div>
    </el-card>
    <!-- 分配权限对话框------------------------------------------ -->
    <el-dialog title="分配权限" @close="setRightDialogClose" :visible.sync="isShowSetRight" width="50%">
      <!-- 树形组件
    show-checkbox:显示复选框
    node-key:设置选中节点对应的值
    default-expand-all:是否默认展开所有节点
    :default-checked-keys 设置默认选中项的数组
      ref:设置引用-->
      <el-tree
        :data="rightsList"
        :default-checked-keys="defKeys"
        show-checkbox
        default-expand-all
        node-key="id"
        ref="treeRef"
        highlight-current
        :props="treeProps"
      ></el-tree>
      <span slot="footer" class="dialog-footer">
        <el-button @click="isShowSetRight = false">取 消</el-button>
        <el-button type="primary" @click="changeRoleRights">确 定</el-button>
      </span>
    </el-dialog>
  </div>
</template>

<script>
import { getRolesAPI, delRoleRightAPI, getRightsTreeAPI, changeRoleRightAPI } from '@/api'
export default {
  data() {
    return {
      roleList: [], //角色列表数据
      isShowSetRight: false, // 是否显示 分配权限 的弹出框
      rightsList: [], //权限树 数据
      //树形控件的属性绑定对象
      treeProps: {
        //通过label设置树形节点文本展示authName
        label: 'authName',
        //设置通过children属性展示子节点信息
        children: 'children'
      },
      defKeys: [], //默认勾选的节点的 key 的数组,三级节点的id
      roleId: null // 当前正在编辑的项 的id
    }
  },
  created() {
    this.getRoleList()
  },
  methods: {
    async getRoleList() {
      const { data: res } = await getRolesAPI()
      //如果返回状态为异常状态则报错并返回
      if (res.meta.status !== 200) return this.$message.error('获取角色列表失败')
      // //如果返回状态正常,将请求的数据保存在data中
      this.roleList = res.data
    },
    // 删除权限
    async removeRightById(role, rightId) {
      //弹窗提示用户是否要删除
      const confirmResult = await this.$confirm('请问是否要删除该权限', '删除提示', {
        confirmButtonText: '确认删除',
        cancelButtonText: '取消',
        type: 'warning'
      }).catch(err => err)
      //如果用户点击确认,则confirmResult 为'confirm'
      //如果用户点击取消, 则confirmResult获取的就是catch的错误消息'cancel'
      if (confirmResult != 'confirm') return this.$message.info('已经取消删除')

      const { data: res } = await delRoleRightAPI(role.id, rightId)
      if (res.meta.status !== 200) return this.$message.error('删除角色权限失败')

      //无需再重新加载所有权限
      //只需要对现有的角色权限进行更新即可
      role.children = res.data
    },
    // 点击设置分配权限
    async showSetRightDialog(role) {
      // 弹出 对话框时,就保存当前项的id
      this.roleId = role.id
      //获取所有权限的数据
      const { data: res } = await getRightsTreeAPI()
      //如果返回状态为异常状态则报错并返回
      if (res.meta.status !== 200) return this.$message.error('获取权限树失败')
      //如果返回状态正常,将请求的数据保存在data中
      this.rightsList = res.data

      // role是当前用户的信息,children里面就是权限
      //调用getLeafKeys进行递归,将三级权限添加到数组中
      this.getLeafKeys(role, this.defKeys)
      // 显示对话框
      this.isShowSetRight = true
    },
    // 通过递归的形式,获取角色下所有三级权限的id,并保存到defKeys中
    getLeafKeys(node, arr) {
      // 如果当前节点不包含children属性,则表示node为三级权限
      if (!node.children) return arr.push(node.id)
      //递归调用 当前子节点每一个元素
      node.children.forEach(item => this.getLeafKeys(item, arr))
    },
    // 当用户关闭树形权限对话框的时候
    setRightDialogClose() {
      // 需要清除掉所有选中状态
      this.defKeys = []
    },
    //当用户在树形权限对话框中点击确定,将用户选择的 权限发送请求进行更新
    async changeRoleRights() {
      //获取所有选中及半选的 所有id
      const keys = [...this.$refs.treeRef.getCheckedKeys(), ...this.$refs.treeRef.getHalfCheckedKeys()]
      //将数组转换为 , 拼接的字符串
      const rids = keys.join(',')
      console.log(rids)
      //发送请求完成更新
      const { data: res } = await changeRoleRightAPI(this.roleId, rids)
      if (res.meta.status !== 200) return this.$message.error('分配权限失败')
      this.$message.success('分配权限成功')
      // 更新整个角色列表
      this.getRoleList()
      // 关闭对话框
      this.isShowSetRight = false
    }
  }
}
</script>

<style lang='less' scoped>
</style>

Table+Pagination 分页

截屏2021-08-20 上午10.48.16.png

<template>
  <div>
    <!-- 卡片容器 -->
    <el-card class="box-card">
      <div class="text item">
        <!-- 数据表格区域-------------------------------------------------- -->
        <!-- 用户列表(表格)区域 -->
        <el-table :data="userList" border stripe>
          <el-table-column label="#" type="index"></el-table-column>
          <el-table-column label="姓名">
            <template slot-scope="scope">{{scope.row.username}}</template>
          </el-table-column>
          <!-- <el-table-column label="姓名" prop="username"></el-table-column> -->
          <el-table-column label="邮箱" prop="email"></el-table-column>
          <el-table-column label="电话" prop="mobile"></el-table-column>
          <el-table-column label="角色" prop="role_name"></el-table-column>
          <el-table-column label="状态">
            <template slot-scope="scope">
              <!-- scope.row---每条数据,还有 scope.$index--index,scope.column参数 -->
              <el-switch v-model="scope.row.mg_state" @change="changeStatus(scope.row)" active-color="#f7d182"></el-switch>
            </template>
          </el-table-column>
          <el-table-column label="操作" width="180px">
            <template slot-scope="scope">
              <!-- 修改 -->
              <el-button @click="showEditDialog(scope.row.id)" type="primary" icon="el-icon-edit" size="mini"></el-button>
              <!-- 删除 -->
              <el-button @click="delUser(scope.row.id)" type="danger" icon="el-icon-delete" size="mini"></el-button>
              <!-- 分配角色 -->
              <el-tooltip class="item" effect="light" content="分配角色" placement="top" :enterable="false">
                <el-button type="success" icon="el-icon-setting" size="mini" @click="setRole(scope.row)"></el-button>
              </el-tooltip>
            </template>
          </el-table-column>
        </el-table>
        <!--  分页导航区域 -->
        <el-pagination
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
          :current-page="queryInfo.pagenum"
          :page-sizes="[1, 2, 5, 10]"
          :page-size="queryInfo.pagesize"
          layout="total, sizes, prev, pager, next, jumper"
          :total="total"
        ></el-pagination>
      </div>
    </el-card>
  </div>
</template>

<script>
import { getUsersAPI } from '@/api'
export default {
  data() {
    return {
      //获取查询用户信息的参数
      queryInfo: {
        query: '',
        pagenum: 1, // 当前页码
        pagesize: 1 // 每页显示行数
      },
      //保存请求回来的用户列表数据
      userList: [],
      total: 0
    }
  },
  created() {
    this.getUserList()
  },
  methods: {
    //发送请求获取用户列表数据
    async getUserList() {
      const { data: res } = await getUsersAPI(this.queryInfo)
      //如果返回状态为异常状态则报错并返回
      if (res.meta.status != 200) return this.$message.error(res.meta.msg)
      //如果返回状态正常,将请求的数据保存在data中
      this.userList = res.data.users

      this.total = res.data.total
    },
    // 切换 每页的数量
    handleSizeChange(val) {
      //以最新的pagesize来请求数据并展示数据
      this.queryInfo.pagesize = val
      this.getUserList()
    },
    // 切换 当前页码
    handleCurrentChange(val) {
      //以最新的val页码来请求数据并展示数据
      this.queryInfo.pagenum = val
      this.getUserList()
    }
  }
}
</script>

<style lang='less' scoped>
</style>

Cascader 级联选择器

截屏2021-08-20 上午11.11.07.png

<template>
  <div>
    <!-- 添加分类对话框 ----------------------------------------------->
    <el-dialog title="添加分类" :visible.sync="isShowAddCate" width="50%" @close="addCateDialogClosed">
      <!-- 添加分类表单 -->
      <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="父级分类" prop="cat_pid">
          <!-- expandTrigger='hover'(鼠标悬停触发级联) v-model(设置级联菜单绑定数据) 
          :options(指定级联菜单数据源)  :props(用来配置数据显示的规则) 
          clearable(提供“X”号完成删除文本功能) change-on-select(是否可以选中任意一级的菜单)-->
          <el-cascader
            :options="parentCateList"
            :props="cascaderProps"
            v-model="selKeys"
            @change="parentCateChange"
            clearable
            style="width:100%"
          ></el-cascader>
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button @click="isShowAddCate = false">取 消</el-button>
        <el-button type="primary" @click="addCate">确 定</el-button>
      </span>
    </el-dialog>
  </div>
</template>

<script>
import { getGoodCatesAPI, addGoodCateAPI } from '@/api'
export default {
  data() {
    return {
      isShowAddCate: false, // 是否显示添加分类对话框
      //添加分类的表单数据对象
      addCateForm: {
        //分类名称
        cat_name: '',
        //添加分类的父级id,0则表示父级为0.添加一级分类
        cat_pid: 0,
        //添加分类的等级,0则表示添加一级分类
        cat_level: 0
      },
      //添加分类校验规则
      addCateFormRules: {
        //验证规则
        cat_name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }]
      },
      parentCateList: [], // 有一二级分类的 分类列表数据
      selKeys: [], // 父级分类下拉框选中的 keys
      cascaderProps: {
        value: 'cat_id',
        label: 'cat_name',
        children: 'children',
        expandTrigger: 'hover', //鼠标悬停触发级联
        checkStrictly: true // 是否可以选中任意一级的菜单
      }
    }
  },

  methods: {
    // 点击添加分类按钮
    showAddCateDialog() {
      this.isShowAddCate = true
      this.getParentCateList()
    },
    //获取父级分类数据列表
    async getParentCateList() {
      const { data: res } = await getGoodCatesAPI({ type: 2 })
      if (res.meta.status !== 200) {
        return this.$message.error('获取商品分类列表数据失败')
      }
      this.parentCateList = res.data
    },
    //级联菜单中选择项发生变化时触发
    parentCateChange() {
      console.log(this.selKeys)
      // 如果两级都选了,则this.selKeys有两个id,最后一个id即为我们要添加分类 的父级id
      if (this.selKeys.length >= 0) {
        this.addCateForm.cat_pid = this.selKeys[this.selKeys.length - 1]
      } else {
        // 没有选择,则表示要添加0级分类
        this.addCateForm.cat_pid = 0
      }
      this.addCateForm.cat_level = this.selKeys.length
    },
    // 点击 添加分类对话框 的确定按钮
    addCate() {
      this.$refs.addCateFormRef.validate(async valid => {
        if (!valid) return

        // 发送添加分类的请求
        const { data: res } = await addGoodCateAPI(this.addCateForm)
        if (res.meta.status !== 201) return this.$message.error('添加分类失败')
        this.$message.success('添加分类成功')
        this.getCateList()
        // 关闭弹出对话框
        this.isShowAddCate = false
      })
    },
    // 关闭 添加分类对话框
    addCateDialogClosed() {
      //当关闭添加分类对话框时,重置表单
      this.$refs.addCateFormRef.resetFields()
      this.selKeys = []
      this.addCateForm.cat_level = 0
      this.addCateForm.cat_pid = 0
    }
  }
}
</script>

<style lang="less" scoped>
</style>

Upload 上传

完成图片上传

因为upload组件进行图片上传的时候并不是使用axios发送请求 所以,我们需要手动为上传图片的请求添加token,即为upload组件添加headers属性

//在页面中添加upload组件,并设置对应的事件和属性
<el-tab-pane label="商品图片" name="3">
  <!-- 商品图片上传
  action:指定图片上传api接口
  :on-preview : 当点击图片时会触发该事件进行预览操作,处理图片预览
  :on-remove : 当用户点击图片右上角的X号时触发执行
  :on-success:当用户点击上传图片并成功上传时触发
  list-type :设置预览图片的方式
  :headers :设置上传图片的请求头 -->
  <el-upload :action="uploadURL" :on-preview="handlePreview" :on-remove="handleRemove" :on-success="handleSuccess" list-type="picture" :headers="headerObj">
    <el-button size="small" type="primary">点击上传</el-button>
  </el-upload>
</el-tab-pane>
//在el-card卡片视图下面添加对话框用来预览图片
<!-- 预览图片对话框 -->
<el-dialog title="图片预览" :visible.sync="previewVisible" width="50%">
  <img :src="previewPath" class="previewImg" />
</el-dialog>

//在data中添加数据
data(){
  return {
    ......
    //添加商品的表单数据对象
    addForm: {
      goods_name: '',
      goods_price: 0,
      goods_weight: 0,
      goods_number: 0,
      goods_cat: [],
      //上传图片数组
      pics: []
    },
    //上传图片的url地址
    uploadURL: 'http://127.0.0.1:8888/api/private/v1/upload',
    //图片上传组件的headers请求头对象
    headerObj: { Authorization: window.sessionStorage.getItem('token') },
    //保存预览图片的url地址
    previewPath: '',
    //控制预览图片对话框的显示和隐藏
    previewVisible:false
  }
},
//在methods中添加事件处理函数
methods:{
  .......
  handlePreview(file) {
    //当用户点击图片进行预览时执行,处理图片预览
    //形参file就是用户预览的那个文件
    this.previewPath = file.response.data.url
    //显示预览图片对话框
    this.previewVisible = true
  },
  handleRemove(file) {
    //当用户点击X号删除时执行
    //形参file就是用户点击删除的文件
    //获取用户点击删除的那个图片的临时路径
    const filePath = file.response.data.tmp_path
    //使用findIndex来查找符合条件的索引
    const index = this.addForm.pics.findIndex(item => item.pic === filePath)
    //移除索引对应的图片
    this.addForm.pics.splice(index, 1)
  },
  handleSuccess(response) {
    //当上传成功时触发执行
    //形参response就是上传成功之后服务器返回的结果
    //将服务器返回的临时路径保存到addForm表单的pics数组中
    this.addForm.pics.push({ pic: response.data.tmp_path })
  }
}

其他插件

vue-table-with-tree-grid

官网

截屏2021-08-20 上午11.05.53.png

基本配置

  1. 下载插件
yarn add vue-table-with-tree-grid
  1. 全局注册 main.js
//全局注册组件
Vue.component('tree-table', TreeTable)
  1. vue组件中使用
<template>
  <div>
    <!-- 卡片视图区域 -->
    <el-card>
      <!-- 分类表格------------------------------------------------------------  -->
      <!-- :data(设置数据源) :columns(设置表格中列配置信息) :selection-type(是否有复选框) 
      :expand-type(是否展开数据) show-index(是否设置索引列) index-text(设置索引列头)
      border(是否添加纵向边框) :show-row-hover(是否鼠标悬停高亮)-->
      <tree-table
        :data="cateList"
        :columns="columns"
        :selection-type="false"
        :expand-type="false"
        show-index
        index-text="#"
        border
        :show-row-hover="false"
      >
        <!-- 是否有效区域, 设置对应的模板列: slot="isok"(与columns中设置的template一致) -->
        <template #isok="scope">
          <i class="el-icon-success" v-if="scope.row.cat_deleted === false" style="color:lightgreen"></i>
          <i class="el-icon-error" v-else style="color:red"></i>
        </template>
        <!-- 排序 -->
        <template slot="order" slot-scope="scope">
          <el-tag size="mini" v-if="scope.row.cat_level===0">一级</el-tag>
          <el-tag size="mini" type="success" v-else-if="scope.row.cat_level===1">二级</el-tag>
          <el-tag size="mini" type="warning" v-else>三级</el-tag>
        </template>
        <!-- 操作 -->
        <template slot="opt" slot-scope="scope">
          <el-button size="mini" type="primary" icon="el-icon-edit">编辑</el-button>
          <el-button size="mini" type="danger" icon="el-icon-delete">删除</el-button>
        </template>
      </tree-table>

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

<script>
import { getGoodCatesAPI } from '@/api'
export default {
  data() {
    return {
      // 商品分类数据列表
      cateList: [],
      //查询分类数据的条件
      queryInfo: {
        type: 3,
        pagenum: 1,
        pagesize: 5
      },
      //保存总数据条数
      total: 0,
      columns: [
        { label: '分类名称', prop: 'cat_name' },
        //type:'template'(将该列设置为模板列),template:'isok'(设置该列模板的名称为isok)
        { label: '是否有效', prop: '', type: 'template', template: 'isok' },
        { label: '排序', prop: '', type: 'template', template: 'order' },
        { label: '操作', prop: '', type: 'template', template: 'opt' }
      ]
    }
  },
  created() {
    this.getCateList()
  },
  methods: {
    async getCateList() {
      //获取商品分类数据
      let { data: res } = await getGoodCatesAPI(this.queryInfo)
      if (res.meta.status !== 200) return this.$message.error('获取商品列表数据失败')
      //将数据列表赋值给cateList
      this.cateList = res.data.result
      //保存总数据条数
      this.total = res.data.total
    },
    handleSizeChange(newSize) {
      //当pagesize发生改变时触发
      this.queryInfo.pagesize = newSize
      this.getCateList()
    },
    handleCurrentChange(newPage) {
      //当pagenum发生改变时触发
      this.queryInfo.pagenum = newPage
      this.getCateList()
    }
  }
}
</script>

<style lang="less" scoped>
</style>

vue-quill-editor富文本

官网 截屏2021-08-20 下午3.20.56.png

  1. 下载插件
yarn add vue-quill-editor
  1. main.js中引入
//导入vue-quill-editor(富文本编辑器)
import VueQuillEditor from 'vue-quill-editor'
//导入vue-quill-editor的样式
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
......
//全局注册组件
Vue.component('tree-table', TreeTable)
//全局注册富文本组件
Vue.use(VueQuillEditor)
  1. src/components/goods/Add.vue中使用
<!-- 富文本编辑器组件 -->
<el-tab-pane label="商品内容" name="4">
  <!-- 富文本编辑器组件 -->
  <quill-editor v-model="addForm.goods_introduce"></quill-editor>
  <!-- 添加商品按钮 -->
  <el-button type="primary" class="btnAdd">添加商品</el-button>
</el-tab-pane>

//在数据中添加goods_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;
}
//给添加商品按钮添加间距
.btnAdd{
  margin-top:15px;
}

lodash 深拷贝

  1. 下载依赖
npm i --save lodash
  1. 在.vue文件中 导入lodash并使用
<script>
//官方推荐将lodash导入为_
import _ from 'lodash'

...其他代码

const form = _.cloneDeep(this.addForm)
</script>

项目优化

截屏2021-08-21 上午9.10.00.png

添加进度条--nprogress

官网

  1. 下载插件
yarn add nprogress
  1. 在src/utils/request.js路由处理器中配置
  • 在请求在到达服务器之前--开启进度条
  • 在服务器的响应结束后--结束进度条 2741629449142_.pic_hd.jpg
// 导入axios
import axios from 'axios'
// 设置请求的根路径
axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'

//导入进度条插件
import NProgress from 'nprogress'
//导入进度条样式
import 'nprogress/nprogress.css'

//请求在到达服务器之前,先会调用use中的这个回调函数来添加请求头信息
axios.interceptors.request.use(config => {
    //当进入request拦截器,表示发送了请求,我们就开启进度条
    NProgress.start()

    //为请求头对象,添加token验证的Authorization字段
    config.headers.Authorization = window.sessionStorage.getItem('token')
    return config
})

//在response拦截器中,服务器响应结束后,隐藏进度条
axios.interceptors.response.use(config => {
    //当进入response拦截器,表示请求已经结束,我们就结束进度条
    NProgress.done()
    return config
})

// 导出
export default axios

解决自动换行的问题

根据ESLint的警告提示更改对应的代码

.prettierrc文件中更改设置"printWidth":200, 将每行代码的文字数量更改为200

{
    "semi":false,
    "singleQuote":true,
    "printWidth":200
}

在build前移除所有的console.log

截屏2021-08-21 上午9.18.49.png

  1. 下载插件(开发依赖)
yarn add babel-plugin-transform-remove-console
  1. babel.config.js中配置,需要只在项目发布阶段 再移除
// 开发阶段:程序员 测试 共同完成 项目的过程 , 不要 移除 console 的输出
// 生成阶段;普通用户可以通过浏览器来使用项目网站 , 要 移除 console的输出
const proPlugs = []
if (process.env.NODE_ENV === 'production') {
  proPlugs.push("transform-remove-console")
}
module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ],
  "plugins": [...proPlugs]
}

生成打包报告

  1. 命令行形式生成打包报告 vue-cli-service build --report

  2. 在vue控制台生成打包报告 点击“任务”=>“build”=>“运行” 运行完毕之后点击右侧“分析”,“控制台”面板查看报告

截屏2021-08-21 上午9.26.17.png

修改webpack的默认配置

默认情况下,vue-cli 3.0生成的项目,隐藏了webpack配置项,如果我们需要配置webpack 需要通过vue.config.js来配置。

配置不同的 入口文件

原因:针对 开发时,我们可能需要一些工具模块辅助

而部署到线上时,我们并不需要那些工具模块了

  • chainWebpack可以通过链式编程的形式,修改webpack配置

  • configureWebpack可以通过操作对象的形式,修改webpack配置

在项目根目录中创建vue.config.js文件

- 设置生产环境production的 入口文件为 main-prod.js
- 设置开发模式 development的 入口文件为 main-dev.js
// webpack不能识别高版本语法,所以需要用老的 Common JS语法导出
module.exports = {
    chainWebpack:config=>{
        //发布模式
        config.when(process.env.NODE_ENV === 'production',config=>{
            //entry找到默认的打包入口,调用clear则是删除默认的打包入口
            //add添加新的打包入口
            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')
        })
    }
}

大文件访问优化

通过external加载外部CDN资源

默认情况下,依赖项的所有第三方包都会被打包到js/chunk-vendors. ****** .js文件中,导致该js文件过大

那么我们可以通过externals排除这些包,使它们不被打包到js/chunk-vendors. ****** .js文件中

  • 好处:

    • 1.距离优势,在各大中心城市机房都有部署,浏览器访问时,会找最近的机房获取文件

    • 2.缓存优势

      • 如果 所有网站 使用的 vue.js 文件 ,都用的是华为的
      • 那么,当 用户 浏览器 访问 网站1时,已经 从 华为服务器 获取 vue.js,并缓存在浏览器
      • 该用户 浏览器 访问 网站2时,用的如果也是 华为的vue.js的话,就不需要去服务器拿,直接从缓存中获取即可
  1. 修改vue.config.js文件的配置,在生产环境下使用externals排除包
module.exports = {
    chainWebpack:config=>{
        //发布模式
        config.when(process.env.NODE_ENV === 'production',config=>{
            //entry找到默认的打包入口,调用clear则是删除默认的打包入口
            //add添加新的打包入口
            config.entry('app').clear().add('./src/main-prod.js')

            //!!使用externals设置排除项,编译时,遇到这些模块不要打包
            config.set('externals',{
                vue:'Vue',
                'vue-router':'VueRouter',
                axios:'axios',
                lodash:'_',
                echarts:'echarts',
                nprogress:'NProgress',
                'vue-quill-editor':'VueQuillEditor',
                'element-ui': 'ElementUI'
            })
        })
        //开发模式
        config.when(process.env.NODE_ENV === 'development',config=>{
            config.entry('app').clear().add('./src/main-dev.js')
        })
    }
}
  1. 在public/index.js文件下,让外部CDN引入这些排除掉的资源,
  • 根据 变量 决定是否 在 index.html 中 引入 外部 js和css

image-20210821150659984.png

    <!-- 引入 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" />
    <!-- element-ui 的样式表文件 -->
    <link rel="stylesheet" href="https://cdn.staticfile.org/element-ui/2.8.2/theme-chalk/index.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>

    <!-- element-ui 的 js 文件 -->
    <script src="https://cdn.staticfile.org/element-ui/2.8.2/index.js"></script>
  1. 打开开发入口文件main-prod.js,删除掉默认的引入代码
import Vue from 'vue'
import App from './App.vue'
import router from './router'
// import './plugins/element.js'
//导入字体图标
import './assets/fonts/iconfont.css'
//导入全局样式
import './assets/css/global.css'
//导入第三方组件vue-table-with-tree-grid
import TreeTable from 'vue-table-with-tree-grid'
//导入进度条插件
import NProgress from 'nprogress'
//导入进度条样式
// import 'nprogress/nprogress.css'
// //导入axios
import axios from 'axios'
// //导入vue-quill-editor(富文本编辑器)
import VueQuillEditor from 'vue-quill-editor'
// //导入vue-quill-editor的样式
// import 'quill/dist/quill.core.css'
// import 'quill/dist/quill.snow.css'
// import 'quill/dist/quill.bubble.css'

定制首页内容

开发环境的首页和发布环境的首页展示内容的形式有所不同 如开发环境中使用的是import加载第三方包,而发布环境则是使用CDN,

那么首页也需根据环境不同来进行不同的实现 我们可以通过插件的方式来定制首页内容,

image.png

  1. vue.config.js进行配置isProd变量(生产模式-true,开发模式-false)
module.exports = {
    chainWebpack:config=>{
        config.when(process.env.NODE_ENV === 'production',config=>{
            ......
            
            //使用插件
            config.plugin('html').tap(args=>{
                //添加参数isProd
                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=>{
                //添加参数isProd
                args[0].isProd = false
                return args
            })
        })
    }
}
  1. 然后在public/index.html中使用插件判断是否为发布环境并定制首页内容
  • 根据不同环境,设置不同的title
  • 根据不同环境,判断是否要引入CDN外部静态资源,避免重复引用模块
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    
    <title><%= htmlWebpackPlugin.options.isProd ? '' : 'dev - ' %>电商后台管理系统</title>

    <% if(htmlWebpackPlugin.options.isProd){ %>
    <!-- nprogress 的样式表文件 -->
    <link rel="stylesheet" href="https://cdn.staticfile.org/nprogress/0.2.0/nprogress.min.css" />
    ........
    <!-- element-ui 的 js 文件 -->
    <script src="https://cdn.staticfile.org/element-ui/2.8.2/index.js"></script>
    <% } %>
  </head>
  .......

路由懒加载

当路由被访问时才加载对应的路由文件,就是路由懒加载。

拆分chunks.js文件

由于默认webpack编译时,将所有的组件编译到 一个chunks.js文件中,体积会很大,浏览器首次加载时,比较耗时,所以将其拆分,优化用户体验 16471629530481_.pic_hd.jpg 1.安装开发依赖

yarn add @babel/plugin-syntax-dynamic-import -D
  1. babel.config.js中声明该插件
module.exports = {
    presets: ['@vue/cli-plugin-babel/preset'],
    plugins: [
        ...其他代码
        
        //配置路由懒加载插件
        '@babel/plugin-syntax-dynamic-import'
    ]
}`
  1. 将路由更改为按需加载的形式,并进行分组打包,打开router.js,更改引入组件代码
import Vue from 'vue'
import Router from 'vue-router'
// 将Login、Home、Welcome打包为一组 login_home_welcome
const Login = () => import(/* webpackChunkName:"login_home_welcome" */ './components/Login.vue')
// import Login from './components/Login.vue'
const Home = () => import(/* webpackChunkName:"login_home_welcome" */ './components/Home.vue')
// import Home from './components/Home.vue'
const Welcome = () => import(/* webpackChunkName:"login_home_welcome" */ './components/Welcome.vue')
// import Welcome from './components/Welcome.vue'

// 将Users打包为一组 user
const Users = () => import(/* webpackChunkName:"user" */ './components/user/Users.vue')
// import Users from './components/user/Users.vue'

项目上线配置

通过node创建服务器

在vue_shop同级创建一个文件夹vue_shop_server存放node服务器 使用终端打开vue_shop_server文件夹,

输入命令 npm init -y 初始化包之后,输入命令 npm i express -S 打开vue_shop目录,复制dist文件夹,粘贴到vue_shop_server中

在vue_shop_server文件夹中创建app.js文件,编写代码如下:

const express = require('express')

const app = express()

app.use(express.static('./dist'))

app.listen(8998,()=>{
    console.log("server running at http://127.0.0.1:8998")
})

然后再次在终端中输入 node app.js

开启gzip压缩

打开vue_shop_server文件夹的终端,输入命令:

npm i compression -D

打开app.js,编写代码:

const express = require('express')
// 导入包
const compression = require('compression')

const app = express()
// 注册中间件
app.use(compression())
app.use(express.static('./dist'))

app.listen(8998,()=>{
    console.log("server running at http://127.0.0.1:8998")
})