feat: add menu management

This commit is contained in:
chansee97 2024-04-11 21:36:54 +08:00
parent 790ca63cb0
commit d490260758
8 changed files with 412 additions and 15 deletions

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { icons } from './icons'
const currentIcon = ref('')
const value = defineModel('value', { type: String })
const searchValue = ref('')
const showPopover = ref(false)
@ -10,7 +10,7 @@ const { t } = useI18n()
const iconList = computed(() => icons.filter(item => item.includes(searchValue.value)))
function handleSelectIcon(icon: string) {
currentIcon.value = icon
value.value = icon
showPopover.value = false
}
</script>
@ -18,9 +18,9 @@ function handleSelectIcon(icon: string) {
<template>
<n-popover v-model:show="showPopover" placement="bottom" trigger="click">
<template #trigger>
<n-input v-model:value="currentIcon" readonly :placeholder="t('components.iconSelector.inputPlaceholder')">
<n-input :value="value" readonly :placeholder="t('components.iconSelector.inputPlaceholder')">
<template #suffix>
<nova-icon :icon="currentIcon || 'icon-park-outline:all-application'" />
<nova-icon :icon="value || 'icon-park-outline:all-application'" />
</template>
</n-input>
</template>

View File

@ -0,0 +1,5 @@
import { request } from '../http'
export function fetchAllRoutes() {
return request.Get<Service.ResponseResult<AppRoute.RowRoute[]> >('/getUserRoutes')
}

View File

@ -1,3 +1,4 @@
export * from './api/list'
export * from './api/system'
export * from './api/login'
export * from './api/list'
export * from './api/test'

View File

@ -100,7 +100,7 @@ export const useTabStore = defineStore('tab-store', {
enabled: true,
strategies: [
{
storage: localStorage,
storage: sessionStorage,
},
],
},

View File

@ -1,7 +1,9 @@
declare namespace AppRoute {
type MenuType = 'dir' | 'page'
/** 单个路由所携带的meta标识 */
interface RouteMeta {
/* 页面标题,通常必选。 */
/* 页面标题,通常必选。 */
title: string
/* 图标,一般配合菜单使用 */
icon?: string
@ -17,14 +19,14 @@ declare namespace AppRoute {
order?: number
/* 嵌套外链 */
herf?: string
/** 当前路由需要选中的菜单项,用于跳转至不在左侧菜单显示的路由且需要高亮某个菜单的情况 */
/** 当前路由不在左侧菜单显示,但需要需要高亮某个菜单的情况 */
activeMenu?: string
/** 当前路由是否会被添加到Tab中 */
withoutTab?: boolean
/** 当前路由是否会被固定在Tab中,用于一些常驻页面 */
pinTab?: boolean
/** 当前路由在左侧菜单是目录还是页面,不设置默认为page */
menuType?: 'dir' | 'page'
menuType?: MenuType
}
interface baseRoute {

View File

@ -1,6 +1,6 @@
import { Icon } from '@iconify/vue'
import { NIcon } from 'naive-ui'
export function renderIcon(icon: string) {
return () => h(NIcon, null, { default: () => h(Icon, { icon }) })
export function renderIcon(icon: string, props?: import('naive-ui').IconProps) {
return () => h(NIcon, props, { default: () => h(Icon, { icon }) })
}

View File

@ -0,0 +1,255 @@
<script setup lang="ts">
import { useLoading } from '@/hooks'
interface Props {
modalName?: string
allRoutes: AppRoute.RowRoute[]
}
const props = withDefaults(defineProps<Props>(), {
modalName: '',
})
const emit = defineEmits<{
open: []
close: []
}>()
const defaultFormModal: AppRoute.RowRoute = {
'name': '',
'path': '',
'id': undefined,
'pid': null,
'meta.title': '',
'meta.icon': '',
'meta.requiresAuth': true,
'meta.roles': [],
'meta.keepAlive': false,
'meta.hide': false,
'meta.order': undefined,
'meta.herf': undefined,
'meta.activeMenu': undefined,
'meta.withoutTab': true,
'meta.pinTab': false,
'meta.menuType': 'page',
}
const formModel = ref()
type ModalType = 'add' | 'view' | 'edit'
const modalType = shallowRef<ModalType>('add')
const modalTitle = computed(() => {
const titleMap: Record<ModalType, string> = {
add: '添加',
view: '查看',
edit: '编辑',
}
return `${titleMap[modalType.value]}${props.modalName}`
})
const modalVisible = ref(false)
async function openModal(type: ModalType = 'add', data: AppRoute.RowRoute) {
emit('open')
modalType.value = type
modalVisible.value = true
const handlers = {
async add() {
formModel.value = { ...defaultFormModal }
},
async view() {
if (data)
formModel.value = { ...data }
},
async edit() {
if (data)
formModel.value = { ...data }
},
}
await handlers[type]()
}
const { loading: submitLoading, startLoading, endLoading } = useLoading(false)
function closeModal() {
modalVisible.value = false
endLoading()
emit('close')
}
defineExpose({
openModal,
})
const formRef = ref()
async function submitModal() {
const handlers = {
async add() {
return new Promise((resolve) => {
setTimeout(() => {
window.$message.success('模拟新增成功')
resolve(true)
}, 2000)
})
},
async edit() {
return new Promise((resolve) => {
setTimeout(() => {
window.$message.success('模拟编辑成功')
resolve(true)
}, 2000)
})
},
async view() {
return true
},
}
await formRef.value?.validate()
startLoading()
await handlers[modalType.value]() && closeModal()
}
const dirTreeOptions = computed(() => {
return filterDirectory(JSON.parse(JSON.stringify(props.allRoutes)))
})
function filterDirectory(node: any[]) {
return node.filter((item) => {
if (item.children) {
const childDir = filterDirectory(item.children)
if (childDir.length > 0)
item.children = childDir
else
Reflect.deleteProperty(item, 'children')
}
return (item['meta.menuType'] === 'dir')
})
}
const rules = {
'name': {
required: true,
message: '请输入菜单名称',
trigger: 'blur',
},
'path': {
required: true,
message: '请输入菜单路径',
trigger: 'blur',
},
'componentPath': {
required: true,
message: '请输入组件路径',
trigger: 'blur',
},
'meta.title': {
required: true,
message: '请输入菜单标题',
trigger: 'blur',
},
}
const options = [
{
label: 'super',
value: 'super',
},
{
label: 'admin',
value: 'admin',
},
{
label: 'user',
value: 'user',
},
]
</script>
<template>
<n-modal
v-model:show="modalVisible" :mask-closable="false" preset="card" :title="modalTitle" class="w-700px"
:segmented="{
content: true,
action: true,
}"
>
<n-form
ref="formRef" :rules="rules" label-placement="left" :model="formModel" label-align="left" :label-width="100"
:disabled="modalType === 'view'"
>
<n-grid :cols="2" :x-gap="18">
<n-form-item-grid-item :span="2" label="父级目录" path="pid">
<n-tree-select
v-model:value="formModel.pid" filterable clearable :options="dirTreeOptions" key-field="id" label-field="meta.title"
children-field="children" placeholder="请选择父级目录"
/>
</n-form-item-grid-item>
<n-form-item-grid-item :span="1" label="菜单名称" path="name">
<n-input v-model:value="formModel.name" />
</n-form-item-grid-item>
<n-form-item-grid-item :span="1" label="标题" path="meta.title">
<n-input v-model:value="formModel['meta.title']" />
</n-form-item-grid-item>
<n-form-item-grid-item :span="2" label="路径" path="path">
<n-input v-model:value="formModel.path" />
</n-form-item-grid-item>
<n-form-item-grid-item :span="1" label="菜单类型" path="meta.menuType">
<n-radio-group v-model:value="formModel['meta.menuType']" name="radiogroup">
<n-space>
<n-radio value="dir">
目录
</n-radio>
<n-radio value="page">
页面
</n-radio>
</n-space>
</n-radio-group>
</n-form-item-grid-item>
<n-form-item-grid-item :span="1" label="图标" path="meta.icon">
<icon-select v-model:value="formModel['meta.icon']" />
</n-form-item-grid-item>
<n-form-item-grid-item v-if="formModel['meta.menuType'] === 'page'" :span="2" label="组件路径" path="componentPath">
<n-input v-model:value="formModel.componentPath" />
</n-form-item-grid-item>
<n-form-item-grid-item :span="1" label="菜单排序" path="meta.order">
<n-input-number v-model:value="formModel['meta.order']" />
</n-form-item-grid-item>
<n-form-item-grid-item :span="1" label="外链页面" path="meta.herf">
<n-input v-model:value="formModel['meta.herf']" />
</n-form-item-grid-item>
<n-form-item-grid-item :span="1" label="登录访问" path="meta.requiresAuth">
<n-switch v-model:value="formModel['meta.requiresAuth']" />
</n-form-item-grid-item>
<n-form-item-grid-item :span="1" label="页面缓存" path="meta.keepAlive">
<n-switch v-model:value="formModel['meta.keepAlive']" />
</n-form-item-grid-item>
<n-form-item-grid-item :span="1" label="标签栏可见" path="meta.withoutTab">
<n-switch v-model:value="formModel['meta.withoutTab']" />
</n-form-item-grid-item>
<n-form-item-grid-item :span="1" label="常驻标签栏" path="meta.pinTab">
<n-switch v-model:value="formModel['meta.pinTab']" />
</n-form-item-grid-item>
<n-form-item-grid-item :span="1" label="侧边菜单隐藏" path="meta.hide">
<n-switch v-model:value="formModel['meta.hide']" />
</n-form-item-grid-item>
<n-form-item-grid-item v-if="formModel['meta.hide']" :span="2" label="高亮菜单" path="meta.activeMenu">
<n-input v-model:value="formModel['meta.activeMenu']" />
</n-form-item-grid-item>
<n-form-item-grid-item :span="2" label="访问角色" path="meta.roles">
<n-select v-model:value="formModel['meta.roles']" multiple filterable :options="options" />
</n-form-item-grid-item>
</n-grid>
</n-form>
<template v-if="modalType !== 'view'" #action>
<n-space justify="center">
<n-button @click="closeModal">
取消
</n-button>
<n-button type="primary" :loading="submitLoading" @click="submitModal">
提交
</n-button>
</n-space>
</template>
</n-modal>
</template>
<style scoped></style>

View File

@ -1,7 +1,141 @@
<script setup lang="ts"></script>
<script setup lang="tsx">
import type { DataTableColumns } from 'naive-ui'
import { NButton, NPopconfirm, NSpace, NTag } from 'naive-ui'
import TableModal from './components/TableModal.vue'
import { fetchAllRoutes } from '@/service'
import { useLoading } from '@/hooks'
import { arrayToTree, renderIcon } from '@/utils'
const { loading, startLoading, endLoading } = useLoading(false)
function deleteData(id: number) {
window.$message.success(`删除菜单id:${id}`)
}
const tableModalRef = ref()
const columns: DataTableColumns<AppRoute.RowRoute> = [
{
title: '名称',
key: 'name',
width: 200,
},
{
title: '图标',
align: 'center',
key: 'meta.icon',
render: (row) => {
return row['meta.icon'] && renderIcon(row['meta.icon'], { size: 20 })()
},
},
{
title: '标题',
align: 'center',
width: 200,
key: 'meta.title',
ellipsis: {
tooltip: true,
},
},
{
title: '路径',
key: 'path',
width: 300,
ellipsis: {
tooltip: true,
},
},
{
title: '组件路径',
key: 'componentPath',
width: 300,
ellipsis: {
tooltip: true,
},
render: (row) => {
return row.componentPath || '-'
},
},
{
title: '排序值',
key: 'meta.order',
align: 'center',
},
{
title: '菜单类型',
align: 'center',
key: 'meta.menuType',
render: (row) => {
const menuType = row['meta.menuType'] || 'page'
const menuTagType: Record<AppRoute.MenuType, NaiveUI.ThemeColor> = {
dir: 'primary',
page: 'warning',
}
return <NTag type={menuTagType[menuType]}>{menuType}</NTag>
},
},
{
title: '操作',
align: 'center',
key: 'actions',
render: (row) => {
const rowData = row as unknown as CommonList.UserList
return (
<NSpace justify="center">
<NButton
size="small"
onClick={() => tableModalRef.value.openModal('view', rowData)}
>
查看
</NButton>
<NButton
size="small"
onClick={() => tableModalRef.value.openModal('edit', rowData)}
>
编辑
</NButton>
<NPopconfirm onPositiveClick={() => deleteData(rowData.id)}>
{{
default: () => '确认删除',
trigger: () => <NButton size="small">删除</NButton>,
}}
</NPopconfirm>
</NSpace>
)
},
},
]
const tableData = ref<AppRoute.RowRoute[]>([])
onMounted(() => {
getAllRoutes()
})
async function getAllRoutes() {
startLoading()
const { data } = await fetchAllRoutes()
tableData.value = arrayToTree(data)
endLoading()
}
</script>
<template>
<div>菜单设置</div>
<n-card>
<n-flex vertical>
<div class="flex gap-4">
<NButton type="primary" @click="tableModalRef.openModal('add')">
<template #icon>
<icon-park-outline-add-one />
</template>
新建
</NButton>
</div>
<n-data-table
:row-key="(row:AppRoute.RowRoute) => row.id" :columns="columns" :data="tableData"
:loading="loading"
/>
<TableModal ref="tableModalRef" :all-routes="tableData" modal-name="菜单" />
</n-flex>
</n-card>
</template>
<style scoped></style>