Bladeren bron

feat: 菜单管理

kailong321200875 1 jaar geleden
bovenliggende
commit
c72b3a33aa

+ 246 - 0
mock/menu/index.ts

@@ -0,0 +1,246 @@
+import config from '@/config/axios/config'
+import { MockMethod } from 'vite-plugin-mock'
+import Mock from 'mockjs'
+import { toAnyString } from '@/utils'
+
+const { code } = config
+
+const timeout = 1000
+
+export default [
+  // 列表接口
+  {
+    url: '/menu/list',
+    method: 'get',
+    timeout,
+    response: () => {
+      return {
+        data: {
+          code: code,
+          data: {
+            list: [
+              {
+                path: '/dashboard',
+                component: '#',
+                redirect: '/dashboard/analysis',
+                name: 'Dashboard',
+                status: Mock.Random.integer(0, 1),
+                id: toAnyString(),
+                meta: {
+                  title: '首页',
+                  icon: 'ant-design:dashboard-filled',
+                  alwaysShow: true
+                },
+                children: [
+                  {
+                    path: 'analysis',
+                    component: 'views/Dashboard/Analysis',
+                    name: 'Analysis',
+                    status: Mock.Random.integer(0, 1),
+                    id: toAnyString(),
+                    meta: {
+                      title: '分析页',
+                      noCache: true
+                    }
+                  },
+                  {
+                    path: 'workplace',
+                    component: 'views/Dashboard/Workplace',
+                    name: 'Workplace',
+                    status: Mock.Random.integer(0, 1),
+                    id: toAnyString(),
+                    meta: {
+                      title: '工作台',
+                      noCache: true
+                    }
+                  }
+                ]
+              },
+              {
+                path: '/external-link',
+                component: '#',
+                meta: {
+                  title: '文档',
+                  icon: 'clarity:document-solid'
+                },
+                name: 'ExternalLink',
+                status: Mock.Random.integer(0, 1),
+                id: toAnyString(),
+                children: [
+                  {
+                    path: 'https://element-plus-admin-doc.cn/',
+                    name: 'DocumentLink',
+                    status: Mock.Random.integer(0, 1),
+                    id: toAnyString(),
+                    meta: {
+                      title: '文档'
+                    }
+                  }
+                ]
+              },
+              {
+                path: '/level',
+                component: '#',
+                redirect: '/level/menu1/menu1-1/menu1-1-1',
+                name: 'Level',
+                status: Mock.Random.integer(0, 1),
+                id: toAnyString(),
+                meta: {
+                  title: '菜单',
+                  icon: 'carbon:skill-level-advanced'
+                },
+                children: [
+                  {
+                    path: 'menu1',
+                    name: 'Menu1',
+                    component: '##',
+                    status: Mock.Random.integer(0, 1),
+                    id: toAnyString(),
+                    redirect: '/level/menu1/menu1-1/menu1-1-1',
+                    meta: {
+                      title: '菜单1'
+                    },
+                    children: [
+                      {
+                        path: 'menu1-1',
+                        name: 'Menu11',
+                        component: '##',
+                        status: Mock.Random.integer(0, 1),
+                        id: toAnyString(),
+                        redirect: '/level/menu1/menu1-1/menu1-1-1',
+                        meta: {
+                          title: '菜单1-1',
+                          alwaysShow: true
+                        },
+                        children: [
+                          {
+                            path: 'menu1-1-1',
+                            name: 'Menu111',
+                            component: 'views/Level/Menu111',
+                            status: Mock.Random.integer(0, 1),
+                            id: toAnyString(),
+                            meta: {
+                              title: '菜单1-1-1',
+                              permission: ['edit', 'add']
+                            }
+                          }
+                        ]
+                      },
+                      {
+                        path: 'menu1-2',
+                        name: 'Menu12',
+                        component: 'views/Level/Menu12',
+                        status: Mock.Random.integer(0, 1),
+                        id: toAnyString(),
+                        meta: {
+                          title: '菜单1-2',
+                          permission: ['edit', 'add']
+                        }
+                      }
+                    ]
+                  },
+                  {
+                    path: 'menu2',
+                    name: 'Menu2Demo',
+                    component: 'views/Level/Menu2',
+                    status: Mock.Random.integer(0, 1),
+                    id: toAnyString(),
+                    meta: {
+                      title: '菜单2',
+                      permission: ['edit', 'add']
+                    }
+                  }
+                ]
+              },
+              {
+                path: '/example',
+                component: '#',
+                redirect: '/example/example-dialog',
+                name: 'Example',
+                status: Mock.Random.integer(0, 1),
+                id: toAnyString(),
+                meta: {
+                  title: '综合示例',
+                  icon: 'ep:management',
+                  alwaysShow: true
+                },
+                children: [
+                  {
+                    path: 'example-dialog',
+                    component: 'views/Example/Dialog/ExampleDialog',
+                    name: 'ExampleDialog',
+                    status: Mock.Random.integer(0, 1),
+                    id: toAnyString(),
+                    meta: {
+                      title: '综合示例-弹窗',
+                      permission: ['edit', 'add', 'delete']
+                    }
+                  },
+                  {
+                    path: 'example-page',
+                    component: 'views/Example/Page/ExamplePage',
+                    name: 'ExamplePage',
+                    status: Mock.Random.integer(0, 1),
+                    id: toAnyString(),
+                    meta: {
+                      title: '综合示例-页面',
+                      permission: ['edit', 'add', 'delete']
+                    }
+                  },
+                  {
+                    path: 'example-add',
+                    component: 'views/Example/Page/ExampleAdd',
+                    name: 'ExampleAdd',
+                    status: Mock.Random.integer(0, 1),
+                    id: toAnyString(),
+                    meta: {
+                      title: '综合示例-新增',
+                      noTagsView: true,
+                      noCache: true,
+                      hidden: true,
+                      showMainRoute: true,
+                      activeMenu: '/example/example-page',
+                      permission: ['edit', 'add', 'delete']
+                    }
+                  },
+                  {
+                    path: 'example-edit',
+                    component: 'views/Example/Page/ExampleEdit',
+                    name: 'ExampleEdit',
+                    status: Mock.Random.integer(0, 1),
+                    id: toAnyString(),
+                    meta: {
+                      title: '综合示例-编辑',
+                      noTagsView: true,
+                      noCache: true,
+                      hidden: true,
+                      showMainRoute: true,
+                      activeMenu: '/example/example-page',
+                      permission: ['edit', 'add', 'delete']
+                    }
+                  },
+                  {
+                    path: 'example-detail',
+                    component: 'views/Example/Page/ExampleDetail',
+                    name: 'ExampleDetail',
+                    status: Mock.Random.integer(0, 1),
+                    id: toAnyString(),
+                    meta: {
+                      title: '综合示例-详情',
+                      noTagsView: true,
+                      noCache: true,
+                      hidden: true,
+                      showMainRoute: true,
+                      activeMenu: '/example/example-page',
+                      permission: ['edit', 'add', 'delete']
+                    }
+                  }
+                ]
+              }
+            ]
+          }
+        }
+      }
+    }
+  }
+] as MockMethod[]

