feat: 菜单管理

This commit is contained in:
kailong321200875 2023-08-05 17:43:24 +08:00
parent 28d0785be8
commit c72b3a33aa
16 changed files with 681 additions and 33 deletions

246
mock/menu/index.ts Normal file
View File

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

View File

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

View File

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

5
src/api/menu/index.ts Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

5
types/router.d.ts vendored
View File

@ -27,9 +27,9 @@ import { defineComponent } from 'vue'
activeMenu: '/dashboard' activeMenu: '/dashboard'
followAuth: '/dashboard'
canTo: true true即使hidden为true( false) canTo: true true即使hidden为true( false)
permission: ['edit','add', 'delete']
} }
**/ **/
declare module 'vue-router' { declare module 'vue-router' {
@ -45,6 +45,7 @@ declare module 'vue-router' {
noTagsView?: boolean noTagsView?: boolean
followAuth?: string followAuth?: string
canTo?: boolean canTo?: boolean
permission?: string[]
} }
} }