Vue2 基础七电商后台管理项目——上

代码下载

后台项目

后台项目这里不做详细记录,只为辅助前端开发,这是后台项目下载地址,接口文档为根目录下的 电商管理后台 API 接口文档.md 文件。

1、将后台项目下载到本地,使用MySQLWorkbench新建名为mydb的数据库。

2、在MySQLWorkbench中,点击 File -> Open SQL Script…,然后选中db文件夹中的mydb.sql文件打开,双击选中mydb数据库并点击运行。

3、使用VSCode打开项目,执行 npm i 安装依赖库,进入 default.json 文件修改 db_config 中的 password 为自己的数据库密码。

4、执行 node app.js 运行后台项目,此时可以使用 Postman 测试相应接口。

项目初始化

  • 执行 vue ui 命令,打开 http://localhost:8000 页面
  • 选择目录创建新项目,填写项目名称,点击下一步
  • 选择一套预设,手动或默认,点击下一步
  • 选择功能,开启 Babel、Router,Linter / Formatter,使用配置文件,点击下一步
  • 选择配置,选择 2.x vue 版本,关闭使用历史模式路由,选择 ESLint + Standard config 配置文件,开启 Lint on save,点击创建项目
  • 填写预设名,点击保存预设并创建项目
  • 点击 插件 -> 添加插件,进入插件查询面板,搜索 vue-cli-plugin-element 并安装
  • 点击 依赖 -> 添加依赖,进入依赖查询面板,搜索 axios 并安装

登录退出

如果服务器和客户端同源,建议可以使用cookie或者session来保持登录状态;如果客户端和服务器跨域了,建议使用token进行维持登录状态。

在登录页面输入账号和密码进行登录,将数据发送给服务器,服务器返回登录的结果,登录成功则返回数据中带有token。客户端得到token并进行保存,后续的请求都需要将此token发送给服务器,服务器会验证token以保证用户身份。

添加公共样式,在assets文件夹下面添加css文件夹,创建global.css文件,添加全局样式:

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

主要实现步骤

1、点击main.js文件(入口文件),导入所需文件:

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 axios from 'axios'
axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'
Vue.prototype.$http = axios

Vue.config.productionTip = false

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

2、打开App.vue(根组件),将根组件的内容进行操作梳理(添加路由占位符,template中留下根节点,script中留下默认导出,去掉组件,style中去掉所有样式):

<template>
  <div id="app">
    <router-view></router-view>
  </div>
</template>

<script>

export default {
  name: 'app'
}
</script>

<style>
</style>

3、添加element-ui的表单组件、在plugins文件夹中打开element.js文件,进行elementui的按需导入:

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.$msg = Message

4、在components文件夹中新建Home.vue组件,添加template,script,style标签,style标签中的scoped可以防止组件之间的样式冲突,没有scoped则样式是全局的:

<template>
    <div>
        Home 组件
        <button @click="logout">退出</button>
    </div>
</template>

<script>
export default {
    methods: {
        logout() {
            window.sessionStorage.removeItem('token')
            console.log('this: ', this)
            this.$router.push('/login')
        }
    }
}
</script>

<style lang="less" scoped>

</style>

5、在components文件夹中新建Login.vue组件,添加template,script,style标签,style标签中的scoped可以防止组件之间的样式冲突,没有scoped则样式是全局的:

<template>
    <div class="login_container">
        <div class="login_box">
            <!-- 头像 -->
            <div class="avatar_box">
                <img src="../assets/logo.png" alt="">
            </div>

            <!-- 表单 -->
            <el-form 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" @click="login">登录</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() {
            this.$refs.loginFormRef.resetFields()
        },
        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.$msg.error('登录失败!') }
                console.log(res)
                this.$msg.success('登录成功!')
                window.sessionStorage.setItem('token', res.data.token)
                this.$router.push('/home')
            })
        }
    }
}
</script>

