从vue到elementUI项目(五)

用户管理理页及详解权限管理

用户管理理页介绍及页面实现思路讲解

管理页功能

  • 新增用户
  • 搜索用户
  • 更新用户
  • 删除用户
  • 分页展示用户列表

先给出用户管理响应的mock接口数据,在mock文件夹下新建一个user.js,内容如下

import Mock from 'mockjs'

// get请求从config.url获取参数,post从config.body中获取参数
function param2Obj(url) {
    const search = url.split('?')[1]
    if (!search) {
        return {}
    }
    return JSON.parse(
        '{"' +
        decodeURIComponent(search)
        .replace(/"/g, '\\"')
        .replace(/&/g, '","')
        .replace(/=/g, '":"') +
        '"}'
    )
}

let List = []
const count = 200

for (let i = 0; i < count; i++) {
    List.push(
        Mock.mock({
            id: Mock.Random.guid(),
            name: Mock.Random.cname(),
            addr: Mock.mock('@county(true)'),
            'age|18-60': 1,
            birth: Mock.Random.date(),
            sex: Mock.Random.integer(0, 1)
        })
    )
}

export default {
    /**
     * 获取列表
     * 要带参数 name, page, limt; name可以不填, page,limit有默认值。
     * @param name, page, limit
     * @return {{code: number, count: number, data: *[]}}
     */
    getUserList: config => {
        const {
            name,
            page = 1,
            limit = 20
        } = param2Obj(config.url)
        console.log('name:' + name, 'page:' + page, '分页大小limit:' + limit)
        const mockList = List.filter(user => {
            if (name && user.name.indexOf(name) === -1 && user.addr.indexOf(name) === -1) return false
            return true
        })
        const pageList = mockList.filter((item, index) => index < limit * page && index >= limit * (page - 1))
        return {
            code: 20000,
            count: mockList.length,
            list: pageList
        }
    },
    /**
     * 增加用户
     * @param name, addr, age, birth, sex
     * @return {{code: number, data: {message: string}}}
     */
    createUser: config => {
        const {
            name,
            addr,
            age,
            birth,
            sex
        } = JSON.parse(config.body)
        console.log(JSON.parse(config.body))
        List.unshift({
            id: Mock.Random.guid(),
            name: name,
            addr: addr,
            age: age,
            birth: birth,
            sex: sex
        })
        return {
            code: 20000,
            data: {
                message: '添加成功'
            }
        }
    },
    /**
     * 删除用户
     * @param id
     * @return {*}
     */
    deleteUser: config => {
        const {
            id
        } = param2Obj(config.url)
        if (!id) {
            return {
                code: -999,
                message: '参数不正确'
            }
        } else {
            List = List.filter(u => u.id !== id)
            return {
                code: 20000,
                message: '删除成功'
            }
        }
    },
    /**
     * 批量删除
     * @param config
     * @return {{code: number, data: {message: string}}}
     */
    batchremove: config => {
        let {
            ids
        } = param2Obj(config.url)
        ids = ids.split(',')
        List = List.filter(u => !ids.includes(u.id))
        return {
            code: 20000,
            data: {
                message: '批量删除成功'
            }
        }
    },
    /**
     * 修改用户
     * @param id, name, addr, age, birth, sex
     * @return {{code: number, data: {message: string}}}
     */
    updateUser: config => {
        const {
            id,
            name,
            addr,
            age,
            birth,
            sex
        } = JSON.parse(config.body)
        const sex_num = parseInt(sex)
        List.some(u => {
            if (u.id === id) {
                u.name = name
                u.addr = addr
                u.age = age
                u.birth = birth
                u.sex = sex_num
                return true
            }
        })
        return {
            code: 20000,
            data: {
                message: '编辑成功'
            }
        }
    }
}

在mock下的index.js就要去引入这些返回

import Mock from 'mockjs'
import homeApi from './home.js'

// 设置200-2000毫秒延时请求数据
Mock.setup({
  timeout: '200-2000',
})

// 首页相关
// 拦截的是 /home/getData
Mock.mock(/\/home\/getData/, 'get', homeApi.getStatisticalData)

// 用户相关
Mock.mock(/\/user\/getUser/, 'get', userApi.getUserList)
Mock.mock(/\/user\/del/, 'get', userApi.deleteUser)
Mock.mock(/\/user\/batchremove/, 'get', userApi.batchremove)
Mock.mock(/\/user\/add/, 'post', userApi.createUser)
Mock.mock(/\/user\/edit/, 'post', userApi.updateUser)
Mock.mock(/\/home\/getData/, 'get', homeApi.getStatisticalData)

考虑到用户管理需要两个组件来完成,一个是表头部分,另一个是表单内容部分,所以新建两个组件,分别为CommonForm.vue(提交新建和搜索),内容如下

<template>
  <div>CommonForm</div>
</template>

<script>
</script>

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

CommonTable.vue(显示用户信息),内容如下

<template>
  <div>CommonTable</div>
</template>

<script>
</script>

新建一个UserManage.vue页面的样式,在scss文件夹下新建common.scss,内容如下

.manage {
    background-color: yellow;
}

在UserManage.vue中引入这两个组件,并新建一个scss来作为他的样式

<template>
  <div class="manage">
    <CommonForm></CommonForm>
    <CommonTable></CommonTable>
  </div>
</template>

<script>
import CommonForm from '../../components/CommonForm'
import CommonTable from '../../components/CommonTable'
export default {
  components: {
    CommonForm,
    CommonTable,
  },
}
</script>

<style lang="scss" scoped>
@import "@/assets/scss/common";
</style>

目前看到占坑的效果

更完善的表单组件封装及思路讲解

分析表单组成

  • 输⼊框
  • 下拉框
  • 日期选择器
  • 单选列表
  • 多选列表
  • 。。。

考虑基本参数

  • 绑定的表单字段
  • 表单描述文本

插槽拓展组件

  • 按钮组

在CommonForm.vue中完成我们表单的初步封装,在页面那种加入了三种控件,分别为input、select和switch、date-picker,需要传入对应的formLabel才会显示出来,比如我们要显示swith与input

  data() {
    return {
      searchFrom: {
        keyword: ''
      },
      formLabel: [
        {
          model: 'keyword',
          label: '' // 组件描述的内容
        },
        {
          type: 'switch', // 用在项目中,是去掉这个的
          label: '' // 组件描述的内容
        }
      ]
    }
  }

现在CommonForm.vue的代码如下,

<template>
  <el-form :inline="inline" :model="form" ref="form" label-width="100px">
    <el-form-item v-for="item in formLabel" :key="item.model" :label="item.label">
      <el-input v-model="form[item.model]" :placeholder="'请输入' + item.label" v-if="!item.type"></el-input>
      <el-select v-model="form[item.model]" placeholder="请选择" v-if="item.type === 'select'">
        <el-option
          v-for="item in item.opts"
          :key="item.value"
          :label="item.label"
          :value="item.value"
        ></el-option>
      </el-select>
      <el-switch v-model="form[item.model]" v-if="item.type === 'switch'"></el-switch>
      <el-date-picker
        v-model="form[item.model]"
        type="date"
        placeholder="选择日期"
        v-if="item.type === 'date'"
        value-format="yyyy-MM-dd"
      ></el-date-picker>
    </el-form-item>
    <el-form-item>
      <slot></slot> // 按钮的插槽,调用时候传入
    </el-form-item>
  </el-form>
</template>

<script>
export default {
  props: {
    inline: Boolean,
    form: Object,
    formLabel: Array
  }
}
</script>

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

我们到UserManage.vue中去正式使用他

<template>
  <div class="manage">
    <div class="manage-header">
      <el-button type="primary">+ 新增</el-button>
      <CommonForm inline :formLabel="formLabel" :form="searchFrom">
        <el-button type="primary">搜索</el-button>
      </CommonForm>
    </div>
    <CommonTable></CommonTable>
  </div>
</template>

<script>
import CommonForm from '../../components/CommonForm'
import CommonTable from '../../components/CommonTable'
export default {
  components: {
    CommonForm,
    CommonTable,
  },
  data() {
    return {
      searchFrom: {
        keyword: ''
      },
      formLabel: [
        {
          model: 'keyword',
          label: '' // 组件描述的内容
        },
      ]
    }
  }
}
</script>

<style lang="scss" scoped>
@import "@/assets/scss/common";
</style>

现在使用额度common.scss样式,只是为了更加符合表单的效果

.manage {
    height: 90%;
    padding-bottom: 20px;
    overflow: hidden;

    &-header {
        display: flex;
        justify-content: space-between;
        align-items: flex-start;
    }
}

目前的效果如下

通用表格组件封装思路以及完成表格组件的封装

表格基本参数分析

  • data: 传⼊的数据列表
  • prop: 传入的数据字段
  • label: 列名

表格可选参数分析

  • width:列宽
  • type:类型

表格扩展

  • 分页参数
    • total: 数据条数总计
    • page: 当前页数
  • 加载状态
    • loading:布尔值

修改CommonTable.vue控件,将需要的表格属性渲染的table-column定义出来

<template>
  <div class="common-table">
    <el-table :data="tableData">
      <el-table-column label="序号" width="85"></el-table-column>
      <el-table-column
        show-overflow-tooltip
        v-for="item in tableLabel"
        :key="item.prop"
        :label="item.label"
      >
        <template slot-scope="scope">
          <span style="margin-left: 10px">{{ scope.row[item.prop] }}</span>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script>
export default {
  props: {
    tableData: Array,
    tableLabel: Array,
    config: Object // 接收分页等配置
  },
}
</script>

在UserManage.vue页面中调用时,需要传入tableData和tableLabel还有config

<template>
  <div class="manage">
    <div class="manage-header">
      <el-button type="primary">+ 新增</el-button>
      <CommonForm inline :formLabel="formLabel" :form="searchFrom">
        <el-button type="primary">搜索</el-button>
      </CommonForm>
    </div>
    <common-table :tableData="tableData" :tableLabel="tableLabel" :config="config"></common-table>
  </div>
</template>
...

tableLabel在data数据区域中直接定义,tableData先定义出来,在methods中去通过访问接口进行请求。config为存放的一些表格参数,预先定义,也是通过接口数据返回后进行设定

<template>
...
</template>

<script>
...
  data() {
    return {
      tableData: [],
      tableLabel: [
        {
          prop: 'name',
          label: '姓名'
        },
        {
          prop: 'age',
          label: '年龄'
        },
        {
          prop: 'sexLabel',
          label: '性别'
        },
        {
          prop: 'birth',
          label: '出生日期',
          width: 200
        },
        {
          prop: 'addr',
          label: '地址',
          width: 320
        }
      ],
      config: { // 分页等配置参数
        page: 1,
        total: 30,
        loading: false
      },
      searchFrom: {
        keyword: ''
      },
      formLabel: [
        {
          model: 'keyword',
          label: '' // 组件描述的内容
        },
      ]
    }
  },
  methods: {
    getList() {
      this.config.loading = true
      this.$http
        .get('/api/user/getUser', {
          params: {
            page: this.config.page,
            name
          }
        })
        .then(res => {
          this.tableData = res.data.list.map(item => { // 处理一下sex的中文显示
            item.sexLabel = item.sex === 0 ? '女' : '男'
            return item
          })
          this.config.total = res.data.count
          this.config.loading = false
        })
    },
  },
  created() {
    this.getList()
  }
}
</script>
...

