feat: update layout configuration to use naive-ui/AppLayout
This commit is contained in:
@ -15,9 +15,11 @@ const themeConfig = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-config-provider :theme="themeConfig">
|
||||
<RouterView />
|
||||
</a-config-provider>
|
||||
<n-config-provider preflight-style-disabled>
|
||||
<a-config-provider :theme="themeConfig">
|
||||
<RouterView />
|
||||
</a-config-provider>
|
||||
</n-config-provider>
|
||||
|
||||
<DynamicDialog /> <ConfirmDialog /> <Toast />
|
||||
</template>
|
||||
|
204
src/layouts/naive-ui/AppLayout.vue
Normal file
204
src/layouts/naive-ui/AppLayout.vue
Normal file
@ -0,0 +1,204 @@
|
||||
<script setup lang="ts">
|
||||
import type { MenuOption } from 'naive-ui';
|
||||
|
||||
import { createGetRoutes } from '@/plugins/router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 侧边栏折叠状态
|
||||
const collapsed = ref(false);
|
||||
|
||||
// 菜单项类型定义
|
||||
type MenuItemWithRoute = MenuOption & {
|
||||
routeName?: string;
|
||||
parentId?: string;
|
||||
originalPath?: string;
|
||||
};
|
||||
|
||||
// 生成菜单项
|
||||
const menuOptions = computed(() => {
|
||||
let flatArray: MenuItemWithRoute[] = createGetRoutes(router)()
|
||||
.filter((route) => !route.path.includes('/:'))
|
||||
.filter((route) => !route.meta.hidden)
|
||||
.map((route) => ({
|
||||
key: route.path,
|
||||
label: route.meta.title || `${(route.name as string) || route.path}`,
|
||||
routeName: route.name as string,
|
||||
}));
|
||||
|
||||
flatArray = flatArray.map((item) => {
|
||||
const originalPath = item.key as string; // 保存原始路径
|
||||
let id = item.key as string;
|
||||
if (flatArray.some((item) => (item.key as string).startsWith(`${id}/`))) {
|
||||
id = `${id}/index`;
|
||||
}
|
||||
// 去掉最前面的 /
|
||||
id = id.replace(/^\//, '');
|
||||
|
||||
let parentId = id.replace(/\/[^/]+$/, '');
|
||||
if (parentId === id) {
|
||||
parentId = '_ROOT_';
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
key: id,
|
||||
parentId,
|
||||
originalPath, // 保存原始路径用于后续映射
|
||||
};
|
||||
});
|
||||
|
||||
const groupItems: MenuItemWithRoute[] = [];
|
||||
for (const flatArrayItem of flatArray) {
|
||||
if (!groupItems.some((item) => item.key === flatArrayItem.parentId) && flatArrayItem.parentId !== '_ROOT_') {
|
||||
let groupItemParentId = flatArrayItem.parentId!.replace(/\/[^/]+$/, '');
|
||||
if (groupItemParentId === flatArrayItem.parentId) groupItemParentId = '_ROOT_';
|
||||
groupItems.push({
|
||||
key: flatArrayItem.parentId!,
|
||||
label: `Group ${flatArrayItem.parentId}`,
|
||||
parentId: groupItemParentId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const tree = arrayToTree([...flatArray, ...groupItems], {
|
||||
id: 'key',
|
||||
parentId: 'parentId',
|
||||
rootId: '_ROOT_',
|
||||
});
|
||||
|
||||
// 递归转换树形结构为 naive-ui menu 格式
|
||||
function convertToMenuOptions(tree: MenuItemWithRoute[]): MenuOption[] {
|
||||
return tree.map((item) => {
|
||||
const menuItem: MenuOption = {
|
||||
key: item.key,
|
||||
label: item.label,
|
||||
};
|
||||
|
||||
if (item.children && item.children.length > 0) {
|
||||
menuItem.children = convertToMenuOptions(item.children);
|
||||
} else if (item.routeName) {
|
||||
// 叶子节点,存储路由映射
|
||||
menuRouteMap.set(item.key as string, item.routeName);
|
||||
// 同时存储路径到 key 的映射(用于高亮显示)
|
||||
if (item.originalPath) {
|
||||
pathToKeyMap.set(item.originalPath, item.key as string);
|
||||
}
|
||||
(menuItem as MenuItemWithRoute).routeName = item.routeName;
|
||||
}
|
||||
|
||||
return menuItem;
|
||||
});
|
||||
}
|
||||
|
||||
// 清空之前的映射
|
||||
menuRouteMap.clear();
|
||||
pathToKeyMap.clear();
|
||||
const result = convertToMenuOptions(tree);
|
||||
|
||||
// 菜单生成后,重新设置当前选中的菜单项
|
||||
nextTick(() => {
|
||||
const currentPath = router.currentRoute.value.path;
|
||||
const menuKey = pathToKeyMap.get(currentPath);
|
||||
if (menuKey) {
|
||||
selectedKey.value = menuKey;
|
||||
} else {
|
||||
const pathWithoutSlash = currentPath.replace(/^\//, '');
|
||||
selectedKey.value = pathWithoutSlash;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
// 当前选中的菜单项
|
||||
const selectedKey = ref<string>();
|
||||
|
||||
// 存储菜单项与路由名称的映射
|
||||
const menuRouteMap = new Map<string, string>();
|
||||
|
||||
// 存储路由路径与菜单 key 的映射(用于高亮显示)
|
||||
const pathToKeyMap = new Map<string, string>();
|
||||
|
||||
// 处理菜单点击
|
||||
const handleMenuSelect = (key: string, item: MenuOption) => {
|
||||
const routeName = menuRouteMap.get(key) || (item as MenuItemWithRoute).routeName;
|
||||
if (routeName) {
|
||||
router.push({ name: routeName as never });
|
||||
}
|
||||
};
|
||||
|
||||
// 监听路由变化,更新选中的菜单项
|
||||
watch(
|
||||
() => router.currentRoute.value.path,
|
||||
(newPath) => {
|
||||
// 使用路径到 key 的映射来找到对应的菜单项
|
||||
const menuKey = pathToKeyMap.get(newPath);
|
||||
if (menuKey) {
|
||||
selectedKey.value = menuKey;
|
||||
} else {
|
||||
// 如果没有找到精确匹配,尝试去掉前面的 / 再匹配
|
||||
const pathWithoutSlash = newPath.replace(/^\//, '');
|
||||
selectedKey.value = pathWithoutSlash;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 切换侧边栏折叠状态
|
||||
const toggleCollapsed = () => {
|
||||
collapsed.value = !collapsed.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-layout has-sider>
|
||||
<n-layout-sider
|
||||
:collapsed="collapsed"
|
||||
:native-scrollbar="false"
|
||||
bordered
|
||||
collapse-mode="width"
|
||||
:collapsed-width="64"
|
||||
:width="240"
|
||||
show-trigger
|
||||
@collapse="collapsed = true"
|
||||
@expand="collapsed = false"
|
||||
>
|
||||
<n-menu
|
||||
:collapsed="collapsed"
|
||||
:collapsed-width="64"
|
||||
:collapsed-icon-size="22"
|
||||
:options="menuOptions"
|
||||
:value="selectedKey"
|
||||
@update:value="handleMenuSelect"
|
||||
/>
|
||||
</n-layout-sider>
|
||||
|
||||
<n-layout>
|
||||
<n-layout-header bordered style="height: 64px; padding: 0 24px; display: flex; align-items: center">
|
||||
<n-button quaternary @click="toggleCollapsed" style="margin-right: 12px">
|
||||
<template #icon>
|
||||
<n-icon size="18">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path v-if="collapsed" fill="currentColor" d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" />
|
||||
<path
|
||||
v-else
|
||||
fill="currentColor"
|
||||
d="M3 18h13v-2H3v2zm0-5h10v-2H3v2zm0-7v2h13V6H3zm18 9.59L17.42 12 21 8.41 19.59 7l-5 5 5 5L21 15.59z"
|
||||
/>
|
||||
</svg>
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<span style="font-size: 18px; font-weight: 500">Vue TS Example</span>
|
||||
</n-layout-header>
|
||||
|
||||
<n-layout-content content-style="padding: 24px;">
|
||||
<router-view />
|
||||
</n-layout-content>
|
||||
|
||||
<n-layout-footer bordered style="padding: 24px; text-align: center">
|
||||
<span style="color: var(--n-text-color-disabled)"> © 2025 Vue TS Example. All rights reserved. </span>
|
||||
</n-layout-footer>
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
</template>
|
Reference in New Issue
Block a user