mirror of
https://github.com/m-xlsea/ruoyi-plus-soybean.git
synced 2025-09-24 07:49:47 +08:00
fix(projects): 修复vertical-mix布局、重构初始化的loading
This commit is contained in:
@ -4,7 +4,11 @@
|
||||
<n-breadcrumb-item>
|
||||
<n-dropdown v-if="breadcrumb.hasChildren" :options="breadcrumb.children" @select="dropdownSelect">
|
||||
<span>
|
||||
<component :is="breadcrumb.icon" v-if="theme.header.crumb.showIcon" class="inline-block mr-4px text-16px" />
|
||||
<component
|
||||
:is="breadcrumb.icon"
|
||||
v-if="theme.header.crumb.showIcon"
|
||||
class="inline-block align-text-bottom mr-4px text-16px"
|
||||
/>
|
||||
<span>{{ breadcrumb.label }}</span>
|
||||
</span>
|
||||
</n-dropdown>
|
||||
|
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<n-button :text="true" class="h-36px" @click="app.toggleSiderCollapse">
|
||||
<icon-ph-caret-double-right-bold v-if="app.siderCollapse" class="text-16px" />
|
||||
<icon-ph-caret-double-left-bold v-else class="text-16px" />
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { NButton } from 'naive-ui';
|
||||
import { useAppStore } from '@/store';
|
||||
|
||||
const app = useAppStore();
|
||||
</script>
|
||||
<style scoped></style>
|
@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="mb-6px px-4px cursor-pointer" @mouseenter="setTrue" @mouseleave="setFalse">
|
||||
<div
|
||||
class="flex-center flex-col py-12px rounded-2px"
|
||||
:class="{ 'text-primary bg-primary-active': isActive, 'text-primary': isHover }"
|
||||
>
|
||||
<component :is="icon" :class="[isMini ? 'text-16px' : 'text-20px']" />
|
||||
<p
|
||||
class="pt-8px text-12px overflow-hidden transition-height duration-300 ease-in-out"
|
||||
:class="[isMini ? 'h-0 pt-0' : 'h-20px pt-8px']"
|
||||
>
|
||||
{{ label }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { VNodeChild } from 'vue';
|
||||
import { useBoolean } from '@/hooks';
|
||||
|
||||
interface Props {
|
||||
/** 路由名称 */
|
||||
routeName: string;
|
||||
/** 路由名称文本 */
|
||||
label: string;
|
||||
/** 当前激活状态的理由名称 */
|
||||
activeRouteName: string;
|
||||
/** 路由图标 */
|
||||
icon?: () => VNodeChild;
|
||||
/** mini尺寸的路由 */
|
||||
isMini?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
icon: undefined,
|
||||
isMini: false
|
||||
});
|
||||
|
||||
const { bool: isHover, setTrue, setFalse } = useBoolean();
|
||||
|
||||
const isActive = computed(() => props.routeName === props.activeRouteName);
|
||||
</script>
|
||||
<style scoped></style>
|
@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative h-full transition-width duration-300 ease-in-out"
|
||||
:style="{ width: app.mixSiderFixed ? theme.sider.mixChildMenuWidth + 'px' : '0px' }"
|
||||
>
|
||||
<dark-mode-container
|
||||
class="drawer-shadow absolute-lt flex-col-stretch h-full nowrap-hidden"
|
||||
:style="{ width: showDrawer ? theme.sider.mixChildMenuWidth + 'px' : '0px' }"
|
||||
>
|
||||
<header class="header-height flex-y-center justify-between" :style="{ height: theme.header.height + 'px' }">
|
||||
<h2 class="text-primary pl-8px text-16px font-bold">{{ title }}</h2>
|
||||
<div class="px-8px text-16px text-gray-600 cursor-pointer" @click="app.toggleMixSiderFixed">
|
||||
<icon-mdi-pin-off v-if="app.mixSiderFixed" />
|
||||
<icon-mdi-pin v-else />
|
||||
</div>
|
||||
</header>
|
||||
<n-scrollbar class="flex-1-hidden">
|
||||
<n-menu
|
||||
:value="activeKey"
|
||||
:options="menus"
|
||||
:expanded-keys="expandedKeys"
|
||||
:indent="18"
|
||||
@update:value="handleUpdateMenu"
|
||||
@update:expanded-keys="handleUpdateExpandedKeys"
|
||||
/>
|
||||
</n-scrollbar>
|
||||
</dark-mode-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { NScrollbar, NMenu } from 'naive-ui';
|
||||
import type { MenuOption } from 'naive-ui';
|
||||
import { DarkModeContainer } from '@/components';
|
||||
import { useAppStore, useThemeStore } from '@/store';
|
||||
import { useAppInfo, useRouterPush } from '@/composables';
|
||||
import { getActiveKeyPathsOfMenus } from '@/utils';
|
||||
import type { GlobalMenuOption } from '@/interface';
|
||||
|
||||
interface Props {
|
||||
/** 菜单抽屉可见性 */
|
||||
visible: boolean;
|
||||
/** 子菜单数据 */
|
||||
menus: GlobalMenuOption[];
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const route = useRoute();
|
||||
const app = useAppStore();
|
||||
const theme = useThemeStore();
|
||||
const { routerPush } = useRouterPush();
|
||||
|
||||
const showDrawer = computed(() => (props.visible && props.menus.length) || app.mixSiderFixed);
|
||||
const { title } = useAppInfo();
|
||||
|
||||
const activeKey = computed(() => route.name as string);
|
||||
const expandedKeys = ref<string[]>([]);
|
||||
|
||||
function handleUpdateMenu(_key: string, item: MenuOption) {
|
||||
const menuItem = item as GlobalMenuOption;
|
||||
routerPush(menuItem.routePath);
|
||||
}
|
||||
|
||||
function handleUpdateExpandedKeys(keys: string[]) {
|
||||
expandedKeys.value = keys;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.name,
|
||||
() => {
|
||||
expandedKeys.value = getActiveKeyPathsOfMenus(activeKey.value, props.menus);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
<style scoped>
|
||||
.drawer-shadow {
|
||||
box-shadow: 2px 0 8px 0 rgb(29 35 41 / 5%);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,5 @@
|
||||
import MixMenuDetail from './MixMenuDetail.vue';
|
||||
import MixMenuDrawer from './MixMenuDrawer.vue';
|
||||
import MixMenuCollapse from './MixMenuCollapse.vue';
|
||||
|
||||
export { MixMenuDetail, MixMenuDrawer, MixMenuCollapse };
|
@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<dark-mode-container class="flex h-full" @mouseleave="resetFirstDegreeMenus">
|
||||
<div class="flex-1 flex-col-stretch h-full">
|
||||
<global-logo :show-title="false" :style="{ height: theme.header.height + 'px' }" />
|
||||
<n-scrollbar class="flex-1-hidden">
|
||||
<mix-menu-detail
|
||||
v-for="item in firstDegreeMenus"
|
||||
:key="item.routeName"
|
||||
:route-name="item.routeName"
|
||||
:active-route-name="activeParentRouteName"
|
||||
:label="item.label"
|
||||
:icon="item.icon"
|
||||
:is-mini="app.siderCollapse"
|
||||
@click="handleMixMenu(item.routeName, item.hasChildren)"
|
||||
/>
|
||||
</n-scrollbar>
|
||||
<mix-menu-collapse />
|
||||
</div>
|
||||
<mix-menu-drawer :visible="drawerVisible" :menus="activeChildMenus" />
|
||||
</dark-mode-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { NScrollbar } from 'naive-ui';
|
||||
import { DarkModeContainer } from '@/components';
|
||||
import { useAppStore, useThemeStore, useRouteStore } from '@/store';
|
||||
import { useRouterPush } from '@/composables';
|
||||
import { useBoolean } from '@/hooks';
|
||||
import { GlobalLogo } from '@/layouts/common';
|
||||
import type { GlobalMenuOption } from '@/interface';
|
||||
import { MixMenuDetail, MixMenuDrawer, MixMenuCollapse } from './components';
|
||||
|
||||
const route = useRoute();
|
||||
const app = useAppStore();
|
||||
const theme = useThemeStore();
|
||||
const routeStore = useRouteStore();
|
||||
const { routerPush } = useRouterPush();
|
||||
const { bool: drawerVisible, setTrue: openDrawer, setFalse: hideDrawer } = useBoolean();
|
||||
|
||||
const activeParentRouteName = ref('');
|
||||
function setActiveParentRouteName(routeName: string) {
|
||||
activeParentRouteName.value = routeName;
|
||||
}
|
||||
|
||||
const firstDegreeMenus = computed(() =>
|
||||
routeStore.menus.map(item => {
|
||||
const { routeName, label } = item;
|
||||
const icon = item?.icon;
|
||||
const hasChildren = Boolean(item.children && item.children.length);
|
||||
|
||||
return {
|
||||
routeName,
|
||||
label,
|
||||
icon,
|
||||
hasChildren
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
function getActiveParentRouteName() {
|
||||
firstDegreeMenus.value.some(item => {
|
||||
const routeName = route.name as string;
|
||||
const flag = routeName?.includes(item.routeName);
|
||||
if (flag) {
|
||||
setActiveParentRouteName(item.routeName);
|
||||
}
|
||||
return flag;
|
||||
});
|
||||
}
|
||||
|
||||
function handleMixMenu(routeName: string, hasChildren: boolean) {
|
||||
setActiveParentRouteName(routeName);
|
||||
if (hasChildren) {
|
||||
openDrawer();
|
||||
} else {
|
||||
routerPush({ name: routeName });
|
||||
}
|
||||
}
|
||||
|
||||
function resetFirstDegreeMenus() {
|
||||
getActiveParentRouteName();
|
||||
hideDrawer();
|
||||
}
|
||||
|
||||
const activeChildMenus = computed(() => {
|
||||
const menus: GlobalMenuOption[] = [];
|
||||
routeStore.menus.some(item => {
|
||||
const flag = item.routeName === activeParentRouteName.value && Boolean(item.children?.length);
|
||||
if (flag) {
|
||||
menus.push(...item.children!);
|
||||
}
|
||||
return flag;
|
||||
});
|
||||
return menus;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.name,
|
||||
() => {
|
||||
getActiveParentRouteName();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
<style scoped></style>
|
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<n-scrollbar>
|
||||
<n-scrollbar class="flex-1-hidden">
|
||||
<n-menu
|
||||
:value="activeKey"
|
||||
:collapsed="app.siderCollapse"
|
||||
:collapsed-width="theme.sider.collapsedWidth"
|
||||
:collapsed-icon-size="22"
|
||||
:options="menus"
|
||||
:options="routeStore.menus"
|
||||
:expanded-keys="expandedKeys"
|
||||
:indent="18"
|
||||
@update:value="handleUpdateMenu"
|
||||
@ -21,6 +21,7 @@ import { NScrollbar, NMenu } from 'naive-ui';
|
||||
import type { MenuOption } from 'naive-ui';
|
||||
import { useAppStore, useThemeStore, useRouteStore } from '@/store';
|
||||
import { useRouterPush } from '@/composables';
|
||||
import { getActiveKeyPathsOfMenus } from '@/utils';
|
||||
import type { GlobalMenuOption } from '@/interface';
|
||||
|
||||
const route = useRoute();
|
||||
@ -29,26 +30,9 @@ const theme = useThemeStore();
|
||||
const routeStore = useRouteStore();
|
||||
const { routerPush } = useRouterPush();
|
||||
|
||||
const menus = computed(() => routeStore.menus as GlobalMenuOption[]);
|
||||
const activeKey = computed(() => route.name as string);
|
||||
const expandedKeys = ref<string[]>([]);
|
||||
|
||||
function getExpendedKeys() {
|
||||
const keys = menus.value.map(menu => getActiveKeysInMenus(menu)).flat();
|
||||
return keys;
|
||||
}
|
||||
|
||||
function getActiveKeysInMenus(menu: GlobalMenuOption) {
|
||||
const keys: string[] = [];
|
||||
if (activeKey.value.includes(menu.routeName)) {
|
||||
keys.push(menu.routeName);
|
||||
}
|
||||
if (menu.children) {
|
||||
keys.push(...menu.children.map(item => getActiveKeysInMenus(item as GlobalMenuOption)).flat(1));
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
function handleUpdateMenu(_key: string, item: MenuOption) {
|
||||
const menuItem = item as GlobalMenuOption;
|
||||
routerPush(menuItem.routePath);
|
||||
@ -61,7 +45,7 @@ function handleUpdateExpandedKeys(keys: string[]) {
|
||||
watch(
|
||||
() => route.name,
|
||||
() => {
|
||||
expandedKeys.value = getExpendedKeys();
|
||||
expandedKeys.value = getActiveKeyPathsOfMenus(activeKey.value, routeStore.menus);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
@ -0,0 +1,3 @@
|
||||
import VerticalMenu from './VerticalMenu.vue';
|
||||
|
||||
export { VerticalMenu };
|
@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<dark-mode-container class="flex-col-stretch h-full">
|
||||
<global-logo v-if="!isHorizontalMix" :show-title="showTitle" :style="{ height: theme.header.height + 'px' }" />
|
||||
<vertical-menu />
|
||||
</dark-mode-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { DarkModeContainer } from '@/components';
|
||||
import { useAppStore, useThemeStore } from '@/store';
|
||||
import { GlobalLogo } from '@/layouts/common';
|
||||
import { VerticalMenu } from './components';
|
||||
|
||||
const app = useAppStore();
|
||||
const theme = useThemeStore();
|
||||
|
||||
const isHorizontalMix = computed(() => theme.layout.mode === 'horizontal-mix');
|
||||
const showTitle = computed(() => !app.siderCollapse && theme.layout.mode !== 'vertical-mix');
|
||||
</script>
|
||||
<style scoped></style>
|
@ -1,3 +1,4 @@
|
||||
import SiderMenu from './SiderMenu.vue';
|
||||
import VerticalSider from './VerticalSider/index.vue';
|
||||
import VerticalMixSider from './VerticalMixSider/index.vue';
|
||||
|
||||
export { SiderMenu };
|
||||
export { VerticalSider, VerticalMixSider };
|
||||
|
@ -1,18 +1,16 @@
|
||||
<template>
|
||||
<dark-mode-container class="global-sider flex-col-stretch h-full">
|
||||
<global-logo :show-title="!app.siderCollapse" :style="{ height: theme.header.height + 'px' }" />
|
||||
<sider-menu class="flex-1-hidden" />
|
||||
</dark-mode-container>
|
||||
<vertical-sider v-if="!isVerticalMix" class="global-sider" />
|
||||
<vertical-mix-sider v-else class="global-sider" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DarkModeContainer } from '@/components';
|
||||
import { useAppStore, useThemeStore } from '@/store';
|
||||
import GlobalLogo from '../GlobalLogo/index.vue';
|
||||
import { SiderMenu } from './components';
|
||||
import { computed } from 'vue';
|
||||
import { useThemeStore } from '@/store';
|
||||
import { VerticalSider, VerticalMixSider } from './components';
|
||||
|
||||
const app = useAppStore();
|
||||
const theme = useThemeStore();
|
||||
|
||||
const isVerticalMix = computed(() => theme.layout.mode === 'vertical-mix');
|
||||
</script>
|
||||
<style scoped>
|
||||
.global-sider {
|
||||
|
Reference in New Issue
Block a user