vue实战电商管理后台

时间:2022-07-26
本文章向大家介绍vue实战电商管理后台,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

涉及技术

后台

Nodejs 搭建并提供API接口文档

电商管理后台 API 接口文档(部分)

API V1 接口说明

  • 接口基准地址:http://127.0.0.1:8888/api/private/v1/
  • 服务端已开启 CORS 跨域支持
  • API V1 认证统一使用 Token 认证
  • 需要授权的 API ,必须在请求头中使用 Authorization 字段提供 token 令牌
  • 使用 HTTP Status Code 标识状态
  • 数据返回格式统一使用 JSON

支持的请求方法

  • GET(SELECT):从服务器取出资源(一项或多项)。
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
  • DELETE(DELETE):从服务器删除资源。
  • HEAD:获取资源的元数据。
  • OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。

通用返回状态说明

状态码

含义

说明

200

OK

请求成功

201

CREATED

创建成功

204

DELETED

删除成功

400

BAD REQUEST

请求的地址不存在或者包含不支持的参数

401

UNAUTHORIZED

未授权

403

FORBIDDEN

被禁止访问

404

NOT FOUND

请求的资源不存在

422

Unprocesable entity

[POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误

500

INTERNAL SERVER ERROR

内部错误


登录

登录验证接口

  • 请求路径:login
  • 请求方法:post
  • 请求参数

参数名

参数说明

备注

username

用户名

不能为空

password

密码

不能为空

  • 响应参数

参数名

参数说明

备注

id

用户 ID

rid

用户角色 ID

username

用户名

mobile

手机号

email

邮箱

token

令牌

基于 jwt 的令牌

  • 响应数据
{
    "data": {
        "id": 500,
        "rid": 0,
        "username": "admin",
        "mobile": "123",
        "email": "123@qq.com",
        "token": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjUwMCwicmlkIjowLCJpYXQiOjE1MTI1NDQyOTksImV4cCI6MTUxMjYzMDY5OX0.eGrsrvwHm-tPsO9r_pxHIQ5i5L1kX9RX444uwnRGaIM"
    },
    "meta": {
        "msg": "登录成功",
        "status": 200
    }
}

前端

Vue + ElementUI

环境搭建

项目配置

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'

// 导入 axios
import axios from 'axios'

// 全局的 axios 默认值
// 配置请求的根路径
axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'

// 设置拦截器
// 在请求或响应被 then 或 catch 处理前拦截它们。 
axios.interceptors.request.use(config => {
  // 在发送请求之前做些什么
  // console.log(config)
  config.headers.Authorization = window.sessionStorage.getItem('token')
  // 必须 return config
  return config
})

// 挂在在Vue实例中,每一个组件可以通过 $http 获取 axios 对象
Vue.prototype.$http = axios

// Vue 控制台提示
Vue.config.productionTip = false

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

vue 默认配置

  1. 导入路由router
  2. 将路由挂载到Vue实例中,方便后面的使用

axios 配置

  1. 导入axios
  2. 配置axios请求的根路径,从API文档中获取
  3. 配置axios请求拦截器,用于处理携带token
  4. 将axios配置到全局Vue实例中,方便后面的使用

其他配置

  1. 导入Element
  2. 导入全局样式表,用于全局通用
  3. 导入字体图标,用于全局通用

App.vue

Vue入口,只需要添加路由占位符即可

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

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

<style></style>

router/index.js

删除多余的配置

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [
  
]

const router = new VueRouter({
  routes
})

export default router

plugins/element.js

由于项目中的 ElementUI 选择的是按需导入组件,所有会出现这个js

// 按需要导入 element 组件
import Vue from 'vue'
import {
  Button,
  Form,
  FormItem,
  Input,
  Container,
  Header
} from 'element-ui'

// 导入弹框提示组件
import { Message } from 'element-ui'


Vue.use(Button)
Vue.use(Form)
Vue.use(FormItem)
Vue.use(Input)
Vue.use(Container)
Vue.use(Header)

// Message 要挂载在全局 Vue 的一个原型(prototype)上
// $message 是自定义名字
// 这样在每个组件中可以使用 this.$message.success("提示信息")
Vue.prototype.$message = Message
  1. Message 比较特殊,导入的方式有所不同,后面可以通过 this.$message.success('操作成功') 调用

项目开发

Login 模块

components/Login.vue

<template>
  <div class="login_container">
    <div class="login_box">
      <!-- 头像区域 -->
      <div class="avatar_box">
        <img src="@/assets/logo.png" alt="" />
      </div>
      <!-- 表单区域
       一、数据绑定
            1. 数据绑定 loginForm 对象
            2. 在data中定义 loginForm 对象
            3. 在 input中使用 loginForm.属性 进行双向绑定

        二、数据验证
            1. 为 el-form 绑定数据验证 loginFormRules 对象
            2. 在data中定义 loginFormRules 对象
            3. 在 el-form-item 中使用 prop="属性名"

        三、表单的实例对象 
            定义 ref="xxx"
            通过 this.$refs.xxx 获取表单的实例对象

            1. 重置  this.$refs.xxx.resetFields(); 
            2. 预验证  this.$refs.loginFormRef.validate((valid) => {});  valid 是一个boolean值
       -->
      <el-form
        ref="loginFormRef"
        class="login_form"
        :model="loginForm"
        :rules="loginFormRules"
      >
        <!-- 用户名 -->
        <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
            type="password"
            prefix-icon="iconfont icon-3702mima"
            v-model="loginForm.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' }]
      }
    }
  },
  methods: {
    // 点击重置按钮事件
    resetLoginForm() {
      //console.log(this)
      this.$refs.loginFormRef.resetFields()
    },
    // 点击登录按钮事件
    login() {
      this.$refs.loginFormRef.validate(async valid => {
        // console.log(valid);
        // 如果验证不成功,直接返回
        if (!valid) {
          return
        }
        // 发起请求
        // 如果返回的结果是 Promise,可以使用 await 和 async 简化操作
        // const result = await this.$http.post("login", this.loginForm);
        // ES6 解构写法 { data: res }
        const { data: res } = await this.$http.post('login', this.loginForm)
        console.log(res)
        if (res.meta.status != 200) {
          //   return console.log("登录失败");
          return this.$message.error('登录失败')
        }
        // console.log("登录成功");
        this.$message.success('登录成功')

        // 1. 将登录成功之后的 token,保存到客户端的 sessionStorage 中
        //   1.1 项目中除了登录外的其他API接口,必须在登录后才能访问
        //   1.2 token 只在当前网站打开期间生效,所以将 token 保存在 sesisonStorage 中
        // 扩展: localStorage生命周期是永久,这意味着除非用户显示在浏览器提供的UI上清除localStorage信息,否则这些信息将永远存在。
        //       sessionStorage生命周期为当前窗口或标签页,一旦窗口或标签页被永久关闭了,那么所有通过sessionStorage存储的数据也就被清空了。
        window.sessionStorage.setItem('token', res.data.token)

        // 2. 通过编程式导航跳转到后台主页,路径地址是 /home
        this.$router.push('/home')
      })
    }
  }
}
</script>

