feat: refactor sidebar management and routing metadata
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
const collapsed = defineModel<boolean>('collapsed');
|
||||
const buttonRef = useTemplateRef('buttonRef');
|
||||
const appStore = useAppStore();
|
||||
|
||||
function toggleCollapsed() {
|
||||
// https://github.com/tusen-ai/naive-ui/issues/3688
|
||||
// hover style 鼠标移出就会消失 如果是点击 button 会聚焦需要失去焦点才会恢复
|
||||
buttonRef.value?.$el.blur();
|
||||
collapsed.value = !collapsed.value;
|
||||
appStore.toggleSidebar();
|
||||
}
|
||||
|
||||
const themeLabels: Record<AppThemeMode, string> = {
|
||||
@@ -19,10 +19,10 @@ const themeLabels: Record<AppThemeMode, string> = {
|
||||
<template>
|
||||
<div class="h-full flex items-center justify-between px-12px shadow-header dark:shadow-gray-700">
|
||||
<NTooltip :disabled="appStore.isMobile" placement="bottom-start">
|
||||
{{ collapsed ? '展开菜单' : '收起菜单' }}
|
||||
{{ appStore.sidebarCollapsed ? '展开菜单' : '收起菜单' }}
|
||||
<template #trigger>
|
||||
<NButton ref="buttonRef" quaternary @click="toggleCollapsed">
|
||||
<icon-line-md:menu-fold-right v-if="collapsed" w-4.5 h-4.5 />
|
||||
<icon-line-md:menu-fold-right v-if="appStore.sidebarCollapsed" w-4.5 h-4.5 />
|
||||
<icon-line-md:menu-fold-left v-else w-4.5 h-4.5 />
|
||||
</NButton>
|
||||
</template>
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { createGetRoutes, router } from '@/plugins/router-plugin';
|
||||
import type { MenuOption } from 'naive-ui';
|
||||
import { RouterLink, type RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import IconMenuRounded from '~icons/material-symbols/menu-rounded';
|
||||
import { useAppStore } from '../../stores/app-store';
|
||||
// 路由转换为菜单树的辅助函数
|
||||
function convertRoutesToMenuOptions(routes: Readonly<RouteRecordRaw[]>): MenuOption[] {
|
||||
const menuMap = new Map<string, MenuOption>();
|
||||
@@ -12,7 +13,7 @@ function convertRoutesToMenuOptions(routes: Readonly<RouteRecordRaw[]>): MenuOpt
|
||||
const validRoutes = routes
|
||||
.filter((route) => {
|
||||
// 过滤掉不需要显示的路由
|
||||
if (route.meta?.hidden === true || route.meta?.layout === false) {
|
||||
if (route.meta?.hideInMenu === true || route.meta?.layout === false) {
|
||||
return false;
|
||||
}
|
||||
// 过滤掉通配符路径
|
||||
@@ -26,11 +27,11 @@ function convertRoutesToMenuOptions(routes: Readonly<RouteRecordRaw[]>): MenuOpt
|
||||
// 构建菜单树
|
||||
for (const route of validRoutes) {
|
||||
const pathSegments = route.path.split('/').filter(Boolean);
|
||||
const text = route.meta?.title || route.name;
|
||||
const menuOption: MenuOption = {
|
||||
label: () => (
|
||||
<RouterLink to={route}>{route.meta?.title || (route.name as string)}</RouterLink>
|
||||
),
|
||||
label: () => (route.meta?.link === false ? text : <RouterLink to={route}>{text}</RouterLink>),
|
||||
key: route.path,
|
||||
icon: () => <IconMenuRounded style="width: 1em; height: 1em;" />,
|
||||
};
|
||||
|
||||
// 如果是根路径或只有一级路径,直接添加到根菜单
|
||||
@@ -52,6 +53,8 @@ function convertRoutesToMenuOptions(routes: Readonly<RouteRecordRaw[]>): MenuOpt
|
||||
parent.children = [];
|
||||
}
|
||||
parent.children.push(menuOption);
|
||||
} else {
|
||||
console.warn(`未找到父菜单项: ${parentPath},无法将子菜单项添加到其下。`);
|
||||
}
|
||||
|
||||
menuMap.set(route.path, menuOption);
|
||||
@@ -79,14 +82,22 @@ watch(
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const appStore = useAppStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- @update:value="handleMenuUpdate" -->
|
||||
<NMenu
|
||||
v-model:value="selectedKey"
|
||||
ref="menuInstRef"
|
||||
mode="vertical"
|
||||
:collapsed="appStore.sidebarCollapsed"
|
||||
:collapsed-width="64"
|
||||
:icon-size="20"
|
||||
:collapsed-icon-size="24"
|
||||
v-model:value="selectedKey"
|
||||
:options="menuOptions"
|
||||
:inverted="false"
|
||||
:root-indent="32"
|
||||
:indent="32"
|
||||
/>
|
||||
|
||||
@@ -3,14 +3,13 @@ import { AdminLayout } from '@sa/materials';
|
||||
import BaseLayoutHeader from './base-layout-header.vue';
|
||||
import BaseLayoutSider from './base-layout-sider.vue';
|
||||
|
||||
const siderCollapse = ref(false);
|
||||
const appStore = useAppStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminLayout :is-mobile="appStore.isMobile" v-model:sider-collapse="siderCollapse">
|
||||
<AdminLayout :is-mobile="appStore.isMobile" v-model:sider-collapse="appStore.sidebarCollapsed">
|
||||
<template #header>
|
||||
<BaseLayoutHeader v-model:collapsed="siderCollapse" />
|
||||
<BaseLayoutHeader />
|
||||
</template>
|
||||
<template #tab>
|
||||
<div class="bg-green-100/28 dark:bg-green-900/28 text-green-900 dark:text-green-100 p-4">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
definePage({
|
||||
meta: {
|
||||
title: '首页',
|
||||
hidden: false,
|
||||
hideInMenu: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
definePage({ meta: { title: '示例演示' } });
|
||||
definePage({ meta: { title: '示例演示', link: false } });
|
||||
</script>
|
||||
<template><div>此页面文件仅用于在侧边栏菜单中显示示例演示分组标题</div></template>
|
||||
|
||||
@@ -41,7 +41,7 @@ declare module 'vue-router' {
|
||||
/**
|
||||
* @description 是否在菜单中隐藏
|
||||
*/
|
||||
hidden?: boolean;
|
||||
hideInMenu?: boolean;
|
||||
/**
|
||||
* @description 菜单标题
|
||||
*/
|
||||
@@ -51,6 +51,13 @@ declare module 'vue-router' {
|
||||
* @description 使用的布局,设置为 false 则表示不使用布局
|
||||
*/
|
||||
layout?: string | false;
|
||||
|
||||
/**
|
||||
* @description 菜单项是否渲染为可点击链接,默认为 true
|
||||
* - true: 使用 RouterLink 包装,可点击跳转
|
||||
* - false: 仅渲染纯文本标签,不可点击(适用于分组标题)
|
||||
*/
|
||||
link?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMediaQuery, usePreferredColorScheme } from '@vueuse/core';
|
||||
import { useLocalStorage, useMediaQuery, usePreferredColorScheme } from '@vueuse/core';
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, watch } from 'vue';
|
||||
|
||||
@@ -11,6 +11,9 @@ export const useAppStore = defineStore('app', () => {
|
||||
const themeMode = useLocalStorage<AppThemeMode>('app-theme-mode', 'system');
|
||||
const preferredColor = usePreferredColorScheme();
|
||||
|
||||
// 侧边栏展开/收起状态
|
||||
const sidebarCollapsed = useLocalStorage<boolean>('app-sidebar-collapsed', false);
|
||||
|
||||
// 计算实际使用的主题
|
||||
const actualTheme = computed(() =>
|
||||
themeMode.value === 'system'
|
||||
@@ -31,16 +34,16 @@ export const useAppStore = defineStore('app', () => {
|
||||
document.documentElement.classList.toggle(DARK_CLASS, isDark.value);
|
||||
}
|
||||
|
||||
// 设置主题
|
||||
function setTheme(mode: AppThemeMode) {
|
||||
themeMode.value = mode;
|
||||
}
|
||||
|
||||
// 循环切换主题
|
||||
function cycleTheme() {
|
||||
const currentIndex = APP_THEME_MODES.indexOf(themeMode.value);
|
||||
const nextIndex = (currentIndex + 1) % APP_THEME_MODES.length;
|
||||
setTheme(APP_THEME_MODES[nextIndex]!);
|
||||
themeMode.value = APP_THEME_MODES[nextIndex]!;
|
||||
}
|
||||
|
||||
// 切换侧边栏展开/收起
|
||||
function toggleSidebar() {
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value;
|
||||
}
|
||||
|
||||
// 监听主题变化,更新 DOM
|
||||
@@ -48,11 +51,11 @@ export const useAppStore = defineStore('app', () => {
|
||||
|
||||
return {
|
||||
themeMode,
|
||||
actualTheme,
|
||||
isDark,
|
||||
isMobile,
|
||||
setTheme,
|
||||
cycleTheme,
|
||||
sidebarCollapsed,
|
||||
toggleSidebar,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user