Browse Source

feat: 🎸 权限管理开发中

chenkl 4 years ago
parent
commit
6d7ea6694d
40 changed files with 2353 additions and 135 deletions
  1. 0 8
      lint-staged.config.js
  2. 5 1
      mock/index.ts
  3. 420 0
      mock/role/admin-role.ts
  4. 97 0
      mock/role/index.ts
  5. 489 0
      mock/role/test-role.ts
  6. 6 0
      mock/role/types.ts
  7. 70 0
      mock/user/index.ts
  8. 6 0
      mock/user/types.ts
  9. 1 6
      package.json
  10. 0 22
      src/assets/icons/svgo.yml
  11. 6 4
      src/components/Detail/index.vue
  12. 159 7
      src/components/Dialog/index.vue
  13. 0 1
      src/components/Search/index.vue
  14. 2 2
      src/components/Sider/Item.vue
  15. 0 5
      src/components/Table/components/TableColumn.vue
  16. 0 4
      src/components/Table/index.vue
  17. 3 1
      src/components/UserInfo/index.vue
  18. 11 11
      src/pages/index/api/index.ts
  19. 30 0
      src/pages/index/router/index.ts
  20. 1 0
      src/pages/index/router/types.d.ts
  21. 79 6
      src/pages/index/store/modules/permission.ts
  22. 10 30
      src/pages/index/views/components-demo/detail/index.vue
  23. 1 1
      src/pages/index/views/example-demo/example-dialog/components/Detail.vue
  24. 2 2
      src/pages/index/views/example-demo/example-dialog/components/InfoWrite.vue
  25. 2 2
      src/pages/index/views/example-demo/example-dialog/index.vue
  26. 1 1
      src/pages/index/views/example-demo/example-page/components/Detail.vue
  27. 2 2
      src/pages/index/views/example-demo/example-page/components/InfoWrite.vue
  28. 2 2
      src/pages/index/views/example-demo/example-page/index.vue
  29. 14 0
      src/pages/index/views/login/api.ts
  30. 34 12
      src/pages/index/views/login/index.vue
  31. 18 0
      src/pages/index/views/role-demo/role/api.ts
  32. 230 0
      src/pages/index/views/role-demo/role/components/InfoWrite.vue
  33. 259 0
      src/pages/index/views/role-demo/role/components/InfoWrite2.vue
  34. 213 0
      src/pages/index/views/role-demo/role/index.vue
  35. 10 0
      src/pages/index/views/role-demo/user/api.ts
  36. 159 0
      src/pages/index/views/role-demo/user/index.vue
  37. 5 0
      src/styles/element-plus.less
  38. 1 0
      src/styles/index.less
  39. 1 1
      src/utils/validate.ts
  40. 4 4
      yarn.lock

+ 0 - 8
lint-staged.config.js

@@ -1,8 +0,0 @@
-module.exports = {
-  '*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
-  '{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': ['prettier --write--parser json'],
-  'package.json': ['prettier --write'],
-  '*.vue': ['prettier --write', 'stylelint --fix'],
-  '*.{scss,less,styl,css,html}': ['stylelint --fix', 'prettier --write'],
-  '*.md': ['prettier --write']
-}

+ 5 - 1
mock/index.ts

@@ -2,9 +2,13 @@ import Mock from 'mockjs'
 import { param2Obj } from '@/utils'
 
 import example from './example'
+import user from './user'
+import role from './role'
 
 const mocks: any[] = [
-  ...example
+  ...example,
+  ...user,
+  ...role
 ]
 
 // for front mock

+ 420 - 0
mock/role/admin-role.ts

@@ -0,0 +1,420 @@
+export const checkedNodes = [{
+  'path': '/components-demo',
+  'title': '功能组件',
+  'name': 'ComponentsDemo',
+  'children': [{
+    'path': '/components-demo/echarts',
+    'title': '图表',
+    'name': 'EchartsDemo'
+  }, {
+    'path': '/components-demo/preview',
+    'title': '图片预览',
+    'name': 'PreviewDemo'
+  }, {
+    'path': '/components-demo/button',
+    'title': '按钮',
+    'name': 'ButtonDemo'
+  }, {
+    'path': '/components-demo/message',
+    'title': '消息提示',
+    'name': 'MessageDemo'
+  }, {
+    'path': '/components-demo/count-to',
+    'title': '数字动画',
+    'name': 'CountToDemo'
+  }, {
+    'path': '/components-demo/search',
+    'title': '查询',
+    'name': 'SearchDemo'
+  }, {
+    'path': '/components-demo/editor',
+    'title': '富文本编辑器',
+    'name': 'EditorDemo'
+  }, {
+    'path': '/components-demo/markdown',
+    'title': 'markdown编辑器',
+    'name': 'MarkdownDemo'
+  }, {
+    'path': '/components-demo/dialog',
+    'title': '弹窗',
+    'name': 'DialogDemo'
+  }, {
+    'path': '/components-demo/more',
+    'title': '显示更多',
+    'name': 'MoreDemo'
+  }, {
+    'path': '/components-demo/detail',
+    'title': '详情组件',
+    'name': 'DetailDemo'
+  }]
+}, {
+  'path': '/components-demo/echarts',
+  'title': '图表',
+  'name': 'EchartsDemo'
+}, {
+  'path': '/components-demo/preview',
+  'title': '图片预览',
+  'name': 'PreviewDemo'
+}, {
+  'path': '/components-demo/button',
+  'title': '按钮',
+  'name': 'ButtonDemo'
+}, {
+  'path': '/components-demo/message',
+  'title': '消息提示',
+  'name': 'MessageDemo'
+}, {
+  'path': '/components-demo/count-to',
+  'title': '数字动画',
+  'name': 'CountToDemo'
+}, {
+  'path': '/components-demo/search',
+  'title': '查询',
+  'name': 'SearchDemo'
+}, {
+  'path': '/components-demo/editor',
+  'title': '富文本编辑器',
+  'name': 'EditorDemo'
+}, {
+  'path': '/components-demo/markdown',
+  'title': 'markdown编辑器',
+  'name': 'MarkdownDemo'
+}, {
+  'path': '/components-demo/dialog',
+  'title': '弹窗',
+  'name': 'DialogDemo'
+}, {
+  'path': '/components-demo/more',
+  'title': '显示更多',
+  'name': 'MoreDemo'
+}, {
+  'path': '/components-demo/detail',
+  'title': '详情组件',
+  'name': 'DetailDemo'
+}, {
+  'path': '/table-demo',
+  'title': '表格',
+  'name': 'TableDemo',
+  'children': [{
+    'path': '/table-demo/basic-table',
+    'title': '基础表格',
+    'name': 'BasicTable'
+  }, {
+    'path': '/table-demo/page-table',
+    'title': '分页表格',
+    'name': 'PageTable'
+  }, {
+    'path': '/table-demo/stripe-table',
+    'title': '带斑马纹表格',
+    'name': 'StripeTable'
+  }, {
+    'path': '/table-demo/border-table',
+    'title': '带边框表格',
+    'name': 'BorderTable'
+  }, {
+    'path': '/table-demo/state-table',
+    'title': '带状态表格',
+    'name': 'StateTable'
+  }, {
+    'path': '/table-demo/fixed-header',
+    'title': '固定表头',
+    'name': 'FixedHeader'
+  }, {
+    'path': '/table-demo/fixed-column',
+    'title': '固定列',
+    'name': 'FixedColumn'
+  }, {
+    'path': '/table-demo/fixed-column-header',
+    'title': '固定列和表头',
+    'name': 'FixedColumnHeader'
+  }, {
+    'path': '/table-demo/fluid-height',
+    'title': '流体高度',
+    'name': 'FluidHeight'
+  }, {
+    'path': '/table-demo/multi-header',
+    'title': '多级表头',
+    'name': 'MultiHeader'
+  }, {
+    'path': '/table-demo/single-choice',
+    'title': '单选',
+    'name': 'SingleChoice'
+  }, {
+    'path': '/table-demo/multiple-choice',
+    'title': '多选',
+    'name': 'MultipleChoice'
+  }, {
+    'path': '/table-demo/sort-table',
+    'title': '排序',
+    'name': 'SortTable'
+  }, {
+    'path': '/table-demo/screen-table',
+    'title': '筛选',
+    'name': 'ScreenTable'
+  }, {
+    'path': '/table-demo/expand-row',
+    'title': '展开行',
+    'name': 'ExpandRow'
+  }, {
+    'path': '/table-demo/tree-and-load',
+    'title': '树形数据与懒加载',
+    'name': 'TreeAndLoad'
+  }, {
+    'path': '/table-demo/custom-header',
+    'title': '自定义表头',
+    'name': 'CustomHeader'
+  }, {
+    'path': '/table-demo/total-table',
+    'title': '表尾合计行',
+    'name': 'TotalTable'
+  }, {
+    'path': '/table-demo/merge-table',
+    'title': '合并行或列',
+    'name': 'MergeTable'
+  }, {
+    'path': '/table-demo/custom-index',
+    'title': '自定义索引',
+    'name': 'CustomIndex'
+  }]
+}, {
+  'path': '/table-demo/basic-table',
+  'title': '基础表格',
+  'name': 'BasicTable'
+}, {
+  'path': '/table-demo/page-table',
+  'title': '分页表格',
+  'name': 'PageTable'
+}, {
+  'path': '/table-demo/stripe-table',
+  'title': '带斑马纹表格',
+  'name': 'StripeTable'
+}, {
+  'path': '/table-demo/border-table',
+  'title': '带边框表格',
+  'name': 'BorderTable'
+}, {
+  'path': '/table-demo/state-table',
+  'title': '带状态表格',
+  'name': 'StateTable'
+}, {
+  'path': '/table-demo/fixed-header',
+  'title': '固定表头',
+  'name': 'FixedHeader'
+}, {
+  'path': '/table-demo/fixed-column',
+  'title': '固定列',
+  'name': 'FixedColumn'
+}, {
+  'path': '/table-demo/fixed-column-header',
+  'title': '固定列和表头',
+  'name': 'FixedColumnHeader'
+}, {
+  'path': '/table-demo/fluid-height',
+  'title': '流体高度',
+  'name': 'FluidHeight'
+}, {
+  'path': '/table-demo/multi-header',
+  'title': '多级表头',
+  'name': 'MultiHeader'
+}, {
+  'path': '/table-demo/single-choice',
+  'title': '单选',
+  'name': 'SingleChoice'
+}, {
+  'path': '/table-demo/multiple-choice',
+  'title': '多选',
+  'name': 'MultipleChoice'
+}, {
+  'path': '/table-demo/sort-table',
+  'title': '排序',
+  'name': 'SortTable'
+}, {
+  'path': '/table-demo/screen-table',
+  'title': '筛选',
+  'name': 'ScreenTable'
+}, {
+  'path': '/table-demo/expand-row',
+  'title': '展开行',
+  'name': 'ExpandRow'
+}, {
+  'path': '/table-demo/tree-and-load',
+  'title': '树形数据与懒加载',
+  'name': 'TreeAndLoad'
+}, {
+  'path': '/table-demo/custom-header',
+  'title': '自定义表头',
+  'name': 'CustomHeader'
+}, {
+  'path': '/table-demo/total-table',
+  'title': '表尾合计行',
+  'name': 'TotalTable'
+}, {
+  'path': '/table-demo/merge-table',
+  'title': '合并行或列',
+  'name': 'MergeTable'
+}, {
+  'path': '/table-demo/custom-index',
+  'title': '自定义索引',
+  'name': 'CustomIndex'
+}, {
+  'path': '/directives-demo',
+  'title': '自定义指令',
+  'name': 'DirectivesDemo',
+  'children': [{
+    'path': '/directives-demo/clipboard',
+    'title': 'Clipboard',
+    'name': 'ClipboardDemo'
+  }]
+}, {
+  'path': '/directives-demo/clipboard',
+  'title': 'Clipboard',
+  'name': 'ClipboardDemo'
+}, {
+  'path': '/hooks-demo',
+  'title': 'Hooks',
+  'name': 'HooksDemo',
+  'children': [{
+    'path': '/hooks-demo/watermark',
+    'title': 'UseWaterMark',
+    'name': 'UseWatermarkDemo'
+  }, {
+    'path': '/hooks-demo/useScrollTo',
+    'title': 'UseScrollTo',
+    'name': 'UseScrollToDemo'
+  }]
+}, {
+  'path': '/hooks-demo/watermark',
+  'title': 'UseWaterMark',
+  'name': 'UseWatermarkDemo'
+}, {
+  'path': '/hooks-demo/useScrollTo',
+  'title': 'UseScrollTo',
+  'name': 'UseScrollToDemo'
+}, {
+  'path': '/icon/index',
+  'title': '图标',
+  'name': 'Icons'
+}, {
+  'path': '/level',
+  'title': '多级菜单缓存',
+  'name': 'Level',
+  'children': [{
+    'path': '/level/menu1',
+    'title': 'Menu1',
+    'name': 'Menu1Demo',
+    'children': [{
+      'path': '/level/menu1/menu1-1',
+      'title': 'Menu1-1',
+      'name': 'Menu11Demo',
+      'children': [{
+        'path': '/level/menu1/menu1-1/menu1-1-1',
+        'title': 'Menu1-1-1',
+        'name': 'Menu111Demo'
+      }]
+    }, {
+      'path': '/level/menu1/menu1-2',
+      'title': 'Menu1-2',
+      'name': 'Menu12Demo'
+    }]
+  }, {
+    'path': '/level/menu2',
+    'title': 'Menu2',
+    'name': 'Menu2Demo'
+  }]
+}, {
+  'path': '/level/menu1',
+  'title': 'Menu1',
+  'name': 'Menu1Demo',
+  'children': [{
+    'path': '/level/menu1/menu1-1',
+    'title': 'Menu1-1',
+    'name': 'Menu11Demo',
+    'children': [{
+      'path': '/level/menu1/menu1-1/menu1-1-1',
+      'title': 'Menu1-1-1',
+      'name': 'Menu111Demo'
+    }]
+  }, {
+    'path': '/level/menu1/menu1-2',
+    'title': 'Menu1-2',
+    'name': 'Menu12Demo'
+  }]
+}, {
+  'path': '/level/menu1/menu1-1',
+  'title': 'Menu1-1',
+  'name': 'Menu11Demo',
+  'children': [{
+    'path': '/level/menu1/menu1-1/menu1-1-1',
+    'title': 'Menu1-1-1',
+    'name': 'Menu111Demo'
+  }]
+}, {
+  'path': '/level/menu1/menu1-1/menu1-1-1',
+  'title': 'Menu1-1-1',
+  'name': 'Menu111Demo'
+}, {
+  'path': '/level/menu1/menu1-2',
+  'title': 'Menu1-2',
+  'name': 'Menu12Demo'
+}, {
+  'path': '/level/menu2',
+  'title': 'Menu2',
+  'name': 'Menu2Demo'
+}, {
+  'path': '/example-demo',
+  'title': '综合实例',
+  'name': 'ExampleDemo',
+  'children': [{
+    'path': '/example-demo/example-dialog',
+    'title': '列表综合实例-弹窗',
+    'name': 'ExampleDialog'
+  }, {
+    'path': '/example-demo/example-page',
+    'title': '列表综合实例-页面',
+    'name': 'ExamplePage'
+  }]
+}, {
+  'path': '/example-demo/example-dialog',
+  'title': '列表综合实例-弹窗',
+  'name': 'ExampleDialog'
+}, {
+  'path': '/example-demo/example-page',
+  'title': '列表综合实例-页面',
+  'name': 'ExamplePage'
+}, {
+  'path': '/role-demo',
+  'title': '权限管理',
+  'name': 'RoleDemo',
+  'children': [{
+    'path': '/role-demo/user',
+    'title': '用户管理',
+    'name': 'User'
+  }, {
+    'path': '/role-demo/role',
+    'title': '角色管理',
+    'name': 'Role'
+  }]
+}, {
+  'path': '/role-demo/user',
+  'title': '用户管理',
+  'name': 'User'
+}, {
+  'path': '/role-demo/role',
+  'title': '角色管理',
+  'name': 'Role'
+}]
+
+export const checkedkeys = ['/components-demo', '/components-demo/echarts', '/components-demo/preview',
+  '/components-demo/button', '/components-demo/message', '/components-demo/count-to', '/components-demo/search',
+  '/components-demo/editor', '/components-demo/markdown', '/components-demo/dialog', '/components-demo/more',
+  '/components-demo/detail', '/table-demo', '/table-demo/basic-table', '/table-demo/page-table',
+  '/table-demo/stripe-table', '/table-demo/border-table', '/table-demo/state-table', '/table-demo/fixed-header',
+  '/table-demo/fixed-column', '/table-demo/fixed-column-header', '/table-demo/fluid-height',
+  '/table-demo/multi-header', '/table-demo/single-choice', '/table-demo/multiple-choice', '/table-demo/sort-table',
+  '/table-demo/screen-table', '/table-demo/expand-row', '/table-demo/tree-and-load', '/table-demo/custom-header',
+  '/table-demo/total-table', '/table-demo/merge-table', '/table-demo/custom-index', '/directives-demo',
+  '/directives-demo/clipboard', '/hooks-demo', '/hooks-demo/watermark', '/hooks-demo/useScrollTo', '/icon/index',
+  '/level', '/level/menu1', '/level/menu1/menu1-1', '/level/menu1/menu1-1/menu1-1-1', '/level/menu1/menu1-2',
+  '/level/menu2', '/example-demo', '/example-demo/example-dialog', '/example-demo/example-page', '/role-demo',
+  '/role-demo/user', '/role-demo/role'
+]