<style lang="less" 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 {
    width: 130px;
    height: 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;
    }
  }
}

.btns {
  // 弹性布局,放置尾部
  display: flex;
  justify-content: flex-end;
}

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

这里我们使用了 ElementUI 的第一个组件 el-form

数据绑定、数据验证、数据实例

<el-form
        ref="loginFormRef"
        class="login_form"
        :model="loginForm"
        :rules="loginFormRules"
      >
        <!-- 用户名 -->
        <el-form-item prop="username">
          <el-input
            prefix-icon="iconfont icon-user"
            v-model="loginForm.username"
          ></el-input>
.....

数据绑定

  1. el-form 中使用 :model 数据绑定,绑定一个在 data() 中定义的对象
  2. el-input 中使用 v-model 进行数据双向绑定,格式 v-model="loginForm.username"

数据验证

  1. el-form 中使用 :rules 数据验证,绑定一个在 data() 中定义的对象
  2. el-form-item 中使用 prop 属性设置为需校验的字段名,格式 prop="username"

对象实例

  1. el-form 中使用 ref 映射一个名称,可通过 this.$refs 获取,格式 ref="loginFormRef"
  2. 获取实例后,可以对表单实例进行重置、预验证等操作

登录、重置

<!-- 按钮区域 -->
<el-form-item class="btns">
    <el-button type="primary" @click="login">登录</el-button>
    <el-button type="info" @click="resetLoginForm">重置</el-button>
</el-form-item>

登录

  1. 当点击登录按钮,调用 login 方法
  2. 先对表单预验证,需要使用表单实例 this.$refs.loginFormRef.validate(valid => console.log(valid)),返回结果true或false,表示表单是否通过预验证
  3. 当通过预验证后,可以通过 axios 发起请求 this.http.post('login', this.loginForm),这里的 http 就是 axios 实例,前面我们已经在Vue实例中注册了,由于结果返回 promise 对象,我们可以通过 ES6 语法进行解构 const { data: res } = await this.
  4. 如果服务器响应成功,可以通过 this.$message.success('登录成功') 返回成功消息
  5. 将服务器返回的 token,保存到客户端的 sessionStorage 中,后面的各种请求都必须携带 token 值
  6. 保存token后,通过编程式导航跳转到后台主页,格式 this.$router.push('/home')