目前的效果如下
> [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TAvZsIZI-1599482162777)(A6C0F965436341E4A5FF05677488EAD6)]

加入表格的序号,序号是通过分页加上当前index得到的

<template>
  <div class="common-table">
    <el-table :data="tableData" height="90%" stripe>
      <el-table-column label="序号" width="85">
        <template slot-scope="scope">
          <span style="margin-left: 10px">{{ (config.page - 1) * 20 + scope.$index + 1 }}</span>
        </template>
      </el-table-column>
      ...
</template>

表格中的最后一列是操作按钮,他们的click事件在后面补充

...
<el-table :data="tableData" height="90%" stripe>
      <el-table-column label="序号" width="85">
  <el-table-column label="操作" min-width="80">
    <template>
      <el-button size="mini">编辑</el-button>
      <el-button size="mini" type="danger">删除</el-button>
    </template>
  </el-table-column>
</el-table>
...

下来还有一个分页控件

<template>
  ...
      <el-table-column label="操作" min-width="60">
        <template>
          <el-button size="mini">编辑</el-button>
          <el-button size="mini" type="danger">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    <el-pagination
      class="pager"
      layout="prev, pager, next"
      :total="config.total"
      :current-page.sync="config.page"
      :page-size="20"
    ></el-pagination>
  </div>