<style lang="less" scoped>
.login_container {
    background-color: #2b4b6b;
    height: 100%;

    .login_box {
        background-color: white;
        width: 450px;
        height: 300px;
        position: absolute;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);

        .avatar_box {
            width: 130px;
            height: 130px;
            background-color: white;
            padding: 10px;
            border: 1px solid #eee;
            border-radius: 50%;
            box-shadow: 0 0 10px #ddd;
            position: absolute;
            left: 50%;
            transform: translate(-50%, -50%);

            img {
                width: 100%;
                height: 100%;
                background-color: #eee;
                border-radius: 50%;
            }
        }

        .login_form {
            position: absolute;
            bottom: 0;
            width: 100%;
            padding: 20px;
            box-sizing: border-box;
        }

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

6、将views删除,将components中的helloworld.vue删除,在router.js中导入组件并设置规则:

const routes = [
  { path: '/', redirect: '/login' },
  { path: '/login', component: login },
  { path: '/home', component: home }
]

补充

1、表单验证,给 <el-form> 添加属性 rules="loginFormRules",loginFormRules是一堆验证规则定义在script中,详见 Longin.vue。

2、导入axios以发送ajax请求 import axios from 'axios',设置请求的根路径 axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/',挂载axios Vue.prototype.$http = axios,详见入口文件 main.js。

3、弹窗提示,在plugins文件夹中打开element.js文件,进行elementui的按需导入 import {Message} from 'element-ui',进行全局挂载 Vue.prototype.$message = Message,在vue组件中编写弹窗代码 this.$message.error('登录失败')

4、登录成功之后的操作,需要将后台返回的token保存到sessionStorage中,操作完毕之后,需要跳转到/home

5、添加路由守卫,如果用户没有登录,不能访问 /home,如果用户通过url地址直接访问,则强制跳转到登录页面,详见router.js:

// 挂载路由导航守卫
router.beforeEach((to, from, next) => {
  // to: 将要访问的路径
  // from: 从哪个路径跳转而来
  // next: 放行函数,next()表示放行,next('/login')表示强制跳转
  if (to.path === '/login') return next()
  const token = window.sessionStorage.getItem('token')
  if (token) return next()
  next('/login')
})

6、添加第三方字体,复制字体文件夹fonts到assets中,在入口文件main.js中导入import './assets/fonts/iconfont.css',然后直接在 <el-input prefix-icon="iconfont icon-3702mima"></el-input>

7、处理ESLint报错,默认情况下,ESLint和vscode格式化工具有冲突,需要添加配置文件解决冲突。在项目根目录添加 .prettierrc 文件:

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

打开.eslintrc.js文件,禁用对 space-before-function-parenvue/multi-word-component-namesnoTabsindent 的检查:

  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'semi': 0, // 结尾分号
    'indent': 0, // 缩进
    'no-trailing-spaces': 'off', // 不允许尾随空格
    'eol-last': 'off', // 文件末尾需要换行
    'vue/multi-word-component-names': 'off', // 要求组件名称以驼峰格式命名,自定义组件名称应该由多单纯组成,防止和html标签冲突
    'space-before-function-paren': 'off' // 函数参数前加空格
  }

首页

布局

1、打开Home.vue组件,进行布局:

