Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion src/layouts/modules/global-menu/context/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { useRoute } from 'vue-router';
import { useContext } from '@sa/hooks';
import type { RouteKey } from '@elegant-router/types';
import { useRouteStore } from '@/store/modules/route';
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';

export const [provideMixMenuContext, useMixMenuContext] = useContext('MixMenu', useMixMenu);

function useMixMenu() {
const route = useRoute();
const routeStore = useRouteStore();
const themeStore = useThemeStore();
const { selectedKey } = useMenu();
const { routerPushByKeyWithMetaQuery } = useRouterPush();

Expand Down Expand Up @@ -100,10 +102,46 @@ function useMixMenu() {
() => secondLevelMenus.value.find(menu => menu.key === activeSecondLevelMenuKey.value)?.children || []
);

const hasChildLevelMenus = computed(() => childLevelMenus.value.length > 0);

function getDeepestLevelMenuKey(): RouteKey | null {
if (!secondLevelMenus.value.length || !themeStore.sider.autoSelectFirstMenu) {
return null;
}

const secondLevelFirstMenu = secondLevelMenus.value[0];

if (!secondLevelFirstMenu) {
return null;
}

function findDeepest(menu: App.Global.Menu): RouteKey {
if (!menu.children?.length) {
return menu.routeKey;
}

return findDeepest(menu.children[0]);
}

return findDeepest(secondLevelFirstMenu);
}

function activeDeepestLevelMenuKey() {
const deepestLevelMenuKey = getDeepestLevelMenuKey();
if (!deepestLevelMenuKey) return;

// select the deepest second level menu
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "select the deepest second level menu" is misleading. The function actually navigates to the deepest menu at any level (could be second, third, fourth level, etc.), not specifically the second level.

Consider updating to "navigate to the deepest menu" or "select and navigate to the deepest level menu" for clarity.

Suggested change
// select the deepest second level menu
// select and navigate to the deepest level menu

Copilot uses AI. Check for mistakes.
handleSelectSecondLevelMenu(deepestLevelMenuKey);
}

watch(
() => route.name,
() => {
getActiveFirstLevelMenuKey();
// if there are child level menus, get the active second level menu key
if (hasChildLevelMenus.value) {
getActiveSecondLevelMenuKey();
}
Comment on lines +141 to +144
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition if (hasChildLevelMenus.value) checks whether the currently active second-level menu (based on the old route) has children, before updating the active second-level menu key for the new route. This creates a dependency on stale state.

The getActiveSecondLevelMenuKey() should be called whenever the route changes, regardless of whether the previous menu had children. Consider removing the conditional check and always calling getActiveSecondLevelMenuKey() after getActiveFirstLevelMenuKey().

Suggested change
// if there are child level menus, get the active second level menu key
if (hasChildLevelMenus.value) {
getActiveSecondLevelMenuKey();
}
// always get the active second level menu key when the route changes
getActiveSecondLevelMenuKey();

Copilot uses AI. Check for mistakes.
Comment on lines +141 to +144
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "if there are child level menus, get the active second level menu key" is misleading. The condition checks whether the OLD active second-level menu has children (using stale state), not whether we need to update the active second-level menu for the NEW route.

Update the comment to clarify this is checking for child level menus before updating, or remove the condition altogether as suggested in the related bug comment.

Suggested change
// if there are child level menus, get the active second level menu key
if (hasChildLevelMenus.value) {
getActiveSecondLevelMenuKey();
}
getActiveSecondLevelMenuKey();

Copilot uses AI. Check for mistakes.
},
{ immediate: true }
);
Expand All @@ -121,7 +159,10 @@ function useMixMenu() {
isActiveSecondLevelMenuHasChildren,
handleSelectSecondLevelMenu,
getActiveSecondLevelMenuKey,
childLevelMenus
childLevelMenus,
hasChildLevelMenus,
getDeepestLevelMenuKey,
activeDeepestLevelMenuKey
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { SimpleScrollbar } from '@sa/materials';
import type { RouteKey } from '@elegant-router/types';
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
Expand All @@ -18,12 +19,28 @@ const appStore = useAppStore();
const themeStore = useThemeStore();
const routeStore = useRouteStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } =
useMixMenuContext('TopHybridHeaderFirst');
const {
firstLevelMenus,
secondLevelMenus,
activeFirstLevelMenuKey,
handleSelectFirstLevelMenu,
activeDeepestLevelMenuKey
} = useMixMenuContext('TopHybridHeaderFirst');
const { selectedKey } = useMenu();

const expandedKeys = ref<string[]>([]);

/**
* Handle first level menu select
* @param key RouteKey
*/
function handleSelectMenu(key: RouteKey) {
handleSelectFirstLevelMenu(key);

// if there are second level menus, select the deepest one by default
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states "if there are second level menus, select the deepest one by default", but this is inaccurate. The activeDeepestLevelMenuKey() function only navigates to the deepest menu when themeStore.sider.autoSelectFirstMenu is enabled. When disabled, the function does nothing.

Update the comment to accurately reflect that navigation depends on the autoSelectFirstMenu setting, e.g., "if there are second level menus and autoSelectFirstMenu is enabled, select the deepest one".

Suggested change
// if there are second level menus, select the deepest one by default
// if there are second level menus and autoSelectFirstMenu is enabled, select the deepest one

Copilot uses AI. Check for mistakes.
activeDeepestLevelMenuKey();
}

function updateExpandedKeys() {
if (appStore.siderCollapse || !selectedKey.value) {
expandedKeys.value = [];
Expand All @@ -49,7 +66,7 @@ watch(
:options="firstLevelMenus"
:indent="18"
responsive
@update:value="handleSelectFirstLevelMenu"
@update:value="handleSelectMenu"
/>
</Teleport>
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script setup lang="ts">
import type { RouteKey } from '@elegant-router/types';
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
Expand All @@ -13,9 +14,25 @@ defineOptions({
const appStore = useAppStore();
const themeStore = useThemeStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } =
useMixMenuContext('TopHybridSidebarFirst');
const {
firstLevelMenus,
secondLevelMenus,
activeFirstLevelMenuKey,
handleSelectFirstLevelMenu,
activeDeepestLevelMenuKey
} = useMixMenuContext('TopHybridSidebarFirst');
const { selectedKey } = useMenu();

/**
* Handle first level menu select
* @param key RouteKey
*/
function handleSelectMenu(key: RouteKey) {
handleSelectFirstLevelMenu(key);

// if there are second level menus, select the deepest one by default
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states "if there are second level menus, select the deepest one by default", but this is inaccurate. The activeDeepestLevelMenuKey() function only navigates to the deepest menu when themeStore.sider.autoSelectFirstMenu is enabled. When disabled, the function does nothing.

Update the comment to accurately reflect that navigation depends on the autoSelectFirstMenu setting, e.g., "if there are second level menus and autoSelectFirstMenu is enabled, select the deepest one".

Suggested change
// if there are second level menus, select the deepest one by default
// if there are second level menus and autoSelectFirstMenu is enabled, select the deepest one

Copilot uses AI. Check for mistakes.
activeDeepestLevelMenuKey();
}
</script>

<template>
Expand All @@ -37,7 +54,7 @@ const { selectedKey } = useMenu();
:sider-collapse="appStore.siderCollapse"
:dark-mode="themeStore.darkMode"
:theme-color="themeStore.themeColor"
@select="handleSelectFirstLevelMenu"
@select="handleSelectMenu"
@toggle-sider-collapse="appStore.toggleSiderCollapse"
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ const {
isActiveSecondLevelMenuHasChildren,
handleSelectSecondLevelMenu,
getActiveSecondLevelMenuKey,
childLevelMenus
childLevelMenus,
hasChildLevelMenus,
activeDeepestLevelMenuKey
} = useMixMenuContext('VerticalHybridHeaderFirst');
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));
const showDrawer = computed(() => hasChildLevelMenus.value && (drawerVisible.value || appStore.mixSiderFixed));

function handleSelectMixMenu(key: RouteKey) {
handleSelectSecondLevelMenu(key);
Expand All @@ -51,12 +51,33 @@ function handleSelectMixMenu(key: RouteKey) {
}
}

/**
* Handle second level menu selection based on autoSelectFirstMenu setting:
* - When disabled: Activate first second-level menu for display only, expand third-level menu if exists
* - When enabled: Navigate to the deepest menu automatically
*/
function handleSelectMenu(key: RouteKey) {
handleSelectFirstLevelMenu(key);

if (secondLevelMenus.value.length > 0) {
handleSelectMixMenu(secondLevelMenus.value[0].routeKey);
if (secondLevelMenus.value.length === 0) return;

const secondFirstMenuKey = secondLevelMenus.value[0].routeKey;

// Case 1: autoSelectFirstMenu disabled - only activate menu for display
if (!themeStore.sider.autoSelectFirstMenu) {
// Check if there are third-level menus
const hasChildren = secondLevelMenus.value.find(menu => menu.key === secondFirstMenuKey)?.children?.length;

// If there are third-level menus, expand them
if (hasChildren) {
handleSelectMixMenu(secondFirstMenuKey);
}
return;
}

// Case 2: autoSelectFirstMenu enabled - navigate to deepest menu
activeDeepestLevelMenuKey();
setDrawerVisible(false);
}

function handleResetActiveMenu() {
Expand Down Expand Up @@ -114,7 +135,9 @@ watch(
</FirstLevelMenu>
<div
class="relative h-full transition-width-300"
:style="{ width: appStore.mixSiderFixed && hasChildMenus ? themeStore.sider.mixChildMenuWidth + 'px' : '0px' }"
:style="{
width: appStore.mixSiderFixed && hasChildLevelMenus ? themeStore.sider.mixChildMenuWidth + 'px' : '0px'
}"
>
<DarkModeContainer
class="absolute-lt h-full flex-col-stretch nowrap-hidden shadow-sm transition-all-300"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const themeStore = useThemeStore();

const layoutMode = computed(() => themeStore.layout.mode);
const isMixLayoutMode = computed(() => layoutMode.value.includes('mix') || layoutMode.value.includes('hybrid'));
const isHybridLayoutMode = computed(() => layoutMode.value.includes('hybrid'));
</script>

<template>
Expand All @@ -32,6 +33,12 @@ const isMixLayoutMode = computed(() => layoutMode.value.includes('mix') || layou
<SettingItem v-if="layoutMode === 'vertical-mix'" key="5" :label="$t('theme.layout.sider.mixChildMenuWidth')">
<NInputNumber v-model:value="themeStore.sider.mixChildMenuWidth" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem v-if="isHybridLayoutMode" key="6" :label="$t('theme.layout.sider.autoSelectFirstMenu')">
<template #suffix>
<IconTooltip :desc="$t('theme.layout.sider.autoSelectFirstMenuTip')" />
</template>
<NSwitch v-model:value="themeStore.sider.autoSelectFirstMenu" />
</SettingItem>
</TransitionGroup>
</template>

Expand Down
5 changes: 4 additions & 1 deletion src/locales/langs/en-us.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,10 @@ const local: App.I18n.Schema = {
collapsedWidth: 'Sider Collapsed Width',
mixWidth: 'Mix Sider Width',
mixCollapsedWidth: 'Mix Sider Collapse Width',
mixChildMenuWidth: 'Mix Child Menu Width'
mixChildMenuWidth: 'Mix Child Menu Width',
autoSelectFirstMenu: 'Auto Select First Submenu',
autoSelectFirstMenuTip:
'When a first-level menu is clicked, the first submenu is automatically selected and navigated to the deepest level'
},
footer: {
title: 'Footer Settings',
Expand Down
4 changes: 3 additions & 1 deletion src/locales/langs/zh-cn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,9 @@ const local: App.I18n.Schema = {
collapsedWidth: '侧边栏折叠宽度',
mixWidth: '混合布局侧边栏宽度',
mixCollapsedWidth: '混合布局侧边栏折叠宽度',
mixChildMenuWidth: '混合布局子菜单宽度'
mixChildMenuWidth: '混合布局子菜单宽度',
autoSelectFirstMenu: '自动选择第一个子菜单',
autoSelectFirstMenuTip: '点击一级菜单时,自动选择并导航到第一个子菜单的最深层级'
},
footer: {
title: '底部设置',
Expand Down
3 changes: 2 additions & 1 deletion src/theme/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ export const themeSettings: App.Theme.ThemeSetting = {
collapsedWidth: 64,
mixWidth: 90,
mixCollapsedWidth: 64,
mixChildMenuWidth: 200
mixChildMenuWidth: 200,
autoSelectFirstMenu: false
},
footer: {
visible: true,
Expand Down
4 changes: 4 additions & 0 deletions src/typings/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ declare namespace App {
mixCollapsedWidth: number;
/** Child menu width when the layout is 'vertical-mix', 'top-hybrid-sidebar-first', or 'top-hybrid-header-first' */
mixChildMenuWidth: number;
/** Whether to auto select the first submenu */
autoSelectFirstMenu: boolean;
};
/** Footer */
footer: {
Expand Down Expand Up @@ -429,6 +431,8 @@ declare namespace App {
mixWidth: string;
mixCollapsedWidth: string;
mixChildMenuWidth: string;
autoSelectFirstMenu: string;
autoSelectFirstMenuTip: string;
};
footer: {
title: string;
Expand Down