tips: localStorage生命周期是永久,这意味着除非用户显示在浏览器提供的UI上清除localStorage信息,否则这些信息将永远存在。 sessionStorage生命周期为当前窗口或标签页,一旦窗口或标签页被永久关闭了,那么所有通过sessionStorage存储的数据也就被清空了

重置

  1. 当点击重置按钮,调用 resetLoginForm 方法
  2. 通过表单实例对表单进行重置操作 this.$refs.loginFormRef.resetFields()

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'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    redirect: '/login'
  },
  {
    path: '/login',
    component: Login
  },
  {
    path: '/home',
    component: Home,
    // 实现首页路由重定向到子路由Welcome
    redirect: '/welcome',
    // 子路由
    children: [
      // 根据服务器的path设置
      { path: '/welcome', component: Welcome }
    ]
  }
]

const router = new VueRouter({
  routes
})

// 挂在路由导航守卫
// to 将要访问的路径
// from 代表从哪个路径跳转而来
// next 是一个函数,表示放行,两种形式【next() 放行、 next('/login') 强制跳转】
router.beforeEach((to, from, next) => {
  // 1. 如果用户访问的是登录页面,直接放行
  if (to.path == '/login') {
    return next()
  }
  // 2. 如果用户访问有权限的页面
  //   2.1 获取token
  const token = window.sessionStorage.getItem('token')
  //   2.2 判断是否存在token
  if (!token) {
    // 强制跳转到登录页面
    return next('/login')
  }
  next()
})

export default router

路由重定向、子路由

  1. 使用 redirect 实现重定向
  2. 使用 children 配置子路由

路由导航守卫

  1. 导航守卫有三个属性,分别是 to 将要访问的路径、from 代表从哪个路径跳转而来 、next 是一个函数,表示放行,有两种形式:next()表示放行 、 next('/login') 表示强制跳转
  2. 如果用户访问的是登录页面 to.path == '/login',直接放行 next()
  3. 如果用户访问的是其他有权限的页面,首先从客户端的 sessionStorage 中获取 token,如果存在则放行next(),如果不存在则跳转登录页面next('/login')

演示

Home 模块

components/Home.vue

<template>
  <el-container class="home-container">
    <!-- 头部区域 -->
    <el-header>
      <div>
        <img src="@/assets/logo.png" />
        <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="toggerCollapse">|||</div>
        <!-- 侧边栏菜单区域
          unique-opened  是否只保持一个子菜单的展开
          collapse 是否水平折叠收起菜单
          collapse-transition 是否开启折叠动画
          router  是否使用 vue-router 的模式,启用该模式会在激活导航时以 index 作为 path 进行路由跳转
         -->
        <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
            v-for="(item, index) in menuList"
            :key="item.id"
            :index="index + ''"
          >
            <!-- 一级菜单的模板 -->
            <template slot="title">
              <!-- 图标 -->
              <i :class="iconsObj[item.id]"></i>
              <!-- 文本 -->
              <span>{{ item.authName }}</span>
            </template>

            <!-- 二级菜单 -->
            <el-menu-item
              v-for="subItem in item.children"
              :key="subItem.id"
              :index="'/' + subItem.path"
              @click="saveNavState('/' + subItem.path)"
            >
              <!-- 二级菜单的模板 -->
              <template slot="title">
                <!-- 图标 -->
                <i class="el-icon-menu"></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>
</template>

