chore: init project

This commit is contained in:
vben
2024-05-19 21:20:42 +08:00
commit 399334ac57
630 changed files with 45623 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
<script lang="ts" setup>
import type { RouteLocationNormalizedLoaded } from 'vue-router';
import { preference, usePreference } from '@vben/preference';
import { storeToRefs, useTabsStore } from '@vben/stores';
import { IFrameRouterView } from '../../iframe';
defineOptions({ name: 'LayoutContent' });
const { keepAlive } = usePreference();
const tabsStore = useTabsStore();
const { getCacheTabs, getExcludeTabs, renderRouteView } =
storeToRefs(tabsStore);
// 页面切换动画
function getTransitionName(route: RouteLocationNormalizedLoaded) {
// 如果偏好设置未设置,则不使用动画
const { keepAlive, pageTransition, pageTransitionEnable, tabsVisible } =
preference;
if (!pageTransition || !pageTransitionEnable) {
return;
}
// 标签页未启用或者未开启缓存,则使用全局配置动画
if (!tabsVisible || !keepAlive) {
return pageTransition;
}
// 如果页面已经加载过,则不使用动画
if (route.meta.loaded) {
return;
}
// 已经打开且已经加载过的页面不使用动画
const inTabs = getCacheTabs.value.includes(route.name as string);
return inTabs && route.meta.loaded ? undefined : pageTransition;
}
</script>
<template>
<IFrameRouterView />
<RouterView v-slot="{ Component, route }">
<Transition :name="getTransitionName(route)" mode="out-in" appear>
<KeepAlive
v-if="keepAlive"
:include="getCacheTabs"
:exclude="getExcludeTabs"
>
<component
:is="Component"
v-if="renderRouteView"
:key="route.fullPath"
class="h-[1000px]"
/>
</KeepAlive>
<component :is="Component" v-else :key="route.fullPath" />
</Transition>
</RouterView>
</template>

View File

@@ -0,0 +1 @@
export { default as LayoutContent } from './content.vue';

View File

@@ -0,0 +1,11 @@
<script lang="ts" setup>
defineOptions({
name: 'LayoutFooter',
});
</script>
<template>
<div class="flex-center text-muted-foreground relative h-full w-full text-xs">
<slot></slot>
</div>
</template>

View File

@@ -0,0 +1 @@
export { default as LayoutFooter } from './footer.vue';

View File

@@ -0,0 +1,40 @@
<script lang="ts" setup>
import { VbenFullScreen } from '@vben-core/shadcn-ui';
import { GlobalSearch, LanguageToggle, ThemeToggle } from '@vben/common-ui';
import { useAccessStore } from '@vben/stores';
interface Props {
/**
* Logo 主题
*/
theme?: string;
}
defineOptions({
name: 'LayoutHeader',
});
withDefaults(defineProps<Props>(), {
theme: 'light',
});
const accessStore = useAccessStore();
</script>
<template>
<div class="flex-center hidden lg:block">
<slot name="breadcrumb"></slot>
</div>
<div class="flex h-full min-w-0 flex-1 items-center">
<slot name="menu"></slot>
</div>
<div class="flex h-full min-w-0 flex-shrink-0 items-center">
<GlobalSearch class="mr-4" :menus="accessStore.getAccessMenus" />
<ThemeToggle class="mr-2" />
<LanguageToggle class="mr-2" />
<VbenFullScreen class="mr-2" />
<slot name="notification"></slot>
<slot name="user-dropdown"></slot>
</div>
</template>

View File

@@ -0,0 +1 @@
export { default as LayoutHeader } from './header.vue';

View File

@@ -0,0 +1 @@
export { default as BasicLayout } from './layout.vue';

View File