template>
    <div class="home-container">
        <el-container>
            <!-- 头部 -->
            <el-header>
                <div>
                    <img src="../assets/avanter.png" alt="">
                    <span>后台管理系统</span>
                </div>
                <el-button type="info" @click="logout">退出</el-button>
            </el-header>

            <!-- 主体 -->
            <el-container>
                <!-- 左边栏 -->
                <el-aside :width="isCollapse ? '64px' : '200px'">
                    <div class="toggle-button" @click="toggleCollapse">|||</div>
                    <!-- 菜单选择区 -->
                    <el-menu background-color="#333744" text-color="#fff" active-text-color="#409EFF" unique-opened :collapse="isCollapse" :collapse-transition="false" router :default-active="activePath">
                        <!-- 一级导航 -->
                        <el-submenu :index="v.id + ''" :key="i" v-for="(v, i) in menulist">
                            <template slot="title">
                                <!-- 图标 -->
                                <i :class="iconsObj[v.id]"></i>
                                <!-- 文本 -->
                                <span>{{v.authName}}</span>
                            </template>
                            <!-- 二级导航 -->
                            <el-menu-item :index="'/home/' + value.path" :key="value.id" v-for="(value) in v.children" @click="saveNavState('/home/' + value.path)">
                                <template slot="title">
                                    <i class="el-icon-menu"></i>
                                    <span>{{value.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>

2、在 plugins -> element.js 文件中导入用到的 element-ui 组件。

3、在 Home.vue 添加样式,默认情况下,跟 element-ui 组件同名的类名可以帮助我们快速的给对应的组件添加样式:

<style lang="less" scoped>
.home-container, .el-container {
    height: 100%;
}
.el-header {
    background-color: #373d41;
    display: flex;
    justify-content: space-between;
    padding-left: 0;
    align-items: center;
    font-size: 20px;
    color: white;

    > div {
        display: flex;
        align-items: center;

        > span {
            margin-left: 15px;
        }
    }
}
.toggle-button {
    background-color: #4a5064;
    font-size: 10px;
    line-height: 24px;
    color: white;
    text-align: center;
    letter-spacing: 0.2em;
    cursor: pointer;
}
.el-aside {
    background-color: #333744;
    .el-menu {
        border-right: none;
    }
}
.el-main {
    background-color: #eaedf1;
}
.iconfont {
    margin-right: 10px;
}
</style>

axios 拦截器

除了登录接口之外,都需要token权限验证,可以通过添加axios请求拦截器来添加token,以保证拥有获取数据的权限。在main.js中添加代码,在将axios挂载到vue原型之前添加下面的代码:

// 请求拦截器,请求在到达服务器之前,先会调用use中的这个回调函数来添加请求头信息
axios.interceptors.request.use(function(config) {
  console.log('request: ', config)
  config.headers.Authorization = window.sessionStorage.getItem('token')
  return config
})

响应拦截器的作用是在接收到响应后进行一些操作,在这里可以打印响应体,方便查看数据:

// 响应拦截器
axios.interceptors.response.use(function(res) {
  console.log('response: ', res)
  return res
})

主要功能实现

<script>
export default {
    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: ''
        }
    },
    created() {
        this.getMenuList()
        this.activePath = window.sessionStorage.getItem('path')
    },
    methods: {
        // 退出
        logout() {
            window.sessionStorage.removeItem('token')
            console.log('this: ', this)
            this.$router.push('/login')
        },
        // 获取边栏数据
        async getMenuList() {
            const { data: res } = await this.$http.get('menus')
            if (res.meta.status !== 200) { return this.$msg.error(res.meta.msg) }
            this.menulist = res.data
        },
        // 展开或折叠左侧边栏
        toggleCollapse() {
            this.isCollapse = !this.isCollapse;
        },
        saveNavState(path) {
            window.sessionStorage.setItem('path', path)
            this.activePath = path
        }
    }
}
</script>

1、请求侧边栏数据getMenuList,通过v-for双重循环渲染左侧菜单。

2、设置激活子菜单样式,通过更改el-menu的active-text-color属性可以设置侧边栏菜单中点击的激活项的文字颜色。

通过更改菜单项模板(template)中的i标签的类名,可以将左侧菜单栏的图标进行设置,需要在项目中使用第三方字体图标。在数据中添加一个iconsObj,然后将图标类名进行数据绑定,绑定iconsObj中的数据。

为了保持左侧菜单每次只能打开一个,显示其中的子菜单,可以在el-menu中添加一个属性unique-opened或者也可以数据绑定进行设置(此时true认为是一个bool值,而不是字符串) :unique-opened="true"

3、制作侧边菜单栏的伸缩功能,伸缩功能是通过collapse属性控制的,在菜单栏上方添加一个div,然后给div添加样式,给div添加事件。

4、在首页添加子级路由,新增子级路由组件Welcome.vue,在router.js中导入子级路由组件,并设置路由规则以及子级路由的默认重定向,打开Home.vue,在main的主体结构中添加一个路由占位符:

import login from '../components/Login.vue'
import home from '../components/Home.vue'
import welcome from '../components/Welcome.vue'

const routes = [
  { path: '/', redirect: '/login' },
  { path: '/login', component: login },
  { 
    path: '/home', 
    component: home, 
    redirect: '/home/welcome', 
    children: [
      { path: '/home/welcome', component: welcome }
    ]
  }
]

制作好了Welcome子级路由之后,需要将所有的侧边栏二级菜单都改造成子级路由链接。只需要将el-menu的router属性设置为true就可以了,此时当我们二级菜单的时候,就会根据菜单的index
属性进行路由跳转,如: /110,绑定index的值为 :index="'/home/' + value.path"

5、当点击二级菜单的时候,被点击的二级子菜单并没有高亮,需要正在被使用的功能高亮显示。以通过设置el-menu的default-active属性来设置当前激活菜单的index,但是default-active属性也不能写死,固定为某个菜单值。先给所有的二级菜单添加点击事件,并将path值作为方法的参数。

然后在数据中添加一个activePath绑定数据,并将el-menu的default-active属性设置为activePath。

最后在created中将sessionStorage中的数据赋值给activePath,this.activePath = window.sessionStorage.getItem("activePath")

用户列表页

新建用户列表组件 user/Users.vue,在router.js中导入子级路由组件Users.vue,并设置路由规则。