<script>
export default {
  data() {
    return {
      // 左侧菜单数据
      menuList: [],
      // 以每个菜单的id作为 key值,找到对应的图片
      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('activePath')
  },
  methods: {
    logout() {
      // 1. 清空 sessionStorage
      window.sessionStorage.clear()
      // 2. 跳转到登录页面
      this.$router.push('/login')
    },
    // 获取所有的菜单
    async getMenuList() {
      const { data: res } = await this.$http.get('menus')
      // console.log(res)
      // 如果失败
      if (res.meta.status !== 200) {
        return this.$message.error(res.meta.msg)
      }
      this.menuList = res.data
    },
    // 点击按钮切换菜单折叠与展开
    toggerCollapse() {
      this.isCollapse = !this.isCollapse
    },
    // 保存链接的激活状态
    saveNavState(activePath) {
      window.sessionStorage.setItem('activePath', activePath)
      this.activePath = activePath
    }
  }
}
</script>

<style lang="less" scoped>
.home-container {
  height: 100%;
}
.el-header {
  background-color: #373d41;
  display: flex;
  justify-content: space-between;
  padding-left: 15px;
  align-items: center;
  color: #fff;
  font-size: 16px;
  > div {
    display: flex;
    align-items: center;
    > img {
      width: 50px;
      height: 50px;
    }
    > span {
      margin-left: 15px;
    }
  }
}
.el-aside {
  background-color: #333744;
  .el-menu {
    border-right: none;
  }
}
.el-main {
  background-color: #eaedf1;
}
.iconfont {
  margin-right: 10px;
}
.toggle-button {
  background-color: #4a5064;
  font-size: 10px;
  line-height: 24px;
  color: #fff;
  text-align: center;
  letter-spacing: 0.5em;
  cursor: pointer;
}
</style>

这里我们使用了 ElementUI 组件 el-containerel-menu

  1. 头部区域 el-header
  2. 侧边栏区域 el-aside
  3. 主内容区域 el-main

退出

  1. 当点击退出按钮,调用 logout 方法
  2. 清空客户端 sessionStorage 中保存的 token,格式 window.sessionStorage.clear()
  3. 使用导航式编程跳转到登录页面,格式 this.$router.push('/login')

菜单

<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
            v-for="(item, index) in menuList"
            :key="item.id"
            :index="index + ''"
          >
            <!-- 一级菜单的模板 -->
            <template slot="title">
              <!-- 图标 -->
              <i :class="iconsObj[item.id]"></i>
              <!-- 文本 -->
              <span>{{ item.authName }}</span>
            </template>

            <!-- 二级菜单 -->
            <el-menu-item
              v-for="subItem in item.children"
              :key="subItem.id"
              :index="'/' + subItem.path"
              @click="saveNavState('/' + subItem.path)"
            >
              <!-- 二级菜单的模板 -->
              <template slot="title">
                <!-- 图标 -->
                <i class="el-icon-menu"></i>
                <!-- 文本 -->
                <span>{{ subItem.authName }}</span>
              </template>
            </el-menu-item>
          </el-submenu>
        </el-menu>

el-menu可选属性

  1. unique-opened 是否只保持一个子菜单的展开
  2. collapse 是否水平折叠收起菜单
  3. collapse-transition 是否开启折叠动画
  4. router 是否使用 vue-router 的模式,启用该模式会在激活导航时以 index 作为 path 进行路由跳转,在二级菜单中,我们将 :index 设置为数据结果中的 path即可实现跳转,注意路径按需要是否添加 /
  5. default-active 是否激活菜单选中状态

el-submenu 表示一级菜单

el-menu-item 表示二级菜单

<template slot="scope"> 表示作用域插槽

生命周期函数

生命周期函数,详细参考文档

每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。

比如 created 钩子可以用来在一个实例被创建之后执行代码:

// 生命周期函数
created() {
  this.getMenuList()
  this.activePath = window.sessionStorage.getItem('activePath')
},

子路由渲染

  1. 当点击左侧二级菜单时,由于右侧内容区域添加了路由占位符 <router-view></router-view> ,用于给 Home 组件的子组件进行页面渲染
  2. 子组件路由配置 在 router/index.js,根据 children 里面的 path 进行跳转

演示

User 模块

components/User.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 class="box-card">
      <!-- 搜索与添加区域
        gutter 间距 
      -->
      <el-row :gutter="20">
        <el-col :span="8">
          <!-- 数据双向绑定 -->
          <el-input
            placeholder="请输入内容"
            clearable
            @clear="getUserList"
            v-model="queryInfo.query"
          >
            <el-button
              @click="getUserList"
              slot="append"
              icon="el-icon-search"
            ></el-button>
          </el-input>
        </el-col>
        <el-col :span="4">
          <el-button type="primary" @click="addDialogVisible = true"
            >添加用户</el-button
          >
        </el-col>
      </el-row>

      <!-- 用户列表区域
        :data 指定数据源
        border 边框
        stripe 斑马线
       -->
      <el-table :data="userList" border stripe>
        <!-- type="index" 索引列 -->
        <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="状态">
          <!-- 作用域插槽渲染 -->
          <template slot-scope="scope">
            <!-- scope.row 相当于当前行的所有数据 -->
            <el-switch
              v-model="scope.row.mg_state"
              @change="userStateChanged(scope.row)"
            >
            </el-switch>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="175px">
          <!-- 作用域插槽渲染 -->
          <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
              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>

      <!-- 分页区域
        handleSizeChange 监听 pageSize 改变时会触发
        handleCurrentChange 监听 currentPage 改变时会触发
        :current-page 当前页数
        :page-sizes 每页显示个数选择器的选项设置
        :page-size 每页显示条目个数
        layout 组件布局,子组件名用逗号分隔
        :total 总条目数
       -->
      <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>

      <!-- 添加用户的对话框 -->
      <el-dialog
        title="添加用户"
        :visible.sync="addDialogVisible"
        width="50%"
        @close="addDialogClosed"
      >
        <!-- 内容主体区域
          :model 数据绑定对象
          :rules 数据验证规则
          ref 引用对象,可以拿到form实例
         -->
        <el-form
          :model="addForm"
          :rules="addFormRules"
          ref="addFormRef"
          label-width="70px"
        >
          <!-- prop 使用具体的校验规则 -->
          <el-form-item label="用户名" prop="username">
            <!-- v-model 数据双向绑定,绑定到 addForm 对象中  -->
            <el-input v-model="addForm.username"></el-input>
          </el-form-item>
          <el-form-item label="密码" prop="password">
            <el-input v-model="addForm.password" type="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" ref="editFormRef" :rules="editFormRules">
          <el-form-item label="用户名">
            <!-- v-model 数据双向绑定,绑定到 editForm 对象中  -->
            <el-input v-model="editForm.username" disabled></el-input>
          </el-form-item>
          <!-- prop 使用具体的校验规则 -->
          <el-form-item label="邮箱" prop="email">
            <!-- v-model 数据双向绑定,绑定到 editForm 对象中  -->
            <el-input v-model="editForm.email"></el-input>
          </el-form-item>
          <el-form-item label="手机" prop="mobile">
            <!-- v-model 数据双向绑定,绑定到 editForm 对象中  -->
            <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="editUserInfo">确 定</el-button>
        </span>
      </el-dialog>

      <!-- 分配角色的对话框 -->
      <el-dialog
        title="分配角色"
        :visible.sync="setRoleDialogVisible"
        width="50%"
        @close="setRoleDialogClosed"
      >
        <div>
          <el-form :model="userInfo" label-widtd="80px">
            <el-form-item label="当前的用户">
              <el-input v-model="userInfo.username" disabled></el-input>
            </el-form-item>
            <el-form-item label="当前的角色">
              <el-input v-model="userInfo.role_name" disabled></el-input>
            </el-form-item>
            <el-form-item label="分配新角色">
              <el-select
                v-model="selectedRoleId"
                placeholder="请选择"
                style="width:100%"
              >
                <el-option
                  v-for="item in rolesList"
                  :key="item.id"
                  :label="item.roleName"
                  :value="item.id"
                >
                </el-option>
              </el-select>
            </el-form-item>
          </el-form>
        </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>
    </el-card>
  </div>
</template>

<script>
export default {
  data() {
    // 验证邮箱的规则
    var checkEmail = (rule, value, callback) => {
      const regEmail = /^([a-zA-Z]|[0-9])(w|-)+@[a-zA-Z0-9]+.([a-zA-Z]{2,4})$/

      if (regEmail.test(value)) {
        // 合法
        return callback()
      }
      callback(new Error('请输入合法的邮箱'))
    }
    // 验证手机号的规则
    var checkMobile = (rule, value, callback) => {
      const regMobile = /^[1][3,4,5,7,8][0-9]{9}$/

      if (regMobile.test(value)) {
        // 合法
        return callback()
      }
      callback(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: 3,
            max: 10,
            message: '用户名长度在 3 - 10 个字符之间',
            trigger: 'blur'
          }
        ],
        password: [
          { required: true, message: '请输入密码', trigger: 'blur' },
          {
            min: 3,
            max: 10,
            message: '密码长度在 6 - 12 个字符之间',
            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: {},
      // 修改表单的验证规则对象
      editFormRules: {
        email: [
          { required: true, message: '请输入邮箱', trigger: 'blur' },
          { validator: checkEmail, trigger: 'blur' }
        ],
        mobile: [
          { required: true, message: '请输入手机', trigger: 'blur' },
          { validator: checkMobile, trigger: 'blur' }
        ]
      },
      // 控制分配角色对话框的显示与隐藏
      setRoleDialogVisible: false,
      // 需要被分配角色的用户信息
      userInfo: {},
      // 所有角色的数据列表
      rolesList: [],
      // 已选中的角色id值
      selectedRoleId: ''
    }
  },
  created() {
    this.getUserList()
  },
  methods: {
    async getUserList() {
      const { data: res } = await this.$http.get('users', {
        params: this.queryInfo
      })
      console.log(res)
      if (res.meta.status !== 200) {
        return this.$message.err('获取用户列表失败')
      }
      this.userList = res.data.users
      this.total = res.data.total
    },
    // 监听 条数 改变的事件
    handleSizeChange(newSize) {
      // console.log(newSize)
      this.queryInfo.pagesize = newSize
      this.getUserList()
    },
    // 监听 页码 改变的事件
    handleCurrentChange(newPage) {
      // console.log(newPage)
      this.queryInfo.pagenum = newPage
      this.getUserList()
    },
    // 监听 switch 开关状态的改变
    async userStateChanged(userInfo) {
      console.log(userInfo)
      const { data: res } = await this.$http.put(
        `users/${userInfo.id}/state/${userInfo.mg_state}`
      )
      if (res.meta.status !== 200) {
        userInfo.mg_state = !userInfo.ms_state
        return this.$message.err(res.meta.msg)
      }
      this.$message.success(res.meta.msg)
    },
    // 监听添加用户对话框的关闭事件
    addDialogClosed() {
      // 对整个表单进行重置,将所有字段值重置为初始值并移除校验结果
      this.$refs.addFormRef.resetFields()
    },
    // 点击按钮添加新用户
    addUser() {
      this.$refs.addFormRef.validate(async valid => {
        // console.log(valid)
        if (!valid) {
          return
        }
        // 可以发起添加用户的网络请求
        const { data: res } = await this.$http.post('users', this.addForm)

        if (res.meta.status !== 201) {
          this.$message.error(res.meta.msg)
        }
        // 隐藏添加用户的对话框
        this.addDialogVisible = false
        // 刷新列表
        this.getUserList()
        // 提示信息
        this.$message.success(res.meta.msg)
      })
    },
    // 展示编辑用户的对话框
    async showEditDialog(id) {
      // console.log(id)
      const { data: res } = await this.$http.get('users/' + id)
      // console.log(res)

      if (res.meta.status !== 200) {
        return this.$message.error(res.meta.msg)
      }
      this.editDialogVisible = true
      this.editForm = res.data
    },
    // 监听修改用户对话框的关闭事件
    editDialogClosed() {
      this.$refs.editFormRef.resetFields()
    },
    // 修改用户信息并提交
    editUserInfo() {
      this.$refs.editFormRef.validate(async valid => {
        console.log(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(res.meta.msg)
        }
        // 隐藏添加用户的对话框
        this.editDialogVisible = false
        // 刷新列表
        this.getUserList()
        // 提示信息
        this.$message.success(res.meta.msg)
      })
    },
    // 根据id删除用户
    async removeUserById(id) {
      // console.log(id)

      // 弹框询问用户是否删除数据
      const confirmRes = await this.$confirm(
        '此操作将永久删除该用户, 是否继续?',
        '提示',
        {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }
      ).catch(err => err) // 捕获了用户的取消行为的异常,下面正常输出 cancel

      // 如果用户确认删除,则返回值为字符串  confirm
      // 如果用户取消删除,则返回值为字符串  cancel
      // console.log(res)

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

      const { data: res } = await this.$http.delete('users/' + id)

      if (res.meta.status !== 200) {
        return this.$message.error(res.meta.msg)
      }
      this.$message.success(res.meta.msg)
      this.getUserList()
    },
    // 展示分配角色的对话框
    async setRole(userInfo) {
      this.userInfo = userInfo

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

      this.rolesList = res.data

      this.setRoleDialogVisible = true
    },
    // 点击按钮,保存分配角色
    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(res.meta.msg)
      }

      this.$message.success(res.meta.msg)
      this.getUserList()
      this.setRoleDialogVisible = false
    },
    // 监听分配角色对话框的关闭事件
    setRoleDialogClosed() {
      this.selectedRoleId = ''
      this.userInfo = {}
    }
  }
}
</script>

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

