mirror of
https://github.com/chansee97/nova-admin.git
synced 2025-04-06 03:57:54 +08:00
feat: add menu management
This commit is contained in:
parent
790ca63cb0
commit
d490260758
@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { icons } from './icons'
|
import { icons } from './icons'
|
||||||
|
|
||||||
const currentIcon = ref('')
|
const value = defineModel('value', { type: String })
|
||||||
const searchValue = ref('')
|
const searchValue = ref('')
|
||||||
const showPopover = ref(false)
|
const showPopover = ref(false)
|
||||||
|
|
||||||
@ -10,7 +10,7 @@ const { t } = useI18n()
|
|||||||
const iconList = computed(() => icons.filter(item => item.includes(searchValue.value)))
|
const iconList = computed(() => icons.filter(item => item.includes(searchValue.value)))
|
||||||
|
|
||||||
function handleSelectIcon(icon: string) {
|
function handleSelectIcon(icon: string) {
|
||||||
currentIcon.value = icon
|
value.value = icon
|
||||||
showPopover.value = false
|
showPopover.value = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -18,9 +18,9 @@ function handleSelectIcon(icon: string) {
|
|||||||
<template>
|
<template>
|
||||||
<n-popover v-model:show="showPopover" placement="bottom" trigger="click">
|
<n-popover v-model:show="showPopover" placement="bottom" trigger="click">
|
||||||
<template #trigger>
|
<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>
|
<template #suffix>
|
||||||
<nova-icon :icon="currentIcon || 'icon-park-outline:all-application'" />
|
<nova-icon :icon="value || 'icon-park-outline:all-application'" />
|
||||||
</template>
|
</template>
|
||||||
</n-input>
|
</n-input>
|
||||||
</template>
|
</template>
|
||||||
|
5
src/service/api/system.ts
Normal file
5
src/service/api/system.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { request } from '../http'
|
||||||
|
|
||||||
|
export function fetchAllRoutes() {
|
||||||
|
return request.Get<Service.ResponseResult<AppRoute.RowRoute[]> >('/getUserRoutes')
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
export * from './api/list'
|
export * from './api/system'
|
||||||
export * from './api/login'
|
export * from './api/login'
|
||||||
|
export * from './api/list'
|
||||||
export * from './api/test'
|
export * from './api/test'
|
||||||
|
@ -100,7 +100,7 @@ export const useTabStore = defineStore('tab-store', {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
strategies: [
|
strategies: [
|
||||||
{
|
{
|
||||||
storage: localStorage,
|
storage: sessionStorage,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
6
src/typings/route.d.ts
vendored
6
src/typings/route.d.ts
vendored
@ -1,4 +1,6 @@
|
|||||||
declare namespace AppRoute {
|
declare namespace AppRoute {
|
||||||
|
|
||||||
|
type MenuType = 'dir' | 'page'
|
||||||
/** 单个路由所携带的meta标识 */
|
/** 单个路由所携带的meta标识 */
|
||||||
interface RouteMeta {
|
interface RouteMeta {
|
||||||
/* 页面标题,通常必选。 */
|
/* 页面标题,通常必选。 */
|
||||||
@ -17,14 +19,14 @@ declare namespace AppRoute {
|
|||||||
order?: number
|
order?: number
|
||||||
/* 嵌套外链 */
|
/* 嵌套外链 */
|
||||||
herf?: string
|
herf?: string
|
||||||
/** 当前路由需要选中的菜单项,用于跳转至不在左侧菜单显示的路由且需要高亮某个菜单的情况 */
|
/** 当前路由不在左侧菜单显示,但需要需要高亮某个菜单的情况 */
|
||||||
activeMenu?: string
|
activeMenu?: string
|
||||||
/** 当前路由是否会被添加到Tab中 */
|
/** 当前路由是否会被添加到Tab中 */
|
||||||
withoutTab?: boolean
|
withoutTab?: boolean
|
||||||
/** 当前路由是否会被固定在Tab中,用于一些常驻页面 */
|
/** 当前路由是否会被固定在Tab中,用于一些常驻页面 */
|
||||||
pinTab?: boolean
|
pinTab?: boolean
|
||||||
/** 当前路由在左侧菜单是目录还是页面,不设置默认为page */
|
/** 当前路由在左侧菜单是目录还是页面,不设置默认为page */
|
||||||
menuType?: 'dir' | 'page'
|
menuType?: MenuType
|
||||||
}
|
}
|
||||||
|
|
||||||
interface baseRoute {
|
interface baseRoute {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
import { NIcon } from 'naive-ui'
|
import { NIcon } from 'naive-ui'
|
||||||
|
|
||||||
export function renderIcon(icon: string) {
|
export function renderIcon(icon: string, props?: import('naive-ui').IconProps) {
|
||||||
return () => h(NIcon, null, { default: () => h(Icon, { icon }) })
|
return () => h(NIcon, props, { default: () => h(Icon, { icon }) })
|
||||||
}
|
}
|
||||||
|
255
src/views/setting/menu/components/TableModal.vue
Normal file
255
src/views/setting/menu/components/TableModal.vue
Normal 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>
|
@ -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>
|
<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>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user