mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-12-06 00:06:01 +08:00
Merge branch 'main' into example
This commit is contained in:
@@ -3,6 +3,7 @@ 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);
|
||||
@@ -10,6 +11,7 @@ export const [provideMixMenuContext, useMixMenuContext] = useContext('MixMenu',
|
||||
function useMixMenu() {
|
||||
const route = useRoute();
|
||||
const routeStore = useRouteStore();
|
||||
const themeStore = useThemeStore();
|
||||
const { selectedKey } = useMenu();
|
||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||
|
||||
@@ -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
|
||||
handleSelectSecondLevelMenu(deepestLevelMenuKey);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.name,
|
||||
() => {
|
||||
getActiveFirstLevelMenuKey();
|
||||
// if there are child level menus, get the active second level menu key
|
||||
if (hasChildLevelMenus.value) {
|
||||
getActiveSecondLevelMenuKey();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
@@ -121,7 +159,10 @@ function useMixMenu() {
|
||||
isActiveSecondLevelMenuHasChildren,
|
||||
handleSelectSecondLevelMenu,
|
||||
getActiveSecondLevelMenuKey,
|
||||
childLevelMenus
|
||||
childLevelMenus,
|
||||
hasChildLevelMenus,
|
||||
getDeepestLevelMenuKey,
|
||||
activeDeepestLevelMenuKey
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -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
|
||||
activeDeepestLevelMenuKey();
|
||||
}
|
||||
|
||||
function updateExpandedKeys() {
|
||||
if (appStore.siderCollapse || !selectedKey.value) {
|
||||
expandedKeys.value = [];
|
||||
@@ -49,7 +66,7 @@ watch(
|
||||
:options="firstLevelMenus"
|
||||
:indent="18"
|
||||
responsive
|
||||
@update:value="handleSelectFirstLevelMenu"
|
||||
@update:value="handleSelectMenu"
|
||||
/>
|
||||
</Teleport>
|
||||
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||||
|
||||
@@ -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';
|
||||
@@ -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
|
||||
activeDeepestLevelMenuKey();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
@@ -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() {
|
||||
@@ -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"
|
||||
|
||||
@@ -26,7 +26,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
const visible = defineModel<boolean>('visible');
|
||||
|
||||
const { removeTab, clearTabs, clearLeftTabs, clearRightTabs } = useTabStore();
|
||||
const { removeTab, clearTabs, clearLeftTabs, clearRightTabs, fixTab, unfixTab, isTabRetain } = useTabStore();
|
||||
const { SvgIconVNode } = useSvgIcon();
|
||||
|
||||
type DropdownOption = {
|
||||
@@ -64,6 +64,23 @@ const options = computed(() => {
|
||||
icon: SvgIconVNode({ icon: 'ant-design:line-outlined', fontSize: 18 })
|
||||
}
|
||||
];
|
||||
|
||||
if (props.tabId !== '/home') {
|
||||
if (isTabRetain(props.tabId)) {
|
||||
opts.push({
|
||||
key: 'unpin',
|
||||
label: $t('dropdown.unpin'),
|
||||
icon: SvgIconVNode({ icon: 'mdi:pin-off-outline', fontSize: 18 })
|
||||
});
|
||||
} else {
|
||||
opts.push({
|
||||
key: 'pin',
|
||||
label: $t('dropdown.pin'),
|
||||
icon: SvgIconVNode({ icon: 'mdi:pin-outline', fontSize: 18 })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const { excludeKeys, disabledKeys } = props;
|
||||
|
||||
const result = opts.filter(opt => !excludeKeys.includes(opt.key));
|
||||
@@ -98,6 +115,12 @@ const dropdownAction: Record<App.Global.DropdownKey, () => void> = {
|
||||
},
|
||||
closeAll() {
|
||||
clearTabs();
|
||||
},
|
||||
pin() {
|
||||
fixTab(props.tabId);
|
||||
},
|
||||
unpin() {
|
||||
unfixTab(props.tabId);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { defu } from 'defu';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { themeSettings } from '@/theme/settings';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
@@ -31,6 +33,8 @@ type ThemePreset = Pick<
|
||||
desc: string;
|
||||
i18nkey?: string;
|
||||
version: string;
|
||||
/** Optional NaiveUI theme overrides */
|
||||
naiveui?: App.Theme.NaiveUIThemeOverride;
|
||||
};
|
||||
|
||||
const presetModules = import.meta.glob('@/theme/preset/*.json', { eager: true, import: 'default' });
|
||||
@@ -76,7 +80,9 @@ const getPresetDesc = (preset: ThemePreset): string => {
|
||||
}
|
||||
};
|
||||
|
||||
const applyPreset = ({ themeScheme, grayscale, colourWeakness, layout, watermark, ...rest }: ThemePreset): void => {
|
||||
const applyPreset = (preset: ThemePreset): void => {
|
||||
const mergedPreset = defu(preset, themeSettings);
|
||||
const { themeScheme, grayscale, colourWeakness, layout, watermark, naiveui, ...rest } = mergedPreset;
|
||||
themeStore.setThemeScheme(themeScheme);
|
||||
themeStore.setGrayscale(grayscale);
|
||||
themeStore.setColourWeakness(colourWeakness);
|
||||
@@ -96,6 +102,9 @@ const applyPreset = ({ themeScheme, grayscale, colourWeakness, layout, watermark
|
||||
tokens: { ...rest.tokens }
|
||||
});
|
||||
|
||||
// Apply NaiveUI theme overrides if present
|
||||
themeStore.setNaiveThemeOverrides(naiveui);
|
||||
|
||||
window.$message?.success($t('theme.appearance.preset.applySuccess'));
|
||||
};
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user