@@ -0,0 +1,233 @@
<script lang="ts" setup>
import { VbenAdminLayout } from '@vben-core/layout-ui';
import { VbenBackTop, VbenLogo } from '@vben-core/shadcn-ui';
import { PreferenceWidget } from '@vben/common-ui';
import { preference, updatePreference, usePreference } from '@vben/preference';
import { computed } from 'vue';
import { LayoutContent } from './content';
import { LayoutFooter } from './footer';
import { LayoutHeader } from './header';
import {
LayoutExtraMenu,
LayoutMenu,
LayoutMixedMenu,
useExtraMenu,
useMixedMenu,
} from './menu';
import { LayoutTabs, LayoutTabsToolbar } from './tabs';
import { Breadcrumb } from './widgets';
defineOptions({ name: 'BasicLayout' });
const { isDark, isHeaderNav, isMixedNav, isSideMixedNav, layout } =
usePreference();
const headerMenuTheme = computed(() => {
return isDark.value ? 'dark' : 'light';
});
const theme = computed(() => {
const dark = isDark.value || preference.semiDarkMenu;
return dark ? 'dark' : 'light';
});
const logoClass = computed(() => {
return preference.sideCollapseShowTitle &&
preference.sideCollapse &&
!isMixedNav.value
? 'mx-auto'
: '';
});
const isMenuRounded = computed(() => {
return preference.navigationStyle === 'rounded';
});
const logoCollapse = computed(() => {
if (isHeaderNav.value || isMixedNav.value) {
return false;
}
const { isMobile, sideCollapse } = preference;
if (!sideCollapse && isMobile) {
return false;
}
return sideCollapse || isSideMixedNav.value;
});
const showHeaderNav = computed(() => {
return isHeaderNav.value || isMixedNav.value;
});
const {
extraActiveMenu,
extraMenus,
extraVisible,
handleDefaultSelect,
handleMenuMouseEnter,
handleMixedMenuSelect,
handleSideMouseLeave,
} = useExtraMenu();
const {
handleMenuSelect,
headerActive,
headerMenus,
sideActive,
sideMenus,
sideVisible,
} = useMixedMenu();
</script>
<template>
<VbenAdminLayout
v-model:side-extra-visible="extraVisible"
:side-collapse-show-title="preference.sideCollapseShowTitle"
:side-collapse="preference.sideCollapse"
:side-extra-collapse="preference.sideExtraCollapse"
:content-compact="preference.contentCompact"
:is-mobile="preference.isMobile"
:layout="layout"
:header-mode="preference.headerMode"
:footer-fixed="preference.footerFixed"
:side-semi-dark="preference.semiDarkMenu"
:side-theme="theme"
:side-visible="sideVisible"
:footer-visible="preference.footerVisible"
:header-visible="preference.headerVisible"
:side-width="preference.sideWidth"
:tabs-visible="preference.tabsVisible"
:side-expand-on-hover="preference.sideExpandOnHover"
@side-mouse-leave="handleSideMouseLeave"
@update:side-collapse="
(value: boolean) => updatePreference('sideCollapse', value)
"
@update:side-extra-collapse="
(value: boolean) => updatePreference('sideExtraCollapse', value)
"
@update:side-visible="
(value: boolean) => updatePreference('sideVisible', value)
"
@update:side-expand-on-hover="
(value: boolean) => updatePreference('sideExpandOnHover', value)
"
>
<template #preference>
<PreferenceWidget />
</template>
<template #back-top>
<VbenBackTop />
</template>
<!-- logo -->
<template #logo>
<VbenLogo
:collapse="logoCollapse"
:src="preference.logo"
:text="preference.appName"
:theme="showHeaderNav ? headerMenuTheme : theme"
:alt="preference.appName"
:class="logoClass"
/>
</template>
<!-- 头部区域 -->
<template #header>
<LayoutHeader :theme="theme">
<template
v-if="!showHeaderNav && preference.breadcrumbVisible"
#breadcrumb
>
<Breadcrumb
:hide-when-only-one="preference.breadcrumbHideOnlyOne"
:type="preference.breadcrumbStyle"
:show-icon="preference.breadcrumbIcon"
:show-home="preference.breadcrumbHome"
/>
</template>
<template v-if="showHeaderNav" #menu>
<LayoutMenu
class="w-full"
:rounded="isMenuRounded"
mode="horizontal"
:theme="headerMenuTheme"
:menus="headerMenus"
:default-active="headerActive"
@select="handleMenuSelect"
/>
</template>
<template #user-dropdown>
<slot name="user-dropdown"></slot>
</template>
<template #notification>
<slot name="notification"></slot>
</template>
</LayoutHeader>
</template>
<!-- 侧边菜单区域 -->
<template #menu>
<LayoutMenu
mode="vertical"
:rounded="isMenuRounded"
:collapse-show-title="preference.sideCollapseShowTitle"
:collapse="preference.sideCollapse"
:theme="theme"
:menus="sideMenus"
:default-active="sideActive"
@select="handleMenuSelect"
/>
</template>
<template #mixed-menu>
<LayoutMixedMenu
:rounded="isMenuRounded"
:collapse="!preference.sideCollapseShowTitle"
:active-path="extraActiveMenu"
:theme="theme"
@select="handleMixedMenuSelect"
@default-select="handleDefaultSelect"
@enter="handleMenuMouseEnter"
/>
</template>
<!-- 侧边额外区域 -->
<template #side-extra>
<LayoutExtraMenu
:rounded="isMenuRounded"
:menus="extraMenus"
:collapse="preference.sideExtraCollapse"
:theme="theme"
/>
</template>
<template #side-extra-title>
<VbenLogo
v-if="preference.logoVisible"
:text="preference.appName"
:theme="theme"
:alt="preference.appName"
/>
</template>
<template #tabs>
<LayoutTabs
v-if="preference.tabsVisible"
:show-icon="preference.tabsIcon"
/>
</template>
<template #tabs-toolbar>
<LayoutTabsToolbar v-if="preference.tabsVisible" />
</template>
<!-- 主体内容 -->
<template #content>
<LayoutContent />
</template>
<!-- 页脚 -->
<template v-if="preference.footerVisible" #footer>
<LayoutFooter v-if="preference.copyright">
{{ preference.copyright }}
</LayoutFooter>
</template>
</VbenAdminLayout>
</template>

