feat: 🎸 权限管理开发中

This commit is contained in:
chenkl 2021-01-15 17:27:24 +08:00
parent 8edb2a3493
commit 6d7ea6694d
40 changed files with 2353 additions and 135 deletions

View File

@ -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']
}

View File

@ -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
mock/role/admin-role.ts Normal file
View File

@ -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
mock/role/index.ts Normal file
View File

@ -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
mock/role/test-role.ts Normal file
View File

@ -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
mock/role/types.ts Normal file
View File

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

70
mock/user/index.ts Normal file
View File

@ -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
mock/user/types.ts Normal file
View File

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

View File

@ -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"
]
}
}

View File

@ -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'

View File

@ -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>

View File

@ -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>

View File

@ -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!!')

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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')

View File

@ -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
// }

View File

@ -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: '角色管理'
}
}
]
}
]

View File

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

View File

@ -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)

View File

@ -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: '阅读数'
}
]

View File

@ -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()

View File

@ -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')
}

View File

@ -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()
}

View File

@ -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()

View File

@ -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')
}

View File

@ -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()
}

View File

@ -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 })
}

View File

@ -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,

View File

@ -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 })
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 })
}

View File

@ -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>

View File

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

View File

@ -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';

View File

@ -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)
}
// 验证身份证号

View File

@ -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"