这里我们使用了 ElementUI 组件 el-breadcrumbel-cardel-rowel-tableel-paginationel-dialog 等等

搜索栏

<el-row :gutter="20">
    <el-col :span="8">
        <!-- 数据双向绑定 -->
        <el-input
                  placeholder="请输入内容"
                  clearable
                  @clear="getUserList"
                  v-model="queryInfo.query"
                  >
            <el-button
                       @click="getUserList"
                       slot="append"
                       icon="el-icon-search"
                       ></el-button>
        </el-input>
    </el-col>
</el-row>
  1. el-input 中使用了数据双向绑定 v-model="queryInfo.query" ,当点击 el-button 调用 getUserList 方法时候,该 queryInfo 会作为参数传入接口中进行查询
  2. clearable 表示是否可清空,默认false,显式调用为 true
  3. clear 在点击由 clearable 属性生成的清空按钮时触发
  4. el-buttonslot="append" 是设置UI样式,紧追 el-input

用户列表

<!-- 用户列表区域
        :data 指定数据源
        border 边框
        stripe 斑马线
       -->
      <el-table :data="userList" border stripe>
        <!-- type="index" 索引列 -->
        <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="状态">
          <!-- 作用域插槽渲染 -->
          <template slot-scope="scope">
            <!-- scope.row 相当于当前行的所有数据 -->
            <el-switch
              v-model="scope.row.mg_state"
              @change="userStateChanged(scope.row)"
            >
            </el-switch>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="175px">
          <!-- 作用域插槽渲染 -->
          <!-- <template slot-scope="scope"> -->
          <template v-slot="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
              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>
  1. el-table元素中注入data对象数组后,在el-table-column中用prop属性来对应对象中的键名即可填入数据,用label属性来定义表格的列名。
  2. 通过 Scoped slot 可以获取到 row, column, $index 和 store(table 内部的状态管理)的数据