绘制用户列表基本结构

  • 使用element-ui面包屑组件完成顶部导航路径(复制面包屑代码,在element.js中导入组件Breadcrumb,BreadcrumbItem)
  • 使用element-ui卡片组件完成主体表格(复制卡片组件代码,在element.js中导入组件Card),再使用element-ui输入框完成搜索框及搜索按钮。
  • 此时需要使用栅格布局来划分结构(复制卡片组件代码,在element.js中导入组件Row,Col),然后再使用el-button制作添加用户按钮。
  • ……
<template>
    <div>
        <!-- 面包屑导航 -->
        <el-breadcrumb separator-class="el-icon-arrow-right">
            <el-breadcrumb-item :to="{ path: '/home/welcome' }">首页</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.trim="queryInfo.query" clearable @clear="getUserList">
                        <el-button slot="append" icon="el-icon-search" @click="queryUserList" :disabled="queryInfo.query ? false : true"></el-button>
                    </el-input>
                </el-col>
                <el-col :span="4">
                    <el-button type="primary" @click="addDialogVisible = true">添加用户</el-button>
                </el-col>
            </el-row>

            <!-- 用户列表 -->
            <el-table :data="userlist" style="width: 100%" border stripe>
                <el-table-column type="index" label="#"></el-table-column>
                <el-table-column  prop="username" label="姓名"></el-table-column>
                <el-table-column prop="email" label="邮箱"></el-table-column>
                <el-table-column prop="mobile" label="电话"></el-table-column>
                <el-table-column prop="role_name" label="角色"></el-table-column>
                <el-table-column label="状态">
                    <template slot-scope="scope">
                        <el-switch v-model="scope.row.mg_state" @change="userStateChanged(scope.row)"></el-switch>
                    </template>
                </el-table-column>
                <el-table-column label="操作" width="180">
                    <template slot-scope="scope">
                        <el-button type="primary" icon="el-icon-edit" size="mini" @click="showEditDialog(scope.row.id)"></el-button>
                        <el-button type="danger" icon="el-icon-delete" size="mini" @click="removeUserById(scope.row.id)"></el-button>
                        <el-tooltip class="item" 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>
                    </template>
                </el-table-column>
            </el-table>

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

        <!-- 添加用户对话框 -->
        <el-dialog title="添加用户" :visible.sync="addDialogVisible" width="50%" @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">
                    <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="addUser">确 定</el-button>
            </span>
        </el-dialog>

        <!-- 编辑用户对话框 -->
        <el-dialog title="修改用户" :visible.sync="editDialogVisible" width="50%" @close="editDialogClosed">
            <el-form :model="editForm" :rules="editFormRules" ref="editFormRef" label-width="70px">
                <el-form-item label="用户名" prop="username">
                    <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="editUser">确 定</el-button>
            </span>
        </el-dialog>

        <!-- 分配角色对话框 -->
        <el-dialog title="分配角色" :visible.sync="setRoleDialogVisible" width="50%">
            <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>
            <span slot="footer" class="dialog-footer">
                <el-button @click="setRoleDialogVisible = false">取 消</el-button>
                <el-button type="primary" @click="saveRoleInfo">确 定</el-button>
            </span>
        </el-dialog>
    </div>
</template>

主要功能实现