View File

@@ -0,0 +1,33 @@
<script lang="ts" setup>
import type { MenuRecordRaw } from '@vben-core/typings';
import { Menu, MenuProps } from '@vben-core/menu-ui';
import { useRoute, useRouter } from 'vue-router';
interface Props extends MenuProps {
collspae?: boolean;
menus: MenuRecordRaw[];
}
defineProps<Props>();
const route = useRoute();
const router = useRouter();
function handleSelect(key: string) {
router.push(key);
}
</script>
<template>
<Menu
:rounded="rounded"
:collapse="collapse"
:default-active="route.path"
:menus="menus"
:theme="theme"
mode="vertical"
@select="handleSelect"
/>
</template>

View File

@@ -0,0 +1,37 @@
import type { MenuRecordRaw } from '@vben-core/typings';
function findMenuByPath(
list: MenuRecordRaw[],
path?: string,
): MenuRecordRaw | null {
for (const menu of list) {
if (menu.path === path) {
return menu;
}
if (menu?.children?.length) {
const findMenu = findMenuByPath(menu.children, path);
if (findMenu) {
return findMenu;
}
}
}
return null;
}
/**
* 查找根菜单
* @param menus
* @param path
*/
function findRootMenuByPath(menus: MenuRecordRaw[], path?: string) {
const findMenu = findMenuByPath(menus, path);
const rootMenuPath = findMenu?.parents?.[0];
const rootMenu = menus.find((item) => item.path === rootMenuPath);
return {
findMenu,
rootMenu,
rootMenuPath,
};
}
export { findMenuByPath, findRootMenuByPath };

View File

