vue项目权限:数据权限、菜单权限、按钮权限

  • Post author:
  • Post category:vue




前言

不管是移动端,还是pc端,可能都会有用户登录操作,不同的用户之间又拥有不同的角色,而不同角色之间势必存在不同的权限;

如果按照类型划分,大概可分为三类:菜单权限、按钮权限、数据权限;

数据权限:前端在请求头统一封装,携带用户信息,由后端解析返回;

菜单权限:通过element菜单组件实现;

按钮权限:通过自定义指令实现;

菜单权限&按钮权限:本质上也是通过数据权限,由后端返回当前用户所有有权限菜单及按钮,然后由前端处理;



login接口返回数据格式

登录接口按照类似格式返回是处理一切权限问题的前提;

内容主要包含用户信息,菜单信息,按钮信息等,其中:

token:用于前端请求头封装,后端根据此字段判断用户信息;

menus:当前登录用户菜单权限列表,用于菜单权限的处理;

permissions:当前登录用户按钮权限列表,用于按钮权限的处理;

具体数据结构如下:

{
    "code": 1,
    "data": {
        "id": "1237361915165020161",
        "username": "admin",
        "phone": "178****9154",
        "nickName": "小d",
        "realName": "段段",
        "sex": 1,
        "deptId": "1237322421447561216",
        "deptName": "测试部门",
        "status": 1,
        "email": "1@qq.com",
        "token":"token串", // 用于前端请求头封装,后端根据此字段判断用户信息
        "menus": [ // 当前登录用户菜单权限列表
            {
                "id": "1236916745927790564",
                "title": "系统管理",
                "icon": "el-icon-star-off",
                "path": "/org",
                "name": "org",
                "children": [
                    {
                        "id": "1236916745927790578",
                        "title": "角色管理", 
                        "icon": "el-icon-s-promotion",
                        "path": "/roles",
                        "name": "roles",
                        "children": [],
                    },
                    {
                        "id": "1236916745927790560",
                        "title": "菜单管理",
                        "icon": "el-icon-s-tools",
                        "path": "/menus",
                        "name": "menus",
                        "children": [],
                    },
                    {
                        "id": "1236916745927790575",
                        "title": "用户管理",
                        "icon": "el-icon-s-custom",
                        "path": "/users",
                        "name": "user",
                        "children": [],
                    }
                ],
                "spread": true,
                "checked": false
            },
            {
                "id": "1236916745927790569",
                "title": "账号管理",
                "icon": "el-icon-s-data",
                "path": "/user",
                "name": "user",
                "children": []
            }
        ],
        "permissions": [ // 当前登录用户按钮权限
            "sys:log:delete",
            "sys:user:add",
            "sys:role:update",
            "sys:dept:list"
        ]
    }
}



数据权限



1.登录接口返回当前用户token

具体的返回形式如login返回数据;



2.login接口数据存储vuex

推荐文章:

链接



3.接口请求二次封装

// 此为移动端配置,引用了vant组件,如要换成其他组件请修改配置
// import Vue from 'vue'
import axios from 'axios'
import qs from 'qs'
import { isEmpty, isFormData, filterObj } from '@/utils/function.js'
import { Toast, Dialog } from 'vant'

let pending = [] // 声明一个数组用于存储每个ajax请求的取消函数和ajax标识
let CancelToken = axios.CancelToken

const TIMEOUT = 3000
const baseURL = process.env.VUE_APP_BASE_UPL

const getErrorString = (errorObj, defaultStr = '') => {
    let errStr = defaultStr
    // 对象
    if (typeof errorObj === 'object') {
        if (errorObj.response && errorObj.response.status) {
            errStr = `${errorObj.response.status}-${errorObj.response.statusText}`
        } else if (errorObj.message && typeof errorObj.message === 'string') {
            errStr = `${errorObj.message} 无返回或非正常操作`
        }
    } else if (typeof errorObj === 'string') {
        errStr = errorObj
    }
    return errStr
}

const removePending = ever => {
    for (const p in pending) {
        if (pending[p].u === ever.url + '&' + ever.method) {
            // 当当前请求在数组中存在时执行函数体
            pending[p].f() // 执行取消操作
            pending.splice(p, 1) // 把这条记录从数组中移除
        }
    }
}

