refactor(projects)!: refactor global menu & support reversed-horizontal-mix-menu. close #365

This commit is contained in:
Soybean 2024-07-22 00:01:52 +08:00
parent 00f41dd25e
commit 087e532613
24 changed files with 588 additions and 310 deletions

View File

@ -1,5 +1,9 @@
import { transformRecordToOption } from '@/utils/common';
export const GLOBAL_HEADER_MENU_ID = '__GLOBAL_HEADER_MENU__';
export const GLOBAL_SIDER_MENU_ID = '__GLOBAL_SIDER_MENU__';
export const themeSchemaRecord: Record<UnionKey.ThemeScheme, App.I18n.I18nKey> = {
light: 'theme.themeSchema.light',
dark: 'theme.themeSchema.dark',

View File

@ -30,17 +30,30 @@ export function useRouterPush(inSetup = true) {
name: key
};
if (query) {
if (Object.keys(query || {}).length) {
routeLocation.query = query;
}
if (params) {
if (Object.keys(params || {}).length) {
routeLocation.params = params;
}
return routerPush(routeLocation);
}
function routerPushByKeyWithMetaQuery(key: RouteKey) {
const allRoutes = router.getRoutes();
const meta = allRoutes.find(item => item.name === key)?.meta || null;
const query: Record<string, string> = {};
meta?.query?.forEach(item => {
query[item.key] = item.value;
});
return routerPushByKey(key, { query });
}
async function toHome() {
return routerPushByKey('root');
}
@ -95,6 +108,7 @@ export function useRouterPush(inSetup = true) {
routerPush,
routerBack,
routerPushByKey,
routerPushByKeyWithMetaQuery,
toLogin,
toggleLoginModule,
redirectFromLogin

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue';
import { computed, defineAsyncComponent } from 'vue';
import { AdminLayout, LAYOUT_SCROLL_EL_ID } from '@sa/materials';
import type { LayoutMode } from '@sa/materials';
import { useAppStore } from '@/store/modules/app';
@ -18,7 +18,9 @@ defineOptions({
const appStore = useAppStore();
const themeStore = useThemeStore();
const { menus } = setupMixMenuContext();
const { childLevelMenus, isActiveFirstLevelMenuHasChildren } = setupMixMenuContext();
const GlobalMenu = defineAsyncComponent(() => import('../modules/global-menu/index.vue'));
const layoutMode = computed(() => {
const vertical: LayoutMode = 'vertical';
@ -26,30 +28,34 @@ const layoutMode = computed(() => {
return themeStore.layout.mode.includes(vertical) ? vertical : horizontal;
});
const headerPropsConfig: Record<UnionKey.ThemeLayoutMode, App.Global.HeaderProps> = {
vertical: {
showLogo: false,
showMenu: false,
showMenuToggler: true
},
'vertical-mix': {
showLogo: false,
showMenu: false,
showMenuToggler: false
},
horizontal: {
showLogo: true,
showMenu: true,
showMenuToggler: false
},
'horizontal-mix': {
showLogo: true,
showMenu: true,
showMenuToggler: false
}
};
const headerProps = computed(() => {
const { mode, reverseHorizontalMix } = themeStore.layout;
const headerProps = computed(() => headerPropsConfig[themeStore.layout.mode]);
const headerPropsConfig: Record<UnionKey.ThemeLayoutMode, App.Global.HeaderProps> = {
vertical: {
showLogo: false,
showMenu: false,
showMenuToggler: true
},
'vertical-mix': {
showLogo: false,
showMenu: false,
showMenuToggler: false
},
horizontal: {
showLogo: true,
showMenu: true,
showMenuToggler: false
},
'horizontal-mix': {
showLogo: true,
showMenu: true,
showMenuToggler: reverseHorizontalMix && isActiveFirstLevelMenuHasChildren.value
}
};
return headerPropsConfig[mode];
});
const siderVisible = computed(() => themeStore.layout.mode !== 'horizontal');
@ -62,11 +68,16 @@ const siderWidth = computed(() => getSiderWidth());
const siderCollapsedWidth = computed(() => getSiderCollapsedWidth());
function getSiderWidth() {
const { reverseHorizontalMix } = themeStore.layout;
const { width, mixWidth, mixChildMenuWidth } = themeStore.sider;
if (isHorizontalMix.value && reverseHorizontalMix) {
return isActiveFirstLevelMenuHasChildren.value ? width : 0;
}
let w = isVerticalMix.value || isHorizontalMix.value ? mixWidth : width;
if (isVerticalMix.value && appStore.mixSiderFixed && menus.value.length) {
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
w += mixChildMenuWidth;
}
@ -78,7 +89,7 @@ function getSiderCollapsedWidth() {
let w = isVerticalMix.value || isHorizontalMix.value ? mixCollapsedWidth : collapsedWidth;
if (isVerticalMix.value && appStore.mixSiderFixed && menus.value.length) {
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
w += mixChildMenuWidth;
}
@ -116,6 +127,7 @@ function getSiderCollapsedWidth() {
<template #sider>
<GlobalSider />
</template>
<GlobalMenu />
<GlobalContent />
<ThemeDrawer />
<template #footer>

View File

@ -26,10 +26,30 @@ function useMixMenu() {
setActiveFirstLevelMenuKey(firstLevelRouteName);
}
const menus = computed(
const allMenus = computed<App.Global.Menu[]>(() => routeStore.menus);
const firstLevelMenus = computed<App.Global.Menu[]>(() =>
routeStore.menus.map(menu => {
const { children: _, ...rest } = menu;
return rest;
})
);
const childLevelMenus = computed<App.Global.Menu[]>(
() => routeStore.menus.find(menu => menu.key === activeFirstLevelMenuKey.value)?.children || []
);
const isActiveFirstLevelMenuHasChildren = computed(() => {
if (!activeFirstLevelMenuKey.value) {
return false;
}
const findItem = allMenus.value.find(item => item.key === activeFirstLevelMenuKey.value);
return Boolean(findItem?.children?.length);
});
watch(
() => route.name,
() => {
@ -39,9 +59,12 @@ function useMixMenu() {
);
return {
allMenus,
firstLevelMenus,
childLevelMenus,
isActiveFirstLevelMenuHasChildren,
activeFirstLevelMenuKey,
setActiveFirstLevelMenuKey,
getActiveFirstLevelMenuKey,
menus
getActiveFirstLevelMenuKey
};
}

View File

@ -1,14 +1,11 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useFullscreen } from '@vueuse/core';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route';
import HorizontalMenu from '../global-menu/base-menu.vue';
import { GLOBAL_HEADER_MENU_ID } from '@/constants/app';
import GlobalLogo from '../global-logo/index.vue';
import GlobalBreadcrumb from '../global-breadcrumb/index.vue';
import GlobalSearch from '../global-search/index.vue';
import { useMixMenuContext } from '../../context';
import ThemeButton from './components/theme-button.vue';
import UserAvatar from './components/user-avatar.vue';
@ -29,29 +26,15 @@ defineProps<Props>();
const appStore = useAppStore();
const themeStore = useThemeStore();
const routeStore = useRouteStore();
const { isFullscreen, toggle } = useFullscreen();
const { menus } = useMixMenuContext();
const headerMenus = computed(() => {
if (themeStore.layout.mode === 'horizontal') {
return routeStore.menus;
}
if (themeStore.layout.mode === 'horizontal-mix') {
return menus.value;
}
return [];
});
</script>
<template>
<DarkModeContainer class="h-full flex-y-center px-12px shadow-header">
<GlobalLogo v-if="showLogo" class="h-full" :style="{ width: themeStore.sider.width + 'px' }" />
<HorizontalMenu v-if="showMenu" mode="horizontal" :menus="headerMenus" class="px-12px" />
<MenuToggler v-if="showMenuToggler" :collapsed="appStore.siderCollapse" @click="appStore.toggleSiderCollapse" />
<div v-if="showMenu" :id="GLOBAL_HEADER_MENU_ID" class="h-full flex-y-center flex-1-hidden"></div>
<div v-else class="h-full flex-y-center flex-1-hidden">
<MenuToggler v-if="showMenuToggler" :collapsed="appStore.siderCollapse" @click="appStore.toggleSiderCollapse" />
<GlobalBreadcrumb v-if="!appStore.isMobile" class="ml-12px" />
</div>
<div class="h-full flex-y-center justify-end">

View File

@ -1,96 +0,0 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import type { MentionOption, MenuProps } from 'naive-ui';
import { SimpleScrollbar } from '@sa/materials';
import type { RouteKey } from '@elegant-router/types';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router';
defineOptions({
name: 'BaseMenu'
});
interface Props {
darkTheme?: boolean;
mode?: MenuProps['mode'];
menus: App.Global.Menu[];
}
const props = withDefaults(defineProps<Props>(), {
mode: 'vertical'
});
const route = useRoute();
const appStore = useAppStore();
const themeStore = useThemeStore();
const routeStore = useRouteStore();
const { routerPushByKey } = useRouterPush();
const naiveMenus = computed(() => props.menus as unknown as MentionOption[]);
const isHorizontal = computed(() => props.mode === 'horizontal');
const siderCollapse = computed(() => themeStore.layout.mode === 'vertical' && appStore.siderCollapse);
const headerHeight = computed(() => `${themeStore.header.height}px`);
const selectedKey = computed(() => {
const { hideInMenu, activeMenu } = route.meta;
const name = route.name as string;
const routeName = (hideInMenu ? activeMenu : name) || name;
return routeName;
});
const expandedKeys = ref<string[]>([]);
function updateExpandedKeys() {
if (isHorizontal.value || siderCollapse.value || !selectedKey.value) {
expandedKeys.value = [];
return;
}
expandedKeys.value = routeStore.getSelectedMenuKeyPath(selectedKey.value);
}
function handleClickMenu(key: RouteKey) {
const query = routeStore.getRouteQueryOfMetaByKey(key);
routerPushByKey(key, { query });
}
watch(
() => route.name,
() => {
updateExpandedKeys();
},
{ immediate: true }
);
</script>
<template>
<SimpleScrollbar>
<NMenu
v-model:expanded-keys="expandedKeys"
:mode="mode"
:value="selectedKey"
:collapsed="siderCollapse"
:collapsed-width="themeStore.sider.collapsedWidth"
:collapsed-icon-size="22"
:options="naiveMenus"
:inverted="darkTheme"
:indent="18"
responsive
@update:value="handleClickMenu"
/>
</SimpleScrollbar>
</template>
<style scoped>
:deep(.n-menu--horizontal) {
--n-item-height: v-bind(headerHeight) !important;
}
</style>

View File

@ -3,31 +3,29 @@ import { computed } from 'vue';
import { createReusableTemplate } from '@vueuse/core';
import { SimpleScrollbar } from '@sa/materials';
import { transformColorWithOpacity } from '@sa/color';
import { useAppStore } from '@/store/modules/app';
import { useRouteStore } from '@/store/modules/route';
import { useThemeStore } from '@/store/modules/theme';
defineOptions({
name: 'FirstLevelMenu'
});
interface Props {
menus: App.Global.Menu[];
activeMenuKey?: string;
inverted?: boolean;
siderCollapse?: boolean;
darkMode?: boolean;
themeColor: string;
}
defineProps<Props>();
const props = defineProps<Props>();
interface Emits {
(e: 'select', menu: App.Global.Menu): boolean;
(e: 'toggleSiderCollapse'): void;
}
const emit = defineEmits<Emits>();
const appStore = useAppStore();
const themeStore = useThemeStore();
const routeStore = useRouteStore();
interface MixMenuItemProps {
/** Menu item label */
label: App.Global.Menu['label'];
@ -36,12 +34,12 @@ interface MixMenuItemProps {
/** Active menu item */
active: boolean;
/** Mini size */
isMini: boolean;
isMini?: boolean;
}
const [DefineMixMenuItem, MixMenuItem] = createReusableTemplate<MixMenuItemProps>();
const selectedBgColor = computed(() => {
const { darkMode, themeColor } = themeStore;
const { darkMode, themeColor } = props;
const light = transformColorWithOpacity(themeColor, 0.1, '#ffffff');
const dark = transformColorWithOpacity(themeColor, 0.3, '#000000');
@ -52,6 +50,10 @@ const selectedBgColor = computed(() => {
function handleClickMixMenu(menu: App.Global.Menu) {
emit('select', menu);
}
function toggleSiderCollapse() {
emit('toggleSiderCollapse');
}
</script>
<template>
@ -80,21 +82,21 @@ function handleClickMixMenu(menu: App.Global.Menu) {
<slot></slot>
<SimpleScrollbar>
<MixMenuItem
v-for="menu in routeStore.menus"
v-for="menu in menus"
:key="menu.key"
:label="menu.label"
:icon="menu.icon"
:active="menu.key === activeMenuKey"
:is-mini="appStore.siderCollapse"
:is-mini="siderCollapse"
@click="handleClickMixMenu(menu)"
/>
</SimpleScrollbar>
<MenuToggler
arrow-icon
:collapsed="appStore.siderCollapse"
:collapsed="siderCollapse"
:z-index="99"
:class="{ 'text-white:88 !hover:text-white': inverted }"
@click="appStore.toggleSiderCollapse"
@click="toggleSiderCollapse"
/>
</div>
</template>

View File

@ -1,28 +0,0 @@
<script setup lang="ts">
import { useRouterPush } from '@/hooks/common/router';
import { useMixMenuContext } from '../../context';
import FirstLevelMenu from './first-level-menu.vue';
defineOptions({
name: 'HorizontalMixMenu'
});
const { activeFirstLevelMenuKey, setActiveFirstLevelMenuKey } = useMixMenuContext();
const { routerPushByKey } = useRouterPush();
function handleSelectMixMenu(menu: App.Global.Menu) {
setActiveFirstLevelMenuKey(menu.key);
if (!menu.children?.length) {
routerPushByKey(menu.routeKey);
}
}
</script>
<template>
<FirstLevelMenu :active-menu-key="activeFirstLevelMenuKey" @select="handleSelectMixMenu">
<slot></slot>
</FirstLevelMenu>
</template>
<style scoped></style>

View File

@ -0,0 +1,33 @@
<script setup lang="ts">
import { computed } from 'vue';
import type { Component } from 'vue';
import { useThemeStore } from '@/store/modules/theme';
import VerticalMenu from './modules/vertical-menu.vue';
import VerticalMixMenu from './modules/vertical-mix-menu.vue';
import HorizontalMenu from './modules/horizontal-menu.vue';
import HorizontalMixMenu from './modules/horizontal-mix-menu.vue';
import ReversedHorizontalMixMenu from './modules/reversed-horizontal-mix-menu.vue';
defineOptions({
name: 'GlobalMenu'
});
const themeStore = useThemeStore();
const activeMenu = computed(() => {
const menuMap: Record<UnionKey.ThemeLayoutMode, Component> = {
vertical: VerticalMenu,
'vertical-mix': VerticalMixMenu,
horizontal: HorizontalMenu,
'horizontal-mix': themeStore.layout.reverseHorizontalMix ? ReversedHorizontalMixMenu : HorizontalMixMenu
};
return menuMap[themeStore.layout.mode];
});
</script>
<template>
<component :is="activeMenu" />
</template>
<style scoped></style>

View File

@ -0,0 +1,39 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { GLOBAL_HEADER_MENU_ID } from '@/constants/app';
import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router';
defineOptions({
name: 'HorizontalMenu'
});
const route = useRoute();
const routeStore = useRouteStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
const selectedKey = computed(() => {
const { hideInMenu, activeMenu } = route.meta;
const name = route.name as string;
const routeName = (hideInMenu ? activeMenu : name) || name;
return routeName;
});
</script>
<template>
<Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
<NMenu
mode="horizontal"
:value="selectedKey"
:options="routeStore.menus"
:indent="18"
responsive
@update:value="routerPushByKeyWithMetaQuery"
/>
</Teleport>
</template>
<style scoped></style>

View File

@ -0,0 +1,68 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
import FirstLevelMenu from '../components/first-level-menu.vue';
import { useMixMenuContext } from '../../../context';
defineOptions({
name: 'HorizontalMixMenu'
});
const route = useRoute();
const appStore = useAppStore();
const themeStore = useThemeStore();
const { allMenus, childLevelMenus, activeFirstLevelMenuKey, setActiveFirstLevelMenuKey } = useMixMenuContext();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
const selectedKey = computed(() => {
const { hideInMenu, activeMenu } = route.meta;
const name = route.name as string;
const routeName = (hideInMenu ? activeMenu : name) || name;
return routeName;
});
function handleSelectMixMenu(menu: App.Global.Menu) {
setActiveFirstLevelMenuKey(menu.key);
if (!menu.children?.length) {
routerPushByKeyWithMetaQuery(menu.routeKey);
}
}
</script>
<template>
<Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
<NMenu
mode="horizontal"
:value="selectedKey"
:options="childLevelMenus"
:indent="18"
responsive
@update:value="routerPushByKeyWithMetaQuery"
/>
</Teleport>
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
<FirstLevelMenu
:menus="allMenus"
:active-menu-key="activeFirstLevelMenuKey"
:inverted="inverted"
:sider-collapse="appStore.siderCollapse"
:dark-mode="themeStore.darkMode"
:theme-color="themeStore.themeColor"
@select="handleSelectMixMenu"
@toggle-sider-collapse="appStore.toggleSiderCollapse"
>
<slot></slot>
</FirstLevelMenu>
</Teleport>
</template>
<style scoped></style>

View File

@ -0,0 +1,97 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import type { RouteKey } from '@elegant-router/types';
import { SimpleScrollbar } from '@sa/materials';
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router';
import { useMixMenuContext } from '../../../context';
defineOptions({
name: 'ReversedHorizontalMixMenu'
});
const route = useRoute();
const appStore = useAppStore();
const themeStore = useThemeStore();
const routeStore = useRouteStore();
const {
firstLevelMenus,
childLevelMenus,
activeFirstLevelMenuKey,
setActiveFirstLevelMenuKey,
isActiveFirstLevelMenuHasChildren
} = useMixMenuContext();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
const selectedKey = computed(() => {
const { hideInMenu, activeMenu } = route.meta;
const name = route.name as string;
const routeName = (hideInMenu ? activeMenu : name) || name;
return routeName;
});
function handleSelectMixMenu(key: RouteKey) {
setActiveFirstLevelMenuKey(key);
if (!isActiveFirstLevelMenuHasChildren.value) {
routerPushByKeyWithMetaQuery(key);
}
}
const expandedKeys = ref<string[]>([]);
function updateExpandedKeys() {
if (appStore.siderCollapse || !selectedKey.value) {
expandedKeys.value = [];
return;
}
expandedKeys.value = routeStore.getSelectedMenuKeyPath(selectedKey.value);
}
watch(
() => route.name,
() => {
updateExpandedKeys();
},
{ immediate: true }
);
</script>
<template>
<Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
<NMenu
mode="horizontal"
:value="activeFirstLevelMenuKey"
:options="firstLevelMenus"
:indent="18"
responsive
@update:value="handleSelectMixMenu"
/>
</Teleport>
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
<SimpleScrollbar>
<NMenu
v-model:expanded-keys="expandedKeys"
mode="vertical"
:value="selectedKey"
:collapsed="appStore.siderCollapse"
:collapsed-width="themeStore.sider.collapsedWidth"
:collapsed-icon-size="22"
:options="childLevelMenus"
:inverted="inverted"
:indent="18"
@update:value="routerPushByKeyWithMetaQuery"
/>
</SimpleScrollbar>
</Teleport>
</template>
<style scoped></style>

View File

@ -0,0 +1,70 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { SimpleScrollbar } from '@sa/materials';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router';
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
defineOptions({
name: 'VerticalMenu'
});
const route = useRoute();
const appStore = useAppStore();
const themeStore = useThemeStore();
const routeStore = useRouteStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
const selectedKey = computed(() => {
const { hideInMenu, activeMenu } = route.meta;
const name = route.name as string;
const routeName = (hideInMenu ? activeMenu : name) || name;
return routeName;
});
const expandedKeys = ref<string[]>([]);
function updateExpandedKeys() {
if (appStore.siderCollapse || !selectedKey.value) {
expandedKeys.value = [];
return;
}
expandedKeys.value = routeStore.getSelectedMenuKeyPath(selectedKey.value);
}
watch(
() => route.name,
() => {
updateExpandedKeys();
},
{ immediate: true }
);
</script>
<template>
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
<SimpleScrollbar>
<NMenu
v-model:expanded-keys="expandedKeys"
mode="vertical"
:value="selectedKey"
:collapsed="appStore.siderCollapse"
:collapsed-width="themeStore.sider.collapsedWidth"
:collapsed-icon-size="22"
:options="routeStore.menus"
:inverted="inverted"
:indent="18"
@update:value="routerPushByKeyWithMetaQuery"
/>
</SimpleScrollbar>
</Teleport>
</template>
<style scoped></style>

View File

@ -0,0 +1,135 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { SimpleScrollbar } from '@sa/materials';
import { useBoolean } from '@sa/hooks';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router';
import { $t } from '@/locales';
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
import { useMixMenuContext } from '../../../context';
import FirstLevelMenu from '../components/first-level-menu.vue';
import GlobalLogo from '../../global-logo/index.vue';
defineOptions({
name: 'VerticalMenuMix'
});
const route = useRoute();
const appStore = useAppStore();
const themeStore = useThemeStore();
const routeStore = useRouteStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean();
const {
allMenus,
childLevelMenus,
activeFirstLevelMenuKey,
setActiveFirstLevelMenuKey,
getActiveFirstLevelMenuKey
//
} = useMixMenuContext();
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
const hasChildMenus = computed(() => childLevelMenus.value.length > 0);
const showDrawer = computed(() => hasChildMenus.value && (drawerVisible.value || appStore.mixSiderFixed));
function handleSelectMixMenu(menu: App.Global.Menu) {
setActiveFirstLevelMenuKey(menu.key);
if (menu.children?.length) {
setDrawerVisible(true);
} else {
routerPushByKeyWithMetaQuery(menu.routeKey);
}
}
function handleResetActiveMenu() {
getActiveFirstLevelMenuKey();
setDrawerVisible(false);
}
const selectedKey = computed(() => {
const { hideInMenu, activeMenu } = route.meta;
const name = route.name as string;
const routeName = (hideInMenu ? activeMenu : name) || name;
return routeName;
});
const expandedKeys = ref<string[]>([]);
function updateExpandedKeys() {
if (appStore.siderCollapse || !selectedKey.value) {
expandedKeys.value = [];
return;
}
expandedKeys.value = routeStore.getSelectedMenuKeyPath(selectedKey.value);
}
watch(
() => route.name,
() => {
updateExpandedKeys();
},
{ immediate: true }
);
</script>
<template>
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
<div class="h-full flex" @mouseleave="handleResetActiveMenu">
<FirstLevelMenu
:menus="allMenus"
:active-menu-key="activeFirstLevelMenuKey"
:inverted="inverted"
:sider-collapse="appStore.siderCollapse"
:dark-mode="themeStore.darkMode"
:theme-color="themeStore.themeColor"
@select="handleSelectMixMenu"
@toggle-sider-collapse="appStore.toggleSiderCollapse"
>
<GlobalLogo :show-title="false" :style="{ height: themeStore.header.height + 'px' }" />
</FirstLevelMenu>
<div
class="relative h-full transition-width-300"
:style="{ width: appStore.mixSiderFixed && hasChildMenus ? themeStore.sider.mixChildMenuWidth + 'px' : '0px' }"
>
<DarkModeContainer
class="absolute-lt h-full flex-col-stretch nowrap-hidden shadow-sm transition-all-300"
:inverted="inverted"
:style="{ width: showDrawer ? themeStore.sider.mixChildMenuWidth + 'px' : '0px' }"
>
<header class="flex-y-center justify-between px-12px" :style="{ height: themeStore.header.height + 'px' }">
<h2 class="text-16px text-primary font-bold">{{ $t('system.title') }}</h2>
<PinToggler
:pin="appStore.mixSiderFixed"
:class="{ 'text-white:88 !hover:text-white': inverted }"
@click="appStore.toggleMixSiderFixed"
/>
</header>
<SimpleScrollbar>
<NMenu
v-model:expanded-keys="expandedKeys"
mode="vertical"
:options="childLevelMenus"
:collapsed="appStore.siderCollapse"
:collapsed-width="themeStore.sider.collapsedWidth"
:collapsed-icon-size="22"
:inverted="inverted"
:indent="18"
@update:value="routerPushByKeyWithMetaQuery"
/>
</SimpleScrollbar>
</DarkModeContainer>
</div>
</div>
</Teleport>
</template>
<style scoped></style>

View File

@ -1,72 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useBoolean } from '@sa/hooks';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
import { $t } from '@/locales';
import { useMixMenuContext } from '../../context';
import FirstLevelMenu from './first-level-menu.vue';
import BaseMenu from './base-menu.vue';
defineOptions({
name: 'VerticalMixMenu'
});
const appStore = useAppStore();
const themeStore = useThemeStore();
const { routerPushByKey } = useRouterPush();
const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean();
const { menus, activeFirstLevelMenuKey, setActiveFirstLevelMenuKey, getActiveFirstLevelMenuKey } = useMixMenuContext();
const siderInverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
const hasMenus = computed(() => menus.value.length > 0);
const showDrawer = computed(() => hasMenus.value && (drawerVisible.value || appStore.mixSiderFixed));
function handleSelectMixMenu(menu: App.Global.Menu) {
setActiveFirstLevelMenuKey(menu.key);
if (menu.children?.length) {
setDrawerVisible(true);
} else {
routerPushByKey(menu.routeKey);
}
}
function handleResetActiveMenu() {
getActiveFirstLevelMenuKey();
setDrawerVisible(false);
}
</script>
<template>
<div class="h-full flex" @mouseleave="handleResetActiveMenu">
<FirstLevelMenu :active-menu-key="activeFirstLevelMenuKey" :inverted="siderInverted" @select="handleSelectMixMenu">
<slot></slot>
</FirstLevelMenu>
<div
class="relative h-full transition-width-300"
:style="{ width: appStore.mixSiderFixed && hasMenus ? themeStore.sider.mixChildMenuWidth + 'px' : '0px' }"
>
<DarkModeContainer
class="absolute-lt h-full flex-col-stretch nowrap-hidden shadow-sm transition-all-300"
:inverted="siderInverted"
:style="{ width: showDrawer ? themeStore.sider.mixChildMenuWidth + 'px' : '0px' }"
>
<header class="flex-y-center justify-between px-12px" :style="{ height: themeStore.header.height + 'px' }">
<h2 class="text-16px text-primary font-bold">{{ $t('system.title') }}</h2>
<PinToggler
:pin="appStore.mixSiderFixed"
:class="{ 'text-white:88 !hover:text-white': siderInverted }"
@click="appStore.toggleMixSiderFixed"
/>
</header>
<BaseMenu :dark-theme="siderInverted" :menus="menus" />
</DarkModeContainer>
</div>
</div>
</template>
<style scoped></style>

View File

@ -2,11 +2,8 @@
import { computed } from 'vue';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route';
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
import GlobalLogo from '../global-logo/index.vue';
import VerticalMenu from '../global-menu/base-menu.vue';
import VerticalMixMenu from '../global-menu/vertical-mix-menu.vue';
import HorizontalMixMenu from '../global-menu/horizontal-mix-menu.vue';
defineOptions({
name: 'GlobalSider'
@ -14,12 +11,12 @@ defineOptions({
const appStore = useAppStore();
const themeStore = useThemeStore();
const routeStore = useRouteStore();
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix');
const isHorizontalMix = computed(() => themeStore.layout.mode === 'horizontal-mix');
const darkMenu = computed(() => !themeStore.darkMode && !isHorizontalMix.value && themeStore.sider.inverted);
const showLogo = computed(() => !isVerticalMix.value && !isHorizontalMix.value);
const menuWrapperClass = computed(() => (showLogo.value ? 'flex-1-hidden' : 'h-full'));
</script>
<template>
@ -29,11 +26,7 @@ const showLogo = computed(() => !isVerticalMix.value && !isHorizontalMix.value);
:show-title="!appStore.siderCollapse"
:style="{ height: themeStore.header.height + 'px' }"
/>
<VerticalMixMenu v-if="isVerticalMix">
<GlobalLogo :show-title="false" :style="{ height: themeStore.header.height + 'px' }" />
</VerticalMixMenu>
<HorizontalMixMenu v-else-if="isHorizontalMix" />
<VerticalMenu v-else :dark-theme="darkMenu" :menus="routeStore.menus" />
<div :id="GLOBAL_SIDER_MENU_ID" :class="menuWrapperClass"></div>
</DarkModeContainer>
</template>

View File

@ -3,6 +3,7 @@ import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales';
import LayoutModeCard from '../components/layout-mode-card.vue';
import SettingItem from '../components/setting-item.vue';
defineOptions({
name: 'LayoutMode'
@ -10,6 +11,10 @@ defineOptions({
const appStore = useAppStore();
const themeStore = useThemeStore();
function handleReverseHorizontalMixChange(value: boolean) {
themeStore.setLayoutReverseHorizontalMix(value);
}
</script>
<template>
@ -44,6 +49,13 @@ const themeStore = useThemeStore();
</div>
</template>
</LayoutModeCard>
<SettingItem
v-if="themeStore.layout.mode === 'horizontal-mix'"
:label="$t('theme.layoutMode.reverseHorizontalMix')"
class="mt-16px"
>
<NSwitch :value="themeStore.layout.reverseHorizontalMix" @update:value="handleReverseHorizontalMixChange" />
</SettingItem>
</template>
<style scoped>

View File

@ -70,7 +70,8 @@ const local: App.I18n.Schema = {
vertical: 'Vertical Menu Mode',
horizontal: 'Horizontal Menu Mode',
'vertical-mix': 'Vertical Mix Menu Mode',
'horizontal-mix': 'Horizontal Mix menu Mode'
'horizontal-mix': 'Horizontal Mix menu Mode',
reverseHorizontalMix: 'Reverse first level menus and child level menus position'
},
recommendColor: 'Apply Recommended Color Algorithm',
recommendColorDesc: 'The recommended color algorithm refers to',

View File

@ -70,7 +70,8 @@ const local: App.I18n.Schema = {
vertical: '左侧菜单模式',
'vertical-mix': '左侧菜单混合模式',
horizontal: '顶部菜单模式',
'horizontal-mix': '顶部菜单混合模式'
'horizontal-mix': '顶部菜单混合模式',
reverseHorizontalMix: '一级菜单与子级菜单位置反转'
},
recommendColor: '应用推荐算法的颜色',
recommendColorDesc: '推荐颜色的算法参照',

View File

@ -354,34 +354,6 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
return getSelectedMenuKeyPathByKey(selectedKey, menus.value);
}
/**
* Get route meta by key
*
* @param key Route key
*/
function getRouteMetaByKey(key: string) {
const allRoutes = router.getRoutes();
return allRoutes.find(route => route.name === key)?.meta || null;
}
/**
* Get route query of meta by key
*
* @param key
*/
function getRouteQueryOfMetaByKey(key: string) {
const meta = getRouteMetaByKey(key);
const query: Record<string, string> = {};
meta?.query?.forEach(item => {
query[item.key] = item.value;
});
return query;
}
return {
resetStore,
routeHome,
@ -398,7 +370,6 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
isInitAuthRoute,
setIsInitAuthRoute,
getIsAuthRouteExist,
getSelectedMenuKeyPath,
getRouteQueryOfMetaByKey
getSelectedMenuKeyPath
};
});

View File

@ -132,6 +132,14 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
);
addThemeVarsToGlobal(themeTokens, darkThemeTokens);
}
/**
* Set layout reverse horizontal mix
*
* @param reverse Reverse horizontal mix
*/
function setLayoutReverseHorizontalMix(reverse: boolean) {
settings.value.layout.reverseHorizontalMix = reverse;
}
/** Cache theme settings */
function cacheThemeSettings() {
@ -193,6 +201,7 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
setThemeScheme,
toggleThemeScheme,
updateThemeColors,
setThemeLayout
setThemeLayout,
setLayoutReverseHorizontalMix
};
});

View File

@ -13,7 +13,8 @@ export const themeSettings: App.Theme.ThemeSetting = {
isInfoFollowPrimary: true,
layout: {
mode: 'vertical',
scrollMode: 'content'
scrollMode: 'content',
reverseHorizontalMix: false
},
page: {
animate: true,

12
src/typings/app.d.ts vendored
View File

@ -24,6 +24,12 @@ declare namespace App {
mode: UnionKey.ThemeLayoutMode;
/** Scroll mode */
scrollMode: UnionKey.ThemeScrollMode;
/**
* Whether to reverse the horizontal mix
*
* if true, the vertical child level menus in left and horizontal first level menus in top
*/
reverseHorizontalMix?: boolean;
};
/** Page */
page: {
@ -164,7 +170,7 @@ declare namespace App {
}
/** The global menu */
interface Menu {
type Menu = {
/**
* The menu key
*
@ -183,7 +189,7 @@ declare namespace App {
icon?: () => VNode;
/** The menu children */
children?: Menu[];
}
};
type Breadcrumb = Omit<Menu, 'children'> & {
options?: Breadcrumb[];
@ -326,7 +332,7 @@ declare namespace App {
theme: {
themeSchema: { title: string } & Record<UnionKey.ThemeScheme, string>;
grayscale: string;
layoutMode: { title: string } & Record<UnionKey.ThemeLayoutMode, string>;
layoutMode: { title: string; reverseHorizontalMix: string } & Record<UnionKey.ThemeLayoutMode, string>;
recommendColor: string;
recommendColorDesc: string;
themeColor: {

View File

@ -20,7 +20,7 @@ declare namespace UnionKey {
* - vertical: the vertical menu in left
* - horizontal: the horizontal menu in top
* - vertical-mix: two vertical mixed menus in left
* - horizontal-mix: the vertical menu in left and horizontal menu in top
* - horizontal-mix: the vertical first level menus in left and horizontal child level menus in top
*/
type ThemeLayoutMode = 'vertical' | 'horizontal' | 'vertical-mix' | 'horizontal-mix';