Compare commits
1 Commits
a003f3e277
...
0e051f321c
Author | SHA1 | Date | |
---|---|---|---|
0e051f321c |
@ -15,11 +15,9 @@ const themeConfig = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-config-provider preflight-style-disabled>
|
|
||||||
<a-config-provider :theme="themeConfig">
|
<a-config-provider :theme="themeConfig">
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</a-config-provider>
|
</a-config-provider>
|
||||||
</n-config-provider>
|
|
||||||
|
|
||||||
<DynamicDialog /> <ConfirmDialog /> <Toast />
|
<DynamicDialog /> <ConfirmDialog /> <Toast />
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,321 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { MenuOption } from 'naive-ui';
|
|
||||||
|
|
||||||
import { createGetRoutes } from '@/plugins/router';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// 响应式断点检测
|
|
||||||
const isMobile = ref(false);
|
|
||||||
const isTablet = ref(false);
|
|
||||||
|
|
||||||
// 检测屏幕尺寸
|
|
||||||
const updateScreenSize = () => {
|
|
||||||
const width = window.innerWidth;
|
|
||||||
const wasMobile = isMobile.value;
|
|
||||||
|
|
||||||
isMobile.value = width < 768;
|
|
||||||
isTablet.value = width >= 768 && width < 1024;
|
|
||||||
|
|
||||||
// 当从移动端切换到桌面端时,关闭抽屉并重置折叠状态
|
|
||||||
if (wasMobile && !isMobile.value) {
|
|
||||||
drawerVisible.value = false;
|
|
||||||
collapsed.value = false;
|
|
||||||
}
|
|
||||||
// 当从桌面端切换到移动端时,关闭抽屉
|
|
||||||
else if (!wasMobile && isMobile.value) {
|
|
||||||
drawerVisible.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 侧边栏状态管理
|
|
||||||
const collapsed = ref(false);
|
|
||||||
const drawerVisible = ref(false);
|
|
||||||
|
|
||||||
// 初始化屏幕尺寸检测
|
|
||||||
onMounted(() => {
|
|
||||||
updateScreenSize();
|
|
||||||
window.addEventListener('resize', updateScreenSize);
|
|
||||||
|
|
||||||
// 移动端默认收起侧边栏
|
|
||||||
if (isMobile.value) {
|
|
||||||
collapsed.value = true;
|
|
||||||
drawerVisible.value = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', updateScreenSize);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 菜单项类型定义
|
|
||||||
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 });
|
|
||||||
|
|
||||||
// 移动端点击菜单项后自动收起侧边栏
|
|
||||||
if (isMobile.value) {
|
|
||||||
drawerVisible.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 监听路由变化,更新选中的菜单项
|
|
||||||
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 toggleSidebar = () => {
|
|
||||||
if (isMobile.value) {
|
|
||||||
// 移动端使用抽屉模式
|
|
||||||
drawerVisible.value = !drawerVisible.value;
|
|
||||||
} else {
|
|
||||||
// 桌面端使用折叠模式
|
|
||||||
collapsed.value = !collapsed.value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<n-layout :has-sider="!isMobile">
|
|
||||||
<!-- 移动端抽屉 -->
|
|
||||||
<n-drawer
|
|
||||||
v-if="isMobile"
|
|
||||||
v-model:show="drawerVisible"
|
|
||||||
:width="280"
|
|
||||||
placement="left"
|
|
||||||
:trap-focus="false"
|
|
||||||
:block-scroll="false"
|
|
||||||
>
|
|
||||||
<n-drawer-content title="菜单" :native-scrollbar="false">
|
|
||||||
<n-menu :options="menuOptions" :value="selectedKey" @update:value="handleMenuSelect" />
|
|
||||||
</n-drawer-content>
|
|
||||||
</n-drawer>
|
|
||||||
|
|
||||||
<!-- 桌面端侧边栏 -->
|
|
||||||
<n-layout-sider
|
|
||||||
v-if="!isMobile"
|
|
||||||
: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: isMobile ? '0 16px' : '0 24px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<n-button
|
|
||||||
quaternary
|
|
||||||
@click="toggleSidebar"
|
|
||||||
:style="{
|
|
||||||
marginRight: isMobile ? '8px' : '12px',
|
|
||||||
padding: isMobile ? '8px' : '6px',
|
|
||||||
}"
|
|
||||||
:size="isMobile ? 'medium' : 'small'"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :size="isMobile ? 20 : 18">
|
|
||||||
<svg viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
v-if="!isMobile && collapsed"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
v-else-if="!isMobile && !collapsed"
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
<!-- 移动端始终显示菜单图标 -->
|
|
||||||
<path v-else fill="currentColor" d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" />
|
|
||||||
</svg>
|
|
||||||
</n-icon>
|
|
||||||
</template>
|
|
||||||
</n-button>
|
|
||||||
<span
|
|
||||||
:style="{
|
|
||||||
fontSize: isMobile ? '16px' : '18px',
|
|
||||||
fontWeight: '500',
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
Vue TS Example
|
|
||||||
</span>
|
|
||||||
</n-layout-header>
|
|
||||||
|
|
||||||
<n-layout-content
|
|
||||||
:content-style="{
|
|
||||||
padding: isMobile ? '16px' : '24px',
|
|
||||||
minHeight: 'calc(100vh - 64px - 72px)', // 减去头部和底部高度
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<router-view />
|
|
||||||
</n-layout-content>
|
|
||||||
|
|
||||||
<n-layout-footer
|
|
||||||
bordered
|
|
||||||
:style="{
|
|
||||||
padding: isMobile ? '16px' : '24px',
|
|
||||||
textAlign: 'center',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
:style="{
|
|
||||||
color: 'var(--n-text-color-disabled)',
|
|
||||||
fontSize: isMobile ? '12px' : '14px',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
© 2025 Vue TS Example. All rights reserved.
|
|
||||||
</span>
|
|
||||||
</n-layout-footer>
|
|
||||||
</n-layout>
|
|
||||||
</n-layout>
|
|
||||||
</template>
|
|
@ -66,8 +66,7 @@ export function Plugins() {
|
|||||||
|
|
||||||
// https://github.com/dishait/vite-plugin-vue-meta-layouts
|
// https://github.com/dishait/vite-plugin-vue-meta-layouts
|
||||||
MetaLayouts({
|
MetaLayouts({
|
||||||
// defaultLayout: 'sakai-vue/AppLayout',
|
defaultLayout: 'sakai-vue/AppLayout',
|
||||||
defaultLayout: 'naive-ui/AppLayout',
|
|
||||||
skipTopLevelRouteLayout: false, // 打开修复 https://github.com/JohnCampionJr/vite-plugin-vue-layouts/issues/134,默认为 false 关闭
|
skipTopLevelRouteLayout: false, // 打开修复 https://github.com/JohnCampionJr/vite-plugin-vue-layouts/issues/134,默认为 false 关闭
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user