</template>

现在的效果为

调整一下common-table和pager控件的样式

<style lang="scss" scoped>
.common-table {
  height: calc(100% - 62px); // 62px是上面的搜索
  background-color: #fff;
  position: relative;
  .pager {
    position: absolute;
    bottom: 0;
    right: 20px;
  }
}
</style>

效果就出来了
> [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jtibp5WU-1599482162780)(449848238EAA4BF184502C30C8009EA2)]

用户管理页面功能实现

  • 表格加载状态
  • 分页功能
  • 编辑功能
  • 删除功能

实现表格加载状态

表格加载状态只需要在el-table中绑定属性v-loading即可,config.loading是我们后台传过来的

<template>
  <div class="common-table">
    <el-table :data="tableData" height="90%" stripe v-loading="config.loading">
...

效果可以看到

实现分页功能

在CommonTable.vue组件中,加入分页的监听事件current-change,让其调用父组件的changePage消息

<template>
...
    <el-pagination
      class="pager"
      layout="prev, pager, next"
      :total="config.total"
      :current-page.sync="config.page"
      :page-size="20"
      @current-change="changePage"
    ></el-pagination>
  </div>
</template>

<script>
export default {
  props: {
    tableData: Array,
    tableLabel: Array,
    config: Object // 接收分页等配置
  },
  methods: {
    changePage(page) {
      this.$emit('changePage', page) // 传给父组件的changePage
    }
  }
}
</script>
...