<script>
export default {
    data() {
        // 邮箱验证
        const checkEmail = (rule, value, cb) => {
            if (/^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]{2,}$/.test(value)) return cb()
            cb(new Error('请输入正确的邮箱'))
        }
        // 手机号验证
        const checkMobile = (rule, value, cb) => {
            if (/^(0|86|17951)?(13[0-9]|14[5-7]|15[012356789]|17[678]|18[0-9])\d{8}$/.test(value)) return cb()
            cb(new Error('请输入正确的手机号')) 
        }
        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: 2, max: 10, message: '长度在 2 到 10 个字符', trigger: 'blur' }
                ],
                password: [
                    { required: true, message: '请输入密码', trigger: 'blur' },
                    { min: 3, max: 10, 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' }
                ]
            },
            // 编辑用户对话框
            editDialogVisible: false,
            // 编辑用户的表单数据
            editForm: {
                id: '',
                username: '',
                email: '',
                mobile: ''
            },
            // 编辑用户表单验证
            editFormRules: {
                username: [
                    { min: 2, max: 10, message: '长度在 2 到 10 个字符', trigger: 'blur' }
                ],
                email: [
                    { required: true, message: '请输入邮箱', trigger: 'blur' },
                    { validator: checkEmail, trigger: 'blur' }
                ],
                mobile: [
                    { required: true, message: '请输入手机号', trigger: 'blur' },
                    { validator: checkMobile, trigger: 'blur' }
                ]
            },
            // 是否显示分配角色对话框
            setRoleDialogVisible: false,
            // 分配角色的用户
            userInfo: {},
            // 角色列表
            rolesList: [],
            // 选中的角色
            selectedRoleId: ''
        }
    },
    created() {
        this.getUserList()
    },
    methods: {
        // 获取用户列表
        async getUserList() {
            const { data: res } = await this.$http.get('users', { params: this.queryInfo })
            if (res.meta.status !== 200) return this.$msg.error(res.meta.msg)
            this.userlist = res.data.users
            this.total = res.data.total
        },
        // 搜索用户列表
        queryUserList() {
            // 重置为第一页
            this.queryInfo.pagenum = 1
            this.getUserList()
        },
        // pagesize 改变
        handleSizeChange(size) {
            console.log('size: ', size);
            this.queryInfo.pagesize = size
            this.getUserList()
        },
        // 页码值 改变
        handleCurrentChange(num) {
            console.log('size: ', size);
            this.queryInfo.pagesize = size
            const maxN = parseInt(this.total / this.queryInfo.pagesize + '') + (this.total % this.queryInfo.pagesize > 0 ? 1 : 0)
            this.queryInfo.pagenum = this.queryInfo.pagenum > maxN ? maxN : this.queryInfo.pagenum
            this.getUserList()
        },
        // witch 开关状态的改变
        async userStateChanged(info) {
            const { data: res } = await this.$http.put(`users/${info.id}/state/${info.mg_state}`)
            if (res.meta.status !== 200) {
                info.mg_state = !info.mg_state
                return this.$msg.error(info.meta.msg)
            }
            return this.$msg.success('更新用户状态成功!')
        },
        // 关闭添加用户对话框
        addDialogClosed() {
            // 重置表单数据
            this.$refs.addFormRef.resetFields()
        },
        // 添加用户
        addUser() {
            // 验证表单
            this.$refs.addFormRef.validate(async valid => {
                // 表单验证失败
                if (!valid) return this.$msg.error('请填写正确的用户数据')
                // 发起请求
                const { data: res } = await this.$http.post('users', this.addForm)
                if (res.meta.status !== 201) return this.$msg.error('添加用户失败')
                this.$msg.success('添加用户成功')
                // 关闭对话框
                this.addDialogVisible = false
                // 刷新列表
                this.getUserList()
            })
        },
        // 显示编辑用户对话框
        async showEditDialog(uid) {
            const { data: res } = await this.$http.get(`users/${uid}`)
            if (res.meta.status !== 200) return this.$msg.error(res.meta.msg)
            this.editForm = res.data
            this.editDialogVisible = true
        },
        // 关闭编辑用户对话框
        editDialogClosed() {
            this.$.$refs.editFormRef.resetFields()
        },
        // 编辑用户
        editUser() {
            // 验证表单
            this.$refs.editFormRef.validate(async valid => {
                // 表单验证失败
                if (!valid) return this.$msg.error('请填写正确的用户数据')
                // 发起请求
                const { data: res } = await this.$http.put(`users/${this.editForm.id}`, this.editForm)
                if (res.meta.status !== 200) return this.$msg.error('修改用户失败')
                this.$msg.success('修改用户成功')
                // 关闭对话框
                this.editDialogVisible = false
                // 刷新列表
                this.getUserList()
            })
        },
        // 根据Id删除对应用户
        async removeUserById(uid) {
            const confirm = await this.$confirm('此操作将永久删除该用户, 是否继续?', '提示', {
                confirmButtonText: '确定',
                cancelButtonText: '取消',
                type: 'warning'
            }).catch(e => e);

            // 如果用户确认删除,则返回值为字符串 confirm;如果用户取消了删除,则返回值为字符串 cancel
            console.log('confirm: ', confirm);
            if (confirm !== 'confirm') return

            const { data: res } = await this.$http.delete('users/' + uid)
            if (res.meta.status !== 200) this.$msg.error(res.meta.msg)
            this.$msg.success('删除用户成功')
            if (this.queryInfo.pagenum > 1 && this.userlist.length === 1) this.queryInfo.pagenum -= 1
            this.getUserList()
        }, 
        // 显示分配角色对话框
        async setRole(userInfo) {
            const { data: res } = await this.$http.get('roles')
            if (res.meta.status !== 200) return this.$msg.error(res.meta.msg)
            this.rolesList = res.data
            this.userInfo = userInfo
            this.selectedRoleId = ''
            this.setRoleDialogVisible = true
        },
        // 分配角色
        async saveRoleInfo() {
            if (!this.selectedRoleId) return this.$msg.error('请选择要分配的角色')
            const { data: res } = await this.$http.put('users/' + this.userInfo.id + '/role', { rid: 'this.selectedRoleId' })
            if (res.meta.status !== 200) return this.$msg.error(res.meta.msg)
            this.$msg.success('分配角色成功')
            this.getUserList()
            this.setRoleDialogVisible = false
        }
    }
}
</script>

