Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
This commit is contained in:
@@ -11,6 +11,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
|
||||
"compact": false,
|
||||
"contentCompact": "wide",
|
||||
"defaultAvatar": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp",
|
||||
"defaultHomePath": "/analytics",
|
||||
"dynamicTitle": true,
|
||||
"enableCheckUpdates": true,
|
||||
"enablePreferences": true,
|
||||
|
@@ -11,6 +11,7 @@ const defaultPreferences: Preferences = {
|
||||
contentCompact: 'wide',
|
||||
defaultAvatar:
|
||||
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
|
||||
defaultHomePath: '/analytics',
|
||||
dynamicTitle: true,
|
||||
enableCheckUpdates: true,
|
||||
enablePreferences: true,
|
||||
|
@@ -35,6 +35,8 @@ interface AppPreferences {
|
||||
contentCompact: ContentCompactType;
|
||||
// /** 应用默认头像 */
|
||||
defaultAvatar: string;
|
||||
/** 默认首页地址 */
|
||||
defaultHomePath: string;
|
||||
// /** 开启动态标题 */
|
||||
dynamicTitle: boolean;
|
||||
/** 是否开启检查更新 */
|
||||
|
@@ -38,6 +38,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@vben-core/composables": "workspace:*",
|
||||
"@vben-core/icons": "workspace:*",
|
||||
"@vben-core/shadcn-ui": "workspace:*",
|
||||
"@vben-core/shared": "workspace:*",
|
||||
"@vben-core/typings": "workspace:*",
|
||||
|
@@ -5,6 +5,7 @@ import type { FormSchema, MaybeComponentProps } from '../types';
|
||||
|
||||
import { computed, nextTick, onUnmounted, useTemplateRef, watch } from 'vue';
|
||||
|
||||
import { CircleAlert } from '@vben-core/icons';
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
FormItem,
|
||||
FormMessage,
|
||||
VbenRenderContent,
|
||||
VbenTooltip,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
import { cn, isFunction, isObject, isString } from '@vben-core/shared/utils';
|
||||
|
||||
@@ -354,6 +356,24 @@ onUnmounted(() => {
|
||||
</template>
|
||||
<!-- <slot></slot> -->
|
||||
</component>
|
||||
<VbenTooltip
|
||||
v-if="compact && isInValid"
|
||||
:delay-duration="300"
|
||||
side="left"
|
||||
>
|
||||
<template #trigger>
|
||||
<slot name="trigger">
|
||||
<CircleAlert
|
||||
:class="
|
||||
cn(
|
||||
'text-foreground/80 hover:text-foreground inline-flex size-5 cursor-pointer',
|
||||
)
|
||||
"
|
||||
/>
|
||||
</slot>
|
||||
</template>
|
||||
<FormMessage />
|
||||
</VbenTooltip>
|
||||
</slot>
|
||||
</FormControl>
|
||||
<!-- 自定义后缀 -->
|
||||
@@ -365,7 +385,7 @@ onUnmounted(() => {
|
||||
</FormDescription>
|
||||
</div>
|
||||
|
||||
<Transition name="slide-up">
|
||||
<Transition name="slide-up" v-if="!compact">
|
||||
<FormMessage class="absolute bottom-1" />
|
||||
</Transition>
|
||||
</div>
|
||||
|
@@ -31,8 +31,8 @@ export function useVbenForm<
|
||||
h(VbenUseForm, { ...props, ...attrs, formApi: extendedApi }, slots);
|
||||
},
|
||||
{
|
||||
inheritAttrs: false,
|
||||
name: 'VbenUseForm',
|
||||
inheritAttrs: false,
|
||||
},
|
||||
);
|
||||
// Add reactivity support
|
||||
|
@@ -31,6 +31,7 @@ import {
|
||||
createSubMenuContext,
|
||||
useMenuStyle,
|
||||
} from '../hooks';
|
||||
import { useMenuScroll } from '../hooks/use-menu-scroll';
|
||||
import { flattedChildren } from '../utils';
|
||||
import SubMenu from './sub-menu.vue';
|
||||
|
||||
@@ -44,6 +45,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
mode: 'vertical',
|
||||
rounded: true,
|
||||
theme: 'dark',
|
||||
scrollToActive: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -206,15 +208,19 @@ function handleResize() {
|
||||
isFirstTimeRender = false;
|
||||
}
|
||||
|
||||
function getActivePaths() {
|
||||
const activeItem = activePath.value && items.value[activePath.value];
|
||||
const enableScroll = computed(
|
||||
() => props.scrollToActive && props.mode === 'vertical' && !props.collapse,
|
||||
);
|
||||
|
||||
if (!activeItem || props.mode === 'horizontal' || props.collapse) {
|
||||
return [];
|
||||
}
|
||||
const { scrollToActiveItem } = useMenuScroll(activePath, {
|
||||
enable: enableScroll,
|
||||
delay: 320,
|
||||
});
|
||||
|
||||
return activeItem.parentPaths;
|
||||
}
|
||||
// 监听 activePath 变化,自动滚动到激活项
|
||||
watch(activePath, () => {
|
||||
scrollToActiveItem();
|
||||
});
|
||||
|
||||
// 默认展开菜单
|
||||
function initMenu() {
|
||||
@@ -318,6 +324,16 @@ function removeSubMenu(subMenu: MenuItemRegistered) {
|
||||
function removeMenuItem(item: MenuItemRegistered) {
|
||||
Reflect.deleteProperty(items.value, item.path);
|
||||
}
|
||||
|
||||
function getActivePaths() {
|
||||
const activeItem = activePath.value && items.value[activePath.value];
|
||||
|
||||
if (!activeItem || props.mode === 'horizontal' || props.collapse) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return activeItem.parentPaths;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<ul
|
||||
|
46
packages/@core/ui-kit/menu-ui/src/hooks/use-menu-scroll.ts
Normal file
46
packages/@core/ui-kit/menu-ui/src/hooks/use-menu-scroll.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { watch } from 'vue';
|
||||
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
|
||||
interface UseMenuScrollOptions {
|
||||
delay?: number;
|
||||
enable?: boolean | Ref<boolean>;
|
||||
}
|
||||
|
||||
export function useMenuScroll(
|
||||
activePath: Ref<string | undefined>,
|
||||
options: UseMenuScrollOptions = {},
|
||||
) {
|
||||
const { enable = true, delay = 320 } = options;
|
||||
|
||||
function scrollToActiveItem() {
|
||||
const isEnabled = typeof enable === 'boolean' ? enable : enable.value;
|
||||
if (!isEnabled) return;
|
||||
|
||||
const activeElement = document.querySelector(
|
||||
`aside li[role=menuitem].is-active`,
|
||||
);
|
||||
if (activeElement) {
|
||||
activeElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'center',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedScroll = useDebounceFn(scrollToActiveItem, delay);
|
||||
|
||||
watch(activePath, () => {
|
||||
const isEnabled = typeof enable === 'boolean' ? enable : enable.value;
|
||||
if (!isEnabled) return;
|
||||
|
||||
debouncedScroll();
|
||||
});
|
||||
|
||||
return {
|
||||
scrollToActiveItem,
|
||||
};
|
||||
}
|
@@ -18,15 +18,9 @@ defineOptions({
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
collapse: false,
|
||||
// theme: 'dark',
|
||||
});
|
||||
|
||||
const forward = useForwardProps(props);
|
||||
|
||||
// const emit = defineEmits<{
|
||||
// 'update:openKeys': [key: Key[]];
|
||||
// 'update:selectedKeys': [key: Key[]];
|
||||
// }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@@ -41,6 +41,12 @@ interface MenuProps {
|
||||
*/
|
||||
rounded?: boolean;
|
||||
|
||||
/**
|
||||
* @zh_CN 是否自动滚动到激活的菜单项
|
||||
* @default false
|
||||
*/
|
||||
scrollToActive?: boolean;
|
||||
|
||||
/**
|
||||
* @zh_CN 菜单主题
|
||||
* @default dark
|
||||
|
@@ -35,7 +35,7 @@ interface Props extends DrawerProps {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
appendToMain: false,
|
||||
closeIconPlacement: 'right',
|
||||
destroyOnClose: true,
|
||||
destroyOnClose: false,
|
||||
drawerApi: undefined,
|
||||
submitting: false,
|
||||
zIndex: 1000,
|
||||
|
@@ -21,9 +21,7 @@ import VbenDrawer from './drawer.vue';
|
||||
|
||||
const USER_DRAWER_INJECT_KEY = Symbol('VBEN_DRAWER_INJECT');
|
||||
|
||||
const DEFAULT_DRAWER_PROPS: Partial<DrawerProps> = {
|
||||
destroyOnClose: true,
|
||||
};
|
||||
const DEFAULT_DRAWER_PROPS: Partial<DrawerProps> = {};
|
||||
|
||||
export function setDefaultDrawerProps(props: Partial<DrawerProps>) {
|
||||
Object.assign(DEFAULT_DRAWER_PROPS, props);
|
||||
@@ -66,9 +64,10 @@ export function useVbenDrawer<
|
||||
slots,
|
||||
);
|
||||
},
|
||||
// eslint-disable-next-line vue/one-component-per-file
|
||||
{
|
||||
inheritAttrs: false,
|
||||
name: 'VbenParentDrawer',
|
||||
inheritAttrs: false,
|
||||
},
|
||||
);
|
||||
return [Drawer, extendedApi as ExtendedDrawerApi] as const;
|
||||
@@ -107,9 +106,10 @@ export function useVbenDrawer<
|
||||
return () =>
|
||||
h(VbenDrawer, { ...props, ...attrs, drawerApi: extendedApi }, slots);
|
||||
},
|
||||
// eslint-disable-next-line vue/one-component-per-file
|
||||
{
|
||||
inheritAttrs: false,
|
||||
name: 'VbenDrawer',
|
||||
inheritAttrs: false,
|
||||
},
|
||||
);
|
||||
injectData.extendApi?.(extendedApi);
|
||||
|
@@ -34,7 +34,7 @@ interface Props extends ModalProps {
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
appendToMain: false,
|
||||
destroyOnClose: true,
|
||||
destroyOnClose: false,
|
||||
modalApi: undefined,
|
||||
});
|
||||
|
||||
|
@@ -17,9 +17,7 @@ import VbenModal from './modal.vue';
|
||||
|
||||
const USER_MODAL_INJECT_KEY = Symbol('VBEN_MODAL_INJECT');
|
||||
|
||||
const DEFAULT_MODAL_PROPS: Partial<ModalProps> = {
|
||||
destroyOnClose: true,
|
||||
};
|
||||
const DEFAULT_MODAL_PROPS: Partial<ModalProps> = {};
|
||||
|
||||
export function setDefaultModalProps(props: Partial<ModalProps>) {
|
||||
Object.assign(DEFAULT_MODAL_PROPS, props);
|
||||
@@ -65,9 +63,10 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
||||
slots,
|
||||
);
|
||||
},
|
||||
// eslint-disable-next-line vue/one-component-per-file
|
||||
{
|
||||
inheritAttrs: false,
|
||||
name: 'VbenParentModal',
|
||||
inheritAttrs: false,
|
||||
},
|
||||
);
|
||||
return [Modal, extendedApi as ExtendedModalApi] as const;
|
||||
@@ -114,9 +113,10 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
||||
slots,
|
||||
);
|
||||
},
|
||||
// eslint-disable-next-line vue/one-component-per-file
|
||||
{
|
||||
inheritAttrs: false,
|
||||
name: 'VbenModal',
|
||||
inheritAttrs: false,
|
||||
},
|
||||
);
|
||||
injectData.extendApi?.(extendedApi);
|
||||
|
@@ -21,6 +21,7 @@ interface Props extends PopoverRootProps {
|
||||
class?: ClassType;
|
||||
contentClass?: ClassType;
|
||||
contentProps?: PopoverContentProps;
|
||||
triggerClass?: ClassType;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
@@ -32,6 +33,7 @@ const delegatedProps = computed(() => {
|
||||
class: _cls,
|
||||
contentClass: _,
|
||||
contentProps: _cProps,
|
||||
triggerClass: _tClass,
|
||||
...delegated
|
||||
} = props;
|
||||
|
||||
@@ -43,7 +45,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
|
||||
<template>
|
||||
<PopoverRoot v-bind="forwarded">
|
||||
<PopoverTrigger>
|
||||
<PopoverTrigger :class="triggerClass">
|
||||
<slot name="trigger"></slot>
|
||||
|
||||
<PopoverContent
|
||||
|
@@ -12,7 +12,6 @@ interface Props extends TabsProps {}
|
||||
|
||||
defineOptions({
|
||||
name: 'VbenTabsChrome',
|
||||
// eslint-disable-next-line perfectionist/sort-objects
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
|
@@ -12,7 +12,7 @@ interface Props extends TabsProps {}
|
||||
|
||||
defineOptions({
|
||||
name: 'VbenTabs',
|
||||
// eslint-disable-next-line perfectionist/sort-objects
|
||||
|
||||
inheritAttrs: false,
|
||||
});
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
|
@@ -15,5 +15,5 @@ pnpm add @vben/constants
|
||||
### 使用
|
||||
|
||||
```ts
|
||||
import { DEFAULT_HOME_PATH } from '@vben/constants';
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
```
|
||||
|
@@ -3,11 +3,6 @@
|
||||
*/
|
||||
export const LOGIN_PATH = '/auth/login';
|
||||
|
||||
/**
|
||||
* @zh_CN 默认首页地址
|
||||
*/
|
||||
export const DEFAULT_HOME_PATH = '/analytics';
|
||||
|
||||
export interface LanguageOption {
|
||||
label: string;
|
||||
value: 'en-US' | 'zh-CN';
|
||||
|
@@ -74,7 +74,7 @@ async function generateAccessible(
|
||||
}
|
||||
|
||||
// 生成菜单
|
||||
const accessibleMenus = await generateMenus(accessibleRoutes, options.router);
|
||||
const accessibleMenus = generateMenus(accessibleRoutes, options.router);
|
||||
|
||||
return { accessibleMenus, accessibleRoutes };
|
||||
}
|
||||
|
@@ -165,13 +165,18 @@ const searchInputProps = computed(() => {
|
||||
};
|
||||
});
|
||||
|
||||
function updateCurrentSelect(v: string) {
|
||||
currentSelect.value = v;
|
||||
}
|
||||
|
||||
defineExpose({ toggleOpenState, open, close });
|
||||
</script>
|
||||
<template>
|
||||
<VbenPopover
|
||||
v-model:open="visible"
|
||||
:content-props="{ align: 'end', alignOffset: -11, sideOffset: 8 }"
|
||||
content-class="p-0 pt-3"
|
||||
content-class="p-0 pt-3 w-full"
|
||||
trigger-class="w-full"
|
||||
>
|
||||
<template #trigger>
|
||||
<template v-if="props.type === 'input'">
|
||||
@@ -183,6 +188,7 @@ defineExpose({ toggleOpenState, open, close });
|
||||
role="combobox"
|
||||
:aria-label="$t('ui.iconPicker.placeholder')"
|
||||
aria-expanded="visible"
|
||||
:[`onUpdate:${modelValueProp}`]="updateCurrentSelect"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<template #[iconSlot]>
|
||||
|
@@ -35,6 +35,16 @@ const getZIndex = computed(() => {
|
||||
return props.zIndex || calcZIndex();
|
||||
});
|
||||
|
||||
/**
|
||||
* 排除ant-message和loading:9999的z-index
|
||||
*/
|
||||
const zIndexExcludeClass = ['ant-message', 'loading'];
|
||||
function isZIndexExcludeClass(element: Element) {
|
||||
return zIndexExcludeClass.some((className) =>
|
||||
element.classList.contains(className),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最大的zIndex值
|
||||
*/
|
||||
@@ -44,7 +54,11 @@ function calcZIndex() {
|
||||
[...elements].forEach((element) => {
|
||||
const style = window.getComputedStyle(element);
|
||||
const zIndex = style.getPropertyValue('z-index');
|
||||
if (zIndex && !Number.isNaN(Number.parseInt(zIndex))) {
|
||||
if (
|
||||
zIndex &&
|
||||
!Number.isNaN(Number.parseInt(zIndex)) &&
|
||||
!isZIndexExcludeClass(element)
|
||||
) {
|
||||
maxZ = Math.max(maxZ, Number.parseInt(zIndex));
|
||||
}
|
||||
});
|
||||
|
@@ -37,6 +37,7 @@ function handleMenuOpen(key: string, path: string[]) {
|
||||
:menus="menus"
|
||||
:mode="mode"
|
||||
:rounded="rounded"
|
||||
scroll-to-active
|
||||
:theme="theme"
|
||||
@open="handleMenuOpen"
|
||||
@select="handleMenuSelect"
|
||||
|
@@ -6,39 +6,55 @@ import { isHttpUrl, openRouteInNewWindow, openWindow } from '@vben/utils';
|
||||
|
||||
function useNavigation() {
|
||||
const router = useRouter();
|
||||
const routes = router.getRoutes();
|
||||
|
||||
const routeMetaMap = new Map<string, RouteRecordNormalized>();
|
||||
|
||||
routes.forEach((route) => {
|
||||
routeMetaMap.set(route.path, route);
|
||||
// 初始化路由映射
|
||||
const initRouteMetaMap = () => {
|
||||
const routes = router.getRoutes();
|
||||
routes.forEach((route) => {
|
||||
routeMetaMap.set(route.path, route);
|
||||
});
|
||||
};
|
||||
|
||||
initRouteMetaMap();
|
||||
|
||||
// 监听路由变化
|
||||
router.afterEach(() => {
|
||||
initRouteMetaMap();
|
||||
});
|
||||
|
||||
const navigation = async (path: string) => {
|
||||
const route = routeMetaMap.get(path);
|
||||
const { openInNewWindow = false, query = {} } = route?.meta ?? {};
|
||||
// 检查是否应该在新窗口打开
|
||||
const shouldOpenInNewWindow = (path: string): boolean => {
|
||||
if (isHttpUrl(path)) {
|
||||
openWindow(path, { target: '_blank' });
|
||||
} else if (openInNewWindow) {
|
||||
openRouteInNewWindow(path);
|
||||
} else {
|
||||
await router.push({
|
||||
path,
|
||||
query,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
const route = routeMetaMap.get(path);
|
||||
return route?.meta?.openInNewWindow ?? false;
|
||||
};
|
||||
|
||||
const navigation = async (path: string) => {
|
||||
try {
|
||||
const route = routeMetaMap.get(path);
|
||||
const { openInNewWindow = false, query = {} } = route?.meta ?? {};
|
||||
|
||||
if (isHttpUrl(path)) {
|
||||
openWindow(path, { target: '_blank' });
|
||||
} else if (openInNewWindow) {
|
||||
openRouteInNewWindow(path);
|
||||
} else {
|
||||
await router.push({
|
||||
path,
|
||||
query,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Navigation failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const willOpenedByWindow = (path: string) => {
|
||||
const route = routeMetaMap.get(path);
|
||||
const { openInNewWindow = false } = route?.meta ?? {};
|
||||
if (isHttpUrl(path)) {
|
||||
return true;
|
||||
} else if (openInNewWindow) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return shouldOpenInNewWindow(path);
|
||||
};
|
||||
|
||||
return { navigation, willOpenedByWindow };
|
||||
|
@@ -11,7 +11,8 @@ defineOptions({
|
||||
name: 'LanguageToggle',
|
||||
});
|
||||
|
||||
async function handleUpdate(value: string) {
|
||||
async function handleUpdate(value: string | undefined) {
|
||||
if (!value) return;
|
||||
const locale = value as SupportedLanguagesType;
|
||||
updatePreferences({
|
||||
app: {
|
||||
|
@@ -36,7 +36,8 @@ const menus = computed((): VbenDropdownMenuItem[] => [
|
||||
|
||||
const { authPanelCenter, authPanelLeft, authPanelRight } = usePreferences();
|
||||
|
||||
function handleUpdate(value: string) {
|
||||
function handleUpdate(value: string | undefined) {
|
||||
if (!value) return;
|
||||
updatePreferences({
|
||||
app: {
|
||||
authPageLayout: value as AuthPageLayoutType,
|
||||
|
@@ -79,14 +79,14 @@ const handleCheckboxChange = () => {
|
||||
</SwitchItem>
|
||||
<CheckboxItem
|
||||
:items="[
|
||||
{ label: '收缩按钮', value: 'collapsed' },
|
||||
{ label: '固定按钮', value: 'fixed' },
|
||||
{ label: $t('preferences.sidebar.buttonCollapsed'), value: 'collapsed' },
|
||||
{ label: $t('preferences.sidebar.buttonFixed'), value: 'fixed' },
|
||||
]"
|
||||
multiple
|
||||
v-model="sidebarButtons"
|
||||
:on-btn-click="handleCheckboxChange"
|
||||
>
|
||||
按钮配置
|
||||
{{ $t('preferences.sidebar.buttons') }}
|
||||
</CheckboxItem>
|
||||
<NumberFieldItem
|
||||
v-model="sidebarWidth"
|
||||
|
@@ -24,7 +24,7 @@ withDefaults(defineProps<{ shouldOnHover?: boolean }>(), {
|
||||
shouldOnHover: false,
|
||||
});
|
||||
|
||||
function handleChange(isDark: boolean) {
|
||||
function handleChange(isDark: boolean | undefined) {
|
||||
updatePreferences({
|
||||
theme: { mode: isDark ? 'dark' : 'light' },
|
||||
});
|
||||
|
@@ -45,6 +45,9 @@
|
||||
"fixed": "Fixed"
|
||||
},
|
||||
"sidebar": {
|
||||
"buttons": "Show Buttons",
|
||||
"buttonFixed": "Fixed",
|
||||
"buttonCollapsed": "Collapsed",
|
||||
"title": "Sidebar",
|
||||
"width": "Width",
|
||||
"visible": "Show Sidebar",
|
||||
|
@@ -45,6 +45,9 @@
|
||||
"fixed": "固定"
|
||||
},
|
||||
"sidebar": {
|
||||
"buttons": "显示按钮",
|
||||
"buttonFixed": "固定按钮",
|
||||
"buttonCollapsed": "折叠按钮",
|
||||
"title": "侧边栏",
|
||||
"width": "宽度",
|
||||
"visible": "显示侧边栏",
|
||||
|
@@ -69,7 +69,7 @@ describe('generateMenus', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const menus = await generateMenus(mockRoutes, mockRouter as any);
|
||||
const menus = generateMenus(mockRoutes, mockRouter as any);
|
||||
expect(menus).toEqual(expectedMenus);
|
||||
});
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('generateMenus', () => {
|
||||
},
|
||||
] as RouteRecordRaw[];
|
||||
|
||||
const menus = await generateMenus(mockRoutesWithMeta, mockRouter as any);
|
||||
const menus = generateMenus(mockRoutesWithMeta, mockRouter as any);
|
||||
expect(menus).toEqual([
|
||||
{
|
||||
badge: undefined,
|
||||
@@ -109,7 +109,7 @@ describe('generateMenus', () => {
|
||||
},
|
||||
] as RouteRecordRaw[];
|
||||
|
||||
const menus = await generateMenus(mockRoutesWithParams, mockRouter as any);
|
||||
const menus = generateMenus(mockRoutesWithParams, mockRouter as any);
|
||||
expect(menus).toEqual([
|
||||
{
|
||||
badge: undefined,
|
||||
@@ -141,10 +141,7 @@ describe('generateMenus', () => {
|
||||
},
|
||||
] as RouteRecordRaw[];
|
||||
|
||||
const menus = await generateMenus(
|
||||
mockRoutesWithRedirect,
|
||||
mockRouter as any,
|
||||
);
|
||||
const menus = generateMenus(mockRoutesWithRedirect, mockRouter as any);
|
||||
expect(menus).toEqual([
|
||||
// Assuming your generateMenus function excludes redirect routes from the menu
|
||||
{
|
||||
@@ -195,7 +192,7 @@ describe('generateMenus', () => {
|
||||
});
|
||||
|
||||
it('should generate menu list with correct order', async () => {
|
||||
const menus = await generateMenus(routes, router);
|
||||
const menus = generateMenus(routes, router);
|
||||
const expectedMenus = [
|
||||
{
|
||||
badge: undefined,
|
||||
@@ -230,7 +227,7 @@ describe('generateMenus', () => {
|
||||
|
||||
it('should handle empty routes', async () => {
|
||||
const emptyRoutes: any[] = [];
|
||||
const menus = await generateMenus(emptyRoutes, router);
|
||||
const menus = generateMenus(emptyRoutes, router);
|
||||
expect(menus).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
@@ -1,30 +1,38 @@
|
||||
import type { Router, RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import type { ExRouteRecordRaw, MenuRecordRaw } from '@vben-core/typings';
|
||||
import type {
|
||||
ExRouteRecordRaw,
|
||||
MenuRecordRaw,
|
||||
RouteMeta,
|
||||
} from '@vben-core/typings';
|
||||
|
||||
import { filterTree, mapTree } from '@vben-core/shared/utils';
|
||||
|
||||
/**
|
||||
* 根据 routes 生成菜单列表
|
||||
* @param routes
|
||||
* @param routes - 路由配置列表
|
||||
* @param router - Vue Router 实例
|
||||
* @returns 生成的菜单列表
|
||||
*/
|
||||
async function generateMenus(
|
||||
function generateMenus(
|
||||
routes: RouteRecordRaw[],
|
||||
router: Router,
|
||||
): Promise<MenuRecordRaw[]> {
|
||||
): MenuRecordRaw[] {
|
||||
// 将路由列表转换为一个以 name 为键的对象映射
|
||||
// 获取所有router最终的path及name
|
||||
const finalRoutesMap: { [key: string]: string } = Object.fromEntries(
|
||||
router.getRoutes().map(({ name, path }) => [name, path]),
|
||||
);
|
||||
|
||||
let menus = mapTree<ExRouteRecordRaw, MenuRecordRaw>(routes, (route) => {
|
||||
// 路由表的路径写法有多种,这里从router获取到最终的path并赋值
|
||||
const path = finalRoutesMap[route.name as string] ?? route.path;
|
||||
// 获取最终的路由路径
|
||||
const path = finalRoutesMap[route.name as string] ?? route.path ?? '';
|
||||
|
||||
// 转换为菜单结构
|
||||
// const path = matchRoute?.path ?? route.path;
|
||||
const { meta, name: routeName, redirect, children } = route;
|
||||
const {
|
||||
meta = {} as RouteMeta,
|
||||
name: routeName,
|
||||
redirect,
|
||||
children = [],
|
||||
} = route;
|
||||
const {
|
||||
activeIcon,
|
||||
badge,
|
||||
@@ -35,24 +43,27 @@ async function generateMenus(
|
||||
link,
|
||||
order,
|
||||
title = '',
|
||||
} = meta || {};
|
||||
} = meta;
|
||||
|
||||
// 确保菜单名称不为空
|
||||
const name = (title || routeName || '') as string;
|
||||
|
||||
// 隐藏子菜单
|
||||
// 处理子菜单
|
||||
const resultChildren = hideChildrenInMenu
|
||||
? []
|
||||
: (children as MenuRecordRaw[]);
|
||||
|
||||
// 将菜单的所有父级和父级菜单记录到菜单项内
|
||||
if (resultChildren && resultChildren.length > 0) {
|
||||
// 设置子菜单的父子关系
|
||||
if (resultChildren.length > 0) {
|
||||
resultChildren.forEach((child) => {
|
||||
child.parents = [...(route.parents || []), path];
|
||||
child.parents = [...(route.parents ?? []), path];
|
||||
child.parent = path;
|
||||
});
|
||||
}
|
||||
// 隐藏子菜单
|
||||
|
||||
// 确定最终路径
|
||||
const resultPath = hideChildrenInMenu ? redirect || path : link || path;
|
||||
|
||||
return {
|
||||
activeIcon,
|
||||
badge,
|
||||
@@ -63,19 +74,17 @@ async function generateMenus(
|
||||
order,
|
||||
parent: route.parent,
|
||||
parents: route.parents,
|
||||
path: resultPath as string,
|
||||
show: !route?.meta?.hideInMenu,
|
||||
children: resultChildren || [],
|
||||
path: resultPath,
|
||||
show: !meta.hideInMenu,
|
||||
children: resultChildren,
|
||||
};
|
||||
});
|
||||
|
||||
// 对菜单进行排序,避免order=0时被替换成999的问题
|
||||
menus = menus.sort((a, b) => (a?.order ?? 999) - (b?.order ?? 999));
|
||||
|
||||
const finalMenus = filterTree(menus, (menu) => {
|
||||
return !!menu.show;
|
||||
});
|
||||
return finalMenus;
|
||||
// 过滤掉隐藏的菜单项
|
||||
return filterTree(menus, (menu) => !!menu.show);
|
||||
}
|
||||
|
||||
export { generateMenus };
|
||||
|
Reference in New Issue
Block a user