@@ -0,0 +1,5 @@
export { default as LayoutExtraMenu } from './extra-menu.vue';
export { default as LayoutMenu } from './menu.vue';
export { default as LayoutMixedMenu } from './mixed-menu.vue';
export * from './use-extra-menu';
export * from './use-mixed-menu';

View File

@@ -0,0 +1,34 @@
<script lang="ts" setup>
import type { MenuRecordRaw } from '@vben-core/typings';
import { Menu, MenuProps } from '@vben-core/menu-ui';
interface Props extends MenuProps {
menus?: MenuRecordRaw[];
}
const props = withDefaults(defineProps<Props>(), {
menus: () => [],
});
const emit = defineEmits<{
select: [string, string?];
}>();
function handleMenuSelect(key: string) {
emit('select', key, props.mode);
}
</script>
<template>
<Menu
:rounded="rounded"
:collapse-show-title="collapseShowTitle"
:collapse="collapse"
:default-active="defaultActive"
:menus="menus"
:theme="theme"
:mode="mode"
@select="handleMenuSelect"
/>
</template>

View File

@@ -0,0 +1,53 @@
<script lang="ts" setup>
import type { NormalMenuProps } from '@vben-core/menu-ui';
import type { MenuRecordRaw } from '@vben-core/typings';
import { NormalMenu } from '@vben-core/menu-ui';
import { useAccessStore } from '@vben/stores';
import { computed, onBeforeMount } from 'vue';
import { useRoute } from 'vue-router';
import { findMenuByPath } from './helper';
interface Props extends NormalMenuProps {}
defineProps<Props>();
const emit = defineEmits<{
defaultSelect: [MenuRecordRaw, MenuRecordRaw?];
enter: [MenuRecordRaw];
select: [MenuRecordRaw];
}>();
const accessStore = useAccessStore();
const route = useRoute();
const menus = computed(() => accessStore.getAccessMenus);
function handleSelect(menu: MenuRecordRaw) {
emit('select', menu);
}
onBeforeMount(() => {
const menu = findMenuByPath(menus.value, route.path);
if (menu) {
const rootMenu = menus.value.find(
(item) => item.path === menu.parents?.[0],
);
emit('defaultSelect', menu, rootMenu);
}
});
</script>
<template>
<NormalMenu
:rounded="rounded"
:collapse="collapse"
:menus="menus"
:active-path="activePath"
:theme="theme"
@select="handleSelect"
@enter="(menu) => emit('enter', menu)"
/>
</template>

View File