父组件接收这个changePage,响应他的是getList,config.page是一个双向绑定的变量,当子组件中改变值以后,父组件也会改变,所以请求getList接口的this.config.page此时也改变了

...
    <common-table
      :tableData="tableData"
      :tableLabel="tableLabel"
      :config="config"
      @changePage="getList"
    ></common-table>
  </div>
</template>
...
export default {
...
  methods: {
    getList() {
      this.config.loading = true
      this.$http
        .get('/api/user/getUser', {
          params: {
            page: this.config.page,
            name
          }
        })
        .then(res => {
          this.tableData = res.data.list.map(item => { // 处理一下sex的中文显示
            item.sexLabel = item.sex === 0 ? '女' : '男'
            return item
          })
          this.config.total = res.data.count
          this.config.loading = false
        })
    },
  },
  created() {
    this.getList()
  }
}
</script>
...

现在也是第二页的显示
> [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KIbkQ8zu-1599482162783)(9DED9D7A459246369389C34987200C37)]

实现编辑功能

编辑功能比较好实现,编辑按钮是在CommonTable.vue组件中的,只需要在,只需要对两个按钮进行事件绑定,然后通过消息传递到UserManage.vue中,顺带把删除的消息传递一块写了

<template>
...
      <el-table-column label="操作" min-width="80">
        <template slot-scope="scope">
          <el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
          <el-button size="mini" type="danger" @click="handleDelete(scope.row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
...
</template>

<script>
export default {
...
  methods: {
    changePage(page) {
      this.$emit('changePage', page) // 传给父组件的changePage
    },
    handleEdit(row) {
      console.log("handleEdit")
      this.$emit("edit", row)
    },
    handleDelete(row) {
      console.log("handleDelete")
      this.$emit("delete", row)
    },
  }
}
</script>
...

在页面UserManage.vue中,去响应传过来的消息,在editUser与delUser中先建个坑

<template>
...
    <common-table
      :tableData="tableData"
      :tableLabel="tableLabel"
      :config="config"
      @changePage="getList"
      @edit="editUser"
      @delete="delUser"
    ></common-table>
  </div>
</template>
...
    editUser(row) {
      console.log("editUser")
      console.log(row)
    },
    delUser(row) {
      console.log("delUser")
    },
...

实现新增功能

新增功能需要做的只是新建一个form,填入对应的内容,这个form可以重复利用之前的组件CommonForm.vue,为此我们需要在data部分新增需要的内容,大概也就这几个

  • operateType:我们需要一个类型,在后面提交数据的时候还需要区分是编辑还是新增
  • isShow:表单是否显示出来
  • operateForm:定义表单需要提交的字段(与接口中返回的内容需要一致)
  • operateFormLabel:表示在CommonForm.vue组件上面我们需要出现的控件

下面是这些data数据的数据部分

  data() {
    return {
...
      operateType: 'add',
      isShow: false,
      // 定义表单需要编辑的字段
      operateForm: {
        name: '',
        addr: '',
        age: '',
        birth: '',
        sex: ''
      },
      operateFormLabel: [
        {
          model: 'name',
          label: '姓名'
        },
        {
          model: 'age',
          label: '年龄'
        },
        {
          model: 'sex',
          label: '性别',
          type: 'select',
          opts:
            [
              {
                label: '男',
                value: 1
              },
              {
                label: '女',
                value: 0
              }
            ]
        },
        {
          model: 'birth',
          label: '出生日期',
          type: 'date'
        },
        {
          model: 'addr',
          label: '地址'
        }
      ],
...
    }
  },

现在来加入表单的部分,因为想把新增和编辑是在同一个表单中进行,所以就只用一个el-dialog来显示,所以UserManage.vue现在的网页部分代码为