用户列表动态数据

<el-table-column label="状态">
          <!-- 作用域插槽渲染 -->
          <template slot-scope="scope">
            <!-- scope.row 相当于当前行的所有数据 -->
            <el-switch
              v-model="scope.row.mg_state"
              @change="userStateChanged(scope.row)"
            >
            </el-switch>
          </template>
......

这里使用新的 ElementUI 组件 Switch 开关

  1. 绑定v-model到一个Boolean类型的变量。
  2. @change switch 状态发生变化时的回调函数

列表分页

<!-- 分页区域
        handleSizeChange 监听 pageSize 改变时会触发
        handleCurrentChange 监听 currentPage 改变时会触发
        :current-page 当前页数
        :page-sizes 每页显示个数选择器的选项设置
        :page-size 每页显示条目个数
        layout 组件布局,子组件名用逗号分隔
        :total 总条目数
       -->
      <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>

这里使用新的 ElementUI 组件 Pagination 分页

  1. size-change pageSize 改变时会触发
  2. current-change currentPage 改变时会触发
  3. current-page 当前页数,支持 .sync 修饰符
  4. page-sizes 每页显示个数选择器的选项设置
  5. page-size 每页显示条目个数,支持 .sync 修饰符
  6. layout 组件布局,子组件名用逗号分隔
  7. total 总条目数
