refactor(project): business changed its name to effects, and universal-ui changed its name to common-ui

This commit is contained in:
vben
2024-07-13 17:25:15 +08:00
parent 5e0b01c725
commit 7eff46d80c
186 changed files with 110 additions and 107 deletions

View 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>

View File

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

View File

@@ -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 };

View 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>

View File

@@ -0,0 +1 @@
export { default as Copyright } from './copyright.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,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>

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,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>

View 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>

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,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>

View 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>

View 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 };

View 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 };

View 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 };

View File

@@ -0,0 +1,3 @@
export { default as LayoutTabbar } from './tabbar.vue';
export { default as LayoutTabbarTools } from './tabbar-tools.vue';
export * from './use-tabs';

View 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>

View 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>

View 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 };