mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-09-17 17:26:38 +08:00
refactor(projects)!: refactor global menu & support reversed-horizontal-mix-menu
. close #365
This commit is contained in:
parent
00f41dd25e
commit
087e532613
@ -1,5 +1,9 @@
|
|||||||
import { transformRecordToOption } from '@/utils/common';
|
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> = {
|
export const themeSchemaRecord: Record<UnionKey.ThemeScheme, App.I18n.I18nKey> = {
|
||||||
light: 'theme.themeSchema.light',
|
light: 'theme.themeSchema.light',
|
||||||
dark: 'theme.themeSchema.dark',
|
dark: 'theme.themeSchema.dark',
|
||||||
|
@ -30,17 +30,30 @@ export function useRouterPush(inSetup = true) {
|
|||||||
name: key
|
name: key
|
||||||
};
|
};
|
||||||
|
|
||||||
if (query) {
|
if (Object.keys(query || {}).length) {
|
||||||
routeLocation.query = query;
|
routeLocation.query = query;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params) {
|
if (Object.keys(params || {}).length) {
|
||||||
routeLocation.params = params;
|
routeLocation.params = params;
|
||||||
}
|
}
|
||||||
|
|
||||||
return routerPush(routeLocation);
|
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() {
|
async function toHome() {
|
||||||
return routerPushByKey('root');
|
return routerPushByKey('root');
|
||||||
}
|
}
|
||||||
@ -95,6 +108,7 @@ export function useRouterPush(inSetup = true) {
|
|||||||
routerPush,
|
routerPush,
|
||||||
routerBack,
|
routerBack,
|
||||||
routerPushByKey,
|
routerPushByKey,
|
||||||
|
routerPushByKeyWithMetaQuery,
|
||||||
toLogin,
|
toLogin,
|
||||||
toggleLoginModule,
|
toggleLoginModule,
|
||||||
redirectFromLogin
|
redirectFromLogin
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, defineAsyncComponent } from 'vue';
|
||||||
import { AdminLayout, LAYOUT_SCROLL_EL_ID } from '@sa/materials';
|
import { AdminLayout, LAYOUT_SCROLL_EL_ID } from '@sa/materials';
|
||||||
import type { LayoutMode } from '@sa/materials';
|
import type { LayoutMode } from '@sa/materials';
|
||||||
import { useAppStore } from '@/store/modules/app';
|
import { useAppStore } from '@/store/modules/app';
|
||||||
@ -18,7 +18,9 @@ defineOptions({
|
|||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
const { menus } = setupMixMenuContext();
|
const { childLevelMenus, isActiveFirstLevelMenuHasChildren } = setupMixMenuContext();
|
||||||
|
|
||||||
|
const GlobalMenu = defineAsyncComponent(() => import('../modules/global-menu/index.vue'));
|
||||||
|
|
||||||
const layoutMode = computed(() => {
|
const layoutMode = computed(() => {
|
||||||
const vertical: LayoutMode = 'vertical';
|
const vertical: LayoutMode = 'vertical';
|
||||||
@ -26,7 +28,10 @@ const layoutMode = computed(() => {
|
|||||||
return themeStore.layout.mode.includes(vertical) ? vertical : horizontal;
|
return themeStore.layout.mode.includes(vertical) ? vertical : horizontal;
|
||||||
});
|
});
|
||||||
|
|
||||||
const headerPropsConfig: Record<UnionKey.ThemeLayoutMode, App.Global.HeaderProps> = {
|
const headerProps = computed(() => {
|
||||||
|
const { mode, reverseHorizontalMix } = themeStore.layout;
|
||||||
|
|
||||||
|
const headerPropsConfig: Record<UnionKey.ThemeLayoutMode, App.Global.HeaderProps> = {
|
||||||
vertical: {
|
vertical: {
|
||||||
showLogo: false,
|
showLogo: false,
|
||||||
showMenu: false,
|
showMenu: false,
|
||||||
@ -45,11 +50,12 @@ const headerPropsConfig: Record<UnionKey.ThemeLayoutMode, App.Global.HeaderProps
|
|||||||
'horizontal-mix': {
|
'horizontal-mix': {
|
||||||
showLogo: true,
|
showLogo: true,
|
||||||
showMenu: true,
|
showMenu: true,
|
||||||
showMenuToggler: false
|
showMenuToggler: reverseHorizontalMix && isActiveFirstLevelMenuHasChildren.value
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerProps = computed(() => headerPropsConfig[themeStore.layout.mode]);
|
return headerPropsConfig[mode];
|
||||||
|
});
|
||||||
|
|
||||||
const siderVisible = computed(() => themeStore.layout.mode !== 'horizontal');
|
const siderVisible = computed(() => themeStore.layout.mode !== 'horizontal');
|
||||||
|
|
||||||
@ -62,11 +68,16 @@ const siderWidth = computed(() => getSiderWidth());
|
|||||||
const siderCollapsedWidth = computed(() => getSiderCollapsedWidth());
|
const siderCollapsedWidth = computed(() => getSiderCollapsedWidth());
|
||||||
|
|
||||||
function getSiderWidth() {
|
function getSiderWidth() {
|
||||||
|
const { reverseHorizontalMix } = themeStore.layout;
|
||||||
const { width, mixWidth, mixChildMenuWidth } = themeStore.sider;
|
const { width, mixWidth, mixChildMenuWidth } = themeStore.sider;
|
||||||
|
|
||||||
|
if (isHorizontalMix.value && reverseHorizontalMix) {
|
||||||
|
return isActiveFirstLevelMenuHasChildren.value ? width : 0;
|
||||||
|
}
|
||||||
|
|
||||||
let w = isVerticalMix.value || isHorizontalMix.value ? mixWidth : width;
|
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;
|
w += mixChildMenuWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,7 +89,7 @@ function getSiderCollapsedWidth() {
|
|||||||
|
|
||||||
let w = isVerticalMix.value || isHorizontalMix.value ? mixCollapsedWidth : collapsedWidth;
|
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;
|
w += mixChildMenuWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,6 +127,7 @@ function getSiderCollapsedWidth() {
|
|||||||
<template #sider>
|
<template #sider>
|
||||||
<GlobalSider />
|
<GlobalSider />
|
||||||
</template>
|
</template>
|
||||||
|
<GlobalMenu />
|
||||||
<GlobalContent />
|
<GlobalContent />
|
||||||
<ThemeDrawer />
|
<ThemeDrawer />
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
@ -26,10 +26,30 @@ function useMixMenu() {
|
|||||||
setActiveFirstLevelMenuKey(firstLevelRouteName);
|
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 || []
|
() => 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(
|
watch(
|
||||||
() => route.name,
|
() => route.name,
|
||||||
() => {
|
() => {
|
||||||
@ -39,9 +59,12 @@ function useMixMenu() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
allMenus,
|
||||||
|
firstLevelMenus,
|
||||||
|
childLevelMenus,
|
||||||
|
isActiveFirstLevelMenuHasChildren,
|
||||||
activeFirstLevelMenuKey,
|
activeFirstLevelMenuKey,
|
||||||
setActiveFirstLevelMenuKey,
|
setActiveFirstLevelMenuKey,
|
||||||
getActiveFirstLevelMenuKey,
|
getActiveFirstLevelMenuKey
|
||||||
menus
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
|
||||||
import { useFullscreen } from '@vueuse/core';
|
import { useFullscreen } from '@vueuse/core';
|
||||||
import { useAppStore } from '@/store/modules/app';
|
import { useAppStore } from '@/store/modules/app';
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import { useRouteStore } from '@/store/modules/route';
|
import { GLOBAL_HEADER_MENU_ID } from '@/constants/app';
|
||||||
import HorizontalMenu from '../global-menu/base-menu.vue';
|
|
||||||
import GlobalLogo from '../global-logo/index.vue';
|
import GlobalLogo from '../global-logo/index.vue';
|
||||||
import GlobalBreadcrumb from '../global-breadcrumb/index.vue';
|
import GlobalBreadcrumb from '../global-breadcrumb/index.vue';
|
||||||
import GlobalSearch from '../global-search/index.vue';
|
import GlobalSearch from '../global-search/index.vue';
|
||||||
import { useMixMenuContext } from '../../context';
|
|
||||||
import ThemeButton from './components/theme-button.vue';
|
import ThemeButton from './components/theme-button.vue';
|
||||||
import UserAvatar from './components/user-avatar.vue';
|
import UserAvatar from './components/user-avatar.vue';
|
||||||
|
|
||||||
@ -29,29 +26,15 @@ defineProps<Props>();
|
|||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
const routeStore = useRouteStore();
|
|
||||||
const { isFullscreen, toggle } = useFullscreen();
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DarkModeContainer class="h-full flex-y-center px-12px shadow-header">
|
<DarkModeContainer class="h-full flex-y-center px-12px shadow-header">
|
||||||
<GlobalLogo v-if="showLogo" class="h-full" :style="{ width: themeStore.sider.width + 'px' }" />
|
<GlobalLogo v-if="showLogo" class="h-full" :style="{ width: themeStore.sider.width + 'px' }" />
|
||||||
<HorizontalMenu v-if="showMenu" mode="horizontal" :menus="headerMenus" class="px-12px" />
|
|
||||||
<div v-else class="h-full flex-y-center flex-1-hidden">
|
|
||||||
<MenuToggler v-if="showMenuToggler" :collapsed="appStore.siderCollapse" @click="appStore.toggleSiderCollapse" />
|
<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">
|
||||||
<GlobalBreadcrumb v-if="!appStore.isMobile" class="ml-12px" />
|
<GlobalBreadcrumb v-if="!appStore.isMobile" class="ml-12px" />
|
||||||
</div>
|
</div>
|
||||||
<div class="h-full flex-y-center justify-end">
|
<div class="h-full flex-y-center justify-end">
|
||||||
|
@ -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>
|
|
@ -3,31 +3,29 @@ 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 { useAppStore } from '@/store/modules/app';
|
|
||||||
import { useRouteStore } from '@/store/modules/route';
|
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'FirstLevelMenu'
|
name: 'FirstLevelMenu'
|
||||||
});
|
});
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
menus: App.Global.Menu[];
|
||||||
activeMenuKey?: string;
|
activeMenuKey?: string;
|
||||||
inverted?: boolean;
|
inverted?: boolean;
|
||||||
|
siderCollapse?: boolean;
|
||||||
|
darkMode?: boolean;
|
||||||
|
themeColor: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: 'select', menu: App.Global.Menu): boolean;
|
(e: 'select', menu: App.Global.Menu): boolean;
|
||||||
|
(e: 'toggleSiderCollapse'): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits<Emits>();
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
const appStore = useAppStore();
|
|
||||||
const themeStore = useThemeStore();
|
|
||||||
const routeStore = useRouteStore();
|
|
||||||
|
|
||||||
interface MixMenuItemProps {
|
interface MixMenuItemProps {
|
||||||
/** Menu item label */
|
/** Menu item label */
|
||||||
label: App.Global.Menu['label'];
|
label: App.Global.Menu['label'];
|
||||||
@ -36,12 +34,12 @@ interface MixMenuItemProps {
|
|||||||
/** Active menu item */
|
/** Active menu item */
|
||||||
active: boolean;
|
active: boolean;
|
||||||
/** Mini size */
|
/** Mini size */
|
||||||
isMini: boolean;
|
isMini?: boolean;
|
||||||
}
|
}
|
||||||
const [DefineMixMenuItem, MixMenuItem] = createReusableTemplate<MixMenuItemProps>();
|
const [DefineMixMenuItem, MixMenuItem] = createReusableTemplate<MixMenuItemProps>();
|
||||||
|
|
||||||
const selectedBgColor = computed(() => {
|
const selectedBgColor = computed(() => {
|
||||||
const { darkMode, themeColor } = themeStore;
|
const { darkMode, themeColor } = props;
|
||||||
|
|
||||||
const light = transformColorWithOpacity(themeColor, 0.1, '#ffffff');
|
const light = transformColorWithOpacity(themeColor, 0.1, '#ffffff');
|
||||||
const dark = transformColorWithOpacity(themeColor, 0.3, '#000000');
|
const dark = transformColorWithOpacity(themeColor, 0.3, '#000000');
|
||||||
@ -52,6 +50,10 @@ const selectedBgColor = computed(() => {
|
|||||||
function handleClickMixMenu(menu: App.Global.Menu) {
|
function handleClickMixMenu(menu: App.Global.Menu) {
|
||||||
emit('select', menu);
|
emit('select', menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleSiderCollapse() {
|
||||||
|
emit('toggleSiderCollapse');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -80,21 +82,21 @@ function handleClickMixMenu(menu: App.Global.Menu) {
|
|||||||
<slot></slot>
|
<slot></slot>
|
||||||
<SimpleScrollbar>
|
<SimpleScrollbar>
|
||||||
<MixMenuItem
|
<MixMenuItem
|
||||||
v-for="menu in routeStore.menus"
|
v-for="menu in menus"
|
||||||
:key="menu.key"
|
:key="menu.key"
|
||||||
:label="menu.label"
|
:label="menu.label"
|
||||||
:icon="menu.icon"
|
:icon="menu.icon"
|
||||||
:active="menu.key === activeMenuKey"
|
:active="menu.key === activeMenuKey"
|
||||||
:is-mini="appStore.siderCollapse"
|
:is-mini="siderCollapse"
|
||||||
@click="handleClickMixMenu(menu)"
|
@click="handleClickMixMenu(menu)"
|
||||||
/>
|
/>
|
||||||
</SimpleScrollbar>
|
</SimpleScrollbar>
|
||||||
<MenuToggler
|
<MenuToggler
|
||||||
arrow-icon
|
arrow-icon
|
||||||
:collapsed="appStore.siderCollapse"
|
:collapsed="siderCollapse"
|
||||||
:z-index="99"
|
:z-index="99"
|
||||||
:class="{ 'text-white:88 !hover:text-white': inverted }"
|
:class="{ 'text-white:88 !hover:text-white': inverted }"
|
||||||
@click="appStore.toggleSiderCollapse"
|
@click="toggleSiderCollapse"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
@ -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>
|
|
33
src/layouts/modules/global-menu/index.vue
Normal file
33
src/layouts/modules/global-menu/index.vue
Normal 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>
|
39
src/layouts/modules/global-menu/modules/horizontal-menu.vue
Normal file
39
src/layouts/modules/global-menu/modules/horizontal-menu.vue
Normal 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>
|
@ -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>
|
@ -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>
|
70
src/layouts/modules/global-menu/modules/vertical-menu.vue
Normal file
70
src/layouts/modules/global-menu/modules/vertical-menu.vue
Normal 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>
|
135
src/layouts/modules/global-menu/modules/vertical-mix-menu.vue
Normal file
135
src/layouts/modules/global-menu/modules/vertical-mix-menu.vue
Normal 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>
|
@ -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>
|
|
@ -2,11 +2,8 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useAppStore } from '@/store/modules/app';
|
import { useAppStore } from '@/store/modules/app';
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
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 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({
|
defineOptions({
|
||||||
name: 'GlobalSider'
|
name: 'GlobalSider'
|
||||||
@ -14,12 +11,12 @@ defineOptions({
|
|||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
const routeStore = useRouteStore();
|
|
||||||
|
|
||||||
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix');
|
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix');
|
||||||
const isHorizontalMix = computed(() => themeStore.layout.mode === 'horizontal-mix');
|
const isHorizontalMix = computed(() => themeStore.layout.mode === 'horizontal-mix');
|
||||||
const darkMenu = computed(() => !themeStore.darkMode && !isHorizontalMix.value && themeStore.sider.inverted);
|
const darkMenu = computed(() => !themeStore.darkMode && !isHorizontalMix.value && themeStore.sider.inverted);
|
||||||
const showLogo = computed(() => !isVerticalMix.value && !isHorizontalMix.value);
|
const showLogo = computed(() => !isVerticalMix.value && !isHorizontalMix.value);
|
||||||
|
const menuWrapperClass = computed(() => (showLogo.value ? 'flex-1-hidden' : 'h-full'));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -29,11 +26,7 @@ const showLogo = computed(() => !isVerticalMix.value && !isHorizontalMix.value);
|
|||||||
:show-title="!appStore.siderCollapse"
|
:show-title="!appStore.siderCollapse"
|
||||||
:style="{ height: themeStore.header.height + 'px' }"
|
:style="{ height: themeStore.header.height + 'px' }"
|
||||||
/>
|
/>
|
||||||
<VerticalMixMenu v-if="isVerticalMix">
|
<div :id="GLOBAL_SIDER_MENU_ID" :class="menuWrapperClass"></div>
|
||||||
<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" />
|
|
||||||
</DarkModeContainer>
|
</DarkModeContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import { useAppStore } from '@/store/modules/app';
|
|||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
import LayoutModeCard from '../components/layout-mode-card.vue';
|
import LayoutModeCard from '../components/layout-mode-card.vue';
|
||||||
|
import SettingItem from '../components/setting-item.vue';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'LayoutMode'
|
name: 'LayoutMode'
|
||||||
@ -10,6 +11,10 @@ defineOptions({
|
|||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
|
function handleReverseHorizontalMixChange(value: boolean) {
|
||||||
|
themeStore.setLayoutReverseHorizontalMix(value);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -44,6 +49,13 @@ const themeStore = useThemeStore();
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</LayoutModeCard>
|
</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>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -70,7 +70,8 @@ const local: App.I18n.Schema = {
|
|||||||
vertical: 'Vertical Menu Mode',
|
vertical: 'Vertical Menu Mode',
|
||||||
horizontal: 'Horizontal Menu Mode',
|
horizontal: 'Horizontal Menu Mode',
|
||||||
'vertical-mix': 'Vertical Mix 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',
|
recommendColor: 'Apply Recommended Color Algorithm',
|
||||||
recommendColorDesc: 'The recommended color algorithm refers to',
|
recommendColorDesc: 'The recommended color algorithm refers to',
|
||||||
|
@ -70,7 +70,8 @@ const local: App.I18n.Schema = {
|
|||||||
vertical: '左侧菜单模式',
|
vertical: '左侧菜单模式',
|
||||||
'vertical-mix': '左侧菜单混合模式',
|
'vertical-mix': '左侧菜单混合模式',
|
||||||
horizontal: '顶部菜单模式',
|
horizontal: '顶部菜单模式',
|
||||||
'horizontal-mix': '顶部菜单混合模式'
|
'horizontal-mix': '顶部菜单混合模式',
|
||||||
|
reverseHorizontalMix: '一级菜单与子级菜单位置反转'
|
||||||
},
|
},
|
||||||
recommendColor: '应用推荐算法的颜色',
|
recommendColor: '应用推荐算法的颜色',
|
||||||
recommendColorDesc: '推荐颜色的算法参照',
|
recommendColorDesc: '推荐颜色的算法参照',
|
||||||
|
@ -354,34 +354,6 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
|||||||
return getSelectedMenuKeyPathByKey(selectedKey, menus.value);
|
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 {
|
return {
|
||||||
resetStore,
|
resetStore,
|
||||||
routeHome,
|
routeHome,
|
||||||
@ -398,7 +370,6 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
|||||||
isInitAuthRoute,
|
isInitAuthRoute,
|
||||||
setIsInitAuthRoute,
|
setIsInitAuthRoute,
|
||||||
getIsAuthRouteExist,
|
getIsAuthRouteExist,
|
||||||
getSelectedMenuKeyPath,
|
getSelectedMenuKeyPath
|
||||||
getRouteQueryOfMetaByKey
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -132,6 +132,14 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
|
|||||||
);
|
);
|
||||||
addThemeVarsToGlobal(themeTokens, darkThemeTokens);
|
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 */
|
/** Cache theme settings */
|
||||||
function cacheThemeSettings() {
|
function cacheThemeSettings() {
|
||||||
@ -193,6 +201,7 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
|
|||||||
setThemeScheme,
|
setThemeScheme,
|
||||||
toggleThemeScheme,
|
toggleThemeScheme,
|
||||||
updateThemeColors,
|
updateThemeColors,
|
||||||
setThemeLayout
|
setThemeLayout,
|
||||||
|
setLayoutReverseHorizontalMix
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -13,7 +13,8 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
|||||||
isInfoFollowPrimary: true,
|
isInfoFollowPrimary: true,
|
||||||
layout: {
|
layout: {
|
||||||
mode: 'vertical',
|
mode: 'vertical',
|
||||||
scrollMode: 'content'
|
scrollMode: 'content',
|
||||||
|
reverseHorizontalMix: false
|
||||||
},
|
},
|
||||||
page: {
|
page: {
|
||||||
animate: true,
|
animate: true,
|
||||||
|
12
src/typings/app.d.ts
vendored
12
src/typings/app.d.ts
vendored
@ -24,6 +24,12 @@ declare namespace App {
|
|||||||
mode: UnionKey.ThemeLayoutMode;
|
mode: UnionKey.ThemeLayoutMode;
|
||||||
/** Scroll mode */
|
/** Scroll mode */
|
||||||
scrollMode: UnionKey.ThemeScrollMode;
|
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 */
|
||||||
page: {
|
page: {
|
||||||
@ -164,7 +170,7 @@ declare namespace App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The global menu */
|
/** The global menu */
|
||||||
interface Menu {
|
type Menu = {
|
||||||
/**
|
/**
|
||||||
* The menu key
|
* The menu key
|
||||||
*
|
*
|
||||||
@ -183,7 +189,7 @@ declare namespace App {
|
|||||||
icon?: () => VNode;
|
icon?: () => VNode;
|
||||||
/** The menu children */
|
/** The menu children */
|
||||||
children?: Menu[];
|
children?: Menu[];
|
||||||
}
|
};
|
||||||
|
|
||||||
type Breadcrumb = Omit<Menu, 'children'> & {
|
type Breadcrumb = Omit<Menu, 'children'> & {
|
||||||
options?: Breadcrumb[];
|
options?: Breadcrumb[];
|
||||||
@ -326,7 +332,7 @@ declare namespace App {
|
|||||||
theme: {
|
theme: {
|
||||||
themeSchema: { title: string } & Record<UnionKey.ThemeScheme, string>;
|
themeSchema: { title: string } & Record<UnionKey.ThemeScheme, string>;
|
||||||
grayscale: string;
|
grayscale: string;
|
||||||
layoutMode: { title: string } & Record<UnionKey.ThemeLayoutMode, string>;
|
layoutMode: { title: string; reverseHorizontalMix: string } & Record<UnionKey.ThemeLayoutMode, string>;
|
||||||
recommendColor: string;
|
recommendColor: string;
|
||||||
recommendColorDesc: string;
|
recommendColorDesc: string;
|
||||||
themeColor: {
|
themeColor: {
|
||||||
|
2
src/typings/union-key.d.ts
vendored
2
src/typings/union-key.d.ts
vendored
@ -20,7 +20,7 @@ declare namespace UnionKey {
|
|||||||
* - vertical: the vertical menu in left
|
* - vertical: the vertical menu in left
|
||||||
* - horizontal: the horizontal menu in top
|
* - horizontal: the horizontal menu in top
|
||||||
* - vertical-mix: two vertical mixed menus in left
|
* - 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';
|
type ThemeLayoutMode = 'vertical' | 'horizontal' | 'vertical-mix' | 'horizontal-mix';
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user