mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-09-17 17:26:38 +08:00
feat(projects): add 'vertical-hybrid-header-first' layout mode
This commit is contained in:
parent
b6ac3106ce
commit
b4e5c6d990
@ -23,6 +23,7 @@ export const loginModuleRecord: Record<UnionKey.LoginModule, App.I18n.I18nKey> =
|
|||||||
export const themeLayoutModeRecord: Record<UnionKey.ThemeLayoutMode, App.I18n.I18nKey> = {
|
export const themeLayoutModeRecord: Record<UnionKey.ThemeLayoutMode, App.I18n.I18nKey> = {
|
||||||
vertical: 'theme.layout.layoutMode.vertical',
|
vertical: 'theme.layout.layoutMode.vertical',
|
||||||
'vertical-mix': 'theme.layout.layoutMode.vertical-mix',
|
'vertical-mix': 'theme.layout.layoutMode.vertical-mix',
|
||||||
|
'vertical-hybrid-header-first': 'theme.layout.layoutMode.vertical-hybrid-header-first',
|
||||||
horizontal: 'theme.layout.layoutMode.horizontal',
|
horizontal: 'theme.layout.layoutMode.horizontal',
|
||||||
'top-hybrid-sidebar-first': 'theme.layout.layoutMode.top-hybrid-sidebar-first',
|
'top-hybrid-sidebar-first': 'theme.layout.layoutMode.top-hybrid-sidebar-first',
|
||||||
'top-hybrid-header-first': 'theme.layout.layoutMode.top-hybrid-header-first'
|
'top-hybrid-header-first': 'theme.layout.layoutMode.top-hybrid-header-first'
|
||||||
|
@ -42,6 +42,11 @@ const headerProps = computed(() => {
|
|||||||
showMenu: false,
|
showMenu: false,
|
||||||
showMenuToggler: false
|
showMenuToggler: false
|
||||||
},
|
},
|
||||||
|
'vertical-hybrid-header-first': {
|
||||||
|
showLogo: !isActiveFirstLevelMenuHasChildren.value,
|
||||||
|
showMenu: true,
|
||||||
|
showMenuToggler: false
|
||||||
|
},
|
||||||
horizontal: {
|
horizontal: {
|
||||||
showLogo: true,
|
showLogo: true,
|
||||||
showMenu: true,
|
showMenu: true,
|
||||||
@ -66,6 +71,8 @@ const siderVisible = computed(() => themeStore.layout.mode !== 'horizontal');
|
|||||||
|
|
||||||
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix');
|
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix');
|
||||||
|
|
||||||
|
const isVerticalHybridHeaderFirst = computed(() => themeStore.layout.mode === 'vertical-hybrid-header-first');
|
||||||
|
|
||||||
const isTopHybridSidebarFirst = computed(() => themeStore.layout.mode === 'top-hybrid-sidebar-first');
|
const isTopHybridSidebarFirst = computed(() => themeStore.layout.mode === 'top-hybrid-sidebar-first');
|
||||||
|
|
||||||
const isTopHybridHeaderFirst = computed(() => themeStore.layout.mode === 'top-hybrid-header-first');
|
const isTopHybridHeaderFirst = computed(() => themeStore.layout.mode === 'top-hybrid-header-first');
|
||||||
@ -74,36 +81,46 @@ const siderWidth = computed(() => getSiderWidth());
|
|||||||
|
|
||||||
const siderCollapsedWidth = computed(() => getSiderCollapsedWidth());
|
const siderCollapsedWidth = computed(() => getSiderCollapsedWidth());
|
||||||
|
|
||||||
function getSiderWidth() {
|
function getSiderAndCollapsedWidth(isCollapsed: boolean) {
|
||||||
const { width, mixWidth, mixChildMenuWidth } = themeStore.sider;
|
const {
|
||||||
|
mixChildMenuWidth,
|
||||||
|
collapsedWidth,
|
||||||
|
width: themeWidth,
|
||||||
|
mixCollapsedWidth,
|
||||||
|
mixWidth: themeMixWidth
|
||||||
|
} = themeStore.sider;
|
||||||
|
|
||||||
|
const width = isCollapsed ? collapsedWidth : themeWidth;
|
||||||
|
const mixWidth = isCollapsed ? mixCollapsedWidth : themeMixWidth;
|
||||||
|
|
||||||
if (isTopHybridHeaderFirst.value) {
|
if (isTopHybridHeaderFirst.value) {
|
||||||
return isActiveFirstLevelMenuHasChildren.value ? width : 0;
|
return isActiveFirstLevelMenuHasChildren.value ? width : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let w = isVerticalMix.value || isTopHybridSidebarFirst.value ? mixWidth : width;
|
if (isVerticalHybridHeaderFirst.value && !isActiveFirstLevelMenuHasChildren.value) {
|
||||||
|
return 0;
|
||||||
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
|
|
||||||
w += mixChildMenuWidth;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return w;
|
const isMixMode = isVerticalMix.value || isTopHybridSidebarFirst.value || isVerticalHybridHeaderFirst.value;
|
||||||
|
let finalWidth = isMixMode ? mixWidth : width;
|
||||||
|
|
||||||
|
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
|
||||||
|
finalWidth += mixChildMenuWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVerticalHybridHeaderFirst.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
|
||||||
|
finalWidth += mixChildMenuWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSiderWidth() {
|
||||||
|
return getSiderAndCollapsedWidth(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSiderCollapsedWidth() {
|
function getSiderCollapsedWidth() {
|
||||||
const { collapsedWidth, mixCollapsedWidth, mixChildMenuWidth } = themeStore.sider;
|
return getSiderAndCollapsedWidth(true);
|
||||||
|
|
||||||
if (isTopHybridHeaderFirst.value) {
|
|
||||||
return isActiveFirstLevelMenuHasChildren.value ? collapsedWidth : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let w = isVerticalMix.value || isTopHybridSidebarFirst.value ? mixCollapsedWidth : collapsedWidth;
|
|
||||||
|
|
||||||
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
|
|
||||||
w += mixChildMenuWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
return w;
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useContext } from '@sa/hooks';
|
import { useContext } from '@sa/hooks';
|
||||||
|
import type { RouteKey } from '@elegant-router/types';
|
||||||
import { useRouteStore } from '@/store/modules/route';
|
import { useRouteStore } from '@/store/modules/route';
|
||||||
|
import { useRouterPush } from '@/hooks/common/router';
|
||||||
|
|
||||||
export const { setupStore: setupMixMenuContext, useStore: useMixMenuContext } = useContext('mix-menu', useMixMenu);
|
export const { setupStore: setupMixMenuContext, useStore: useMixMenuContext } = useContext('mix-menu', useMixMenu);
|
||||||
|
|
||||||
@ -9,6 +11,17 @@ function useMixMenu() {
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const routeStore = useRouteStore();
|
const routeStore = useRouteStore();
|
||||||
const { selectedKey } = useMenu();
|
const { selectedKey } = useMenu();
|
||||||
|
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||||
|
|
||||||
|
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 activeFirstLevelMenuKey = ref('');
|
const activeFirstLevelMenuKey = ref('');
|
||||||
|
|
||||||
@ -22,20 +35,6 @@ function useMixMenu() {
|
|||||||
setActiveFirstLevelMenuKey(firstLevelRouteName);
|
setActiveFirstLevelMenuKey(firstLevelRouteName);
|
||||||
}
|
}
|
||||||
|
|
||||||
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(() => {
|
const isActiveFirstLevelMenuHasChildren = computed(() => {
|
||||||
if (!activeFirstLevelMenuKey.value) {
|
if (!activeFirstLevelMenuKey.value) {
|
||||||
return false;
|
return false;
|
||||||
@ -46,6 +45,54 @@ function useMixMenu() {
|
|||||||
return Boolean(findItem?.children?.length);
|
return Boolean(findItem?.children?.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function handleSelectFirstLevelMenu(key: RouteKey) {
|
||||||
|
setActiveFirstLevelMenuKey(key);
|
||||||
|
|
||||||
|
if (!isActiveFirstLevelMenuHasChildren.value) {
|
||||||
|
routerPushByKeyWithMetaQuery(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const secondLevelMenus = computed<App.Global.Menu[]>(
|
||||||
|
() => allMenus.value.find(menu => menu.key === activeFirstLevelMenuKey.value)?.children || []
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeSecondLevelMenuKey = ref('');
|
||||||
|
|
||||||
|
function setActiveSecondLevelMenuKey(key: string) {
|
||||||
|
activeSecondLevelMenuKey.value = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveSecondLevelMenuKey() {
|
||||||
|
const [firstLevelRouteName, level2SuffixName] = selectedKey.value.split('_');
|
||||||
|
|
||||||
|
const secondLevelRouteName = `${firstLevelRouteName}_${level2SuffixName}`;
|
||||||
|
|
||||||
|
setActiveSecondLevelMenuKey(secondLevelRouteName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isActiveSecondLevelMenuHasChildren = computed(() => {
|
||||||
|
if (!activeSecondLevelMenuKey.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const findItem = secondLevelMenus.value.find(item => item.key === activeSecondLevelMenuKey.value);
|
||||||
|
|
||||||
|
return Boolean(findItem?.children?.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSelectSecondLevelMenu(key: RouteKey) {
|
||||||
|
setActiveSecondLevelMenuKey(key);
|
||||||
|
|
||||||
|
if (!isActiveSecondLevelMenuHasChildren.value) {
|
||||||
|
routerPushByKeyWithMetaQuery(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const childLevelMenus = computed<App.Global.Menu[]>(
|
||||||
|
() => secondLevelMenus.value.find(menu => menu.key === activeSecondLevelMenuKey.value)?.children || []
|
||||||
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.name,
|
() => route.name,
|
||||||
() => {
|
() => {
|
||||||
@ -55,13 +102,19 @@ function useMixMenu() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
allMenus,
|
|
||||||
firstLevelMenus,
|
firstLevelMenus,
|
||||||
childLevelMenus,
|
|
||||||
isActiveFirstLevelMenuHasChildren,
|
|
||||||
activeFirstLevelMenuKey,
|
activeFirstLevelMenuKey,
|
||||||
setActiveFirstLevelMenuKey,
|
setActiveFirstLevelMenuKey,
|
||||||
getActiveFirstLevelMenuKey
|
isActiveFirstLevelMenuHasChildren,
|
||||||
|
handleSelectFirstLevelMenu,
|
||||||
|
getActiveFirstLevelMenuKey,
|
||||||
|
secondLevelMenus,
|
||||||
|
activeSecondLevelMenuKey,
|
||||||
|
setActiveSecondLevelMenuKey,
|
||||||
|
isActiveSecondLevelMenuHasChildren,
|
||||||
|
handleSelectSecondLevelMenu,
|
||||||
|
getActiveSecondLevelMenuKey,
|
||||||
|
childLevelMenus
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import { computed } from 'vue';
|
|||||||
import { createReusableTemplate } from '@vueuse/core';
|
import { createReusableTemplate } from '@vueuse/core';
|
||||||
import { SimpleScrollbar } from '@sa/materials';
|
import { SimpleScrollbar } from '@sa/materials';
|
||||||
import { transformColorWithOpacity } from '@sa/color';
|
import { transformColorWithOpacity } from '@sa/color';
|
||||||
|
import type { RouteKey } from '@elegant-router/types';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'FirstLevelMenu'
|
name: 'FirstLevelMenu'
|
||||||
@ -20,7 +21,7 @@ interface Props {
|
|||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: 'select', menu: App.Global.Menu): boolean;
|
(e: 'select', menuKey: RouteKey): boolean;
|
||||||
(e: 'toggleSiderCollapse'): void;
|
(e: 'toggleSiderCollapse'): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,8 +48,8 @@ const selectedBgColor = computed(() => {
|
|||||||
return darkMode ? dark : light;
|
return darkMode ? dark : light;
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleClickMixMenu(menu: App.Global.Menu) {
|
function handleClickMixMenu(menuKey: RouteKey) {
|
||||||
emit('select', menu);
|
emit('select', menuKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSiderCollapse() {
|
function toggleSiderCollapse() {
|
||||||
@ -88,7 +89,7 @@ function toggleSiderCollapse() {
|
|||||||
:icon="menu.icon"
|
:icon="menu.icon"
|
||||||
:active="menu.key === activeMenuKey"
|
:active="menu.key === activeMenuKey"
|
||||||
:is-mini="siderCollapse"
|
:is-mini="siderCollapse"
|
||||||
@click="handleClickMixMenu(menu)"
|
@click="handleClickMixMenu(menu.routeKey)"
|
||||||
/>
|
/>
|
||||||
</SimpleScrollbar>
|
</SimpleScrollbar>
|
||||||
<MenuToggler
|
<MenuToggler
|
||||||
|
@ -5,6 +5,7 @@ import { useAppStore } from '@/store/modules/app';
|
|||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import VerticalMenu from './modules/vertical-menu.vue';
|
import VerticalMenu from './modules/vertical-menu.vue';
|
||||||
import VerticalMixMenu from './modules/vertical-mix-menu.vue';
|
import VerticalMixMenu from './modules/vertical-mix-menu.vue';
|
||||||
|
import VerticalHybridHeaderFirst from './modules/vertical-hybrid-header-first.vue';
|
||||||
import HorizontalMenu from './modules/horizontal-menu.vue';
|
import HorizontalMenu from './modules/horizontal-menu.vue';
|
||||||
import TopHybridSidebarFirst from './modules/top-hybrid-sidebar-first.vue';
|
import TopHybridSidebarFirst from './modules/top-hybrid-sidebar-first.vue';
|
||||||
import TopHybridHeaderFirst from './modules/top-hybrid-header-first.vue';
|
import TopHybridHeaderFirst from './modules/top-hybrid-header-first.vue';
|
||||||
@ -20,6 +21,7 @@ const activeMenu = computed(() => {
|
|||||||
const menuMap: Record<UnionKey.ThemeLayoutMode, Component> = {
|
const menuMap: Record<UnionKey.ThemeLayoutMode, Component> = {
|
||||||
vertical: VerticalMenu,
|
vertical: VerticalMenu,
|
||||||
'vertical-mix': VerticalMixMenu,
|
'vertical-mix': VerticalMixMenu,
|
||||||
|
'vertical-hybrid-header-first': VerticalHybridHeaderFirst,
|
||||||
horizontal: HorizontalMenu,
|
horizontal: HorizontalMenu,
|
||||||
'top-hybrid-sidebar-first': TopHybridSidebarFirst,
|
'top-hybrid-sidebar-first': TopHybridSidebarFirst,
|
||||||
'top-hybrid-header-first': TopHybridHeaderFirst
|
'top-hybrid-header-first': TopHybridHeaderFirst
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import type { RouteKey } from '@elegant-router/types';
|
|
||||||
import { SimpleScrollbar } from '@sa/materials';
|
import { SimpleScrollbar } from '@sa/materials';
|
||||||
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
|
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
|
||||||
import { useAppStore } from '@/store/modules/app';
|
import { useAppStore } from '@/store/modules/app';
|
||||||
@ -11,7 +10,7 @@ import { useRouterPush } from '@/hooks/common/router';
|
|||||||
import { useMenu, useMixMenuContext } from '../../../context';
|
import { useMenu, useMixMenuContext } from '../../../context';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'ReversedHorizontalMixMenu'
|
name: 'TopHybridHeaderFirst'
|
||||||
});
|
});
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@ -19,23 +18,9 @@ const appStore = useAppStore();
|
|||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
const routeStore = useRouteStore();
|
const routeStore = useRouteStore();
|
||||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||||
const {
|
const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } = useMixMenuContext();
|
||||||
firstLevelMenus,
|
|
||||||
childLevelMenus,
|
|
||||||
activeFirstLevelMenuKey,
|
|
||||||
setActiveFirstLevelMenuKey,
|
|
||||||
isActiveFirstLevelMenuHasChildren
|
|
||||||
} = useMixMenuContext();
|
|
||||||
const { selectedKey } = useMenu();
|
const { selectedKey } = useMenu();
|
||||||
|
|
||||||
function handleSelectMixMenu(key: RouteKey) {
|
|
||||||
setActiveFirstLevelMenuKey(key);
|
|
||||||
|
|
||||||
if (!isActiveFirstLevelMenuHasChildren.value) {
|
|
||||||
routerPushByKeyWithMetaQuery(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const expandedKeys = ref<string[]>([]);
|
const expandedKeys = ref<string[]>([]);
|
||||||
|
|
||||||
function updateExpandedKeys() {
|
function updateExpandedKeys() {
|
||||||
@ -63,7 +48,7 @@ watch(
|
|||||||
:options="firstLevelMenus"
|
:options="firstLevelMenus"
|
||||||
:indent="18"
|
:indent="18"
|
||||||
responsive
|
responsive
|
||||||
@update:value="handleSelectMixMenu"
|
@update:value="handleSelectFirstLevelMenu"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||||||
@ -75,7 +60,7 @@ watch(
|
|||||||
:collapsed="appStore.siderCollapse"
|
:collapsed="appStore.siderCollapse"
|
||||||
:collapsed-width="themeStore.sider.collapsedWidth"
|
:collapsed-width="themeStore.sider.collapsedWidth"
|
||||||
:collapsed-icon-size="22"
|
:collapsed-icon-size="22"
|
||||||
:options="childLevelMenus"
|
:options="secondLevelMenus"
|
||||||
:indent="18"
|
:indent="18"
|
||||||
@update:value="routerPushByKeyWithMetaQuery"
|
@update:value="routerPushByKeyWithMetaQuery"
|
||||||
/>
|
/>
|
||||||
|
@ -7,22 +7,14 @@ import FirstLevelMenu from '../components/first-level-menu.vue';
|
|||||||
import { useMenu, useMixMenuContext } from '../../../context';
|
import { useMenu, useMixMenuContext } from '../../../context';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'HorizontalMixMenu'
|
name: 'TopHybridSidebarFirst'
|
||||||
});
|
});
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||||
const { allMenus, childLevelMenus, activeFirstLevelMenuKey, setActiveFirstLevelMenuKey } = useMixMenuContext();
|
const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } = useMixMenuContext();
|
||||||
const { selectedKey } = useMenu();
|
const { selectedKey } = useMenu();
|
||||||
|
|
||||||
function handleSelectMixMenu(menu: App.Global.Menu) {
|
|
||||||
setActiveFirstLevelMenuKey(menu.key);
|
|
||||||
|
|
||||||
if (!menu.children?.length) {
|
|
||||||
routerPushByKeyWithMetaQuery(menu.routeKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -30,7 +22,7 @@ function handleSelectMixMenu(menu: App.Global.Menu) {
|
|||||||
<NMenu
|
<NMenu
|
||||||
mode="horizontal"
|
mode="horizontal"
|
||||||
:value="selectedKey"
|
:value="selectedKey"
|
||||||
:options="childLevelMenus"
|
:options="secondLevelMenus"
|
||||||
:indent="18"
|
:indent="18"
|
||||||
responsive
|
responsive
|
||||||
@update:value="routerPushByKeyWithMetaQuery"
|
@update:value="routerPushByKeyWithMetaQuery"
|
||||||
@ -38,12 +30,12 @@ function handleSelectMixMenu(menu: App.Global.Menu) {
|
|||||||
</Teleport>
|
</Teleport>
|
||||||
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||||||
<FirstLevelMenu
|
<FirstLevelMenu
|
||||||
:menus="allMenus"
|
:menus="firstLevelMenus"
|
||||||
:active-menu-key="activeFirstLevelMenuKey"
|
:active-menu-key="activeFirstLevelMenuKey"
|
||||||
:sider-collapse="appStore.siderCollapse"
|
:sider-collapse="appStore.siderCollapse"
|
||||||
:dark-mode="themeStore.darkMode"
|
:dark-mode="themeStore.darkMode"
|
||||||
:theme-color="themeStore.themeColor"
|
:theme-color="themeStore.themeColor"
|
||||||
@select="handleSelectMixMenu"
|
@select="handleSelectFirstLevelMenu"
|
||||||
@toggle-sider-collapse="appStore.toggleSiderCollapse"
|
@toggle-sider-collapse="appStore.toggleSiderCollapse"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
@ -0,0 +1,149 @@
|
|||||||
|
<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 { useBoolean } from '@sa/hooks';
|
||||||
|
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 { useMenu, useMixMenuContext } from '../../../context';
|
||||||
|
import FirstLevelMenu from '../components/first-level-menu.vue';
|
||||||
|
import GlobalLogo from '../../global-logo/index.vue';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'VerticalHybridHeaderFirst'
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
const routeStore = useRouteStore();
|
||||||
|
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||||
|
const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean();
|
||||||
|
const {
|
||||||
|
firstLevelMenus,
|
||||||
|
activeFirstLevelMenuKey,
|
||||||
|
handleSelectFirstLevelMenu,
|
||||||
|
getActiveFirstLevelMenuKey,
|
||||||
|
secondLevelMenus,
|
||||||
|
activeSecondLevelMenuKey,
|
||||||
|
isActiveSecondLevelMenuHasChildren,
|
||||||
|
handleSelectSecondLevelMenu,
|
||||||
|
getActiveSecondLevelMenuKey,
|
||||||
|
childLevelMenus
|
||||||
|
} = useMixMenuContext();
|
||||||
|
const { selectedKey } = useMenu();
|
||||||
|
|
||||||
|
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(key: RouteKey) {
|
||||||
|
handleSelectSecondLevelMenu(key);
|
||||||
|
|
||||||
|
if (isActiveSecondLevelMenuHasChildren.value) {
|
||||||
|
setDrawerVisible(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelectMenu(key: RouteKey) {
|
||||||
|
handleSelectFirstLevelMenu(key);
|
||||||
|
|
||||||
|
if (secondLevelMenus.value.length > 0) {
|
||||||
|
handleSelectMixMenu(secondLevelMenus.value[0].routeKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResetActiveMenu() {
|
||||||
|
setDrawerVisible(false);
|
||||||
|
|
||||||
|
if (!appStore.mixSiderFixed) {
|
||||||
|
getActiveFirstLevelMenuKey();
|
||||||
|
getActiveSecondLevelMenuKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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="handleSelectMenu"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||||||
|
<div class="h-full flex" @mouseleave="handleResetActiveMenu">
|
||||||
|
<FirstLevelMenu
|
||||||
|
:menus="secondLevelMenus"
|
||||||
|
:active-menu-key="activeSecondLevelMenuKey"
|
||||||
|
: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"
|
||||||
|
:value="selectedKey"
|
||||||
|
:options="childLevelMenus"
|
||||||
|
:inverted="inverted"
|
||||||
|
:indent="18"
|
||||||
|
@update:value="routerPushByKeyWithMetaQuery"
|
||||||
|
/>
|
||||||
|
</SimpleScrollbar>
|
||||||
|
</DarkModeContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
@ -3,6 +3,7 @@ import { computed, ref, watch } from 'vue';
|
|||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { SimpleScrollbar } from '@sa/materials';
|
import { SimpleScrollbar } from '@sa/materials';
|
||||||
import { useBoolean } from '@sa/hooks';
|
import { useBoolean } from '@sa/hooks';
|
||||||
|
import type { RouteKey } from '@elegant-router/types';
|
||||||
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
|
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
|
||||||
import { useAppStore } from '@/store/modules/app';
|
import { useAppStore } from '@/store/modules/app';
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
@ -24,28 +25,26 @@ const routeStore = useRouteStore();
|
|||||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||||
const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean();
|
const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean();
|
||||||
const {
|
const {
|
||||||
allMenus,
|
firstLevelMenus,
|
||||||
childLevelMenus,
|
secondLevelMenus,
|
||||||
activeFirstLevelMenuKey,
|
activeFirstLevelMenuKey,
|
||||||
setActiveFirstLevelMenuKey,
|
isActiveFirstLevelMenuHasChildren,
|
||||||
getActiveFirstLevelMenuKey
|
getActiveFirstLevelMenuKey,
|
||||||
//
|
handleSelectFirstLevelMenu
|
||||||
} = useMixMenuContext();
|
} = useMixMenuContext();
|
||||||
const { selectedKey } = useMenu();
|
const { selectedKey } = useMenu();
|
||||||
|
|
||||||
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
|
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
|
||||||
|
|
||||||
const hasChildMenus = computed(() => childLevelMenus.value.length > 0);
|
const hasChildMenus = computed(() => secondLevelMenus.value.length > 0);
|
||||||
|
|
||||||
const showDrawer = computed(() => hasChildMenus.value && (drawerVisible.value || appStore.mixSiderFixed));
|
const showDrawer = computed(() => hasChildMenus.value && (drawerVisible.value || appStore.mixSiderFixed));
|
||||||
|
|
||||||
function handleSelectMixMenu(menu: App.Global.Menu) {
|
function handleSelectMenu(key: RouteKey) {
|
||||||
setActiveFirstLevelMenuKey(menu.key);
|
handleSelectFirstLevelMenu(key);
|
||||||
|
|
||||||
if (menu.children?.length) {
|
if (isActiveFirstLevelMenuHasChildren.value) {
|
||||||
setDrawerVisible(true);
|
setDrawerVisible(true);
|
||||||
} else {
|
|
||||||
routerPushByKeyWithMetaQuery(menu.routeKey);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,13 +79,13 @@ watch(
|
|||||||
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||||||
<div class="h-full flex" @mouseleave="handleResetActiveMenu">
|
<div class="h-full flex" @mouseleave="handleResetActiveMenu">
|
||||||
<FirstLevelMenu
|
<FirstLevelMenu
|
||||||
:menus="allMenus"
|
:menus="firstLevelMenus"
|
||||||
:active-menu-key="activeFirstLevelMenuKey"
|
:active-menu-key="activeFirstLevelMenuKey"
|
||||||
:inverted="inverted"
|
:inverted="inverted"
|
||||||
:sider-collapse="appStore.siderCollapse"
|
:sider-collapse="appStore.siderCollapse"
|
||||||
:dark-mode="themeStore.darkMode"
|
:dark-mode="themeStore.darkMode"
|
||||||
:theme-color="themeStore.themeColor"
|
:theme-color="themeStore.themeColor"
|
||||||
@select="handleSelectMixMenu"
|
@select="handleSelectMenu"
|
||||||
@toggle-sider-collapse="appStore.toggleSiderCollapse"
|
@toggle-sider-collapse="appStore.toggleSiderCollapse"
|
||||||
>
|
>
|
||||||
<GlobalLogo :show-title="false" :style="{ height: themeStore.header.height + 'px' }" />
|
<GlobalLogo :show-title="false" :style="{ height: themeStore.header.height + 'px' }" />
|
||||||
@ -113,7 +112,7 @@ watch(
|
|||||||
v-model:expanded-keys="expandedKeys"
|
v-model:expanded-keys="expandedKeys"
|
||||||
mode="vertical"
|
mode="vertical"
|
||||||
:value="selectedKey"
|
:value="selectedKey"
|
||||||
:options="childLevelMenus"
|
:options="secondLevelMenus"
|
||||||
:inverted="inverted"
|
:inverted="inverted"
|
||||||
:indent="18"
|
:indent="18"
|
||||||
@update:value="routerPushByKeyWithMetaQuery"
|
@update:value="routerPushByKeyWithMetaQuery"
|
||||||
|
@ -12,16 +12,13 @@ defineOptions({
|
|||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix');
|
|
||||||
const isTopHybridSidebarFirst = computed(() => themeStore.layout.mode === 'top-hybrid-sidebar-first');
|
const isTopHybridSidebarFirst = computed(() => themeStore.layout.mode === 'top-hybrid-sidebar-first');
|
||||||
const isTopHybridHeaderFirst = computed(() => themeStore.layout.mode === 'top-hybrid-header-first');
|
const isTopHybridHeaderFirst = computed(() => themeStore.layout.mode === 'top-hybrid-header-first');
|
||||||
const darkMenu = computed(
|
const darkMenu = computed(
|
||||||
() =>
|
() =>
|
||||||
!themeStore.darkMode && !isTopHybridSidebarFirst.value && !isTopHybridHeaderFirst.value && themeStore.sider.inverted
|
!themeStore.darkMode && !isTopHybridSidebarFirst.value && !isTopHybridHeaderFirst.value && themeStore.sider.inverted
|
||||||
);
|
);
|
||||||
const showLogo = computed(
|
const showLogo = computed(() => themeStore.layout.mode === 'vertical');
|
||||||
() => !isVerticalMix.value && !isTopHybridSidebarFirst.value && !isTopHybridHeaderFirst.value
|
|
||||||
);
|
|
||||||
const menuWrapperClass = computed(() => (showLogo.value ? 'flex-1-hidden' : 'h-full'));
|
const menuWrapperClass = computed(() => (showLogo.value ? 'flex-1-hidden' : 'h-full'));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -43,6 +43,11 @@ const layoutConfig: LayoutConfig = {
|
|||||||
menuClass: 'w-1/4 h-full',
|
menuClass: 'w-1/4 h-full',
|
||||||
mainClass: 'w-2/3 h-3/4'
|
mainClass: 'w-2/3 h-3/4'
|
||||||
},
|
},
|
||||||
|
'vertical-hybrid-header-first': {
|
||||||
|
placement: 'bottom',
|
||||||
|
menuClass: 'w-1/4 h-full',
|
||||||
|
mainClass: 'w-2/3 h-3/4'
|
||||||
|
},
|
||||||
horizontal: {
|
horizontal: {
|
||||||
placement: 'bottom',
|
placement: 'bottom',
|
||||||
menuClass: 'w-full h-1/4',
|
menuClass: 'w-full h-1/4',
|
||||||
|
@ -30,6 +30,14 @@ const themeStore = useThemeStore();
|
|||||||
<div class="layout-main"></div>
|
<div class="layout-main"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<template #vertical-hybrid-header-first>
|
||||||
|
<div class="layout-sider h-full w-8px !bg-primary"></div>
|
||||||
|
<div class="layout-sider h-full w-16px !bg-primary-300"></div>
|
||||||
|
<div class="vertical-wrapper">
|
||||||
|
<div class="layout-header bg-primary"></div>
|
||||||
|
<div class="layout-main"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template #horizontal>
|
<template #horizontal>
|
||||||
<div class="layout-header !bg-primary"></div>
|
<div class="layout-header !bg-primary"></div>
|
||||||
<div class="horizontal-wrapper">
|
<div class="horizontal-wrapper">
|
||||||
|
@ -91,11 +91,14 @@ const local: App.I18n.Schema = {
|
|||||||
vertical: 'Vertical Mode',
|
vertical: 'Vertical Mode',
|
||||||
horizontal: 'Horizontal Mode',
|
horizontal: 'Horizontal Mode',
|
||||||
'vertical-mix': 'Vertical Mix Mode',
|
'vertical-mix': 'Vertical Mix Mode',
|
||||||
|
'vertical-hybrid-header-first': 'Left Hybrid Header-First',
|
||||||
'top-hybrid-sidebar-first': 'Top-Hybrid Sidebar-First',
|
'top-hybrid-sidebar-first': 'Top-Hybrid Sidebar-First',
|
||||||
'top-hybrid-header-first': 'Top-Hybrid Header-First',
|
'top-hybrid-header-first': 'Top-Hybrid Header-First',
|
||||||
vertical_detail: 'Vertical menu layout, with the menu on the left and content on the right.',
|
vertical_detail: 'Vertical menu layout, with the menu on the left and content on the right.',
|
||||||
'vertical-mix_detail':
|
'vertical-mix_detail':
|
||||||
'Vertical mix-menu layout, with the primary menu on the dark left side and the secondary menu on the lighter right side.',
|
'Vertical mix-menu layout, with the primary menu on the dark left side and the secondary menu on the lighter left side.',
|
||||||
|
'vertical-hybrid-header-first_detail':
|
||||||
|
'Vertical mix-menu layout, with the primary menu at the top, the secondary menu on the dark left side, and the secondary menu on the lighter left side.',
|
||||||
horizontal_detail: 'Horizontal menu layout, with the menu at the top and content below.',
|
horizontal_detail: 'Horizontal menu layout, with the menu at the top and content below.',
|
||||||
'top-hybrid-sidebar-first_detail':
|
'top-hybrid-sidebar-first_detail':
|
||||||
'Top hybrid layout, with the primary menu on the left and the secondary menu at the top.',
|
'Top hybrid layout, with the primary menu on the left and the secondary menu at the top.',
|
||||||
|
@ -90,11 +90,14 @@ const local: App.I18n.Schema = {
|
|||||||
title: '布局模式',
|
title: '布局模式',
|
||||||
vertical: '左侧菜单模式',
|
vertical: '左侧菜单模式',
|
||||||
'vertical-mix': '左侧菜单混合模式',
|
'vertical-mix': '左侧菜单混合模式',
|
||||||
|
'vertical-hybrid-header-first': '左侧混合-顶部优先',
|
||||||
horizontal: '顶部菜单模式',
|
horizontal: '顶部菜单模式',
|
||||||
'top-hybrid-sidebar-first': '顶部混合-侧边优先',
|
'top-hybrid-sidebar-first': '顶部混合-侧边优先',
|
||||||
'top-hybrid-header-first': '顶部混合-顶部优先',
|
'top-hybrid-header-first': '顶部混合-顶部优先',
|
||||||
vertical_detail: '左侧菜单布局,菜单在左,内容在右。',
|
vertical_detail: '左侧菜单布局,菜单在左,内容在右。',
|
||||||
'vertical-mix_detail': '左侧双菜单布局,一级菜单在左侧深色区域,二级菜单在右侧浅色区域。',
|
'vertical-mix_detail': '左侧双菜单布局,一级菜单在左侧深色区域,二级菜单在左侧浅色区域。',
|
||||||
|
'vertical-hybrid-header-first_detail':
|
||||||
|
'左侧混合布局,一级菜单在顶部,二级菜单在左侧浅色区域,三级菜单在左侧深色区域。',
|
||||||
horizontal_detail: '顶部菜单布局,菜单在顶部,内容在下方。',
|
horizontal_detail: '顶部菜单布局,菜单在顶部,内容在下方。',
|
||||||
'top-hybrid-sidebar-first_detail': '顶部混合布局,一级菜单在左侧,二级菜单在顶部。',
|
'top-hybrid-sidebar-first_detail': '顶部混合布局,一级菜单在左侧,二级菜单在顶部。',
|
||||||
'top-hybrid-header-first_detail': '顶部混合布局,一级菜单在顶部,二级菜单在左侧。'
|
'top-hybrid-header-first_detail': '顶部混合布局,一级菜单在顶部,二级菜单在左侧。'
|
||||||
|
1
src/typings/union-key.d.ts
vendored
1
src/typings/union-key.d.ts
vendored
@ -35,6 +35,7 @@ declare namespace UnionKey {
|
|||||||
| 'vertical'
|
| 'vertical'
|
||||||
| 'horizontal'
|
| 'horizontal'
|
||||||
| 'vertical-mix'
|
| 'vertical-mix'
|
||||||
|
| 'vertical-hybrid-header-first'
|
||||||
| 'top-hybrid-sidebar-first'
|
| 'top-hybrid-sidebar-first'
|
||||||
| 'top-hybrid-header-first';
|
| 'top-hybrid-header-first';
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user