+ 97 - 0
mock/role/index.ts

@@ -0,0 +1,97 @@
+import wsCache from '@/cache'
+import { Role } from './types'
+import { checkedNodes, checkedkeys } from './admin-role'
+import { checkedRoleNodes } from './test-role'
+
+let List: Role[] = wsCache.get('roleList') || [
+  {
+    roleName: 'admin',
+    id: '1',
+    checkedNodes: checkedNodes,
+    checkedkeys: checkedkeys
+  },
+  {
+    roleName: 'test',
+    id: '2',
+    checkedNodes: checkedRoleNodes,
+    checkedkeys: []
+  }
+]
+
+export default [
+  // 列表接口
+  {
+    url: 'http://mockjs.test.cn/role/list',
+    type: 'get',
+    response: (config: any) => {
+      const {
+        roleName,
+        pageIndex,
+        pageSize
+      } = config.query
+
+      const mockList = List.filter(item => {
+        if (roleName && item.roleName.indexOf(roleName) < 0) return false
+        return true
+      })
+      const pageList = mockList.filter((item, index) => index < pageSize * pageIndex && index >= pageSize * (pageIndex - 1))
+
+      return {
+        code: '0000',
+        data: {
+          total: mockList.length,
+          list: pageList
+        }
+      }
+    }
+  },
+
+  // 详情接口
+  {
+    url: 'http://mockjs.test.cn/role/detail',
+    type: 'get',
+    response: (config: any) => {
+      const {
+        id
+      } = config.query
+      for (const role of List) {
+        if (role.id === id) {
+          return {
+            code: '0000',
+            data: role
+          }
+        }
+      }
+    }
+  },
+
+  // 保存接口
+  {
+    url: 'http://mockjs.test.cn/role/save',
+    type: 'post',
+    response: (config: any) => {
+      const data = config.body
+      if (!data.id) {
+        List = [data].concat(List)
+        return {
+          code: '0000',
+          data: 'success'
+        }
+      } else {
+        List.map(item => {
+          if (item.id === data.id) {
+            for (const key in item) {
+              item[key] = data[key]
+            }
+          }
+        })
+        // 存在缓存中,避免刷新没有掉
+        wsCache.set('roleList', List)
+        return {
+          code: '0000',
+          data: 'success'
+        }
+      }
+    }
+  }
+]

+ 489 - 0
mock/role/test-role.ts