// 监听 条数 改变的事件
handleSizeChange(newSize) {
  // console.log(newSize)
  this.queryInfo.pagesize = newSize
  this.getUserList()
},
// 监听 页码 改变的事件
handleCurrentChange(newPage) {
  // console.log(newPage)
  this.queryInfo.pagenum = newPage
  this.getUserList()
}

添加用户

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

当点击添加用户按钮,会触发 addDialogVisible = true 事件,弹出Dialog对话框

<!-- 添加用户的对话框 -->
      <el-dialog
        title="添加用户"
        :visible.sync="addDialogVisible"
        width="50%"
        @close="addDialogClosed"
      >
        <!-- 内容主体区域
          :model 数据绑定对象
          :rules 数据验证规则
          ref 引用对象,可以拿到form实例
         -->
        <el-form
          :model="addForm"
          :rules="addFormRules"
          ref="addFormRef"
          label-width="70px"
        >
          <!-- prop 使用具体的校验规则 -->
          <el-form-item label="用户名" prop="username">
            <!-- v-model 数据双向绑定,绑定到 addForm 对象中  -->
            <el-input v-model="addForm.username"></el-input>
          </el-form-item>
          <el-form-item label="密码" prop="password">
            <el-input v-model="addForm.password" type="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

  1. :visible.sync 是否显示 Dialog,支持 .sync 修饰符,必须在 data() 中定义该 boolean 值
  2. @close 特定事件,Dialog 关闭的回调

el-form

  1. :model 数据绑定的对象,必须在 data() 中定义
  2. :rules 数据验证规则,必须在 data() 中定义
  3. ref 表单的实例,可以通过 this.$refs.xxRef 调用指定表单实例

el-form-item

  1. prop 属性,设置为需校验的字段名即可,必须在 data() 中定义
  2. v-model 数据双向绑定,必须在 data() 中定义

el-button

当点击确定按钮,会调用 @click="addUser" 事件

// 点击按钮添加新用户
addUser() {
    this.$refs.addFormRef.validate(async valid => {
        // console.log(valid)
        if (!valid) {
            return
        }
        // 可以发起添加用户的网络请求
        const { data: res } = await this.$http.post('users', this.addForm)

        if (res.meta.status !== 201) {
            this.$message.error(res.meta.msg)
        }
        // 隐藏添加用户的对话框
        this.addDialogVisible = false
        // 刷新列表
        this.getUserList()
        // 提示信息
        this.$message.success(res.meta.msg)
    })
}
  1. 使用 this.$refs.xxRef 调用表单实例的 validate 方法,该方法对整个表单进行校验的方法,参数为一个回调函数。该回调函数会在校验结束后被调用,并传入两个参数:是否校验成功和未通过校验的字段。若不传入回调函数,则会返回一个 promise,这里使用 ES6 语法解构,如果不通过直接返回。
  2. 当预验证通过后,可以发起添加用户的网络请求,此时的 this.addForm 就是对话框的内容
  3. 如果添加成功,隐藏对话框,刷新列表,提示信息即可

