mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-24 15:26:15 +08:00
chore: init project
This commit is contained in:
62
packages/business/layouts/package.json
Normal file
62
packages/business/layouts/package.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "@vben/layouts",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||
"directory": "packages/business/layouts"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/vbenjs/vue-vben-admin/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pnpm vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"sideEffects": [
|
||||
"**/*.css"
|
||||
],
|
||||
"main": "./dist/index.mjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"imports": {
|
||||
"#*": "./src/*"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"development": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@vben-core/iconify": "workspace:*",
|
||||
"@vben-core/layout-ui": "workspace:*",
|
||||
"@vben-core/menu-ui": "workspace:*",
|
||||
"@vben-core/shadcn-ui": "workspace:*",
|
||||
"@vben-core/tabs-ui": "workspace:*",
|
||||
"@vben-core/toolkit": "workspace:*",
|
||||
"@vben/common-ui": "workspace:*",
|
||||
"@vben/locales": "workspace:*",
|
||||
"@vben/preference": "workspace:*",
|
||||
"@vben/stores": "workspace:*",
|
||||
"vue": "^3.4.27",
|
||||
"vue-router": "^4.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vben-core/typings": "workspace:*"
|
||||
}
|
||||
}
|
1
packages/business/layouts/postcss.config.mjs
Normal file
1
packages/business/layouts/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@vben/tailwind-config/postcss';
|
@@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@vben/locales';
|
||||
import { preference, usePreference } from '@vben/preference';
|
||||
|
||||
import AuthenticationFromView from './from-view.vue';
|
||||
import SloganIcon from './icons/slogan.vue';
|
||||
import Toolbar from './toolbar.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'Authentication',
|
||||
});
|
||||
|
||||
const { authPanelCenter, authPanelLeft, authPanelRight } = usePreference();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-body flex min-h-full flex-1 select-none overflow-x-hidden">
|
||||
<AuthenticationFromView
|
||||
v-if="authPanelLeft"
|
||||
class="-enter-x min-h-full w-2/5"
|
||||
transition-name="slide-left"
|
||||
/>
|
||||
|
||||
<div class="absolute left-0 top-0 z-10 flex flex-1">
|
||||
<div
|
||||
class="-enter-x text-foreground ml-4 mt-4 flex flex-1 items-center sm:left-6 sm:top-6"
|
||||
:class="
|
||||
authPanelLeft || authPanelCenter
|
||||
? 'lg:text-foreground'
|
||||
: 'lg:text-white'
|
||||
"
|
||||
>
|
||||
<img
|
||||
:alt="preference.appName"
|
||||
:src="preference.logo"
|
||||
:width="42"
|
||||
class="mr-2"
|
||||
/>
|
||||
<p class="text-xl font-medium">
|
||||
{{ preference.appName }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!authPanelCenter" class="relative hidden w-0 flex-1 lg:block">
|
||||
<div
|
||||
class="absolute inset-0 h-full w-full bg-gradient-to-r from-[var(--color-authentication-from)] to-[var(--color-authentication-to)]"
|
||||
>
|
||||
<div class="flex-center mr-20 flex h-full flex-col">
|
||||
<SloganIcon
|
||||
:alt="preference.appName"
|
||||
class="animate-float h-64 w-2/5"
|
||||
/>
|
||||
<div class="-enter-x text-1xl mt-6 font-sans text-white lg:text-2xl">
|
||||
{{ $t('authentication.layout-title') }}
|
||||
</div>
|
||||
<div class="-enter-x dark:text-muted-foreground mt-2 text-white/60">
|
||||
{{ $t('authentication.layout-desc') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="authPanelCenter"
|
||||
class="flex-center w-full dark:bg-[var(--color-authentication-to)]"
|
||||
>
|
||||
<AuthenticationFromView
|
||||
class="enter-y md:bg-background w-full rounded-3xl pb-20 shadow-2xl md:w-2/3 lg:w-1/2 xl:w-2/5"
|
||||
>
|
||||
<template #toolbar>
|
||||
<Toolbar class="bg-muted" />
|
||||
</template>
|
||||
</AuthenticationFromView>
|
||||
</div>
|
||||
<AuthenticationFromView
|
||||
v-if="authPanelRight"
|
||||
class="enter-x min-h-full w-2/5 flex-1"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- background-image: radial-gradient(
|
||||
rgba(255, 255, 255, 0.1) 1px,
|
||||
transparent 1px
|
||||
); -->
|
36
packages/business/layouts/src/authentication/from-view.vue
Normal file
36
packages/business/layouts/src/authentication/from-view.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { preference } from '@vben/preference';
|
||||
|
||||
import Toolbar from './toolbar.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'AuthenticationFormView',
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="relative flex flex-col items-center justify-center px-6 py-10 lg:flex-initial lg:px-8"
|
||||
>
|
||||
<slot name="toolbar">
|
||||
<Toolbar />
|
||||
</slot>
|
||||
|
||||
<RouterView v-slot="{ Component, route }">
|
||||
<Transition name="slide-right" mode="out-in" appear>
|
||||
<KeepAlive :include="['Login']">
|
||||
<component
|
||||
:is="Component"
|
||||
:key="route.fullPath"
|
||||
class="mt-6 w-full sm:mx-auto md:max-w-md"
|
||||
/>
|
||||
</KeepAlive>
|
||||
</Transition>
|
||||
</RouterView>
|
||||
|
||||
<div
|
||||
class="text-muted-foreground absolute bottom-3 flex text-center text-xs"
|
||||
>
|
||||
{{ preference.copyright }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
4568
packages/business/layouts/src/authentication/icons/slogan.vue
Normal file
4568
packages/business/layouts/src/authentication/icons/slogan.vue
Normal file
File diff suppressed because it is too large
Load Diff
1
packages/business/layouts/src/authentication/index.ts
Normal file
1
packages/business/layouts/src/authentication/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as AuthPageLayout } from './authentication.vue';
|
24
packages/business/layouts/src/authentication/toolbar.vue
Normal file
24
packages/business/layouts/src/authentication/toolbar.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
AuthenticationColorToggle,
|
||||
AuthenticationLayoutToggle,
|
||||
LanguageToggle,
|
||||
ThemeToggle,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
defineOptions({
|
||||
name: 'AuthenticationToolbar',
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="flex-center bg-background absolute right-2 top-4 rounded-3xl px-3 py-1 dark:bg-[var(--color-authentication-to)]"
|
||||
>
|
||||
<div class="hidden md:flex">
|
||||
<AuthenticationColorToggle />
|
||||
<AuthenticationLayoutToggle />
|
||||
</div>
|
||||
<LanguageToggle />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</template>
|
60
packages/business/layouts/src/basic/content/content.vue
Normal file
60
packages/business/layouts/src/basic/content/content.vue
Normal 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>
|
1
packages/business/layouts/src/basic/content/index.ts
Normal file
1
packages/business/layouts/src/basic/content/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as LayoutContent } from './content.vue';
|
11
packages/business/layouts/src/basic/footer/footer.vue
Normal file
11
packages/business/layouts/src/basic/footer/footer.vue
Normal 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>
|
1
packages/business/layouts/src/basic/footer/index.ts
Normal file
1
packages/business/layouts/src/basic/footer/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as LayoutFooter } from './footer.vue';
|
40
packages/business/layouts/src/basic/header/header.vue
Normal file
40
packages/business/layouts/src/basic/header/header.vue
Normal 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>
|
1
packages/business/layouts/src/basic/header/index.ts
Normal file
1
packages/business/layouts/src/basic/header/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as LayoutHeader } from './header.vue';
|
1
packages/business/layouts/src/basic/index.ts
Normal file
1
packages/business/layouts/src/basic/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as BasicLayout } from './layout.vue';
|
233
packages/business/layouts/src/basic/layout.vue
Normal file
233
packages/business/layouts/src/basic/layout.vue
Normal 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>
|
33
packages/business/layouts/src/basic/menu/extra-menu.vue
Normal file
33
packages/business/layouts/src/basic/menu/extra-menu.vue
Normal 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>
|
37
packages/business/layouts/src/basic/menu/helper.ts
Normal file
37
packages/business/layouts/src/basic/menu/helper.ts
Normal 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 };
|
5
packages/business/layouts/src/basic/menu/index.ts
Normal file
5
packages/business/layouts/src/basic/menu/index.ts
Normal 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';
|
34
packages/business/layouts/src/basic/menu/menu.vue
Normal file
34
packages/business/layouts/src/basic/menu/menu.vue
Normal 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>
|
53
packages/business/layouts/src/basic/menu/mixed-menu.vue
Normal file
53
packages/business/layouts/src/basic/menu/mixed-menu.vue
Normal 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>
|
90
packages/business/layouts/src/basic/menu/use-extra-menu.ts
Normal file
90
packages/business/layouts/src/basic/menu/use-extra-menu.ts
Normal 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 };
|
118
packages/business/layouts/src/basic/menu/use-mixed-menu.ts
Normal file
118
packages/business/layouts/src/basic/menu/use-mixed-menu.ts
Normal 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 };
|
3
packages/business/layouts/src/basic/tabs/index.ts
Normal file
3
packages/business/layouts/src/basic/tabs/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as LayoutTabs } from './tabs.vue';
|
||||
export { default as LayoutTabsToolbar } from './tabs-toolbar.vue';
|
||||
export * from './use-tabs';
|
35
packages/business/layouts/src/basic/tabs/tabs-toolbar.vue
Normal file
35
packages/business/layouts/src/basic/tabs/tabs-toolbar.vue
Normal 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>
|
32
packages/business/layouts/src/basic/tabs/tabs.vue
Normal file
32
packages/business/layouts/src/basic/tabs/tabs.vue
Normal 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>
|
184
packages/business/layouts/src/basic/tabs/use-tabs.ts
Normal file
184
packages/business/layouts/src/basic/tabs/use-tabs.ts
Normal 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 };
|
88
packages/business/layouts/src/basic/widgets/breadcrumb.vue
Normal file
88
packages/business/layouts/src/basic/widgets/breadcrumb.vue
Normal 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>
|
1
packages/business/layouts/src/basic/widgets/index.ts
Normal file
1
packages/business/layouts/src/basic/widgets/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Breadcrumb } from './breadcrumb.vue';
|
84
packages/business/layouts/src/iframe/iframe-router-view.vue
Normal file
84
packages/business/layouts/src/iframe/iframe-router-view.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<script lang="ts" setup>
|
||||
import { Spinner } from '@vben/common-ui';
|
||||
import { preference } from '@vben/preference';
|
||||
import { useTabsStore } from '@vben/stores';
|
||||
import { computed, ref } from 'vue';
|
||||
import { type RouteLocationNormalized, useRoute } from 'vue-router';
|
||||
|
||||
defineOptions({ name: 'IFrameRouterView' });
|
||||
|
||||
const spinning = ref(true);
|
||||
const tabsStore = useTabsStore();
|
||||
const route = useRoute();
|
||||
|
||||
const iframeRoutes = computed(() => {
|
||||
if (!preference.tabsVisible) {
|
||||
return route.meta.iframeSrc ? [route] : [];
|
||||
}
|
||||
const tabs = tabsStore.getTabs.filter((tab) => !!tab.meta?.iframeSrc);
|
||||
return tabs;
|
||||
});
|
||||
|
||||
const tabNames = computed(() => {
|
||||
const names = new Set<string>();
|
||||
iframeRoutes.value.forEach((item) => {
|
||||
names.add(item.name as string);
|
||||
});
|
||||
return names;
|
||||
});
|
||||
|
||||
const showIframe = computed(() => iframeRoutes.value.length > 0);
|
||||
|
||||
function routeShow(tabItem: RouteLocationNormalized) {
|
||||
const { name } = tabItem;
|
||||
return name === route.name;
|
||||
}
|
||||
|
||||
function canRender(tabItem: RouteLocationNormalized) {
|
||||
const { meta, name } = tabItem;
|
||||
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!tabsStore.renderRouteView) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!preference.tabsVisible) {
|
||||
return routeShow(tabItem);
|
||||
}
|
||||
|
||||
// 跟随 keepAlive 状态,与其他tab页保持一致
|
||||
if (
|
||||
!meta?.keepAlive &&
|
||||
tabNames.value.has(name as string) &&
|
||||
name !== route.name
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return tabsStore.getTabs.findIndex((tab) => tab.name === name) !== -1;
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
spinning.value = false;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<template v-if="showIframe">
|
||||
<template v-for="item in iframeRoutes" :key="item.fullPath">
|
||||
<div
|
||||
v-show="routeShow(item)"
|
||||
v-if="canRender(item)"
|
||||
class="relative size-full"
|
||||
>
|
||||
<Spinner :spinning="spinning" />
|
||||
<iframe
|
||||
:src="item.meta.iframeSrc as string"
|
||||
class="size-full"
|
||||
@load="hideLoading"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
6
packages/business/layouts/src/iframe/iframe-view.vue
Normal file
6
packages/business/layouts/src/iframe/iframe-view.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
defineOptions({ name: 'IFrameView' });
|
||||
</script>
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
2
packages/business/layouts/src/iframe/index.ts
Normal file
2
packages/business/layouts/src/iframe/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as IFrameRouterView } from './iframe-router-view.vue';
|
||||
export { default as IFrameView } from './iframe-view.vue';
|
3
packages/business/layouts/src/index.ts
Normal file
3
packages/business/layouts/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './authentication';
|
||||
export * from './basic';
|
||||
export * from './iframe';
|
1
packages/business/layouts/tailwind.config.mjs
Normal file
1
packages/business/layouts/tailwind.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@vben/tailwind-config';
|
5
packages/business/layouts/tsconfig.json
Normal file
5
packages/business/layouts/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/library.json",
|
||||
"include": ["src"]
|
||||
}
|
3
packages/business/layouts/vite.config.mts
Normal file
3
packages/business/layouts/vite.config.mts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { defineConfig } from '@vben/vite-config';
|
||||
|
||||
export default defineConfig();
|
Reference in New Issue
Block a user