feat:动态权限
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
332
resources/admin/components/admin/icons/index.vue
Normal file
332
resources/admin/components/admin/icons/index.vue
Normal 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>
|
@@ -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 = () => {
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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'])
|
||||
}
|
||||
|
@@ -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)
|
||||
})
|
||||
|
@@ -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',
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ export interface Permission {
|
||||
|
||||
parent_id: number
|
||||
|
||||
title: string
|
||||
permission_name: string
|
||||
|
||||
type: number
|
||||
|
||||
|
Reference in New Issue
Block a user