function handleResponse (response, options) {
    if (!response || !options) return
    if (options.showLoading && options.data && !options.data.hideLoading) {
        Toast.clear()
    }
    if (response.status === 200) {
        const data = response.data
        const contentType = response.headers['content-type']
        if (data.code === '40001') {
            Dialog.alert({
                message: data.message || '内部错误'
            }).then(() => {
            })
            return
        }
        if (contentType && contentType.indexOf('json') !== -1) {
            if (response.data.constructor === window.Blob) {
                const reader = new FileReader()
                reader.readAsText(response.data)
                reader.addEventListener('loadend', function () {
                    const res = JSON.parse(reader.result)
                    Toast(`${response.data.message || '未知异常'}`)
                })
            } else {
                const data = eval(response.data)
                const { success, message } = data
                if (!success && !options.noToast) {
                    Toast(`${response.data.message || '未知异常'}`)
                }
            }
        }
    } else if (response.status === 401 && options.showLoading) {
        Toast(`${response.data.message || '未知异常'}`)
    } else if (response.status > 401) {
        Toast(`${response.data.message || '未知异常'}`)
    } else if (response.status === '504') {
        Toast('请求错误 网络异常')
    }
}
const fetch = options => {
    const {
        method = 'get',
        data,
        url,
        headers,
        config
    } = options
    const { token } = store.state.login.user
    const axiosConfig = {
        timeout: TIMEOUT,
        baseURL,
        withCredentials: true,
        headers: {
            ...headers,
            'token': token // 重点:用户请求头的封装
        }
    }
    const instance = axios.create(axiosConfig)
    instance.interceptors.request.use(
        config => {
            // 判断是否需要显示满屏loading遮罩
            if (options.showLoading && options.data && !options.data.hideLoading) {
                Toast.loading({
                    mask: true,
                    loadingType: 'spinner',
                    // message: '加载中...',
                    duration: 30000,
                    getContainer: '#app'
                })
            }
            removePending(config) // 在一个ajax发送前执行一下取消操作
            config.cancelToken = new CancelToken(c => {
                // 这里的ajax标识我是用请求地址&请求方式拼接的字符串,当然你可以选择其他的一些方式
                pending.push({ u: config.url + '&' + config.method, f: c })
            })

            return config
        },
        err => {
            return Promise.reject(new Error(getErrorString(err, 'request错误')))
        }
    )

    instance.interceptors.response.use(
        response => {
            try {
                handleResponse(response, options)
                // ------------------------------------------------------------------------------------------
                removePending(options.config) // 在一个ajax响应后再执行一下取消操作,把已经完成的请求从pending中移除
                // -------------------------------------------------------------------------------------------
            } catch (err) { }
            return response
        },
        err => {
            handleResponse(err.response, options)
            return Promise.reject(new Error(getErrorString(err, 'response错误')))
        }
    )
    const filterData = isFormData ? data : filterObj(data)
    switch (method.toLowerCase()) {
        case 'get':
            return instance.get(
                `${url}${!isEmpty(data) ? `?${qs.stringify(data)}` : ''}`,
                config
            )
        case 'delete':
            return instance.delete(url, { data: filterData })
        case 'head':
            return instance.head(url, filterData)
        case 'post':
            return instance.post(url, filterData, config)
        case 'put':
            return instance.put(url, filterData, config)
        case 'patch':
            return instance.patch(url, filterData)
        default:
            return instance(options)
    }
}

export default function request (options) {
    return new Promise((resolve, reject) => {
        fetch(options)
            .then(response => {
                resolve(response.data)
            })
            .catch(err => {
                reject(err)
            })
    })
}



查看封装是否成功

在这里插入图片描述

至此,封装完结;请求头中插入token信息,后端根据token进行用户解析,从而达到数据权限的处理;



菜单权限



1.登录接口返回当前用户权限菜单

具体的返回形式如login返回数据;



2.login接口数据存储vuex

推荐文章:

链接



3.element的menu组件渲染菜单

在这里插入图片描述



按钮权限



1.登录接口返回当前用户按钮菜单

具体的返回形式如login返回数据;



2.login接口数据存储vuex

推荐文章:

链接



3.自定义指令处理权限菜单



新建directives文件夹—–index.js文件
import directives from './permission'
export default {
    install(Vue, options) {
        Vue.directive('has',directives);
    }
}


新建directives文件夹—–permission.js文件
import store from '@/store'
export default {
    inserted:(el,binding,vnode) => {
       let { userInfo = {} } = store.getters
       let {permissions = []} = userInfo
       permissions && !permissions.some(item => item==binding.value)&&(el.parentNode.removeChild(el));
    }
}


按钮权限增加
// 示例如下
<el-button v-has='sys:log:delete' type="primary" round>主要按钮</el-button>



版权声明:本文为duanhy_love原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。