<template>
  <div class="manage">
    <el-dialog :title="operateType === 'add' ? '新增用户' : '更新用户'" :visible.sync="isShow">
      <common-form :formLabel="operateFormLabel" :form="operateForm" ref="form"></common-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="isShow = false">取 消</el-button>
        <el-button type="primary" @click="confirm">确 定</el-button>
      </div>
    </el-dialog>
    <div class="manage-header">
      <el-button type="primary" @click="addUser">+ 新增</el-button>
      <CommonForm inline :formLabel="formLabel" :form="searchFrom">
        <el-button type="primary">搜索</el-button>
      </CommonForm>
    </div>
    <common-table
      :tableData="tableData"
      :tableLabel="tableLabel"
      :config="config"
      @changePage="getList"
      @edit="editUser"
      @delete="delUser"
    ></common-table>
  </div>
</template>

接着来改editUser部分代码、增加addUser代码,以及实现confirm的提交,先用输出占坑

...
    addUser() {
      // this.operateType = 'edit'
      // this.isShow = true
      // this.operateForm = row
      // console.log("addUser")
      console.log("addUser")
      this.operateType = 'add'
      this.isShow = true
    },
    editUser(row) {
      // console.log("editUser")
      // console.log(row)
      // 显示编辑框
      this.operateType = 'edit'
      this.isShow = true
      this.operateForm = row
    },
...
    confirm() {
      // console.log(row)
      if (this.operateType === 'edit') {
        console.log("edit", this.operateForm)
      }
      else {
        console.log("add", this.operateForm)
      }
      // 无论是新增还是编辑都需要重新刷新表格
      this.getList()
    }
  },
...

现在所能看到的新增点击事件如下
> [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HaTFultD-1599482162784)(70882FDB5A1E49889A6F61EC57122D4F)]

编辑后可以看到的内容
> [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Th1GCkib-1599482162786)(6E9A58E733BA4CCD8725504C99E2393A)]

实现删除功能

之前给删除留了坑,我们想要在删除的时候,进行一下消息提示,也就是使用eleme组件的"Message Box弹框"

delUser(row) {
      // console.log("editUser")
      console.log(row)
      this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.$message({
          type: 'success',
          message: '删除成功!'
        });
        // 提交给后台进行删除
        console.log("提交给后台进行删除")
        // 删除后进行刷新
        this.getList()
      }).catch(() => {
        this.$message({
          type: 'info',
          message: '已取消删除'
        });
      });
    },

企业开发之权限管理思路路讲解

什么是权限管理理

  • 根据不同用户,返回不同菜单
  • 严格控制用户权限
  • 实现思路
    • 动态路由
    • 后端返回的数据格式要求
    • 触发时机
      • 登陆成功的时候触发操作
      • Cookie中存在对应数据,首次进入页面时

为了实现权限管理,需要先加入一个登陆页面,只有当登陆成功后,才能进行后续的操作

修改之前views/Login下的Login.vue代码

<template>
  <div style="padding: 20px">
    <el-form :model="form" label-width="120">
      <el-form-item label="用户名">
        <el-input v-model="form.username"></el-input>
      </el-form-item>
      <el-form-item label="密码">
        <el-input v-model="form.password" type="password"></el-input>
      </el-form-item>
      <el-form-item align="center">
        <el-button type="primary" @click="login">登录</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      form: {
        username: '',
        password: ''
      }
    }
  },
  methods: {
    login() {
      this.$http.post('api/permission/getMenu', this.form).then(res => {
        res = res.data
        if (res.code === 20000) {
          console.log("20000")
        } else {
          console.log("other")
        }
      })
    }
  }
}
</script>

<style lang="scss" scoped>
.el-form {
  width: 50%;
  margin: auto;
  padding: 45px;
  height: 450px;
  background-color: #fff;
}
</style>

在mock中新建一个permission.js,响应这个登陆请求