1、请求用户列表数据getUserList,使用表格来展示用户列表数据,使用element-ui表格组件完成列表展示数据(复制表格代码,在element.js中导入组件Table,TableColumn)。

在渲染展示状态时,会使用作用域插槽获取每一行的数据,再使用switch开关组件展示状态信息(复制开关组件代码,在element.js中导入组件Switch)。

而渲染操作列时,也是使用作用域插槽来进行渲染的,在操作列中包含了修改,删除,分配角色按钮。

当把鼠标放到分配角色按钮上时希望能有一些文字提示,此时我们需要使用文字提示组件(复制文字提示组件代码,在element.js中导入组件Tooltip),将分配角色按钮包含。

2、实现用户列表分页,使用表格来展示用户列表数据,可以使用分页组件完成列表分页展示数据(复制分页组件代码,在element.js中导入组件Pagination)。更改组件中的绑定数据,添加两个事件的事件处理函数@size-change,@current-change。

3、实现更新用户状态,当用户点击列表中的switch组件时,用户的状态应该跟随发生改变。

首先监听用户点击switch组件的事件,并将作用域插槽的数据当做事件参数进行传递,在事件中发送请求完成状态的更改。

4、实现搜索功能,添加数据绑定,添加搜索按钮的点击事件(当用户点击搜索按钮的时候,调用queryUserList方法根据文本框内容重新请求用户列表第一页数据)。

当在输入框中输入内容并点击搜索之后,会按照搜索关键字搜索,希望能够提供一个X删除搜索关键字并重新获取所有的用户列表数据,只需要给文本框添加clearable属性并添加clear事件,在clear事件中重新请求数据即可。

5、实现添加用户,当点击添加用户按钮的时候,弹出一个对话框来实现添加用户的功能,首先需要复制对话框组件的代码并在element.js文件中引入Dialog组件

接下来要为“添加用户”按钮添加点击事件,在事件中将addDialogVisible设置为true,即显示对话框。

更改Dialog组件中的内容,添加数据绑定和校验规则。

当关闭对话框时重置表单,给el-dialog添加@close事件,在事件中添加重置表单的代码。

点击对话框中的确定按钮,发送请求完成添加用户的操作,首先给确定按钮添加点击事件,在点击事件中完成业务逻辑代码。

6、修改用户信息,为用户列表中的修改按钮绑定点击事件,在页面中添加修改用户对话框,并修改对话框的属性,根据id查询需要修改的用户数据。

在弹出窗中添加修改用户信息的表单并做响应的数据绑定以及数据验证;监听对话框关闭事件,在对话框关闭之后,重置表单;在用户点击确定按钮的时候,验证数据成功之后发送请求完成修改。

7、删除用户,在点击删除按钮的时候,应该跳出提示信息框,让用户确认要进行删除操作。

如果想要使用确认取消提示框,需要先将提示信息框挂载到vue中。导入MessageBox组件,并将MessageBox组件挂载到实例Vue.prototype.$confirm = MessageBox.confirm

给用户列表中的删除按钮添加事件,并在事件处理函数中弹出确定取消窗,最后再根据id发送删除用户的请求。

8、分配角色,添加分配角色对话框,给分配角色按钮添加点击事件,点击之后弹出一个对话框进行角色分配。

在element.js中引入Select,Option,注册Select,Option。

当用户点击对话框中的确定之后,完成分配角色的操作

权限列表页

添加权限列表路由,创建权限管理组件(Rights.vue),并在router.js添加对应的路由规则。

绘制用户列表基本结构

  • 添加面包屑导航,在Rights.vue中添加面包屑组件展示导航路径。
  • ……