修改用户

<el-table-column label="操作" width="175px">
          <!-- 作用域插槽渲染 -->
          <template v-slot="scope">
            <!-- 修改用户 -->
            <el-button
              type="primary"
              icon="el-icon-edit"
              size="mini"
              @click="showEditDialog(scope.row.id)"
            ></el-button>
......

tips: 参考文档:插槽 参考文档:作用域插槽 在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slotslot-scope 这两个目前已被废弃但未被移除且仍在文档中的 attribute。新语法的由来可查阅这份 RFC。 在向具名插槽提供内容的时候,我们可以在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称 现在 <template> 元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有 v-slot<template> 中的内容都会被视为默认插槽的内容。 注意 v-slot 只能添加在 <template> (只有一种例外情况),这一点和已经废弃的 slot attribute 不同。

  1. 这里使用作用域插槽 v-slot="scope" 渲染,其中 scope 为自定义名称,当调用 scope.row 代表的是当前行的所有数据
  2. 当点击修改按钮,会调用 @click="showEditDialog(scope.row.id)" 事件,把当前行的id值 scope.row.id 作为参数传入
  3. 使用 axios 请求API this.$http.get('users/' + id) 获取当前该id的用户,并设置 this.editForm = res.data 为该用户的信息

删除用户

<template v-slot="scope">
            <!-- 删除 -->
            <el-button
              type="danger"
              icon="el-icon-delete"
              size="mini"
              @click="removeUserById(scope.row.id)"
            ></el-button>
......
// 根据id删除用户
    async removeUserById(id) {
      // console.log(id)

      // 弹框询问用户是否删除数据
      const confirmRes = await this.$confirm(
        '此操作将永久删除该用户, 是否继续?',
        '提示',
        {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }
      ).catch(err => err) // 捕获了用户的取消行为的异常,下面正常输出 cancel

      // 如果用户确认删除,则返回值为字符串  confirm
      // 如果用户取消删除,则返回值为字符串  cancel
      // console.log(res)

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

      const { data: res } = await this.$http.delete('users/' + id)

      if (res.meta.status !== 200) {
        return this.$message.error(res.meta.msg)
      }
      this.$message.success(res.meta.msg)
      this.getUserList()
    }

这里引用了一个新的 ElementUI 组件 MessageBox 弹框, 需要导入并挂载在全局Vue中

// MessageBox 要挂载在全局 Vue 的一个原型(prototype)上
Vue.prototype.$confirm = MessageBox.confirm
  1. 使用 this.$confirm 调用弹框,当点击取消会抛出异常,捕获后返回 cancel 字符串,当点击确定会返回 confirm 字符串,通过这两个字符串判断用户的实际操作
  2. 通过 axios 调用 API this.$http.delete('users/' + id) ,会返回 Promise 对象,可以使用 ES6 语法解构

分配角色

<!-- 提示信息 -->
<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>

这里使用了新的 ElementUI 组件 Tooltip 文字提示

  1. effect 默认提供的主题,可选dark/light
  2. placement Tooltip 的出现位置
  3. enterable 鼠标是否可进入到 tooltip 中
<!-- 分配角色的对话框 -->
      <el-dialog
        title="分配角色"
        :visible.sync="setRoleDialogVisible"
        width="50%"
        @close="setRoleDialogClosed"
      >
        <div>
          <el-form :model="userInfo" label-widtd="80px">
            <el-form-item label="当前的用户">
              <el-input v-model="userInfo.username" disabled></el-input>
            </el-form-item>
            <el-form-item label="当前的角色">
              <el-input v-model="userInfo.role_name" disabled></el-input>
            </el-form-item>
            <el-form-item label="分配新角色">
              <el-select
                v-model="selectedRoleId"
                placeholder="请选择"
                style="width:100%"
              >
                <el-option
                  v-for="item in rolesList"
                  :key="item.id"
                  :label="item.roleName"
                  :value="item.id"
                >
                </el-option>
              </el-select>
            </el-form-item>
          </el-form>
        </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>

这里使用了新的 ElementUI 组件 Select 选择器

  1. el-select 中的 v-model的值为当前被选中的el-option的 value 属性值,该值要在 data() 中定义
  2. el-option 使用 v-for 遍历角色集合,其中value 是选项的值, label 选项的标签,若不设置则默认与 value 相同