@@ -0,0 +1,489 @@
+export const checkedRoleNodes = [{
+    path: '/components-demo',
+    component: '#',
+    redirect: '/components-demo/echarts',
+    name: 'ComponentsDemo',
+    meta: {
+      title: '功能组件',
+      icon: 'component',
+      alwaysShow: true
+    },
+    children: [{
+        path: 'echarts',
+        component: 'pages/index/views/components-demo/echarts/index.vue',
+        name: 'EchartsDemo',
+        meta: {
+          title: '图表'
+        }
+      },
+      {
+        path: 'preview',
+        component: 'pages/index/views/components-demo/preview/index.vue',
+        name: 'PreviewDemo',
+        meta: {
+          title: '图片预览'
+        }
+      },
+      {
+        path: 'button',
+        component: 'pages/index/views/components-demo/button/index.vue',
+        name: 'ButtonDemo',
+        meta: {
+          title: '按钮'
+        }
+      },
+      {
+        path: 'message',
+        component: 'pages/index/views/components-demo/message/index.vue',
+        name: 'MessageDemo',
+        meta: {
+          title: '消息提示'
+        }
+      },
+      {
+        path: 'count-to',
+        component: 'pages/index/views/components-demo/count-to/index.vue',
+        name: 'CountToDemo',
+        meta: {
+          title: '数字动画'
+        }
+      },
+      {
+        path: 'search',
+        component: 'pages/index/views/components-demo/search/index.vue',
+        name: 'SearchDemo',
+        meta: {
+          title: '查询'
+        }
+      },
+      {
+        path: 'editor',
+        component: 'pages/index/views/components-demo/editor/index.vue',
+        name: 'EditorDemo',
+        meta: {
+          title: '富文本编辑器'
+        }
+      },
+      {
+        path: 'markdown',
+        component: 'pages/index/views/components-demo/markdown/index.vue',
+        name: 'MarkdownDemo',
+        meta: {
+          title: 'markdown编辑器'
+        }
+      },
+      {
+        path: 'dialog',
+        component: 'pages/index/views/components-demo/dialog/index.vue',
+        name: 'DialogDemo',
+        meta: {
+          title: '弹窗'
+        }
+      },
+      {
+        path: 'more',
+        component: 'pages/index/views/components-demo/more/index.vue',
+        name: 'MoreDemo',
+        meta: {
+          title: '显示更多'
+        }
+      },
+      {
+        path: 'detail',
+        component: 'pages/index/views/components-demo/detail/index.vue',
+        name: 'DetailDemo',
+        meta: {
+          title: '详情组件'
+        }
+      }
+    ]
+  },
+  {
+    path: '/table-demo',
+    component: '#',
+    redirect: '/table-demo/basic-usage',
+    name: 'TableDemo',
+    meta: {
+      title: '表格',
+      icon: 'table',
+      alwaysShow: true
+    },
+    children: [
+      {
+        path: 'basic-table',
+        component: 'pages/index/views/table-demo/basic-table/index.vue',
+        name: 'BasicTable',
+        meta: {
+          title: '基础表格'
+        }
+      },
+      {
+        path: 'page-table',
+        component: 'pages/index/views/table-demo/page-table/index.vue',
+        name: 'PageTable',
+        meta: {
+          title: '分页表格'
+        }
+      },
+      {
+        path: 'stripe-table',
+        component: 'pages/index/views/table-demo/stripe-table/index.vue',
+        name: 'StripeTable',
+        meta: {
+          title: '带斑马纹表格'
+        }
+      },
+      {
+        path: 'border-table',
+        component: 'pages/index/views/table-demo/border-table/index.vue',
+        name: 'BorderTable',
+        meta: {
+          title: '带边框表格'
+        }
+      },
+      {
+        path: 'state-table',
+        component: 'pages/index/views/table-demo/state-table/index.vue',
+        name: 'StateTable',
+        meta: {
+          title: '带状态表格'
+        }
+      },
+      {
+        path: 'fixed-header',
+        component: 'pages/index/views/table-demo/fixed-header/index.vue',
+        name: 'FixedHeader',
+        meta: {
+          title: '固定表头'
+        }
+      },
+      {
+        path: 'fixed-column',
+        component: 'pages/index/views/table-demo/fixed-column/index.vue',
+        name: 'FixedColumn',
+        meta: {
+          title: '固定列'
+        }
+      },
+      {
+        path: 'fixed-column-header',
+        component: 'pages/index/views/table-demo/fixed-column-header/index.vue',
+        name: 'FixedColumnHeader',
+        meta: {
+          title: '固定列和表头'
+        }
+      },
+      {
+        path: 'fluid-height',
+        component: 'pages/index/views/table-demo/fluid-height/index.vue',
+        name: 'FluidHeight',
+        meta: {
+          title: '流体高度'
+        }
+      },
+      {
+        path: 'multi-header',
+        component: 'pages/index/views/table-demo/multi-header/index.vue',
+        name: 'MultiHeader',
+        meta: {
+          title: '多级表头'
+        }
+      },
+      {
+        path: 'single-choice',
+        component: 'pages/index/views/table-demo/single-choice/index.vue',
+        name: 'SingleChoice',
+        meta: {
+          title: '单选'
+        }
+      },
+      {
+        path: 'multiple-choice',
+        component: 'pages/index/views/table-demo/multiple-choice/index.vue',
+        name: 'MultipleChoice',
+        meta: {
+          title: '多选'
+        }
+      },
+      {
+        path: 'sort-table',
+        component: 'pages/index/views/table-demo/sort-table/index.vue',
+        name: 'SortTable',
+        meta: {
+          title: '排序'
+        }
+      },
+      {
+        path: 'screen-table',
+        component: 'pages/index/views/table-demo/screen-table/index.vue',
+        name: 'ScreenTable',
+        meta: {
+          title: '筛选'
+        }
+      },
+      {
+        path: 'expand-row',
+        component: 'pages/index/views/table-demo/expand-row/index.vue',
+        name: 'ExpandRow',
+        meta: {
+          title: '展开行'
+        }
+      },
+      {
+        path: 'tree-and-load',
+        component: 'pages/index/views/table-demo/tree-and-load/index.vue',
+        name: 'TreeAndLoad',
+        meta: {
+          title: '树形数据与懒加载'
+        }
+      },
+      {
+        path: 'custom-header',
+        component: 'pages/index/views/table-demo/custom-header/index.vue',
+        name: 'CustomHeader',
+        meta: {
+          title: '自定义表头'
+        }
+      },
+      {
+        path: 'total-table',
+        component: 'pages/index/views/table-demo/total-table/index.vue',
+        name: 'TotalTable',
+        meta: {
+          title: '表尾合计行'
+        }
+      },
+      {
+        path: 'merge-table',
+        component: 'pages/index/views/table-demo/merge-table/index.vue',
+        name: 'MergeTable',
+        meta: {
+          title: '合并行或列'
+        }
+      },
+      {
+        path: 'custom-index',
+        component: 'pages/index/views/table-demo/custom-index/index.vue',
+        name: 'CustomIndex',
+        meta: {
+          title: '自定义索引'
+        }
+      }
+    ]
+  },
+  {
+    path: '/directives-demo',
+    component: '#',
+    redirect: '/directives-demo/clipboard',
+    name: 'DirectivesDemo',
+    meta: {
+      title: '自定义指令',
+      icon: 'clipboard',
+      alwaysShow: true
+    },
+    children: [{
+      path: 'clipboard',
+      component: 'pages/index/views/directives-demo/clipboard/index.vue',
+      name: 'ClipboardDemo',
+      meta: {
+        title: 'Clipboard'
+      }
+    }]
+  },
+  {
+    path: '/hooks-demo',
+    component: '#',
+    redirect: '/hooks-demo/watermark',
+    name: 'HooksDemo',
+    meta: {
+      title: 'Hooks',
+      icon: 'international',
+      alwaysShow: true
+    },
+    children: [{
+        path: 'watermark',
+        component: 'pages/index/views/hooks-demo/useWatermark/index.vue',
+        name: 'UseWatermarkDemo',
+        meta: {
+          title: 'UseWaterMark'
+        }
+      },
+      {
+        path: 'useScrollTo',
+        component: 'pages/index/views/hooks-demo/useScrollTo/index.vue',
+        name: 'UseScrollToDemo',
+        meta: {
+          title: 'UseScrollTo'
+        }
+      }
+    ]
+  },
+  {
+    path: '/icon',
+    component: '#',
+    name: 'IconsDemo',
+    meta: {
+      title: '图标',
+      icon: 'icon'
+    },
+    children: [{
+      path: 'index',
+      component: 'pages/index/views/icons/index.vue',
+      name: 'Icons',
+      meta: {
+        title: '图标',
+        icon: 'icon'
+      }
+    }]
+  },
+  {
+    path: '/level',
+    component: '#',
+    redirect: '/level/menu1/menu1-1/menu1-1-1',
+    name: 'Level',
+    meta: {
+      title: '多级菜单缓存',
+      icon: 'nested'
+    },
+    children: [{
+        path: 'menu1',
+        name: 'Menu1Demo',
+        component: '##Menu1Demo',
+        redirect: '/level/menu1/menu1-1/menu1-1-1',
+        meta: {
+          title: 'Menu1'
+        },
+        children: [{
+            path: 'menu1-1',
+            name: 'Menu11Demo',
+            component: '##Menu11Demo',
+            redirect: '/level/menu1/menu1-1/menu1-1-1',
+            meta: {
+              title: 'Menu1-1',
+              alwaysShow: true
+            },
+            children: [{
+              path: 'menu1-1-1',
+              name: 'Menu111Demo',
+              component: 'pages/index/views/level/Menu111.vue',
+              meta: {
+                title: 'Menu1-1-1'
+              }
+            }]
+          },
+          {
+            path: 'menu1-2',
+            name: 'Menu12Demo',
+            component: 'pages/index/views/level/Menu12.vue',
+            meta: {
+              title: 'Menu1-2'
+            }
+          }
+        ]
+      },
+      {
+        path: 'menu2',
+        name: 'Menu2Demo',
+        component: 'pages/index/views/level/Menu2.vue',
+        meta: {
+          title: 'Menu2'
+        }
+      }
+    ]
+  },
+  {
+    path: '/example-demo',
+    component: '#',
+    name: 'ExampleDemo',
+    redirect: '/example-demo/example-dialog',
+    meta: {
+      alwaysShow: true,
+      icon: 'example',
+      title: '综合实例'
+    },
+    children: [{
+        path: 'example-dialog',
+        component: 'pages/index/views/example-demo/example-dialog/index.vue',
+        name: 'ExampleDialog',
+        meta: {
+          title: '列表综合实例-弹窗'
+        }
+      },
+      {
+        path: 'example-page',
+        component: 'pages/index/views/example-demo/example-page/index.vue',
+        name: 'ExamplePage',
+        meta: {
+          title: '列表综合实例-页面'
+        }
+      },
+      {
+        path: 'example-add',
+        component: 'pages/index/views/example-demo/example-page/example-add.vue',
+        name: 'ExampleAdd',
+        meta: {
+          title: '列表综合实例-新增',
+          noTagsView: true,
+          noCache: true,
+          hidden: true,
+          showMainRoute: true,
+          activeMenu: '/example-demo/example-page'
+        }
+      },
+      {
+        path: 'example-edit',
+        component: 'pages/index/views/example-demo/example-page/example-edit.vue',
+        name: 'ExampleEdit',
+        meta: {
+          title: '列表综合实例-编辑',
+          noTagsView: true,
+          noCache: true,
+          hidden: true,
+          showMainRoute: true,
+          activeMenu: '/example-demo/example-page'
+        }
+      },
+      {
+        path: 'example-detail',
+        component: 'pages/index/views/example-demo/example-page/example-detail.vue',
+        name: 'ExampleDetail',
+        meta: {
+          title: '列表综合实例-详情',
+          noTagsView: true,
+          noCache: true,
+          hidden: true,
+          showMainRoute: true,
+          activeMenu: '/example-demo/example-page'
+        }
+      }
+    ]
+  },
+  {
+    path: '/role-demo',
+    component: '#',
+    redirect: '/role-demo/user',
+    name: 'RoleDemo',
+    meta: {
+      title: '权限管理',
+      icon: 'user',
+      alwaysShow: true
+    },
+    children: [{
+        path: 'user',
+        component: 'pages/index/views/role-demo/user/index.vue',
+        name: 'User',
+        meta: {
+          title: '用户管理'
+        }
+      },
+      {
+        path: 'role',
+        component: 'pages/index/views/role-demo/role/index.vue',
+        name: 'Role',
+        meta: {
+          title: '角色管理'
+        }
+      }
+    ]
+  }
+]

+ 6 - 0
mock/role/types.ts