<template>
    <div>
        <!-- 面包屑导航 -->
        <el-breadcrumb separator-class="el-icon-arrow-right">
            <el-breadcrumb-item :to="{ path: '/home/welcome' }">首页</el-breadcrumb-item>
            <el-breadcrumb-item>权限管理</el-breadcrumb-item>
            <el-breadcrumb-item>权限列表</el-breadcrumb-item>
        </el-breadcrumb>

        <!-- 卡片 -->
        <el-card >
            <!-- 权限列表 -->
            <el-table :data="rightsList" style="width: 100%" border stripe>
                <el-table-column type="index" label="#"></el-table-column>
                <el-table-column  prop="authName" label="权限名称"></el-table-column>
                <el-table-column prop="path" label="路径"></el-table-column>
                <el-table-column label="权限等级">
                    <template slot-scope="scope">
                        <el-tag v-if="scope.row.level == 0">一级</el-tag>
                        <el-tag type="success" v-else-if="scope.row.level == 1">二级</el-tag>
                        <el-tag type="warning" v-else>三级</el-tag>
                    </template>
                </el-table-column>
            </el-table>
        </el-card>
    </div>
</template>

主要功能实现

显示数据,在data中添加一个rightsList数据,在methods中提供一个getRightsList方法发送请求获取权限列表数据,在created中调用这个方法获取数据:

<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.$msg.error(res.meta.msg)
            this.rightsList = res.data
        }
    }
}
</script>

角色列表页

添加角色列表路由,添加角色列表子组件(power/Roles.vue),并添加对应的规则。

绘制用户列表基本结构

  • 添加面包屑导航,在Roles.vue中添加面包屑组件展示导航路径。
  • ……
<template>
    <div>
        <!-- 面包屑导航 -->
        <el-breadcrumb separator-class="el-icon-arrow-right">
            <el-breadcrumb-item :to="{ path: '/home/welcome' }">首页</el-breadcrumb-item>
            <el-breadcrumb-item>权限管理</el-breadcrumb-item>
            <el-breadcrumb-item>角色列表</el-breadcrumb-item>
        </el-breadcrumb>

        <!-- 卡片 -->
        <el-card>
            <el-button type="primary">添加角色</el-button>

            <!-- 权限列表 -->
            <el-table :data="rolesList" style="width: 100%" border stripe>
                <el-table-column type="expand">
                    <template slot-scope="scope">
                        <el-row v-for="(v, i) in scope.row.children" :key="v.id" :class="['vcenter', 'bdbottom', i == 0 ? 'bdtop': '']">
                            <!-- 一级权限 -->
                            <el-col :span="5">
                                <el-tag closable @close="removeRightById(scope.row, v.id)">{{v.authName}}</el-tag>
                                <i class="el-icon-caret-right"></i>
                            </el-col>
                            <!-- 二级和三级权限 -->
                            <el-col :span="19">
                                <el-row v-for="(v1, i1) in v.children" :key="v1.id" :class="['vcenter', i1 !== 0 ? 'bdtop' : '']">
                                    <!-- 二级权限 -->
                                    <el-col :span="6">
                                        <el-tag type="success" closable @close="removeRightById(scope.row, v1.id)">{{v1.authName}}</el-tag>
                                        <i class="el-icon-caret-right"></i>
                                    </el-col>
                                    <!-- 三级权限 -->
                                    <el-col :span="18">
                                        <el-tag type="warning" v-for="(v2) in v1.children" :key="v2.id" closable @close="removeRightById(scope.row, v2.id)">{{v2.authName}}</el-tag>
                                    </el-col>
                                </el-row>
                            </el-col>
                        </el-row>
                        <!-- <pre>{{scope.row}}</pre> -->
                    </template>
                </el-table-column>
                <el-table-column type="index" label="#"></el-table-column>
                <el-table-column  prop="roleName" label="角色名称"></el-table-column>
                <el-table-column prop="roleDesc" label="角色描述"></el-table-column>
                <el-table-column label="操作">
                    <template slot-scope="scope">
                        <el-button type="primary" icon="el-icon-edit" size="mini">编辑</el-button>
                        <el-button type="danger" icon="el-icon-delete" size="mini">删除</el-button>
                        <el-button type="warning" icon="el-icon-setting" size="mini" @click="showSetRightDialog(scope.row)">分配权限</el-button>
                    </template>
                </el-table-column>
            </el-table>
        </el-card>

        <!-- 分配权限对话框 -->
        <el-dialog title="分配权限" :visible.sync="setRightDialogVisible" width="50%">
            <el-tree :data="rightsList" :props="treeProps" show-checkbox node-key="id" default-expand-all :default-checked-keys="defKeys" ref="treeRef"></el-tree>
            <!-- <pre>{{rightsList}}</pre> -->
            <span slot="footer" class="dialog-footer">
                <el-button @click="setRightDialogVisible = false">取 消</el-button>
                <el-button type="primary" @click="allotRights">确 定</el-button>
            </span>
        </el-dialog>
    </div>