+ 0 - 8
mock/role/index.ts

@@ -105,14 +105,6 @@ const adminList = [
             meta: {
               title: 'UseForm'
             }
-          },
-          {
-            path: 'ref-form',
-            component: 'views/Components/Form/RefForm',
-            name: 'RefForm',
-            meta: {
-              title: 'RefForm'
-            }
           }
         ]
       },

+ 1 - 2
package.json

@@ -52,8 +52,7 @@
     "vue": "3.3.4",
     "vue-i18n": "9.2.2",
     "vue-router": "^4.2.4",
-    "vue-types": "^5.1.0",
-    "web-storage-cache": "^1.1.1"
+    "vue-types": "^5.1.0"
   },
   "devDependencies": {
     "@commitlint/cli": "^17.6.7",

+ 5 - 0
src/api/menu/index.ts

@@ -0,0 +1,5 @@
+import request from '@/config/axios'
+
+export const getMenuListApi = () => {
+  return request.get({ url: '/menu/list' })
+}

+ 2 - 2
src/hooks/web/useTable.ts

@@ -12,7 +12,7 @@ interface UseTableConfig {
   immediate?: boolean
   fetchDataApi: () => Promise<{
     list: any[]
-    total: number
+    total?: number
   }>
   fetchDelApi?: () => Promise<boolean>
 }
@@ -83,7 +83,7 @@ export const useTable = (config: UseTableConfig) => {
         console.log('fetchDataApi res', res)
         if (res) {
           dataList.value = res.list
-          total.value = res.total
+          total.value = res.total || 0
         }
       } catch (err) {
         console.log('fetchDataApi error')

+ 20 - 1
src/locales/en.ts

@@ -161,7 +161,8 @@ export default {
     sticky: 'Sticky',
     treeTable: 'Tree table',
     PicturePreview: 'Table Image Preview',
-    department: 'Department management'
+    department: 'Department management',
+    menuManagement: 'Menu management'
   },
   permission: {
     hasPermission: 'Please set the operation permission value'
@@ -504,6 +505,24 @@ export default {
     disable: 'Disable',
     superiorDepartment: 'Superior department'
   },
+  menu: {
+    menuName: 'Menu name',
+    icon: 'Icon',
+    // 权限
+    permission: 'Permission',
+    component: 'Component',
+    path: 'Path',
+    status: 'Status',
+    hidden: 'Hidden',
+    alwaysShow: 'Always show',
+    noCache: 'No cache',
+    breadcrumb: 'Breadcrumb',
+    affix: 'Affix',
+    noTagsView: 'No tags view',
+    activeMenu: 'Active menu',
+    canTo: 'Can to',
+    name: 'Name'
+  },
   inputPasswordDemo: {
     title: 'InputPassword',
     inputPasswordDes: 'Secondary packaging of Input components based on ElementPlus'

+ 19 - 1
src/locales/zh-CN.ts

@@ -161,7 +161,8 @@ export default {
     sticky: '黏性',
     treeTable: '树形表格',
     PicturePreview: '表格图片预览',
-    department: '部门管理'
+    department: '部门管理',
+    menuManagement: '菜单管理'
   },
   permission: {
     hasPermission: '请设置操作权限值'
@@ -499,6 +500,23 @@ export default {
     // 上级部门
     superiorDepartment: '上级部门'
   },
+  menu: {
+    menuName: '菜单名称',
+    icon: '图标',
+    permission: '权限标识',
+    component: '组件',
+    path: '路径',
+    status: '状态',
+    hidden: '是否隐藏',
+    alwaysShow: '是否一直显示',
+    noCache: '是否清除缓存',
+    breadcrumb: '是否显示面包屑',
+    affix: '是否固定在标签页',
+    noTagsView: '是否隐藏标签页',
+    activeMenu: '高亮菜单',
+    canTo: '是否可跳转',
+    name: '组件名称'
+  },
   inputPasswordDemo: {
     title: '密码输入框',
     inputPasswordDes: '基于 ElementPlus 的 Input 组件二次封装'

+ 16 - 8
src/router/index.ts

@@ -517,6 +517,14 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
       alwaysShow: true
     },
     children: [
+      {
+        path: 'department',
+        component: () => import('@/views/Authorization/Department/Department.vue'),
+        name: 'Department',
+        meta: {
+          title: t('router.department')
+        }
+      },
       {
         path: 'user',
         component: () => import('@/views/Authorization/User/User.vue'),
@@ -526,19 +534,19 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
         }
       },
       {
-        path: 'role',
-        component: () => import('@/views/Authorization/Role.vue'),
-        name: 'Role',
+        path: 'menu',
+        component: () => import('@/views/Authorization/Menu/Menu.vue'),
+        name: 'Menu',
         meta: {
-          title: t('router.role')
+          title: t('router.menuManagement')
         }
       },
       {
-        path: 'department',
-        component: () => import('@/views/Authorization/Department/Department.vue'),
-        name: 'Department',
+        path: 'role',
+        component: () => import('@/views/Authorization/Role/Role.vue'),
+        name: 'Role',
         meta: {
-          title: t('router.department')
+          title: t('router.role')
         }
       }
     ]

+ 7 - 5
src/views/Authorization/Department/Department.vue

@@ -11,8 +11,8 @@ import {
   saveDepartmentApi,
   deleteDepartmentApi
 } from '@/api/department'
+import type { DepartmentItem } from '@/api/department/types'
 import { useTable } from '@/hooks/web/useTable'
-import { TableData } from '@/api/table/types'
 import { ref, unref, reactive } from 'vue'
 import Write from './components/Write.vue'
 import Detail from './components/Detail.vue'
@@ -238,7 +238,7 @@ const { allSchemas } = useCrudSchemas(crudSchemas)
 const dialogVisible = ref(false)
 const dialogTitle = ref('')
 
-const currentRow = ref<TableData | null>(null)
+const currentRow = ref<DepartmentItem | null>(null)
 const actionType = ref('')
 
 const AddAction = () => {
@@ -250,16 +250,18 @@ const AddAction = () => {
 
 const delLoading = ref(false)
 
-const delData = async (row: TableData | null) => {
+const delData = async (row: DepartmentItem | null) => {
   const elTableExpose = await getElTableExpose()
-  ids.value = row ? [row.id] : elTableExpose?.getSelectionRows().map((v: TableData) => v.id) || []
+  ids.value = row
+    ? [row.id]
+    : elTableExpose?.getSelectionRows().map((v: DepartmentItem) => v.id) || []
   delLoading.value = true
   await delList(unref(ids).length).finally(() => {
     delLoading.value = false
   })
 }
 
-const action = (row: TableData, type: string) => {
+const action = (row: DepartmentItem, type: string) => {
   dialogTitle.value = t(type === 'edit' ? 'exampleDemo.edit' : 'exampleDemo.detail')
   actionType.value = type
   currentRow.value = row

+ 2 - 2
src/views/Authorization/Department/components/Detail.vue

@@ -1,11 +1,11 @@
 <script setup lang="ts">
 import { PropType } from 'vue'
-import type { TableData } from '@/api/table/types'
+import { DepartmentItem } from '@/api/department/types'
 import { Descriptions, DescriptionsSchema } from '@/components/Descriptions'
 
 defineProps({
   currentRow: {
-    type: Object as PropType<Nullable<TableData>>,
+    type: Object as PropType<Nullable<DepartmentItem>>,
     default: () => null
   },
   detailSchema: {

+ 2 - 2
src/views/Authorization/Department/components/Write.vue

@@ -2,14 +2,14 @@
 import { Form, FormSchema } from '@/components/Form'
 import { useForm } from '@/hooks/web/useForm'
 import { PropType, reactive, watch } from 'vue'
-import { TableData } from '@/api/table/types'
 import { useValidator } from '@/hooks/web/useValidator'
+import { DepartmentItem } from '@/api/department/types'
 
 const { required } = useValidator()
 
 const props = defineProps({
   currentRow: {
-    type: Object as PropType<Nullable<TableData>>,
+    type: Object as PropType<Nullable<DepartmentItem>>,
     default: () => null
   },
   formSchema: {

+ 193 - 0
src/views/Authorization/Menu/Menu.vue

@@ -0,0 +1,193 @@
+<script setup lang="tsx">
+import { reactive, ref, unref } from 'vue'
+import { getMenuListApi } from '@/api/menu'
+import { useTable } from '@/hooks/web/useTable'
+import { useI18n } from '@/hooks/web/useI18n'
+import { Table, TableColumn } from '@/components/Table'
+import { ElButton, ElTag } from 'element-plus'
+import { Icon } from '@/components/Icon'
+import { Search } from '@/components/Search'
+import { FormSchema } from '@/components/Form'
+import { ContentWrap } from '@/components/ContentWrap'
+import Write from './components/Write.vue'
+import { Dialog } from '@/components/Dialog'
+
+const { t } = useI18n()
+
+const { tableRegister, tableState, tableMethods } = useTable({
+  fetchDataApi: async () => {
+    const res = await getMenuListApi()
+    return {
+      list: res.data.list || []
+    }
+  }
+})
+
+const { dataList, loading } = tableState
+const { getList } = tableMethods
+
+const tableColumns = reactive<TableColumn[]>([
+  {
+    field: 'index',
+    label: t('userDemo.index'),
+    type: 'index'
+  },
+  {
+    field: 'meta.title',
+    label: t('menu.menuName')
+  },
+  {
+    field: 'meta.icon',
+    label: t('menu.icon'),
+    slots: {
+      default: (data: any) => {
+        const icon = data[0].row.meta.icon
+        if (icon) {
+          return (
+            <>
+              <Icon icon={icon} />
+            </>
+          )
+        } else {
+          return null
+        }
+      }
+    }
+  },
+  {
+    field: 'meta.permission',
+    label: t('menu.permission'),
+    slots: {
+      default: (data: any) => {
+        const permission = data[0].row.meta.permission
+        return permission ? <>{permission.join(', ')}</> : null
+      }
+    }
+  },
+  {
+    field: 'component',
+    label: t('menu.component'),
+    slots: {
+      default: (data: any) => {
+        const component = data[0].row.component
+        return <>{component === '#' ? '顶级目录' : component === '##' ? '子目录' : component}</>
+      }
+    }
+  },
+  {
+    field: 'path',
+    label: t('menu.path')
+  },
+  {
+    field: 'status',
+    label: t('menu.status'),
+    slots: {
+      default: (data: any) => {
+        return (
+          <>
+            <ElTag type={data[0].row.status === 0 ? 'danger' : 'success'}>
+              {data[0].row.status === 1 ? t('userDemo.enable') : t('userDemo.disable')}
+            </ElTag>
+          </>
+        )
+      }
+    }
+  },
+  {
+    field: 'action',
+    label: t('userDemo.action'),
+    width: 240,
+    slots: {
+      default: (data: any) => {
+        const row = data[0].row
+        return (
+          <>
+            <ElButton type="primary" onClick={() => action(row, 'edit')}>
+              {t('exampleDemo.edit')}
+            </ElButton>
+            <ElButton type="danger">{t('exampleDemo.del')}</ElButton>
+          </>
+        )
+      }
+    }
+  }
+])
+
+const searchSchema = reactive<FormSchema[]>([
+  {
+    field: 'meta.title',
+    label: t('menu.menuName'),
+    component: 'Input'
+  }
+])
+
+const searchParams = ref({})
+const setSearchParams = (data: any) => {
+  searchParams.value = data
+  getList()
+}
+
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+
+const currentRow = ref()
+const actionType = ref('')
+
+const writeRef = ref<ComponentRef<typeof Write>>()
+
+const saveLoading = ref(false)
+
+const action = (row: any, type: string) => {
+  dialogTitle.value = t(type === 'edit' ? 'exampleDemo.edit' : 'exampleDemo.detail')
+  actionType.value = type
+  currentRow.value = row
+  dialogVisible.value = true
+}
+
+const AddAction = () => {
+  dialogTitle.value = t('exampleDemo.add')
+  currentRow.value = undefined
+  dialogVisible.value = true
+  actionType.value = ''
+}
+
+const save = async () => {
+  const write = unref(writeRef)
+  const formData = await write?.submit()
+  if (formData) {
+    saveLoading.value = true
+    setTimeout(() => {
+      saveLoading.value = false
+      dialogVisible.value = false
+    }, 1000)
+  }
+}
+</script>
+
+<template>
+  <ContentWrap>
+    <Search :schema="searchSchema" @reset="setSearchParams" @search="setSearchParams" />
+    <div class="mb-10px">
+      <ElButton type="primary" @click="AddAction">{{ t('exampleDemo.add') }}</ElButton>
+    </div>
+    <Table
+      :columns="tableColumns"
+      default-expand-all
+      node-key="id"
+      :data="dataList"
+      :loading="loading"
+      @register="tableRegister"
+    />
+  </ContentWrap>
+
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
+    <Write v-if="actionType !== 'detail'" ref="writeRef" :current-row="currentRow" />
+
+    <template #footer>
+      <ElButton v-if="actionType !== 'detail'" type="primary" :loading="saveLoading" @click="save">
+        {{ t('exampleDemo.save') }}
+      </ElButton>
+      <ElButton @click="dialogVisible = false">{{ t('dialogDemo.close') }}</ElButton>
+    </template>
+  </Dialog>
+</template>

+ 164 - 0
src/views/Authorization/Menu/components/Write.vue

@@ -0,0 +1,164 @@
+<script setup lang="ts">
+import { Form, FormSchema } from '@/components/Form'
+import { useForm } from '@/hooks/web/useForm'
+import { PropType, reactive, watch } from 'vue'
+import { useValidator } from '@/hooks/web/useValidator'
+import { useI18n } from '@/hooks/web/useI18n'
+
+const { t } = useI18n()
+
+const { required } = useValidator()
+
+const props = defineProps({
+  currentRow: {
+    type: Object as PropType<any>,
+    default: () => null
+  }
+})
+
+const formSchema = reactive<FormSchema[]>([
+  {
+    field: 'meta.title',
+    label: t('menu.menuName'),
+    component: 'Input'
+  },
+  {
+    field: 'component',
+    label: t('menu.component'),
+    component: 'Input'
+  },
+  {
+    field: 'name',
+    label: t('menu.name'),
+    component: 'Input'
+  },
+  {
+    field: 'meta.icon',
+    label: t('menu.icon'),
+    component: 'Input'
+  },
+  {
+    field: 'path',
+    label: t('menu.path'),
+    component: 'Input'
+  },
+  {
+    field: 'status',
+    label: t('menu.status'),
+    component: 'Select',
+    componentProps: {
+      options: [
+        {
+          label: t('userDemo.disable'),
+          value: 0
+        },
+        {
+          label: t('userDemo.enable'),
+          value: 1
+        }
+      ]
+    }
+  },
+  {
+    field: 'meta.activeMenu',
+    label: t('menu.activeMenu'),
+    component: 'Input'
+  },
+  {
+    field: 'meta.permission',
+    label: t('menu.permission'),
+    component: 'CheckboxGroup',
+    componentProps: {
+      options: [
+        {
+          label: 'add',
+          value: 'add'
+        },
+        {
+          label: 'edit',
+          value: 'edit'
+        },
+        {
+          label: 'delete',
+          value: 'delete'
+        }
+      ]
+    }
+  },
+  {
+    field: 'meta.hidden',
+    label: t('menu.hidden'),
+    component: 'Switch'
+  },
+  {
+    field: 'meta.alwaysShow',
+    label: t('menu.alwaysShow'),
+    component: 'Switch'
+  },
+  {
+    field: 'meta.noCache',
+    label: t('menu.noCache'),
+    component: 'Switch'
+  },
+  {
+    field: 'meta.breadcrumb',
+    label: t('menu.breadcrumb'),
+    component: 'Switch'
+  },
+  {
+    field: 'meta.affix',
+    label: t('menu.affix'),
+    component: 'Switch'
+  },
+  {
+    field: 'meta.noTagsView',
+    label: t('menu.noTagsView'),
+    component: 'Switch'
+  },
+  {
+    field: 'canTo',
+    label: t('menu.canTo'),
+    component: 'Switch'
+  }
+])
+
+const rules = reactive({
+  component: [required()],
+  path: [required()],
+  'meta.title': [required()]
+})
+
+const { formRegister, formMethods } = useForm()
+const { setValues, getFormData, getElFormExpose } = formMethods
+
+const submit = async () => {
+  const elForm = await getElFormExpose()
+  const valid = await elForm?.validate().catch((err) => {
+    console.log(err)
+  })
+  if (valid) {
+    const formData = getFormData()
+    return formData
+  }
+}
+
+watch(
+  () => props.currentRow,
+  (currentRow) => {
+    if (!currentRow) return
+    setValues(currentRow)
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+
+defineExpose({
+  submit
+})
+</script>
+
+<template>
+  <Form :rules="rules" @register="formRegister" :schema="formSchema" />
+</template>

+ 0 - 0
src/views/Authorization/Role.vue → src/views/Authorization/Role/Role.vue


+ 1 - 0
src/views/Authorization/User/components/Write.vue

@@ -21,6 +21,7 @@ const props = defineProps({
 const rules = reactive({
   username: [required()],
   account: [required()],
+  'department.id': [required()],
   role: [required()],
   email: [required()],
   createTime: [required()]

+ 3 - 2
types/router.d.ts

@@ -27,9 +27,9 @@ import { defineComponent } from 'vue'
 
     activeMenu: '/dashboard'  显示高亮的路由路径
 
-    followAuth: '/dashboard'  跟随哪个路由进行权限过滤
-
     canTo: true               设置为true即使hidden为true,也依然可以进行路由跳转(默认 false)
+
+    permission: ['edit','add', 'delete']    设置该路由的权限
   }
 **/
 declare module 'vue-router' {
@@ -45,6 +45,7 @@ declare module 'vue-router' {
     noTagsView?: boolean
     followAuth?: string
     canTo?: boolean
+    permission?: string[]
   }
 }