feat:动态权限

This commit is contained in:
JaguarJack
2022-12-10 18:29:42 +08:00
parent 8c537e6656
commit c4270a2fc8
46 changed files with 948 additions and 177 deletions

View File

@@ -1,5 +1,5 @@
<template>
<el-select v-bind="$attrs">
<el-select v-bind="$attrs" class="w-full" clearable filterable>
<el-option-group v-for="group in elOptions" :key="group.label" :label="group.label" v-if="group">
<el-option v-for="item in group.options" :key="item.value" :label="item.label" :value="item.value" />
</el-option-group>
@@ -11,7 +11,7 @@
<script lang="ts" setup>
import http from '/admin/support/http'
import { ref } from 'vue'
import { ref, watch } from 'vue'
const props = defineProps({
options: {
@@ -26,6 +26,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
query: {
type: Object,
default: null,
},
})
interface Option {
@@ -38,11 +42,21 @@ interface GroupOption {
options: Array<Option>
}
const elOptions = props.group ? ref<Array<GroupOption>>() : ref<Array<Option>>()
if (props.api) {
http.get('options/' + props.api).then(r => {
const getOptions = () => {
http.get('options/' + props.api, props.query).then(r => {
elOptions.value = r.data.data
})
}
const elOptions = props.group ? ref<Array<GroupOption>>() : ref<Array<Option>>()
if (props.api) {
if (!props.query) {
getOptions()
} else {
watch(props, function () {
getOptions()
})
}
} else {
elOptions.value = props.options
}

View File

@@ -0,0 +1,332 @@
<template>
<div :class="`grid ${grid} gap-y-6`">
<div v-for="icon in icons" :key="icon" class="flex justify-center hover:cursor-pointer" @click="selectIcon(icon)">
<div v-if="modelValue === icon">
<div class="flex justify-center w-full text-violet-700"><Icon :name="icon" /></div>
<div class="text-sm text-violet-700">{{ icon }}</div>
</div>
<div v-else>
<div class="flex justify-center w-full"><Icon :name="icon" /></div>
<div class="text-sm">{{ icon }}</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
const props = defineProps({
modelValue: {
type: String,
require: true,
},
grid: {
type: String,
default: 'grid-cols-5',
},
})
const emits = defineEmits(['update:modelValue', 'close'])
const selectIcon = (icon: string) => {
emits('update:modelValue', icon)
emits('close')
}
// icons
const icons = [
'academic-cap',
'adjustments-horizontal',
'adjustments-vertical',
'archive-box-arrow-down',
'archive-box-x-mark',
'archive-box',
'arrow-down-circle',
'arrow-down-left',
'arrow-down-on-square-stack',
'arrow-down-on-square',
'arrow-down-right',
'arrow-down-tray',
'arrow-down',
'arrow-left-circle',
'arrow-left-on-rectangle',
'arrow-left',
'arrow-long-down',
'arrow-long-left',
'arrow-long-right',
'arrow-long-up',
'arrow-path-rounded-square',
'arrow-path',
'arrow-right-circle',
'arrow-right-on-rectangle',
'arrow-right',
'arrow-small-down',
'arrow-small-left',
'arrow-small-right',
'arrow-small-up',
'arrow-top-right-on-square',
'arrow-trending-down',
'arrow-trending-up',
'arrow-up-circle',
'arrow-up-left',
'arrow-up-on-square-stack',
'arrow-up-on-square',
'arrow-up-right',
'arrow-up-tray',
'arrow-up',
'arrow-uturn-down',
'arrow-uturn-left',
'arrow-uturn-right',
'arrow-uturn-up',
'arrows-pointing-in',
'arrows-pointing-out',
'arrows-right-left',
'arrows-up-down',
'at-symbol',
'backspace',
'backward',
'banknotes',
'bars-2',
'bars-3-bottom-left',
'bars-3-bottom-right',
'bars-3-center-left',
'bars-3',
'bars-4',
'bars-arrow-down',
'bars-arrow-up',
'battery-0',
'battery-100',
'battery-50',
'beaker',
'bell-alert',
'bell-slash',
'bell-snooze',
'bell',
'bolt-slash',
'bolt',
'book-open',
'bookmark-slash',
'bookmark-square',
'bookmark',
'briefcase',
'bug-ant',
'building-library',
'building-office-2',
'building-office',
'building-storefront',
'cake',
'calculator',
'calendar-days',
'calendar',
'camera',
'chart-bar-square',
'chart-bar',
'chart-pie',
'chat-bubble-bottom-center-text',
'chat-bubble-bottom-center',
'chat-bubble-left-ellipsis',
'chat-bubble-left-right',
'chat-bubble-left',
'chat-bubble-oval-left-ellipsis',
'chat-bubble-oval-left',
'check-badge',
'check-circle',
'check',
'chevron-double-down',
'chevron-double-left',
'chevron-double-right',
'chevron-double-up',
'chevron-down',
'chevron-left',
'chevron-right',
'chevron-up-down',
'chevron-up',
'circle-stack',
'clipboard-document-check',
'clipboard-document-list',
'clipboard-document',
'clipboard',
'clock',
'cloud-arrow-down',
'cloud-arrow-up',
'cloud',
'code-bracket-square',
'code-bracket',
'cog-6-tooth',
'cog-8-tooth',
'cog',
'command-line',
'computer-desktop',
'cpu-chip',
'credit-card',
'cube-transparent',
'cube',
'currency-bangladeshi',
'currency-dollar',
'currency-euro',
'currency-pound',
'currency-rupee',
'currency-yen',
'cursor-arrow-rays',
'cursor-arrow-ripple',
'device-phone-mobile',
'device-tablet',
'document-arrow-down',
'document-arrow-up',
'document-chart-bar',
'document-check',
'document-duplicate',
'document-magnifying-glass',
'document-minus',
'document-plus',
'document-text',
'document',
'ellipsis-horizontal-circle',
'ellipsis-horizontal',
'ellipsis-vertical',
'envelope-open',
'envelope',
'exclamation-circle',
'exclamation-triangle',
'eye-dropper',
'eye-slash',
'eye',
'face-frown',
'face-smile',
'film',
'finger-print',
'fire',
'flag',
'folder-arrow-down',
'folder-minus',
'folder-open',
'folder-plus',
'folder',
'forward',
'funnel',
'gif',
'gift-top',
'gift',
'globe-alt',
'globe-americas',
'globe-asia-australia',
'globe-europe-africa',
'hand-raised',
'hand-thumb-down',
'hand-thumb-up',
'hashtag',
'heart',
'home-modern',
'home',
'identification',
'inbox-arrow-down',
'inbox-stack',
'inbox',
'information-circle',
'key',
'language',
'lifebuoy',
'light-bulb',
'link',
'list-bullet',
'lock-closed',
'lock-open',
'magnifying-glass-circle',
'magnifying-glass-minus',
'magnifying-glass-plus',
'magnifying-glass',
'map-pin',
'map',
'megaphone',
'microphone',
'minus-circle',
'minus-small',
'minus',
'moon',
'musical-note',
'newspaper',
'no-symbol',
'paint-brush',
'paper-airplane',
'paper-clip',
'pause-circle',
'pause',
'pencil-square',
'pencil',
'phone-arrow-down-left',
'phone-arrow-up-right',
'phone-x-mark',
'phone',
'photo',
'play-circle',
'play-pause',
'play',
'plus-circle',
'plus-small',
'plus',
'power',
'presentation-chart-bar',
'presentation-chart-line',
'printer',
'puzzle-piece',
'qr-code',
'question-mark-circle',
'queue-list',
'radio',
'receipt-percent',
'receipt-refund',
'rectangle-group',
'rectangle-stack',
'rocket-launch',
'rss',
'scale',
'scissors',
'server-stack',
'server',
'share',
'shield-check',
'shield-exclamation',
'shopping-bag',
'shopping-cart',
'signal-slash',
'signal',
'sparkles',
'speaker-wave',
'speaker-x-mark',
'square-2-stack',
'square-3-stack-3d',
'squares-2x2',
'squares-plus',
'star',
'stop-circle',
'stop',
'sun',
'swatch',
'table-cells',
'tag',
'ticket',
'trash',
'trophy',
'truck',
'tv',
'user-circle',
'user-group',
'user-minus',
'user-plus',
'user',
'users',
'variable',
'video-camera-slash',
'video-camera',
'view-columns',
'viewfinder-circle',
'wallet',
'wifi',
'window',
'wrench-screwdriver',
'wrench',
'x-circle',
'x-mark',
]
</script>
<style scoped></style>

View File

@@ -1,10 +1,11 @@
<template>
<el-switch @change="enabled(api, id)" :active-value="Status.ENABLE" :inactive-value="Status.DISABLE" :model-value="modelValue" :loading="loading" />
<el-switch @change="enabled(api, id)" :active-value="activeValue" :inactive-value="inactiveValue" :model-value="modelValue" :loading="loading" />
</template>
<script lang="ts" setup>
import { useEnabled } from '/admin/composables/curd/useEnabled'
import { Status } from '/admin/enum/app'
import { ref } from 'vue'
const props = defineProps({
modelValue: Boolean | Number | String,
@@ -16,8 +17,19 @@ const emits = defineEmits(['update:modelValue', 'refresh'])
const { enabled, success, loading, afterEnabled } = useEnabled()
const activeValue = ref<boolean | number | string>()
const inactiveValue = ref<boolean | number | string>()
if (typeof props.modelValue === 'boolean') {
activeValue.value = true
inactiveValue.value = false
} else {
activeValue.value = Status.ENABLE
inactiveValue.value = Status.DISABLE
}
success(() => {
emits('update:modelValue', props.modelValue === Status.ENABLE ? Status.DISABLE : Status.ENABLE)
emits('update:modelValue', props.modelValue === activeValue.value ? inactiveValue.value : activeValue.value)
})
afterEnabled.value = () => {

View File

@@ -1,12 +1,12 @@
import { ref } from 'vue'
import { t } from '/admin/support/helper'
const visible = ref<boolean>(false)
const id = ref(null)
const title = ref<string>('')
export function useOpen() {
const visible = ref<boolean>(false)
const id = ref(null)
const title = ref<string>('')
const open = (primary: any) => {
console.log(primary)
title.value = primary ? t('system.edit') : t('system.add')
id.value = primary
visible.value = true

View File

@@ -2,14 +2,14 @@ import http from '/admin/support/http'
import { Ref, ref } from 'vue'
import { isFunction } from '../../support/helper'
const loading = ref<boolean>(true)
const data = ref<object>()
// 后置钩子
const afterShow = ref()
export function useShow(path: string, id: string | number, fillData: null | Ref = null) {
const loading = ref<boolean>(true)
const data = ref<object>()
// 后置钩子
const afterShow = ref()
http.get(path + '/' + id).then(r => {
loading.value = false
data.value = r.data.data

View File

@@ -37,7 +37,7 @@ export const enum WhiteListPage {
* menu 类型
*/
export const enum MenuType {
PAGE_TYPE = 1,
Button_Type,
TOP_TYPE = 1,
PAGE_TYPE = 2,
Button_Type = 3,
}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { h, defineComponent, VNode } from 'vue'
import { h, defineComponent, VNode, toRaw } from 'vue'
import { usePermissionsStore } from '/admin/stores/modules/user/permissions'
import MenuItem from './item.vue'
import menus from './menus.vue'
@@ -106,8 +106,10 @@ export default defineComponent({
// 后端的 permissions 返回 undefined则认为该后端无权限系统
const permissions = userStore.getPermissions === undefined ? [] : userStore.getPermissions
const vnodes = getVNodes(filterMenus(permissionsStore.getMenusFrom(permissions)), props.subMenuClass)
console.log(permissionsStore.getMenusFrom(permissions))
console.log(filterMenus(permissionsStore.getMenusFrom(permissions)))
const vnodes = getVNodes(filterMenus(permissionsStore.getMenusFrom(permissions)), props.subMenuClass)
return () => {
return h(
menus,

View File

@@ -1,9 +1,16 @@
import { RouteRecordRaw } from 'vue-router'
// @ts-ignore
const modules = import.meta.glob('@/module/**/views/router.ts', { eager: true })
let moduleRoutes: RouteRecordRaw[] = []
export function getModuleRoutes() {
const modules = import.meta.glob('@/module/**/views/router.ts', { eager: true })
let moduleRoutes: RouteRecordRaw[] = []
Object.keys(modules).forEach(routePath => {
moduleRoutes = moduleRoutes.concat(modules[routePath].default)
})
export default moduleRoutes
Object.keys(modules).forEach(routePath => {
moduleRoutes = moduleRoutes.concat(modules[routePath].default)
})
return moduleRoutes
}
export function getModuleViewComponents() {
return import.meta.glob(['@/module/**/views/**/*.vue', '@/module/!User/views/**/*.vue', '@/module/!Develop/views/**/*.vue', '@/module/!Options/views/**/*.vue'])
}

View File

@@ -5,6 +5,7 @@ import { WhiteListPage } from '/admin/enum/app'
import { Router, RouteRecordRaw } from 'vue-router'
import { usePermissionsStore } from '/admin/stores/modules/user/permissions'
import { Menu } from '/admin/types/Menu'
import { toRaw } from 'vue'
const guard = (router: Router) => {
// white list
@@ -36,7 +37,7 @@ const guard = (router: Router) => {
// 挂载路由(实际是从后端获取用户的权限)
const permissionStore = usePermissionsStore()
// 动态路由挂载
const asyncRoutes = permissionStore.getAsyncMenusFrom(userStore.getPermissions)
const asyncRoutes = permissionStore.getAsyncMenusFrom(toRaw(userStore.getPermissions))
asyncRoutes.forEach((route: Menu) => {
router.addRoute(route as unknown as RouteRecordRaw)
})

View File

@@ -1,8 +1,10 @@
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import type { App } from 'vue'
// module routers
import moduleRoutes from './constantRoutes'
import { getModuleRoutes, getModuleViewComponents } from './constantRoutes'
const moduleRoutes = getModuleRoutes()
getModuleViewComponents()
export const constantRoutes: RouteRecordRaw[] = [
{
path: '/dashboard',

View File

@@ -128,7 +128,7 @@ export const useUserStore = defineStore('UserStore', {
getUserInfo() {
return new Promise((resolve, reject) => {
http
.get('/user/info')
.get('/user/online')
.then(response => {
const { id, nickname, email, avatar, permissions, roles, rememberToken, status } = response.data.data
// set user info

View File

@@ -4,6 +4,8 @@ import { MenuType } from '/admin/enum/app'
import { Menu } from '/admin/types/Menu'
import { constantRoutes } from '/admin/router'
import { RouteRecordRaw } from 'vue-router'
import { toRaw } from 'vue'
import { getModuleViewComponents } from '/admin/router/constantRoutes'
interface Permissions {
menus: Menu[]
@@ -68,15 +70,15 @@ export const usePermissionsStore = defineStore('PermissionsStore', {
const menus: Permission[] = []
permissions.forEach(permission => {
if (permission.type === MenuType.PAGE_TYPE) {
if (permission.type === MenuType.PAGE_TYPE || permission.type === MenuType.TOP_TYPE) {
menus.push(permission)
}
// set map
this.menuPathMap.set(permission.route, permission.title)
this.menuPathMap.set(permission.route, permission.permission_name)
})
this.setAsyncMenus(this.getAsnycMenus(menus))
this.setAsyncMenus(this.getAsnycMenus(menus, 0, '', getModuleViewComponents()))
return this.asyncMenus
},
@@ -94,7 +96,7 @@ export const usePermissionsStore = defineStore('PermissionsStore', {
}
const asyncMenus = this.getAsyncMenusFrom(permissions, force)
this.setMenus(asyncMenus)
this.setMenus(toRaw(asyncMenus))
return this.menus
},
@@ -118,23 +120,31 @@ export const usePermissionsStore = defineStore('PermissionsStore', {
* @param permissions
* @param parentId
* @param path
* @param viewComponents
* @returns
*/
getAsnycMenus(permissions: Permission[], parentId: number = 0, path: string = ''): Menu[] {
getAsnycMenus(permissions: Permission[], parentId: number = 0, path: string = '', viewComponents: any): Menu[] {
const menus: Menu[] = []
console.log(viewComponents)
permissions.forEach(permission => {
if (permission.parent_id === parentId) {
// menu
let importComponent
if (permission.type === MenuType.TOP_TYPE) {
importComponent = () => import(permission.component)
} else {
importComponent = viewComponents['/modules' + permission.component]
}
const menu: Menu = Object.assign({
path: this.resoulveRoutePath(permission.route, path),
name: permission.module + '_' + permission.permission_mark,
component: importComponent,
redirect: permission.redirect,
meta: Object.assign({ title: permission.title, icon: permission.icon, hidden: permission.hidden, is_inner: permission.is_inner }),
meta: Object.assign({ title: permission.permission_name, icon: permission.icon, hidden: permission.hidden, is_inner: permission.is_inner }),
})
// child menu
const children = this.getAsnycMenus(permissions, permission.id, menu.path)
const children = this.getAsnycMenus(permissions, permission.id, menu.path, viewComponents)
if (children.length > 0) {
menu.children = children
}

View File

@@ -3,7 +3,7 @@ export interface Permission {
parent_id: number
title: string
permission_name: string
type: number