@@ -0,0 +1,6 @@
+export interface Role {
+  roleName: String
+  id: String
+  checkedNodes: any[]
+  checkedkeys: any[]
+}

+ 70 - 0
mock/user/index.ts

@@ -0,0 +1,70 @@
+import { User } from './types'
+
+const List: User[] = [
+  {
+    userName: 'admin',
+    password: 'admin',
+    role: 'admin',
+    roleId: '1'
+  },
+  {
+    userName: 'test',
+    password: 'test',
+    role: 'test',
+    roleId: '2'
+  }
+]
+
+export default [
+  // 列表接口
+  {
+    url: 'http://mockjs.test.cn/user/list',
+    type: 'get',
+    response: (config: any) => {
+      const {
+        userName,
+        pageIndex,
+        pageSize
+      } = config.query
+
+      const mockList = List.filter(item => {
+        if (userName && item.userName.indexOf(userName) < 0) return false
+        return true
+      })
+      const pageList = mockList.filter((item, index) => index < pageSize * pageIndex && index >= pageSize * (pageIndex - 1))
+
+      return {
+        code: '0000',
+        data: {
+          total: mockList.length,
+          list: pageList
+        }
+      }
+    }
+  },
+
+  // 登录接口
+  {
+    url: 'http://mockjs.test.cn/user/login',
+    type: 'post',
+    response: (config: any) => {
+      const data = config.body
+      let hasUser = false
+      for (const user of List) {
+        if (user.userName === data.userName && user.password === data.passWord) {
+          hasUser = true
+          return {
+            code: '0000',
+            data: user
+          }
+        }
+      }
+      if (!hasUser) {
+        return {
+          code: '500',
+          message: '账号或密码错误'
+        }
+      }
+    }
+  }
+]

+ 6 - 0
mock/user/types.ts

@@ -0,0 +1,6 @@
+export interface User {
+  userName: String
+  password: String
+  role: String
+  roleId: String
+}

+ 1 - 6
package.json

@@ -20,7 +20,7 @@
     "clipboard": "^2.0.6",
     "core-js": "^3.6.5",
     "echarts": "^4.9.0",
-    "element-plus": "1.0.1-beta.14",
+    "element-plus": "1.0.1-beta.26",
     "highlight.js": "^10.4.0",
     "lodash-es": "^4.17.15",
     "mitt": "^2.1.0",
@@ -93,10 +93,5 @@
     "hooks": {
       "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
     }
-  },
-  "lint-staged": {
-    "*.{js,vue}": [
-      "vue-cli-service lint"
-    ]
   }
 }

+ 0 - 22
src/assets/icons/svgo.yml

@@ -1,22 +0,0 @@
-# replace default config
-
-# multipass: true
-# full: true
-
-plugins:
-
-  # - name
-  #
-  # or:
-  # - name: false
-  # - name: true
-  #
-  # or:
-  # - name:
-  #     param1: 1
-  #     param2: 2
-
-- removeAttrs:
-    attrs:
-      - 'fill'
-      - 'fill-rule'

+ 6 - 4
src/components/Detail/index.vue

@@ -33,12 +33,14 @@
               :class="{'detail__content--flex': !vertical}"
             >
               <div class="content__item--label" :style="labelStyleObj">
-                <slot v-if="item.slots && item.slots.title" :name="item.slots.title" :row="item" />
-                <template v-else>{{ item.label }}</template>
+                <slot :name="item.field" :row="item">
+                  {{ item.label }}
+                </slot>
               </div>
               <div class="content__item--message" :style="messageStyleObj">
-                <slot v-if="item.slots && item.slots.default" :name="item.slots.default" :row="data" />
-                <template v-else>{{ data[item.field] }}</template>
+                <slot :name="`${item.field}Content`" :row="data">
+                  {{ data[item.field] }}
+                </slot>
               </div>
             </div>
           </el-col>

+ 159 - 7
src/components/Dialog/index.vue

@@ -1,16 +1,33 @@
 <template>
   <el-dialog
+    ref="dialogRef"
     v-bind="getBindValue"
+    :fullscreen="fullscreen"
     destroy-on-close
+    lock-scroll
     :close-on-click-modal="false"
     top="10vh"
   >
-    <template v-if="slots.title" #title>
-      <slot name="title" />
+    <template #title>
+      <slot name="title">
+        {{ title }}
+      </slot>
+      <svg-icon
+        v-if="showFullscreen"
+        :icon-class="fullscreen ? 'exit-fullscreen' : 'fullscreen'"
+        class-name="dialog__icon"
+        @click="toggleFull"
+      />
     </template>
 
     <!-- 弹窗内容 -->
-    <el-scrollbar class="com-dialog__content">
+    <el-scrollbar
+      :class="fullscreen && slots.footer
+        ? 'com-dialog__content--footer'
+        : (fullscreen && !slots.footer
+          ? 'com-dialog__content--fullscreen'
+          : 'com-dialog__content')"
+    >
       <div class="content__wrap">
         <slot />
       </div>
@@ -23,24 +40,149 @@
 </template>
 
 <script lang="ts">
-import { defineComponent, computed } from 'vue'
+import { defineComponent, ref, computed, PropType, nextTick, unref } from 'vue'
+import SvgIcon from '@/components/SvgIcon/index.vue'
 export default defineComponent({
   name: 'Dialog',
+  components: {
+    SvgIcon
+  },
+  props: {
+    title: {
+      type: String as PropType<string>,
+      default: ''
+    },
+    // 是否显示全屏按钮
+    showFullscreen: {
+      type: Boolean as PropType<boolean>,
+      default: false
+    },
+    // 是否可以拖拽
+    draggable: {
+      type: Boolean as PropType<boolean>,
+      default: false
+    }
+  },
   setup(props, { slots, attrs }) {
+    const dialogRef = ref<HTMLElement | null>(null)
+
+    const fullscreen = ref<boolean>(false)
+
     const getBindValue = computed((): any => {
-      const bindValue = { ...attrs, ...props }
-      return bindValue
+      const delArr: string[] = ['showFullscreen', 'draggable']
+      const obj = { ...attrs, ...props }
+      for (const key in obj) {
+        if (delArr.indexOf(key) !== -1) {
+          delete obj[key]
+        }
+      }
+      return obj
     })
 
+    function toggleFull(): void {
+      fullscreen.value = !fullscreen.value
+      // 全屏的时候需要重新定义left top
+      if (fullscreen.value && props.draggable) {
+        const dragDom = unref(dialogRef as any).$refs.dialogRef
+        dragDom.style.cssText += `;left:0px;top:0px;`
+      }
+    }
+
+    function initDraggable() {
+      nextTick(() => {
+        const dragDom = unref(dialogRef as any).$refs.dialogRef
+        const dialogHeaderEl = dragDom.querySelector('.el-dialog__header') as HTMLElement
+        dragDom.style.cssText += ';top:0px;'
+        dialogHeaderEl.style.cssText += ';cursor:move;user-select:none;'
+        dialogHeaderEl.onmousedown = (e) => {
+          const disX = e.clientX - dialogHeaderEl.offsetLeft
+          const disY = e.clientY - dialogHeaderEl.offsetTop
+
+          const dragDomWidth = dragDom.offsetWidth
+          const dragDomHeight = dragDom.offsetHeight
+
+          const screenWidth = document.body.clientWidth
+          const screenHeight = document.body.clientHeight
+
+          const minDragDomLeft = dragDom.offsetLeft
+          const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth
+
+          const minDragDomTop = dragDom.offsetTop
+          const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight
+
+          const styleLeftStr = getComputedStyle(dragDom).left
+          const styleTopStr = getComputedStyle(dragDom).top
+          if (!styleLeftStr || !styleTopStr) return
+          let styleLeft: number
+          let styleTop: number
+
+          // Format may be "##%" or "##px"
+          if (styleLeftStr.includes('%')) {
+            styleLeft = +document.body.clientWidth * (+styleLeftStr.replace(/%/g, '') / 100)
+            styleTop = +document.body.clientHeight * (+styleTopStr.replace(/%/g, '') / 100)
+          } else {
+            styleLeft = +styleLeftStr.replace(/px/g, '')
+            styleTop = +styleTopStr.replace(/px/g, '')
+          }
+
+          document.onmousemove = (e) => {
+            let left = e.clientX - disX
+            let top = e.clientY - disY
+
+            // Handle edge cases
+            if (-(left) > minDragDomLeft) {
+              left = -minDragDomLeft
+            } else if (left > maxDragDomLeft) {
+              left = maxDragDomLeft
+            }
+            if (-(top) > minDragDomTop) {
+              top = -minDragDomTop
+            } else if (top > maxDragDomTop) {
+              top = maxDragDomTop
+            }
+
+            // Move current element
+            dragDom.style.cssText += `;left:${left + styleLeft}px;top:${top + styleTop}px;`
+          }
+
+          document.onmouseup = () => {
+            document.onmousemove = null
+            document.onmouseup = null
+          }
+        }
+      })
+    }
+
+    if (props.draggable) {
+      initDraggable()
+    }
+
     return {
+      dialogRef,
+      fullscreen,
       getBindValue,
-      slots
+      slots,
+      toggleFull,
+      initDraggable
     }
   }
 })
 </script>
 
 <style lang="less" scoped>