import Mock from 'mockjs'
export default {
  getMenu: config => {
    const { username, password } = JSON.parse(config.body)
    console.log(JSON.parse(config.body))
    // 先判断用户是否存在
    if (username === 'admin' || username === 'wp') {
      // 判断账号和密码是否对应
      if (username === 'admin' && password === '123456') {
        return {
          code: 20000,
          data: {
            menu: [
              {
                path: '/',
                name: 'home',
                label: '首页',
                icon: 's-home',
                url: 'Home/Home'
              },
              {
                path: '/video',
                name: 'video',
                label: '视频管理页',
                icon: 'video-play',
                url: 'VideoManage/VideoManage'
              },
              {
                path: '/user',
                name: 'user',
                label: '用户管理页',
                icon: 'user',
                url: 'UserManage/UserManage'
              },
              {
                label: '其他',
                icon: 'location',
                children: [
                  {
                    path: '/page1',
                    name: 'page1',
                    label: '页面1',
                    icon: 'setting',
                    url: 'Other/PageOne'
                  },
                  {
                    path: '/page2',
                    name: 'page2',
                    label: '页面2',
                    icon: 'setting',
                    url: 'Other/PageTwo'
                  }
                ]
              }
            ],
            message: '获取成功'
          }
        }
      } else if (username === 'wp' && password === '123456') {
        return {
          code: 20000,
          data: {
            menu: [
              {
                path: '/',
                name: 'home',
                label: '首页',
                icon: 's-home',
                url: 'Home/Home'
              },
              {
                path: '/video',
                name: 'video',
                label: '视频管理页',
                icon: 'video-play',
                url: 'VideoManage/VideoManage'
              }
            ],
            message: '获取成功'
          }
        }
      } else {
        return {
          code: -999,
          data: {
            message: '密码错误'
          }
        }
      }
    } else {
      return {
        code: -999,
        data: {
          message: '用户不存在'
        }
      }
    }
  }
}

在router/index.js引入这个permission.js

import Mock from 'mockjs'
import homeApi from './home.js'
import userApi from './user.js'
import permissionApi from './permission.js'
...
// 权限相关
Mock.mock(/\/permission\/getMenu/, 'post', permissionApi.getMenu)

在路由中定义一个/login,找到router/index.js,加入如下新增内容

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

Vue.use(VueRouter)