@@ -0,0 +1,90 @@
import type { MenuRecordRaw } from '@vben-core/typings';
import { preference } from '@vben/preference';
import { useAccessStore } from '@vben/stores';
import { computed, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { findRootMenuByPath } from './helper';
function useExtraMenu() {
const accessStore = useAccessStore();
const menus = computed(() => accessStore.getAccessMenus);
const route = useRoute();
const router = useRouter();
const extraMenus = ref<MenuRecordRaw[]>([]);
const extraVisible = ref<boolean>(false);
const extraActiveMenu = ref('');
/**
* 选择混合菜单事件
* @param menu
*/
const handleMixedMenuSelect = (menu: MenuRecordRaw) => {
extraMenus.value = menu?.children ?? [];
extraActiveMenu.value = menu.parents?.[0] ?? menu.path;
const hasChildren = extraMenus.value.length > 0;
extraVisible.value = hasChildren;
if (!hasChildren) {
router.push(menu.path);
}
};
/**
* 选择默认菜单事件
* @param menu
* @param rootMenu
*/
const handleDefaultSelect = (
menu: MenuRecordRaw,
rootMenu?: MenuRecordRaw,
) => {
extraMenus.value = rootMenu?.children ?? [];
extraActiveMenu.value = menu.parents?.[0] ?? menu.path;
if (preference.sideExpandOnHover) {
extraVisible.value = extraMenus.value.length > 0;
}
};
/**
* 侧边菜单鼠标移出事件
*/
const handleSideMouseLeave = () => {
if (preference.sideExpandOnHover) {
return;
}
extraVisible.value = false;
const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath(
menus.value,
route.path,
);
extraActiveMenu.value = rootMenuPath ?? findMenu?.path ?? '';
extraMenus.value = rootMenu?.children ?? [];
};
const handleMenuMouseEnter = (menu: MenuRecordRaw) => {
if (!preference.sideExpandOnHover) {
const { findMenu } = findRootMenuByPath(menus.value, menu.path);
extraMenus.value = findMenu?.children ?? [];
extraActiveMenu.value = menu.parents?.[0] ?? menu.path;
extraVisible.value = extraMenus.value.length > 0;
}
};
return {
extraActiveMenu,
extraMenus,
extraVisible,
handleDefaultSelect,
handleMenuMouseEnter,
handleMixedMenuSelect,
handleSideMouseLeave,
};
}
export { useExtraMenu };

View File

@@ -0,0 +1,118 @@
import type { MenuRecordRaw } from '@vben-core/typings';
import { preference, usePreference } from '@vben/preference';
import { useAccessStore } from '@vben/stores';
import { computed, onBeforeMount, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { findRootMenuByPath } from './helper';
function useMixedMenu() {
const accessStore = useAccessStore();
const route = useRoute();
const router = useRouter();
const splitSideMenus = ref<MenuRecordRaw[]>([]);
const rootMenuPath = ref<string>('');
const { isMixedNav } = usePreference();
const sideVisible = computed(() => {
if (isMixedNav.value) {
return preference.sideVisible && splitSideMenus.value.length > 0;
}
return preference.sideVisible;
});
const menus = computed(() => accessStore.getAccessMenus);
/**
* 头部菜单
*/
const headerMenus = computed(() => {
if (!isMixedNav.value) {
return menus.value;
}
return menus.value.map((item) => {
return {
...item,
children: [],
};
});
});
/**
* 侧边菜单
*/
const sideMenus = computed(() => {
if (!isMixedNav.value) {
return menus.value;
}
return splitSideMenus.value;
});
/**
* 侧边菜单激活路径
*/
const sideActive = computed(() => {
return route.path;
});
/**
* 头部菜单激活路径
*/
const headerActive = computed(() => {
if (!isMixedNav.value) {
return route.path;
}
return rootMenuPath.value;
});
/**
* 菜单点击事件处理
* @param key 菜单路径
* @param mode 菜单模式
*/
const handleMenuSelect = (key: string, mode?: string) => {
if (!isMixedNav.value || mode === 'vertical') {
router.push(key);
return;
}
const rootMenu = menus.value.find((item) => item.path === key);
rootMenuPath.value = rootMenu?.path ?? '';
splitSideMenus.value = rootMenu?.children ?? [];
if (splitSideMenus.value.length === 0) {
router.push(key);
}
};
/**
* 计算侧边菜单
* @param path 路由路径
*/
function calcSideMenus(path: string = route.path) {
let { rootMenu } = findRootMenuByPath(menus.value, path);
if (!rootMenu) {
rootMenu = menus.value.find((item) => item.path === path);
}
rootMenuPath.value = rootMenu?.path ?? '';
splitSideMenus.value = rootMenu?.children ?? [];
}
// 初始化计算侧边菜单
onBeforeMount(() => {
calcSideMenus();
});
return {
handleMenuSelect,
headerActive,
headerMenus,
sideActive,
sideMenus,
sideVisible,
};
}
export { useMixedMenu };

View File

@@ -0,0 +1,3 @@
export { default as LayoutTabs } from './tabs.vue';
export { default as LayoutTabsToolbar } from './tabs-toolbar.vue';
export * from './use-tabs';

View File

@@ -0,0 +1,35 @@
<script lang="ts" setup>
import { TabsMore, TabsScreen } from '@vben-core/tabs-ui';
import { preference, updatePreference } from '@vben/preference';
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useTabs } from './use-tabs';
const route = useRoute();
const { createContextMenus } = useTabs();
const menus = computed(() => {
return createContextMenus(route);
});
function handleScreenChange(screen: boolean) {
updatePreference({
headerVisible: !screen,
sideVisible: !screen,
});
}
</script>
<template>
<div class="flex-center h-full">
<TabsMore :menus="menus" />
<TabsScreen
:screen="!preference.headerVisible && !preference.sideVisible"
@change="handleScreenChange"
@update:screen="handleScreenChange"
/>
</div>
</template>

View File

@@ -0,0 +1,32 @@
<script lang="ts" setup>
import { TabsView } from '@vben-core/tabs-ui';
import { useTabs } from './use-tabs';
defineOptions({
name: 'LayoutTabs',
});
defineProps<{ showIcon?: boolean }>();
const {
createContextMenus,
currentActive,
currentTabs,
handleClick,
handleClose,
handleUnPushPin,
} = useTabs();
</script>
<template>
<TabsView
:show-icon="showIcon"
:tabs="currentTabs"
:menus="createContextMenus"
:active="currentActive"
@update:active="handleClick"
@close="handleClose"
@un-push-pin="handleUnPushPin"
/>
</template>

View File

@@ -0,0 +1,184 @@
import type { IContextMenuItem } from '@vben-core/tabs-ui';
import type { TabItem } from '@vben-core/typings';
import {
IcRoundClose,
IcRoundMultipleStop,
IcRoundRefresh,
MdiArrowExpandHorizontal,
MdiFormatHorizontalAlignLeft,
MdiFormatHorizontalAlignRight,
MdiPin,
MdiPinOff,
} from '@vben-core/iconify';
import { filterTree } from '@vben-core/toolkit';
import { storeToRefs, useAccessStore, useTabsStore } from '@vben/stores';
import { computed, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
function useTabs() {
const router = useRouter();
const route = useRoute();
const accessStore = useAccessStore();
const tabsStore = useTabsStore();
const { accessMenus } = storeToRefs(accessStore);
const currentActive = computed(() => {
return route.path;
});
const currentTabs = computed(() => {
return tabsStore.getTabs;
});
/**
* 初始化固定标签页
*/
const initAffixTabs = () => {
const affixTabs = filterTree(router.getRoutes(), (route) => {
return !!route.meta?.affixTab;
});
tabsStore.setAffixTabs(affixTabs);
};
// 点击tab,跳转路由
const handleClick = (key: string) => {
router.push(key);
};
// 关闭tab
const handleClose = async (key: string) => {
await tabsStore.closeTabByKey(key, router);
};
watch(
() => accessMenus.value,
() => {
initAffixTabs();
},
{ immediate: true },
);
watch(
() => route.path,
() => {
tabsStore.addTab(route);
},
{ immediate: true },
);
const createContextMenus = (tab: TabItem) => {
const tabs = tabsStore.getTabs;
const affixTabs = tabsStore.affixTabs;
const index = tabs.findIndex((item) => item.path === tab.path);
const disabled = tabs.length <= 1;
const { meta } = tab;
const affixTab = meta?.affixTab ?? false;
const isCurrentTab = route.path === tab.path;
// 当前处于最左侧或者减去固定标签页的数量等于0
const closeLeftDisabled =
index === 0 || index - affixTabs.length <= 0 || !isCurrentTab;
const closeRightDisabled = !isCurrentTab || index === tabs.length - 1;
const closeOtherDisabled =
disabled || !isCurrentTab || tabs.length - affixTabs.length <= 1;
const menus: IContextMenuItem[] = [
{
disabled: !isCurrentTab,
handler: async () => {
await tabsStore.refreshTab(router);
},
icon: IcRoundRefresh,
key: 'reload',
text: '重新加载',
},
{
disabled: !!affixTab || disabled,
handler: async () => {
await tabsStore.closeTab(tab, router);
},
icon: IcRoundClose,
key: 'close',
text: '关闭标签页',
},
{
handler: async () => {
await (affixTab
? tabsStore.unPushPinTab(tab)
: tabsStore.pushPinTab(tab));
},
icon: affixTab ? MdiPinOff : MdiPin,
key: 'affix',
separator: true,
text: affixTab ? '取消固定标签页' : '固定标签页',
},
{
disabled: closeLeftDisabled,
handler: async () => {
await tabsStore.closeLeftTabs(tab);
},
icon: MdiFormatHorizontalAlignLeft,
key: 'close-left',
text: '关闭左侧标签页',
},
{
disabled: closeRightDisabled,
handler: async () => {
await tabsStore.closeRightTabs(tab);
},
icon: MdiFormatHorizontalAlignRight,
key: 'close-right',
separator: true,
text: '关闭右侧标签页',
},
{
disabled: closeOtherDisabled,
handler: async () => {
await tabsStore.closeOtherTabs(tab);
},
icon: MdiArrowExpandHorizontal,
key: 'close-other',
text: '关闭其他标签页',
},
{
disabled,
handler: async () => {
await tabsStore.closeAllTabs(router);
},
icon: IcRoundMultipleStop,
key: 'close-all',
text: '关闭全部标签页',
},
// {
// icon: 'icon-[material-symbols--back-to-tab-sharp]',
// key: 'close-all',
// text: '新窗口打开',
// },
];
return menus;
};
/**
* 取消固定标签页
*/
const handleUnPushPin = async (tab: TabItem) => {
await tabsStore.unPushPinTab(tab);
};
return {
createContextMenus,
currentActive,
currentTabs,
handleClick,
handleClose,
handleUnPushPin,
};
}
export { useTabs };

View File

@@ -0,0 +1,88 @@
<script lang="ts" setup>
import type { IBreadcrumb } from '@vben-core/shadcn-ui';
import { VbenBackgroundBreadcrumb, VbenBreadcrumb } from '@vben-core/shadcn-ui';
import { BreadcrumbStyle } from '@vben-core/typings';
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
interface Props {
hideWhenOnlyOne?: boolean;
showHome?: boolean;
showIcon?: boolean;
type?: BreadcrumbStyle;
}
const props = withDefaults(defineProps<Props>(), {
showHome: true,
showIcon: false,
type: 'normal',
});
const route = useRoute();
const router = useRouter();
const breadcrumbs = computed((): IBreadcrumb[] => {
const matched = route.matched;
const resultBreadcrumb: IBreadcrumb[] = [];
for (const match of matched) {
const {
meta,
path,
// children = []
} = match;
const { hideChildrenInMenu, hideInBreadcrumb, icon, name, title } =
meta || {};
if (hideInBreadcrumb || hideChildrenInMenu || !path) {
continue;
}
resultBreadcrumb.push({
icon: icon as string,
path: path || route.path,
title: (title || name) as string,
// items: children.map((child) => {
// return {
// icon: child?.meta?.icon as string,
// path: child.path,
// title: child?.meta?.title as string,
// };
// }),
});
}
if (props.showHome) {
resultBreadcrumb.unshift({
icon: 'mdi:home-outline',
isHome: true,
path: '/',
});
}
if (props.hideWhenOnlyOne && resultBreadcrumb.length === 1) {
return [];
}
return resultBreadcrumb;
});
function handleSelect(path: string) {
router.push(path);
}
</script>
<template>
<VbenBreadcrumb
v-if="type === 'normal'"
:breadcrumbs="breadcrumbs"
class="ml-2"
:show-icon="showIcon"
@select="handleSelect"
/>
<VbenBackgroundBreadcrumb
v-if="type === 'background'"
:breadcrumbs="breadcrumbs"
class="ml-2"
:show-icon="showIcon"
@select="handleSelect"
/>
</template>

View File

@@ -0,0 +1 @@
export { default as Breadcrumb } from './breadcrumb.vue';