refactor(project): business changed its name to effects, and universal-ui changed its name to common-ui
This commit is contained in:
75
packages/effects/layouts/src/basic/content/content.vue
Normal file
75
packages/effects/layouts/src/basic/content/content.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<script lang="ts" setup>
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router';
|
||||
|
||||
import { preferences, usePreferences } from '@vben-core/preferences';
|
||||
import { Spinner } from '@vben-core/shadcn-ui';
|
||||
import { storeToRefs, useCoreTabbarStore } from '@vben-core/stores';
|
||||
|
||||
import { IFrameRouterView } from '../../iframe';
|
||||
import { useContentSpinner } from './use-content-spinner';
|
||||
|
||||
defineOptions({ name: 'LayoutContent' });
|
||||
|
||||
const tabbarStore = useCoreTabbarStore();
|
||||
const { keepAlive } = usePreferences();
|
||||
const { spinning } = useContentSpinner();
|
||||
|
||||
const { getCachedTabs, getExcludeCachedTabs, renderRouteView } =
|
||||
storeToRefs(tabbarStore);
|
||||
|
||||
// 页面切换动画
|
||||
function getTransitionName(route: RouteLocationNormalizedLoaded) {
|
||||
// 如果偏好设置未设置,则不使用动画
|
||||
const { tabbar, transition } = preferences;
|
||||
const transitionName = transition.name;
|
||||
if (!transitionName || !transition.enable) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 标签页未启用或者未开启缓存,则使用全局配置动画
|
||||
if (!tabbar.enable || !keepAlive) {
|
||||
return transitionName;
|
||||
}
|
||||
|
||||
// 如果页面已经加载过,则不使用动画
|
||||
// if (route.meta.loaded) {
|
||||
// return;
|
||||
// }
|
||||
// 已经打开且已经加载过的页面不使用动画
|
||||
const inTabs = getCachedTabs.value.includes(route.name as string);
|
||||
|
||||
return inTabs && route.meta.loaded ? undefined : transitionName;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative h-full">
|
||||
<Spinner
|
||||
v-if="preferences.transition.loading"
|
||||
:spinning="spinning"
|
||||
class="h-[var(--vben-content-client-height)]"
|
||||
/>
|
||||
<IFrameRouterView />
|
||||
<RouterView v-slot="{ Component, route }">
|
||||
<Transition :name="getTransitionName(route)" appear mode="out-in">
|
||||
<KeepAlive
|
||||
v-if="keepAlive"
|
||||
:exclude="getExcludeCachedTabs"
|
||||
:include="getCachedTabs"
|
||||
>
|
||||
<component
|
||||
:is="Component"
|
||||
v-if="renderRouteView"
|
||||
v-show="!route.meta.iframeSrc"
|
||||
:key="route.fullPath"
|
||||
/>
|
||||
</KeepAlive>
|
||||
<component
|
||||
:is="Component"
|
||||
v-else-if="renderRouteView"
|
||||
:key="route.fullPath"
|
||||
/>
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</div>
|
||||
</template>
|
1
packages/effects/layouts/src/basic/content/index.ts
Normal file
1
packages/effects/layouts/src/basic/content/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as LayoutContent } from './content.vue';
|
@@ -0,0 +1,50 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { preferences } from '@vben-core/preferences';
|
||||
|
||||
function useContentSpinner() {
|
||||
const spinning = ref(false);
|
||||
const startTime = ref(0);
|
||||
const router = useRouter();
|
||||
const minShowTime = 500;
|
||||
const enableLoading = computed(() => preferences.transition.loading);
|
||||
|
||||
const onEnd = () => {
|
||||
if (!enableLoading.value) {
|
||||
return;
|
||||
}
|
||||
const processTime = performance.now() - startTime.value;
|
||||
if (processTime < minShowTime) {
|
||||
setTimeout(() => {
|
||||
spinning.value = false;
|
||||
}, minShowTime - processTime);
|
||||
} else {
|
||||
spinning.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
router.beforeEach((to) => {
|
||||
if (to.meta.loaded || !enableLoading.value || to.meta.iframeSrc) {
|
||||
return true;
|
||||
}
|
||||
startTime.value = performance.now();
|
||||
spinning.value = true;
|
||||
return true;
|
||||
});
|
||||
|
||||
router.afterEach((to) => {
|
||||
if (to.meta.loaded || !enableLoading.value || to.meta.iframeSrc) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 关闭加载动画
|
||||
onEnd();
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return { spinning };
|
||||
}
|
||||
|
||||
export { useContentSpinner };
|
44
packages/effects/layouts/src/basic/copyright/copyright.vue
Normal file
44
packages/effects/layouts/src/basic/copyright/copyright.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script lang="ts" setup>
|
||||
interface Props {
|
||||
companyName: string;
|
||||
companySiteLink?: string;
|
||||
date: string;
|
||||
icp?: string;
|
||||
icpLink?: string;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'Copyright',
|
||||
});
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
companyName: 'Vben Admin Pro',
|
||||
companySiteLink: '',
|
||||
date: '2024',
|
||||
icp: '',
|
||||
icpLink: '',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-md flex-center">
|
||||
<a
|
||||
v-if="icp"
|
||||
:href="icpLink || 'javascript:void 0'"
|
||||
class="hover:text-primary-hover"
|
||||
target="_blank"
|
||||
>
|
||||
{{ icp }}
|
||||
</a>
|
||||
|
||||
Copyright © {{ date }}
|
||||
<a
|
||||
v-if="companyName"
|
||||
:href="companySiteLink || 'javascript:void 0'"
|
||||
class="hover:text-primary-hover mx-1"
|
||||
target="_blank"
|
||||
>
|
||||
{{ companyName }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
1
packages/effects/layouts/src/basic/copyright/index.ts
Normal file
1
packages/effects/layouts/src/basic/copyright/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Copyright } from './copyright.vue';
|
11
packages/effects/layouts/src/basic/footer/footer.vue
Normal file
11
packages/effects/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/effects/layouts/src/basic/footer/index.ts
Normal file
1
packages/effects/layouts/src/basic/footer/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as LayoutFooter } from './footer.vue';
|
47
packages/effects/layouts/src/basic/header/header.vue
Normal file
47
packages/effects/layouts/src/basic/header/header.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script lang="ts" setup>
|
||||
import { preferences, usePreferences } from '@vben-core/preferences';
|
||||
import { VbenFullScreen } from '@vben-core/shadcn-ui';
|
||||
import { useCoreAccessStore } from '@vben-core/stores';
|
||||
|
||||
import { GlobalSearch, LanguageToggle, ThemeToggle } from '../../widgets';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Logo 主题
|
||||
*/
|
||||
theme?: string;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'LayoutHeader',
|
||||
});
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
theme: 'light',
|
||||
});
|
||||
|
||||
const accessStore = useCoreAccessStore();
|
||||
const { globalSearchShortcutKey } = usePreferences();
|
||||
</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
|
||||
v-if="preferences.widget.globalSearch"
|
||||
:enable-shortcut-key="globalSearchShortcutKey"
|
||||
:menus="accessStore.accessMenus"
|
||||
class="mr-4"
|
||||
/>
|
||||
<ThemeToggle v-if="preferences.widget.themeToggle" class="mr-2" />
|
||||
<LanguageToggle v-if="preferences.widget.languageToggle" class="mr-2" />
|
||||
<VbenFullScreen v-if="preferences.widget.fullscreen" class="mr-2" />
|
||||
<slot v-if="preferences.widget.notification" name="notification"></slot>
|
||||
<slot name="user-dropdown"></slot>
|
||||
</div>
|
||||
</template>
|
1
packages/effects/layouts/src/basic/header/index.ts
Normal file
1
packages/effects/layouts/src/basic/header/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as LayoutHeader } from './header.vue';
|
1
packages/effects/layouts/src/basic/index.ts
Normal file
1
packages/effects/layouts/src/basic/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as BasicLayout } from './layout.vue';
|
288
packages/effects/layouts/src/basic/layout.vue
Normal file
288
packages/effects/layouts/src/basic/layout.vue
Normal file
@@ -0,0 +1,288 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { VbenAdminLayout } from '@vben-core/layout-ui';
|
||||
import { $t } from '@vben-core/locales';
|
||||
import {
|
||||
preferences,
|
||||
updatePreferences,
|
||||
usePreferences,
|
||||
} from '@vben-core/preferences';
|
||||
import { VbenBackTop, VbenLogo } from '@vben-core/shadcn-ui';
|
||||
import { mapTree } from '@vben-core/toolkit';
|
||||
import { MenuRecordRaw } from '@vben-core/typings';
|
||||
|
||||
import { Breadcrumb, CozeAssistant, Preferences } from '../widgets';
|
||||
import { LayoutContent } from './content';
|
||||
import { Copyright } from './copyright';
|
||||
import { LayoutFooter } from './footer';
|
||||
import { LayoutHeader } from './header';
|
||||
import {
|
||||
LayoutExtraMenu,
|
||||
LayoutMenu,
|
||||
LayoutMixedMenu,
|
||||
useExtraMenu,
|
||||
useMixedMenu,
|
||||
} from './menu';
|
||||
import { LayoutTabbar, LayoutTabbarTools } from './tabbar';
|
||||
|
||||
defineOptions({ name: 'BasicLayout' });
|
||||
|
||||
const emit = defineEmits<{ clearPreferencesAndLogout: [] }>();
|
||||
|
||||
const { isDark, isHeaderNav, isMixedNav, isSideMixedNav, layout } =
|
||||
usePreferences();
|
||||
|
||||
const headerMenuTheme = computed(() => {
|
||||
return isDark.value ? 'dark' : 'light';
|
||||
});
|
||||
|
||||
const theme = computed(() => {
|
||||
const dark = isDark.value || preferences.theme.semiDarkMenu;
|
||||
return dark ? 'dark' : 'light';
|
||||
});
|
||||
|
||||
const logoClass = computed(() => {
|
||||
let cls = '';
|
||||
const { collapsed, collapsedShowTitle } = preferences.sidebar;
|
||||
if (collapsedShowTitle && collapsed && !isMixedNav.value) {
|
||||
cls += ' mx-auto';
|
||||
}
|
||||
if (isSideMixedNav.value) {
|
||||
cls += ' flex-center';
|
||||
}
|
||||
return cls;
|
||||
});
|
||||
|
||||
const isMenuRounded = computed(() => {
|
||||
return preferences.navigation.styleType === 'rounded';
|
||||
});
|
||||
|
||||
const logoCollapse = computed(() => {
|
||||
if (isHeaderNav.value || isMixedNav.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { isMobile } = preferences.app;
|
||||
const { collapsed } = preferences.sidebar;
|
||||
|
||||
if (!collapsed && isMobile) {
|
||||
return false;
|
||||
}
|
||||
return collapsed || isSideMixedNav.value;
|
||||
});
|
||||
|
||||
const showHeaderNav = computed(() => {
|
||||
return isHeaderNav.value || isMixedNav.value;
|
||||
});
|
||||
|
||||
const {
|
||||
extraActiveMenu,
|
||||
extraMenus,
|
||||
handleDefaultSelect,
|
||||
handleMenuMouseEnter,
|
||||
handleMixedMenuSelect,
|
||||
handleSideMouseLeave,
|
||||
sidebarExtraVisible,
|
||||
} = useExtraMenu();
|
||||
|
||||
const {
|
||||
handleMenuSelect,
|
||||
headerActive,
|
||||
headerMenus,
|
||||
sideActive,
|
||||
sideMenus,
|
||||
sideVisible,
|
||||
} = useMixedMenu();
|
||||
|
||||
function wrapperMenus(menus: MenuRecordRaw[]) {
|
||||
return mapTree(menus, (item) => {
|
||||
return { ...item, name: $t(item.name) };
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSidebar() {
|
||||
updatePreferences({
|
||||
sidebar: {
|
||||
hidden: !preferences.sidebar.hidden,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function clearPreferencesAndLogout() {
|
||||
emit('clearPreferencesAndLogout');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VbenAdminLayout
|
||||
v-model:sidebar-extra-visible="sidebarExtraVisible"
|
||||
:content-compact="preferences.app.contentCompact"
|
||||
:footer-enable="preferences.footer.enable"
|
||||
:footer-fixed="preferences.footer.fixed"
|
||||
:header-hidden="preferences.header.hidden"
|
||||
:header-mode="preferences.header.mode"
|
||||
:header-toggle-sidebar-button="preferences.widget.sidebarToggle"
|
||||
:header-visible="preferences.header.enable"
|
||||
:is-mobile="preferences.app.isMobile"
|
||||
:layout="layout"
|
||||
:sidebar-collapse="preferences.sidebar.collapsed"
|
||||
:sidebar-collapse-show-title="preferences.sidebar.collapsedShowTitle"
|
||||
:sidebar-enable="sideVisible"
|
||||
:sidebar-expand-on-hover="preferences.sidebar.expandOnHover"
|
||||
:sidebar-extra-collapse="preferences.sidebar.extraCollapse"
|
||||
:sidebar-hidden="preferences.sidebar.hidden"
|
||||
:sidebar-semi-dark="preferences.theme.semiDarkMenu"
|
||||
:sidebar-theme="theme"
|
||||
:sidebar-width="preferences.sidebar.width"
|
||||
:tabbar-enable="preferences.tabbar.enable"
|
||||
@side-mouse-leave="handleSideMouseLeave"
|
||||
@toggle-sidebar="toggleSidebar"
|
||||
@update:sidebar-collapse="
|
||||
(value: boolean) => updatePreferences({ sidebar: { collapsed: value } })
|
||||
"
|
||||
@update:sidebar-enable="
|
||||
(value: boolean) => updatePreferences({ sidebar: { enable: value } })
|
||||
"
|
||||
@update:sidebar-expand-on-hover="
|
||||
(value: boolean) =>
|
||||
updatePreferences({ sidebar: { expandOnHover: value } })
|
||||
"
|
||||
@update:sidebar-extra-collapse="
|
||||
(value: boolean) =>
|
||||
updatePreferences({ sidebar: { extraCollapse: value } })
|
||||
"
|
||||
>
|
||||
<template v-if="preferences.app.enablePreferences" #preferences>
|
||||
<Preferences @clear-preferences-and-logout="clearPreferencesAndLogout" />
|
||||
</template>
|
||||
|
||||
<template #floating-groups>
|
||||
<CozeAssistant
|
||||
v-if="preferences.widget.aiAssistant"
|
||||
:is-mobile="preferences.app.isMobile"
|
||||
/>
|
||||
<VbenBackTop />
|
||||
</template>
|
||||
|
||||
<!-- logo -->
|
||||
<template #logo>
|
||||
<VbenLogo
|
||||
:alt="preferences.app.name"
|
||||
:class="logoClass"
|
||||
:collapse="logoCollapse"
|
||||
:src="preferences.logo.source"
|
||||
:text="preferences.app.name"
|
||||
:theme="showHeaderNav ? headerMenuTheme : theme"
|
||||
/>
|
||||
</template>
|
||||
<!-- 头部区域 -->
|
||||
<template #header>
|
||||
<LayoutHeader :theme="theme">
|
||||
<template
|
||||
v-if="!showHeaderNav && preferences.breadcrumb.enable"
|
||||
#breadcrumb
|
||||
>
|
||||
<Breadcrumb
|
||||
:hide-when-only-one="preferences.breadcrumb.hideOnlyOne"
|
||||
:show-home="preferences.breadcrumb.showHome"
|
||||
:show-icon="preferences.breadcrumb.showIcon"
|
||||
:type="preferences.breadcrumb.styleType"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="showHeaderNav" #menu>
|
||||
<LayoutMenu
|
||||
:default-active="headerActive"
|
||||
:menus="wrapperMenus(headerMenus)"
|
||||
:rounded="isMenuRounded"
|
||||
:theme="headerMenuTheme"
|
||||
class="w-full"
|
||||
mode="horizontal"
|
||||
@select="handleMenuSelect"
|
||||
/>
|
||||
</template>
|
||||
<template #user-dropdown>
|
||||
<slot name="user-dropdown"></slot>
|
||||
</template>
|
||||
<template #notification>
|
||||
<slot name="notification"></slot>
|
||||
</template>
|
||||
</LayoutHeader>
|
||||
</template>
|
||||
<!-- 侧边菜单区域 -->
|
||||
<template #menu>
|
||||
<LayoutMenu
|
||||
:accordion="preferences.navigation.accordion"
|
||||
:collapse="preferences.sidebar.collapsed"
|
||||
:collapse-show-title="preferences.sidebar.collapsedShowTitle"
|
||||
:default-active="sideActive"
|
||||
:menus="wrapperMenus(sideMenus)"
|
||||
:rounded="isMenuRounded"
|
||||
:theme="theme"
|
||||
mode="vertical"
|
||||
@select="handleMenuSelect"
|
||||
/>
|
||||
</template>
|
||||
<template #mixed-menu>
|
||||
<LayoutMixedMenu
|
||||
:active-path="extraActiveMenu"
|
||||
:collapse="!preferences.sidebar.collapsedShowTitle"
|
||||
:menus="wrapperMenus(headerMenus)"
|
||||
:rounded="isMenuRounded"
|
||||
:theme="theme"
|
||||
@default-select="handleDefaultSelect"
|
||||
@enter="handleMenuMouseEnter"
|
||||
@select="handleMixedMenuSelect"
|
||||
/>
|
||||
</template>
|
||||
<!-- 侧边额外区域 -->
|
||||
<template #side-extra>
|
||||
<LayoutExtraMenu
|
||||
:accordion="preferences.navigation.accordion"
|
||||
:collapse="preferences.sidebar.extraCollapse"
|
||||
:menus="wrapperMenus(extraMenus)"
|
||||
:rounded="isMenuRounded"
|
||||
:theme="theme"
|
||||
/>
|
||||
</template>
|
||||
<template #side-extra-title>
|
||||
<VbenLogo
|
||||
v-if="preferences.logo.enable"
|
||||
:alt="preferences.app.name"
|
||||
:text="preferences.app.name"
|
||||
:theme="theme"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #tabbar>
|
||||
<LayoutTabbar
|
||||
v-if="preferences.tabbar.enable"
|
||||
:show-icon="preferences.tabbar.showIcon"
|
||||
/>
|
||||
</template>
|
||||
<template #tabbar-tools>
|
||||
<LayoutTabbarTools v-if="preferences.tabbar.enable" />
|
||||
</template>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<template #content>
|
||||
<LayoutContent />
|
||||
</template>
|
||||
<!-- 页脚 -->
|
||||
<template v-if="preferences.footer.enable" #footer>
|
||||
<LayoutFooter>
|
||||
<Copyright
|
||||
v-if="preferences.copyright.enable"
|
||||
v-bind="preferences.copyright"
|
||||
/>
|
||||
</LayoutFooter>
|
||||
</template>
|
||||
|
||||
<template #extra>
|
||||
<slot name="extra"></slot>
|
||||
<Transition v-if="preferences.widget.lockScreen" name="slide-up">
|
||||
<slot name="lock-screen"></slot>
|
||||
</Transition>
|
||||
</template>
|
||||
</VbenAdminLayout>
|
||||
</template>
|
39
packages/effects/layouts/src/basic/menu/extra-menu.vue
Normal file
39
packages/effects/layouts/src/basic/menu/extra-menu.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MenuRecordRaw } from '@vben-core/typings';
|
||||
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { Menu, MenuProps } from '@vben-core/menu-ui';
|
||||
|
||||
import { useNavigation } from './use-navigation';
|
||||
|
||||
interface Props extends MenuProps {
|
||||
collspae?: boolean;
|
||||
menus: MenuRecordRaw[];
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
accordion: true,
|
||||
menus: () => [],
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const { navigation } = useNavigation();
|
||||
|
||||
async function handleSelect(key: string) {
|
||||
await navigation(key);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Menu
|
||||
:accordion="accordion"
|
||||
:collapse="collapse"
|
||||
:default-active="route.path"
|
||||
:menus="menus"
|
||||
:rounded="rounded"
|
||||
:theme="theme"
|
||||
mode="vertical"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
</template>
|
5
packages/effects/layouts/src/basic/menu/index.ts
Normal file
5
packages/effects/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';
|
36
packages/effects/layouts/src/basic/menu/menu.vue
Normal file
36
packages/effects/layouts/src/basic/menu/menu.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<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>(), {
|
||||
accordion: true,
|
||||
menus: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [string, string?];
|
||||
}>();
|
||||
|
||||
function handleMenuSelect(key: string) {
|
||||
emit('select', key, props.mode);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Menu
|
||||
:accordion="accordion"
|
||||
:collapse="collapse"
|
||||
:collapse-show-title="collapseShowTitle"
|
||||
:default-active="defaultActive"
|
||||
:menus="menus"
|
||||
:mode="mode"
|
||||
:rounded="rounded"
|
||||
:theme="theme"
|
||||
@select="handleMenuSelect"
|
||||
/>
|
||||
</template>
|
48
packages/effects/layouts/src/basic/menu/mixed-menu.vue
Normal file
48
packages/effects/layouts/src/basic/menu/mixed-menu.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script lang="ts" setup>
|
||||
import type { NormalMenuProps } from '@vben-core/menu-ui';
|
||||
import type { MenuRecordRaw } from '@vben-core/typings';
|
||||
|
||||
import { onBeforeMount } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { findMenuByPath } from '@vben-core/helpers';
|
||||
import { NormalMenu } from '@vben-core/menu-ui';
|
||||
|
||||
interface Props extends NormalMenuProps {}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
defaultSelect: [MenuRecordRaw, MenuRecordRaw?];
|
||||
enter: [MenuRecordRaw];
|
||||
select: [MenuRecordRaw];
|
||||
}>();
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
function handleSelect(menu: MenuRecordRaw) {
|
||||
emit('select', menu);
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
const menu = findMenuByPath(props.menus || [], route.path);
|
||||
if (menu) {
|
||||
const rootMenu = (props.menus || []).find(
|
||||
(item) => item.path === menu.parents?.[0],
|
||||
);
|
||||
emit('defaultSelect', menu, rootMenu);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NormalMenu
|
||||
:active-path="activePath"
|
||||
:collapse="collapse"
|
||||
:menus="menus"
|
||||
:rounded="rounded"
|
||||
:theme="theme"
|
||||
@enter="(menu) => emit('enter', menu)"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
</template>
|
92
packages/effects/layouts/src/basic/menu/use-extra-menu.ts
Normal file
92
packages/effects/layouts/src/basic/menu/use-extra-menu.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { MenuRecordRaw } from '@vben-core/typings';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { findRootMenuByPath } from '@vben-core/helpers';
|
||||
import { preferences } from '@vben-core/preferences';
|
||||
import { useCoreAccessStore } from '@vben-core/stores';
|
||||
|
||||
import { useNavigation } from './use-navigation';
|
||||
|
||||
function useExtraMenu() {
|
||||
const accessStore = useCoreAccessStore();
|
||||
const { navigation } = useNavigation();
|
||||
|
||||
const menus = computed(() => accessStore.accessMenus);
|
||||
|
||||
const route = useRoute();
|
||||
const extraMenus = ref<MenuRecordRaw[]>([]);
|
||||
const sidebarExtraVisible = ref<boolean>(false);
|
||||
const extraActiveMenu = ref('');
|
||||
|
||||
/**
|
||||
* 选择混合菜单事件
|
||||
* @param menu
|
||||
*/
|
||||
const handleMixedMenuSelect = async (menu: MenuRecordRaw) => {
|
||||
extraMenus.value = menu?.children ?? [];
|
||||
extraActiveMenu.value = menu.parents?.[0] ?? menu.path;
|
||||
const hasChildren = extraMenus.value.length > 0;
|
||||
|
||||
sidebarExtraVisible.value = hasChildren;
|
||||
if (!hasChildren) {
|
||||
await navigation(menu.path);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 选择默认菜单事件
|
||||
* @param menu
|
||||
* @param rootMenu
|
||||
*/
|
||||
const handleDefaultSelect = (
|
||||
menu: MenuRecordRaw,
|
||||
rootMenu?: MenuRecordRaw,
|
||||
) => {
|
||||
extraMenus.value = rootMenu?.children ?? [];
|
||||
extraActiveMenu.value = menu.parents?.[0] ?? menu.path;
|
||||
|
||||
if (preferences.sidebar.expandOnHover) {
|
||||
sidebarExtraVisible.value = extraMenus.value.length > 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 侧边菜单鼠标移出事件
|
||||
*/
|
||||
const handleSideMouseLeave = () => {
|
||||
if (preferences.sidebar.expandOnHover) {
|
||||
return;
|
||||
}
|
||||
sidebarExtraVisible.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 (!preferences.sidebar.expandOnHover) {
|
||||
const { findMenu } = findRootMenuByPath(menus.value, menu.path);
|
||||
extraMenus.value = findMenu?.children ?? [];
|
||||
extraActiveMenu.value = menu.parents?.[0] ?? menu.path;
|
||||
sidebarExtraVisible.value = extraMenus.value.length > 0;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
extraActiveMenu,
|
||||
extraMenus,
|
||||
handleDefaultSelect,
|
||||
handleMenuMouseEnter,
|
||||
handleMixedMenuSelect,
|
||||
handleSideMouseLeave,
|
||||
sidebarExtraVisible,
|
||||
};
|
||||
}
|
||||
|
||||
export { useExtraMenu };
|
120
packages/effects/layouts/src/basic/menu/use-mixed-menu.ts
Normal file
120
packages/effects/layouts/src/basic/menu/use-mixed-menu.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import type { MenuRecordRaw } from '@vben-core/typings';
|
||||
|
||||
import { computed, onBeforeMount, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { findRootMenuByPath } from '@vben-core/helpers';
|
||||
import { preferences, usePreferences } from '@vben-core/preferences';
|
||||
import { useCoreAccessStore } from '@vben-core/stores';
|
||||
|
||||
import { useNavigation } from './use-navigation';
|
||||
|
||||
function useMixedMenu() {
|
||||
const accessStore = useCoreAccessStore();
|
||||
const { navigation } = useNavigation();
|
||||
const route = useRoute();
|
||||
const splitSideMenus = ref<MenuRecordRaw[]>([]);
|
||||
const rootMenuPath = ref<string>('');
|
||||
|
||||
const { isMixedNav } = usePreferences();
|
||||
|
||||
const needSplit = computed(
|
||||
() => preferences.navigation.split && isMixedNav.value,
|
||||
);
|
||||
|
||||
const sideVisible = computed(() => {
|
||||
const enableSidebar = preferences.sidebar.enable;
|
||||
if (needSplit.value) {
|
||||
return enableSidebar && splitSideMenus.value.length > 0;
|
||||
}
|
||||
return enableSidebar;
|
||||
});
|
||||
const menus = computed(() => accessStore.accessMenus);
|
||||
|
||||
/**
|
||||
* 头部菜单
|
||||
*/
|
||||
const headerMenus = computed(() => {
|
||||
if (!needSplit.value) {
|
||||
return menus.value;
|
||||
}
|
||||
return menus.value.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
children: [],
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 侧边菜单
|
||||
*/
|
||||
const sideMenus = computed(() => {
|
||||
return needSplit.value ? splitSideMenus.value : menus.value;
|
||||
});
|
||||
|
||||
/**
|
||||
* 侧边菜单激活路径
|
||||
*/
|
||||
const sideActive = computed(() => {
|
||||
return route.path;
|
||||
});
|
||||
|
||||
/**
|
||||
* 头部菜单激活路径
|
||||
*/
|
||||
const headerActive = computed(() => {
|
||||
if (!needSplit.value) {
|
||||
return route.path;
|
||||
}
|
||||
return rootMenuPath.value;
|
||||
});
|
||||
|
||||
/**
|
||||
* 菜单点击事件处理
|
||||
* @param key 菜单路径
|
||||
* @param mode 菜单模式
|
||||
*/
|
||||
const handleMenuSelect = (key: string, mode?: string) => {
|
||||
if (!needSplit.value || mode === 'vertical') {
|
||||
navigation(key);
|
||||
return;
|
||||
}
|
||||
|
||||
const rootMenu = menus.value.find((item) => item.path === key);
|
||||
rootMenuPath.value = rootMenu?.path ?? '';
|
||||
splitSideMenus.value = rootMenu?.children ?? [];
|
||||
if (splitSideMenus.value.length === 0) {
|
||||
navigation(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 };
|
19
packages/effects/layouts/src/basic/menu/use-navigation.ts
Normal file
19
packages/effects/layouts/src/basic/menu/use-navigation.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { isHttpUrl, openWindow } from '@vben-core/toolkit';
|
||||
|
||||
function useNavigation() {
|
||||
const router = useRouter();
|
||||
|
||||
const navigation = async (path: string) => {
|
||||
if (isHttpUrl(path)) {
|
||||
openWindow(path, { target: '_blank' });
|
||||
} else {
|
||||
await router.push(path);
|
||||
}
|
||||
};
|
||||
|
||||
return { navigation };
|
||||
}
|
||||
|
||||
export { useNavigation };
|
3
packages/effects/layouts/src/basic/tabbar/index.ts
Normal file
3
packages/effects/layouts/src/basic/tabbar/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as LayoutTabbar } from './tabbar.vue';
|
||||
export { default as LayoutTabbarTools } from './tabbar-tools.vue';
|
||||
export * from './use-tabs';
|
28
packages/effects/layouts/src/basic/tabbar/tabbar-tools.vue
Normal file
28
packages/effects/layouts/src/basic/tabbar/tabbar-tools.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { preferences } from '@vben-core/preferences';
|
||||
import { TabsToolMore, TabsToolScreen } from '@vben-core/tabs-ui';
|
||||
|
||||
import { updateContentScreen, useTabs } from './use-tabs';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const { createContextMenus } = useTabs();
|
||||
|
||||
const menus = computed(() => {
|
||||
return createContextMenus(route);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-center h-full">
|
||||
<TabsToolMore :menus="menus" />
|
||||
<TabsToolScreen
|
||||
:screen="preferences.sidebar.hidden"
|
||||
@change="updateContentScreen"
|
||||
@update:screen="updateContentScreen"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
45
packages/effects/layouts/src/basic/tabbar/tabbar.vue
Normal file
45
packages/effects/layouts/src/basic/tabbar/tabbar.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script lang="ts" setup>
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { preferences } from '@vben-core/preferences';
|
||||
import { useCoreTabbarStore } from '@vben-core/stores';
|
||||
import { TabsView } from '@vben-core/tabs-ui';
|
||||
|
||||
import { useTabs } from './use-tabs';
|
||||
|
||||
defineOptions({
|
||||
name: 'LayoutTabbar',
|
||||
});
|
||||
|
||||
defineProps<{ showIcon?: boolean }>();
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const coreTabbarStore = useCoreTabbarStore();
|
||||
|
||||
const {
|
||||
createContextMenus,
|
||||
currentActive,
|
||||
currentTabs,
|
||||
handleClick,
|
||||
handleClose,
|
||||
handleUnpinTab,
|
||||
} = useTabs();
|
||||
|
||||
// 刷新后如果不保持tab状态,关闭其他tab
|
||||
if (!preferences.tabbar.persist) {
|
||||
coreTabbarStore.closeOtherTabs(route);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabsView
|
||||
:active="currentActive"
|
||||
:menus="createContextMenus"
|
||||
:show-icon="showIcon"
|
||||
:tabs="currentTabs"
|
||||
@close="handleClose"
|
||||
@unpin-tab="handleUnpinTab"
|
||||
@update:active="handleClick"
|
||||
/>
|
||||
</template>
|
244
packages/effects/layouts/src/basic/tabbar/use-tabs.ts
Normal file
244
packages/effects/layouts/src/basic/tabbar/use-tabs.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import type { IContextMenuItem } from '@vben-core/tabs-ui';
|
||||
import type { TabItem } from '@vben-core/typings';
|
||||
import type {
|
||||
RouteLocationNormalized,
|
||||
RouteLocationNormalizedGeneric,
|
||||
} from 'vue-router';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import {
|
||||
IcRoundClose,
|
||||
IcRoundFitScreen,
|
||||
IcRoundMultipleStop,
|
||||
IcRoundRefresh,
|
||||
IcRoundTableView,
|
||||
IcTwotoneFitScreen,
|
||||
MdiArrowExpandHorizontal,
|
||||
MdiFormatHorizontalAlignLeft,
|
||||
MdiFormatHorizontalAlignRight,
|
||||
MdiPin,
|
||||
MdiPinOff,
|
||||
} from '@vben-core/iconify';
|
||||
import { $t, useI18n } from '@vben-core/locales';
|
||||
import { updatePreferences, usePreferences } from '@vben-core/preferences';
|
||||
import {
|
||||
storeToRefs,
|
||||
useCoreAccessStore,
|
||||
useCoreTabbarStore,
|
||||
} from '@vben-core/stores';
|
||||
import { filterTree, openWindow } from '@vben-core/toolkit';
|
||||
|
||||
function updateContentScreen(screen: boolean) {
|
||||
updatePreferences({
|
||||
header: {
|
||||
hidden: !!screen,
|
||||
},
|
||||
sidebar: {
|
||||
hidden: !!screen,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function useTabs() {
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const accessStore = useCoreAccessStore();
|
||||
const { contentIsMaximize } = usePreferences();
|
||||
const coreTabbarStore = useCoreTabbarStore();
|
||||
const { accessMenus } = storeToRefs(accessStore);
|
||||
|
||||
const currentActive = computed(() => {
|
||||
return route.path;
|
||||
});
|
||||
|
||||
const { locale } = useI18n();
|
||||
const currentTabs = ref<RouteLocationNormalizedGeneric[]>();
|
||||
watch([() => coreTabbarStore.getTabs, () => locale.value], ([tabs, _]) => {
|
||||
currentTabs.value = tabs.map((item) => wrapperTabLocale(item));
|
||||
});
|
||||
|
||||
/**
|
||||
* 初始化固定标签页
|
||||
*/
|
||||
const initAffixTabs = () => {
|
||||
const affixTabs = filterTree(router.getRoutes(), (route) => {
|
||||
return !!route.meta?.affixTab;
|
||||
});
|
||||
coreTabbarStore.setAffixTabs(affixTabs);
|
||||
};
|
||||
|
||||
// 点击tab,跳转路由
|
||||
const handleClick = (key: string) => {
|
||||
router.push(key);
|
||||
};
|
||||
|
||||
// 关闭tab
|
||||
const handleClose = async (key: string) => {
|
||||
await coreTabbarStore.closeTabByKey(key, router);
|
||||
};
|
||||
|
||||
function wrapperTabLocale(tab: RouteLocationNormalizedGeneric) {
|
||||
return {
|
||||
...tab,
|
||||
meta: {
|
||||
...tab.meta,
|
||||
title: $t(tab.meta.title as string),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
watch(
|
||||
() => accessMenus.value,
|
||||
() => {
|
||||
initAffixTabs();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
coreTabbarStore.addTab(route as RouteLocationNormalized);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const createContextMenus = (tab: TabItem) => {
|
||||
const tabs = coreTabbarStore.getTabs;
|
||||
const affixTabs = coreTabbarStore.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[] = [
|
||||
{
|
||||
handler: async () => {
|
||||
if (!contentIsMaximize.value) {
|
||||
await router.push(tab.fullPath);
|
||||
}
|
||||
updateContentScreen(!contentIsMaximize.value);
|
||||
},
|
||||
icon: contentIsMaximize.value ? IcRoundFitScreen : IcTwotoneFitScreen,
|
||||
key: contentIsMaximize.value ? 'restore-maximize' : 'maximize',
|
||||
text: contentIsMaximize.value
|
||||
? $t('preferences.tabbar.contextMenu.restoreMaximize')
|
||||
: $t('preferences.tabbar.contextMenu.maximize'),
|
||||
},
|
||||
{
|
||||
disabled: !isCurrentTab,
|
||||
handler: async () => {
|
||||
await coreTabbarStore.refresh(router);
|
||||
},
|
||||
icon: IcRoundRefresh,
|
||||
key: 'reload',
|
||||
text: $t('preferences.tabbar.contextMenu.reload'),
|
||||
},
|
||||
{
|
||||
disabled: !!affixTab || disabled,
|
||||
handler: async () => {
|
||||
await coreTabbarStore.closeTab(tab, router);
|
||||
},
|
||||
icon: IcRoundClose,
|
||||
key: 'close',
|
||||
text: $t('preferences.tabbar.contextMenu.close'),
|
||||
},
|
||||
{
|
||||
handler: async () => {
|
||||
await (affixTab
|
||||
? coreTabbarStore.unpinTab(tab)
|
||||
: coreTabbarStore.pinTab(tab));
|
||||
},
|
||||
icon: affixTab ? MdiPinOff : MdiPin,
|
||||
key: 'affix',
|
||||
text: affixTab
|
||||
? $t('preferences.tabbar.contextMenu.unpin')
|
||||
: $t('preferences.tabbar.contextMenu.pin'),
|
||||
},
|
||||
{
|
||||
handler: async () => {
|
||||
const { hash, origin } = location;
|
||||
const path = tab.fullPath;
|
||||
const fullPath = path.startsWith('/') ? path : `/${path}`;
|
||||
const url = `${origin}${hash ? '/#' : ''}${fullPath}`;
|
||||
openWindow(url, { target: '_blank' });
|
||||
},
|
||||
icon: IcRoundTableView,
|
||||
key: 'open-in-new-window',
|
||||
separator: true,
|
||||
text: $t('preferences.tabbar.contextMenu.openInNewWindow'),
|
||||
},
|
||||
|
||||
{
|
||||
disabled: closeLeftDisabled,
|
||||
handler: async () => {
|
||||
await coreTabbarStore.closeLeftTabs(tab);
|
||||
},
|
||||
icon: MdiFormatHorizontalAlignLeft,
|
||||
key: 'close-left',
|
||||
text: $t('preferences.tabbar.contextMenu.closeLeft'),
|
||||
},
|
||||
{
|
||||
disabled: closeRightDisabled,
|
||||
handler: async () => {
|
||||
await coreTabbarStore.closeRightTabs(tab);
|
||||
},
|
||||
icon: MdiFormatHorizontalAlignRight,
|
||||
key: 'close-right',
|
||||
separator: true,
|
||||
text: $t('preferences.tabbar.contextMenu.closeRight'),
|
||||
},
|
||||
{
|
||||
disabled: closeOtherDisabled,
|
||||
handler: async () => {
|
||||
await coreTabbarStore.closeOtherTabs(tab);
|
||||
},
|
||||
icon: MdiArrowExpandHorizontal,
|
||||
key: 'close-other',
|
||||
text: $t('preferences.tabbar.contextMenu.closeOther'),
|
||||
},
|
||||
{
|
||||
disabled,
|
||||
handler: async () => {
|
||||
await coreTabbarStore.closeAllTabs(router);
|
||||
},
|
||||
icon: IcRoundMultipleStop,
|
||||
key: 'close-all',
|
||||
text: $t('preferences.tabbar.contextMenu.closeAll'),
|
||||
},
|
||||
];
|
||||
return menus;
|
||||
};
|
||||
|
||||
/**
|
||||
* 取消固定标签页
|
||||
*/
|
||||
const handleUnpinTab = async (tab: TabItem) => {
|
||||
await coreTabbarStore.unpinTab(tab);
|
||||
};
|
||||
|
||||
return {
|
||||
createContextMenus,
|
||||
currentActive,
|
||||
currentTabs,
|
||||
handleClick,
|
||||
handleClose,
|
||||
handleUnpinTab,
|
||||
};
|
||||
}
|
||||
|
||||
export { updateContentScreen, useTabs };
|
Reference in New Issue
Block a user