+.dialog__icon {
+  position: absolute;
+  top: 22px;
+  right: 45px;
+  color: #909399;
+  font-size: 12px;
+  color: #909399;
+  cursor: pointer;
+  transition: color 0.2s;
+  &:hover {
+    color: #409EFF;
+  }
+}
 .com-dialog__content {
   .content__wrap {
     padding-right: 10px;
@@ -50,4 +192,14 @@ export default defineComponent({
 		overflow-x: hidden; // 隐藏横向滚动栏
 	}
 }
+.com-dialog__content--fullscreen {
+  @{deep}(.el-scrollbar__wrap) {
+    height: calc(~"100vh - 46px - 60px"); // 最大高度
+  }
+}
+.com-dialog__content--footer {
+  @{deep}(.el-scrollbar__wrap) {
+    max-height: calc(~"100vh - 46px - 60px - 66px"); // 最大高度
+  }
+}
 </style>

+ 0 - 1
src/components/Search/index.vue

@@ -259,7 +259,6 @@ export default defineComponent({
       try {
         form.validate((valid: boolean) => {
           if (valid) {
-            console.log(valid)
             emit('search-submit', unref(formInline))
           } else {
             console.log('error submit!!')

+ 2 - 2
src/components/Sider/Item.vue

@@ -1,6 +1,6 @@
 <template>
-  <i v-if="icon.includes('el-icon')" :class="[icon, 'sub-el-icon', 'anticon']" />
-  <svg-icon v-else :icon-class="icon" class="anticon" />
+  <i v-if="icon && icon.includes('el-icon')" :class="[icon, 'sub-el-icon', 'anticon']" />
+  <svg-icon v-else-if="icon" :icon-class="icon" class="anticon" />
   <slot name="title">
     <span class="anticon-item">{{ title }}</span>
   </slot>

+ 0 - 5
src/components/Table/components/TableColumn.vue

@@ -34,11 +34,6 @@
               :index="scope.$index"
             />
           </template>
-
-          <!-- 不需要插槽 -->
-          <!-- <span v-if="!item.slots || !item.slots.default">
-            {{ scope.row[item.field] }}
-          </span> -->
         </el-table-column>
       </template>
     </template>

+ 0 - 4
src/components/Table/index.vue

@@ -53,10 +53,6 @@
                 :index="scope.$index"
               />
             </template>
-            <!-- 不需要插槽 -->
-            <!-- <span v-if="!item.slots || !item.slots.default">
-              {{ scope.row[item.field] }}
-            </span> -->
           </el-table-column>
         </template>
       </template>

+ 3 - 1
src/components/UserInfo/index.vue

@@ -25,12 +25,14 @@ import { resetRouter } from '_p/index/router'
 import wsCache from '@/cache'
 import { useRouter } from 'vue-router'
 import { tagsViewStore } from '_p/index/store/modules/tagsView'
+import { appStore } from '_p/index/store/modules/app'
 export default defineComponent({
   name: 'UserInfo',
   setup() {
     const { replace, push } = useRouter()
     async function loginOut(): Promise<void> {
-      wsCache.clear()
+      // wsCache.clear()
+      wsCache.delete(appStore.userInfo)
       await resetRouter() // 重置静态路由表
       await tagsViewStore.delAllViews() // 删除所有的tags标签页
       replace('/login')

+ 11 - 11
src/pages/index/api/index.ts

@@ -1,14 +1,14 @@
-import { EmptyObj } from '@/types/glob'
+// import { EmptyObj } from '@/types/glob'
 
-const modulesFiles: any = require.context('./modules', true, /\.ts$/)
+// const modulesFiles: any = require.context('./modules', true, /\.ts$/)
 
-const modules: EmptyObj = modulesFiles.keys().reduce((modules: EmptyObj, modulePath: string): Object => {
-  const moduleName: string = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
-  const value: EmptyObj = modulesFiles(modulePath)
-  modules[moduleName] = value.default
-  return modules
-}, {})
+// const modules: EmptyObj = modulesFiles.keys().reduce((modules: EmptyObj, modulePath: string): Object => {
+//   const moduleName: string = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
+//   const value: EmptyObj = modulesFiles(modulePath)
+//   modules[moduleName] = value.default
+//   return modules
+// }, {})
 
-export default {
-  ...modules
-}
+// export default {
+//   ...modules
+// }

+ 30 - 0
src/pages/index/router/index.ts

@@ -26,6 +26,7 @@ const Layout = () => import('../layout/index.vue')
     activeMenu: '/dashboard'  显示高亮的路由路径
     followAuth: '/dashboard'  跟随哪个路由进行权限过滤
     showMainRoute: true       设置为true即使hidden为true,也依然可以进行路由跳转(默认 false)
+    followRoute: '/dashboard' 为路由设置跟随其他路由的权限
   }
 **/
 
@@ -573,6 +574,35 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
         }
       }
     ]
+  },
+  {
+    path: '/role-demo',
+    component: Layout,
+    redirect: '/role-demo/user',
+    name: 'RoleDemo',
+    meta: {
+      title: '权限管理',
+      icon: 'user',
+      alwaysShow: true
+    },
+    children: [
+      {
+        path: 'user',
+        component: () => import('_p/index/views/role-demo/user/index.vue'),
+        name: 'User',
+        meta: {
+          title: '用户管理'
+        }
+      },
+      {
+        path: 'role',
+        component: () => import('_p/index/views/role-demo/role/index.vue'),
+        name: 'Role',
+        meta: {
+          title: '角色管理'
+        }
+      }
+    ]
   }
 ]
 

+ 1 - 0
src/pages/index/router/types.d.ts

@@ -12,6 +12,7 @@ export interface RouteMeta {
   noTagsView?: boolean
   followAuth?: string
   showMainRoute?: boolean
+  followRoute?: string
 }
 
 export interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {

+ 79 - 6
src/pages/index/store/modules/permission.ts

@@ -4,6 +4,15 @@ import { deepClone } from '@/utils'
 import store from '../index'
 import { VuexModule, getModule, Module, Mutation, Action } from 'vuex-module-decorators'
 import { AppRouteRecordRaw } from '_p/index/router/types'
+import wsCache from '@/cache'
+import { isExternal } from '@/utils/validate'
+import path from 'path'
+import { getParentLayout } from '_p/index/router/utils'
+
+import { appStore } from '_p/index/store/modules/app'
+
+/* Layout */
+const Layout = () => import('_p/index/layout/index.vue')
 
 export interface PermissionState {
   routers: AppRouteRecordRaw[]
@@ -19,8 +28,7 @@ class Permission extends VuexModule implements PermissionState {
 
   @Mutation
   private SET_ROUTERS(routers: AppRouteRecordRaw[]): void {
-    // const flatRoutes: AppRouteRecordRaw[] = getFlatRoutes(deepClone(asyncRouterMap, ['component']))
-    // const flatRoutes: AppRouteRecordRaw[] = deepClone(asyncRouterMap, ['component'])
+    // 动态路由,404一定要放到最后面
     this.addRouters = routers.concat([{
       path: '/:path(.*)*',
       redirect: '/404',
@@ -30,6 +38,7 @@ class Permission extends VuexModule implements PermissionState {
         breadcrumb: false
       }
     }])
+    // 渲染菜单的所有路由
     this.routers = deepClone(constantRouterMap, ['component']).concat(routers)
   }
   @Mutation
@@ -40,7 +49,16 @@ class Permission extends VuexModule implements PermissionState {
   @Action
   public GenerateRoutes(): Promise<unknown> {
     return new Promise(resolve => {
-      const routerMap: AppRouteRecordRaw[] = generateRoutes(deepClone(asyncRouterMap, ['component']))
+      // 路由权限控制
+      let routerMap: AppRouteRecordRaw[] = []
+      if (wsCache.get(appStore.userInfo).roleName === 'admin') {
+        // 模拟前端控制权限
+        routerMap = generateRoutes(deepClone(asyncRouterMap, ['component']))
+      } else {
+        // 模拟后端控制权限
+        routerMap = getFilterRoutes(wsCache.get(appStore.userInfo).checkedNodes)
+      }
+      // const routerMap: AppRouteRecordRaw[] = generateRoutes(deepClone(asyncRouterMap, ['component']))
       this.SET_ROUTERS(routerMap)
       resolve()
     })
@@ -52,7 +70,7 @@ class Permission extends VuexModule implements PermissionState {
 }
 
 // 路由过滤,主要用于权限控制
-function generateRoutes(routes: AppRouteRecordRaw[]): AppRouteRecordRaw[] {
+function generateRoutes(routes: AppRouteRecordRaw[], basePath = '/'): AppRouteRecordRaw[] {
   const res: AppRouteRecordRaw[] = []
 
   for (const route of routes) {
@@ -61,12 +79,37 @@ function generateRoutes(routes: AppRouteRecordRaw[]): AppRouteRecordRaw[] {
       continue
     }
 
+    let onlyOneChild = null
+
+    if (route.children && route.children.length === 1 && !route.meta.alwaysShow) {
+      onlyOneChild = isExternal(route.children[0].path)
+        ? route.children[0].path
+        : path.resolve(path.resolve(basePath, route.path), route.children[0].path)
+    }
+
     let data: any = null
-    data = Object.assign({}, route)
+
+    // 如不需要路由权限,可注释以下逻辑
+    // 权限过滤,通过获取登录信息里面的角色权限,动态的渲染菜单。
+    const list = wsCache.get(appStore.userInfo).checkedNodes
+    // 开发者可以根据实际情况进行扩展
+    for (const item of list) {
+      // 通过路径去匹配
+      if (isExternal(item.path) && (onlyOneChild === item.path || route.path === item.path)) {
+        data = Object.assign({}, route)
+      } else {
+        const routePath = path.resolve(basePath, onlyOneChild || route.path)
+        if (routePath === item.path || (route.meta && route.meta.followRoute === item.path)) {
+          data = Object.assign({}, route)
+        }
+      }
+    }
+    // 如不需要路由权限,解注释下面一行
+    // data = Object.assign({}, route)
 
     // recursive child routes
     if (route.children && data) {
-      data.children = generateRoutes(route.children)
+      data.children = generateRoutes(route.children, path.resolve(basePath, data.path))
     }
     if (data) {
       res.push(data as AppRouteRecordRaw)
@@ -75,4 +118,34 @@ function generateRoutes(routes: AppRouteRecordRaw[]): AppRouteRecordRaw[] {
   return res
 }
 
+// 模拟后端过滤路由
+function getFilterRoutes(routes: any[]): any[] {
+  const res = []
+
+  for (const route of routes) {
+    const data: any = {
+      path: route.path,
+      name: route.name,
+      redirect: route.redirect
+    }
+    data.meta = Object.assign({}, route.meta || {}, { title: route.title })
+    if (route.component) {
+      // 动态加载路由文件,可根据实际情况进行自定义逻辑
+      data.component = route.component === '#'
+        ? Layout
+        : (route.component.includes('##')
+          ? getParentLayout(route.component.split('##')[1])
+          : () => new Promise((resolve) => {
+            resolve(import(`${route.component}`))
+          }))
+    }
+    // recursive child routes
+    if (route.children) {
+      data.children = getFilterRoutes(route.children)
+    }
+    res.push(data)
+  }
+  return res
+}
+
 export const permissionStore = getModule<Permission>(Permission)

+ 10 - 30
src/pages/index/views/components-demo/detail/index.vue

@@ -62,7 +62,7 @@
         <template #title="scope">
           <span class="is-required-item">{{ scope.row.label }}</span>
         </template>
-        <template #titleDefault>
+        <template #titleContent>
           <el-form-item prop="title">
             <el-input v-model="form.title" placeholder="请输入标题" />
           </el-form-item>
@@ -71,7 +71,7 @@
         <template #author="scope">
           <span class="is-required-item">{{ scope.row.label }}</span>
         </template>
-        <template #authorDefault>
+        <template #authorContent>
           <el-form-item prop="author">
             <el-input v-model="form.author" placeholder="请输入作者" />
           </el-form-item>
@@ -80,7 +80,7 @@
         <template #time="scope">
           <span class="is-required-item">{{ scope.row.label }}</span>
         </template>
-        <template #timeDefault>
+        <template #timeContent>
           <el-form-item prop="display_time">
             <el-date-picker
               v-model="form.display_time"
@@ -94,7 +94,7 @@
         <template #importance="scope">
           <span class="is-required-item">{{ scope.row.label }}</span>
         </template>
-        <template #importanceDefault>
+        <template #importanceContent>
           <el-form-item prop="importance">
             <el-select v-model="form.importance" placeholder="请选择重要性" style="width: 100%;">
               <el-option label="重要" value="3" />
@@ -107,7 +107,7 @@
         <template #pageviews="scope">
           <span class="is-required-item">{{ scope.row.label }}</span>
         </template>
-        <template #pageviewsDefault>
+        <template #pageviewsContent>
           <el-form-item prop="pageviews">
             <el-input-number
               v-model="form.pageviews"
@@ -172,43 +172,23 @@ const fromSchema: any[] = [
   {
     field: 'title',
     label: '标题',
-    span: 24,
-    slots: {
-      title: 'title',
-      default: 'titleDefault'
-    }
+    span: 24
   },
   {
     field: 'author',
-    label: '作者',
-    slots: {
-      title: 'author',
-      default: 'authorDefault'
-    }
+    label: '作者'
   },
   {
     field: 'display_time',
-    label: '创建时间',
-    slots: {
-      title: 'time',
-      default: 'timeDefault'
-    }
+    label: '创建时间'
   },
   {
     field: 'importance',
-    label: '重要性',
-    slots: {
-      title: 'importance',
-      default: 'importanceDefault'
-    }
+    label: '重要性'
   },
   {
     field: 'pageviews',
-    label: '阅读数',
-    slots: {
-      title: 'pageviews',
-      default: 'pageviewsDefault'
-    }
+    label: '阅读数'
   }
 ]
 

+ 1 - 1
src/pages/index/views/example-demo/example-dialog/components/Detail.vue

@@ -82,7 +82,7 @@ export default defineComponent({
               id: id
             }
           })
-          if (res.code === '0000') {
+          if (res.code) {
             for (const key in form) {
               if (key === 'importance') {
                 form[key] = res.data[key].toString()

+ 2 - 2
src/pages/index/views/example-demo/example-dialog/components/InfoWrite.vue

@@ -118,7 +118,7 @@ export default defineComponent({
               id: id
             }
           })
-          if (res.code === '0000') {
+          if (res) {
             for (const key in form) {
               if (key === 'importance') {
                 form[key] = res.data[key].toString()
@@ -148,7 +148,7 @@ export default defineComponent({
             const res = await setExampApi({
               data: formData
             })
-            if (res.code === '0000') {
+            if (res) {
               Message.success(form.id ? '编辑成功' : '新增成功')
               emit('success', form.id ? 'edit' : 'add')
             }

+ 2 - 2
src/pages/index/views/example-demo/example-dialog/index.vue

@@ -154,7 +154,7 @@ export default defineComponent({
         const res = await getExampleListApi({
           params: Object.assign(defalutParams, data || {})
         })
-        if (res.code === '0000') {
+        if (res.code) {
           total.value = res.data.total
           tableData.value = res.data.list
         }
@@ -205,7 +205,7 @@ export default defineComponent({
         const res = await delsExampApi({
           data: { ids }
         })
-        if (res.code === '0000') {
+        if (res.code) {
           Message.success('删除成功!')
           getExampleList()
         }

+ 1 - 1
src/pages/index/views/example-demo/example-page/components/Detail.vue

@@ -84,7 +84,7 @@ export default defineComponent({
               id: id
             }
           })
-          if (res.code === '0000') {
+          if (res.code) {
             for (const key in form) {
               if (key === 'importance') {
                 form[key] = res.data[key].toString()

+ 2 - 2
src/pages/index/views/example-demo/example-page/components/InfoWrite.vue

@@ -121,7 +121,7 @@ export default defineComponent({
               id: id
             }
           })
-          if (res.code === '0000') {
+          if (res) {
             for (const key in form) {
               if (key === 'importance') {
                 form[key] = res.data[key].toString()
@@ -151,7 +151,7 @@ export default defineComponent({
             const res = await setExampApi({
               data: formData
             })
-            if (res.code === '0000') {
+            if (res) {
               Message.success(form.id ? '编辑成功' : '新增成功')
               emit('success', form.id ? 'edit' : 'add')
             }

+ 2 - 2
src/pages/index/views/example-demo/example-page/index.vue

@@ -131,7 +131,7 @@ export default defineComponent({
         const res = await getExampleListApi({
           params: Object.assign(defalutParams, data || {})
         })
-        if (res.code === '0000') {
+        if (res) {
           total.value = res.data.total
           tableData.value = res.data.list
         }
@@ -182,7 +182,7 @@ export default defineComponent({
         const res = await delsExampApi({
           data: { ids }
         })
-        if (res.code === '0000') {
+        if (res) {
           Message.success('删除成功!')
           getExampleList()
         }

+ 14 - 0
src/pages/index/views/login/api.ts

@@ -0,0 +1,14 @@
+import { fetch } from '_p/index/axios-config/axios'
+
+interface PropsData {
+  params?: any
+  data?: any
+}
+
+export const loginApi = ({ data }: PropsData) => {
+  return fetch({ url: '/user/login', method: 'post', data })
+}
+
+export const getRoleDetApi = ({ params }: PropsData) => {
+  return fetch({ url: '/role/detail', method: 'get', params })
+}

+ 34 - 12
src/pages/index/views/login/index.vue

@@ -14,7 +14,7 @@
           <el-form-item prop="userName">
             <el-input
               v-model="form.userName"
-              placeholder="请输入账号"
+              placeholder="请输入账号 admin or test"
               class="form--input"
             >
               <template #prefix>
@@ -30,7 +30,7 @@
               show-password
               :minlength="3"
               :maxlength="18"
-              placeholder="请输入密码"
+              placeholder="请输入密码 admin or test"
               class="form--input"
             >
               <template #prefix>
@@ -63,6 +63,9 @@ import type { RouteRecordRaw } from 'vue-router'
 import { permissionStore } from '_p/index/store/modules/permission'
 import { appStore } from '_p/index/store/modules/app'
 import wsCache from '@/cache'
+import { ElNotification } from 'element-plus'
+
+import { loginApi, getRoleDetApi } from './api'
 
 interface FormModule {
   userName: string,
@@ -88,8 +91,8 @@ export default defineComponent({
       immediate: true
     })
     const form = reactive<FormModule>({
-      userName: 'admin',
-      passWord: 'admin'
+      userName: '',
+      passWord: ''
     })
     const rules = reactive<RulesModule>({
       userName: [{ required: true, message: '请输入账号' }],
@@ -100,16 +103,28 @@ export default defineComponent({
       if (!formWrap) return
       loading.value = true
       try {
-        formWrap.validate((valid: boolean) => {
+        formWrap.validate(async(valid: boolean) => {
           if (valid) {
-            permissionStore.GenerateRoutes().then(() => {
-              permissionStore.addRouters.forEach(async(route: RouteRecordRaw) => {
-                await addRoute(route.name!, route) // 动态添加可访问路由表
+            // 模拟登录接口之后返回角色信息
+            const res = await loginApi({ data: form })
+            if (res) {
+              // 获取权限信息
+              const role = await getRoleDetApi({
+                params: {
+                  id: res.data.roleId
+                }
               })
-              wsCache.set(appStore.userInfo, form)
-              permissionStore.SetIsAddRouters(true)
-              push({ path: redirect.value || '/' })
-            })
+              if (role) {
+                wsCache.set(appStore.userInfo, Object.assign(form, role.data))
+                permissionStore.GenerateRoutes().then(() => {
+                  permissionStore.addRouters.forEach(async(route: RouteRecordRaw) => {
+                    await addRoute(route.name!, route) // 动态添加可访问路由表
+                  })
+                  permissionStore.SetIsAddRouters(true)
+                  push({ path: redirect.value || '/' })
+                })
+              }
+            }
           } else {
             console.log('error submit!!')
             return false
@@ -121,6 +136,13 @@ export default defineComponent({
         loading.value = false
       }
     }
+
+    ElNotification({
+      title: '提示',
+      message: '账号 admin 为 前端 控制路由权限,账号 test 为 后端 控制路由权限。密码与账号相同',
+      duration: 60000
+    })
+
     return {
       loginForm,
       loading, redirect, form, rules,

+ 18 - 0
src/pages/index/views/role-demo/role/api.ts

@@ -0,0 +1,18 @@
+import { fetch } from '_p/index/axios-config/axios'
+
+interface PropsData {
+  params?: any
+  data?: any
+}
+
+export const getRoleListApi = ({ params }: PropsData) => {
+  return fetch({ url: '/role/list', method: 'get', params })
+}
+
+export const setRoleApi = ({ data }: PropsData) => {
+  return fetch({ url: '/role/save', method: 'post', data })
+}
+
+export const getRoleDetApi = ({ params }: PropsData) => {
+  return fetch({ url: '/role/detail', method: 'get', params })
+}

+ 230 - 0
src/pages/index/views/role-demo/role/components/InfoWrite.vue

@@ -0,0 +1,230 @@
+<template>
+  <div>
+    <el-form
+      ref="formRef"
+      :model="form"
+      :rules="rules"
+      label-width="100px"
+    >
+      <el-row>
+        <el-col :span="24">
+          <el-form-item prop="roleName" label="角色名">
+            <el-input v-model="form.roleName" disabled placeholder="请输入角色名" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item label="角色权限">
+            <el-tree
+              ref="tree"
+              :check-strictly="false"
+              :data="routesData"
+              :props="defaultProps"
+              show-checkbox
+              accordion
+              node-key="path"
+              highlight-current
+              class="permission-tree"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <div class="dialong__button--wrap">
+      <el-button @click="close">取消</el-button>
+      <el-button :loading="subLoading" type="primary" @click="setListData">保存</el-button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import path from 'path'
+import { defineComponent, PropType, ref, reactive, nextTick, unref } from 'vue'
+import { setRoleApi, getRoleDetApi } from '../api'
+import { asyncRouterMap } from '_p/index/router'
+import { AppRouteRecordRaw } from '_p/index/router/types'
+import { isExternal } from '@/utils/validate'
+import { Message } from '_c/Message'
+
+const requiredRule = {
+  required: true,
+  message: '该项为必填项'
+}
+
+interface Form {
+  id: String
+  roleName: String
+  checkedNodes: any[]
+  checkedkeys: any[]
+}
+
+interface Rules {
+  roleName: any[]
+}
+
+interface DefaultProps {
+  children: String
+  label: String
+}
+
+export default defineComponent({
+  name: 'InfoWrite',
+  props: {
+    info: {
+      type: Object as PropType<object>,
+      default: () => null
+    }
+  },
+  emits: ['success', 'close'],
+  setup(props, { emit }) {
+    const tree = ref<HTMLElement | null>(null)
+    const formRef = ref<HTMLElement | null>(null)
+
+    const subLoading = ref<boolean>(false)
+
+    let form = reactive<Form>({
+      id: '', // id
+      roleName: '', // 角色名
+      checkedNodes: [], // 被选中的节点
+      checkedkeys: [] // 被选中的keys
+    })
+
+    const rules = reactive<Rules>({
+      roleName: [requiredRule]
+    })
+
+    const defaultProps = reactive<DefaultProps>({
+      children: 'children',
+      label: 'title'
+    })
+
+    const routesData = ref<any[]>(generateRoutes([...asyncRouterMap]))
+
+    async function getDet(): Promise<void> {
+      if (props.info) {
+        const id = (props.info as any).id
+        try {
+          const res = await getRoleDetApi({
+            params: {
+              id: id
+            }
+          })
+          if (res) {
+            const formData = {}
+            for (const key in form) {
+              formData[key] = res.data[key]
+            }
+            form = Object.assign(form, formData)
+            nextTick(() => {
+              const treeRef = unref(tree as any)
+              treeRef.setCheckedKeys(form.checkedkeys)
+            })
+          }
+        } catch (e) {
+          console.log(e)
+        }
+      }
+    }
+
+    // 新增或者编辑
+    function setListData() {
+      try {
+        subLoading.value = true
+        const formRefWrap = unref(formRef as any)
+        formRefWrap.validate(async(valid: boolean) => {
+          if (valid) {
+            const treeRef = unref(tree as any)
+            // 获取所有被选中节点,由于是前端渲染,所以只要保存一维数组就行
+            form.checkedNodes = treeRef.getCheckedNodes(false, true)
+            console.log(JSON.stringify(form.checkedNodes))
+            // 获取所有被选中的keys,便于渲染是否选中
+            form.checkedkeys = treeRef.getCheckedKeys()
+            console.log(JSON.stringify(form.checkedkeys))
+            const res = await setRoleApi({
+              data: form
+            })
+            if (res) {
+              Message.success(form.id ? '编辑成功,请重新退出登录后查看效果' : '新增成功,请重新退出登录后查看效果')
+              emit('success', form.id ? 'edit' : 'add')
+            }
+          } else {
+            console.log('error submit!!')
+            return false
+          }
+        })
+      } catch (err) {
+        console.log(err)
+      } finally {
+        subLoading.value = false
+      }
+    }
+
+    function close() {
+      emit('close')
+    }
+
+    function generateRoutes(routes: AppRouteRecordRaw[], basePath = '/') {
+      const res: any[] = []
+
+      for (let route of routes) {
+        // skip some route
+        if (route.meta.hidden) { continue }
+
+        const oneShowingChild = onlyOneShowingChild(route.children, route, path.resolve(basePath, route.path))
+
+        if (route.children && oneShowingChild && !route.meta.alwaysShow) {
+          route = oneShowingChild
+        }
+
+        const data = {
+          path: isExternal(route.path) ? route.path : path.resolve(basePath, route.path),
+          title: route.meta && route.meta.title,
+          name: route.name
+        }
+        // recursive child routes
+        if (route.children) {
+          (data as any).children = generateRoutes(route.children, data.path)
+        }
+        res.push(data)
+      }
+      return res
+    }
+
+    function onlyOneShowingChild(children: AppRouteRecordRaw[] = [], parent: AppRouteRecordRaw, basePath: string) {
+      let onlyOneChild = null
+      const showingChildren = children.filter(item => !item.meta.hidden)
+      // When there is only one child route, the child route is displayed by default
+      if (showingChildren.length === 1) {
+        onlyOneChild = showingChildren[0]
+        onlyOneChild.path = isExternal(onlyOneChild.path) ? onlyOneChild.path : path.resolve(basePath, onlyOneChild.path)
+        return onlyOneChild
+      }
+
+      // Show parent if there are no child route to display
+      if (showingChildren.length === 0) {
+        onlyOneChild = { ...parent, path: '', noShowingChildren: true }
+        return onlyOneChild
+      }
+
+      return false
+    }
+
+    getDet()
+
+    return {
+      formRef,
+      routesData,
+      tree,
+      subLoading,
+      form,
+      rules,
+      defaultProps,
+      getDet,
+      close,
+      setListData
+    }
+  }
+})
+</script>
+
+<style>
+</style>

+ 259 - 0
src/pages/index/views/role-demo/role/components/InfoWrite2.vue

@@ -0,0 +1,259 @@
+<template>
+  <div>
+    <el-form
+      ref="formRef"
+      :model="form"
+      :rules="rules"
+      label-width="130px"
+    >
+      <el-row>
+        <el-col :span="24">
+          <el-form-item prop="roleName" label="角色名">
+            <el-input v-model="form.roleName" disabled placeholder="请输入角色名" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="角色权限">
+            <el-tree
+              ref="tree"
+              :check-strictly="false"
+              :expand-on-click-node="false"
+              :data="routesData"
+              :props="defaultProps"
+              accordion
+              node-key="path"
+              highlight-current
+              class="permission-tree"
+              @node-click="handleNodeClick"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col v-if="seletTreeData" :span="12">
+          <el-form-item label="title">
+            <el-input v-model="seletTreeData.title" />
+          </el-form-item>
+          <el-form-item label="component">
+            <el-input v-model="seletTreeData.component" />
+          </el-form-item>
+          <el-form-item label="redirect">
+            <el-input v-model="seletTreeData.redirect" />
+          </el-form-item>
+          <el-form-item label="activeMenu">
+            <el-input v-model="seletTreeData.meta.activeMenu" />
+          </el-form-item>
+          <el-form-item label="name">
+            <el-input v-model="seletTreeData.name" />
+          </el-form-item>
+          <el-form-item label="icon">
+            <el-input v-model="seletTreeData.meta.icon" />
+          </el-form-item>
+          <el-form-item label="hidden">
+            <el-switch v-model="seletTreeData.meta.hidden" />
+          </el-form-item>
+          <el-form-item label="alwaysShow">
+            <el-switch v-model="seletTreeData.meta.alwaysShow" />
+          </el-form-item>
+          <el-form-item label="noCache">
+            <el-switch v-model="seletTreeData.meta.noCache" />
+          </el-form-item>
+          <el-form-item label="breadcrumb">
+            <el-switch v-model="seletTreeData.meta.breadcrumb" />
+          </el-form-item>
+          <el-form-item label="affix">
+            <el-switch v-model="seletTreeData.meta.affix" />
+          </el-form-item>
+          <el-form-item label="noTagsView">
+            <el-switch v-model="seletTreeData.meta.noTagsView" />
+          </el-form-item>
+          <el-form-item label="showMainRoute">
+            <el-switch v-model="seletTreeData.meta.showMainRoute" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <div class="dialong__button--wrap">
+      <el-button @click="close">取消</el-button>
+      <el-button :loading="subLoading" type="primary" @click="setListData">保存</el-button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { setRoleApi, getRoleDetApi } from '../api'
+import { defineComponent, PropType, ref, reactive, nextTick, unref } from 'vue'
+import { AppRouteRecordRaw } from '_p/index/router/types'
+import { isExternal } from '@/utils/validate'
+import { Message } from '_c/Message'
+
+const requiredRule = {
+  required: true,
+  message: '该项为必填项'
+}
+
+interface Form {
+  id: String
+  roleName: String
+  checkedNodes: any[]
+  checkedkeys: any[]
+}
+
+interface Rules {
+  roleName: any[]
+}
+
+interface DefaultProps {
+  children: String
+  label: String
+}
+
+export default defineComponent({
+  name: 'InfoWrite2',
+  props: {
+    info: {
+      type: Object as PropType<object>,
+      default: () => null
+    }
+  },
+  emits: ['success', 'close'],
+  setup(props, { emit }) {
+    const tree = ref<HTMLElement | null>(null)
+    const formRef = ref<HTMLElement | null>(null)
+
+    const subLoading = ref<boolean>(false)
+
+    let form = reactive<Form>({
+      id: '', // id
+      roleName: '', // 角色名
+      checkedNodes: [], // 被选中的节点
+      checkedkeys: [] // 被选中的keys
+    })
+
+    const rules = reactive<Rules>({
+      roleName: [requiredRule]
+    })
+
+    const defaultProps = reactive<DefaultProps>({
+      children: 'children',
+      label: 'title'
+    })
+
+    const routesData = ref<any[]>([])
+
+    const seletTreeData = ref<any>(null) // 选中的菜单
+
+    async function getDet(): Promise<void> {
+      if (props.info) {
+        const id = (props.info as any).id
+        try {
+          const res = await getRoleDetApi({
+            params: {
+              id: id
+            }
+          })
+          if (res) {
+            const formData = {}
+            for (const key in form) {
+              formData[key] = res.data[key]
+            }
+            form = Object.assign(form, formData)
+            routesData.value = generateRoutes(form.checkedNodes)
+            nextTick(() => {
+              const treeRef = unref(tree as any)
+              treeRef.setCheckedKeys(form.checkedkeys)
+            })
+          }
+        } catch (e) {
+          console.log(e)
+        }
+      }
+    }
+
+    // 新增或者编辑
+    function setListData() {
+      try {
+        subLoading.value = true
+        const formRefWrap = unref(formRef as any)
+        formRefWrap.validate(async(valid: boolean) => {
+          if (valid) {
+            const res = await setRoleApi({
+              data: Object.assign(form, { checkedNodes: routesData })
+            })
+            if (res) {
+              Message.success(form.id ? '编辑成功,请重新退出登录后查看效果' : '新增成功,请重新退出登录后查看效果')
+              emit('success', form.id ? 'edit' : 'add')
+            }
+          } else {
+            console.log('error submit!!')
+            return false
+          }
+        })
+      } catch (err) {
+        console.log(err)
+      } finally {
+        subLoading.value = false
+      }
+    }
+
+    // 树形点击
+    function handleNodeClick(data: any) {
+      seletTreeData.value = data
+    }
+
+    function close() {
+      emit('close')
+    }
+
+    function generateRoutes(routes: any[]) {
+      const res: any[] = []
+
+      for (const route of routes) {
+        const data: any = {
+          path: route.path,
+          title: route.title || (route.meta && route.meta.title),
+          name: route.name,
+          redirect: route.redirect || '',
+          component: route.component || '',
+          meta: {
+            hidden: route.meta && route.meta.hidden,
+            alwaysShow: route.meta && route.meta.alwaysShow,
+            icon: route.meta && route.meta.icon,
+            noCache: route.meta && route.meta.noCache,
+            breadcrumb: route.meta && route.meta.breadcrumb,
+            affix: route.meta && route.meta.affix,
+            noTagsView: route.meta && route.meta.noTagsView,
+            activeMenu: route.meta && route.meta.activeMenu,
+            showMainRoute: route.meta && route.meta.showMainRoute
+          }
+        }
+        // recursive child routes
+        if (route.children) {
+          data.children = generateRoutes(route.children)
+        }
+        res.push(data)
+      }
+      return res
+    }
+
+    getDet()
+
+    return {
+      formRef,
+      routesData,
+      tree,
+      subLoading,
+      form,
+      rules,
+      defaultProps,
+      getDet,
+      handleNodeClick,
+      close,
+      setListData,
+      seletTreeData
+    }
+  }
+})
+
+</script>
+
+<style>
+</style>

+ 213 - 0
src/pages/index/views/role-demo/role/index.vue

@@ -0,0 +1,213 @@
+<template>
+  <div>
+    <el-alert
+      effect="dark"
+      :closable="false"
+      title="由于是模拟数据,所以只提供了两种不同权限的角色,开发者可根据实际情况自行改造结合。"
+      type="info"
+      style="margin-bottom: 20px;"
+    />
+
+    <div class="search__example--wrap">
+      <com-search
+        :data="searchData"
+        @search-submit="searchSubmit"
+        @reset-submit="resetSubmit"
+      />
+    </div>
+
+    <com-table
+      v-loading="loading"
+      :columns="columns"
+      :data="tableData"
+      :pagination="{
+        currentPage: defalutParams.pageIndex,
+        total: total,
+        onSizeChange: handleSizeChange,
+        onCurrentChange: handleCurrentChange
+      }"
+    >
+      <template #remark="scope">
+        <span>模拟</span>
+        <el-tag
+          :type="scope.row.roleName === 'admin' ? 'success' : 'warning'"
+          style="margin: 0 15px;"
+        >{{ scope.row.roleName === 'admin' ? '前端' : '后端' }}</el-tag>
+        <span>角色</span>
+      </template>
+
+      <template #action="scope">
+        <el-button type="primary" size="mini" @click="open(scope.row)">编辑</el-button>
+      </template>
+    </com-table>
+
+    <com-dialog v-model="dialogVisible" :title="title">
+      <info-write
+        v-if="comName === 'InfoWrite' && dialogVisible"
+        :info="info"
+        @close="toggleVisible"
+        @success="success"
+      />
+      <info-write2
+        v-if="comName === 'InfoWrite2' && dialogVisible"
+        :info="info"
+        @close="toggleVisible"
+        @success="success"
+      />
+    </com-dialog>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue'
+
+import { useExample } from '@/hooks/useExample'
+
+import { getRoleListApi } from './api'
+
+import InfoWrite from './components/InfoWrite.vue'
+import InfoWrite2 from './components/InfoWrite2.vue'
+
+const searchData = [
+  {
+    label: '角色名',
+    value: '',
+    itemType: 'input',
+    field: 'roleName',
+    placeholder: '请输入角色名',
+    clearable: true
+  }
+]
+
+const columns = [
+  {
+    field: 'roleName',
+    label: '角色名'
+  },
+  {
+    label: '备注',
+    slots: {
+      default: 'remark'
+    }
+  },
+  {
+    field: 'action',
+    label: '操作',
+    width: '80px',
+    slots: {
+      default: 'action'
+    }
+  }
+]
+
+export default defineComponent({
+  name: 'Role',
+  components: {
+    InfoWrite,
+    InfoWrite2
+  },
+  setup() {
+    const info = ref<any>(null)
+
+    const {
+      defalutParams,
+      tableData,
+      loading,
+      total,
+      dialogVisible,
+      title,
+      currentChange,
+      sizeChange,
+      comName,
+      toggleVisible
+    } = useExample()
+
+    // 请求数据
+    async function getRoleList(data?: any): Promise<void> {
+      try {
+        const res = await getRoleListApi({
+          params: Object.assign(defalutParams, data || {})
+        })
+        if (res) {
+          total.value = res.data.total
+          tableData.value = res.data.list
+        }
+      } finally {
+        loading.value = false
+      }
+    }
+
+    // 查询
+    function searchSubmit(data: any) {
+      // 该方法重置了一些默认参数
+      currentChange(1)
+      getRoleList(data)
+    }
+
+    // 重置
+    function resetSubmit(data: any) {
+      // 该方法重置了一些默认参数
+      currentChange(1)
+      getRoleList(data)
+    }
+
+    // 展示多少条
+    function handleSizeChange(val: number) {
+      // 该方法重置了一些默认参数
+      sizeChange(val)
+      getRoleList()
+    }
+
+    // 展示第几页
+    function handleCurrentChange(val: number) {
+      // 该方法重置了一些默认参数
+      currentChange(val)
+      getRoleList()
+    }
+
+    // 打开弹窗
+    function open(row: any) {
+      comName.value = row.roleName === 'admin' ? 'InfoWrite' : 'InfoWrite2'
+      title.value = !row ? '新增' : '编辑'
+      info.value = row || null
+      toggleVisible(true)
+    }
+
+    // 成功之后的回调
+    function success(type: string) {
+      if (type === 'add') {
+        currentChange(1)
+      }
+      toggleVisible()
+      getRoleList()
+    }
+
+    getRoleList()
+
+    return {
+      searchData,
+      columns,
+      info,
+      defalutParams,
+      tableData,
+      loading,
+      total,
+      dialogVisible,
+      title,
+      currentChange,
+      sizeChange,
+      comName,
+      toggleVisible,
+      searchSubmit,
+      resetSubmit,
+      handleSizeChange,
+      handleCurrentChange,
+      open,
+      success
+    }
+  }
+})
+</script>
+
+<style>
+</style>

+ 10 - 0
src/pages/index/views/role-demo/user/api.ts

@@ -0,0 +1,10 @@
+import { fetch } from '_p/index/axios-config/axios'
+
+interface PropsData {
+  params?: any
+  data?: any
+}
+
+export const getUserListApi = ({ params }: PropsData): any => {
+  return fetch({ url: '/user/list', method: 'get', params })
+}

+ 159 - 0
src/pages/index/views/role-demo/user/index.vue

@@ -0,0 +1,159 @@
+<template>
+  <div>
+    <el-alert
+      effect="dark"
+      :closable="false"
+      title="由于是模拟数据,所以只提供了两种不同权限的帐号,开发者可根据实际情况自行改造结合。"
+      type="info"
+      style="margin-bottom: 20px;"
+    />
+
+    <div class="search__example--wrap">
+      <com-search
+        :data="searchData"
+        @search-submit="searchSubmit"
+        @reset-submit="resetSubmit"
+      />
+    </div>
+
+    <com-table
+      v-loading="loading"
+      :columns="columns"
+      :data="tableData"
+      :pagination="{
+        currentPage: defalutParams.pageIndex,
+        total: total,
+        onSizeChange: handleSizeChange,
+        onCurrentChange: handleCurrentChange
+      }"
+    >
+      <template #remark="scope">
+        <span>模拟</span>
+        <el-tag
+          :type="scope.row.userName === 'admin' ? 'success' : 'warning'"
+          style="margin: 0 15px;"
+        >{{ scope.row.userName === 'admin' ? '前端' : '后端' }}</el-tag>
+        <span>控制路由权限</span>
+      </template>
+    </com-table>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+import { useExample } from '@/hooks/useExample'
+
+import { getUserListApi } from './api'
+
+const searchData = [
+  {
+    label: '帐号',
+    value: '',
+    itemType: 'input',
+    field: 'userName',
+    placeholder: '请输入帐号',
+    clearable: true
+  }
+]
+
+const columns = [
+  {
+    field: 'userName',
+    label: '帐号'
+  },
+  {
+    field: 'password',
+    label: '密码'
+  },
+  {
+    field: 'role',
+    label: '角色'
+  },
+  {
+    label: '备注',
+    slots: {
+      default: 'remark'
+    }
+  }
+]
+
+export default defineComponent({
+  name: 'User',
+  setup() {
+    const {
+      defalutParams,
+      tableData,
+      loading,
+      total,
+      title,
+      currentChange,
+      sizeChange
+    } = useExample()
+
+    // 请求数据
+    async function getUserList(data?: any): Promise<void> {
+      try {
+        const res = await getUserListApi({
+          params: Object.assign(defalutParams, data || {})
+        })
+        if (res) {
+          total.value = res.data.total
+          tableData.value = res.data.list
+        }
+      } finally {
+        loading.value = false
+      }
+    }
+
+    // 查询
+    function searchSubmit(data: any) {
+      // 该方法重置了一些默认参数
+      currentChange(1)
+      getUserList(data)
+    }
+
+    // 重置
+    function resetSubmit(data: any) {
+      // 该方法重置了一些默认参数
+      currentChange(1)
+      getUserList(data)
+    }
+
+    // 展示多少条
+    function handleSizeChange(val: number) {
+      // 该方法重置了一些默认参数
+      sizeChange(val)
+      getUserList()
+    }
+
+    // 展示第几页
+    function handleCurrentChange(val: number) {
+      // 该方法重置了一些默认参数
+      currentChange(val)
+      getUserList()
+    }
+
+    getUserList()
+
+    return {
+      searchData,
+      columns,
+      defalutParams,
+      tableData,
+      loading,
+      total,
+      title,
+      currentChange,
+      sizeChange,
+      searchSubmit,
+      resetSubmit,
+      handleSizeChange,
+      handleCurrentChange
+    }
+  }
+})
+</script>
+
+<style>
+</style>

+ 5 - 0
src/styles/element-plus.less

@@ -0,0 +1,5 @@
+// 新版element-plus样式问题,先手动修复,后面更新版本在看看会不会出问题
+.el-overlay {
+  overflow: hidden !important;
+  text-align: left;
+}

+ 1 - 0
src/styles/index.less

@@ -1,4 +1,5 @@
 @import '~element-plus/lib/theme-chalk/index.css';
+@import  './element-plus.less';
 @import './transition.less';
 @import './sider.less';
 @import './glob.less';

+ 1 - 1
src/utils/validate.ts

@@ -10,7 +10,7 @@ export function isEmail(path: any): boolean {
 
 // 验证手机
 export function isPhone(tel: any): boolean {
-  return /^[1][3,4,5,7,8][0-9]{9}$/.test(tel)
+  return /^[1][3,4,5,6,7,8,9][0-9]{9}$/.test(tel)
 }
 
 // 验证身份证号

+ 4 - 4
yarn.lock

@@ -3954,10 +3954,10 @@ electron-to-chromium@^1.3.621:
   resolved "https://registry.npm.taobao.org/electron-to-chromium/download/electron-to-chromium-1.3.622.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Felectron-to-chromium%2Fdownload%2Felectron-to-chromium-1.3.622.tgz#9726bd2e67a5462154750ce9701ca6af07d07877"
   integrity sha1-lya9LmelRiFUdQzpcBymrwfQeHc=
 
-element-plus@1.0.1-beta.14:
-  version "1.0.1-beta.14"
-  resolved "https://registry.yarnpkg.com/element-plus/-/element-plus-1.0.1-beta.14.tgz#67e6742ef0a380156d306d519d474220f8c3e03e"
-  integrity sha512-iqc8lAmj4yYTVQFlxwm5IWj3vxufgmF8FVwKgEKJfy1qQQVqA34R81IgywQpYh3jO/d+ofmHXhsm+z3ojXVp0A==
+element-plus@1.0.1-beta.26:
+  version "1.0.1-beta.26"
+  resolved "https://registry.yarnpkg.com/element-plus/-/element-plus-1.0.1-beta.26.tgz#f24181aab2569b62ca01e63541209b70d524b8ab"
+  integrity sha512-nFzkn31AlZ+bXjnAAeXRoewesC57fI6yaUbxZaE+f1maj9ll5dbtiwk6petJhYvjQ3si4fN3A9P/qZA3ZPeyMg==
   dependencies:
     "@popperjs/core" "^2.4.4"
     async-validator "^3.4.0"