const routes = [{
    path: '/',
    component: () => import('@/views/Main.vue'),
    children: [{
     ...
    ]
  },
  {
    path: '/login',
    name: 'login',
    component: () => import('@/views/Login/Login')
  }
]
...

测试一下登陆,访问链接http://localhost:3333/#/login,可以看到如下

输入admin、123456后提交,可以看到后台输出

权限管理之动态返回菜单的实现

更改路由表

  • 根据是否需要权限的路由分类

vuex里补充mutation

  • 保存菜单
  • 动态添加菜单

生成路由的时机

  • 登录时
  • 刷新时

点击退出时,清除cookie后,刷新下页面

先要安装js-cookie

yarn add js-cookie -S

之前的左侧菜单项都是在CommonAside.vue中的data写死的,现在想让在登录后自动维护一下动态菜单,之前在tab.js中定义的menu这个时候就能用起来了

先来屡一下,我们一共需要这几个操作

  • clearMenu:在退出登录的时候情况菜单
  • setMenu:当接口回复菜单的json数据时,保存到cookie中
  • addMenu:从cookie中获取菜单的数据,通过router.addRoutes添加到路由中
...
  state: {
    isCollapse: false,
    menu: [],
    currentMenu: {},
 ...
    clearMenu(state) {
      state.menu = []
      Cookie.remove('menu')
    },
    setMenu(state, val) {
      state.menu = val
      Cookie.set('menu', JSON.stringify(val))
    },
    addMenu(state, router) {
      // 从cookie中取出来menu数据,将这个数据添加到router中
      if (!Cookie.get('menu')) {
        return
      }
      let menu = JSON.parse(Cookie.get('menu'))
      state.menu = menu
      let currentMenu = [
        {
          path: '/',
          component: () => import(`@/views/Main`),
          children: [],
        },
      ]
      menu.forEach((item) => {
        if (item.children) {
          item.children = item.children.map((item) => {
            item.component = () => import(`@/views/${item.url}`)
            return item
          })
          currentMenu[0].children.push(...item.children)
        } else {
          item.component = () => import(`@/views/${item.url}`)
          currentMenu[0].children.push(item)
        }
      })
      router.addRoutes(currentMenu) // 动态添加到路由中
    },
...

在CommonAside.vue中,我们需要用这个store中的menu替换下之前使用的asideMenu

...
<script>
export default {
  computed: {
    noChildren() {
      // return this.asideMenu.filter((item) => !item.children)
      return this.menu.filter((item) => !item.children)
    },
    hasChildren() {
      // return this.asideMenu.filter((item) => item.children)
      return this.menu.filter((item) => item.children)
    },
    isCollapse() {
      return this.$store.state.tab.isCollapse
    },
    menu() {
      return this.$store.state.tab.menu
    },
  },
...

在views/Login/Login.vue登录成功时,需要完成菜单的操作

...
  methods: {
    login() {
      this.$http.post('api/permission/getMenu', this.form).then((res) => {
        res = res.data
        if (res.code === 20000) {
          console.log('20000')
          // 接收菜单
          this.$store.commit('clearMenu')
          this.$store.commit('setMenu', res.data.menu)
          this.$store.commit('addMenu', this.$router)
          this.$router.push({ name: 'home' })
        } else {
          console.log('other')
...

接着我们再来做测试,首先用管理员admin/123456登入系统

再用wp/123456登入

可以看到菜单是不一样的

权限管理之路由守卫判断用户登录状态

permission.js中返回token可以通过mock来实现

...
if (username === 'admin' && password === '123456') {
    return {
        code: 20000,
        data: {
            menu: [{
                    path: '/',
                    name: 'home',
                    label: '首页',
                    icon: 's-home',
                    url: 'Home/Home'
                },
...
            token: Mock.Random.guid(),
            message: '获取成功'
        }
    }
} else if (username === 'wp' && password === '123456') {
...
    token: Mock.Random.guid(),
    message: '获取成功'
...

在store中新建一个user.js去保存这个token以及相关的处理

import Cookie from 'js-cookie'

export default {
    state: {
        token: ''
    },
    mutations: {
        setToken(state, val) {
            state.token = val
            Cookie.set('token', val)
        },
        clearToken(state) {
            state.token = ''
            Cookie.remove('token')
        },
        getToken(state) {
            state.token = Cookie.get('token')
        }
    },
    actions: {},
}

要使用还得在store/index.js去注册他

import Vue from 'vue'
import Vuex from 'vuex'
import tab from './tab'
import user from './user'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    tab,
    user
  }
})

那么我们需要完成以后这些事,登录时保存token

  • 保存在vuex里
  • 保存在cookie里

在Login/Login.vue中登录后,设置cookie

...
  methods: {
    login() {
      this.$http.post('api/permission/getMenu', this.form).then((res) => {
        res = res.data
        if (res.code === 20000) {
          console.log('20000')
          // 接收菜单
          this.$store.commit('clearMenu')
          this.$store.commit('setMenu', res.data.menu)
          this.$store.commit('setToken', res.data.token)
          this.$store.commit('addMenu', this.$router)
          this.$router.push({ name: 'home' })
        } else {
          console.log('other')
...

router/index.js,现在的内容如下

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

Vue.use(VueRouter)

const routes = [
  {
    path: '/login',
    name: 'login',
    component: () => import('@/views/Login/Login')
  }
]

const router = new VueRouter({
  routes
})

export default router

要完成在所以的路由中都去判断是否有token,要在路由中加一个守卫,找到main.js,进入路由守卫,并且在路由开始时创建菜单

...
router.beforeEach((to, from, next) => {
  // 防止刷新后vuex里丢失token
  store.commit('getToken')
  // 防止刷新后vuex里丢失标签列表tagList
  store.commit('getMenu')
  let token = store.state.user.token
  // 过滤登录页,防止死循环
  if (!token && to.name !== 'login') {
    next({
      name: 'login'
    })
  } else {
    next()
  }
})

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

我们来完成退出删除token事件,这样才能测试路由守卫是否设置成功,找到components/CommonHeader.vue,加入退出按钮的事件,在退出按钮清除cookie的时候,我们希望能对页面进行刷新,所以还使用reload

<template>
...
        <el-dropdown-menu slot="dropdown">
          <el-dropdown-item>个人中心</el-dropdown-item>
          <el-dropdown-item @click.native="logOut">退出</el-dropdown-item>
        </el-dropdown-menu>
      </el-dropdown>
    </div>
  </header>
</template>
<script>
import { mapState } from 'vuex'
export default {
...
  methods: {
    collapseMenu() {
      this.$store.commit('collapseMenu')
    },
    logOut() {
      this.$store.commit('clearToken')
      this.$store.commit('clearMenu')
      location.reload()
    },
  },
}
...

来测试一下使用退出,已经自动跳转到login页面了

最后再来补充一些未完成的

为这些接口加入模拟api

之前都只是进行输出占坑,既然在mock/user.js中已经定义了很多的api,就用起来。这几个接口在views/UserManage/UserManage.vue中

...
    addUser() {
      // this.operateType = 'edit'
      // this.isShow = true
      // this.operateForm = row
      // console.log("addUser")
      console.log('addUser')
      this.operateType = 'add'
      this.isShow = true
    },
    editUser(row) {
      // console.log("editUser")
      // console.log(row)
      // 显示编辑框
      this.operateType = 'edit'
      this.isShow = true
      this.operateForm = row
    },
    delUser(row) {
      // console.log("editUser")
      console.log(row)
      this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
      })
        .then(() => {
          let id = row.id
          this.$http
            .get('/api/user/del', {
              params: {
                id,
              },
            })
            .then((res) => {
              console.log(res.data)
              this.$message({
                type: 'success',
                message: '删除成功!',
              })
              this.getList()
            })
          // this.$message({
          //   type: 'success',
          //   message: '删除成功!',
          // })
          // 提交给后台进行删除
          console.log('提交给后台进行删除')
          // 删除后进行刷新
          this.getList()
        })
        .catch(() => {
          this.$message({
            type: 'info',
            message: '已取消删除',
          })
        })
    },
    confirm() {
      if (this.operateType === 'edit') {
        this.$http.post('/api/user/edit', this.operateForm).then((res) => {
          console.log(res.data)
          this.isShow = false
          this.getList()
        })
      } else {
        this.$http.post('/api/user/add', this.operateForm).then((res) => {
          console.log(res.data)
          this.isShow = false
          this.getList()
        })
      }
      // 无论是新增还是编辑都需要重新刷新表格
      this.getList()
    },
  },
  created() {
    this.getList()
  },
}
</script>
...

在浏览器中做一下测试http://localhost:3333/#/user

保存taglist到cookie中

如果不把taglist保存到cookie中,每当刷新页面,这些信息就会丢失,总是vuex中的数据感觉总是那么不靠谱,因为之前在main.js中已经在路由守卫中使用了getMenu,但之前没有保存

...
router.beforeEach((to, from, next) => {
  // 防止刷新后vuex里丢失token
  store.commit('getToken')
  // 防止刷新后vuex里丢失标签列表tagList
  store.commit('getMenu')
  let token = store.state.user.token
  // 过滤登录页,防止死循环
...

所以我们只需要在store/tab.js中selectMenu选择是保存到Cookie中

...
  mutations: {
    selectMenu(state, val) {
      if (val.name !== 'home') {
        state.currentMenu = val
        let result = state.tabsList.findIndex((item) => item.name === val.name)
        result === -1 ? state.tabsList.push(val) : ''
        Cookie.set('tagList', JSON.stringify(state.tabsList))
      } else {
        state.currentMenu = null
      }
    },
...
    getMenu(state) {
      if (Cookie.get('tagList')) {
        let tagList = JSON.parse(Cookie.get('tagList'))
        state.tabsList = tagList
      }
    },
  },
...
}

这时再刷新就不会丢失tag了

完成搜索功能

最后还差一下UserManage.vue页面中的搜索功能,我们之前已经为搜索文本框绑定了keyword属性,只需要利用这个值,传入到接口getList即可

<template>
  <div class="manage">
    <el-dialog
      :title="operateType === 'add' ? '新增用户' : '更新用户'"
      :visible.sync="isShow"
    >
      <common-form
        :formLabel="operateFormLabel"
        :form="operateForm"
        ref="form"
      ></common-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="isShow = false">取 消</el-button>
        <el-button type="primary" @click="confirm">确 定</el-button>
      </div>
...
  methods: {
    getList(name = '') {
      console.log(name)
      this.config.loading = true
      // 搜索时,页码需要设置为1,才能正确返回数据,因为数据是从第一页开始返回的
      name ? (this.config.page = 1) : ''
      this.$http
        .get('/api/user/getUser', {
          params: {
            page: this.config.page,
            name,
          },
        })
        .then((res) => {
          this.tableData = res.data.list.map((item) => {
            // 处理一下sex的中文显示
            item.sexLabel = item.sex === 0 ? '女' : '男'
            return item
          })
          this.config.total = res.data.count
          this.config.loading = false
        })
    },
...

来看看搜索,当然现在只能搜索name
> [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V0TS6FNQ-1599482162790)(61864C73EE224D6BB838B3833EF31A38)]

项目总结

项目当中遇到的坑以及解决思路

  • 通过vue-devtool调试
  • 通过console输出调试

组件的封装思路路

  • 判断的基本参数
    • 哪些写死
    • 哪些是传进来
  • 拓展
    • 自定义事件,判断传出哪些参数
    • 插槽扩展
  • 优化
    • 提高他的适应性
      • vif,velse根据⽗父组件传⼊入的条件来生成对应的模板

学习一个新技术

  • EChart
    • 大局观,直接看快速教程
    • 分成几部分,在对应部分查找文档

全文所涉及的代码下载地址

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值