</template>

主要功能实现

添加角色、删除角色与用户列表页的添加用户、删除用户类似,编辑角色请参照之前编写过的代码还有接口文档完成效果。

<script>
export default {
    data() {
        return {
            rolesList: [],
            rightsList: [],
            setRightDialogVisible: false,
            treeProps: {
                children: 'children',
                label: 'authName'
            },
            defKeys: [],
            roleId: ''
        }
    },
    created() {
        this.getRolesList()
    },
    methods: {
        async getRolesList() {
            const { data: res } = await this.$http.get('roles')
            if (res.meta.status !== 200) return this.$msg.error(res.meta.msg)
            this.rolesList = res.data
        },
        async removeRightById(role, rightId) {
            const confirm = await this.$confirm('此操作将永久删除该权限, 是否继续?', '提示', {
                confirmButtonText: '确定',
                cancelButtonText: '取消',
                type: 'warning'
            }).catch(e => e);

            // 如果用户确认删除,则返回值为字符串 confirm;如果用户取消了删除,则返回值为字符串 cancel
            console.log('confirm: ', confirm);
            if (confirm !== 'confirm') return

            // 确认删除
            const { data: res } = await this.$http.delete(`roles/${role.id}/rights/${rightId}`)
            if (res.meta.status !== 200) return this.$msg.error(res.meta.msg)
            this.$msg.success('权限删除成功')

            // 会触发页面重新渲染
            // this.getRolesList()
            role.children = res.data
        },
        async showSetRightDialog(role) {
            console.log('role: ', role);
            // 获取权限列表
            const { data: res } = await this.$http.get('rights/tree')
            if (res.meta.status !== 200) return this.$msg.error(res.meta.msg)

            this.rightsList = res.data
            // let keys = []
            // this.getLeafKeys(role, keys)
            // this.defKeys = keys
            this.defKeys = this.getLeafKeys(role)
            this.setRightDialogVisible = true
            this.roleId = role.id
        },
        // 递归获取角色所有三级权限id,保存到defKeys中
        getLeafKeys(node, arr) {
            // 三级权限
            if (!node.children) return [node.id]

            // return node.children.forEach(element => {
            //     return this.getLeafKeys(element, arr)
            // });

            return node.children.reduce((pv, cv) => pv.concat(this.getLeafKeys(cv)), [])
        },
        // 分配权限
        async allotRights() {
            const keys = [...this.$refs.treeRef.getCheckedKeys(), ...this.$refs.treeRef.getHalfCheckedKeys()]
            const rids = keys.join(',')
            console.log('rids: ', rids);
            const { data: res } = await this.$http.post(`roles/${this.roleId}/rights`, { rids })
            if (res.meta.status !== 200) return this.$msg.error(res.meta.msg)
            this.$msg.success('分配权限成功')
            this.getRolesList()
            this.setRightDialogVisible = false
        }
    }
}
</script>

1、显示数据,在data中添加一个rolesList数据,在methods中提供一个getRolesList方法发送请求获取权限列表数据,在created中调用这个方法获取数据。

2、生成权限列表,使用三重嵌套for循环生成权限下拉列表。

3、美化样式,通过设置global.css中的#app样式min-width:1366px 解决三级权限换行的问题。

通过给一级权限el-row添加display:flex,align-items:center的方式解决一级权限垂直居中的问题,二级权限也类似添加。因为需要给多个内容添加,可以将这个样式设置为一个.vcenter{display:flex;align-items:center}

4、添加权限删除功能,给每一个权限的el-tag添加closable属性,是的权限右侧出现“X”图标。

再给el-tag添加绑定close事件处理函数removeRightById(scope.row, v.id)removeRightById(scope.row, v1.id)removeRightById(scope.row, v2.id)

5、完成权限分配功能,先给分配权限按钮添加事件showSetRightDialog,在showSetRightDialog函数中请求权限树数据并显示对话框。添加分配权限对话框,并添加绑定数据setRightDialogVisible。

6、完成树形结构弹窗,在element.js中引入Tree,注册Tree。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容