feat(projects): 迁移多页签

This commit is contained in:
Soybean 2022-01-20 00:56:59 +08:00
parent cc290accc2
commit 28efbdbc70
26 changed files with 868 additions and 20 deletions

View File

@ -75,18 +75,18 @@ const routes: AuthRoute.Route[] = [
}
];
const routeHome: AuthRoute.RoutePath = '/dashboard/analysis';
function dataMiddleware(data: AuthRoute.Route[]): ApiRoute.Route {
const routeHomeName: AuthRoute.RouteKey = 'dashboard_analysis';
function sortRoutes() {
routes.sort((next, pre) => Number(next.meta?.order) - Number(pre.meta?.order));
function sortRoutes(sorts: AuthRoute.Route[]) {
return sorts.sort((next, pre) => Number(next.meta?.order) - Number(pre.meta?.order));
}
sortRoutes();
const data: ApiRoute.Route = {
routes,
home: routeHome
return {
routes: sortRoutes(data),
home: routeHomeName
};
}
const apis: MockMethod[] = [
{
@ -96,7 +96,7 @@ const apis: MockMethod[] = [
return {
code: 200,
message: 'ok',
data
data: dataMiddleware(routes)
};
}
}

View File

@ -0,0 +1,46 @@
<template>
<div ref="bsWrap" class="h-full text-left">
<div ref="bsContent" class="inline-block" :class="{ 'h-full': !isScrollY }">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue';
import { useElementSize } from '@vueuse/core';
import BScroll from '@better-scroll/core';
import type { Options } from '@better-scroll/core';
interface Props {
/** better-scroll的配置: https://better-scroll.github.io/docs/zh-CN/guide/base-scroll-options.html */
options: Options;
}
const props = defineProps<Props>();
const bsWrap = ref<HTMLElement>();
const instance = ref<BScroll>();
const bsContent = ref<HTMLElement>();
const isScrollY = computed(() => Boolean(props.options.scrollY));
function initBetterScroll() {
if (!bsWrap.value) return;
instance.value = new BScroll(bsWrap.value, props.options);
}
// BS
const { width, height } = useElementSize(bsContent);
watch([() => width.value, () => height.value], () => {
if (instance.value) {
instance.value.refresh();
}
});
onMounted(() => {
initBetterScroll();
});
defineExpose({ instance });
</script>
<style scoped></style>

View File

@ -0,0 +1,71 @@
<template>
<div
class="relative flex-center h-30px pl-14px border-1px border-[#e5e7eb] dark:border-[#ffffff3d] rounded-2px cursor-pointer transition-colors duration-300 ease-in-out"
:class="[closable ? 'pr-6px' : 'pr-14px']"
:style="buttonStyle"
@mouseenter="setTrue"
@mouseleave="setFalse"
>
<span class="whitespace-nowrap">
<slot></slot>
</span>
<div v-if="closable" class="pl-10px">
<icon-close :is-active="isIconActive" :primary-color="primaryColor" @click="handleClose" />
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { useBoolean } from '@/hooks';
import { addColorAlpha } from '@/utils';
import IconClose from '../IconClose/index.vue';
interface Props {
/** 激活状态 */
isActive?: boolean;
/** 主题颜色 */
primaryColor?: string;
/** 是否显示关闭图标 */
closable?: boolean;
/** 暗黑模式 */
darkMode?: boolean;
}
interface Emits {
/** 点击关闭图标 */
(e: 'close'): void;
}
const props = withDefaults(defineProps<Props>(), {
isActive: false,
primaryColor: '#1890ff',
closable: true,
darkMode: false
});
const emit = defineEmits<Emits>();
const { bool: isHover, setTrue, setFalse } = useBoolean();
const isIconActive = computed(() => props.isActive || isHover.value);
const buttonStyle = computed(() => {
const style: { [key: string]: string } = {};
if (isIconActive.value) {
style.color = props.primaryColor;
style.borderColor = addColorAlpha(props.primaryColor, 0.3);
if (props.isActive) {
const alpha = props.darkMode ? 0.15 : 0.1;
style.backgroundColor = addColorAlpha(props.primaryColor, alpha);
}
}
return style;
});
function handleClose(e: MouseEvent) {
e.stopPropagation();
emit('close');
}
</script>
<style scoped></style>

View File

@ -0,0 +1,79 @@
<template>
<svg>
<defs>
<symbol id="geometry-left" viewBox="0 0 214 36">
<path d="M17 0h197v36H0v-2c4.5 0 9-3.5 9-8V8c0-4.5 3.5-8 8-8z"></path>
</symbol>
<symbol id="geometry-right" viewBox="0 0 214 36">
<use xlink:href="#geometry-left"></use>
</symbol>
<clipPath>
<rect width="100%" height="100%" x="0"></rect>
</clipPath>
</defs>
<svg width="52%" height="100%">
<use
xlink:href="#geometry-left"
width="214"
height="36"
:fill="fill"
class="transition-fill duration-300 ease-in-out"
></use>
</svg>
<g transform="scale(-1, 1)">
<svg width="52%" height="100%" x="-100%" y="0">
<use
xlink:href="#geometry-right"
width="214"
height="36"
:fill="fill"
class="transition-fill duration-300 ease-in-out"
></use>
</svg>
</g>
</svg>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { mixColor } from '@/utils';
interface Props {
/** 激活状态 */
isActive?: boolean;
/** 鼠标悬浮状态 */
isHover?: boolean;
/** 主题颜色 */
primaryColor?: string;
/** 暗黑模式 */
darkMode?: boolean;
}
/** 填充的背景颜色: [默认颜色, 暗黑主题颜色] */
type FillColor = [string, string];
const props = withDefaults(defineProps<Props>(), {
isActive: false,
isHover: false,
primaryColor: '#409EFF',
darkMode: false
});
const defaultColor: FillColor = ['#fff', '#18181c'];
const hoverColor: FillColor = ['#dee1e6', '#3f3c37'];
const mixColors: FillColor = ['#ffffff', '#000000'];
const fill = computed(() => {
const index = Number(props.darkMode);
let color = defaultColor[index];
if (props.isHover) {
color = hoverColor[index];
}
if (props.isActive) {
const alpha = props.darkMode ? 0.1 : 0.15;
color = mixColor(mixColors[index], props.primaryColor, alpha);
}
return color;
});
</script>
<style scoped></style>

View File

@ -0,0 +1,3 @@
import SvgRadiusBg from './SvgRadiusBg.vue';
export { SvgRadiusBg };

View File

@ -0,0 +1,66 @@
<template>
<div
class="relative flex-y-center h-34px px-24px -mr-18px cursor-pointer"
:class="{ 'z-10': isActive, 'z-9': isHover }"
@mouseenter="setTrue"
@mouseleave="setFalse"
>
<div class="absolute-lb wh-full overflow-hidden">
<svg-radius-bg
class="wh-full"
:is-active="isActive"
:is-hover="isHover"
:dark-mode="darkMode"
:primary-color="primaryColor"
/>
</div>
<span class="relative whitespace-nowrap z-2">
<slot></slot>
</span>
<div v-if="closable" class="pl-18px">
<icon-close :is-active="isActive" :primary-color="primaryColor" @click="handleClose" />
</div>
<n-divider v-if="!isHover && !isActive" :vertical="true" class="absolute right-0 !bg-[#a4abb8] z-2" />
</div>
</template>
<script setup lang="ts">
import { NDivider } from 'naive-ui';
import { useBoolean } from '@/hooks';
import IconClose from '../IconClose/index.vue';
import { SvgRadiusBg } from './components';
interface Props {
/** 激活状态 */
isActive?: boolean;
/** 主题颜色 */
primaryColor?: string;
/** 是否显示关闭图标 */
closable?: boolean;
/** 暗黑模式 */
darkMode?: boolean;
}
interface Emits {
/** 点击关闭图标 */
(e: 'close'): void;
}
withDefaults(defineProps<Props>(), {
isActive: false,
primaryColor: '#409EFF',
closable: true,
darkMode: false,
isLast: false
});
const emit = defineEmits<Emits>();
const { bool: isHover, setTrue, setFalse } = useBoolean();
function handleClose(e: MouseEvent) {
e.stopPropagation();
emit('close');
}
</script>
<style scoped></style>

View File

@ -0,0 +1,35 @@
<template>
<div
class="relative flex-center w-18px h-18px text-14px"
:style="{ color: isActive ? primaryColor : defaultColor }"
@mouseenter="setTrue"
@mouseleave="setFalse"
>
<transition name="fade">
<icon-mdi:close-circle v-if="isHover" key="hover" class="absolute" />
<icon-mdi:close v-else key="unhover" class="absolute" />
</transition>
</div>
</template>
<script lang="ts" setup>
import { useBoolean } from '@/hooks';
interface Props {
/** 激活状态 */
isActive?: boolean;
/** 主题颜色 */
primaryColor?: string;
/** 默认颜色 */
defaultColor?: string;
}
withDefaults(defineProps<Props>(), {
isPrimary: false,
primaryColor: '#1890ff',
defaultColor: '#9ca3af'
});
const { bool: isHover, setTrue, setFalse } = useBoolean();
</script>
<style scoped></style>

View File

@ -1,4 +1,7 @@
import BetterScroll from './BetterScroll/index.vue';
import ButtonTab from './ButtonTab/index.vue';
import ChromeTab from './ChromeTab/index.vue';
import CountTo from './CountTo/index.vue';
import ImageVerify from './ImageVerify/index.vue';
export { CountTo, ImageVerify };
export { BetterScroll, ButtonTab, ChromeTab, CountTo, ImageVerify };

View File

@ -6,5 +6,7 @@ export enum EnumStorageKey {
/** 用户刷新token */
'refresh-koken' = '__REFRESH_TOKEN__',
/** 用户信息 */
'user-info' = '__USER_INFO__'
'user-info' = '__USER_INFO__',
/** 多页签路由信息 */
'tab-routes' = '__TAB_ROUTES__'
}

View File

@ -1,3 +1,5 @@
export interface ExposeLayoutMixMenu {
resetFirstDegreeMenus(): void;
import BScroll from '@better-scroll/core';
export interface ExposeBetterScroll {
instance: BScroll;
}

View File

@ -1,4 +1,5 @@
import type { VNodeChild } from 'vue';
import type { RouteLocationNormalizedLoaded } from 'vue-router';
import type { DropdownOption } from 'naive-ui';
/** 菜单项配置 */
@ -20,3 +21,6 @@ export type GlobalBreadcrumb = DropdownOption & {
hasChildren: boolean;
children?: GlobalBreadcrumb[];
};
/** 多页签Tab的路由 */
export type GlobalTabRoute = Pick<RouteLocationNormalizedLoaded, 'name' | 'path' | 'meta'>;

View File

@ -0,0 +1,23 @@
<template>
<hover-container class="w-64px h-full" tooltip-content="重新加载" placement="bottom-end" @click="handleRefresh">
<icon-mdi-refresh class="text-18px" :class="{ 'animate-spin': loading }" />
</hover-container>
</template>
<script setup lang="ts">
import { HoverContainer } from '@/components';
import { useAppStore } from '@/store';
import { useLoading } from '@/hooks';
const app = useAppStore();
const { loading, startLoading, endLoading } = useLoading();
function handleRefresh() {
startLoading();
app.reloadPage();
setTimeout(() => {
endLoading();
}, 1000);
}
</script>
<style scoped></style>

View File

@ -0,0 +1,135 @@
<template>
<n-dropdown
:show="dropdownVisible"
:options="options"
placement="bottom-start"
:x="x"
:y="y"
@clickoutside="hide"
@select="handleDropdown"
/>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { NDropdown } from 'naive-ui';
import type { DropdownOption } from 'naive-ui';
import { useAppStore, useTabStore } from '@/store';
import { iconifyRender } from '@/utils';
interface Props {
/** 右键菜单可见性 */
visible?: boolean;
/** 当前路由路径 */
currentPath?: string;
/** 鼠标x坐标 */
x: number;
/** 鼠标y坐标 */
y: number;
}
interface Emits {
(e: 'update:visible', visible: boolean): void;
}
type DropdownKey = 'reload-current' | 'close-current' | 'close-other' | 'close-left' | 'close-right' | 'close-all';
type Option = DropdownOption & {
key: DropdownKey;
};
const props = withDefaults(defineProps<Props>(), {
visible: false,
currentPath: ''
});
const emit = defineEmits<Emits>();
const app = useAppStore();
const tab = useTabStore();
const dropdownVisible = computed({
get() {
return props.visible;
},
set(visible: boolean) {
emit('update:visible', visible);
}
});
function hide() {
dropdownVisible.value = false;
}
const options = computed<Option[]>(() => [
{
label: '重新加载',
key: 'reload-current',
disabled: props.currentPath !== tab.activeTab,
icon: iconifyRender('ant-design:reload-outlined')
},
{
label: '关闭',
key: 'close-current',
disabled: props.currentPath === tab.homeTab.path,
icon: iconifyRender('ant-design:close-outlined')
},
{
label: '关闭其他',
key: 'close-other',
icon: iconifyRender('ant-design:column-width-outlined')
},
{
label: '关闭左侧',
key: 'close-left',
icon: iconifyRender('mdi:format-horizontal-align-left')
},
{
label: '关闭右侧',
key: 'close-right',
icon: iconifyRender('mdi:format-horizontal-align-right')
}
]);
const actionMap = new Map<DropdownKey, () => void>([
[
'reload-current',
() => {
app.reloadPage();
}
],
[
'close-current',
() => {
tab.removeTab(props.currentPath);
}
],
[
'close-other',
() => {
tab.clearTab([props.currentPath]);
}
],
[
'close-left',
() => {
tab.clearLeftTab(props.currentPath);
}
],
[
'close-right',
() => {
tab.clearRightTab(props.currentPath);
}
]
]);
function handleDropdown(optionKey: string) {
const key = optionKey as DropdownKey;
const actionFunc = actionMap.get(key);
if (actionFunc) {
actionFunc();
}
hide();
}
</script>
<style scoped></style>

View File

@ -0,0 +1,3 @@
import ContextMenu from './ContextMenu.vue';
export { ContextMenu };

View File

@ -0,0 +1,102 @@
<template>
<div ref="tabRef" class="flex items-end h-full" :class="[isChromeMode ? 'flex items-end' : 'flex-y-center']">
<component
:is="activeComponent"
v-for="(item, index) in tab.tabs"
:key="item.path"
:is-active="tab.activeTab === item.path"
:primary-color="theme.themeColor"
:closable="item.path !== tab.homeTab.path"
:dark-mode="theme.darkMode"
:class="{ '!mr-0': isChromeMode && index === tab.tabs.length - 1, 'mr-10px': !isChromeMode }"
@click="tab.handleClickTab(item.path)"
@close="tab.removeTab(item.path)"
@contextmenu="handleContextMenu($event, item.path)"
>
{{ item.meta.title }}
</component>
</div>
<context-menu
v-model:visible="dropdown.visible"
:current-path="dropdown.currentPath"
:x="dropdown.x"
:y="dropdown.y"
/>
</template>
<script setup lang="ts">
import { ref, reactive, computed, nextTick, watch } from 'vue';
import { useEventListener } from '@vueuse/core';
import { ChromeTab, ButtonTab } from '@/components';
import { useThemeStore, useTabStore } from '@/store';
import { setTabRoutes } from '@/utils';
import { ContextMenu } from './components';
interface Emits {
(e: 'scroll', clientX: number): void;
}
const emit = defineEmits<Emits>();
const theme = useThemeStore();
const tab = useTabStore();
const isChromeMode = computed(() => theme.tab.mode === 'chrome');
const activeComponent = computed(() => (isChromeMode.value ? ChromeTab : ButtonTab));
// tabclientX
const tabRef = ref<HTMLElement>();
async function getActiveTabClientX() {
await nextTick();
if (tabRef.value) {
const activeTabElement = tabRef.value.children[tab.activeTabIndex];
const { x, width } = activeTabElement.getBoundingClientRect();
const clientX = x + width / 2;
setTimeout(() => {
emit('scroll', clientX);
}, 50);
}
}
const dropdown = reactive({
visible: false,
x: 0,
y: 0,
currentPath: ''
});
function showDropdown() {
dropdown.visible = true;
}
function hideDropdown() {
dropdown.visible = false;
}
function setDropdown(x: number, y: number, currentPath: string) {
Object.assign(dropdown, { x, y, currentPath });
}
/** 点击右键菜单 */
async function handleContextMenu(e: MouseEvent, path: string) {
e.preventDefault();
const { clientX, clientY } = e;
hideDropdown();
setDropdown(clientX, clientY, path);
await nextTick();
showDropdown();
}
watch(
() => tab.activeTabIndex,
() => {
getActiveTabClientX();
},
{
immediate: true
}
);
/** 页面离开时缓存多页签数据 */
useEventListener(window, 'beforeunload', () => {
setTabRoutes(tab.tabs);
});
</script>
<style scoped></style>

View File

@ -0,0 +1,4 @@
import TabDetail from './TabDetail/index.vue';
import ReloadButton from './ReloadButton/index.vue';
export { TabDetail, ReloadButton };

View File

@ -1,9 +1,60 @@
<template>
<dark-mode-container class="global-tab flex-y-center h-full"></dark-mode-container>
<dark-mode-container class="global-tab flex-y-center w-full pl-16px" :style="{ height: theme.tab.height + 'px' }">
<div ref="bsWrapper" class="flex-1-hidden h-full">
<better-scroll ref="bsScroll" :options="{ scrollX: true, scrollY: false, click: isMobile }">
<tab-detail @scroll="handleScroll" />
</better-scroll>
</div>
<reload-button />
</dark-mode-container>
</template>
<script setup lang="ts">
import { DarkModeContainer } from '@/components';
import { ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useElementBounding } from '@vueuse/core';
import { DarkModeContainer, BetterScroll } from '@/components';
import { useThemeStore, useTabStore } from '@/store';
import { useIsMobile } from '@/composables';
import type { ExposeBetterScroll } from '@/interface';
import { TabDetail, ReloadButton } from './components';
const route = useRoute();
const theme = useThemeStore();
const tab = useTabStore();
const bsWrapper = ref<HTMLElement>();
const { width: bsWrapperWidth, left: bsWrapperLeft } = useElementBounding(bsWrapper);
const bsScroll = ref<ExposeBetterScroll>();
const isMobile = useIsMobile();
function handleScroll(clientX: number) {
const currentX = clientX - bsWrapperLeft.value;
const deltaX = currentX - bsWrapperWidth.value / 2;
if (bsScroll.value) {
const { maxScrollX, x: leftX } = bsScroll.value.instance;
const rightX = maxScrollX - leftX;
const update = deltaX > 0 ? Math.max(-deltaX, rightX) : Math.min(-deltaX, -leftX);
bsScroll.value?.instance.scrollBy(update, 0, 300);
}
}
function init() {
tab.iniTabStore(route);
}
watch(
() => route.path,
() => {
tab.addTab(route);
tab.setActiveTab(route.path);
}
);
//
init();
</script>
<style scoped>
.global-tab {

View File

@ -1,4 +1,5 @@
export * from './app';
export * from './theme';
export * from './auth';
export * from './tab';
export * from './route';

View File

@ -3,10 +3,13 @@ import { defineStore } from 'pinia';
import { fetchUserRoutes } from '@/service';
import { transformAuthRouteToMenu, transformAuthRoutesToVueRoutes } from '@/utils';
import type { GlobalMenuOption } from '@/interface';
import { useTabStore } from '../tab';
interface RouteState {
/** 是否添加过动态路由 */
isAddedDynamicRoute: boolean;
/** 路由首页name */
routeHomeName: AuthRoute.RouteKey;
/** 菜单 */
menus: GlobalMenuOption[];
}
@ -14,6 +17,7 @@ interface RouteState {
export const useRouteStore = defineStore('route-store', {
state: (): RouteState => ({
isAddedDynamicRoute: false,
routeHomeName: 'dashboard_analysis',
menus: []
}),
actions: {
@ -22,8 +26,11 @@ export const useRouteStore = defineStore('route-store', {
* @param router -
*/
async initDynamicRoute(router: Router) {
const { initHomeTab } = useTabStore();
const { data } = await fetchUserRoutes();
if (data) {
this.routeHomeName = data.home;
this.menus = transformAuthRouteToMenu(data.routes);
const vueRoutes = transformAuthRoutesToVueRoutes(data.routes);
@ -31,6 +38,7 @@ export const useRouteStore = defineStore('route-store', {
router.addRoute(route);
});
initHomeTab(data.home, router);
this.isAddedDynamicRoute = true;
}
}

View File

@ -0,0 +1,33 @@
import type { RouteRecordNormalized, RouteLocationNormalizedLoaded } from 'vue-router';
import type { GlobalTabRoute } from '@/interface';
/**
* vue路由获取tab路由
* @param route
*/
export function getTabRouteByVueRoute(route: RouteRecordNormalized | RouteLocationNormalizedLoaded) {
const tabRoute: GlobalTabRoute = {
name: route.name,
path: route.path,
meta: route.meta
};
return tabRoute;
}
/**
*
* @param tabs -
* @param path -
*/
export function getIndexInTabRoutes(tabs: GlobalTabRoute[], path: string) {
return tabs.findIndex(tab => tab.path === path);
}
/**
*
* @param tabs -
* @param path -
*/
export function isInTabRoutes(tabs: GlobalTabRoute[], path: string) {
return getIndexInTabRoutes(tabs, path) > -1;
}

View File

@ -0,0 +1,153 @@
import type { Router, RouteLocationNormalizedLoaded } from 'vue-router';
import { defineStore } from 'pinia';
import { useRouterPush } from '@/composables';
import { getTabRoutes } from '@/utils';
import type { GlobalTabRoute } from '@/interface';
import { useThemeStore } from '../theme';
import { getTabRouteByVueRoute, isInTabRoutes, getIndexInTabRoutes } from './helpers';
interface TabState {
/** 多页签数据 */
tabs: GlobalTabRoute[];
/** 多页签首页 */
homeTab: GlobalTabRoute;
/** 当前激活状态的页签(路由path) */
activeTab: string;
}
export const useTabStore = defineStore('tab-store', {
state: (): TabState => ({
tabs: [],
homeTab: {
name: 'root',
path: '/',
meta: {
title: 'root'
}
},
activeTab: ''
}),
getters: {
/** 当前激活状态的页签索引 */
activeTabIndex(state) {
const { tabs, activeTab } = state;
return tabs.findIndex(tab => tab.path === activeTab);
}
},
actions: {
/**
*
* @param path - path
*/
setActiveTab(path: string) {
this.activeTab = path;
},
/**
*
* @param routeHomeName - name
* @param router -
*/
initHomeTab(routeHomeName: string, router: Router) {
const routes = router.getRoutes();
const findHome = routes.find(item => item.name === routeHomeName);
if (findHome) {
this.homeTab = getTabRouteByVueRoute(findHome);
}
},
/**
*
* @param route -
*/
addTab(route: RouteLocationNormalizedLoaded) {
if (!isInTabRoutes(this.tabs, route.path)) {
this.tabs.push(getTabRouteByVueRoute(route));
}
},
/**
*
* @param path - path
*/
removeTab(path: string) {
const { routerPush } = useRouterPush(false);
const isActive = this.activeTab === path;
const updateTabs = this.tabs.filter(tab => tab.path !== path);
this.tabs = updateTabs;
if (isActive && updateTabs.length) {
const activePath = updateTabs[updateTabs.length - 1].path;
this.setActiveTab(activePath);
routerPush(activePath);
}
},
/**
* ()
* @param excludes - path
*/
clearTab(excludes: string[] = []) {
const { routerPush } = useRouterPush(false);
const homePath = this.homeTab.path;
const remain = [homePath, ...excludes];
const hasActive = remain.includes(this.activeTab);
const updateTabs = this.tabs.filter(tab => remain.includes(tab.path));
this.tabs = updateTabs;
if (!hasActive && updateTabs.length) {
const activePath = updateTabs[updateTabs.length - 1].path;
this.setActiveTab(activePath);
routerPush(activePath);
}
},
/**
*
* @param path - path
*/
clearLeftTab(path: string) {
const index = getIndexInTabRoutes(this.tabs, path);
if (index > -1) {
const excludes = this.tabs.slice(index).map(item => item.path);
this.clearTab(excludes);
}
},
/**
*
* @param path - path
*/
clearRightTab(path: string) {
const index = getIndexInTabRoutes(this.tabs, path);
if (index > -1) {
const excludes = this.tabs.slice(0, index + 1).map(item => item.path);
this.clearTab(excludes);
}
},
/**
* tab
* @param path - path
*/
handleClickTab(path: string) {
const { routerPush } = useRouterPush(false);
const isActive = this.activeTab === path;
if (!isActive) {
this.setActiveTab(path);
routerPush(path);
}
},
/** 初始化Tab状态 */
iniTabStore(currentRoute: RouteLocationNormalizedLoaded) {
const theme = useThemeStore();
const isHome = currentRoute.path === this.homeTab.path;
const tabs: GlobalTabRoute[] = theme.tab.isCache ? getTabRoutes() : [];
const hasHome = isInTabRoutes(tabs, this.homeTab.path);
const hasCurrent = isInTabRoutes(tabs, currentRoute.path);
if (!hasHome) {
tabs.unshift(this.homeTab);
}
if (!isHome && !hasCurrent) {
tabs.push(getTabRouteByVueRoute(currentRoute));
}
this.tabs = tabs;
this.setActiveTab(currentRoute.path);
}
}
});

View File

@ -5,6 +5,6 @@ declare namespace ApiRoute {
/** 动态路由 */
routes: AuthRoute.Route[];
/** 路由首页对应的key */
home: AuthRoute.RoutePath;
home: AuthRoute.RouteKey;
}
}

View File

@ -1,4 +1,5 @@
export * from './helpers';
export * from './menu';
export * from './breadcrumb';
export * from './tab';
export * from './regexp';

23
src/utils/router/tab.ts Normal file
View File

@ -0,0 +1,23 @@
import { EnumStorageKey } from '@/enum';
import type { GlobalTabRoute } from '@/interface';
import { setLocal, getLocal } from '../storage';
/** 缓存多页签数据 */
export function setTabRoutes(data: GlobalTabRoute[]) {
setLocal(EnumStorageKey['tab-routes'], data);
}
/** 获取缓存的多页签数据 */
export function getTabRoutes() {
const routes: GlobalTabRoute[] = [];
const data = getLocal<GlobalTabRoute[]>(EnumStorageKey['tab-routes']);
if (data) {
routes.push(...data);
}
return routes;
}
/** 清空多页签数据 */
export function clearTabRoutes() {
setTabRoutes([]);
}

View File

@ -126,4 +126,4 @@ onMounted(() => {
renderPieChart();
});
</script>
<style scoped></style>
<style></style>

View File

@ -18,7 +18,7 @@ export default defineConfig(configEnv => {
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "./src/styles/scss/global.scss" as *;`
additionalData: `@use "${fileURLToPath(new URL('./src', import.meta.url))}/styles/scss/global.scss" as *;`
}
}
},